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