]> git.ipfire.org Git - thirdparty/git.git/blob - git-add--interactive.perl
The twentieth batch
[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, "--", $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
758 for (my $i = 0; $i < @{$src->{TEXT}}; $i++) {
759 my $dest =
760 $src->{TEXT}->[$i] =~ /^(old|new) mode (\d+)$/ ? $mode :
761 $src->{TEXT}->[$i] =~ /^deleted file/ ? $deletion :
762 $head;
763 push @{$dest->{TEXT}}, $src->{TEXT}->[$i];
764 push @{$dest->{DISPLAY}}, $src->{DISPLAY}->[$i];
765 }
766 return ($head, $mode, $deletion);
767 }
768
769 sub hunk_splittable {
770 my ($text) = @_;
771
772 my @s = split_hunk($text);
773 return (1 < @s);
774 }
775
776 sub parse_hunk_header {
777 my ($line) = @_;
778 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
779 $line =~ /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
780 $o_cnt = 1 unless defined $o_cnt;
781 $n_cnt = 1 unless defined $n_cnt;
782 return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
783 }
784
785 sub format_hunk_header {
786 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = @_;
787 return ("@@ -$o_ofs" .
788 (($o_cnt != 1) ? ",$o_cnt" : '') .
789 " +$n_ofs" .
790 (($n_cnt != 1) ? ",$n_cnt" : '') .
791 " @@\n");
792 }
793
794 sub split_hunk {
795 my ($text, $display) = @_;
796 my @split = ();
797 if (!defined $display) {
798 $display = $text;
799 }
800 # If there are context lines in the middle of a hunk,
801 # it can be split, but we would need to take care of
802 # overlaps later.
803
804 my ($o_ofs, undef, $n_ofs) = parse_hunk_header($text->[0]);
805 my $hunk_start = 1;
806
807 OUTER:
808 while (1) {
809 my $next_hunk_start = undef;
810 my $i = $hunk_start - 1;
811 my $this = +{
812 TEXT => [],
813 DISPLAY => [],
814 TYPE => 'hunk',
815 OLD => $o_ofs,
816 NEW => $n_ofs,
817 OCNT => 0,
818 NCNT => 0,
819 ADDDEL => 0,
820 POSTCTX => 0,
821 USE => undef,
822 };
823
824 while (++$i < @$text) {
825 my $line = $text->[$i];
826 my $display = $display->[$i];
827 if ($line =~ /^\\/) {
828 push @{$this->{TEXT}}, $line;
829 push @{$this->{DISPLAY}}, $display;
830 next;
831 }
832 if ($line =~ /^ /) {
833 if ($this->{ADDDEL} &&
834 !defined $next_hunk_start) {
835 # We have seen leading context and
836 # adds/dels and then here is another
837 # context, which is trailing for this
838 # split hunk and leading for the next
839 # one.
840 $next_hunk_start = $i;
841 }
842 push @{$this->{TEXT}}, $line;
843 push @{$this->{DISPLAY}}, $display;
844 $this->{OCNT}++;
845 $this->{NCNT}++;
846 if (defined $next_hunk_start) {
847 $this->{POSTCTX}++;
848 }
849 next;
850 }
851
852 # add/del
853 if (defined $next_hunk_start) {
854 # We are done with the current hunk and
855 # this is the first real change for the
856 # next split one.
857 $hunk_start = $next_hunk_start;
858 $o_ofs = $this->{OLD} + $this->{OCNT};
859 $n_ofs = $this->{NEW} + $this->{NCNT};
860 $o_ofs -= $this->{POSTCTX};
861 $n_ofs -= $this->{POSTCTX};
862 push @split, $this;
863 redo OUTER;
864 }
865 push @{$this->{TEXT}}, $line;
866 push @{$this->{DISPLAY}}, $display;
867 $this->{ADDDEL}++;
868 if ($line =~ /^-/) {
869 $this->{OCNT}++;
870 }
871 else {
872 $this->{NCNT}++;
873 }
874 }
875
876 push @split, $this;
877 last;
878 }
879
880 for my $hunk (@split) {
881 $o_ofs = $hunk->{OLD};
882 $n_ofs = $hunk->{NEW};
883 my $o_cnt = $hunk->{OCNT};
884 my $n_cnt = $hunk->{NCNT};
885
886 my $head = format_hunk_header($o_ofs, $o_cnt, $n_ofs, $n_cnt);
887 my $display_head = $head;
888 unshift @{$hunk->{TEXT}}, $head;
889 if ($diff_use_color) {
890 $display_head = colored($fraginfo_color, $head);
891 }
892 unshift @{$hunk->{DISPLAY}}, $display_head;
893 }
894 return @split;
895 }
896
897 sub find_last_o_ctx {
898 my ($it) = @_;
899 my $text = $it->{TEXT};
900 my ($o_ofs, $o_cnt) = parse_hunk_header($text->[0]);
901 my $i = @{$text};
902 my $last_o_ctx = $o_ofs + $o_cnt;
903 while (0 < --$i) {
904 my $line = $text->[$i];
905 if ($line =~ /^ /) {
906 $last_o_ctx--;
907 next;
908 }
909 last;
910 }
911 return $last_o_ctx;
912 }
913
914 sub merge_hunk {
915 my ($prev, $this) = @_;
916 my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) =
917 parse_hunk_header($prev->{TEXT}[0]);
918 my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) =
919 parse_hunk_header($this->{TEXT}[0]);
920
921 my (@line, $i, $ofs, $o_cnt, $n_cnt);
922 $ofs = $o0_ofs;
923 $o_cnt = $n_cnt = 0;
924 for ($i = 1; $i < @{$prev->{TEXT}}; $i++) {
925 my $line = $prev->{TEXT}[$i];
926 if ($line =~ /^\+/) {
927 $n_cnt++;
928 push @line, $line;
929 next;
930 } elsif ($line =~ /^\\/) {
931 push @line, $line;
932 next;
933 }
934
935 last if ($o1_ofs <= $ofs);
936
937 $o_cnt++;
938 $ofs++;
939 if ($line =~ /^ /) {
940 $n_cnt++;
941 }
942 push @line, $line;
943 }
944
945 for ($i = 1; $i < @{$this->{TEXT}}; $i++) {
946 my $line = $this->{TEXT}[$i];
947 if ($line =~ /^\+/) {
948 $n_cnt++;
949 push @line, $line;
950 next;
951 } elsif ($line =~ /^\\/) {
952 push @line, $line;
953 next;
954 }
955 $ofs++;
956 $o_cnt++;
957 if ($line =~ /^ /) {
958 $n_cnt++;
959 }
960 push @line, $line;
961 }
962 my $head = format_hunk_header($o0_ofs, $o_cnt, $n0_ofs, $n_cnt);
963 @{$prev->{TEXT}} = ($head, @line);
964 }
965
966 sub coalesce_overlapping_hunks {
967 my (@in) = @_;
968 my @out = ();
969
970 my ($last_o_ctx, $last_was_dirty);
971 my $ofs_delta = 0;
972
973 for (@in) {
974 if ($_->{TYPE} ne 'hunk') {
975 push @out, $_;
976 next;
977 }
978 my $text = $_->{TEXT};
979 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
980 parse_hunk_header($text->[0]);
981 unless ($_->{USE}) {
982 $ofs_delta += $o_cnt - $n_cnt;
983 # If this hunk has been edited then subtract
984 # the delta that is due to the edit.
985 if ($_->{OFS_DELTA}) {
986 $ofs_delta -= $_->{OFS_DELTA};
987 }
988 next;
989 }
990 if ($ofs_delta) {
991 if ($patch_mode_flavour{IS_REVERSE}) {
992 $o_ofs -= $ofs_delta;
993 } else {
994 $n_ofs += $ofs_delta;
995 }
996 $_->{TEXT}->[0] = format_hunk_header($o_ofs, $o_cnt,
997 $n_ofs, $n_cnt);
998 }
999 # If this hunk was edited then adjust the offset delta
1000 # to reflect the edit.
1001 if ($_->{OFS_DELTA}) {
1002 $ofs_delta += $_->{OFS_DELTA};
1003 }
1004 if (defined $last_o_ctx &&
1005 $o_ofs <= $last_o_ctx &&
1006 !$_->{DIRTY} &&
1007 !$last_was_dirty) {
1008 merge_hunk($out[-1], $_);
1009 }
1010 else {
1011 push @out, $_;
1012 }
1013 $last_o_ctx = find_last_o_ctx($out[-1]);
1014 $last_was_dirty = $_->{DIRTY};
1015 }
1016 return @out;
1017 }
1018
1019 sub reassemble_patch {
1020 my $head = shift;
1021 my @patch;
1022
1023 # Include everything in the header except the beginning of the diff.
1024 push @patch, (grep { !/^[-+]{3}/ } @$head);
1025
1026 # Then include any headers from the hunk lines, which must
1027 # come before any actual hunk.
1028 while (@_ && $_[0] !~ /^@/) {
1029 push @patch, shift;
1030 }
1031
1032 # Then begin the diff.
1033 push @patch, grep { /^[-+]{3}/ } @$head;
1034
1035 # And then the actual hunks.
1036 push @patch, @_;
1037
1038 return @patch;
1039 }
1040
1041 sub color_diff {
1042 return map {
1043 colored((/^@/ ? $fraginfo_color :
1044 /^\+/ ? $diff_new_color :
1045 /^-/ ? $diff_old_color :
1046 $diff_plain_color),
1047 $_);
1048 } @_;
1049 }
1050
1051 my %edit_hunk_manually_modes = (
1052 stage => N__(
1053 "If the patch applies cleanly, the edited hunk will immediately be
1054 marked for staging."),
1055 stash => N__(
1056 "If the patch applies cleanly, the edited hunk will immediately be
1057 marked for stashing."),
1058 reset_head => N__(
1059 "If the patch applies cleanly, the edited hunk will immediately be
1060 marked for unstaging."),
1061 reset_nothead => N__(
1062 "If the patch applies cleanly, the edited hunk will immediately be
1063 marked for applying."),
1064 checkout_index => N__(
1065 "If the patch applies cleanly, the edited hunk will immediately be
1066 marked for discarding."),
1067 checkout_head => N__(
1068 "If the patch applies cleanly, the edited hunk will immediately be
1069 marked for discarding."),
1070 checkout_nothead => N__(
1071 "If the patch applies cleanly, the edited hunk will immediately be
1072 marked for applying."),
1073 worktree_head => N__(
1074 "If the patch applies cleanly, the edited hunk will immediately be
1075 marked for discarding."),
1076 worktree_nothead => N__(
1077 "If the patch applies cleanly, the edited hunk will immediately be
1078 marked for applying."),
1079 );
1080
1081 sub recount_edited_hunk {
1082 local $_;
1083 my ($oldtext, $newtext) = @_;
1084 my ($o_cnt, $n_cnt) = (0, 0);
1085 for (@{$newtext}[1..$#{$newtext}]) {
1086 my $mode = substr($_, 0, 1);
1087 if ($mode eq '-') {
1088 $o_cnt++;
1089 } elsif ($mode eq '+') {
1090 $n_cnt++;
1091 } elsif ($mode eq ' ' or $mode eq "\n") {
1092 $o_cnt++;
1093 $n_cnt++;
1094 }
1095 }
1096 my ($o_ofs, undef, $n_ofs, undef) =
1097 parse_hunk_header($newtext->[0]);
1098 $newtext->[0] = format_hunk_header($o_ofs, $o_cnt, $n_ofs, $n_cnt);
1099 my (undef, $orig_o_cnt, undef, $orig_n_cnt) =
1100 parse_hunk_header($oldtext->[0]);
1101 # Return the change in the number of lines inserted by this hunk
1102 return $orig_o_cnt - $orig_n_cnt - $o_cnt + $n_cnt;
1103 }
1104
1105 sub edit_hunk_manually {
1106 my ($oldtext) = @_;
1107
1108 my $hunkfile = $repo->repo_path . "/addp-hunk-edit.diff";
1109 my $fh;
1110 open $fh, '>', $hunkfile
1111 or die sprintf(__("failed to open hunk edit file for writing: %s"), $!);
1112 print $fh Git::comment_lines __("Manual hunk edit mode -- see bottom for a quick guide.\n");
1113 print $fh @$oldtext;
1114 my $is_reverse = $patch_mode_flavour{IS_REVERSE};
1115 my ($remove_plus, $remove_minus) = $is_reverse ? ('-', '+') : ('+', '-');
1116 my $comment_line_char = Git::get_comment_line_char;
1117 print $fh Git::comment_lines sprintf(__ <<EOF, $remove_minus, $remove_plus, $comment_line_char),
1118 ---
1119 To remove '%s' lines, make them ' ' lines (context).
1120 To remove '%s' lines, delete them.
1121 Lines starting with %s will be removed.
1122 EOF
1123 __($edit_hunk_manually_modes{$patch_mode}),
1124 # TRANSLATORS: 'it' refers to the patch mentioned in the previous messages.
1125 __ <<EOF2 ;
1126 If it does not apply cleanly, you will be given an opportunity to
1127 edit again. If all lines of the hunk are removed, then the edit is
1128 aborted and the hunk is left unchanged.
1129 EOF2
1130 close $fh;
1131
1132 chomp(my ($editor) = run_cmd_pipe(qw(git var GIT_EDITOR)));
1133 system('sh', '-c', $editor.' "$@"', $editor, $hunkfile);
1134
1135 if ($? != 0) {
1136 return undef;
1137 }
1138
1139 open $fh, '<', $hunkfile
1140 or die sprintf(__("failed to open hunk edit file for reading: %s"), $!);
1141 my @newtext = grep { !/^\Q$comment_line_char\E/ } <$fh>;
1142 close $fh;
1143 unlink $hunkfile;
1144
1145 # Abort if nothing remains
1146 if (!grep { /\S/ } @newtext) {
1147 return undef;
1148 }
1149
1150 # Reinsert the first hunk header if the user accidentally deleted it
1151 if ($newtext[0] !~ /^@/) {
1152 unshift @newtext, $oldtext->[0];
1153 }
1154 return \@newtext;
1155 }
1156
1157 sub diff_applies {
1158 return run_git_apply($patch_mode_flavour{APPLY_CHECK} . ' --check',
1159 map { @{$_->{TEXT}} } @_);
1160 }
1161
1162 sub _restore_terminal_and_die {
1163 ReadMode 'restore';
1164 print "\n";
1165 exit 1;
1166 }
1167
1168 sub prompt_single_character {
1169 if ($use_readkey) {
1170 local $SIG{TERM} = \&_restore_terminal_and_die;
1171 local $SIG{INT} = \&_restore_terminal_and_die;
1172 ReadMode 'cbreak';
1173 my $key = ReadKey 0;
1174 ReadMode 'restore';
1175 if ($use_termcap and $key eq "\e") {
1176 while (!defined $term_escapes{$key}) {
1177 my $next = ReadKey 0.5;
1178 last if (!defined $next);
1179 $key .= $next;
1180 }
1181 $key =~ s/\e/^[/;
1182 }
1183 print "$key" if defined $key;
1184 print "\n";
1185 return $key;
1186 } else {
1187 return <STDIN>;
1188 }
1189 }
1190
1191 sub prompt_yesno {
1192 my ($prompt) = @_;
1193 while (1) {
1194 print colored $prompt_color, $prompt;
1195 my $line = prompt_single_character;
1196 return undef unless defined $line;
1197 return 0 if $line =~ /^n/i;
1198 return 1 if $line =~ /^y/i;
1199 }
1200 }
1201
1202 sub edit_hunk_loop {
1203 my ($head, $hunks, $ix) = @_;
1204 my $hunk = $hunks->[$ix];
1205 my $text = $hunk->{TEXT};
1206
1207 while (1) {
1208 my $newtext = edit_hunk_manually($text);
1209 if (!defined $newtext) {
1210 return undef;
1211 }
1212 my $newhunk = {
1213 TEXT => $newtext,
1214 TYPE => $hunk->{TYPE},
1215 USE => 1,
1216 DIRTY => 1,
1217 };
1218 $newhunk->{OFS_DELTA} = recount_edited_hunk($text, $newtext);
1219 # If this hunk has already been edited then add the
1220 # offset delta of the previous edit to get the real
1221 # delta from the original unedited hunk.
1222 $hunk->{OFS_DELTA} and
1223 $newhunk->{OFS_DELTA} += $hunk->{OFS_DELTA};
1224 if (diff_applies($head,
1225 @{$hunks}[0..$ix-1],
1226 $newhunk,
1227 @{$hunks}[$ix+1..$#{$hunks}])) {
1228 $newhunk->{DISPLAY} = [color_diff(@{$newtext})];
1229 return $newhunk;
1230 }
1231 else {
1232 prompt_yesno(
1233 # TRANSLATORS: do not translate [y/n]
1234 # The program will only accept that input
1235 # at this point.
1236 # Consider translating (saying "no" discards!) as
1237 # (saying "n" for "no" discards!) if the translation
1238 # of the word "no" does not start with n.
1239 __('Your edited hunk does not apply. Edit again '
1240 . '(saying "no" discards!) [y/n]? ')
1241 ) or return undef;
1242 }
1243 }
1244 }
1245
1246 my %help_patch_modes = (
1247 stage => N__(
1248 "y - stage this hunk
1249 n - do not stage this hunk
1250 q - quit; do not stage this hunk or any of the remaining ones
1251 a - stage this hunk and all later hunks in the file
1252 d - do not stage this hunk or any of the later hunks in the file"),
1253 stash => N__(
1254 "y - stash this hunk
1255 n - do not stash this hunk
1256 q - quit; do not stash this hunk or any of the remaining ones
1257 a - stash this hunk and all later hunks in the file
1258 d - do not stash this hunk or any of the later hunks in the file"),
1259 reset_head => N__(
1260 "y - unstage this hunk
1261 n - do not unstage this hunk
1262 q - quit; do not unstage this hunk or any of the remaining ones
1263 a - unstage this hunk and all later hunks in the file
1264 d - do not unstage this hunk or any of the later hunks in the file"),
1265 reset_nothead => N__(
1266 "y - apply this hunk to index
1267 n - do not apply this hunk to index
1268 q - quit; do not apply this hunk or any of the remaining ones
1269 a - apply this hunk and all later hunks in the file
1270 d - do not apply this hunk or any of the later hunks in the file"),
1271 checkout_index => N__(
1272 "y - discard this hunk from worktree
1273 n - do not discard this hunk from worktree
1274 q - quit; do not discard this hunk or any of the remaining ones
1275 a - discard this hunk and all later hunks in the file
1276 d - do not discard this hunk or any of the later hunks in the file"),
1277 checkout_head => N__(
1278 "y - discard this hunk from index and worktree
1279 n - do not discard this hunk from index and worktree
1280 q - quit; do not discard this hunk or any of the remaining ones
1281 a - discard this hunk and all later hunks in the file
1282 d - do not discard this hunk or any of the later hunks in the file"),
1283 checkout_nothead => N__(
1284 "y - apply this hunk to index and worktree
1285 n - do not apply this hunk to index and worktree
1286 q - quit; do not apply this hunk or any of the remaining ones
1287 a - apply this hunk and all later hunks in the file
1288 d - do not apply this hunk or any of the later hunks in the file"),
1289 worktree_head => N__(
1290 "y - discard this hunk from worktree
1291 n - do not discard this hunk from worktree
1292 q - quit; do not discard this hunk or any of the remaining ones
1293 a - discard this hunk and all later hunks in the file
1294 d - do not discard this hunk or any of the later hunks in the file"),
1295 worktree_nothead => N__(
1296 "y - apply this hunk to worktree
1297 n - do not apply this hunk to worktree
1298 q - quit; do not apply this hunk or any of the remaining ones
1299 a - apply this hunk and all later hunks in the file
1300 d - do not apply this hunk or any of the later hunks in the file"),
1301 );
1302
1303 sub help_patch_cmd {
1304 local $_;
1305 my $other = $_[0] . ",?";
1306 print colored $help_color, __($help_patch_modes{$patch_mode}), "\n",
1307 map { "$_\n" } grep {
1308 my $c = quotemeta(substr($_, 0, 1));
1309 $other =~ /,$c/
1310 } split "\n", __ <<EOF ;
1311 g - select a hunk to go to
1312 / - search for a hunk matching the given regex
1313 j - leave this hunk undecided, see next undecided hunk
1314 J - leave this hunk undecided, see next hunk
1315 k - leave this hunk undecided, see previous undecided hunk
1316 K - leave this hunk undecided, see previous hunk
1317 s - split the current hunk into smaller hunks
1318 e - manually edit the current hunk
1319 ? - print help
1320 EOF
1321 }
1322
1323 sub apply_patch {
1324 my $cmd = shift;
1325 my $ret = run_git_apply $cmd, @_;
1326 if (!$ret) {
1327 print STDERR @_;
1328 }
1329 return $ret;
1330 }
1331
1332 sub apply_patch_for_checkout_commit {
1333 my $reverse = shift;
1334 my $applies_index = run_git_apply 'apply '.$reverse.' --cached --check', @_;
1335 my $applies_worktree = run_git_apply 'apply '.$reverse.' --check', @_;
1336
1337 if ($applies_worktree && $applies_index) {
1338 run_git_apply 'apply '.$reverse.' --cached', @_;
1339 run_git_apply 'apply '.$reverse, @_;
1340 return 1;
1341 } elsif (!$applies_index) {
1342 print colored $error_color, __("The selected hunks do not apply to the index!\n");
1343 if (prompt_yesno __("Apply them to the worktree anyway? ")) {
1344 return run_git_apply 'apply '.$reverse, @_;
1345 } else {
1346 print colored $error_color, __("Nothing was applied.\n");
1347 return 0;
1348 }
1349 } else {
1350 print STDERR @_;
1351 return 0;
1352 }
1353 }
1354
1355 sub patch_update_cmd {
1356 my @all_mods = list_modified($patch_mode_flavour{FILTER});
1357 error_msg sprintf(__("ignoring unmerged: %s\n"), $_->{VALUE})
1358 for grep { $_->{UNMERGED} } @all_mods;
1359 @all_mods = grep { !$_->{UNMERGED} } @all_mods;
1360
1361 my @mods = grep { !($_->{BINARY}) } @all_mods;
1362 my @them;
1363
1364 if (!@mods) {
1365 if (@all_mods) {
1366 print STDERR __("Only binary files changed.\n");
1367 } else {
1368 print STDERR __("No changes.\n");
1369 }
1370 return 0;
1371 }
1372 if ($patch_mode_only) {
1373 @them = @mods;
1374 }
1375 else {
1376 @them = list_and_choose({ PROMPT => __('Patch update'),
1377 HEADER => $status_head, },
1378 @mods);
1379 }
1380 for (@them) {
1381 return 0 if patch_update_file($_->{VALUE});
1382 }
1383 }
1384
1385 # Generate a one line summary of a hunk.
1386 sub summarize_hunk {
1387 my $rhunk = shift;
1388 my $summary = $rhunk->{TEXT}[0];
1389
1390 # Keep the line numbers, discard extra context.
1391 $summary =~ s/@@(.*?)@@.*/$1 /s;
1392 $summary .= " " x (20 - length $summary);
1393
1394 # Add some user context.
1395 for my $line (@{$rhunk->{TEXT}}) {
1396 if ($line =~ m/^[+-].*\w/) {
1397 $summary .= $line;
1398 last;
1399 }
1400 }
1401
1402 chomp $summary;
1403 return substr($summary, 0, 80) . "\n";
1404 }
1405
1406
1407 # Print a one-line summary of each hunk in the array ref in
1408 # the first argument, starting with the index in the 2nd.
1409 sub display_hunks {
1410 my ($hunks, $i) = @_;
1411 my $ctr = 0;
1412 $i ||= 0;
1413 for (; $i < @$hunks && $ctr < 20; $i++, $ctr++) {
1414 my $status = " ";
1415 if (defined $hunks->[$i]{USE}) {
1416 $status = $hunks->[$i]{USE} ? "+" : "-";
1417 }
1418 printf "%s%2d: %s",
1419 $status,
1420 $i + 1,
1421 summarize_hunk($hunks->[$i]);
1422 }
1423 return $i;
1424 }
1425
1426 my %patch_update_prompt_modes = (
1427 stage => {
1428 mode => N__("Stage mode change [y,n,q,a,d%s,?]? "),
1429 deletion => N__("Stage deletion [y,n,q,a,d%s,?]? "),
1430 hunk => N__("Stage this hunk [y,n,q,a,d%s,?]? "),
1431 },
1432 stash => {
1433 mode => N__("Stash mode change [y,n,q,a,d%s,?]? "),
1434 deletion => N__("Stash deletion [y,n,q,a,d%s,?]? "),
1435 hunk => N__("Stash this hunk [y,n,q,a,d%s,?]? "),
1436 },
1437 reset_head => {
1438 mode => N__("Unstage mode change [y,n,q,a,d%s,?]? "),
1439 deletion => N__("Unstage deletion [y,n,q,a,d%s,?]? "),
1440 hunk => N__("Unstage this hunk [y,n,q,a,d%s,?]? "),
1441 },
1442 reset_nothead => {
1443 mode => N__("Apply mode change to index [y,n,q,a,d%s,?]? "),
1444 deletion => N__("Apply deletion to index [y,n,q,a,d%s,?]? "),
1445 hunk => N__("Apply this hunk to index [y,n,q,a,d%s,?]? "),
1446 },
1447 checkout_index => {
1448 mode => N__("Discard mode change from worktree [y,n,q,a,d%s,?]? "),
1449 deletion => N__("Discard deletion from worktree [y,n,q,a,d%s,?]? "),
1450 hunk => N__("Discard this hunk from worktree [y,n,q,a,d%s,?]? "),
1451 },
1452 checkout_head => {
1453 mode => N__("Discard mode change from index and worktree [y,n,q,a,d%s,?]? "),
1454 deletion => N__("Discard deletion from index and worktree [y,n,q,a,d%s,?]? "),
1455 hunk => N__("Discard this hunk from index and worktree [y,n,q,a,d%s,?]? "),
1456 },
1457 checkout_nothead => {
1458 mode => N__("Apply mode change to index and worktree [y,n,q,a,d%s,?]? "),
1459 deletion => N__("Apply deletion to index and worktree [y,n,q,a,d%s,?]? "),
1460 hunk => N__("Apply this hunk to index and worktree [y,n,q,a,d%s,?]? "),
1461 },
1462 worktree_head => {
1463 mode => N__("Discard mode change from worktree [y,n,q,a,d%s,?]? "),
1464 deletion => N__("Discard deletion from worktree [y,n,q,a,d%s,?]? "),
1465 hunk => N__("Discard this hunk from worktree [y,n,q,a,d%s,?]? "),
1466 },
1467 worktree_nothead => {
1468 mode => N__("Apply mode change to worktree [y,n,q,a,d%s,?]? "),
1469 deletion => N__("Apply deletion to worktree [y,n,q,a,d%s,?]? "),
1470 hunk => N__("Apply this hunk to worktree [y,n,q,a,d%s,?]? "),
1471 },
1472 );
1473
1474 sub patch_update_file {
1475 my $quit = 0;
1476 my ($ix, $num);
1477 my $path = shift;
1478 my ($head, @hunk) = parse_diff($path);
1479 ($head, my $mode, my $deletion) = parse_diff_header($head);
1480 for (@{$head->{DISPLAY}}) {
1481 print;
1482 }
1483
1484 if (@{$mode->{TEXT}}) {
1485 unshift @hunk, $mode;
1486 }
1487 if (@{$deletion->{TEXT}}) {
1488 foreach my $hunk (@hunk) {
1489 push @{$deletion->{TEXT}}, @{$hunk->{TEXT}};
1490 push @{$deletion->{DISPLAY}}, @{$hunk->{DISPLAY}};
1491 }
1492 @hunk = ($deletion);
1493 }
1494
1495 $num = scalar @hunk;
1496 $ix = 0;
1497
1498 while (1) {
1499 my ($prev, $next, $other, $undecided, $i);
1500 $other = '';
1501
1502 if ($num <= $ix) {
1503 $ix = 0;
1504 }
1505 for ($i = 0; $i < $ix; $i++) {
1506 if (!defined $hunk[$i]{USE}) {
1507 $prev = 1;
1508 $other .= ',k';
1509 last;
1510 }
1511 }
1512 if ($ix) {
1513 $other .= ',K';
1514 }
1515 for ($i = $ix + 1; $i < $num; $i++) {
1516 if (!defined $hunk[$i]{USE}) {
1517 $next = 1;
1518 $other .= ',j';
1519 last;
1520 }
1521 }
1522 if ($ix < $num - 1) {
1523 $other .= ',J';
1524 }
1525 if ($num > 1) {
1526 $other .= ',g,/';
1527 }
1528 for ($i = 0; $i < $num; $i++) {
1529 if (!defined $hunk[$i]{USE}) {
1530 $undecided = 1;
1531 last;
1532 }
1533 }
1534 last if (!$undecided);
1535
1536 if ($hunk[$ix]{TYPE} eq 'hunk' &&
1537 hunk_splittable($hunk[$ix]{TEXT})) {
1538 $other .= ',s';
1539 }
1540 if ($hunk[$ix]{TYPE} eq 'hunk') {
1541 $other .= ',e';
1542 }
1543 for (@{$hunk[$ix]{DISPLAY}}) {
1544 print;
1545 }
1546 print colored $prompt_color, "(", ($ix+1), "/$num) ",
1547 sprintf(__($patch_update_prompt_modes{$patch_mode}{$hunk[$ix]{TYPE}}), $other);
1548
1549 my $line = prompt_single_character;
1550 last unless defined $line;
1551 if ($line) {
1552 if ($line =~ /^y/i) {
1553 $hunk[$ix]{USE} = 1;
1554 }
1555 elsif ($line =~ /^n/i) {
1556 $hunk[$ix]{USE} = 0;
1557 }
1558 elsif ($line =~ /^a/i) {
1559 while ($ix < $num) {
1560 if (!defined $hunk[$ix]{USE}) {
1561 $hunk[$ix]{USE} = 1;
1562 }
1563 $ix++;
1564 }
1565 next;
1566 }
1567 elsif ($line =~ /^g(.*)/) {
1568 my $response = $1;
1569 unless ($other =~ /g/) {
1570 error_msg __("No other hunks to goto\n");
1571 next;
1572 }
1573 my $no = $ix > 10 ? $ix - 10 : 0;
1574 while ($response eq '') {
1575 $no = display_hunks(\@hunk, $no);
1576 if ($no < $num) {
1577 print __("go to which hunk (<ret> to see more)? ");
1578 } else {
1579 print __("go to which hunk? ");
1580 }
1581 $response = <STDIN>;
1582 if (!defined $response) {
1583 $response = '';
1584 }
1585 chomp $response;
1586 }
1587 if ($response !~ /^\s*\d+\s*$/) {
1588 error_msg sprintf(__("Invalid number: '%s'\n"),
1589 $response);
1590 } elsif (0 < $response && $response <= $num) {
1591 $ix = $response - 1;
1592 } else {
1593 error_msg sprintf(__n("Sorry, only %d hunk available.\n",
1594 "Sorry, only %d hunks available.\n", $num), $num);
1595 }
1596 next;
1597 }
1598 elsif ($line =~ /^d/i) {
1599 while ($ix < $num) {
1600 if (!defined $hunk[$ix]{USE}) {
1601 $hunk[$ix]{USE} = 0;
1602 }
1603 $ix++;
1604 }
1605 next;
1606 }
1607 elsif ($line =~ /^q/i) {
1608 for ($i = 0; $i < $num; $i++) {
1609 if (!defined $hunk[$i]{USE}) {
1610 $hunk[$i]{USE} = 0;
1611 }
1612 }
1613 $quit = 1;
1614 last;
1615 }
1616 elsif ($line =~ m|^/(.*)|) {
1617 my $regex = $1;
1618 unless ($other =~ m|/|) {
1619 error_msg __("No other hunks to search\n");
1620 next;
1621 }
1622 if ($regex eq "") {
1623 print colored $prompt_color, __("search for regex? ");
1624 $regex = <STDIN>;
1625 if (defined $regex) {
1626 chomp $regex;
1627 }
1628 }
1629 my $search_string;
1630 eval {
1631 $search_string = qr{$regex}m;
1632 };
1633 if ($@) {
1634 my ($err,$exp) = ($@, $1);
1635 $err =~ s/ at .*git-add--interactive line \d+, <STDIN> line \d+.*$//;
1636 error_msg sprintf(__("Malformed search regexp %s: %s\n"), $exp, $err);
1637 next;
1638 }
1639 my $iy = $ix;
1640 while (1) {
1641 my $text = join ("", @{$hunk[$iy]{TEXT}});
1642 last if ($text =~ $search_string);
1643 $iy++;
1644 $iy = 0 if ($iy >= $num);
1645 if ($ix == $iy) {
1646 error_msg __("No hunk matches the given pattern\n");
1647 last;
1648 }
1649 }
1650 $ix = $iy;
1651 next;
1652 }
1653 elsif ($line =~ /^K/) {
1654 if ($other =~ /K/) {
1655 $ix--;
1656 }
1657 else {
1658 error_msg __("No previous hunk\n");
1659 }
1660 next;
1661 }
1662 elsif ($line =~ /^J/) {
1663 if ($other =~ /J/) {
1664 $ix++;
1665 }
1666 else {
1667 error_msg __("No next hunk\n");
1668 }
1669 next;
1670 }
1671 elsif ($line =~ /^k/) {
1672 if ($other =~ /k/) {
1673 while (1) {
1674 $ix--;
1675 last if (!$ix ||
1676 !defined $hunk[$ix]{USE});
1677 }
1678 }
1679 else {
1680 error_msg __("No previous hunk\n");
1681 }
1682 next;
1683 }
1684 elsif ($line =~ /^j/) {
1685 if ($other !~ /j/) {
1686 error_msg __("No next hunk\n");
1687 next;
1688 }
1689 }
1690 elsif ($line =~ /^s/) {
1691 unless ($other =~ /s/) {
1692 error_msg __("Sorry, cannot split this hunk\n");
1693 next;
1694 }
1695 my @split = split_hunk($hunk[$ix]{TEXT}, $hunk[$ix]{DISPLAY});
1696 if (1 < @split) {
1697 print colored $header_color, sprintf(
1698 __n("Split into %d hunk.\n",
1699 "Split into %d hunks.\n",
1700 scalar(@split)), scalar(@split));
1701 }
1702 splice (@hunk, $ix, 1, @split);
1703 $num = scalar @hunk;
1704 next;
1705 }
1706 elsif ($line =~ /^e/) {
1707 unless ($other =~ /e/) {
1708 error_msg __("Sorry, cannot edit this hunk\n");
1709 next;
1710 }
1711 my $newhunk = edit_hunk_loop($head, \@hunk, $ix);
1712 if (defined $newhunk) {
1713 splice @hunk, $ix, 1, $newhunk;
1714 }
1715 }
1716 else {
1717 help_patch_cmd($other);
1718 next;
1719 }
1720 # soft increment
1721 while (1) {
1722 $ix++;
1723 last if ($ix >= $num ||
1724 !defined $hunk[$ix]{USE});
1725 }
1726 }
1727 }
1728
1729 @hunk = coalesce_overlapping_hunks(@hunk);
1730
1731 my $n_lofs = 0;
1732 my @result = ();
1733 for (@hunk) {
1734 if ($_->{USE}) {
1735 push @result, @{$_->{TEXT}};
1736 }
1737 }
1738
1739 if (@result) {
1740 my @patch = reassemble_patch($head->{TEXT}, @result);
1741 my $apply_routine = $patch_mode_flavour{APPLY};
1742 &$apply_routine(@patch);
1743 refresh();
1744 }
1745
1746 print "\n";
1747 return $quit;
1748 }
1749
1750 sub diff_cmd {
1751 my @mods = list_modified('index-only');
1752 @mods = grep { !($_->{BINARY}) } @mods;
1753 return if (!@mods);
1754 my (@them) = list_and_choose({ PROMPT => __('Review diff'),
1755 IMMEDIATE => 1,
1756 HEADER => $status_head, },
1757 @mods);
1758 return if (!@them);
1759 my $reference = (is_initial_commit()) ? get_empty_tree() : 'HEAD';
1760 system(qw(git diff -p --cached), $reference, '--',
1761 map { $_->{VALUE} } @them);
1762 }
1763
1764 sub quit_cmd {
1765 print __("Bye.\n");
1766 exit(0);
1767 }
1768
1769 sub help_cmd {
1770 # TRANSLATORS: please do not translate the command names
1771 # 'status', 'update', 'revert', etc.
1772 print colored $help_color, __ <<'EOF' ;
1773 status - show paths with changes
1774 update - add working tree state to the staged set of changes
1775 revert - revert staged set of changes back to the HEAD version
1776 patch - pick hunks and update selectively
1777 diff - view diff between HEAD and index
1778 add untracked - add contents of untracked files to the staged set of changes
1779 EOF
1780 }
1781
1782 sub process_args {
1783 return unless @ARGV;
1784 my $arg = shift @ARGV;
1785 if ($arg =~ /--patch(?:=(.*))?/) {
1786 if (defined $1) {
1787 if ($1 eq 'reset') {
1788 $patch_mode = 'reset_head';
1789 $patch_mode_revision = 'HEAD';
1790 $arg = shift @ARGV or die __("missing --");
1791 if ($arg ne '--') {
1792 $patch_mode_revision = $arg;
1793 $patch_mode = ($arg eq 'HEAD' ?
1794 'reset_head' : 'reset_nothead');
1795 $arg = shift @ARGV or die __("missing --");
1796 }
1797 } elsif ($1 eq 'checkout') {
1798 $arg = shift @ARGV or die __("missing --");
1799 if ($arg eq '--') {
1800 $patch_mode = 'checkout_index';
1801 } else {
1802 $patch_mode_revision = $arg;
1803 $patch_mode = ($arg eq 'HEAD' ?
1804 'checkout_head' : 'checkout_nothead');
1805 $arg = shift @ARGV or die __("missing --");
1806 }
1807 } elsif ($1 eq 'worktree') {
1808 $arg = shift @ARGV or die __("missing --");
1809 if ($arg eq '--') {
1810 $patch_mode = 'checkout_index';
1811 } else {
1812 $patch_mode_revision = $arg;
1813 $patch_mode = ($arg eq 'HEAD' ?
1814 'worktree_head' : 'worktree_nothead');
1815 $arg = shift @ARGV or die __("missing --");
1816 }
1817 } elsif ($1 eq 'stage' or $1 eq 'stash') {
1818 $patch_mode = $1;
1819 $arg = shift @ARGV or die __("missing --");
1820 } else {
1821 die sprintf(__("unknown --patch mode: %s"), $1);
1822 }
1823 } else {
1824 $patch_mode = 'stage';
1825 $arg = shift @ARGV or die __("missing --");
1826 }
1827 die sprintf(__("invalid argument %s, expecting --"),
1828 $arg) unless $arg eq "--";
1829 %patch_mode_flavour = %{$patch_modes{$patch_mode}};
1830 $patch_mode_only = 1;
1831 }
1832 elsif ($arg ne "--") {
1833 die sprintf(__("invalid argument %s, expecting --"), $arg);
1834 }
1835 }
1836
1837 sub main_loop {
1838 my @cmd = ([ 'status', \&status_cmd, ],
1839 [ 'update', \&update_cmd, ],
1840 [ 'revert', \&revert_cmd, ],
1841 [ 'add untracked', \&add_untracked_cmd, ],
1842 [ 'patch', \&patch_update_cmd, ],
1843 [ 'diff', \&diff_cmd, ],
1844 [ 'quit', \&quit_cmd, ],
1845 [ 'help', \&help_cmd, ],
1846 );
1847 while (1) {
1848 my ($it) = list_and_choose({ PROMPT => __('What now'),
1849 SINGLETON => 1,
1850 LIST_FLAT => 4,
1851 HEADER => __('*** Commands ***'),
1852 ON_EOF => \&quit_cmd,
1853 IMMEDIATE => 1 }, @cmd);
1854 if ($it) {
1855 eval {
1856 $it->[1]->();
1857 };
1858 if ($@) {
1859 print "$@";
1860 }
1861 }
1862 }
1863 }
1864
1865 process_args();
1866 refresh();
1867 if ($patch_mode_only) {
1868 patch_update_cmd();
1869 }
1870 else {
1871 status_cmd();
1872 main_loop();
1873 }