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