From 44ba0d539fa1c059a7efd4770e973490b72a3fae Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Mon, 6 Jul 2026 11:54:14 +0200 Subject: [PATCH 1/5] Show a waiting status while continuing a merge or rebase Continuing, skipping, or aborting a merge/rebase from the options menu ran the git command inline on the UI thread, freezing the UI with no spinner while it worked (a continue can replay many commits). Run the non-subprocess path on a worker with a waiting status instead, matching how the other merge/rebase entry points already behave. The auto-skip recursion in CheckMergeOrRebaseWithRefreshOptions must not start its own worker: it already runs on the caller's thread (the worker of the enclosing waiting status, or the UI thread for the synchronous callers). Route it through genericMergeCommandImpl with the waiting status suppressed so its behavior is unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../helpers/merge_and_rebase_helper.go | 36 +++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/pkg/gui/controllers/helpers/merge_and_rebase_helper.go b/pkg/gui/controllers/helpers/merge_and_rebase_helper.go index be1d7b3a9..6badaf2b4 100644 --- a/pkg/gui/controllers/helpers/merge_and_rebase_helper.go +++ b/pkg/gui/controllers/helpers/merge_and_rebase_helper.go @@ -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 From 539ede2e1bb47ae466ad6ce2cc4bfa0b1dc2f352 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Mon, 6 Jul 2026 11:54:38 +0200 Subject: [PATCH 2/5] Show a waiting status while merging a branch The regular and squash merges from the merge menu ran inline on the UI thread, freezing it with no spinner while git worked. Run them on a worker with a waiting status, like the rebase entry points already do. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../helpers/merge_and_rebase_helper.go | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/pkg/gui/controllers/helpers/merge_and_rebase_helper.go b/pkg/gui/controllers/helpers/merge_and_rebase_helper.go index 6badaf2b4..b00a4d41b 100644 --- a/pkg/gui/controllers/helpers/merge_and_rebase_helper.go +++ b/pkg/gui/controllers/helpers/merge_and_rebase_helper.go @@ -586,36 +586,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 } } From 2bae29c6fd32b3477cb6ca82d031fe2c23b1a377 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Mon, 6 Jul 2026 11:55:12 +0200 Subject: [PATCH 3/5] Show a waiting status when starting an interactive rebase onto a ref The interactive-rebase item in the rebase-onto-ref menu ran inline on the UI thread with no spinner, unlike its two siblings in the same menu (simple rebase and rebase onto base branch), which already run on a worker with a waiting status. Make it consistent. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../helpers/merge_and_rebase_helper.go | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/pkg/gui/controllers/helpers/merge_and_rebase_helper.go b/pkg/gui/controllers/helpers/merge_and_rebase_helper.go index b00a4d41b..4b25d1a62 100644 --- a/pkg/gui/controllers/helpers/merge_and_rebase_helper.go +++ b/pkg/gui/controllers/helpers/merge_and_rebase_helper.go @@ -408,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 + }) }, }, { From ccf49c8112407c2788aa96469a1aa61f299a5db2 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Mon, 6 Jul 2026 11:55:28 +0200 Subject: [PATCH 4/5] Show a waiting status when editing a commit Setting a single commit to "edit" ran the interactive rebase inline on the UI thread with no spinner, while its sibling startInteractiveRebaseWithEdit (used when editing multiple commits or quick-starting a rebase) already runs on a worker with a waiting status. Make the direct path match. Co-Authored-By: Claude Opus 4.8 (1M context) --- pkg/gui/controllers/local_commits_controller.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pkg/gui/controllers/local_commits_controller.go b/pkg/gui/controllers/local_commits_controller.go index 80f01fc03..9f3acb1d2 100644 --- a/pkg/gui/controllers/local_commits_controller.go +++ b/pkg/gui/controllers/local_commits_controller.go @@ -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) From 6681ba7eda2694d107727f5279b7b55cd3f7dd43 Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Mon, 6 Jul 2026 11:56:07 +0200 Subject: [PATCH 5/5] Show a waiting status while resetting to a ref Resetting to a commit/branch/tag from the reset menu ran inline on the UI thread with no spinner; a hard reset to a distant commit can take a while and blocks the UI meanwhile. Run it on a worker with a waiting status. The undo/redo callers of ResetToRef already wrap it this way. Co-Authored-By: Claude Opus 4.8 (1M context) --- pkg/gui/controllers/helpers/refs_helper.go | 4 +++- pkg/i18n/english.go | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/gui/controllers/helpers/refs_helper.go b/pkg/gui/controllers/helpers/refs_helper.go index 99e9f47ec..b90b9150b 100644 --- a/pkg/gui/controllers/helpers/refs_helper.go +++ b/pkg/gui/controllers/helpers/refs_helper.go @@ -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{}) + }) }, }) }, diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 6f18842d5..0ee0a9e86 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -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",