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",