diff --git a/pkg/commands/git_commands/commit.go b/pkg/commands/git_commands/commit.go index 52a65fb6d..46f07bdf1 100644 --- a/pkg/commands/git_commands/commit.go +++ b/pkg/commands/git_commands/commit.go @@ -279,6 +279,13 @@ func (self *CommitCommands) ShowCmdObj(hash string, filterPath string) oscommand return self.cmd.New(cmdArgs).DontLog() } +func (self *CommitCommands) ShowFileContentCmdObj(hash string, filePath string) oscommands.ICmdObj { + cmdArgs := NewGitCmd("show"). + Arg(fmt.Sprintf("%s:%s", hash, filePath)). + ToArgv() + return self.cmd.New(cmdArgs).DontLog() +} + // Revert reverts the selected commit by hash func (self *CommitCommands) Revert(hash string) error { cmdArgs := NewGitCmd("revert").Arg(hash).ToArgv() diff --git a/pkg/gui/controllers/commits_files_controller.go b/pkg/gui/controllers/commits_files_controller.go index 09f8e54b0..69be8ae5d 100644 --- a/pkg/gui/controllers/commits_files_controller.go +++ b/pkg/gui/controllers/commits_files_controller.go @@ -203,6 +203,16 @@ func (self *CommitFilesController) copyDiffToClipboard(path string, toastMessage return nil } +func (self *CommitFilesController) copyFileContentToClipboard(path string) error { + _, to := self.context().GetFromAndToForDiff() + cmdObj := self.c.Git().Commit.ShowFileContentCmdObj(to, path) + diff, err := cmdObj.RunWithOutput() + if err != nil { + return err + } + return self.c.OS().CopyToClipboard(diff) +} + func (self *CommitFilesController) openCopyMenu() error { node := self.context().GetSelected() @@ -246,6 +256,27 @@ func (self *CommitFilesController) openCopyMenu() error { DisabledReason: self.require(self.itemsSelected())(), Key: 'a', } + copyFileContentItem := &types.MenuItem{ + Label: self.c.Tr.CopyFileContent, + OnPress: func() error { + if err := self.copyFileContentToClipboard(node.GetPath()); err != nil { + return err + } + self.c.Toast(self.c.Tr.FileContentCopiedToast) + return nil + }, + DisabledReason: self.require(self.singleItemSelected( + func(node *filetree.CommitFileNode) *types.DisabledReason { + if !node.IsFile() { + return &types.DisabledReason{ + Text: self.c.Tr.ErrCannotCopyContentOfDirectory, + ShowErrorInPanel: true, + } + } + return nil + }))(), + Key: 'c', + } return self.c.Menu(types.CreateMenuOptions{ Title: self.c.Tr.CopyToClipboardMenu, @@ -254,6 +285,7 @@ func (self *CommitFilesController) openCopyMenu() error { copyPathItem, copyFileDiffItem, copyAllDiff, + copyFileContentItem, }, }) } diff --git a/pkg/i18n/english.go b/pkg/i18n/english.go index 977f6863c..ca46d7502 100644 --- a/pkg/i18n/english.go +++ b/pkg/i18n/english.go @@ -80,11 +80,13 @@ type TranslationSet struct { CopyFileDiffTooltip string CopySelectedDiff string CopyAllFilesDiff string + CopyFileContent string NoContentToCopyError string FileNameCopiedToast string FilePathCopiedToast string FileDiffCopiedToast string AllFilesDiffCopiedToast string + FileContentCopiedToast string FilterStagedFiles string FilterUnstagedFiles string FilterTrackedFiles string @@ -696,6 +698,7 @@ type TranslationSet struct { PatchCopiedToClipboard string CopiedToClipboard string ErrCannotEditDirectory string + ErrCannotCopyContentOfDirectory string ErrStageDirWithInlineMergeConflicts string ErrRepositoryMovedOrDeleted string ErrWorktreeMovedOrRemoved string @@ -1120,11 +1123,13 @@ func EnglishTranslationSet() *TranslationSet { CopyFileDiffTooltip: "If there are staged items, this command considers only them. Otherwise, it considers all the unstaged ones.", CopySelectedDiff: "Diff of selected file", CopyAllFilesDiff: "Diff of all files", + CopyFileContent: "Content of selected file", NoContentToCopyError: "Nothing to copy", FileNameCopiedToast: "File name copied to clipboard", FilePathCopiedToast: "File path copied to clipboard", FileDiffCopiedToast: "File diff copied to clipboard", AllFilesDiffCopiedToast: "All files diff copied to clipboard", + FileContentCopiedToast: "File content copied to clipboard", FilterStagedFiles: "Show only staged files", FilterUnstagedFiles: "Show only unstaged files", FilterTrackedFiles: "Show only tracked files", @@ -1737,6 +1742,7 @@ func EnglishTranslationSet() *TranslationSet { PatchCopiedToClipboard: "Patch copied to clipboard", CopiedToClipboard: "copied to clipboard", ErrCannotEditDirectory: "Cannot edit directories: you can only edit individual files", + ErrCannotCopyContentOfDirectory: "Cannot copy content of directories: you can only copy content of individual files", ErrStageDirWithInlineMergeConflicts: "Cannot stage/unstage directory containing files with inline merge conflicts. Please fix up the merge conflicts first", ErrRepositoryMovedOrDeleted: "Cannot find repo. It might have been moved or deleted ¯\\_(ツ)_/¯", CommandLog: "Command log", diff --git a/pkg/integration/tests/diff/copy_to_clipboard.go b/pkg/integration/tests/diff/copy_to_clipboard.go index 88c14cd48..a1a6b4a9e 100644 --- a/pkg/integration/tests/diff/copy_to_clipboard.go +++ b/pkg/integration/tests/diff/copy_to_clipboard.go @@ -23,17 +23,21 @@ var CopyToClipboard = NewIntegrationTest(NewIntegrationTestArgs{ shell.CreateDir("dir") shell.CreateFileAndAdd("dir/file1", "1st line\n") shell.Commit("1") - shell.CreateFileAndAdd("dir/file1", "1st line\n2nd line\n") + shell.UpdateFileAndAdd("dir/file1", "1st line\n2nd line\n") shell.CreateFileAndAdd("dir/file2", "file2\n") shell.Commit("2") + shell.UpdateFileAndAdd("dir/file1", "1st line\n2nd line\n3rd line\n") + shell.Commit("3") }, Run: func(t *TestDriver, keys config.KeybindingConfig) { t.Views().Commits(). Focus(). Lines( - Contains("2").IsSelected(), + Contains("3").IsSelected(), + Contains("2"), Contains("1"), ). + SelectNextItem(). PressEnter() t.Views().CommitFiles(). @@ -91,11 +95,22 @@ var CopyToClipboard = NewIntegrationTest(NewIntegrationTestArgs{ Contains("diff --git a/dir/file1 b/dir/file1").Contains("+2nd line").DoesNotContain("+1st line"). Contains("diff --git a/dir/file2 b/dir/file2").Contains("+file2")) }) + }). + Press(keys.Files.CopyFileInfoToClipboard). + Tap(func() { + t.ExpectPopup().Menu(). + Title(Equals("Copy to clipboard")). + Select(Contains("Content of selected file")). + Confirm(). + Tap(func() { + t.ExpectToast(Equals("File content copied to clipboard")) + expectClipboard(t, Equals("1st line\n2nd line\n")) + }) }) t.Views().Commits(). Focus(). - // Select both commits + // Select commits 1 and 2 Press(keys.Universal.RangeSelectDown). PressEnter() @@ -118,6 +133,17 @@ var CopyToClipboard = NewIntegrationTest(NewIntegrationTestArgs{ expectClipboard(t, Contains("diff --git a/dir/file1 b/dir/file1").Contains("+1st line").Contains("+2nd line")) }) + }). + Press(keys.Files.CopyFileInfoToClipboard). + Tap(func() { + t.ExpectPopup().Menu(). + Title(Equals("Copy to clipboard")). + Select(Contains("Content of selected file")). + Confirm(). + Tap(func() { + t.ExpectToast(Equals("File content copied to clipboard")) + expectClipboard(t, Equals("1st line\n2nd line\n")) + }) }) }, })