1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-06-23 00:39:13 +02:00

Support creating worktrees from refs

This commit is contained in:
Jesse Duffield
2023-07-16 19:39:53 +10:00
parent 4b2622d93b
commit 18ea68c23a
9 changed files with 239 additions and 29 deletions

View File

@ -6,8 +6,6 @@ import (
"io/fs" "io/fs"
"log" "log"
"os" "os"
"github.com/jesseduffield/lazygit/pkg/commands/models"
) )
type WorktreeCommands struct { type WorktreeCommands struct {
@ -20,10 +18,30 @@ func NewWorktreeCommands(gitCommon *GitCommon) *WorktreeCommands {
} }
} }
func (self *WorktreeCommands) New(worktreePath string, committish string) error { type NewWorktreeOpts struct {
cmdArgs := NewGitCmd("worktree").Arg("add", worktreePath, committish).ToArgv() // required. The path of the new worktree.
Path string
// required. The base branch/ref.
Base string
return self.cmd.New(cmdArgs).Run() // if true, ends up with a detached head
Detach bool
// optional. if empty, and if detach is false, we will checkout the base
Branch string
}
func (self *WorktreeCommands) New(opts NewWorktreeOpts) error {
if opts.Detach && opts.Branch != "" {
panic("cannot specify branch when detaching")
}
cmdArgs := NewGitCmd("worktree").Arg("add").
ArgIf(opts.Detach, "--detach").
ArgIf(opts.Branch != "", "-b", opts.Branch).
Arg(opts.Path, opts.Base)
return self.cmd.New(cmdArgs.ToArgv()).Run()
} }
func (self *WorktreeCommands) Delete(worktreePath string, force bool) error { func (self *WorktreeCommands) Delete(worktreePath string, force bool) error {
@ -38,25 +56,25 @@ func (self *WorktreeCommands) Detach(worktreePath string) error {
return self.cmd.New(cmdArgs).SetWd(worktreePath).Run() return self.cmd.New(cmdArgs).SetWd(worktreePath).Run()
} }
func (self *WorktreeCommands) IsCurrentWorktree(w *models.Worktree) bool { func (self *WorktreeCommands) IsCurrentWorktree(path string) bool {
return IsCurrentWorktree(w) return IsCurrentWorktree(path)
} }
func IsCurrentWorktree(w *models.Worktree) bool { func IsCurrentWorktree(path string) bool {
pwd, err := os.Getwd() pwd, err := os.Getwd()
if err != nil { if err != nil {
log.Fatalln(err.Error()) log.Fatalln(err.Error())
} }
return EqualPath(pwd, w.Path) return EqualPath(pwd, path)
} }
func (self *WorktreeCommands) IsWorktreePathMissing(w *models.Worktree) bool { func (self *WorktreeCommands) IsWorktreePathMissing(path string) bool {
if _, err := os.Stat(w.Path); err != nil { if _, err := os.Stat(path); err != nil {
if errors.Is(err, fs.ErrNotExist) { if errors.Is(err, fs.ErrNotExist) {
return true return true
} }
log.Fatalln(fmt.Errorf("failed to check if worktree path `%s` exists\n%w", w.Path, err).Error()) log.Fatalln(fmt.Errorf("failed to check if worktree path `%s` exists\n%w", path, err).Error())
} }
return false return false
} }

View File

@ -132,6 +132,7 @@ type KeybindingConfig struct {
Status KeybindingStatusConfig `yaml:"status"` Status KeybindingStatusConfig `yaml:"status"`
Files KeybindingFilesConfig `yaml:"files"` Files KeybindingFilesConfig `yaml:"files"`
Branches KeybindingBranchesConfig `yaml:"branches"` Branches KeybindingBranchesConfig `yaml:"branches"`
Worktrees KeybindingWorktreesConfig `yaml:"worktrees"`
Commits KeybindingCommitsConfig `yaml:"commits"` Commits KeybindingCommitsConfig `yaml:"commits"`
Stash KeybindingStashConfig `yaml:"stash"` Stash KeybindingStashConfig `yaml:"stash"`
CommitFiles KeybindingCommitFilesConfig `yaml:"commitFiles"` CommitFiles KeybindingCommitFilesConfig `yaml:"commitFiles"`
@ -246,6 +247,10 @@ type KeybindingBranchesConfig struct {
FetchRemote string `yaml:"fetchRemote"` FetchRemote string `yaml:"fetchRemote"`
} }
type KeybindingWorktreesConfig struct {
ViewWorktreeOptions string `yaml:"viewWorktreeOptions"`
}
type KeybindingCommitsConfig struct { type KeybindingCommitsConfig struct {
SquashDown string `yaml:"squashDown"` SquashDown string `yaml:"squashDown"`
RenameCommit string `yaml:"renameCommit"` RenameCommit string `yaml:"renameCommit"`
@ -587,6 +592,9 @@ func GetDefaultConfig() *UserConfig {
SetUpstream: "u", SetUpstream: "u",
FetchRemote: "f", FetchRemote: "f",
}, },
Worktrees: KeybindingWorktreesConfig{
ViewWorktreeOptions: "w",
},
Commits: KeybindingCommitsConfig{ Commits: KeybindingCommitsConfig{
SquashDown: "s", SquashDown: "s",
RenameCommit: "r", RenameCommit: "r",

View File

@ -18,6 +18,9 @@ func (gui *Gui) Helpers() *helpers.Helpers {
func (gui *Gui) resetHelpersAndControllers() { func (gui *Gui) resetHelpersAndControllers() {
helperCommon := gui.c helperCommon := gui.c
recordDirectoryHelper := helpers.NewRecordDirectoryHelper(helperCommon)
reposHelper := helpers.NewRecentReposHelper(helperCommon, recordDirectoryHelper, gui.onNewRepo)
worktreeHelper := helpers.NewWorktreeHelper(helperCommon, reposHelper)
refsHelper := helpers.NewRefsHelper(helperCommon) refsHelper := helpers.NewRefsHelper(helperCommon)
rebaseHelper := helpers.NewMergeAndRebaseHelper(helperCommon, refsHelper) rebaseHelper := helpers.NewMergeAndRebaseHelper(helperCommon, refsHelper)
@ -41,12 +44,10 @@ func (gui *Gui) resetHelpersAndControllers() {
gpgHelper := helpers.NewGpgHelper(helperCommon) gpgHelper := helpers.NewGpgHelper(helperCommon)
viewHelper := helpers.NewViewHelper(helperCommon, gui.State.Contexts) viewHelper := helpers.NewViewHelper(helperCommon, gui.State.Contexts)
recordDirectoryHelper := helpers.NewRecordDirectoryHelper(helperCommon)
patchBuildingHelper := helpers.NewPatchBuildingHelper(helperCommon) patchBuildingHelper := helpers.NewPatchBuildingHelper(helperCommon)
stagingHelper := helpers.NewStagingHelper(helperCommon) stagingHelper := helpers.NewStagingHelper(helperCommon)
mergeConflictsHelper := helpers.NewMergeConflictsHelper(helperCommon) mergeConflictsHelper := helpers.NewMergeConflictsHelper(helperCommon)
reposHelper := helpers.NewRecentReposHelper(helperCommon, recordDirectoryHelper, gui.onNewRepo)
worktreeHelper := helpers.NewWorktreeHelper(helperCommon, reposHelper)
refreshHelper := helpers.NewRefreshHelper( refreshHelper := helpers.NewRefreshHelper(
helperCommon, helperCommon,
refsHelper, refsHelper,
@ -241,6 +242,18 @@ func (gui *Gui) resetHelpersAndControllers() {
controllers.AttachControllers(context, controllers.NewBasicCommitsController(common, context)) controllers.AttachControllers(context, controllers.NewBasicCommitsController(common, context))
} }
for _, context := range []controllers.CanViewWorktreeOptions{
gui.State.Contexts.LocalCommits,
gui.State.Contexts.ReflogCommits,
gui.State.Contexts.SubCommits,
gui.State.Contexts.Stash,
gui.State.Contexts.Branches,
gui.State.Contexts.RemoteBranches,
gui.State.Contexts.Tags,
} {
controllers.AttachControllers(context, controllers.NewWorktreeOptionsController(common, context))
}
controllers.AttachControllers(gui.State.Contexts.ReflogCommits, controllers.AttachControllers(gui.State.Contexts.ReflogCommits,
reflogCommitsController, reflogCommitsController,
) )

View File

@ -204,7 +204,7 @@ func (self *BranchesController) press(selectedBranch *models.Branch) error {
if selectedBranch.CheckedOutByOtherWorktree { if selectedBranch.CheckedOutByOtherWorktree {
worktreeForRef, ok := self.worktreeForBranch(selectedBranch) worktreeForRef, ok := self.worktreeForBranch(selectedBranch)
if ok && !self.c.Git().Worktree.IsCurrentWorktree(worktreeForRef) { if ok && !self.c.Git().Worktree.IsCurrentWorktree(worktreeForRef.Path) {
return self.promptToCheckoutWorktree(worktreeForRef) return self.promptToCheckoutWorktree(worktreeForRef)
} }
} }
@ -228,7 +228,7 @@ func (self *BranchesController) promptToCheckoutWorktree(worktree *models.Worktr
Title: "Switch to worktree", Title: "Switch to worktree",
Prompt: fmt.Sprintf("This branch is checked out by worktree %s. Do you want to switch to that worktree?", worktree.Name()), Prompt: fmt.Sprintf("This branch is checked out by worktree %s. Do you want to switch to that worktree?", worktree.Name()),
HandleConfirm: func() error { HandleConfirm: func() error {
return self.c.Helpers().Worktree.Switch(worktree, context.LOCAL_BRANCHES_CONTEXT_KEY) return self.c.Helpers().Worktree.Switch(worktree.Path, context.LOCAL_BRANCHES_CONTEXT_KEY)
}, },
}) })
} }
@ -346,7 +346,7 @@ func (self *BranchesController) promptWorktreeBranchDelete(selectedBranch *model
{ {
Label: "Switch to worktree", Label: "Switch to worktree",
OnPress: func() error { OnPress: func() error {
return self.c.Helpers().Worktree.Switch(worktree, context.LOCAL_BRANCHES_CONTEXT_KEY) return self.c.Helpers().Worktree.Switch(worktree.Path, context.LOCAL_BRANCHES_CONTEXT_KEY)
}, },
}, },
{ {

View File

@ -9,7 +9,9 @@ import (
"strings" "strings"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"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/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
) )
@ -66,10 +68,14 @@ func (self *WorktreeHelper) NewWorktree() error {
HandleConfirm: func(path string) error { HandleConfirm: func(path string) error {
return self.c.Prompt(types.PromptOpts{ return self.c.Prompt(types.PromptOpts{
Title: self.c.Tr.NewWorktreeBranch, Title: self.c.Tr.NewWorktreeBranch,
HandleConfirm: func(committish string) error { // TODO: suggestions
HandleConfirm: func(base string) error {
return self.c.WithWaitingStatus(self.c.Tr.AddingWorktree, func(gocui.Task) error { return self.c.WithWaitingStatus(self.c.Tr.AddingWorktree, func(gocui.Task) error {
self.c.LogAction(self.c.Tr.Actions.AddWorktree) self.c.LogAction(self.c.Tr.Actions.AddWorktree)
if err := self.c.Git().Worktree.New(sanitizedBranchName(path), committish); err != nil { if err := self.c.Git().Worktree.New(git_commands.NewWorktreeOpts{
Path: path,
Base: base,
}); err != nil {
return err return err
} }
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.WORKTREES, types.BRANCHES, types.FILES}}) return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.WORKTREES, types.BRANCHES, types.FILES}})
@ -80,14 +86,70 @@ func (self *WorktreeHelper) NewWorktree() error {
}) })
} }
func (self *WorktreeHelper) Switch(worktree *models.Worktree, contextKey types.ContextKey) error { func (self *WorktreeHelper) NewWorktreeCheckout(base string, isBranch bool, detached bool) error {
if self.c.Git().Worktree.IsCurrentWorktree(worktree) { opts := git_commands.NewWorktreeOpts{
Base: base,
Detach: detached,
}
f := func() error {
return self.c.WithWaitingStatus(self.c.Tr.AddingWorktree, func(gocui.Task) error {
self.c.LogAction(self.c.Tr.Actions.AddWorktree)
if err := self.c.Git().Worktree.New(opts); err != nil {
return err
}
return self.Switch(opts.Path, context.LOCAL_BRANCHES_CONTEXT_KEY)
})
}
return self.c.Prompt(types.PromptOpts{
Title: self.c.Tr.NewWorktreePath,
HandleConfirm: func(path string) error {
opts.Path = path
if detached {
return f()
}
if isBranch {
// prompt for the new branch name where a blank means we just check out the branch
return self.c.Prompt(types.PromptOpts{
Title: fmt.Sprintf("New branch name (leave blank to checkout %s)", base),
// TODO: suggestions
HandleConfirm: func(branchName string) error {
opts.Branch = branchName
return f()
},
})
} else {
// prompt for the new branch name where a blank means we just check out the branch
return self.c.Prompt(types.PromptOpts{
Title: "New branch name",
// TODO: suggestions
HandleConfirm: func(branchName string) error {
if branchName == "" {
return self.c.ErrorMsg("Branch name cannot be blank")
}
opts.Branch = branchName
return f()
},
})
}
},
})
}
func (self *WorktreeHelper) Switch(path string, contextKey types.ContextKey) error {
if self.c.Git().Worktree.IsCurrentWorktree(path) {
return self.c.ErrorMsg(self.c.Tr.AlreadyInWorktree) return self.c.ErrorMsg(self.c.Tr.AlreadyInWorktree)
} }
self.c.LogAction(self.c.Tr.SwitchToWorktree) self.c.LogAction(self.c.Tr.SwitchToWorktree)
return self.reposHelper.DispatchSwitchTo(worktree.Path, true, self.c.Tr.ErrWorktreeMovedOrRemoved, contextKey) return self.reposHelper.DispatchSwitchTo(path, true, self.c.Tr.ErrWorktreeMovedOrRemoved, contextKey)
} }
func (self *WorktreeHelper) Remove(worktree *models.Worktree, force bool) error { func (self *WorktreeHelper) Remove(worktree *models.Worktree, force bool) error {
@ -139,3 +201,51 @@ func (self *WorktreeHelper) Detach(worktree *models.Worktree) error {
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.WORKTREES, types.BRANCHES, types.FILES}}) return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.WORKTREES, types.BRANCHES, types.FILES}})
}) })
} }
func (self *WorktreeHelper) ViewWorktreeOptions(context types.IListContext, ref string) error {
if context == self.c.Contexts().Branches {
return self.ViewBranchWorktreeOptions(ref)
}
return self.ViewRefWorktreeOptions(ref)
}
func (self *WorktreeHelper) ViewBranchWorktreeOptions(branchName string) error {
return self.c.Menu(types.CreateMenuOptions{
Title: self.c.Tr.WorktreeTitle,
Items: []*types.MenuItem{
{
LabelColumns: []string{"Create new worktree from branch"},
OnPress: func() error {
return self.NewWorktreeCheckout(branchName, true, false)
},
},
{
LabelColumns: []string{"Create new worktree from branch (detached)"},
OnPress: func() error {
return self.NewWorktreeCheckout(branchName, true, true)
},
},
},
})
}
func (self *WorktreeHelper) ViewRefWorktreeOptions(ref string) error {
return self.c.Menu(types.CreateMenuOptions{
Title: self.c.Tr.WorktreeTitle,
Items: []*types.MenuItem{
{
LabelColumns: []string{"Create new worktree from ref"},
OnPress: func() error {
return self.NewWorktreeCheckout(ref, false, false)
},
},
{
LabelColumns: []string{"Create new worktree from ref (detached)"},
OnPress: func() error {
return self.NewWorktreeCheckout(ref, false, true)
},
},
},
})
}

View File

@ -0,0 +1,59 @@
package controllers
import (
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
// This controller is for all contexts that have items you can create a worktree from
var _ types.IController = &WorktreeOptionsController{}
type CanViewWorktreeOptions interface {
types.IListContext
}
type WorktreeOptionsController struct {
baseController
c *ControllerCommon
context CanViewWorktreeOptions
}
func NewWorktreeOptionsController(controllerCommon *ControllerCommon, context CanViewWorktreeOptions) *WorktreeOptionsController {
return &WorktreeOptionsController{
baseController: baseController{},
c: controllerCommon,
context: context,
}
}
func (self *WorktreeOptionsController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
bindings := []*types.Binding{
{
Key: opts.GetKey(opts.Config.Worktrees.ViewWorktreeOptions),
Handler: self.checkSelected(self.viewWorktreeOptions),
Description: self.c.Tr.ViewWorktreeOptions,
OpensMenu: true,
},
}
return bindings
}
func (self *WorktreeOptionsController) checkSelected(callback func(string) error) func() error {
return func() error {
ref := self.context.GetSelectedItemId()
if ref == "" {
return nil
}
return callback(ref)
}
}
func (self *WorktreeOptionsController) Context() types.Context {
return self.context
}
func (self *WorktreeOptionsController) viewWorktreeOptions(ref string) error {
return self.c.Helpers().Worktree.ViewWorktreeOptions(self.context, ref)
}

View File

@ -62,7 +62,7 @@ func (self *WorktreesController) GetOnRenderToMain() func() error {
} }
missing := "" missing := ""
if self.c.Git().Worktree.IsWorktreePathMissing(worktree) { if self.c.Git().Worktree.IsWorktreePathMissing(worktree.Path) {
missing = style.FgRed.Sprintf(" %s", self.c.Tr.MissingWorktree) missing = style.FgRed.Sprintf(" %s", self.c.Tr.MissingWorktree)
} }
@ -95,7 +95,7 @@ func (self *WorktreesController) remove(worktree *models.Worktree) error {
return self.c.ErrorMsg(self.c.Tr.CantDeleteMainWorktree) return self.c.ErrorMsg(self.c.Tr.CantDeleteMainWorktree)
} }
if self.c.Git().Worktree.IsCurrentWorktree(worktree) { if self.c.Git().Worktree.IsCurrentWorktree(worktree.Path) {
return self.c.ErrorMsg(self.c.Tr.CantDeleteCurrentWorktree) return self.c.ErrorMsg(self.c.Tr.CantDeleteCurrentWorktree)
} }
@ -107,7 +107,7 @@ func (self *WorktreesController) GetOnClick() func() error {
} }
func (self *WorktreesController) enter(worktree *models.Worktree) error { func (self *WorktreesController) enter(worktree *models.Worktree) error {
return self.c.Helpers().Worktree.Switch(worktree, context.WORKTREES_CONTEXT_KEY) return self.c.Helpers().Worktree.Switch(worktree.Path, context.WORKTREES_CONTEXT_KEY)
} }
func (self *WorktreesController) checkSelected(callback func(worktree *models.Worktree) error) func() error { func (self *WorktreesController) checkSelected(callback func(worktree *models.Worktree) error) func() error {

View File

@ -8,11 +8,11 @@ import (
"github.com/samber/lo" "github.com/samber/lo"
) )
func GetWorktreeDisplayStrings(worktrees []*models.Worktree, isCurrent func(*models.Worktree) bool, isMissing func(*models.Worktree) bool) [][]string { func GetWorktreeDisplayStrings(worktrees []*models.Worktree, isCurrent func(string) bool, isMissing func(string) bool) [][]string {
return lo.Map(worktrees, func(worktree *models.Worktree, _ int) []string { return lo.Map(worktrees, func(worktree *models.Worktree, _ int) []string {
return GetWorktreeDisplayString( return GetWorktreeDisplayString(
isCurrent(worktree), isCurrent(worktree.Path),
isMissing(worktree), isMissing(worktree.Path),
worktree) worktree)
}) })
} }

View File

@ -562,6 +562,7 @@ type TranslationSet struct {
CreateWorktree string CreateWorktree string
NewWorktreePath string NewWorktreePath string
NewWorktreeBranch string NewWorktreeBranch string
ViewWorktreeOptions string
Name string Name string
Branch string Branch string
Path string Path string
@ -1286,6 +1287,7 @@ func EnglishTranslationSet() TranslationSet {
CreateWorktree: "Create worktree", CreateWorktree: "Create worktree",
NewWorktreePath: "New worktree path", NewWorktreePath: "New worktree path",
NewWorktreeBranch: "New worktree branch (leave blank to use the current branch)", NewWorktreeBranch: "New worktree branch (leave blank to use the current branch)",
ViewWorktreeOptions: "View worktree options",
Name: "Name", Name: "Name",
Branch: "Branch", Branch: "Branch",
Path: "Path", Path: "Path",