Skip to content

Commit 3d747bd

Browse files
committed
Merge branch 'pw/add-p-select' into pu
"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 b04ac4f + 05f700e commit 3d747bd

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
@@ -1028,6 +1028,171 @@ sub color_diff {
10281028
} @_;
10291029
}
10301030

1031+
sub label_hunk_lines {
1032+
local $_;
1033+
my $hunk = shift;
1034+
my $i = 0;
1035+
my $labels = [ map { /^[-+]/ ? ++$i : 0 } @{$hunk->{TEXT}} ];
1036+
if ($i > 1) {
1037+
@{$hunk}{qw(LABELS MAX_LABEL)} = ($labels, $i);
1038+
return 1;
1039+
}
1040+
return 0;
1041+
}
1042+
1043+
sub select_hunk_lines {
1044+
my ($hunk, $selected) = @_;
1045+
my ($text, $labels) = @{$hunk}{qw(TEXT LABELS)};
1046+
my ($i, $o_cnt, $n_cnt) = (0, 0, 0);
1047+
my ($push_eol, @newtext);
1048+
# Lines with this mode will become context lines if they are
1049+
# not selected
1050+
my $context_mode = $patch_mode_flavour{IS_REVERSE} ? '+' : '-';
1051+
for $i (1..$#{$text}) {
1052+
my $mode = substr($text->[$i], 0, 1);
1053+
if ($mode eq '\\') {
1054+
push @newtext, $text->[$i] if ($push_eol);
1055+
undef $push_eol;
1056+
} elsif ($labels->[$i] and $selected->[$labels->[$i]]) {
1057+
push @newtext, $text->[$i];
1058+
if ($mode eq '+') {
1059+
$n_cnt++;
1060+
} else {
1061+
$o_cnt++;
1062+
}
1063+
$push_eol = 1;
1064+
} elsif ($mode eq ' ' or $mode eq $context_mode) {
1065+
push @newtext, ' ' . substr($text->[$i], 1);
1066+
$o_cnt++; $n_cnt++;
1067+
$push_eol = 1;
1068+
} else {
1069+
undef $push_eol;
1070+
}
1071+
}
1072+
my ($o_ofs, $orig_o_cnt, $n_ofs, $orig_n_cnt) =
1073+
parse_hunk_header($text->[0]);
1074+
unshift @newtext, format_hunk_header($o_ofs, $o_cnt, $n_ofs, $n_cnt);
1075+
my $newhunk = {
1076+
TEXT => \@newtext,
1077+
DISPLAY => [ color_diff(@newtext) ],
1078+
OFS_DELTA => $orig_o_cnt - $orig_n_cnt - $o_cnt + $n_cnt,
1079+
TYPE => $hunk->{TYPE},
1080+
USE => 1,
1081+
};
1082+
# If this hunk has previously been edited add the offset delta
1083+
# of the old hunk to get the real delta from the original
1084+
# hunk.
1085+
if ($hunk->{OFS_DELTA}) {
1086+
$newhunk->{OFS_DELTA} += $hunk->{OFS_DELTA};
1087+
}
1088+
return $newhunk;
1089+
}
1090+
1091+
sub check_hunk_label {
1092+
my ($max_label, $label) = ($_[0]->{MAX_LABEL}, $_[1]);
1093+
if ($label < 1 or $label > $max_label) {
1094+
error_msg sprintf(__("invalid hunk line '%d'\n"), $label);
1095+
return 0;
1096+
}
1097+
return 1;
1098+
}
1099+
1100+
sub split_hunk_selection {
1101+
local $_;
1102+
my @fields = @_;
1103+
my @ret;
1104+
for (@fields) {
1105+
while ($_ ne '') {
1106+
if (/^[0-9]-$/) {
1107+
push @ret, $_;
1108+
last;
1109+
} elsif (/^([0-9](?:-[0-9])?)(.*)/) {
1110+
push @ret, $1;
1111+
$_ = $2;
1112+
} else {
1113+
error_msg sprintf
1114+
__("invalid hunk line '%s'\n"),
1115+
substr($_, 0, 1);
1116+
return ();
1117+
}
1118+
}
1119+
}
1120+
return @ret;
1121+
}
1122+
1123+
sub parse_hunk_selection {
1124+
local $_;
1125+
my ($hunk, $line) = @_;
1126+
my ($max_label, $invert) = ($hunk->{MAX_LABEL}, undef);
1127+
my @selected = (0) x ($max_label + 1);
1128+
my @fields = split(/[,\s]+/, $line);
1129+
if ($fields[0] =~ /^-(.*)/) {
1130+
$invert = 1;
1131+
if ($1 ne '') {
1132+
$fields[0] = $1;
1133+
} else {
1134+
shift @fields;
1135+
unless (@fields) {
1136+
error_msg __("no lines to invert\n");
1137+
return undef;
1138+
}
1139+
}
1140+
}
1141+
if ($max_label < 10) {
1142+
@fields = split_hunk_selection(@fields) or return undef;
1143+
}
1144+
for (@fields) {
1145+
if (my ($lo, $hi) = /^([0-9]+)-([0-9]*)$/) {
1146+
if ($hi eq '') {
1147+
$hi = $max_label;
1148+
}
1149+
check_hunk_label($hunk, $lo) or return undef;
1150+
check_hunk_label($hunk, $hi) or return undef;
1151+
if ($hi < $lo) {
1152+
($lo, $hi) = ($hi, $lo);
1153+
}
1154+
@selected[$lo..$hi] = (1) x (1 + $hi - $lo);
1155+
} elsif (/^([0-9]+)$/) {
1156+
check_hunk_label($hunk, $1) or return undef;
1157+
$selected[$1] = 1;
1158+
} else {
1159+
error_msg sprintf(__("invalid hunk line '%s'\n"), $_);
1160+
return undef;
1161+
}
1162+
}
1163+
if ($invert) {
1164+
@selected = map { !$_ } @selected;
1165+
}
1166+
return \@selected;
1167+
}
1168+
1169+
sub display_hunk_lines {
1170+
my ($display, $labels, $max_label) =
1171+
@{$_[0]}{qw(DISPLAY LABELS MAX_LABEL)};
1172+
my $width = int(log($max_label) / log(10)) + 1;
1173+
my $padding = ' ' x ($width + 1);
1174+
for my $i (0..$#{$display}) {
1175+
if ($labels->[$i]) {
1176+
printf '%*d %s', $width, $labels->[$i], $display->[$i];
1177+
} else {
1178+
print $padding . $display->[$i];
1179+
}
1180+
}
1181+
}
1182+
1183+
sub select_lines_loop {
1184+
my $hunk = shift;
1185+
display_hunk_lines($hunk);
1186+
my $selection = undef;
1187+
until (defined $selection) {
1188+
print colored $prompt_color, __("select lines? ");
1189+
my $text = <STDIN>;
1190+
defined $text and $text =~ /\S/ or return undef;
1191+
$selection = parse_hunk_selection($hunk, $text);
1192+
}
1193+
return select_hunk_lines($hunk, $selection);
1194+
}
1195+
10311196
my %edit_hunk_manually_modes = (
10321197
stage => N__(
10331198
"If the patch applies cleanly, the edited hunk will immediately be
@@ -1276,6 +1441,7 @@ sub help_patch_cmd {
12761441
J - leave this hunk undecided, see next hunk
12771442
k - leave this hunk undecided, see previous undecided hunk
12781443
K - leave this hunk undecided, see previous hunk
1444+
l - select hunk lines to use
12791445
s - split the current hunk into smaller hunks
12801446
e - manually edit the current hunk
12811447
? - print help
@@ -1492,6 +1658,9 @@ sub patch_update_file {
14921658
if ($hunk[$ix]{TYPE} eq 'hunk') {
14931659
$other .= ',e';
14941660
}
1661+
if (label_hunk_lines($hunk[$ix])) {
1662+
$other .= ',l';
1663+
}
14951664
for (@{$hunk[$ix]{DISPLAY}}) {
14961665
print;
14971666
}
@@ -1639,6 +1808,18 @@ sub patch_update_file {
16391808
next;
16401809
}
16411810
}
1811+
elsif ($line =~ /^l/) {
1812+
unless ($other =~ /l/) {
1813+
error_msg __("Cannot select line by line\n");
1814+
next;
1815+
}
1816+
my $newhunk = select_lines_loop($hunk[$ix]);
1817+
if ($newhunk) {
1818+
splice @hunk, $ix, 1, $newhunk;
1819+
} else {
1820+
next;
1821+
}
1822+
}
16421823
elsif ($line =~ /^s/) {
16431824
unless ($other =~ /s/) {
16441825
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
@@ -403,6 +403,63 @@ test_expect_failure 'split hunk "add -p (no, yes, edit)"' '
403403
! grep "^+31" actual
404404
'
405405

406+
test_expect_success 'setup expected diff' '
407+
cat >expected <<-\EOF
408+
diff --git a/test b/test
409+
index 0889435..341cc6b 100644
410+
--- a/test
411+
+++ b/test
412+
@@ -1,6 +1,9 @@
413+
+5
414+
10
415+
20
416+
+21
417+
30
418+
40
419+
50
420+
60
421+
+61
422+
\ No newline at end of file
423+
EOF
424+
'
425+
426+
test_expect_success 'can stage individual lines of patch' '
427+
git reset &&
428+
printf 61 >>test &&
429+
printf "%s\n" l "1,2 4-" |
430+
EDITOR=: git add -p 2>error &&
431+
test_must_be_empty error &&
432+
git diff --cached HEAD >actual &&
433+
diff_cmp expected actual
434+
'
435+
436+
test_expect_success 'setup expected diff' '
437+
cat >expected <<-\EOF
438+
diff --git a/test b/test
439+
index 0889435..cc6163b 100644
440+
--- a/test
441+
+++ b/test
442+
@@ -1,6 +1,8 @@
443+
+5
444+
10
445+
20
446+
30
447+
40
448+
50
449+
60
450+
+61
451+
\ No newline at end of file
452+
EOF
453+
'
454+
455+
test_expect_success 'can reset individual lines of patch' '
456+
printf "%s\n" l -13 |
457+
EDITOR=: git reset -p 2>error &&
458+
test_must_be_empty error &&
459+
git diff --cached HEAD >actual &&
460+
diff_cmp expected actual
461+
'
462+
406463
test_expect_success 'patch mode ignores unmerged entries' '
407464
git reset --hard &&
408465
test_commit conflict &&
@@ -639,4 +696,12 @@ test_expect_success 'add -p patch editing works with pathological context lines'
639696
test_cmp expected-2 actual
640697
'
641698

699+
test_expect_success 'add -p selecting lines works with pathological context lines' '
700+
git reset &&
701+
printf "%s\n" l 2 y |
702+
GIT_EDITOR=./editor git add -p &&
703+
git cat-file blob :a >actual &&
704+
test_cmp expected-2 actual
705+
'
706+
642707
test_done

0 commit comments

Comments
 (0)