Repository: JanDeDobbeleer/oh-my-posh
Pull request #2791 (2022-09-15 07:04:35)

feat(wsl): allow using native executable on shared drive
relates to #2097

### 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/segments/fossil.go b/src/segments/fossil.go
index a026672e..b094808b 100644
--- a/src/segments/fossil.go
+++ b/src/segments/fossil.go
@@ -38,12 +38,11 @@ func (f *Fossil) Template() string {
 }
 
 func (f *Fossil) Enabled() bool {
-	command := f.getCommand(FOSSILCOMMAND)
-	if !f.env.HasCommand(command) {
+	if !f.hasCommand(FOSSILCOMMAND) {
 		return false
 	}
 	// run fossil command
-	output, err := f.env.RunCommand(command, "status")
+	output, err := f.env.RunCommand(f.command, "status")
 	if err != nil {
 		return false
 	}
diff --git a/src/segments/git.go b/src/segments/git.go
index abf673e7..13d8fc53 100644
--- a/src/segments/git.go
+++ b/src/segments/git.go
@@ -164,11 +164,7 @@ func (g *Git) Kraken() string {
 }
 
 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
-	g.IsWslSharedPath = g.env.InWSLSharedDrive()
-
-	if !g.env.HasCommand(g.getCommand(GITCOMMAND)) {
+	if !g.hasCommand(GITCOMMAND) {
 		return false
 	}
 
@@ -376,7 +372,7 @@ func (g *Git) setGitStatus() {
 
 func (g *Git) getGitCommandOutput(args ...string) string {
 	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...)
+	val, err := g.env.RunCommand(g.command, args...)
 	if err != nil {
 		return ""
 	}
diff --git a/src/segments/scm.go b/src/segments/scm.go
index 8bc8963d..0853e054 100644
--- a/src/segments/scm.go
+++ b/src/segments/scm.go
@@ -7,6 +7,11 @@ import (
 	"strings"
 )
 
+const (
+	// Fallback to native command
+	NativeFallback properties.Property = "native_fallback"
+)
+
 // ScmStatus represents part of the status of a repository
 type ScmStatus struct {
 	Unmerged   int
@@ -45,6 +50,7 @@ type scm struct {
 	env   environment.Environment
 
 	IsWslSharedPath bool
+	CommandMissing  bool
 	Dir             string // actual repo root directory
 
 	workingDir string
@@ -107,13 +113,28 @@ func (s *scm) convertToLinuxPath(path string) string {
 	return s.env.ConvertToLinuxPath(path)
 }
 
-func (s *scm) getCommand(command string) string {
+func (s *scm) hasCommand(command string) bool {
 	if len(s.command) > 0 {
-		return s.command
+		return true
 	}
-	s.command = command
+	// when in a WSL shared folder, we must use command.exe and convert paths accordingly
+	// for worktrees, stashes, and path to work
+	s.IsWslSharedPath = s.env.InWSLSharedDrive()
 	if s.env.GOOS() == environment.WINDOWS || s.IsWslSharedPath {
-		s.command += ".exe"
+		command += ".exe"
+	}
+	if s.env.HasCommand(command) {
+		s.command = command
+		return true
+	}
+	s.CommandMissing = true
+	// only use the native fallback when set by the user
+	if s.IsWslSharedPath && s.props.GetBool(NativeFallback, false) {
+		command = strings.TrimSuffix(command, ".exe")
+		if s.env.HasCommand(command) {
+			s.command = command
+			return true
+		}
 	}
-	return s.command
+	return false
 }
diff --git a/src/segments/svn.go b/src/segments/svn.go
index ce730cc4..4d6fd71f 100644
--- a/src/segments/svn.go
+++ b/src/segments/svn.go
@@ -62,10 +62,7 @@ func (s *Svn) Enabled() bool {
 }
 
 func (s *Svn) shouldDisplay() bool {
-	// when in a WSL shared folder, we must use git.exe and convert paths accordingly
-	// for worktrees, stashes, and path to work
-	s.IsWslSharedPath = s.env.InWSLSharedDrive()
-	if !s.env.HasCommand(s.getCommand(SVNCOMMAND)) {
+	if !s.hasCommand(SVNCOMMAND) {
 		return false
 	}
 	Svndir, err := s.env.HasParentFilePath(".svn")
@@ -120,7 +117,7 @@ func (s *Svn) setSvnStatus() {
 
 func (s *Svn) getSvnCommandOutput(command string, args ...string) string {
 	args = append([]string{command, s.realDir}, args...)
-	val, err := s.env.RunCommand(s.getCommand(SVNCOMMAND), args...)
+	val, err := s.env.RunCommand(s.command, args...)
 	if err != nil {
 		return ""
 	}
diff --git a/website/docs/segments/fossil.mdx b/website/docs/segments/fossil.mdx
index 52116c8e..3a85dcbb 100644
--- a/website/docs/segments/fossil.mdx
+++ b/website/docs/segments/fossil.mdx
@@ -28,6 +28,11 @@ Local changes can also be displayed which uses the following syntax:
 }
 ```
 
+## Properties
+
+- native_fallback: `boolean` - when set to `true` and `fossil.exe` is not available when inside a WSL2 shared Windows drive, we will fallback to the native fossil
+executable to fetch data. Not all information can be displayed in this case. Defaults to `false`.
+
 ## Template ([info][templates])
 
 :::note default template
diff --git a/website/docs/segments/git.mdx b/website/docs/segments/git.mdx
index 0eedbb90..7e5a05a8 100644
--- a/website/docs/segments/git.mdx
+++ b/website/docs/segments/git.mdx
@@ -66,6 +66,8 @@ You can set the following properties to `true` to enable fetching additional inf
 - ignore_submodules: `map[string]string` - map of repo's where to change the [--ignore-submodules][submodules] flag (`none`, `untracked`, `dirty` or `all`).
 For example `"ignore_submodules": { "/Users/me/repos/repo1": "all" }`. If you want to override for all repo's, use `*` to set the mode
 instead of the repo path.
+- native_fallback: `boolean` - when set to `true` and `git.exe` is not available when inside a WSL2 shared Windows drive, we will fallback to the native git
+executable to fetch data. Not all information can be displayed in this case. Defaults to `false`.
 
 ### Icons
 
diff --git a/website/docs/segments/svn.mdx b/website/docs/segments/svn.mdx
index 5111b156..e614fc43 100644
--- a/website/docs/segments/svn.mdx
+++ b/website/docs/segments/svn.mdx
@@ -40,6 +40,8 @@ As doing multiple [subversion][svn] calls can slow down the prompt experience, w
 You can set the following properties to `true` to enable fetching additional information (and populate the template).
 
 - fetch_status: `boolean` - fetch the local changes - defaults to `false`
+- native_fallback: `boolean` - when set to `true` and `svn.exe` is not available when inside a WSL2 shared Windows drive, we will fallback to the native svn
+executable to fetch data. Not all information can be displayed in this case. Defaults to `false`.
 
 ## Template ([info][templates])
 


Test patch:
diff --git a/src/segments/fossil_test.go b/src/segments/fossil_test.go
index a9879252..47d6f222 100644
--- a/src/segments/fossil_test.go
+++ b/src/segments/fossil_test.go
@@ -54,6 +54,7 @@ func TestFossilStatus(t *testing.T) {
 		env := new(mock.MockedEnvironment)
 		env.On("GOOS").Return("unix")
 		env.On("IsWsl").Return(false)
+		env.On("InWSLSharedDrive").Return(false)
 		env.On("HasCommand", FOSSILCOMMAND).Return(tc.HasCommand)
 		env.On("RunCommand", FOSSILCOMMAND, []string{"status"}).Return(strings.ReplaceAll(tc.Output, "\t", ""), tc.OutputError)
 		f := &Fossil{
diff --git a/src/segments/git_test.go b/src/segments/git_test.go
index 0fcdb504..1a96bedc 100644
--- a/src/segments/git_test.go
+++ b/src/segments/git_test.go
@@ -154,8 +154,9 @@ func TestGetGitOutputForCommand(t *testing.T) {
 	env.On("GOOS").Return("unix")
 	g := &Git{
 		scm: scm{
-			env:   env,
-			props: properties.Map{},
+			env:     env,
+			props:   properties.Map{},
+			command: GITCOMMAND,
 		},
 	}
 	got := g.getGitCommandOutput(commandArgs...)
@@ -308,6 +309,7 @@ func TestSetGitHEADContextClean(t *testing.T) {
 					TagIcon:        "tag ",
 					RevertIcon:     "revert ",
 				},
+				command: GITCOMMAND,
 			},
 			ShortHash: "1234567",
 			Ref:       tc.Ref,
@@ -346,6 +348,7 @@ func TestSetPrettyHEADName(t *testing.T) {
 					CommitIcon: "commit ",
 					TagIcon:    "tag ",
 				},
+				command: GITCOMMAND,
 			},
 			ShortHash: tc.ShortHash,
 		}
@@ -473,8 +476,9 @@ func TestSetGitStatus(t *testing.T) {
 		env.MockGitCommand("", strings.ReplaceAll(tc.Output, "\t", ""), "status", "-unormal", "--branch", "--porcelain=2")
 		g := &Git{
 			scm: scm{
-				env:   env,
-				props: properties.Map{},
+				env:     env,
+				props:   properties.Map{},
+				command: GITCOMMAND,
 			},
 		}
 		if tc.ExpectedWorking == nil {
@@ -546,8 +550,9 @@ func TestGitUpstream(t *testing.T) {
 		}
 		g := &Git{
 			scm: scm{
-				env:   env,
-				props: props,
+				env:     env,
+				props:   props,
+				command: GITCOMMAND,
 			},
 			Upstream: "origin/main",
 		}
@@ -595,48 +600,6 @@ func TestGetBranchStatus(t *testing.T) {
 	}
 }
 
-func TestGetGitCommand(t *testing.T) {
-	cases := []struct {
-		Case            string
-		Expected        string
-		IsWSL           bool
-		IsWSL1          bool
-		GOOS            string
-		CWD             string
-		IsWslSharedPath bool
-	}{
-		{Case: "On Windows", Expected: "git.exe", GOOS: environment.WINDOWS},
-		{Case: "Non Windows", Expected: "git"},
-		{Case: "Iside WSL2, non shared", IsWSL: true, Expected: "git"},
-		{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"},
-	}
-
-	for _, tc := range cases {
-		env := new(mock.MockedEnvironment)
-		env.On("IsWsl").Return(tc.IsWSL)
-		env.On("GOOS").Return(tc.GOOS)
-		env.On("Pwd").Return(tc.CWD)
-		wslUname := "5.10.60.1-microsoft-standard-WSL2"
-		if tc.IsWSL1 {
-			wslUname = "4.4.0-19041-Microsoft"
-		}
-		env.On("RunCommand", "uname", []string{"-r"}).Return(wslUname, nil)
-		g := &Git{
-			scm: scm{
-				env: env,
-			},
-		}
-		if tc.IsWslSharedPath {
-			env.On("InWSLSharedDrive").Return(true)
-			g.IsWslSharedPath = tc.IsWslSharedPath
-		} else {
-			env.On("InWSLSharedDrive").Return(false)
-		}
-		assert.Equal(t, tc.Expected, g.getCommand(GITCOMMAND), tc.Case)
-	}
-}
-
 func TestGitTemplateString(t *testing.T) {
 	cases := []struct {
 		Case     string
diff --git a/src/segments/posh_git_test.go b/src/segments/posh_git_test.go
index fee78e89..16a5d76e 100644
--- a/src/segments/posh_git_test.go
+++ b/src/segments/posh_git_test.go
@@ -193,6 +193,7 @@ func TestPoshGitSegment(t *testing.T) {
 				props: &properties.Map{
 					FetchUpstreamIcon: tc.FetchUpstreamIcon,
 				},
+				command: GITCOMMAND,
 			},
 		}
 		if len(tc.Template) == 0 {
diff --git a/src/segments/scm_test.go b/src/segments/scm_test.go
index 9b6f711d..b0d89868 100644
--- a/src/segments/scm_test.go
+++ b/src/segments/scm_test.go
@@ -1,6 +1,8 @@
 package segments
 
 import (
+	"oh-my-posh/environment"
+	"oh-my-posh/mock"
 	"oh-my-posh/properties"
 	"testing"
 
@@ -154,3 +156,39 @@ func TestTruncateBranchWithSymbol(t *testing.T) {
 		assert.Equal(t, tc.Expected, p.truncateBranch(tc.Branch), tc.Case)
 	}
 }
+
+func TestHasCommand(t *testing.T) {
+	cases := []struct {
+		Case            string
+		ExpectedCommand string
+		Command         string
+		GOOS            string
+		IsWslSharedPath bool
+		NativeFallback  bool
+	}{
+		{Case: "On Windows", ExpectedCommand: "git.exe", GOOS: environment.WINDOWS},
+		{Case: "Cache", ExpectedCommand: "git.exe", Command: "git.exe"},
+		{Case: "Non Windows", ExpectedCommand: "git"},
+		{Case: "Iside WSL2, non shared", ExpectedCommand: "git"},
+		{Case: "Iside WSL2, shared", ExpectedCommand: "git.exe", IsWslSharedPath: true},
+		{Case: "Iside WSL2, shared fallback", ExpectedCommand: "git", IsWslSharedPath: true, NativeFallback: true},
+	}
+
+	for _, tc := range cases {
+		env := new(mock.MockedEnvironment)
+		env.On("GOOS").Return(tc.GOOS)
+		env.On("InWSLSharedDrive").Return(tc.IsWslSharedPath)
+		env.On("HasCommand", "git").Return(true)
+		env.On("HasCommand", "git.exe").Return(!tc.NativeFallback)
+		s := &scm{
+			env: env,
+			props: properties.Map{
+				NativeFallback: tc.NativeFallback,
+			},
+			command: tc.Command,
+		}
+
+		_ = s.hasCommand(GITCOMMAND)
+		assert.Equal(t, tc.ExpectedCommand, s.command, tc.Case)
+	}
+}
diff --git a/src/segments/svn_test.go b/src/segments/svn_test.go
index 6ce67135..ccbea01f 100644
--- a/src/segments/svn_test.go
+++ b/src/segments/svn_test.go
@@ -234,8 +234,9 @@ R       Moved.File`,
 
 		s := &Svn{
 			scm: scm{
-				env:   env,
-				props: properties.Map{},
+				env:     env,
+				props:   properties.Map{},
+				command: SVNCOMMAND,
 			},
 		}
 		s.setSvnStatus()