From: Javier Bassi <javierbassi@gmail.com>
To: git@vger.kernel.org
Cc: Junio C Hamano <gitster@pobox.com>,
Abraham Samuel Adekunle <abrahamadekunle50@gmail.com>,
Rene Scharfe <l.s.r@web.de>, Elijah Newren <newren@gmail.com>,
Ruben Justo <rjusto@gmail.com>, Patrick Steinhardt <ps@pks.im>,
Javier Bassi <javierbassi@gmail.com>
Subject: [PATCH] add -p: introduce 'w' command to view hunk with --word-diff
Date: Wed, 6 May 2026 20:54:59 -0300 [thread overview]
Message-ID: <20260506235459.529862-1-javierbassi@gmail.com> (raw)
When using `git add --patch`, reviewing changes in long lines can be
difficult with the default line-based diff. This is particularly
noticeable in formats such as JSONP, CSV, LaTeX, Markdown, or other
plain text where small inline edits can be hard to spot.
Added `w - print the current hunk with word-diff` during hunk selection
to re-display the current hunk using `--word-diff`. This provides a
clearer inline view of changes without modifying the hunk or affecting
how patches are applied or staged.
Signed-off-by: Javier Bassi <javierbassi@gmail.com>
---
Documentation/git-add.adoc | 1 +
add-patch.c | 105 ++++++++++++++++++++++++++++++++++++-
t/t3701-add-interactive.sh | 59 +++++++++++++--------
3 files changed, 142 insertions(+), 23 deletions(-)
diff --git a/Documentation/git-add.adoc b/Documentation/git-add.adoc
index 941135dc63..d2ee1cf9a9 100644
--- a/Documentation/git-add.adoc
+++ b/Documentation/git-add.adoc
@@ -351,6 +351,7 @@ 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
+ w - print the current hunk with word-diff
p - print the current hunk
P - print the current hunk using the pager
? - print help
diff --git a/add-patch.c b/add-patch.c
index f27edcbe8d..0364f4bc97 100644
--- a/add-patch.c
+++ b/add-patch.c
@@ -7,6 +7,7 @@
#include "commit.h"
#include "config.h"
#include "diff.h"
+#include "diffcore.h"
#include "editor.h"
#include "environment.h"
#include "gettext.h"
@@ -1508,6 +1509,105 @@ static void summarize_hunk(struct add_p_state *s, struct hunk *hunk,
strbuf_complete_line(out);
}
+static void trim_trailing_lf(struct strbuf *buf)
+{
+ if (buf->len && buf->buf[buf->len - 1] == '\n')
+ strbuf_setlen(buf, buf->len - 1);
+}
+
+static void add_word_diff_line(struct strbuf *old, struct strbuf *new,
+ const char *line, size_t len, char marker)
+{
+ if (marker == '-' || marker == '+' || *line == ' ') {
+ line++;
+ len--;
+ }
+
+ if (marker != '+')
+ strbuf_add(old, line, len);
+ if (marker != '-')
+ strbuf_add(new, line, len);
+}
+
+static void build_word_diff_files(struct add_p_state *s, struct hunk *hunk,
+ struct strbuf *old, struct strbuf *new)
+{
+ size_t i;
+ char last_marker = '\0';
+
+ for (i = hunk->start; i < hunk->end; i = find_next_line(&s->plain, i)) {
+ size_t next = find_next_line(&s->plain, i);
+ char marker = normalize_marker(s->plain.buf + i);
+
+ if (marker == '\\') {
+ if (last_marker != '+')
+ trim_trailing_lf(old);
+ if (last_marker != '-')
+ trim_trailing_lf(new);
+ continue;
+ }
+
+ if (marker != ' ' && marker != '-' && marker != '+')
+ BUG("unhandled diff marker: '%c'", marker);
+
+ add_word_diff_line(old, new, s->plain.buf + i, next - i,
+ marker);
+ last_marker = marker;
+ }
+}
+
+static struct diff_filespec *word_diff_filespec(struct repository *r,
+ const char *name,
+ struct strbuf *buf)
+{
+ struct diff_filespec *spec = alloc_filespec(name);
+ size_t size;
+
+ fill_filespec(spec, null_oid(r->hash_algo), 0, 0100644);
+ spec->data = strbuf_detach(buf, &size);
+ spec->size = size;
+ spec->should_free = 1;
+ spec->is_stdin = 1;
+
+ return spec;
+}
+
+static void show_hunk_word_diff(struct add_p_state *s, struct hunk *hunk,
+ int colored)
+{
+ struct hunk_header *header = &hunk->header;
+ struct strbuf old = STRBUF_INIT, new = STRBUF_INIT;
+ struct diff_options opts;
+ struct diff_queue_struct queue;
+
+ if (!header->old_offset && !header->new_offset) {
+ strbuf_reset(&s->buf);
+ render_hunk(s, hunk, 0, colored, &s->buf);
+ fputs(s->buf.buf, stdout);
+ return;
+ }
+
+ build_word_diff_files(s, hunk, &old, &new);
+
+ repo_diff_setup(s->r, &opts);
+ opts.output_format = DIFF_FORMAT_PATCH;
+ opts.use_color = colored ? s->cfg.use_color_diff : GIT_COLOR_NEVER;
+ opts.word_diff = DIFF_WORDS_PLAIN;
+ opts.context = header->old_count > header->new_count ?
+ header->old_count : header->new_count;
+ opts.flags.suppress_diff_headers = 1;
+ diff_setup_done(&opts);
+
+ memcpy(&queue, &diff_queued_diff, sizeof(diff_queued_diff));
+ diff_queue_init(&diff_queued_diff);
+ diff_queue(&diff_queued_diff,
+ word_diff_filespec(s->r, "a", &old),
+ word_diff_filespec(s->r, "b", &new));
+ diffcore_std(&opts);
+ diff_flush(&opts);
+ memcpy(&diff_queued_diff, &queue, sizeof(diff_queued_diff));
+}
+
#define DISPLAY_HUNKS_LINES 20
static size_t display_hunks(struct add_p_state *s,
struct file_diff *file_diff, size_t start_index)
@@ -1540,6 +1640,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"
+ "w - print the current hunk with word-diff\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"
@@ -1731,7 +1832,7 @@ static size_t patch_update_file(struct add_p_state *s,
permitted |= ALLOW_GOTO_PREVIOUS_FILE;
strbuf_addstr(&s->buf, ",<");
}
- strbuf_addstr(&s->buf, ",p,P");
+ strbuf_addstr(&s->buf, ",w,p,P");
}
if (file_diff->deleted)
prompt_mode_type = PROMPT_DELETION;
@@ -1953,6 +2054,8 @@ static size_t patch_update_file(struct add_p_state *s,
hunk->use = USE_HUNK;
goto soft_increment;
}
+ } else if (s->answer.buf[0] == 'w') {
+ show_hunk_word_diff(s, hunk, colored);
} else if (ch == 'p') {
rendered_hunk_index = -1;
use_pager = (s->answer.buf[0] == 'P') ? 1 : 0;
diff --git a/t/t3701-add-interactive.sh b/t/t3701-add-interactive.sh
index 6e120a4001..e1ce98d62b 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,w,p,P,?]? Unknown command ${SQ}W${SQ} (use ${SQ}?${SQ} for help)
+ (1/1) Stage addition [y,n,q,a,d,e,w,p,P,?]?$SP
EOF
git add -p -- command <command >actual 2>&1 &&
test_cmp expect actual
@@ -332,9 +332,9 @@ test_expect_success 'different prompts for mode change/deleted' '
git -c core.filemode=true add -p >actual &&
sed -n "s/^\(([0-9/]*) Stage .*?\).*/\1/p" actual >actual.filtered &&
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,?]?
+ (1/1) Stage deletion [y,n,q,a,d,w,p,P,?]?
+ (1/2) Stage mode change [y,n,q,a,d,k,K,j,J,g,/,w,p,P,?]?
+ (2/2) Stage this hunk [y,n,q,a,d,K,J,g,/,e,w,p,P,?]?
EOF
test_cmp expect actual.filtered
'
@@ -521,13 +521,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,w,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,w,p,P,?]?_
EOF
test_write_lines s y g 1 | git add -p >actual &&
tail -n 7 <actual >actual.trimmed &&
@@ -540,7 +540,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,w,p,P,?]?_
EOF
test_write_lines s y g1 | git add -p >actual &&
tail -n 4 <actual >actual.trimmed &&
@@ -550,11 +550,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,w,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,w,p,P,?]?_
EOF
test_write_lines s y /1,2 | git add -p >actual &&
tail -n 5 <actual >actual.trimmed &&
@@ -567,7 +567,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,w,p,P,?]?_
EOF
test_write_lines s y / 1,2 | git add -p >actual &&
tail -n 4 <actual >actual.trimmed &&
@@ -579,27 +579,42 @@ 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,w,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,w,p,P,?]?_
EOF
test_write_lines s y g 1 p | git add -p >actual &&
tail -n 7 <actual >actual.trimmed &&
test_cmp expect actual.trimmed
'
+test_expect_success 'print hunk with word-diff' '
+ test_when_finished "rm -rf word-diff-repo" &&
+ git init word-diff-repo &&
+ (
+ cd word-diff-repo &&
+ test_write_lines "alpha old beta" context >word-diff &&
+ git add word-diff &&
+ git commit -m word-diff &&
+ test_write_lines "alpha new beta" context >word-diff &&
+ test_write_lines w n | git add -p word-diff >actual &&
+ test_grep "alpha \\[-old-\\]{+new+} beta" actual &&
+ git diff --cached --exit-code
+ )
+'
+
test_expect_success TTY 'print again the hunk (PAGER)' '
test_when_finished "git reset" &&
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,w,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,w,p,P,?]? <RESET>
EOF
test_write_lines s y g 1 P |
(
@@ -796,21 +811,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,w,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,w,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,w,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,w,p,P,?]? <RESET>
EOF
test_cmp expect actual
'
@@ -1424,9 +1439,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,w,p,P,?]?
+ (2/2) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,e,w,p,P,?]? Sorry, cannot split this hunk
+ (2/2) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,e,w,p,P,?]?
EOF
test_cmp expect actual
'
--
2.54.0
next reply other threads:[~2026-05-06 23:58 UTC|newest]
Thread overview: 9+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-05-06 23:54 Javier Bassi [this message]
2026-05-07 7:55 ` [PATCH] add -p: introduce 'w' command to view hunk with --word-diff Pablo
2026-05-07 13:24 ` Phillip Wood
2026-05-07 14:53 ` Pablo
2026-05-07 13:24 ` Phillip Wood
2026-05-07 14:39 ` D. Ben Knoble
2026-05-11 0:16 ` Junio C Hamano
2026-05-11 19:16 ` D. Ben Knoble
2026-05-12 0:04 ` Junio C Hamano
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260506235459.529862-1-javierbassi@gmail.com \
--to=javierbassi@gmail.com \
--cc=abrahamadekunle50@gmail.com \
--cc=git@vger.kernel.org \
--cc=gitster@pobox.com \
--cc=l.s.r@web.de \
--cc=newren@gmail.com \
--cc=ps@pks.im \
--cc=rjusto@gmail.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.