From 6095a68e5e76b1a4dbcb70361cf929d34aa8291d Mon Sep 17 00:00:00 2001 From: Heath Stewart Date: Thu, 9 Jan 2025 19:04:17 -0800 Subject: [PATCH] fix(cache): invalidate Git cache for worktrees resolves #6085 --- src/segments/git.go | 119 ++++++++++++++++++--------------- src/segments/git_test.go | 20 +++--- src/segments/mercurial.go | 8 +-- src/segments/mercurial_test.go | 8 +-- src/segments/sapling.go | 8 +-- src/segments/sapling_test.go | 6 +- src/segments/scm.go | 6 +- src/segments/svn.go | 12 ++-- src/segments/svn_test.go | 4 +- 9 files changed, 101 insertions(+), 90 deletions(-) diff --git a/src/segments/git.go b/src/segments/git.go index 0d80abd32199..cb13e77aa8fa 100644 --- a/src/segments/git.go +++ b/src/segments/git.go @@ -224,8 +224,15 @@ func (g *Git) CacheKey() (string, bool) { return "", false } - ref := g.FileContents(dir.Path, "HEAD") + if !g.isRepo(dir) { + return "", false + } + + ref := g.FileContents(g.mainSCMDir, "HEAD") ref = strings.Replace(ref, "ref: refs/heads/", "", 1) + + // Use the repo clone in the cache key so the mapped path is consistent + // for primary and worktree repos. return fmt.Sprintf("%s@%s", dir.Path, ref), true } @@ -295,7 +302,7 @@ func (g *Git) StashCount() int { return g.stashCount } - stashContent := g.FileContents(g.rootDir, "logs/refs/stash") + stashContent := g.FileContents(g.scmDir, "logs/refs/stash") if stashContent == "" { return 0 } @@ -335,11 +342,11 @@ func (g *Git) shouldDisplay() bool { } if g.props.GetBool(FetchBareInfo, false) { - g.realDir = g.env.Pwd() + g.repoRootDir = g.env.Pwd() bare := g.getGitCommandOutput("rev-parse", "--is-bare-repository") if bare == trueStr { g.IsBare = true - g.workingDir = g.realDir + g.mainSCMDir = g.repoRootDir return true } } @@ -349,21 +356,25 @@ func (g *Git) shouldDisplay() bool { return false } + return g.isRepo(gitdir) +} + +func (g *Git) isRepo(gitdir *runtime.FileInfo) bool { g.setDir(gitdir.Path) if !gitdir.IsDir { if g.hasWorktree(gitdir) { - g.realDir = g.convertToWindowsPath(g.realDir) + g.repoRootDir = g.convertToWindowsPath(g.repoRootDir) return true } return false } - g.workingDir = gitdir.Path - g.rootDir = gitdir.Path + g.mainSCMDir = gitdir.Path + g.scmDir = gitdir.Path // convert the worktree file path to a windows one when in a WSL shared folder - g.realDir = strings.TrimSuffix(g.convertToWindowsPath(gitdir.Path), "/.git") + g.repoRootDir = strings.TrimSuffix(g.convertToWindowsPath(gitdir.Path), "/.git") return true } @@ -377,10 +388,10 @@ func (g *Git) getBareRepoInfo() { if file, err := g.env.HasParentFilePath(".git", true); err == nil && !file.IsDir { content := g.FileContents(file.ParentFolder, ".git") dir := strings.TrimPrefix(content, "gitdir: ") - g.workingDir = filepath.Join(file.ParentFolder, dir) + g.mainSCMDir = filepath.Join(file.ParentFolder, dir) } - head := g.FileContents(g.workingDir, "HEAD") + head := g.FileContents(g.mainSCMDir, "HEAD") branchIcon := g.props.GetString(BranchIcon, "\uE0A0") g.Ref = strings.Replace(head, "ref: refs/heads/", "", 1) g.HEAD = fmt.Sprintf("%s%s", branchIcon, g.formatBranch(g.Ref)) @@ -405,7 +416,7 @@ func (g *Git) setDir(dir string) { } func (g *Git) hasWorktree(gitdir *runtime.FileInfo) bool { - g.rootDir = gitdir.Path + g.scmDir = gitdir.Path content := g.env.FileContent(gitdir.Path) content = strings.Trim(content, " \r\n") matches := regex.FindNamedRegexMatch(`^gitdir: (?P.*)$`, content) @@ -417,20 +428,20 @@ func (g *Git) hasWorktree(gitdir *runtime.FileInfo) bool { // if we open a worktree file in a WSL shared folder, we have to convert it back // to the mounted path - g.workingDir = g.convertToLinuxPath(matches["dir"]) + g.mainSCMDir = g.convertToLinuxPath(matches["dir"]) // if we don't do this, we will identify the submodule as a worktree - isSubmodule := strings.Contains(g.workingDir, "/modules/") + isSubmodule := strings.Contains(g.mainSCMDir, "/modules/") // in worktrees, the path looks like this: gitdir: path/.git/worktrees/branch - // rootDir needs to become path/.git - // realDir needs to become path - ind := strings.LastIndex(g.workingDir, "/worktrees/") + // scmDir needs to become path/.git + // repoRootDir needs to become path + ind := strings.LastIndex(g.mainSCMDir, "/worktrees/") if ind > -1 && !isSubmodule { - gitDir := filepath.Join(g.workingDir, "gitdir") - g.rootDir = g.workingDir[:ind] + gitDir := filepath.Join(g.mainSCMDir, "gitdir") + g.scmDir = g.mainSCMDir[:ind] gitDirContent := g.env.FileContent(gitDir) - g.realDir = strings.TrimSuffix(gitDirContent, ".git\n") + g.repoRootDir = strings.TrimSuffix(gitDirContent, ".git\n") g.IsWorkTree = true return true } @@ -438,33 +449,33 @@ func (g *Git) hasWorktree(gitdir *runtime.FileInfo) bool { // 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 if isSubmodule { - g.rootDir = resolveGitPath(gitdir.ParentFolder, g.workingDir) + g.scmDir = resolveGitPath(gitdir.ParentFolder, g.mainSCMDir) // 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.rootDir, "/worktrees/") - if ind > -1 && g.env.HasFilesInDir(g.rootDir, "gitdir") { - gitDir := filepath.Join(g.rootDir, "gitdir") + ind = strings.LastIndex(g.scmDir, "/worktrees/") + if ind > -1 && g.env.HasFilesInDir(g.scmDir, "gitdir") { + gitDir := filepath.Join(g.scmDir, "gitdir") realGitFolder := g.env.FileContent(gitDir) - g.realDir = strings.TrimSuffix(realGitFolder, ".git\n") - g.rootDir = g.rootDir[:ind] - g.workingDir = g.rootDir + g.repoRootDir = strings.TrimSuffix(realGitFolder, ".git\n") + g.scmDir = g.scmDir[:ind] + g.mainSCMDir = g.scmDir g.IsWorkTree = true return true } - g.realDir = g.rootDir - g.workingDir = g.rootDir + g.repoRootDir = g.scmDir + g.mainSCMDir = g.scmDir return true } // check for separate git folder(--separate-git-dir) // check if the folder contains a HEAD file - if g.env.HasFilesInDir(g.workingDir, "HEAD") { - gitFolder := strings.TrimSuffix(g.rootDir, ".git") - g.rootDir = g.workingDir - g.workingDir = gitFolder - g.realDir = gitFolder + if g.env.HasFilesInDir(g.mainSCMDir, "HEAD") { + gitFolder := strings.TrimSuffix(g.scmDir, ".git") + g.scmDir = g.mainSCMDir + g.mainSCMDir = gitFolder + g.repoRootDir = gitFolder return true } @@ -473,7 +484,7 @@ func (g *Git) hasWorktree(gitdir *runtime.FileInfo) bool { func (g *Git) shouldIgnoreStatus() bool { list := g.props.GetStringArray(IgnoreStatus, []string{}) - return g.env.DirMatchesOneOf(g.realDir, list) + return g.env.DirMatchesOneOf(g.repoRootDir, list) } func (g *Git) setBranchStatus() { @@ -668,7 +679,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...) + args = append([]string{"-C", g.repoRootDir, "--no-optional-locks", "-c", "core.quotepath=false", "-c", "color.status=false"}, args...) val, err := g.env.RunCommand(g.command, args...) if err != nil { return "" @@ -695,7 +706,7 @@ func (g *Git) setGitHEADContext() { getPrettyNameOrigin := func(file string) string { var origin string - head := g.FileContents(g.workingDir, file) + head := g.FileContents(g.mainSCMDir, file) if head == "detached HEAD" { origin = formatDetached() } else { @@ -706,11 +717,11 @@ func (g *Git) setGitHEADContext() { } parseInt := func(file string) int { - val, _ := strconv.Atoi(g.FileContents(g.workingDir, file)) + val, _ := strconv.Atoi(g.FileContents(g.mainSCMDir, file)) return val } - if g.env.HasFolder(g.workingDir + "/rebase-merge") { + if g.env.HasFolder(g.mainSCMDir + "/rebase-merge") { head := getPrettyNameOrigin("rebase-merge/head-name") onto := g.getGitRefFileSymbolicName("rebase-merge/onto") onto = g.formatBranch(onto) @@ -729,7 +740,7 @@ func (g *Git) setGitHEADContext() { return } - if g.env.HasFolder(g.workingDir + "/rebase-apply") { + if g.env.HasFolder(g.mainSCMDir + "/rebase-apply") { head := getPrettyNameOrigin("rebase-apply/head-name") current := parseInt("rebase-apply/next") total := parseInt("rebase-apply/last") @@ -751,7 +762,7 @@ func (g *Git) setGitHEADContext() { if g.hasGitFile("MERGE_MSG") { g.Merge = true icon := g.props.GetString(MergeIcon, "\uE727 ") - mergeContext := g.FileContents(g.workingDir, "MERGE_MSG") + mergeContext := g.FileContents(g.mainSCMDir, "MERGE_MSG") matches := regex.FindNamedRegexMatch(`Merge (remote-tracking )?(?Pbranch|commit|tag) '(?P.*)'`, mergeContext) // head := g.getGitRefFileSymbolicName("ORIG_HEAD") if matches != nil && matches["theirs"] != "" { @@ -779,7 +790,7 @@ func (g *Git) setGitHEADContext() { // the todo file. if g.hasGitFile("CHERRY_PICK_HEAD") { g.CherryPick = true - sha := g.FileContents(g.workingDir, "CHERRY_PICK_HEAD") + sha := g.FileContents(g.mainSCMDir, "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 @@ -787,14 +798,14 @@ func (g *Git) setGitHEADContext() { if g.hasGitFile("REVERT_HEAD") { g.Revert = true - sha := g.FileContents(g.workingDir, "REVERT_HEAD") + sha := g.FileContents(g.mainSCMDir, "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.workingDir, "sequencer/todo") + todo := g.FileContents(g.mainSCMDir, "sequencer/todo") matches := regex.FindNamedRegexMatch(`^(?Pp|pick|revert)\s+(?P\S+)`, todo) if matches != nil && matches["sha"] != "" { action := matches["action"] @@ -825,18 +836,18 @@ func (g *Git) formatSHA(sha string) string { } func (g *Git) hasGitFile(file string) bool { - return g.env.HasFilesInDir(g.workingDir, file) + return g.env.HasFilesInDir(g.mainSCMDir, file) } func (g *Git) getGitRefFileSymbolicName(refFile string) string { - ref := g.FileContents(g.workingDir, refFile) + ref := g.FileContents(g.mainSCMDir, 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.ShortHash) == 0 { - HEADRef := g.FileContents(g.workingDir, "HEAD") + HEADRef := g.FileContents(g.mainSCMDir, "HEAD") g.Detached = !strings.HasPrefix(HEADRef, "ref:") if strings.HasPrefix(HEADRef, BRANCHPREFIX) { branchName := strings.TrimPrefix(HEADRef, BRANCHPREFIX) @@ -873,10 +884,10 @@ func (g *Git) WorktreeCount() int { if g.worktreeCount > 0 { return g.worktreeCount } - if !g.env.HasFolder(g.rootDir + "/worktrees") { + if !g.env.HasFolder(g.scmDir + "/worktrees") { return 0 } - worktreeFolders := g.env.LsDir(g.rootDir + "/worktrees") + worktreeFolders := g.env.LsDir(g.scmDir + "/worktrees") var count int for _, folder := range worktreeFolders { if folder.IsDir() { @@ -891,7 +902,7 @@ func (g *Git) getRemoteURL() string { if len(upstream) == 0 { upstream = "origin" } - cfg, err := ini.Load(g.rootDir + "/config") + cfg, err := ini.Load(g.scmDir + "/config") if err != nil { return g.getGitCommandOutput("remote", "get-url", upstream) } @@ -905,7 +916,7 @@ func (g *Git) getRemoteURL() string { func (g *Git) Remotes() map[string]string { var remotes = make(map[string]string) - location := filepath.Join(g.rootDir, "config") + location := filepath.Join(g.scmDir, "config") config := g.env.FileContent(location) cfg, err := ini.Load([]byte(config)) if err != nil { @@ -941,7 +952,7 @@ func (g *Git) getSwitchMode(property properties.Property, gitSwitch, mode string mode = val } // get the specific repo mode - if val := repoModes[g.realDir]; len(val) != 0 { + if val := repoModes[g.repoRootDir]; len(val) != 0 { mode = val } if len(mode) == 0 { @@ -952,12 +963,12 @@ func (g *Git) getSwitchMode(property properties.Property, gitSwitch, mode string func (g *Git) repoName() string { if !g.IsWorkTree { - return path.Base(g.convertToLinuxPath(g.realDir)) + return path.Base(g.convertToLinuxPath(g.repoRootDir)) } - ind := strings.LastIndex(g.workingDir, ".git/worktrees") + ind := strings.LastIndex(g.mainSCMDir, ".git/worktrees") if ind > -1 { - return path.Base(g.workingDir[:ind]) + return path.Base(g.mainSCMDir[:ind]) } return "" diff --git a/src/segments/git_test.go b/src/segments/git_test.go index f5df399a2c9a..21e2d0a10601 100644 --- a/src/segments/git_test.go +++ b/src/segments/git_test.go @@ -59,7 +59,7 @@ func TestEnabledInWorkingDirectory(t *testing.T) { g.Init(properties.Map{}, env) assert.True(t, g.Enabled()) - assert.Equal(t, fileInfo.Path, g.workingDir) + assert.Equal(t, fileInfo.Path, g.mainSCMDir) } func TestResolveEmptyGitPath(t *testing.T) { @@ -139,9 +139,9 @@ func TestEnabledInWorktree(t *testing.T) { g.Init(properties.Map{}, env) assert.Equal(t, tc.ExpectedEnabled, g.hasWorktree(fileInfo), 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) + assert.Equal(t, tc.ExpectedWorkingFolder, g.mainSCMDir, tc.Case) + assert.Equal(t, tc.ExpectedRealFolder, g.repoRootDir, tc.Case) + assert.Equal(t, tc.ExpectedRootFolder, g.scmDir, tc.Case) } } @@ -629,7 +629,7 @@ func TestGetStashContextZeroEntries(t *testing.T) { g := &Git{ scm: scm{ - workingDir: "", + mainSCMDir: "", }, } g.Init(properties.Map{}, env) @@ -934,7 +934,7 @@ func TestGitUntrackedMode(t *testing.T) { g := &Git{ scm: scm{ - realDir: "foo", + repoRootDir: "foo", }, } g.Init(props, new(mock.Environment)) @@ -979,7 +979,7 @@ func TestGitIgnoreSubmodules(t *testing.T) { g := &Git{ scm: scm{ - realDir: "foo", + repoRootDir: "foo", }, } g.Init(props, new(mock.Environment)) @@ -1165,7 +1165,7 @@ func TestGitRemotes(t *testing.T) { g := &Git{ scm: scm{ - realDir: "foo", + repoRootDir: "foo", }, } g.Init(properties.Map{}, env) @@ -1210,8 +1210,8 @@ func TestGitRepoName(t *testing.T) { g := &Git{ scm: scm{ - realDir: tc.RealDir, - workingDir: tc.WorkingDir, + repoRootDir: tc.RealDir, + mainSCMDir: tc.WorkingDir, }, IsWorkTree: tc.IsWorkTree, } diff --git a/src/segments/mercurial.go b/src/segments/mercurial.go index 1d03134aa228..559c3e855f31 100644 --- a/src/segments/mercurial.go +++ b/src/segments/mercurial.go @@ -83,10 +83,10 @@ func (hg *Mercurial) shouldDisplay() bool { hg.setDir(hgdir.ParentFolder) - hg.workingDir = hgdir.Path - hg.rootDir = hgdir.Path + hg.mainSCMDir = hgdir.Path + hg.scmDir = hgdir.Path // convert the worktree file path to a windows one when in a WSL shared folder - hg.realDir = strings.TrimSuffix(hg.convertToWindowsPath(hgdir.Path), "/.hg") + hg.repoRootDir = strings.TrimSuffix(hg.convertToWindowsPath(hgdir.Path), "/.hg") return true } @@ -165,7 +165,7 @@ func RemoveAtIndex(s []string, index int) []string { } func (hg *Mercurial) getHgCommandOutput(command string, args ...string) string { - args = append([]string{"-R", hg.realDir, command}, args...) + args = append([]string{"-R", hg.repoRootDir, command}, args...) val, err := hg.env.RunCommand(hg.command, args...) if err != nil { return "" diff --git a/src/segments/mercurial_test.go b/src/segments/mercurial_test.go index c80f4844b653..fc7603d7d0bd 100644 --- a/src/segments/mercurial_test.go +++ b/src/segments/mercurial_test.go @@ -42,8 +42,8 @@ func TestMercurialEnabledInWorkingDirectory(t *testing.T) { hg.Init(properties.Map{}, env) assert.True(t, hg.Enabled()) - assert.Equal(t, fileInfo.Path, hg.workingDir) - assert.Equal(t, fileInfo.Path, hg.realDir) + assert.Equal(t, fileInfo.Path, hg.mainSCMDir) + assert.Equal(t, fileInfo.Path, hg.repoRootDir) } func TestMercurialGetIdInfo(t *testing.T) { @@ -159,8 +159,8 @@ A Added.File } assert.True(t, hg.Enabled()) - assert.Equal(t, fileInfo.Path, hg.workingDir) - assert.Equal(t, fileInfo.Path, hg.realDir) + assert.Equal(t, fileInfo.Path, hg.mainSCMDir) + assert.Equal(t, fileInfo.Path, hg.repoRootDir) assert.Equal(t, tc.ExpectedWorking, hg.Working, tc.Case) assert.Equal(t, tc.ExpectedBranch, hg.Branch, tc.Case) assert.Equal(t, tc.ExpectedChangeSetID, hg.ChangeSetID, tc.Case) diff --git a/src/segments/sapling.go b/src/segments/sapling.go index db6a0bec00f4..0ebec3719eab 100644 --- a/src/segments/sapling.go +++ b/src/segments/sapling.go @@ -82,11 +82,11 @@ func (sl *Sapling) shouldDisplay() bool { return false } - sl.workingDir = slDir.Path - sl.rootDir = slDir.Path + sl.mainSCMDir = slDir.Path + sl.scmDir = slDir.Path // convert the worktree file path to a windows one when in a WSL shared folder - sl.realDir = strings.TrimSuffix(sl.convertToWindowsPath(slDir.Path), "/.sl") - sl.RepoName = path.Base(sl.convertToLinuxPath(sl.realDir)) + sl.repoRootDir = strings.TrimSuffix(sl.convertToWindowsPath(slDir.Path), "/.sl") + sl.RepoName = path.Base(sl.convertToLinuxPath(sl.repoRootDir)) sl.setDir(slDir.Path) return true diff --git a/src/segments/sapling_test.go b/src/segments/sapling_test.go index 28bc29b86d6f..d92576a80069 100644 --- a/src/segments/sapling_test.go +++ b/src/segments/sapling_test.go @@ -151,9 +151,9 @@ func TestShouldDisplay(t *testing.T) { got := sl.shouldDisplay() assert.Equal(t, tc.Expected, got, tc.Case) if tc.Expected { - assert.Equal(t, "/sapling/repo/.sl", sl.workingDir, tc.Case) - assert.Equal(t, "/sapling/repo/.sl", sl.rootDir, tc.Case) - assert.Equal(t, "/sapling/repo", sl.realDir, tc.Case) + assert.Equal(t, "/sapling/repo/.sl", sl.mainSCMDir, tc.Case) + assert.Equal(t, "/sapling/repo/.sl", sl.scmDir, tc.Case) + assert.Equal(t, "/sapling/repo", sl.repoRootDir, tc.Case) assert.Equal(t, "repo", sl.RepoName, tc.Case) } } diff --git a/src/segments/scm.go b/src/segments/scm.go index 58f5b6c8e66e..2b1eff137dc9 100644 --- a/src/segments/scm.go +++ b/src/segments/scm.go @@ -84,9 +84,9 @@ type scm struct { Dir string RepoName string - workingDir string - rootDir string - realDir string + mainSCMDir string + scmDir string + repoRootDir string command string IsWslSharedPath bool CommandMissing bool diff --git a/src/segments/svn.go b/src/segments/svn.go index 392a95c01405..08ae7f68caa4 100644 --- a/src/segments/svn.go +++ b/src/segments/svn.go @@ -79,21 +79,21 @@ func (s *Svn) shouldDisplay() bool { } if Svndir.IsDir { - s.workingDir = Svndir.Path - s.rootDir = Svndir.Path + s.mainSCMDir = Svndir.Path + s.scmDir = Svndir.Path // convert the worktree file path to a windows one when in a WSL shared folder - s.realDir = strings.TrimSuffix(s.convertToWindowsPath(Svndir.Path), "/.svn") + s.repoRootDir = strings.TrimSuffix(s.convertToWindowsPath(Svndir.Path), "/.svn") return true } // handle worktree - s.rootDir = Svndir.Path + s.scmDir = Svndir.Path dirPointer := strings.Trim(s.env.FileContent(Svndir.Path), " \r\n") matches := regex.FindNamedRegexMatch(`^Svndir: (?P.*)$`, dirPointer) if matches != nil && matches["dir"] != "" { // if we open a worktree file in a WSL shared folder, we have to convert it back // to the mounted path - s.workingDir = s.convertToLinuxPath(matches["dir"]) + s.mainSCMDir = s.convertToLinuxPath(matches["dir"]) } return false } @@ -141,7 +141,7 @@ func (s *Svn) Repo() string { } func (s *Svn) getSvnCommandOutput(command string, args ...string) string { - args = append([]string{command, s.realDir}, args...) + args = append([]string{command, s.repoRootDir}, args...) val, err := s.env.RunCommand(s.command, args...) if err != nil { return "" diff --git a/src/segments/svn_test.go b/src/segments/svn_test.go index b42b1d9dc5a0..bcd79b663054 100644 --- a/src/segments/svn_test.go +++ b/src/segments/svn_test.go @@ -44,8 +44,8 @@ func TestSvnEnabledInWorkingDirectory(t *testing.T) { s.Init(properties.Map{}, env) assert.True(t, s.Enabled()) - assert.Equal(t, fileInfo.Path, s.workingDir) - assert.Equal(t, fileInfo.Path, s.realDir) + assert.Equal(t, fileInfo.Path, s.mainSCMDir) + assert.Equal(t, fileInfo.Path, s.repoRootDir) } func TestSvnTemplateString(t *testing.T) {