mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-08-28 18:24:17 -05:00
`git worktree list` reports the main worktree as the common git dir with a trailing "/.git" removed, which equals the working tree only when the git dir sits inside it. In a submodule, a bare repo, or a repo using core.worktree it doesn't, so comparing the reported path against the working tree path matches nothing: no worktree is recognized as current or as main. Most visibly, inside a submodule lazygit claimed we were in a linked worktree named after the submodule, and offered to remove that "worktree". Comparing git dirs identifies a worktree unambiguously, so use that. A worktree whose directory is gone has no git dir to compare, and there we still have nothing better than its path. The submodule tests were asserting the linked-worktree suffix in the status view; it is gone now, and the repo name still says which submodule we're in. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
361 lines
12 KiB
Go
361 lines
12 KiB
Go
package git_commands
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/go-errors/errors"
|
|
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
|
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
|
"github.com/spf13/afero"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
func TestGetWorktrees(t *testing.T) {
|
|
type scenario struct {
|
|
testName string
|
|
repoPaths *RepoPaths
|
|
before func(runner *oscommands.FakeCmdObjRunner, fs afero.Fs, getRevParseArgs argFn)
|
|
expectedWorktrees []*models.Worktree
|
|
expectedErr string
|
|
}
|
|
|
|
scenarios := []scenario{
|
|
{
|
|
testName: "Single worktree (main)",
|
|
repoPaths: &RepoPaths{
|
|
repoPath: "/path/to/repo",
|
|
worktreePath: "/path/to/repo",
|
|
repoGitDirPath: "/path/to/repo/.git",
|
|
worktreeGitDirPath: "/path/to/repo/.git",
|
|
},
|
|
before: func(runner *oscommands.FakeCmdObjRunner, fs afero.Fs, getRevParseArgs argFn) {
|
|
runner.ExpectGitArgs([]string{"worktree", "list", "--porcelain"},
|
|
`worktree /path/to/repo
|
|
HEAD d85cc9d281fa6ae1665c68365fc70e75e82a042d
|
|
branch refs/heads/mybranch
|
|
`,
|
|
nil)
|
|
|
|
gitArgsMainWorktree := append(append([]string{"-C", "/path/to/repo"}, getRevParseArgs()...), "--absolute-git-dir")
|
|
runner.ExpectGitArgs(gitArgsMainWorktree, "/path/to/repo/.git", nil)
|
|
_ = fs.MkdirAll("/path/to/repo/.git", 0o755)
|
|
},
|
|
expectedWorktrees: []*models.Worktree{
|
|
{
|
|
IsMain: true,
|
|
IsCurrent: true,
|
|
Path: "/path/to/repo",
|
|
IsPathMissing: false,
|
|
GitDir: "/path/to/repo/.git",
|
|
Branch: "mybranch",
|
|
Head: "d85cc9d281fa6ae1665c68365fc70e75e82a042d",
|
|
Name: "repo",
|
|
},
|
|
},
|
|
expectedErr: "",
|
|
},
|
|
{
|
|
testName: "Multiple worktrees (main + linked)",
|
|
repoPaths: &RepoPaths{
|
|
repoPath: "/path/to/repo",
|
|
worktreePath: "/path/to/repo",
|
|
repoGitDirPath: "/path/to/repo/.git",
|
|
worktreeGitDirPath: "/path/to/repo/.git",
|
|
},
|
|
before: func(runner *oscommands.FakeCmdObjRunner, fs afero.Fs, getRevParseArgs argFn) {
|
|
runner.ExpectGitArgs([]string{"worktree", "list", "--porcelain"},
|
|
`worktree /path/to/repo
|
|
HEAD d85cc9d281fa6ae1665c68365fc70e75e82a042d
|
|
branch refs/heads/mybranch
|
|
|
|
worktree /path/to/repo-worktree
|
|
HEAD 775955775e79b8f5b4c4b56f82fbf657e2d5e4de
|
|
branch refs/heads/mybranch-worktree
|
|
`,
|
|
nil)
|
|
gitArgsMainWorktree := append(append([]string{"-C", "/path/to/repo"}, getRevParseArgs()...), "--absolute-git-dir")
|
|
runner.ExpectGitArgs(gitArgsMainWorktree, "/path/to/repo/.git", nil)
|
|
gitArgsLinkedWorktree := append(append([]string{"-C", "/path/to/repo-worktree"}, getRevParseArgs()...), "--absolute-git-dir")
|
|
runner.ExpectGitArgs(gitArgsLinkedWorktree, "/path/to/repo/.git/worktrees/repo-worktree", nil)
|
|
|
|
_ = fs.MkdirAll("/path/to/repo/.git", 0o755)
|
|
_ = fs.MkdirAll("/path/to/repo-worktree", 0o755)
|
|
_ = fs.MkdirAll("/path/to/repo/.git/worktrees/repo-worktree", 0o755)
|
|
_ = afero.WriteFile(fs, "/path/to/repo-worktree/.git", []byte("gitdir: /path/to/repo/.git/worktrees/repo-worktree"), 0o755)
|
|
},
|
|
expectedWorktrees: []*models.Worktree{
|
|
{
|
|
IsMain: true,
|
|
IsCurrent: true,
|
|
Path: "/path/to/repo",
|
|
IsPathMissing: false,
|
|
GitDir: "/path/to/repo/.git",
|
|
Branch: "mybranch",
|
|
Head: "d85cc9d281fa6ae1665c68365fc70e75e82a042d",
|
|
Name: "repo",
|
|
},
|
|
{
|
|
IsMain: false,
|
|
IsCurrent: false,
|
|
Path: "/path/to/repo-worktree",
|
|
IsPathMissing: false,
|
|
GitDir: "/path/to/repo/.git/worktrees/repo-worktree",
|
|
Branch: "mybranch-worktree",
|
|
Head: "775955775e79b8f5b4c4b56f82fbf657e2d5e4de",
|
|
Name: "repo-worktree",
|
|
},
|
|
},
|
|
expectedErr: "",
|
|
},
|
|
{
|
|
testName: "Worktree missing path",
|
|
repoPaths: &RepoPaths{
|
|
repoPath: "/path/to/repo",
|
|
worktreePath: "/path/to/repo",
|
|
repoGitDirPath: "/path/to/repo/.git",
|
|
worktreeGitDirPath: "/path/to/repo/.git",
|
|
},
|
|
before: func(runner *oscommands.FakeCmdObjRunner, fs afero.Fs, getRevParseArgs argFn) {
|
|
runner.ExpectGitArgs([]string{"worktree", "list", "--porcelain"},
|
|
`worktree /path/to/worktree
|
|
HEAD 775955775e79b8f5b4c4b56f82fbf657e2d5e4de
|
|
branch refs/heads/missingbranch
|
|
`,
|
|
nil)
|
|
|
|
_ = fs.MkdirAll("/path/to/repo/.git", 0o755)
|
|
},
|
|
expectedWorktrees: []*models.Worktree{
|
|
{
|
|
IsMain: false,
|
|
IsCurrent: false,
|
|
Path: "/path/to/worktree",
|
|
IsPathMissing: true,
|
|
GitDir: "",
|
|
Branch: "missingbranch",
|
|
Head: "775955775e79b8f5b4c4b56f82fbf657e2d5e4de",
|
|
Name: "worktree",
|
|
},
|
|
},
|
|
expectedErr: "",
|
|
},
|
|
{
|
|
testName: "In linked worktree",
|
|
repoPaths: &RepoPaths{
|
|
repoPath: "/path/to/repo",
|
|
worktreePath: "/path/to/repo-worktree",
|
|
repoGitDirPath: "/path/to/repo/.git",
|
|
worktreeGitDirPath: "/path/to/repo/.git/worktrees/repo-worktree",
|
|
},
|
|
before: func(runner *oscommands.FakeCmdObjRunner, fs afero.Fs, getRevParseArgs argFn) {
|
|
runner.ExpectGitArgs([]string{"worktree", "list", "--porcelain"},
|
|
`worktree /path/to/repo
|
|
HEAD d85cc9d281fa6ae1665c68365fc70e75e82a042d
|
|
branch refs/heads/mybranch
|
|
|
|
worktree /path/to/repo-worktree
|
|
HEAD 775955775e79b8f5b4c4b56f82fbf657e2d5e4de
|
|
branch refs/heads/mybranch-worktree
|
|
`,
|
|
nil)
|
|
gitArgsMainWorktree := append(append([]string{"-C", "/path/to/repo"}, getRevParseArgs()...), "--absolute-git-dir")
|
|
runner.ExpectGitArgs(gitArgsMainWorktree, "/path/to/repo/.git", nil)
|
|
gitArgsLinkedWorktree := append(append([]string{"-C", "/path/to/repo-worktree"}, getRevParseArgs()...), "--absolute-git-dir")
|
|
runner.ExpectGitArgs(gitArgsLinkedWorktree, "/path/to/repo/.git/worktrees/repo-worktree", nil)
|
|
|
|
_ = fs.MkdirAll("/path/to/repo/.git", 0o755)
|
|
_ = fs.MkdirAll("/path/to/repo-worktree", 0o755)
|
|
_ = fs.MkdirAll("/path/to/repo/.git/worktrees/repo-worktree", 0o755)
|
|
_ = afero.WriteFile(fs, "/path/to/repo-worktree/.git", []byte("gitdir: /path/to/repo/.git/worktrees/repo-worktree"), 0o755)
|
|
},
|
|
expectedWorktrees: []*models.Worktree{
|
|
{
|
|
IsMain: false,
|
|
IsCurrent: true,
|
|
Path: "/path/to/repo-worktree",
|
|
IsPathMissing: false,
|
|
GitDir: "/path/to/repo/.git/worktrees/repo-worktree",
|
|
Branch: "mybranch-worktree",
|
|
Head: "775955775e79b8f5b4c4b56f82fbf657e2d5e4de",
|
|
Name: "repo-worktree",
|
|
},
|
|
{
|
|
IsMain: true,
|
|
IsCurrent: false,
|
|
Path: "/path/to/repo",
|
|
IsPathMissing: false,
|
|
GitDir: "/path/to/repo/.git",
|
|
Branch: "mybranch",
|
|
Head: "d85cc9d281fa6ae1665c68365fc70e75e82a042d",
|
|
Name: "repo",
|
|
},
|
|
},
|
|
expectedErr: "",
|
|
},
|
|
{
|
|
testName: "In a submodule",
|
|
repoPaths: &RepoPaths{
|
|
repoPath: "/path/to/repo/mysubmodule",
|
|
worktreePath: "/path/to/repo/mysubmodule",
|
|
repoGitDirPath: "/path/to/repo/.git/modules/mysubmodule",
|
|
worktreeGitDirPath: "/path/to/repo/.git/modules/mysubmodule",
|
|
},
|
|
before: func(runner *oscommands.FakeCmdObjRunner, fs afero.Fs, getRevParseArgs argFn) {
|
|
// A submodule's git dir doesn't live inside its working tree, and
|
|
// `git worktree list` reports the git dir rather than the working
|
|
// tree it belongs to.
|
|
runner.ExpectGitArgs([]string{"worktree", "list", "--porcelain"},
|
|
`worktree /path/to/repo/.git/modules/mysubmodule
|
|
HEAD d85cc9d281fa6ae1665c68365fc70e75e82a042d
|
|
branch refs/heads/mybranch
|
|
`,
|
|
nil)
|
|
|
|
gitArgs := append(append([]string{"-C", "/path/to/repo/.git/modules/mysubmodule"}, getRevParseArgs()...), "--absolute-git-dir")
|
|
runner.ExpectGitArgs(gitArgs, "/path/to/repo/.git/modules/mysubmodule", nil)
|
|
|
|
_ = fs.MkdirAll("/path/to/repo/.git/modules/mysubmodule", 0o755)
|
|
},
|
|
expectedWorktrees: []*models.Worktree{
|
|
{
|
|
IsMain: true,
|
|
IsCurrent: true,
|
|
Path: "/path/to/repo/.git/modules/mysubmodule",
|
|
IsPathMissing: false,
|
|
GitDir: "/path/to/repo/.git/modules/mysubmodule",
|
|
Branch: "mybranch",
|
|
Head: "d85cc9d281fa6ae1665c68365fc70e75e82a042d",
|
|
Name: "mysubmodule",
|
|
},
|
|
},
|
|
expectedErr: "",
|
|
},
|
|
{
|
|
testName: "Detached HEAD worktree",
|
|
repoPaths: &RepoPaths{
|
|
repoPath: "/path/to/repo",
|
|
worktreePath: "/path/to/repo",
|
|
repoGitDirPath: "/path/to/repo/.git",
|
|
worktreeGitDirPath: "/path/to/repo/.git",
|
|
},
|
|
before: func(runner *oscommands.FakeCmdObjRunner, fs afero.Fs, getRevParseArgs argFn) {
|
|
runner.ExpectGitArgs([]string{"worktree", "list", "--porcelain"},
|
|
`worktree /path/to/repo
|
|
HEAD d85cc9d281fa6ae1665c68365fc70e75e82a042d
|
|
branch refs/heads/mybranch
|
|
|
|
worktree /path/to/repo-worktree
|
|
HEAD 775955775e79b8f5b4c4b56f82fbf657e2d5e4de
|
|
detached
|
|
`,
|
|
nil)
|
|
gitArgsMainWorktree := append(append([]string{"-C", "/path/to/repo"}, getRevParseArgs()...), "--absolute-git-dir")
|
|
runner.ExpectGitArgs(gitArgsMainWorktree, "/path/to/repo/.git", nil)
|
|
gitArgsLinkedWorktree := append(append([]string{"-C", "/path/to/repo-worktree"}, getRevParseArgs()...), "--absolute-git-dir")
|
|
runner.ExpectGitArgs(gitArgsLinkedWorktree, "/path/to/repo/.git/worktrees/repo-worktree", nil)
|
|
|
|
_ = fs.MkdirAll("/path/to/repo/.git", 0o755)
|
|
_ = fs.MkdirAll("/path/to/repo-worktree", 0o755)
|
|
_ = fs.MkdirAll("/path/to/repo/.git/worktrees/repo-worktree", 0o755)
|
|
},
|
|
expectedWorktrees: []*models.Worktree{
|
|
{
|
|
IsMain: true,
|
|
IsCurrent: true,
|
|
Path: "/path/to/repo",
|
|
IsPathMissing: false,
|
|
GitDir: "/path/to/repo/.git",
|
|
Branch: "mybranch",
|
|
Head: "d85cc9d281fa6ae1665c68365fc70e75e82a042d",
|
|
Name: "repo",
|
|
},
|
|
{
|
|
IsMain: false,
|
|
IsCurrent: false,
|
|
Path: "/path/to/repo-worktree",
|
|
IsPathMissing: false,
|
|
GitDir: "/path/to/repo/.git/worktrees/repo-worktree",
|
|
Branch: "",
|
|
Head: "775955775e79b8f5b4c4b56f82fbf657e2d5e4de",
|
|
Name: "repo-worktree",
|
|
},
|
|
},
|
|
expectedErr: "",
|
|
},
|
|
}
|
|
|
|
for _, s := range scenarios {
|
|
t.Run(s.testName, func(t *testing.T) {
|
|
runner := oscommands.NewFakeRunner(t)
|
|
fs := afero.NewMemMapFs()
|
|
version, err := GetGitVersion(oscommands.NewDummyOSCommand())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
getRevParseArgs := func() []string {
|
|
return []string{"rev-parse", "--path-format=absolute"}
|
|
}
|
|
|
|
s.before(runner, fs, getRevParseArgs)
|
|
|
|
loader := &WorktreeLoader{
|
|
GitCommon: buildGitCommon(commonDeps{runner: runner, fs: fs, repoPaths: s.repoPaths, gitVersion: version}),
|
|
}
|
|
|
|
worktrees, err := loader.GetWorktrees()
|
|
if s.expectedErr != "" {
|
|
assert.EqualError(t, errors.New(s.expectedErr), err.Error())
|
|
} else {
|
|
assert.NoError(t, err)
|
|
assert.EqualValues(t, s.expectedWorktrees, worktrees)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetUniqueNamesFromPaths(t *testing.T) {
|
|
for _, scenario := range []struct {
|
|
input []string
|
|
expected []string
|
|
}{
|
|
{
|
|
input: []string{},
|
|
expected: []string{},
|
|
},
|
|
{
|
|
input: []string{
|
|
"/my/path/feature/one",
|
|
},
|
|
expected: []string{
|
|
"one",
|
|
},
|
|
},
|
|
{
|
|
input: []string{
|
|
"/my/path/feature/one/",
|
|
},
|
|
expected: []string{
|
|
"one",
|
|
},
|
|
},
|
|
{
|
|
input: []string{
|
|
"/a/b/c/d",
|
|
"/a/b/c/e",
|
|
"/a/b/f/d",
|
|
"/a/e/c/d",
|
|
},
|
|
expected: []string{
|
|
"b/c/d",
|
|
"e",
|
|
"f/d",
|
|
"e/c/d",
|
|
},
|
|
},
|
|
} {
|
|
actual := getUniqueNamesFromPaths(scenario.input)
|
|
assert.EqualValues(t, scenario.expected, actual)
|
|
}
|
|
}
|