Skip to content

Commit eae69f5

Browse files
committed
Merge branch 'pw/add-p-select' into next
"git add -p" interactive interface learned to let users choose individual added/removed lines to be used in the operation, instead of accepting or rejecting a whole hunk. * pw/add-p-select: add -p: optimize line selection for short hunks add -p: allow line selection to be inverted add -p: select individual hunk lines
2 parents c48e98c + 05f700e commit eae69f5

File tree

3 files changed

+256
-0
lines changed

3 files changed

+256
-0
lines changed

Documentation/git-add.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,10 +332,20 @@ 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 (these can be omitted if there are fewer than ten
344+
labelled lines), to specify a range of lines use a dash between them. If
345+
the upper bound of a range of lines is omitted it defaults to the last
346+
line. To invert the selection prefix it with "-" so "-3-5,8" will select
347+
everything except lines 3, 4, 5 and 8.
348+
+
339349
After deciding the fate for all hunks, if there is any hunk
340350
that was chosen, the index is updated with the selected hunks.
341351
+

git-add--interactive.perl

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,6 +1021,171 @@ sub color_diff {
10211021
} @_;
10221022
}
10231023

1024+
sub label_hunk_lines {
1025+
local $_;
1026+
my $hunk = shift;
1027+
my $i = 0;
1028+
my $labels = [ map { /^[-+]/ ? ++$i : 0 } @{$hunk->{TEXT}} ];
1029+
if ($i > 1) {
1030+
@{$hunk}{qw(LABELS MAX_LABEL)} = ($labels, $i);
1031+
return 1;
1032+
}
1033+
return 0;
1034+
}
1035+
1036+
sub select_hunk_lines {
1037+
my ($hunk, $selected) = @_;
1038+
my ($text, $labels) = @{$hunk}{qw(TEXT LABELS)};
1039+
my ($i, $o_cnt, $n_cnt) = (0, 0, 0);
1040+
my ($push_eol, @newtext);
1041+
# Lines with this mode will become context lines if they are
1042+
# not selected
1043+
my $context_mode = $patch_mode_flavour{IS_REVERSE} ? '+' : '-';
1044+
for $i (1..$#{$text}) {
1045+
my $mode = substr($text->[$i], 0, 1);
1046+
if ($mode eq '\\') {
1047+
push @newtext, $text->[$i] if ($push_eol);
1048+
undef $push_eol;
1049+
} elsif ($labels->[$i] and $selected->[$labels->[$i]]) {
1050+
push @newtext, $text->[$i];
1051+
if ($mode eq '+') {
1052+
$n_cnt++;
1053+
} else {
1054+
$o_cnt++;
1055+
}
1056+
$push_eol = 1;
1057+
} elsif ($mode eq ' ' or $mode eq $context_mode) {
1058+
push @newtext, ' ' . substr($text->[$i], 1);
1059+
$o_cnt++; $n_cnt++;
1060+
$push_eol = 1;
1061+
} else {
1062+
undef $push_eol;
1063+
}
1064+
}
1065+
my ($o_ofs, $orig_o_cnt, $n_ofs, $orig_n_cnt) =
1066+
parse_hunk_header($text->[0]);
1067+
unshift @newtext, format_hunk_header($o_ofs, $o_cnt, $n_ofs, $n_cnt);
1068+
my $newhunk = {
1069+
TEXT => \@newtext,
1070+
DISPLAY => [ color_diff(@newtext) ],
1071+
OFS_DELTA => $orig_o_cnt - $orig_n_cnt - $o_cnt + $n_cnt,
1072+
TYPE => $hunk->{TYPE},
1073+
USE => 1,
1074+
};
1075+
# If this hunk has previously been edited add the offset delta
1076+
# of the old hunk to get the real delta from the original
1077+
# hunk.
1078+
if ($hunk->{OFS_DELTA}) {
1079+
$newhunk->{OFS_DELTA} += $hunk->{OFS_DELTA};
1080+
}
1081+
return $newhunk;
1082+
}
1083+
1084+
sub check_hunk_label {
1085+
my ($max_label, $label) = ($_[0]->{MAX_LABEL}, $_[1]);
1086+
if ($label < 1 or $label > $max_label) {
1087+
error_msg sprintf(__("invalid hunk line '%d'\n"), $label);
1088+
return 0;
1089+
}
1090+
return 1;
1091+
}
1092+
1093+
sub split_hunk_selection {
1094+
local $_;
1095+
my @fields = @_;
1096+
my @ret;
1097+
for (@fields) {
1098+
while ($_ ne '') {
1099+
if (/^[0-9]-$/) {
1100+
push @ret, $_;
1101+
last;
1102+
} elsif (/^([0-9](?:-[0-9])?)(.*)/) {
1103+
push @ret, $1;
1104+
$_ = $2;
1105+
} else {
1106+
error_msg sprintf
1107+
__("invalid hunk line '%s'\n"),
1108+
substr($_, 0, 1);
1109+
return ();
1110+
}
1111+
}
1112+
}
1113+
return @ret;
1114+
}
1115+
1116+
sub parse_hunk_selection {
1117+
local $_;
1118+
my ($hunk, $line) = @_;
1119+
my ($max_label, $invert) = ($hunk->{MAX_LABEL}, undef);
1120+
my @selected = (0) x ($max_label + 1);
1121+
my @fields = split(/[,\s]+/, $line);
1122+
if ($fields[0] =~ /^-(.*)/) {
1123+
$invert = 1;
1124+
if ($1 ne '') {
1125+
$fields[0] = $1;
1126+
} else {
1127+
shift @fields;
1128+
unless (@fields) {
1129+
error_msg __("no lines to invert\n");
1130+
return undef;
1131+
}
1132+
}
1133+
}
1134+
if ($max_label < 10) {
1135+
@fields = split_hunk_selection(@fields) or return undef;
1136+
}
1137+
for (@fields) {
1138+
if (my ($lo, $hi) = /^([0-9]+)-([0-9]*)$/) {
1139+
if ($hi eq '') {
1140+
$hi = $max_label;
1141+
}
1142+
check_hunk_label($hunk, $lo) or return undef;
1143+
check_hunk_label($hunk, $hi) or return undef;
1144+
if ($hi < $lo) {
1145+
($lo, $hi) = ($hi, $lo);
1146+
}
1147+
@selected[$lo..$hi] = (1) x (1 + $hi - $lo);
1148+
} elsif (/^([0-9]+)$/) {
1149+
check_hunk_label($hunk, $1) or return undef;
1150+
$selected[$1] = 1;
1151+
} else {
1152+
error_msg sprintf(__("invalid hunk line '%s'\n"), $_);
1153+
return undef;
1154+
}
1155+
}
1156+
if ($invert) {
1157+
@selected = map { !$_ } @selected;
1158+
}
1159+
return \@selected;
1160+
}
1161+
1162+
sub display_hunk_lines {
1163+
my ($display, $labels, $max_label) =
1164+
@{$_[0]}{qw(DISPLAY LABELS MAX_LABEL)};
1165+
my $width = int(log($max_label) / log(10)) + 1;
1166+
my $padding = ' ' x ($width + 1);
1167+
for my $i (0..$#{$display}) {
1168+
if ($labels->[$i]) {
1169+
printf '%*d %s', $width, $labels->[$i], $display->[$i];
1170+
} else {
1171+
print $padding . $display->[$i];
1172+
}
1173+
}
1174+
}
1175+
1176+
sub select_lines_loop {
1177+
my $hunk = shift;
1178+
display_hunk_lines($hunk);
1179+
my $selection = undef;
1180+
until (defined $selection) {
1181+
print colored $prompt_color, __("select lines? ");
1182+
my $text = <STDIN>;
1183+
defined $text and $text =~ /\S/ or return undef;
1184+
$selection = parse_hunk_selection($hunk, $text);
1185+
}
1186+
return select_hunk_lines($hunk, $selection);
1187+
}
1188+
10241189
my %edit_hunk_manually_modes = (
10251190
stage => N__(
10261191
"If the patch applies cleanly, the edited hunk will immediately be
@@ -1269,6 +1434,7 @@ sub help_patch_cmd {
12691434
J - leave this hunk undecided, see next hunk
12701435
k - leave this hunk undecided, see previous undecided hunk
12711436
K - leave this hunk undecided, see previous hunk
1437+
l - select hunk lines to use
12721438
s - split the current hunk into smaller hunks
12731439
e - manually edit the current hunk
12741440
? - print help
@@ -1485,6 +1651,9 @@ sub patch_update_file {
14851651
if ($hunk[$ix]{TYPE} eq 'hunk') {
14861652
$other .= ',e';
14871653
}
1654+
if (label_hunk_lines($hunk[$ix])) {
1655+
$other .= ',l';
1656+
}
14881657
for (@{$hunk[$ix]{DISPLAY}}) {
14891658
print;
14901659
}
@@ -1632,6 +1801,18 @@ sub patch_update_file {
16321801
next;
16331802
}
16341803
}
1804+
elsif ($line =~ /^l/) {
1805+
unless ($other =~ /l/) {
1806+
error_msg __("Cannot select line by line\n");
1807+
next;
1808+
}
1809+
my $newhunk = select_lines_loop($hunk[$ix]);
1810+
if ($newhunk) {
1811+
splice @hunk, $ix, 1, $newhunk;
1812+
} else {
1813+
next;
1814+
}
1815+
}
16351816
elsif ($line =~ /^s/) {
16361817
unless ($other =~ /s/) {
16371818
error_msg __("Sorry, cannot split this hunk\n");

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 -13 |
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 &&
@@ -596,4 +653,12 @@ test_expect_success 'add -p patch editing works with pathological context lines'
596653
test_cmp expected-2 actual
597654
'
598655

656+
test_expect_success 'add -p selecting lines works with pathological context lines' '
657+
git reset &&
658+
printf "%s\n" l 2 y |
659+
GIT_EDITOR=./editor git add -p &&
660+
git cat-file blob :a >actual &&
661+
test_cmp expected-2 actual
662+
'
663+
599664
test_done

0 commit comments

Comments
 (0)