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