1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-03-31 22:22:14 +02:00

Add worktree integration tests

This commit is contained in:
Jesse Duffield 2023-07-17 22:03:51 +10:00
parent 18a508b29c
commit 277142fc4b
12 changed files with 385 additions and 18 deletions

View File

@ -108,6 +108,8 @@ func CheckedOutByOtherWorktree(branch *models.Branch, worktrees []*models.Worktr
return !IsCurrentWorktree(worktree.Path)
}
// If in a non-bare repo, this returns the path of the main worktree
// TODO: see if this works with a bare repo.
func GetCurrentRepoPath() string {
pwd, err := os.Getwd()
if err != nil {
@ -128,7 +130,7 @@ func GetCurrentRepoPath() string {
}
// either in a submodule, a worktree, or a bare repo
worktreeGitPath, ok := WorktreeGitPath(pwd)
worktreeGitPath, ok := LinkedWorktreeGitPath(pwd)
if !ok {
// fallback
return currentPath()

View File

@ -28,6 +28,8 @@ func NewWorktreeLoader(
}
func (self *WorktreeLoader) GetWorktrees() ([]*models.Worktree, error) {
currentRepoPath := GetCurrentRepoPath()
cmdArgs := NewGitCmd("worktree").Arg("list", "--porcelain").ToArgv()
worktreesOutput, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
if err != nil {
@ -46,8 +48,9 @@ func (self *WorktreeLoader) GetWorktrees() ([]*models.Worktree, error) {
}
if strings.HasPrefix(splitLine, "worktree ") {
path := strings.SplitN(splitLine, " ", 2)[1]
current = &models.Worktree{
IsMain: len(worktrees) == 0,
IsMain: path == currentRepoPath,
Path: path,
}
} else if strings.HasPrefix(splitLine, "branch ") {
@ -88,7 +91,7 @@ func (self *WorktreeLoader) GetWorktrees() ([]*models.Worktree, error) {
continue
}
rebaseBranch, ok := rebaseBranch(worktree.Path)
rebaseBranch, ok := rebaseBranch(worktree)
if ok {
worktree.Branch = rebaseBranch
}
@ -97,11 +100,17 @@ func (self *WorktreeLoader) GetWorktrees() ([]*models.Worktree, error) {
return worktrees, nil
}
func rebaseBranch(worktreePath string) (string, bool) {
// need to find the actual path of the worktree in the .git dir
gitPath, ok := WorktreeGitPath(worktreePath)
if !ok {
return "", false
func rebaseBranch(worktree *models.Worktree) (string, bool) {
var gitPath string
if worktree.Main() {
gitPath = filepath.Join(worktree.Path, ".git")
} else {
// need to find the path of the linked worktree in the .git dir
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`
@ -117,7 +126,7 @@ func rebaseBranch(worktreePath string) (string, bool) {
return shortHeadName, true
}
func WorktreeGitPath(worktreePath string) (string, bool) {
func LinkedWorktreeGitPath(worktreePath string) (string, bool) {
// first we get the path of the worktree, then we look at the contents of the `.git` file in that path
// then we look for the line that says `gitdir: /path/to/.git/worktrees/<worktree-name>`
// then we return that path

View File

@ -23,6 +23,7 @@ func NewWorktreesContext(c *ContextCommon) *WorktreesContext {
getDisplayStrings := func(startIdx int, length int) [][]string {
return presentation.GetWorktreeDisplayStrings(
c.Tr,
viewModel.GetFilteredList(),
c.Git().Worktree.IsCurrentWorktree,
c.Git().Worktree.IsWorktreePathMissing,

View File

@ -94,7 +94,7 @@ func (self *WorktreeHelper) NewWorktree() error {
HandleConfirm: func(base string) error {
// we assume that the base can be checked out
canCheckoutBase := true
return self.NewWorktreeCheckout(base, canCheckoutBase, detached)
return self.NewWorktreeCheckout(base, canCheckoutBase, detached, context.WORKTREES_CONTEXT_KEY)
},
})
}
@ -120,7 +120,7 @@ func (self *WorktreeHelper) NewWorktree() error {
})
}
func (self *WorktreeHelper) NewWorktreeCheckout(base string, canCheckoutBase bool, detached bool) error {
func (self *WorktreeHelper) NewWorktreeCheckout(base string, canCheckoutBase bool, detached bool, contextKey types.ContextKey) error {
opts := git_commands.NewWorktreeOpts{
Base: base,
Detach: detached,
@ -132,7 +132,7 @@ func (self *WorktreeHelper) NewWorktreeCheckout(base string, canCheckoutBase boo
if err := self.c.Git().Worktree.New(opts); err != nil {
return err
}
return self.Switch(opts.Path, context.LOCAL_BRANCHES_CONTEXT_KEY)
return self.Switch(opts.Path, contextKey)
})
}
@ -251,13 +251,13 @@ func (self *WorktreeHelper) ViewBranchWorktreeOptions(branchName string, canChec
{
LabelColumns: []string{utils.ResolvePlaceholderString(self.c.Tr.CreateWorktreeFrom, placeholders)},
OnPress: func() error {
return self.NewWorktreeCheckout(branchName, canCheckoutBase, false)
return self.NewWorktreeCheckout(branchName, canCheckoutBase, false, context.LOCAL_BRANCHES_CONTEXT_KEY)
},
},
{
LabelColumns: []string{utils.ResolvePlaceholderString(self.c.Tr.CreateWorktreeFromDetached, placeholders)},
OnPress: func() error {
return self.NewWorktreeCheckout(branchName, canCheckoutBase, true)
return self.NewWorktreeCheckout(branchName, canCheckoutBase, true, context.LOCAL_BRANCHES_CONTEXT_KEY)
},
},
},

View File

@ -4,20 +4,22 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/presentation/icons"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/samber/lo"
)
func GetWorktreeDisplayStrings(worktrees []*models.Worktree, isCurrent func(string) bool, isMissing func(string) bool) [][]string {
func GetWorktreeDisplayStrings(tr *i18n.TranslationSet, worktrees []*models.Worktree, isCurrent func(string) bool, isMissing func(string) bool) [][]string {
return lo.Map(worktrees, func(worktree *models.Worktree, _ int) []string {
return GetWorktreeDisplayString(
tr,
isCurrent(worktree.Path),
isMissing(worktree.Path),
worktree)
})
}
func GetWorktreeDisplayString(isCurrent bool, isPathMissing bool, worktree *models.Worktree) []string {
func GetWorktreeDisplayString(tr *i18n.TranslationSet, isCurrent bool, isPathMissing bool, worktree *models.Worktree) []string {
textStyle := theme.DefaultTextColor
current := ""
@ -41,8 +43,10 @@ func GetWorktreeDisplayString(isCurrent bool, isPathMissing bool, worktree *mode
name := worktree.Name()
if worktree.Main() {
// TODO: i18n
name += " (main worktree)"
name += " " + tr.MainWorktree
}
if isPathMissing && !icons.IsIconEnabled() {
name += " " + tr.MissingWorktree
}
res = append(res, textStyle.Sprint(name))
return res

View File

@ -254,6 +254,13 @@ func (self *Shell) Init() *Shell {
return self
}
func (self *Shell) AddWorktree(base string, path string, newBranchName string) *Shell {
return self.RunCommand([]string{
"git", "worktree", "add", "-b",
newBranchName, path, base,
})
}
func (self *Shell) MakeExecutable(path string) *Shell {
// 0755 sets the executable permission for owner, and read/execute permissions for group and others
err := os.Chmod(filepath.Join(self.dir, path), 0o755)

View File

@ -129,6 +129,10 @@ func (self *Views) Files() *ViewDriver {
return self.regularView("files")
}
func (self *Views) Worktrees() *ViewDriver {
return self.regularView("worktrees")
}
func (self *Views) Status() *ViewDriver {
return self.regularView("status")
}

View File

@ -26,6 +26,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/integration/tests/tag"
"github.com/jesseduffield/lazygit/pkg/integration/tests/ui"
"github.com/jesseduffield/lazygit/pkg/integration/tests/undo"
"github.com/jesseduffield/lazygit/pkg/integration/tests/worktree"
)
var tests = []*components.IntegrationTest{
@ -219,4 +220,8 @@ var tests = []*components.IntegrationTest{
ui.SwitchTabFromMenu,
undo.UndoCheckoutAndDrop,
undo.UndoDrop,
worktree.AddFromBranch,
worktree.Crud,
worktree.Rebase,
worktree.WorktreeInRepo,
}

View File

@ -0,0 +1,60 @@
package worktree
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var AddFromBranch = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Add a worktree via the branches view, then switch back to the main 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`).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()
}).
// confirm we're still focused on the branches view
IsFocused().
Lines(
Contains("newbranch").IsSelected(),
Contains("mybranch (worktree)"),
).
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()
}).
Lines(
Contains("mybranch").IsSelected(),
Contains("newbranch (worktree)"),
)
},
})

View File

@ -0,0 +1,120 @@
package worktree
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var Crud = NewIntegrationTest(NewIntegrationTestArgs{
Description: "From the worktrees view, add a work tree, switch to it, switch back, and remove it",
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().
Lines(
Contains("mybranch"),
)
t.Views().Status().
Lines(
Contains("repo → mybranch"),
)
t.Views().Worktrees().
Focus().
Lines(
Contains("repo (main)"),
).
Press(keys.Universal.New).
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("Worktree")).
Select(Contains(`Create worktree from ref`).DoesNotContain(("detached"))).
Confirm()
t.ExpectPopup().Prompt().
Title(Equals("New worktree base ref")).
InitialText(Equals("mybranch")).
Confirm()
t.ExpectPopup().Prompt().
Title(Equals("New worktree path")).
Type("../linked-worktree").
Confirm()
t.ExpectPopup().Prompt().
Title(Equals("New branch name (leave blank to checkout mybranch)")).
Type("newbranch").
Confirm()
}).
Lines(
Contains("linked-worktree").IsSelected(),
Contains("repo (main)"),
).
// confirm we're still in the same view
IsFocused()
// status panel includes the worktree if it's a linked worktree
t.Views().Status().
Lines(
Contains("repo(linked-worktree) → newbranch"),
)
t.Views().Branches().
Lines(
Contains("newbranch"),
Contains("mybranch"),
)
t.Views().Worktrees().
// confirm we can't remove the current worktree
Press(keys.Universal.Remove).
Tap(func() {
t.ExpectPopup().Alert().
Title(Equals("Error")).
Content(Equals("You cannot remove the current worktree!")).
Confirm()
}).
// confirm we cannot remove the main worktree
NavigateToLine(Contains("repo (main)")).
Press(keys.Universal.Remove).
Tap(func() {
t.ExpectPopup().Alert().
Title(Equals("Error")).
Content(Equals("You cannot remove the main worktree!")).
Confirm()
}).
// switch back to main worktree
Press(keys.Universal.Select).
Lines(
Contains("repo (main)").IsSelected(),
Contains("linked-worktree"),
)
t.Views().Branches().
Lines(
Contains("mybranch"),
Contains("newbranch"),
)
t.Views().Worktrees().
// remove linked worktree
NavigateToLine(Contains("linked-worktree")).
Press(keys.Universal.Remove).
Tap(func() {
t.ExpectPopup().Confirmation().
Title(Equals("Remove worktree")).
Content(Contains("Are you sure you want to remove worktree 'linked-worktree'?")).
Confirm()
}).
Lines(
Contains("repo (main)").IsSelected(),
)
},
})

View File

@ -0,0 +1,70 @@
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 rebase, 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.
var Rebase = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Verify that when you start a rebase in a 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"),
Contains("newbranch (worktree)"),
)
t.Views().Commits().
Focus().
NavigateToLine(Contains("commit 2")).
Press(keys.Universal.Edit)
t.Views().Information().Content(Contains("Rebasing"))
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("Rebasing"))
}).
Lines(
Contains("newbranch").IsSelected(),
Contains("mybranch (worktree)"),
).
// switch back to main worktree
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()
t.Views().Information().Content(Contains("Rebasing"))
})
},
})

View File

@ -0,0 +1,85 @@
package worktree
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var WorktreeInRepo = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Add a worktree inside the repo, then remove the directory and confirm the worktree is removed",
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().
Lines(
Contains("mybranch"),
)
t.Views().Worktrees().
Focus().
Lines(
Contains("repo (main)"),
).
Press(keys.Universal.New).
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("Worktree")).
Select(Contains(`Create worktree from ref`).DoesNotContain(("detached"))).
Confirm()
t.ExpectPopup().Prompt().
Title(Equals("New worktree base ref")).
InitialText(Equals("mybranch")).
Confirm()
t.ExpectPopup().Prompt().
Title(Equals("New worktree path")).
Type("linked-worktree").
Confirm()
t.ExpectPopup().Prompt().
Title(Equals("New branch name (leave blank to checkout mybranch)")).
Type("newbranch").
Confirm()
}).
Lines(
Contains("linked-worktree").IsSelected(),
Contains("repo (main)"),
).
// switch back to main worktree
NavigateToLine(Contains("repo (main)")).
Press(keys.Universal.Select).
Lines(
Contains("repo (main)").IsSelected(),
Contains("linked-worktree"),
)
t.Views().Files().
Focus().
Lines(
Contains("linked-worktree"),
).
Press(keys.Universal.Remove).
Tap(func() {
t.ExpectPopup().Menu().
Title(Equals("linked-worktree")).
Select(Contains("Discard all changes")).
Confirm()
}).
IsEmpty()
// confirm worktree appears as missing
t.Views().Worktrees().
Focus().
Lines(
Contains("repo (main)").IsSelected(),
Contains("linked-worktree (missing)"),
)
},
})