From d08241b2ea2f6cbf030be31c116502c92e55b1c7 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Fri, 10 Aug 2018 21:33:49 +1000 Subject: [PATCH] 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 {