1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-06-02 23:27:32 +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) 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 { func GetCurrentRepoPath() string {
pwd, err := os.Getwd() pwd, err := os.Getwd()
if err != nil { if err != nil {
@ -128,7 +130,7 @@ func GetCurrentRepoPath() string {
} }
// either in a submodule, a worktree, or a bare repo // either in a submodule, a worktree, or a bare repo
worktreeGitPath, ok := WorktreeGitPath(pwd) worktreeGitPath, ok := LinkedWorktreeGitPath(pwd)
if !ok { if !ok {
// fallback // fallback
return currentPath() return currentPath()

View File

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

View File

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

View File

@ -94,7 +94,7 @@ func (self *WorktreeHelper) NewWorktree() error {
HandleConfirm: func(base string) error { HandleConfirm: func(base string) error {
// we assume that the base can be checked out // we assume that the base can be checked out
canCheckoutBase := true 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{ opts := git_commands.NewWorktreeOpts{
Base: base, Base: base,
Detach: detached, Detach: detached,
@ -132,7 +132,7 @@ func (self *WorktreeHelper) NewWorktreeCheckout(base string, canCheckoutBase boo
if err := self.c.Git().Worktree.New(opts); err != nil { if err := self.c.Git().Worktree.New(opts); err != nil {
return err 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)}, LabelColumns: []string{utils.ResolvePlaceholderString(self.c.Tr.CreateWorktreeFrom, placeholders)},
OnPress: func() error { 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)}, LabelColumns: []string{utils.ResolvePlaceholderString(self.c.Tr.CreateWorktreeFromDetached, placeholders)},
OnPress: func() error { 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/commands/models"
"github.com/jesseduffield/lazygit/pkg/gui/presentation/icons" "github.com/jesseduffield/lazygit/pkg/gui/presentation/icons"
"github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/theme" "github.com/jesseduffield/lazygit/pkg/theme"
"github.com/samber/lo" "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 lo.Map(worktrees, func(worktree *models.Worktree, _ int) []string {
return GetWorktreeDisplayString( return GetWorktreeDisplayString(
tr,
isCurrent(worktree.Path), isCurrent(worktree.Path),
isMissing(worktree.Path), isMissing(worktree.Path),
worktree) 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 textStyle := theme.DefaultTextColor
current := "" current := ""
@ -41,8 +43,10 @@ func GetWorktreeDisplayString(isCurrent bool, isPathMissing bool, worktree *mode
name := worktree.Name() name := worktree.Name()
if worktree.Main() { if worktree.Main() {
// TODO: i18n name += " " + tr.MainWorktree
name += " (main worktree)" }
if isPathMissing && !icons.IsIconEnabled() {
name += " " + tr.MissingWorktree
} }
res = append(res, textStyle.Sprint(name)) res = append(res, textStyle.Sprint(name))
return res return res

View File

@ -254,6 +254,13 @@ func (self *Shell) Init() *Shell {
return self 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 { func (self *Shell) MakeExecutable(path string) *Shell {
// 0755 sets the executable permission for owner, and read/execute permissions for group and others // 0755 sets the executable permission for owner, and read/execute permissions for group and others
err := os.Chmod(filepath.Join(self.dir, path), 0o755) err := os.Chmod(filepath.Join(self.dir, path), 0o755)

View File

@ -129,6 +129,10 @@ func (self *Views) Files() *ViewDriver {
return self.regularView("files") return self.regularView("files")
} }
func (self *Views) Worktrees() *ViewDriver {
return self.regularView("worktrees")
}
func (self *Views) Status() *ViewDriver { func (self *Views) Status() *ViewDriver {
return self.regularView("status") 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/tag"
"github.com/jesseduffield/lazygit/pkg/integration/tests/ui" "github.com/jesseduffield/lazygit/pkg/integration/tests/ui"
"github.com/jesseduffield/lazygit/pkg/integration/tests/undo" "github.com/jesseduffield/lazygit/pkg/integration/tests/undo"
"github.com/jesseduffield/lazygit/pkg/integration/tests/worktree"
) )
var tests = []*components.IntegrationTest{ var tests = []*components.IntegrationTest{
@ -219,4 +220,8 @@ var tests = []*components.IntegrationTest{
ui.SwitchTabFromMenu, ui.SwitchTabFromMenu,
undo.UndoCheckoutAndDrop, undo.UndoCheckoutAndDrop,
undo.UndoDrop, 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)"),
)
},
})