@matheusbsbs / matheus-bagliano / commits / 510aeca199
Fork 0

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 <johannes.schindelin@gmx.de> Signed-off-by: Junio C Hamano <gitster@pobox.com>

Johannes Schindelin committed Dec 13, 2019 at 08:07 UTC 510aeca199c4feeb38d318cc151ecf5464a3a865
2 files changed +225 -2
add-patch.c
+213 -2
@@ -28,7 +28,7 @@ struct hunk_header {
28 };
29
30 struct hunk {
31 - size_t start, end, colored_start, colored_end;
31 + size_t start, end, colored_start, colored_end, splittable_into;
32 enum { UNDECIDED_HUNK = 0, SKIP_HUNK, USE_HUNK } use;
33 struct hunk_header header;
34 };
@@ -155,7 +155,7 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
155 struct argv_array args = ARGV_ARRAY_INIT;
156 struct strbuf *plain = &s->plain, *colored = NULL;
157 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';
159 size_t file_diff_alloc = 0, i, color_arg_index;
160 struct file_diff *file_diff = NULL;
161 struct hunk *hunk = NULL;
@@ -217,6 +217,7 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
217 hunk->start = p - plain->buf;
218 if (colored_p)
219 hunk->colored_start = colored_p - colored->buf;
220 + marker = '\0';
221 } else if (p == plain->buf)
222 BUG("diff starts with unexpected line:\n"
223 "%.*s\n", (int)(eol - p), p);
@@ -225,6 +226,13 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
226 else if (starts_with(p, "@@ ") ||
227 (hunk == &file_diff->head &&
228 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 +
236 file_diff->hunk_nr++;
237 ALLOC_GROW(file_diff->hunk, file_diff->hunk_nr,
238 file_diff->hunk_alloc);
@@ -239,6 +247,12 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
247 file_diff->deleted = 1;
248 else if (parse_hunk_header(s, hunk) < 0)
249 return -1;
250 +
251 + /*
252 + * Start counting into how many hunks this one can be
253 + * split
254 + */
255 + marker = *p;
256 } else if (hunk == &file_diff->head &&
257 skip_prefix(p, "old mode ", &mode_change) &&
258 is_octal(mode_change, eol - mode_change)) {
@@ -286,6 +300,11 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
300 (int)(eol - (plain->buf + file_diff->head.start)),
301 plain->buf + file_diff->head.start);
302
303 + if ((marker == '-' || marker == '+') && *p == ' ')
304 + hunk->splittable_into++;
305 + if (marker && *p != '\\')
306 + marker = *p;
307 +
308 p = eol == pend ? pend : eol + 1;
309 hunk->end = p - plain->buf;
310
@@ -311,9 +330,30 @@ static int parse_diff(struct add_p_state *s, const struct pathspec *ps)
330 }
331 }
332
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 +
340 return 0;
341 }
342
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 +
357 static void render_hunk(struct add_p_state *s, struct hunk *hunk,
358 ssize_t delta, int colored, struct strbuf *out)
359 {
@@ -412,6 +452,165 @@ static void reassemble_patch(struct add_p_state *s,
452 }
453 }
454
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 +
614 static const char help_patch_text[] =
615 N_("y - stage this hunk\n"
616 "n - do not stage this hunk\n"
@@ -421,6 +620,7 @@ N_("y - stage this hunk\n"
620 "J - leave this hunk undecided, see next hunk\n"
621 "k - leave this hunk undecided, see previous undecided hunk\n"
622 "K - leave this hunk undecided, see previous hunk\n"
623 + "s - split the current hunk into smaller hunks\n"
624 "? - print help\n");
625
626 static int patch_update_file(struct add_p_state *s,
@@ -477,6 +677,8 @@ static int patch_update_file(struct add_p_state *s,
677 strbuf_addstr(&s->buf, ",j");
678 if (hunk_index + 1 < file_diff->hunk_nr)
679 strbuf_addstr(&s->buf, ",J");
680 + if (hunk->splittable_into > 1)
681 + strbuf_addstr(&s->buf, ",s");
682
683 if (file_diff->deleted)
684 prompt_mode_type = PROMPT_DELETION;
@@ -539,6 +741,15 @@ soft_increment:
741 hunk_index = undecided_next;
742 else
743 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);
753 } else
754 color_fprintf(stdout, s->s.help_color,
755 _(help_patch_text));
t/t3701-add-interactive.sh
+12
@@ -442,6 +442,18 @@ test_expect_failure 'split hunk "add -p (no, yes, edit)"' '
442 ! grep "^+31" actual
443 '
444
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 +
457 test_expect_failure 'edit, adding lines to the first hunk' '
458 test_write_lines 10 11 20 30 40 50 51 60 >test &&
459 git reset &&