KiCad PCB EDA Suite
Loading...
Searching...
No Matches
panel_git_stashes.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 modify it
7 * under the terms of the GNU General Public License as published by the
8 * Free Software Foundation, either version 3 of the License, or (at your
9 * option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License along
17 * with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19
21
22#include <memory>
23
24#include <wx/textdlg.h>
25
26#include <confirm.h>
31#include <eda_base_frame.h>
34#include <kiplatform/ui.h>
35#include <project.h>
36
37
38namespace
39{
40
41// A stash index is only valid for the listing it came from. Another program adding or dropping
42// a stash shifts every index below it, so act on the stash itself.
43static bool findStashByIdUnlocked( KIGIT_COMMON& aCommon, const wxString& aId, size_t& aIndexOut )
44{
45 if( aId.IsEmpty() )
46 return false;
47
48 for( const KIGIT_STASH_INFO& s : aCommon.GetStashes() )
49 {
50 if( s.id == aId )
51 {
52 aIndexOut = s.index;
53 return true;
54 }
55 }
56
57 return false;
58}
59
60} // namespace
61
62
64 PANEL_GIT_STASHES_BASE( aParent ),
65 m_dialog( aDialog ),
66 m_frame( aFrame )
67{
68 m_stashesList->AppendColumn( _( "Description" ), wxLIST_FORMAT_LEFT, FromDIP( 360 ) );
69 m_stashesList->AppendColumn( _( "Date" ), wxLIST_FORMAT_LEFT, FromDIP( 100 ) );
70 m_stashFiles->AppendColumn( _( "File" ), wxLIST_FORMAT_LEFT, FromDIP( 300 ) );
71 m_stashFiles->SetToolTip( _( "Double click a file to see what the stash changes in it." ) );
72
74
75 // The detail pane appears once a stash is selected.
76 m_stashSashPos = FromDIP( 460 );
78
79 m_stashesList->Bind( wxEVT_LIST_ITEM_SELECTED,
80 [this]( wxListEvent& aEvent )
81 {
82 if( !m_fillingStashes )
83 showStashDetail( stashIdForRow( aEvent.GetIndex() ) );
84 } );
85
86 m_stashesList->Bind( wxEVT_LIST_ITEM_DESELECTED,
87 [this]( wxListEvent& )
88 {
90 && m_stashesList->GetNextItem( -1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED ) == -1 )
91 {
93 }
94 } );
95
96 // A stash is a commit, so comparing it with the commit it was taken from is what it changes.
97 m_stashFiles->Bind( wxEVT_LIST_ITEM_ACTIVATED,
98 [this]( wxListEvent& aEvent )
99 {
100 long sel = m_stashesList->GetNextItem( -1, wxLIST_NEXT_ALL,
101 wxLIST_STATE_SELECTED );
102 wxString id = stashIdForRow( sel );
103
104 if( !id.IsEmpty() )
105 {
107 m_stashFiles->GetItemText( aEvent.GetIndex() ) );
108 }
109 } );
110
111 m_applyStashButton->Bind( wxEVT_BUTTON,
112 [this]( wxCommandEvent& )
113 {
114 onStashApply();
115 } );
116 m_popStashButton->Bind( wxEVT_BUTTON,
117 [this]( wxCommandEvent& )
118 {
119 onStashPop();
120 } );
121 m_dropStashButton->Bind( wxEVT_BUTTON,
122 [this]( wxCommandEvent& )
123 {
124 onStashDrop();
125 } );
126 m_stashSaveButton->Bind( wxEVT_BUTTON,
127 [this]( wxCommandEvent& )
128 {
129 onStashSave();
130 } );
131}
132
133
135{
136 if( !m_dialog->SessionStillCurrent() )
137 return;
138
139 m_detailQuery.Cancel();
140
141 auto stashes = std::make_shared<std::vector<KIGIT_STASH_INFO>>();
142
143 m_listQuery.Run(
144 m_frame->Prj().GetGitCommon(),
145 [stashes]( KIGIT_COMMON& aCommon )
146 {
147 *stashes = aCommon.GetStashes();
148 },
149 [this, stashes]()
150 {
151 if( !m_dialog->SessionStillCurrent() )
152 return;
153
154 long selectedRow = m_stashesList->GetNextItem( -1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED );
155 wxString selected = stashIdForRow( selectedRow );
156 long selectRow = wxNOT_FOUND;
157
158 m_detailQuery.Cancel();
159 m_fillingStashes = true;
160 m_stashesList->DeleteAllItems();
161 m_stashIds.clear();
162
163 for( const KIGIT_STASH_INFO& s : *stashes )
164 {
165 long idx = m_stashesList->InsertItem( m_stashesList->GetItemCount(), s.message );
166 m_stashesList->SetItem( idx, 1, KIGIT::RelativeDate( s.when ) );
167 m_stashesList->SetItemData( idx, static_cast<long>( s.index ) );
168 m_stashIds.push_back( s.id );
169
170 // Keep the selection on the same stash, or start on the newest.
171 if( s.id == selected || ( selected.IsEmpty() && idx == 0 ) )
172 selectRow = idx;
173 }
174
175 int shown = m_stashesList->GetItemCount();
176 m_stashCount->SetLabel( shown == 1 ? _( "1 stash" ) : wxString::Format( _( "%d stashes" ), shown ) );
177
178 if( selectRow != wxNOT_FOUND )
179 {
180 m_stashesList->SetItemState( selectRow, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED );
181 showStashDetail( stashIdForRow( selectRow ) );
182 }
183 else
184 {
186 }
187
188 m_fillingStashes = false;
189 } );
190}
191
192
193void PANEL_GIT_STASHES::showStashDetail( const wxString& aId )
194{
195 // The old file list must not be used with the newly selected stash while its read is pending.
197
198 if( aId.IsEmpty() || !m_dialog->SessionStillCurrent() )
199 return;
200
201 auto detail = std::make_shared<KIGIT_STASH_DETAIL>();
202
203 m_detailQuery.Run(
204 m_frame->Prj().GetGitCommon(),
205 [detail, aId]( KIGIT_COMMON& aCommon )
206 {
207 *detail = aCommon.GetStashDetail( aId );
208 },
209 [this, detail, aId]()
210 {
211 if( !m_dialog->SessionStillCurrent() )
212 return;
213
214 long selectedRow = m_stashesList->GetNextItem( -1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED );
215
216 if( stashIdForRow( selectedRow ) == aId )
217 fillStashDetail( *detail );
218 } );
219}
220
221
223{
224 if( !aDetail.found )
225 {
227 return;
228 }
229
230 m_stashWhen->SetLabel( wxString::Format( _( "Stashed: %s" ), KIGIT::RelativeDate( aDetail.when ) ) );
231 m_stashBranch->SetLabel( wxString::Format( _( "Branch: %s" ), aDetail.branch ) );
232 m_stashCommit->SetLabel( wxString::Format( _( "Commit: %s" ), aDetail.baseHash ) );
233 m_stashMessage->ChangeValue( aDetail.message );
234
235 m_stashFiles->DeleteAllItems();
236
237 for( const wxString& file : aDetail.files )
238 m_stashFiles->InsertItem( m_stashFiles->GetItemCount(), file );
239
240 if( !m_stashesSplitter->IsSplit() )
242
243 m_stashDetail->Layout();
244}
245
246
252
253
254wxString PANEL_GIT_STASHES::stashIdForRow( long aRow ) const
255{
256 if( aRow < 0 || static_cast<size_t>( aRow ) >= m_stashIds.size() )
257 return wxEmptyString;
258
259 return m_stashIds[aRow];
260}
261
262
263void PANEL_GIT_STASHES::restoreSelectedStash( bool aRemoveAfterApply )
264{
265 if( !m_dialog->SessionStillCurrent() )
266 return;
267
268 // Resolved before the dialogs below. Each runs a nested event loop in which a refresh can
269 // rebuild the list, and a row number would then stand for a different stash.
270 wxString id = stashIdForRow( m_stashesList->GetNextItem( -1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED ) );
271
272 if( id.IsEmpty() )
273 return;
274
275 // Applying needs a clean tree, so report unsaved editors before asking to rewrite files.
276 m_dialog->RefuseOverUncommittedWork(
277 [this, id, aRemoveAfterApply]( const GIT_WORKTREE_STATUS& aStatus )
278 {
279 wxString question = aRemoveAfterApply ? _( "Apply this stash and remove it from the list?" )
280 : _( "Apply this stash to the working tree?" );
281
282 if( !IsOK( this, question ) )
283 return;
284
285 if( !m_dialog->ConfirmWorkingTreeChange( aStatus ) )
286 return;
287
288 m_dialog->RunGitAction(
289 [id, aRemoveAfterApply]( KIGIT_COMMON& aCommon )
290 {
291 size_t index = 0;
292
293 if( !findStashByIdUnlocked( aCommon, id, index ) )
294 return false;
295
296 return aRemoveAfterApply ? aCommon.StashPop( index ) : aCommon.StashApply( index );
297 },
298 aRemoveAfterApply ? _( "Could not pop the stash. Another program may have changed the stash "
299 "list, so nothing was applied." )
300 : _( "Could not apply the stash. Another program may have changed the stash "
301 "list, so nothing was applied." ),
302 /* rewrites working tree */ true );
303 } );
304}
305
306
311
312
317
318
320{
321 if( !m_dialog->SessionStillCurrent() )
322 return;
323
324 // Resolved before the confirmation, which runs a nested event loop in which a refresh can
325 // rebuild the list and renumber the rows.
326 wxString id = stashIdForRow( m_stashesList->GetNextItem( -1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED ) );
327
328 if( id.IsEmpty() )
329 return;
330
331 if( !IsOK( this, _( "Drop this stash? This cannot be undone." ) ) )
332 return;
333
334 m_dialog->RunGitAction(
335 [id]( KIGIT_COMMON& aCommon )
336 {
337 size_t index = 0;
338
339 if( !findStashByIdUnlocked( aCommon, id, index ) )
340 return false;
341
342 return aCommon.StashDrop( index );
343 },
344 _( "Could not drop the stash. Another program may have changed the stash list, so nothing "
345 "was dropped." ) );
346}
347
348
350{
351 if( !m_dialog->SessionStillCurrent() )
352 return;
353
354 struct PREFLIGHT
355 {
356 wxString lfsMessage;
357 KIGIT::GIT_IDENTITY identity;
358 };
359
360 std::shared_ptr<KIGIT_COMMON> common = m_frame->Prj().GetGitCommon();
361 auto preflight = std::make_shared<PREFLIGHT>();
362
363 m_actionQuery.Run(
364 common,
365 [preflight]( KIGIT_COMMON& aCommon )
366 {
367 preflight->lfsMessage = KIGIT::PROJECT_GIT_UTILS::LfsRefusal( &aCommon );
368 preflight->identity = KIGIT::ReadGitIdentity( aCommon );
369 },
370 [this, common, preflight]()
371 {
372 if( !m_dialog->SessionStillCurrent() || !common->GetRepo() )
373 return;
374
375 if( !preflight->lfsMessage.IsEmpty() )
376 {
377 DisplayErrorMessage( this, preflight->lfsMessage );
378 return;
379 }
380
381 // Stashing writes a commit object, so it needs an author identity like a commit does.
382 if( !KIGIT::EnsureGitIdentity( this, m_frame, common, preflight->identity ) )
383 return;
384
385 // Unsaved editor changes are written to disk here, so they are part of what gets
386 // stashed instead of surviving in memory.
387 if( !m_dialog->ConfirmWorkingTreeChange() )
388 return;
389
391 } );
392}
393
394
396{
397 struct OUTCOME
398 {
399 GIT_WORKTREE_STATUS worktree;
400 bool hasNewFilesOnly = false;
401 };
402
403 wxString projectPath = m_frame->Prj().GetProjectPath();
404 bool dark = KIPLATFORM::UI::IsDarkTheme();
405 auto outcome = std::make_shared<OUTCOME>();
406
407 m_actionQuery.Run(
408 m_frame->Prj().GetGitCommon(),
409 [outcome, projectPath, dark]( KIGIT_COMMON& aCommon )
410 {
411 outcome->worktree = ReadGitWorktreeStatus( aCommon, projectPath );
412 outcome->hasNewFilesOnly = !outcome->worktree.hasTrackedChanges
413 && !BuildGitChangeEntries( aCommon, projectPath, dark ).empty();
414 },
415 [this, outcome]()
416 {
417 if( !m_dialog->SessionStillCurrent() )
418 return;
419
420 if( !ProjectSafeToRewrite( m_frame, this, outcome->worktree, /* allow tracked changes */ true ) )
421 return;
422
423 if( !outcome->worktree.hasTrackedChanges )
424 {
425 // A stash only includes files that have been committed before. When the only changes
426 // are brand new files, saying "no changes" would contradict what the panel shows.
427 if( outcome->hasNewFilesOnly )
428 {
429 DisplayInfoMessage( this, _( "The changed files have never been committed, and a stash only "
430 "includes files that have been. Commit them from the Version "
431 "Control panel first, or discard them there." ) );
432 }
433 else
434 {
435 DisplayInfoMessage( this, _( "There are no changes to stash." ) );
436 }
437
438 return;
439 }
440
441 wxTextEntryDialog dlg( this, _( "Optional description for this stash:" ), _( "Stash Changes" ) );
442
443 if( dlg.ShowModal() != wxID_OK || !m_dialog->SessionStillCurrent() )
444 return;
445
446 // Editing can resume while either the status read or the description dialog runs.
447 // Do not rewrite a document whose editor has acquired fresh unsaved changes.
448 if( !ProjectSafeToRewrite( m_frame, this, outcome->worktree, /* allow tracked changes */ true ) )
449 return;
450
451 wxString message = dlg.GetValue().Trim( true ).Trim( false );
452
453 m_dialog->RunGitAction(
454 [message]( KIGIT_COMMON& aCommon )
455 {
456 return aCommon.StashSave( message );
457 },
458 _( "Could not stash the changes." ), /* rewrites working tree */ true );
459 } );
460}
int index
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 wxString LfsRefusal(KIGIT_COMMON *aCommon)
Return a refusal message for LFS files or an unknown project folder, otherwise empty.
The project's git session.
bool StashPop(size_t aIndex)
std::vector< KIGIT_STASH_INFO > GetStashes() const
bool StashApply(size_t aIndex)
bool StashDrop(size_t aIndex)
PANEL_GIT_STASHES_BASE(wxWindow *parent, wxWindowID id=wxID_ANY, const wxPoint &pos=wxDefaultPosition, const wxSize &size=wxSize(-1,-1), long style=wxTAB_TRAVERSAL, const wxString &name=wxEmptyString)
wxSplitterWindow * m_stashesSplitter
int m_stashSashPos
Where the user left the sash between the list and its detail pane.
void showStashDetail(const wxString &aId)
Fill the detail pane with the stash aId names, or hide it when the id is empty or gone.
void Populate()
Fill the list from the repository's stashes. The repository is read on a worker.
EDA_BASE_FRAME * m_frame
wxString stashIdForRow(long aRow) const
The stash shown on aRow, by its commit, or empty when the row is gone.
PANEL_GIT_STASHES(wxWindow *aParent, GIT_ADVANCED_DIALOG *aDialog, EDA_BASE_FRAME *aFrame)
KIGIT_QUERY m_listQuery
List and detail reads share the session lock without waiting on the UI thread.
KIGIT_QUERY m_detailQuery
void checkStashChanges()
After saving the editors, read the changes before asking for a stash description.
std::vector< wxString > m_stashIds
The stash commit behind each row, since a row's index is only valid for the listing it came from.
void restoreSelectedStash(bool aRemoveAfterApply)
bool m_fillingStashes
True while the list is being refilled, when its selection events are its own.
GIT_ADVANCED_DIALOG * m_dialog
KIGIT_QUERY m_actionQuery
Reads needed before an action's confirmation, separate from list refreshes.
void fillStashDetail(const KIGIT_STASH_DETAIL &aDetail)
bool IsOK(wxWindow *aParent, const wxString &aMessage)
Display a yes/no dialog with aMessage and returns the user response.
Definition confirm.cpp:274
void DisplayErrorMessage(wxWindow *aParent, const wxString &aText, const wxString &aExtraInfo)
Display an error message with aMessage.
Definition confirm.cpp:217
This file is part of the common library.
#define _(s)
Base window classes and related definitions.
void ShowGitCommitDiff(EDA_BASE_FRAME *aFrame, const wxString &aCommitHash, wxWindow *aParentWindow, const wxString &aRelativePath)
Open the diff for what a commit changed, comparing it against its parent.
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.
std::string message
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.
void UnsplitKeepingSash(wxSplitterWindow *aSplitter, wxWindow *aPane, int &aSashPos)
Close a splitter's second pane, keeping the sash where the user left it for next time.
wxString RelativeDate(const wxDateTime &aWhen)
"just now", "3 hours ago", or the ISO date once it is more than a month old.
void MarkDestructive(wxButton *aButton)
Paint a button's text the diff's red, for one that throws something away.
bool IsDarkTheme()
Determine if the desktop interface is currently using a dark theme or a light theme.
Definition wxgtk/ui.cpp:54
Repository state used before an operation that requires a clean working tree.
Repository values and whether Git can use them to record a commit.
wxString branch
The branch the stash was taken on, as its message records it.
std::vector< wxString > files
Paths the stash changes, new files included.
wxString message
What was stashed, without the "WIP on branch:" prefix.
wxString baseHash
Short hash of the commit the stash was taken on.