* [RFC PATCH 0/1] add -p: support discarding hunks
@ 2026-03-25 7:50 Luiz Campos
2026-03-25 7:50 ` [RFC PATCH 1/1] add -p: support discarding hunks with 'x' Luiz Campos
2026-03-25 18:03 ` [RFC PATCH 0/1] add -p: support discarding hunks Junio C Hamano
0 siblings, 2 replies; 10+ messages in thread
From: Luiz Campos @ 2026-03-25 7:50 UTC (permalink / raw)
To: git; +Cc: luizedc1, peff, sagotsky, Johannes.Schindelin
Hi,
This is an RFC for adding a 'discard hunk' action to `git add -p`.
Currently, when using `git add -p`, users can stage or skip hunks,
but cannot discard unwanted changes directly from the working tree.
This often leads to repeatedly skipping the same hunks across
multiple passes.
This patch introduces a new 'x' action to discard the current hunk
by reverse-applying it to the working tree.
This idea was previously discussed on the mailing list:
https://lore.kernel.org/git/X%2FiFCo0bXLR%2BLZXs@coredump.intra.peff.net/t/#m0576e6f3c6375e11cc4693b9dca3c1fc57baadd0
Open questions:
- Should discard happen immediately or be deferred until patch application?
- Are there edge cases involving overlapping hunks or edited hunks?
Feedback is very welcome.
Thanks,
Luiz
Luiz Campos (1):
[RFC PATCH 0/1] add -p: support discarding hunks with 'x'
Documentation/git-add.adoc | 7 +-
add-patch.c | 137 ++++++++++++++++++++++++++++---------
t/t3701-add-interactive.sh | 58 ++++++++++------
3 files changed, 149 insertions(+), 53 deletions(-)
--
2.43.0
^ permalink raw reply [flat|nested] 10+ messages in thread
* [RFC PATCH 1/1] add -p: support discarding hunks with 'x'
2026-03-25 7:50 [RFC PATCH 0/1] add -p: support discarding hunks Luiz Campos
@ 2026-03-25 7:50 ` Luiz Campos
2026-03-25 15:44 ` D. Ben Knoble
` (2 more replies)
2026-03-25 18:03 ` [RFC PATCH 0/1] add -p: support discarding hunks Junio C Hamano
1 sibling, 3 replies; 10+ messages in thread
From: Luiz Campos @ 2026-03-25 7:50 UTC (permalink / raw)
To: git; +Cc: luizedc1, peff, sagotsky, Johannes.Schindelin
When using `git add -p`, users can stage or skip hunks,
but cannot discard unwanted changes from the working tree.
Introduce a new 'x' action to discard the current hunk by
reverse-applying it.
This idea was suggested in a previous mailing list discussion:
https://lore.kernel.org/git/X%2FiFCo0bXLR%2BLZXs@coredump.intra.peff.net/t/#m0576e6f3c6375e11cc4693b9dca3c1fc57baadd0
Feedback is very welcome.
Signed-off-by: Luiz Campos <luizedc1@gmail.com>
---
Documentation/git-add.adoc | 7 +-
add-patch.c | 137 ++++++++++++++++++++++++++++---------
t/t3701-add-interactive.sh | 58 ++++++++++------
3 files changed, 149 insertions(+), 53 deletions(-)
diff --git a/Documentation/git-add.adoc b/Documentation/git-add.adoc
index 941135dc63..0ab81e5615 100644
--- a/Documentation/git-add.adoc
+++ b/Documentation/git-add.adoc
@@ -351,12 +351,15 @@ patch::
K - go to the previous hunk, roll over at the top
s - split the current hunk into smaller hunks
e - manually edit the current hunk
+ x - discard this hunk from the worktree
p - print the current hunk
P - print the current hunk using the pager
? - print help
+
-After deciding the fate for all hunks, if there is any hunk
-that was chosen, the index is updated with the selected hunks.
+After deciding the fate for all hunks, any hunks marked for
+discard are removed from the working tree (reverted to the index
+version for those lines). Then, if there is any hunk chosen for
+staging, the index is updated with those hunks.
+
You can omit having to type return here, by setting the configuration
variable `interactive.singleKey` to `true`.
diff --git a/add-patch.c b/add-patch.c
index 4e28e5c187..ea38ab453e 100644
--- a/add-patch.c
+++ b/add-patch.c
@@ -259,7 +259,7 @@ struct hunk_header {
struct hunk {
size_t start, end, colored_start, colored_end, splittable_into;
ssize_t delta;
- enum { UNDECIDED_HUNK = 0, SKIP_HUNK, USE_HUNK } use;
+ enum { UNDECIDED_HUNK = 0, SKIP_HUNK, USE_HUNK, DISCARD_HUNK } use;
struct hunk_header header;
};
@@ -884,17 +884,35 @@ static void render_diff_header(struct add_p_state *s,
}
}
+static bool should_merge_hunk(struct file_diff *file_diff,
+ size_t hunk_index, int use_all,
+ int merge_for_discard)
+{
+ if (use_all)
+ return true;
+ return merge_for_discard
+ ? file_diff->hunk[hunk_index].use == DISCARD_HUNK
+ : file_diff->hunk[hunk_index].use == USE_HUNK;
+}
+
+enum reassemble_mode {
+ REASSEMBLE_STAGE,
+ REASSEMBLE_DISCARD
+};
+
/* Coalesce hunks again that were split */
static int merge_hunks(struct add_p_state *s, struct file_diff *file_diff,
- size_t *hunk_index, int use_all, struct hunk *merged)
+ size_t *hunk_index, int use_all, struct hunk *merged,
+ int merge_for_discard)
{
size_t i = *hunk_index, delta;
struct hunk *hunk = file_diff->hunk + i;
/* `header` corresponds to the merged hunk */
struct hunk_header *header = &merged->header, *next;
- if (!use_all && hunk->use != USE_HUNK)
+ if (!should_merge_hunk(file_diff, *hunk_index, use_all, merge_for_discard)) {
return 0;
+ }
*merged = *hunk;
/* We simply skip the colored part (if any) when merging hunks */
@@ -907,10 +925,10 @@ static int merge_hunks(struct add_p_state *s, struct file_diff *file_diff,
/*
* Stop merging hunks when:
*
- * - the hunk is not selected for use, or
+ * - the hunk is not selected for use (or discard, when merging discards), or
* - the hunk does not overlap with the already-merged hunk(s)
*/
- if ((!use_all && hunk->use != USE_HUNK) ||
+ if (!should_merge_hunk(file_diff, i + 1, use_all, merge_for_discard) ||
header->new_offset >= next->new_offset + merged->delta ||
header->new_offset + header->new_count
< next->new_offset + merged->delta)
@@ -1014,11 +1032,13 @@ static int merge_hunks(struct add_p_state *s, struct file_diff *file_diff,
static void reassemble_patch(struct add_p_state *s,
struct file_diff *file_diff, int use_all,
+ enum reassemble_mode mode,
struct strbuf *out)
{
struct hunk *hunk;
size_t save_len = s->plain.len, i;
ssize_t delta = 0;
+ int merge_for_discard = (mode == REASSEMBLE_DISCARD);
render_diff_header(s, file_diff, 0, out);
@@ -1026,25 +1046,26 @@ static void reassemble_patch(struct add_p_state *s,
struct hunk merged = { 0 };
hunk = file_diff->hunk + i;
- if (!use_all && hunk->use != USE_HUNK)
+ if (!should_merge_hunk(file_diff, i, use_all, merge_for_discard)) {
delta += hunk->header.old_count
- hunk->header.new_count;
- else {
- /* merge overlapping hunks into a temporary hunk */
- if (merge_hunks(s, file_diff, &i, use_all, &merged))
- hunk = &merged;
+ continue;
+ }
- render_hunk(s, hunk, delta, 0, out);
+ if (merge_hunks(s, file_diff, &i, use_all, &merged,
+ merge_for_discard))
+ hunk = &merged;
- /*
- * In case `merge_hunks()` used `plain` as a scratch
- * pad (this happens when an edited hunk had to be
- * coalesced with another hunk).
- */
- strbuf_setlen(&s->plain, save_len);
+ render_hunk(s, hunk, delta, 0, out);
- delta += hunk->delta;
- }
+ /*
+ * In case `merge_hunks()` used `plain` as a scratch
+ * pad (this happens when an edited hunk had to be
+ * coalesced with another hunk).
+ */
+ strbuf_setlen(&s->plain, save_len);
+
+ delta += hunk->delta;
}
}
@@ -1348,7 +1369,7 @@ static int run_apply_check(struct add_p_state *s,
struct child_process cp = CHILD_PROCESS_INIT;
strbuf_reset(&s->buf);
- reassemble_patch(s, file_diff, 1, &s->buf);
+ reassemble_patch(s, file_diff, 1, REASSEMBLE_STAGE, &s->buf);
setup_child_process(s, &cp,
"apply", "--check", NULL);
@@ -1522,7 +1543,8 @@ static size_t display_hunks(struct add_p_state *s,
strbuf_reset(&s->buf);
strbuf_addf(&s->buf, "%c%2d: ", hunk->use == USE_HUNK ? '+'
- : hunk->use == SKIP_HUNK ? '-' : ' ',
+ : hunk->use == SKIP_HUNK ? '-'
+ : hunk->use == DISCARD_HUNK ? 'x' : ' ',
(int)start_index);
summarize_hunk(s, hunk, &s->buf);
fputs(s->buf.buf, stdout);
@@ -1540,6 +1562,7 @@ N_("j - go to the next undecided hunk, roll over at the bottom\n"
"/ - search for a hunk matching the given regex\n"
"s - split the current hunk into smaller hunks\n"
"e - manually edit the current hunk\n"
+ "x - discard this hunk from the worktree\n"
"p - print the current hunk\n"
"P - print the current hunk using the pager\n"
"> - go to the next file, roll over at the bottom\n"
@@ -1547,21 +1570,57 @@ N_("j - go to the next undecided hunk, roll over at the bottom\n"
"? - print help\n"
"HUNKS SUMMARY - Hunks: %d, USE: %d, SKIP: %d\n");
+static int apply_discard_hunks(struct add_p_state *s,
+ struct file_diff *file_diff)
+{
+ struct child_process check_cp = CHILD_PROCESS_INIT;
+ struct child_process apply_cp = CHILD_PROCESS_INIT;
+
+ strbuf_reset(&s->buf);
+ reassemble_patch(s, file_diff, 0, REASSEMBLE_DISCARD, &s->buf);
+
+ discard_index(s->index);
+
+ setup_child_process(s, &check_cp, "apply", "-R", "--check", NULL);
+ if (pipe_command(&check_cp, s->buf.buf, s->buf.len, NULL, 0, NULL, 0)) {
+ error(_("'git apply -R --check' failed"));
+ return -1;
+ }
+
+ setup_child_process(s, &apply_cp, "apply", "-R", NULL);
+ if (pipe_command(&apply_cp, s->buf.buf, s->buf.len, NULL, 0, NULL, 0)) {
+ error(_("'git apply -R' failed"));
+ return -1;
+ }
+
+ return 0;
+}
+
static void apply_patch(struct add_p_state *s, struct file_diff *file_diff)
{
struct child_process cp = CHILD_PROCESS_INIT;
size_t j;
+ int needs_refresh = 0;
+
+ if (s->mode == &patch_mode_add) {
+ for (j = 0; j < file_diff->hunk_nr; j++) {
+ if (file_diff->hunk[j].use == DISCARD_HUNK)
+ break;
+ }
+ if (j < file_diff->hunk_nr && apply_discard_hunks(s, file_diff))
+ return;
+ if (j < file_diff->hunk_nr)
+ needs_refresh = 1;
+ }
- /* Any hunk to be used? */
for (j = 0; j < file_diff->hunk_nr; j++)
if (file_diff->hunk[j].use == USE_HUNK)
break;
if (j < file_diff->hunk_nr ||
- (!file_diff->hunk_nr && file_diff->head.use == USE_HUNK)) {
- /* At least one hunk selected: apply */
+ (!file_diff->hunk_nr && file_diff->head.use == USE_HUNK)) {
strbuf_reset(&s->buf);
- reassemble_patch(s, file_diff, 0, &s->buf);
+ reassemble_patch(s, file_diff, 0, REASSEMBLE_STAGE, &s->buf);
discard_index(s->index);
if (s->mode->apply_for_checkout)
@@ -1574,13 +1633,15 @@ static void apply_patch(struct add_p_state *s, struct file_diff *file_diff)
NULL, 0, NULL, 0))
error(_("'git apply' failed"));
}
- if (read_index_from(s->index, s->index_file, s->r->gitdir) >= 0 &&
- s->index == s->r->index) {
- repo_refresh_and_write_index(s->r, REFRESH_QUIET, 0,
- 1, NULL, NULL, NULL);
- }
+ needs_refresh = 1;
}
+ if (needs_refresh &&
+ read_index_from(s->index, s->index_file, s->r->gitdir) >= 0 &&
+ s->index == s->r->index) {
+ repo_refresh_and_write_index(s->r, REFRESH_QUIET, 0,
+ 1, NULL, NULL, NULL);
+ }
}
static size_t dec_mod(size_t a, size_t m)
@@ -1636,7 +1697,8 @@ static size_t patch_update_file(struct add_p_state *s,
ALLOW_SPLIT = 1 << 5,
ALLOW_EDIT = 1 << 6,
ALLOW_GOTO_PREVIOUS_FILE = 1 << 7,
- ALLOW_GOTO_NEXT_FILE = 1 << 8
+ ALLOW_GOTO_NEXT_FILE = 1 << 8,
+ ALLOW_DISCARD = 1 << 9
} permitted = 0;
if (hunk_index >= file_diff->hunk_nr)
@@ -1722,6 +1784,10 @@ static size_t patch_update_file(struct add_p_state *s,
!file_diff->deleted) {
permitted |= ALLOW_EDIT;
strbuf_addstr(&s->buf, ",e");
+ if (s->mode == &patch_mode_add) {
+ permitted |= ALLOW_DISCARD;
+ strbuf_addstr(&s->buf, ",x");
+ }
}
if (!s->cfg.auto_advance && s->file_diff_nr > 1) {
permitted |= ALLOW_GOTO_NEXT_FILE;
@@ -1750,6 +1816,8 @@ static size_t patch_update_file(struct add_p_state *s,
if (hunk->use != UNDECIDED_HUNK) {
if (hunk->use == USE_HUNK)
hunk_use_decision = _(" (was: y)");
+ else if (hunk->use == DISCARD_HUNK)
+ hunk_use_decision = _(" (was: x)");
else
hunk_use_decision = _(" (was: n)");
}
@@ -1780,6 +1848,13 @@ static size_t patch_update_file(struct add_p_state *s,
} else if (ch == 'n') {
hunk->use = SKIP_HUNK;
goto soft_increment;
+ } else if (ch == 'x') {
+ if (!(permitted & ALLOW_DISCARD))
+ err(s, _("Sorry, cannot discard this hunk"));
+ else {
+ hunk->use = DISCARD_HUNK;
+ goto soft_increment;
+ }
} else if (ch == 'a') {
if (file_diff->hunk_nr) {
for (; hunk_index < file_diff->hunk_nr; hunk_index++) {
diff --git a/t/t3701-add-interactive.sh b/t/t3701-add-interactive.sh
index 6e120a4001..0eb813f74c 100755
--- a/t/t3701-add-interactive.sh
+++ b/t/t3701-add-interactive.sh
@@ -48,8 +48,8 @@ test_expect_success 'unknown command' '
git add -N command &&
git diff command >expect &&
cat >>expect <<-EOF &&
- (1/1) Stage addition [y,n,q,a,d,e,p,P,?]? Unknown command ${SQ}W${SQ} (use ${SQ}?${SQ} for help)
- (1/1) Stage addition [y,n,q,a,d,e,p,P,?]?$SP
+ (1/1) Stage addition [y,n,q,a,d,e,x,p,P,?]? Unknown command ${SQ}W${SQ} (use ${SQ}?${SQ} for help)
+ (1/1) Stage addition [y,n,q,a,d,e,x,p,P,?]?$SP
EOF
git add -p -- command <command >actual 2>&1 &&
test_cmp expect actual
@@ -334,7 +334,7 @@ test_expect_success 'different prompts for mode change/deleted' '
cat >expect <<-\EOF &&
(1/1) Stage deletion [y,n,q,a,d,p,P,?]?
(1/2) Stage mode change [y,n,q,a,d,k,K,j,J,g,/,p,P,?]?
- (2/2) Stage this hunk [y,n,q,a,d,K,J,g,/,e,p,P,?]?
+ (2/2) Stage this hunk [y,n,q,a,d,K,J,g,/,e,x,p,P,?]?
EOF
test_cmp expect actual.filtered
'
@@ -447,6 +447,24 @@ test_expect_success 'add first line works' '
test_cmp expected-output output
'
+test_expect_success 'add -p discard removes worktree change' '
+ test_when_finished "rm -rf discard-testrepo" &&
+ mkdir discard-testrepo &&
+ (
+ cd discard-testrepo &&
+ git init -b main &&
+ echo clean >discard-me &&
+ git add discard-me &&
+ git commit -m base &&
+ echo extra >>discard-me &&
+ test_write_lines x | git add -p discard-me &&
+ printf "clean\n" >expect &&
+ test_cmp expect discard-me &&
+ git diff --cached >tmp &&
+ test_must_be_empty tmp
+ )
+'
+
test_expect_success 'setup expected' '
cat >expected <<-\EOF
diff --git a/non-empty b/non-empty
@@ -521,13 +539,13 @@ test_expect_success 'split hunk setup' '
test_expect_success 'goto hunk 1 with "g 1"' '
test_when_finished "git reset" &&
tr _ " " >expect <<-EOF &&
- (2/2) Stage this hunk [y,n,q,a,d,K,J,g,/,e,p,P,?]? + 1: -1,2 +1,3 +15
+ (2/2) Stage this hunk [y,n,q,a,d,K,J,g,/,e,x,p,P,?]? + 1: -1,2 +1,3 +15
_ 2: -2,4 +3,8 +21
go to which hunk? @@ -1,2 +1,3 @@
_10
+15
_20
- (1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]?_
+ (1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,x,p,P,?]?_
EOF
test_write_lines s y g 1 | git add -p >actual &&
tail -n 7 <actual >actual.trimmed &&
@@ -540,7 +558,7 @@ test_expect_success 'goto hunk 1 with "g1"' '
_10
+15
_20
- (1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]?_
+ (1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,x,p,P,?]?_
EOF
test_write_lines s y g1 | git add -p >actual &&
tail -n 4 <actual >actual.trimmed &&
@@ -550,11 +568,11 @@ test_expect_success 'goto hunk 1 with "g1"' '
test_expect_success 'navigate to hunk via regex /pattern' '
test_when_finished "git reset" &&
tr _ " " >expect <<-EOF &&
- (2/2) Stage this hunk [y,n,q,a,d,K,J,g,/,e,p,P,?]? @@ -1,2 +1,3 @@
+ (2/2) Stage this hunk [y,n,q,a,d,K,J,g,/,e,x,p,P,?]? @@ -1,2 +1,3 @@
_10
+15
_20
- (1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]?_
+ (1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,x,p,P,?]?_
EOF
test_write_lines s y /1,2 | git add -p >actual &&
tail -n 5 <actual >actual.trimmed &&
@@ -567,7 +585,7 @@ test_expect_success 'navigate to hunk via regex / pattern' '
_10
+15
_20
- (1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]?_
+ (1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,x,p,P,?]?_
EOF
test_write_lines s y / 1,2 | git add -p >actual &&
tail -n 4 <actual >actual.trimmed &&
@@ -579,11 +597,11 @@ test_expect_success 'print again the hunk' '
tr _ " " >expect <<-EOF &&
+15
20
- (1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]? @@ -1,2 +1,3 @@
+ (1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,x,p,P,?]? @@ -1,2 +1,3 @@
10
+15
20
- (1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]?_
+ (1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,x,p,P,?]?_
EOF
test_write_lines s y g 1 p | git add -p >actual &&
tail -n 7 <actual >actual.trimmed &&
@@ -595,11 +613,11 @@ test_expect_success TTY 'print again the hunk (PAGER)' '
cat >expect <<-EOF &&
<GREEN>+<RESET><GREEN>15<RESET>
20<RESET>
- <BOLD;BLUE>(1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]? <RESET>PAGER <CYAN>@@ -1,2 +1,3 @@<RESET>
+ <BOLD;BLUE>(1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,x,p,P,?]? <RESET>PAGER <CYAN>@@ -1,2 +1,3 @@<RESET>
PAGER 10<RESET>
PAGER <GREEN>+<RESET><GREEN>15<RESET>
PAGER 20<RESET>
- <BOLD;BLUE>(1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]? <RESET>
+ <BOLD;BLUE>(1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,x,p,P,?]? <RESET>
EOF
test_write_lines s y g 1 P |
(
@@ -796,21 +814,21 @@ test_expect_success 'colors can be overridden' '
<BLUE>+<RESET><BLUE>new<RESET>
<CYAN> more-context<RESET>
<BLUE>+<RESET><BLUE>another-one<RESET>
- <YELLOW>(1/1) Stage this hunk [y,n,q,a,d,s,e,p,P,?]? <RESET><BOLD>Split into 2 hunks.<RESET>
+ <YELLOW>(1/1) Stage this hunk [y,n,q,a,d,s,e,x,p,P,?]? <RESET><BOLD>Split into 2 hunks.<RESET>
<MAGENTA>@@ -1,3 +1,3 @@<RESET>
<CYAN> context<RESET>
<BOLD>-old<RESET>
<BLUE>+<RESET><BLUE>new<RESET>
<CYAN> more-context<RESET>
- <YELLOW>(1/2) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]? <RESET><MAGENTA>@@ -3 +3,2 @@<RESET>
+ <YELLOW>(1/2) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,e,x,p,P,?]? <RESET><MAGENTA>@@ -3 +3,2 @@<RESET>
<CYAN> more-context<RESET>
<BLUE>+<RESET><BLUE>another-one<RESET>
- <YELLOW>(2/2) Stage this hunk [y,n,q,a,d,K,J,g,/,e,p,P,?]? <RESET><MAGENTA>@@ -1,3 +1,3 @@<RESET>
+ <YELLOW>(2/2) Stage this hunk [y,n,q,a,d,K,J,g,/,e,x,p,P,?]? <RESET><MAGENTA>@@ -1,3 +1,3 @@<RESET>
<CYAN> context<RESET>
<BOLD>-old<RESET>
<BLUE>+new<RESET>
<CYAN> more-context<RESET>
- <YELLOW>(1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]? <RESET>
+ <YELLOW>(1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,x,p,P,?]? <RESET>
EOF
test_cmp expect actual
'
@@ -1424,9 +1442,9 @@ test_expect_success 'invalid option s is rejected' '
test_write_lines j s q | git add -p >out &&
sed -ne "s/ @@.*//" -e "s/ \$//" -e "/^(/p" <out >actual &&
cat >expect <<-EOF &&
- (1/2) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,s,e,p,P,?]?
- (2/2) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]? Sorry, cannot split this hunk
- (2/2) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]?
+ (1/2) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,s,e,x,p,P,?]?
+ (2/2) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,e,x,p,P,?]? Sorry, cannot split this hunk
+ (2/2) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,e,x,p,P,?]?
EOF
test_cmp expect actual
'
--
2.43.0
^ permalink raw reply related [flat|nested] 10+ messages in thread
* Re: [RFC PATCH 1/1] add -p: support discarding hunks with 'x'
2026-03-25 7:50 ` [RFC PATCH 1/1] add -p: support discarding hunks with 'x' Luiz Campos
@ 2026-03-25 15:44 ` D. Ben Knoble
2026-03-25 17:04 ` Luiz Eduardo Campos
2026-03-25 16:24 ` Phillip Wood
2026-03-25 16:49 ` Johannes Schindelin
2 siblings, 1 reply; 10+ messages in thread
From: D. Ben Knoble @ 2026-03-25 15:44 UTC (permalink / raw)
To: Luiz Campos; +Cc: git, peff, sagotsky, Johannes.Schindelin
On Wed, Mar 25, 2026 at 3:53 AM Luiz Campos <luizedc1@gmail.com> wrote:
>
> When using `git add -p`, users can stage or skip hunks,
> but cannot discard unwanted changes from the working tree.
>
> Introduce a new 'x' action to discard the current hunk by
> reverse-applying it.
>
> This idea was suggested in a previous mailing list discussion:
> https://lore.kernel.org/git/X%2FiFCo0bXLR%2BLZXs@coredump.intra.peff.net/t/#m0576e6f3c6375e11cc4693b9dca3c1fc57baadd0
>
> Feedback is very welcome.
>
> Signed-off-by: Luiz Campos <luizedc1@gmail.com>
One feature the Fugitive Git client for Vim supports is to discard
hunks; when it does so, it also prints a message explaining how to
recover the hunk if you need it.
I think it writes the file as a blob to Git's database before
restoring from the index.
I'm not suggesting Git copy this necessarily, but we might want to
consider how to help folks when they lose a hunk they didn't mean to.
If it's never been in the index, it can be impossible to recover!
PS How different is this from "git restore -p" ?
--
D. Ben Knoble
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [RFC PATCH 1/1] add -p: support discarding hunks with 'x'
2026-03-25 7:50 ` [RFC PATCH 1/1] add -p: support discarding hunks with 'x' Luiz Campos
2026-03-25 15:44 ` D. Ben Knoble
@ 2026-03-25 16:24 ` Phillip Wood
2026-03-25 18:38 ` Luiz Eduardo Campos
2026-03-25 16:49 ` Johannes Schindelin
2 siblings, 1 reply; 10+ messages in thread
From: Phillip Wood @ 2026-03-25 16:24 UTC (permalink / raw)
To: Luiz Campos, git; +Cc: peff, sagotsky, Johannes.Schindelin
Hi Luiz
On 25/03/2026 07:50, Luiz Campos wrote:
> When using `git add -p`, users can stage or skip hunks,
> but cannot discard unwanted changes from the working tree.
>
> Introduce a new 'x' action to discard the current hunk by
> reverse-applying it.
>
> This idea was suggested in a previous mailing list discussion:
> https://lore.kernel.org/git/X%2FiFCo0bXLR%2BLZXs@coredump.intra.peff.net/t/#m0576e6f3c6375e11cc4693b9dca3c1fc57baadd0
I tend to agree with peff's comments in that thread that it is rather
unexpected for "git add" to modify the working copy. I also think that a
command that lets you stage some changes and discard others could be
useful as I do both fairly frequently from my editor. Regardless of
whether we want a new command the implementation will be similar so I've
left some comments on the code below.
> diff --git a/Documentation/git-add.adoc b/Documentation/git-add.adoc
> index 941135dc63..0ab81e5615 100644
> --- a/Documentation/git-add.adoc
> +++ b/Documentation/git-add.adoc
> @@ -351,12 +351,15 @@ patch::
> K - go to the previous hunk, roll over at the top
> s - split the current hunk into smaller hunks
> e - manually edit the current hunk
> + x - discard this hunk from the worktree
> p - print the current hunk
> P - print the current hunk using the pager
> ? - print help
> +
> -After deciding the fate for all hunks, if there is any hunk
> -that was chosen, the index is updated with the selected hunks.
> +After deciding the fate for all hunks, any hunks marked for
> +discard are removed from the working tree (reverted to the index
> +version for those lines). Then, if there is any hunk chosen for
> +staging, the index is updated with those hunks.
Makes sense.
> +
> You can omit having to type return here, by setting the configuration
> variable `interactive.singleKey` to `true`.
> diff --git a/add-patch.c b/add-patch.c
> index 4e28e5c187..ea38ab453e 100644
> --- a/add-patch.c
> +++ b/add-patch.c
> @@ -259,7 +259,7 @@ struct hunk_header {
> struct hunk {
> size_t start, end, colored_start, colored_end, splittable_into;
> ssize_t delta;
> - enum { UNDECIDED_HUNK = 0, SKIP_HUNK, USE_HUNK } use;
> + enum { UNDECIDED_HUNK = 0, SKIP_HUNK, USE_HUNK, DISCARD_HUNK } use;
> struct hunk_header header;
> };
>
> @@ -884,17 +884,35 @@ static void render_diff_header(struct add_p_state *s,
> }
> }
>
> +static bool should_merge_hunk(struct file_diff *file_diff,
> + size_t hunk_index, int use_all,
> + int merge_for_discard)
> +{
> + if (use_all)
> + return true;
If we're looking for hunks to discard then we want to return false if
all the hunks have been selected to be staged.
> + return merge_for_discard
> + ? file_diff->hunk[hunk_index].use == DISCARD_HUNK
> + : file_diff->hunk[hunk_index].use == USE_HUNK;
It would be simpler just to take USE_HUNK or DISCARD_HUNK as an argument
rather than a boolean here.
> /* Coalesce hunks again that were split */
> static int merge_hunks(struct add_p_state *s, struct file_diff *file_diff,
> - size_t *hunk_index, int use_all, struct hunk *merged)
> + size_t *hunk_index, int use_all, struct hunk *merged,
> + int merge_for_discard)
Taking the type of hunk we want to retain (USE_HUNK or DISCARD_HUNK)
would avoid having to convert merge_for_discard back into the hunk type
in should_merge_hunk().
> {
> size_t i = *hunk_index, delta;
> struct hunk *hunk = file_diff->hunk + i;
> /* `header` corresponds to the merged hunk */
> struct hunk_header *header = &merged->header, *next;
>
> - if (!use_all && hunk->use != USE_HUNK)
> + if (!should_merge_hunk(file_diff, *hunk_index, use_all, merge_for_discard)) {
> return 0;
> + }
There's no need to add braces here
> @@ -1014,11 +1032,13 @@ static int merge_hunks(struct add_p_state *s, struct file_diff *file_diff,
>
> static void reassemble_patch(struct add_p_state *s,
> struct file_diff *file_diff, int use_all,
> + enum reassemble_mode mode,
> struct strbuf *out)
> {
> struct hunk *hunk;
> size_t save_len = s->plain.len, i;
> ssize_t delta = 0;
> + int merge_for_discard = (mode == REASSEMBLE_DISCARD);
It would by simpler just to take USE_HUNK or DISCARD_HUNK as a parameter
and pass that to reassemble_patch() rather than forcing the caller to
pass an enum that we then transform to a boolean.
>
> render_diff_header(s, file_diff, 0, out);
>
> @@ -1026,25 +1046,26 @@ static void reassemble_patch(struct add_p_state *s,
> struct hunk merged = { 0 };
>
> hunk = file_diff->hunk + i;
> - if (!use_all && hunk->use != USE_HUNK)
> + if (!should_merge_hunk(file_diff, i, use_all, merge_for_discard)) {
> delta += hunk->header.old_count
> - hunk->header.new_count;
> - else {
> - /* merge overlapping hunks into a temporary hunk */
> - if (merge_hunks(s, file_diff, &i, use_all, &merged))
> - hunk = &merged;
> + continue;
I'm not sure this is an improvement - it certainly makes the patch
harder to read because you end up changing the indentation of otherwise
unchanged lines that were in the else clause.
> + }
>
> - render_hunk(s, hunk, delta, 0, out);
> + if (merge_hunks(s, file_diff, &i, use_all, &merged,
> + merge_for_discard))
> + hunk = &merged;
>
> - /*
> - * In case `merge_hunks()` used `plain` as a scratch
> - * pad (this happens when an edited hunk had to be
> - * coalesced with another hunk).
> - */
> - strbuf_setlen(&s->plain, save_len);
> + render_hunk(s, hunk, delta, 0, out);
We need to tell render_hunk whether a hunk is being applied in reverse
or not so that it knows whether to apply delta to the old offset or the
new offset. Currently it uses s->mode->reverse but that will not be
correct for the patch that discards changes from the working tree. I'm
not sure what the best way of doing that is, the simplest approach is to
invert s->mode->reverse when we want to keep hunks marked DISCARD_HUNK
and then restore the original value.
> - delta += hunk->delta;
> - }
> + /*
> + * In case `merge_hunks()` used `plain` as a scratch
> + * pad (this happens when an edited hunk had to be
> + * coalesced with another hunk).
> + */
> + strbuf_setlen(&s->plain, save_len);
> +
> + delta += hunk->delta;
This is unchanged code that re-indented because of the addition of
"continue" above.
> }
> }
> @@ -1540,6 +1562,7 @@ N_("j - go to the next undecided hunk, roll over at the bottom\n"
> "/ - search for a hunk matching the given regex\n"
> "s - split the current hunk into smaller hunks\n"
> "e - manually edit the current hunk\n"
> + "x - discard this hunk from the worktree\n"
It would be nice to avoid showing this for 'checkout -p' etc.
> "p - print the current hunk\n"
> "P - print the current hunk using the pager\n"
> "> - go to the next file, roll over at the bottom\n"
> @@ -1547,21 +1570,57 @@ N_("j - go to the next undecided hunk, roll over at the bottom\n"
> "? - print help\n"
> "HUNKS SUMMARY - Hunks: %d, USE: %d, SKIP: %d\n");
>
> +static int apply_discard_hunks(struct add_p_state *s,
> + struct file_diff *file_diff)
> +{
> + struct child_process check_cp = CHILD_PROCESS_INIT;
> + struct child_process apply_cp = CHILD_PROCESS_INIT;
> +
> + strbuf_reset(&s->buf);
> + reassemble_patch(s, file_diff, 0, REASSEMBLE_DISCARD, &s->buf);
> +
> + discard_index(s->index);
> +
> + setup_child_process(s, &check_cp, "apply", "-R", "--check", NULL);
> + if (pipe_command(&check_cp, s->buf.buf, s->buf.len, NULL, 0, NULL, 0)) {
> + error(_("'git apply -R --check' failed"));
> + return -1;
> + }
Why do we need to run "git apply --check" here?
> + setup_child_process(s, &apply_cp, "apply", "-R", NULL);
> + if (pipe_command(&apply_cp, s->buf.buf, s->buf.len, NULL, 0, NULL, 0)) {
> + error(_("'git apply -R' failed"));
> + return -1;
> + }
> +
> + return 0;
> +}
> +
> static void apply_patch(struct add_p_state *s, struct file_diff *file_diff)
> {
> struct child_process cp = CHILD_PROCESS_INIT;
> size_t j;
> + int needs_refresh = 0;
> +
> + if (s->mode == &patch_mode_add) {
> + for (j = 0; j < file_diff->hunk_nr; j++) {
> + if (file_diff->hunk[j].use == DISCARD_HUNK)
> + break;
> + }
> + if (j < file_diff->hunk_nr && apply_discard_hunks(s, file_diff))
> + return;
> + if (j < file_diff->hunk_nr)
> + needs_refresh = 1;
> + }
>
> - /* Any hunk to be used? */
Isn't this comment still relevant?
> for (j = 0; j < file_diff->hunk_nr; j++)
> if (file_diff->hunk[j].use == USE_HUNK)
> break;
>
> if (j < file_diff->hunk_nr ||
> - (!file_diff->hunk_nr && file_diff->head.use == USE_HUNK)) {
> - /* At least one hunk selected: apply */
> + (!file_diff->hunk_nr && file_diff->head.use == USE_HUNK)) {
What's the point of this change?
> strbuf_reset(&s->buf);
> - reassemble_patch(s, file_diff, 0, &s->buf);
> + reassemble_patch(s, file_diff, 0, REASSEMBLE_STAGE, &s->buf);
>
> discard_index(s->index);
> if (s->mode->apply_for_checkout)
> @@ -1574,13 +1633,15 @@ static void apply_patch(struct add_p_state *s, struct file_diff *file_diff)
> NULL, 0, NULL, 0))
> error(_("'git apply' failed"));
> }
> - if (read_index_from(s->index, s->index_file, s->r->gitdir) >= 0 &&
> - s->index == s->r->index) {
> - repo_refresh_and_write_index(s->r, REFRESH_QUIET, 0,
> - 1, NULL, NULL, NULL);
> - }
> + needs_refresh = 1;
> }
>
> + if (needs_refresh &&
> + read_index_from(s->index, s->index_file, s->r->gitdir) >= 0 &&
> + s->index == s->r->index) {
> + repo_refresh_and_write_index(s->r, REFRESH_QUIET, 0,
> + 1, NULL, NULL, NULL);
We now wait until we've applied both patches before refreshing the index
- sounds sensible.
> + }
> }
> @@ -1722,6 +1784,10 @@ static size_t patch_update_file(struct add_p_state *s,
> !file_diff->deleted) {
> permitted |= ALLOW_EDIT;
> strbuf_addstr(&s->buf, ",e");
> + if (s->mode == &patch_mode_add) {
> + permitted |= ALLOW_DISCARD;
> + strbuf_addstr(&s->buf, ",x");
> + }
So 'x' is only permitted if 'e' is what's the reason for that?
> diff --git a/t/t3701-add-interactive.sh b/t/t3701-add-interactive.sh
> [...]
> +test_expect_success 'add -p discard removes worktree change' '
> + test_when_finished "rm -rf discard-testrepo" &&
> + mkdir discard-testrepo &&
It's nice to see a test for 'x', but I'm not sure why this test needs to
be in a separate repository - why can't it use the same repository as
the other tests?
> + (
> + cd discard-testrepo &&
> + git init -b main &&
> + echo clean >discard-me &&
> + git add discard-me &&
> + git commit -m base &&
> + echo extra >>discard-me &&
> + test_write_lines x | git add -p discard-me &&
> + printf "clean\n" >expect &&
> + test_cmp expect discard-me &&
> + git diff --cached >tmp &&
> + test_must_be_empty tmp
It would be nice to see tests that split a hunk like
-a
+A
b
-c
+C
d
-e
+E
and then (1) stage the first and third sub-hunks and discard the second,
(2) discard the first and third sub-hunks and stage the second. It would
also be nice to see a test that discards a hunk with pathological
context lines - see 2bd69b9024c (add -p: fix checkout -p with
pathological context, 2019-06-12) for an example.
Thanks
Phillip
> + )
> +'
> +
> test_expect_success 'setup expected' '
> cat >expected <<-\EOF
> diff --git a/non-empty b/non-empty
> @@ -521,13 +539,13 @@ test_expect_success 'split hunk setup' '
> test_expect_success 'goto hunk 1 with "g 1"' '
> test_when_finished "git reset" &&
> tr _ " " >expect <<-EOF &&
> - (2/2) Stage this hunk [y,n,q,a,d,K,J,g,/,e,p,P,?]? + 1: -1,2 +1,3 +15
> + (2/2) Stage this hunk [y,n,q,a,d,K,J,g,/,e,x,p,P,?]? + 1: -1,2 +1,3 +15
> _ 2: -2,4 +3,8 +21
> go to which hunk? @@ -1,2 +1,3 @@
> _10
> +15
> _20
> - (1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]?_
> + (1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,x,p,P,?]?_
> EOF
> test_write_lines s y g 1 | git add -p >actual &&
> tail -n 7 <actual >actual.trimmed &&
> @@ -540,7 +558,7 @@ test_expect_success 'goto hunk 1 with "g1"' '
> _10
> +15
> _20
> - (1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]?_
> + (1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,x,p,P,?]?_
> EOF
> test_write_lines s y g1 | git add -p >actual &&
> tail -n 4 <actual >actual.trimmed &&
> @@ -550,11 +568,11 @@ test_expect_success 'goto hunk 1 with "g1"' '
> test_expect_success 'navigate to hunk via regex /pattern' '
> test_when_finished "git reset" &&
> tr _ " " >expect <<-EOF &&
> - (2/2) Stage this hunk [y,n,q,a,d,K,J,g,/,e,p,P,?]? @@ -1,2 +1,3 @@
> + (2/2) Stage this hunk [y,n,q,a,d,K,J,g,/,e,x,p,P,?]? @@ -1,2 +1,3 @@
> _10
> +15
> _20
> - (1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]?_
> + (1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,x,p,P,?]?_
> EOF
> test_write_lines s y /1,2 | git add -p >actual &&
> tail -n 5 <actual >actual.trimmed &&
> @@ -567,7 +585,7 @@ test_expect_success 'navigate to hunk via regex / pattern' '
> _10
> +15
> _20
> - (1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]?_
> + (1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,x,p,P,?]?_
> EOF
> test_write_lines s y / 1,2 | git add -p >actual &&
> tail -n 4 <actual >actual.trimmed &&
> @@ -579,11 +597,11 @@ test_expect_success 'print again the hunk' '
> tr _ " " >expect <<-EOF &&
> +15
> 20
> - (1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]? @@ -1,2 +1,3 @@
> + (1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,x,p,P,?]? @@ -1,2 +1,3 @@
> 10
> +15
> 20
> - (1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]?_
> + (1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,x,p,P,?]?_
> EOF
> test_write_lines s y g 1 p | git add -p >actual &&
> tail -n 7 <actual >actual.trimmed &&
> @@ -595,11 +613,11 @@ test_expect_success TTY 'print again the hunk (PAGER)' '
> cat >expect <<-EOF &&
> <GREEN>+<RESET><GREEN>15<RESET>
> 20<RESET>
> - <BOLD;BLUE>(1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]? <RESET>PAGER <CYAN>@@ -1,2 +1,3 @@<RESET>
> + <BOLD;BLUE>(1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,x,p,P,?]? <RESET>PAGER <CYAN>@@ -1,2 +1,3 @@<RESET>
> PAGER 10<RESET>
> PAGER <GREEN>+<RESET><GREEN>15<RESET>
> PAGER 20<RESET>
> - <BOLD;BLUE>(1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]? <RESET>
> + <BOLD;BLUE>(1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,x,p,P,?]? <RESET>
> EOF
> test_write_lines s y g 1 P |
> (
> @@ -796,21 +814,21 @@ test_expect_success 'colors can be overridden' '
> <BLUE>+<RESET><BLUE>new<RESET>
> <CYAN> more-context<RESET>
> <BLUE>+<RESET><BLUE>another-one<RESET>
> - <YELLOW>(1/1) Stage this hunk [y,n,q,a,d,s,e,p,P,?]? <RESET><BOLD>Split into 2 hunks.<RESET>
> + <YELLOW>(1/1) Stage this hunk [y,n,q,a,d,s,e,x,p,P,?]? <RESET><BOLD>Split into 2 hunks.<RESET>
> <MAGENTA>@@ -1,3 +1,3 @@<RESET>
> <CYAN> context<RESET>
> <BOLD>-old<RESET>
> <BLUE>+<RESET><BLUE>new<RESET>
> <CYAN> more-context<RESET>
> - <YELLOW>(1/2) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]? <RESET><MAGENTA>@@ -3 +3,2 @@<RESET>
> + <YELLOW>(1/2) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,e,x,p,P,?]? <RESET><MAGENTA>@@ -3 +3,2 @@<RESET>
> <CYAN> more-context<RESET>
> <BLUE>+<RESET><BLUE>another-one<RESET>
> - <YELLOW>(2/2) Stage this hunk [y,n,q,a,d,K,J,g,/,e,p,P,?]? <RESET><MAGENTA>@@ -1,3 +1,3 @@<RESET>
> + <YELLOW>(2/2) Stage this hunk [y,n,q,a,d,K,J,g,/,e,x,p,P,?]? <RESET><MAGENTA>@@ -1,3 +1,3 @@<RESET>
> <CYAN> context<RESET>
> <BOLD>-old<RESET>
> <BLUE>+new<RESET>
> <CYAN> more-context<RESET>
> - <YELLOW>(1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]? <RESET>
> + <YELLOW>(1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,x,p,P,?]? <RESET>
> EOF
> test_cmp expect actual
> '
> @@ -1424,9 +1442,9 @@ test_expect_success 'invalid option s is rejected' '
> test_write_lines j s q | git add -p >out &&
> sed -ne "s/ @@.*//" -e "s/ \$//" -e "/^(/p" <out >actual &&
> cat >expect <<-EOF &&
> - (1/2) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,s,e,p,P,?]?
> - (2/2) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]? Sorry, cannot split this hunk
> - (2/2) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]?
> + (1/2) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,s,e,x,p,P,?]?
> + (2/2) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,e,x,p,P,?]? Sorry, cannot split this hunk
> + (2/2) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,e,x,p,P,?]?
> EOF
> test_cmp expect actual
> '
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [RFC PATCH 1/1] add -p: support discarding hunks with 'x'
2026-03-25 7:50 ` [RFC PATCH 1/1] add -p: support discarding hunks with 'x' Luiz Campos
2026-03-25 15:44 ` D. Ben Knoble
2026-03-25 16:24 ` Phillip Wood
@ 2026-03-25 16:49 ` Johannes Schindelin
2026-03-25 18:58 ` Luiz Eduardo Campos
2 siblings, 1 reply; 10+ messages in thread
From: Johannes Schindelin @ 2026-03-25 16:49 UTC (permalink / raw)
To: Luiz Campos; +Cc: git, peff, sagotsky
Hi Luiz,
On Wed, 25 Mar 2026, Luiz Campos wrote:
> When using `git add -p`, users can stage or skip hunks,
> but cannot discard unwanted changes from the working tree.
>
> Introduce a new 'x' action to discard the current hunk by
> reverse-applying it.
>
> This idea was suggested in a previous mailing list discussion:
> https://lore.kernel.org/git/X%2FiFCo0bXLR%2BLZXs@coredump.intra.peff.net/t/#m0576e6f3c6375e11cc4693b9dca3c1fc57baadd0
Sounds good!
Just two minor comments (not really actionable, I think):
> @@ -1026,25 +1046,26 @@ static void reassemble_patch(struct add_p_state *s,
> struct hunk merged = { 0 };
>
> hunk = file_diff->hunk + i;
> - if (!use_all && hunk->use != USE_HUNK)
> + if (!should_merge_hunk(file_diff, i, use_all, merge_for_discard)) {
> delta += hunk->header.old_count
> - hunk->header.new_count;
> - else {
> - /* merge overlapping hunks into a temporary hunk */
> - if (merge_hunks(s, file_diff, &i, use_all, &merged))
> - hunk = &merged;
> + continue;
> + }
>
> - render_hunk(s, hunk, delta, 0, out);
> + if (merge_hunks(s, file_diff, &i, use_all, &merged,
> + merge_for_discard))
> + hunk = &merged;
>
> - /*
> - * In case `merge_hunks()` used `plain` as a scratch
> - * pad (this happens when an edited hunk had to be
> - * coalesced with another hunk).
> - */
> - strbuf_setlen(&s->plain, save_len);
> + render_hunk(s, hunk, delta, 0, out);
>
> - delta += hunk->delta;
> - }
> + /*
> + * In case `merge_hunks()` used `plain` as a scratch
> + * pad (this happens when an edited hunk had to be
> + * coalesced with another hunk).
> + */
> + strbuf_setlen(&s->plain, save_len);
> +
> + delta += hunk->delta;
This hunk is quite hard to read because of the `if ... else ...` -> `if {
... continue; } ...` change that de-indents a large chunk of code.
After pouring over the diff for a bit, I was able to convince myself that
the diff is correct.
> @@ -1547,21 +1570,57 @@ N_("j - go to the next undecided hunk, roll over at the bottom\n"
> "? - print help\n"
> "HUNKS SUMMARY - Hunks: %d, USE: %d, SKIP: %d\n");
>
> +static int apply_discard_hunks(struct add_p_state *s,
> + struct file_diff *file_diff)
> +{
> + struct child_process check_cp = CHILD_PROCESS_INIT;
> + struct child_process apply_cp = CHILD_PROCESS_INIT;
> +
> + strbuf_reset(&s->buf);
> + reassemble_patch(s, file_diff, 0, REASSEMBLE_DISCARD, &s->buf);
If you detect an empty patch here and indicate this via an early return
value, then...
> +
> + discard_index(s->index);
> +
> + setup_child_process(s, &check_cp, "apply", "-R", "--check", NULL);
> + if (pipe_command(&check_cp, s->buf.buf, s->buf.len, NULL, 0, NULL, 0)) {
> + error(_("'git apply -R --check' failed"));
> + return -1;
> + }
> +
> + setup_child_process(s, &apply_cp, "apply", "-R", NULL);
> + if (pipe_command(&apply_cp, s->buf.buf, s->buf.len, NULL, 0, NULL, 0)) {
> + error(_("'git apply -R' failed"));
> + return -1;
> + }
> +
> + return 0;
> +}
> +
> static void apply_patch(struct add_p_state *s, struct file_diff *file_diff)
> {
> struct child_process cp = CHILD_PROCESS_INIT;
> size_t j;
> + int needs_refresh = 0;
> +
> + if (s->mode == &patch_mode_add) {
> + for (j = 0; j < file_diff->hunk_nr; j++) {
> + if (file_diff->hunk[j].use == DISCARD_HUNK)
> + break;
> + }
> + if (j < file_diff->hunk_nr && apply_discard_hunks(s, file_diff))
> + return;
> + if (j < file_diff->hunk_nr)
> + needs_refresh = 1;
> + }
... then this loop is no longer necessary.
Other than that, looks good to me!
Ciao,
Johannes
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [RFC PATCH 1/1] add -p: support discarding hunks with 'x'
2026-03-25 15:44 ` D. Ben Knoble
@ 2026-03-25 17:04 ` Luiz Eduardo Campos
0 siblings, 0 replies; 10+ messages in thread
From: Luiz Eduardo Campos @ 2026-03-25 17:04 UTC (permalink / raw)
To: D. Ben Knoble; +Cc: git, peff, sagotsky, Johannes.Schindelin
Hi Ben,
Thanks for the feedback!
(Sending this message again because I had not configured my
email client to send it plain text, so I apologize if you received
duplicated content)
> One feature the Fugitive Git client for Vim supports is to discard
> hunks; when it does so, it also prints a message explaining how to
> recover the hunk if you need it.
That’s a great point. I hadn’t considered recoverability in this
initial version. Storing the discarded hunk as a blob (or otherwise
making it recoverable) seems like a useful safety measure.
> If it's never been in the index, it can be impossible to recover!
Agreed — this is probably something that should be addressed before
considering this feature complete.
> PS How different is this from "git restore -p" ?
My understanding is that `git restore -p` already allows discarding
changes interactively, but it requires a separate pass. The goal here
was to allow discarding during `git add -p`, so users can decide what
to do with each hunk in a single pass.
That said, I’m not yet sure whether integrating this into `add -p` is
the best approach, or if this should be handled differently.
Thanks again for the insights!
Luiz
Em qua., 25 de mar. de 2026 às 12:44, D. Ben Knoble
<ben.knoble@gmail.com> escreveu:
>
> On Wed, Mar 25, 2026 at 3:53 AM Luiz Campos <luizedc1@gmail.com> wrote:
> >
> > When using `git add -p`, users can stage or skip hunks,
> > but cannot discard unwanted changes from the working tree.
> >
> > Introduce a new 'x' action to discard the current hunk by
> > reverse-applying it.
> >
> > This idea was suggested in a previous mailing list discussion:
> > https://lore.kernel.org/git/X%2FiFCo0bXLR%2BLZXs@coredump.intra.peff.net/t/#m0576e6f3c6375e11cc4693b9dca3c1fc57baadd0
> >
> > Feedback is very welcome.
> >
> > Signed-off-by: Luiz Campos <luizedc1@gmail.com>
>
> One feature the Fugitive Git client for Vim supports is to discard
> hunks; when it does so, it also prints a message explaining how to
> recover the hunk if you need it.
>
> I think it writes the file as a blob to Git's database before
> restoring from the index.
>
> I'm not suggesting Git copy this necessarily, but we might want to
> consider how to help folks when they lose a hunk they didn't mean to.
> If it's never been in the index, it can be impossible to recover!
>
> PS How different is this from "git restore -p" ?
>
>
> --
> D. Ben Knoble
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [RFC PATCH 0/1] add -p: support discarding hunks
2026-03-25 7:50 [RFC PATCH 0/1] add -p: support discarding hunks Luiz Campos
2026-03-25 7:50 ` [RFC PATCH 1/1] add -p: support discarding hunks with 'x' Luiz Campos
@ 2026-03-25 18:03 ` Junio C Hamano
2026-03-25 19:22 ` Luiz Eduardo Campos
1 sibling, 1 reply; 10+ messages in thread
From: Junio C Hamano @ 2026-03-25 18:03 UTC (permalink / raw)
To: Luiz Campos; +Cc: git, peff, sagotsky, Johannes.Schindelin
Luiz Campos <luizedc1@gmail.com> writes:
> Hi,
>
> This is an RFC for adding a 'discard hunk' action to `git add -p`.
>
> Currently, when using `git add -p`, users can stage or skip hunks,
> but cannot discard unwanted changes directly from the working tree.
> This often leads to repeatedly skipping the same hunks across
> multiple passes.
>
> This patch introduces a new 'x' action to discard the current hunk
> by reverse-applying it to the working tree.
>
> This idea was previously discussed on the mailing list:
> https://lore.kernel.org/git/X%2FiFCo0bXLR%2BLZXs@coredump.intra.peff.net/t/#m0576e6f3c6375e11cc4693b9dca3c1fc57baadd0
>
> Open questions:
> - Should discard happen immediately or be deferred until patch application?
> - Are there edge cases involving overlapping hunks or edited hunks?
After reading the discussion (by the way, I do not recall seeing it,
so thank you very much for having a link to it), I agree with what
Peff said back then. "add -p" that touches the working tree feels
quite weird.
In addition to that, letting it make destructive change makes the
idea even less appetizing. Once you remove the changes introduced
by the hunk, it is forever gone. A "discard" in "add -p" would not
solve your problem without adding many unhappy users who lost their
work by mistake. I do not want to see people trigger "discard" by
mistake in "add -p" session _and_ find that there is no way to undo
that mistaken discard.
"stash -p" followed by "add -p" is probably the best we can do that
is safe. When the unwanted change is truly unwanted garbage that
you would never ever want to see again, "restore -p" followed by
"add -p" would be an alternative.
One reason why they are not satisfying is because during the later
"add -p" session, we will notice that some unwanted things we failed
to notice and get rid of (either by sending them to stash or restoring
it away) are still there, reminding us that we are imperfect human,
and at that point, it is not easy to switch back to the "stash -p"
or "restore -p" from there.
What you want is probably a _single_ command that lets you inspect
the differences among the HEAD, the index, and the working tree, and
allows you to move things hunk-by-hunk in different directions.
* You can go through the "git diff --cached" (i.e., changes already in
the index), and selectively undo/revert the changes to the index,
similar to "git reset -p".
* You can go through the "git diff" (i.e., changes between the
index and the working tree), and selectively apply the changes to
the index, similar to "git add -p".
* You can go through the "git diff HEAD" (i.e. changes since your
last commit), and selectively send the changes to a stash entry,
similar to "git stash -p". This is not destructive.
And if the single command lets you switch among working with these
modes, you no longer need to worry about forgetting to send
some changes to stash to concentrate on working on the rest.
In addition, optionally you can also have this in the same command:
* You can go through the "git diff", and selectively revert the
changes to the working tree, similar to "git restore -p".
This additional mode *is* destructive, but if you know from the hunk
that you will never need the change in it, it would be a right tool
for it.
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [RFC PATCH 1/1] add -p: support discarding hunks with 'x'
2026-03-25 16:24 ` Phillip Wood
@ 2026-03-25 18:38 ` Luiz Eduardo Campos
0 siblings, 0 replies; 10+ messages in thread
From: Luiz Eduardo Campos @ 2026-03-25 18:38 UTC (permalink / raw)
To: phillip.wood; +Cc: git, peff, sagotsky, Johannes.Schindelin
Hi Phillip,
Thanks for the thorough review.
> it is rather unexpected for "git add" to modify the working copy
I share that concern. My motivation was allowing stage-and-discard in
a single pass, which I find myself wanting regularly, and discussion in
the linked thread suggested it could live in add -p. That said, I can
see the argument that git add modifying the worktree breaks the mental
model. I'm open to discussing whether a separate command or a flag on
git restore -p would be a better home for this — the implementation
would be largely the same either way.
> It would be simpler just to take USE_HUNK or DISCARD_HUNK as an argument
Agreed. I'll drop both the merge_for_discard boolean and the enum
reassemble_mode, and instead pass the desired hunk type directly
(USE_HUNK or DISCARD_HUNK) through reassemble_patch,
merge_hunks, and should_merge_hunk. That removes the indirection
entirely.
> There's no need to add braces here
Right, the project style omits braces for single-statement blocks.
I'll drop them.
> I'm not sure this is an improvement — it certainly makes the patch harder to
> read because you end up changing the indentation of otherwise unchanged
> lines that were in the else clause.
> This is unchanged code that re-indented because of the addition of
> "continue" above.
Good point. With the simplified should_merge_hunk() taking a hunk
type, I can keep the original if / else structure in
reassemble_patch() and just swap out the condition. That avoids the
continue rewrite and the indentation churn entirely.
> We need to tell render_hunk whether a hunk is being applied in reverse or
> not so that it knows whether to apply delta to the old offset or the new offset.
You're right. I was wrong to think the delta direction would be the same as for
staging. For the discard patch, skipped hunks' changes are still present in
the worktree, so the new_offset (worktree side) is already correct and must not
be adjusted. The adjustment needs to go to old_offset instead — exactly the
is_reverse = 1 behaviour. This is consistent with patch_mode_checkout_index,
which does the same operation (diff-files + apply -R to worktree) and
sets is_reverse = 1.
I'll temporarily flip is_reverse (or pass an explicit flag to render_hunk) when
assembling the discard patch, and add the split-hunk tests below to confirm the
offsets come out correct.
> It would be nice to avoid showing this for 'checkout -p' etc.
The help text display already filters lines from
help_patch_remainder against the options present in s->buf
(the prompt string). Since 'x' is only added to the prompt in
patch_mode_add, the "x - discard..." line will already be suppressed
for checkout -p, reset -p, etc.
I'll add a comment in the next version to make that clearer, and add a
test that verifies checkout -p help output does not mention 'x'.
> Why do we need to run "git apply --check" here?
We don't — the existing staging codepath in apply_patch does not run a
separate --check before applying either; it just runs git apply
directly and checks the exit code. I'll do the same and remove the
redundant --check pass.
I'll remove the redundant check and just run git apply -R directly.
> Isn't this comment still relevant?
Yes, I'll restore it. I removed it by accident when restructuring the
function; it still applies to the USE_HUNK scan that follows.
> What's the point of this change?
There is none — it's an accidental whitespace change on the alignment
of the (!file_diff->hunk_nr continuation line. I'll drop it.
> So 'x' is only permitted if 'e' is — what's the reason for that?
The nesting was unintentional, but the conditions guarding 'e' —
hunk_index + 1 > file_diff->mode_change and
!file_diff->deleted — do also apply to 'x', since reverse-applying
a mode-change or deletion hunk is not meaningful.
However, the ADD_P_DISALLOW_EDIT flag (used by git history split)
should not gate 'x'. I'll give 'x' its own block that checks only
mode_change and deleted, independent of ALLOW_EDIT.
> I'm not sure why this test needs to be in a separate repository
The sub-repo was a workaround: the test creates a commit
(git commit -m base), and that extra commit shifted the history and
caused later tests that depend on specific HEAD state to fail.
Instead, I'll rework the test to use a file already tracked in the
shared repo, dirty it, run git add -p with 'x', and clean up with
test_when_finished. No extra commit needed.
> It would be nice to see tests that split a hunk [...] and pathological context lines
Agreed. I'll add tests that:
(1) split a three-subhunk change (-a/+A, -c/+C, -e/+E), stage the
first and third, and discard the second
(2) the inverse — discard the first and third, stage the second
(3) a pathological-context case along the lines of 2bd69b9024c
Thanks again for the review — very helpful.
Luiz
Em qua., 25 de mar. de 2026 às 13:24, Phillip Wood
<phillip.wood123@gmail.com> escreveu:
>
> Hi Luiz
>
> On 25/03/2026 07:50, Luiz Campos wrote:
> > When using `git add -p`, users can stage or skip hunks,
> > but cannot discard unwanted changes from the working tree.
> >
> > Introduce a new 'x' action to discard the current hunk by
> > reverse-applying it.
> >
> > This idea was suggested in a previous mailing list discussion:
> > https://lore.kernel.org/git/X%2FiFCo0bXLR%2BLZXs@coredump.intra.peff.net/t/#m0576e6f3c6375e11cc4693b9dca3c1fc57baadd0
>
> I tend to agree with peff's comments in that thread that it is rather
> unexpected for "git add" to modify the working copy. I also think that a
> command that lets you stage some changes and discard others could be
> useful as I do both fairly frequently from my editor. Regardless of
> whether we want a new command the implementation will be similar so I've
> left some comments on the code below.
>
> > diff --git a/Documentation/git-add.adoc b/Documentation/git-add.adoc
> > index 941135dc63..0ab81e5615 100644
> > --- a/Documentation/git-add.adoc
> > +++ b/Documentation/git-add.adoc
> > @@ -351,12 +351,15 @@ patch::
> > K - go to the previous hunk, roll over at the top
> > s - split the current hunk into smaller hunks
> > e - manually edit the current hunk
> > + x - discard this hunk from the worktree
> > p - print the current hunk
> > P - print the current hunk using the pager
> > ? - print help
> > +
> > -After deciding the fate for all hunks, if there is any hunk
> > -that was chosen, the index is updated with the selected hunks.
> > +After deciding the fate for all hunks, any hunks marked for
> > +discard are removed from the working tree (reverted to the index
> > +version for those lines). Then, if there is any hunk chosen for
> > +staging, the index is updated with those hunks.
>
> Makes sense.
>
> > +
> > You can omit having to type return here, by setting the configuration
> > variable `interactive.singleKey` to `true`.
> > diff --git a/add-patch.c b/add-patch.c
> > index 4e28e5c187..ea38ab453e 100644
> > --- a/add-patch.c
> > +++ b/add-patch.c
> > @@ -259,7 +259,7 @@ struct hunk_header {
> > struct hunk {
> > size_t start, end, colored_start, colored_end, splittable_into;
> > ssize_t delta;
> > - enum { UNDECIDED_HUNK = 0, SKIP_HUNK, USE_HUNK } use;
> > + enum { UNDECIDED_HUNK = 0, SKIP_HUNK, USE_HUNK, DISCARD_HUNK } use;
> > struct hunk_header header;
> > };
> >
> > @@ -884,17 +884,35 @@ static void render_diff_header(struct add_p_state *s,
> > }
> > }
> >
> > +static bool should_merge_hunk(struct file_diff *file_diff,
> > + size_t hunk_index, int use_all,
> > + int merge_for_discard)
> > +{
> > + if (use_all)
> > + return true;
>
> If we're looking for hunks to discard then we want to return false if
> all the hunks have been selected to be staged.
>
> > + return merge_for_discard
> > + ? file_diff->hunk[hunk_index].use == DISCARD_HUNK
> > + : file_diff->hunk[hunk_index].use == USE_HUNK;
>
> It would be simpler just to take USE_HUNK or DISCARD_HUNK as an argument
> rather than a boolean here.
>
> > /* Coalesce hunks again that were split */
> > static int merge_hunks(struct add_p_state *s, struct file_diff *file_diff,
> > - size_t *hunk_index, int use_all, struct hunk *merged)
> > + size_t *hunk_index, int use_all, struct hunk *merged,
> > + int merge_for_discard)
>
> Taking the type of hunk we want to retain (USE_HUNK or DISCARD_HUNK)
> would avoid having to convert merge_for_discard back into the hunk type
> in should_merge_hunk().
>
> > {
> > size_t i = *hunk_index, delta;
> > struct hunk *hunk = file_diff->hunk + i;
> > /* `header` corresponds to the merged hunk */
> > struct hunk_header *header = &merged->header, *next;
> >
> > - if (!use_all && hunk->use != USE_HUNK)
> > + if (!should_merge_hunk(file_diff, *hunk_index, use_all, merge_for_discard)) {
> > return 0;
> > + }
>
> There's no need to add braces here
>
> > @@ -1014,11 +1032,13 @@ static int merge_hunks(struct add_p_state *s, struct file_diff *file_diff,
> >
> > static void reassemble_patch(struct add_p_state *s,
> > struct file_diff *file_diff, int use_all,
> > + enum reassemble_mode mode,
> > struct strbuf *out)
> > {
> > struct hunk *hunk;
> > size_t save_len = s->plain.len, i;
> > ssize_t delta = 0;
> > + int merge_for_discard = (mode == REASSEMBLE_DISCARD);
>
> It would by simpler just to take USE_HUNK or DISCARD_HUNK as a parameter
> and pass that to reassemble_patch() rather than forcing the caller to
> pass an enum that we then transform to a boolean.
>
> >
> > render_diff_header(s, file_diff, 0, out);
> >
> > @@ -1026,25 +1046,26 @@ static void reassemble_patch(struct add_p_state *s,
> > struct hunk merged = { 0 };
> >
> > hunk = file_diff->hunk + i;
> > - if (!use_all && hunk->use != USE_HUNK)
> > + if (!should_merge_hunk(file_diff, i, use_all, merge_for_discard)) {
> > delta += hunk->header.old_count
> > - hunk->header.new_count;
> > - else {
> > - /* merge overlapping hunks into a temporary hunk */
> > - if (merge_hunks(s, file_diff, &i, use_all, &merged))
> > - hunk = &merged;
> > + continue;
>
> I'm not sure this is an improvement - it certainly makes the patch
> harder to read because you end up changing the indentation of otherwise
> unchanged lines that were in the else clause.
>
> > + }
> >
> > - render_hunk(s, hunk, delta, 0, out);
> > + if (merge_hunks(s, file_diff, &i, use_all, &merged,
> > + merge_for_discard))
> > + hunk = &merged;
> >
> > - /*
> > - * In case `merge_hunks()` used `plain` as a scratch
> > - * pad (this happens when an edited hunk had to be
> > - * coalesced with another hunk).
> > - */
> > - strbuf_setlen(&s->plain, save_len);
> > + render_hunk(s, hunk, delta, 0, out);
>
> We need to tell render_hunk whether a hunk is being applied in reverse
> or not so that it knows whether to apply delta to the old offset or the
> new offset. Currently it uses s->mode->reverse but that will not be
> correct for the patch that discards changes from the working tree. I'm
> not sure what the best way of doing that is, the simplest approach is to
> invert s->mode->reverse when we want to keep hunks marked DISCARD_HUNK
> and then restore the original value.
>
> > - delta += hunk->delta;
> > - }
> > + /*
> > + * In case `merge_hunks()` used `plain` as a scratch
> > + * pad (this happens when an edited hunk had to be
> > + * coalesced with another hunk).
> > + */
> > + strbuf_setlen(&s->plain, save_len);
> > +
> > + delta += hunk->delta;
>
> This is unchanged code that re-indented because of the addition of
> "continue" above.
>
> > }
> > }
>
> > @@ -1540,6 +1562,7 @@ N_("j - go to the next undecided hunk, roll over at the bottom\n"
> > "/ - search for a hunk matching the given regex\n"
> > "s - split the current hunk into smaller hunks\n"
> > "e - manually edit the current hunk\n"
> > + "x - discard this hunk from the worktree\n"
>
> It would be nice to avoid showing this for 'checkout -p' etc.
>
> > "p - print the current hunk\n"
> > "P - print the current hunk using the pager\n"
> > "> - go to the next file, roll over at the bottom\n"
> > @@ -1547,21 +1570,57 @@ N_("j - go to the next undecided hunk, roll over at the bottom\n"
> > "? - print help\n"
> > "HUNKS SUMMARY - Hunks: %d, USE: %d, SKIP: %d\n");
> >
> > +static int apply_discard_hunks(struct add_p_state *s,
> > + struct file_diff *file_diff)
> > +{
> > + struct child_process check_cp = CHILD_PROCESS_INIT;
> > + struct child_process apply_cp = CHILD_PROCESS_INIT;
> > +
> > + strbuf_reset(&s->buf);
> > + reassemble_patch(s, file_diff, 0, REASSEMBLE_DISCARD, &s->buf);
> > +
> > + discard_index(s->index);
> > +
> > + setup_child_process(s, &check_cp, "apply", "-R", "--check", NULL);
> > + if (pipe_command(&check_cp, s->buf.buf, s->buf.len, NULL, 0, NULL, 0)) {
> > + error(_("'git apply -R --check' failed"));
> > + return -1;
> > + }
>
> Why do we need to run "git apply --check" here?
>
> > + setup_child_process(s, &apply_cp, "apply", "-R", NULL);
> > + if (pipe_command(&apply_cp, s->buf.buf, s->buf.len, NULL, 0, NULL, 0)) {
> > + error(_("'git apply -R' failed"));
> > + return -1;
> > + }
> > +
> > + return 0;
> > +}
> > +
> > static void apply_patch(struct add_p_state *s, struct file_diff *file_diff)
> > {
> > struct child_process cp = CHILD_PROCESS_INIT;
> > size_t j;
> > + int needs_refresh = 0;
> > +
> > + if (s->mode == &patch_mode_add) {
> > + for (j = 0; j < file_diff->hunk_nr; j++) {
> > + if (file_diff->hunk[j].use == DISCARD_HUNK)
> > + break;
> > + }
> > + if (j < file_diff->hunk_nr && apply_discard_hunks(s, file_diff))
> > + return;
> > + if (j < file_diff->hunk_nr)
> > + needs_refresh = 1;
> > + }
> >
> > - /* Any hunk to be used? */
>
> Isn't this comment still relevant?
>
> > for (j = 0; j < file_diff->hunk_nr; j++)
> > if (file_diff->hunk[j].use == USE_HUNK)
> > break;
> >
> > if (j < file_diff->hunk_nr ||
> > - (!file_diff->hunk_nr && file_diff->head.use == USE_HUNK)) {
> > - /* At least one hunk selected: apply */
> > + (!file_diff->hunk_nr && file_diff->head.use == USE_HUNK)) {
>
> What's the point of this change?
>
> > strbuf_reset(&s->buf);
> > - reassemble_patch(s, file_diff, 0, &s->buf);
> > + reassemble_patch(s, file_diff, 0, REASSEMBLE_STAGE, &s->buf);
> >
> > discard_index(s->index);
> > if (s->mode->apply_for_checkout)
> > @@ -1574,13 +1633,15 @@ static void apply_patch(struct add_p_state *s, struct file_diff *file_diff)
> > NULL, 0, NULL, 0))
> > error(_("'git apply' failed"));
> > }
> > - if (read_index_from(s->index, s->index_file, s->r->gitdir) >= 0 &&
> > - s->index == s->r->index) {
> > - repo_refresh_and_write_index(s->r, REFRESH_QUIET, 0,
> > - 1, NULL, NULL, NULL);
> > - }
> > + needs_refresh = 1;
> > }
> >
> > + if (needs_refresh &&
> > + read_index_from(s->index, s->index_file, s->r->gitdir) >= 0 &&
> > + s->index == s->r->index) {
> > + repo_refresh_and_write_index(s->r, REFRESH_QUIET, 0,
> > + 1, NULL, NULL, NULL);
>
> We now wait until we've applied both patches before refreshing the index
> - sounds sensible.
>
> > + }
> > }
>
>
> > @@ -1722,6 +1784,10 @@ static size_t patch_update_file(struct add_p_state *s,
> > !file_diff->deleted) {
> > permitted |= ALLOW_EDIT;
> > strbuf_addstr(&s->buf, ",e");
> > + if (s->mode == &patch_mode_add) {
> > + permitted |= ALLOW_DISCARD;
> > + strbuf_addstr(&s->buf, ",x");
> > + }
>
> So 'x' is only permitted if 'e' is what's the reason for that?
> > diff --git a/t/t3701-add-interactive.sh b/t/t3701-add-interactive.sh
> > [...]
> > +test_expect_success 'add -p discard removes worktree change' '
> > + test_when_finished "rm -rf discard-testrepo" &&
> > + mkdir discard-testrepo &&
>
> It's nice to see a test for 'x', but I'm not sure why this test needs to
> be in a separate repository - why can't it use the same repository as
> the other tests?
>
> > + (
> > + cd discard-testrepo &&
> > + git init -b main &&
> > + echo clean >discard-me &&
> > + git add discard-me &&
> > + git commit -m base &&
> > + echo extra >>discard-me &&
> > + test_write_lines x | git add -p discard-me &&
> > + printf "clean\n" >expect &&
> > + test_cmp expect discard-me &&
> > + git diff --cached >tmp &&
> > + test_must_be_empty tmp
>
> It would be nice to see tests that split a hunk like
>
> -a
> +A
> b
> -c
> +C
> d
> -e
> +E
>
> and then (1) stage the first and third sub-hunks and discard the second,
> (2) discard the first and third sub-hunks and stage the second. It would
> also be nice to see a test that discards a hunk with pathological
> context lines - see 2bd69b9024c (add -p: fix checkout -p with
> pathological context, 2019-06-12) for an example.
>
> Thanks
>
> Phillip
>
>
> > + )
> > +'
> > +
> > test_expect_success 'setup expected' '
> > cat >expected <<-\EOF
> > diff --git a/non-empty b/non-empty
> > @@ -521,13 +539,13 @@ test_expect_success 'split hunk setup' '
> > test_expect_success 'goto hunk 1 with "g 1"' '
> > test_when_finished "git reset" &&
> > tr _ " " >expect <<-EOF &&
> > - (2/2) Stage this hunk [y,n,q,a,d,K,J,g,/,e,p,P,?]? + 1: -1,2 +1,3 +15
> > + (2/2) Stage this hunk [y,n,q,a,d,K,J,g,/,e,x,p,P,?]? + 1: -1,2 +1,3 +15
> > _ 2: -2,4 +3,8 +21
> > go to which hunk? @@ -1,2 +1,3 @@
> > _10
> > +15
> > _20
> > - (1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]?_
> > + (1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,x,p,P,?]?_
> > EOF
> > test_write_lines s y g 1 | git add -p >actual &&
> > tail -n 7 <actual >actual.trimmed &&
> > @@ -540,7 +558,7 @@ test_expect_success 'goto hunk 1 with "g1"' '
> > _10
> > +15
> > _20
> > - (1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]?_
> > + (1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,x,p,P,?]?_
> > EOF
> > test_write_lines s y g1 | git add -p >actual &&
> > tail -n 4 <actual >actual.trimmed &&
> > @@ -550,11 +568,11 @@ test_expect_success 'goto hunk 1 with "g1"' '
> > test_expect_success 'navigate to hunk via regex /pattern' '
> > test_when_finished "git reset" &&
> > tr _ " " >expect <<-EOF &&
> > - (2/2) Stage this hunk [y,n,q,a,d,K,J,g,/,e,p,P,?]? @@ -1,2 +1,3 @@
> > + (2/2) Stage this hunk [y,n,q,a,d,K,J,g,/,e,x,p,P,?]? @@ -1,2 +1,3 @@
> > _10
> > +15
> > _20
> > - (1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]?_
> > + (1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,x,p,P,?]?_
> > EOF
> > test_write_lines s y /1,2 | git add -p >actual &&
> > tail -n 5 <actual >actual.trimmed &&
> > @@ -567,7 +585,7 @@ test_expect_success 'navigate to hunk via regex / pattern' '
> > _10
> > +15
> > _20
> > - (1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]?_
> > + (1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,x,p,P,?]?_
> > EOF
> > test_write_lines s y / 1,2 | git add -p >actual &&
> > tail -n 4 <actual >actual.trimmed &&
> > @@ -579,11 +597,11 @@ test_expect_success 'print again the hunk' '
> > tr _ " " >expect <<-EOF &&
> > +15
> > 20
> > - (1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]? @@ -1,2 +1,3 @@
> > + (1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,x,p,P,?]? @@ -1,2 +1,3 @@
> > 10
> > +15
> > 20
> > - (1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]?_
> > + (1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,x,p,P,?]?_
> > EOF
> > test_write_lines s y g 1 p | git add -p >actual &&
> > tail -n 7 <actual >actual.trimmed &&
> > @@ -595,11 +613,11 @@ test_expect_success TTY 'print again the hunk (PAGER)' '
> > cat >expect <<-EOF &&
> > <GREEN>+<RESET><GREEN>15<RESET>
> > 20<RESET>
> > - <BOLD;BLUE>(1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]? <RESET>PAGER <CYAN>@@ -1,2 +1,3 @@<RESET>
> > + <BOLD;BLUE>(1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,x,p,P,?]? <RESET>PAGER <CYAN>@@ -1,2 +1,3 @@<RESET>
> > PAGER 10<RESET>
> > PAGER <GREEN>+<RESET><GREEN>15<RESET>
> > PAGER 20<RESET>
> > - <BOLD;BLUE>(1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]? <RESET>
> > + <BOLD;BLUE>(1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,x,p,P,?]? <RESET>
> > EOF
> > test_write_lines s y g 1 P |
> > (
> > @@ -796,21 +814,21 @@ test_expect_success 'colors can be overridden' '
> > <BLUE>+<RESET><BLUE>new<RESET>
> > <CYAN> more-context<RESET>
> > <BLUE>+<RESET><BLUE>another-one<RESET>
> > - <YELLOW>(1/1) Stage this hunk [y,n,q,a,d,s,e,p,P,?]? <RESET><BOLD>Split into 2 hunks.<RESET>
> > + <YELLOW>(1/1) Stage this hunk [y,n,q,a,d,s,e,x,p,P,?]? <RESET><BOLD>Split into 2 hunks.<RESET>
> > <MAGENTA>@@ -1,3 +1,3 @@<RESET>
> > <CYAN> context<RESET>
> > <BOLD>-old<RESET>
> > <BLUE>+<RESET><BLUE>new<RESET>
> > <CYAN> more-context<RESET>
> > - <YELLOW>(1/2) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]? <RESET><MAGENTA>@@ -3 +3,2 @@<RESET>
> > + <YELLOW>(1/2) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,e,x,p,P,?]? <RESET><MAGENTA>@@ -3 +3,2 @@<RESET>
> > <CYAN> more-context<RESET>
> > <BLUE>+<RESET><BLUE>another-one<RESET>
> > - <YELLOW>(2/2) Stage this hunk [y,n,q,a,d,K,J,g,/,e,p,P,?]? <RESET><MAGENTA>@@ -1,3 +1,3 @@<RESET>
> > + <YELLOW>(2/2) Stage this hunk [y,n,q,a,d,K,J,g,/,e,x,p,P,?]? <RESET><MAGENTA>@@ -1,3 +1,3 @@<RESET>
> > <CYAN> context<RESET>
> > <BOLD>-old<RESET>
> > <BLUE>+new<RESET>
> > <CYAN> more-context<RESET>
> > - <YELLOW>(1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]? <RESET>
> > + <YELLOW>(1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,x,p,P,?]? <RESET>
> > EOF
> > test_cmp expect actual
> > '
> > @@ -1424,9 +1442,9 @@ test_expect_success 'invalid option s is rejected' '
> > test_write_lines j s q | git add -p >out &&
> > sed -ne "s/ @@.*//" -e "s/ \$//" -e "/^(/p" <out >actual &&
> > cat >expect <<-EOF &&
> > - (1/2) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,s,e,p,P,?]?
> > - (2/2) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]? Sorry, cannot split this hunk
> > - (2/2) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]?
> > + (1/2) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,s,e,x,p,P,?]?
> > + (2/2) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,e,x,p,P,?]? Sorry, cannot split this hunk
> > + (2/2) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,e,x,p,P,?]?
> > EOF
> > test_cmp expect actual
> > '
>
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [RFC PATCH 1/1] add -p: support discarding hunks with 'x'
2026-03-25 16:49 ` Johannes Schindelin
@ 2026-03-25 18:58 ` Luiz Eduardo Campos
0 siblings, 0 replies; 10+ messages in thread
From: Luiz Eduardo Campos @ 2026-03-25 18:58 UTC (permalink / raw)
To: Johannes Schindelin; +Cc: git, peff, sagotsky
Hi Johannes,
Thank you for taking the time to review this!
> This hunk is quite hard to read because of the `if ... else ...` -> `if {
> ... continue; } ...` change that de-indents a large chunk of code.
You are right. Even though you marked it as "not really actionable",
I think it is worth fixing: I can keep the original if/else structure
and just replace the condition with the should_merge_hunk() call.
That avoids the indentation churn and keeps the diff focused on what
actually changes.
> If you detect an empty patch here and indicate this via an early return
> value, then...
> ... then this loop is no longer necessary.
Good idea. I will have apply_discard_hunks() check whether any hunk
is marked DISCARD_HUNK before going through the apply machinery, and
use the return value to distinguish "nothing to do" from "applied" and
"error". With that in place the pre-scan loop in apply_patch() can be
dropped, and needs_refresh can just be set based on whether
apply_discard_hunks() actually applied something.
I will address this in v2; the implementation might still live in add -p,
or I may fold it into a shared path that fits better (suggestions are
welcome!)
Thanks,
Luiz
Em qua., 25 de mar. de 2026 às 13:49, Johannes Schindelin
<Johannes.Schindelin@gmx.de> escreveu:
>
> Hi Luiz,
>
> On Wed, 25 Mar 2026, Luiz Campos wrote:
>
> > When using `git add -p`, users can stage or skip hunks,
> > but cannot discard unwanted changes from the working tree.
> >
> > Introduce a new 'x' action to discard the current hunk by
> > reverse-applying it.
> >
> > This idea was suggested in a previous mailing list discussion:
> > https://lore.kernel.org/git/X%2FiFCo0bXLR%2BLZXs@coredump.intra.peff.net/t/#m0576e6f3c6375e11cc4693b9dca3c1fc57baadd0
>
> Sounds good!
>
> Just two minor comments (not really actionable, I think):
>
> > @@ -1026,25 +1046,26 @@ static void reassemble_patch(struct add_p_state *s,
> > struct hunk merged = { 0 };
> >
> > hunk = file_diff->hunk + i;
> > - if (!use_all && hunk->use != USE_HUNK)
> > + if (!should_merge_hunk(file_diff, i, use_all, merge_for_discard)) {
> > delta += hunk->header.old_count
> > - hunk->header.new_count;
> > - else {
> > - /* merge overlapping hunks into a temporary hunk */
> > - if (merge_hunks(s, file_diff, &i, use_all, &merged))
> > - hunk = &merged;
> > + continue;
> > + }
> >
> > - render_hunk(s, hunk, delta, 0, out);
> > + if (merge_hunks(s, file_diff, &i, use_all, &merged,
> > + merge_for_discard))
> > + hunk = &merged;
> >
> > - /*
> > - * In case `merge_hunks()` used `plain` as a scratch
> > - * pad (this happens when an edited hunk had to be
> > - * coalesced with another hunk).
> > - */
> > - strbuf_setlen(&s->plain, save_len);
> > + render_hunk(s, hunk, delta, 0, out);
> >
> > - delta += hunk->delta;
> > - }
> > + /*
> > + * In case `merge_hunks()` used `plain` as a scratch
> > + * pad (this happens when an edited hunk had to be
> > + * coalesced with another hunk).
> > + */
> > + strbuf_setlen(&s->plain, save_len);
> > +
> > + delta += hunk->delta;
>
> This hunk is quite hard to read because of the `if ... else ...` -> `if {
> ... continue; } ...` change that de-indents a large chunk of code.
>
> After pouring over the diff for a bit, I was able to convince myself that
> the diff is correct.
>
> > @@ -1547,21 +1570,57 @@ N_("j - go to the next undecided hunk, roll over at the bottom\n"
> > "? - print help\n"
> > "HUNKS SUMMARY - Hunks: %d, USE: %d, SKIP: %d\n");
> >
> > +static int apply_discard_hunks(struct add_p_state *s,
> > + struct file_diff *file_diff)
> > +{
> > + struct child_process check_cp = CHILD_PROCESS_INIT;
> > + struct child_process apply_cp = CHILD_PROCESS_INIT;
> > +
> > + strbuf_reset(&s->buf);
> > + reassemble_patch(s, file_diff, 0, REASSEMBLE_DISCARD, &s->buf);
>
> If you detect an empty patch here and indicate this via an early return
> value, then...
>
> > +
> > + discard_index(s->index);
> > +
> > + setup_child_process(s, &check_cp, "apply", "-R", "--check", NULL);
> > + if (pipe_command(&check_cp, s->buf.buf, s->buf.len, NULL, 0, NULL, 0)) {
> > + error(_("'git apply -R --check' failed"));
> > + return -1;
> > + }
> > +
> > + setup_child_process(s, &apply_cp, "apply", "-R", NULL);
> > + if (pipe_command(&apply_cp, s->buf.buf, s->buf.len, NULL, 0, NULL, 0)) {
> > + error(_("'git apply -R' failed"));
> > + return -1;
> > + }
> > +
> > + return 0;
> > +}
> > +
> > static void apply_patch(struct add_p_state *s, struct file_diff *file_diff)
> > {
> > struct child_process cp = CHILD_PROCESS_INIT;
> > size_t j;
> > + int needs_refresh = 0;
> > +
> > + if (s->mode == &patch_mode_add) {
> > + for (j = 0; j < file_diff->hunk_nr; j++) {
> > + if (file_diff->hunk[j].use == DISCARD_HUNK)
> > + break;
> > + }
> > + if (j < file_diff->hunk_nr && apply_discard_hunks(s, file_diff))
> > + return;
> > + if (j < file_diff->hunk_nr)
> > + needs_refresh = 1;
> > + }
>
> ... then this loop is no longer necessary.
>
> Other than that, looks good to me!
>
> Ciao,
> Johannes
^ permalink raw reply [flat|nested] 10+ messages in thread
* Re: [RFC PATCH 0/1] add -p: support discarding hunks
2026-03-25 18:03 ` [RFC PATCH 0/1] add -p: support discarding hunks Junio C Hamano
@ 2026-03-25 19:22 ` Luiz Eduardo Campos
0 siblings, 0 replies; 10+ messages in thread
From: Luiz Eduardo Campos @ 2026-03-25 19:22 UTC (permalink / raw)
To: Junio C Hamano; +Cc: git, peff, sagotsky, Johannes.Schindelin
Hi Junio,
Thanks a lot for the detailed feedback.
> "add -p" that touches the working tree feels quite weird.
I understand the concern, and I agree that having `git add -p`
perform destructive changes on the working tree is a significant
departure from its current mental model.
> people trigger "discard" by mistake ... no way to undo
This is a very good point. I had been thinking of this primarily
as a convenience for workflows where users repeatedly skip hunks,
but you are right that making such an operation easy to trigger
in an interactive session could lead to accidental data loss,
and that would be problematic.
> What you want is probably a single command ...
This is a very interesting direction. My original motivation was
exactly to avoid having to switch between `git add -p`,
`git restore -p`, and `git stash -p` when reviewing changes,
but I had not considered approaching it as a single interface with
modes over the different views (HEAD, index, worktree) and
non-destructive flows like stashing.
That does seem like a more coherent model. In the meantime I will
rely on the workflows you mentioned (`stash -p` or `restore -p`
followed by `git add -p`) rather than pursuing discard within
`add -p` alone.
I will step back from the narrow "discard in add -p" RFC while I
think more about this broader direction. One question I would like
guidance on is whether something like what you describe would be
more appropriate as a new top-level command, or as an extension of
an existing one (and if so, which entry point would be the least
surprising).
If a unified tool ever includes a destructive "revert hunk in the
worktree" mode, I agree it would need to be very clearly separated
and hard to trigger by mistake.
Thanks again for the guidance; it is very helpful.
Luiz
Em qua., 25 de mar. de 2026 às 15:03, Junio C Hamano
<gitster@pobox.com> escreveu:
>
> Luiz Campos <luizedc1@gmail.com> writes:
>
> > Hi,
> >
> > This is an RFC for adding a 'discard hunk' action to `git add -p`.
> >
> > Currently, when using `git add -p`, users can stage or skip hunks,
> > but cannot discard unwanted changes directly from the working tree.
> > This often leads to repeatedly skipping the same hunks across
> > multiple passes.
> >
> > This patch introduces a new 'x' action to discard the current hunk
> > by reverse-applying it to the working tree.
> >
> > This idea was previously discussed on the mailing list:
> > https://lore.kernel.org/git/X%2FiFCo0bXLR%2BLZXs@coredump.intra.peff.net/t/#m0576e6f3c6375e11cc4693b9dca3c1fc57baadd0
> >
> > Open questions:
> > - Should discard happen immediately or be deferred until patch application?
> > - Are there edge cases involving overlapping hunks or edited hunks?
>
> After reading the discussion (by the way, I do not recall seeing it,
> so thank you very much for having a link to it), I agree with what
> Peff said back then. "add -p" that touches the working tree feels
> quite weird.
>
> In addition to that, letting it make destructive change makes the
> idea even less appetizing. Once you remove the changes introduced
> by the hunk, it is forever gone. A "discard" in "add -p" would not
> solve your problem without adding many unhappy users who lost their
> work by mistake. I do not want to see people trigger "discard" by
> mistake in "add -p" session _and_ find that there is no way to undo
> that mistaken discard.
>
> "stash -p" followed by "add -p" is probably the best we can do that
> is safe. When the unwanted change is truly unwanted garbage that
> you would never ever want to see again, "restore -p" followed by
> "add -p" would be an alternative.
>
> One reason why they are not satisfying is because during the later
> "add -p" session, we will notice that some unwanted things we failed
> to notice and get rid of (either by sending them to stash or restoring
> it away) are still there, reminding us that we are imperfect human,
> and at that point, it is not easy to switch back to the "stash -p"
> or "restore -p" from there.
>
> What you want is probably a _single_ command that lets you inspect
> the differences among the HEAD, the index, and the working tree, and
> allows you to move things hunk-by-hunk in different directions.
>
> * You can go through the "git diff --cached" (i.e., changes already in
> the index), and selectively undo/revert the changes to the index,
> similar to "git reset -p".
>
> * You can go through the "git diff" (i.e., changes between the
> index and the working tree), and selectively apply the changes to
> the index, similar to "git add -p".
>
> * You can go through the "git diff HEAD" (i.e. changes since your
> last commit), and selectively send the changes to a stash entry,
> similar to "git stash -p". This is not destructive.
>
> And if the single command lets you switch among working with these
> modes, you no longer need to worry about forgetting to send
> some changes to stash to concentrate on working on the rest.
>
> In addition, optionally you can also have this in the same command:
>
> * You can go through the "git diff", and selectively revert the
> changes to the working tree, similar to "git restore -p".
>
> This additional mode *is* destructive, but if you know from the hunk
> that you will never need the change in it, it would be a right tool
> for it.
^ permalink raw reply [flat|nested] 10+ messages in thread
end of thread, other threads:[~2026-03-25 19:23 UTC | newest]
Thread overview: 10+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-03-25 7:50 [RFC PATCH 0/1] add -p: support discarding hunks Luiz Campos
2026-03-25 7:50 ` [RFC PATCH 1/1] add -p: support discarding hunks with 'x' Luiz Campos
2026-03-25 15:44 ` D. Ben Knoble
2026-03-25 17:04 ` Luiz Eduardo Campos
2026-03-25 16:24 ` Phillip Wood
2026-03-25 18:38 ` Luiz Eduardo Campos
2026-03-25 16:49 ` Johannes Schindelin
2026-03-25 18:58 ` Luiz Eduardo Campos
2026-03-25 18:03 ` [RFC PATCH 0/1] add -p: support discarding hunks Junio C Hamano
2026-03-25 19:22 ` Luiz Eduardo Campos
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox