Repository: JanDeDobbeleer/oh-my-posh
Pull request #1389 (2021-12-08 11:03:13)

feat(git): better logic
### Prerequisites

- [x] I have read and understood the `CONTRIBUTING` guide
- [x] The commit message follows the [conventional commits][cc] guidelines
- [x] Tests for the changes have been added (for bug fixes/features)
- [x] Docs have been added / updated (for bug fixes/features)

### Description

Makes the git segment more efficient and better tested

<!---

Tips:

If you're not comfortable with working with Git, we're working a guide (https://ohmyposh.dev/docs/contributing_git) to help you out.
Oh My Posh advises GitKraken (https://www.gitkraken.com/invite/nQmDPR9D) as your preferred cross platform Git GUI power tool.

-->

[cc]: https://www.conventionalcommits.org/en/v1.0.0/#summary



Patch:
diff --git a/src/environment.go b/src/environment.go
index 8d828544..81480cee 100644
--- a/src/environment.go
+++ b/src/environment.go
@@ -270,9 +270,6 @@ func (env *environment) getRuntimeGOOS() string {
 }
 
 func (env *environment) runCommand(command string, args ...string) (string, error) {
-	if env.getRuntimeGOOS() == windowsPlatform && !strings.HasSuffix(command, ".exe") {
-		command += ".exe"
-	}
 	defer env.trace(time.Now(), "runCommand", append([]string{command}, args...)...)
 	if cmd, ok := env.cmdCache.get(command); ok {
 		command = cmd
diff --git a/src/segment_deprecated.go b/src/segment_deprecated.go
index 6436799c..e6f1a2d6 100644
--- a/src/segment_deprecated.go
+++ b/src/segment_deprecated.go
@@ -87,13 +87,15 @@ func (g *git) deprecatedString(statusColorsEnabled bool) string {
 	if len(g.BranchStatus) > 0 {
 		buffer.WriteString(g.BranchStatus)
 	}
-	if g.Staging.Changed {
+
+	// status
+	if g.Staging.Changed() {
 		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, " |"))
 	}
-	if g.Working.Changed {
+	if g.Working.Changed() {
 		fmt.Fprint(buffer, g.getStatusDetailString(g.Working, WorkingColor, LocalWorkingIcon, " \uF044"))
 	}
 	if g.StashCount != 0 {
@@ -114,7 +116,7 @@ func (g *git) SetStatusColor() {
 }
 
 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)
 	} else if g.Ahead > 0 && g.Behind > 0 {
 		return g.props.getColor(AheadAndBehindColor, defaultValue)
diff --git a/src/segment_git.go b/src/segment_git.go
index a9cebba3..e02fe8a9 100644
--- a/src/segment_git.go
+++ b/src/segment_git.go
@@ -14,37 +14,25 @@ type GitStatus struct {
 	Deleted  int
 	Added    int
 	Modified int
-	Changed  bool
 }
 
-func (s *GitStatus) parse(output []string, working bool) {
-	if len(output) <= 1 {
+func (s *GitStatus) add(code string) {
+	switch code {
+	case ".":
 		return
+	case "D":
+		s.Deleted++
+	case "A", "?":
+		s.Added++
+	case "U":
+		s.Unmerged++
+	case "M", "R", "C", "m":
+		s.Modified++
 	}
-	for _, line := range output[1:] {
-		if len(line) < 2 {
-			continue
-		}
-		code := line[0:1]
-		if working {
-			code = line[1:2]
-		}
-		switch code {
-		case "?":
-			if working {
-				s.Added++
-			}
-		case "D":
-			s.Deleted++
-		case "A":
-			s.Added++
-		case "U":
-			s.Unmerged++
-		case "M", "R", "C", "m":
-			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 {
@@ -71,6 +59,8 @@ type git struct {
 	Ahead         int
 	Behind        int
 	HEAD          string
+	Ref           string
+	Hash          string
 	BranchStatus  string
 	Upstream      string
 	UpstreamIcon  string
@@ -133,6 +123,9 @@ const (
 	GitlabIcon Property = "gitlab_icon"
 	// GitIcon shows when the upstream can't be identified
 	GitIcon Property = "git_icon"
+
+	DETACHED     = "(detached)"
+	BRANCHPREFIX = "ref: refs/heads/"
 )
 
 func (g *git) enabled() bool {
@@ -147,12 +140,10 @@ func (g *git) enabled() bool {
 		return false
 	}
 
-	g.Staging = &GitStatus{}
-	g.Working = &GitStatus{}
-
 	if gitdir.isDir {
 		g.gitWorkingFolder = gitdir.path
 		g.gitRootFolder = gitdir.path
+		g.gitWorktreeFolder = strings.TrimSuffix(gitdir.path, ".git")
 		return true
 	}
 	// handle worktree
@@ -186,10 +177,15 @@ func (g *git) string() string {
 	statusColorsEnabled := g.props.getBool(StatusColorsEnabled, false)
 	displayStatus := g.props.getOneOfBool(FetchStatus, DisplayStatus, false)
 	if !displayStatus {
-		g.HEAD = g.getPrettyHEADName()
+		g.setPrettyHEADName()
 	}
 	if displayStatus || statusColorsEnabled {
 		g.setGitStatus()
+		g.setGitHEADContext()
+		g.setBranchStatus()
+	} else {
+		g.Working = &GitStatus{}
+		g.Staging = &GitStatus{}
 	}
 	if g.Upstream != "" && g.props.getOneOfBool(FetchUpstreamIcon, DisplayUpstreamIcon, false) {
 		g.UpstreamIcon = g.getUpstreamIcon()
@@ -228,23 +224,26 @@ func (g *git) init(props properties, env environmentInfo) {
 	g.env = env
 }
 
-func (g *git) getBranchStatus() string {
-	if g.Ahead > 0 && g.Behind > 0 {
-		return fmt.Sprintf(" %s%d %s%d", g.props.getString(BranchAheadIcon, "\u2191"), g.Ahead, g.props.getString(BranchBehindIcon, "\u2193"), g.Behind)
-	}
-	if g.Ahead > 0 {
-		return fmt.Sprintf(" %s%d", g.props.getString(BranchAheadIcon, "\u2191"), g.Ahead)
-	}
-	if g.Behind > 0 {
-		return fmt.Sprintf(" %s%d", g.props.getString(BranchBehindIcon, "\u2193"), g.Behind)
-	}
-	if g.Behind == 0 && g.Ahead == 0 && g.Upstream != "" {
-		return fmt.Sprintf(" %s", g.props.getString(BranchIdenticalIcon, "\u2261"))
-	}
-	if g.Upstream == "" {
-		return fmt.Sprintf(" %s", g.props.getString(BranchGoneIcon, "\u2262"))
+func (g *git) setBranchStatus() {
+	getBranchStatus := func() string {
+		if g.Ahead > 0 && g.Behind > 0 {
+			return fmt.Sprintf(" %s%d %s%d", g.props.getString(BranchAheadIcon, "\u2191"), g.Ahead, g.props.getString(BranchBehindIcon, "\u2193"), g.Behind)
+		}
+		if g.Ahead > 0 {
+			return fmt.Sprintf(" %s%d", g.props.getString(BranchAheadIcon, "\u2191"), g.Ahead)
+		}
+		if g.Behind > 0 {
+			return fmt.Sprintf(" %s%d", g.props.getString(BranchBehindIcon, "\u2193"), g.Behind)
+		}
+		if g.Behind == 0 && g.Ahead == 0 && g.Upstream != "" {
+			return fmt.Sprintf(" %s", g.props.getString(BranchIdenticalIcon, "\u2261"))
+		}
+		if g.Upstream == "" {
+			return fmt.Sprintf(" %s", g.props.getString(BranchGoneIcon, "\u2262"))
+		}
+		return ""
 	}
-	return ""
+	g.BranchStatus = getBranchStatus()
 }
 
 func (g *git) getUpstreamIcon() string {
@@ -266,20 +265,45 @@ func (g *git) getUpstreamIcon() string {
 }
 
 func (g *git) setGitStatus() {
-	output := g.getGitCommandOutput("status", "-unormal", "--short", "--branch")
-	splittedOutput := strings.Split(output, "\n")
-	g.Working.parse(splittedOutput, true)
-	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"]
+	addToStatus := func(status string) {
+		if len(status) <= 4 {
+			return
+		}
+		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 "
+	)
+	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 {
@@ -309,53 +333,74 @@ func (g *git) getGitCommandOutput(args ...string) string {
 	return val
 }
 
-func (g *git) getGitHEADContext(ref string) string {
+func (g *git) setGitHEADContext() {
 	branchIcon := g.props.getString(BranchIcon, "\uE0A0")
-	if ref == "" {
-		ref = g.getPrettyHEADName()
+	if g.Ref == DETACHED {
+		g.setPrettyHEADName()
 	} else {
-		ref = g.truncateBranch(ref)
-		ref = fmt.Sprintf("%s%s", branchIcon, ref)
+		head := g.formatHEAD(g.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") {
-		head := g.getGitFileContents(g.gitWorkingFolder, "rebase-merge/head-name")
-		origin := strings.Replace(head, "refs/heads/", "", 1)
-		origin = g.truncateBranch(origin)
+		origin := getPrettyNameOrigin("rebase-merge/head-name")
 		onto := g.getGitRefFileSymbolicName("rebase-merge/onto")
-		onto = g.truncateBranch(onto)
+		onto = g.formatHEAD(onto)
 		step := g.getGitFileContents(g.gitWorkingFolder, "rebase-merge/msgnum")
 		total := g.getGitFileContents(g.gitWorkingFolder, "rebase-merge/end")
 		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") {
-		head := g.getGitFileContents(g.gitWorkingFolder, "rebase-apply/head-name")
-		origin := strings.Replace(head, "refs/heads/", "", 1)
-		origin = g.truncateBranch(origin)
+		origin := getPrettyNameOrigin("rebase-apply/head-name")
 		step := g.getGitFileContents(g.gitWorkingFolder, "rebase-apply/next")
 		total := g.getGitFileContents(g.gitWorkingFolder, "rebase-apply/last")
 		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
-	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 ")
 		mergeContext := g.getGitFileContents(g.gitWorkingFolder, "MERGE_MSG")
-		matches := findNamedRegexMatch(`Merge (?P<type>(remote-tracking )?branch|commit|tag) '(?P<head>.*)' into`, mergeContext)
-
-		if matches != nil && matches["head"] != "" {
-			var headIcon string
+		matches := findNamedRegexMatch(`Merge (remote-tracking )?(?P<type>branch|commit|tag) '(?P<theirs>.*)'`, mergeContext)
+		// head := g.getGitRefFileSymbolicName("ORIG_HEAD")
+		if matches != nil && matches["theirs"] != "" {
+			var headIcon, theirs string
 			switch matches["type"] {
 			case "tag":
 				headIcon = g.props.getString(TagIcon, "\uF412")
+				theirs = matches["theirs"]
 			case "commit":
-				headIcon = g.props.getString(CommitIcon, "\uF417")
+				headIcon = commitIcon
+				theirs = g.formatSHA(matches["theirs"])
 			default:
 				headIcon = branchIcon
+				theirs = g.formatHEAD(matches["theirs"])
 			}
-			head := g.truncateBranch(matches["head"])
-			return fmt.Sprintf("%s%s%s into %s", icon, headIcon, head, ref)
+			g.HEAD = fmt.Sprintf("%s%s%s into %s", icon, headIcon, theirs, formatDetached())
+			return
 		}
 	}
 	// sequencer status
@@ -365,13 +410,17 @@ func (g *git) getGitHEADContext(ref string) string {
 	// the todo file.
 	if g.hasGitFile("CHERRY_PICK_HEAD") {
 		sha := g.getGitFileContents(g.gitWorkingFolder, "CHERRY_PICK_HEAD")
-		icon := g.props.getString(CherryPickIcon, "\uE29B ")
-		return fmt.Sprintf("%s%s onto %s", icon, sha[0:6], ref)
-	} else if g.hasGitFile("REVERT_HEAD") {
+		cherry := g.props.getString(CherryPickIcon, "\uE29B ")
+		g.HEAD = fmt.Sprintf("%s%s%s onto %s", cherry, commitIcon, g.formatSHA(sha), formatDetached())
+		return
+	}
+	if g.hasGitFile("REVERT_HEAD") {
 		sha := g.getGitFileContents(g.gitWorkingFolder, "REVERT_HEAD")
-		icon := g.props.getString(RevertIcon, "\uF0E2 ")
-		return fmt.Sprintf("%s%s onto %s", icon, sha[0:6], ref)
-	} else if g.hasGitFile("sequencer/todo") {
+		revert := g.props.getString(RevertIcon, "\uF0E2 ")
+		g.HEAD = fmt.Sprintf("%s%s%s onto %s", revert, commitIcon, g.formatSHA(sha), formatDetached())
+		return
+	}
+	if g.hasGitFile("sequencer/todo") {
 		todo := g.getGitFileContents(g.gitWorkingFolder, "sequencer/todo")
 		matches := findNamedRegexMatch(`^(?P<action>p|pick|revert)\s+(?P<sha>\S+)`, todo)
 		if matches != nil && matches["sha"] != "" {
@@ -379,24 +428,33 @@ func (g *git) getGitHEADContext(ref string) string {
 			sha := matches["sha"]
 			switch action {
 			case "p", "pick":
-				icon := g.props.getString(CherryPickIcon, "\uE29B ")
-				return fmt.Sprintf("%s%s onto %s", icon, sha[0:6], ref)
+				cherry := g.props.getString(CherryPickIcon, "\uE29B ")
+				g.HEAD = fmt.Sprintf("%s%s%s onto %s", cherry, commitIcon, g.formatSHA(sha), formatDetached())
+				return
 			case "revert":
-				icon := g.props.getString(RevertIcon, "\uF0E2 ")
-				return fmt.Sprintf("%s%s onto %s", icon, sha[0:6], ref)
+				revert := g.props.getString(RevertIcon, "\uF0E2 ")
+				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)
-	if maxLength == 0 || len(branch) < maxLength {
-		return branch
+	if maxLength == 0 || len(head) < maxLength {
+		return head
 	}
 	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 {
@@ -412,28 +470,32 @@ func (g *git) getGitRefFileSymbolicName(refFile string) string {
 	return g.getGitCommandOutput("name-rev", "--name-only", "--exclude=tags/*", ref)
 }
 
-func (g *git) getPrettyHEADName() string {
-	var ref string
-	HEAD := g.getGitFileContents(g.gitWorkingFolder, "HEAD")
-	branchPrefix := "ref: refs/heads/"
-	if strings.HasPrefix(HEAD, branchPrefix) {
-		ref = strings.TrimPrefix(HEAD, branchPrefix)
-	}
-	if ref != "" {
-		ref = g.truncateBranch(ref)
-		return fmt.Sprintf("%s%s", g.props.getString(BranchIcon, "\uE0A0"), ref)
+func (g *git) setPrettyHEADName() {
+	// we didn't fetch status, fallback to parsing the HEAD file
+	if len(g.Hash) == 0 {
+		HEADRef := g.getGitFileContents(g.gitWorkingFolder, "HEAD")
+		if strings.HasPrefix(HEADRef, 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]
+		}
 	}
 	// check for tag
-	ref = g.getGitCommandOutput("describe", "--tags", "--exact-match")
-	if ref != "" {
-		return fmt.Sprintf("%s%s", g.props.getString(TagIcon, "\uF412"), ref)
+	tagName := g.getGitCommandOutput("describe", "--tags", "--exact-match")
+	if len(tagName) > 0 {
+		g.HEAD = fmt.Sprintf("%s%s", g.props.getString(TagIcon, "\uF412"), tagName)
+		return
 	}
 	// fallback to commit
-	ref = g.getGitCommandOutput("rev-parse", "--short", "HEAD")
-	if ref == "" {
-		return g.props.getString(NoCommitsIcon, "\uF594 ")
+	if len(g.Hash) == 0 {
+		g.HEAD = 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 {
@@ -453,11 +515,6 @@ func (g *git) getWorktreeContext() int {
 	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 {
 	cfg, err := ini.Load(g.gitRootFolder + "/config")
 	if err != nil {
diff --git a/src/writer_ansi.go b/src/writer_ansi.go
index 460aa036..2e9da58a 100644
--- a/src/writer_ansi.go
+++ b/src/writer_ansi.go
@@ -179,9 +179,9 @@ func (a *AnsiWriter) asAnsiColors(background, foreground string) (AnsiColor, Ans
 
 func (a *AnsiWriter) isKeyword(color string) (string, bool) {
 	switch {
-	case color == Background:
+	case color == Background && a.Colors != nil:
 		return a.Colors.Background, true
-	case color == Foreground:
+	case color == Foreground && a.Colors != nil:
 		return a.Colors.Foreground, true
 	case color == ParentBackground && a.ParentColors != nil:
 		return a.ParentColors.Background, true


Test patch:
diff --git a/src/segment_deprecated_test.go b/src/segment_deprecated_test.go
index 5f230048..e7143591 100644
--- a/src/segment_deprecated_test.go
+++ b/src/segment_deprecated_test.go
@@ -13,8 +13,7 @@ import (
 func TestGetStatusDetailStringDefault(t *testing.T) {
 	expected := "icon +1"
 	status := &GitStatus{
-		Changed: true,
-		Added:   1,
+		Added: 1,
 	}
 	g := &git{}
 	assert.Equal(t, expected, g.getStatusDetailString(status, WorkingColor, LocalWorkingIcon, "icon"))
@@ -23,8 +22,7 @@ func TestGetStatusDetailStringDefault(t *testing.T) {
 func TestGetStatusDetailStringDefaultColorOverride(t *testing.T) {
 	expected := "<#123456>icon +1</>"
 	status := &GitStatus{
-		Changed: true,
-		Added:   1,
+		Added: 1,
 	}
 	var props properties = map[Property]interface{}{
 		WorkingColor: "#123456",
@@ -38,8 +36,7 @@ func TestGetStatusDetailStringDefaultColorOverride(t *testing.T) {
 func TestGetStatusDetailStringDefaultColorOverrideAndIconColorOverride(t *testing.T) {
 	expected := "<#789123>work</> <#123456>+1</>"
 	status := &GitStatus{
-		Changed: true,
-		Added:   1,
+		Added: 1,
 	}
 	var props properties = map[Property]interface{}{
 		WorkingColor:     "#123456",
@@ -54,8 +51,7 @@ func TestGetStatusDetailStringDefaultColorOverrideAndIconColorOverride(t *testin
 func TestGetStatusDetailStringDefaultColorOverrideNoIconColorOverride(t *testing.T) {
 	expected := "<#123456>work +1</>"
 	status := &GitStatus{
-		Changed: true,
-		Added:   1,
+		Added: 1,
 	}
 	var props properties = map[Property]interface{}{
 		WorkingColor:     "#123456",
@@ -70,8 +66,7 @@ func TestGetStatusDetailStringDefaultColorOverrideNoIconColorOverride(t *testing
 func TestGetStatusDetailStringNoStatus(t *testing.T) {
 	expected := "icon"
 	status := &GitStatus{
-		Changed: true,
-		Added:   1,
+		Added: 1,
 	}
 	var props properties = map[Property]interface{}{
 		DisplayStatusDetail: false,
@@ -85,8 +80,7 @@ func TestGetStatusDetailStringNoStatus(t *testing.T) {
 func TestGetStatusDetailStringNoStatusColorOverride(t *testing.T) {
 	expected := "<#123456>icon</>"
 	status := &GitStatus{
-		Changed: true,
-		Added:   1,
+		Added: 1,
 	}
 	var props properties = map[Property]interface{}{
 		DisplayStatusDetail: false,
@@ -106,8 +100,9 @@ func TestGetStatusColorLocalChangesStaging(t *testing.T) {
 	g := &git{
 		props: props,
 		Staging: &GitStatus{
-			Changed: true,
+			Modified: 1,
 		},
+		Working: &GitStatus{},
 	}
 	assert.Equal(t, expected, g.getStatusColor("#fg1111"))
 }
@@ -121,7 +116,7 @@ func TestGetStatusColorLocalChangesWorking(t *testing.T) {
 		props:   props,
 		Staging: &GitStatus{},
 		Working: &GitStatus{
-			Changed: true,
+			Modified: 1,
 		},
 	}
 	assert.Equal(t, expected, g.getStatusColor("#fg1111"))
@@ -196,8 +191,9 @@ func TestSetStatusColorForeground(t *testing.T) {
 	g := &git{
 		props: props,
 		Staging: &GitStatus{
-			Changed: true,
+			Added: 1,
 		},
+		Working: &GitStatus{},
 	}
 	g.SetStatusColor()
 	assert.Equal(t, expected, g.props[ForegroundOverride])
@@ -210,9 +206,10 @@ func TestSetStatusColorBackground(t *testing.T) {
 		ColorBackground:   true,
 	}
 	g := &git{
-		props: props,
-		Staging: &GitStatus{
-			Changed: true,
+		props:   props,
+		Staging: &GitStatus{},
+		Working: &GitStatus{
+			Modified: 1,
 		},
 	}
 	g.SetStatusColor()
@@ -221,16 +218,32 @@ func TestSetStatusColorBackground(t *testing.T) {
 
 func TestStatusColorsWithoutDisplayStatus(t *testing.T) {
 	expected := changesColor
-	context := &detachedContext{
-		status: "## main...origin/main [ahead 33]\n M myfile",
-	}
-	g := setupHEADContextEnv(context)
-	var props properties = map[Property]interface{}{
-		DisplayStatus:       false,
-		StatusColorsEnabled: true,
-		LocalChangesColor:   expected,
+	status := "## main...origin/main [ahead 33]\n M myfile"
+	env := new(MockedEnvironment)
+	env.On("isWsl", nil).Return(false)
+	env.On("getRuntimeGOOS", nil).Return("unix")
+	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,
+			StatusColorsEnabled: true,
+			LocalChangesColor:   expected,
+		},
 	}
-	g.props = props
+	g.Working = &GitStatus{}
+	g.Staging = &GitStatus{}
 	g.string()
 	assert.Equal(t, expected, g.props[BackgroundOverride])
 }
diff --git a/src/segment_git_test.go b/src/segment_git_test.go
index 1eb0411e..feebbe1c 100644
--- a/src/segment_git_test.go
+++ b/src/segment_git_test.go
@@ -2,6 +2,7 @@ package main
 
 import (
 	"fmt"
+	"strings"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -9,6 +10,7 @@ import (
 
 const (
 	changesColor = "#BD8BDE"
+	branchName   = "main"
 )
 
 func TestEnabledGitNotFound(t *testing.T) {
@@ -76,299 +78,303 @@ func TestGetGitOutputForCommand(t *testing.T) {
 	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) {
 	args = append([]string{"-C", "", "--no-optional-locks", "-c", "core.quotepath=false", "-c", "color.status=false"}, args...)
 	m.On("runCommand", "git", args).Return(returnValue, nil)
 }
 
-func TestGetGitDetachedCommitHash(t *testing.T) {
-	want := "\uf417lalasha1"
-	context := &detachedContext{
-		currentCommit: "lalasha1",
-	}
-	g := setupHEADContextEnv(context)
-	got := g.getGitHEADContext("")
-	assert.Equal(t, want, got)
-}
-
-func TestGetGitHEADContextTagName(t *testing.T) {
-	want := "\uf412lalasha1"
-	context := &detachedContext{
-		currentCommit: "whatever",
-		tagName:       "lalasha1",
-	}
-	g := setupHEADContextEnv(context)
-	got := g.getGitHEADContext("")
-	assert.Equal(t, want, got)
-}
-
-func TestGetGitHEADContextRebaseMerge(t *testing.T) {
-	want := "\ue728 \ue0a0cool-feature-bro onto \ue0a0main (2/3) at \uf417whatever"
-	context := &detachedContext{
-		currentCommit: "whatever",
-		rebase:        "true",
-		rebaseMerge:   true,
-		origin:        "cool-feature-bro",
-		onto:          "main",
-		step:          "2",
-		total:         "3",
-	}
-	g := setupHEADContextEnv(context)
-	got := g.getGitHEADContext("")
-	assert.Equal(t, want, got)
-}
-
-func TestGetGitHEADContextRebaseApply(t *testing.T) {
-	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)
-	got := g.getGitHEADContext("")
-	assert.Equal(t, want, got)
-}
-
-func TestGetGitHEADContextRebaseUnknown(t *testing.T) {
-	want := "\uf417whatever"
-	context := &detachedContext{
-		currentCommit: "whatever",
-		rebase:        "true",
-	}
-	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",
+func TestSetGitHEADContextClean(t *testing.T) {
+	cases := []struct {
+		Case        string
+		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",
+		},
 	}
-	g := setupHEADContextEnv(context)
-	got := g.getGitHEADContext("")
-	assert.Equal(t, want, got)
-}
+	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)
 
-func TestGetGitHEADContextMerge(t *testing.T) {
-	want := "\ue727 \ue0a0feat into \ue0a0main"
-	context := &detachedContext{
-		merge:         true,
-		mergeHEAD:     "feat",
-		mergeMsgStart: "Merge branch",
+		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("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",
+func TestSetPrettyHEADName(t *testing.T) {
+	cases := []struct {
+		Case     string
+		Expected string
+		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"},
 	}
-	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",
+	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("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",
+func TestSetGitStatus(t *testing.T) {
+	cases := []struct {
+		Case             string
+		Output           string
+		ExpectedWorking  *GitStatus
+		ExpectedStaging  *GitStatus
+		ExpectedHash     string
+		ExpectedRef      string
+		ExpectedUpstream string
+		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)
-	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",
+	for _, tc := range cases {
+		env := new(MockedEnvironment)
+		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 {
+			tc.ExpectedWorking = &GitStatus{}
+		}
+		if tc.ExpectedStaging == nil {
+			tc.ExpectedStaging = &GitStatus{}
+		}
+		g.Working = &GitStatus{}
+		g.Staging = &GitStatus{}
+		g.setGitStatus()
+		assert.Equal(t, tc.ExpectedStaging, g.Staging, tc.Case)
+		assert.Equal(t, tc.ExpectedWorking, g.Working, tc.Case)
+		assert.Equal(t, tc.ExpectedHash, g.Hash, tc.Case)
+		assert.Equal(t, tc.ExpectedRef, g.Ref, tc.Case)
+		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 TestGetStashContextZeroEntries(t *testing.T) {
@@ -392,62 +398,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) {
 	expected := "x1"
 	status := &GitStatus{
@@ -471,71 +421,6 @@ func TestGitStatusEmpty(t *testing.T) {
 	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) {
 	cases := []struct {
 		Case     string
@@ -580,7 +465,7 @@ func TestGetBranchStatus(t *testing.T) {
 		Behind   int
 		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: "Behind", Expected: " down8", Behind: 8},
 		{Case: "Behind and ahead", Expected: " up7 down8", Behind: 8, Ahead: 7},
@@ -601,7 +486,8 @@ func TestGetBranchStatus(t *testing.T) {
 			Behind:   tc.Behind,
 			Upstream: tc.Upstream,
 		}
-		assert.Equal(t, tc.Expected, g.getBranchStatus(), tc.Case)
+		g.setBranchStatus()
+		assert.Equal(t, tc.Expected, g.BranchStatus, tc.Case)
 	}
 }
 
@@ -657,7 +543,7 @@ func TestTruncateBranch(t *testing.T) {
 		g := &git{
 			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 +570,7 @@ func TestTruncateBranchWithSymbol(t *testing.T) {
 		g := &git{
 			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 +616,10 @@ func TestGitTemplateString(t *testing.T) {
 	}{
 		{
 			Case:     "Only HEAD name",
-			Expected: "main",
+			Expected: branchName,
 			Template: "{{ .HEAD }}",
 			Git: &git{
-				HEAD:   "main",
+				HEAD:   branchName,
 				Behind: 2,
 			},
 		},
@@ -742,23 +628,20 @@ func TestGitTemplateString(t *testing.T) {
 			Expected: "main \uF044 +2 ~3",
 			Template: "{{ .HEAD }}{{ if .Working.Changed }} \uF044 {{ .Working.String }}{{ end }}",
 			Git: &git{
-				HEAD: "main",
+				HEAD: branchName,
 				Working: &GitStatus{
 					Added:    2,
 					Modified: 3,
-					Changed:  true,
 				},
 			},
 		},
 		{
 			Case:     "No working area changes",
-			Expected: "main",
+			Expected: branchName,
 			Template: "{{ .HEAD }}{{ if .Working.Changed }} \uF044 {{ .Working.String }}{{ end }}",
 			Git: &git{
-				HEAD: "main",
-				Working: &GitStatus{
-					Changed: false,
-				},
+				HEAD:    branchName,
+				Working: &GitStatus{},
 			},
 		},
 		{
@@ -766,16 +649,14 @@ func TestGitTemplateString(t *testing.T) {
 			Expected: "main \uF046 +5 ~1 \uF044 +2 ~3",
 			Template: "{{ .HEAD }}{{ if .Staging.Changed }} \uF046 {{ .Staging.String }}{{ end }}{{ if .Working.Changed }} \uF044 {{ .Working.String }}{{ end }}",
 			Git: &git{
-				HEAD: "main",
+				HEAD: branchName,
 				Working: &GitStatus{
 					Added:    2,
 					Modified: 3,
-					Changed:  true,
 				},
 				Staging: &GitStatus{
 					Added:    5,
 					Modified: 1,
-					Changed:  true,
 				},
 			},
 		},
@@ -784,16 +665,14 @@ func TestGitTemplateString(t *testing.T) {
 			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
 			Git: &git{
-				HEAD: "main",
+				HEAD: branchName,
 				Working: &GitStatus{
 					Added:    2,
 					Modified: 3,
-					Changed:  true,
 				},
 				Staging: &GitStatus{
 					Added:    5,
 					Modified: 1,
-					Changed:  true,
 				},
 			},
 		},
@@ -802,26 +681,24 @@ func TestGitTemplateString(t *testing.T) {
 			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
 			Git: &git{
-				HEAD: "main",
+				HEAD: branchName,
 				Working: &GitStatus{
 					Added:    2,
 					Modified: 3,
-					Changed:  true,
 				},
 				Staging: &GitStatus{
 					Added:    5,
 					Modified: 1,
-					Changed:  true,
 				},
 				StashCount: 3,
 			},
 		},
 		{
 			Case:     "No local changes",
-			Expected: "main",
+			Expected: branchName,
 			Template: "{{ .HEAD }}{{ if .Staging.Changed }} \uF046{{ .Staging.String }}{{ end }}{{ if .Working.Changed }} \uF044{{ .Working.String }}{{ end }}",
 			Git: &git{
-				HEAD:    "main",
+				HEAD:    branchName,
 				Staging: &GitStatus{},
 				Working: &GitStatus{},
 			},
@@ -831,7 +708,7 @@ func TestGitTemplateString(t *testing.T) {
 			Expected: "from GitHub on main",
 			Template: "from {{ .UpstreamIcon }} on {{ .HEAD }}",
 			Git: &git{
-				HEAD:         "main",
+				HEAD:         branchName,
 				Staging:      &GitStatus{},
 				Working:      &GitStatus{},
 				UpstreamIcon: "GitHub",