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
30#include <git/git_backend.h>
31#include <git/libgit_backend.h>
44
45#include <git2.h>
46
47#include <project.h>
50
51#ifndef __WINDOWS__
52#include <sys/stat.h>
53#include <unistd.h>
54#endif
55
56#include <fstream>
57#include <future>
58#include <set>
59#include <thread>
60
61#include <wx/ffile.h>
62#include <wx/file.h>
63#include <wx/filename.h>
64#include <wx/textfile.h>
65
66
67static const char* TEST_AUTHOR_NAME = "Test Author";
69
70
77{
79 {
80 m_repoPath = TempPath() + wxFileName::GetPathSeparator() + wxT( "repo" );
81 m_remotePath = TempPath() + wxFileName::GetPathSeparator() + wxT( "remote.git" );
83 }
84
85
86 bool ready() const { return m_ready; }
87 const wxString& repoPath() const { return m_repoPath; }
88 const wxString& tempBase() const { return TempPath(); }
89 const wxString& remotePath() const { return m_remotePath; }
90
91
94 {
95 git_repository* repo = nullptr;
96 git_repository_open( &repo, m_repoPath.ToUTF8().data() );
97 return repo;
98 }
99
100
102 git_oid createCommit( git_repository* aRepo, const wxString& aFile, const wxString& aContent,
103 const wxString& aMessage )
104 {
105 KI_TEST::WriteGitFile( m_repoPath, aFile, aContent.utf8_string() );
106 return KI_TEST::CommitGitFile( aRepo, aFile, aMessage );
107 }
108
109
111 git_oid createDetachedCommit( git_repository* aRepo, const git_oid& aParentOid, const wxString& aFile,
112 const std::string& aContent, const char* aMessage )
113 {
114 git_oid result = {};
115
116 git_commit* parentCommit = nullptr;
117 BOOST_REQUIRE_EQUAL( git_commit_lookup( &parentCommit, aRepo, &aParentOid ), GIT_OK );
118
119 git_oid blobOid;
120 BOOST_REQUIRE_EQUAL( git_blob_create_from_buffer( &blobOid, aRepo, aContent.data(), aContent.size() ), GIT_OK );
121
122 git_tree* parentTree = nullptr;
123 BOOST_REQUIRE_EQUAL( git_commit_tree( &parentTree, parentCommit ), GIT_OK );
124
125 git_treebuilder* builder = nullptr;
126 BOOST_REQUIRE_EQUAL( git_treebuilder_new( &builder, aRepo, parentTree ), GIT_OK );
127 BOOST_REQUIRE_EQUAL(
128 git_treebuilder_insert( nullptr, builder, aFile.ToUTF8().data(), &blobOid, GIT_FILEMODE_BLOB ),
129 GIT_OK );
130
131 git_oid treeOid;
132 BOOST_REQUIRE_EQUAL( git_treebuilder_write( &treeOid, builder ), GIT_OK );
133 git_treebuilder_free( builder );
134 git_tree_free( parentTree );
135
136 git_tree* tree = nullptr;
137 BOOST_REQUIRE_EQUAL( git_tree_lookup( &tree, aRepo, &treeOid ), GIT_OK );
138
139 git_signature* sig = nullptr;
140 BOOST_REQUIRE_EQUAL( git_signature_now( &sig, TEST_AUTHOR_NAME, TEST_AUTHOR_EMAIL ), GIT_OK );
141
142 const git_commit* parents[1] = { parentCommit };
143 BOOST_REQUIRE_EQUAL(
144 git_commit_create( &result, aRepo, nullptr, sig, sig, nullptr, aMessage, tree, 1, parents ), GIT_OK );
145
146 git_signature_free( sig );
147 git_tree_free( tree );
148 git_commit_free( parentCommit );
149 return result;
150 }
151
153 void writeFetchHead( const git_oid& aOid )
154 {
155 char hex[GIT_OID_HEXSZ + 1] = { 0 };
156 git_oid_tostr( hex, sizeof( hex ), &aOid );
157
158 wxString fetchHead = repoPath() + wxFileName::GetPathSeparator() + wxT( ".git" )
159 + wxFileName::GetPathSeparator() + wxT( "FETCH_HEAD" );
160 std::ofstream f( fetchHead.ToStdString() );
161 f << hex << "\t\tbranch 'master' of test\n";
162 }
163
166 {
167 git_remote* remote = nullptr;
168 int rc = git_remote_create( &remote, aRepo, "origin", m_remotePath.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
237 {
238 KI_TEST::WriteGitFile( tempBase(), wxS( "global-ignore" ), "" );
239 const wxString path = tempBase() + wxS( "/global-ignore" );
240 git_config* rawConfig = nullptr;
241 BOOST_REQUIRE_EQUAL( git_repository_config( &rawConfig, aRepo ), GIT_OK );
242 KIGIT::GitConfigPtr config( rawConfig );
243 BOOST_REQUIRE_EQUAL( git_config_set_string( config.get(), "core.excludesfile", path.utf8_str() ), GIT_OK );
244 }
245
246
247private:
249 {
252
253 git_config* cfg = nullptr;
254 BOOST_REQUIRE_EQUAL( git_repository_config( &cfg, repo.get() ), GIT_OK );
256 BOOST_REQUIRE_EQUAL( git_config_set_bool( cfg, "pull.rebase", 0 ), GIT_OK );
257
258 createCommit( repo.get(), wxT( "file.txt" ), wxT( "initial\n" ), wxT( "Initial commit" ) );
259 return true;
260 }
261
262
264 {
265 git_clone_options opts;
266 git_clone_init_options( &opts, GIT_CLONE_OPTIONS_VERSION );
267 opts.bare = 1;
268
269 git_repository* bare = nullptr;
270 int rc = git_clone( &bare, m_repoPath.ToUTF8().data(), m_remotePath.ToUTF8().data(), &opts );
271
272 if( bare )
273 git_repository_free( bare );
274
275 return rc == 0;
276 }
277
278
279 wxString m_repoPath;
280 wxString m_remotePath;
282};
283
284
285BOOST_FIXTURE_TEST_SUITE( LibgitBackend, GIT_BACKEND_FIXTURE )
286
287
288// The single scope predicate behind every checklist, so a file elsewhere in the repository
289// cannot leak into a project commit (issue #15910).
290// Getting changes keeps whichever FETCH_HEAD entry libgit2 marked to merge, without reading
291// its name. That is only sound while at most one entry is ever marked, so the assumption is
292// checked here rather than left for a reader to take on trust.
293BOOST_AUTO_TEST_CASE( FetchHeadMarksAtMostOneBranchToMerge )
294{
295 BOOST_TEST_REQUIRE( ready() );
296
297 git_repository* repo = openRepo();
298 BOOST_TEST_REQUIRE( repo );
299 KIGIT::GitRepositoryPtr repoPtr( repo );
300 BOOST_REQUIRE( addOrigin( repo ) );
301
302 // Two more branches on the remote, named either side of the current one alphabetically.
303 {
304 git_repository* bare = nullptr;
305 BOOST_REQUIRE_EQUAL( git_repository_open( &bare, remotePath().ToUTF8().data() ), GIT_OK );
306 KIGIT::GitRepositoryPtr barePtr( bare );
307
308 git_oid head;
309 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &head, bare, "HEAD" ), GIT_OK );
310
311 for( const char* name : { "refs/heads/aaa", "refs/heads/zzz" } )
312 {
313 git_reference* ref = nullptr;
314 BOOST_REQUIRE_EQUAL( git_reference_create( &ref, bare, name, &head, 1, nullptr ), GIT_OK );
315 git_reference_free( ref );
316 }
317 }
318
319 struct MARKED
320 {
321 int total = 0;
322 std::vector<std::string> names;
323 };
324
325 auto collect = []( git_repository* aRepo )
326 {
327 MARKED marked;
328
329 auto cb = []( const char* aName, const char*, const git_oid*, unsigned int aIsMerge, void* aPayload ) -> int
330 {
331 MARKED* m = static_cast<MARKED*>( aPayload );
332 m->total++;
333
334 if( aIsMerge )
335 m->names.push_back( std::string( aName ? aName : "" ) );
336
337 return 0;
338 };
339
340 BOOST_REQUIRE_EQUAL( git_repository_fetchhead_foreach( aRepo, cb, &marked ), GIT_OK );
341 return marked;
342 };
343
344 KIGIT_COMMON common( repo );
345
346 // Add Version Control wires up origin without per-branch tracking. Nothing is marked, so the
347 // by-name fallback below the callback is what chooses the branch.
348 {
349 BOOST_REQUIRE( clearUpstreamConfig( repo, wxT( "master" ) ) );
350
351 GIT_PULL_HANDLER handler( &common );
352 BOOST_REQUIRE_MESSAGE( handler.PerformFetch(), handler.GetErrorString() );
353
354 MARKED marked = collect( repo );
355 BOOST_CHECK_EQUAL( marked.total, 3 );
356 BOOST_CHECK_EQUAL( marked.names.size(), 0u );
357 }
358
359 // With tracking configured exactly one is marked, and it is the branch being tracked, so
360 // keeping the last one keeps the right one.
361 {
362 BOOST_REQUIRE( setUpstreamConfig( repo, wxT( "master" ) ) );
363
364 GIT_PULL_HANDLER handler( &common );
365 BOOST_REQUIRE_MESSAGE( handler.PerformFetch(), handler.GetErrorString() );
366
367 MARKED marked = collect( repo );
368 BOOST_CHECK_EQUAL( marked.total, 3 );
369 BOOST_REQUIRE_EQUAL( marked.names.size(), 1u );
370 BOOST_CHECK_EQUAL( marked.names[0], "refs/heads/master" );
371 }
372}
373
374
375// KiCad cannot read or write Git LFS, so an operation that would put a pointer file where a
376// design file belongs is refused. The remote can start using LFS at any time, so the commit
377// being merged has to be judged, not the one already held.
378BOOST_AUTO_TEST_CASE( PerformPull_RefusesACommitThatBringsGitLfs )
379{
380 BOOST_TEST_REQUIRE( ready() );
381
382 git_repository* repo = openRepo();
383 BOOST_TEST_REQUIRE( repo );
384 KIGIT::GitRepositoryPtr repoPtr( repo );
385 BOOST_REQUIRE( addOrigin( repo ) );
386 BOOST_REQUIRE( setUpstreamConfig( repo, wxT( "master" ) ) );
387
388 // The remote starts keeping design files in LFS, and moves master on to that commit.
389 {
390 git_repository* bare = nullptr;
391 BOOST_REQUIRE_EQUAL( git_repository_open( &bare, remotePath().ToUTF8().data() ), GIT_OK );
392 KIGIT::GitRepositoryPtr barePtr( bare );
393
394 git_oid base;
395 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &base, bare, "refs/heads/master" ), GIT_OK );
396
397 git_oid attrs = createDetachedCommit( bare, base, wxT( ".gitattributes" ), "*.kicad_pcb filter=lfs -text\n",
398 "Track boards with LFS" );
399
400 git_oid board =
401 createDetachedCommit( bare, attrs, wxT( "board.kicad_pcb" ),
402 "version https://git-lfs.github.com/spec/v1\n"
403 "oid sha256:0000000000000000000000000000000000000000000000000000000000000000\n"
404 "size 1\n",
405 "Add the board" );
406
407 git_reference* ref = nullptr;
408 BOOST_REQUIRE_EQUAL( git_reference_create( &ref, bare, "refs/heads/master", &board, 1, nullptr ), GIT_OK );
409 git_reference_free( ref );
410 }
411
412 KIGIT_COMMON common( repo, repoPath() );
413 GIT_PULL_HANDLER handler( &common );
414
415 PullResult result = handler.PerformPull();
416
417 BOOST_CHECK_MESSAGE( result != PullResult::Success && result != PullResult::FastForward,
418 "the pull should refuse a commit that brings Git LFS in" );
419 BOOST_CHECK_MESSAGE( handler.GetErrorString().Upper().Contains( wxS( "LFS" ) ),
420 "the refusal should name Git LFS, got: " + handler.GetErrorString().ToStdString() );
421
422 // Above all, the pointer text must not have been written where the board belongs.
423 BOOST_CHECK( !wxFileName::FileExists( repoPath() + wxFileName::GetPathSeparator() + wxT( "board.kicad_pcb" ) ) );
424}
425
426
427// Cancel has to reach a push that is already writing objects, as it does a fetch that is
428// already receiving them. libgit2 abandons the transfer when the progress callback refuses.
429BOOST_AUTO_TEST_CASE( PushProgressStopsWhenTheOperationIsCancelled )
430{
431 BOOST_TEST_REQUIRE( ready() );
432
433 git_repository* repo = openRepo();
434 BOOST_TEST_REQUIRE( repo );
435 KIGIT::GitRepositoryPtr repoPtr( repo );
436
437 KIGIT_COMMON common( repo );
438 GIT_PUSH_HANDLER handler( &common );
439
440 // Running normally, the push carries on.
441 BOOST_CHECK_EQUAL( push_transfer_progress_cb( 1, 10, 128, &handler ), 0 );
442
443 handler.CancelThisOperation();
444
445 BOOST_CHECK_MESSAGE( push_transfer_progress_cb( 2, 10, 256, &handler ) != 0,
446 "a cancelled push should stop at its next progress tick" );
447}
448
449
450BOOST_AUTO_TEST_CASE( IsWithinProjectPath_ScopesToProjectDirectory )
451{
452 const wxString project = wxT( "/repo/proj/" );
453
454 // Files inside the project directory are in scope.
455 BOOST_CHECK( KIGIT::PROJECT_GIT_UTILS::IsWithinProjectPath( wxT( "/repo/proj/proj.kicad_sch" ), project ) );
456 BOOST_CHECK( KIGIT::PROJECT_GIT_UTILS::IsWithinProjectPath( wxT( "/repo/proj/sub/board.kicad_pcb" ), project ) );
457
458 // Files elsewhere in the repository are not.
459 BOOST_CHECK( !KIGIT::PROJECT_GIT_UTILS::IsWithinProjectPath( wxT( "/repo/outside.txt" ), project ) );
460 BOOST_CHECK( !KIGIT::PROJECT_GIT_UTILS::IsWithinProjectPath( wxT( "/repo/other/f.txt" ), project ) );
461
462 // A sibling whose name merely shares the project's prefix must not match.
463 BOOST_CHECK( !KIGIT::PROJECT_GIT_UTILS::IsWithinProjectPath( wxT( "/repo/proj-extra/f.txt" ), project ) );
464
465 // The predicate normalizes a missing trailing separator before comparing, so the
466 // prefix collision above is still rejected when the caller omits the separator.
467 BOOST_CHECK( KIGIT::PROJECT_GIT_UTILS::IsWithinProjectPath( wxT( "/repo/proj/f.txt" ), wxT( "/repo/proj" ) ) );
468 BOOST_CHECK( !KIGIT::PROJECT_GIT_UTILS::IsWithinProjectPath( wxT( "/repo/proj-extra/f.txt" ),
469 wxT( "/repo/proj" ) ) );
470
471#ifdef __WINDOWS__
472 BOOST_CHECK( KIGIT::PROJECT_GIT_UTILS::IsWithinProjectPath( wxT( "C:\\repo\\proj/file.kicad_sch" ),
473 wxT( "C:\\repo\\proj\\" ) ) );
474 BOOST_CHECK( !KIGIT::PROJECT_GIT_UTILS::IsWithinProjectPath( wxT( "C:\\repo\\proj-extra/file.kicad_sch" ),
475 wxT( "C:\\repo\\proj\\" ) ) );
476#endif
477
478 // An empty project path matches nothing rather than the whole repository.
479 BOOST_CHECK( !KIGIT::PROJECT_GIT_UTILS::IsWithinProjectPath( wxT( "/repo/proj/f.txt" ), wxEmptyString ) );
480}
481
482
483// A message-only amend leaves HEAD and upstream at different OIDs with identical trees. Without
484// the content-aware filter every file touched since the merge-base showed a clock icon.
485BOOST_AUTO_TEST_CASE( GetDifferentFiles_SameTreeAmendHidesAheadFiles )
486{
487 BOOST_TEST_REQUIRE( ready() );
488
489 git_repository* repo = openRepo();
490 BOOST_TEST_REQUIRE( repo );
491
492 // Add a second commit so the merge-base in GetDifferentFiles isn't the root.
493 git_oid c1 = createCommit( repo, wxT( "file.txt" ), wxT( "edited\n" ), wxT( "Edit file" ) );
494
495 // Simulate "pushed": create the remote-tracking ref pointing at C1.
496 git_reference* trackingRef = nullptr;
497 git_reference_create( &trackingRef, repo, "refs/remotes/origin/master", &c1, 1, nullptr );
498
499 if( trackingRef )
500 git_reference_free( trackingRef );
501
502 addOrigin( repo );
503 setUpstreamConfig( repo, wxT( "master" ) );
504
505 // Amend C1 message-only. Same tree, different commit OID.
506 git_reference* headRef = nullptr;
507 git_repository_head( &headRef, repo );
508
509 git_commit* headCommit = nullptr;
510 git_reference_peel( (git_object**) &headCommit, headRef, GIT_OBJECT_COMMIT );
511
512 git_tree* tree = nullptr;
513 git_commit_tree( &tree, headCommit );
514
515 git_signature* sig = nullptr;
516 git_signature_now( &sig, TEST_AUTHOR_NAME, TEST_AUTHOR_EMAIL );
517
518 git_oid amendedOid;
519 int rc = git_commit_amend( &amendedOid, headCommit, "HEAD", sig, sig, nullptr, "Edit file (message-only amend)",
520 tree );
521 BOOST_TEST_REQUIRE( rc == 0 );
522
523 git_tree_free( tree );
524 git_commit_free( headCommit );
525 git_reference_free( headRef );
526 git_signature_free( sig );
527
528 // Now: HEAD points at C1', upstream still at C1, both have the same tree.
529 // Pre-filter: AHEAD = { file.txt } (touched in C1 relative to root).
530 // Post-filter (the fix under test): AHEAD = {} because HEAD's blob == upstream's blob.
531 KIGIT_COMMON common( repo );
532 auto [ahead, behind] = common.GetDifferentFiles();
533
534 BOOST_CHECK_MESSAGE( ahead.empty(), "Message-only amend should not report any AHEAD files; got "
535 + std::to_string( ahead.size() ) );
536 BOOST_CHECK_MESSAGE( behind.empty(), "Message-only amend should not report any BEHIND files; got "
537 + std::to_string( behind.size() ) );
538
539 git_repository_free( repo );
540}
541
542
543// Positive control for the content-aware filter, which must not over-filter.
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
576// origin without per-branch tracking leaves FETCH_HEAD unmarked, which used to fail (#24332).
577BOOST_AUTO_TEST_CASE( PerformPull_NoUpstreamConfig_FallbackSucceedsAndWritesUpstream )
578{
579 BOOST_TEST_REQUIRE( ready() );
580
581 git_repository* repo = openRepo();
582 BOOST_TEST_REQUIRE( repo );
583
584 addOrigin( repo );
585 clearUpstreamConfig( repo, wxT( "master" ) );
586
587 KIGIT_COMMON common( repo );
588 GIT_PULL_HANDLER handler( &common );
589
590 PullResult result = handler.PerformPull();
591
592 // Without the fallback, this would be PullResult::Error with "Could not lookup commit".
593 BOOST_CHECK_MESSAGE( result == PullResult::UpToDate || result == PullResult::Success
595 "Pull without upstream config should succeed via fallback; got "
596 + std::to_string( static_cast<int>( result ) ) + ", err='"
597 + handler.GetErrorString().ToStdString() + "'" );
598
599 // The fallback also wires up tracking so subsequent pulls take the normal path.
600 BOOST_CHECK_EQUAL( readConfig( repo, wxT( "branch.master.remote" ) ), wxString( "origin" ) );
601 BOOST_CHECK_EQUAL( readConfig( repo, wxT( "branch.master.merge" ) ), wxString( "refs/heads/master" ) );
602
603 git_repository_free( repo );
604}
605
606
611BOOST_AUTO_TEST_CASE( PerformPull_RebaseConfigReplaysLocalCommit )
612{
613 BOOST_TEST_REQUIRE( ready() );
614
615 git_repository* repo = openRepo();
616 BOOST_TEST_REQUIRE( repo );
617
618 addOrigin( repo );
619 setUpstreamConfig( repo, wxT( "master" ) );
620
621 git_object* initial = nullptr;
622 BOOST_TEST_REQUIRE( git_revparse_single( &initial, repo, "HEAD" ) == 0 );
623
624 KIGIT_COMMON common( repo );
625 git_oid remoteOid = createCommit( repo, wxT( "remote.txt" ), wxT( "remote\n" ), wxT( "Remote commit" ) );
626
627 {
628 GIT_PUSH_HANDLER pushHandler( &common );
629 BOOST_TEST_REQUIRE( static_cast<int>( pushHandler.PerformPush() ) == static_cast<int>( PushResult::Success ) );
630 }
631
632 BOOST_TEST_REQUIRE( git_reset( repo, initial, GIT_RESET_HARD, nullptr ) == 0 );
633 git_object_free( initial );
634
635 createCommit( repo, wxT( "local.txt" ), wxT( "local\n" ), wxT( "Local commit" ) );
636
637 git_config* cfg = nullptr;
638 BOOST_TEST_REQUIRE( git_repository_config( &cfg, repo ) == 0 );
639 git_config_set_bool( cfg, "pull.rebase", 1 );
640 git_config_free( cfg );
641
642 GIT_PULL_HANDLER handler( &common );
643 PullResult result = handler.PerformPull();
644
645 BOOST_CHECK_MESSAGE( result == PullResult::Success,
646 "Rebase pull failed: " + handler.GetErrorString().ToStdString() );
647 BOOST_CHECK_EQUAL( git_repository_state( repo ), static_cast<int>( GIT_REPOSITORY_STATE_NONE ) );
648
649 git_commit* head = nullptr;
650 BOOST_TEST_REQUIRE( git_revparse_single( (git_object**) &head, repo, "HEAD" ) == 0 );
651 BOOST_CHECK( git_oid_equal( git_commit_parent_id( head, 0 ), &remoteOid ) );
652 git_commit_free( head );
653
654 git_repository_free( repo );
655}
656
657
658// Tracking config is what lets later operations take the FETCH_HEAD path.
659BOOST_AUTO_TEST_CASE( Push_FirstPushSetsUpstreamTracking )
660{
661 BOOST_TEST_REQUIRE( ready() );
662
663 git_repository* repo = openRepo();
664 BOOST_TEST_REQUIRE( repo );
665
666 // Add a new local commit so there is something to actually push beyond the
667 // initial state shared with the bare remote.
668 createCommit( repo, wxT( "file.txt" ), wxT( "second\n" ), wxT( "Second commit" ) );
669
670 addOrigin( repo );
671 clearUpstreamConfig( repo, wxT( "master" ) );
672
673 KIGIT_COMMON common( repo );
674 GIT_PUSH_HANDLER handler( &common );
675
676 PushResult result = handler.PerformPush();
677 BOOST_TEST_REQUIRE( static_cast<int>( result ) == static_cast<int>( PushResult::Success ) );
678
679 BOOST_CHECK_EQUAL( readConfig( repo, wxT( "branch.master.remote" ) ), wxString( "origin" ) );
680 BOOST_CHECK_EQUAL( readConfig( repo, wxT( "branch.master.merge" ) ), wxString( "refs/heads/master" ) );
681
682 git_repository_free( repo );
683}
684
685
686// A detached HEAD is not a branch, so its name is the literal "HEAD" and there is no branch on
687// the remote for it to go to.
688BOOST_AUTO_TEST_CASE( Push_RefusesOnADetachedHead )
689{
690 BOOST_TEST_REQUIRE( ready() );
691
692 git_repository* repo = openRepo();
693 BOOST_TEST_REQUIRE( repo );
694
695 createCommit( repo, wxT( "file.txt" ), wxT( "second\n" ), wxT( "Second commit" ) );
696 addOrigin( repo );
697
698 git_oid headOid;
699 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &headOid, repo, "HEAD" ), GIT_OK );
700 BOOST_REQUIRE_EQUAL( git_repository_set_head_detached( repo, &headOid ), GIT_OK );
701
702 KIGIT_COMMON common( repo );
703 GIT_PUSH_HANDLER handler( &common );
704
705 BOOST_CHECK_EQUAL( static_cast<int>( handler.PerformPush() ), static_cast<int>( PushResult::Error ) );
706 BOOST_CHECK_MESSAGE( handler.GetErrorString().Contains( wxS( "branch" ) ),
707 "the refusal should say the project is not on a branch, got: "
708 + handler.GetErrorString().ToStdString() );
709
710 git_repository_free( repo );
711}
712
713
714// Behind the upstream. libgit2 reports it through the return code, not the message.
715BOOST_AUTO_TEST_CASE( Push_ReportsNonFastForwardWhenRemoteIsAhead )
716{
717 BOOST_TEST_REQUIRE( ready() );
718
719 git_repository* repo = openRepo();
720 BOOST_TEST_REQUIRE( repo );
721
722 {
723 git_repository* remote = nullptr;
724 BOOST_REQUIRE_EQUAL( git_repository_open( &remote, remotePath().ToUTF8().data() ), GIT_OK );
725
726 git_oid remoteHead;
727 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &remoteHead, remote, "refs/heads/master" ), GIT_OK );
728
729 git_oid ahead = createDetachedCommit( remote, remoteHead, wxT( "theirs.txt" ), "theirs\n", "Remote commit" );
730
731 git_reference* ref = nullptr;
732 BOOST_REQUIRE_EQUAL( git_reference_create( &ref, remote, "refs/heads/master", &ahead, 1, nullptr ), GIT_OK );
733 git_reference_free( ref );
734 git_repository_free( remote );
735 }
736
737 createCommit( repo, wxT( "file.txt" ), wxT( "second\n" ), wxT( "Local commit" ) );
738 addOrigin( repo );
739
740 KIGIT_COMMON common( repo );
741 GIT_PUSH_HANDLER handler( &common );
742
743 PushResult result = handler.PerformPush();
744
745 BOOST_CHECK_EQUAL( static_cast<int>( result ), static_cast<int>( PushResult::NonFastForward ) );
746
747 git_repository_free( repo );
748}
749
750
751// A refusal the client-side check let through arrives only on the per-reference callback. A
752// read-only refs directory provokes one, which directory permissions cannot do on Windows.
753#ifndef __WINDOWS__
754BOOST_AUTO_TEST_CASE( Push_ReportsErrorWhenRemoteRefusesTheUpdate )
755{
756 BOOST_TEST_REQUIRE( ready() );
757
758 git_repository* repo = openRepo();
759 BOOST_TEST_REQUIRE( repo );
760
761 createCommit( repo, wxT( "file.txt" ), wxT( "second\n" ), wxT( "Local commit" ) );
762 addOrigin( repo );
763
764 wxString headsDir = remotePath() + wxFileName::GetPathSeparator() + wxT( "refs" ) + wxFileName::GetPathSeparator()
765 + wxT( "heads" );
766
767 BOOST_REQUIRE( wxFileName::DirExists( headsDir ) );
768
769 // Directory permissions do not bind root, so there the refusal cannot be staged.
770 if( geteuid() == 0 )
771 return;
772
773 // Put the permissions back however the test ends, or the fixture cannot remove its directory.
774 struct WRITABLE_AGAIN
775 {
776 wxString dir;
777 ~WRITABLE_AGAIN() { chmod( dir.fn_str(), 0755 ); }
778 } restore{ headsDir };
779
780 chmod( headsDir.fn_str(), 0555 );
781
782 KIGIT_COMMON common( repo );
783 GIT_PUSH_HANDLER handler( &common );
784
785 PushResult result = handler.PerformPush();
786
787 BOOST_CHECK_EQUAL( static_cast<int>( result ), static_cast<int>( PushResult::Error ) );
788
789 // A reason that went missing reads back as the words "No error".
790 const std::vector<wxString>& errors = handler.GetErrorStrings();
791
792 BOOST_REQUIRE_EQUAL( errors.size(), 1u );
793 BOOST_CHECK_MESSAGE( !errors.front().EndsWith( wxS( "No error" ) ),
794 "the refusal lost git's reason: " + errors.front().ToStdString() );
795
796 git_repository_free( repo );
797}
798#endif
799
800
801// Left mid-rebase, every later commit is refused until it is finished from the command line.
802BOOST_AUTO_TEST_CASE( PerformPull_RebaseConflictLeavesRepositoryAsItWas )
803{
804 BOOST_TEST_REQUIRE( ready() );
805
806 git_repository* repo = openRepo();
807 BOOST_TEST_REQUIRE( repo );
808
809 {
810 git_repository* remote = nullptr;
811 BOOST_REQUIRE_EQUAL( git_repository_open( &remote, remotePath().ToUTF8().data() ), GIT_OK );
812
813 git_oid remoteHead;
814 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &remoteHead, remote, "refs/heads/master" ), GIT_OK );
815
816 git_oid ahead = createDetachedCommit( remote, remoteHead, wxT( "file.txt" ), "remote\n", "Remote change" );
817
818 git_reference* ref = nullptr;
819 BOOST_REQUIRE_EQUAL( git_reference_create( &ref, remote, "refs/heads/master", &ahead, 1, nullptr ), GIT_OK );
820 git_reference_free( ref );
821 git_repository_free( remote );
822 }
823
824 git_oid localHead = createCommit( repo, wxT( "file.txt" ), wxT( "local\n" ), wxT( "Local change" ) );
825
826 addOrigin( repo );
827 setUpstreamConfig( repo, wxT( "master" ) );
828
829 {
830 git_config* cfg = nullptr;
831 BOOST_REQUIRE_EQUAL( git_repository_config( &cfg, repo ), GIT_OK );
832 git_config_set_bool( cfg, "pull.rebase", 1 );
833 git_config_free( cfg );
834 }
835
836 KIGIT_COMMON common( repo );
837 GIT_PULL_HANDLER handler( &common );
838
839 PullResult result = handler.PerformPull();
840
841 BOOST_CHECK_EQUAL( static_cast<int>( result ), static_cast<int>( PullResult::Conflict ) );
842 BOOST_CHECK_EQUAL( git_repository_state( repo ), GIT_REPOSITORY_STATE_NONE );
843
844 git_oid headNow;
845 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &headNow, repo, "HEAD" ), GIT_OK );
846 BOOST_CHECK( git_oid_equal( &headNow, &localHead ) );
847
848 wxTextFile file( repoPath() + wxFileName::GetPathSeparator() + wxT( "file.txt" ) );
849 BOOST_REQUIRE( file.Open() );
850 BOOST_CHECK_EQUAL( file.GetFirstLine(), wxString( "local" ) );
851
852 git_repository_free( repo );
853}
854
855
856// The fast-forward must follow branch.<name>.merge, not a remote branch of the same name.
857BOOST_AUTO_TEST_CASE( PerformPull_FastForwardFollowsConfiguredUpstreamBranch )
858{
859 BOOST_TEST_REQUIRE( ready() );
860
861 git_repository* repo = openRepo();
862 BOOST_TEST_REQUIRE( repo );
863
864 git_oid ahead;
865
866 {
867 git_repository* remote = nullptr;
868 BOOST_REQUIRE_EQUAL( git_repository_open( &remote, remotePath().ToUTF8().data() ), GIT_OK );
869
870 // The remote's branch is called main, while the local one stays master.
871 git_reference* masterRef = nullptr;
872 BOOST_REQUIRE_EQUAL( git_reference_lookup( &masterRef, remote, "refs/heads/master" ), GIT_OK );
873
874 git_reference* mainRef = nullptr;
875 BOOST_REQUIRE_EQUAL( git_reference_rename( &mainRef, masterRef, "refs/heads/main", 0, nullptr ), GIT_OK );
876 git_reference_free( masterRef );
877 BOOST_REQUIRE_EQUAL( git_repository_set_head( remote, "refs/heads/main" ), GIT_OK );
878
879 git_oid remoteHead = *git_reference_target( mainRef );
880 git_reference_free( mainRef );
881
882 ahead = createDetachedCommit( remote, remoteHead, wxT( "theirs.txt" ), "theirs\n", "Remote commit" );
883
884 git_reference* ref = nullptr;
885 BOOST_REQUIRE_EQUAL( git_reference_create( &ref, remote, "refs/heads/main", &ahead, 1, nullptr ), GIT_OK );
886 git_reference_free( ref );
887 git_repository_free( remote );
888 }
889
890 addOrigin( repo );
891
892 {
893 git_config* cfg = nullptr;
894 BOOST_REQUIRE_EQUAL( git_repository_config( &cfg, repo ), GIT_OK );
895 git_config_set_string( cfg, "branch.master.remote", "origin" );
896 git_config_set_string( cfg, "branch.master.merge", "refs/heads/main" );
897 git_config_free( cfg );
898 }
899
900 KIGIT_COMMON common( repo );
901 GIT_PULL_HANDLER handler( &common );
902
903 PullResult result = handler.PerformPull();
904
905 BOOST_CHECK_EQUAL( static_cast<int>( result ), static_cast<int>( PullResult::FastForward ) );
906
907 git_oid headNow;
908 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &headNow, repo, "HEAD" ), GIT_OK );
909 BOOST_CHECK( git_oid_equal( &headNow, &ahead ) );
910
911 git_repository_free( repo );
912}
913
914
915// Files are written before the branch moves, so a branch that cannot move must not leave the
916// tree holding a commit HEAD does not point at.
917BOOST_AUTO_TEST_CASE( PerformPull_FastForwardRollsBackWhenBranchCannotMove )
918{
919 BOOST_TEST_REQUIRE( ready() );
920
921 git_repository* repo = openRepo();
922 BOOST_TEST_REQUIRE( repo );
923
924 git_oid before;
925 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &before, repo, "HEAD" ), GIT_OK );
926
927 wxString trackedPath = repoPath() + wxFileName::GetPathSeparator() + wxT( "file.txt" );
928 wxString contentBefore;
929 {
930 wxFFile f( trackedPath, wxT( "rb" ) );
931 f.ReadAll( &contentBefore );
932 }
933
934 git_oid ahead;
935 {
936 git_repository* remote = nullptr;
937 BOOST_REQUIRE_EQUAL( git_repository_open( &remote, remotePath().ToUTF8().data() ), GIT_OK );
938
939 git_oid remoteHead;
940 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &remoteHead, remote, "refs/heads/master" ), GIT_OK );
941
942 ahead = createDetachedCommit( remote, remoteHead, wxT( "file.txt" ), "changed by remote\n", "Remote edit" );
943
944 git_reference* ref = nullptr;
945 BOOST_REQUIRE_EQUAL( git_reference_create( &ref, remote, "refs/heads/master", &ahead, 1, nullptr ), GIT_OK );
946 git_reference_free( ref );
947 git_repository_free( remote );
948 }
949
950 addOrigin( repo );
951
952 // Sits outside the paths the pull touches. Rolling back must leave it exactly as it is.
953 wxString bystanderPath = repoPath() + wxFileName::GetPathSeparator() + wxT( "bystander.txt" );
954 {
955 wxFFile bystander( bystanderPath, wxT( "wb" ) );
956 BOOST_TEST_REQUIRE( bystander.IsOpened() );
957 bystander.Write( wxT( "written by something else\n" ) );
958 }
959
960 // A real reference lock, so the update fails after the checkout, with no production seam.
961 wxString refLock = repoPath() + wxFileName::GetPathSeparator() + wxT( ".git" ) + wxFileName::GetPathSeparator()
962 + wxT( "refs" ) + wxFileName::GetPathSeparator() + wxT( "heads" )
963 + wxFileName::GetPathSeparator() + wxT( "master.lock" );
964 {
965 wxFFile lock( refLock, wxT( "wb" ) );
966 BOOST_TEST_REQUIRE( lock.IsOpened() );
967 lock.Write( wxT( "locked\n" ) );
968 }
969
970 KIGIT_COMMON common( repo );
971 GIT_PULL_HANDLER handler( &common );
972
973 PullResult result = handler.PerformPull();
974
975 BOOST_CHECK_EQUAL( static_cast<int>( result ), static_cast<int>( PullResult::Error ) );
976
977 wxRemoveFile( refLock );
978 git_repository_free( repo );
979
980 // Reopen so nothing is answered from a cached index or reference.
981 git_repository* fresh = nullptr;
982 BOOST_REQUIRE_EQUAL( git_repository_open( &fresh, repoPath().ToUTF8().data() ), GIT_OK );
983
984 git_oid headAfter;
985 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &headAfter, fresh, "HEAD" ), GIT_OK );
986 BOOST_CHECK_MESSAGE( git_oid_equal( &headAfter, &before ), "HEAD must not have moved" );
987
988 wxString contentAfter;
989 {
990 wxFFile f( trackedPath, wxT( "rb" ) );
991 f.ReadAll( &contentAfter );
992 }
993
994 BOOST_CHECK_MESSAGE( contentAfter == contentBefore, "the working file must be back at the commit HEAD points at" );
995
996 // A tree left at the incoming commit would show up here as an uncommitted change.
997 git_status_options opts;
998 git_status_init_options( &opts, GIT_STATUS_OPTIONS_VERSION );
999 opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
1000
1001 git_status_list* list = nullptr;
1002 BOOST_REQUIRE_EQUAL( git_status_list_new( &list, fresh, &opts ), GIT_OK );
1003 size_t dirty = git_status_list_entrycount( list );
1004 git_status_list_free( list );
1005
1006 BOOST_CHECK_MESSAGE( dirty == 0, "the rollback must leave no phantom local changes" );
1007
1008 // Only the paths the pull wrote. Widening it would delete files it never touched.
1009 BOOST_CHECK_MESSAGE( wxFileName::FileExists( bystanderPath ),
1010 "the rollback must not remove a file outside the paths the pull wrote" );
1011
1012 git_repository_free( fresh );
1013}
1014
1015
1016// An untracked directory where the incoming commit wants a file blocks the checkout, and the
1017// rollback must not take the directory with it.
1018BOOST_AUTO_TEST_CASE( PerformPull_RollbackKeepsAnUntrackedDirectoryItCannotIdentify )
1019{
1020 BOOST_TEST_REQUIRE( ready() );
1021
1022 git_repository* repo = openRepo();
1023 BOOST_TEST_REQUIRE( repo );
1024
1025 // The remote adds a file called "notes".
1026 {
1027 git_repository* remote = nullptr;
1028 BOOST_REQUIRE_EQUAL( git_repository_open( &remote, remotePath().ToUTF8().data() ), GIT_OK );
1029
1030 git_oid remoteHead;
1031 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &remoteHead, remote, "refs/heads/master" ), GIT_OK );
1032
1033 git_oid ahead = createDetachedCommit( remote, remoteHead, wxT( "notes" ), "from the remote\n", "Add notes" );
1034
1035 git_reference* ref = nullptr;
1036 BOOST_REQUIRE_EQUAL( git_reference_create( &ref, remote, "refs/heads/master", &ahead, 1, nullptr ), GIT_OK );
1037 git_reference_free( ref );
1038 git_repository_free( remote );
1039 }
1040
1041 addOrigin( repo );
1042
1043 // An untracked directory does not make the tree dirty, and the collision check names the
1044 // file inside it, so the pull reaches the checkout.
1045 wxString notesDir = repoPath() + wxFileName::GetPathSeparator() + wxT( "notes" );
1046 BOOST_REQUIRE( wxFileName::Mkdir( notesDir, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
1047
1048 wxString keptFile = notesDir + wxFileName::GetPathSeparator() + wxT( "keep.txt" );
1049 {
1050 wxFFile f( keptFile, wxT( "wb" ) );
1051 BOOST_TEST_REQUIRE( f.IsOpened() );
1052 f.Write( wxT( "the only copy\n" ) );
1053 }
1054
1055 KIGIT_COMMON common( repo );
1056 GIT_PULL_HANDLER handler( &common );
1057
1058 PullResult result = handler.PerformPull();
1059
1060 // Every failure code is negative, and a checkout over a directory can only fail.
1061 BOOST_CHECK_MESSAGE( static_cast<int>( result ) < 0, "the checkout cannot succeed over a directory" );
1062
1063 git_repository_free( repo );
1064
1065 BOOST_CHECK_MESSAGE( wxFileName::DirExists( notesDir ),
1066 "the rollback must not remove an untracked directory it did not write" );
1067 BOOST_REQUIRE_MESSAGE( wxFileName::FileExists( keptFile ), "the file inside it must survive" );
1068
1069 wxString after;
1070 {
1071 wxFFile f( keptFile, wxT( "rb" ) );
1072 f.ReadAll( &after );
1073 }
1074
1075 BOOST_CHECK_EQUAL( after.ToStdString(), std::string( "the only copy\n" ) );
1076}
1077
1078
1079BOOST_AUTO_TEST_CASE( PerformPull_AdoptRemoteHistoryLeavesUnbornBranchOnFailedCheckout )
1080{
1081 BOOST_TEST_REQUIRE( ready() );
1082
1083 wxString emptyDir = tempBase() + wxFileName::GetPathSeparator() + wxT( "unborn_rollback" );
1084 wxFileName::Mkdir( emptyDir, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
1085
1086 git_repository* fresh = nullptr;
1087 BOOST_REQUIRE_EQUAL( git_repository_init( &fresh, emptyDir.ToUTF8().data(), 0 ), GIT_OK );
1088
1089 {
1090 git_remote* remote = nullptr;
1091 BOOST_REQUIRE_EQUAL( git_remote_create( &remote, fresh, "origin", remotePath().ToUTF8().data() ), GIT_OK );
1092 git_remote_free( remote );
1093 }
1094
1095 // The checkout refuses, which this path must survive without leaving a branch behind.
1096 wxString collide = emptyDir + wxFileName::GetPathSeparator() + wxT( "file.txt" );
1097 {
1098 wxFFile f( collide, wxT( "wb" ) );
1099 f.Write( wxT( "mine\n" ) );
1100 }
1101
1102 KIGIT_COMMON common( fresh );
1103 GIT_PULL_HANDLER handler( &common );
1104
1105 PullResult result = handler.PerformPull();
1106
1107 BOOST_CHECK_MESSAGE( result != PullResult::Success && result != PullResult::FastForward,
1108 "a colliding untracked file must not be overwritten" );
1109
1110 git_repository_free( fresh );
1111
1112 git_repository* reopened = nullptr;
1113 BOOST_REQUIRE_EQUAL( git_repository_open( &reopened, emptyDir.ToUTF8().data() ), GIT_OK );
1114
1115 BOOST_CHECK_MESSAGE( git_repository_head_unborn( reopened ) == 1,
1116 "the branch must still be unborn after a refused checkout" );
1117
1118 wxString kept;
1119 {
1120 wxFFile f( collide, wxT( "rb" ) );
1121 f.ReadAll( &kept );
1122 }
1123
1124 BOOST_CHECK_EQUAL( kept.ToStdString(), std::string( "mine\n" ) );
1125
1126 git_repository_free( reopened );
1127
1128 if( wxFileName::DirExists( emptyDir ) )
1129 wxFileName::Rmdir( emptyDir, wxPATH_RMDIR_RECURSIVE );
1130}
1131
1132
1133BOOST_AUTO_TEST_CASE( PerformPull_ExplainsWhenActionLockHeld )
1134{
1135 BOOST_TEST_REQUIRE( ready() );
1136
1137 git_repository* repo = openRepo();
1138 BOOST_TEST_REQUIRE( repo );
1139
1140 addOrigin( repo );
1141
1142 KIGIT_COMMON common( repo );
1143 GIT_PULL_HANDLER handler( &common );
1144
1145 std::unique_lock<std::mutex> held = common.LockAction();
1146
1147 PullResult result = std::async( std::launch::async,
1148 [&handler]()
1149 {
1150 return handler.PerformPull();
1151 } )
1152 .get();
1153
1154 BOOST_CHECK_EQUAL( static_cast<int>( result ), static_cast<int>( PullResult::Error ) );
1155 BOOST_CHECK( !handler.GetErrorStrings().empty() );
1156
1157 held.unlock();
1158 git_repository_free( repo );
1159}
1160
1161
1162// The user has to be told which file, since the checklist may not even show it.
1163BOOST_AUTO_TEST_CASE( PerformPull_NamesUntrackedFileBlockingFastForward )
1164{
1165 BOOST_TEST_REQUIRE( ready() );
1166
1167 git_repository* repo = openRepo();
1168 BOOST_TEST_REQUIRE( repo );
1169
1170 {
1171 git_repository* remote = nullptr;
1172 BOOST_REQUIRE_EQUAL( git_repository_open( &remote, remotePath().ToUTF8().data() ), GIT_OK );
1173
1174 git_oid remoteHead;
1175 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &remoteHead, remote, "refs/heads/master" ), GIT_OK );
1176
1177 git_oid ahead = createDetachedCommit( remote, remoteHead, wxT( "theirs.txt" ), "theirs\n", "Remote adds" );
1178
1179 git_reference* ref = nullptr;
1180 BOOST_REQUIRE_EQUAL( git_reference_create( &ref, remote, "refs/heads/master", &ahead, 1, nullptr ), GIT_OK );
1181 git_reference_free( ref );
1182 git_repository_free( remote );
1183 }
1184
1185 wxString collidingPath = repoPath() + wxFileName::GetPathSeparator() + wxT( "theirs.txt" );
1186 {
1187 std::ofstream f( collidingPath.ToStdString() );
1188 f << "mine, never committed\n";
1189 }
1190
1191 addOrigin( repo );
1192 setUpstreamConfig( repo, wxT( "master" ) );
1193
1194 git_oid headBefore;
1195 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &headBefore, repo, "HEAD" ), GIT_OK );
1196
1197 KIGIT_COMMON common( repo );
1198 GIT_PULL_HANDLER handler( &common );
1199
1200 PullResult result = handler.PerformPull();
1201
1202 BOOST_CHECK_EQUAL( static_cast<int>( result ), static_cast<int>( PullResult::DirtyWorkdir ) );
1203 BOOST_CHECK( handler.GetErrorString().Contains( wxT( "theirs.txt" ) ) );
1204
1205 git_oid headAfter;
1206 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &headAfter, repo, "HEAD" ), GIT_OK );
1207 BOOST_CHECK( git_oid_equal( &headBefore, &headAfter ) );
1208
1209 wxTextFile file( collidingPath );
1210 BOOST_REQUIRE( file.Open() );
1211 BOOST_CHECK_EQUAL( file.GetFirstLine(), wxString( "mine, never committed" ) );
1212
1213 git_repository_free( repo );
1214}
1215
1216
1217// The same collision when the branches have diverged, which takes the merge path.
1218BOOST_AUTO_TEST_CASE( PerformPull_NamesUntrackedFileBlockingMerge )
1219{
1220 BOOST_TEST_REQUIRE( ready() );
1221
1222 git_repository* repo = openRepo();
1223 BOOST_TEST_REQUIRE( repo );
1224
1225 {
1226 git_repository* remote = nullptr;
1227 BOOST_REQUIRE_EQUAL( git_repository_open( &remote, remotePath().ToUTF8().data() ), GIT_OK );
1228
1229 git_oid remoteHead;
1230 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &remoteHead, remote, "refs/heads/master" ), GIT_OK );
1231
1232 git_oid ahead = createDetachedCommit( remote, remoteHead, wxT( "theirs.txt" ), "theirs\n", "Remote adds" );
1233
1234 git_reference* ref = nullptr;
1235 BOOST_REQUIRE_EQUAL( git_reference_create( &ref, remote, "refs/heads/master", &ahead, 1, nullptr ), GIT_OK );
1236 git_reference_free( ref );
1237 git_repository_free( remote );
1238 }
1239
1240 git_oid localHead = createCommit( repo, wxT( "other.txt" ), wxT( "local\n" ), wxT( "Local change" ) );
1241
1242 wxString collidingPath = repoPath() + wxFileName::GetPathSeparator() + wxT( "theirs.txt" );
1243 {
1244 std::ofstream f( collidingPath.ToStdString() );
1245 f << "mine, never committed\n";
1246 }
1247
1248 addOrigin( repo );
1249 setUpstreamConfig( repo, wxT( "master" ) );
1250
1251 KIGIT_COMMON common( repo );
1252 GIT_PULL_HANDLER handler( &common );
1253
1254 PullResult result = handler.PerformPull();
1255
1256 BOOST_CHECK_EQUAL( static_cast<int>( result ), static_cast<int>( PullResult::DirtyWorkdir ) );
1257 BOOST_CHECK( handler.GetErrorString().Contains( wxT( "theirs.txt" ) ) );
1258 BOOST_CHECK_EQUAL( git_repository_state( repo ), GIT_REPOSITORY_STATE_NONE );
1259
1260 git_oid headAfter;
1261 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &headAfter, repo, "HEAD" ), GIT_OK );
1262 BOOST_CHECK( git_oid_equal( &localHead, &headAfter ) );
1263
1264 git_repository_free( repo );
1265}
1266
1267
1268// With no commit of its own to merge from, getting changes adopts the remote history.
1269BOOST_AUTO_TEST_CASE( PerformPull_UnbornBranchAdoptsRemoteHistory )
1270{
1271 BOOST_TEST_REQUIRE( ready() );
1272
1273 wxString freshPath = tempBase() + wxFileName::GetPathSeparator() + wxT( "fresh" );
1274
1275 git_repository* repo = nullptr;
1276 git_repository_init_options initOpts;
1277 git_repository_init_options_init( &initOpts, GIT_REPOSITORY_INIT_OPTIONS_VERSION );
1278 initOpts.flags = GIT_REPOSITORY_INIT_MKPATH;
1279 initOpts.initial_head = "master";
1280 BOOST_REQUIRE_EQUAL( git_repository_init_ext( &repo, freshPath.ToUTF8().data(), &initOpts ), GIT_OK );
1281 BOOST_REQUIRE_EQUAL( git_repository_head_unborn( repo ), 1 );
1282
1283 addOrigin( repo );
1284
1285 KIGIT_COMMON common( repo );
1286 GIT_PULL_HANDLER handler( &common );
1287
1288 PullResult result = handler.PerformPull();
1289
1290 BOOST_CHECK_EQUAL( static_cast<int>( result ), static_cast<int>( PullResult::FastForward ) );
1291 BOOST_CHECK_EQUAL( git_repository_head_unborn( repo ), 0 );
1292 BOOST_CHECK_EQUAL( git_repository_head_detached( repo ), 0 );
1293 BOOST_CHECK( wxFileName::FileExists( freshPath + wxFileName::GetPathSeparator() + wxT( "file.txt" ) ) );
1294
1295 git_repository_free( repo );
1296}
1297
1298
1299BOOST_AUTO_TEST_CASE( AbandonOperation_ClearsInterruptedMerge )
1300{
1301 BOOST_TEST_REQUIRE( ready() );
1302
1303 git_repository* repo = openRepo();
1304 BOOST_TEST_REQUIRE( repo );
1305
1306 git_oid base;
1307 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &base, repo, "HEAD" ), GIT_OK );
1308
1309 git_oid side = createDetachedCommit( repo, base, wxT( "file.txt" ), "side\n", "Side change" );
1310 git_oid localHead = createCommit( repo, wxT( "file.txt" ), wxT( "local\n" ), wxT( "Local change" ) );
1311
1312 {
1313 git_annotated_commit* sideCommit = nullptr;
1314 BOOST_REQUIRE_EQUAL( git_annotated_commit_lookup( &sideCommit, repo, &side ), GIT_OK );
1315
1316 const git_annotated_commit* heads[1] = { sideCommit };
1317 git_checkout_options checkoutOpts = GIT_CHECKOUT_OPTIONS_INIT;
1318 checkoutOpts.checkout_strategy = GIT_CHECKOUT_SAFE | GIT_CHECKOUT_ALLOW_CONFLICTS;
1319
1320 BOOST_REQUIRE_EQUAL( git_merge( repo, heads, 1, nullptr, &checkoutOpts ), GIT_OK );
1321 git_annotated_commit_free( sideCommit );
1322 }
1323
1324 BOOST_REQUIRE_EQUAL( git_repository_state( repo ), GIT_REPOSITORY_STATE_MERGE );
1325
1326 KIGIT_COMMON common( repo );
1327 wxString error;
1328
1329 // The panel names the operation it offers to abandon, so the session has to tell which it is.
1330 BOOST_CHECK( common.IsOperationInProgress() );
1331 BOOST_CHECK( common.OperationInProgress() == GitOperation::Merge );
1332 BOOST_CHECK( common.AbandonOperation( error ) );
1333 BOOST_CHECK( !common.IsOperationInProgress() );
1334 BOOST_CHECK( common.OperationInProgress() == GitOperation::None );
1335 BOOST_CHECK_EQUAL( git_repository_state( repo ), GIT_REPOSITORY_STATE_NONE );
1336
1337 git_index* index = nullptr;
1338 BOOST_REQUIRE_EQUAL( git_repository_index( &index, repo ), GIT_OK );
1339 BOOST_CHECK( !git_index_has_conflicts( index ) );
1340 git_index_free( index );
1341
1342 git_oid headNow;
1343 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &headNow, repo, "HEAD" ), GIT_OK );
1344 BOOST_CHECK( git_oid_equal( &headNow, &localHead ) );
1345
1346 wxTextFile file( repoPath() + wxFileName::GetPathSeparator() + wxT( "file.txt" ) );
1347 BOOST_REQUIRE( file.Open() );
1348 BOOST_CHECK_EQUAL( file.GetFirstLine(), wxString( "local" ) );
1349
1350 git_repository_free( repo );
1351}
1352
1353
1354// An interrupted rebase also leaves HEAD detached. Abandoning it must put the branch back.
1355// A bisect is a search, not a half-finished change. Resetting and clearing its bookkeeping
1356// leaves the project on whichever commit the search had reached, with no way back to where it
1357// started.
1358// Abandoning goes back to the last commit for the files the operation touched. Uncommitted work
1359// in a file it never went near is somebody's own, and a repository can hold several projects.
1360BOOST_AUTO_TEST_CASE( AbandonOperation_KeepsWorkTheOperationDidNotTouch )
1361{
1362 BOOST_TEST_REQUIRE( ready() );
1363
1364 git_repository* repo = openRepo();
1365 BOOST_TEST_REQUIRE( repo );
1366
1367 // A second tracked file, committed before the branch point so neither side changes it.
1368 createCommit( repo, wxT( "bystander.txt" ), wxT( "keep\n" ), wxT( "Add bystander" ) );
1369
1370 git_oid base;
1371 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &base, repo, "HEAD" ), GIT_OK );
1372
1373 git_oid sideEdit = createDetachedCommit( repo, base, wxT( "file.txt" ), "side\n", "Side change" );
1374
1375 // The side also brings a file of its own, which the merge stages as an addition.
1376 git_oid side = createDetachedCommit( repo, sideEdit, wxT( "fromside.txt" ), "new\n", "Side addition" );
1377
1378 createCommit( repo, wxT( "file.txt" ), wxT( "local\n" ), wxT( "Local change" ) );
1379
1380 // The user's own edit, in a file the merge has no business in.
1381 wxString bystanderPath = repoPath() + wxFileName::GetPathSeparator() + wxT( "bystander.txt" );
1382 {
1383 std::ofstream f( bystanderPath.ToStdString() );
1384 f << "my own work\n";
1385 }
1386
1387 {
1388 git_annotated_commit* sideCommit = nullptr;
1389 BOOST_REQUIRE_EQUAL( git_annotated_commit_lookup( &sideCommit, repo, &side ), GIT_OK );
1390
1391 const git_annotated_commit* heads[1] = { sideCommit };
1392 git_checkout_options checkoutOpts = GIT_CHECKOUT_OPTIONS_INIT;
1393 checkoutOpts.checkout_strategy = GIT_CHECKOUT_SAFE | GIT_CHECKOUT_ALLOW_CONFLICTS;
1394
1395 BOOST_REQUIRE_EQUAL( git_merge( repo, heads, 1, nullptr, &checkoutOpts ), GIT_OK );
1396 git_annotated_commit_free( sideCommit );
1397 }
1398
1399 BOOST_REQUIRE_EQUAL( git_repository_state( repo ), GIT_REPOSITORY_STATE_MERGE );
1400
1401 KIGIT_COMMON common( repo );
1402 wxString error;
1403
1404 BOOST_REQUIRE_MESSAGE( common.AbandonOperation( error ), error );
1405
1406 // The merge is gone and its file is back.
1407 BOOST_CHECK_EQUAL( git_repository_state( repo ), GIT_REPOSITORY_STATE_NONE );
1408
1409 git_index* index = nullptr;
1410 BOOST_REQUIRE_EQUAL( git_repository_index( &index, repo ), GIT_OK );
1411 KIGIT::GitIndexPtr indexPtr( index );
1412 BOOST_CHECK( !git_index_has_conflicts( index ) );
1413
1414 {
1415 std::ifstream f( ( repoPath() + wxFileName::GetPathSeparator() + wxT( "file.txt" ) ).ToStdString() );
1416 std::string content( ( std::istreambuf_iterator<char>( f ) ), std::istreambuf_iterator<char>() );
1417 BOOST_CHECK_EQUAL( content, "local\n" );
1418 }
1419
1420 // The file the merge brought in is gone again, as git's own abort leaves it.
1421 BOOST_CHECK( !wxFileName::FileExists( repoPath() + wxFileName::GetPathSeparator() + wxT( "fromside.txt" ) ) );
1422
1423 // And the edit the merge never touched is still there.
1424 {
1425 std::ifstream f( bystanderPath.ToStdString() );
1426 std::string content( ( std::istreambuf_iterator<char>( f ) ), std::istreambuf_iterator<char>() );
1427 BOOST_CHECK_EQUAL( content, "my own work\n" );
1428 }
1429
1430 git_repository_free( repo );
1431}
1432
1433
1434BOOST_AUTO_TEST_CASE( AbandonOperation_RefusesDuringABisect )
1435{
1436 BOOST_TEST_REQUIRE( ready() );
1437
1438 git_repository* repo = openRepo();
1439 BOOST_TEST_REQUIRE( repo );
1440
1441 git_oid head;
1442 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &head, repo, "HEAD" ), GIT_OK );
1443
1444 // What `git bisect start` leaves behind, which is how git itself recognises the state.
1445 wxString gitDir = repoPath() + wxFileName::GetPathSeparator() + wxT( ".git" ) + wxFileName::GetPathSeparator();
1446
1447 {
1448 std::ofstream log( ( gitDir + wxT( "BISECT_LOG" ) ).ToStdString() );
1449 log << "git bisect start\n";
1450 }
1451
1452 {
1453 std::ofstream start( ( gitDir + wxT( "BISECT_START" ) ).ToStdString() );
1454 start << "master\n";
1455 }
1456
1457 BOOST_REQUIRE_EQUAL( git_repository_state( repo ), GIT_REPOSITORY_STATE_BISECT );
1458
1459 KIGIT_COMMON common( repo );
1460 wxString error;
1461
1462 BOOST_CHECK_MESSAGE( !common.AbandonOperation( error ), "a bisect must not be abandoned from here" );
1463 BOOST_CHECK( !error.IsEmpty() );
1464 BOOST_CHECK_MESSAGE( wxFileName::FileExists( gitDir + wxT( "BISECT_LOG" ) ),
1465 "the bisect bookkeeping must survive" );
1466
1467 wxRemoveFile( gitDir + wxT( "BISECT_LOG" ) );
1468 wxRemoveFile( gitDir + wxT( "BISECT_START" ) );
1469 git_repository_free( repo );
1470}
1471
1472
1473BOOST_AUTO_TEST_CASE( AbandonOperation_RefusesAnInteractiveRebaseInOurWords )
1474{
1475 BOOST_TEST_REQUIRE( ready() );
1476
1477 git_repository* repo = openRepo();
1478 BOOST_TEST_REQUIRE( repo );
1479
1480 const wxString sep = wxFileName::GetPathSeparator();
1481
1482 // What `git rebase -i` leaves behind. libgit2 cannot open one, so the abort fails.
1483 const wxString rebaseDir = repoPath() + sep + wxT( ".git" ) + sep + wxT( "rebase-merge" );
1484
1485 BOOST_REQUIRE( wxFileName::Mkdir( rebaseDir, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
1486
1487 git_oid head;
1488 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &head, repo, "HEAD" ), GIT_OK );
1489
1490 const std::string headHash = git_oid_tostr_s( &head );
1491
1492 auto writeRebaseFile = [&]( const wxString& aName, const std::string& aText )
1493 {
1494 std::ofstream out( ( rebaseDir + sep + aName ).ToStdString() );
1495 out << aText << "\n";
1496 };
1497
1498 writeRebaseFile( wxT( "interactive" ), "" );
1499 writeRebaseFile( wxT( "head-name" ), "refs/heads/master" );
1500 writeRebaseFile( wxT( "onto" ), headHash );
1501 writeRebaseFile( wxT( "orig-head" ), headHash );
1502
1503 BOOST_REQUIRE_EQUAL( git_repository_state( repo ), GIT_REPOSITORY_STATE_REBASE_INTERACTIVE );
1504
1505 KIGIT_COMMON common( repo );
1506 wxString error;
1507
1508 BOOST_CHECK( !common.AbandonOperation( error ) );
1509 BOOST_CHECK_MESSAGE( error.Contains( wxT( "another program" ) ), error );
1510 BOOST_CHECK_MESSAGE( !error.Lower().Contains( wxT( "not supported" ) ), error );
1511
1512 for( const wxString& name : { wxT( "interactive" ), wxT( "head-name" ), wxT( "onto" ), wxT( "orig-head" ) } )
1513 wxRemoveFile( rebaseDir + sep + name );
1514
1515 wxFileName::Rmdir( rebaseDir );
1516 git_repository_free( repo );
1517}
1518
1519
1520BOOST_AUTO_TEST_CASE( AbandonOperation_ClearsInterruptedRebase )
1521{
1522 BOOST_TEST_REQUIRE( ready() );
1523
1524 git_repository* repo = openRepo();
1525 BOOST_TEST_REQUIRE( repo );
1526
1527 git_oid base;
1528 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &base, repo, "HEAD" ), GIT_OK );
1529
1530 git_oid side = createDetachedCommit( repo, base, wxT( "file.txt" ), "side\n", "Side change" );
1531 git_oid localHead = createCommit( repo, wxT( "file.txt" ), wxT( "local\n" ), wxT( "Local change" ) );
1532
1533 {
1534 git_annotated_commit* upstream = nullptr;
1535 BOOST_REQUIRE_EQUAL( git_annotated_commit_lookup( &upstream, repo, &side ), GIT_OK );
1536
1537 git_rebase* rebase = nullptr;
1538 BOOST_REQUIRE_EQUAL( git_rebase_init( &rebase, repo, nullptr, upstream, nullptr, nullptr ), GIT_OK );
1539
1540 git_rebase_operation* op = nullptr;
1541 BOOST_REQUIRE_EQUAL( git_rebase_next( &op, rebase ), GIT_OK );
1542
1543 // Walk away mid-rebase, as a crash would.
1544 git_rebase_free( rebase );
1545 git_annotated_commit_free( upstream );
1546 }
1547
1548 BOOST_REQUIRE_EQUAL( git_repository_state( repo ), GIT_REPOSITORY_STATE_REBASE_MERGE );
1549 BOOST_REQUIRE_EQUAL( git_repository_head_detached( repo ), 1 );
1550
1551 KIGIT_COMMON common( repo );
1552 wxString error;
1553
1554 BOOST_CHECK( common.AbandonOperation( error ) );
1555 BOOST_CHECK_EQUAL( git_repository_state( repo ), GIT_REPOSITORY_STATE_NONE );
1556 BOOST_CHECK_EQUAL( git_repository_head_detached( repo ), 0 );
1557
1558 git_oid headNow;
1559 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &headNow, repo, "HEAD" ), GIT_OK );
1560 BOOST_CHECK( git_oid_equal( &headNow, &localHead ) );
1561
1562 wxTextFile file( repoPath() + wxFileName::GetPathSeparator() + wxT( "file.txt" ) );
1563 BOOST_REQUIRE( file.Open() );
1564 BOOST_CHECK_EQUAL( file.GetFirstLine(), wxString( "local" ) );
1565
1566 git_repository_free( repo );
1567}
1568
1569
1570// A fast-forward over a locally modified file must refuse, not move the branch past a change
1571// it could not apply. Otherwise the local edit reads as reverting the remote change.
1572BOOST_AUTO_TEST_CASE( PerformPull_FastForwardRefusesOverDirtyTrackedFile )
1573{
1574 BOOST_TEST_REQUIRE( ready() );
1575
1576 git_repository* repo = openRepo();
1577 BOOST_TEST_REQUIRE( repo );
1578
1579 {
1580 git_repository* remote = nullptr;
1581 BOOST_REQUIRE_EQUAL( git_repository_open( &remote, remotePath().ToUTF8().data() ), GIT_OK );
1582
1583 git_oid remoteHead;
1584 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &remoteHead, remote, "refs/heads/master" ), GIT_OK );
1585
1586 git_oid ahead = createDetachedCommit( remote, remoteHead, wxT( "file.txt" ), "remote\n", "Remote change" );
1587
1588 git_reference* ref = nullptr;
1589 BOOST_REQUIRE_EQUAL( git_reference_create( &ref, remote, "refs/heads/master", &ahead, 1, nullptr ), GIT_OK );
1590 git_reference_free( ref );
1591 git_repository_free( remote );
1592 }
1593
1594 wxString filePath = repoPath() + wxFileName::GetPathSeparator() + wxT( "file.txt" );
1595 {
1596 std::ofstream f( filePath.ToStdString() );
1597 f << "local edit\n";
1598 }
1599
1600 addOrigin( repo );
1601 setUpstreamConfig( repo, wxT( "master" ) );
1602
1603 git_oid headBefore;
1604 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &headBefore, repo, "HEAD" ), GIT_OK );
1605
1606 KIGIT_COMMON common( repo );
1607 GIT_PULL_HANDLER handler( &common );
1608
1609 PullResult result = handler.PerformPull();
1610
1611 BOOST_CHECK_EQUAL( static_cast<int>( result ), static_cast<int>( PullResult::DirtyWorkdir ) );
1612
1613 git_oid headAfter;
1614 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &headAfter, repo, "HEAD" ), GIT_OK );
1615 BOOST_CHECK( git_oid_equal( &headBefore, &headAfter ) );
1616
1617 wxTextFile file( filePath );
1618 BOOST_REQUIRE( file.Open() );
1619 BOOST_CHECK_EQUAL( file.GetFirstLine(), wxString( "local edit" ) );
1620
1621 git_repository_free( repo );
1622}
1623
1624
1625// Equivalent to git checkout HEAD with a path, so other changes and untracked files stay.
1626BOOST_AUTO_TEST_CASE( DiscardChangesRestoresOnlyTheGivenPaths )
1627{
1628 BOOST_TEST_REQUIRE( ready() );
1629
1630 git_repository* repo = openRepo();
1631 BOOST_TEST_REQUIRE( repo );
1632
1633 createCommit( repo, wxT( "keep.txt" ), wxT( "keep\n" ), wxT( "Add keep" ) );
1634 createCommit( repo, wxT( "gone.txt" ), wxT( "gone\n" ), wxT( "Add gone" ) );
1635
1636 wxString workDir = wxString::FromUTF8( git_repository_workdir( repo ) );
1637
1638 auto writeAll = []( const wxString& aPath, const std::string& aContent )
1639 {
1640 std::ofstream f( aPath.ToStdString(), std::ios::binary );
1641 f << aContent;
1642 };
1643
1644 auto readAll = []( const wxString& aPath )
1645 {
1646 std::ifstream in( aPath.ToStdString(), std::ios::binary );
1647 return std::string( ( std::istreambuf_iterator<char>( in ) ), std::istreambuf_iterator<char>() );
1648 };
1649
1650 writeAll( workDir + wxT( "file.txt" ), "accidental\n" );
1651 writeAll( workDir + wxT( "keep.txt" ), "still dirty\n" );
1652 writeAll( workDir + wxT( "new.txt" ), "never committed\n" );
1653 BOOST_TEST_REQUIRE( wxRemoveFile( workDir + wxT( "gone.txt" ) ) );
1654
1655 KIGIT_COMMON common( repo );
1656
1657 BOOST_CHECK( common.IsPathInHead( wxT( "file.txt" ) ) );
1658 BOOST_CHECK( !common.IsPathInHead( wxT( "new.txt" ) ) );
1659
1660 wxString error;
1661 BOOST_REQUIRE_MESSAGE( common.DiscardChanges( { wxT( "file.txt" ), wxT( "gone.txt" ) }, error ), error );
1662
1663 BOOST_CHECK_EQUAL( readAll( workDir + wxT( "file.txt" ) ), "initial\n" );
1664 BOOST_CHECK_EQUAL( readAll( workDir + wxT( "gone.txt" ) ), "gone\n" );
1665 BOOST_CHECK_EQUAL( readAll( workDir + wxT( "keep.txt" ) ), "still dirty\n" );
1666 BOOST_CHECK_EQUAL( readAll( workDir + wxT( "new.txt" ) ), "never committed\n" );
1667
1668 git_repository_free( repo );
1669}
1670
1671
1672// libgit2 reads checkout paths as wildmatch patterns, so a name holding a glob character must
1673// be matched literally or the wrong file is restored.
1674BOOST_AUTO_TEST_CASE( DiscardChangesMatchesPathsLiterally )
1675{
1676 BOOST_TEST_REQUIRE( ready() );
1677
1678 git_repository* repo = openRepo();
1679 BOOST_TEST_REQUIRE( repo );
1680
1681 createCommit( repo, wxT( "board[1].txt" ), wxT( "committed\n" ), wxT( "Add bracketed" ) );
1682 createCommit( repo, wxT( "board1.txt" ), wxT( "other committed\n" ), wxT( "Add plain" ) );
1683
1684 wxString workDir = wxString::FromUTF8( git_repository_workdir( repo ) );
1685
1686 auto writeAll = []( const wxString& aPath, const std::string& aContent )
1687 {
1688 std::ofstream f( aPath.ToStdString(), std::ios::binary );
1689 f << aContent;
1690 };
1691
1692 auto readAll = []( const wxString& aPath )
1693 {
1694 std::ifstream in( aPath.ToStdString(), std::ios::binary );
1695 return std::string( ( std::istreambuf_iterator<char>( in ) ), std::istreambuf_iterator<char>() );
1696 };
1697
1698 writeAll( workDir + wxT( "board[1].txt" ), "edited\n" );
1699 writeAll( workDir + wxT( "board1.txt" ), "edited too\n" );
1700
1701 KIGIT_COMMON common( repo );
1702
1703 wxString error;
1704 BOOST_REQUIRE_MESSAGE( common.DiscardChanges( { wxT( "board[1].txt" ) }, error ), error );
1705
1706 BOOST_CHECK_EQUAL( readAll( workDir + wxT( "board[1].txt" ) ), "committed\n" );
1707 BOOST_CHECK_EQUAL( readAll( workDir + wxT( "board1.txt" ) ), "edited too\n" );
1708
1709 git_repository_free( repo );
1710}
1711
1712
1713// libgit2 reads a status pathspec as a wildmatch pattern, so a folder name holding a glob
1714// character must be matched literally or the wrong folder is listed.
1715BOOST_AUTO_TEST_CASE( GetFileStatusMatchesPathspecLiterally )
1716{
1717 BOOST_TEST_REQUIRE( ready() );
1718
1719 git_repository* repo = openRepo();
1720 BOOST_TEST_REQUIRE( repo );
1721
1722 wxString workDir = wxString::FromUTF8( git_repository_workdir( repo ) );
1723
1724 auto writeAll = []( const wxString& aPath, const std::string& aContent )
1725 {
1726 wxFileName fn( aPath );
1727 fn.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
1728
1729 std::ofstream f( aPath.ToStdString(), std::ios::binary );
1730 f << aContent;
1731 };
1732
1733 writeAll( workDir + wxT( "a[1]/x.txt" ), "one\n" );
1734 writeAll( workDir + wxT( "a1/y.txt" ), "two\n" );
1735
1736 KIGIT_COMMON common( repo );
1737 GIT_STATUS_HANDLER handler( &common );
1738
1739 std::map<wxString, FileStatus> status = handler.GetFileStatus( wxT( "a[1]/" ), true );
1740
1741 bool bracketed = false;
1742 bool plain = false;
1743
1744 for( const auto& [path, fileStatus] : status )
1745 {
1746 bracketed |= path.EndsWith( wxT( "a[1]/x.txt" ) );
1747 plain |= path.EndsWith( wxT( "a1/y.txt" ) );
1748 }
1749
1750 BOOST_CHECK_MESSAGE( bracketed, "the folder named in the pathspec was not listed" );
1751 BOOST_CHECK_MESSAGE( !plain, "a folder the pathspec does not name was listed" );
1752
1753 git_repository_free( repo );
1754}
1755
1756
1757// The panel can only say the remote moved on if the session can tell.
1758BOOST_AUTO_TEST_CASE( HasUpstreamCommitsSeesTheRemoteMoveOn )
1759{
1760 BOOST_TEST_REQUIRE( ready() );
1761
1762 git_repository* repo = openRepo();
1763 BOOST_TEST_REQUIRE( repo );
1764
1765 KIGIT_COMMON common( repo );
1766 BOOST_REQUIRE( common.AddRemote( wxT( "origin" ), wxT( "https://example.com/x.git" ) ) );
1767
1768 git_reference* head = nullptr;
1769 BOOST_REQUIRE_EQUAL( git_repository_head( &head, repo ), GIT_OK );
1770
1771 wxString branch = wxString::FromUTF8( git_reference_shorthand( head ) );
1772 std::string trackingRef = ( wxT( "refs/remotes/origin/" ) + branch ).utf8_string();
1773 std::string upstreamName = ( wxT( "origin/" ) + branch ).utf8_string();
1774 git_oid base = *git_reference_target( head );
1775
1776 git_reference* tracking = nullptr;
1777 BOOST_REQUIRE_EQUAL( git_reference_create( &tracking, repo, trackingRef.c_str(), &base, 0, nullptr ), GIT_OK );
1778 git_reference_free( tracking );
1779 BOOST_REQUIRE_EQUAL( git_branch_set_upstream( head, upstreamName.c_str() ), GIT_OK );
1780
1781 // In step with the upstream.
1782 BOOST_CHECK( !common.HasUpstreamCommits() );
1783
1784 // The upstream moves on.
1785 git_oid remoteTip = createDetachedCommit( repo, base, wxT( "file.txt" ), "remote\n", "Remote change" );
1786 BOOST_REQUIRE_EQUAL( git_reference_create( &tracking, repo, trackingRef.c_str(), &remoteTip, 1, nullptr ), GIT_OK );
1787 git_reference_free( tracking );
1788
1789 BOOST_CHECK( common.HasUpstreamCommits() );
1790
1791 git_reference_free( head );
1792 git_repository_free( repo );
1793}
1794
1795
1796// Rebuilding the index from HEAD would drop the conflict stages before libgit2's unmerged-index
1797// guard could see them, recording the markers as resolved content.
1798BOOST_AUTO_TEST_CASE( Commit_RefusesConflictedRepository )
1799{
1800 BOOST_TEST_REQUIRE( ready() );
1801
1802 git_repository* repo = openRepo();
1803 BOOST_TEST_REQUIRE( repo );
1804
1805 git_oid baseOid;
1806 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &baseOid, repo, "HEAD" ), GIT_OK );
1807
1808 createCommit( repo, wxT( "file.txt" ), wxT( "mine\n" ), wxT( "Local change" ) );
1809
1810 git_oid remoteCommitOid = createDetachedCommit( repo, baseOid, wxT( "file.txt" ), "remote\n", "Remote change" );
1811
1812 git_annotated_commit* theirs = nullptr;
1813 BOOST_REQUIRE_EQUAL( git_annotated_commit_lookup( &theirs, repo, &remoteCommitOid ), GIT_OK );
1814
1815 const git_annotated_commit* heads[1] = { theirs };
1816 BOOST_REQUIRE_EQUAL( git_merge( repo, heads, 1, nullptr, nullptr ), GIT_OK );
1817 git_annotated_commit_free( theirs );
1818
1819 git_index* index = nullptr;
1820 BOOST_REQUIRE_EQUAL( git_repository_index( &index, repo ), GIT_OK );
1821 BOOST_REQUIRE( git_index_has_conflicts( index ) );
1822 git_index_free( index );
1823
1824 git_oid headBefore;
1825 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &headBefore, repo, "HEAD" ), GIT_OK );
1826
1827 wxString workDir = wxString::FromUTF8( git_repository_workdir( repo ) );
1828
1829 auto readAll = []( const wxString& aPath )
1830 {
1831 std::ifstream in( aPath.ToStdString(), std::ios::binary );
1832 return std::string( ( std::istreambuf_iterator<char>( in ) ), std::istreambuf_iterator<char>() );
1833 };
1834
1835 std::string conflictedBytes = readAll( workDir + wxT( "file.txt" ) );
1836
1837 GIT_COMMIT_HANDLER handler( repo );
1838
1839 BOOST_CHECK( handler.PerformCommit( { wxT( "file.txt" ) }, wxT( "Oops" ), TEST_AUTHOR_NAME, TEST_AUTHOR_EMAIL )
1841 BOOST_CHECK( !handler.GetErrorString().IsEmpty() );
1842
1843 BOOST_CHECK( handler.PerformAmend( { wxT( "file.txt" ) }, wxT( "Oops" ), TEST_AUTHOR_NAME, TEST_AUTHOR_EMAIL )
1845
1846 // A clean selected file elsewhere must not erase the unrelated conflict either.
1847 {
1848 std::ofstream f( ( workDir + wxT( "other.txt" ) ).ToStdString(), std::ios::binary );
1849 f << "clean\n";
1850 }
1851
1852 BOOST_CHECK( handler.PerformCommit( { wxT( "other.txt" ) }, wxT( "Oops" ), TEST_AUTHOR_NAME, TEST_AUTHOR_EMAIL )
1854
1855 git_oid headAfter;
1856 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &headAfter, repo, "HEAD" ), GIT_OK );
1857 BOOST_CHECK_EQUAL( git_oid_cmp( &headBefore, &headAfter ), 0 );
1858
1859 BOOST_REQUIRE_EQUAL( git_repository_index( &index, repo ), GIT_OK );
1860 BOOST_CHECK( git_index_has_conflicts( index ) );
1861 git_index_free( index );
1862
1863 BOOST_CHECK_EQUAL( git_repository_state( repo ), GIT_REPOSITORY_STATE_MERGE );
1864 BOOST_CHECK_EQUAL( readAll( workDir + wxT( "file.txt" ) ), conflictedBytes );
1865
1866 // An in-progress merge whose conflicts were staged as resolved is still not an ordinary
1867 // commit: the single-parent path would record the wrong parent structure.
1868 BOOST_REQUIRE_EQUAL( git_repository_index( &index, repo ), GIT_OK );
1869 BOOST_REQUIRE_EQUAL( git_index_add_bypath( index, "file.txt" ), GIT_OK );
1870 BOOST_REQUIRE_EQUAL( git_index_write( index ), GIT_OK );
1871 git_index_free( index );
1872
1873 BOOST_CHECK( handler.PerformCommit( { wxT( "file.txt" ) }, wxT( "Oops" ), TEST_AUTHOR_NAME, TEST_AUTHOR_EMAIL )
1875 BOOST_CHECK_EQUAL( git_repository_state( repo ), GIT_REPOSITORY_STATE_MERGE );
1876
1877 git_repository_free( repo );
1878}
1879
1880
1881// A surviving staged entry would leave a phantom New row in the checklist.
1882BOOST_AUTO_TEST_CASE( DiscardChangesDropsStagedEntryForNeverCommittedFile )
1883{
1884 BOOST_TEST_REQUIRE( ready() );
1885
1886 git_repository* repo = openRepo();
1887 BOOST_TEST_REQUIRE( repo );
1888
1889 wxString workDir = wxString::FromUTF8( git_repository_workdir( repo ) );
1890
1891 {
1892 std::ofstream f( ( workDir + wxT( "staged.txt" ) ).ToStdString(), std::ios::binary );
1893 f << "staged outside\n";
1894 }
1895
1896 git_index* index = nullptr;
1897 BOOST_REQUIRE_EQUAL( git_repository_index( &index, repo ), GIT_OK );
1898 BOOST_REQUIRE_EQUAL( git_index_add_bypath( index, "staged.txt" ), GIT_OK );
1899 BOOST_REQUIRE_EQUAL( git_index_write( index ), GIT_OK );
1900 git_index_free( index );
1901
1902 BOOST_TEST_REQUIRE( wxRemoveFile( workDir + wxT( "staged.txt" ) ) );
1903
1904 KIGIT_COMMON common( repo );
1905 wxString error;
1906
1907 BOOST_REQUIRE_MESSAGE( common.DiscardChanges( { wxT( "staged.txt" ) }, error ), error );
1908 BOOST_CHECK( !common.HasUncommittedTrackedChanges() );
1909
1910 git_repository_free( repo );
1911}
1912
1913
1914// A second operation that cannot get the lock returns Busy without touching the cancellation
1915// of the one holding it, and a cancelled operation leaves none behind for the next.
1916BOOST_AUTO_TEST_CASE( BusyOperationDoesNotConsumeAnotherCancellation )
1917{
1918 BOOST_TEST_REQUIRE( ready() );
1919
1920 git_repository* repo = openRepo();
1921 BOOST_TEST_REQUIRE( repo );
1922
1923 addOrigin( repo );
1924
1925 KIGIT_COMMON common( repo );
1926
1927 GIT_PUSH_HANDLER pushA( &common );
1928 KIGIT_COMMON::CANCEL_TOKEN tokenA = pushA.CancelToken();
1929
1930 BOOST_REQUIRE( tokenA );
1931 BOOST_REQUIRE( !tokenA->load() );
1932
1933 {
1934 // What the backend does for A once it owns the action lock.
1935 KIGIT_COMMON::OPERATION_GUARD operationA( &common, tokenA );
1936
1937 tokenA->store( true );
1938
1939 // A is holding the action lock, so B cannot start.
1940 std::unique_lock<std::mutex> heldByA = common.LockAction();
1941
1942 GIT_PUSH_HANDLER pushB( &common );
1943 KIGIT_COMMON::CANCEL_TOKEN tokenB = pushB.CancelToken();
1944
1945 BOOST_CHECK_MESSAGE( tokenB != tokenA, "each operation must own its own token" );
1946
1947 // From another thread, since trying a lock from the thread that holds it is not defined.
1948 PushResult resultB = std::async( std::launch::async,
1949 [&pushB]()
1950 {
1951 return pushB.PerformPush();
1952 } )
1953 .get();
1954
1955 BOOST_CHECK_EQUAL( static_cast<int>( resultB ), static_cast<int>( PushResult::Busy ) );
1956
1957 BOOST_CHECK_MESSAGE( tokenA->load(), "a Busy operation must not clear another's cancellation" );
1958 BOOST_CHECK_MESSAGE( !tokenB->load(), "a Busy operation must not inherit another's cancellation" );
1959 BOOST_CHECK_MESSAGE( common.GetRecentCommits().empty(),
1960 "a Busy operation must leave the active read cancelled" );
1961 }
1962
1963 BOOST_CHECK_MESSAGE( !common.GetRecentCommits().empty(),
1964 "later reads must not inherit the finished operation's cancellation" );
1965
1966 // A cancelled operation used to leave its token published for the next one to adopt.
1967 GIT_PUSH_HANDLER pushC( &common );
1968
1969 BOOST_CHECK_MESSAGE( pushC.CancelToken() != tokenA, "a later operation must not inherit the retired token" );
1970 BOOST_CHECK_MESSAGE( !pushC.IsThisOperationCancelled(), "a later operation must start uncancelled" );
1971
1972 git_repository_free( repo );
1973}
1974
1975
1976// Cancel from the first progress update.
1978{
1979public:
1980 void SetNumPhases( int ) override {}
1981 void AddPhases( int ) override {}
1982 void BeginPhase( int ) override {}
1983 void AdvancePhase() override {}
1984 void AdvancePhase( const wxString& ) override {}
1985 void Report( const wxString& ) override {}
1986 void SetCurrentProgress( double ) override {}
1987 void SetMaxProgress( int ) override {}
1988 void AdvanceProgress() override {}
1989 void SetTitle( const wxString& ) override {}
1990 bool IsCancelled() const override { return true; }
1991
1992 bool KeepRefreshing( bool = false ) override
1993 {
1994 m_ticks++;
1995 return false;
1996 }
1997
1998 int Ticks() const { return m_ticks; }
1999
2000private:
2001 int m_ticks = 0;
2002};
2003
2004
2005// Cancelling in the progress dialog must stop the clone and remove its partial checkout.
2006BOOST_AUTO_TEST_CASE( Clone_CancelFromProgressReporterStopsTheClone )
2007{
2008 BOOST_TEST_REQUIRE( ready() );
2009
2010 wxString dest = tempBase() + wxFileName::GetPathSeparator() + wxT( "clone_cancelled" );
2011
2012 KIGIT_COMMON common( nullptr );
2013 GIT_CLONE_HANDLER handler( &common );
2014
2015 handler.SetRemote( wxS( "file://" ) + remotePath() );
2016 handler.SetClonePath( dest );
2017
2018 auto reporter = std::make_unique<CLONE_CANCEL_REPORTER>();
2019 CLONE_CANCEL_REPORTER* watcher = reporter.get();
2020
2021 handler.SetProgressReporter( std::move( reporter ) );
2022
2023 bool cloned = handler.PerformClone();
2024
2025 BOOST_REQUIRE_MESSAGE( watcher->Ticks() > 0, "the clone must report progress at least once" );
2026 BOOST_CHECK_MESSAGE( handler.IsThisOperationCancelled(), "the clone must record the cancellation" );
2027 BOOST_CHECK_MESSAGE( !cloned, "a clone cancelled from the progress dialog must not report success" );
2028 BOOST_CHECK_MESSAGE( !wxDirExists( dest ), "a cancelled clone must not leave a directory that blocks a retry" );
2029
2030 if( common.GetRepo() )
2031 {
2032 git_repository_free( common.GetRepo() );
2033 common.SetRepo( nullptr );
2034 }
2035}
2036
2037
2038// Cancel pressed while the task is still queued must stop it before any network work.
2039BOOST_AUTO_TEST_CASE( CancelRequestedWhileQueuedAbortsTransfer )
2040{
2041 BOOST_TEST_REQUIRE( ready() );
2042
2043 git_repository* repo = openRepo();
2044 BOOST_TEST_REQUIRE( repo );
2045
2046 createCommit( repo, wxT( "file.txt" ), wxT( "second\n" ), wxT( "Second commit" ) );
2047 addOrigin( repo );
2048
2049 KIGIT_COMMON common( repo );
2050
2051 // What the panel does: make the token, then cancel it before the worker starts.
2053 token->store( true );
2054
2055 GIT_PULL_HANDLER pullHandler( &common );
2056 pullHandler.SetCancelToken( token );
2057
2058 BOOST_CHECK( !pullHandler.PerformFetch() );
2059 BOOST_CHECK( pullHandler.IsThisOperationCancelled() );
2060
2061 GIT_PUSH_HANDLER pushHandler( &common );
2062 pushHandler.SetCancelToken( token );
2063
2064 BOOST_CHECK_EQUAL( static_cast<int>( pushHandler.PerformPush() ), static_cast<int>( PushResult::Error ) );
2065
2066 // The session was never shutting down, so it must not look like it afterwards.
2067 BOOST_CHECK( !common.IsShuttingDown() );
2068
2069 git_repository_free( repo );
2070}
2071
2072
2073// The session-wide shutdown flag reaches an operation that starts after it was raised.
2074BOOST_AUTO_TEST_CASE( ShutdownRequestAbortsAnOperationThatStartsAfterIt )
2075{
2076 BOOST_TEST_REQUIRE( ready() );
2077
2078 git_repository* repo = openRepo();
2079 BOOST_TEST_REQUIRE( repo );
2080
2081 createCommit( repo, wxT( "file.txt" ), wxT( "second\n" ), wxT( "Second commit" ) );
2082 addOrigin( repo );
2083
2084 KIGIT_COMMON common( repo );
2085
2086 common.SetShuttingDown( true );
2087
2088 GIT_PULL_HANDLER pullHandler( &common );
2089 BOOST_CHECK( !pullHandler.PerformFetch() );
2090
2091 GIT_PUSH_HANDLER pushHandler( &common );
2092 PushResult result = pushHandler.PerformPush();
2093
2094 BOOST_CHECK_EQUAL( static_cast<int>( result ), static_cast<int>( PushResult::Error ) );
2095 BOOST_CHECK( common.IsShuttingDown() );
2096
2097 git_repository_free( repo );
2098}
2099
2100
2101BOOST_AUTO_TEST_CASE( Amend_MessageOnlyRewritesHeadKeepsTree )
2102{
2103 BOOST_TEST_REQUIRE( ready() );
2104
2105 git_repository* repo = openRepo();
2106 BOOST_TEST_REQUIRE( repo );
2107
2108 git_reference* headRefBefore = nullptr;
2109 git_repository_head( &headRefBefore, repo );
2110 git_oid oidBefore = *git_reference_target( headRefBefore );
2111
2112 git_commit* commitBefore = nullptr;
2113 git_reference_peel( (git_object**) &commitBefore, headRefBefore, GIT_OBJECT_COMMIT );
2114 git_oid treeBefore = *git_commit_tree_id( commitBefore );
2115
2116 git_commit_free( commitBefore );
2117 git_reference_free( headRefBefore );
2118
2119 GIT_COMMIT_HANDLER handler( repo );
2120 CommitResult result = handler.PerformAmend( {}, wxT( "Amended message" ), TEST_AUTHOR_NAME, TEST_AUTHOR_EMAIL );
2121 BOOST_CHECK_EQUAL( static_cast<int>( result ), static_cast<int>( CommitResult::Success ) );
2122
2123 git_reference* headRefAfter = nullptr;
2124 git_repository_head( &headRefAfter, repo );
2125 git_oid oidAfter = *git_reference_target( headRefAfter );
2126
2127 git_commit* commitAfter = nullptr;
2128 git_reference_peel( (git_object**) &commitAfter, headRefAfter, GIT_OBJECT_COMMIT );
2129 git_oid treeAfter = *git_commit_tree_id( commitAfter );
2130
2131 BOOST_CHECK( !git_oid_equal( &oidBefore, &oidAfter ) );
2132 BOOST_CHECK( git_oid_equal( &treeBefore, &treeAfter ) );
2133
2134 wxString amendedMsg = wxString::FromUTF8( git_commit_message( commitAfter ) );
2135 BOOST_CHECK_EQUAL( amendedMsg.Trim(), wxString( "Amended message" ) );
2136
2137 git_commit_free( commitAfter );
2138 git_reference_free( headRefAfter );
2139 git_repository_free( repo );
2140}
2141
2142
2143BOOST_AUTO_TEST_CASE( Amend_StagedFileChangesTreeAndKeepsParent )
2144{
2145 BOOST_TEST_REQUIRE( ready() );
2146
2147 git_repository* repo = openRepo();
2148 BOOST_TEST_REQUIRE( repo );
2149
2150 git_reference* headRefBefore = nullptr;
2151 git_repository_head( &headRefBefore, repo );
2152
2153 git_commit* commitBefore = nullptr;
2154 git_reference_peel( (git_object**) &commitBefore, headRefBefore, GIT_OBJECT_COMMIT );
2155 git_oid treeBefore = *git_commit_tree_id( commitBefore );
2156 unsigned int parentCountBefore = git_commit_parentcount( commitBefore );
2157
2158 git_commit_free( commitBefore );
2159 git_reference_free( headRefBefore );
2160
2161 // Add a new file to the working tree so the amend has something to stage.
2162 wxString newFile = repoPath() + wxFileName::GetPathSeparator() + wxT( "new.txt" );
2163 {
2164 std::ofstream f( newFile.ToStdString() );
2165 f << "new content\n";
2166 }
2167
2168 GIT_COMMIT_HANDLER handler( repo );
2169 CommitResult result = handler.PerformAmend( { wxT( "new.txt" ) }, wxT( "Amended with file" ), TEST_AUTHOR_NAME,
2171 BOOST_CHECK_EQUAL( static_cast<int>( result ), static_cast<int>( CommitResult::Success ) );
2172
2173 git_reference* headRefAfter = nullptr;
2174 git_repository_head( &headRefAfter, repo );
2175
2176 git_commit* commitAfter = nullptr;
2177 git_reference_peel( (git_object**) &commitAfter, headRefAfter, GIT_OBJECT_COMMIT );
2178 git_oid treeAfter = *git_commit_tree_id( commitAfter );
2179 unsigned int parentCountAfter = git_commit_parentcount( commitAfter );
2180
2181 BOOST_CHECK( !git_oid_equal( &treeBefore, &treeAfter ) );
2182 BOOST_CHECK_EQUAL( parentCountAfter, parentCountBefore );
2183
2184 // new.txt should now be in HEAD's tree.
2185 git_tree* treeObj = nullptr;
2186 git_commit_tree( &treeObj, commitAfter );
2187
2188 const git_tree_entry* entry = git_tree_entry_byname( treeObj, "new.txt" );
2189 BOOST_CHECK( entry != nullptr );
2190
2191 git_tree_free( treeObj );
2192 git_commit_free( commitAfter );
2193 git_reference_free( headRefAfter );
2194 git_repository_free( repo );
2195}
2196
2197
2198// A repository with no commits yet must fail cleanly rather than crash or write a bogus commit.
2199BOOST_AUTO_TEST_CASE( Amend_UnbornBranchReturnsError )
2200{
2201 BOOST_TEST_REQUIRE( ready() );
2202
2203 wxString unbornDir = repoPath() + wxT( "_unborn" );
2204 wxFileName::Mkdir( unbornDir, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
2205
2206 git_repository* unbornRepo = nullptr;
2207 BOOST_TEST_REQUIRE( git_repository_init( &unbornRepo, unbornDir.ToUTF8().data(), 0 ) == 0 );
2208
2209 GIT_COMMIT_HANDLER handler( unbornRepo );
2210 CommitResult result = handler.PerformAmend( {}, wxT( "should fail" ), TEST_AUTHOR_NAME, TEST_AUTHOR_EMAIL );
2211
2212 BOOST_CHECK_EQUAL( static_cast<int>( result ), static_cast<int>( CommitResult::Error ) );
2213 BOOST_CHECK( !handler.GetErrorString().IsEmpty() );
2214
2215 git_repository_free( unbornRepo );
2216
2217 if( wxFileName::DirExists( unbornDir ) )
2218 wxFileName::Rmdir( unbornDir, wxPATH_RMDIR_RECURSIVE );
2219}
2220
2221
2222BOOST_AUTO_TEST_CASE( SetupRemote_PreservesSshSchemePortAndUsername )
2223{
2224 BOOST_TEST_REQUIRE( ready() );
2225
2226 KIGIT::GitRepositoryPtr repo( openRepo() );
2227 BOOST_REQUIRE( repo );
2228
2229 KIGIT_COMMON common( repo.get() );
2230 GIT_INIT_HANDLER handler( &common );
2232 config.url = wxS( "ssh://[email protected]:29022/srv/git/repos/l3-auth-30.git" );
2233 config.username = wxS( "saved-user" );
2234 config.password = wxS( "key-passphrase" );
2236
2237 BOOST_REQUIRE( handler.SetupRemote( config ) );
2238 BOOST_CHECK_EQUAL( readConfig( repo.get(), wxS( "remote.origin.url" ) ).ToStdString(), config.url.ToStdString() );
2239 BOOST_CHECK_EQUAL( common.GetRemote().ToStdString(), config.url.ToStdString() );
2240 BOOST_CHECK_EQUAL( common.GetUsername().ToStdString(), "runner" );
2241 BOOST_CHECK_EQUAL( common.GetPassword().ToStdString(), "key-passphrase" );
2242 BOOST_CHECK( common.GetConnType() == KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_SSH );
2243}
2244
2245
2246BOOST_AUTO_TEST_CASE( SetupRemote_UpdatesExistingOriginWithScpAddress )
2247{
2248 BOOST_TEST_REQUIRE( ready() );
2249
2250 KIGIT::GitRepositoryPtr repo( openRepo() );
2251 BOOST_REQUIRE( repo );
2252 BOOST_REQUIRE( addOrigin( repo.get() ) );
2253
2254 KIGIT_COMMON common( repo.get() );
2255 GIT_INIT_HANDLER handler( &common );
2257 config.url = wxS( "runner@host:path/repo" );
2258 config.username = wxS( "saved-user" );
2260
2261 BOOST_REQUIRE( handler.SetupRemote( config ) );
2262 BOOST_CHECK_EQUAL( readConfig( repo.get(), wxS( "remote.origin.url" ) ).ToStdString(), config.url.ToStdString() );
2263 BOOST_CHECK_EQUAL( common.GetRemote().ToStdString(), config.url.ToStdString() );
2264 BOOST_CHECK_EQUAL( common.GetUsername().ToStdString(), "runner" );
2265 BOOST_CHECK( common.GetConnType() == KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_SSH );
2266}
2267
2268
2269BOOST_AUTO_TEST_CASE( SetupRemote_ExtractsHttpCredentialsWithoutPersistingThem )
2270{
2271 BOOST_TEST_REQUIRE( ready() );
2272
2273 KIGIT::GitRepositoryPtr repo( openRepo() );
2274 BOOST_REQUIRE( repo );
2275
2276 KIGIT_COMMON common( repo.get() );
2277 GIT_INIT_HANDLER handler( &common );
2279 config.url = wxS( "https://url-user:pa@ss:[email protected]:8443/x/[email protected]" );
2280 config.username = wxS( "saved-user" );
2282
2283 BOOST_REQUIRE( handler.SetupRemote( config ) );
2284 BOOST_CHECK_EQUAL( readConfig( repo.get(), wxS( "remote.origin.url" ) ).ToStdString(),
2285 "https://example.com:8443/x/[email protected]" );
2286 BOOST_CHECK_EQUAL( common.GetRemote().ToStdString(), "https://example.com:8443/x/[email protected]" );
2287 BOOST_CHECK_EQUAL( common.GetUsername().ToStdString(), "url-user" );
2288 BOOST_CHECK_EQUAL( common.GetPassword().ToStdString(), "pa@ss:word" );
2290
2291 // A password typed separately replaces the one pasted in the URL.
2292 config.password = wxS( "typed@pass:word" );
2293 BOOST_REQUIRE( handler.SetupRemote( config ) );
2294 BOOST_CHECK_EQUAL( readConfig( repo.get(), wxS( "remote.origin.url" ) ).ToStdString(),
2295 "https://example.com:8443/x/[email protected]" );
2296 BOOST_CHECK_EQUAL( common.GetPassword().ToStdString(), "typed@pass:word" );
2297}
2298
2299
2300BOOST_AUTO_TEST_CASE( SetupRemote_KeepsSeparateHttpCredentialsForImmediateFetch )
2301{
2302 BOOST_TEST_REQUIRE( ready() );
2303
2304 KIGIT::GitRepositoryPtr repo( openRepo() );
2305 BOOST_REQUIRE( repo );
2306
2307 KIGIT_COMMON common( repo.get() );
2308 GIT_INIT_HANDLER handler( &common );
2310 config.url = wxS( "http://example.com/repo.git" );
2311 config.username = wxS( "alice" );
2312 config.password = wxS( "typed@pass:word" );
2314
2315 BOOST_REQUIRE( handler.SetupRemote( config ) );
2316 BOOST_CHECK_EQUAL( readConfig( repo.get(), wxS( "remote.origin.url" ) ).ToStdString(), config.url.ToStdString() );
2317 BOOST_CHECK_EQUAL( common.GetRemote().ToStdString(), config.url.ToStdString() );
2318 BOOST_CHECK_EQUAL( common.GetUsername().ToStdString(), "alice" );
2319 BOOST_CHECK_EQUAL( common.GetPassword().ToStdString(), "typed@pass:word" );
2320}
2321
2322
2323BOOST_AUTO_TEST_CASE( InitializeRepository_SeedsGitignoreWithKicadEntries )
2324{
2325 BOOST_TEST_REQUIRE( ready() );
2326
2327 wxString freshDir = repoPath() + wxT( "_fresh" );
2328 wxFileName::Mkdir( freshDir, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
2329
2330 KIGIT_COMMON common( nullptr );
2331 GIT_INIT_HANDLER handler( &common );
2332
2333 InitResult result = handler.InitializeRepository( freshDir );
2334 BOOST_REQUIRE_EQUAL( static_cast<int>( result ), static_cast<int>( InitResult::Success ) );
2335
2336 wxFileName gitignoreFile( freshDir, wxT( ".gitignore" ) );
2337 BOOST_TEST_REQUIRE( gitignoreFile.FileExists() );
2338
2339 KIGIT::GitRepositoryPtr repo( common.GetRepo() );
2340 common.SetRepo( nullptr );
2341 BOOST_REQUIRE( repo );
2342 useEmptyGlobalIgnore( repo.get() );
2343
2344 const std::pair<const char*, bool> cases[] = {
2345 { "board.kicad_pcb", false },
2346 { "lib.kicad_sym", false },
2347 { "board.kicad_pcb-bak", true },
2348 { "board.lck", true },
2349 { "board.LCK", true },
2350 { "~board.kicad_pcb", true },
2351 { "proj.kicad_prl", true },
2352 { "upper.KiCaD_PrL", true },
2353 { "proj-backups/board.zip", true },
2354 { "data-proj-backups/board.kicad_pcb", true },
2355 { "nested/proj-backups/old/board.zip", true },
2356 { "proj-backups-old/board.kicad_pcb", false },
2357 { ".history/board.kicad_pcb", true },
2358 { ".history/.git/objects/ab/cdef", true },
2359 { ".history-notes/board.kicad_pcb", false },
2360 { "named/.history", false },
2361 { "named/proj-backups", false },
2362 { "_autosave-board.kicad_pcb", true },
2363 { "_autosave-design/board.kicad_pcb", true },
2364 { "fp-info-cache", true },
2365 { "fp-info-cache.kicad_pcb", false },
2366 { "cache.kicad_prl/board.kicad_pcb", true },
2367 { "board.kicad_pcb.mine", true },
2368 { "board.kicad_pcb.theirs", true },
2369 { "board.kicad_pcb.mine.1", true },
2370 { "board.kicad_pcb.theirs.17", true },
2371 { "board.kicad_pcb.mine.99", true },
2372 { "board.kicad_pcb.mine.100", false },
2373 { "board.kicad_pcb.mine.1notes", false },
2374 { "board.kicad_pcb.theirs.notes", false },
2375 { "board.mine.kicad_pcb", false },
2376 { "board.kicad_pcb.1", false },
2377 };
2378
2379 for( const auto& [path, transient] : cases )
2380 {
2382 {
2383 KI_TEST::WriteGitFile( freshDir, wxString::FromUTF8( path ), "test file\n" );
2384 int ignored = 0;
2385 BOOST_REQUIRE_EQUAL( git_ignore_path_is_ignored( &ignored, repo.get(), path ), GIT_OK );
2386 BOOST_CHECK_EQUAL( ignored != 0, transient );
2388 }
2389 }
2390
2391 repo.reset();
2392
2393 if( wxFileName::DirExists( freshDir ) )
2394 wxFileName::Rmdir( freshDir, wxPATH_RMDIR_RECURSIVE );
2395}
2396
2397
2398// A curated .gitignore must not be edited. Appending defaults dirtied a fresh clone.
2399BOOST_AUTO_TEST_CASE( InitializeRepository_LeavesExistingGitignoreUntouched )
2400{
2401 BOOST_TEST_REQUIRE( ready() );
2402
2403 wxString freshDir = repoPath() + wxT( "_existing" );
2404 wxFileName::Mkdir( freshDir, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
2405
2406 const wxString curated = wxT( "# my custom ignores\n.history/\nbuild/\n" );
2407 wxFileName gitignoreFile( freshDir, wxT( ".gitignore" ) );
2408
2409 {
2410 wxFFile f( gitignoreFile.GetFullPath(), wxT( "wb" ) );
2411 f.Write( curated );
2412 }
2413
2414 KIGIT_COMMON common( nullptr );
2415 GIT_INIT_HANDLER handler( &common );
2416
2417 InitResult result = handler.InitializeRepository( freshDir );
2418 BOOST_CHECK_EQUAL( static_cast<int>( result ), static_cast<int>( InitResult::Success ) );
2419
2420 wxString after;
2421 {
2422 wxFFile f( gitignoreFile.GetFullPath(), wxT( "rb" ) );
2423 f.ReadAll( &after );
2424 }
2425
2426 BOOST_CHECK_EQUAL( after.ToStdString(), curated.ToStdString() );
2427
2428 if( wxFileName::DirExists( freshDir ) )
2429 wxFileName::Rmdir( freshDir, wxPATH_RMDIR_RECURSIVE );
2430}
2431
2432
2433// An empty file is still the team's file, so it stays empty rather than being filled in.
2434BOOST_AUTO_TEST_CASE( InitializeRepository_LeavesEmptyGitignoreEmpty )
2435{
2436 BOOST_TEST_REQUIRE( ready() );
2437
2438 wxString freshDir = repoPath() + wxT( "_emptyignore" );
2439 wxFileName::Mkdir( freshDir, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
2440
2441 wxFileName gitignoreFile( freshDir, wxT( ".gitignore" ) );
2442
2443 {
2444 wxFFile f( gitignoreFile.GetFullPath(), wxT( "wb" ) );
2445 }
2446
2447 KIGIT_COMMON common( nullptr );
2448 GIT_INIT_HANDLER handler( &common );
2449
2450 BOOST_CHECK_EQUAL( static_cast<int>( handler.InitializeRepository( freshDir ) ),
2451 static_cast<int>( InitResult::Success ) );
2452
2453 BOOST_TEST_REQUIRE( gitignoreFile.FileExists() );
2454 BOOST_CHECK_EQUAL( gitignoreFile.GetSize().GetValue(), 0u );
2455
2456 if( wxFileName::DirExists( freshDir ) )
2457 wxFileName::Rmdir( freshDir, wxPATH_RMDIR_RECURSIVE );
2458}
2459
2460
2461// LoadProject() needs a real project file with a meta section.
2462static wxString seedProjectFile( const wxString& aDir )
2463{
2464 wxString path = aDir + wxFileName::GetPathSeparator() + wxT( "proj.kicad_pro" );
2465
2466 if( !wxFileName::FileExists( path ) )
2467 {
2468 wxFFile f( path, wxT( "w" ) );
2469 f.Write( wxT( "{\"meta\": {\"filename\": \"proj.kicad_pro\", \"version\": 3}}" ) );
2470 }
2471
2472 return path;
2473}
2474
2475
2476BOOST_AUTO_TEST_CASE( ProjectGitCommon_LazyCreateAttachRelease )
2477{
2478 BOOST_TEST_REQUIRE( ready() );
2479
2480 wxString projectPath = seedProjectFile( repoPath() );
2481
2482 SETTINGS_MANAGER mgr;
2483 BOOST_TEST_REQUIRE( mgr.LoadProject( projectPath ) );
2484
2485 PROJECT* project = mgr.GetProject( projectPath );
2486 BOOST_TEST_REQUIRE( project != nullptr );
2487
2488 std::shared_ptr<KIGIT_COMMON> common = project->GetGitCommon();
2489 BOOST_TEST_REQUIRE( common != nullptr );
2490 BOOST_CHECK( common->GetRepo() == nullptr );
2491 BOOST_CHECK( common == project->GetGitCommon() );
2492
2493 BOOST_TEST_REQUIRE( project->AttachGitRepository() );
2494 git_repository* repo = common->GetRepo();
2495 BOOST_TEST_REQUIRE( repo != nullptr );
2496 BOOST_CHECK( !common->GetProjectDir().IsEmpty() );
2497
2498 BOOST_TEST_REQUIRE( project->AttachGitRepository() );
2499 BOOST_CHECK( common->GetRepo() == repo );
2500
2501 project->ReleaseGitCommon();
2502
2503 std::shared_ptr<KIGIT_COMMON> fresh = project->GetGitCommon();
2504 BOOST_TEST_REQUIRE( fresh != nullptr );
2505 BOOST_CHECK( fresh != common );
2506 BOOST_CHECK( fresh->GetRepo() == nullptr );
2507 BOOST_CHECK( common->GetRepo() == nullptr );
2508
2509 // Unloading with the repository still attached exercises the destructor release path.
2510 BOOST_TEST_REQUIRE( project->AttachGitRepository() );
2511 BOOST_CHECK( mgr.UnloadProject( project, false ) );
2512}
2513
2514
2515BOOST_AUTO_TEST_CASE( ProjectGitCommon_ReleaseSkipsFreeWhenBackendGone )
2516{
2517 BOOST_TEST_REQUIRE( ready() );
2518
2519 wxString projectPath = seedProjectFile( repoPath() );
2520
2521 SETTINGS_MANAGER mgr;
2522 BOOST_TEST_REQUIRE( mgr.LoadProject( projectPath ) );
2523
2524 PROJECT* project = mgr.GetProject( projectPath );
2525 BOOST_TEST_REQUIRE( project != nullptr );
2526 BOOST_TEST_REQUIRE( project->AttachGitRepository() );
2527
2528 git_repository* repo = project->GetGitCommon()->GetRepo();
2529 BOOST_TEST_REQUIRE( repo != nullptr );
2530
2531 // Simulate the process-exit ordering where the backend is torn down before the
2532 // last project. The release must clear the handle without freeing it, since a free
2533 // would call into a shut-down libgit2.
2534 GIT_BACKEND* backend = GetGitBackend();
2535 SetGitBackend( nullptr );
2536
2537 project->ReleaseGitCommon();
2538
2539 BOOST_CHECK( project->GetGitCommon()->GetRepo() == nullptr );
2540
2541 SetGitBackend( backend );
2542
2543 // The release skipped the free on purpose, so reclaim the handle now.
2544 git_repository_free( repo );
2545}
2546
2547
2548BOOST_AUTO_TEST_CASE( ProjectGitCommon_AttachOutsideRepositoryFails )
2549{
2550 BOOST_TEST_REQUIRE( ready() );
2551
2552 wxFileName tempBase( repoPath(), wxEmptyString );
2553 tempBase.RemoveLastDir();
2554 wxString outsideDir = tempBase.GetPath() + wxFileName::GetPathSeparator() + wxT( "no_repo_here" );
2555 wxFileName::Mkdir( outsideDir, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL );
2556
2557 wxString projectPath = seedProjectFile( outsideDir );
2558
2559 SETTINGS_MANAGER mgr;
2560 BOOST_TEST_REQUIRE( mgr.LoadProject( projectPath ) );
2561
2562 PROJECT* project = mgr.GetProject( projectPath );
2563 BOOST_TEST_REQUIRE( project != nullptr );
2564
2565 BOOST_CHECK( !project->AttachGitRepository() );
2566 BOOST_CHECK( project->GetGitCommon()->GetRepo() == nullptr );
2567}
2568
2569
2570BOOST_AUTO_TEST_CASE( ProjectGitCommon_AttachHonorsDisabledSetting )
2571{
2572 BOOST_TEST_REQUIRE( ready() );
2573
2574 wxString projectPath = seedProjectFile( repoPath() );
2575
2576 SETTINGS_MANAGER mgr;
2577 BOOST_TEST_REQUIRE( mgr.LoadProject( projectPath ) );
2578
2579 PROJECT* project = mgr.GetProject( projectPath );
2580 BOOST_TEST_REQUIRE( project != nullptr );
2581
2582 project->GetLocalSettings().m_GitIntegrationDisabled = true;
2583
2584 BOOST_CHECK( !project->AttachGitRepository() );
2585 BOOST_CHECK( project->GetGitCommon()->GetRepo() == nullptr );
2586
2587 project->GetLocalSettings().m_GitIntegrationDisabled = false;
2588
2589 BOOST_CHECK( project->AttachGitRepository() );
2590 BOOST_CHECK( project->GetGitCommon()->GetRepo() != nullptr );
2591
2592 project->ReleaseGitCommon();
2593}
2594
2595
2596BOOST_AUTO_TEST_CASE( ProjectGitCommon_ReleaseDefersFreeWhileActionMutexHeld )
2597{
2598 BOOST_TEST_REQUIRE( ready() );
2599
2600 wxString projectPath = seedProjectFile( repoPath() );
2601
2602 SETTINGS_MANAGER mgr;
2603 BOOST_TEST_REQUIRE( mgr.LoadProject( projectPath ) );
2604
2605 PROJECT* project = mgr.GetProject( projectPath );
2606 BOOST_TEST_REQUIRE( project != nullptr );
2607
2608 BOOST_TEST_REQUIRE( project->AttachGitRepository() );
2609 std::shared_ptr<KIGIT_COMMON> common = project->GetGitCommon();
2610
2611 std::promise<void> lockHeld;
2612 std::promise<void> allowUnlock;
2613
2614 std::thread worker(
2615 [&]()
2616 {
2617 std::unique_lock<std::mutex> lock = common->LockAction();
2618 lockHeld.set_value();
2619 allowUnlock.get_future().wait();
2620 } );
2621
2622 lockHeld.get_future().wait();
2623
2624 // The worker holds the action mutex, so the release must defer the free to the
2625 // orphan registry instead of blocking. The repo handle must stay valid for the
2626 // running operation until it finishes.
2627 project->ReleaseGitCommon();
2628
2629 BOOST_CHECK( common->GetRepo() != nullptr );
2630
2631 allowUnlock.set_value();
2632 worker.join();
2633
2634 BOOST_REQUIRE_EQUAL( GetGitBackend()->OrphanRegistry().JoinAll( std::chrono::seconds( 5 ) ), 0u );
2635 BOOST_CHECK( common->GetRepo() == nullptr );
2636}
2637
2638
2639// A listed path that is not a regular file is still a path. A symlink to a directory, and a
2640// submodule, both look absent to a regular-file test, and recording them as deletions drops
2641// them out of the project.
2642#ifndef __WINDOWS__
2643BOOST_AUTO_TEST_CASE( CommitKeepsAPathThatIsNotARegularFile )
2644{
2645 BOOST_TEST_REQUIRE( ready() );
2646
2647 git_repository* repo = openRepo();
2648 BOOST_TEST_REQUIRE( repo );
2649
2650 wxString sep = wxFileName::GetPathSeparator();
2651 BOOST_REQUIRE( wxFileName::Mkdir( repoPath() + sep + wxT( "shared" ), 0700, wxPATH_MKDIR_FULL ) );
2652
2653 {
2654 std::ofstream f( ( repoPath() + sep + wxT( "shared" ) + sep + wxT( "lib.txt" ) ).ToStdString() );
2655 f << "shared\n";
2656 }
2657
2658 BOOST_REQUIRE_EQUAL( symlink( "shared", ( repoPath() + sep + wxT( "link" ) ).ToUTF8().data() ), 0 );
2659
2660 GIT_COMMIT_HANDLER handler( repo );
2661
2662 BOOST_REQUIRE( handler.PerformCommit( { wxT( "link" ) }, wxT( "Add the shared library link" ), wxT( "Test Author" ),
2663 wxT( "[email protected]" ) )
2665
2666 git_oid headOid;
2667 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &headOid, repo, "HEAD" ), GIT_OK );
2668
2669 git_commit* head = nullptr;
2670 BOOST_REQUIRE_EQUAL( git_commit_lookup( &head, repo, &headOid ), GIT_OK );
2671
2672 git_tree* tree = nullptr;
2673 BOOST_REQUIRE_EQUAL( git_commit_tree( &tree, head ), GIT_OK );
2674
2675 git_tree_entry* entry = nullptr;
2676 BOOST_CHECK_MESSAGE( git_tree_entry_bypath( &entry, tree, "link" ) == GIT_OK,
2677 "the commit dropped the symlink instead of recording it" );
2678
2679 if( entry )
2680 git_tree_entry_free( entry );
2681
2682 git_tree_free( tree );
2683 git_commit_free( head );
2684 git_repository_free( repo );
2685}
2686#endif
2687
2688
2689BOOST_AUTO_TEST_CASE( IsTransientGitFile_HandlesDosPaths )
2690{
2692
2693 BOOST_CHECK( PROJECT_GIT_UTILS::IsTransientGitFile( wxFileName( "proj\\.history\\board.kicad_pcb", wxPATH_DOS ) ) );
2694 BOOST_CHECK(
2695 PROJECT_GIT_UTILS::IsTransientGitFile( wxFileName( "proj\\data-proj-backups\\board.zip", wxPATH_DOS ) ) );
2696 BOOST_CHECK( PROJECT_GIT_UTILS::IsTransientGitFile( wxFileName( "proj\\settings.KICAD_PRL", wxPATH_DOS ) ) );
2697 BOOST_CHECK( PROJECT_GIT_UTILS::IsTransientGitFile( wxFileName( "proj\\board.kicad_pcb.theirs.99", wxPATH_DOS ) ) );
2698 BOOST_CHECK( !PROJECT_GIT_UTILS::IsTransientGitFile(
2699 wxFileName( "proj\\.history-notes\\board.kicad_pcb", wxPATH_DOS ) ) );
2700 BOOST_CHECK( !PROJECT_GIT_UTILS::IsTransientGitFile( wxFileName( "proj\\board.kicad_pcb.mine.100", wxPATH_DOS ) ) );
2701}
2702
2703
2704BOOST_AUTO_TEST_CASE( BuildGitChangeEntries_KeepsTrackedAndStagedTransientFiles )
2705{
2706 BOOST_REQUIRE( ready() );
2707 KIGIT::GitRepositoryPtr repo( openRepo() );
2708 BOOST_REQUIRE( repo );
2709 useEmptyGlobalIgnore( repo.get() );
2710
2711 // An ancestor outside the project must not hide its files.
2712 const wxString prefix = wxS( "workspace-backups/project/" );
2713 const wxString projectPath = repoPath() + wxS( "/" ) + prefix;
2714 KI_TEST::WriteGitFile( repoPath(), prefix + wxS( ".gitignore" ), "" );
2715 KI_TEST::WriteGitFile( repoPath(), prefix + wxS( "tracked.kicad_prl" ), "original settings\n" );
2716 KI_TEST::WriteGitFile( repoPath(), prefix + wxS( "deleted.kicad_pcb.mine" ), "original recovery\n" );
2717 KI_TEST::CommitAllGitFiles( repo.get(), wxS( "Track files the user chose to keep" ) );
2718
2719 KI_TEST::WriteGitFile( repoPath(), prefix + wxS( "tracked.kicad_prl" ), "changed settings with more data\n" );
2720 BOOST_REQUIRE( wxRemoveFile( projectPath + wxS( "deleted.kicad_pcb.mine" ) ) );
2721 KI_TEST::WriteGitFile( repoPath(), prefix + wxS( "staged.kicad_prl" ), "staged settings\n" );
2722
2723 git_index* rawIndex = nullptr;
2724 BOOST_REQUIRE_EQUAL( git_repository_index( &rawIndex, repo.get() ), GIT_OK );
2725 KIGIT::GitIndexPtr index( rawIndex );
2726 BOOST_REQUIRE_EQUAL( git_index_add_bypath( index.get(), ( prefix + wxS( "staged.kicad_prl" ) ).utf8_str() ),
2727 GIT_OK );
2728 BOOST_REQUIRE_EQUAL( git_index_write( index.get() ), GIT_OK );
2729
2730 const char* untracked[] = {
2731 "board.kicad_pcb", "untracked.kicad_prl", "board.kicad_pcb.theirs.17",
2732 "_autosave-board.kicad_pcb", "project-backups/board.zip", ".history/revisions/board.kicad_pcb",
2733 };
2734
2735 for( const char* path : untracked )
2736 KI_TEST::WriteGitFile( repoPath(), prefix + wxString::FromUTF8( path ), "new file\n" );
2737
2738 const std::set<wxString> expected = {
2739 prefix + wxS( "board.kicad_pcb" ),
2740 prefix + wxS( "tracked.kicad_prl" ),
2741 prefix + wxS( "deleted.kicad_pcb.mine" ),
2742 prefix + wxS( "staged.kicad_prl" ),
2743 };
2744
2745 auto checkEntries = [&repo, &expected]( const wxString& aProjectPath )
2746 {
2747 KIGIT_COMMON common( repo.get(), aProjectPath );
2748 std::unique_lock<std::mutex> lock = common.LockAction();
2749 std::set<wxString> paths;
2750
2751 for( const FILE_CHECKLIST_ENTRY& entry : BuildGitChangeEntries( common, aProjectPath, false ) )
2752 paths.insert( entry.path );
2753
2754 BOOST_CHECK_EQUAL_COLLECTIONS( paths.begin(), paths.end(), expected.begin(), expected.end() );
2755 };
2756
2757 checkEntries( projectPath );
2758
2759#ifndef __WINDOWS__
2760 const wxString alias = tempBase() + wxS( "/project-alias" );
2761 BOOST_REQUIRE_EQUAL( symlink( projectPath.utf8_str(), alias.utf8_str() ), 0 );
2762 checkEntries( alias );
2763#endif
2764}
2765
2766
2767// WriteTreeToDir must reproduce the full tree of a ref, including files in subdirectories, so a
2768// hierarchical schematic's sub-sheets are present when the committed side is diffed.
2769BOOST_AUTO_TEST_CASE( WriteTreeToDir_MaterializesSubdirectoryFiles )
2770{
2771 BOOST_TEST_REQUIRE( ready() );
2772
2773 git_repository* repo = openRepo();
2774 BOOST_TEST_REQUIRE( repo );
2775
2776 const wxString sep = wxFileName::GetPathSeparator();
2777
2778 createCommit( repo, wxT( "root.kicad_sch" ), wxT( "root\n" ), wxT( "add root" ) );
2779
2780 BOOST_REQUIRE( wxFileName::Mkdir( repoPath() + sep + wxT( "sub" ), wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
2781 createCommit( repo, wxT( "sub/child.kicad_sch" ), wxT( "child\n" ), wxT( "add child" ) );
2782
2783 // Under the fixture's own directory, so a failed run leaves nothing behind in the system temp.
2784 wxString dest = tempBase() + wxFileName::GetPathSeparator() + wxT( "tree_out" );
2785
2786 BOOST_CHECK( KIGIT::PROJECT_GIT_UTILS::WriteTreeToDir( repo, wxT( "HEAD" ), dest ) );
2787 BOOST_CHECK( wxFileName::FileExists( dest + sep + wxT( "root.kicad_sch" ) ) );
2788 BOOST_CHECK_MESSAGE( wxFileName::FileExists( dest + sep + wxT( "sub" ) + sep + wxT( "child.kicad_sch" ) ),
2789 "sub-sheet file in a subdirectory must be materialized alongside the root" );
2790
2791 if( wxFileName::DirExists( dest ) )
2792 wxFileName::Rmdir( dest, wxPATH_RMDIR_RECURSIVE );
2793
2794 git_repository_free( repo );
2795}
2796
2797
2798// The repository must stay untouched until the final commit, and that commit must also bring the
2799// working tree to the merged content.
2800BOOST_AUTO_TEST_CASE( MergeConflictRoundTrip )
2801{
2802 BOOST_TEST_REQUIRE( ready() );
2803
2804 git_repository* repo = openRepo();
2805 BOOST_TEST_REQUIRE( repo );
2806
2807 // Remember the shared base, then diverge: "mine" on HEAD, "remote" on a side commit.
2808 git_reference* headRef = nullptr;
2809 BOOST_REQUIRE_EQUAL( git_repository_head( &headRef, repo ), GIT_OK );
2810 git_oid baseOid = *git_reference_target( headRef );
2811 git_reference_free( headRef );
2812
2813 createCommit( repo, wxT( "file.txt" ), wxT( "mine\n" ), wxT( "Local change" ) );
2814
2815 // The remote-side commit shares the base but does not move HEAD.
2816 git_oid remoteCommitOid = createDetachedCommit( repo, baseOid, wxT( "file.txt" ), "remote\n", "Remote change" );
2817
2818 // A pull would leave this in FETCH_HEAD.
2819 writeFetchHead( remoteCommitOid );
2820
2821 KIGIT_COMMON common( repo );
2822
2823 wxString stageDir = tempBase() + wxFileName::GetPathSeparator() + wxT( "stages" );
2824 wxString headOid, theirsOid, conflictsError;
2825 KIGIT_ERROR_MESSAGE error;
2826 std::vector<KIGIT_CONFLICT_SIDES> conflicts;
2827
2828 BOOST_REQUIRE_MESSAGE( common.GetMergeConflicts( stageDir, headOid, theirsOid, conflicts, conflictsError ),
2829 "GetMergeConflicts failed: " + conflictsError.ToStdString() );
2830 BOOST_REQUIRE_EQUAL( conflicts.size(), 1u );
2831 BOOST_CHECK_EQUAL( conflicts[0].path.ToStdString(), "file.txt" );
2832 BOOST_CHECK( conflicts[0].hasAncestor && conflicts[0].hasOurs && conflicts[0].hasTheirs );
2833
2834 // Reported so the resolutions can be pinned to it.
2835 {
2836 char expect[GIT_OID_HEXSZ + 1] = { 0 };
2837 git_oid_tostr( expect, sizeof( expect ), &remoteCommitOid );
2838 BOOST_CHECK_EQUAL( theirsOid.ToStdString(), std::string( expect ) );
2839 }
2840
2841 auto readAll = []( const wxString& aPath )
2842 {
2843 std::ifstream in( aPath.ToStdString(), std::ios::binary );
2844 return std::string( ( std::istreambuf_iterator<char>( in ) ), std::istreambuf_iterator<char>() );
2845 };
2846
2847 BOOST_CHECK_EQUAL( readAll( conflicts[0].oursFile ), "mine\n" );
2848 BOOST_CHECK_EQUAL( readAll( conflicts[0].theirsFile ), "remote\n" );
2849 BOOST_CHECK_EQUAL( readAll( conflicts[0].ancestorFile ), "initial\n" );
2850
2851 // The repository must not have been touched by the conflict listing.
2852 BOOST_CHECK_EQUAL( git_repository_state( repo ), GIT_REPOSITORY_STATE_NONE );
2853
2854 // Refuse to apply resolutions against a moved head.
2856 static_cast<int>( common.CreateMergedCommit( wxT( "0000000000000000000000000000000000000000" ), theirsOid,
2857 { { conflicts[0].path, conflicts[0].oursFile } }, error ) ),
2858 static_cast<int>( MergeCommitResult::FailedCleanly ) );
2859
2860 // A background fetch can move FETCH_HEAD while the user resolves. The pinned commit id must
2861 // win over whatever FETCH_HEAD says by then.
2862 writeFetchHead( baseOid );
2863
2864 // A save landing while the user resolves must be refused, not reverted by the final
2865 // checkout.
2866 wxString trackedFile = repoPath() + wxFileName::GetPathSeparator() + wxT( "file.txt" );
2867 {
2868 std::ofstream f( trackedFile.ToStdString(), std::ios::binary );
2869 f << "edited during resolution\n";
2870 }
2871
2872 BOOST_CHECK_EQUAL( static_cast<int>( common.CreateMergedCommit(
2873 headOid, theirsOid, { { conflicts[0].path, conflicts[0].oursFile } }, error ) ),
2874 static_cast<int>( MergeCommitResult::FailedCleanly ) );
2875
2876 {
2877 std::ofstream f( trackedFile.ToStdString(), std::ios::binary );
2878 f << "mine\n";
2879 }
2880
2881 // A checkout failure, here injected with git's own index lock, must leave the branch where
2882 // it was. Recording the merge first would make a later pull report up to date while the
2883 // files still hold the old content.
2884 wxString indexLock = repoPath() + wxFileName::GetPathSeparator() + wxT( ".git" ) + wxFileName::GetPathSeparator()
2885 + wxT( "index.lock" );
2886 {
2887 std::ofstream f( indexLock.ToStdString() );
2888 }
2889
2890 {
2891 wxString lockedMerged = tempBase() + wxFileName::GetPathSeparator() + wxT( "locked_merged.txt" );
2892 std::ofstream f( lockedMerged.ToStdString(), std::ios::binary );
2893 f << "merged\n";
2894 f.close();
2895
2896 // The lock also blocks the rollback, so the working files are left part way. The caller
2897 // keeps its recovery copies on this result.
2898 BOOST_CHECK_EQUAL( static_cast<int>( common.CreateMergedCommit(
2899 headOid, theirsOid, { { conflicts[0].path, lockedMerged } }, error ) ),
2900 static_cast<int>( MergeCommitResult::FailedPartially ) );
2901
2902 git_oid headNow;
2903 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &headNow, repo, "HEAD" ), GIT_OK );
2904
2905 char headNowStr[GIT_OID_HEXSZ + 1] = { 0 };
2906 git_oid_tostr( headNowStr, sizeof( headNowStr ), &headNow );
2907 BOOST_CHECK_EQUAL( std::string( headNowStr ), headOid.ToStdString() );
2908 }
2909
2910 wxRemoveFile( indexLock );
2911
2912#ifndef __WINDOWS__
2913 // A failure after the checkout, injected here by making the branch ref unwritable, must
2914 // roll the working files back automatically. The operation required a clean tree, so the
2915 // rollback is lossless by construction. Directory permissions do not bind root.
2916 if( geteuid() != 0 )
2917 {
2918 // The ref update writes a lock file next to the ref and renames it into place, so the
2919 // way to make it fail is denying writes on the containing directory.
2920 wxString refsDir = repoPath() + wxFileName::GetPathSeparator() + wxT( ".git" ) + wxFileName::GetPathSeparator()
2921 + wxT( "refs" ) + wxFileName::GetPathSeparator() + wxT( "heads" );
2922
2923 BOOST_REQUIRE( wxFileName::DirExists( refsDir ) );
2924
2925 // Put the permissions back however this ends, or the fixture cannot remove its directory.
2926 struct WRITABLE_AGAIN
2927 {
2928 wxString dir;
2929 ~WRITABLE_AGAIN() { chmod( dir.fn_str(), 0755 ); }
2930 } restore{ refsDir };
2931
2932 chmod( refsDir.fn_str(), 0555 );
2933
2934 wxString refMerged = tempBase() + wxFileName::GetPathSeparator() + wxT( "ref_merged.txt" );
2935 {
2936 std::ofstream f( refMerged.ToStdString() );
2937 f << "merged\n";
2938 }
2939
2940 BOOST_CHECK_EQUAL( static_cast<int>( common.CreateMergedCommit( headOid, theirsOid,
2941 { { conflicts[0].path, refMerged } }, error ) ),
2942 static_cast<int>( MergeCommitResult::FailedCleanly ) );
2943
2944 // The branch did not move and the working file is back at the pre-merge content.
2945 git_oid headNow;
2946 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &headNow, repo, "HEAD" ), GIT_OK );
2947
2948 char headNowStr[GIT_OID_HEXSZ + 1] = { 0 };
2949 git_oid_tostr( headNowStr, sizeof( headNowStr ), &headNow );
2950 BOOST_CHECK_EQUAL( std::string( headNowStr ), headOid.ToStdString() );
2951
2952 std::ifstream in( trackedFile.ToStdString(), std::ios::binary );
2953 std::string rolled( ( std::istreambuf_iterator<char>( in ) ), std::istreambuf_iterator<char>() );
2954 BOOST_CHECK_EQUAL( rolled, "mine\n" );
2955 }
2956#endif
2957
2958 // The branch did not move, so the half-written file is an ordinary local modification. Put it
2959 // back the way a user cleaning up would, so the retry starts from a clean tree.
2960 {
2961 std::ofstream f( trackedFile.ToStdString(), std::ios::binary );
2962 f << "mine\n";
2963 }
2964
2965 // Resolve with fresh content and finish the merge.
2966 wxString mergedFile = tempBase() + wxFileName::GetPathSeparator() + wxT( "merged.txt" );
2967 {
2968 std::ofstream f( mergedFile.ToStdString(), std::ios::binary );
2969 f << "merged\n";
2970 }
2971
2972 BOOST_REQUIRE_MESSAGE( common.CreateMergedCommit( headOid, theirsOid, { { conflicts[0].path, mergedFile } }, error )
2974 "CreateMergedCommit failed: " + error.summary.ToStdString() );
2975
2976 // HEAD is now a merge commit of both sides, the working file carries the resolution, and no
2977 // merge state is left behind.
2978 git_oid newHead;
2979 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &newHead, repo, "HEAD" ), GIT_OK );
2980
2981 git_commit* mergeCommit = nullptr;
2982 BOOST_REQUIRE_EQUAL( git_commit_lookup( &mergeCommit, repo, &newHead ), GIT_OK );
2983 BOOST_CHECK_EQUAL( git_commit_parentcount( mergeCommit ), 2u );
2984
2985 // Merged against the pinned remote commit, not the one FETCH_HEAD moved to meanwhile.
2986 const git_oid* secondParent = git_commit_parent_id( mergeCommit, 1 );
2987 BOOST_REQUIRE( secondParent );
2988 BOOST_CHECK( git_oid_equal( secondParent, &remoteCommitOid ) );
2989 git_commit_free( mergeCommit );
2990
2991 BOOST_CHECK_EQUAL( readAll( repoPath() + wxFileName::GetPathSeparator() + wxT( "file.txt" ) ), "merged\n" );
2992 BOOST_CHECK_EQUAL( git_repository_state( repo ), GIT_REPOSITORY_STATE_NONE );
2993
2994 git_repository_free( repo );
2995}
2996
2997
2998// The panel writes the .mine or .theirs recovery copy into the working tree before finishing
2999// the merge, so an untracked file must not count as a change made during resolution.
3000BOOST_AUTO_TEST_CASE( MergeConflictFinishesWithAnUntrackedFilePresent )
3001{
3002 BOOST_TEST_REQUIRE( ready() );
3003
3004 git_repository* repo = openRepo();
3005 BOOST_TEST_REQUIRE( repo );
3006
3007 git_reference* headRef = nullptr;
3008 BOOST_REQUIRE_EQUAL( git_repository_head( &headRef, repo ), GIT_OK );
3009 git_oid baseOid = *git_reference_target( headRef );
3010 git_reference_free( headRef );
3011
3012 createCommit( repo, wxT( "file.txt" ), wxT( "mine\n" ), wxT( "Local change" ) );
3013 git_oid remoteCommitOid = createDetachedCommit( repo, baseOid, wxT( "file.txt" ), "remote\n", "Remote change" );
3014 writeFetchHead( remoteCommitOid );
3015
3016 KIGIT_COMMON common( repo );
3017
3018 wxString stageDir = tempBase() + wxFileName::GetPathSeparator() + wxT( "stages" );
3019 wxString headOid, theirsOid, conflictsError;
3020 KIGIT_ERROR_MESSAGE error;
3021 std::vector<KIGIT_CONFLICT_SIDES> conflicts;
3022
3023 BOOST_REQUIRE_MESSAGE( common.GetMergeConflicts( stageDir, headOid, theirsOid, conflicts, conflictsError ),
3024 "GetMergeConflicts failed: " + conflictsError.ToStdString() );
3025 BOOST_REQUIRE_EQUAL( conflicts.size(), 1u );
3026
3027 // The recovery copy the panel leaves beside the file.
3028 {
3029 wxString copy = repoPath() + wxFileName::GetPathSeparator() + wxT( "file.txt.mine" );
3030 std::ofstream f( copy.ToStdString() );
3031 f << "mine\n";
3032 }
3033
3034 BOOST_CHECK_MESSAGE(
3035 common.CreateMergedCommit( headOid, theirsOid, { { conflicts[0].path, conflicts[0].theirsFile } }, error )
3037 "refused with an untracked file present: " + error.summary.ToStdString() );
3038
3039 git_repository_free( repo );
3040}
3041
3042
3043// A rename on the remote arrives as two conflict entries, the old name with no remote side and the
3044// new name with no base. Taking theirs on both must leave the content on the new name only.
3045BOOST_AUTO_TEST_CASE( MergeConflictRenameTakeTheirsLandsOnNewPath )
3046{
3047 BOOST_TEST_REQUIRE( ready() );
3048
3049 git_repository* repo = openRepo();
3050 BOOST_TEST_REQUIRE( repo );
3051
3052 wxString base;
3053
3054 for( int i = 1; i <= 12; ++i )
3055 base << wxString::Format( wxT( "line %d\n" ), i );
3056
3057 git_oid baseOid = createCommit( repo, wxT( "file.txt" ), base, wxT( "Base" ) );
3058
3059 wxString mine = base;
3060 mine.Replace( wxT( "line 6\n" ), wxT( "line 6 mine\n" ) );
3061 createCommit( repo, wxT( "file.txt" ), mine, wxT( "Local change" ) );
3062
3063 // Theirs: file.txt removed, renamed.txt added with the same text except line 6.
3064 wxString theirs = base;
3065 theirs.Replace( wxT( "line 6\n" ), wxT( "line 6 theirs\n" ) );
3066
3067 git_oid theirsOidRaw;
3068 {
3069 git_commit* baseCommit = nullptr;
3070 BOOST_REQUIRE_EQUAL( git_commit_lookup( &baseCommit, repo, &baseOid ), GIT_OK );
3071 git_tree* baseTree = nullptr;
3072 BOOST_REQUIRE_EQUAL( git_commit_tree( &baseTree, baseCommit ), GIT_OK );
3073
3074 git_treebuilder* tb = nullptr;
3075 BOOST_REQUIRE_EQUAL( git_treebuilder_new( &tb, repo, baseTree ), GIT_OK );
3076 BOOST_REQUIRE_EQUAL( git_treebuilder_remove( tb, "file.txt" ), GIT_OK );
3077
3078 std::string content = theirs.utf8_string();
3079 git_oid blob;
3080 BOOST_REQUIRE_EQUAL( git_blob_create_from_buffer( &blob, repo, content.data(), content.size() ), GIT_OK );
3081 BOOST_REQUIRE_EQUAL( git_treebuilder_insert( nullptr, tb, "renamed.txt", &blob, GIT_FILEMODE_BLOB ), GIT_OK );
3082
3083 git_oid treeOid;
3084 BOOST_REQUIRE_EQUAL( git_treebuilder_write( &treeOid, tb ), GIT_OK );
3085 git_tree* tree = nullptr;
3086 BOOST_REQUIRE_EQUAL( git_tree_lookup( &tree, repo, &treeOid ), GIT_OK );
3087
3088 git_signature* sig = nullptr;
3089 BOOST_REQUIRE_EQUAL( git_signature_now( &sig, "QA", "[email protected]" ), GIT_OK );
3090 const git_commit* parents[1] = { baseCommit };
3091 BOOST_REQUIRE_EQUAL( git_commit_create( &theirsOidRaw, repo, nullptr, sig, sig, nullptr, "Rename and edit",
3092 tree, 1, parents ),
3093 GIT_OK );
3094 git_signature_free( sig );
3095 git_tree_free( tree );
3096 git_treebuilder_free( tb );
3097 git_tree_free( baseTree );
3098 git_commit_free( baseCommit );
3099 }
3100
3101 writeFetchHead( theirsOidRaw );
3102
3103 KIGIT_COMMON common( repo );
3104 wxString stageDir = tempBase() + wxFileName::GetPathSeparator() + wxT( "stages" );
3105 wxString headOid, theirsOid, conflictsError;
3106 KIGIT_ERROR_MESSAGE error;
3107 std::vector<KIGIT_CONFLICT_SIDES> conflicts;
3108
3109 BOOST_REQUIRE_MESSAGE( common.GetMergeConflicts( stageDir, headOid, theirsOid, conflicts, conflictsError ),
3110 conflictsError );
3111
3112
3113 BOOST_REQUIRE_EQUAL( conflicts.size(), 2u );
3114
3115 std::vector<std::pair<wxString, wxString>> resolved;
3116
3117 for( const KIGIT_CONFLICT_SIDES& c : conflicts )
3118 resolved.emplace_back( c.path, c.hasTheirs ? c.theirsFile : wxString() );
3119
3120 BOOST_REQUIRE_MESSAGE( common.CreateMergedCommit( headOid, theirsOid, resolved, error )
3122 error.summary );
3123
3124 wxString workdir = wxString::FromUTF8( git_repository_workdir( repo ) );
3125 bool renamedExists = wxFileExists( workdir + wxT( "renamed.txt" ) );
3126 bool oldExists = wxFileExists( workdir + wxT( "file.txt" ) );
3127
3128 BOOST_CHECK_MESSAGE( renamedExists, "theirs' content must land on the renamed path" );
3129 BOOST_CHECK_MESSAGE( !oldExists, "the old path must not survive a take-theirs of a rename" );
3130
3131 git_repository_free( repo );
3132}
3133
3134
3135// Rebuilding from the parent would drop every file the amended commit held but the list omits.
3136BOOST_AUTO_TEST_CASE( Amend_KeepsFilesAlreadyInCommit )
3137{
3138 BOOST_TEST_REQUIRE( ready() );
3139
3140 git_repository* repo = openRepo();
3141 BOOST_TEST_REQUIRE( repo );
3142
3143 // The commit to amend carries second.txt, which the amend below does not mention.
3144 createCommit( repo, wxT( "second.txt" ), wxT( "two\n" ), wxT( "Add second" ) );
3145
3146 {
3147 wxString filePath = repoPath() + wxFileName::GetPathSeparator() + wxT( "file.txt" );
3148 std::ofstream f( filePath.ToStdString() );
3149 f << "amended\n";
3150 }
3151
3152 GIT_COMMIT_HANDLER handler( repo );
3153 BOOST_REQUIRE( handler.PerformAmend( { wxT( "file.txt" ) }, wxT( "Amended" ), TEST_AUTHOR_NAME, TEST_AUTHOR_EMAIL )
3155
3156 git_oid headOid;
3157 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &headOid, repo, "HEAD" ), GIT_OK );
3158
3159 git_commit* head = nullptr;
3160 BOOST_REQUIRE_EQUAL( git_commit_lookup( &head, repo, &headOid ), GIT_OK );
3161
3162 git_tree* tree = nullptr;
3163 BOOST_REQUIRE_EQUAL( git_commit_tree( &tree, head ), GIT_OK );
3164
3165 BOOST_CHECK_MESSAGE( git_tree_entry_byname( tree, "second.txt" ) != nullptr,
3166 "a file already in the amended commit must survive an amend that does not list it" );
3167 BOOST_CHECK( git_tree_entry_byname( tree, "file.txt" ) != nullptr );
3168
3169 git_tree_free( tree );
3170 git_commit_free( head );
3171 git_repository_free( repo );
3172}
3173
3174
3175// A switch that would overwrite local changes must report a conflict and change nothing.
3176// A remote-tracking branch cannot be stood on. Checking one out could only attach HEAD to a
3177// local branch of the same name, which may be at another commit, so it is refused outright and
3178// nothing on disk is touched.
3179BOOST_AUTO_TEST_CASE( SwitchToBranch_RefusesARemoteBranch )
3180{
3181 BOOST_TEST_REQUIRE( ready() );
3182
3183 git_repository* repo = openRepo();
3184 BOOST_TEST_REQUIRE( repo );
3185
3186 git_oid localTip = createCommit( repo, wxT( "file.txt" ), wxT( "local\n" ), wxT( "local" ) );
3187
3188 git_commit* tipCommit = nullptr;
3189 BOOST_REQUIRE_EQUAL( git_commit_lookup( &tipCommit, repo, &localTip ), GIT_OK );
3190 KIGIT::GitCommitPtr tipPtr( tipCommit );
3191
3192 git_reference* localBranch = nullptr;
3193 BOOST_REQUIRE_EQUAL( git_branch_create( &localBranch, repo, "feature", tipCommit, 0 ), GIT_OK );
3194 git_reference_free( localBranch );
3195
3196 // origin/feature further along than the local branch of the same name, as a fetch leaves it.
3197 git_oid remoteTip = createDetachedCommit( repo, localTip, wxT( "file.txt" ), "remote\n", "remote" );
3198 git_reference* tracking = nullptr;
3199 BOOST_REQUIRE_EQUAL( git_reference_create( &tracking, repo, "refs/remotes/origin/feature", &remoteTip, 1, nullptr ),
3200 GIT_OK );
3201 git_reference_free( tracking );
3202
3203 KIGIT_COMMON common( repo );
3204 GIT_BRANCH_HANDLER handler( &common );
3205
3206 BOOST_CHECK( handler.SwitchToBranch( wxT( "origin/feature" ) ) == BranchResult::Error );
3207 BOOST_CHECK_MESSAGE( handler.GetErrorString().Contains( wxS( "remote" ) ),
3208 "the refusal should say the branch is on the remote, got: "
3209 + handler.GetErrorString().ToStdString() );
3210
3211 // Nothing moved and nothing was written.
3212 git_oid headNow;
3213 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &headNow, repo, "HEAD" ), GIT_OK );
3214 BOOST_CHECK( git_oid_equal( &headNow, &localTip ) );
3215
3216 std::ifstream f( ( repoPath() + wxFileName::GetPathSeparator() + wxT( "file.txt" ) ).ToStdString() );
3217 std::string content( ( std::istreambuf_iterator<char>( f ) ), std::istreambuf_iterator<char>() );
3218 BOOST_CHECK_EQUAL( content, "local\n" );
3219
3220 git_repository_free( repo );
3221}
3222
3223
3224BOOST_AUTO_TEST_CASE( SwitchToBranch_ReportsConflictOverLocalChanges )
3225{
3226 BOOST_TEST_REQUIRE( ready() );
3227
3228 git_repository* repo = openRepo();
3229 BOOST_TEST_REQUIRE( repo );
3230
3231 git_oid headOid;
3232 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &headOid, repo, "HEAD" ), GIT_OK );
3233
3234 git_commit* headCommit = nullptr;
3235 BOOST_REQUIRE_EQUAL( git_commit_lookup( &headCommit, repo, &headOid ), GIT_OK );
3236
3237 git_reference* oldBranch = nullptr;
3238 BOOST_REQUIRE_EQUAL( git_branch_create( &oldBranch, repo, "old", headCommit, 0 ), GIT_OK );
3239 git_reference_free( oldBranch );
3240 git_commit_free( headCommit );
3241
3242 git_oid advancedOid = createCommit( repo, wxT( "file.txt" ), wxT( "master2\n" ), wxT( "Advance master" ) );
3243
3244 wxString workDir = wxString::FromUTF8( git_repository_workdir( repo ) );
3245
3246 {
3247 std::ofstream f( ( workDir + wxT( "file.txt" ) ).ToStdString(), std::ios::binary );
3248 f << "local edit\n";
3249 }
3250
3251 KIGIT_COMMON common( repo );
3252 GIT_BRANCH_HANDLER handler( &common );
3253
3254 BOOST_CHECK( handler.SwitchToBranch( wxT( "old" ) ) == BranchResult::Conflict );
3255
3256 git_oid afterOid;
3257 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &afterOid, repo, "HEAD" ), GIT_OK );
3258 BOOST_CHECK_EQUAL( git_oid_cmp( &afterOid, &advancedOid ), 0 );
3259
3260 std::ifstream in( ( workDir + wxT( "file.txt" ) ).ToStdString(), std::ios::binary );
3261 std::string content( ( std::istreambuf_iterator<char>( in ) ), std::istreambuf_iterator<char>() );
3262 BOOST_CHECK_EQUAL( content, "local edit\n" );
3263
3264 git_repository_free( repo );
3265}
3266
3267
3268// A never-committed file the target branch carries must stop the switch too.
3269BOOST_AUTO_TEST_CASE( SwitchToBranch_ReportsConflictOverUntrackedCollision )
3270{
3271 BOOST_TEST_REQUIRE( ready() );
3272
3273 git_repository* repo = openRepo();
3274 BOOST_TEST_REQUIRE( repo );
3275
3276 git_oid headOid;
3277 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &headOid, repo, "HEAD" ), GIT_OK );
3278
3279 git_commit* headCommit = nullptr;
3280 BOOST_REQUIRE_EQUAL( git_commit_lookup( &headCommit, repo, &headOid ), GIT_OK );
3281
3282 git_reference* plainBranch = nullptr;
3283 BOOST_REQUIRE_EQUAL( git_branch_create( &plainBranch, repo, "plain", headCommit, 0 ), GIT_OK );
3284 git_reference_free( plainBranch );
3285 git_commit_free( headCommit );
3286
3287 createCommit( repo, wxT( "extra.txt" ), wxT( "committed\n" ), wxT( "Add extra" ) );
3288
3289 KIGIT_COMMON common( repo );
3290 GIT_BRANCH_HANDLER handler( &common );
3291
3292 BOOST_REQUIRE( handler.SwitchToBranch( wxT( "plain" ) ) == BranchResult::Success );
3293
3294 wxString workDir = wxString::FromUTF8( git_repository_workdir( repo ) );
3295
3296 {
3297 std::ofstream f( ( workDir + wxT( "extra.txt" ) ).ToStdString(), std::ios::binary );
3298 f << "local\n";
3299 }
3300
3301 BOOST_CHECK( handler.SwitchToBranch( wxT( "master" ) ) == BranchResult::Conflict );
3302
3303 std::ifstream in( ( workDir + wxT( "extra.txt" ) ).ToStdString(), std::ios::binary );
3304 std::string content( ( std::istreambuf_iterator<char>( in ) ), std::istreambuf_iterator<char>() );
3305 BOOST_CHECK_EQUAL( content, "local\n" );
3306
3307 git_repository_free( repo );
3308}
3309
3310
3311// A file deleted from the working tree is offered in the checklist, so committing it must
3312// record the deletion instead of failing to add the missing path.
3313BOOST_AUTO_TEST_CASE( Commit_RecordsDeletedFile )
3314{
3315 BOOST_TEST_REQUIRE( ready() );
3316
3317 git_repository* repo = openRepo();
3318 BOOST_TEST_REQUIRE( repo );
3319
3320 createCommit( repo, wxT( "doomed.txt" ), wxT( "doomed\n" ), wxT( "Add doomed" ) );
3321
3322 wxString workDir = wxString::FromUTF8( git_repository_workdir( repo ) );
3323 BOOST_TEST_REQUIRE( wxRemoveFile( workDir + wxT( "doomed.txt" ) ) );
3324
3325 GIT_COMMIT_HANDLER handler( repo );
3326 CommitResult result = handler.PerformCommit( { wxT( "doomed.txt" ) }, wxT( "Remove doomed" ), TEST_AUTHOR_NAME,
3328
3329 BOOST_REQUIRE_MESSAGE( result == CommitResult::Success, handler.GetErrorString() );
3330
3331 git_oid headOid;
3332 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &headOid, repo, "HEAD" ), GIT_OK );
3333
3334 git_commit* head = nullptr;
3335 BOOST_REQUIRE_EQUAL( git_commit_lookup( &head, repo, &headOid ), GIT_OK );
3336
3337 git_tree* tree = nullptr;
3338 BOOST_REQUIRE_EQUAL( git_commit_tree( &tree, head ), GIT_OK );
3339
3340 BOOST_CHECK( git_tree_entry_byname( tree, "doomed.txt" ) == nullptr );
3341 BOOST_CHECK( git_tree_entry_byname( tree, "file.txt" ) != nullptr );
3342
3343 git_tree_free( tree );
3344 git_commit_free( head );
3345 git_repository_free( repo );
3346}
3347
3348
3349// The selected entries must also advance to the committed content, or they linger afterwards as
3350// phantom staged changes.
3351BOOST_AUTO_TEST_CASE( Commit_PreservesIndexEntriesOfUnselectedFiles )
3352{
3353 BOOST_TEST_REQUIRE( ready() );
3354
3355 git_repository* repo = openRepo();
3356 BOOST_TEST_REQUIRE( repo );
3357
3358 createCommit( repo, wxT( "board.txt" ), wxT( "board v1\n" ), wxT( "Add board" ) );
3359 createCommit( repo, wxT( "doomed.txt" ), wxT( "doomed\n" ), wxT( "Add doomed" ) );
3360
3361 wxString workDir = wxString::FromUTF8( git_repository_workdir( repo ) );
3362
3363 auto writeAll = [&]( const wxString& aName, const std::string& aContent )
3364 {
3365 std::ofstream f( ( workDir + aName ).ToStdString(), std::ios::binary );
3366 f << aContent;
3367 };
3368
3369 auto readAll = [&]( const wxString& aName )
3370 {
3371 std::ifstream in( ( workDir + aName ).ToStdString(), std::ios::binary );
3372 return std::string( ( std::istreambuf_iterator<char>( in ) ), std::istreambuf_iterator<char>() );
3373 };
3374
3375 // Staged content of a path, or a marker for "no entry staged".
3376 auto stagedContent = [&]( const char* aPath )
3377 {
3378 std::string result = "<none>";
3379 git_index* index = nullptr;
3380 BOOST_REQUIRE_EQUAL( git_repository_index( &index, repo ), GIT_OK );
3381 BOOST_REQUIRE_EQUAL( git_index_read( index, 1 ), GIT_OK );
3382
3383 if( const git_index_entry* entry = git_index_get_bypath( index, aPath, 0 ) )
3384 {
3385 git_blob* blob = nullptr;
3386 BOOST_REQUIRE_EQUAL( git_blob_lookup( &blob, repo, &entry->id ), GIT_OK );
3387 result.assign( static_cast<const char*>( git_blob_rawcontent( blob ) ), git_blob_rawsize( blob ) );
3388 git_blob_free( blob );
3389 }
3390
3391 git_index_free( index );
3392 return result;
3393 };
3394
3395 auto committedContent = [&]( const char* aPath )
3396 {
3397 std::string result = "<none>";
3398 git_oid headOid;
3399 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &headOid, repo, "HEAD" ), GIT_OK );
3400
3401 git_commit* head = nullptr;
3402 BOOST_REQUIRE_EQUAL( git_commit_lookup( &head, repo, &headOid ), GIT_OK );
3403
3404 git_tree* tree = nullptr;
3405 BOOST_REQUIRE_EQUAL( git_commit_tree( &tree, head ), GIT_OK );
3406
3407 git_tree_entry* entry = nullptr;
3408
3409 if( git_tree_entry_bypath( &entry, tree, aPath ) == GIT_OK )
3410 {
3411 git_blob* blob = nullptr;
3412 BOOST_REQUIRE_EQUAL( git_blob_lookup( &blob, repo, git_tree_entry_id( entry ) ), GIT_OK );
3413 result.assign( static_cast<const char*>( git_blob_rawcontent( blob ) ), git_blob_rawsize( blob ) );
3414 git_blob_free( blob );
3415 git_tree_entry_free( entry );
3416 }
3417
3418 git_tree_free( tree );
3419 git_commit_free( head );
3420 return result;
3421 };
3422
3423 // Three kinds of staging the user did outside KiCad and did not select: a modification, a
3424 // brand new file, and a deletion.
3425 writeAll( wxT( "notes.txt" ), "staged A\n" );
3426 writeAll( wxT( "board.txt" ), "board staged\n" );
3427
3428 git_index* index = nullptr;
3429 BOOST_REQUIRE_EQUAL( git_repository_index( &index, repo ), GIT_OK );
3430 BOOST_REQUIRE_EQUAL( git_index_add_bypath( index, "notes.txt" ), GIT_OK );
3431 BOOST_REQUIRE_EQUAL( git_index_add_bypath( index, "board.txt" ), GIT_OK );
3432 BOOST_REQUIRE_EQUAL( git_index_remove_bypath( index, "doomed.txt" ), GIT_OK );
3433 BOOST_REQUIRE_EQUAL( git_index_write( index ), GIT_OK );
3434 git_index_free( index );
3435
3436 // The staged version and the working version differ, so a lost index entry cannot be
3437 // recovered from the working file.
3438 writeAll( wxT( "notes.txt" ), "working B\n" );
3439 writeAll( wxT( "board.txt" ), "board working\n" );
3440
3441 // The selected file is a fourth path, untouched by any of the staging above.
3442 writeAll( wxT( "file.txt" ), "selected\n" );
3443
3444 GIT_COMMIT_HANDLER handler( repo );
3446 handler.PerformCommit( { wxT( "file.txt" ) }, wxT( "Only file.txt" ), TEST_AUTHOR_NAME, TEST_AUTHOR_EMAIL );
3447 BOOST_REQUIRE_MESSAGE( result == CommitResult::Success, handler.GetErrorString() );
3448
3449 // The commit holds HEAD plus the selected file only.
3450 BOOST_CHECK_EQUAL( committedContent( "file.txt" ), "selected\n" );
3451 BOOST_CHECK_EQUAL( committedContent( "board.txt" ), "board v1\n" );
3452 BOOST_CHECK_EQUAL( committedContent( "notes.txt" ), "<none>" );
3453 BOOST_CHECK_EQUAL( committedContent( "doomed.txt" ), "doomed\n" );
3454
3455 // Every unselected staged decision survives, and the working files are untouched.
3456 BOOST_CHECK_EQUAL( stagedContent( "notes.txt" ), "staged A\n" );
3457 BOOST_CHECK_EQUAL( stagedContent( "board.txt" ), "board staged\n" );
3458 BOOST_CHECK_EQUAL( stagedContent( "doomed.txt" ), "<none>" );
3459 BOOST_CHECK_EQUAL( readAll( wxT( "notes.txt" ) ), "working B\n" );
3460 BOOST_CHECK_EQUAL( readAll( wxT( "board.txt" ) ), "board working\n" );
3461
3462 // The selected path matches what was committed, so it is not left looking staged.
3463 BOOST_CHECK_EQUAL( stagedContent( "file.txt" ), "selected\n" );
3464
3465 // A selected deletion behaves the same way, and still leaves the others alone.
3466 BOOST_TEST_REQUIRE( wxRemoveFile( workDir + wxT( "file.txt" ) ) );
3467
3468 result = handler.PerformCommit( { wxT( "file.txt" ) }, wxT( "Remove file.txt" ), TEST_AUTHOR_NAME,
3470 BOOST_REQUIRE_MESSAGE( result == CommitResult::Success, handler.GetErrorString() );
3471
3472 BOOST_CHECK_EQUAL( committedContent( "file.txt" ), "<none>" );
3473 BOOST_CHECK_EQUAL( stagedContent( "file.txt" ), "<none>" );
3474 BOOST_CHECK_EQUAL( stagedContent( "notes.txt" ), "staged A\n" );
3475 BOOST_CHECK_EQUAL( stagedContent( "board.txt" ), "board staged\n" );
3476 BOOST_CHECK_EQUAL( stagedContent( "doomed.txt" ), "<none>" );
3477
3478 // A message-only amend keeps the tree and must not disturb the staged entries either.
3479 result = handler.PerformAmend( {}, wxT( "Reworded" ), TEST_AUTHOR_NAME, TEST_AUTHOR_EMAIL );
3480 BOOST_REQUIRE_MESSAGE( result == CommitResult::Success, handler.GetErrorString() );
3481
3482 BOOST_CHECK_EQUAL( stagedContent( "notes.txt" ), "staged A\n" );
3483 BOOST_CHECK_EQUAL( stagedContent( "board.txt" ), "board staged\n" );
3484 BOOST_CHECK_EQUAL( stagedContent( "doomed.txt" ), "<none>" );
3485 BOOST_CHECK_EQUAL( committedContent( "board.txt" ), "board v1\n" );
3486
3487 // A failed commit must not cost the user any staged content either.
3488 writeAll( wxT( "late.txt" ), "late\n" );
3489 BOOST_CHECK( handler.PerformCommit( { wxT( "late.txt" ) }, wxT( "Bad author" ), wxEmptyString, wxEmptyString )
3491
3492 BOOST_CHECK_EQUAL( stagedContent( "notes.txt" ), "staged A\n" );
3493 BOOST_CHECK_EQUAL( stagedContent( "board.txt" ), "board staged\n" );
3494 BOOST_CHECK_EQUAL( stagedContent( "doomed.txt" ), "<none>" );
3495
3496 git_repository_free( repo );
3497}
3498
3499
3500// libgit2 wants UTF-8 paths. A locale conversion would mangle or reject non-ASCII names, so
3501// check that they survive an add, a modify, an amend and a delete with byte-exact identity.
3502BOOST_AUTO_TEST_CASE( Commit_HandlesNonAsciiPaths )
3503{
3504 BOOST_TEST_REQUIRE( ready() );
3505
3506 git_repository* repo = openRepo();
3507 BOOST_TEST_REQUIRE( repo );
3508
3509 wxString workDir = wxString::FromUTF8( git_repository_workdir( repo ) );
3510
3511 const wxString umlaut = wxString::FromUTF8( "Gr\xc3\xb6\xc3\x9f"
3512 "e.kicad_sch" );
3513 const wxString cyrillic = wxString::FromUTF8( "\xd0\xbf\xd0\xbb\xd0\xb0\xd1\x82\xd0\xb0.txt" );
3514 const std::string umlautUtf8 = umlaut.utf8_string();
3515 const std::string cyrillicUtf8 = cyrillic.utf8_string();
3516
3517 auto writeAll = [&]( const wxString& aName, const std::string& aContent )
3518 {
3519 KI_TEST::WriteGitFile( repoPath(), aName, aContent );
3520 };
3521
3522 auto inHeadTree = [&]( const std::string& aPath )
3523 {
3524 git_oid headOid;
3525 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &headOid, repo, "HEAD" ), GIT_OK );
3526
3527 git_commit* head = nullptr;
3528 BOOST_REQUIRE_EQUAL( git_commit_lookup( &head, repo, &headOid ), GIT_OK );
3529
3530 git_tree* tree = nullptr;
3531 BOOST_REQUIRE_EQUAL( git_commit_tree( &tree, head ), GIT_OK );
3532
3533 git_tree_entry* entry = nullptr;
3534 bool found = git_tree_entry_bypath( &entry, tree, aPath.c_str() ) == GIT_OK;
3535
3536 if( found )
3537 git_tree_entry_free( entry );
3538
3539 git_tree_free( tree );
3540 git_commit_free( head );
3541 return found;
3542 };
3543
3544 writeAll( umlaut, "one\n" );
3545 writeAll( cyrillic, "one\n" );
3546
3547 GIT_COMMIT_HANDLER handler( repo );
3549 handler.PerformCommit( { umlaut, cyrillic }, wxT( "Add" ), TEST_AUTHOR_NAME, TEST_AUTHOR_EMAIL );
3550 BOOST_REQUIRE_MESSAGE( result == CommitResult::Success, handler.GetErrorString() );
3551
3552 // The recorded path must be the same bytes as the file on disk, not a locale transcoding.
3553 BOOST_CHECK( inHeadTree( umlautUtf8 ) );
3554 BOOST_CHECK( inHeadTree( cyrillicUtf8 ) );
3555
3556 writeAll( umlaut, "two\n" );
3557 result = handler.PerformCommit( { umlaut }, wxT( "Modify" ), TEST_AUTHOR_NAME, TEST_AUTHOR_EMAIL );
3558 BOOST_REQUIRE_MESSAGE( result == CommitResult::Success, handler.GetErrorString() );
3559 BOOST_CHECK( inHeadTree( umlautUtf8 ) );
3560
3561 writeAll( umlaut, "three\n" );
3562 result = handler.PerformAmend( { umlaut }, wxT( "Amended" ), TEST_AUTHOR_NAME, TEST_AUTHOR_EMAIL );
3563 BOOST_REQUIRE_MESSAGE( result == CommitResult::Success, handler.GetErrorString() );
3564 BOOST_CHECK( inHeadTree( umlautUtf8 ) );
3565 BOOST_CHECK( inHeadTree( cyrillicUtf8 ) );
3566
3567 BOOST_TEST_REQUIRE( wxRemoveFile( workDir + umlaut ) );
3568 result = handler.PerformCommit( { umlaut }, wxT( "Delete" ), TEST_AUTHOR_NAME, TEST_AUTHOR_EMAIL );
3569 BOOST_REQUIRE_MESSAGE( result == CommitResult::Success, handler.GetErrorString() );
3570
3571 BOOST_CHECK( !inHeadTree( umlautUtf8 ) );
3572 BOOST_CHECK( inHeadTree( cyrillicUtf8 ) );
3573
3574 git_repository_free( repo );
3575}
3576
3577
3578// While another process holds .git/index.lock, KiCad's own index write must fail cleanly and
3579// leave that process's staging on disk untouched.
3580BOOST_AUTO_TEST_CASE( Commit_DoesNotDamageIndexWhileAnotherProcessHoldsTheLock )
3581{
3582 BOOST_TEST_REQUIRE( ready() );
3583
3584 git_repository* repo = openRepo();
3585 BOOST_TEST_REQUIRE( repo );
3586
3587 wxString workDir = wxString::FromUTF8( git_repository_workdir( repo ) );
3588
3589 {
3590 std::ofstream f( ( workDir + wxT( "notes.txt" ) ).ToStdString(), std::ios::binary );
3591 f << "staged elsewhere\n";
3592 }
3593
3594 git_repository* foreignRepo = nullptr;
3595 BOOST_REQUIRE_EQUAL( git_repository_open( &foreignRepo, repoPath().ToUTF8().data() ), GIT_OK );
3596
3597 git_index* foreign = nullptr;
3598 BOOST_REQUIRE_EQUAL( git_repository_index( &foreign, foreignRepo ), GIT_OK );
3599 BOOST_REQUIRE_EQUAL( git_index_add_bypath( foreign, "notes.txt" ), GIT_OK );
3600 BOOST_REQUIRE_EQUAL( git_index_write( foreign ), GIT_OK );
3601 git_index_free( foreign );
3602 git_repository_free( foreignRepo );
3603
3604 {
3605 std::ofstream f( ( workDir + wxT( "file.txt" ) ).ToStdString(), std::ios::binary );
3606 f << "selected\n";
3607 }
3608
3609 // Stand in for another git process that is part way through its own index write.
3610 wxString indexLock = repoPath() + wxFileName::GetPathSeparator() + wxT( ".git" ) + wxFileName::GetPathSeparator()
3611 + wxT( "index.lock" );
3612 {
3613 std::ofstream f( indexLock.ToStdString() );
3614 f << "held\n";
3615 }
3616
3617 GIT_COMMIT_HANDLER handler( repo );
3619 handler.PerformCommit( { wxT( "file.txt" ) }, wxT( "Only file.txt" ), TEST_AUTHOR_NAME, TEST_AUTHOR_EMAIL );
3620
3621 // The commit itself does not need the index lock, so it exists. Only the staged file list
3622 // could not be rewritten, which is a different outcome from a failed commit.
3623 BOOST_CHECK( result == CommitResult::SuccessIndexStale );
3624 BOOST_CHECK( !handler.GetErrorString().IsEmpty() );
3625
3626 BOOST_TEST_REQUIRE( wxRemoveFile( indexLock ) );
3627
3628 git_oid committed;
3629 BOOST_CHECK_MESSAGE( git_reference_name_to_id( &committed, repo, "HEAD" ) == GIT_OK,
3630 "the commit must exist even though the index could not be rewritten" );
3631
3632 // The other process's staging is still exactly as it left it.
3633 git_index* after = nullptr;
3634 BOOST_REQUIRE_EQUAL( git_repository_index( &after, repo ), GIT_OK );
3635 BOOST_REQUIRE_EQUAL( git_index_read( after, 1 ), GIT_OK );
3636
3637 const git_index_entry* notes = git_index_get_bypath( after, "notes.txt", 0 );
3638 BOOST_REQUIRE_MESSAGE( notes, "the locked-out write damaged another process's staging" );
3639
3640 git_blob* blob = nullptr;
3641 BOOST_REQUIRE_EQUAL( git_blob_lookup( &blob, repo, &notes->id ), GIT_OK );
3642 std::string content( static_cast<const char*>( git_blob_rawcontent( blob ) ), git_blob_rawsize( blob ) );
3643 BOOST_CHECK_EQUAL( content, "staged elsewhere\n" );
3644
3645 git_blob_free( blob );
3646 git_index_free( after );
3647 git_repository_free( repo );
3648}
3649
3650
3651// Amend needs the same protection as Commit. Staging done outside KiCad must survive it.
3652BOOST_AUTO_TEST_CASE( Amend_KeepsStagingWrittenByAnotherProcess )
3653{
3654 BOOST_TEST_REQUIRE( ready() );
3655
3656 git_repository* repo = openRepo();
3657 BOOST_TEST_REQUIRE( repo );
3658
3659 wxString workDir = wxString::FromUTF8( git_repository_workdir( repo ) );
3660
3661 git_index* cached = nullptr;
3662 BOOST_REQUIRE_EQUAL( git_repository_index( &cached, repo ), GIT_OK );
3663 KIGIT::GitIndexPtr cachedPtr( cached );
3664
3665 {
3666 std::ofstream f( ( workDir + wxT( "notes.txt" ) ).ToStdString(), std::ios::binary );
3667 f << "staged elsewhere\n";
3668 }
3669
3670 // A second handle on the same directory stands in for a terminal git process.
3671 git_repository* foreignRepo = nullptr;
3672 BOOST_REQUIRE_EQUAL( git_repository_open( &foreignRepo, repoPath().ToUTF8().data() ), GIT_OK );
3673
3674 git_index* foreign = nullptr;
3675 BOOST_REQUIRE_EQUAL( git_repository_index( &foreign, foreignRepo ), GIT_OK );
3676 BOOST_REQUIRE_EQUAL( git_index_add_bypath( foreign, "notes.txt" ), GIT_OK );
3677 BOOST_REQUIRE_EQUAL( git_index_write( foreign ), GIT_OK );
3678
3679 const git_index_entry* stagedBefore = git_index_get_bypath( foreign, "notes.txt", 0 );
3680 BOOST_REQUIRE( stagedBefore );
3681 git_oid stagedBlob = stagedBefore->id;
3682
3683 git_index_free( foreign );
3684 git_repository_free( foreignRepo );
3685
3686 {
3687 std::ofstream f( ( workDir + wxT( "file.txt" ) ).ToStdString(), std::ios::binary );
3688 f << "amended\n";
3689 }
3690
3691 GIT_COMMIT_HANDLER handler( repo );
3692 CommitResult result = handler.PerformAmend( { wxT( "file.txt" ) }, wxT( "Amended message" ), TEST_AUTHOR_NAME,
3694 BOOST_REQUIRE_MESSAGE( result == CommitResult::Success, handler.GetErrorString() );
3695
3696 git_repository_free( repo );
3697
3698 // Reopen so nothing is answered from a cached index.
3699 git_repository* fresh = nullptr;
3700 BOOST_REQUIRE_EQUAL( git_repository_open( &fresh, repoPath().ToUTF8().data() ), GIT_OK );
3701
3702 git_index* after = nullptr;
3703 BOOST_REQUIRE_EQUAL( git_repository_index( &after, fresh ), GIT_OK );
3704
3705 const git_index_entry* notes = git_index_get_bypath( after, "notes.txt", 0 );
3706 BOOST_REQUIRE_MESSAGE( notes, "the externally staged file must still be staged after an amend" );
3707 BOOST_CHECK_MESSAGE( git_oid_equal( &notes->id, &stagedBlob ),
3708 "the externally staged blob must be the exact one the other process wrote" );
3709
3710 // The amend must not have pulled notes.txt into the commit.
3711 git_oid headOid;
3712 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &headOid, fresh, "HEAD" ), GIT_OK );
3713
3714 git_commit* headCommit = nullptr;
3715 BOOST_REQUIRE_EQUAL( git_commit_lookup( &headCommit, fresh, &headOid ), GIT_OK );
3716
3717 git_tree* headTree = nullptr;
3718 BOOST_REQUIRE_EQUAL( git_commit_tree( &headTree, headCommit ), GIT_OK );
3719
3720 git_tree_entry* leaked = nullptr;
3721 BOOST_CHECK_MESSAGE( git_tree_entry_bypath( &leaked, headTree, "notes.txt" ) != GIT_OK,
3722 "an unselected staged file must not ride into the amended commit" );
3723 git_tree_entry_free( leaked );
3724
3725 git_tree_free( headTree );
3726 git_commit_free( headCommit );
3727 git_index_free( after );
3728 git_repository_free( fresh );
3729}
3730
3731
3732BOOST_AUTO_TEST_CASE( Commit_KeepsStagingWrittenByAnotherProcess )
3733{
3734 BOOST_TEST_REQUIRE( ready() );
3735
3736 git_repository* repo = openRepo();
3737 BOOST_TEST_REQUIRE( repo );
3738
3739 wxString workDir = wxString::FromUTF8( git_repository_workdir( repo ) );
3740
3741 // Make the repository handle cache an index that knows nothing about the staging below.
3742 git_index* cached = nullptr;
3743 BOOST_REQUIRE_EQUAL( git_repository_index( &cached, repo ), GIT_OK );
3744 KIGIT::GitIndexPtr cachedPtr( cached );
3745 BOOST_REQUIRE_EQUAL( git_index_entrycount( cached ), 1 );
3746
3747 {
3748 std::ofstream f( ( workDir + wxT( "notes.txt" ) ).ToStdString(), std::ios::binary );
3749 f << "staged elsewhere\n";
3750 }
3751
3752 // A second repository handle on the same directory stands in for the terminal.
3753 git_repository* foreignRepo = nullptr;
3754 BOOST_REQUIRE_EQUAL( git_repository_open( &foreignRepo, repoPath().ToUTF8().data() ), GIT_OK );
3755
3756 git_index* foreign = nullptr;
3757 BOOST_REQUIRE_EQUAL( git_repository_index( &foreign, foreignRepo ), GIT_OK );
3758 BOOST_REQUIRE_EQUAL( git_index_add_bypath( foreign, "notes.txt" ), GIT_OK );
3759 BOOST_REQUIRE_EQUAL( git_index_write( foreign ), GIT_OK );
3760 git_index_free( foreign );
3761 git_repository_free( foreignRepo );
3762
3763 {
3764 std::ofstream f( ( workDir + wxT( "file.txt" ) ).ToStdString(), std::ios::binary );
3765 f << "selected\n";
3766 }
3767
3768 GIT_COMMIT_HANDLER handler( repo );
3770 handler.PerformCommit( { wxT( "file.txt" ) }, wxT( "Only file.txt" ), TEST_AUTHOR_NAME, TEST_AUTHOR_EMAIL );
3771 BOOST_REQUIRE_MESSAGE( result == CommitResult::Success, handler.GetErrorString() );
3772
3773 git_index* after = nullptr;
3774 BOOST_REQUIRE_EQUAL( git_repository_index( &after, repo ), GIT_OK );
3775 BOOST_REQUIRE_EQUAL( git_index_read( after, 1 ), GIT_OK );
3776
3777 const git_index_entry* notes = git_index_get_bypath( after, "notes.txt", 0 );
3778 BOOST_REQUIRE_MESSAGE( notes, "staging written by another process was lost" );
3779
3780 git_blob* blob = nullptr;
3781 BOOST_REQUIRE_EQUAL( git_blob_lookup( &blob, repo, &notes->id ), GIT_OK );
3782 std::string content( static_cast<const char*>( git_blob_rawcontent( blob ) ), git_blob_rawsize( blob ) );
3783 BOOST_CHECK_EQUAL( content, "staged elsewhere\n" );
3784
3785 git_blob_free( blob );
3786 git_index_free( after );
3787 git_repository_free( repo );
3788}
3789
3790
3791// The panel promises to commit exactly the checked files. Content staged outside KiCad for
3792// an unchecked file must not ride into the commit.
3793BOOST_AUTO_TEST_CASE( Commit_IgnoresContentStagedElsewhere )
3794{
3795 BOOST_TEST_REQUIRE( ready() );
3796
3797 git_repository* repo = openRepo();
3798 BOOST_TEST_REQUIRE( repo );
3799
3800 createCommit( repo, wxT( "sneaky.txt" ), wxT( "clean\n" ), wxT( "Add sneaky" ) );
3801
3802 wxString workDir = wxString::FromUTF8( git_repository_workdir( repo ) );
3803
3804 // Stage a change to sneaky.txt the way a command line git add would, then change the
3805 // checked file too.
3806 {
3807 std::ofstream f( ( workDir + wxT( "sneaky.txt" ) ).ToStdString(), std::ios::binary );
3808 f << "staged elsewhere\n";
3809 }
3810
3811 git_index* index = nullptr;
3812 BOOST_REQUIRE_EQUAL( git_repository_index( &index, repo ), GIT_OK );
3813 BOOST_REQUIRE_EQUAL( git_index_add_bypath( index, "sneaky.txt" ), GIT_OK );
3814 BOOST_REQUIRE_EQUAL( git_index_write( index ), GIT_OK );
3815 git_index_free( index );
3816
3817 {
3818 std::ofstream f( ( workDir + wxT( "file.txt" ) ).ToStdString(), std::ios::binary );
3819 f << "wanted\n";
3820 }
3821
3822 GIT_COMMIT_HANDLER handler( repo );
3824 handler.PerformCommit( { wxT( "file.txt" ) }, wxT( "Only file.txt" ), TEST_AUTHOR_NAME, TEST_AUTHOR_EMAIL );
3825
3826 BOOST_REQUIRE_MESSAGE( result == CommitResult::Success, handler.GetErrorString() );
3827
3828 git_oid headOid;
3829 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &headOid, repo, "HEAD" ), GIT_OK );
3830
3831 git_commit* head = nullptr;
3832 BOOST_REQUIRE_EQUAL( git_commit_lookup( &head, repo, &headOid ), GIT_OK );
3833
3834 git_tree* tree = nullptr;
3835 BOOST_REQUIRE_EQUAL( git_commit_tree( &tree, head ), GIT_OK );
3836
3837 const git_tree_entry* sneaky = git_tree_entry_byname( tree, "sneaky.txt" );
3838 BOOST_REQUIRE( sneaky );
3839
3840 git_blob* blob = nullptr;
3841 BOOST_REQUIRE_EQUAL( git_blob_lookup( &blob, repo, git_tree_entry_id( sneaky ) ), GIT_OK );
3842
3843 std::string sneakyContent( static_cast<const char*>( git_blob_rawcontent( blob ) ), git_blob_rawsize( blob ) );
3844
3845 BOOST_CHECK_EQUAL( sneakyContent, "clean\n" );
3846
3847 git_blob_free( blob );
3848 git_tree_free( tree );
3849 git_commit_free( head );
3850 git_repository_free( repo );
3851}
3852
3853
3854// The old version arrives as a new commit on top of the head, and never over uncommitted work.
3855BOOST_AUTO_TEST_CASE( RestoreVersionRoundTrip )
3856{
3857 BOOST_TEST_REQUIRE( ready() );
3858
3859 git_repository* repo = openRepo();
3860 BOOST_TEST_REQUIRE( repo );
3861
3862 git_oid secondOid = createCommit( repo, wxT( "file.txt" ), wxT( "second\n" ), wxT( "Second version" ) );
3863
3864 git_object* firstObj = nullptr;
3865 BOOST_REQUIRE_EQUAL( git_revparse_single( &firstObj, repo, "HEAD~1" ), GIT_OK );
3866 git_oid firstOid = *git_object_id( firstObj );
3867 git_object_free( firstObj );
3868
3869 char firstHash[GIT_OID_HEXSZ + 1] = { 0 };
3870 git_oid_tostr( firstHash, sizeof( firstHash ), &firstOid );
3871
3872 KIGIT_COMMON common( repo, repoPath() );
3873 KIGIT_ERROR_MESSAGE error;
3874
3875 // Refuses over uncommitted changes.
3876 wxString trackedFile = repoPath() + wxFileName::GetPathSeparator() + wxT( "file.txt" );
3877 {
3878 std::ofstream f( trackedFile.ToStdString() );
3879 f << "uncommitted\n";
3880 }
3881
3882 BOOST_CHECK( common.RestoreVersion( wxString::FromUTF8( firstHash ), error ) == RestoreResult::Dirty );
3883
3884 {
3885 std::ofstream f( trackedFile.ToStdString() );
3886 f << "second\n";
3887 }
3888
3889 // Restores as a new commit whose parent is the current head.
3890 BOOST_REQUIRE_MESSAGE( common.RestoreVersion( wxString::FromUTF8( firstHash ), error ) == RestoreResult::Restored,
3891 "RestoreVersion failed: " + error.summary.ToStdString() );
3892
3893 git_oid newHead;
3894 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &newHead, repo, "HEAD" ), GIT_OK );
3895
3896 git_commit* restoreCommit = nullptr;
3897 BOOST_REQUIRE_EQUAL( git_commit_lookup( &restoreCommit, repo, &newHead ), GIT_OK );
3898 BOOST_CHECK_EQUAL( git_commit_parentcount( restoreCommit ), 1u );
3899 BOOST_CHECK( git_oid_equal( git_commit_parent_id( restoreCommit, 0 ), &secondOid ) );
3900 git_commit_free( restoreCommit );
3901
3902 std::ifstream in( trackedFile.ToStdString() );
3903 std::string content( ( std::istreambuf_iterator<char>( in ) ), std::istreambuf_iterator<char>() );
3904 BOOST_CHECK_EQUAL( content, "initial\n" );
3905
3906 // Restoring the same version again is a no-op.
3907 BOOST_CHECK( common.RestoreVersion( wxString::FromUTF8( firstHash ), error ) == RestoreResult::AlreadyCurrent );
3908
3909 git_repository_free( repo );
3910}
3911
3912
3913// Restoring one project in a shared repository must leave the siblings current.
3914BOOST_AUTO_TEST_CASE( RestoreScopesToProjectDirectory )
3915{
3916 BOOST_TEST_REQUIRE( ready() );
3917
3918 KIGIT::GitRepositoryPtr repo( openRepo() );
3919 BOOST_REQUIRE( repo );
3920
3921 git_oid rootOid;
3922 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &rootOid, repo.get(), "HEAD" ), GIT_OK );
3923 createCommit( repo.get(), "projects/board/f.txt", "a1\n", "A v1" );
3924 git_oid bothV1 = createCommit( repo.get(), "projects/board2/g.txt", "b1\n", "B v1" );
3925
3926 createCommit( repo.get(), "projects/board/f.txt", "a2\n", "A v2" );
3927 createCommit( repo.get(), "projects/board2/g.txt", "b2\n", "B v2" );
3928
3929 char v1Hash[GIT_OID_HEXSZ + 1] = { 0 };
3930 git_oid_tostr( v1Hash, sizeof( v1Hash ), &bothV1 );
3931
3932 const wxString projectPath = repoPath() + wxS( "/projects/board" );
3933 KIGIT_COMMON common( repo.get(), projectPath );
3934 KIGIT_ERROR_MESSAGE error;
3935
3936 BOOST_REQUIRE_MESSAGE( common.RestoreVersion( wxString::FromUTF8( v1Hash ), error ) == RestoreResult::Restored,
3937 "RestoreVersion failed: " + error.summary.ToStdString() );
3938
3939 auto readAll = []( const wxString& aPath )
3940 {
3941 std::ifstream in( aPath.ToStdString(), std::ios::binary );
3942 return std::string( ( std::istreambuf_iterator<char>( in ) ), std::istreambuf_iterator<char>() );
3943 };
3944
3945 // The restored project went back, the sibling stayed current.
3946 BOOST_CHECK_EQUAL( readAll( repoPath() + wxS( "/projects/board/f.txt" ) ), "a1\n" );
3947 BOOST_CHECK_EQUAL( readAll( repoPath() + wxS( "/projects/board2/g.txt" ) ), "b2\n" );
3948 BOOST_CHECK_EQUAL( readAll( repoPath() + wxS( "/file.txt" ) ), "initial\n" );
3949
3950 // A version from before the project's folder existed is refused, not emptied.
3951 char rootHash[GIT_OID_HEXSZ + 1] = { 0 };
3952 git_oid_tostr( rootHash, sizeof( rootHash ), &rootOid );
3953
3954 BOOST_CHECK( common.RestoreVersion( wxString::FromUTF8( rootHash ), error ) == RestoreResult::Error );
3955 BOOST_CHECK_EQUAL( readAll( repoPath() + wxS( "/projects/board/f.txt" ) ), "a1\n" );
3956
3957#ifndef __WINDOWS__
3958 // A symlink to the project must restore the same folder.
3959 const wxString projectLink = TempPath() + wxS( "/project-link" );
3960 BOOST_REQUIRE_EQUAL( symlink( projectPath.utf8_str(), projectLink.utf8_str() ), 0 );
3961 createCommit( repo.get(), "projects/board/f.txt", "a3\n", "A v3" );
3962
3963 KIGIT_COMMON linkedProject( repo.get(), projectLink );
3964 BOOST_REQUIRE_MESSAGE( linkedProject.RestoreVersion( wxString::FromUTF8( v1Hash ), error )
3966 "RestoreVersion failed: " + error.summary.ToStdString() );
3967 BOOST_CHECK_EQUAL( readAll( repoPath() + wxS( "/projects/board/f.txt" ) ), "a1\n" );
3968 BOOST_CHECK_EQUAL( readAll( repoPath() + wxS( "/projects/board2/g.txt" ) ), "b2\n" );
3969#endif
3970}
3971
3972
3973BOOST_AUTO_TEST_CASE( RestoreRefusesUnknownOrOutsideProject )
3974{
3975 BOOST_TEST_REQUIRE( ready() );
3976
3977 KIGIT::GitRepositoryPtr repo( openRepo() );
3978 BOOST_REQUIRE( repo );
3979
3980 git_oid firstOid;
3981 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &firstOid, repo.get(), "HEAD" ), GIT_OK );
3982 git_oid currentOid = createCommit( repo.get(), "file.txt", "second\n", "Second version" );
3983
3984 char firstHash[GIT_OID_HEXSZ + 1] = { 0 };
3985 git_oid_tostr( firstHash, sizeof( firstHash ), &firstOid );
3986
3987 const wxString outsidePath = repoPath() + wxS( "2" );
3988 BOOST_REQUIRE( wxFileName::Mkdir( outsidePath ) );
3989 const std::vector<wxString> projectPaths = { wxEmptyString, TempPath(), outsidePath };
3990
3991 for( const wxString& projectPath : projectPaths )
3992 {
3993 KIGIT_COMMON common( repo.get(), projectPath );
3994 KIGIT_ERROR_MESSAGE error;
3995
3996 BOOST_CHECK( common.RestoreVersion( wxString::FromUTF8( firstHash ), error ) == RestoreResult::Error );
3997 BOOST_CHECK( !error.details.IsEmpty() );
3998
3999 git_oid afterOid;
4000 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &afterOid, repo.get(), "HEAD" ), GIT_OK );
4001 BOOST_CHECK( git_oid_equal( &currentOid, &afterOid ) );
4002
4003 unsigned int status = 0;
4004 BOOST_REQUIRE_EQUAL( git_status_file( &status, repo.get(), "file.txt" ), GIT_OK );
4005 BOOST_CHECK_EQUAL( status, GIT_STATUS_CURRENT );
4006
4007 wxString contents;
4008 wxFile file( repoPath() + wxS( "/file.txt" ) );
4009 BOOST_REQUIRE( file.IsOpened() );
4010 BOOST_REQUIRE( file.ReadAll( &contents ) );
4011 BOOST_CHECK_EQUAL( contents.ToStdString(), "second\n" );
4012 }
4013}
4014
4015
4016// The colliding content was never committed, so nothing could bring it back.
4017BOOST_AUTO_TEST_CASE( RestoreRefusesUntrackedCollision )
4018{
4019 BOOST_TEST_REQUIRE( ready() );
4020
4021 git_repository* repo = openRepo();
4022 BOOST_TEST_REQUIRE( repo );
4023
4024 // A version that carries notes.txt, then a commit deleting it.
4025 git_oid withNotesOid = createCommit( repo, wxT( "notes.txt" ), wxT( "keep\n" ), wxT( "Add notes" ) );
4026
4027 {
4028 git_index* index = nullptr;
4029 BOOST_REQUIRE_EQUAL( git_repository_index( &index, repo ), GIT_OK );
4030 BOOST_REQUIRE_EQUAL( git_index_remove_bypath( index, "notes.txt" ), GIT_OK );
4031 git_index_write( index );
4032
4033 git_oid treeOid;
4034 BOOST_REQUIRE_EQUAL( git_index_write_tree( &treeOid, index ), GIT_OK );
4035 git_index_free( index );
4036
4037 git_tree* tree = nullptr;
4038 BOOST_REQUIRE_EQUAL( git_tree_lookup( &tree, repo, &treeOid ), GIT_OK );
4039
4040 git_commit* parent = nullptr;
4041 BOOST_REQUIRE_EQUAL( git_commit_lookup( &parent, repo, &withNotesOid ), GIT_OK );
4042
4043 git_signature* sig = nullptr;
4044 BOOST_REQUIRE_EQUAL( git_signature_now( &sig, TEST_AUTHOR_NAME, TEST_AUTHOR_EMAIL ), GIT_OK );
4045
4046 const git_commit* parents[1] = { parent };
4047 git_oid deleteOid;
4048 BOOST_REQUIRE_EQUAL(
4049 git_commit_create( &deleteOid, repo, "HEAD", sig, sig, nullptr, "Delete notes", tree, 1, parents ),
4050 GIT_OK );
4051
4052 git_signature_free( sig );
4053 git_commit_free( parent );
4054 git_tree_free( tree );
4055 wxRemoveFile( repoPath() + wxFileName::GetPathSeparator() + wxT( "notes.txt" ) );
4056 }
4057
4058 char withNotesHash[GIT_OID_HEXSZ + 1] = { 0 };
4059 git_oid_tostr( withNotesHash, sizeof( withNotesHash ), &withNotesOid );
4060
4061 KIGIT_COMMON common( repo, repoPath() );
4062 KIGIT_ERROR_MESSAGE error;
4063
4064 // A new, never committed notes.txt collides with the version being restored.
4065 wxString notesFile = repoPath() + wxFileName::GetPathSeparator() + wxT( "notes.txt" );
4066 {
4067 std::ofstream f( notesFile.ToStdString(), std::ios::binary );
4068 f << "precious\n";
4069 }
4070
4071 // An untracked file the old version does not carry must not block anything.
4072 wxString unrelatedFile = repoPath() + wxFileName::GetPathSeparator() + wxT( "unrelated.txt" );
4073 {
4074 std::ofstream f( unrelatedFile.ToStdString(), std::ios::binary );
4075 f << "scratch\n";
4076 }
4077
4078 auto readAll = []( const wxString& aPath )
4079 {
4080 std::ifstream in( aPath.ToStdString(), std::ios::binary );
4081 return std::string( ( std::istreambuf_iterator<char>( in ) ), std::istreambuf_iterator<char>() );
4082 };
4083
4084 BOOST_CHECK( common.RestoreVersion( wxString::FromUTF8( withNotesHash ), error ) == RestoreResult::Dirty );
4085 BOOST_CHECK_EQUAL( readAll( notesFile ), "precious\n" );
4086
4087 // With the colliding file gone, the harmless untracked one alone does not block.
4088 wxRemoveFile( notesFile );
4089
4090 BOOST_REQUIRE_MESSAGE( common.RestoreVersion( wxString::FromUTF8( withNotesHash ), error )
4092 "RestoreVersion failed: " + error.summary.ToStdString() );
4093 BOOST_CHECK_EQUAL( readAll( notesFile ), "keep\n" );
4094 BOOST_CHECK_EQUAL( readAll( unrelatedFile ), "scratch\n" );
4095
4096 git_repository_free( repo );
4097}
4098
4099
4100// A file deleted here and changed on the remote conflicts with no local side. Keeping the
4101// deletion must produce a merge commit without the file, not a zero byte one.
4102BOOST_AUTO_TEST_CASE( MergeConflictKeepsDeletion )
4103{
4104 BOOST_TEST_REQUIRE( ready() );
4105
4106 git_repository* repo = openRepo();
4107 BOOST_TEST_REQUIRE( repo );
4108
4109 git_reference* headRef = nullptr;
4110 BOOST_REQUIRE_EQUAL( git_repository_head( &headRef, repo ), GIT_OK );
4111 git_oid baseOid = *git_reference_target( headRef );
4112 git_reference_free( headRef );
4113
4114 // Local side deletes the file.
4115 {
4116 git_index* index = nullptr;
4117 BOOST_REQUIRE_EQUAL( git_repository_index( &index, repo ), GIT_OK );
4118 BOOST_REQUIRE_EQUAL( git_index_remove_bypath( index, "file.txt" ), GIT_OK );
4119 git_index_write( index );
4120
4121 git_oid treeOid;
4122 BOOST_REQUIRE_EQUAL( git_index_write_tree( &treeOid, index ), GIT_OK );
4123 git_index_free( index );
4124
4125 git_tree* tree = nullptr;
4126 BOOST_REQUIRE_EQUAL( git_tree_lookup( &tree, repo, &treeOid ), GIT_OK );
4127
4128 git_commit* baseCommit = nullptr;
4129 BOOST_REQUIRE_EQUAL( git_commit_lookup( &baseCommit, repo, &baseOid ), GIT_OK );
4130
4131 git_signature* sig = nullptr;
4132 BOOST_REQUIRE_EQUAL( git_signature_now( &sig, TEST_AUTHOR_NAME, TEST_AUTHOR_EMAIL ), GIT_OK );
4133
4134 const git_commit* parents[1] = { baseCommit };
4135 git_oid deleteOid;
4136 BOOST_REQUIRE_EQUAL(
4137 git_commit_create( &deleteOid, repo, "HEAD", sig, sig, nullptr, "Delete file", tree, 1, parents ),
4138 GIT_OK );
4139
4140 git_signature_free( sig );
4141 git_commit_free( baseCommit );
4142 git_tree_free( tree );
4143 wxRemoveFile( repoPath() + wxFileName::GetPathSeparator() + wxT( "file.txt" ) );
4144 }
4145
4146 // Remote side modifies it, without moving HEAD.
4147 git_oid remoteCommitOid = createDetachedCommit( repo, baseOid, wxT( "file.txt" ), "remote\n", "Remote change" );
4148
4149 writeFetchHead( remoteCommitOid );
4150
4151 KIGIT_COMMON common( repo );
4152
4153 wxString stageDir = tempBase() + wxFileName::GetPathSeparator() + wxT( "del_stages" );
4154 wxString headOid, theirsOid, conflictsError;
4155 KIGIT_ERROR_MESSAGE error;
4156 std::vector<KIGIT_CONFLICT_SIDES> conflicts;
4157
4158 BOOST_REQUIRE_MESSAGE( common.GetMergeConflicts( stageDir, headOid, theirsOid, conflicts, conflictsError ),
4159 "GetMergeConflicts failed: " + conflictsError.ToStdString() );
4160 BOOST_REQUIRE_EQUAL( conflicts.size(), 1u );
4161 BOOST_CHECK( !conflicts[0].hasOurs );
4162 BOOST_CHECK( conflicts[0].hasTheirs );
4163
4164 // Keep the deletion: an empty resolved path, not the extracted empty placeholder file.
4165 BOOST_REQUIRE_MESSAGE( common.CreateMergedCommit( headOid, theirsOid, { { conflicts[0].path, wxString() } }, error )
4167 "CreateMergedCommit failed: " + error.summary.ToStdString() );
4168
4169 git_oid newHead;
4170 BOOST_REQUIRE_EQUAL( git_reference_name_to_id( &newHead, repo, "HEAD" ), GIT_OK );
4171
4172 git_commit* mergeCommit = nullptr;
4173 BOOST_REQUIRE_EQUAL( git_commit_lookup( &mergeCommit, repo, &newHead ), GIT_OK );
4174 BOOST_CHECK_EQUAL( git_commit_parentcount( mergeCommit ), 2u );
4175
4176 git_tree* mergedTree = nullptr;
4177 BOOST_REQUIRE_EQUAL( git_commit_tree( &mergedTree, mergeCommit ), GIT_OK );
4178 BOOST_CHECK_MESSAGE( git_tree_entry_byname( mergedTree, "file.txt" ) == nullptr,
4179 "keeping the deletion must not commit the file" );
4180 git_tree_free( mergedTree );
4181 git_commit_free( mergeCommit );
4182
4183 BOOST_CHECK( !wxFileName::FileExists( repoPath() + wxFileName::GetPathSeparator() + wxT( "file.txt" ) ) );
4184
4185 git_repository_free( repo );
4186}
4187
4188
4189// Viewing a diff materializes a committed tree, and that must never replace what the user has
4190// staged.
4191BOOST_AUTO_TEST_CASE( WriteTreeToDir_LeavesIndexUntouched )
4192{
4193 BOOST_TEST_REQUIRE( ready() );
4194
4195 git_repository* repo = openRepo();
4196 BOOST_TEST_REQUIRE( repo );
4197
4198 // Stage content that differs from both HEAD and the working file.
4199 wxString filePath = repoPath() + wxFileName::GetPathSeparator() + wxT( "file.txt" );
4200 {
4201 std::ofstream f( filePath.ToStdString() );
4202 f << "staged-work-in-progress";
4203 }
4204
4205 git_index* index = nullptr;
4206 BOOST_REQUIRE_EQUAL( git_repository_index( &index, repo ), GIT_OK );
4207 git_index_add_bypath( index, "file.txt" );
4208 git_index_write( index );
4209
4210 const git_index_entry* before = git_index_get_bypath( index, "file.txt", 0 );
4211 BOOST_REQUIRE( before );
4212 git_oid stagedOid = before->id;
4213
4214 // Under the fixture's own directory, so a failed run leaves nothing behind in the system temp.
4215 wxString dest = tempBase() + wxFileName::GetPathSeparator() + wxT( "tree_out" );
4216
4217 BOOST_CHECK( KIGIT::PROJECT_GIT_UTILS::WriteTreeToDir( repo, wxT( "HEAD" ), dest ) );
4218
4219 // Re-read the index from disk and confirm the staged blob is still the one we staged, not
4220 // the committed version that was checked out.
4221 git_index_read( index, 1 );
4222 const git_index_entry* after = git_index_get_bypath( index, "file.txt", 0 );
4223 BOOST_REQUIRE( after );
4224 BOOST_CHECK_MESSAGE( git_oid_equal( &stagedOid, &after->id ),
4225 "WriteTreeToDir must not rewrite the repository index" );
4226
4227 git_index_free( index );
4228
4229 if( wxFileName::DirExists( dest ) )
4230 wxFileName::Rmdir( dest, wxPATH_RMDIR_RECURSIVE );
4231
4232 git_repository_free( repo );
4233}
4234
4235
int index
const char * name
void AdvancePhase() override
Use the next available virtual zone of the dialog progress bar.
void SetTitle(const wxString &) override
Change the title displayed on the window caption.
void AdvanceProgress() override
Increment the progress bar length (inside the current virtual zone).
void SetCurrentProgress(double) override
Set the progress value to aProgress (0..1).
void Report(const wxString &) override
Display aMessage in the progress bar dialog.
void BeginPhase(int) override
Initialize the aPhase virtual zone of the dialog progress bar.
bool IsCancelled() const override
void AdvancePhase(const wxString &) override
Use the next available virtual zone of the dialog progress bar and updates the message.
void SetMaxProgress(int) override
Fix the value that gives the 100 percent progress bar length (inside the current virtual zone).
bool KeepRefreshing(bool=false) override
Update the UI (if any).
void SetNumPhases(int) override
Set the number of phases.
BranchResult SwitchToBranch(const wxString &aBranchName)
Switch to the specified branch.
void SetRemote(const wxString &aRemote)
void SetClonePath(const wxString &aPath)
CommitResult PerformCommit(const std::vector< wxString > &aFiles, const wxString &aMessage, const wxString &aAuthorName, const wxString &aAuthorEmail)
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.
bool SetupRemote(const RemoteConfig &aConfig)
Set up a remote for the repository.
void SetProgressReporter(std::unique_ptr< PROGRESS_REPORTER > aProgressReporter)
bool PerformFetch(bool aSkipLock=false)
PullResult PerformPull()
PushResult PerformPush(bool aForce=false)
std::map< wxString, FileStatus > GetFileStatus(const wxString &aPathspec=wxEmptyString, bool aChangedOnly=false)
Get detailed file status for all files in the specified path.
Utility class with helper functions for project level git operations.
static bool IsTransientGitFile(const wxFileName &aFile)
Match a project-relative path against the shared transient patterns.
static bool WriteTreeToDir(git_repository *aRepo, const wxString &aRef, const wxString &aDestDir)
Check out a whole tree into a directory, leaving the repository's working copy and index alone.
static bool IsWithinProjectPath(const wxString &aAbsPath, const wxString &aProjectPath)
Test whether an absolute file path lives inside the current project directory.
Publishes a token for as long as its operation runs.
The project's git session.
std::shared_ptr< std::atomic< bool > > CANCEL_TOKEN
One token per operation.
GIT_CONN_TYPE GetConnType() const
std::vector< KIGIT_COMMIT_INFO > GetRecentCommits(size_t aMax=50, const KIGIT_HISTORY_FILTER &aFilter={}) const
bool HasUncommittedTrackedChanges(bool *aReadOk=nullptr) const
wxString GetRemote() const
bool IsOperationInProgress() const
True while a merge, rebase, cherry-pick or revert is in progress, which refuses commits.
std::unique_lock< std::mutex > LockAction()
bool HasUpstreamCommits() const
True when the upstream branch has commits the current branch does not.
git_repository * GetRepo() const
bool AddRemote(const wxString &aName, const wxString &aUrl)
static CANCEL_TOKEN NewCancelToken()
bool GetMergeConflicts(const wxString &aStageDir, wxString &aHeadOid, wxString &aTheirsOid, std::vector< KIGIT_CONFLICT_SIDES > &aConflicts, wxString &aError)
Recompute the merge in memory and extract each conflicted file's three versions into aStageDir,...
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 IsShuttingDown() const
Background timers ask this before starting work of their own.
bool DiscardChanges(const std::vector< wxString > &aPaths, wxString &aError)
Put the given working directory relative paths back to their committed state.
wxString GetUsername() const
void SetShuttingDown(bool aCancel)
Stop the operation running now and keep later ones from starting.
bool IsPathInHead(const wxString &aPath)
True when the working directory relative path exists in the HEAD commit's tree.
MergeCommitResult CreateMergedCommit(const wxString &aExpectedHeadOid, const wxString &aTheirsOid, const std::vector< std::pair< wxString, wxString > > &aResolvedFiles, KIGIT_ERROR_MESSAGE &aError)
Redo the merge with each conflicted path replaced by its resolved file, then commit and check out.
bool AbandonOperation(wxString &aError)
Drop that operation and return to the last commit. The caller holds the action lock.
wxString GetPassword()
GitOperation OperationInProgress() const
Which operation that is, so the panel can name it.
RestoreResult RestoreVersion(const wxString &aCommitHash, KIGIT_ERROR_MESSAGE &aError)
Restore the project as it was at aCommitHash, recorded as a new commit on top of the current head.
void SetRepo(git_repository *aRepo)
const std::vector< wxString > & GetErrorStrings() const
wxString GetErrorString()
void SetCancelToken(const KIGIT_COMMON::CANCEL_TOKEN &aToken)
Callbacks read this token rather than the session, so one operation's cancel cannot reach another.
const KIGIT_COMMON::CANCEL_TOKEN & CancelToken() const
bool IsThisOperationCancelled() const
Give each test a backend and a temporary directory.
const wxString & TempPath() const
Container for project specific data.
Definition project.h:63
bool LoadProject(const wxString &aFullPath, bool aSetActive=true)
Load a project or sets up a new project with a specified path.
PROJECT * GetProject(const wxString &aFullPath) const
Retrieve a loaded project by name.
bool UnloadProject(PROJECT *aProject, bool aSave=true)
Save, unload and unregister the given PROJECT.
static std::string ToStdString(const wxString &aStr)
void SetGitBackend(GIT_BACKEND *aBackend)
GIT_BACKEND * GetGitBackend()
CommitResult
Definition git_backend.h:54
@ SuccessIndexStale
Committed, but the staged list was left as another program had it. Never retry on this.
Definition git_backend.h:58
InitResult
PullResult
PushResult
@ Busy
Another git operation held the action lock, so nothing was pushed.
std::vector< FILE_CHECKLIST_ENTRY > BuildGitChangeEntries(KIGIT_COMMON &aCommon, const wxString &aProjectPath, bool aDarkTheme)
Build the file checklist entries for the project's changed files, transient files filtered out and ea...
int push_transfer_progress_cb(unsigned int aCurrent, unsigned int aTotal, size_t aBytes, void *aPayload)
@ AlreadyCurrent
The chosen version's content matches the current head.
@ Restored
A new commit carrying the old version's content was created.
@ Error
The restore failed. See the error string.
@ Dirty
Uncommitted changes exist, and restoring would overwrite them.
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_index, decltype([](git_index *aIndex) { git_index_free(aIndex); })> GitIndexPtr
A unique pointer for git_index objects with automatic cleanup.
KIGIT::GitRepositoryPtr InitGitRepository(const wxString &aPath, const char *aBranch="main", const char *aAuthorName="QA Test", const char *aAuthorEmail="[email protected]")
Pin the branch and author so repository setup does not depend on the user's Git config.
void WriteGitFile(const wxString &aRepoPath, const wxString &aRelativePath, const std::string &aContents)
Write test data without staging it. Create parent directories when needed.
git_oid CommitAllGitFiles(git_repository *aRepo, const wxString &aMessage)
Stage all changes, including deletions, and commit them.
git_oid CommitGitFile(git_repository *aRepo, const wxString &aPath, const wxString &aMessage)
Stage one file and commit the index. Other staged changes are kept.
bool collide(T aObject, U aAnotherObject, int aLayer, int aMinDistance)
Used by SHAPE_INDEX to implement Query().
One file row in a FILE_CHECKLIST_CTRL.
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.
void useEmptyGlobalIgnore(git_repository *aRepo)
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.
void writeFetchHead(const git_oid &aOid)
Write .git/FETCH_HEAD naming aOid, as a fetch would leave it.
git_oid createDetachedCommit(git_repository *aRepo, const git_oid &aParentOid, const wxString &aFile, const std::string &aContent, const char *aMessage)
Create a commit on top of aParentOid setting aFile to aContent, without moving HEAD.
const wxString & tempBase() const
A summary the user can act on, with the raw text that explains it kept apart for the details.
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_CHECK_EQUAL_COLLECTIONS(mixed.begin(), mixed.end(), expMixed.begin(), expMixed.end())
static const char * TEST_AUTHOR_NAME
static const char * TEST_AUTHOR_EMAIL
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
std::string path
IbisParser parser & reporter
static const char * TEST_AUTHOR_NAME
static const char * TEST_AUTHOR_EMAIL
BOOST_AUTO_TEST_CASE(FetchHeadMarksAtMostOneBranchToMerge)
static wxString seedProjectFile(const wxString &aDir)
VECTOR3I expected(15, 30, 45)
BOOST_TEST_CONTEXT("Test Clearance")
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")
Definition of file extensions used in Kicad.