Share cherry-pick state across worktrees of the same repo

Copied commits are referenced by hash, and hashes resolve identically in
every worktree of a repo (shared object database), so there's no reason
pasting shouldn't work after switching worktrees. Move the clipboard out
of the per-worktree GuiRepoState into a per-repo SharedRepoState, keyed
by the repo's common git dir; every worktree's Modes.CherryPicking now
points at the same instance. Submodules and unrelated repos have
different git dirs, so they keep isolated clipboards. As a side effect,
cancelling a cherry-pick in one worktree now correctly clears it in all
of them.
This commit is contained in:
Paul Nodet 2026-06-11 03:45:36 +02:00
parent d3f16e5d3b
commit a869854c63
No known key found for this signature in database
3 changed files with 31 additions and 7 deletions

View file

@ -76,7 +76,11 @@ type Gui struct {
// this is a mapping of repos to gui states, so that we can restore the original
// gui state when returning from a subrepo.
// In repos with multiple worktrees, we store a separate repo state per worktree.
RepoStateMap map[Repo]*GuiRepoState
RepoStateMap map[Repo]*GuiRepoState
// Holds state shared between all worktrees of the same repo, keyed by the
// repo's common git dir (one entry per repo, where RepoStateMap has one
// entry per worktree).
sharedRepoStateMap map[Repo]*SharedRepoState
Config config.AppConfigurer
Updater *updates.Updater
statusManager *status.StatusManager
@ -259,6 +263,14 @@ type GuiRepoState struct {
var _ types.IRepoStateAccessor = new(GuiRepoState)
// SharedRepoState is state shared between all worktrees of the same repo.
// Unlike GuiRepoState, of which we keep one instance per worktree, there is
// only one instance of this per repo; e.g. commits copied for cherry-picking
// in one worktree can be pasted in another.
type SharedRepoState struct {
CherryPicking *cherrypicking.CherryPicking
}
func (self *GuiRepoState) GetViewsSetup() bool {
return self.ViewsSetup
}
@ -591,6 +603,15 @@ func (gui *Gui) resetState(startArgs appTypes.StartArgs) types.Context {
return gui.c.Context().Current()
}
repoGitDirPath := gui.git.RepoPaths.RepoGitDirPath()
sharedState := gui.sharedRepoStateMap[Repo(repoGitDirPath)]
if sharedState == nil {
sharedState = &SharedRepoState{
CherryPicking: cherrypicking.New(),
}
gui.sharedRepoStateMap[Repo(repoGitDirPath)] = sharedState
}
contextTree := gui.contextTree()
initialScreenMode := initialScreenMode(startArgs, gui.Config)
@ -614,7 +635,7 @@ func (gui *Gui) resetState(startArgs appTypes.StartArgs) types.Context {
},
Modes: &types.Modes{
Filtering: filtering.New(startArgs.FilterPath, ""),
CherryPicking: cherrypicking.New(),
CherryPicking: sharedState.CherryPicking,
Diffing: diffing.New(),
MarkedBaseCommit: marked_base_commit.New(),
},
@ -738,6 +759,7 @@ func NewGui(
showRecentRepos: showRecentRepos,
RepoPathStack: &utils.StringStack{},
RepoStateMap: map[Repo]*GuiRepoState{},
sharedRepoStateMap: map[Repo]*SharedRepoState{},
GuiLog: []string{},
// initializing this to true for the time being; it will be reset to the

View file

@ -8,8 +8,13 @@ import (
)
type Modes struct {
Filtering filtering.Filtering
CherryPicking *cherrypicking.CherryPicking
Filtering filtering.Filtering
// Shared between all worktrees of the same repo (see gui.SharedRepoState).
// Mutate it through this pointer, but never replace it, otherwise it is no
// longer shared.
CherryPicking *cherrypicking.CherryPicking
Diffing diffing.Diffing
MarkedBaseCommit marked_base_commit.MarkedBaseCommit
}

View file

@ -44,7 +44,6 @@ var CherryPickAcrossWorktrees = NewIntegrationTest(NewIntegrationTestArgs{
).
Press(keys.Commits.PasteCommits)
/* EXPECTED:
t.ExpectPopup().Alert().
Title(Equals("Cherry-pick")).
Content(Contains("Are you sure you want to cherry-pick the 1 copied commit(s) onto this branch?")).
@ -57,7 +56,5 @@ var CherryPickAcrossWorktrees = NewIntegrationTest(NewIntegrationTestArgs{
Contains("one"),
Contains("base").IsSelected(),
)
ACTUAL: */
t.ExpectToast(Equals("Disabled: No copied commits"))
},
})