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