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