KiCad PCB EDA Suite
Loading...
Searching...
No Matches
panel_git_branches.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#include <vector>
24
25#include <wx/richmsgdlg.h>
26#include <wx/textdlg.h>
27
28#include <confirm.h>
33#include <eda_base_frame.h>
36#include <project.h>
37
38
40 PANEL_GIT_BRANCHES_BASE( aParent ),
41 m_dialog( aDialog ),
42 m_frame( aFrame )
43{
44 m_branchSearch->SetDescriptiveText( _( "Search branches" ) );
45
46 m_branchesList->AppendColumn( _( "Branch" ), wxLIST_FORMAT_LEFT, FromDIP( 220 ) );
47 m_branchesList->AppendColumn( _( "Status" ), wxLIST_FORMAT_LEFT, FromDIP( 70 ) );
48 m_branchesList->AppendColumn( _( "Commit" ), wxLIST_FORMAT_LEFT, FromDIP( 100 ) );
49 m_branchesList->AppendColumn( _( "Date" ), wxLIST_FORMAT_LEFT, FromDIP( 90 ) );
50
53
54 // The detail pane appears once a branch is selected.
55 m_branchSashPos = FromDIP( 460 );
57
58 m_branchSearch->Bind( wxEVT_TEXT,
59 [this]( wxCommandEvent& )
60 {
62 } );
63
64 m_branchesList->Bind( wxEVT_LIST_ITEM_SELECTED,
65 [this]( wxListEvent& aEvent )
66 {
68 showBranchDetail( m_branchesList->GetItemText( aEvent.GetIndex(), 0 ) );
69 } );
70
71 m_branchesList->Bind( wxEVT_LIST_ITEM_DESELECTED,
72 [this]( wxListEvent& )
73 {
75 && m_branchesList->GetNextItem( -1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED ) == -1 )
76 {
78 }
79 } );
80
81 // Double-clicking a branch switches the working tree to it.
82 m_branchesList->SetToolTip( _( "Double click a local branch to switch the project to it." ) );
83 m_branchesList->Bind( wxEVT_LIST_ITEM_ACTIVATED,
84 [this]( wxListEvent& aEvent )
85 {
86 int row = aEvent.GetIndex();
87
88 // A remote branch is someone else's work to start from, not
89 // somewhere to stand, so New Branch is the way to take it up.
90 if( row < static_cast<int>( m_branchIsRemote.size() ) && m_branchIsRemote[row] )
91 {
92 DisplayInfoMessage( this, _( "This branch is on the remote. Select it and "
93 "press New Branch to start your own branch "
94 "from it." ) );
95 return;
96 }
97
98 m_dialog->SwitchToBranch( m_branchesList->GetItemText( row, 0 ) );
99 } );
100
101 m_newBranchButton->Bind( wxEVT_BUTTON,
102 [this]( wxCommandEvent& )
103 {
104 onNewBranch();
105 } );
106 m_deleteBranchButton->Bind( wxEVT_BUTTON,
107 [this]( wxCommandEvent& )
108 {
110 } );
111 m_forcePushButton->Bind( wxEVT_BUTTON,
112 [this]( wxCommandEvent& )
113 {
114 onForcePush();
115 } );
116}
117
118
123
124
126{
127 m_push.Cancel();
128}
129
130
132{
133 if( !m_dialog->SessionStillCurrent() )
134 return;
135
136 m_detailQuery.Cancel();
137
138 auto branches = std::make_shared<std::vector<KIGIT_BRANCH_INFO>>();
139
140 m_listQuery.Run(
141 m_frame->Prj().GetGitCommon(),
142 [branches]( KIGIT_COMMON& aCommon )
143 {
144 *branches = aCommon.GetBranches();
145 KIGIT::SortBranchesForDisplay( *branches );
146 },
147 [this, branches]()
148 {
149 if( !m_dialog->SessionStillCurrent() )
150 return;
151
152 // Preserve selections and search edits made while the repository was being read.
153 wxString search = m_branchSearch->GetValue().Lower();
154 wxString selected = KIGIT::SelectedText( m_branchesList, 0 );
155 long selectRow = wxNOT_FOUND;
156
157 m_detailQuery.Cancel();
158 m_fillingBranches = true;
159 m_branchesList->DeleteAllItems();
160 m_branchIsRemote.clear();
161
162 for( const KIGIT_BRANCH_INFO& branch : *branches )
163 {
164 if( !search.IsEmpty() && !branch.name.Lower().Contains( search ) )
165 continue;
166
167 long idx = m_branchesList->InsertItem( m_branchesList->GetItemCount(), branch.name );
168
169 if( branch.isCurrent )
170 m_branchesList->SetItem( idx, 1, _( "current" ) );
171 else if( branch.isRemote )
172 m_branchesList->SetItem( idx, 1, _( "remote" ) );
173
174 m_branchesList->SetItem( idx, 2, branch.tipHash );
175 m_branchesList->SetItem( idx, 3, KIGIT::RelativeDate( branch.when ) );
176 m_branchIsRemote.push_back( branch.isRemote );
177
178 // Keep the selection where it was, or start on the current branch.
179 if( branch.name == selected || ( selected.IsEmpty() && branch.isCurrent ) )
180 selectRow = idx;
181 }
182
183 int shown = m_branchesList->GetItemCount();
184 m_branchCount->SetLabel( shown == 1 ? _( "1 branch" ) : wxString::Format( _( "%d branches" ), shown ) );
185
186 if( selectRow != wxNOT_FOUND )
187 {
188 m_branchesList->SetItemState( selectRow, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED );
189 showBranchDetail( m_branchesList->GetItemText( selectRow, 0 ) );
190 }
191 else
192 {
194 }
195
196 m_fillingBranches = false;
197 } );
198}
199
200
205
206
207void PANEL_GIT_BRANCHES::showBranchDetail( const wxString& aBranch )
208{
210
211 if( aBranch.IsEmpty() || !m_dialog->SessionStillCurrent() )
212 return;
213
214 auto detail = std::make_shared<KIGIT_BRANCH_DETAIL>();
215
216 m_detailQuery.Run(
217 m_frame->Prj().GetGitCommon(),
218 [detail, aBranch]( KIGIT_COMMON& aCommon )
219 {
220 *detail = aCommon.GetBranchDetail( aBranch );
221 },
222 [this, detail, aBranch]()
223 {
224 if( m_dialog->SessionStillCurrent() && KIGIT::SelectedText( m_branchesList, 0 ) == aBranch )
225 {
226 fillBranchDetail( *detail );
227 }
228 } );
229}
230
231
233{
234 if( !aDetail.found )
235 {
237 return;
238 }
239
240 m_branchTitle->SetLabel( wxString::Format( _( "Branch: %s" ), aDetail.name ) );
241 m_branchCommit->SetLabel(
242 wxString::Format( _( "Commit: %s, %s" ), aDetail.tipHash, KIGIT::RelativeDate( aDetail.when ) ) );
243 m_branchAuthor->SetLabel( wxString::Format( _( "Author: %s <%s>" ), aDetail.author, aDetail.email ) );
244 m_branchMessage->ChangeValue( aDetail.message );
245
246 wxString sync;
247
248 if( aDetail.upstream.IsEmpty() )
249 sync = _( "Not tracking another branch." );
250 else if( !aDetail.divergence )
251 sync = wxString::Format( _( "Could not compare with %s." ), aDetail.upstream );
252 else
253 {
254 const KIGIT_AHEAD_BEHIND& counts = *aDetail.divergence;
255
256 if( counts.ahead == 0 && counts.behind == 0 )
257 sync = wxString::Format( _( "Up to date with %s." ), aDetail.upstream );
258 else if( counts.behind == 0 )
259 sync = wxString::Format( _( "%zu commits ahead of %s." ), counts.ahead, aDetail.upstream );
260 else if( counts.ahead == 0 )
261 sync = wxString::Format( _( "%zu commits behind %s." ), counts.behind, aDetail.upstream );
262 else
263 sync = wxString::Format( _( "%zu commits ahead and %zu behind %s." ), counts.ahead, counts.behind,
264 aDetail.upstream );
265 }
266
267 m_branchSync->SetLabel( sync );
268
269 if( !m_branchesSplitter->IsSplit() )
271
272 m_branchDetail->Layout();
273}
274
275
281
282
284{
285 if( !m_dialog->SessionStillCurrent() )
286 return;
287
288 // The new branch starts where the selected one is, so a branch can be made off any other.
289 wxString from = KIGIT::SelectedText( m_branchesList, 0 );
290 wxString prompt =
291 from.IsEmpty() ? _( "New branch name:" ) : wxString::Format( _( "New branch from '%s', name:" ), from );
292 wxString name;
293 wxString nameError;
294
295 do
296 {
297 if( !nameError.IsEmpty() )
298 DisplayErrorMessage( this, nameError );
299
300 name = wxGetTextFromUser( prompt, _( "New Branch" ), name, this );
301
302 if( name.IsEmpty() || !m_dialog->SessionStillCurrent() )
303 return;
304
305 nameError = KIGIT::BranchNameError( name );
306 } while( !nameError.IsEmpty() );
307
308 m_dialog->RunGitAction(
309 [name, from]( KIGIT_COMMON& aCommon )
310 {
311 return aCommon.CreateBranch( name, from );
312 },
313 wxString::Format( _( "Could not create branch '%s'." ), name ) );
314}
315
316
318{
319 if( !m_dialog->SessionStillCurrent() )
320 return;
321
322 wxString name = KIGIT::SelectedText( m_branchesList, 0 );
323
324 if( name.IsEmpty() )
325 return;
326
327 // Deleting a remote-tracking branch would mean deleting it on the server, and the lookup
328 // below could otherwise hit a local branch of the same name.
329 if( KIGIT::SelectedText( m_branchesList, 1 ) == _( "remote" ) )
330 {
331 DisplayInfoMessage( this, _( "This branch lives on the remote, so it cannot be deleted from here." ) );
332 return;
333 }
334
335 struct OUTCOME
336 {
337 wxString current;
338 std::optional<size_t> unmerged;
339 bool checked = false;
340 };
341
342 auto outcome = std::make_shared<OUTCOME>();
343
344 m_actionQuery.Run(
345 m_frame->Prj().GetGitCommon(),
346 [outcome, name]( KIGIT_COMMON& aCommon )
347 {
348 outcome->current = aCommon.GetCurrentBranchName();
349 outcome->unmerged = aCommon.GetUnmergedCommitCount( name );
350 outcome->checked = true;
351 },
352 [this, outcome, name]()
353 {
354 if( !m_dialog->SessionStillCurrent() || !outcome->checked )
355 return;
356
357 if( name == outcome->current )
358 {
359 DisplayErrorMessage( this, _( "You cannot delete the branch you are on." ) );
360 return;
361 }
362
363 if( !outcome->unmerged )
364 {
365 DisplayErrorMessage( this, _( "Could not compare this branch with the current version." ) );
366 return;
367 }
368
369 wxString question;
370
371 if( *outcome->unmerged == 1 )
372 {
373 question = wxString::Format( _( "Branch '%s' has one commit that is not in the current version."
374 "\n\nDelete it anyway?" ),
375 name );
376 }
377 else if( *outcome->unmerged > 1 )
378 {
379 question = wxString::Format( _( "Branch '%s' has %zu commits that are not in the current version."
380 "\n\nDelete it anyway?" ),
381 name, *outcome->unmerged );
382 }
383 else
384 {
385 question = wxString::Format( _( "Delete branch '%s'?" ), name );
386 }
387
388 wxRichMessageDialog confirm( this, question, _( "Delete Branch" ),
389 wxYES_NO | wxNO_DEFAULT | wxICON_WARNING | wxCENTER );
390 confirm.SetYesNoLabels( _( "&Delete" ), _( "Cancel" ) );
391
392 if( confirm.ShowModal() != wxID_YES || !m_dialog->SessionStillCurrent() )
393 return;
394
395 m_dialog->RunGitAction(
396 [name]( KIGIT_COMMON& aCommon )
397 {
398 return aCommon.DeleteBranch( name );
399 },
400 wxString::Format( _( "Could not delete branch '%s'." ), name ) );
401 } );
402}
403
404
406{
407 if( m_push.IsBusy() )
408 {
410 return;
411 }
412
413 if( !m_dialog->SessionStillCurrent() )
414 return;
415
416 struct OUTCOME
417 {
418 wxString branch;
419 wxString headOid;
420 wxString upstream;
421 bool hasRemote = false;
422 bool detached = false;
423 bool checked = false;
424 };
425
426 std::shared_ptr<KIGIT_COMMON> common = m_frame->Prj().GetGitCommon();
427 auto outcome = std::make_shared<OUTCOME>();
428
429 m_actionQuery.Run(
430 common,
431 [outcome]( KIGIT_COMMON& aCommon )
432 {
433 outcome->hasRemote = aCommon.HasPushAndPullRemote();
434 outcome->detached = aCommon.IsHeadDetached();
435 outcome->branch = aCommon.GetCurrentBranchName();
436 outcome->headOid = aCommon.GetHeadCommitId();
437
438 // Push targets the same-named remote ref, even with a differently named upstream.
439 outcome->upstream = aCommon.GetRemoteNameOrDefault() + wxS( "/" ) + outcome->branch;
440 outcome->checked = true;
441 },
442 [this, common, outcome]()
443 {
444 if( !m_dialog->SessionStillCurrent() || !outcome->checked || m_push.IsBusy() )
445 return;
446
447 if( outcome->detached )
448 {
449 DisplayInfoMessage( this,
450 _( "This project is showing an old snapshot rather than a branch, so there "
451 "is no branch to push. Double click a branch to go back to it first." ) );
452 return;
453 }
454
455 if( !outcome->hasRemote )
456 {
457 DisplayInfoMessage( this, _( "No remote is configured to push to." ) );
458 return;
459 }
460
461 wxRichMessageDialog confirm(
462 this,
463 wxString::Format( _( "Force push replaces '%s' with your local '%s'.\n\n"
464 "Commits that exist only on the remote will be lost." ),
465 outcome->upstream, outcome->branch ),
466 _( "Force Push" ), wxYES_NO | wxNO_DEFAULT | wxICON_WARNING | wxCENTER );
467 confirm.SetYesNoLabels( _( "Force &Push" ), _( "Cancel" ) );
468
469 if( confirm.ShowModal() == wxID_YES && m_dialog->SessionStillCurrent() )
470 startForcePush( common, outcome->branch, outcome->headOid );
471 } );
472}
473
474
475void PANEL_GIT_BRANCHES::startForcePush( std::shared_ptr<KIGIT_COMMON> aCommon, const wxString& aExpectedBranch,
476 const wxString& aExpectedHeadOid )
477{
478 // Carried rather than captured by reference, because Start returns before the work runs.
479 struct OUTCOME
480 {
482 wxString error;
483 bool authFailure = false;
484 bool cancelled = false;
485 };
486
487 auto outcome = std::make_shared<OUTCOME>();
488
489 m_dialog->SetPaneTransferBusy( true, _( "Pushing..." ) );
490
491 bool started = m_push.Start(
492 "git force push",
493 [aCommon, outcome, aExpectedBranch, aExpectedHeadOid, alive = m_push.Alive(),
494 dialog = m_dialog]( const KIGIT_COMMON::CANCEL_TOKEN& aToken )
495 {
496 aCommon->ClearAuthFailure();
497
498 GIT_PUSH_HANDLER handler( aCommon.get() );
499 handler.SetProgressReporter( std::make_unique<KIGIT_STATUS_REPORTER>(
500 alive,
501 [dialog]( const wxString& aText )
502 {
503 dialog->ShowTransferProgress( aText );
504 } ) );
505 handler.SetCancelToken( aToken );
506 handler.SetWaitForActionLock( true );
507 handler.SetExpectedTarget( aExpectedBranch, aExpectedHeadOid );
508 outcome->result = handler.PerformPush( true );
509 outcome->error = handler.GetErrorString();
510 outcome->authFailure = aCommon->WasAuthFailure();
511
512 // This operation's own token, so a force push that never became active cannot
513 // consume a cancel belonging to the transfer that is running.
514 outcome->cancelled = handler.IsThisOperationCancelled();
515
516 // The other surfaces need this even when the page that started it has gone.
517 if( outcome->result == PushResult::Success && !outcome->cancelled )
518 {
520 [aCommon]()
521 {
522 aCommon->NotifyRefreshListeners();
523 } );
524 }
525 },
526 [this, aCommon, outcome]()
527 {
528 if( !m_dialog->SessionStillCurrent() )
529 return;
530
531 m_dialog->SetPaneTransferBusy( false );
532
533 if( outcome->cancelled || outcome->result == PushResult::Success )
534 return;
535
536 if( outcome->result == PushResult::Busy )
537 {
539 return;
540 }
541
542 if( outcome->authFailure && KIGIT::PromptForCredentials( this, aCommon.get(), &m_frame->Prj() ) )
543 {
544 onForcePush();
545 return;
546 }
547
548 DisplayErrorMessage( this, _( "Force push failed." ), outcome->error );
549 } );
550
551 if( !started )
552 {
553 m_dialog->SetPaneTransferBusy( false );
554 DisplayInfoMessage( this, _( "Could not start the force push." ) );
555 }
556}
const char * name
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...
The project's git session.
std::shared_ptr< std::atomic< bool > > CANCEL_TOKEN
One token per operation.
wxString GetCurrentBranchName() const
wxString GetRemoteNameOrDefault() const
Returns GetRemotename() when non-empty, otherwise "origin".
bool CreateBranch(const wxString &aName, const wxString &aFrom=wxEmptyString)
wxString GetHeadCommitId() const
Full commit ID at HEAD, or empty for an unborn or missing HEAD.
bool HasPushAndPullRemote() const
bool IsHeadDetached() const
True when HEAD points at a commit rather than a branch, as after checking out a tag.
wxSplitterWindow * m_branchesSplitter
PANEL_GIT_BRANCHES_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)
void StopTransfer()
Ask a force push still running to stop. It finishes on its own.
PANEL_GIT_BRANCHES(wxWindow *aParent, GIT_ADVANCED_DIALOG *aDialog, EDA_BASE_FRAME *aFrame)
void fillBranchDetail(const KIGIT_BRANCH_DETAIL &aDetail)
int m_branchSashPos
Where the user left the sash between the list and its detail pane.
KIGIT_QUERY m_listQuery
List and detail reads share the session lock without waiting on the UI thread.
void showBranchDetail(const wxString &aBranch)
Fill the detail pane with aBranch, or hide it when the name is empty or gone.
GIT_ADVANCED_DIALOG * m_dialog
void reloadBranches()
Refill the list after the search text changed.
void onForcePush()
Force push the current branch, after an explicit confirmation.
void Populate()
Fill the list from the repository's branches. The repository is read on a worker.
EDA_BASE_FRAME * m_frame
std::vector< bool > m_branchIsRemote
Whether each listed row is a remote-tracking branch.
void startForcePush(std::shared_ptr< KIGIT_COMMON > aCommon, const wxString &aExpectedBranch, const wxString &aExpectedHeadOid)
KIGIT_TRANSFER m_push
The force push this page started.
KIGIT_QUERY m_actionQuery
Reads needed before an action's confirmation, separate from list refreshes.
bool m_fillingBranches
True while the list is being refilled, when its selection events are its own.
void DisplayInfoMessage(wxWindow *aParent, const wxString &aMessage, const wxString &aExtraInfo)
Display an informational message box with aMessage.
Definition confirm.cpp:245
void DisplayErrorMessage(wxWindow *aParent, const wxString &aText, const wxString &aExtraInfo)
Display an error message with aMessage.
Definition confirm.cpp:217
This file is part of the common library.
#define _(s)
Base window classes and related definitions.
PushResult
@ Busy
Another git operation held the action lock, so nothing was pushed.
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 ...
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.
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.
void ShowBusyMessage(wxWindow *aParent)
Show a busy message for a user action. UI thread only.
wxString SelectedText(wxListCtrl *aList, int aColumn)
The text of the selected row in a report list, or empty when nothing is selected.
wxString RelativeDate(const wxDateTime &aWhen)
"just now", "3 hours ago", or the ISO date once it is more than a month old.
void CallOnUi(std::function< void()> aFn)
Run aFn on the UI thread.
void MarkDestructive(wxButton *aButton)
Paint a button's text the diff's red, for one that throws something away.
size_t behind
Commits only in the second reference.
size_t ahead
Commits only in the first reference.
std::optional< KIGIT_AHEAD_BEHIND > divergence
Comparison with upstream, if it could be read.
wxString upstream
The branch this one tracks, or empty.
wxString result
Test unit parsing edge cases and error handling.