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
24#include <git/git_progress.h>
26#include <kiplatform/secrets.h>
27#include <trace_helpers.h>
28
29#include <git2.h>
30#include <wx/filename.h>
31#include <wx/log.h>
32#include <wx/textfile.h>
33#include <wx/utils.h>
34#include <map>
35#include <vector>
36
37KIGIT_COMMON::KIGIT_COMMON( git_repository* aRepo ) :
39 m_nextPublicKey( 0 ), m_secretFetched( false )
40{}
41
42
44 // Initialize base class and member variables
45 m_repo( aOther.m_repo ),
46 m_projectDir( aOther.m_projectDir ),
47 m_connType( aOther.m_connType ),
48 m_remote( aOther.m_remote ),
49 m_hostname( aOther.m_hostname ),
50 m_username( aOther.m_username ),
51 m_password( aOther.m_password ),
53
54 // The mutex is default-initialized, not copied
56 m_publicKeys( aOther.m_publicKeys ),
59{
60}
61
62
65
66
67git_repository* KIGIT_COMMON::GetRepo() const
68{
69 return m_repo;
70}
71
72
74{
75 if( !m_projectDir.IsEmpty() )
76 return m_projectDir;
77
78 if( m_repo )
79 {
80 const char* workdir = git_repository_workdir( m_repo );
81
82 if( workdir )
83 return wxString( workdir );
84 }
85
86 return wxEmptyString;
87}
88
89
91{
92 wxCHECK( m_repo, wxEmptyString );
93
94 git_reference* head = nullptr;
95
96 if( git_repository_head( &head, m_repo ) != GIT_OK )
97 return wxEmptyString;
98
99 KIGIT::GitReferencePtr headPtr( head );
100
101 if( !git_reference_is_branch( head ) )
102 return wxEmptyString;
103
104 git_reference* upstream = nullptr;
105
106 if( git_branch_upstream( &upstream, head ) == GIT_OK )
107 {
108 KIGIT::GitReferencePtr upstreamPtr( upstream );
109 const char* shorthand = git_reference_shorthand( upstream );
110
111 if( shorthand )
112 return wxString::FromUTF8( shorthand );
113 }
114
115 // No upstream configured. Synthesise the target that PerformPull's fallback
116 // and the first push will use.
117 const char* branch_shorthand = git_reference_shorthand( head );
118
119 if( !branch_shorthand )
120 return wxEmptyString;
121
122 return wxString::Format( "%s/%s", GetRemoteNameOrDefault(), branch_shorthand );
123}
124
125
127{
128 wxCHECK( m_repo, wxEmptyString );
129 git_reference* head = nullptr;
130
131 int retval = git_repository_head( &head, m_repo );
132
133 if( retval && retval != GIT_EUNBORNBRANCH && retval != GIT_ENOTFOUND )
134 return wxEmptyString;
135
136 KIGIT::GitReferencePtr headPtr( head );
137 git_reference* branch;
138
139 if( git_reference_resolve( &branch, head ) )
140 {
141 wxLogTrace( traceGit, "Failed to resolve branch" );
142 return wxEmptyString;
143 }
144
145 KIGIT::GitReferencePtr branchPtr( branch );
146 const char* branchName = "";
147
148 if( git_branch_name( &branchName, branch ) )
149 {
150 wxLogTrace( traceGit, "Failed to get branch name" );
151 return wxEmptyString;
152 }
153
154 return wxString( branchName );
155}
156
157
159{
160 if( !m_secretFetched )
161 {
163 {
164 wxString secret;
165
167 m_password = secret;
168 }
169
170 m_secretFetched = true;
171 }
172
173 return m_password;
174}
175
176
177std::vector<wxString> KIGIT_COMMON::GetBranchNames() const
178{
179 if( !m_repo )
180 return {};
181
182 std::vector<wxString> branchNames;
183 std::map<git_time_t, wxString> branchNamesMap;
184 wxString firstName;
185
186 git_branch_iterator* branchIterator = nullptr;
187
188 if( git_branch_iterator_new( &branchIterator, m_repo, GIT_BRANCH_LOCAL ) )
189 {
190 wxLogTrace( traceGit, "Failed to get branch iterator" );
191 return branchNames;
192 }
193
194 KIGIT::GitBranchIteratorPtr branchIteratorPtr( branchIterator );
195 git_reference* branchReference = nullptr;
196 git_branch_t branchType;
197
198 while( git_branch_next( &branchReference, &branchType, branchIterator ) != GIT_ITEROVER )
199 {
200 const char* branchName = "";
201 KIGIT::GitReferencePtr branchReferencePtr( branchReference );
202
203 if( git_branch_name( &branchName, branchReference ) )
204 {
205 wxLogTrace( traceGit, "Failed to get branch name in iter loop" );
206 continue;
207 }
208
209 const git_oid* commitId = git_reference_target( branchReference );
210
211 git_commit* commit = nullptr;
212
213 if( git_commit_lookup( &commit, m_repo, commitId ) )
214 {
215 wxLogTrace( traceGit, "Failed to get commit in iter loop" );
216 continue;
217 }
218
219 KIGIT::GitCommitPtr commitPtr( commit );
220 git_time_t commitTime = git_commit_time( commit );
221
222 if( git_branch_is_head( branchReference ) )
223 firstName = branchName;
224 else
225 branchNamesMap.emplace( commitTime, branchName );
226 }
227
228 // Add the current branch to the top of the list
229 if( !firstName.IsEmpty() )
230 branchNames.push_back( firstName );
231
232 // Add the remaining branches in order from newest to oldest
233 for( auto rit = branchNamesMap.rbegin(); rit != branchNamesMap.rend(); ++rit )
234 branchNames.push_back( rit->second );
235
236 return branchNames;
237}
238
239
240std::vector<wxString> KIGIT_COMMON::GetProjectDirs()
241{
242 wxCHECK( m_repo, {} );
243 std::vector<wxString> projDirs;
244
245 git_oid oid;
246 git_commit* commit;
247 git_tree *tree;
248
249 if( git_reference_name_to_id( &oid, m_repo, "HEAD" ) != GIT_OK )
250 {
251 wxLogTrace( traceGit, "An error occurred: %s", KIGIT_COMMON::GetLastGitError() );
252 return projDirs;
253 }
254
255 if( git_commit_lookup( &commit, m_repo, &oid ) != GIT_OK )
256 {
257 wxLogTrace( traceGit, "An error occurred: %s", KIGIT_COMMON::GetLastGitError() );
258 return projDirs;
259 }
260
261 KIGIT::GitCommitPtr commitPtr( commit );
262
263 if( git_commit_tree( &tree, commit ) != GIT_OK )
264 {
265 wxLogTrace( traceGit, "An error occurred: %s", KIGIT_COMMON::GetLastGitError() );
266 return projDirs;
267 }
268
269 KIGIT::GitTreePtr treePtr( tree );
270
271 // Define callback
272 git_tree_walk(
273 tree, GIT_TREEWALK_PRE,
274 []( const char* root, const git_tree_entry* entry, void* payload )
275 {
276 std::vector<wxString>* prjs = static_cast<std::vector<wxString>*>( payload );
277 wxFileName root_fn( git_tree_entry_name( entry ) );
278
279 root_fn.SetPath( root );
280
281 if( git_tree_entry_type( entry ) == GIT_OBJECT_BLOB
282 && ( ( root_fn.GetExt() == "kicad_pro" ) || ( root_fn.GetExt() == "pro" ) ) )
283 {
284 prjs->push_back( root_fn.GetFullPath() );
285 }
286
287 return 0; // continue walking
288 },
289 &projDirs );
290
291 std::sort( projDirs.begin(), projDirs.end(),
292 []( const wxString& a, const wxString& b )
293 {
294 int a_freq = a.Freq( wxFileName::GetPathSeparator() );
295 int b_freq = b.Freq( wxFileName::GetPathSeparator() );
296
297 if( a_freq == b_freq )
298 return a < b;
299 else
300 return a_freq < b_freq;
301
302 } );
303
304 return projDirs;
305}
306
307
308std::pair<std::set<wxString>, std::set<wxString>> KIGIT_COMMON::GetDifferentFiles() const
309{
310 std::pair<std::set<wxString>, std::set<wxString>> modified_files;
311
312 if( !m_repo || IsCancelled() )
313 return modified_files;
314
315 git_reference* head = nullptr;
316 git_reference* remote_head = nullptr;
317
318 if( git_repository_head( &head, m_repo ) != GIT_OK )
319 {
320 wxLogTrace( traceGit, "Failed to get modified HEAD" );
321 return modified_files;
322 }
323
324 KIGIT::GitReferencePtr headPtr( head );
325
326 if( git_branch_upstream( &remote_head, head ) != GIT_OK )
327 {
328 // Branch has no upstream tracking ref configured. Without an upstream there is
329 // nothing to compare against, so leave both sets empty. Walking commits unbounded
330 // would otherwise dump the entire history's root tree into the result.
331 wxLogTrace( traceGit, "Failed to get modified remote HEAD" );
332 return modified_files;
333 }
334
335 KIGIT::GitReferencePtr remoteHeadPtr( remote_head );
336
337 const git_oid* head_oid = git_reference_target( head );
338 const git_oid* remote_oid = git_reference_target( remote_head );
339
340 if( !head_oid || !remote_oid )
341 return modified_files;
342
343 auto load_tree =
344 [this]( const git_oid* aOid ) -> git_tree*
345 {
346 git_commit* commit = nullptr;
347
348 if( git_commit_lookup( &commit, m_repo, aOid ) != GIT_OK )
349 {
350 wxLogTrace( traceGit, "Failed to lookup commit for diff: %s",
352 return nullptr;
353 }
354
355 KIGIT::GitCommitPtr commitPtr( commit );
356 git_tree* tree = nullptr;
357
358 if( git_commit_tree( &tree, commit ) != GIT_OK )
359 {
360 wxLogTrace( traceGit, "Failed to get commit tree for diff: %s",
362 return nullptr;
363 }
364
365 return tree;
366 };
367
368 git_tree* head_tree = load_tree( head_oid );
369 KIGIT::GitTreePtr headTreePtr( head_tree );
370
371 git_tree* remote_tree = load_tree( remote_oid );
372 KIGIT::GitTreePtr remoteTreePtr( remote_tree );
373
374 if( !head_tree || !remote_tree )
375 return modified_files;
376
377 // Find the merge-base so AHEAD and BEHIND can be distinguished. AHEAD = files that
378 // changed between merge-base and HEAD (only in local commits). BEHIND = files that
379 // changed between merge-base and the remote tip (only in remote commits). Without a
380 // shared history the merge-base lookup fails; in that case we treat both sets as empty
381 // because there is no meaningful "ahead vs behind" partition to compute.
382 git_oid base_oid;
383
384 if( git_merge_base( &base_oid, m_repo, head_oid, remote_oid ) != GIT_OK )
385 {
386 wxLogTrace( traceGit, "No merge base between local and remote: %s",
388 return modified_files;
389 }
390
391 git_tree* base_tree = load_tree( &base_oid );
392 KIGIT::GitTreePtr baseTreePtr( base_tree );
393
394 if( !base_tree )
395 return modified_files;
396
397 auto collect_paths =
398 [this]( git_tree* aOldTree, git_tree* aNewTree, std::set<wxString>& aOut )
399 {
400 if( IsCancelled() )
401 return;
402
403 git_diff_options localOpts;
404 git_diff_init_options( &localOpts, GIT_DIFF_OPTIONS_VERSION );
405
406 git_diff* diff = nullptr;
407
408 if( git_diff_tree_to_tree( &diff, m_repo, aOldTree, aNewTree, &localOpts )
409 != GIT_OK )
410 {
411 wxLogTrace( traceGit, "Failed to diff trees: %s",
413 return;
414 }
415
417 [&aOut]( const git_diff_delta& aDelta )
418 {
419 if( aDelta.new_file.path )
420 aOut.insert( wxString::FromUTF8( aDelta.new_file.path ) );
421
422 if( aDelta.old_file.path )
423 aOut.insert( wxString::FromUTF8( aDelta.old_file.path ) );
424 } );
425
426 git_diff_free( diff );
427 };
428
429 collect_paths( base_tree, head_tree, modified_files.first ); // AHEAD
430 collect_paths( base_tree, remote_tree, modified_files.second ); // BEHIND
431
432 // Filter both sets to files whose content actually differs between HEAD and remote.
433 // Without this, a file touched by a commit that has since been replaced with an
434 // identical-tree commit (e.g. a message-only amend) keeps an AHEAD marker even
435 // though its blob matches the remote's blob.
436 std::set<wxString> actuallyDifferent;
437 collect_paths( head_tree, remote_tree, actuallyDifferent );
438
439 auto filterToDifferent = [&]( std::set<wxString>& aSet )
440 {
441 for( auto it = aSet.begin(); it != aSet.end(); )
442 it = actuallyDifferent.count( *it ) ? std::next( it ) : aSet.erase( it );
443 };
444
445 filterToDifferent( modified_files.first );
446 filterToDifferent( modified_files.second );
447
448 return modified_files;
449}
450
451
453{
454 if( !m_repo )
455 return false;
456
457 git_reference* head = nullptr;
458 git_reference* remote_head = nullptr;
459
460 if( git_repository_head( &head, m_repo ) != GIT_OK )
461 {
462 wxLogTrace( traceGit, "Failed to get HEAD: %s", KIGIT_COMMON::GetLastGitError() );
463 return false;
464 }
465
466 KIGIT::GitReferencePtr headPtr( head );
467
468 if( git_branch_upstream( &remote_head, head ) != GIT_OK )
469 {
470 // No remote branch, so we have local commits (new repo?)
471 wxLogTrace( traceGit, "Failed to get remote HEAD: %s", KIGIT_COMMON::GetLastGitError() );
472 return true;
473 }
474
475 KIGIT::GitReferencePtr remoteHeadPtr( remote_head );
476 const git_oid* head_oid = git_reference_target( head );
477 const git_oid* remote_oid = git_reference_target( remote_head );
478 git_revwalk* walker = nullptr;
479
480 if( git_revwalk_new( &walker, m_repo ) != GIT_OK )
481 {
482 wxLogTrace( traceGit, "Failed to create revwalker: %s", KIGIT_COMMON::GetLastGitError() );
483 return false;
484 }
485
486 KIGIT::GitRevWalkPtr walkerPtr( walker );
487
488 if( !head_oid || git_revwalk_push( walker, head_oid ) != GIT_OK )
489 {
490 wxLogTrace( traceGit, "Failed to push commits: %s", KIGIT_COMMON::GetLastGitError() );
491 return false;
492 }
493
494 if( remote_oid && git_revwalk_hide( walker, remote_oid ) != GIT_OK )
495 {
496 wxLogTrace( traceGit, "Failed to push/hide commits: %s", KIGIT_COMMON::GetLastGitError() );
497 return false;
498 }
499
500 git_oid oid;
501
502 // If we can't walk to the next commit, then we are at or behind the remote
503 if( git_revwalk_next( &oid, walker ) != GIT_OK )
504 {
505 wxLogTrace( traceGit, "Failed to walk to next commit: %s", KIGIT_COMMON::GetLastGitError() );
506 return false;
507 }
508
509 return true;
510}
511
512
514{
515 wxCHECK( m_repo, false );
516
517 // Prefer the upstream remote configured for the current branch. Fall back to
518 // "origin" and then to any remote that exposes a fetch URL so that repos
519 // cloned with non-default remote names also enable push/pull operations.
520 auto checkRemote =
521 [this]( const char* aName ) -> bool
522 {
523 git_remote* remote = nullptr;
524
525 if( git_remote_lookup( &remote, m_repo, aName ) != GIT_OK )
526 return false;
527
528 KIGIT::GitRemotePtr remotePtr( remote );
529
530 const char* fetch_url = git_remote_url( remote );
531 const char* push_url = git_remote_pushurl( remote );
532
533 // libgit2 defaults to the fetch URL for pushing when no push URL is set
534 if( !push_url )
535 push_url = fetch_url;
536
537 return fetch_url && push_url;
538 };
539
540 std::string preferred = GetRemoteNameOrDefault().utf8_string();
541
542 if( checkRemote( preferred.c_str() ) )
543 return true;
544
545 if( preferred != "origin" && checkRemote( "origin" ) )
546 return true;
547
548 git_strarray remotes = { nullptr, 0 };
549
550 if( git_remote_list( &remotes, m_repo ) != GIT_OK )
551 {
552 wxLogTrace( traceGit, "Failed to enumerate remotes for haspushpull" );
553 return false;
554 }
555
556 KIGIT::GitStrArrayPtr remotesPtr( &remotes );
557
558 for( size_t ii = 0; ii < remotes.count; ++ii )
559 {
560 if( checkRemote( remotes.strings[ii] ) )
561 return true;
562 }
563
564 return false;
565}
566
567
569{
570 wxString remoteName = GetRemotename();
571
572 if( remoteName.IsEmpty() )
573 remoteName = wxS( "origin" );
574
575 return remoteName;
576}
577
578
580{
581 wxCHECK( m_repo, wxEmptyString );
582
583 wxString retval;
584 git_reference* head = nullptr;
585 git_reference* upstream = nullptr;
586
587 if( git_repository_head( &head, m_repo ) != GIT_OK )
588 {
589 wxLogTrace( traceGit, "Failed to get remote name: %s", KIGIT_COMMON::GetLastGitError() );
590 return retval;
591 }
592
593 KIGIT::GitReferencePtr headPtr( head );
594
595 if( git_branch_upstream( &upstream, head ) != GIT_OK )
596 {
597 wxLogTrace( traceGit, "Failed to get upstream branch: %s", KIGIT_COMMON::GetLastGitError() );
598 git_strarray remotes = { nullptr, 0 };
599
600 if( git_remote_list( &remotes, m_repo ) == GIT_OK )
601 {
602 // No upstream tracking branch. Prefer "origin" when present, otherwise pick the
603 // single configured remote. Returning empty for ambiguous (multiple) remotes
604 // tells callers to fall back to whatever default they want.
605 if( remotes.count == 1 )
606 {
607 retval = remotes.strings[0];
608 }
609 else
610 {
611 for( size_t ii = 0; ii < remotes.count; ++ii )
612 {
613 if( strcmp( remotes.strings[ii], "origin" ) == 0 )
614 {
615 retval = remotes.strings[ii];
616 break;
617 }
618 }
619 }
620
621 git_strarray_dispose( &remotes );
622 }
623 else
624 {
625 wxLogTrace( traceGit, "Failed to list remotes: %s", KIGIT_COMMON::GetLastGitError() );
626
627 // If we can't get the remote name from the upstream branch or the list of remotes,
628 // just return the default remote name
629
630 git_remote* remote = nullptr;
631
632 if( git_remote_lookup( &remote, m_repo, "origin" ) == GIT_OK )
633 {
634 retval = git_remote_name( remote );
635 git_remote_free( remote );
636 }
637 else
638 {
639 wxLogTrace( traceGit, "Failed to get remote name from default remote: %s",
641 }
642 }
643
644 return retval;
645 }
646
647 KIGIT::GitReferencePtr upstreamPtr( upstream );
648 git_buf remote_name = GIT_BUF_INIT_CONST( nullptr, 0 );
649
650 if( git_branch_remote_name( &remote_name, m_repo, git_reference_name( upstream ) ) == GIT_OK )
651 {
652 retval = remote_name.ptr;
653 git_buf_dispose( &remote_name );
654 }
655 else
656 {
657 wxLogTrace( traceGit,
658 "Failed to get remote name from upstream branch: %s",
660 }
661
662 return retval;
663}
664
665
666void KIGIT_COMMON::SetSSHKey( const wxString& aKey )
667{
668 auto it = std::find( m_publicKeys.begin(), m_publicKeys.end(), aKey );
669
670 if( it != m_publicKeys.end() )
671 m_publicKeys.erase( it );
672
673 m_publicKeys.insert( m_publicKeys.begin(), aKey );
674}
675
676
678{
679 if( !m_repo )
680 return wxEmptyString;
681
682 // Prefer the working directory (the user-visible project root). Fall back to
683 // git_repository_path (the .git directory) only for bare repositories.
684 if( const char* workdir = git_repository_workdir( m_repo ) )
685 return wxString::FromUTF8( workdir );
686
687 if( const char* path = git_repository_path( m_repo ) )
688 return wxString::FromUTF8( path );
689
690 return wxEmptyString;
691}
692
693
695{
696 m_publicKeys.clear();
697
698 wxFileName keyFile( wxGetHomeDir(), wxEmptyString );
699 keyFile.AppendDir( ".ssh" );
700 keyFile.SetFullName( "id_rsa" );
701
702 if( keyFile.FileExists() )
703 m_publicKeys.push_back( keyFile.GetFullPath() );
704
705 keyFile.SetFullName( "id_dsa" );
706
707 if( keyFile.FileExists() )
708 m_publicKeys.push_back( keyFile.GetFullPath() );
709
710 keyFile.SetFullName( "id_ecdsa" );
711
712 if( keyFile.FileExists() )
713 m_publicKeys.push_back( keyFile.GetFullPath() );
714
715 keyFile.SetFullName( "id_ed25519" );
716
717 if( keyFile.FileExists() )
718 m_publicKeys.push_back( keyFile.GetFullPath() );
719
720 // Parse SSH config file for hostname information
721 wxFileName sshConfig( wxGetHomeDir(), wxEmptyString );
722 sshConfig.AppendDir( ".ssh" );
723 sshConfig.SetFullName( "config" );
724
725 if( sshConfig.FileExists() )
726 {
727 wxTextFile configFile( sshConfig.GetFullPath() );
728 configFile.Open();
729
730 bool match = false;
731
732 for( wxString line = configFile.GetFirstLine(); !configFile.Eof(); line = configFile.GetNextLine() )
733 {
734 line.Trim( false ).Trim( true );
735
736 if( line.StartsWith( "Host " ) )
737 match = false;
738
739 // The difference here is that we are matching either "Hostname" or "Host" to get the
740 // match. This is because in the absence of a "Hostname" line, the "Host" line is used
741 if( line.StartsWith( "Host" ) && line.Contains( m_hostname ) )
742 match = true;
743
744 if( match && line.StartsWith( "IdentityFile" ) )
745 {
746 wxString keyPath = line.AfterFirst( ' ' ).Trim( false ).Trim( true );
747
748 // Expand ~ to home directory if present
749 if( keyPath.StartsWith( "~" ) )
750 keyPath.Replace( "~", wxGetHomeDir(), false );
751
752 // Add the public key to the beginning of the list
753 if( wxFileName::FileExists( keyPath ) )
754 SetSSHKey( keyPath );
755 }
756 }
757
758 configFile.Close();
759 }
760}
761
762
764{
765 wxCHECK( m_repo, /* void */ );
766
767 // We want to get the current branch's upstream url as well as the stored password
768 // if one exists given the url and username.
769
770 wxString remote_name = GetRemotename();
771 git_remote* remote = nullptr;
772
773 m_remote.clear();
774 m_password.clear();
775 m_secretFetched = false;
776
777 if( git_remote_lookup( &remote, m_repo, remote_name.ToStdString().c_str() ) == GIT_OK )
778 {
779 const char* url = git_remote_url( remote );
780
781 if( url )
782 m_remote = url;
783
784 git_remote_free( remote );
785 }
786
789}
790
791
793{
794 wxString remote = m_remote;
795
796 if( remote.IsEmpty() )
797 remote = GetRemotename();
798
799 if( remote.StartsWith( "https://" ) || remote.StartsWith( "http://" ) )
800 {
802 }
803 else if( remote.StartsWith( "ssh://" ) || remote.StartsWith( "git@" ) || remote.StartsWith( "git+ssh://" )
804 || remote.EndsWith( ".git" ) )
805 {
807 }
808
810}
811
812
814{
815 if( m_remote.StartsWith( "https://" ) || m_remote.StartsWith( "http://" ) )
817 else if( m_remote.StartsWith( "ssh://" ) || m_remote.StartsWith( "git@" ) || m_remote.StartsWith( "git+ssh://" ) )
819 else
821
823 {
824 wxString uri = m_remote;
825 size_t atPos = uri.find( '@' );
826
827 if( atPos != wxString::npos )
828 {
829 size_t protoEnd = uri.find( "//" );
830
831 if( protoEnd != wxString::npos )
832 {
833 wxString credentials = uri.Mid( protoEnd + 2, atPos - protoEnd - 2 );
834 size_t colonPos = credentials.find( ':' );
835
836 if( colonPos != wxString::npos )
837 {
838 m_username = credentials.Left( colonPos );
839 m_password = credentials.Mid( colonPos + 1, credentials.Length() - colonPos - 1 );
840 }
841 else
842 {
843 m_username = credentials;
844 }
845 }
846 else
847 {
848 m_username = uri.Left( atPos );
849 }
850 }
851
852 if( m_remote.StartsWith( "git@" ) )
853 {
854 // SSH format: git@hostname:path
855 size_t colonPos = m_remote.find( ':' );
856
857 if( colonPos != wxString::npos )
858 m_hostname = m_remote.Mid( 4, colonPos - 4 );
859 }
860 else
861 {
862 // other URL format: proto://[user@]hostname/path
863 size_t hostStart = m_remote.find( "://" ) + 2;
864 size_t hostEnd = m_remote.find( '/', hostStart );
865 wxString host;
866
867 if( hostEnd != wxString::npos )
868 host = m_remote.Mid( hostStart, hostEnd - hostStart );
869 else
870 host = m_remote.Mid( hostStart );
871
872 atPos = host.find( '@' );
873
874 if( atPos != wxString::npos )
875 m_hostname = host.Mid( atPos + 1 );
876 else
877 m_hostname = host;
878 }
879 }
880
881 m_secretFetched = !m_password.IsEmpty();
882}
883
884
885int KIGIT_COMMON::HandleSSHKeyAuthentication( git_cred** aOut, const wxString& aUsername )
886{
888 {
889 if( HandleSSHAgentAuthentication( aOut, aUsername ) == GIT_OK )
890 return GIT_OK;
891 // Agent unavailable or has no matching key; fall through to configured key.
892 }
893
894 // SSH key authentication with password
895 wxString sshKey = GetNextPublicKey();
896
897 if( sshKey.IsEmpty() )
898 {
899 wxLogTrace( traceGit, "Finished testing all possible ssh keys" );
900 m_testedTypes |= GIT_CREDENTIAL_SSH_KEY;
901 return GIT_PASSTHROUGH;
902 }
903
904 wxString sshPubKey = sshKey + ".pub";
905 wxString password = GetPassword();
906
907 wxLogTrace( traceGit, "Testing %s\n", sshKey );
908
909 if( git_credential_ssh_key_new( aOut, aUsername.mbc_str(), sshPubKey.mbc_str(), sshKey.mbc_str(),
910 password.mbc_str() ) != GIT_OK )
911 {
912 wxLogTrace( traceGit, "Failed to create SSH key credential for %s: %s",
913 aUsername, KIGIT_COMMON::GetLastGitError() );
914 return GIT_PASSTHROUGH;
915 }
916
917 return GIT_OK;
918}
919
920
921int KIGIT_COMMON::HandlePlaintextAuthentication( git_cred** aOut, const wxString& aUsername )
922{
923 wxString password = GetPassword();
924
925 git_credential_userpass_plaintext_new( aOut, aUsername.mbc_str(), password.mbc_str() );
926 m_testedTypes |= GIT_CREDENTIAL_USERPASS_PLAINTEXT;
927
928 return GIT_OK;
929}
930
931
932int KIGIT_COMMON::HandleSSHAgentAuthentication( git_cred** aOut, const wxString& aUsername )
933{
935
936 if( git_credential_ssh_key_from_agent( aOut, aUsername.mbc_str() ) != GIT_OK )
937 {
938 wxLogTrace( traceGit, "Failed to create SSH agent credential for %s: %s",
939 aUsername, KIGIT_COMMON::GetLastGitError() );
940 return GIT_PASSTHROUGH;
941 }
942
943 return GIT_OK;
944}
945
946
947extern "C" int fetchhead_foreach_cb( const char*, const char*,
948 const git_oid* aOID, unsigned int aIsMerge, void* aPayload )
949{
950 if( aIsMerge )
951 git_oid_cpy( (git_oid*) aPayload, aOID );
952
953 return 0;
954}
955
956
957extern "C" void clone_progress_cb( const char* aStr, size_t aLen, size_t aTotal, void* aPayload )
958{
959 KIGIT_REPO_MIXIN* parent = reinterpret_cast<KIGIT_REPO_MIXIN*>( aPayload );
960
961 wxString progressMessage( aStr );
962 parent->UpdateProgress( aLen, aTotal, progressMessage );
963}
964
965
966extern "C" int progress_cb( const char* str, int len, void* aPayload )
967{
968 KIGIT_REPO_MIXIN* parent = reinterpret_cast<KIGIT_REPO_MIXIN*>( aPayload );
969
970 if( parent->GetCommon()->IsCancelled() )
971 {
972 wxLogTrace( traceGit, "Progress CB cancelled" );
973 return GIT_EUSER;
974 }
975
976 wxString progressMessage( str, len );
977 parent->UpdateProgress( 0, 0, progressMessage );
978
979 return 0;
980}
981
982
983extern "C" int transfer_progress_cb( const git_transfer_progress* aStats, void* aPayload )
984{
985 KIGIT_REPO_MIXIN* parent = reinterpret_cast<KIGIT_REPO_MIXIN*>( aPayload );
986
987 wxString progressMessage = wxString::Format( _( "Received %u of %u objects" ),
988 aStats->received_objects,
989 aStats->total_objects );
990 if( parent->GetCommon()->IsCancelled() )
991 {
992 wxLogTrace( traceGit, "Transfer progress cancelled" );
993 return GIT_EUSER;
994 }
995
996 parent->UpdateProgress( aStats->received_objects, aStats->total_objects, progressMessage );
997
998 return 0;
999}
1000
1001
1002extern "C" int update_cb( const char* aRefname, const git_oid* aFirst, const git_oid* aSecond,
1003 void* aPayload )
1004{
1005 constexpr int cstring_len = 8;
1006 char a_str[cstring_len + 1];
1007 char b_str[cstring_len + 1];
1008
1009 KIGIT_REPO_MIXIN* parent = reinterpret_cast<KIGIT_REPO_MIXIN*>( aPayload );
1010 wxString status;
1011
1012 git_oid_tostr( b_str, cstring_len, aSecond );
1013
1014#if ( LIBGIT2_VER_MAJOR >= 1 ) || ( LIBGIT2_VER_MINOR >= 99 )
1015 if( !git_oid_is_zero( aFirst ) )
1016#else
1017 if( !git_oid_iszero( aFirst ) )
1018#endif
1019 {
1020 git_oid_tostr( a_str, cstring_len, aFirst );
1021 status = wxString::Format( _( "* [updated] %s..%s %s" ), a_str, b_str, aRefname );
1022 }
1023 else
1024 {
1025 status = wxString::Format( _( "* [new] %s %s" ), b_str, aRefname );
1026 }
1027
1028 parent->UpdateProgress( 0, 0, status );
1029
1030 return 0;
1031}
1032
1033
1034extern "C" int push_transfer_progress_cb( unsigned int aCurrent, unsigned int aTotal, size_t aBytes,
1035 void* aPayload )
1036{
1037 long long progress = 100;
1038 KIGIT_REPO_MIXIN* parent = reinterpret_cast<KIGIT_REPO_MIXIN*>( aPayload );
1039
1040 if( aTotal != 0 )
1041 {
1042 progress = ( aCurrent * 100ll ) / aTotal;
1043 }
1044
1045 wxString progressMessage = wxString::Format( _( "Writing objects: %lld%% (%u/%u), %zu bytes" ),
1046 progress, aCurrent, aTotal, aBytes );
1047 parent->UpdateProgress( aCurrent, aTotal, progressMessage );
1048
1049 return 0;
1050}
1051
1052
1053extern "C" int push_update_reference_cb( const char* aRefname, const char* aStatus, void* aPayload )
1054{
1055 KIGIT_REPO_MIXIN* parent = reinterpret_cast<KIGIT_REPO_MIXIN*>( aPayload );
1056 wxString status( aStatus );
1057
1058 if( !status.IsEmpty() )
1059 {
1060 wxString statusMessage = wxString::Format( _( "* [rejected] %s (%s)" ), aRefname, aStatus );
1061 parent->UpdateProgress( 0, 0, statusMessage );
1062 }
1063 else
1064 {
1065 wxString statusMessage = wxString::Format( _( "[updated] %s" ), aRefname );
1066 parent->UpdateProgress( 0, 0, statusMessage );
1067 }
1068
1069 return 0;
1070}
1071
1072
1073extern "C" int credentials_cb( git_cred** aOut, const char* aUrl, const char* aUsername,
1074 unsigned int aAllowedTypes, void* aPayload )
1075{
1076 KIGIT_REPO_MIXIN* parent = reinterpret_cast<KIGIT_REPO_MIXIN*>( aPayload );
1077 KIGIT_COMMON* common = parent->GetCommon();
1078
1079 wxLogTrace( traceGit, "Credentials callback for %s, testing %d", aUrl, aAllowedTypes );
1080
1082 {
1083 wxLogTrace( traceGit, "Local repository, no credentials needed" );
1084 return GIT_PASSTHROUGH;
1085 }
1086
1087 if( aAllowedTypes & GIT_CREDENTIAL_USERNAME
1088 && !( parent->TestedTypes() & GIT_CREDENTIAL_USERNAME ) )
1089 {
1090 wxString username = parent->GetUsername().Trim().Trim( false );
1091 wxLogTrace( traceGit, "Username credential for %s at %s with allowed type %d",
1092 username, aUrl, aAllowedTypes );
1093
1094 if( git_credential_username_new( aOut, username.ToStdString().c_str() ) != GIT_OK )
1095 {
1096 wxLogTrace( traceGit, "Failed to create username credential for %s: %s",
1097 username, KIGIT_COMMON::GetLastGitError() );
1098 }
1099 else
1100 {
1101 wxLogTrace( traceGit, "Created username credential for %s", username );
1102 }
1103
1104 parent->TestedTypes() |= GIT_CREDENTIAL_USERNAME;
1105 }
1107 && ( aAllowedTypes & GIT_CREDENTIAL_USERPASS_PLAINTEXT )
1108 && !( parent->TestedTypes() & GIT_CREDENTIAL_USERPASS_PLAINTEXT )
1109 && !parent->GetUsername().IsEmpty() )
1110 {
1111 // Plaintext authentication
1112 wxLogTrace( traceGit, "Plaintext authentication for %s at %s with allowed type %d",
1113 parent->GetUsername(), aUrl, aAllowedTypes );
1114 return common->HandlePlaintextAuthentication( aOut, parent->GetUsername() );
1115 }
1117 && ( aAllowedTypes & GIT_CREDENTIAL_SSH_KEY )
1118 && !( parent->TestedTypes() & GIT_CREDENTIAL_SSH_KEY ) )
1119 {
1120 // SSH key authentication
1121 int result = common->HandleSSHKeyAuthentication( aOut, parent->GetUsername() );
1122
1123 // Translate exhausted-keys PASSTHROUGH into a proper auth error so the
1124 // retry loop runs and libgit2 doesn't emit "no callback set".
1125 if( result == GIT_PASSTHROUGH )
1126 {
1127 git_error_clear();
1128 git_error_set_str( GIT_ERROR_NET, _( "Unable to authenticate" ).mbc_str() );
1129 common->SetAuthFailure();
1130 return GIT_EAUTH;
1131 }
1132
1133 return result;
1134 }
1135 else
1136 {
1137 // If we didn't find anything to try, then we don't have a callback set that the
1138 // server likes
1139 if( !parent->TestedTypes() )
1140 return GIT_PASSTHROUGH;
1141
1142 git_error_clear();
1143 git_error_set_str( GIT_ERROR_NET, _( "Unable to authenticate" ).mbc_str() );
1144
1145 // Otherwise, we did try something but we failed, so return an authentication error
1146 common->SetAuthFailure();
1147 return GIT_EAUTH;
1148 }
1149
1150 return GIT_OK;
1151};
1152
1153
1154namespace KIGIT
1155{
1156
1157git_tree* ResolveRefToTree( git_repository* aRepo, const wxString& aRef )
1158{
1159 if( !aRepo )
1160 return nullptr;
1161
1162 git_object* obj = nullptr;
1163 std::string refStr( aRef.ToUTF8() );
1164
1165 if( git_revparse_single( &obj, aRepo, refStr.c_str() ) != 0 )
1166 {
1167 wxLogTrace( traceGit, "git_revparse_single failed for ref '%s': %s",
1169 return nullptr;
1170 }
1171
1172 KIGIT::GitObjectPtr objPtr( obj );
1173 git_tree* tree = nullptr;
1174
1175 if( git_object_peel( reinterpret_cast<git_object**>( &tree ), obj, GIT_OBJECT_TREE ) != 0 )
1176 {
1177 wxLogTrace( traceGit, "git_object_peel to tree failed for '%s': %s",
1179 return nullptr;
1180 }
1181
1182 return tree;
1183}
1184
1185
1186void CollectDiffDeltas( git_diff* aDiff,
1187 const std::function<void( const git_diff_delta& )>& aCallback )
1188{
1189 if( !aDiff )
1190 return;
1191
1192 std::size_t numDeltas = git_diff_num_deltas( aDiff );
1193
1194 for( std::size_t ii = 0; ii < numDeltas; ++ii )
1195 {
1196 const git_diff_delta* delta = git_diff_get_delta( aDiff, ii );
1197
1198 if( !delta )
1199 continue;
1200
1201 aCallback( *delta );
1202 }
1203}
1204
1205} // namespace KIGIT
virtual void UpdateProgress(int aCurrent, int aTotal, const wxString &aMessage)
std::mutex m_gitActionMutex
git_repository * m_repo
std::vector< wxString > GetBranchNames() const
GIT_CONN_TYPE GetConnType() const
unsigned m_testedTypes
static wxString GetLastGitError()
wxString GetCurrentBranchName() const
wxString GetGitRootDirectory() const
void SetSSHKey(const wxString &aSSHKey)
bool IsCancelled() const
static const unsigned KIGIT_CREDENTIAL_SSH_AGENT
int HandlePlaintextAuthentication(git_cred **aOut, const wxString &aUsername)
std::vector< wxString > GetProjectDirs()
Return a vector of project files in the repository.
wxString GetRemoteNameOrDefault() const
Returns GetRemotename() when non-empty, otherwise "origin".
git_repository * GetRepo() const
wxString GetProjectDir() const
Get the project directory path.
wxString GetNextPublicKey()
KIGIT_COMMON(git_repository *aRepo)
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 ...
std::vector< wxString > m_publicKeys
wxString m_projectDir
bool HasPushAndPullRemote() const
GIT_CONN_TYPE m_connType
void UpdateCurrentBranchInfo()
bool HasLocalCommits() const
wxString GetPassword()
int HandleSSHAgentAuthentication(git_cred **aOut, const wxString &aUsername)
wxString GetUpstreamShorthand() const
Returns the upstream shorthand for the current branch (e.g.
wxString GetRemotename() const
int HandleSSHKeyAuthentication(git_cred **aOut, const wxString &aUsername)
wxString GetUsername() const
Get the username.
unsigned & TestedTypes()
Return the connection types that have been tested for authentication.
KIGIT_COMMON * GetCommon() const
Get the common object.
KIGIT_COMMON::GIT_CONN_TYPE GetConnType() const
Get the connection type.
#define _(s)
const wxChar *const traceGit
Flag to enable Git debugging output.
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)
int transfer_progress_cb(const git_transfer_progress *aStats, void *aPayload)
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)
std::unique_ptr< git_tree, decltype([](git_tree *aTree) { git_tree_free(aTree); })> GitTreePtr
A unique pointer for git_tree objects with automatic cleanup.
git_tree * ResolveRefToTree(git_repository *aRepo, const wxString &aRef)
Resolve a string ref (branch name, short OID, full OID, tag) to its tree.
std::unique_ptr< git_revwalk, decltype([](git_revwalk *aWalker) { git_revwalk_free(aWalker); })> GitRevWalkPtr
A unique pointer for git_revwalk objects with automatic cleanup.
std::unique_ptr< git_commit, decltype([](git_commit *aCommit) { git_commit_free(aCommit); })> GitCommitPtr
A unique pointer for git_commit objects with automatic cleanup.
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_reference, decltype([](git_reference *aRef) { git_reference_free(aRef); })> GitReferencePtr
A unique pointer for git_reference objects with automatic cleanup.
std::unique_ptr< git_strarray, decltype([](git_strarray *aArray) { git_strarray_free(aArray); })> GitStrArrayPtr
A unique pointer for git_strarray objects with automatic cleanup.
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_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.
bool GetSecret(const wxString &aService, const wxString &aKey, wxString &aSecret)
std::string path
wxString result
Test unit parsing edge cases and error handling.
int delta
wxLogTrace helper definitions.