Skip to content

Commit 7d4860b

Browse files
phillipwoodgitster
authored andcommitted
add -p: select individual hunk lines
When I end up editing hunks it is almost always because I want to stage a subset of the lines in the hunk. Doing this by editing the hunk is inconvenient and error prone (especially so if the patch is going to be reversed before being applied). Instead offer an option for add -p to stage individual lines. When the user presses 'l' the hunk is redrawn with labels by the insertions and deletions and they are prompted to enter a list of the lines they wish to stage. Ranges of lines may be specified using 'a-b' where 'b' may be omitted to mean all lines from 'a' to the end of the hunk. Signed-off-by: Phillip Wood <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 3a8522f commit 7d4860b

File tree

3 files changed

+213
-0
lines changed

3 files changed

+213
-0
lines changed

Documentation/git-add.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,10 +332,18 @@ patch::
332332
J - leave this hunk undecided, see next hunk
333333
k - leave this hunk undecided, see previous undecided hunk
334334
K - leave this hunk undecided, see previous hunk
335+
l - select hunk lines to use
335336
s - split the current hunk into smaller hunks
336337
e - manually edit the current hunk
337338
? - print help
338339
+
340+
If you press "l" then the hunk will be reprinted with each insertion or
341+
deletion labelled with a number and you will be prompted to enter which
342+
lines you wish to select. Individual line numbers should be separated by
343+
a space or comma, to specify a range of lines use a dash between
344+
them. If the upper bound of a range of lines is omitted it defaults to
345+
the last line.
346+
+
339347
After deciding the fate for all hunks, if there is any hunk
340348
that was chosen, the index is updated with the selected hunks.
341349
+

git-add--interactive.perl

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,6 +1013,130 @@ sub color_diff {
10131013
} @_;
10141014
}
10151015

1016+
sub label_hunk_lines {
1017+
local $_;
1018+
my $hunk = shift;
1019+
my $i = 0;
1020+
my $labels = [ map { /^[-+]/ ? ++$i : 0 } @{$hunk->{TEXT}} ];
1021+
if ($i > 1) {
1022+
@{$hunk}{qw(LABELS MAX_LABEL)} = ($labels, $i);
1023+
return 1;
1024+
}
1025+
return 0;
1026+
}
1027+
1028+
sub select_hunk_lines {
1029+
my ($hunk, $selected) = @_;
1030+
my ($text, $labels) = @{$hunk}{qw(TEXT LABELS)};
1031+
my ($i, $o_cnt, $n_cnt) = (0, 0, 0);
1032+
my ($push_eol, @newtext);
1033+
# Lines with this mode will become context lines if they are
1034+
# not selected
1035+
my $context_mode = $patch_mode_flavour{IS_REVERSE} ? '+' : '-';
1036+
for $i (1..$#{$text}) {
1037+
my $mode = substr($text->[$i], 0, 1);
1038+
if ($mode eq '\\') {
1039+
push @newtext, $text->[$i] if ($push_eol);
1040+
undef $push_eol;
1041+
} elsif ($labels->[$i] and $selected->[$labels->[$i]]) {
1042+
push @newtext, $text->[$i];
1043+
if ($mode eq '+') {
1044+
$n_cnt++;
1045+
} else {
1046+
$o_cnt++;
1047+
}
1048+
$push_eol = 1;
1049+
} elsif ($mode eq ' ' or $mode eq $context_mode) {
1050+
push @newtext, ' ' . substr($text->[$i], 1);
1051+
$o_cnt++; $n_cnt++;
1052+
$push_eol = 1;
1053+
} else {
1054+
undef $push_eol;
1055+
}
1056+
}
1057+
my ($o_ofs, $orig_o_cnt, $n_ofs, $orig_n_cnt) =
1058+
parse_hunk_header($text->[0]);
1059+
unshift @newtext, format_hunk_header($o_ofs, $o_cnt, $n_ofs, $n_cnt);
1060+
my $newhunk = {
1061+
TEXT => \@newtext,
1062+
DISPLAY => [ color_diff(@newtext) ],
1063+
OFS_DELTA => $orig_o_cnt - $orig_n_cnt - $o_cnt + $n_cnt,
1064+
TYPE => $hunk->{TYPE},
1065+
USE => 1,
1066+
};
1067+
# If this hunk has previously been edited add the offset delta
1068+
# of the old hunk to get the real delta from the original
1069+
# hunk.
1070+
if ($hunk->{OFS_DELTA}) {
1071+
$newhunk->{OFS_DELTA} += $hunk->{OFS_DELTA};
1072+
}
1073+
return $newhunk;
1074+
}
1075+
1076+
sub check_hunk_label {
1077+
my ($max_label, $label) = ($_[0]->{MAX_LABEL}, $_[1]);
1078+
if ($label < 1 or $label > $max_label) {
1079+
error_msg sprintf(__("invalid hunk line '%d'\n"), $label);
1080+
return 0;
1081+
}
1082+
return 1;
1083+
}
1084+
1085+
sub parse_hunk_selection {
1086+
local $_;
1087+
my ($hunk, $line) = @_;
1088+
my $max_label = $hunk->{MAX_LABEL};
1089+
my @selected = (0) x ($max_label + 1);
1090+
my @fields = split(/[,\s]+/, $line);
1091+
for (@fields) {
1092+
if (my ($lo, $hi) = /^([0-9]+)-([0-9]*)$/) {
1093+
if ($hi eq '') {
1094+
$hi = $max_label;
1095+
}
1096+
check_hunk_label($hunk, $lo) or return undef;
1097+
check_hunk_label($hunk, $hi) or return undef;
1098+
if ($hi < $lo) {
1099+
($lo, $hi) = ($hi, $lo);
1100+
}
1101+
@selected[$lo..$hi] = (1) x (1 + $hi - $lo);
1102+
} elsif (/^([0-9]+)$/) {
1103+
check_hunk_label($hunk, $1) or return undef;
1104+
$selected[$1] = 1;
1105+
} else {
1106+
error_msg sprintf(__("invalid hunk line '%s'\n"), $_);
1107+
return undef;
1108+
}
1109+
}
1110+
return \@selected;
1111+
}
1112+
1113+
sub display_hunk_lines {
1114+
my ($display, $labels, $max_label) =
1115+
@{$_[0]}{qw(DISPLAY LABELS MAX_LABEL)};
1116+
my $width = int(log($max_label) / log(10)) + 1;
1117+
my $padding = ' ' x ($width + 1);
1118+
for my $i (0..$#{$display}) {
1119+
if ($labels->[$i]) {
1120+
printf '%*d %s', $width, $labels->[$i], $display->[$i];
1121+
} else {
1122+
print $padding . $display->[$i];
1123+
}
1124+
}
1125+
}
1126+
1127+
sub select_lines_loop {
1128+
my $hunk = shift;
1129+
display_hunk_lines($hunk);
1130+
my $selection = undef;
1131+
until (defined $selection) {
1132+
print colored $prompt_color, __("select lines? ");
1133+
my $text = <STDIN>;
1134+
defined $text and $text =~ /\S/ or return undef;
1135+
$selection = parse_hunk_selection($hunk, $text);
1136+
}
1137+
return select_hunk_lines($hunk, $selection);
1138+
}
1139+
10161140
my %edit_hunk_manually_modes = (
10171141
stage => N__(
10181142
"If the patch applies cleanly, the edited hunk will immediately be
@@ -1255,6 +1379,7 @@ sub help_patch_cmd {
12551379
J - leave this hunk undecided, see next hunk
12561380
k - leave this hunk undecided, see previous undecided hunk
12571381
K - leave this hunk undecided, see previous hunk
1382+
l - select hunk lines to use
12581383
s - split the current hunk into smaller hunks
12591384
e - manually edit the current hunk
12601385
? - print help
@@ -1471,6 +1596,9 @@ sub patch_update_file {
14711596
if ($hunk[$ix]{TYPE} eq 'hunk') {
14721597
$other .= ',e';
14731598
}
1599+
if (label_hunk_lines($hunk[$ix])) {
1600+
$other .= ',l';
1601+
}
14741602
for (@{$hunk[$ix]{DISPLAY}}) {
14751603
print;
14761604
}
@@ -1610,6 +1738,18 @@ sub patch_update_file {
16101738
next;
16111739
}
16121740
}
1741+
elsif ($line =~ /^l/) {
1742+
unless ($other =~ /l/) {
1743+
error_msg __("Cannot select line by line\n");
1744+
next;
1745+
}
1746+
my $newhunk = select_lines_loop($hunk[$ix]);
1747+
if ($newhunk) {
1748+
splice @hunk, $ix, 1, $newhunk;
1749+
} else {
1750+
next;
1751+
}
1752+
}
16131753
elsif ($other =~ /s/ && $line =~ /^s/) {
16141754
my @split = split_hunk($hunk[$ix]{TEXT}, $hunk[$ix]{DISPLAY});
16151755
if (1 < @split) {

t/t3701-add-interactive.sh

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,63 @@ test_expect_failure 'split hunk "add -p (no, yes, edit)"' '
360360
! grep "^+31" actual
361361
'
362362

363+
test_expect_success 'setup expected diff' '
364+
cat >expected <<-\EOF
365+
diff --git a/test b/test
366+
index 0889435..341cc6b 100644
367+
--- a/test
368+
+++ b/test
369+
@@ -1,6 +1,9 @@
370+
+5
371+
10
372+
20
373+
+21
374+
30
375+
40
376+
50
377+
60
378+
+61
379+
\ No newline at end of file
380+
EOF
381+
'
382+
383+
test_expect_success 'can stage individual lines of patch' '
384+
git reset &&
385+
printf 61 >>test &&
386+
printf "%s\n" l "1,2 4-" |
387+
EDITOR=: git add -p 2>error &&
388+
test_must_be_empty error &&
389+
git diff --cached HEAD >actual &&
390+
diff_cmp expected actual
391+
'
392+
393+
test_expect_success 'setup expected diff' '
394+
cat >expected <<-\EOF
395+
diff --git a/test b/test
396+
index 0889435..cc6163b 100644
397+
--- a/test
398+
+++ b/test
399+
@@ -1,6 +1,8 @@
400+
+5
401+
10
402+
20
403+
30
404+
40
405+
50
406+
60
407+
+61
408+
\ No newline at end of file
409+
EOF
410+
'
411+
412+
test_expect_success 'can reset individual lines of patch' '
413+
printf "%s\n" l 2 |
414+
EDITOR=: git reset -p 2>error &&
415+
test_must_be_empty error &&
416+
git diff --cached HEAD >actual &&
417+
diff_cmp expected actual
418+
'
419+
363420
test_expect_success 'patch mode ignores unmerged entries' '
364421
git reset --hard &&
365422
test_commit conflict &&
@@ -528,4 +585,12 @@ test_expect_success 'add -p patch editing works with pathological context lines'
528585
test_cmp expected-2 actual
529586
'
530587

588+
test_expect_success 'add -p selecting lines works with pathological context lines' '
589+
git reset &&
590+
printf "%s\n" l 2 y |
591+
GIT_EDITOR=./editor git add -p &&
592+
git cat-file blob :a >actual &&
593+
test_cmp expected-2 actual
594+
'
595+
531596
test_done

0 commit comments

Comments
 (0)