KiCad PCB EDA Suite
Loading...
Searching...
No Matches
dialog_git_repository.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
21#include <confirm.h>
23
24#include <git2.h>
27#include <git/kigit_ssh_keys.h>
28#include <git/git_repo_mixin.h>
29#include <gestfich.h>
30
31#include <cerrno>
32#include <cstring>
33#include <fstream>
34
35#include <wx/clipbrd.h>
36#include <wx/msgdlg.h>
37#include <wx/regex.h>
38#include <wx/stdpaths.h>
39
40
42 wxString aURL ) :
44 m_repository( aRepository ),
45 m_incomeURL( aURL ),
46 m_prevFile( wxEmptyString ),
47 m_tempRepo( false )
48{
49 OptOut( this );
50
51 m_txtURL->SetFocus();
52
53 if( !m_repository )
54 {
55 // Make a temporary repository to test the connection
56 m_tempRepo = true;
57 m_tempPath = wxFileName::CreateTempFileName( "kicadtestrepo" );
58
59 git_repository_init_options options;
60 git_repository_init_init_options( &options, GIT_REPOSITORY_INIT_OPTIONS_VERSION );
61 options.flags = GIT_REPOSITORY_INIT_MKPATH | GIT_REPOSITORY_INIT_NO_REINIT;
62 git_repository_init_ext( &m_repository, m_tempPath.ToStdString().c_str(), &options );
63 }
64
65 SetupStandardButtons( { { wxID_HELP, _( "Test Connection" ) } } );
66 Layout();
68
69 m_txtURL->Bind( wxEVT_TEXT,
70 [this]( wxCommandEvent& aEvent )
71 {
74 aEvent.Skip();
75 } );
76}
77
78
80{
81 if( m_tempRepo )
82 {
83 git_repository_free( m_repository );
85 }
86}
87
88
90{
91 wxClipboardLocker lock;
92 if( !lock )
93 return false;
94
95 if( !wxTheClipboard->IsSupported( wxDF_TEXT ) )
96 return false;
97
98 wxTextDataObject textData;
99 if( !wxTheClipboard->GetData( textData ) )
100 return false;
101
102 wxString clipboardText = textData.GetText();
103 if( clipboardText.empty() )
104 return false;
105
107
108 if( parsed.connType != KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_LOCAL && !parsed.hostname.IsEmpty()
109 && clipboardText.find_first_of( " \t\r\n" ) == wxString::npos )
110 {
111 m_txtURL->SetValue( clipboardText );
112 return true;
113 }
114
115 return false;
116}
117
118
119void DIALOG_GIT_REPOSITORY::setDefaultSSHKey( const wxString& aHost )
120{
121 std::vector<wxString> keys = KIGIT::DefaultSSHKeyCandidates( KIGIT::HomeDir(), aHost );
122
123 if( keys.empty() )
124 return;
125
126 // Unticked greys out the picker and passphrase, hiding the key the connection will use
127 m_cbCustom->SetValue( true );
128 m_fpSSHKey->SetFileName( keys.front() );
129
130 wxFileDirPickerEvent evt;
131 evt.SetPath( keys.front() );
132 OnFileUpdated( evt );
133}
134
135
136void DIALOG_GIT_REPOSITORY::onCbCustom( wxCommandEvent& event )
137{
139 event.Skip();
140}
141
142
143void DIALOG_GIT_REPOSITORY::OnUpdateUI( wxUpdateUIEvent& event )
144{
145 // event.Enable( !m_txtName->GetValue().IsEmpty() && !m_txtURL->GetValue().IsEmpty() );
146}
147
148
150{
151 if( aEncrypted )
152 {
153 m_txtPassword->Enable();
154 m_txtPassword->SetToolTip( _( "Enter the password for the SSH key" ) );
155 }
156 else
157 {
158 m_txtPassword->SetValue( wxEmptyString );
159 m_txtPassword->SetToolTip( wxEmptyString );
160 m_txtPassword->Disable();
161 }
162}
163
164
165static wxString get_repo_name( const wxString& aRepoAddr )
166{
167 wxString addr = aRepoAddr;
168
169 // Strip GitHub/GitLab web-UI path suffixes so that pasting a browser URL
170 // (e.g. .../repo/tree/master, .../repo/blob/main/README.md, .../repo/pulls)
171 // still gives the repository name rather than a branch/page name.
172 static wxRegEx webSuffix(
173 R"((/-)?/(tree|blob|commits?|raw|releases|tags|branches|pulls|pull|issues|merge_requests|wiki|actions)/.*$)",
174 wxRE_ADVANCED );
175
176 webSuffix.ReplaceAll( &addr, wxEmptyString );
177
178 while( addr.EndsWith( "/" ) )
179 addr.RemoveLast();
180
181 if( addr.EndsWith( ".git" ) )
182 addr.RemoveLast( 4 );
183
184 size_t last_slash = addr.find_last_of( "/\\" );
185
186 if( last_slash == wxString::npos )
187 return addr.AfterLast( ':' );
188
189 return addr.substr( last_slash + 1 );
190}
191
192
193void DIALOG_GIT_REPOSITORY::OnLocationExit( wxFocusEvent& event )
194{
197 event.Skip();
198}
199
200
202{
203 wxString url = m_txtURL->GetValue();
204
205 if( url.IsEmpty() )
206 return;
207
210 m_ConnType->SetSelection( static_cast<int>( parsed.connType ) );
211
212 if( parsed.connType == KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_SSH && !wasSSH && m_fpSSHKey->GetPath().IsEmpty() )
213 setDefaultSSHKey( parsed.hostname );
214
215 if( !parsed.username.IsEmpty() )
216 SetUsername( parsed.username );
217
218 if( !parsed.password.IsEmpty() )
219 SetPassword( parsed.password );
220
221 // Keep the SSH username and port in the address. Passwords belong in the password field.
222 if( parsed.url != url )
223 m_txtURL->ChangeValue( parsed.url );
224
225 m_txtName->SetValue( get_repo_name( parsed.url ) );
226}
227
228
229void DIALOG_GIT_REPOSITORY::OnTestClick( wxCommandEvent& event )
230{
231 wxString url = GetRepoURL();
232
233 if( wxString( url ).Trim().Trim( false ).IsEmpty() )
234 return;
235
236 wxString error;
237 wxString details;
238 bool success = false;
239 git_remote* remote = nullptr;
240 git_remote_callbacks callbacks;
241 git_remote_init_callbacks( &callbacks, GIT_REMOTE_CALLBACKS_VERSION );
242
243 // We track if we have already tried to connect.
244 // If we have, the server may come back to offer another connection
245 // type, so we need to keep track of how many times we have tried.
246
247 KIGIT_COMMON common( m_repository );
248 common.SetRemote( url );
249
250 // SetUsername clears the fetched-secret flag, so it comes first. The other order tests the
251 // keychain's secret instead of the one just typed.
252 common.SetUsername( GetUsername() );
253 common.SetPassword( m_txtPassword->GetValue() );
254 common.SetSSHKey( m_fpSSHKey->GetFileName().GetFullPath() );
255
256 KIGIT_REPO_MIXIN repoMixin( &common );
257 callbacks.payload = &repoMixin;
258 callbacks.credentials = credentials_cb;
259 callbacks.certificate_check = certificate_check_cb;
260
261 git_proxy_options proxyOpts;
262 git_proxy_init_options( &proxyOpts, GIT_PROXY_OPTIONS_VERSION );
263 proxyOpts.type = GIT_PROXY_AUTO;
264 proxyOpts.url = "";
265
266 int result = git_remote_create_anonymous( &remote, m_repository, url.utf8_str() );
267 KIGIT::GitRemotePtr remotePtr( remote );
268
269 if( result == GIT_OK )
270 {
271 // url.insteadOf may have rewritten the URL, which would otherwise drop the credentials typed here
272 if( const char* remoteUrl = git_remote_url( remote ) )
273 common.FollowRewrittenUrl( wxString::FromUTF8( remoteUrl ) );
274
275 bool retriedAfterTrust = false;
276
277 while( true )
278 {
279 repoMixin.ResetTransferState();
280 result = git_remote_connect( remote, GIT_DIRECTION_FETCH, &callbacks, &proxyOpts, nullptr );
281
282 if( result == GIT_OK )
283 {
284 git_remote_disconnect( remote );
285 success = true;
286 break;
287 }
288
290 KIGIT_OPERATION::CONNECT, wxEmptyString );
291 git_remote_disconnect( remote );
292 error = report.summary;
293 details = report.details;
294
295 std::optional<KIGIT::HOST_KEY_FAILURE> hostKey = repoMixin.GetHostKeyFailure();
296
297 if( !hostKey )
298 break;
299
300 // The prompt has told the user why, so a declined key ends the test here
301 if( !KIGIT::PromptToTrustHost( this, *hostKey, retriedAfterTrust ) )
302 return;
303
304 retriedAfterTrust = true;
305 }
306 }
307 else
308 {
309 KIGIT_FAILURE_CONTEXT context;
311 context.url = url;
312
313 KIGIT_FAILURE_REPORT report =
315 error = report.summary;
316 details = report.details;
317 }
318
319 if( !success )
320 {
321 DisplayErrorMessage( this, error, details );
322 return;
323 }
324
325 auto dlg = KICAD_MESSAGE_DIALOG( this, _( "Connection successful" ), _( "Test Connection" ),
326 wxOK | wxICON_INFORMATION );
327 dlg.ShowModal();
328}
329
330
331void DIALOG_GIT_REPOSITORY::OnFileUpdated( wxFileDirPickerEvent& aEvent )
332{
333 wxString file = aEvent.GetPath();
334
335 if( file.ends_with( wxS( ".pub" ) ) )
336 file = file.Left( file.size() - 4 );
337
338 std::ifstream ifs( file.fn_str() );
339
340 if( !ifs.good() || !ifs.is_open() )
341 {
342 DisplayErrorMessage( this, wxString::Format( _( "Could not open private key '%s'" ), file ),
343 wxString::Format( "%s: %d", std::strerror( errno ), errno ) );
344 return;
345 }
346
347 std::string keyText( ( std::istreambuf_iterator<char>( ifs ) ), std::istreambuf_iterator<char>() );
348
349 ifs.close();
350
351 wxString key = wxString::FromUTF8( keyText );
352
353 if( !key.BeforeFirst( '\n' ).Contains( wxS( "PRIVATE KEY" ) ) )
354 {
355 DisplayErrorMessage( this, _( "Invalid SSH Key" ),
356 _( "The selected file is not a valid SSH private key" ) );
357 CallAfter( [this] { SetRepoSSHPath( m_prevFile ); } );
358 return;
359 }
360
362
363 wxString pubFile = file + wxS( ".pub" );
364 std::ifstream pubIfs( pubFile.fn_str() );
365
366 if( !pubIfs.good() || !pubIfs.is_open() )
367 {
368 DisplayErrorMessage( this, wxString::Format( _( "Could not open public key '%s'" ),
369 file + ".pub" ),
370 wxString::Format( "%s: %d", std::strerror( errno ), errno ) );
371 aEvent.SetPath( wxEmptyString );
372 CallAfter( [this] { SetRepoSSHPath( m_prevFile ); } );
373 return;
374 }
375
376 m_prevFile = file;
377 pubIfs.close();
378}
379
380
381void DIALOG_GIT_REPOSITORY::OnOKClick( wxCommandEvent& event )
382{
383 // Save the repository details
384
385 if( m_txtURL->GetValue().IsEmpty() )
386 {
387 DisplayErrorMessage( this, _( "Missing information" ),
388 _( "Please enter a URL for the repository" ) );
389 return;
390 }
391
392 EndModal( wxID_OK );
393}
394
395
397{
398 if( m_ConnType->GetSelection() == static_cast<int>( KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_LOCAL ) )
399 {
400 m_panelAuth->Enable( false );
401 }
402 else
403 {
404 m_panelAuth->Enable( true );
405
406 if( m_ConnType->GetSelection() == static_cast<int>( KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_SSH ) )
407 {
409 m_cbCustom->Enable( true );
410 m_fpSSHKey->Enable( m_cbCustom->IsChecked() );
411
412 // Edit an embedded SSH username in the address so the two fields cannot disagree.
413 m_txtUsername->Enable( m_cbCustom->IsChecked() && parsed.username.IsEmpty() );
414 m_txtPassword->Enable( m_cbCustom->IsChecked() );
415 m_labelPass1->SetLabel( _( "SSH key password:" ) );
416 }
417 else
418 {
419 m_cbCustom->Enable( false );
420 m_fpSSHKey->Enable( false );
421 m_txtUsername->Enable( true );
422 m_txtPassword->Enable( true );
423 m_labelPass1->SetLabel( _( "Password:" ) );
424 }
425 }
426}
427
428
429void DIALOG_GIT_REPOSITORY::OnSelectConnType( wxCommandEvent& event )
430{
432}
433
435{
436 if( !m_incomeURL.empty() )
437 m_txtURL->SetValue( m_incomeURL );
438 else
440
441 if( !m_txtURL->GetValue().IsEmpty() )
443 else
444 m_ConnType->SetSelection( static_cast<int>( KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_LOCAL ) );
445
447
448 return true;
449}
DIALOG_GIT_REPOSITORY_BASE(wxWindow *parent, wxWindowID id=wxID_ANY, const wxString &title=_("Git Repository"), const wxPoint &pos=wxDefaultPosition, const wxSize &size=wxSize(-1,-1), long style=wxCAPTION|wxDEFAULT_DIALOG_STYLE|wxRESIZE_BORDER)
void OnSelectConnType(wxCommandEvent &event) override
void SetRepoSSHPath(const wxString &aPath)
KIGIT_COMMON::GIT_CONN_TYPE GetRepoType() const
void OnFileUpdated(wxFileDirPickerEvent &event) override
void SetPassword(const wxString &aPassword)
void onCbCustom(wxCommandEvent &event) override
void SetEncrypted(bool aEncrypted=true)
void OnLocationExit(wxFocusEvent &event) override
DIALOG_GIT_REPOSITORY(wxWindow *aParent, git_repository *aRepository, wxString aURL=wxEmptyString)
void setDefaultSSHKey(const wxString &aHost)
void OnTestClick(wxCommandEvent &event) override
void OnOKClick(wxCommandEvent &event) override
void SetUsername(const wxString &aUsername)
virtual bool TransferDataToWindow() override
void OnUpdateUI(wxUpdateUIEvent &event) override
void OptOut(wxWindow *aWindow)
Opt out of control state saving.
void SetupStandardButtons(std::map< int, wxString > aLabels={})
void finishDialogSettings()
In all dialogs, we must call the same functions to fix minimal dlg size, the default position and per...
The project's git session.
void FollowRewrittenUrl(const wxString &aRawUrl)
Bind the session to a URL that url.insteadOf rewrote, keeping the username, password and key entered ...
void SetSSHKey(const wxString &aSSHKey)
void SetUsername(const wxString &aUsername)
static KIGIT_ERROR_INFO GetLastGitErrorInfo(int aCode)
The error libgit2 holds for a call that returned aCode.
static REMOTE_URL ParseRemoteURL(const wxString &aURL)
void SetPassword(const wxString &aPassword)
void SetRemote(const wxString &aRemote)
std::optional< KIGIT::HOST_KEY_FAILURE > GetHostKeyFailure() const
The host key this operation's transfer refused, for the UI to offer to trust or to report.
KIGIT_FAILURE_REPORT ReportTransferFailure(const KIGIT_ERROR_INFO &aError, KIGIT_OPERATION aOperation, const wxString &aRemoteName)
Classify a failed libgit2 transfer call and queue its summary and details.
void ResetTransferState()
Start a transfer with nothing left from an earlier one on this handler or session: no keys or methods...
void DisplayErrorMessage(wxWindow *aParent, const wxString &aText, const wxString &aExtraInfo)
Display an error message with aMessage.
Definition confirm.cpp:217
This file is part of the common library.
#define KICAD_MESSAGE_DIALOG
Definition confirm.h:48
static wxString get_repo_name(const wxString &aRepoAddr)
#define _(s)
bool RmDirRecursive(const wxString &aFileName, wxString *aErrors)
Remove the directory aDirName and all its contents including subdirectories and their files.
Definition gestfich.cpp:457
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 credentials_cb(git_cred **aOut, const char *aUrl, const char *aUsername, unsigned int aAllowedTypes, void *aPayload)
bool IsPrivateKeyEncrypted(const wxString &aKeyText)
Report whether an SSH private key is protected by a passphrase.
bool PromptToTrustHost(wxWindow *aParent, const HOST_KEY_FAILURE &aFailure, bool aRetriedAfterTrust)
Ask whether to trust a server seen for the first time, and remember its key when the user agrees.
APIEXPORT wxString HomeDir()
The user's home folder, where ssh looks for its config and default keys.
APIEXPORT KIGIT_FAILURE_REPORT DescribeGitFailure(const KIGIT_ERROR_INFO &aError, const KIGIT_AUTH_ATTEMPT &aAuth, const KIGIT_FAILURE_CONTEXT &aContext)
Turn a failed transfer into a message the user can act on.
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.
A transport address with any embedded credentials extracted.
wxString url
Keeps the SSH username, scheme and port, but never a password.
What the failed operation was doing, as far as the message needs it.
KIGIT_OPERATION operation
wxString url
The address connected to. Messages show only its host and its stripped form.
wxString summary
What happened and what to do next, in the user's terms. Never raw text.
wxString details
The raw error, the server's text and the address without its password.
wxString result
Test unit parsing edge cases and error handling.