KiCad PCB EDA Suite
Loading...
Searching...
No Matches
git_init_handler.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.TXT for contributors.
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 3
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 */
19
20#include "git_init_handler.h"
21#include "git_backend.h"
24#include <paths.h>
25#include <trace_helpers.h>
26
27#include <git2.h>
28
29#include <set>
30#include <string>
31
32#include <wx/ffile.h>
33#include <wx/filename.h>
34#include <wx/log.h>
35#include <wx/tokenzr.h>
36
37
38// Seed the project .gitignore with KiCad-generated paths that should never be
39// committed (local history, backups, autosaves, lock files, footprint cache,
40// project-local settings which may hold remote URLs and usernames).
41// Append-only: existing entries are preserved.
42static void ensureProjectGitignore( const wxString& aProjectPath )
43{
44 static const wxString kicadEntries[] = {
45 wxS( ".history/" ), wxS( "*-backups/" ), wxS( "_autosave-*" ),
46 wxS( "fp-info-cache" ), wxS( "~*.lck" ), wxS( "*.kicad_prl" ),
47 };
48
49 wxFileName ignoreFile( aProjectPath, wxS( ".gitignore" ) );
50 std::set<wxString> existing;
51 bool fileExists = ignoreFile.FileExists();
52 bool hasTrailingNewline = true;
53
54 if( fileExists )
55 {
56 wxFFile in( ignoreFile.GetFullPath(), wxT( "r" ) );
57 wxString contents;
58
59 if( !in.IsOpened() || !in.ReadAll( &contents ) )
60 return;
61
62 hasTrailingNewline = contents.empty() || contents.EndsWith( wxS( "\n" ) );
63
64 wxStringTokenizer tok( contents, wxS( "\n" ) );
65
66 while( tok.HasMoreTokens() )
67 {
68 wxString line = tok.GetNextToken();
69 line.Trim().Trim( false );
70
71 if( !line.empty() && !line.StartsWith( wxS( "#" ) ) )
72 existing.insert( line );
73 }
74 }
75
76 wxString missing;
77
78 for( const wxString& entry : kicadEntries )
79 {
80 if( existing.find( entry ) == existing.end() )
81 missing += entry + wxS( "\n" );
82 }
83
84 if( missing.empty() )
85 return;
86
87 wxFFile out( ignoreFile.GetFullPath(), wxT( "a" ) );
88
89 if( !out.IsOpened() )
90 return;
91
92 if( !fileExists )
93 out.Write( wxS( "# KiCad-generated files and directories.\n" ) );
94 else if( !hasTrailingNewline )
95 out.Write( wxS( "\n" ) );
96
97 out.Write( missing );
98}
99
100
101// Add .gitattributes entries that route KiCad design files through our merge
102// drivers. The drivers themselves are registered with libgit2 at kiface init
103// (KIGIT_PCB_MERGE::Apply etc.); the attribute file tells git which paths to
104// route to which driver.
105//
106// Append-only: existing user entries are preserved. If a path glob is already
107// present with any merge=... value, we leave it alone — never overwrite a
108// user's choice.
109static void ensureProjectGitattributes( const wxString& aProjectPath )
110{
111 static const std::pair<wxString, wxString> kicadEntries[] = {
112 { wxS( "*.kicad_pcb" ), wxS( "merge=kicad-pcb" ) },
113 { wxS( "*.kicad_sch" ), wxS( "merge=kicad-sch" ) },
114 { wxS( "*.kicad_sym" ), wxS( "merge=kicad-sym-lib" ) },
115 { wxS( "*.kicad_mod" ), wxS( "merge=kicad-fp" ) },
116 };
117
118 wxFileName attrFile( aProjectPath, wxS( ".gitattributes" ) );
119
120 // Map glob -> existing merge driver, if any.
121 std::set<wxString> existingPatterns;
122 bool fileExists = attrFile.FileExists();
123 bool hasTrailingNewline = true;
124
125 if( fileExists )
126 {
127 wxFFile in( attrFile.GetFullPath(), wxT( "r" ) );
128 wxString contents;
129
130 if( !in.IsOpened() || !in.ReadAll( &contents ) )
131 return;
132
133 hasTrailingNewline = contents.empty() || contents.EndsWith( wxS( "\n" ) );
134
135 wxStringTokenizer tok( contents, wxS( "\n" ) );
136
137 while( tok.HasMoreTokens() )
138 {
139 wxString line = tok.GetNextToken();
140 line.Trim().Trim( false );
141
142 if( line.empty() || line.StartsWith( wxS( "#" ) ) )
143 continue;
144
145 // First whitespace-delimited token is the pattern.
146 wxString pattern = line.BeforeFirst( ' ' );
147 pattern = pattern.BeforeFirst( '\t' );
148
149 if( !pattern.empty() )
150 existingPatterns.insert( pattern );
151 }
152 }
153
154 wxString missing;
155
156 for( const auto& [glob, attr] : kicadEntries )
157 {
158 if( existingPatterns.find( glob ) == existingPatterns.end() )
159 missing += glob + wxS( " " ) + attr + wxS( "\n" );
160 }
161
162 if( missing.empty() )
163 return;
164
165 wxFFile out( attrFile.GetFullPath(), wxT( "a" ) );
166
167 if( !out.IsOpened() )
168 return;
169
170 if( !fileExists )
171 out.Write( wxS( "# KiCad design files use custom merge drivers.\n" ) );
172 else if( !hasTrailingNewline )
173 out.Write( wxS( "\n" ) );
174
175 out.Write( missing );
176}
177
178
179// Resolve the absolute path to the kicad-cli binary alongside the running
180// process. Used as the driver / mergetool command so the per-repo git config
181// doesn't depend on PATH (which differs under installers, portable builds,
182// and per-user PATH overrides).
183static wxString resolveKicadCliPath()
184{
185 return PATHS::ResolveSiblingExecutable( wxT( "kicad-cli" ) );
186}
187
188
189// Set a string config key, but only if not already present so the user's
190// custom config is never stomped. Returns true if anything was written.
191static bool setConfigIfMissing( git_config* aConfig, const char* aKey, const wxString& aValue )
192{
193 git_config_entry* entry = nullptr;
194 int result = git_config_get_entry( &entry, aConfig, aKey );
195 KIGIT::GitConfigEntryPtr entryPtr( entry );
196
197 if( result == 0 && entry )
198 {
199 wxLogTrace( traceGit, "Config key '%s' already set; leaving alone", aKey );
200 return false;
201 }
202
203 std::string val( aValue.ToUTF8() );
204
205 if( git_config_set_string( aConfig, aKey, val.c_str() ) != 0 )
206 {
207 wxLogTrace( traceGit, "git_config_set_string('%s') failed: %s",
209 return false;
210 }
211
212 return true;
213}
214
215
216// Configure the merge drivers and mergetool entries pointing at kicad-cli so
217// command-line `git merge` / `git mergetool` invocations route KiCad design
218// files through us. Append-only at the config level too: any existing
219// merge.kicad-* or mergetool.kicad config entry is preserved. `merge.tool`
220// is only set to "kicad" when the user hasn't already picked a tool.
221static void ensureMergeDriverConfig( const wxString& aProjectPath )
222{
223 const wxString cliPath = resolveKicadCliPath();
224
225 if( cliPath.IsEmpty() )
226 {
227 wxLogTrace( traceGit,
228 "kicad-cli not found next to running binary; skipping merge driver "
229 "config (libgit2 in-process driver still works)." );
230 return;
231 }
232
233 git_repository* repo = nullptr;
234
235 if( git_repository_open( &repo, aProjectPath.ToUTF8() ) != 0 )
236 {
237 wxLogTrace( traceGit, "git_repository_open failed: %s",
239 return;
240 }
241
242 KIGIT::GitRepositoryPtr repoPtr( repo );
243
244 git_config* config = nullptr;
245
246 if( git_repository_config( &config, repo ) != 0 )
247 {
248 wxLogTrace( traceGit, "git_repository_config failed: %s",
250 return;
251 }
252
253 KIGIT::GitConfigPtr configPtr( config );
254
255 // Per-format merge driver entries. Command-line `git merge` invokes the
256 // configured driver command for paths tagged `merge=kicad-*` in
257 // .gitattributes; the hidden `kicad-cli git-mergedriver` hook performs a
258 // structural 3-way merge (NOT a text merge), writing the merged file and
259 // returning 0 when clean. On unresolved conflicts it returns non-zero, so
260 // git leaves the path unmerged and the user resolves it interactively with
261 // `git mergetool` (configured below). The document kind is passed via
262 // --kind because git hands the driver extension-less temp paths.
263 // (KiCad's own in-app git integration uses the IN-PROCESS libgit2 driver
264 // registered via git_merge_driver_register instead of these commands.)
265 struct DRIVER_ENTRY
266 {
267 const char* nameKey;
268 const char* driverKey;
269 const char* humanName;
270 const char* cliCommand;
271 };
272
273 static const DRIVER_ENTRY drivers[] = {
274 { "merge.kicad-pcb.name", "merge.kicad-pcb.driver", "KiCad PCB merge driver",
275 "git-mergedriver --kind pcb \"%O\" \"%A\" \"%B\" --output \"%A\"" },
276 { "merge.kicad-sch.name", "merge.kicad-sch.driver", "KiCad schematic merge driver",
277 "git-mergedriver --kind sch \"%O\" \"%A\" \"%B\" --output \"%A\"" },
278 { "merge.kicad-sym-lib.name", "merge.kicad-sym-lib.driver",
279 "KiCad symbol library merge driver",
280 "git-mergedriver --kind sym \"%O\" \"%A\" \"%B\" --output \"%A\"" },
281 { "merge.kicad-fp.name", "merge.kicad-fp.driver", "KiCad footprint merge driver",
282 "git-mergedriver --kind fp \"%O\" \"%A\" \"%B\" --output \"%A\"" }
283 };
284
285 for( const DRIVER_ENTRY& d : drivers )
286 {
287 setConfigIfMissing( config, d.nameKey, wxString::FromUTF8( d.humanName ) );
288
289 wxString cmd = wxT( "\"" ) + cliPath + wxT( "\" " ) + wxString::FromUTF8( d.cliCommand );
290 setConfigIfMissing( config, d.driverKey, cmd );
291 }
292
293 // `git mergetool --tool=kicad` entry. Variables expanded by git:
294 // $BASE, $LOCAL, $REMOTE — ancestor / ours / theirs scratch files
295 // $MERGED — path the merged blob is written back to
296 wxString mergetoolCmd = wxT( "\"" ) + cliPath + wxT( "\" mergetool "
297 "\"$BASE\" \"$LOCAL\" \"$REMOTE\" "
298 "--output \"$MERGED\"" );
299 setConfigIfMissing( config, "mergetool.kicad.cmd", mergetoolCmd );
300 setConfigIfMissing( config, "mergetool.kicad.trustExitCode", wxT( "true" ) );
301
302 // merge.tool — only opt in when the user hasn't already picked a tool,
303 // so existing meld/kdiff3/etc. setups stay untouched.
304 setConfigIfMissing( config, "merge.tool", wxT( "kicad" ) );
305}
306
307
310
311
314
315
316bool GIT_INIT_HANDLER::IsRepository( const wxString& aPath )
317{
318 return GetGitBackend()->IsRepository( this, aPath );
319}
320
321
323{
325
328
329 return result;
330}
331
332
333void ApplyKicadGitConventions( const wxString& aProjectPath )
334{
335 ensureProjectGitignore( aProjectPath );
336 ensureProjectGitattributes( aProjectPath );
337 ensureMergeDriverConfig( aProjectPath );
338}
339
340
342{
343 return GetGitBackend()->SetupRemote( this, aConfig );
344}
345
346
347void GIT_INIT_HANDLER::UpdateProgress( int aCurrent, int aTotal, const wxString& aMessage )
348{
349 ReportProgress( aCurrent, aTotal, aMessage );
350}
virtual InitResult InitializeRepository(GIT_INIT_HANDLER *aHandler, const wxString &aPath)=0
virtual bool IsRepository(GIT_INIT_HANDLER *aHandler, const wxString &aPath)=0
virtual bool SetupRemote(GIT_INIT_HANDLER *aHandler, const RemoteConfig &aConfig)=0
bool IsRepository(const wxString &aPath)
Check if a directory is already a git repository.
void UpdateProgress(int aCurrent, int aTotal, const wxString &aMessage) override
GIT_INIT_HANDLER(KIGIT_COMMON *aCommon)
InitResult InitializeRepository(const wxString &aPath)
Initialize a new git repository in the specified directory.
bool SetupRemote(const RemoteConfig &aConfig)
Set up a remote for the repository.
void ReportProgress(int aCurrent, int aTotal, const wxString &aMessage)
static wxString GetLastGitError()
KIGIT_REPO_MIXIN(KIGIT_COMMON *aCommon)
static wxString ResolveSiblingExecutable(const wxString &aBaseName)
Resolve a sibling executable alongside the running process.
Definition paths.cpp:717
GIT_BACKEND * GetGitBackend()
static void ensureProjectGitignore(const wxString &aProjectPath)
static wxString resolveKicadCliPath()
static void ensureProjectGitattributes(const wxString &aProjectPath)
void ApplyKicadGitConventions(const wxString &aProjectPath)
Apply KiCad's standard repo conventions to a project directory:
static void ensureMergeDriverConfig(const wxString &aProjectPath)
static bool setConfigIfMissing(git_config *aConfig, const char *aKey, const wxString &aValue)
InitResult
APIEXPORT void ApplyKicadGitConventions(const wxString &aProjectPath)
Apply KiCad's standard repo conventions to a project directory:
const wxChar *const traceGit
Flag to enable Git debugging output.
static CLI::MERGETOOL_COMMAND mergetoolCmd
std::unique_ptr< git_repository, decltype([](git_repository *aRepo) { git_repository_free(aRepo); })> GitRepositoryPtr
A unique pointer for git_repository objects with automatic cleanup.
std::unique_ptr< git_config, decltype([](git_config *aConfig) { git_config_free(aConfig); })> GitConfigPtr
A unique pointer for git_config objects with automatic cleanup.
std::unique_ptr< git_config_entry, decltype([](git_config_entry *aEntry) { git_config_entry_free(aEntry); })> GitConfigEntryPtr
A unique pointer for git_config_entry objects with automatic cleanup.
wxString result
Test unit parsing edge cases and error handling.
wxLogTrace helper definitions.