mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-08-28 10:15:32 -05:00
The worktrees tab now displays the checked-out branch (or detached HEAD state) for each worktree, making it much easier to see which worktree holds which branch. This is useful when you need to "unlock" a branch that's occupied by another worktree: you can now clearly see which worktrees are on a branch vs detached, without having to select each one individually. Also rename the "(main)" label to "(main worktree)" to avoid confusion with the common "main" branch name, and move it after the branch column.
313 lines
9.5 KiB
Go
313 lines
9.5 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",
|
|
},
|
|
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",
|
|
},
|
|
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",
|
|
},
|
|
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",
|
|
},
|
|
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: "Detached HEAD worktree",
|
|
repoPaths: &RepoPaths{
|
|
repoPath: "/path/to/repo",
|
|
worktreePath: "/path/to/repo",
|
|
},
|
|
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)
|
|
}
|
|
}
|