diff options
author | Joel Baranick <joel.baranick@ensighten.com> | 2022-09-01 11:25:41 -0700 |
---|---|---|
committer | Jesse Duffield <jessedduffield@gmail.com> | 2023-07-30 18:35:21 +1000 |
commit | f8ba899b8734db7dcf3ae57cc34939db18a1a414 (patch) | |
tree | e909f4582fed5afbead1b0f0717eab1846869195 /pkg/gui | |
parent | 52447e5d46cedda76926dbee36b867674335d508 (diff) |
Initial addition of support for worktrees
Diffstat (limited to 'pkg/gui')
-rw-r--r-- | pkg/gui/context/context.go | 4 | ||||
-rw-r--r-- | pkg/gui/context/setup.go | 1 | ||||
-rw-r--r-- | pkg/gui/context/worktrees_context.go | 52 | ||||
-rw-r--r-- | pkg/gui/controllers.go | 6 | ||||
-rw-r--r-- | pkg/gui/controllers/helpers/refresh_helper.go | 17 | ||||
-rw-r--r-- | pkg/gui/controllers/worktrees_controller.go | 186 | ||||
-rw-r--r-- | pkg/gui/gui.go | 4 | ||||
-rw-r--r-- | pkg/gui/presentation/icons/git_icons.go | 5 | ||||
-rw-r--r-- | pkg/gui/presentation/worktrees.go | 35 | ||||
-rw-r--r-- | pkg/gui/types/common.go | 1 | ||||
-rw-r--r-- | pkg/gui/types/refresh.go | 1 | ||||
-rw-r--r-- | pkg/gui/types/views.go | 1 | ||||
-rw-r--r-- | pkg/gui/views.go | 3 |
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 |