Repository: JanDeDobbeleer/oh-my-posh
Pull request #2719 (2022-08-28 13:35:49)

feat(git): expose directory
### 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/environment/shell.go b/src/environment/shell.go
index dbfea3f9..fb2815d1 100644
--- a/src/environment/shell.go
+++ b/src/environment/shell.go
@@ -726,10 +726,8 @@ func (env *ShellEnvironment) TemplateCache() *TemplateCache {
 		val := splitted[1:]
 		tmplCache.Env[key] = strings.Join(val, separator)
 	}
-	pwd := env.Pwd()
-	pwd = strings.Replace(pwd, env.Home(), "~", 1)
-	tmplCache.PWD = pwd
-	tmplCache.Folder = Base(env, pwd)
+	tmplCache.PWD = env.Pwd()
+	tmplCache.Folder = Base(env, tmplCache.PWD)
 	tmplCache.UserName = env.User()
 	if host, err := env.Host(); err == nil {
 		tmplCache.HostName = host
diff --git a/src/segments/git.go b/src/segments/git.go
index ed9bcf36..5cd6055d 100644
--- a/src/segments/git.go
+++ b/src/segments/git.go
@@ -117,7 +117,7 @@ func (g *Git) Enabled() bool {
 	if !g.shouldDisplay() {
 		return false
 	}
-	g.RepoName = environment.Base(g.env, g.realFolder)
+	g.RepoName = environment.Base(g.env, g.realDir)
 	displayStatus := g.props.GetBool(FetchStatus, false)
 	if displayStatus {
 		g.setGitStatus()
@@ -159,19 +159,21 @@ func (g *Git) shouldDisplay() bool {
 		return false
 	}
 
+	// convert the worktree file path to a windows one when in wsl 2 shared folder
+	g.Dir = strings.TrimSuffix(g.convertToWindowsPath(gitdir.Path), ".git")
+
 	if !gitdir.IsDir {
 		return g.hasWorktree(gitdir)
 	}
 
-	g.workingFolder = gitdir.Path
-	g.rootFolder = gitdir.Path
-	// convert the worktree file path to a windows one when in wsl 2 shared folder
-	g.realFolder = strings.TrimSuffix(g.convertToWindowsPath(gitdir.Path), ".git")
+	g.workingDir = gitdir.Path
+	g.rootDir = gitdir.Path
+	g.realDir = g.Dir
 	return true
 }
 
 func (g *Git) hasWorktree(gitdir *environment.FileInfo) bool {
-	g.rootFolder = gitdir.Path
+	g.rootDir = gitdir.Path
 	dirPointer := strings.Trim(g.env.FileContent(gitdir.Path), " \r\n")
 	matches := regex.FindNamedRegexMatch(`^gitdir: (?P<dir>.*)$`, dirPointer)
 	if matches == nil || matches["dir"] == "" {
@@ -179,49 +181,49 @@ func (g *Git) hasWorktree(gitdir *environment.FileInfo) bool {
 	}
 	// if we open a worktree file in a shared wsl2 folder, we have to convert it back
 	// to the mounted path
-	g.workingFolder = g.convertToLinuxPath(matches["dir"])
+	g.workingDir = g.convertToLinuxPath(matches["dir"])
 
 	// in worktrees, the path looks like this: gitdir: path/.git/worktrees/branch
 	// strips the last .git/worktrees part
 	// :ind+5 = index + /.git
-	ind := strings.LastIndex(g.workingFolder, "/.git/worktrees")
+	ind := strings.LastIndex(g.workingDir, "/.git/worktrees")
 	if ind > -1 {
-		gitDir := filepath.Join(g.workingFolder, "gitdir")
-		g.rootFolder = g.workingFolder[:ind+5]
-		g.realFolder = strings.TrimSuffix(g.env.FileContent(gitDir), ".git\n")
+		gitDir := filepath.Join(g.workingDir, "gitdir")
+		g.rootDir = g.workingDir[:ind+5]
+		g.realDir = strings.TrimSuffix(g.env.FileContent(gitDir), ".git\n")
 		g.IsWorkTree = true
 		return true
 	}
 	// in submodules, the path looks like this: gitdir: ../.git/modules/test-submodule
 	// we need the parent folder to detect where the real .git folder is
-	ind = strings.LastIndex(g.workingFolder, "/.git/modules")
+	ind = strings.LastIndex(g.workingDir, "/.git/modules")
 	if ind > -1 {
-		g.rootFolder = resolveGitPath(gitdir.ParentFolder, g.workingFolder)
+		g.rootDir = resolveGitPath(gitdir.ParentFolder, g.workingDir)
 		// this might be both a worktree and a submodule, where the path would look like
 		// this: path/.git/modules/module/path/worktrees/location. We cannot distinguish
 		// between worktree and a module path containing the word 'worktree,' however.
-		ind = strings.LastIndex(g.rootFolder, "/worktrees/")
-		if ind > -1 && g.env.HasFilesInDir(g.rootFolder, "gitdir") {
-			gitDir := filepath.Join(g.rootFolder, "gitdir")
+		ind = strings.LastIndex(g.rootDir, "/worktrees/")
+		if ind > -1 && g.env.HasFilesInDir(g.rootDir, "gitdir") {
+			gitDir := filepath.Join(g.rootDir, "gitdir")
 			realGitFolder := g.env.FileContent(gitDir)
-			g.realFolder = strings.TrimSuffix(realGitFolder, ".git\n")
-			g.rootFolder = g.rootFolder[:ind]
-			g.workingFolder = g.rootFolder
+			g.realDir = strings.TrimSuffix(realGitFolder, ".git\n")
+			g.rootDir = g.rootDir[:ind]
+			g.workingDir = g.rootDir
 			g.IsWorkTree = true
 			return true
 		}
-		g.realFolder = g.rootFolder
-		g.workingFolder = g.rootFolder
+		g.realDir = g.rootDir
+		g.workingDir = g.rootDir
 		return true
 	}
 
 	// check for separate git folder(--separate-git-dir)
 	// check if the folder contains a HEAD file
-	if g.env.HasFilesInDir(g.workingFolder, "HEAD") {
-		gitFolder := strings.TrimSuffix(g.rootFolder, ".git")
-		g.rootFolder = g.workingFolder
-		g.workingFolder = gitFolder
-		g.realFolder = gitFolder
+	if g.env.HasFilesInDir(g.workingDir, "HEAD") {
+		gitFolder := strings.TrimSuffix(g.rootDir, ".git")
+		g.rootDir = g.workingDir
+		g.workingDir = gitFolder
+		g.realDir = gitFolder
 		return true
 	}
 	return false
@@ -331,7 +333,7 @@ func (g *Git) setGitStatus() {
 }
 
 func (g *Git) getGitCommandOutput(args ...string) string {
-	args = append([]string{"-C", g.realFolder, "--no-optional-locks", "-c", "core.quotepath=false", "-c", "color.status=false"}, args...)
+	args = append([]string{"-C", g.realDir, "--no-optional-locks", "-c", "core.quotepath=false", "-c", "color.status=false"}, args...)
 	val, err := g.env.RunCommand(g.getCommand(GITCOMMAND), args...)
 	if err != nil {
 		return ""
@@ -357,7 +359,7 @@ func (g *Git) setGitHEADContext() {
 
 	getPrettyNameOrigin := func(file string) string {
 		var origin string
-		head := g.FileContents(g.workingFolder, file)
+		head := g.FileContents(g.workingDir, file)
 		if head == "detached HEAD" {
 			origin = formatDetached()
 		} else {
@@ -367,20 +369,20 @@ func (g *Git) setGitHEADContext() {
 		return origin
 	}
 
-	if g.env.HasFolder(g.workingFolder + "/rebase-merge") {
+	if g.env.HasFolder(g.workingDir + "/rebase-merge") {
 		origin := getPrettyNameOrigin("rebase-merge/head-name")
 		onto := g.getGitRefFileSymbolicName("rebase-merge/onto")
 		onto = g.formatHEAD(onto)
-		step := g.FileContents(g.workingFolder, "rebase-merge/msgnum")
-		total := g.FileContents(g.workingFolder, "rebase-merge/end")
+		step := g.FileContents(g.workingDir, "rebase-merge/msgnum")
+		total := g.FileContents(g.workingDir, "rebase-merge/end")
 		icon := g.props.GetString(RebaseIcon, "\uE728 ")
 		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.workingFolder + "/rebase-apply") {
+	if g.env.HasFolder(g.workingDir + "/rebase-apply") {
 		origin := getPrettyNameOrigin("rebase-apply/head-name")
-		step := g.FileContents(g.workingFolder, "rebase-apply/next")
-		total := g.FileContents(g.workingFolder, "rebase-apply/last")
+		step := g.FileContents(g.workingDir, "rebase-apply/next")
+		total := g.FileContents(g.workingDir, "rebase-apply/last")
 		icon := g.props.GetString(RebaseIcon, "\uE728 ")
 		g.HEAD = fmt.Sprintf("%s%s (%s/%s) at %s", icon, origin, step, total, g.HEAD)
 		return
@@ -389,7 +391,7 @@ func (g *Git) setGitHEADContext() {
 	commitIcon := g.props.GetString(CommitIcon, "\uF417")
 	if g.hasGitFile("MERGE_MSG") {
 		icon := g.props.GetString(MergeIcon, "\uE727 ")
-		mergeContext := g.FileContents(g.workingFolder, "MERGE_MSG")
+		mergeContext := g.FileContents(g.workingDir, "MERGE_MSG")
 		matches := regex.FindNamedRegexMatch(`Merge (remote-tracking )?(?P<type>branch|commit|tag) '(?P<theirs>.*)'`, mergeContext)
 		// head := g.getGitRefFileSymbolicName("ORIG_HEAD")
 		if matches != nil && matches["theirs"] != "" {
@@ -415,19 +417,19 @@ func (g *Git) setGitHEADContext() {
 	// reverts then CHERRY_PICK_HEAD/REVERT_HEAD will not exist so we have to read
 	// the todo file.
 	if g.hasGitFile("CHERRY_PICK_HEAD") {
-		sha := g.FileContents(g.workingFolder, "CHERRY_PICK_HEAD")
+		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") {
-		sha := g.FileContents(g.workingFolder, "REVERT_HEAD")
+		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.workingFolder, "sequencer/todo")
+		todo := g.FileContents(g.workingDir, "sequencer/todo")
 		matches := regex.FindNamedRegexMatch(`^(?P<action>p|pick|revert)\s+(?P<sha>\S+)`, todo)
 		if matches != nil && matches["sha"] != "" {
 			action := matches["action"]
@@ -464,18 +466,18 @@ func (g *Git) formatSHA(sha string) string {
 }
 
 func (g *Git) hasGitFile(file string) bool {
-	return g.env.HasFilesInDir(g.workingFolder, file)
+	return g.env.HasFilesInDir(g.workingDir, file)
 }
 
 func (g *Git) getGitRefFileSymbolicName(refFile string) string {
-	ref := g.FileContents(g.workingFolder, refFile)
+	ref := g.FileContents(g.workingDir, refFile)
 	return g.getGitCommandOutput("name-rev", "--name-only", "--exclude=tags/*", ref)
 }
 
 func (g *Git) setPrettyHEADName() {
 	// we didn't fetch status, fallback to parsing the HEAD file
 	if len(g.Hash) == 0 {
-		HEADRef := g.FileContents(g.workingFolder, "HEAD")
+		HEADRef := g.FileContents(g.workingDir, "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))
@@ -501,7 +503,7 @@ func (g *Git) setPrettyHEADName() {
 }
 
 func (g *Git) getStashContext() int {
-	stashContent := g.FileContents(g.rootFolder, "logs/refs/stash")
+	stashContent := g.FileContents(g.rootDir, "logs/refs/stash")
 	if stashContent == "" {
 		return 0
 	}
@@ -510,10 +512,10 @@ func (g *Git) getStashContext() int {
 }
 
 func (g *Git) getWorktreeContext() int {
-	if !g.env.HasFolder(g.rootFolder + "/worktrees") {
+	if !g.env.HasFolder(g.rootDir + "/worktrees") {
 		return 0
 	}
-	worktreeFolders := g.env.LsDir(g.rootFolder + "/worktrees")
+	worktreeFolders := g.env.LsDir(g.rootDir + "/worktrees")
 	var count int
 	for _, folder := range worktreeFolders {
 		if folder.IsDir() {
@@ -535,7 +537,7 @@ func (g *Git) getOriginURL(upstream string) string {
 		return fmt.Sprintf("https://%s", url)
 	}
 	var url string
-	cfg, err := ini.Load(g.rootFolder + "/config")
+	cfg, err := ini.Load(g.rootDir + "/config")
 	if err != nil {
 		url = g.getGitCommandOutput("remote", "get-url", upstream)
 		return cleanSSHURL(url)
@@ -562,7 +564,7 @@ func (g *Git) getSwitchMode(property properties.Property, gitSwitch, mode string
 		mode = val
 	}
 	// get the specific repo mode
-	if val := repoModes[g.realFolder]; len(val) != 0 {
+	if val := repoModes[g.realDir]; len(val) != 0 {
 		mode = val
 	}
 	if len(mode) == 0 {
diff --git a/src/segments/scm.go b/src/segments/scm.go
index a63a1e0f..d9abef14 100644
--- a/src/segments/scm.go
+++ b/src/segments/scm.go
@@ -45,10 +45,12 @@ type scm struct {
 	env   environment.Environment
 
 	IsWslSharedPath bool
-	workingFolder   string
-	rootFolder      string
-	realFolder      string // real folder (can be different from current path when in worktrees)
-	command         string
+	Dir             string // actual repo root directory
+
+	workingDir string
+	rootDir    string
+	realDir    string // real directory (can be different from current path when in worktrees)
+	command    string
 }
 
 const (
diff --git a/src/segments/svn.go b/src/segments/svn.go
index 2ac8fc84..292a1ec6 100644
--- a/src/segments/svn.go
+++ b/src/segments/svn.go
@@ -78,20 +78,20 @@ func (s *Svn) shouldDisplay() bool {
 	}
 
 	if Svndir.IsDir {
-		s.workingFolder = Svndir.Path
-		s.rootFolder = Svndir.Path
+		s.workingDir = Svndir.Path
+		s.rootDir = Svndir.Path
 		// convert the worktree file path to a windows one when in wsl 2 shared folder
-		s.realFolder = strings.TrimSuffix(s.convertToWindowsPath(Svndir.Path), ".svn")
+		s.realDir = strings.TrimSuffix(s.convertToWindowsPath(Svndir.Path), ".svn")
 		return true
 	}
 	// handle worktree
-	s.rootFolder = Svndir.Path
+	s.rootDir = Svndir.Path
 	dirPointer := strings.Trim(s.env.FileContent(Svndir.Path), " \r\n")
 	matches := regex.FindNamedRegexMatch(`^Svndir: (?P<dir>.*)$`, dirPointer)
 	if matches != nil && matches["dir"] != "" {
 		// if we open a worktree file in a shared wsl2 folder, we have to convert it back
 		// to the mounted path
-		s.workingFolder = s.convertToLinuxPath(matches["dir"])
+		s.workingDir = s.convertToLinuxPath(matches["dir"])
 		return false
 	}
 	return false
@@ -121,7 +121,7 @@ func (s *Svn) setSvnStatus() {
 }
 
 func (s *Svn) getSvnCommandOutput(command string, args ...string) string {
-	args = append([]string{command, s.realFolder}, args...)
+	args = append([]string{command, s.realDir}, args...)
 	val, err := s.env.RunCommand(s.getCommand(SVNCOMMAND), args...)
 	if err != nil {
 		return ""
diff --git a/website/docs/segments/git.mdx b/website/docs/segments/git.mdx
index cdd66a75..88ecfda8 100644
--- a/website/docs/segments/git.mdx
+++ b/website/docs/segments/git.mdx
@@ -130,6 +130,7 @@ instead of the repo path.
 - `.StashCount`: `int` - the stash count
 - `.WorktreeCount`: `int` - the worktree count
 - `.IsWorkTree`: `boolean` - if in a worktree repo or not
+- `.Dir`: `string` - the repository's root directory
 
 ### GitStatus
 


Test patch:
diff --git a/src/segments/git_test.go b/src/segments/git_test.go
index 025ffb6f..5afc2564 100644
--- a/src/segments/git_test.go
+++ b/src/segments/git_test.go
@@ -54,7 +54,7 @@ func TestEnabledInWorkingDirectory(t *testing.T) {
 		},
 	}
 	assert.True(t, g.Enabled())
-	assert.Equal(t, fileInfo.Path, g.workingFolder)
+	assert.Equal(t, fileInfo.Path, g.workingDir)
 }
 
 func TestResolveEmptyGitPath(t *testing.T) {
@@ -136,9 +136,9 @@ func TestEnabledInWorktree(t *testing.T) {
 			},
 		}
 		assert.Equal(t, tc.ExpectedEnabled, g.hasWorktree(fileInfo), tc.Case)
-		assert.Equal(t, tc.ExpectedWorkingFolder, g.workingFolder, tc.Case)
-		assert.Equal(t, tc.ExpectedRealFolder, g.realFolder, tc.Case)
-		assert.Equal(t, tc.ExpectedRootFolder, g.rootFolder, tc.Case)
+		assert.Equal(t, tc.ExpectedWorkingFolder, g.workingDir, tc.Case)
+		assert.Equal(t, tc.ExpectedRealFolder, g.realDir, tc.Case)
+		assert.Equal(t, tc.ExpectedRootFolder, g.rootDir, tc.Case)
 	}
 }
 
@@ -507,8 +507,8 @@ func TestGetStashContextZeroEntries(t *testing.T) {
 		env.On("FileContent", "/logs/refs/stash").Return(tc.StashContent)
 		g := &Git{
 			scm: scm{
-				env:           env,
-				workingFolder: "",
+				env:        env,
+				workingDir: "",
 			},
 		}
 		got := g.getStashContext()
@@ -809,7 +809,7 @@ func TestGitUntrackedMode(t *testing.T) {
 				props: properties.Map{
 					UntrackedModes: tc.UntrackedModes,
 				},
-				realFolder: "foo",
+				realDir: "foo",
 			},
 		}
 		got := g.getUntrackedFilesMode()
@@ -851,7 +851,7 @@ func TestGitIgnoreSubmodules(t *testing.T) {
 				props: properties.Map{
 					IgnoreSubmodules: tc.IgnoreSubmodules,
 				},
-				realFolder: "foo",
+				realDir: "foo",
 			},
 		}
 		got := g.getIgnoreSubmodulesMode()
diff --git a/src/segments/svn_test.go b/src/segments/svn_test.go
index af47a2d5..6ce67135 100644
--- a/src/segments/svn_test.go
+++ b/src/segments/svn_test.go
@@ -45,8 +45,8 @@ func TestSvnEnabledInWorkingDirectory(t *testing.T) {
 		},
 	}
 	assert.True(t, s.Enabled())
-	assert.Equal(t, fileInfo.Path, s.workingFolder)
-	assert.Equal(t, fileInfo.Path, s.realFolder)
+	assert.Equal(t, fileInfo.Path, s.workingDir)
+	assert.Equal(t, fileInfo.Path, s.realDir)
 }
 
 func TestSvnTemplateString(t *testing.T) {