]> git.ipfire.org Git - thirdparty/git.git/blame - git-add--interactive.perl
verify_path: drop clever fallthrough
[thirdparty/git.git] / git-add--interactive.perl
CommitLineData
3328aced 1#!/usr/bin/perl
5cde71d6 2
d48b2841 3use 5.008;
5cde71d6 4use strict;
3328aced 5use warnings;
1d542a54 6use Git qw(unquote_path);
258e7790 7use Git::I18N;
b4c61ed6 8
8851f480
JH
9binmode(STDOUT, ":raw");
10
b4c61ed6
JH
11my $repo = Git->repository();
12
f87e310d
JK
13my $menu_use_color = $repo->get_colorbool('color.interactive');
14my ($prompt_color, $header_color, $help_color) =
15 $menu_use_color ? (
16 $repo->get_color('color.interactive.prompt', 'bold blue'),
17 $repo->get_color('color.interactive.header', 'bold'),
18 $repo->get_color('color.interactive.help', 'red bold'),
19 ) : ();
a3019736
TR
20my $error_color = ();
21if ($menu_use_color) {
9aad6cba
SB
22 my $help_color_spec = ($repo->config('color.interactive.help') or
23 'red bold');
a3019736
TR
24 $error_color = $repo->get_color('color.interactive.error',
25 $help_color_spec);
26}
b4c61ed6 27
f87e310d
JK
28my $diff_use_color = $repo->get_colorbool('color.diff');
29my ($fraginfo_color) =
30 $diff_use_color ? (
31 $repo->get_color('color.diff.frag', 'cyan'),
32 ) : ();
ac083c47
TR
33my ($diff_plain_color) =
34 $diff_use_color ? (
35 $repo->get_color('color.diff.plain', ''),
36 ) : ();
37my ($diff_old_color) =
38 $diff_use_color ? (
39 $repo->get_color('color.diff.old', 'red'),
40 ) : ();
41my ($diff_new_color) =
42 $diff_use_color ? (
43 $repo->get_color('color.diff.new', 'green'),
44 ) : ();
b4c61ed6 45
f87e310d 46my $normal_color = $repo->get_color("", "reset");
b4c61ed6 47
2cc0f53b 48my $diff_algorithm = $repo->config('diff.algorithm');
433860f3 49my $diff_indent_heuristic = $repo->config_bool('diff.indentheuristic');
01143847 50my $diff_filter = $repo->config('interactive.difffilter');
2cc0f53b 51
ca6ac7f1 52my $use_readkey = 0;
b5cc0032
TR
53my $use_termcap = 0;
54my %term_escapes;
55
748aa689
TR
56sub ReadMode;
57sub ReadKey;
ca6ac7f1
TR
58if ($repo->config_bool("interactive.singlekey")) {
59 eval {
748aa689
TR
60 require Term::ReadKey;
61 Term::ReadKey->import;
ca6ac7f1
TR
62 $use_readkey = 1;
63 };
b294097b
SR
64 if (!$use_readkey) {
65 print STDERR "missing Term::ReadKey, disabling interactive.singlekey\n";
66 }
b5cc0032
TR
67 eval {
68 require Term::Cap;
69 my $termcap = Term::Cap->Tgetent;
70 foreach (values %$termcap) {
71 $term_escapes{$_} = 1 if /^\e/;
72 }
73 $use_termcap = 1;
74 };
ca6ac7f1
TR
75}
76
b4c61ed6
JH
77sub colored {
78 my $color = shift;
79 my $string = join("", @_);
80
f87e310d 81 if (defined $color) {
b4c61ed6
JH
82 # Put a color code at the beginning of each line, a reset at the end
83 # color after newlines that are not at the end of the string
84 $string =~ s/(\n+)(.)/$1$color$2/g;
85 # reset before newlines
86 $string =~ s/(\n+)/$normal_color$1/g;
87 # codes at beginning and end (if necessary):
88 $string =~ s/^/$color/;
89 $string =~ s/$/$normal_color/ unless $string =~ /\n$/;
90 }
91 return $string;
92}
5cde71d6 93
b63e9950 94# command line options
c852bd54 95my $patch_mode_only;
b63e9950 96my $patch_mode;
d002ef4d 97my $patch_mode_revision;
b63e9950 98
8f0bef6d 99sub apply_patch;
4f353658 100sub apply_patch_for_checkout_commit;
dda1f2a5 101sub apply_patch_for_stash;
8f0bef6d
TR
102
103my %patch_modes = (
104 'stage' => {
105 DIFF => 'diff-files -p',
106 APPLY => sub { apply_patch 'apply --cached', @_; },
107 APPLY_CHECK => 'apply --cached',
8f0bef6d 108 FILTER => 'file-only',
7b8c7051 109 IS_REVERSE => 0,
8f0bef6d 110 },
dda1f2a5
TR
111 'stash' => {
112 DIFF => 'diff-index -p HEAD',
113 APPLY => sub { apply_patch 'apply --cached', @_; },
114 APPLY_CHECK => 'apply --cached',
dda1f2a5 115 FILTER => undef,
7b8c7051 116 IS_REVERSE => 0,
dda1f2a5 117 },
d002ef4d
TR
118 'reset_head' => {
119 DIFF => 'diff-index -p --cached',
120 APPLY => sub { apply_patch 'apply -R --cached', @_; },
121 APPLY_CHECK => 'apply -R --cached',
d002ef4d 122 FILTER => 'index-only',
7b8c7051 123 IS_REVERSE => 1,
d002ef4d
TR
124 },
125 'reset_nothead' => {
126 DIFF => 'diff-index -R -p --cached',
127 APPLY => sub { apply_patch 'apply --cached', @_; },
128 APPLY_CHECK => 'apply --cached',
d002ef4d 129 FILTER => 'index-only',
7b8c7051 130 IS_REVERSE => 0,
d002ef4d 131 },
4f353658
TR
132 'checkout_index' => {
133 DIFF => 'diff-files -p',
134 APPLY => sub { apply_patch 'apply -R', @_; },
135 APPLY_CHECK => 'apply -R',
4f353658 136 FILTER => 'file-only',
7b8c7051 137 IS_REVERSE => 1,
4f353658
TR
138 },
139 'checkout_head' => {
140 DIFF => 'diff-index -p',
141 APPLY => sub { apply_patch_for_checkout_commit '-R', @_ },
142 APPLY_CHECK => 'apply -R',
4f353658 143 FILTER => undef,
7b8c7051 144 IS_REVERSE => 1,
4f353658
TR
145 },
146 'checkout_nothead' => {
147 DIFF => 'diff-index -R -p',
148 APPLY => sub { apply_patch_for_checkout_commit '', @_ },
149 APPLY_CHECK => 'apply',
4f353658 150 FILTER => undef,
7b8c7051 151 IS_REVERSE => 0,
4f353658 152 },
8f0bef6d
TR
153);
154
0539d5e6
VA
155$patch_mode = 'stage';
156my %patch_mode_flavour = %{$patch_modes{$patch_mode}};
b63e9950 157
5cde71d6 158sub run_cmd_pipe {
df17e77c 159 if ($^O eq 'MSWin32') {
21e9757e
AR
160 my @invalid = grep {m/[":*]/} @_;
161 die "$^O does not support: @invalid\n" if @invalid;
162 my @args = map { m/ /o ? "\"$_\"": $_ } @_;
163 return qx{@args};
164 } else {
165 my $fh = undef;
166 open($fh, '-|', @_) or die;
167 return <$fh>;
168 }
5cde71d6
JH
169}
170
171my ($GIT_DIR) = run_cmd_pipe(qw(git rev-parse --git-dir));
172
173if (!defined $GIT_DIR) {
174 exit(1); # rev-parse would have already said "not a git repo"
175}
176chomp($GIT_DIR);
177
178sub refresh {
179 my $fh;
21e9757e 180 open $fh, 'git update-index --refresh |'
5cde71d6
JH
181 or die;
182 while (<$fh>) {
183 ;# ignore 'needs update'
184 }
185 close $fh;
186}
187
188sub list_untracked {
189 map {
190 chomp $_;
8851f480 191 unquote_path($_);
5cde71d6 192 }
4c841684 193 run_cmd_pipe(qw(git ls-files --others --exclude-standard --), @ARGV);
5cde71d6
JH
194}
195
258e7790
VA
196# TRANSLATORS: you can adjust this to align "git add -i" status menu
197my $status_fmt = __('%12s %12s %s');
198my $status_head = sprintf($status_fmt, __('staged'), __('unstaged'), __('path'));
5cde71d6 199
18bc7616
JK
200{
201 my $initial;
202 sub is_initial_commit {
203 $initial = system('git rev-parse HEAD -- >/dev/null 2>&1') != 0
204 unless defined $initial;
205 return $initial;
206 }
207}
208
209sub get_empty_tree {
210 return '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
211}
212
954312a3
JK
213sub get_diff_reference {
214 my $ref = shift;
215 if (defined $ref and $ref ne 'HEAD') {
216 return $ref;
217 } elsif (is_initial_commit()) {
218 return get_empty_tree();
219 } else {
220 return 'HEAD';
221 }
222}
223
5cde71d6 224# Returns list of hashes, contents of each of which are:
5cde71d6
JH
225# VALUE: pathname
226# BINARY: is a binary path
227# INDEX: is index different from HEAD?
228# FILE: is file different from index?
229# INDEX_ADDDEL: is it add/delete between HEAD and index?
230# FILE_ADDDEL: is it add/delete between index and file?
4066bd67 231# UNMERGED: is the path unmerged
5cde71d6
JH
232
233sub list_modified {
234 my ($only) = @_;
235 my (%data, @return);
236 my ($add, $del, $adddel, $file);
237
954312a3 238 my $reference = get_diff_reference($patch_mode_revision);
5cde71d6 239 for (run_cmd_pipe(qw(git diff-index --cached
18bc7616 240 --numstat --summary), $reference,
7288e12c 241 '--', @ARGV)) {
5cde71d6
JH
242 if (($add, $del, $file) =
243 /^([-\d]+) ([-\d]+) (.*)/) {
244 my ($change, $bin);
8851f480 245 $file = unquote_path($file);
5cde71d6 246 if ($add eq '-' && $del eq '-') {
55aa0442 247 $change = __('binary');
5cde71d6
JH
248 $bin = 1;
249 }
250 else {
251 $change = "+$add/-$del";
252 }
253 $data{$file} = {
254 INDEX => $change,
255 BINARY => $bin,
55aa0442 256 FILE => __('nothing'),
5cde71d6
JH
257 }
258 }
259 elsif (($adddel, $file) =
260 /^ (create|delete) mode [0-7]+ (.*)$/) {
8851f480 261 $file = unquote_path($file);
5cde71d6
JH
262 $data{$file}{INDEX_ADDDEL} = $adddel;
263 }
264 }
265
7288e12c 266 for (run_cmd_pipe(qw(git diff-files --numstat --summary --raw --), @ARGV)) {
5cde71d6
JH
267 if (($add, $del, $file) =
268 /^([-\d]+) ([-\d]+) (.*)/) {
8851f480 269 $file = unquote_path($file);
5cde71d6
JH
270 my ($change, $bin);
271 if ($add eq '-' && $del eq '-') {
55aa0442 272 $change = __('binary');
5cde71d6
JH
273 $bin = 1;
274 }
275 else {
276 $change = "+$add/-$del";
277 }
278 $data{$file}{FILE} = $change;
279 if ($bin) {
280 $data{$file}{BINARY} = 1;
281 }
282 }
283 elsif (($adddel, $file) =
284 /^ (create|delete) mode [0-7]+ (.*)$/) {
8851f480 285 $file = unquote_path($file);
5cde71d6
JH
286 $data{$file}{FILE_ADDDEL} = $adddel;
287 }
4066bd67
JK
288 elsif (/^:[0-7]+ [0-7]+ [0-9a-f]+ [0-9a-f]+ (.) (.*)$/) {
289 $file = unquote_path($2);
290 if (!exists $data{$file}) {
291 $data{$file} = +{
55aa0442 292 INDEX => __('unchanged'),
4066bd67
JK
293 BINARY => 0,
294 };
295 }
296 if ($1 eq 'U') {
297 $data{$file}{UNMERGED} = 1;
298 }
299 }
5cde71d6
JH
300 }
301
302 for (sort keys %data) {
303 my $it = $data{$_};
304
305 if ($only) {
306 if ($only eq 'index-only') {
55aa0442 307 next if ($it->{INDEX} eq __('unchanged'));
5cde71d6
JH
308 }
309 if ($only eq 'file-only') {
55aa0442 310 next if ($it->{FILE} eq __('nothing'));
5cde71d6
JH
311 }
312 }
313 push @return, +{
314 VALUE => $_,
5cde71d6
JH
315 %$it,
316 };
317 }
318 return @return;
319}
320
321sub find_unique {
322 my ($string, @stuff) = @_;
323 my $found = undef;
324 for (my $i = 0; $i < @stuff; $i++) {
325 my $it = $stuff[$i];
326 my $hit = undef;
327 if (ref $it) {
328 if ((ref $it) eq 'ARRAY') {
329 $it = $it->[0];
330 }
331 else {
332 $it = $it->{VALUE};
333 }
334 }
335 eval {
336 if ($it =~ /^$string/) {
337 $hit = 1;
338 };
339 };
340 if (defined $hit && defined $found) {
341 return undef;
342 }
343 if ($hit) {
344 $found = $i + 1;
345 }
346 }
347 return $found;
348}
349
14cb5038
WC
350# inserts string into trie and updates count for each character
351sub update_trie {
352 my ($trie, $string) = @_;
353 foreach (split //, $string) {
354 $trie = $trie->{$_} ||= {COUNT => 0};
355 $trie->{COUNT}++;
356 }
357}
358
359# returns an array of tuples (prefix, remainder)
360sub find_unique_prefixes {
361 my @stuff = @_;
362 my @return = ();
363
364 # any single prefix exceeding the soft limit is omitted
365 # if any prefix exceeds the hard limit all are omitted
366 # 0 indicates no limit
367 my $soft_limit = 0;
368 my $hard_limit = 3;
369
370 # build a trie modelling all possible options
371 my %trie;
372 foreach my $print (@stuff) {
373 if ((ref $print) eq 'ARRAY') {
374 $print = $print->[0];
375 }
63320989 376 elsif ((ref $print) eq 'HASH') {
14cb5038
WC
377 $print = $print->{VALUE};
378 }
379 update_trie(\%trie, $print);
380 push @return, $print;
381 }
382
383 # use the trie to find the unique prefixes
384 for (my $i = 0; $i < @return; $i++) {
385 my $ret = $return[$i];
386 my @letters = split //, $ret;
387 my %search = %trie;
388 my ($prefix, $remainder);
389 my $j;
390 for ($j = 0; $j < @letters; $j++) {
391 my $letter = $letters[$j];
392 if ($search{$letter}{COUNT} == 1) {
393 $prefix = substr $ret, 0, $j + 1;
394 $remainder = substr $ret, $j + 1;
395 last;
396 }
397 else {
398 my $prefix = substr $ret, 0, $j;
399 return ()
400 if ($hard_limit && $j + 1 > $hard_limit);
401 }
402 %search = %{$search{$letter}};
403 }
8851f480
JH
404 if (ord($letters[0]) > 127 ||
405 ($soft_limit && $j + 1 > $soft_limit)) {
14cb5038
WC
406 $prefix = undef;
407 $remainder = $ret;
408 }
409 $return[$i] = [$prefix, $remainder];
410 }
411 return @return;
412}
413
63320989
WC
414# filters out prefixes which have special meaning to list_and_choose()
415sub is_valid_prefix {
416 my $prefix = shift;
417 return (defined $prefix) &&
418 !($prefix =~ /[\s,]/) && # separators
419 !($prefix =~ /^-/) && # deselection
420 !($prefix =~ /^\d+/) && # selection
7e018be2
WC
421 ($prefix ne '*') && # "all" wildcard
422 ($prefix ne '?'); # prompt help
63320989
WC
423}
424
14cb5038
WC
425# given a prefix/remainder tuple return a string with the prefix highlighted
426# for now use square brackets; later might use ANSI colors (underline, bold)
427sub highlight_prefix {
428 my $prefix = shift;
429 my $remainder = shift;
b4c61ed6
JH
430
431 if (!defined $prefix) {
432 return $remainder;
433 }
434
435 if (!is_valid_prefix($prefix)) {
436 return "$prefix$remainder";
437 }
438
f87e310d 439 if (!$menu_use_color) {
b4c61ed6
JH
440 return "[$prefix]$remainder";
441 }
442
443 return "$prompt_color$prefix$normal_color$remainder";
14cb5038
WC
444}
445
a3019736
TR
446sub error_msg {
447 print STDERR colored $error_color, @_;
448}
449
5cde71d6
JH
450sub list_and_choose {
451 my ($opts, @stuff) = @_;
452 my (@chosen, @return);
a9c4641d
AK
453 if (!@stuff) {
454 return @return;
455 }
5cde71d6 456 my $i;
14cb5038 457 my @prefixes = find_unique_prefixes(@stuff) unless $opts->{LIST_ONLY};
5cde71d6
JH
458
459 TOPLOOP:
460 while (1) {
461 my $last_lf = 0;
462
463 if ($opts->{HEADER}) {
464 if (!$opts->{LIST_FLAT}) {
465 print " ";
466 }
b4c61ed6 467 print colored $header_color, "$opts->{HEADER}\n";
5cde71d6
JH
468 }
469 for ($i = 0; $i < @stuff; $i++) {
470 my $chosen = $chosen[$i] ? '*' : ' ';
471 my $print = $stuff[$i];
63320989
WC
472 my $ref = ref $print;
473 my $highlighted = highlight_prefix(@{$prefixes[$i]})
474 if @prefixes;
475 if ($ref eq 'ARRAY') {
476 $print = $highlighted || $print->[0];
477 }
478 elsif ($ref eq 'HASH') {
479 my $value = $highlighted || $print->{VALUE};
480 $print = sprintf($status_fmt,
481 $print->{INDEX},
482 $print->{FILE},
483 $value);
484 }
485 else {
486 $print = $highlighted || $print;
5cde71d6
JH
487 }
488 printf("%s%2d: %s", $chosen, $i+1, $print);
489 if (($opts->{LIST_FLAT}) &&
490 (($i + 1) % ($opts->{LIST_FLAT}))) {
491 print "\t";
492 $last_lf = 0;
493 }
494 else {
495 print "\n";
496 $last_lf = 1;
497 }
498 }
499 if (!$last_lf) {
500 print "\n";
501 }
502
503 return if ($opts->{LIST_ONLY});
504
b4c61ed6 505 print colored $prompt_color, $opts->{PROMPT};
5cde71d6
JH
506 if ($opts->{SINGLETON}) {
507 print "> ";
508 }
509 else {
510 print ">> ";
511 }
512 my $line = <STDIN>;
c95c0248
JLH
513 if (!$line) {
514 print "\n";
515 $opts->{ON_EOF}->() if $opts->{ON_EOF};
516 last;
517 }
5cde71d6 518 chomp $line;
6a6eb3d0 519 last if $line eq '';
7e018be2
WC
520 if ($line eq '?') {
521 $opts->{SINGLETON} ?
522 singleton_prompt_help_cmd() :
523 prompt_help_cmd();
524 next TOPLOOP;
525 }
5cde71d6
JH
526 for my $choice (split(/[\s,]+/, $line)) {
527 my $choose = 1;
528 my ($bottom, $top);
529
530 # Input that begins with '-'; unchoose
531 if ($choice =~ s/^-//) {
532 $choose = 0;
533 }
1e5aaa6d
CM
534 # A range can be specified like 5-7 or 5-.
535 if ($choice =~ /^(\d+)-(\d*)$/) {
536 ($bottom, $top) = ($1, length($2) ? $2 : 1 + @stuff);
5cde71d6
JH
537 }
538 elsif ($choice =~ /^\d+$/) {
539 $bottom = $top = $choice;
540 }
541 elsif ($choice eq '*') {
542 $bottom = 1;
543 $top = 1 + @stuff;
544 }
545 else {
546 $bottom = $top = find_unique($choice, @stuff);
547 if (!defined $bottom) {
13c58c17 548 error_msg sprintf(__("Huh (%s)?\n"), $choice);
5cde71d6
JH
549 next TOPLOOP;
550 }
551 }
552 if ($opts->{SINGLETON} && $bottom != $top) {
13c58c17 553 error_msg sprintf(__("Huh (%s)?\n"), $choice);
5cde71d6
JH
554 next TOPLOOP;
555 }
556 for ($i = $bottom-1; $i <= $top-1; $i++) {
6a6eb3d0 557 next if (@stuff <= $i || $i < 0);
5cde71d6 558 $chosen[$i] = $choose;
5cde71d6
JH
559 }
560 }
12db334e 561 last if ($opts->{IMMEDIATE} || $line eq '*');
5cde71d6
JH
562 }
563 for ($i = 0; $i < @stuff; $i++) {
564 if ($chosen[$i]) {
565 push @return, $stuff[$i];
566 }
567 }
568 return @return;
569}
570
7e018be2 571sub singleton_prompt_help_cmd {
5fa83264 572 print colored $help_color, __ <<'EOF' ;
7e018be2
WC
573Prompt help:
5741 - select a numbered item
575foo - select item based on unique prefix
576 - (empty) select nothing
577EOF
578}
579
580sub prompt_help_cmd {
5fa83264 581 print colored $help_color, __ <<'EOF' ;
7e018be2
WC
582Prompt help:
5831 - select a single item
5843-5 - select a range of items
5852-3,6-9 - select multiple ranges
586foo - select item based on unique prefix
587-... - unselect specified items
588* - choose all items
589 - (empty) finish selecting
590EOF
591}
592
5cde71d6
JH
593sub status_cmd {
594 list_and_choose({ LIST_ONLY => 1, HEADER => $status_head },
595 list_modified());
596 print "\n";
597}
598
599sub say_n_paths {
600 my $did = shift @_;
601 my $cnt = scalar @_;
c4a85c3b
VA
602 if ($did eq 'added') {
603 printf(__n("added %d path\n", "added %d paths\n",
604 $cnt), $cnt);
605 } elsif ($did eq 'updated') {
606 printf(__n("updated %d path\n", "updated %d paths\n",
607 $cnt), $cnt);
608 } elsif ($did eq 'reverted') {
609 printf(__n("reverted %d path\n", "reverted %d paths\n",
610 $cnt), $cnt);
611 } else {
612 printf(__n("touched %d path\n", "touched %d paths\n",
613 $cnt), $cnt);
5cde71d6
JH
614 }
615}
616
617sub update_cmd {
618 my @mods = list_modified('file-only');
619 return if (!@mods);
620
258e7790 621 my @update = list_and_choose({ PROMPT => __('Update'),
5cde71d6
JH
622 HEADER => $status_head, },
623 @mods);
624 if (@update) {
a4f7112f 625 system(qw(git update-index --add --remove --),
5cde71d6
JH
626 map { $_->{VALUE} } @update);
627 say_n_paths('updated', @update);
628 }
629 print "\n";
630}
631
632sub revert_cmd {
258e7790 633 my @update = list_and_choose({ PROMPT => __('Revert'),
5cde71d6
JH
634 HEADER => $status_head, },
635 list_modified());
636 if (@update) {
18bc7616
JK
637 if (is_initial_commit()) {
638 system(qw(git rm --cached),
639 map { $_->{VALUE} } @update);
5cde71d6 640 }
18bc7616
JK
641 else {
642 my @lines = run_cmd_pipe(qw(git ls-tree HEAD --),
643 map { $_->{VALUE} } @update);
644 my $fh;
645 open $fh, '| git update-index --index-info'
646 or die;
647 for (@lines) {
648 print $fh $_;
649 }
650 close($fh);
651 for (@update) {
652 if ($_->{INDEX_ADDDEL} &&
653 $_->{INDEX_ADDDEL} eq 'create') {
654 system(qw(git update-index --force-remove --),
655 $_->{VALUE});
13c58c17 656 printf(__("note: %s is untracked now.\n"), $_->{VALUE});
18bc7616 657 }
5cde71d6
JH
658 }
659 }
660 refresh();
661 say_n_paths('reverted', @update);
662 }
663 print "\n";
664}
665
666sub add_untracked_cmd {
258e7790 667 my @add = list_and_choose({ PROMPT => __('Add untracked') },
5cde71d6
JH
668 list_untracked());
669 if (@add) {
670 system(qw(git update-index --add --), @add);
671 say_n_paths('added', @add);
a9c4641d 672 } else {
258e7790 673 print __("No untracked files.\n");
5cde71d6
JH
674 }
675 print "\n";
676}
677
8f0bef6d
TR
678sub run_git_apply {
679 my $cmd = shift;
680 my $fh;
933e44d3 681 open $fh, '| git ' . $cmd . " --recount --allow-overlap";
8f0bef6d
TR
682 print $fh @_;
683 return close $fh;
684}
685
5cde71d6
JH
686sub parse_diff {
687 my ($path) = @_;
8f0bef6d 688 my @diff_cmd = split(" ", $patch_mode_flavour{DIFF});
2cc0f53b 689 if (defined $diff_algorithm) {
e5c29097 690 splice @diff_cmd, 1, 0, "--diff-algorithm=${diff_algorithm}";
2cc0f53b 691 }
433860f3
MH
692 if ($diff_indent_heuristic) {
693 splice @diff_cmd, 1, 0, "--indent-heuristic";
46e3d17f 694 }
d002ef4d 695 if (defined $patch_mode_revision) {
954312a3 696 push @diff_cmd, get_diff_reference($patch_mode_revision);
d002ef4d 697 }
8f0bef6d 698 my @diff = run_cmd_pipe("git", @diff_cmd, "--", $path);
4af756f3
WC
699 my @colored = ();
700 if ($diff_use_color) {
01143847
JK
701 my @display_cmd = ("git", @diff_cmd, qw(--color --), $path);
702 if (defined $diff_filter) {
703 # quotemeta is overkill, but sufficient for shell-quoting
704 my $diff = join(' ', map { quotemeta } @display_cmd);
705 @display_cmd = ("$diff | $diff_filter");
706 }
707
708 @colored = run_cmd_pipe(@display_cmd);
5cde71d6 709 }
0392513f 710 my (@hunk) = { TEXT => [], DISPLAY => [], TYPE => 'header' };
b4c61ed6 711
4af756f3
WC
712 for (my $i = 0; $i < @diff; $i++) {
713 if ($diff[$i] =~ /^@@ /) {
0392513f
JK
714 push @hunk, { TEXT => [], DISPLAY => [],
715 TYPE => 'hunk' };
b4c61ed6 716 }
4af756f3
WC
717 push @{$hunk[-1]{TEXT}}, $diff[$i];
718 push @{$hunk[-1]{DISPLAY}},
01143847 719 (@colored ? $colored[$i] : $diff[$i]);
b4c61ed6 720 }
4af756f3 721 return @hunk;
b4c61ed6
JH
722}
723
b717a627
JK
724sub parse_diff_header {
725 my $src = shift;
726
0392513f
JK
727 my $head = { TEXT => [], DISPLAY => [], TYPE => 'header' };
728 my $mode = { TEXT => [], DISPLAY => [], TYPE => 'mode' };
24ab81ae 729 my $deletion = { TEXT => [], DISPLAY => [], TYPE => 'deletion' };
b717a627
JK
730
731 for (my $i = 0; $i < @{$src->{TEXT}}; $i++) {
24ab81ae
JK
732 my $dest =
733 $src->{TEXT}->[$i] =~ /^(old|new) mode (\d+)$/ ? $mode :
734 $src->{TEXT}->[$i] =~ /^deleted file/ ? $deletion :
735 $head;
b717a627
JK
736 push @{$dest->{TEXT}}, $src->{TEXT}->[$i];
737 push @{$dest->{DISPLAY}}, $src->{DISPLAY}->[$i];
738 }
24ab81ae 739 return ($head, $mode, $deletion);
b717a627
JK
740}
741
835b2aeb
JH
742sub hunk_splittable {
743 my ($text) = @_;
744
745 my @s = split_hunk($text);
746 return (1 < @s);
747}
748
749sub parse_hunk_header {
750 my ($line) = @_;
751 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
7288ed8e
JLH
752 $line =~ /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
753 $o_cnt = 1 unless defined $o_cnt;
754 $n_cnt = 1 unless defined $n_cnt;
835b2aeb
JH
755 return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
756}
757
758sub split_hunk {
4af756f3 759 my ($text, $display) = @_;
835b2aeb 760 my @split = ();
4af756f3
WC
761 if (!defined $display) {
762 $display = $text;
763 }
835b2aeb
JH
764 # If there are context lines in the middle of a hunk,
765 # it can be split, but we would need to take care of
766 # overlaps later.
767
7b40a455 768 my ($o_ofs, undef, $n_ofs) = parse_hunk_header($text->[0]);
835b2aeb 769 my $hunk_start = 1;
835b2aeb
JH
770
771 OUTER:
772 while (1) {
773 my $next_hunk_start = undef;
774 my $i = $hunk_start - 1;
775 my $this = +{
776 TEXT => [],
4af756f3 777 DISPLAY => [],
0392513f 778 TYPE => 'hunk',
835b2aeb
JH
779 OLD => $o_ofs,
780 NEW => $n_ofs,
781 OCNT => 0,
782 NCNT => 0,
783 ADDDEL => 0,
784 POSTCTX => 0,
4af756f3 785 USE => undef,
835b2aeb
JH
786 };
787
788 while (++$i < @$text) {
789 my $line = $text->[$i];
4af756f3 790 my $display = $display->[$i];
835b2aeb
JH
791 if ($line =~ /^ /) {
792 if ($this->{ADDDEL} &&
793 !defined $next_hunk_start) {
794 # We have seen leading context and
795 # adds/dels and then here is another
796 # context, which is trailing for this
797 # split hunk and leading for the next
798 # one.
799 $next_hunk_start = $i;
800 }
801 push @{$this->{TEXT}}, $line;
4af756f3 802 push @{$this->{DISPLAY}}, $display;
835b2aeb
JH
803 $this->{OCNT}++;
804 $this->{NCNT}++;
805 if (defined $next_hunk_start) {
806 $this->{POSTCTX}++;
807 }
808 next;
809 }
810
811 # add/del
812 if (defined $next_hunk_start) {
813 # We are done with the current hunk and
814 # this is the first real change for the
815 # next split one.
816 $hunk_start = $next_hunk_start;
817 $o_ofs = $this->{OLD} + $this->{OCNT};
818 $n_ofs = $this->{NEW} + $this->{NCNT};
819 $o_ofs -= $this->{POSTCTX};
820 $n_ofs -= $this->{POSTCTX};
821 push @split, $this;
822 redo OUTER;
823 }
824 push @{$this->{TEXT}}, $line;
4af756f3 825 push @{$this->{DISPLAY}}, $display;
835b2aeb
JH
826 $this->{ADDDEL}++;
827 if ($line =~ /^-/) {
828 $this->{OCNT}++;
829 }
830 else {
831 $this->{NCNT}++;
832 }
833 }
834
835 push @split, $this;
836 last;
837 }
838
839 for my $hunk (@split) {
840 $o_ofs = $hunk->{OLD};
841 $n_ofs = $hunk->{NEW};
7b40a455
JLH
842 my $o_cnt = $hunk->{OCNT};
843 my $n_cnt = $hunk->{NCNT};
835b2aeb
JH
844
845 my $head = ("@@ -$o_ofs" .
846 (($o_cnt != 1) ? ",$o_cnt" : '') .
847 " +$n_ofs" .
848 (($n_cnt != 1) ? ",$n_cnt" : '') .
849 " @@\n");
4af756f3 850 my $display_head = $head;
835b2aeb 851 unshift @{$hunk->{TEXT}}, $head;
4af756f3
WC
852 if ($diff_use_color) {
853 $display_head = colored($fraginfo_color, $head);
854 }
855 unshift @{$hunk->{DISPLAY}}, $display_head;
835b2aeb 856 }
4af756f3 857 return @split;
835b2aeb
JH
858}
859
7a26e653
JH
860sub find_last_o_ctx {
861 my ($it) = @_;
862 my $text = $it->{TEXT};
863 my ($o_ofs, $o_cnt) = parse_hunk_header($text->[0]);
864 my $i = @{$text};
865 my $last_o_ctx = $o_ofs + $o_cnt;
866 while (0 < --$i) {
867 my $line = $text->[$i];
868 if ($line =~ /^ /) {
869 $last_o_ctx--;
870 next;
871 }
872 last;
873 }
874 return $last_o_ctx;
875}
876
877sub merge_hunk {
878 my ($prev, $this) = @_;
879 my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) =
880 parse_hunk_header($prev->{TEXT}[0]);
881 my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) =
882 parse_hunk_header($this->{TEXT}[0]);
883
884 my (@line, $i, $ofs, $o_cnt, $n_cnt);
885 $ofs = $o0_ofs;
886 $o_cnt = $n_cnt = 0;
887 for ($i = 1; $i < @{$prev->{TEXT}}; $i++) {
888 my $line = $prev->{TEXT}[$i];
889 if ($line =~ /^\+/) {
890 $n_cnt++;
891 push @line, $line;
892 next;
893 }
894
895 last if ($o1_ofs <= $ofs);
896
897 $o_cnt++;
898 $ofs++;
899 if ($line =~ /^ /) {
900 $n_cnt++;
901 }
902 push @line, $line;
903 }
904
905 for ($i = 1; $i < @{$this->{TEXT}}; $i++) {
906 my $line = $this->{TEXT}[$i];
907 if ($line =~ /^\+/) {
908 $n_cnt++;
909 push @line, $line;
910 next;
911 }
912 $ofs++;
913 $o_cnt++;
914 if ($line =~ /^ /) {
915 $n_cnt++;
916 }
917 push @line, $line;
918 }
919 my $head = ("@@ -$o0_ofs" .
920 (($o_cnt != 1) ? ",$o_cnt" : '') .
921 " +$n0_ofs" .
922 (($n_cnt != 1) ? ",$n_cnt" : '') .
923 " @@\n");
924 @{$prev->{TEXT}} = ($head, @line);
925}
926
927sub coalesce_overlapping_hunks {
928 my (@in) = @_;
929 my @out = ();
930
931 my ($last_o_ctx, $last_was_dirty);
932
933 for (grep { $_->{USE} } @in) {
3d792161
TR
934 if ($_->{TYPE} ne 'hunk') {
935 push @out, $_;
936 next;
937 }
7a26e653
JH
938 my $text = $_->{TEXT};
939 my ($o_ofs) = parse_hunk_header($text->[0]);
940 if (defined $last_o_ctx &&
941 $o_ofs <= $last_o_ctx &&
942 !$_->{DIRTY} &&
943 !$last_was_dirty) {
944 merge_hunk($out[-1], $_);
945 }
946 else {
947 push @out, $_;
948 }
949 $last_o_ctx = find_last_o_ctx($out[-1]);
950 $last_was_dirty = $_->{DIRTY};
951 }
952 return @out;
953}
835b2aeb 954
e1327ed5
JK
955sub reassemble_patch {
956 my $head = shift;
957 my @patch;
958
959 # Include everything in the header except the beginning of the diff.
960 push @patch, (grep { !/^[-+]{3}/ } @$head);
961
962 # Then include any headers from the hunk lines, which must
963 # come before any actual hunk.
964 while (@_ && $_[0] !~ /^@/) {
965 push @patch, shift;
966 }
967
968 # Then begin the diff.
969 push @patch, grep { /^[-+]{3}/ } @$head;
970
971 # And then the actual hunks.
972 push @patch, @_;
973
974 return @patch;
975}
976
ac083c47
TR
977sub color_diff {
978 return map {
979 colored((/^@/ ? $fraginfo_color :
980 /^\+/ ? $diff_new_color :
981 /^-/ ? $diff_old_color :
982 $diff_plain_color),
983 $_);
984 } @_;
985}
986
c9d96164
VA
987my %edit_hunk_manually_modes = (
988 stage => N__(
989"If the patch applies cleanly, the edited hunk will immediately be
990marked for staging."),
991 stash => N__(
992"If the patch applies cleanly, the edited hunk will immediately be
993marked for stashing."),
994 reset_head => N__(
995"If the patch applies cleanly, the edited hunk will immediately be
996marked for unstaging."),
997 reset_nothead => N__(
998"If the patch applies cleanly, the edited hunk will immediately be
999marked for applying."),
1000 checkout_index => N__(
1001"If the patch applies cleanly, the edited hunk will immediately be
0301f1fd 1002marked for discarding."),
c9d96164
VA
1003 checkout_head => N__(
1004"If the patch applies cleanly, the edited hunk will immediately be
1005marked for discarding."),
1006 checkout_nothead => N__(
1007"If the patch applies cleanly, the edited hunk will immediately be
1008marked for applying."),
1009);
1010
ac083c47
TR
1011sub edit_hunk_manually {
1012 my ($oldtext) = @_;
1013
1014 my $hunkfile = $repo->repo_path . "/addp-hunk-edit.diff";
1015 my $fh;
1016 open $fh, '>', $hunkfile
13c58c17 1017 or die sprintf(__("failed to open hunk edit file for writing: %s"), $!);
c9d96164 1018 print $fh Git::comment_lines __("Manual hunk edit mode -- see bottom for a quick guide.\n");
ac083c47 1019 print $fh @$oldtext;
7b8c7051
JDL
1020 my $is_reverse = $patch_mode_flavour{IS_REVERSE};
1021 my ($remove_plus, $remove_minus) = $is_reverse ? ('-', '+') : ('+', '-');
c9d96164
VA
1022 my $comment_line_char = Git::get_comment_line_char;
1023 print $fh Git::comment_lines sprintf(__ <<EOF, $remove_minus, $remove_plus, $comment_line_char),
1024---
1025To remove '%s' lines, make them ' ' lines (context).
1026To remove '%s' lines, delete them.
1027Lines starting with %s will be removed.
ac083c47 1028EOF
c9d96164
VA
1029__($edit_hunk_manually_modes{$patch_mode}),
1030# TRANSLATORS: 'it' refers to the patch mentioned in the previous messages.
1031__ <<EOF2 ;
1032If it does not apply cleanly, you will be given an opportunity to
1033edit again. If all lines of the hunk are removed, then the edit is
1034aborted and the hunk is left unchanged.
1035EOF2
ac083c47
TR
1036 close $fh;
1037
b4479f07 1038 chomp(my $editor = run_cmd_pipe(qw(git var GIT_EDITOR)));
ac083c47
TR
1039 system('sh', '-c', $editor.' "$@"', $editor, $hunkfile);
1040
1d398a03
DM
1041 if ($? != 0) {
1042 return undef;
1043 }
1044
ac083c47 1045 open $fh, '<', $hunkfile
13c58c17 1046 or die sprintf(__("failed to open hunk edit file for reading: %s"), $!);
d85d7ecb 1047 my @newtext = grep { !/^\Q$comment_line_char\E/ } <$fh>;
ac083c47
TR
1048 close $fh;
1049 unlink $hunkfile;
1050
1051 # Abort if nothing remains
1052 if (!grep { /\S/ } @newtext) {
1053 return undef;
1054 }
1055
1056 # Reinsert the first hunk header if the user accidentally deleted it
1057 if ($newtext[0] !~ /^@/) {
1058 unshift @newtext, $oldtext->[0];
1059 }
1060 return \@newtext;
1061}
1062
1063sub diff_applies {
9dce8323 1064 return run_git_apply($patch_mode_flavour{APPLY_CHECK} . ' --check',
8f0bef6d 1065 map { @{$_->{TEXT}} } @_);
ac083c47
TR
1066}
1067
ca6ac7f1
TR
1068sub _restore_terminal_and_die {
1069 ReadMode 'restore';
1070 print "\n";
1071 exit 1;
1072}
1073
1074sub prompt_single_character {
1075 if ($use_readkey) {
1076 local $SIG{TERM} = \&_restore_terminal_and_die;
1077 local $SIG{INT} = \&_restore_terminal_and_die;
1078 ReadMode 'cbreak';
1079 my $key = ReadKey 0;
1080 ReadMode 'restore';
b5cc0032
TR
1081 if ($use_termcap and $key eq "\e") {
1082 while (!defined $term_escapes{$key}) {
1083 my $next = ReadKey 0.5;
1084 last if (!defined $next);
1085 $key .= $next;
1086 }
1087 $key =~ s/\e/^[/;
1088 }
ca6ac7f1
TR
1089 print "$key" if defined $key;
1090 print "\n";
1091 return $key;
1092 } else {
1093 return <STDIN>;
1094 }
1095}
1096
ac083c47
TR
1097sub prompt_yesno {
1098 my ($prompt) = @_;
1099 while (1) {
1100 print colored $prompt_color, $prompt;
ca6ac7f1 1101 my $line = prompt_single_character;
d5addcf5 1102 return undef unless defined $line;
ac083c47
TR
1103 return 0 if $line =~ /^n/i;
1104 return 1 if $line =~ /^y/i;
1105 }
1106}
1107
1108sub edit_hunk_loop {
1109 my ($head, $hunk, $ix) = @_;
1110 my $text = $hunk->[$ix]->{TEXT};
1111
1112 while (1) {
1113 $text = edit_hunk_manually($text);
1114 if (!defined $text) {
1115 return undef;
1116 }
0392513f
JK
1117 my $newhunk = {
1118 TEXT => $text,
1119 TYPE => $hunk->[$ix]->{TYPE},
7a26e653
JH
1120 USE => 1,
1121 DIRTY => 1,
0392513f 1122 };
ac083c47
TR
1123 if (diff_applies($head,
1124 @{$hunk}[0..$ix-1],
1125 $newhunk,
1126 @{$hunk}[$ix+1..$#{$hunk}])) {
1127 $newhunk->{DISPLAY} = [color_diff(@{$text})];
1128 return $newhunk;
1129 }
1130 else {
1131 prompt_yesno(
258e7790
VA
1132 # TRANSLATORS: do not translate [y/n]
1133 # The program will only accept that input
1134 # at this point.
1135 # Consider translating (saying "no" discards!) as
1136 # (saying "n" for "no" discards!) if the translation
1137 # of the word "no" does not start with n.
1138 __('Your edited hunk does not apply. Edit again '
1139 . '(saying "no" discards!) [y/n]? ')
ac083c47
TR
1140 ) or return undef;
1141 }
1142 }
1143}
1144
186b52c5
VA
1145my %help_patch_modes = (
1146 stage => N__(
1147"y - stage this hunk
1148n - do not stage this hunk
1149q - quit; do not stage this hunk or any of the remaining ones
1150a - stage this hunk and all later hunks in the file
1151d - do not stage this hunk or any of the later hunks in the file"),
1152 stash => N__(
1153"y - stash this hunk
1154n - do not stash this hunk
1155q - quit; do not stash this hunk or any of the remaining ones
1156a - stash this hunk and all later hunks in the file
1157d - do not stash this hunk or any of the later hunks in the file"),
1158 reset_head => N__(
1159"y - unstage this hunk
1160n - do not unstage this hunk
1161q - quit; do not unstage this hunk or any of the remaining ones
1162a - unstage this hunk and all later hunks in the file
1163d - do not unstage this hunk or any of the later hunks in the file"),
1164 reset_nothead => N__(
1165"y - apply this hunk to index
1166n - do not apply this hunk to index
1167q - quit; do not apply this hunk or any of the remaining ones
1168a - apply this hunk and all later hunks in the file
1169d - do not apply this hunk or any of the later hunks in the file"),
1170 checkout_index => N__(
1171"y - discard this hunk from worktree
1172n - do not discard this hunk from worktree
1173q - quit; do not discard this hunk or any of the remaining ones
1174a - discard this hunk and all later hunks in the file
1175d - do not discard this hunk or any of the later hunks in the file"),
1176 checkout_head => N__(
1177"y - discard this hunk from index and worktree
1178n - do not discard this hunk from index and worktree
1179q - quit; do not discard this hunk or any of the remaining ones
1180a - discard this hunk and all later hunks in the file
1181d - do not discard this hunk or any of the later hunks in the file"),
1182 checkout_nothead => N__(
1183"y - apply this hunk to index and worktree
1184n - do not apply this hunk to index and worktree
1185q - quit; do not apply this hunk or any of the remaining ones
1186a - apply this hunk and all later hunks in the file
1187d - do not apply this hunk or any of the later hunks in the file"),
1188);
1189
5cde71d6 1190sub help_patch_cmd {
186b52c5 1191 print colored $help_color, __($help_patch_modes{$patch_mode}), "\n", __ <<EOF ;
070434d0 1192g - select a hunk to go to
dd971cc9 1193/ - search for a hunk matching the given regex
5cde71d6
JH
1194j - leave this hunk undecided, see next undecided hunk
1195J - leave this hunk undecided, see next hunk
1196k - leave this hunk undecided, see previous undecided hunk
1197K - leave this hunk undecided, see previous hunk
835b2aeb 1198s - split the current hunk into smaller hunks
ac083c47 1199e - manually edit the current hunk
280e50c7 1200? - print help
5cde71d6
JH
1201EOF
1202}
1203
8f0bef6d
TR
1204sub apply_patch {
1205 my $cmd = shift;
9dce8323 1206 my $ret = run_git_apply $cmd, @_;
8f0bef6d
TR
1207 if (!$ret) {
1208 print STDERR @_;
1209 }
1210 return $ret;
1211}
1212
4f353658
TR
1213sub apply_patch_for_checkout_commit {
1214 my $reverse = shift;
9dce8323
JH
1215 my $applies_index = run_git_apply 'apply '.$reverse.' --cached --check', @_;
1216 my $applies_worktree = run_git_apply 'apply '.$reverse.' --check', @_;
4f353658
TR
1217
1218 if ($applies_worktree && $applies_index) {
9dce8323
JH
1219 run_git_apply 'apply '.$reverse.' --cached', @_;
1220 run_git_apply 'apply '.$reverse, @_;
4f353658
TR
1221 return 1;
1222 } elsif (!$applies_index) {
258e7790
VA
1223 print colored $error_color, __("The selected hunks do not apply to the index!\n");
1224 if (prompt_yesno __("Apply them to the worktree anyway? ")) {
9dce8323 1225 return run_git_apply 'apply '.$reverse, @_;
4f353658 1226 } else {
258e7790 1227 print colored $error_color, __("Nothing was applied.\n");
4f353658
TR
1228 return 0;
1229 }
1230 } else {
1231 print STDERR @_;
1232 return 0;
1233 }
1234}
1235
5cde71d6 1236sub patch_update_cmd {
8f0bef6d 1237 my @all_mods = list_modified($patch_mode_flavour{FILTER});
13c58c17 1238 error_msg sprintf(__("ignoring unmerged: %s\n"), $_->{VALUE})
4066bd67
JK
1239 for grep { $_->{UNMERGED} } @all_mods;
1240 @all_mods = grep { !$_->{UNMERGED} } @all_mods;
1241
9fe7a643 1242 my @mods = grep { !($_->{BINARY}) } @all_mods;
b63e9950 1243 my @them;
5cde71d6 1244
b63e9950 1245 if (!@mods) {
9fe7a643 1246 if (@all_mods) {
258e7790 1247 print STDERR __("Only binary files changed.\n");
9fe7a643 1248 } else {
258e7790 1249 print STDERR __("No changes.\n");
9fe7a643 1250 }
b63e9950
WC
1251 return 0;
1252 }
c852bd54 1253 if ($patch_mode_only) {
b63e9950
WC
1254 @them = @mods;
1255 }
1256 else {
258e7790 1257 @them = list_and_choose({ PROMPT => __('Patch update'),
b63e9950
WC
1258 HEADER => $status_head, },
1259 @mods);
1260 }
12db334e 1261 for (@them) {
9a7a1e03 1262 return 0 if patch_update_file($_->{VALUE});
12db334e 1263 }
a7d9da6c 1264}
5cde71d6 1265
3f6aff68
WP
1266# Generate a one line summary of a hunk.
1267sub summarize_hunk {
1268 my $rhunk = shift;
1269 my $summary = $rhunk->{TEXT}[0];
1270
1271 # Keep the line numbers, discard extra context.
1272 $summary =~ s/@@(.*?)@@.*/$1 /s;
1273 $summary .= " " x (20 - length $summary);
1274
1275 # Add some user context.
1276 for my $line (@{$rhunk->{TEXT}}) {
1277 if ($line =~ m/^[+-].*\w/) {
1278 $summary .= $line;
1279 last;
1280 }
1281 }
1282
1283 chomp $summary;
1284 return substr($summary, 0, 80) . "\n";
1285}
1286
1287
1288# Print a one-line summary of each hunk in the array ref in
41ccfdd9 1289# the first argument, starting with the index in the 2nd.
3f6aff68
WP
1290sub display_hunks {
1291 my ($hunks, $i) = @_;
1292 my $ctr = 0;
1293 $i ||= 0;
1294 for (; $i < @$hunks && $ctr < 20; $i++, $ctr++) {
1295 my $status = " ";
1296 if (defined $hunks->[$i]{USE}) {
1297 $status = $hunks->[$i]{USE} ? "+" : "-";
1298 }
1299 printf "%s%2d: %s",
1300 $status,
1301 $i + 1,
1302 summarize_hunk($hunks->[$i]);
1303 }
1304 return $i;
1305}
1306
0539d5e6
VA
1307my %patch_update_prompt_modes = (
1308 stage => {
1309 mode => N__("Stage mode change [y,n,q,a,d,/%s,?]? "),
1310 deletion => N__("Stage deletion [y,n,q,a,d,/%s,?]? "),
1311 hunk => N__("Stage this hunk [y,n,q,a,d,/%s,?]? "),
1312 },
1313 stash => {
1314 mode => N__("Stash mode change [y,n,q,a,d,/%s,?]? "),
1315 deletion => N__("Stash deletion [y,n,q,a,d,/%s,?]? "),
1316 hunk => N__("Stash this hunk [y,n,q,a,d,/%s,?]? "),
1317 },
1318 reset_head => {
1319 mode => N__("Unstage mode change [y,n,q,a,d,/%s,?]? "),
1320 deletion => N__("Unstage deletion [y,n,q,a,d,/%s,?]? "),
1321 hunk => N__("Unstage this hunk [y,n,q,a,d,/%s,?]? "),
1322 },
1323 reset_nothead => {
1324 mode => N__("Apply mode change to index [y,n,q,a,d,/%s,?]? "),
1325 deletion => N__("Apply deletion to index [y,n,q,a,d,/%s,?]? "),
1326 hunk => N__("Apply this hunk to index [y,n,q,a,d,/%s,?]? "),
1327 },
1328 checkout_index => {
1329 mode => N__("Discard mode change from worktree [y,n,q,a,d,/%s,?]? "),
1330 deletion => N__("Discard deletion from worktree [y,n,q,a,d,/%s,?]? "),
1331 hunk => N__("Discard this hunk from worktree [y,n,q,a,d,/%s,?]? "),
1332 },
1333 checkout_head => {
1334 mode => N__("Discard mode change from index and worktree [y,n,q,a,d,/%s,?]? "),
1335 deletion => N__("Discard deletion from index and worktree [y,n,q,a,d,/%s,?]? "),
1336 hunk => N__("Discard this hunk from index and worktree [y,n,q,a,d,/%s,?]? "),
1337 },
1338 checkout_nothead => {
1339 mode => N__("Apply mode change to index and worktree [y,n,q,a,d,/%s,?]? "),
1340 deletion => N__("Apply deletion to index and worktree [y,n,q,a,d,/%s,?]? "),
1341 hunk => N__("Apply this hunk to index and worktree [y,n,q,a,d,/%s,?]? "),
1342 },
1343);
1344
a7d9da6c 1345sub patch_update_file {
9a7a1e03 1346 my $quit = 0;
5cde71d6 1347 my ($ix, $num);
a7d9da6c 1348 my $path = shift;
5cde71d6 1349 my ($head, @hunk) = parse_diff($path);
24ab81ae 1350 ($head, my $mode, my $deletion) = parse_diff_header($head);
4af756f3
WC
1351 for (@{$head->{DISPLAY}}) {
1352 print;
1353 }
ca724686
JK
1354
1355 if (@{$mode->{TEXT}}) {
0392513f 1356 unshift @hunk, $mode;
ca724686 1357 }
8947fdd5
JK
1358 if (@{$deletion->{TEXT}}) {
1359 foreach my $hunk (@hunk) {
1360 push @{$deletion->{TEXT}}, @{$hunk->{TEXT}};
1361 push @{$deletion->{DISPLAY}}, @{$hunk->{DISPLAY}};
1362 }
24ab81ae
JK
1363 @hunk = ($deletion);
1364 }
ca724686 1365
5cde71d6
JH
1366 $num = scalar @hunk;
1367 $ix = 0;
1368
1369 while (1) {
835b2aeb 1370 my ($prev, $next, $other, $undecided, $i);
5cde71d6
JH
1371 $other = '';
1372
1373 if ($num <= $ix) {
1374 $ix = 0;
1375 }
835b2aeb 1376 for ($i = 0; $i < $ix; $i++) {
5cde71d6
JH
1377 if (!defined $hunk[$i]{USE}) {
1378 $prev = 1;
57886bc7 1379 $other .= ',k';
5cde71d6
JH
1380 last;
1381 }
1382 }
1383 if ($ix) {
57886bc7 1384 $other .= ',K';
5cde71d6 1385 }
835b2aeb 1386 for ($i = $ix + 1; $i < $num; $i++) {
5cde71d6
JH
1387 if (!defined $hunk[$i]{USE}) {
1388 $next = 1;
57886bc7 1389 $other .= ',j';
5cde71d6
JH
1390 last;
1391 }
1392 }
1393 if ($ix < $num - 1) {
57886bc7 1394 $other .= ',J';
5cde71d6 1395 }
070434d0 1396 if ($num > 1) {
4404b2e3 1397 $other .= ',g';
070434d0 1398 }
835b2aeb 1399 for ($i = 0; $i < $num; $i++) {
5cde71d6
JH
1400 if (!defined $hunk[$i]{USE}) {
1401 $undecided = 1;
1402 last;
1403 }
1404 }
1405 last if (!$undecided);
1406
0392513f
JK
1407 if ($hunk[$ix]{TYPE} eq 'hunk' &&
1408 hunk_splittable($hunk[$ix]{TEXT})) {
57886bc7 1409 $other .= ',s';
835b2aeb 1410 }
0392513f
JK
1411 if ($hunk[$ix]{TYPE} eq 'hunk') {
1412 $other .= ',e';
1413 }
4af756f3
WC
1414 for (@{$hunk[$ix]{DISPLAY}}) {
1415 print;
1416 }
0539d5e6
VA
1417 print colored $prompt_color,
1418 sprintf(__($patch_update_prompt_modes{$patch_mode}{$hunk[$ix]{TYPE}}), $other);
1419
ca6ac7f1 1420 my $line = prompt_single_character;
a8bec7ab 1421 last unless defined $line;
5cde71d6
JH
1422 if ($line) {
1423 if ($line =~ /^y/i) {
1424 $hunk[$ix]{USE} = 1;
1425 }
1426 elsif ($line =~ /^n/i) {
1427 $hunk[$ix]{USE} = 0;
1428 }
1429 elsif ($line =~ /^a/i) {
1430 while ($ix < $num) {
1431 if (!defined $hunk[$ix]{USE}) {
1432 $hunk[$ix]{USE} = 1;
1433 }
1434 $ix++;
1435 }
1436 next;
1437 }
070434d0
WP
1438 elsif ($other =~ /g/ && $line =~ /^g(.*)/) {
1439 my $response = $1;
1440 my $no = $ix > 10 ? $ix - 10 : 0;
1441 while ($response eq '') {
070434d0
WP
1442 $no = display_hunks(\@hunk, $no);
1443 if ($no < $num) {
258e7790
VA
1444 print __("go to which hunk (<ret> to see more)? ");
1445 } else {
1446 print __("go to which hunk? ");
070434d0 1447 }
070434d0 1448 $response = <STDIN>;
68c02d7c
TR
1449 if (!defined $response) {
1450 $response = '';
1451 }
070434d0
WP
1452 chomp $response;
1453 }
1454 if ($response !~ /^\s*\d+\s*$/) {
13c58c17
VA
1455 error_msg sprintf(__("Invalid number: '%s'\n"),
1456 $response);
070434d0
WP
1457 } elsif (0 < $response && $response <= $num) {
1458 $ix = $response - 1;
1459 } else {
c4a85c3b
VA
1460 error_msg sprintf(__n("Sorry, only %d hunk available.\n",
1461 "Sorry, only %d hunks available.\n", $num), $num);
070434d0
WP
1462 }
1463 next;
1464 }
5cde71d6
JH
1465 elsif ($line =~ /^d/i) {
1466 while ($ix < $num) {
1467 if (!defined $hunk[$ix]{USE}) {
1468 $hunk[$ix]{USE} = 0;
1469 }
1470 $ix++;
1471 }
1472 next;
1473 }
9a7a1e03 1474 elsif ($line =~ /^q/i) {
f5ea3f2b
JH
1475 for ($i = 0; $i < $num; $i++) {
1476 if (!defined $hunk[$i]{USE}) {
1477 $hunk[$i]{USE} = 0;
9a7a1e03 1478 }
9a7a1e03
MM
1479 }
1480 $quit = 1;
f5ea3f2b 1481 last;
9a7a1e03 1482 }
dd971cc9 1483 elsif ($line =~ m|^/(.*)|) {
ca6ac7f1
TR
1484 my $regex = $1;
1485 if ($1 eq "") {
258e7790 1486 print colored $prompt_color, __("search for regex? ");
ca6ac7f1
TR
1487 $regex = <STDIN>;
1488 if (defined $regex) {
1489 chomp $regex;
1490 }
1491 }
dd971cc9
WP
1492 my $search_string;
1493 eval {
ca6ac7f1 1494 $search_string = qr{$regex}m;
dd971cc9
WP
1495 };
1496 if ($@) {
1497 my ($err,$exp) = ($@, $1);
1498 $err =~ s/ at .*git-add--interactive line \d+, <STDIN> line \d+.*$//;
13c58c17 1499 error_msg sprintf(__("Malformed search regexp %s: %s\n"), $exp, $err);
dd971cc9
WP
1500 next;
1501 }
1502 my $iy = $ix;
1503 while (1) {
1504 my $text = join ("", @{$hunk[$iy]{TEXT}});
1505 last if ($text =~ $search_string);
1506 $iy++;
1507 $iy = 0 if ($iy >= $num);
1508 if ($ix == $iy) {
258e7790 1509 error_msg __("No hunk matches the given pattern\n");
dd971cc9
WP
1510 last;
1511 }
1512 }
1513 $ix = $iy;
1514 next;
1515 }
ace30ba8
WP
1516 elsif ($line =~ /^K/) {
1517 if ($other =~ /K/) {
1518 $ix--;
1519 }
1520 else {
258e7790 1521 error_msg __("No previous hunk\n");
ace30ba8 1522 }
5cde71d6
JH
1523 next;
1524 }
ace30ba8
WP
1525 elsif ($line =~ /^J/) {
1526 if ($other =~ /J/) {
1527 $ix++;
1528 }
1529 else {
258e7790 1530 error_msg __("No next hunk\n");
ace30ba8 1531 }
5cde71d6
JH
1532 next;
1533 }
ace30ba8
WP
1534 elsif ($line =~ /^k/) {
1535 if ($other =~ /k/) {
1536 while (1) {
1537 $ix--;
1538 last if (!$ix ||
1539 !defined $hunk[$ix]{USE});
1540 }
1541 }
1542 else {
258e7790 1543 error_msg __("No previous hunk\n");
5cde71d6
JH
1544 }
1545 next;
1546 }
ace30ba8
WP
1547 elsif ($line =~ /^j/) {
1548 if ($other !~ /j/) {
258e7790 1549 error_msg __("No next hunk\n");
ace30ba8 1550 next;
5cde71d6 1551 }
5cde71d6 1552 }
835b2aeb 1553 elsif ($other =~ /s/ && $line =~ /^s/) {
4af756f3 1554 my @split = split_hunk($hunk[$ix]{TEXT}, $hunk[$ix]{DISPLAY});
835b2aeb 1555 if (1 < @split) {
c4a85c3b
VA
1556 print colored $header_color, sprintf(
1557 __n("Split into %d hunk.\n",
1558 "Split into %d hunks.\n",
1559 scalar(@split)), scalar(@split));
835b2aeb 1560 }
4af756f3 1561 splice (@hunk, $ix, 1, @split);
835b2aeb
JH
1562 $num = scalar @hunk;
1563 next;
1564 }
0392513f 1565 elsif ($other =~ /e/ && $line =~ /^e/) {
ac083c47
TR
1566 my $newhunk = edit_hunk_loop($head, \@hunk, $ix);
1567 if (defined $newhunk) {
1568 splice @hunk, $ix, 1, $newhunk;
1569 }
1570 }
5cde71d6
JH
1571 else {
1572 help_patch_cmd($other);
1573 next;
1574 }
1575 # soft increment
1576 while (1) {
1577 $ix++;
1578 last if ($ix >= $num ||
1579 !defined $hunk[$ix]{USE});
1580 }
1581 }
1582 }
1583
7a26e653
JH
1584 @hunk = coalesce_overlapping_hunks(@hunk);
1585
7b40a455 1586 my $n_lofs = 0;
5cde71d6
JH
1587 my @result = ();
1588 for (@hunk) {
8cbd4310
TR
1589 if ($_->{USE}) {
1590 push @result, @{$_->{TEXT}};
5cde71d6
JH
1591 }
1592 }
1593
1594 if (@result) {
e1327ed5 1595 my @patch = reassemble_patch($head->{TEXT}, @result);
8f0bef6d
TR
1596 my $apply_routine = $patch_mode_flavour{APPLY};
1597 &$apply_routine(@patch);
5cde71d6
JH
1598 refresh();
1599 }
1600
1601 print "\n";
9a7a1e03 1602 return $quit;
5cde71d6
JH
1603}
1604
1605sub diff_cmd {
1606 my @mods = list_modified('index-only');
1607 @mods = grep { !($_->{BINARY}) } @mods;
1608 return if (!@mods);
258e7790 1609 my (@them) = list_and_choose({ PROMPT => __('Review diff'),
5cde71d6
JH
1610 IMMEDIATE => 1,
1611 HEADER => $status_head, },
1612 @mods);
1613 return if (!@them);
258e7790 1614 my $reference = (is_initial_commit()) ? get_empty_tree() : 'HEAD';
18bc7616
JK
1615 system(qw(git diff -p --cached), $reference, '--',
1616 map { $_->{VALUE} } @them);
5cde71d6
JH
1617}
1618
1619sub quit_cmd {
258e7790 1620 print __("Bye.\n");
5cde71d6
JH
1621 exit(0);
1622}
1623
1624sub help_cmd {
5fa83264
VA
1625# TRANSLATORS: please do not translate the command names
1626# 'status', 'update', 'revert', etc.
1627 print colored $help_color, __ <<'EOF' ;
5cde71d6
JH
1628status - show paths with changes
1629update - add working tree state to the staged set of changes
1630revert - revert staged set of changes back to the HEAD version
1631patch - pick hunks and update selectively
e519eccd 1632diff - view diff between HEAD and index
5cde71d6
JH
1633add untracked - add contents of untracked files to the staged set of changes
1634EOF
1635}
1636
b63e9950
WC
1637sub process_args {
1638 return unless @ARGV;
1639 my $arg = shift @ARGV;
d002ef4d
TR
1640 if ($arg =~ /--patch(?:=(.*))?/) {
1641 if (defined $1) {
1642 if ($1 eq 'reset') {
1643 $patch_mode = 'reset_head';
1644 $patch_mode_revision = 'HEAD';
258e7790 1645 $arg = shift @ARGV or die __("missing --");
d002ef4d
TR
1646 if ($arg ne '--') {
1647 $patch_mode_revision = $arg;
1648 $patch_mode = ($arg eq 'HEAD' ?
1649 'reset_head' : 'reset_nothead');
258e7790 1650 $arg = shift @ARGV or die __("missing --");
d002ef4d 1651 }
4f353658 1652 } elsif ($1 eq 'checkout') {
258e7790 1653 $arg = shift @ARGV or die __("missing --");
4f353658
TR
1654 if ($arg eq '--') {
1655 $patch_mode = 'checkout_index';
1656 } else {
1657 $patch_mode_revision = $arg;
1658 $patch_mode = ($arg eq 'HEAD' ?
1659 'checkout_head' : 'checkout_nothead');
258e7790 1660 $arg = shift @ARGV or die __("missing --");
4f353658 1661 }
dda1f2a5
TR
1662 } elsif ($1 eq 'stage' or $1 eq 'stash') {
1663 $patch_mode = $1;
258e7790 1664 $arg = shift @ARGV or die __("missing --");
d002ef4d 1665 } else {
13c58c17 1666 die sprintf(__("unknown --patch mode: %s"), $1);
d002ef4d
TR
1667 }
1668 } else {
1669 $patch_mode = 'stage';
258e7790 1670 $arg = shift @ARGV or die __("missing --");
d002ef4d 1671 }
13c58c17
VA
1672 die sprintf(__("invalid argument %s, expecting --"),
1673 $arg) unless $arg eq "--";
d002ef4d 1674 %patch_mode_flavour = %{$patch_modes{$patch_mode}};
c852bd54 1675 $patch_mode_only = 1;
b63e9950
WC
1676 }
1677 elsif ($arg ne "--") {
13c58c17 1678 die sprintf(__("invalid argument %s, expecting --"), $arg);
b63e9950
WC
1679 }
1680}
1681
5cde71d6
JH
1682sub main_loop {
1683 my @cmd = ([ 'status', \&status_cmd, ],
1684 [ 'update', \&update_cmd, ],
1685 [ 'revert', \&revert_cmd, ],
1686 [ 'add untracked', \&add_untracked_cmd, ],
1687 [ 'patch', \&patch_update_cmd, ],
1688 [ 'diff', \&diff_cmd, ],
1689 [ 'quit', \&quit_cmd, ],
1690 [ 'help', \&help_cmd, ],
1691 );
1692 while (1) {
258e7790 1693 my ($it) = list_and_choose({ PROMPT => __('What now'),
5cde71d6
JH
1694 SINGLETON => 1,
1695 LIST_FLAT => 4,
258e7790 1696 HEADER => __('*** Commands ***'),
c95c0248 1697 ON_EOF => \&quit_cmd,
5cde71d6
JH
1698 IMMEDIATE => 1 }, @cmd);
1699 if ($it) {
1700 eval {
1701 $it->[1]->();
1702 };
1703 if ($@) {
1704 print "$@";
1705 }
1706 }
1707 }
1708}
1709
b63e9950 1710process_args();
5cde71d6 1711refresh();
c852bd54 1712if ($patch_mode_only) {
b63e9950
WC
1713 patch_update_cmd();
1714}
1715else {
1716 status_cmd();
1717 main_loop();
1718}