KiCad PCB EDA Suite
Loading...
Searching...
No Matches
git_status_entries.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 The KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 3
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 */
19
21
22#include <fstream>
23#include <map>
24#include <memory>
25#include <optional>
26#include <set>
27#include <sys/stat.h>
28#include <mutex>
29#include <vector>
30
31#include <wx/choicdlg.h>
32#include <wx/dir.h>
33#include <wx/filename.h>
34#include <wx/richmsgdlg.h>
35#include <wx/weakref.h>
36
37#include <reporter.h>
38
39#include <confirm.h>
46#include <git/text_diff.h>
49#include <eda_base_frame.h>
50#include <frame_type.h>
54#include <i18n_utility.h>
55#include <kiway.h>
56#include <kiway_player.h>
57#include <mail_type.h>
58#include <pgm_base.h>
59#include <project.h>
65
66
67// Fill the status label, icon and color. Return false for changes the checklist does not show.
68static bool statusToDisplay( KIGIT_COMMON::GIT_STATUS aStatus, wxString& aWord, BITMAPS& aIcon, wxColour& aColour,
69 const KICAD_DIFF::DIFF_COLOR_THEME& aTheme )
70{
71 switch( aStatus )
72 {
74 aWord = _( "Modified" );
76 aColour = aTheme.modified.ToColour();
77 return true;
80 aWord = _( "New" );
81 aIcon = BITMAPS::git_add;
82 aColour = aTheme.added.ToColour();
83 return true;
85 aWord = _( "Deleted" );
86 aIcon = BITMAPS::git_delete;
87 aColour = aTheme.removed.ToColour();
88 return true;
90 aWord = _( "Conflict" );
92 aColour = aTheme.conflict.ToColour();
93 return true;
94 default: return false;
95 }
96}
97
98
99std::vector<FILE_CHECKLIST_ENTRY> BuildGitChangeEntries( KIGIT_COMMON& aCommon, const wxString& aProjectPath,
100 bool aDarkTheme )
101{
102 std::vector<FILE_CHECKLIST_ENTRY> entries;
103
104 const std::optional<wxString> prefix = aCommon.GetProjectPrefix();
105
106 if( !prefix )
107 return entries;
108
109 GIT_STATUS_HANDLER handler( &aCommon );
110 wxString workDir = handler.GetWorkingDirectory();
111
112 std::map<wxString, FileStatus> statusMap = handler.GetFileStatus( *prefix, /* changed only */ true );
114
115 for( const auto& [path, fileStatus] : statusMap )
116 {
117 wxString word;
119 wxColour colour;
120
121 if( !statusToDisplay( fileStatus.status, word, icon, colour, theme ) )
122 continue;
123
125 continue;
126
127 // The commit handler stages paths relative to the working directory.
128 wxString relativePath = path;
129
130 if( relativePath.StartsWith( workDir ) )
131 relativePath = relativePath.Mid( workDir.length() );
132
133#ifdef __WINDOWS__
134 relativePath.Replace( wxS( "\\" ), wxS( "/" ) );
135#endif
136
137 // Only files git does not know at all are hidden. One that is tracked, or staged to be,
138 // is carried by a commit and blocks a merge, so hiding it would leave the user with
139 // nothing to commit and no way forward.
140 bool untracked = ( fileStatus.gitStatus & GIT_STATUS_WT_NEW )
141 && !( fileStatus.gitStatus & GIT_STATUS_INDEX_NEW );
142
143 if( untracked )
144 {
145 // Use the session prefix so a project opened through a symlink has the same paths.
146 const wxString pathPrefix = relativePath.Left( prefix->length() );
147
148#ifdef __WINDOWS__
149 bool inProject = pathPrefix.CmpNoCase( *prefix ) == 0;
150#else
151 bool inProject = pathPrefix == *prefix;
152#endif
153
154 if( inProject
155 && KIGIT::PROJECT_GIT_UTILS::IsTransientGitFile( wxFileName( relativePath.Mid( prefix->length() ) ) ) )
156 {
157 continue;
158 }
159 }
160
162 entry.section = _( "Changes" );
163 entry.path = relativePath;
164 entry.label = wxFileName( relativePath ).GetFullName();
165 entry.status = word;
166 entry.statusColour = colour;
167 entry.icon = icon;
168 entry.checked = true;
169 entries.push_back( entry );
170 }
171
172 return entries;
173}
174
175
176namespace
177{
178
179struct EDITOR_DOC
180{
181 FRAME_T frameType;
182 MAIL_T reloadMail;
183 MAIL_T settingsMail;
184};
185
186
187const EDITOR_DOC g_editorDocs[] = {
190};
191
192
193// Nanosecond stamp on POSIX, because wxFileName truncates to whole seconds there. A same-size
194// rewrite in the same second as the save before it would otherwise be missed.
195bool makeFileStamp( const wxFileName& aFn, FILE_STAMP& aOut )
196{
197 if( !aFn.IsOk() || !aFn.FileExists() )
198 return false;
199
200 wxString path = aFn.GetFullPath();
201
202#ifdef __WINDOWS__
203 aOut.modified = aFn.GetModificationTime().GetValue().GetValue();
204#else
205 struct stat st;
206
207 if( stat( path.fn_str(), &st ) != 0 )
208 return false;
209
210#ifdef __APPLE__
211 aOut.modified = static_cast<long long>( st.st_mtimespec.tv_sec ) * 1000000000LL + st.st_mtimespec.tv_nsec;
212#else
213 aOut.modified = static_cast<long long>( st.st_mtim.tv_sec ) * 1000000000LL + st.st_mtim.tv_nsec;
214#endif
215#endif
216
217 aOut.size = static_cast<long long>( aFn.GetSize().GetValue() );
218 return true;
219}
220
221
222// Add aFn to the snapshot when it exists on disk.
223void addFileToSnapshot( DOCUMENT_SNAPSHOT& aSnapshot, const wxFileName& aFn )
224{
225 FILE_STAMP stamp;
226
227 if( makeFileStamp( aFn, stamp ) )
228 aSnapshot.files[aFn.GetFullPath()] = stamp;
229}
230
231
232// A schematic document spans every sheet file, not just the root the frame names.
233DOCUMENT_SNAPSHOT documentSnapshot( KIWAY& aKiway, FRAME_T aFrameType )
234{
236 KIWAY_PLAYER* player = aKiway.Player( aFrameType, false );
237
238 if( !player )
239 return result;
240
241 addFileToSnapshot( result, wxFileName( player->GetCurrentFileName() ) );
242
243 if( aFrameType == FRAME_SCH )
244 {
245 // A shared hierarchy can reference sheets above the project directory, and a pull can
246 // rewrite anything in the repository, so scan from the repository root.
247 wxString scanRoot = player->Prj().GetGitCommon()->GetProjectDir();
248
249 if( scanRoot.IsEmpty() )
250 scanRoot = player->Prj().GetProjectPath();
251
252 if( !scanRoot.IsEmpty() )
253 {
254 wxArrayString sheetFiles;
255 wxDir::GetAllFiles( scanRoot, &sheetFiles, wxS( "*.kicad_sch" ), wxDIR_FILES | wxDIR_DIRS );
256 sheetFiles.Sort();
257
258 for( const wxString& sheet : sheetFiles )
259 addFileToSnapshot( result, wxFileName( sheet ) );
260 }
261 }
262
263 return result;
264}
265
266// A pull or branch switch can change the settings files on their own, and the loaded settings
267// object would write the old values back on the next save.
268DOCUMENT_SNAPSHOT settingsSnapshot()
269{
271 PROJECT& prj = Pgm().GetSettingsManager().Prj();
272
273 if( prj.GetProjectFullName().IsEmpty() )
274 return result;
275
276 wxFileName prl( prj.GetProjectFullName() );
278
279 addFileToSnapshot( result, wxFileName( prj.GetProjectFullName() ) );
280 addFileToSnapshot( result, prl );
281 return result;
282}
283
284
285// The files whose on-disk identity differs, including those present in only one snapshot.
286std::set<wxString> changedFilesBetween( const DOCUMENT_SNAPSHOT& aBefore, const DOCUMENT_SNAPSHOT& aAfter )
287{
288 std::set<wxString> changed;
289
290 for( const auto& [path, stamp] : aBefore.files )
291 {
292 auto it = aAfter.files.find( path );
293
294 if( it == aAfter.files.end() || it->second != stamp )
295 changed.insert( path );
296 }
297
298 for( const auto& [path, stamp] : aAfter.files )
299 {
300 if( !aBefore.files.count( path ) )
301 changed.insert( path );
302 }
303
304 return changed;
305}
306
307} // namespace
308
309
311{
312 EDITOR_SNAPSHOT before;
313
314 for( const EDITOR_DOC& doc : g_editorDocs )
315 before.editors[doc.frameType] = documentSnapshot( aKiway, doc.frameType );
316
317 before.settings = settingsSnapshot();
318
319 return before;
320}
321
322
323void ReloadOpenEditors( KIWAY& aKiway, const EDITOR_SNAPSHOT& aBefore )
324{
325 // Reread the settings first, so a reloading editor gets the fresh values and the next save
326 // does not write the old ones back.
327 bool settingsChanged = !changedFilesBetween( aBefore.settings, settingsSnapshot() ).empty();
328
329 if( settingsChanged )
330 {
331 PROJECT& prj = Pgm().GetSettingsManager().Prj();
332
335 }
336
337 const DOCUMENT_SNAPSHOT noFiles;
338
339 for( const EDITOR_DOC& doc : g_editorDocs )
340 {
341 if( !aKiway.Player( doc.frameType, false ) )
342 continue;
343
344 // Reloading throws away undo history and the view state, so only do it for a document
345 // whose file actually changed on disk.
346 auto it = aBefore.editors.find( doc.frameType );
347 const DOCUMENT_SNAPSHOT& before = it == aBefore.editors.end() ? noFiles : it->second;
348 std::set<wxString> changed = changedFilesBetween( before, documentSnapshot( aKiway, doc.frameType ) );
349
350 if( changed.empty() )
351 {
352 // Settings that changed on their own still have to reach the live editor: the board
353 // caches resolved netclasses per net, and routing would keep using the old values.
354 if( settingsChanged )
355 {
356 std::string settingsPayload;
357 aKiway.ExpressMail( doc.frameType, doc.settingsMail, settingsPayload );
358 }
359
360 continue;
361 }
362
363 // The editor skips the reload when no changed file belongs to its document. The schematic
364 // snapshot spans the repository, and another project's change must not cost this one its undo.
365 wxString joined;
366
367 for( const wxString& path : changed )
368 joined += path + wxS( "\n" );
369
370 std::string payload = joined.utf8_string();
371 aKiway.ExpressMail( doc.frameType, doc.reloadMail, payload );
372 }
373}
374
375
376namespace
377{
378
379// Keep window trackers together on the UI thread. Worker dispatch copies only this context's
380// shared pointer, so it never adds or removes weak-reference nodes on a live window.
381struct WORKTREE_QUERY_CONTEXT
382{
383 WORKTREE_QUERY_CONTEXT( EDA_BASE_FRAME* aFrame, wxWindow* aParent ) :
384 frame( aFrame ),
385 parent( aParent ? aParent : aFrame ),
386 session( aFrame->Prj().GetGitCommon() )
387 {
388 }
389
390 bool IsAlive() const
391 {
392 return frame.get() && parent.get() && !frame->IsBeingDeleted() && !parent->IsBeingDeleted();
393 }
394
395 bool IsCurrent() const
396 {
397 return IsAlive() && !session->IsShuttingDown() && frame->Prj().GetGitCommon() == session
399 && !frame->Prj().GetLocalSettings().m_GitIntegrationDisabled;
400 }
401
402 wxWeakRef<EDA_BASE_FRAME> frame;
403 wxWeakRef<wxWindow> parent;
404 std::shared_ptr<KIGIT_COMMON> session;
405};
406
407
408// These checks precede UI confirmations, so they are snapshots rather than part of a write
409// transaction. The backend still validates the working tree under the mutation's own lock.
410void checkProjectWorktree( EDA_BASE_FRAME* aFrame, wxWindow* aParent, KIGIT_QUERY& aQuery,
411 std::function<void( const wxString&, const GIT_WORKTREE_STATUS& )> aReady,
412 std::function<void()> aFinished = {} )
413{
414 struct OUTCOME
415 {
416 GIT_WORKTREE_STATUS worktree;
417 wxString currentBranch;
418 };
419
420 wxString projectPath = aFrame->Prj().GetProjectPath();
421 auto context = std::make_shared<WORKTREE_QUERY_CONTEXT>( aFrame, aParent );
422 auto outcome = std::make_shared<OUTCOME>();
423
424 aQuery.Run(
425 context->session,
426 [outcome, projectPath]( KIGIT_COMMON& aCommon )
427 {
428 outcome->currentBranch = aCommon.GetCurrentBranchName();
429 outcome->worktree = ReadGitWorktreeStatus( aCommon, projectPath );
430 },
431 [context, outcome, aReady, aFinished]()
432 {
433 auto proceed = [&]()
434 {
435 if( !context->IsCurrent() )
436 {
437 return;
438 }
439
440 if( !ProjectSafeToRewrite( context->frame, context->parent, outcome->worktree ) )
441 return;
442
443 aReady( outcome->currentBranch, outcome->worktree );
444 };
445
446 proceed();
447
448 if( aFinished && context->IsAlive() )
449 {
450 aFinished();
451 }
452 } );
453}
454
455} // namespace
456
457
458void ProjectClearOfUncommittedWork( EDA_BASE_FRAME* aFrame, wxWindow* aParent, KIGIT_QUERY& aQuery,
459 std::function<void( const GIT_WORKTREE_STATUS& )> aReady )
460{
461 checkProjectWorktree( aFrame, aParent, aQuery,
462 [aReady]( const wxString&, const GIT_WORKTREE_STATUS& aStatus )
463 {
464 aReady( aStatus );
465 } );
466}
467
468
469void SwitchProjectToBranch( EDA_BASE_FRAME* aFrame, wxWindow* aParent, const wxString& aBranch, KIGIT_QUERY& aQuery,
470 std::function<void()> aFinished )
471{
472 if( aBranch.IsEmpty() )
473 {
474 if( aFinished )
475 aFinished();
476
477 return;
478 }
479
480 auto context = std::make_shared<WORKTREE_QUERY_CONTEXT>( aFrame, aParent );
481
482 checkProjectWorktree(
483 aFrame, aParent, aQuery,
484 [context, aBranch]( const wxString& aCurrentBranch, const GIT_WORKTREE_STATUS& aStatus )
485 {
486 const wxWeakRef<EDA_BASE_FRAME>& frame = context->frame;
487 const wxWeakRef<wxWindow>& parent = context->parent;
488 const std::shared_ptr<KIGIT_COMMON>& common = context->session;
489
490 if( aBranch == aCurrentBranch )
491 return;
492
493 if( !IsOK( parent, wxString::Format( _( "Switch to branch '%s'? This replaces the project files "
494 "on disk with that branch's version." ),
495 aBranch ) ) )
496 {
497 return;
498 }
499
500 // The confirmation runs a nested event loop and may have closed the project.
501 if( !context->IsCurrent() )
502 {
503 return;
504 }
505
506 // The confirmation may have allowed new edits. Saving them here would create
507 // the uncommitted files that the checkout refuses to overwrite.
508 if( !ProjectSafeToRewrite( frame, parent, aStatus ) )
509 return;
510
511 if( !context->IsCurrent() )
512 {
513 return;
514 }
515
516 EDITOR_SNAPSHOT docsBefore = CaptureOpenEditorDocs( frame->Kiway() );
518 wxString resultError;
519
520 {
521 std::unique_lock<std::mutex> lock = KIGIT::TryLockAction( *common, parent );
522
523 if( !lock.owns_lock() )
524 return;
525
526 GIT_BRANCH_HANDLER handler( common.get() );
527 result = handler.SwitchToBranch( aBranch );
528 resultError = handler.GetErrorString();
529
530 // The next transfer must use the new branch's upstream and credentials.
531 // Update them before releasing the same lock that protects the checkout.
533 common->UpdateCurrentBranchInfo();
534 }
535
537 {
538 DisplayInfoMessage( parent, resultError );
539 return;
540 }
541
543 {
544 DisplayErrorMessage( parent, wxString::Format( _( "Could not switch to branch '%s'." ), aBranch ),
545 resultError.IsEmpty() ? KIGIT_COMMON::GetLastGitError() : resultError );
546 return;
547 }
548
549 ReloadOpenEditors( frame->Kiway(), docsBefore );
550 common->NotifyRefreshListeners();
551 },
552 std::move( aFinished ) );
553}
554
555
556namespace
557{
558
559enum class EDITOR_SCOPE
560{
561 DOCUMENTS,
562 LIBRARIES,
563 ALL
564};
565
566
567struct GIT_EDITOR
568{
569 FRAME_T frameType;
570 const wxChar* label;
571 std::optional<MAIL_T> saveMail;
572};
573
574
575const GIT_EDITOR g_gitEditors[] = {
576 { FRAME_SCH, _HKI( "Schematic Editor" ), MAIL_SCH_SAVE },
577 { FRAME_PCB_EDITOR, _HKI( "PCB Editor" ), MAIL_PCB_SAVE },
578 { FRAME_SCH_SYMBOL_EDITOR, _HKI( "Symbol Editor" ), std::nullopt },
579 { FRAME_FOOTPRINT_EDITOR, _HKI( "Footprint Editor" ), std::nullopt },
580};
581
582
583// Keep editor identifiers across prompts, since a prompt can outlive an editor window.
584std::vector<const GIT_EDITOR*> dirtyEditors( KIWAY& aKiway, EDITOR_SCOPE aScope )
585{
586 std::vector<const GIT_EDITOR*> dirty;
587
588 for( const GIT_EDITOR& editor : g_gitEditors )
589 {
590 bool document = editor.saveMail.has_value();
591
592 if( ( aScope == EDITOR_SCOPE::DOCUMENTS && !document ) || ( aScope == EDITOR_SCOPE::LIBRARIES && document ) )
593 {
594 continue;
595 }
596
597 KIWAY_PLAYER* player = aKiway.Player( editor.frameType, false );
598
599 if( player && player->IsContentModified() )
600 dirty.push_back( &editor );
601 }
602
603 return dirty;
604}
605
606
607std::vector<wxString> dirtyEditorNames( KIWAY& aKiway, EDITOR_SCOPE aScope )
608{
609 std::vector<wxString> names;
610
611 for( const GIT_EDITOR* editor : dirtyEditors( aKiway, aScope ) )
612 names.push_back( wxGetTranslation( editor->label ) );
613
614 return names;
615}
616
617} // namespace
618
619
621{
622 return !dirtyEditors( aKiway, EDITOR_SCOPE::DOCUMENTS ).empty();
623}
624
625
626bool EditorsDirty( KIWAY& aKiway )
627{
628 return !dirtyEditors( aKiway, EDITOR_SCOPE::ALL ).empty();
629}
630
631
632std::vector<wxString> LibraryEditorsWithUnsavedChanges( KIWAY& aKiway )
633{
634 return dirtyEditorNames( aKiway, EDITOR_SCOPE::LIBRARIES );
635}
636
637
638std::vector<wxString> ChangedTrackedFilesOutsideProject( KIGIT_COMMON& aCommon, const wxString& aProjectPath )
639{
640 std::vector<wxString> paths;
641
642 if( !aCommon.GetRepo() )
643 return paths;
644
645 GIT_STATUS_HANDLER handler( &aCommon );
646 wxString workDir = handler.GetWorkingDirectory();
647
648 for( const auto& [path, fileStatus] : handler.GetFileStatus( wxEmptyString, /* changed only */ true ) )
649 {
650 // The display status groups untracked and staged new files together. Only raw flags
651 // distinguish changes to tracked files from untracked siblings that do not block this.
652 if( ( fileStatus.gitStatus & ~( GIT_STATUS_WT_NEW | GIT_STATUS_IGNORED ) ) == GIT_STATUS_CURRENT )
653 {
654 continue;
655 }
656
658 continue;
659
660 wxString relativePath = path;
661
662 if( relativePath.StartsWith( workDir ) )
663 relativePath = relativePath.Mid( workDir.length() );
664
665 paths.push_back( relativePath );
666 }
667
668 return paths;
669}
670
671
672GIT_WORKTREE_STATUS ReadGitWorktreeStatus( KIGIT_COMMON& aCommon, const wxString& aProjectPath )
673{
674 GIT_WORKTREE_STATUS status;
675
676 if( !aCommon.GetRepo() )
677 return status;
678
679 status.hasTrackedChanges = aCommon.HasUncommittedTrackedChanges( &status.checked );
681
682 // A sibling project's tracked edits block the same repository-wide rewrite.
683 if( status.hasTrackedChanges )
684 status.outsideProject = ChangedTrackedFilesOutsideProject( aCommon, aProjectPath );
685
686 return status;
687}
688
689
690wxString GIT_WORKTREE_STATUS::RefusalMessage( const std::vector<wxString>& aUnsavedEditors,
691 bool aAllowTrackedChanges ) const
692{
693 if( !aUnsavedEditors.empty() )
694 {
695 wxString listed;
696
697 for( const wxString& name : aUnsavedEditors )
698 listed += wxString::Format( wxS( "\n - %s" ), name );
699
700 wxString message = aAllowTrackedChanges
701 ? _( "The following editors have unsaved changes:%s\n\n"
702 "Save or discard those changes before stashing." )
703 : _( "The following editors have unsaved changes:%s\n\n"
704 "Save or discard those changes first. If saving leaves uncommitted "
705 "files, commit or stash them before trying again." );
706
707 return wxString::Format( message, listed );
708 }
709
710 if( !checked )
711 return _( "Could not check the working files. Try again before rewriting project files." );
712
713 if( !lfsMessage.IsEmpty() )
714 return lfsMessage;
715
716 if( !hasTrackedChanges || aAllowTrackedChanges )
717 return wxEmptyString;
718
719 if( !outsideProject.empty() )
720 {
721 wxString listed;
722
723 for( const wxString& path : outsideProject )
724 listed += wxS( "\n " ) + path;
725
726 return wxString::Format( _( "There are uncommitted changes elsewhere in this repository:%s\n\n"
727 "Commit or discard them from that project first, then try again." ),
728 listed );
729 }
730
731 return _( "There are changes that are not committed yet, and this would run over them. "
732 "Commit them first, or set them aside with Stash Changes." );
733}
734
735
736bool ProjectSafeToRewrite( EDA_BASE_FRAME* aFrame, wxWindow* aParent, const GIT_WORKTREE_STATUS& aStatus,
737 bool aAllowTrackedChanges )
738{
739 std::vector<wxString> unsaved = dirtyEditorNames( aFrame->Kiway(), EDITOR_SCOPE::ALL );
740 wxString message = aStatus.RefusalMessage( unsaved, aAllowTrackedChanges );
741
742 if( message.IsEmpty() )
743 return true;
744
745 DisplayInfoMessage( aParent ? aParent : aFrame, message );
746 return false;
747}
748
749
750bool PrepareEditorsForGitOperation( EDA_BASE_FRAME* aFrame, GIT_EDITOR_PREP aPurpose, bool* aSavedEditors )
751{
752 if( aSavedEditors )
753 *aSavedEditors = false;
754
755 // Rewriting a library an editor holds would let its next save overwrite what arrived.
756 if( aPurpose == GIT_EDITOR_PREP::WORKING_TREE )
757 {
758 std::vector<wxString> libraryEditors = LibraryEditorsWithUnsavedChanges( aFrame->Kiway() );
759
760 if( !libraryEditors.empty() )
761 {
762 wxString listed;
763
764 for( const wxString& name : libraryEditors )
765 listed += wxString::Format( wxS( "\n - %s" ), name );
766
767 DisplayInfoMessage( aFrame, wxString::Format( _( "The following editors have unsaved library "
768 "changes:%s\n\nThis rewrites files on disk. Save or "
769 "discard those changes first, then try again." ),
770 listed ) );
771 return false;
772 }
773 }
774
775 std::vector<const GIT_EDITOR*> dirty = dirtyEditors( aFrame->Kiway(), EDITOR_SCOPE::DOCUMENTS );
776
777 if( !dirty.empty() )
778 {
779 wxString listed;
780
781 for( const GIT_EDITOR* editor : dirty )
782 listed += wxString::Format( wxS( "\n - %s" ), wxGetTranslation( editor->label ) );
783
784 wxString message;
785 long style = wxYES_DEFAULT | wxICON_WARNING | wxCENTER;
786
787 if( aPurpose == GIT_EDITOR_PREP::WORKING_TREE )
788 {
789 message = wxString::Format( _( "The following editors have unsaved changes:%s\n\n"
790 "This rewrites those files on disk. They must be saved "
791 "first, otherwise the next save would overwrite what "
792 "arrives." ),
793 listed );
794 style |= wxYES_NO;
795 }
796 else
797 {
798 message = wxString::Format( _( "The following editors have unsaved changes:%s\n\n"
799 "Save them and review the updated file list before committing?" ),
800 listed );
801 style |= wxYES_NO | wxCANCEL;
802 }
803
804 wxRichMessageDialog dlg( aFrame, message, _( "Unsaved Changes" ), style );
805
806 if( aPurpose == GIT_EDITOR_PREP::WORKING_TREE )
807 dlg.SetYesNoLabels( _( "&Save and Continue" ), _( "Cancel" ) );
808 else
809 dlg.SetYesNoCancelLabels( _( "&Save and Review" ), _( "Commit &Anyway" ), _( "Cancel" ) );
810
811 int answer = dlg.ShowModal();
812
813 if( answer == wxID_CANCEL )
814 return false;
815
816 // Rewriting the working tree offers no "continue anyway", so anything else is a cancel.
817 if( aPurpose == GIT_EDITOR_PREP::WORKING_TREE && answer != wxID_YES )
818 return false;
819
820 if( answer == wxID_YES )
821 {
822 for( const GIT_EDITOR* editor : dirty )
823 {
824 std::string payload;
825 aFrame->Kiway().ExpressMail( editor->frameType, *editor->saveMail, payload );
826
827 if( payload != "success" )
828 {
830 aFrame, wxString::Format( _( "Could not save %s." ), wxGetTranslation( editor->label ) ) );
831 return false;
832 }
833 }
834
835 // Include project changes from saving editors only when the user chose to save.
836 if( aPurpose == GIT_EDITOR_PREP::COMMIT )
837 aFrame->GetSettingsManager()->SaveProject();
838
839 if( aSavedEditors )
840 *aSavedEditors = true;
841 }
842 }
843
844 return true;
845}
846
847
849{
850 m_root.MakeDirectory( wxS( "kicad_gitdiff_" ), 0700 );
851}
852
853
854wxString GIT_DIFF_WORKSPACE::Materialize( git_repository* aRepo, const wxString& aRef, const wxString& aRelPath,
856{
857 if( !IsOk() )
858 return wxEmptyString;
859
860 const wxString sep = wxFileName::GetPathSeparator();
861
862 // A schematic root sheet references its sub-sheet files by relative path, so check out the
863 // whole tree and the sub-sheets come along. A board is self-contained, so write just the blob.
864 if( aKind == KICAD_DIFF::DOC_KIND::SCH )
865 {
866 wxString dir = Root() + sep + wxString::Format( wxS( "s%d" ), m_slot++ );
867
868 if( !KIGIT::PROJECT_GIT_UTILS::WriteTreeToDir( aRepo, aRef, dir ) )
869 return wxEmptyString;
870
871 wxString path = dir + sep + aRelPath;
872
873 return wxFileName::FileExists( path ) ? path : wxString( wxEmptyString );
874 }
875
876 wxString path = Root() + sep + wxString::Format( wxS( "s%d_" ), m_slot++ ) + wxFileName( aRelPath ).GetFullName();
877
878 if( !KIGIT::PROJECT_GIT_UTILS::WriteBlobToFile( aRepo, aRef, aRelPath, path ) )
879 return wxEmptyString;
880
881 return path;
882}
883
884
885namespace
886{
887
893struct MODAL_GIT_DIFF_REQUEST
894{
895 MODAL_GIT_DIFF_REQUEST( EDA_BASE_FRAME* aFrame, wxWindow* aParent ) :
896 frame( aFrame ),
897 parent( aParent ),
898 session( aFrame->Prj().GetGitCommon() )
899 {
900 }
901
902 bool IsCurrent() const
903 {
904 return frame.get() && parent.get() && !frame->IsBeingDeleted() && !parent->IsBeingDeleted()
905 && frame->Prj().GetGitCommon() == session && !session->IsShuttingDown();
906 }
907
908 wxWeakRef<EDA_BASE_FRAME> frame;
909 wxWeakRef<wxWindow> parent;
910 std::shared_ptr<KIGIT_COMMON> session;
911 KIGIT_QUERY query;
912};
913
914
915struct MODAL_GIT_DIFF_RESULT
916{
917 std::unique_ptr<GIT_DIFF_WORKSPACE> workspace;
918 GIT_FILE_SIDES sides;
919 std::vector<KIGIT::TEXT_DIFF_LINE> textLines;
920 bool ready = false;
921};
922
923
924std::string readDiffText( const wxString& aPath )
925{
926 if( aPath.IsEmpty() )
927 return {};
928
929 std::ifstream in( aPath.fn_str(), std::ios::binary );
930 return std::string( ( std::istreambuf_iterator<char>( in ) ), std::istreambuf_iterator<char>() );
931}
932
933
934// A named file can be loaded directly. Otherwise ShowGitCommitDiff obtains the choice first.
935// An empty commit hash compares HEAD with the working copy.
936void loadModalGitDiff( const std::shared_ptr<MODAL_GIT_DIFF_REQUEST>& aRequest, const wxString& aRelativePath,
937 const wxString& aCommitHash )
938{
939 auto result = std::make_shared<MODAL_GIT_DIFF_RESULT>();
940 result->sides.error = _( "Could not compare this file." );
941
942 aRequest->query.Run(
943 aRequest->session,
944 [result, aRelativePath, aCommitHash]( KIGIT_COMMON& aCommon )
945 {
946 result->workspace = std::make_unique<GIT_DIFF_WORKSPACE>();
947
948 if( !result->workspace->IsOk() )
949 {
950 result->sides.error = _( "Could not create a temporary directory for the diff." );
951 return;
952 }
953
954 bool materialized =
955 aCommitHash.IsEmpty()
956 ? MaterializeWorkingFile( aCommon, *result->workspace, aRelativePath, result->sides )
957 : MaterializeCommitFile( aCommon, *result->workspace, aCommitHash, aRelativePath,
958 result->sides );
959
960 if( !materialized )
961 return;
962
963 if( result->sides.kind == KICAD_DIFF::DOC_KIND::UNKNOWN )
964 {
965 std::string before = readDiffText( result->sides.before );
966 std::string after = readDiffText( result->sides.after );
967
968 if( KIGIT::TextLooksBinary( before ) || KIGIT::TextLooksBinary( after ) )
969 {
970 result->sides.error = wxString::Format(
971 _( "'%s' is not a plain text file. KiCad compares design files and text files "
972 "only, so there is no comparison to show." ),
973 aRelativePath );
974 return;
975 }
976
977 result->textLines =
979 }
980
981 result->ready = true;
982 },
983 [aRequest, result, aRelativePath, aCommitHash]()
984 {
985 if( !aRequest->IsCurrent() )
986 return;
987
988 wxWindow* parent = aRequest->parent.get();
989
990 if( !result->ready )
991 {
992 if( !result->sides.error.IsEmpty() )
993 DisplayInfoMessage( parent, result->sides.error );
994
995 return;
996 }
997
998 if( result->sides.kind == KICAD_DIFF::DOC_KIND::UNKNOWN )
999 {
1000 DIALOG_TEXT_DIFF dlg( parent, aRelativePath, result->textLines );
1001 dlg.ShowModal();
1002 return;
1003 }
1004
1005 wxString beforeLabel = aCommitHash.IsEmpty()
1006 ? _( "BASE (HEAD)" )
1007 : wxString::Format( _( "BASE (parent of %s)" ), aCommitHash );
1008 wxString afterLabel = aCommitHash.IsEmpty() ? _( "WORKING (changes)" )
1009 : wxString::Format( _( "WORKING (%s)" ), aCommitHash );
1010
1011 // The owning kiface loads and displays documents on the UI thread. Keep the
1012 // scratch files alive until the modal comparison has closed.
1014 KICAD_DIFF::DispatchOpenDiffDialog( aRequest->frame->Kiway(), result->sides.kind, result->sides.before,
1015 result->sides.after, beforeLabel, afterLabel, parent, &reporter );
1016
1017 if( aRequest->IsCurrent() && reporter.HasMessage() )
1018 DisplayErrorMessage( parent, _( "Could not open the diff." ), reporter.GetMessages() );
1019 } );
1020}
1021
1022} // namespace
1023
1024
1025void ShowGitFileDiff( EDA_BASE_FRAME* aFrame, const wxString& aRelativePath )
1026{
1027 auto request = std::make_shared<MODAL_GIT_DIFF_REQUEST>( aFrame, aFrame );
1028 loadModalGitDiff( request, aRelativePath, wxEmptyString );
1029}
1030
1031
1032bool MaterializeWorkingFile( KIGIT_COMMON& aCommon, GIT_DIFF_WORKSPACE& aWorkspace, const wxString& aRelativePath,
1033 GIT_FILE_SIDES& aSides )
1034{
1035 git_repository* repo = aCommon.GetRepo();
1036
1037 if( !repo )
1038 return false;
1039
1040 aSides.kind = KICAD_DIFF::DocKindFromExtension( aRelativePath );
1041
1042 GIT_STATUS_HANDLER handler( &aCommon );
1043 aSides.after = handler.GetWorkingDirectory() + aRelativePath;
1044
1045 // A new file has no committed version, so that side stays empty and shows as added.
1046 bool inHead = aCommon.IsPathInHead( aRelativePath );
1047
1048 if( inHead )
1049 aSides.before = aWorkspace.Materialize( repo, wxT( "HEAD" ), aRelativePath, aSides.kind );
1050
1051 if( inHead && aSides.before.IsEmpty() )
1052 {
1053 aSides.error = wxString::Format( _( "Could not read the committed version of '%s'." ), aRelativePath );
1054 return false;
1055 }
1056
1057 // A deleted file has no working copy, so an empty path makes the loader synthesize an empty
1058 // document instead of failing on a file that is gone.
1059 if( !wxFileName::FileExists( aSides.after ) )
1060 aSides.after.clear();
1061
1062 return true;
1063}
1064
1065
1066void ShowGitCommitDiff( EDA_BASE_FRAME* aFrame, const wxString& aCommitHash, wxWindow* aParentWindow,
1067 const wxString& aRelativePath )
1068{
1069 auto request = std::make_shared<MODAL_GIT_DIFF_REQUEST>( aFrame, aParentWindow ? aParentWindow : aFrame );
1070
1071 // A caller that named a file knows its own list, which for a stash also holds the files
1072 // that were untracked. Those are not in the parent-to-commit diff used by the chooser.
1073 if( !aRelativePath.IsEmpty() )
1074 {
1075 loadModalGitDiff( request, aRelativePath, aCommitHash );
1076 return;
1077 }
1078
1079 auto changedPaths = std::make_shared<std::vector<wxString>>();
1080
1081 request->query.Run(
1082 request->session,
1083 [changedPaths, aCommitHash]( KIGIT_COMMON& aCommon )
1084 {
1085 git_repository* repo = aCommon.GetRepo();
1086 wxString parentRef;
1087 git_object* commitObj = nullptr;
1088
1089 // A root commit has no parent, so compare it against the empty tree.
1090 if( git_revparse_single( &commitObj, repo, aCommitHash.utf8_string().c_str() ) == GIT_OK )
1091 {
1092 if( git_object_type( commitObj ) == GIT_OBJECT_COMMIT
1093 && git_commit_parentcount( reinterpret_cast<git_commit*>( commitObj ) ) > 0 )
1094 {
1095 parentRef = aCommitHash + wxT( "^" );
1096 }
1097
1098 git_object_free( commitObj );
1099 }
1100
1101 for( const KIGIT::CHANGED_FILE& file : KIGIT::CompareRefs( repo, parentRef, aCommitHash ) )
1102 changedPaths->push_back( file.path );
1103 },
1104 [request, changedPaths, aCommitHash]()
1105 {
1106 if( !request->IsCurrent() )
1107 return;
1108
1109 wxWindow* parent = request->parent.get();
1110
1111 if( changedPaths->empty() )
1112 {
1113 DisplayInfoMessage( parent, _( "This commit has no file changes to show." ) );
1114 return;
1115 }
1116
1117 wxString chosen = changedPaths->front();
1118
1119 if( changedPaths->size() > 1 )
1120 {
1121 wxArrayString choices;
1122
1123 for( const wxString& path : *changedPaths )
1124 choices.Add( path );
1125
1126 int sel = wxGetSingleChoiceIndex( _( "This commit changed more than one file. "
1127 "Select one to compare:" ),
1128 _( "Select File to Compare" ), choices, parent );
1129
1130 if( sel < 0 )
1131 return;
1132
1133 chosen = ( *changedPaths )[sel];
1134 }
1135
1136 // A modal chooser may have processed a project close or switch.
1137 if( request->IsCurrent() )
1138 loadModalGitDiff( request, chosen, aCommitHash );
1139 } );
1140}
1141
1142
1143bool MaterializeCommitFile( KIGIT_COMMON& aCommon, GIT_DIFF_WORKSPACE& aWorkspace, const wxString& aCommitHash,
1144 const wxString& aRelativePath, GIT_FILE_SIDES& aSides )
1145{
1146 // The caller may have run a chooser since it last looked, so the project may have closed.
1147 git_repository* repo = aCommon.GetRepo();
1148
1149 if( !repo )
1150 return false;
1151
1152 // A root commit has no "<hash>^". An empty base ref compares it against the empty tree, so
1153 // its files show as added.
1154 wxString parentRef;
1155 git_object* commitObj = nullptr;
1156 unsigned parentCount = 0;
1157
1158 if( git_revparse_single( &commitObj, repo, aCommitHash.utf8_string().c_str() ) == GIT_OK )
1159 {
1160 if( git_object_type( commitObj ) == GIT_OBJECT_COMMIT )
1161 parentCount = git_commit_parentcount( reinterpret_cast<git_commit*>( commitObj ) );
1162
1163 if( parentCount > 0 )
1164 parentRef = aCommitHash + wxT( "^" );
1165
1166 git_object_free( commitObj );
1167 }
1168
1169 for( const KIGIT::CHANGED_FILE& file : KIGIT::CompareRefs( repo, parentRef, aCommitHash ) )
1170 {
1171 if( file.path != aRelativePath )
1172 continue;
1173
1174 aSides.kind = KICAD_DIFF::DocKindFromExtension( file.path );
1175
1176 // Only the side that exists is written out.
1177 bool hasParent = file.status != KIGIT::FILE_CHANGE_STATUS::ADDED;
1178 bool hasCommit = file.status != KIGIT::FILE_CHANGE_STATUS::REMOVED;
1179
1180 if( hasParent )
1181 {
1182 wxString parentPath = file.oldPath.IsEmpty() ? file.path : file.oldPath;
1183 aSides.before = aWorkspace.Materialize( repo, parentRef, parentPath, aSides.kind );
1184 }
1185
1186 if( hasCommit )
1187 aSides.after = aWorkspace.Materialize( repo, aCommitHash, file.path, aSides.kind );
1188
1189 if( ( hasParent && aSides.before.IsEmpty() ) || ( hasCommit && aSides.after.IsEmpty() ) )
1190 {
1191 aSides.error = _( "Could not read this commit's file contents." );
1192 return false;
1193 }
1194
1195 return true;
1196 }
1197
1198 // A stash keeps the files that were untracked when it was taken in a third parent, so they
1199 // are missing from the diff above. Nothing came before them, so only one side has content.
1200 if( parentCount > 2 )
1201 {
1202 aSides.kind = KICAD_DIFF::DocKindFromExtension( aRelativePath );
1203 aSides.after = aWorkspace.Materialize( repo, aCommitHash + wxT( "^3" ), aRelativePath, aSides.kind );
1204
1205 if( !aSides.after.IsEmpty() )
1206 return true;
1207 }
1208
1209 aSides.error = wxString::Format( _( "This commit did not change '%s'." ), aRelativePath );
1210 return false;
1211}
const char * name
BITMAPS
A list of all bitmap identifiers.
@ INVALID_BITMAP
Read-only line diff, for the file types that have no visual diff.
The base frame for deriving all KiCad main window classes.
SETTINGS_MANAGER * GetSettingsManager() const
virtual wxString GetCurrentFileName() const
Get the full filename + path of the currently opened file in the frame.
virtual bool IsContentModified() const
Get if the contents of the frame have been modified since the last save.
BranchResult SwitchToBranch(const wxString &aBranchName)
Switch to the specified branch.
A private scratch directory for extracting committed file versions to diff, removed when this object ...
bool IsOk() const
True when the scratch directory was created.
const wxString & Root() const
The scratch directory, for callers that lay out their own files (e.g. footprint libraries).
wxString Materialize(git_repository *aRepo, const wxString &aRef, const wxString &aRelPath, KICAD_DIFF::DOC_KIND aKind)
Extract aRelPath at aRef into a uniquely named slot and return the path to load, or an empty string w...
SCOPED_TEMP_PATH m_root
std::map< wxString, FileStatus > GetFileStatus(const wxString &aPathspec=wxEmptyString, bool aChangedOnly=false)
Get detailed file status for all files in the specified path.
wxString GetWorkingDirectory()
Get the repository working directory path.
virtual bool LoadFromFile(const wxString &aDirectory="")
Loads the backing file from disk and then calls Load()
wxColour ToColour() const
Definition color4d.cpp:221
static bool WriteBlobToFile(git_repository *aRepo, const wxString &aRef, const wxString &aPath, const wxString &aDestPath)
Write a file as it exists at a git ref to a path on disk, so a committed version can be diffed.
static bool IsTransientGitFile(const wxFileName &aFile)
Match a project-relative path against the shared transient patterns.
static bool WriteTreeToDir(git_repository *aRepo, const wxString &aRef, const wxString &aDestDir)
Check out a whole tree into a directory, leaving the repository's working copy and index alone.
static bool IsWithinProjectPath(const wxString &aAbsPath, const wxString &aProjectPath)
Test whether an absolute file path lives inside the current project directory.
static wxString LfsRefusal(KIGIT_COMMON *aCommon)
Return a refusal message for LFS files or an unknown project folder, otherwise empty.
The project's git session.
bool HasUncommittedTrackedChanges(bool *aReadOk=nullptr) const
std::optional< wxString > GetProjectPrefix() const
Project folder relative to the repository, with a trailing slash.
static wxString GetLastGitError()
git_repository * GetRepo() const
wxString GetProjectDir() const
Return the path set by SetProjectDir(), or the repository's working directory.
bool IsPathInHead(const wxString &aPath)
True when the working directory relative path exists in the HEAD commit's tree.
wxString GetErrorString()
Read repository state without blocking the UI.
void Run(std::shared_ptr< KIGIT_COMMON > aSession, std::function< void(KIGIT_COMMON &)> aWork, std::function< void()> aDone)
Run aWork under the session's action lock on a worker, then aDone on the UI thread.
PROJECT & Prj() const
Return a reference to the PROJECT associated with this KIWAY.
KIWAY & Kiway() const
Return a reference to the KIWAY that this object has an opportunity to participate in.
A wxFrame capable of the OpenProjectFiles function, meaning it can load a portion of a KiCad project.
A minimalistic software bus for communications between various DLLs/DSOs (DSOs) within the same KiCad...
Definition kiway.h:348
virtual KIWAY_PLAYER * Player(FRAME_T aFrameType, bool doCreate=true, wxTopLevelWindow *aParent=nullptr)
Return the KIWAY_PLAYER* given a FRAME_T.
Definition kiway.cpp:401
virtual void ExpressMail(FRAME_T aDestination, MAIL_T aCommand, std::string &aPayload, wxWindow *aSource=nullptr, bool aFromOtherThread=false)
Send aPayload to aDestination from aSource.
Definition kiway.cpp:499
virtual COMMON_SETTINGS * GetCommonSettings() const
Definition pgm_base.cpp:550
virtual SETTINGS_MANAGER & GetSettingsManager() const
Definition pgm_base.h:123
bool LoadFromFile(const wxString &aDirectory="") override
Loads the backing file from disk and then calls Load()
Container for project specific data.
Definition project.h:63
virtual std::shared_ptr< KIGIT_COMMON > GetGitCommon() const
This project's Git session, created empty on first use.
Definition project.cpp:84
virtual const wxString GetProjectFullName() const
Return the full path and name of the project.
Definition project.cpp:255
virtual const wxString GetProjectPath() const
Return the full path of the project.
Definition project.cpp:261
virtual PROJECT_LOCAL_SETTINGS & GetLocalSettings() const
Definition project.h:207
virtual PROJECT_FILE & GetProjectFile() const
Definition project.h:201
bool SaveProject(const wxString &aFullPath=wxEmptyString, PROJECT *aProject=nullptr)
Save a loaded project.
PROJECT & Prj() const
A helper while we are not MDI-capable – return the one and only project.
A wrapper for reporting to a wxString object.
Definition reporter.h:242
bool IsOK(wxWindow *aParent, const wxString &aMessage)
Display a yes/no dialog with aMessage and returns the user response.
Definition confirm.cpp:274
void DisplayInfoMessage(wxWindow *aParent, const wxString &aMessage, const wxString &aExtraInfo)
Display an informational message box with aMessage.
Definition confirm.cpp:245
void DisplayErrorMessage(wxWindow *aParent, const wxString &aText, const wxString &aExtraInfo)
Display an error message with aMessage.
Definition confirm.cpp:217
This file is part of the common library.
#define _(s)
Base window classes and related definitions.
FRAME_T
The set of EDA_BASE_FRAME derivatives, typically stored in EDA_BASE_FRAME::m_Ident.
Definition frame_type.h:29
@ FRAME_PCB_EDITOR
Definition frame_type.h:38
@ FRAME_SCH_SYMBOL_EDITOR
Definition frame_type.h:31
@ FRAME_SCH
Definition frame_type.h:30
@ FRAME_FOOTPRINT_EDITOR
Definition frame_type.h:39
bool PrepareEditorsForGitOperation(EDA_BASE_FRAME *aFrame, GIT_EDITOR_PREP aPurpose, bool *aSavedEditors)
Offer to save unsaved schematic and board edits before a git operation.
std::vector< wxString > ChangedTrackedFilesOutsideProject(KIGIT_COMMON &aCommon, const wxString &aProjectPath)
Repository relative paths of tracked files changed outside the project directory, which the checklist...
void ShowGitCommitDiff(EDA_BASE_FRAME *aFrame, const wxString &aCommitHash, wxWindow *aParentWindow, const wxString &aRelativePath)
Open the diff for what a commit changed, comparing it against its parent.
EDITOR_SNAPSHOT CaptureOpenEditorDocs(KIWAY &aKiway)
Record the on-disk identity of the open documents, before an operation that may rewrite them.
bool MaterializeWorkingFile(KIGIT_COMMON &aCommon, GIT_DIFF_WORKSPACE &aWorkspace, const wxString &aRelativePath, GIT_FILE_SIDES &aSides)
Write the committed version of a file into aWorkspace and name its working copy beside it.
bool MaterializeCommitFile(KIGIT_COMMON &aCommon, GIT_DIFF_WORKSPACE &aWorkspace, const wxString &aCommitHash, const wxString &aRelativePath, GIT_FILE_SIDES &aSides)
Write both sides of one file a commit changed into aWorkspace, the parent's version and the commit's.
std::vector< FILE_CHECKLIST_ENTRY > BuildGitChangeEntries(KIGIT_COMMON &aCommon, const wxString &aProjectPath, bool aDarkTheme)
Build the file checklist entries for the project's changed files, transient files filtered out and ea...
void ReloadOpenEditors(KIWAY &aKiway, const EDITOR_SNAPSHOT &aBefore)
Tell the open board and schematic editors to reload from disk.
std::vector< wxString > LibraryEditorsWithUnsavedChanges(KIWAY &aKiway)
Names of the library editors holding unsaved changes.
void ProjectClearOfUncommittedWork(EDA_BASE_FRAME *aFrame, wxWindow *aParent, KIGIT_QUERY &aQuery, std::function< void(const GIT_WORKTREE_STATUS &)> aReady)
Check the working tree and Git LFS on a worker, then run aReady on the UI thread when the operation c...
GIT_WORKTREE_STATUS ReadGitWorktreeStatus(KIGIT_COMMON &aCommon, const wxString &aProjectPath)
Read tracked changes and Git LFS using a captured project path.
bool DocumentEditorsDirty(KIWAY &aKiway)
Unsaved schematic or board edits can be saved for a commit, even with no changes on disk.
static bool statusToDisplay(KIGIT_COMMON::GIT_STATUS aStatus, wxString &aWord, BITMAPS &aIcon, wxColour &aColour, const KICAD_DIFF::DIFF_COLOR_THEME &aTheme)
void ShowGitFileDiff(EDA_BASE_FRAME *aFrame, const wxString &aRelativePath)
Open the diff for a changed file, comparing the committed version at HEAD with the working copy.
bool ProjectSafeToRewrite(EDA_BASE_FRAME *aFrame, wxWindow *aParent, const GIT_WORKTREE_STATUS &aStatus, bool aAllowTrackedChanges)
Combine the repository snapshot with live editor state and explain any refusal.
void SwitchProjectToBranch(EDA_BASE_FRAME *aFrame, wxWindow *aParent, const wxString &aBranch, KIGIT_QUERY &aQuery, std::function< void()> aFinished)
Check the working tree on a worker, then confirm switching to aBranch and recheck unsaved editors.
bool EditorsDirty(KIWAY &aKiway)
Unsaved document or library edits block operations that rewrite files. Call on the UI thread.
GIT_EDITOR_PREP
What the caller is about to do, which decides the wording and whether the project file is written.
@ COMMIT
Saving dirty editors for a commit also saves the project file.
@ WORKING_TREE
The working files are about to be rewritten, by a pull, branch switch or stash.
static const std::string ProjectLocalSettingsFileExtension
Some functions to handle hotkeys in KiCad.
std::string message
PROJECT & Prj()
Definition kicad.cpp:737
static std::vector< std::string > changedPaths(git_repository *aRepo)
MAIL_T
The set of mail types sendable via KIWAY::ExpressMail() and supplied as the aCommand parameter to tha...
Definition mail_type.h:34
@ MAIL_SCH_SAVE
Definition mail_type.h:38
@ MAIL_PCB_RELOAD
Definition mail_type.h:62
@ MAIL_SCH_PROJECT_SETTINGS_CHANGED
Definition mail_type.h:63
@ MAIL_PCB_SAVE
Definition mail_type.h:39
@ MAIL_PCB_PROJECT_SETTINGS_CHANGED
Definition mail_type.h:64
@ MAIL_SCH_RELOAD
Definition mail_type.h:61
DOC_KIND DocKindFromExtension(const wxString &aPath)
Map a path's extension to a DOC_KIND (.kicad_pcb -> PCB, .kicad_sch -> SCH, .kicad_sym -> SYM_LIB,...
DOC_KIND
Document type a diff/merge entry point should route to, derived from a file path's extension.
int DispatchOpenDiffDialog(KIWAY &aKiway, DOC_KIND aKind, const wxString &aFileA, const wxString &aFileB, const wxString &aLabelA, const wxString &aLabelB, wxWindow *aParent, REPORTER *aReporter)
Open DIALOG_KICAD_DIFF on two files by calling the owning kiface's KIFACE_OPEN_DIFF_DIALOG function e...
@ ALL
All except INITIAL_ADD.
Definition view_item.h:55
std::vector< CHANGED_FILE > CompareRefs(git_repository *aRepo, const wxString &aBaseRef, const wxString &aHeadRef)
Compare two git refs (branch / tag / commit OID) within a repository and return the per-file change l...
std::vector< wxString > SplitTextLines(const std::string &aContent)
Tolerates CRLF endings and content that is not valid UTF-8.
Definition text_diff.cpp:28
std::unique_lock< std::mutex > TryLockAction(KIGIT_COMMON &aCommon, wxWindow *aParent)
Try the action lock and show a busy message if it is unavailable.
std::vector< TEXT_DIFF_LINE > BuildTextDiff(const std::vector< wxString > &aOld, const std::vector< wxString > &aNew)
Every line of both sides appears once. There are no hunks and nothing is elided.
Definition text_diff.cpp:77
#define _HKI(x)
Definition page_info.cpp:40
PGM_BASE & Pgm()
The global program "get" accessor.
see class PGM_BASE
void Format(OUTPUTFORMATTER *out, int aNestLevel, int aCtl, const CPTREE &aTree)
Output a PTREE into s-expression format via an OUTPUTFORMATTER derivative.
Definition ptree.cpp:194
The files making up one open document, each with its on-disk identity.
std::map< wxString, FILE_STAMP > files
What the open editors and the project settings looked like on disk before an operation that may rewri...
std::map< FRAME_T, DOCUMENT_SNAPSHOT > editors
DOCUMENT_SNAPSHOT settings
One file row in a FILE_CHECKLIST_CTRL.
wxString label
Shown in place of the path.
BITMAPS icon
wxString path
Row identity.
wxColour statusColour
Text colour for the status, or the default when not set.
wxString section
wxString status
Free text, e.g. "Modified".
bool checked
On-disk identity of one file, used to tell whether a git operation rewrote it.
long long modified
Modification time, nanoseconds where the platform provides them.
The two sides of one file, written out for a comparison.
KICAD_DIFF::DOC_KIND kind
wxString before
Empty when the file did not exist on that side.
wxString error
Why the sides could not be written, when they could not.
Repository state used before an operation that requires a clean working tree.
std::vector< wxString > outsideProject
wxString RefusalMessage(const std::vector< wxString > &aUnsavedEditors, bool aAllowTrackedChanges=false) const
Empty when rewriting is allowed.
Diff colors with defaults for light backgrounds.
Definition diff_scene.h:275
static DIFF_COLOR_THEME ForText(bool aDarkTheme)
Resolve change colors for UI text. Pass the desktop theme captured on the UI thread.
std::string path
IbisParser parser & reporter
wxString result
Test unit parsing edge cases and error handling.
Definition of file extensions used in Kicad.