Repository: JanDeDobbeleer/oh-my-posh
Pull request #2737 (2022-09-01 18:19:07)

feat(git): add link to GitKraken
### 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

<!--TemplateBody-->

<!---

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/color/ansi.go b/src/color/ansi.go
index b307cfce..c9b2dbc0 100644
--- a/src/color/ansi.go
+++ b/src/color/ansi.go
@@ -140,6 +140,22 @@ func (a *Ansi) InitPlain() {
 }
 
 func (a *Ansi) GenerateHyperlink(text string) string {
+	parts := strings.SplitAfter(text, ")")
+	var buffer strings.Builder
+	var part string
+	for i := range parts {
+		if strings.Contains(parts[i], "[") && !strings.Contains(parts[i], "]") {
+			part += parts[i]
+			continue
+		}
+		part += parts[i]
+		buffer.WriteString(a.replaceHyperlink(part))
+		part = ""
+	}
+	return buffer.String()
+}
+
+func (a *Ansi) replaceHyperlink(text string) string {
 	// hyperlink matching
 	results := regex.FindNamedRegexMatch("(?P<ALL>(?:\\[(?P<TEXT>.+)\\])(?:\\((?P<URL>.*)\\)))", text)
 	if len(results) != 3 {
diff --git a/src/segments/git.go b/src/segments/git.go
index a7355ef6..e88fa409 100644
--- a/src/segments/git.go
+++ b/src/segments/git.go
@@ -2,6 +2,7 @@ package segments
 
 import (
 	"fmt"
+	url2 "net/url"
 	"oh-my-posh/environment"
 	"oh-my-posh/properties"
 	"oh-my-posh/regex"
@@ -34,27 +35,6 @@ func (s *GitStatus) add(code string) {
 	}
 }
 
-type Git struct {
-	scm
-
-	Working       *GitStatus
-	Staging       *GitStatus
-	Ahead         int
-	Behind        int
-	HEAD          string
-	Ref           string
-	Hash          string
-	BranchStatus  string
-	Upstream      string
-	UpstreamIcon  string
-	UpstreamURL   string
-	UpstreamGone  bool
-	StashCount    int
-	WorktreeCount int
-	IsWorkTree    bool
-	RepoName      string
-}
-
 const (
 	// FetchStatus fetches the status of the repository
 	FetchStatus properties.Property = "fetch_status"
@@ -109,6 +89,28 @@ const (
 	GITCOMMAND   = "git"
 )
 
+type Git struct {
+	scm
+
+	Working       *GitStatus
+	Staging       *GitStatus
+	Ahead         int
+	Behind        int
+	HEAD          string
+	Ref           string
+	Hash          string
+	ShortHash     string
+	BranchStatus  string
+	Upstream      string
+	UpstreamIcon  string
+	UpstreamURL   string
+	UpstreamGone  bool
+	StashCount    int
+	WorktreeCount int
+	IsWorkTree    bool
+	RepoName      string
+}
+
 func (g *Git) Template() string {
 	return " {{ .HEAD }} {{ .BranchStatus }}{{ if .Working.Changed }} \uF044 {{ .Working.String }}{{ end }}{{ if and (.Staging.Changed) (.Working.Changed) }} |{{ end }}{{ if .Staging.Changed }} \uF046 {{ .Staging.String }}{{ end }}{{ if gt .StashCount 0}} \uF692 {{ .StashCount }}{{ end }}{{ if gt .WorktreeCount 0}} \uf1bb {{ .WorktreeCount }}{{ end }} " //nolint: lll
 }
@@ -140,6 +142,12 @@ func (g *Git) Enabled() bool {
 	return true
 }
 
+func (g *Git) Kraken() string {
+	root := g.getGitCommandOutput("rev-list", "--max-parents=0", "HEAD")
+	remote := g.getGitCommandOutput("remote", "get-url", "origin")
+	return fmt.Sprintf("gitkraken://repolink/%s/commit/%s?url=%s", root, g.Hash, url2.QueryEscape(remote))
+}
+
 func (g *Git) shouldDisplay() bool {
 	// when in a WSL shared folder, we must use git.exe and convert paths accordingly
 	// for worktrees, stashes, and path to work
@@ -307,7 +315,8 @@ func (g *Git) setGitStatus() {
 	output := g.getGitCommandOutput(args...)
 	for _, line := range strings.Split(output, "\n") {
 		if strings.HasPrefix(line, HASH) && len(line) >= len(HASH)+7 {
-			g.Hash = line[len(HASH) : len(HASH)+7]
+			g.ShortHash = line[len(HASH) : len(HASH)+7]
+			g.Hash = line[len(HASH):]
 			continue
 		}
 		if strings.HasPrefix(line, REF) && len(line) > len(REF) {
@@ -480,7 +489,7 @@ func (g *Git) getGitRefFileSymbolicName(refFile string) string {
 
 func (g *Git) setPrettyHEADName() {
 	// we didn't fetch status, fallback to parsing the HEAD file
-	if len(g.Hash) == 0 {
+	if len(g.ShortHash) == 0 {
 		HEADRef := g.FileContents(g.workingDir, "HEAD")
 		if strings.HasPrefix(HEADRef, BRANCHPREFIX) {
 			branchName := strings.TrimPrefix(HEADRef, BRANCHPREFIX)
@@ -489,7 +498,8 @@ func (g *Git) setPrettyHEADName() {
 		}
 		// no branch, points to commit
 		if len(HEADRef) >= 7 {
-			g.Hash = HEADRef[0:7]
+			g.ShortHash = HEADRef[0:7]
+			g.Hash = HEADRef[0:]
 		}
 	}
 	// check for tag
@@ -499,11 +509,11 @@ func (g *Git) setPrettyHEADName() {
 		return
 	}
 	// fallback to commit
-	if len(g.Hash) == 0 {
+	if len(g.ShortHash) == 0 {
 		g.HEAD = g.props.GetString(NoCommitsIcon, "\uF594 ")
 		return
 	}
-	g.HEAD = fmt.Sprintf("%s%s", g.props.GetString(CommitIcon, "\uF417"), g.Hash)
+	g.HEAD = fmt.Sprintf("%s%s", g.props.GetString(CommitIcon, "\uF417"), g.ShortHash)
 }
 
 func (g *Git) getStashContext() int {
diff --git a/website/docs/segments/git.mdx b/website/docs/segments/git.mdx
index 88ecfda8..f8746293 100644
--- a/website/docs/segments/git.mdx
+++ b/website/docs/segments/git.mdx
@@ -131,6 +131,7 @@ instead of the repo path.
 - `.WorktreeCount`: `int` - the worktree count
 - `.IsWorkTree`: `boolean` - if in a worktree repo or not
 - `.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 }}`
 
 ### GitStatus
 
@@ -146,3 +147,4 @@ instead of the repo path.
 [hyperlinks]: /docs/configuration/templates#helper-functions
 [untracked]: https://git-scm.com/docs/git-status#Documentation/git-status.txt---untracked-filesltmodegt
 [submodules]: https://git-scm.com/docs/git-status#Documentation/git-status.txt---ignore-submodulesltwhengt
+[kraken-ref]: https://www.gitkraken.com/invite/nQmDPR9D


Test patch:
diff --git a/src/color/ansi_test.go b/src/color/ansi_test.go
index 2dd0a230..2d76ebd6 100644
--- a/src/color/ansi_test.go
+++ b/src/color/ansi_test.go
@@ -31,9 +31,19 @@ func TestGenerateHyperlinkWithUrl(t *testing.T) {
 		ShellName string
 		Expected  string
 	}{
+		{
+			Text:      "[google](http://www.google.be) [maps (2/2)](http://maps.google.be)",
+			ShellName: shell.FISH,
+			Expected:  "\x1b]8;;http://www.google.be\x1b\\google\x1b]8;;\x1b\\ \x1b]8;;http://maps.google.be\x1b\\maps (2/2)\x1b]8;;\x1b\\",
+		},
 		{Text: "[google](http://www.google.be)", ShellName: shell.ZSH, Expected: "%{\x1b]8;;http://www.google.be\x1b\\%}google%{\x1b]8;;\x1b\\%}"},
 		{Text: "[google](http://www.google.be)", ShellName: shell.PWSH, Expected: "\x1b]8;;http://www.google.be\x1b\\google\x1b]8;;\x1b\\"},
 		{Text: "[google](http://www.google.be)", ShellName: shell.BASH, Expected: "\\[\x1b]8;;http://www.google.be\x1b\\\\\\]google\\[\x1b]8;;\x1b\\\\\\]"},
+		{
+			Text:      "[google](http://www.google.be) [maps](http://maps.google.be)",
+			ShellName: shell.FISH,
+			Expected:  "\x1b]8;;http://www.google.be\x1b\\google\x1b]8;;\x1b\\ \x1b]8;;http://maps.google.be\x1b\\maps\x1b]8;;\x1b\\",
+		},
 	}
 	for _, tc := range cases {
 		a := Ansi{}
diff --git a/src/segments/git_test.go b/src/segments/git_test.go
index adbf87ee..fb0107c8 100644
--- a/src/segments/git_test.go
+++ b/src/segments/git_test.go
@@ -308,8 +308,8 @@ func TestSetGitHEADContextClean(t *testing.T) {
 					RevertIcon:     "revert ",
 				},
 			},
-			Hash: "1234567",
-			Ref:  tc.Ref,
+			ShortHash: "1234567",
+			Ref:       tc.Ref,
 		}
 		g.setGitHEADContext()
 		assert.Equal(t, tc.Expected, g.HEAD, tc.Case)
@@ -318,17 +318,17 @@ func TestSetGitHEADContextClean(t *testing.T) {
 
 func TestSetPrettyHEADName(t *testing.T) {
 	cases := []struct {
-		Case     string
-		Expected string
-		Hash     string
-		Tag      string
-		HEAD     string
+		Case      string
+		Expected  string
+		ShortHash 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: "hash on tag", ShortHash: "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: "hash on commit", ShortHash: "1234567", Expected: "commit 1234567"},
 		{Case: "no hash on commit", Expected: "commit 1234567", HEAD: "12345678910"},
 	}
 	for _, tc := range cases {
@@ -346,7 +346,7 @@ func TestSetPrettyHEADName(t *testing.T) {
 					TagIcon:    "tag ",
 				},
 			},
-			Hash: tc.Hash,
+			ShortHash: tc.ShortHash,
 		}
 		g.setPrettyHEADName()
 		assert.Equal(t, tc.Expected, g.HEAD, tc.Case)
@@ -485,7 +485,7 @@ func TestSetGitStatus(t *testing.T) {
 		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.ExpectedHash, g.ShortHash, 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)