Allow removing lines from patch directly

I always press 'd' in the patch building view, expecting that I can do
exactly what I can do in the staging view, to find out I need to go
space -> ctrl+p -> d and I think it's time to honour the muscle memory
and support this convenience keybinding.
This commit is contained in:
Jesse Duffield 2026-02-08 09:04:26 +11:00
parent 3b677cbcfe
commit 5f8bca6508
13 changed files with 175 additions and 0 deletions

View file

@ -237,6 +237,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` o `` | Open file | Open file in default application. |
| `` e `` | Edit file | Open file in external editor. |
| `` <space> `` | Toggle lines in patch | |
| `` d `` | Remove lines from commit | Remove the selected lines from this commit. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes these lines. |
| `` <esc> `` | Exit custom patch builder | |
| `` / `` | Search the current view by text | |

View file

@ -278,6 +278,7 @@ _凡例`c-b` はctrl+b、`a-b` はalt+b、`B` はshift+bを意味
| `` o `` | ファイルを開く | デフォルトのアプリケーションでファイルを開きます。 |
| `` e `` | ファイルを編集 | 外部エディタでファイルを開きます。 |
| `` <space> `` | パッチ内の行を切り替え | |
| `` d `` | Remove lines from commit | Remove the selected lines from this commit. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes these lines. |
| `` <esc> `` | カスタムパッチビルダーを終了 | |
| `` / `` | 現在のビューをテキストで検索 | |

View file

@ -178,6 +178,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` o `` | 파일 닫기 | Open file in default application. |
| `` e `` | 파일 편집 | Open file in external editor. |
| `` <space> `` | Line(s)을 패치에 추가/삭제 | |
| `` d `` | Remove lines from commit | Remove the selected lines from this commit. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes these lines. |
| `` <esc> `` | Exit custom patch builder | |
| `` / `` | 검색 시작 | |

View file

@ -245,6 +245,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` o `` | Open bestand | Open file in default application. |
| `` e `` | Verander bestand | Open file in external editor. |
| `` <space> `` | Voeg toe/verwijder lijn(en) in patch | |
| `` d `` | Remove lines from commit | Remove the selected lines from this commit. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes these lines. |
| `` <esc> `` | Sluit lijn-bij-lijn modus | |
| `` / `` | Start met zoeken | |

View file

@ -124,6 +124,7 @@ _Legenda: `<c-b>` oznacza ctrl+b, `<a-b>` oznacza alt+b, `B` oznacza shift+b_
| `` o `` | Otwórz plik | Otwórz plik w domyślnej aplikacji. |
| `` e `` | Edytuj plik | Otwórz plik w zewnętrznym edytorze. |
| `` <space> `` | Przełącz linie w łatce | |
| `` d `` | Remove lines from commit | Remove the selected lines from this commit. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes these lines. |
| `` <esc> `` | Wyjdź z budowniczego niestandardowej łatki | |
| `` / `` | Szukaj w bieżącym widoku po tekście | |

View file

@ -295,6 +295,7 @@ _Legend: `<c-b>` means ctrl+b, `<a-b>` means alt+b, `B` means shift+b_
| `` o `` | Abrir arquivo | Abrir arquivo no aplicativo padrão. |
| `` e `` | Editar arquivo | Abrir arquivo no editor externo. |
| `` <space> `` | Alternar linhas no caminho | |
| `` d `` | Remove lines from commit | Remove the selected lines from this commit. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes these lines. |
| `` <esc> `` | Sair do construtor de patch personalizado | |
| `` / `` | Search the current view by text | |

View file

@ -138,6 +138,7 @@ _Связки клавиш_
| `` o `` | Открыть файл | Open file in default application. |
| `` e `` | Редактировать файл | Open file in external editor. |
| `` <space> `` | Добавить/удалить строку(и) для патча | |
| `` d `` | Remove lines from commit | Remove the selected lines from this commit. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes these lines. |
| `` <esc> `` | Выйти из сборщика пользовательских патчей | |
| `` / `` | Найти | |

View file

@ -260,6 +260,7 @@ _图例`<c-b>` 意味着ctrl+b, `<a-b>意味着Alt+b, `B` 意味着shift+b_
| `` o `` | 打开文件 | 使用默认程序打开该文件 |
| `` e `` | 编辑文件 | 使用外部编辑器打开文件 |
| `` <space> `` | 添加/移除 行到补丁 | |
| `` d `` | Remove lines from commit | Remove the selected lines from this commit. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes these lines. |
| `` <esc> `` | 退出逐行模式 | |
| `` / `` | 开始搜索 | |

View file

@ -72,6 +72,7 @@ _說明`<c-b>` 表示 CtrlB、`<a-b>` 表示 AltB`B`表示 ShiftB
| `` o `` | 開啟檔案 | 使用預設軟體開啟 |
| `` e `` | 編輯檔案 | 使用外部編輯器開啟 |
| `` <space> `` | 向 (或從) 補丁中添加/刪除行 | |
| `` d `` | Remove lines from commit | Remove the selected lines from this commit. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes these lines. |
| `` <esc> `` | 退出自訂補丁建立器 | |
| `` / `` | 搜尋 | |

View file

@ -1,7 +1,10 @@
package controllers
import (
"fmt"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/samber/lo"
)
@ -42,6 +45,14 @@ func (self *PatchBuildingController) GetKeybindings(opts types.KeybindingsOpts)
Description: self.c.Tr.ToggleSelectionForPatch,
DisplayOnScreen: true,
},
{
Key: opts.GetKey(opts.Config.Universal.Remove),
Handler: self.DiscardSelection,
GetDisabledReason: self.getDisabledReasonForDiscard,
Description: self.c.Tr.RemoveSelectionFromPatch,
Tooltip: self.c.Tr.RemoveSelectionFromPatchTooltip,
DisplayOnScreen: true,
},
{
Key: opts.GetKey(opts.Config.Universal.Return),
Handler: self.Escape,
@ -168,6 +179,83 @@ func (self *PatchBuildingController) toggleSelection() error {
return nil
}
func (self *PatchBuildingController) getDisabledReasonForDiscard() *types.DisabledReason {
if !self.c.Git().Patch.PatchBuilder.CanRebase {
return &types.DisabledReason{Text: self.c.Tr.CanOnlyRemoveLinesFromLocalCommits}
}
if !self.c.Git().Patch.PatchBuilder.IsEmpty() {
return &types.DisabledReason{Text: self.c.Tr.MustClearPatchBeforeRemovingLines}
}
return nil
}
func (self *PatchBuildingController) DiscardSelection() error {
if self.c.UserConfig().Git.DiffContextSize == 0 {
return fmt.Errorf(self.c.Tr.Actions.NotEnoughContextToRemoveLines,
keybindings.Label(self.c.UserConfig().Keybinding.Universal.IncreaseContextInDiffView))
}
if ok, err := self.c.Helpers().PatchBuilding.ValidateNormalWorkingTreeState(); !ok {
return err
}
self.c.Confirm(types.ConfirmOpts{
Title: self.c.Tr.RemoveLinesFromCommitTitle,
Prompt: self.c.Tr.RemoveLinesFromCommitPrompt,
HandleConfirm: func() error {
return self.removeSelectionFromCommit()
},
})
return nil
}
func (self *PatchBuildingController) addSelectionToPatch() error {
self.context().GetMutex().Lock()
defer self.context().GetMutex().Unlock()
filename := self.c.Contexts().CommitFiles.GetSelectedPath()
if filename == "" {
return nil
}
state := self.context().GetState()
lineIndicesToToggle := state.LineIndicesOfAddedOrDeletedLinesInSelectedPatchRange()
if len(lineIndicesToToggle) == 0 {
return nil
}
return self.c.Git().Patch.PatchBuilder.AddFileLineRange(filename, lineIndicesToToggle)
}
func (self *PatchBuildingController) removeSelectionFromCommit() error {
if err := self.addSelectionToPatch(); err != nil {
return err
}
if self.c.Git().Patch.PatchBuilder.IsEmpty() {
return nil
}
self.c.Helpers().PatchBuilding.Escape()
return self.c.WithWaitingStatus(self.c.Tr.RebasingStatus, func(gocui.Task) error {
commitIndex := self.getPatchCommitIndex()
self.c.LogAction(self.c.Tr.Actions.RemovePatchFromCommit)
err := self.c.Git().Patch.DeletePatchesFromCommit(self.c.Model().Commits, commitIndex)
return self.c.Helpers().MergeAndRebase.CheckMergeOrRebase(err)
})
}
func (self *PatchBuildingController) getPatchCommitIndex() int {
for index, commit := range self.c.Model().Commits {
if commit.Hash() == self.c.Git().Patch.PatchBuilder.To {
return index
}
}
return -1
}
func (self *PatchBuildingController) Escape() error {
context := self.c.Contexts().CustomPatchBuilder
state := context.GetState()

View file

@ -300,6 +300,8 @@ type TranslationSet struct {
ToggleSelectHunkTooltip string
HunkStagingHint string
ToggleSelectionForPatch string
RemoveSelectionFromPatch string
RemoveSelectionFromPatchTooltip string
EditHunk string
EditHunkTooltip string
ToggleStagingView string
@ -434,6 +436,8 @@ type TranslationSet struct {
CheckoutCommitFileTooltip string
CannotCheckoutWithModifiedFilesErr string
CanOnlyDiscardFromLocalCommits string
CanOnlyRemoveLinesFromLocalCommits string
MustClearPatchBeforeRemovingLines string
Remove string
DiscardOldFileChangeTooltip string
DiscardFileChangesTitle string
@ -688,6 +692,8 @@ type TranslationSet struct {
BranchUnknown string
DiscardChangeTitle string
DiscardChangePrompt string
RemoveLinesFromCommitTitle string
RemoveLinesFromCommitPrompt string
CreateNewBranchFromCommit string
BuildingPatch string
ViewCommits string
@ -1011,6 +1017,7 @@ type Actions struct {
ResolveConflictByDeletingFile string
NotEnoughContextToStage string
NotEnoughContextToDiscard string
NotEnoughContextToRemoveLines string
NotEnoughContextForCustomPatch string
IgnoreExcludeFile string
IgnoreFileErr string
@ -1409,6 +1416,8 @@ func EnglishTranslationSet() *TranslationSet {
ToggleSelectHunkTooltip: "Toggle line-by-line vs. hunk selection mode.",
HunkStagingHint: englishHunkStagingHint,
ToggleSelectionForPatch: `Toggle lines in patch`,
RemoveSelectionFromPatch: `Remove lines from commit`,
RemoveSelectionFromPatchTooltip: "Remove the selected lines from this commit. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes these lines.",
EditHunk: `Edit hunk`,
EditHunkTooltip: "Edit selected hunk in external editor.",
ToggleStagingView: "Switch view",
@ -1546,6 +1555,8 @@ func EnglishTranslationSet() *TranslationSet {
CheckoutCommitFileTooltip: "Checkout file. This replaces the file in your working tree with the version from the selected commit.",
CannotCheckoutWithModifiedFilesErr: "You have local modifications for the file(s) you are trying to check out. You need to stash or discard these first.",
CanOnlyDiscardFromLocalCommits: "Changes can only be discarded from local commits",
CanOnlyRemoveLinesFromLocalCommits: "Lines can only be removed from local commits",
MustClearPatchBeforeRemovingLines: "Clear the current custom patch first before removing lines from the commit",
Remove: "Remove",
DiscardOldFileChangeTooltip: "Discard this commit's changes to this file. This runs an interactive rebase in the background, so you may get a merge conflict if a later commit also changes this file.",
DiscardFileChangesTitle: "Discard file changes",
@ -1800,6 +1811,8 @@ func EnglishTranslationSet() *TranslationSet {
BranchUnknown: "Branch unknown",
DiscardChangeTitle: "Discard change",
DiscardChangePrompt: "Are you sure you want to discard this change (git reset)? It is irreversible.\nTo disable this dialogue set the config key of 'gui.skipDiscardChangeWarning' to true",
RemoveLinesFromCommitTitle: "Remove lines from commit",
RemoveLinesFromCommitPrompt: "Are you sure you want to remove the selected lines from this commit?",
CreateNewBranchFromCommit: "Create new branch off of commit",
BuildingPatch: "Building patch",
ViewCommits: "View commits",
@ -2083,6 +2096,7 @@ func EnglishTranslationSet() *TranslationSet {
ResolveConflictByDeletingFile: "Resolve by deleting file",
NotEnoughContextToStage: "Staging or unstaging changes is not possible with a diff context size of 0. Increase the context using '%s'.",
NotEnoughContextToDiscard: "Discarding changes is not possible with a diff context size of 0. Increase the context using '%s'.",
NotEnoughContextToRemoveLines: "Removing lines from a commit is not possible with a diff context size of 0. Increase the context using '%s'.",
NotEnoughContextForCustomPatch: "Creating custom patches is not possible with a diff context size of 0. Increase the context using '%s'.",
IgnoreExcludeFile: "Ignore or exclude file",
IgnoreFileErr: "Cannot ignore .gitignore",

View file

@ -0,0 +1,63 @@
package patch_building
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var RemoveLinesFromCommit = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Remove specific lines from a commit using the 'd' shortcut in the patch building view",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {},
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"),
).
PressEnter()
t.Views().CommitFiles().
IsFocused().
Lines(
Contains("A file1").IsSelected(),
).
PressEnter()
// Select the second line (+2nd line) and press 'd' to remove it
t.Views().PatchBuilding().
IsFocused().
SelectNextItem().
SelectedLines(
Contains("+2nd line"),
).
Press(keys.Universal.Remove)
t.ExpectPopup().Confirmation().
Title(Equals("Remove lines from commit")).
Content(Equals("Are you sure you want to remove the selected lines from this commit?")).
Confirm()
// After the rebase, we should be back at the commit files view
// and the commit should now only contain the 1st and 3rd lines
t.Views().CommitFiles().
IsFocused().
Lines(
Contains("A file1").IsSelected(),
).
PressEscape()
t.Views().Main().ContainsLines(
Equals("+1st line"),
Equals("+3rd line"),
)
},
})

View file

@ -348,6 +348,7 @@ var tests = []*components.IntegrationTest{
patch_building.MoveToNewCommitInLastCommitOfStackedBranch,
patch_building.MoveToNewCommitPartialHunk,
patch_building.RemoveFromCommit,
patch_building.RemoveLinesFromCommit,
patch_building.RemovePartsOfAddedFile,
patch_building.ResetWithEscape,
patch_building.SelectAllFiles,