mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-11-06 13:00:59 -04:00
Remove a worktree inside the repo properly when discarding it
If a linked worktree lives inside the repo's working tree, it shows up in the files panel. Discarding it there deleted its directory like that of any untracked file. git still had the worktree on record, so the worktrees panel listed it as missing, and its branch couldn't be checked out anywhere else until the worktree was pruned. Show the same menu for removing the worktree as the worktrees panel does. Like a submodule, a worktree can't be discarded together with other entries, because it has a menu of its own. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3ffcc0c7b0
commit
e2a29a6d77
@@ -1691,24 +1691,41 @@ func findSubmoduleNode(nodes []*filetree.FileNode, submodules []*models.Submodul
|
||||
return nil
|
||||
}
|
||||
|
||||
func findWorktreeNode(nodes []*filetree.FileNode) *models.File {
|
||||
for _, node := range nodes {
|
||||
if worktreeFile := node.FindFirstFileBy(func(f *models.File) bool { return f.IsWorktree }); worktreeFile != nil {
|
||||
return worktreeFile
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *FilesController) canRemove(selectedNodes []*filetree.FileNode) *types.DisabledReason {
|
||||
// Return disabled if the selection contains multiple changed items and includes a submodule change.
|
||||
// Return disabled if the selection contains multiple changed items and
|
||||
// includes a submodule change or a worktree. These have menus of their own.
|
||||
submodules := self.c.Model().Submodules
|
||||
hasFiles := false
|
||||
uniqueSelectedSubmodules := set.New[*models.SubmoduleConfig]()
|
||||
uniqueSelectedWorktreePaths := set.New[string]()
|
||||
|
||||
for _, node := range selectedNodes {
|
||||
_ = node.ForEachFile(func(f *models.File) error {
|
||||
if submodule := f.SubmoduleConfig(submodules); submodule != nil {
|
||||
uniqueSelectedSubmodules.Add(submodule)
|
||||
} else if f.IsWorktree {
|
||||
uniqueSelectedWorktreePaths.Add(f.Path)
|
||||
} else {
|
||||
hasFiles = true
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if uniqueSelectedSubmodules.Len() > 0 && (hasFiles || uniqueSelectedSubmodules.Len() > 1) {
|
||||
if uniqueSelectedSubmodules.Len() > 0 &&
|
||||
(hasFiles || uniqueSelectedWorktreePaths.Len() > 0 || uniqueSelectedSubmodules.Len() > 1) {
|
||||
return &types.DisabledReason{Text: self.c.Tr.MultiSelectNotSupportedForSubmodules}
|
||||
}
|
||||
if uniqueSelectedWorktreePaths.Len() > 0 && (hasFiles || uniqueSelectedWorktreePaths.Len() > 1) {
|
||||
return &types.DisabledReason{Text: self.c.Tr.MultiSelectNotSupportedForWorktrees}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -1737,6 +1754,13 @@ func (self *FilesController) remove(selectedNodes []*filetree.FileNode) error {
|
||||
return self.c.Menu(types.CreateMenuOptions{Title: submoduleNode.GetPath(), Items: menuItems})
|
||||
}
|
||||
|
||||
// Likewise, if we have a worktree, it is the only thing selected
|
||||
if worktreeFile := findWorktreeNode(selectedNodes); worktreeFile != nil {
|
||||
if worktree, ok := self.c.Helpers().WorkingTree.WorktreeForFile(worktreeFile); ok {
|
||||
return self.c.Helpers().BranchesHelper.RemoveWorktreeMenu(worktree)
|
||||
}
|
||||
}
|
||||
|
||||
discardAllChangesItem := types.MenuItem{
|
||||
Label: self.c.Tr.DiscardAllChanges,
|
||||
OnPress: func() error {
|
||||
|
||||
@@ -293,8 +293,8 @@ func (self *BranchesHelper) RemoveWorktreeMenu(worktree *models.Worktree) error
|
||||
|
||||
// RemoveWorktreeAndDeleteBranch removes the worktree and deletes the local branch
|
||||
// it has checked out, force-warning first if the branch isn't fully merged. It's
|
||||
// the worktrees-panel counterpart to deleting a worktree-checked-out branch from
|
||||
// the branches panel.
|
||||
// the remove-worktree menu's counterpart to deleting a worktree-checked-out
|
||||
// branch from the branches panel.
|
||||
func (self *BranchesHelper) RemoveWorktreeAndDeleteBranch(
|
||||
worktree *models.Worktree, branch *models.Branch,
|
||||
) error {
|
||||
|
||||
@@ -113,6 +113,12 @@ func (self *WorkingTreeHelper) FileForSubmodule(submodule *models.SubmoduleConfi
|
||||
return nil
|
||||
}
|
||||
|
||||
// WorktreeForFile returns the worktree that an entry of the files panel is, if
|
||||
// it is one
|
||||
func (self *WorkingTreeHelper) WorktreeForFile(file *models.File) (*models.Worktree, bool) {
|
||||
return worktreeOfFile(file, self.c.Model().Worktrees, self.c.Git().RepoPaths.WorktreePath())
|
||||
}
|
||||
|
||||
func (self *WorkingTreeHelper) OpenMergeTool() error {
|
||||
self.c.LogAction(self.c.Tr.Actions.OpenMergeTool)
|
||||
return self.c.RunSubprocessAndRefresh(
|
||||
|
||||
@@ -1003,6 +1003,7 @@ type TranslationSet struct {
|
||||
SelectedItemIsNotABranch string
|
||||
SelectedItemDoesNotHaveFiles string
|
||||
MultiSelectNotSupportedForSubmodules string
|
||||
MultiSelectNotSupportedForWorktrees string
|
||||
NothingToStageForSubmodule string
|
||||
CommandDoesNotSupportOpeningInEditor string
|
||||
CustomCommands string
|
||||
@@ -2199,6 +2200,7 @@ func EnglishTranslationSet() *TranslationSet {
|
||||
SelectedItemIsNotABranch: "Selected item is not a branch",
|
||||
SelectedItemDoesNotHaveFiles: "Selected item does not have files to view",
|
||||
MultiSelectNotSupportedForSubmodules: "Multiselection not supported for submodules",
|
||||
MultiSelectNotSupportedForWorktrees: "Multiselection not supported for worktrees",
|
||||
NothingToStageForSubmodule: "Nothing to stage: the parent repo can only stage a new submodule commit, not the uncommitted changes inside a submodule. Commit inside the submodule first.",
|
||||
CommandDoesNotSupportOpeningInEditor: "This command doesn't support switching to the editor",
|
||||
CustomCommands: "Custom commands",
|
||||
|
||||
@@ -696,6 +696,7 @@ var tests = []*components.IntegrationTest{
|
||||
worktree.CustomCommand,
|
||||
worktree.DefaultPathTilde,
|
||||
worktree.DetachWorktreeFromBranch,
|
||||
worktree.DiscardRangeWithWorktree,
|
||||
worktree.DotfileBareRepo,
|
||||
worktree.DoubleNestedLinkedSubmodule,
|
||||
worktree.EditFileOfBranchInOtherWorktree,
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package worktree
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var DiscardRangeWithWorktree = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Discarding a range of files that includes a worktree inside the repo is not supported, because a worktree has its own menu for removing it",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {
|
||||
config.GetUserConfig().Gui.ShowFileTree = false
|
||||
},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.NewBranch("mybranch")
|
||||
shell.CreateFileAndAdd("README.md", "hello world")
|
||||
shell.Commit("initial commit")
|
||||
shell.AddWorktree("mybranch", "nested-worktree", "newbranch")
|
||||
shell.CreateFile("untracked-file", "content")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
Lines(
|
||||
Equals("?? nested-worktree (worktree)").IsSelected(),
|
||||
Equals("?? untracked-file"),
|
||||
).
|
||||
Press(keys.Universal.RangeSelectDown).
|
||||
Press(keys.Universal.Remove).
|
||||
Tap(func() {
|
||||
t.ExpectToast(Equals("Disabled: Multiselection not supported for worktrees"))
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -64,18 +64,18 @@ var WorktreeInRepo = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Press(keys.Universal.Remove).
|
||||
Tap(func() {
|
||||
t.ExpectPopup().Menu().
|
||||
Title(Equals("Discard changes")).
|
||||
Select(Contains("Discard all changes")).
|
||||
Title(Equals("Remove worktree 'linked-worktree'?")).
|
||||
Select(MatchesRegexp("Remove worktree$")).
|
||||
Confirm()
|
||||
}).
|
||||
IsEmpty()
|
||||
|
||||
// confirm worktree appears as missing
|
||||
t.FileSystem().PathNotPresent("linked-worktree")
|
||||
|
||||
t.Views().Worktrees().
|
||||
Focus().
|
||||
Lines(
|
||||
Contains("(main worktree)").IsSelected(),
|
||||
Contains("linked-worktree (missing)"),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user