Repository: JanDeDobbeleer/oh-my-posh
Pull request #4227 (2023-09-04 15:00:08)

fix(git): correctly identify conflicted files
### Prerequisites

- [x] I have read and understood the [contributing guide][CONTRIBUTING.md].
- [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

<!--
copilot:summary
-->
### <samp>🤖 Generated by Copilot at a955a54</samp>

This pull request adds new features and documentation to the git segment of oh-my-posh. It enables the prompt to display the number and state of merge conflicts and other git operations, such as rebase and merge. It also fixes a typo and adds new properties to the `website/docs/segments/git.mdx` file.

### How

<!--
copilot:walkthrough
-->
### <samp>🤖 Generated by Copilot at a955a54</samp>

*  Add fields and test cases for rebase and merge conflicts in git segment ([link](https://github.com/JanDeDobbeleer/oh-my-posh/pull/4227/files?diff=unified&w=0#diff-7290b1c5198130d19c091496b4368edbcfd2b10d49813e6f92f01fbe26a56122R439-R440), [link](https://github.com/JanDeDobbeleer/oh-my-posh/pull/4227/files?diff=unified&w=0#diff-7290b1c5198130d19c091496b4368edbcfd2b10d49813e6f92f01fbe26a56122R540-R573), [link](https://github.com/JanDeDobbeleer/oh-my-posh/pull/4227/files?diff=unified&w=0#diff-7290b1c5198130d19c091496b4368edbcfd2b10d49813e6f92f01fbe26a56122R593-R594), [link](https://github.com/JanDeDobbeleer/oh-my-posh/pull/4227/files?diff=unified&w=0#diff-5ca08bba45ba772ce1cbd182429db15292b228ac7c5dc0cf370dcbe55611fd92L47-R47), [link](https://github.com/JanDeDobbeleer/oh-my-posh/pull/4227/files?diff=unified&w=0#diff-5ca08bba45ba772ce1cbd182429db15292b228ac7c5dc0cf370dcbe55611fd92R496-R506))
*  Add fields and logic for detecting detached head, rebase, merge, cherry pick, and revert operations in git segment ([link](https://github.com/JanDeDobbeleer/oh-my-posh/pull/4227/files?diff=unified&w=0#diff-5ca08bba45ba772ce1cbd182429db15292b228ac7c5dc0cf370dcbe55611fd92R138-R142), [link](https://github.com/JanDeDobbeleer/oh-my-posh/pull/4227/files?diff=unified&w=0#diff-5ca08bba45ba772ce1cbd182429db15292b228ac7c5dc0cf370dcbe55611fd92R574), [link](https://github.com/JanDeDobbeleer/oh-my-posh/pull/4227/files?diff=unified&w=0#diff-5ca08bba45ba772ce1cbd182429db15292b228ac7c5dc0cf370dcbe55611fd92R601), [link](https://github.com/JanDeDobbeleer/oh-my-posh/pull/4227/files?diff=unified&w=0#diff-5ca08bba45ba772ce1cbd182429db15292b228ac7c5dc0cf370dcbe55611fd92L593-R613), [link](https://github.com/JanDeDobbeleer/oh-my-posh/pull/4227/files?diff=unified&w=0#diff-5ca08bba45ba772ce1cbd182429db15292b228ac7c5dc0cf370dcbe55611fd92L601-R626), [link](https://github.com/JanDeDobbeleer/oh-my-posh/pull/4227/files?diff=unified&w=0#diff-5ca08bba45ba772ce1cbd182429db15292b228ac7c5dc0cf370dcbe55611fd92R655), [link](https://github.com/JanDeDobbeleer/oh-my-posh/pull/4227/files?diff=unified&w=0#diff-5ca08bba45ba772ce1cbd182429db15292b228ac7c5dc0cf370dcbe55611fd92L636-R663), [link](https://github.com/JanDeDobbeleer/oh-my-posh/pull/4227/files?diff=unified&w=0#diff-5ca08bba45ba772ce1cbd182429db15292b228ac7c5dc0cf370dcbe55611fd92R669))
*  Fix typo in `codecommit_icon` property documentation ([link](https://github.com/JanDeDobbeleer/oh-my-posh/pull/4227/files?diff=unified&w=0#diff-8396c4caea4be2af52706d5b8d5f1ff73062c8b0274d540d6a4cace1ffa277caL119-R119))

- refactor(git): add booleans to identify state
- fix(git): correctly identify conflicted files

<!---

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.

-->

[CONTRIBUTING.md]: https://github.com/JanDeDobbeleer/oh-my-posh/blob/main/CONTRIBUTING.md
[cc]: https://www.conventionalcommits.org/en/v1.0.0/#summary



Patch:
diff --git a/src/segments/git.go b/src/segments/git.go
index 7c987f65..7450dc34 100644
--- a/src/segments/git.go
+++ b/src/segments/git.go
@@ -44,7 +44,7 @@ func (s *GitStatus) add(code string) {
 		s.Added++
 	case "?":
 		s.Untracked++
-	case "U":
+	case "U", "AA":
 		s.Unmerged++
 	case "M", "R", "C", "m":
 		s.Modified++
@@ -135,6 +135,11 @@ type Git struct {
 	IsWorkTree     bool
 	IsBare         bool
 	User           *User
+	Detached       bool
+	Merge          bool
+	Rebase         bool
+	CherryPick     bool
+	Revert         bool
 
 	// needed for posh-git support
 	poshgit       bool
@@ -488,6 +493,17 @@ func (g *Git) setGitStatus() {
 		if len(status) <= 4 {
 			return
 		}
+
+		// map conflicts separately when in a merge or rebase
+		if g.Rebase || g.Merge {
+			conflict := "AA"
+			full := status[2:4]
+			if full == conflict {
+				g.Staging.add(conflict)
+				return
+			}
+		}
+
 		workingCode := status[3:4]
 		stagingCode := status[2:3]
 		g.Working.add(workingCode)
@@ -555,6 +571,7 @@ func (g *Git) getGitCommandOutput(args ...string) string {
 func (g *Git) setGitHEADContext() {
 	branchIcon := g.props.GetString(BranchIcon, "\uE0A0")
 	if g.Ref == DETACHED {
+		g.Detached = true
 		g.setPrettyHEADName()
 	} else {
 		head := g.formatHEAD(g.Ref)
@@ -581,6 +598,7 @@ func (g *Git) setGitHEADContext() {
 	}
 
 	if g.env.HasFolder(g.workingDir + "/rebase-merge") {
+		g.Rebase = true
 		origin := getPrettyNameOrigin("rebase-merge/head-name")
 		onto := g.getGitRefFileSymbolicName("rebase-merge/onto")
 		onto = g.formatHEAD(onto)
@@ -590,7 +608,9 @@ func (g *Git) setGitHEADContext() {
 		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.workingDir + "/rebase-apply") {
+		g.Rebase = true
 		origin := getPrettyNameOrigin("rebase-apply/head-name")
 		step := g.FileContents(g.workingDir, "rebase-apply/next")
 		total := g.FileContents(g.workingDir, "rebase-apply/last")
@@ -598,9 +618,12 @@ func (g *Git) setGitHEADContext() {
 		g.HEAD = fmt.Sprintf("%s%s (%s/%s) at %s", icon, origin, step, total, g.HEAD)
 		return
 	}
+
 	// merge
 	commitIcon := g.props.GetString(CommitIcon, "\uF417")
+
 	if g.hasGitFile("MERGE_MSG") {
+		g.Merge = true
 		icon := g.props.GetString(MergeIcon, "\uE727 ")
 		mergeContext := g.FileContents(g.workingDir, "MERGE_MSG")
 		matches := regex.FindNamedRegexMatch(`Merge (remote-tracking )?(?P<type>branch|commit|tag) '(?P<theirs>.*)'`, mergeContext)
@@ -622,23 +645,28 @@ func (g *Git) setGitHEADContext() {
 			return
 		}
 	}
+
 	// sequencer status
 	// see if a cherry-pick or revert is in progress, if the user has committed a
 	// conflict resolution with 'git commit' in the middle of a sequence of picks or
 	// reverts then CHERRY_PICK_HEAD/REVERT_HEAD will not exist so we have to read
 	// the todo file.
 	if g.hasGitFile("CHERRY_PICK_HEAD") {
+		g.CherryPick = true
 		sha := g.FileContents(g.workingDir, "CHERRY_PICK_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") {
+		g.Revert = true
 		sha := g.FileContents(g.workingDir, "REVERT_HEAD")
 		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.FileContents(g.workingDir, "sequencer/todo")
 		matches := regex.FindNamedRegexMatch(`^(?P<action>p|pick|revert)\s+(?P<sha>\S+)`, todo)
@@ -647,16 +675,19 @@ func (g *Git) setGitHEADContext() {
 			sha := matches["sha"]
 			switch action {
 			case "p", "pick":
+				g.CherryPick = true
 				cherry := g.props.GetString(CherryPickIcon, "\uE29B ")
 				g.HEAD = fmt.Sprintf("%s%s%s onto %s", cherry, commitIcon, g.formatSHA(sha), formatDetached())
 				return
 			case "revert":
+				g.Revert = true
 				revert := g.props.GetString(RevertIcon, "\uF0E2 ")
 				g.HEAD = fmt.Sprintf("%s%s%s onto %s", revert, commitIcon, g.formatSHA(sha), formatDetached())
 				return
 			}
 		}
 	}
+
 	g.HEAD = formatDetached()
 }
 
diff --git a/website/docs/segments/git.mdx b/website/docs/segments/git.mdx
index a6d70370..a20654db 100644
--- a/website/docs/segments/git.mdx
+++ b/website/docs/segments/git.mdx
@@ -116,7 +116,7 @@ You can set the following properties to `true` to enable fetching additional inf
 | `gitlab_icon`       | `string`            | icon/text to display when the upstream is Gitlab - defaults to `\uF296 `                                                                                                     |
 | `bitbucket_icon`    | `string`            | icon/text to display when the upstream is Bitbucket - defaults to `\uF171 `                                                                                                  |
 | `azure_devops_icon` | `string`            | icon/text to display when the upstream is Azure DevOps - defaults to `\uEBE8 `                                                                                               |
-| `codecommit_icon`   | `string`            | icon/text to display when the upstream is AWS CodeCommit - defaults to `\uF270 `                                                                                               |
+| `codecommit_icon`   | `string`            | icon/text to display when the upstream is AWS CodeCommit - defaults to `\uF270 `                                                                                             |
 | `git_icon`          | `string`            | icon/text to display when the upstream is not known/mapped - defaults to `\uE5FB `                                                                                           |
 | `upstream_icons`    | `map[string]string` | a key, value map representing the remote URL (or a part of that URL) and icon to use in case the upstream URL contains the key. These get precedence over the standard icons |
 
@@ -153,6 +153,11 @@ You can set the following properties to `true` to enable fetching additional inf
 | `.Dir`           | `string`  | the repository's root directory                                                                                                  |
 | `.Kraken`        | `string`  | a link to the current HEAD in [GitKraken][kraken-ref] for use in [hyperlinks][hyperlinks] in templates `{{ url .HEAD .Kraken }}` |
 | `.Commit`        | `Commit`  | HEAD commit information (see below)                                                                                              |
+| `.Detached`      | `boolean` | true when the head is detached                                                                                                   |
+| `.Merge`         | `boolean` | true when in a merge                                                                                                             |
+| `.Rebase`        | `boolean` | true when in a rebase                                                                                                            |
+| `.CherryPick`    | `boolean` | true when in a cherry pick                                                                                                       |
+| `.Revert`        | `boolean` | true when in a revert                                                                                                            |
 
 ### Status
 


Test patch:
diff --git a/src/segments/git_test.go b/src/segments/git_test.go
index e7720067..e6f179dd 100644
--- a/src/segments/git_test.go
+++ b/src/segments/git_test.go
@@ -436,6 +436,8 @@ func TestSetGitStatus(t *testing.T) {
 		ExpectedUpstreamGone bool
 		ExpectedAhead        int
 		ExpectedBehind       int
+		Rebase               bool
+		Merge                bool
 	}{
 		{
 			Case: "all different options on working and staging, no remote",
@@ -535,6 +537,40 @@ func TestSetGitStatus(t *testing.T) {
 			ExpectedRef:          "branch-is-gone",
 			ExpectedUpstreamGone: true,
 		},
+		{
+			Case: "rebase with 2 merge conflicts",
+			Output: `
+			# branch.oid 1234567891011121314
+			# branch.head rework-git-status
+			# branch.upstream origin/rework-git-status
+			# branch.ab +0 -0
+			1 AA N...
+			1 AA N...
+			`,
+			ExpectedUpstream: "origin/rework-git-status",
+			ExpectedHash:     "1234567",
+			ExpectedRef:      "rework-git-status",
+			Rebase:           true,
+			ExpectedStaging:  &GitStatus{ScmStatus: ScmStatus{Unmerged: 2}},
+		},
+		{
+			Case: "merge with 4 merge conflicts",
+			Output: `
+			# branch.oid 1234567891011121314
+			# branch.head rework-git-status
+			# branch.upstream origin/rework-git-status
+			# branch.ab +0 -0
+			1 AA N...
+			1 AA N...
+			1 AA N...
+			1 AA N...
+			`,
+			ExpectedUpstream: "origin/rework-git-status",
+			ExpectedHash:     "1234567",
+			ExpectedRef:      "rework-git-status",
+			Merge:            true,
+			ExpectedStaging:  &GitStatus{ScmStatus: ScmStatus{Unmerged: 4}},
+		},
 	}
 	for _, tc := range cases {
 		env := new(mock.MockedEnvironment)
@@ -554,6 +590,8 @@ func TestSetGitStatus(t *testing.T) {
 		if tc.ExpectedStaging == nil {
 			tc.ExpectedStaging = &GitStatus{}
 		}
+		g.Rebase = tc.Rebase
+		g.Merge = tc.Merge
 		tc.ExpectedStaging.Formats = map[string]string{}
 		tc.ExpectedWorking.Formats = map[string]string{}
 		g.setGitStatus()