diff --git a/pkg/commands/git_commands/diff.go b/pkg/commands/git_commands/diff.go index ef70d0422..910db7600 100644 --- a/pkg/commands/git_commands/diff.go +++ b/pkg/commands/git_commands/diff.go @@ -186,15 +186,26 @@ func (self *DiffCommands) CustomPatchDiffCmdObj(dir string, mode DiffMode) *osco // want to restrict the diff to specific paths, pass them in additionalArgs // after the `--`. func (self *DiffCommands) GetDiff(staged bool, additionalArgs ...string) (string, error) { + return self.getDiffCmdObj(self.repoPaths.worktreePath, staged, additionalArgs).RunWithOutput() +} + +// GetDiffInOtherWorktree is GetDiff without --staged, run in the worktree at +// worktreePath. That has to be a worktree of our repo other than the current one. A +// diff against the working copy is then a diff against that worktree's files. +func (self *DiffCommands) GetDiffInOtherWorktree(worktreePath string, additionalArgs ...string) (string, error) { + return ForOtherRepo(self.getDiffCmdObj(worktreePath, false, additionalArgs)).RunWithOutput() +} + +func (self *DiffCommands) getDiffCmdObj(worktreePath string, staged bool, additionalArgs []string) *oscommands.CmdObj { return self.cmd.New( NewGitCmd("diff"). ParseableDiffPrefixes(). Arg("--no-ext-diff", "--no-color"). ArgIf(staged, "--staged"). - Dir(self.repoPaths.worktreePath). + Dir(worktreePath). Arg(additionalArgs...). ToArgv(), - ).DontLog().RunWithOutput() + ).DontLog() } type DiffToolCmdOptions struct { diff --git a/pkg/gui/context/commit_files_context.go b/pkg/gui/context/commit_files_context.go index 7cce0dd69..71719c631 100644 --- a/pkg/gui/context/commit_files_context.go +++ b/pkg/gui/context/commit_files_context.go @@ -24,6 +24,7 @@ var ( _ types.IFilterableContext = (*CommitFilesContext)(nil) _ types.DiffMainViewContext = (*CommitFilesContext)(nil) _ types.PullRequestDiffContext = (*CommitFilesContext)(nil) + _ types.BranchDiffContext = (*CommitFilesContext)(nil) ) func (self *CommitFilesContext) GetDiffMainViewType() types.DiffMainViewType { @@ -40,6 +41,15 @@ func (self *CommitFilesContext) PullRequestDiff() types.PullRequestDiff { return types.PullRequestDiff{} } +// BranchOfDiff asks the panel this one was entered from, since the files listed here +// are those of the commits selected there. +func (self *CommitFilesContext) BranchOfDiff() *models.Branch { + if parent, ok := self.GetParentContext().(types.BranchDiffContext); ok { + return parent.BranchOfDiff() + } + return nil +} + func NewCommitFilesContext(c *ContextCommon) *CommitFilesContext { viewModel := filetree.NewCommitFileTreeViewModel( func() []*models.CommitFile { return c.Model().CommitFiles }, diff --git a/pkg/gui/context/sub_commits_context.go b/pkg/gui/context/sub_commits_context.go index c5d70d0fe..e99b5d281 100644 --- a/pkg/gui/context/sub_commits_context.go +++ b/pkg/gui/context/sub_commits_context.go @@ -26,6 +26,7 @@ var ( _ types.ISearchableContext = (*SubCommitsContext)(nil) _ types.DiffMainViewContext = (*SubCommitsContext)(nil) _ types.PullRequestDiffContext = (*SubCommitsContext)(nil) + _ types.BranchDiffContext = (*SubCommitsContext)(nil) ) func (self *SubCommitsContext) GetDiffMainViewType() types.DiffMainViewType { @@ -49,6 +50,13 @@ func (self *SubCommitsContext) PullRequestDiff() types.PullRequestDiff { self.GetCommits(), startIdx, endIdx, branch.Name, self.c.Model().Branches, self.c.Model().PullRequestsMap) } +// BranchOfDiff is nil if the panel was entered from a tag, a remote branch or the +// reflog instead of a local branch. +func (self *SubCommitsContext) BranchOfDiff() *models.Branch { + branch, _ := self.GetRef().(*models.Branch) + return branch +} + func NewSubCommitsContext( c *ContextCommon, ) *SubCommitsContext { diff --git a/pkg/gui/controllers.go b/pkg/gui/controllers.go index 67ff1c810..39d03ef9d 100644 --- a/pkg/gui/controllers.go +++ b/pkg/gui/controllers.go @@ -65,7 +65,8 @@ func (gui *Gui) resetHelpersAndControllers() { searchHelper, ) diffLineHelper := helpers.NewDiffLineHelper(helperCommon) - diffHelper := helpers.NewDiffHelper(helperCommon, diffLineHelper) + filesHelper := helpers.NewFilesHelper(helperCommon) + diffHelper := helpers.NewDiffHelper(helperCommon, diffLineHelper, filesHelper) cherryPickHelper := helpers.NewCherryPickHelper( helperCommon, rebaseHelper, @@ -92,7 +93,7 @@ func (gui *Gui) resetHelpersAndControllers() { CustomPatch: customPatchHelper, Bisect: bisectHelper, Suggestions: suggestionsHelper, - Files: helpers.NewFilesHelper(helperCommon), + Files: filesHelper, WorkingTree: helpers.NewWorkingTreeHelper(helperCommon, refsHelper, commitsHelper, gpgHelper, rebaseHelper), Tags: helpers.NewTagsHelper(helperCommon, commitsHelper, gpgHelper), BranchesHelper: helpers.NewBranchesHelper(helperCommon, worktreeHelper), diff --git a/pkg/gui/controllers/commits_files_controller.go b/pkg/gui/controllers/commits_files_controller.go index c6b0fb293..e945ef8ca 100644 --- a/pkg/gui/controllers/commits_files_controller.go +++ b/pkg/gui/controllers/commits_files_controller.go @@ -3,7 +3,6 @@ package controllers import ( "errors" "fmt" - "path/filepath" "strings" "github.com/jesseduffield/lazygit/pkg/commands/git_commands" @@ -267,7 +266,7 @@ func (self *CommitFilesController) openCopyMenu() error { copyAbsolutePathItem := &types.MenuItem{ Label: self.c.Tr.CopyAbsoluteFilePath, OnPress: func() error { - absPath, err := filepath.Abs(node.GetPath()) + absPath, err := self.c.Helpers().Diff.AbsolutePathOfFileOfDiff(node.GetPath()) if err != nil { return err } @@ -428,11 +427,11 @@ func (self *CommitFilesController) canDiscardFileChanges(nodes []*filetree.Commi } func (self *CommitFilesController) open(node *filetree.CommitFileNode) error { - return self.c.Helpers().Files.OpenFile(node.GetPath()) + return self.c.Helpers().Diff.OpenFileOfDiff(node.GetPath()) } func (self *CommitFilesController) edit(nodes []*filetree.CommitFileNode) error { - return self.c.Helpers().Files.EditFiles(lo.FilterMap(nodes, + return self.c.Helpers().Diff.EditFilesOfDiff(lo.FilterMap(nodes, func(node *filetree.CommitFileNode, _ int) (string, bool) { return node.GetPath(), node.IsFile() })) diff --git a/pkg/gui/controllers/helpers/diff_helper.go b/pkg/gui/controllers/helpers/diff_helper.go index a6ef3dc28..d855282ea 100644 --- a/pkg/gui/controllers/helpers/diff_helper.go +++ b/pkg/gui/controllers/helpers/diff_helper.go @@ -1,6 +1,8 @@ package helpers import ( + "errors" + "path/filepath" "strings" "github.com/jesseduffield/lazygit/pkg/commands/git_commands" @@ -10,18 +12,21 @@ import ( "github.com/jesseduffield/lazygit/pkg/gui/modes/diffing" "github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/jesseduffield/lazygit/pkg/utils" "github.com/samber/lo" ) type DiffHelper struct { c *HelperCommon diffLineHelper *DiffLineHelper + filesHelper *FilesHelper } -func NewDiffHelper(c *HelperCommon, diffLineHelper *DiffLineHelper) *DiffHelper { +func NewDiffHelper(c *HelperCommon, diffLineHelper *DiffLineHelper, filesHelper *FilesHelper) *DiffHelper { return &DiffHelper{ c: c, diffLineHelper: diffLineHelper, + filesHelper: filesHelper, } } @@ -224,13 +229,144 @@ func (self *DiffHelper) OpenDiffToolForRef(selectedRef models.Ref) error { return err } +// EditFilesOfDiff opens files of the diff that the current side panel shows in the +// editor. They are opened from the worktree of the diff (see otherWorktreeOfDiff). +// Files that don't exist there are left out, and if none of them exist, it returns an +// error (see existingPathsInWorktreeOfDiff). +func (self *DiffHelper) EditFilesOfDiff(paths []string) error { + existingPaths, err := self.existingPathsInWorktreeOfDiff(paths) + if err != nil { + return err + } + return self.filesHelper.EditFiles(existingPaths) +} + +// EditFileOfDiffAtLine opens a file of the diff shown in the given view in the editor, +// at a line of that diff. The line is carried forward to where it is in the file now +// (see AdjustLineNumber). Like EditFilesOfDiff, it opens the file from the worktree of +// the diff, and returns an error if the file doesn't exist there. +func (self *DiffHelper) EditFileOfDiffAtLine(path string, lineNumber int, viewName string) error { + path, err := self.existingPathInWorktreeOfDiff(path) + if err != nil { + return err + } + lineNumber = self.AdjustLineNumber(path, lineNumber, viewName) + return self.filesHelper.EditFileAtLine(path, lineNumber) +} + +// OpenFileOfDiff opens a file of the diff that the current side panel shows with the +// default application for it. Like EditFilesOfDiff, it opens the file from the +// worktree of the diff, and returns an error if the file doesn't exist there. +func (self *DiffHelper) OpenFileOfDiff(path string) error { + path, err := self.existingPathInWorktreeOfDiff(path) + if err != nil { + return err + } + return self.filesHelper.OpenFile(path) +} + +// existingPathsInWorktreeOfDiff returns the absolute paths of those of the given files +// of the diff that exist in the worktree of the diff. A file of a diff may be missing +// there, for example if a later commit deleted it, or if the worktree has a branch +// checked out that doesn't have it. An editor would create such a file as a new, empty +// one, so it is left out. +// +// If none of the files exist, it returns an error. If only some of them are missing, it +// leaves them out silently. The user would see such an error only after returning from +// the editor. +func (self *DiffHelper) existingPathsInWorktreeOfDiff(paths []string) ([]string, error) { + absPaths := make([]string, 0, len(paths)) + for _, path := range paths { + absPath, err := self.AbsolutePathOfFileOfDiff(path) + if err != nil { + return nil, err + } + absPaths = append(absPaths, absPath) + } + + existingPaths := lo.Filter(absPaths, func(path string, _ int) bool { + exists, err := self.c.OS().FileExists(path) + // If we can't tell, the editor reports the problem + return exists || err != nil + }) + if len(existingPaths) > 0 { + return existingPaths, nil + } + + if len(absPaths) == 1 { + return nil, errors.New(utils.ResolvePlaceholderString( + self.c.Tr.FileOfDiffDoesNotExist, map[string]string{"path": absPaths[0]})) + } + worktreePath, ok := self.otherWorktreeOfDiff() + if !ok { + worktreePath = self.c.Git().RepoPaths.WorktreePath() + } + return nil, errors.New(utils.ResolvePlaceholderString( + self.c.Tr.NoFileOfDiffExists, map[string]string{"worktreePath": worktreePath})) +} + +// existingPathInWorktreeOfDiff is existingPathsInWorktreeOfDiff for a single file. +func (self *DiffHelper) existingPathInWorktreeOfDiff(path string) (string, error) { + existingPaths, err := self.existingPathsInWorktreeOfDiff([]string{path}) + if err != nil { + return "", err + } + return existingPaths[0], nil +} + +// AbsolutePathOfFileOfDiff returns the absolute path of a file of the diff that the +// current side panel shows, in the worktree that EditFilesOfDiff opens it from. +func (self *DiffHelper) AbsolutePathOfFileOfDiff(path string) (string, error) { + return filepath.Abs(self.pathInWorktreeOfDiff(path)) +} + +// otherWorktreeOfDiff returns the path of the worktree to open files of the diff in +// the current side panel from, if that isn't the current worktree. If the diff is of a +// commit of a branch, and another worktree has that branch checked out, that is the +// other worktree. It has the files as the branch has them; the current worktree may +// have them in a different state, or not at all. +func (self *DiffHelper) otherWorktreeOfDiff() (string, bool) { + branchDiffContext, ok := self.c.Context().CurrentSide().(types.BranchDiffContext) + if !ok { + return "", false + } + branch := branchDiffContext.BranchOfDiff() + if branch == nil { + return "", false + } + worktree, ok := git_commands.WorktreeForBranch(branch, self.c.Model().Worktrees) + if !ok || worktree.IsCurrent || worktree.IsPathMissing { + return "", false + } + return worktree.Path, true +} + +// pathInWorktreeOfDiff returns the path to open a file of the diff from. The path +// passed in is either relative to the current worktree, or an absolute path in it, as +// a diff renderer may state paths. +func (self *DiffHelper) pathInWorktreeOfDiff(path string) string { + worktreePath, ok := self.otherWorktreeOfDiff() + if !ok { + return path + } + if filepath.IsAbs(path) { + relativePath := repoRelativePath(self.c.Git().RepoPaths.WorktreePath(), path) + if relativePath == "" { + return path + } + path = relativePath + } + return filepath.Join(worktreePath, path) +} + // AdjustLineNumber is used to adjust a line number in the diff that's currently // being viewed, so that it corresponds to the line number in the actual working // copy state of the file. It is used when clicking on a delta hyperlink in a // diff, or when pressing `e` in a focused diff. It works // by getting a diff of what's being viewed in the main view against the working -// copy, and then using that diff to adjust the line number. -// path is the file path of the file being viewed +// copy, and then using that diff to adjust the line number. The working copy is that +// of the worktree the file is opened from (see otherWorktreeOfDiff). +// path is the file path of the file being viewed, in that worktree // linenumber is the line number to adjust (one-based) // viewname is the name of the view that shows the diff. We need to pass it // because the diff adjustment is slightly different depending on which view is @@ -258,7 +394,13 @@ func (self *DiffHelper) AdjustLineNumber(path string, linenumber int, viewname s func (self *DiffHelper) adjustLineNumber(linenumber int, diffArgs ...string) int { args := append([]string{"--unified=0"}, diffArgs...) - diff, err := self.c.Git().Diff.GetDiff(false, args...) + var diff string + var err error + if worktreePath, ok := self.otherWorktreeOfDiff(); ok { + diff, err = self.c.Git().Diff.GetDiffInOtherWorktree(worktreePath, args...) + } else { + diff, err = self.c.Git().Diff.GetDiff(false, args...) + } if err != nil { return linenumber } diff --git a/pkg/gui/controllers/main_view_controller.go b/pkg/gui/controllers/main_view_controller.go index 9135f363f..487b15549 100644 --- a/pkg/gui/controllers/main_view_controller.go +++ b/pkg/gui/controllers/main_view_controller.go @@ -1085,13 +1085,10 @@ func (self *MainViewController) editDiffLine(viewLine int, beforeEdit func()) er // it opens the file without jumping anywhere — as pressing edit on a file in a side // panel does. if info.Type == types.DiffLineFileHeader { - return self.c.Helpers().Files.EditFiles([]string{info.Path}) + return self.c.Helpers().Diff.EditFilesOfDiff([]string{info.Path}) } - // The diff may be of an older commit, whose line numbers aren't the file's current - // ones, so they have to be carried forward before we can point an editor at them. - lineNumber := self.c.Helpers().Diff.AdjustLineNumber(info.Path, info.NewLine, self.context.GetViewName()) - return self.c.Helpers().Files.EditFileAtLine(info.Path, lineNumber) + return self.c.Helpers().Diff.EditFileOfDiffAtLine(info.Path, info.NewLine, self.context.GetViewName()) } // openPullRequestAtSelectedLine opens the pull request of the branch whose commit the diff --git a/pkg/gui/gui.go b/pkg/gui/gui.go index 7659a5f00..6f1a04875 100644 --- a/pkg/gui/gui.go +++ b/pkg/gui/gui.go @@ -417,10 +417,9 @@ func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, contextKey types.Context filepath := matches[1] if matches[2] != "" { lineNumber := utils.MustConvertToInt(matches[2]) - lineNumber = gui.helpers.Diff.AdjustLineNumber(filepath, lineNumber, viewname) - return gui.helpers.Files.EditFileAtLine(filepath, lineNumber) + return gui.helpers.Diff.EditFileOfDiffAtLine(filepath, lineNumber, viewname) } - return gui.helpers.Files.EditFiles([]string{filepath}) + return gui.helpers.Diff.EditFilesOfDiff([]string{filepath}) } if entry, ok := strings.CutPrefix(url, helpers.DiffStatLinkScheme); ok { diff --git a/pkg/gui/types/context.go b/pkg/gui/types/context.go index 958ebf759..46a111a42 100644 --- a/pkg/gui/types/context.go +++ b/pkg/gui/types/context.go @@ -253,6 +253,19 @@ type PullRequestDiff struct { BaseHash string } +// BranchDiffContext is implemented by the side panel contexts that show, in their +// focused main view, the diff of a commit of a branch entered from the branches panel: +// the sub-commits panel, and the commit files panel entered from it. If another +// worktree has that branch checked out, files of the diff are opened from there (see +// DiffHelper.EditFilesOfDiff). +type BranchDiffContext interface { + Context + + // BranchOfDiff returns the local branch whose commits the panel shows the diff of, + // or nil if the panel wasn't entered from a local branch. + BranchOfDiff() *models.Branch +} + // DiffPaneContext is one of the two panes the main section can show, as the thing // that holds a diff with a selection in it. The panels that act on such a selection // are handed the pane it was made in, and speak to it through this. diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 61f81a1f0..1cb516631 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -422,6 +422,8 @@ type TranslationSet struct { JumpToFileInDiffTooltip string OnlyOneFileInDiff string NoFileInDiffNamed string + FileOfDiffDoesNotExist string + NoFileOfDiffExists string PrevConflict string NextConflict string SelectPrevHunk string @@ -1617,6 +1619,8 @@ func EnglishTranslationSet() *TranslationSet { JumpToFileInDiffTooltip: "Pick one of the files of the diff shown in the main view, and scroll the main view to it. The focus stays in this panel.", OnlyOneFileInDiff: "There is only one file in this diff", NoFileInDiffNamed: "This diff has no file named '{{.path}}'", + FileOfDiffDoesNotExist: "File '{{.path}}' doesn't exist in the working tree", + NoFileOfDiffExists: "None of the selected files exist in the working tree at '{{.worktreePath}}'", PrevConflict: "Previous conflict", NextConflict: "Next conflict", SelectPrevHunk: "Previous hunk", diff --git a/pkg/integration/tests/commit/edit_files_missing_from_working_tree.go b/pkg/integration/tests/commit/edit_files_missing_from_working_tree.go new file mode 100644 index 000000000..2239b2602 --- /dev/null +++ b/pkg/integration/tests/commit/edit_files_missing_from_working_tree.go @@ -0,0 +1,102 @@ +package commit + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var EditFilesMissingFromWorkingTree = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Edit or open files of a commit that a later commit deleted", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) { + config.GetUserConfig().Gui.UseHunkModeInDiffView = false + config.GetUserConfig().OS.Edit = "echo {{filename}} > edit-command" + config.GetUserConfig().OS.EditAtLine = "echo {{filename}}:{{line}} > edit-command" + config.GetUserConfig().OS.Open = "echo {{filename}} > open-command" + }, + SetupRepo: func(shell *Shell) { + shell.CreateFileAndAdd("file1", "one\n") + shell.CreateFileAndAdd("file2", "two\n") + shell.CreateFileAndAdd("file3", "three\n") + shell.Commit("add files") + shell.DeleteFileAndAdd("file2") + shell.DeleteFileAndAdd("file3") + shell.Commit("delete files") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + Focus(). + Lines( + Contains("delete files").IsSelected(), + Contains("add files"), + ). + NavigateToLine(Contains("add files")). + PressEnter() + + t.Views().CommitFiles(). + IsFocused(). + Lines( + Equals("▼ /").IsSelected(), + Contains("A file1"), + Contains("A file2"), + Contains("A file3"), + ). + NavigateToLine(Contains("file2")). + Press(keys.Universal.Edit). + Tap(func() { + t.ExpectPopup().Alert(). + Title(Equals("Error")). + Content(Contains("/repo/file2' doesn't exist in the working tree")). + Confirm() + }). + Press(keys.Universal.OpenFile). + Tap(func() { + t.ExpectPopup().Alert(). + Title(Equals("Error")). + Content(Contains("/repo/file2' doesn't exist in the working tree")). + Confirm() + }). + Press(keys.Universal.FocusMainView) + + t.Views().Main(). + IsFocused(). + NavigateToLine(Contains("+two")). + Press(keys.Universal.Edit). + Tap(func() { + t.ExpectPopup().Alert(). + Title(Equals("Error")). + Content(Contains("/repo/file2' doesn't exist in the working tree")). + Confirm() + }). + PressEscape() + + t.Views().CommitFiles(). + IsFocused(). + Press(keys.Universal.ToggleRangeSelect). + SelectNextItem(). + SelectedLines( + Contains("A file2"), + Contains("A file3"), + ). + Press(keys.Universal.Edit). + Tap(func() { + t.ExpectPopup().Alert(). + Title(Equals("Error")). + Content(Contains("None of the selected files exist in the working tree at '")). + Confirm() + }). + // If only some of the files are missing, the others are opened without + // complaint + NavigateToLine(Contains("file1")). + SelectedLines( + Contains("A file1"), + Contains("A file2"), + ). + Press(keys.Universal.Edit). + Tap(func() { + t.FileSystem().FileContent("edit-command", + Contains("/repo/file1\n").DoesNotContain("file2")) + }) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 496789e0c..aea6c4f13 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -135,6 +135,7 @@ var tests = []*components.IntegrationTest{ commit.DiscardRenamedFile, commit.DiscardSubmoduleChanges, commit.DoNotShowBranchMarkerForHeadCommit, + commit.EditFilesMissingFromWorkingTree, commit.FailHooksThenCommitNoHooks, commit.FindBaseCommitForFixup, commit.FindBaseCommitForFixupDisregardFixupsForSameBaseCommit, @@ -694,6 +695,7 @@ var tests = []*components.IntegrationTest{ worktree.DetachWorktreeFromBranch, worktree.DotfileBareRepo, worktree.DoubleNestedLinkedSubmodule, + worktree.EditFileOfBranchInOtherWorktree, worktree.ExcludeFileInWorktree, worktree.FastForwardWorktreeBranch, worktree.FastForwardWorktreeBranchShouldNotPolluteCurrentWorktree, @@ -702,6 +704,7 @@ var tests = []*components.IntegrationTest{ worktree.LocationCandidates, worktree.NewWorktreePicker, worktree.NewWorktreePickerRemote, + worktree.OpenFileOfBranchInOtherWorktree, worktree.RemoveWorktreeAndBothBranches, worktree.RemoveWorktreeAndBranch, worktree.RemoveWorktreeAndDeleteLocalAndRemoteBranch, diff --git a/pkg/integration/tests/worktree/edit_file_of_branch_in_other_worktree.go b/pkg/integration/tests/worktree/edit_file_of_branch_in_other_worktree.go new file mode 100644 index 000000000..c7aeacf5d --- /dev/null +++ b/pkg/integration/tests/worktree/edit_file_of_branch_in_other_worktree.go @@ -0,0 +1,71 @@ +package worktree + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var EditFileOfBranchInOtherWorktree = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Edit a file of a commit of a branch that is checked out in another worktree", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) { + config.GetUserConfig().Gui.UseHunkModeInDiffView = false + config.GetUserConfig().OS.Edit = "echo {{filename}} > edit-command" + config.GetUserConfig().OS.EditAtLine = "echo {{filename}}:{{line}} > edit-command" + }, + SetupRepo: func(shell *Shell) { + shell.CreateFileAndAdd("README.md", "hello world") + shell.Commit("initial commit") + shell.NewBranch("mybranch") + shell.CreateFileAndAdd("file.txt", "1\n2\n3\n") + shell.Commit("add file") + shell.Checkout("master") + shell.AddWorktreeCheckout("mybranch", "../linked-worktree") + + // Move the lines of the file down in the linked worktree, so that a line of + // the commit's diff is at a different line of the file there + shell.UpdateFile("../linked-worktree/file.txt", "0\n1\n2\n3\n") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Branches(). + Focus(). + Lines( + Contains("master").IsSelected(), + Contains("mybranch (worktree linked-worktree)"), + ). + NavigateToLine(Contains("mybranch")). + PressEnter() + + t.Views().SubCommits(). + IsFocused(). + Lines( + Contains("add file").IsSelected(), + Contains("initial commit"), + ). + Press(keys.Universal.FocusMainView) + + t.Views().Main(). + IsFocused(). + NavigateToLine(Contains("+2")). + Press(keys.Universal.Edit). + Tap(func() { + t.FileSystem().FileContent("edit-command", Contains("/linked-worktree/file.txt:3\n")) + }). + PressEscape() + + t.Views().SubCommits(). + IsFocused(). + PressEnter() + + t.Views().CommitFiles(). + IsFocused(). + Lines( + Contains("A file.txt").IsSelected(), + ). + Press(keys.Universal.Edit). + Tap(func() { + t.FileSystem().FileContent("edit-command", Contains("/linked-worktree/file.txt\n")) + }) + }, +}) diff --git a/pkg/integration/tests/worktree/open_file_of_branch_in_other_worktree.go b/pkg/integration/tests/worktree/open_file_of_branch_in_other_worktree.go new file mode 100644 index 000000000..e0bf265a2 --- /dev/null +++ b/pkg/integration/tests/worktree/open_file_of_branch_in_other_worktree.go @@ -0,0 +1,64 @@ +package worktree + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var OpenFileOfBranchInOtherWorktree = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Open a file of a commit of a branch that is checked out in another worktree, and copy its absolute path", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) { + config.GetUserConfig().OS.Open = "echo {{filename}} > open-command" + config.GetUserConfig().OS.CopyToClipboardCmd = "printf '%s' {{text}} > clipboard" + }, + SetupRepo: func(shell *Shell) { + shell.CreateFileAndAdd("README.md", "hello world") + shell.Commit("initial commit") + shell.NewBranch("mybranch") + shell.CreateFileAndAdd("file.txt", "content") + shell.Commit("add file") + shell.Checkout("master") + shell.AddWorktreeCheckout("mybranch", "../linked-worktree") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Branches(). + Focus(). + Lines( + Contains("master").IsSelected(), + Contains("mybranch (worktree linked-worktree)"), + ). + NavigateToLine(Contains("mybranch")). + PressEnter() + + t.Views().SubCommits(). + IsFocused(). + Lines( + Contains("add file").IsSelected(), + Contains("initial commit"), + ). + PressEnter() + + t.Views().CommitFiles(). + IsFocused(). + Lines( + Contains("A file.txt").IsSelected(), + ). + Press(keys.Universal.OpenFile). + Tap(func() { + t.FileSystem().FileContent("open-command", Contains("/linked-worktree/file.txt\n")) + }). + Press(keys.Files.CopyFileInfoToClipboard). + Tap(func() { + t.ExpectPopup().Menu(). + Title(Equals("Copy to clipboard")). + Select(Contains("Absolute path")). + Confirm() + + t.ExpectToast(Equals("File path copied to clipboard")) + + t.FileSystem().FileContent("clipboard", Contains("/linked-worktree/file.txt")) + }) + }, +})