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
82
84{
85 if( m_tempRepo )
86 {
87 git_repository_free( m_repository );
89 }
90}
91
92
94{
95 wxClipboardLocker lock;
96 if( !lock )
97 return false;
98
99 if( !wxTheClipboard->IsSupported( wxDF_TEXT ) )
100 return false;
101
102 wxTextDataObject textData;
103 if( !wxTheClipboard->GetData( textData ) )
104 return false;
105
106 wxString clipboardText = textData.GetText();
107 if( clipboardText.empty() )
108 return false;
109
110 if( std::get<0>( isValidHTTPS( clipboardText ) )
111 || std::get<0>( isValidSSH( clipboardText ) ) )
112 {
113 m_txtURL->SetValue( clipboardText );
114 }
115
116 return false;
117}
118
119
121{
122 wxFileName sshKey;
123 sshKey.SetPath( wxGetUserHome() );
124 wxString retval;
125
126 sshKey.AppendDir( ".ssh" );
127 sshKey.SetFullName( "id_rsa" );
128
129 if( sshKey.FileExists() )
130 {
131 retval = sshKey.GetFullPath();
132 }
133 else if( sshKey.SetFullName( "id_dsa" ); sshKey.FileExists() )
134 {
135 retval = sshKey.GetFullPath();
136 }
137 else if( sshKey.SetFullName( "id_ecdsa" ); sshKey.FileExists() )
138 {
139 retval = sshKey.GetFullPath();
140 }
141
142 if( !retval.empty() )
143 {
144 m_fpSSHKey->SetFileName( retval );
145 wxFileDirPickerEvent evt;
146 evt.SetPath( retval );
147 OnFileUpdated( evt );
148 }
149}
150
151
152void DIALOG_GIT_REPOSITORY::onCbCustom( wxCommandEvent& event )
153{
155 event.Skip();
156}
157
158
159void DIALOG_GIT_REPOSITORY::OnUpdateUI( wxUpdateUIEvent& event )
160{
161 // event.Enable( !m_txtName->GetValue().IsEmpty() && !m_txtURL->GetValue().IsEmpty() );
162}
163
164
166{
167 if( aEncrypted )
168 {
169 m_txtPassword->Enable();
170 m_txtPassword->SetToolTip( _( "Enter the password for the SSH key" ) );
171 }
172 else
173 {
174 m_txtPassword->SetValue( wxEmptyString );
175 m_txtPassword->SetToolTip( wxEmptyString );
176 m_txtPassword->Disable();
177 }
178}
179
180
181std::tuple<bool,wxString,wxString,wxString> DIALOG_GIT_REPOSITORY::isValidHTTPS( const wxString& url )
182{
183 wxRegEx regex( R"((https?:\/\/)(([^:]+)(:([^@]+))?@)?([^\/]+\/[^\s]+))" );
184
185 if( regex.Matches( url ) )
186 {
187 wxString username = regex.GetMatch( url, 3 );
188 wxString password = regex.GetMatch( url, 5 );
189 wxString repoAddress = regex.GetMatch( url, 1 ) + regex.GetMatch( url, 6 );
190 return std::make_tuple( true, username, password, repoAddress );
191 }
192
193 return std::make_tuple( false, "", "", "" );
194}
195
196
197std::tuple<bool,wxString, wxString> DIALOG_GIT_REPOSITORY::isValidSSH( const wxString& url )
198{
199 wxRegEx regex( R"((?:ssh:\/\/)?([^@]+)@([^\/]+\/[^\s]+))" );
200
201 if( regex.Matches( url ) )
202 {
203 wxString username = regex.GetMatch( url, 1 );
204 wxString repoAddress = regex.GetMatch( url, 2 );
205 return std::make_tuple( true, username, repoAddress );
206 }
207
208 return std::make_tuple( false, "", "" );
209}
210
211
212static wxString get_repo_name( wxString& aRepoAddr )
213{
214 wxString addr = aRepoAddr;
215
216 // Strip GitHub/GitLab web-UI path suffixes so that pasting a browser URL
217 // (e.g. .../repo/tree/master, .../repo/blob/main/README.md, .../repo/pulls)
218 // still gives the repository name rather than a branch/page name.
219 static wxRegEx webSuffix(
220 R"((/-)?/(tree|blob|commits?|raw|releases|tags|branches|pulls|pull|issues|merge_requests|wiki|actions)/.*$)",
221 wxRE_ADVANCED );
222
223 webSuffix.ReplaceAll( &addr, wxEmptyString );
224
225 while( addr.EndsWith( "/" ) )
226 addr.RemoveLast();
227
228 if( addr.EndsWith( ".git" ) )
229 addr.RemoveLast( 4 );
230
231 size_t last_slash = addr.find_last_of( '/' );
232
233 if( last_slash == wxString::npos )
234 return addr;
235
236 return addr.substr( last_slash + 1 );
237}
238
239
240void DIALOG_GIT_REPOSITORY::OnLocationExit( wxFocusEvent& event )
241{
244 event.Skip();
245}
246
247
249{
250 wxString url = m_txtURL->GetValue();
251
252 if( url.IsEmpty() )
253 return;
254
255 if( url.Contains( "https://" ) || url.Contains( "http://" ) )
256 {
257 auto [valid, username, password, repoAddress] = isValidHTTPS( url );
258
259 if( valid )
260 {
261 m_fullURL = url;
262 m_ConnType->SetSelection( static_cast<int>( KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_HTTPS ) );
263 SetUsername( username );
264 SetPassword( password );
265 m_txtURL->SetValue( repoAddress );
266
267 m_txtName->SetValue( get_repo_name( repoAddress ) );
268 }
269 }
270 else if( url.Contains( "ssh://" ) || url.Contains( "git@" ) )
271 {
272 auto [valid, username, repoAddress] = isValidSSH( url );
273
274 if( valid )
275 {
276 m_fullURL = url;
277 m_ConnType->SetSelection( static_cast<int>( KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_SSH ) );
278 m_txtUsername->SetValue( username );
279 m_txtURL->SetValue( repoAddress );
280
281 m_txtName->SetValue( get_repo_name( repoAddress ) );
282
284 }
285 }
286 else
287 {
288 if( m_fullURL.IsEmpty() )
289 m_fullURL = url;
290
291 // URL without user@ prefix (e.g. "host:path/repo.git")
292 size_t colonPos = url.find( ':' );
293 size_t slashPos = url.find( '/' );
294
295 if( colonPos != wxString::npos && ( slashPos == wxString::npos || colonPos < slashPos ) )
296 {
297 m_ConnType->SetSelection( static_cast<int>( KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_SSH ) );
299
300 m_txtName->SetValue( get_repo_name( url ) );
301 }
302 }
303}
304
305
306void DIALOG_GIT_REPOSITORY::OnTestClick( wxCommandEvent& event )
307{
308 if( m_txtURL->GetValue().Trim().Trim( false ).IsEmpty() )
309 return;
310
311 wxString error;
312 bool success = false;
313 git_remote* remote = nullptr;
314 git_remote_callbacks callbacks;
315 git_remote_init_callbacks( &callbacks, GIT_REMOTE_CALLBACKS_VERSION );
316
317 // We track if we have already tried to connect.
318 // If we have, the server may come back to offer another connection
319 // type, so we need to keep track of how many times we have tried.
320
321 KIGIT_COMMON common( m_repository );
322 common.SetRemote( m_txtURL->GetValue() );
323 callbacks.credentials = credentials_cb;
324 common.SetPassword( m_txtPassword->GetValue() );
325 common.SetUsername( m_txtUsername->GetValue() );
326 common.SetSSHKey( m_fpSSHKey->GetFileName().GetFullPath() );
327 KIGIT_REPO_MIXIN repoMixin( &common );
328 callbacks.payload = &repoMixin;
329
330 wxString txtURL = m_txtURL->GetValue();
331 git_remote_create_with_fetchspec( &remote, m_repository, "origin", txtURL.mbc_str(),
332 "+refs/heads/*:refs/remotes/origin/*" );
333 KIGIT::GitRemotePtr remotePtr( remote );
334
335 if( git_remote_connect( remote, GIT_DIRECTION_FETCH, &callbacks, nullptr, nullptr ) == GIT_OK )
336 success = true;
337 else
339
340 git_remote_disconnect( remote );
341
342 auto dlg = KICAD_MESSAGE_DIALOG( this, wxEmptyString, _( "Test Connection" ),
343 wxOK | wxICON_INFORMATION );
344
345 if( success )
346 {
347 dlg.SetMessage( _( "Connection successful" ) );
348 }
349 else
350 {
351 dlg.SetMessage( wxString::Format( _( "Could not connect to '%s' " ),
352 m_txtURL->GetValue() ) );
353 dlg.SetExtendedMessage( error );
354 }
355
356 dlg.ShowModal();
357}
358
359
360void DIALOG_GIT_REPOSITORY::OnFileUpdated( wxFileDirPickerEvent& aEvent )
361{
362 wxString file = aEvent.GetPath();
363
364 if( file.ends_with( wxS( ".pub" ) ) )
365 file = file.Left( file.size() - 4 );
366
367 std::ifstream ifs( file.fn_str() );
368
369 if( !ifs.good() || !ifs.is_open() )
370 {
371 DisplayErrorMessage( this, wxString::Format( _( "Could not open private key '%s'" ), file ),
372 wxString::Format( "%s: %d", std::strerror( errno ), errno ) );
373 return;
374 }
375
376 std::string line;
377 std::getline( ifs, line );
378
379 bool isValid = ( line.find( "PRIVATE KEY" ) != std::string::npos );
380 bool isEncrypted = ( line.find( "ENCRYPTED" ) != std::string::npos );
381
382 if( !isValid )
383 {
384 DisplayErrorMessage( this, _( "Invalid SSH Key" ),
385 _( "The selected file is not a valid SSH private key" ) );
386 CallAfter( [this] { SetRepoSSHPath( m_prevFile ); } );
387 return;
388 }
389
390 if( isEncrypted )
391 {
392 m_txtPassword->Enable();
393 m_txtPassword->SetToolTip( _( "Enter the password for the SSH key" ) );
394 }
395 else
396 {
397 m_txtPassword->SetValue( wxEmptyString );
398 m_txtPassword->SetToolTip( wxEmptyString );
399 m_txtPassword->Disable();
400 }
401
402 ifs.close();
403
404 wxString pubFile = file + wxS( ".pub" );
405 std::ifstream pubIfs( pubFile.fn_str() );
406
407 if( !pubIfs.good() || !pubIfs.is_open() )
408 {
409 DisplayErrorMessage( this, wxString::Format( _( "Could not open public key '%s'" ),
410 file + ".pub" ),
411 wxString::Format( "%s: %d", std::strerror( errno ), errno ) );
412 aEvent.SetPath( wxEmptyString );
413 CallAfter( [this] { SetRepoSSHPath( m_prevFile ); } );
414 return;
415 }
416
417 m_prevFile = file;
418 pubIfs.close();
419}
420
421
422void DIALOG_GIT_REPOSITORY::OnOKClick( wxCommandEvent& event )
423{
424 // Save the repository details
425
426 if( m_txtURL->GetValue().IsEmpty() )
427 {
428 DisplayErrorMessage( this, _( "Missing information" ),
429 _( "Please enter a URL for the repository" ) );
430 return;
431 }
432
433 EndModal( wxID_OK );
434}
435
436
438{
439 if( m_ConnType->GetSelection() == static_cast<int>( KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_LOCAL ) )
440 {
441 m_panelAuth->Enable( false );
442 }
443 else
444 {
445 m_panelAuth->Enable( true );
446
447 if( m_ConnType->GetSelection() == static_cast<int>( KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_SSH ) )
448 {
449 m_cbCustom->Enable( true );
450 m_fpSSHKey->Enable( m_cbCustom->IsChecked() );
451 m_txtUsername->Enable( m_cbCustom->IsChecked() );
452 m_txtPassword->Enable( m_cbCustom->IsChecked() );
453 m_labelPass1->SetLabel( _( "SSH key password:" ) );
454 }
455 else
456 {
457 m_cbCustom->Enable( false );
458 m_fpSSHKey->Enable( false );
459 m_txtUsername->Enable( true );
460 m_txtPassword->Enable( true );
461 m_labelPass1->SetLabel( _( "Password:" ) );
462 }
463 }
464}
465
466
467void DIALOG_GIT_REPOSITORY::OnSelectConnType( wxCommandEvent& event )
468{
470}
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:390
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.