diff --git a/docs-master/keybindings/Keybindings_en.md b/docs-master/keybindings/Keybindings_en.md index 6f3d6cbac..749cdd9e8 100644 --- a/docs-master/keybindings/Keybindings_en.md +++ b/docs-master/keybindings/Keybindings_en.md @@ -226,6 +226,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. | | `` v `` | Toggle range select | | | `` e `` | Edit file | Open file in external editor. | +| `` `` | Stage | Toggle selection staged / unstaged. | | `` `` | Copy selected text to clipboard | | | `` , h `` | Go to previous hunk | | | `` , l `` | Go to next hunk | | @@ -339,6 +340,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. | | `` v `` | Toggle range select | | | `` e `` | Edit file | Open file in external editor. | +| `` `` | Stage | Toggle selection staged / unstaged. | | `` `` | Copy selected text to clipboard | | | `` , h `` | Go to previous hunk | | | `` , l `` | Go to next hunk | | diff --git a/docs-master/keybindings/Keybindings_ja.md b/docs-master/keybindings/Keybindings_ja.md index 7fd62f726..331becbd0 100644 --- a/docs-master/keybindings/Keybindings_ja.md +++ b/docs-master/keybindings/Keybindings_ja.md @@ -195,6 +195,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` a `` | ハンクの選択を切り替える | Toggle line-by-line vs. hunk selection mode. | | `` v `` | 範囲選択を切り替え | | | `` e `` | ファイルを編集 | 外部エディタでファイルを開きます。 | +| `` `` | ステージ | 選択された部分のステージ / アンステージを切り替えます。 | | `` `` | 選択したテキストをクリップボードにコピー | | | `` , h `` | 前のハンクに移動 | | | `` , l `` | 次のハンクに移動 | | @@ -316,6 +317,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` a `` | ハンクの選択を切り替える | Toggle line-by-line vs. hunk selection mode. | | `` v `` | 範囲選択を切り替え | | | `` e `` | ファイルを編集 | 外部エディタでファイルを開きます。 | +| `` `` | ステージ | 選択された部分のステージ / アンステージを切り替えます。 | | `` `` | 選択したテキストをクリップボードにコピー | | | `` , h `` | 前のハンクに移動 | | | `` , l `` | 次のハンクに移動 | | diff --git a/docs-master/keybindings/Keybindings_ko.md b/docs-master/keybindings/Keybindings_ko.md index b0437591f..adf015638 100644 --- a/docs-master/keybindings/Keybindings_ko.md +++ b/docs-master/keybindings/Keybindings_ko.md @@ -87,6 +87,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. | | `` v `` | 드래그 선택 전환 | | | `` e `` | 파일 편집 | Open file in external editor. | +| `` `` | Staged 전환 | 선택한 행을 staged / unstaged | | `` `` | 선택한 텍스트를 클립보드에 복사 | | | `` , h `` | 이전 hunk를 선택 | | | `` , l `` | 다음 hunk를 선택 | | @@ -173,6 +174,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. | | `` v `` | 드래그 선택 전환 | | | `` e `` | 파일 편집 | Open file in external editor. | +| `` `` | Staged 전환 | 선택한 행을 staged / unstaged | | `` `` | 선택한 텍스트를 클립보드에 복사 | | | `` , h `` | 이전 hunk를 선택 | | | `` , l `` | 다음 hunk를 선택 | | diff --git a/docs-master/keybindings/Keybindings_nl.md b/docs-master/keybindings/Keybindings_nl.md index 3d7012267..e2db96275 100644 --- a/docs-master/keybindings/Keybindings_nl.md +++ b/docs-master/keybindings/Keybindings_nl.md @@ -234,6 +234,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` a `` | Wissel tussen hunk selectie aan of uit | Wissel tussen regel-voor-regel of hunk selectie modus. | | `` v `` | Toggle drag selecteer | | | `` e `` | Verander bestand | Open bestand in externe editor. | +| `` `` | Toggle staged | Toggle lijnen staged / unstaged | | `` `` | Copy selected text to clipboard | | | `` , h `` | Selecteer de vorige hunk | | | `` , l `` | Selecteer de volgende hunk | | @@ -317,6 +318,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` a `` | Wissel tussen hunk selectie aan of uit | Wissel tussen regel-voor-regel of hunk selectie modus. | | `` v `` | Toggle drag selecteer | | | `` e `` | Verander bestand | Open bestand in externe editor. | +| `` `` | Toggle staged | Toggle lijnen staged / unstaged | | `` `` | Copy selected text to clipboard | | | `` , h `` | Selecteer de vorige hunk | | | `` , l `` | Selecteer de volgende hunk | | diff --git a/docs-master/keybindings/Keybindings_pl.md b/docs-master/keybindings/Keybindings_pl.md index e79338b3c..0b1f4c763 100644 --- a/docs-master/keybindings/Keybindings_pl.md +++ b/docs-master/keybindings/Keybindings_pl.md @@ -102,6 +102,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. | | `` v `` | Przełącz zaznaczenie zakresu | | | `` e `` | Edytuj plik | Otwórz plik w zewnętrznym edytorze. | +| `` `` | Zatwierdź | Przełącz zaznaczenie zatwierdzone/niezatwierdzone. | | `` `` | Kopiuj zaznaczony tekst do schowka | | | `` , h `` | Idź do poprzedniego fragmentu | | | `` , l `` | Idź do następnego fragmentu | | @@ -212,6 +213,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. | | `` v `` | Przełącz zaznaczenie zakresu | | | `` e `` | Edytuj plik | Otwórz plik w zewnętrznym edytorze. | +| `` `` | Zatwierdź | Przełącz zaznaczenie zatwierdzone/niezatwierdzone. | | `` `` | Kopiuj zaznaczony tekst do schowka | | | `` , h `` | Idź do poprzedniego fragmentu | | | `` , l `` | Idź do następnego fragmentu | | diff --git a/docs-master/keybindings/Keybindings_pt.md b/docs-master/keybindings/Keybindings_pt.md index 4041c5edd..8f8d2814b 100644 --- a/docs-master/keybindings/Keybindings_pt.md +++ b/docs-master/keybindings/Keybindings_pt.md @@ -238,6 +238,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` a `` | Toggle hunk selection | Ativa/desativa modo linha por linha vs. modo de seleção por partes. | | `` v `` | Toggle range select | | | `` e `` | Editar arquivo | Abrir arquivo no editor externo. | +| `` `` | Etapa | Ativar/desativar seleção em staged/unstaged | | `` `` | Copiar texto selecionado para área de transferência | | | `` , h `` | Ir para o local anterior | | | `` , l `` | Ir para o próximo trecho | | @@ -348,6 +349,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` a `` | Toggle hunk selection | Ativa/desativa modo linha por linha vs. modo de seleção por partes. | | `` v `` | Toggle range select | | | `` e `` | Editar arquivo | Abrir arquivo no editor externo. | +| `` `` | Etapa | Ativar/desativar seleção em staged/unstaged | | `` `` | Copiar texto selecionado para área de transferência | | | `` , h `` | Ir para o local anterior | | | `` , l `` | Ir para o próximo trecho | | diff --git a/docs-master/keybindings/Keybindings_ru.md b/docs-master/keybindings/Keybindings_ru.md index d113eb4fa..399e86305 100644 --- a/docs-master/keybindings/Keybindings_ru.md +++ b/docs-master/keybindings/Keybindings_ru.md @@ -77,6 +77,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. | | `` v `` | Переключить выборку перетаскивания | | | `` e `` | Редактировать файл | Open file in external editor. | +| `` `` | Переключить индекс | Переключить строку в проиндексированные / непроиндексированные | | `` `` | Скопировать выделенный текст в буфер обмена | | | `` , h `` | Выбрать предыдущую часть | | | `` , l `` | Выбрать следующую часть | | @@ -117,6 +118,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. | | `` v `` | Переключить выборку перетаскивания | | | `` e `` | Редактировать файл | Open file in external editor. | +| `` `` | Переключить индекс | Переключить строку в проиндексированные / непроиндексированные | | `` `` | Скопировать выделенный текст в буфер обмена | | | `` , h `` | Выбрать предыдущую часть | | | `` , l `` | Выбрать следующую часть | | diff --git a/docs-master/keybindings/Keybindings_zh-CN.md b/docs-master/keybindings/Keybindings_zh-CN.md index 97d006713..9e12bc5d2 100644 --- a/docs-master/keybindings/Keybindings_zh-CN.md +++ b/docs-master/keybindings/Keybindings_zh-CN.md @@ -289,6 +289,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` a `` | 切换代码块选择 | 切换逐行选择与代码块选择模式。 | | `` v `` | 切换拖动选择 | | | `` e `` | 编辑文件 | 使用外部编辑器打开文件 | +| `` `` | 切换暂存状态 | 切换行暂存状态 | | `` `` | 复制选中文本到剪贴板 | | | `` , h `` | 选择上一个区块 | | | `` , l `` | 选择下一个区块 | | @@ -345,6 +346,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` a `` | 切换代码块选择 | 切换逐行选择与代码块选择模式。 | | `` v `` | 切换拖动选择 | | | `` e `` | 编辑文件 | 使用外部编辑器打开文件 | +| `` `` | 切换暂存状态 | 切换行暂存状态 | | `` `` | 复制选中文本到剪贴板 | | | `` , h `` | 选择上一个区块 | | | `` , l `` | 选择下一个区块 | | diff --git a/docs-master/keybindings/Keybindings_zh-TW.md b/docs-master/keybindings/Keybindings_zh-TW.md index cc8116843..2adf9ac93 100644 --- a/docs-master/keybindings/Keybindings_zh-TW.md +++ b/docs-master/keybindings/Keybindings_zh-TW.md @@ -78,6 +78,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` a `` | 切換程式碼塊選擇 | 切換逐行選擇與程式碼塊選擇模式。 | | `` v `` | 切換拖曳選擇 | | | `` e `` | 編輯檔案 | 使用外部編輯器開啟 | +| `` `` | 切換預存 | 切換現有行的狀態 (已預存/未預存) | | `` `` | 複製所選文本至剪貼簿 | | | `` , h `` | 選擇上一段 | | | `` , l `` | 選擇下一段 | | @@ -367,6 +368,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` a `` | 切換程式碼塊選擇 | 切換逐行選擇與程式碼塊選擇模式。 | | `` v `` | 切換拖曳選擇 | | | `` e `` | 編輯檔案 | 使用外部編輯器開啟 | +| `` `` | 切換預存 | 切換現有行的狀態 (已預存/未預存) | | `` `` | 複製所選文本至剪貼簿 | | | `` , h `` | 選擇上一段 | | | `` , l `` | 選擇下一段 | | diff --git a/pkg/gui/controllers/helpers/diff_line_queries.go b/pkg/gui/controllers/helpers/diff_line_queries.go index 77f82953b..1f623b937 100644 --- a/pkg/gui/controllers/helpers/diff_line_queries.go +++ b/pkg/gui/controllers/helpers/diff_line_queries.go @@ -34,6 +34,15 @@ func (self *DiffLineHelper) DiffLinesInBufferRange(view *gocui.View, first int, return infos } +// ChangeLinesInBufferRange returns the change lines — the additions and deletions — +// among the diff lines shown by the rows in the inclusive buffer-line range. Those are +// the lines a patch is built from: a patch carries whatever context it needs around +// them by itself, so a selection contributes only its changes. +func (self *DiffLineHelper) ChangeLinesInBufferRange(view *gocui.View, first int, last int) []types.DiffLineInfo { + return lo.Filter(self.DiffLinesInBufferRange(view, first, last), + func(info types.DiffLineInfo, _ int) bool { return info.IsChange() }) +} + // changeLines resolves view's rendered diff to one flag per buffer line: whether // that row is a change line (an addition or a deletion), as opposed to context, a // header, or a row whose identity couldn't be recovered. Those are the rows a diff --git a/pkg/gui/controllers/main_view_controller.go b/pkg/gui/controllers/main_view_controller.go index a504596b5..b814ea0fa 100644 --- a/pkg/gui/controllers/main_view_controller.go +++ b/pkg/gui/controllers/main_view_controller.go @@ -79,6 +79,15 @@ func (self *MainViewController) GetKeybindings(opts types.KeybindingsOpts) []*ty GetDisabledReason: self.diffSelectionDisabledReason, Tooltip: self.c.Tr.EditFileTooltip, }, + { + Keys: opts.GetKeys(opts.Config.Universal.Select), + Handler: self.primaryAction, + Description: self.c.Tr.Stage, + DescriptionFunc: self.workingTreeActionDescription(self.c.Tr.Stage), + GetDisabledReason: self.diffSelectionDisabledReason, + Tooltip: self.c.Tr.StageSelectionTooltip, + DisplayOnScreen: true, + }, { Keys: opts.GetKeys(opts.Config.Universal.CopyToClipboard), Handler: self.copySelection, @@ -269,6 +278,39 @@ func (self *MainViewController) diffSource() types.FocusedMainViewDiffSource { return sidePanel.GetFocusedMainViewDiffSource() } +// focusedMainViewActions returns what the panel beneath the focused main view does to +// a selection in its diff, or nil where it does nothing to it — a panel whose diff can +// be read and copied but not acted on. +func (self *MainViewController) focusedMainViewActions() types.FocusedMainViewActions { + actions, _ := self.diffSource().(types.FocusedMainViewActions) + return actions +} + +// primaryAction acts on the selected diff lines, leaving what that means to the panel +// beneath — which also re-renders the diff, since it is the one that changed it. +func (self *MainViewController) primaryAction() error { + actions := self.focusedMainViewActions() + if actions == nil { + return nil + } + first, last, ok := self.context.GetView().SelectedBufferLineRange() + if !ok { + return nil + } + return actions.PrimaryAction(self.context, first, last) +} + +// workingTreeActionDescription gives a command's description only where the command +// applies — over the working tree's diff — so that it is listed there and nowhere else. +func (self *MainViewController) workingTreeActionDescription(description string) func() string { + return func() string { + if self.diffMainViewType() != types.DiffMainViewTypeStaging { + return "" + } + return description + } +} + // copySelection copies the selected diff lines to the clipboard — not as the diff // renderer drew them, but as they read in the diff itself, which is both what you meant // to copy and the only form a renderer can't have mangled. A selection that is all diff --git a/pkg/gui/controllers/working_tree_diff_actions.go b/pkg/gui/controllers/working_tree_diff_actions.go index 889cc0859..e75d538f2 100644 --- a/pkg/gui/controllers/working_tree_diff_actions.go +++ b/pkg/gui/controllers/working_tree_diff_actions.go @@ -1,8 +1,16 @@ package controllers import ( + "fmt" + "path/filepath" + + "github.com/jesseduffield/generics/set" + "github.com/jesseduffield/lazygit/pkg/commands/git_commands" + "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/commands/patch" "github.com/jesseduffield/lazygit/pkg/gui/context" "github.com/jesseduffield/lazygit/pkg/gui/types" + "github.com/samber/lo" ) // WorkingTreeDiffActions implements what the files panel offers on the diff it renders @@ -12,7 +20,7 @@ type WorkingTreeDiffActions struct { c *ControllerCommon } -var _ types.FocusedMainViewDiffSource = &WorkingTreeDiffActions{} +var _ types.FocusedMainViewActions = &WorkingTreeDiffActions{} func NewWorkingTreeDiffActions(c *ControllerCommon) *WorkingTreeDiffActions { return &WorkingTreeDiffActions{c: c} @@ -42,3 +50,137 @@ func (self *WorkingTreeDiffActions) PlainDiff(pane types.DiffPaneContext, paths func (self *WorkingTreeDiffActions) showsStagedSide(pane types.DiffPaneContext) bool { return pane.GetKey() == self.c.Contexts().NormalSecondary.GetKey() } + +// PrimaryAction stages the selected diff lines, or takes them back out of the index +// when what is selected is the staged side of the diff. +func (self *WorkingTreeDiffActions) PrimaryAction(pane types.DiffPaneContext, firstBufferLine int, lastBufferLine int) error { + if self.c.UserConfig().Git.DiffContextSize == 0 { + return fmt.Errorf(self.c.Tr.Actions.NotEnoughContextToStage, + self.c.UserConfig().Keybinding.Universal.IncreaseContextInDiffView) + } + + infos, onStagedSide, ok := self.diffLineSelection(pane, firstBufferLine, lastBufferLine) + if !ok { + return nil + } + + // Either way the patch goes to the index: forwards from the unstaged side to stage + // it, backwards from the staged side to take it back out. + return self.applyDiffLineSelection(infos, onStagedSide, + git_commands.ApplyPatchOpts{Reverse: onStagedSide, Cached: true}) +} + +// diffLineSelection resolves what the user has selected in a pane of the focused main +// view to the change lines to act on, and reports whether they are the staged side of +// the diff — which is a question about the pane, so it is the same for every file of a +// directory's diff. ok is false when the selection holds no change line, in which case +// there is nothing to act on. +func (self *WorkingTreeDiffActions) diffLineSelection( + pane types.DiffPaneContext, firstBufferLine int, lastBufferLine int, +) (infos []types.DiffLineInfo, onStagedSide bool, ok bool) { + infos = self.c.Helpers().DiffLine.ChangeLinesInBufferRange(pane.GetView(), firstBufferLine, lastBufferLine) + if len(infos) == 0 { + return nil, false, false + } + return infos, self.showsStagedSide(pane), true +} + +// applyDiffLineSelection applies the selected change lines, a patch per file, and +// re-renders what that changed. onStagedSide says which of the file's two diffs the +// lines were selected in and so are to be found in; opts says how to apply them. +func (self *WorkingTreeDiffActions) applyDiffLineSelection( + infos []types.DiffLineInfo, onStagedSide bool, opts git_commands.ApplyPatchOpts, +) error { + self.c.LogAction(self.c.Tr.Actions.ApplyPatch) + + // A directory's diff spans several files, and a patch is of one file, so the + // selected lines are grouped by the file they belong to and applied file by file. + infosByFile := lo.GroupBy(infos, func(info types.DiffLineInfo) string { return info.Path }) + for path, fileInfos := range infosByFile { + file := self.fileForDiffLinePath(path) + if file == nil { + continue + } + if err := self.applyDiffLines(file, fileInfos, onStagedSide, opts); err != nil { + return err + } + } + + // Block input until the refresh has landed, so that a quick second keypress acts on + // the diff as it now is rather than on the one we just changed. + self.c.RefreshBlockingInput(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES}}) + return nil +} + +// fileForDiffLinePath maps the absolute path a diff line carries to the working tree +// file it belongs to, or nil for a path that is no file of this repo's working tree. +func (self *WorkingTreeDiffActions) fileForDiffLinePath(path string) *models.File { + relativePath, err := filepath.Rel(self.c.Git().RepoPaths.WorktreePath(), path) + if err != nil { + return nil + } + return self.context().FileTreeViewModel.GetFile(filepath.ToSlash(relativePath)) +} + +// applyDiffLines applies the given change lines of one file — a line, a hunk, a range — +// as a patch built from that file's own diff: +// +// - stage: read the unstaged diff, apply it to the index +// - unstage: read the staged diff, apply it to the index backwards +// +// sourceCached names the diff the lines were selected in, which is where they are found +// again; opts says how to apply what is built from them. The two are independent — a +// discard reads one side and reverses it — so they are passed separately. +// +// Each selected line is looked for by where it sits in the file. This tells the two +// halves of a modified line apart: the deletion and the addition replacing it share a +// position in the new file and differ only in being a deletion. Context lines are not +// selected: a patch of the lines you picked keeps whatever context it needs around +// them by itself. +func (self *WorkingTreeDiffActions) applyDiffLines( + file *models.File, infos []types.DiffLineInfo, sourceCached bool, opts git_commands.ApplyPatchOpts, +) error { + parsedPatch := patch.Parse(self.c.Git().WorkingTree.WorktreeFileDiff(file, true, sourceCached)) + + type changeLine struct { + lineNumber int + isDeletion bool + } + selected := set.New[changeLine]() + for _, info := range infos { + if info.Type == types.DiffLineDeleted { + selected.Add(changeLine{info.OldLine, true}) + } else { + selected.Add(changeLine{info.NewLine, false}) + } + } + + var patchLineIndices []int + for idx, line := range parsedPatch.Lines() { + var key changeLine + switch { + case line.IsAddition(): + key = changeLine{parsedPatch.LineNumberOfLine(idx), false} + case line.IsDeletion(): + key = changeLine{parsedPatch.OldLineNumberOfLine(idx), true} + default: + continue + } + if selected.Includes(key) { + patchLineIndices = append(patchLineIndices, idx) + } + } + + patchToApply := parsedPatch. + Transform(patch.TransformOpts{ + Reverse: opts.Reverse, + IncludedLineIndices: patchLineIndices, + FileNameOverride: file.GetPath(), + }). + FormatPlain() + if patchToApply == "" { + return nil + } + + return self.c.Git().Patch.ApplyPatch(patchToApply, opts) +} diff --git a/pkg/gui/types/context.go b/pkg/gui/types/context.go index 2ed28dd17..f468687d9 100644 --- a/pkg/gui/types/context.go +++ b/pkg/gui/types/context.go @@ -239,6 +239,24 @@ type FocusedMainViewDiffSource interface { PlainDiff(pane DiffPaneContext, paths []string) string } +// FocusedMainViewActions describes what a side panel does when the user acts on a +// selection of diff lines in the focused main view. The main view owns the selection +// and the keys; what acting on it means is the panel's business, e.g. the working +// tree panel stages and unstages. +// +// It extends the diff source rather than standing beside it, because acting on a +// selection needs the diff behind the rendering just as reading it does; a panel that +// implements only the source offers a diff to read and copy but nothing to do to it. +type FocusedMainViewActions interface { + FocusedMainViewDiffSource + + // PrimaryAction acts on the diff lines in the inclusive buffer-line range that the + // current selection in the given pane covers (see View.SelectedBufferLineRange): a + // single line, a range, or a hunk. The panel re-renders the diff itself, being the + // one that knows what it did to it. + PrimaryAction(pane DiffPaneContext, firstBufferLine int, lastBufferLine int) error +} + type IListContext interface { Context diff --git a/pkg/integration/tests/main_view/no_selection_over_a_binary_diff.go b/pkg/integration/tests/main_view/no_selection_over_a_binary_diff.go index 332b52b14..2405902ed 100644 --- a/pkg/integration/tests/main_view/no_selection_over_a_binary_diff.go +++ b/pkg/integration/tests/main_view/no_selection_over_a_binary_diff.go @@ -6,16 +6,18 @@ import ( ) var NoSelectionOverABinaryDiff = NewIntegrationTest(NewIntegrationTestArgs{ - Description: "A diff with nothing selectable in it shows no selection, and a refresh doesn't bring one", + Description: "A diff with nothing selectable in it shows no selection, however it came to be showing", ExtraCmdArgs: []string{}, Skip: false, SetupConfig: func(cfg *config.AppConfig) { cfg.GetUserConfig().Gui.UseHunkModeInStagingView = false }, SetupRepo: func(shell *Shell) { + shell.CreateFileAndAdd("text", "one\ntwo\nthree\n") shell.CreateFileAndAdd("binary", "\x00one\x00two\x00") shell.Commit("one") + shell.UpdateFile("text", "one\nTWO\nthree\n") shell.UpdateFile("binary", "\x00one\x00TWO\x00") }, Run: func(t *TestDriver, keys config.KeybindingConfig) { @@ -23,6 +25,7 @@ var NoSelectionOverABinaryDiff = NewIntegrationTest(NewIntegrationTestArgs{ // refresh, which renders the same diff again, doesn't make one appear. t.Views().Files(). IsFocused(). + NavigateToLine(Contains("binary")). Press(keys.Universal.FocusMainView) t.Views().Main(). @@ -32,5 +35,22 @@ var NoSelectionOverABinaryDiff = NewIntegrationTest(NewIntegrationTestArgs{ t.GlobalPress(keys.Universal.Refresh) }). SelectionIsHidden() + + // The same when acting on a diff of several files leaves nothing selectable in + // it: staging the text file's only change leaves the binary one behind. + t.Views().Files(). + Focus(). + NavigateToLine(Contains("▼ /")). + Press(keys.Universal.FocusMainView) + + t.Views().Main(). + IsFocused(). + SelectionIsActive(). + SelectedLines( + Contains("-two"), + ). + Press(keys.Main.ToggleSelectHunk). + PressPrimaryAction(). + SelectionIsHidden() }, }) diff --git a/pkg/integration/tests/main_view/stage_diff_lines.go b/pkg/integration/tests/main_view/stage_diff_lines.go new file mode 100644 index 000000000..6ce83e03e --- /dev/null +++ b/pkg/integration/tests/main_view/stage_diff_lines.go @@ -0,0 +1,70 @@ +package main_view + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var StageDiffLines = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Stage a line and a hunk of the working tree's diff from the focused main view", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(cfg *config.AppConfig) { + cfg.GetUserConfig().Gui.UseHunkModeInStagingView = false + }, + SetupRepo: func(shell *Shell) { + shell.CreateFileAndAdd("file1", "one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\n") + shell.Commit("one") + + // Two change blocks, far enough apart to stay separate hunks. + shell.UpdateFile("file1", "one\ntwo\nADD1\nADD2\nthree\nfour\nfive\nsix\nseven\neight\nNINE\nten\n") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsFocused(). + Lines( + Contains("file1").IsSelected(), + ). + Press(keys.Universal.FocusMainView) + + // A single line goes into the index by itself, leaving the rest of its block + // unstaged — which is the whole point of staging from the diff. + t.Views().Main(). + IsFocused(). + SelectedLines( + Contains("+ADD1"), + ). + PressPrimaryAction() + + t.Views().Files().Lines( + Contains("MM file1"), + ) + t.Views().Secondary(). + ContainsLines( + Contains("+ADD1"), + ). + Content(DoesNotContain("+ADD2")) + t.Views().Main(). + Content(DoesNotContain("+ADD1")). + ContainsLines( + Contains("+ADD2"), + ) + + // A whole change block goes in one press. + t.Views().Main(). + IsFocused(). + NavigateToLine(Contains("-nine")). + Press(keys.Main.ToggleSelectHunk). + SelectedLines( + Contains("-nine"), + Contains("+NINE"), + ). + PressPrimaryAction() + + t.Views().Secondary().ContainsLines( + Contains("-nine"), + Contains("+NINE"), + ) + t.Views().Main().Content(DoesNotContain("NINE")) + }, +}) diff --git a/pkg/integration/tests/main_view/stage_diff_lines_of_a_path_with_a_space.go b/pkg/integration/tests/main_view/stage_diff_lines_of_a_path_with_a_space.go new file mode 100644 index 000000000..f50b4e7b9 --- /dev/null +++ b/pkg/integration/tests/main_view/stage_diff_lines_of_a_path_with_a_space.go @@ -0,0 +1,52 @@ +package main_view + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var StageDiffLinesOfAPathWithASpace = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Stage a line of a file whose path contains a space from the focused main view", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(cfg *config.AppConfig) { + cfg.GetUserConfig().Gui.UseHunkModeInStagingView = false + }, + SetupRepo: func(shell *Shell) { + // git ends the path field of the diff's "---" and "+++" lines with a tab + // when the path contains a space, and the view shows the tab as spaces. + shell.CreateFileAndAdd("my file", "one\ntwo\nthree\n") + shell.Commit("one") + + shell.UpdateFile("my file", "one\nADD1\nADD2\ntwo\nthree\n") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsFocused(). + Lines( + Contains("my file").IsSelected(), + ). + Press(keys.Universal.FocusMainView) + + t.Views().Main(). + IsFocused(). + SelectedLines( + Contains("+ADD1"), + ). + PressPrimaryAction() + + t.Views().Files().Lines( + Contains("MM my file"), + ) + t.Views().Secondary(). + ContainsLines( + Contains("+ADD1"), + ). + Content(DoesNotContain("+ADD2")) + t.Views().Main(). + Content(DoesNotContain("+ADD1")). + ContainsLines( + Contains("+ADD2"), + ) + }, +}) diff --git a/pkg/integration/tests/main_view/stage_range_spanning_files.go b/pkg/integration/tests/main_view/stage_range_spanning_files.go new file mode 100644 index 000000000..70f614229 --- /dev/null +++ b/pkg/integration/tests/main_view/stage_range_spanning_files.go @@ -0,0 +1,55 @@ +package main_view + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var StageRangeSpanningFiles = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Stage a range reaching from one file's diff into another's, in a directory's focused main view", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(cfg *config.AppConfig) { + cfg.GetUserConfig().Gui.UseHunkModeInStagingView = false + }, + SetupRepo: func(shell *Shell) { + shell.CreateFileAndAdd("fileA", "a\n") + shell.CreateFileAndAdd("fileB", "b\n") + shell.CreateFileAndAdd("fileC", "c\n") + shell.Commit("one") + + shell.UpdateFile("fileA", "a\nfromA\n") + shell.UpdateFile("fileB", "b\nfromB\n") + shell.UpdateFile("fileC", "c\nfromC\n") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + // With the root of the tree selected, the main view shows all three files' diffs. + t.Views().Files(). + IsFocused(). + Lines( + Contains("▼ /").IsSelected(), + Contains(" M fileA"), + Contains(" M fileB"), + Contains(" M fileC"), + ). + Press(keys.Universal.FocusMainView) + + t.Views().Main(). + IsFocused(). + SelectedLines( + Contains("+fromA"), + ). + Press(keys.Universal.ToggleRangeSelect). + NavigateToLine(Contains("+fromB")). + PressPrimaryAction() + + // Both files the range reached into are staged, each by its own patch, and the + // file below it is untouched. + t.Views().Files().Lines( + Contains("▼ /"), + Contains("M fileA"), + Contains("M fileB"), + Contains(" M fileC"), + ) + }, +}) diff --git a/pkg/integration/tests/main_view/unstage_diff_lines.go b/pkg/integration/tests/main_view/unstage_diff_lines.go new file mode 100644 index 000000000..41f631b0e --- /dev/null +++ b/pkg/integration/tests/main_view/unstage_diff_lines.go @@ -0,0 +1,57 @@ +package main_view + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var UnstageDiffLines = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Take a line back out of the index from the staged half of the focused main view", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(cfg *config.AppConfig) { + cfg.GetUserConfig().Gui.UseHunkModeInStagingView = false + }, + SetupRepo: func(shell *Shell) { + shell.CreateFileAndAdd("file1", "one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\n") + shell.Commit("one") + + // Two staged additions, far enough apart to be separate hunks, plus an unstaged + // one, so that the diff is split into a staged and an unstaged half. + shell.UpdateFileAndAdd("file1", "one\nSTAGED1\ntwo\nthree\nfour\nfive\nsix\nseven\nSTAGED2\neight\nnine\nten\n") + shell.UpdateFile("file1", "one\nSTAGED1\ntwo\nthree\nUNSTAGED\nfour\nfive\nsix\nseven\nSTAGED2\neight\nnine\nten\n") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsFocused(). + Lines( + Contains("MM file1").IsSelected(), + ). + Press(keys.Universal.FocusMainView) + + // The main half holds the unstaged changes; the staged ones are next door. + t.Views().Main(). + IsFocused(). + SelectedLines( + Contains("+UNSTAGED"), + ). + Press(keys.Universal.TogglePanel) + + t.Views().Secondary(). + IsFocused(). + SelectedLines( + Contains("+STAGED1"), + ). + PressPrimaryAction() + + // The line acted on is out of the index, and the one below it stays in. + t.Views().Secondary(). + Content(DoesNotContain("+STAGED1")). + ContainsLines( + Contains("+STAGED2"), + ) + t.Views().Main().ContainsLines( + Contains("+STAGED1"), + ) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index a46d8fcf4..9a1a61bd8 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -409,6 +409,10 @@ var tests = []*components.IntegrationTest{ main_view.SelectVisibleHunkOnFocusingMainView, main_view.SelectionCommandsOnlyWhereTheyApply, main_view.SelectionOverTheCustomPatch, + main_view.StageDiffLines, + main_view.StageDiffLinesOfAPathWithASpace, + main_view.StageRangeSpanningFiles, + main_view.UnstageDiffLines, misc.ConfirmOnQuit, misc.CopyConfirmationMessageToClipboard, misc.CopyToClipboard,