Add more worktree tests

This commit is contained in:
Jesse Duffield 2023-07-24 16:36:11 +10:00
parent b93b9dae88
commit a313b16704
12 changed files with 325 additions and 30 deletions

View file

@ -74,6 +74,7 @@ The permitted contexts are:
| -------------- | -------------------------------------------------------------------------------------------------------- | | -------------- | -------------------------------------------------------------------------------------------------------- |
| status | The 'Status' tab | | status | The 'Status' tab |
| files | The 'Files' tab | | files | The 'Files' tab |
| worktrees | The 'Worktrees' tab |
| localBranches | The 'Local Branches' tab | | localBranches | The 'Local Branches' tab |
| remotes | The 'Remotes' tab | | remotes | The 'Remotes' tab |
| remoteBranches | The context you get when pressing enter on a remote in the remotes tab | | remoteBranches | The context you get when pressing enter on a remote in the remotes tab |
@ -300,6 +301,7 @@ SelectedRemote
SelectedTag SelectedTag
SelectedStashEntry SelectedStashEntry
SelectedCommitFile SelectedCommitFile
SelectedWorktree
CheckedOutBranch CheckedOutBranch
``` ```

View file

@ -19,12 +19,16 @@ func NewBisectCommands(gitCommon *GitCommon) *BisectCommands {
// This command is pretty cheap to run so we're not storing the result anywhere. // This command is pretty cheap to run so we're not storing the result anywhere.
// But if it becomes problematic we can chang that. // But if it becomes problematic we can chang that.
func (self *BisectCommands) GetInfo() *BisectInfo { func (self *BisectCommands) GetInfo() *BisectInfo {
return self.GetInfoForGitDir(self.dotGitDir)
}
func (self *BisectCommands) GetInfoForGitDir(gitDir string) *BisectInfo {
var err error var err error
info := &BisectInfo{started: false, log: self.Log, newTerm: "bad", oldTerm: "good"} info := &BisectInfo{started: false, log: self.Log, newTerm: "bad", oldTerm: "good"}
// we return nil if we're not in a git bisect session. // we return nil if we're not in a git bisect session.
// we know we're in a session by the presence of a .git/BISECT_START file // we know we're in a session by the presence of a .git/BISECT_START file
bisectStartPath := filepath.Join(self.dotGitDir, "BISECT_START") bisectStartPath := filepath.Join(gitDir, "BISECT_START")
exists, err := self.os.FileExists(bisectStartPath) exists, err := self.os.FileExists(bisectStartPath)
if err != nil { if err != nil {
self.Log.Infof("error getting git bisect info: %s", err.Error()) self.Log.Infof("error getting git bisect info: %s", err.Error())
@ -44,7 +48,7 @@ func (self *BisectCommands) GetInfo() *BisectInfo {
info.started = true info.started = true
info.start = strings.TrimSpace(string(startContent)) info.start = strings.TrimSpace(string(startContent))
termsContent, err := os.ReadFile(filepath.Join(self.dotGitDir, "BISECT_TERMS")) termsContent, err := os.ReadFile(filepath.Join(gitDir, "BISECT_TERMS"))
if err != nil { if err != nil {
// old git versions won't have this file so we default to bad/good // old git versions won't have this file so we default to bad/good
} else { } else {
@ -53,7 +57,7 @@ func (self *BisectCommands) GetInfo() *BisectInfo {
info.oldTerm = splitContent[1] info.oldTerm = splitContent[1]
} }
bisectRefsDir := filepath.Join(self.dotGitDir, "refs", "bisect") bisectRefsDir := filepath.Join(gitDir, "refs", "bisect")
files, err := os.ReadDir(bisectRefsDir) files, err := os.ReadDir(bisectRefsDir)
if err != nil { if err != nil {
self.Log.Infof("error getting git bisect info: %s", err.Error()) self.Log.Infof("error getting git bisect info: %s", err.Error())
@ -85,7 +89,7 @@ func (self *BisectCommands) GetInfo() *BisectInfo {
info.statusMap[sha] = status info.statusMap[sha] = status
} }
currentContent, err := os.ReadFile(filepath.Join(self.dotGitDir, "BISECT_EXPECTED_REV")) currentContent, err := os.ReadFile(filepath.Join(gitDir, "BISECT_EXPECTED_REV"))
if err != nil { if err != nil {
self.Log.Infof("error getting git bisect info: %s", err.Error()) self.Log.Infof("error getting git bisect info: %s", err.Error())
return info return info

View file

@ -48,10 +48,23 @@ func (self *WorktreeLoader) GetWorktrees() ([]*models.Worktree, error) {
} }
if strings.HasPrefix(splitLine, "worktree ") { if strings.HasPrefix(splitLine, "worktree ") {
path := strings.SplitN(splitLine, " ", 2)[1] path := strings.SplitN(splitLine, " ", 2)[1]
isMain := path == currentRepoPath
var gitDir string
if isMain {
gitDir = filepath.Join(path, ".git")
} else {
var ok bool
gitDir, ok = LinkedWorktreeGitPath(path)
if !ok {
self.Log.Warnf("Could not find git dir for worktree %s", path)
}
}
current = &models.Worktree{ current = &models.Worktree{
IsMain: path == currentRepoPath, IsMain: path == currentRepoPath,
Path: path, Path: path,
GitDir: gitDir,
} }
} else if strings.HasPrefix(splitLine, "branch ") { } else if strings.HasPrefix(splitLine, "branch ") {
branch := strings.SplitN(splitLine, " ", 2)[1] branch := strings.SplitN(splitLine, " ", 2)[1]
@ -91,9 +104,21 @@ func (self *WorktreeLoader) GetWorktrees() ([]*models.Worktree, error) {
continue continue
} }
// If we couldn't find the git directory, we can't find the branch name
if worktree.GitDir == "" {
continue
}
rebaseBranch, ok := rebaseBranch(worktree) rebaseBranch, ok := rebaseBranch(worktree)
if ok { if ok {
worktree.Branch = rebaseBranch worktree.Branch = rebaseBranch
continue
}
bisectBranch, ok := bisectBranch(worktree)
if ok {
worktree.Branch = bisectBranch
continue
} }
} }
@ -101,29 +126,25 @@ func (self *WorktreeLoader) GetWorktrees() ([]*models.Worktree, error) {
} }
func rebaseBranch(worktree *models.Worktree) (string, bool) { func rebaseBranch(worktree *models.Worktree) (string, bool) {
var gitPath string for _, dir := range []string{"rebase-merge", "rebase-apply"} {
if worktree.Main() { if bytesContent, err := os.ReadFile(filepath.Join(worktree.GitDir, dir, "head-name")); err == nil {
gitPath = filepath.Join(worktree.Path, ".git") headName := strings.TrimSpace(string(bytesContent))
} else { shortHeadName := strings.TrimPrefix(headName, "refs/heads/")
// need to find the path of the linked worktree in the .git dir return shortHeadName, true
var ok bool
gitPath, ok = LinkedWorktreeGitPath(worktree.Path)
if !ok {
return "", false
} }
} }
// now we look inside that git path for a file `rebase-merge/head-name` return "", false
// if it exists, we update the worktree to say that it has that for a head }
headNameContents, err := os.ReadFile(filepath.Join(gitPath, "rebase-merge", "head-name"))
func bisectBranch(worktree *models.Worktree) (string, bool) {
bisectStartPath := filepath.Join(worktree.GitDir, "BISECT_START")
startContent, err := os.ReadFile(bisectStartPath)
if err != nil { if err != nil {
return "", false return "", false
} }
headName := strings.TrimSpace(string(headNameContents)) return strings.TrimSpace(string(startContent)), true
shortHeadName := strings.TrimPrefix(headName, "refs/heads/")
return shortHeadName, true
} }
func LinkedWorktreeGitPath(worktreePath string) (string, bool) { func LinkedWorktreeGitPath(worktreePath string) (string, bool) {

View file

@ -4,9 +4,19 @@ package models
type Worktree struct { type Worktree struct {
// if false, this is a linked worktree // if false, this is a linked worktree
IsMain bool IsMain bool
Path string // path to the directory of the worktree i.e. the directory that contains all the user's files
Path string
// path of the git directory for this worktree. The equivalent of the .git directory
// in the main worktree. For linked worktrees this would be <repo_path>/.git/worktrees/<name>
GitDir string
// If the worktree has a branch checked out, this field will be set to the branch name.
// A branch is considered 'checked out' if:
// * the worktree is directly on the branch
// * the worktree is mid-rebase on the branch
// * the worktree is mid-bisect on the branch
Branch string Branch string
// based on the path, but uniquified // based on the path, but uniquified. Not the same name that git uses in the worktrees/ folder (no good reason for this,
// I just prefer my naming convention better)
NameField string NameField string
} }

View file

@ -475,7 +475,10 @@ func (self *BranchesController) rename(branch *models.Branch) error {
} }
// need to find where the branch is now so that we can re-select it. That means we need to refetch the branches synchronously and then find our branch // need to find where the branch is now so that we can re-select it. That means we need to refetch the branches synchronously and then find our branch
_ = self.c.Refresh(types.RefreshOptions{Mode: types.SYNC, Scope: []types.RefreshableView{types.BRANCHES}}) _ = self.c.Refresh(types.RefreshOptions{
Mode: types.SYNC,
Scope: []types.RefreshableView{types.BRANCHES, types.WORKTREES},
})
// now that we've got our stuff again we need to find that branch and reselect it. // now that we've got our stuff again we need to find that branch and reselect it.
for i, newBranch := range self.c.Model().Branches { for i, newBranch := range self.c.Model().Branches {

View file

@ -33,6 +33,7 @@ type SessionState struct {
SelectedStashEntry *models.StashEntry SelectedStashEntry *models.StashEntry
SelectedCommitFile *models.CommitFile SelectedCommitFile *models.CommitFile
SelectedCommitFilePath string SelectedCommitFilePath string
SelectedWorktree *models.Worktree
CheckedOutBranch *models.Branch CheckedOutBranch *models.Branch
} }
@ -50,6 +51,7 @@ func (self *SessionStateLoader) call() *SessionState {
SelectedCommitFile: self.c.Contexts().CommitFiles.GetSelectedFile(), SelectedCommitFile: self.c.Contexts().CommitFiles.GetSelectedFile(),
SelectedCommitFilePath: self.c.Contexts().CommitFiles.GetSelectedPath(), SelectedCommitFilePath: self.c.Contexts().CommitFiles.GetSelectedPath(),
SelectedSubCommit: self.c.Contexts().SubCommits.GetSelected(), SelectedSubCommit: self.c.Contexts().SubCommits.GetSelected(),
SelectedWorktree: self.c.Contexts().Worktrees.GetSelected(),
CheckedOutBranch: self.refsHelper.GetCheckedOutRef(), CheckedOutBranch: self.refsHelper.GetCheckedOutRef(),
} }
} }

View file

@ -221,7 +221,11 @@ var tests = []*components.IntegrationTest{
undo.UndoCheckoutAndDrop, undo.UndoCheckoutAndDrop,
undo.UndoDrop, undo.UndoDrop,
worktree.AddFromBranch, worktree.AddFromBranch,
worktree.AddFromBranchDetached,
worktree.AddFromCommit,
worktree.Bisect,
worktree.Crud, worktree.Crud,
worktree.CustomCommand,
worktree.DetachWorktreeFromBranch, worktree.DetachWorktreeFromBranch,
worktree.ForceRemoveWorktree, worktree.ForceRemoveWorktree,
worktree.Rebase, worktree.Rebase,

View file

@ -0,0 +1,46 @@
package worktree
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var AddFromBranchDetached = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Add a detached worktree via the branches view",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.NewBranch("mybranch")
shell.CreateFileAndAdd("README.md", "hello world")
shell.Commit("initial commit")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Branches().
Focus().
Lines(
Contains("mybranch"),
).
Press(keys.Worktrees.ViewWorktreeOptions).
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("Worktree")).
Select(Contains(`Create worktree from mybranch (detached)`)).
Confirm()
t.ExpectPopup().Prompt().
Title(Equals("New worktree path")).
Type("../linked-worktree").
Confirm()
}).
// confirm we're still focused on the branches view
IsFocused().
Lines(
Contains("(no branch)").IsSelected(),
Contains("mybranch (worktree)"),
)
t.Views().Status().
Content(Contains("repo(linked-worktree)"))
},
})

View file

@ -0,0 +1,56 @@
package worktree
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var AddFromCommit = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Add a worktree via the commits view",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.NewBranch("mybranch")
shell.CreateFileAndAdd("README.md", "hello world")
shell.Commit("initial commit")
shell.EmptyCommit("commit two")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Commits().
Focus().
Lines(
Contains("commit two").IsSelected(),
Contains("initial commit"),
).
NavigateToLine(Contains("initial commit")).
Press(keys.Worktrees.ViewWorktreeOptions).
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("Worktree")).
Select(MatchesRegexp(`Create worktree from .*`).DoesNotContain("detached")).
Confirm()
t.ExpectPopup().Prompt().
Title(Equals("New worktree path")).
Type("../linked-worktree").
Confirm()
t.ExpectPopup().Prompt().
Title(Equals("New branch name")).
Type("newbranch").
Confirm()
}).
Lines(
Contains("initial commit"),
)
// Confirm we're now in the branches view
t.Views().Branches().
IsFocused().
Lines(
Contains("newbranch").IsSelected(),
Contains("mybranch (worktree)"),
)
},
})

View file

@ -0,0 +1,88 @@
package worktree
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
// This is important because `git worktree list` will show a worktree being in a detached head state (which is true)
// when it's in the middle of a bisect, but it won't tell you about the branch it's on.
// Even so, if you attempt to check out that branch from another worktree git won't let you, so we need to
// keep track of the association ourselves.
// not bothering to test the linked worktree here because it's the same logic as the rebase test
var Bisect = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Verify that when you start a bisect in a linked worktree, Lazygit still associates the worktree with the branch",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.NewBranch("mybranch")
shell.CreateFileAndAdd("README.md", "hello world")
shell.Commit("initial commit")
shell.EmptyCommit("commit 2")
shell.EmptyCommit("commit 3")
shell.AddWorktree("mybranch", "../linked-worktree", "newbranch")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Branches().
Focus().
Lines(
Contains("mybranch").IsSelected(),
Contains("newbranch (worktree)"),
)
// start a bisect on the main worktree
t.Views().Commits().
Focus().
SelectedLine(Contains("commit 3")).
Press(keys.Commits.ViewBisectOptions).
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("Bisect")).
Select(MatchesRegexp(`Mark .* as bad`)).
Confirm()
t.Views().Information().Content(Contains("Bisecting"))
}).
NavigateToLine(Contains("initial commit")).
Press(keys.Commits.ViewBisectOptions).
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("Bisect")).
Select(MatchesRegexp(`Mark .* as good`)).
Confirm()
})
t.Views().Branches().
Focus().
// switch to linked worktree
NavigateToLine(Contains("newbranch")).
Press(keys.Universal.Select).
Tap(func() {
t.ExpectPopup().Confirmation().
Title(Equals("Switch to worktree")).
Content(Equals("This branch is checked out by worktree linked-worktree. Do you want to switch to that worktree?")).
Confirm()
t.Views().Information().Content(DoesNotContain("Bisecting"))
}).
Lines(
Contains("newbranch").IsSelected(),
Contains("mybranch (worktree)"),
)
// switch back to main worktree
t.Views().Branches().
Focus().
NavigateToLine(Contains("mybranch")).
Press(keys.Universal.Select).
Tap(func() {
t.ExpectPopup().Confirmation().
Title(Equals("Switch to worktree")).
Content(Equals("This branch is checked out by worktree repo. Do you want to switch to that worktree?")).
Confirm()
})
},
})

View file

@ -0,0 +1,40 @@
package worktree
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var CustomCommand = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Verify that custom commands work with worktrees by deleting a worktree via a custom command",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(cfg *config.AppConfig) {
cfg.UserConfig.CustomCommands = []config.CustomCommand{
{
Key: "d",
Context: "worktrees",
Command: "git worktree remove {{ .SelectedWorktree.Path | quote }}",
},
}
},
SetupRepo: func(shell *Shell) {
shell.NewBranch("mybranch")
shell.CreateFileAndAdd("README.md", "hello world")
shell.Commit("initial commit")
shell.AddWorktree("mybranch", "../linked-worktree", "newbranch")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Worktrees().
Focus().
Lines(
Contains("repo (main)"),
Contains("linked-worktree"),
).
NavigateToLine(Contains("linked-worktree")).
Press("d").
Lines(
Contains("repo (main)"),
)
},
})

View file

@ -10,8 +10,11 @@ import (
// Even so, if you attempt to check out that branch from another worktree git won't let you, so we need to // Even so, if you attempt to check out that branch from another worktree git won't let you, so we need to
// keep track of the association ourselves. // keep track of the association ourselves.
// We need different logic for associated the branch depending on whether it's a main worktree or
// linked worktree, so this test handles both.
var Rebase = NewIntegrationTest(NewIntegrationTestArgs{ var Rebase = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Verify that when you start a rebase in a worktree, Lazygit still associates the worktree with the branch", Description: "Verify that when you start a rebase in a linked or main worktree, Lazygit still associates the worktree with the branch",
ExtraCmdArgs: []string{}, ExtraCmdArgs: []string{},
Skip: false, Skip: false,
SetupConfig: func(config *config.AppConfig) {}, SetupConfig: func(config *config.AppConfig) {},
@ -27,10 +30,11 @@ var Rebase = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().Branches(). t.Views().Branches().
Focus(). Focus().
Lines( Lines(
Contains("mybranch"), Contains("mybranch").IsSelected(),
Contains("newbranch (worktree)"), Contains("newbranch (worktree)"),
) )
// start a rebase on the main worktree
t.Views().Commits(). t.Views().Commits().
Focus(). Focus().
NavigateToLine(Contains("commit 2")). NavigateToLine(Contains("commit 2")).
@ -54,8 +58,19 @@ var Rebase = NewIntegrationTest(NewIntegrationTestArgs{
Lines( Lines(
Contains("newbranch").IsSelected(), Contains("newbranch").IsSelected(),
Contains("mybranch (worktree)"), Contains("mybranch (worktree)"),
). )
// switch back to main worktree
// start a rebase on the linked worktree
t.Views().Commits().
Focus().
NavigateToLine(Contains("commit 2")).
Press(keys.Universal.Edit)
t.Views().Information().Content(Contains("Rebasing"))
// switch back to main worktree
t.Views().Branches().
Focus().
NavigateToLine(Contains("mybranch")). NavigateToLine(Contains("mybranch")).
Press(keys.Universal.Select). Press(keys.Universal.Select).
Tap(func() { Tap(func() {
@ -63,8 +78,12 @@ var Rebase = NewIntegrationTest(NewIntegrationTestArgs{
Title(Equals("Switch to worktree")). Title(Equals("Switch to worktree")).
Content(Equals("This branch is checked out by worktree repo. Do you want to switch to that worktree?")). Content(Equals("This branch is checked out by worktree repo. Do you want to switch to that worktree?")).
Confirm() Confirm()
}).
t.Views().Information().Content(Contains("Rebasing")) Lines(
}) Contains("(no branch").IsSelected(),
Contains("mybranch"),
// even though the linked worktree is rebasing, we still associate it with the branch
Contains("newbranch (worktree)"),
)
}, },
}) })