From 3ac5ef2d418879999d05f08fd174e05286b0fd58 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Fri, 22 Sep 2023 16:09:02 +0200 Subject: [PATCH] Show sync status in branches list When pulling/pushing/fast-forwarding a branch, show this state in the branches list for that branch for as long as the operation takes, to make it easier to see when it's done (without having to stare at the status bar in the lower left). This will hopefully help with making these operations feel more predictable, now that we no longer show a loader panel for them. --- pkg/gui/context/branches_context.go | 3 ++ pkg/gui/controllers/branches_controller.go | 3 ++ pkg/gui/controllers/helpers/refresh_helper.go | 2 +- pkg/gui/controllers/status_controller.go | 2 +- pkg/gui/controllers/sync_controller.go | 33 ++++++++++----- pkg/gui/gui.go | 31 ++++++++++++++ pkg/gui/presentation/branches.go | 22 +++++++--- pkg/gui/presentation/ref_operations.go | 21 ++++++++++ pkg/gui/presentation/status.go | 5 ++- pkg/gui/types/common.go | 14 +++++++ pkg/gui/view_helpers.go | 42 +++++++++++++++++++ pkg/i18n/english.go | 2 + 12 files changed, 159 insertions(+), 21 deletions(-) create mode 100644 pkg/gui/presentation/ref_operations.go diff --git a/pkg/gui/context/branches_context.go b/pkg/gui/context/branches_context.go index ac1fae52c..16bd58709 100644 --- a/pkg/gui/context/branches_context.go +++ b/pkg/gui/context/branches_context.go @@ -27,6 +27,9 @@ func NewBranchesContext(c *ContextCommon) *BranchesContext { getDisplayStrings := func(_ int, _ int) [][]string { return presentation.GetBranchListDisplayStrings( viewModel.GetItems(), + func(branch *models.Branch) types.RefOperation { + return c.State().GetRefOperation(branch.FullRefName()) + }, c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL, c.Modes().Diffing.Ref, c.Tr, diff --git a/pkg/gui/controllers/branches_controller.go b/pkg/gui/controllers/branches_controller.go index 45f950457..a8e48c052 100644 --- a/pkg/gui/controllers/branches_controller.go +++ b/pkg/gui/controllers/branches_controller.go @@ -571,6 +571,9 @@ func (self *BranchesController) fastForward(branch *models.Branch) error { ) return self.c.WithWaitingStatus(message, func(task gocui.Task) error { + self.c.State().SetRefOperation(branch.FullRefName(), types.RefOperationFastForwarding, context.LOCAL_BRANCHES_CONTEXT_KEY) + defer func() { self.c.State().ClearRefOperation(branch.FullRefName(), context.LOCAL_BRANCHES_CONTEXT_KEY) }() + worktree, ok := self.worktreeForBranch(branch) if ok { self.c.LogAction(action) diff --git a/pkg/gui/controllers/helpers/refresh_helper.go b/pkg/gui/controllers/helpers/refresh_helper.go index 578089af1..51a9e66a9 100644 --- a/pkg/gui/controllers/helpers/refresh_helper.go +++ b/pkg/gui/controllers/helpers/refresh_helper.go @@ -676,7 +676,7 @@ func (self *RefreshHelper) refreshStatus() { repoName := self.c.Git().RepoPaths.RepoName() - status := presentation.FormatStatus(repoName, currentBranch, linkedWorktreeName, workingTreeState, self.c.Tr) + status := presentation.FormatStatus(repoName, currentBranch, types.RefOperationNone, linkedWorktreeName, workingTreeState, self.c.Tr) self.c.SetViewContent(self.c.Views().Status, status) } diff --git a/pkg/gui/controllers/status_controller.go b/pkg/gui/controllers/status_controller.go index 8344dfe76..efa893340 100644 --- a/pkg/gui/controllers/status_controller.go +++ b/pkg/gui/controllers/status_controller.go @@ -106,7 +106,7 @@ func (self *StatusController) onClick() error { } cx, _ := self.c.Views().Status.Cursor() - upstreamStatus := presentation.BranchStatus(currentBranch, self.c.Tr) + upstreamStatus := presentation.BranchStatus(currentBranch, types.RefOperationNone, self.c.Tr) repoName := self.c.Git().RepoPaths.RepoName() workingTreeState := self.c.Git().Status.WorkingTreeState() switch workingTreeState { diff --git a/pkg/gui/controllers/sync_controller.go b/pkg/gui/controllers/sync_controller.go index a77d047a6..e9f12eabb 100644 --- a/pkg/gui/controllers/sync_controller.go +++ b/pkg/gui/controllers/sync_controller.go @@ -7,6 +7,7 @@ import ( "github.com/jesseduffield/gocui" "github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/utils" ) @@ -73,13 +74,13 @@ func (self *SyncController) push(currentBranch *models.Branch) error { if currentBranch.IsTrackingRemote() { opts := pushOpts{} if currentBranch.HasCommitsToPull() { - return self.requestToForcePush(opts) + return self.requestToForcePush(currentBranch, opts) } else { - return self.pushAux(opts) + return self.pushAux(currentBranch, opts) } } else { if self.c.Git().Config.GetPushToCurrent() { - return self.pushAux(pushOpts{setUpstream: true}) + return self.pushAux(currentBranch, pushOpts{setUpstream: true}) } else { return self.c.Helpers().Upstream.PromptForUpstreamWithInitialContent(currentBranch, func(upstream string) error { upstreamRemote, upstreamBranch, err := self.c.Helpers().Upstream.ParseUpstream(upstream) @@ -87,7 +88,7 @@ func (self *SyncController) push(currentBranch *models.Branch) error { return self.c.Error(err) } - return self.pushAux(pushOpts{ + return self.pushAux(currentBranch, pushOpts{ setUpstream: true, upstreamRemote: upstreamRemote, upstreamBranch: upstreamBranch, @@ -107,11 +108,11 @@ func (self *SyncController) pull(currentBranch *models.Branch) error { return self.c.Error(err) } - return self.PullAux(PullFilesOptions{Action: action}) + return self.PullAux(currentBranch, PullFilesOptions{Action: action}) }) } - return self.PullAux(PullFilesOptions{Action: action}) + return self.PullAux(currentBranch, PullFilesOptions{Action: action}) } func (self *SyncController) setCurrentBranchUpstream(upstream string) error { @@ -139,8 +140,13 @@ type PullFilesOptions struct { Action string } -func (self *SyncController) PullAux(opts PullFilesOptions) error { +func (self *SyncController) PullAux(currentBranch *models.Branch, opts PullFilesOptions) error { return self.c.WithWaitingStatus(self.c.Tr.PullingStatus, func(task gocui.Task) error { + self.c.State().SetRefOperation(currentBranch.FullRefName(), types.RefOperationPulling, context.LOCAL_BRANCHES_CONTEXT_KEY) + defer func() { + self.c.State().ClearRefOperation(currentBranch.FullRefName(), context.LOCAL_BRANCHES_CONTEXT_KEY) + }() + return self.pullWithLock(task, opts) }) } @@ -167,8 +173,13 @@ type pushOpts struct { setUpstream bool } -func (self *SyncController) pushAux(opts pushOpts) error { +func (self *SyncController) pushAux(currentBranch *models.Branch, opts pushOpts) error { return self.c.WithWaitingStatus(self.c.Tr.PushingStatus, func(task gocui.Task) error { + self.c.State().SetRefOperation(currentBranch.FullRefName(), types.RefOperationPushing, context.LOCAL_BRANCHES_CONTEXT_KEY) + defer func() { + self.c.State().ClearRefOperation(currentBranch.FullRefName(), context.LOCAL_BRANCHES_CONTEXT_KEY) + }() + self.c.LogAction(self.c.Tr.Actions.Push) err := self.c.Git().Sync.Push( task, @@ -192,7 +203,7 @@ func (self *SyncController) pushAux(opts pushOpts) error { newOpts := opts newOpts.force = true - return self.pushAux(newOpts) + return self.pushAux(currentBranch, newOpts) }, }) return nil @@ -203,7 +214,7 @@ func (self *SyncController) pushAux(opts pushOpts) error { }) } -func (self *SyncController) requestToForcePush(opts pushOpts) error { +func (self *SyncController) requestToForcePush(currentBranch *models.Branch, opts pushOpts) error { forcePushDisabled := self.c.UserConfig.Git.DisableForcePushing if forcePushDisabled { return self.c.ErrorMsg(self.c.Tr.ForcePushDisabled) @@ -214,7 +225,7 @@ func (self *SyncController) requestToForcePush(opts pushOpts) error { Prompt: self.forcePushPrompt(), HandleConfirm: func() error { opts.force = true - return self.pushAux(opts) + return self.pushAux(currentBranch, opts) }, }) } diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 1f76e281c..d851d548d 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -110,6 +110,10 @@ type Gui struct { // lazygit was opened in, or if we'll retain the one we're currently in. RetainOriginalDir bool + refOperations map[string]types.RefOperation + refOperationsMutex *deadlock.Mutex + contextsWithInlineStatus map[types.ContextKey]*inlineStatusInfo + PrevLayout PrevLayout // this is the initial dir we are in upon opening lazygit. We hold onto this @@ -180,6 +184,29 @@ func (self *StateAccessor) SetRetainOriginalDir(value bool) { self.gui.RetainOriginalDir = value } +func (self *StateAccessor) GetRefOperation(ref string) types.RefOperation { + self.gui.refOperationsMutex.Lock() + defer self.gui.refOperationsMutex.Unlock() + + return self.gui.refOperations[ref] +} + +func (self *StateAccessor) SetRefOperation(ref string, operation types.RefOperation, contextKey types.ContextKey) { + self.gui.refOperationsMutex.Lock() + defer self.gui.refOperationsMutex.Unlock() + + self.gui.refOperations[ref] = operation + self.gui.startRenderingInlineStatus(contextKey) +} + +func (self *StateAccessor) ClearRefOperation(ref string, contextKey types.ContextKey) { + self.gui.refOperationsMutex.Lock() + defer self.gui.refOperationsMutex.Unlock() + + self.gui.stopRenderingInlineStatus(contextKey) + delete(self.gui.refOperations, ref) +} + // we keep track of some stuff from one render to the next to see if certain // things have changed type PrevLayout struct { @@ -473,6 +500,10 @@ func NewGui( }, InitialDir: initialDir, afterLayoutFuncs: make(chan func() error, 1000), + + refOperations: make(map[string]types.RefOperation), + refOperationsMutex: &deadlock.Mutex{}, + contextsWithInlineStatus: make(map[types.ContextKey]*inlineStatusInfo), } gui.PopupHandler = popup.NewPopupHandler( diff --git a/pkg/gui/presentation/branches.go b/pkg/gui/presentation/branches.go index 6d4620238..d550fcd9f 100644 --- a/pkg/gui/presentation/branches.go +++ b/pkg/gui/presentation/branches.go @@ -9,6 +9,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/gui/presentation/icons" "github.com/jesseduffield/lazygit/pkg/gui/style" + "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/i18n" "github.com/jesseduffield/lazygit/pkg/theme" "github.com/jesseduffield/lazygit/pkg/utils" @@ -19,6 +20,7 @@ var branchPrefixColorCache = make(map[string]style.TextStyle) func GetBranchListDisplayStrings( branches []*models.Branch, + getRefOperation func(branch *models.Branch) types.RefOperation, fullDescription bool, diffName string, tr *i18n.TranslationSet, @@ -27,13 +29,14 @@ func GetBranchListDisplayStrings( ) [][]string { return lo.Map(branches, func(branch *models.Branch, _ int) []string { diffed := branch.Name == diffName - return getBranchDisplayStrings(branch, fullDescription, diffed, tr, userConfig, worktrees) + return getBranchDisplayStrings(branch, getRefOperation(branch), fullDescription, diffed, tr, userConfig, worktrees) }) } // getBranchDisplayStrings returns the display string of branch func getBranchDisplayStrings( b *models.Branch, + refOperation types.RefOperation, fullDescription bool, diffed bool, tr *i18n.TranslationSet, @@ -51,7 +54,7 @@ func getBranchDisplayStrings( } coloredName := nameTextStyle.Sprint(displayName) - branchStatus := utils.WithPadding(ColoredBranchStatus(b, tr), 2, utils.AlignLeft) + branchStatus := utils.WithPadding(ColoredBranchStatus(b, refOperation, tr), 2, utils.AlignLeft) if git_commands.CheckedOutByOtherWorktree(b, worktrees) { worktreeIcon := lo.Ternary(icons.IsIconEnabled(), icons.LINKED_WORKTREE_ICON, fmt.Sprintf("(%s)", tr.LcWorktree)) coloredName = fmt.Sprintf("%s %s", coloredName, style.FgDefault.Sprint(worktreeIcon)) @@ -109,9 +112,11 @@ func GetBranchTextStyle(name string) style.TextStyle { } } -func ColoredBranchStatus(branch *models.Branch, tr *i18n.TranslationSet) string { +func ColoredBranchStatus(branch *models.Branch, refOperation types.RefOperation, tr *i18n.TranslationSet) string { colour := style.FgYellow - if branch.UpstreamGone { + if refOperation != types.RefOperationNone { + colour = style.FgCyan + } else if branch.UpstreamGone { colour = style.FgRed } else if branch.MatchesUpstream() { colour = style.FgGreen @@ -119,10 +124,15 @@ func ColoredBranchStatus(branch *models.Branch, tr *i18n.TranslationSet) string colour = style.FgMagenta } - return colour.Sprint(BranchStatus(branch, tr)) + return colour.Sprint(BranchStatus(branch, refOperation, tr)) } -func BranchStatus(branch *models.Branch, tr *i18n.TranslationSet) string { +func BranchStatus(branch *models.Branch, refOperation types.RefOperation, tr *i18n.TranslationSet) string { + refOperationStr := refOperationToString(refOperation, tr) + if refOperationStr != "" { + return refOperationStr + " " + utils.Loader() + } + if !branch.IsTrackingRemote() { return "" } diff --git a/pkg/gui/presentation/ref_operations.go b/pkg/gui/presentation/ref_operations.go new file mode 100644 index 000000000..b87b3fbb4 --- /dev/null +++ b/pkg/gui/presentation/ref_operations.go @@ -0,0 +1,21 @@ +package presentation + +import ( + "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/i18n" +) + +func refOperationToString(refOperation types.RefOperation, tr *i18n.TranslationSet) string { + switch refOperation { + case types.RefOperationNone: + return "" + case types.RefOperationPushing: + return tr.PushingStatus + case types.RefOperationPulling: + return tr.PullingStatus + case types.RefOperationFastForwarding: + return tr.FastForwardingOperation + } + + return "" +} diff --git a/pkg/gui/presentation/status.go b/pkg/gui/presentation/status.go index f70210c27..af7099ac9 100644 --- a/pkg/gui/presentation/status.go +++ b/pkg/gui/presentation/status.go @@ -7,14 +7,15 @@ import ( "github.com/jesseduffield/lazygit/pkg/commands/types/enums" "github.com/jesseduffield/lazygit/pkg/gui/presentation/icons" "github.com/jesseduffield/lazygit/pkg/gui/style" + "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/i18n" ) -func FormatStatus(repoName string, currentBranch *models.Branch, linkedWorktreeName string, workingTreeState enums.RebaseMode, tr *i18n.TranslationSet) string { +func FormatStatus(repoName string, currentBranch *models.Branch, refOperation types.RefOperation, linkedWorktreeName string, workingTreeState enums.RebaseMode, tr *i18n.TranslationSet) string { status := "" if currentBranch.IsRealBranch() { - status += ColoredBranchStatus(currentBranch, tr) + " " + status += ColoredBranchStatus(currentBranch, refOperation, tr) + " " } if workingTreeState != enums.REBASE_MODE_NONE { diff --git a/pkg/gui/types/common.go b/pkg/gui/types/common.go index 80d960eab..9e9a2b23f 100644 --- a/pkg/gui/types/common.go +++ b/pkg/gui/types/common.go @@ -265,6 +265,17 @@ type Mutexes struct { PtyMutex *deadlock.Mutex } +type RefOperation int + +// An long-running operation on a ref (branch or tag). We display these in the +// list of branches or tags so that there's better feedback of what's happening. +const ( + RefOperationNone RefOperation = iota + RefOperationPushing + RefOperationPulling + RefOperationFastForwarding +) + type IStateAccessor interface { GetRepoPathStack() *utils.StringStack GetRepoState() IRepoStateAccessor @@ -277,6 +288,9 @@ type IStateAccessor interface { SetShowExtrasWindow(bool) GetRetainOriginalDir() bool SetRetainOriginalDir(bool) + GetRefOperation(ref string) RefOperation + SetRefOperation(ref string, operation RefOperation, contextKey ContextKey) + ClearRefOperation(ref string, contextKey ContextKey) } type IRepoStateAccessor interface { diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index 58c973712..3a4a1be21 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -148,3 +148,45 @@ func (gui *Gui) postRefreshUpdate(c types.Context) error { return nil } + +type inlineStatusInfo struct { + refCount int + stop chan struct{} +} + +func (gui *Gui) startRenderingInlineStatus(contextKey types.ContextKey) { + info := gui.contextsWithInlineStatus[contextKey] + if info == nil { + info = &inlineStatusInfo{refCount: 0, stop: make(chan struct{})} + gui.contextsWithInlineStatus[contextKey] = info + + go utils.Safe(func() { + ticker := time.NewTicker(time.Millisecond * utils.LoaderAnimationInterval) + defer ticker.Stop() + outer: + for { + select { + case <-ticker.C: + gui.c.OnUIThread(func() error { + _ = gui.c.ContextForKey(contextKey).HandleRender() + return nil + }) + case <-info.stop: + break outer + } + } + }) + } + + info.refCount++ +} + +func (gui *Gui) stopRenderingInlineStatus(contextKey types.ContextKey) { + if info := gui.contextsWithInlineStatus[contextKey]; info != nil { + info.refCount-- + if info.refCount <= 0 { + info.stop <- struct{}{} + delete(gui.contextsWithInlineStatus, contextKey) + } + } +} diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 15deab925..454a2a739 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -185,6 +185,7 @@ type TranslationSet struct { ReturnToFilesPanel string FastForward string FastForwarding string + FastForwardingOperation string FoundConflictsTitle string ViewConflictsMenuItem string AbortMenuItem string @@ -981,6 +982,7 @@ func EnglishTranslationSet() TranslationSet { ReturnToFilesPanel: `Return to files panel`, FastForward: `Fast-forward this branch from its upstream`, FastForwarding: "Fast-forwarding {{.branch}}", + FastForwardingOperation: "Fast-forwarding", FoundConflictsTitle: "Conflicts!", ViewConflictsMenuItem: "View conflicts", AbortMenuItem: "Abort the %s",