diff --git a/pkg/gui/controllers/helpers/app_status_helper.go b/pkg/gui/controllers/helpers/app_status_helper.go index a6befe1a2..0ce30b460 100644 --- a/pkg/gui/controllers/helpers/app_status_helper.go +++ b/pkg/gui/controllers/helpers/app_status_helper.go @@ -33,13 +33,32 @@ func (self *AppStatusHelper) Toast(message string) { self.renderAppStatus() } +// A custom task for WithWaitingStatus calls; it wraps the original one and +// hides the status whenever the task is paused, and shows it again when +// continued. +type appStatusHelperTask struct { + gocui.Task + waitingStatusHandle *status.WaitingStatusHandle +} + +// poor man's version of explicitly saying that struct X implements interface Y +var _ gocui.Task = appStatusHelperTask{} + +func (self appStatusHelperTask) Pause() { + self.waitingStatusHandle.Hide() + self.Task.Pause() +} + +func (self appStatusHelperTask) Continue() { + self.Task.Continue() + self.waitingStatusHandle.Show() +} + // withWaitingStatus wraps a function and shows a waiting status while the function is still executing func (self *AppStatusHelper) WithWaitingStatus(message string, f func(gocui.Task) error) { self.c.OnWorker(func(task gocui.Task) { - self.statusMgr().WithWaitingStatus(message, func() { - self.renderAppStatus() - - if err := f(task); err != nil { + self.statusMgr().WithWaitingStatus(message, self.renderAppStatus, func(waitingStatusHandle *status.WaitingStatusHandle) { + if err := f(appStatusHelperTask{task, waitingStatusHandle}); err != nil { self.c.OnUIThread(func() error { return self.c.Error(err) }) diff --git a/pkg/gui/status/status_manager.go b/pkg/gui/status/status_manager.go index d54853c6e..1f4aaa569 100644 --- a/pkg/gui/status/status_manager.go +++ b/pkg/gui/status/status_manager.go @@ -16,6 +16,24 @@ type StatusManager struct { mutex deadlock.Mutex } +// Can be used to manipulate a waiting status while it is running (e.g. pause +// and resume it) +type WaitingStatusHandle struct { + statusManager *StatusManager + message string + renderFunc func() + id int +} + +func (self *WaitingStatusHandle) Show() { + self.id = self.statusManager.addStatus(self.message, "waiting") + self.renderFunc() +} + +func (self *WaitingStatusHandle) Hide() { + self.statusManager.removeStatus(self.id) +} + type appStatus struct { message string statusType string @@ -26,39 +44,17 @@ func NewStatusManager() *StatusManager { return &StatusManager{} } -func (self *StatusManager) WithWaitingStatus(message string, f func()) { - self.mutex.Lock() +func (self *StatusManager) WithWaitingStatus(message string, renderFunc func(), f func(*WaitingStatusHandle)) { + handle := &WaitingStatusHandle{statusManager: self, message: message, renderFunc: renderFunc, id: -1} + handle.Show() - self.nextId += 1 - id := self.nextId + f(handle) - newStatus := appStatus{ - message: message, - statusType: "waiting", - id: id, - } - self.statuses = append([]appStatus{newStatus}, self.statuses...) - - self.mutex.Unlock() - - f() - - self.removeStatus(id) + handle.Hide() } func (self *StatusManager) AddToastStatus(message string) int { - self.mutex.Lock() - defer self.mutex.Unlock() - - self.nextId++ - id := self.nextId - - newStatus := appStatus{ - message: message, - statusType: "toast", - id: id, - } - self.statuses = append([]appStatus{newStatus}, self.statuses...) + id := self.addStatus(message, "toast") go func() { time.Sleep(time.Second * 2) @@ -84,6 +80,23 @@ func (self *StatusManager) HasStatus() bool { return len(self.statuses) > 0 } +func (self *StatusManager) addStatus(message string, statusType string) int { + self.mutex.Lock() + defer self.mutex.Unlock() + + self.nextId++ + id := self.nextId + + newStatus := appStatus{ + message: message, + statusType: statusType, + id: id, + } + self.statuses = append([]appStatus{newStatus}, self.statuses...) + + return id +} + func (self *StatusManager) removeStatus(id int) { self.mutex.Lock() defer self.mutex.Unlock() diff --git a/pkg/integration/tests/branch/delete_remote_branch_with_credential_prompt.go b/pkg/integration/tests/branch/delete_remote_branch_with_credential_prompt.go new file mode 100644 index 000000000..f145eceaa --- /dev/null +++ b/pkg/integration/tests/branch/delete_remote_branch_with_credential_prompt.go @@ -0,0 +1,87 @@ +package branch + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var DeleteRemoteBranchWithCredentialPrompt = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Delete a remote branch where credentials are required", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) { + }, + SetupRepo: func(shell *Shell) { + shell.EmptyCommit("one") + + shell.CloneIntoRemote("origin") + + shell.NewBranch("mybranch") + + shell.PushBranch("origin", "mybranch") + + // actually getting a password prompt is tricky: it requires SSH'ing into localhost under a newly created, restricted, user. + // This is not easy to do in a cross-platform way, nor is it easy to do in a docker container. + // If you can think of a way to do it, please let me know! + shell.CopyHelpFile("pre-push", ".git/hooks/pre-push") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + deleteBranch := func() { + t.Views().Branches(). + Focus(). + Press(keys.Universal.Remove) + + t.ExpectPopup(). + Menu(). + Title(Equals("Delete branch 'mybranch'?")). + Select(Contains("Delete remote branch")). + Confirm() + + t.ExpectPopup(). + Confirmation(). + Title(Equals("Delete branch 'mybranch'?")). + Content(Equals("Are you sure you want to delete the remote branch 'mybranch' from 'origin'?")). + Confirm() + } + + t.Views().Status().Content(Contains("✓ repo → mybranch")) + + deleteBranch() + + // correct credentials are: username=username, password=password + + t.ExpectPopup().Prompt(). + Title(Equals("Username")). + Type("username"). + Confirm() + + // enter incorrect password + t.ExpectPopup().Prompt(). + Title(Equals("Password")). + Type("incorrect password"). + Confirm() + + t.ExpectPopup().Alert(). + Title(Equals("Error")). + Content(Contains("incorrect username/password")). + Confirm() + + t.Views().Status().Content(Contains("✓ repo → mybranch")) + + // try again with correct password + deleteBranch() + + t.ExpectPopup().Prompt(). + Title(Equals("Username")). + Type("username"). + Confirm() + + t.ExpectPopup().Prompt(). + Title(Equals("Password")). + Type("password"). + Confirm() + + t.Views().Status().Content(Contains("repo → mybranch").DoesNotContain("✓")) + t.Views().Branches().TopLines(Contains("mybranch (upstream gone)")) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 12c4464b5..f4ec26ec2 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -38,6 +38,7 @@ var tests = []*components.IntegrationTest{ branch.CheckoutByName, branch.CreateTag, branch.Delete, + branch.DeleteRemoteBranchWithCredentialPrompt, branch.DetachedHead, branch.OpenWithCliArg, branch.Rebase,