KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_libgit_backend.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
26
28
29#include <git/git_backend.h>
30#include <git/libgit_backend.h>
38
39#include <git2.h>
40
41#include <fstream>
42
43#include <wx/ffile.h>
44#include <wx/filename.h>
45#include <wx/textfile.h>
46#include <wx/utils.h>
47
48
49static const char* TEST_AUTHOR_NAME = "Test Author";
51
52
59{
61 {
63 m_backend->Init();
65
66 m_tempBase = wxFileName::GetTempDir() + wxFileName::GetPathSeparator()
67 + wxString::Format( "kicad_libgit_backend_test_%ld_%ld", wxGetProcessId(), s_counter++ );
68 wxFileName::Mkdir( m_tempBase, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
69
70 m_repoPath = m_tempBase + wxFileName::GetPathSeparator() + wxT( "repo" );
71 m_remotePath = m_tempBase + wxFileName::GetPathSeparator() + wxT( "remote.git" );
72
73 wxFileName::Mkdir( m_repoPath, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
74
76 }
77
78
80 {
81 if( wxFileName::DirExists( m_tempBase ) )
82 wxFileName::Rmdir( m_tempBase, wxPATH_RMDIR_RECURSIVE );
83
84 SetGitBackend( nullptr );
85 m_backend->Shutdown();
86 delete m_backend;
87 }
88
89
90 bool ready() const { return m_ready; }
91 const wxString& repoPath() const { return m_repoPath; }
92 const wxString& remotePath() const { return m_remotePath; }
93
94
96 git_repository* openRepo() const
97 {
98 git_repository* repo = nullptr;
99 git_repository_open( &repo, m_repoPath.ToUTF8().data() );
100 return repo;
101 }
102
103
105 git_oid createCommit( git_repository* aRepo, const wxString& aFile, const wxString& aContent,
106 const wxString& aMessage )
107 {
108 git_oid commitOid = {};
109
110 wxString filePath = m_repoPath + wxFileName::GetPathSeparator() + aFile;
111 {
112 std::ofstream f( filePath.ToStdString() );
113 f << aContent.ToStdString();
114 }
115
116 git_index* index = nullptr;
117
118 if( git_repository_index( &index, aRepo ) != 0 )
119 return commitOid;
120
121 git_index_add_bypath( index, aFile.ToUTF8().data() );
122 git_index_write( index );
123
124 git_oid treeOid;
125 git_index_write_tree( &treeOid, index );
126 git_index_free( index );
127
128 git_tree* tree = nullptr;
129
130 if( git_tree_lookup( &tree, aRepo, &treeOid ) != 0 )
131 return commitOid;
132
133 git_signature* sig = nullptr;
134 git_signature_now( &sig, TEST_AUTHOR_NAME, TEST_AUTHOR_EMAIL );
135
136 // Parent is HEAD if it resolves, otherwise this becomes the root commit.
137 git_commit* parent = nullptr;
138 git_reference* headRef = nullptr;
139
140 if( git_repository_head( &headRef, aRepo ) == 0 )
141 {
142 git_reference_peel( (git_object**) &parent, headRef, GIT_OBJECT_COMMIT );
143 git_reference_free( headRef );
144 }
145
146 const git_commit* parents[1] = { parent };
147 const git_commit** parentsPtr = parent ? parents : nullptr;
148 size_t parentsCount = parent ? 1 : 0;
149
150 git_commit_create( &commitOid, aRepo, "HEAD", sig, sig, nullptr, aMessage.ToUTF8().data(), tree, parentsCount,
151 parentsPtr );
152
153 if( parent )
154 git_commit_free( parent );
155
156 git_signature_free( sig );
157 git_tree_free( tree );
158
159 return commitOid;
160 }
161
162
164 bool addOrigin( git_repository* aRepo )
165 {
166 wxString fileUrl = wxS( "file://" ) + m_remotePath;
167 git_remote* remote = nullptr;
168 int rc = git_remote_create( &remote, aRepo, "origin", fileUrl.ToUTF8().data() );
169
170 if( remote )
171 git_remote_free( remote );
172
173 return rc == 0;
174 }
175
176
179 bool clearUpstreamConfig( git_repository* aRepo, const wxString& aBranch )
180 {
181 git_config* cfg = nullptr;
182
183 if( git_repository_config( &cfg, aRepo ) != 0 )
184 return false;
185
186 wxString remoteKey = wxString::Format( "branch.%s.remote", aBranch );
187 wxString mergeKey = wxString::Format( "branch.%s.merge", aBranch );
188
189 git_config_delete_entry( cfg, remoteKey.ToUTF8().data() );
190 git_config_delete_entry( cfg, mergeKey.ToUTF8().data() );
191
192 git_config_free( cfg );
193 return true;
194 }
195
196
198 bool setUpstreamConfig( git_repository* aRepo, const wxString& aBranch )
199 {
200 git_config* cfg = nullptr;
201
202 if( git_repository_config( &cfg, aRepo ) != 0 )
203 return false;
204
205 wxString remoteKey = wxString::Format( "branch.%s.remote", aBranch );
206 wxString mergeKey = wxString::Format( "branch.%s.merge", aBranch );
207 wxString mergeVal = wxString::Format( "refs/heads/%s", aBranch );
208
209 git_config_set_string( cfg, remoteKey.ToUTF8().data(), "origin" );
210 git_config_set_string( cfg, mergeKey.ToUTF8().data(), mergeVal.ToUTF8().data() );
211 git_config_free( cfg );
212 return true;
213 }
214
215
217 wxString readConfig( git_repository* aRepo, const wxString& aKey )
218 {
219 git_config* cfg = nullptr;
220
221 if( git_repository_config( &cfg, aRepo ) != 0 )
222 return wxEmptyString;
223
224 git_buf buf = { nullptr, 0, 0 };
225 wxString result;
226
227 if( git_config_get_string_buf( &buf, cfg, aKey.ToUTF8().data() ) == 0 && buf.ptr )
228 result = wxString::FromUTF8( buf.ptr );
229
230 git_buf_dispose( &buf );
231 git_config_free( cfg );
232 return result;
233 }
234
235
236private:
238 {
239 git_repository* repo = nullptr;
240
241 // Force "master" as the initial branch so the tests are independent of the
242 // user's init.defaultBranch (often "main") in system or global gitconfig.
243 git_repository_init_options initOpts;
244 git_repository_init_options_init( &initOpts, GIT_REPOSITORY_INIT_OPTIONS_VERSION );
245 initOpts.initial_head = "master";
246
247 if( git_repository_init_ext( &repo, m_repoPath.ToUTF8().data(), &initOpts ) != 0 )
248 return false;
249
250 git_config* cfg = nullptr;
251
252 if( git_repository_config( &cfg, repo ) == 0 )
253 {
254 git_config_set_string( cfg, "user.name", TEST_AUTHOR_NAME );
255 git_config_set_string( cfg, "user.email", TEST_AUTHOR_EMAIL );
256 git_config_free( cfg );
257 }
258
259 createCommit( repo, wxT( "file.txt" ), wxT( "initial\n" ), wxT( "Initial commit" ) );
260 git_repository_free( repo );
261 return true;
262 }
263
264
266 {
267 git_clone_options opts;
268 git_clone_init_options( &opts, GIT_CLONE_OPTIONS_VERSION );
269 opts.bare = 1;
270
271 wxString sourceUrl = wxS( "file://" ) + m_repoPath;
272
273 git_repository* bare = nullptr;
274 int rc = git_clone( &bare, sourceUrl.ToUTF8().data(), m_remotePath.ToUTF8().data(), &opts );
275
276 if( bare )
277 git_repository_free( bare );
278
279 return rc == 0;
280 }
281
282
284 wxString m_tempBase;
285 wxString m_repoPath;
286 wxString m_remotePath;
288
289 static long s_counter;
290};
291
293
294
295BOOST_FIXTURE_TEST_SUITE( LibgitBackend, GIT_BACKEND_FIXTURE )
296
297
298
304BOOST_AUTO_TEST_CASE( IsWithinProjectPath_ScopesToProjectDirectory )
305{
306 const wxString project = wxT( "/repo/proj/" );
307
308 // Files inside the project directory are in scope.
309 BOOST_CHECK( KIGIT::PROJECT_GIT_UTILS::IsWithinProjectPath( wxT( "/repo/proj/proj.kicad_sch" ), project ) );
310 BOOST_CHECK( KIGIT::PROJECT_GIT_UTILS::IsWithinProjectPath( wxT( "/repo/proj/sub/board.kicad_pcb" ), project ) );
311
312 // Files elsewhere in the repository are not.
313 BOOST_CHECK( !KIGIT::PROJECT_GIT_UTILS::IsWithinProjectPath( wxT( "/repo/outside.txt" ), project ) );
314 BOOST_CHECK( !KIGIT::PROJECT_GIT_UTILS::IsWithinProjectPath( wxT( "/repo/other/f.txt" ), project ) );
315
316 // A sibling whose name merely shares the project's prefix must not match.
317 BOOST_CHECK( !KIGIT::PROJECT_GIT_UTILS::IsWithinProjectPath( wxT( "/repo/proj-extra/f.txt" ), project ) );
318
319 // The predicate normalizes a missing trailing separator before comparing, so the
320 // prefix collision above is still rejected when the caller omits the separator.
321 BOOST_CHECK( KIGIT::PROJECT_GIT_UTILS::IsWithinProjectPath( wxT( "/repo/proj/f.txt" ), wxT( "/repo/proj" ) ) );
322 BOOST_CHECK( !KIGIT::PROJECT_GIT_UTILS::IsWithinProjectPath( wxT( "/repo/proj-extra/f.txt" ),
323 wxT( "/repo/proj" ) ) );
324
325 // An empty project path matches nothing rather than the whole repository.
326 BOOST_CHECK( !KIGIT::PROJECT_GIT_UTILS::IsWithinProjectPath( wxT( "/repo/proj/f.txt" ), wxEmptyString ) );
327}
328
329
336BOOST_AUTO_TEST_CASE( AmendFileList_ExcludesFilesOutsideProject )
337{
338 BOOST_TEST_REQUIRE( ready() );
339
340 git_repository* repo = openRepo();
341 BOOST_TEST_REQUIRE( repo );
342
343 // The commit to be amended touches both a project-subdir file and an outside file, so
344 // the HEAD-vs-parent diff contains one in-scope and one out-of-scope path.
345 wxFileName::Mkdir( repoPath() + wxFileName::GetPathSeparator() + wxT( "proj" ), wxS_DIR_DEFAULT,
346 wxPATH_MKDIR_FULL );
347 {
348 std::ofstream f( ( repoPath() + wxFileName::GetPathSeparator() + wxT( "proj/proj.kicad_sch" ) ).ToStdString() );
349 f << "sch\n";
350 }
351 {
352 std::ofstream f( ( repoPath() + wxFileName::GetPathSeparator() + wxT( "outside.txt" ) ).ToStdString() );
353 f << "unrelated\n";
354 }
355
356 {
357 git_index* index = nullptr;
358 BOOST_TEST_REQUIRE( git_repository_index( &index, repo ) == 0 );
359 git_index_add_bypath( index, "proj/proj.kicad_sch" );
360 git_index_add_bypath( index, "outside.txt" );
361 git_index_write( index );
362
363 git_oid treeOid;
364 git_index_write_tree( &treeOid, index );
365 git_index_free( index );
366
367 git_tree* tree = nullptr;
368 BOOST_TEST_REQUIRE( git_tree_lookup( &tree, repo, &treeOid ) == 0 );
369
370 git_signature* sig = nullptr;
371 git_signature_now( &sig, TEST_AUTHOR_NAME, TEST_AUTHOR_EMAIL );
372
373 git_reference* headRefForParent = nullptr;
374 git_repository_head( &headRefForParent, repo );
375
376 git_commit* parent = nullptr;
377 git_reference_peel( (git_object**) &parent, headRefForParent, GIT_OBJECT_COMMIT );
378 git_reference_free( headRefForParent );
379
380 const git_commit* parents[1] = { parent };
381 git_oid commitOid;
382 git_commit_create( &commitOid, repo, "HEAD", sig, sig, nullptr, "touch both", tree, 1, parents );
383
384 git_commit_free( parent );
385 git_signature_free( sig );
386 git_tree_free( tree );
387 }
388
389 KIGIT_COMMON common( repo );
390 common.SetProjectDir( repoPath() + wxFileName::GetPathSeparator() );
391
392 GIT_STATUS_HANDLER statusHandler( &common );
393 wxString repoWorkDir = statusHandler.GetWorkingDirectory();
394 wxString projectPath = repoWorkDir + wxT( "proj" ) + wxFileName::GetPathSeparator();
395
396 std::map<wxString, int> modifiedFiles;
397
398 // Mirror the amend handler's diff-merge (HEAD vs parent), including the scope filter.
399 git_reference* headRef = nullptr;
400 BOOST_TEST_REQUIRE( git_repository_head( &headRef, repo ) == GIT_OK );
401
402 git_commit* lastCommit = nullptr;
403 git_reference_peel( (git_object**) &lastCommit, headRef, GIT_OBJECT_COMMIT );
404
405 git_commit* parentCommit = nullptr;
406 git_commit_parent( &parentCommit, lastCommit, 0 );
407
408 git_tree* parentTree = nullptr;
409 git_tree* lastTree = nullptr;
410 git_commit_tree( &parentTree, parentCommit );
411 git_commit_tree( &lastTree, lastCommit );
412
413 git_diff* diff = nullptr;
414 git_diff_tree_to_tree( &diff, repo, parentTree, lastTree, nullptr );
415
416 size_t deltas = git_diff_num_deltas( diff );
417
418 for( size_t ii = 0; ii < deltas; ++ii )
419 {
420 const git_diff_delta* delta = git_diff_get_delta( diff, ii );
421 const char* path = delta->new_file.path ? delta->new_file.path : delta->old_file.path;
422
423 if( !path )
424 continue;
425
426 wxString absPath = repoWorkDir + wxString::FromUTF8( path );
427
428 if( !KIGIT::PROJECT_GIT_UTILS::IsWithinProjectPath( absPath, projectPath ) )
429 continue;
430
431 modifiedFiles[wxString::FromUTF8( path )] |= GIT_STATUS_INDEX_MODIFIED;
432 }
433
434 BOOST_CHECK_MESSAGE( modifiedFiles.count( wxT( "outside.txt" ) ) == 0,
435 "outside.txt must not leak into the amend checklist" );
436 BOOST_CHECK_MESSAGE( modifiedFiles.count( wxT( "proj/proj.kicad_sch" ) ) == 1,
437 "the project file should still appear in the amend checklist" );
438
439 git_diff_free( diff );
440 git_tree_free( lastTree );
441 git_tree_free( parentTree );
442 git_commit_free( parentCommit );
443 git_commit_free( lastCommit );
444 git_reference_free( headRef );
445 git_repository_free( repo );
446}
447
448
449
455BOOST_AUTO_TEST_CASE( GetUpstreamShorthand_NoUpstreamFallsBackToRemoteSlashBranch )
456{
457 BOOST_TEST_REQUIRE( ready() );
458
459 git_repository* repo = openRepo();
460 BOOST_TEST_REQUIRE( repo );
461
462 // No upstream config, no remote-tracking ref. Should still produce a useful
463 // shorthand by combining the default remote name with the current branch.
464 clearUpstreamConfig( repo, wxT( "master" ) );
465
466 KIGIT_COMMON common( repo );
467 BOOST_CHECK_EQUAL( common.GetUpstreamShorthand(), wxString( "origin/master" ) );
468
469 git_repository_free( repo );
470}
471
472
481BOOST_AUTO_TEST_CASE( GetDifferentFiles_SameTreeAmendHidesAheadFiles )
482{
483 BOOST_TEST_REQUIRE( ready() );
484
485 git_repository* repo = openRepo();
486 BOOST_TEST_REQUIRE( repo );
487
488 // Add a second commit so the merge-base in GetDifferentFiles isn't the root.
489 git_oid c1 = createCommit( repo, wxT( "file.txt" ), wxT( "edited\n" ), wxT( "Edit file" ) );
490
491 // Simulate "pushed": create the remote-tracking ref pointing at C1.
492 git_reference* trackingRef = nullptr;
493 git_reference_create( &trackingRef, repo, "refs/remotes/origin/master", &c1, 1, nullptr );
494
495 if( trackingRef )
496 git_reference_free( trackingRef );
497
498 addOrigin( repo );
499 setUpstreamConfig( repo, wxT( "master" ) );
500
501 // Amend C1 message-only. Same tree, different commit OID.
502 git_reference* headRef = nullptr;
503 git_repository_head( &headRef, repo );
504
505 git_commit* headCommit = nullptr;
506 git_reference_peel( (git_object**) &headCommit, headRef, GIT_OBJECT_COMMIT );
507
508 git_tree* tree = nullptr;
509 git_commit_tree( &tree, headCommit );
510
511 git_signature* sig = nullptr;
512 git_signature_now( &sig, TEST_AUTHOR_NAME, TEST_AUTHOR_EMAIL );
513
514 git_oid amendedOid;
515 int rc = git_commit_amend( &amendedOid, headCommit, "HEAD", sig, sig, nullptr, "Edit file (message-only amend)",
516 tree );
517 BOOST_TEST_REQUIRE( rc == 0 );
518
519 git_tree_free( tree );
520 git_commit_free( headCommit );
521 git_reference_free( headRef );
522 git_signature_free( sig );
523
524 // Now: HEAD points at C1', upstream still at C1, both have the same tree.
525 // Pre-filter: AHEAD = { file.txt } (touched in C1 relative to root).
526 // Post-filter (the fix under test): AHEAD = {} because HEAD's blob == upstream's blob.
527 KIGIT_COMMON common( repo );
528 auto [ahead, behind] = common.GetDifferentFiles();
529
530 BOOST_CHECK_MESSAGE( ahead.empty(), "Message-only amend should not report any AHEAD files; got "
531 + std::to_string( ahead.size() ) );
532 BOOST_CHECK_MESSAGE( behind.empty(), "Message-only amend should not report any BEHIND files; got "
533 + std::to_string( behind.size() ) );
534
535 git_repository_free( repo );
536}
537
538
544BOOST_AUTO_TEST_CASE( GetDifferentFiles_DifferentTreeAmendKeepsAheadFile )
545{
546 BOOST_TEST_REQUIRE( ready() );
547
548 git_repository* repo = openRepo();
549 BOOST_TEST_REQUIRE( repo );
550
551 git_oid c1 = createCommit( repo, wxT( "file.txt" ), wxT( "edited\n" ), wxT( "Edit file" ) );
552
553 git_reference* trackingRef = nullptr;
554 git_reference_create( &trackingRef, repo, "refs/remotes/origin/master", &c1, 1, nullptr );
555
556 if( trackingRef )
557 git_reference_free( trackingRef );
558
559 addOrigin( repo );
560 setUpstreamConfig( repo, wxT( "master" ) );
561
562 // Make a new commit on top with different content (replacing C1 effectively).
563 // Use the same path so the file remains "the same file" to the diff.
564 createCommit( repo, wxT( "file.txt" ), wxT( "edited again\n" ), wxT( "Re-edit file" ) );
565
566 KIGIT_COMMON common( repo );
567 auto [ahead, behind] = common.GetDifferentFiles();
568
569 BOOST_CHECK_MESSAGE( ahead.count( wxT( "file.txt" ) ) == 1,
570 "AHEAD should contain file.txt when content differs from upstream" );
571
572 git_repository_free( repo );
573}
574
575
583BOOST_AUTO_TEST_CASE( PerformPull_NoUpstreamConfig_FallbackSucceedsAndWritesUpstream )
584{
585 BOOST_TEST_REQUIRE( ready() );
586
587 git_repository* repo = openRepo();
588 BOOST_TEST_REQUIRE( repo );
589
590 addOrigin( repo );
591 clearUpstreamConfig( repo, wxT( "master" ) );
592
593 KIGIT_COMMON common( repo );
594 GIT_PULL_HANDLER handler( &common );
595
596 PullResult result = handler.PerformPull();
597
598 // Without the fallback, this would be PullResult::Error with "Could not lookup commit".
601 "Pull without upstream config should succeed via fallback; got "
602 + std::to_string( static_cast<int>( result ) ) + ", err='"
603 + handler.GetErrorString().ToStdString() + "'" );
604
605 // The fallback also wires up tracking so subsequent pulls take the normal path.
606 BOOST_CHECK_EQUAL( readConfig( repo, wxT( "branch.master.remote" ) ), wxString( "origin" ) );
607 BOOST_CHECK_EQUAL( readConfig( repo, wxT( "branch.master.merge" ) ), wxString( "refs/heads/master" ) );
608
609 git_repository_free( repo );
610}
611
612
618BOOST_AUTO_TEST_CASE( Push_FirstPushSetsUpstreamTracking )
619{
620 BOOST_TEST_REQUIRE( ready() );
621
622 git_repository* repo = openRepo();
623 BOOST_TEST_REQUIRE( repo );
624
625 // Add a new local commit so there is something to actually push beyond the
626 // initial state shared with the bare remote.
627 createCommit( repo, wxT( "file.txt" ), wxT( "second\n" ), wxT( "Second commit" ) );
628
629 addOrigin( repo );
630 clearUpstreamConfig( repo, wxT( "master" ) );
631
632 KIGIT_COMMON common( repo );
633 GIT_PUSH_HANDLER handler( &common );
634
635 PushResult result = handler.PerformPush();
636 BOOST_TEST_REQUIRE( static_cast<int>( result ) == static_cast<int>( PushResult::Success ) );
637
638 BOOST_CHECK_EQUAL( readConfig( repo, wxT( "branch.master.remote" ) ), wxString( "origin" ) );
639 BOOST_CHECK_EQUAL( readConfig( repo, wxT( "branch.master.merge" ) ), wxString( "refs/heads/master" ) );
640
641 git_repository_free( repo );
642}
643
644
649BOOST_AUTO_TEST_CASE( Amend_MessageOnlyRewritesHeadKeepsTree )
650{
651 BOOST_TEST_REQUIRE( ready() );
652
653 git_repository* repo = openRepo();
654 BOOST_TEST_REQUIRE( repo );
655
656 git_reference* headRefBefore = nullptr;
657 git_repository_head( &headRefBefore, repo );
658 git_oid oidBefore = *git_reference_target( headRefBefore );
659
660 git_commit* commitBefore = nullptr;
661 git_reference_peel( (git_object**) &commitBefore, headRefBefore, GIT_OBJECT_COMMIT );
662 git_oid treeBefore = *git_commit_tree_id( commitBefore );
663
664 git_commit_free( commitBefore );
665 git_reference_free( headRefBefore );
666
667 GIT_COMMIT_HANDLER handler( repo );
668 CommitResult result = handler.PerformAmend( {}, wxT( "Amended message" ), TEST_AUTHOR_NAME, TEST_AUTHOR_EMAIL );
669 BOOST_CHECK_EQUAL( static_cast<int>( result ), static_cast<int>( CommitResult::Success ) );
670
671 git_reference* headRefAfter = nullptr;
672 git_repository_head( &headRefAfter, repo );
673 git_oid oidAfter = *git_reference_target( headRefAfter );
674
675 git_commit* commitAfter = nullptr;
676 git_reference_peel( (git_object**) &commitAfter, headRefAfter, GIT_OBJECT_COMMIT );
677 git_oid treeAfter = *git_commit_tree_id( commitAfter );
678
679 BOOST_CHECK( !git_oid_equal( &oidBefore, &oidAfter ) );
680 BOOST_CHECK( git_oid_equal( &treeBefore, &treeAfter ) );
681
682 wxString amendedMsg = wxString::FromUTF8( git_commit_message( commitAfter ) );
683 BOOST_CHECK_EQUAL( amendedMsg.Trim(), wxString( "Amended message" ) );
684
685 git_commit_free( commitAfter );
686 git_reference_free( headRefAfter );
687 git_repository_free( repo );
688}
689
690
694BOOST_AUTO_TEST_CASE( Amend_StagedFileChangesTreeAndKeepsParent )
695{
696 BOOST_TEST_REQUIRE( ready() );
697
698 git_repository* repo = openRepo();
699 BOOST_TEST_REQUIRE( repo );
700
701 git_reference* headRefBefore = nullptr;
702 git_repository_head( &headRefBefore, repo );
703
704 git_commit* commitBefore = nullptr;
705 git_reference_peel( (git_object**) &commitBefore, headRefBefore, GIT_OBJECT_COMMIT );
706 git_oid treeBefore = *git_commit_tree_id( commitBefore );
707 unsigned int parentCountBefore = git_commit_parentcount( commitBefore );
708
709 git_commit_free( commitBefore );
710 git_reference_free( headRefBefore );
711
712 // Add a new file to the working tree so the amend has something to stage.
713 wxString newFile = repoPath() + wxFileName::GetPathSeparator() + wxT( "new.txt" );
714 {
715 std::ofstream f( newFile.ToStdString() );
716 f << "new content\n";
717 }
718
719 GIT_COMMIT_HANDLER handler( repo );
720 CommitResult result = handler.PerformAmend( { wxT( "new.txt" ) }, wxT( "Amended with file" ), TEST_AUTHOR_NAME,
722 BOOST_CHECK_EQUAL( static_cast<int>( result ), static_cast<int>( CommitResult::Success ) );
723
724 git_reference* headRefAfter = nullptr;
725 git_repository_head( &headRefAfter, repo );
726
727 git_commit* commitAfter = nullptr;
728 git_reference_peel( (git_object**) &commitAfter, headRefAfter, GIT_OBJECT_COMMIT );
729 git_oid treeAfter = *git_commit_tree_id( commitAfter );
730 unsigned int parentCountAfter = git_commit_parentcount( commitAfter );
731
732 BOOST_CHECK( !git_oid_equal( &treeBefore, &treeAfter ) );
733 BOOST_CHECK_EQUAL( parentCountAfter, parentCountBefore );
734
735 // new.txt should now be in HEAD's tree.
736 git_tree* treeObj = nullptr;
737 git_commit_tree( &treeObj, commitAfter );
738
739 const git_tree_entry* entry = git_tree_entry_byname( treeObj, "new.txt" );
740 BOOST_CHECK( entry != nullptr );
741
742 git_tree_free( treeObj );
743 git_commit_free( commitAfter );
744 git_reference_free( headRefAfter );
745 git_repository_free( repo );
746}
747
748
753BOOST_AUTO_TEST_CASE( Amend_UnbornBranchReturnsError )
754{
755 BOOST_TEST_REQUIRE( ready() );
756
757 wxString unbornDir = repoPath() + wxT( "_unborn" );
758 wxFileName::Mkdir( unbornDir, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
759
760 git_repository* unbornRepo = nullptr;
761 BOOST_TEST_REQUIRE( git_repository_init( &unbornRepo, unbornDir.ToUTF8().data(), 0 ) == 0 );
762
763 GIT_COMMIT_HANDLER handler( unbornRepo );
764 CommitResult result = handler.PerformAmend( {}, wxT( "should fail" ), TEST_AUTHOR_NAME, TEST_AUTHOR_EMAIL );
765
766 BOOST_CHECK_EQUAL( static_cast<int>( result ), static_cast<int>( CommitResult::Error ) );
767 BOOST_CHECK( !handler.GetErrorString().IsEmpty() );
768
769 git_repository_free( unbornRepo );
770
771 if( wxFileName::DirExists( unbornDir ) )
772 wxFileName::Rmdir( unbornDir, wxPATH_RMDIR_RECURSIVE );
773}
774
775
780BOOST_AUTO_TEST_CASE( InitializeRepository_SeedsGitignoreWithKicadEntries )
781{
782 BOOST_TEST_REQUIRE( ready() );
783
784 wxString freshDir = repoPath() + wxT( "_fresh" );
785 wxFileName::Mkdir( freshDir, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
786
787 KIGIT_COMMON common( nullptr );
788 GIT_INIT_HANDLER handler( &common );
789
790 InitResult result = handler.InitializeRepository( freshDir );
791 BOOST_CHECK_EQUAL( static_cast<int>( result ), static_cast<int>( InitResult::Success ) );
792
793 wxFileName gitignoreFile( freshDir, wxT( ".gitignore" ) );
794 BOOST_TEST_REQUIRE( gitignoreFile.FileExists() );
795
796 wxString contents;
797 {
798 wxFFile f( gitignoreFile.GetFullPath(), wxT( "r" ) );
799 f.ReadAll( &contents );
800 }
801
802 BOOST_CHECK( contents.Contains( wxT( ".history/" ) ) );
803 BOOST_CHECK( contents.Contains( wxT( "*-backups/" ) ) );
804 BOOST_CHECK( contents.Contains( wxT( "_autosave-*" ) ) );
805 BOOST_CHECK( contents.Contains( wxT( "fp-info-cache" ) ) );
806 BOOST_CHECK( contents.Contains( wxT( "~*.lck" ) ) );
807
808 if( wxFileName::DirExists( freshDir ) )
809 wxFileName::Rmdir( freshDir, wxPATH_RMDIR_RECURSIVE );
810}
811
812
818BOOST_AUTO_TEST_CASE( InitializeRepository_GitignoreDoesNotDuplicateEntries )
819{
820 BOOST_TEST_REQUIRE( ready() );
821
822 wxString freshDir = repoPath() + wxT( "_dedup" );
823 wxFileName::Mkdir( freshDir, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
824
825 wxFileName gitignoreFile( freshDir, wxT( ".gitignore" ) );
826 {
827 wxFFile f( gitignoreFile.GetFullPath(), wxT( "w" ) );
828 f.Write( wxT( "# my custom ignores\n.history/\nbuild/\n" ) );
829 }
830
831 KIGIT_COMMON common( nullptr );
832 GIT_INIT_HANDLER handler( &common );
833
834 InitResult result = handler.InitializeRepository( freshDir );
835 BOOST_CHECK_EQUAL( static_cast<int>( result ), static_cast<int>( InitResult::Success ) );
836
837 wxTextFile tf;
838 BOOST_TEST_REQUIRE( tf.Open( gitignoreFile.GetFullPath() ) );
839
840 int historyCount = 0;
841 int buildCount = 0;
842 int fpInfoCount = 0;
843
844 for( size_t i = 0; i < tf.GetLineCount(); ++i )
845 {
846 wxString line = tf.GetLine( i );
847 line.Trim().Trim( false );
848
849 if( line == wxT( ".history/" ) )
850 historyCount++;
851 else if( line == wxT( "build/" ) )
852 buildCount++;
853 else if( line == wxT( "fp-info-cache" ) )
854 fpInfoCount++;
855 }
856
857 BOOST_CHECK_EQUAL( historyCount, 1 ); // was already present; not duplicated
858 BOOST_CHECK_EQUAL( buildCount, 1 ); // user's custom entry preserved
859 BOOST_CHECK_EQUAL( fpInfoCount, 1 ); // KiCad default that was missing got appended
860
861 if( wxFileName::DirExists( freshDir ) )
862 wxFileName::Rmdir( freshDir, wxPATH_RMDIR_RECURSIVE );
863}
864
865
int index
wxString GetErrorString() const
CommitResult PerformAmend(const std::vector< wxString > &aFiles, const wxString &aMessage, const wxString &aAuthorName, const wxString &aAuthorEmail)
InitResult InitializeRepository(const wxString &aPath)
Initialize a new git repository in the specified directory.
PullResult PerformPull()
PushResult PerformPush(bool aForce=false)
wxString GetWorkingDirectory()
Get the repository working directory path.
static bool IsWithinProjectPath(const wxString &aAbsPath, const wxString &aProjectPath)
Test whether an absolute file path lives inside the current project directory.
void SetProjectDir(const wxString &aProjectDir)
Set the project directory path, preserving any symlinks in the path.
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 ...
wxString GetUpstreamShorthand() const
Returns the upstream shorthand for the current branch (e.g.
wxString GetErrorString()
static std::string ToStdString(const wxString &aStr)
void SetGitBackend(GIT_BACKEND *aBackend)
CommitResult
Definition git_backend.h:52
InitResult
PullResult
PushResult
Build a temp directory tree containing a local working repo with one commit and a bare "remote" repo ...
const wxString & repoPath() const
wxString readConfig(git_repository *aRepo, const wxString &aKey)
Read a string value from the repo config, or "" if absent.
git_oid createCommit(git_repository *aRepo, const wxString &aFile, const wxString &aContent, const wxString &aMessage)
Create a new commit on the current branch from a single (path, content) pair.
git_repository * openRepo() const
Open the working repo. Caller frees with git_repository_free.
bool clearUpstreamConfig(git_repository *aRepo, const wxString &aBranch)
Drop branch.
bool addOrigin(git_repository *aRepo)
Add an origin remote pointing at the bare remote on disk.
const wxString & remotePath() const
bool setUpstreamConfig(git_repository *aRepo, const wxString &aBranch)
Set branch.<aBranch>.merge = refs/heads/<aBranch>, branch.<aBranch>.remote = origin.
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
static const char * TEST_AUTHOR_NAME
static const char * TEST_AUTHOR_EMAIL
BOOST_AUTO_TEST_SUITE_END()
std::string path
static const char * TEST_AUTHOR_NAME
BOOST_AUTO_TEST_CASE(IsWithinProjectPath_ScopesToProjectDirectory)
The commit/amend checklists scope files to the project directory so that unrelated files elsewhere in...
static const char * TEST_AUTHOR_EMAIL
BOOST_CHECK_MESSAGE(totalMismatches==0, std::to_string(totalMismatches)+" board(s) with strategy disagreements")
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")
int delta