]> git.ipfire.org Git - thirdparty/git.git/blob - git-add--interactive.perl
Merge branch 'ab/pager-exit-log'
[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_context_color) =
34 $diff_use_color ? (
35 $repo->get_color($repo->config('color.diff.context') ? 'color.diff.context' : '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 my @out = <$fh>;
181 close $fh || die "Cannot close @_ ($!)";
182 return @out;
183 }
184 }
185
186 my ($GIT_DIR) = run_cmd_pipe(qw(git rev-parse --git-dir));
187
188 if (!defined $GIT_DIR) {
189 exit(1); # rev-parse would have already said "not a git repo"
190 }
191 chomp($GIT_DIR);
192
193 sub refresh {
194 my $fh;
195 open $fh, 'git update-index --refresh |'
196 or die;
197 while (<$fh>) {
198 ;# ignore 'needs update'
199 }
200 close $fh;
201 }
202
203 sub list_untracked {
204 map {
205 chomp $_;
206 unquote_path($_);
207 }
208 run_cmd_pipe(qw(git ls-files --others --exclude-standard --), @ARGV);
209 }
210
211 # TRANSLATORS: you can adjust this to align "git add -i" status menu
212 my $status_fmt = __('%12s %12s %s');
213 my $status_head = sprintf($status_fmt, __('staged'), __('unstaged'), __('path'));
214
215 {
216 my $initial;
217 sub is_initial_commit {
218 $initial = system('git rev-parse HEAD -- >/dev/null 2>&1') != 0
219 unless defined $initial;
220 return $initial;
221 }
222 }
223
224 {
225 my $empty_tree;
226 sub get_empty_tree {
227 return $empty_tree if defined $empty_tree;
228
229 ($empty_tree) = run_cmd_pipe(qw(git hash-object -t tree /dev/null));
230 chomp $empty_tree;
231 return $empty_tree;
232 }
233 }
234
235 sub get_diff_reference {
236 my $ref = shift;
237 if (defined $ref and $ref ne 'HEAD') {
238 return $ref;
239 } elsif (is_initial_commit()) {
240 return get_empty_tree();
241 } else {
242 return 'HEAD';
243 }
244 }
245
246 # Returns list of hashes, contents of each of which are:
247 # VALUE: pathname
248 # BINARY: is a binary path
249 # INDEX: is index different from HEAD?
250 # FILE: is file different from index?
251 # INDEX_ADDDEL: is it add/delete between HEAD and index?
252 # FILE_ADDDEL: is it add/delete between index and file?
253 # UNMERGED: is the path unmerged
254
255 sub list_modified {
256 my ($only) = @_;
257 my (%data, @return);
258 my ($add, $del, $adddel, $file);
259
260 my $reference = get_diff_reference($patch_mode_revision);
261 for (run_cmd_pipe(qw(git diff-index --cached
262 --numstat --summary), $reference,
263 '--', @ARGV)) {
264 if (($add, $del, $file) =
265 /^([-\d]+) ([-\d]+) (.*)/) {
266 my ($change, $bin);
267 $file = unquote_path($file);
268 if ($add eq '-' && $del eq '-') {
269 $change = __('binary');
270 $bin = 1;
271 }
272 else {
273 $change = "+$add/-$del";
274 }
275 $data{$file} = {
276 INDEX => $change,
277 BINARY => $bin,
278 FILE => __('nothing'),
279 }
280 }
281 elsif (($adddel, $file) =
282 /^ (create|delete) mode [0-7]+ (.*)$/) {
283 $file = unquote_path($file);
284 $data{$file}{INDEX_ADDDEL} = $adddel;
285 }
286 }
287
288 for (run_cmd_pipe(qw(git diff-files --ignore-submodules=dirty --numstat --summary --raw --), @ARGV)) {
289 if (($add, $del, $file) =
290 /^([-\d]+) ([-\d]+) (.*)/) {
291 $file = unquote_path($file);
292 my ($change, $bin);
293 if ($add eq '-' && $del eq '-') {
294 $change = __('binary');
295 $bin = 1;
296 }
297 else {
298 $change = "+$add/-$del";
299 }
300 $data{$file}{FILE} = $change;
301 if ($bin) {
302 $data{$file}{BINARY} = 1;
303 }
304 }
305 elsif (($adddel, $file) =
306 /^ (create|delete) mode [0-7]+ (.*)$/) {
307 $file = unquote_path($file);
308 $data{$file}{FILE_ADDDEL} = $adddel;
309 }
310 elsif (/^:[0-7]+ [0-7]+ [0-9a-f]+ [0-9a-f]+ (.) (.*)$/) {
311 $file = unquote_path($2);
312 if (!exists $data{$file}) {
313 $data{$file} = +{
314 INDEX => __('unchanged'),
315 BINARY => 0,
316 };
317 }
318 if ($1 eq 'U') {
319 $data{$file}{UNMERGED} = 1;
320 }
321 }
322 }
323
324 for (sort keys %data) {
325 my $it = $data{$_};
326
327 if ($only) {
328 if ($only eq 'index-only') {
329 next if ($it->{INDEX} eq __('unchanged'));
330 }
331 if ($only eq 'file-only') {
332 next if ($it->{FILE} eq __('nothing'));
333 }
334 }
335 push @return, +{
336 VALUE => $_,
337 %$it,
338 };
339 }
340 return @return;
341 }
342
343 sub find_unique {
344 my ($string, @stuff) = @_;
345 my $found = undef;
346 for (my $i = 0; $i < @stuff; $i++) {
347 my $it = $stuff[$i];
348 my $hit = undef;
349 if (ref $it) {
350 if ((ref $it) eq 'ARRAY') {
351 $it = $it->[0];
352 }
353 else {
354 $it = $it->{VALUE};
355 }
356 }
357 eval {
358 if ($it =~ /^$string/) {
359 $hit = 1;
360 };
361 };
362 if (defined $hit && defined $found) {
363 return undef;
364 }
365 if ($hit) {
366 $found = $i + 1;
367 }
368 }
369 return $found;
370 }
371
372 # inserts string into trie and updates count for each character
373 sub update_trie {
374 my ($trie, $string) = @_;
375 foreach (split //, $string) {
376 $trie = $trie->{$_} ||= {COUNT => 0};
377 $trie->{COUNT}++;
378 }
379 }
380
381 # returns an array of tuples (prefix, remainder)
382 sub find_unique_prefixes {
383 my @stuff = @_;
384 my @return = ();
385
386 # any single prefix exceeding the soft limit is omitted
387 # if any prefix exceeds the hard limit all are omitted
388 # 0 indicates no limit
389 my $soft_limit = 0;
390 my $hard_limit = 3;
391
392 # build a trie modelling all possible options
393 my %trie;
394 foreach my $print (@stuff) {
395 if ((ref $print) eq 'ARRAY') {
396 $print = $print->[0];
397 }
398 elsif ((ref $print) eq 'HASH') {
399 $print = $print->{VALUE};
400 }
401 update_trie(\%trie, $print);
402 push @return, $print;
403 }
404
405 # use the trie to find the unique prefixes
406 for (my $i = 0; $i < @return; $i++) {
407 my $ret = $return[$i];
408 my @letters = split //, $ret;
409 my %search = %trie;
410 my ($prefix, $remainder);
411 my $j;
412 for ($j = 0; $j < @letters; $j++) {
413 my $letter = $letters[$j];
414 if ($search{$letter}{COUNT} == 1) {
415 $prefix = substr $ret, 0, $j + 1;
416 $remainder = substr $ret, $j + 1;
417 last;
418 }
419 else {
420 my $prefix = substr $ret, 0, $j;
421 return ()
422 if ($hard_limit && $j + 1 > $hard_limit);
423 }
424 %search = %{$search{$letter}};
425 }
426 if (ord($letters[0]) > 127 ||
427 ($soft_limit && $j + 1 > $soft_limit)) {
428 $prefix = undef;
429 $remainder = $ret;
430 }
431 $return[$i] = [$prefix, $remainder];
432 }
433 return @return;
434 }
435
436 # filters out prefixes which have special meaning to list_and_choose()
437 sub is_valid_prefix {
438 my $prefix = shift;
439 return (defined $prefix) &&
440 !($prefix =~ /[\s,]/) && # separators
441 !($prefix =~ /^-/) && # deselection
442 !($prefix =~ /^\d+/) && # selection
443 ($prefix ne '*') && # "all" wildcard
444 ($prefix ne '?'); # prompt help
445 }
446
447 # given a prefix/remainder tuple return a string with the prefix highlighted
448 # for now use square brackets; later might use ANSI colors (underline, bold)
449 sub highlight_prefix {
450 my $prefix = shift;
451 my $remainder = shift;
452
453 if (!defined $prefix) {
454 return $remainder;
455 }
456
457 if (!is_valid_prefix($prefix)) {
458 return "$prefix$remainder";
459 }
460
461 if (!$menu_use_color) {
462 return "[$prefix]$remainder";
463 }
464
465 return "$prompt_color$prefix$normal_color$remainder";
466 }
467
468 sub error_msg {
469 print STDERR colored $error_color, @_;
470 }
471
472 sub list_and_choose {
473 my ($opts, @stuff) = @_;
474 my (@chosen, @return);
475 if (!@stuff) {
476 return @return;
477 }
478 my $i;
479 my @prefixes = find_unique_prefixes(@stuff) unless $opts->{LIST_ONLY};
480
481 TOPLOOP:
482 while (1) {
483 my $last_lf = 0;
484
485 if ($opts->{HEADER}) {
486 my $indent = $opts->{LIST_FLAT} ? "" : " ";
487 print colored $header_color, "$indent$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, qw(--no-color --), $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 my $addition;
756
757 for (my $i = 0; $i < @{$src->{TEXT}}; $i++) {
758 if ($src->{TEXT}->[$i] =~ /^new file/) {
759 $addition = 1;
760 $head->{TYPE} = 'addition';
761 }
762 my $dest =
763 $src->{TEXT}->[$i] =~ /^(old|new) mode (\d+)$/ ? $mode :
764 $src->{TEXT}->[$i] =~ /^deleted file/ ? $deletion :
765 $head;
766 push @{$dest->{TEXT}}, $src->{TEXT}->[$i];
767 push @{$dest->{DISPLAY}}, $src->{DISPLAY}->[$i];
768 }
769 return ($head, $mode, $deletion, $addition);
770 }
771
772 sub hunk_splittable {
773 my ($text) = @_;
774
775 my @s = split_hunk($text);
776 return (1 < @s);
777 }
778
779 sub parse_hunk_header {
780 my ($line) = @_;
781 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
782 $line =~ /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
783 $o_cnt = 1 unless defined $o_cnt;
784 $n_cnt = 1 unless defined $n_cnt;
785 return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
786 }
787
788 sub format_hunk_header {
789 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = @_;
790 return ("@@ -$o_ofs" .
791 (($o_cnt != 1) ? ",$o_cnt" : '') .
792 " +$n_ofs" .
793 (($n_cnt != 1) ? ",$n_cnt" : '') .
794 " @@\n");
795 }
796
797 sub split_hunk {
798 my ($text, $display) = @_;
799 my @split = ();
800 if (!defined $display) {
801 $display = $text;
802 }
803 # If there are context lines in the middle of a hunk,
804 # it can be split, but we would need to take care of
805 # overlaps later.
806
807 my ($o_ofs, undef, $n_ofs) = parse_hunk_header($text->[0]);
808 my $hunk_start = 1;
809
810 OUTER:
811 while (1) {
812 my $next_hunk_start = undef;
813 my $i = $hunk_start - 1;
814 my $this = +{
815 TEXT => [],
816 DISPLAY => [],
817 TYPE => 'hunk',
818 OLD => $o_ofs,
819 NEW => $n_ofs,
820 OCNT => 0,
821 NCNT => 0,
822 ADDDEL => 0,
823 POSTCTX => 0,
824 USE => undef,
825 };
826
827 while (++$i < @$text) {
828 my $line = $text->[$i];
829 my $display = $display->[$i];
830 if ($line =~ /^\\/) {
831 push @{$this->{TEXT}}, $line;
832 push @{$this->{DISPLAY}}, $display;
833 next;
834 }
835 if ($line =~ /^ /) {
836 if ($this->{ADDDEL} &&
837 !defined $next_hunk_start) {
838 # We have seen leading context and
839 # adds/dels and then here is another
840 # context, which is trailing for this
841 # split hunk and leading for the next
842 # one.
843 $next_hunk_start = $i;
844 }
845 push @{$this->{TEXT}}, $line;
846 push @{$this->{DISPLAY}}, $display;
847 $this->{OCNT}++;
848 $this->{NCNT}++;
849 if (defined $next_hunk_start) {
850 $this->{POSTCTX}++;
851 }
852 next;
853 }
854
855 # add/del
856 if (defined $next_hunk_start) {
857 # We are done with the current hunk and
858 # this is the first real change for the
859 # next split one.
860 $hunk_start = $next_hunk_start;
861 $o_ofs = $this->{OLD} + $this->{OCNT};
862 $n_ofs = $this->{NEW} + $this->{NCNT};
863 $o_ofs -= $this->{POSTCTX};
864 $n_ofs -= $this->{POSTCTX};
865 push @split, $this;
866 redo OUTER;
867 }
868 push @{$this->{TEXT}}, $line;
869 push @{$this->{DISPLAY}}, $display;
870 $this->{ADDDEL}++;
871 if ($line =~ /^-/) {
872 $this->{OCNT}++;
873 }
874 else {
875 $this->{NCNT}++;
876 }
877 }
878
879 push @split, $this;
880 last;
881 }
882
883 for my $hunk (@split) {
884 $o_ofs = $hunk->{OLD};
885 $n_ofs = $hunk->{NEW};
886 my $o_cnt = $hunk->{OCNT};
887 my $n_cnt = $hunk->{NCNT};
888
889 my $head = format_hunk_header($o_ofs, $o_cnt, $n_ofs, $n_cnt);
890 my $display_head = $head;
891 unshift @{$hunk->{TEXT}}, $head;
892 if ($diff_use_color) {
893 $display_head = colored($fraginfo_color, $head);
894 }
895 unshift @{$hunk->{DISPLAY}}, $display_head;
896 }
897 return @split;
898 }
899
900 sub find_last_o_ctx {
901 my ($it) = @_;
902 my $text = $it->{TEXT};
903 my ($o_ofs, $o_cnt) = parse_hunk_header($text->[0]);
904 my $i = @{$text};
905 my $last_o_ctx = $o_ofs + $o_cnt;
906 while (0 < --$i) {
907 my $line = $text->[$i];
908 if ($line =~ /^ /) {
909 $last_o_ctx--;
910 next;
911 }
912 last;
913 }
914 return $last_o_ctx;
915 }
916
917 sub merge_hunk {
918 my ($prev, $this) = @_;
919 my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) =
920 parse_hunk_header($prev->{TEXT}[0]);
921 my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) =
922 parse_hunk_header($this->{TEXT}[0]);
923
924 my (@line, $i, $ofs, $o_cnt, $n_cnt);
925 $ofs = $o0_ofs;
926 $o_cnt = $n_cnt = 0;
927 for ($i = 1; $i < @{$prev->{TEXT}}; $i++) {
928 my $line = $prev->{TEXT}[$i];
929 if ($line =~ /^\+/) {
930 $n_cnt++;
931 push @line, $line;
932 next;
933 } elsif ($line =~ /^\\/) {
934 push @line, $line;
935 next;
936 }
937
938 last if ($o1_ofs <= $ofs);
939
940 $o_cnt++;
941 $ofs++;
942 if ($line =~ /^ /) {
943 $n_cnt++;
944 }
945 push @line, $line;
946 }
947
948 for ($i = 1; $i < @{$this->{TEXT}}; $i++) {
949 my $line = $this->{TEXT}[$i];
950 if ($line =~ /^\+/) {
951 $n_cnt++;
952 push @line, $line;
953 next;
954 } elsif ($line =~ /^\\/) {
955 push @line, $line;
956 next;
957 }
958 $ofs++;
959 $o_cnt++;
960 if ($line =~ /^ /) {
961 $n_cnt++;
962 }
963 push @line, $line;
964 }
965 my $head = format_hunk_header($o0_ofs, $o_cnt, $n0_ofs, $n_cnt);
966 @{$prev->{TEXT}} = ($head, @line);
967 }
968
969 sub coalesce_overlapping_hunks {
970 my (@in) = @_;
971 my @out = ();
972
973 my ($last_o_ctx, $last_was_dirty);
974 my $ofs_delta = 0;
975
976 for (@in) {
977 if ($_->{TYPE} ne 'hunk') {
978 push @out, $_;
979 next;
980 }
981 my $text = $_->{TEXT};
982 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
983 parse_hunk_header($text->[0]);
984 unless ($_->{USE}) {
985 $ofs_delta += $o_cnt - $n_cnt;
986 # If this hunk has been edited then subtract
987 # the delta that is due to the edit.
988 if ($_->{OFS_DELTA}) {
989 $ofs_delta -= $_->{OFS_DELTA};
990 }
991 next;
992 }
993 if ($ofs_delta) {
994 if ($patch_mode_flavour{IS_REVERSE}) {
995 $o_ofs -= $ofs_delta;
996 } else {
997 $n_ofs += $ofs_delta;
998 }
999 $_->{TEXT}->[0] = format_hunk_header($o_ofs, $o_cnt,
1000 $n_ofs, $n_cnt);
1001 }
1002 # If this hunk was edited then adjust the offset delta
1003 # to reflect the edit.
1004 if ($_->{OFS_DELTA}) {
1005 $ofs_delta += $_->{OFS_DELTA};
1006 }
1007 if (defined $last_o_ctx &&
1008 $o_ofs <= $last_o_ctx &&
1009 !$_->{DIRTY} &&
1010 !$last_was_dirty) {
1011 merge_hunk($out[-1], $_);
1012 }
1013 else {
1014 push @out, $_;
1015 }
1016 $last_o_ctx = find_last_o_ctx($out[-1]);
1017 $last_was_dirty = $_->{DIRTY};
1018 }
1019 return @out;
1020 }
1021
1022 sub reassemble_patch {
1023 my $head = shift;
1024 my @patch;
1025
1026 # Include everything in the header except the beginning of the diff.
1027 push @patch, (grep { !/^[-+]{3}/ } @$head);
1028
1029 # Then include any headers from the hunk lines, which must
1030 # come before any actual hunk.
1031 while (@_ && $_[0] !~ /^@/) {
1032 push @patch, shift;
1033 }
1034
1035 # Then begin the diff.
1036 push @patch, grep { /^[-+]{3}/ } @$head;
1037
1038 # And then the actual hunks.
1039 push @patch, @_;
1040
1041 return @patch;
1042 }
1043
1044 sub color_diff {
1045 return map {
1046 colored((/^@/ ? $fraginfo_color :
1047 /^\+/ ? $diff_new_color :
1048 /^-/ ? $diff_old_color :
1049 $diff_context_color),
1050 $_);
1051 } @_;
1052 }
1053
1054 my %edit_hunk_manually_modes = (
1055 stage => N__(
1056 "If the patch applies cleanly, the edited hunk will immediately be
1057 marked for staging."),
1058 stash => N__(
1059 "If the patch applies cleanly, the edited hunk will immediately be
1060 marked for stashing."),
1061 reset_head => N__(
1062 "If the patch applies cleanly, the edited hunk will immediately be
1063 marked for unstaging."),
1064 reset_nothead => N__(
1065 "If the patch applies cleanly, the edited hunk will immediately be
1066 marked for applying."),
1067 checkout_index => N__(
1068 "If the patch applies cleanly, the edited hunk will immediately be
1069 marked for discarding."),
1070 checkout_head => N__(
1071 "If the patch applies cleanly, the edited hunk will immediately be
1072 marked for discarding."),
1073 checkout_nothead => N__(
1074 "If the patch applies cleanly, the edited hunk will immediately be
1075 marked for applying."),
1076 worktree_head => N__(
1077 "If the patch applies cleanly, the edited hunk will immediately be
1078 marked for discarding."),
1079 worktree_nothead => N__(
1080 "If the patch applies cleanly, the edited hunk will immediately be
1081 marked for applying."),
1082 );
1083
1084 sub recount_edited_hunk {
1085 local $_;
1086 my ($oldtext, $newtext) = @_;
1087 my ($o_cnt, $n_cnt) = (0, 0);
1088 for (@{$newtext}[1..$#{$newtext}]) {
1089 my $mode = substr($_, 0, 1);
1090 if ($mode eq '-') {
1091 $o_cnt++;
1092 } elsif ($mode eq '+') {
1093 $n_cnt++;
1094 } elsif ($mode eq ' ' or $mode eq "\n") {
1095 $o_cnt++;
1096 $n_cnt++;
1097 }
1098 }
1099 my ($o_ofs, undef, $n_ofs, undef) =
1100 parse_hunk_header($newtext->[0]);
1101 $newtext->[0] = format_hunk_header($o_ofs, $o_cnt, $n_ofs, $n_cnt);
1102 my (undef, $orig_o_cnt, undef, $orig_n_cnt) =
1103 parse_hunk_header($oldtext->[0]);
1104 # Return the change in the number of lines inserted by this hunk
1105 return $orig_o_cnt - $orig_n_cnt - $o_cnt + $n_cnt;
1106 }
1107
1108 sub edit_hunk_manually {
1109 my ($oldtext) = @_;
1110
1111 my $hunkfile = $repo->repo_path . "/addp-hunk-edit.diff";
1112 my $fh;
1113 open $fh, '>', $hunkfile
1114 or die sprintf(__("failed to open hunk edit file for writing: %s"), $!);
1115 print $fh Git::comment_lines __("Manual hunk edit mode -- see bottom for a quick guide.\n");
1116 print $fh @$oldtext;
1117 my $is_reverse = $patch_mode_flavour{IS_REVERSE};
1118 my ($remove_plus, $remove_minus) = $is_reverse ? ('-', '+') : ('+', '-');
1119 my $comment_line_char = Git::get_comment_line_char;
1120 print $fh Git::comment_lines sprintf(__ <<EOF, $remove_minus, $remove_plus, $comment_line_char),
1121 ---
1122 To remove '%s' lines, make them ' ' lines (context).
1123 To remove '%s' lines, delete them.
1124 Lines starting with %s will be removed.
1125 EOF
1126 __($edit_hunk_manually_modes{$patch_mode}),
1127 # TRANSLATORS: 'it' refers to the patch mentioned in the previous messages.
1128 __ <<EOF2 ;
1129 If it does not apply cleanly, you will be given an opportunity to
1130 edit again. If all lines of the hunk are removed, then the edit is
1131 aborted and the hunk is left unchanged.
1132 EOF2
1133 close $fh;
1134
1135 chomp(my ($editor) = run_cmd_pipe(qw(git var GIT_EDITOR)));
1136 system('sh', '-c', $editor.' "$@"', $editor, $hunkfile);
1137
1138 if ($? != 0) {
1139 return undef;
1140 }
1141
1142 open $fh, '<', $hunkfile
1143 or die sprintf(__("failed to open hunk edit file for reading: %s"), $!);
1144 my @newtext = grep { !/^\Q$comment_line_char\E/ } <$fh>;
1145 close $fh;
1146 unlink $hunkfile;
1147
1148 # Abort if nothing remains
1149 if (!grep { /\S/ } @newtext) {
1150 return undef;
1151 }
1152
1153 # Reinsert the first hunk header if the user accidentally deleted it
1154 if ($newtext[0] !~ /^@/) {
1155 unshift @newtext, $oldtext->[0];
1156 }
1157 return \@newtext;
1158 }
1159
1160 sub diff_applies {
1161 return run_git_apply($patch_mode_flavour{APPLY_CHECK} . ' --check',
1162 map { @{$_->{TEXT}} } @_);
1163 }
1164
1165 sub _restore_terminal_and_die {
1166 ReadMode 'restore';
1167 print "\n";
1168 exit 1;
1169 }
1170
1171 sub prompt_single_character {
1172 if ($use_readkey) {
1173 local $SIG{TERM} = \&_restore_terminal_and_die;
1174 local $SIG{INT} = \&_restore_terminal_and_die;
1175 ReadMode 'cbreak';
1176 my $key = ReadKey 0;
1177 ReadMode 'restore';
1178 if ($use_termcap and $key eq "\e") {
1179 while (!defined $term_escapes{$key}) {
1180 my $next = ReadKey 0.5;
1181 last if (!defined $next);
1182 $key .= $next;
1183 }
1184 $key =~ s/\e/^[/;
1185 }
1186 print "$key" if defined $key;
1187 print "\n";
1188 return $key;
1189 } else {
1190 return <STDIN>;
1191 }
1192 }
1193
1194 sub prompt_yesno {
1195 my ($prompt) = @_;
1196 while (1) {
1197 print colored $prompt_color, $prompt;
1198 my $line = prompt_single_character;
1199 return undef unless defined $line;
1200 return 0 if $line =~ /^n/i;
1201 return 1 if $line =~ /^y/i;
1202 }
1203 }
1204
1205 sub edit_hunk_loop {
1206 my ($head, $hunks, $ix) = @_;
1207 my $hunk = $hunks->[$ix];
1208 my $text = $hunk->{TEXT};
1209
1210 while (1) {
1211 my $newtext = edit_hunk_manually($text);
1212 if (!defined $newtext) {
1213 return undef;
1214 }
1215 my $newhunk = {
1216 TEXT => $newtext,
1217 TYPE => $hunk->{TYPE},
1218 USE => 1,
1219 DIRTY => 1,
1220 };
1221 $newhunk->{OFS_DELTA} = recount_edited_hunk($text, $newtext);
1222 # If this hunk has already been edited then add the
1223 # offset delta of the previous edit to get the real
1224 # delta from the original unedited hunk.
1225 $hunk->{OFS_DELTA} and
1226 $newhunk->{OFS_DELTA} += $hunk->{OFS_DELTA};
1227 if (diff_applies($head,
1228 @{$hunks}[0..$ix-1],
1229 $newhunk,
1230 @{$hunks}[$ix+1..$#{$hunks}])) {
1231 $newhunk->{DISPLAY} = [color_diff(@{$newtext})];
1232 return $newhunk;
1233 }
1234 else {
1235 prompt_yesno(
1236 # TRANSLATORS: do not translate [y/n]
1237 # The program will only accept that input
1238 # at this point.
1239 # Consider translating (saying "no" discards!) as
1240 # (saying "n" for "no" discards!) if the translation
1241 # of the word "no" does not start with n.
1242 __('Your edited hunk does not apply. Edit again '
1243 . '(saying "no" discards!) [y/n]? ')
1244 ) or return undef;
1245 }
1246 }
1247 }
1248
1249 my %help_patch_modes = (
1250 stage => N__(
1251 "y - stage this hunk
1252 n - do not stage this hunk
1253 q - quit; do not stage this hunk or any of the remaining ones
1254 a - stage this hunk and all later hunks in the file
1255 d - do not stage this hunk or any of the later hunks in the file"),
1256 stash => N__(
1257 "y - stash this hunk
1258 n - do not stash this hunk
1259 q - quit; do not stash this hunk or any of the remaining ones
1260 a - stash this hunk and all later hunks in the file
1261 d - do not stash this hunk or any of the later hunks in the file"),
1262 reset_head => N__(
1263 "y - unstage this hunk
1264 n - do not unstage this hunk
1265 q - quit; do not unstage this hunk or any of the remaining ones
1266 a - unstage this hunk and all later hunks in the file
1267 d - do not unstage this hunk or any of the later hunks in the file"),
1268 reset_nothead => N__(
1269 "y - apply this hunk to index
1270 n - do not apply this hunk to index
1271 q - quit; do not apply this hunk or any of the remaining ones
1272 a - apply this hunk and all later hunks in the file
1273 d - do not apply this hunk or any of the later hunks in the file"),
1274 checkout_index => N__(
1275 "y - discard this hunk from worktree
1276 n - do not discard this hunk from worktree
1277 q - quit; do not discard this hunk or any of the remaining ones
1278 a - discard this hunk and all later hunks in the file
1279 d - do not discard this hunk or any of the later hunks in the file"),
1280 checkout_head => N__(
1281 "y - discard this hunk from index and worktree
1282 n - do not discard this hunk from index and worktree
1283 q - quit; do not discard this hunk or any of the remaining ones
1284 a - discard this hunk and all later hunks in the file
1285 d - do not discard this hunk or any of the later hunks in the file"),
1286 checkout_nothead => N__(
1287 "y - apply this hunk to index and worktree
1288 n - do not apply this hunk to index and worktree
1289 q - quit; do not apply this hunk or any of the remaining ones
1290 a - apply this hunk and all later hunks in the file
1291 d - do not apply this hunk or any of the later hunks in the file"),
1292 worktree_head => N__(
1293 "y - discard this hunk from worktree
1294 n - do not discard this hunk from worktree
1295 q - quit; do not discard this hunk or any of the remaining ones
1296 a - discard this hunk and all later hunks in the file
1297 d - do not discard this hunk or any of the later hunks in the file"),
1298 worktree_nothead => N__(
1299 "y - apply this hunk to worktree
1300 n - do not apply this hunk to worktree
1301 q - quit; do not apply this hunk or any of the remaining ones
1302 a - apply this hunk and all later hunks in the file
1303 d - do not apply this hunk or any of the later hunks in the file"),
1304 );
1305
1306 sub help_patch_cmd {
1307 local $_;
1308 my $other = $_[0] . ",?";
1309 print colored $help_color, __($help_patch_modes{$patch_mode}), "\n",
1310 map { "$_\n" } grep {
1311 my $c = quotemeta(substr($_, 0, 1));
1312 $other =~ /,$c/
1313 } split "\n", __ <<EOF ;
1314 g - select a hunk to go to
1315 / - search for a hunk matching the given regex
1316 j - leave this hunk undecided, see next undecided hunk
1317 J - leave this hunk undecided, see next hunk
1318 k - leave this hunk undecided, see previous undecided hunk
1319 K - leave this hunk undecided, see previous hunk
1320 s - split the current hunk into smaller hunks
1321 e - manually edit the current hunk
1322 ? - print help
1323 EOF
1324 }
1325
1326 sub apply_patch {
1327 my $cmd = shift;
1328 my $ret = run_git_apply $cmd, @_;
1329 if (!$ret) {
1330 print STDERR @_;
1331 }
1332 return $ret;
1333 }
1334
1335 sub apply_patch_for_checkout_commit {
1336 my $reverse = shift;
1337 my $applies_index = run_git_apply 'apply '.$reverse.' --cached --check', @_;
1338 my $applies_worktree = run_git_apply 'apply '.$reverse.' --check', @_;
1339
1340 if ($applies_worktree && $applies_index) {
1341 run_git_apply 'apply '.$reverse.' --cached', @_;
1342 run_git_apply 'apply '.$reverse, @_;
1343 return 1;
1344 } elsif (!$applies_index) {
1345 print colored $error_color, __("The selected hunks do not apply to the index!\n");
1346 if (prompt_yesno __("Apply them to the worktree anyway? ")) {
1347 return run_git_apply 'apply '.$reverse, @_;
1348 } else {
1349 print colored $error_color, __("Nothing was applied.\n");
1350 return 0;
1351 }
1352 } else {
1353 print STDERR @_;
1354 return 0;
1355 }
1356 }
1357
1358 sub patch_update_cmd {
1359 my @all_mods = list_modified($patch_mode_flavour{FILTER});
1360 error_msg sprintf(__("ignoring unmerged: %s\n"), $_->{VALUE})
1361 for grep { $_->{UNMERGED} } @all_mods;
1362 @all_mods = grep { !$_->{UNMERGED} } @all_mods;
1363
1364 my @mods = grep { !($_->{BINARY}) } @all_mods;
1365 my @them;
1366
1367 if (!@mods) {
1368 if (@all_mods) {
1369 print STDERR __("Only binary files changed.\n");
1370 } else {
1371 print STDERR __("No changes.\n");
1372 }
1373 return 0;
1374 }
1375 if ($patch_mode_only) {
1376 @them = @mods;
1377 }
1378 else {
1379 @them = list_and_choose({ PROMPT => __('Patch update'),
1380 HEADER => $status_head, },
1381 @mods);
1382 }
1383 for (@them) {
1384 return 0 if patch_update_file($_->{VALUE});
1385 }
1386 }
1387
1388 # Generate a one line summary of a hunk.
1389 sub summarize_hunk {
1390 my $rhunk = shift;
1391 my $summary = $rhunk->{TEXT}[0];
1392
1393 # Keep the line numbers, discard extra context.
1394 $summary =~ s/@@(.*?)@@.*/$1 /s;
1395 $summary .= " " x (20 - length $summary);
1396
1397 # Add some user context.
1398 for my $line (@{$rhunk->{TEXT}}) {
1399 if ($line =~ m/^[+-].*\w/) {
1400 $summary .= $line;
1401 last;
1402 }
1403 }
1404
1405 chomp $summary;
1406 return substr($summary, 0, 80) . "\n";
1407 }
1408
1409
1410 # Print a one-line summary of each hunk in the array ref in
1411 # the first argument, starting with the index in the 2nd.
1412 sub display_hunks {
1413 my ($hunks, $i) = @_;
1414 my $ctr = 0;
1415 $i ||= 0;
1416 for (; $i < @$hunks && $ctr < 20; $i++, $ctr++) {
1417 my $status = " ";
1418 if (defined $hunks->[$i]{USE}) {
1419 $status = $hunks->[$i]{USE} ? "+" : "-";
1420 }
1421 printf "%s%2d: %s",
1422 $status,
1423 $i + 1,
1424 summarize_hunk($hunks->[$i]);
1425 }
1426 return $i;
1427 }
1428
1429 my %patch_update_prompt_modes = (
1430 stage => {
1431 mode => N__("Stage mode change [y,n,q,a,d%s,?]? "),
1432 deletion => N__("Stage deletion [y,n,q,a,d%s,?]? "),
1433 addition => N__("Stage addition [y,n,q,a,d%s,?]? "),
1434 hunk => N__("Stage this hunk [y,n,q,a,d%s,?]? "),
1435 },
1436 stash => {
1437 mode => N__("Stash mode change [y,n,q,a,d%s,?]? "),
1438 deletion => N__("Stash deletion [y,n,q,a,d%s,?]? "),
1439 addition => N__("Stash addition [y,n,q,a,d%s,?]? "),
1440 hunk => N__("Stash this hunk [y,n,q,a,d%s,?]? "),
1441 },
1442 reset_head => {
1443 mode => N__("Unstage mode change [y,n,q,a,d%s,?]? "),
1444 deletion => N__("Unstage deletion [y,n,q,a,d%s,?]? "),
1445 addition => N__("Unstage addition [y,n,q,a,d%s,?]? "),
1446 hunk => N__("Unstage this hunk [y,n,q,a,d%s,?]? "),
1447 },
1448 reset_nothead => {
1449 mode => N__("Apply mode change to index [y,n,q,a,d%s,?]? "),
1450 deletion => N__("Apply deletion to index [y,n,q,a,d%s,?]? "),
1451 addition => N__("Apply addition to index [y,n,q,a,d%s,?]? "),
1452 hunk => N__("Apply this hunk to index [y,n,q,a,d%s,?]? "),
1453 },
1454 checkout_index => {
1455 mode => N__("Discard mode change from worktree [y,n,q,a,d%s,?]? "),
1456 deletion => N__("Discard deletion from worktree [y,n,q,a,d%s,?]? "),
1457 addition => N__("Discard addition from worktree [y,n,q,a,d%s,?]? "),
1458 hunk => N__("Discard this hunk from worktree [y,n,q,a,d%s,?]? "),
1459 },
1460 checkout_head => {
1461 mode => N__("Discard mode change from index and worktree [y,n,q,a,d%s,?]? "),
1462 deletion => N__("Discard deletion from index and worktree [y,n,q,a,d%s,?]? "),
1463 addition => N__("Discard addition from index and worktree [y,n,q,a,d%s,?]? "),
1464 hunk => N__("Discard this hunk from index and worktree [y,n,q,a,d%s,?]? "),
1465 },
1466 checkout_nothead => {
1467 mode => N__("Apply mode change to index and worktree [y,n,q,a,d%s,?]? "),
1468 deletion => N__("Apply deletion to index and worktree [y,n,q,a,d%s,?]? "),
1469 addition => N__("Apply addition to index and worktree [y,n,q,a,d%s,?]? "),
1470 hunk => N__("Apply this hunk to index and worktree [y,n,q,a,d%s,?]? "),
1471 },
1472 worktree_head => {
1473 mode => N__("Discard mode change from worktree [y,n,q,a,d%s,?]? "),
1474 deletion => N__("Discard deletion from worktree [y,n,q,a,d%s,?]? "),
1475 addition => N__("Discard addition from worktree [y,n,q,a,d%s,?]? "),
1476 hunk => N__("Discard this hunk from worktree [y,n,q,a,d%s,?]? "),
1477 },
1478 worktree_nothead => {
1479 mode => N__("Apply mode change to worktree [y,n,q,a,d%s,?]? "),
1480 deletion => N__("Apply deletion to worktree [y,n,q,a,d%s,?]? "),
1481 addition => N__("Apply addition to worktree [y,n,q,a,d%s,?]? "),
1482 hunk => N__("Apply this hunk to worktree [y,n,q,a,d%s,?]? "),
1483 },
1484 );
1485
1486 sub patch_update_file {
1487 my $quit = 0;
1488 my ($ix, $num);
1489 my $path = shift;
1490 my ($head, @hunk) = parse_diff($path);
1491 ($head, my $mode, my $deletion, my $addition) = parse_diff_header($head);
1492 for (@{$head->{DISPLAY}}) {
1493 print;
1494 }
1495
1496 if (@{$mode->{TEXT}}) {
1497 unshift @hunk, $mode;
1498 }
1499 if (@{$deletion->{TEXT}}) {
1500 foreach my $hunk (@hunk) {
1501 push @{$deletion->{TEXT}}, @{$hunk->{TEXT}};
1502 push @{$deletion->{DISPLAY}}, @{$hunk->{DISPLAY}};
1503 }
1504 @hunk = ($deletion);
1505 }
1506
1507 $num = scalar @hunk;
1508 $ix = 0;
1509
1510 while (1) {
1511 my ($prev, $next, $other, $undecided, $i);
1512 $other = '';
1513
1514 last if ($ix and !$num);
1515 if ($num <= $ix) {
1516 $ix = 0;
1517 }
1518 for ($i = 0; $i < $ix; $i++) {
1519 if (!defined $hunk[$i]{USE}) {
1520 $prev = 1;
1521 $other .= ',k';
1522 last;
1523 }
1524 }
1525 if ($ix) {
1526 $other .= ',K';
1527 }
1528 for ($i = $ix + 1; $i < $num; $i++) {
1529 if (!defined $hunk[$i]{USE}) {
1530 $next = 1;
1531 $other .= ',j';
1532 last;
1533 }
1534 }
1535 if ($ix < $num - 1) {
1536 $other .= ',J';
1537 }
1538 if ($num > 1) {
1539 $other .= ',g,/';
1540 }
1541 for ($i = 0; $i < $num; $i++) {
1542 if (!defined $hunk[$i]{USE}) {
1543 $undecided = 1;
1544 last;
1545 }
1546 }
1547 last if (!$undecided && ($num || !$addition));
1548
1549 if ($num) {
1550 if ($hunk[$ix]{TYPE} eq 'hunk' &&
1551 hunk_splittable($hunk[$ix]{TEXT})) {
1552 $other .= ',s';
1553 }
1554 if ($hunk[$ix]{TYPE} eq 'hunk') {
1555 $other .= ',e';
1556 }
1557 for (@{$hunk[$ix]{DISPLAY}}) {
1558 print;
1559 }
1560 }
1561 my $type = $num ? $hunk[$ix]{TYPE} : $head->{TYPE};
1562 print colored $prompt_color, "(", ($ix+1), "/", ($num ? $num : 1), ") ",
1563 sprintf(__($patch_update_prompt_modes{$patch_mode}{$type}), $other);
1564
1565 my $line = prompt_single_character;
1566 last unless defined $line;
1567 if ($line) {
1568 if ($line =~ /^y/i) {
1569 if ($num) {
1570 $hunk[$ix]{USE} = 1;
1571 } else {
1572 $head->{USE} = 1;
1573 }
1574 }
1575 elsif ($line =~ /^n/i) {
1576 if ($num) {
1577 $hunk[$ix]{USE} = 0;
1578 } else {
1579 $head->{USE} = 0;
1580 }
1581 }
1582 elsif ($line =~ /^a/i) {
1583 if ($num) {
1584 while ($ix < $num) {
1585 if (!defined $hunk[$ix]{USE}) {
1586 $hunk[$ix]{USE} = 1;
1587 }
1588 $ix++;
1589 }
1590 } else {
1591 $head->{USE} = 1;
1592 $ix++;
1593 }
1594 next;
1595 }
1596 elsif ($line =~ /^g(.*)/) {
1597 my $response = $1;
1598 unless ($other =~ /g/) {
1599 error_msg __("No other hunks to goto\n");
1600 next;
1601 }
1602 my $no = $ix > 10 ? $ix - 10 : 0;
1603 while ($response eq '') {
1604 $no = display_hunks(\@hunk, $no);
1605 if ($no < $num) {
1606 print __("go to which hunk (<ret> to see more)? ");
1607 } else {
1608 print __("go to which hunk? ");
1609 }
1610 $response = <STDIN>;
1611 if (!defined $response) {
1612 $response = '';
1613 }
1614 chomp $response;
1615 }
1616 if ($response !~ /^\s*\d+\s*$/) {
1617 error_msg sprintf(__("Invalid number: '%s'\n"),
1618 $response);
1619 } elsif (0 < $response && $response <= $num) {
1620 $ix = $response - 1;
1621 } else {
1622 error_msg sprintf(__n("Sorry, only %d hunk available.\n",
1623 "Sorry, only %d hunks available.\n", $num), $num);
1624 }
1625 next;
1626 }
1627 elsif ($line =~ /^d/i) {
1628 if ($num) {
1629 while ($ix < $num) {
1630 if (!defined $hunk[$ix]{USE}) {
1631 $hunk[$ix]{USE} = 0;
1632 }
1633 $ix++;
1634 }
1635 } else {
1636 $head->{USE} = 0;
1637 $ix++;
1638 }
1639 next;
1640 }
1641 elsif ($line =~ /^q/i) {
1642 if ($num) {
1643 for ($i = 0; $i < $num; $i++) {
1644 if (!defined $hunk[$i]{USE}) {
1645 $hunk[$i]{USE} = 0;
1646 }
1647 }
1648 } elsif (!defined $head->{USE}) {
1649 $head->{USE} = 0;
1650 }
1651 $quit = 1;
1652 last;
1653 }
1654 elsif ($line =~ m|^/(.*)|) {
1655 my $regex = $1;
1656 unless ($other =~ m|/|) {
1657 error_msg __("No other hunks to search\n");
1658 next;
1659 }
1660 if ($regex eq "") {
1661 print colored $prompt_color, __("search for regex? ");
1662 $regex = <STDIN>;
1663 if (defined $regex) {
1664 chomp $regex;
1665 }
1666 }
1667 my $search_string;
1668 eval {
1669 $search_string = qr{$regex}m;
1670 };
1671 if ($@) {
1672 my ($err,$exp) = ($@, $1);
1673 $err =~ s/ at .*git-add--interactive line \d+, <STDIN> line \d+.*$//;
1674 error_msg sprintf(__("Malformed search regexp %s: %s\n"), $exp, $err);
1675 next;
1676 }
1677 my $iy = $ix;
1678 while (1) {
1679 my $text = join ("", @{$hunk[$iy]{TEXT}});
1680 last if ($text =~ $search_string);
1681 $iy++;
1682 $iy = 0 if ($iy >= $num);
1683 if ($ix == $iy) {
1684 error_msg __("No hunk matches the given pattern\n");
1685 last;
1686 }
1687 }
1688 $ix = $iy;
1689 next;
1690 }
1691 elsif ($line =~ /^K/) {
1692 if ($other =~ /K/) {
1693 $ix--;
1694 }
1695 else {
1696 error_msg __("No previous hunk\n");
1697 }
1698 next;
1699 }
1700 elsif ($line =~ /^J/) {
1701 if ($other =~ /J/) {
1702 $ix++;
1703 }
1704 else {
1705 error_msg __("No next hunk\n");
1706 }
1707 next;
1708 }
1709 elsif ($line =~ /^k/) {
1710 if ($other =~ /k/) {
1711 while (1) {
1712 $ix--;
1713 last if (!$ix ||
1714 !defined $hunk[$ix]{USE});
1715 }
1716 }
1717 else {
1718 error_msg __("No previous hunk\n");
1719 }
1720 next;
1721 }
1722 elsif ($line =~ /^j/) {
1723 if ($other !~ /j/) {
1724 error_msg __("No next hunk\n");
1725 next;
1726 }
1727 }
1728 elsif ($line =~ /^s/) {
1729 unless ($other =~ /s/) {
1730 error_msg __("Sorry, cannot split this hunk\n");
1731 next;
1732 }
1733 my @split = split_hunk($hunk[$ix]{TEXT}, $hunk[$ix]{DISPLAY});
1734 if (1 < @split) {
1735 print colored $header_color, sprintf(
1736 __n("Split into %d hunk.\n",
1737 "Split into %d hunks.\n",
1738 scalar(@split)), scalar(@split));
1739 }
1740 splice (@hunk, $ix, 1, @split);
1741 $num = scalar @hunk;
1742 next;
1743 }
1744 elsif ($line =~ /^e/) {
1745 unless ($other =~ /e/) {
1746 error_msg __("Sorry, cannot edit this hunk\n");
1747 next;
1748 }
1749 my $newhunk = edit_hunk_loop($head, \@hunk, $ix);
1750 if (defined $newhunk) {
1751 splice @hunk, $ix, 1, $newhunk;
1752 }
1753 }
1754 else {
1755 help_patch_cmd($other);
1756 next;
1757 }
1758 # soft increment
1759 while (1) {
1760 $ix++;
1761 last if ($ix >= $num ||
1762 !defined $hunk[$ix]{USE});
1763 }
1764 }
1765 }
1766
1767 @hunk = coalesce_overlapping_hunks(@hunk) if ($num);
1768
1769 my $n_lofs = 0;
1770 my @result = ();
1771 for (@hunk) {
1772 if ($_->{USE}) {
1773 push @result, @{$_->{TEXT}};
1774 }
1775 }
1776
1777 if (@result or $head->{USE}) {
1778 my @patch = reassemble_patch($head->{TEXT}, @result);
1779 my $apply_routine = $patch_mode_flavour{APPLY};
1780 &$apply_routine(@patch);
1781 refresh();
1782 }
1783
1784 print "\n";
1785 return $quit;
1786 }
1787
1788 sub diff_cmd {
1789 my @mods = list_modified('index-only');
1790 @mods = grep { !($_->{BINARY}) } @mods;
1791 return if (!@mods);
1792 my (@them) = list_and_choose({ PROMPT => __('Review diff'),
1793 IMMEDIATE => 1,
1794 HEADER => $status_head, },
1795 @mods);
1796 return if (!@them);
1797 my $reference = (is_initial_commit()) ? get_empty_tree() : 'HEAD';
1798 system(qw(git diff -p --cached), $reference, '--',
1799 map { $_->{VALUE} } @them);
1800 }
1801
1802 sub quit_cmd {
1803 print __("Bye.\n");
1804 exit(0);
1805 }
1806
1807 sub help_cmd {
1808 # TRANSLATORS: please do not translate the command names
1809 # 'status', 'update', 'revert', etc.
1810 print colored $help_color, __ <<'EOF' ;
1811 status - show paths with changes
1812 update - add working tree state to the staged set of changes
1813 revert - revert staged set of changes back to the HEAD version
1814 patch - pick hunks and update selectively
1815 diff - view diff between HEAD and index
1816 add untracked - add contents of untracked files to the staged set of changes
1817 EOF
1818 }
1819
1820 sub process_args {
1821 return unless @ARGV;
1822 my $arg = shift @ARGV;
1823 if ($arg =~ /--patch(?:=(.*))?/) {
1824 if (defined $1) {
1825 if ($1 eq 'reset') {
1826 $patch_mode = 'reset_head';
1827 $patch_mode_revision = 'HEAD';
1828 $arg = shift @ARGV or die __("missing --");
1829 if ($arg ne '--') {
1830 $patch_mode_revision = $arg;
1831
1832 # NEEDSWORK: Instead of comparing to the literal "HEAD",
1833 # compare the commit objects instead so that other ways of
1834 # saying the same thing (such as "@") are also handled
1835 # appropriately.
1836 #
1837 # This applies to the cases below too.
1838 $patch_mode = ($arg eq 'HEAD' ?
1839 'reset_head' : 'reset_nothead');
1840 $arg = shift @ARGV or die __("missing --");
1841 }
1842 } elsif ($1 eq 'checkout') {
1843 $arg = shift @ARGV or die __("missing --");
1844 if ($arg eq '--') {
1845 $patch_mode = 'checkout_index';
1846 } else {
1847 $patch_mode_revision = $arg;
1848 $patch_mode = ($arg eq 'HEAD' ?
1849 'checkout_head' : 'checkout_nothead');
1850 $arg = shift @ARGV or die __("missing --");
1851 }
1852 } elsif ($1 eq 'worktree') {
1853 $arg = shift @ARGV or die __("missing --");
1854 if ($arg eq '--') {
1855 $patch_mode = 'checkout_index';
1856 } else {
1857 $patch_mode_revision = $arg;
1858 $patch_mode = ($arg eq 'HEAD' ?
1859 'worktree_head' : 'worktree_nothead');
1860 $arg = shift @ARGV or die __("missing --");
1861 }
1862 } elsif ($1 eq 'stage' or $1 eq 'stash') {
1863 $patch_mode = $1;
1864 $arg = shift @ARGV or die __("missing --");
1865 } else {
1866 die sprintf(__("unknown --patch mode: %s"), $1);
1867 }
1868 } else {
1869 $patch_mode = 'stage';
1870 $arg = shift @ARGV or die __("missing --");
1871 }
1872 die sprintf(__("invalid argument %s, expecting --"),
1873 $arg) unless $arg eq "--";
1874 %patch_mode_flavour = %{$patch_modes{$patch_mode}};
1875 $patch_mode_only = 1;
1876 }
1877 elsif ($arg ne "--") {
1878 die sprintf(__("invalid argument %s, expecting --"), $arg);
1879 }
1880 }
1881
1882 sub main_loop {
1883 my @cmd = ([ 'status', \&status_cmd, ],
1884 [ 'update', \&update_cmd, ],
1885 [ 'revert', \&revert_cmd, ],
1886 [ 'add untracked', \&add_untracked_cmd, ],
1887 [ 'patch', \&patch_update_cmd, ],
1888 [ 'diff', \&diff_cmd, ],
1889 [ 'quit', \&quit_cmd, ],
1890 [ 'help', \&help_cmd, ],
1891 );
1892 while (1) {
1893 my ($it) = list_and_choose({ PROMPT => __('What now'),
1894 SINGLETON => 1,
1895 LIST_FLAT => 4,
1896 HEADER => __('*** Commands ***'),
1897 ON_EOF => \&quit_cmd,
1898 IMMEDIATE => 1 }, @cmd);
1899 if ($it) {
1900 eval {
1901 $it->[1]->();
1902 };
1903 if ($@) {
1904 print "$@";
1905 }
1906 }
1907 }
1908 }
1909
1910 process_args();
1911 refresh();
1912 if ($patch_mode_only) {
1913 patch_update_cmd();
1914 }
1915 else {
1916 status_cmd();
1917 main_loop();
1918 }