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>
22
23#include <git2.h>
26#include <git/git_repo_mixin.h>
27#include <gestfich.h>
28
29#include <cerrno>
30#include <cstring>
31#include <fstream>
32
33#include <wx/clipbrd.h>
34#include <wx/msgdlg.h>
35#include <wx/regex.h>
36#include <wx/stdpaths.h>
37
38
39DIALOG_GIT_REPOSITORY::DIALOG_GIT_REPOSITORY( wxWindow* aParent, git_repository* aRepository,
40 wxString aURL ) :
42 m_repository( aRepository ),
43 m_incomeURL( aURL ),
44 m_prevFile( wxEmptyString ),
45 m_tempRepo( false )
46{
47 m_txtURL->SetFocus();
48
49 if( !m_repository )
50 {
51 // Make a temporary repository to test the connection
52 m_tempRepo = true;
53 m_tempPath = wxFileName::CreateTempFileName( "kicadtestrepo" );
54
55 git_repository_init_options options;
56 git_repository_init_init_options( &options, GIT_REPOSITORY_INIT_OPTIONS_VERSION );
57 options.flags = GIT_REPOSITORY_INIT_MKPATH | GIT_REPOSITORY_INIT_NO_REINIT;
58 git_repository_init_ext( &m_repository, m_tempPath.ToStdString().c_str(), &options );
59 }
60
61 SetupStandardButtons( { { wxID_HELP, _( "Test Connection" ) } } );
62 Layout();
64
65 m_txtURL->Bind( wxEVT_TEXT,
66 [this]( wxCommandEvent& aEvent )
67 {
70 aEvent.Skip();
71 } );
72}
73
74
76{
77 if( m_tempRepo )
78 {
79 git_repository_free( m_repository );
81 }
82}
83
84
86{
87 wxClipboardLocker lock;
88 if( !lock )
89 return false;
90
91 if( !wxTheClipboard->IsSupported( wxDF_TEXT ) )
92 return false;
93
94 wxTextDataObject textData;
95 if( !wxTheClipboard->GetData( textData ) )
96 return false;
97
98 wxString clipboardText = textData.GetText();
99 if( clipboardText.empty() )
100 return false;
101
102 if( std::get<0>( isValidHTTPS( clipboardText ) )
103 || std::get<0>( isValidSSH( clipboardText ) ) )
104 {
105 m_txtURL->SetValue( clipboardText );
106 return true;
107 }
108
109 return false;
110}
111
112
114{
115 wxFileName sshKey;
116 sshKey.SetPath( wxGetUserHome() );
117 wxString retval;
118
119 sshKey.AppendDir( ".ssh" );
120 sshKey.SetFullName( "id_rsa" );
121
122 if( sshKey.FileExists() )
123 {
124 retval = sshKey.GetFullPath();
125 }
126 else if( sshKey.SetFullName( "id_dsa" ); sshKey.FileExists() )
127 {
128 retval = sshKey.GetFullPath();
129 }
130 else if( sshKey.SetFullName( "id_ecdsa" ); sshKey.FileExists() )
131 {
132 retval = sshKey.GetFullPath();
133 }
134
135 if( !retval.empty() )
136 {
137 m_fpSSHKey->SetFileName( retval );
138 wxFileDirPickerEvent evt;
139 evt.SetPath( retval );
140 OnFileUpdated( evt );
141 }
142}
143
144
145void DIALOG_GIT_REPOSITORY::onCbCustom( wxCommandEvent& event )
146{
148 event.Skip();
149}
150
151
152void DIALOG_GIT_REPOSITORY::OnUpdateUI( wxUpdateUIEvent& event )
153{
154 // event.Enable( !m_txtName->GetValue().IsEmpty() && !m_txtURL->GetValue().IsEmpty() );
155}
156
157
159{
160 if( aEncrypted )
161 {
162 m_txtPassword->Enable();
163 m_txtPassword->SetToolTip( _( "Enter the password for the SSH key" ) );
164 }
165 else
166 {
167 m_txtPassword->SetValue( wxEmptyString );
168 m_txtPassword->SetToolTip( wxEmptyString );
169 m_txtPassword->Disable();
170 }
171}
172
173
174std::tuple<bool,wxString,wxString,wxString> DIALOG_GIT_REPOSITORY::isValidHTTPS( const wxString& url )
175{
176 wxRegEx regex( R"((https?:\/\/)(([^:]+)(:([^@]+))?@)?([^\/]+\/[^\s]+))" );
177
178 if( regex.Matches( url ) )
179 {
180 wxString username = regex.GetMatch( url, 3 );
181 wxString password = regex.GetMatch( url, 5 );
182 wxString repoAddress = regex.GetMatch( url, 1 ) + regex.GetMatch( url, 6 );
183 return std::make_tuple( true, username, password, repoAddress );
184 }
185
186 return std::make_tuple( false, "", "", "" );
187}
188
189
190std::tuple<bool,wxString, wxString> DIALOG_GIT_REPOSITORY::isValidSSH( const wxString& url )
191{
192 wxRegEx regex( R"((?:ssh:\/\/)?([^@]+)@([^\/]+\/[^\s]+))" );
193
194 if( regex.Matches( url ) )
195 {
196 wxString username = regex.GetMatch( url, 1 );
197 wxString repoAddress = regex.GetMatch( url, 2 );
198 return std::make_tuple( true, username, repoAddress );
199 }
200
201 return std::make_tuple( false, "", "" );
202}
203
204
205static wxString get_repo_name( wxString& aRepoAddr )
206{
207 wxString addr = aRepoAddr;
208
209 // Strip GitHub/GitLab web-UI path suffixes so that pasting a browser URL
210 // (e.g. .../repo/tree/master, .../repo/blob/main/README.md, .../repo/pulls)
211 // still gives the repository name rather than a branch/page name.
212 static wxRegEx webSuffix(
213 R"((/-)?/(tree|blob|commits?|raw|releases|tags|branches|pulls|pull|issues|merge_requests|wiki|actions)/.*$)",
214 wxRE_ADVANCED );
215
216 webSuffix.ReplaceAll( &addr, wxEmptyString );
217
218 while( addr.EndsWith( "/" ) )
219 addr.RemoveLast();
220
221 if( addr.EndsWith( ".git" ) )
222 addr.RemoveLast( 4 );
223
224 size_t last_slash = addr.find_last_of( '/' );
225
226 if( last_slash == wxString::npos )
227 return addr;
228
229 return addr.substr( last_slash + 1 );
230}
231
232
233void DIALOG_GIT_REPOSITORY::OnLocationExit( wxFocusEvent& event )
234{
237 event.Skip();
238}
239
240
242{
243 wxString url = m_txtURL->GetValue();
244
245 if( url.IsEmpty() )
246 return;
247
248 if( url.Contains( "https://" ) || url.Contains( "http://" ) )
249 {
250 auto [valid, username, password, repoAddress] = isValidHTTPS( url );
251
252 if( valid )
253 {
254 m_fullURL = url;
255 m_ConnType->SetSelection( static_cast<int>( KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_HTTPS ) );
256
257 if( !username.IsEmpty() )
258 SetUsername( username );
259
260 if( !password.IsEmpty() )
261 SetPassword( password );
262
263 m_txtURL->ChangeValue( repoAddress );
264 m_txtName->SetValue( get_repo_name( repoAddress ) );
265 }
266 }
267 else if( url.Contains( "ssh://" ) || url.Contains( "git@" ) )
268 {
269 auto [valid, username, repoAddress] = isValidSSH( url );
270
271 if( valid )
272 {
273 m_fullURL = url;
274 m_ConnType->SetSelection( static_cast<int>( KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_SSH ) );
275 m_txtUsername->SetValue( username );
276 m_txtURL->ChangeValue( repoAddress );
277 m_cbCustom->SetValue( false );
278
279 m_txtName->SetValue( get_repo_name( repoAddress ) );
280
282 }
283 }
284 else
285 {
286 if( m_fullURL.IsEmpty() )
287 m_fullURL = url;
288
289 // URL without user@ prefix (e.g. "host:path/repo.git")
290 size_t colonPos = url.find( ':' );
291 size_t slashPos = url.find( '/' );
292
293 if( colonPos != wxString::npos && ( slashPos == wxString::npos || colonPos < slashPos ) )
294 {
295 m_ConnType->SetSelection( static_cast<int>( KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_SSH ) );
297
298 m_txtName->SetValue( get_repo_name( url ) );
299 }
300 }
301}
302
303
304void DIALOG_GIT_REPOSITORY::OnTestClick( wxCommandEvent& event )
305{
306 if( m_txtURL->GetValue().Trim().Trim( false ).IsEmpty() )
307 return;
308
309 wxString error;
310 bool success = false;
311 git_remote* remote = nullptr;
312 git_remote_callbacks callbacks;
313 git_remote_init_callbacks( &callbacks, GIT_REMOTE_CALLBACKS_VERSION );
314
315 // We track if we have already tried to connect.
316 // If we have, the server may come back to offer another connection
317 // type, so we need to keep track of how many times we have tried.
318
319 wxString fullURL = m_fullURL.IsEmpty() ? m_txtURL->GetValue() : m_fullURL;
320
321 KIGIT_COMMON common( m_repository );
322 common.SetRemote( fullURL );
323 common.SetPassword( m_txtPassword->GetValue() );
324 common.SetUsername( m_txtUsername->GetValue() );
325 common.SetSSHKey( m_fpSSHKey->GetFileName().GetFullPath() );
326
327 KIGIT_REPO_MIXIN repoMixin( &common );
328 callbacks.payload = &repoMixin;
329 callbacks.credentials = credentials_cb;
330
331 git_proxy_options proxyOpts;
332 git_proxy_init_options( &proxyOpts, GIT_PROXY_OPTIONS_VERSION );
333 proxyOpts.type = GIT_PROXY_AUTO;
334 proxyOpts.url = "";
335
336 git_remote_create_anonymous( &remote, m_repository, fullURL.mbc_str() );
337 KIGIT::GitRemotePtr remotePtr( remote );
338
339 if( git_remote_connect( remote, GIT_DIRECTION_FETCH, &callbacks, &proxyOpts, nullptr ) == GIT_OK )
340 success = true;
341 else
343
344 git_remote_disconnect( remote );
345
346 auto dlg = KICAD_MESSAGE_DIALOG( this, wxEmptyString, _( "Test Connection" ),
347 wxOK | wxICON_INFORMATION );
348
349 if( success )
350 {
351 dlg.SetMessage( _( "Connection successful" ) );
352 }
353 else
354 {
355 dlg.SetMessage( wxString::Format( _( "Could not connect to '%s' " ),
356 m_txtURL->GetValue() ) );
357 dlg.SetExtendedMessage( error );
358 }
359
360 dlg.ShowModal();
361}
362
363
364void DIALOG_GIT_REPOSITORY::OnFileUpdated( wxFileDirPickerEvent& aEvent )
365{
366 wxString file = aEvent.GetPath();
367
368 if( file.ends_with( wxS( ".pub" ) ) )
369 file = file.Left( file.size() - 4 );
370
371 std::ifstream ifs( file.fn_str() );
372
373 if( !ifs.good() || !ifs.is_open() )
374 {
375 DisplayErrorMessage( this, wxString::Format( _( "Could not open private key '%s'" ), file ),
376 wxString::Format( "%s: %d", std::strerror( errno ), errno ) );
377 return;
378 }
379
380 std::string line;
381 std::getline( ifs, line );
382
383 bool isValid = ( line.find( "PRIVATE KEY" ) != std::string::npos );
384 bool isEncrypted = ( line.find( "ENCRYPTED" ) != std::string::npos );
385
386 if( !isValid )
387 {
388 DisplayErrorMessage( this, _( "Invalid SSH Key" ),
389 _( "The selected file is not a valid SSH private key" ) );
390 CallAfter( [this] { SetRepoSSHPath( m_prevFile ); } );
391 return;
392 }
393
394 if( isEncrypted )
395 {
396 m_txtPassword->Enable();
397 m_txtPassword->SetToolTip( _( "Enter the password for the SSH key" ) );
398 }
399 else
400 {
401 m_txtPassword->SetValue( wxEmptyString );
402 m_txtPassword->SetToolTip( wxEmptyString );
403 m_txtPassword->Disable();
404 }
405
406 ifs.close();
407
408 wxString pubFile = file + wxS( ".pub" );
409 std::ifstream pubIfs( pubFile.fn_str() );
410
411 if( !pubIfs.good() || !pubIfs.is_open() )
412 {
413 DisplayErrorMessage( this, wxString::Format( _( "Could not open public key '%s'" ),
414 file + ".pub" ),
415 wxString::Format( "%s: %d", std::strerror( errno ), errno ) );
416 aEvent.SetPath( wxEmptyString );
417 CallAfter( [this] { SetRepoSSHPath( m_prevFile ); } );
418 return;
419 }
420
421 m_prevFile = file;
422 pubIfs.close();
423}
424
425
426void DIALOG_GIT_REPOSITORY::OnOKClick( wxCommandEvent& event )
427{
428 // Save the repository details
429
430 if( m_txtURL->GetValue().IsEmpty() )
431 {
432 DisplayErrorMessage( this, _( "Missing information" ),
433 _( "Please enter a URL for the repository" ) );
434 return;
435 }
436
437 EndModal( wxID_OK );
438}
439
440
442{
443 if( m_ConnType->GetSelection() == static_cast<int>( KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_LOCAL ) )
444 {
445 m_panelAuth->Enable( false );
446 }
447 else
448 {
449 m_panelAuth->Enable( true );
450
451 if( m_ConnType->GetSelection() == static_cast<int>( KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_SSH ) )
452 {
453 m_cbCustom->Enable( true );
454 m_fpSSHKey->Enable( m_cbCustom->IsChecked() );
455 m_txtUsername->Enable( m_cbCustom->IsChecked() );
456 m_txtPassword->Enable( m_cbCustom->IsChecked() );
457 m_labelPass1->SetLabel( _( "SSH key password:" ) );
458 }
459 else
460 {
461 m_cbCustom->Enable( false );
462 m_fpSSHKey->Enable( false );
463 m_txtUsername->Enable( true );
464 m_txtPassword->Enable( true );
465 m_labelPass1->SetLabel( _( "Password:" ) );
466 }
467 }
468}
469
470
471void DIALOG_GIT_REPOSITORY::OnSelectConnType( wxCommandEvent& event )
472{
474}
475
477{
478 if( !m_incomeURL.empty() )
479 m_txtURL->SetValue( m_incomeURL );
480 else
482
483 if( !m_txtURL->GetValue().IsEmpty() )
485 else
486 m_ConnType->SetSelection( static_cast<int>( KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_LOCAL ) );
487
489
490 return true;
491}
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
std::tuple< bool, wxString, wxString > isValidSSH(const wxString &url)
void SetRepoSSHPath(const wxString &aPath)
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 OnTestClick(wxCommandEvent &event) override
void OnOKClick(wxCommandEvent &event) override
void SetUsername(const wxString &aUsername)
std::tuple< bool, wxString, wxString, wxString > isValidHTTPS(const wxString &url)
virtual bool TransferDataToWindow() override
void OnUpdateUI(wxUpdateUIEvent &event) override
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...
static wxString GetLastGitError()
void SetSSHKey(const wxString &aSSHKey)
void SetUsername(const wxString &aUsername)
void SetPassword(const wxString &aPassword)
void SetRemote(const wxString &aRemote)
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(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:403
int credentials_cb(git_cred **aOut, const char *aUrl, const char *aUsername, unsigned int aAllowedTypes, void *aPayload)
std::unique_ptr< git_remote, decltype([](git_remote *aRemote) { git_remote_free(aRemote); })> GitRemotePtr
A unique pointer for git_remote objects with automatic cleanup.