diff --git a/docs-master/keybindings/Keybindings_en.md b/docs-master/keybindings/Keybindings_en.md index c361cefa0..57a34d9ef 100644 --- a/docs-master/keybindings/Keybindings_en.md +++ b/docs-master/keybindings/Keybindings_en.md @@ -225,6 +225,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Switch view | Switch to other view (staged/unstaged changes). | | `` `` | Exit back to side panel | | | `` `` | Stage | Toggle selection staged / unstaged. | +| `` d `` | Discard | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. | | `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. | | `` v `` | Toggle range select | | | `` , h `` | Go to previous hunk | | @@ -337,6 +338,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Switch view | Switch to other view (staged/unstaged changes). | | `` `` | Exit back to side panel | | | `` `` | Stage | Toggle selection staged / unstaged. | +| `` d `` | Discard | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. | | `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. | | `` v `` | Toggle range select | | | `` , h `` | Go to previous hunk | | diff --git a/docs-master/keybindings/Keybindings_ja.md b/docs-master/keybindings/Keybindings_ja.md index ec6b53dee..6c2c9f3ce 100644 --- a/docs-master/keybindings/Keybindings_ja.md +++ b/docs-master/keybindings/Keybindings_ja.md @@ -194,6 +194,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | ビューを切り替え | 他のビュー(ステージされた変更/ステージされていない変更)に切り替えます。 | | `` `` | サイドパネルに戻る | | | `` `` | ステージ | 選択された部分のステージ / アンステージを切り替えます。 | +| `` d `` | 破棄 | ステージされていない変更が選択されている場合、`git reset`を使用して変更を破棄します。ステージされた変更が選択されている場合、変更をアンステージします。 | | `` a `` | ハンクの選択を切り替える | Toggle line-by-line vs. hunk selection mode. | | `` v `` | 範囲選択を切り替え | | | `` , h `` | 前のハンクに移動 | | @@ -314,6 +315,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | ビューを切り替え | 他のビュー(ステージされた変更/ステージされていない変更)に切り替えます。 | | `` `` | サイドパネルに戻る | | | `` `` | ステージ | 選択された部分のステージ / アンステージを切り替えます。 | +| `` d `` | 破棄 | ステージされていない変更が選択されている場合、`git reset`を使用して変更を破棄します。ステージされた変更が選択されている場合、変更をアンステージします。 | | `` a `` | ハンクの選択を切り替える | Toggle line-by-line vs. hunk selection mode. | | `` v `` | 範囲選択を切り替え | | | `` , h `` | 前のハンクに移動 | | diff --git a/docs-master/keybindings/Keybindings_ko.md b/docs-master/keybindings/Keybindings_ko.md index 0aff8c9d0..85376e1e8 100644 --- a/docs-master/keybindings/Keybindings_ko.md +++ b/docs-master/keybindings/Keybindings_ko.md @@ -86,6 +86,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | 패널 전환 | Switch to other view (staged/unstaged changes). | | `` `` | Exit back to side panel | | | `` `` | Staged 전환 | 선택한 행을 staged / unstaged | +| `` d `` | 변경을 삭제 (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. | | `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. | | `` v `` | 드래그 선택 전환 | | | `` , h `` | 이전 hunk를 선택 | | @@ -171,6 +172,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | 패널 전환 | Switch to other view (staged/unstaged changes). | | `` `` | Exit back to side panel | | | `` `` | Staged 전환 | 선택한 행을 staged / unstaged | +| `` d `` | 변경을 삭제 (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. | | `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. | | `` v `` | 드래그 선택 전환 | | | `` , h `` | 이전 hunk를 선택 | | diff --git a/docs-master/keybindings/Keybindings_nl.md b/docs-master/keybindings/Keybindings_nl.md index 78897cde3..a94e77195 100644 --- a/docs-master/keybindings/Keybindings_nl.md +++ b/docs-master/keybindings/Keybindings_nl.md @@ -233,6 +233,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Ga naar een ander paneel | Switch to other view (staged/unstaged changes). | | `` `` | Exit back to side panel | | | `` `` | Toggle staged | Toggle lijnen staged / unstaged | +| `` d `` | Verwijdert change (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. | | `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. | | `` v `` | Toggle drag selecteer | | | `` , h `` | Selecteer de vorige hunk | | @@ -315,6 +316,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Ga naar een ander paneel | Switch to other view (staged/unstaged changes). | | `` `` | Exit back to side panel | | | `` `` | Toggle staged | Toggle lijnen staged / unstaged | +| `` d `` | Verwijdert change (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. | | `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. | | `` v `` | Toggle drag selecteer | | | `` , h `` | Selecteer de vorige hunk | | diff --git a/docs-master/keybindings/Keybindings_pl.md b/docs-master/keybindings/Keybindings_pl.md index 4d53a81c5..3e1647e48 100644 --- a/docs-master/keybindings/Keybindings_pl.md +++ b/docs-master/keybindings/Keybindings_pl.md @@ -101,6 +101,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Przełącz widok | Przełącz na inny widok (zatwierdzone/niezatwierdzone zmiany). | | `` `` | Exit back to side panel | | | `` `` | Zatwierdź | Przełącz zaznaczenie zatwierdzone/niezatwierdzone. | +| `` d `` | Odrzuć | Gdy zaznaczona jest niezatwierdzona zmiana, odrzuć ją używając `git reset`. Gdy zaznaczona jest zatwierdzona zmiana, cofnij zatwierdzenie. | | `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. | | `` v `` | Przełącz zaznaczenie zakresu | | | `` , h `` | Idź do poprzedniego fragmentu | | @@ -210,6 +211,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Przełącz widok | Przełącz na inny widok (zatwierdzone/niezatwierdzone zmiany). | | `` `` | Exit back to side panel | | | `` `` | Zatwierdź | Przełącz zaznaczenie zatwierdzone/niezatwierdzone. | +| `` d `` | Odrzuć | Gdy zaznaczona jest niezatwierdzona zmiana, odrzuć ją używając `git reset`. Gdy zaznaczona jest zatwierdzona zmiana, cofnij zatwierdzenie. | | `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. | | `` v `` | Przełącz zaznaczenie zakresu | | | `` , h `` | Idź do poprzedniego fragmentu | | diff --git a/docs-master/keybindings/Keybindings_pt.md b/docs-master/keybindings/Keybindings_pt.md index c4916921a..ba4bda91e 100644 --- a/docs-master/keybindings/Keybindings_pt.md +++ b/docs-master/keybindings/Keybindings_pt.md @@ -237,6 +237,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Mudar de visão | Alternar para outra visão (staged/não processadas alterações). | | `` `` | Exit back to side panel | | | `` `` | Etapa | Ativar/desativar seleção em staged/unstaged | +| `` d `` | Descartar | Quando a mudança não desejada for selecionada, descarte a mudança usando `git reset`. Quando a mudança em fase é selecionada, despare a mudança. | | `` a `` | Toggle hunk selection | Ativa/desativa modo linha por linha vs. modo de seleção por partes. | | `` v `` | Toggle range select | | | `` , h `` | Ir para o local anterior | | @@ -346,6 +347,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Mudar de visão | Alternar para outra visão (staged/não processadas alterações). | | `` `` | Exit back to side panel | | | `` `` | Etapa | Ativar/desativar seleção em staged/unstaged | +| `` d `` | Descartar | Quando a mudança não desejada for selecionada, descarte a mudança usando `git reset`. Quando a mudança em fase é selecionada, despare a mudança. | | `` a `` | Toggle hunk selection | Ativa/desativa modo linha por linha vs. modo de seleção por partes. | | `` v `` | Toggle range select | | | `` , h `` | Ir para o local anterior | | diff --git a/docs-master/keybindings/Keybindings_ru.md b/docs-master/keybindings/Keybindings_ru.md index 4b4b9e38e..a1b18494c 100644 --- a/docs-master/keybindings/Keybindings_ru.md +++ b/docs-master/keybindings/Keybindings_ru.md @@ -76,6 +76,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Переключиться на другую панель (проиндексированные/непроиндексированные изменения) | Switch to other view (staged/unstaged changes). | | `` `` | Exit back to side panel | | | `` `` | Переключить индекс | Переключить строку в проиндексированные / непроиндексированные | +| `` d `` | Отменить изменение (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. | | `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. | | `` v `` | Переключить выборку перетаскивания | | | `` , h `` | Выбрать предыдущую часть | | @@ -115,6 +116,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | Переключиться на другую панель (проиндексированные/непроиндексированные изменения) | Switch to other view (staged/unstaged changes). | | `` `` | Exit back to side panel | | | `` `` | Переключить индекс | Переключить строку в проиндексированные / непроиндексированные | +| `` d `` | Отменить изменение (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. | | `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. | | `` v `` | Переключить выборку перетаскивания | | | `` , h `` | Выбрать предыдущую часть | | diff --git a/docs-master/keybindings/Keybindings_zh-CN.md b/docs-master/keybindings/Keybindings_zh-CN.md index 1869450b2..175d6afd2 100644 --- a/docs-master/keybindings/Keybindings_zh-CN.md +++ b/docs-master/keybindings/Keybindings_zh-CN.md @@ -288,6 +288,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | 切换到其他面板 | 切换到其他视图(已暂存/未暂存的变更) | | `` `` | 退出回到侧边面板 | | | `` `` | 切换暂存状态 | 切换行暂存状态 | +| `` d `` | 取消变更(git reset) | 当选择未暂存的变更时,使用git reset丢弃该变更。当选择已暂存的变更时,取消暂存该变更 | | `` a `` | 切换代码块选择 | 切换逐行选择与代码块选择模式。 | | `` v `` | 切换拖动选择 | | | `` , h `` | 选择上一个区块 | | @@ -343,6 +344,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | 切换到其他面板 | 切换到其他视图(已暂存/未暂存的变更) | | `` `` | 退出回到侧边面板 | | | `` `` | 切换暂存状态 | 切换行暂存状态 | +| `` d `` | 取消变更(git reset) | 当选择未暂存的变更时,使用git reset丢弃该变更。当选择已暂存的变更时,取消暂存该变更 | | `` a `` | 切换代码块选择 | 切换逐行选择与代码块选择模式。 | | `` v `` | 切换拖动选择 | | | `` , h `` | 选择上一个区块 | | diff --git a/docs-master/keybindings/Keybindings_zh-TW.md b/docs-master/keybindings/Keybindings_zh-TW.md index 31eee2750..08a000577 100644 --- a/docs-master/keybindings/Keybindings_zh-TW.md +++ b/docs-master/keybindings/Keybindings_zh-TW.md @@ -84,6 +84,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | 切換至另一個面板 (已預存/未預存更改) | Switch to other view (staged/unstaged changes). | | `` `` | Exit back to side panel | | | `` `` | 切換預存 | 切換現有行的狀態 (已預存/未預存) | +| `` d `` | 刪除變更 (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. | | `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. | | `` v `` | 切換拖曳選擇 | | | `` , h `` | 選擇上一段 | | @@ -372,6 +373,7 @@ _This file is auto-generated. To update, make the changes in the pkg/i18n direct | `` `` | 切換至另一個面板 (已預存/未預存更改) | Switch to other view (staged/unstaged changes). | | `` `` | Exit back to side panel | | | `` `` | 切換預存 | 切換現有行的狀態 (已預存/未預存) | +| `` d `` | 刪除變更 (git reset) | When unstaged change is selected, discard the change using `git reset`. When staged change is selected, unstage the change. | | `` a `` | Toggle hunk selection | Toggle line-by-line vs. hunk selection mode. | | `` v `` | 切換拖曳選擇 | | | `` , h `` | 選擇上一段 | | diff --git a/pkg/gui/controllers/commits_files_controller.go b/pkg/gui/controllers/commits_files_controller.go index 5154e9581..0435b183b 100644 --- a/pkg/gui/controllers/commits_files_controller.go +++ b/pkg/gui/controllers/commits_files_controller.go @@ -610,6 +610,18 @@ func (self *CommitFilesController) PrimaryAction(mainViewName string, firstLineI }) } +// DiscardSelection removes the selected diff line(s) from the commit the focused main +// view shows the files of, via a rebase. Same target derivation as the patch toggle. +func (self *CommitFilesController) DiscardSelection(mainViewName string, firstLineIdx int, lastLineIdx int) error { + from, to, reverse := self.c.Helpers().CommitFiles.CurrentFromToReverseForPatchBuilding() + canRebase := self.context().GetCanRebase() + return discardSelectionFromCommit(self.c, mainViewName, firstLineIdx, lastLineIdx, from, to, reverse, canRebase) +} + +func (self *CommitFilesController) DiscardSelectionDisabledReason() *types.DisabledReason { + return discardFromCommitDisabledReason(self.c, self.context().GetCanRebase()) +} + // pathsForDiff returns the file paths to use for a diff command. When a text // filter is active and the node is a directory, only the visible (filtered) // file paths are returned so the diff reflects what the user sees. diff --git a/pkg/gui/controllers/files_controller.go b/pkg/gui/controllers/files_controller.go index 021e0567b..10c480a0e 100644 --- a/pkg/gui/controllers/files_controller.go +++ b/pkg/gui/controllers/files_controller.go @@ -467,23 +467,78 @@ func (self *FilesController) PrimaryAction(mainViewName string, firstLineIdx int self.c.UserConfig().Keybinding.Universal.IncreaseContextInDiffView) } + infos, onStagedSide, ok := self.diffLineSelection(mainViewName, firstLineIdx, lastLineIdx) + if !ok { + return nil + } + + // The whole diff shown in the main view is on one side; space unstages when that's + // the staged side (reverse), otherwise it stages. Either way it applies to the index. + return self.applyDiffLineSelection(mainViewName, firstLineIdx, infos, onStagedSide, + git_commands.ApplyPatchOpts{Reverse: onStagedSide, Cached: true}) +} + +// DiscardSelectionDisabledReason: discarding from the working tree is always available +// (a zero-context diff is reported as an error from DiscardSelection itself, matching the +// staging view). +func (self *FilesController) DiscardSelectionDisabledReason() *types.DisabledReason { + return nil +} + +func (self *FilesController) DiscardSelection(mainViewName string, firstLineIdx int, lastLineIdx int) error { + if self.c.UserConfig().Git.DiffContextSize == 0 { + return fmt.Errorf(self.c.Tr.Actions.NotEnoughContextToDiscard, + self.c.UserConfig().Keybinding.Universal.IncreaseContextInDiffView) + } + + infos, onStagedSide, ok := self.diffLineSelection(mainViewName, firstLineIdx, lastLineIdx) + if !ok { + return nil + } + + // Discarding always reverses the change. On the staged side that's just unstaging + // (apply reverse to the index) — same as space there — so it follows the identical + // path and isn't destructive, hence no confirm. On the unstaged side it removes the + // change from the working tree (reverse, not cached), which is destructive. + return self.c.ConfirmIf(!onStagedSide && !self.c.UserConfig().Gui.SkipDiscardChangeWarning, + types.ConfirmOpts{ + Title: self.c.Tr.DiscardChangeTitle, + Prompt: self.c.Tr.DiscardChangePrompt, + HandleConfirm: func() error { + return self.applyDiffLineSelection(mainViewName, firstLineIdx, infos, onStagedSide, + git_commands.ApplyPatchOpts{Reverse: true, Cached: onStagedSide}) + }, + }) +} + +// diffLineSelection resolves the change lines the user has selected in the focused main +// view and which side of the diff they're on. onStagedSide is true when the diff shown +// is the staged side — the secondary half of a split, or the main half of an only-staged +// file; it's the same for every file in a multi-file (directory) diff. ok is false when +// there's no file or the selection covers no change lines (the caller does nothing). +func (self *FilesController) diffLineSelection(mainViewName string, firstLineIdx int, lastLineIdx int) (infos []types.DiffLineInfo, onStagedSide bool, ok bool) { node := self.context().GetSelected() if node == nil { - return nil + return nil, false, false } - infos := self.c.Helpers().Staging.ChangeLinesInViewRange(mainViewName, firstLineIdx, lastLineIdx) + infos = self.c.Helpers().Staging.ChangeLinesInViewRange(mainViewName, firstLineIdx, lastLineIdx) if len(infos) == 0 { - return nil + return nil, false, false } - // The whole diff shown in the main view is on one side — the staged diff in - // the secondary half of a split, and in the main half when there are only - // staged changes; in those cases space unstages, otherwise it stages. The - // direction is the same for every file in a multi-file (directory) diff. _, mainShowsStaged := self.diffSplitState(node) - reverse := mainShowsStaged || mainViewName == self.c.Contexts().NormalSecondary.GetViewName() + onStagedSide = mainShowsStaged || mainViewName == self.c.Contexts().NormalSecondary.GetViewName() + return infos, onStagedSide, true +} +// applyDiffLineSelection applies the selected change lines (grouped per file for a +// multi-file diff) with opts, then re-establishes the selection and focus the way the +// staging view does — shared by the primary action (stage/unstage) and discard, since +// discarding on the staged side is just unstaging and must follow the identical path. +// onStagedSide names which diff each file's patch is read from and drives the focus- +// follow; opts says how to apply it. +func (self *FilesController) applyDiffLineSelection(mainViewName string, firstLineIdx int, infos []types.DiffLineInfo, onStagedSide bool, opts git_commands.ApplyPatchOpts) error { // A directory diff spans several files; group the selected change lines by // file and apply one patch per file. infosByFile := lo.GroupBy(infos, func(info types.DiffLineInfo) string { return info.Path }) @@ -494,24 +549,21 @@ func (self *FilesController) PrimaryAction(mainViewName string, firstLineIdx int if file == nil { continue } - // Staging reads the side being acted on (unstaged when staging, staged when - // unstaging — both = reverse here) and applies to the index either way. - if err := self.applyDiffLines(file, fileInfos, reverse, git_commands.ApplyPatchOpts{Reverse: reverse, Cached: true}); err != nil { + if err := self.applyDiffLines(file, fileInfos, onStagedSide, opts); err != nil { return err } } self.c.Refresh(types.RefreshOptions{Scope: []types.RefreshableView{types.FILES, types.STAGING}}) - // Focus follows the side that was acted on. Staging keeps it in the main half - // (which always holds the unstaged side, or the staged side once the file has - // only staged changes). Unstaging keeps it on the staged side, which lives in - // the secondary half once the file is split into staged + unstaged, and moves - // back to the main half when the staged side empties and the split collapses. - // The model is up to date now (Refresh above is synchronous), so the post-op - // split is read from the freshly selected node. + // Focus follows the side that was acted on. The unstaged side stays in the main half + // (which always holds the unstaged side, or the staged side once the file has only + // staged changes). The staged side lives in the secondary half once the file is split + // into staged + unstaged, and moves back to the main half when the staged side empties + // and the split collapses. The model is up to date now (Refresh above is synchronous), + // so the post-op split is read from the freshly selected node. focusViewName := self.c.Contexts().Normal.GetViewName() - if reverse { + if onStagedSide { if node := self.context().GetSelected(); node != nil { if split, _ := self.diffSplitState(node); split { focusViewName = self.c.Contexts().NormalSecondary.GetViewName() @@ -519,9 +571,9 @@ func (self *FilesController) PrimaryAction(mainViewName string, firstLineIdx int } } - // The staging Refresh above queued the main-view re-render; re-establish the - // selection in whichever pane now holds the acted-on side once that render lands, - // and focus that pane if staging moved it there. + // The Refresh above queued the main-view re-render; re-establish the selection at its + // change-line ordinal once that render lands, and focus the pane now holding the + // acted-on side. revealSelectionAfterPrimaryAction(self.c, mainViewName, focusViewName, firstLineIdx) if focusViewName != mainViewName { self.c.Context().Push(mainContextForViewName(self.c, focusViewName), types.OnFocusOpts{}) diff --git a/pkg/gui/controllers/main_view_controller.go b/pkg/gui/controllers/main_view_controller.go index a60333e0f..71b3d24f4 100644 --- a/pkg/gui/controllers/main_view_controller.go +++ b/pkg/gui/controllers/main_view_controller.go @@ -79,6 +79,14 @@ func (self *MainViewController) GetKeybindings(opts types.KeybindingsOpts) []*ty Tooltip: self.c.Tr.StageSelectionTooltip, DisplayOnScreen: selectionShown, }, + { + Keys: opts.GetKeys(opts.Config.Universal.Remove), + Handler: self.discardSelection, + GetDisabledReason: self.discardSelectionDisabledReason, + Description: self.c.Tr.DiscardSelection, + Tooltip: self.c.Tr.DiscardSelectionTooltip, + DisplayOnScreen: selectionShown, + }, { Keys: opts.GetKeys(opts.Config.Universal.GoInto), Handler: self.enter, @@ -306,6 +314,29 @@ func (self *MainViewController) primaryAction() error { return actions.PrimaryAction(self.context.GetViewName(), first, last) } +// discardSelection discards the selected diff line(s), delegating to the panel beneath +// (working-tree discard for files, line-removal from the commit for the commit panels). +// As with the primary action, the handler does its own re-render. A no-op when the panel +// beneath offers no actions; discardSelectionDisabledReason gates the cases where the +// panel has discard but it isn't applicable right now. +func (self *MainViewController) discardSelection() error { + actions := self.focusedMainViewActions() + if actions == nil { + return nil + } + v := self.context.GetView() + first, last := v.SelectedLineRange() + return actions.DiscardSelection(self.context.GetViewName(), first, last) +} + +func (self *MainViewController) discardSelectionDisabledReason() *types.DisabledReason { + actions := self.focusedMainViewActions() + if actions == nil { + return nil + } + return actions.DiscardSelectionDisabledReason() +} + // revealSelectionAfterPrimaryAction re-establishes the focused-main-view selection after a // primary action (staging or a patch toggle) re-renders the diff. The selection's // change-line ordinal is read from the source pane (still showing the pre-action diff diff --git a/pkg/gui/controllers/patch_building_from_main_view.go b/pkg/gui/controllers/patch_building_from_main_view.go index 525df27ff..d66dbdb5a 100644 --- a/pkg/gui/controllers/patch_building_from_main_view.go +++ b/pkg/gui/controllers/patch_building_from_main_view.go @@ -1,9 +1,11 @@ package controllers import ( + "fmt" "path/filepath" "github.com/jesseduffield/lazygit/pkg/commands/patch" + "github.com/jesseduffield/lazygit/pkg/gocui" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/samber/lo" ) @@ -128,6 +130,102 @@ func togglePatchLines(c *ControllerCommon, infos []types.DiffLineInfo) error { return nil } +// discardFromCommitDisabledReason reports why discarding lines from the commit shown in +// the focused main view is unavailable, or nil when it's available. It mirrors the patch +// builder's own guard: the diff must be a local commit's (canRebase), no rebase may be in +// progress, and the diff context size must be non-zero (a patch can't be built from a +// zero-context diff). Stash and sub-commits of another branch are never rebaseable, so +// they always get the local-commits reason. +func discardFromCommitDisabledReason(c *ControllerCommon, canRebase bool) *types.DisabledReason { + if !canRebase { + return &types.DisabledReason{Text: c.Tr.CanOnlyDiscardFromLocalCommits} + } + if c.Git().Status.WorkingTreeState().Any() { + return &types.DisabledReason{Text: c.Tr.CantPatchWhileRebasingError} + } + if c.UserConfig().Git.DiffContextSize == 0 { + return &types.DisabledReason{Text: fmt.Sprintf(c.Tr.Actions.NotEnoughContextToRemoveLines, + c.UserConfig().Keybinding.Universal.IncreaseContextInDiffView)} + } + return nil +} + +// discardSelectionFromCommit discards the selected diff line(s) — a single line, a +// range, or a hunk — from the commit whose diff the focused main view shows, by building +// a one-off patch from the selection and removing it from the commit via a rebase. It is +// the patch-building counterpart of the working-tree discard the files panel does, and +// mirrors the patch builder's own "discard lines from commit": any in-progress custom +// patch is reset first (the confirm prompt warns when one exists), then a fresh patch +// holding exactly the selection is built for (from, to, reverse) and removed from the +// commit. The caller has already established (via canRebase) that the commit is on a +// local branch; the target commit is the one identified by `to`. +func discardSelectionFromCommit( + c *ControllerCommon, + mainViewName string, + firstLineIdx int, + lastLineIdx int, + from string, + to string, + reverse bool, + canRebase bool, +) error { + infos := c.Helpers().Staging.ChangeLinesInViewRange(mainViewName, firstLineIdx, lastLineIdx) + if len(infos) == 0 { + return nil + } + + commitIndex := -1 + for i, commit := range c.Model().Commits { + if commit.Hash() == to { + commitIndex = i + break + } + } + if commitIndex == -1 { + return nil + } + + patchBuilder := c.Git().Patch.PatchBuilder + prompt := lo.Ternary(patchBuilder.Active(), + c.Tr.DiscardLinesFromCommitPromptWithReset, + c.Tr.DiscardLinesFromCommitPrompt) + + c.Confirm(types.ConfirmOpts{ + Title: c.Tr.DiscardLinesFromCommitTitle, + Prompt: prompt, + HandleConfirm: func() error { + // Build a fresh patch holding exactly the selection: reset any active patch, + // then add the selected lines (togglePatchLines adds them, the patch being empty). + if patchBuilder.Active() { + patchBuilder.Reset() + } + patchBuilder.Start(from, to, reverse, canRebase) + if err := togglePatchLines(c, infos); err != nil { + return err + } + if patchBuilder.IsEmpty() { + return nil + } + + return c.WithWaitingStatusBlockingInput(types.WaitingStatusOpts{ + Message: c.Tr.RebasingStatus, + HideWorkingTreeState: true, + }, func(gocui.Task) error { + c.LogAction(c.Tr.Actions.RemovePatchFromCommit) + err := c.Git().Patch.DeletePatchesFromCommit(c.Model().Commits, commitIndex) + // The rebase removes the selected lines, so the commit's diff shrinks + // there. Re-establish the selection at its change-line ordinal once the + // refresh below re-renders the diff, advancing to the next surviving change + // just like staging — installed before the refresh so the re-render rides + // it. The diff stays in the same pane, so source and target are the same. + revealSelectionAfterPrimaryAction(c, mainViewName, mainViewName, firstLineIdx) + return c.Helpers().MergeAndRebase.CheckMergeOrRebase(err) + }) + }, + }) + return nil +} + // patchFilename maps a diff line's absolute path to the key the patch builder stores // the file under — its repo-relative, slash-separated path. func patchFilename(c *ControllerCommon, absPath string) string { diff --git a/pkg/gui/controllers/switch_to_diff_files_controller.go b/pkg/gui/controllers/switch_to_diff_files_controller.go index 2fc533317..28e0594e2 100644 --- a/pkg/gui/controllers/switch_to_diff_files_controller.go +++ b/pkg/gui/controllers/switch_to_diff_files_controller.go @@ -190,6 +190,32 @@ func (self *SwitchToDiffFilesController) PrimaryAction(mainViewName string, firs }) } +// DiscardSelection removes the selected line(s) of the whole-commit diff from the commit, +// via a rebase. Same target derivation as the patch toggle; only enabled for local +// commits (see DiscardSelectionDisabledReason), so stash and other-branch sub-commits +// don't reach the rebase. +func (self *SwitchToDiffFilesController) DiscardSelection(mainViewName string, firstLineIdx int, lastLineIdx int) error { + ref := self.context.GetSelectedRef() + if ref == nil { + return nil + } + refsRange := self.context.GetSelectedRefRangeForDiffFiles() + + from, to := context.FromAndToForDiff(ref, refsRange) + from, reverse := self.c.Modes().Diffing.GetFromAndReverseArgsForDiff(from) + canRebase := self.canRebase(ref, refsRange) + + return discardSelectionFromCommit(self.c, mainViewName, firstLineIdx, lastLineIdx, from, to, reverse, canRebase) +} + +func (self *SwitchToDiffFilesController) DiscardSelectionDisabledReason() *types.DisabledReason { + canRebase := false + if ref := self.context.GetSelectedRef(); ref != nil { + canRebase = self.canRebase(ref, self.context.GetSelectedRefRangeForDiffFiles()) + } + return discardFromCommitDisabledReason(self.c, canRebase) +} + func (self *SwitchToDiffFilesController) canEnter() *types.DisabledReason { refRange := self.context.GetSelectedRefRangeForDiffFiles() if refRange != nil { diff --git a/pkg/gui/types/context.go b/pkg/gui/types/context.go index b7fbdb1d2..e8196cea6 100644 --- a/pkg/gui/types/context.go +++ b/pkg/gui/types/context.go @@ -373,6 +373,14 @@ type FocusedMainViewActions interface { // the selection itself (staging can move the acted-on side to the other pane, // which the handler then focuses). PrimaryAction(mainViewName string, firstLineIdx int, lastLineIdx int) error + + // DiscardSelection discards the selected diff line(s) when the user presses the + // remove key: from the working tree for the files panel, or from the commit (via a + // rebase) for the commit panels. DiscardSelectionDisabledReason reports why discard + // is unavailable here — e.g. the diff isn't a local commit's, or a rebase is in + // progress — or nil when it's available. + DiscardSelection(mainViewName string, firstLineIdx int, lastLineIdx int) error + DiscardSelectionDisabledReason() *DisabledReason } type IController interface { diff --git a/pkg/integration/tests/patch_building/discard_lines_from_commit_main_view.go b/pkg/integration/tests/patch_building/discard_lines_from_commit_main_view.go new file mode 100644 index 000000000..9d90093f1 --- /dev/null +++ b/pkg/integration/tests/patch_building/discard_lines_from_commit_main_view.go @@ -0,0 +1,62 @@ +package patch_building + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var DiscardLinesFromCommitMainView = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Discard a line from a local commit straight from the focused main view, without entering the commit files", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) { + config.GetUserConfig().Gui.UseHunkModeInStagingView = false + }, + SetupRepo: func(shell *Shell) { + shell.EmptyCommit("first commit") + + shell.CreateFileAndAdd("file1", "1st line\n2nd line\n3rd line\n") + shell.Commit("commit to remove from") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + Focus(). + Lines( + Contains("commit to remove from").IsSelected(), + Contains("first commit"), + ). + // Focus the commit's diff straight from the commits panel, rather than + // entering the commit files panel first. + Press(keys.Universal.FocusMainView) + + t.Views().Main(). + IsFocused(). + SelectedLines( + Contains("+1st line"), + ). + // Move down to the second line and discard just that one from the commit. + Press(keys.Universal.NextItem). + SelectedLines( + Contains("+2nd line"), + ). + Press(keys.Universal.Remove) + + t.ExpectPopup().Confirmation(). + Title(Equals("Discard lines from commit")). + Content(Equals("Are you sure you want to discard the selected lines from this commit?")). + Confirm() + + // After the rebase the commit keeps only the 1st and 3rd lines, and the selection + // advances to the next surviving change (the 3rd line, now at the discarded line's + // ordinal) rather than staying stale — like staging or discarding in the files panel. + t.Views().Main(). + ContainsLines( + Equals("+1st line"), + Equals("+3rd line"), + ). + Content(DoesNotContain("2nd line")). + SelectedLines( + Contains("+3rd line"), + ) + }, +}) diff --git a/pkg/integration/tests/staging/discard_from_main_view.go b/pkg/integration/tests/staging/discard_from_main_view.go new file mode 100644 index 000000000..b68011dba --- /dev/null +++ b/pkg/integration/tests/staging/discard_from_main_view.go @@ -0,0 +1,58 @@ +package staging + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var DiscardFromMainView = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Discard a hunk from the working tree in the focused main view, without diving into the staging view", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) { + config.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 separate change blocks, far enough apart to stay distinct hunks. + shell.UpdateFile("file1", "one\ntwo\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) + + t.Views().Main(). + IsFocused(). + SelectedLines( + Contains("-three"), + ). + // `a` extends the selection to the whole change block around the cursor. + Press(keys.Main.ToggleSelectHunk). + SelectedLines( + Contains("-three"), + Contains("+THREE"), + ). + Press(keys.Universal.Remove) + + t.ExpectPopup().Confirmation(). + Title(Equals("Discard change")). + Content(Contains("Are you sure you want to discard this change")). + Confirm() + + // The first block was discarded from the working tree, leaving line 3 at its + // committed value; the other block stays. + t.Views().Main(). + IsFocused(). + Content(DoesNotContain("THREE")). + ContainsLines( + Contains("-nine"), + Contains("+NINE"), + ) + }, +}) diff --git a/pkg/integration/tests/staging/discard_from_staged_main_view.go b/pkg/integration/tests/staging/discard_from_staged_main_view.go new file mode 100644 index 000000000..27e8de652 --- /dev/null +++ b/pkg/integration/tests/staging/discard_from_staged_main_view.go @@ -0,0 +1,57 @@ +package staging + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var DiscardFromStagedMainView = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Discarding a hunk on the staged half of the focused main view just unstages it, advancing the selection to the next staged hunk like unstaging does", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(config *config.AppConfig) { + config.GetUserConfig().Gui.UseHunkModeInStagingView = true + }, + SetupRepo: func(shell *Shell) { + shell.CreateFileAndAdd("file1", "one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\nnine\nten\neleven\n") + shell.Commit("one") + + // Two staged hunks... + shell.UpdateFileAndAdd("file1", "one\ntwo\nTHREE\nfour\nfive\nsix\nseven\neight\nNINE\nten\neleven\n") + // ...plus an unstaged change, so the main view splits into staged/unstaged. + shell.UpdateFile("file1", "one\ntwo\nTHREE\nfour\nfive\nSIX\nseven\neight\nNINE\nten\neleven\n") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Files(). + IsFocused(). + Press(keys.Universal.FocusMainView) + + // The unstaged half is focused first; switch to the staged half. + t.Views().Main(). + IsFocused(). + Press(keys.Universal.TogglePanel) + + t.Views().Secondary(). + IsFocused(). + SelectedLines( + Contains("-three"), + Contains("+THREE"), + ). + // Discarding on the staged side is just unstaging — no discard confirm — so the + // selection stays in the staged half and advances to the next staged hunk + // rather than getting lost or jumping to the unstaged half. + Press(keys.Universal.Remove) + + t.Views().Secondary(). + IsFocused(). + SelectedLines( + Contains("-nine"), + Contains("+NINE"), + ) + + // The unstaged change is untouched. + t.Views().Main(). + Content(Contains("-six")). + Content(Contains("+SIX")) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 7df2f6c41..8142f74b2 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -366,6 +366,7 @@ var tests = []*components.IntegrationTest{ patch_building.BuildMultiFileFromWholeCommitMainView, patch_building.CopyRenamedFileDiff, patch_building.DiscardLinesFromCommit, + patch_building.DiscardLinesFromCommitMainView, patch_building.EditLineInPatchBuildingPanel, patch_building.MoveRangeToIndex, patch_building.MoveToEarlierCommit, @@ -414,6 +415,8 @@ var tests = []*components.IntegrationTest{ staging.DiffChangeScreenMode, staging.DiffContextChange, staging.DiscardAllChanges, + staging.DiscardFromMainView, + staging.DiscardFromStagedMainView, staging.FocusFollowsStagedSideToSecondaryAfterUnstaging, staging.FocusReturnsToMainAfterUnstagingLastStagedHunk, staging.Search,