KiCad PCB EDA Suite
Loading...
Searching...
No Matches
project_tree_pane.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) 2012 SoftPLC Corporation, Dick Hollenbeck <[email protected]>
5 * Copyright (C) 2012 Jean-Pierre Charras, jp.charras at wanadoo.fr
6 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
7 *
8 * This program is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU General Public License
10 * as published by the Free Software Foundation; either version 2
11 * of the License, or (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program. If not, see <https://www.gnu.org/licenses/>.
20 */
21
22#include <stack>
23#include <git/git_backend.h>
24
25#include <wx/regex.h>
26#include <wx/richmsgdlg.h>
27#include <wx/stdpaths.h>
28#include <wx/string.h>
29#include <wx/msgdlg.h>
30#include <wx/textdlg.h>
31#include <wx/timer.h>
32#include <wx/wupdlock.h>
33#include <wx/log.h>
34
35#include <frame_type.h>
36#include <kiway.h>
37#include <mail_type.h>
39
41#include <advanced_config.h>
42#include <bitmaps.h>
43#include <bitmap_store.h>
44#include <confirm.h>
48#include <gestfich.h>
49#include <macros.h>
50#include <trace_helpers.h>
53#include <core/kicad_algo.h>
54#include <paths.h>
56#include <scoped_set_reset.h>
57#include <string_utils.h>
58#include <thread_pool.h>
59#include <launch_ext.h>
60#include <wx/dcclient.h>
61#include <wx/progdlg.h>
62#include <wx/settings.h>
63
80
82
83#include "project_tree_item.h"
84#include "project_tree.h"
85#include "pgm_kicad.h"
86#include "kicad_id.h"
87#include "kicad_manager_frame.h"
88
89#include "project_tree_pane.h"
90#include <widgets/kistatusbar.h>
91
92#include <kiplatform/io.h>
93#include <kiplatform/secrets.h>
94
95
96/* Note about the project tree build process:
97 * Building the project tree can be *very* long if there are a lot of subdirectories in the
98 * working directory. Unfortunately, this happens easily if the project file *.pro is in the
99 * user's home directory.
100 * So the tree project is built "on demand":
101 * First the tree is built from the current directory and shows files and subdirs.
102 * > First level subdirs trees are built (i.e subdirs contents are not read)
103 * > When expanding a subdir, each subdir contains is read, and the corresponding sub tree is
104 * populated on the fly.
105 */
106
107// list of files extensions listed in the tree project window
108// Add extensions in a compatible regex format to see others files types
109static const wxChar* s_allowedExtensionsToList[] =
110{
111 wxT( "^.*\\.pro$" ),
112 wxT( "^.*\\.kicad_pro$" ),
113 wxT( "^.*\\.pdf$" ),
114 wxT( "^.*\\.sch$" ), // Legacy Eeschema files
115 wxT( "^.*\\.kicad_sch$" ), // S-expr Eeschema files
116 wxT( "^[^$].*\\.brd$" ), // Legacy Pcbnew files
117 wxT( "^[^$].*\\.kicad_pcb$" ), // S format Pcbnew board files
118 wxT( "^[^$].*\\.kicad_dru$" ), // Design rule files
119 wxT( "^[^$].*\\.kicad_wks$" ), // S format kicad drawing sheet files
120 wxT( "^[^$].*\\.kicad_mod$" ), // S format kicad footprint files, currently not listed
121 wxT( "^.*\\.net$" ), // pcbnew netlist file
122 wxT( "^.*\\.cir$" ), // Spice netlist file
123 wxT( "^.*\\.lib$" ), // Legacy schematic library file
124 wxT( "^.*\\.kicad_sym$" ), // S-expr symbol libraries
125 wxT( "^.*\\.txt$" ), // Text files
126 wxT( "^.*\\.md$" ), // Markdown files
127 wxT( "^.*\\.pho$" ), // Gerber file (Old Kicad extension)
128 wxT( "^.*\\.gbr$" ), // Gerber file
129 wxT( "^.*\\.gbrjob$" ), // Gerber job file
130 wxT( "^.*\\.gb[alops]$" ), // Gerber back (or bottom) layer file (deprecated Protel ext)
131 wxT( "^.*\\.gt[alops]$" ), // Gerber front (or top) layer file (deprecated Protel ext)
132 wxT( "^.*\\.g[0-9]{1,2}$" ), // Gerber inner layer file (deprecated Protel ext)
133 wxT( "^.*\\.gm[0-9]{1,2}$" ), // Gerber mechanical layer file (deprecated Protel ext)
134 wxT( "^.*\\.gko$" ), // Gerber keepout layer file (deprecated Protel ext)
135 wxT( "^.*\\.odt$" ),
136 wxT( "^.*\\.htm$" ),
137 wxT( "^.*\\.html$" ),
138 wxT( "^.*\\.rpt$" ), // Report files
139 wxT( "^.*\\.csv$" ), // Report files in comma separated format
140 wxT( "^.*\\.pos$" ), // Footprint position files
141 wxT( "^.*\\.cmp$" ), // CvPcb cmp/footprint link files
142 wxT( "^.*\\.drl$" ), // Excellon drill files
143 wxT( "^.*\\.nc$" ), // Excellon NC drill files (alternate file ext)
144 wxT( "^.*\\.xnc$" ), // Excellon NC drill files (alternate file ext)
145 wxT( "^.*\\.svg$" ), // SVG print/plot files
146 wxT( "^.*\\.ps$" ), // PostScript plot files
147 wxT( "^.*\\.zip$" ), // Zip archive files
148 wxT( "^.*\\.kicad_jobset" ), // KiCad jobs file
149 nullptr // end of list
150};
151
152
158
159
161{
162 ID_PROJECT_TXTEDIT = 8700, // Start well above wxIDs
168
169 ID_GIT_INITIALIZE_PROJECT, // Initialize a new git repository in an existing project
170 ID_GIT_REMOTE_SETTINGS, // Configure (or set) the default remote on an existing repo
171 ID_GIT_CLONE_PROJECT, // Clone a project from a remote repository
172 ID_GIT_COMMIT_PROJECT, // Commit all files in the project
173 ID_GIT_COMMIT_FILE, // Commit a single file
174 ID_GIT_AMEND_COMMIT, // Amend the last commit on HEAD
175 ID_GIT_SYNC_PROJECT, // Sync the project with the remote repository (pull and push -- same as Update)
176 ID_GIT_FETCH, // Fetch the remote repository (without merging -- this is the same as Refresh)
177 ID_GIT_PUSH, // Push the local repository to the remote repository
178 ID_GIT_PULL, // Pull the remote repository to the local repository
179 ID_GIT_RESOLVE_CONFLICT, // Present the user with a resolve conflicts dialog (ours/theirs/merge)
180 ID_GIT_REVERT_LOCAL, // Revert the local repository to the last commit
181 ID_GIT_COMPARE, // Compare the current project to a different branch or commit in the git repository
182 ID_GIT_REMOVE_VCS, // Toggle Git integration for this project (preference in kicad_prl)
183 ID_GIT_ADD_TO_INDEX, // Add a file to the git index
184 ID_GIT_REMOVE_FROM_INDEX, // Remove a file from the git index
185 ID_GIT_SWITCH_BRANCH, // Switch the local repository to a different branch
186 ID_GIT_SWITCH_QUICK1, // Switch the local repository to the first quick branch
187 ID_GIT_SWITCH_QUICK2, // Switch the local repository to the second quick branch
188 ID_GIT_SWITCH_QUICK3, // Switch the local repository to the third quick branch
189 ID_GIT_SWITCH_QUICK4, // Switch the local repository to the fourth quick branch
190 ID_GIT_SWITCH_QUICK5, // Switch the local repository to the fifth quick branch
191
193};
194
195
196BEGIN_EVENT_TABLE( PROJECT_TREE_PANE, wxSashLayoutWindow )
197 EVT_TREE_ITEM_ACTIVATED( ID_PROJECT_TREE, PROJECT_TREE_PANE::onSelect )
198 EVT_TREE_ITEM_EXPANDED( ID_PROJECT_TREE, PROJECT_TREE_PANE::onExpand )
199 EVT_TREE_ITEM_RIGHT_CLICK( ID_PROJECT_TREE, PROJECT_TREE_PANE::onRight )
206
224
226
227 EVT_IDLE( PROJECT_TREE_PANE::onIdle )
228 EVT_PAINT( PROJECT_TREE_PANE::onPaint )
229END_EVENT_TABLE()
230
231
232wxDECLARE_EVENT( UPDATE_ICONS, wxCommandEvent );
233
235 wxSashLayoutWindow( parent, ID_LEFT_FRAME, wxDefaultPosition, wxDefaultSize, wxNO_BORDER | wxTAB_TRAVERSAL )
236{
237 m_Parent = parent;
238 m_TreeProject = nullptr;
239 m_isRenaming = false;
240 m_selectedItem = nullptr;
241 m_watcherNeedReset = false;
242 m_gitLastError = GIT_ERROR_NONE;
243 m_watcher = nullptr;
244 m_gitIconsInitialized = false;
245
246 Bind( wxEVT_FSWATCHER,
247 wxFileSystemWatcherEventHandler( PROJECT_TREE_PANE::onFileSystemEvent ), this );
248
249 Bind( wxEVT_SYS_COLOUR_CHANGED,
250 wxSysColourChangedEventHandler( PROJECT_TREE_PANE::onThemeChanged ), this );
251
252 m_gitSyncTimer.SetOwner( this );
253 m_gitStatusTimer.SetOwner( this );
254 m_gitFeedbackTimer.SetOwner( this );
255 Bind( wxEVT_TIMER, wxTimerEventHandler( PROJECT_TREE_PANE::onGitSyncTimer ), this, m_gitSyncTimer.GetId() );
256 Bind( wxEVT_TIMER, wxTimerEventHandler( PROJECT_TREE_PANE::onGitStatusTimer ), this, m_gitStatusTimer.GetId() );
257 Bind( wxEVT_TIMER, wxTimerEventHandler( PROJECT_TREE_PANE::onGitFeedbackTimer ), this, m_gitFeedbackTimer.GetId() );
258 /*
259 * Filtering is now inverted: the filters are actually used to _enable_ support
260 * for a given file type.
261 */
262 for( int ii = 0; s_allowedExtensionsToList[ii] != nullptr; ii++ )
263 m_filters.emplace_back( s_allowedExtensionsToList[ii] );
264
265 m_filters.emplace_back( wxT( "^no KiCad files found" ) );
266
268}
269
270
272{
273 Unbind( wxEVT_FSWATCHER,
274 wxFileSystemWatcherEventHandler( PROJECT_TREE_PANE::onFileSystemEvent ), this );
275 Unbind( wxEVT_SYS_COLOUR_CHANGED,
276 wxSysColourChangedEventHandler( PROJECT_TREE_PANE::onThemeChanged ), this );
277
278 m_gitSyncTimer.Stop();
279 m_gitStatusTimer.Stop();
280 m_gitFeedbackTimer.Stop();
281 Unbind( wxEVT_TIMER, wxTimerEventHandler( PROJECT_TREE_PANE::onGitSyncTimer ), this, m_gitSyncTimer.GetId() );
282 Unbind( wxEVT_TIMER, wxTimerEventHandler( PROJECT_TREE_PANE::onGitStatusTimer ), this, m_gitStatusTimer.GetId() );
283 Unbind( wxEVT_TIMER, wxTimerEventHandler( PROJECT_TREE_PANE::onGitFeedbackTimer ), this,
284 m_gitFeedbackTimer.GetId() );
286
287 if( m_gitSyncTask.valid() )
288 m_gitSyncTask.wait();
289
290 if( m_gitStatusIconTask.valid() )
291 m_gitStatusIconTask.wait();
292}
293
294
296{
297 if( m_watcher )
298 {
299 m_watcher->RemoveAll();
300 m_watcher->SetOwner( nullptr );
301 delete m_watcher;
302 m_watcher = nullptr;
303 }
304}
305
306
308{
309 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
310
311 if( tree_data.size() != 1 )
312 return;
313
314 wxString prj_filename = tree_data[0]->GetFileName();
315
316 m_Parent->LoadProject( prj_filename );
317}
318
319
320void PROJECT_TREE_PANE::onOpenDirectory( wxCommandEvent& event )
321{
322 // Get the root directory name:
323 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
324
325 for( PROJECT_TREE_ITEM* item_data : tree_data )
326 {
327 // Ask for the new sub directory name
328 wxString curr_dir = item_data->GetDir();
329
330 if( curr_dir.IsEmpty() )
331 {
332 // Use project path if the tree view path was empty.
333 curr_dir = wxPathOnly( m_Parent->GetProjectFileName() );
334
335 // As a last resort use the user's documents folder.
336 if( curr_dir.IsEmpty() || !wxFileName::DirExists( curr_dir ) )
338
339 if( !curr_dir.IsEmpty() )
340 curr_dir += wxFileName::GetPathSeparator();
341 }
342
343 LaunchExternal( curr_dir );
344 }
345}
346
347
348void PROJECT_TREE_PANE::onCreateNewDirectory( wxCommandEvent& event )
349{
350 // Get the root directory name:
351 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
352
353 for( PROJECT_TREE_ITEM* item_data : tree_data )
354 {
355 wxString prj_dir = wxPathOnly( m_Parent->GetProjectFileName() );
356
357 // Ask for the new sub directory name
358 wxString curr_dir = item_data->GetDir();
359
360 if( curr_dir.IsEmpty() )
361 curr_dir = prj_dir;
362
363 wxString new_dir = wxGetTextFromUser( _( "Directory name:" ), _( "Create New Directory" ) );
364
365 if( new_dir.IsEmpty() )
366 return;
367
368 wxString full_dirname = curr_dir + wxFileName::GetPathSeparator() + new_dir;
369
370 if( !wxMkdir( full_dirname ) )
371 return;
372
373 addItemToProjectTree( full_dirname, item_data->GetId(), nullptr, false );
374 }
375}
376
377
379{
380 switch( type )
381 {
400 case TREE_FILE_TYPE::DRILL_NC: return "nc";
401 case TREE_FILE_TYPE::DRILL_XNC: return "xnc";
411
415 case TREE_FILE_TYPE::DIRECTORY: break;
416 }
417
418 return wxEmptyString;
419}
420
421
422std::vector<wxString> getProjects( const wxDir& dir )
423{
424 std::vector<wxString> projects;
425 wxString dir_filename;
426 bool haveFile = dir.GetFirst( &dir_filename );
427
428 while( haveFile )
429 {
430 wxFileName file( dir_filename );
431
432 if( file.GetExt() == FILEEXT::LegacyProjectFileExtension
433 || file.GetExt() == FILEEXT::ProjectFileExtension )
434 projects.push_back( file.GetName() );
435
436 haveFile = dir.GetNext( &dir_filename );
437 }
438
439 return projects;
440}
441
442
443
444
445
446wxTreeItemId PROJECT_TREE_PANE::addItemToProjectTree( const wxString& aName,
447 const wxTreeItemId& aParent,
448 std::vector<wxString>* aProjectNames,
449 bool aRecurse )
450{
452 wxFileName fn( aName );
453
454 if( KIPLATFORM::IO::IsFileHidden( aName ) )
455 return wxTreeItemId();
456
457 if( wxDirExists( aName ) )
458 {
460 }
461 else
462 {
463 // Filter
464 wxRegEx reg;
465 bool addFile = false;
466
467 for( const wxString& m_filter : m_filters )
468 {
469 wxCHECK2_MSG( reg.Compile( m_filter, wxRE_ICASE ), continue,
470 wxString::Format( "Regex %s failed to compile.", m_filter ) );
471
472 if( reg.Matches( aName ) )
473 {
474 addFile = true;
475 break;
476 }
477 }
478
479 if( !addFile )
480 return wxTreeItemId();
481
482 for( int i = static_cast<int>( TREE_FILE_TYPE::LEGACY_PROJECT );
483 i < static_cast<int>( TREE_FILE_TYPE::MAX ); i++ )
484 {
485 wxString ext = GetFileExt( (TREE_FILE_TYPE) i );
486
487 if( ext == wxT( "" ) )
488 continue;
489
490 if( reg.Compile( wxString::FromAscii( "^.*\\." ) + ext + wxString::FromAscii( "$" ), wxRE_ICASE )
491 && reg.Matches( aName ) )
492 {
493 type = (TREE_FILE_TYPE) i;
494 break;
495 }
496 }
497 }
498
499 wxString file = wxFileNameFromPath( aName );
500 wxFileName currfile( file );
501 wxFileName project( m_Parent->GetProjectFileName() );
502 bool showAllSchematics = m_TreeProject->GetGitRepo() != nullptr;
503
504 // Ignore legacy projects with the same name as the current project
505 if( ( type == TREE_FILE_TYPE::LEGACY_PROJECT )
506 && ( currfile.GetName().CmpNoCase( project.GetName() ) == 0 ) )
507 {
508 return wxTreeItemId();
509 }
510
511 if( !showAllSchematics && ( currfile.GetExt() == GetFileExt( TREE_FILE_TYPE::LEGACY_SCHEMATIC )
512 || currfile.GetExt() == GetFileExt( TREE_FILE_TYPE::SEXPR_SCHEMATIC ) ) )
513 {
514 if( aProjectNames )
515 {
516 if( !alg::contains( *aProjectNames, currfile.GetName() ) )
517 return wxTreeItemId();
518 }
519 else
520 {
521 PROJECT_TREE_ITEM* parentTreeItem = GetItemIdData( aParent );
522
523 if( !parentTreeItem )
524 return wxTreeItemId();
525
526 wxDir parentDir( parentTreeItem->GetDir() );
527 std::vector<wxString> projects = getProjects( parentDir );
528
529 if( !alg::contains( projects, currfile.GetName() ) )
530 return wxTreeItemId();
531 }
532 }
533
534 // also check to see if it is already there.
535 wxTreeItemIdValue cookie;
536 wxTreeItemId kid = m_TreeProject->GetFirstChild( aParent, cookie );
537
538 while( kid.IsOk() )
539 {
540 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
541
542 if( itemData && itemData->GetFileName() == aName )
543 return itemData->GetId(); // well, we would have added it, but it is already here!
544
545 kid = m_TreeProject->GetNextChild( aParent, cookie );
546 }
547
548 // Only show current files if both legacy and current files are present
553 {
554 kid = m_TreeProject->GetFirstChild( aParent, cookie );
555
556 while( kid.IsOk() )
557 {
558 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
559
560 if( itemData )
561 {
562 wxFileName fname( itemData->GetFileName() );
563
564 if( fname.GetName().CmpNoCase( currfile.GetName() ) == 0 )
565 {
566 switch( type )
567 {
569 if( itemData->GetType() == TREE_FILE_TYPE::JSON_PROJECT )
570 return wxTreeItemId();
571
572 break;
573
575 if( itemData->GetType() == TREE_FILE_TYPE::SEXPR_SCHEMATIC )
576 return wxTreeItemId();
577
578 break;
579
581 if( itemData->GetType() == TREE_FILE_TYPE::LEGACY_PROJECT )
582 m_TreeProject->Delete( kid );
583
584 break;
585
587 if( itemData->GetType() == TREE_FILE_TYPE::LEGACY_SCHEMATIC )
588 m_TreeProject->Delete( kid );
589
590 break;
591
592 default:
593 break;
594 }
595 }
596 }
597
598 kid = m_TreeProject->GetNextChild( aParent, cookie );
599 }
600 }
601
602 // Append the item (only appending the filename not the full path):
603 wxTreeItemId newItemId = m_TreeProject->AppendItem( aParent, file );
604 PROJECT_TREE_ITEM* data = new PROJECT_TREE_ITEM( type, aName, m_TreeProject );
605
606 m_TreeProject->SetItemData( newItemId, data );
607 data->SetState( 0 );
608
609 // Mark root files (files which have the same aName as the project)
610 wxString fileName = currfile.GetName().Lower();
611 wxString projName = project.GetName().Lower();
612
613 if( fileName == projName || fileName.StartsWith( projName + "-" ) )
614 data->SetRootFile( true );
615
616#ifndef __WINDOWS__
617 bool subdir_populated = false;
618#endif
619
620 // This section adds dirs and files found in the subdirs
621 // in this case AddFile is recursive, but for the first level only.
622 if( TREE_FILE_TYPE::DIRECTORY == type && aRecurse )
623 {
624 wxDir dir( aName );
625
626 if( dir.IsOpened() ) // protected dirs will not open properly.
627 {
628 std::vector<wxString> projects = getProjects( dir );
629 wxString dir_filename;
630 bool haveFile = dir.GetFirst( &dir_filename );
631
632 data->SetPopulated( true );
633
634#ifndef __WINDOWS__
635 subdir_populated = aRecurse;
636#endif
637
638 while( haveFile )
639 {
640 // Add name in tree, but do not recurse
641 wxString path = aName + wxFileName::GetPathSeparator() + dir_filename;
642 addItemToProjectTree( path, newItemId, &projects, false );
643
644 haveFile = dir.GetNext( &dir_filename );
645 }
646 }
647
648 // Sort filenames by alphabetic order
649 m_TreeProject->SortChildren( newItemId );
650 }
651
652#ifndef __WINDOWS__
653 if( subdir_populated )
654 m_watcherNeedReset = true;
655#endif
656
657 return newItemId;
658}
659
660
662{
663 std::lock_guard<std::mutex> lock1( m_gitStatusMutex );
664 std::lock_guard<std::mutex> lock2( m_gitTreeCacheMutex );
665
666 m_gitStatusTimer.Stop();
667 m_gitSyncTimer.Stop();
668
669 if( m_TreeProject && m_TreeProject->GetGitRepo() )
670 m_TreeProject->GitCommon()->SetCancelled( true );
671
672 m_gitTreeCache.clear();
673 m_gitStatusIcons.clear();
674
675 wxString pro_dir = m_Parent->GetProjectFileName();
676
677 if( !m_TreeProject )
678 m_TreeProject = new PROJECT_TREE( this );
679 else
680 m_TreeProject->DeleteAllItems();
681
682 if( !pro_dir ) // This is empty from PROJECT_TREE_PANE constructor
683 return;
684
685 if( m_TreeProject->GetGitRepo() )
686 {
688 m_gitIconsInitialized = false;
689 }
690
691 wxFileName fn = pro_dir;
692 bool prjReset = false;
693
694 if( !fn.IsOk() )
695 {
696 fn.Clear();
697 fn.SetPath( PATHS::GetDefaultUserProjectsPath() );
698 fn.SetName( NAMELESS_PROJECT );
700 prjReset = true;
701 }
702
703 bool prjOpened = fn.FileExists();
704
705 // Bind the git repository to the project tree (if it exists and not disabled for this project)
706 if( Pgm().GetCommonSettings()->m_Git.enableGit
707 && !Prj().GetLocalSettings().m_GitIntegrationDisabled )
708 {
709 m_TreeProject->SetGitRepo( KIGIT::PROJECT_GIT_UTILS::GetRepositoryForFile( fn.GetPath().c_str() ) );
710
711 if( m_TreeProject->GetGitRepo() )
712 {
713 // Reset the cancel flag so git operations work after project switches.
714 // EmptyTreePrj() sets this to true during shutdown.
715 m_TreeProject->GitCommon()->SetCancelled( false );
716
717 const char* canonicalWorkDir = git_repository_workdir( m_TreeProject->GetGitRepo() );
718
719 if( canonicalWorkDir )
720 {
722 fn.GetPath(), wxString::FromUTF8( canonicalWorkDir ) );
723 m_TreeProject->GitCommon()->SetProjectDir( symlinkWorkDir );
724 }
725
726 m_TreeProject->GitCommon()->SetUsername( Prj().GetLocalSettings().m_GitRepoUsername );
727 m_TreeProject->GitCommon()->SetSSHKey( Prj().GetLocalSettings().m_GitSSHKey );
728 m_TreeProject->GitCommon()->UpdateCurrentBranchInfo();
729 }
730 }
731
732 // We may have opened a legacy project, in which case GetProjectFileName will return the
733 // name of the migrated (new format) file, which may not have been saved to disk yet.
734 if( !prjOpened && !prjReset )
735 {
737 prjOpened = fn.FileExists();
738
739 // Set the ext back so that in the tree view we see the (not-yet-saved) new file
741 }
742
743 // root tree:
744 m_root = m_TreeProject->AddRoot( fn.GetFullName(), static_cast<int>( TREE_FILE_TYPE::ROOT ),
745 static_cast<int>( TREE_FILE_TYPE::ROOT ) );
746 m_TreeProject->SetItemBold( m_root, true );
747
748 // The main project file is now a JSON file
751
752 m_TreeProject->SetItemData( m_root, data );
753
754 // Now adding all current files if available
755 if( prjOpened )
756 {
757 pro_dir = wxPathOnly( m_Parent->GetProjectFileName() );
758 wxDir dir( pro_dir );
759
760 if( dir.IsOpened() ) // protected dirs will not open, see "man opendir()"
761 {
762 std::vector<wxString> projects = getProjects( dir );
763 wxString filename;
764 bool haveFile = dir.GetFirst( &filename );
765
766 while( haveFile )
767 {
768 if( filename != fn.GetFullName() )
769 {
770 wxString name = dir.GetName() + wxFileName::GetPathSeparator() + filename;
771
772 // Add items living in the project directory, and populate the item
773 // if it is a directory (sub directories will be not populated)
774 addItemToProjectTree( name, m_root, &projects, true );
775 }
776
777 haveFile = dir.GetNext( &filename );
778 }
779 }
780 }
781 else
782 {
783 m_TreeProject->AppendItem( m_root, wxT( "Empty project" ) );
784 }
785
786 m_TreeProject->Expand( m_root );
787
788 // Sort filenames by alphabetic order
789 m_TreeProject->SortChildren( m_root );
790
791 CallAfter(
792 [this] ()
793 {
794 wxLogTrace( traceGit, "PROJECT_TREE_PANE::ReCreateTreePrj: starting timers" );
795 m_gitSyncTimer.Start( 100, wxTIMER_ONE_SHOT );
796 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
797 } );
798}
799
800
802{
803 if( !m_TreeProject->GetGitRepo() )
804 return false;
805
806 GIT_STATUS_HANDLER statusHandler( m_TreeProject->GitCommon() );
807 return statusHandler.HasChangedFiles();
808}
809
810
811void PROJECT_TREE_PANE::onRight( wxTreeEvent& Event )
812{
813 wxTreeItemId curr_item = Event.GetItem();
814
815 // Ensure item is selected (Under Windows right click does not select the item)
816 m_TreeProject->SelectItem( curr_item );
817
818 std::vector<PROJECT_TREE_ITEM*> selection = GetSelectedData();
819 KIGIT_COMMON* git = m_TreeProject->GitCommon();
820
821 bool can_switch_to_project = true;
822 bool can_create_new_directory = true;
823 bool can_open_this_directory = true;
824 bool can_edit = true;
825 bool can_rename = true;
826 bool can_delete = true;
827 bool run_jobs = false;
828
829 bool vcs_has_repo = m_TreeProject->GetGitRepo() != nullptr;
830 bool vcs_can_commit = hasChangedFiles();
831 bool vcs_can_init = !vcs_has_repo;
832 bool vcs_can_set_remote = vcs_has_repo;
833 bool vcs_can_amend = vcs_has_repo && git_repository_head_unborn( m_TreeProject->GetGitRepo() ) == 0;
834 bool gitIntegrationDisabled = Prj().GetLocalSettings().m_GitIntegrationDisabled;
835 // Disable/Enable is a per-project preference toggle, so it's available whenever we
836 // detected a repository for this project or integration is currently disabled.
837 bool vcs_can_remove = vcs_has_repo || gitIntegrationDisabled;
838 bool vcs_can_fetch = vcs_has_repo && git->HasPushAndPullRemote();
839 bool vcs_can_push = vcs_can_fetch && git->HasLocalCommits();
840 bool vcs_can_pull = vcs_can_fetch;
841 bool vcs_can_switch = vcs_has_repo;
842 bool vcs_menu = Pgm().GetCommonSettings()->m_Git.enableGit;
843
844 // Check if the libgit2 library is available via backend
845 bool libgit_init = GetGitBackend() && GetGitBackend()->IsLibraryAvailable();
846
847 vcs_menu &= libgit_init;
848
849 if( selection.size() == 0 )
850 return;
851
852 // Remove things that don't make sense for multiple selections
853 if( selection.size() != 1 )
854 {
855 can_switch_to_project = false;
856 can_create_new_directory = false;
857 can_rename = false;
858 }
859
860 for( PROJECT_TREE_ITEM* item : selection )
861 {
862 // Check for empty project
863 if( !item )
864 {
865 can_switch_to_project = false;
866 can_edit = false;
867 can_rename = false;
868 continue;
869 }
870
871 can_delete = item->CanDelete();
872 can_rename = item->CanRename();
873
874 switch( item->GetType() )
875 {
878 can_rename = false;
879
880 if( item->GetId() == m_TreeProject->GetRootItem() )
881 {
882 can_switch_to_project = false;
883 }
884 else
885 {
886 can_create_new_directory = false;
887 can_open_this_directory = false;
888 }
889 break;
890
892 can_switch_to_project = false;
893 can_edit = false;
894 break;
895
898 can_edit = false;
899 can_switch_to_project = false;
900 can_create_new_directory = false;
901 can_open_this_directory = false;
902 break;
903
905 run_jobs = true;
906 can_edit = false;
908
912
913 default:
914 can_switch_to_project = false;
915 can_create_new_directory = false;
916 can_open_this_directory = false;
917
918 break;
919 }
920 }
921
922 wxMenu popup_menu;
923 wxString text;
924 wxString help_text;
925
926 if( can_switch_to_project )
927 {
928 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_SWITCH_TO_OTHER, _( "Switch to this Project" ),
929 _( "Close all editors, and switch to the selected project" ),
931 popup_menu.AppendSeparator();
932 }
933
934 if( can_create_new_directory )
935 {
936 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_NEWDIR, _( "New Directory..." ),
937 _( "Create a New Directory" ), KiBitmap( BITMAPS::directory ) );
938 }
939
940 if( can_open_this_directory )
941 {
942 if( selection.size() == 1 )
943 {
944#ifdef __APPLE__
945 text = _( "Reveal in Finder" );
946 help_text = _( "Reveals the directory in a Finder window" );
947#else
948 text = _( "Open Directory in File Explorer" );
949 help_text = _( "Opens the directory in the default system file manager" );
950#endif
951 }
952 else
953 {
954#ifdef __APPLE__
955 text = _( "Reveal in Finder" );
956 help_text = _( "Reveals the directories in a Finder window" );
957#else
958 text = _( "Open Directories in File Explorer" );
959 help_text = _( "Opens the directories in the default system file manager" );
960#endif
961 }
962
963 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_OPEN_DIR, text, help_text,
965 }
966
967 if( can_edit )
968 {
969 if( selection.size() == 1 )
970 help_text = _( "Open the file in a Text Editor" );
971 else
972 help_text = _( "Open files in a Text Editor" );
973
974 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_TXTEDIT, _( "Edit in a Text Editor" ), help_text,
976 }
977
978 if( run_jobs && selection.size() == 1 )
979 {
980 KIUI::AddMenuItem( &popup_menu, ID_JOBS_RUN, _( "Run Jobs" ), help_text,
982 }
983
984 if( can_rename )
985 {
986 if( selection.size() == 1 )
987 {
988 text = _( "Rename File..." );
989 help_text = _( "Rename file" );
990 }
991 else
992 {
993 text = _( "Rename Files..." );
994 help_text = _( "Rename files" );
995 }
996
997 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_RENAME, text, help_text,
999 }
1000
1001 if( can_delete )
1002 {
1003 if( selection.size() == 1 )
1004 help_text = _( "Delete the file and its content" );
1005 else
1006 help_text = _( "Delete the files and their contents" );
1007
1008 if( can_switch_to_project
1009 || can_create_new_directory
1010 || can_open_this_directory
1011 || can_edit
1012 || can_rename )
1013 {
1014 popup_menu.AppendSeparator();
1015 }
1016
1017#ifdef __WINDOWS__
1018 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_DELETE, _( "Delete" ), help_text,
1020#else
1021 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_DELETE, _( "Move to Trash" ), help_text,
1023#endif
1024 }
1025
1026 if( vcs_menu )
1027 {
1028 wxMenu* vcs_submenu = new wxMenu();
1029 wxMenu* branch_submenu = new wxMenu();
1030 wxMenuItem* vcs_menuitem = nullptr;
1031
1032 vcs_menuitem = vcs_submenu->Append( ID_GIT_INITIALIZE_PROJECT,
1033 _( "Add Project to Version Control..." ),
1034 _( "Initialize a new repository" ) );
1035 vcs_menuitem->Enable( vcs_can_init );
1036
1037 vcs_menuitem = vcs_submenu->Append( ID_GIT_REMOTE_SETTINGS, _( "Configure Default Remote..." ),
1038 _( "Set or change the default remote URL and credentials" ) );
1039 vcs_menuitem->Enable( vcs_can_set_remote );
1040
1041 vcs_menuitem = vcs_submenu->Append( ID_GIT_COMMIT_PROJECT, _( "Commit Project..." ),
1042 _( "Commit changes to the local repository" ) );
1043 vcs_menuitem->Enable( vcs_can_commit );
1044
1045 vcs_menuitem = vcs_submenu->Append( ID_GIT_AMEND_COMMIT, _( "Amend Last Commit..." ),
1046 _( "Rewrite the most recent commit on the current branch" ) );
1047 vcs_menuitem->Enable( vcs_can_amend );
1048
1049 wxString pushLabel = _( "Push" );
1050 wxString pullLabel = _( "Pull" );
1051
1052 if( !m_gitCurrentUpstream.empty() && !m_gitCurrentBranchName.empty() )
1053 {
1054 pushLabel = wxString::Format( _( "Push %s to %s" ), m_gitCurrentBranchName, m_gitCurrentUpstream );
1055 pullLabel = wxString::Format( _( "Pull from %s" ), m_gitCurrentUpstream );
1056 }
1057
1058 vcs_menuitem =
1059 vcs_submenu->Append( ID_GIT_PUSH, pushLabel, _( "Push committed local changes to remote repository" ) );
1060 vcs_menuitem->Enable( vcs_can_push );
1061
1062 vcs_menuitem =
1063 vcs_submenu->Append( ID_GIT_PULL, pullLabel, _( "Pull changes from remote repository into local" ) );
1064 vcs_menuitem->Enable( vcs_can_pull );
1065
1066 vcs_submenu->AppendSeparator();
1067
1068 vcs_menuitem = vcs_submenu->Append( ID_GIT_COMMIT_FILE, _( "Commit File..." ),
1069 _( "Commit changes to the local repository" ) );
1070 vcs_menuitem->Enable( vcs_can_commit );
1071
1072 vcs_submenu->AppendSeparator();
1073
1074 // vcs_menuitem = vcs_submenu->Append( ID_GIT_COMPARE, _( "Diff" ),
1075 // _( "Show changes between the repository and working tree" ) );
1076 // vcs_menuitem->Enable( vcs_can_diff );
1077
1078 std::vector<wxString> branchNames = m_TreeProject->GitCommon()->GetBranchNames();
1079
1080 // Skip the first one (that is the current branch)
1081 for( size_t ii = 1; ii < branchNames.size() && ii < 6; ++ii )
1082 {
1083 wxString msg = _( "Switch to branch " ) + branchNames[ii];
1084 vcs_menuitem = branch_submenu->Append( ID_GIT_SWITCH_BRANCH + ii, branchNames[ii], msg );
1085 vcs_menuitem->Enable( vcs_can_switch );
1086 }
1087
1088 vcs_menuitem = branch_submenu->Append( ID_GIT_SWITCH_BRANCH, _( "Other..." ),
1089 _( "Switch to a different branch" ) );
1090 vcs_menuitem->Enable( vcs_can_switch );
1091
1092 vcs_submenu->Append( ID_GIT_SWITCH_BRANCH, _( "Switch to Branch" ), branch_submenu );
1093
1094 vcs_submenu->AppendSeparator();
1095
1096 if( gitIntegrationDisabled )
1097 {
1098 vcs_menuitem = vcs_submenu->Append( ID_GIT_REMOVE_VCS, _( "Enable Git Integration" ),
1099 _( "Re-enable Git integration for this project" ) );
1100 }
1101 else
1102 {
1103 vcs_menuitem = vcs_submenu->Append( ID_GIT_REMOVE_VCS, _( "Disable Git Integration" ),
1104 _( "Disable Git integration for this project" ) );
1105 }
1106
1107 vcs_menuitem->Enable( vcs_can_remove );
1108
1109 popup_menu.AppendSeparator();
1110 popup_menu.AppendSubMenu( vcs_submenu, _( "Version Control" ) );
1111 }
1112
1113 if( popup_menu.GetMenuItemCount() > 0 )
1114 PopupMenu( &popup_menu );
1115}
1116
1117
1119{
1120 wxString editorname = Pgm().GetTextEditor();
1121
1122 if( editorname.IsEmpty() )
1123 {
1124 wxMessageBox( _( "No text editor selected in KiCad. Please choose one." ) );
1125 return;
1126 }
1127
1128 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
1129
1130 for( PROJECT_TREE_ITEM* item_data : tree_data )
1131 {
1132 wxString fullFileName = item_data->GetFileName();
1133
1134 if( !fullFileName.IsEmpty() )
1135 {
1136 ExecuteFile( editorname, fullFileName.wc_str(), nullptr, false );
1137 }
1138 }
1139}
1140
1141
1142void PROJECT_TREE_PANE::onDeleteFile( wxCommandEvent& event )
1143{
1144 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
1145
1146 for( PROJECT_TREE_ITEM* item_data : tree_data )
1147 item_data->Delete();
1148}
1149
1150
1151void PROJECT_TREE_PANE::onRenameFile( wxCommandEvent& event )
1152{
1153 wxTreeItemId curr_item = m_TreeProject->GetFocusedItem();
1154 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
1155
1156 // XXX: Unnecessary?
1157 if( tree_data.size() != 1 )
1158 return;
1159
1160 wxString buffer = m_TreeProject->GetItemText( curr_item );
1161 wxString msg = wxString::Format( _( "Change filename: '%s'" ),
1162 tree_data[0]->GetFileName() );
1163 wxTextEntryDialog dlg( wxGetTopLevelParent( this ), msg, _( "Change filename" ), buffer );
1164
1165 if( dlg.ShowModal() != wxID_OK )
1166 return; // canceled by user
1167
1168 buffer = dlg.GetValue();
1169 buffer.Trim( true );
1170 buffer.Trim( false );
1171
1172 if( buffer.IsEmpty() )
1173 return; // empty file name not allowed
1174
1175 tree_data[0]->Rename( buffer, true );
1176 m_isRenaming = true;
1177}
1178
1179
1180void PROJECT_TREE_PANE::onSelect( wxTreeEvent& Event )
1181{
1182 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
1183
1184 if( tree_data.size() != 1 )
1185 return;
1186
1187 // Bookmark the selected item but don't try and activate it until later. If we do it now,
1188 // there will be more events at least on Windows in this frame that will steal focus from
1189 // any newly launched windows
1190 m_selectedItem = tree_data[0];
1191}
1192
1193
1194void PROJECT_TREE_PANE::onIdle( wxIdleEvent& aEvent )
1195{
1196 // Idle executes once all other events finished processing. This makes it ideal to launch
1197 // a new window without starting Focus wars.
1198 if( m_watcherNeedReset )
1199 {
1200 m_selectedItem = nullptr;
1202 }
1203
1204 if( m_selectedItem != nullptr && m_TreeProject->GetRootItem().IsOk() )
1205 {
1206 // Make sure m_selectedItem still exists in the tree before activating it.
1207 std::vector<wxTreeItemId> validItemIds;
1208 m_TreeProject->GetItemsRecursively( m_TreeProject->GetRootItem(), validItemIds );
1209
1210 for( wxTreeItemId id : validItemIds )
1211 {
1212 if( GetItemIdData( id ) == m_selectedItem )
1213 {
1214 // Activate launches a window which may run the event loop on top of us and cause
1215 // onIdle to get called again, so be sure to null out m_selectedItem first.
1217 m_selectedItem = nullptr;
1218
1219 item->Activate( this );
1220 break;
1221 }
1222 }
1223 }
1224}
1225
1226
1227void PROJECT_TREE_PANE::onExpand( wxTreeEvent& Event )
1228{
1229 wxTreeItemId itemId = Event.GetItem();
1230 PROJECT_TREE_ITEM* tree_data = GetItemIdData( itemId );
1231
1232 if( !tree_data )
1233 return;
1234
1235 if( tree_data->GetType() != TREE_FILE_TYPE::DIRECTORY )
1236 return;
1237
1238 // explore list of non populated subdirs, and populate them
1239 wxTreeItemIdValue cookie;
1240 wxTreeItemId kid = m_TreeProject->GetFirstChild( itemId, cookie );
1241
1242#ifndef __WINDOWS__
1243 bool subdir_populated = false;
1244#endif
1245
1246 for( ; kid.IsOk(); kid = m_TreeProject->GetNextChild( itemId, cookie ) )
1247 {
1248 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
1249
1250 if( !itemData || itemData->GetType() != TREE_FILE_TYPE::DIRECTORY )
1251 continue;
1252
1253 if( itemData->IsPopulated() )
1254 continue;
1255
1256 wxString fileName = itemData->GetFileName();
1257 wxDir dir( fileName );
1258
1259 if( dir.IsOpened() )
1260 {
1261 std::vector<wxString> projects = getProjects( dir );
1262 wxString dir_filename;
1263 bool haveFile = dir.GetFirst( &dir_filename );
1264
1265 while( haveFile )
1266 {
1267 // Add name to tree item, but do not recurse in subdirs:
1268 wxString name = fileName + wxFileName::GetPathSeparator() + dir_filename;
1269 addItemToProjectTree( name, kid, &projects, false );
1270
1271 haveFile = dir.GetNext( &dir_filename );
1272 }
1273
1274 itemData->SetPopulated( true ); // set state to populated
1275
1276#ifndef __WINDOWS__
1277 subdir_populated = true;
1278#endif
1279 }
1280
1281 // Sort filenames by alphabetic order
1282 m_TreeProject->SortChildren( kid );
1283 }
1284
1285#ifndef __WINDOWS__
1286 if( subdir_populated )
1287 m_watcherNeedReset = true;
1288#endif
1289}
1290
1291
1292std::vector<PROJECT_TREE_ITEM*> PROJECT_TREE_PANE::GetSelectedData()
1293{
1294 wxArrayTreeItemIds selection;
1295 std::vector<PROJECT_TREE_ITEM*> data;
1296
1297 m_TreeProject->GetSelections( selection );
1298
1299 for( const wxTreeItemId itemId : selection )
1300 {
1301 PROJECT_TREE_ITEM* item = GetItemIdData( itemId );
1302
1303 if( !item )
1304 {
1305 wxLogTrace( traceGit, wxS( "Null tree item returned for selection, dynamic_cast failed?" ) );
1306 continue;
1307 }
1308
1309 data.push_back( item );
1310 }
1311
1312 return data;
1313}
1314
1315
1317{
1318 return dynamic_cast<PROJECT_TREE_ITEM*>( m_TreeProject->GetItemData( aId ) );
1319}
1320
1321
1322wxTreeItemId PROJECT_TREE_PANE::findSubdirTreeItem( const wxString& aSubDir )
1323{
1324 wxString prj_dir = wxPathOnly( m_Parent->GetProjectFileName() );
1325
1326 // If the subdir is the current working directory, return m_root
1327 // in main list:
1328 if( prj_dir == aSubDir )
1329 return m_root;
1330
1331 // The subdir is in the main tree or in a subdir: Locate it
1332 wxTreeItemIdValue cookie;
1333 wxTreeItemId root_id = m_root;
1334 std::stack<wxTreeItemId> subdirs_id;
1335
1336 wxTreeItemId child = m_TreeProject->GetFirstChild( root_id, cookie );
1337
1338 while( true )
1339 {
1340 if( ! child.IsOk() )
1341 {
1342 if( subdirs_id.empty() ) // all items were explored
1343 {
1344 root_id = child; // Not found: return an invalid wxTreeItemId
1345 break;
1346 }
1347 else
1348 {
1349 root_id = subdirs_id.top();
1350 subdirs_id.pop();
1351 child = m_TreeProject->GetFirstChild( root_id, cookie );
1352
1353 if( !child.IsOk() )
1354 continue;
1355 }
1356 }
1357
1358 PROJECT_TREE_ITEM* itemData = GetItemIdData( child );
1359
1360 if( itemData && ( itemData->GetType() == TREE_FILE_TYPE::DIRECTORY ) )
1361 {
1362 if( itemData->GetFileName() == aSubDir ) // Found!
1363 {
1364 root_id = child;
1365 break;
1366 }
1367
1368 // child is a subdir, push in list to explore it later
1369 if( itemData->IsPopulated() )
1370 subdirs_id.push( child );
1371 }
1372
1373 child = m_TreeProject->GetNextChild( root_id, cookie );
1374 }
1375
1376 return root_id;
1377}
1378
1379
1380void PROJECT_TREE_PANE::onFileSystemEvent( wxFileSystemWatcherEvent& event )
1381{
1382 // No need to process events when we're shutting down
1383 if( !m_watcher )
1384 return;
1385
1386 // Ignore events that are not file creation, deletion, renaming or modification because
1387 // they are not relevant to the project tree.
1388 if( !( event.GetChangeType() & ( wxFSW_EVENT_CREATE |
1389 wxFSW_EVENT_DELETE |
1390 wxFSW_EVENT_RENAME |
1391 wxFSW_EVENT_MODIFY ) ) )
1392 {
1393 return;
1394 }
1395
1396 const wxFileName& pathModified = event.GetPath();
1397
1398 // Ignore events from .history directory (local backup)
1399 if( pathModified.GetFullPath().Contains( wxS( ".history" ) ) )
1400 return;
1401
1402 wxString subdir = pathModified.GetPath();
1403 wxString fn = pathModified.GetFullPath();
1404
1405 // Adjust directories to look like a file item (path and name).
1406 if( pathModified.GetFullName().IsEmpty() )
1407 {
1408 subdir = subdir.BeforeLast( '/' );
1409 fn = fn.BeforeLast( '/' );
1410 }
1411
1412 wxTreeItemId root_id = findSubdirTreeItem( subdir );
1413
1414 if( !root_id.IsOk() )
1415 return;
1416
1417 CallAfter( [this] ()
1418 {
1419 wxLogTrace( traceGit, wxS( "File system event detected, updating tree cache" ) );
1420 m_gitStatusTimer.Start( 1500, wxTIMER_ONE_SHOT );
1421 } );
1422
1423 wxTreeItemIdValue cookie; // dummy variable needed by GetFirstChild()
1424 wxTreeItemId kid = m_TreeProject->GetFirstChild( root_id, cookie );
1425
1426 switch( event.GetChangeType() )
1427 {
1428 case wxFSW_EVENT_CREATE:
1429 {
1430 wxTreeItemId newitem = addItemToProjectTree( fn, root_id, nullptr, true );
1431
1432 // If we are in the process of renaming a file, select the new one
1433 // This is needed for MSW and OSX, since we don't get RENAME events from them, just a
1434 // pair of DELETE and CREATE events.
1435 if( m_isRenaming && newitem.IsOk() )
1436 {
1437 m_TreeProject->SelectItem( newitem );
1438 m_isRenaming = false;
1439 }
1440 }
1441 break;
1442
1443 case wxFSW_EVENT_DELETE:
1444 while( kid.IsOk() )
1445 {
1446 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
1447
1448 if( itemData && itemData->GetFileName() == fn )
1449 {
1450 m_TreeProject->Delete( kid );
1451 return;
1452 }
1453 kid = m_TreeProject->GetNextChild( root_id, cookie );
1454 }
1455 break;
1456
1457 case wxFSW_EVENT_RENAME :
1458 {
1459 const wxFileName& newpath = event.GetNewPath();
1460 wxString newdir = newpath.GetPath();
1461 wxString newfn = newpath.GetFullPath();
1462
1463 while( kid.IsOk() )
1464 {
1465 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
1466
1467 if( itemData && itemData->GetFileName() == fn )
1468 {
1469 m_TreeProject->Delete( kid );
1470 break;
1471 }
1472
1473 kid = m_TreeProject->GetNextChild( root_id, cookie );
1474 }
1475
1476 // Add the new item only if it is not the current project file (root item).
1477 // Remember: this code is called by a wxFileSystemWatcherEvent event, and not always
1478 // called after an actual file rename, and the cleanup code does not explore the
1479 // root item, because it cannot be renamed by the user. Also, ensure the new file
1480 // actually exists on the file system before it is readded. On Linux, moving a file
1481 // to the trash can cause the same path to be returned in both the old and new paths
1482 // of the event, even though the file isn't there anymore.
1483 PROJECT_TREE_ITEM* rootData = GetItemIdData( root_id );
1484
1485 if( rootData && newpath.Exists() && ( newfn != rootData->GetFileName() ) )
1486 {
1487 wxTreeItemId newroot_id = findSubdirTreeItem( newdir );
1488 wxTreeItemId newitem = addItemToProjectTree( newfn, newroot_id, nullptr, true );
1489
1490 // If the item exists, select it
1491 if( newitem.IsOk() )
1492 m_TreeProject->SelectItem( newitem );
1493 }
1494
1495 m_isRenaming = false;
1496 }
1497 break;
1498
1499 default:
1500 return;
1501 }
1502
1503 // Sort filenames by alphabetic order
1504 m_TreeProject->SortChildren( root_id );
1505}
1506
1507
1509{
1510 m_watcherNeedReset = false;
1511
1512 wxString prj_dir = wxPathOnly( m_Parent->GetProjectFileName() );
1513
1514#if defined( _WIN32 )
1515 KISTATUSBAR* statusBar = static_cast<KISTATUSBAR*>( m_Parent->GetStatusBar() );
1516
1517 if( KIPLATFORM::ENV::IsNetworkPath( prj_dir ) )
1518 {
1519 // Due to a combination of a bug in SAMBA sending bad change event IDs and wxWidgets
1520 // choosing to fault on an invalid event ID instead of sanely ignoring them we need to
1521 // avoid spawning a filewatcher. Unfortunately this punishes corporate environments with
1522 // Windows Server shares :/
1523 m_Parent->m_FileWatcherInfo = _( "Network path: not monitoring folder changes" );
1524 statusBar->SetEllipsedTextField( m_Parent->m_FileWatcherInfo, 1 );
1525 return;
1526 }
1527 else
1528 {
1529 m_Parent->m_FileWatcherInfo = _( "Local path: monitoring folder changes" );
1530 statusBar->SetEllipsedTextField( m_Parent->m_FileWatcherInfo, 1 );
1531 }
1532#endif
1533
1534 // Prepare file watcher:
1535 if( m_watcher )
1536 {
1537 m_watcher->RemoveAll();
1538 }
1539 else
1540 {
1541 // Create a wxWidgets log handler to catch errors during watcher creation
1542 // We need to to this because we cannot get error codes from the wxFileSystemWatcher
1543 // constructor. On Linux, if inotify cannot be initialized (usually due to resource limits),
1544 // wxWidgets will throw a system error and then, we throw another error below, trying to
1545 // add paths to a null watcher. We skip this by installing a temporary log handler that
1546 // catches errors during watcher creation and aborts if any error is detected.
1547 class WatcherLogHandler : public wxLog
1548 {
1549 public:
1550 explicit WatcherLogHandler( bool* err ) :
1551 m_err( err )
1552 {
1553 if( m_err )
1554 *m_err = false;
1555 }
1556
1557 protected:
1558 void DoLogTextAtLevel( wxLogLevel level, const wxString& text ) override
1559 {
1560 if( m_err && ( level == wxLOG_Error || level == wxLOG_FatalError ) )
1561 *m_err = true;
1562 }
1563
1564 private:
1565 bool* m_err;
1566 };
1567
1568 bool watcherHasError = false;
1569 WatcherLogHandler tmpLog( &watcherHasError );
1570 wxLog* oldLog = wxLog::SetActiveTarget( &tmpLog );
1571
1573 m_watcher->SetOwner( this );
1574
1575 // Restore previous log handler
1576 wxLog::SetActiveTarget( oldLog );
1577
1578 if( watcherHasError )
1579 {
1580 return;
1581 }
1582 }
1583
1584 // We can see wxString under a debugger, not a wxFileName
1585 wxFileName fn;
1586 fn.AssignDir( prj_dir );
1587 fn.DontFollowLink();
1588
1589 // Add directories which should be monitored.
1590 // under windows, we add the curr dir and all subdirs
1591 // under unix, we add only the curr dir and the populated subdirs
1592 // see http://docs.wxwidgets.org/trunk/classwx_file_system_watcher.htm
1593 // under unix, the file watcher needs more work to be efficient
1594 // moreover, under wxWidgets 2.9.4, AddTree does not work properly.
1595 {
1596 wxLogNull logNo; // avoid log messages
1597#ifdef __WINDOWS__
1598 if( ! m_watcher->AddTree( fn ) )
1599 {
1600 wxLogTrace( tracePathsAndFiles, "%s: failed to add '%s'\n", __func__,
1601 TO_UTF8( fn.GetFullPath() ) );
1602 return;
1603 }
1604 }
1605#else
1606 if( !m_watcher->Add( fn ) )
1607 {
1608 wxLogTrace( tracePathsAndFiles, "%s: failed to add '%s'\n", __func__,
1609 TO_UTF8( fn.GetFullPath() ) );
1610 return;
1611 }
1612 }
1613
1614 if( m_TreeProject->IsEmpty() )
1615 return;
1616
1617 // Add subdirs
1618 wxTreeItemIdValue cookie;
1619 wxTreeItemId root_id = m_root;
1620
1621 std::stack < wxTreeItemId > subdirs_id;
1622
1623 wxTreeItemId kid = m_TreeProject->GetFirstChild( root_id, cookie );
1624 int total_watch_count = 0;
1625
1626 while( total_watch_count < ADVANCED_CFG::GetCfg().m_MaxFilesystemWatchers )
1627 {
1628 if( !kid.IsOk() )
1629 {
1630 if( subdirs_id.empty() ) // all items were explored
1631 {
1632 break;
1633 }
1634 else
1635 {
1636 root_id = subdirs_id.top();
1637 subdirs_id.pop();
1638 kid = m_TreeProject->GetFirstChild( root_id, cookie );
1639
1640 if( !kid.IsOk() )
1641 continue;
1642 }
1643 }
1644
1645 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
1646
1647 if( itemData && itemData->GetType() == TREE_FILE_TYPE::DIRECTORY )
1648 {
1649 // we can see wxString under a debugger, not a wxFileName
1650 const wxString& path = itemData->GetFileName();
1651
1652 // Skip .history directory and its descendants (local backup)
1653 if( path.Contains( wxS( ".history" ) ) )
1654 {
1655 kid = m_TreeProject->GetNextChild( root_id, cookie );
1656 continue;
1657 }
1658
1659 wxLogTrace( tracePathsAndFiles, "%s: add '%s'\n", __func__, TO_UTF8( path ) );
1660
1661 if( wxFileName::IsDirReadable( path ) ) // linux whines about watching protected dir
1662 {
1663 {
1664 // Silence OS errors that come from the watcher
1665 wxLogNull silence;
1666 fn.AssignDir( path );
1667 m_watcher->Add( fn );
1668 total_watch_count++;
1669 }
1670
1671 // if kid is a subdir, push in list to explore it later
1672 if( itemData->IsPopulated() && m_TreeProject->GetChildrenCount( kid ) )
1673 subdirs_id.push( kid );
1674 }
1675 }
1676
1677 kid = m_TreeProject->GetNextChild( root_id, cookie );
1678 }
1679
1680 if( total_watch_count >= ADVANCED_CFG::GetCfg().m_MaxFilesystemWatchers )
1681 wxLogTrace( tracePathsAndFiles, "%s: too many directories to watch\n", __func__ );
1682#endif
1683
1684#if defined(DEBUG) && 1
1685 wxArrayString paths;
1686 m_watcher->GetWatchedPaths( &paths );
1687 wxLogTrace( tracePathsAndFiles, "%s: watched paths:", __func__ );
1688
1689 for( unsigned ii = 0; ii < paths.GetCount(); ii++ )
1690 wxLogTrace( tracePathsAndFiles, " %s\n", TO_UTF8( paths[ii] ) );
1691#endif
1692 }
1693
1694
1696{
1697 // Make sure we don't try to inspect the tree after we've deleted its items.
1699
1700 m_TreeProject->DeleteAllItems();
1701
1702 if( m_TreeProject->GetGitRepo() )
1703 {
1704 KIGIT_COMMON* common = m_TreeProject->GitCommon();
1705 common->SetCancelled( true );
1706
1707 std::unique_lock<std::mutex> lock( common->m_gitActionMutex, std::try_to_lock );
1708
1709 constexpr auto kGraceMs = std::chrono::seconds( 2 );
1710 auto graceEnd = std::chrono::steady_clock::now() + kGraceMs;
1711
1712 while( !lock.owns_lock() && std::chrono::steady_clock::now() < graceEnd )
1713 {
1714 if( lock.try_lock() )
1715 break;
1716 std::this_thread::sleep_for( std::chrono::milliseconds( 50 ) );
1717 }
1718
1719 constexpr auto kCheckInterval = std::chrono::seconds( 30 );
1720 bool userAbandoned = false;
1721
1722 while( !lock.owns_lock() && !userAbandoned )
1723 {
1724 auto intervalEnd = std::chrono::steady_clock::now() + kCheckInterval;
1725
1726 {
1727 wxProgressDialog progress( _( "Please wait" ),
1728 _( "Closing project..." ),
1729 100, this,
1730 wxPD_APP_MODAL | wxPD_SMOOTH );
1731
1732 while( !lock.try_lock()
1733 && std::chrono::steady_clock::now() < intervalEnd )
1734 {
1735 progress.Pulse();
1736 std::this_thread::sleep_for( std::chrono::milliseconds( 100 ) );
1737 wxYield();
1738 }
1739 }
1740
1741 if( lock.owns_lock() )
1742 break;
1743
1744 wxMessageDialog ask( this,
1745 _( "A Git operation is still running.\n"
1746 "Keep waiting, or abandon?" ),
1747 _( "Git Operation Delayed" ),
1748 wxYES_NO | wxICON_QUESTION );
1749 ask.SetYesNoLabels( _( "Keep Waiting" ), _( "Abandon" ) );
1750
1751 if( ask.ShowModal() == wxID_NO )
1752 userAbandoned = true;
1753 }
1754
1755 if( userAbandoned )
1756 {
1757 git_repository* orphan = m_TreeProject->GetGitRepo();
1758 std::shared_ptr<KIGIT_COMMON> oldCommon = m_TreeProject->TakeGitCommon();
1759 m_TreeProject->SetGitRepo( nullptr );
1760
1761 // Register the cleanup thread with the orphan registry so the
1762 // shutdown path can wait for it before tearing down libgit2.
1763 // Cancellation has already been requested via SetCancelled() above,
1764 // and progress_cb/transfer_progress_cb honour it so long-running
1765 // fetches will exit with GIT_EUSER as soon as they hit a callback.
1766
1767 GIT_BACKEND* backend = GetGitBackend();
1768 wxString projectDir = oldCommon ? oldCommon->GetProjectDir() : wxString();
1769
1770 auto cleanup = [orphan, old = std::move( oldCommon )]() mutable
1771 {
1772 std::lock_guard<std::mutex> g( old->m_gitActionMutex );
1773 git_repository_free( orphan );
1774 };
1775
1776 bool registered = false;
1777
1778 if( backend )
1779 {
1780 std::string label = "abandon close " + projectDir.ToStdString();
1781 registered = backend->OrphanRegistry().Register( label, std::move( cleanup ) );
1782 }
1783
1784 if( !registered )
1785 {
1786 // Either no backend is available, or the registry has already
1787 // entered shutdown. Fall back to freeing synchronously so the
1788 // repository handle is not leaked; this blocks on the git
1789 // action mutex but is safe because libgit2 shutdown is gated
1790 // on the same registry.
1791
1792 cleanup();
1793 }
1794 }
1795 else
1796 {
1797 git_repository* repo = m_TreeProject->GetGitRepo();
1799 m_TreeProject->SetGitRepo( nullptr );
1800 }
1801 }
1802}
1803
1804
1806{
1807 git_repository* repo = m_TreeProject ? m_TreeProject->GetGitRepo() : nullptr;
1808
1809 if( !repo )
1810 return;
1811
1812 KIGIT_COMMON* common = m_TreeProject->GitCommon();
1813 common->SetCancelled( true );
1814
1815 std::unique_lock<std::mutex> gitLock( common->m_gitActionMutex, std::try_to_lock );
1816
1817 if( gitLock.owns_lock() )
1818 {
1819 m_TreeProject->SetGitRepo( nullptr );
1820 git_repository_free( repo );
1821 return;
1822 }
1823
1824 // A background fetch or status update still holds the git action mutex, so freeing the
1825 // repository here would pull it out from under a running libgit2 call. Hand the handle
1826 // and its KIGIT_COMMON to the orphan registry, which frees it once the mutex is released.
1827 // Any worker pinning the common keeps it alive until it drops its own shared handle.
1828 std::shared_ptr<KIGIT_COMMON> oldCommon = m_TreeProject->TakeGitCommon();
1829
1830 GIT_BACKEND* backend = GetGitBackend();
1831 wxString projectDir = oldCommon ? oldCommon->GetProjectDir() : wxString();
1832
1833 auto cleanup = [repo, old = std::move( oldCommon )]() mutable
1834 {
1835 std::lock_guard<std::mutex> g( old->m_gitActionMutex );
1836 git_repository_free( repo );
1837 };
1838
1839 bool registered = false;
1840
1841 if( backend )
1842 {
1843 std::string label = "release repo " + projectDir.ToStdString();
1844 registered = backend->OrphanRegistry().Register( label, std::move( cleanup ) );
1845 }
1846
1847 if( !registered )
1848 {
1849 // The registry is unavailable or shutting down, so block on the mutex here rather
1850 // than leak the handle.
1851 cleanup();
1852 }
1853}
1854
1855
1856void PROJECT_TREE_PANE::onThemeChanged( wxSysColourChangedEvent &aEvent )
1857{
1859 m_TreeProject->LoadIcons();
1860 m_TreeProject->Refresh();
1861
1862 aEvent.Skip();
1863}
1864
1865
1866void PROJECT_TREE_PANE::onPaint( wxPaintEvent& event )
1867{
1868 wxRect rect( wxPoint( 0, 0 ), GetClientSize() );
1869 wxPaintDC dc( this );
1870
1871 dc.SetBrush( wxSystemSettings::GetColour( wxSYS_COLOUR_FRAMEBK ) );
1872 dc.SetPen( wxPen( wxSystemSettings::GetColour( wxSYS_COLOUR_ACTIVEBORDER ), 1 ) );
1873
1874 dc.DrawLine( rect.GetLeft(), rect.GetTop(), rect.GetLeft(), rect.GetBottom() );
1875 dc.DrawLine( rect.GetRight(), rect.GetTop(), rect.GetRight(), rect.GetBottom() );
1876}
1877
1878
1879void KICAD_MANAGER_FRAME::OnChangeWatchedPaths( wxCommandEvent& aEvent )
1880{
1881 m_projectTreePane->FileWatcherReset();
1882}
1883
1884
1885// Prompts for git credentials after an auth failure. Returns false if cancelled.
1886static bool promptForGitCredentials( wxWindow* aParent, KIGIT_COMMON* aCommon )
1887{
1888 DIALOG_GIT_CREDENTIALS dlg( aParent, aCommon->GetRemote(), aCommon->GetConnType(), aCommon->GetUsername(),
1889 wxEmptyString );
1890
1891 if( dlg.ShowModal() != wxID_OK )
1892 return false;
1893
1894 aCommon->SetUsername( dlg.GetUsername() );
1895 aCommon->SetPassword( dlg.GetPassword() );
1896
1897 if( dlg.GetConnType() == KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_SSH && !dlg.GetSSHKey().IsEmpty() )
1898 {
1899 aCommon->SetSSHKey( dlg.GetSSHKey() );
1900 }
1901
1902 if( dlg.SaveCredentials() )
1903 {
1904 KIPLATFORM::SECRETS::StoreSecret( aCommon->GetRemote(), aCommon->GetUsername(), dlg.GetPassword() );
1905 }
1906
1907 aCommon->ClearAuthFailure();
1908 aCommon->TestedTypes() = 0;
1909 aCommon->ResetNextKey();
1910 return true;
1911}
1912
1913
1914void PROJECT_TREE_PANE::onGitInitializeProject( wxCommandEvent& aEvent )
1915{
1916 PROJECT_TREE_ITEM* tree_data = GetItemIdData( m_TreeProject->GetRootItem() );
1917
1918 wxString dir = tree_data->GetDir();
1919
1920 if( dir.empty() )
1921 {
1922 wxLogError( "Failed to initialize git project: project directory is empty." );
1923 return;
1924 }
1925
1926 GIT_INIT_HANDLER initHandler( m_TreeProject->GitCommon() );
1927 wxWindow* topLevelParent = wxGetTopLevelParent( this );
1928
1929 if( initHandler.IsRepository( dir ) )
1930 {
1931 DisplayInfoMessage( topLevelParent,
1932 _( "The selected directory is already a Git project." ) );
1933 return;
1934 }
1935
1936 InitResult result = initHandler.InitializeRepository( dir );
1938 {
1939 DisplayErrorMessage( m_parent, _( "Failed to initialize Git project." ),
1940 initHandler.GetErrorString() );
1941 return;
1942 }
1943
1944 m_gitLastError = GIT_ERROR_NONE;
1945
1946 m_TreeProject->GitCommon()->SetCancelled( false );
1947
1948 const char* canonicalWorkDir = git_repository_workdir( initHandler.GetRepo() );
1949
1950 if( canonicalWorkDir )
1951 {
1953 dir, wxString::FromUTF8( canonicalWorkDir ) );
1954 m_TreeProject->GitCommon()->SetProjectDir( symlinkWorkDir );
1955 }
1956
1957 DIALOG_GIT_REPOSITORY dlg( topLevelParent, initHandler.GetRepo() );
1958 dlg.SetTitle( _( "Set default remote" ) );
1959 dlg.SetSkipButtonLabel( _( "Skip" ) );
1960
1961 if( dlg.ShowModal() != wxID_OK )
1962 return;
1963
1964 // Set up the remote
1965 RemoteConfig remoteConfig;
1966 remoteConfig.url = dlg.GetRepoURL();
1967 remoteConfig.username = dlg.GetUsername();
1968 remoteConfig.password = dlg.GetPassword();
1969 remoteConfig.sshKey = dlg.GetRepoSSHPath();
1970 remoteConfig.connType = dlg.GetRepoType();
1971
1972 if( !initHandler.SetupRemote( remoteConfig ) )
1973 {
1974 DisplayErrorMessage( m_parent, _( "Failed to set default remote." ),
1975 initHandler.GetErrorString() );
1976 return;
1977 }
1978
1979 m_gitLastError = GIT_ERROR_NONE;
1980
1981 KIGIT_COMMON* common = m_TreeProject->GitCommon();
1982
1983 while( true )
1984 {
1985 common->ClearAuthFailure();
1986
1987 GIT_PULL_HANDLER handler( common );
1988 handler.SetProgressReporter(
1989 std::make_unique<WX_PROGRESS_REPORTER>( this, _( "Fetch Remote" ), 1, PR_NO_ABORT ) );
1990
1991 if( handler.PerformFetch() )
1992 break;
1993
1994 if( common->WasAuthFailure() && promptForGitCredentials( m_parent, common ) )
1995 continue;
1996
1997 break;
1998 }
1999
2003
2007 Prj().GetLocalSettings().m_GitRepoType = "https";
2008 else
2009 Prj().GetLocalSettings().m_GitRepoType = "local";
2010}
2011
2012
2013void PROJECT_TREE_PANE::onGitRemoteSettings( wxCommandEvent& aEvent )
2014{
2015 KIGIT_COMMON* common = m_TreeProject->GitCommon();
2016 git_repository* repo = common ? common->GetRepo() : nullptr;
2017
2018 if( !repo )
2019 return;
2020
2021 wxString existingURL;
2022 git_remote* remote = nullptr;
2023
2024 if( git_remote_lookup( &remote, repo, "origin" ) == GIT_OK )
2025 {
2026 KIGIT::GitRemotePtr remotePtr( remote );
2027
2028 if( const char* url = git_remote_url( remote ) )
2029 existingURL = wxString::FromUTF8( url );
2030 }
2031
2032 wxWindow* topLevelParent = wxGetTopLevelParent( this );
2033 DIALOG_GIT_REPOSITORY dlg( topLevelParent, repo, existingURL );
2034 dlg.SetTitle( _( "Configure Default Remote" ) );
2035
2036 if( dlg.ShowModal() != wxID_OK )
2037 return;
2038
2039 GIT_INIT_HANDLER initHandler( common );
2040
2041 RemoteConfig remoteConfig;
2042 remoteConfig.url = dlg.GetRepoURL();
2043 remoteConfig.username = dlg.GetUsername();
2044 remoteConfig.password = dlg.GetPassword();
2045 remoteConfig.sshKey = dlg.GetRepoSSHPath();
2046 remoteConfig.connType = dlg.GetRepoType();
2047
2048 if( !initHandler.SetupRemote( remoteConfig ) )
2049 {
2050 DisplayErrorMessage( topLevelParent, _( "Failed to configure remote." ), initHandler.GetErrorString() );
2051 return;
2052 }
2053
2057
2061 Prj().GetLocalSettings().m_GitRepoType = "https";
2062 else
2063 Prj().GetLocalSettings().m_GitRepoType = "local";
2064
2065 common->UpdateCurrentBranchInfo();
2066 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
2067}
2068
2069
2070void PROJECT_TREE_PANE::onGitCompare( wxCommandEvent& aEvent )
2071{
2072
2073}
2074
2075
2076void PROJECT_TREE_PANE::onGitPullProject( wxCommandEvent& aEvent )
2077{
2078 git_repository* repo = m_TreeProject->GetGitRepo();
2079
2080 if( !repo )
2081 return;
2082
2083 KIGIT_COMMON* common = m_TreeProject->GitCommon();
2084
2085 while( true )
2086 {
2087 common->ClearAuthFailure();
2088
2089 GIT_PULL_HANDLER handler( common );
2090 handler.SetProgressReporter(
2091 std::make_unique<WX_PROGRESS_REPORTER>( this, _( "Fetch Remote" ), 1, PR_NO_ABORT ) );
2092
2093 PullResult pullResult = handler.PerformPull();
2094
2095 if( pullResult >= PullResult::Success )
2096 {
2097 wxString upstream = common->GetUpstreamShorthand();
2098 wxString branch = common->GetCurrentBranchName();
2099
2100 switch( pullResult )
2101 {
2103 showGitFeedback( upstream.empty() ? _( "Already up to date." )
2104 : wxString::Format( _( "Already up to date with %s." ), upstream ) );
2105 break;
2106
2109 upstream.empty() || branch.empty()
2110 ? _( "Pulled changes (fast-forward)." )
2111 : wxString::Format( _( "Pulled %s into %s (fast-forward)." ), upstream, branch ) );
2112 break;
2113
2114 default:
2115 showGitFeedback( upstream.empty() || branch.empty()
2116 ? _( "Pulled changes." )
2117 : wxString::Format( _( "Pulled %s into %s." ), upstream, branch ) );
2118 break;
2119 }
2120
2121 break;
2122 }
2123
2124 if( pullResult == PullResult::Conflict )
2125 {
2126 wxRichMessageDialog dlg( wxGetTopLevelParent( this ),
2127 _( "Your local changes and the remote changes conflict, so the pull was "
2128 "cancelled and nothing was changed.\n\n"
2129 "You can discard your local commits and use the remote version, or "
2130 "rebase your commits on top of the remote." ),
2131 _( "Pull Conflict" ),
2132 wxYES_NO | wxCANCEL | wxCANCEL_DEFAULT | wxICON_WARNING | wxCENTER );
2133
2134 dlg.SetYesNoCancelLabels( _( "&Use Remote (discard my changes)" ), _( "&Rebase" ), _( "Cancel" ) );
2135
2136 int choice = dlg.ShowModal();
2137
2138 if( choice == wxID_YES )
2139 {
2140 if( handler.ResetToUpstream() )
2141 showGitFeedback( _( "Reset to the remote version." ) );
2142 else
2143 DisplayErrorMessage( m_parent, _( "Failed to reset to remote" ), handler.GetErrorString() );
2144 }
2145 else if( choice == wxID_NO )
2146 {
2147 PullResult rebaseResult = handler.RebaseOntoUpstream();
2148
2149 if( rebaseResult == PullResult::Success )
2150 showGitFeedback( _( "Rebased your changes onto the remote." ) );
2151 else
2152 DisplayErrorMessage( m_parent, _( "Could not rebase" ), handler.GetErrorString() );
2153 }
2154 else
2155 {
2156 showGitFeedback( _( "Pull cancelled." ) );
2157 }
2158
2159 break;
2160 }
2161
2162 if( common->WasAuthFailure() && promptForGitCredentials( m_parent, common ) )
2163 continue;
2164
2165 showGitFeedback( _( "Pull failed." ) );
2166 DisplayErrorMessage( m_parent, _( "Failed to pull project" ), handler.GetErrorString() );
2167 break;
2168 }
2169
2170 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
2171}
2172
2173
2174void PROJECT_TREE_PANE::onGitPushProject( wxCommandEvent& aEvent )
2175{
2176 git_repository* repo = m_TreeProject->GetGitRepo();
2177
2178 if( !repo )
2179 return;
2180
2181 KIGIT_COMMON* common = m_TreeProject->GitCommon();
2182 bool force = false;
2183
2184 while( true )
2185 {
2186 common->ClearAuthFailure();
2187
2188 GIT_PUSH_HANDLER handler( common );
2189 handler.SetProgressReporter(
2190 std::make_unique<WX_PROGRESS_REPORTER>( this, _( "Fetch Remote" ), 1, PR_NO_ABORT ) );
2191
2192 PushResult pushResult = handler.PerformPush( force );
2193
2194 if( pushResult == PushResult::Success )
2195 {
2196 wxString upstream = common->GetUpstreamShorthand();
2197 wxString branch = common->GetCurrentBranchName();
2198
2199 showGitFeedback( upstream.empty() || branch.empty()
2200 ? _( "Pushed to remote." )
2201 : wxString::Format( _( "Pushed %s to %s." ), branch, upstream ) );
2202 break;
2203 }
2204
2205 if( common->WasAuthFailure() && promptForGitCredentials( m_parent, common ) )
2206 continue;
2207
2208 if( pushResult == PushResult::NonFastForward && !force )
2209 {
2210 wxString upstream = common->GetUpstreamShorthand();
2211
2212 wxRichMessageDialog dlg(
2213 wxGetTopLevelParent( this ),
2214 wxString::Format( _( "Push rejected: your local branch and %s have diverged.\n\n"
2215 "This usually happens after amending or rewriting a commit "
2216 "that was already pushed. Force push to overwrite the remote?\n\n"
2217 "Anyone who already pulled the previous version will need to "
2218 "reset their copy." ),
2219 upstream.empty() ? wxString( "the remote" ) : upstream ),
2220 _( "Force Push?" ), wxYES_NO | wxNO_DEFAULT | wxICON_WARNING | wxCENTER );
2221
2222 dlg.SetYesNoLabels( _( "&Force Push" ), _( "Cancel" ) );
2223
2224 if( dlg.ShowModal() == wxID_YES )
2225 {
2226 force = true;
2227 continue;
2228 }
2229 }
2230
2231 showGitFeedback( _( "Push failed." ) );
2232 DisplayErrorMessage( m_parent, _( "Failed to push project" ), handler.GetErrorString() );
2233 break;
2234 }
2235
2236 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
2237}
2238
2239
2240void PROJECT_TREE_PANE::onGitSwitchBranch( wxCommandEvent& aEvent )
2241{
2242 if( !m_TreeProject->GetGitRepo() )
2243 return;
2244
2245 GIT_BRANCH_HANDLER branchHandler( m_TreeProject->GitCommon() );
2246 wxString branchName;
2247
2248 if( aEvent.GetId() == ID_GIT_SWITCH_BRANCH )
2249 {
2250 DIALOG_GIT_SWITCH dlg( wxGetTopLevelParent( this ), m_TreeProject->GetGitRepo() );
2251
2252 int retval = dlg.ShowModal();
2253 branchName = dlg.GetBranchName();
2254
2255 if( retval == wxID_ADD )
2256 KIGIT::PROJECT_GIT_UTILS::CreateBranch( m_TreeProject->GetGitRepo(), branchName );
2257 else if( retval != wxID_OK )
2258 return;
2259 }
2260 else
2261 {
2262 std::vector<wxString> branches = m_TreeProject->GitCommon()->GetBranchNames();
2263 int branchIndex = aEvent.GetId() - ID_GIT_SWITCH_BRANCH;
2264
2265 if( branchIndex < 0 || static_cast<size_t>( branchIndex ) >= branches.size() )
2266 return;
2267
2268 branchName = branches[branchIndex];
2269 }
2270
2271 wxLogTrace( traceGit, wxS( "onGitSwitchBranch: Switching to branch '%s'" ), branchName );
2272 if( branchHandler.SwitchToBranch( branchName ) != BranchResult::Success )
2273 {
2274 DisplayError( m_parent, branchHandler.GetErrorString() );
2275 return;
2276 }
2277
2278 m_TreeProject->GitCommon()->UpdateCurrentBranchInfo();
2279 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
2280}
2281
2282
2283void PROJECT_TREE_PANE::onGitRemoveVCS( wxCommandEvent& aEvent )
2284{
2285 PROJECT_LOCAL_SETTINGS& localSettings = Prj().GetLocalSettings();
2286
2287 // Toggle the Git integration disabled preference
2288 localSettings.m_GitIntegrationDisabled = !localSettings.m_GitIntegrationDisabled;
2289
2290 wxLogTrace( traceGit, wxS( "onGitRemoveVCS: Git integration %s" ),
2291 localSettings.m_GitIntegrationDisabled ? wxS( "disabled" ) : wxS( "enabled" ) );
2292
2293 if( localSettings.m_GitIntegrationDisabled )
2294 {
2295 // Disabling Git integration - release the repo and clear item states
2297 m_gitIconsInitialized = false;
2298
2299 // Clear all item states to remove git status icons
2300 std::stack<wxTreeItemId> items;
2301 items.push( m_TreeProject->GetRootItem() );
2302
2303 while( !items.empty() )
2304 {
2305 wxTreeItemId current = items.top();
2306 items.pop();
2307
2308 m_TreeProject->SetItemState( current, wxTREE_ITEMSTATE_NONE );
2309
2310 wxTreeItemIdValue cookie;
2311 wxTreeItemId child = m_TreeProject->GetFirstChild( current, cookie );
2312
2313 while( child.IsOk() )
2314 {
2315 items.push( child );
2316 child = m_TreeProject->GetNextChild( current, cookie );
2317 }
2318 }
2319 }
2320 else
2321 {
2322 // Re-enabling Git integration - try to find and connect to the repository
2323 wxFileName fn( Prj().GetProjectPath() );
2324 m_TreeProject->SetGitRepo( KIGIT::PROJECT_GIT_UTILS::GetRepositoryForFile( fn.GetPath().c_str() ) );
2325
2326 if( m_TreeProject->GetGitRepo() )
2327 {
2328 m_TreeProject->GitCommon()->SetCancelled( false );
2329
2330 const char* canonicalWorkDir = git_repository_workdir( m_TreeProject->GetGitRepo() );
2331
2332 if( canonicalWorkDir )
2333 {
2335 fn.GetPath(), wxString::FromUTF8( canonicalWorkDir ) );
2336 m_TreeProject->GitCommon()->SetProjectDir( symlinkWorkDir );
2337 }
2338
2339 m_TreeProject->GitCommon()->SetUsername( localSettings.m_GitRepoUsername );
2340 m_TreeProject->GitCommon()->SetSSHKey( localSettings.m_GitSSHKey );
2341 }
2342 }
2343
2344 // Save the preference to the project local settings file
2345 localSettings.SaveToFile( Prj().GetProjectPath() );
2346}
2347
2348
2350{
2351 wxLogTrace( traceGit, wxS( "updateGitStatusIcons: Updating git status icons" ) );
2352 std::unique_lock<std::mutex> lock( m_gitStatusMutex, std::try_to_lock );
2353
2354 if( !lock.owns_lock() )
2355 {
2356 wxLogTrace( traceGit, wxS( "updateGitStatusIcons: Failed to acquire lock for git status icon update" ) );
2357 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
2358 return;
2359 }
2360
2361 if( !Pgm().GetCommonSettings()->m_Git.enableGit || !m_TreeProject )
2362 {
2363 wxLogTrace( traceGit, wxS( "updateGitStatusIcons: Git is disabled or tree control is null" ) );
2364 return;
2365 }
2366
2367 std::stack<wxTreeItemId> items;
2368 items.push(m_TreeProject->GetRootItem());
2369
2370 while( !items.empty() )
2371 {
2372 wxTreeItemId current = items.top();
2373 items.pop();
2374
2375 if( m_TreeProject->ItemHasChildren( current ) )
2376 {
2377 wxTreeItemIdValue cookie;
2378 wxTreeItemId child = m_TreeProject->GetFirstChild( current, cookie );
2379
2380 while( child.IsOk() )
2381 {
2382 items.push( child );
2383
2384 if( auto it = m_gitStatusIcons.find( child ); it != m_gitStatusIcons.end() )
2385 {
2386 m_TreeProject->SetItemState( child, static_cast<int>( it->second ) );
2387 }
2388
2389 child = m_TreeProject->GetNextChild( current, cookie );
2390 }
2391 }
2392 }
2393
2394 if( !m_gitCurrentBranchName.empty() )
2395 {
2396 wxTreeItemId kid = m_TreeProject->GetRootItem();
2397 PROJECT_TREE_ITEM* rootItem = kid.IsOk() ? GetItemIdData( kid ) : nullptr;
2398
2399 if( rootItem )
2400 {
2401 wxString filename = wxFileNameFromPath( rootItem->GetFileName() );
2402 m_TreeProject->SetItemText( kid, filename + " [" + m_gitCurrentBranchName + "]" );
2403 m_gitIconsInitialized = true;
2404 }
2405 }
2406
2407 wxLogTrace( traceGit, wxS( "updateGitStatusIcons: Git status icons updated" ) );
2408}
2409
2410
2412{
2413 wxLogTrace( traceGit, wxS( "updateTreeCache: Updating tree cache" ) );
2414
2415 std::unique_lock<std::mutex> lock( m_gitTreeCacheMutex, std::try_to_lock );
2416
2417 if( !lock.owns_lock() )
2418 {
2419 wxLogTrace( traceGit, wxS( "updateTreeCache: Failed to acquire lock for tree cache update" ) );
2420 return;
2421 }
2422
2423 if( !m_TreeProject )
2424 {
2425 wxLogTrace( traceGit, wxS( "updateTreeCache: Tree control is null" ) );
2426 return;
2427 }
2428
2429 wxTreeItemId kid = m_TreeProject->GetRootItem();
2430
2431 if( !kid.IsOk() )
2432 return;
2433
2434 // Collect a map to easily set the state of each item
2435 m_gitTreeCache.clear();
2436 std::stack<wxTreeItemId> items;
2437 items.push( kid );
2438
2439 while( !items.empty() )
2440 {
2441 kid = items.top();
2442 items.pop();
2443
2444 PROJECT_TREE_ITEM* nextItem = GetItemIdData( kid );
2445
2446 if( !nextItem )
2447 continue;
2448
2449 wxString gitAbsPath = nextItem->GetFileName();
2450#ifdef _WIN32
2451 gitAbsPath.Replace( wxS( "\\" ), wxS( "/" ) );
2452#endif
2453 m_gitTreeCache[gitAbsPath] = kid;
2454
2455 wxTreeItemIdValue cookie;
2456 wxTreeItemId child = m_TreeProject->GetFirstChild( kid, cookie );
2457
2458 while( child.IsOk() )
2459 {
2460 items.push( child );
2461 child = m_TreeProject->GetNextChild( kid, cookie );
2462 }
2463 }
2464}
2465
2466
2468{
2469 wxLogTrace( traceGit, wxS( "updateGitStatusIconMap: Updating git status icons" ) );
2470#if defined( _WIN32 )
2472
2473 if( refresh != 0
2474 && KIPLATFORM::ENV::IsNetworkPath( wxPathOnly( m_Parent->GetProjectFileName() ) ) )
2475 {
2476 // Need to treat windows network paths special here until we get the samba bug fixed
2477 // https://github.com/wxWidgets/wxWidgets/issues/18953
2478 CallAfter(
2479 [this, refresh]()
2480 {
2481 m_gitStatusTimer.Start( refresh, wxTIMER_ONE_SHOT );
2482 } );
2483 }
2484
2485#endif
2486
2487 if( !Pgm().GetCommonSettings()->m_Git.enableGit || !m_TreeProject )
2488 return;
2489
2490 std::unique_lock<std::mutex> lock1( m_gitStatusMutex, std::try_to_lock );
2491 std::unique_lock<std::mutex> lock2( m_gitTreeCacheMutex, std::try_to_lock );
2492
2493 if( !lock1.owns_lock() || !lock2.owns_lock() )
2494 {
2495 wxLogTrace( traceGit, wxS( "updateGitStatusIconMap: Failed to acquire locks for git status icon update" ) );
2496 return;
2497 }
2498
2499 // Pin a shared handle so a concurrent releaseGitRepo()/TakeGitCommon() on the UI thread
2500 // cannot free the common while we dereference the repo or hold its action mutex. The
2501 // embedded action mutex can only guard the repo, not the object that owns the mutex, so
2502 // ownership has to be pinned before anything else is touched.
2503 std::shared_ptr<KIGIT_COMMON> gitCommon = m_TreeProject->GitCommonPtr();
2504
2505 if( !gitCommon || !gitCommon->GetRepo() )
2506 {
2507 wxLogTrace( traceGit, wxS( "updateGitStatusIconMap: No git repository found" ) );
2508 return;
2509 }
2510
2511 // Acquire the git action mutex to synchronize with EmptyTreePrj() shutdown.
2512 // This ensures the repository isn't freed while we're using it.
2513 std::unique_lock<std::mutex> gitLock( gitCommon->m_gitActionMutex, std::try_to_lock );
2514
2515 if( !gitLock.owns_lock() )
2516 {
2517 wxLogTrace( traceGit, wxS( "updateGitStatusIconMap: Failed to acquire git action mutex" ) );
2518 return;
2519 }
2520
2521 // Check if cancellation was requested (e.g., during shutdown)
2522 if( gitCommon->IsCancelled() )
2523 {
2524 wxLogTrace( traceGit, wxS( "updateGitStatusIconMap: Cancelled" ) );
2525 return;
2526 }
2527
2528 GIT_STATUS_HANDLER statusHandler( gitCommon.get() );
2529
2530 // Set up pathspec for project files
2531 wxFileName rootFilename( Prj().GetProjectFullName() );
2532 wxString repoWorkDir = statusHandler.GetWorkingDirectory();
2533
2534 wxFileName relative = rootFilename;
2535 relative.MakeRelativeTo( repoWorkDir );
2536 wxString pathspecStr = relative.GetPath( wxPATH_GET_VOLUME | wxPATH_GET_SEPARATOR );
2537
2538#ifdef _WIN32
2539 pathspecStr.Replace( wxS( "\\" ), wxS( "/" ) );
2540#endif
2541
2542 // Get file status
2543 auto fileStatusMap = statusHandler.GetFileStatus( pathspecStr );
2544 auto [localChanges, remoteChanges] = gitCommon->GetDifferentFiles();
2545 statusHandler.UpdateRemoteStatus( localChanges, remoteChanges, fileStatusMap );
2546
2547 bool updated = false;
2548
2549 // Update status icons based on file status
2550 for( const auto& [absPath, fileStatus] : fileStatusMap )
2551 {
2552 auto iter = m_gitTreeCache.find( absPath );
2553 if( iter == m_gitTreeCache.end() )
2554 {
2555 wxLogTrace( traceGit, wxS( "File '%s' not found in tree cache" ), absPath );
2556 continue;
2557 }
2558
2559 auto [it, inserted] = m_gitStatusIcons.try_emplace( iter->second, fileStatus.status );
2560 if( inserted || it->second != fileStatus.status )
2561 updated = true;
2562 it->second = fileStatus.status;
2563 }
2564
2565 // Get the current branch name
2566 wxString branchName = statusHandler.GetCurrentBranchName();
2567
2568 if( branchName != m_gitCurrentBranchName )
2569 updated = true;
2570
2571 m_gitCurrentBranchName = branchName;
2572 m_gitCurrentUpstream = gitCommon->GetUpstreamShorthand();
2573
2574 wxLogTrace( traceGit, wxS( "updateGitStatusIconMap: Updated git status icons" ) );
2575
2576 // Update UI if icons changed
2577 if( updated || !m_gitIconsInitialized )
2578 {
2579 CallAfter(
2580 [this]()
2581 {
2583 } );
2584 }
2585}
2586
2587
2588void PROJECT_TREE_PANE::onGitCommit( wxCommandEvent& aEvent )
2589{
2590 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
2591
2592 git_repository* repo = m_TreeProject->GetGitRepo();
2593
2594 if( repo == nullptr )
2595 {
2596 wxMessageBox( _( "The selected directory is not a Git project." ) );
2597 return;
2598 }
2599
2600 // Flush in-memory editor and project state so the index snapshots the user's
2601 // actual work instead of a stale on-disk version.
2602 {
2603 struct DirtyEditor
2604 {
2605 FRAME_T frameType;
2606 MAIL_T saveMail;
2607 wxString label;
2608 };
2609
2610 const DirtyEditor candidates[] = {
2611 { FRAME_SCH, MAIL_SCH_SAVE, _( "Schematic Editor" ) },
2612 { FRAME_PCB_EDITOR, MAIL_PCB_SAVE, _( "PCB Editor" ) },
2613 };
2614
2615 std::vector<const DirtyEditor*> dirty;
2616
2617 for( const DirtyEditor& d : candidates )
2618 {
2619 KIWAY_PLAYER* frame = m_Parent->Kiway().Player( d.frameType, false );
2620
2621 if( frame && frame->IsContentModified() )
2622 dirty.push_back( &d );
2623 }
2624
2625 if( !dirty.empty() )
2626 {
2627 wxString listed;
2628
2629 for( const DirtyEditor* d : dirty )
2630 listed += wxString::Format( wxS( "\n • %s" ), d->label );
2631
2632 wxRichMessageDialog dlg( wxGetTopLevelParent( this ),
2633 wxString::Format( _( "The following editors have unsaved changes:%s\n\n"
2634 "Save them before committing?" ),
2635 listed ),
2636 _( "Unsaved Changes" ),
2637 wxYES_NO | wxCANCEL | wxYES_DEFAULT | wxICON_WARNING | wxCENTER );
2638
2639 dlg.SetYesNoCancelLabels( _( "&Save and Commit" ), _( "Commit &Anyway" ), _( "Cancel" ) );
2640
2641 int answer = dlg.ShowModal();
2642
2643 if( answer == wxID_CANCEL )
2644 return;
2645
2646 if( answer == wxID_YES )
2647 {
2648 for( const DirtyEditor* d : dirty )
2649 {
2650 std::string payload;
2651 m_Parent->Kiway().ExpressMail( d->frameType, d->saveMail, payload );
2652
2653 if( payload != "success" )
2654 {
2655 DisplayErrorMessage( wxGetTopLevelParent( this ),
2656 wxString::Format( _( "Could not save %s." ), d->label ) );
2657 return;
2658 }
2659 }
2660 }
2661 }
2662
2663 m_Parent->GetSettingsManager()->SaveProject();
2664 }
2665
2666 // Get git configuration
2667 GIT_CONFIG_HANDLER configHandler( m_TreeProject->GitCommon() );
2668 GitUserConfig userConfig = configHandler.GetUserConfig();
2669
2670 // Collect modified files in the repository
2671 GIT_STATUS_HANDLER statusHandler( m_TreeProject->GitCommon() );
2672 auto fileStatusMap = statusHandler.GetFileStatus();
2673
2674 std::map<wxString, int> modifiedFiles;
2675 std::set<wxString> selected_files;
2676
2677 for( PROJECT_TREE_ITEM* item : tree_data )
2678 {
2679 if( item->GetType() == TREE_FILE_TYPE::DIRECTORY )
2680 continue;
2681
2682 wxString itemPath = item->GetFileName();
2683#ifdef _WIN32
2684 itemPath.Replace( wxS( "\\" ), wxS( "/" ) );
2685#endif
2686 selected_files.emplace( itemPath );
2687 }
2688
2689 wxString repoWorkDir = statusHandler.GetWorkingDirectory();
2690
2691 wxString projectPath = Prj().GetProjectPath();
2692#ifdef _WIN32
2693 projectPath.Replace( wxS( "\\" ), wxS( "/" ) );
2694#endif
2695
2696 for( const auto& [absPath, fileStatus] : fileStatusMap )
2697 {
2698 // Skip current, conflicted, or ignored files
2699 if( fileStatus.status == KIGIT_COMMON::GIT_STATUS::GIT_STATUS_CURRENT
2701 || fileStatus.status == KIGIT_COMMON::GIT_STATUS::GIT_STATUS_IGNORED )
2702 {
2703 continue;
2704 }
2705
2706 wxFileName fn( absPath );
2707
2708 // Convert to relative path for the modifiedFiles map
2709 wxString relativePath = absPath;
2710 if( relativePath.StartsWith( repoWorkDir ) )
2711 {
2712 relativePath = relativePath.Mid( repoWorkDir.length() );
2713#ifdef _WIN32
2714 relativePath.Replace( wxS( "\\" ), wxS( "/" ) );
2715#endif
2716 }
2717
2718 // Do not commit files outside the project directory
2719 if( !KIGIT::PROJECT_GIT_UTILS::IsWithinProjectPath( absPath, projectPath ) )
2720 continue;
2721
2722 // Skip lock files
2723 if( fn.GetExt().CmpNoCase( FILEEXT::LockFileExtension ) == 0 )
2724 continue;
2725
2726 // Skip autosave, lock, and backup files
2727 if( fn.GetName().StartsWith( FILEEXT::LockFilePrefix )
2728 || fn.GetName().EndsWith( FILEEXT::BackupFileSuffix ) )
2729 {
2730 continue;
2731 }
2732
2733 // Skip archived project backups
2734 if( fn.GetPath().Contains( Prj().GetProjectName() + wxT( "-backups" ) ) )
2735 continue;
2736
2737 // Skip local project history (which is also a submodule, we don't support for the time)
2738 if( fn.IsDir() && fn.GetDirs().Last().StartsWith( wxS( ".history" ) ) )
2739 {
2740 continue;
2741 }
2742
2743 if( aEvent.GetId() == ID_GIT_COMMIT_PROJECT )
2744 {
2745 // Don't add an untracked project-local settings file to the repo on its own
2746 // Only commit it if it's already tracked, or if the user selects it explicitly.
2747 if( fn.GetExt().CmpNoCase( FILEEXT::ProjectLocalSettingsFileExtension ) == 0
2748 && fileStatus.status == KIGIT_COMMON::GIT_STATUS::GIT_STATUS_UNTRACKED )
2749 {
2750 continue;
2751 }
2752
2753 modifiedFiles.emplace( relativePath, fileStatus.gitStatus );
2754 }
2755 else if( selected_files.count( absPath ) )
2756 {
2757 modifiedFiles.emplace( relativePath, fileStatus.gitStatus );
2758 }
2759 }
2760
2761 // Create a commit dialog
2762 DIALOG_GIT_COMMIT dlg( wxGetTopLevelParent( this ), repo, userConfig.authorName, userConfig.authorEmail,
2763 modifiedFiles );
2764 auto ret = dlg.ShowModal();
2765
2766 if( ret != wxID_OK )
2767 return;
2768
2769 std::vector<wxString> files = dlg.GetSelectedFiles();
2770
2771 if( dlg.GetCommitMessage().IsEmpty() )
2772 {
2773 wxMessageBox( _( "Discarding commit due to empty commit message." ) );
2774 return;
2775 }
2776
2777 if( files.empty() )
2778 {
2779 wxMessageBox( _( "Discarding commit due to empty file selection." ) );
2780 return;
2781 }
2782
2783 GIT_COMMIT_HANDLER commitHandler( repo );
2784 auto result = commitHandler.PerformCommit( files, dlg.GetCommitMessage(),
2785 dlg.GetAuthorName(), dlg.GetAuthorEmail() );
2786
2788 {
2789 wxMessageBox( wxString::Format( _( "Failed to create commit: %s" ),
2790 commitHandler.GetErrorString() ) );
2791 return;
2792 }
2793
2794 wxLogTrace( traceGit, wxS( "Created commit" ) );
2795 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
2796}
2797
2798
2799void PROJECT_TREE_PANE::onGitAmendCommit( wxCommandEvent& aEvent )
2800{
2801 git_repository* repo = m_TreeProject->GetGitRepo();
2802
2803 if( repo == nullptr )
2804 {
2805 wxMessageBox( _( "The selected directory is not a Git project." ) );
2806 return;
2807 }
2808
2809 wxString reopenPath = Prj().GetProjectPath();
2810
2811 if( git_repository* fresh = KIGIT::PROJECT_GIT_UTILS::GetRepositoryForFile( reopenPath.c_str() ) )
2812 {
2813 // Swapping the handle is only safe when no background fetch or status update holds
2814 // the old one, so keep the existing handle if the mutex is busy.
2815 std::unique_lock<std::mutex> gitLock( m_TreeProject->GitCommon()->m_gitActionMutex, std::try_to_lock );
2816
2817 if( gitLock.owns_lock() )
2818 {
2819 m_TreeProject->SetGitRepo( fresh );
2820 git_repository_free( repo );
2821 repo = fresh;
2822 }
2823 else
2824 {
2825 git_repository_free( fresh );
2826 }
2827 }
2828
2829 if( git_repository_head_unborn( repo ) != 0 )
2830 {
2831 DisplayErrorMessage( wxGetTopLevelParent( this ), _( "Cannot amend: the branch has no commits yet." ) );
2832 return;
2833 }
2834
2835 // Look up HEAD commit so we can pre-fill the message and check whether HEAD has
2836 // already been published to the upstream branch.
2837 git_reference* headRef = nullptr;
2838
2839 if( git_repository_head( &headRef, repo ) != GIT_OK )
2840 return;
2841
2842 KIGIT::GitReferencePtr headRefPtr( headRef );
2843 git_commit* headCommit = nullptr;
2844
2845 if( git_reference_peel( (git_object**) &headCommit, headRef, GIT_OBJECT_COMMIT ) != GIT_OK )
2846 return;
2847
2848 KIGIT::GitCommitPtr headCommitPtr( headCommit );
2849 wxString headMessage = wxString::FromUTF8( git_commit_message( headCommit ) );
2850 const git_oid* headOid = git_commit_id( headCommit );
2851
2852 // Detect whether HEAD has already been pushed: head is published when it is the
2853 // upstream OID or an ancestor of it.
2854 bool headIsPublished = false;
2855 git_reference* upstreamRef = nullptr;
2856
2857 if( git_branch_upstream( &upstreamRef, headRef ) == GIT_OK )
2858 {
2859 KIGIT::GitReferencePtr upstreamRefPtr( upstreamRef );
2860 git_oid upstreamOid;
2861
2862 if( git_reference_name_to_id( &upstreamOid, repo, git_reference_name( upstreamRef ) ) == GIT_OK )
2863 {
2864 headIsPublished = git_oid_equal( headOid, &upstreamOid )
2865 || git_graph_descendant_of( repo, &upstreamOid, headOid ) == 1;
2866 }
2867 }
2868
2869 if( headIsPublished )
2870 {
2871 wxString upstreamName = m_TreeProject->GitCommon()->GetUpstreamShorthand();
2872
2873 wxRichMessageDialog dlg( wxGetTopLevelParent( this ),
2874 wxString::Format( _( "The last commit has already been pushed to %s.\n\n"
2875 "Amending rewrites public history. Anyone who pulled "
2876 "this commit will need to reset their copy." ),
2877 upstreamName ),
2878 _( "Amend Published Commit?" ), wxYES_NO | wxNO_DEFAULT | wxICON_WARNING | wxCENTER );
2879
2880 dlg.SetYesNoLabels( _( "Amend &Anyway" ), _( "Cancel" ) );
2881
2882 if( dlg.ShowModal() != wxID_YES )
2883 return;
2884 }
2885
2886 // Flush in-memory editor and project state so the amend captures the user's actual
2887 // work instead of a stale on-disk version.
2888 {
2889 struct DirtyEditor
2890 {
2891 FRAME_T frameType;
2892 MAIL_T saveMail;
2893 wxString label;
2894 };
2895
2896 const DirtyEditor candidates[] = {
2897 { FRAME_SCH, MAIL_SCH_SAVE, _( "Schematic Editor" ) },
2898 { FRAME_PCB_EDITOR, MAIL_PCB_SAVE, _( "PCB Editor" ) },
2899 };
2900
2901 std::vector<const DirtyEditor*> dirty;
2902
2903 for( const DirtyEditor& d : candidates )
2904 {
2905 KIWAY_PLAYER* frame = m_Parent->Kiway().Player( d.frameType, false );
2906
2907 if( frame && frame->IsContentModified() )
2908 dirty.push_back( &d );
2909 }
2910
2911 if( !dirty.empty() )
2912 {
2913 wxString listed;
2914
2915 for( const DirtyEditor* d : dirty )
2916 listed += wxString::Format( wxS( "\n • %s" ), d->label );
2917
2918 wxRichMessageDialog dlg( wxGetTopLevelParent( this ),
2919 wxString::Format( _( "The following editors have unsaved changes:%s\n\n"
2920 "Save them before amending?" ),
2921 listed ),
2922 _( "Unsaved Changes" ),
2923 wxYES_NO | wxCANCEL | wxYES_DEFAULT | wxICON_WARNING | wxCENTER );
2924
2925 dlg.SetYesNoCancelLabels( _( "&Save and Amend" ), _( "Amend &Anyway" ), _( "Cancel" ) );
2926
2927 int answer = dlg.ShowModal();
2928
2929 if( answer == wxID_CANCEL )
2930 return;
2931
2932 if( answer == wxID_YES )
2933 {
2934 for( const DirtyEditor* d : dirty )
2935 {
2936 std::string payload;
2937 m_Parent->Kiway().ExpressMail( d->frameType, d->saveMail, payload );
2938
2939 if( payload != "success" )
2940 {
2941 DisplayErrorMessage( wxGetTopLevelParent( this ),
2942 wxString::Format( _( "Could not save %s." ), d->label ) );
2943 return;
2944 }
2945 }
2946 }
2947 }
2948
2949 m_Parent->GetSettingsManager()->SaveProject();
2950 }
2951
2952 // Collect modified files for the dialog's checklist.
2953 GIT_CONFIG_HANDLER configHandler( m_TreeProject->GitCommon() );
2954 GitUserConfig userConfig = configHandler.GetUserConfig();
2955
2956 GIT_STATUS_HANDLER statusHandler( m_TreeProject->GitCommon() );
2957 auto fileStatusMap = statusHandler.GetFileStatus();
2958 wxString repoWorkDir = statusHandler.GetWorkingDirectory();
2959 wxString projectPath = Prj().GetProjectPath();
2960
2961#ifdef _WIN32
2962 projectPath.Replace( wxS( "\\" ), wxS( "/" ) );
2963#endif
2964
2965 std::map<wxString, int> modifiedFiles;
2966
2967 for( const auto& [absPath, fileStatus] : fileStatusMap )
2968 {
2969 if( fileStatus.status == KIGIT_COMMON::GIT_STATUS::GIT_STATUS_CURRENT
2971 || fileStatus.status == KIGIT_COMMON::GIT_STATUS::GIT_STATUS_IGNORED )
2972 {
2973 continue;
2974 }
2975
2976 wxFileName fn( absPath );
2977 wxString relativePath = absPath;
2978
2979 if( relativePath.StartsWith( repoWorkDir ) )
2980 {
2981 relativePath = relativePath.Mid( repoWorkDir.length() );
2982#ifdef _WIN32
2983 relativePath.Replace( wxS( "\\" ), wxS( "/" ) );
2984#endif
2985 }
2986
2987 if( !KIGIT::PROJECT_GIT_UTILS::IsWithinProjectPath( absPath, projectPath ) )
2988 continue;
2989
2990 if( fn.GetExt().CmpNoCase( FILEEXT::LockFileExtension ) == 0 )
2991 continue;
2992
2993 if( fn.GetName().StartsWith( FILEEXT::LockFilePrefix ) || fn.GetName().EndsWith( FILEEXT::BackupFileSuffix ) )
2994 {
2995 continue;
2996 }
2997
2998 if( fn.GetPath().Contains( Prj().GetProjectName() + wxT( "-backups" ) ) )
2999 continue;
3000
3001 modifiedFiles.emplace( relativePath, fileStatus.gitStatus );
3002 }
3003
3004 if( git_reference* headRefForDiff = nullptr; git_repository_head( &headRefForDiff, repo ) == GIT_OK )
3005 {
3006 KIGIT::GitReferencePtr headRefForDiffPtr( headRefForDiff );
3007 git_commit* lastCommit = nullptr;
3008
3009 if( git_reference_peel( (git_object**) &lastCommit, headRefForDiff, GIT_OBJECT_COMMIT ) == GIT_OK )
3010 {
3011 KIGIT::GitCommitPtr lastCommitPtr( lastCommit );
3012 git_commit* parentCommit = nullptr;
3013 git_tree* parentTree = nullptr;
3014 git_tree* lastTree = nullptr;
3015
3016 if( git_commit_parentcount( lastCommit ) > 0
3017 && git_commit_parent( &parentCommit, lastCommit, 0 ) == GIT_OK )
3018 {
3019 git_commit_tree( &parentTree, parentCommit );
3020 }
3021
3022 KIGIT::GitCommitPtr parentCommitPtr( parentCommit );
3023 KIGIT::GitTreePtr parentTreePtr( parentTree );
3024
3025 if( git_commit_tree( &lastTree, lastCommit ) == GIT_OK )
3026 {
3027 KIGIT::GitTreePtr lastTreePtr( lastTree );
3028 git_diff* diff = nullptr;
3029
3030 if( git_diff_tree_to_tree( &diff, repo, parentTree, lastTree, nullptr ) == GIT_OK )
3031 {
3032 KIGIT::GitDiffPtr diffPtr( diff );
3033
3035 [&modifiedFiles, &repoWorkDir, &projectPath]( const git_diff_delta& aDelta )
3036 {
3037 const char* path = aDelta.new_file.path ? aDelta.new_file.path
3038 : aDelta.old_file.path;
3039
3040 if( !path )
3041 return;
3042
3043 // Skip files from the previous commit that live outside the
3044 // current project directory (issue #15910).
3045 wxString absPath = repoWorkDir + wxString::FromUTF8( path );
3046
3047 if( !KIGIT::PROJECT_GIT_UTILS::IsWithinProjectPath( absPath, projectPath ) )
3048 return;
3049
3050 int flag = GIT_STATUS_INDEX_MODIFIED;
3051
3052 if( aDelta.status == GIT_DELTA_ADDED )
3053 flag = GIT_STATUS_INDEX_NEW;
3054 else if( aDelta.status == GIT_DELTA_DELETED )
3055 flag = GIT_STATUS_INDEX_DELETED;
3056
3057 modifiedFiles[wxString::FromUTF8( path )] |= flag;
3058 } );
3059 }
3060 }
3061 }
3062 }
3063
3064 DIALOG_GIT_COMMIT dlg( wxGetTopLevelParent( this ), repo, userConfig.authorName, userConfig.authorEmail,
3065 modifiedFiles );
3066 dlg.SetTitle( _( "Amend Last Commit" ) );
3067 dlg.SetCommitMessage( headMessage );
3068 dlg.SetFileSelectionRequired( false );
3069
3070 if( dlg.ShowModal() != wxID_OK )
3071 return;
3072
3073 if( dlg.GetCommitMessage().IsEmpty() )
3074 {
3075 wxMessageBox( _( "Discarding amend due to empty commit message." ) );
3076 return;
3077 }
3078
3079 GIT_COMMIT_HANDLER commitHandler( repo );
3081 dlg.GetAuthorName(), dlg.GetAuthorEmail() );
3082
3084 {
3085 wxMessageBox( wxString::Format( _( "Failed to amend commit: %s" ), commitHandler.GetErrorString() ) );
3086 return;
3087 }
3088
3089 wxLogTrace( traceGit, wxS( "Amended commit" ) );
3090 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
3091}
3092
3093
3094void PROJECT_TREE_PANE::onGitAddToIndex( wxCommandEvent& aEvent )
3095{
3096
3097}
3098
3099
3100bool PROJECT_TREE_PANE::canFileBeAddedToVCS( const wxString& aFile )
3101{
3102 if( !m_TreeProject->GetGitRepo() )
3103 return false;
3104
3105 GIT_STATUS_HANDLER statusHandler( m_TreeProject->GitCommon() );
3106 auto fileStatusMap = statusHandler.GetFileStatus();
3107
3108 // Check if file is already tracked or staged
3109 for( const auto& [filePath, fileStatus] : fileStatusMap )
3110 {
3111 if( filePath.EndsWith( aFile ) || filePath == aFile )
3112 {
3113 // File can be added if it's untracked
3114 return fileStatus.status == KIGIT_COMMON::GIT_STATUS::GIT_STATUS_UNTRACKED;
3115 }
3116 }
3117
3118 // If file not found in status, it might be addable
3119 return true;
3120}
3121
3122
3123void PROJECT_TREE_PANE::onGitSyncProject( wxCommandEvent& aEvent )
3124{
3125 wxLogTrace( traceGit, "Syncing project" );
3126 git_repository* repo = m_TreeProject->GetGitRepo();
3127
3128 if( !repo )
3129 {
3130 wxLogTrace( traceGit, "sync: No git repository found" );
3131 return;
3132 }
3133
3134 GIT_SYNC_HANDLER handler( repo );
3135 handler.PerformSync();
3136}
3137
3138
3139void PROJECT_TREE_PANE::onGitFetch( wxCommandEvent& aEvent )
3140{
3141 KIGIT_COMMON* gitCommon = m_TreeProject->GitCommon();
3142
3143 if( !gitCommon )
3144 return;
3145
3146 while( true )
3147 {
3148 gitCommon->ClearAuthFailure();
3149
3150 GIT_PULL_HANDLER handler( gitCommon );
3151
3152 if( handler.PerformFetch() )
3153 {
3154 wxString remoteName = gitCommon->GetRemoteNameOrDefault();
3155
3156 showGitFeedback( remoteName.empty() ? _( "Fetched from remote." )
3157 : wxString::Format( _( "Fetched from %s." ), remoteName ) );
3158 break;
3159 }
3160
3161 if( gitCommon->WasAuthFailure() && promptForGitCredentials( m_parent, gitCommon ) )
3162 continue;
3163
3164 showGitFeedback( _( "Fetch failed." ) );
3165 break;
3166 }
3167
3168 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
3169}
3170
3171
3172void PROJECT_TREE_PANE::onGitResolveConflict( wxCommandEvent& aEvent )
3173{
3174 git_repository* repo = m_TreeProject->GetGitRepo();
3175
3176 if( !repo )
3177 return;
3178
3179 GIT_RESOLVE_CONFLICT_HANDLER handler( repo );
3180 handler.PerformResolveConflict();
3181}
3182
3183
3184void PROJECT_TREE_PANE::onGitRevertLocal( wxCommandEvent& aEvent )
3185{
3186 git_repository* repo = m_TreeProject->GetGitRepo();
3187
3188 if( !repo )
3189 return;
3190
3191 GIT_REVERT_HANDLER handler( repo );
3192 handler.PerformRevert();
3193}
3194
3195
3196void PROJECT_TREE_PANE::onGitRemoveFromIndex( wxCommandEvent& aEvent )
3197{
3198 git_repository* repo = m_TreeProject->GetGitRepo();
3199
3200 if( !repo )
3201 return;
3202
3203 GIT_REMOVE_FROM_INDEX_HANDLER handler( repo );
3204 handler.PerformRemoveFromIndex();
3205}
3206
3207
3209{
3210
3211}
3212
3213
3214void PROJECT_TREE_PANE::onGitSyncTimer( wxTimerEvent& aEvent )
3215{
3216 wxLogTrace( traceGit, "onGitSyncTimer" );
3217 COMMON_SETTINGS::GIT& gitSettings = Pgm().GetCommonSettings()->m_Git;
3218
3219 if( !gitSettings.enableGit || !m_TreeProject )
3220 return;
3221
3223
3224 m_gitSyncTask = tp.submit_task( [this]()
3225 {
3226 // Pin a shared handle so a concurrent releaseGitRepo() cannot free the common while
3227 // PerformFetch() is inside it holding the action mutex.
3228 std::shared_ptr<KIGIT_COMMON> gitCommon = m_TreeProject->GitCommonPtr();
3229
3230 if( !gitCommon || !gitCommon->GetRepo() )
3231 {
3232 wxLogTrace( traceGit, "onGitSyncTimer: No git repository found" );
3233 return;
3234 }
3235
3236 // Check if cancellation was requested (e.g., during shutdown)
3237 if( gitCommon->IsCancelled() )
3238 {
3239 wxLogTrace( traceGit, "onGitSyncTimer: Cancelled" );
3240 return;
3241 }
3242
3243 GIT_PULL_HANDLER handler( gitCommon.get() );
3244 handler.PerformFetch();
3245
3246 // Only schedule the follow-up work if not cancelled
3247 if( !gitCommon->IsCancelled() )
3248 CallAfter( [this]() { gitStatusTimerHandler(); } );
3249 } );
3250
3251 if( gitSettings.updatInterval > 0 )
3252 {
3253 wxLogTrace( traceGit, "onGitSyncTimer: Restarting git sync timer" );
3254 // We store the timer interval in minutes but wxTimer uses milliseconds
3255 m_gitSyncTimer.Start( gitSettings.updatInterval * 60 * 1000, wxTIMER_ONE_SHOT );
3256 }
3257}
3258
3259
3261{
3262 // Check if git is still available and not cancelled before spawning background work
3263 KIGIT_COMMON* gitCommon = m_TreeProject ? m_TreeProject->GitCommon() : nullptr;
3264
3265 if( !gitCommon || gitCommon->IsCancelled() )
3266 return;
3267
3270
3271 m_gitStatusIconTask = tp.submit_task( [this]() { updateGitStatusIconMap(); } );
3272}
3273
3274void PROJECT_TREE_PANE::onGitStatusTimer( wxTimerEvent& aEvent )
3275{
3276 wxLogTrace( traceGit, "onGitStatusTimer" );
3277
3278 if( !Pgm().GetCommonSettings()->m_Git.enableGit || !m_TreeProject )
3279 return;
3280
3282}
3283
3284
3285void PROJECT_TREE_PANE::showGitFeedback( const wxString& aText )
3286{
3287 if( KISTATUSBAR* sb = dynamic_cast<KISTATUSBAR*>( m_Parent->GetStatusBar() ) )
3288 {
3289 sb->SetEllipsedTextField( aText, 0 );
3290 m_gitFeedbackTimer.Start( 8000, wxTIMER_ONE_SHOT );
3291 }
3292}
3293
3294
3295void PROJECT_TREE_PANE::onGitFeedbackTimer( wxTimerEvent& aEvent )
3296{
3297 if( m_Parent )
3298 m_Parent->PrintPrjInfo();
3299}
const char * name
wxBitmap KiBitmap(BITMAPS aBitmap, int aHeightTag)
Construct a wxBitmap from an image identifier Returns the image from the active theme if the image ha...
Definition bitmap.cpp:100
BITMAP_STORE * GetBitmapStore()
Definition bitmap.cpp:88
@ directory_browser
static const ADVANCED_CFG & GetCfg()
Get the singleton instance's config, which is shared by all consumers.
void ThemeChanged()
Notifies the store that the icon theme has been changed by the user, so caches must be invalidated.
void SetFileSelectionRequired(bool aRequired)
When false, OK is allowed with no files selected (e.g. amending only the message).
wxString GetCommitMessage() const
void SetCommitMessage(const wxString &aMessage)
Pre-fill the commit message.
wxString GetAuthorEmail() const
std::vector< wxString > GetSelectedFiles() const
wxString GetAuthorName() const
KIGIT_COMMON::GIT_CONN_TYPE GetConnType() const
KIGIT_COMMON::GIT_CONN_TYPE GetRepoType() const
wxString GetRepoSSHPath() const
void SetSkipButtonLabel(const wxString &aLabel)
wxString GetBranchName() const
int ShowModal() override
virtual bool IsContentModified() const
Get if the contents of the frame have been modified since the last save.
virtual bool IsLibraryAvailable()=0
KIGIT_ORPHAN_REGISTRY & OrphanRegistry()
Return the process-wide orphan thread registry owned by this backend.
Definition git_backend.h:73
BranchResult SwitchToBranch(const wxString &aBranchName)
Switch to the specified branch.
CommitResult PerformCommit(const std::vector< wxString > &aFiles, const wxString &aMessage, const wxString &aAuthorName, const wxString &aAuthorEmail)
wxString GetErrorString() const
CommitResult PerformAmend(const std::vector< wxString > &aFiles, const wxString &aMessage, const wxString &aAuthorName, const wxString &aAuthorEmail)
GitUserConfig GetUserConfig()
Get user configuration (name and email) from git config Falls back to common settings if not found in...
bool IsRepository(const wxString &aPath)
Check if a directory is already a git repository.
InitResult InitializeRepository(const wxString &aPath)
Initialize a new git repository in the specified directory.
bool SetupRemote(const RemoteConfig &aConfig)
Set up a remote for the repository.
void SetProgressReporter(std::unique_ptr< WX_PROGRESS_REPORTER > aProgressReporter)
bool PerformFetch(bool aSkipLock=false)
PullResult RebaseOntoUpstream()
PullResult PerformPull()
PushResult PerformPush(bool aForce=false)
wxString GetCurrentBranchName()
Get the current branch name.
void UpdateRemoteStatus(const std::set< wxString > &aLocalChanges, const std::set< wxString > &aRemoteChanges, std::map< wxString, FileStatus > &aFileStatus)
Get status for modified files based on local/remote changes.
wxString GetWorkingDirectory()
Get the repository working directory path.
bool HasChangedFiles()
Check if the repository has any changed files.
std::map< wxString, FileStatus > GetFileStatus(const wxString &aPathspec=wxEmptyString)
Get detailed file status for all files in the specified path.
The main KiCad project manager frame.
void OnChangeWatchedPaths(wxCommandEvent &aEvent)
Called by sending a event with id = ID_INIT_WATCHED_PATHS rebuild the list of watched paths.
PROJECT_TREE_PANE * m_projectTreePane
static git_repository * GetRepositoryForFile(const char *aFilename)
Discover and open the repository that contains the given file.
static wxString ComputeSymlinkPreservingWorkDir(const wxString &aUserProjectPath, const wxString &aCanonicalWorkDir)
Compute a working directory path that preserves symlinks from the user's project path.
static bool IsWithinProjectPath(const wxString &aAbsPath, const wxString &aProjectPath)
Test whether an absolute file path lives inside the current project directory.
static bool RemoveVCS(git_repository *&aRepo, const wxString &aProjectPath=wxEmptyString, bool aRemoveGitDir=false, wxString *aErrors=nullptr)
Remove version control from a directory by freeing the repository and optionally removing the ....
static int CreateBranch(git_repository *aRepo, const wxString &aBranchName)
Create a new branch based on HEAD.
std::mutex m_gitActionMutex
const wxString & GetRemote() const
GIT_CONN_TYPE GetConnType() const
bool WasAuthFailure() const
wxString GetCurrentBranchName() const
void SetSSHKey(const wxString &aSSHKey)
bool IsCancelled() const
void SetUsername(const wxString &aUsername)
wxString GetRemoteNameOrDefault() const
Returns GetRemotename() when non-empty, otherwise "origin".
git_repository * GetRepo() const
bool HasPushAndPullRemote() const
wxString GetUsername() const
void UpdateCurrentBranchInfo()
bool HasLocalCommits() const
wxString GetUpstreamShorthand() const
Returns the upstream shorthand for the current branch (e.g.
void SetCancelled(bool aCancel)
unsigned & TestedTypes()
void SetPassword(const wxString &aPassword)
wxString GetErrorString()
bool Register(const std::string &aLabel, F &&aWork)
Spawn a tracked orphan thread running aWork.
git_repository * GetRepo() const
Get a pointer to the git repository.
void SetEllipsedTextField(const wxString &aText, int aFieldId)
Set the text in a field using wxELLIPSIZE_MIDDLE option to adjust the text size to the field size.
A wxFrame capable of the OpenProjectFiles function, meaning it can load a portion of a KiCad project.
static wxString GetDefaultUserProjectsPath()
Gets the default path we point users to create projects.
Definition paths.cpp:137
virtual COMMON_SETTINGS * GetCommonSettings() const
Definition pgm_base.cpp:562
virtual const wxString & GetTextEditor(bool aCanShowFileChooser=true)
Return the path to the preferred text editor application.
Definition pgm_base.cpp:219
The project local settings are things that are attached to a particular project, but also might be pa...
bool m_GitIntegrationDisabled
If true, KiCad will not use Git integration for this project even if a .git directory exists.
bool SaveToFile(const wxString &aDirectory="", bool aForce=false) override
Calls Store() and then writes the contents of the JSON document to a file.
Handle one item (a file or a directory name) for the tree file.
void SetRootFile(bool aValue)
void SetState(int state)
const wxString & GetFileName() const
bool IsPopulated() const
void SetPopulated(bool aValue)
TREE_FILE_TYPE GetType() const
const wxString GetDir() const
void Activate(PROJECT_TREE_PANE *aTreePrjFrame)
PROJECT_TREE_PANE Window to display the tree files.
PROJECT_TREE_ITEM * m_selectedItem
std::unordered_map< wxString, wxTreeItemId > m_gitTreeCache
void onGitFetch(wxCommandEvent &event)
Fetch the latest changes from the git repository.
std::map< wxTreeItemId, KIGIT_COMMON::GIT_STATUS > m_gitStatusIcons
void onGitInitializeProject(wxCommandEvent &event)
Initialize a new git repository in the current project directory.
void showGitFeedback(const wxString &aText)
Show a short message in the project status bar after a git operation.
void onGitSyncProject(wxCommandEvent &event)
Sync the current project with the git repository.
void onDeleteFile(wxCommandEvent &event)
Function onDeleteFile Delete the selected file or directory in the tree project.
void onGitRemoveVCS(wxCommandEvent &event)
Remove the git repository from the current project directory.
void EmptyTreePrj()
Delete all m_TreeProject entries.
void FileWatcherReset()
Reinit the watched paths Should be called after opening a new project to rebuild the list of watched ...
std::vector< PROJECT_TREE_ITEM * > GetSelectedData()
Function GetSelectedData return the item data from item currently selected (highlighted) Note this is...
void onOpenDirectory(wxCommandEvent &event)
Function onOpenDirectory Handles the right-click menu for opening a directory in the current system f...
void onGitResolveConflict(wxCommandEvent &event)
Resolve conflicts in the git repository.
void onFileSystemEvent(wxFileSystemWatcherEvent &event)
called when a file or directory is modified/created/deleted The tree project is modified when a file ...
wxFileSystemWatcher * m_watcher
void onRight(wxTreeEvent &Event)
Called on a right click on an item.
void onGitCommit(wxCommandEvent &event)
Commit the current project saved changes to the git repository.
void onSelect(wxTreeEvent &Event)
Called on a double click on an item.
PROJECT_TREE * m_TreeProject
PROJECT_TREE_PANE(KICAD_MANAGER_FRAME *parent)
KICAD_MANAGER_FRAME * m_Parent
std::future< void > m_gitSyncTask
void onExpand(wxTreeEvent &Event)
Called on a click on the + or - button of an item with children.
void onGitSyncTimer(wxTimerEvent &event)
void onIdle(wxIdleEvent &aEvent)
Idle event handler, used process the selected items at a point in time when all other events have bee...
void updateTreeCache()
Updates the map of the wxtreeitemid to the name of each file for use in the thread.
void onGitRevertLocal(wxCommandEvent &event)
Revert the local repository to the last commit.
void onGitPullProject(wxCommandEvent &event)
Pull the latest changes from the git repository.
void onGitAmendCommit(wxCommandEvent &event)
Amend (rewrite) the last commit on the current branch.
void onGitFeedbackTimer(wxTimerEvent &event)
static wxString GetFileExt(TREE_FILE_TYPE type)
friend class PROJECT_TREE_ITEM
void onGitRemoveFromIndex(wxCommandEvent &event)
Remove a file from the git index.
wxTreeItemId findSubdirTreeItem(const wxString &aSubDir)
Function findSubdirTreeItem searches for the item in tree project which is the node of the subdirecto...
void ReCreateTreePrj()
Create or modify the tree showing project file names.
void onThemeChanged(wxSysColourChangedEvent &aEvent)
void onRunSelectedJobsFile(wxCommandEvent &event)
Run a selected jobs file.
void onGitCompare(wxCommandEvent &event)
Compare the current project to a different branch in the git repository.
void onGitStatusTimer(wxTimerEvent &event)
void shutdownFileWatcher()
Shutdown the file watcher.
std::mutex m_gitTreeCacheMutex
wxTreeItemId addItemToProjectTree(const wxString &aName, const wxTreeItemId &aParent, std::vector< wxString > *aProjectNames, bool aRecurse)
Function addItemToProjectTree.
bool hasChangedFiles()
Returns true if the current project has any uncommitted changes.
void onGitRemoteSettings(wxCommandEvent &event)
Configure (or change) the default remote on an already-initialized repository.
void onOpenSelectedFileWithTextEditor(wxCommandEvent &event)
Function onOpenSelectedFileWithTextEditor Call the text editor to open the selected file in the tree ...
void onGitAddToIndex(wxCommandEvent &event)
Add a file to the git index.
void updateGitStatusIcons()
Updates the icons shown in the tree project to reflect the current git status.
std::future< void > m_gitStatusIconTask
void updateGitStatusIconMap()
This is a threaded call that will change the map of git status icons for use in the main thread.
PROJECT_TREE_ITEM * GetItemIdData(wxTreeItemId aId)
Function GetItemIdData return the item data corresponding to a wxTreeItemId identifier.
void onGitSwitchBranch(wxCommandEvent &event)
Switch to a different branch in the git repository.
void onCreateNewDirectory(wxCommandEvent &event)
Function onCreateNewDirectory Creates a new subdirectory inside the current kicad project directory t...
void onSwitchToSelectedProject(wxCommandEvent &event)
Switch to a other project selected from the tree project (by selecting an other .pro file inside the ...
void onPaint(wxPaintEvent &aEvent)
We don't have uniform borders so we have to draw them ourselves.
void onRenameFile(wxCommandEvent &event)
Function onRenameFile Rename the selected file or directory in the tree project.
void releaseGitRepo()
Releases the git repository handle, deferring the free to the orphan registry while a background git ...
void onGitPushProject(wxCommandEvent &event)
Push the current project changes to the git repository.
bool canFileBeAddedToVCS(const wxString &aFilePath)
Returns true if the file has already been added to the repository or false if it has not been added y...
std::vector< wxString > m_filters
PROJECT_TREE This is the class to show (as a tree) the files in the project directory.
virtual const wxString GetProjectPath() const
Return the full path of the project.
Definition project.cpp:183
virtual PROJECT_LOCAL_SETTINGS & GetLocalSettings() const
Definition project.h:207
void DisplayInfoMessage(wxWindow *aParent, const wxString &aMessage, const wxString &aExtraInfo)
Display an informational message box with aMessage.
Definition confirm.cpp:245
void DisplayErrorMessage(wxWindow *aParent, const wxString &aText, const wxString &aExtraInfo)
Display an error message with aMessage.
Definition confirm.cpp:217
void DisplayError(wxWindow *aParent, const wxString &aText)
Display an error or warning message box with aMessage.
Definition confirm.cpp:192
This file is part of the common library.
static bool registered
#define _(s)
EVT_MENU_RANGE(ID_GERBVIEW_DRILL_FILE1, ID_GERBVIEW_DRILL_FILEMAX, GERBVIEW_FRAME::OnDrlFileHistory) EVT_MENU_RANGE(ID_GERBVIEW_ZIP_FILE1
FRAME_T
The set of EDA_BASE_FRAME derivatives, typically stored in EDA_BASE_FRAME::m_Ident.
Definition frame_type.h:29
@ FRAME_PCB_EDITOR
Definition frame_type.h:38
@ FRAME_SCH
Definition frame_type.h:30
int ExecuteFile(const wxString &aEditorName, const wxString &aFileName, wxProcess *aCallback, bool aFileForKicad)
Call the executable file aEditorName with the parameter aFileName.
Definition gestfich.cpp:161
GIT_BACKEND * GetGitBackend()
CommitResult
Definition git_backend.h:52
InitResult
PullResult
PushResult
int m_GitIconRefreshInterval
The interval in milliseconds to refresh the git icons in the project tree.
static const std::string LegacySchematicFileExtension
static const std::string HtmlFileExtension
static const wxString GerberFileExtensionsRegex
static const std::string NetlistFileExtension
static const std::string GerberJobFileExtension
static const std::string ReportFileExtension
static const std::string LockFileExtension
static const std::string ProjectFileExtension
static const std::string FootprintPlaceFileExtension
static const std::string LegacyPcbFileExtension
static const std::string LegacyProjectFileExtension
static const std::string ProjectLocalSettingsFileExtension
static const std::string KiCadSchematicFileExtension
static const std::string LegacySymbolLibFileExtension
static const std::string LockFilePrefix
static const std::string CsvFileExtension
static const std::string KiCadSymbolLibFileExtension
static const std::string SpiceFileExtension
static const std::string PdfFileExtension
static const std::string TextFileExtension
static const std::string DrawingSheetFileExtension
static const std::string BackupFileSuffix
static const std::string KiCadJobSetFileExtension
static const std::string FootprintAssignmentFileExtension
static const std::string SVGFileExtension
static const std::string DrillFileExtension
static const std::string DesignRulesFileExtension
static const std::string MarkdownFileExtension
static const std::string KiCadFootprintFileExtension
static const std::string ArchiveFileExtension
static const std::string KiCadPcbFileExtension
const wxChar *const tracePathsAndFiles
Flag to enable path and file name debug output.
const wxChar *const traceGit
Flag to enable Git debugging output.
PROJECT & Prj()
Definition kicad.cpp:728
@ ID_PROJECT_TREE
Definition kicad_id.h:32
@ ID_LEFT_FRAME
Definition kicad_id.h:31
EVT_MENU(ID_COMPARE_PROJECT_BRANCHES, KICAD_MANAGER_FRAME::OnCompareProjectBranches) KICAD_MANAGER_FRAME
bool LaunchExternal(const wxString &aPath)
Launches the given file or folder in the host OS.
This file contains miscellaneous commonly used macros and functions.
#define KI_FALLTHROUGH
The KI_FALLTHROUGH macro is to be used when switch statement cases should purposely fallthrough from ...
Definition macros.h:79
MAIL_T
The set of mail types sendable via KIWAY::ExpressMail() and supplied as the aCommand parameter to tha...
Definition mail_type.h:34
@ MAIL_SCH_SAVE
Definition mail_type.h:39
@ MAIL_PCB_SAVE
Definition mail_type.h:40
std::unique_ptr< git_tree, decltype([](git_tree *aTree) { git_tree_free(aTree); })> GitTreePtr
A unique pointer for git_tree objects with automatic cleanup.
std::unique_ptr< git_commit, decltype([](git_commit *aCommit) { git_commit_free(aCommit); })> GitCommitPtr
A unique pointer for git_commit objects with automatic cleanup.
void CollectDiffDeltas(git_diff *aDiff, const std::function< void(const git_diff_delta &)> &aCallback)
Walk every delta in a computed diff, invoking aCallback once per delta.
std::unique_ptr< git_reference, decltype([](git_reference *aRef) { git_reference_free(aRef); })> GitReferencePtr
A unique pointer for git_reference objects with automatic cleanup.
std::unique_ptr< git_diff, decltype([](git_diff *aDiff) { git_diff_free(aDiff); })> GitDiffPtr
A unique pointer for git_diff objects with automatic cleanup.
std::unique_ptr< git_remote, decltype([](git_remote *aRemote) { git_remote_free(aRemote); })> GitRemotePtr
A unique pointer for git_remote objects with automatic cleanup.
bool IsNetworkPath(const wxString &aPath)
Determines if a given path is a network shared file apth On Windows for example, any form of path is ...
bool IsFileHidden(const wxString &aFileName)
Helper function to determine the status of the 'Hidden' file attribute.
Definition unix/io.cpp:109
bool StoreSecret(const wxString &aService, const wxString &aKey, const wxString &aSecret)
KICOMMON_API wxMenuItem * AddMenuItem(wxMenu *aMenu, int aId, const wxString &aText, const wxBitmapBundle &aImage, wxItemKind aType=wxITEM_NORMAL)
Create and insert a menu item with an icon into aMenu.
bool contains(const _Container &__container, _Value __value)
Returns true if the container contains the given value.
Definition kicad_algo.h:96
wxDECLARE_EVENT(wxCUSTOM_PANEL_SHOWN_EVENT, wxCommandEvent)
PGM_BASE & Pgm()
The global program "get" accessor.
#define NAMELESS_PROJECT
default name for nameless projects
Definition project.h:41
project_tree_ids
The frame that shows the tree list of files and subdirectories inside the working directory.
@ ID_PROJECT_OPEN_DIR
@ ID_GIT_PUSH
@ ID_GIT_REVERT_LOCAL
@ ID_JOBS_RUN
@ ID_PROJECT_SWITCH_TO_OTHER
@ ID_PROJECT_NEWDIR
@ ID_PROJECT_RENAME
@ ID_GIT_SWITCH_QUICK5
@ ID_GIT_REMOVE_VCS
@ ID_GIT_COMMIT_FILE
@ ID_GIT_PULL
@ ID_GIT_AMEND_COMMIT
@ ID_GIT_COMPARE
@ ID_PROJECT_TXTEDIT
@ ID_GIT_SWITCH_QUICK2
@ ID_GIT_REMOVE_FROM_INDEX
@ ID_GIT_RESOLVE_CONFLICT
@ ID_GIT_CLONE_PROJECT
@ ID_GIT_SWITCH_QUICK4
@ ID_GIT_SWITCH_QUICK3
@ ID_GIT_COMMIT_PROJECT
@ ID_GIT_SWITCH_BRANCH
@ ID_GIT_ADD_TO_INDEX
@ ID_GIT_SYNC_PROJECT
@ ID_GIT_FETCH
@ ID_GIT_SWITCH_QUICK1
@ ID_GIT_INITIALIZE_PROJECT
@ ID_PROJECT_DELETE
@ ID_GIT_REMOTE_SETTINGS
std::vector< wxString > getProjects(const wxDir &dir)
static const wxChar * s_allowedExtensionsToList[]
static bool promptForGitCredentials(wxWindow *aParent, KIGIT_COMMON *aCommon)
#define wxFileSystemWatcher
#define TO_UTF8(wxstring)
Convert a wxString to a UTF8 encoded C string for all wxWidgets build modes.
KIGIT_COMMON::GIT_CONN_TYPE connType
std::string path
wxString result
Test unit parsing edge cases and error handling.
thread_pool & GetKiCadThreadPool()
Get a reference to the current thread pool.
static thread_pool * tp
BS::priority_thread_pool thread_pool
Definition thread_pool.h:27
wxLogTrace helper definitions.
TREE_FILE_TYPE
Definition of file extensions used in Kicad.
#define PR_NO_ABORT