Skip to content

Commit e8d3a7a

Browse files
authored
Add a "Content of selected file" entry to the copy menu for commit files (#4341)
- **PR Description** Some people have the need to get at the file content of a file as it was in an earlier commit. They expect to press `e` in the commit files view and get the file opened in the state it was in at that commit, somehow. I explained a few times (e.g. in #3902, #4109, and #4327) that this is not how `e` should work; so if we want this as a feature, we'd need it as a separate command. However, it's a bit tricky to implement; if we check out the selected file to a temp file, it's unclear when to remove the file again. Alternatively we could use EditAndWait to open the file, then we would block until the user closes the editor and delete the file then. This is probably not the best user experience though, e.g. it wouldn't allow opening two files from an older commit at once. Instead of finding solutions to all these questions, this PR takes a simpler route and adds a command to the "Copy to Clipboard" menu that allows copying the entire content of a file. Users can then open a new, empty editor buffer and paste it in, this is almost as good as opening a temp file, but without all the questions. This was suggested in #4327. - **Please check if the PR fulfills these requirements** * [x] Cheatsheets are up-to-date (run `go generate ./...`) * [x] Code has been formatted (see [here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#code-formatting)) * [x] Tests have been added/updated (see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md) for the integration test guide) * [x] Text is internationalised (see [here](https://github.com/jesseduffield/lazygit/blob/master/CONTRIBUTING.md#internationalisation)) * [ ] If a new UserConfig entry was added, make sure it can be hot-reloaded (see [here](https://github.com/jesseduffield/lazygit/blob/master/docs/dev/Codebase_Guide.md#using-userconfig)) * [ ] Docs have been updated if necessary * [x] You've read through your own file changes for silly mistakes etc
2 parents 357c046 + f7295a9 commit e8d3a7a

File tree

4 files changed

+74
-3
lines changed

4 files changed

+74
-3
lines changed

pkg/commands/git_commands/commit.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,13 @@ func (self *CommitCommands) ShowCmdObj(hash string, filterPath string) oscommand
279279
return self.cmd.New(cmdArgs).DontLog()
280280
}
281281

282+
func (self *CommitCommands) ShowFileContentCmdObj(hash string, filePath string) oscommands.ICmdObj {
283+
cmdArgs := NewGitCmd("show").
284+
Arg(fmt.Sprintf("%s:%s", hash, filePath)).
285+
ToArgv()
286+
return self.cmd.New(cmdArgs).DontLog()
287+
}
288+
282289
// Revert reverts the selected commit by hash
283290
func (self *CommitCommands) Revert(hash string) error {
284291
cmdArgs := NewGitCmd("revert").Arg(hash).ToArgv()

pkg/gui/controllers/commits_files_controller.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,16 @@ func (self *CommitFilesController) copyDiffToClipboard(path string, toastMessage
203203
return nil
204204
}
205205

206+
func (self *CommitFilesController) copyFileContentToClipboard(path string) error {
207+
_, to := self.context().GetFromAndToForDiff()
208+
cmdObj := self.c.Git().Commit.ShowFileContentCmdObj(to, path)
209+
diff, err := cmdObj.RunWithOutput()
210+
if err != nil {
211+
return err
212+
}
213+
return self.c.OS().CopyToClipboard(diff)
214+
}
215+
206216
func (self *CommitFilesController) openCopyMenu() error {
207217
node := self.context().GetSelected()
208218

@@ -246,6 +256,27 @@ func (self *CommitFilesController) openCopyMenu() error {
246256
DisabledReason: self.require(self.itemsSelected())(),
247257
Key: 'a',
248258
}
259+
copyFileContentItem := &types.MenuItem{
260+
Label: self.c.Tr.CopyFileContent,
261+
OnPress: func() error {
262+
if err := self.copyFileContentToClipboard(node.GetPath()); err != nil {
263+
return err
264+
}
265+
self.c.Toast(self.c.Tr.FileContentCopiedToast)
266+
return nil
267+
},
268+
DisabledReason: self.require(self.singleItemSelected(
269+
func(node *filetree.CommitFileNode) *types.DisabledReason {
270+
if !node.IsFile() {
271+
return &types.DisabledReason{
272+
Text: self.c.Tr.ErrCannotCopyContentOfDirectory,
273+
ShowErrorInPanel: true,
274+
}
275+
}
276+
return nil
277+
}))(),
278+
Key: 'c',
279+
}
249280

250281
return self.c.Menu(types.CreateMenuOptions{
251282
Title: self.c.Tr.CopyToClipboardMenu,
@@ -254,6 +285,7 @@ func (self *CommitFilesController) openCopyMenu() error {
254285
copyPathItem,
255286
copyFileDiffItem,
256287
copyAllDiff,
288+
copyFileContentItem,
257289
},
258290
})
259291
}

pkg/i18n/english.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,13 @@ type TranslationSet struct {
8080
CopyFileDiffTooltip string
8181
CopySelectedDiff string
8282
CopyAllFilesDiff string
83+
CopyFileContent string
8384
NoContentToCopyError string
8485
FileNameCopiedToast string
8586
FilePathCopiedToast string
8687
FileDiffCopiedToast string
8788
AllFilesDiffCopiedToast string
89+
FileContentCopiedToast string
8890
FilterStagedFiles string
8991
FilterUnstagedFiles string
9092
FilterTrackedFiles string
@@ -696,6 +698,7 @@ type TranslationSet struct {
696698
PatchCopiedToClipboard string
697699
CopiedToClipboard string
698700
ErrCannotEditDirectory string
701+
ErrCannotCopyContentOfDirectory string
699702
ErrStageDirWithInlineMergeConflicts string
700703
ErrRepositoryMovedOrDeleted string
701704
ErrWorktreeMovedOrRemoved string
@@ -1120,11 +1123,13 @@ func EnglishTranslationSet() *TranslationSet {
11201123
CopyFileDiffTooltip: "If there are staged items, this command considers only them. Otherwise, it considers all the unstaged ones.",
11211124
CopySelectedDiff: "Diff of selected file",
11221125
CopyAllFilesDiff: "Diff of all files",
1126+
CopyFileContent: "Content of selected file",
11231127
NoContentToCopyError: "Nothing to copy",
11241128
FileNameCopiedToast: "File name copied to clipboard",
11251129
FilePathCopiedToast: "File path copied to clipboard",
11261130
FileDiffCopiedToast: "File diff copied to clipboard",
11271131
AllFilesDiffCopiedToast: "All files diff copied to clipboard",
1132+
FileContentCopiedToast: "File content copied to clipboard",
11281133
FilterStagedFiles: "Show only staged files",
11291134
FilterUnstagedFiles: "Show only unstaged files",
11301135
FilterTrackedFiles: "Show only tracked files",
@@ -1737,6 +1742,7 @@ func EnglishTranslationSet() *TranslationSet {
17371742
PatchCopiedToClipboard: "Patch copied to clipboard",
17381743
CopiedToClipboard: "copied to clipboard",
17391744
ErrCannotEditDirectory: "Cannot edit directories: you can only edit individual files",
1745+
ErrCannotCopyContentOfDirectory: "Cannot copy content of directories: you can only copy content of individual files",
17401746
ErrStageDirWithInlineMergeConflicts: "Cannot stage/unstage directory containing files with inline merge conflicts. Please fix up the merge conflicts first",
17411747
ErrRepositoryMovedOrDeleted: "Cannot find repo. It might have been moved or deleted ¯\\_(ツ)_/¯",
17421748
CommandLog: "Command log",

pkg/integration/tests/diff/copy_to_clipboard.go

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,17 +23,21 @@ var CopyToClipboard = NewIntegrationTest(NewIntegrationTestArgs{
2323
shell.CreateDir("dir")
2424
shell.CreateFileAndAdd("dir/file1", "1st line\n")
2525
shell.Commit("1")
26-
shell.CreateFileAndAdd("dir/file1", "1st line\n2nd line\n")
26+
shell.UpdateFileAndAdd("dir/file1", "1st line\n2nd line\n")
2727
shell.CreateFileAndAdd("dir/file2", "file2\n")
2828
shell.Commit("2")
29+
shell.UpdateFileAndAdd("dir/file1", "1st line\n2nd line\n3rd line\n")
30+
shell.Commit("3")
2931
},
3032
Run: func(t *TestDriver, keys config.KeybindingConfig) {
3133
t.Views().Commits().
3234
Focus().
3335
Lines(
34-
Contains("2").IsSelected(),
36+
Contains("3").IsSelected(),
37+
Contains("2"),
3538
Contains("1"),
3639
).
40+
SelectNextItem().
3741
PressEnter()
3842

3943
t.Views().CommitFiles().
@@ -91,11 +95,22 @@ var CopyToClipboard = NewIntegrationTest(NewIntegrationTestArgs{
9195
Contains("diff --git a/dir/file1 b/dir/file1").Contains("+2nd line").DoesNotContain("+1st line").
9296
Contains("diff --git a/dir/file2 b/dir/file2").Contains("+file2"))
9397
})
98+
}).
99+
Press(keys.Files.CopyFileInfoToClipboard).
100+
Tap(func() {
101+
t.ExpectPopup().Menu().
102+
Title(Equals("Copy to clipboard")).
103+
Select(Contains("Content of selected file")).
104+
Confirm().
105+
Tap(func() {
106+
t.ExpectToast(Equals("File content copied to clipboard"))
107+
expectClipboard(t, Equals("1st line\n2nd line\n"))
108+
})
94109
})
95110

96111
t.Views().Commits().
97112
Focus().
98-
// Select both commits
113+
// Select commits 1 and 2
99114
Press(keys.Universal.RangeSelectDown).
100115
PressEnter()
101116

@@ -118,6 +133,17 @@ var CopyToClipboard = NewIntegrationTest(NewIntegrationTestArgs{
118133
expectClipboard(t,
119134
Contains("diff --git a/dir/file1 b/dir/file1").Contains("+1st line").Contains("+2nd line"))
120135
})
136+
}).
137+
Press(keys.Files.CopyFileInfoToClipboard).
138+
Tap(func() {
139+
t.ExpectPopup().Menu().
140+
Title(Equals("Copy to clipboard")).
141+
Select(Contains("Content of selected file")).
142+
Confirm().
143+
Tap(func() {
144+
t.ExpectToast(Equals("File content copied to clipboard"))
145+
expectClipboard(t, Equals("1st line\n2nd line\n"))
146+
})
121147
})
122148
},
123149
})

0 commit comments

Comments
 (0)