Repository: JanDeDobbeleer/oh-my-posh
Pull request #1651 (2022-01-25 23:51:35)

fix(git): detect when upstream no longer exists
### 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

Currently the git segment does not detect when an upstream is removed (e.g., a PR is merged and the remote branch is deleted).

This change adds functionality to detect when upstream is removed; if the porcelain status reports `branch.upstream` but no `branch.ab`, it is considered _gone_.

The UI has also been changed to reflect this third state; it now matches posh-git's behavior, which only shows the Gone icon if an upstream branch is set but does not report ahead/behind counts.

### Example

This is what the `jandedobbeleer` theme should show in each case:

| State | Before | After | Notes |
|:------|:------|:------|:------|
| No remote | ` xyzzy ≢` | ` xyzzy` | Changed (no longer shows icon) |
| Remote, no `branch.upstream` | `xyzzy ≢` | `xyzzy` | Changed (no longer shows icon) |
| Remote, upstream exists | ` xyzzy ≡` |  ` xyzzy ≡` | Unchanged |
| Remote, upstream gone | `xyzzy ≢` | `xyzzy ≢` | Unchanged |

[cc]: https://www.conventionalcommits.org/en/v1.0.0/#summary


JanDeDobbeleer: Awesome. I'll add this ASAP but you'll have to rework as #1629 is going to layout everything quite differently.
JanDeDobbeleer: @cdonnellytx you can rebase this (but probably rework) on main, then we can get this baby merged!
JanDeDobbeleer: @cdonnellytx I believe we can add this boolean property to the docs as well? Can be useful for coloring I suppose.

Patch:
diff --git a/src/segments/git.go b/src/segments/git.go
index 28ce0591..e0b71e76 100644
--- a/src/segments/git.go
+++ b/src/segments/git.go
@@ -45,6 +45,7 @@ type Git struct {
 	Upstream      string
 	UpstreamIcon  string
 	UpstreamURL   string
+	UpstreamGone  bool
 	StashCount    int
 	WorktreeCount int
 	IsWorkTree    bool
@@ -205,12 +206,12 @@ func (g *Git) setBranchStatus() {
 		if g.Behind > 0 {
 			return fmt.Sprintf(" %s%d", g.props.GetString(BranchBehindIcon, "\u2193"), g.Behind)
 		}
+		if g.UpstreamGone {
+			return fmt.Sprintf(" %s", g.props.GetString(BranchGoneIcon, "\u2262"))
+		}
 		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 ""
 	}
 	g.BranchStatus = getBranchStatus()
@@ -268,7 +269,9 @@ func (g *Git) setGitStatus() {
 			continue
 		}
 		if strings.HasPrefix(line, UPSTREAM) && len(line) > len(UPSTREAM) {
+			// status reports upstream, but upstream may be gone (must check BRANCHSTATUS)
 			g.Upstream = line[len(UPSTREAM):]
+			g.UpstreamGone = true
 			continue
 		}
 		if strings.HasPrefix(line, BRANCHSTATUS) && len(line) > len(BRANCHSTATUS) {
@@ -279,6 +282,8 @@ func (g *Git) setGitStatus() {
 				behind, _ := strconv.Atoi(splitted[1])
 				g.Behind = -behind
 			}
+			// confirmed: upstream exists
+			g.UpstreamGone = false
 			continue
 		}
 		addToStatus(line)


Test patch:
diff --git a/src/segments/git_test.go b/src/segments/git_test.go
index 7375533a..ca5cab39 100644
--- a/src/segments/git_test.go
+++ b/src/segments/git_test.go
@@ -321,15 +321,16 @@ func TestSetPrettyHEADName(t *testing.T) {
 
 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                 string
+		Output               string
+		ExpectedWorking      *GitStatus
+		ExpectedStaging      *GitStatus
+		ExpectedHash         string
+		ExpectedRef          string
+		ExpectedUpstream     string
+		ExpectedUpstreamGone bool
+		ExpectedAhead        int
+		ExpectedBehind       int
 	}{
 		{
 			Case: "all different options on working and staging, no remote",
@@ -357,6 +358,7 @@ func TestSetGitStatus(t *testing.T) {
 			# branch.oid 1234567891011121314
 			# branch.head rework-git-status
 			# branch.upstream origin/rework-git-status
+			# branch.ab +0 -0
 			1 .R N...
 			1 .C N...
 			1 .M N...
@@ -379,6 +381,7 @@ func TestSetGitStatus(t *testing.T) {
 			# branch.oid 1234567891011121314
 			# branch.head rework-git-status
 			# branch.upstream origin/rework-git-status
+			# branch.ab +0 -0
 			`,
 			ExpectedUpstream: "origin/rework-git-status",
 			ExpectedHash:     "1234567",
@@ -414,6 +417,18 @@ func TestSetGitStatus(t *testing.T) {
 			ExpectedRef:      "main",
 			ExpectedWorking:  &GitStatus{ScmStatus: ScmStatus{Added: 3}},
 		},
+		{
+			Case: "remote branch was deleted",
+			Output: `
+			# branch.oid 1234567891011121314
+			# branch.head branch-is-gone
+			# branch.upstream origin/branch-is-gone
+			`,
+			ExpectedUpstream:     "origin/branch-is-gone",
+			ExpectedHash:         "1234567",
+			ExpectedRef:          "branch-is-gone",
+			ExpectedUpstreamGone: true,
+		},
 	}
 	for _, tc := range cases {
 		env := new(mock.MockedEnvironment)
@@ -437,6 +452,7 @@ func TestSetGitStatus(t *testing.T) {
 		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.ExpectedUpstreamGone, g.UpstreamGone, tc.Case)
 		assert.Equal(t, tc.ExpectedAhead, g.Ahead, tc.Case)
 		assert.Equal(t, tc.ExpectedBehind, g.Behind, tc.Case)
 	}
@@ -505,17 +521,19 @@ func TestGitUpstream(t *testing.T) {
 
 func TestGetBranchStatus(t *testing.T) {
 	cases := []struct {
-		Case     string
-		Expected string
-		Ahead    int
-		Behind   int
-		Upstream string
+		Case         string
+		Expected     string
+		Ahead        int
+		Behind       int
+		Upstream     string
+		UpstreamGone bool
 	}{
 		{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},
-		{Case: "Gone", Expected: " gone"},
+		{Case: "Gone", Expected: " gone", Upstream: branchName, UpstreamGone: true},
+		{Case: "No remote", Expected: "", Upstream: ""},
 		{Case: "Default (bug)", Expected: "", Behind: -8, Upstream: "wonky"},
 	}
 
@@ -530,9 +548,10 @@ func TestGetBranchStatus(t *testing.T) {
 			scm: scm{
 				props: props,
 			},
-			Ahead:    tc.Ahead,
-			Behind:   tc.Behind,
-			Upstream: tc.Upstream,
+			Ahead:        tc.Ahead,
+			Behind:       tc.Behind,
+			Upstream:     tc.Upstream,
+			UpstreamGone: tc.UpstreamGone,
 		}
 		g.setBranchStatus()
 		assert.Equal(t, tc.Expected, g.BranchStatus, tc.Case)