diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index e2881cca148..3ae4da7d541 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -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 diff --git a/pkg/gui/types/modes.go b/pkg/gui/types/modes.go index a11ed0081f6..c2f89785791 100644 --- a/pkg/gui/types/modes.go +++ b/pkg/gui/types/modes.go @@ -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 } diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 1b264e50dc9..1ddbee5af04 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -490,6 +490,7 @@ var tests = []*components.IntegrationTest{ worktree.AssociateBranchRebase, worktree.BareRepo, worktree.BareRepoWorktreeConfig, + worktree.CherryPickAcrossWorktrees, worktree.Crud, worktree.CustomCommand, worktree.DetachWorktreeFromBranch, diff --git a/pkg/integration/tests/worktree/cherry_pick_across_worktrees.go b/pkg/integration/tests/worktree/cherry_pick_across_worktrees.go new file mode 100644 index 00000000000..1c907d93024 --- /dev/null +++ b/pkg/integration/tests/worktree/cherry_pick_across_worktrees.go @@ -0,0 +1,60 @@ +package worktree + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var CherryPickAcrossWorktrees = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Copy a commit in one worktree and paste it in another worktree of the same repo", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.NewBranch("mybranch") + shell.EmptyCommit("base") + // the linked worktree's branch stays at "base" + shell.AddWorktree("mybranch", "../linked-worktree", "newbranch") + shell.EmptyCommit("one") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + Focus(). + Lines( + Contains("one").IsSelected(), + Contains("base"), + ). + Press(keys.Commits.CherryPickCopy) + + t.Views().Information().Content(Contains("1 commit copied")) + + t.Views().Worktrees(). + Focus(). + Lines( + Contains("(main worktree)").IsSelected(), + Contains("linked-worktree"), + ). + NavigateToLine(Contains("linked-worktree")). + Press(keys.Universal.Select) + + t.Views().Commits(). + Focus(). + Lines( + Contains("base"), + ). + Press(keys.Commits.PasteCommits) + + 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?")). + Confirm() + + t.Views().Information().Content(DoesNotContain("commit copied")) + + t.Views().Commits(). + Lines( + Contains("one"), + Contains("base").IsSelected(), + ) + }, +})