KiCad PCB EDA Suite
Loading...
Searching...
No Matches
git_commit_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 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#include <cli/exit_codes.h>
24#include <frame_type.h>
25#include <kiway.h>
26#include <kiway_player.h>
27#include <mail_type.h>
28
29#include <algorithm>
30#include <memory>
31
32#include <wx/app.h>
33#include <wx/button.h>
34#include <wx/dataview.h>
35#include <wx/file.h>
36#include <wx/filefn.h>
37#include <wx/filename.h>
38#include <wx/menu.h>
39#include <wx/msgdlg.h>
40#include <wx/settings.h>
41#include <wx/sizer.h>
42#include <wx/statbox.h>
43#include <wx/stattext.h>
44#include <wx/textctrl.h>
45#include <wx/weakref.h>
46
47#include <wx/choicdlg.h>
48
49#include <bitmaps.h>
50#include <confirm.h>
55#include <kiplatform/ui.h>
57#include <reporter.h>
65#include <eda_base_frame.h>
67#include <pgm_base.h>
68#include <project.h>
71#include <thread>
75
76
77namespace
78{
79enum
80{
81 ID_OPEN_ADVANCED = wxID_HIGHEST + 1,
82 ID_SET_AUTHOR
83};
84}
85
86
87// Pin both the branch (including an unborn one) and its tip while a commit's status reads run.
88// The caller holds the action lock, just as it does when validating this before the write.
89static std::pair<wxString, wxString> commitHead( git_repository* aRepo )
90{
91 std::pair<wxString, wxString> state;
92 git_reference* head = nullptr;
93
94 if( git_reference_lookup( &head, aRepo, "HEAD" ) == GIT_OK )
95 {
96 if( const char* target = git_reference_symbolic_target( head ) )
97 state.first = wxString::FromUTF8( target );
98
99 git_reference_free( head );
100 }
101
102 git_oid oid;
103
104 if( git_reference_name_to_id( &oid, aRepo, "HEAD" ) == GIT_OK )
105 state.second = wxString::FromUTF8( git_oid_tostr_s( &oid ) );
106
107 return state;
108}
109
110
111// Check again under the action lock before amending. A push can change eligibility without moving HEAD.
112static wxString amendRefusal( KIGIT_COMMON& aCommon, const std::pair<wxString, wxString>& aExpectedHead )
113{
114 if( aExpectedHead.second.IsEmpty() || aExpectedHead != commitHead( aCommon.GetRepo() ) )
115 return _( "The branch or its last commit changed. Select the commit to amend again." );
116
117 if( aCommon.IsHeadDetached() || aCommon.IsHeadUnborn() )
118 return _( "Switch to a branch before amending its last commit." );
119
120 if( aCommon.IsOperationInProgress() )
121 return _( "Finish or abandon the current Git operation before amending a commit." );
122
123 if( !aCommon.HasLocalCommits() )
124 return _( "Only an unpushed commit can be amended." );
125
126 return wxEmptyString;
127}
128
129
130// Make aName at HEAD and move onto it, so the next commit lands there. The caller holds the git
131// action lock. A failure leaves nothing behind and says why in aError and aDetail.
132static bool moveToNewBranch( KIGIT_COMMON& aCommon, const wxString& aName, wxString& aError, wxString& aDetail )
133{
134 GIT_BRANCH_HANDLER branchHandler( &aCommon );
135
136 // With no commits there is nothing to branch from, so the first commit makes the branch.
137 if( aCommon.IsHeadUnborn() )
138 {
139 if( !aCommon.StartUnbornBranch( aName ) )
140 {
141 aError = wxString::Format( _( "Could not start branch '%s'." ), aName );
143 return false;
144 }
145
146 return true;
147 }
148
149 if( !aCommon.CreateBranch( aName ) )
150 {
151 aError = wxString::Format( _( "Could not create branch '%s'." ), aName );
153 return false;
154 }
155
156 if( branchHandler.SwitchToBranch( aName ) != BranchResult::Success )
157 {
158 aError = wxString::Format( _( "Could not switch to the new branch '%s'." ), aName );
159 aDetail = branchHandler.GetErrorString();
160 aCommon.DeleteBranch( aName );
161 return false;
162 }
163
164 return true;
165}
166
167
168// Go back to aPrevious and drop aName, after the commit meant for it failed. The caller holds
169// the git action lock.
170static void dropNewBranch( KIGIT_COMMON& aCommon, const wxString& aName, const wxString& aPrevious )
171{
172 // Before the first commit there was no branch to come back to, and the name is only where
173 // HEAD points.
174 if( aPrevious.IsEmpty() )
175 return;
176
177 GIT_BRANCH_HANDLER branchHandler( &aCommon );
178 branchHandler.SwitchToBranch( aPrevious );
179 aCommon.DeleteBranch( aName );
180}
181
182
183// Claim a name for the version a conflict is about to replace. An earlier copy beside the same
184// file may be the only remaining source of its work, so it is never overwritten.
185static bool reserveRecoveryFile( const wxString& aPreferredPath, wxString& aPathOut, wxString& aCreatedDir )
186{
187 aPathOut.Clear();
188 aCreatedDir.Clear();
189
190 wxFileName parent( aPreferredPath );
191
192 if( !parent.DirExists() )
193 {
194 if( !parent.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) )
195 return false;
196
197 aCreatedDir = parent.GetPath();
198 }
199
200 for( int attempt = 0; attempt < 100; attempt++ )
201 {
202 wxString candidate =
203 attempt == 0 ? aPreferredPath : wxString::Format( wxS( "%s.%d" ), aPreferredPath, attempt );
204
205 // Exclusive create, so two names are never handed out for one file.
206 wxFile out;
207
208 if( out.Create( candidate, false ) )
209 {
210 aPathOut = candidate;
211 return true;
212 }
213
214 // Free and still not creatable means the filesystem is refusing, and counting up
215 // will not change that.
216 if( !wxFileName::FileExists( candidate ) )
217 return false;
218 }
219
220 return false;
221}
222
223
225 GIT_COMMIT_PANE( aParent, aParent )
226{
227}
228
229
231 GIT_COMMIT_PANE_BASE( aParent ),
232 m_frame( aFrame ),
233 m_hasUnpushedCommits( false ),
234 m_hasRemote( false ),
235 m_headDetached( false ),
236 m_upstreamAhead( false ),
237 m_operationInProgress( false ),
239 m_editorsDirty( false )
240{
241 m_summary->SetHint( _( "Summarize your changes..." ) );
242 m_newBranchName->SetHint( _( "Branch name" ) );
243
244 // Sized by hand, or the longest message would set the width of the whole row.
245 m_recentMessages->SetMinSize( FromDIP( wxSize( 170, -1 ) ) );
247
249
250 // Only a platform without a native hint for a multi line box gets the label laid over it.
251 if( !m_description->SetHint( _( "Extended Description (optional)" ) ) )
252 {
253 m_descriptionHint = new wxStaticText( this, wxID_ANY, _( "Extended Description (optional)" ) );
254 m_descriptionHint->SetForegroundColour( wxSystemSettings::GetColour( wxSYS_COLOUR_GRAYTEXT ) );
255 m_descriptionHint->Bind( wxEVT_LEFT_DOWN,
256 [this]( wxMouseEvent& )
257 {
258 m_description->SetFocus();
259 } );
260 }
261
263 m_settingsButton->Bind( wxEVT_BUTTON, &GIT_COMMIT_PANE::onSettings, this );
264 m_commitButton->Bind( wxEVT_BUTTON, &GIT_COMMIT_PANE::onCommit, this );
265
266 // Commit from the keyboard, since the description box takes a plain Return as a new line.
267 // Adding Shift does what the button's menu calls Commit and Push.
268 auto commitOnEnter = [this]( wxKeyEvent& aEvent )
269 {
270 bool enter = aEvent.GetKeyCode() == WXK_RETURN || aEvent.GetKeyCode() == WXK_NUMPAD_ENTER;
271
272 if( enter && ( aEvent.CmdDown() || aEvent.ControlDown() ) )
273 {
274 if( aEvent.ShiftDown() )
275 {
276 if( m_commitAndPushItem->IsEnabled() )
277 {
278 doCommit( true );
279 return;
280 }
281 }
282 else if( m_commitButton->IsEnabled() )
283 {
285 return;
286 }
287 }
288
289 aEvent.Skip();
290 };
291
292 m_summary->Bind( wxEVT_CHAR_HOOK, commitOnEnter );
293 m_description->Bind( wxEVT_CHAR_HOOK, commitOnEnter );
294 m_commitAndPushItem = m_commitButton->GetSplitButtonMenu()->Append( wxID_ANY, _( "Commit and Push" ) );
295 m_commitButton->GetSplitButtonMenu()->Bind( wxEVT_MENU, &GIT_COMMIT_PANE::onCommitAndPush, this,
296 m_commitAndPushItem->GetId() );
297 m_pullButton->Bind( wxEVT_BUTTON, &GIT_COMMIT_PANE::onPull, this );
298 m_amendCheckbox->Bind( wxEVT_CHECKBOX, &GIT_COMMIT_PANE::onAmendToggled, this );
299 m_summary->Bind( wxEVT_TEXT, &GIT_COMMIT_PANE::onMessageChanged, this );
300 m_newBranchName->Bind( wxEVT_TEXT,
301 [this]( wxCommandEvent& )
302 {
304 } );
305 m_newBranchCheckbox->Bind( wxEVT_CHECKBOX, &GIT_COMMIT_PANE::onNewBranchToggled, this );
306 m_recentMessages->Bind( wxEVT_CHOICE, &GIT_COMMIT_PANE::onRecentMessage, this );
307 m_showDiffButton->Bind( wxEVT_BUTTON, &GIT_COMMIT_PANE::onShowDiff, this );
308 m_showDiffButton->SetToolTip( _( "Show what changed in the selected file. Pick a file in the list first." ) );
309 m_description->Bind( wxEVT_TEXT,
310 [this]( wxCommandEvent& )
311 {
313
316 } );
317 m_checklist->Bind( wxEVT_DATAVIEW_ITEM_VALUE_CHANGED, &GIT_COMMIT_PANE::onChecklistChanged, this );
318 m_checklist->SetToolTip( _( "Double click a file to see what changed in it. Right click for more." ) );
319 m_checklist->Bind( wxEVT_DATAVIEW_ITEM_ACTIVATED, &GIT_COMMIT_PANE::onFileActivated, this );
320 m_checklist->Bind( wxEVT_DATAVIEW_ITEM_CONTEXT_MENU, &GIT_COMMIT_PANE::onChecklistContextMenu, this );
321 m_checklist->Bind( wxEVT_DATAVIEW_SELECTION_CHANGED,
322 [this]( wxDataViewEvent& )
323 {
324 m_showDiffButton->Enable( m_checklist->GetSelection().IsOk() );
325
327 m_fileSelectedHandler( m_checklist->GetItemPath( m_checklist->GetSelection() ) );
328 } );
329 Bind( wxEVT_SIZE, &GIT_COMMIT_PANE::onSize, this );
330 Bind( wxEVT_IDLE, &GIT_COMMIT_PANE::onIdle, this );
331
332 // Inside the advanced window the page is wide enough for the list and the message side
333 // by side.
334 m_inAdvancedWindow = aParent != aFrame;
335
337 layoutWide();
338
339 // Sized by hand, or a long branch name would set the width of the whole row. Wide enough
340 // for an ordinary name, with the tooltip carrying the rest when one is longer.
341
342 // Measured while every control is still shown.
343 m_minDockWidth = GetSizer()->CalcMin().x;
344
346}
347
348
350{
351 wxSizer* column = GetSizer();
352 column->Detach( m_changesRow );
353 column->Detach( m_emptyLabel );
354 column->Detach( m_checklist );
355 column->GetItem( m_description )->SetProportion( 1 );
356 column->SetMinSize( FromDIP( wxSize( 280, -1 ) ) );
357
358 wxStaticText* heading = new wxStaticText( this, wxID_ANY, _( "Commit Message" ) );
359 heading->SetFont( heading->GetFont().Bold() );
360 column->Insert( 0, heading, 0, wxLEFT | wxRIGHT | wxTOP, 8 );
361
362 // Use the diff colors for the summary's line counts.
363 wxStaticBoxSizer* summary = new wxStaticBoxSizer( wxVERTICAL, this, _( "Summary" ) );
364 wxWindow* box = summary->GetStaticBox();
365
366 m_summaryFiles = new wxStaticText( box, wxID_ANY, wxEmptyString );
367 m_summaryAdded = new wxStaticText( box, wxID_ANY, wxEmptyString );
368 m_summaryRemoved = new wxStaticText( box, wxID_ANY, wxEmptyString );
369
371 m_summaryAdded->SetForegroundColour( theme.added.ToColour() );
372 m_summaryRemoved->SetForegroundColour( theme.removed.ToColour() );
373
374 wxBoxSizer* lines = new wxBoxSizer( wxHORIZONTAL );
375 lines->Add( m_summaryAdded, 0, wxRIGHT, 8 );
376 lines->Add( m_summaryRemoved, 0 );
377 summary->Add( m_summaryFiles, 0, wxLEFT | wxRIGHT | wxTOP, 4 );
378 summary->Add( lines, 0, wxALL, 4 );
379
380 // Under the commit options box.
381 size_t afterOptions = column->GetItemCount();
382
383 for( size_t ii = 0; ii < column->GetItemCount(); ++ii )
384 {
385 if( column->GetItem( ii )->GetSizer() == m_optionsSizer )
386 afterOptions = ii + 1;
387 }
388
389 column->Insert( afterOptions, summary, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, 8 );
390
391 wxBoxSizer* files = new wxBoxSizer( wxVERTICAL );
392 files->Add( m_changesRow, 0, wxEXPAND | wxLEFT | wxRIGHT | wxTOP, 8 );
393 files->Add( m_emptyLabel, 0, wxALL, 8 );
394 files->Add( m_checklist, 1, wxALL | wxEXPAND, 4 );
395
396 wxBoxSizer* row = new wxBoxSizer( wxHORIZONTAL );
397 row->Add( files, 1, wxEXPAND );
398 row->Add( column, 0, wxEXPAND );
399
400 // The old sizer lives on as the right column.
401 SetSizer( row, false );
402 Layout();
403}
404
405
406void GIT_COMMIT_PANE::SetFileSelectedHandler( std::function<void( const wxString& )> aHandler )
407{
408 m_fileSelectedHandler = std::move( aHandler );
409}
410
411
413{
414 m_refresh.Cancel();
415 m_actionQuery.Cancel();
416
417 // The window's Pull and Push run through this pane, so it goes when the pane goes.
419
420 // The transfer outlives this pane, and its completion refreshes the other windows, so
421 // nothing has to be reported here. Asking it to stop is the destructor's own job.
422 if( std::shared_ptr<KIGIT_COMMON> common = m_listenerSession.lock() )
423 common->RemoveRefreshListener( this );
424}
425
426
428{
429 std::shared_ptr<KIGIT_COMMON> common = m_frame->Prj().GetGitCommon();
430
431 if( m_listenerSession.lock() == common )
432 return;
433
434 // Loading a project replaces the git session, so move the listener to the new one.
435 if( std::shared_ptr<KIGIT_COMMON> previous = m_listenerSession.lock() )
436 previous->RemoveRefreshListener( this );
437
438 // The window still lists the previous project, so it closes with the project that filled it.
439 // A first attach replaced nothing, and must not take the window from whoever opened it.
442
443 m_refresh.Cancel();
444 m_actionQuery.Cancel();
445
446 if( m_actionPending )
447 {
448 m_actionPending = false;
449 setBusy( false, wxEmptyString );
450 }
451 m_hasRepo = false;
452
453 m_amendCheckbox->SetValue( false );
454 m_amendHead = {};
455 m_listenerAttached = true;
456
457 // The remembered paths describe the previous project's file list, so a path that repeats in
458 // the new project would inherit the old project's tick state.
459 m_shownPaths.clear();
460
461 common->AddRefreshListener( this,
462 [this]()
463 {
465 } );
466 m_listenerSession = common;
467}
468
469
471{
472 bool hasRepo = false;
473 bool hasLocalCommits = false;
474 bool hasRemote = false;
475 bool headDetached = false;
477 bool upstreamAhead = false;
479 wxString attachError;
481 std::vector<FILE_CHECKLIST_ENTRY> entries;
482 std::vector<KIGIT_BRANCH_INFO> branches;
483 std::vector<wxString> recentMessages;
484 std::pair<int, int> lineStats{ 0, 0 };
485};
486
487
489{
490 syncListener();
491
492 // A session attached before the preference was turned off is still bound.
493 if( !Pgm().GetCommonSettings()->m_Git.enableGit )
494 {
495 m_refresh.Cancel();
497 return;
498 }
499
500 std::shared_ptr<KIGIT_COMMON> common = m_frame->Prj().GetGitCommon();
501 wxString projectPath = m_frame->Prj().GetProjectPath();
502 bool dark = KIPLATFORM::UI::IsDarkTheme();
503 bool countLines = m_summaryFiles != nullptr;
504 auto status = std::make_shared<STATUS>();
505 status->attachError = common->GetAttachError();
506
507 m_refresh.Run(
508 common,
509 [status, projectPath, dark, countLines]( KIGIT_COMMON& aCommon )
510 {
511 status->hasRepo = true;
512 status->entries = BuildGitChangeEntries( aCommon, projectPath, dark );
513 status->hasLocalCommits = aCommon.HasLocalCommits();
514 status->hasRemote = aCommon.HasPushAndPullRemote();
515 status->headDetached = aCommon.IsHeadDetached();
516 status->operationInProgress = aCommon.IsOperationInProgress();
517 status->operation = aCommon.OperationInProgress();
518 status->upstreamAhead = aCommon.HasUpstreamCommits();
519 status->branches = aCommon.GetBranches();
520 KIGIT::SortBranchesForDisplay( status->branches );
521 status->currentBranch = aCommon.GetCurrentBranchName();
522 status->recentMessages = aCommon.GetRecentCommitMessages( 10 );
523
524 if( countLines )
525 {
526 std::set<wxString> changed;
527
528 for( const FILE_CHECKLIST_ENTRY& entry : status->entries )
529 changed.insert( entry.path );
530
531 status->lineStats = aCommon.GetWorkingTreeLineStats( changed );
532 }
533 },
534 [this, common, status]()
535 {
536 if( m_frame->Prj().GetGitCommon() == common && !common->IsShuttingDown()
537 && Pgm().GetCommonSettings()->m_Git.enableGit )
538 {
539 applyStatus( *status );
540 }
541 } );
542}
543
544
546{
547 std::vector<FILE_CHECKLIST_ENTRY> entries = aStatus.entries;
548 m_hasRepo = aStatus.hasRepo;
549 m_hasRemote = aStatus.hasRemote;
552 m_operation = aStatus.operation;
554
555 // Without a remote there is nowhere to push to, so local commits are not waiting for it.
557 m_lineStats = aStatus.lineStats;
558 populateBranches( aStatus );
560
561 // Allow saving unsaved editor changes for review even when no files are listed yet.
562 m_editorsDirty = aStatus.hasRepo && DocumentEditorsDirty( m_frame->Kiway() );
563
564 // Amending is offered only while the last commit has not been pushed, so an already
565 // published commit can never be rewritten from here.
567
568 // Preserve unticked files across refreshes. New files start unchecked while amending.
569 std::set<wxString> stillChecked;
570
571 for( const wxString& path : m_checklist->GetCheckedPaths() )
572 stillChecked.insert( path );
573
574 for( FILE_CHECKLIST_ENTRY& entry : entries )
575 {
576 if( ( m_shownPaths.count( entry.path ) && !stillChecked.count( entry.path ) )
577 || ( m_amendCheckbox->IsChecked() && !m_shownPaths.count( entry.path ) ) )
578 entry.checked = false;
579 }
580
581 m_shownPaths.clear();
582
583 for( const FILE_CHECKLIST_ENTRY& entry : entries )
584 m_shownPaths.insert( entry.path );
585
586 m_checklist->SetEntries( entries );
588
589 bool hasChanges = !entries.empty();
590
591 m_checklist->Show( hasChanges );
592 m_changesRow->ShowItems( hasChanges );
593
594 // The message field also has to be there for a message-only amend and for committing
595 // unsaved editor changes, when the list itself is empty.
596 updateMessageVisibility( hasChanges );
597
599 {
600 setStatusText( wxString::Format( _( "This project is in the middle of a %s that was started outside "
601 "KiCad or interrupted. Abandon it to go back to the last commit, "
602 "or finish it with the program that started it." ),
603 operationNoun() ) );
604 m_emptyLabel->Show( true );
605 }
606 else if( m_headDetached )
607 {
608 setStatusText( _( "This project is showing an old snapshot rather than a branch, so it cannot be "
609 "committed or pushed from here. In the Advanced Git window, choose Branches "
610 "and double click a branch to go back to it." ) );
611 m_emptyLabel->Show( true );
612 }
613 else if( hasChanges )
614 {
615 m_emptyLabel->Show( false );
616 }
617 else
618 {
619 if( !Pgm().GetCommonSettings()->m_Git.enableGit )
620 setStatusText( _( "Version control is turned off in Preferences." ) );
621 else if( !aStatus.hasRepo && !aStatus.attachError.IsEmpty() )
622 setStatusText( aStatus.attachError );
623 else if( !aStatus.hasRepo )
624 setStatusText( _( "This project is not under version control. To set it up, right click "
625 "the project in the project manager and choose Version Control." ) );
626 else if( m_upstreamAhead )
627 setStatusText( _( "The tracked branch has changes this project does not. Get Changes brings them in." ) );
628 else if( m_hasUnpushedCommits )
629 setStatusText( _( "Committed changes are waiting to be pushed." ) );
630 else
631 setStatusText( _( "No changes" ) );
632
633 m_emptyLabel->Show( true );
634 }
635
637 Layout();
639}
640
641
643{
645 dialog->ReloadPages();
646}
647
648
650{
651 m_actionQuery.Cancel();
652
653 if( m_actionPending )
654 {
655 m_actionPending = false;
656 setBusy( false, wxEmptyString );
657 }
658 m_hasRepo = false;
659 m_hasUnpushedCommits = false;
660 m_hasRemote = false;
661 m_headDetached = false;
662 m_operationInProgress = false;
664 m_upstreamAhead = false;
665 m_editorsDirty = false;
666
667 m_shownPaths.clear();
668 m_checklist->SetEntries( {} );
669 m_checklist->Show( false );
670 m_changesRow->ShowItems( false );
671 m_branchName->SetLabel( wxEmptyString );
672 m_branchRow->ShowItems( false );
673 m_settingsButton->Show();
674
675 m_canAmend = false;
676 m_amendCheckbox->SetValue( false );
677 m_amendHead = {};
679
680 setStatusText( _( "Version control is turned off in Preferences." ) );
681 m_emptyLabel->Show( true );
682
684 Layout();
685}
686
687
688void GIT_COMMIT_PANE::setStatusText( const wxString& aText )
689{
690 m_statusText = aText;
691
692 // Wrap to the panel width so a long line is not clipped when the pane is narrow.
693 m_emptyLabel->SetLabel( m_statusText );
694 m_emptyLabel->Wrap( std::max( 40, GetClientSize().GetWidth() - 16 ) );
695}
696
697
698void GIT_COMMIT_PANE::onIdle( wxIdleEvent& aEvent )
699{
700 aEvent.Skip();
701
702 // Every layout path moves the box, so the hint follows it from here.
704
705 // Editing does not announce itself to the pane, so watch for the unsaved editor state
706 // changing while the pane is visible. The refresh runs only on the transition.
707 if( !Pgm().GetCommonSettings()->m_Git.enableGit || m_transfer.IsBusy() || m_actionPending || m_refresh.IsBusy()
708 || !IsShownOnScreen() )
709 return;
710
711 bool dirty = m_hasRepo && DocumentEditorsDirty( m_frame->Kiway() );
712
713 if( dirty != m_editorsDirty )
715}
716
717
718void GIT_COMMIT_PANE::onSize( wxSizeEvent& aEvent )
719{
720 aEvent.Skip();
721
722 if( m_emptyLabel && m_emptyLabel->IsShown() && !m_statusText.IsEmpty() )
724}
725
726
727void GIT_COMMIT_PANE::onSettings( wxCommandEvent& aEvent )
728{
729 wxMenu menu;
730
731 // Inside the advanced window the entry would only raise the window it is in.
732 if( !m_inAdvancedWindow )
733 {
734 menu.Append( ID_OPEN_ADVANCED, _( "Open Advanced Git Window..." ) );
735 menu.Bind( wxEVT_MENU,
736 [this]( wxCommandEvent& )
737 {
738 // The window belongs to the session, not to this panel, so all three
739 // frames raise the same one.
741 },
742 ID_OPEN_ADVANCED );
743 }
744
745 menu.Append( ID_SET_AUTHOR, _( "Set Author..." ) );
746 menu.Bind( wxEVT_MENU,
747 [this]( wxCommandEvent& )
748 {
749 setAuthor();
750 },
751 ID_SET_AUTHOR );
752
753 PopupMenu( &menu );
754}
755
756
758{
759 std::shared_ptr<KIGIT_COMMON> common = m_frame->Prj().GetGitCommon();
760 auto identity = std::make_shared<KIGIT::GIT_IDENTITY>();
761
763 common,
764 [identity]( KIGIT_COMMON& aSession )
765 {
766 *identity = KIGIT::ReadGitIdentity( aSession );
767 },
768 [this, common, identity]()
769 {
770 KIGIT::EditGitIdentity( this, m_frame, common, *identity );
771 },
772 _( "Reading author..." ) );
773}
774
776{
777 // A commit is possible with listed changes, unsaved editor changes, or an amend. Both callers
778 // share this, so the fields cannot disappear while the buttons still ask for a message.
779 bool show = aHasChanges || m_editorsDirty || m_amendCheckbox->IsChecked();
780
781 m_summary->Show( show );
782 m_summaryCount->Show( show );
783 m_description->Show( show );
784
785 // The options box stays for a message only amend, with just the amend tick in it.
786 m_optionsSizer->ShowItems( show || m_canAmend );
787 m_amendCheckbox->Show( m_canAmend || m_amendCheckbox->IsChecked() );
788 m_signOffCheckbox->Show( show );
789 m_newBranchCheckbox->Show( show );
790 m_newBranchName->Show( show );
791}
792
793
795{
796 switch( m_operation )
797 {
798 case GitOperation::Merge: return _( "merge" );
799 case GitOperation::Rebase: return _( "rebase" );
800 case GitOperation::CherryPick: return _( "cherry-pick" );
801 case GitOperation::Revert: return _( "revert" );
802 default: return _( "git operation" );
803 }
804}
805
806
808{
809 switch( m_operation )
810 {
811 case GitOperation::Merge: return _( "Abandon Merge" );
812 case GitOperation::Rebase: return _( "Abandon Rebase" );
813 case GitOperation::CherryPick: return _( "Abandon Cherry-pick" );
814 case GitOperation::Revert: return _( "Abandon Revert" );
815 default: return _( "Abandon Operation" );
816 }
817}
818
819
821{
822 // While a transfer runs the busy state owns the controls. Checklist or message edits
823 // must not re-enable the buttons under it.
824 if( m_transfer.IsBusy() || m_actionPending )
825 return;
826
828 {
829 m_commitButton->Enable( false );
830 m_pullButton->SetLabel( abandonLabel() );
831 m_pullButton->Enable( true );
832 return;
833 }
834
835 if( m_headDetached )
836 {
837 m_commitButton->Enable( false );
838 m_pullButton->Enable( false );
839 return;
840 }
841
842 m_pullButton->SetLabel( _( "Get Changes" ) );
843
844 // The empty state label only shows when there is nothing to commit, so the button carries
845 // the notice the rest of the time.
846 if( m_upstreamAhead )
847 m_pullButton->SetToolTip( _( "The tracked branch has commits this project does not have." ) );
848 else
849 m_pullButton->UnsetToolTip();
850
851 bool hasCheckedChanges = !m_checklist->GetCheckedPaths().empty();
852 bool amend = m_amendCheckbox->IsChecked();
853 bool newBranch = !amend && m_newBranchCheckbox->IsChecked();
854 bool hasMessage = !m_summary->GetValue().Trim().IsEmpty();
855 bool hasBranchName = !newBranch || !m_newBranchName->GetValue().Trim().IsEmpty();
856
857 // An amend stays on the branch its commit is on.
858 m_newBranchCheckbox->Enable( !amend );
859 m_newBranchName->Enable( newBranch );
860
861 wxString mainLabel = _( "Commit" );
862 wxString menuLabel = _( "Commit and Push" );
863 bool mainEnabled = false;
864 bool menuEnabled = false;
865 m_commitButtonPushes = false;
866
867 // A disabled button with nothing said about it reads as broken, so each state the user can
868 // act on names what it is waiting for.
869 wxString blocked;
870
871 if( amend )
872 {
873 // A message-only amend needs no checked files, just a message.
874 mainLabel = _( "Amend" );
875 menuLabel = _( "Amend and Push" );
876 mainEnabled = hasMessage && m_canAmend;
877 menuEnabled = hasMessage && m_canAmend;
878
879 if( !m_canAmend )
880 blocked = _( "This commit can no longer be amended. Untick Amend to start a new commit." );
881 else if( !hasMessage )
882 blocked = _( "Write a summary to amend the last commit." );
883 }
884 else if( hasCheckedChanges || m_editorsDirty )
885 {
886 // The commit button also starts saving unsaved editor changes for review.
887 mainEnabled = hasMessage && hasBranchName;
888 menuEnabled = hasMessage && hasBranchName;
889
890 if( !hasMessage )
891 blocked = _( "Write a summary to commit." );
892 else if( !hasBranchName )
893 blocked = _( "Name the new branch, or untick Create a new branch." );
894 }
895 else if( m_hasUnpushedCommits )
896 {
897 mainLabel = _( "Push" );
898 mainEnabled = true;
900 }
901 else if( !m_shownPaths.empty() )
902 {
903 blocked = _( "Tick a file to commit it." );
904 }
905
906 m_commitButton->SetLabel( mainLabel );
907 m_commitButton->Enable( mainEnabled );
908 m_commitAndPushItem->SetItemLabel( menuLabel );
909 m_commitAndPushItem->Enable( menuEnabled );
910
911 // Unsaved editor changes are a fact about the commit the user is about to make, so this
912 // goes beside the file list rather than only where there is nothing else to say.
913 wxString reason;
914
915 if( m_editorsDirty )
916 reason = _( "The open editors have unsaved changes. Committing offers to save them first." );
917
918 // A disabled control shows no tooltip, so the one line the panel has says why Get Changes
919 // is off.
921 {
922 if( !reason.IsEmpty() )
923 reason += wxS( "\n" );
924
925 reason += _( "This project has no remote, so there is nothing to get changes from or push to. "
926 "Add one from the Remotes page of the Advanced Git window." );
927 }
928
929 if( !blocked.IsEmpty() )
930 {
931 if( !reason.IsEmpty() )
932 reason += wxS( "\n" );
933
934 reason += blocked;
935 }
936
937 if( !reason.IsEmpty() )
938 setStatusText( reason );
939
940 // With nothing listed the empty state owns the line and has already set its own text.
941 bool showReason = !reason.IsEmpty() || m_shownPaths.empty();
942
943 if( m_emptyLabel->IsShown() != showReason )
944 {
945 m_emptyLabel->Show( showReason );
946 Layout();
947 }
948
949 // Pushing and pulling need a remote. Committing does not.
950 m_pullButton->Enable( m_hasRemote );
951}
952
953
955{
956 // Git tools show the first line whole up to 72 columns.
957 int left = 72 - static_cast<int>( m_summary->GetValue().length() );
958 m_summaryCount->SetLabel( wxString::Format( wxS( "%d" ), left ) );
959}
960
961
962void GIT_COMMIT_PANE::onMessageChanged( wxCommandEvent& aEvent )
963{
966
969}
970
971
973{
974 return KIGIT::ComposeCommitMessage( m_summary->GetValue(), m_description->GetValue(), wxEmptyString );
975}
976
977
978void GIT_COMMIT_PANE::SetCommitMessage( const wxString& aMessage )
979{
980 wxString summary;
981 wxString description;
982
983 KIGIT::SplitCommitMessage( aMessage, summary, description );
984
985 // ChangeValue rather than SetValue, so filling one form does not echo back into the other.
986 m_summary->ChangeValue( summary );
987 setDescription( description );
990}
991
992
993void GIT_COMMIT_PANE::onNewBranchToggled( wxCommandEvent& aEvent )
994{
996
997 if( m_newBranchCheckbox->IsChecked() )
998 m_newBranchName->SetFocus();
999}
1000
1001
1002void GIT_COMMIT_PANE::onShowDiff( wxCommandEvent& aEvent )
1003{
1004 wxString path = m_checklist->GetItemPath( m_checklist->GetSelection() );
1005
1006 if( !path.IsEmpty() )
1008}
1009
1010
1012{
1013 m_branchName->SetLabel( wxEmptyString );
1014 m_branchRow->ShowItems( aStatus.hasRepo );
1015
1016 // The advanced window is reachable without a repository, as the old button was.
1017 m_settingsButton->Show();
1018
1019 if( !aStatus.hasRepo )
1020 return;
1021
1022 // This says where a commit will land. Switching branch is a different act, and it belongs
1023 // to the project tree and the advanced window, which is where the surfaces for it are.
1025
1026 wxString shown = m_currentBranch;
1027
1028 // Detached, or no commits yet, leaves no branch to name. Say which rather than show
1029 // nothing at all.
1030 if( shown.IsEmpty() )
1031 shown = m_headDetached ? _( "Not on a branch" ) : _( "No commits yet" );
1032
1033 m_branchName->SetLabel( shown );
1034 m_branchRow->Layout();
1035}
1036
1037
1038void GIT_COMMIT_PANE::populateRecentMessages( const std::vector<wxString>& aMessages )
1039{
1040 m_recentMessageTexts = aMessages;
1041
1042 // A dropdown always shows its selection, so the first entry is the caption.
1043 m_recentMessages->Clear();
1044 m_recentMessages->Append( _( "Recent messages" ) );
1045
1046 for( const wxString& message : m_recentMessageTexts )
1047 {
1048 wxString line = message.BeforeFirst( '\n' );
1049
1050 if( line.length() > 40 )
1051 line = line.Left( 37 ) + wxS( "..." );
1052
1053 m_recentMessages->Append( line );
1054 }
1055
1056 m_recentMessages->SetSelection( 0 );
1057 m_recentMessages->Show( !m_recentMessageTexts.empty() );
1058}
1059
1060
1061void GIT_COMMIT_PANE::onRecentMessage( wxCommandEvent& aEvent )
1062{
1063 int picked = m_recentMessages->GetSelection() - 1;
1064 wxString summary, description;
1065
1066 // The caption entry starts the message over.
1067 if( picked >= 0 && picked < static_cast<int>( m_recentMessageTexts.size() ) )
1068 KIGIT::SplitCommitMessage( m_recentMessageTexts[picked], summary, description );
1069
1070 m_summary->SetValue( summary );
1071 setDescription( description );
1072 m_recentMessages->SetSelection( 0 );
1073}
1074
1075
1077{
1078 if( !m_descriptionHint )
1079 return;
1080
1081 // Over the box's top left corner, where typed text would start.
1082 bool show = m_description->IsShown() && m_description->IsEmpty();
1083 wxPoint at = m_description->GetPosition() + wxPoint( FromDIP( 6 ), FromDIP( 4 ) );
1084
1085 if( m_descriptionHint->IsShown() != show )
1086 m_descriptionHint->Show( show );
1087
1088 if( m_descriptionHint->GetPosition() != at )
1089 m_descriptionHint->SetPosition( at );
1090}
1091
1092
1093void GIT_COMMIT_PANE::setDescription( const wxString& aText )
1094{
1095 m_description->ChangeValue( aText );
1097}
1098
1099
1100void GIT_COMMIT_PANE::onChecklistChanged( wxDataViewEvent& aEvent )
1101{
1103 updateSummary();
1104}
1105
1106
1108{
1109 // A ticked file is staged for the commit, an unticked one is left out.
1110 int staged = static_cast<int>( m_checklist->GetCheckedPaths().size() );
1111 int unstaged = static_cast<int>( m_shownPaths.size() ) - staged;
1112
1113 m_changesLabel->SetLabel( wxString::Format( _( "Changes to Commit (%d of %d)" ), staged,
1114 static_cast<int>( m_shownPaths.size() ) ) );
1115 m_showDiffButton->Enable( m_checklist->GetSelection().IsOk() );
1116
1117 if( m_summaryFiles )
1118 {
1119 wxString stagedText = staged == 1 ? _( "1 file will be committed" )
1120 : wxString::Format( _( "%d files will be committed" ), staged );
1121 wxString unstagedText = unstaged == 1 ? _( "1 left out" )
1122 : wxString::Format( _( "%d left out" ), unstaged );
1123
1124 m_summaryFiles->SetLabel( wxString::Format( wxS( "%s, %s" ), stagedText, unstagedText ) );
1125 m_summaryAdded->SetLabel( wxString::Format( wxS( "+%d" ), m_lineStats.first ) );
1126 m_summaryRemoved->SetLabel( wxString::Format( wxS( "-%d" ), m_lineStats.second ) );
1127 }
1128
1129 Layout();
1130}
1131
1132
1133void GIT_COMMIT_PANE::onChecklistContextMenu( wxDataViewEvent& aEvent )
1134{
1135 if( m_transfer.IsBusy() || m_actionPending )
1136 return;
1137
1138 wxString path = m_checklist->GetItemPath( aEvent.GetItem() );
1139
1140 if( path.IsEmpty() )
1141 return;
1142
1143 wxMenu menu;
1144 menu.Append( 1, _( "Show History for This File" ) );
1145 menu.AppendSeparator();
1146 menu.Append( 2, _( "Discard Changes" ) );
1147
1148 switch( GetPopupMenuSelectionFromUser( menu ) )
1149 {
1150 case 1:
1151 // From the Changes page the window is already open, so it only turns to History.
1153 dialog->ShowHistoryForFile( path );
1154 else
1156
1157 break;
1158
1159 case 2:
1160 discardFile( m_frame->Prj().GetGitCommon(), path );
1161 break;
1162
1163 default:
1164 break;
1165 }
1166}
1167
1168
1169void GIT_COMMIT_PANE::discardFile( std::shared_ptr<KIGIT_COMMON> aCommon, const wxString& aPath )
1170{
1171 struct FILE_STATE
1172 {
1173 bool tracked = false;
1174 bool conflicted = false;
1175 wxString root;
1176 };
1177
1178 auto state = std::make_shared<FILE_STATE>();
1179
1181 aCommon,
1182 [state, aPath]( KIGIT_COMMON& aSession )
1183 {
1184 state->tracked = aSession.IsPathInHead( aPath );
1185 state->conflicted = aSession.IsPathConflicted( aPath );
1186 state->root = aSession.GetGitRootDirectory();
1187 },
1188 [this, aCommon, aPath, state]()
1189 {
1190 confirmDiscardFile( aCommon, aPath, state->tracked, state->conflicted, state->root );
1191 },
1192 _( "Checking changes..." ) );
1193}
1194
1195
1196void GIT_COMMIT_PANE::confirmDiscardFile( std::shared_ptr<KIGIT_COMMON> aCommon, const wxString& aPath, bool aTracked,
1197 bool aConflicted, wxString aRoot )
1198{
1199 // An unsaved editor's next save would undo the discard, so settle that first.
1200 if( EditorsDirty( m_frame->Kiway() ) )
1201 {
1202 DisplayInfoMessage( this, _( "An editor has unsaved changes. Save or revert them before discarding "
1203 "files, so the discard is not undone by the next save." ) );
1204 return;
1205 }
1206
1207 wxString name = wxFileName( aPath ).GetFullName();
1208 wxString question;
1209
1210 if( aConflicted )
1211 {
1212 // Putting back what HEAD holds also clears the conflict, so the file is settled rather
1213 // than only reverted.
1214 question = wxString::Format( _( "Resolve the conflict in '%s' by going back to the version this "
1215 "project was on?\n\n"
1216 "The version that arrived is dropped, and the file no longer needs "
1217 "resolving." ),
1218 name );
1219 }
1220 else if( aTracked )
1221 {
1222 question = wxString::Format( _( "Throw away the changes to '%s'?\n\n"
1223 "The file goes back to the last committed version." ),
1224 name );
1225 }
1226 else
1227 {
1228 question = wxString::Format( _( "Move '%s' to the trash?\n\n"
1229 "It has never been committed, so there is no version to bring back." ),
1230 name );
1231 }
1232
1233 wxMessageDialog dlg( this, question, _( "Discard Changes" ), wxYES_NO | wxNO_DEFAULT | wxICON_WARNING );
1234 dlg.SetYesNoLabels( aConflicted ? _( "Resolve" )
1235 : aTracked ? _( "Discard" )
1236 : _( "Move to Trash" ),
1237 _( "Cancel" ) );
1238
1239 if( dlg.ShowModal() != wxID_YES )
1240 return;
1241
1242 if( aCommon != m_frame->Prj().GetGitCommon() || aCommon->IsShuttingDown() )
1243 return;
1244
1245 if( EditorsDirty( m_frame->Kiway() ) )
1246 return;
1247
1248 // Snapshot first, so an editor showing the file reloads the committed version afterwards.
1249 EDITOR_SNAPSHOT docsBefore = CaptureOpenEditorDocs( m_frame->Kiway() );
1250
1251 // Dropping the staged entry before touching the file means a contended lock or an index
1252 // failure leaves everything unchanged.
1253 std::unique_lock<std::mutex> lock = KIGIT::TryLockAction( *aCommon, this );
1254
1255 if( !lock.owns_lock() )
1256 return;
1257
1258 // The question above ran without the lock, so the project may have closed meanwhile.
1259 if( !aCommon->GetRepo() )
1260 return;
1261
1262 wxString error;
1263
1264 if( !aCommon->DiscardChanges( { aPath }, error ) )
1265 {
1266 lock.unlock();
1267 DisplayErrorMessage( this, wxString::Format( _( "Could not discard the changes to '%s'." ), name ),
1268 error );
1269 return;
1270 }
1271
1272 lock.unlock();
1273
1274 if( !aTracked )
1275 {
1276 if( !aRoot.EndsWith( wxS( "/" ) ) )
1277 aRoot += wxS( "/" );
1278
1279 wxFileName fullPath( aRoot + aPath );
1280
1281 // A previous discard can have removed the file already, so a missing file is done,
1282 // not an error.
1283 if( fullPath.FileExists() && !KIPLATFORM::ENV::MoveToTrash( fullPath.GetFullPath(), error ) )
1284 {
1285 // The staged entry is already gone, so the panels still refresh.
1286 aCommon->NotifyRefreshListeners();
1287 DisplayErrorMessage( this,
1288 wxString::Format( _( "Could not move '%s' to the trash. The file is "
1289 "still on disk, so discard it again to retry." ),
1290 name ),
1291 error );
1292 return;
1293 }
1294 }
1295
1296 ReloadOpenEditors( m_frame->Kiway(), docsBefore );
1297 aCommon->NotifyRefreshListeners();
1298}
1299
1300
1301void GIT_COMMIT_PANE::onFileActivated( wxDataViewEvent& aEvent )
1302{
1303 wxString path = m_checklist->GetItemPath( aEvent.GetItem() );
1304
1305 if( !path.IsEmpty() )
1307}
1308
1309
1310void GIT_COMMIT_PANE::onCommit( wxCommandEvent& aEvent )
1311{
1313}
1314
1315
1316void GIT_COMMIT_PANE::onCommitAndPush( wxCommandEvent& aEvent )
1317{
1318 doCommit( true );
1319}
1320
1321
1322void GIT_COMMIT_PANE::onPull( wxCommandEvent& aEvent )
1323{
1324 syncListener();
1325
1326 std::shared_ptr<KIGIT_COMMON> common = m_frame->Prj().GetGitCommon();
1327
1328 if( !common->GetRepo() )
1329 return;
1330
1331 if( m_actionPending )
1332 {
1333 m_actionQuery.Cancel();
1334 m_actionPending = false;
1335 setBusy( false, _( "Cancelled." ) );
1336 return;
1337 }
1338
1339 // While a transfer runs this button reads Cancel. The transfer callbacks poll the flag and
1340 // abort on their next tick, and the completion then reports the cancellation. A stalled
1341 // connection runs no callbacks until it times out, so the cancel cannot be prompt.
1342 if( m_transfer.IsBusy() )
1343 {
1344 m_transfer.Cancel();
1345
1346 m_pullButton->Enable( false );
1347 m_frame->SetStatusText( _( "Cancelling. A stalled connection stops only when it times out." ) );
1348 return;
1349 }
1350
1352 {
1353 abandonOperation( common );
1354 return;
1355 }
1356
1357 Pull();
1358}
1359
1360
1362{
1363 // The Advanced window also calls this, with the panel's status out of sight.
1364 if( m_transfer.IsBusy() || m_actionPending )
1365 {
1366 KIGIT::ShowBusyMessage( this );
1367 return false;
1368 }
1369
1371 {
1372 DisplayInfoMessage( this, wxString::Format( _( "Finish or abandon the %s in progress first." ),
1373 operationNoun() ) );
1374 return false;
1375 }
1376
1377 // The panel disables its own buttons for this, but the advanced window's reach here too.
1378 if( m_headDetached )
1379 {
1380 DisplayInfoMessage( this, _( "This project is showing an old snapshot rather than a branch, so it cannot be "
1381 "committed or pushed from here. In the Advanced Git window, choose Branches "
1382 "and double click a branch to go back to it." ) );
1383 return false;
1384 }
1385
1386 return true;
1387}
1388
1389
1391{
1392 syncListener();
1393
1394 std::shared_ptr<KIGIT_COMMON> common = m_frame->Prj().GetGitCommon();
1395
1396 if( !common->GetRepo() || !transferCanStart() )
1397 return;
1398
1399 if( !m_hasRemote )
1400 {
1401 DisplayInfoMessage( this, _( "No remote is configured to get changes from." ) );
1402 return;
1403 }
1404
1405 submitPull( common, false );
1406}
1407
1408
1410{
1411 syncListener();
1412
1413 std::shared_ptr<KIGIT_COMMON> common = m_frame->Prj().GetGitCommon();
1414
1415 if( !common->GetRepo() || !transferCanStart() )
1416 return;
1417
1418 if( !m_hasRemote )
1419 {
1420 DisplayInfoMessage( this, _( "No remote is configured to push to." ) );
1421 return;
1422 }
1423
1425 {
1426 DisplayInfoMessage( this, _( "There are no commits to push." ) );
1427 return;
1428 }
1429
1430 submitPush( common );
1431}
1432
1433
1434void GIT_COMMIT_PANE::abandonOperation( std::shared_ptr<KIGIT_COMMON> aCommon )
1435{
1436 struct PREPARATION
1437 {
1438 std::vector<wxString> outside;
1439 bool ready = false;
1440 };
1441
1442 auto preparation = std::make_shared<PREPARATION>();
1443 const wxString projectPath = m_frame->Prj().GetProjectPath();
1444
1446 aCommon,
1447 [preparation, projectPath]( KIGIT_COMMON& aSession )
1448 {
1449 // Going back to the last commit covers the whole repository, so work in a
1450 // sibling project has to be named before the user agrees.
1451 preparation->outside = ChangedTrackedFilesOutsideProject( aSession, projectPath );
1452 preparation->ready = true;
1453 },
1454 [this, aCommon, preparation]()
1455 {
1456 if( preparation->ready )
1457 finishAbandonOperation( aCommon, preparation->outside );
1458 },
1459 _( "Checking working files..." ) );
1460}
1461
1462
1463void GIT_COMMIT_PANE::finishAbandonOperation( std::shared_ptr<KIGIT_COMMON> aCommon,
1464 const std::vector<wxString>& aOutside )
1465{
1466 // Saving would only turn unsaved edits into changes that are then thrown away, so ask for
1467 // a decision in the editors first rather than offer a save.
1468 auto canAbandon = [this, aCommon]()
1469 {
1470 if( aCommon != m_frame->Prj().GetGitCommon() || aCommon->IsShuttingDown()
1471 || !Pgm().GetCommonSettings()->m_Git.enableGit )
1472 {
1473 return false;
1474 }
1475
1476 if( EditorsDirty( m_frame->Kiway() ) )
1477 {
1478 DisplayInfoMessage( this, _( "The open editors have unsaved changes. Save or revert them first, so it "
1479 "is clear what is being abandoned." ) );
1480 return false;
1481 }
1482
1483 return true;
1484 };
1485
1486 if( !canAbandon() )
1487 return;
1488
1489 wxString question = wxString::Format( _( "Abandon the %s and go back to the last commit?\n\n"
1490 "Changes made to the files since that commit are discarded." ),
1491 operationNoun() );
1492
1493 if( !aOutside.empty() )
1494 {
1495 wxString listed;
1496
1497 for( const wxString& path : aOutside )
1498 listed += wxS( "\n " ) + path;
1499
1500 question += wxString::Format( _( "\n\nThis also discards uncommitted changes elsewhere in this "
1501 "repository:%s" ),
1502 listed );
1503 }
1504
1505 if( !IsOK( this, question ) || !canAbandon() )
1506 return;
1507
1508 EDITOR_SNAPSHOT docsBefore = CaptureOpenEditorDocs( m_frame->Kiway() );
1509 wxString error;
1510 bool ok = false;
1511
1512 {
1513 std::unique_lock<std::mutex> lock = KIGIT::TryLockAction( *aCommon, this );
1514
1515 if( !lock.owns_lock() )
1516 return;
1517
1518 ok = aCommon->AbandonOperation( error );
1519 }
1520
1521 if( !ok )
1522 {
1523 DisplayErrorMessage( this, wxString::Format( _( "Could not abandon the %s." ), operationNoun() ), error );
1524 return;
1525 }
1526
1527 ReloadOpenEditors( m_frame->Kiway(), docsBefore );
1528 aCommon->NotifyRefreshListeners();
1529 setBusy( false, _( "Back at the last commit." ) );
1530}
1531
1532
1533void GIT_COMMIT_PANE::onAmendToggled( wxCommandEvent& aEvent )
1534{
1535 if( m_amendCheckbox->IsChecked() )
1536 {
1537 BeginAmend();
1538 return;
1539 }
1540
1541 m_amendHead = {};
1543 Layout();
1545}
1546
1547
1548void GIT_COMMIT_PANE::BeginAmend( const wxString& aExpectedCommit )
1549{
1550 struct AMEND_INFO
1551 {
1552 std::pair<wxString, wxString> head;
1553 wxString message;
1554 wxString refusal;
1555 };
1556
1557 std::shared_ptr<KIGIT_COMMON> common = m_frame->Prj().GetGitCommon();
1558 auto info = std::make_shared<AMEND_INFO>();
1559
1561 common,
1562 [info, aExpectedCommit]( KIGIT_COMMON& aSession )
1563 {
1564 info->head = commitHead( aSession.GetRepo() );
1565 info->refusal = amendRefusal( aSession, info->head );
1566
1567 if( !aExpectedCommit.IsEmpty() && aExpectedCommit != info->head.second )
1568 info->refusal = _( "The selected commit is no longer the last commit on this branch." );
1569
1570 if( info->refusal.IsEmpty() )
1571 {
1572 wxString summary, description;
1573 KIGIT::SplitCommitMessage( aSession.GetLastCommitMessage(), summary, description );
1574 info->message = KIGIT::ComposeCommitMessage( summary, description, wxEmptyString );
1575 }
1576 },
1577 [this, common, info, aExpectedCommit]()
1578 {
1579 if( !info->refusal.IsEmpty() )
1580 {
1581 if( m_amendHead.second.IsEmpty() )
1582 m_amendCheckbox->SetValue( false );
1583
1585 DisplayInfoMessage( this, info->refusal );
1586 return;
1587 }
1588
1589 bool fromHistory = !aExpectedCommit.IsEmpty();
1590 wxString draft = GetCommitMessage();
1591 wxWeakRef<GIT_COMMIT_PANE> pane( this );
1592 wxWeakRef<EDA_BASE_FRAME> frame( m_frame );
1593
1594 if( fromHistory && !draft.IsEmpty() && draft != info->message
1595 && !IsOK( this, _( "Replace the current commit message with the selected commit's message?" ) ) )
1596 {
1597 return;
1598 }
1599
1600 if( !pane || pane->IsBeingDeleted() || !frame || frame->IsBeingDeleted()
1601 || common != frame->Prj().GetGitCommon() || common->IsShuttingDown()
1602 || !Pgm().GetCommonSettings()->m_Git.enableGit
1603 || frame->Prj().GetLocalSettings().m_GitIntegrationDisabled )
1604 {
1605 return;
1606 }
1607
1608 if( GetCommitMessage() != draft )
1609 return;
1610
1611 m_canAmend = true;
1612 m_amendHead = info->head;
1613 m_amendCheckbox->SetValue( true );
1614 m_newBranchCheckbox->SetValue( false );
1615
1616 // History starts with a message-only amend. Files can be selected on this form.
1617 if( fromHistory )
1618 m_checklist->SetAllChecked( false );
1619
1620 // Keep a draft when the user ticks Amend while composing a message.
1621 if( fromHistory || draft.IsEmpty() )
1622 {
1623 SetCommitMessage( info->message );
1624
1627 }
1628
1630 Layout();
1632 m_summary->SetFocus();
1633 },
1634 _( "Reading last commit..." ) );
1635}
1636
1637
1639{
1641 bool amend = false;
1642 bool newBranch = false;
1643 bool andPush = false;
1644 bool signOff = false;
1646 wxString branchName;
1647 wxString summary;
1648 wxString description;
1649 wxString lfsMessage;
1651 std::vector<wxString> files;
1652 std::pair<wxString, wxString> head;
1653};
1654
1655
1656void GIT_COMMIT_PANE::doCommit( bool aAndPush )
1657{
1658 syncListener();
1659
1660 if( m_actionPending || m_transfer.IsBusy() )
1661 {
1662 KIGIT::ShowBusyMessage( this );
1663 return;
1664 }
1665
1666 std::shared_ptr<KIGIT_COMMON> common = m_frame->Prj().GetGitCommon();
1667
1668 if( !common->GetRepo() )
1669 return;
1670
1671 const bool amend = m_amendCheckbox->IsChecked();
1672 const bool newBranch = !amend && m_newBranchCheckbox->IsChecked();
1673 wxString branchName = m_newBranchName->GetValue();
1674 branchName.Trim( true ).Trim( false );
1675
1676 if( newBranch )
1677 {
1678 if( wxString nameError = KIGIT::BranchNameError( branchName ); !nameError.IsEmpty() )
1679 {
1680 DisplayErrorMessage( this, nameError );
1681 m_newBranchName->SetFocus();
1682 return;
1683 }
1684 }
1685
1686 std::vector<wxString> files = m_checklist->GetCheckedPaths();
1687
1688 // Unsaved editor changes can be saved for review. With only local commits waiting,
1689 // the button reads "Push".
1690 if( files.empty() && !amend && !DocumentEditorsDirty( m_frame->Kiway() ) )
1691 {
1692 if( !aAndPush || !m_hasUnpushedCommits )
1693 return;
1694
1695 submitPush( common );
1696 return;
1697 }
1698
1699 auto request = std::make_shared<COMMIT_REQUEST>();
1700 request->amend = amend;
1701 request->head = m_amendHead;
1702 request->newBranch = newBranch;
1703 request->andPush = aAndPush;
1704 request->signOff = m_signOffCheckbox->IsChecked();
1705 request->branchName = branchName;
1706 request->displayedBranch = m_currentBranch;
1707 request->summary = m_summary->GetValue();
1708 request->description = m_description->GetValue();
1709 request->files = std::move( files );
1710
1712 common,
1713 [request]( KIGIT_COMMON& aCommon )
1714 {
1715 if( request->amend )
1716 request->amendRefusal = amendRefusal( aCommon, request->head );
1717 else
1718 request->head = commitHead( aCommon.GetRepo() );
1719
1720 request->identity = KIGIT::ReadGitIdentity( aCommon );
1721 request->lfsMessage = KIGIT::PROJECT_GIT_UTILS::LfsRefusal( &aCommon );
1722 },
1723 [this, common, request]()
1724 {
1725 saveForCommit( common, request );
1726 },
1727 _( "Checking changes..." ) );
1728}
1729
1730
1731void GIT_COMMIT_PANE::saveForCommit( std::shared_ptr<KIGIT_COMMON> aCommon, std::shared_ptr<COMMIT_REQUEST> aRequest )
1732{
1733 if( !aRequest->amendRefusal.IsEmpty() )
1734 {
1735 DisplayInfoMessage( this, aRequest->amendRefusal );
1736 aCommon->NotifyRefreshListeners();
1737 return;
1738 }
1739
1740 if( !aRequest->displayedBranch.IsEmpty()
1741 && aRequest->head.first != wxS( "refs/heads/" ) + aRequest->displayedBranch )
1742 {
1743 DisplayInfoMessage( this, _( "The branch changed while preparing the commit. "
1744 "Review the changes and commit again." ) );
1745 aCommon->NotifyRefreshListeners();
1746 return;
1747 }
1748
1749 if( !aRequest->lfsMessage.IsEmpty() )
1750 {
1751 DisplayErrorMessage( this, aRequest->lfsMessage );
1752 return;
1753 }
1754
1755 bool saved = false;
1756
1758 return;
1759
1760 if( aCommon != m_frame->Prj().GetGitCommon() || aCommon->IsShuttingDown() )
1761 return;
1762
1763 if( saved )
1764 {
1765 // Saving can change which files belong in the commit. Show them before committing.
1766 aCommon->NotifyRefreshListeners();
1767 return;
1768 }
1769
1770 finishCommit( aCommon, *aRequest );
1771}
1772
1773
1774void GIT_COMMIT_PANE::finishCommit( std::shared_ptr<KIGIT_COMMON> common, const COMMIT_REQUEST& aRequest )
1775{
1776 const std::vector<wxString>& files = aRequest.files;
1777 const bool amend = aRequest.amend;
1778 const bool newBranch = aRequest.newBranch;
1779 const wxString& branchName = aRequest.branchName;
1780
1781 // Entered on the strength of unsaved editor changes alone, but the user declined the save,
1782 // so nothing reached the working tree and there is nothing to commit.
1783 if( files.empty() && !amend )
1784 {
1785 setBusy( false, _( "Nothing was saved, so there is nothing to commit." ) );
1786 return;
1787 }
1788
1789 // A fresh machine has no git identity yet, and the commit cannot be recorded without one.
1790 if( !KIGIT::EnsureGitIdentity( this, m_frame, common, aRequest.identity ) )
1791 return;
1792
1793 if( common != m_frame->Prj().GetGitCommon() || common->IsShuttingDown() )
1794 return;
1795
1797 wxString error;
1798 wxString branchError;
1799 wxString branchDetail;
1800 wxString previousBranch;
1801
1802 {
1803 // Keep the lock through branch creation and the commit.
1804 std::unique_lock<std::mutex> lock = KIGIT::TryLockAction( *common, this );
1805
1806 if( !lock.owns_lock() )
1807 return;
1808
1809 if( aRequest.head != commitHead( common->GetRepo() ) )
1810 {
1811 lock.unlock();
1812 DisplayInfoMessage( this, _( "The branch or its last commit changed while preparing the commit. "
1813 "Review the changes and commit again." ) );
1814 common->NotifyRefreshListeners();
1815 return;
1816 }
1817
1818 if( amend )
1819 {
1820 wxString refusal = amendRefusal( *common, aRequest.head );
1821
1822 if( !refusal.IsEmpty() )
1823 {
1824 lock.unlock();
1825 DisplayInfoMessage( this, refusal );
1826 common->NotifyRefreshListeners();
1827 return;
1828 }
1829 }
1830
1831 GIT_CONFIG_HANDLER configHandler( common.get() );
1832 GitUserConfig userConfig = configHandler.GetUserConfig();
1833 wxString signOff;
1834
1835 if( aRequest.signOff )
1836 signOff = wxString::Format( wxS( "%s <%s>" ), userConfig.authorName, userConfig.authorEmail );
1837
1838 wxString message = KIGIT::ComposeCommitMessage( aRequest.summary, aRequest.description, signOff );
1839
1840 // The new branch starts at HEAD, so switching to it leaves the working tree as it is.
1841 if( newBranch )
1842 previousBranch = common->GetCurrentBranchName();
1843
1844 if( !newBranch || moveToNewBranch( *common, branchName, branchError, branchDetail ) )
1845 {
1846 GIT_COMMIT_HANDLER commitHandler( common->GetRepo() );
1847
1848 if( amend )
1849 result = commitHandler.PerformAmend( files, message, userConfig.authorName, userConfig.authorEmail );
1850 else
1851 result = commitHandler.PerformCommit( files, message, userConfig.authorName, userConfig.authorEmail );
1852
1853 error = commitHandler.GetErrorString();
1854
1856
1857 // A failed commit must not leave the project on an empty new branch.
1858 if( newBranch && !committed )
1859 dropNewBranch( *common, branchName, previousBranch );
1860 }
1861 }
1862
1863 if( !branchError.IsEmpty() )
1864 {
1865 DisplayErrorMessage( this, branchError, branchDetail );
1866 return;
1867 }
1868
1869 // The commit exists on this path, so saying it failed would invite a second one.
1871 {
1872 DisplayInfoMessage( this, error );
1873 }
1874 else if( result != CommitResult::Success )
1875 {
1876 DisplayErrorMessage( this, wxString::Format( _( "Could not create the commit: %s" ), error ) );
1877 return;
1878 }
1879
1880 // The other commit pane can still edit the shared message while this one is preparing.
1881 if( m_summary->GetValue() == aRequest.summary && m_description->GetValue() == aRequest.description )
1882 {
1883 m_summary->Clear();
1884 setDescription( wxEmptyString );
1885 }
1886 m_amendCheckbox->SetValue( false );
1887 m_amendHead = {};
1888 m_newBranchCheckbox->SetValue( false );
1889 m_newBranchName->Clear();
1890 common->NotifyRefreshListeners();
1891
1892 if( aRequest.andPush )
1893 submitPush( common );
1894 else
1895 setBusy( false, _( "Committed." ) );
1896}
1897
1898
1899void GIT_COMMIT_PANE::runActionQuery( std::shared_ptr<KIGIT_COMMON> aCommon, std::function<void( KIGIT_COMMON& )> aWork,
1900 std::function<void()> aDone, const wxString& aStatus )
1901{
1902 if( m_actionPending || m_transfer.IsBusy() )
1903 {
1904 KIGIT::ShowBusyMessage( this );
1905 return;
1906 }
1907
1908 m_actionPending = true;
1909 setBusy( true, aStatus );
1910 auto read = std::make_shared<bool>( false );
1911
1912 m_actionQuery.Run(
1913 aCommon,
1914 [read, work = std::move( aWork )]( KIGIT_COMMON& aSession )
1915 {
1916 work( aSession );
1917 *read = true;
1918 },
1919 [this, aCommon, read, done = std::move( aDone )]()
1920 {
1921 m_actionPending = false;
1922 setBusy( false, wxEmptyString );
1923
1924 if( *read && aCommon == m_frame->Prj().GetGitCommon() && !aCommon->IsShuttingDown()
1925 && aCommon->GetRepo() && Pgm().GetCommonSettings()->m_Git.enableGit )
1926 {
1927 done();
1928 }
1929 } );
1930}
1931
1932
1933void GIT_COMMIT_PANE::SetTransferBusy( bool aBusy, const wxString& aStatus )
1934{
1935 if( !aBusy && m_transfer.IsBusy() )
1936 return;
1937
1938 setBusy( aBusy, aStatus );
1939}
1940
1941
1942void GIT_COMMIT_PANE::ShowTransferProgress( const wxString& aText )
1943{
1944 m_frame->SetStatusText( aText );
1945}
1946
1947
1948void GIT_COMMIT_PANE::setBusy( bool aBusy, const wxString& aStatus )
1949{
1950 m_checklist->Enable( !aBusy );
1951 m_commitButton->Enable( !aBusy );
1952 m_amendCheckbox->Enable( !aBusy );
1953 m_signOffCheckbox->Enable( !aBusy );
1954 m_newBranchCheckbox->Enable( !aBusy );
1955 m_newBranchName->Enable( !aBusy && m_newBranchCheckbox->IsChecked() );
1956 m_recentMessages->Enable( !aBusy );
1957 m_summary->Enable( !aBusy );
1958 m_description->Enable( !aBusy );
1959 m_frame->SetStatusText( aStatus );
1960
1961 // The frame status bar is overwritten within seconds by the editors, so an outcome shown
1962 // with the empty label also goes to the panel's own label, where it stays.
1963 if( !aBusy && !aStatus.IsEmpty() && m_emptyLabel->IsShown() && !m_headDetached && !m_operationInProgress )
1964 setStatusText( aStatus );
1965
1967 m_busyChangedHandler( aBusy );
1968
1969 // While a transfer runs, the network button turns into its cancel control.
1970 if( aBusy )
1971 {
1972 m_pullButton->SetLabel( _( "Cancel" ) );
1973 m_pullButton->Enable( true );
1974 }
1975 else
1976 {
1977 m_pullButton->SetLabel( _( "Get Changes" ) );
1979 }
1980}
1981
1982
1983void GIT_COMMIT_PANE::submitPush( std::shared_ptr<KIGIT_COMMON> aCommon )
1984{
1985 struct TARGET
1986 {
1987 bool hasRemote = false;
1988 wxString branch;
1989 wxString head;
1990 };
1991
1992 auto target = std::make_shared<TARGET>();
1993
1995 aCommon,
1996 [target]( KIGIT_COMMON& aSession )
1997 {
1998 target->hasRemote = aSession.HasPushAndPullRemote();
1999 target->branch = aSession.GetCurrentBranchName();
2000 target->head = aSession.GetHeadCommitId();
2001 },
2002 [this, aCommon, target]()
2003 {
2004 if( !target->hasRemote )
2005 {
2006 setBusy( false, _( "Committed. No remote is configured to push to." ) );
2007 aCommon->NotifyRefreshListeners();
2008 return;
2009 }
2010
2011 startPush( aCommon, target->branch, target->head );
2012 },
2013 _( "Checking remote..." ) );
2014}
2015
2016
2017void GIT_COMMIT_PANE::startPush( std::shared_ptr<KIGIT_COMMON> aCommon, const wxString& aBranch, const wxString& aHead )
2018{
2019 setBusy( true, _( "Pushing..." ) );
2020
2021 // Carried rather than captured by reference, because Start returns before the work runs.
2022 struct OUTCOME
2023 {
2025 wxString error;
2026 bool authFailure = false;
2027 bool cancelled = false;
2028 };
2029
2030 auto outcome = std::make_shared<OUTCOME>();
2031
2032 bool started = m_transfer.Start(
2033 "git push",
2034 [aCommon, outcome, aBranch, aHead, alive = m_transfer.Alive(),
2035 pane = this]( const KIGIT_COMMON::CANCEL_TOKEN& aToken )
2036 {
2037 aCommon->ClearAuthFailure();
2038
2039 GIT_PUSH_HANDLER handler( aCommon.get() );
2040 handler.SetProgressReporter( std::make_unique<KIGIT_STATUS_REPORTER>(
2041 alive,
2042 [pane]( const wxString& aText )
2043 {
2044 pane->ShowTransferProgress( aText );
2045 } ) );
2046 handler.SetWaitForActionLock( true );
2047 handler.SetExpectedTarget( aBranch, aHead );
2048 handler.SetCancelToken( aToken );
2049 outcome->result = handler.PerformPush();
2050 outcome->error = handler.GetErrorString();
2051 outcome->authFailure = aCommon->WasAuthFailure();
2052
2053 // This operation's own token, so a cancel aimed at something else is not
2054 // reported here and this one's is not left behind for the next operation.
2055 outcome->cancelled = handler.IsThisOperationCancelled();
2056
2057 // The other surfaces need this even when the pane that started it has gone.
2058 if( outcome->result == PushResult::Success )
2059 {
2061 [aCommon]()
2062 {
2063 aCommon->NotifyRefreshListeners();
2064 } );
2065 }
2066 },
2067 [this, aCommon, outcome]()
2068 {
2069 onPushDone( aCommon, static_cast<int>( outcome->result ), outcome->error, outcome->authFailure,
2070 outcome->cancelled );
2071 } );
2072
2073 if( !started )
2074 setBusy( false, _( "Could not start the push." ) );
2075}
2076
2077
2078void GIT_COMMIT_PANE::onPushDone( std::shared_ptr<KIGIT_COMMON> aCommon, int aResult, const wxString& aError,
2079 bool aAuthFailure, bool aCancelled )
2080{
2081 PushResult result = static_cast<PushResult>( aResult );
2082
2084 {
2085 // The shared refresh already ran in the completion.
2086 setBusy( false, _( "Pushed to remote." ) );
2087 return;
2088 }
2089
2090 if( result == PushResult::Busy )
2091 {
2092 setBusy( false, wxEmptyString );
2093 KIGIT::ShowBusyMessage( this );
2094 return;
2095 }
2096
2097 if( aCancelled )
2098 {
2099 setBusy( false, _( "Cancelled." ) );
2100 return;
2101 }
2102
2103 if( aAuthFailure )
2104 {
2105 if( KIGIT::PromptForCredentials( this, aCommon.get(), &m_frame->Prj() ) )
2106 submitPush( aCommon );
2107 else
2108 setBusy( false, _( "Cancelled." ) );
2109
2110 return;
2111 }
2112
2113 // The remote moved on since the last sync. Offer to bring those changes in and push
2114 // again, rather than force over them.
2116 {
2117 // Getting their changes needs a clean tree. Files left unticked at the commit, or edits
2118 // still in an editor, block it, and offering the click anyway would only fail later.
2119 auto worktree = std::make_shared<GIT_WORKTREE_STATUS>();
2120 wxString projectPath = m_frame->Prj().GetProjectPath();
2121
2123 aCommon,
2124 [worktree, projectPath]( KIGIT_COMMON& aSession )
2125 {
2126 *worktree = ReadGitWorktreeStatus( aSession, projectPath );
2127 },
2128 [this, aCommon, worktree]()
2129 {
2130 if( !ProjectSafeToRewrite( m_frame, this, *worktree ) )
2131 return;
2132
2133 if( IsOK( this, _( "Someone else pushed changes first.\n\n"
2134 "Get their changes and push again?" ) )
2135 && aCommon == m_frame->Prj().GetGitCommon() && !aCommon->IsShuttingDown()
2136 && Pgm().GetCommonSettings()->m_Git.enableGit )
2137 {
2138 submitPull( aCommon, true );
2139 }
2140 },
2141 _( "Checking working files..." ) );
2142
2143 return;
2144 }
2145
2146 setBusy( false, _( "Push failed." ) );
2147 DisplayErrorMessage( this, wxString::Format( _( "Could not push: %s" ), aError ) );
2148}
2149
2150
2151// The worker owns the materialized conflict files until the UI finishes resolving them. A
2152// cancelled query drops the same object, so scratch files cannot outlive an abandoned flow.
2154{
2155 std::unique_ptr<GIT_DIFF_WORKSPACE> workspace;
2156 wxString headOid;
2157 wxString theirsOid;
2158 wxString rootDir;
2159 wxString error;
2160 std::vector<KIGIT_CONFLICT_SIDES> conflicts;
2162 bool rebaseConfigured = false;
2163 bool prepared = false;
2164};
2165
2166
2167void GIT_COMMIT_PANE::resolvePullConflicts( std::shared_ptr<KIGIT_COMMON> aCommon, bool aThenPush )
2168{
2169 auto preparation = std::make_shared<MERGE_PREPARATION>();
2170
2172 aCommon,
2173 [preparation]( KIGIT_COMMON& aSession )
2174 {
2175 preparation->workspace = std::make_unique<GIT_DIFF_WORKSPACE>();
2176
2177 if( !preparation->workspace->IsOk() )
2178 return;
2179
2180 preparation->rebaseConfigured = aSession.IsPullRebaseConfigured();
2181 preparation->identity = KIGIT::ReadGitIdentity( aSession );
2182 preparation->rootDir = aSession.GetGitRootDirectory();
2183 preparation->prepared = aSession.GetMergeConflicts(
2184 preparation->workspace->Root() + wxFileName::GetPathSeparator() + wxS( "stages" ),
2185 preparation->headOid, preparation->theirsOid, preparation->conflicts, preparation->error );
2186 },
2187 [this, aCommon, aThenPush, preparation]()
2188 {
2189 finishPullConflictPreparation( aCommon, aThenPush, preparation );
2190 },
2191 _( "Preparing the merge..." ) );
2192}
2193
2194
2195void GIT_COMMIT_PANE::finishPullConflictPreparation( std::shared_ptr<KIGIT_COMMON> aCommon, bool aThenPush,
2196 std::shared_ptr<MERGE_PREPARATION> aPreparation )
2197{
2198 if( !aPreparation->workspace || !aPreparation->workspace->IsOk() )
2199 {
2200 DisplayErrorMessage( this, _( "Could not create a temporary directory for the merge." ) );
2201 return;
2202 }
2203
2204 if( !aPreparation->prepared )
2205 {
2206 DisplayErrorMessage( this, _( "Could not prepare the merge." ), aPreparation->error );
2207 return;
2208 }
2209
2210 // Editors remain usable while the repository is read and while another editor's merge
2211 // tool is open. Refuse to reload over anything typed during either interval.
2212 auto canResolve = [this, aCommon]()
2213 {
2214 if( aCommon != m_frame->Prj().GetGitCommon() || aCommon->IsShuttingDown()
2215 || !Pgm().GetCommonSettings()->m_Git.enableGit )
2216 {
2217 return false;
2218 }
2219
2220 if( EditorsDirty( m_frame->Kiway() ) )
2221 {
2222 DisplayInfoMessage( this, _( "The open editors have unsaved changes. Save or revert them before "
2223 "resolving the merge." ) );
2224 return false;
2225 }
2226
2227 return true;
2228 };
2229
2230 if( !canResolve() )
2231 return;
2232
2233 // A conflicted rebase was already abandoned by the time it got here, and settling it from
2234 // this window can only produce a merge commit. Someone who asked for rebases is told that
2235 // before one is made.
2236 if( aPreparation->rebaseConfigured
2237 && !IsOK( this, _( "This project is set to rebase when getting changes, but a conflict can only be "
2238 "settled here by merging. Carry on and make a merge commit, or cancel and rebase "
2239 "from a git command line?" ) ) )
2240 {
2241 setBusy( false, _( "Getting changes was cancelled. Your files are unchanged." ) );
2242 return;
2243 }
2244
2245 if( !canResolve() )
2246 return;
2247
2248 // The merge commit needs an identity whatever the user decides about the files, so it is
2249 // asked for before any merging. Cancelling it then costs no work.
2250 if( !KIGIT::EnsureGitIdentity( this, m_frame, aCommon, aPreparation->identity ) )
2251 {
2252 setBusy( false, _( "Getting changes was cancelled. Your files are unchanged." ) );
2253 return;
2254 }
2255
2256 if( !canResolve() )
2257 return;
2258
2259 GIT_DIFF_WORKSPACE& workspace = *aPreparation->workspace;
2260 const wxString sep = wxFileName::GetPathSeparator();
2261 const wxString& headOid = aPreparation->headOid;
2262 const wxString& theirsOid = aPreparation->theirsOid;
2263 const std::vector<KIGIT_CONFLICT_SIDES>& conflicts = aPreparation->conflicts;
2264 wxString error;
2265
2266 std::vector<std::pair<wxString, wxString>> resolved;
2267
2268 // The version the user does not keep is still somebody's work, so it is saved beside the
2269 // project file. Written only once every conflict is answered, so cancelling changes nothing.
2270 struct LOSING_COPY
2271 {
2272 wxString source;
2273 wxString relativeDest;
2274 };
2275
2276 std::vector<LOSING_COPY> losingCopies;
2277
2278 // Files the user merged by hand. The scratch directory holding them goes when this returns.
2279 int handMerged = 0;
2280
2281 for( size_t ii = 0; ii < conflicts.size(); )
2282 {
2283 if( !canResolve() )
2284 return;
2285
2286 const KIGIT_CONFLICT_SIDES& conflict = conflicts[ii];
2288
2289 // The merge tool needs all three sides. A deletion is kept or undone whole, and two files
2290 // created independently have no base to merge from, so they are kept or taken whole too.
2291 const bool mergeable = kind != KICAD_DIFF::DOC_KIND::UNKNOWN && conflict.hasAncestor && conflict.hasOurs
2292 && conflict.hasTheirs;
2293
2294 wxArrayString choices;
2295
2296 if( mergeable )
2297 choices.Add( _( "Open the merge tool" ) );
2298
2299 // A side with no base was added, not deleted. A renamed file arrives as two such entries.
2300 choices.Add( conflict.hasOurs ? _( "Keep my version" )
2301 : conflict.hasAncestor ? _( "Keep it deleted" )
2302 : _( "Leave it out" ) );
2303 choices.Add( conflict.hasTheirs ? _( "Use the remote version" )
2304 : conflict.hasAncestor ? _( "Delete it, as on the remote" )
2305 : _( "Leave it out, as on the remote" ) );
2306
2307 wxString situation;
2308
2309 if( !conflict.hasOurs && !conflict.hasAncestor )
2310 {
2311 situation = _( "'%s' exists only on the remote (%zu of %zu).\n\n"
2312 "How should it be resolved?" );
2313 }
2314 else if( !conflict.hasOurs )
2315 {
2316 situation = _( "'%s' was deleted here but changed on the remote (%zu of %zu).\n\n"
2317 "How should it be resolved?" );
2318 }
2319 else if( !conflict.hasTheirs && !conflict.hasAncestor )
2320 {
2321 situation = _( "'%s' exists only here (%zu of %zu).\n\n"
2322 "How should it be resolved?" );
2323 }
2324 else if( !conflict.hasTheirs )
2325 {
2326 situation = _( "'%s' was changed here but deleted on the remote (%zu of %zu).\n\n"
2327 "How should it be resolved?" );
2328 }
2329 else
2330 {
2331 situation = _( "'%s' was changed both here and on the remote (%zu of %zu).\n\n"
2332 "How should it be resolved?" );
2333 }
2334
2335 int sel = wxGetSingleChoiceIndex( wxString::Format( situation, conflict.path, ii + 1, conflicts.size() ),
2336 _( "Resolve Conflict" ), choices, this );
2337
2338 if( sel < 0 )
2339 {
2340 wxString warning = handMerged == 1
2341 ? _( "Stop resolving? The file you merged by hand is discarded "
2342 "with the rest." )
2343 : wxString::Format( _( "Stop resolving? The %d files you merged by "
2344 "hand are discarded with the rest." ),
2345 handMerged );
2346
2347 if( handMerged > 0 && !IsOK( this, warning ) )
2348 continue;
2349
2350 // Nothing was touched yet, so cancelling really does leave everything as it was.
2351 setBusy( false, _( "Getting changes was cancelled. Your files are unchanged." ) );
2352 return;
2353 }
2354
2355 int action = mergeable ? sel : sel + 1;
2356
2357 if( action == 0 )
2358 {
2359 wxString mergedOut = workspace.Root() + sep + wxString::Format( wxS( "m%zu_" ), ii )
2360 + wxFileName( conflict.path ).GetFullName();
2361
2364 m_frame->Kiway(), kind, conflict.ancestorFile, conflict.oursFile, conflict.theirsFile, mergedOut,
2365 /* interactive */ true, kind == KICAD_DIFF::DOC_KIND::FOOTPRINT, &reporter );
2366
2367 if( rc != 0 )
2368 {
2369 // Cancelled or failed. Offer the choices for this file again.
2370 if( rc != CLI::EXIT_CODES::ERR_CANCELLED && reporter.HasMessage() )
2371 DisplayErrorMessage( this, _( "The merge tool did not produce a result." ),
2372 reporter.GetMessages() );
2373
2374 continue;
2375 }
2376
2377 resolved.emplace_back( conflict.path, mergedOut );
2378 ++handMerged;
2379
2380 // A merged result replaces both originals, so both are kept.
2381 if( conflict.hasOurs )
2382 losingCopies.push_back( { conflict.oursFile, conflict.path + wxS( ".mine" ) } );
2383
2384 if( conflict.hasTheirs )
2385 losingCopies.push_back( { conflict.theirsFile, conflict.path + wxS( ".theirs" ) } );
2386 }
2387 else if( action == 1 )
2388 {
2389 // An empty path records the resolution "the file is deleted".
2390 resolved.emplace_back( conflict.path, conflict.hasOurs ? conflict.oursFile : wxString() );
2391
2392 if( conflict.hasTheirs )
2393 losingCopies.push_back( { conflict.theirsFile, conflict.path + wxS( ".theirs" ) } );
2394 }
2395 else
2396 {
2397 resolved.emplace_back( conflict.path, conflict.hasTheirs ? conflict.theirsFile : wxString() );
2398
2399 if( conflict.hasOurs )
2400 losingCopies.push_back( { conflict.oursFile, conflict.path + wxS( ".mine" ) } );
2401 }
2402
2403 ++ii;
2404 }
2405
2406 if( !canResolve() )
2407 return;
2408
2409 // Written before the merge is published, so a copy that cannot be made stops the merge
2410 // rather than losing the version it was meant to protect.
2411 wxString rootDir = aPreparation->rootDir;
2412
2413 if( !rootDir.EndsWith( wxS( "/" ) ) )
2414 rootDir += wxS( "/" );
2415
2416 std::vector<wxString> savedCopies;
2417 std::vector<wxString> createdDirs;
2418
2419 // Undoing the copies keeps every later abort honest when it says the files are unchanged.
2420 // Only what this attempt made is removed, so an older recovery file survives.
2421 auto discardSavedCopies = [&savedCopies, &createdDirs]()
2422 {
2423 for( const wxString& path : savedCopies )
2424 wxRemoveFile( path );
2425
2426 // Deepest first, and only while empty, so a directory holding anything else stays.
2427 for( auto dir = createdDirs.rbegin(); dir != createdDirs.rend(); ++dir )
2428 wxFileName::Rmdir( *dir );
2429
2430 savedCopies.clear();
2431 createdDirs.clear();
2432 };
2433
2434 for( const LOSING_COPY& copy : losingCopies )
2435 {
2436 wxString saved;
2437 wxString createdDir;
2438
2439 bool reserved = reserveRecoveryFile( rootDir + copy.relativeDest, saved, createdDir );
2440
2441 if( !createdDir.IsEmpty() )
2442 createdDirs.push_back( createdDir );
2443
2444 if( !reserved || !wxCopyFile( copy.source, saved, true ) )
2445 {
2446 if( !saved.IsEmpty() )
2447 wxRemoveFile( saved );
2448
2449 discardSavedCopies();
2450 setBusy( false, _( "Getting changes was cancelled. Your files are unchanged." ) );
2451 DisplayErrorMessage( this, wxString::Format( _( "Could not save the version you are replacing "
2452 "as '%s', so the merge was stopped." ),
2453 copy.relativeDest ) );
2454 return;
2455 }
2456
2457 savedCopies.push_back( saved );
2458 }
2459
2461
2462 // The lock was released while the user resolved, and the background fetch may hold it now.
2463 // Throwing the resolutions away would mean doing every file again, so let the user retry.
2464 while( true )
2465 {
2466 if( !canResolve() )
2467 {
2468 discardSavedCopies();
2469 return;
2470 }
2471
2472 std::unique_lock<std::mutex> lock = aCommon->TryLockAction();
2473
2474 if( lock.owns_lock() )
2475 {
2476 result = aCommon->CreateMergedCommit( headOid, theirsOid, resolved, error );
2477 break;
2478 }
2479
2480 if( !IsOK( this, _( "Another git operation is in progress. Try again?" ) ) )
2481 {
2482 discardSavedCopies();
2483 setBusy( false, _( "Getting changes was cancelled. Your files are unchanged." ) );
2484 return;
2485 }
2486 }
2487
2489 {
2490 // Nothing reached the files, so the copies that would have explained it go too.
2491 discardSavedCopies();
2492 }
2493 else
2494 {
2495 // Whatever was written has to reach the editors and panels, and a partial write leaves
2496 // the saved copies as the only way back to the old version.
2498 aCommon->NotifyRefreshListeners();
2499 }
2500
2502 {
2503 DisplayErrorMessage( this, _( "Could not finish the merge." ), error );
2504 return;
2505 }
2506
2507 if( aThenPush )
2508 submitPush( aCommon );
2509 else
2510 setBusy( false, _( "Changes merged." ) );
2511}
2512
2513
2514void GIT_COMMIT_PANE::submitPull( std::shared_ptr<KIGIT_COMMON> aCommon, bool aThenPush )
2515{
2516 // Version control can be removed while a transfer runs, and leaving the busy state behind
2517 // would disable the panel for good.
2518 if( !aCommon->GetRepo() )
2519 {
2520 setBusy( false, wxEmptyString );
2521 return;
2522 }
2523
2524 struct CHECK
2525 {
2526 KIGIT::GIT_IDENTITY identity;
2527 GIT_WORKTREE_STATUS worktree;
2528 };
2529
2530 auto result = std::make_shared<CHECK>();
2531 wxString projectPath = m_frame->Prj().GetProjectPath();
2532
2534 aCommon,
2535 [result, projectPath]( KIGIT_COMMON& aSession )
2536 {
2537 result->identity = KIGIT::ReadGitIdentity( aSession );
2538 result->worktree = ReadGitWorktreeStatus( aSession, projectPath );
2539 },
2540 [this, aCommon, aThenPush, result]()
2541 {
2542 if( !ProjectSafeToRewrite( m_frame, this, result->worktree ) )
2543 return;
2544
2545 // A non-fast-forward pull creates a commit, so obtain its identity before
2546 // the worker can change files. The repository check was in the query above.
2547 if( !KIGIT::EnsureGitIdentity( this, m_frame, aCommon, result->identity ) )
2548 return;
2549
2550 if( aCommon != m_frame->Prj().GetGitCommon() || aCommon->IsShuttingDown() )
2551 return;
2552
2553 // The identity prompt can allow new editor changes while it is open.
2554 if( !ProjectSafeToRewrite( m_frame, this, result->worktree ) )
2555 return;
2556
2557 startPull( aCommon, aThenPush );
2558 },
2559 _( "Checking changes..." ) );
2560}
2561
2562
2563void GIT_COMMIT_PANE::startPull( std::shared_ptr<KIGIT_COMMON> aCommon, bool aThenPush )
2564{
2565 setBusy( true, _( "Getting changes..." ) );
2566
2567 // Record what the open documents look like on disk now, so only the ones the merge actually
2568 // rewrites are reloaded afterwards.
2570
2571 EDITOR_SNAPSHOT docsBefore = m_docsBeforePull;
2572
2573 // The KIWAY outlives this pane, so the editors are still reached when the pane has gone.
2574 KIWAY* kiway = &m_frame->Kiway();
2575
2576 // Carried rather than captured by reference, because Start returns before the work runs.
2577 struct OUTCOME
2578 {
2580 wxString error;
2581 bool authFailure = false;
2582 bool cancelled = false;
2583 };
2584
2585 auto outcome = std::make_shared<OUTCOME>();
2586
2587 bool started = m_transfer.Start(
2588 "git pull",
2589 [aCommon, docsBefore, kiway, outcome, alive = m_transfer.Alive(),
2590 pane = this]( const KIGIT_COMMON::CANCEL_TOKEN& aToken )
2591 {
2592 aCommon->ClearAuthFailure();
2593
2594 GIT_PULL_HANDLER handler( aCommon.get() );
2595 handler.SetProgressReporter( std::make_unique<KIGIT_STATUS_REPORTER>(
2596 alive,
2597 [pane]( const wxString& aText )
2598 {
2599 pane->ShowTransferProgress( aText );
2600 } ) );
2601 handler.SetWaitForActionLock( true );
2602 handler.SetCancelToken( aToken );
2603 outcome->result = handler.PerformPull();
2604 outcome->error = handler.GetErrorStrings().empty() ? wxString() : handler.GetErrorString();
2605 outcome->authFailure = aCommon->WasAuthFailure();
2606
2607 // This operation's own token, so a cancel aimed at something else is not
2608 // reported here and this one's is not left behind for the next operation.
2609 outcome->cancelled = handler.IsThisOperationCancelled();
2610
2611 PullResult result = outcome->result;
2612
2614 [aCommon, docsBefore, result, kiway]()
2615 {
2616 // A merge that failed partway can still have written files, so the
2617 // editors are brought up to date whatever the outcome. Nothing
2618 // reloads when nothing changed on disk.
2619 ReloadOpenEditors( *kiway, docsBefore );
2620
2622 aCommon->NotifyRefreshListeners();
2623 } );
2624 },
2625 [this, aCommon, aThenPush, outcome]()
2626 {
2627 onPullDone( aCommon, static_cast<int>( outcome->result ), outcome->error, outcome->authFailure,
2628 aThenPush, outcome->cancelled );
2629 } );
2630
2631 if( !started )
2632 setBusy( false, _( "Could not start getting changes." ) );
2633}
2634
2635
2636void GIT_COMMIT_PANE::onPullDone( std::shared_ptr<KIGIT_COMMON> aCommon, int aResult, const wxString& aError,
2637 bool aAuthFailure, bool aThenPush, bool aCancelled )
2638{
2639 PullResult result = static_cast<PullResult>( aResult );
2640
2642 {
2643 // The editor reloads and the panel refresh already ran in the completion, before the
2644 // window check, so they happen even when the initiating window is gone.
2646 aCommon->NotifyRefreshListeners();
2647
2648 // Pushing afterwards belongs only to the push-was-rejected recovery. A plain Get
2649 // Changes must not publish local commits the user chose to keep local.
2650 if( aThenPush )
2651 submitPush( aCommon );
2652 else
2653 setBusy( false, result == PullResult::UpToDate ? _( "Already up to date." ) : _( "Changes received." ) );
2654
2655 return;
2656 }
2657
2658 // A rejected login is not a merge problem, so offer the credentials prompt and retry,
2659 // matching the push path.
2660 if( aCancelled )
2661 {
2662 setBusy( false, _( "Cancelled." ) );
2663 return;
2664 }
2665
2666 if( aAuthFailure )
2667 {
2668 if( KIGIT::PromptForCredentials( this, aCommon.get(), &m_frame->Prj() ) )
2669 submitPull( aCommon, aThenPush );
2670 else
2671 setBusy( false, _( "Cancelled." ) );
2672
2673 return;
2674 }
2675
2676 // Conflicting changes are resolved file by file, with the visual merge for design files.
2677 // The failed merge already restored the pre-pull state, so cancelling costs nothing.
2679 {
2680 resolvePullConflicts( aCommon, aThenPush );
2681 return;
2682 }
2683
2684 // Something uncommitted appeared after the pre-pull check, or an untracked file collides
2685 // with an incoming one. This is the panel's own job to fix, not the command line's.
2687 {
2688 setBusy( false, _( "Commit your changes first." ) );
2689 aCommon->NotifyRefreshListeners();
2690
2691 // The backend names the files when it can, which the generic text cannot.
2692 if( !aError.IsEmpty() )
2693 {
2694 DisplayInfoMessage( this, aError );
2695 }
2696 else
2697 {
2698 DisplayInfoMessage( this, _( "There are changes that are not committed yet, and getting changes "
2699 "would run over them. Commit them first, then get changes again. To "
2700 "set them aside instead, use Stash Changes in the Advanced Git "
2701 "window." ) );
2702 }
2703
2704 return;
2705 }
2706
2707 // The failure may or may not have touched the working tree, so refresh the panels and
2708 // report the backend's actual error rather than guessing at a cause.
2709 setBusy( false, _( "Could not get changes." ) );
2710 aCommon->NotifyRefreshListeners();
2711 DisplayErrorMessage( this, _( "Could not get changes." ), aError );
2712}
const char * name
wxBitmapBundle KiBitmapBundle(BITMAPS aBitmap, int aMinHeight)
Definition bitmap.cpp:106
The base frame for deriving all KiCad main window classes.
The advanced Git window: a non-modal dialog with a top bar naming the repository and branch and a sid...
static void ShowHistoryForFile(EDA_BASE_FRAME *aParent, GIT_COMMIT_PANE *aPane, const wxString &aPath)
Open the window on its History page, narrowed to the commits that changed aPath.
static void CloseForPane(GIT_COMMIT_PANE *aPane)
Close the live window if the given pane opened it, for when that pane goes away.
static void CloseActive()
Close the live window, for when the project it was filled from goes away.
static GIT_ADVANCED_DIALOG * Active()
The live window, or null when none is open.
static void ShowForFrame(EDA_BASE_FRAME *aParent, GIT_COMMIT_PANE *aPane)
Raise the one window for this session, creating it on first use.
BranchResult SwitchToBranch(const wxString &aBranchName)
Switch to the specified branch.
CommitResult PerformCommit(const std::vector< wxString > &aFiles, const wxString &aMessage, const wxString &aAuthorName, const wxString &aAuthorEmail)
wxString GetErrorString() const
CommitResult PerformAmend(const std::vector< wxString > &aFiles, const wxString &aMessage, const wxString &aAuthorName, const wxString &aAuthorEmail)
wxStaticBoxSizer * m_optionsSizer
STD_BITMAP_BUTTON * m_settingsButton
FILE_CHECKLIST_CTRL * m_checklist
GIT_COMMIT_PANE_BASE(wxWindow *parent, wxWindowID id=wxID_ANY, const wxPoint &pos=wxDefaultPosition, const wxSize &size=wxSize(500, 360), long style=wxTAB_TRAVERSAL, const wxString &name=wxEmptyString)
wxString abandonLabel() const
The label the network button carries while an operation is in progress.
void submitPull(std::shared_ptr< KIGIT_COMMON > aCommon, bool aThenPush)
aThenPush pushes after a successful pull, which belongs only to the rejected-push recovery flow.
void updateCommitButton()
Set what the commit buttons say, and whether they are enabled, for the current state.
void onPull(wxCommandEvent &aEvent)
void onMessageChanged(wxCommandEvent &aEvent)
void resolvePullConflicts(std::shared_ptr< KIGIT_COMMON > aCommon, bool aThenPush)
Resolve a conflicted pull file by file, using the visual merge tool for design files,...
void ShowTransferProgress(const wxString &aText)
Show a line of transfer progress. Called from the UI thread by a KIGIT_STATUS_REPORTER.
void SetTransferBusy(bool aBusy, const wxString &aStatus=wxEmptyString)
Put the panel into the state it uses while a transfer runs, for one started elsewhere.
void Pull()
Get changes from the remote, as the Get Changes button does.
wxString GetCommitMessage() const
The message as typed. A second form on the same project shows the same text.
void onNewBranchToggled(wxCommandEvent &aEvent)
void abandonOperation(std::shared_ptr< KIGIT_COMMON > aCommon)
Drop an in-progress merge or rebase and return the project to its last commit.
void BeginAmend(const wxString &aExpectedCommit=wxEmptyString)
Prepare the amend form. A history entry supplies its hash and starts with no files selected.
void finishCommit(std::shared_ptr< KIGIT_COMMON > aCommon, const COMMIT_REQUEST &aRequest)
bool m_inAdvancedWindow
True on the advanced window's Changes page, false in the docked panel.
std::function< void(const wxString &)> m_fileSelectedHandler
void setDescription(const wxString &aText)
void startPush(std::shared_ptr< KIGIT_COMMON > aCommon, const wxString &aBranch, const wxString &aHead)
void discardFile(std::shared_ptr< KIGIT_COMMON > aCommon, const wxString &aPath)
KIGIT_QUERY m_refresh
Coalesce refreshes while a repository read is running, and discard stale results.
std::pair< int, int > m_lineStats
Lines the working tree adds and removes against HEAD, read at the last refresh.
void onSettings(wxCommandEvent &aEvent)
The gear's menu: the advanced window and the author.
bool transferCanStart()
False, with a message, while a transfer is running or a merge waits to be finished.
void updateMessageVisibility(bool aHasChanges)
Show the message field whenever a commit is possible, including a message-only amend.
EDA_BASE_FRAME * m_frame
void RefreshFileList()
Read the project's git status and repopulate the list.
bool m_editorsDirty
True when an open editor held unsaved changes at the last refresh.
void finishPullConflictPreparation(std::shared_ptr< KIGIT_COMMON > aCommon, bool aThenPush, std::shared_ptr< MERGE_PREPARATION > aPreparation)
void setBusy(bool aBusy, const wxString &aStatus)
Enable or disable the controls while a network operation runs, showing a status.
void ReloadAdvancedWindow()
Reload the advanced window when it is open, for a preferences change it also shows.
bool m_canAmend
Whether the current branch allows an amend. A refresh must not change an active amend into a commit.
wxString operationNoun() const
The noun for the operation in progress, for messages.
void onChecklistChanged(wxDataViewEvent &aEvent)
void populateBranches(const STATUS &aStatus)
Fill the branch dropdown from the completed snapshot.
void onFileActivated(wxDataViewEvent &aEvent)
bool m_upstreamAhead
True when the upstream has commits this project does not, so Get Changes has work to do.
void onPullDone(std::shared_ptr< KIGIT_COMMON > aCommon, int aResult, const wxString &aError, bool aAuthFailure, bool aThenPush, bool aCancelled)
bool m_commitButtonPushes
True while the main button reads Push, with nothing left to commit.
wxStaticText * m_summaryRemoved
void updateSummary()
Refresh the staged count in the changes heading, and the summary box where the wide layout has one.
void saveForCommit(std::shared_ptr< KIGIT_COMMON > aCommon, std::shared_ptr< COMMIT_REQUEST > aRequest)
void onCommit(wxCommandEvent &aEvent)
The split button's main half. It commits, or pushes when that is all there is to do.
void showDisabledState()
Clear the list and disable every control while version control is off.
void submitPush(std::shared_ptr< KIGIT_COMMON > aCommon)
void updateDescriptionHint()
Where the platform has no hint for a multi line box, a label lies over the description box while it i...
void onPushDone(std::shared_ptr< KIGIT_COMMON > aCommon, int aResult, const wxString &aError, bool aAuthFailure, bool aCancelled)
std::function< void(bool)> m_busyChangedHandler
Told when a transfer starts and ends. Empty unless an owning window asked.
void layoutWide()
Put the file list beside the commit controls instead of above them, for a wide host,...
KIGIT_TRANSFER m_transfer
The push or pull this panel submitted last.
std::pair< wxString, wxString > m_amendHead
The branch and commit chosen when amend started. Keep them fixed while editing.
void onChecklistContextMenu(wxDataViewEvent &aEvent)
void setStatusText(const wxString &aText)
Set the status line text and wrap it to the current panel width.
std::function< void()> m_messageChangedHandler
Told when the message is edited. Empty unless an owning window asked.
bool m_headDetached
True when HEAD is a commit rather than a branch, so nothing can be committed or pushed.
wxString m_statusText
Unwrapped status text, kept so it can be re-wrapped on resize.
std::vector< wxString > m_recentMessageTexts
The full messages behind the Recent messages dropdown, in its order after the caption.
wxString m_currentBranch
The branch a commit will land on, empty when detached or before the first commit.
void populateRecentMessages(const std::vector< wxString > &aMessages)
Fill the Recent messages dropdown from the completed snapshot.
void Push()
Push the local commits, as the Push button does.
void onShowDiff(wxCommandEvent &aEvent)
bool m_operationInProgress
True while a merge or rebase is in progress. The network button then reads Abandon.
void SetCommitMessage(const wxString &aMessage)
wxStaticText * m_summaryFiles
wxMenuItem * m_commitAndPushItem
The arrow menu's entry, relabelled with the main button.
void startPull(std::shared_ptr< KIGIT_COMMON > aCommon, bool aThenPush)
bool m_hasRepo
Whether the last completed status snapshot had a repository.
void SetFileSelectedHandler(std::function< void(const wxString &)> aHandler)
Called with the path of the file the user selects in the list, or empty when none is.
void onCommitAndPush(wxCommandEvent &aEvent)
GIT_COMMIT_PANE(EDA_BASE_FRAME *aParent)
int m_minDockWidth
The widest row's minimum width, measured before anything is hidden.
GitOperation m_operation
Which operation, so the button and the explanation can name it.
~GIT_COMMIT_PANE() override
std::set< wxString > m_shownPaths
Paths shown in the list at the last refresh, so a refresh can tell which files the user deliberately ...
void onRecentMessage(wxCommandEvent &aEvent)
void runActionQuery(std::shared_ptr< KIGIT_COMMON > aCommon, std::function< void(KIGIT_COMMON &)> aWork, std::function< void()> aDone, const wxString &aStatus)
Read an action's prerequisites off the UI thread, then resume only in the same project.
void onAmendToggled(wxCommandEvent &aEvent)
void onIdle(wxIdleEvent &aEvent)
bool m_listenerAttached
True once this panel has attached its refresh listener to a session at least once.
void onSize(wxSizeEvent &aEvent)
void syncListener()
Register a refresh listener with the project's current git session, re-registering if the session cha...
void confirmDiscardFile(std::shared_ptr< KIGIT_COMMON > aCommon, const wxString &aPath, bool aTracked, bool aConflicted, wxString aRoot)
KIGIT_QUERY m_actionQuery
EDITOR_SNAPSHOT m_docsBeforePull
On-disk identity of the open documents, taken before a pull so only the ones the merge rewrote are re...
wxStaticText * m_descriptionHint
The description box's hint, a label laid over it while it is empty.
void updateSummaryCount()
Show how many of the summary's 72 columns are left.
void applyStatus(const STATUS &aStatus)
Apply a completed repository snapshot on the UI thread.
void finishAbandonOperation(std::shared_ptr< KIGIT_COMMON > aCommon, const std::vector< wxString > &aOutside)
std::weak_ptr< KIGIT_COMMON > m_listenerSession
The git session this panel registered a refresh listener with, weak so it does not keep the session a...
void doCommit(bool aAndPush)
Commit the checked files (or amend the last commit when the amend box is ticked), then push when aAnd...
wxStaticText * m_summaryAdded
GitUserConfig GetUserConfig()
Get user configuration (name and email) from git config Falls back to common settings if not found in...
A private scratch directory for extracting committed file versions to diff, removed when this object ...
const wxString & Root() const
The scratch directory, for callers that lay out their own files (e.g. footprint libraries).
wxColour ToColour() const
Definition color4d.cpp:221
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.
std::pair< int, int > GetWorkingTreeLineStats(const std::set< wxString > &aPaths) const
std::shared_ptr< std::atomic< bool > > CANCEL_TOKEN
One token per operation.
wxString GetLastCommitMessage() const
static wxString GetLastGitError()
wxString GetCurrentBranchName() const
wxString GetGitRootDirectory() const
bool IsOperationInProgress() const
True while a merge, rebase, cherry-pick or revert is in progress, which refuses commits.
bool IsPullRebaseConfigured() const
True when the repository is set to rebase rather than merge when getting changes.
bool HasUpstreamCommits() const
True when the upstream branch has commits the current branch does not.
git_repository * GetRepo() const
std::vector< wxString > GetRecentCommitMessages(size_t aMax) const
bool CreateBranch(const wxString &aName, const wxString &aFrom=wxEmptyString)
bool GetMergeConflicts(const wxString &aStageDir, wxString &aHeadOid, wxString &aTheirsOid, std::vector< KIGIT_CONFLICT_SIDES > &aConflicts, wxString &aError)
Recompute the merge in memory and extract each conflicted file's three versions into aStageDir,...
bool StartUnbornBranch(const wxString &aName)
Point HEAD at a branch that does not exist yet, so the first commit creates it.
bool IsHeadUnborn() const
True while the repository has no commits, so HEAD names a branch that does not exist yet.
wxString GetHeadCommitId() const
Full commit ID at HEAD, or empty for an unborn or missing HEAD.
bool HasPushAndPullRemote() const
bool IsPathInHead(const wxString &aPath)
True when the working directory relative path exists in the HEAD commit's tree.
bool HasLocalCommits() const
True when HEAD has commits its upstream does not, or has no upstream yet.
GitOperation OperationInProgress() const
Which operation that is, so the panel can name it.
std::vector< KIGIT_BRANCH_INFO > GetBranches() const
Local and remote branches with their tip commits.
bool DeleteBranch(const wxString &aName)
bool IsPathConflicted(const wxString &aPath) const
True while aPath is one of the files a merge or rebase stopped on.
bool IsHeadDetached() const
True when HEAD points at a commit rather than a branch, as after checking out a tag.
wxString GetErrorString()
A minimalistic software bus for communications between various DLLs/DSOs (DSOs) within the same KiCad...
Definition kiway.h:348
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 CHECK(x)
#define _(s)
Base window classes and related definitions.
CommitResult
Definition git_backend.h:53
@ SuccessIndexStale
Committed, but the staged list was left as another program had it. Never retry on this.
Definition git_backend.h:57
MergeCommitResult
Definition git_backend.h:66
static wxString amendRefusal(KIGIT_COMMON &aCommon, const std::pair< wxString, wxString > &aExpectedHead)
static bool moveToNewBranch(KIGIT_COMMON &aCommon, const wxString &aName, wxString &aError, wxString &aDetail)
static std::pair< wxString, wxString > commitHead(git_repository *aRepo)
static void dropNewBranch(KIGIT_COMMON &aCommon, const wxString &aName, const wxString &aPrevious)
static bool reserveRecoveryFile(const wxString &aPreferredPath, wxString &aPathOut, wxString &aCreatedDir)
PullResult
PushResult
@ Busy
Another git operation held the action lock, so nothing was pushed.
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...
EDITOR_SNAPSHOT CaptureOpenEditorDocs(KIWAY &aKiway)
Record the on-disk identity of the open documents, before an operation that may rewrite them.
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.
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.
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.
bool EditorsDirty(KIWAY &aKiway)
Unsaved document or library edits block operations that rewrite files. Call on the UI thread.
@ COMMIT
Saving dirty editors for a commit also saves the project file.
std::string source
std::string message
GitOperation
static const int ERR_CANCELLED
The user cancelled the operation.
Definition exit_codes.h:45
int DispatchMerge(KIWAY &aKiway, DOC_KIND aKind, const wxString &aAncestor, const wxString &aOurs, const wxString &aTheirs, const wxString &aOutput, bool aInteractive, bool aSingleFile, REPORTER *aReporter)
Run a 3-way document/library merge by calling the owning kiface's KIFACE_MERGE_DOCUMENT function expo...
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.
void SortBranchesForDisplay(std::vector< KIGIT_BRANCH_INFO > &aBranches)
Put the current branch first, then local and remote branches with the newest tips first.
void SplitCommitMessage(const wxString &aMessage, wxString &aSummary, wxString &aDescription)
Split a commit message into its first line and the description after the first blank line.
wxString BranchNameError(const wxString &aName)
Empty when aName is a branch name git would accept, otherwise why it would not, in the words to show ...
GIT_IDENTITY ReadGitIdentity(KIGIT_COMMON &aCommon)
Read on a worker while holding the session's action lock.
bool EnsureGitIdentity(wxWindow *aParent, EDA_BASE_FRAME *aFrame, const std::shared_ptr< KIGIT_COMMON > &aCommon, const GIT_IDENTITY &aIdentity)
Open the same editor when the worker snapshot has no usable Git identity.
bool PromptForCredentials(wxWindow *aParent, KIGIT_COMMON *aCommon, PROJECT *aProject=nullptr)
Ask the user for the credentials of the session's remote and apply them to the session.
wxString ComposeCommitMessage(const wxString &aSummary, const wxString &aDescription, const wxString &aSignOff)
Join a summary line, an optional description and an optional sign-off into one commit message.
void ShowBusyMessage(wxWindow *aParent)
Show a busy message for a user action. UI thread only.
std::unique_lock< std::mutex > TryLockAction(KIGIT_COMMON &aCommon, wxWindow *aParent)
Try the action lock and show a busy message if it is unavailable.
bool EditGitIdentity(wxWindow *aParent, EDA_BASE_FRAME *aFrame, const std::shared_ptr< KIGIT_COMMON > &aCommon, const GIT_IDENTITY &aIdentity)
Edit the repository's author and refresh Git views after saving.
void CallOnUi(std::function< void()> aFn)
Run aFn on the UI thread.
bool MoveToTrash(const wxString &aPath, wxString &aError)
Move the specified file/directory to the trash bin/recycle bin.
void EllipsizeChoiceBox(wxChoice *aChoice)
Configure a wxChoice control to ellipsize the shown text in the button with the ellipses placed at th...
Definition wxgtk/ui.cpp:407
bool IsDarkTheme()
Determine if the desktop interface is currently using a dark theme or a light theme.
Definition wxgtk/ui.cpp:54
PGM_BASE & Pgm()
The global program "get" accessor.
see class PGM_BASE
What the open editors and the project settings looked like on disk before an operation that may rewri...
One file row in a FILE_CHECKLIST_CTRL.
wxString path
Row identity.
std::pair< wxString, wxString > head
std::vector< KIGIT_CONFLICT_SIDES > conflicts
std::unique_ptr< GIT_DIFF_WORKSPACE > workspace
std::pair< int, int > lineStats
std::vector< KIGIT_BRANCH_INFO > branches
std::vector< FILE_CHECKLIST_ENTRY > entries
std::vector< wxString > recentMessages
Repository state used before an operation that requires a clean working tree.
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.
Repository values and whether Git can use them to record a commit.
wxString theirsFile
Extracted remote version, an empty file when absent.
wxString ancestorFile
Extracted common-ancestor version, an empty file when absent.
wxString oursFile
Extracted local version, an empty file when absent.
bool hasOurs
False when the local side deleted the file.
wxString path
Repository-relative path of the conflicted file.
bool hasTheirs
False when the remote side deleted the file.
bool hasAncestor
False when the file did not exist in the common ancestor.
std::string path
IbisParser parser & reporter
wxString result
Test unit parsing edge cases and error handling.
Definition of file extensions used in Kicad.