1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-03-29 22:07:13 +02:00

Initial addition of support for worktrees

This commit is contained in:
Joel Baranick 2022-09-01 11:25:41 -07:00 committed by Jesse Duffield
parent 52447e5d46
commit f8ba899b87
20 changed files with 439 additions and 0 deletions

2
.gitignore vendored
View File

@ -39,3 +39,5 @@ test/results/**
oryxBuildBinary
__debug_bin
.worktrees

View File

@ -116,6 +116,7 @@ func localisedTitle(tr *i18n.TranslationSet, str string) string {
"stash": tr.StashTitle,
"suggestions": tr.SuggestionsCheatsheetTitle,
"extras": tr.ExtrasTitle,
"worktrees": tr.WorktreesTitle,
}
title, ok := contextTitleMap[str]

View File

@ -50,6 +50,7 @@ type Loaders struct {
RemoteLoader *git_commands.RemoteLoader
StashLoader *git_commands.StashLoader
TagLoader *git_commands.TagLoader
Worktrees *git_commands.WorktreeLoader
}
func NewGitCommand(
@ -133,6 +134,7 @@ func NewGitCommandAux(
commitLoader := git_commands.NewCommitLoader(cmn, cmd, dotGitDir, statusCommands.RebaseMode, gitCommon)
reflogCommitLoader := git_commands.NewReflogCommitLoader(cmn, cmd)
remoteLoader := git_commands.NewRemoteLoader(cmn, cmd, repo.Remotes)
worktreeLoader := git_commands.NewWorktreeLoader(cmn, cmd)
stashLoader := git_commands.NewStashLoader(cmn, cmd)
tagLoader := git_commands.NewTagLoader(cmn, cmd)
@ -161,6 +163,7 @@ func NewGitCommandAux(
FileLoader: fileLoader,
ReflogCommitLoader: reflogCommitLoader,
RemoteLoader: remoteLoader,
Worktrees: worktreeLoader,
StashLoader: stashLoader,
TagLoader: tagLoader,
},

View File

@ -2,6 +2,7 @@ package git_commands
import (
"fmt"
"os"
"regexp"
"strings"
@ -117,6 +118,11 @@ outer:
}
func (self *BranchLoader) obtainBranches() []*models.Branch {
currentDir, err := os.Getwd()
if err != nil {
panic(err)
}
output, err := self.getRawBranches()
if err != nil {
panic(err)
@ -138,6 +144,11 @@ func (self *BranchLoader) obtainBranches() []*models.Branch {
return nil, false
}
if len(split[6]) > 0 && split[6] != currentDir {
// Ignore line because it is a branch checked out in a different worktree
return nil, false
}
return obtainBranch(split), true
})
}
@ -166,6 +177,7 @@ var branchFields = []string{
"upstream:track",
"subject",
fmt.Sprintf("objectname:short=%d", utils.COMMIT_HASH_SHORT_SIZE),
"worktreepath",
}
// Obtain branch information from parsed line output of getRawBranches()

View File

@ -0,0 +1,80 @@
package git_commands
import (
"os"
"path/filepath"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
)
type WorktreeLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
}
func NewWorktreeLoader(
common *common.Common,
cmd oscommands.ICmdObjBuilder,
) *WorktreeLoader {
return &WorktreeLoader{
Common: common,
cmd: cmd,
}
}
func (self *WorktreeLoader) GetWorktrees() ([]*models.Worktree, error) {
currentDir, err := os.Getwd()
if err != nil {
return nil, err
}
cmdArgs := NewGitCmd("worktree").Arg("list", "--porcelain", "-z").ToArgv()
worktreesOutput, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
if err != nil {
return nil, err
}
splitLines := strings.Split(worktreesOutput, "\x00")
var worktrees []*models.Worktree
var currentWorktree *models.Worktree
for _, splitLine := range splitLines {
if len(splitLine) == 0 && currentWorktree != nil {
worktrees = append(worktrees, currentWorktree)
currentWorktree = nil
continue
}
if strings.HasPrefix(splitLine, "worktree ") {
main := false
name := "main"
path := strings.SplitN(splitLine, " ", 2)[1]
if len(worktrees) == 0 {
main = true
} else {
name = filepath.Base(path)
}
currentWorktree = &models.Worktree{
Name: name,
Path: path,
Main: main,
Current: path == currentDir,
}
}
}
/*
worktree /Users/jbaranick/Source/lazygit
HEAD f6d6b5dec0432ffa953611700ab9b1ff0089f948
branch refs/heads/worktree_support
worktree /Users/jbaranick/Source/lazygit/.worktrees/worktree_tests
HEAD f6d6b5dec0432ffa953611700ab9b1ff0089f948
branch refs/heads/worktree_tests
*/
return worktrees, nil
}

View File

@ -0,0 +1,21 @@
package models
// Worktree : A git worktree
type Worktree struct {
Name string
Main bool
Current bool
Path string
}
func (w *Worktree) RefName() string {
return w.Name
}
func (w *Worktree) ID() string {
return w.RefName()
}
func (w *Worktree) Description() string {
return w.RefName()
}

View File

@ -11,6 +11,7 @@ const (
FILES_CONTEXT_KEY types.ContextKey = "files"
LOCAL_BRANCHES_CONTEXT_KEY types.ContextKey = "localBranches"
REMOTES_CONTEXT_KEY types.ContextKey = "remotes"
WORKTREES_CONTEXT_KEY types.ContextKey = "worktrees"
REMOTE_BRANCHES_CONTEXT_KEY types.ContextKey = "remoteBranches"
TAGS_CONTEXT_KEY types.ContextKey = "tags"
LOCAL_COMMITS_CONTEXT_KEY types.ContextKey = "commits"
@ -49,6 +50,7 @@ var AllContextKeys = []types.ContextKey{
FILES_CONTEXT_KEY,
LOCAL_BRANCHES_CONTEXT_KEY,
REMOTES_CONTEXT_KEY,
WORKTREES_CONTEXT_KEY,
REMOTE_BRANCHES_CONTEXT_KEY,
TAGS_CONTEXT_KEY,
LOCAL_COMMITS_CONTEXT_KEY,
@ -84,6 +86,7 @@ type ContextTree struct {
LocalCommits *LocalCommitsContext
CommitFiles *CommitFilesContext
Remotes *RemotesContext
Worktrees *WorktreesContext
Submodules *SubmodulesContext
RemoteBranches *RemoteBranchesContext
ReflogCommits *ReflogCommitsContext
@ -121,6 +124,7 @@ func (self *ContextTree) Flatten() []types.Context {
self.Files,
self.SubCommits,
self.Remotes,
self.Worktrees,
self.RemoteBranches,
self.Tags,
self.Branches,

View File

@ -29,6 +29,7 @@ func NewContextTree(c *ContextCommon) *ContextTree {
Submodules: NewSubmodulesContext(c),
Menu: NewMenuContext(c),
Remotes: NewRemotesContext(c),
Worktrees: NewWorktreesContext(c),
RemoteBranches: NewRemoteBranchesContext(c),
LocalCommits: NewLocalCommitsContext(c),
CommitFiles: commitFilesContext,

View File

@ -0,0 +1,52 @@
package context
import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type WorktreesContext struct {
*FilteredListViewModel[*models.Worktree]
*ListContextTrait
}
var _ types.IListContext = (*WorktreesContext)(nil)
func NewWorktreesContext(c *ContextCommon) *WorktreesContext {
viewModel := NewFilteredListViewModel(
func() []*models.Worktree { return c.Model().Worktrees },
func(Worktree *models.Worktree) []string {
return []string{Worktree.Name}
},
)
getDisplayStrings := func(startIdx int, length int) [][]string {
return presentation.GetWorktreeListDisplayStrings(c.Model().Worktrees)
}
return &WorktreesContext{
FilteredListViewModel: viewModel,
ListContextTrait: &ListContextTrait{
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
View: c.Views().Worktrees,
WindowName: "branches",
Key: WORKTREES_CONTEXT_KEY,
Kind: types.SIDE_CONTEXT,
Focusable: true,
})),
list: viewModel,
getDisplayStrings: getDisplayStrings,
c: c,
},
}
}
func (self *WorktreesContext) GetSelectedItemId() string {
item := self.GetSelected()
if item == nil {
return ""
}
return item.ID()
}

View File

@ -138,6 +138,7 @@ func (gui *Gui) resetHelpersAndControllers() {
common,
func(branches []*models.RemoteBranch) { gui.State.Model.RemoteBranches = branches },
)
worktreesController := controllers.NewWorktreesController(common)
undoController := controllers.NewUndoController(common)
globalController := controllers.NewGlobalController(common)
contextLinesController := controllers.NewContextLinesController(common)
@ -177,6 +178,7 @@ func (gui *Gui) resetHelpersAndControllers() {
for _, context := range []types.Context{
gui.State.Contexts.Status,
gui.State.Contexts.Remotes,
gui.State.Contexts.Worktrees,
gui.State.Contexts.Tags,
gui.State.Contexts.Branches,
gui.State.Contexts.RemoteBranches,
@ -298,6 +300,10 @@ func (gui *Gui) resetHelpersAndControllers() {
remotesController,
)
controllers.AttachControllers(gui.State.Contexts.Worktrees,
worktreesController,
)
controllers.AttachControllers(gui.State.Contexts.Stash,
stashController,
)

View File

@ -83,6 +83,7 @@ func (self *RefreshHelper) Refresh(options types.RefreshOptions) error {
types.REFLOG,
types.TAGS,
types.REMOTES,
types.WORKTREES,
types.STATUS,
types.BISECT_INFO,
types.STAGING,
@ -150,6 +151,10 @@ func (self *RefreshHelper) Refresh(options types.RefreshOptions) error {
refresh("remotes", func() { _ = self.refreshRemotes() })
}
if scopeSet.Includes(types.WORKTREES) {
refresh("worktrees", func() { _ = self.refreshWorktrees() })
}
if scopeSet.Includes(types.STAGING) {
refresh("staging", func() {
fileWg.Wait()
@ -197,6 +202,7 @@ func getScopeNames(scopes []types.RefreshableView) []string {
types.REFLOG: "reflog",
types.TAGS: "tags",
types.REMOTES: "remotes",
types.WORKTREES: "worktrees",
types.STATUS: "status",
types.BISECT_INFO: "bisect",
types.STAGING: "staging",
@ -589,6 +595,17 @@ func (self *RefreshHelper) refreshRemotes() error {
return nil
}
func (self *RefreshHelper) refreshWorktrees() error {
worktrees, err := self.c.Git().Loaders.Worktrees.GetWorktrees()
if err != nil {
return self.c.Error(err)
}
self.c.Model().Worktrees = worktrees
return self.c.PostRefreshUpdate(self.c.Contexts().Worktrees)
}
func (self *RefreshHelper) refreshStashEntries() error {
self.c.Model().StashEntries = self.c.Git().Loaders.StashLoader.
GetStashEntries(self.c.Modes().Filtering.GetPath())

View File

@ -0,0 +1,186 @@
package controllers
import (
"fmt"
"os"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/context"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
)
type WorktreesController struct {
baseController
c *ControllerCommon
}
var _ types.IController = &WorktreesController{}
func NewWorktreesController(
common *ControllerCommon,
) *WorktreesController {
return &WorktreesController{
baseController: baseController{},
c: common,
}
}
func (self *WorktreesController) GetKeybindings(opts types.KeybindingsOpts) []*types.Binding {
bindings := []*types.Binding{
{
Key: opts.GetKey(opts.Config.Universal.Select),
Handler: self.checkSelected(self.enter),
Description: self.c.Tr.EnterWorktree,
},
//{
// Key: opts.GetKey(opts.Config.Universal.Remove),
// Handler: self.withSelectedTag(self.delete),
// Description: self.c.Tr.LcDeleteTag,
//},
//{
// Key: opts.GetKey(opts.Config.Branches.PushTag),
// Handler: self.withSelectedTag(self.push),
// Description: self.c.Tr.LcPushTag,
//},
//{
// Key: opts.GetKey(opts.Config.Universal.New),
// Handler: self.create,
// Description: self.c.Tr.LcCreateTag,
//},
//{
// Key: opts.GetKey(opts.Config.Commits.ViewResetOptions),
// Handler: self.withSelectedTag(self.createResetMenu),
// Description: self.c.Tr.LcViewResetOptions,
// OpensMenu: true,
//},
}
return bindings
}
func (self *WorktreesController) GetOnRenderToMain() func() error {
return func() error {
var task types.UpdateTask
worktree := self.context().GetSelected()
if worktree == nil {
task = types.NewRenderStringTask("No worktrees")
} else {
task = types.NewRenderStringTask(fmt.Sprintf("%s\nPath: %s", style.FgGreen.Sprint(worktree.Name), worktree.Path))
}
return self.c.RenderToMainViews(types.RefreshMainOpts{
Pair: self.c.MainViewPairs().Normal,
Main: &types.ViewUpdateOpts{
Title: "Worktree",
Task: task,
},
})
}
}
//func (self *WorktreesController) switchToWorktree(worktree *models.Worktree) error {
// //self.c.LogAction(self.c.Tr.Actions.CheckoutTag)
// //if err := self.helpers.Refs.CheckoutRef(tag.Name, types.CheckoutRefOptions{}); err != nil {
// // return err
// //}
// //return self.c.PushContext(self.contexts.Branches)
//
// wd, err := os.Getwd()
// if err != nil {
// return err
// }
// gui.RepoPathStack.Push(wd)
//
// return gui.dispatchSwitchToRepo(submodule.Path, true)
//}
// func (self *WorktreesController) delete(tag *models.Tag) error {
// prompt := utils.ResolvePlaceholderString(
// self.c.Tr.DeleteTagPrompt,
// map[string]string{
// "tagName": tag.Name,
// },
// )
//
// return self.c.Confirm(types.ConfirmOpts{
// Title: self.c.Tr.DeleteTagTitle,
// Prompt: prompt,
// HandleConfirm: func() error {
// self.c.LogAction(self.c.Tr.Actions.DeleteTag)
// if err := self.git.Tag.Delete(tag.Name); err != nil {
// return self.c.Error(err)
// }
// return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC, Scope: []types.RefreshableView{types.COMMITS, types.TAGS}})
// },
// })
// }
//
// func (self *WorktreesController) push(tag *models.Tag) error {
// title := utils.ResolvePlaceholderString(
// self.c.Tr.PushTagTitle,
// map[string]string{
// "tagName": tag.Name,
// },
// )
//
// return self.c.Prompt(types.PromptOpts{
// Title: title,
// InitialContent: "origin",
// FindSuggestionsFunc: self.helpers.Suggestions.GetRemoteSuggestionsFunc(),
// HandleConfirm: func(response string) error {
// return self.c.WithWaitingStatus(self.c.Tr.PushingTagStatus, func() error {
// self.c.LogAction(self.c.Tr.Actions.PushTag)
// err := self.git.Tag.Push(response, tag.Name)
// if err != nil {
// _ = self.c.Error(err)
// }
//
// return nil
// })
// },
// })
// }
//
// func (self *WorktreesController) createResetMenu(tag *models.Tag) error {
// return self.helpers.Refs.CreateGitResetMenu(tag.Name)
// }
//
// func (self *WorktreesController) create() error {
// // leaving commit SHA blank so that we're just creating the tag for the current commit
// return self.helpers.Tags.CreateTagMenu("", func() { self.context().SetSelectedLineIdx(0) })
// }
func (self *WorktreesController) GetOnClick() func() error {
return self.checkSelected(self.enter)
}
func (self *WorktreesController) enter(worktree *models.Worktree) error {
wd, err := os.Getwd()
if err != nil {
return err
}
self.c.State().GetRepoPathStack().Push(wd)
return self.c.Helpers().Repos.DispatchSwitchToRepo(worktree.Path, true)
}
func (self *WorktreesController) checkSelected(callback func(worktree *models.Worktree) error) func() error {
return func() error {
worktree := self.context().GetSelected()
if worktree == nil {
return nil
}
return callback(worktree)
}
}
func (self *WorktreesController) Context() types.Context {
return self.context()
}
func (self *WorktreesController) context() *context.WorktreesContext {
return self.c.Contexts().Worktrees
}

View File

@ -569,6 +569,10 @@ func (gui *Gui) viewTabMap() map[string][]context.TabView {
Tab: gui.c.Tr.TagsTitle,
ViewName: "tags",
},
{
Tab: gui.c.Tr.WorktreesTitle,
ViewName: "worktrees",
},
},
"commits": {
{

View File

@ -14,6 +14,7 @@ var (
MERGE_COMMIT_ICON = "\U000f062d" // 󰘭
DEFAULT_REMOTE_ICON = "\uf02a2" // 󰊢
STASH_ICON = "\uf01c" // 
WORKTREE_ICON = "\uf02b" // 
)
var remoteIcons = map[string]string{
@ -68,3 +69,7 @@ func IconForRemote(remote *models.Remote) string {
func IconForStash(stash *models.StashEntry) string {
return STASH_ICON
}
func IconForWorktree(tag *models.Worktree) string {
return WORKTREE_ICON
}

View File

@ -0,0 +1,35 @@
package presentation
import (
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/presentation/icons"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/theme"
)
func GetWorktreeListDisplayStrings(worktrees []*models.Worktree) [][]string {
return slices.Map(worktrees, func(worktree *models.Worktree) []string {
return getWorktreeDisplayStrings(worktree)
})
}
// getWorktreeDisplayStrings returns the display string of branch
func getWorktreeDisplayStrings(w *models.Worktree) []string {
textStyle := theme.DefaultTextColor
current := ""
currentColor := style.FgCyan
if w.Current {
current = " *"
currentColor = style.FgGreen
}
res := make([]string, 0, 3)
res = append(res, currentColor.Sprint(current))
if icons.IsIconEnabled() {
res = append(res, textStyle.Sprint(icons.IconForWorktree(w)))
}
res = append(res, textStyle.Sprint(w.Name))
return res
}

View File

@ -201,6 +201,7 @@ type Model struct {
StashEntries []*models.StashEntry
SubCommits []*models.Commit
Remotes []*models.Remote
Worktrees []*models.Worktree
// FilteredReflogCommits are the ones that appear in the reflog panel.
// when in filtering mode we only include the ones that match the given path

View File

@ -13,6 +13,7 @@ const (
REFLOG
TAGS
REMOTES
WORKTREES
STATUS
SUBMODULES
STAGING

View File

@ -8,6 +8,7 @@ type Views struct {
Files *gocui.View
Branches *gocui.View
Remotes *gocui.View
Worktrees *gocui.View
Tags *gocui.View
RemoteBranches *gocui.View
ReflogCommits *gocui.View

View File

@ -29,6 +29,7 @@ func (gui *Gui) orderedViewNameMappings() []viewNameMapping {
{viewPtr: &gui.Views.Files, name: "files"},
{viewPtr: &gui.Views.Tags, name: "tags"},
{viewPtr: &gui.Views.Remotes, name: "remotes"},
{viewPtr: &gui.Views.Worktrees, name: "worktrees"},
{viewPtr: &gui.Views.Branches, name: "localBranches"},
{viewPtr: &gui.Views.RemoteBranches, name: "remoteBranches"},
{viewPtr: &gui.Views.ReflogCommits, name: "reflogCommits"},
@ -113,6 +114,8 @@ func (gui *Gui) createAllViews() error {
gui.Views.Remotes.Title = gui.c.Tr.RemotesTitle
gui.Views.Worktrees.Title = gui.c.Tr.WorktreesTitle
gui.Views.Tags.Title = gui.c.Tr.TagsTitle
gui.Views.Files.Title = gui.c.Tr.FilesTitle

View File

@ -200,6 +200,7 @@ type TranslationSet struct {
TagsTitle string
MenuTitle string
RemotesTitle string
WorktreesTitle string
RemoteBranchesTitle string
PatchBuildingTitle string
InformationTitle string
@ -541,6 +542,7 @@ type TranslationSet struct {
FilterPrefix string
ExitSearchMode string
ExitTextFilterMode string
EnterWorktree string
Actions Actions
Bisect Bisect
}
@ -897,6 +899,7 @@ func EnglishTranslationSet() TranslationSet {
TagsTitle: "Tags",
MenuTitle: "Menu",
RemotesTitle: "Remotes",
WorktreesTitle: "Worktrees",
RemoteBranchesTitle: "Remote branches",
PatchBuildingTitle: "Main panel (patch building)",
InformationTitle: "Information",
@ -1239,6 +1242,7 @@ func EnglishTranslationSet() TranslationSet {
SearchKeybindings: "%s: Next match, %s: Previous match, %s: Exit search mode",
SearchPrefix: "Search: ",
FilterPrefix: "Filter: ",
EnterWorktree: "Enter worktree",
Actions: Actions{
// TODO: combine this with the original keybinding descriptions (those are all in lowercase atm)
CheckoutCommit: "Checkout commit",