Skip to content

Commit dffc452

Browse files
committed
add -p: introduce 'w' command to view hunk with --word-diff
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. Introduce a new interactive command `w` during hunk selection to re-display the current hunk using `--word-diff`. This provides a clearer, inline view of changes without affecting how patches are applied or staged. The command is intended as a read-only visualization aid. It does not modify the hunk, nor does it change staging behavior, which remains line-based. Signed-off-by: CrimsonGlory <javierbassi@gmail.com>
1 parent 94f0577 commit dffc452

2 files changed

Lines changed: 139 additions & 23 deletions

File tree

add-patch.c

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "commit.h"
88
#include "config.h"
99
#include "diff.h"
10+
#include "diffcore.h"
1011
#include "editor.h"
1112
#include "environment.h"
1213
#include "gettext.h"
@@ -1508,6 +1509,105 @@ static void summarize_hunk(struct add_p_state *s, struct hunk *hunk,
15081509
strbuf_complete_line(out);
15091510
}
15101511

1512+
static void trim_trailing_lf(struct strbuf *buf)
1513+
{
1514+
if (buf->len && buf->buf[buf->len - 1] == '\n')
1515+
strbuf_setlen(buf, buf->len - 1);
1516+
}
1517+
1518+
static void add_word_diff_line(struct strbuf *old, struct strbuf *new,
1519+
const char *line, size_t len, char marker)
1520+
{
1521+
if (marker == '-' || marker == '+' || *line == ' ') {
1522+
line++;
1523+
len--;
1524+
}
1525+
1526+
if (marker != '+')
1527+
strbuf_add(old, line, len);
1528+
if (marker != '-')
1529+
strbuf_add(new, line, len);
1530+
}
1531+
1532+
static void build_word_diff_files(struct add_p_state *s, struct hunk *hunk,
1533+
struct strbuf *old, struct strbuf *new)
1534+
{
1535+
size_t i;
1536+
char last_marker = '\0';
1537+
1538+
for (i = hunk->start; i < hunk->end; i = find_next_line(&s->plain, i)) {
1539+
size_t next = find_next_line(&s->plain, i);
1540+
char marker = normalize_marker(s->plain.buf + i);
1541+
1542+
if (marker == '\\') {
1543+
if (last_marker != '+')
1544+
trim_trailing_lf(old);
1545+
if (last_marker != '-')
1546+
trim_trailing_lf(new);
1547+
continue;
1548+
}
1549+
1550+
if (marker != ' ' && marker != '-' && marker != '+')
1551+
BUG("unhandled diff marker: '%c'", marker);
1552+
1553+
add_word_diff_line(old, new, s->plain.buf + i, next - i,
1554+
marker);
1555+
last_marker = marker;
1556+
}
1557+
}
1558+
1559+
static struct diff_filespec *word_diff_filespec(struct repository *r,
1560+
const char *name,
1561+
struct strbuf *buf)
1562+
{
1563+
struct diff_filespec *spec = alloc_filespec(name);
1564+
size_t size;
1565+
1566+
fill_filespec(spec, null_oid(r->hash_algo), 0, 0100644);
1567+
spec->data = strbuf_detach(buf, &size);
1568+
spec->size = size;
1569+
spec->should_free = 1;
1570+
spec->is_stdin = 1;
1571+
1572+
return spec;
1573+
}
1574+
1575+
static void show_hunk_word_diff(struct add_p_state *s, struct hunk *hunk,
1576+
int colored)
1577+
{
1578+
struct hunk_header *header = &hunk->header;
1579+
struct strbuf old = STRBUF_INIT, new = STRBUF_INIT;
1580+
struct diff_options opts;
1581+
struct diff_queue_struct queue;
1582+
1583+
if (!header->old_offset && !header->new_offset) {
1584+
strbuf_reset(&s->buf);
1585+
render_hunk(s, hunk, 0, colored, &s->buf);
1586+
fputs(s->buf.buf, stdout);
1587+
return;
1588+
}
1589+
1590+
build_word_diff_files(s, hunk, &old, &new);
1591+
1592+
repo_diff_setup(s->r, &opts);
1593+
opts.output_format = DIFF_FORMAT_PATCH;
1594+
opts.use_color = colored ? s->cfg.use_color_diff : GIT_COLOR_NEVER;
1595+
opts.word_diff = DIFF_WORDS_PLAIN;
1596+
opts.context = header->old_count > header->new_count ?
1597+
header->old_count : header->new_count;
1598+
opts.flags.suppress_diff_headers = 1;
1599+
diff_setup_done(&opts);
1600+
1601+
memcpy(&queue, &diff_queued_diff, sizeof(diff_queued_diff));
1602+
diff_queue_init(&diff_queued_diff);
1603+
diff_queue(&diff_queued_diff,
1604+
word_diff_filespec(s->r, "a", &old),
1605+
word_diff_filespec(s->r, "b", &new));
1606+
diffcore_std(&opts);
1607+
diff_flush(&opts);
1608+
memcpy(&diff_queued_diff, &queue, sizeof(diff_queued_diff));
1609+
}
1610+
15111611
#define DISPLAY_HUNKS_LINES 20
15121612
static size_t display_hunks(struct add_p_state *s,
15131613
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"
15401640
"/ - search for a hunk matching the given regex\n"
15411641
"s - split the current hunk into smaller hunks\n"
15421642
"e - manually edit the current hunk\n"
1643+
"w - print the current hunk with word-diff\n"
15431644
"p - print the current hunk\n"
15441645
"P - print the current hunk using the pager\n"
15451646
"> - 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,
17311832
permitted |= ALLOW_GOTO_PREVIOUS_FILE;
17321833
strbuf_addstr(&s->buf, ",<");
17331834
}
1734-
strbuf_addstr(&s->buf, ",p,P");
1835+
strbuf_addstr(&s->buf, ",w,p,P");
17351836
}
17361837
if (file_diff->deleted)
17371838
prompt_mode_type = PROMPT_DELETION;
@@ -1953,6 +2054,8 @@ static size_t patch_update_file(struct add_p_state *s,
19532054
hunk->use = USE_HUNK;
19542055
goto soft_increment;
19552056
}
2057+
} else if (s->answer.buf[0] == 'w') {
2058+
show_hunk_word_diff(s, hunk, colored);
19562059
} else if (ch == 'p') {
19572060
rendered_hunk_index = -1;
19582061
use_pager = (s->answer.buf[0] == 'P') ? 1 : 0;

t/t3701-add-interactive.sh

Lines changed: 35 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ test_expect_success 'unknown command' '
4848
git add -N command &&
4949
git diff command >expect &&
5050
cat >>expect <<-EOF &&
51-
(1/1) Stage addition [y,n,q,a,d,e,p,P,?]? Unknown command ${SQ}W${SQ} (use ${SQ}?${SQ} for help)
52-
(1/1) Stage addition [y,n,q,a,d,e,p,P,?]?$SP
51+
(1/1) Stage addition [y,n,q,a,d,e,w,p,P,?]? Unknown command ${SQ}W${SQ} (use ${SQ}?${SQ} for help)
52+
(1/1) Stage addition [y,n,q,a,d,e,w,p,P,?]?$SP
5353
EOF
5454
git add -p -- command <command >actual 2>&1 &&
5555
test_cmp expect actual
@@ -332,9 +332,9 @@ test_expect_success 'different prompts for mode change/deleted' '
332332
git -c core.filemode=true add -p >actual &&
333333
sed -n "s/^\(([0-9/]*) Stage .*?\).*/\1/p" actual >actual.filtered &&
334334
cat >expect <<-\EOF &&
335-
(1/1) Stage deletion [y,n,q,a,d,p,P,?]?
336-
(1/2) Stage mode change [y,n,q,a,d,k,K,j,J,g,/,p,P,?]?
337-
(2/2) Stage this hunk [y,n,q,a,d,K,J,g,/,e,p,P,?]?
335+
(1/1) Stage deletion [y,n,q,a,d,w,p,P,?]?
336+
(1/2) Stage mode change [y,n,q,a,d,k,K,j,J,g,/,w,p,P,?]?
337+
(2/2) Stage this hunk [y,n,q,a,d,K,J,g,/,e,w,p,P,?]?
338338
EOF
339339
test_cmp expect actual.filtered
340340
'
@@ -521,13 +521,13 @@ test_expect_success 'split hunk setup' '
521521
test_expect_success 'goto hunk 1 with "g 1"' '
522522
test_when_finished "git reset" &&
523523
tr _ " " >expect <<-EOF &&
524-
(2/2) Stage this hunk [y,n,q,a,d,K,J,g,/,e,p,P,?]? + 1: -1,2 +1,3 +15
524+
(2/2) Stage this hunk [y,n,q,a,d,K,J,g,/,e,w,p,P,?]? + 1: -1,2 +1,3 +15
525525
_ 2: -2,4 +3,8 +21
526526
go to which hunk? @@ -1,2 +1,3 @@
527527
_10
528528
+15
529529
_20
530-
(1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]?_
530+
(1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,w,p,P,?]?_
531531
EOF
532532
test_write_lines s y g 1 | git add -p >actual &&
533533
tail -n 7 <actual >actual.trimmed &&
@@ -540,7 +540,7 @@ test_expect_success 'goto hunk 1 with "g1"' '
540540
_10
541541
+15
542542
_20
543-
(1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]?_
543+
(1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,w,p,P,?]?_
544544
EOF
545545
test_write_lines s y g1 | git add -p >actual &&
546546
tail -n 4 <actual >actual.trimmed &&
@@ -550,11 +550,11 @@ test_expect_success 'goto hunk 1 with "g1"' '
550550
test_expect_success 'navigate to hunk via regex /pattern' '
551551
test_when_finished "git reset" &&
552552
tr _ " " >expect <<-EOF &&
553-
(2/2) Stage this hunk [y,n,q,a,d,K,J,g,/,e,p,P,?]? @@ -1,2 +1,3 @@
553+
(2/2) Stage this hunk [y,n,q,a,d,K,J,g,/,e,w,p,P,?]? @@ -1,2 +1,3 @@
554554
_10
555555
+15
556556
_20
557-
(1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]?_
557+
(1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,w,p,P,?]?_
558558
EOF
559559
test_write_lines s y /1,2 | git add -p >actual &&
560560
tail -n 5 <actual >actual.trimmed &&
@@ -567,7 +567,7 @@ test_expect_success 'navigate to hunk via regex / pattern' '
567567
_10
568568
+15
569569
_20
570-
(1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]?_
570+
(1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,w,p,P,?]?_
571571
EOF
572572
test_write_lines s y / 1,2 | git add -p >actual &&
573573
tail -n 4 <actual >actual.trimmed &&
@@ -579,27 +579,40 @@ test_expect_success 'print again the hunk' '
579579
tr _ " " >expect <<-EOF &&
580580
+15
581581
20
582-
(1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]? @@ -1,2 +1,3 @@
582+
(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 @@
583583
10
584584
+15
585585
20
586-
(1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]?_
586+
(1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,w,p,P,?]?_
587587
EOF
588588
test_write_lines s y g 1 p | git add -p >actual &&
589589
tail -n 7 <actual >actual.trimmed &&
590590
test_cmp expect actual.trimmed
591591
'
592592

593+
test_expect_success 'print hunk with word-diff' '
594+
head=$(git rev-parse HEAD) &&
595+
test_when_finished "git reset --hard $head && rm -f word-diff" &&
596+
git reset --hard &&
597+
test_write_lines "alpha old beta" context >word-diff &&
598+
git add word-diff &&
599+
git commit -m word-diff &&
600+
test_write_lines "alpha new beta" context >word-diff &&
601+
test_write_lines w n | git add -p word-diff >actual &&
602+
test_grep "alpha \\[-old-\\]{+new+} beta" actual &&
603+
git diff --cached --exit-code
604+
'
605+
593606
test_expect_success TTY 'print again the hunk (PAGER)' '
594607
test_when_finished "git reset" &&
595608
cat >expect <<-EOF &&
596609
<GREEN>+<RESET><GREEN>15<RESET>
597610
20<RESET>
598-
<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>
611+
<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>
599612
PAGER 10<RESET>
600613
PAGER <GREEN>+<RESET><GREEN>15<RESET>
601614
PAGER 20<RESET>
602-
<BOLD;BLUE>(1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]? <RESET>
615+
<BOLD;BLUE>(1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,w,p,P,?]? <RESET>
603616
EOF
604617
test_write_lines s y g 1 P |
605618
(
@@ -796,21 +809,21 @@ test_expect_success 'colors can be overridden' '
796809
<BLUE>+<RESET><BLUE>new<RESET>
797810
<CYAN> more-context<RESET>
798811
<BLUE>+<RESET><BLUE>another-one<RESET>
799-
<YELLOW>(1/1) Stage this hunk [y,n,q,a,d,s,e,p,P,?]? <RESET><BOLD>Split into 2 hunks.<RESET>
812+
<YELLOW>(1/1) Stage this hunk [y,n,q,a,d,s,e,w,p,P,?]? <RESET><BOLD>Split into 2 hunks.<RESET>
800813
<MAGENTA>@@ -1,3 +1,3 @@<RESET>
801814
<CYAN> context<RESET>
802815
<BOLD>-old<RESET>
803816
<BLUE>+<RESET><BLUE>new<RESET>
804817
<CYAN> more-context<RESET>
805-
<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>
818+
<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>
806819
<CYAN> more-context<RESET>
807820
<BLUE>+<RESET><BLUE>another-one<RESET>
808-
<YELLOW>(2/2) Stage this hunk [y,n,q,a,d,K,J,g,/,e,p,P,?]? <RESET><MAGENTA>@@ -1,3 +1,3 @@<RESET>
821+
<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>
809822
<CYAN> context<RESET>
810823
<BOLD>-old<RESET>
811824
<BLUE>+new<RESET>
812825
<CYAN> more-context<RESET>
813-
<YELLOW>(1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]? <RESET>
826+
<YELLOW>(1/2) Stage this hunk (was: y) [y,n,q,a,d,k,K,j,J,g,/,e,w,p,P,?]? <RESET>
814827
EOF
815828
test_cmp expect actual
816829
'
@@ -1424,9 +1437,9 @@ test_expect_success 'invalid option s is rejected' '
14241437
test_write_lines j s q | git add -p >out &&
14251438
sed -ne "s/ @@.*//" -e "s/ \$//" -e "/^(/p" <out >actual &&
14261439
cat >expect <<-EOF &&
1427-
(1/2) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,s,e,p,P,?]?
1428-
(2/2) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]? Sorry, cannot split this hunk
1429-
(2/2) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,e,p,P,?]?
1440+
(1/2) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,s,e,w,p,P,?]?
1441+
(2/2) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,e,w,p,P,?]? Sorry, cannot split this hunk
1442+
(2/2) Stage this hunk [y,n,q,a,d,k,K,j,J,g,/,e,w,p,P,?]?
14301443
EOF
14311444
test_cmp expect actual
14321445
'

0 commit comments

Comments
 (0)