diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | branches_panel.go | 142 | ||||
-rw-r--r-- | files_panel.go | 424 | ||||
-rw-r--r-- | gitcommands.go | 638 | ||||
-rw-r--r-- | gui.go | 263 | ||||
-rw-r--r-- | main.go | 78 | ||||
-rw-r--r-- | merge_panel.go | 260 | ||||
-rw-r--r-- | newFile | 1 | ||||
-rw-r--r-- | newFile2 | 1 | ||||
-rw-r--r-- | stash_panel.go | 114 | ||||
-rwxr-xr-x | test/generate_basic_repo.sh | 37 | ||||
-rw-r--r-- | view_helpers.go | 289 |
12 files changed, 1191 insertions, 1057 deletions
diff --git a/.gitignore b/.gitignore index a1a4f660d..5210291e4 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ commands.log extra/lgit.rb notes/go.notes TODO.notes +TODO.md diff --git a/branches_panel.go b/branches_panel.go index 12f2394a6..8b84ee908 100644 --- a/branches_panel.go +++ b/branches_panel.go @@ -1,17 +1,21 @@ package main import ( - "fmt" + "fmt" - "github.com/jesseduffield/gocui" + "github.com/jesseduffield/gocui" ) func handleBranchPress(g *gocui.Gui, v *gocui.View) error { - branch := getSelectedBranch(v) - if output, err := gitCheckout(branch.Name, false); err != nil { - createErrorPanel(g, output) - } - return refreshSidePanels(g) + index := getItemPosition(v) + if index == 0 { + return createErrorPanel(g, "You have already checked out this branch") + } + branch := getSelectedBranch(v) + if output, err := gitCheckout(branch.Name, false); err != nil { + createErrorPanel(g, output) + } + return refreshSidePanels(g) } func handleForceCheckout(g *gocui.Gui, v *gocui.View) error { @@ -25,87 +29,87 @@ 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 { - if output, err := gitCheckout(trimmedContent(v), false); err != nil { - return createErrorPanel(g, output) - } - return refreshSidePanels(g) - }) - return nil + 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) + } + return refreshSidePanels(g) + }) + return nil } 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 { - if output, err := gitNewBranch(trimmedContent(v)); err != nil { - return createErrorPanel(g, output) - } - refreshSidePanels(g) - return handleBranchSelect(g, v) - }) - return nil + branch := state.Branches[0] + 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) + } + refreshSidePanels(g) + return handleBranchSelect(g, v) + }) + return nil } func handleMerge(g *gocui.Gui, v *gocui.View) error { - checkedOutBranch := state.Branches[0] - selectedBranch := getSelectedBranch(v) - defer refreshSidePanels(g) - if checkedOutBranch.Name == selectedBranch.Name { - return createErrorPanel(g, "You cannot merge a branch into itself") - } - if output, err := gitMerge(selectedBranch.Name); err != nil { - return createErrorPanel(g, output) - } - return nil + checkedOutBranch := state.Branches[0] + selectedBranch := getSelectedBranch(v) + defer refreshSidePanels(g) + if checkedOutBranch.Name == selectedBranch.Name { + return createErrorPanel(g, "You cannot merge a branch into itself") + } + if output, err := gitMerge(selectedBranch.Name); err != nil { + return createErrorPanel(g, output) + } + return nil } func getSelectedBranch(v *gocui.View) Branch { - lineNumber := getItemPosition(v) - return state.Branches[lineNumber] + lineNumber := getItemPosition(v) + return state.Branches[lineNumber] } func renderBranchesOptions(g *gocui.Gui) error { - return renderOptionsMap(g, map[string]string{ - "space": "checkout", - "f": "force checkout", - "m": "merge", - "c": "checkout by name", - "n": "checkout new branch", - }) + return renderOptionsMap(g, map[string]string{ + "space": "checkout", + "f": "force checkout", + "m": "merge", + "c": "checkout by name", + "n": "new branch", + }) } // may want to standardise how these select methods work func handleBranchSelect(g *gocui.Gui, v *gocui.View) error { - if err := renderBranchesOptions(g); err != nil { - return err - } - // This really shouldn't happen: there should always be a master branch - if len(state.Branches) == 0 { - return renderString(g, "main", "No branches for this repo") - } - go func() { - branch := getSelectedBranch(v) - diff, _ := getBranchDiff(branch.Name, branch.BaseBranch) - renderString(g, "main", diff) - }() - return nil + if err := renderBranchesOptions(g); err != nil { + return err + } + // This really shouldn't happen: there should always be a master branch + if len(state.Branches) == 0 { + return renderString(g, "main", "No branches for this repo") + } + go func() { + branch := getSelectedBranch(v) + diff, _ := getBranchDiff(branch.Name, branch.BaseBranch) + renderString(g, "main", diff) + }() + return nil } // refreshStatus is called at the end of this because that's when we can // be sure there is a state.Branches array to pick the current branch from func refreshBranches(g *gocui.Gui) error { - g.Update(func(g *gocui.Gui) error { - v, err := g.View("branches") - if err != nil { - panic(err) - } - state.Branches = getGitBranches() - v.Clear() - for _, branch := range state.Branches { - fmt.Fprintln(v, branch.DisplayString) - } - resetOrigin(v) - return refreshStatus(g) - }) - return nil + g.Update(func(g *gocui.Gui) error { + v, err := g.View("branches") + if err != nil { + panic(err) + } + state.Branches = getGitBranches() + v.Clear() + for _, branch := range state.Branches { + fmt.Fprintln(v, branch.DisplayString) + } + resetOrigin(v) + return refreshStatus(g) + }) + return nil } diff --git a/files_panel.go b/files_panel.go index cb15e8b3b..f29cbb75a 100644 --- a/files_panel.go +++ b/files_panel.go @@ -2,65 +2,65 @@ package main import ( - // "io" - // "io/ioutil" + // "io" + // "io/ioutil" - // "strings" + // "strings" - "errors" - "strings" + "errors" + "strings" - "github.com/fatih/color" - "github.com/jesseduffield/gocui" + "github.com/fatih/color" + "github.com/jesseduffield/gocui" ) var ( - // ErrNoFiles : when there are no modified files in the repo - ErrNoFiles = errors.New("No changed files") + // ErrNoFiles : when there are no modified files in the repo + ErrNoFiles = errors.New("No changed files") ) func stagedFiles(files []GitFile) []GitFile { - result := make([]GitFile, 0) - for _, file := range files { - if file.HasStagedChanges { - result = append(result, file) - } - } - return result + result := make([]GitFile, 0) + for _, file := range files { + if file.HasStagedChanges { + result = append(result, file) + } + } + return result } func handleFilePress(g *gocui.Gui, v *gocui.View) error { - file, err := getSelectedFile(g) - if err != nil { - return err - } + file, err := getSelectedFile(g) + if err != nil { + return err + } - if file.HasUnstagedChanges { - stageFile(file.Name) - } else { - unStageFile(file.Name) - } + if file.HasUnstagedChanges { + stageFile(file.Name) + } else { + unStageFile(file.Name, file.Tracked) + } - if err := refreshFiles(g); err != nil { - return err - } - if err := handleFileSelect(g, v); err != nil { - return err - } + if err := refreshFiles(g); err != nil { + return err + } + if err := handleFileSelect(g, v); err != nil { + return err + } - return nil + return nil } func getSelectedFile(g *gocui.Gui) (GitFile, error) { - if len(state.GitFiles) == 0 { - return GitFile{}, ErrNoFiles - } - filesView, err := g.View("files") - if err != nil { - panic(err) - } - lineNumber := getItemPosition(filesView) - return state.GitFiles[lineNumber], nil + if len(state.GitFiles) == 0 { + return GitFile{}, ErrNoFiles + } + filesView, err := g.View("files") + if err != nil { + panic(err) + } + lineNumber := getItemPosition(filesView) + return state.GitFiles[lineNumber], nil } func handleFileRemove(g *gocui.Gui, v *gocui.View) error { @@ -83,93 +83,96 @@ func handleFileRemove(g *gocui.Gui, v *gocui.View) error { } func handleIgnoreFile(g *gocui.Gui, v *gocui.View) error { - file, err := getSelectedFile(g) - if err != nil { - return err - } - if file.Tracked { - return createErrorPanel(g, "Cannot ignore tracked files") - } - gitIgnore(file.Name) - return refreshFiles(g) + file, err := getSelectedFile(g) + if err != nil { + return err + } + if file.Tracked { + return createErrorPanel(g, "Cannot ignore tracked files") + } + gitIgnore(file.Name) + return refreshFiles(g) } func renderfilesOptions(g *gocui.Gui, gitFile *GitFile) error { - optionsMap := map[string]string{ - "tab": "next panel", - "S": "stash files", - "c": "commit changes", - "o": "open", - "s": "open in sublime", - "i": "ignore", - "d": "delete", - "space": "toggle staged", - } - if state.HasMergeConflicts { - optionsMap["a"] = "abort merge" - optionsMap["m"] = "resolve merge conflicts" - } - if gitFile == nil { - return renderOptionsMap(g, optionsMap) - } - if gitFile.Tracked { - optionsMap["d"] = "checkout" - } - return renderOptionsMap(g, optionsMap) + optionsMap := map[string]string{ + "tab": "next panel", + "S": "stash files", + "c": "commit changes", + "o": "open", + "s": "open in sublime", + "v": "open in vscode", + "i": "ignore", + "d": "delete", + "space": "toggle staged", + } + if state.HasMergeConflicts { + optionsMap["a"] = "abort merge" + optionsMap["m"] = "resolve merge conflicts" + } + if gitFile == nil { + return renderOptionsMap(g, optionsMap) + } + if gitFile.Tracked { + optionsMap["d"] = "checkout" + } + return renderOptionsMap(g, optionsMap) } func handleFileSelect(g *gocui.Gui, v *gocui.View) error { - gitFile, err := getSelectedFile(g) - if err != nil { - if err != ErrNoFiles { - return err - } - renderString(g, "main", "No changed files") - colorLog(color.FgRed, "error") - return renderfilesOptions(g, nil) - } - renderfilesOptions(g, &gitFile) - var content string - if gitFile.HasMergeConflicts { - return refreshMergePanel(g) - } + gitFile, err := getSelectedFile(g) + if err != nil { + if err != ErrNoFiles { + return err + } + renderString(g, "main", "No changed files") + return renderfilesOptions(g, nil) + } + renderfilesOptions(g, &gitFile) + var content string + if gitFile.HasMergeConflicts { + return refreshMergePanel(g) + } - content = getDiff(gitFile) - return renderString(g, "main", content) + content = getDiff(gitFile) + return renderString(g, "main", content) } 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 { - message := trimmedContent(v) - if message == "" { - return createErrorPanel(g, "You cannot commit without a commit message") - } - if err := gitCommit(message); err != nil { - panic(err) - } - refreshFiles(g) - return refreshCommits(g) - }) - return nil + 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 { + message := trimmedContent(v) + if message == "" { + return createErrorPanel(g, "You cannot commit without a commit message") + } + if err := gitCommit(message); err != nil { + panic(err) + } + refreshFiles(g) + return refreshCommits(g) + }) + return nil } func genericFileOpen(g *gocui.Gui, v *gocui.View, open func(string) (string, error)) error { - file, err := getSelectedFile(g) - if err != nil { - return err - } - _, err = open(file.Name) - return err + file, err := getSelectedFile(g) + if err != nil { + return err + } + _, err = open(file.Name) + return err } func handleFileOpen(g *gocui.Gui, v *gocui.View) error { - return genericFileOpen(g, v, openFile) + return genericFileOpen(g, v, openFile) } func handleSublimeFileOpen(g *gocui.Gui, v *gocui.View) error { - return genericFileOpen(g, v, sublimeOpenFile) + return genericFileOpen(g, v, sublimeOpenFile) +} +func handleVsCodeFileOpen(g *gocui.Gui, v *gocui.View) error { + return genericFileOpen(g, v, vsCodeOpenFile) } func handleRefreshFiles(g *gocui.Gui, v *gocui.View) error { @@ -177,128 +180,129 @@ func handleRefreshFiles(g *gocui.Gui, v *gocui.View) error { } func refreshStateGitFiles() { - // get files to stage - gitFiles := getGitStatusFiles() - state.GitFiles = mergeGitStatusFiles(state.GitFiles, gitFiles) - updateHasMergeConflictStatus() + // get files to stage + gitFiles := getGitStatusFiles() + state.GitFiles = mergeGitStatusFiles(state.GitFiles, gitFiles) + updateHasMergeConflictStatus() } func updateHasMergeConflictStatus() error { - merging, err := isInMergeState() - if err != nil { - return err - } - state.HasMergeConflicts = merging - return nil + merging, err := isInMergeState() + if err != nil { + return err + } + state.HasMergeConflicts = merging + return nil } func renderGitFile(gitFile GitFile, filesView *gocui.View) { - // potentially inefficient to be instantiating these color - // objects with each render - red := color.New(color.FgRed) - green := color.New(color.FgGreen) - if !gitFile.Tracked { - red.Fprintln(filesView, gitFile.DisplayString) - return - } - green.Fprint(filesView, gitFile.DisplayString[0:1]) - red.Fprint(filesView, gitFile.DisplayString[1:3]) - if gitFile.HasUnstagedChanges { - red.Fprintln(filesView, gitFile.Name) - } else { - green.Fprintln(filesView, gitFile.Name) - } + // potentially inefficient to be instantiating these color + // objects with each render + red := color.New(color.FgRed) + green := color.New(color.FgGreen) + if !gitFile.Tracked && !gitFile.HasStagedChanges { + red.Fprintln(filesView, gitFile.DisplayString) + return + } + green.Fprint(filesView, gitFile.DisplayString[0:1]) + red.Fprint(filesView, gitFile.DisplayString[1:3]) + if gitFile.HasUnstagedChanges { + red.Fprintln(filesView, gitFile.Name) + } else { + green.Fprintln(filesView, gitFile.Name) + } } func catSelectedFile(g *gocui.Gui) (string, error) { - item, err := getSelectedFile(g) - if err != nil { - if err != ErrNoFiles { - return "", err - } - return "", renderString(g, "main", "No file to display") - } - cat, err := catFile(item.Name) - if err != nil { - panic(err) - } - return cat, nil + item, err := getSelectedFile(g) + if err != nil { + if err != ErrNoFiles { + return "", err + } + return "", renderString(g, "main", "No file to display") + } + cat, err := catFile(item.Name) + if err != nil { + panic(err) + } + return cat, nil } func refreshFiles(g *gocui.Gui) error { - filesView, err := g.View("files") - if err != nil { - return err - } - refreshStateGitFiles() - filesView.Clear() - for _, gitFile := range state.GitFiles { - renderGitFile(gitFile, filesView) - } - correctCursor(filesView) - if filesView == g.CurrentView() { - handleFileSelect(g, filesView) - } - return nil + filesView, err := g.View("files") + if err != nil { + return err + } + refreshStateGitFiles() + filesView.Clear() + for _, gitFile := range state.GitFiles { + renderGitFile(gitFile, filesView) + } + correctCursor(filesView) + if filesView == g.CurrentView() { + handleFileSelect(g, filesView) + } + return nil } func pullFiles(g *gocui.Gui, v *gocui.View) error { - devLog("pulling...") - createMessagePanel(g, v, "", "Pulling...") - go func() { - if output, err := gitPull(); err != nil { - createErrorPanel(g, output) - } else { - closeConfirmationPrompt(g) - refreshCommits(g) - refreshFiles(g) - refreshStatus(g) - devLog("pulled.") - } - }() - return nil + devLog("pulling...") + createMessagePanel(g, v, "", "Pulling...") + go func() { + if output, err := gitPull(); err != nil { + createErrorPanel(g, output) + } else { + closeConfirmationPrompt(g) + refreshCommits(g) + refreshStatus(g) + devLog("pulled.") + } + refreshFiles(g) + }() + return nil } func pushFiles(g *gocui.Gui, v *gocui.View) error { - devLog("pushing...") - createMessagePanel(g, v, "", "Pushing...") - go func() { - if output, err := gitPush(); err != nil { - createErrorPanel(g, output) - } else { - closeConfirmationPrompt(g) - refreshCommits(g) - refreshStatus(g) - devLog("pushed.") - } - }() - return nil + devLog("pushing...") + createMessagePanel(g, v, "", "Pushing...") + go func() { + if output, err := gitPush(); err != nil { + createErrorPanel(g, output) + } else { + closeConfirmationPrompt(g) + refreshCommits(g) + refreshStatus(g) + devLog("pushed.") + } + }() + return nil } func handleSwitchToMerge(g *gocui.Gui, v *gocui.View) error { - mergeView, err := g.View("main") - if err != nil { - return err - } - file, err := getSelectedFile(g) - if err != nil { - if err != ErrNoFiles { - return err - } - return nil - } - if !file.HasMergeConflicts { - return nil - } - switchFocus(g, v, mergeView) - return refreshMergePanel(g) + mergeView, err := g.View("main") + if err != nil { + return err + } + file, err := getSelectedFile(g) + if err != nil { + if err != ErrNoFiles { + return err + } + return nil + } + if !file.HasMergeConflicts { + return nil + } + switchFocus(g, v, mergeView) + return refreshMergePanel(g) } func handleAbortMerge(g *gocui.Gui, v *gocui.View) error { - output, err := gitAbortMerge() - if err != nil { - return createErrorPanel(g, output) - } - createMessagePanel(g, v, "", "Merge aborted") - return refreshFiles(g) + output, err := gitAbortMerge() + if err != nil { + return createErrorPanel(g, output) + } + createMessagePanel(g, v, "", "Merge aborted") + refreshStatus(g) + return refreshFiles(g) } diff --git a/gitcommands.go b/gitcommands.go index 7d2651eea..38da938b2 100644 --- a/gitcommands.go +++ b/gitcommands.go @@ -2,268 +2,317 @@ package main import ( - // "log" - "errors" - "fmt" - "os" - "os/exec" - "strings" - "time" - - "github.com/fatih/color" + // "log" + "errors" + "fmt" + "os" + "os/exec" + "strings" + "time" + + "github.com/fatih/color" ) // GitFile : A staged/unstaged file // TODO: decide whether to give all of these the Git prefix type GitFile struct { - Name string - HasStagedChanges bool - HasUnstagedChanges bool - Tracked bool - Deleted bool - HasMergeConflicts bool - DisplayString string + Name string + HasStagedChanges bool + HasUnstagedChanges bool + Tracked bool + Deleted bool + HasMergeConflicts bool + DisplayString string } // Branch : A git branch type Branch struct { - Name string - Type string - BaseBranch string - DisplayString string + Name string + Type string + BaseBranch string + DisplayString string } // Commit : A git commit type Commit struct { - Sha string - Name string - Pushed bool - DisplayString string + Sha string + Name string + Pushed bool + DisplayString string } // StashEntry : A git stash entry type StashEntry struct { - Index int - Name string - DisplayString string + Index int + Name string + DisplayString string } // Map (from https://gobyexample.com/collection-functions) func Map(vs []string, f func(string) string) []string { - vsm := make([]string, len(vs)) - for i, v := range vs { - vsm[i] = f(v) - } - return vsm + vsm := make([]string, len(vs)) + for i, v := range vs { + vsm[i] = f(v) + } + return vsm } func includesString(list []string, a string) bool { - for _, b := range list { - if b == a { - return true - } - } - return false + for _, b := range list { + if b == a { + return true + } + } + return false } // not sure how to genericise this because []interface{} doesn't accept e.g. // []int arguments func includesInt(list []int, a int) bool { - for _, b := range list { - if b == a { - return true - } - } - return false + for _, b := range list { + if b == a { + return true + } + } + return false } func mergeGitStatusFiles(oldGitFiles, newGitFiles []GitFile) []GitFile { - if len(oldGitFiles) == 0 { - return newGitFiles - } - - appendedIndexes := make([]int, 0) - - // retain position of files we already could see - result := make([]GitFile, 0) - for _, oldGitFile := range oldGitFiles { - for newIndex, newGitFile := range newGitFiles { - if oldGitFile.Name == newGitFile.Name { - result = append(result, newGitFile) - appendedIndexes = append(appendedIndexes, newIndex) - break - } - } - } - - // append any new files to the end - for index, newGitFile := range newGitFiles { - if !includesInt(appendedIndexes, index) { - result = append(result, newGitFile) - } - } - - return result + if len(oldGitFiles) == 0 { + return newGitFiles + } + + appendedIndexes := make([]int, 0) + + // retain position of files we already could see + result := make([]GitFile, 0) + for _, oldGitFile := range oldGitFiles { + for newIndex, newGitFile := range newGitFiles { + if oldGitFile.Name == newGitFile.Name { + result = append(result, newGitFile) + appendedIndexes = append(appendedIndexes, newIndex) + break + } + } + } + + // append any new files to the end + for index, newGitFile := range newGitFiles { + if !includesInt(appendedIndexes, index) { + result = append(result, newGitFile) + } + } + + return result } func runDirectCommand(command string) (string, error) { - timeStart := time.Now() + timeStart := time.Now() - commandLog(command) - cmdOut, err := exec.Command("bash", "-c", command).CombinedOutput() - devLog("run direct command time for command: ", command, time.Now().Sub(timeStart)) + commandLog(command) + cmdOut, err := exec. + Command("bash", "-c", command). + CombinedOutput() + devLog("run direct command time for command: ", command, time.Now().Sub(timeStart)) - return string(cmdOut), err + return string(cmdOut), err } func branchStringParts(branchString string) (string, string) { - splitBranchName := strings.Split(branchString, "\t") - return splitBranchName[0], splitBranchName[1] + // expect string to be something like '4w master` + splitBranchName := strings.Split(branchString, "\t") + // if we have no \t then we have no recency, so just output that as blank + if len(splitBranchName) == 1 { + return "", branchString + } + 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 + 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)) + return colour.SprintFunc()(fmt.Sprint(str)) } func withPadding(str string, padding int) string { - return str + strings.Repeat(" ", padding-len(str)) + if padding-len(str) < 0 { + return str + } + return str + strings.Repeat(" ", padding-len(str)) } func branchFromLine(line string, index int) Branch { - recency, name := branchStringParts(line) - branchType, branchBase, colourAttr := branchPropertiesFromName(name) - if index == 0 { - recency = " *" - } - colour := color.New(colourAttr) - displayString := withPadding(recency, 4) + coloredString(name, colour) - return Branch{ - Name: name, - Type: branchType, - BaseBranch: branchBase, - DisplayString: displayString, - } + recency, name := branchStringParts(line) + branchType, branchBase, colourAttr := branchPropertiesFromName(name) + if index == 0 { + recency = " *" + } + colour := color.New(colourAttr) + displayString := withPadding(recency, 4) + coloredString(name, colour) + return Branch{ + Name: name, + Type: branchType, + BaseBranch: branchBase, + DisplayString: displayString, + } } func getGitBranches() []Branch { - branches := make([]Branch, 0) - // check if there are any branches - branchCheck, _ := runDirectCommand("git branch") - if branchCheck == "" { - return branches - } - rawString, _ := runDirectCommand(getBranchesCommand) - branchLines := splitLines(rawString) - if len(branchLines) == 0 { - // sometimes the getBranchesCommand command returns nothing, in which case - // we assume you've just init'd or cloned the repo and you've got master - // checked out - branches = append(branches, branchFromLine(" *\tmaster", 0)) - } - for i, line := range branchLines { - branches = append(branches, branchFromLine(line, i)) - } - return branches + branches := make([]Branch, 0) + // check if there are any branches + branchCheck, _ := runDirectCommand("git branch") + if branchCheck == "" { + return append(branches, branchFromLine("master", 0)) + } + rawString, _ := runDirectCommand(getBranchesCommand) + branchLines := splitLines(rawString) + for i, line := range branchLines { + branches = append(branches, branchFromLine(line, i)) + } + branches = getAndMergeFetchedBranches(branches) + return branches +} + +func branchAlreadyStored(branchLine string, branches []Branch) bool { + for _, branch := range branches { + if branch.Name == branchLine { + return true + } + } + return false +} + +// here branches contains all the branches that we've checked out, along with +// the recency. In this function we |