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