feat(git): better logic

This commit is contained in:
Jan De Dobbeleer 2021-12-05 18:51:10 +01:00 committed by Jan De Dobbeleer
parent 4f921dbb0f
commit 98472abafc
4 changed files with 497 additions and 559 deletions

View file

@ -87,13 +87,13 @@ func (g *git) deprecatedString(statusColorsEnabled bool) string {
if len(g.BranchStatus) > 0 { if len(g.BranchStatus) > 0 {
buffer.WriteString(g.BranchStatus) buffer.WriteString(g.BranchStatus)
} }
if g.Staging.Changed { if g.Staging.Changed() {
fmt.Fprint(buffer, g.getStatusDetailString(g.Staging, StagingColor, LocalStagingIcon, " \uF046")) fmt.Fprint(buffer, g.getStatusDetailString(g.Staging, StagingColor, LocalStagingIcon, " \uF046"))
} }
if g.Staging.Changed && g.Working.Changed { if g.Staging.Changed() && g.Working.Changed() {
fmt.Fprint(buffer, g.props.getString(StatusSeparatorIcon, " |")) fmt.Fprint(buffer, g.props.getString(StatusSeparatorIcon, " |"))
} }
if g.Working.Changed { if g.Working.Changed() {
fmt.Fprint(buffer, g.getStatusDetailString(g.Working, WorkingColor, LocalWorkingIcon, " \uF044")) fmt.Fprint(buffer, g.getStatusDetailString(g.Working, WorkingColor, LocalWorkingIcon, " \uF044"))
} }
if g.StashCount != 0 { if g.StashCount != 0 {
@ -114,7 +114,7 @@ func (g *git) SetStatusColor() {
} }
func (g *git) getStatusColor(defaultValue string) string { func (g *git) getStatusColor(defaultValue string) string {
if g.Staging.Changed || g.Working.Changed { if g.Staging.Changed() || g.Working.Changed() {
return g.props.getColor(LocalChangesColor, defaultValue) return g.props.getColor(LocalChangesColor, defaultValue)
} else if g.Ahead > 0 && g.Behind > 0 { } else if g.Ahead > 0 && g.Behind > 0 {
return g.props.getColor(AheadAndBehindColor, defaultValue) return g.props.getColor(AheadAndBehindColor, defaultValue)

View file

@ -13,7 +13,6 @@ import (
func TestGetStatusDetailStringDefault(t *testing.T) { func TestGetStatusDetailStringDefault(t *testing.T) {
expected := "icon +1" expected := "icon +1"
status := &GitStatus{ status := &GitStatus{
Changed: true,
Added: 1, Added: 1,
} }
g := &git{} g := &git{}
@ -23,7 +22,6 @@ func TestGetStatusDetailStringDefault(t *testing.T) {
func TestGetStatusDetailStringDefaultColorOverride(t *testing.T) { func TestGetStatusDetailStringDefaultColorOverride(t *testing.T) {
expected := "<#123456>icon +1</>" expected := "<#123456>icon +1</>"
status := &GitStatus{ status := &GitStatus{
Changed: true,
Added: 1, Added: 1,
} }
var props properties = map[Property]interface{}{ var props properties = map[Property]interface{}{
@ -38,7 +36,6 @@ func TestGetStatusDetailStringDefaultColorOverride(t *testing.T) {
func TestGetStatusDetailStringDefaultColorOverrideAndIconColorOverride(t *testing.T) { func TestGetStatusDetailStringDefaultColorOverrideAndIconColorOverride(t *testing.T) {
expected := "<#789123>work</> <#123456>+1</>" expected := "<#789123>work</> <#123456>+1</>"
status := &GitStatus{ status := &GitStatus{
Changed: true,
Added: 1, Added: 1,
} }
var props properties = map[Property]interface{}{ var props properties = map[Property]interface{}{
@ -54,7 +51,6 @@ func TestGetStatusDetailStringDefaultColorOverrideAndIconColorOverride(t *testin
func TestGetStatusDetailStringDefaultColorOverrideNoIconColorOverride(t *testing.T) { func TestGetStatusDetailStringDefaultColorOverrideNoIconColorOverride(t *testing.T) {
expected := "<#123456>work +1</>" expected := "<#123456>work +1</>"
status := &GitStatus{ status := &GitStatus{
Changed: true,
Added: 1, Added: 1,
} }
var props properties = map[Property]interface{}{ var props properties = map[Property]interface{}{
@ -70,7 +66,6 @@ func TestGetStatusDetailStringDefaultColorOverrideNoIconColorOverride(t *testing
func TestGetStatusDetailStringNoStatus(t *testing.T) { func TestGetStatusDetailStringNoStatus(t *testing.T) {
expected := "icon" expected := "icon"
status := &GitStatus{ status := &GitStatus{
Changed: true,
Added: 1, Added: 1,
} }
var props properties = map[Property]interface{}{ var props properties = map[Property]interface{}{
@ -85,7 +80,6 @@ func TestGetStatusDetailStringNoStatus(t *testing.T) {
func TestGetStatusDetailStringNoStatusColorOverride(t *testing.T) { func TestGetStatusDetailStringNoStatusColorOverride(t *testing.T) {
expected := "<#123456>icon</>" expected := "<#123456>icon</>"
status := &GitStatus{ status := &GitStatus{
Changed: true,
Added: 1, Added: 1,
} }
var props properties = map[Property]interface{}{ var props properties = map[Property]interface{}{
@ -106,8 +100,9 @@ func TestGetStatusColorLocalChangesStaging(t *testing.T) {
g := &git{ g := &git{
props: props, props: props,
Staging: &GitStatus{ Staging: &GitStatus{
Changed: true, Modified: 1,
}, },
Working: &GitStatus{},
} }
assert.Equal(t, expected, g.getStatusColor("#fg1111")) assert.Equal(t, expected, g.getStatusColor("#fg1111"))
} }
@ -121,7 +116,7 @@ func TestGetStatusColorLocalChangesWorking(t *testing.T) {
props: props, props: props,
Staging: &GitStatus{}, Staging: &GitStatus{},
Working: &GitStatus{ Working: &GitStatus{
Changed: true, Modified: 1,
}, },
} }
assert.Equal(t, expected, g.getStatusColor("#fg1111")) assert.Equal(t, expected, g.getStatusColor("#fg1111"))
@ -196,8 +191,9 @@ func TestSetStatusColorForeground(t *testing.T) {
g := &git{ g := &git{
props: props, props: props,
Staging: &GitStatus{ Staging: &GitStatus{
Changed: true, Added: 1,
}, },
Working: &GitStatus{},
} }
g.SetStatusColor() g.SetStatusColor()
assert.Equal(t, expected, g.props[ForegroundOverride]) assert.Equal(t, expected, g.props[ForegroundOverride])
@ -211,8 +207,9 @@ func TestSetStatusColorBackground(t *testing.T) {
} }
g := &git{ g := &git{
props: props, props: props,
Staging: &GitStatus{ Staging: &GitStatus{},
Changed: true, Working: &GitStatus{
Modified: 1,
}, },
} }
g.SetStatusColor() g.SetStatusColor()
@ -221,16 +218,30 @@ func TestSetStatusColorBackground(t *testing.T) {
func TestStatusColorsWithoutDisplayStatus(t *testing.T) { func TestStatusColorsWithoutDisplayStatus(t *testing.T) {
expected := changesColor expected := changesColor
context := &detachedContext{ status := "## main...origin/main [ahead 33]\n M myfile"
status: "## main...origin/main [ahead 33]\n M myfile", env := new(MockedEnvironment)
} env.On("isWsl", nil).Return(false)
g := setupHEADContextEnv(context) env.On("getRuntimeGOOS", nil).Return("unix")
var props properties = map[Property]interface{}{ env.On("hasFolder", "/rebase-merge").Return(false)
env.On("hasFolder", "/rebase-apply").Return(false)
env.On("hasFolder", "/sequencer").Return(false)
env.On("getFileContent", "/HEAD").Return(status)
env.On("hasFilesInDir", "", "CHERRY_PICK_HEAD").Return(false)
env.On("hasFilesInDir", "", "REVERT_HEAD").Return(false)
env.On("hasFilesInDir", "", "MERGE_MSG").Return(false)
env.On("hasFilesInDir", "", "MERGE_HEAD").Return(false)
env.On("hasFilesInDir", "", "sequencer/todo").Return(false)
env.mockGitCommand("", "describe", "--tags", "--exact-match")
env.mockGitCommand(status, "status", "-unormal", "--branch", "--porcelain=2")
g := &git{
env: env,
gitWorkingFolder: "",
props: map[Property]interface{}{
DisplayStatus: false, DisplayStatus: false,
StatusColorsEnabled: true, StatusColorsEnabled: true,
LocalChangesColor: expected, LocalChangesColor: expected,
},
} }
g.props = props
g.string() g.string()
assert.Equal(t, expected, g.props[BackgroundOverride]) assert.Equal(t, expected, g.props[BackgroundOverride])
} }

View file

@ -14,29 +14,15 @@ type GitStatus struct {
Deleted int Deleted int
Added int Added int
Modified int Modified int
Changed bool
} }
func (s *GitStatus) parse(output []string, working bool) { func (s *GitStatus) add(code string) {
if len(output) <= 1 {
return
}
for _, line := range output[1:] {
if len(line) < 2 {
continue
}
code := line[0:1]
if working {
code = line[1:2]
}
switch code { switch code {
case "?": case ".":
if working { return
s.Added++
}
case "D": case "D":
s.Deleted++ s.Deleted++
case "A": case "A", "?":
s.Added++ s.Added++
case "U": case "U":
s.Unmerged++ s.Unmerged++
@ -44,7 +30,9 @@ func (s *GitStatus) parse(output []string, working bool) {
s.Modified++ s.Modified++
} }
} }
s.Changed = s.Added > 0 || s.Deleted > 0 || s.Modified > 0 || s.Unmerged > 0
func (s *GitStatus) Changed() bool {
return s.Added > 0 || s.Deleted > 0 || s.Modified > 0 || s.Unmerged > 0
} }
func (s *GitStatus) String() string { func (s *GitStatus) String() string {
@ -71,6 +59,8 @@ type git struct {
Ahead int Ahead int
Behind int Behind int
HEAD string HEAD string
Ref string
Hash string
BranchStatus string BranchStatus string
Upstream string Upstream string
UpstreamIcon string UpstreamIcon string
@ -133,6 +123,9 @@ const (
GitlabIcon Property = "gitlab_icon" GitlabIcon Property = "gitlab_icon"
// GitIcon shows when the upstream can't be identified // GitIcon shows when the upstream can't be identified
GitIcon Property = "git_icon" GitIcon Property = "git_icon"
DETACHED = "(detached)"
BRANCHPREFIX = "ref: refs/heads/"
) )
func (g *git) enabled() bool { func (g *git) enabled() bool {
@ -147,12 +140,10 @@ func (g *git) enabled() bool {
return false return false
} }
g.Staging = &GitStatus{}
g.Working = &GitStatus{}
if gitdir.isDir { if gitdir.isDir {
g.gitWorkingFolder = gitdir.path g.gitWorkingFolder = gitdir.path
g.gitRootFolder = gitdir.path g.gitRootFolder = gitdir.path
g.gitWorktreeFolder = strings.TrimSuffix(gitdir.path, ".git")
return true return true
} }
// handle worktree // handle worktree
@ -186,10 +177,12 @@ func (g *git) string() string {
statusColorsEnabled := g.props.getBool(StatusColorsEnabled, false) statusColorsEnabled := g.props.getBool(StatusColorsEnabled, false)
displayStatus := g.props.getOneOfBool(FetchStatus, DisplayStatus, false) displayStatus := g.props.getOneOfBool(FetchStatus, DisplayStatus, false)
if !displayStatus { if !displayStatus {
g.HEAD = g.getPrettyHEADName() g.setPrettyHEADName()
} }
if displayStatus || statusColorsEnabled { if displayStatus || statusColorsEnabled {
g.setGitStatus() g.setGitStatus()
g.setGitHEADContext()
g.BranchStatus = g.getBranchStatus()
} }
if g.Upstream != "" && g.props.getOneOfBool(FetchUpstreamIcon, DisplayUpstreamIcon, false) { if g.Upstream != "" && g.props.getOneOfBool(FetchUpstreamIcon, DisplayUpstreamIcon, false) {
g.UpstreamIcon = g.getUpstreamIcon() g.UpstreamIcon = g.getUpstreamIcon()
@ -266,20 +259,47 @@ func (g *git) getUpstreamIcon() string {
} }
func (g *git) setGitStatus() { func (g *git) setGitStatus() {
output := g.getGitCommandOutput("status", "-unormal", "--short", "--branch") addToStatus := func(status string) {
splittedOutput := strings.Split(output, "\n") if len(status) <= 4 {
g.Working.parse(splittedOutput, true) return
g.Staging.parse(splittedOutput, false)
status := g.parseGitStatusInfo(splittedOutput[0])
if status["local"] != "" {
g.Ahead, _ = strconv.Atoi(status["ahead"])
g.Behind, _ = strconv.Atoi(status["behind"])
if status["upstream_status"] != "gone" {
g.Upstream = status["upstream"]
} }
workingCode := status[3:4]
stagingCode := status[2:3]
g.Working.add(workingCode)
g.Staging.add(stagingCode)
}
const (
HASH = "# branch.oid "
REF = "# branch.head "
UPSTREAM = "# branch.upstream "
BRANCHSTATUS = "# branch.ab "
)
g.Staging = &GitStatus{}
g.Working = &GitStatus{}
output := g.getGitCommandOutput("status", "-unormal", "--branch", "--porcelain=2")
for _, line := range strings.Split(output, "\n") {
if strings.HasPrefix(line, HASH) {
g.Hash = line[len(HASH) : len(HASH)+7]
continue
}
if strings.HasPrefix(line, REF) {
g.Ref = line[len(REF):]
continue
}
if strings.HasPrefix(line, UPSTREAM) {
g.Upstream = line[len(UPSTREAM):]
continue
}
if strings.HasPrefix(line, BRANCHSTATUS) {
status := line[len(BRANCHSTATUS):]
splitted := strings.Split(status, " ")
g.Ahead, _ = strconv.Atoi(splitted[0])
behind, _ := strconv.Atoi(splitted[1])
g.Behind = -behind
continue
}
addToStatus(line)
} }
g.HEAD = g.getGitHEADContext(status["local"])
g.BranchStatus = g.getBranchStatus()
} }
func (g *git) getGitCommand() string { func (g *git) getGitCommand() string {
@ -309,53 +329,74 @@ func (g *git) getGitCommandOutput(args ...string) string {
return val return val
} }
func (g *git) getGitHEADContext(ref string) string { func (g *git) setGitHEADContext() {
branchIcon := g.props.getString(BranchIcon, "\uE0A0") branchIcon := g.props.getString(BranchIcon, "\uE0A0")
if ref == "" { if g.Ref == DETACHED {
ref = g.getPrettyHEADName() g.setPrettyHEADName()
} else { } else {
ref = g.truncateBranch(ref) head := g.formatHEAD(g.Ref)
ref = fmt.Sprintf("%s%s", branchIcon, ref) g.HEAD = fmt.Sprintf("%s%s", branchIcon, head)
} }
// rebase
formatDetached := func() string {
if g.Ref == DETACHED {
return fmt.Sprintf("%sdetached at %s", branchIcon, g.HEAD)
}
return g.HEAD
}
getPrettyNameOrigin := func(file string) string {
var origin string
head := g.getGitFileContents(g.gitWorkingFolder, file)
if head == "detached HEAD" {
origin = formatDetached()
} else {
head = strings.Replace(head, "refs/heads/", "", 1)
origin = branchIcon + g.formatHEAD(head)
}
return origin
}
if g.env.hasFolder(g.gitWorkingFolder + "/rebase-merge") { if g.env.hasFolder(g.gitWorkingFolder + "/rebase-merge") {
head := g.getGitFileContents(g.gitWorkingFolder, "rebase-merge/head-name") origin := getPrettyNameOrigin("rebase-merge/head-name")
origin := strings.Replace(head, "refs/heads/", "", 1)
origin = g.truncateBranch(origin)
onto := g.getGitRefFileSymbolicName("rebase-merge/onto") onto := g.getGitRefFileSymbolicName("rebase-merge/onto")
onto = g.truncateBranch(onto) onto = g.formatHEAD(onto)
step := g.getGitFileContents(g.gitWorkingFolder, "rebase-merge/msgnum") step := g.getGitFileContents(g.gitWorkingFolder, "rebase-merge/msgnum")
total := g.getGitFileContents(g.gitWorkingFolder, "rebase-merge/end") total := g.getGitFileContents(g.gitWorkingFolder, "rebase-merge/end")
icon := g.props.getString(RebaseIcon, "\uE728 ") icon := g.props.getString(RebaseIcon, "\uE728 ")
return fmt.Sprintf("%s%s%s onto %s%s (%s/%s) at %s", icon, branchIcon, origin, branchIcon, onto, step, total, ref) g.HEAD = fmt.Sprintf("%s%s onto %s%s (%s/%s) at %s", icon, origin, branchIcon, onto, step, total, g.HEAD)
return
} }
if g.env.hasFolder(g.gitWorkingFolder + "/rebase-apply") { if g.env.hasFolder(g.gitWorkingFolder + "/rebase-apply") {
head := g.getGitFileContents(g.gitWorkingFolder, "rebase-apply/head-name") origin := getPrettyNameOrigin("rebase-apply/head-name")
origin := strings.Replace(head, "refs/heads/", "", 1)
origin = g.truncateBranch(origin)
step := g.getGitFileContents(g.gitWorkingFolder, "rebase-apply/next") step := g.getGitFileContents(g.gitWorkingFolder, "rebase-apply/next")
total := g.getGitFileContents(g.gitWorkingFolder, "rebase-apply/last") total := g.getGitFileContents(g.gitWorkingFolder, "rebase-apply/last")
icon := g.props.getString(RebaseIcon, "\uE728 ") icon := g.props.getString(RebaseIcon, "\uE728 ")
return fmt.Sprintf("%s%s%s (%s/%s) at %s", icon, branchIcon, origin, step, total, ref) g.HEAD = fmt.Sprintf("%s%s (%s/%s) at %s", icon, origin, step, total, g.HEAD)
return
} }
// merge // merge
if g.hasGitFile("MERGE_MSG") && g.hasGitFile("MERGE_HEAD") { commitIcon := g.props.getString(CommitIcon, "\uF417")
if g.hasGitFile("MERGE_MSG") {
icon := g.props.getString(MergeIcon, "\uE727 ") icon := g.props.getString(MergeIcon, "\uE727 ")
mergeContext := g.getGitFileContents(g.gitWorkingFolder, "MERGE_MSG") mergeContext := g.getGitFileContents(g.gitWorkingFolder, "MERGE_MSG")
matches := findNamedRegexMatch(`Merge (?P<type>(remote-tracking )?branch|commit|tag) '(?P<head>.*)' into`, mergeContext) matches := findNamedRegexMatch(`Merge (remote-tracking )?(?P<type>branch|commit|tag) '(?P<theirs>.*)'`, mergeContext)
// head := g.getGitRefFileSymbolicName("ORIG_HEAD")
if matches != nil && matches["head"] != "" { if matches != nil && matches["theirs"] != "" {
var headIcon string var headIcon, theirs string
switch matches["type"] { switch matches["type"] {
case "tag": case "tag":
headIcon = g.props.getString(TagIcon, "\uF412") headIcon = g.props.getString(TagIcon, "\uF412")
theirs = matches["theirs"]
case "commit": case "commit":
headIcon = g.props.getString(CommitIcon, "\uF417") headIcon = commitIcon
theirs = g.formatSHA(matches["theirs"])
default: default:
headIcon = branchIcon headIcon = branchIcon
theirs = g.formatHEAD(matches["theirs"])
} }
head := g.truncateBranch(matches["head"]) g.HEAD = fmt.Sprintf("%s%s%s into %s", icon, headIcon, theirs, formatDetached())
return fmt.Sprintf("%s%s%s into %s", icon, headIcon, head, ref) return
} }
} }
// sequencer status // sequencer status
@ -365,13 +406,17 @@ func (g *git) getGitHEADContext(ref string) string {
// the todo file. // the todo file.
if g.hasGitFile("CHERRY_PICK_HEAD") { if g.hasGitFile("CHERRY_PICK_HEAD") {
sha := g.getGitFileContents(g.gitWorkingFolder, "CHERRY_PICK_HEAD") sha := g.getGitFileContents(g.gitWorkingFolder, "CHERRY_PICK_HEAD")
icon := g.props.getString(CherryPickIcon, "\uE29B ") cherry := g.props.getString(CherryPickIcon, "\uE29B ")
return fmt.Sprintf("%s%s onto %s", icon, sha[0:6], ref) g.HEAD = fmt.Sprintf("%s%s%s onto %s", cherry, commitIcon, g.formatSHA(sha), formatDetached())
} else if g.hasGitFile("REVERT_HEAD") { return
}
if g.hasGitFile("REVERT_HEAD") {
sha := g.getGitFileContents(g.gitWorkingFolder, "REVERT_HEAD") sha := g.getGitFileContents(g.gitWorkingFolder, "REVERT_HEAD")
icon := g.props.getString(RevertIcon, "\uF0E2 ") revert := g.props.getString(RevertIcon, "\uF0E2 ")
return fmt.Sprintf("%s%s onto %s", icon, sha[0:6], ref) g.HEAD = fmt.Sprintf("%s%s%s onto %s", revert, commitIcon, g.formatSHA(sha), formatDetached())
} else if g.hasGitFile("sequencer/todo") { return
}
if g.hasGitFile("sequencer/todo") {
todo := g.getGitFileContents(g.gitWorkingFolder, "sequencer/todo") todo := g.getGitFileContents(g.gitWorkingFolder, "sequencer/todo")
matches := findNamedRegexMatch(`^(?P<action>p|pick|revert)\s+(?P<sha>\S+)`, todo) matches := findNamedRegexMatch(`^(?P<action>p|pick|revert)\s+(?P<sha>\S+)`, todo)
if matches != nil && matches["sha"] != "" { if matches != nil && matches["sha"] != "" {
@ -379,24 +424,33 @@ func (g *git) getGitHEADContext(ref string) string {
sha := matches["sha"] sha := matches["sha"]
switch action { switch action {
case "p", "pick": case "p", "pick":
icon := g.props.getString(CherryPickIcon, "\uE29B ") cherry := g.props.getString(CherryPickIcon, "\uE29B ")
return fmt.Sprintf("%s%s onto %s", icon, sha[0:6], ref) g.HEAD = fmt.Sprintf("%s%s%s onto %s", cherry, commitIcon, g.formatSHA(sha), formatDetached())
return
case "revert": case "revert":
icon := g.props.getString(RevertIcon, "\uF0E2 ") revert := g.props.getString(RevertIcon, "\uF0E2 ")
return fmt.Sprintf("%s%s onto %s", icon, sha[0:6], ref) g.HEAD = fmt.Sprintf("%s%s%s onto %s", revert, commitIcon, g.formatSHA(sha), formatDetached())
return
} }
} }
} }
return ref g.HEAD = formatDetached()
} }
func (g *git) truncateBranch(branch string) string { func (g *git) formatHEAD(head string) string {
maxLength := g.props.getInt(BranchMaxLength, 0) maxLength := g.props.getInt(BranchMaxLength, 0)
if maxLength == 0 || len(branch) < maxLength { if maxLength == 0 || len(head) < maxLength {
return branch return head
} }
symbol := g.props.getString(TruncateSymbol, "") symbol := g.props.getString(TruncateSymbol, "")
return branch[0:maxLength] + symbol return head[0:maxLength] + symbol
}
func (g *git) formatSHA(sha string) string {
if len(sha) <= 7 {
return sha
}
return sha[0:7]
} }
func (g *git) hasGitFile(file string) bool { func (g *git) hasGitFile(file string) bool {
@ -412,28 +466,32 @@ func (g *git) getGitRefFileSymbolicName(refFile string) string {
return g.getGitCommandOutput("name-rev", "--name-only", "--exclude=tags/*", ref) return g.getGitCommandOutput("name-rev", "--name-only", "--exclude=tags/*", ref)
} }
func (g *git) getPrettyHEADName() string { func (g *git) setPrettyHEADName() {
var ref string // we didn't fetch status, fallback to parsing the HEAD file
HEAD := g.getGitFileContents(g.gitWorkingFolder, "HEAD") if len(g.Hash) == 0 {
branchPrefix := "ref: refs/heads/" HEADRef := g.getGitFileContents(g.gitWorkingFolder, "HEAD")
if strings.HasPrefix(HEAD, branchPrefix) { if strings.HasPrefix(HEADRef, BRANCHPREFIX) {
ref = strings.TrimPrefix(HEAD, branchPrefix) branchName := strings.TrimPrefix(HEADRef, BRANCHPREFIX)
g.HEAD = fmt.Sprintf("%s%s", g.props.getString(BranchIcon, "\uE0A0"), g.formatHEAD(branchName))
return
}
// no branch, points to commit
if len(HEADRef) >= 7 {
g.Hash = HEADRef[0:7]
} }
if ref != "" {
ref = g.truncateBranch(ref)
return fmt.Sprintf("%s%s", g.props.getString(BranchIcon, "\uE0A0"), ref)
} }
// check for tag // check for tag
ref = g.getGitCommandOutput("describe", "--tags", "--exact-match") tagName := g.getGitCommandOutput("describe", "--tags", "--exact-match")
if ref != "" { if len(tagName) > 0 {
return fmt.Sprintf("%s%s", g.props.getString(TagIcon, "\uF412"), ref) g.HEAD = fmt.Sprintf("%s%s", g.props.getString(TagIcon, "\uF412"), tagName)
return
} }
// fallback to commit // fallback to commit
ref = g.getGitCommandOutput("rev-parse", "--short", "HEAD") if len(g.Hash) == 0 {
if ref == "" { g.HEAD = g.props.getString(NoCommitsIcon, "\uF594 ")
return g.props.getString(NoCommitsIcon, "\uF594 ") return
} }
return fmt.Sprintf("%s%s", g.props.getString(CommitIcon, "\uF417"), ref) g.HEAD = fmt.Sprintf("%s%s", g.props.getString(CommitIcon, "\uF417"), g.Hash)
} }
func (g *git) getStashContext() int { func (g *git) getStashContext() int {
@ -453,11 +511,6 @@ func (g *git) getWorktreeContext() int {
return len(worktreeFolders) return len(worktreeFolders)
} }
func (g *git) parseGitStatusInfo(branchInfo string) map[string]string {
var branchRegex = `^## (?P<local>\S+?)(\.{3}(?P<upstream>\S+?)( \[(?P<upstream_status>(ahead (?P<ahead>\d+)(, )?)?(behind (?P<behind>\d+))?(gone)?)])?)?$`
return findNamedRegexMatch(branchRegex, branchInfo)
}
func (g *git) getOriginURL(upstream string) string { func (g *git) getOriginURL(upstream string) string {
cfg, err := ini.Load(g.gitRootFolder + "/config") cfg, err := ini.Load(g.gitRootFolder + "/config")
if err != nil { if err != nil {

View file

@ -2,6 +2,7 @@ package main
import ( import (
"fmt" "fmt"
"strings"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -9,6 +10,7 @@ import (
const ( const (
changesColor = "#BD8BDE" changesColor = "#BD8BDE"
branchName = "main"
) )
func TestEnabledGitNotFound(t *testing.T) { func TestEnabledGitNotFound(t *testing.T) {
@ -76,299 +78,301 @@ func TestGetGitOutputForCommand(t *testing.T) {
assert.Equal(t, want, got) assert.Equal(t, want, got)
} }
type detachedContext struct {
currentCommit string
rebase string
rebaseMerge bool
rebaseApply bool
origin string
onto string
step string
total string
branchName string
tagName string
cherryPick bool
cherryPickSHA string
revert bool
revertSHA string
sequencer bool
sequencerTodo string
merge bool
mergeHEAD string
mergeMsgStart string
status string
}
func setupHEADContextEnv(context *detachedContext) *git {
env := new(MockedEnvironment)
env.On("isWsl", nil).Return(false)
env.On("hasFolder", "/rebase-merge").Return(context.rebaseMerge)
env.On("hasFolder", "/rebase-apply").Return(context.rebaseApply)
env.On("hasFolder", "/sequencer").Return(context.sequencer)
env.On("getFileContent", "/rebase-merge/head-name").Return(context.origin)
env.On("getFileContent", "/rebase-merge/onto").Return(context.onto)
env.On("getFileContent", "/rebase-merge/msgnum").Return(context.step)
env.On("getFileContent", "/rebase-apply/next").Return(context.step)
env.On("getFileContent", "/rebase-merge/end").Return(context.total)
env.On("getFileContent", "/rebase-apply/last").Return(context.total)
env.On("getFileContent", "/rebase-apply/head-name").Return(context.origin)
env.On("getFileContent", "/CHERRY_PICK_HEAD").Return(context.cherryPickSHA)
env.On("getFileContent", "/REVERT_HEAD").Return(context.revertSHA)
env.On("getFileContent", "/MERGE_MSG").Return(fmt.Sprintf("%s '%s' into %s", context.mergeMsgStart, context.mergeHEAD, context.onto))
env.On("getFileContent", "/sequencer/todo").Return(context.sequencerTodo)
env.On("getFileContent", "/HEAD").Return(context.branchName)
env.On("hasFilesInDir", "", "CHERRY_PICK_HEAD").Return(context.cherryPick)
env.On("hasFilesInDir", "", "REVERT_HEAD").Return(context.revert)
env.On("hasFilesInDir", "", "MERGE_MSG").Return(context.merge)
env.On("hasFilesInDir", "", "MERGE_HEAD").Return(context.merge)
env.On("hasFilesInDir", "", "sequencer/todo").Return(context.sequencer)
env.mockGitCommand(context.currentCommit, "rev-parse", "--short", "HEAD")
env.mockGitCommand(context.tagName, "describe", "--tags", "--exact-match")
env.mockGitCommand(context.origin, "name-rev", "--name-only", "--exclude=tags/*", context.origin)
env.mockGitCommand(context.onto, "name-rev", "--name-only", "--exclude=tags/*", context.onto)
env.mockGitCommand(context.branchName, "branch", "--show-current")
env.mockGitCommand(context.status, "status", "-unormal", "--short", "--branch")
env.On("getRuntimeGOOS", nil).Return("unix")
g := &git{
env: env,
gitWorkingFolder: "",
Working: &GitStatus{},
Staging: &GitStatus{},
}
return g
}
func (m *MockedEnvironment) mockGitCommand(returnValue string, args ...string) { func (m *MockedEnvironment) mockGitCommand(returnValue string, args ...string) {
args = append([]string{"-C", "", "--no-optional-locks", "-c", "core.quotepath=false", "-c", "color.status=false"}, args...) args = append([]string{"-C", "", "--no-optional-locks", "-c", "core.quotepath=false", "-c", "color.status=false"}, args...)
m.On("runCommand", "git", args).Return(returnValue, nil) m.On("runCommand", "git", args).Return(returnValue, nil)
} }
func TestGetGitDetachedCommitHash(t *testing.T) { func TestSetGitHEADContextClean(t *testing.T) {
want := "\uf417lalasha1" cases := []struct {
context := &detachedContext{ Case string
currentCommit: "lalasha1", Expected string
Ref string
RebaseMerge bool
RebaseApply bool
Merge bool
CherryPick bool
Revert bool
Sequencer bool
Ours string
Theirs string
Step string
Total string
}{
{Case: "detached on commit", Ref: DETACHED, Expected: "branch detached at commit 1234567"},
{Case: "not detached, clean", Ref: "main", Expected: "branch main"},
{
Case: "rebase merge",
Ref: DETACHED,
Expected: "rebase branch origin/main onto branch main (1/2) at commit 1234567",
RebaseMerge: true,
Ours: "refs/heads/origin/main",
Theirs: "main",
Step: "1",
Total: "2",
},
{
Case: "rebase apply",
Ref: DETACHED,
Expected: "rebase branch origin/main (1/2) at commit 1234567",
RebaseApply: true,
Ours: "refs/heads/origin/main",
Step: "1",
Total: "2",
},
{
Case: "merge branch",
Ref: "main",
Expected: "merge branch feat-1 into branch main",
Merge: true,
Theirs: "branch 'feat-1'",
Ours: "main",
},
{
Case: "merge commit",
Ref: "main",
Expected: "merge commit 1234567 into branch main",
Merge: true,
Theirs: "commit '123456789101112'",
Ours: "main",
},
{
Case: "merge tag",
Ref: "main",
Expected: "merge tag 1.2.4 into branch main",
Merge: true,
Theirs: "tag '1.2.4'",
Ours: "main",
},
{
Case: "cherry pick",
Ref: "main",
Expected: "pick commit 1234567 onto branch main",
CherryPick: true,
Theirs: "123456789101012",
Ours: "main",
},
{
Case: "revert",
Ref: "main",
Expected: "revert commit 1234567 onto branch main",
Revert: true,
Theirs: "123456789101012",
Ours: "main",
},
{
Case: "sequencer cherry",
Ref: "main",
Expected: "pick commit 1234567 onto branch main",
Sequencer: true,
Theirs: "pick 123456789101012",
Ours: "main",
},
{
Case: "sequencer cherry p",
Ref: "main",
Expected: "pick commit 1234567 onto branch main",
Sequencer: true,
Theirs: "p 123456789101012",
Ours: "main",
},
{
Case: "sequencer revert",
Ref: "main",
Expected: "revert commit 1234567 onto branch main",
Sequencer: true,
Theirs: "revert 123456789101012",
Ours: "main",
},
}
for _, tc := range cases {
env := new(MockedEnvironment)
env.On("getRuntimeGOOS", nil).Return("unix")
env.On("isWsl", nil).Return(false)
env.mockGitCommand("", "describe", "--tags", "--exact-match")
env.mockGitCommand(tc.Theirs, "name-rev", "--name-only", "--exclude=tags/*", tc.Theirs)
env.mockGitCommand(tc.Ours, "name-rev", "--name-only", "--exclude=tags/*", tc.Ours)
// rebase merge
env.On("hasFolder", "/rebase-merge").Return(tc.RebaseMerge)
env.On("getFileContent", "/rebase-merge/head-name").Return(tc.Ours)
env.On("getFileContent", "/rebase-merge/onto").Return(tc.Theirs)
env.On("getFileContent", "/rebase-merge/msgnum").Return(tc.Step)
env.On("getFileContent", "/rebase-merge/end").Return(tc.Total)
// rebase apply
env.On("hasFolder", "/rebase-apply").Return(tc.RebaseApply)
env.On("getFileContent", "/rebase-apply/head-name").Return(tc.Ours)
env.On("getFileContent", "/rebase-apply/next").Return(tc.Step)
env.On("getFileContent", "/rebase-apply/last").Return(tc.Total)
// merge
env.On("hasFilesInDir", "", "MERGE_MSG").Return(tc.Merge)
env.On("getFileContent", "/MERGE_MSG").Return(fmt.Sprintf("Merge %s into %s", tc.Theirs, tc.Ours))
// cherry pick
env.On("hasFilesInDir", "", "CHERRY_PICK_HEAD").Return(tc.CherryPick)
env.On("getFileContent", "/CHERRY_PICK_HEAD").Return(tc.Theirs)
// revert
env.On("hasFilesInDir", "", "REVERT_HEAD").Return(tc.Revert)
env.On("getFileContent", "/REVERT_HEAD").Return(tc.Theirs)
// sequencer
env.On("hasFilesInDir", "", "sequencer/todo").Return(tc.Sequencer)
env.On("getFileContent", "/sequencer/todo").Return(tc.Theirs)
g := &git{
env: env,
props: map[Property]interface{}{
BranchIcon: "branch ",
CommitIcon: "commit ",
RebaseIcon: "rebase ",
MergeIcon: "merge ",
CherryPickIcon: "pick ",
TagIcon: "tag ",
RevertIcon: "revert ",
},
Hash: "1234567",
Ref: tc.Ref,
}
g.setGitHEADContext()
assert.Equal(t, tc.Expected, g.HEAD, tc.Case)
} }
g := setupHEADContextEnv(context)
got := g.getGitHEADContext("")
assert.Equal(t, want, got)
} }
func TestGetGitHEADContextTagName(t *testing.T) { func TestSetPrettyHEADName(t *testing.T) {
want := "\uf412lalasha1" cases := []struct {
context := &detachedContext{ Case string
currentCommit: "whatever", Expected string
tagName: "lalasha1", Hash string
Tag string
HEAD string
}{
{Case: "main", Expected: "branch main", HEAD: BRANCHPREFIX + "main"},
{Case: "no hash", Expected: "commit 1234567", HEAD: "12345678910"},
{Case: "hash on tag", Hash: "132312322321", Expected: "tag tag-1", HEAD: "12345678910", Tag: "tag-1"},
{Case: "no hash on tag", Expected: "tag tag-1", Tag: "tag-1"},
{Case: "hash on commit", Hash: "1234567", Expected: "commit 1234567"},
{Case: "no hash on commit", Expected: "commit 1234567", HEAD: "12345678910"},
}
for _, tc := range cases {
env := new(MockedEnvironment)
env.On("getFileContent", "/HEAD").Return(tc.HEAD)
env.On("getRuntimeGOOS", nil).Return("unix")
env.On("isWsl", nil).Return(false)
env.mockGitCommand(tc.Tag, "describe", "--tags", "--exact-match")
g := &git{
env: env,
props: map[Property]interface{}{
BranchIcon: "branch ",
CommitIcon: "commit ",
TagIcon: "tag ",
},
Hash: tc.Hash,
}
g.setPrettyHEADName()
assert.Equal(t, tc.Expected, g.HEAD, tc.Case)
} }
g := setupHEADContextEnv(context)
got := g.getGitHEADContext("")
assert.Equal(t, want, got)
} }
func TestGetGitHEADContextRebaseMerge(t *testing.T) { func TestSetGitStatus(t *testing.T) {
want := "\ue728 \ue0a0cool-feature-bro onto \ue0a0main (2/3) at \uf417whatever" cases := []struct {
context := &detachedContext{ Case string
currentCommit: "whatever", Output string
rebase: "true", ExpectedWorking *GitStatus
rebaseMerge: true, ExpectedStaging *GitStatus
origin: "cool-feature-bro", ExpectedHash string
onto: "main", ExpectedRef string
step: "2", ExpectedUpstream string
total: "3", ExpectedAhead int
ExpectedBehind int
}{
{
Case: "all different options on working and staging, no remote",
Output: `
# branch.oid 1234567891011121314
# branch.head rework-git-status
1 .R N...
1 .C N...
1 .M N...
1 .m N...
1 .? N...
1 .D N...
1 .A N...
1 .U N...
1 A. N...
`,
ExpectedWorking: &GitStatus{Modified: 4, Added: 2, Deleted: 1, Unmerged: 1},
ExpectedStaging: &GitStatus{Added: 1},
ExpectedHash: "1234567",
ExpectedRef: "rework-git-status",
},
{
Case: "all different options on working and staging, with remote",
Output: `
# branch.oid 1234567891011121314
# branch.head rework-git-status
# branch.upstream origin/rework-git-status
1 .R N...
1 .C N...
1 .M N...
1 .m N...
1 .? N...
1 .D N...
1 .A N...
1 .U N...
1 A. N...
`,
ExpectedWorking: &GitStatus{Modified: 4, Added: 2, Deleted: 1, Unmerged: 1},
ExpectedStaging: &GitStatus{Added: 1},
ExpectedUpstream: "origin/rework-git-status",
ExpectedHash: "1234567",
ExpectedRef: "rework-git-status",
},
{
Case: "remote with equal branch",
Output: `
# branch.oid 1234567891011121314
# branch.head rework-git-status
# branch.upstream origin/rework-git-status
`,
ExpectedUpstream: "origin/rework-git-status",
ExpectedHash: "1234567",
ExpectedRef: "rework-git-status",
},
{
Case: "remote with branch status",
Output: `
# branch.oid 1234567891011121314
# branch.head rework-git-status
# branch.upstream origin/rework-git-status
# branch.ab +2 -1
`,
ExpectedUpstream: "origin/rework-git-status",
ExpectedHash: "1234567",
ExpectedRef: "rework-git-status",
ExpectedAhead: 2,
ExpectedBehind: 1,
},
} }
g := setupHEADContextEnv(context) for _, tc := range cases {
got := g.getGitHEADContext("") env := new(MockedEnvironment)
assert.Equal(t, want, got) env.On("getRuntimeGOOS", nil).Return("unix")
env.On("isWsl", nil).Return(false)
env.mockGitCommand(strings.ReplaceAll(tc.Output, "\t", ""), "status", "-unormal", "--branch", "--porcelain=2")
g := &git{
env: env,
} }
if tc.ExpectedWorking == nil {
func TestGetGitHEADContextRebaseApply(t *testing.T) { tc.ExpectedWorking = &GitStatus{}
want := "\ue728 \ue0a0cool-feature-bro (2/3) at \uf417whatever"
context := &detachedContext{
currentCommit: "whatever",
rebase: "true",
rebaseApply: true,
origin: "cool-feature-bro",
step: "2",
total: "3",
} }
g := setupHEADContextEnv(context) if tc.ExpectedStaging == nil {
got := g.getGitHEADContext("") tc.ExpectedStaging = &GitStatus{}
assert.Equal(t, want, got)
} }
g.setGitStatus()
func TestGetGitHEADContextRebaseUnknown(t *testing.T) { assert.Equal(t, tc.ExpectedStaging, g.Staging, tc.Case)
want := "\uf417whatever" assert.Equal(t, tc.ExpectedWorking, g.Working, tc.Case)
context := &detachedContext{ assert.Equal(t, tc.ExpectedHash, g.Hash, tc.Case)
currentCommit: "whatever", assert.Equal(t, tc.ExpectedRef, g.Ref, tc.Case)
rebase: "true", assert.Equal(t, tc.ExpectedUpstream, g.Upstream, tc.Case)
assert.Equal(t, tc.ExpectedAhead, g.Ahead, tc.Case)
assert.Equal(t, tc.ExpectedBehind, g.Behind, tc.Case)
} }
g := setupHEADContextEnv(context)
got := g.getGitHEADContext("")
assert.Equal(t, want, got)
}
func TestGetGitHEADContextCherryPickOnBranch(t *testing.T) {
want := "\ue29b pickme onto \ue0a0main"
context := &detachedContext{
currentCommit: "whatever",
branchName: "main",
cherryPick: true,
cherryPickSHA: "pickme",
}
g := setupHEADContextEnv(context)
got := g.getGitHEADContext("main")
assert.Equal(t, want, got)
}
func TestGetGitHEADContextCherryPickOnTag(t *testing.T) {
want := "\ue29b pickme onto \uf412v3.4.6"
context := &detachedContext{
currentCommit: "whatever",
tagName: "v3.4.6",
cherryPick: true,
cherryPickSHA: "pickme",
}
g := setupHEADContextEnv(context)
got := g.getGitHEADContext("")
assert.Equal(t, want, got)
}
func TestGetGitHEADContextRevertOnBranch(t *testing.T) {
want := "\uf0e2 012345 onto \ue0a0main"
context := &detachedContext{
currentCommit: "whatever",
branchName: "main",
revert: true,
revertSHA: "01234567",
}
g := setupHEADContextEnv(context)
got := g.getGitHEADContext("main")
assert.Equal(t, want, got)
}
func TestGetGitHEADContextRevertOnTag(t *testing.T) {
want := "\uf0e2 012345 onto \uf412v3.4.6"
context := &detachedContext{
currentCommit: "whatever",
tagName: "v3.4.6",
revert: true,
revertSHA: "01234567",
}
g := setupHEADContextEnv(context)
got := g.getGitHEADContext("")
assert.Equal(t, want, got)
}
func TestGetGitHEADContextSequencerCherryPickOnBranch(t *testing.T) {
want := "\ue29b pickme onto \ue0a0main"
context := &detachedContext{
currentCommit: "whatever",
branchName: "main",
sequencer: true,
sequencerTodo: "pick pickme message\npick notme message",
}
g := setupHEADContextEnv(context)
got := g.getGitHEADContext("main")
assert.Equal(t, want, got)
}
func TestGetGitHEADContextSequencerCherryPickOnTag(t *testing.T) {
want := "\ue29b pickme onto \uf412v3.4.6"
context := &detachedContext{
currentCommit: "whatever",
tagName: "v3.4.6",
sequencer: true,
sequencerTodo: "pick pickme message\npick notme message",
}
g := setupHEADContextEnv(context)
got := g.getGitHEADContext("")
assert.Equal(t, want, got)
}
func TestGetGitHEADContextSequencerRevertOnBranch(t *testing.T) {
want := "\uf0e2 012345 onto \ue0a0main"
context := &detachedContext{
currentCommit: "whatever",
branchName: "main",
sequencer: true,
sequencerTodo: "revert 01234567 message\nrevert notme message",
}
g := setupHEADContextEnv(context)
got := g.getGitHEADContext("main")
assert.Equal(t, want, got)
}
func TestGetGitHEADContextSequencerRevertOnTag(t *testing.T) {
want := "\uf0e2 012345 onto \uf412v3.4.6"
context := &detachedContext{
currentCommit: "whatever",
tagName: "v3.4.6",
sequencer: true,
sequencerTodo: "revert 01234567 message\nrevert notme message",
}
g := setupHEADContextEnv(context)
got := g.getGitHEADContext("")
assert.Equal(t, want, got)
}
func TestGetGitHEADContextMerge(t *testing.T) {
want := "\ue727 \ue0a0feat into \ue0a0main"
context := &detachedContext{
merge: true,
mergeHEAD: "feat",
mergeMsgStart: "Merge branch",
}
g := setupHEADContextEnv(context)
got := g.getGitHEADContext("main")
assert.Equal(t, want, got)
}
func TestGetGitHEADContextMergeRemote(t *testing.T) {
want := "\ue727 \ue0a0feat into \ue0a0main"
context := &detachedContext{
merge: true,
mergeHEAD: "feat",
mergeMsgStart: "Merge remote-tracking branch",
}
g := setupHEADContextEnv(context)
got := g.getGitHEADContext("main")
assert.Equal(t, want, got)
}
func TestGetGitHEADContextMergeTag(t *testing.T) {
want := "\ue727 \uf412v7.8.9 into \ue0a0main"
context := &detachedContext{
merge: true,
mergeHEAD: "v7.8.9",
mergeMsgStart: "Merge tag",
}
g := setupHEADContextEnv(context)
got := g.getGitHEADContext("main")
assert.Equal(t, want, got)
}
func TestGetGitHEADContextMergeCommit(t *testing.T) {
want := "\ue727 \uf4178d7e869 into \ue0a0main"
context := &detachedContext{
merge: true,
mergeHEAD: "8d7e869",
mergeMsgStart: "Merge commit",
}
g := setupHEADContextEnv(context)
got := g.getGitHEADContext("main")
assert.Equal(t, want, got)
}
func TestGetGitHEADContextMergeIntoTag(t *testing.T) {
want := "\ue727 \ue0a0feat into \uf412v3.4.6"
context := &detachedContext{
tagName: "v3.4.6",
merge: true,
mergeHEAD: "feat",
mergeMsgStart: "Merge branch",
}
g := setupHEADContextEnv(context)
got := g.getGitHEADContext("")
assert.Equal(t, want, got)
} }
func TestGetStashContextZeroEntries(t *testing.T) { func TestGetStashContextZeroEntries(t *testing.T) {
@ -392,62 +396,6 @@ func TestGetStashContextZeroEntries(t *testing.T) {
} }
} }
func TestParseGitBranchInfoEqual(t *testing.T) {
g := git{}
branchInfo := "## master...origin/master"
got := g.parseGitStatusInfo(branchInfo)
assert.Equal(t, "master", got["local"])
assert.Equal(t, "origin/master", got["upstream"])
assert.Empty(t, got["ahead"])
assert.Empty(t, got["behind"])
}
func TestParseGitBranchInfoAhead(t *testing.T) {
g := git{}
branchInfo := "## master...origin/master [ahead 1]"
got := g.parseGitStatusInfo(branchInfo)
assert.Equal(t, "master", got["local"])
assert.Equal(t, "origin/master", got["upstream"])
assert.Equal(t, "1", got["ahead"])
assert.Empty(t, got["behind"])
}
func TestParseGitBranchInfoBehind(t *testing.T) {
g := git{}
branchInfo := "## master...origin/master [behind 1]"
got := g.parseGitStatusInfo(branchInfo)
assert.Equal(t, "master", got["local"])
assert.Equal(t, "origin/master", got["upstream"])
assert.Equal(t, "1", got["behind"])
assert.Empty(t, got["ahead"])
}
func TestParseGitBranchInfoBehindandAhead(t *testing.T) {
g := git{}
branchInfo := "## master...origin/master [ahead 1, behind 2]"
got := g.parseGitStatusInfo(branchInfo)
assert.Equal(t, "master", got["local"])
assert.Equal(t, "origin/master", got["upstream"])
assert.Equal(t, "2", got["behind"])
assert.Equal(t, "1", got["ahead"])
}
func TestParseGitBranchInfoNoRemote(t *testing.T) {
g := git{}
branchInfo := "## master"
got := g.parseGitStatusInfo(branchInfo)
assert.Equal(t, "master", got["local"])
assert.Empty(t, got["upstream"])
}
func TestParseGitBranchInfoRemoteGone(t *testing.T) {
g := git{}
branchInfo := "## test-branch...origin/test-branch [gone]"
got := g.parseGitStatusInfo(branchInfo)
assert.Equal(t, "test-branch", got["local"])
assert.Equal(t, "gone", got["upstream_status"])
}
func TestGitStatusUnmerged(t *testing.T) { func TestGitStatusUnmerged(t *testing.T) {
expected := "x1" expected := "x1"
status := &GitStatus{ status := &GitStatus{
@ -471,71 +419,6 @@ func TestGitStatusEmpty(t *testing.T) {
assert.Equal(t, expected, status.String()) assert.Equal(t, expected, status.String())
} }
func TestParseGitStatsWorking(t *testing.T) {
status := &GitStatus{}
output := []string{
"## amazing-feat",
" M change.go",
"DD change.go",
" ? change.go",
" ? change.go",
" A change.go",
" U change.go",
" R change.go",
" C change.go",
}
status.parse(output, true)
assert.Equal(t, 3, status.Modified)
assert.Equal(t, 1, status.Unmerged)
assert.Equal(t, 3, status.Added)
assert.Equal(t, 1, status.Deleted)
assert.True(t, status.Changed)
}
func TestParseGitStatsStaging(t *testing.T) {
status := &GitStatus{}
output := []string{
"## amazing-feat",
" M change.go",
"DD change.go",
" ? change.go",
"?? change.go",
" A change.go",
"DU change.go",
"MR change.go",
"AC change.go",
}
status.parse(output, false)
assert.Equal(t, 1, status.Modified)
assert.Equal(t, 0, status.Unmerged)
assert.Equal(t, 1, status.Added)
assert.Equal(t, 2, status.Deleted)
assert.True(t, status.Changed)
}
func TestParseGitStatsNoChanges(t *testing.T) {
status := &GitStatus{}
expected := &GitStatus{}
output := []string{
"## amazing-feat",
}
status.parse(output, false)
assert.Equal(t, expected, status)
assert.False(t, status.Changed)
}
func TestParseGitStatsInvalidLine(t *testing.T) {
status := &GitStatus{}
expected := &GitStatus{}
output := []string{
"## amazing-feat",
"#",
}
status.parse(output, false)
assert.Equal(t, expected, status)
assert.False(t, status.Changed)
}
func TestGitUpstream(t *testing.T) { func TestGitUpstream(t *testing.T) {
cases := []struct { cases := []struct {
Case string Case string
@ -580,7 +463,7 @@ func TestGetBranchStatus(t *testing.T) {
Behind int Behind int
Upstream string Upstream string
}{ }{
{Case: "Equal with remote", Expected: " equal", Upstream: "main"}, {Case: "Equal with remote", Expected: " equal", Upstream: branchName},
{Case: "Ahead", Expected: " up2", Ahead: 2}, {Case: "Ahead", Expected: " up2", Ahead: 2},
{Case: "Behind", Expected: " down8", Behind: 8}, {Case: "Behind", Expected: " down8", Behind: 8},
{Case: "Behind and ahead", Expected: " up7 down8", Behind: 8, Ahead: 7}, {Case: "Behind and ahead", Expected: " up7 down8", Behind: 8, Ahead: 7},
@ -657,7 +540,7 @@ func TestTruncateBranch(t *testing.T) {
g := &git{ g := &git{
props: props, props: props,
} }
assert.Equal(t, tc.Expected, g.truncateBranch(tc.Branch), tc.Case) assert.Equal(t, tc.Expected, g.formatHEAD(tc.Branch), tc.Case)
} }
} }
@ -684,7 +567,7 @@ func TestTruncateBranchWithSymbol(t *testing.T) {
g := &git{ g := &git{
props: props, props: props,
} }
assert.Equal(t, tc.Expected, g.truncateBranch(tc.Branch), tc.Case) assert.Equal(t, tc.Expected, g.formatHEAD(tc.Branch), tc.Case)
} }
} }
@ -730,10 +613,10 @@ func TestGitTemplateString(t *testing.T) {
}{ }{
{ {
Case: "Only HEAD name", Case: "Only HEAD name",
Expected: "main", Expected: branchName,
Template: "{{ .HEAD }}", Template: "{{ .HEAD }}",
Git: &git{ Git: &git{
HEAD: "main", HEAD: branchName,
Behind: 2, Behind: 2,
}, },
}, },
@ -742,23 +625,20 @@ func TestGitTemplateString(t *testing.T) {
Expected: "main \uF044 +2 ~3", Expected: "main \uF044 +2 ~3",
Template: "{{ .HEAD }}{{ if .Working.Changed }} \uF044 {{ .Working.String }}{{ end }}", Template: "{{ .HEAD }}{{ if .Working.Changed }} \uF044 {{ .Working.String }}{{ end }}",
Git: &git{ Git: &git{
HEAD: "main", HEAD: branchName,
Working: &GitStatus{ Working: &GitStatus{
Added: 2, Added: 2,
Modified: 3, Modified: 3,
Changed: true,
}, },
}, },
}, },
{ {
Case: "No working area changes", Case: "No working area changes",
Expected: "main", Expected: branchName,
Template: "{{ .HEAD }}{{ if .Working.Changed }} \uF044 {{ .Working.String }}{{ end }}", Template: "{{ .HEAD }}{{ if .Working.Changed }} \uF044 {{ .Working.String }}{{ end }}",
Git: &git{ Git: &git{
HEAD: "main", HEAD: branchName,
Working: &GitStatus{ Working: &GitStatus{},
Changed: false,
},
}, },
}, },
{ {
@ -766,16 +646,14 @@ func TestGitTemplateString(t *testing.T) {
Expected: "main \uF046 +5 ~1 \uF044 +2 ~3", Expected: "main \uF046 +5 ~1 \uF044 +2 ~3",
Template: "{{ .HEAD }}{{ if .Staging.Changed }} \uF046 {{ .Staging.String }}{{ end }}{{ if .Working.Changed }} \uF044 {{ .Working.String }}{{ end }}", Template: "{{ .HEAD }}{{ if .Staging.Changed }} \uF046 {{ .Staging.String }}{{ end }}{{ if .Working.Changed }} \uF044 {{ .Working.String }}{{ end }}",
Git: &git{ Git: &git{
HEAD: "main", HEAD: branchName,
Working: &GitStatus{ Working: &GitStatus{
Added: 2, Added: 2,
Modified: 3, Modified: 3,
Changed: true,
}, },
Staging: &GitStatus{ Staging: &GitStatus{
Added: 5, Added: 5,
Modified: 1, Modified: 1,
Changed: true,
}, },
}, },
}, },
@ -784,16 +662,14 @@ func TestGitTemplateString(t *testing.T) {
Expected: "main \uF046 +5 ~1 | \uF044 +2 ~3", Expected: "main \uF046 +5 ~1 | \uF044 +2 ~3",
Template: "{{ .HEAD }}{{ if .Staging.Changed }} \uF046 {{ .Staging.String }}{{ end }}{{ if and (.Working.Changed) (.Staging.Changed) }} |{{ end }}{{ if .Working.Changed }} \uF044 {{ .Working.String }}{{ end }}", //nolint:lll Template: "{{ .HEAD }}{{ if .Staging.Changed }} \uF046 {{ .Staging.String }}{{ end }}{{ if and (.Working.Changed) (.Staging.Changed) }} |{{ end }}{{ if .Working.Changed }} \uF044 {{ .Working.String }}{{ end }}", //nolint:lll
Git: &git{ Git: &git{
HEAD: "main", HEAD: branchName,
Working: &GitStatus{ Working: &GitStatus{
Added: 2, Added: 2,
Modified: 3, Modified: 3,
Changed: true,
}, },
Staging: &GitStatus{ Staging: &GitStatus{
Added: 5, Added: 5,
Modified: 1, Modified: 1,
Changed: true,
}, },
}, },
}, },
@ -802,26 +678,24 @@ func TestGitTemplateString(t *testing.T) {
Expected: "main \uF046 +5 ~1 | \uF044 +2 ~3 \uf692 3", Expected: "main \uF046 +5 ~1 | \uF044 +2 ~3 \uf692 3",
Template: "{{ .HEAD }}{{ if .Staging.Changed }} \uF046 {{ .Staging.String }}{{ end }}{{ if and (.Working.Changed) (.Staging.Changed) }} |{{ end }}{{ if .Working.Changed }} \uF044 {{ .Working.String }}{{ end }}{{ if gt .StashCount 0 }} \uF692 {{ .StashCount }}{{ end }}", //nolint:lll Template: "{{ .HEAD }}{{ if .Staging.Changed }} \uF046 {{ .Staging.String }}{{ end }}{{ if and (.Working.Changed) (.Staging.Changed) }} |{{ end }}{{ if .Working.Changed }} \uF044 {{ .Working.String }}{{ end }}{{ if gt .StashCount 0 }} \uF692 {{ .StashCount }}{{ end }}", //nolint:lll
Git: &git{ Git: &git{
HEAD: "main", HEAD: branchName,
Working: &GitStatus{ Working: &GitStatus{
Added: 2, Added: 2,
Modified: 3, Modified: 3,
Changed: true,
}, },
Staging: &GitStatus{ Staging: &GitStatus{
Added: 5, Added: 5,
Modified: 1, Modified: 1,
Changed: true,
}, },
StashCount: 3, StashCount: 3,
}, },
}, },
{ {
Case: "No local changes", Case: "No local changes",
Expected: "main", Expected: branchName,
Template: "{{ .HEAD }}{{ if .Staging.Changed }} \uF046{{ .Staging.String }}{{ end }}{{ if .Working.Changed }} \uF044{{ .Working.String }}{{ end }}", Template: "{{ .HEAD }}{{ if .Staging.Changed }} \uF046{{ .Staging.String }}{{ end }}{{ if .Working.Changed }} \uF044{{ .Working.String }}{{ end }}",
Git: &git{ Git: &git{
HEAD: "main", HEAD: branchName,
Staging: &GitStatus{}, Staging: &GitStatus{},
Working: &GitStatus{}, Working: &GitStatus{},
}, },
@ -831,7 +705,7 @@ func TestGitTemplateString(t *testing.T) {
Expected: "from GitHub on main", Expected: "from GitHub on main",
Template: "from {{ .UpstreamIcon }} on {{ .HEAD }}", Template: "from {{ .UpstreamIcon }} on {{ .HEAD }}",
Git: &git{ Git: &git{
HEAD: "main", HEAD: branchName,
Staging: &GitStatus{}, Staging: &GitStatus{},
Working: &GitStatus{}, Working: &GitStatus{},
UpstreamIcon: "GitHub", UpstreamIcon: "GitHub",