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 {
687 git_repository* repo = m_TreeProject->GetGitRepo();
689 m_TreeProject->SetGitRepo( nullptr );
690 m_gitIconsInitialized = false;
691 }
692
693 wxFileName fn = pro_dir;
694 bool prjReset = false;
695
696 if( !fn.IsOk() )
697 {
698 fn.Clear();
699 fn.SetPath( PATHS::GetDefaultUserProjectsPath() );
700 fn.SetName( NAMELESS_PROJECT );
702 prjReset = true;
703 }
704
705 bool prjOpened = fn.FileExists();
706
707 // Bind the git repository to the project tree (if it exists and not disabled for this project)
708 if( Pgm().GetCommonSettings()->m_Git.enableGit
709 && !Prj().GetLocalSettings().m_GitIntegrationDisabled )
710 {
711 m_TreeProject->SetGitRepo( KIGIT::PROJECT_GIT_UTILS::GetRepositoryForFile( fn.GetPath().c_str() ) );
712
713 if( m_TreeProject->GetGitRepo() )
714 {
715 // Reset the cancel flag so git operations work after project switches.
716 // EmptyTreePrj() sets this to true during shutdown.
717 m_TreeProject->GitCommon()->SetCancelled( false );
718
719 const char* canonicalWorkDir = git_repository_workdir( m_TreeProject->GetGitRepo() );
720
721 if( canonicalWorkDir )
722 {
724 fn.GetPath(), wxString::FromUTF8( canonicalWorkDir ) );
725 m_TreeProject->GitCommon()->SetProjectDir( symlinkWorkDir );
726 }
727
728 m_TreeProject->GitCommon()->SetUsername( Prj().GetLocalSettings().m_GitRepoUsername );
729 m_TreeProject->GitCommon()->SetSSHKey( Prj().GetLocalSettings().m_GitSSHKey );
730 m_TreeProject->GitCommon()->UpdateCurrentBranchInfo();
731 }
732 }
733
734 // We may have opened a legacy project, in which case GetProjectFileName will return the
735 // name of the migrated (new format) file, which may not have been saved to disk yet.
736 if( !prjOpened && !prjReset )
737 {
739 prjOpened = fn.FileExists();
740
741 // Set the ext back so that in the tree view we see the (not-yet-saved) new file
743 }
744
745 // root tree:
746 m_root = m_TreeProject->AddRoot( fn.GetFullName(), static_cast<int>( TREE_FILE_TYPE::ROOT ),
747 static_cast<int>( TREE_FILE_TYPE::ROOT ) );
748 m_TreeProject->SetItemBold( m_root, true );
749
750 // The main project file is now a JSON file
753
754 m_TreeProject->SetItemData( m_root, data );
755
756 // Now adding all current files if available
757 if( prjOpened )
758 {
759 pro_dir = wxPathOnly( m_Parent->GetProjectFileName() );
760 wxDir dir( pro_dir );
761
762 if( dir.IsOpened() ) // protected dirs will not open, see "man opendir()"
763 {
764 std::vector<wxString> projects = getProjects( dir );
765 wxString filename;
766 bool haveFile = dir.GetFirst( &filename );
767
768 while( haveFile )
769 {
770 if( filename != fn.GetFullName() )
771 {
772 wxString name = dir.GetName() + wxFileName::GetPathSeparator() + filename;
773
774 // Add items living in the project directory, and populate the item
775 // if it is a directory (sub directories will be not populated)
776 addItemToProjectTree( name, m_root, &projects, true );
777 }
778
779 haveFile = dir.GetNext( &filename );
780 }
781 }
782 }
783 else
784 {
785 m_TreeProject->AppendItem( m_root, wxT( "Empty project" ) );
786 }
787
788 m_TreeProject->Expand( m_root );
789
790 // Sort filenames by alphabetic order
791 m_TreeProject->SortChildren( m_root );
792
793 CallAfter(
794 [this] ()
795 {
796 wxLogTrace( traceGit, "PROJECT_TREE_PANE::ReCreateTreePrj: starting timers" );
797 m_gitSyncTimer.Start( 100, wxTIMER_ONE_SHOT );
798 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
799 } );
800}
801
802
804{
805 if( !m_TreeProject->GetGitRepo() )
806 return false;
807
808 GIT_STATUS_HANDLER statusHandler( m_TreeProject->GitCommon() );
809 return statusHandler.HasChangedFiles();
810}
811
812
813void PROJECT_TREE_PANE::onRight( wxTreeEvent& Event )
814{
815 wxTreeItemId curr_item = Event.GetItem();
816
817 // Ensure item is selected (Under Windows right click does not select the item)
818 m_TreeProject->SelectItem( curr_item );
819
820 std::vector<PROJECT_TREE_ITEM*> selection = GetSelectedData();
821 KIGIT_COMMON* git = m_TreeProject->GitCommon();
822
823 bool can_switch_to_project = true;
824 bool can_create_new_directory = true;
825 bool can_open_this_directory = true;
826 bool can_edit = true;
827 bool can_rename = true;
828 bool can_delete = true;
829 bool run_jobs = false;
830
831 bool vcs_has_repo = m_TreeProject->GetGitRepo() != nullptr;
832 bool vcs_can_commit = hasChangedFiles();
833 bool vcs_can_init = !vcs_has_repo;
834 bool vcs_can_set_remote = vcs_has_repo;
835 bool vcs_can_amend = vcs_has_repo && git_repository_head_unborn( m_TreeProject->GetGitRepo() ) == 0;
836 bool gitIntegrationDisabled = Prj().GetLocalSettings().m_GitIntegrationDisabled;
837 // Disable/Enable is a per-project preference toggle, so it's available whenever we
838 // detected a repository for this project or integration is currently disabled.
839 bool vcs_can_remove = vcs_has_repo || gitIntegrationDisabled;
840 bool vcs_can_fetch = vcs_has_repo && git->HasPushAndPullRemote();
841 bool vcs_can_push = vcs_can_fetch && git->HasLocalCommits();
842 bool vcs_can_pull = vcs_can_fetch;
843 bool vcs_can_switch = vcs_has_repo;
844 bool vcs_menu = Pgm().GetCommonSettings()->m_Git.enableGit;
845
846 // Check if the libgit2 library is available via backend
847 bool libgit_init = GetGitBackend() && GetGitBackend()->IsLibraryAvailable();
848
849 vcs_menu &= libgit_init;
850
851 if( selection.size() == 0 )
852 return;
853
854 // Remove things that don't make sense for multiple selections
855 if( selection.size() != 1 )
856 {
857 can_switch_to_project = false;
858 can_create_new_directory = false;
859 can_rename = false;
860 }
861
862 for( PROJECT_TREE_ITEM* item : selection )
863 {
864 // Check for empty project
865 if( !item )
866 {
867 can_switch_to_project = false;
868 can_edit = false;
869 can_rename = false;
870 continue;
871 }
872
873 can_delete = item->CanDelete();
874 can_rename = item->CanRename();
875
876 switch( item->GetType() )
877 {
880 can_rename = false;
881
882 if( item->GetId() == m_TreeProject->GetRootItem() )
883 {
884 can_switch_to_project = false;
885 }
886 else
887 {
888 can_create_new_directory = false;
889 can_open_this_directory = false;
890 }
891 break;
892
894 can_switch_to_project = false;
895 can_edit = false;
896 break;
897
900 can_edit = false;
901 can_switch_to_project = false;
902 can_create_new_directory = false;
903 can_open_this_directory = false;
904 break;
905
907 run_jobs = true;
908 can_edit = false;
910
914
915 default:
916 can_switch_to_project = false;
917 can_create_new_directory = false;
918 can_open_this_directory = false;
919
920 break;
921 }
922 }
923
924 wxMenu popup_menu;
925 wxString text;
926 wxString help_text;
927
928 if( can_switch_to_project )
929 {
930 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_SWITCH_TO_OTHER, _( "Switch to this Project" ),
931 _( "Close all editors, and switch to the selected project" ),
933 popup_menu.AppendSeparator();
934 }
935
936 if( can_create_new_directory )
937 {
938 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_NEWDIR, _( "New Directory..." ),
939 _( "Create a New Directory" ), KiBitmap( BITMAPS::directory ) );
940 }
941
942 if( can_open_this_directory )
943 {
944 if( selection.size() == 1 )
945 {
946#ifdef __APPLE__
947 text = _( "Reveal in Finder" );
948 help_text = _( "Reveals the directory in a Finder window" );
949#else
950 text = _( "Open Directory in File Explorer" );
951 help_text = _( "Opens the directory in the default system file manager" );
952#endif
953 }
954 else
955 {
956#ifdef __APPLE__
957 text = _( "Reveal in Finder" );
958 help_text = _( "Reveals the directories in a Finder window" );
959#else
960 text = _( "Open Directories in File Explorer" );
961 help_text = _( "Opens the directories in the default system file manager" );
962#endif
963 }
964
965 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_OPEN_DIR, text, help_text,
967 }
968
969 if( can_edit )
970 {
971 if( selection.size() == 1 )
972 help_text = _( "Open the file in a Text Editor" );
973 else
974 help_text = _( "Open files in a Text Editor" );
975
976 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_TXTEDIT, _( "Edit in a Text Editor" ), help_text,
978 }
979
980 if( run_jobs && selection.size() == 1 )
981 {
982 KIUI::AddMenuItem( &popup_menu, ID_JOBS_RUN, _( "Run Jobs" ), help_text,
984 }
985
986 if( can_rename )
987 {
988 if( selection.size() == 1 )
989 {
990 text = _( "Rename File..." );
991 help_text = _( "Rename file" );
992 }
993 else
994 {
995 text = _( "Rename Files..." );
996 help_text = _( "Rename files" );
997 }
998
999 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_RENAME, text, help_text,
1001 }
1002
1003 if( can_delete )
1004 {
1005 if( selection.size() == 1 )
1006 help_text = _( "Delete the file and its content" );
1007 else
1008 help_text = _( "Delete the files and their contents" );
1009
1010 if( can_switch_to_project
1011 || can_create_new_directory
1012 || can_open_this_directory
1013 || can_edit
1014 || can_rename )
1015 {
1016 popup_menu.AppendSeparator();
1017 }
1018
1019#ifdef __WINDOWS__
1020 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_DELETE, _( "Delete" ), help_text,
1022#else
1023 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_DELETE, _( "Move to Trash" ), help_text,
1025#endif
1026 }
1027
1028 if( vcs_menu )
1029 {
1030 wxMenu* vcs_submenu = new wxMenu();
1031 wxMenu* branch_submenu = new wxMenu();
1032 wxMenuItem* vcs_menuitem = nullptr;
1033
1034 vcs_menuitem = vcs_submenu->Append( ID_GIT_INITIALIZE_PROJECT,
1035 _( "Add Project to Version Control..." ),
1036 _( "Initialize a new repository" ) );
1037 vcs_menuitem->Enable( vcs_can_init );
1038
1039 vcs_menuitem = vcs_submenu->Append( ID_GIT_REMOTE_SETTINGS, _( "Configure Default Remote..." ),
1040 _( "Set or change the default remote URL and credentials" ) );
1041 vcs_menuitem->Enable( vcs_can_set_remote );
1042
1043 vcs_menuitem = vcs_submenu->Append( ID_GIT_COMMIT_PROJECT, _( "Commit Project..." ),
1044 _( "Commit changes to the local repository" ) );
1045 vcs_menuitem->Enable( vcs_can_commit );
1046
1047 vcs_menuitem = vcs_submenu->Append( ID_GIT_AMEND_COMMIT, _( "Amend Last Commit..." ),
1048 _( "Rewrite the most recent commit on the current branch" ) );
1049 vcs_menuitem->Enable( vcs_can_amend );
1050
1051 wxString pushLabel = _( "Push" );
1052 wxString pullLabel = _( "Pull" );
1053
1054 if( !m_gitCurrentUpstream.empty() && !m_gitCurrentBranchName.empty() )
1055 {
1056 pushLabel = wxString::Format( _( "Push %s to %s" ), m_gitCurrentBranchName, m_gitCurrentUpstream );
1057 pullLabel = wxString::Format( _( "Pull from %s" ), m_gitCurrentUpstream );
1058 }
1059
1060 vcs_menuitem =
1061 vcs_submenu->Append( ID_GIT_PUSH, pushLabel, _( "Push committed local changes to remote repository" ) );
1062 vcs_menuitem->Enable( vcs_can_push );
1063
1064 vcs_menuitem =
1065 vcs_submenu->Append( ID_GIT_PULL, pullLabel, _( "Pull changes from remote repository into local" ) );
1066 vcs_menuitem->Enable( vcs_can_pull );
1067
1068 vcs_submenu->AppendSeparator();
1069
1070 vcs_menuitem = vcs_submenu->Append( ID_GIT_COMMIT_FILE, _( "Commit File..." ),
1071 _( "Commit changes to the local repository" ) );
1072 vcs_menuitem->Enable( vcs_can_commit );
1073
1074 vcs_submenu->AppendSeparator();
1075
1076 // vcs_menuitem = vcs_submenu->Append( ID_GIT_COMPARE, _( "Diff" ),
1077 // _( "Show changes between the repository and working tree" ) );
1078 // vcs_menuitem->Enable( vcs_can_diff );
1079
1080 std::vector<wxString> branchNames = m_TreeProject->GitCommon()->GetBranchNames();
1081
1082 // Skip the first one (that is the current branch)
1083 for( size_t ii = 1; ii < branchNames.size() && ii < 6; ++ii )
1084 {
1085 wxString msg = _( "Switch to branch " ) + branchNames[ii];
1086 vcs_menuitem = branch_submenu->Append( ID_GIT_SWITCH_BRANCH + ii, branchNames[ii], msg );
1087 vcs_menuitem->Enable( vcs_can_switch );
1088 }
1089
1090 vcs_menuitem = branch_submenu->Append( ID_GIT_SWITCH_BRANCH, _( "Other..." ),
1091 _( "Switch to a different branch" ) );
1092 vcs_menuitem->Enable( vcs_can_switch );
1093
1094 vcs_submenu->Append( ID_GIT_SWITCH_BRANCH, _( "Switch to Branch" ), branch_submenu );
1095
1096 vcs_submenu->AppendSeparator();
1097
1098 if( gitIntegrationDisabled )
1099 {
1100 vcs_menuitem = vcs_submenu->Append( ID_GIT_REMOVE_VCS, _( "Enable Git Integration" ),
1101 _( "Re-enable Git integration for this project" ) );
1102 }
1103 else
1104 {
1105 vcs_menuitem = vcs_submenu->Append( ID_GIT_REMOVE_VCS, _( "Disable Git Integration" ),
1106 _( "Disable Git integration for this project" ) );
1107 }
1108
1109 vcs_menuitem->Enable( vcs_can_remove );
1110
1111 popup_menu.AppendSeparator();
1112 popup_menu.AppendSubMenu( vcs_submenu, _( "Version Control" ) );
1113 }
1114
1115 if( popup_menu.GetMenuItemCount() > 0 )
1116 PopupMenu( &popup_menu );
1117}
1118
1119
1121{
1122 wxString editorname = Pgm().GetTextEditor();
1123
1124 if( editorname.IsEmpty() )
1125 {
1126 wxMessageBox( _( "No text editor selected in KiCad. Please choose one." ) );
1127 return;
1128 }
1129
1130 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
1131
1132 for( PROJECT_TREE_ITEM* item_data : tree_data )
1133 {
1134 wxString fullFileName = item_data->GetFileName();
1135
1136 if( !fullFileName.IsEmpty() )
1137 {
1138 ExecuteFile( editorname, fullFileName.wc_str(), nullptr, false );
1139 }
1140 }
1141}
1142
1143
1144void PROJECT_TREE_PANE::onDeleteFile( wxCommandEvent& event )
1145{
1146 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
1147
1148 for( PROJECT_TREE_ITEM* item_data : tree_data )
1149 item_data->Delete();
1150}
1151
1152
1153void PROJECT_TREE_PANE::onRenameFile( wxCommandEvent& event )
1154{
1155 wxTreeItemId curr_item = m_TreeProject->GetFocusedItem();
1156 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
1157
1158 // XXX: Unnecessary?
1159 if( tree_data.size() != 1 )
1160 return;
1161
1162 wxString buffer = m_TreeProject->GetItemText( curr_item );
1163 wxString msg = wxString::Format( _( "Change filename: '%s'" ),
1164 tree_data[0]->GetFileName() );
1165 wxTextEntryDialog dlg( wxGetTopLevelParent( this ), msg, _( "Change filename" ), buffer );
1166
1167 if( dlg.ShowModal() != wxID_OK )
1168 return; // canceled by user
1169
1170 buffer = dlg.GetValue();
1171 buffer.Trim( true );
1172 buffer.Trim( false );
1173
1174 if( buffer.IsEmpty() )
1175 return; // empty file name not allowed
1176
1177 tree_data[0]->Rename( buffer, true );
1178 m_isRenaming = true;
1179}
1180
1181
1182void PROJECT_TREE_PANE::onSelect( wxTreeEvent& Event )
1183{
1184 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
1185
1186 if( tree_data.size() != 1 )
1187 return;
1188
1189 // Bookmark the selected item but don't try and activate it until later. If we do it now,
1190 // there will be more events at least on Windows in this frame that will steal focus from
1191 // any newly launched windows
1192 m_selectedItem = tree_data[0];
1193}
1194
1195
1196void PROJECT_TREE_PANE::onIdle( wxIdleEvent& aEvent )
1197{
1198 // Idle executes once all other events finished processing. This makes it ideal to launch
1199 // a new window without starting Focus wars.
1200 if( m_watcherNeedReset )
1201 {
1202 m_selectedItem = nullptr;
1204 }
1205
1206 if( m_selectedItem != nullptr && m_TreeProject->GetRootItem().IsOk() )
1207 {
1208 // Make sure m_selectedItem still exists in the tree before activating it.
1209 std::vector<wxTreeItemId> validItemIds;
1210 m_TreeProject->GetItemsRecursively( m_TreeProject->GetRootItem(), validItemIds );
1211
1212 for( wxTreeItemId id : validItemIds )
1213 {
1214 if( GetItemIdData( id ) == m_selectedItem )
1215 {
1216 // Activate launches a window which may run the event loop on top of us and cause
1217 // onIdle to get called again, so be sure to null out m_selectedItem first.
1219 m_selectedItem = nullptr;
1220
1221 item->Activate( this );
1222 break;
1223 }
1224 }
1225 }
1226}
1227
1228
1229void PROJECT_TREE_PANE::onExpand( wxTreeEvent& Event )
1230{
1231 wxTreeItemId itemId = Event.GetItem();
1232 PROJECT_TREE_ITEM* tree_data = GetItemIdData( itemId );
1233
1234 if( !tree_data )
1235 return;
1236
1237 if( tree_data->GetType() != TREE_FILE_TYPE::DIRECTORY )
1238 return;
1239
1240 // explore list of non populated subdirs, and populate them
1241 wxTreeItemIdValue cookie;
1242 wxTreeItemId kid = m_TreeProject->GetFirstChild( itemId, cookie );
1243
1244#ifndef __WINDOWS__
1245 bool subdir_populated = false;
1246#endif
1247
1248 for( ; kid.IsOk(); kid = m_TreeProject->GetNextChild( itemId, cookie ) )
1249 {
1250 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
1251
1252 if( !itemData || itemData->GetType() != TREE_FILE_TYPE::DIRECTORY )
1253 continue;
1254
1255 if( itemData->IsPopulated() )
1256 continue;
1257
1258 wxString fileName = itemData->GetFileName();
1259 wxDir dir( fileName );
1260
1261 if( dir.IsOpened() )
1262 {
1263 std::vector<wxString> projects = getProjects( dir );
1264 wxString dir_filename;
1265 bool haveFile = dir.GetFirst( &dir_filename );
1266
1267 while( haveFile )
1268 {
1269 // Add name to tree item, but do not recurse in subdirs:
1270 wxString name = fileName + wxFileName::GetPathSeparator() + dir_filename;
1271 addItemToProjectTree( name, kid, &projects, false );
1272
1273 haveFile = dir.GetNext( &dir_filename );
1274 }
1275
1276 itemData->SetPopulated( true ); // set state to populated
1277
1278#ifndef __WINDOWS__
1279 subdir_populated = true;
1280#endif
1281 }
1282
1283 // Sort filenames by alphabetic order
1284 m_TreeProject->SortChildren( kid );
1285 }
1286
1287#ifndef __WINDOWS__
1288 if( subdir_populated )
1289 m_watcherNeedReset = true;
1290#endif
1291}
1292
1293
1294std::vector<PROJECT_TREE_ITEM*> PROJECT_TREE_PANE::GetSelectedData()
1295{
1296 wxArrayTreeItemIds selection;
1297 std::vector<PROJECT_TREE_ITEM*> data;
1298
1299 m_TreeProject->GetSelections( selection );
1300
1301 for( const wxTreeItemId itemId : selection )
1302 {
1303 PROJECT_TREE_ITEM* item = GetItemIdData( itemId );
1304
1305 if( !item )
1306 {
1307 wxLogTrace( traceGit, wxS( "Null tree item returned for selection, dynamic_cast failed?" ) );
1308 continue;
1309 }
1310
1311 data.push_back( item );
1312 }
1313
1314 return data;
1315}
1316
1317
1319{
1320 return dynamic_cast<PROJECT_TREE_ITEM*>( m_TreeProject->GetItemData( aId ) );
1321}
1322
1323
1324wxTreeItemId PROJECT_TREE_PANE::findSubdirTreeItem( const wxString& aSubDir )
1325{
1326 wxString prj_dir = wxPathOnly( m_Parent->GetProjectFileName() );
1327
1328 // If the subdir is the current working directory, return m_root
1329 // in main list:
1330 if( prj_dir == aSubDir )
1331 return m_root;
1332
1333 // The subdir is in the main tree or in a subdir: Locate it
1334 wxTreeItemIdValue cookie;
1335 wxTreeItemId root_id = m_root;
1336 std::stack<wxTreeItemId> subdirs_id;
1337
1338 wxTreeItemId child = m_TreeProject->GetFirstChild( root_id, cookie );
1339
1340 while( true )
1341 {
1342 if( ! child.IsOk() )
1343 {
1344 if( subdirs_id.empty() ) // all items were explored
1345 {
1346 root_id = child; // Not found: return an invalid wxTreeItemId
1347 break;
1348 }
1349 else
1350 {
1351 root_id = subdirs_id.top();
1352 subdirs_id.pop();
1353 child = m_TreeProject->GetFirstChild( root_id, cookie );
1354
1355 if( !child.IsOk() )
1356 continue;
1357 }
1358 }
1359
1360 PROJECT_TREE_ITEM* itemData = GetItemIdData( child );
1361
1362 if( itemData && ( itemData->GetType() == TREE_FILE_TYPE::DIRECTORY ) )
1363 {
1364 if( itemData->GetFileName() == aSubDir ) // Found!
1365 {
1366 root_id = child;
1367 break;
1368 }
1369
1370 // child is a subdir, push in list to explore it later
1371 if( itemData->IsPopulated() )
1372 subdirs_id.push( child );
1373 }
1374
1375 child = m_TreeProject->GetNextChild( root_id, cookie );
1376 }
1377
1378 return root_id;
1379}
1380
1381
1382void PROJECT_TREE_PANE::onFileSystemEvent( wxFileSystemWatcherEvent& event )
1383{
1384 // No need to process events when we're shutting down
1385 if( !m_watcher )
1386 return;
1387
1388 // Ignore events that are not file creation, deletion, renaming or modification because
1389 // they are not relevant to the project tree.
1390 if( !( event.GetChangeType() & ( wxFSW_EVENT_CREATE |
1391 wxFSW_EVENT_DELETE |
1392 wxFSW_EVENT_RENAME |
1393 wxFSW_EVENT_MODIFY ) ) )
1394 {
1395 return;
1396 }
1397
1398 const wxFileName& pathModified = event.GetPath();
1399
1400 // Ignore events from .history directory (local backup)
1401 if( pathModified.GetFullPath().Contains( wxS( ".history" ) ) )
1402 return;
1403
1404 wxString subdir = pathModified.GetPath();
1405 wxString fn = pathModified.GetFullPath();
1406
1407 // Adjust directories to look like a file item (path and name).
1408 if( pathModified.GetFullName().IsEmpty() )
1409 {
1410 subdir = subdir.BeforeLast( '/' );
1411 fn = fn.BeforeLast( '/' );
1412 }
1413
1414 wxTreeItemId root_id = findSubdirTreeItem( subdir );
1415
1416 if( !root_id.IsOk() )
1417 return;
1418
1419 CallAfter( [this] ()
1420 {
1421 wxLogTrace( traceGit, wxS( "File system event detected, updating tree cache" ) );
1422 m_gitStatusTimer.Start( 1500, wxTIMER_ONE_SHOT );
1423 } );
1424
1425 wxTreeItemIdValue cookie; // dummy variable needed by GetFirstChild()
1426 wxTreeItemId kid = m_TreeProject->GetFirstChild( root_id, cookie );
1427
1428 switch( event.GetChangeType() )
1429 {
1430 case wxFSW_EVENT_CREATE:
1431 {
1432 wxTreeItemId newitem = addItemToProjectTree( fn, root_id, nullptr, true );
1433
1434 // If we are in the process of renaming a file, select the new one
1435 // This is needed for MSW and OSX, since we don't get RENAME events from them, just a
1436 // pair of DELETE and CREATE events.
1437 if( m_isRenaming && newitem.IsOk() )
1438 {
1439 m_TreeProject->SelectItem( newitem );
1440 m_isRenaming = false;
1441 }
1442 }
1443 break;
1444
1445 case wxFSW_EVENT_DELETE:
1446 while( kid.IsOk() )
1447 {
1448 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
1449
1450 if( itemData && itemData->GetFileName() == fn )
1451 {
1452 m_TreeProject->Delete( kid );
1453 return;
1454 }
1455 kid = m_TreeProject->GetNextChild( root_id, cookie );
1456 }
1457 break;
1458
1459 case wxFSW_EVENT_RENAME :
1460 {
1461 const wxFileName& newpath = event.GetNewPath();
1462 wxString newdir = newpath.GetPath();
1463 wxString newfn = newpath.GetFullPath();
1464
1465 while( kid.IsOk() )
1466 {
1467 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
1468
1469 if( itemData && itemData->GetFileName() == fn )
1470 {
1471 m_TreeProject->Delete( kid );
1472 break;
1473 }
1474
1475 kid = m_TreeProject->GetNextChild( root_id, cookie );
1476 }
1477
1478 // Add the new item only if it is not the current project file (root item).
1479 // Remember: this code is called by a wxFileSystemWatcherEvent event, and not always
1480 // called after an actual file rename, and the cleanup code does not explore the
1481 // root item, because it cannot be renamed by the user. Also, ensure the new file
1482 // actually exists on the file system before it is readded. On Linux, moving a file
1483 // to the trash can cause the same path to be returned in both the old and new paths
1484 // of the event, even though the file isn't there anymore.
1485 PROJECT_TREE_ITEM* rootData = GetItemIdData( root_id );
1486
1487 if( rootData && newpath.Exists() && ( newfn != rootData->GetFileName() ) )
1488 {
1489 wxTreeItemId newroot_id = findSubdirTreeItem( newdir );
1490 wxTreeItemId newitem = addItemToProjectTree( newfn, newroot_id, nullptr, true );
1491
1492 // If the item exists, select it
1493 if( newitem.IsOk() )
1494 m_TreeProject->SelectItem( newitem );
1495 }
1496
1497 m_isRenaming = false;
1498 }
1499 break;
1500
1501 default:
1502 return;
1503 }
1504
1505 // Sort filenames by alphabetic order
1506 m_TreeProject->SortChildren( root_id );
1507}
1508
1509
1511{
1512 m_watcherNeedReset = false;
1513
1514 wxString prj_dir = wxPathOnly( m_Parent->GetProjectFileName() );
1515
1516#if defined( _WIN32 )
1517 KISTATUSBAR* statusBar = static_cast<KISTATUSBAR*>( m_Parent->GetStatusBar() );
1518
1519 if( KIPLATFORM::ENV::IsNetworkPath( prj_dir ) )
1520 {
1521 // Due to a combination of a bug in SAMBA sending bad change event IDs and wxWidgets
1522 // choosing to fault on an invalid event ID instead of sanely ignoring them we need to
1523 // avoid spawning a filewatcher. Unfortunately this punishes corporate environments with
1524 // Windows Server shares :/
1525 m_Parent->m_FileWatcherInfo = _( "Network path: not monitoring folder changes" );
1526 statusBar->SetEllipsedTextField( m_Parent->m_FileWatcherInfo, 1 );
1527 return;
1528 }
1529 else
1530 {
1531 m_Parent->m_FileWatcherInfo = _( "Local path: monitoring folder changes" );
1532 statusBar->SetEllipsedTextField( m_Parent->m_FileWatcherInfo, 1 );
1533 }
1534#endif
1535
1536 // Prepare file watcher:
1537 if( m_watcher )
1538 {
1539 m_watcher->RemoveAll();
1540 }
1541 else
1542 {
1543 // Create a wxWidgets log handler to catch errors during watcher creation
1544 // We need to to this because we cannot get error codes from the wxFileSystemWatcher
1545 // constructor. On Linux, if inotify cannot be initialized (usually due to resource limits),
1546 // wxWidgets will throw a system error and then, we throw another error below, trying to
1547 // add paths to a null watcher. We skip this by installing a temporary log handler that
1548 // catches errors during watcher creation and aborts if any error is detected.
1549 class WatcherLogHandler : public wxLog
1550 {
1551 public:
1552 explicit WatcherLogHandler( bool* err ) :
1553 m_err( err )
1554 {
1555 if( m_err )
1556 *m_err = false;
1557 }
1558
1559 protected:
1560 void DoLogTextAtLevel( wxLogLevel level, const wxString& text ) override
1561 {
1562 if( m_err && ( level == wxLOG_Error || level == wxLOG_FatalError ) )
1563 *m_err = true;
1564 }
1565
1566 private:
1567 bool* m_err;
1568 };
1569
1570 bool watcherHasError = false;
1571 WatcherLogHandler tmpLog( &watcherHasError );
1572 wxLog* oldLog = wxLog::SetActiveTarget( &tmpLog );
1573
1575 m_watcher->SetOwner( this );
1576
1577 // Restore previous log handler
1578 wxLog::SetActiveTarget( oldLog );
1579
1580 if( watcherHasError )
1581 {
1582 return;
1583 }
1584 }
1585
1586 // We can see wxString under a debugger, not a wxFileName
1587 wxFileName fn;
1588 fn.AssignDir( prj_dir );
1589 fn.DontFollowLink();
1590
1591 // Add directories which should be monitored.
1592 // under windows, we add the curr dir and all subdirs
1593 // under unix, we add only the curr dir and the populated subdirs
1594 // see http://docs.wxwidgets.org/trunk/classwx_file_system_watcher.htm
1595 // under unix, the file watcher needs more work to be efficient
1596 // moreover, under wxWidgets 2.9.4, AddTree does not work properly.
1597 {
1598 wxLogNull logNo; // avoid log messages
1599#ifdef __WINDOWS__
1600 if( ! m_watcher->AddTree( fn ) )
1601 {
1602 wxLogTrace( tracePathsAndFiles, "%s: failed to add '%s'\n", __func__,
1603 TO_UTF8( fn.GetFullPath() ) );
1604 return;
1605 }
1606 }
1607#else
1608 if( !m_watcher->Add( fn ) )
1609 {
1610 wxLogTrace( tracePathsAndFiles, "%s: failed to add '%s'\n", __func__,
1611 TO_UTF8( fn.GetFullPath() ) );
1612 return;
1613 }
1614 }
1615
1616 if( m_TreeProject->IsEmpty() )
1617 return;
1618
1619 // Add subdirs
1620 wxTreeItemIdValue cookie;
1621 wxTreeItemId root_id = m_root;
1622
1623 std::stack < wxTreeItemId > subdirs_id;
1624
1625 wxTreeItemId kid = m_TreeProject->GetFirstChild( root_id, cookie );
1626 int total_watch_count = 0;
1627
1628 while( total_watch_count < ADVANCED_CFG::GetCfg().m_MaxFilesystemWatchers )
1629 {
1630 if( !kid.IsOk() )
1631 {
1632 if( subdirs_id.empty() ) // all items were explored
1633 {
1634 break;
1635 }
1636 else
1637 {
1638 root_id = subdirs_id.top();
1639 subdirs_id.pop();
1640 kid = m_TreeProject->GetFirstChild( root_id, cookie );
1641
1642 if( !kid.IsOk() )
1643 continue;
1644 }
1645 }
1646
1647 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
1648
1649 if( itemData && itemData->GetType() == TREE_FILE_TYPE::DIRECTORY )
1650 {
1651 // we can see wxString under a debugger, not a wxFileName
1652 const wxString& path = itemData->GetFileName();
1653
1654 // Skip .history directory and its descendants (local backup)
1655 if( path.Contains( wxS( ".history" ) ) )
1656 {
1657 kid = m_TreeProject->GetNextChild( root_id, cookie );
1658 continue;
1659 }
1660
1661 wxLogTrace( tracePathsAndFiles, "%s: add '%s'\n", __func__, TO_UTF8( path ) );
1662
1663 if( wxFileName::IsDirReadable( path ) ) // linux whines about watching protected dir
1664 {
1665 {
1666 // Silence OS errors that come from the watcher
1667 wxLogNull silence;
1668 fn.AssignDir( path );
1669 m_watcher->Add( fn );
1670 total_watch_count++;
1671 }
1672
1673 // if kid is a subdir, push in list to explore it later
1674 if( itemData->IsPopulated() && m_TreeProject->GetChildrenCount( kid ) )
1675 subdirs_id.push( kid );
1676 }
1677 }
1678
1679 kid = m_TreeProject->GetNextChild( root_id, cookie );
1680 }
1681
1682 if( total_watch_count >= ADVANCED_CFG::GetCfg().m_MaxFilesystemWatchers )
1683 wxLogTrace( tracePathsAndFiles, "%s: too many directories to watch\n", __func__ );
1684#endif
1685
1686#if defined(DEBUG) && 1
1687 wxArrayString paths;
1688 m_watcher->GetWatchedPaths( &paths );
1689 wxLogTrace( tracePathsAndFiles, "%s: watched paths:", __func__ );
1690
1691 for( unsigned ii = 0; ii < paths.GetCount(); ii++ )
1692 wxLogTrace( tracePathsAndFiles, " %s\n", TO_UTF8( paths[ii] ) );
1693#endif
1694 }
1695
1696
1698{
1699 // Make sure we don't try to inspect the tree after we've deleted its items.
1701
1702 m_TreeProject->DeleteAllItems();
1703
1704 if( m_TreeProject->GetGitRepo() )
1705 {
1706 KIGIT_COMMON* common = m_TreeProject->GitCommon();
1707 common->SetCancelled( true );
1708
1709 std::unique_lock<std::mutex> lock( common->m_gitActionMutex, std::try_to_lock );
1710
1711 constexpr auto kGraceMs = std::chrono::seconds( 2 );
1712 auto graceEnd = std::chrono::steady_clock::now() + kGraceMs;
1713
1714 while( !lock.owns_lock() && std::chrono::steady_clock::now() < graceEnd )
1715 {
1716 if( lock.try_lock() )
1717 break;
1718 std::this_thread::sleep_for( std::chrono::milliseconds( 50 ) );
1719 }
1720
1721 constexpr auto kCheckInterval = std::chrono::seconds( 30 );
1722 bool userAbandoned = false;
1723
1724 while( !lock.owns_lock() && !userAbandoned )
1725 {
1726 auto intervalEnd = std::chrono::steady_clock::now() + kCheckInterval;
1727
1728 {
1729 wxProgressDialog progress( _( "Please wait" ),
1730 _( "Closing project..." ),
1731 100, this,
1732 wxPD_APP_MODAL | wxPD_SMOOTH );
1733
1734 while( !lock.try_lock()
1735 && std::chrono::steady_clock::now() < intervalEnd )
1736 {
1737 progress.Pulse();
1738 std::this_thread::sleep_for( std::chrono::milliseconds( 100 ) );
1739 wxYield();
1740 }
1741 }
1742
1743 if( lock.owns_lock() )
1744 break;
1745
1746 wxMessageDialog ask( this,
1747 _( "A Git operation is still running.\n"
1748 "Keep waiting, or abandon?" ),
1749 _( "Git Operation Delayed" ),
1750 wxYES_NO | wxICON_QUESTION );
1751 ask.SetYesNoLabels( _( "Keep Waiting" ), _( "Abandon" ) );
1752
1753 if( ask.ShowModal() == wxID_NO )
1754 userAbandoned = true;
1755 }
1756
1757 if( userAbandoned )
1758 {
1759 git_repository* orphan = m_TreeProject->GetGitRepo();
1760 std::unique_ptr<KIGIT_COMMON> oldCommon = m_TreeProject->TakeGitCommon();
1761 m_TreeProject->SetGitRepo( nullptr );
1762
1763 // Register the cleanup thread with the orphan registry so the
1764 // shutdown path can wait for it before tearing down libgit2.
1765 // Cancellation has already been requested via SetCancelled() above,
1766 // and progress_cb/transfer_progress_cb honour it so long-running
1767 // fetches will exit with GIT_EUSER as soon as they hit a callback.
1768
1769 GIT_BACKEND* backend = GetGitBackend();
1770 wxString projectDir = oldCommon ? oldCommon->GetProjectDir() : wxString();
1771
1772 auto cleanup = [orphan, old = std::move( oldCommon )]() mutable
1773 {
1774 std::lock_guard<std::mutex> g( old->m_gitActionMutex );
1775 git_repository_free( orphan );
1776 };
1777
1778 bool registered = false;
1779
1780 if( backend )
1781 {
1782 std::string label = "abandon close " + projectDir.ToStdString();
1783 registered = backend->OrphanRegistry().Register( label, std::move( cleanup ) );
1784 }
1785
1786 if( !registered )
1787 {
1788 // Either no backend is available, or the registry has already
1789 // entered shutdown. Fall back to freeing synchronously so the
1790 // repository handle is not leaked; this blocks on the git
1791 // action mutex but is safe because libgit2 shutdown is gated
1792 // on the same registry.
1793
1794 cleanup();
1795 }
1796 }
1797 else
1798 {
1799 git_repository* repo = m_TreeProject->GetGitRepo();
1801 m_TreeProject->SetGitRepo( nullptr );
1802 }
1803 }
1804}
1805
1806
1807void PROJECT_TREE_PANE::onThemeChanged( wxSysColourChangedEvent &aEvent )
1808{
1810 m_TreeProject->LoadIcons();
1811 m_TreeProject->Refresh();
1812
1813 aEvent.Skip();
1814}
1815
1816
1817void PROJECT_TREE_PANE::onPaint( wxPaintEvent& event )
1818{
1819 wxRect rect( wxPoint( 0, 0 ), GetClientSize() );
1820 wxPaintDC dc( this );
1821
1822 dc.SetBrush( wxSystemSettings::GetColour( wxSYS_COLOUR_FRAMEBK ) );
1823 dc.SetPen( wxPen( wxSystemSettings::GetColour( wxSYS_COLOUR_ACTIVEBORDER ), 1 ) );
1824
1825 dc.DrawLine( rect.GetLeft(), rect.GetTop(), rect.GetLeft(), rect.GetBottom() );
1826 dc.DrawLine( rect.GetRight(), rect.GetTop(), rect.GetRight(), rect.GetBottom() );
1827}
1828
1829
1830void KICAD_MANAGER_FRAME::OnChangeWatchedPaths( wxCommandEvent& aEvent )
1831{
1832 m_projectTreePane->FileWatcherReset();
1833}
1834
1835
1836// Prompts for git credentials after an auth failure. Returns false if cancelled.
1837static bool promptForGitCredentials( wxWindow* aParent, KIGIT_COMMON* aCommon )
1838{
1839 DIALOG_GIT_CREDENTIALS dlg( aParent, aCommon->GetRemote(), aCommon->GetConnType(), aCommon->GetUsername(),
1840 wxEmptyString );
1841
1842 if( dlg.ShowModal() != wxID_OK )
1843 return false;
1844
1845 aCommon->SetUsername( dlg.GetUsername() );
1846 aCommon->SetPassword( dlg.GetPassword() );
1847
1848 if( dlg.GetConnType() == KIGIT_COMMON::GIT_CONN_TYPE::GIT_CONN_SSH && !dlg.GetSSHKey().IsEmpty() )
1849 {
1850 aCommon->SetSSHKey( dlg.GetSSHKey() );
1851 }
1852
1853 if( dlg.SaveCredentials() )
1854 {
1855 KIPLATFORM::SECRETS::StoreSecret( aCommon->GetRemote(), aCommon->GetUsername(), dlg.GetPassword() );
1856 }
1857
1858 aCommon->ClearAuthFailure();
1859 aCommon->TestedTypes() = 0;
1860 aCommon->ResetNextKey();
1861 return true;
1862}
1863
1864
1865void PROJECT_TREE_PANE::onGitInitializeProject( wxCommandEvent& aEvent )
1866{
1867 PROJECT_TREE_ITEM* tree_data = GetItemIdData( m_TreeProject->GetRootItem() );
1868
1869 wxString dir = tree_data->GetDir();
1870
1871 if( dir.empty() )
1872 {
1873 wxLogError( "Failed to initialize git project: project directory is empty." );
1874 return;
1875 }
1876
1877 GIT_INIT_HANDLER initHandler( m_TreeProject->GitCommon() );
1878 wxWindow* topLevelParent = wxGetTopLevelParent( this );
1879
1880 if( initHandler.IsRepository( dir ) )
1881 {
1882 DisplayInfoMessage( topLevelParent,
1883 _( "The selected directory is already a Git project." ) );
1884 return;
1885 }
1886
1887 InitResult result = initHandler.InitializeRepository( dir );
1889 {
1890 DisplayErrorMessage( m_parent, _( "Failed to initialize Git project." ),
1891 initHandler.GetErrorString() );
1892 return;
1893 }
1894
1895 m_gitLastError = GIT_ERROR_NONE;
1896
1897 m_TreeProject->GitCommon()->SetCancelled( false );
1898
1899 const char* canonicalWorkDir = git_repository_workdir( initHandler.GetRepo() );
1900
1901 if( canonicalWorkDir )
1902 {
1904 dir, wxString::FromUTF8( canonicalWorkDir ) );
1905 m_TreeProject->GitCommon()->SetProjectDir( symlinkWorkDir );
1906 }
1907
1908 DIALOG_GIT_REPOSITORY dlg( topLevelParent, initHandler.GetRepo() );
1909 dlg.SetTitle( _( "Set default remote" ) );
1910 dlg.SetSkipButtonLabel( _( "Skip" ) );
1911
1912 if( dlg.ShowModal() != wxID_OK )
1913 return;
1914
1915 // Set up the remote
1916 RemoteConfig remoteConfig;
1917 remoteConfig.url = dlg.GetRepoURL();
1918 remoteConfig.username = dlg.GetUsername();
1919 remoteConfig.password = dlg.GetPassword();
1920 remoteConfig.sshKey = dlg.GetRepoSSHPath();
1921 remoteConfig.connType = dlg.GetRepoType();
1922
1923 if( !initHandler.SetupRemote( remoteConfig ) )
1924 {
1925 DisplayErrorMessage( m_parent, _( "Failed to set default remote." ),
1926 initHandler.GetErrorString() );
1927 return;
1928 }
1929
1930 m_gitLastError = GIT_ERROR_NONE;
1931
1932 KIGIT_COMMON* common = m_TreeProject->GitCommon();
1933
1934 while( true )
1935 {
1936 common->ClearAuthFailure();
1937
1938 GIT_PULL_HANDLER handler( common );
1939 handler.SetProgressReporter(
1940 std::make_unique<WX_PROGRESS_REPORTER>( this, _( "Fetch Remote" ), 1, PR_NO_ABORT ) );
1941
1942 if( handler.PerformFetch() )
1943 break;
1944
1945 if( common->WasAuthFailure() && promptForGitCredentials( m_parent, common ) )
1946 continue;
1947
1948 break;
1949 }
1950
1954
1958 Prj().GetLocalSettings().m_GitRepoType = "https";
1959 else
1960 Prj().GetLocalSettings().m_GitRepoType = "local";
1961}
1962
1963
1964void PROJECT_TREE_PANE::onGitRemoteSettings( wxCommandEvent& aEvent )
1965{
1966 KIGIT_COMMON* common = m_TreeProject->GitCommon();
1967 git_repository* repo = common ? common->GetRepo() : nullptr;
1968
1969 if( !repo )
1970 return;
1971
1972 wxString existingURL;
1973 git_remote* remote = nullptr;
1974
1975 if( git_remote_lookup( &remote, repo, "origin" ) == GIT_OK )
1976 {
1977 KIGIT::GitRemotePtr remotePtr( remote );
1978
1979 if( const char* url = git_remote_url( remote ) )
1980 existingURL = wxString::FromUTF8( url );
1981 }
1982
1983 wxWindow* topLevelParent = wxGetTopLevelParent( this );
1984 DIALOG_GIT_REPOSITORY dlg( topLevelParent, repo, existingURL );
1985 dlg.SetTitle( _( "Configure Default Remote" ) );
1986
1987 if( dlg.ShowModal() != wxID_OK )
1988 return;
1989
1990 GIT_INIT_HANDLER initHandler( common );
1991
1992 RemoteConfig remoteConfig;
1993 remoteConfig.url = dlg.GetRepoURL();
1994 remoteConfig.username = dlg.GetUsername();
1995 remoteConfig.password = dlg.GetPassword();
1996 remoteConfig.sshKey = dlg.GetRepoSSHPath();
1997 remoteConfig.connType = dlg.GetRepoType();
1998
1999 if( !initHandler.SetupRemote( remoteConfig ) )
2000 {
2001 DisplayErrorMessage( topLevelParent, _( "Failed to configure remote." ), initHandler.GetErrorString() );
2002 return;
2003 }
2004
2008
2012 Prj().GetLocalSettings().m_GitRepoType = "https";
2013 else
2014 Prj().GetLocalSettings().m_GitRepoType = "local";
2015
2016 common->UpdateCurrentBranchInfo();
2017 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
2018}
2019
2020
2021void PROJECT_TREE_PANE::onGitCompare( wxCommandEvent& aEvent )
2022{
2023
2024}
2025
2026
2027void PROJECT_TREE_PANE::onGitPullProject( wxCommandEvent& aEvent )
2028{
2029 git_repository* repo = m_TreeProject->GetGitRepo();
2030
2031 if( !repo )
2032 return;
2033
2034 KIGIT_COMMON* common = m_TreeProject->GitCommon();
2035
2036 while( true )
2037 {
2038 common->ClearAuthFailure();
2039
2040 GIT_PULL_HANDLER handler( common );
2041 handler.SetProgressReporter(
2042 std::make_unique<WX_PROGRESS_REPORTER>( this, _( "Fetch Remote" ), 1, PR_NO_ABORT ) );
2043
2044 PullResult pullResult = handler.PerformPull();
2045
2046 if( pullResult >= PullResult::Success )
2047 {
2048 wxString upstream = common->GetUpstreamShorthand();
2049 wxString branch = common->GetCurrentBranchName();
2050
2051 switch( pullResult )
2052 {
2054 showGitFeedback( upstream.empty() ? _( "Already up to date." )
2055 : wxString::Format( _( "Already up to date with %s." ), upstream ) );
2056 break;
2057
2060 upstream.empty() || branch.empty()
2061 ? _( "Pulled changes (fast-forward)." )
2062 : wxString::Format( _( "Pulled %s into %s (fast-forward)." ), upstream, branch ) );
2063 break;
2064
2065 default:
2066 showGitFeedback( upstream.empty() || branch.empty()
2067 ? _( "Pulled changes." )
2068 : wxString::Format( _( "Pulled %s into %s." ), upstream, branch ) );
2069 break;
2070 }
2071
2072 break;
2073 }
2074
2075 if( pullResult == PullResult::Conflict )
2076 {
2077 wxRichMessageDialog dlg( wxGetTopLevelParent( this ),
2078 _( "Your local changes and the remote changes conflict, so the pull was "
2079 "cancelled and nothing was changed.\n\n"
2080 "You can discard your local commits and use the remote version, or "
2081 "rebase your commits on top of the remote." ),
2082 _( "Pull Conflict" ),
2083 wxYES_NO | wxCANCEL | wxCANCEL_DEFAULT | wxICON_WARNING | wxCENTER );
2084
2085 dlg.SetYesNoCancelLabels( _( "&Use Remote (discard my changes)" ), _( "&Rebase" ), _( "Cancel" ) );
2086
2087 int choice = dlg.ShowModal();
2088
2089 if( choice == wxID_YES )
2090 {
2091 if( handler.ResetToUpstream() )
2092 showGitFeedback( _( "Reset to the remote version." ) );
2093 else
2094 DisplayErrorMessage( m_parent, _( "Failed to reset to remote" ), handler.GetErrorString() );
2095 }
2096 else if( choice == wxID_NO )
2097 {
2098 PullResult rebaseResult = handler.RebaseOntoUpstream();
2099
2100 if( rebaseResult == PullResult::Success )
2101 showGitFeedback( _( "Rebased your changes onto the remote." ) );
2102 else
2103 DisplayErrorMessage( m_parent, _( "Could not rebase" ), handler.GetErrorString() );
2104 }
2105 else
2106 {
2107 showGitFeedback( _( "Pull cancelled." ) );
2108 }
2109
2110 break;
2111 }
2112
2113 if( common->WasAuthFailure() && promptForGitCredentials( m_parent, common ) )
2114 continue;
2115
2116 showGitFeedback( _( "Pull failed." ) );
2117 DisplayErrorMessage( m_parent, _( "Failed to pull project" ), handler.GetErrorString() );
2118 break;
2119 }
2120
2121 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
2122}
2123
2124
2125void PROJECT_TREE_PANE::onGitPushProject( wxCommandEvent& aEvent )
2126{
2127 git_repository* repo = m_TreeProject->GetGitRepo();
2128
2129 if( !repo )
2130 return;
2131
2132 KIGIT_COMMON* common = m_TreeProject->GitCommon();
2133 bool force = false;
2134
2135 while( true )
2136 {
2137 common->ClearAuthFailure();
2138
2139 GIT_PUSH_HANDLER handler( common );
2140 handler.SetProgressReporter(
2141 std::make_unique<WX_PROGRESS_REPORTER>( this, _( "Fetch Remote" ), 1, PR_NO_ABORT ) );
2142
2143 PushResult pushResult = handler.PerformPush( force );
2144
2145 if( pushResult == PushResult::Success )
2146 {
2147 wxString upstream = common->GetUpstreamShorthand();
2148 wxString branch = common->GetCurrentBranchName();
2149
2150 showGitFeedback( upstream.empty() || branch.empty()
2151 ? _( "Pushed to remote." )
2152 : wxString::Format( _( "Pushed %s to %s." ), branch, upstream ) );
2153 break;
2154 }
2155
2156 if( common->WasAuthFailure() && promptForGitCredentials( m_parent, common ) )
2157 continue;
2158
2159 if( pushResult == PushResult::NonFastForward && !force )
2160 {
2161 wxString upstream = common->GetUpstreamShorthand();
2162
2163 wxRichMessageDialog dlg(
2164 wxGetTopLevelParent( this ),
2165 wxString::Format( _( "Push rejected: your local branch and %s have diverged.\n\n"
2166 "This usually happens after amending or rewriting a commit "
2167 "that was already pushed. Force push to overwrite the remote?\n\n"
2168 "Anyone who already pulled the previous version will need to "
2169 "reset their copy." ),
2170 upstream.empty() ? wxString( "the remote" ) : upstream ),
2171 _( "Force Push?" ), wxYES_NO | wxNO_DEFAULT | wxICON_WARNING | wxCENTER );
2172
2173 dlg.SetYesNoLabels( _( "&Force Push" ), _( "Cancel" ) );
2174
2175 if( dlg.ShowModal() == wxID_YES )
2176 {
2177 force = true;
2178 continue;
2179 }
2180 }
2181
2182 showGitFeedback( _( "Push failed." ) );
2183 DisplayErrorMessage( m_parent, _( "Failed to push project" ), handler.GetErrorString() );
2184 break;
2185 }
2186
2187 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
2188}
2189
2190
2191void PROJECT_TREE_PANE::onGitSwitchBranch( wxCommandEvent& aEvent )
2192{
2193 if( !m_TreeProject->GetGitRepo() )
2194 return;
2195
2196 GIT_BRANCH_HANDLER branchHandler( m_TreeProject->GitCommon() );
2197 wxString branchName;
2198
2199 if( aEvent.GetId() == ID_GIT_SWITCH_BRANCH )
2200 {
2201 DIALOG_GIT_SWITCH dlg( wxGetTopLevelParent( this ), m_TreeProject->GetGitRepo() );
2202
2203 int retval = dlg.ShowModal();
2204 branchName = dlg.GetBranchName();
2205
2206 if( retval == wxID_ADD )
2207 KIGIT::PROJECT_GIT_UTILS::CreateBranch( m_TreeProject->GetGitRepo(), branchName );
2208 else if( retval != wxID_OK )
2209 return;
2210 }
2211 else
2212 {
2213 std::vector<wxString> branches = m_TreeProject->GitCommon()->GetBranchNames();
2214 int branchIndex = aEvent.GetId() - ID_GIT_SWITCH_BRANCH;
2215
2216 if( branchIndex < 0 || static_cast<size_t>( branchIndex ) >= branches.size() )
2217 return;
2218
2219 branchName = branches[branchIndex];
2220 }
2221
2222 wxLogTrace( traceGit, wxS( "onGitSwitchBranch: Switching to branch '%s'" ), branchName );
2223 if( branchHandler.SwitchToBranch( branchName ) != BranchResult::Success )
2224 {
2225 DisplayError( m_parent, branchHandler.GetErrorString() );
2226 return;
2227 }
2228
2229 m_TreeProject->GitCommon()->UpdateCurrentBranchInfo();
2230 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
2231}
2232
2233
2234void PROJECT_TREE_PANE::onGitRemoveVCS( wxCommandEvent& aEvent )
2235{
2236 PROJECT_LOCAL_SETTINGS& localSettings = Prj().GetLocalSettings();
2237
2238 // Toggle the Git integration disabled preference
2239 localSettings.m_GitIntegrationDisabled = !localSettings.m_GitIntegrationDisabled;
2240
2241 wxLogTrace( traceGit, wxS( "onGitRemoveVCS: Git integration %s" ),
2242 localSettings.m_GitIntegrationDisabled ? wxS( "disabled" ) : wxS( "enabled" ) );
2243
2244 if( localSettings.m_GitIntegrationDisabled )
2245 {
2246 // Disabling Git integration - clear the repo reference and item states
2247 m_TreeProject->SetGitRepo( nullptr );
2248 m_gitIconsInitialized = false;
2249
2250 // Clear all item states to remove git status icons
2251 std::stack<wxTreeItemId> items;
2252 items.push( m_TreeProject->GetRootItem() );
2253
2254 while( !items.empty() )
2255 {
2256 wxTreeItemId current = items.top();
2257 items.pop();
2258
2259 m_TreeProject->SetItemState( current, wxTREE_ITEMSTATE_NONE );
2260
2261 wxTreeItemIdValue cookie;
2262 wxTreeItemId child = m_TreeProject->GetFirstChild( current, cookie );
2263
2264 while( child.IsOk() )
2265 {
2266 items.push( child );
2267 child = m_TreeProject->GetNextChild( current, cookie );
2268 }
2269 }
2270 }
2271 else
2272 {
2273 // Re-enabling Git integration - try to find and connect to the repository
2274 wxFileName fn( Prj().GetProjectPath() );
2275 m_TreeProject->SetGitRepo( KIGIT::PROJECT_GIT_UTILS::GetRepositoryForFile( fn.GetPath().c_str() ) );
2276
2277 if( m_TreeProject->GetGitRepo() )
2278 {
2279 m_TreeProject->GitCommon()->SetCancelled( false );
2280
2281 const char* canonicalWorkDir = git_repository_workdir( m_TreeProject->GetGitRepo() );
2282
2283 if( canonicalWorkDir )
2284 {
2286 fn.GetPath(), wxString::FromUTF8( canonicalWorkDir ) );
2287 m_TreeProject->GitCommon()->SetProjectDir( symlinkWorkDir );
2288 }
2289
2290 m_TreeProject->GitCommon()->SetUsername( localSettings.m_GitRepoUsername );
2291 m_TreeProject->GitCommon()->SetSSHKey( localSettings.m_GitSSHKey );
2292 }
2293 }
2294
2295 // Save the preference to the project local settings file
2296 localSettings.SaveToFile( Prj().GetProjectPath() );
2297}
2298
2299
2301{
2302 wxLogTrace( traceGit, wxS( "updateGitStatusIcons: Updating git status icons" ) );
2303 std::unique_lock<std::mutex> lock( m_gitStatusMutex, std::try_to_lock );
2304
2305 if( !lock.owns_lock() )
2306 {
2307 wxLogTrace( traceGit, wxS( "updateGitStatusIcons: Failed to acquire lock for git status icon update" ) );
2308 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
2309 return;
2310 }
2311
2312 if( !Pgm().GetCommonSettings()->m_Git.enableGit || !m_TreeProject )
2313 {
2314 wxLogTrace( traceGit, wxS( "updateGitStatusIcons: Git is disabled or tree control is null" ) );
2315 return;
2316 }
2317
2318 std::stack<wxTreeItemId> items;
2319 items.push(m_TreeProject->GetRootItem());
2320
2321 while( !items.empty() )
2322 {
2323 wxTreeItemId current = items.top();
2324 items.pop();
2325
2326 if( m_TreeProject->ItemHasChildren( current ) )
2327 {
2328 wxTreeItemIdValue cookie;
2329 wxTreeItemId child = m_TreeProject->GetFirstChild( current, cookie );
2330
2331 while( child.IsOk() )
2332 {
2333 items.push( child );
2334
2335 if( auto it = m_gitStatusIcons.find( child ); it != m_gitStatusIcons.end() )
2336 {
2337 m_TreeProject->SetItemState( child, static_cast<int>( it->second ) );
2338 }
2339
2340 child = m_TreeProject->GetNextChild( current, cookie );
2341 }
2342 }
2343 }
2344
2345 if( !m_gitCurrentBranchName.empty() )
2346 {
2347 wxTreeItemId kid = m_TreeProject->GetRootItem();
2348 PROJECT_TREE_ITEM* rootItem = kid.IsOk() ? GetItemIdData( kid ) : nullptr;
2349
2350 if( rootItem )
2351 {
2352 wxString filename = wxFileNameFromPath( rootItem->GetFileName() );
2353 m_TreeProject->SetItemText( kid, filename + " [" + m_gitCurrentBranchName + "]" );
2354 m_gitIconsInitialized = true;
2355 }
2356 }
2357
2358 wxLogTrace( traceGit, wxS( "updateGitStatusIcons: Git status icons updated" ) );
2359}
2360
2361
2363{
2364 wxLogTrace( traceGit, wxS( "updateTreeCache: Updating tree cache" ) );
2365
2366 std::unique_lock<std::mutex> lock( m_gitTreeCacheMutex, std::try_to_lock );
2367
2368 if( !lock.owns_lock() )
2369 {
2370 wxLogTrace( traceGit, wxS( "updateTreeCache: Failed to acquire lock for tree cache update" ) );
2371 return;
2372 }
2373
2374 if( !m_TreeProject )
2375 {
2376 wxLogTrace( traceGit, wxS( "updateTreeCache: Tree control is null" ) );
2377 return;
2378 }
2379
2380 wxTreeItemId kid = m_TreeProject->GetRootItem();
2381
2382 if( !kid.IsOk() )
2383 return;
2384
2385 // Collect a map to easily set the state of each item
2386 m_gitTreeCache.clear();
2387 std::stack<wxTreeItemId> items;
2388 items.push( kid );
2389
2390 while( !items.empty() )
2391 {
2392 kid = items.top();
2393 items.pop();
2394
2395 PROJECT_TREE_ITEM* nextItem = GetItemIdData( kid );
2396
2397 if( !nextItem )
2398 continue;
2399
2400 wxString gitAbsPath = nextItem->GetFileName();
2401#ifdef _WIN32
2402 gitAbsPath.Replace( wxS( "\\" ), wxS( "/" ) );
2403#endif
2404 m_gitTreeCache[gitAbsPath] = kid;
2405
2406 wxTreeItemIdValue cookie;
2407 wxTreeItemId child = m_TreeProject->GetFirstChild( kid, cookie );
2408
2409 while( child.IsOk() )
2410 {
2411 items.push( child );
2412 child = m_TreeProject->GetNextChild( kid, cookie );
2413 }
2414 }
2415}
2416
2417
2419{
2420 wxLogTrace( traceGit, wxS( "updateGitStatusIconMap: Updating git status icons" ) );
2421#if defined( _WIN32 )
2423
2424 if( refresh != 0
2425 && KIPLATFORM::ENV::IsNetworkPath( wxPathOnly( m_Parent->GetProjectFileName() ) ) )
2426 {
2427 // Need to treat windows network paths special here until we get the samba bug fixed
2428 // https://github.com/wxWidgets/wxWidgets/issues/18953
2429 CallAfter(
2430 [this, refresh]()
2431 {
2432 m_gitStatusTimer.Start( refresh, wxTIMER_ONE_SHOT );
2433 } );
2434 }
2435
2436#endif
2437
2438 if( !Pgm().GetCommonSettings()->m_Git.enableGit || !m_TreeProject )
2439 return;
2440
2441 std::unique_lock<std::mutex> lock1( m_gitStatusMutex, std::try_to_lock );
2442 std::unique_lock<std::mutex> lock2( m_gitTreeCacheMutex, std::try_to_lock );
2443
2444 if( !lock1.owns_lock() || !lock2.owns_lock() )
2445 {
2446 wxLogTrace( traceGit, wxS( "updateGitStatusIconMap: Failed to acquire locks for git status icon update" ) );
2447 return;
2448 }
2449
2450 if( !m_TreeProject->GetGitRepo() )
2451 {
2452 wxLogTrace( traceGit, wxS( "updateGitStatusIconMap: No git repository found" ) );
2453 return;
2454 }
2455
2456 // Acquire the git action mutex to synchronize with EmptyTreePrj() shutdown.
2457 // This ensures the repository isn't freed while we're using it.
2458 std::unique_lock<std::mutex> gitLock( m_TreeProject->GitCommon()->m_gitActionMutex, std::try_to_lock );
2459
2460 if( !gitLock.owns_lock() )
2461 {
2462 wxLogTrace( traceGit, wxS( "updateGitStatusIconMap: Failed to acquire git action mutex" ) );
2463 return;
2464 }
2465
2466 // Check if cancellation was requested (e.g., during shutdown)
2467 if( m_TreeProject->GitCommon()->IsCancelled() )
2468 {
2469 wxLogTrace( traceGit, wxS( "updateGitStatusIconMap: Cancelled" ) );
2470 return;
2471 }
2472
2473 GIT_STATUS_HANDLER statusHandler( m_TreeProject->GitCommon() );
2474
2475 // Set up pathspec for project files
2476 wxFileName rootFilename( Prj().GetProjectFullName() );
2477 wxString repoWorkDir = statusHandler.GetWorkingDirectory();
2478
2479 wxFileName relative = rootFilename;
2480 relative.MakeRelativeTo( repoWorkDir );
2481 wxString pathspecStr = relative.GetPath( wxPATH_GET_VOLUME | wxPATH_GET_SEPARATOR );
2482
2483#ifdef _WIN32
2484 pathspecStr.Replace( wxS( "\\" ), wxS( "/" ) );
2485#endif
2486
2487 // Get file status
2488 auto fileStatusMap = statusHandler.GetFileStatus( pathspecStr );
2489 auto [localChanges, remoteChanges] = m_TreeProject->GitCommon()->GetDifferentFiles();
2490 statusHandler.UpdateRemoteStatus( localChanges, remoteChanges, fileStatusMap );
2491
2492 bool updated = false;
2493
2494 // Update status icons based on file status
2495 for( const auto& [absPath, fileStatus] : fileStatusMap )
2496 {
2497 auto iter = m_gitTreeCache.find( absPath );
2498 if( iter == m_gitTreeCache.end() )
2499 {
2500 wxLogTrace( traceGit, wxS( "File '%s' not found in tree cache" ), absPath );
2501 continue;
2502 }
2503
2504 auto [it, inserted] = m_gitStatusIcons.try_emplace( iter->second, fileStatus.status );
2505 if( inserted || it->second != fileStatus.status )
2506 updated = true;
2507 it->second = fileStatus.status;
2508 }
2509
2510 // Get the current branch name
2511 wxString branchName = statusHandler.GetCurrentBranchName();
2512
2513 if( branchName != m_gitCurrentBranchName )
2514 updated = true;
2515
2516 m_gitCurrentBranchName = branchName;
2517 m_gitCurrentUpstream = m_TreeProject->GitCommon()->GetUpstreamShorthand();
2518
2519 wxLogTrace( traceGit, wxS( "updateGitStatusIconMap: Updated git status icons" ) );
2520
2521 // Update UI if icons changed
2522 if( updated || !m_gitIconsInitialized )
2523 {
2524 CallAfter(
2525 [this]()
2526 {
2528 } );
2529 }
2530}
2531
2532
2533void PROJECT_TREE_PANE::onGitCommit( wxCommandEvent& aEvent )
2534{
2535 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
2536
2537 git_repository* repo = m_TreeProject->GetGitRepo();
2538
2539 if( repo == nullptr )
2540 {
2541 wxMessageBox( _( "The selected directory is not a Git project." ) );
2542 return;
2543 }
2544
2545 // Flush in-memory editor and project state so the index snapshots the user's
2546 // actual work instead of a stale on-disk version.
2547 {
2548 struct DirtyEditor
2549 {
2550 FRAME_T frameType;
2551 MAIL_T saveMail;
2552 wxString label;
2553 };
2554
2555 const DirtyEditor candidates[] = {
2556 { FRAME_SCH, MAIL_SCH_SAVE, _( "Schematic Editor" ) },
2557 { FRAME_PCB_EDITOR, MAIL_PCB_SAVE, _( "PCB Editor" ) },
2558 };
2559
2560 std::vector<const DirtyEditor*> dirty;
2561
2562 for( const DirtyEditor& d : candidates )
2563 {
2564 KIWAY_PLAYER* frame = m_Parent->Kiway().Player( d.frameType, false );
2565
2566 if( frame && frame->IsContentModified() )
2567 dirty.push_back( &d );
2568 }
2569
2570 if( !dirty.empty() )
2571 {
2572 wxString listed;
2573
2574 for( const DirtyEditor* d : dirty )
2575 listed += wxString::Format( wxS( "\n • %s" ), d->label );
2576
2577 wxRichMessageDialog dlg( wxGetTopLevelParent( this ),
2578 wxString::Format( _( "The following editors have unsaved changes:%s\n\n"
2579 "Save them before committing?" ),
2580 listed ),
2581 _( "Unsaved Changes" ),
2582 wxYES_NO | wxCANCEL | wxYES_DEFAULT | wxICON_WARNING | wxCENTER );
2583
2584 dlg.SetYesNoCancelLabels( _( "&Save and Commit" ), _( "Commit &Anyway" ), _( "Cancel" ) );
2585
2586 int answer = dlg.ShowModal();
2587
2588 if( answer == wxID_CANCEL )
2589 return;
2590
2591 if( answer == wxID_YES )
2592 {
2593 for( const DirtyEditor* d : dirty )
2594 {
2595 std::string payload;
2596 m_Parent->Kiway().ExpressMail( d->frameType, d->saveMail, payload );
2597
2598 if( payload != "success" )
2599 {
2600 DisplayErrorMessage( wxGetTopLevelParent( this ),
2601 wxString::Format( _( "Could not save %s." ), d->label ) );
2602 return;
2603 }
2604 }
2605 }
2606 }
2607
2608 m_Parent->GetSettingsManager()->SaveProject();
2609 }
2610
2611 // Get git configuration
2612 GIT_CONFIG_HANDLER configHandler( m_TreeProject->GitCommon() );
2613 GitUserConfig userConfig = configHandler.GetUserConfig();
2614
2615 // Collect modified files in the repository
2616 GIT_STATUS_HANDLER statusHandler( m_TreeProject->GitCommon() );
2617 auto fileStatusMap = statusHandler.GetFileStatus();
2618
2619 std::map<wxString, int> modifiedFiles;
2620 std::set<wxString> selected_files;
2621
2622 for( PROJECT_TREE_ITEM* item : tree_data )
2623 {
2624 if( item->GetType() == TREE_FILE_TYPE::DIRECTORY )
2625 continue;
2626
2627 wxString itemPath = item->GetFileName();
2628#ifdef _WIN32
2629 itemPath.Replace( wxS( "\\" ), wxS( "/" ) );
2630#endif
2631 selected_files.emplace( itemPath );
2632 }
2633
2634 wxString repoWorkDir = statusHandler.GetWorkingDirectory();
2635
2636 wxString projectPath = Prj().GetProjectPath();
2637#ifdef _WIN32
2638 projectPath.Replace( wxS( "\\" ), wxS( "/" ) );
2639#endif
2640
2641 for( const auto& [absPath, fileStatus] : fileStatusMap )
2642 {
2643 // Skip current, conflicted, or ignored files
2644 if( fileStatus.status == KIGIT_COMMON::GIT_STATUS::GIT_STATUS_CURRENT
2646 || fileStatus.status == KIGIT_COMMON::GIT_STATUS::GIT_STATUS_IGNORED )
2647 {
2648 continue;
2649 }
2650
2651 wxFileName fn( absPath );
2652
2653 // Convert to relative path for the modifiedFiles map
2654 wxString relativePath = absPath;
2655 if( relativePath.StartsWith( repoWorkDir ) )
2656 {
2657 relativePath = relativePath.Mid( repoWorkDir.length() );
2658#ifdef _WIN32
2659 relativePath.Replace( wxS( "\\" ), wxS( "/" ) );
2660#endif
2661 }
2662
2663 // Do not commit files outside the project directory
2664 if( !absPath.StartsWith( projectPath ) )
2665 continue;
2666
2667 // Skip lock files
2668 if( fn.GetExt().CmpNoCase( FILEEXT::LockFileExtension ) == 0 )
2669 continue;
2670
2671 // Skip autosave, lock, and backup files
2672 if( fn.GetName().StartsWith( FILEEXT::LockFilePrefix )
2673 || fn.GetName().EndsWith( FILEEXT::BackupFileSuffix ) )
2674 {
2675 continue;
2676 }
2677
2678 // Skip archived project backups
2679 if( fn.GetPath().Contains( Prj().GetProjectName() + wxT( "-backups" ) ) )
2680 continue;
2681
2682 // Skip local project history (which is also a submodule, we don't support for the time)
2683 if( fn.IsDir() && fn.GetDirs().Last().StartsWith( wxS( ".history" ) ) )
2684 {
2685 continue;
2686 }
2687
2688 if( aEvent.GetId() == ID_GIT_COMMIT_PROJECT )
2689 {
2690 // Don't add an untracked project-local settings file to the repo on its own
2691 // Only commit it if it's already tracked, or if the user selects it explicitly.
2692 if( fn.GetExt().CmpNoCase( FILEEXT::ProjectLocalSettingsFileExtension ) == 0
2693 && fileStatus.status == KIGIT_COMMON::GIT_STATUS::GIT_STATUS_UNTRACKED )
2694 {
2695 continue;
2696 }
2697
2698 modifiedFiles.emplace( relativePath, fileStatus.gitStatus );
2699 }
2700 else if( selected_files.count( absPath ) )
2701 {
2702 modifiedFiles.emplace( relativePath, fileStatus.gitStatus );
2703 }
2704 }
2705
2706 // Create a commit dialog
2707 DIALOG_GIT_COMMIT dlg( wxGetTopLevelParent( this ), repo, userConfig.authorName, userConfig.authorEmail,
2708 modifiedFiles );
2709 auto ret = dlg.ShowModal();
2710
2711 if( ret != wxID_OK )
2712 return;
2713
2714 std::vector<wxString> files = dlg.GetSelectedFiles();
2715
2716 if( dlg.GetCommitMessage().IsEmpty() )
2717 {
2718 wxMessageBox( _( "Discarding commit due to empty commit message." ) );
2719 return;
2720 }
2721
2722 if( files.empty() )
2723 {
2724 wxMessageBox( _( "Discarding commit due to empty file selection." ) );
2725 return;
2726 }
2727
2728 GIT_COMMIT_HANDLER commitHandler( repo );
2729 auto result = commitHandler.PerformCommit( files, dlg.GetCommitMessage(),
2730 dlg.GetAuthorName(), dlg.GetAuthorEmail() );
2731
2733 {
2734 wxMessageBox( wxString::Format( _( "Failed to create commit: %s" ),
2735 commitHandler.GetErrorString() ) );
2736 return;
2737 }
2738
2739 wxLogTrace( traceGit, wxS( "Created commit" ) );
2740 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
2741}
2742
2743
2744void PROJECT_TREE_PANE::onGitAmendCommit( wxCommandEvent& aEvent )
2745{
2746 git_repository* repo = m_TreeProject->GetGitRepo();
2747
2748 if( repo == nullptr )
2749 {
2750 wxMessageBox( _( "The selected directory is not a Git project." ) );
2751 return;
2752 }
2753
2754 wxString reopenPath = Prj().GetProjectPath();
2755
2756 if( git_repository* fresh = KIGIT::PROJECT_GIT_UTILS::GetRepositoryForFile( reopenPath.c_str() ) )
2757 {
2758 m_TreeProject->SetGitRepo( fresh );
2759 git_repository_free( repo );
2760 repo = fresh;
2761 }
2762
2763 if( git_repository_head_unborn( repo ) != 0 )
2764 {
2765 DisplayErrorMessage( wxGetTopLevelParent( this ), _( "Cannot amend: the branch has no commits yet." ) );
2766 return;
2767 }
2768
2769 // Look up HEAD commit so we can pre-fill the message and check whether HEAD has
2770 // already been published to the upstream branch.
2771 git_reference* headRef = nullptr;
2772
2773 if( git_repository_head( &headRef, repo ) != GIT_OK )
2774 return;
2775
2776 KIGIT::GitReferencePtr headRefPtr( headRef );
2777 git_commit* headCommit = nullptr;
2778
2779 if( git_reference_peel( (git_object**) &headCommit, headRef, GIT_OBJECT_COMMIT ) != GIT_OK )
2780 return;
2781
2782 KIGIT::GitCommitPtr headCommitPtr( headCommit );
2783 wxString headMessage = wxString::FromUTF8( git_commit_message( headCommit ) );
2784 const git_oid* headOid = git_commit_id( headCommit );
2785
2786 // Detect whether HEAD has already been pushed: head is published when it is the
2787 // upstream OID or an ancestor of it.
2788 bool headIsPublished = false;
2789 git_reference* upstreamRef = nullptr;
2790
2791 if( git_branch_upstream( &upstreamRef, headRef ) == GIT_OK )
2792 {
2793 KIGIT::GitReferencePtr upstreamRefPtr( upstreamRef );
2794 git_oid upstreamOid;
2795
2796 if( git_reference_name_to_id( &upstreamOid, repo, git_reference_name( upstreamRef ) ) == GIT_OK )
2797 {
2798 headIsPublished = git_oid_equal( headOid, &upstreamOid )
2799 || git_graph_descendant_of( repo, &upstreamOid, headOid ) == 1;
2800 }
2801 }
2802
2803 if( headIsPublished )
2804 {
2805 wxString upstreamName = m_TreeProject->GitCommon()->GetUpstreamShorthand();
2806
2807 wxRichMessageDialog dlg( wxGetTopLevelParent( this ),
2808 wxString::Format( _( "The last commit has already been pushed to %s.\n\n"
2809 "Amending rewrites public history. Anyone who pulled "
2810 "this commit will need to reset their copy." ),
2811 upstreamName ),
2812 _( "Amend Published Commit?" ), wxYES_NO | wxNO_DEFAULT | wxICON_WARNING | wxCENTER );
2813
2814 dlg.SetYesNoLabels( _( "Amend &Anyway" ), _( "Cancel" ) );
2815
2816 if( dlg.ShowModal() != wxID_YES )
2817 return;
2818 }
2819
2820 // Flush in-memory editor and project state so the amend captures the user's actual
2821 // work instead of a stale on-disk version.
2822 {
2823 struct DirtyEditor
2824 {
2825 FRAME_T frameType;
2826 MAIL_T saveMail;
2827 wxString label;
2828 };
2829
2830 const DirtyEditor candidates[] = {
2831 { FRAME_SCH, MAIL_SCH_SAVE, _( "Schematic Editor" ) },
2832 { FRAME_PCB_EDITOR, MAIL_PCB_SAVE, _( "PCB Editor" ) },
2833 };
2834
2835 std::vector<const DirtyEditor*> dirty;
2836
2837 for( const DirtyEditor& d : candidates )
2838 {
2839 KIWAY_PLAYER* frame = m_Parent->Kiway().Player( d.frameType, false );
2840
2841 if( frame && frame->IsContentModified() )
2842 dirty.push_back( &d );
2843 }
2844
2845 if( !dirty.empty() )
2846 {
2847 wxString listed;
2848
2849 for( const DirtyEditor* d : dirty )
2850 listed += wxString::Format( wxS( "\n • %s" ), d->label );
2851
2852 wxRichMessageDialog dlg( wxGetTopLevelParent( this ),
2853 wxString::Format( _( "The following editors have unsaved changes:%s\n\n"
2854 "Save them before amending?" ),
2855 listed ),
2856 _( "Unsaved Changes" ),
2857 wxYES_NO | wxCANCEL | wxYES_DEFAULT | wxICON_WARNING | wxCENTER );
2858
2859 dlg.SetYesNoCancelLabels( _( "&Save and Amend" ), _( "Amend &Anyway" ), _( "Cancel" ) );
2860
2861 int answer = dlg.ShowModal();
2862
2863 if( answer == wxID_CANCEL )
2864 return;
2865
2866 if( answer == wxID_YES )
2867 {
2868 for( const DirtyEditor* d : dirty )
2869 {
2870 std::string payload;
2871 m_Parent->Kiway().ExpressMail( d->frameType, d->saveMail, payload );
2872
2873 if( payload != "success" )
2874 {
2875 DisplayErrorMessage( wxGetTopLevelParent( this ),
2876 wxString::Format( _( "Could not save %s." ), d->label ) );
2877 return;
2878 }
2879 }
2880 }
2881 }
2882
2883 m_Parent->GetSettingsManager()->SaveProject();
2884 }
2885
2886 // Collect modified files for the dialog's checklist.
2887 GIT_CONFIG_HANDLER configHandler( m_TreeProject->GitCommon() );
2888 GitUserConfig userConfig = configHandler.GetUserConfig();
2889
2890 GIT_STATUS_HANDLER statusHandler( m_TreeProject->GitCommon() );
2891 auto fileStatusMap = statusHandler.GetFileStatus();
2892 wxString repoWorkDir = statusHandler.GetWorkingDirectory();
2893 wxString projectPath = Prj().GetProjectPath();
2894
2895#ifdef _WIN32
2896 projectPath.Replace( wxS( "\\" ), wxS( "/" ) );
2897#endif
2898
2899 std::map<wxString, int> modifiedFiles;
2900
2901 for( const auto& [absPath, fileStatus] : fileStatusMap )
2902 {
2903 if( fileStatus.status == KIGIT_COMMON::GIT_STATUS::GIT_STATUS_CURRENT
2905 || fileStatus.status == KIGIT_COMMON::GIT_STATUS::GIT_STATUS_IGNORED )
2906 {
2907 continue;
2908 }
2909
2910 wxFileName fn( absPath );
2911 wxString relativePath = absPath;
2912
2913 if( relativePath.StartsWith( repoWorkDir ) )
2914 {
2915 relativePath = relativePath.Mid( repoWorkDir.length() );
2916#ifdef _WIN32
2917 relativePath.Replace( wxS( "\\" ), wxS( "/" ) );
2918#endif
2919 }
2920
2921 if( !absPath.StartsWith( projectPath ) )
2922 continue;
2923
2924 if( fn.GetExt().CmpNoCase( FILEEXT::LockFileExtension ) == 0 )
2925 continue;
2926
2927 if( fn.GetName().StartsWith( FILEEXT::LockFilePrefix ) || fn.GetName().EndsWith( FILEEXT::BackupFileSuffix ) )
2928 {
2929 continue;
2930 }
2931
2932 if( fn.GetPath().Contains( Prj().GetProjectName() + wxT( "-backups" ) ) )
2933 continue;
2934
2935 modifiedFiles.emplace( relativePath, fileStatus.gitStatus );
2936 }
2937
2938 if( git_reference* headRefForDiff = nullptr; git_repository_head( &headRefForDiff, repo ) == GIT_OK )
2939 {
2940 KIGIT::GitReferencePtr headRefForDiffPtr( headRefForDiff );
2941 git_commit* lastCommit = nullptr;
2942
2943 if( git_reference_peel( (git_object**) &lastCommit, headRefForDiff, GIT_OBJECT_COMMIT ) == GIT_OK )
2944 {
2945 KIGIT::GitCommitPtr lastCommitPtr( lastCommit );
2946 git_commit* parentCommit = nullptr;
2947 git_tree* parentTree = nullptr;
2948 git_tree* lastTree = nullptr;
2949
2950 if( git_commit_parentcount( lastCommit ) > 0
2951 && git_commit_parent( &parentCommit, lastCommit, 0 ) == GIT_OK )
2952 {
2953 git_commit_tree( &parentTree, parentCommit );
2954 }
2955
2956 KIGIT::GitCommitPtr parentCommitPtr( parentCommit );
2957 KIGIT::GitTreePtr parentTreePtr( parentTree );
2958
2959 if( git_commit_tree( &lastTree, lastCommit ) == GIT_OK )
2960 {
2961 KIGIT::GitTreePtr lastTreePtr( lastTree );
2962 git_diff* diff = nullptr;
2963
2964 if( git_diff_tree_to_tree( &diff, repo, parentTree, lastTree, nullptr ) == GIT_OK )
2965 {
2966 KIGIT::GitDiffPtr diffPtr( diff );
2967
2969 [&modifiedFiles]( const git_diff_delta& aDelta )
2970 {
2971 const char* path = aDelta.new_file.path ? aDelta.new_file.path
2972 : aDelta.old_file.path;
2973
2974 if( !path )
2975 return;
2976
2977 int flag = GIT_STATUS_INDEX_MODIFIED;
2978
2979 if( aDelta.status == GIT_DELTA_ADDED )
2980 flag = GIT_STATUS_INDEX_NEW;
2981 else if( aDelta.status == GIT_DELTA_DELETED )
2982 flag = GIT_STATUS_INDEX_DELETED;
2983
2984 modifiedFiles[wxString::FromUTF8( path )] |= flag;
2985 } );
2986 }
2987 }
2988 }
2989 }
2990
2991 DIALOG_GIT_COMMIT dlg( wxGetTopLevelParent( this ), repo, userConfig.authorName, userConfig.authorEmail,
2992 modifiedFiles );
2993 dlg.SetTitle( _( "Amend Last Commit" ) );
2994 dlg.SetCommitMessage( headMessage );
2995
2996 if( dlg.ShowModal() != wxID_OK )
2997 return;
2998
2999 if( dlg.GetCommitMessage().IsEmpty() )
3000 {
3001 wxMessageBox( _( "Discarding amend due to empty commit message." ) );
3002 return;
3003 }
3004
3005 GIT_COMMIT_HANDLER commitHandler( repo );
3007 dlg.GetAuthorName(), dlg.GetAuthorEmail() );
3008
3010 {
3011 wxMessageBox( wxString::Format( _( "Failed to amend commit: %s" ), commitHandler.GetErrorString() ) );
3012 return;
3013 }
3014
3015 wxLogTrace( traceGit, wxS( "Amended commit" ) );
3016 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
3017}
3018
3019
3020void PROJECT_TREE_PANE::onGitAddToIndex( wxCommandEvent& aEvent )
3021{
3022
3023}
3024
3025
3026bool PROJECT_TREE_PANE::canFileBeAddedToVCS( const wxString& aFile )
3027{
3028 if( !m_TreeProject->GetGitRepo() )
3029 return false;
3030
3031 GIT_STATUS_HANDLER statusHandler( m_TreeProject->GitCommon() );
3032 auto fileStatusMap = statusHandler.GetFileStatus();
3033
3034 // Check if file is already tracked or staged
3035 for( const auto& [filePath, fileStatus] : fileStatusMap )
3036 {
3037 if( filePath.EndsWith( aFile ) || filePath == aFile )
3038 {
3039 // File can be added if it's untracked
3040 return fileStatus.status == KIGIT_COMMON::GIT_STATUS::GIT_STATUS_UNTRACKED;
3041 }
3042 }
3043
3044 // If file not found in status, it might be addable
3045 return true;
3046}
3047
3048
3049void PROJECT_TREE_PANE::onGitSyncProject( wxCommandEvent& aEvent )
3050{
3051 wxLogTrace( traceGit, "Syncing project" );
3052 git_repository* repo = m_TreeProject->GetGitRepo();
3053
3054 if( !repo )
3055 {
3056 wxLogTrace( traceGit, "sync: No git repository found" );
3057 return;
3058 }
3059
3060 GIT_SYNC_HANDLER handler( repo );
3061 handler.PerformSync();
3062}
3063
3064
3065void PROJECT_TREE_PANE::onGitFetch( wxCommandEvent& aEvent )
3066{
3067 KIGIT_COMMON* gitCommon = m_TreeProject->GitCommon();
3068
3069 if( !gitCommon )
3070 return;
3071
3072 while( true )
3073 {
3074 gitCommon->ClearAuthFailure();
3075
3076 GIT_PULL_HANDLER handler( gitCommon );
3077
3078 if( handler.PerformFetch() )
3079 {
3080 wxString remoteName = gitCommon->GetRemoteNameOrDefault();
3081
3082 showGitFeedback( remoteName.empty() ? _( "Fetched from remote." )
3083 : wxString::Format( _( "Fetched from %s." ), remoteName ) );
3084 break;
3085 }
3086
3087 if( gitCommon->WasAuthFailure() && promptForGitCredentials( m_parent, gitCommon ) )
3088 continue;
3089
3090 showGitFeedback( _( "Fetch failed." ) );
3091 break;
3092 }
3093
3094 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
3095}
3096
3097
3098void PROJECT_TREE_PANE::onGitResolveConflict( wxCommandEvent& aEvent )
3099{
3100 git_repository* repo = m_TreeProject->GetGitRepo();
3101
3102 if( !repo )
3103 return;
3104
3105 GIT_RESOLVE_CONFLICT_HANDLER handler( repo );
3106 handler.PerformResolveConflict();
3107}
3108
3109
3110void PROJECT_TREE_PANE::onGitRevertLocal( wxCommandEvent& aEvent )
3111{
3112 git_repository* repo = m_TreeProject->GetGitRepo();
3113
3114 if( !repo )
3115 return;
3116
3117 GIT_REVERT_HANDLER handler( repo );
3118 handler.PerformRevert();
3119}
3120
3121
3122void PROJECT_TREE_PANE::onGitRemoveFromIndex( wxCommandEvent& aEvent )
3123{
3124 git_repository* repo = m_TreeProject->GetGitRepo();
3125
3126 if( !repo )
3127 return;
3128
3129 GIT_REMOVE_FROM_INDEX_HANDLER handler( repo );
3130 handler.PerformRemoveFromIndex();
3131}
3132
3133
3135{
3136
3137}
3138
3139
3140void PROJECT_TREE_PANE::onGitSyncTimer( wxTimerEvent& aEvent )
3141{
3142 wxLogTrace( traceGit, "onGitSyncTimer" );
3143 COMMON_SETTINGS::GIT& gitSettings = Pgm().GetCommonSettings()->m_Git;
3144
3145 if( !gitSettings.enableGit || !m_TreeProject )
3146 return;
3147
3149
3150 m_gitSyncTask = tp.submit_task( [this]()
3151 {
3152 KIGIT_COMMON* gitCommon = m_TreeProject->GitCommon();
3153
3154 if( !gitCommon )
3155 {
3156 wxLogTrace( traceGit, "onGitSyncTimer: No git repository found" );
3157 return;
3158 }
3159
3160 // Check if cancellation was requested (e.g., during shutdown)
3161 if( gitCommon->IsCancelled() )
3162 {
3163 wxLogTrace( traceGit, "onGitSyncTimer: Cancelled" );
3164 return;
3165 }
3166
3167 GIT_PULL_HANDLER handler( gitCommon );
3168 handler.PerformFetch();
3169
3170 // Only schedule the follow-up work if not cancelled
3171 if( !gitCommon->IsCancelled() )
3172 CallAfter( [this]() { gitStatusTimerHandler(); } );
3173 } );
3174
3175 if( gitSettings.updatInterval > 0 )
3176 {
3177 wxLogTrace( traceGit, "onGitSyncTimer: Restarting git sync timer" );
3178 // We store the timer interval in minutes but wxTimer uses milliseconds
3179 m_gitSyncTimer.Start( gitSettings.updatInterval * 60 * 1000, wxTIMER_ONE_SHOT );
3180 }
3181}
3182
3183
3185{
3186 // Check if git is still available and not cancelled before spawning background work
3187 KIGIT_COMMON* gitCommon = m_TreeProject ? m_TreeProject->GitCommon() : nullptr;
3188
3189 if( !gitCommon || gitCommon->IsCancelled() )
3190 return;
3191
3194
3195 m_gitStatusIconTask = tp.submit_task( [this]() { updateGitStatusIconMap(); } );
3196}
3197
3198void PROJECT_TREE_PANE::onGitStatusTimer( wxTimerEvent& aEvent )
3199{
3200 wxLogTrace( traceGit, "onGitStatusTimer" );
3201
3202 if( !Pgm().GetCommonSettings()->m_Git.enableGit || !m_TreeProject )
3203 return;
3204
3206}
3207
3208
3209void PROJECT_TREE_PANE::showGitFeedback( const wxString& aText )
3210{
3211 if( KISTATUSBAR* sb = dynamic_cast<KISTATUSBAR*>( m_Parent->GetStatusBar() ) )
3212 {
3213 sb->SetEllipsedTextField( aText, 0 );
3214 m_gitFeedbackTimer.Start( 8000, wxTIMER_ONE_SHOT );
3215 }
3216}
3217
3218
3219void PROJECT_TREE_PANE::onGitFeedbackTimer( wxTimerEvent& aEvent )
3220{
3221 if( m_Parent )
3222 m_Parent->PrintPrjInfo();
3223}
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.
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 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:528
virtual const wxString & GetTextEditor(bool aCanShowFileChooser=true)
Return the path to the preferred text editor application.
Definition pgm_base.cpp:218
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 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:206
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:160
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:730
@ 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:40
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