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".
599 BOOST_CHECK_MESSAGE( result == PullResult::UpToDate || result == PullResult::Success
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
617BOOST_AUTO_TEST_CASE( PerformPull_RebaseConfigReplaysLocalCommit )
618{
619 BOOST_TEST_REQUIRE( ready() );
620
621 git_repository* repo = openRepo();
622 BOOST_TEST_REQUIRE( repo );
623
624 addOrigin( repo );
625 setUpstreamConfig( repo, wxT( "master" ) );
626
627 git_object* initial = nullptr;
628 BOOST_TEST_REQUIRE( git_revparse_single( &initial, repo, "HEAD" ) == 0 );
629
630 KIGIT_COMMON common( repo );
631 git_oid remoteOid = createCommit( repo, wxT( "remote.txt" ), wxT( "remote\n" ), wxT( "Remote commit" ) );
632
633 {
634 GIT_PUSH_HANDLER pushHandler( &common );
635 BOOST_TEST_REQUIRE( static_cast<int>( pushHandler.PerformPush() ) == static_cast<int>( PushResult::Success ) );
636 }
637
638 BOOST_TEST_REQUIRE( git_reset( repo, initial, GIT_RESET_HARD, nullptr ) == 0 );
639 git_object_free( initial );
640
641 createCommit( repo, wxT( "local.txt" ), wxT( "local\n" ), wxT( "Local commit" ) );
642
643 git_config* cfg = nullptr;
644 BOOST_TEST_REQUIRE( git_repository_config( &cfg, repo ) == 0 );
645 git_config_set_bool( cfg, "pull.rebase", 1 );
646 git_config_free( cfg );
647
648 GIT_PULL_HANDLER handler( &common );
649 PullResult result = handler.PerformPull();
650
651 BOOST_CHECK_MESSAGE( result == PullResult::Success,
652 "Rebase pull failed: " + handler.GetErrorString().ToStdString() );
653 BOOST_CHECK_EQUAL( git_repository_state( repo ), static_cast<int>( GIT_REPOSITORY_STATE_NONE ) );
654
655 git_commit* head = nullptr;
656 BOOST_TEST_REQUIRE( git_revparse_single( (git_object**) &head, repo, "HEAD" ) == 0 );
657 BOOST_CHECK( git_oid_equal( git_commit_parent_id( head, 0 ), &remoteOid ) );
658 git_commit_free( head );
659
660 git_repository_free( repo );
661}
662
663
669BOOST_AUTO_TEST_CASE( Push_FirstPushSetsUpstreamTracking )
670{
671 BOOST_TEST_REQUIRE( ready() );
672
673 git_repository* repo = openRepo();
674 BOOST_TEST_REQUIRE( repo );
675
676 // Add a new local commit so there is something to actually push beyond the
677 // initial state shared with the bare remote.
678 createCommit( repo, wxT( "file.txt" ), wxT( "second\n" ), wxT( "Second commit" ) );
679
680 addOrigin( repo );
681 clearUpstreamConfig( repo, wxT( "master" ) );
682
683 KIGIT_COMMON common( repo );
684 GIT_PUSH_HANDLER handler( &common );
685
686 PushResult result = handler.PerformPush();
687 BOOST_TEST_REQUIRE( static_cast<int>( result ) == static_cast<int>( PushResult::Success ) );
688
689 BOOST_CHECK_EQUAL( readConfig( repo, wxT( "branch.master.remote" ) ), wxString( "origin" ) );
690 BOOST_CHECK_EQUAL( readConfig( repo, wxT( "branch.master.merge" ) ), wxString( "refs/heads/master" ) );
691
692 git_repository_free( repo );
693}
694
695
700BOOST_AUTO_TEST_CASE( Amend_MessageOnlyRewritesHeadKeepsTree )
701{
702 BOOST_TEST_REQUIRE( ready() );
703
704 git_repository* repo = openRepo();
705 BOOST_TEST_REQUIRE( repo );
706
707 git_reference* headRefBefore = nullptr;
708 git_repository_head( &headRefBefore, repo );
709 git_oid oidBefore = *git_reference_target( headRefBefore );
710
711 git_commit* commitBefore = nullptr;
712 git_reference_peel( (git_object**) &commitBefore, headRefBefore, GIT_OBJECT_COMMIT );
713 git_oid treeBefore = *git_commit_tree_id( commitBefore );
714
715 git_commit_free( commitBefore );
716 git_reference_free( headRefBefore );
717
718 GIT_COMMIT_HANDLER handler( repo );
719 CommitResult result = handler.PerformAmend( {}, wxT( "Amended message" ), TEST_AUTHOR_NAME, TEST_AUTHOR_EMAIL );
720 BOOST_CHECK_EQUAL( static_cast<int>( result ), static_cast<int>( CommitResult::Success ) );
721
722 git_reference* headRefAfter = nullptr;
723 git_repository_head( &headRefAfter, repo );
724 git_oid oidAfter = *git_reference_target( headRefAfter );
725
726 git_commit* commitAfter = nullptr;
727 git_reference_peel( (git_object**) &commitAfter, headRefAfter, GIT_OBJECT_COMMIT );
728 git_oid treeAfter = *git_commit_tree_id( commitAfter );
729
730 BOOST_CHECK( !git_oid_equal( &oidBefore, &oidAfter ) );
731 BOOST_CHECK( git_oid_equal( &treeBefore, &treeAfter ) );
732
733 wxString amendedMsg = wxString::FromUTF8( git_commit_message( commitAfter ) );
734 BOOST_CHECK_EQUAL( amendedMsg.Trim(), wxString( "Amended message" ) );
735
736 git_commit_free( commitAfter );
737 git_reference_free( headRefAfter );
738 git_repository_free( repo );
739}
740
741
745BOOST_AUTO_TEST_CASE( Amend_StagedFileChangesTreeAndKeepsParent )
746{
747 BOOST_TEST_REQUIRE( ready() );
748
749 git_repository* repo = openRepo();
750 BOOST_TEST_REQUIRE( repo );
751
752 git_reference* headRefBefore = nullptr;
753 git_repository_head( &headRefBefore, repo );
754
755 git_commit* commitBefore = nullptr;
756 git_reference_peel( (git_object**) &commitBefore, headRefBefore, GIT_OBJECT_COMMIT );
757 git_oid treeBefore = *git_commit_tree_id( commitBefore );
758 unsigned int parentCountBefore = git_commit_parentcount( commitBefore );
759
760 git_commit_free( commitBefore );
761 git_reference_free( headRefBefore );
762
763 // Add a new file to the working tree so the amend has something to stage.
764 wxString newFile = repoPath() + wxFileName::GetPathSeparator() + wxT( "new.txt" );
765 {
766 std::ofstream f( newFile.ToStdString() );
767 f << "new content\n";
768 }
769
770 GIT_COMMIT_HANDLER handler( repo );
771 CommitResult result = handler.PerformAmend( { wxT( "new.txt" ) }, wxT( "Amended with file" ), TEST_AUTHOR_NAME,
773 BOOST_CHECK_EQUAL( static_cast<int>( result ), static_cast<int>( CommitResult::Success ) );
774
775 git_reference* headRefAfter = nullptr;
776 git_repository_head( &headRefAfter, repo );
777
778 git_commit* commitAfter = nullptr;
779 git_reference_peel( (git_object**) &commitAfter, headRefAfter, GIT_OBJECT_COMMIT );
780 git_oid treeAfter = *git_commit_tree_id( commitAfter );
781 unsigned int parentCountAfter = git_commit_parentcount( commitAfter );
782
783 BOOST_CHECK( !git_oid_equal( &treeBefore, &treeAfter ) );
784 BOOST_CHECK_EQUAL( parentCountAfter, parentCountBefore );
785
786 // new.txt should now be in HEAD's tree.
787 git_tree* treeObj = nullptr;
788 git_commit_tree( &treeObj, commitAfter );
789
790 const git_tree_entry* entry = git_tree_entry_byname( treeObj, "new.txt" );
791 BOOST_CHECK( entry != nullptr );
792
793 git_tree_free( treeObj );
794 git_commit_free( commitAfter );
795 git_reference_free( headRefAfter );
796 git_repository_free( repo );
797}
798
799
804BOOST_AUTO_TEST_CASE( Amend_UnbornBranchReturnsError )
805{
806 BOOST_TEST_REQUIRE( ready() );
807
808 wxString unbornDir = repoPath() + wxT( "_unborn" );
809 wxFileName::Mkdir( unbornDir, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
810
811 git_repository* unbornRepo = nullptr;
812 BOOST_TEST_REQUIRE( git_repository_init( &unbornRepo, unbornDir.ToUTF8().data(), 0 ) == 0 );
813
814 GIT_COMMIT_HANDLER handler( unbornRepo );
815 CommitResult result = handler.PerformAmend( {}, wxT( "should fail" ), TEST_AUTHOR_NAME, TEST_AUTHOR_EMAIL );
816
817 BOOST_CHECK_EQUAL( static_cast<int>( result ), static_cast<int>( CommitResult::Error ) );
818 BOOST_CHECK( !handler.GetErrorString().IsEmpty() );
819
820 git_repository_free( unbornRepo );
821
822 if( wxFileName::DirExists( unbornDir ) )
823 wxFileName::Rmdir( unbornDir, wxPATH_RMDIR_RECURSIVE );
824}
825
826
831BOOST_AUTO_TEST_CASE( InitializeRepository_SeedsGitignoreWithKicadEntries )
832{
833 BOOST_TEST_REQUIRE( ready() );
834
835 wxString freshDir = repoPath() + wxT( "_fresh" );
836 wxFileName::Mkdir( freshDir, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
837
838 KIGIT_COMMON common( nullptr );
839 GIT_INIT_HANDLER handler( &common );
840
841 InitResult result = handler.InitializeRepository( freshDir );
842 BOOST_CHECK_EQUAL( static_cast<int>( result ), static_cast<int>( InitResult::Success ) );
843
844 wxFileName gitignoreFile( freshDir, wxT( ".gitignore" ) );
845 BOOST_TEST_REQUIRE( gitignoreFile.FileExists() );
846
847 wxString contents;
848 {
849 wxFFile f( gitignoreFile.GetFullPath(), wxT( "r" ) );
850 f.ReadAll( &contents );
851 }
852
853 BOOST_CHECK( contents.Contains( wxT( ".history/" ) ) );
854 BOOST_CHECK( contents.Contains( wxT( "*-backups/" ) ) );
855 BOOST_CHECK( contents.Contains( wxT( "_autosave-*" ) ) );
856 BOOST_CHECK( contents.Contains( wxT( "fp-info-cache" ) ) );
857 BOOST_CHECK( contents.Contains( wxT( "~*.lck" ) ) );
858
859 if( wxFileName::DirExists( freshDir ) )
860 wxFileName::Rmdir( freshDir, wxPATH_RMDIR_RECURSIVE );
861}
862
863
869BOOST_AUTO_TEST_CASE( InitializeRepository_GitignoreDoesNotDuplicateEntries )
870{
871 BOOST_TEST_REQUIRE( ready() );
872
873 wxString freshDir = repoPath() + wxT( "_dedup" );
874 wxFileName::Mkdir( freshDir, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
875
876 wxFileName gitignoreFile( freshDir, wxT( ".gitignore" ) );
877 {
878 wxFFile f( gitignoreFile.GetFullPath(), wxT( "w" ) );
879 f.Write( wxT( "# my custom ignores\n.history/\nbuild/\n" ) );
880 }
881
882 KIGIT_COMMON common( nullptr );
883 GIT_INIT_HANDLER handler( &common );
884
885 InitResult result = handler.InitializeRepository( freshDir );
886 BOOST_CHECK_EQUAL( static_cast<int>( result ), static_cast<int>( InitResult::Success ) );
887
888 wxTextFile tf;
889 BOOST_TEST_REQUIRE( tf.Open( gitignoreFile.GetFullPath() ) );
890
891 int historyCount = 0;
892 int buildCount = 0;
893 int fpInfoCount = 0;
894
895 for( size_t i = 0; i < tf.GetLineCount(); ++i )
896 {
897 wxString line = tf.GetLine( i );
898 line.Trim().Trim( false );
899
900 if( line == wxT( ".history/" ) )
901 historyCount++;
902 else if( line == wxT( "build/" ) )
903 buildCount++;
904 else if( line == wxT( "fp-info-cache" ) )
905 fpInfoCount++;
906 }
907
908 BOOST_CHECK_EQUAL( historyCount, 1 ); // was already present; not duplicated
909 BOOST_CHECK_EQUAL( buildCount, 1 ); // user's custom entry preserved
910 BOOST_CHECK_EQUAL( fpInfoCount, 1 ); // KiCad default that was missing got appended
911
912 if( wxFileName::DirExists( freshDir ) )
913 wxFileName::Rmdir( freshDir, wxPATH_RMDIR_RECURSIVE );
914}
915
916
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
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")
int delta