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 (C) 2023 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>
28#include <gestfich.h>
29
30#include <cerrno>
31#include <cstring>
32#include <fstream>
33
34#include <wx/clipbrd.h>
35#include <wx/msgdlg.h>
36#include <wx/regex.h>
37#include <wx/stdpaths.h>
38
39
40DIALOG_GIT_REPOSITORY::DIALOG_GIT_REPOSITORY( wxWindow* aParent, git_repository* aRepository,
41 wxString aURL ) :
43 m_repository( aRepository ),
44 m_prevFile( wxEmptyString ),
45 m_tested( 0 ),
46 m_failedTest( false ),
47 m_testError( wxEmptyString ),
48 m_tempRepo( false ),
49 m_repoType( KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_LOCAL )
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 = GIT_REPOSITORY_INIT_OPTIONS_INIT;
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
74
75 Layout();
76}
77
79{
80 if( m_tempRepo )
81 {
82 git_repository_free( m_repository );
84 }
85}
86
87
89{
90 if( wxTheClipboard->Open() && wxTheClipboard->IsSupported( wxDF_TEXT ) )
91 {
92 wxString clipboardText;
93 wxTextDataObject textData;
94
95 if( wxTheClipboard->GetData( textData ) && !( clipboardText = textData.GetText() ).empty() )
96 {
97 if( std::get<0>( isValidHTTPS( clipboardText ) )
98 || std::get<0>( isValidSSH( clipboardText ) ) )
99 {
100 m_txtURL->SetValue( clipboardText );
101 }
102 }
103
104 wxTheClipboard->Close();
105 }
106
107 return false;
108}
109
110
112{
113 wxFileName sshKey;
114 sshKey.SetPath( wxGetUserHome() );
115 wxString retval;
116
117 sshKey.AppendDir( ".ssh" );
118 sshKey.SetFullName( "id_rsa" );
119
120 if( sshKey.FileExists() )
121 {
122 retval = sshKey.GetFullPath();
123 }
124 else if( sshKey.SetFullName( "id_dsa" ); sshKey.FileExists() )
125 {
126 retval = sshKey.GetFullPath();
127 }
128 else if( sshKey.SetFullName( "id_ecdsa" ); sshKey.FileExists() )
129 {
130 retval = sshKey.GetFullPath();
131 }
132
133 if( !retval.empty() )
134 {
135 m_fpSSHKey->SetFileName( retval );
136 wxFileDirPickerEvent evt;
137 evt.SetPath( retval );
138 OnFileUpdated( evt );
139 }
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
164std::tuple<bool,wxString,wxString,wxString> DIALOG_GIT_REPOSITORY::isValidHTTPS( const wxString& url )
165{
166 wxRegEx regex( R"((https?:\/\/)(([^:]+)(:([^@]+))?@)?([^\/]+\/[^\s]+))" );
167
168 if( regex.Matches( url ) )
169 {
170 wxString username = regex.GetMatch( url, 3 );
171 wxString password = regex.GetMatch( url, 5 );
172 wxString repoAddress = regex.GetMatch( url, 1 ) + regex.GetMatch( url, 6 );
173 return std::make_tuple( true, username, password, repoAddress );
174 }
175
176 return std::make_tuple( false, "", "", "" );
177}
178
179
180std::tuple<bool,wxString, wxString> DIALOG_GIT_REPOSITORY::isValidSSH( const wxString& url )
181{
182 wxRegEx regex( R"((?:ssh:\/\/)?([^@]+)@([^\/]+\/[^\s]+))" );
183
184 if( regex.Matches( url ) )
185 {
186 wxString username = regex.GetMatch( url, 1 );
187 wxString repoAddress = regex.GetMatch( url, 2 );
188 return std::make_tuple( true, username, repoAddress );
189 }
190
191 return std::make_tuple( false, "", "" );
192}
193
194
195static wxString get_repo_name( wxString& aRepoAddr )
196{
197 wxString retval;
198 size_t last_slash = aRepoAddr.find_last_of( '/' );
199 bool ends_with_dot_git = aRepoAddr.EndsWith( ".git" );
200
201 if( ends_with_dot_git )
202 retval = aRepoAddr.substr( last_slash + 1, aRepoAddr.size() - last_slash - 5 );
203 else
204 retval = aRepoAddr.substr( last_slash + 1, aRepoAddr.size() - last_slash );
205
206 return retval;
207}
208
209
210void DIALOG_GIT_REPOSITORY::OnLocationExit( wxFocusEvent& event )
211{
214 event.Skip();
215}
216
217
219{
220 wxString url = m_txtURL->GetValue();
221
222 if( url.IsEmpty() )
223 return;
224
225 if( url.Contains( "https://" ) || url.Contains( "http://" ) )
226 {
227 auto [valid, username, password, repoAddress] = isValidHTTPS( url );
228
229 if( valid )
230 {
231 m_ConnType->SetSelection( static_cast<int>( KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_HTTPS ) );
232 SetUsername( username );
233 SetPassword( password );
234 m_txtURL->SetValue( repoAddress );
235
236 if( m_txtName->GetValue().IsEmpty() )
237 m_txtName->SetValue( get_repo_name( repoAddress ) );
238
239 }
240 }
241 else if( url.Contains( "ssh://" ) || url.Contains( "git@" ) )
242 {
243 auto [valid, username, repoAddress] = isValidSSH( url );
244
245 if( valid )
246 {
247 m_ConnType->SetSelection( static_cast<int>( KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_SSH ) );
248 m_txtUsername->SetValue( username );
249 m_txtURL->SetValue( repoAddress );
250
251 if( m_txtName->GetValue().IsEmpty() )
252 m_txtName->SetValue( get_repo_name( repoAddress ) );
253
255 }
256 }
257}
258
259
260void DIALOG_GIT_REPOSITORY::OnTestClick( wxCommandEvent& event )
261{
262 git_remote* remote = nullptr;
263 git_remote_callbacks callbacks = GIT_REMOTE_CALLBACKS_INIT;
264
265 // We track if we have already tried to connect.
266 // If we have, the server may come back to offer another connection
267 // type, so we need to keep track of how many times we have tried.
268 m_tested = 0;
269
270 callbacks.credentials = []( git_cred** aOut, const char* aUrl, const char* aUsername,
271 unsigned int aAllowedTypes, void* aPayload ) -> int
272 {
273 DIALOG_GIT_REPOSITORY* dialog = static_cast<DIALOG_GIT_REPOSITORY*>( aPayload );
274
276 return GIT_PASSTHROUGH;
277
278 if( aAllowedTypes & GIT_CREDTYPE_USERNAME
279 && !( dialog->GetTested() & GIT_CREDTYPE_USERNAME ) )
280 {
281 wxString username = dialog->GetUsername().Trim().Trim( false );
282 git_cred_username_new( aOut, username.ToStdString().c_str() );
283 dialog->GetTested() |= GIT_CREDTYPE_USERNAME;
284 }
286 && ( aAllowedTypes & GIT_CREDTYPE_USERPASS_PLAINTEXT )
287 && !( dialog->GetTested() & GIT_CREDTYPE_USERPASS_PLAINTEXT ) )
288 {
289 wxString username = dialog->GetUsername().Trim().Trim( false );
290 wxString password = dialog->GetPassword().Trim().Trim( false );
291
292 git_cred_userpass_plaintext_new( aOut, username.ToStdString().c_str(),
293 password.ToStdString().c_str() );
294 dialog->GetTested() |= GIT_CREDTYPE_USERPASS_PLAINTEXT;
295 }
297 && ( aAllowedTypes & GIT_CREDTYPE_SSH_KEY )
298 && !( dialog->GetTested() & GIT_CREDTYPE_SSH_KEY ) )
299 {
300 // SSH key authentication
301 wxString sshKey = dialog->GetRepoSSHPath();
302 wxString sshPubKey = sshKey + ".pub";
303 wxString username = dialog->GetUsername().Trim().Trim( false );
304 wxString password = dialog->GetPassword().Trim().Trim( false );
305
306 git_cred_ssh_key_new( aOut, username.ToStdString().c_str(),
307 sshPubKey.ToStdString().c_str(), sshKey.ToStdString().c_str(),
308 password.ToStdString().c_str() );
309 dialog->GetTested() |= GIT_CREDTYPE_SSH_KEY;
310 }
311 else
312 {
313 return GIT_PASSTHROUGH;
314 }
315
316 return GIT_OK;
317 };
318
319 callbacks.payload = this;
320
321 wxString txtURL = m_txtURL->GetValue();
322 git_remote_create_with_fetchspec( &remote, m_repository, "origin", txtURL.ToStdString().c_str(),
323 "+refs/heads/*:refs/remotes/origin/*" );
324
325 if( git_remote_connect( remote, GIT_DIRECTION_FETCH, &callbacks, nullptr, nullptr ) != GIT_OK )
326 SetTestResult( true, git_error_last()->message );
327 else
328 SetTestResult( false, wxEmptyString );
329
330 git_remote_disconnect( remote );
331 git_remote_free( remote );
332
333 auto dlg = wxMessageDialog( this, wxEmptyString, _( "Test connection" ), wxOK | wxICON_INFORMATION );
334
335 if( !m_failedTest )
336 {
337 dlg.SetMessage( _( "Connection successful" ) );
338 }
339 else
340 {
341 dlg.SetMessage( wxString::Format( _( "Could not connect to '%s' " ), m_txtURL->GetValue() ) );
342 dlg.SetExtendedMessage( m_testError );
343 }
344
345 dlg.ShowModal();
346}
347
348
349void DIALOG_GIT_REPOSITORY::OnFileUpdated( wxFileDirPickerEvent& aEvent )
350{
351 wxString file = aEvent.GetPath();
352
353 if( file.ends_with( wxS( ".pub" ) ) )
354 file = file.Left( file.size() - 4 );
355
356 std::ifstream ifs( file.fn_str() );
357
358 if( !ifs.good() || !ifs.is_open() )
359 {
360 DisplayErrorMessage( this, wxString::Format( _( "Could not open private key '%s'" ), file ),
361 wxString::Format( "%s: %d", std::strerror( errno ), errno ) );
362 return;
363 }
364
365 std::string line;
366 std::getline( ifs, line );
367
368 bool isValid = ( line.find( "PRIVATE KEY" ) != std::string::npos );
369 bool isEncrypted = ( line.find( "ENCRYPTED" ) != std::string::npos );
370
371 if( !isValid )
372 {
373 DisplayErrorMessage( this, _( "Invalid SSH Key" ),
374 _( "The selected file is not a valid SSH private key" ) );
375 CallAfter( [this] { SetRepoSSHPath( m_prevFile ); } );
376 return;
377 }
378
379 if( isEncrypted )
380 {
381 m_txtPassword->Enable();
382 m_txtPassword->SetToolTip( _( "Enter the password for the SSH key" ) );
383 }
384 else
385 {
386 m_txtPassword->SetValue( wxEmptyString );
387 m_txtPassword->SetToolTip( wxEmptyString );
388 m_txtPassword->Disable();
389 }
390
391 ifs.close();
392
393 wxString pubFile = file + wxS( ".pub" );
394 std::ifstream pubIfs( pubFile.fn_str() );
395
396 if( !pubIfs.good() || !pubIfs.is_open() )
397 {
398 DisplayErrorMessage( this, wxString::Format( _( "Could not open public key '%s'" ),
399 file + ".pub" ),
400 wxString::Format( "%s: %d", std::strerror( errno ), errno ) );
401 aEvent.SetPath( wxEmptyString );
402 CallAfter( [this] { SetRepoSSHPath( m_prevFile ); } );
403 return;
404 }
405
406 m_prevFile = file;
407 pubIfs.close();
408}
409
410
411void DIALOG_GIT_REPOSITORY::OnOKClick( wxCommandEvent& event )
412{
413 // Save the repository details
414
415 if( m_txtName->GetValue().IsEmpty() )
416 {
417 DisplayErrorMessage( this, _( "Missing information" ),
418 _( "Please enter a name for the repository" ) );
419 return;
420 }
421
422 if( m_txtURL->GetValue().IsEmpty() )
423 {
424 DisplayErrorMessage( this, _( "Missing information" ),
425 _( "Please enter a URL for the repository" ) );
426 return;
427 }
428
429 EndModal( wxID_OK );
430}
431
432
434{
435 if( m_ConnType->GetSelection() == static_cast<int>( KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_LOCAL ) )
436 {
437 m_panelAuth->Show( false );
438 }
439 else
440 {
441 m_panelAuth->Show( true );
442
443 if( m_ConnType->GetSelection() == static_cast<int>( KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_SSH ) )
444 {
445 m_fpSSHKey->Show( true );
446 m_labelSSH->Show( true );
447 m_labelPass1->SetLabel( _( "SSH Key Password" ) );
448 }
449 else
450 {
451 m_fpSSHKey->Show( false );
452 m_labelSSH->Show( false );
453 m_labelPass1->SetLabel( _( "Password" ) );
455 }
456 }
457
458 Layout();
459}
460
461
462void DIALOG_GIT_REPOSITORY::OnSelectConnType( wxCommandEvent& event )
463{
465}
Class DIALOG_GIT_REPOSITORY_BASE.
void OnSelectConnType(wxCommandEvent &event) override
std::tuple< bool, wxString, wxString > isValidSSH(const wxString &url)
void SetRepoSSHPath(const wxString &aPath)
KIGIT_COMMON::GIT_CONN_TYPE GetRepoType() const
void OnFileUpdated(wxFileDirPickerEvent &event) override
void SetPassword(const wxString &aPassword)
wxString GetRepoSSHPath() const
void SetTestResult(bool aFailed, const wxString &aError)
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
wxString GetUsername() const
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
wxString GetPassword() const
void SetupStandardButtons(std::map< int, wxString > aLabels={})
void DisplayErrorMessage(wxWindow *aParent, const wxString &aText, const wxString &aExtraInfo)
Display an error message with aMessage.
Definition: confirm.cpp:305
This file is part of the common library.
static wxString get_repo_name(wxString &aRepoAddr)
#define _(s)
bool RmDirRecursive(const wxString &aFileName, wxString *aErrors)
Removes the directory aDirName and all its contents including subdirectories and their files.
Definition: gestfich.cpp:326