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#ifdef __WINDOWS__
326 BOOST_CHECK( KIGIT::PROJECT_GIT_UTILS::IsWithinProjectPath( wxT( "C:\\repo\\proj/file.kicad_sch" ),
327 wxT( "C:\\repo\\proj\\" ) ) );
328 BOOST_CHECK( !KIGIT::PROJECT_GIT_UTILS::IsWithinProjectPath( wxT( "C:\\repo\\proj-extra/file.kicad_sch" ),
329 wxT( "C:\\repo\\proj\\" ) ) );
330#endif
331
332 // An empty project path matches nothing rather than the whole repository.
333 BOOST_CHECK( !KIGIT::PROJECT_GIT_UTILS::IsWithinProjectPath( wxT( "/repo/proj/f.txt" ), wxEmptyString ) );
334}
335
336
343BOOST_AUTO_TEST_CASE( AmendFileList_ExcludesFilesOutsideProject )
344{
345 BOOST_TEST_REQUIRE( ready() );
346
347 git_repository* repo = openRepo();
348 BOOST_TEST_REQUIRE( repo );
349
350 // The commit to be amended touches both a project-subdir file and an outside file, so
351 // the HEAD-vs-parent diff contains one in-scope and one out-of-scope path.
352 wxFileName::Mkdir( repoPath() + wxFileName::GetPathSeparator() + wxT( "proj" ), wxS_DIR_DEFAULT,
353 wxPATH_MKDIR_FULL );
354 {
355 std::ofstream f( ( repoPath() + wxFileName::GetPathSeparator() + wxT( "proj/proj.kicad_sch" ) ).ToStdString() );
356 f << "sch\n";
357 }
358 {
359 std::ofstream f( ( repoPath() + wxFileName::GetPathSeparator() + wxT( "outside.txt" ) ).ToStdString() );
360 f << "unrelated\n";
361 }
362
363 {
364 git_index* index = nullptr;
365 BOOST_TEST_REQUIRE( git_repository_index( &index, repo ) == 0 );
366 git_index_add_bypath( index, "proj/proj.kicad_sch" );
367 git_index_add_bypath( index, "outside.txt" );
368 git_index_write( index );
369
370 git_oid treeOid;
371 git_index_write_tree( &treeOid, index );
372 git_index_free( index );
373
374 git_tree* tree = nullptr;
375 BOOST_TEST_REQUIRE( git_tree_lookup( &tree, repo, &treeOid ) == 0 );
376
377 git_signature* sig = nullptr;
378 git_signature_now( &sig, TEST_AUTHOR_NAME, TEST_AUTHOR_EMAIL );
379
380 git_reference* headRefForParent = nullptr;
381 git_repository_head( &headRefForParent, repo );
382
383 git_commit* parent = nullptr;
384 git_reference_peel( (git_object**) &parent, headRefForParent, GIT_OBJECT_COMMIT );
385 git_reference_free( headRefForParent );
386
387 const git_commit* parents[1] = { parent };
388 git_oid commitOid;
389 git_commit_create( &commitOid, repo, "HEAD", sig, sig, nullptr, "touch both", tree, 1, parents );
390
391 git_commit_free( parent );
392 git_signature_free( sig );
393 git_tree_free( tree );
394 }
395
396 KIGIT_COMMON common( repo );
397 common.SetProjectDir( repoPath() + wxFileName::GetPathSeparator() );
398
399 GIT_STATUS_HANDLER statusHandler( &common );
400 wxString repoWorkDir = statusHandler.GetWorkingDirectory();
401 wxString projectPath = repoWorkDir + wxT( "proj" ) + wxFileName::GetPathSeparator();
402
403 std::map<wxString, int> modifiedFiles;
404
405 // Mirror the amend handler's diff-merge (HEAD vs parent), including the scope filter.
406 git_reference* headRef = nullptr;
407 BOOST_TEST_REQUIRE( git_repository_head( &headRef, repo ) == GIT_OK );
408
409 git_commit* lastCommit = nullptr;
410 git_reference_peel( (git_object**) &lastCommit, headRef, GIT_OBJECT_COMMIT );
411
412 git_commit* parentCommit = nullptr;
413 git_commit_parent( &parentCommit, lastCommit, 0 );
414
415 git_tree* parentTree = nullptr;
416 git_tree* lastTree = nullptr;
417 git_commit_tree( &parentTree, parentCommit );
418 git_commit_tree( &lastTree, lastCommit );
419
420 git_diff* diff = nullptr;
421 git_diff_tree_to_tree( &diff, repo, parentTree, lastTree, nullptr );
422
423 size_t deltas = git_diff_num_deltas( diff );
424
425 for( size_t ii = 0; ii < deltas; ++ii )
426 {
427 const git_diff_delta* delta = git_diff_get_delta( diff, ii );
428 const char* path = delta->new_file.path ? delta->new_file.path : delta->old_file.path;
429
430 if( !path )
431 continue;
432
433 wxString absPath = repoWorkDir + wxString::FromUTF8( path );
434
435 if( !KIGIT::PROJECT_GIT_UTILS::IsWithinProjectPath( absPath, projectPath ) )
436 continue;
437
438 modifiedFiles[wxString::FromUTF8( path )] |= GIT_STATUS_INDEX_MODIFIED;
439 }
440
441 BOOST_CHECK_MESSAGE( modifiedFiles.count( wxT( "outside.txt" ) ) == 0,
442 "outside.txt must not leak into the amend checklist" );
443 BOOST_CHECK_MESSAGE( modifiedFiles.count( wxT( "proj/proj.kicad_sch" ) ) == 1,
444 "the project file should still appear in the amend checklist" );
445
446 git_diff_free( diff );
447 git_tree_free( lastTree );
448 git_tree_free( parentTree );
449 git_commit_free( parentCommit );
450 git_commit_free( lastCommit );
451 git_reference_free( headRef );
452 git_repository_free( repo );
453}
454
455
456
462BOOST_AUTO_TEST_CASE( GetUpstreamShorthand_NoUpstreamFallsBackToRemoteSlashBranch )
463{
464 BOOST_TEST_REQUIRE( ready() );
465
466 git_repository* repo = openRepo();
467 BOOST_TEST_REQUIRE( repo );
468
469 // No upstream config, no remote-tracking ref. Should still produce a useful
470 // shorthand by combining the default remote name with the current branch.
471 clearUpstreamConfig( repo, wxT( "master" ) );
472
473 KIGIT_COMMON common( repo );
474 BOOST_CHECK_EQUAL( common.GetUpstreamShorthand(), wxString( "origin/master" ) );
475
476 git_repository_free( repo );
477}
478
479
488BOOST_AUTO_TEST_CASE( GetDifferentFiles_SameTreeAmendHidesAheadFiles )
489{
490 BOOST_TEST_REQUIRE( ready() );
491
492 git_repository* repo = openRepo();
493 BOOST_TEST_REQUIRE( repo );
494
495 // Add a second commit so the merge-base in GetDifferentFiles isn't the root.
496 git_oid c1 = createCommit( repo, wxT( "file.txt" ), wxT( "edited\n" ), wxT( "Edit file" ) );
497
498 // Simulate "pushed": create the remote-tracking ref pointing at C1.
499 git_reference* trackingRef = nullptr;
500 git_reference_create( &trackingRef, repo, "refs/remotes/origin/master", &c1, 1, nullptr );
501
502 if( trackingRef )
503 git_reference_free( trackingRef );
504
505 addOrigin( repo );
506 setUpstreamConfig( repo, wxT( "master" ) );
507
508 // Amend C1 message-only. Same tree, different commit OID.
509 git_reference* headRef = nullptr;
510 git_repository_head( &headRef, repo );
511
512 git_commit* headCommit = nullptr;
513 git_reference_peel( (git_object**) &headCommit, headRef, GIT_OBJECT_COMMIT );
514
515 git_tree* tree = nullptr;
516 git_commit_tree( &tree, headCommit );
517
518 git_signature* sig = nullptr;
519 git_signature_now( &sig, TEST_AUTHOR_NAME, TEST_AUTHOR_EMAIL );
520
521 git_oid amendedOid;
522 int rc = git_commit_amend( &amendedOid, headCommit, "HEAD", sig, sig, nullptr, "Edit file (message-only amend)",
523 tree );
524 BOOST_TEST_REQUIRE( rc == 0 );
525
526 git_tree_free( tree );
527 git_commit_free( headCommit );
528 git_reference_free( headRef );
529 git_signature_free( sig );
530
531 // Now: HEAD points at C1', upstream still at C1, both have the same tree.
532 // Pre-filter: AHEAD = { file.txt } (touched in C1 relative to root).
533 // Post-filter (the fix under test): AHEAD = {} because HEAD's blob == upstream's blob.
534 KIGIT_COMMON common( repo );
535 auto [ahead, behind] = common.GetDifferentFiles();
536
537 BOOST_CHECK_MESSAGE( ahead.empty(), "Message-only amend should not report any AHEAD files; got "
538 + std::to_string( ahead.size() ) );
539 BOOST_CHECK_MESSAGE( behind.empty(), "Message-only amend should not report any BEHIND files; got "
540 + std::to_string( behind.size() ) );
541
542 git_repository_free( repo );
543}
544
545
551BOOST_AUTO_TEST_CASE( GetDifferentFiles_DifferentTreeAmendKeepsAheadFile )
552{
553 BOOST_TEST_REQUIRE( ready() );
554
555 git_repository* repo = openRepo();
556 BOOST_TEST_REQUIRE( repo );
557
558 git_oid c1 = createCommit( repo, wxT( "file.txt" ), wxT( "edited\n" ), wxT( "Edit file" ) );
559
560 git_reference* trackingRef = nullptr;
561 git_reference_create( &trackingRef, repo, "refs/remotes/origin/master", &c1, 1, nullptr );
562
563 if( trackingRef )
564 git_reference_free( trackingRef );
565
566 addOrigin( repo );
567 setUpstreamConfig( repo, wxT( "master" ) );
568
569 // Make a new commit on top with different content (replacing C1 effectively).
570 // Use the same path so the file remains "the same file" to the diff.
571 createCommit( repo, wxT( "file.txt" ), wxT( "edited again\n" ), wxT( "Re-edit file" ) );
572
573 KIGIT_COMMON common( repo );
574 auto [ahead, behind] = common.GetDifferentFiles();
575
576 BOOST_CHECK_MESSAGE( ahead.count( wxT( "file.txt" ) ) == 1,
577 "AHEAD should contain file.txt when content differs from upstream" );
578
579 git_repository_free( repo );
580}
581
582
590BOOST_AUTO_TEST_CASE( PerformPull_NoUpstreamConfig_FallbackSucceedsAndWritesUpstream )
591{
592 BOOST_TEST_REQUIRE( ready() );
593
594 git_repository* repo = openRepo();
595 BOOST_TEST_REQUIRE( repo );
596
597 addOrigin( repo );
598 clearUpstreamConfig( repo, wxT( "master" ) );
599
600 KIGIT_COMMON common( repo );
601 GIT_PULL_HANDLER handler( &common );
602
603 PullResult result = handler.PerformPull();
604
605 // Without the fallback, this would be PullResult::Error with "Could not lookup commit".
606 BOOST_CHECK_MESSAGE( result == PullResult::UpToDate || result == PullResult::Success
608 "Pull without upstream config should succeed via fallback; got "
609 + std::to_string( static_cast<int>( result ) ) + ", err='"
610 + handler.GetErrorString().ToStdString() + "'" );
611
612 // The fallback also wires up tracking so subsequent pulls take the normal path.
613 BOOST_CHECK_EQUAL( readConfig( repo, wxT( "branch.master.remote" ) ), wxString( "origin" ) );
614 BOOST_CHECK_EQUAL( readConfig( repo, wxT( "branch.master.merge" ) ), wxString( "refs/heads/master" ) );
615
616 git_repository_free( repo );
617}
618
619
624BOOST_AUTO_TEST_CASE( PerformPull_RebaseConfigReplaysLocalCommit )
625{
626 BOOST_TEST_REQUIRE( ready() );
627
628 git_repository* repo = openRepo();
629 BOOST_TEST_REQUIRE( repo );
630
631 addOrigin( repo );
632 setUpstreamConfig( repo, wxT( "master" ) );
633
634 git_object* initial = nullptr;
635 BOOST_TEST_REQUIRE( git_revparse_single( &initial, repo, "HEAD" ) == 0 );
636
637 KIGIT_COMMON common( repo );
638 git_oid remoteOid = createCommit( repo, wxT( "remote.txt" ), wxT( "remote\n" ), wxT( "Remote commit" ) );
639
640 {
641 GIT_PUSH_HANDLER pushHandler( &common );
642 BOOST_TEST_REQUIRE( static_cast<int>( pushHandler.PerformPush() ) == static_cast<int>( PushResult::Success ) );
643 }
644
645 BOOST_TEST_REQUIRE( git_reset( repo, initial, GIT_RESET_HARD, nullptr ) == 0 );
646 git_object_free( initial );
647
648 createCommit( repo, wxT( "local.txt" ), wxT( "local\n" ), wxT( "Local commit" ) );
649
650 git_config* cfg = nullptr;
651 BOOST_TEST_REQUIRE( git_repository_config( &cfg, repo ) == 0 );
652 git_config_set_bool( cfg, "pull.rebase", 1 );
653 git_config_free( cfg );
654
655 GIT_PULL_HANDLER handler( &common );
656 PullResult result = handler.PerformPull();
657
658 BOOST_CHECK_MESSAGE( result == PullResult::Success,
659 "Rebase pull failed: " + handler.GetErrorString().ToStdString() );
660 BOOST_CHECK_EQUAL( git_repository_state( repo ), static_cast<int>( GIT_REPOSITORY_STATE_NONE ) );
661
662 git_commit* head = nullptr;
663 BOOST_TEST_REQUIRE( git_revparse_single( (git_object**) &head, repo, "HEAD" ) == 0 );
664 BOOST_CHECK( git_oid_equal( git_commit_parent_id( head, 0 ), &remoteOid ) );
665 git_commit_free( head );
666
667 git_repository_free( repo );
668}
669
670
676BOOST_AUTO_TEST_CASE( Push_FirstPushSetsUpstreamTracking )
677{
678 BOOST_TEST_REQUIRE( ready() );
679
680 git_repository* repo = openRepo();
681 BOOST_TEST_REQUIRE( repo );
682
683 // Add a new local commit so there is something to actually push beyond the
684 // initial state shared with the bare remote.
685 createCommit( repo, wxT( "file.txt" ), wxT( "second\n" ), wxT( "Second commit" ) );
686
687 addOrigin( repo );
688 clearUpstreamConfig( repo, wxT( "master" ) );
689
690 KIGIT_COMMON common( repo );
691 GIT_PUSH_HANDLER handler( &common );
692
693 PushResult result = handler.PerformPush();
694 BOOST_TEST_REQUIRE( static_cast<int>( result ) == static_cast<int>( PushResult::Success ) );
695
696 BOOST_CHECK_EQUAL( readConfig( repo, wxT( "branch.master.remote" ) ), wxString( "origin" ) );
697 BOOST_CHECK_EQUAL( readConfig( repo, wxT( "branch.master.merge" ) ), wxString( "refs/heads/master" ) );
698
699 git_repository_free( repo );
700}
701
702
707BOOST_AUTO_TEST_CASE( Amend_MessageOnlyRewritesHeadKeepsTree )
708{
709 BOOST_TEST_REQUIRE( ready() );
710
711 git_repository* repo = openRepo();
712 BOOST_TEST_REQUIRE( repo );
713
714 git_reference* headRefBefore = nullptr;
715 git_repository_head( &headRefBefore, repo );
716 git_oid oidBefore = *git_reference_target( headRefBefore );
717
718 git_commit* commitBefore = nullptr;
719 git_reference_peel( (git_object**) &commitBefore, headRefBefore, GIT_OBJECT_COMMIT );
720 git_oid treeBefore = *git_commit_tree_id( commitBefore );
721
722 git_commit_free( commitBefore );
723 git_reference_free( headRefBefore );
724
725 GIT_COMMIT_HANDLER handler( repo );
726 CommitResult result = handler.PerformAmend( {}, wxT( "Amended message" ), TEST_AUTHOR_NAME, TEST_AUTHOR_EMAIL );
727 BOOST_CHECK_EQUAL( static_cast<int>( result ), static_cast<int>( CommitResult::Success ) );
728
729 git_reference* headRefAfter = nullptr;
730 git_repository_head( &headRefAfter, repo );
731 git_oid oidAfter = *git_reference_target( headRefAfter );
732
733 git_commit* commitAfter = nullptr;
734 git_reference_peel( (git_object**) &commitAfter, headRefAfter, GIT_OBJECT_COMMIT );
735 git_oid treeAfter = *git_commit_tree_id( commitAfter );
736
737 BOOST_CHECK( !git_oid_equal( &oidBefore, &oidAfter ) );
738 BOOST_CHECK( git_oid_equal( &treeBefore, &treeAfter ) );
739
740 wxString amendedMsg = wxString::FromUTF8( git_commit_message( commitAfter ) );
741 BOOST_CHECK_EQUAL( amendedMsg.Trim(), wxString( "Amended message" ) );
742
743 git_commit_free( commitAfter );
744 git_reference_free( headRefAfter );
745 git_repository_free( repo );
746}
747
748
752BOOST_AUTO_TEST_CASE( Amend_StagedFileChangesTreeAndKeepsParent )
753{
754 BOOST_TEST_REQUIRE( ready() );
755
756 git_repository* repo = openRepo();
757 BOOST_TEST_REQUIRE( repo );
758
759 git_reference* headRefBefore = nullptr;
760 git_repository_head( &headRefBefore, repo );
761
762 git_commit* commitBefore = nullptr;
763 git_reference_peel( (git_object**) &commitBefore, headRefBefore, GIT_OBJECT_COMMIT );
764 git_oid treeBefore = *git_commit_tree_id( commitBefore );
765 unsigned int parentCountBefore = git_commit_parentcount( commitBefore );
766
767 git_commit_free( commitBefore );
768 git_reference_free( headRefBefore );
769
770 // Add a new file to the working tree so the amend has something to stage.
771 wxString newFile = repoPath() + wxFileName::GetPathSeparator() + wxT( "new.txt" );
772 {
773 std::ofstream f( newFile.ToStdString() );
774 f << "new content\n";
775 }
776
777 GIT_COMMIT_HANDLER handler( repo );
778 CommitResult result = handler.PerformAmend( { wxT( "new.txt" ) }, wxT( "Amended with file" ), TEST_AUTHOR_NAME,
780 BOOST_CHECK_EQUAL( static_cast<int>( result ), static_cast<int>( CommitResult::Success ) );
781
782 git_reference* headRefAfter = nullptr;
783 git_repository_head( &headRefAfter, repo );
784
785 git_commit* commitAfter = nullptr;
786 git_reference_peel( (git_object**) &commitAfter, headRefAfter, GIT_OBJECT_COMMIT );
787 git_oid treeAfter = *git_commit_tree_id( commitAfter );
788 unsigned int parentCountAfter = git_commit_parentcount( commitAfter );
789
790 BOOST_CHECK( !git_oid_equal( &treeBefore, &treeAfter ) );
791 BOOST_CHECK_EQUAL( parentCountAfter, parentCountBefore );
792
793 // new.txt should now be in HEAD's tree.
794 git_tree* treeObj = nullptr;
795 git_commit_tree( &treeObj, commitAfter );
796
797 const git_tree_entry* entry = git_tree_entry_byname( treeObj, "new.txt" );
798 BOOST_CHECK( entry != nullptr );
799
800 git_tree_free( treeObj );
801 git_commit_free( commitAfter );
802 git_reference_free( headRefAfter );
803 git_repository_free( repo );
804}
805
806
811BOOST_AUTO_TEST_CASE( Amend_UnbornBranchReturnsError )
812{
813 BOOST_TEST_REQUIRE( ready() );
814
815 wxString unbornDir = repoPath() + wxT( "_unborn" );
816 wxFileName::Mkdir( unbornDir, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
817
818 git_repository* unbornRepo = nullptr;
819 BOOST_TEST_REQUIRE( git_repository_init( &unbornRepo, unbornDir.ToUTF8().data(), 0 ) == 0 );
820
821 GIT_COMMIT_HANDLER handler( unbornRepo );
822 CommitResult result = handler.PerformAmend( {}, wxT( "should fail" ), TEST_AUTHOR_NAME, TEST_AUTHOR_EMAIL );
823
824 BOOST_CHECK_EQUAL( static_cast<int>( result ), static_cast<int>( CommitResult::Error ) );
825 BOOST_CHECK( !handler.GetErrorString().IsEmpty() );
826
827 git_repository_free( unbornRepo );
828
829 if( wxFileName::DirExists( unbornDir ) )
830 wxFileName::Rmdir( unbornDir, wxPATH_RMDIR_RECURSIVE );
831}
832
833
838BOOST_AUTO_TEST_CASE( InitializeRepository_SeedsGitignoreWithKicadEntries )
839{
840 BOOST_TEST_REQUIRE( ready() );
841
842 wxString freshDir = repoPath() + wxT( "_fresh" );
843 wxFileName::Mkdir( freshDir, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
844
845 KIGIT_COMMON common( nullptr );
846 GIT_INIT_HANDLER handler( &common );
847
848 InitResult result = handler.InitializeRepository( freshDir );
849 BOOST_CHECK_EQUAL( static_cast<int>( result ), static_cast<int>( InitResult::Success ) );
850
851 wxFileName gitignoreFile( freshDir, wxT( ".gitignore" ) );
852 BOOST_TEST_REQUIRE( gitignoreFile.FileExists() );
853
854 wxString contents;
855 {
856 wxFFile f( gitignoreFile.GetFullPath(), wxT( "r" ) );
857 f.ReadAll( &contents );
858 }
859
860 BOOST_CHECK( contents.Contains( wxT( ".history/" ) ) );
861 BOOST_CHECK( contents.Contains( wxT( "*-backups/" ) ) );
862 BOOST_CHECK( contents.Contains( wxT( "_autosave-*" ) ) );
863 BOOST_CHECK( contents.Contains( wxT( "fp-info-cache" ) ) );
864 BOOST_CHECK( contents.Contains( wxT( "~*.lck" ) ) );
865
866 if( wxFileName::DirExists( freshDir ) )
867 wxFileName::Rmdir( freshDir, wxPATH_RMDIR_RECURSIVE );
868}
869
870
876BOOST_AUTO_TEST_CASE( InitializeRepository_GitignoreDoesNotDuplicateEntries )
877{
878 BOOST_TEST_REQUIRE( ready() );
879
880 wxString freshDir = repoPath() + wxT( "_dedup" );
881 wxFileName::Mkdir( freshDir, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
882
883 wxFileName gitignoreFile( freshDir, wxT( ".gitignore" ) );
884 {
885 wxFFile f( gitignoreFile.GetFullPath(), wxT( "w" ) );
886 f.Write( wxT( "# my custom ignores\n.history/\nbuild/\n" ) );
887 }
888
889 KIGIT_COMMON common( nullptr );
890 GIT_INIT_HANDLER handler( &common );
891
892 InitResult result = handler.InitializeRepository( freshDir );
893 BOOST_CHECK_EQUAL( static_cast<int>( result ), static_cast<int>( InitResult::Success ) );
894
895 wxTextFile tf;
896 BOOST_TEST_REQUIRE( tf.Open( gitignoreFile.GetFullPath() ) );
897
898 int historyCount = 0;
899 int buildCount = 0;
900 int fpInfoCount = 0;
901
902 for( size_t i = 0; i < tf.GetLineCount(); ++i )
903 {
904 wxString line = tf.GetLine( i );
905 line.Trim().Trim( false );
906
907 if( line == wxT( ".history/" ) )
908 historyCount++;
909 else if( line == wxT( "build/" ) )
910 buildCount++;
911 else if( line == wxT( "fp-info-cache" ) )
912 fpInfoCount++;
913 }
914
915 BOOST_CHECK_EQUAL( historyCount, 1 ); // was already present; not duplicated
916 BOOST_CHECK_EQUAL( buildCount, 1 ); // user's custom entry preserved
917 BOOST_CHECK_EQUAL( fpInfoCount, 1 ); // KiCad default that was missing got appended
918
919 if( wxFileName::DirExists( freshDir ) )
920 wxFileName::Rmdir( freshDir, wxPATH_RMDIR_RECURSIVE );
921}
922
923
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