From de7ab80539a337252aae834ef10fda9574115446 Mon Sep 17 00:00:00 2001 From: Anthony HAMON Date: Fri, 10 Aug 2018 09:54:21 +0200 Subject: [PATCH 01/20] apply gofmt -s -w --- keybindings.go | 106 ++++++++++++++++++++++++------------------------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/keybindings.go b/keybindings.go index 0fb800d6e..07309790f 100644 --- a/keybindings.go +++ b/keybindings.go @@ -14,65 +14,65 @@ type Binding struct { func keybindings(g *gocui.Gui) error { bindings := []Binding{ - Binding{ViewName: "", Key: 'q', Modifier: gocui.ModNone, Handler: quit}, - Binding{ViewName: "", Key: gocui.KeyCtrlC, Modifier: gocui.ModNone, Handler: quit}, - Binding{ViewName: "", Key: gocui.KeyPgup, Modifier: gocui.ModNone, Handler: scrollUpMain}, - Binding{ViewName: "", Key: gocui.KeyPgdn, Modifier: gocui.ModNone, Handler: scrollDownMain}, - Binding{ViewName: "", Key: 'P', Modifier: gocui.ModNone, Handler: pushFiles}, - Binding{ViewName: "", Key: 'p', Modifier: gocui.ModNone, Handler: pullFiles}, - Binding{ViewName: "", Key: 'R', Modifier: gocui.ModNone, Handler: handleRefresh}, - Binding{ViewName: "files", Key: 'c', Modifier: gocui.ModNone, Handler: handleCommitPress}, - Binding{ViewName: "files", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleFilePress}, - Binding{ViewName: "files", Key: 'd', Modifier: gocui.ModNone, Handler: handleFileRemove}, - Binding{ViewName: "files", Key: 'm', Modifier: gocui.ModNone, Handler: handleSwitchToMerge}, - Binding{ViewName: "files", Key: 'e', Modifier: gocui.ModNone, Handler: handleFileEdit}, - Binding{ViewName: "files", Key: 'o', Modifier: gocui.ModNone, Handler: handleFileOpen}, - Binding{ViewName: "files", Key: 's', Modifier: gocui.ModNone, Handler: handleSublimeFileOpen}, - Binding{ViewName: "files", Key: 'v', Modifier: gocui.ModNone, Handler: handleVsCodeFileOpen}, - Binding{ViewName: "files", Key: 'i', Modifier: gocui.ModNone, Handler: handleIgnoreFile}, - Binding{ViewName: "files", Key: 'r', Modifier: gocui.ModNone, Handler: handleRefreshFiles}, - Binding{ViewName: "files", Key: 'S', Modifier: gocui.ModNone, Handler: handleStashSave}, - Binding{ViewName: "files", Key: 'a', Modifier: gocui.ModNone, Handler: handleAbortMerge}, - Binding{ViewName: "files", Key: 't', Modifier: gocui.ModNone, Handler: handleAddPatch}, - Binding{ViewName: "files", Key: 'D', Modifier: gocui.ModNone, Handler: handleResetHard}, - Binding{ViewName: "main", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: handleEscapeMerge}, - Binding{ViewName: "main", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handlePickHunk}, - Binding{ViewName: "main", Key: 'b', Modifier: gocui.ModNone, Handler: handlePickBothHunks}, - Binding{ViewName: "main", Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: handleSelectPrevConflict}, - Binding{ViewName: "main", Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: handleSelectNextConflict}, - Binding{ViewName: "main", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: handleSelectTop}, - Binding{ViewName: "main", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: handleSelectBottom}, - Binding{ViewName: "main", Key: 'h', Modifier: gocui.ModNone, Handler: handleSelectPrevConflict}, - Binding{ViewName: "main", Key: 'l', Modifier: gocui.ModNone, Handler: handleSelectNextConflict}, - Binding{ViewName: "main", Key: 'k', Modifier: gocui.ModNone, Handler: handleSelectTop}, - Binding{ViewName: "main", Key: 'j', Modifier: gocui.ModNone, Handler: handleSelectBottom}, - Binding{ViewName: "main", Key: 'z', Modifier: gocui.ModNone, Handler: handlePopFileSnapshot}, - Binding{ViewName: "branches", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleBranchPress}, - Binding{ViewName: "branches", Key: 'c', Modifier: gocui.ModNone, Handler: handleCheckoutByName}, - Binding{ViewName: "branches", Key: 'F', Modifier: gocui.ModNone, Handler: handleForceCheckout}, - Binding{ViewName: "branches", Key: 'n', Modifier: gocui.ModNone, Handler: handleNewBranch}, - Binding{ViewName: "branches", Key: 'm', Modifier: gocui.ModNone, Handler: handleMerge}, - Binding{ViewName: "commits", Key: 's', Modifier: gocui.ModNone, Handler: handleCommitSquashDown}, - Binding{ViewName: "commits", Key: 'r', Modifier: gocui.ModNone, Handler: handleRenameCommit}, - Binding{ViewName: "commits", Key: 'g', Modifier: gocui.ModNone, Handler: handleResetToCommit}, - Binding{ViewName: "stash", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleStashApply}, - Binding{ViewName: "stash", Key: 'g', Modifier: gocui.ModNone, Handler: handleStashPop}, - Binding{ViewName: "stash", Key: 'd', Modifier: gocui.ModNone, Handler: handleStashDrop}, + {ViewName: "", Key: 'q', Modifier: gocui.ModNone, Handler: quit}, + {ViewName: "", Key: gocui.KeyCtrlC, Modifier: gocui.ModNone, Handler: quit}, + {ViewName: "", Key: gocui.KeyPgup, Modifier: gocui.ModNone, Handler: scrollUpMain}, + {ViewName: "", Key: gocui.KeyPgdn, Modifier: gocui.ModNone, Handler: scrollDownMain}, + {ViewName: "", Key: 'P', Modifier: gocui.ModNone, Handler: pushFiles}, + {ViewName: "", Key: 'p', Modifier: gocui.ModNone, Handler: pullFiles}, + {ViewName: "", Key: 'R', Modifier: gocui.ModNone, Handler: handleRefresh}, + {ViewName: "files", Key: 'c', Modifier: gocui.ModNone, Handler: handleCommitPress}, + {ViewName: "files", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleFilePress}, + {ViewName: "files", Key: 'd', Modifier: gocui.ModNone, Handler: handleFileRemove}, + {ViewName: "files", Key: 'm', Modifier: gocui.ModNone, Handler: handleSwitchToMerge}, + {ViewName: "files", Key: 'e', Modifier: gocui.ModNone, Handler: handleFileEdit}, + {ViewName: "files", Key: 'o', Modifier: gocui.ModNone, Handler: handleFileOpen}, + {ViewName: "files", Key: 's', Modifier: gocui.ModNone, Handler: handleSublimeFileOpen}, + {ViewName: "files", Key: 'v', Modifier: gocui.ModNone, Handler: handleVsCodeFileOpen}, + {ViewName: "files", Key: 'i', Modifier: gocui.ModNone, Handler: handleIgnoreFile}, + {ViewName: "files", Key: 'r', Modifier: gocui.ModNone, Handler: handleRefreshFiles}, + {ViewName: "files", Key: 'S', Modifier: gocui.ModNone, Handler: handleStashSave}, + {ViewName: "files", Key: 'a', Modifier: gocui.ModNone, Handler: handleAbortMerge}, + {ViewName: "files", Key: 't', Modifier: gocui.ModNone, Handler: handleAddPatch}, + {ViewName: "files", Key: 'D', Modifier: gocui.ModNone, Handler: handleResetHard}, + {ViewName: "main", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: handleEscapeMerge}, + {ViewName: "main", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handlePickHunk}, + {ViewName: "main", Key: 'b', Modifier: gocui.ModNone, Handler: handlePickBothHunks}, + {ViewName: "main", Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: handleSelectPrevConflict}, + {ViewName: "main", Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: handleSelectNextConflict}, + {ViewName: "main", Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: handleSelectTop}, + {ViewName: "main", Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: handleSelectBottom}, + {ViewName: "main", Key: 'h', Modifier: gocui.ModNone, Handler: handleSelectPrevConflict}, + {ViewName: "main", Key: 'l', Modifier: gocui.ModNone, Handler: handleSelectNextConflict}, + {ViewName: "main", Key: 'k', Modifier: gocui.ModNone, Handler: handleSelectTop}, + {ViewName: "main", Key: 'j', Modifier: gocui.ModNone, Handler: handleSelectBottom}, + {ViewName: "main", Key: 'z', Modifier: gocui.ModNone, Handler: handlePopFileSnapshot}, + {ViewName: "branches", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleBranchPress}, + {ViewName: "branches", Key: 'c', Modifier: gocui.ModNone, Handler: handleCheckoutByName}, + {ViewName: "branches", Key: 'F', Modifier: gocui.ModNone, Handler: handleForceCheckout}, + {ViewName: "branches", Key: 'n', Modifier: gocui.ModNone, Handler: handleNewBranch}, + {ViewName: "branches", Key: 'm', Modifier: gocui.ModNone, Handler: handleMerge}, + {ViewName: "commits", Key: 's', Modifier: gocui.ModNone, Handler: handleCommitSquashDown}, + {ViewName: "commits", Key: 'r', Modifier: gocui.ModNone, Handler: handleRenameCommit}, + {ViewName: "commits", Key: 'g', Modifier: gocui.ModNone, Handler: handleResetToCommit}, + {ViewName: "stash", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleStashApply}, + {ViewName: "stash", Key: 'g', Modifier: gocui.ModNone, Handler: handleStashPop}, + {ViewName: "stash", Key: 'd', Modifier: gocui.ModNone, Handler: handleStashDrop}, } // Would make these keybindings global but that interferes with editing // input in the confirmation panel for _, viewName := range []string{"files", "branches", "commits", "stash"} { bindings = append(bindings, []Binding{ - Binding{ViewName: viewName, Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: nextView}, - Binding{ViewName: viewName, Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: previousView}, - Binding{ViewName: viewName, Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: nextView}, - Binding{ViewName: viewName, Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: cursorUp}, - Binding{ViewName: viewName, Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: cursorDown}, - Binding{ViewName: viewName, Key: 'h', Modifier: gocui.ModNone, Handler: previousView}, - Binding{ViewName: viewName, Key: 'l', Modifier: gocui.ModNone, Handler: nextView}, - Binding{ViewName: viewName, Key: 'k', Modifier: gocui.ModNone, Handler: cursorUp}, - Binding{ViewName: viewName, Key: 'j', Modifier: gocui.ModNone, Handler: cursorDown}, + {ViewName: viewName, Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: nextView}, + {ViewName: viewName, Key: gocui.KeyArrowLeft, Modifier: gocui.ModNone, Handler: previousView}, + {ViewName: viewName, Key: gocui.KeyArrowRight, Modifier: gocui.ModNone, Handler: nextView}, + {ViewName: viewName, Key: gocui.KeyArrowUp, Modifier: gocui.ModNone, Handler: cursorUp}, + {ViewName: viewName, Key: gocui.KeyArrowDown, Modifier: gocui.ModNone, Handler: cursorDown}, + {ViewName: viewName, Key: 'h', Modifier: gocui.ModNone, Handler: previousView}, + {ViewName: viewName, Key: 'l', Modifier: gocui.ModNone, Handler: nextView}, + {ViewName: viewName, Key: 'k', Modifier: gocui.ModNone, Handler: cursorUp}, + {ViewName: viewName, Key: 'j', Modifier: gocui.ModNone, Handler: cursorDown}, }...) } From d08241b2ea2f6cbf030be31c116502c92e55b1c7 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Fri, 10 Aug 2018 21:33:49 +1000 Subject: [PATCH 02/20] Obtain branches in a more robust way. Begin refactor work on gitcommands --- branch.go | 42 ++++++++ branch_list_builder.go | 122 ++++++++++++++++++++++ branches_panel.go | 4 +- colorer.go | 17 ++++ gitcommands.go | 158 +---------------------------- gui.go | 2 + main.go | 4 +- merge_panel.go | 2 +- {bin => scripts}/push_new_patch.go | 0 status_panel.go | 9 +- utils.go | 23 +++++ view_helpers.go | 16 --- 12 files changed, 221 insertions(+), 178 deletions(-) create mode 100644 branch.go create mode 100644 branch_list_builder.go create mode 100644 colorer.go rename {bin => scripts}/push_new_patch.go (100%) create mode 100644 utils.go diff --git a/branch.go b/branch.go new file mode 100644 index 000000000..382d25bbc --- /dev/null +++ b/branch.go @@ -0,0 +1,42 @@ +package main + +import ( + "strings" + + "github.com/fatih/color" +) + +// Branch : A git branch +type Branch struct { + Name string + Recency string +} + +func (b *Branch) getDisplayString() string { + return withPadding(b.Recency, 4) + coloredString(b.Name, b.getColor()) +} + +func (b *Branch) getColor() color.Attribute { + switch b.getType() { + case "feature": + return color.FgGreen + case "bugfix": + return color.FgYellow + case "hotfix": + return color.FgRed + default: + return color.FgWhite + } +} + +// expected to return feature/bugfix/hotfix or blank string +func (b *Branch) getType() string { + return strings.Split(b.Name, "/")[0] +} + +func withPadding(str string, padding int) string { + if padding-len(str) < 0 { + return str + } + return str + strings.Repeat(" ", padding-len(str)) +} diff --git a/branch_list_builder.go b/branch_list_builder.go new file mode 100644 index 000000000..fdd88c850 --- /dev/null +++ b/branch_list_builder.go @@ -0,0 +1,122 @@ +package main + +import ( + "regexp" + "strings" + + "gopkg.in/src-d/go-git.v4/plumbing" +) + +// context: +// we want to only show 'safe' branches (ones that haven't e.g. been deleted) +// which `git branch -a` gives us, but we also want the recency data that +// git reflog gives us. +// So we get the HEAD, then append get the reflog branches that intersect with +// our safe branches, then add the remaining safe branches, ensuring uniqueness +// along the way + +type branchListBuilder struct{} + +func newBranchListBuilder() *branchListBuilder { + return &branchListBuilder{} +} + +func (b *branchListBuilder) obtainCurrentBranch() Branch { + // Using git-go whenever possible + head, err := r.Head() + if err != nil { + panic(err) + } + name := head.Name().Short() + return Branch{Name: name, Recency: " *"} +} + +func (*branchListBuilder) obtainReflogBranches() []Branch { + branches := make([]Branch, 0) + rawString, err := runDirectCommand("git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD") + if err != nil { + return branches + } + + branchLines := splitLines(rawString) + for _, line := range branchLines { + timeNumber, timeUnit, branchName := branchInfoFromLine(line) + timeUnit = abbreviatedTimeUnit(timeUnit) + branch := Branch{Name: branchName, Recency: timeNumber + timeUnit} + branches = append(branches, branch) + } + return branches +} + +func (b *branchListBuilder) obtainSafeBranches() []Branch { + branches := make([]Branch, 0) + + bIter, err := r.Branches() + if err != nil { + panic(err) + } + err = bIter.ForEach(func(b *plumbing.Reference) error { + name := b.Name().Short() + branches = append(branches, Branch{Name: name}) + return nil + }) + + return branches +} + +func (b *branchListBuilder) appendNewBranches(finalBranches, newBranches, existingBranches []Branch, included bool) []Branch { + for _, newBranch := range newBranches { + if included == branchIncluded(newBranch.Name, existingBranches) { + finalBranches = append(finalBranches, newBranch) + } + + } + return finalBranches +} + +func (b *branchListBuilder) build() []Branch { + branches := make([]Branch, 0) + head := b.obtainCurrentBranch() + validBranches := b.obtainSafeBranches() + reflogBranches := uniqueByName(append(b.obtainReflogBranches(), head)) + + branches = b.appendNewBranches(branches, reflogBranches, validBranches, true) + branches = b.appendNewBranches(branches, validBranches, branches, false) + + return branches +} + +func uniqueByName(branches []Branch) []Branch { + finalBranches := make([]Branch, 0) + for _, branch := range branches { + if branchIncluded(branch.Name, finalBranches) { + continue + } + finalBranches = append(finalBranches, branch) + } + return finalBranches +} + +// A line will have the form '10 days ago master' so we need to strip out the +// useful information from that into timeNumber, timeUnit, and branchName +func branchInfoFromLine(line string) (string, string, string) { + r := regexp.MustCompile("\\|.*\\s") + line = r.ReplaceAllString(line, " ") + words := strings.Split(line, " ") + return words[0], words[1], words[3] +} + +func abbreviatedTimeUnit(timeUnit string) string { + r := regexp.MustCompile("s$") + timeUnit = r.ReplaceAllString(timeUnit, "") + timeUnitMap := map[string]string{ + "hour": "h", + "minute": "m", + "second": "s", + "week": "w", + "year": "y", + "day": "d", + "month": "m", + } + return timeUnitMap[timeUnit] +} diff --git a/branches_panel.go b/branches_panel.go index 0b8508a3d..3102f0c28 100644 --- a/branches_panel.go +++ b/branches_panel.go @@ -91,7 +91,7 @@ func handleBranchSelect(g *gocui.Gui, v *gocui.View) error { } go func() { branch := getSelectedBranch(v) - diff, err := getBranchGraph(branch.Name, branch.BaseBranch) + diff, err := getBranchGraph(branch.Name) if err != nil && strings.HasPrefix(diff, "fatal: ambiguous argument") { diff = "There is no tracking for this branch" } @@ -111,7 +111,7 @@ func refreshBranches(g *gocui.Gui) error { state.Branches = getGitBranches() v.Clear() for _, branch := range state.Branches { - fmt.Fprintln(v, branch.DisplayString) + fmt.Fprintln(v, branch.getDisplayString()) } resetOrigin(v) return refreshStatus(g) diff --git a/colorer.go b/colorer.go new file mode 100644 index 000000000..2b9142264 --- /dev/null +++ b/colorer.go @@ -0,0 +1,17 @@ +package main + +import ( + "fmt" + + "github.com/fatih/color" +) + +func coloredString(str string, colorAttribute color.Attribute) string { + colour := color.New(colorAttribute) + return coloredStringDirect(str, colour) +} + +// used for aggregating a few color attributes rather than just sending a single one +func coloredStringDirect(str string, colour *color.Color) string { + return colour.SprintFunc()(fmt.Sprint(str)) +} diff --git a/gitcommands.go b/gitcommands.go index fbe8ddae5..2e0ad011f 100644 --- a/gitcommands.go +++ b/gitcommands.go @@ -7,11 +7,9 @@ import ( "fmt" "os" "os/exec" - "regexp" "strings" "time" - "github.com/fatih/color" "github.com/jesseduffield/gocui" gitconfig "github.com/tcnksm/go-gitconfig" git "gopkg.in/src-d/go-git.v4" @@ -19,9 +17,6 @@ import ( ) var ( - // ErrNoCheckedOutBranch : When we have no checked out branch - ErrNoCheckedOutBranch = errors.New("No currently checked out branch") - // ErrNoOpenCommand : When we don't know which command to use to open a file ErrNoOpenCommand = errors.New("Unsure what command to use to open this file") ) @@ -38,14 +33,6 @@ type GitFile struct { DisplayString string } -// Branch : A git branch -type Branch struct { - Name string - Type string - BaseBranch string - DisplayString string -} - // Commit : A git commit type Commit struct { Sha string @@ -140,29 +127,6 @@ func branchStringParts(branchString string) (string, string) { return splitBranchName[0], splitBranchName[1] } -// branchPropertiesFromName : returns branch type, base, and color -func branchPropertiesFromName(name string) (string, string, color.Attribute) { - if strings.Contains(name, "feature/") { - return "feature", "develop", color.FgGreen - } else if strings.Contains(name, "bugfix/") { - return "bugfix", "develop", color.FgYellow - } else if strings.Contains(name, "hotfix/") { - return "hotfix", "master", color.FgRed - } - return "other", name, color.FgWhite -} - -func coloredString(str string, colour *color.Color) string { - return colour.SprintFunc()(fmt.Sprint(str)) -} - -func withPadding(str string, padding int) string { - if padding-len(str) < 0 { - return str - } - return str + strings.Repeat(" ", padding-len(str)) -} - // TODO: DRY up this function and getGitBranches func getGitStashEntries() []StashEntry { stashEntries := make([]StashEntry, 0) @@ -214,10 +178,6 @@ func getGitStatusFiles() []GitFile { Deleted: unstagedChange == "D" || stagedChange == "D", HasMergeConflicts: change == "UU", } - devLog("tracked", gitFile.Tracked) - devLog("hasUnstagedChanges", gitFile.HasUnstagedChanges) - devLog("HasStagedChanges", gitFile.HasStagedChanges) - devLog("DisplayString", gitFile.DisplayString) gitFiles = append(gitFiles, gitFile) } devLog(gitFiles) @@ -328,12 +288,8 @@ func runSubProcess(g *gocui.Gui, cmdName string, commandArgs ...string) { }) } -func getBranchGraph(branch string, baseBranch string) (string, error) { +func getBranchGraph(branch string) (string, error) { return runCommand("git log --graph --color --abbrev-commit --decorate --date=relative --pretty=medium -100 " + branch) - - // Leaving this guy commented out in case there's backlash from the design - // change and I want to make this configurable - // return runCommand("git log -p -30 --color --no-merges " + branch) } func verifyInGitRepo() { @@ -480,11 +436,7 @@ func gitPull() (string, error) { } func gitPush() (string, error) { - branchName := gitCurrentBranchName() - if branchName == "" { - return "", ErrNoCheckedOutBranch - } - return runDirectCommand("git push -u origin " + branchName) + return runDirectCommand("git push -u origin " + state.Branches[0].Name) } func gitSquashPreviousTwoCommits(message string) (string, error) { @@ -539,91 +491,12 @@ func gitCommitsToPush() []string { return splitLines(pushables) } -func gitCurrentBranchName() string { - branchName, err := runDirectCommand("git symbolic-ref --short HEAD") - // if there is an error, assume there are no branches yet - if err != nil { - return "" - } - return strings.TrimSpace(branchName) -} - -// A line will have the form '10 days ago master' so we need to strip out the -// useful information from that into timeNumber, timeUnit, and branchName -func branchInfoFromLine(line string) (string, string, string) { - r := regexp.MustCompile("\\|.*\\s") - line = r.ReplaceAllString(line, " ") - words := strings.Split(line, " ") - return words[0], words[1], words[3] -} - -func abbreviatedTimeUnit(timeUnit string) string { - r := regexp.MustCompile("s$") - timeUnit = r.ReplaceAllString(timeUnit, "") - timeUnitMap := map[string]string{ - "hour": "h", - "minute": "m", - "second": "s", - "week": "w", - "year": "y", - "day": "d", - "month": "m", - } - return timeUnitMap[timeUnit] -} - -func getBranches() []Branch { - branches := make([]Branch, 0) - rawString, err := runDirectCommand("git reflog -n100 --pretty='%cr|%gs' --grep-reflog='checkout: moving' HEAD") - if err != nil { - return branches - } - - branchLines := splitLines(rawString) - for i, line := range branchLines { - timeNumber, timeUnit, branchName := branchInfoFromLine(line) - timeUnit = abbreviatedTimeUnit(timeUnit) - - if branchAlreadyStored(branchName, branches) { - continue - } - - branch := constructBranch(timeNumber+timeUnit, branchName, i) - branches = append(branches, branch) - } - return branches -} - -func constructBranch(prefix, name string, index int) Branch { - branchType, branchBase, colourAttr := branchPropertiesFromName(name) - if index == 0 { - prefix = " *" - } - colour := color.New(colourAttr) - displayString := withPadding(prefix, 4) + coloredString(name, colour) - return Branch{ - Name: name, - Type: branchType, - BaseBranch: branchBase, - DisplayString: displayString, - } -} - func getGitBranches() []Branch { - // check if there are any branches - branchCheck, _ := runCommand("git branch") - if branchCheck == "" { - return []Branch{constructBranch("", gitCurrentBranchName(), 0)} - } - branches := getBranches() - if len(branches) == 0 { - branches = append(branches, constructBranch("", gitCurrentBranchName(), 0)) - } - branches = getAndMergeFetchedBranches(branches) - return branches + builder := newBranchListBuilder() + return builder.build() } -func branchAlreadyStored(branchName string, branches []Branch) bool { +func branchIncluded(branchName string, branches []Branch) bool { for _, existingBranch := range branches { if existingBranch.Name == branchName { return true @@ -632,27 +505,6 @@ func branchAlreadyStored(branchName string, branches []Branch) bool { return false } -// here branches contains all the branches that we've checked out, along with -// the recency. In this function we append the branches that are in our heads -// directory i.e. things we've fetched but haven't necessarily checked out. -// Worth mentioning this has nothing to do with the 'git merge' operation -func getAndMergeFetchedBranches(branches []Branch) []Branch { - rawString, err := runDirectCommand("git branch --sort=-committerdate --no-color") - if err != nil { - return branches - } - branchLines := splitLines(rawString) - for _, line := range branchLines { - line = strings.Replace(line, "* ", "", -1) - line = strings.TrimSpace(line) - if branchAlreadyStored(line, branches) { - continue - } - branches = append(branches, constructBranch("", line, len(branches))) - } - return branches -} - func gitResetHard() error { return w.Reset(&git.ResetOptions{Mode: git.HardReset}) } diff --git a/gui.go b/gui.go index 4d5d0b679..af1b07a93 100644 --- a/gui.go +++ b/gui.go @@ -261,6 +261,8 @@ func run() (err error) { } defer g.Close() + g.FgColor = gocui.ColorMagenta + goEvery(g, time.Second*60, fetch) goEvery(g, time.Second*10, refreshFiles) goEvery(g, time.Millisecond*10, updateLoader) diff --git a/main.go b/main.go index c918c7348..c36ca468b 100644 --- a/main.go +++ b/main.go @@ -28,6 +28,7 @@ var ( versionFlag = flag.Bool("v", false, "Print the current version") w *git.Worktree + r *git.Repository ) func homeDirectory() string { @@ -88,7 +89,8 @@ func fallbackVersion() string { } func setupWorktree() { - r, err := git.PlainOpen(".") + var err error + r, err = git.PlainOpen(".") if err != nil { panic(err) } diff --git a/merge_panel.go b/merge_panel.go index 6602dbbf1..f5ca12a23 100644 --- a/merge_panel.go +++ b/merge_panel.go @@ -56,7 +56,7 @@ func coloredConflictFile(content string, conflicts []conflict, conflictIndex int if i == conflict.end && len(remainingConflicts) > 0 { conflict, remainingConflicts = shiftConflict(remainingConflicts) } - outputBuffer.WriteString(coloredString(line, colour) + "\n") + outputBuffer.WriteString(coloredStringDirect(line, colour) + "\n") } return outputBuffer.String(), nil } diff --git a/bin/push_new_patch.go b/scripts/push_new_patch.go similarity index 100% rename from bin/push_new_patch.go rename to scripts/push_new_patch.go diff --git a/status_panel.go b/status_panel.go index 393c62344..46bc394ae 100644 --- a/status_panel.go +++ b/status_panel.go @@ -24,16 +24,15 @@ func refreshStatus(g *gocui.Gui) error { return err } if state.HasMergeConflicts { - colour := color.New(color.FgYellow) - fmt.Fprint(v, coloredString(" (merging)", colour)) + fmt.Fprint(v, coloredString(" (merging)", color.FgYellow)) } + if len(branches) == 0 { return nil } branch := branches[0] - // utilising the fact these all have padding to only grab the name - // from the display string with the existing coloring applied - fmt.Fprint(v, " "+branch.DisplayString[4:]) + name := coloredString(branch.Name, branch.getColor()) + fmt.Fprint(v, " "+name) return nil }) diff --git a/utils.go b/utils.go new file mode 100644 index 000000000..2b9671eb1 --- /dev/null +++ b/utils.go @@ -0,0 +1,23 @@ +package main + +import ( + "strings" + + "github.com/jesseduffield/gocui" +) + +func splitLines(multilineString string) []string { + multilineString = strings.Replace(multilineString, "\r", "", -1) + if multilineString == "" || multilineString == "\n" { + return make([]string, 0) + } + lines := strings.Split(multilineString, "\n") + if lines[len(lines)-1] == "" { + return lines[:len(lines)-1] + } + return lines +} + +func trimmedContent(v *gocui.View) string { + return strings.TrimSpace(v.Buffer()) +} diff --git a/view_helpers.go b/view_helpers.go index bb8b86da7..2f5f2caf0 100644 --- a/view_helpers.go +++ b/view_helpers.go @@ -121,10 +121,6 @@ func getItemPosition(v *gocui.View) int { return oy + cy } -func trimmedContent(v *gocui.View) string { - return strings.TrimSpace(v.Buffer()) -} - func cursorUp(g *gocui.Gui, v *gocui.View) error { // swallowing cursor movements in main // TODO: pull this out @@ -199,18 +195,6 @@ func renderString(g *gocui.Gui, viewName, s string) error { return nil } -func splitLines(multilineString string) []string { - multilineString = strings.Replace(multilineString, "\r", "", -1) - if multilineString == "" || multilineString == "\n" { - return make([]string, 0) - } - lines := strings.Split(multilineString, "\n") - if lines[len(lines)-1] == "" { - return lines[:len(lines)-1] - } - return lines -} - func optionsMapToString(optionsMap map[string]string) string { optionsArray := make([]string, 0) for key, description := range optionsMap { From c470c1f575eee8f8b3a7dc8a39297c32d6dc3abb Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Fri, 10 Aug 2018 21:38:51 +1000 Subject: [PATCH 03/20] keep asterisk at the checked out branch --- branch_list_builder.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/branch_list_builder.go b/branch_list_builder.go index fdd88c850..5393bea3e 100644 --- a/branch_list_builder.go +++ b/branch_list_builder.go @@ -78,7 +78,8 @@ func (b *branchListBuilder) build() []Branch { branches := make([]Branch, 0) head := b.obtainCurrentBranch() validBranches := b.obtainSafeBranches() - reflogBranches := uniqueByName(append(b.obtainReflogBranches(), head)) + reflogBranches := b.obtainReflogBranches() + reflogBranches = uniqueByName(append([]Branch{head}, reflogBranches...)) branches = b.appendNewBranches(branches, reflogBranches, validBranches, true) branches = b.appendNewBranches(branches, validBranches, branches, false) From 1fded005c4e63724ca2e5e7d4239db391834fc36 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Fri, 10 Aug 2018 21:40:21 +1000 Subject: [PATCH 04/20] revert to using default border color --- gui.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui.go b/gui.go index af1b07a93..4cf7d7df5 100644 --- a/gui.go +++ b/gui.go @@ -261,7 +261,7 @@ func run() (err error) { } defer g.Close() - g.FgColor = gocui.ColorMagenta + g.FgColor = gocui.ColorDefault goEvery(g, time.Second*60, fetch) goEvery(g, time.Second*10, refreshFiles) From eb9f01ecfa6bf509833ada9f1aef0700d54af393 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Fri, 10 Aug 2018 21:51:21 +1000 Subject: [PATCH 05/20] move withPadding into utils file --- branch.go | 7 ------- utils.go | 7 +++++++ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/branch.go b/branch.go index 382d25bbc..78c2e55aa 100644 --- a/branch.go +++ b/branch.go @@ -33,10 +33,3 @@ func (b *Branch) getColor() color.Attribute { func (b *Branch) getType() string { return strings.Split(b.Name, "/")[0] } - -func withPadding(str string, padding int) string { - if padding-len(str) < 0 { - return str - } - return str + strings.Repeat(" ", padding-len(str)) -} diff --git a/utils.go b/utils.go index 2b9671eb1..08d27806d 100644 --- a/utils.go +++ b/utils.go @@ -21,3 +21,10 @@ func splitLines(multilineString string) []string { func trimmedContent(v *gocui.View) string { return strings.TrimSpace(v.Buffer()) } + +func withPadding(str string, padding int) string { + if padding-len(str) < 0 { + return str + } + return str + strings.Repeat(" ", padding-len(str)) +} From 95b7c1d0a45503fc327f474714142bc0077c1a0c Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Fri, 10 Aug 2018 22:08:12 +1000 Subject: [PATCH 06/20] revert to using the direct git command for getting the current branch because it breaks if you've just done a git init --- branch_list_builder.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/branch_list_builder.go b/branch_list_builder.go index 5393bea3e..113a5a44a 100644 --- a/branch_list_builder.go +++ b/branch_list_builder.go @@ -22,13 +22,10 @@ func newBranchListBuilder() *branchListBuilder { } func (b *branchListBuilder) obtainCurrentBranch() Branch { - // Using git-go whenever possible - head, err := r.Head() - if err != nil { - panic(err) - } - name := head.Name().Short() - return Branch{Name: name, Recency: " *"} + // I used go-git for this, but that breaks if you've just done a git init, + // even though you're on 'master' + branchName, _ := runDirectCommand("git symbolic-ref --short HEAD") + return Branch{Name: strings.TrimSpace(branchName), Recency: " *"} } func (*branchListBuilder) obtainReflogBranches() []Branch { @@ -77,12 +74,15 @@ func (b *branchListBuilder) appendNewBranches(finalBranches, newBranches, existi func (b *branchListBuilder) build() []Branch { branches := make([]Branch, 0) head := b.obtainCurrentBranch() - validBranches := b.obtainSafeBranches() + safeBranches := b.obtainSafeBranches() + if len(safeBranches) == 0 { + return append(branches, head) + } reflogBranches := b.obtainReflogBranches() reflogBranches = uniqueByName(append([]Branch{head}, reflogBranches...)) - branches = b.appendNewBranches(branches, reflogBranches, validBranches, true) - branches = b.appendNewBranches(branches, validBranches, branches, false) + branches = b.appendNewBranches(branches, reflogBranches, safeBranches, true) + branches = b.appendNewBranches(branches, safeBranches, branches, false) return branches } From 28505dddaf221cdd05f3a044f58ced49f132a5e1 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Fri, 10 Aug 2018 22:09:10 +1000 Subject: [PATCH 07/20] move color functions into utils --- colorer.go | 17 ----------------- utils.go | 12 ++++++++++++ 2 files changed, 12 insertions(+), 17 deletions(-) delete mode 100644 colorer.go diff --git a/colorer.go b/colorer.go deleted file mode 100644 index 2b9142264..000000000 --- a/colorer.go +++ /dev/null @@ -1,17 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/fatih/color" -) - -func coloredString(str string, colorAttribute color.Attribute) string { - colour := color.New(colorAttribute) - return coloredStringDirect(str, colour) -} - -// used for aggregating a few color attributes rather than just sending a single one -func coloredStringDirect(str string, colour *color.Color) string { - return colour.SprintFunc()(fmt.Sprint(str)) -} diff --git a/utils.go b/utils.go index 08d27806d..1ab5a9ed2 100644 --- a/utils.go +++ b/utils.go @@ -1,8 +1,10 @@ package main import ( + "fmt" "strings" + "github.com/fatih/color" "github.com/jesseduffield/gocui" ) @@ -28,3 +30,13 @@ func withPadding(str string, padding int) string { } return str + strings.Repeat(" ", padding-len(str)) } + +func coloredString(str string, colorAttribute color.Attribute) string { + colour := color.New(colorAttribute) + return coloredStringDirect(str, colour) +} + +// used for aggregating a few color attributes rather than just sending a single one +func coloredStringDirect(str string, colour *color.Color) string { + return colour.SprintFunc()(fmt.Sprint(str)) +} From 59650cff2644e8e7f3b1c77ade4145c886aa6f73 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Fri, 10 Aug 2018 22:24:10 +1000 Subject: [PATCH 08/20] compare branches for name equality regardless of case --- gitcommands.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gitcommands.go b/gitcommands.go index 2e0ad011f..2d073aec9 100644 --- a/gitcommands.go +++ b/gitcommands.go @@ -498,7 +498,7 @@ func getGitBranches() []Branch { func branchIncluded(branchName string, branches []Branch) bool { for _, existingBranch := range branches { - if existingBranch.Name == branchName { + if strings.ToLower(existingBranch.Name) == strings.ToLower(branchName) { return true } } From 4853d186ee48e4769cb8102a2b4b8d801ea2537e Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Fri, 10 Aug 2018 22:46:04 +1000 Subject: [PATCH 09/20] add git plumbing to Gopkg.lock --- Gopkg.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/Gopkg.lock b/Gopkg.lock index 65f765807..f0eeb5c47 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -279,6 +279,7 @@ "github.com/jesseduffield/gocui", "github.com/tcnksm/go-gitconfig", "gopkg.in/src-d/go-git.v4", + "gopkg.in/src-d/go-git.v4/plumbing", "gopkg.in/src-d/go-git.v4/plumbing/object", ] solver-name = "gps-cdcl" From aa4160d57ae460a7735fb2beb2ab7904151de50e Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Fri, 10 Aug 2018 22:49:54 +1000 Subject: [PATCH 10/20] merge feature/clearing-commit-panel into master --- branches_panel.go | 4 ++-- commits_panel.go | 2 +- confirmation_panel.go | 17 ++++++++++++++--- files_panel.go | 7 ++++--- stash_panel.go | 2 +- 5 files changed, 22 insertions(+), 10 deletions(-) diff --git a/branches_panel.go b/branches_panel.go index 3102f0c28..00091ffb8 100644 --- a/branches_panel.go +++ b/branches_panel.go @@ -30,7 +30,7 @@ func handleForceCheckout(g *gocui.Gui, v *gocui.View) error { } func handleCheckoutByName(g *gocui.Gui, v *gocui.View) error { - createPromptPanel(g, v, "Branch Name:", func(g *gocui.Gui, v *gocui.View) error { + createPromptPanel(g, v, "Branch Name:", nil, func(g *gocui.Gui, v *gocui.View) error { if output, err := gitCheckout(trimmedContent(v), false); err != nil { return createErrorPanel(g, output) } @@ -41,7 +41,7 @@ func handleCheckoutByName(g *gocui.Gui, v *gocui.View) error { func handleNewBranch(g *gocui.Gui, v *gocui.View) error { branch := state.Branches[0] - createPromptPanel(g, v, "New Branch Name (Branch is off of "+branch.Name+")", func(g *gocui.Gui, v *gocui.View) error { + createPromptPanel(g, v, "New Branch Name (Branch is off of "+branch.Name+")", nil, func(g *gocui.Gui, v *gocui.View) error { if output, err := gitNewBranch(trimmedContent(v)); err != nil { return createErrorPanel(g, output) } diff --git a/commits_panel.go b/commits_panel.go index 88c7b8ebe..e8ff68678 100644 --- a/commits_panel.go +++ b/commits_panel.go @@ -109,7 +109,7 @@ func handleRenameCommit(g *gocui.Gui, v *gocui.View) error { if getItemPosition(v) != 0 { return createErrorPanel(g, "Can only rename topmost commit") } - createPromptPanel(g, v, "Rename Commit", func(g *gocui.Gui, v *gocui.View) error { + createPromptPanel(g, v, "Rename Commit", nil, func(g *gocui.Gui, v *gocui.View) error { if output, err := gitRenameCommit(v.Buffer()); err != nil { return createErrorPanel(g, output) } diff --git a/confirmation_panel.go b/confirmation_panel.go index 362b75c49..520c8b593 100644 --- a/confirmation_panel.go +++ b/confirmation_panel.go @@ -55,7 +55,7 @@ func getConfirmationPanelDimensions(g *gocui.Gui, prompt string) (int, int, int, height/2 + panelHeight/2 } -func createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, handleConfirm func(*gocui.Gui, *gocui.View) error) error { +func createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, initialValue *[]byte, handleConfirm func(*gocui.Gui, *gocui.View) error) error { // only need to fit one line x0, y0, x1, y1 := getConfirmationPanelDimensions(g, "") if confirmationView, err := g.SetView("confirmation", x0, y0, x1, y1, 0); err != nil { @@ -65,11 +65,22 @@ func createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, hand g.Cursor = true + handleConfirmAndClear := func(gui *gocui.Gui, view *gocui.View) error { + *initialValue = nil + return handleConfirm(g, view) + } + + handleClose := func(gui *gocui.Gui, view *gocui.View) error { + *initialValue = []byte(strings.TrimSpace(view.Buffer())) + return nil + } + confirmationView.Editable = true confirmationView.Title = title - confirmationView.FgColor = gocui.ColorWhite + confirmationView.Write(*initialValue) + confirmationView.SetCursor(len(*initialValue), 0) switchFocus(g, currentView, confirmationView) - return setKeyBindings(g, handleConfirm, nil) + return setKeyBindings(g, handleConfirmAndClear, handleClose) } return nil } diff --git a/files_panel.go b/files_panel.go index 32ed36ad4..b2de2c226 100644 --- a/files_panel.go +++ b/files_panel.go @@ -15,8 +15,9 @@ import ( ) var ( - errNoFiles = errors.New("No changed files") - errNoUsername = errors.New(`No username set. Please do: git config --global user.name "Your Name"`) + savedCommitMessage = &[]byte{} + errNoFiles = errors.New("No changed files") + errNoUsername = errors.New(`No username set. Please do: git config --global user.name "Your Name"`) ) func stagedFiles(files []GitFile) []GitFile { @@ -177,7 +178,7 @@ func handleCommitPress(g *gocui.Gui, filesView *gocui.View) error { if len(stagedFiles(state.GitFiles)) == 0 && !state.HasMergeConflicts { return createErrorPanel(g, "There are no staged files to commit") } - createPromptPanel(g, filesView, "Commit message", func(g *gocui.Gui, v *gocui.View) error { + createPromptPanel(g, filesView, "Commit message", savedCommitMessage, func(g *gocui.Gui, v *gocui.View) error { message := trimmedContent(v) if message == "" { return createErrorPanel(g, "You cannot commit without a commit message") diff --git a/stash_panel.go b/stash_panel.go index 33c7e297b..a4a2207d8 100644 --- a/stash_panel.go +++ b/stash_panel.go @@ -82,7 +82,7 @@ func stashDo(g *gocui.Gui, v *gocui.View, method string) error { } func handleStashSave(g *gocui.Gui, filesView *gocui.View) error { - createPromptPanel(g, filesView, "Stash changes", func(g *gocui.Gui, v *gocui.View) error { + createPromptPanel(g, filesView, "Stash changes", nil, func(g *gocui.Gui, v *gocui.View) error { if output, err := gitStashSave(trimmedContent(v)); err != nil { createErrorPanel(g, output) } From f20121cb0bca9355d6a08d8d8153b2a34ccf5120 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Fri, 10 Aug 2018 23:08:03 +1000 Subject: [PATCH 11/20] add FIXME note about trimming the trailing newline upon confirming in a prompt panel --- confirmation_panel.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/confirmation_panel.go b/confirmation_panel.go index 520c8b593..7ae55eaf9 100644 --- a/confirmation_panel.go +++ b/confirmation_panel.go @@ -71,6 +71,8 @@ func createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, init } handleClose := func(gui *gocui.Gui, view *gocui.View) error { + // FIXME: trimming a newline that is no doubt caused by the enter keybinding + // on the editor. We should just define a new editor that doesn't do that *initialValue = []byte(strings.TrimSpace(view.Buffer())) return nil } From 7eb673e574debea04f46bfc50d4a2fdf9fa6aebe Mon Sep 17 00:00:00 2001 From: Glenn Vriesman Date: Fri, 10 Aug 2018 15:10:15 +0200 Subject: [PATCH 12/20] Feature: Added repository name to status --- status_panel.go | 3 ++- utils.go | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/status_panel.go b/status_panel.go index 46bc394ae..f3fcb8078 100644 --- a/status_panel.go +++ b/status_panel.go @@ -32,7 +32,8 @@ func refreshStatus(g *gocui.Gui) error { } branch := branches[0] name := coloredString(branch.Name, branch.getColor()) - fmt.Fprint(v, " "+name) + repo := getCurrentProject() + fmt.Fprint(v, " "+repo+" → "+name) return nil }) diff --git a/utils.go b/utils.go index 1ab5a9ed2..e2de46233 100644 --- a/utils.go +++ b/utils.go @@ -2,6 +2,9 @@ package main import ( "fmt" + "log" + "os" + "path/filepath" "strings" "github.com/fatih/color" @@ -40,3 +43,12 @@ func coloredString(str string, colorAttribute color.Attribute) string { func coloredStringDirect(str string, colour *color.Color) string { return colour.SprintFunc()(fmt.Sprint(str)) } + +// used to get the project name +func getCurrentProject() string { + pwd, err := os.Getwd() + if err != nil { + log.Fatalln(err.Error()) + } + return filepath.Base(pwd) +} From 7aa884ed8f19eacb03a5ee28baba989783746706 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Fri, 10 Aug 2018 23:36:54 +1000 Subject: [PATCH 13/20] step one on restoring multiline commits --- confirmation_panel.go | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/confirmation_panel.go b/confirmation_panel.go index 7ae55eaf9..c82b6b65b 100644 --- a/confirmation_panel.go +++ b/confirmation_panel.go @@ -56,6 +56,9 @@ func getConfirmationPanelDimensions(g *gocui.Gui, prompt string) (int, int, int, } func createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, initialValue *[]byte, handleConfirm func(*gocui.Gui, *gocui.View) error) error { + if initialValue == nil { + initialValue = &[]byte{} + } // only need to fit one line x0, y0, x1, y1 := getConfirmationPanelDimensions(g, "") if confirmationView, err := g.SetView("confirmation", x0, y0, x1, y1, 0); err != nil { @@ -79,14 +82,29 @@ func createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, init confirmationView.Editable = true confirmationView.Title = title - confirmationView.Write(*initialValue) - confirmationView.SetCursor(len(*initialValue), 0) + restorePreviousBuffer(confirmationView, initialValue) switchFocus(g, currentView, confirmationView) return setKeyBindings(g, handleConfirmAndClear, handleClose) } return nil } +func restorePreviousBuffer(confirmationView *gocui.View, initialValue *[]byte) { + confirmationView.Write(*initialValue) + x, y := getCursorPositionFromBuffer(initialValue) + devLog("New cursor position:", x, y) + confirmationView.SetCursor(0, 0) + confirmationView.MoveCursor(x, y, false) +} + +func getCursorPositionFromBuffer(initialValue *[]byte) (int, int) { + split := strings.Split(string(*initialValue), "\n") + lastLine := split[len(split)-1] + x := len(lastLine) + y := len(split) + return x, y +} + func createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error { g.Update(func(g *gocui.Gui) error { // delete the existing confirmation panel if it exists From 1ef794e09f2bbdee7364a1738801e1a4eee78421 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sat, 11 Aug 2018 09:43:22 +1000 Subject: [PATCH 14/20] Update README.md --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 0bd9b310f..9449977cd 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ Are YOU tired of typing every git command directly into the terminal, but you're ![Gif](https://image.ibb.co/mmeXho/optimisedgif.gif) +[Twitch Stream](https://www.twitch.tv/jesseduffield) + ## Installation ### Homebrew @@ -74,3 +76,6 @@ We love your input! Please check out the [contributing guide](CONTRIBUTING.md). ## Work in progress This is still a work in progress so there's still bugs to iron out and as this is my first project in Go the code could no doubt use an increase in quality, but I'll be improving on it whenever I find the time. If you have any feedback feel free to [raise an issue](https://github.com/jesseduffield/lazygit/issues)/[submit a PR](https://github.com/jesseduffield/lazygit/pulls). + +## Social +If you want to see what I (Jesse) am up to in terms of development, follow me on [twitter](https://twitter.com/DuffieldJesse) or watch me program on [twitch](https://www.twitch.tv/jesseduffield) From b2fbccd39233feddf5543dd57c6f04533895cdb2 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sat, 11 Aug 2018 12:35:17 +1000 Subject: [PATCH 15/20] remove time logging --- ZHgalGrWSF | 1 + gitcommands.go | 4 ---- 2 files changed, 1 insertion(+), 4 deletions(-) create mode 100644 ZHgalGrWSF diff --git a/ZHgalGrWSF b/ZHgalGrWSF new file mode 100644 index 000000000..f1f0d2371 --- /dev/null +++ b/ZHgalGrWSF @@ -0,0 +1 @@ +GaUMygWjJa \ No newline at end of file diff --git a/gitcommands.go b/gitcommands.go index 2d073aec9..7c1ec84bd 100644 --- a/gitcommands.go +++ b/gitcommands.go @@ -107,13 +107,11 @@ func mergeGitStatusFiles(oldGitFiles, newGitFiles []GitFile) []GitFile { } func runDirectCommand(command string) (string, error) { - timeStart := time.Now() commandLog(command) cmdOut, err := exec. Command(state.Platform.shell, state.Platform.shellArg, command). CombinedOutput() - devLog("run direct command time for command: ", command, time.Now().Sub(timeStart)) return sanitisedCommandOutput(cmdOut, err) } @@ -218,12 +216,10 @@ func sanitisedCommandOutput(output []byte, err error) (string, error) { } func runCommand(command string) (string, error) { - commandStartTime := time.Now() commandLog(command) splitCmd := strings.Split(command, " ") devLog(splitCmd) cmdOut, err := exec.Command(splitCmd[0], splitCmd[1:]...).CombinedOutput() - devLog("run command time: ", time.Now().Sub(commandStartTime)) return sanitisedCommandOutput(cmdOut, err) } From 3b018e040fa023bf64f84e5f57ed7f5440b94830 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sat, 11 Aug 2018 13:17:20 +1000 Subject: [PATCH 16/20] make commit messages multilined and add ability save commit message between edits --- commit_message_panel.go | 42 +++++++++++++++++++++++++++++++++++++++++ confirmation_panel.go | 21 +++++++++++++-------- files_panel.go | 23 +++++++--------------- gitcommands.go | 1 - gui.go | 17 ++++++++++++++--- keybindings.go | 3 +++ view_helpers.go | 18 ++++++++++++++++++ 7 files changed, 97 insertions(+), 28 deletions(-) create mode 100644 commit_message_panel.go diff --git a/commit_message_panel.go b/commit_message_panel.go new file mode 100644 index 000000000..7ff609976 --- /dev/null +++ b/commit_message_panel.go @@ -0,0 +1,42 @@ +package main + +import "github.com/jesseduffield/gocui" + +func handleCommitConfirm(g *gocui.Gui, v *gocui.View) error { + message := trimmedContent(v) + if message == "" { + return createErrorPanel(g, "You cannot commit without a commit message") + } + if output, err := gitCommit(g, message); err != nil { + if err == errNoUsername { + return createErrorPanel(g, err.Error()) + } + return createErrorPanel(g, output) + } + refreshFiles(g) + g.SetViewOnBottom("commitMessage") + return refreshCommits(g) +} + +func handleCommitClose(g *gocui.Gui, v *gocui.View) error { + g.SetViewOnBottom("commitMessage") + return switchFocus(g, v, getFilesView(g)) +} + +func handleNewlineCommitMessage(g *gocui.Gui, v *gocui.View) error { + // resising ahead of time so that the top line doesn't get hidden to make + // room for the cursor on the second line + x0, y0, x1, y1 := getConfirmationPanelDimensions(g, v.Buffer()) + if _, err := g.SetView("commitMessage", x0, y0, x1, y1+1, 0); err != nil { + if err != gocui.ErrUnknownView { + return err + } + } + + v.EditNewLine() + return nil +} + +func handleCommitFocused(g *gocui.Gui, v *gocui.View) error { + return renderString(g, "options", "esc: close, enter: confirm") +} diff --git a/confirmation_panel.go b/confirmation_panel.go index c82b6b65b..03e078155 100644 --- a/confirmation_panel.go +++ b/confirmation_panel.go @@ -56,6 +56,7 @@ func getConfirmationPanelDimensions(g *gocui.Gui, prompt string) (int, int, int, } func createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, initialValue *[]byte, handleConfirm func(*gocui.Gui, *gocui.View) error) error { + g.SetViewOnBottom("commitMessage") if initialValue == nil { initialValue = &[]byte{} } @@ -66,9 +67,7 @@ func createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, init return err } - g.Cursor = true - - handleConfirmAndClear := func(gui *gocui.Gui, view *gocui.View) error { + handleConfirm := func(gui *gocui.Gui, view *gocui.View) error { *initialValue = nil return handleConfirm(g, view) } @@ -84,7 +83,7 @@ func createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, init confirmationView.Title = title restorePreviousBuffer(confirmationView, initialValue) switchFocus(g, currentView, confirmationView) - return setKeyBindings(g, handleConfirmAndClear, handleClose) + return setKeyBindings(g, handleConfirm, handleClose) } return nil } @@ -106,6 +105,7 @@ func getCursorPositionFromBuffer(initialValue *[]byte) (int, int) { } func createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error { + g.SetViewOnBottom("commitMessage") g.Update(func(g *gocui.Gui) error { // delete the existing confirmation panel if it exists if view, _ := g.View("confirmation"); view != nil { @@ -172,15 +172,20 @@ func trimTrailingNewline(str string) string { return str } -func resizeConfirmationPanel(g *gocui.Gui) error { +func resizeConfirmationPanel(g *gocui.Gui, viewName string) error { // If the confirmation panel is already displayed, just resize the width, // otherwise continue - if v, err := g.View("confirmation"); err == nil { + g.Update(func(g *gocui.Gui) error { + v, err := g.View(viewName) + if err != nil { + return nil + } content := trimTrailingNewline(v.Buffer()) x0, y0, x1, y1 := getConfirmationPanelDimensions(g, content) - if _, err = g.SetView("confirmation", x0, y0, x1, y1, 0); err != nil { + if _, err := g.SetView(viewName, x0, y0, x1, y1, 0); err != nil { return err } - } + return nil + }) return nil } diff --git a/files_panel.go b/files_panel.go index b2de2c226..75e20885f 100644 --- a/files_panel.go +++ b/files_panel.go @@ -15,9 +15,8 @@ import ( ) var ( - savedCommitMessage = &[]byte{} - errNoFiles = errors.New("No changed files") - errNoUsername = errors.New(`No username set. Please do: git config --global user.name "Your Name"`) + errNoFiles = errors.New("No changed files") + errNoUsername = errors.New(`No username set. Please do: git config --global user.name "Your Name"`) ) func stagedFiles(files []GitFile) []GitFile { @@ -178,19 +177,11 @@ func handleCommitPress(g *gocui.Gui, filesView *gocui.View) error { if len(stagedFiles(state.GitFiles)) == 0 && !state.HasMergeConflicts { return createErrorPanel(g, "There are no staged files to commit") } - createPromptPanel(g, filesView, "Commit message", savedCommitMessage, func(g *gocui.Gui, v *gocui.View) error { - message := trimmedContent(v) - if message == "" { - return createErrorPanel(g, "You cannot commit without a commit message") - } - if output, err := gitCommit(g, message); err != nil { - if err == errNoUsername { - return createErrorPanel(g, err.Error()) - } - return createErrorPanel(g, output) - } - refreshFiles(g) - return refreshCommits(g) + commitMessageView := getCommitMessageView(g) + g.Update(func(g *gocui.Gui) error { + g.SetViewOnTop("commitMessage") + switchFocus(g, filesView, commitMessageView) + return nil }) return nil } diff --git a/gitcommands.go b/gitcommands.go index 7c1ec84bd..798627c24 100644 --- a/gitcommands.go +++ b/gitcommands.go @@ -218,7 +218,6 @@ func sanitisedCommandOutput(output []byte, err error) (string, error) { func runCommand(command string) (string, error) { commandLog(command) splitCmd := strings.Split(command, " ") - devLog(splitCmd) cmdOut, err := exec.Command(splitCmd[0], splitCmd[1:]...).CombinedOutput() return sanitisedCommandOutput(cmdOut, err) } diff --git a/gui.go b/gui.go index 4cf7d7df5..b6258ec95 100644 --- a/gui.go +++ b/gui.go @@ -199,15 +199,26 @@ func layout(g *gocui.Gui) error { if err != gocui.ErrUnknownView { return err } - v.BgColor = gocui.ColorDefault v.FgColor = gocui.ColorBlue v.Frame = false } - if err = resizeConfirmationPanel(g); err != nil { - return err + if getCommitMessageView(g) == nil { + // doesn't matter where this view starts because it will be hidden + if commitMessageView, err := g.SetView("commitMessage", 0, 0, width, height, 0); err != nil { + if err != gocui.ErrUnknownView { + return err + } + g.SetViewOnBottom("commitMessage") + commitMessageView.Title = "Commit message" + commitMessageView.FgColor = gocui.ColorWhite + commitMessageView.Editable = true + } } + resizeConfirmationPanel(g, "commitMessage") + resizeConfirmationPanel(g, "confirmation") + if v, err := g.SetView("version", width-len(version)-1, optionsTop, width, optionsTop+2, 0); err != nil { if err != gocui.ErrUnknownView { return err diff --git a/keybindings.go b/keybindings.go index 07309790f..bc4a67015 100644 --- a/keybindings.go +++ b/keybindings.go @@ -58,6 +58,9 @@ func keybindings(g *gocui.Gui) error { {ViewName: "stash", Key: gocui.KeySpace, Modifier: gocui.ModNone, Handler: handleStashApply}, {ViewName: "stash", Key: 'g', Modifier: gocui.ModNone, Handler: handleStashPop}, {ViewName: "stash", Key: 'd', Modifier: gocui.ModNone, Handler: handleStashDrop}, + {ViewName: "commitMessage", Key: gocui.KeyEnter, Modifier: gocui.ModNone, Handler: handleCommitConfirm}, + {ViewName: "commitMessage", Key: gocui.KeyEsc, Modifier: gocui.ModNone, Handler: handleCommitClose}, + {ViewName: "commitMessage", Key: gocui.KeyTab, Modifier: gocui.ModNone, Handler: handleNewlineCommitMessage}, } // Would make these keybindings global but that interferes with editing diff --git a/view_helpers.go b/view_helpers.go index 2f5f2caf0..fdf3a802f 100644 --- a/view_helpers.go +++ b/view_helpers.go @@ -75,6 +75,8 @@ func newLineFocused(g *gocui.Gui, v *gocui.View) error { return handleBranchSelect(g, v) case "confirmation": return nil + case "commitMessage": + return handleCommitFocused(g, v) case "main": // TODO: pull this out into a 'view focused' function refreshMergePanel(g) @@ -215,3 +217,19 @@ func loader() string { index := nanos / 50000000 % int64(len(characters)) return characters[index : index+1] } + +// TODO: refactor properly +func getFilesView(g *gocui.Gui) *gocui.View { + v, _ := g.View("files") + return v +} + +func getCommitsView(g *gocui.Gui) *gocui.View { + v, _ := g.View("commits") + return v +} + +func getCommitMessageView(g *gocui.Gui) *gocui.View { + v, _ := g.View("commitMessage") + return v +} From 47bf649a69f11df1d0713b075f9a5c4b53f26f87 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sat, 11 Aug 2018 13:18:33 +1000 Subject: [PATCH 17/20] switch focus back to files view after confirming commit message --- commit_message_panel.go | 1 + 1 file changed, 1 insertion(+) diff --git a/commit_message_panel.go b/commit_message_panel.go index 7ff609976..49551c1eb 100644 --- a/commit_message_panel.go +++ b/commit_message_panel.go @@ -15,6 +15,7 @@ func handleCommitConfirm(g *gocui.Gui, v *gocui.View) error { } refreshFiles(g) g.SetViewOnBottom("commitMessage") + switchFocus(g, v, getFilesView(g)) return refreshCommits(g) } From b8daf71db6a8f0908f0c114f30afc673c07bbe92 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sat, 11 Aug 2018 13:19:54 +1000 Subject: [PATCH 18/20] Delete ZHgalGrWSF --- ZHgalGrWSF | 1 - 1 file changed, 1 deletion(-) delete mode 100644 ZHgalGrWSF diff --git a/ZHgalGrWSF b/ZHgalGrWSF deleted file mode 100644 index f1f0d2371..000000000 --- a/ZHgalGrWSF +++ /dev/null @@ -1 +0,0 @@ -GaUMygWjJa \ No newline at end of file From 8ae346787aaef5261acd0026b61a18883f2d0149 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sat, 11 Aug 2018 13:24:05 +1000 Subject: [PATCH 19/20] revert use of stored values in confirmation panels --- branches_panel.go | 4 ++-- commits_panel.go | 2 +- confirmation_panel.go | 36 ++---------------------------------- stash_panel.go | 2 +- 4 files changed, 6 insertions(+), 38 deletions(-) diff --git a/branches_panel.go b/branches_panel.go index 00091ffb8..3102f0c28 100644 --- a/branches_panel.go +++ b/branches_panel.go @@ -30,7 +30,7 @@ func handleForceCheckout(g *gocui.Gui, v *gocui.View) error { } func handleCheckoutByName(g *gocui.Gui, v *gocui.View) error { - createPromptPanel(g, v, "Branch Name:", nil, func(g *gocui.Gui, v *gocui.View) error { + createPromptPanel(g, v, "Branch Name:", func(g *gocui.Gui, v *gocui.View) error { if output, err := gitCheckout(trimmedContent(v), false); err != nil { return createErrorPanel(g, output) } @@ -41,7 +41,7 @@ func handleCheckoutByName(g *gocui.Gui, v *gocui.View) error { func handleNewBranch(g *gocui.Gui, v *gocui.View) error { branch := state.Branches[0] - createPromptPanel(g, v, "New Branch Name (Branch is off of "+branch.Name+")", nil, func(g *gocui.Gui, v *gocui.View) error { + createPromptPanel(g, v, "New Branch Name (Branch is off of "+branch.Name+")", func(g *gocui.Gui, v *gocui.View) error { if output, err := gitNewBranch(trimmedContent(v)); err != nil { return createErrorPanel(g, output) } diff --git a/commits_panel.go b/commits_panel.go index e8ff68678..88c7b8ebe 100644 --- a/commits_panel.go +++ b/commits_panel.go @@ -109,7 +109,7 @@ func handleRenameCommit(g *gocui.Gui, v *gocui.View) error { if getItemPosition(v) != 0 { return createErrorPanel(g, "Can only rename topmost commit") } - createPromptPanel(g, v, "Rename Commit", nil, func(g *gocui.Gui, v *gocui.View) error { + createPromptPanel(g, v, "Rename Commit", func(g *gocui.Gui, v *gocui.View) error { if output, err := gitRenameCommit(v.Buffer()); err != nil { return createErrorPanel(g, output) } diff --git a/confirmation_panel.go b/confirmation_panel.go index 03e078155..563db72d8 100644 --- a/confirmation_panel.go +++ b/confirmation_panel.go @@ -55,11 +55,8 @@ func getConfirmationPanelDimensions(g *gocui.Gui, prompt string) (int, int, int, height/2 + panelHeight/2 } -func createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, initialValue *[]byte, handleConfirm func(*gocui.Gui, *gocui.View) error) error { +func createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, handleConfirm func(*gocui.Gui, *gocui.View) error) error { g.SetViewOnBottom("commitMessage") - if initialValue == nil { - initialValue = &[]byte{} - } // only need to fit one line x0, y0, x1, y1 := getConfirmationPanelDimensions(g, "") if confirmationView, err := g.SetView("confirmation", x0, y0, x1, y1, 0); err != nil { @@ -67,43 +64,14 @@ func createPromptPanel(g *gocui.Gui, currentView *gocui.View, title string, init return err } - handleConfirm := func(gui *gocui.Gui, view *gocui.View) error { - *initialValue = nil - return handleConfirm(g, view) - } - - handleClose := func(gui *gocui.Gui, view *gocui.View) error { - // FIXME: trimming a newline that is no doubt caused by the enter keybinding - // on the editor. We should just define a new editor that doesn't do that - *initialValue = []byte(strings.TrimSpace(view.Buffer())) - return nil - } - confirmationView.Editable = true confirmationView.Title = title - restorePreviousBuffer(confirmationView, initialValue) switchFocus(g, currentView, confirmationView) - return setKeyBindings(g, handleConfirm, handleClose) + return setKeyBindings(g, handleConfirm, nil) } return nil } -func restorePreviousBuffer(confirmationView *gocui.View, initialValue *[]byte) { - confirmationView.Write(*initialValue) - x, y := getCursorPositionFromBuffer(initialValue) - devLog("New cursor position:", x, y) - confirmationView.SetCursor(0, 0) - confirmationView.MoveCursor(x, y, false) -} - -func getCursorPositionFromBuffer(initialValue *[]byte) (int, int) { - split := strings.Split(string(*initialValue), "\n") - lastLine := split[len(split)-1] - x := len(lastLine) - y := len(split) - return x, y -} - func createConfirmationPanel(g *gocui.Gui, currentView *gocui.View, title, prompt string, handleConfirm, handleClose func(*gocui.Gui, *gocui.View) error) error { g.SetViewOnBottom("commitMessage") g.Update(func(g *gocui.Gui) error { diff --git a/stash_panel.go b/stash_panel.go index a4a2207d8..33c7e297b 100644 --- a/stash_panel.go +++ b/stash_panel.go @@ -82,7 +82,7 @@ func stashDo(g *gocui.Gui, v *gocui.View, method string) error { } func handleStashSave(g *gocui.Gui, filesView *gocui.View) error { - createPromptPanel(g, filesView, "Stash changes", nil, func(g *gocui.Gui, v *gocui.View) error { + createPromptPanel(g, filesView, "Stash changes", func(g *gocui.Gui, v *gocui.View) error { if output, err := gitStashSave(trimmedContent(v)); err != nil { createErrorPanel(g, output) } From 6d3d40c41fb8703409dc6e3fcd8cb6efbff21d8d Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sat, 11 Aug 2018 13:27:34 +1000 Subject: [PATCH 20/20] handle error on confirmation panel resize --- gui.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/gui.go b/gui.go index b6258ec95..033dbe06a 100644 --- a/gui.go +++ b/gui.go @@ -216,8 +216,12 @@ func layout(g *gocui.Gui) error { } } - resizeConfirmationPanel(g, "commitMessage") - resizeConfirmationPanel(g, "confirmation") + if err = resizeConfirmationPanel(g, "commitMessage"); err != nil { + return err + } + if err = resizeConfirmationPanel(g, "confirmation"); err != nil { + return err + } if v, err := g.SetView("version", width-len(version)-1, optionsTop, width, optionsTop+2, 0); err != nil { if err != gocui.ErrUnknownView {