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.
This commit is contained in:
Stefan Haller 2023-09-22 16:09:02 +02:00
parent 6da1cf87b1
commit 3ac5ef2d41
12 changed files with 159 additions and 21 deletions

View file

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

View file

@ -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)

View file

@ -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)
}

View file

@ -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 {

View file

@ -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)
},
})
}

View file

@ -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(

View file

@ -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 ""
}

View file

@ -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 ""
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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)
}
}
}

View file

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