mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-07-28 16:02:01 +03:00
Handle deleting branch attached to worktree
This commit is contained in:
@ -49,6 +49,13 @@ func (self *GitCommandBuilder) RepoPath(value string) *GitCommandBuilder {
|
|||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (self *GitCommandBuilder) WorktreePath(path string) *GitCommandBuilder {
|
||||||
|
// worktree path comes before the command
|
||||||
|
self.args = append([]string{"--work-tree", path}, self.args...)
|
||||||
|
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
func (self *GitCommandBuilder) ToArgv() []string {
|
func (self *GitCommandBuilder) ToArgv() []string {
|
||||||
return append([]string{"git"}, self.args...)
|
return append([]string{"git"}, self.args...)
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,12 @@ func (self *WorktreeCommands) Delete(worktreePath string, force bool) error {
|
|||||||
return self.cmd.New(cmdArgs).Run()
|
return self.cmd.New(cmdArgs).Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (self *WorktreeCommands) Detach(worktreePath string) error {
|
||||||
|
cmdArgs := NewGitCmd("checkout").Arg("--detach").WorktreePath(worktreePath).ToArgv()
|
||||||
|
|
||||||
|
return self.cmd.New(cmdArgs).Run()
|
||||||
|
}
|
||||||
|
|
||||||
func (self *WorktreeCommands) IsCurrentWorktree(w *models.Worktree) bool {
|
func (self *WorktreeCommands) IsCurrentWorktree(w *models.Worktree) bool {
|
||||||
return IsCurrentWorktree(w)
|
return IsCurrentWorktree(w)
|
||||||
}
|
}
|
||||||
|
@ -203,7 +203,7 @@ func (self *BranchesController) press(selectedBranch *models.Branch) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if selectedBranch.CheckedOutByOtherWorktree {
|
if selectedBranch.CheckedOutByOtherWorktree {
|
||||||
worktreeForRef, ok := self.worktreeForRef(selectedBranch.Name)
|
worktreeForRef, ok := self.worktreeForBranch(selectedBranch)
|
||||||
if ok && !self.c.Git().Worktree.IsCurrentWorktree(worktreeForRef) {
|
if ok && !self.c.Git().Worktree.IsCurrentWorktree(worktreeForRef) {
|
||||||
return self.promptToCheckoutWorktree(worktreeForRef)
|
return self.promptToCheckoutWorktree(worktreeForRef)
|
||||||
}
|
}
|
||||||
@ -213,9 +213,9 @@ func (self *BranchesController) press(selectedBranch *models.Branch) error {
|
|||||||
return self.c.Helpers().Refs.CheckoutRef(selectedBranch.Name, types.CheckoutRefOptions{})
|
return self.c.Helpers().Refs.CheckoutRef(selectedBranch.Name, types.CheckoutRefOptions{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *BranchesController) worktreeForRef(ref string) (*models.Worktree, bool) {
|
func (self *BranchesController) worktreeForBranch(branch *models.Branch) (*models.Worktree, bool) {
|
||||||
for _, worktree := range self.c.Model().Worktrees {
|
for _, worktree := range self.c.Model().Worktrees {
|
||||||
if worktree.Branch == ref {
|
if worktree.Branch == branch.Name {
|
||||||
return worktree, true
|
return worktree, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -325,9 +325,47 @@ func (self *BranchesController) delete(branch *models.Branch) error {
|
|||||||
if checkedOutBranch.Name == branch.Name {
|
if checkedOutBranch.Name == branch.Name {
|
||||||
return self.c.ErrorMsg(self.c.Tr.CantDeleteCheckOutBranch)
|
return self.c.ErrorMsg(self.c.Tr.CantDeleteCheckOutBranch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if branch.CheckedOutByOtherWorktree {
|
||||||
|
return self.promptWorktreeBranchDelete(branch)
|
||||||
|
}
|
||||||
|
|
||||||
return self.deleteWithForce(branch, false)
|
return self.deleteWithForce(branch, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (self *BranchesController) promptWorktreeBranchDelete(selectedBranch *models.Branch) error {
|
||||||
|
worktree, ok := self.worktreeForBranch(selectedBranch)
|
||||||
|
if !ok {
|
||||||
|
self.c.Log.Error("CheckedOutByOtherWorktree out of sync with list of worktrees")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.c.Menu(types.CreateMenuOptions{
|
||||||
|
Title: fmt.Sprintf("Branch %s is checked out by worktree %s", selectedBranch.Name, worktree.Name()),
|
||||||
|
Items: []*types.MenuItem{
|
||||||
|
{
|
||||||
|
Label: "Switch to worktree " + worktree.Name(),
|
||||||
|
OnPress: func() error {
|
||||||
|
return self.c.Helpers().Worktree.Switch(worktree, context.LOCAL_BRANCHES_CONTEXT_KEY)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "Detach worktree",
|
||||||
|
Tooltip: "This will run `git checkout --detach` on the worktree so that it stops hogging the branch, but the worktree's working tree will be left alone",
|
||||||
|
OnPress: func() error {
|
||||||
|
return self.c.Helpers().Worktree.Detach(worktree)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "Remove worktree",
|
||||||
|
OnPress: func() error {
|
||||||
|
return self.c.Helpers().Worktree.Remove(worktree, false)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (self *BranchesController) deleteWithForce(selectedBranch *models.Branch, force bool) error {
|
func (self *BranchesController) deleteWithForce(selectedBranch *models.Branch, force bool) error {
|
||||||
title := self.c.Tr.DeleteBranch
|
title := self.c.Tr.DeleteBranch
|
||||||
var templateStr string
|
var templateStr string
|
||||||
|
@ -6,10 +6,12 @@ import (
|
|||||||
"io/fs"
|
"io/fs"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/jesseduffield/gocui"
|
"github.com/jesseduffield/gocui"
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||||
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type IWorktreeHelper interface {
|
type IWorktreeHelper interface {
|
||||||
@ -87,3 +89,49 @@ func (self *WorktreeHelper) Switch(worktree *models.Worktree, contextKey types.C
|
|||||||
|
|
||||||
return self.reposHelper.DispatchSwitchTo(worktree.Path, true, self.c.Tr.ErrWorktreeMovedOrRemoved, contextKey)
|
return self.reposHelper.DispatchSwitchTo(worktree.Path, true, self.c.Tr.ErrWorktreeMovedOrRemoved, contextKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (self *WorktreeHelper) Remove(worktree *models.Worktree, force bool) error {
|
||||||
|
title := self.c.Tr.RemoveWorktreeTitle
|
||||||
|
var templateStr string
|
||||||
|
if force {
|
||||||
|
templateStr = self.c.Tr.ForceRemoveWorktreePrompt
|
||||||
|
} else {
|
||||||
|
templateStr = self.c.Tr.RemoveWorktreePrompt
|
||||||
|
}
|
||||||
|
message := utils.ResolvePlaceholderString(
|
||||||
|
templateStr,
|
||||||
|
map[string]string{
|
||||||
|
"worktreeName": worktree.Name(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.c.Confirm(types.ConfirmOpts{
|
||||||
|
Title: title,
|
||||||
|
Prompt: message,
|
||||||
|
HandleConfirm: func() error {
|
||||||
|
return self.c.WithWaitingStatus(self.c.Tr.RemovingWorktree, func(gocui.Task) error {
|
||||||
|
self.c.LogAction(self.c.Tr.RemoveWorktree)
|
||||||
|
if err := self.c.Git().Worktree.Delete(worktree.Path, force); err != nil {
|
||||||
|
errMessage := err.Error()
|
||||||
|
if !strings.Contains(errMessage, "--force") {
|
||||||
|
return self.c.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !force {
|
||||||
|
return self.Remove(worktree, true)
|
||||||
|
}
|
||||||
|
return self.c.ErrorMsg(errMessage)
|
||||||
|
}
|
||||||
|
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.WORKTREES}})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *WorktreeHelper) Detach(worktree *models.Worktree) error {
|
||||||
|
return self.c.WithWaitingStatus(self.c.Tr.DetachingWorktree, func(gocui.Task) error {
|
||||||
|
self.c.LogAction(self.c.Tr.RemovingWorktree)
|
||||||
|
|
||||||
|
return self.c.Git().Worktree.Detach(worktree.Path)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -5,12 +5,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
|
|
||||||
"github.com/jesseduffield/gocui"
|
|
||||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||||
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
"github.com/jesseduffield/lazygit/pkg/gui/context"
|
||||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type WorktreesController struct {
|
type WorktreesController struct {
|
||||||
@ -101,41 +99,7 @@ func (self *WorktreesController) remove(worktree *models.Worktree) error {
|
|||||||
return self.c.ErrorMsg(self.c.Tr.CantDeleteCurrentWorktree)
|
return self.c.ErrorMsg(self.c.Tr.CantDeleteCurrentWorktree)
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.removeWithForce(worktree, false)
|
return self.c.Helpers().Worktree.Remove(worktree, false)
|
||||||
}
|
|
||||||
|
|
||||||
func (self *WorktreesController) removeWithForce(worktree *models.Worktree, force bool) error {
|
|
||||||
title := self.c.Tr.RemoveWorktreeTitle
|
|
||||||
var templateStr string
|
|
||||||
if force {
|
|
||||||
templateStr = self.c.Tr.ForceRemoveWorktreePrompt
|
|
||||||
} else {
|
|
||||||
templateStr = self.c.Tr.RemoveWorktreePrompt
|
|
||||||
}
|
|
||||||
message := utils.ResolvePlaceholderString(
|
|
||||||
templateStr,
|
|
||||||
map[string]string{
|
|
||||||
"worktreeName": worktree.Name(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.c.Confirm(types.ConfirmOpts{
|
|
||||||
Title: title,
|
|
||||||
Prompt: message,
|
|
||||||
HandleConfirm: func() error {
|
|
||||||
return self.c.WithWaitingStatus(self.c.Tr.RemovingWorktree, func(gocui.Task) error {
|
|
||||||
self.c.LogAction(self.c.Tr.RemovingWorktree)
|
|
||||||
if err := self.c.Git().Worktree.Delete(worktree.Path, force); err != nil {
|
|
||||||
errMessage := err.Error()
|
|
||||||
if !force {
|
|
||||||
return self.removeWithForce(worktree, true)
|
|
||||||
}
|
|
||||||
return self.c.ErrorMsg(errMessage)
|
|
||||||
}
|
|
||||||
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.WORKTREES}})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *WorktreesController) GetOnClick() func() error {
|
func (self *WorktreesController) GetOnClick() func() error {
|
||||||
|
@ -545,6 +545,8 @@ type TranslationSet struct {
|
|||||||
SwitchToWorktree string
|
SwitchToWorktree string
|
||||||
RemoveWorktree string
|
RemoveWorktree string
|
||||||
RemoveWorktreeTitle string
|
RemoveWorktreeTitle string
|
||||||
|
DetachWorktree string
|
||||||
|
DetachingWorktree string
|
||||||
WorktreesTitle string
|
WorktreesTitle string
|
||||||
WorktreeTitle string
|
WorktreeTitle string
|
||||||
RemoveWorktreePrompt string
|
RemoveWorktreePrompt string
|
||||||
@ -1272,6 +1274,8 @@ func EnglishTranslationSet() TranslationSet {
|
|||||||
RemoveWorktreePrompt: "Are you sure you want to remove worktree '{{.worktreeName}}'?",
|
RemoveWorktreePrompt: "Are you sure you want to remove worktree '{{.worktreeName}}'?",
|
||||||
ForceRemoveWorktreePrompt: "'{{.worktreeName}}' contains modified or untracked files (to be honest, it could contain both). Are you sure you want to remove it?",
|
ForceRemoveWorktreePrompt: "'{{.worktreeName}}' contains modified or untracked files (to be honest, it could contain both). Are you sure you want to remove it?",
|
||||||
RemovingWorktree: "Deleting worktree",
|
RemovingWorktree: "Deleting worktree",
|
||||||
|
DetachWorktree: "Detach worktree",
|
||||||
|
DetachingWorktree: "Detaching worktree",
|
||||||
AddingWorktree: "Adding worktree",
|
AddingWorktree: "Adding worktree",
|
||||||
CantDeleteCurrentWorktree: "You cannot remove the current worktree!",
|
CantDeleteCurrentWorktree: "You cannot remove the current worktree!",
|
||||||
AlreadyInWorktree: "You are already in the selected worktree",
|
AlreadyInWorktree: "You are already in the selected worktree",
|
||||||
|
Reference in New Issue
Block a user