diff --git a/pkg/commands/git_commands/git_command_builder.go b/pkg/commands/git_commands/git_command_builder.go index 05d788b4c..78e0236a3 100644 --- a/pkg/commands/git_commands/git_command_builder.go +++ b/pkg/commands/git_commands/git_command_builder.go @@ -42,20 +42,30 @@ func (self *GitCommandBuilder) Config(value string) *GitCommandBuilder { return self } -func (self *GitCommandBuilder) RepoPath(value string) *GitCommandBuilder { +// the -C arg will make git do a `cd` to the directory before doing anything else +func (self *GitCommandBuilder) Dir(path string) *GitCommandBuilder { // repo path comes before the command - self.args = append([]string{"-C", value}, self.args...) + self.args = append([]string{"-C", path}, self.args...) return self } -func (self *GitCommandBuilder) WorktreePath(path string) *GitCommandBuilder { - // worktree path comes before the command +// Note, you may prefer to use the Dir method instead of this one +func (self *GitCommandBuilder) Worktree(path string) *GitCommandBuilder { + // worktree arg comes before the command self.args = append([]string{"--work-tree", path}, self.args...) return self } +// Note, you may prefer to use the Dir method instead of this one +func (self *GitCommandBuilder) GitDir(path string) *GitCommandBuilder { + // git dir arg comes before the command + self.args = append([]string{"--git-dir", path}, self.args...) + + return self +} + func (self *GitCommandBuilder) ToArgv() []string { return append([]string{"git"}, self.args...) } diff --git a/pkg/commands/git_commands/git_command_builder_test.go b/pkg/commands/git_commands/git_command_builder_test.go index becee087c..69d41854c 100644 --- a/pkg/commands/git_commands/git_command_builder_test.go +++ b/pkg/commands/git_commands/git_command_builder_test.go @@ -45,7 +45,7 @@ func TestGitCommandBuilder(t *testing.T) { expected: []string{"git", "-c", "user.email=bar", "-c", "user.name=foo", "push"}, }, { - input: NewGitCmd("push").RepoPath("a/b/c").ToArgv(), + input: NewGitCmd("push").Dir("a/b/c").ToArgv(), expected: []string{"git", "-C", "a/b/c", "push"}, }, } diff --git a/pkg/commands/git_commands/submodule.go b/pkg/commands/git_commands/submodule.go index ca3a23ef5..3d8602b9a 100644 --- a/pkg/commands/git_commands/submodule.go +++ b/pkg/commands/git_commands/submodule.go @@ -82,7 +82,7 @@ func (self *SubmoduleCommands) Stash(submodule *models.SubmoduleConfig) error { } cmdArgs := NewGitCmd("stash"). - RepoPath(submodule.Path). + Dir(submodule.Path). Arg("--include-untracked"). ToArgv() diff --git a/pkg/commands/git_commands/worktree.go b/pkg/commands/git_commands/worktree.go index 65ab152a7..e35b3a715 100644 --- a/pkg/commands/git_commands/worktree.go +++ b/pkg/commands/git_commands/worktree.go @@ -54,9 +54,9 @@ func (self *WorktreeCommands) Delete(worktreePath string, force bool) error { } func (self *WorktreeCommands) Detach(worktreePath string) error { - cmdArgs := NewGitCmd("checkout").Arg("--detach").ToArgv() + cmdArgs := NewGitCmd("checkout").Arg("--detach").GitDir(filepath.Join(worktreePath, ".git")).ToArgv() - return self.cmd.New(cmdArgs).SetWd(worktreePath).Run() + return self.cmd.New(cmdArgs).Run() } func (self *WorktreeCommands) IsCurrentWorktree(path string) bool { diff --git a/pkg/integration/components/shell.go b/pkg/integration/components/shell.go index 809cb1d5b..bfa82c45e 100644 --- a/pkg/integration/components/shell.go +++ b/pkg/integration/components/shell.go @@ -261,6 +261,16 @@ func (self *Shell) AddWorktree(base string, path string, newBranchName string) * }) } +func (self *Shell) AddFileInWorktree(worktreePath string) *Shell { + self.CreateFile(filepath.Join(worktreePath, "content"), "content") + + self.RunCommand([]string{ + "git", "-C", worktreePath, "add", "content", + }) + + return self +} + 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) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index d2e2848e7..e22ed7334 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -222,6 +222,9 @@ var tests = []*components.IntegrationTest{ undo.UndoDrop, worktree.AddFromBranch, worktree.Crud, + worktree.DetachWorktreeFromBranch, + worktree.ForceRemoveWorktree, worktree.Rebase, + worktree.RemoveWorktreeFromBranch, worktree.WorktreeInRepo, } diff --git a/pkg/integration/tests/worktree/detach_worktree_from_branch.go b/pkg/integration/tests/worktree/detach_worktree_from_branch.go new file mode 100644 index 000000000..4dd3c23fd --- /dev/null +++ b/pkg/integration/tests/worktree/detach_worktree_from_branch.go @@ -0,0 +1,48 @@ +package worktree + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var DetachWorktreeFromBranch = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Detach a worktree from 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") + 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)"), + ). + NavigateToLine(Contains("newbranch")). + Press(keys.Universal.Remove). + Tap(func() { + t.ExpectPopup().Menu(). + Title(Equals("Branch newbranch is checked out by worktree linked-worktree")). + Select(Equals("Detach worktree")). + Confirm() + }). + Lines( + Contains("mybranch"), + Contains("newbranch").DoesNotContain("(worktree").IsSelected(), + ) + + t.Views().Worktrees(). + Focus(). + Lines( + Contains("repo (main)").IsSelected(), + Contains("linked-worktree"), + ) + }, +}) diff --git a/pkg/integration/tests/worktree/force_remove_worktree.go b/pkg/integration/tests/worktree/force_remove_worktree.go new file mode 100644 index 000000000..23d0b9a88 --- /dev/null +++ b/pkg/integration/tests/worktree/force_remove_worktree.go @@ -0,0 +1,46 @@ +package worktree + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var ForceRemoveWorktree = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Force remove a dirty worktree", + 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") + shell.AddFileInWorktree("../linked-worktree") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Worktrees(). + Focus(). + Lines( + Contains("repo (main)").IsSelected(), + Contains("linked-worktree"), + ). + NavigateToLine(Contains("linked-worktree")). + Press(keys.Universal.Remove). + Tap(func() { + t.ExpectPopup().Confirmation(). + Title(Equals("Remove worktree")). + Content(Equals("Are you sure you want to remove worktree 'linked-worktree'?")). + Confirm() + + t.ExpectPopup().Confirmation(). + Title(Equals("Remove worktree")). + Content(Equals("'linked-worktree' contains modified or untracked files (to be honest, it could contain both). Are you sure you want to remove it?")). + Confirm() + }). + Lines( + Contains("repo (main)").IsSelected(), + ) + }, +}) diff --git a/pkg/integration/tests/worktree/remove_worktree_from_branch.go b/pkg/integration/tests/worktree/remove_worktree_from_branch.go new file mode 100644 index 000000000..06c729edb --- /dev/null +++ b/pkg/integration/tests/worktree/remove_worktree_from_branch.go @@ -0,0 +1,58 @@ +package worktree + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var RemoveWorktreeFromBranch = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Remove a worktree from 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") + shell.EmptyCommit("commit 2") + shell.EmptyCommit("commit 3") + shell.AddWorktree("mybranch", "../linked-worktree", "newbranch") + shell.AddFileInWorktree("../linked-worktree") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Branches(). + Focus(). + Lines( + Contains("mybranch").IsSelected(), + Contains("newbranch (worktree)"), + ). + NavigateToLine(Contains("newbranch")). + Press(keys.Universal.Remove). + Tap(func() { + t.ExpectPopup().Menu(). + Title(Equals("Branch newbranch is checked out by worktree linked-worktree")). + Select(Equals("Remove worktree")). + Confirm() + + t.ExpectPopup().Confirmation(). + Title(Equals("Remove worktree")). + Content(Equals("Are you sure you want to remove worktree 'linked-worktree'?")). + Confirm() + + t.ExpectPopup().Confirmation(). + Title(Equals("Remove worktree")). + Content(Equals("'linked-worktree' contains modified or untracked files (to be honest, it could contain both). Are you sure you want to remove it?")). + Confirm() + }). + Lines( + Contains("mybranch"), + Contains("newbranch").DoesNotContain("(worktree").IsSelected(), + ) + + t.Views().Worktrees(). + Focus(). + Lines( + Contains("repo (main)").IsSelected(), + ) + }, +})