diff options
author | Jesse Duffield <jessedduffield@gmail.com> | 2019-11-18 09:38:36 +1100 |
---|---|---|
committer | Jesse Duffield <jessedduffield@gmail.com> | 2019-11-21 22:07:14 +1100 |
commit | 3c1322914518168374be02a78cee968cf1d13730 (patch) | |
tree | d31c1064a1101a6081db025328f39789fc3b74c4 | |
parent | cea24c2cf98c48e187900041d9e3bbeb93596019 (diff) |
add tags panel
-rw-r--r-- | pkg/commands/commit.go | 8 | ||||
-rw-r--r-- | pkg/commands/commit_list_builder.go | 1 | ||||
-rw-r--r-- | pkg/commands/git.go | 18 | ||||
-rw-r--r-- | pkg/commands/loading_tags.go | 78 | ||||
-rw-r--r-- | pkg/commands/tag.go | 11 | ||||
-rw-r--r-- | pkg/gui/branches_panel.go | 9 | ||||
-rw-r--r-- | pkg/gui/commits_panel.go | 27 | ||||
-rw-r--r-- | pkg/gui/gui.go | 9 | ||||
-rw-r--r-- | pkg/gui/keybindings.go | 39 | ||||
-rw-r--r-- | pkg/gui/list_view.go | 10 | ||||
-rw-r--r-- | pkg/gui/remotes_panel.go | 8 | ||||
-rw-r--r-- | pkg/gui/tags_panel.go | 149 | ||||
-rw-r--r-- | pkg/gui/view_helpers.go | 2 | ||||
-rw-r--r-- | pkg/i18n/english.go | 27 |
14 files changed, 390 insertions, 6 deletions
diff --git a/pkg/commands/commit.go b/pkg/commands/commit.go index 4102af4c0..5e4633c18 100644 --- a/pkg/commands/commit.go +++ b/pkg/commands/commit.go @@ -1,6 +1,8 @@ package commands import ( + "strings" + "github.com/fatih/color" "github.com/jesseduffield/lazygit/pkg/theme" "github.com/jesseduffield/lazygit/pkg/utils" @@ -14,6 +16,7 @@ type Commit struct { DisplayString string Action string // one of "", "pick", "edit", "squash", "reword", "drop", "fixup" Copied bool // to know if this commit is ready to be cherry-picked somewhere + Tags []string } // GetDisplayStrings is a function. @@ -52,9 +55,12 @@ func (c *Commit) GetDisplayStrings(isFocused bool) []string { } actionString := "" + tagString := "" if c.Action != "" { actionString = cyan.Sprint(utils.WithPadding(c.Action, 7)) + " " + } else if len(c.Tags) > 0 { + tagString = utils.ColoredString(strings.Join(c.Tags, " "), color.FgMagenta) + " " } - return []string{shaColor.Sprint(c.Sha), actionString + defaultColor.Sprint(c.Name)} + return []string{shaColor.Sprint(c.Sha), actionString + tagString + defaultColor.Sprint(c.Name)} } diff --git a/pkg/commands/commit_list_builder.go b/pkg/commands/commit_list_builder.go index aab6de6a3..48ae793b6 100644 --- a/pkg/commands/commit_list_builder.go +++ b/pkg/commands/commit_list_builder.go @@ -78,6 +78,7 @@ func (c *CommitListBuilder) GetCommits() ([]*Commit, error) { Name: strings.Join(splitLine[1:], " "), Status: status, DisplayString: strings.Join(splitLine, " "), + // TODO: add tags here }) } if rebaseMode != "" { diff --git a/pkg/commands/git.go b/pkg/commands/git.go index 76c753940..faa1493db 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -419,7 +419,7 @@ func (c *GitCommand) Push(branchName string, force bool, upstream string, ask fu setUpstreamArg = "--set-upstream " + upstream } - cmd := fmt.Sprintf("git push %s %s", forceFlag, setUpstreamArg) + cmd := fmt.Sprintf("git push --follow-tags %s %s", forceFlag, setUpstreamArg) return c.OSCommand.DetectUnamePass(cmd, ask) } @@ -1099,3 +1099,19 @@ func (c *GitCommand) RenameRemote(oldRemoteName string, newRemoteName string) er func (c *GitCommand) UpdateRemoteUrl(remoteName string, updatedUrl string) error { return c.OSCommand.RunCommand(fmt.Sprintf("git remote set-url %s %s", remoteName, updatedUrl)) } + +func (c *GitCommand) CreateLightweightTag(tagName string, commitSha string) error { + return c.OSCommand.RunCommand(fmt.Sprintf("git tag %s %s", tagName, commitSha)) +} + +func (c *GitCommand) ShowTag(tagName string) (string, error) { + return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git tag -n99 %s", tagName)) +} + +func (c *GitCommand) DeleteTag(tagName string) error { + return c.OSCommand.RunCommand(fmt.Sprintf("git tag -d %s", tagName)) +} + +func (c *GitCommand) PushTag(remoteName string, tagName string) error { + return c.OSCommand.RunCommand(fmt.Sprintf("git push %s %s", remoteName, tagName)) +} diff --git a/pkg/commands/loading_tags.go b/pkg/commands/loading_tags.go new file mode 100644 index 000000000..75a2d994d --- /dev/null +++ b/pkg/commands/loading_tags.go @@ -0,0 +1,78 @@ +package commands + +import ( + "regexp" + "sort" + "strings" + + "github.com/jesseduffield/lazygit/pkg/utils" +) + +const semverRegex = `v?((\d+\.?)+)([^\d]?.*)` + +func (c *GitCommand) GetTags() ([]*Tag, error) { + // get remote branches + remoteBranchesStr, err := c.OSCommand.RunCommandWithOutput("git tag --list") + if err != nil { + return nil, err + } + + content := utils.TrimTrailingNewline(remoteBranchesStr) + if content == "" { + return nil, nil + } + + split := strings.Split(content, "\n") + + // first step is to get our remotes from go-git + tags := make([]*Tag, len(split)) + for i, tagName := range split { + + tags[i] = &Tag{ + Name: tagName, + } + } + + // now lets sort our tags by name numerically + re := regexp.MustCompile(semverRegex) + + // the reason this is complicated is because we're both sorting alphabetically + // and when we're dealing with semver strings + sort.Slice(tags, func(i, j int) bool { + a := tags[i].Name + b := tags[j].Name + + matchA := re.FindStringSubmatch(a) + matchB := re.FindStringSubmatch(b) + + if len(matchA) > 0 && len(matchB) > 0 { + numbersA := strings.Split(matchA[1], ".") + numbersB := strings.Split(matchB[1], ".") + k := 0 + for { + if len(numbersA) == k && len(numbersB) == k { + break + } + if len(numbersA) == k { + return true + } + if len(numbersB) == k { + return false + } + if mustConvertToInt(numbersA[k]) < mustConvertToInt(numbersB[k]) { + return true + } + if mustConvertToInt(numbersA[k]) > mustConvertToInt(numbersB[k]) { + return false + } + k++ + } + + return strings.ToLower(matchA[3]) < strings.ToLower(matchB[3]) + } + + return strings.ToLower(a) < strings.ToLower(b) + }) + + return tags, nil +} diff --git a/pkg/commands/tag.go b/pkg/commands/tag.go new file mode 100644 index 000000000..99a4a7f0e --- /dev/null +++ b/pkg/commands/tag.go @@ -0,0 +1,11 @@ +package commands + +// Tag : A git tag +type Tag struct { + Name string +} + +// GetDisplayStrings returns the display string of a remote +func (r *Tag) GetDisplayStrings(isFocused bool) []string { + return []string{r.Name} +} diff --git a/pkg/gui/branches_panel.go b/pkg/gui/branches_panel.go index 790f24bd4..f78d86ab8 100644 --- a/pkg/gui/branches_panel.go +++ b/pkg/gui/branches_panel.go @@ -80,6 +80,10 @@ func (gui *Gui) refreshBranches(g *gocui.Gui) error { return err } + if err := gui.refreshTags(); err != nil { + return err + } + g.Update(func(g *gocui.Gui) error { builder, err := commands.NewBranchListBuilder(gui.Log, gui.GitCommand) if err != nil { @@ -373,7 +377,7 @@ func (gui *Gui) handleFastForward(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) onBranchesTabClick(tabIndex int) error { - contexts := []string{"local-branches", "remotes", "tabs"} + contexts := []string{"local-branches", "remotes", "tags"} branchesView := gui.getBranchesView() branchesView.TabIndex = tabIndex @@ -388,6 +392,7 @@ func (gui *Gui) switchBranchesPanelContext(context string) error { "local-branches": 0, "remotes": 1, "remote-branches": 1, + "tags": 2, } branchesView.TabIndex = contextTabIndexMap[context] @@ -399,6 +404,8 @@ func (gui *Gui) switchBranchesPanelContext(context string) error { return gui.renderRemotesWithSelection() case "remote-branches": return gui.renderRemoteBranchesWithSelection() + case "tags": + return gui.renderTagsWithSelection() } return nil diff --git a/pkg/gui/commits_panel.go b/pkg/gui/commits_panel.go index c29935f14..22ea6fe7e 100644 --- a/pkg/gui/commits_panel.go +++ b/pkg/gui/commits_panel.go @@ -584,3 +584,30 @@ func (gui *Gui) handleCreateCommitResetMenu(g *gocui.Gui, v *gocui.View) error { return gui.createMenu(fmt.Sprintf("%s %s", gui.Tr.SLocalize("resetTo"), commit.Sha), options, len(options), handleMenuPress) } + +func (gui *Gui) handleTagCommit(g *gocui.Gui, v *gocui.View) error { + // TODO: bring up menu asking if you want to make a lightweight or annotated tag + // if annotated, switch to a subprocess to create the message + + commit := gui.getSelectedCommit(g) + if commit == nil { + return nil + } + + return gui.handleCreateLightweightTag(commit.Sha) +} + +func (gui *Gui) handleCreateLightweightTag(commitSha string) error { + return gui.createPromptPanel(gui.g, gui.getCommitsView(), gui.Tr.SLocalize("TagNameTitle"), "", func(g *gocui.Gui, v *gocui.View) error { + if err := gui.GitCommand.CreateLightweightTag(v.Buffer(), commitSha); err != nil { + return gui.createErrorPanel(g, err.Error()) + } + if err := gui.refreshCommits(g); err != nil { + return gui.createErrorPanel(g, err.Error()) + } + if err := gui.refreshTags(); err != nil { + return gui.createErrorPanel(g, err.Error()) + } + return gui.handleCommitSelect(g, v) + }) +} diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index c08c82817..3b6f2fb11 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -120,6 +120,10 @@ type remoteBranchesState struct { SelectedLine int } +type tagsPanelState struct { + SelectedLine int +} + type commitPanelState struct { SelectedLine int SpecificDiffMode bool @@ -148,6 +152,7 @@ type panelStates struct { Branches *branchPanelState Remotes *remotePanelState RemoteBranches *remoteBranchesState + Tags *tagsPanelState Commits *commitPanelState Stash *stashPanelState Menu *menuPanelState @@ -166,6 +171,7 @@ type guiState struct { DiffEntries []*commands.Commit Remotes []*commands.Remote RemoteBranches []*commands.RemoteBranch + Tags []*commands.Tag MenuItemCount int // can't store the actual list because it's of interface{} type PreviousView string Platform commands.Platform @@ -198,6 +204,7 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *comma Branches: &branchPanelState{SelectedLine: 0}, Remotes: &remotePanelState{SelectedLine: 0}, RemoteBranches: &remoteBranchesState{SelectedLine: -1}, + Tags: &tagsPanelState{SelectedLine: -1}, Commits: &commitPanelState{SelectedLine: -1}, CommitFiles: &commitFilesPanelState{SelectedLine: -1}, Stash: &stashPanelState{SelectedLine: -1}, @@ -497,7 +504,7 @@ func (gui *Gui) layout(g *gocui.Gui) error { return err } branchesView.Title = gui.Tr.SLocalize("BranchesTitle") - branchesView.Tabs = []string{"Local Branches", "Remotes"} + branchesView.Tabs = []string{"Local Branches", "Remotes", "Tags"} branchesView.FgColor = textColor } diff --git a/pkg/gui/keybindings.go b/pkg/gui/keybindings.go index a64bb2965..2dfdedf61 100644 --- a/pkg/gui/keybindings.go +++ b/pkg/gui/keybindings.go @@ -403,6 +403,38 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Description: gui.Tr.SLocalize("FastForward"), }, { + ViewName: "branches", + Contexts: []string{"tags"}, + Key: gocui.KeySpace, + Modifier: gocui.ModNone, + Handler: gui.handleCheckoutTag, + Description: gui.Tr.SLocalize("checkout"), + }, + { + ViewName: "branches", + Contexts: []string{"tags"}, + Key: 'd', + Modifier: gocui.ModNone, + Handler: gui.handleDeleteTag, + Description: gui.Tr.SLocalize("deleteTag"), + }, + { + ViewName: "branches", + Contexts: []string{"tags"}, + Key: 'P', + Modifier: gocui.ModNone, + Handler: gui.handlePushTag, + Description: gui.Tr.SLocalize("pushTags"), + }, + { + ViewName: "branches", + Contexts: []string{"tags"}, + Key: 'n', + Modifier: gocui.ModNone, + Handler: gui.handleCreateTag, + Description: gui.Tr.SLocalize("createTag"), + }, + { ViewName: "branches", Key: ']', Modifier: gocui.ModNone, @@ -556,6 +588,13 @@ func (gui *Gui) GetInitialKeybindings() []*Binding { Description: gui.Tr.SLocalize("CommitsDiff"), }, { + ViewName: "commits", + Key: 'T', + Modifier: gocui.ModNone, + Handler: gui.handleTagCommit, + Description: gui.Tr.SLocalize("tagCommit"), + }, + { ViewName: "stash", Key: gocui.KeySpace, Modifier: gocui.ModNone, diff --git a/pkg/gui/list_view.go b/pkg/gui/list_view.go index 3a93bd526..4d72efc43 100644 --- a/pkg/gui/list_view.go +++ b/pkg/gui/list_view.go @@ -117,6 +117,16 @@ func (gui *Gui) getListViews() []*listView { rendersToMainView: true, }, { + viewName: "branches", + context: "tags", + getItemsLength: func() int { return len(gui.State.Tags) }, + getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.Tags.SelectedLine }, + handleFocus: gui.handleTagSelect, + handleItemSelect: gui.handleTagSelect, + gui: gui, + rendersToMainView: true, + }, + { viewName: "commits", getItemsLength: func() int { return len(gui.State.Commits) }, getSelectedLineIdxPtr: func() *int { return &gui.State.Panels.Commits.SelectedLine }, diff --git a/pkg/gui/remotes_panel.go b/pkg/gui/remotes_panel.go index bf9f8e110..90c2c8d59 100644 --- a/pkg/gui/remotes_panel.go +++ b/pkg/gui/remotes_panel.go @@ -35,6 +35,9 @@ func (gui *Gui) handleRemoteSelect(g *gocui.Gui, v *gocui.View) error { gui.getMainView().Title = "Remote" remote := gui.getSelectedRemote() + if remote == nil { + return gui.renderString(g, "main", "No remotes") + } if err := gui.focusPoint(0, gui.State.Panels.Remotes.SelectedLine, len(gui.State.Remotes), v); err != nil { return err } @@ -42,8 +45,6 @@ func (gui *Gui) handleRemoteSelect(g *gocui.Gui, v *gocui.View) error { return gui.renderString(g, "main", fmt.Sprintf("%s\nUrls:\n%s", utils.ColoredString(remote.Name, color.FgGreen), strings.Join(remote.Urls, "\n"))) } -// gui.refreshStatus is called at the end of this because that's when we can -// be sure there is a state.Remotes array to pick the current remote from func (gui *Gui) refreshRemotes() error { prevSelectedRemote := gui.getSelectedRemote() @@ -92,6 +93,9 @@ func (gui *Gui) renderRemotesWithSelection() error { func (gui *Gui) handleRemoteEnter(g *gocui.Gui, v *gocui.View) error { // naive implementation: get the branches and render them to the list, change the context remote := gui.getSelectedRemote() + if remote == nil { + return nil + } gui.State.RemoteBranches = remote.Branches diff --git a/pkg/gui/tags_panel.go b/pkg/gui/tags_panel.go new file mode 100644 index 000000000..e1f9d364a --- /dev/null +++ b/pkg/gui/tags_panel.go @@ -0,0 +1,149 @@ +package gui + +import ( + "fmt" + + "github.com/jesseduffield/gocui" + "github.com/jesseduffield/lazygit/pkg/commands" +) + +// list panel functions + +func (gui *Gui) getSelectedTag() *commands.Tag { + selectedLine := gui.State.Panels.Tags.SelectedLine + if selectedLine == -1 || len(gui.State.Tags) == 0 { + return nil + } + + return gui.State.Tags[selectedLine] +} + +func (gui *Gui) handleTagSelect(g *gocui.Gui, v *gocui.View) error { + if gui.popupPanelFocused() { + return nil + } + + gui.State.SplitMainPanel = false + + if _, err := gui.g.SetCurrentView(v.Name()); err != nil { + return err + } + + gui.getMainView().Title = "Tag" + + tag := gui.getSelectedTag() + if tag == nil { + return gui.renderString(g, "main", "No tags") + } + if err := gui.focusPoint(0, gui.State.Panels.Tags.SelectedLine, len(gui.State.Tags), v); err != nil { + return err + } + + go func() { + show, err := gui.GitCommand.ShowTag(tag.Name) + if err != nil { + show = "" + } + + graph, err := gui.GitCommand.GetBranchGraph(tag.Name) + if err != nil { + graph = "No graph for tag " + tag.Name + } + + _ = gui.renderString(g, "main", fmt.Sprintf("%s\n%s", show, graph)) + }() + + return nil +} + +func (gui *Gui) refreshTags() error { + tags, err := gui.GitCommand.GetTags() + if err != nil { + return gui.createErrorPanel(gui.g, err.Error()) + } + + gui.State.Tags = tags + + if gui.getBranchesView().Context == "tags" { + gui.renderTagsWithSelection() + } + + return nil +} + +func (gui *Gui) renderTagsWithSelection() error { + branchesView := gui.getBranchesView() + + gui.refreshSelectedLine(&gui.State.Panels.Tags.SelectedLine, len(gui.State.Tags)) + if err := gui.renderListPanel(branchesView, gui.State.Tags); err != nil { + return err + } + if err := gui.handleTagSelect(gui.g, branchesView); err != nil { + return err + } + + return nil +} + +func (gui *Gui) handleCheckoutTag(g *gocui.Gui, v *gocui.View) error { + tag := gui.getSelectedTag() + if tag == nil { + return nil + } + if err := gui.handleCheckoutBranch(tag.Name); err != nil { + return err + } + return gui.switchBranchesPanelContext("local-branches") +} + +func (gui *Gui) handleDeleteTag(g *gocui.Gui, v *gocui.View) error { + tag := gui.getSelectedTag() + if tag == nil { + return nil + } + + prompt := gui.Tr.TemplateLocalize( + "DeleteTagPrompt", + Teml{ + "tagName": tag.Name, + }, + ) + + return gui.createConfirmationPanel(gui.g, v, true, gui.Tr.SLocalize("DeleteTagTitle"), prompt, func(g *gocui.Gui, v *gocui.View) error { + if err := gui.GitCommand.DeleteTag(tag.Name); err != nil { + return gui.createErrorPanel(gui.g, err.Error()) + } + return gui.refreshTags() + }, nil) +} + +func (gui *Gui) handlePushTag(g *gocui.Gui, v *gocui.View) error { + tag := gui.getSelectedTag() + if tag == nil { + return nil + } + + title := gui.Tr.TemplateLocalize( + "PushTagTitle", + Teml{ + "tagName": tag.Name, + }, + ) + + return gui.createPromptPanel(gui.g, v, title, "origin", func(g *gocui.Gui, v *gocui.View) error { + if err := gui.GitCommand.PushTag(v.Buffer(), tag.Name); err != nil { + return gui.createErrorPanel(gui.g, err.Error()) + } + return gui.refreshTags() + }) +} + +func (gui *Gui) handleCreateTag(g *gocui.Gui, v *gocui.View) error { + return gui.createPromptPanel(gui.g, v, gui.Tr.SLocalize("CreateTagTitle"), "", func(g *gocui.Gui, v *gocui.View) error { + // leaving commit SHA blank so that we're just creating the tag for the current commit + if err := gui.GitCommand.CreateLightweightTag(v.Buffer(), ""); err != nil { + return gui.createErrorPanel(gui.g, err.Error()) + } + return gui.refreshTags() + }) +} diff --git a/pkg/gui/view_helpers.go b/pkg/gui/view_helpers.go index e671165a3..6112a5422 100644 --- a/pkg/gui/view_helpers.go +++ b/pkg/gui/view_helpers.go @@ -112,6 +112,8 @@ func (gui *Gui) newLineFocused(g *gocui.Gui, v *gocui.View) error { return gui.handleRemoteSelect(g, v) case "remote-branches": return gui.handleRemoteBranchSelect(g, v) + case "tags": + return gui.handleTagSelect(g, v) default: return errors.New("unknown branches panel context: " + branchesView.Context) } diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 3dae1106b..e1f53d05b 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -885,6 +885,33 @@ func addEnglish(i18nObject *i18n.Bundle) error { }, &i18n.Message{ ID: "editRemote", Other: "edit remote", + }, &i18n.Message{ + ID: "tagCommit", + Other: "tag commit", + }, &i18n.Message{ + ID: "TagNameTitle", + Other: "Tag name:", + }, &i18n.Message{ + ID: "deleteTag", + Other: "delete tag", + }, &i18n.Message{ + ID: "DeleteTagTitle", + Other: "Delete tag", + }, &i18n.Message{ + ID: "DeleteTagPrompt", + Other: "Are you sure you want to delete tag '{{.tagName}}'?", + }, &i18n.Message{ + ID: "PushTagTitle", + Other: "remote to push tag '{{.tagName}}' to:", + }, &i18n.Message{ + ID: "pushTags", + Other: "push tags", + }, &i18n.Message{ + ID: "createTag", + Other: "create tag", + }, &i18n.Message{ + ID: "CreateTagTitle", + Other: "Tag name:", }, ) } |