]> git.ipfire.org Git - thirdparty/git.git/blob - git-add--interactive.perl
Merge branch 'pb/ref-filter-with-crlf'
[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 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 if (!$opts->{LIST_FLAT}) {
487 print " ";
488 }
489 print colored $header_color, "$opts->{HEADER}\n";
490 }
491 for ($i = 0; $i < @stuff; $i++) {
492 my $chosen = $chosen[$i] ? '*' : ' ';
493 my $print = $stuff[$i];
494 my $ref = ref $print;
495 my $highlighted = highlight_prefix(@{$prefixes[$i]})
496 if @prefixes;
497 if ($ref eq 'ARRAY') {
498 $print = $highlighted || $print->[0];
499 }
500 elsif ($ref eq 'HASH') {
501 my $value = $highlighted || $print->{VALUE};
502 $print = sprintf($status_fmt,
503 $print->{INDEX},
504 $print->{FILE},
505 $value);
506 }
507 else {
508 $print = $highlighted || $print;
509 }
510 printf("%s%2d: %s", $chosen, $i+1, $print);
511 if (($opts->{LIST_FLAT}) &&
512 (($i + 1) % ($opts->{LIST_FLAT}))) {
513 print "\t";
514 $last_lf = 0;
515 }
516 else {
517 print "\n";
518 $last_lf = 1;
519 }
520 }
521 if (!$last_lf) {
522 print "\n";
523 }
524
525 return if ($opts->{LIST_ONLY});
526
527 print colored $prompt_color, $opts->{PROMPT};
528 if ($opts->{SINGLETON}) {
529 print "> ";
530 }
531 else {
532 print ">> ";
533 }
534 my $line = <STDIN>;
535 if (!$line) {
536 print "\n";
537 $opts->{ON_EOF}->() if $opts->{ON_EOF};
538 last;
539 }
540 chomp $line;
541 last if $line eq '';
542 if ($line eq '?') {
543 $opts->{SINGLETON} ?
544 singleton_prompt_help_cmd() :
545 prompt_help_cmd();
546 next TOPLOOP;
547 }
548 for my $choice (split(/[\s,]+/, $line)) {
549 my $choose = 1;
550 my ($bottom, $top);
551
552 # Input that begins with '-'; unchoose
553 if ($choice =~ s/^-//) {
554 $choose = 0;
555 }
556 # A range can be specified like 5-7 or 5-.
557 if ($choice =~ /^(\d+)-(\d*)$/) {
558 ($bottom, $top) = ($1, length($2) ? $2 : 1 + @stuff);
559 }
560 elsif ($choice =~ /^\d+$/) {
561 $bottom = $top = $choice;
562 }
563 elsif ($choice eq '*') {
564 $bottom = 1;
565 $top = 1 + @stuff;
566 }
567 else {
568 $bottom = $top = find_unique($choice, @stuff);
569 if (!defined $bottom) {
570 error_msg sprintf(__("Huh (%s)?\n"), $choice);
571 next TOPLOOP;
572 }
573 }
574 if ($opts->{SINGLETON} && $bottom != $top) {
575 error_msg sprintf(__("Huh (%s)?\n"), $choice);
576 next TOPLOOP;
577 }
578 for ($i = $bottom-1; $i <= $top-1; $i++) {
579 next if (@stuff <= $i || $i < 0);
580 $chosen[$i] = $choose;
581 }
582 }
583 last if ($opts->{IMMEDIATE} || $line eq '*');
584 }
585 for ($i = 0; $i < @stuff; $i++) {
586 if ($chosen[$i]) {
587 push @return, $stuff[$i];
588 }
589 }
590 return @return;
591 }
592
593 sub singleton_prompt_help_cmd {
594 print colored $help_color, __ <<'EOF' ;
595 Prompt help:
596 1 - select a numbered item
597 foo - select item based on unique prefix
598 - (empty) select nothing
599 EOF
600 }
601
602 sub prompt_help_cmd {
603 print colored $help_color, __ <<'EOF' ;
604 Prompt help:
605 1 - select a single item
606 3-5 - select a range of items
607 2-3,6-9 - select multiple ranges
608 foo - select item based on unique prefix
609 -... - unselect specified items
610 * - choose all items
611 - (empty) finish selecting
612 EOF
613 }
614
615 sub status_cmd {
616 list_and_choose({ LIST_ONLY => 1, HEADER => $status_head },
617 list_modified());
618 print "\n";
619 }
620
621 sub say_n_paths {
622 my $did = shift @_;
623 my $cnt = scalar @_;
624 if ($did eq 'added') {
625 printf(__n("added %d path\n", "added %d paths\n",
626 $cnt), $cnt);
627 } elsif ($did eq 'updated') {
628 printf(__n("updated %d path\n", "updated %d paths\n",
629 $cnt), $cnt);
630 } elsif ($did eq 'reverted') {
631 printf(__n("reverted %d path\n", "reverted %d paths\n",
632 $cnt), $cnt);
633 } else {
634 printf(__n("touched %d path\n", "touched %d paths\n",
635 $cnt), $cnt);
636 }
637 }
638
639 sub update_cmd {
640 my @mods = list_modified('file-only');
641 return if (!@mods);
642
643 my @update = list_and_choose({ PROMPT => __('Update'),
644 HEADER => $status_head, },
645 @mods);
646 if (@update) {
647 system(qw(git update-index --add --remove --),
648 map { $_->{VALUE} } @update);
649 say_n_paths('updated', @update);
650 }
651 print "\n";
652 }
653
654 sub revert_cmd {
655 my @update = list_and_choose({ PROMPT => __('Revert'),
656 HEADER => $status_head, },
657 list_modified());
658 if (@update) {
659 if (is_initial_commit()) {
660 system(qw(git rm --cached),
661 map { $_->{VALUE} } @update);
662 }
663 else {
664 my @lines = run_cmd_pipe(qw(git ls-tree HEAD --),
665 map { $_->{VALUE} } @update);
666 my $fh;
667 open $fh, '| git update-index --index-info'
668 or die;
669 for (@lines) {
670 print $fh $_;
671 }
672 close($fh);
673 for (@update) {
674 if ($_->{INDEX_ADDDEL} &&
675 $_->{INDEX_ADDDEL} eq 'create') {
676 system(qw(git update-index --force-remove --),
677 $_->{VALUE});
678 printf(__("note: %s is untracked now.\n"), $_->{VALUE});
679 }
680 }
681 }
682 refresh();
683 say_n_paths('reverted', @update);
684 }
685 print "\n";
686 }
687
688 sub add_untracked_cmd {
689 my @add = list_and_choose({ PROMPT => __('Add untracked') },
690 list_untracked());
691 if (@add) {
692 system(qw(git update-index --add --), @add);
693 say_n_paths('added', @add);
694 } else {
695 print __("No untracked files.\n");
696 }
697 print "\n";
698 }
699
700 sub run_git_apply {
701 my $cmd = shift;
702 my $fh;
703 open $fh, '| git ' . $cmd . " --allow-overlap";
704 print $fh @_;
705 return close $fh;
706 }
707
708 sub parse_diff {
709 my ($path) = @_;
710 my @diff_cmd = split(" ", $patch_mode_flavour{DIFF});
711 if (defined $diff_algorithm) {
712 splice @diff_cmd, 1, 0, "--diff-algorithm=${diff_algorithm}";
713 }
714 if (defined $patch_mode_revision) {
715 push @diff_cmd, get_diff_reference($patch_mode_revision);
716 }
717 my @diff = run_cmd_pipe("git", @diff_cmd, qw(--no-color --), $path);
718 my @colored = ();
719 if ($diff_use_color) {
720 my @display_cmd = ("git", @diff_cmd, qw(--color --), $path);
721 if (defined $diff_filter) {
722 # quotemeta is overkill, but sufficient for shell-quoting
723 my $diff = join(' ', map { quotemeta } @display_cmd);
724 @display_cmd = ("$diff | $diff_filter");
725 }
726
727 @colored = run_cmd_pipe(@display_cmd);
728 }
729 my (@hunk) = { TEXT => [], DISPLAY => [], TYPE => 'header' };
730
731 if (@colored && @colored != @diff) {
732 print STDERR
733 "fatal: mismatched output from interactive.diffFilter\n",
734 "hint: Your filter must maintain a one-to-one correspondence\n",
735 "hint: between its input and output lines.\n";
736 exit 1;
737 }
738
739 for (my $i = 0; $i < @diff; $i++) {
740 if ($diff[$i] =~ /^@@ /) {
741 push @hunk, { TEXT => [], DISPLAY => [],
742 TYPE => 'hunk' };
743 }
744 push @{$hunk[-1]{TEXT}}, $diff[$i];
745 push @{$hunk[-1]{DISPLAY}},
746 (@colored ? $colored[$i] : $diff[$i]);
747 }
748 return @hunk;
749 }
750
751 sub parse_diff_header {
752 my $src = shift;
753
754 my $head = { TEXT => [], DISPLAY => [], TYPE => 'header' };
755 my $mode = { TEXT => [], DISPLAY => [], TYPE => 'mode' };
756 my $deletion = { TEXT => [], DISPLAY => [], TYPE => 'deletion' };
757 my $addition;
758
759 for (my $i = 0; $i < @{$src->{TEXT}}; $i++) {
760 if ($src->{TEXT}->[$i] =~ /^new file/) {
761 $addition = 1;
762 $head->{TYPE} = 'addition';
763 }
764 my $dest =
765 $src->{TEXT}->[$i] =~ /^(old|new) mode (\d+)$/ ? $mode :
766 $src->{TEXT}->[$i] =~ /^deleted file/ ? $deletion :
767 $head;
768 push @{$dest->{TEXT}}, $src->{TEXT}->[$i];
769 push @{$dest->{DISPLAY}}, $src->{DISPLAY}->[$i];
770 }
771 return ($head, $mode, $deletion, $addition);
772 }
773
774 sub hunk_splittable {
775 my ($text) = @_;
776
777 my @s = split_hunk($text);
778 return (1 < @s);
779 }
780
781 sub parse_hunk_header {
782 my ($line) = @_;
783 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
784 $line =~ /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
785 $o_cnt = 1 unless defined $o_cnt;
786 $n_cnt = 1 unless defined $n_cnt;
787 return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
788 }
789
790 sub format_hunk_header {
791 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = @_;
792 return ("@@ -$o_ofs" .
793 (($o_cnt != 1) ? ",$o_cnt" : '') .
794 " +$n_ofs" .
795 (($n_cnt != 1) ? ",$n_cnt" : '') .
796 " @@\n");
797 }
798
799 sub split_hunk {
800 my ($text, $display) = @_;
801 my @split = ();
802 if (!defined $display) {
803 $display = $text;
804 }
805 # If there are context lines in the middle of a hunk,
806 # it can be split, but we would need to take care of
807 # overlaps later.
808
809 my ($o_ofs, undef, $n_ofs) = parse_hunk_header($text->[0]);
810 my $hunk_start = 1;
811
812 OUTER:
813 while (1) {
814 my $next_hunk_start = undef;
815 my $i = $hunk_start - 1;
816 my $this = +{
817 TEXT => [],
818 DISPLAY => [],
819 TYPE => 'hunk',
820 OLD => $o_ofs,
821 NEW => $n_ofs,
822 OCNT => 0,
823 NCNT => 0,
824 ADDDEL => 0,
825 POSTCTX => 0,
826 USE => undef,
827 };
828
829 while (++$i < @$text) {
830 my $line = $text->[$i];
831 my $display = $display->[$i];
832 if ($line =~ /^\\/) {
833 push @{$this->{TEXT}}, $line;
834 push @{$this->{DISPLAY}}, $display;
835 next;
836 }
837 if ($line =~ /^ /) {
838 if ($this->{ADDDEL} &&
839 !defined $next_hunk_start) {
840 # We have seen leading context and
841 # adds/dels and then here is another
842 # context, which is trailing for this
843 # split hunk and leading for the next
844 # one.
845 $next_hunk_start = $i;
846 }
847 push @{$this->{TEXT}}, $line;
848 push @{$this->{DISPLAY}}, $display;
849 $this->{OCNT}++;
850 $this->{NCNT}++;
851 if (defined $next_hunk_start) {
852 $this->{POSTCTX}++;
853 }
854 next;
855 }
856
857 # add/del
858 if (defined $next_hunk_start) {
859 # We are done with the current hunk and
860 # this is the first real change for the
861 # next split one.
862 $hunk_start = $next_hunk_start;
863 $o_ofs = $this->{OLD} + $this->{OCNT};
864 $n_ofs = $this->{NEW} + $this->{NCNT};
865 $o_ofs -= $this->{POSTCTX};
866 $n_ofs -= $this->{POSTCTX};
867 push @split, $this;
868 redo OUTER;
869 }
870 push @{$this->{TEXT}}, $line;
871 push @{$this->{DISPLAY}}, $display;
872 $this->{ADDDEL}++;
873 if ($line =~ /^-/) {
874 $this->{OCNT}++;
875 }
876 else {
877 $this->{NCNT}++;
878 }
879 }
880
881 push @split, $this;
882 last;
883 }
884
885 for my $hunk (@split) {
886 $o_ofs = $hunk->{OLD};
887 $n_ofs = $hunk->{NEW};
888 my $o_cnt = $hunk->{OCNT};
889 my $n_cnt = $hunk->{NCNT};
890
891 my $head = format_hunk_header($o_ofs, $o_cnt, $n_ofs, $n_cnt);
892 my $display_head = $head;
893 unshift @{$hunk->{TEXT}}, $head;
894 if ($diff_use_color) {
895 $display_head = colored($fraginfo_color, $head);
896 }
897 unshift @{$hunk->{DISPLAY}}, $display_head;
898 }
899 return @split;
900 }
901
902 sub find_last_o_ctx {
903 my ($it) = @_;
904 my $text = $it->{TEXT};
905 my ($o_ofs, $o_cnt) = parse_hunk_header($text->[0]);
906 my $i = @{$text};
907 my $last_o_ctx = $o_ofs + $o_cnt;
908 while (0 < --$i) {
909 my $line = $text->[$i];
910 if ($line =~ /^ /) {
911 $last_o_ctx--;
912 next;
913 }
914 last;
915 }
916 return $last_o_ctx;
917 }
918
919 sub merge_hunk {
920 my ($prev, $this) = @_;
921 my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) =
922 parse_hunk_header($prev->{TEXT}[0]);
923 my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) =
924 parse_hunk_header($this->{TEXT}[0]);
925
926 my (@line, $i, $ofs, $o_cnt, $n_cnt);
927 $ofs = $o0_ofs;
928 $o_cnt = $n_cnt = 0;
929 for ($i = 1; $i < @{$prev->{TEXT}}; $i++) {
930 my $line = $prev->{TEXT}[$i];
931 if ($line =~ /^\+/) {
932 $n_cnt++;
933 push @line, $line;
934 next;
935 } elsif ($line =~ /^\\/) {
936 push @line, $line;
937 next;
938 }
939
940 last if ($o1_ofs <= $ofs);
941
942 $o_cnt++;
943 $ofs++;
944 if ($line =~ /^ /) {
945 $n_cnt++;
946 }
947 push @line, $line;
948 }
949
950 for ($i = 1; $i < @{$this->{TEXT}}; $i++) {
951 my $line = $this->{TEXT}[$i];
952 if ($line =~ /^\+/) {
953 $n_cnt++;
954 push @line, $line;
955 next;
956 } elsif ($line =~ /^\\/) {
957 push @line, $line;
958 next;
959 }
960 $ofs++;
961 $o_cnt++;
962 if ($line =~ /^ /) {
963 $n_cnt++;
964 }
965 push @line, $line;
966 }
967 my $head = format_hunk_header($o0_ofs, $o_cnt, $n0_ofs, $n_cnt);
968 @{$prev->{TEXT}} = ($head, @line);
969 }
970
971 sub coalesce_overlapping_hunks {
972 my (@in) = @_;
973 my @out = ();
974
975 my ($last_o_ctx, $last_was_dirty);
976 my $ofs_delta = 0;
977
978 for (@in) {
979 if ($_->{TYPE} ne 'hunk') {
980 push @out, $_;
981 next;
982 }
983 my $text = $_->{TEXT};
984 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
985 parse_hunk_header($text->[0]);
986 unless ($_->{USE}) {
987 $ofs_delta += $o_cnt - $n_cnt;
988 # If this hunk has been edited then subtract
989 # the delta that is due to the edit.
990 if ($_->{OFS_DELTA}) {
991 $ofs_delta -= $_->{OFS_DELTA};
992 }
993 next;
994 }
995 if ($ofs_delta) {
996 if ($patch_mode_flavour{IS_REVERSE}) {
997 $o_ofs -= $ofs_delta;
998 } else {
999 $n_ofs += $ofs_delta;
1000 }
1001 $_->{TEXT}->[0] = format_hunk_header($o_ofs, $o_cnt,
1002 $n_ofs, $n_cnt);
1003 }
1004 # If this hunk was edited then adjust the offset delta
1005 # to reflect the edit.
1006 if ($_->{OFS_DELTA}) {
1007 $ofs_delta += $_->{OFS_DELTA};
1008 }
1009 if (defined $last_o_ctx &&
1010 $o_ofs <= $last_o_ctx &&
1011 !$_->{DIRTY} &&
1012 !$last_was_dirty) {
1013 merge_hunk($out[-1], $_);
1014 }
1015 else {
1016 push @out, $_;
1017 }
1018 $last_o_ctx = find_last_o_ctx($out[-1]);
1019 $last_was_dirty = $_->{DIRTY};
1020 }
1021 return @out;
1022 }
1023
1024 sub reassemble_patch {
1025 my $head = shift;
1026 my @patch;
1027
1028 # Include everything in the header except the beginning of the diff.
1029 push @patch, (grep { !/^[-+]{3}/ } @$head);
1030
1031 # Then include any headers from the hunk lines, which must
1032 # come before any actual hunk.
1033 while (@_ && $_[0] !~ /^@/) {
1034 push @patch, shift;
1035 }
1036
1037 # Then begin the diff.
1038 push @patch, grep { /^[-+]{3}/ } @$head;
1039
1040 # And then the actual hunks.
1041 push @patch, @_;
1042
1043 return @patch;
1044 }
1045
1046 sub color_diff {
1047 return map {
1048 colored((/^@/ ? $fraginfo_color :
1049 /^\+/ ? $diff_new_color :
1050 /^-/ ? $diff_old_color :
1051 $diff_plain_color),
1052 $_);
1053 } @_;
1054 }
1055
1056 my %edit_hunk_manually_modes = (
1057 stage => N__(
1058 "If the patch applies cleanly, the edited hunk will immediately be
1059 marked for staging."),
1060 stash => N__(
1061 "If the patch applies cleanly, the edited hunk will immediately be
1062 marked for stashing."),
1063 reset_head => N__(
1064 "If the patch applies cleanly, the edited hunk will immediately be
1065 marked for unstaging."),
1066 reset_nothead => N__(
1067 "If the patch applies cleanly, the edited hunk will immediately be
1068 marked for applying."),
1069 checkout_index => N__(
1070 "If the patch applies cleanly, the edited hunk will immediately be
1071 marked for discarding."),
1072 checkout_head => N__(
1073 "If the patch applies cleanly, the edited hunk will immediately be
1074 marked for discarding."),
1075 checkout_nothead => N__(
1076 "If the patch applies cleanly, the edited hunk will immediately be
1077 marked for applying."),
1078 worktree_head => N__(
1079 "If the patch applies cleanly, the edited hunk will immediately be
1080 marked for discarding."),
1081 worktree_nothead => N__(
1082 "If the patch applies cleanly, the edited hunk will immediately be
1083 marked for applying."),
1084 );
1085
1086 sub recount_edited_hunk {
1087 local $_;
1088 my ($oldtext, $newtext) = @_;
1089 my ($o_cnt, $n_cnt) = (0, 0);
1090 for (@{$newtext}[1..$#{$newtext}]) {
1091 my $mode = substr($_, 0, 1);
1092 if ($mode eq '-') {
1093 $o_cnt++;
1094 } elsif ($mode eq '+') {
1095 $n_cnt++;
1096 } elsif ($mode eq ' ' or $mode eq "\n") {
1097 $o_cnt++;
1098 $n_cnt++;
1099 }
1100 }
1101 my ($o_ofs, undef, $n_ofs, undef) =
1102 parse_hunk_header($newtext->[0]);
1103 $newtext->[0] = format_hunk_header($o_ofs, $o_cnt, $n_ofs, $n_cnt);
1104 my (undef, $orig_o_cnt, undef, $orig_n_cnt) =
1105 parse_hunk_header($oldtext->[0]);
1106 # Return the change in the number of lines inserted by this hunk
1107 return $orig_o_cnt - $orig_n_cnt - $o_cnt + $n_cnt;
1108 }
1109
1110 sub edit_hunk_manually {
1111 my ($oldtext) = @_;
1112
1113 my $hunkfile = $repo->repo_path . "/addp-hunk-edit.diff";
1114 my $fh;
1115 open $fh, '>', $hunkfile
1116 or die sprintf(__("failed to open hunk edit file for writing: %s"), $!);
1117 print $fh Git::comment_lines __("Manual hunk edit mode -- see bottom for a quick guide.\n");
1118 print $fh @$oldtext;
1119 my $is_reverse = $patch_mode_flavour{IS_REVERSE};
1120 my ($remove_plus, $remove_minus) = $is_reverse ? ('-', '+') : ('+', '-');
1121 my $comment_line_char = Git::get_comment_line_char;
1122 print $fh Git::comment_lines sprintf(__ <<EOF, $remove_minus, $remove_plus, $comment_line_char),
1123 ---
1124 To remove '%s' lines, make them ' ' lines (context).
1125 To remove '%s' lines, delete them.
1126 Lines starting with %s will be removed.
1127 EOF
1128 __($edit_hunk_manually_modes{$patch_mode}),
1129 # TRANSLATORS: 'it' refers to the patch mentioned in the previous messages.
1130 __ <<EOF2 ;
1131 If it does not apply cleanly, you will be given an opportunity to
1132 edit again. If all lines of the hunk are removed, then the edit is
1133 aborted and the hunk is left unchanged.
1134 EOF2
1135 close $fh;
1136
1137 chomp(my ($editor) = run_cmd_pipe(qw(git var GIT_EDITOR)));
1138 system('sh', '-c', $editor.' "$@"', $editor, $hunkfile);
1139
1140 if ($? != 0) {
1141 return undef;
1142 }
1143
1144 open $fh, '<', $hunkfile
1145 or die sprintf(__("failed to open hunk edit file for reading: %s"), $!);
1146 my @newtext = grep { !/^\Q$comment_line_char\E/ } <$fh>;
1147 close $fh;
1148 unlink $hunkfile;
1149
1150 # Abort if nothing remains
1151 if (!grep { /\S/ } @newtext) {
1152 return undef;
1153 }
1154
1155 # Reinsert the first hunk header if the user accidentally deleted it
1156 if ($newtext[0] !~ /^@/) {
1157 unshift @newtext, $oldtext->[0];
1158 }
1159 return \@newtext;
1160 }
1161
1162 sub diff_applies {
1163 return run_git_apply($patch_mode_flavour{APPLY_CHECK} . ' --check',
1164 map { @{$_->{TEXT}} } @_);
1165 }
1166
1167 sub _restore_terminal_and_die {
1168 ReadMode 'restore';
1169 print "\n";
1170 exit 1;
1171 }
1172
1173 sub prompt_single_character {
1174 if ($use_readkey) {
1175 local $SIG{TERM} = \&_restore_terminal_and_die;
1176 local $SIG{INT} = \&_restore_terminal_and_die;
1177 ReadMode 'cbreak';
1178 my $key = ReadKey 0;
1179 ReadMode 'restore';
1180 if ($use_termcap and $key eq "\e") {
1181 while (!defined $term_escapes{$key}) {
1182 my $next = ReadKey 0.5;
1183 last if (!defined $next);
1184 $key .= $next;
1185 }
1186 $key =~ s/\e/^[/;
1187 }
1188 print "$key" if defined $key;
1189 print "\n";
1190 return $key;
1191 } else {
1192 return <STDIN>;
1193 }
1194 }
1195
1196 sub prompt_yesno {
1197 my ($prompt) = @_;
1198 while (1) {
1199 print colored $prompt_color, $prompt;
1200 my $line = prompt_single_character;
1201 return undef unless defined $line;
1202 return 0 if $line =~ /^n/i;
1203 return 1 if $line =~ /^y/i;
1204 }
1205 }
1206
1207 sub edit_hunk_loop {
1208 my ($head, $hunks, $ix) = @_;
1209 my $hunk = $hunks->[$ix];
1210 my $text = $hunk->{TEXT};
1211
1212 while (1) {
1213 my $newtext = edit_hunk_manually($text);
1214 if (!defined $newtext) {
1215 return undef;
1216 }
1217 my $newhunk = {
1218 TEXT => $newtext,
1219 TYPE => $hunk->{TYPE},
1220 USE => 1,
1221 DIRTY => 1,
1222 };
1223 $newhunk->{OFS_DELTA} = recount_edited_hunk($text, $newtext);
1224 # If this hunk has already been edited then add the
1225 # offset delta of the previous edit to get the real
1226 # delta from the original unedited hunk.
1227 $hunk->{OFS_DELTA} and
1228 $newhunk->{OFS_DELTA} += $hunk->{OFS_DELTA};
1229 if (diff_applies($head,
1230 @{$hunks}[0..$ix-1],
1231 $newhunk,
1232 @{$hunks}[$ix+1..$#{$hunks}])) {
1233 $newhunk->{DISPLAY} = [color_diff(@{$newtext})];
1234 return $newhunk;
1235 }
1236 else {
1237 prompt_yesno(
1238 # TRANSLATORS: do not translate [y/n]
1239 # The program will only accept that input
1240 # at this point.
1241 # Consider translating (saying "no" discards!) as
1242 # (saying "n" for "no" discards!) if the translation
1243 # of the word "no" does not start with n.
1244 __('Your edited hunk does not apply. Edit again '
1245 . '(saying "no" discards!) [y/n]? ')
1246 ) or return undef;
1247 }
1248 }
1249 }
1250
1251 my %help_patch_modes = (
1252 stage => N__(
1253 "y - stage this hunk
1254 n - do not stage this hunk
1255 q - quit; do not stage this hunk or any of the remaining ones
1256 a - stage this hunk and all later hunks in the file
1257 d - do not stage this hunk or any of the later hunks in the file"),
1258 stash => N__(
1259 "y - stash this hunk
1260 n - do not stash this hunk
1261 q - quit; do not stash this hunk or any of the remaining ones
1262 a - stash this hunk and all later hunks in the file
1263 d - do not stash this hunk or any of the later hunks in the file"),
1264 reset_head => N__(
1265 "y - unstage this hunk
1266 n - do not unstage this hunk
1267 q - quit; do not unstage this hunk or any of the remaining ones
1268 a - unstage this hunk and all later hunks in the file
1269 d - do not unstage this hunk or any of the later hunks in the file"),
1270 reset_nothead => N__(
1271 "y - apply this hunk to index
1272 n - do not apply this hunk to index
1273 q - quit; do not apply this hunk or any of the remaining ones
1274 a - apply this hunk and all later hunks in the file
1275 d - do not apply this hunk or any of the later hunks in the file"),
1276 checkout_index => N__(
1277 "y - discard this hunk from worktree
1278 n - do not discard this hunk from worktree
1279 q - quit; do not discard this hunk or any of the remaining ones
1280 a - discard this hunk and all later hunks in the file
1281 d - do not discard this hunk or any of the later hunks in the file"),
1282 checkout_head => N__(
1283 "y - discard this hunk from index and worktree
1284 n - do not discard this hunk from index and worktree
1285 q - quit; do not discard this hunk or any of the remaining ones
1286 a - discard this hunk and all later hunks in the file
1287 d - do not discard this hunk or any of the later hunks in the file"),
1288 checkout_nothead => N__(
1289 "y - apply this hunk to index and worktree
1290 n - do not apply this hunk to index and worktree
1291 q - quit; do not apply this hunk or any of the remaining ones
1292 a - apply this hunk and all later hunks in the file
1293 d - do not apply this hunk or any of the later hunks in the file"),
1294 worktree_head => N__(
1295 "y - discard this hunk from worktree
1296 n - do not discard this hunk from worktree
1297 q - quit; do not discard this hunk or any of the remaining ones
1298 a - discard this hunk and all later hunks in the file
1299 d - do not discard this hunk or any of the later hunks in the file"),
1300 worktree_nothead => N__(
1301 "y - apply this hunk to worktree
1302 n - do not apply this hunk to worktree
1303 q - quit; do not apply this hunk or any of the remaining ones
1304 a - apply this hunk and all later hunks in the file
1305 d - do not apply this hunk or any of the later hunks in the file"),
1306 );
1307
1308 sub help_patch_cmd {
1309 local $_;
1310 my $other = $_[0] . ",?";
1311 print colored $help_color, __($help_patch_modes{$patch_mode}), "\n",
1312 map { "$_\n" } grep {
1313 my $c = quotemeta(substr($_, 0, 1));
1314 $other =~ /,$c/
1315 } split "\n", __ <<EOF ;
1316 g - select a hunk to go to
1317 / - search for a hunk matching the given regex
1318 j - leave this hunk undecided, see next undecided hunk
1319 J - leave this hunk undecided, see next hunk
1320 k - leave this hunk undecided, see previous undecided hunk
1321 K - leave this hunk undecided, see previous hunk
1322 s - split the current hunk into smaller hunks
1323 e - manually edit the current hunk
1324 ? - print help
1325 EOF
1326 }
1327
1328 sub apply_patch {
1329 my $cmd = shift;
1330 my $ret = run_git_apply $cmd, @_;
1331 if (!$ret) {
1332 print STDERR @_;
1333 }
1334 return $ret;
1335 }
1336
1337 sub apply_patch_for_checkout_commit {
1338 my $reverse = shift;
1339 my $applies_index = run_git_apply 'apply '.$reverse.' --cached --check', @_;
1340 my $applies_worktree = run_git_apply 'apply '.$reverse.' --check', @_;
1341
1342 if ($applies_worktree && $applies_index) {
1343 run_git_apply 'apply '.$reverse.' --cached', @_;
1344 run_git_apply 'apply '.$reverse, @_;
1345 return 1;
1346 } elsif (!$applies_index) {
1347 print colored $error_color, __("The selected hunks do not apply to the index!\n");
1348 if (prompt_yesno __("Apply them to the worktree anyway? ")) {
1349 return run_git_apply 'apply '.$reverse, @_;
1350 } else {
1351 print colored $error_color, __("Nothing was applied.\n");
1352 return 0;
1353 }
1354 } else {
1355 print STDERR @_;
1356 return 0;
1357 }
1358 }
1359
1360 sub patch_update_cmd {
1361 my @all_mods = list_modified($patch_mode_flavour{FILTER});
1362 error_msg sprintf(__("ignoring unmerged: %s\n"), $_->{VALUE})
1363 for grep { $_->{UNMERGED} } @all_mods;
1364 @all_mods = grep { !$_->{UNMERGED} } @all_mods;
1365
1366 my @mods = grep { !($_->{BINARY}) } @all_mods;
1367 my @them;
1368
1369 if (!@mods) {
1370 if (@all_mods) {
1371 print STDERR __("Only binary files changed.\n");
1372 } else {
1373 print STDERR __("No changes.\n");
1374 }
1375 return 0;
1376 }
1377 if ($patch_mode_only) {
1378 @them = @mods;
1379 }
1380 else {
1381 @them = list_and_choose({ PROMPT => __('Patch update'),
1382 HEADER => $status_head, },
1383 @mods);
1384 }
1385 for (@them) {
1386 return 0 if patch_update_file($_->{VALUE});
1387 }
1388 }
1389
1390 # Generate a one line summary of a hunk.
1391 sub summarize_hunk {
1392 my $rhunk = shift;
1393 my $summary = $rhunk->{TEXT}[0];
1394
1395 # Keep the line numbers, discard extra context.
1396 $summary =~ s/@@(.*?)@@.*/$1 /s;
1397 $summary .= " " x (20 - length $summary);
1398
1399 # Add some user context.
1400 for my $line (@{$rhunk->{TEXT}}) {
1401 if ($line =~ m/^[+-].*\w/) {
1402 $summary .= $line;
1403 last;
1404 }
1405 }
1406
1407 chomp $summary;
1408 return substr($summary, 0, 80) . "\n";
1409 }
1410
1411
1412 # Print a one-line summary of each hunk in the array ref in
1413 # the first argument, starting with the index in the 2nd.
1414 sub display_hunks {
1415 my ($hunks, $i) = @_;
1416 my $ctr = 0;
1417 $i ||= 0;
1418 for (; $i < @$hunks && $ctr < 20; $i++, $ctr++) {
1419 my $status = " ";
1420 if (defined $hunks->[$i]{USE}) {
1421 $status = $hunks->[$i]{USE} ? "+" : "-";
1422 }
1423 printf "%s%2d: %s",
1424 $status,
1425 $i + 1,
1426 summarize_hunk($hunks->[$i]);
1427 }
1428 return $i;
1429 }
1430
1431 my %patch_update_prompt_modes = (
1432 stage => {
1433 mode => N__("Stage mode change [y,n,q,a,d%s,?]? "),
1434 deletion => N__("Stage deletion [y,n,q,a,d%s,?]? "),
1435 addition => N__("Stage addition [y,n,q,a,d%s,?]? "),
1436 hunk => N__("Stage this hunk [y,n,q,a,d%s,?]? "),
1437 },
1438 stash => {
1439 mode => N__("Stash mode change [y,n,q,a,d%s,?]? "),
1440 deletion => N__("Stash deletion [y,n,q,a,d%s,?]? "),
1441 addition => N__("Stash addition [y,n,q,a,d%s,?]? "),
1442 hunk => N__("Stash this hunk [y,n,q,a,d%s,?]? "),
1443 },
1444 reset_head => {
1445 mode => N__("Unstage mode change [y,n,q,a,d%s,?]? "),
1446 deletion => N__("Unstage deletion [y,n,q,a,d%s,?]? "),
1447 addition => N__("Unstage addition [y,n,q,a,d%s,?]? "),
1448 hunk => N__("Unstage this hunk [y,n,q,a,d%s,?]? "),
1449 },
1450 reset_nothead => {
1451 mode => N__("Apply mode change to index [y,n,q,a,d%s,?]? "),
1452 deletion => N__("Apply deletion to index [y,n,q,a,d%s,?]? "),
1453 addition => N__("Apply addition to index [y,n,q,a,d%s,?]? "),
1454 hunk => N__("Apply this hunk to index [y,n,q,a,d%s,?]? "),
1455 },
1456 checkout_index => {
1457 mode => N__("Discard mode change from worktree [y,n,q,a,d%s,?]? "),
1458 deletion => N__("Discard deletion from worktree [y,n,q,a,d%s,?]? "),
1459 addition => N__("Discard addition from worktree [y,n,q,a,d%s,?]? "),
1460 hunk => N__("Discard this hunk from worktree [y,n,q,a,d%s,?]? "),
1461 },
1462 checkout_head => {
1463 mode => N__("Discard mode change from index and worktree [y,n,q,a,d%s,?]? "),
1464 deletion => N__("Discard deletion from index and worktree [y,n,q,a,d%s,?]? "),
1465 addition => N__("Discard addition from index and worktree [y,n,q,a,d%s,?]? "),
1466 hunk => N__("Discard this hunk from index and worktree [y,n,q,a,d%s,?]? "),
1467 },
1468 checkout_nothead => {
1469 mode => N__("Apply mode change to index and worktree [y,n,q,a,d%s,?]? "),
1470 deletion => N__("Apply deletion to index and worktree [y,n,q,a,d%s,?]? "),
1471 addition => N__("Apply addition to index and worktree [y,n,q,a,d%s,?]? "),
1472 hunk => N__("Apply this hunk to index and worktree [y,n,q,a,d%s,?]? "),
1473 },
1474 worktree_head => {
1475 mode => N__("Discard mode change from worktree [y,n,q,a,d%s,?]? "),
1476 deletion => N__("Discard deletion from worktree [y,n,q,a,d%s,?]? "),
1477 addition => N__("Discard addition from worktree [y,n,q,a,d%s,?]? "),
1478 hunk => N__("Discard this hunk from worktree [y,n,q,a,d%s,?]? "),
1479 },
1480 worktree_nothead => {
1481 mode => N__("Apply mode change to worktree [y,n,q,a,d%s,?]? "),
1482 deletion => N__("Apply deletion to worktree [y,n,q,a,d%s,?]? "),
1483 addition => N__("Apply addition to worktree [y,n,q,a,d%s,?]? "),
1484 hunk => N__("Apply this hunk to worktree [y,n,q,a,d%s,?]? "),
1485 },
1486 );
1487
1488 sub patch_update_file {
1489 my $quit = 0;
1490 my ($ix, $num);
1491 my $path = shift;
1492 my ($head, @hunk) = parse_diff($path);
1493 ($head, my $mode, my $deletion, my $addition) = parse_diff_header($head);
1494 for (@{$head->{DISPLAY}}) {
1495 print;
1496 }
1497
1498 if (@{$mode->{TEXT}}) {
1499 unshift @hunk, $mode;
1500 }
1501 if (@{$deletion->{TEXT}}) {
1502 foreach my $hunk (@hunk) {
1503 push @{$deletion->{TEXT}}, @{$hunk->{TEXT}};
1504 push @{$deletion->{DISPLAY}}, @{$hunk->{DISPLAY}};
1505 }
1506 @hunk = ($deletion);
1507 }
1508
1509 $num = scalar @hunk;
1510 $ix = 0;
1511
1512 while (1) {
1513 my ($prev, $next, $other, $undecided, $i);
1514 $other = '';
1515
1516 last if ($ix and !$num);
1517 if ($num <= $ix) {
1518 $ix = 0;
1519 }
1520 for ($i = 0; $i < $ix; $i++) {
1521 if (!defined $hunk[$i]{USE}) {
1522 $prev = 1;
1523 $other .= ',k';
1524 last;
1525 }
1526 }
1527 if ($ix) {
1528 $other .= ',K';
1529 }
1530 for ($i = $ix + 1; $i < $num; $i++) {
1531 if (!defined $hunk[$i]{USE}) {
1532 $next = 1;
1533 $other .= ',j';
1534 last;
1535 }
1536 }
1537 if ($ix < $num - 1) {
1538 $other .= ',J';
1539 }
1540 if ($num > 1) {
1541 $other .= ',g,/';
1542 }
1543 for ($i = 0; $i < $num; $i++) {
1544 if (!defined $hunk[$i]{USE}) {
1545 $undecided = 1;
1546 last;
1547 }
1548 }
1549 last if (!$undecided && ($num || !$addition));
1550
1551 if ($num) {
1552 if ($hunk[$ix]{TYPE} eq 'hunk' &&
1553 hunk_splittable($hunk[$ix]{TEXT})) {
1554 $other .= ',s';
1555 }
1556 if ($hunk[$ix]{TYPE} eq 'hunk') {
1557 $other .= ',e';
1558 }
1559 for (@{$hunk[$ix]{DISPLAY}}) {
1560 print;
1561 }
1562 }
1563 my $type = $num ? $hunk[$ix]{TYPE} : $head->{TYPE};
1564 print colored $prompt_color, "(", ($ix+1), "/", ($num ? $num : 1), ") ",
1565 sprintf(__($patch_update_prompt_modes{$patch_mode}{$type}), $other);
1566
1567 my $line = prompt_single_character;
1568 last unless defined $line;
1569 if ($line) {
1570 if ($line =~ /^y/i) {
1571 if ($num) {
1572 $hunk[$ix]{USE} = 1;
1573 } else {
1574 $head->{USE} = 1;
1575 }
1576 }
1577 elsif ($line =~ /^n/i) {
1578 if ($num) {
1579 $hunk[$ix]{USE} = 0;
1580 } else {
1581 $head->{USE} = 0;
1582 }
1583 }
1584 elsif ($line =~ /^a/i) {
1585 if ($num) {
1586 while ($ix < $num) {
1587 if (!defined $hunk[$ix]{USE}) {
1588 $hunk[$ix]{USE} = 1;
1589 }
1590 $ix++;
1591 }
1592 } else {
1593 $head->{USE} = 1;
1594 $ix++;
1595 }
1596 next;
1597 }
1598 elsif ($line =~ /^g(.*)/) {
1599 my $response = $1;
1600 unless ($other =~ /g/) {
1601 error_msg __("No other hunks to goto\n");
1602 next;
1603 }
1604 my $no = $ix > 10 ? $ix - 10 : 0;
1605 while ($response eq '') {
1606 $no = display_hunks(\@hunk, $no);
1607 if ($no < $num) {
1608 print __("go to which hunk (<ret> to see more)? ");
1609 } else {
1610 print __("go to which hunk? ");
1611 }
1612 $response = <STDIN>;
1613 if (!defined $response) {
1614 $response = '';
1615 }
1616 chomp $response;
1617 }
1618 if ($response !~ /^\s*\d+\s*$/) {
1619 error_msg sprintf(__("Invalid number: '%s'\n"),
1620 $response);
1621 } elsif (0 < $response && $response <= $num) {
1622 $ix = $response - 1;
1623 } else {
1624 error_msg sprintf(__n("Sorry, only %d hunk available.\n",
1625 "Sorry, only %d hunks available.\n", $num), $num);
1626 }
1627 next;
1628 }
1629 elsif ($line =~ /^d/i) {
1630 if ($num) {
1631 while ($ix < $num) {
1632 if (!defined $hunk[$ix]{USE}) {
1633 $hunk[$ix]{USE} = 0;
1634 }
1635 $ix++;
1636 }
1637 } else {
1638 $head->{USE} = 0;
1639 $ix++;
1640 }
1641 next;
1642 }
1643 elsif ($line =~ /^q/i) {
1644 if ($num) {
1645 for ($i = 0; $i < $num; $i++) {
1646 if (!defined $hunk[$i]{USE}) {
1647 $hunk[$i]{USE} = 0;
1648 }
1649 }
1650 } elsif (!defined $head->{USE}) {
1651 $head->{USE} = 0;
1652 }
1653 $quit = 1;
1654 last;
1655 }
1656 elsif ($line =~ m|^/(.*)|) {
1657 my $regex = $1;
1658 unless ($other =~ m|/|) {
1659 error_msg __("No other hunks to search\n");
1660 next;
1661 }
1662 if ($regex eq "") {
1663 print colored $prompt_color, __("search for regex? ");
1664 $regex = <STDIN>;
1665 if (defined $regex) {
1666 chomp $regex;
1667 }
1668 }
1669 my $search_string;
1670 eval {
1671 $search_string = qr{$regex}m;
1672 };
1673 if ($@) {
1674 my ($err,$exp) = ($@, $1);
1675 $err =~ s/ at .*git-add--interactive line \d+, <STDIN> line \d+.*$//;
1676 error_msg sprintf(__("Malformed search regexp %s: %s\n"), $exp, $err);
1677 next;
1678 }
1679 my $iy = $ix;
1680 while (1) {
1681 my $text = join ("", @{$hunk[$iy]{TEXT}});
1682 last if ($text =~ $search_string);
1683 $iy++;
1684 $iy = 0 if ($iy >= $num);
1685 if ($ix == $iy) {
1686 error_msg __("No hunk matches the given pattern\n");
1687 last;
1688 }
1689 }
1690 $ix = $iy;
1691 next;
1692 }
1693 elsif ($line =~ /^K/) {
1694 if ($other =~ /K/) {
1695 $ix--;
1696 }
1697 else {
1698 error_msg __("No previous hunk\n");
1699 }
1700 next;
1701 }
1702 elsif ($line =~ /^J/) {
1703 if ($other =~ /J/) {
1704 $ix++;
1705 }
1706 else {
1707 error_msg __("No next hunk\n");
1708 }
1709 next;
1710 }
1711 elsif ($line =~ /^k/) {
1712 if ($other =~ /k/) {
1713 while (1) {
1714 $ix--;
1715 last if (!$ix ||
1716 !defined $hunk[$ix]{USE});
1717 }
1718 }
1719 else {
1720 error_msg __("No previous hunk\n");
1721 }
1722 next;
1723 }
1724 elsif ($line =~ /^j/) {
1725 if ($other !~ /j/) {
1726 error_msg __("No next hunk\n");
1727 next;
1728 }
1729 }
1730 elsif ($line =~ /^s/) {
1731 unless ($other =~ /s/) {
1732 error_msg __("Sorry, cannot split this hunk\n");
1733 next;
1734 }
1735 my @split = split_hunk($hunk[$ix]{TEXT}, $hunk[$ix]{DISPLAY});
1736 if (1 < @split) {
1737 print colored $header_color, sprintf(
1738 __n("Split into %d hunk.\n",
1739 "Split into %d hunks.\n",
1740 scalar(@split)), scalar(@split));
1741 }
1742 splice (@hunk, $ix, 1, @split);
1743 $num = scalar @hunk;
1744 next;
1745 }
1746 elsif ($line =~ /^e/) {
1747 unless ($other =~ /e/) {
1748 error_msg __("Sorry, cannot edit this hunk\n");
1749 next;
1750 }
1751 my $newhunk = edit_hunk_loop($head, \@hunk, $ix);
1752 if (defined $newhunk) {
1753 splice @hunk, $ix, 1, $newhunk;
1754 }
1755 }
1756 else {
1757 help_patch_cmd($other);
1758 next;
1759 }
1760 # soft increment
1761 while (1) {
1762 $ix++;
1763 last if ($ix >= $num ||
1764 !defined $hunk[$ix]{USE});
1765 }
1766 }
1767 }
1768
1769 @hunk = coalesce_overlapping_hunks(@hunk) if ($num);
1770
1771 my $n_lofs = 0;
1772 my @result = ();
1773 for (@hunk) {
1774 if ($_->{USE}) {
1775 push @result, @{$_->{TEXT}};
1776 }
1777 }
1778
1779 if (@result or $head->{USE}) {
1780 my @patch = reassemble_patch($head->{TEXT}, @result);
1781 my $apply_routine = $patch_mode_flavour{APPLY};
1782 &$apply_routine(@patch);
1783 refresh();
1784 }
1785
1786 print "\n";
1787 return $quit;
1788 }
1789
1790 sub diff_cmd {
1791 my @mods = list_modified('index-only');
1792 @mods = grep { !($_->{BINARY}) } @mods;
1793 return if (!@mods);
1794 my (@them) = list_and_choose({ PROMPT => __('Review diff'),
1795 IMMEDIATE => 1,
1796 HEADER => $status_head, },
1797 @mods);
1798 return if (!@them);
1799 my $reference = (is_initial_commit()) ? get_empty_tree() : 'HEAD';
1800 system(qw(git diff -p --cached), $reference, '--',
1801 map { $_->{VALUE} } @them);
1802 }
1803
1804 sub quit_cmd {
1805 print __("Bye.\n");
1806 exit(0);
1807 }
1808
1809 sub help_cmd {
1810 # TRANSLATORS: please do not translate the command names
1811 # 'status', 'update', 'revert', etc.
1812 print colored $help_color, __ <<'EOF' ;
1813 status - show paths with changes
1814 update - add working tree state to the staged set of changes
1815 revert - revert staged set of changes back to the HEAD version
1816 patch - pick hunks and update selectively
1817 diff - view diff between HEAD and index
1818 add untracked - add contents of untracked files to the staged set of changes
1819 EOF
1820 }
1821
1822 sub process_args {
1823 return unless @ARGV;
1824 my $arg = shift @ARGV;
1825 if ($arg =~ /--patch(?:=(.*))?/) {
1826 if (defined $1) {
1827 if ($1 eq 'reset') {
1828 $patch_mode = 'reset_head';
1829 $patch_mode_revision = 'HEAD';
1830 $arg = shift @ARGV or die __("missing --");
1831 if ($arg ne '--') {
1832 $patch_mode_revision = $arg;
1833
1834 # NEEDSWORK: Instead of comparing to the literal "HEAD",
1835 # compare the commit objects instead so that other ways of
1836 # saying the same thing (such as "@") are also handled
1837 # appropriately.
1838 #
1839 # This applies to the cases below too.
1840 $patch_mode = ($arg eq 'HEAD' ?
1841 'reset_head' : 'reset_nothead');
1842 $arg = shift @ARGV or die __("missing --");
1843 }
1844 } elsif ($1 eq 'checkout') {
1845 $arg = shift @ARGV or die __("missing --");
1846 if ($arg eq '--') {
1847 $patch_mode = 'checkout_index';
1848 } else {
1849 $patch_mode_revision = $arg;
1850 $patch_mode = ($arg eq 'HEAD' ?
1851 'checkout_head' : 'checkout_nothead');
1852 $arg = shift @ARGV or die __("missing --");
1853 }
1854 } elsif ($1 eq 'worktree') {
1855 $arg = shift @ARGV or die __("missing --");
1856 if ($arg eq '--') {
1857 $patch_mode = 'checkout_index';
1858 } else {
1859 $patch_mode_revision = $arg;
1860 $patch_mode = ($arg eq 'HEAD' ?
1861 'worktree_head' : 'worktree_nothead');
1862 $arg = shift @ARGV or die __("missing --");
1863 }
1864 } elsif ($1 eq 'stage' or $1 eq 'stash') {
1865 $patch_mode = $1;
1866 $arg = shift @ARGV or die __("missing --");
1867 } else {
1868 die sprintf(__("unknown --patch mode: %s"), $1);
1869 }
1870 } else {
1871 $patch_mode = 'stage';
1872 $arg = shift @ARGV or die __("missing --");
1873 }
1874 die sprintf(__("invalid argument %s, expecting --"),
1875 $arg) unless $arg eq "--";
1876 %patch_mode_flavour = %{$patch_modes{$patch_mode}};
1877 $patch_mode_only = 1;
1878 }
1879 elsif ($arg ne "--") {
1880 die sprintf(__("invalid argument %s, expecting --"), $arg);
1881 }
1882 }
1883
1884 sub main_loop {
1885 my @cmd = ([ 'status', \&status_cmd, ],
1886 [ 'update', \&update_cmd, ],
1887 [ 'revert', \&revert_cmd, ],
1888 [ 'add untracked', \&add_untracked_cmd, ],
1889 [ 'patch', \&patch_update_cmd, ],
1890 [ 'diff', \&diff_cmd, ],
1891 [ 'quit', \&quit_cmd, ],
1892 [ 'help', \&help_cmd, ],
1893 );
1894 while (1) {
1895 my ($it) = list_and_choose({ PROMPT => __('What now'),
1896 SINGLETON => 1,
1897 LIST_FLAT => 4,
1898 HEADER => __('*** Commands ***'),
1899 ON_EOF => \&quit_cmd,
1900 IMMEDIATE => 1 }, @cmd);
1901 if ($it) {
1902 eval {
1903 $it->[1]->();
1904 };
1905 if ($@) {
1906 print "$@";
1907 }
1908 }
1909 }
1910 }
1911
1912 process_args();
1913 refresh();
1914 if ($patch_mode_only) {
1915 patch_update_cmd();
1916 }
1917 else {
1918 status_cmd();
1919 main_loop();
1920 }