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