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