KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_kigit_common.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
21
24
25#include <git2.h>
26
27#include <wx/filename.h>
28#include <wx/string.h>
29#include <wx/stdpaths.h>
30
31#include <chrono>
32#include <fstream>
33#include <memory>
34#include <cstdlib>
35
36
37namespace
38{
39
40struct GitInitGuard
41{
42 GitInitGuard() { git_libgit2_init(); }
43 ~GitInitGuard() { git_libgit2_shutdown(); }
44};
45
46
47struct ScopedTempDir
48{
49 wxString path;
50
52 {
53 auto now = std::chrono::steady_clock::now().time_since_epoch();
54 long long ticks = std::chrono::duration_cast<std::chrono::nanoseconds>( now ).count();
55
56 wxString base = wxFileName::GetTempDir();
57 wxFileName fn;
58 fn.AssignDir( base );
59 fn.AppendDir( wxString::Format( "kicad-qa-git-%lld-%p", ticks, this ) );
60 wxFileName::Mkdir( fn.GetPath(), wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
61
62#ifdef __WXMAC__
63 // Store the realpath-resolved form. libgit2 canonicalizes the repo workdir with
64 // realpath(3), so on macOS -- where /var is a symlink to /private/var -- any path
65 // derived from the repo differs textually from the one we built above unless we
66 // resolve symlinks here too. Comparisons against GetGitRootDirectory() rely on this.
67 char* resolved = realpath( fn.GetPath().utf8_string().c_str(), nullptr );
68
69 if( resolved )
70 {
71 path = wxString::FromUTF8( resolved );
72 free( resolved );
73 }
74 else
75 {
76 path = fn.GetPath();
77 }
78#else
79 path = fn.GetPath();
80#endif
81 }
82
84 {
85 if( !path.IsEmpty() )
86 wxFileName::Rmdir( path, wxPATH_RMDIR_RECURSIVE );
87 }
88};
89
90
91// Initialize a git repository at the given path with one initial commit on "main"
92git_repository* makeRepoWithCommit( const wxString& aRepoPath, const wxString& aFileName,
93 const std::string& aFileContents )
94{
95 git_repository* repo = nullptr;
96 git_repository_init_options init_opts = GIT_REPOSITORY_INIT_OPTIONS_INIT;
97 init_opts.flags = GIT_REPOSITORY_INIT_MKPATH;
98 init_opts.initial_head = "main";
99
100 BOOST_REQUIRE_EQUAL( git_repository_init_ext( &repo, aRepoPath.utf8_string().c_str(),
101 &init_opts ),
102 GIT_OK );
103
104 // Write a file
105 wxFileName filePath( aRepoPath, aFileName );
106 {
107 std::ofstream f( filePath.GetFullPath().utf8_string() );
108 f << aFileContents;
109 }
110
111 // Configure user
112 git_config* cfg = nullptr;
113 BOOST_REQUIRE_EQUAL( git_repository_config( &cfg, repo ), GIT_OK );
114 KIGIT::GitConfigPtr cfgPtr( cfg );
115 git_config_set_string( cfg, "user.name", "QA Test" );
116 git_config_set_string( cfg, "user.email", "[email protected]" );
117
118 // Stage and commit
119 git_index* index = nullptr;
120 BOOST_REQUIRE_EQUAL( git_repository_index( &index, repo ), GIT_OK );
121 KIGIT::GitIndexPtr indexPtr( index );
122 BOOST_REQUIRE_EQUAL( git_index_add_bypath( index, aFileName.utf8_string().c_str() ), GIT_OK );
123 BOOST_REQUIRE_EQUAL( git_index_write( index ), GIT_OK );
124
125 git_oid tree_oid;
126 BOOST_REQUIRE_EQUAL( git_index_write_tree( &tree_oid, index ), GIT_OK );
127
128 git_tree* tree = nullptr;
129 BOOST_REQUIRE_EQUAL( git_tree_lookup( &tree, repo, &tree_oid ), GIT_OK );
130 KIGIT::GitTreePtr treePtr( tree );
131
132 git_signature* sig = nullptr;
133 BOOST_REQUIRE_EQUAL( git_signature_now( &sig, "QA Test", "[email protected]" ), GIT_OK );
134 KIGIT::GitSignaturePtr sigPtr( sig );
135
136 git_oid commit_oid;
137 BOOST_REQUIRE_EQUAL( git_commit_create( &commit_oid, repo, "HEAD", sig, sig, nullptr,
138 "initial", tree, 0, nullptr ),
139 GIT_OK );
140
141 return repo;
142}
143
144} // namespace
145
146
147BOOST_AUTO_TEST_SUITE( KiGitCommon )
148
149
150// GetGitRootDirectory must return the working directory (project root) and not the
151// internal .git folder. Older code returned git_repository_path() which produced a path
152// ending in /.git/, breaking the version-control popup-menu detection logic for whether a
153// project lives at the repository root.
154BOOST_AUTO_TEST_CASE( GitRootDirectoryReturnsWorkdir )
155{
156 GitInitGuard libgit;
157 ScopedTempDir tmp;
158
159 git_repository* repo = makeRepoWithCommit( tmp.path, "file.txt", "data\n" );
160 KIGIT::GitRepositoryPtr repoPtr( repo );
161
162 KIGIT_COMMON common( repo );
163 wxString root = common.GetGitRootDirectory();
164
165 BOOST_CHECK( !root.IsEmpty() );
166 BOOST_CHECK_MESSAGE( !root.Contains( wxS( "/.git" ) ) && !root.Contains( wxS( "\\.git" ) ),
167 "GetGitRootDirectory should return the working directory, got: "
168 + root.ToStdString() );
169
170 // Should be the repo path (with trailing separator).
171 wxFileName rootFn;
172 rootFn.AssignDir( root );
173 rootFn.MakeAbsolute();
174 wxFileName tmpFn;
175 tmpFn.AssignDir( tmp.path );
176 tmpFn.MakeAbsolute();
177 BOOST_CHECK_EQUAL( rootFn.GetFullPath().ToStdString(), tmpFn.GetFullPath().ToStdString() );
178}
179
180
181// HasPushAndPullRemote must report true for repositories whose remote uses a name other
182// than "origin", because libgit2 allows arbitrary remote names and many users rename or
183// add additional remotes.
184BOOST_AUTO_TEST_CASE( HasPushAndPullRemoteAcceptsNonOriginName )
185{
186 GitInitGuard libgit;
187 ScopedTempDir tmp;
188
189 git_repository* repo = makeRepoWithCommit( tmp.path, "file.txt", "data\n" );
190 KIGIT::GitRepositoryPtr repoPtr( repo );
191
192 // No remotes configured.
193 KIGIT_COMMON common( repo );
194 BOOST_CHECK( !common.HasPushAndPullRemote() );
195
196 // Add a remote with a non-default name.
197 git_remote* remote = nullptr;
198 BOOST_REQUIRE_EQUAL( git_remote_create( &remote, repo, "github",
199 "[email protected]:example/repo.git" ),
200 GIT_OK );
201 KIGIT::GitRemotePtr remotePtr( remote );
202
203 BOOST_CHECK( common.HasPushAndPullRemote() );
204}
205
206
207BOOST_AUTO_TEST_CASE( HasPushAndPullRemoteFindsOrigin )
208{
209 GitInitGuard libgit;
210 ScopedTempDir tmp;
211
212 git_repository* repo = makeRepoWithCommit( tmp.path, "file.txt", "data\n" );
213 KIGIT::GitRepositoryPtr repoPtr( repo );
214
215 git_remote* remote = nullptr;
216 BOOST_REQUIRE_EQUAL( git_remote_create( &remote, repo, "origin",
217 "[email protected]:repo.git" ),
218 GIT_OK );
219 KIGIT::GitRemotePtr remotePtr( remote );
220
221 KIGIT_COMMON common( repo );
222 BOOST_CHECK( common.HasPushAndPullRemote() );
223}
224
225
226// GetDifferentFiles previously walked unbounded history when no upstream OID was available
227// and dumped every file in the root commit's tree into the modified set, falsely flagging
228// every file as ahead of the remote. With no upstream configured, both sets must be empty.
229BOOST_AUTO_TEST_CASE( GetDifferentFilesEmptyWithoutUpstream )
230{
231 GitInitGuard libgit;
232 ScopedTempDir tmp;
233
234 git_repository* repo = makeRepoWithCommit( tmp.path, "file.txt", "data\n" );
235 KIGIT::GitRepositoryPtr repoPtr( repo );
236
237 KIGIT_COMMON common( repo );
238 auto [local, remote] = common.GetDifferentFiles();
239
240 BOOST_CHECK_MESSAGE( local.empty(),
241 "Expected no AHEAD files when no upstream is configured, got "
242 + std::to_string( local.size() ) );
243 BOOST_CHECK_MESSAGE( remote.empty(),
244 "Expected no BEHIND files when no upstream is configured, got "
245 + std::to_string( remote.size() ) );
246}
247
248
249// Regression test for the root-commit dump that this commit fixes. Set up an upstream
250// tracking ref whose history is disjoint from the local HEAD's history (separate root
251// commits, no shared ancestors). Pre-fix, get_modified_files() walked the local history
252// past the unrelated upstream and reached the local root commit. Because the root commit
253// has no parent, the code dumped *every* file in its tree into the AHEAD set, including
254// files like "untouched.txt" that were neither created nor modified relative to the
255// upstream's view. The fix bails out before we can include these phantom changes.
256BOOST_AUTO_TEST_CASE( GetDifferentFilesHandlesUnrelatedHistories )
257{
258 GitInitGuard libgit;
259 ScopedTempDir tmp;
260
261 // Local root commit with two files. The bug previously listed both as AHEAD.
262 git_repository* repo = makeRepoWithCommit( tmp.path, "untouched.txt", "stable\n" );
263 KIGIT::GitRepositoryPtr repoPtr( repo );
264
265 // Add a second file to local HEAD so we have more than the bare initial tree.
266 {
267 wxFileName extra( tmp.path, wxS( "second.txt" ) );
268 std::ofstream f( extra.GetFullPath().utf8_string() );
269 f << "more\n";
270 }
271
272 git_index* index = nullptr;
273 BOOST_REQUIRE_EQUAL( git_repository_index( &index, repo ), GIT_OK );
274 KIGIT::GitIndexPtr indexPtr( index );
275 BOOST_REQUIRE_EQUAL( git_index_add_bypath( index, "second.txt" ), GIT_OK );
276 BOOST_REQUIRE_EQUAL( git_index_write( index ), GIT_OK );
277
278 git_oid newTreeOid;
279 BOOST_REQUIRE_EQUAL( git_index_write_tree( &newTreeOid, index ), GIT_OK );
280
281 git_tree* newTree = nullptr;
282 BOOST_REQUIRE_EQUAL( git_tree_lookup( &newTree, repo, &newTreeOid ), GIT_OK );
283 KIGIT::GitTreePtr newTreePtr( newTree );
284
285 git_reference* head = nullptr;
286 BOOST_REQUIRE_EQUAL( git_repository_head( &head, repo ), GIT_OK );
287 KIGIT::GitReferencePtr headPtr( head );
288
289 git_commit* parentCommit = nullptr;
290 BOOST_REQUIRE_EQUAL(
291 git_commit_lookup( &parentCommit, repo, git_reference_target( head ) ), GIT_OK );
292 KIGIT::GitCommitPtr parentCommitPtr( parentCommit );
293
294 git_signature* sig = nullptr;
295 BOOST_REQUIRE_EQUAL( git_signature_now( &sig, "QA Test", "[email protected]" ), GIT_OK );
296 KIGIT::GitSignaturePtr sigPtr( sig );
297
298 const git_commit* parents[1] = { parentCommit };
299 git_oid secondCommitOid;
300 BOOST_REQUIRE_EQUAL( git_commit_create( &secondCommitOid, repo, "HEAD", sig, sig, nullptr,
301 "second", newTree, 1, parents ),
302 GIT_OK );
303
304 // Build a disjoint upstream history with its own root commit and a different file.
305 git_treebuilder* tb = nullptr;
306 BOOST_REQUIRE_EQUAL( git_treebuilder_new( &tb, repo, nullptr ), GIT_OK );
307
308 git_oid blobOid;
309 const std::string upstreamData = "upstream\n";
310 BOOST_REQUIRE_EQUAL( git_blob_create_from_buffer( &blobOid, repo, upstreamData.data(),
311 upstreamData.size() ),
312 GIT_OK );
313 BOOST_REQUIRE_EQUAL( git_treebuilder_insert( nullptr, tb, "remote_only.txt", &blobOid,
314 GIT_FILEMODE_BLOB ),
315 GIT_OK );
316
317 git_oid remoteTreeOid;
318 BOOST_REQUIRE_EQUAL( git_treebuilder_write( &remoteTreeOid, tb ), GIT_OK );
319 git_treebuilder_free( tb );
320
321 git_tree* remoteTree = nullptr;
322 BOOST_REQUIRE_EQUAL( git_tree_lookup( &remoteTree, repo, &remoteTreeOid ), GIT_OK );
323 KIGIT::GitTreePtr remoteTreePtr( remoteTree );
324
325 git_oid remoteCommitOid;
326 BOOST_REQUIRE_EQUAL( git_commit_create( &remoteCommitOid, repo, nullptr, sig, sig, nullptr,
327 "upstream root", remoteTree, 0, nullptr ),
328 GIT_OK );
329
330 git_reference* remoteRef = nullptr;
331 BOOST_REQUIRE_EQUAL( git_reference_create( &remoteRef, repo, "refs/remotes/origin/main",
332 &remoteCommitOid, true, nullptr ),
333 GIT_OK );
334 KIGIT::GitReferencePtr remoteRefPtr( remoteRef );
335
336 git_config* cfg = nullptr;
337 BOOST_REQUIRE_EQUAL( git_repository_config( &cfg, repo ), GIT_OK );
338 KIGIT::GitConfigPtr cfgPtr( cfg );
339 BOOST_REQUIRE_EQUAL( git_config_set_string( cfg, "branch.main.remote", "origin" ),
340 GIT_OK );
341 BOOST_REQUIRE_EQUAL( git_config_set_string( cfg, "branch.main.merge", "refs/heads/main" ),
342 GIT_OK );
343
344 KIGIT_COMMON common( repo );
345 auto [local, remote] = common.GetDifferentFiles();
346
347 // With unrelated histories there is no merge base, so the implementation cannot
348 // attribute changes to either side. Pre-fix, the revwalk reached both root commits
349 // and dumped both trees in full; the new merge-base approach short-circuits and
350 // reports no AHEAD/BEHIND files in this ambiguous state. In particular, the
351 // never-touched-locally "untouched.txt" must not appear in the AHEAD set.
352 BOOST_CHECK_MESSAGE( local.find( wxS( "untouched.txt" ) ) == local.end(),
353 "untouched.txt should not be reported as AHEAD when the local and "
354 "remote histories share no ancestor" );
355 BOOST_CHECK_MESSAGE( remote.find( wxS( "untouched.txt" ) ) == remote.end(),
356 "untouched.txt should not be reported as BEHIND when the local "
357 "and remote histories share no ancestor" );
358}
359
360
361// When local has commits ahead of a real upstream, only files that actually changed in those
362// local commits should be reported as AHEAD. Files that exist in both trees unchanged must
363// not show up as ahead. This is the common-case behaviour the issue reproducer exercises.
364BOOST_AUTO_TEST_CASE( GetDifferentFilesReportsOnlyTouchedFilesWhenAhead )
365{
366 GitInitGuard libgit;
367 ScopedTempDir tmp;
368
369 // Initial commit becomes the shared base. Two files: only one will be modified later.
370 git_repository* repo = makeRepoWithCommit( tmp.path, "untouched.txt", "stable\n" );
371 KIGIT::GitRepositoryPtr repoPtr( repo );
372
373 {
374 wxFileName extra( tmp.path, wxS( "touched.txt" ) );
375 std::ofstream f( extra.GetFullPath().utf8_string() );
376 f << "v1\n";
377 }
378
379 git_index* index = nullptr;
380 BOOST_REQUIRE_EQUAL( git_repository_index( &index, repo ), GIT_OK );
381 KIGIT::GitIndexPtr indexPtr( index );
382 BOOST_REQUIRE_EQUAL( git_index_add_bypath( index, "touched.txt" ), GIT_OK );
383 BOOST_REQUIRE_EQUAL( git_index_write( index ), GIT_OK );
384
385 git_oid treeOid;
386 BOOST_REQUIRE_EQUAL( git_index_write_tree( &treeOid, index ), GIT_OK );
387
388 git_tree* tree = nullptr;
389 BOOST_REQUIRE_EQUAL( git_tree_lookup( &tree, repo, &treeOid ), GIT_OK );
390 KIGIT::GitTreePtr treePtr( tree );
391
392 git_reference* head = nullptr;
393 BOOST_REQUIRE_EQUAL( git_repository_head( &head, repo ), GIT_OK );
394 KIGIT::GitReferencePtr headPtr( head );
395
396 git_commit* parent = nullptr;
397 BOOST_REQUIRE_EQUAL( git_commit_lookup( &parent, repo, git_reference_target( head ) ),
398 GIT_OK );
399 KIGIT::GitCommitPtr parentPtr( parent );
400
401 git_signature* sig = nullptr;
402 BOOST_REQUIRE_EQUAL( git_signature_now( &sig, "QA Test", "[email protected]" ), GIT_OK );
403 KIGIT::GitSignaturePtr sigPtr( sig );
404
405 const git_commit* parents[1] = { parent };
406 git_oid base_oid;
407 BOOST_REQUIRE_EQUAL( git_commit_create( &base_oid, repo, "HEAD", sig, sig, nullptr,
408 "add touched", tree, 1, parents ),
409 GIT_OK );
410
411 // Mark this commit as the upstream tip and configure a remote so libgit2 accepts the
412 // branch.main.remote = origin pointer below.
413 git_remote* origin = nullptr;
414 BOOST_REQUIRE_EQUAL( git_remote_create( &origin, repo, "origin",
415 "[email protected]:repo.git" ),
416 GIT_OK );
417 KIGIT::GitRemotePtr originPtr( origin );
418
419 git_reference* upstream_ref = nullptr;
420 BOOST_REQUIRE_EQUAL( git_reference_create( &upstream_ref, repo, "refs/remotes/origin/main",
421 &base_oid, true, nullptr ),
422 GIT_OK );
423 KIGIT::GitReferencePtr upstreamRefPtr( upstream_ref );
424
425 git_config* cfg = nullptr;
426 BOOST_REQUIRE_EQUAL( git_repository_config( &cfg, repo ), GIT_OK );
427 KIGIT::GitConfigPtr cfgPtr( cfg );
428 BOOST_REQUIRE_EQUAL( git_config_set_string( cfg, "branch.main.remote", "origin" ),
429 GIT_OK );
430 BOOST_REQUIRE_EQUAL( git_config_set_string( cfg, "branch.main.merge", "refs/heads/main" ),
431 GIT_OK );
432
433 // Local-only commit modifying just touched.txt.
434 {
435 wxFileName extra( tmp.path, wxS( "touched.txt" ) );
436 std::ofstream f( extra.GetFullPath().utf8_string() );
437 f << "v2\n";
438 }
439
440 BOOST_REQUIRE_EQUAL( git_index_add_bypath( index, "touched.txt" ), GIT_OK );
441 BOOST_REQUIRE_EQUAL( git_index_write( index ), GIT_OK );
442
443 git_oid newTreeOid;
444 BOOST_REQUIRE_EQUAL( git_index_write_tree( &newTreeOid, index ), GIT_OK );
445
446 git_tree* newTree = nullptr;
447 BOOST_REQUIRE_EQUAL( git_tree_lookup( &newTree, repo, &newTreeOid ), GIT_OK );
448 KIGIT::GitTreePtr newTreePtr( newTree );
449
450 git_commit* baseCommit = nullptr;
451 BOOST_REQUIRE_EQUAL( git_commit_lookup( &baseCommit, repo, &base_oid ), GIT_OK );
452 KIGIT::GitCommitPtr baseCommitPtr( baseCommit );
453
454 const git_commit* aheadParents[1] = { baseCommit };
455 git_oid aheadOid;
456 BOOST_REQUIRE_EQUAL( git_commit_create( &aheadOid, repo, "HEAD", sig, sig, nullptr,
457 "modify touched", newTree, 1, aheadParents ),
458 GIT_OK );
459
460 KIGIT_COMMON common( repo );
461 auto [local, remote] = common.GetDifferentFiles();
462
463 BOOST_CHECK( remote.empty() );
464 BOOST_CHECK_MESSAGE( local.find( wxS( "touched.txt" ) ) != local.end(),
465 "touched.txt should be reported as AHEAD" );
466 BOOST_CHECK_MESSAGE( local.find( wxS( "untouched.txt" ) ) == local.end(),
467 "untouched.txt should NOT be reported as AHEAD - this was the "
468 "regression in issue 21576" );
469}
470
471
int index
wxString GetGitRootDirectory() const
std::pair< std::set< wxString >, std::set< wxString > > GetDifferentFiles() const
Return a pair of sets of files that differ locally from the remote repository The first set is files ...
bool HasPushAndPullRemote() const
std::unique_ptr< git_tree, decltype([](git_tree *aTree) { git_tree_free(aTree); })> GitTreePtr
A unique pointer for git_tree objects with automatic cleanup.
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_commit, decltype([](git_commit *aCommit) { git_commit_free(aCommit); })> GitCommitPtr
A unique pointer for git_commit 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_reference, decltype([](git_reference *aRef) { git_reference_free(aRef); })> GitReferencePtr
A unique pointer for git_reference objects with automatic cleanup.
std::unique_ptr< git_signature, decltype([](git_signature *aSignature) { git_signature_free(aSignature); })> GitSignaturePtr
A unique pointer for git_signature objects with automatic cleanup.
std::unique_ptr< git_index, decltype([](git_index *aIndex) { git_index_free(aIndex); })> GitIndexPtr
A unique pointer for git_index objects with automatic cleanup.
std::unique_ptr< git_remote, decltype([](git_remote *aRemote) { git_remote_free(aRemote); })> GitRemotePtr
A unique pointer for git_remote objects with automatic cleanup.
Scoped temporary directory used by the tests below.
ScopedTempDir(const wxString &aTag)
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_AUTO_TEST_SUITE_END()
BOOST_AUTO_TEST_CASE(GitRootDirectoryReturnsWorkdir)
BOOST_CHECK_EQUAL(result, "25.4")