summaryrefslogtreecommitdiffstats
path: root/pkg/gui
diff options
context:
space:
mode:
authorJoel Baranick <joel.baranick@ensighten.com>2022-09-01 11:25:41 -0700
committerJesse Duffield <jessedduffield@gmail.com>2023-07-30 18:35:21 +1000
commitf8ba899b8734db7dcf3ae57cc34939db18a1a414 (patch)
treee909f4582fed5afbead1b0f0717eab1846869195 /pkg/gui
parent52447e5d46cedda76926dbee36b867674335d508 (diff)
Initial addition of support for worktrees
Diffstat (limited to 'pkg/gui')
-rw-r--r--pkg/gui/context/context.go4
-rw-r--r--pkg/gui/context/setup.go1
-rw-r--r--pkg/gui/context/worktrees_context.go52
-rw-r--r--pkg/gui/controllers.go6
-rw-r--r--pkg/gui/controllers/helpers/refresh_helper.go17
-rw-r--r--pkg/gui/controllers/worktrees_controller.go186
-rw-r--r--pkg/gui/gui.go4
-rw-r--r--pkg/gui/presentation/icons/git_icons.go5
-rw-r--r--pkg/gui/presentation/worktrees.go35
-rw-r--r--pkg/gui/types/common.go1
-rw-r--r--pkg/gui/types/refresh.go1
-rw-r--r--pkg/gui/types/views.go1
-rw-r--r--pkg/gui/views.go3
13 files changed, 316 insertions, 0 deletions
diff --git a/pkg/gui/context/context.go b/pkg/gui/context/context.go
index ab188d761..12fc285ae 100644
--- a/pkg/gui/context/context.go
+++ b/pkg/gui/context/context.go
@@ -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,
diff --git a/pkg/gui/context/setup.go b/pkg/gui/context/setup.go
index 775803884..ecb47d3f8 100644
--- a/pkg/gui/context/setup.go
+++ b/pkg/gui/context/setup.go
@@ -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,
diff --git a/pkg/gui/context/worktrees_context.go b/pkg/gui/context/worktrees_context.go
new file mode 100644
index 000000000..6ce8f8a80
--- /dev/null
+++ b/pkg/gui/context/worktrees_context.go
@@ -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()
+}
diff --git a/pkg/gui/controllers.go b/pkg/gui/controllers.go
index d2ee837ae..328620d48 100644
--- a/pkg/gui/controllers.go
+++ b/pkg/gui/controllers.go
@@ -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,
)
diff --git a/pkg/gui/controllers/helpers/refresh_helper.go b/pkg/gui/controllers/helpers/refresh_helper.go
index c8fbe8627..8431ae4cf 100644
--- a/pkg/gui/controllers/helpers/refresh_helper.go
+++ b/pkg/gui/controllers/helpers/refresh_helper.go
@@ -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())
diff --git a/pkg/gui/controllers/worktrees_controller.go b/pkg/gui/controllers/worktrees_controller.go
new file mode 100644
index 000000000..f3b5ed60b
--- /dev/null
+++ b/pkg/gui/controllers/worktrees_controller.go
@@ -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
+}
diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go
index 9a8d96712..0de209fd1 100644
--- a/pkg/gui/gui.go
+++ b/pkg/gui/gui.go
@@ -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": {
{
diff --git a/pkg/gui/presentation/icons/git_icons.go b/pkg/gui/presentation/icons/git_icons.go
index 6fd8bfb57..0111ca2b5 100644
--- a/pkg/gui/presentation/icons/git_icons.go
+++ b/pkg/gui/presentation/icons/git_icons.go
@@ -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
+}
diff --git a/pkg/gui/presentation/worktrees.go b/pkg/gui/presentation/worktrees.go
new file mode 100644
index 000000000..4bb778944
--- /dev/null
+++ b/pkg/gui/presentation/worktrees.go
@@ -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
+}
diff --git a/pkg/gui/types/common.go b/pkg/gui/types/common.go
index e485fbceb..b0944fb92 100644
--- a/pkg/gui/types/common.go
+++ b/pkg/gui/types/common.go
@@ -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
diff --git a/pkg/gui/types/refresh.go b/pkg/gui/types/refresh.go
index 6d6c6f8a4..552bfae04 100644
--- a/pkg/gui/types/refresh.go
+++ b/pkg/gui/types/refresh.go
@@ -13,6 +13,7 @@ const (
REFLOG
TAGS
REMOTES
+ WORKTREES
STATUS
SUBMODULES
STAGING
diff --git a/pkg/gui/types/views.go b/pkg/gui/types/views.go
index 8b8a62e61..69a79f8a0 100644
--- a/pkg/gui/types/views.go
+++ b/pkg/gui/types/views.go
@@ -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
diff --git a/pkg/gui/views.go b/pkg/gui/views.go
index 8567af797..594ca5fb3 100644
--- a/pkg/gui/views.go
+++ b/pkg/gui/views.go
@@ -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