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