fix(git): support submodules woth worktrees

resolves #2514
This commit is contained in:
Jan De Dobbeleer 2022-07-14 08:00:18 +02:00 committed by Jan De Dobbeleer
parent 859fd0bbef
commit f2ccee6df6
3 changed files with 149 additions and 120 deletions

View file

@ -110,8 +110,7 @@ func (env *ShellEnvironment) ConvertToWindowsPath(path string) string {
}
func (env *ShellEnvironment) ConvertToLinuxPath(path string) string {
linuxPath, err := env.RunCommand("wslpath", "-u", path)
if err == nil {
if linuxPath, err := env.RunCommand("wslpath", "-u", path); err == nil {
return linuxPath
}
return path

View file

@ -5,6 +5,7 @@ import (
"oh-my-posh/environment"
"oh-my-posh/properties"
"oh-my-posh/regex"
"path/filepath"
"strconv"
"strings"
@ -144,63 +145,84 @@ func (g *Git) shouldDisplay() bool {
// 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)) {
return false
}
gitdir, err := g.env.HasParentFilePath(".git")
if err != nil {
return false
}
if g.shouldIgnoreRootRepository(gitdir.ParentFolder) {
return false
}
if gitdir.IsDir {
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")
return true
if !gitdir.IsDir {
return g.hasWorktree(gitdir)
}
// handle worktree
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")
return true
}
func (g *Git) hasWorktree(gitdir *environment.FileInfo) bool {
g.rootFolder = gitdir.Path
dirPointer := strings.Trim(g.env.FileContent(gitdir.Path), " \r\n")
matches := regex.FindNamedRegexMatch(`^gitdir: (?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
g.workingFolder = g.convertToLinuxPath(matches["dir"])
if matches == nil || matches["dir"] == "" {
return false
}
// 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"])
// 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")
if ind > -1 {
g.rootFolder = g.workingFolder[:ind+5]
g.realFolder = strings.TrimSuffix(g.env.FileContent(g.workingFolder+"/gitdir"), ".git\n")
// 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")
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")
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")
if ind > -1 {
g.rootFolder = filepath.Join(gitdir.ParentFolder, g.workingFolder)
// this might be both a worktree and a submodule, where the path would look like
// this: ../.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, g.env.PathSeparator()+"worktrees"+g.env.PathSeparator())
if ind > -1 && g.env.HasFilesInDir(g.rootFolder, "gitdir") {
gitDir := filepath.Join(g.rootFolder, "gitdir")
realGitFolder := filepath.Clean(g.env.FileContent(gitDir))
g.realFolder = strings.TrimSuffix(realGitFolder, ".git\n")
g.rootFolder = g.rootFolder[:ind]
g.workingFolder = g.rootFolder
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")
if ind > -1 {
g.rootFolder = gitdir.ParentFolder + "/" + g.workingFolder
g.realFolder = g.rootFolder
g.workingFolder = g.rootFolder
return true
}
g.realFolder = g.rootFolder
g.workingFolder = g.rootFolder
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
return true
}
return false
// 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
return true
}
return false
}

View file

@ -5,6 +5,8 @@ import (
"oh-my-posh/environment"
"oh-my-posh/mock"
"oh-my-posh/properties"
"os"
"path/filepath"
"strings"
"testing"
@ -55,91 +57,97 @@ func TestEnabledInWorkingDirectory(t *testing.T) {
assert.Equal(t, fileInfo.Path, g.workingFolder)
}
func TestEnabledInWorkingTree(t *testing.T) {
env := new(mock.MockedEnvironment)
env.On("InWSLSharedDrive").Return(false)
env.On("HasCommand", "git").Return(true)
env.On("GOOS").Return("")
env.On("IsWsl").Return(false)
env.On("PathSeparator").Return("/")
fileInfo := &environment.FileInfo{
Path: "/dev/folder_worktree/.git",
ParentFolder: "/dev/folder_worktree",
IsDir: false,
}
env.On("FileContent", "/dev/real_folder/.git/worktrees/folder_worktree/HEAD").Return("")
env.MockGitCommand(fileInfo.ParentFolder, "", "describe", "--tags", "--exact-match")
env.On("HasParentFilePath", ".git").Return(fileInfo, nil)
env.On("FileContent", "/dev/folder_worktree/.git").Return("gitdir: /dev/real_folder/.git/worktrees/folder_worktree")
env.On("FileContent", "/dev/real_folder/.git/worktrees/folder_worktree/gitdir").Return("/dev/folder_worktree.git\n")
g := &Git{
scm: scm{
env: env,
props: properties.Map{},
func TestEnabledInWorktree(t *testing.T) {
cases := []struct {
Case string
ExpectedEnabled bool
WorkingFolder string
WorkingFolderAddon string
WorkingFolderContent string
GitDirContent string
ExpectedRealFolder string
ExpectedWorkingFolder string
ExpectedRootFolder string
WindowsPaths bool
}{
{
Case: "worktree",
ExpectedEnabled: true,
WorkingFolder: "/dev/.git/worktrees/folder_worktree",
WorkingFolderAddon: "gitdir",
WorkingFolderContent: "/dev/worktree.git\n",
ExpectedWorkingFolder: "/dev/.git/worktrees/folder_worktree",
ExpectedRealFolder: "/dev/worktree",
ExpectedRootFolder: "/dev/.git",
},
{
Case: "submodule",
ExpectedEnabled: true,
WorkingFolder: "./.git/modules/submodule",
ExpectedWorkingFolder: "/dev/.git/modules/submodule",
ExpectedRealFolder: "/dev/.git/modules/submodule",
ExpectedRootFolder: "/dev/.git/modules/submodule",
WindowsPaths: true,
},
{
Case: "submodule with root working folder",
ExpectedEnabled: true,
WorkingFolder: "/repo/.git/modules/submodule",
ExpectedWorkingFolder: "/dev/repo/.git/modules/submodule",
ExpectedRealFolder: "/dev/repo/.git/modules/submodule",
ExpectedRootFolder: "/dev/repo/.git/modules/submodule",
WindowsPaths: true,
},
{
Case: "submodule with worktrees",
ExpectedEnabled: true,
WorkingFolder: "./.git/modules/module/path/worktrees/location",
WorkingFolderAddon: "/dev/",
GitDirContent: "/dev/worktree.git\n",
ExpectedWorkingFolder: "/dev/.git/modules/module/path",
ExpectedRealFolder: "/dev/worktree",
ExpectedRootFolder: "/dev/.git/modules/module/path",
WindowsPaths: true,
},
{
Case: "separate git dir",
ExpectedEnabled: true,
WorkingFolder: "/dev/separate/.git/posh",
ExpectedWorkingFolder: "/dev/",
ExpectedRealFolder: "/dev/",
ExpectedRootFolder: "/dev/separate/.git/posh",
},
}
assert.True(t, g.Enabled())
assert.Equal(t, "/dev/real_folder/.git/worktrees/folder_worktree", g.workingFolder)
assert.Equal(t, "/dev/folder_worktree", g.realFolder)
fileInfo := &environment.FileInfo{
Path: "/dev/.git",
ParentFolder: "/dev",
}
for _, tc := range cases {
env := new(mock.MockedEnvironment)
env.On("FileContent", "/dev/.git").Return(fmt.Sprintf("gitdir: %s", tc.WorkingFolder))
env.On("FileContent", filepath.Join(tc.WorkingFolder, tc.WorkingFolderAddon)).Return(tc.WorkingFolderContent)
env.On("HasFilesInDir", tc.WorkingFolder, "HEAD").Return(true)
env.On("HasFilesInDir", filepath.Join(tc.WorkingFolderAddon, tc.WorkingFolder), "gitdir").Return(true)
env.On("FileContent", filepath.Join(tc.WorkingFolderAddon, tc.WorkingFolder, "gitdir")).Return(tc.GitDirContent)
env.On("PathSeparator").Return(string(os.PathSeparator))
g := &Git{
scm: scm{
env: env,
props: properties.Map{},
},
}
assert.Equal(t, tc.ExpectedEnabled, g.hasWorktree(fileInfo), tc.Case)
if tc.WindowsPaths {
tc.ExpectedRealFolder = filepath.Clean(tc.ExpectedRealFolder)
tc.ExpectedRootFolder = filepath.Clean(tc.ExpectedRootFolder)
tc.ExpectedWorkingFolder = filepath.Clean(tc.ExpectedWorkingFolder)
}
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)
}
}
func TestEnabledInSubmodule(t *testing.T) {
env := new(mock.MockedEnvironment)
env.On("InWSLSharedDrive").Return(false)
env.On("HasCommand", "git").Return(true)
env.On("GOOS").Return("")
env.On("IsWsl").Return(false)
env.On("PathSeparator").Return("/")
fileInfo := &environment.FileInfo{
Path: "/dev/parent/test-submodule/.git",
ParentFolder: "/dev/parent/test-submodule",
IsDir: false,
}
env.On("FileContent", "/dev/parent/test-submodule/../.git/modules/test-submodule/HEAD").Return("")
env.MockGitCommand("/dev/parent/test-submodule/../.git/modules/test-submodule", "", "describe", "--tags", "--exact-match")
env.On("HasParentFilePath", ".git").Return(fileInfo, nil)
env.On("FileContent", "/dev/parent/test-submodule/.git").Return("gitdir: ../.git/modules/test-submodule")
env.On("FileContent", "/dev/parent/.git/modules/test-submodule").Return("/dev/folder_worktree.git\n")
g := &Git{
scm: scm{
env: env,
props: properties.Map{},
},
}
assert.True(t, g.Enabled())
assert.Equal(t, "/dev/parent/test-submodule/../.git/modules/test-submodule", g.workingFolder)
assert.Equal(t, "/dev/parent/test-submodule/../.git/modules/test-submodule", g.realFolder)
assert.Equal(t, "/dev/parent/test-submodule/../.git/modules/test-submodule", g.rootFolder)
}
func TestEnabledInSeparateGitDir(t *testing.T) {
env := new(mock.MockedEnvironment)
env.On("InWSLSharedDrive").Return(false)
env.On("HasCommand", "git").Return(true)
env.On("GOOS").Return("")
env.On("IsWsl").Return(false)
env.On("PathSeparator").Return("/")
fileInfo := &environment.FileInfo{
Path: "/dev/parent/test-separate-git-dir/.git",
ParentFolder: "/dev/parent/test-separate-git-dir",
IsDir: false,
}
env.On("HasFilesInDir", "/dev/separate-git-dir", "HEAD").Return(true)
env.On("FileContent", "/dev/parent/test-separate-git-dir//HEAD").Return("")
env.MockGitCommand("/dev/parent/test-separate-git-dir/", "", "describe", "--tags", "--exact-match")
env.On("HasParentFilePath", ".git").Return(fileInfo, nil)
env.On("FileContent", "/dev/parent/test-separate-git-dir/.git").Return("gitdir: /dev/separate-git-dir")
g := &Git{
scm: scm{
env: env,
props: properties.Map{},
},
}
assert.True(t, g.Enabled())
assert.Equal(t, "/dev/parent/test-separate-git-dir/", g.workingFolder)
assert.Equal(t, "/dev/parent/test-separate-git-dir/", g.realFolder)
assert.Equal(t, "/dev/separate-git-dir", g.rootFolder)
}
func TestGetGitOutputForCommand(t *testing.T) {
args := []string{"-C", "", "--no-optional-locks", "-c", "core.quotepath=false", "-c", "color.status=false"}
commandArgs := []string{"symbolic-ref", "--short", "HEAD"}