KiCad PCB EDA Suite
Loading...
Searching...
No Matches
project_tree_pane.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright (C) 2012 SoftPLC Corporation, Dick Hollenbeck <[email protected]>
5 * Copyright (C) 2012 Jean-Pierre Charras, jp.charras at wanadoo.fr
6 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
7 *
8 * This program is free software; you can redistribute it and/or
9 * modify it under the terms of the GNU General Public License
10 * as published by the Free Software Foundation; either version 2
11 * of the License, or (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, you may find one here:
20 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
21 * or you may search the http://www.gnu.org website for the version 2 license,
22 * or you may write to the Free Software Foundation, Inc.,
23 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
24 */
25
26#include <stack>
27#include <thread>
28#include <git2.h>
29
30#include <wx/regex.h>
31#include <wx/stdpaths.h>
32#include <wx/string.h>
33#include <wx/msgdlg.h>
34#include <wx/textdlg.h>
35#include <wx/timer.h>
36
37#include <advanced_config.h>
38#include <bitmaps.h>
39#include <bitmap_store.h>
40#include <confirm.h>
43#include <gestfich.h>
44#include <macros.h>
45#include <trace_helpers.h>
48#include <core/kicad_algo.h>
49#include <paths.h>
51#include <scoped_set_reset.h>
52#include <string_utils.h>
53#include <thread_pool.h>
54#include <launch_ext.h>
55#include <wx/dcclient.h>
56#include <wx/progdlg.h>
57#include <wx/settings.h>
58
73
75
76#include "project_tree_item.h"
77#include "project_tree.h"
78#include "pgm_kicad.h"
79#include "kicad_id.h"
80#include "kicad_manager_frame.h"
81
82#include "project_tree_pane.h"
83#include <widgets/kistatusbar.h>
84
85#include <kiplatform/io.h>
86#include <kiplatform/secrets.h>
87
88
89/* Note about the project tree build process:
90 * Building the project tree can be *very* long if there are a lot of subdirectories in the
91 * working directory. Unfortunately, this happens easily if the project file *.pro is in the
92 * user's home directory.
93 * So the tree project is built "on demand":
94 * First the tree is built from the current directory and shows files and subdirs.
95 * > First level subdirs trees are built (i.e subdirs contents are not read)
96 * > When expanding a subdir, each subdir contains is read, and the corresponding sub tree is
97 * populated on the fly.
98 */
99
100// list of files extensions listed in the tree project window
101// Add extensions in a compatible regex format to see others files types
102static const wxChar* s_allowedExtensionsToList[] = {
103 wxT( "^.*\\.pro$" ),
104 wxT( "^.*\\.kicad_pro$" ),
105 wxT( "^.*\\.pdf$" ),
106 wxT( "^.*\\.sch$" ), // Legacy Eeschema files
107 wxT( "^.*\\.kicad_sch$" ), // S-expr Eeschema files
108 wxT( "^[^$].*\\.brd$" ), // Legacy Pcbnew files
109 wxT( "^[^$].*\\.kicad_pcb$" ), // S format Pcbnew board files
110 wxT( "^[^$].*\\.kicad_dru$" ), // Design rule files
111 wxT( "^[^$].*\\.kicad_wks$" ), // S format kicad drawing sheet files
112 wxT( "^[^$].*\\.kicad_mod$" ), // S format kicad footprint files, currently not listed
113 wxT( "^.*\\.net$" ), // pcbnew netlist file
114 wxT( "^.*\\.cir$" ), // Spice netlist file
115 wxT( "^.*\\.lib$" ), // Legacy schematic library file
116 wxT( "^.*\\.kicad_sym$" ), // S-expr symbol libraries
117 wxT( "^.*\\.txt$" ), // Text files
118 wxT( "^.*\\.md$" ), // Markdown files
119 wxT( "^.*\\.pho$" ), // Gerber file (Old Kicad extension)
120 wxT( "^.*\\.gbr$" ), // Gerber file
121 wxT( "^.*\\.gbrjob$" ), // Gerber job file
122 wxT( "^.*\\.gb[alops]$" ), // Gerber back (or bottom) layer file (deprecated Protel ext)
123 wxT( "^.*\\.gt[alops]$" ), // Gerber front (or top) layer file (deprecated Protel ext)
124 wxT( "^.*\\.g[0-9]{1,2}$" ), // Gerber inner layer file (deprecated Protel ext)
125 wxT( "^.*\\.gm[0-9]{1,2}$" ), // Gerber mechanical layer file (deprecated Protel ext)
126 wxT( "^.*\\.gko$" ), // Gerber keepout layer file (deprecated Protel ext)
127 wxT( "^.*\\.odt$" ),
128 wxT( "^.*\\.htm$" ),
129 wxT( "^.*\\.html$" ),
130 wxT( "^.*\\.rpt$" ), // Report files
131 wxT( "^.*\\.csv$" ), // Report files in comma separated format
132 wxT( "^.*\\.pos$" ), // Footprint position files
133 wxT( "^.*\\.cmp$" ), // CvPcb cmp/footprint link files
134 wxT( "^.*\\.drl$" ), // Excellon drill files
135 wxT( "^.*\\.nc$" ), // Excellon NC drill files (alternate file ext)
136 wxT( "^.*\\.xnc$" ), // Excellon NC drill files (alternate file ext)
137 wxT( "^.*\\.svg$" ), // SVG print/plot files
138 wxT( "^.*\\.ps$" ), // PostScript plot files
139 wxT( "^.*\\.zip$" ), // Zip archive files
140 wxT( "^.*\\.kicad_jobset" ), // KiCad jobs file
141 nullptr // end of list
142};
143
144
159
160 ID_GIT_INITIALIZE_PROJECT, // Initialize a new git repository in an existing project
161 ID_GIT_CLONE_PROJECT, // Clone a project from a remote repository
162 ID_GIT_COMMIT_PROJECT, // Commit all files in the project
163 ID_GIT_COMMIT_FILE, // Commit a single file
164 ID_GIT_SYNC_PROJECT, // Sync the project with the remote repository (pull and push -- same as Update)
165 ID_GIT_FETCH, // Fetch the remote repository (without merging -- this is the same as Refresh)
166 ID_GIT_PUSH, // Push the local repository to the remote repository
167 ID_GIT_PULL, // Pull the remote repository to the local repository
168 ID_GIT_RESOLVE_CONFLICT, // Present the user with a resolve conflicts dialog (ours/theirs/merge)
169 ID_GIT_REVERT_LOCAL, // Revert the local repository to the last commit
170 ID_GIT_COMPARE, // Compare the current project to a different branch or commit in the git repository
171 ID_GIT_REMOVE_VCS, // Remove the git repository data from the project directory (rm .git)
172 ID_GIT_ADD_TO_INDEX, // Add a file to the git index
173 ID_GIT_REMOVE_FROM_INDEX, // Remove a file from the git index
174 ID_GIT_SWITCH_BRANCH, // Switch the local repository to a different branch
175 ID_GIT_SWITCH_QUICK1, // Switch the local repository to the first quick branch
176 ID_GIT_SWITCH_QUICK2, // Switch the local repository to the second quick branch
177 ID_GIT_SWITCH_QUICK3, // Switch the local repository to the third quick branch
178 ID_GIT_SWITCH_QUICK4, // Switch the local repository to the fourth quick branch
179 ID_GIT_SWITCH_QUICK5, // Switch the local repository to the fifth quick branch
180
182};
183
184
185BEGIN_EVENT_TABLE( PROJECT_TREE_PANE, wxSashLayoutWindow )
186 EVT_TREE_ITEM_ACTIVATED( ID_PROJECT_TREE, PROJECT_TREE_PANE::onSelect )
187 EVT_TREE_ITEM_EXPANDED( ID_PROJECT_TREE, PROJECT_TREE_PANE::onExpand )
188 EVT_TREE_ITEM_RIGHT_CLICK( ID_PROJECT_TREE, PROJECT_TREE_PANE::onRight )
195
211
213
214 EVT_IDLE( PROJECT_TREE_PANE::onIdle )
215 EVT_PAINT( PROJECT_TREE_PANE::onPaint )
216END_EVENT_TABLE()
217
218
219wxDECLARE_EVENT( UPDATE_ICONS, wxCommandEvent );
220
222 wxSashLayoutWindow( parent, ID_LEFT_FRAME, wxDefaultPosition, wxDefaultSize,
223 wxNO_BORDER | wxTAB_TRAVERSAL )
224{
225 m_Parent = parent;
226 m_TreeProject = nullptr;
227 m_isRenaming = false;
228 m_selectedItem = nullptr;
229 m_watcherNeedReset = false;
230 m_gitLastError = GIT_ERROR_NONE;
231 m_watcher = nullptr;
232 m_gitIconsInitialized = false;
233
234 Bind( wxEVT_FSWATCHER,
235 wxFileSystemWatcherEventHandler( PROJECT_TREE_PANE::onFileSystemEvent ), this );
236
237 Bind( wxEVT_SYS_COLOUR_CHANGED,
238 wxSysColourChangedEventHandler( PROJECT_TREE_PANE::onThemeChanged ), this );
239
240 m_gitSyncTimer.SetOwner( this );
241 m_gitStatusTimer.SetOwner( this );
242 Bind( wxEVT_TIMER, wxTimerEventHandler( PROJECT_TREE_PANE::onGitSyncTimer ), this, m_gitSyncTimer.GetId() );
243 Bind( wxEVT_TIMER, wxTimerEventHandler( PROJECT_TREE_PANE::onGitStatusTimer ), this, m_gitStatusTimer.GetId() );
244 /*
245 * Filtering is now inverted: the filters are actually used to _enable_ support
246 * for a given file type.
247 */
248 for( int ii = 0; s_allowedExtensionsToList[ii] != nullptr; ii++ )
249 m_filters.emplace_back( s_allowedExtensionsToList[ii] );
250
251 m_filters.emplace_back( wxT( "^no KiCad files found" ) );
252
253 ReCreateTreePrj();
254}
255
256
258{
259 Unbind( wxEVT_FSWATCHER,
260 wxFileSystemWatcherEventHandler( PROJECT_TREE_PANE::onFileSystemEvent ), this );
261 Unbind( wxEVT_SYS_COLOUR_CHANGED,
262 wxSysColourChangedEventHandler( PROJECT_TREE_PANE::onThemeChanged ), this );
263
264 m_gitSyncTimer.Stop();
265 m_gitStatusTimer.Stop();
266 Unbind( wxEVT_TIMER, wxTimerEventHandler( PROJECT_TREE_PANE::onGitSyncTimer ), this,
267 m_gitSyncTimer.GetId() );
268 Unbind( wxEVT_TIMER, wxTimerEventHandler( PROJECT_TREE_PANE::onGitStatusTimer ), this,
269 m_gitStatusTimer.GetId() );
271}
272
273
275{
276 if( m_watcher )
277 {
278 m_watcher->RemoveAll();
279 m_watcher->SetOwner( nullptr );
280 delete m_watcher;
281 m_watcher = nullptr;
282 }
283}
284
285
287{
288 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
289
290 if( tree_data.size() != 1 )
291 return;
292
293 wxString prj_filename = tree_data[0]->GetFileName();
294
295 m_Parent->LoadProject( prj_filename );
296}
297
298
299void PROJECT_TREE_PANE::onOpenDirectory( wxCommandEvent& event )
300{
301 // Get the root directory name:
302 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
303
304 for( PROJECT_TREE_ITEM* item_data : tree_data )
305 {
306 // Ask for the new sub directory name
307 wxString curr_dir = item_data->GetDir();
308
309 if( curr_dir.IsEmpty() )
310 {
311 // Use project path if the tree view path was empty.
312 curr_dir = wxPathOnly( m_Parent->GetProjectFileName() );
313
314 // As a last resort use the user's documents folder.
315 if( curr_dir.IsEmpty() || !wxFileName::DirExists( curr_dir ) )
317
318 if( !curr_dir.IsEmpty() )
319 curr_dir += wxFileName::GetPathSeparator();
320 }
321
322 LaunchExternal( curr_dir );
323 }
324}
325
326
327void PROJECT_TREE_PANE::onCreateNewDirectory( wxCommandEvent& event )
328{
329 // Get the root directory name:
330 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
331
332 for( PROJECT_TREE_ITEM* item_data : tree_data )
333 {
334 wxString prj_dir = wxPathOnly( m_Parent->GetProjectFileName() );
335
336 // Ask for the new sub directory name
337 wxString curr_dir = item_data->GetDir();
338
339 if( curr_dir.IsEmpty() )
340 curr_dir = prj_dir;
341
342 wxString new_dir = wxGetTextFromUser( _( "Directory name:" ), _( "Create New Directory" ) );
343
344 if( new_dir.IsEmpty() )
345 return;
346
347 wxString full_dirname = curr_dir + wxFileName::GetPathSeparator() + new_dir;
348
349 if( !wxMkdir( full_dirname ) )
350 return;
351
352 addItemToProjectTree( full_dirname, item_data->GetId(), nullptr, false );
353 }
354}
355
356
358{
359 switch( type )
360 {
361 case TREE_FILE_TYPE::LEGACY_PROJECT: return FILEEXT::LegacyProjectFileExtension;
362 case TREE_FILE_TYPE::JSON_PROJECT: return FILEEXT::ProjectFileExtension;
363 case TREE_FILE_TYPE::LEGACY_SCHEMATIC: return FILEEXT::LegacySchematicFileExtension;
364 case TREE_FILE_TYPE::SEXPR_SCHEMATIC: return FILEEXT::KiCadSchematicFileExtension;
365 case TREE_FILE_TYPE::LEGACY_PCB: return FILEEXT::LegacyPcbFileExtension;
366 case TREE_FILE_TYPE::SEXPR_PCB: return FILEEXT::KiCadPcbFileExtension;
367 case TREE_FILE_TYPE::GERBER: return FILEEXT::GerberFileExtensionsRegex;
368 case TREE_FILE_TYPE::GERBER_JOB_FILE: return FILEEXT::GerberJobFileExtension;
369 case TREE_FILE_TYPE::HTML: return FILEEXT::HtmlFileExtension;
370 case TREE_FILE_TYPE::PDF: return FILEEXT::PdfFileExtension;
371 case TREE_FILE_TYPE::TXT: return FILEEXT::TextFileExtension;
372 case TREE_FILE_TYPE::MD: return FILEEXT::MarkdownFileExtension;
373 case TREE_FILE_TYPE::NET: return FILEEXT::NetlistFileExtension;
374 case TREE_FILE_TYPE::NET_SPICE: return FILEEXT::SpiceFileExtension;
375 case TREE_FILE_TYPE::CMP_LINK: return FILEEXT::FootprintAssignmentFileExtension;
376 case TREE_FILE_TYPE::REPORT: return FILEEXT::ReportFileExtension;
377 case TREE_FILE_TYPE::FP_PLACE: return FILEEXT::FootprintPlaceFileExtension;
378 case TREE_FILE_TYPE::DRILL: return FILEEXT::DrillFileExtension;
379 case TREE_FILE_TYPE::DRILL_NC: return "nc";
380 case TREE_FILE_TYPE::DRILL_XNC: return "xnc";
381 case TREE_FILE_TYPE::SVG: return FILEEXT::SVGFileExtension;
382 case TREE_FILE_TYPE::DRAWING_SHEET: return FILEEXT::DrawingSheetFileExtension;
383 case TREE_FILE_TYPE::FOOTPRINT_FILE: return FILEEXT::KiCadFootprintFileExtension;
384 case TREE_FILE_TYPE::SCHEMATIC_LIBFILE: return FILEEXT::LegacySymbolLibFileExtension;
385 case TREE_FILE_TYPE::SEXPR_SYMBOL_LIB_FILE: return FILEEXT::KiCadSymbolLibFileExtension;
386 case TREE_FILE_TYPE::DESIGN_RULES: return FILEEXT::DesignRulesFileExtension;
387 case TREE_FILE_TYPE::ZIP_ARCHIVE: return FILEEXT::ArchiveFileExtension;
388 case TREE_FILE_TYPE::JOBSET_FILE: return FILEEXT::KiCadJobSetFileExtension;
389
390 case TREE_FILE_TYPE::ROOT:
391 case TREE_FILE_TYPE::UNKNOWN:
392 case TREE_FILE_TYPE::MAX:
393 case TREE_FILE_TYPE::DIRECTORY: break;
394 }
395
396 return wxEmptyString;
397}
398
399
400std::vector<wxString> getProjects( const wxDir& dir )
401{
402 std::vector<wxString> projects;
403 wxString dir_filename;
404 bool haveFile = dir.GetFirst( &dir_filename );
405
406 while( haveFile )
407 {
408 wxFileName file( dir_filename );
409
410 if( file.GetExt() == FILEEXT::LegacyProjectFileExtension
411 || file.GetExt() == FILEEXT::ProjectFileExtension )
412 projects.push_back( file.GetName() );
413
414 haveFile = dir.GetNext( &dir_filename );
415 }
416
417 return projects;
418}
419
420
421static git_repository* get_git_repository_for_file( const char* filename )
422{
423 git_repository* repo = nullptr;
424 git_buf repo_path = GIT_BUF_INIT;
425
426 // Find the repository path for the given file
427 if( git_repository_discover( &repo_path, filename, 0, NULL ) != GIT_OK )
428 {
429 wxLogTrace( traceGit, "Can't repo discover %s: %s", filename, KIGIT_COMMON::GetLastGitError() );
430 return nullptr;
431 }
432
433 KIGIT::GitBufPtr repo_path_ptr( &repo_path );
434
435 if( git_repository_open( &repo, repo_path.ptr ) != GIT_OK )
436 {
437 wxLogTrace( traceGit, "Can't open repo for %s: %s", repo_path.ptr, KIGIT_COMMON::GetLastGitError() );
438 return nullptr;
439 }
440
441 return repo;
442}
443
444
445wxTreeItemId PROJECT_TREE_PANE::addItemToProjectTree( const wxString& aName,
446 const wxTreeItemId& aParent,
447 std::vector<wxString>* aProjectNames,
448 bool aRecurse )
449{
450 TREE_FILE_TYPE type = TREE_FILE_TYPE::UNKNOWN;
451 wxFileName fn( aName );
452
453 if( KIPLATFORM::IO::IsFileHidden( aName ) )
454 return wxTreeItemId();
455
456 if( wxDirExists( aName ) )
457 {
458 type = TREE_FILE_TYPE::DIRECTORY;
459 }
460 else
461 {
462 // Filter
463 wxRegEx reg;
464 bool addFile = false;
465
466 for( const wxString& m_filter : m_filters )
467 {
468 wxCHECK2_MSG( reg.Compile( m_filter, wxRE_ICASE ), continue,
469 wxString::Format( "Regex %s failed to compile.", m_filter ) );
470
471 if( reg.Matches( aName ) )
472 {
473 addFile = true;
474 break;
475 }
476 }
477
478 if( !addFile )
479 return wxTreeItemId();
480
481 for( int i = static_cast<int>( TREE_FILE_TYPE::LEGACY_PROJECT );
482 i < static_cast<int>( TREE_FILE_TYPE::MAX ); i++ )
483 {
484 wxString ext = GetFileExt( (TREE_FILE_TYPE) i );
485
486 if( ext == wxT( "" ) )
487 continue;
488
489 if( reg.Compile( wxString::FromAscii( "^.*\\." ) + ext + wxString::FromAscii( "$" ),
490 wxRE_ICASE ) && reg.Matches( aName ) )
491 {
492 type = (TREE_FILE_TYPE) i;
493 break;
494 }
495 }
496 }
497
498 wxString file = wxFileNameFromPath( aName );
499 wxFileName currfile( file );
500 wxFileName project( m_Parent->GetProjectFileName() );
501
502 // Ignore legacy projects with the same name as the current project
503 if( ( type == TREE_FILE_TYPE::LEGACY_PROJECT )
504 && ( currfile.GetName().CmpNoCase( project.GetName() ) == 0 ) )
505 {
506 return wxTreeItemId();
507 }
508
509 if( currfile.GetExt() == GetFileExt( TREE_FILE_TYPE::LEGACY_SCHEMATIC )
510 || currfile.GetExt() == GetFileExt( TREE_FILE_TYPE::SEXPR_SCHEMATIC ) )
511 {
512 if( aProjectNames )
513 {
514 if( !alg::contains( *aProjectNames, currfile.GetName() ) )
515 return wxTreeItemId();
516 }
517 else
518 {
519 PROJECT_TREE_ITEM* parentTreeItem = GetItemIdData( aParent );
520 wxDir parentDir( parentTreeItem->GetDir() );
521 std::vector<wxString> projects = getProjects( parentDir );
522
523 if( !alg::contains( projects, currfile.GetName() ) )
524 return wxTreeItemId();
525 }
526 }
527
528 // also check to see if it is already there.
529 wxTreeItemIdValue cookie;
530 wxTreeItemId kid = m_TreeProject->GetFirstChild( aParent, cookie );
531
532 while( kid.IsOk() )
533 {
534 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
535
536 if( itemData && itemData->GetFileName() == aName )
537 return itemData->GetId(); // well, we would have added it, but it is already here!
538
539 kid = m_TreeProject->GetNextChild( aParent, cookie );
540 }
541
542 // Only show current files if both legacy and current files are present
543 if( type == TREE_FILE_TYPE::LEGACY_PROJECT || type == TREE_FILE_TYPE::JSON_PROJECT
544 || type == TREE_FILE_TYPE::LEGACY_SCHEMATIC || type == TREE_FILE_TYPE::SEXPR_SCHEMATIC )
545 {
546 kid = m_TreeProject->GetFirstChild( aParent, cookie );
547
548 while( kid.IsOk() )
549 {
550 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
551
552 if( itemData )
553 {
554 wxFileName fname( itemData->GetFileName() );
555
556 if( fname.GetName().CmpNoCase( currfile.GetName() ) == 0 )
557 {
558 switch( type )
559 {
560 case TREE_FILE_TYPE::LEGACY_PROJECT:
561 if( itemData->GetType() == TREE_FILE_TYPE::JSON_PROJECT )
562 return wxTreeItemId();
563
564 break;
565
566 case TREE_FILE_TYPE::LEGACY_SCHEMATIC:
567 if( itemData->GetType() == TREE_FILE_TYPE::SEXPR_SCHEMATIC )
568 return wxTreeItemId();
569
570 break;
571
572 case TREE_FILE_TYPE::JSON_PROJECT:
573 if( itemData->GetType() == TREE_FILE_TYPE::LEGACY_PROJECT )
574 m_TreeProject->Delete( kid );
575
576 break;
577
578 case TREE_FILE_TYPE::SEXPR_SCHEMATIC:
579 if( itemData->GetType() == TREE_FILE_TYPE::LEGACY_SCHEMATIC )
580 m_TreeProject->Delete( kid );
581
582 break;
583
584 default:
585 break;
586 }
587 }
588 }
589
590 kid = m_TreeProject->GetNextChild( aParent, cookie );
591 }
592 }
593
594 // Append the item (only appending the filename not the full path):
595 wxTreeItemId newItemId = m_TreeProject->AppendItem( aParent, file );
596 PROJECT_TREE_ITEM* data = new PROJECT_TREE_ITEM( type, aName, m_TreeProject );
597
598 m_TreeProject->SetItemData( newItemId, data );
599 data->SetState( 0 );
600
601 // Mark root files (files which have the same aName as the project)
602 wxString fileName = currfile.GetName().Lower();
603 wxString projName = project.GetName().Lower();
604
605 if( fileName == projName || fileName.StartsWith( projName + "-" ) )
606 data->SetRootFile( true );
607
608#ifndef __WINDOWS__
609 bool subdir_populated = false;
610#endif
611
612 // This section adds dirs and files found in the subdirs
613 // in this case AddFile is recursive, but for the first level only.
614 if( TREE_FILE_TYPE::DIRECTORY == type && aRecurse )
615 {
616 wxDir dir( aName );
617
618 if( dir.IsOpened() ) // protected dirs will not open properly.
619 {
620 std::vector<wxString> projects = getProjects( dir );
621 wxString dir_filename;
622 bool haveFile = dir.GetFirst( &dir_filename );
623
624 data->SetPopulated( true );
625
626#ifndef __WINDOWS__
627 subdir_populated = aRecurse;
628#endif
629
630 while( haveFile )
631 {
632 // Add name in tree, but do not recurse
633 wxString path = aName + wxFileName::GetPathSeparator() + dir_filename;
634 addItemToProjectTree( path, newItemId, &projects, false );
635
636 haveFile = dir.GetNext( &dir_filename );
637 }
638 }
639
640 // Sort filenames by alphabetic order
641 m_TreeProject->SortChildren( newItemId );
642 }
643
644#ifndef __WINDOWS__
645 if( subdir_populated )
646 m_watcherNeedReset = true;
647#endif
648
649 return newItemId;
650}
651
652
654{
655 std::lock_guard<std::mutex> lock1( m_gitStatusMutex );
656 std::lock_guard<std::mutex> lock2( m_gitTreeCacheMutex );
658
659 tp.wait_for_tasks();
660 m_gitStatusTimer.Stop();
661 m_gitSyncTimer.Stop();
662 m_gitTreeCache.clear();
663 m_gitStatusIcons.clear();
664
665 wxString pro_dir = m_Parent->GetProjectFileName();
666
667 if( !m_TreeProject )
668 m_TreeProject = new PROJECT_TREE( this );
669 else
670 m_TreeProject->DeleteAllItems();
671
672 if( !pro_dir ) // This is empty from PROJECT_TREE_PANE constructor
673 return;
674
676 {
677 git_repository_free( m_TreeProject->GetGitRepo() );
678 m_TreeProject->SetGitRepo( nullptr );
679 m_gitIconsInitialized = false;
680 }
681
682 wxFileName fn = pro_dir;
683 bool prjReset = false;
684
685 if( !fn.IsOk() )
686 {
687 fn.Clear();
688 fn.SetPath( PATHS::GetDefaultUserProjectsPath() );
689 fn.SetName( NAMELESS_PROJECT );
691 prjReset = true;
692 }
693
694 bool prjOpened = fn.FileExists();
695
696 // Bind the git repository to the project tree (if it exists)
697 if( Pgm().GetCommonSettings()->m_Git.enableGit )
698 {
699 m_TreeProject->SetGitRepo( get_git_repository_for_file( fn.GetPath().c_str() ) );
700
702 {
703 m_TreeProject->GitCommon()->SetUsername( Prj().GetLocalSettings().m_GitRepoUsername );
704 m_TreeProject->GitCommon()->SetSSHKey( Prj().GetLocalSettings().m_GitSSHKey );
706 }
707 }
708
709 // We may have opened a legacy project, in which case GetProjectFileName will return the
710 // name of the migrated (new format) file, which may not have been saved to disk yet.
711 if( !prjOpened && !prjReset )
712 {
714 prjOpened = fn.FileExists();
715
716 // Set the ext back so that in the tree view we see the (not-yet-saved) new file
718 }
719
720 // root tree:
721 m_root = m_TreeProject->AddRoot( fn.GetFullName(), static_cast<int>( TREE_FILE_TYPE::ROOT ),
722 static_cast<int>( TREE_FILE_TYPE::ROOT ) );
723 m_TreeProject->SetItemBold( m_root, true );
724
725 // The main project file is now a JSON file
726 PROJECT_TREE_ITEM* data = new PROJECT_TREE_ITEM( TREE_FILE_TYPE::JSON_PROJECT,
727 fn.GetFullPath(), m_TreeProject );
728
729 m_TreeProject->SetItemData( m_root, data );
730
731 // Now adding all current files if available
732 if( prjOpened )
733 {
734 pro_dir = wxPathOnly( m_Parent->GetProjectFileName() );
735 wxDir dir( pro_dir );
736
737 if( dir.IsOpened() ) // protected dirs will not open, see "man opendir()"
738 {
739 std::vector<wxString> projects = getProjects( dir );
740 wxString filename;
741 bool haveFile = dir.GetFirst( &filename );
742
743 while( haveFile )
744 {
745 if( filename != fn.GetFullName() )
746 {
747 wxString name = dir.GetName() + wxFileName::GetPathSeparator() + filename;
748
749 // Add items living in the project directory, and populate the item
750 // if it is a directory (sub directories will be not populated)
751 addItemToProjectTree( name, m_root, &projects, true );
752 }
753
754 haveFile = dir.GetNext( &filename );
755 }
756 }
757 }
758 else
759 {
760 m_TreeProject->AppendItem( m_root, wxT( "Empty project" ) );
761 }
762
763 m_TreeProject->Expand( m_root );
764
765 // Sort filenames by alphabetic order
766 m_TreeProject->SortChildren( m_root );
767
768 CallAfter( [this] ()
769 {
770 wxLogTrace( traceGit, "PROJECT_TREE_PANE::ReCreateTreePrj: starting timers" );
771 m_gitSyncTimer.Start( 100, wxTIMER_ONE_SHOT );
772 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
773 } );
774}
775
776
778{
779 git_repository* repo = m_TreeProject->GetGitRepo();
780
781 if( !repo )
782 return false;
783
784 git_status_options opts;
785 git_status_init_options( &opts, GIT_STATUS_OPTIONS_VERSION );
786
787 opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
788 opts.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX
789 | GIT_STATUS_OPT_SORT_CASE_SENSITIVELY;
790
791 git_status_list* status_list = nullptr;
792
793 if( git_status_list_new( &status_list, repo, &opts ) != GIT_OK )
794 {
795 wxLogError( _( "Failed to get status list: %s" ), KIGIT_COMMON::GetLastGitError() );
796 return false;
797 }
798
799 KIGIT::GitStatusListPtr status_list_ptr( status_list );
800 return ( git_status_list_entrycount( status_list ) > 0 );
801}
802
803
804void PROJECT_TREE_PANE::onRight( wxTreeEvent& Event )
805{
806 wxTreeItemId curr_item = Event.GetItem();
807
808 // Ensure item is selected (Under Windows right click does not select the item)
809 m_TreeProject->SelectItem( curr_item );
810
811 std::vector<PROJECT_TREE_ITEM*> selection = GetSelectedData();
813 wxFileName prj_dir( Prj().GetProjectPath(), wxEmptyString );
814 wxFileName git_dir( git->GetGitRootDirectory(), wxEmptyString );
815 prj_dir.Normalize( wxPATH_NORM_ABSOLUTE | wxPATH_NORM_CASE | wxPATH_NORM_DOTS
816 | wxPATH_NORM_ENV_VARS | wxPATH_NORM_TILDE );
817 git_dir.Normalize( wxPATH_NORM_ABSOLUTE | wxPATH_NORM_CASE | wxPATH_NORM_DOTS
818 | wxPATH_NORM_ENV_VARS | wxPATH_NORM_TILDE );
819 wxString prj_name = prj_dir.GetFullPath();
820 wxString git_name = git_dir.GetFullPath();
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_remove = vcs_has_repo && git_name.StartsWith( prj_name ); // This means the .git is a subdirectory of the project
834 bool vcs_can_fetch = vcs_has_repo && git->HasPushAndPullRemote();
835 bool vcs_can_push = vcs_can_fetch && git->HasLocalCommits();
836 bool vcs_can_pull = vcs_can_fetch;
837 bool vcs_can_switch = vcs_has_repo;
838 bool vcs_menu = Pgm().GetCommonSettings()->m_Git.enableGit;
839
840 // Check if the libgit2 library has been successfully initialized
841#if ( LIBGIT2_VER_MAJOR >= 1 ) || ( LIBGIT2_VER_MINOR >= 99 )
842 int major, minor, rev;
843 bool libgit_init = ( git_libgit2_version( &major, &minor, &rev ) == GIT_OK );
844#else
845 //Work around libgit2 API change for supporting older platforms
846 bool libgit_init = true;
847#endif
848
849 vcs_menu &= libgit_init;
850
851 if( selection.size() == 0 )
852 return;
853
854 // Remove things that don't make sense for multiple selections
855 if( selection.size() != 1 )
856 {
857 can_switch_to_project = false;
858 can_create_new_directory = false;
859 can_rename = false;
860 }
861
862 for( PROJECT_TREE_ITEM* item : selection )
863 {
864 // Check for empty project
865 if( !item )
866 {
867 can_switch_to_project = false;
868 can_edit = false;
869 can_rename = false;
870 continue;
871 }
872
873 can_delete = item->CanDelete();
874 can_rename = item->CanRename();
875
876 switch( item->GetType() )
877 {
878 case TREE_FILE_TYPE::JSON_PROJECT:
879 case TREE_FILE_TYPE::LEGACY_PROJECT:
880 can_rename = false;
881
882 if( item->GetId() == m_TreeProject->GetRootItem() )
883 {
884 can_switch_to_project = false;
885 }
886 else
887 {
888 can_create_new_directory = false;
889 can_open_this_directory = false;
890 }
891 break;
892
893 case TREE_FILE_TYPE::DIRECTORY:
894 can_switch_to_project = false;
895 can_edit = false;
896 break;
897
898 case TREE_FILE_TYPE::ZIP_ARCHIVE:
899 case TREE_FILE_TYPE::PDF:
900 can_edit = false;
901 can_switch_to_project = false;
902 can_create_new_directory = false;
903 can_open_this_directory = false;
904 break;
905
906 case TREE_FILE_TYPE::JOBSET_FILE:
907 run_jobs = true;
908 can_edit = false;
910
911 case TREE_FILE_TYPE::SEXPR_SCHEMATIC:
912 case TREE_FILE_TYPE::SEXPR_PCB:
914
915 default:
916 can_switch_to_project = false;
917 can_create_new_directory = false;
918 can_open_this_directory = false;
919
920 break;
921 }
922 }
923
924 wxMenu popup_menu;
925 wxString text;
926 wxString help_text;
927
928 if( can_switch_to_project )
929 {
930 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_SWITCH_TO_OTHER, _( "Switch to this Project" ),
931 _( "Close all editors, and switch to the selected project" ),
932 KiBitmap( BITMAPS::open_project ) );
933 popup_menu.AppendSeparator();
934 }
935
936 if( can_create_new_directory )
937 {
938 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_NEWDIR, _( "New Directory..." ),
939 _( "Create a New Directory" ), KiBitmap( BITMAPS::directory ) );
940 }
941
942 if( can_open_this_directory )
943 {
944 if( selection.size() == 1 )
945 {
946#ifdef __APPLE__
947 text = _( "Reveal in Finder" );
948 help_text = _( "Reveals the directory in a Finder window" );
949#else
950 text = _( "Open Directory in File Explorer" );
951 help_text = _( "Opens the directory in the default system file manager" );
952#endif
953 }
954 else
955 {
956#ifdef __APPLE__
957 text = _( "Reveal in Finder" );
958 help_text = _( "Reveals the directories in a Finder window" );
959#else
960 text = _( "Open Directories in File Explorer" );
961 help_text = _( "Opens the directories in the default system file manager" );
962#endif
963 }
964
965 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_OPEN_DIR, text, help_text,
966 KiBitmap( BITMAPS::directory_browser ) );
967 }
968
969 if( can_edit )
970 {
971 if( selection.size() == 1 )
972 help_text = _( "Open the file in a Text Editor" );
973 else
974 help_text = _( "Open files in a Text Editor" );
975
976 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_TXTEDIT, _( "Edit in a Text Editor" ), help_text,
977 KiBitmap( BITMAPS::editor ) );
978 }
979
980 if( run_jobs && selection.size() == 1 )
981 {
982 KIUI::AddMenuItem( &popup_menu, ID_JOBS_RUN, _( "Run Jobs" ), help_text,
983 KiBitmap( BITMAPS::exchange ) );
984 }
985
986 if( can_rename )
987 {
988 if( selection.size() == 1 )
989 {
990 text = _( "Rename File..." );
991 help_text = _( "Rename file" );
992 }
993 else
994 {
995 text = _( "Rename Files..." );
996 help_text = _( "Rename files" );
997 }
998
999 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_RENAME, text, help_text,
1000 KiBitmap( BITMAPS::right ) );
1001 }
1002
1003 if( can_delete )
1004 {
1005 if( selection.size() == 1 )
1006 help_text = _( "Delete the file and its content" );
1007 else
1008 help_text = _( "Delete the files and their contents" );
1009
1010 if( can_switch_to_project
1011 || can_create_new_directory
1012 || can_open_this_directory
1013 || can_edit
1014 || can_rename )
1015 {
1016 popup_menu.AppendSeparator();
1017 }
1018
1019#ifdef __WINDOWS__
1020 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_DELETE, _( "Delete" ), help_text,
1021 KiBitmap( BITMAPS::trash ) );
1022#else
1023 KIUI::AddMenuItem( &popup_menu, ID_PROJECT_DELETE, _( "Move to Trash" ), help_text,
1024 KiBitmap( BITMAPS::trash ) );
1025#endif
1026 }
1027
1028 if( vcs_menu )
1029 {
1030 wxMenu* vcs_submenu = new wxMenu();
1031 wxMenu* branch_submenu = new wxMenu();
1032 wxMenuItem* vcs_menuitem = nullptr;
1033
1034 vcs_menuitem = vcs_submenu->Append( ID_GIT_INITIALIZE_PROJECT,
1035 _( "Add Project to Version Control..." ),
1036 _( "Initialize a new repository" ) );
1037 vcs_menuitem->Enable( vcs_can_init );
1038
1039
1040 vcs_menuitem = vcs_submenu->Append( ID_GIT_COMMIT_PROJECT, _( "Commit Project..." ),
1041 _( "Commit changes to the local repository" ) );
1042 vcs_menuitem->Enable( vcs_can_commit );
1043
1044 vcs_menuitem = vcs_submenu->Append( ID_GIT_PUSH, _( "Push" ),
1045 _( "Push committed local changes to remote repository" ) );
1046 vcs_menuitem->Enable( vcs_can_push );
1047
1048 vcs_menuitem = vcs_submenu->Append( ID_GIT_PULL, _( "Pull" ),
1049 _( "Pull changes from remote repository into local" ) );
1050 vcs_menuitem->Enable( vcs_can_pull );
1051
1052 vcs_submenu->AppendSeparator();
1053
1054 vcs_menuitem = vcs_submenu->Append( ID_GIT_COMMIT_FILE, _( "Commit File..." ),
1055 _( "Commit changes to the local repository" ) );
1056 vcs_menuitem->Enable( vcs_can_commit );
1057
1058 vcs_submenu->AppendSeparator();
1059
1060 // vcs_menuitem = vcs_submenu->Append( ID_GIT_COMPARE, _( "Diff" ),
1061 // _( "Show changes between the repository and working tree" ) );
1062 // vcs_menuitem->Enable( vcs_can_diff );
1063
1064 std::vector<wxString> branchNames = m_TreeProject->GitCommon()->GetBranchNames();
1065
1066 // Skip the first one (that is the current branch)
1067 for( size_t ii = 1; ii < branchNames.size() && ii < 6; ++ii )
1068 {
1069 wxString msg = _( "Switch to branch " ) + branchNames[ii];
1070 vcs_menuitem = branch_submenu->Append( ID_GIT_SWITCH_BRANCH + ii, branchNames[ii], msg );
1071 vcs_menuitem->Enable( vcs_can_switch );
1072 }
1073
1074 vcs_menuitem = branch_submenu->Append( ID_GIT_SWITCH_BRANCH, _( "Other..." ),
1075 _( "Switch to a different branch" ) );
1076 vcs_menuitem->Enable( vcs_can_switch );
1077
1078 vcs_submenu->Append( ID_GIT_SWITCH_BRANCH, _( "Switch to Branch" ), branch_submenu );
1079
1080 vcs_submenu->AppendSeparator();
1081
1082 vcs_menuitem = vcs_submenu->Append( ID_GIT_REMOVE_VCS, _( "Remove Version Control" ),
1083 _( "Delete all version control files from the project directory." ) );
1084 vcs_menuitem->Enable( vcs_can_remove );
1085
1086 popup_menu.AppendSeparator();
1087 popup_menu.AppendSubMenu( vcs_submenu, _( "Version Control" ) );
1088 }
1089
1090 if( popup_menu.GetMenuItemCount() > 0 )
1091 PopupMenu( &popup_menu );
1092}
1093
1094
1096{
1097 wxString editorname = Pgm().GetTextEditor();
1098
1099 if( editorname.IsEmpty() )
1100 {
1101 wxMessageBox( _( "No text editor selected in KiCad. Please choose one." ) );
1102 return;
1103 }
1104
1105 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
1106
1107 for( PROJECT_TREE_ITEM* item_data : tree_data )
1108 {
1109 wxString fullFileName = item_data->GetFileName();
1110
1111 if( !fullFileName.IsEmpty() )
1112 {
1113 ExecuteFile( editorname, fullFileName.wc_str(), nullptr, false );
1114 }
1115 }
1116}
1117
1118
1119void PROJECT_TREE_PANE::onDeleteFile( wxCommandEvent& event )
1120{
1121 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
1122
1123 for( PROJECT_TREE_ITEM* item_data : tree_data )
1124 item_data->Delete();
1125}
1126
1127
1128void PROJECT_TREE_PANE::onRenameFile( wxCommandEvent& event )
1129{
1130 wxTreeItemId curr_item = m_TreeProject->GetFocusedItem();
1131 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
1132
1133 // XXX: Unnecessary?
1134 if( tree_data.size() != 1 )
1135 return;
1136
1137 wxString buffer = m_TreeProject->GetItemText( curr_item );
1138 wxString msg = wxString::Format( _( "Change filename: '%s'" ),
1139 tree_data[0]->GetFileName() );
1140 wxTextEntryDialog dlg( wxGetTopLevelParent( this ), msg, _( "Change filename" ), buffer );
1141
1142 if( dlg.ShowModal() != wxID_OK )
1143 return; // canceled by user
1144
1145 buffer = dlg.GetValue();
1146 buffer.Trim( true );
1147 buffer.Trim( false );
1148
1149 if( buffer.IsEmpty() )
1150 return; // empty file name not allowed
1151
1152 tree_data[0]->Rename( buffer, true );
1153 m_isRenaming = true;
1154}
1155
1156
1157void PROJECT_TREE_PANE::onSelect( wxTreeEvent& Event )
1158{
1159 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
1160
1161 if( tree_data.size() != 1 )
1162 return;
1163
1164 // Bookmark the selected item but don't try and activate it until later. If we do it now,
1165 // there will be more events at least on Windows in this frame that will steal focus from
1166 // any newly launched windows
1167 m_selectedItem = tree_data[0];
1168}
1169
1170
1171void PROJECT_TREE_PANE::onIdle( wxIdleEvent& aEvent )
1172{
1173 // Idle executes once all other events finished processing. This makes it ideal to launch
1174 // a new window without starting Focus wars.
1175 if( m_watcherNeedReset )
1176 {
1177 m_selectedItem = nullptr;
1179 }
1180
1181 if( m_selectedItem != nullptr )
1182 {
1183 // Activate launches a window which may run the event loop on top of us and cause OnIdle
1184 // to get called again, so be sure to block off the activation condition first.
1186 m_selectedItem = nullptr;
1187
1188 item->Activate( this );
1189 }
1190}
1191
1192
1193void PROJECT_TREE_PANE::onExpand( wxTreeEvent& Event )
1194{
1195 wxTreeItemId itemId = Event.GetItem();
1196 PROJECT_TREE_ITEM* tree_data = GetItemIdData( itemId );
1197
1198 if( !tree_data )
1199 return;
1200
1201 if( tree_data->GetType() != TREE_FILE_TYPE::DIRECTORY )
1202 return;
1203
1204 // explore list of non populated subdirs, and populate them
1205 wxTreeItemIdValue cookie;
1206 wxTreeItemId kid = m_TreeProject->GetFirstChild( itemId, cookie );
1207
1208#ifndef __WINDOWS__
1209 bool subdir_populated = false;
1210#endif
1211
1212 for( ; kid.IsOk(); kid = m_TreeProject->GetNextChild( itemId, cookie ) )
1213 {
1214 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
1215
1216 if( !itemData || itemData->GetType() != TREE_FILE_TYPE::DIRECTORY )
1217 continue;
1218
1219 if( itemData->IsPopulated() )
1220 continue;
1221
1222 wxString fileName = itemData->GetFileName();
1223 wxDir dir( fileName );
1224
1225 if( dir.IsOpened() )
1226 {
1227 std::vector<wxString> projects = getProjects( dir );
1228 wxString dir_filename;
1229 bool haveFile = dir.GetFirst( &dir_filename );
1230
1231 while( haveFile )
1232 {
1233 // Add name to tree item, but do not recurse in subdirs:
1234 wxString name = fileName + wxFileName::GetPathSeparator() + dir_filename;
1235 addItemToProjectTree( name, kid, &projects, false );
1236
1237 haveFile = dir.GetNext( &dir_filename );
1238 }
1239
1240 itemData->SetPopulated( true ); // set state to populated
1241
1242#ifndef __WINDOWS__
1243 subdir_populated = true;
1244#endif
1245 }
1246
1247 // Sort filenames by alphabetic order
1248 m_TreeProject->SortChildren( kid );
1249 }
1250
1251#ifndef __WINDOWS__
1252 if( subdir_populated )
1253 m_watcherNeedReset = true;
1254#endif
1255}
1256
1257
1258std::vector<PROJECT_TREE_ITEM*> PROJECT_TREE_PANE::GetSelectedData()
1259{
1260 wxArrayTreeItemIds selection;
1261 std::vector<PROJECT_TREE_ITEM*> data;
1262
1263 m_TreeProject->GetSelections( selection );
1264
1265 for( auto it = selection.begin(); it != selection.end(); it++ )
1266 {
1267 PROJECT_TREE_ITEM* item = GetItemIdData( *it );
1268
1269 if( !item )
1270 {
1271 wxLogTrace( traceGit, wxS( "Null tree item returned for selection, dynamic_cast "
1272 "failed?" ) );
1273 continue;
1274 }
1275
1276 data.push_back( item );
1277 }
1278
1279 return data;
1280}
1281
1282
1284{
1285 return dynamic_cast<PROJECT_TREE_ITEM*>( m_TreeProject->GetItemData( aId ) );
1286}
1287
1288
1289wxTreeItemId PROJECT_TREE_PANE::findSubdirTreeItem( const wxString& aSubDir )
1290{
1291 wxString prj_dir = wxPathOnly( m_Parent->GetProjectFileName() );
1292
1293 // If the subdir is the current working directory, return m_root
1294 // in main list:
1295 if( prj_dir == aSubDir )
1296 return m_root;
1297
1298 // The subdir is in the main tree or in a subdir: Locate it
1299 wxTreeItemIdValue cookie;
1300 wxTreeItemId root_id = m_root;
1301 std::stack<wxTreeItemId> subdirs_id;
1302
1303 wxTreeItemId child = m_TreeProject->GetFirstChild( root_id, cookie );
1304
1305 while( true )
1306 {
1307 if( ! child.IsOk() )
1308 {
1309 if( subdirs_id.empty() ) // all items were explored
1310 {
1311 root_id = child; // Not found: return an invalid wxTreeItemId
1312 break;
1313 }
1314 else
1315 {
1316 root_id = subdirs_id.top();
1317 subdirs_id.pop();
1318 child = m_TreeProject->GetFirstChild( root_id, cookie );
1319
1320 if( !child.IsOk() )
1321 continue;
1322 }
1323 }
1324
1325 PROJECT_TREE_ITEM* itemData = GetItemIdData( child );
1326
1327 if( itemData && ( itemData->GetType() == TREE_FILE_TYPE::DIRECTORY ) )
1328 {
1329 if( itemData->GetFileName() == aSubDir ) // Found!
1330 {
1331 root_id = child;
1332 break;
1333 }
1334
1335 // child is a subdir, push in list to explore it later
1336 if( itemData->IsPopulated() )
1337 subdirs_id.push( child );
1338 }
1339
1340 child = m_TreeProject->GetNextChild( root_id, cookie );
1341 }
1342
1343 return root_id;
1344}
1345
1346
1347void PROJECT_TREE_PANE::onFileSystemEvent( wxFileSystemWatcherEvent& event )
1348{
1349 // No need to process events when we're shutting down
1350 if( !m_watcher )
1351 return;
1352
1353 // Ignore events that are not file creation, deletion, renaming or modification because
1354 // they are not relevant to the project tree.
1355 if( !( event.GetChangeType() & ( wxFSW_EVENT_CREATE |
1356 wxFSW_EVENT_DELETE |
1357 wxFSW_EVENT_RENAME |
1358 wxFSW_EVENT_MODIFY ) ) )
1359 {
1360 return;
1361 }
1362
1363 const wxFileName& pathModified = event.GetPath();
1364 wxString subdir = pathModified.GetPath();
1365 wxString fn = pathModified.GetFullPath();
1366
1367 // Adjust directories to look like a file item (path and name).
1368 if( pathModified.GetFullName().IsEmpty() )
1369 {
1370 subdir = subdir.BeforeLast( '/' );
1371 fn = fn.BeforeLast( '/' );
1372 }
1373
1374 wxTreeItemId root_id = findSubdirTreeItem( subdir );
1375
1376 if( !root_id.IsOk() )
1377 return;
1378
1379 CallAfter( [this] ()
1380 {
1381 wxLogTrace( traceGit, wxS( "File system event detected, updating tree cache" ) );
1382 m_gitStatusTimer.Start( 1500, wxTIMER_ONE_SHOT );
1383 } );
1384
1385 wxTreeItemIdValue cookie; // dummy variable needed by GetFirstChild()
1386 wxTreeItemId kid = m_TreeProject->GetFirstChild( root_id, cookie );
1387
1388 switch( event.GetChangeType() )
1389 {
1390 case wxFSW_EVENT_CREATE:
1391 {
1392 wxTreeItemId newitem = addItemToProjectTree( fn, root_id, nullptr, true );
1393
1394 // If we are in the process of renaming a file, select the new one
1395 // This is needed for MSW and OSX, since we don't get RENAME events from them, just a
1396 // pair of DELETE and CREATE events.
1397 if( m_isRenaming && newitem.IsOk() )
1398 {
1399 m_TreeProject->SelectItem( newitem );
1400 m_isRenaming = false;
1401 }
1402 }
1403 break;
1404
1405 case wxFSW_EVENT_DELETE:
1406 while( kid.IsOk() )
1407 {
1408 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
1409
1410 if( itemData && itemData->GetFileName() == fn )
1411 {
1412 m_TreeProject->Delete( kid );
1413 return;
1414 }
1415 kid = m_TreeProject->GetNextChild( root_id, cookie );
1416 }
1417 break;
1418
1419 case wxFSW_EVENT_RENAME :
1420 {
1421 const wxFileName& newpath = event.GetNewPath();
1422 wxString newdir = newpath.GetPath();
1423 wxString newfn = newpath.GetFullPath();
1424
1425 while( kid.IsOk() )
1426 {
1427 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
1428
1429 if( itemData && itemData->GetFileName() == fn )
1430 {
1431 m_TreeProject->Delete( kid );
1432 break;
1433 }
1434
1435 kid = m_TreeProject->GetNextChild( root_id, cookie );
1436 }
1437
1438 // Add the new item only if it is not the current project file (root item).
1439 // Remember: this code is called by a wxFileSystemWatcherEvent event, and not always
1440 // called after an actual file rename, and the cleanup code does not explore the
1441 // root item, because it cannot be renamed by the user. Also, ensure the new file
1442 // actually exists on the file system before it is readded. On Linux, moving a file
1443 // to the trash can cause the same path to be returned in both the old and new paths
1444 // of the event, even though the file isn't there anymore.
1445 PROJECT_TREE_ITEM* rootData = GetItemIdData( root_id );
1446
1447 if( rootData && newpath.Exists() && ( newfn != rootData->GetFileName() ) )
1448 {
1449 wxTreeItemId newroot_id = findSubdirTreeItem( newdir );
1450 wxTreeItemId newitem = addItemToProjectTree( newfn, newroot_id, nullptr, true );
1451
1452 // If the item exists, select it
1453 if( newitem.IsOk() )
1454 m_TreeProject->SelectItem( newitem );
1455 }
1456
1457 m_isRenaming = false;
1458 }
1459 break;
1460
1461 default:
1462 return;
1463 }
1464
1465 // Sort filenames by alphabetic order
1466 m_TreeProject->SortChildren( root_id );
1467}
1468
1469
1471{
1472 m_watcherNeedReset = false;
1473
1474 wxString prj_dir = wxPathOnly( m_Parent->GetProjectFileName() );
1475
1476#if defined( _WIN32 )
1477 KISTATUSBAR* statusBar = static_cast<KISTATUSBAR*>( m_Parent->GetStatusBar() );
1478
1479 if( KIPLATFORM::ENV::IsNetworkPath( prj_dir ) )
1480 {
1481 // Due to a combination of a bug in SAMBA sending bad change event IDs and wxWidgets
1482 // choosing to fault on an invalid event ID instead of sanely ignoring them we need to
1483 // avoid spawning a filewatcher. Unfortunately this punishes corporate environments with
1484 // Windows Server shares :/
1485 m_Parent->m_FileWatcherInfo = _( "Network path: not monitoring folder changes" );
1487 return;
1488 }
1489 else
1490 {
1491 m_Parent->m_FileWatcherInfo = _( "Local path: monitoring folder changes" );
1493 }
1494#endif
1495
1496 // Prepare file watcher:
1497 if( m_watcher )
1498 {
1499 m_watcher->RemoveAll();
1500 }
1501 else
1502 {
1504 m_watcher->SetOwner( this );
1505 }
1506
1507 // We can see wxString under a debugger, not a wxFileName
1508 wxFileName fn;
1509 fn.AssignDir( prj_dir );
1510 fn.DontFollowLink();
1511
1512 // Add directories which should be monitored.
1513 // under windows, we add the curr dir and all subdirs
1514 // under unix, we add only the curr dir and the populated subdirs
1515 // see http://docs.wxwidgets.org/trunk/classwx_file_system_watcher.htm
1516 // under unix, the file watcher needs more work to be efficient
1517 // moreover, under wxWidgets 2.9.4, AddTree does not work properly.
1518 {
1519 wxLogNull logNo; // avoid log messages
1520#ifdef __WINDOWS__
1521 if( ! m_watcher->AddTree( fn ) )
1522 {
1523 wxLogTrace( tracePathsAndFiles, "%s: failed to add '%s'\n", __func__,
1524 TO_UTF8( fn.GetFullPath() ) );
1525 return;
1526 }
1527 }
1528#else
1529 if( !m_watcher->Add( fn ) )
1530 {
1531 wxLogTrace( tracePathsAndFiles, "%s: failed to add '%s'\n", __func__,
1532 TO_UTF8( fn.GetFullPath() ) );
1533 return;
1534 }
1535 }
1536
1537 if( m_TreeProject->IsEmpty() )
1538 return;
1539
1540 // Add subdirs
1541 wxTreeItemIdValue cookie;
1542 wxTreeItemId root_id = m_root;
1543
1544 std::stack < wxTreeItemId > subdirs_id;
1545
1546 wxTreeItemId kid = m_TreeProject->GetFirstChild( root_id, cookie );
1547 int total_watch_count = 0;
1548
1549 while( total_watch_count < ADVANCED_CFG::GetCfg().m_MaxFilesystemWatchers )
1550 {
1551 if( !kid.IsOk() )
1552 {
1553 if( subdirs_id.empty() ) // all items were explored
1554 {
1555 break;
1556 }
1557 else
1558 {
1559 root_id = subdirs_id.top();
1560 subdirs_id.pop();
1561 kid = m_TreeProject->GetFirstChild( root_id, cookie );
1562
1563 if( !kid.IsOk() )
1564 continue;
1565 }
1566 }
1567
1568 PROJECT_TREE_ITEM* itemData = GetItemIdData( kid );
1569
1570 if( itemData && itemData->GetType() == TREE_FILE_TYPE::DIRECTORY )
1571 {
1572 // we can see wxString under a debugger, not a wxFileName
1573 const wxString& path = itemData->GetFileName();
1574
1575 wxLogTrace( tracePathsAndFiles, "%s: add '%s'\n", __func__, TO_UTF8( path ) );
1576
1577 if( wxFileName::IsDirReadable( path ) ) // linux whines about watching protected dir
1578 {
1579 {
1580 // Silence OS errors that come from the watcher
1581 wxLogNull silence;
1582 fn.AssignDir( path );
1583 m_watcher->Add( fn );
1584 total_watch_count++;
1585 }
1586
1587 // if kid is a subdir, push in list to explore it later
1588 if( itemData->IsPopulated() && m_TreeProject->GetChildrenCount( kid ) )
1589 subdirs_id.push( kid );
1590 }
1591 }
1592
1593 kid = m_TreeProject->GetNextChild( root_id, cookie );
1594 }
1595
1596 if( total_watch_count >= ADVANCED_CFG::GetCfg().m_MaxFilesystemWatchers )
1597 wxLogTrace( tracePathsAndFiles, "%s: too many directories to watch\n", __func__ );
1598#endif
1599
1600#if defined(DEBUG) && 1
1601 wxArrayString paths;
1602 m_watcher->GetWatchedPaths( &paths );
1603 wxLogTrace( tracePathsAndFiles, "%s: watched paths:", __func__ );
1604
1605 for( unsigned ii = 0; ii < paths.GetCount(); ii++ )
1606 wxLogTrace( tracePathsAndFiles, " %s\n", TO_UTF8( paths[ii] ) );
1607#endif
1608 }
1609
1610
1612{
1613 // Make sure we don't try to inspect the tree after we've deleted its items.
1615
1616 m_TreeProject->DeleteAllItems();
1617
1618 // Remove the git repository when the project is unloaded
1619 if( m_TreeProject->GetGitRepo() )
1620 {
1621 // We need to lock the mutex to ensure that no other thread is using the git repository
1622 std::unique_lock<std::mutex> lock( m_TreeProject->GitCommon()->m_gitActionMutex, std::try_to_lock );
1623
1624 if( !lock.owns_lock() )
1625 {
1626 // Block until any in-flight Git actions complete, showing a pulsing progress dialog
1627 {
1628 wxProgressDialog progress(
1629 _("Please wait"),
1630 _("Waiting for Git operations to finish..."),
1631 100,
1632 this,
1633 wxPD_APP_MODAL | wxPD_AUTO_HIDE | wxPD_SMOOTH
1634 );
1635
1636 // Keep trying to acquire the lock, pulsing the dialog every 100 ms
1637 while ( !lock.try_lock() )
1638 {
1639 progress.Pulse();
1640 std::this_thread::sleep_for( std::chrono::milliseconds(100) );
1641 // allow UI events to process so dialog remains responsive
1642 wxYield();
1643 }
1644 }
1645 }
1646
1647 git_repository_free( m_TreeProject->GetGitRepo() );
1648 m_TreeProject->SetGitRepo( nullptr );
1649 }
1650}
1651
1652
1653void PROJECT_TREE_PANE::onThemeChanged( wxSysColourChangedEvent &aEvent )
1654{
1657 m_TreeProject->Refresh();
1658
1659 aEvent.Skip();
1660}
1661
1662
1663void PROJECT_TREE_PANE::onPaint( wxPaintEvent& event )
1664{
1665 wxRect rect( wxPoint( 0, 0 ), GetClientSize() );
1666 wxPaintDC dc( this );
1667
1668 dc.SetBrush( wxSystemSettings::GetColour( wxSYS_COLOUR_FRAMEBK ) );
1669 dc.SetPen( wxPen( wxSystemSettings::GetColour( wxSYS_COLOUR_ACTIVEBORDER ), 1 ) );
1670
1671 dc.DrawLine( rect.GetLeft(), rect.GetTop(), rect.GetLeft(), rect.GetBottom() );
1672 dc.DrawLine( rect.GetRight(), rect.GetTop(), rect.GetRight(), rect.GetBottom() );
1673}
1674
1675
1676void KICAD_MANAGER_FRAME::OnChangeWatchedPaths( wxCommandEvent& aEvent )
1677{
1679}
1680
1681
1682void PROJECT_TREE_PANE::onGitInitializeProject( wxCommandEvent& aEvent )
1683{
1684 PROJECT_TREE_ITEM* tree_data = GetItemIdData( m_TreeProject->GetRootItem() );
1685
1686 wxString dir = tree_data->GetDir();
1687
1688 if( dir.empty() )
1689 {
1690 wxLogError( "Failed to initialize git project: project directory is empty." );
1691 return;
1692 }
1693
1694 // Check if the directory is already a git repository
1695 git_repository* repo = nullptr;
1696 int error = git_repository_open( &repo, dir.mb_str() );
1697
1698 if( error == 0 )
1699 {
1700 // Directory is already a git repository
1701 wxWindow* topLevelParent = wxGetTopLevelParent( this );
1702
1703 DisplayInfoMessage( topLevelParent,
1704 _( "The selected directory is already a Git project." ) );
1705 git_repository_free( repo );
1706 return;
1707 }
1708
1709 KIGIT::GitRepositoryPtr repoPtr( repo );
1710 DIALOG_GIT_REPOSITORY dlg( wxGetTopLevelParent( this ), nullptr );
1711
1712 dlg.SetTitle( _( "Set default remote" ) );
1713
1714 if( dlg.ShowModal() != wxID_OK )
1715 return;
1716
1717 // Directory is not a git repository
1718 if( git_repository_init( &repo, dir.mb_str(), 0 ) != GIT_OK )
1719 {
1720 if( m_gitLastError != git_error_last()->klass )
1721 {
1722 m_gitLastError = git_error_last()->klass;
1723 DisplayErrorMessage( m_parent, _( "Failed to initialize Git project." ),
1725 }
1726
1727 return;
1728 }
1729 else
1730 {
1731 m_TreeProject->SetGitRepo( repoPtr.release() );
1732 m_gitLastError = GIT_ERROR_NONE;
1733 }
1734
1735 //Set up the git remote
1739
1740 git_remote* remote = nullptr;
1741 wxString fullURL;
1742
1744 {
1745 fullURL = dlg.GetUsername() + "@" + dlg.GetRepoURL();
1746 }
1748 {
1749 fullURL = dlg.GetRepoURL().StartsWith( "https" ) ? "https://" : "http://";
1750
1751 if( !dlg.GetUsername().empty() )
1752 {
1753 fullURL.append( dlg.GetUsername() );
1754
1755 if( !dlg.GetPassword().empty() )
1756 {
1757 fullURL.append( wxS( ":" ) );
1758 fullURL.append( dlg.GetPassword() );
1759 }
1760
1761 fullURL.append( wxS( "@" ) );
1762 }
1763
1764 fullURL.append( dlg.GetBareRepoURL() );
1765 }
1766 else
1767 {
1768 fullURL = dlg.GetRepoURL();
1769 }
1770
1771
1772 error = git_remote_create_with_fetchspec( &remote, repo, "origin",
1773 fullURL.ToStdString().c_str(),
1774 "+refs/heads/*:refs/remotes/origin/*" );
1775
1776 if( error != GIT_OK )
1777 {
1778 if( m_gitLastError != git_error_last()->klass )
1779 {
1780 m_gitLastError = git_error_last()->klass;
1781 DisplayErrorMessage( m_parent, _( "Failed to set default remote." ),
1783 }
1784
1785 return;
1786 }
1787
1788 m_gitLastError = GIT_ERROR_NONE;
1789
1791
1792 handler.SetProgressReporter( std::make_unique<WX_PROGRESS_REPORTER>( this, _( "Fetch Remote" ), 1, PR_NO_ABORT ) );
1793
1794 handler.PerformFetch();
1795
1799
1803 Prj().GetLocalSettings().m_GitRepoType = "https";
1804 else
1805 Prj().GetLocalSettings().m_GitRepoType = "local";
1806}
1807
1808
1809void PROJECT_TREE_PANE::onGitCompare( wxCommandEvent& aEvent )
1810{
1811
1812}
1813
1814
1815void PROJECT_TREE_PANE::onGitPullProject( wxCommandEvent& aEvent )
1816{
1817 git_repository* repo = m_TreeProject->GetGitRepo();
1818
1819 if( !repo )
1820 return;
1821
1823
1824 handler.SetProgressReporter( std::make_unique<WX_PROGRESS_REPORTER>( this, _( "Fetch Remote" ), 1,
1825 PR_NO_ABORT ) );
1826
1827 if( handler.PerformPull() < PullResult::Success )
1828 {
1829 wxString errorMessage = handler.GetErrorString();
1830
1831 DisplayErrorMessage( m_parent, _( "Failed to pull project" ), errorMessage );
1832 }
1833
1834 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
1835}
1836
1837
1838void PROJECT_TREE_PANE::onGitPushProject( wxCommandEvent& aEvent )
1839{
1840 git_repository* repo = m_TreeProject->GetGitRepo();
1841
1842 if( !repo )
1843 return;
1844
1846
1847 handler.SetProgressReporter( std::make_unique<WX_PROGRESS_REPORTER>( this, _( "Fetch Remote" ), 1,
1848 PR_NO_ABORT ) );
1849
1850 if( handler.PerformPush() != PushResult::Success )
1851 {
1852 wxString errorMessage = handler.GetErrorString();
1853
1854 DisplayErrorMessage( m_parent, _( "Failed to push project" ), errorMessage );
1855 }
1856
1857 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
1858}
1859
1860
1861static int git_create_branch( git_repository* aRepo, wxString& aBranchName )
1862{
1863 git_oid head_oid;
1864
1865 if( int error = git_reference_name_to_id( &head_oid, aRepo, "HEAD" ) != GIT_OK )
1866 {
1867 wxLogTrace( traceGit, "Failed to lookup HEAD reference: %s", KIGIT_COMMON::GetLastGitError() );
1868 return error;
1869 }
1870
1871 // Lookup the current commit object
1872 git_commit* commit = nullptr;
1873
1874 if( int error = git_commit_lookup( &commit, aRepo, &head_oid ) != GIT_OK )
1875 {
1876 wxLogTrace( traceGit, "Failed to lookup commit: %s", KIGIT_COMMON::GetLastGitError() );
1877 return error;
1878 }
1879
1880 KIGIT::GitCommitPtr commitPtr( commit );
1881 git_reference* branchRef = nullptr;
1882
1883 if( int error = git_branch_create( &branchRef, aRepo, aBranchName.mb_str(), commit, 0 ) != GIT_OK )
1884 {
1885 wxLogTrace( traceGit, "Failed to create branch: %s", KIGIT_COMMON::GetLastGitError() );
1886 return error;
1887 }
1888
1889 git_reference_free( branchRef );
1890
1891 return 0;
1892}
1893
1894
1895void PROJECT_TREE_PANE::onGitSwitchBranch( wxCommandEvent& aEvent )
1896{
1897 git_repository* repo = m_TreeProject->GetGitRepo();
1898
1899 if( !repo )
1900 return;
1901
1902 wxString branchName;
1903
1904 if( aEvent.GetId() == ID_GIT_SWITCH_BRANCH )
1905 {
1906 DIALOG_GIT_SWITCH dlg( wxGetTopLevelParent( this ), repo );
1907
1908 int retval = dlg.ShowModal();
1909 branchName = dlg.GetBranchName();
1910
1911 if( retval == wxID_ADD )
1912 git_create_branch( repo, branchName );
1913 else if( retval != wxID_OK )
1914 return;
1915 }
1916 else
1917 {
1918 std::vector<wxString> branches = m_TreeProject->GitCommon()->GetBranchNames();
1919 int branchIndex = aEvent.GetId() - ID_GIT_SWITCH_BRANCH;
1920
1921 if( branchIndex < 0 || static_cast<size_t>( branchIndex ) >= branches.size() )
1922 return;
1923
1924 branchName = branches[branchIndex];
1925 }
1926
1927 // Retrieve the reference to the existing branch using libgit2
1928 git_reference* branchRef = nullptr;
1929
1930 if( git_reference_lookup( &branchRef, repo, branchName.mb_str() ) != GIT_OK &&
1931 git_reference_dwim( &branchRef, repo, branchName.mb_str() ) != GIT_OK )
1932 {
1933 wxString errorMessage = wxString::Format( _( "Failed to lookup branch '%s': %s" ),
1934 branchName, KIGIT_COMMON::GetLastGitError() );
1935 DisplayError( m_parent, errorMessage );
1936 return;
1937 }
1938
1939 KIGIT::GitReferencePtr branchRefPtr( branchRef );
1940 const char* branchRefName = git_reference_name( branchRef );
1941 git_object* branchObj = nullptr;
1942
1943 if( git_revparse_single( &branchObj, repo, branchName.mb_str() ) != 0 )
1944 {
1945 wxString errorMessage =
1946 wxString::Format( _( "Failed to find branch head for '%s'" ), branchName );
1947 DisplayError( m_parent, errorMessage );
1948 return;
1949 }
1950
1951 KIGIT::GitObjectPtr branchObjPtr( branchObj );
1952
1953 // Switch to the branch
1954 if( git_checkout_tree( repo, branchObj, nullptr ) != 0 )
1955 {
1956 wxString errorMessage =
1957 wxString::Format( _( "Failed to switch to branch '%s'" ), branchName );
1958 DisplayError( m_parent, errorMessage );
1959 return;
1960 }
1961
1962 // Update the HEAD reference
1963 if( git_repository_set_head( repo, branchRefName ) != 0 )
1964 {
1965 wxString errorMessage = wxString::Format(
1966 _( "Failed to update HEAD reference for branch '%s'" ), branchName );
1967 DisplayError( m_parent, errorMessage );
1968 return;
1969 }
1970}
1971
1972
1973void PROJECT_TREE_PANE::onGitRemoveVCS( wxCommandEvent& aEvent )
1974{
1975 git_repository* repo = m_TreeProject->GetGitRepo();
1976
1977 if( !repo
1978 || !IsOK( wxGetTopLevelParent( this ),
1979 _( "Are you sure you want to remove Git tracking from this project?" ) ) )
1980 {
1981 return;
1982 }
1983
1984 // Remove the VCS (git) from the project directory
1985 git_repository_free( repo );
1986 m_TreeProject->SetGitRepo( nullptr );
1987
1988 // Remove the .git directory
1989 wxFileName fn( m_Parent->GetProjectFileName() );
1990 fn.AppendDir( ".git" );
1991
1992 wxString errors;
1993
1994 if( !RmDirRecursive( fn.GetPath(), &errors ) )
1995 {
1996 DisplayErrorMessage( m_parent, _( "Failed to remove Git directory" ), errors );
1997 }
1998
1999 // Clear all item states
2000 std::stack<wxTreeItemId> items;
2001 items.push( m_TreeProject->GetRootItem() );
2002
2003 while( !items.empty() )
2004 {
2005 wxTreeItemId current = items.top();
2006 items.pop();
2007
2008 // Process the current item
2009 m_TreeProject->SetItemState( current, wxTREE_ITEMSTATE_NONE );
2010
2011 wxTreeItemIdValue cookie;
2012 wxTreeItemId child = m_TreeProject->GetFirstChild( current, cookie );
2013
2014 while( child.IsOk() )
2015 {
2016 items.push( child );
2017 child = m_TreeProject->GetNextChild( current, cookie );
2018 }
2019 }
2020}
2021
2022
2024{
2025 wxLogTrace( traceGit, wxS( "updateGitStatusIcons: Updating git status icons" ) );
2026 std::unique_lock<std::mutex> lock( m_gitStatusMutex, std::try_to_lock );
2027
2028 if( !lock.owns_lock() )
2029 {
2030 wxLogTrace( traceGit, wxS( "updateGitStatusIcons: Failed to acquire lock for git status icon update" ) );
2031 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
2032 return;
2033 }
2034
2035 if( !Pgm().GetCommonSettings()->m_Git.enableGit || !m_TreeProject )
2036 {
2037 wxLogTrace( traceGit, wxS( "updateGitStatusIcons: Git is disabled or tree control is null" ) );
2038 return;
2039 }
2040
2041 std::stack<wxTreeItemId> items;
2042 items.push(m_TreeProject->GetRootItem());
2043
2044 while( !items.empty() )
2045 {
2046 wxTreeItemId current = items.top();
2047 items.pop();
2048
2049 if( m_TreeProject->ItemHasChildren( current ) )
2050 {
2051 wxTreeItemIdValue cookie;
2052 wxTreeItemId child = m_TreeProject->GetFirstChild( current, cookie );
2053
2054 while( child.IsOk() )
2055 {
2056 items.push( child );
2057
2058 if( auto it = m_gitStatusIcons.find( child ); it != m_gitStatusIcons.end() )
2059 {
2060 m_TreeProject->SetItemState( child, static_cast<int>( it->second ) );
2061 }
2062
2063 child = m_TreeProject->GetNextChild( current, cookie );
2064 }
2065 }
2066 }
2067
2068 if (!m_gitCurrentBranchName.empty())
2069 {
2070 wxTreeItemId kid = m_TreeProject->GetRootItem();
2071 PROJECT_TREE_ITEM* rootItem = GetItemIdData( kid );
2072 wxString filename = wxFileNameFromPath( rootItem->GetFileName() );
2073 m_TreeProject->SetItemText( kid, filename + " [" + m_gitCurrentBranchName + "]" );
2074 m_gitIconsInitialized = true;
2075 }
2076
2077 wxLogTrace( traceGit, wxS( "updateGitStatusIcons: Git status icons updated" ) );
2078}
2079
2080
2082{
2083 wxLogTrace( traceGit, wxS( "updateTreeCache: Updating tree cache" ) );
2084
2085 std::unique_lock<std::mutex> lock( m_gitTreeCacheMutex, std::try_to_lock );
2086
2087 if( !lock.owns_lock() )
2088 {
2089 wxLogTrace( traceGit, wxS( "updateTreeCache: Failed to acquire lock for tree cache update" ) );
2090 return;
2091 }
2092
2093 if( !m_TreeProject )
2094 {
2095 wxLogTrace( traceGit, wxS( "updateTreeCache: Tree control is null" ) );
2096 return;
2097 }
2098
2099 wxTreeItemId kid = m_TreeProject->GetRootItem();
2100
2101 if( !kid.IsOk() )
2102 return;
2103
2104 // Collect a map to easily set the state of each item
2105 m_gitTreeCache.clear();
2106 std::stack<wxTreeItemId> items;
2107 items.push( kid );
2108
2109 while( !items.empty() )
2110 {
2111 kid = items.top();
2112 items.pop();
2113
2114 PROJECT_TREE_ITEM* nextItem = GetItemIdData( kid );
2115
2116 wxString gitAbsPath = nextItem->GetFileName();
2117#ifdef _WIN32
2118 gitAbsPath.Replace( wxS( "\\" ), wxS( "/" ) );
2119#endif
2120 m_gitTreeCache[gitAbsPath] = kid;
2121
2122 wxTreeItemIdValue cookie;
2123 wxTreeItemId child = m_TreeProject->GetFirstChild( kid, cookie );
2124
2125 while( child.IsOk() )
2126 {
2127 items.push( child );
2128 child = m_TreeProject->GetNextChild( kid, cookie );
2129 }
2130 }
2131}
2132
2133
2135{
2136 wxLogTrace( traceGit, wxS( "updateGitStatusIconMap: Updating git status icons" ) );
2137#if defined( _WIN32 )
2139
2140 if( refresh != 0
2142 {
2143 // Need to treat windows network paths special here until we get the samba bug fixed
2144 // https://github.com/wxWidgets/wxWidgets/issues/18953
2145 CallAfter(
2146 [this, refresh]()
2147 {
2148 m_gitStatusTimer.Start( refresh, wxTIMER_ONE_SHOT );
2149 } );
2150 }
2151
2152#endif
2153
2154 if( !Pgm().GetCommonSettings()->m_Git.enableGit || !m_TreeProject )
2155 return;
2156
2157 std::unique_lock<std::mutex> lock1( m_gitStatusMutex, std::try_to_lock );
2158 std::unique_lock<std::mutex> lock2( m_gitTreeCacheMutex, std::try_to_lock );
2159
2160 if( !lock1.owns_lock() || !lock2.owns_lock() )
2161 {
2162 wxLogTrace( traceGit, wxS( "updateGitStatusIconMap: Failed to acquire locks for git status icon update" ) );
2163 return;
2164 }
2165
2166 git_repository* repo = m_TreeProject->GetGitRepo();
2167
2168 if( !repo )
2169 {
2170 wxLogTrace( traceGit, wxS( "updateGitStatusIconMap: No git repository found" ) );
2171 return;
2172 }
2173
2174 // Get Current Branch
2175 wxFileName rootFilename( Prj().GetProjectFullName() );
2176 wxString repoWorkDir( git_repository_workdir( repo ) );
2177
2178 wxFileName relative = rootFilename;
2179 relative.MakeRelativeTo( repoWorkDir );
2180 wxString pathspecStr = relative.GetPath( wxPATH_GET_VOLUME | wxPATH_GET_SEPARATOR );
2181
2182#ifdef _WIN32
2183 pathspecStr.Replace( wxS( "\\" ), wxS( "/" ) );
2184#endif
2185
2186 const char* pathspec[] = { pathspecStr.c_str().AsChar() };
2187
2188 git_status_options status_options;
2189 git_status_init_options( &status_options, GIT_STATUS_OPTIONS_VERSION );
2190 status_options.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
2191 status_options.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED | GIT_STATUS_OPT_INCLUDE_UNMODIFIED;
2192 status_options.pathspec = { (char**) pathspec, 1 };
2193
2194 git_index* index = nullptr;
2195
2196 if( git_repository_index( &index, repo ) != GIT_OK )
2197 {
2198 m_gitLastError = giterr_last()->klass;
2199 wxLogTrace( traceGit, wxS( "Failed to get git index: %s" ), KIGIT_COMMON::GetLastGitError() );
2200 return;
2201 }
2202
2203 KIGIT::GitIndexPtr indexPtr( index );
2204 git_status_list* status_list = nullptr;
2205
2206 if( git_status_list_new( &status_list, repo, &status_options ) != GIT_OK )
2207 {
2208 wxLogTrace( traceGit, wxS( "Failed to get git status list: %s" ), KIGIT_COMMON::GetLastGitError() );
2209 return;
2210 }
2211
2212 KIGIT::GitStatusListPtr statusListPtr( status_list );
2213 auto [localChanges, remoteChanges] = m_TreeProject->GitCommon()->GetDifferentFiles();
2214
2215 size_t count = git_status_list_entrycount( status_list );
2216 bool updated = false;
2217
2218 for( size_t ii = 0; ii < count; ++ii )
2219 {
2220 const git_status_entry* entry = git_status_byindex( status_list, ii );
2221 std::string path( entry->head_to_index? entry->head_to_index->old_file.path
2222 : entry->index_to_workdir->old_file.path );
2223
2224 wxString absPath = repoWorkDir;
2225 absPath << path;
2226
2227 auto iter = m_gitTreeCache.find( absPath );
2228
2229 if( iter == m_gitTreeCache.end() )
2230 {
2231 wxLogTrace( traceGit, wxS( "File '%s' not found in tree cache" ), absPath );
2232 continue;
2233 }
2234
2235 // If we are current, don't continue because we still need to check to see if the
2236 // current commit is ahead/behind the remote. If the file is modified/added/deleted,
2237 // that is the main status we want to show.
2238 if( entry->status & GIT_STATUS_IGNORED )
2239 {
2240 wxLogTrace( traceGit, wxS( "File '%s' is ignored" ), absPath );
2241 auto [it, inserted] = m_gitStatusIcons.try_emplace( iter->second,
2243
2244 if( inserted || it->second != KIGIT_COMMON::GIT_STATUS::GIT_STATUS_IGNORED )
2245 updated = true;
2246
2248 }
2249 else if( entry->status & ( GIT_STATUS_INDEX_MODIFIED | GIT_STATUS_WT_MODIFIED ) )
2250 {
2251 wxLogTrace( traceGit, wxS( "File '%s' is modified in %s" ),
2252 absPath, ( entry->status & GIT_STATUS_INDEX_MODIFIED )? "index" : "working tree" );
2253 auto [it, inserted] = m_gitStatusIcons.try_emplace( iter->second,
2255
2256 if( inserted || it->second != KIGIT_COMMON::GIT_STATUS::GIT_STATUS_MODIFIED )
2257 updated = true;
2258
2260 }
2261 else if( entry->status & ( GIT_STATUS_INDEX_NEW | GIT_STATUS_WT_NEW ) )
2262 {
2263 wxLogTrace( traceGit, wxS( "File '%s' is new in %s" ),
2264 absPath, ( entry->status & GIT_STATUS_INDEX_NEW )? "index" : "working tree" );
2265 auto [it, inserted] = m_gitStatusIcons.try_emplace( iter->second,
2267
2268 if( inserted || it->second != KIGIT_COMMON::GIT_STATUS::GIT_STATUS_ADDED )
2269 updated = true;
2270
2272 }
2273 else if( entry->status & ( GIT_STATUS_INDEX_DELETED | GIT_STATUS_WT_DELETED ) )
2274 {
2275 wxLogTrace( traceGit, wxS( "File '%s' is deleted in %s" ),
2276 absPath, ( entry->status & GIT_STATUS_INDEX_DELETED )? "index" : "working tree" );
2277 auto [it, inserted] = m_gitStatusIcons.try_emplace( iter->second,
2279
2280 if( inserted || it->second != KIGIT_COMMON::GIT_STATUS::GIT_STATUS_DELETED )
2281 updated = true;
2282
2284 }
2285 else if( localChanges.count( path ) )
2286 {
2287 wxLogTrace( traceGit, wxS( "File '%s' is ahead of remote" ), absPath );
2288 auto [it, inserted] = m_gitStatusIcons.try_emplace( iter->second,
2290
2291 if( inserted || it->second != KIGIT_COMMON::GIT_STATUS::GIT_STATUS_AHEAD )
2292 updated = true;
2293
2295 }
2296 else if( remoteChanges.count( path ) )
2297 {
2298 wxLogTrace( traceGit, wxS( "File '%s' is behind remote" ), absPath );
2299 auto [it, inserted] = m_gitStatusIcons.try_emplace( iter->second,
2301
2302 if( inserted || it->second != KIGIT_COMMON::GIT_STATUS::GIT_STATUS_BEHIND )
2303 updated = true;
2304
2306 }
2307 else
2308 {
2309 // If we are here, the file is unmodified and not ignored
2310 auto [it, inserted] = m_gitStatusIcons.try_emplace( iter->second,
2312
2313 if( inserted || it->second != KIGIT_COMMON::GIT_STATUS::GIT_STATUS_CURRENT )
2314 updated = true;
2315
2317 }
2318 }
2319
2320 git_reference* currentBranchReference = nullptr;
2321 int rc = git_repository_head( &currentBranchReference, repo );
2322 KIGIT::GitReferencePtr currentBranchReferencePtr( currentBranchReference );
2323
2324 // Get the current branch name
2325 if( currentBranchReference )
2326 {
2327 m_gitCurrentBranchName = git_reference_shorthand( currentBranchReference );
2328 }
2329 else if( rc == GIT_EUNBORNBRANCH )
2330 {
2331 // TODO: couldn't immediately figure out if libgit2 can return the name of an unborn branch
2332 // For now, just do nothing
2333 }
2334 else
2335 {
2336 if( giterr_last()->klass != m_gitLastError )
2337 wxLogTrace( "git", "Failed to lookup current branch: %s", KIGIT_COMMON::GetLastGitError() );
2338
2339 m_gitLastError = giterr_last()->klass;
2340 }
2341
2342 wxLogTrace( traceGit, wxS( "updateGitStatusIconMap: Updated git status icons" ) );
2343 // If the icons are not changed, queue an event to update in the main thread
2344 if( updated || !m_gitIconsInitialized )
2345 {
2346 CallAfter(
2347 [this]()
2348 {
2350 } );
2351 }
2352}
2353
2354
2355void PROJECT_TREE_PANE::onGitCommit( wxCommandEvent& aEvent )
2356{
2357 std::vector<PROJECT_TREE_ITEM*> tree_data = GetSelectedData();
2358
2359 git_repository* repo = m_TreeProject->GetGitRepo();
2360
2361 if( repo == nullptr )
2362 {
2363 wxMessageBox( _( "The selected directory is not a Git project." ) );
2364 return;
2365 }
2366
2367 git_config* config = nullptr;
2368 git_repository_config( &config, repo );
2369 KIGIT::GitConfigPtr configPtr( config );
2370
2371 // Read relevant data from the git config
2372 wxString authorName;
2373 wxString authorEmail;
2374
2375 // Read author name
2376 git_config_entry* name_c = nullptr;
2377 git_config_entry* email_c = nullptr;
2378 int authorNameError = git_config_get_entry( &name_c, config, "user.name" );
2379 KIGIT::GitConfigEntryPtr namePtr( name_c );
2380
2381 if( authorNameError != 0 || name_c == nullptr )
2382 {
2383 authorName = Pgm().GetCommonSettings()->m_Git.authorName;
2384 }
2385 else
2386 {
2387 authorName = name_c->value;
2388 }
2389
2390 // Read author email
2391 int authorEmailError = git_config_get_entry( &email_c, config, "user.email" );
2392
2393 if( authorEmailError != 0 || email_c == nullptr )
2394 {
2395 authorEmail = Pgm().GetCommonSettings()->m_Git.authorEmail;
2396 }
2397 else
2398 {
2399 authorEmail = email_c->value;
2400 }
2401
2402 // Collect modified files in the repository
2403 git_status_options status_options;
2404 git_status_init_options( &status_options, GIT_STATUS_OPTIONS_VERSION );
2405 status_options.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
2406 status_options.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED;
2407
2408 git_status_list* status_list = nullptr;
2409 git_status_list_new( &status_list, repo, &status_options );
2410 KIGIT::GitStatusListPtr statusListPtr( status_list );
2411
2412 std::map<wxString, int> modifiedFiles;
2413
2414 size_t count = git_status_list_entrycount( status_list );
2415
2416 std::set<wxString> selected_files;
2417
2418 for( PROJECT_TREE_ITEM* item : tree_data )
2419 {
2420 if( item->GetType() != TREE_FILE_TYPE::DIRECTORY )
2421 selected_files.emplace( item->GetFileName() );
2422 }
2423
2424 for( size_t i = 0; i < count; ++i )
2425 {
2426 const git_status_entry* entry = git_status_byindex( status_list, i );
2427
2428 // Check if the file is modified (index or workdir changes)
2429 if( entry->status == GIT_STATUS_CURRENT
2430 || ( entry->status & ( GIT_STATUS_CONFLICTED | GIT_STATUS_IGNORED ) ) )
2431 {
2432 continue;
2433 }
2434
2435 wxFileName fn;
2436 wxString filePath;
2437
2438 // TODO: we are kind of erasing the difference between workdir and index here,
2439 // because the Commit dialog doesn't show that difference.
2440 // Entry may only have a head_to_index if it was previously staged
2441 if( entry->index_to_workdir )
2442 {
2443 fn.Assign( entry->index_to_workdir->old_file.path );
2444 fn.MakeAbsolute( git_repository_workdir( repo ) );
2445 filePath = wxString( entry->index_to_workdir->old_file.path, wxConvUTF8 );
2446 }
2447 else if( entry->head_to_index )
2448 {
2449 fn.Assign( entry->head_to_index->old_file.path );
2450 fn.MakeAbsolute( git_repository_workdir( repo ) );
2451 filePath = wxString( entry->head_to_index->old_file.path, wxConvUTF8 );
2452 }
2453 else
2454 {
2455 wxCHECK2_MSG( false, continue, "File status with neither git_status_entry set!" );
2456 }
2457
2458 // Do not commit files outside the project directory
2459 wxString projectPath = Prj().GetProjectPath();
2460 wxString fileName = fn.GetFullPath();
2461
2462 if( !fileName.StartsWith( projectPath ) )
2463 continue;
2464
2465 // Skip lock files
2466 if( fn.GetExt().CmpNoCase( FILEEXT::LockFileExtension ) == 0 )
2467 continue;
2468
2469 // Skip autosave, lock, and backup files
2470 if( fn.GetName().StartsWith( FILEEXT::AutoSaveFilePrefix )
2471 || fn.GetName().StartsWith( FILEEXT::LockFilePrefix )
2472 || fn.GetName().EndsWith( FILEEXT::BackupFileSuffix ) )
2473 {
2474 continue;
2475 }
2476
2477 // Skip archived project backups
2478 if( fn.GetPath().Contains( Prj().GetProjectName() + wxT( "-backups" ) ) )
2479 continue;
2480
2481 if( aEvent.GetId() == ID_GIT_COMMIT_PROJECT )
2482 {
2483 modifiedFiles.emplace( filePath, entry->status );
2484 }
2485 else if( selected_files.count( fn.GetFullPath() ) )
2486 {
2487 modifiedFiles.emplace( filePath, entry->status );
2488 }
2489 }
2490
2491 // Create a commit dialog
2492 DIALOG_GIT_COMMIT dlg( wxGetTopLevelParent( this ), repo, authorName, authorEmail,
2493 modifiedFiles );
2494 auto ret = dlg.ShowModal();
2495
2496 if( ret != wxID_OK )
2497 return;
2498
2499 // Commit the changes
2500 git_oid tree_id;
2501 git_tree* tree = nullptr;
2502 git_commit* parent = nullptr;
2503 git_index* index = nullptr;
2504
2505 std::vector<wxString> files = dlg.GetSelectedFiles();
2506
2507 if( dlg.GetCommitMessage().IsEmpty() )
2508 {
2509 wxMessageBox( _( "Discarding commit due to empty commit message." ) );
2510 return;
2511 }
2512
2513 if( files.empty() )
2514 {
2515 wxMessageBox( _( "Discarding commit due to empty file selection." ) );
2516 return;
2517 }
2518
2519 if( git_repository_index( &index, repo ) != 0 )
2520 {
2521 wxLogTrace( traceGit, wxString::Format( _( "Failed to get repository index: %s" ),
2523 return;
2524 }
2525
2526 KIGIT::GitIndexPtr indexPtr( index );
2527
2528 for( wxString& file : files )
2529 {
2530 if( git_index_add_bypath( index, file.mb_str() ) != 0 )
2531 {
2532 wxMessageBox( wxString::Format( _( "Failed to add file to index: %s" ),
2534 return;
2535 }
2536 }
2537
2538 if( git_index_write( index ) != 0 )
2539 {
2540 wxLogTrace( traceGit, wxString::Format( _( "Failed to write index: %s" ),
2542 return;
2543 }
2544
2545 if( git_index_write_tree( &tree_id, index ) != 0)
2546 {
2547 wxLogTrace( traceGit, wxString::Format( _( "Failed to write tree: %s" ),
2549 return;
2550 }
2551
2552 if( git_tree_lookup( &tree, repo, &tree_id ) != 0 )
2553 {
2554 wxLogTrace( traceGit, wxString::Format( _( "Failed to lookup tree: %s" ),
2556 return;
2557 }
2558
2559 KIGIT::GitTreePtr treePtr( tree );
2560 git_reference* headRef = nullptr;
2561
2562 if( git_repository_head_unborn( repo ) == 0 )
2563 {
2564 if( git_repository_head( &headRef, repo ) != 0 )
2565 {
2566 wxLogTrace( traceGit, wxString::Format( _( "Failed to get HEAD reference: %s" ),
2568 return;
2569 }
2570
2571 KIGIT::GitReferencePtr headRefPtr( headRef );
2572
2573 if( git_reference_peel( (git_object**) &parent, headRef, GIT_OBJECT_COMMIT ) != 0 )
2574 {
2575 wxLogTrace( traceGit, wxString::Format( _( "Failed to get commit: %s" ),
2577 return;
2578 }
2579 }
2580
2581 KIGIT::GitCommitPtr parentPtr( parent );
2582 const wxString& commit_msg = dlg.GetCommitMessage();
2583 const wxString& author_name = dlg.GetAuthorName();
2584 const wxString& author_email = dlg.GetAuthorEmail();
2585
2586 git_signature* author = nullptr;
2587
2588 if( git_signature_now( &author, author_name.mb_str(), author_email.mb_str() ) != 0 )
2589 {
2590 wxLogTrace( traceGit, wxString::Format( _( "Failed to create author signature: %s" ),
2592 return;
2593 }
2594
2595 KIGIT::GitSignaturePtr authorPtr( author );
2596 git_oid oid;
2597
2598#if( LIBGIT2_VER_MAJOR == 1 && LIBGIT2_VER_MINOR == 8 \
2599 && ( LIBGIT2_VER_REVISION < 2 || LIBGIT2_VER_REVISION == 3 ) )
2600 /*
2601 * For libgit2 versions 1.8.0, 1.8.1. (cf19ddc52)
2602 * This change was reverted for 1.8.2 (49d3fadfc, main branch)
2603 * The revert for 1.8.2 was not included for 1.8.3 (which is on the maint/v1.8 branch, not main)
2604 * This change was also reverted for 1.8.4 (94ba816f6, also maint/v1.8 branch)
2605 *
2606 * As of 1.8.4, the history is like this:
2607 *
2608 * * 3f4182d15 (tag: v1.8.4, maint/v1.8)
2609 * * 94ba816f6 Revert "commit: fix const declaration" [puts const back]
2610 * * 3353f78e8 (tag: v1.8.3)
2611 * | * 4ce872a0f (tag: v1.8.2-rc1, tag: v1.8.2)
2612 * | * 49d3fadfc Revert "commit: fix const declaration" [puts const back]
2613 * |/
2614 * * 36f7e21ad (tag: v1.8.1)
2615 * * d74d49148 (tag: v1.8.0)
2616 * * cf19ddc52 commit: fix const declaration [removes const]
2617 */
2618 git_commit* const parents[1] = { parent };
2619#else
2620 // For libgit2 versions older than 1.8.0, or equal to 1.8.2, or 1.8.4+
2621 const git_commit* parents[1] = { parent };
2622#endif
2623
2624 if( git_commit_create( &oid, repo, "HEAD", author, author, nullptr, commit_msg.mb_str(), tree,
2625 1, parents ) != 0 )
2626 {
2627 wxMessageBox( wxString::Format( _( "Failed to create commit: %s" ),
2629 return;
2630 }
2631
2632 wxLogTrace( traceGit, wxString::Format( _( "Created commit with id: %s" ),
2633 git_oid_tostr_s( &oid ) ) );
2634
2635 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
2636}
2637
2638
2639void PROJECT_TREE_PANE::onGitAddToIndex( wxCommandEvent& aEvent )
2640{
2641
2642}
2643
2644
2645bool PROJECT_TREE_PANE::canFileBeAddedToVCS( const wxString& aFile )
2646{
2647 git_index *index;
2648 size_t entry_pos;
2649
2650 git_repository* repo = m_TreeProject->GetGitRepo();
2651
2652 if( !repo )
2653 return false;
2654
2655 if( git_repository_index( &index, repo ) != 0 )
2656 {
2657 wxLogTrace( traceGit, "Failed to get git index: %s", KIGIT_COMMON::GetLastGitError() );
2658 return false;
2659 }
2660
2661 KIGIT::GitIndexPtr indexPtr( index );
2662
2663 // If we successfully find the file in the index, we may not add it to the VCS
2664 if( git_index_find( &entry_pos, index, aFile.mb_str() ) == 0 )
2665 {
2666 wxLogTrace( traceGit, "File already in index: %s", aFile );
2667 return false;
2668 }
2669
2670 return true;
2671}
2672
2673
2674void PROJECT_TREE_PANE::onGitSyncProject( wxCommandEvent& aEvent )
2675{
2676 wxLogTrace( traceGit, "Syncing project" );
2677 git_repository* repo = m_TreeProject->GetGitRepo();
2678
2679 if( !repo )
2680 {
2681 wxLogTrace( traceGit, "sync: No git repository found" );
2682 return;
2683 }
2684
2685 GIT_SYNC_HANDLER handler( repo );
2686 handler.PerformSync();
2687}
2688
2689
2690void PROJECT_TREE_PANE::onGitFetch( wxCommandEvent& aEvent )
2691{
2692 KIGIT_COMMON* gitCommon = m_TreeProject->GitCommon();
2693
2694 if( !gitCommon )
2695 return;
2696
2697 GIT_PULL_HANDLER handler( gitCommon );
2698 handler.PerformFetch();
2699
2700 m_gitStatusTimer.Start( 500, wxTIMER_ONE_SHOT );
2701}
2702
2703
2704void PROJECT_TREE_PANE::onGitResolveConflict( wxCommandEvent& aEvent )
2705{
2706 git_repository* repo = m_TreeProject->GetGitRepo();
2707
2708 if( !repo )
2709 return;
2710
2711 GIT_RESOLVE_CONFLICT_HANDLER handler( repo );
2712 handler.PerformResolveConflict();
2713}
2714
2715
2716void PROJECT_TREE_PANE::onGitRevertLocal( wxCommandEvent& aEvent )
2717{
2718 git_repository* repo = m_TreeProject->GetGitRepo();
2719
2720 if( !repo )
2721 return;
2722
2723 GIT_REVERT_HANDLER handler( repo );
2724 handler.PerformRevert();
2725}
2726
2727
2728void PROJECT_TREE_PANE::onGitRemoveFromIndex( wxCommandEvent& aEvent )
2729{
2730 git_repository* repo = m_TreeProject->GetGitRepo();
2731
2732 if( !repo )
2733 return;
2734
2735 GIT_REMOVE_FROM_INDEX_HANDLER handler( repo );
2736 handler.PerformRemoveFromIndex();
2737}
2738
2739
2741{
2742
2743}
2744
2745
2746void PROJECT_TREE_PANE::onGitSyncTimer( wxTimerEvent& aEvent )
2747{
2748 wxLogTrace( traceGit, "onGitSyncTimer" );
2749 COMMON_SETTINGS::GIT& gitSettings = Pgm().GetCommonSettings()->m_Git;
2750
2751 if( !gitSettings.enableGit || !m_TreeProject )
2752 return;
2753
2755
2756 tp.push_task( [this]()
2757 {
2758 KIGIT_COMMON* gitCommon = m_TreeProject->GitCommon();
2759
2760 if( !gitCommon )
2761 {
2762 wxLogTrace( traceGit, "onGitSyncTimer: No git repository found" );
2763 return;
2764 }
2765
2766 GIT_PULL_HANDLER handler( gitCommon );
2767 handler.PerformFetch();
2768
2769 CallAfter( [this]()
2770 {
2772 } );
2773 } );
2774
2775 if( gitSettings.updatInterval > 0 )
2776 {
2777 wxLogTrace( traceGit, "onGitSyncTimer: Restarting git sync timer" );
2778 // We store the timer interval in minutes but wxTimer uses milliseconds
2779 m_gitSyncTimer.Start( gitSettings.updatInterval * 60 * 1000, wxTIMER_ONE_SHOT );
2780 }
2781}
2782
2783
2785{
2788
2789 tp.push_task(
2790 [this]()
2791 {
2793 } );
2794}
2795
2796void PROJECT_TREE_PANE::onGitStatusTimer( wxTimerEvent& aEvent )
2797{
2798 wxLogTrace( traceGit, "onGitStatusTimer" );
2799 if( !Pgm().GetCommonSettings()->m_Git.enableGit || !m_TreeProject )
2800 return;
2801
2803}
const char * name
Definition: DXF_plotter.cpp:62
wxBitmap KiBitmap(BITMAPS aBitmap, int aHeightTag)
Construct a wxBitmap from an image identifier Returns the image from the active theme if the image ha...
Definition: bitmap.cpp:104
BITMAP_STORE * GetBitmapStore()
Definition: bitmap.cpp:92
static const ADVANCED_CFG & GetCfg()
Get the singleton instance's config, which is shared by all consumers.
void ThemeChanged()
Notifies the store that the icon theme has been changed by the user, so caches must be invalidated.
wxString GetCommitMessage() const
wxString GetAuthorEmail() const
std::vector< wxString > GetSelectedFiles() const
wxString GetAuthorName() const
KIGIT_COMMON::GIT_CONN_TYPE GetRepoType() const
wxString GetBareRepoURL() const
Get the Bare Repo U R L object.
wxString GetRepoSSHPath() const
wxString GetUsername() const
wxString GetPassword() const
wxString GetBranchName() const
int ShowModal() override
void SetProgressReporter(std::unique_ptr< WX_PROGRESS_REPORTER > aProgressReporter)
Definition: git_progress.h:40
bool PerformFetch(bool aSkipLock=false)
PullResult PerformPull()
PushResult PerformPush()
The main KiCad project manager frame.
PROJECT_TREE_PANE * m_leftWin
const wxString GetProjectFileName() const
void OnChangeWatchedPaths(wxCommandEvent &aEvent)
Called by sending a event with id = ID_INIT_WATCHED_PATHS rebuild the list of watched paths.
void LoadProject(const wxFileName &aProjectFileName)
std::mutex m_gitActionMutex
std::vector< wxString > GetBranchNames() const
static wxString GetLastGitError()
wxString GetGitRootDirectory() const
void SetSSHKey(const wxString &aSSHKey)
void SetUsername(const wxString &aUsername)
std::pair< std::set< wxString >, std::set< wxString > > GetDifferentFiles() const
Return a pair of sets of files that differ locally from the remote repository The first set is files ...
bool HasPushAndPullRemote() const
void UpdateCurrentBranchInfo()
bool HasLocalCommits() const
void SetPassword(const wxString &aPassword)
wxString GetErrorString()
KISTATUSBAR is a wxStatusBar suitable for Kicad manager.
Definition: kistatusbar.h:45
void SetEllipsedTextField(const wxString &aText, int aFieldId)
Set the text in a field using wxELLIPSIZE_MIDDLE option to adjust the text size to the field size.
static wxString GetDefaultUserProjectsPath()
Gets the default path we point users to create projects.
Definition: paths.cpp:143
virtual COMMON_SETTINGS * GetCommonSettings() const
Definition: pgm_base.cpp:556
virtual const wxString & GetTextEditor(bool aCanShowFileChooser=true)
Return the path to the preferred text editor application.
Definition: pgm_base.cpp:188
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 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 ...
wxString m_gitCurrentBranchName
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
KICAD_MANAGER_FRAME * m_Parent
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.
std::mutex m_gitStatusMutex
void onGitRevertLocal(wxCommandEvent &event)
Revert the local repository to the last commit.
void onGitPullProject(wxCommandEvent &event)
Pull the latest changes from the git repository.
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 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.
void updateGitStatusIconMap()
This is a threaded call that will change the map of git status icons for use in the main thread.
PROJECT_TREE_ITEM * GetItemIdData(wxTreeItemId aId)
Function GetItemIdData return the item data corresponding to a wxTreeItemId identifier.
void onGitSwitchBranch(wxCommandEvent &event)
Switch to a different branch in the git repository.
void onCreateNewDirectory(wxCommandEvent &event)
Function onCreateNewDirectory Creates a new subdirectory inside the current kicad project directory t...
void onSwitchToSelectedProject(wxCommandEvent &event)
Switch to a other project selected from the tree project (by selecting an other .pro file inside the ...
void onPaint(wxPaintEvent &aEvent)
We don't have uniform borders so we have to draw them ourselves.
void onRenameFile(wxCommandEvent &event)
Function onRenameFile Rename the selected file or directory in the tree project.
void onGitPushProject(wxCommandEvent &event)
Push the current project changes to the git repository.
bool canFileBeAddedToVCS(const wxString &aFilePath)
Returns true if the file has already been added to the repository or false if it has not been added y...
std::vector< wxString > m_filters
PROJECT_TREE This is the class to show (as a tree) the files in the project directory.
Definition: project_tree.h:43
git_repository * GetGitRepo() const
Definition: project_tree.h:61
KIGIT_COMMON * GitCommon() const
Definition: project_tree.h:63
void LoadIcons()
void SetGitRepo(git_repository *aRepo)
Definition: project_tree.h:60
virtual const wxString GetProjectPath() const
Return the full path of the project.
Definition: project.cpp:148
virtual PROJECT_LOCAL_SETTINGS & GetLocalSettings() const
Definition: project.h:210
bool IsOK(wxWindow *aParent, const wxString &aMessage)
Display a yes/no dialog with aMessage and returns the user response.
Definition: confirm.cpp:260
void DisplayInfoMessage(wxWindow *aParent, const wxString &aMessage, const wxString &aExtraInfo)
Display an informational message box with aMessage.
Definition: confirm.cpp:231
void DisplayErrorMessage(wxWindow *aParent, const wxString &aText, const wxString &aExtraInfo)
Display an error message with aMessage.
Definition: confirm.cpp:203
void DisplayError(wxWindow *aParent, const wxString &aText)
Display an error or warning message box with aMessage.
Definition: confirm.cpp:178
This file is part of the common library.
#define _(s)
EVT_MENU_RANGE(ID_GERBVIEW_DRILL_FILE1, ID_GERBVIEW_DRILL_FILEMAX, GERBVIEW_FRAME::OnDrlFileHistory) EVT_MENU_RANGE(ID_GERBVIEW_ZIP_FILE1
int ExecuteFile(const wxString &aEditorName, const wxString &aFileName, wxProcess *aCallback, bool aFileForKicad)
Call the executable file aEditorName with the parameter aFileName.
Definition: gestfich.cpp:143
bool RmDirRecursive(const wxString &aFileName, wxString *aErrors)
Remove the directory aDirName and all its contents including subdirectories and their files.
Definition: gestfich.cpp:311
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 KiCadSchematicFileExtension
static const std::string LegacySymbolLibFileExtension
static const std::string LockFilePrefix
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 AutoSaveFilePrefix
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:597
#define GIT_BUF_INIT
@ ID_PROJECT_TREE
Definition: kicad_id.h:36
@ ID_LEFT_FRAME
Definition: kicad_id.h:35
bool LaunchExternal(const wxString &aPath)
Launches the given file or folder in the host OS.
Definition: launch_ext.cpp:25
This file contains miscellaneous commonly used macros and functions.
#define KI_FALLTHROUGH
The KI_FALLTHROUGH macro is to be used when switch statement cases should purposely fallthrough from ...
Definition: macros.h:83
std::unique_ptr< git_object, decltype([](git_object *aObject) { git_object_free(aObject) GitObjectPtr
A unique pointer for git_object objects with automatic cleanup.
std::unique_ptr< git_status_list, decltype([](git_status_list *aList) { git_status_list_free(aList) GitStatusListPtr
A unique pointer for git_status_list objects with automatic cleanup.
std::unique_ptr< git_commit, decltype([](git_commit *aCommit) { git_commit_free(aCommit) GitCommitPtr
A unique pointer for git_commit objects with automatic cleanup.
std::unique_ptr< git_buf, decltype([](git_buf *aBuf) { git_buf_free(aBuf) GitBufPtr
A unique pointer for git_buf objects with automatic cleanup.
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_config_entry, decltype([](git_config_entry *aEntry) { git_config_entry_free(aEntry) GitConfigEntryPtr
A unique pointer for git_config_entry objects with automatic cleanup.
std::unique_ptr< git_index, decltype([](git_index *aIndex) { git_index_free(aIndex) GitIndexPtr
A unique pointer for git_index objects with automatic cleanup.
std::unique_ptr< git_signature, decltype([](git_signature *aSignature) { git_signature_free(aSignature) GitSignaturePtr
A unique pointer for git_signature objects with automatic cleanup.
std::unique_ptr< git_repository, decltype([](git_repository *aRepo) { git_repository_free(aRepo) GitRepositoryPtr
A unique pointer for git_repository objects with automatic cleanup.
std::unique_ptr< git_config, decltype([](git_config *aConfig) { git_config_free(aConfig) GitConfigPtr
A unique pointer for git_config objects with automatic cleanup.
std::unique_ptr< git_reference, decltype([](git_reference *aRef) { git_reference_free(aRef) GitReferencePtr
A unique pointer for git_reference objects with automatic cleanup.
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:70
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.
Definition: ui_common.cpp:382
bool contains(const _Container &__container, _Value __value)
Returns true if the container contains the given value.
Definition: kicad_algo.h:100
PGM_BASE & Pgm()
The global program "get" accessor.
Definition: pgm_base.cpp:893
#define NAMELESS_PROJECT
default name for nameless projects
Definition: project.h:43
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_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
std::vector< wxString > getProjects(const wxDir &dir)
static git_repository * get_git_repository_for_file(const char *filename)
static const wxChar * s_allowedExtensionsToList[]
static int git_create_branch(git_repository *aRepo, wxString &aBranchName)
wxDECLARE_EVENT(UPDATE_ICONS, wxCommandEvent)
#define wxFileSystemWatcher
#define TO_UTF8(wxstring)
Convert a wxString to a UTF8 encoded C string for all wxWidgets build modes.
Definition: string_utils.h:429
thread_pool & GetKiCadThreadPool()
Get a reference to the current thread pool.
Definition: thread_pool.cpp:30
static thread_pool * tp
Definition: thread_pool.cpp:28
BS::thread_pool thread_pool
Definition: thread_pool.h:31
wxLogTrace helper definitions.
TREE_FILE_TYPE
Definition of file extensions used in Kicad.
#define PR_NO_ABORT