Skip to content

Commit 510aeca

Browse files
dschogitster
authored andcommitted
built-in add -p: implement the hunk splitting feature
If this developer's workflow is any indication, then this is *the* most useful feature of Git's interactive `add `command. Note: once again, this is not a verbatim conversion from the Perl code to C: the `hunk_splittable()` function, for example, essentially did all the work of splitting the hunk, just to find out whether more than one hunk would have been the result (and then tossed that result into the trash). In C we instead count the number of resulting hunks (without actually doing the work of splitting, but just counting the transitions from non-context lines to context lines), and store that information with the hunk, and we do that *while* parsing the diff in the first place. Another deviation: the built-in `git add -p` was designed with a single strbuf holding the diff (and another one holding the colored diff, if that one was asked for) in mind, and hunks essentially store just the start and end offsets pointing into that strbuf. As a consequence, when we split hunks, we now use a special mode where the hunk header is generated dynamically, and only the rest of the hunk is stored using such start/end offsets. This way, we also avoid the frequent formatting/re-parsing of the hunk header of the Perl version. Signed-off-by: Johannes Schindelin <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 0ecd9d2 commit 510aeca

File tree

2 files changed

+225
-2
lines changed

2 files changed

+225
-2
lines changed

add-patch.c

Lines changed: 213 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ struct hunk_header {
2828
};
2929

3030
struct hunk {
31-
size_t start, end, colored_start, colored_end;
31+
size_t start, end, colored_start, colored_end, splittable_into;
3232
enum { UNDECIDED_HUNK = 0, SKIP_HUNK, USE_HUNK } use;
3333
struct hunk_header header;
3434
};
@@ -155,7 +155,7 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
155155
struct argv_array args = ARGV_ARRAY_INIT;
156156
struct strbuf *plain = &s->plain, *colored = NULL;
157157
struct child_process cp = CHILD_PROCESS_INIT;
158-
char *p, *pend, *colored_p = NULL, *colored_pend = NULL;
158+
char *p, *pend, *colored_p = NULL, *colored_pend = NULL, marker = '\0';
159159
size_t file_diff_alloc = 0, i, color_arg_index;
160160
struct file_diff *file_diff = NULL;
161161
struct hunk *hunk = NULL;
@@ -217,6 +217,7 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
217217
hunk->start = p - plain->buf;
218218
if (colored_p)
219219
hunk->colored_start = colored_p - colored->buf;
220+
marker = '\0';
220221
} else if (p == plain->buf)
221222
BUG("diff starts with unexpected line:\n"
222223
"%.*s\n", (int)(eol - p), p);
@@ -225,6 +226,13 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
225226
else if (starts_with(p, "@@ ") ||
226227
(hunk == &file_diff->head &&
227228
skip_prefix(p, "deleted file", &deleted))) {
229+
if (marker == '-' || marker == '+')
230+
/*
231+
* Should not happen; previous hunk did not end
232+
* in a context line? Handle it anyway.
233+
*/
234+
hunk->splittable_into++;
235+
228236
file_diff->hunk_nr++;
229237
ALLOC_GROW(file_diff->hunk, file_diff->hunk_nr,
230238
file_diff->hunk_alloc);
@@ -239,6 +247,12 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
239247
file_diff->deleted = 1;
240248
else if (parse_hunk_header(s, hunk) < 0)
241249
return -1;
250+
251+
/*
252+
* Start counting into how many hunks this one can be
253+
* split
254+
*/
255+
marker = *p;
242256
} else if (hunk == &file_diff->head &&
243257
skip_prefix(p, "old mode ", &mode_change) &&
244258
is_octal(mode_change, eol - mode_change)) {
@@ -286,6 +300,11 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
286300
(int)(eol - (plain->buf + file_diff->head.start)),
287301
plain->buf + file_diff->head.start);
288302

303+
if ((marker == '-' || marker == '+') && *p == ' ')
304+
hunk->splittable_into++;
305+
if (marker && *p != '\\')
306+
marker = *p;
307+
289308
p = eol == pend ? pend : eol + 1;
290309
hunk->end = p - plain->buf;
291310

@@ -311,9 +330,30 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
311330
}
312331
}
313332

333+
if (marker == '-' || marker == '+')
334+
/*
335+
* Last hunk ended in non-context line (i.e. it appended lines
336+
* to the file, so there are no trailing context lines).
337+
*/
338+
hunk->splittable_into++;
339+
314340
return 0;
315341
}
316342

343+
static size_t find_next_line(struct strbuf *sb, size_t offset)
344+
{
345+
char *eol;
346+
347+
if (offset >= sb->len)
348+
BUG("looking for next line beyond buffer (%d >= %d)\n%s",
349+
(int)offset, (int)sb->len, sb->buf);
350+
351+
eol = memchr(sb->buf + offset, '\n', sb->len - offset);
352+
if (!eol)
353+
return sb->len;
354+
return eol - sb->buf + 1;
355+
}
356+
317357
static void render_hunk(struct add_p_state *s, struct hunk *hunk,
318358
ssize_t delta, int colored, struct strbuf *out)
319359
{
@@ -412,6 +452,165 @@ static void reassemble_patch(struct add_p_state *s,
412452
}
413453
}
414454

455+
static int split_hunk(struct add_p_state *s, struct file_diff *file_diff,
456+
size_t hunk_index)
457+
{
458+
int colored = !!s->colored.len, first = 1;
459+
struct hunk *hunk = file_diff->hunk + hunk_index;
460+
size_t splittable_into;
461+
size_t end, colored_end, current, colored_current = 0, context_line_count;
462+
struct hunk_header remaining, *header;
463+
char marker, ch;
464+
465+
if (hunk_index >= file_diff->hunk_nr)
466+
BUG("invalid hunk index: %d (must be >= 0 and < %d)",
467+
(int)hunk_index, (int)file_diff->hunk_nr);
468+
469+
if (hunk->splittable_into < 2)
470+
return 0;
471+
splittable_into = hunk->splittable_into;
472+
473+
end = hunk->end;
474+
colored_end = hunk->colored_end;
475+
476+
remaining = hunk->header;
477+
478+
file_diff->hunk_nr += splittable_into - 1;
479+
ALLOC_GROW(file_diff->hunk, file_diff->hunk_nr, file_diff->hunk_alloc);
480+
if (hunk_index + splittable_into < file_diff->hunk_nr)
481+
memmove(file_diff->hunk + hunk_index + splittable_into,
482+
file_diff->hunk + hunk_index + 1,
483+
(file_diff->hunk_nr - hunk_index - splittable_into)
484+
* sizeof(*hunk));
485+
hunk = file_diff->hunk + hunk_index;
486+
hunk->splittable_into = 1;
487+
memset(hunk + 1, 0, (splittable_into - 1) * sizeof(*hunk));
488+
489+
header = &hunk->header;
490+
header->old_count = header->new_count = 0;
491+
492+
current = hunk->start;
493+
if (colored)
494+
colored_current = hunk->colored_start;
495+
marker = '\0';
496+
context_line_count = 0;
497+
498+
while (splittable_into > 1) {
499+
ch = s->plain.buf[current];
500+
501+
if (!ch)
502+
BUG("buffer overrun while splitting hunks");
503+
504+
/*
505+
* Is this the first context line after a chain of +/- lines?
506+
* Then record the start of the next split hunk.
507+
*/
508+
if ((marker == '-' || marker == '+') && ch == ' ') {
509+
first = 0;
510+
hunk[1].start = current;
511+
if (colored)
512+
hunk[1].colored_start = colored_current;
513+
context_line_count = 0;
514+
}
515+
516+
/*
517+
* Was the previous line a +/- one? Alternatively, is this the
518+
* first line (and not a +/- one)?
519+
*
520+
* Then just increment the appropriate counter and continue
521+
* with the next line.
522+
*/
523+
if (marker != ' ' || (ch != '-' && ch != '+')) {
524+
next_hunk_line:
525+
/* Comment lines are attached to the previous line */
526+
if (ch == '\\')
527+
ch = marker ? marker : ' ';
528+
529+
/* current hunk not done yet */
530+
if (ch == ' ')
531+
context_line_count++;
532+
else if (ch == '-')
533+
header->old_count++;
534+
else if (ch == '+')
535+
header->new_count++;
536+
else
537+
BUG("unhandled diff marker: '%c'", ch);
538+
marker = ch;
539+
current = find_next_line(&s->plain, current);
540+
if (colored)
541+
colored_current =
542+
find_next_line(&s->colored,
543+
colored_current);
544+
continue;
545+
}
546+
547+
/*
548+
* We got us the start of a new hunk!
549+
*
550+
* This is a context line, so it is shared with the previous
551+
* hunk, if any.
552+
*/
553+
554+
if (first) {
555+
if (header->old_count || header->new_count)
556+
BUG("counts are off: %d/%d",
557+
(int)header->old_count,
558+
(int)header->new_count);
559+
560+
header->old_count = context_line_count;
561+
header->new_count = context_line_count;
562+
context_line_count = 0;
563+
first = 0;
564+
goto next_hunk_line;
565+
}
566+
567+
remaining.old_offset += header->old_count;
568+
remaining.old_count -= header->old_count;
569+
remaining.new_offset += header->new_count;
570+
remaining.new_count -= header->new_count;
571+
572+
/* initialize next hunk header's offsets */
573+
hunk[1].header.old_offset =
574+
header->old_offset + header->old_count;
575+
hunk[1].header.new_offset =
576+
header->new_offset + header->new_count;
577+
578+
/* add one split hunk */
579+
header->old_count += context_line_count;
580+
header->new_count += context_line_count;
581+
582+
hunk->end = current;
583+
if (colored)
584+
hunk->colored_end = colored_current;
585+
586+
hunk++;
587+
hunk->splittable_into = 1;
588+
hunk->use = hunk[-1].use;
589+
header = &hunk->header;
590+
591+
header->old_count = header->new_count = context_line_count;
592+
context_line_count = 0;
593+
594+
splittable_into--;
595+
marker = ch;
596+
}
597+
598+
/* last hunk simply gets the rest */
599+
if (header->old_offset != remaining.old_offset)
600+
BUG("miscounted old_offset: %lu != %lu",
601+
header->old_offset, remaining.old_offset);
602+
if (header->new_offset != remaining.new_offset)
603+
BUG("miscounted new_offset: %lu != %lu",
604+
header->new_offset, remaining.new_offset);
605+
header->old_count = remaining.old_count;
606+
header->new_count = remaining.new_count;
607+
hunk->end = end;
608+
if (colored)
609+
hunk->colored_end = colored_end;
610+
611+
return 0;
612+
}
613+
415614
static const char help_patch_text[] =
416615
N_("y - stage this hunk\n"
417616
"n - do not stage this hunk\n"
@@ -421,6 +620,7 @@ N_("y - stage this hunk\n"
421620
"J - leave this hunk undecided, see next hunk\n"
422621
"k - leave this hunk undecided, see previous undecided hunk\n"
423622
"K - leave this hunk undecided, see previous hunk\n"
623+
"s - split the current hunk into smaller hunks\n"
424624
"? - print help\n");
425625

426626
static int patch_update_file(struct add_p_state *s,
@@ -477,6 +677,8 @@ static int patch_update_file(struct add_p_state *s,
477677
strbuf_addstr(&s->buf, ",j");
478678
if (hunk_index + 1 < file_diff->hunk_nr)
479679
strbuf_addstr(&s->buf, ",J");
680+
if (hunk->splittable_into > 1)
681+
strbuf_addstr(&s->buf, ",s");
480682

481683
if (file_diff->deleted)
482684
prompt_mode_type = PROMPT_DELETION;
@@ -539,6 +741,15 @@ static int patch_update_file(struct add_p_state *s,
539741
hunk_index = undecided_next;
540742
else
541743
err(s, _("No next hunk"));
744+
} else if (s->answer.buf[0] == 's') {
745+
size_t splittable_into = hunk->splittable_into;
746+
if (splittable_into < 2)
747+
err(s, _("Sorry, cannot split this hunk"));
748+
else if (!split_hunk(s, file_diff,
749+
hunk - file_diff->hunk))
750+
color_fprintf_ln(stdout, s->s.header_color,
751+
_("Split into %d hunks."),
752+
(int)splittable_into);
542753
} else
543754
color_fprintf(stdout, s->s.help_color,
544755
_(help_patch_text));

t/t3701-add-interactive.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,18 @@ test_expect_failure 'split hunk "add -p (no, yes, edit)"' '
442442
! grep "^+31" actual
443443
'
444444

445+
test_expect_success 'split hunk with incomplete line at end' '
446+
git reset --hard &&
447+
printf "missing LF" >>test &&
448+
git add test &&
449+
test_write_lines before 10 20 30 40 50 60 70 >test &&
450+
git grep --cached missing &&
451+
test_write_lines s n y q | git add -p &&
452+
test_must_fail git grep --cached missing &&
453+
git grep before &&
454+
test_must_fail git grep --cached before
455+
'
456+
445457
test_expect_failure 'edit, adding lines to the first hunk' '
446458
test_write_lines 10 11 20 30 40 50 51 60 >test &&
447459
git reset &&

0 commit comments

Comments
 (0)