Repository: JanDeDobbeleer/oh-my-posh
Pull request #1486 (2021-12-22 16:31:57)

fix(git): use the right git executable
### 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)
- [ ] Docs have been added / updated (for bug fixes/features)

### Description

Fix the neverending issue with git in WSL. Fix also the issue with worktrees and stashes.

<!---

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


lnu: scenarios tested:

- windows: git.exe used, not path conversions
- linux/macos: git used, no path conversions
- wsl1:
  - linux folder: git used no path conversions
  - windows shared folder: git.exe used, path converted to windows ones for git.exe to work(git.exe -C). Linux path still used when reading files directly(stashes, worktrees, ...).
- wsl2:
  - linux folder: git used no path conversions
  - windows shared folder: git.exe used, path converted to windows ones for git.exe to work(git.exe -C). Linux path still used when reading files directly(stashes, worktrees, ...).

Patch:
diff --git a/src/environment.go b/src/environment.go
index 8ec42dba..89aa7973 100644
--- a/src/environment.go
+++ b/src/environment.go
@@ -100,12 +100,16 @@ type environmentInfo interface {
 	doGet(url string, timeout int, requestModifiers ...HTTPRequestModifier) ([]byte, error)
 	hasParentFilePath(path string) (fileInfo *fileInfo, err error)
 	isWsl() bool
+	isWsl2() bool
 	stackCount() int
 	getTerminalWidth() (int, error)
 	getCachePath() string
 	cache() cache
 	close()
 	logs() string
+	inWSLSharedDrive() bool
+	convertToLinuxPath(path string) string
+	convertToWindowsPath(path string) string
 }
 
 type commandCache struct {
diff --git a/src/environment_unix.go b/src/environment_unix.go
index d6523ae7..17fa42e0 100644
--- a/src/environment_unix.go
+++ b/src/environment_unix.go
@@ -5,6 +5,7 @@ package main
 import (
 	"errors"
 	"os"
+	"strings"
 	"time"
 
 	"github.com/shirou/gopsutil/v3/host"
@@ -33,6 +34,15 @@ func (env *environment) isWsl() bool {
 	return env.getenv("WSL_DISTRO_NAME") != ""
 }
 
+func (env *environment) isWsl2() bool {
+	defer env.trace(time.Now(), "isWsl2")
+	if !env.isWsl() {
+		return false
+	}
+	uname := env.getFileContent("/proc/sys/kernel/osrelease")
+	return strings.Contains(uname, "WSL2")
+}
+
 func (env *environment) getTerminalWidth() (int, error) {
 	defer env.trace(time.Now(), "getTerminalWidth")
 	width, err := terminal.Width()
@@ -63,3 +73,23 @@ func (env *environment) getCachePath() string {
 func (env *environment) getWindowsRegistryKeyValue(path string) (*windowsRegistryValue, error) {
 	return nil, errors.New("not implemented")
 }
+
+func (env *environment) inWSLSharedDrive() bool {
+	return env.isWsl() && strings.HasPrefix(env.getcwd(), "/mnt/")
+}
+
+func (env *environment) convertToWindowsPath(path string) string {
+	windowsPath, err := env.runCommand("wslpath", "-w", path)
+	if err == nil {
+		return windowsPath
+	}
+	return path
+}
+
+func (env *environment) convertToLinuxPath(path string) string {
+	linuxPath, err := env.runCommand("wslpath", "-u", path)
+	if err == nil {
+		return linuxPath
+	}
+	return path
+}
diff --git a/src/environment_windows.go b/src/environment_windows.go
index d4ce5b7e..40e28da6 100644
--- a/src/environment_windows.go
+++ b/src/environment_windows.go
@@ -75,6 +75,11 @@ func (env *environment) isWsl() bool {
 	return false
 }
 
+func (env *environment) isWsl2() bool {
+	defer env.trace(time.Now(), "isWsl2")
+	return false
+}
+
 func (env *environment) getTerminalWidth() (int, error) {
 	defer env.trace(time.Now(), "getTerminalWidth")
 	handle, err := syscall.Open("CONOUT$", syscall.O_RDWR, 0)
@@ -243,3 +248,15 @@ func (env *environment) getWindowsRegistryKeyValue(path string) (*windowsRegistr
 		return nil, errors.New(errorLogMsg)
 	}
 }
+
+func (env *environment) inWSLSharedDrive() bool {
+	return false
+}
+
+func (env *environment) convertToWindowsPath(path string) string {
+	return path
+}
+
+func (env *environment) convertToLinuxPath(path string) string {
+	return path
+}
diff --git a/src/segment_git.go b/src/segment_git.go
index 7ecca3c9..b13583fc 100644
--- a/src/segment_git.go
+++ b/src/segment_git.go
@@ -45,11 +45,13 @@ type git struct {
 	WorktreeCount int
 	IsWorkTree    bool
 
-	gitWorkingFolder  string // .git working folder, can be different of root if using worktree
-	gitRootFolder     string // .git root folder
-	gitWorktreeFolder string // .git real worktree path
+	gitWorkingFolder string // .git working folder
+	gitRootFolder    string // .git root folder
+	gitRealFolder    string // .git real folder(can be different from current path when in worktrees)
 
 	gitCommand string
+
+	IsWslSharedPath bool
 }
 
 const (
@@ -102,6 +104,10 @@ const (
 )
 
 func (g *git) enabled() bool {
+	// when in wsl/wsl2 and in a windows shared folder
+	// we must use git.exe and convert paths accordingly
+	// for worktrees, stashes, and path to work
+	g.IsWslSharedPath = g.env.inWSLSharedDrive()
 	if !g.env.hasCommand(g.getGitCommand()) {
 		return false
 	}
@@ -116,7 +122,8 @@ func (g *git) enabled() bool {
 	if gitdir.isDir {
 		g.gitWorkingFolder = gitdir.path
 		g.gitRootFolder = gitdir.path
-		g.gitWorktreeFolder = strings.TrimSuffix(gitdir.path, ".git")
+		// convert the worktree file path to a windows one when in wsl 2 shared folder
+		g.gitRealFolder = strings.TrimSuffix(g.convertToWindowsPath(gitdir.path), ".git")
 		return true
 	}
 	// handle worktree
@@ -124,13 +131,15 @@ func (g *git) enabled() bool {
 	dirPointer := strings.Trim(g.env.getFileContent(gitdir.path), " \r\n")
 	matches := findNamedRegexMatch(`^gitdir: (?P<dir>.*)$`, dirPointer)
 	if matches != nil && matches["dir"] != "" {
-		g.gitWorkingFolder = matches["dir"]
+		// if we open a worktree file in a shared wsl2 folder, we have to convert it back
+		// to the mounted path
+		g.gitWorkingFolder = 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.gitWorkingFolder, "/.git/worktrees")
 		g.gitRootFolder = g.gitWorkingFolder[:ind+5]
-		g.gitWorktreeFolder = strings.TrimSuffix(g.env.getFileContent(g.gitWorkingFolder+"/gitdir"), ".git\n")
+		g.gitRealFolder = strings.TrimSuffix(g.env.getFileContent(g.gitWorkingFolder+"/gitdir"), ".git\n")
 		g.IsWorkTree = true
 		return true
 	}
@@ -278,25 +287,15 @@ func (g *git) getGitCommand() string {
 	if len(g.gitCommand) > 0 {
 		return g.gitCommand
 	}
-	inWSL2SharedDrive := func(env environmentInfo) bool {
-		if !env.isWsl() {
-			return false
-		}
-		if !strings.HasPrefix(env.getcwd(), "/mnt/") {
-			return false
-		}
-		uname, _ := g.env.runCommand("uname", "-r")
-		return strings.Contains(uname, "WSL2")
-	}
 	g.gitCommand = "git"
-	if g.env.getRuntimeGOOS() == windowsPlatform || inWSL2SharedDrive(g.env) {
+	if g.env.getRuntimeGOOS() == windowsPlatform || g.IsWslSharedPath {
 		g.gitCommand = "git.exe"
 	}
 	return g.gitCommand
 }
 
 func (g *git) getGitCommandOutput(args ...string) string {
-	args = append([]string{"-C", g.gitWorktreeFolder, "--no-optional-locks", "-c", "core.quotepath=false", "-c", "color.status=false"}, args...)
+	args = append([]string{"-C", g.gitRealFolder, "--no-optional-locks", "-c", "core.quotepath=false", "-c", "color.status=false"}, args...)
 	val, _ := g.env.runCommand(g.getGitCommand(), args...)
 	return val
 }
@@ -490,3 +489,17 @@ func (g *git) getOriginURL(upstream string) string {
 	}
 	return keyVal
 }
+
+func (g *git) convertToWindowsPath(path string) string {
+	if !g.IsWslSharedPath {
+		return path
+	}
+	return g.env.convertToWindowsPath(path)
+}
+
+func (g *git) convertToLinuxPath(path string) string {
+	if !g.IsWslSharedPath {
+		return path
+	}
+	return g.env.convertToLinuxPath(path)
+}


Test patch:
diff --git a/src/segment_git_test.go b/src/segment_git_test.go
index ee813a68..89907fd7 100644
--- a/src/segment_git_test.go
+++ b/src/segment_git_test.go
@@ -15,6 +15,7 @@ const (
 
 func TestEnabledGitNotFound(t *testing.T) {
 	env := new(MockedEnvironment)
+	env.On("inWSLSharedDrive", nil).Return(false)
 	env.On("hasCommand", "git").Return(false)
 	env.On("getRuntimeGOOS", nil).Return("")
 	env.On("isWsl", nil).Return(false)
@@ -28,6 +29,7 @@ func TestEnabledGitNotFound(t *testing.T) {
 
 func TestEnabledInWorkingDirectory(t *testing.T) {
 	env := new(MockedEnvironment)
+	env.On("inWSLSharedDrive", nil).Return(false)
 	env.On("hasCommand", "git").Return(true)
 	env.On("getRuntimeGOOS", nil).Return("")
 	env.On("isWsl", nil).Return(false)
@@ -48,6 +50,7 @@ func TestEnabledInWorkingDirectory(t *testing.T) {
 
 func TestEnabledInWorkingTree(t *testing.T) {
 	env := new(MockedEnvironment)
+	env.On("inWSLSharedDrive", nil).Return(false)
 	env.On("hasCommand", "git").Return(true)
 	env.On("getRuntimeGOOS", nil).Return("")
 	env.On("isWsl", nil).Return(false)
@@ -66,7 +69,7 @@ func TestEnabledInWorkingTree(t *testing.T) {
 	}
 	assert.True(t, g.enabled())
 	assert.Equal(t, "/dev/real_folder/.git/worktrees/folder_worktree", g.gitWorkingFolder)
-	assert.Equal(t, "/dev/folder_worktree", g.gitWorktreeFolder)
+	assert.Equal(t, "/dev/folder_worktree", g.gitRealFolder)
 }
 
 func TestGetGitOutputForCommand(t *testing.T) {
@@ -195,6 +198,7 @@ func TestSetGitHEADContextClean(t *testing.T) {
 	}
 	for _, tc := range cases {
 		env := new(MockedEnvironment)
+		env.On("inWSLSharedDrive", nil).Return(false)
 		env.On("getRuntimeGOOS", nil).Return("unix")
 		env.On("isWsl", nil).Return(false)
 		env.mockGitCommand("", "describe", "--tags", "--exact-match")
@@ -537,17 +541,18 @@ func TestShouldIgnoreRootRepository(t *testing.T) {
 
 func TestGetGitCommand(t *testing.T) {
 	cases := []struct {
-		Case     string
-		Expected string
-		IsWSL    bool
-		IsWSL1   bool
-		GOOS     string
-		CWD      string
+		Case            string
+		Expected        string
+		IsWSL           bool
+		IsWSL1          bool
+		GOOS            string
+		CWD             string
+		IsWslSharedPath bool
 	}{
 		{Case: "On Windows", Expected: "git.exe", GOOS: windowsPlatform},
 		{Case: "Non Windows", Expected: "git"},
 		{Case: "Iside WSL2, non shared", IsWSL: true, Expected: "git"},
-		{Case: "Iside WSL2, shared", Expected: "git.exe", IsWSL: true, CWD: "/mnt/bill"},
+		{Case: "Iside WSL2, shared", Expected: "git.exe", IsWSL: true, IsWslSharedPath: true, CWD: "/mnt/bill"},
 		{Case: "Iside WSL1, shared", Expected: "git", IsWSL: true, IsWSL1: true, CWD: "/mnt/bill"},
 	}
 
@@ -566,6 +571,12 @@ func TestGetGitCommand(t *testing.T) {
 				env: env,
 			},
 		}
+		if tc.IsWslSharedPath {
+			env.On("inWSLSharedDrive", nil).Return(true)
+			g.IsWslSharedPath = tc.IsWslSharedPath
+		} else {
+			env.On("inWSLSharedDrive", nil).Return(false)
+		}
 		assert.Equal(t, tc.Expected, g.getGitCommand(), tc.Case)
 	}
 }
diff --git a/src/segment_path_test.go b/src/segment_path_test.go
index 7b7051ba..e3ee0b25 100644
--- a/src/segment_path_test.go
+++ b/src/segment_path_test.go
@@ -154,6 +154,11 @@ func (env *MockedEnvironment) isWsl() bool {
 	return args.Bool(0)
 }
 
+func (env *MockedEnvironment) isWsl2() bool {
+	args := env.Called(nil)
+	return args.Bool(0)
+}
+
 func (env *MockedEnvironment) getTerminalWidth() (int, error) {
 	args := env.Called(nil)
 	return args.Int(0), args.Error(1)
@@ -178,6 +183,21 @@ func (env *MockedEnvironment) logs() string {
 	return args.String(0)
 }
 
+func (env *MockedEnvironment) inWSLSharedDrive() bool {
+	args := env.Called(nil)
+	return args.Bool(0)
+}
+
+func (env *MockedEnvironment) convertToWindowsPath(path string) string {
+	args := env.Called(nil)
+	return args.String(0)
+}
+
+func (env *MockedEnvironment) convertToLinuxPath(path string) string {
+	args := env.Called(nil)
+	return args.String(0)
+}
+
 const (
 	homeBill        = "/home/bill"
 	homeJan         = "/usr/home/jan"