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"
25#include <paths.h>
26#include <trace_helpers.h>
27
28#include <git2.h>
29
30#include <set>
31#include <string>
32
33#include <wx/ffile.h>
34#include <wx/file.h>
35#include <wx/filename.h>
36#include <wx/log.h>
37#include <wx/tokenzr.h>
38
39
40// Seed a new .gitignore with the same generated paths the change list hides.
41// An existing file belongs to the team and is left alone.
42static void ensureProjectGitignore( const wxString& aProjectPath )
43{
44 wxFileName ignoreFile( aProjectPath, wxS( ".gitignore" ) );
45
46 if( ignoreFile.FileExists() )
47 return;
48
49 // Exclusive create, so a file that appears between the test above and here is left alone.
50 wxFile out;
51
52 if( !out.Create( ignoreFile.GetFullPath(), false ) )
53 return;
54
55 wxString contents = wxS( "# KiCad-generated files and directories.\n" );
56
57 for( const wxString& entry : KIGIT::PROJECT_GIT_UTILS::TransientGitIgnorePatterns() )
58 contents += entry + wxS( "\n" );
59
60 // A half written file would be taken for the team's own on every later init.
61 if( !out.Write( contents ) || !out.Close() )
62 {
63 out.Close();
64 wxRemoveFile( ignoreFile.GetFullPath() );
65 }
66}
67
68
69// Add .gitattributes entries that route KiCad design files through our merge
70// drivers. The drivers themselves are registered with libgit2 at kiface init
71// (KIGIT_PCB_MERGE::Apply etc.); the attribute file tells git which paths to
72// route to which driver.
73//
74// Append-only: existing user entries are preserved. If a path glob is already
75// present with any merge=... value, we leave it alone — never overwrite a
76// user's choice.
77static void ensureProjectGitattributes( const wxString& aProjectPath )
78{
79 static const std::pair<wxString, wxString> kicadEntries[] = {
80 { wxS( "*.kicad_pcb" ), wxS( "merge=kicad-pcb" ) },
81 { wxS( "*.kicad_sch" ), wxS( "merge=kicad-sch" ) },
82 { wxS( "*.kicad_sym" ), wxS( "merge=kicad-sym-lib" ) },
83 { wxS( "*.kicad_mod" ), wxS( "merge=kicad-fp" ) },
84 };
85
86 wxFileName attrFile( aProjectPath, wxS( ".gitattributes" ) );
87
88 // Map glob -> existing merge driver, if any.
89 std::set<wxString> existingPatterns;
90 bool fileExists = attrFile.FileExists();
91 bool hasTrailingNewline = true;
92
93 if( fileExists )
94 {
95 wxFFile in( attrFile.GetFullPath(), wxT( "r" ) );
96 wxString contents;
97
98 if( !in.IsOpened() || !in.ReadAll( &contents ) )
99 return;
100
101 hasTrailingNewline = contents.empty() || contents.EndsWith( wxS( "\n" ) );
102
103 wxStringTokenizer tok( contents, wxS( "\n" ) );
104
105 while( tok.HasMoreTokens() )
106 {
107 wxString line = tok.GetNextToken();
108 line.Trim().Trim( false );
109
110 if( line.empty() || line.StartsWith( wxS( "#" ) ) )
111 continue;
112
113 // First whitespace-delimited token is the pattern.
114 wxString pattern = line.BeforeFirst( ' ' );
115 pattern = pattern.BeforeFirst( '\t' );
116
117 if( !pattern.empty() )
118 existingPatterns.insert( pattern );
119 }
120 }
121
122 wxString missing;
123
124 for( const auto& [glob, attr] : kicadEntries )
125 {
126 if( existingPatterns.find( glob ) == existingPatterns.end() )
127 missing += glob + wxS( " " ) + attr + wxS( "\n" );
128 }
129
130 if( missing.empty() )
131 return;
132
133 wxFFile out( attrFile.GetFullPath(), wxT( "a" ) );
134
135 if( !out.IsOpened() )
136 return;
137
138 if( !fileExists )
139 out.Write( wxS( "# KiCad design files use custom merge drivers.\n" ) );
140 else if( !hasTrailingNewline )
141 out.Write( wxS( "\n" ) );
142
143 out.Write( missing );
144}
145
146
147// Resolve the absolute path to the kicad-cli binary alongside the running
148// process. Used as the driver / mergetool command so the per-repo git config
149// doesn't depend on PATH (which differs under installers, portable builds,
150// and per-user PATH overrides).
151static wxString resolveKicadCliPath()
152{
153 return PATHS::ResolveSiblingExecutable( wxT( "kicad-cli" ) );
154}
155
156
157// Set a string config key, but only if not already present so the user's
158// custom config is never stomped. Returns true if anything was written.
159static bool setConfigIfMissing( git_config* aConfig, const char* aKey, const wxString& aValue )
160{
161 git_config_entry* entry = nullptr;
162 int result = git_config_get_entry( &entry, aConfig, aKey );
163 KIGIT::GitConfigEntryPtr entryPtr( entry );
164
165 if( result == 0 && entry )
166 {
167 wxLogTrace( traceGit, "Config key '%s' already set; leaving alone", aKey );
168 return false;
169 }
170
171 std::string val( aValue.ToUTF8() );
172
173 if( git_config_set_string( aConfig, aKey, val.c_str() ) != 0 )
174 {
175 wxLogTrace( traceGit, "git_config_set_string('%s') failed: %s",
177 return false;
178 }
179
180 return true;
181}
182
183
184// Configure the merge drivers and mergetool entries pointing at kicad-cli so
185// command-line `git merge` / `git mergetool` invocations route KiCad design
186// files through us. Append-only at the config level too: any existing
187// merge.kicad-* or mergetool.kicad config entry is preserved. `merge.tool`
188// is only set to "kicad" when the user hasn't already picked a tool.
189static void ensureMergeDriverConfig( const wxString& aProjectPath )
190{
191 const wxString cliPath = resolveKicadCliPath();
192
193 if( cliPath.IsEmpty() )
194 {
195 wxLogTrace( traceGit,
196 "kicad-cli not found next to running binary; skipping merge driver "
197 "config (libgit2 in-process driver still works)." );
198 return;
199 }
200
201 git_repository* repo = nullptr;
202
203 if( git_repository_open( &repo, aProjectPath.ToUTF8() ) != 0 )
204 {
205 wxLogTrace( traceGit, "git_repository_open failed: %s",
207 return;
208 }
209
210 KIGIT::GitRepositoryPtr repoPtr( repo );
211
212 git_config* config = nullptr;
213
214 if( git_repository_config( &config, repo ) != 0 )
215 {
216 wxLogTrace( traceGit, "git_repository_config failed: %s",
218 return;
219 }
220
221 KIGIT::GitConfigPtr configPtr( config );
222
223 // Per-format merge driver entries. Command-line `git merge` invokes the
224 // configured driver command for paths tagged `merge=kicad-*` in
225 // .gitattributes; the hidden `kicad-cli git-mergedriver` hook performs a
226 // structural 3-way merge (NOT a text merge), writing the merged file and
227 // returning 0 when clean. On unresolved conflicts it returns non-zero, so
228 // git leaves the path unmerged and the user resolves it interactively with
229 // `git mergetool` (configured below). The document kind is passed via
230 // --kind because git hands the driver extension-less temp paths.
231 // (KiCad's own in-app git integration uses the IN-PROCESS libgit2 driver
232 // registered via git_merge_driver_register instead of these commands.)
233 struct DRIVER_ENTRY
234 {
235 const char* nameKey;
236 const char* driverKey;
237 const char* humanName;
238 const char* cliCommand;
239 };
240
241 static const DRIVER_ENTRY drivers[] = {
242 { "merge.kicad-pcb.name", "merge.kicad-pcb.driver", "KiCad PCB merge driver",
243 "git-mergedriver --kind pcb \"%O\" \"%A\" \"%B\" --output \"%A\"" },
244 { "merge.kicad-sch.name", "merge.kicad-sch.driver", "KiCad schematic merge driver",
245 "git-mergedriver --kind sch \"%O\" \"%A\" \"%B\" --output \"%A\"" },
246 { "merge.kicad-sym-lib.name", "merge.kicad-sym-lib.driver",
247 "KiCad symbol library merge driver",
248 "git-mergedriver --kind sym \"%O\" \"%A\" \"%B\" --output \"%A\"" },
249 { "merge.kicad-fp.name", "merge.kicad-fp.driver", "KiCad footprint merge driver",
250 "git-mergedriver --kind fp \"%O\" \"%A\" \"%B\" --output \"%A\"" }
251 };
252
253 for( const DRIVER_ENTRY& d : drivers )
254 {
255 setConfigIfMissing( config, d.nameKey, wxString::FromUTF8( d.humanName ) );
256
257 wxString cmd = wxT( "\"" ) + cliPath + wxT( "\" " ) + wxString::FromUTF8( d.cliCommand );
258 setConfigIfMissing( config, d.driverKey, cmd );
259 }
260
261 // `git mergetool --tool=kicad` entry. Variables expanded by git:
262 // $BASE, $LOCAL, $REMOTE — ancestor / ours / theirs scratch files
263 // $MERGED — path the merged blob is written back to
264 wxString mergetoolCmd = wxT( "\"" ) + cliPath + wxT( "\" mergetool "
265 "\"$BASE\" \"$LOCAL\" \"$REMOTE\" "
266 "--output \"$MERGED\"" );
267 setConfigIfMissing( config, "mergetool.kicad.cmd", mergetoolCmd );
268 setConfigIfMissing( config, "mergetool.kicad.trustExitCode", wxT( "true" ) );
269
270 // merge.tool — only opt in when the user hasn't already picked a tool,
271 // so existing meld/kdiff3/etc. setups stay untouched.
272 setConfigIfMissing( config, "merge.tool", wxT( "kicad" ) );
273}
274
275
278
279
282
283
284bool GIT_INIT_HANDLER::IsRepository( const wxString& aPath )
285{
286 return GetGitBackend()->IsRepository( this, aPath );
287}
288
289
291{
293
296
297 return result;
298}
299
300
301void ApplyKicadGitConventions( const wxString& aProjectPath )
302{
303 ensureProjectGitignore( aProjectPath );
304 ensureProjectGitattributes( aProjectPath );
305 ensureMergeDriverConfig( aProjectPath );
306}
307
308
310{
311 return GetGitBackend()->SetupRemote( this, aConfig );
312}
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.
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.
static const std::vector< wxString > & TransientGitIgnorePatterns()
Shared patterns for new .gitignore files and the untracked-file filter.
The project's git session.
static wxString GetLastGitError()
libgit2's last message, without credentials from any URL it quotes, so it is safe to show or trace.
KIGIT_REPO_MIXIN(KIGIT_COMMON *aCommon)
static wxString ResolveSiblingExecutable(const wxString &aBaseName)
Resolve a sibling executable alongside the running process.
Definition paths.cpp:669
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.