KiCad PCB EDA Suite
Loading...
Searching...
No Matches
kicad_git_common.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.TXT for contributors.
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 3
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 */
19
20#include "kicad_git_common.h"
21#include "kicad_git_memory.h"
22#include "git_repo_mixin.h"
23#include "git_backend.h"
24#include "kigit_ssh_keys.h"
25#include "project_git_utils.h"
26
27#include <git/git_progress.h>
29#include <kiplatform/secrets.h>
30#include <trace_helpers.h>
31
32#include <git2.h>
33#include <wx/arrstr.h>
34#include <wx/base64.h>
35#include <wx/filename.h>
36#include <wx/log.h>
37#include <wx/utils.h>
38#include <algorithm>
39#include <cctype>
40#include <cerrno>
41#include <chrono>
42#include <cstdlib>
43#include <cstring>
44#include <map>
45#include <set>
46#include <string>
47#include <thread>
48#include <vector>
49
50
52{
53 static wxWindow* window = nullptr;
54
55 return window;
56}
57
58
59KIGIT_COMMON::KIGIT_COMMON( git_repository* aRepo, const wxString& aProjectPath ) :
60 m_repo( aRepo ),
61 m_projectPath( aProjectPath ),
63 m_testedTypes( 0 ),
64 m_nextPublicKey( 0 ),
65 m_secretFetched( false )
66{}
67
68
70 // Initialize base class and member variables
71 m_repo( aOther.m_repo ),
72 m_projectDir( aOther.m_projectDir ),
74 m_connType( aOther.m_connType ),
75 m_remote( aOther.m_remote ),
77 m_hostname( aOther.m_hostname ),
78 m_port( aOther.m_port ),
79 m_username( aOther.m_username ),
80 m_password( aOther.m_password ),
81 m_testedTypes( aOther.m_testedTypes.load() ),
82
83 // The mutex is default-initialized, not copied
86 m_publicKeys( aOther.m_publicKeys ),
91{
92}
93
94
97
98
100{
101 return m_repo;
102}
103
104
106{
107 if( !m_projectDir.IsEmpty() )
108 return m_projectDir;
109
110 if( m_repo )
111 {
112 const char* workdir = git_repository_workdir( m_repo );
113
114 if( workdir )
115 return wxString( workdir );
116 }
117
118 return wxEmptyString;
119}
120
121
122std::optional<wxString> KIGIT_COMMON::GetProjectPrefix() const
123{
124 if( !m_repo || !git_repository_workdir( m_repo ) )
125 return std::nullopt;
126
128}
129
130
132{
133 if( !m_repo )
134 return wxEmptyString;
135
136 git_reference* head = nullptr;
137
138 int retval = git_repository_head( &head, m_repo );
139
140 if( retval && retval != GIT_EUNBORNBRANCH && retval != GIT_ENOTFOUND )
141 return wxEmptyString;
142
143 // An unborn or missing HEAD leaves head null, so there is no branch to resolve.
144 if( !head )
145 return wxEmptyString;
146
147 KIGIT::GitReferencePtr headPtr( head );
148 git_reference* branch;
149
150 if( git_reference_resolve( &branch, head ) )
151 {
152 wxLogTrace( traceGit, "Failed to resolve branch" );
153 return wxEmptyString;
154 }
155
156 KIGIT::GitReferencePtr branchPtr( branch );
157 const char* branchName = "";
158
159 if( git_branch_name( &branchName, branch ) )
160 {
161 wxLogTrace( traceGit, "Failed to get branch name" );
162 return wxEmptyString;
163 }
164
165 return wxString( branchName );
166}
167
168
170{
171 git_oid oid;
172
173 if( !m_repo || git_reference_name_to_id( &oid, m_repo, "HEAD" ) != GIT_OK )
174 return wxEmptyString;
175
176 return wxString::FromUTF8( git_oid_tostr_s( &oid ) );
177}
178
179
181{
182 wxString remote;
183 wxString username;
184
185 {
186 std::lock_guard<std::mutex> lock( m_credentialsMutex );
187
188 if( m_secretFetched )
189 return m_password;
190
192 {
193 m_secretFetched = true;
194 return m_password;
195 }
196
197 remote = m_remote;
198 username = m_username;
199 }
200
201 // The lock is not held here: asking the operating system can raise a permission dialog, so
202 // it finishes only once a person has decided.
203 wxString secret;
204 bool found = KIPLATFORM::SECRETS::GetSecret( remote, username, secret );
205
206 std::lock_guard<std::mutex> lock( m_credentialsMutex );
207
208 // The user may have typed a password, or changed the remote, while the keychain was being
209 // asked. Either way this answer is about the wrong thing and is dropped.
210 if( m_secretFetched || m_remote != remote || m_username != username )
211 return m_password;
212
213 if( found )
214 m_password = secret;
215
216 m_secretFetched = true;
217 return m_password;
218}
219
220
222{
223 if( !m_repo )
224 return wxEmptyString;
225
226 git_oid headOid;
227
228 if( git_reference_name_to_id( &headOid, m_repo, "HEAD" ) != GIT_OK )
229 return wxEmptyString;
230
231 git_commit* commit = nullptr;
232
233 if( git_commit_lookup( &commit, m_repo, &headOid ) != GIT_OK )
234 return wxEmptyString;
235
236 KIGIT::GitCommitPtr commitPtr( commit );
237 const char* message = git_commit_message( commit );
238
239 return message ? KIGIT::DecodeRepoText( message ) : wxString();
240}
241
242
244{
245 if( aReadOk )
246 *aReadOk = false;
247
248 if( !m_repo )
249 return false;
250
251 git_status_options statusOpts;
252 git_status_init_options( &statusOpts, GIT_STATUS_OPTIONS_VERSION );
253 statusOpts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
254
255 git_status_list* statusList = nullptr;
256
257 if( git_status_list_new( &statusList, m_repo, &statusOpts ) != GIT_OK )
258 return false;
259
260 KIGIT::GitStatusListPtr statusListPtr( statusList );
261
262 if( aReadOk )
263 *aReadOk = true;
264
265 return git_status_list_entrycount( statusList ) > 0;
266}
267
268
269namespace
270{
271
272// The refs pointing at each commit, keyed by the commit's full hash, HEAD first.
273std::map<std::string, std::vector<wxString>> collectRefNames( git_repository* aRepo )
274{
275 std::map<std::string, std::vector<wxString>> names;
276
277 git_oid headOid;
278
279 if( git_reference_name_to_id( &headOid, aRepo, "HEAD" ) == GIT_OK )
280 names[git_oid_tostr_s( &headOid )].push_back( wxS( "HEAD" ) );
281
282 git_reference_iterator* iter = nullptr;
283
284 if( git_reference_iterator_new( &iter, aRepo ) != GIT_OK )
285 return names;
286
287 KIGIT::GitReferenceIteratorPtr iterPtr( iter );
288 git_reference* ref = nullptr;
289
290 // Iteration is by ref name, which puts local branches before remote ones and tags last.
291 while( git_reference_next( &ref, iter ) == GIT_OK )
292 {
293 KIGIT::GitReferencePtr refPtr( ref );
294
295 if( !git_reference_is_branch( ref ) && !git_reference_is_remote( ref ) && !git_reference_is_tag( ref ) )
296 continue;
297
298 git_object* target = nullptr;
299
300 if( git_reference_peel( &target, ref, GIT_OBJECT_COMMIT ) != GIT_OK )
301 continue;
302
303 KIGIT::GitObjectPtr targetPtr( target );
304
305 std::string hash = git_oid_tostr_s( git_object_id( target ) );
306 names[hash].push_back( KIGIT::DecodeRepoText( git_reference_shorthand( ref ) ) );
307 }
308
309 return names;
310}
311
312
313wxString shortHashOf( const git_oid* aOid )
314{
315 // A 7-char abbreviation can be ambiguous in a large repository, which makes a later
316 // revparse of the hash fail. Twelve characters is what git uses for big repos.
317 char shortHash[13] = { 0 };
318 git_oid_tostr( shortHash, sizeof( shortHash ), aOid );
319 return wxString::FromUTF8( shortHash );
320}
321
322
323KIGIT::FILE_CHANGE_STATUS deltaStatus( git_delta_t aDelta )
324{
325 switch( aDelta )
326 {
327 case GIT_DELTA_ADDED: return KIGIT::FILE_CHANGE_STATUS::ADDED;
328 case GIT_DELTA_DELETED: return KIGIT::FILE_CHANGE_STATUS::REMOVED;
329 case GIT_DELTA_RENAMED: return KIGIT::FILE_CHANGE_STATUS::RENAMED;
330 case GIT_DELTA_COPIED: return KIGIT::FILE_CHANGE_STATUS::COPIED;
331 case GIT_DELTA_TYPECHANGE: return KIGIT::FILE_CHANGE_STATUS::TYPECHANGE;
333 }
334}
335
336} // namespace
337
338
340{
341 if( !m_repo )
342 return wxDateTime();
343
344 const char* gitDir = git_repository_path( m_repo );
345
346 if( !gitDir )
347 return wxDateTime();
348
349 // Git rewrites FETCH_HEAD on every fetch, so its timestamp is the last one.
350 wxFileName fetchHead( wxString::FromUTF8( gitDir ), wxS( "FETCH_HEAD" ) );
351
352 if( !fetchHead.FileExists() )
353 return wxDateTime();
354
355 return fetchHead.GetModificationTime();
356}
357
358
360{
362
363 if( !m_repo )
364 return detail;
365
366 git_oid oid;
367
368 if( git_oid_fromstr( &oid, aId.utf8_string().c_str() ) != GIT_OK )
369 return detail;
370
371 git_commit* commit = nullptr;
372
373 if( git_commit_lookup( &commit, m_repo, &oid ) != GIT_OK )
374 return detail;
375
376 KIGIT::GitCommitPtr commitPtr( commit );
377
378 detail.found = true;
379 detail.when = wxDateTime( static_cast<time_t>( git_commit_time( commit ) ) );
380
381 // Git writes "WIP on <branch>: <commit> <subject>", or "On <branch>: <message>" for a
382 // message of the user's own. The branch is kept apart from the rest.
383 wxString raw;
384
385 if( const char* message = git_commit_message( commit ) )
387
388 raw.Trim();
389 detail.message = raw;
390
391 wxString rest;
392
393 if( raw.StartsWith( wxS( "WIP on " ), &rest ) || raw.StartsWith( wxS( "On " ), &rest ) )
394 {
395 int colon = rest.Find( wxS( ": " ) );
396
397 if( colon != wxNOT_FOUND )
398 {
399 detail.branch = rest.Left( colon );
400 detail.message = rest.Mid( colon + 2 );
401 }
402 }
403
404 // The first parent is the commit the stash was taken on. A third parent, when there is
405 // one, holds the untracked files.
406 git_commit* base = nullptr;
407
408 if( git_commit_parent( &base, commit, 0 ) != GIT_OK )
409 return detail;
410
411 KIGIT::GitCommitPtr basePtr( base );
412 detail.baseHash = shortHashOf( git_commit_id( base ) );
413
414 git_tree* baseTree = nullptr;
415 git_tree* stashTree = nullptr;
416
417 if( git_commit_tree( &baseTree, base ) != GIT_OK || git_commit_tree( &stashTree, commit ) != GIT_OK )
418 return detail;
419
420 KIGIT::GitTreePtr baseTreePtr( baseTree );
421 KIGIT::GitTreePtr stashTreePtr( stashTree );
422 git_diff* diff = nullptr;
423
424 if( git_diff_tree_to_tree( &diff, m_repo, baseTree, stashTree, nullptr ) == GIT_OK )
425 {
426 KIGIT::GitDiffPtr diffPtr( diff );
427
429 [&detail]( const git_diff_delta& aDelta )
430 {
431 const char* path =
432 aDelta.new_file.path ? aDelta.new_file.path : aDelta.old_file.path;
433 detail.files.push_back( wxString::FromUTF8( path ) );
434 } );
435 }
436
437 if( git_commit_parentcount( commit ) > 2 )
438 {
439 git_commit* untracked = nullptr;
440
441 if( git_commit_parent( &untracked, commit, 2 ) == GIT_OK )
442 {
443 KIGIT::GitCommitPtr untrackedPtr( untracked );
444 git_tree* untrackedTree = nullptr;
445
446 if( git_commit_tree( &untrackedTree, untracked ) == GIT_OK )
447 {
448 KIGIT::GitTreePtr untrackedTreePtr( untrackedTree );
449
450 git_tree_walk(
451 untrackedTree, GIT_TREEWALK_PRE,
452 []( const char* aRoot, const git_tree_entry* aEntry, void* aPayload ) -> int
453 {
454 if( git_tree_entry_type( aEntry ) == GIT_OBJECT_BLOB )
455 {
456 auto* files = static_cast<std::vector<wxString>*>( aPayload );
457 files->push_back( wxString::FromUTF8( aRoot )
458 + wxString::FromUTF8( git_tree_entry_name( aEntry ) ) );
459 }
460
461 return 0;
462 },
463 &detail.files );
464 }
465 }
466 }
467
468 return detail;
469}
470
471
472std::pair<int, int> KIGIT_COMMON::GetWorkingTreeLineStats( const std::set<wxString>& aPaths ) const
473{
474 if( !m_repo )
475 return { 0, 0 };
476
477 // An unborn repository has no HEAD tree, and then everything in the tree is new.
478 KIGIT::GitTreePtr headTree;
479 git_object* headObj = nullptr;
480
481 if( git_revparse_single( &headObj, m_repo, "HEAD^{tree}" ) == GIT_OK )
482 headTree.reset( reinterpret_cast<git_tree*>( headObj ) );
483
484 git_diff_options opts;
485 git_diff_options_init( &opts, GIT_DIFF_OPTIONS_VERSION );
486 opts.flags = GIT_DIFF_INCLUDE_UNTRACKED | GIT_DIFF_RECURSE_UNTRACKED_DIRS | GIT_DIFF_SHOW_UNTRACKED_CONTENT;
487
488 git_diff* diff = nullptr;
489
490 if( git_diff_tree_to_workdir_with_index( &diff, m_repo, headTree.get(), &opts ) != GIT_OK )
491 return { 0, 0 };
492
493 KIGIT::GitDiffPtr diffPtr( diff );
494 std::pair<int, int> total{ 0, 0 };
495
496 for( size_t ii = 0; ii < git_diff_num_deltas( diff ); ++ii )
497 {
498 git_patch* patch = nullptr;
499
500 if( git_patch_from_diff( &patch, diff, ii ) != GIT_OK )
501 continue;
502
503 KIGIT::GitPatchPtr patchPtr( patch );
504 const git_diff_delta* delta = git_patch_get_delta( patch );
505 const char* path = delta->new_file.path ? delta->new_file.path : delta->old_file.path;
506
507 if( !path || aPaths.count( wxString::FromUTF8( path ) ) == 0 )
508 continue;
509
510 size_t added = 0;
511 size_t removed = 0;
512
513 if( git_patch_line_stats( nullptr, &added, &removed, patch ) == GIT_OK )
514 {
515 total.first += static_cast<int>( added );
516 total.second += static_cast<int>( removed );
517 }
518 }
519
520 return total;
521}
522
523
524std::vector<KIGIT_BRANCH_INFO> KIGIT_COMMON::GetBranches() const
525{
526 if( !m_repo )
527 return {};
528
529 std::vector<KIGIT_BRANCH_INFO> branches;
530
531 git_branch_iterator* branchIterator = nullptr;
532
533 if( git_branch_iterator_new( &branchIterator, m_repo, GIT_BRANCH_ALL ) )
534 {
535 wxLogTrace( traceGit, "Failed to get branch iterator" );
536 return {};
537 }
538
539 KIGIT::GitBranchIteratorPtr branchIteratorPtr( branchIterator );
540 git_reference* branchReference = nullptr;
541 git_branch_t branchType;
542
543 // Only GIT_ITEROVER means the end. Any other error leaves the reference invalid.
544 while( true )
545 {
546 branchReference = nullptr;
547
548 int rc = git_branch_next( &branchReference, &branchType, branchIterator );
549
550 if( rc == GIT_ITEROVER )
551 break;
552
553 if( rc != GIT_OK )
554 {
555 wxLogTrace( traceGit, "Failed to iterate branches: %s", KIGIT_COMMON::GetLastGitError() );
556 break;
557 }
558
559 const char* branchName = "";
560 KIGIT::GitReferencePtr branchReferencePtr( branchReference );
561
562 // origin/HEAD and other symbolic aliases do not name a separate branch.
563 if( git_reference_type( branchReference ) != GIT_REFERENCE_DIRECT )
564 continue;
565
566 if( git_branch_name( &branchName, branchReference ) )
567 {
568 wxLogTrace( traceGit, "Failed to get branch name in iter loop" );
569 continue;
570 }
571
572 const git_oid* commitId = git_reference_target( branchReference );
573 git_commit* commit = nullptr;
574
575 if( !commitId || git_commit_lookup( &commit, m_repo, commitId ) != GIT_OK )
576 {
577 wxLogTrace( traceGit, "Failed to get commit in iter loop" );
578 continue;
579 }
580
581 KIGIT::GitCommitPtr commitPtr( commit );
582 git_time_t commitTime = git_commit_time( commit );
583
585 info.name = KIGIT::DecodeRepoText( branchName );
586 info.isRemote = branchType == GIT_BRANCH_REMOTE;
587 info.isCurrent = !info.isRemote && git_branch_is_head( branchReference ) == 1;
588 info.tipHash = shortHashOf( commitId );
589 info.message = KIGIT::DecodeRepoText( git_commit_message( commit ) );
590 info.when = wxDateTime( static_cast<time_t>( commitTime ) );
591
592 branches.push_back( std::move( info ) );
593 }
594
595 return branches;
596}
597
598
600{
602
603 if( !m_repo )
604 return detail;
605
606 // The short name as the Branches page lists it, local or remote, resolved as git would.
607 git_reference* ref = nullptr;
608
609 if( git_reference_dwim( &ref, m_repo, aBranch.utf8_string().c_str() ) != GIT_OK )
610 return detail;
611
612 KIGIT::GitReferencePtr refPtr( ref );
613 git_object* target = nullptr;
614
615 if( git_reference_peel( &target, ref, GIT_OBJECT_COMMIT ) != GIT_OK )
616 return detail;
617
618 KIGIT::GitObjectPtr targetPtr( target );
619 git_commit* commit = reinterpret_cast<git_commit*>( target );
620
621 detail.found = true;
622 detail.name = aBranch;
623 detail.tipHash = shortHashOf( git_commit_id( commit ) );
624 detail.when = wxDateTime( static_cast<time_t>( git_commit_time( commit ) ) );
625
626 if( const git_signature* author = git_commit_author( commit ) )
627 {
628 detail.author = KIGIT::DecodeRepoText( author->name );
629 detail.email = KIGIT::DecodeRepoText( author->email );
630 }
631
632 if( const char* message = git_commit_message( commit ) )
634
635 detail.message.Trim();
636
637 // Only a local branch tracks another. A remote-tracking branch has nothing to compare with.
638 git_reference* upstream = nullptr;
639
640 if( !git_reference_is_branch( ref ) || git_branch_upstream( &upstream, ref ) != GIT_OK )
641 return detail;
642
643 KIGIT::GitReferencePtr upstreamPtr( upstream );
644 const char* upstreamName = "";
645
646 if( git_branch_name( &upstreamName, upstream ) == GIT_OK )
647 detail.upstream = KIGIT::DecodeRepoText( upstreamName );
648
649 detail.divergence = compareRefs( ref, upstream );
650
651 return detail;
652}
653
654
655std::vector<wxString> KIGIT_COMMON::GetProjectDirs()
656{
657 if( !m_repo )
658 return {};
659
660 std::vector<wxString> projDirs;
661
662 git_oid oid;
663 git_commit* commit;
664 git_tree *tree;
665
666 if( git_reference_name_to_id( &oid, m_repo, "HEAD" ) != GIT_OK )
667 {
668 wxLogTrace( traceGit, "An error occurred: %s", KIGIT_COMMON::GetLastGitError() );
669 return projDirs;
670 }
671
672 if( git_commit_lookup( &commit, m_repo, &oid ) != GIT_OK )
673 {
674 wxLogTrace( traceGit, "An error occurred: %s", KIGIT_COMMON::GetLastGitError() );
675 return projDirs;
676 }
677
678 KIGIT::GitCommitPtr commitPtr( commit );
679
680 if( git_commit_tree( &tree, commit ) != GIT_OK )
681 {
682 wxLogTrace( traceGit, "An error occurred: %s", KIGIT_COMMON::GetLastGitError() );
683 return projDirs;
684 }
685
686 KIGIT::GitTreePtr treePtr( tree );
687
688 // Define callback
689 git_tree_walk(
690 tree, GIT_TREEWALK_PRE,
691 []( const char* root, const git_tree_entry* entry, void* payload )
692 {
693 std::vector<wxString>* prjs = static_cast<std::vector<wxString>*>( payload );
694 wxFileName root_fn( git_tree_entry_name( entry ) );
695
696 root_fn.SetPath( root );
697
698 if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB
699 && ( ( root_fn.GetExt() == "kicad_pro" ) || ( root_fn.GetExt() == "pro" ) ) )
700 {
701 prjs->push_back( root_fn.GetFullPath() );
702 }
703
704 return 0; // continue walking
705 },
706 &projDirs );
707
708 std::sort( projDirs.begin(), projDirs.end(),
709 []( const wxString& a, const wxString& b )
710 {
711 int a_freq = a.Freq( wxFileName::GetPathSeparator() );
712 int b_freq = b.Freq( wxFileName::GetPathSeparator() );
713
714 if( a_freq == b_freq )
715 return a < b;
716 else
717 return a_freq < b_freq;
718 } );
719
720 return projDirs;
721}
722
723
724std::pair<std::set<wxString>, std::set<wxString>> KIGIT_COMMON::GetDifferentFiles() const
725{
726 std::pair<std::set<wxString>, std::set<wxString>> modified_files;
727
728 if( !m_repo || IsShuttingDown() )
729 return modified_files;
730
731 git_reference* head = nullptr;
732 git_reference* remote_head = nullptr;
733
734 if( git_repository_head( &head, m_repo ) != GIT_OK )
735 {
736 wxLogTrace( traceGit, "Failed to get modified HEAD" );
737 return modified_files;
738 }
739
740 KIGIT::GitReferencePtr headPtr( head );
741
742 if( git_branch_upstream( &remote_head, head ) != GIT_OK )
743 {
744 // Branch has no upstream tracking ref configured. Without an upstream there is
745 // nothing to compare against, so leave both sets empty. Walking commits unbounded
746 // would otherwise dump the entire history's root tree into the result.
747 wxLogTrace( traceGit, "Failed to get modified remote HEAD" );
748 return modified_files;
749 }
750
751 KIGIT::GitReferencePtr remoteHeadPtr( remote_head );
752
753 const git_oid* head_oid = git_reference_target( head );
754 const git_oid* remote_oid = git_reference_target( remote_head );
755
756 if( !head_oid || !remote_oid )
757 return modified_files;
758
759 auto load_tree = [this]( const git_oid* aOid ) -> git_tree*
760 {
761 git_commit* commit = nullptr;
762
763 if( git_commit_lookup( &commit, m_repo, aOid ) != GIT_OK )
764 {
765 wxLogTrace( traceGit, "Failed to lookup commit for diff: %s", KIGIT_COMMON::GetLastGitError() );
766 return nullptr;
767 }
768
769 KIGIT::GitCommitPtr commitPtr( commit );
770 git_tree* tree = nullptr;
771
772 if( git_commit_tree( &tree, commit ) != GIT_OK )
773 {
774 wxLogTrace( traceGit, "Failed to get commit tree for diff: %s", KIGIT_COMMON::GetLastGitError() );
775 return nullptr;
776 }
777
778 return tree;
779 };
780
781 git_tree* head_tree = load_tree( head_oid );
782 KIGIT::GitTreePtr headTreePtr( head_tree );
783
784 git_tree* remote_tree = load_tree( remote_oid );
785 KIGIT::GitTreePtr remoteTreePtr( remote_tree );
786
787 if( !head_tree || !remote_tree )
788 return modified_files;
789
790 // Find the merge-base so AHEAD and BEHIND can be distinguished. AHEAD = files that
791 // changed between merge-base and HEAD (only in local commits). BEHIND = files that
792 // changed between merge-base and the remote tip (only in remote commits). Without a
793 // shared history the merge-base lookup fails; in that case we treat both sets as empty
794 // because there is no meaningful "ahead vs behind" partition to compute.
795 git_oid base_oid;
796
797 if( git_merge_base( &base_oid, m_repo, head_oid, remote_oid ) != GIT_OK )
798 {
799 wxLogTrace( traceGit, "No merge base between local and remote: %s", KIGIT_COMMON::GetLastGitError() );
800 return modified_files;
801 }
802
803 git_tree* base_tree = load_tree( &base_oid );
804 KIGIT::GitTreePtr baseTreePtr( base_tree );
805
806 if( !base_tree )
807 return modified_files;
808
809 auto collect_paths = [this]( git_tree* aOldTree, git_tree* aNewTree, std::set<wxString>& aOut )
810 {
811 if( IsShuttingDown() )
812 return;
813
814 git_diff_options localOpts;
815 git_diff_init_options( &localOpts, GIT_DIFF_OPTIONS_VERSION );
816
817 git_diff* diff = nullptr;
818
819 if( git_diff_tree_to_tree( &diff, m_repo, aOldTree, aNewTree, &localOpts ) != GIT_OK )
820 {
821 wxLogTrace( traceGit, "Failed to diff trees: %s", KIGIT_COMMON::GetLastGitError() );
822 return;
823 }
824
826 [&aOut]( const git_diff_delta& aDelta )
827 {
828 if( aDelta.new_file.path )
829 aOut.insert( wxString::FromUTF8( aDelta.new_file.path ) );
830
831 if( aDelta.old_file.path )
832 aOut.insert( wxString::FromUTF8( aDelta.old_file.path ) );
833 } );
834
835 git_diff_free( diff );
836 };
837
838 collect_paths( base_tree, head_tree, modified_files.first ); // AHEAD
839 collect_paths( base_tree, remote_tree, modified_files.second ); // BEHIND
840
841 // Filter both sets to files whose content actually differs between HEAD and remote.
842 // Without this, a file touched by a commit that has since been replaced with an
843 // identical-tree commit (e.g. a message-only amend) keeps an AHEAD marker even
844 // though its blob matches the remote's blob.
845 std::set<wxString> actuallyDifferent;
846 collect_paths( head_tree, remote_tree, actuallyDifferent );
847
848 auto filterToDifferent = [&]( std::set<wxString>& aSet )
849 {
850 for( auto it = aSet.begin(); it != aSet.end(); )
851 it = actuallyDifferent.count( *it ) ? std::next( it ) : aSet.erase( it );
852 };
853
854 filterToDifferent( modified_files.first );
855 filterToDifferent( modified_files.second );
856
857 return modified_files;
858}
859
860
861std::optional<KIGIT_AHEAD_BEHIND> KIGIT_COMMON::compareRefs( const git_reference* aBranch,
862 const git_reference* aBase ) const
863{
864 if( !m_repo || !aBranch || !aBase )
865 return std::nullopt;
866
867 git_object* branchCommit = nullptr;
868
869 if( git_reference_peel( &branchCommit, aBranch, GIT_OBJECT_COMMIT ) != GIT_OK )
870 return std::nullopt;
871
872 KIGIT::GitObjectPtr branchPtr( branchCommit );
873 git_object* baseCommit = nullptr;
874
875 if( git_reference_peel( &baseCommit, aBase, GIT_OBJECT_COMMIT ) != GIT_OK )
876 return std::nullopt;
877
878 KIGIT::GitObjectPtr basePtr( baseCommit );
879 KIGIT_AHEAD_BEHIND counts;
880
881 int result = git_graph_ahead_behind( &counts.ahead, &counts.behind, m_repo, git_object_id( branchCommit ),
882 git_object_id( baseCommit ) );
883
884 if( result != GIT_OK )
885 return std::nullopt;
886
887 return counts;
888}
889
890
892{
893 if( !m_repo || IsHeadDetached() )
894 return false;
895
896 git_reference* head = nullptr;
897
898 if( git_repository_head( &head, m_repo ) != GIT_OK )
899 return false;
900
901 KIGIT::GitReferencePtr headPtr( head );
902 git_reference* upstream = nullptr;
903 int result = git_branch_upstream( &upstream, head );
904
905 if( result != GIT_OK )
906 {
907 // Treat a branch without an upstream as unpublished.
908 return result == GIT_ENOTFOUND;
909 }
910
911 KIGIT::GitReferencePtr upstreamPtr( upstream );
912 std::optional<KIGIT_AHEAD_BEHIND> counts = compareRefs( head, upstream );
913
914 return counts && counts->ahead > 0;
915}
916
917
919{
920 return m_repo && git_repository_head_detached( m_repo ) == 1;
921}
922
923
925{
926 if( !m_repo )
927 return false;
928
929 git_config* config = nullptr;
930
931 if( git_repository_config( &config, m_repo ) != GIT_OK )
932 return false;
933
934 KIGIT::GitConfigPtr configPtr( config );
935 int rebase = 0;
936
937 return git_config_get_bool( &rebase, config, "pull.rebase" ) == GIT_OK && rebase;
938}
939
940
942{
943 if( !m_repo )
944 return false;
945
946 git_reference* head = nullptr;
947
948 if( git_repository_head( &head, m_repo ) != GIT_OK )
949 return false;
950
951 KIGIT::GitReferencePtr headPtr( head );
952 git_reference* upstream = nullptr;
953
954 if( git_branch_upstream( &upstream, head ) != GIT_OK )
955 return false;
956
957 KIGIT::GitReferencePtr upstreamPtr( upstream );
958 std::optional<KIGIT_AHEAD_BEHIND> counts = compareRefs( head, upstream );
959
960 return counts && counts->behind > 0;
961}
962
963
964std::optional<size_t> KIGIT_COMMON::GetUnmergedCommitCount( const wxString& aBranch ) const
965{
966 if( !m_repo )
967 return std::nullopt;
968
969 git_reference* branch = nullptr;
970
971 if( git_branch_lookup( &branch, m_repo, aBranch.utf8_string().c_str(), GIT_BRANCH_LOCAL ) != GIT_OK )
972 return std::nullopt;
973
974 KIGIT::GitReferencePtr branchPtr( branch );
975 git_reference* head = nullptr;
976
977 if( git_repository_head( &head, m_repo ) != GIT_OK )
978 return std::nullopt;
979
980 KIGIT::GitReferencePtr headPtr( head );
981 std::optional<KIGIT_AHEAD_BEHIND> counts = compareRefs( branch, head );
982
983 if( !counts )
984 return std::nullopt;
985
986 return counts->ahead;
987}
988
989
990std::vector<wxString> KIGIT_COMMON::GetRecentCommitMessages( size_t aMax ) const
991{
992 std::vector<wxString> messages;
993
994 if( !m_repo )
995 return messages;
996
997 git_revwalk* walker = nullptr;
998
999 if( git_revwalk_new( &walker, m_repo ) != GIT_OK )
1000 return messages;
1001
1002 KIGIT::GitRevWalkPtr walkerPtr( walker );
1003 git_revwalk_sorting( walker, GIT_SORT_TIME | GIT_SORT_TOPOLOGICAL );
1004
1005 if( git_revwalk_push_head( walker ) != GIT_OK )
1006 return messages;
1007
1008 std::set<wxString> seen;
1009 git_oid oid;
1010
1011 while( messages.size() < aMax && git_revwalk_next( &oid, walker ) == GIT_OK )
1012 {
1013 git_commit* commit = nullptr;
1014
1015 if( git_commit_lookup( &commit, m_repo, &oid ) != GIT_OK )
1016 continue;
1017
1018 KIGIT::GitCommitPtr commitPtr( commit );
1019 wxString message;
1020
1021 if( const char* text = git_commit_message( commit ) )
1023
1024 message.Trim();
1025
1026 if( !message.IsEmpty() && seen.insert( message ).second )
1027 messages.push_back( message );
1028 }
1029
1030 return messages;
1031}
1032
1033
1034namespace
1035{
1036
1037// The id of the tree entry at aPath in aCommit's tree, or empty when there is none.
1038std::string treeEntryIdAt( git_commit* aCommit, const char* aPath )
1039{
1040 git_tree* tree = nullptr;
1041
1042 if( git_commit_tree( &tree, aCommit ) != GIT_OK )
1043 return {};
1044
1045 KIGIT::GitTreePtr treePtr( tree );
1046 git_tree_entry* entry = nullptr;
1047
1048 if( git_tree_entry_bypath( &entry, tree, aPath ) != GIT_OK )
1049 return {};
1050
1051 KIGIT::GitTreeEntryPtr entryPtr( entry );
1052
1053 return git_oid_tostr_s( git_tree_entry_id( entry ) );
1054}
1055
1056
1057// True when aCommit changed what sits at aPath, judged against its first parent. A folder works
1058// like a file, since a folder's entry id changes with anything under it.
1059bool commitTouchesPath( git_commit* aCommit, const std::string& aPath )
1060{
1061 std::string after = treeEntryIdAt( aCommit, aPath.c_str() );
1062 std::string before;
1063
1064 if( git_commit_parentcount( aCommit ) > 0 )
1065 {
1066 git_commit* parent = nullptr;
1067
1068 if( git_commit_parent( &parent, aCommit, 0 ) == GIT_OK )
1069 {
1070 KIGIT::GitCommitPtr parentPtr( parent );
1071 before = treeEntryIdAt( parent, aPath.c_str() );
1072 }
1073 }
1074
1075 return after != before;
1076}
1077
1078} // namespace
1079
1080
1081std::vector<KIGIT_COMMIT_INFO> KIGIT_COMMON::GetRecentCommits( size_t aMax, const KIGIT_HISTORY_FILTER& aFilter ) const
1082{
1083 std::vector<KIGIT_COMMIT_INFO> commits;
1085
1086 if( !m_repo || IsShuttingDown() || ( cancel && cancel->load() ) )
1087 return commits;
1088
1089 git_revwalk* walker = nullptr;
1090
1091 if( git_revwalk_new( &walker, m_repo ) != GIT_OK )
1092 return commits;
1093
1094 KIGIT::GitRevWalkPtr walkerPtr( walker );
1095
1096 // Time order, but a child always before its parent, so two tips made in the same second
1097 // still list in a fixed order.
1098 git_revwalk_sorting( walker, GIT_SORT_TIME | GIT_SORT_TOPOLOGICAL );
1099
1100 if( aFilter.branch.IsEmpty() )
1101 {
1102 git_revwalk_push_head( walker );
1103 git_revwalk_push_glob( walker, "refs/heads/*" );
1104 git_revwalk_push_glob( walker, "refs/remotes/*" );
1105 }
1106 else
1107 {
1108 // The short name as the Branches page lists it, local or remote, resolved as git would.
1109 git_reference* ref = nullptr;
1110
1111 if( git_reference_dwim( &ref, m_repo, aFilter.branch.utf8_string().c_str() ) != GIT_OK )
1112 return commits;
1113
1114 KIGIT::GitReferencePtr refPtr( ref );
1115 git_object* target = nullptr;
1116
1117 if( git_reference_peel( &target, ref, GIT_OBJECT_COMMIT ) != GIT_OK )
1118 return commits;
1119
1120 KIGIT::GitObjectPtr targetPtr( target );
1121 git_revwalk_push( walker, git_object_id( target ) );
1122 }
1123
1124 std::map<std::string, std::vector<wxString>> refNames = collectRefNames( m_repo );
1125 wxString search = aFilter.search.Lower();
1126 std::string path = aFilter.path.utf8_string();
1127 git_oid oid;
1128
1129 while( commits.size() < aMax && git_revwalk_next( &oid, walker ) == GIT_OK )
1130 {
1131 // A search without matches can walk the entire repository. Let a newer query or a
1132 // closing project release the action lock before that walk reaches its end.
1133 if( ( cancel && cancel->load() ) || IsShuttingDown() )
1134 break;
1135
1136 git_commit* commit = nullptr;
1137
1138 if( git_commit_lookup( &commit, m_repo, &oid ) != GIT_OK )
1139 continue;
1140
1141 KIGIT::GitCommitPtr commitPtr( commit );
1142
1143 if( !path.empty() && !commitTouchesPath( commit, path ) )
1144 continue;
1145
1147 info.hash = shortHashOf( &oid );
1148
1149 if( const char* summary = git_commit_summary( commit ) )
1150 info.summary = KIGIT::DecodeRepoText( summary );
1151
1152 if( const git_signature* author = git_commit_author( commit ) )
1153 {
1154 info.author = KIGIT::DecodeRepoText( author->name );
1155 info.email = KIGIT::DecodeRepoText( author->email );
1156 }
1157
1158 info.when = wxDateTime( static_cast<time_t>( git_commit_time( commit ) ) );
1159
1160 std::string fullHash = git_oid_tostr_s( &oid );
1161
1162 auto matches = [&]( const wxString& aText )
1163 {
1164 return aText.Lower().Contains( search );
1165 };
1166
1167 if( !search.IsEmpty() && !matches( info.summary ) && !matches( info.author ) && !matches( info.email )
1168 && !matches( fullHash ) )
1169 {
1170 continue;
1171 }
1172
1173 auto named = refNames.find( fullHash );
1174
1175 if( named != refNames.end() )
1176 info.refs = named->second;
1177
1178 commits.push_back( info );
1179 }
1180
1181 return commits;
1182}
1183
1184
1186{
1188
1189 if( !m_repo )
1190 return detail;
1191
1192 git_object* obj = nullptr;
1193
1194 if( git_revparse_single( &obj, m_repo, aHash.utf8_string().c_str() ) != GIT_OK )
1195 return detail;
1196
1197 KIGIT::GitObjectPtr objPtr( obj );
1198
1199 if( git_object_type( obj ) != GIT_OBJECT_COMMIT )
1200 return detail;
1201
1202 git_commit* commit = reinterpret_cast<git_commit*>( obj );
1203 const git_oid* oid = git_commit_id( commit );
1204
1205 detail.found = true;
1206 detail.hash = wxString::FromUTF8( git_oid_tostr_s( oid ) );
1207 detail.shortHash = shortHashOf( oid );
1208
1209 if( const char* message = git_commit_message( commit ) )
1211
1212 detail.message.Trim();
1213
1214 if( const git_signature* author = git_commit_author( commit ) )
1215 {
1216 detail.author = KIGIT::DecodeRepoText( author->name );
1217 detail.email = KIGIT::DecodeRepoText( author->email );
1218 }
1219
1220 detail.when = wxDateTime( static_cast<time_t>( git_commit_time( commit ) ) );
1221
1222 for( unsigned ii = 0; ii < git_commit_parentcount( commit ); ++ii )
1223 detail.parents.push_back( shortHashOf( git_commit_parent_id( commit, ii ) ) );
1224
1225 std::map<std::string, std::vector<wxString>> refNames = collectRefNames( m_repo );
1226 auto named = refNames.find( git_oid_tostr_s( oid ) );
1227
1228 if( named != refNames.end() )
1229 detail.refs = named->second;
1230
1231 git_tree* tree = nullptr;
1232
1233 if( git_commit_tree( &tree, commit ) != GIT_OK )
1234 return detail;
1235
1236 KIGIT::GitTreePtr treePtr( tree );
1237
1238 // A root commit diffs against the empty tree, so its files all count as added.
1239 KIGIT::GitTreePtr parentTree;
1240
1241 if( git_commit_parentcount( commit ) > 0 )
1242 {
1243 git_commit* parent = nullptr;
1244
1245 if( git_commit_parent( &parent, commit, 0 ) == GIT_OK )
1246 {
1247 KIGIT::GitCommitPtr parentPtr( parent );
1248 git_tree* raw = nullptr;
1249
1250 if( git_commit_tree( &raw, parent ) == GIT_OK )
1251 parentTree.reset( raw );
1252 }
1253 }
1254
1255 git_diff* diff = nullptr;
1256
1257 if( git_diff_tree_to_tree( &diff, m_repo, parentTree.get(), tree, nullptr ) != GIT_OK )
1258 return detail;
1259
1260 KIGIT::GitDiffPtr diffPtr( diff );
1261
1262 git_diff_find_options findOpts;
1263 git_diff_find_options_init( &findOpts, GIT_DIFF_FIND_OPTIONS_VERSION );
1264 findOpts.flags = GIT_DIFF_FIND_RENAMES | GIT_DIFF_FIND_COPIES;
1265 git_diff_find_similar( diff, &findOpts );
1266
1267 for( size_t ii = 0; ii < git_diff_num_deltas( diff ); ++ii )
1268 {
1269 git_patch* patch = nullptr;
1270
1271 if( git_patch_from_diff( &patch, diff, ii ) != GIT_OK )
1272 continue;
1273
1274 KIGIT::GitPatchPtr patchPtr( patch );
1275 const git_diff_delta* delta = git_patch_get_delta( patch );
1276
1277 KIGIT_COMMIT_FILE file;
1278 file.path = wxString::FromUTF8( delta->new_file.path ? delta->new_file.path : delta->old_file.path );
1279 file.status = deltaStatus( delta->status );
1280 file.binary = ( delta->flags & GIT_DIFF_FLAG_BINARY ) != 0;
1281
1282 if( delta->status == GIT_DELTA_RENAMED || delta->status == GIT_DELTA_COPIED )
1283 file.oldPath = wxString::FromUTF8( delta->old_file.path );
1284
1285 size_t added = 0;
1286 size_t removed = 0;
1287
1288 if( git_patch_line_stats( nullptr, &added, &removed, patch ) == GIT_OK )
1289 {
1290 file.added = static_cast<int>( added );
1291 file.removed = static_cast<int>( removed );
1292 }
1293
1294 detail.added += file.added;
1295 detail.removed += file.removed;
1296 detail.files.push_back( file );
1297 }
1298
1299 return detail;
1300}
1301
1302
1303namespace
1304{
1305struct STASH_COLLECT
1306{
1307 git_repository* repo;
1308 std::vector<KIGIT_STASH_INFO>* out;
1309};
1310
1311
1312int collectStash( size_t aIndex, const char* aMessage, const git_oid* aStashId, void* aPayload )
1313{
1314 STASH_COLLECT* ctx = static_cast<STASH_COLLECT*>( aPayload );
1315
1317 info.index = aIndex;
1318 info.message = KIGIT::DecodeRepoText( aMessage );
1319
1320 char hash[GIT_OID_HEXSZ + 1] = { 0 };
1321 git_oid_tostr( hash, sizeof( hash ), aStashId );
1322 info.id = wxString::FromUTF8( hash );
1323
1324 git_commit* commit = nullptr;
1325
1326 if( git_commit_lookup( &commit, ctx->repo, aStashId ) == GIT_OK )
1327 {
1328 KIGIT::GitCommitPtr commitPtr( commit );
1329 info.when = wxDateTime( static_cast<time_t>( git_commit_time( commit ) ) );
1330 }
1331
1332 ctx->out->push_back( info );
1333 return 0;
1334}
1335} // namespace
1336
1337
1338std::vector<KIGIT_STASH_INFO> KIGIT_COMMON::GetStashes() const
1339{
1340 std::vector<KIGIT_STASH_INFO> stashes;
1341
1342 if( !m_repo )
1343 return stashes;
1344
1345 STASH_COLLECT ctx{ m_repo, &stashes };
1346 git_stash_foreach( m_repo, collectStash, &ctx );
1347
1348 return stashes;
1349}
1350
1351
1352std::vector<KIGIT_TAG_INFO> KIGIT_COMMON::GetTags() const
1353{
1354 std::vector<KIGIT_TAG_INFO> tags;
1355
1356 if( !m_repo )
1357 return tags;
1358
1359 git_strarray tagNames = { nullptr, 0 };
1360
1361 if( git_tag_list( &tagNames, m_repo ) != GIT_OK )
1362 return tags;
1363
1364 KIGIT::GitStrArrayPtr tagNamesPtr( &tagNames );
1365
1366 for( size_t i = 0; i < tagNames.count; ++i )
1367 {
1368 wxString tagName = KIGIT::DecodeRepoText( tagNames.strings[i] );
1371 info.name = tagName;
1372
1373 // A tag on a tree or blob has no commit to show, but it is still a tag.
1374 if( detail.found )
1375 {
1376 info.hash = detail.hash;
1377 info.summary = detail.message.BeforeFirst( '\n' );
1378 info.when = detail.when;
1379 }
1380
1381 tags.push_back( info );
1382 }
1383
1384 // Newest first, as the History page lists commits. A tag with no date goes last.
1385 std::stable_sort( tags.begin(), tags.end(),
1386 []( const KIGIT_TAG_INFO& a, const KIGIT_TAG_INFO& b )
1387 {
1388 if( !a.when.IsValid() || !b.when.IsValid() )
1389 return a.when.IsValid();
1390
1391 return a.when > b.when;
1392 } );
1393
1394 return tags;
1395}
1396
1397
1398std::vector<std::pair<wxString, wxString>> KIGIT_COMMON::GetRemotes() const
1399{
1400 std::vector<std::pair<wxString, wxString>> remotes;
1401
1402 if( !m_repo )
1403 return remotes;
1404
1405 git_strarray names = { nullptr, 0 };
1406
1407 if( git_remote_list( &names, m_repo ) != GIT_OK )
1408 return remotes;
1409
1410 KIGIT::GitStrArrayPtr namesPtr( &names );
1411
1412 for( size_t i = 0; i < names.count; ++i )
1413 {
1414 wxString name = wxString::FromUTF8( names.strings[i] );
1415 git_remote* remote = nullptr;
1416
1417 if( git_remote_lookup( &remote, m_repo, names.strings[i] ) != GIT_OK )
1418 continue;
1419
1420 KIGIT::GitRemotePtr remotePtr( remote );
1421
1422 const char* url = git_remote_url( remote );
1423 remotes.emplace_back( name, url ? KIGIT::StripEmbeddedUserinfo( wxString::FromUTF8( url ) ) : wxString() );
1424 }
1425
1426 return remotes;
1427}
1428
1429
1431{
1432 return m_repo && git_repository_head_unborn( m_repo ) == 1;
1433}
1434
1435
1436bool KIGIT_COMMON::StartUnbornBranch( const wxString& aName )
1437{
1438 // Pointing HEAD at a name does not check it the way creating a branch does.
1439 if( !m_repo || !IsHeadUnborn() || !KIGIT::BranchNameError( aName ).IsEmpty() )
1440 return false;
1441
1442 wxString ref = wxS( "refs/heads/" ) + aName;
1443
1444 return git_repository_set_head( m_repo, ref.utf8_string().c_str() ) == GIT_OK;
1445}
1446
1447
1448bool KIGIT_COMMON::CreateBranch( const wxString& aName, const wxString& aFrom )
1449{
1450 if( !m_repo || !KIGIT::BranchNameError( aName ).IsEmpty() )
1451 return false;
1452
1453 return GetGitBackend()->CreateBranch( m_repo, aName, aFrom ) == 0;
1454}
1455
1456
1457bool KIGIT_COMMON::DeleteBranch( const wxString& aName )
1458{
1459 return GetGitBackend()->DeleteBranch( m_repo, aName );
1460}
1461
1462
1463bool KIGIT_COMMON::StashSave( const wxString& aMessage )
1464{
1465 return GetGitBackend()->StashSave( m_repo, aMessage );
1466}
1467
1468
1469bool KIGIT_COMMON::StashApply( size_t aIndex )
1470{
1471 return GetGitBackend()->StashApply( m_repo, aIndex );
1472}
1473
1474
1475bool KIGIT_COMMON::StashPop( size_t aIndex )
1476{
1477 return GetGitBackend()->StashPop( m_repo, aIndex );
1478}
1479
1480
1481bool KIGIT_COMMON::StashDrop( size_t aIndex )
1482{
1483 return GetGitBackend()->StashDrop( m_repo, aIndex );
1484}
1485
1486
1487bool KIGIT_COMMON::CreateTag( const wxString& aName, const wxString& aMessage )
1488{
1489 return GetGitBackend()->CreateTag( m_repo, aName, aMessage );
1490}
1491
1492
1493KIGIT_TAG_DETAIL KIGIT_COMMON::GetTagDetail( const wxString& aName ) const
1494{
1496
1497 if( !m_repo )
1498 return detail;
1499
1500 git_object* obj = nullptr;
1501 wxString spec = wxS( "refs/tags/" ) + aName;
1502
1503 if( git_revparse_single( &obj, m_repo, spec.utf8_string().c_str() ) != GIT_OK )
1504 return detail;
1505
1506 KIGIT::GitObjectPtr objPtr( obj );
1507
1508 // An annotated tag is its own object on the way to the commit, with a message and a tagger
1509 // of its own.
1510 git_object* peeled = nullptr;
1511
1512 if( git_object_peel( &peeled, obj, GIT_OBJECT_COMMIT ) != GIT_OK )
1513 return detail;
1514
1515 KIGIT::GitObjectPtr peeledPtr( peeled );
1516 git_commit* commit = reinterpret_cast<git_commit*>( peeled );
1517
1518 detail.found = true;
1519 detail.name = aName;
1520 detail.hash = shortHashOf( git_commit_id( commit ) );
1521
1522 const char* message = nullptr;
1523 const git_signature* who = nullptr;
1524
1525 if( git_object_type( obj ) == GIT_OBJECT_TAG )
1526 {
1527 git_tag* tag = reinterpret_cast<git_tag*>( obj );
1528 message = git_tag_message( tag );
1529 who = git_tag_tagger( tag );
1530 }
1531
1532 if( !message )
1533 message = git_commit_message( commit );
1534
1535 bool annotated = who != nullptr;
1536
1537 if( !who )
1538 who = git_commit_author( commit );
1539
1540 if( message )
1542
1543 detail.message.Trim();
1544
1545 if( who )
1546 {
1547 detail.author = KIGIT::DecodeRepoText( who->name );
1548 detail.email = KIGIT::DecodeRepoText( who->email );
1549 detail.when = wxDateTime( static_cast<time_t>( annotated ? who->when.time : git_commit_time( commit ) ) );
1550 }
1551
1552 return detail;
1553}
1554
1555
1556bool KIGIT_COMMON::DeleteTag( const wxString& aName )
1557{
1558 return GetGitBackend()->DeleteTag( m_repo, aName );
1559}
1560
1561
1562bool KIGIT_COMMON::CheckoutTag( const wxString& aName )
1563{
1564 return GetGitBackend()->CheckoutTag( m_repo, aName );
1565}
1566
1567
1568bool KIGIT_COMMON::AddRemote( const wxString& aName, const wxString& aUrl )
1569{
1570 return GetGitBackend()->AddRemote( m_repo, aName, aUrl );
1571}
1572
1573
1574bool KIGIT_COMMON::SetRemoteUrl( const wxString& aName, const wxString& aUrl )
1575{
1576 return GetGitBackend()->SetRemoteUrl( m_repo, aName, aUrl );
1577}
1578
1579
1580bool KIGIT_COMMON::RemoveRemote( const wxString& aName )
1581{
1582 return GetGitBackend()->RemoveRemote( m_repo, aName );
1583}
1584
1585
1586bool KIGIT_COMMON::SetUserConfig( const wxString& aName, const wxString& aEmail )
1587{
1588 return GetGitBackend()->SetUserConfig( m_repo, aName, aEmail );
1589}
1590
1591
1593{
1594 return GetGitBackend()->RestoreVersion( *this, aCommitHash, aError );
1595}
1596
1597
1598bool KIGIT_COMMON::DiscardChanges( const std::vector<wxString>& aPaths, wxString& aError )
1599{
1600 return GetGitBackend()->DiscardChanges( m_repo, aPaths, aError );
1601}
1602
1603
1604bool KIGIT_COMMON::IsPathInHead( const wxString& aPath )
1605{
1606 return GetGitBackend()->IsPathInHead( m_repo, aPath );
1607}
1608
1609
1610bool KIGIT_COMMON::IsPathConflicted( const wxString& aPath ) const
1611{
1612 if( !m_repo || aPath.IsEmpty() )
1613 return false;
1614
1615 git_index* index = nullptr;
1616
1617 if( git_repository_index( &index, m_repo ) != GIT_OK )
1618 return false;
1619
1620 KIGIT::GitIndexPtr indexPtr( index );
1621
1622 const git_index_entry* ancestor = nullptr;
1623 const git_index_entry* ours = nullptr;
1624 const git_index_entry* theirs = nullptr;
1625
1626 return git_index_conflict_get( &ancestor, &ours, &theirs, index, aPath.utf8_string().c_str() ) == GIT_OK;
1627}
1628
1629
1631{
1632 return m_repo && git_repository_state( m_repo ) != GIT_REPOSITORY_STATE_NONE;
1633}
1634
1635
1637{
1638 if( !m_repo )
1639 return GitOperation::None;
1640
1641 switch( git_repository_state( m_repo ) )
1642 {
1643 case GIT_REPOSITORY_STATE_NONE: return GitOperation::None;
1644 case GIT_REPOSITORY_STATE_MERGE: return GitOperation::Merge;
1645 case GIT_REPOSITORY_STATE_REBASE:
1646 case GIT_REPOSITORY_STATE_REBASE_INTERACTIVE:
1647 case GIT_REPOSITORY_STATE_REBASE_MERGE:
1648 case GIT_REPOSITORY_STATE_APPLY_MAILBOX_OR_REBASE: return GitOperation::Rebase;
1649 case GIT_REPOSITORY_STATE_CHERRYPICK:
1650 case GIT_REPOSITORY_STATE_CHERRYPICK_SEQUENCE: return GitOperation::CherryPick;
1651 case GIT_REPOSITORY_STATE_REVERT:
1652 case GIT_REPOSITORY_STATE_REVERT_SEQUENCE: return GitOperation::Revert;
1653 default: return GitOperation::Other;
1654 }
1655}
1656
1657
1658bool KIGIT_COMMON::AbandonOperation( wxString& aError )
1659{
1660 return GetGitBackend()->AbandonOperation( m_repo, aError );
1661}
1662
1663
1664bool KIGIT_COMMON::GetMergeConflicts( const wxString& aStageDir, wxString& aHeadOid, wxString& aTheirsOid,
1665 std::vector<KIGIT_CONFLICT_SIDES>& aConflicts, wxString& aError )
1666{
1667 return GetGitBackend()->GetMergeConflicts( m_repo, aStageDir, aHeadOid, aTheirsOid, aConflicts, aError );
1668}
1669
1670
1671MergeCommitResult KIGIT_COMMON::CreateMergedCommit( const wxString& aExpectedHeadOid, const wxString& aTheirsOid,
1672 const std::vector<std::pair<wxString, wxString>>& aResolvedFiles,
1673 KIGIT_ERROR_MESSAGE& aError )
1674{
1675 return GetGitBackend()->CreateMergedCommit( m_repo, aExpectedHeadOid, aTheirsOid, aResolvedFiles, aError );
1676}
1677
1678
1679void KIGIT_COMMON::AddRefreshListener( void* aOwner, std::function<void()> aCallback )
1680{
1681 m_refreshListeners.emplace_back( aOwner, std::move( aCallback ) );
1682}
1683
1684
1686{
1687 std::erase_if( m_refreshListeners,
1688 [aOwner]( const auto& aEntry )
1689 {
1690 return aEntry.first == aOwner;
1691 } );
1692}
1693
1694
1696{
1697 // Copy first: a listener could add or remove one while it runs.
1698 std::vector<std::pair<void*, std::function<void()>>> listeners = m_refreshListeners;
1699
1700 for( const auto& [owner, callback] : listeners )
1701 {
1702 // An earlier listener in this round can have removed this one.
1703 bool registered = std::any_of( m_refreshListeners.begin(), m_refreshListeners.end(),
1704 [target = owner]( const auto& aEntry )
1705 {
1706 return aEntry.first == target;
1707 } );
1708
1709 if( registered )
1710 callback();
1711 }
1712}
1713
1714
1716{
1717 if( !m_repo )
1718 return false;
1719
1720
1721 // Prefer the upstream remote configured for the current branch. Fall back to
1722 // "origin" and then to any remote that exposes a fetch URL so that repos
1723 // cloned with non-default remote names also enable push/pull operations.
1724 auto checkRemote =
1725 [this]( const char* aName ) -> bool
1726 {
1727 git_remote* remote = nullptr;
1728
1729 if( git_remote_lookup( &remote, m_repo, aName ) != GIT_OK )
1730 return false;
1731
1732 KIGIT::GitRemotePtr remotePtr( remote );
1733
1734 const char* fetch_url = git_remote_url( remote );
1735 const char* push_url = git_remote_pushurl( remote );
1736
1737 // libgit2 defaults to the fetch URL for pushing when no push URL is set
1738 if( !push_url )
1739 push_url = fetch_url;
1740
1741 return fetch_url && push_url;
1742 };
1743
1744 std::string preferred = GetRemoteNameOrDefault().utf8_string();
1745
1746 if( checkRemote( preferred.c_str() ) )
1747 return true;
1748
1749 if( preferred != "origin" && checkRemote( "origin" ) )
1750 return true;
1751
1752 git_strarray remotes = { nullptr, 0 };
1753
1754 if( git_remote_list( &remotes, m_repo ) != GIT_OK )
1755 {
1756 wxLogTrace( traceGit, "Failed to enumerate remotes for haspushpull" );
1757 return false;
1758 }
1759
1760 KIGIT::GitStrArrayPtr remotesPtr( &remotes );
1761
1762 for( size_t ii = 0; ii < remotes.count; ++ii )
1763 {
1764 if( checkRemote( remotes.strings[ii] ) )
1765 return true;
1766 }
1767
1768 return false;
1769}
1770
1771
1773{
1774 wxString remoteName = GetRemotename();
1775
1776 if( remoteName.IsEmpty() )
1777 remoteName = wxS( "origin" );
1778
1779 return remoteName;
1780}
1781
1782
1784{
1785 wxString retval;
1786 git_strarray remotes = { nullptr, 0 };
1787
1788 if( git_remote_list( &remotes, m_repo ) == GIT_OK )
1789 {
1790 // No upstream tracking branch. Prefer "origin" when present, otherwise pick the
1791 // single configured remote. Returning empty for ambiguous (multiple) remotes
1792 // tells callers to fall back to whatever default they want.
1793 if( remotes.count == 1 )
1794 {
1795 retval = remotes.strings[0];
1796 }
1797 else
1798 {
1799 for( size_t ii = 0; ii < remotes.count; ++ii )
1800 {
1801 if( strcmp( remotes.strings[ii], "origin" ) == 0 )
1802 {
1803 retval = remotes.strings[ii];
1804 break;
1805 }
1806 }
1807 }
1808
1809 git_strarray_dispose( &remotes );
1810 }
1811 else
1812 {
1813 wxLogTrace( traceGit, "Failed to list remotes: %s", KIGIT_COMMON::GetLastGitError() );
1814
1815 // If we can't get the remote name from the upstream branch or the list of remotes,
1816 // just return the default remote name
1817
1818 git_remote* remote = nullptr;
1819
1820 if( git_remote_lookup( &remote, m_repo, "origin" ) == GIT_OK )
1821 {
1822 retval = git_remote_name( remote );
1823 git_remote_free( remote );
1824 }
1825 else
1826 {
1827 wxLogTrace( traceGit, "Failed to get remote name from default remote: %s",
1829 }
1830 }
1831
1832 return retval;
1833}
1834
1835
1837{
1838 if( !m_repo )
1839 return wxEmptyString;
1840
1841
1842 wxString retval;
1843 git_reference* head = nullptr;
1844 git_reference* upstream = nullptr;
1845
1846 // An unborn HEAD has no branch to track, but the remote still exists
1847 if( git_repository_head( &head, m_repo ) != GIT_OK )
1848 {
1849 wxLogTrace( traceGit, "Failed to get remote name: %s", KIGIT_COMMON::GetLastGitError() );
1851 }
1852
1853 KIGIT::GitReferencePtr headPtr( head );
1854
1855 if( git_branch_upstream( &upstream, head ) != GIT_OK )
1856 {
1857 wxLogTrace( traceGit, "Failed to get upstream branch: %s", KIGIT_COMMON::GetLastGitError() );
1859 }
1860
1861 KIGIT::GitReferencePtr upstreamPtr( upstream );
1862 git_buf remote_name = GIT_BUF_INIT_CONST( nullptr, 0 );
1863
1864 if( git_branch_remote_name( &remote_name, m_repo, git_reference_name( upstream ) ) == GIT_OK )
1865 {
1866 retval = remote_name.ptr;
1867 git_buf_dispose( &remote_name );
1868 }
1869 else
1870 {
1871 wxLogTrace( traceGit,
1872 "Failed to get remote name from upstream branch: %s",
1874 }
1875
1876 return retval;
1877}
1878
1879
1880void KIGIT_COMMON::SetSSHKey( const wxString& aKey )
1881{
1882 // GetNextPublicKey reads an empty result as the end of the list, so an empty key at the
1883 // front would hide every key behind it.
1884 if( aKey.IsEmpty() )
1885 return;
1886
1887 std::lock_guard<std::mutex> lock( m_credentialsMutex );
1888 m_projectSSHKey = aKey;
1890}
1891
1892
1894{
1895 std::lock_guard<std::mutex> lock( m_credentialsMutex );
1896
1897 if( !m_projectSSHKey.IsEmpty() )
1898 return m_projectSSHKey;
1899
1900 return m_publicKeys.empty() ? wxString() : m_publicKeys.front();
1901}
1902
1903
1905{
1906 std::lock_guard<std::mutex> lock( m_credentialsMutex );
1907
1909 return m_projectSSHKey;
1910
1911 for( const wxString& key : m_publicKeys )
1912 {
1913 if( KIGIT::IsOfferableSSHKey( key ) )
1914 return key;
1915 }
1916
1917 return wxEmptyString;
1918}
1919
1920
1922{
1923 std::lock_guard<std::mutex> lock( m_credentialsMutex );
1924 return { m_remote, m_connType, m_username };
1925}
1926
1927
1929{
1930 std::lock_guard<std::mutex> lock( m_credentialsMutex );
1931
1932 if( m_remote != aFor.remote || m_connType != aFor.connType || m_username != aFor.username )
1933 return false;
1934
1935 m_connType = aEntered.connType;
1936
1937 if( aEntered.username )
1938 m_username = *aEntered.username;
1939
1940 m_password = aEntered.password;
1941 m_secretFetched = true;
1942
1943 // As SetSSHKey, which also refuses an empty key
1944 if( !aEntered.sshKey.IsEmpty() )
1945 {
1946 m_projectSSHKey = aEntered.sshKey;
1948 }
1949
1950 m_nextPublicKey = 0;
1951 m_testedTypes = 0;
1952 return true;
1953}
1954
1955
1956int KIGIT_COMMON::SSHPortFor( const wxString& aHost ) const
1957{
1958 std::lock_guard<std::mutex> lock( m_credentialsMutex );
1959
1960 if( m_port < 0 || !m_hostname.IsSameAs( aHost, false ) )
1961 return -1;
1962
1963 return m_port == 0 ? 22 : m_port;
1964}
1965
1966
1967std::unique_lock<std::mutex> KIGIT_COMMON::TryLockActionFor( std::chrono::milliseconds aWait )
1968{
1969 const auto deadline = std::chrono::steady_clock::now() + aWait;
1970 std::unique_lock<std::mutex> lock( m_gitActionMutex, std::defer_lock );
1971
1972 // Not a timed mutex, so poll. A status read lets go within a few of these
1973 while( !lock.try_lock() && std::chrono::steady_clock::now() < deadline )
1974 std::this_thread::sleep_for( std::chrono::milliseconds( 20 ) );
1975
1976 return lock;
1977}
1978
1979
1981{
1982 m_publicKeys.clear();
1983 m_nextPublicKey = 0;
1984
1986 m_publicKeys.push_back( m_projectSSHKey );
1987 else if( !m_projectSSHKey.IsEmpty() )
1988 wxLogTrace( traceGit, "Not offering project SSH key %s, it or its .pub is missing", m_projectSSHKey );
1989
1990 for( const wxString& key : m_discoveredKeys )
1991 {
1993 m_publicKeys.push_back( key );
1994 }
1995}
1996
1997
1999{
2000 std::lock_guard<std::mutex> lock( m_credentialsMutex );
2001
2002 // A key deleted since discovery would end the whole attempt instead of failing over to the next one
2003 while( m_nextPublicKey < static_cast<int>( m_publicKeys.size() ) )
2004 {
2005 wxString key = m_publicKeys[m_nextPublicKey++];
2006
2007 if( KIGIT::IsOfferableSSHKey( key ) )
2008 return key;
2009 }
2010
2011 return wxEmptyString;
2012}
2013
2014
2016{
2017 if( !m_repo )
2018 return wxEmptyString;
2019
2020 // Prefer the working directory (the user-visible project root). Fall back to
2021 // git_repository_path (the .git directory) only for bare repositories.
2022 if( const char* workdir = git_repository_workdir( m_repo ) )
2023 return wxString::FromUTF8( workdir );
2024
2025 if( const char* path = git_repository_path( m_repo ) )
2026 return wxString::FromUTF8( path );
2027
2028 return wxEmptyString;
2029}
2030
2031
2033{
2034 // Deadlocks if the credentials lock is already held. SyncRemoteUrl releases it
2035 // first for that reason.
2036 std::lock_guard<std::mutex> lock( m_credentialsMutex );
2037
2041}
2042
2043
2045{
2046 if( !m_repo )
2047 return;
2048
2049
2050 // We want to get the current branch's upstream url as well as the stored password
2051 // if one exists given the url and username.
2052
2053 wxString remote_name = GetRemotename();
2054 wxString url;
2055 git_remote* remote = nullptr;
2056
2057 if( git_remote_lookup( &remote, m_repo, remote_name.ToStdString().c_str() ) == GIT_OK )
2058 {
2059 // UTF-8 as the transfers read it, or a non-ASCII path would look changed on every fetch
2060 if( const char* remoteUrl = git_remote_url( remote ) )
2061 url = wxString::FromUTF8( remoteUrl );
2062
2063 git_remote_free( remote );
2064 }
2065
2066 SyncRemoteUrl( url );
2067}
2068
2069
2070void KIGIT_COMMON::SyncRemoteUrl( const wxString& aRawUrl )
2071{
2072 REMOTE_URL parsed = ParseRemoteURL( aRawUrl );
2073 bool keysStale = false;
2074
2075 {
2076 // The authentication callback reads these on a worker thread, so update them together
2077 std::lock_guard<std::mutex> lock( m_credentialsMutex );
2078
2079 // HTTPS drops the username from the stored URL, so it is compared on its own
2080 if( parsed.url != m_remote || parsed.username != m_urlUsername )
2081 {
2082 m_remote = parsed.url;
2083 m_urlUsername = parsed.username;
2084 m_connType = parsed.connType;
2085 m_hostname = parsed.hostname;
2086 m_port = parsed.port;
2087 m_username = parsed.username;
2088 m_password = parsed.password;
2089 m_secretFetched = !m_password.IsEmpty();
2090 }
2091
2092 // SetRemote can move the host without gathering keys, so compare against the gathered list
2093 keysStale = m_hostname != m_publicKeysHost;
2094 }
2095
2096 if( keysStale )
2098}
2099
2100
2101void KIGIT_COMMON::FollowRewrittenUrl( const wxString& aRawUrl )
2102{
2103 wxString username;
2104 wxString password;
2105 bool secretFetched;
2106
2107 {
2108 std::lock_guard<std::mutex> lock( m_credentialsMutex );
2109 username = m_username;
2110 password = m_password;
2111 secretFetched = m_secretFetched;
2112 }
2113
2114 SyncRemoteUrl( aRawUrl );
2115
2116 std::lock_guard<std::mutex> lock( m_credentialsMutex );
2117 m_username = username;
2118 m_password = password;
2119 m_secretFetched = secretFetched;
2120}
2121
2122
2124{
2125 std::lock_guard<std::mutex> lock( m_credentialsMutex );
2126 return m_connType;
2127}
2128
2129
2131{
2132 std::lock_guard<std::mutex> lock( m_credentialsMutex );
2133 m_connType = aConnType;
2134}
2135
2136
2137// Index of the '@' ending the userinfo of proto://user:password@host/path, or npos. The last
2138// one, since the first would split inside a password that contains an '@'.
2139static size_t findUserinfoAt( const wxString& aUrl )
2140{
2141 size_t protoSep = aUrl.find( "://" );
2142
2143 if( protoSep == wxString::npos )
2144 return wxString::npos;
2145
2146 size_t hostStart = protoSep + 3;
2147 size_t pathStart = aUrl.find_first_of( "/?#", hostStart );
2148 size_t searchEnd = ( pathStart == wxString::npos ) ? aUrl.length() : pathStart;
2149
2150 if( searchEnd <= hostStart )
2151 return wxString::npos;
2152
2153 size_t at = aUrl.rfind( '@', searchEnd - 1 );
2154
2155 return ( at != wxString::npos && at >= hostStart ) ? at : wxString::npos;
2156}
2157
2158
2160{
2161 const git_error* error = git_error_last();
2162
2163 if( error == nullptr )
2164 return wxString( "No error" );
2165
2166 // libgit2 quotes config lines and URLs verbatim, passwords included
2167 return KIGIT::StripEmbeddedUserinfo( wxString::FromUTF8( error->message ? error->message : "" ) );
2168}
2169
2170
2172{
2174 info.code = aCode;
2175
2176 // libgit2 before 1.8 answers null rather than "no error"
2177 if( const git_error* error = git_error_last() )
2178 {
2179 info.klass = error->klass;
2180 info.message = wxString::FromUTF8( error->message ? error->message : "" );
2181 }
2182
2183 return info;
2184}
2185
2186
2187// libgit2's git_str_decode_percent when the whole result is UTF-8; otherwise the component stays as written
2188static wxString decodePercent( const wxString& aText )
2189{
2190 const std::string text = aText.utf8_string();
2191 std::string decoded;
2192
2193 for( size_t ii = 0; ii < text.size(); ++ii )
2194 {
2195 if( text[ii] == '%' && ii + 2 < text.size() && isxdigit( (unsigned char) text[ii + 1] )
2196 && isxdigit( (unsigned char) text[ii + 2] ) )
2197 {
2198 decoded += static_cast<char>( std::stoi( text.substr( ii + 1, 2 ), nullptr, 16 ) );
2199 ii += 2;
2200 }
2201 else
2202 {
2203 decoded += text[ii];
2204 }
2205 }
2206
2207 wxString result = wxString::FromUTF8( decoded );
2208
2209 // wxString cannot hold bytes that are not UTF-8, so such text stays as written
2210 return result.IsEmpty() && !decoded.empty() ? aText : result;
2211}
2212
2213
2214// 0 when the address names no port, -1 when it names one that cannot be used
2215static int parsePort( const wxString& aPort )
2216{
2217 long port = 0;
2218
2219 if( aPort.IsEmpty() )
2220 return 0;
2221
2222 if( !aPort.ToLong( &port ) || port <= 0 || port > 65535 )
2223 return -1;
2224
2225 return static_cast<int>( port );
2226}
2227
2228
2229static bool isScpIpv6( const wxString& aText, size_t aBracket )
2230{
2231 size_t colons = 0;
2232
2233 for( size_t ii = aBracket + 1; ii < aText.length(); ++ii )
2234 {
2235 wxUniChar c = aText[ii];
2236
2237 if( c == ':' )
2238 colons++;
2239
2240 if( c == ']' )
2241 return colons > 1;
2242
2243 if( c != ':' && !wxIsxdigit( c ) )
2244 return false;
2245 }
2246
2247 return false;
2248}
2249
2250
2251static bool scpHasUser( const wxString& aText, size_t aStart )
2252{
2253 for( size_t ii = aStart; ii < aText.length(); ++ii )
2254 {
2255 if( aText[ii] == '@' )
2256 return true;
2257
2258 if( aText[ii] == ':' )
2259 return false;
2260 }
2261
2262 return false;
2263}
2264
2265
2266// libgit2's git_net_url_parse_scp, state for state, so the host key check binds the host and port it dials
2267static bool parseScpAddress( const wxString& aURL, wxString& aUser, wxString& aHost, wxString& aPort )
2268{
2269 enum
2270 {
2271 NONE,
2272 USER,
2273 HOST_START,
2274 HOST,
2275 HOST_END,
2276 IPV6,
2277 IPV6_END,
2278 PORT_START,
2279 PORT,
2280 PORT_END,
2281 PATH_START
2282 } state = NONE;
2283
2284 size_t user = 0, userLen = 0, host = 0, hostLen = 0, port = 0, portLen = 0;
2285 int bracket = 0;
2286 bool havePath = false;
2287
2288 for( size_t ii = 0; ii < aURL.length() && !havePath; ++ii )
2289 {
2290 wxUniChar c = aURL[ii];
2291
2292 switch( state )
2293 {
2294 case NONE:
2295 if( c == '@' || c == ':' )
2296 return false;
2297
2298 if( c == '[' )
2299 {
2300 if( isScpIpv6( aURL, ii ) )
2301 {
2302 state = IPV6;
2303 host = ii;
2304 }
2305 else if( bracket++ > 1 )
2306 {
2307 return false;
2308 }
2309 }
2310 else if( scpHasUser( aURL, ii ) )
2311 {
2312 state = USER;
2313 user = ii;
2314 }
2315 else
2316 {
2317 state = HOST;
2318 host = ii;
2319 }
2320
2321 break;
2322
2323 case USER:
2324 if( c == '@' )
2325 {
2326 userLen = ii - user;
2327 state = HOST_START;
2328 }
2329
2330 break;
2331
2332 case HOST_START:
2333 state = c == '[' ? IPV6 : HOST;
2334 host = ii;
2335 break;
2336
2337 case HOST:
2338 if( c == ':' )
2339 {
2340 hostLen = ii - host;
2341 state = bracket ? PORT_START : PATH_START;
2342 }
2343 else if( c == ']' )
2344 {
2345 if( bracket-- == 0 )
2346 return false;
2347
2348 hostLen = ii - host;
2349 state = HOST_END;
2350 }
2351
2352 break;
2353
2354 case HOST_END:
2355 if( c != ':' )
2356 return false;
2357
2358 state = PATH_START;
2359 break;
2360
2361 case IPV6:
2362 if( c == ']' )
2363 state = IPV6_END;
2364
2365 break;
2366
2367 case IPV6_END:
2368 if( c != ':' )
2369 return false;
2370
2371 hostLen = ii - host;
2372 state = bracket ? PORT_START : PATH_START;
2373 break;
2374
2375 case PORT_START:
2376 port = ii;
2377 state = PORT;
2378 break;
2379
2380 case PORT:
2381 if( c == ']' )
2382 {
2383 if( bracket-- == 0 )
2384 return false;
2385
2386 portLen = ii - port;
2387 state = PORT_END;
2388 }
2389
2390 break;
2391
2392 case PORT_END:
2393 if( c != ':' )
2394 return false;
2395
2396 state = PATH_START;
2397 break;
2398
2399 case PATH_START: havePath = true; break;
2400 }
2401 }
2402
2403 if( !havePath || hostLen == 0 )
2404 return false;
2405
2406 aUser = aURL.Mid( user, userLen );
2407 aHost = aURL.Mid( host, hostLen );
2408 aPort = aURL.Mid( port, portLen );
2409
2410 // libgit2 keeps an IPv6 host in its brackets here, but hands it out without them for URLs
2411 if( aHost.StartsWith( "[" ) && aHost.EndsWith( "]" ) )
2412 aHost = aHost.Mid( 1, aHost.length() - 2 );
2413
2414 return true;
2415}
2416
2417
2419{
2421 result.url = aURL;
2422
2423 size_t schemeEnd = aURL.find( "://" );
2424 size_t hostStart = 0;
2425 size_t hostEnd;
2426
2427 if( schemeEnd != wxString::npos )
2428 {
2429 wxString scheme = aURL.Left( schemeEnd ).Lower();
2430
2431 if( scheme == "https" || scheme == "http" )
2433 else if( scheme == "ssh" || scheme == "ssh+git" || scheme == "git+ssh" )
2435 else
2436 return result;
2437
2438 hostStart = schemeEnd + 3;
2439 hostEnd = aURL.find_first_of( "/?#", hostStart );
2440 size_t at = findUserinfoAt( aURL );
2441
2442 if( at != wxString::npos )
2443 {
2444 wxString credentials = aURL.Mid( hostStart, at - hostStart );
2445 size_t colon = credentials.find( ':' );
2446 wxString user = credentials.Left( colon );
2447 result.username = decodePercent( user );
2448
2449 if( colon != wxString::npos )
2450 result.password = decodePercent( credentials.Mid( colon + 1 ) );
2451
2452 wxString userPrefix;
2453
2454 if( result.connType == GIT_CONN_TYPE::GIT_CONN_SSH && !user.IsEmpty() )
2455 userPrefix = user + "@";
2456
2457 result.url = aURL.Left( hostStart ) + userPrefix + aURL.Mid( at + 1 );
2458 hostStart = at + 1;
2459 }
2460 }
2461 else
2462 {
2463 // A colon before the path marks an SCP address, except for Windows drive paths.
2464 if( aURL.length() >= 2 && wxIsalpha( aURL[0] ) && aURL[1] == ':' )
2465 return result;
2466
2467 hostEnd = aURL.find( ':' );
2468 size_t bracket = aURL.find( '[' );
2469
2470 if( bracket != wxString::npos && bracket < hostEnd )
2471 {
2472 size_t endBracket = aURL.find( ']', bracket );
2473
2474 if( endBracket != wxString::npos )
2475 hostEnd = aURL.find( ':', endBracket + 1 );
2476 }
2477
2478 size_t slash = aURL.find_first_of( "/\\" );
2479
2480 if( hostEnd == wxString::npos || hostEnd == 0 || ( slash != wxString::npos && slash < hostEnd ) )
2481 {
2482 return result;
2483 }
2484
2486 wxString port;
2487
2488 // An address libgit2 cannot read leaves no host, which the host key check refuses
2489 if( parseScpAddress( aURL, result.username, result.hostname, port ) )
2490 result.port = parsePort( port );
2491
2492 return result;
2493 }
2494
2495 wxString host = aURL.Mid( hostStart, hostEnd == wxString::npos ? wxString::npos : hostEnd - hostStart );
2496 wxString port;
2497
2498 if( host.StartsWith( "[" ) )
2499 {
2500 result.hostname = host.AfterFirst( '[' ).BeforeFirst( ']' );
2501 port = host.AfterFirst( ']' ).AfterFirst( ':' );
2502 }
2503 else
2504 {
2505 result.hostname = host.BeforeFirst( ':' );
2506 port = host.AfterFirst( ':' );
2507 }
2508
2509 // libgit2 connects to the decoded name, so %67he.example is ghe.example
2510 result.hostname = decodePercent( result.hostname );
2511 result.port = parsePort( port );
2512
2513 return result;
2514}
2515
2516
2518{
2519 std::lock_guard<std::mutex> lock( m_credentialsMutex );
2521}
2522
2523
2525{
2527 m_connType = parsed.connType;
2528 m_remote = parsed.url;
2529 m_urlUsername = parsed.username;
2530 m_hostname = parsed.hostname;
2531 m_port = parsed.port;
2532
2533 if( !parsed.username.IsEmpty() )
2534 m_username = parsed.username;
2535
2536 if( !parsed.password.IsEmpty() )
2537 m_password = parsed.password;
2538
2539 m_secretFetched = !m_password.IsEmpty();
2540}
2541
2542
2543int KIGIT_COMMON::HandleSSHKeyAuthentication( git_cred** aOut, const wxString& aUsername, KIGIT_AUTH_ATTEMPT& aAttempt )
2544{
2546 {
2547 if( HandleSSHAgentAuthentication( aOut, aUsername ) == GIT_OK )
2548 {
2549 aAttempt.agentTried = true;
2550 return GIT_OK;
2551 }
2552
2553 // Agent unavailable or has no matching key; fall through to configured key.
2554 }
2555
2556 // SSH key authentication with password
2557 wxString sshKey = GetNextPublicKey();
2558
2559 if( sshKey.IsEmpty() )
2560 {
2561 wxLogTrace( traceGit, "Finished testing all possible ssh keys" );
2562 m_testedTypes |= GIT_CREDENTIAL_SSH_KEY;
2563 return GIT_PASSTHROUGH;
2564 }
2565
2566 wxString sshPubKey = sshKey + ".pub";
2567 wxString password = GetPassword();
2568
2569 wxLogTrace( traceGit, "Testing %s\n", sshKey );
2570
2571 if( git_credential_ssh_key_new( aOut, aUsername.mbc_str(), sshPubKey.mbc_str(), sshKey.mbc_str(),
2572 password.mbc_str() ) != GIT_OK )
2573 {
2574 wxLogTrace( traceGit, "Failed to create SSH key credential for %s: %s", sshKey,
2576 return GIT_PASSTHROUGH;
2577 }
2578
2579 // libssh2 reads the file only once authentication starts, so a load failure names this key
2580 aAttempt.keysTried.push_back( sshKey );
2581 return GIT_OK;
2582}
2583
2584
2585int KIGIT_COMMON::HandlePlaintextAuthentication( git_cred** aOut, const wxString& aUsername )
2586{
2587 wxString password = GetPassword();
2588
2589 int rc = git_credential_userpass_plaintext_new( aOut, aUsername.mbc_str(), password.mbc_str() );
2590 m_testedTypes |= GIT_CREDENTIAL_USERPASS_PLAINTEXT;
2591
2592 return rc;
2593}
2594
2595
2596int KIGIT_COMMON::HandleSSHAgentAuthentication( git_cred** aOut, const wxString& aUsername )
2597{
2599
2600 if( git_credential_ssh_key_from_agent( aOut, aUsername.mbc_str() ) != GIT_OK )
2601 {
2602 wxLogTrace( traceGit, "Failed to create SSH agent credential: %s", KIGIT_COMMON::GetLastGitError() );
2603 return GIT_PASSTHROUGH;
2604 }
2605
2606 return GIT_OK;
2607}
2608
2609
2610// libgit2 marks at most one entry to merge, the branch the current one tracks, so the name is
2611// not needed to tell them apart. FetchHeadMarksAtMostOneBranchToMerge holds that to account.
2612extern "C" int fetchhead_foreach_cb( const char*, const char*,
2613 const git_oid* aOID, unsigned int aIsMerge, void* aPayload )
2614{
2615 if( aIsMerge )
2616 git_oid_cpy( (git_oid*) aPayload, aOID );
2617
2618 return 0;
2619}
2620
2621
2622extern "C" void clone_progress_cb( const char* aStr, size_t aLen, size_t aTotal, void* aPayload )
2623{
2624 KIGIT_REPO_MIXIN* parent = reinterpret_cast<KIGIT_REPO_MIXIN*>( aPayload );
2625
2626 wxString progressMessage( aStr );
2627 parent->UpdateProgress( aLen, aTotal, progressMessage );
2628}
2629
2630
2631extern "C" int progress_cb( const char* str, int len, void* aPayload )
2632{
2633 KIGIT_REPO_MIXIN* parent = reinterpret_cast<KIGIT_REPO_MIXIN*>( aPayload );
2634
2635 if( parent->IsThisOperationCancelled() )
2636 {
2637 wxLogTrace( traceGit, "Progress CB cancelled" );
2638 return GIT_EUSER;
2639 }
2640
2641 parent->ReceiveSideband( str, len );
2642 return 0;
2643}
2644
2645
2646extern "C" int transfer_progress_cb( const git_transfer_progress* aStats, void* aPayload )
2647{
2648 KIGIT_REPO_MIXIN* parent = reinterpret_cast<KIGIT_REPO_MIXIN*>( aPayload );
2649
2650 wxString progressMessage = wxString::Format( _( "Received %u of %u objects" ),
2651 aStats->received_objects,
2652 aStats->total_objects );
2653 if( parent->IsThisOperationCancelled() )
2654 {
2655 wxLogTrace( traceGit, "Transfer progress cancelled" );
2656 return GIT_EUSER;
2657 }
2658
2659 parent->UpdateProgress( aStats->received_objects, aStats->total_objects, progressMessage );
2660
2661 return 0;
2662}
2663
2664
2665extern "C" int update_cb( const char* aRefname, const git_oid* aFirst, const git_oid* aSecond,
2666 void* aPayload )
2667{
2668 constexpr int cstring_len = 8;
2669 char a_str[cstring_len + 1];
2670 char b_str[cstring_len + 1];
2671
2672 KIGIT_REPO_MIXIN* parent = reinterpret_cast<KIGIT_REPO_MIXIN*>( aPayload );
2673 wxString status;
2674
2675 git_oid_tostr( b_str, cstring_len, aSecond );
2676
2677#if ( LIBGIT2_VER_MAJOR >= 1 ) || ( LIBGIT2_VER_MINOR >= 99 )
2678 if( !git_oid_is_zero( aFirst ) )
2679#else
2680 if( !git_oid_iszero( aFirst ) )
2681#endif
2682 {
2683 git_oid_tostr( a_str, cstring_len, aFirst );
2684 status = wxString::Format( _( "* [updated] %s..%s %s" ), a_str, b_str, aRefname );
2685 }
2686 else
2687 {
2688 status = wxString::Format( _( "* [new] %s %s" ), b_str, aRefname );
2689 }
2690
2691 parent->UpdateProgress( 0, 0, status );
2692
2693 return 0;
2694}
2695
2696
2697extern "C" int push_transfer_progress_cb( unsigned int aCurrent, unsigned int aTotal, size_t aBytes,
2698 void* aPayload )
2699{
2700 long long progress = 100;
2701 KIGIT_REPO_MIXIN* parent = reinterpret_cast<KIGIT_REPO_MIXIN*>( aPayload );
2702
2703 if( parent->IsThisOperationCancelled() )
2704 {
2705 wxLogTrace( traceGit, "Push progress cancelled" );
2706 return GIT_EUSER;
2707 }
2708
2709 if( aTotal != 0 )
2710 {
2711 progress = ( aCurrent * 100ll ) / aTotal;
2712 }
2713
2714 wxString progressMessage = wxString::Format( _( "Writing objects: %lld%% (%u/%u), %zu bytes" ),
2715 progress, aCurrent, aTotal, aBytes );
2716 parent->UpdateProgress( aCurrent, aTotal, progressMessage );
2717
2718 return 0;
2719}
2720
2721
2722extern "C" int push_update_reference_cb( const char* aRefname, const char* aStatus, void* aPayload )
2723{
2724 KIGIT_REPO_MIXIN* parent = reinterpret_cast<KIGIT_REPO_MIXIN*>( aPayload );
2725
2726 if( aStatus && *aStatus )
2727 {
2728 const wxString status = KIGIT::DecodeServerText( aStatus, strlen( aStatus ) );
2729
2730 parent->SetPushRefusal( wxString::Format( wxS( "%s: %s" ), aRefname, status ) );
2731
2732 wxString statusMessage = wxString::Format( _( "* [rejected] %s (%s)" ), aRefname,
2733 KIGIT::PrintableServerText( status ) );
2734 parent->UpdateProgress( 0, 0, statusMessage );
2735
2736 // Without this the push reports success even though the remote refused it.
2737 git_error_set_str( GIT_ERROR_NET, aStatus );
2738 return GIT_ERROR;
2739 }
2740 else
2741 {
2742 wxString statusMessage = wxString::Format( _( "[updated] %s" ), aRefname );
2743 parent->UpdateProgress( 0, 0, statusMessage );
2744 }
2745
2746 return 0;
2747}
2748
2749
2750extern "C" int credentials_cb( git_cred** aOut, const char* aUrl, const char* aUsername,
2751 unsigned int aAllowedTypes, void* aPayload )
2752{
2753 KIGIT_REPO_MIXIN* parent = reinterpret_cast<KIGIT_REPO_MIXIN*>( aPayload );
2754 KIGIT_COMMON* common = parent->GetCommon();
2755
2756 // The transport's URL outranks a cached remote that may be empty or stale
2757 common->SyncRemoteUrl( wxString::FromUTF8( aUrl ) );
2758
2759 // The configured remote URL may embed a password. Keep it out of the trace log.
2760 wxString safeUrl = KIGIT::StripEmbeddedUserinfo( wxString::FromUTF8( aUrl ) );
2761
2762 wxLogTrace( traceGit, "Credentials callback for %s, testing %d", safeUrl, aAllowedTypes );
2763
2764 parent->AuthAttempt().connType = parent->GetConnType();
2765
2767 {
2768 wxLogTrace( traceGit, "Local repository, no credentials needed" );
2769 return GIT_PASSTHROUGH;
2770 }
2771
2772 if( aAllowedTypes & GIT_CREDENTIAL_USERNAME
2773 && !( parent->TestedTypes() & GIT_CREDENTIAL_USERNAME ) )
2774 {
2775 wxString username = parent->GetUsername().Trim().Trim( false );
2776 // A token in the URL parses as the username, so no trace names it
2777 wxLogTrace( traceGit, "Username credential at %s with allowed type %d", safeUrl, aAllowedTypes );
2778
2779 // An empty username makes the server answer "unsupported authentication", which tells
2780 // the user nothing. Fail authentication instead, so the caller prompts and retries.
2781 if( username.IsEmpty() )
2782 {
2783 git_error_clear();
2784 git_error_set_str( GIT_ERROR_NET, _( "Unable to authenticate" ).mbc_str() );
2785 parent->SetAuthFailure();
2786 return GIT_EAUTH;
2787 }
2788
2789 if( int rc = git_credential_username_new( aOut, username.ToStdString().c_str() ); rc != GIT_OK )
2790 {
2791 wxLogTrace( traceGit, "Failed to create username credential: %s", KIGIT_COMMON::GetLastGitError() );
2792 return rc;
2793 }
2794 else
2795 {
2796 wxLogTrace( traceGit, "Created username credential" );
2797 }
2798
2799 parent->TestedTypes() |= GIT_CREDENTIAL_USERNAME;
2800 }
2802 && ( aAllowedTypes & GIT_CREDENTIAL_USERPASS_PLAINTEXT )
2803 && !( parent->TestedTypes() & GIT_CREDENTIAL_USERPASS_PLAINTEXT )
2804 && !parent->GetUsername().IsEmpty() )
2805 {
2806 // Plaintext authentication
2807 wxLogTrace( traceGit, "Plaintext authentication at %s with allowed type %d", safeUrl, aAllowedTypes );
2808 return common->HandlePlaintextAuthentication( aOut, parent->GetUsername() );
2809 }
2811 && ( aAllowedTypes & GIT_CREDENTIAL_SSH_KEY )
2812 && !( parent->TestedTypes() & GIT_CREDENTIAL_SSH_KEY ) )
2813 {
2814 // libgit2 fails a credential whose user differs from the one the SSH session already uses
2815 wxString sshUser = ( aUsername && *aUsername ) ? wxString::FromUTF8( aUsername ) : parent->GetUsername();
2816
2817 // SSH key authentication
2818 int result = common->HandleSSHKeyAuthentication( aOut, sshUser, parent->AuthAttempt() );
2819
2820 // Translate exhausted-keys PASSTHROUGH into a proper auth error so the
2821 // retry loop runs and libgit2 doesn't emit "no callback set".
2822 if( result == GIT_PASSTHROUGH )
2823 {
2824 git_error_clear();
2825 git_error_set_str( GIT_ERROR_NET, _( "Unable to authenticate" ).mbc_str() );
2826 parent->SetAuthFailure();
2827 return GIT_EAUTH;
2828 }
2829
2830 return result;
2831 }
2832 else
2833 {
2834 // Signal an auth failure so the caller prompts and retries, rather than letting libgit2 report
2835 // "no callback set" as a dead end.
2836 git_error_clear();
2837 git_error_set_str( GIT_ERROR_NET, _( "Unable to authenticate" ).mbc_str() );
2838 parent->SetAuthFailure();
2839 return GIT_EAUTH;
2840 }
2841
2842 return GIT_OK;
2843};
2844
2845
2846extern "C" int certificate_check_cb( git_cert* aCert, int aValid, const char* aHost, void* aPayload )
2847{
2848 if( !aCert || !aHost || !aPayload || aCert->cert_type != GIT_CERT_HOSTKEY_LIBSSH2 )
2849 return GIT_PASSTHROUGH;
2850
2851 const git_cert_hostkey* cert = reinterpret_cast<const git_cert_hostkey*>( aCert );
2852
2853 KIGIT_REPO_MIXIN* handler = reinterpret_cast<KIGIT_REPO_MIXIN*>( aPayload );
2854
2855 auto refuse = [&]( const wxString& aReason )
2856 {
2857 // libgit2 v1.9.0 ssh_libssh2.c:758-760 replaces any error the callback sets
2858 handler->SetHostKeyRefusal( aReason );
2859 return GIT_ECERTIFICATE;
2860 };
2861
2862 // libgit2 1.9 connects on a passthrough even when nothing was verified
2863 auto refuseUnreadable = [&]()
2864 {
2865 wxLogTrace( traceGit, "Host key from %s cannot be read, refusing it", aHost );
2866 return refuse( wxString::Format( _( "KiCad could not read the key %s presented, so the connection was not "
2867 "made. Ask your administrator which host key the server uses." ),
2868 wxString::FromUTF8( aHost ) ) );
2869 };
2870
2871 if( !( cert->type & GIT_CERT_SSH_RAW ) || !cert->hostkey || cert->hostkey_len == 0 )
2872 return refuseUnreadable();
2873
2875 key.rawKey.assign( cert->hostkey, cert->hostkey_len );
2876 key.host = wxString::FromUTF8( aHost );
2877
2878 // An SCP address hands over an IPv6 host still in its brackets
2879 if( key.host.StartsWith( wxS( "[" ) ) && key.host.EndsWith( wxS( "]" ) ) )
2880 key.host = key.host.Mid( 1, key.host.length() - 2 );
2881
2882 key.port = handler->GetCommon()->SSHPortFor( key.host );
2883
2884 // Checking another port's entries would let a wildcard or plain-host line vouch for this server
2885 if( key.port < 0 )
2886 {
2887 wxLogTrace( traceGit, "Host %s does not match the synced remote, refusing its key", key.host );
2888 return refuse( wxString::Format( _( "The address of %s could not be matched to the remote, so its key was "
2889 "not checked. Check the remote URL." ),
2890 key.host ) );
2891 }
2892
2893 key.keyType = KIGIT::HostKeyType( key.rawKey );
2895
2896 if( key.keyType.IsEmpty() )
2897 return refuseUnreadable();
2898
2900
2901 wxLogTrace( traceGit, "Host key %s SHA256:%s for %s port %d, known_hosts state %d, libgit2 valid %d", key.keyType,
2902 key.fingerprint, key.host, key.port, static_cast<int>( key.state ), aValid );
2903
2904 // libssh2 accepts revoked keys and port-22 lines for other ports, and 1.5.0 reads no known_hosts at all
2906 return 0;
2907
2908 handler->SetHostKeyFailure( key );
2909 return GIT_ECERTIFICATE;
2910}
2911
2912
2913namespace KIGIT
2914{
2915
2916bool IsPrivateKeyEncrypted( const wxString& aKeyText )
2917{
2918 wxArrayString lines = wxSplit( aKeyText, '\n', '\0' );
2919
2920 if( lines.IsEmpty() || !lines[0].Contains( wxS( "PRIVATE KEY" ) ) )
2921 return false;
2922
2923 if( lines[0].Contains( wxS( "ENCRYPTED" ) ) )
2924 return true;
2925
2926 // A PEM key carries Proc-Type in the header block, which ends at the blank line.
2927 for( size_t ii = 1; ii < lines.GetCount(); ++ii )
2928 {
2929 if( lines[ii].Trim().IsEmpty() )
2930 break;
2931
2932 if( lines[ii].StartsWith( wxS( "Proc-Type:" ) ) && lines[ii].Contains( wxS( "ENCRYPTED" ) ) )
2933 return true;
2934 }
2935
2936 if( !lines[0].Contains( wxS( "OPENSSH PRIVATE KEY" ) ) )
2937 return false;
2938
2939 // An OpenSSH key carries its cipher name in the body, right after the magic.
2940 wxString body;
2941
2942 for( size_t ii = 1; ii < lines.GetCount(); ++ii )
2943 {
2944 if( lines[ii].StartsWith( wxS( "-----" ) ) )
2945 break;
2946
2947 body += lines[ii];
2948 }
2949
2950 wxMemoryBuffer raw = wxBase64Decode( body, wxBase64DecodeMode_SkipWS );
2951
2952 const char magic[] = "openssh-key-v1";
2953 const size_t magicLen = sizeof( magic );
2954 const unsigned char* data = static_cast<const unsigned char*>( raw.GetData() );
2955
2956 if( raw.GetDataLen() < magicLen + 4 || std::memcmp( data, magic, magicLen ) != 0 )
2957 return false;
2958
2959 uint32_t nameLen =
2960 ( static_cast<uint32_t>( data[magicLen] ) << 24 ) | ( static_cast<uint32_t>( data[magicLen + 1] ) << 16 )
2961 | ( static_cast<uint32_t>( data[magicLen + 2] ) << 8 ) | static_cast<uint32_t>( data[magicLen + 3] );
2962
2963 if( nameLen == 0 || raw.GetDataLen() < magicLen + 4 + nameLen )
2964 return false;
2965
2966 std::string cipher( reinterpret_cast<const char*>( data + magicLen + 4 ), nameLen );
2967
2968 return cipher != "none";
2969}
2970
2971
2972git_tree* ResolveRefToTree( git_repository* aRepo, const wxString& aRef )
2973{
2974 if( !aRepo )
2975 return nullptr;
2976
2977 git_object* obj = nullptr;
2978 std::string refStr( aRef.ToUTF8() );
2979
2980 if( git_revparse_single( &obj, aRepo, refStr.c_str() ) != 0 )
2981 {
2982 wxLogTrace( traceGit, "git_revparse_single failed for ref '%s': %s",
2984 return nullptr;
2985 }
2986
2987 KIGIT::GitObjectPtr objPtr( obj );
2988 git_tree* tree = nullptr;
2989
2990 if( git_object_peel( reinterpret_cast<git_object**>( &tree ), obj, GIT_OBJECT_TREE ) != 0 )
2991 {
2992 wxLogTrace( traceGit, "git_object_peel to tree failed for '%s': %s",
2994 return nullptr;
2995 }
2996
2997 return tree;
2998}
2999
3000
3001std::vector<wxString> CompareBranchRefs( git_repository* aRepo )
3002{
3003 std::vector<wxString> refs;
3004
3005 if( !aRepo )
3006 return refs;
3007
3008 auto resolves = [&]( const wxString& aRef )
3009 {
3010 return !aRef.IsEmpty() && KIGIT::GitTreePtr( ResolveRefToTree( aRepo, aRef ) ) != nullptr;
3011 };
3012
3013 auto add = [&]( const wxString& aRef )
3014 {
3015 if( resolves( aRef ) && std::find( refs.begin(), refs.end(), aRef ) == refs.end() )
3016 refs.push_back( aRef );
3017 };
3018
3019 wxString current;
3020 wxString upstream;
3021 git_reference* head = nullptr;
3022
3023 if( git_repository_head( &head, aRepo ) == 0 )
3024 {
3025 KIGIT::GitReferencePtr headPtr( head );
3026
3027 if( git_reference_is_branch( head ) )
3028 {
3029 current = wxString::FromUTF8( git_reference_shorthand( head ) );
3030
3031 git_reference* tracking = nullptr;
3032
3033 if( git_branch_upstream( &tracking, head ) == 0 )
3034 {
3035 KIGIT::GitReferencePtr trackingPtr( tracking );
3036 upstream = wxString::FromUTF8( git_reference_shorthand( tracking ) );
3037 }
3038 }
3039 }
3040
3041 const wxString main = wxS( "main" );
3042 const wxString master = wxS( "master" );
3043
3044 for( const wxString& base : { main, master, upstream } )
3045 {
3046 if( base != current && resolves( base ) )
3047 {
3048 refs.push_back( base );
3049 break;
3050 }
3051 }
3052
3053 add( current.IsEmpty() ? wxString( wxS( "HEAD" ) ) : current );
3054 add( main );
3055 add( master );
3056 add( upstream );
3057 add( wxS( "HEAD" ) );
3058
3059 return refs;
3060}
3061
3062
3063void CollectDiffDeltas( git_diff* aDiff,
3064 const std::function<void( const git_diff_delta& )>& aCallback )
3065{
3066 if( !aDiff )
3067 return;
3068
3069 std::size_t numDeltas = git_diff_num_deltas( aDiff );
3070
3071 for( std::size_t ii = 0; ii < numDeltas; ++ii )
3072 {
3073 const git_diff_delta* delta = git_diff_get_delta( aDiff, ii );
3074
3075 if( !delta )
3076 continue;
3077
3078 aCallback( *delta );
3079 }
3080}
3081
3082
3083wxString BranchNameError( const wxString& aName )
3084{
3085 if( aName.IsEmpty() )
3086 return _( "Enter a name for the branch." );
3087
3088 // git refuses all of these, but says so in its own words. Name them while the user can
3089 // still fix the name.
3090 if( aName.Contains( wxS( " " ) ) )
3091 return _( "A branch name cannot contain spaces." );
3092
3093 if( aName.StartsWith( wxS( "-" ) ) )
3094 return _( "A branch name cannot start with a dash." );
3095
3096 if( aName.Contains( wxS( ".." ) ) )
3097 return _( "A branch name cannot contain two dots in a row." );
3098
3099 if( aName.EndsWith( wxS( "/" ) ) )
3100 return _( "A branch name cannot end with a slash." );
3101
3102 int valid = 0;
3103
3104 if( git_branch_name_is_valid( &valid, aName.utf8_string().c_str() ) != GIT_OK || !valid )
3105 return wxString::Format( _( "'%s' is not a valid branch name." ), aName );
3106
3107 return wxEmptyString;
3108}
3109
3110
3111wxString DecodeRepoText( const char* aText )
3112{
3113 if( !aText || !*aText )
3114 return wxString();
3115
3116 wxString decoded = wxString::FromUTF8( aText );
3117
3118 if( !decoded.IsEmpty() )
3119 return decoded;
3120
3121 return wxString( aText, wxConvISO8859_1 );
3122}
3123
3124
3125wxString BusyMessage()
3126{
3127 return _( "Another git operation is in progress. Try again in a moment." );
3128}
3129
3130
3131wxString ComposeCommitMessage( const wxString& aSummary, const wxString& aDescription, const wxString& aSignOff )
3132{
3133 wxString message = aSummary;
3134 message.Replace( wxS( "\r\n" ), wxS( "\n" ) );
3135 message.Trim( true ).Trim( false );
3136
3137 wxString description = aDescription;
3138 description.Replace( wxS( "\r\n" ), wxS( "\n" ) );
3139 description.Trim( true ).Trim( false );
3140
3141 if( !description.IsEmpty() )
3142 message << wxS( "\n\n" ) << description;
3143
3144 if( !aSignOff.IsEmpty() )
3145 {
3146 wxString trailer = wxS( "Signed-off-by: " ) + aSignOff;
3147 bool signedOff = false;
3148
3149 // An amended message already carries its own sign off line.
3150 for( wxString line : wxSplit( message, '\n' ) )
3151 {
3152 if( line.Trim() == trailer )
3153 signedOff = true;
3154 }
3155
3156 if( !signedOff )
3157 message << wxS( "\n\n" ) << trailer;
3158 }
3159
3160 return message;
3161}
3162
3163
3164void SplitCommitMessage( const wxString& aMessage, wxString& aSummary, wxString& aDescription )
3165{
3166 wxString message = aMessage;
3167 message.Replace( wxS( "\r\n" ), wxS( "\n" ) );
3168 message.Trim( true ).Trim( false );
3169
3170 aSummary = message.BeforeFirst( '\n' );
3171 aDescription = message.AfterFirst( '\n' );
3172 aDescription.Trim( false );
3173}
3174
3175} // namespace KIGIT
int index
const char * name
virtual RestoreResult RestoreVersion(KIGIT_COMMON &aCommon, const wxString &aCommitHash, KIGIT_ERROR_MESSAGE &aError)=0
virtual bool CreateTag(git_repository *aRepo, const wxString &aName, const wxString &aMessage)=0
virtual bool CheckoutTag(git_repository *aRepo, const wxString &aName)=0
virtual int CreateBranch(git_repository *aRepo, const wxString &aBranchName, const wxString &aFrom)=0
virtual bool DeleteTag(git_repository *aRepo, const wxString &aName)=0
virtual bool SetRemoteUrl(git_repository *aRepo, const wxString &aName, const wxString &aUrl)=0
virtual bool AbandonOperation(git_repository *aRepo, wxString &aError)=0
Drop a merge, rebase, cherry-pick or revert left in progress and return to the last commit.
virtual bool GetMergeConflicts(git_repository *aRepo, const wxString &aStageDir, wxString &aHeadOid, wxString &aTheirsOid, std::vector< KIGIT_CONFLICT_SIDES > &aConflicts, wxString &aError)=0
virtual bool DeleteBranch(git_repository *aRepo, const wxString &aBranchName)=0
virtual bool AddRemote(git_repository *aRepo, const wxString &aName, const wxString &aUrl)=0
virtual bool StashPop(git_repository *aRepo, size_t aIndex)=0
virtual MergeCommitResult CreateMergedCommit(git_repository *aRepo, const wxString &aExpectedHeadOid, const wxString &aTheirsOid, const std::vector< std::pair< wxString, wxString > > &aResolvedFiles, KIGIT_ERROR_MESSAGE &aError)=0
virtual bool StashApply(git_repository *aRepo, size_t aIndex)=0
virtual bool SetUserConfig(git_repository *aRepo, const wxString &aName, const wxString &aEmail)=0
virtual bool IsPathInHead(git_repository *aRepo, const wxString &aPath)=0
virtual bool DiscardChanges(git_repository *aRepo, const std::vector< wxString > &aPaths, wxString &aError)=0
virtual bool StashSave(git_repository *aRepo, const wxString &aMessage)=0
virtual bool StashDrop(git_repository *aRepo, size_t aIndex)=0
virtual bool RemoveRemote(git_repository *aRepo, const wxString &aName)=0
static std::optional< wxString > ProjectPrefix(const wxString &aWorkDir, const wxString &aProjectPath)
The project's folder inside the repository, ending in a slash.
The project's git session.
std::mutex m_gitActionMutex
void FollowRewrittenUrl(const wxString &aRawUrl)
Bind the session to a URL that url.insteadOf rewrote, keeping the username, password and key entered ...
git_repository * m_repo
std::pair< int, int > GetWorkingTreeLineStats(const std::set< wxString > &aPaths) const
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
void SyncRemoteUrl(const wxString &aRawUrl)
Bind the session to the URL a transfer is about to connect to.
CANCEL_TOKEN ActiveCancelToken() const
bool StashPop(size_t aIndex)
std::optional< wxString > GetProjectPrefix() const
Project folder relative to the repository, with a trailing slash.
void SetConnType(GIT_CONN_TYPE aConnType)
For a remote whose address names no transport, the user picks one in the prompt.
wxString GetLastCommitMessage() const
std::mutex m_credentialsMutex
Serializes the credential fields between the UI thread and the authentication callback's worker threa...
wxString GetCurrentBranchName() const
wxString GetGitRootDirectory() const
bool IsOperationInProgress() const
True while a merge, rebase, cherry-pick or revert is in progress, which refuses commits.
KIGIT_TAG_DETAIL GetTagDetail(const wxString &aName) const
wxString GetSSHKey() const
The key the project was set up with, even when its file is gone, otherwise the first key offered.
void SetSSHKey(const wxString &aSSHKey)
bool IsPullRebaseConfigured() const
True when the repository is set to rebase rather than merge when getting changes.
KIGIT_COMMIT_DETAIL GetCommitDetail(const wxString &aHash) const
wxString m_projectPath
wxString m_urlUsername
static const unsigned KIGIT_CREDENTIAL_SSH_AGENT
int HandlePlaintextAuthentication(git_cred **aOut, const wxString &aUsername)
bool HasUpstreamCommits() const
True when the upstream branch has commits the current branch does not.
bool CheckoutTag(const wxString &aName)
wxString remoteNameWithoutUpstream() const
Remote to use when HEAD names no upstream, empty when several remotes leave it ambiguous.
wxString m_publicKeysHost
bool DeleteTag(const wxString &aName)
std::vector< wxString > GetProjectDirs()
Return a vector of project files in the repository.
wxString GetRemoteNameOrDefault() const
Returns GetRemotename() when non-empty, otherwise "origin".
static KIGIT_ERROR_INFO GetLastGitErrorInfo(int aCode)
The error libgit2 holds for a call that returned aCode.
git_repository * GetRepo() const
KIGIT_COMMON(git_repository *aRepo, const wxString &aProjectPath=wxEmptyString)
aProjectPath is the absolute project directory.
CREDENTIAL_IDENTITY GetCredentialIdentity() const
std::vector< wxString > GetRecentCommitMessages(size_t aMax) const
wxString GetProjectDir() const
Return the path set by SetProjectDir(), or the repository's working directory.
bool RemoveRemote(const wxString &aName)
std::optional< size_t > GetUnmergedCommitCount(const wxString &aBranch) const
Commits in aBranch that HEAD does not contain. No value when either cannot be read.
bool CreateBranch(const wxString &aName, const wxString &aFrom=wxEmptyString)
wxString GetNextPublicKey()
By value, so the caller never holds a pointer into the vector.
bool AddRemote(const wxString &aName, const wxString &aUrl)
int SSHPortFor(const wxString &aHost) const
The SSH port the session's remote names for aHost, 22 when it names none, or -1 when the remote is on...
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,...
void AddRefreshListener(void *aOwner, std::function< void()> aCallback)
std::vector< KIGIT_STASH_INFO > GetStashes() const
std::unique_lock< std::mutex > TryLockActionFor(std::chrono::milliseconds aWait)
TryLockAction that keeps trying for aWait before giving up.
bool StartUnbornBranch(const wxString &aName)
Point HEAD at a branch that does not exist yet, so the first commit creates it.
std::atomic< unsigned > m_testedTypes
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 IsHeadUnborn() const
True while the repository has no commits, so HEAD names a branch that does not exist yet.
bool ApplyPromptedCredentials(const CREDENTIAL_IDENTITY &aFor, const PROMPTED_CREDENTIALS &aEntered)
Apply what the prompt collected, unless the session has moved to another remote since aFor was taken.
std::vector< std::pair< void *, std::function< void()> > > m_refreshListeners
std::vector< wxString > m_publicKeys
wxString m_projectDir
bool IsShuttingDown() const
Background timers ask this before starting work of their own.
KIGIT_BRANCH_DETAIL GetBranchDetail(const wxString &aBranch) const
wxString GetHeadCommitId() const
Full commit ID at HEAD, or empty for an unborn or missing HEAD.
bool HasPushAndPullRemote() const
bool StashApply(size_t aIndex)
bool DiscardChanges(const std::vector< wxString > &aPaths, wxString &aError)
Put the given working directory relative paths back to their committed state.
void rebuildPublicKeysUnlocked()
Rebuild m_publicKeys as the project key, when offerable, then the discovered keys,...
GIT_CONN_TYPE m_connType
bool SetUserConfig(const wxString &aName, const wxString &aEmail)
void updateConnectionTypeUnlocked()
Body of updateConnectionType, for callers that already hold m_credentialsMutex.
bool IsPathInHead(const wxString &aPath)
True when the working directory relative path exists in the HEAD commit's tree.
KIGIT_STASH_DETAIL GetStashDetail(const wxString &aId) const
std::vector< std::pair< wxString, wxString > > GetRemotes() const
Returns every remote as a (name, URL) pair.
static wxString GetLastGitError()
libgit2's last message, without credentials from any URL it quotes, so it is safe to show or trace.
bool CreateTag(const wxString &aName, const wxString &aMessage=wxEmptyString)
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 GetFirstOfferedSSHKey() const
The key the next attempt will try first, for the credential prompt to show.
void RemoveRefreshListener(void *aOwner)
bool StashDrop(size_t aIndex)
bool HasLocalCommits() const
True when HEAD has commits its upstream does not, or has no upstream yet.
std::vector< wxString > m_discoveredKeys
bool StashSave(const wxString &aMessage)
Set the uncommitted tracked changes aside as a new stash, as git stash push would.
wxDateTime GetLastFetchTime() const
static REMOTE_URL ParseRemoteURL(const wxString &aURL)
wxString GetPassword()
GitOperation OperationInProgress() const
Which operation that is, so the panel can name it.
int HandleSSHAgentAuthentication(git_cred **aOut, const wxString &aUsername)
std::optional< KIGIT_AHEAD_BEHIND > compareRefs(const git_reference *aBranch, const git_reference *aBase) const
Count commits unique to aBranch and aBase. No value if the history cannot be read.
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.
std::vector< KIGIT_TAG_INFO > GetTags() const
std::vector< KIGIT_BRANCH_INFO > GetBranches() const
Local and remote branches with their tip commits.
bool DeleteBranch(const wxString &aName)
wxString m_projectSSHKey
bool IsPathConflicted(const wxString &aPath) const
True while aPath is one of the files a merge or rebase stopped on.
wxString GetRemotename() const
bool IsHeadDetached() const
True when HEAD points at a commit rather than a branch, as after checking out a tag.
int HandleSSHKeyAuthentication(git_cred **aOut, const wxString &aUsername, KIGIT_AUTH_ATTEMPT &aAttempt)
Offer the agent's keys once, then the next key file, recording in aAttempt which file was offered.
bool SetRemoteUrl(const wxString &aName, const wxString &aUrl)
void SetPushRefusal(const wxString &aStatus)
void SetHostKeyRefusal(const wxString &aReason)
wxString GetUsername() const
Get the username.
KIGIT_AUTH_ATTEMPT & AuthAttempt()
std::atomic< unsigned > & TestedTypes()
Return the connection types that have been tested for authentication.
bool IsThisOperationCancelled() const
void SetHostKeyFailure(const KIGIT::HOST_KEY_FAILURE &aFailure)
KIGIT_COMMON * GetCommon() const
Get the common object.
KIGIT_COMMON::GIT_CONN_TYPE GetConnType() const
Get the connection type.
void ReceiveSideband(const char *aData, size_t aLength)
Keep sideband text the server sent for the user, such as why it refused a pushed branch,...
void UpdateProgress(int aCurrent, int aTotal, const wxString &aMessage) override
static bool registered
#define _(s)
@ NONE
Definition eda_fill.h:42
GIT_BACKEND * GetGitBackend()
MergeCommitResult
Definition git_backend.h:67
const wxChar *const traceGit
Flag to enable Git debugging output.
std::string message
@ USER
The main config directory (e.g. ~/.config/kicad/)
int certificate_check_cb(git_cert *aCert, int aValid, const char *aHost, void *aPayload)
Check an SSH host key against known_hosts, which decides alone.
int fetchhead_foreach_cb(const char *, const char *, const git_oid *aOID, unsigned int aIsMerge, void *aPayload)
int progress_cb(const char *str, int len, void *aPayload)
int push_update_reference_cb(const char *aRefname, const char *aStatus, void *aPayload)
int update_cb(const char *aRefname, const git_oid *aFirst, const git_oid *aSecond, void *aPayload)
static size_t findUserinfoAt(const wxString &aUrl)
static bool scpHasUser(const wxString &aText, size_t aStart)
int transfer_progress_cb(const git_transfer_progress *aStats, void *aPayload)
static bool isScpIpv6(const wxString &aText, size_t aBracket)
static int parsePort(const wxString &aPort)
int credentials_cb(git_cred **aOut, const char *aUrl, const char *aUsername, unsigned int aAllowedTypes, void *aPayload)
void clone_progress_cb(const char *aStr, size_t aLen, size_t aTotal, void *aPayload)
int push_transfer_progress_cb(unsigned int aCurrent, unsigned int aTotal, size_t aBytes, void *aPayload)
static bool parseScpAddress(const wxString &aURL, wxString &aUser, wxString &aHost, wxString &aPort)
static wxString decodePercent(const wxString &aText)
GitOperation
RestoreResult
std::unique_ptr< git_tree, decltype([](git_tree *aTree) { git_tree_free(aTree); })> GitTreePtr
A unique pointer for git_tree objects with automatic cleanup.
APIEXPORT wxString StripEmbeddedUserinfo(const wxString &aText)
Remove the user and password from every URL quoted in a message, since the remote's can carry a passw...
bool IsPrivateKeyEncrypted(const wxString &aKeyText)
Report whether an SSH private key is protected by a passphrase.
void SplitCommitMessage(const wxString &aMessage, wxString &aSummary, wxString &aDescription)
Split a commit message into its first line and the description after the first blank line.
APIEXPORT bool IsOfferableSSHKey(const wxString &aKey)
Whether aKey can be offered to a server.
APIEXPORT wxString PrintableServerText(const wxString &aText)
Text a server sent, made safe to show: no terminal escapes, control or bidi characters,...
wxString BranchNameError(const wxString &aName)
Empty when aName is a branch name git would accept, otherwise why it would not, in the words to show ...
git_tree * ResolveRefToTree(git_repository *aRepo, const wxString &aRef)
Resolve a string ref (branch name, short OID, full OID, tag) to its tree.
@ MATCH
An entry names the host with this key.
std::unique_ptr< git_revwalk, decltype([](git_revwalk *aWalker) { git_revwalk_free(aWalker); })> GitRevWalkPtr
A unique pointer for git_revwalk objects with automatic cleanup.
wxString BusyMessage()
What every caller says when it finds the session already busy.
std::unique_ptr< git_commit, decltype([](git_commit *aCommit) { git_commit_free(aCommit); })> GitCommitPtr
A unique pointer for git_commit objects with automatic cleanup.
void CollectDiffDeltas(git_diff *aDiff, const std::function< void(const git_diff_delta &)> &aCallback)
Walk every delta in a computed diff, invoking aCallback once per delta.
std::unique_ptr< git_status_list, decltype([](git_status_list *aList) { git_status_list_free(aList); })> GitStatusListPtr
A unique pointer for git_status_list objects with automatic cleanup.
APIEXPORT KNOWN_HOST_STATE LookupKnownHost(const wxString &aKnownHostsText, const wxString &aHost, int aPort, const wxString &aKeyType, const std::string &aRawKey)
Classify a server's key against the text of a known_hosts file, as OpenSSH reads it.
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_patch, decltype([](git_patch *aPatch) { git_patch_free(aPatch); })> GitPatchPtr
A unique pointer for git_patch objects with automatic cleanup.
std::unique_ptr< git_reference, decltype([](git_reference *aRef) { git_reference_free(aRef); })> GitReferencePtr
A unique pointer for git_reference objects with automatic cleanup.
wxString ComposeCommitMessage(const wxString &aSummary, const wxString &aDescription, const wxString &aSignOff)
Join a summary line, an optional description and an optional sign-off into one commit message.
APIEXPORT wxString DecodeServerText(const char *aData, size_t aLength)
Bytes a server sent, as UTF-8 or, when they are not valid UTF-8, one character for each byte.
std::unique_ptr< git_strarray, decltype([](git_strarray *aArray) { git_strarray_free(aArray); })> GitStrArrayPtr
A unique pointer for git_strarray objects with automatic cleanup.
APIEXPORT wxString HomeDir()
The user's home folder, where ssh looks for its config and default keys.
APIEXPORT wxString ReadKnownHosts()
The text of KnownHostsPath(). Empty when it is missing or cannot be read.
std::unique_ptr< git_reference_iterator, decltype([](git_reference_iterator *aIter) { git_reference_iterator_free(aIter); })> GitReferenceIteratorPtr
A unique pointer for git_reference_iterator objects with automatic cleanup.
APIEXPORT wxWindow *& ActiveAdvancedGitWindow()
The advanced git window, or null when none is open.
APIEXPORT wxString HostKeyType(const std::string &aRawKey)
The type a raw SSH key names for itself, such as ssh-ed25519. Empty when the key is malformed.
std::unique_ptr< git_tree_entry, decltype([](git_tree_entry *aEntry) { git_tree_entry_free(aEntry); })> GitTreeEntryPtr
A unique pointer for git_tree_entry objects with automatic cleanup.
APIEXPORT wxString HostKeyFingerprint(const std::string &aRawKey)
SHA256 of a raw key in unpadded base64, as ssh shows it after "SHA256:".
wxString DecodeRepoText(const char *aText)
Decode text that came out of a repository.
std::vector< wxString > CompareBranchRefs(git_repository *aRepo)
Refs to offer when comparing branches, base first and head second.
APIEXPORT bool IsSameSSHKey(const wxString &aKey, const wxString &aOther)
Whether two key paths name the same file.
std::unique_ptr< git_branch_iterator, decltype([](git_branch_iterator *aIter) { git_branch_iterator_free(aIter); })> GitBranchIteratorPtr
A unique pointer for git_branch_iterator objects with automatic cleanup.
std::unique_ptr< git_index, decltype([](git_index *aIndex) { git_index_free(aIndex); })> GitIndexPtr
A unique pointer for git_index objects with automatic cleanup.
std::unique_ptr< git_diff, decltype([](git_diff *aDiff) { git_diff_free(aDiff); })> GitDiffPtr
A unique pointer for git_diff objects with automatic cleanup.
std::unique_ptr< git_object, decltype([](git_object *aObject) { git_object_free(aObject); })> GitObjectPtr
A unique pointer for git_object objects with automatic cleanup.
std::unique_ptr< git_remote, decltype([](git_remote *aRemote) { git_remote_free(aRemote); })> GitRemotePtr
A unique pointer for git_remote objects with automatic cleanup.
APIEXPORT std::vector< wxString > DefaultSSHKeyCandidates(const wxString &aHomeDir, const wxString &aHost)
Private keys to offer aHost, best first.
bool GetSecret(const wxString &aService, const wxString &aKey, wxString &aSecret)
A server key the connection refused, for the UI to offer to trust or to report.
wxString keyType
As known_hosts names it, such as ssh-ed25519.
wxString fingerprint
SHA256 in unpadded base64, as ssh shows it after "SHA256:".
std::string rawKey
The key as the server sent it.
size_t behind
Commits only in the second reference.
size_t ahead
Commits only in the first reference.
What the credential callback handed out during one operation, kept to explain a rejection.
KIGIT_COMMON::GIT_CONN_TYPE connType
std::vector< wxString > keysTried
Key files offered, in order.
bool agentTried
The SSH agent was offered before any key file.
wxString oldPath
For a rename or copy, where the file came from.
KIGIT::FILE_CHANGE_STATUS status
The remote the credential prompt was opened for.
What the credential prompt collected.
std::optional< wxString > username
A transport address with any embedded credentials extracted.
int port
0 when the address names none.
wxString url
Keeps the SSH username, scheme and port, but never a password.
What libgit2 reported for a failed call, read before anything else can replace it.
A summary the user can act on, with the raw text that explains it kept apart for the details.
wxString search
Keep only commits whose summary, author or hash contains this.
wxString path
Keep only commits that changed this file or folder, relative to the root.
wxString branch
One branch, local or remote, or empty for every branch and HEAD.
static std::string decoded(const std::string &aBase64)
std::string path
wxString result
Test unit parsing edge cases and error handling.
int delta
wxLogTrace helper definitions.