summaryrefslogtreecommitdiffstats
path: root/pkg
diff options
context:
space:
mode:
authorJesse Duffield <jessedduffield@gmail.com>2019-11-18 09:38:36 +1100
committerJesse Duffield <jessedduffield@gmail.com>2019-11-21 22:07:14 +1100
commit3c1322914518168374be02a78cee968cf1d13730 (patch)
treed31c1064a1101a6081db025328f39789fc3b74c4 /pkg
parentcea24c2cf98c48e187900041d9e3bbeb93596019 (diff)
add tags panel
Diffstat (limited to 'pkg')
-rw-r--r--pkg/commands/commit.go8
-rw-r--r--pkg/commands/commit_list_builder.go1
-rw-r--r--pkg/commands/git.go18
-rw-r--r--pkg/commands/loading_tags.go78
-rw-r--r--pkg/commands/tag.go11
-rw-r--r--pkg/gui/branches_panel.go9
-rw-r--r--pkg/gui/commits_panel.go27
-rw-r--r--pkg/gui/gui.go9
-rw-r--r--pkg/gui/keybindings.go39
-rw-r--r--pkg/gui/list_view.go10
-rw-r--r--pkg/gui/remotes_panel.go8
-rw-r--r--pkg/gui/tags_panel.go149
-rw-r--r--pkg/gui/view_helpers.go2
-rw-r--r--pkg/i18n/english.go27
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:",
},
)
}