Show a spinner for more long-running operations (#5765)
Some checks are pending
Continuous Integration / Integration Tests - git ${{matrix.git-version}} (2.32.0) (push) Waiting to run
Continuous Integration / ci - ${{matrix.os}} (~/.cache/go-build, ubuntu-latest) (push) Waiting to run
Continuous Integration / ci - ${{matrix.os}} (~\AppData\Local\go-build, windows-latest) (push) Waiting to run
Continuous Integration / Integration Tests - git ${{matrix.git-version}} (2.38.2) (push) Waiting to run
Continuous Integration / Integration Tests - git ${{matrix.git-version}} (2.44.0) (push) Waiting to run
Continuous Integration / Integration Tests - git ${{matrix.git-version}} (latest) (push) Waiting to run
Continuous Integration / build (push) Waiting to run
Continuous Integration / check-codebase (push) Waiting to run
Continuous Integration / lint (push) Waiting to run
Continuous Integration / upload-coverage (push) Blocked by required conditions
Continuous Integration / check-for-fixups (push) Waiting to run
Codespell / Check for spelling errors (push) Waiting to run
Generate Sponsors README / deploy (push) Waiting to run

Some operations used to freeze lazygit while they ran, with no sign that
anything was happening — the UI would just lock up until they finished.

This affected:

- Merging a branch (including squash merges)
- Rebasing a branch interactively onto another ref
- Setting a commit to "edit"
- Resetting to a commit, branch, or tag
- Continuing, skipping, or aborting a merge or rebase

Many of these were usually fast under normal conditions (e.g. a hard
reset when the head doesn't change, or a "rebase --continue" when
there's only a handful of commits left), but in some cases they could
take long (e.g. a hard reset to some distant commit where lots of files
changed). Now each of these shows a spinner while it works and keeps the
UI responsive, matching how similar operations already behave.
This commit is contained in:
Stefan Haller 2026-07-06 12:36:47 +02:00 committed by GitHub
commit 3537f855b0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 79 additions and 43 deletions

View file

@ -79,6 +79,18 @@ func (self *MergeAndRebaseHelper) ContinueRebase() error {
}
func (self *MergeAndRebaseHelper) genericMergeCommand(command string) error {
return self.genericMergeCommandImpl(command, true)
}
// genericMergeCommandImpl runs a merge/rebase continue/skip/abort and handles
// the result. Continuing can be slow (it may replay many commits), so the
// non-subprocess path runs on a worker with a waiting status.
//
// showWaitingStatus is false only for the recursive auto-skip in
// CheckMergeOrRebaseWithRefreshOptions: that call already runs on the caller's
// thread (the worker of the enclosing waiting status, or the UI thread for the
// synchronous callers), so it must not spin up a second one.
func (self *MergeAndRebaseHelper) genericMergeCommandImpl(command string, showWaitingStatus bool) error {
status := self.c.Git().Status.WorkingTreeState()
if status.None() {
@ -123,12 +135,22 @@ func (self *MergeAndRebaseHelper) genericMergeCommand(command string) error {
self.RecordWhetherMergeOrRebaseStartedInLazygit()
return err
}
result := self.c.Git().Rebase.GenericMergeOrRebaseAction(commandType, command)
return self.CheckMergeOrRebaseWithRefreshOptions(result,
types.RefreshOptions{
Mode: types.ASYNC,
CommitSelection: commitSelectionAfterMerge(result == nil && selectHeadCommitOnSuccess),
runAction := func() error {
result := self.c.Git().Rebase.GenericMergeOrRebaseAction(commandType, command)
return self.CheckMergeOrRebaseWithRefreshOptions(result,
types.RefreshOptions{
Mode: types.ASYNC,
CommitSelection: commitSelectionAfterMerge(result == nil && selectHeadCommitOnSuccess),
})
}
if showWaitingStatus {
return self.c.WithWaitingStatus(status.Title(self.c.Tr), func(gocui.Task) error {
return runAction()
})
}
return runAction()
}
// commitSelectionAfterMerge maps whether a merge/rebase/pull created a new
@ -191,9 +213,9 @@ func (self *MergeAndRebaseHelper) CheckMergeOrRebaseWithRefreshOptions(result er
if result == nil {
return nil
} else if strings.Contains(result.Error(), "No changes - did you forget to use") {
return self.genericMergeCommand(REBASE_OPTION_SKIP)
return self.genericMergeCommandImpl(REBASE_OPTION_SKIP, false)
} else if strings.Contains(result.Error(), "The previous cherry-pick is now empty") {
return self.genericMergeCommand(REBASE_OPTION_SKIP)
return self.genericMergeCommandImpl(REBASE_OPTION_SKIP, false)
} else if strings.Contains(result.Error(), "No rebase in progress?") {
// assume in this case that we're already done
return nil
@ -386,21 +408,23 @@ func (self *MergeAndRebaseHelper) RebaseOntoRef(ref string) error {
Tooltip: self.c.Tr.InteractiveRebaseTooltip,
OnPress: func() error {
self.c.LogAction(self.c.Tr.Actions.RebaseBranch)
baseCommit := self.c.Modes().MarkedBaseCommit.GetHash()
var err error
if baseCommit != "" {
err = self.c.Git().Rebase.EditRebaseFromBaseCommit(ref, baseCommit)
} else {
err = self.c.Git().Rebase.EditRebase(ref)
}
if err = self.CheckMergeOrRebase(err); err != nil {
return err
}
if err = self.ResetMarkedBaseCommit(); err != nil {
return err
}
self.c.Context().Push(self.c.Contexts().LocalCommits, types.OnFocusOpts{})
return nil
return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func(task gocui.Task) error {
baseCommit := self.c.Modes().MarkedBaseCommit.GetHash()
var err error
if baseCommit != "" {
err = self.c.Git().Rebase.EditRebaseFromBaseCommit(ref, baseCommit)
} else {
err = self.c.Git().Rebase.EditRebase(ref)
}
if err = self.CheckMergeOrRebase(err); err != nil {
return err
}
if err = self.ResetMarkedBaseCommit(); err != nil {
return err
}
self.c.Context().Push(self.c.Contexts().LocalCommits, types.OnFocusOpts{})
return nil
})
},
},
{
@ -564,36 +588,42 @@ func (self *MergeAndRebaseHelper) MergeRefIntoCheckedOutBranch(refName string) e
func (self *MergeAndRebaseHelper) RegularMerge(refName string, variant git_commands.MergeVariant) func() error {
return func() error {
self.c.LogAction(self.c.Tr.Actions.Merge)
err := self.c.Git().Branch.Merge(refName, variant)
return self.CheckMergeOrRebaseAndSelectHeadCommit(err)
return self.c.WithWaitingStatus(self.c.Tr.MergingStatus, func(gocui.Task) error {
err := self.c.Git().Branch.Merge(refName, variant)
return self.CheckMergeOrRebaseAndSelectHeadCommit(err)
})
}
}
func (self *MergeAndRebaseHelper) SquashMergeUncommitted(refName string) func() error {
return func() error {
self.c.LogAction(self.c.Tr.Actions.SquashMerge)
err := self.c.Git().Branch.Merge(refName, git_commands.MERGE_VARIANT_SQUASH)
return self.CheckMergeOrRebase(err)
return self.c.WithWaitingStatus(self.c.Tr.MergingStatus, func(gocui.Task) error {
err := self.c.Git().Branch.Merge(refName, git_commands.MERGE_VARIANT_SQUASH)
return self.CheckMergeOrRebase(err)
})
}
}
func (self *MergeAndRebaseHelper) SquashMergeCommitted(refName, checkedOutBranchName string) func() error {
return func() error {
self.c.LogAction(self.c.Tr.Actions.SquashMerge)
err := self.c.Git().Branch.Merge(refName, git_commands.MERGE_VARIANT_SQUASH)
if err = self.CheckMergeOrRebase(err); err != nil {
return err
}
message := utils.ResolvePlaceholderString(self.c.UserConfig().Git.Merging.SquashMergeMessage, map[string]string{
"selectedRef": refName,
"currentBranch": checkedOutBranchName,
return self.c.WithWaitingStatus(self.c.Tr.MergingStatus, func(gocui.Task) error {
err := self.c.Git().Branch.Merge(refName, git_commands.MERGE_VARIANT_SQUASH)
if err = self.CheckMergeOrRebase(err); err != nil {
return err
}
message := utils.ResolvePlaceholderString(self.c.UserConfig().Git.Merging.SquashMergeMessage, map[string]string{
"selectedRef": refName,
"currentBranch": checkedOutBranchName,
})
err = self.c.Git().Commit.CommitCmdObj(message, "", false).Run()
if err != nil {
return err
}
self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
return nil
})
err = self.c.Git().Commit.CommitCmdObj(message, "", false).Run()
if err != nil {
return err
}
self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})
return nil
}
}

View file

@ -288,7 +288,9 @@ func (self *RefsHelper) CreateGitResetMenu(name string, ref string) error {
Prompt: self.c.Tr.ResetHardConfirmation,
HandleConfirm: func() error {
self.c.LogAction("Reset")
return self.ResetToRef(ref, row.strength, []string{})
return self.c.WithWaitingStatus(self.c.Tr.ResettingStatus, func(gocui.Task) error {
return self.ResetToRef(ref, row.strength, []string{})
})
},
})
},

View file

@ -589,9 +589,11 @@ func (self *LocalCommitsController) edit(selectedCommits []*models.Commit, start
commits := self.c.Model().Commits
if !commits[endIdx].IsMerge() {
err := self.c.Git().Rebase.InteractiveRebase(commits, startIdx, endIdx, todo.Edit, "")
return self.c.Helpers().MergeAndRebase.CheckMergeOrRebaseWithRefreshOptions(
err, types.RefreshOptions{Mode: types.BLOCK_UI})
return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func(gocui.Task) error {
err := self.c.Git().Rebase.InteractiveRebase(commits, startIdx, endIdx, todo.Edit, "")
return self.c.Helpers().MergeAndRebase.CheckMergeOrRebaseWithRefreshOptions(
err, types.RefreshOptions{Mode: types.BLOCK_UI})
})
}
return self.startInteractiveRebaseWithEdit(selectedCommits)

View file

@ -430,6 +430,7 @@ type TranslationSet struct {
CommittingStatus string
RewordingStatus string
RevertingStatus string
ResettingStatus string
CreatingFixupCommitStatus string
MovingCommitsToNewBranchStatus string
CommitFiles string
@ -1582,6 +1583,7 @@ func EnglishTranslationSet() *TranslationSet {
CommittingStatus: "Committing",
RewordingStatus: "Rewording",
RevertingStatus: "Reverting",
ResettingStatus: "Resetting",
CreatingFixupCommitStatus: "Creating fixup commit",
MovingCommitsToNewBranchStatus: "Moving commits to new branch",
CommitFiles: "Commit files",