1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-05-21 22:43:27 +02:00

Show sync status in branches list (#3021)

This commit is contained in:
Stefan Haller 2023-10-10 08:32:42 +02:00 committed by GitHub
commit 013cfc77a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 377 additions and 108 deletions

View File

@ -7,7 +7,6 @@ import (
"strings" "strings"
"github.com/go-errors/errors" "github.com/go-errors/errors"
"github.com/sasha-s/go-deadlock"
"github.com/spf13/afero" "github.com/spf13/afero"
gogit "github.com/jesseduffield/go-git/v5" gogit "github.com/jesseduffield/go-git/v5"
@ -63,7 +62,6 @@ func NewGitCommand(
version *git_commands.GitVersion, version *git_commands.GitVersion,
osCommand *oscommands.OSCommand, osCommand *oscommands.OSCommand,
gitConfig git_config.IGitConfig, gitConfig git_config.IGitConfig,
syncMutex *deadlock.Mutex,
) (*GitCommand, error) { ) (*GitCommand, error) {
currentPath, err := os.Getwd() currentPath, err := os.Getwd()
if err != nil { if err != nil {
@ -118,7 +116,6 @@ func NewGitCommand(
gitConfig, gitConfig,
repoPaths, repoPaths,
repository, repository,
syncMutex,
), nil ), nil
} }
@ -129,7 +126,6 @@ func NewGitCommandAux(
gitConfig git_config.IGitConfig, gitConfig git_config.IGitConfig,
repoPaths *git_commands.RepoPaths, repoPaths *git_commands.RepoPaths,
repo *gogit.Repository, repo *gogit.Repository,
syncMutex *deadlock.Mutex,
) *GitCommand { ) *GitCommand {
cmd := NewGitCmdObjBuilder(cmn.Log, osCommand.Cmd) cmd := NewGitCmdObjBuilder(cmn.Log, osCommand.Cmd)
@ -140,7 +136,7 @@ func NewGitCommandAux(
// common ones are: cmn, osCommand, dotGitDir, configCommands // common ones are: cmn, osCommand, dotGitDir, configCommands
configCommands := git_commands.NewConfigCommands(cmn, gitConfig, repo) configCommands := git_commands.NewConfigCommands(cmn, gitConfig, repo)
gitCommon := git_commands.NewGitCommon(cmn, version, cmd, osCommand, repoPaths, repo, configCommands, syncMutex) gitCommon := git_commands.NewGitCommon(cmn, version, cmd, osCommand, repoPaths, repo, configCommands)
fileLoader := git_commands.NewFileLoader(gitCommon, cmd, configCommands) fileLoader := git_commands.NewFileLoader(gitCommon, cmd, configCommands)
statusCommands := git_commands.NewStatusCommands(gitCommon) statusCommands := git_commands.NewStatusCommands(gitCommon)

View File

@ -4,7 +4,6 @@ import (
gogit "github.com/jesseduffield/go-git/v5" gogit "github.com/jesseduffield/go-git/v5"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common" "github.com/jesseduffield/lazygit/pkg/common"
"github.com/sasha-s/go-deadlock"
) )
type GitCommon struct { type GitCommon struct {
@ -15,8 +14,6 @@ type GitCommon struct {
repoPaths *RepoPaths repoPaths *RepoPaths
repo *gogit.Repository repo *gogit.Repository
config *ConfigCommands config *ConfigCommands
// mutex for doing things like push/pull/fetch
syncMutex *deadlock.Mutex
} }
func NewGitCommon( func NewGitCommon(
@ -27,7 +24,6 @@ func NewGitCommon(
repoPaths *RepoPaths, repoPaths *RepoPaths,
repo *gogit.Repository, repo *gogit.Repository,
config *ConfigCommands, config *ConfigCommands,
syncMutex *deadlock.Mutex,
) *GitCommon { ) *GitCommon {
return &GitCommon{ return &GitCommon{
Common: cmn, Common: cmn,
@ -37,6 +33,5 @@ func NewGitCommon(
repoPaths: repoPaths, repoPaths: repoPaths,
repo: repo, repo: repo,
config: config, config: config,
syncMutex: syncMutex,
} }
} }

View File

@ -53,7 +53,7 @@ func (self *RemoteCommands) DeleteRemoteBranch(task gocui.Task, remoteName strin
Arg(remoteName, "--delete", branchName). Arg(remoteName, "--delete", branchName).
ToArgv() ToArgv()
return self.cmd.New(cmdArgs).PromptOnCredentialRequest(task).WithMutex(self.syncMutex).Run() return self.cmd.New(cmdArgs).PromptOnCredentialRequest(task).Run()
} }
func (self *RemoteCommands) DeleteRemoteTag(task gocui.Task, remoteName string, tagName string) error { func (self *RemoteCommands) DeleteRemoteTag(task gocui.Task, remoteName string, tagName string) error {
@ -61,7 +61,7 @@ func (self *RemoteCommands) DeleteRemoteTag(task gocui.Task, remoteName string,
Arg(remoteName, "--delete", tagName). Arg(remoteName, "--delete", tagName).
ToArgv() ToArgv()
return self.cmd.New(cmdArgs).PromptOnCredentialRequest(task).WithMutex(self.syncMutex).Run() return self.cmd.New(cmdArgs).PromptOnCredentialRequest(task).Run()
} }
// CheckRemoteBranchExists Returns remote branch // CheckRemoteBranchExists Returns remote branch

View File

@ -36,7 +36,7 @@ func (self *SyncCommands) PushCmdObj(task gocui.Task, opts PushOpts) (oscommands
ArgIf(opts.UpstreamBranch != "", opts.UpstreamBranch). ArgIf(opts.UpstreamBranch != "", opts.UpstreamBranch).
ToArgv() ToArgv()
cmdObj := self.cmd.New(cmdArgs).PromptOnCredentialRequest(task).WithMutex(self.syncMutex) cmdObj := self.cmd.New(cmdArgs).PromptOnCredentialRequest(task)
return cmdObj, nil return cmdObj, nil
} }
@ -70,7 +70,6 @@ func (self *SyncCommands) FetchBackgroundCmdObj() oscommands.ICmdObj {
cmdObj := self.cmd.New(cmdArgs) cmdObj := self.cmd.New(cmdArgs)
cmdObj.DontLog().FailOnCredentialRequest() cmdObj.DontLog().FailOnCredentialRequest()
cmdObj.WithMutex(self.syncMutex)
return cmdObj return cmdObj
} }
@ -96,7 +95,7 @@ func (self *SyncCommands) Pull(task gocui.Task, opts PullOptions) error {
// setting GIT_SEQUENCE_EDITOR to ':' as a way of skipping it, in case the user // setting GIT_SEQUENCE_EDITOR to ':' as a way of skipping it, in case the user
// has 'pull.rebase = interactive' configured. // has 'pull.rebase = interactive' configured.
return self.cmd.New(cmdArgs).AddEnvVars("GIT_SEQUENCE_EDITOR=:").PromptOnCredentialRequest(task).WithMutex(self.syncMutex).Run() return self.cmd.New(cmdArgs).AddEnvVars("GIT_SEQUENCE_EDITOR=:").PromptOnCredentialRequest(task).Run()
} }
func (self *SyncCommands) FastForward( func (self *SyncCommands) FastForward(
@ -110,7 +109,7 @@ func (self *SyncCommands) FastForward(
Arg(remoteBranchName + ":" + branchName). Arg(remoteBranchName + ":" + branchName).
ToArgv() ToArgv()
return self.cmd.New(cmdArgs).PromptOnCredentialRequest(task).WithMutex(self.syncMutex).Run() return self.cmd.New(cmdArgs).PromptOnCredentialRequest(task).Run()
} }
func (self *SyncCommands) FetchRemote(task gocui.Task, remoteName string) error { func (self *SyncCommands) FetchRemote(task gocui.Task, remoteName string) error {
@ -118,5 +117,5 @@ func (self *SyncCommands) FetchRemote(task gocui.Task, remoteName string) error
Arg(remoteName). Arg(remoteName).
ToArgv() ToArgv()
return self.cmd.New(cmdArgs).PromptOnCredentialRequest(task).WithMutex(self.syncMutex).Run() return self.cmd.New(cmdArgs).PromptOnCredentialRequest(task).Run()
} }

View File

@ -52,5 +52,5 @@ func (self *TagCommands) Push(task gocui.Task, remoteName string, tagName string
cmdArgs := NewGitCmd("push").Arg(remoteName, "tag", tagName). cmdArgs := NewGitCmd("push").Arg(remoteName, "tag", tagName).
ToArgv() ToArgv()
return self.cmd.New(cmdArgs).PromptOnCredentialRequest(task).WithMutex(self.syncMutex).Run() return self.cmd.New(cmdArgs).PromptOnCredentialRequest(task).Run()
} }

View File

@ -65,6 +65,10 @@ func (b *Branch) ID() string {
return b.RefName() return b.RefName()
} }
func (b *Branch) URN() string {
return "branch-" + b.ID()
}
func (b *Branch) Description() string { func (b *Branch) Description() string {
return b.RefName() return b.RefName()
} }

View File

@ -24,6 +24,10 @@ func (t *Tag) ID() string {
return t.RefName() return t.RefName()
} }
func (t *Tag) URN() string {
return "tag-" + t.ID()
}
func (t *Tag) Description() string { func (t *Tag) Description() string {
return t.Message return t.Message
} }

View File

@ -27,6 +27,7 @@ func NewBranchesContext(c *ContextCommon) *BranchesContext {
getDisplayStrings := func(_ int, _ int) [][]string { getDisplayStrings := func(_ int, _ int) [][]string {
return presentation.GetBranchListDisplayStrings( return presentation.GetBranchListDisplayStrings(
viewModel.GetItems(), viewModel.GetItems(),
c.State().GetItemOperation,
c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL, c.State().GetRepoState().GetScreenMode() != types.SCREEN_NORMAL,
c.Modes().Diffing.Ref, c.Modes().Diffing.Ref,
c.Tr, c.Tr,

View File

@ -27,7 +27,10 @@ func NewTagsContext(
) )
getDisplayStrings := func(_ int, _ int) [][]string { getDisplayStrings := func(_ int, _ int) [][]string {
return presentation.GetTagListDisplayStrings(viewModel.GetItems(), c.Modes().Diffing.Ref) return presentation.GetTagListDisplayStrings(
viewModel.GetItems(),
c.State().GetItemOperation,
c.Modes().Diffing.Ref, c.Tr)
} }
return &TagsContext{ return &TagsContext{

View File

@ -112,6 +112,7 @@ func (gui *Gui) resetHelpersAndControllers() {
Confirmation: helpers.NewConfirmationHelper(helperCommon), Confirmation: helpers.NewConfirmationHelper(helperCommon),
Mode: modeHelper, Mode: modeHelper,
AppStatus: appStatusHelper, AppStatus: appStatusHelper,
InlineStatus: helpers.NewInlineStatusHelper(helperCommon),
WindowArrangement: helpers.NewWindowArrangementHelper( WindowArrangement: helpers.NewWindowArrangementHelper(
gui.c, gui.c,
windowHelper, windowHelper,

View File

@ -34,9 +34,10 @@ func NewBranchesController(
func (self *BranchesController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { func (self *BranchesController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
return []*types.Binding{ return []*types.Binding{
{ {
Key: opts.GetKey(opts.Config.Universal.Select), Key: opts.GetKey(opts.Config.Universal.Select),
Handler: self.checkSelected(self.press), Handler: self.checkSelected(self.press),
Description: self.c.Tr.Checkout, GetDisabledReason: self.getDisabledReasonForPress,
Description: self.c.Tr.Checkout,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.New), Key: opts.GetKey(opts.Config.Universal.New),
@ -299,6 +300,18 @@ func (self *BranchesController) press(selectedBranch *models.Branch) error {
return self.c.Helpers().Refs.CheckoutRef(selectedBranch.Name, types.CheckoutRefOptions{}) return self.c.Helpers().Refs.CheckoutRef(selectedBranch.Name, types.CheckoutRefOptions{})
} }
func (self *BranchesController) getDisabledReasonForPress() string {
currentBranch := self.c.Helpers().Refs.GetCheckedOutRef()
if currentBranch != nil {
op := self.c.State().GetItemOperation(currentBranch)
if op == types.ItemOperationFastForwarding || op == types.ItemOperationPulling {
return self.c.Tr.CantCheckoutBranchWhilePulling
}
}
return ""
}
func (self *BranchesController) worktreeForBranch(branch *models.Branch) (*models.Worktree, bool) { func (self *BranchesController) worktreeForBranch(branch *models.Branch) (*models.Worktree, bool) {
return git_commands.WorktreeForBranch(branch, self.c.Model().Worktrees) return git_commands.WorktreeForBranch(branch, self.c.Model().Worktrees)
} }
@ -563,14 +576,7 @@ func (self *BranchesController) fastForward(branch *models.Branch) error {
action := self.c.Tr.Actions.FastForwardBranch action := self.c.Tr.Actions.FastForwardBranch
message := utils.ResolvePlaceholderString( return self.c.WithInlineStatus(branch, types.ItemOperationFastForwarding, context.LOCAL_BRANCHES_CONTEXT_KEY, func(task gocui.Task) error {
self.c.Tr.FastForwarding,
map[string]string{
"branch": branch.Name,
},
)
return self.c.WithWaitingStatus(message, func(task gocui.Task) error {
worktree, ok := self.worktreeForBranch(branch) worktree, ok := self.worktreeForBranch(branch)
if ok { if ok {
self.c.LogAction(action) self.c.LogAction(action)
@ -590,24 +596,17 @@ func (self *BranchesController) fastForward(branch *models.Branch) error {
WorktreeGitDir: worktreeGitDir, WorktreeGitDir: worktreeGitDir,
}, },
) )
if err != nil { _ = self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
_ = self.c.Error(err) return err
}
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
} else { } else {
self.c.LogAction(action) self.c.LogAction(action)
err := self.c.Git().Sync.FastForward( err := self.c.Git().Sync.FastForward(
task, branch.Name, branch.UpstreamRemote, branch.UpstreamBranch, task, branch.Name, branch.UpstreamRemote, branch.UpstreamBranch,
) )
if err != nil {
_ = self.c.Error(err)
}
_ = self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.BRANCHES}}) _ = self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.BRANCHES}})
return err
} }
return nil
}) })
} }

View File

@ -46,6 +46,7 @@ type Helpers struct {
Confirmation *ConfirmationHelper Confirmation *ConfirmationHelper
Mode *ModeHelper Mode *ModeHelper
AppStatus *AppStatusHelper AppStatus *AppStatusHelper
InlineStatus *InlineStatusHelper
WindowArrangement *WindowArrangementHelper WindowArrangement *WindowArrangementHelper
Search *SearchHelper Search *SearchHelper
Worktree *WorktreeHelper Worktree *WorktreeHelper
@ -81,6 +82,7 @@ func NewStubHelpers() *Helpers {
Confirmation: &ConfirmationHelper{}, Confirmation: &ConfirmationHelper{},
Mode: &ModeHelper{}, Mode: &ModeHelper{},
AppStatus: &AppStatusHelper{}, AppStatus: &AppStatusHelper{},
InlineStatus: &InlineStatusHelper{},
WindowArrangement: &WindowArrangementHelper{}, WindowArrangement: &WindowArrangementHelper{},
Search: &SearchHelper{}, Search: &SearchHelper{},
Worktree: &WorktreeHelper{}, Worktree: &WorktreeHelper{},

View File

@ -0,0 +1,129 @@
package helpers
import (
"time"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sasha-s/go-deadlock"
)
type InlineStatusHelper struct {
c *HelperCommon
contextsWithInlineStatus map[types.ContextKey]*inlineStatusInfo
mutex *deadlock.Mutex
}
func NewInlineStatusHelper(c *HelperCommon) *InlineStatusHelper {
return &InlineStatusHelper{
c: c,
contextsWithInlineStatus: make(map[types.ContextKey]*inlineStatusInfo),
mutex: &deadlock.Mutex{},
}
}
type InlineStatusOpts struct {
Item types.HasUrn
Operation types.ItemOperation
ContextKey types.ContextKey
}
type inlineStatusInfo struct {
refCount int
stop chan struct{}
}
// A custom task for WithInlineStatus calls; it wraps the original one and
// hides the status whenever the task is paused, and shows it again when
// continued.
type inlineStatusHelperTask struct {
gocui.Task
inlineStatusHelper *InlineStatusHelper
opts InlineStatusOpts
}
// poor man's version of explicitly saying that struct X implements interface Y
var _ gocui.Task = inlineStatusHelperTask{}
func (self inlineStatusHelperTask) Pause() {
self.inlineStatusHelper.stop(self.opts)
self.Task.Pause()
self.inlineStatusHelper.renderContext(self.opts.ContextKey)
}
func (self inlineStatusHelperTask) Continue() {
self.Task.Continue()
self.inlineStatusHelper.start(self.opts)
}
func (self *InlineStatusHelper) WithInlineStatus(opts InlineStatusOpts, f func(gocui.Task) error) {
self.c.OnWorker(func(task gocui.Task) {
self.start(opts)
err := f(inlineStatusHelperTask{task, self, opts})
if err != nil {
self.c.OnUIThread(func() error {
return self.c.Error(err)
})
}
self.stop(opts)
})
}
func (self *InlineStatusHelper) start(opts InlineStatusOpts) {
self.c.State().SetItemOperation(opts.Item, opts.Operation)
self.mutex.Lock()
defer self.mutex.Unlock()
info := self.contextsWithInlineStatus[opts.ContextKey]
if info == nil {
info = &inlineStatusInfo{refCount: 0, stop: make(chan struct{})}
self.contextsWithInlineStatus[opts.ContextKey] = info
go utils.Safe(func() {
ticker := time.NewTicker(time.Millisecond * utils.LoaderAnimationInterval)
defer ticker.Stop()
outer:
for {
select {
case <-ticker.C:
self.renderContext(opts.ContextKey)
case <-info.stop:
break outer
}
}
})
}
info.refCount++
}
func (self *InlineStatusHelper) stop(opts InlineStatusOpts) {
self.mutex.Lock()
if info := self.contextsWithInlineStatus[opts.ContextKey]; info != nil {
info.refCount--
if info.refCount <= 0 {
info.stop <- struct{}{}
delete(self.contextsWithInlineStatus, opts.ContextKey)
}
}
self.mutex.Unlock()
self.c.State().ClearItemOperation(opts.Item)
}
func (self *InlineStatusHelper) renderContext(contextKey types.ContextKey) {
self.c.OnUIThread(func() error {
_ = self.c.ContextForKey(contextKey).HandleRender()
return nil
})
}

View File

@ -115,12 +115,15 @@ func (self *RefreshHelper) Refresh(options types.RefreshOptions) error {
} }
} }
includeWorktreesWithBranches := false
if scopeSet.Includes(types.COMMITS) || scopeSet.Includes(types.BRANCHES) || scopeSet.Includes(types.REFLOG) || scopeSet.Includes(types.BISECT_INFO) { if scopeSet.Includes(types.COMMITS) || scopeSet.Includes(types.BRANCHES) || scopeSet.Includes(types.REFLOG) || scopeSet.Includes(types.BISECT_INFO) {
// whenever we change commits, we should update branches because the upstream/downstream // whenever we change commits, we should update branches because the upstream/downstream
// counts can change. Whenever we change branches we should also change commits // counts can change. Whenever we change branches we should also change commits
// e.g. in the case of switching branches. // e.g. in the case of switching branches.
refresh("commits and commit files", self.refreshCommitsAndCommitFiles) refresh("commits and commit files", self.refreshCommitsAndCommitFiles)
refresh("reflog and branches", self.refreshReflogAndBranches)
includeWorktreesWithBranches = scopeSet.Includes(types.WORKTREES)
refresh("reflog and branches", func() { self.refreshReflogAndBranches(includeWorktreesWithBranches) })
} else if scopeSet.Includes(types.REBASE_COMMITS) { } else if scopeSet.Includes(types.REBASE_COMMITS) {
// the above block handles rebase commits so we only need to call this one // the above block handles rebase commits so we only need to call this one
// if we've asked specifically for rebase commits and not those other things // if we've asked specifically for rebase commits and not those other things
@ -157,7 +160,7 @@ func (self *RefreshHelper) Refresh(options types.RefreshOptions) error {
refresh("remotes", func() { _ = self.refreshRemotes() }) refresh("remotes", func() { _ = self.refreshRemotes() })
} }
if scopeSet.Includes(types.WORKTREES) { if scopeSet.Includes(types.WORKTREES) && !includeWorktreesWithBranches {
refresh("worktrees", func() { _ = self.refreshWorktrees() }) refresh("worktrees", func() { _ = self.refreshWorktrees() })
} }
@ -242,7 +245,7 @@ func (self *RefreshHelper) refreshReflogCommitsConsideringStartup() {
case types.INITIAL: case types.INITIAL:
self.c.OnWorker(func(_ gocui.Task) { self.c.OnWorker(func(_ gocui.Task) {
_ = self.refreshReflogCommits() _ = self.refreshReflogCommits()
self.refreshBranches() self.refreshBranches(false)
self.c.State().GetRepoState().SetStartupStage(types.COMPLETE) self.c.State().GetRepoState().SetStartupStage(types.COMPLETE)
}) })
@ -251,10 +254,10 @@ func (self *RefreshHelper) refreshReflogCommitsConsideringStartup() {
} }
} }
func (self *RefreshHelper) refreshReflogAndBranches() { func (self *RefreshHelper) refreshReflogAndBranches(refreshWorktrees bool) {
self.refreshReflogCommitsConsideringStartup() self.refreshReflogCommitsConsideringStartup()
self.refreshBranches() self.refreshBranches(refreshWorktrees)
} }
func (self *RefreshHelper) refreshCommitsAndCommitFiles() { func (self *RefreshHelper) refreshCommitsAndCommitFiles() {
@ -419,7 +422,7 @@ func (self *RefreshHelper) refreshStateSubmoduleConfigs() error {
// self.refreshStatus is called at the end of this because that's when we can // self.refreshStatus is called at the end of this because that's when we can
// be sure there is a State.Model.Branches array to pick the current branch from // be sure there is a State.Model.Branches array to pick the current branch from
func (self *RefreshHelper) refreshBranches() { func (self *RefreshHelper) refreshBranches(refreshWorktrees bool) {
self.c.Mutexes().RefreshingBranchesMutex.Lock() self.c.Mutexes().RefreshingBranchesMutex.Lock()
defer self.c.Mutexes().RefreshingBranchesMutex.Unlock() defer self.c.Mutexes().RefreshingBranchesMutex.Unlock()
@ -443,6 +446,13 @@ func (self *RefreshHelper) refreshBranches() {
self.c.Model().Branches = branches self.c.Model().Branches = branches
if refreshWorktrees {
self.loadWorktrees()
if err := self.c.PostRefreshUpdate(self.c.Contexts().Worktrees); err != nil {
self.c.Log.Error(err)
}
}
if err := self.c.PostRefreshUpdate(self.c.Contexts().Branches); err != nil { if err := self.c.PostRefreshUpdate(self.c.Contexts().Branches); err != nil {
self.c.Log.Error(err) self.c.Log.Error(err)
} }
@ -636,15 +646,18 @@ func (self *RefreshHelper) refreshRemotes() error {
return nil return nil
} }
func (self *RefreshHelper) refreshWorktrees() error { func (self *RefreshHelper) loadWorktrees() {
worktrees, err := self.c.Git().Loaders.Worktrees.GetWorktrees() worktrees, err := self.c.Git().Loaders.Worktrees.GetWorktrees()
if err != nil { if err != nil {
self.c.Log.Error(err) self.c.Log.Error(err)
self.c.Model().Worktrees = []*models.Worktree{} self.c.Model().Worktrees = []*models.Worktree{}
return nil
} }
self.c.Model().Worktrees = worktrees self.c.Model().Worktrees = worktrees
}
func (self *RefreshHelper) refreshWorktrees() error {
self.loadWorktrees()
// need to refresh branches because the branches view shows worktrees against // need to refresh branches because the branches view shows worktrees against
// branches // branches
@ -678,7 +691,7 @@ func (self *RefreshHelper) refreshStatus() {
repoName := self.c.Git().RepoPaths.RepoName() repoName := self.c.Git().RepoPaths.RepoName()
status := presentation.FormatStatus(repoName, currentBranch, linkedWorktreeName, workingTreeState, self.c.Tr) status := presentation.FormatStatus(repoName, currentBranch, types.ItemOperationNone, linkedWorktreeName, workingTreeState, self.c.Tr)
self.c.SetViewContent(self.c.Views().Status, status) self.c.SetViewContent(self.c.Views().Status, status)
} }

View File

@ -173,11 +173,6 @@ func (self *ReposHelper) DispatchSwitchTo(path string, errMsg string, contextKey
return err return err
} }
// these two mutexes are used by our background goroutines (triggered via `self.goEvery`. We don't want to
// switch to a repo while one of these goroutines is in the process of updating something
self.c.Mutexes().SyncMutex.Lock()
defer self.c.Mutexes().SyncMutex.Unlock()
self.c.Mutexes().RefreshingFilesMutex.Lock() self.c.Mutexes().RefreshingFilesMutex.Lock()
defer self.c.Mutexes().RefreshingFilesMutex.Unlock() defer self.c.Mutexes().RefreshingFilesMutex.Unlock()

View File

@ -106,7 +106,7 @@ func (self *StatusController) onClick() error {
} }
cx, _ := self.c.Views().Status.Cursor() cx, _ := self.c.Views().Status.Cursor()
upstreamStatus := presentation.BranchStatus(currentBranch, self.c.Tr) upstreamStatus := presentation.BranchStatus(currentBranch, types.ItemOperationNone, self.c.Tr)
repoName := self.c.Git().RepoPaths.RepoName() repoName := self.c.Git().RepoPaths.RepoName()
workingTreeState := self.c.Git().Status.WorkingTreeState() workingTreeState := self.c.Git().Status.WorkingTreeState()
switch workingTreeState { switch workingTreeState {

View File

@ -7,6 +7,7 @@ import (
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/models" "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/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
) )
@ -30,14 +31,16 @@ func NewSyncController(
func (self *SyncController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding { func (self *SyncController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
bindings := []*types.Binding{ bindings := []*types.Binding{
{ {
Key: opts.GetKey(opts.Config.Universal.Push), Key: opts.GetKey(opts.Config.Universal.Push),
Handler: opts.Guards.NoPopupPanel(self.HandlePush), Handler: opts.Guards.NoPopupPanel(self.HandlePush),
Description: self.c.Tr.Push, GetDisabledReason: self.getDisabledReasonForPushOrPull,
Description: self.c.Tr.Push,
}, },
{ {
Key: opts.GetKey(opts.Config.Universal.Pull), Key: opts.GetKey(opts.Config.Universal.Pull),
Handler: opts.Guards.NoPopupPanel(self.HandlePull), Handler: opts.Guards.NoPopupPanel(self.HandlePull),
Description: self.c.Tr.Pull, GetDisabledReason: self.getDisabledReasonForPushOrPull,
Description: self.c.Tr.Pull,
}, },
} }
@ -56,6 +59,18 @@ func (self *SyncController) HandlePull() error {
return self.branchCheckedOut(self.pull)() return self.branchCheckedOut(self.pull)()
} }
func (self *SyncController) getDisabledReasonForPushOrPull() string {
currentBranch := self.c.Helpers().Refs.GetCheckedOutRef()
if currentBranch != nil {
op := self.c.State().GetItemOperation(currentBranch)
if op != types.ItemOperationNone {
return self.c.Tr.CantPullOrPushSameBranchTwice
}
}
return ""
}
func (self *SyncController) branchCheckedOut(f func(*models.Branch) error) func() error { func (self *SyncController) branchCheckedOut(f func(*models.Branch) error) func() error {
return func() error { return func() error {
currentBranch := self.c.Helpers().Refs.GetCheckedOutRef() currentBranch := self.c.Helpers().Refs.GetCheckedOutRef()
@ -73,13 +88,13 @@ func (self *SyncController) push(currentBranch *models.Branch) error {
if currentBranch.IsTrackingRemote() { if currentBranch.IsTrackingRemote() {
opts := pushOpts{} opts := pushOpts{}
if currentBranch.HasCommitsToPull() { if currentBranch.HasCommitsToPull() {
return self.requestToForcePush(opts) return self.requestToForcePush(currentBranch, opts)
} else { } else {
return self.pushAux(opts) return self.pushAux(currentBranch, opts)
} }
} else { } else {
if self.c.Git().Config.GetPushToCurrent() { if self.c.Git().Config.GetPushToCurrent() {
return self.pushAux(pushOpts{setUpstream: true}) return self.pushAux(currentBranch, pushOpts{setUpstream: true})
} else { } else {
return self.c.Helpers().Upstream.PromptForUpstreamWithInitialContent(currentBranch, func(upstream string) error { return self.c.Helpers().Upstream.PromptForUpstreamWithInitialContent(currentBranch, func(upstream string) error {
upstreamRemote, upstreamBranch, err := self.c.Helpers().Upstream.ParseUpstream(upstream) upstreamRemote, upstreamBranch, err := self.c.Helpers().Upstream.ParseUpstream(upstream)
@ -87,7 +102,7 @@ func (self *SyncController) push(currentBranch *models.Branch) error {
return self.c.Error(err) return self.c.Error(err)
} }
return self.pushAux(pushOpts{ return self.pushAux(currentBranch, pushOpts{
setUpstream: true, setUpstream: true,
upstreamRemote: upstreamRemote, upstreamRemote: upstreamRemote,
upstreamBranch: upstreamBranch, upstreamBranch: upstreamBranch,
@ -107,11 +122,11 @@ func (self *SyncController) pull(currentBranch *models.Branch) error {
return self.c.Error(err) 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 { func (self *SyncController) setCurrentBranchUpstream(upstream string) error {
@ -139,8 +154,8 @@ type PullFilesOptions struct {
Action string 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 { return self.c.WithInlineStatus(currentBranch, types.ItemOperationPulling, context.LOCAL_BRANCHES_CONTEXT_KEY, func(task gocui.Task) error {
return self.pullWithLock(task, opts) return self.pullWithLock(task, opts)
}) })
} }
@ -167,8 +182,8 @@ type pushOpts struct {
setUpstream bool 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 { return self.c.WithInlineStatus(currentBranch, types.ItemOperationPushing, context.LOCAL_BRANCHES_CONTEXT_KEY, func(task gocui.Task) error {
self.c.LogAction(self.c.Tr.Actions.Push) self.c.LogAction(self.c.Tr.Actions.Push)
err := self.c.Git().Sync.Push( err := self.c.Git().Sync.Push(
task, task,
@ -192,18 +207,18 @@ func (self *SyncController) pushAux(opts pushOpts) error {
newOpts := opts newOpts := opts
newOpts.force = true newOpts.force = true
return self.pushAux(newOpts) return self.pushAux(currentBranch, newOpts)
}, },
}) })
return nil return nil
} }
_ = self.c.Error(err) return err
} }
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
}) })
} }
func (self *SyncController) requestToForcePush(opts pushOpts) error { func (self *SyncController) requestToForcePush(currentBranch *models.Branch, opts pushOpts) error {
forcePushDisabled := self.c.UserConfig.Git.DisableForcePushing forcePushDisabled := self.c.UserConfig.Git.DisableForcePushing
if forcePushDisabled { if forcePushDisabled {
return self.c.ErrorMsg(self.c.Tr.ForcePushDisabled) return self.c.ErrorMsg(self.c.Tr.ForcePushDisabled)
@ -214,7 +229,7 @@ func (self *SyncController) requestToForcePush(opts pushOpts) error {
Prompt: self.forcePushPrompt(), Prompt: self.forcePushPrompt(),
HandleConfirm: func() error { HandleConfirm: func() error {
opts.force = true opts.force = true
return self.pushAux(opts) return self.pushAux(currentBranch, opts)
}, },
}) })
} }

View File

@ -92,10 +92,9 @@ func (self *TagsController) checkout(tag *models.Tag) error {
func (self *TagsController) localDelete(tag *models.Tag) error { func (self *TagsController) localDelete(tag *models.Tag) error {
return self.c.WithWaitingStatus(self.c.Tr.DeletingStatus, func(gocui.Task) error { return self.c.WithWaitingStatus(self.c.Tr.DeletingStatus, func(gocui.Task) error {
self.c.LogAction(self.c.Tr.Actions.DeleteLocalTag) self.c.LogAction(self.c.Tr.Actions.DeleteLocalTag)
if err := self.c.Git().Tag.LocalDelete(tag.Name); err != nil { err := self.c.Git().Tag.LocalDelete(tag.Name)
return self.c.Error(err) _ = self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.COMMITS, types.TAGS}})
} return err
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.COMMITS, types.TAGS}})
}) })
} }
@ -130,10 +129,10 @@ func (self *TagsController) remoteDelete(tag *models.Tag) error {
Title: confirmTitle, Title: confirmTitle,
Prompt: confirmPrompt, Prompt: confirmPrompt,
HandleConfirm: func() error { HandleConfirm: func() error {
return self.c.WithWaitingStatus(self.c.Tr.DeletingStatus, func(t gocui.Task) error { return self.c.WithInlineStatus(tag, types.ItemOperationDeleting, context.TAGS_CONTEXT_KEY, func(task gocui.Task) error {
self.c.LogAction(self.c.Tr.Actions.DeleteRemoteTag) self.c.LogAction(self.c.Tr.Actions.DeleteRemoteTag)
if err := self.c.Git().Remote.DeleteRemoteTag(t, upstream, tag.Name); err != nil { if err := self.c.Git().Remote.DeleteRemoteTag(task, upstream, tag.Name); err != nil {
return self.c.Error(err) return err
} }
self.c.Toast(self.c.Tr.RemoteTagDeletedMessage) self.c.Toast(self.c.Tr.RemoteTagDeletedMessage)
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.COMMITS, types.TAGS}}) return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.COMMITS, types.TAGS}})
@ -189,14 +188,17 @@ func (self *TagsController) push(tag *models.Tag) error {
InitialContent: "origin", InitialContent: "origin",
FindSuggestionsFunc: self.c.Helpers().Suggestions.GetRemoteSuggestionsFunc(), FindSuggestionsFunc: self.c.Helpers().Suggestions.GetRemoteSuggestionsFunc(),
HandleConfirm: func(response string) error { HandleConfirm: func(response string) error {
return self.c.WithWaitingStatus(self.c.Tr.PushingTagStatus, func(task gocui.Task) error { return self.c.WithInlineStatus(tag, types.ItemOperationPushing, context.TAGS_CONTEXT_KEY, func(task gocui.Task) error {
self.c.LogAction(self.c.Tr.Actions.PushTag) self.c.LogAction(self.c.Tr.Actions.PushTag)
err := self.c.Git().Tag.Push(task, response, tag.Name) err := self.c.Git().Tag.Push(task, response, tag.Name)
if err != nil {
_ = self.c.Error(err)
}
return nil // Render again to remove the inline status:
self.c.OnUIThread(func() error {
_ = self.c.Contexts().Tags.HandleRender()
return nil
})
return err
}) })
}, },
}) })

View File

@ -110,6 +110,12 @@ type Gui struct {
// lazygit was opened in, or if we'll retain the one we're currently in. // lazygit was opened in, or if we'll retain the one we're currently in.
RetainOriginalDir bool RetainOriginalDir bool
// stores long-running operations associated with items (e.g. when a branch
// is being pushed). At the moment the rule is to use an item operation when
// we need to talk to the remote.
itemOperations map[string]types.ItemOperation
itemOperationsMutex *deadlock.Mutex
PrevLayout PrevLayout PrevLayout PrevLayout
// this is the initial dir we are in upon opening lazygit. We hold onto this // this is the initial dir we are in upon opening lazygit. We hold onto this
@ -180,6 +186,27 @@ func (self *StateAccessor) SetRetainOriginalDir(value bool) {
self.gui.RetainOriginalDir = value self.gui.RetainOriginalDir = value
} }
func (self *StateAccessor) GetItemOperation(item types.HasUrn) types.ItemOperation {
self.gui.itemOperationsMutex.Lock()
defer self.gui.itemOperationsMutex.Unlock()
return self.gui.itemOperations[item.URN()]
}
func (self *StateAccessor) SetItemOperation(item types.HasUrn, operation types.ItemOperation) {
self.gui.itemOperationsMutex.Lock()
defer self.gui.itemOperationsMutex.Unlock()
self.gui.itemOperations[item.URN()] = operation
}
func (self *StateAccessor) ClearItemOperation(item types.HasUrn) {
self.gui.itemOperationsMutex.Lock()
defer self.gui.itemOperationsMutex.Unlock()
delete(self.gui.itemOperations, item.URN())
}
// we keep track of some stuff from one render to the next to see if certain // we keep track of some stuff from one render to the next to see if certain
// things have changed // things have changed
type PrevLayout struct { type PrevLayout struct {
@ -273,7 +300,6 @@ func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, contextKey types.Context
gui.gitVersion, gui.gitVersion,
gui.os, gui.os,
git_config.NewStdCachedGitConfig(gui.Log), git_config.NewStdCachedGitConfig(gui.Log),
gui.Mutexes.SyncMutex,
) )
if err != nil { if err != nil {
return err return err
@ -463,7 +489,6 @@ func NewGui(
RefreshingFilesMutex: &deadlock.Mutex{}, RefreshingFilesMutex: &deadlock.Mutex{},
RefreshingBranchesMutex: &deadlock.Mutex{}, RefreshingBranchesMutex: &deadlock.Mutex{},
RefreshingStatusMutex: &deadlock.Mutex{}, RefreshingStatusMutex: &deadlock.Mutex{},
SyncMutex: &deadlock.Mutex{},
LocalCommitsMutex: &deadlock.Mutex{}, LocalCommitsMutex: &deadlock.Mutex{},
SubCommitsMutex: &deadlock.Mutex{}, SubCommitsMutex: &deadlock.Mutex{},
AuthorsMutex: &deadlock.Mutex{}, AuthorsMutex: &deadlock.Mutex{},
@ -473,6 +498,9 @@ func NewGui(
}, },
InitialDir: initialDir, InitialDir: initialDir,
afterLayoutFuncs: make(chan func() error, 1000), afterLayoutFuncs: make(chan func() error, 1000),
itemOperations: make(map[string]types.ItemOperation),
itemOperationsMutex: &deadlock.Mutex{},
} }
gui.PopupHandler = popup.NewPopupHandler( gui.PopupHandler = popup.NewPopupHandler(

View File

@ -5,6 +5,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/controllers/helpers"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
) )
@ -199,3 +200,8 @@ func (self *guiCommon) RunningIntegrationTest() bool {
func (self *guiCommon) InDemo() bool { func (self *guiCommon) InDemo() bool {
return self.gui.integrationTest != nil && self.gui.integrationTest.IsDemo() return self.gui.integrationTest != nil && self.gui.integrationTest.IsDemo()
} }
func (self *guiCommon) WithInlineStatus(item types.HasUrn, operation types.ItemOperation, contextKey types.ContextKey, f func(gocui.Task) error) error {
self.gui.helpers.InlineStatus.WithInlineStatus(helpers.InlineStatusOpts{Item: item, Operation: operation, ContextKey: contextKey}, f)
return nil
}

View File

@ -9,6 +9,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/presentation/icons" "github.com/jesseduffield/lazygit/pkg/gui/presentation/icons"
"github.com/jesseduffield/lazygit/pkg/gui/style" "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/i18n"
"github.com/jesseduffield/lazygit/pkg/theme" "github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
@ -19,6 +20,7 @@ var branchPrefixColorCache = make(map[string]style.TextStyle)
func GetBranchListDisplayStrings( func GetBranchListDisplayStrings(
branches []*models.Branch, branches []*models.Branch,
getItemOperation func(item types.HasUrn) types.ItemOperation,
fullDescription bool, fullDescription bool,
diffName string, diffName string,
tr *i18n.TranslationSet, tr *i18n.TranslationSet,
@ -27,13 +29,14 @@ func GetBranchListDisplayStrings(
) [][]string { ) [][]string {
return lo.Map(branches, func(branch *models.Branch, _ int) []string { return lo.Map(branches, func(branch *models.Branch, _ int) []string {
diffed := branch.Name == diffName diffed := branch.Name == diffName
return getBranchDisplayStrings(branch, fullDescription, diffed, tr, userConfig, worktrees) return getBranchDisplayStrings(branch, getItemOperation(branch), fullDescription, diffed, tr, userConfig, worktrees)
}) })
} }
// getBranchDisplayStrings returns the display string of branch // getBranchDisplayStrings returns the display string of branch
func getBranchDisplayStrings( func getBranchDisplayStrings(
b *models.Branch, b *models.Branch,
itemOperation types.ItemOperation,
fullDescription bool, fullDescription bool,
diffed bool, diffed bool,
tr *i18n.TranslationSet, tr *i18n.TranslationSet,
@ -51,7 +54,7 @@ func getBranchDisplayStrings(
} }
coloredName := nameTextStyle.Sprint(displayName) coloredName := nameTextStyle.Sprint(displayName)
branchStatus := utils.WithPadding(ColoredBranchStatus(b, tr), 2, utils.AlignLeft) branchStatus := utils.WithPadding(ColoredBranchStatus(b, itemOperation, tr), 2, utils.AlignLeft)
if git_commands.CheckedOutByOtherWorktree(b, worktrees) { if git_commands.CheckedOutByOtherWorktree(b, worktrees) {
worktreeIcon := lo.Ternary(icons.IsIconEnabled(), icons.LINKED_WORKTREE_ICON, fmt.Sprintf("(%s)", tr.LcWorktree)) worktreeIcon := lo.Ternary(icons.IsIconEnabled(), icons.LINKED_WORKTREE_ICON, fmt.Sprintf("(%s)", tr.LcWorktree))
coloredName = fmt.Sprintf("%s %s", coloredName, style.FgDefault.Sprint(worktreeIcon)) 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, itemOperation types.ItemOperation, tr *i18n.TranslationSet) string {
colour := style.FgYellow colour := style.FgYellow
if branch.UpstreamGone { if itemOperation != types.ItemOperationNone {
colour = style.FgCyan
} else if branch.UpstreamGone {
colour = style.FgRed colour = style.FgRed
} else if branch.MatchesUpstream() { } else if branch.MatchesUpstream() {
colour = style.FgGreen colour = style.FgGreen
@ -119,10 +124,15 @@ func ColoredBranchStatus(branch *models.Branch, tr *i18n.TranslationSet) string
colour = style.FgMagenta colour = style.FgMagenta
} }
return colour.Sprint(BranchStatus(branch, tr)) return colour.Sprint(BranchStatus(branch, itemOperation, tr))
} }
func BranchStatus(branch *models.Branch, tr *i18n.TranslationSet) string { func BranchStatus(branch *models.Branch, itemOperation types.ItemOperation, tr *i18n.TranslationSet) string {
itemOperationStr := itemOperationToString(itemOperation, tr)
if itemOperationStr != "" {
return itemOperationStr + " " + utils.Loader()
}
if !branch.IsTrackingRemote() { if !branch.IsTrackingRemote() {
return "" return ""
} }

View File

@ -0,0 +1,23 @@
package presentation
import (
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/i18n"
)
func itemOperationToString(itemOperation types.ItemOperation, tr *i18n.TranslationSet) string {
switch itemOperation {
case types.ItemOperationNone:
return ""
case types.ItemOperationPushing:
return tr.PushingStatus
case types.ItemOperationPulling:
return tr.PullingStatus
case types.ItemOperationFastForwarding:
return tr.FastForwarding
case types.ItemOperationDeleting:
return tr.DeletingStatus
}
return ""
}

View File

@ -7,14 +7,15 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/types/enums" "github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/gui/presentation/icons" "github.com/jesseduffield/lazygit/pkg/gui/presentation/icons"
"github.com/jesseduffield/lazygit/pkg/gui/style" "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/i18n"
) )
func FormatStatus(repoName string, currentBranch *models.Branch, linkedWorktreeName string, workingTreeState enums.RebaseMode, tr *i18n.TranslationSet) string { func FormatStatus(repoName string, currentBranch *models.Branch, itemOperation types.ItemOperation, linkedWorktreeName string, workingTreeState enums.RebaseMode, tr *i18n.TranslationSet) string {
status := "" status := ""
if currentBranch.IsRealBranch() { if currentBranch.IsRealBranch() {
status += ColoredBranchStatus(currentBranch, tr) + " " status += ColoredBranchStatus(currentBranch, itemOperation, tr) + " "
} }
if workingTreeState != enums.REBASE_MODE_NONE { if workingTreeState != enums.REBASE_MODE_NONE {

View File

@ -4,19 +4,27 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/presentation/icons" "github.com/jesseduffield/lazygit/pkg/gui/presentation/icons"
"github.com/jesseduffield/lazygit/pkg/gui/style" "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/theme"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo" "github.com/samber/lo"
) )
func GetTagListDisplayStrings(tags []*models.Tag, diffName string) [][]string { func GetTagListDisplayStrings(
tags []*models.Tag,
getItemOperation func(item types.HasUrn) types.ItemOperation,
diffName string,
tr *i18n.TranslationSet,
) [][]string {
return lo.Map(tags, func(tag *models.Tag, _ int) []string { return lo.Map(tags, func(tag *models.Tag, _ int) []string {
diffed := tag.Name == diffName diffed := tag.Name == diffName
return getTagDisplayStrings(tag, diffed) return getTagDisplayStrings(tag, getItemOperation(tag), diffed, tr)
}) })
} }
// getTagDisplayStrings returns the display string of branch // getTagDisplayStrings returns the display string of branch
func getTagDisplayStrings(t *models.Tag, diffed bool) []string { func getTagDisplayStrings(t *models.Tag, itemOperation types.ItemOperation, diffed bool, tr *i18n.TranslationSet) []string {
textStyle := theme.DefaultTextColor textStyle := theme.DefaultTextColor
if diffed { if diffed {
textStyle = theme.DiffTerminalColor textStyle = theme.DiffTerminalColor
@ -26,6 +34,11 @@ func getTagDisplayStrings(t *models.Tag, diffed bool) []string {
res = append(res, textStyle.Sprint(icons.IconForTag(t))) res = append(res, textStyle.Sprint(icons.IconForTag(t)))
} }
descriptionColor := style.FgYellow descriptionColor := style.FgYellow
res = append(res, textStyle.Sprint(t.Name), descriptionColor.Sprint(t.Description())) descriptionStr := descriptionColor.Sprint(t.Description())
itemOperationStr := itemOperationToString(itemOperation, tr)
if itemOperationStr != "" {
descriptionStr = style.FgCyan.Sprint(itemOperationStr+" "+utils.Loader()) + " " + descriptionStr
}
res = append(res, textStyle.Sprint(t.Name), descriptionStr)
return res return res
} }

View File

@ -87,6 +87,12 @@ type IGuiCommon interface {
// resized, if in accordion mode. // resized, if in accordion mode.
AfterLayout(f func() error) AfterLayout(f func() error)
// Wraps a function, attaching the given operation to the given item while
// the function is executing, and also causes the given context to be
// redrawn periodically. This allows the operation to be visualized with a
// spinning loader animation (e.g. when a branch is being pushed).
WithInlineStatus(item HasUrn, operation ItemOperation, contextKey ContextKey, f func(gocui.Task) error) error
// returns the gocui Gui struct. There is a good chance you don't actually want to use // returns the gocui Gui struct. There is a good chance you don't actually want to use
// this struct and instead want to use another method above // this struct and instead want to use another method above
GocuiGui() *gocui.Gui GocuiGui() *gocui.Gui
@ -256,7 +262,6 @@ type Mutexes struct {
RefreshingFilesMutex *deadlock.Mutex RefreshingFilesMutex *deadlock.Mutex
RefreshingBranchesMutex *deadlock.Mutex RefreshingBranchesMutex *deadlock.Mutex
RefreshingStatusMutex *deadlock.Mutex RefreshingStatusMutex *deadlock.Mutex
SyncMutex *deadlock.Mutex
LocalCommitsMutex *deadlock.Mutex LocalCommitsMutex *deadlock.Mutex
SubCommitsMutex *deadlock.Mutex SubCommitsMutex *deadlock.Mutex
AuthorsMutex *deadlock.Mutex AuthorsMutex *deadlock.Mutex
@ -265,6 +270,24 @@ type Mutexes struct {
PtyMutex *deadlock.Mutex PtyMutex *deadlock.Mutex
} }
// A long-running operation associated with an item. For example, we'll show
// that a branch is being pushed from so that there's visual feedback about
// what's happening and so that you can see multiple branches' concurrent
// operations
type ItemOperation int
const (
ItemOperationNone ItemOperation = iota
ItemOperationPushing
ItemOperationPulling
ItemOperationFastForwarding
ItemOperationDeleting
)
type HasUrn interface {
URN() string
}
type IStateAccessor interface { type IStateAccessor interface {
GetRepoPathStack() *utils.StringStack GetRepoPathStack() *utils.StringStack
GetRepoState() IRepoStateAccessor GetRepoState() IRepoStateAccessor
@ -277,6 +300,9 @@ type IStateAccessor interface {
SetShowExtrasWindow(bool) SetShowExtrasWindow(bool)
GetRetainOriginalDir() bool GetRetainOriginalDir() bool
SetRetainOriginalDir(bool) SetRetainOriginalDir(bool)
GetItemOperation(item HasUrn) ItemOperation
SetItemOperation(item HasUrn, operation ItemOperation)
ClearItemOperation(item HasUrn)
} }
type IRepoStateAccessor interface { type IRepoStateAccessor interface {

View File

@ -169,7 +169,7 @@ func chineseTranslationSet() TranslationSet {
ToggleStagingPanel: `切换到其他面板`, ToggleStagingPanel: `切换到其他面板`,
ReturnToFilesPanel: `返回文件面板`, ReturnToFilesPanel: `返回文件面板`,
FastForward: `从上游快进此分支`, FastForward: `从上游快进此分支`,
FastForwarding: "抓取并快进 {{.branch}} ...", FastForwarding: "抓取并快进",
FoundConflictsTitle: "自动合并失败", FoundConflictsTitle: "自动合并失败",
ViewMergeRebaseOptions: "查看 合并/变基 选项", ViewMergeRebaseOptions: "查看 合并/变基 选项",
NotMergingOrRebasing: "您目前既不进行变基也不进行合并", NotMergingOrRebasing: "您目前既不进行变基也不进行合并",

View File

@ -134,7 +134,7 @@ func dutchTranslationSet() TranslationSet {
ToggleStagingPanel: `Ga naar een ander paneel`, ToggleStagingPanel: `Ga naar een ander paneel`,
ReturnToFilesPanel: `Ga terug naar het bestanden paneel`, ReturnToFilesPanel: `Ga terug naar het bestanden paneel`,
FastForward: `Fast-forward deze branch vanaf zijn upstream`, FastForward: `Fast-forward deze branch vanaf zijn upstream`,
FastForwarding: "Fast-forwarding {{.branch}} ...", FastForwarding: "Fast-forwarding",
FoundConflictsTitle: "Conflicten!", FoundConflictsTitle: "Conflicten!",
ViewMergeRebaseOptions: "Bekijk merge/rebase opties", ViewMergeRebaseOptions: "Bekijk merge/rebase opties",
NotMergingOrRebasing: "Je bent momenteel niet aan het rebasen of mergen", NotMergingOrRebasing: "Je bent momenteel niet aan het rebasen of mergen",

View File

@ -57,6 +57,8 @@ type TranslationSet struct {
ResetFilter string ResetFilter string
MergeConflictsTitle string MergeConflictsTitle string
Checkout string Checkout string
CantCheckoutBranchWhilePulling string
CantPullOrPushSameBranchTwice string
NoChangedFiles string NoChangedFiles string
SoftReset string SoftReset string
AlreadyCheckedOutBranch string AlreadyCheckedOutBranch string
@ -846,6 +848,8 @@ func EnglishTranslationSet() TranslationSet {
Scroll: "Scroll", Scroll: "Scroll",
MergeConflictsTitle: "Merge conflicts", MergeConflictsTitle: "Merge conflicts",
Checkout: "Checkout", Checkout: "Checkout",
CantCheckoutBranchWhilePulling: "You cannot checkout another branch while pulling the current branch",
CantPullOrPushSameBranchTwice: "You cannot push or pull a branch while it is already being pushed or pulled",
FileFilter: "Filter files by status", FileFilter: "Filter files by status",
FilterStagedFiles: "Show only staged files", FilterStagedFiles: "Show only staged files",
FilterUnstagedFiles: "Show only unstaged files", FilterUnstagedFiles: "Show only unstaged files",
@ -980,7 +984,7 @@ func EnglishTranslationSet() TranslationSet {
ToggleStagingPanel: `Switch to other panel (staged/unstaged changes)`, ToggleStagingPanel: `Switch to other panel (staged/unstaged changes)`,
ReturnToFilesPanel: `Return to files panel`, ReturnToFilesPanel: `Return to files panel`,
FastForward: `Fast-forward this branch from its upstream`, FastForward: `Fast-forward this branch from its upstream`,
FastForwarding: "Fast-forwarding {{.branch}}", FastForwarding: "Fast-forwarding",
FoundConflictsTitle: "Conflicts!", FoundConflictsTitle: "Conflicts!",
ViewConflictsMenuItem: "View conflicts", ViewConflictsMenuItem: "View conflicts",
AbortMenuItem: "Abort the %s", AbortMenuItem: "Abort the %s",

View File

@ -170,7 +170,7 @@ func koreanTranslationSet() TranslationSet {
ToggleStagingPanel: `패널 전환`, ToggleStagingPanel: `패널 전환`,
ReturnToFilesPanel: `파일 목록으로 돌아가기`, ReturnToFilesPanel: `파일 목록으로 돌아가기`,
FastForward: `Fast-forward this branch from its upstream`, FastForward: `Fast-forward this branch from its upstream`,
FastForwarding: "Fast-forwarding {{.branch}} ...", FastForwarding: "Fast-forwarding",
FoundConflictsTitle: "Auto-merge failed", FoundConflictsTitle: "Auto-merge failed",
ViewMergeRebaseOptions: "View merge/rebase options", ViewMergeRebaseOptions: "View merge/rebase options",
NotMergingOrRebasing: "You are currently neither rebasing nor merging", NotMergingOrRebasing: "You are currently neither rebasing nor merging",

View File

@ -201,7 +201,7 @@ func RussianTranslationSet() TranslationSet {
ToggleStagingPanel: `Переключиться на другую панель (проиндексированные/непроиндексированные изменения)`, ToggleStagingPanel: `Переключиться на другую панель (проиндексированные/непроиндексированные изменения)`,
ReturnToFilesPanel: `Вернуться к панели файлов`, ReturnToFilesPanel: `Вернуться к панели файлов`,
FastForward: `Перемотать эту ветку вперёд из её upstream-ветки`, FastForward: `Перемотать эту ветку вперёд из её upstream-ветки`,
FastForwarding: "Получить изменения и перемотать вперёд {{.branch}} ...", FastForwarding: "Получить изменения и перемотать вперёд",
FoundConflictsTitle: "Конфликты!", FoundConflictsTitle: "Конфликты!",
ViewConflictsMenuItem: "Просмотр конфликтов", ViewConflictsMenuItem: "Просмотр конфликтов",
AbortMenuItem: "Прервать %s", AbortMenuItem: "Прервать %s",

View File

@ -234,7 +234,7 @@ func traditionalChineseTranslationSet() TranslationSet {
ToggleStagingPanel: `切換至另一個面板 (已預存/未預存更改)`, ToggleStagingPanel: `切換至另一個面板 (已預存/未預存更改)`,
ReturnToFilesPanel: `返回檔案面板`, ReturnToFilesPanel: `返回檔案面板`,
FastForward: `從上游快進此分支`, FastForward: `從上游快進此分支`,
FastForwarding: "{{.branch}} 的擷取和快進中...", FastForwarding: "的擷取和快進中",
FoundConflictsTitle: "自動合併失敗", FoundConflictsTitle: "自動合併失敗",
ViewMergeRebaseOptions: "查看合併/變基選項", ViewMergeRebaseOptions: "查看合併/變基選項",
NotMergingOrRebasing: "你當前既不在變基也不在合併中", NotMergingOrRebasing: "你當前既不在變基也不在合併中",