First batch post 2.28
[git.git] / git-add--interactive.perl
blobf36c0078ac9a71758a7e1cab701d3a633b66c0d3
1 #!/usr/bin/perl
3 use 5.008;
4 use strict;
5 use warnings;
6 use Git qw(unquote_path);
7 use Git::I18N;
9 binmode(STDOUT, ":raw");
11 my $repo = Git->repository();
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);
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 ) : ();
46 my $normal_color = $repo->get_color("", "reset");
48 my $diff_algorithm = $repo->config('diff.algorithm');
49 my $diff_filter = $repo->config('interactive.difffilter');
51 my $use_readkey = 0;
52 my $use_termcap = 0;
53 my %term_escapes;
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;
63 if (!$use_readkey) {
64 print STDERR "missing Term::ReadKey, disabling interactive.singlekey\n";
66 eval {
67 require Term::Cap;
68 my $termcap = Term::Cap->Tgetent;
69 foreach (values %$termcap) {
70 $term_escapes{$_} = 1 if /^\e/;
72 $use_termcap = 1;
76 sub colored {
77 my $color = shift;
78 my $string = join("", @_);
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$/;
90 return $string;
93 # command line options
94 my $patch_mode_only;
95 my $patch_mode;
96 my $patch_mode_revision;
98 sub apply_patch;
99 sub apply_patch_for_checkout_commit;
100 sub apply_patch_for_stash;
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,
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,
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,
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,
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,
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,
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,
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,
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,
168 $patch_mode = 'stage';
169 my %patch_mode_flavour = %{$patch_modes{$patch_mode}};
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;
186 my ($GIT_DIR) = run_cmd_pipe(qw(git rev-parse --git-dir));
188 if (!defined $GIT_DIR) {
189 exit(1); # rev-parse would have already said "not a git repo"
191 chomp($GIT_DIR);
193 sub refresh {
194 my $fh;
195 open $fh, 'git update-index --refresh |'
196 or die;
197 while (<$fh>) {
198 ;# ignore 'needs update'
200 close $fh;
203 sub list_untracked {
204 map {
205 chomp $_;
206 unquote_path($_);
208 run_cmd_pipe(qw(git ls-files --others --exclude-standard --), @ARGV);
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'));
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;
225 my $empty_tree;
226 sub get_empty_tree {
227 return $empty_tree if defined $empty_tree;
229 ($empty_tree) = run_cmd_pipe(qw(git hash-object -t tree /dev/null));
230 chomp $empty_tree;
231 return $empty_tree;
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';
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
255 sub list_modified {
256 my ($only) = @_;
257 my (%data, @return);
258 my ($add, $del, $adddel, $file);
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;
272 else {
273 $change = "+$add/-$del";
275 $data{$file} = {
276 INDEX => $change,
277 BINARY => $bin,
278 FILE => __('nothing'),
281 elsif (($adddel, $file) =
282 /^ (create|delete) mode [0-7]+ (.*)$/) {
283 $file = unquote_path($file);
284 $data{$file}{INDEX_ADDDEL} = $adddel;
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;
297 else {
298 $change = "+$add/-$del";
300 $data{$file}{FILE} = $change;
301 if ($bin) {
302 $data{$file}{BINARY} = 1;
305 elsif (($adddel, $file) =
306 /^ (create|delete) mode [0-7]+ (.*)$/) {
307 $file = unquote_path($file);
308 $data{$file}{FILE_ADDDEL} = $adddel;
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,
318 if ($1 eq 'U') {
319 $data{$file}{UNMERGED} = 1;
324 for (sort keys %data) {
325 my $it = $data{$_};
327 if ($only) {
328 if ($only eq 'index-only') {
329 next if ($it->{INDEX} eq __('unchanged'));
331 if ($only eq 'file-only') {
332 next if ($it->{FILE} eq __('nothing'));
335 push @return, +{
336 VALUE => $_,
337 %$it,
340 return @return;
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];
353 else {
354 $it = $it->{VALUE};
357 eval {
358 if ($it =~ /^$string/) {
359 $hit = 1;
362 if (defined $hit && defined $found) {
363 return undef;
365 if ($hit) {
366 $found = $i + 1;
369 return $found;
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}++;
381 # returns an array of tuples (prefix, remainder)
382 sub find_unique_prefixes {
383 my @stuff = @_;
384 my @return = ();
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;
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];
398 elsif ((ref $print) eq 'HASH') {
399 $print = $print->{VALUE};
401 update_trie(\%trie, $print);
402 push @return, $print;
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;
419 else {
420 my $prefix = substr $ret, 0, $j;
421 return ()
422 if ($hard_limit && $j + 1 > $hard_limit);
424 %search = %{$search{$letter}};
426 if (ord($letters[0]) > 127 ||
427 ($soft_limit && $j + 1 > $soft_limit)) {
428 $prefix = undef;
429 $remainder = $ret;
431 $return[$i] = [$prefix, $remainder];
433 return @return;
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
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;
453 if (!defined $prefix) {
454 return $remainder;
457 if (!is_valid_prefix($prefix)) {
458 return "$prefix$remainder";
461 if (!$menu_use_color) {
462 return "[$prefix]$remainder";
465 return "$prompt_color$prefix$normal_color$remainder";
468 sub error_msg {
469 print STDERR colored $error_color, @_;
472 sub list_and_choose {
473 my ($opts, @stuff) = @_;
474 my (@chosen, @return);
475 if (!@stuff) {
476 return @return;
478 my $i;
479 my @prefixes = find_unique_prefixes(@stuff) unless $opts->{LIST_ONLY};
481 TOPLOOP:
482 while (1) {
483 my $last_lf = 0;
485 if ($opts->{HEADER}) {
486 if (!$opts->{LIST_FLAT}) {
487 print " ";
489 print colored $header_color, "$opts->{HEADER}\n";
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];
500 elsif ($ref eq 'HASH') {
501 my $value = $highlighted || $print->{VALUE};
502 $print = sprintf($status_fmt,
503 $print->{INDEX},
504 $print->{FILE},
505 $value);
507 else {
508 $print = $highlighted || $print;
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;
516 else {
517 print "\n";
518 $last_lf = 1;
521 if (!$last_lf) {
522 print "\n";
525 return if ($opts->{LIST_ONLY});
527 print colored $prompt_color, $opts->{PROMPT};
528 if ($opts->{SINGLETON}) {
529 print "> ";
531 else {
532 print ">> ";
534 my $line = <STDIN>;
535 if (!$line) {
536 print "\n";
537 $opts->{ON_EOF}->() if $opts->{ON_EOF};
538 last;
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;
548 for my $choice (split(/[\s,]+/, $line)) {
549 my $choose = 1;
550 my ($bottom, $top);
552 # Input that begins with '-'; unchoose
553 if ($choice =~ s/^-//) {
554 $choose = 0;
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);
560 elsif ($choice =~ /^\d+$/) {
561 $bottom = $top = $choice;
563 elsif ($choice eq '*') {
564 $bottom = 1;
565 $top = 1 + @stuff;
567 else {
568 $bottom = $top = find_unique($choice, @stuff);
569 if (!defined $bottom) {
570 error_msg sprintf(__("Huh (%s)?\n"), $choice);
571 next TOPLOOP;
574 if ($opts->{SINGLETON} && $bottom != $top) {
575 error_msg sprintf(__("Huh (%s)?\n"), $choice);
576 next TOPLOOP;
578 for ($i = $bottom-1; $i <= $top-1; $i++) {
579 next if (@stuff <= $i || $i < 0);
580 $chosen[$i] = $choose;
583 last if ($opts->{IMMEDIATE} || $line eq '*');
585 for ($i = 0; $i < @stuff; $i++) {
586 if ($chosen[$i]) {
587 push @return, $stuff[$i];
590 return @return;
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
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
615 sub status_cmd {
616 list_and_choose({ LIST_ONLY => 1, HEADER => $status_head },
617 list_modified());
618 print "\n";
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);
639 sub update_cmd {
640 my @mods = list_modified('file-only');
641 return if (!@mods);
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);
651 print "\n";
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);
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 $_;
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});
682 refresh();
683 say_n_paths('reverted', @update);
685 print "\n";
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");
697 print "\n";
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;
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}";
714 if (defined $patch_mode_revision) {
715 push @diff_cmd, get_diff_reference($patch_mode_revision);
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");
727 @colored = run_cmd_pipe(@display_cmd);
729 my (@hunk) = { TEXT => [], DISPLAY => [], TYPE => 'header' };
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;
739 for (my $i = 0; $i < @diff; $i++) {
740 if ($diff[$i] =~ /^@@ /) {
741 push @hunk, { TEXT => [], DISPLAY => [],
742 TYPE => 'hunk' };
744 push @{$hunk[-1]{TEXT}}, $diff[$i];
745 push @{$hunk[-1]{DISPLAY}},
746 (@colored ? $colored[$i] : $diff[$i]);
748 return @hunk;
751 sub parse_diff_header {
752 my $src = shift;
754 my $head = { TEXT => [], DISPLAY => [], TYPE => 'header' };
755 my $mode = { TEXT => [], DISPLAY => [], TYPE => 'mode' };
756 my $deletion = { TEXT => [], DISPLAY => [], TYPE => 'deletion' };
757 my $addition = { TEXT => [], DISPLAY => [], TYPE => 'addition' };
759 for (my $i = 0; $i < @{$src->{TEXT}}; $i++) {
760 my $dest =
761 $src->{TEXT}->[$i] =~ /^(old|new) mode (\d+)$/ ? $mode :
762 $src->{TEXT}->[$i] =~ /^deleted file/ ? $deletion :
763 $src->{TEXT}->[$i] =~ /^new file/ ? $addition :
764 $head;
765 push @{$dest->{TEXT}}, $src->{TEXT}->[$i];
766 push @{$dest->{DISPLAY}}, $src->{DISPLAY}->[$i];
768 return ($head, $mode, $deletion, $addition);
771 sub hunk_splittable {
772 my ($text) = @_;
774 my @s = split_hunk($text);
775 return (1 < @s);
778 sub parse_hunk_header {
779 my ($line) = @_;
780 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
781 $line =~ /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
782 $o_cnt = 1 unless defined $o_cnt;
783 $n_cnt = 1 unless defined $n_cnt;
784 return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
787 sub format_hunk_header {
788 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = @_;
789 return ("@@ -$o_ofs" .
790 (($o_cnt != 1) ? ",$o_cnt" : '') .
791 " +$n_ofs" .
792 (($n_cnt != 1) ? ",$n_cnt" : '') .
793 " @@\n");
796 sub split_hunk {
797 my ($text, $display) = @_;
798 my @split = ();
799 if (!defined $display) {
800 $display = $text;
802 # If there are context lines in the middle of a hunk,
803 # it can be split, but we would need to take care of
804 # overlaps later.
806 my ($o_ofs, undef, $n_ofs) = parse_hunk_header($text->[0]);
807 my $hunk_start = 1;
809 OUTER:
810 while (1) {
811 my $next_hunk_start = undef;
812 my $i = $hunk_start - 1;
813 my $this = +{
814 TEXT => [],
815 DISPLAY => [],
816 TYPE => 'hunk',
817 OLD => $o_ofs,
818 NEW => $n_ofs,
819 OCNT => 0,
820 NCNT => 0,
821 ADDDEL => 0,
822 POSTCTX => 0,
823 USE => undef,
826 while (++$i < @$text) {
827 my $line = $text->[$i];
828 my $display = $display->[$i];
829 if ($line =~ /^\\/) {
830 push @{$this->{TEXT}}, $line;
831 push @{$this->{DISPLAY}}, $display;
832 next;
834 if ($line =~ /^ /) {
835 if ($this->{ADDDEL} &&
836 !defined $next_hunk_start) {
837 # We have seen leading context and
838 # adds/dels and then here is another
839 # context, which is trailing for this
840 # split hunk and leading for the next
841 # one.
842 $next_hunk_start = $i;
844 push @{$this->{TEXT}}, $line;
845 push @{$this->{DISPLAY}}, $display;
846 $this->{OCNT}++;
847 $this->{NCNT}++;
848 if (defined $next_hunk_start) {
849 $this->{POSTCTX}++;
851 next;
854 # add/del
855 if (defined $next_hunk_start) {
856 # We are done with the current hunk and
857 # this is the first real change for the
858 # next split one.
859 $hunk_start = $next_hunk_start;
860 $o_ofs = $this->{OLD} + $this->{OCNT};
861 $n_ofs = $this->{NEW} + $this->{NCNT};
862 $o_ofs -= $this->{POSTCTX};
863 $n_ofs -= $this->{POSTCTX};
864 push @split, $this;
865 redo OUTER;
867 push @{$this->{TEXT}}, $line;
868 push @{$this->{DISPLAY}}, $display;
869 $this->{ADDDEL}++;
870 if ($line =~ /^-/) {
871 $this->{OCNT}++;
873 else {
874 $this->{NCNT}++;
878 push @split, $this;
879 last;
882 for my $hunk (@split) {
883 $o_ofs = $hunk->{OLD};
884 $n_ofs = $hunk->{NEW};
885 my $o_cnt = $hunk->{OCNT};
886 my $n_cnt = $hunk->{NCNT};
888 my $head = format_hunk_header($o_ofs, $o_cnt, $n_ofs, $n_cnt);
889 my $display_head = $head;
890 unshift @{$hunk->{TEXT}}, $head;
891 if ($diff_use_color) {
892 $display_head = colored($fraginfo_color, $head);
894 unshift @{$hunk->{DISPLAY}}, $display_head;
896 return @split;
899 sub find_last_o_ctx {
900 my ($it) = @_;
901 my $text = $it->{TEXT};
902 my ($o_ofs, $o_cnt) = parse_hunk_header($text->[0]);
903 my $i = @{$text};
904 my $last_o_ctx = $o_ofs + $o_cnt;
905 while (0 < --$i) {
906 my $line = $text->[$i];
907 if ($line =~ /^ /) {
908 $last_o_ctx--;
909 next;
911 last;
913 return $last_o_ctx;
916 sub merge_hunk {
917 my ($prev, $this) = @_;
918 my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) =
919 parse_hunk_header($prev->{TEXT}[0]);
920 my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) =
921 parse_hunk_header($this->{TEXT}[0]);
923 my (@line, $i, $ofs, $o_cnt, $n_cnt);
924 $ofs = $o0_ofs;
925 $o_cnt = $n_cnt = 0;
926 for ($i = 1; $i < @{$prev->{TEXT}}; $i++) {
927 my $line = $prev->{TEXT}[$i];
928 if ($line =~ /^\+/) {
929 $n_cnt++;
930 push @line, $line;
931 next;
932 } elsif ($line =~ /^\\/) {
933 push @line, $line;
934 next;
937 last if ($o1_ofs <= $ofs);
939 $o_cnt++;
940 $ofs++;
941 if ($line =~ /^ /) {
942 $n_cnt++;
944 push @line, $line;
947 for ($i = 1; $i < @{$this->{TEXT}}; $i++) {
948 my $line = $this->{TEXT}[$i];
949 if ($line =~ /^\+/) {
950 $n_cnt++;
951 push @line, $line;
952 next;
953 } elsif ($line =~ /^\\/) {
954 push @line, $line;
955 next;
957 $ofs++;
958 $o_cnt++;
959 if ($line =~ /^ /) {
960 $n_cnt++;
962 push @line, $line;
964 my $head = format_hunk_header($o0_ofs, $o_cnt, $n0_ofs, $n_cnt);
965 @{$prev->{TEXT}} = ($head, @line);
968 sub coalesce_overlapping_hunks {
969 my (@in) = @_;
970 my @out = ();
972 my ($last_o_ctx, $last_was_dirty);
973 my $ofs_delta = 0;
975 for (@in) {
976 if ($_->{TYPE} ne 'hunk') {
977 push @out, $_;
978 next;
980 my $text = $_->{TEXT};
981 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
982 parse_hunk_header($text->[0]);
983 unless ($_->{USE}) {
984 $ofs_delta += $o_cnt - $n_cnt;
985 # If this hunk has been edited then subtract
986 # the delta that is due to the edit.
987 if ($_->{OFS_DELTA}) {
988 $ofs_delta -= $_->{OFS_DELTA};
990 next;
992 if ($ofs_delta) {
993 if ($patch_mode_flavour{IS_REVERSE}) {
994 $o_ofs -= $ofs_delta;
995 } else {
996 $n_ofs += $ofs_delta;
998 $_->{TEXT}->[0] = format_hunk_header($o_ofs, $o_cnt,
999 $n_ofs, $n_cnt);
1001 # If this hunk was edited then adjust the offset delta
1002 # to reflect the edit.
1003 if ($_->{OFS_DELTA}) {
1004 $ofs_delta += $_->{OFS_DELTA};
1006 if (defined $last_o_ctx &&
1007 $o_ofs <= $last_o_ctx &&
1008 !$_->{DIRTY} &&
1009 !$last_was_dirty) {
1010 merge_hunk($out[-1], $_);
1012 else {
1013 push @out, $_;
1015 $last_o_ctx = find_last_o_ctx($out[-1]);
1016 $last_was_dirty = $_->{DIRTY};
1018 return @out;
1021 sub reassemble_patch {
1022 my $head = shift;
1023 my @patch;
1025 # Include everything in the header except the beginning of the diff.
1026 push @patch, (grep { !/^[-+]{3}/ } @$head);
1028 # Then include any headers from the hunk lines, which must
1029 # come before any actual hunk.
1030 while (@_ && $_[0] !~ /^@/) {
1031 push @patch, shift;
1034 # Then begin the diff.
1035 push @patch, grep { /^[-+]{3}/ } @$head;
1037 # And then the actual hunks.
1038 push @patch, @_;
1040 return @patch;
1043 sub color_diff {
1044 return map {
1045 colored((/^@/ ? $fraginfo_color :
1046 /^\+/ ? $diff_new_color :
1047 /^-/ ? $diff_old_color :
1048 $diff_plain_color),
1049 $_);
1050 } @_;
1053 my %edit_hunk_manually_modes = (
1054 stage => N__(
1055 "If the patch applies cleanly, the edited hunk will immediately be
1056 marked for staging."),
1057 stash => N__(
1058 "If the patch applies cleanly, the edited hunk will immediately be
1059 marked for stashing."),
1060 reset_head => N__(
1061 "If the patch applies cleanly, the edited hunk will immediately be
1062 marked for unstaging."),
1063 reset_nothead => N__(
1064 "If the patch applies cleanly, the edited hunk will immediately be
1065 marked for applying."),
1066 checkout_index => N__(
1067 "If the patch applies cleanly, the edited hunk will immediately be
1068 marked for discarding."),
1069 checkout_head => N__(
1070 "If the patch applies cleanly, the edited hunk will immediately be
1071 marked for discarding."),
1072 checkout_nothead => N__(
1073 "If the patch applies cleanly, the edited hunk will immediately be
1074 marked for applying."),
1075 worktree_head => N__(
1076 "If the patch applies cleanly, the edited hunk will immediately be
1077 marked for discarding."),
1078 worktree_nothead => N__(
1079 "If the patch applies cleanly, the edited hunk will immediately be
1080 marked for applying."),
1083 sub recount_edited_hunk {
1084 local $_;
1085 my ($oldtext, $newtext) = @_;
1086 my ($o_cnt, $n_cnt) = (0, 0);
1087 for (@{$newtext}[1..$#{$newtext}]) {
1088 my $mode = substr($_, 0, 1);
1089 if ($mode eq '-') {
1090 $o_cnt++;
1091 } elsif ($mode eq '+') {
1092 $n_cnt++;
1093 } elsif ($mode eq ' ' or $mode eq "\n") {
1094 $o_cnt++;
1095 $n_cnt++;
1098 my ($o_ofs, undef, $n_ofs, undef) =
1099 parse_hunk_header($newtext->[0]);
1100 $newtext->[0] = format_hunk_header($o_ofs, $o_cnt, $n_ofs, $n_cnt);
1101 my (undef, $orig_o_cnt, undef, $orig_n_cnt) =
1102 parse_hunk_header($oldtext->[0]);
1103 # Return the change in the number of lines inserted by this hunk
1104 return $orig_o_cnt - $orig_n_cnt - $o_cnt + $n_cnt;
1107 sub edit_hunk_manually {
1108 my ($oldtext) = @_;
1110 my $hunkfile = $repo->repo_path . "/addp-hunk-edit.diff";
1111 my $fh;
1112 open $fh, '>', $hunkfile
1113 or die sprintf(__("failed to open hunk edit file for writing: %s"), $!);
1114 print $fh Git::comment_lines __("Manual hunk edit mode -- see bottom for a quick guide.\n");
1115 print $fh @$oldtext;
1116 my $is_reverse = $patch_mode_flavour{IS_REVERSE};
1117 my ($remove_plus, $remove_minus) = $is_reverse ? ('-', '+') : ('+', '-');
1118 my $comment_line_char = Git::get_comment_line_char;
1119 print $fh Git::comment_lines sprintf(__ <<EOF, $remove_minus, $remove_plus, $comment_line_char),
1121 To remove '%s' lines, make them ' ' lines (context).
1122 To remove '%s' lines, delete them.
1123 Lines starting with %s will be removed.
1125 __($edit_hunk_manually_modes{$patch_mode}),
1126 # TRANSLATORS: 'it' refers to the patch mentioned in the previous messages.
1127 __ <<EOF2 ;
1128 If it does not apply cleanly, you will be given an opportunity to
1129 edit again. If all lines of the hunk are removed, then the edit is
1130 aborted and the hunk is left unchanged.
1131 EOF2
1132 close $fh;
1134 chomp(my ($editor) = run_cmd_pipe(qw(git var GIT_EDITOR)));
1135 system('sh', '-c', $editor.' "$@"', $editor, $hunkfile);
1137 if ($? != 0) {
1138 return undef;
1141 open $fh, '<', $hunkfile
1142 or die sprintf(__("failed to open hunk edit file for reading: %s"), $!);
1143 my @newtext = grep { !/^\Q$comment_line_char\E/ } <$fh>;
1144 close $fh;
1145 unlink $hunkfile;
1147 # Abort if nothing remains
1148 if (!grep { /\S/ } @newtext) {
1149 return undef;
1152 # Reinsert the first hunk header if the user accidentally deleted it
1153 if ($newtext[0] !~ /^@/) {
1154 unshift @newtext, $oldtext->[0];
1156 return \@newtext;
1159 sub diff_applies {
1160 return run_git_apply($patch_mode_flavour{APPLY_CHECK} . ' --check',
1161 map { @{$_->{TEXT}} } @_);
1164 sub _restore_terminal_and_die {
1165 ReadMode 'restore';
1166 print "\n";
1167 exit 1;
1170 sub prompt_single_character {
1171 if ($use_readkey) {
1172 local $SIG{TERM} = \&_restore_terminal_and_die;
1173 local $SIG{INT} = \&_restore_terminal_and_die;
1174 ReadMode 'cbreak';
1175 my $key = ReadKey 0;
1176 ReadMode 'restore';
1177 if ($use_termcap and $key eq "\e") {
1178 while (!defined $term_escapes{$key}) {
1179 my $next = ReadKey 0.5;
1180 last if (!defined $next);
1181 $key .= $next;
1183 $key =~ s/\e/^[/;
1185 print "$key" if defined $key;
1186 print "\n";
1187 return $key;
1188 } else {
1189 return <STDIN>;
1193 sub prompt_yesno {
1194 my ($prompt) = @_;
1195 while (1) {
1196 print colored $prompt_color, $prompt;
1197 my $line = prompt_single_character;
1198 return undef unless defined $line;
1199 return 0 if $line =~ /^n/i;
1200 return 1 if $line =~ /^y/i;
1204 sub edit_hunk_loop {
1205 my ($head, $hunks, $ix) = @_;
1206 my $hunk = $hunks->[$ix];
1207 my $text = $hunk->{TEXT};
1209 while (1) {
1210 my $newtext = edit_hunk_manually($text);
1211 if (!defined $newtext) {
1212 return undef;
1214 my $newhunk = {
1215 TEXT => $newtext,
1216 TYPE => $hunk->{TYPE},
1217 USE => 1,
1218 DIRTY => 1,
1220 $newhunk->{OFS_DELTA} = recount_edited_hunk($text, $newtext);
1221 # If this hunk has already been edited then add the
1222 # offset delta of the previous edit to get the real
1223 # delta from the original unedited hunk.
1224 $hunk->{OFS_DELTA} and
1225 $newhunk->{OFS_DELTA} += $hunk->{OFS_DELTA};
1226 if (diff_applies($head,
1227 @{$hunks}[0..$ix-1],
1228 $newhunk,
1229 @{$hunks}[$ix+1..$#{$hunks}])) {
1230 $newhunk->{DISPLAY} = [color_diff(@{$newtext})];
1231 return $newhunk;
1233 else {
1234 prompt_yesno(
1235 # TRANSLATORS: do not translate [y/n]
1236 # The program will only accept that input
1237 # at this point.
1238 # Consider translating (saying "no" discards!) as
1239 # (saying "n" for "no" discards!) if the translation
1240 # of the word "no" does not start with n.
1241 __('Your edited hunk does not apply. Edit again '
1242 . '(saying "no" discards!) [y/n]? ')
1243 ) or return undef;
1248 my %help_patch_modes = (
1249 stage => N__(
1250 "y - stage this hunk
1251 n - do not stage this hunk
1252 q - quit; do not stage this hunk or any of the remaining ones
1253 a - stage this hunk and all later hunks in the file
1254 d - do not stage this hunk or any of the later hunks in the file"),
1255 stash => N__(
1256 "y - stash this hunk
1257 n - do not stash this hunk
1258 q - quit; do not stash this hunk or any of the remaining ones
1259 a - stash this hunk and all later hunks in the file
1260 d - do not stash this hunk or any of the later hunks in the file"),
1261 reset_head => N__(
1262 "y - unstage this hunk
1263 n - do not unstage this hunk
1264 q - quit; do not unstage this hunk or any of the remaining ones
1265 a - unstage this hunk and all later hunks in the file
1266 d - do not unstage this hunk or any of the later hunks in the file"),
1267 reset_nothead => N__(
1268 "y - apply this hunk to index
1269 n - do not apply this hunk to index
1270 q - quit; do not apply this hunk or any of the remaining ones
1271 a - apply this hunk and all later hunks in the file
1272 d - do not apply this hunk or any of the later hunks in the file"),
1273 checkout_index => N__(
1274 "y - discard this hunk from worktree
1275 n - do not discard this hunk from worktree
1276 q - quit; do not discard this hunk or any of the remaining ones
1277 a - discard this hunk and all later hunks in the file
1278 d - do not discard this hunk or any of the later hunks in the file"),
1279 checkout_head => N__(
1280 "y - discard this hunk from index and worktree
1281 n - do not discard this hunk from index and worktree
1282 q - quit; do not discard this hunk or any of the remaining ones
1283 a - discard this hunk and all later hunks in the file
1284 d - do not discard this hunk or any of the later hunks in the file"),
1285 checkout_nothead => N__(
1286 "y - apply this hunk to index and worktree
1287 n - do not apply this hunk to index and worktree
1288 q - quit; do not apply this hunk or any of the remaining ones
1289 a - apply this hunk and all later hunks in the file
1290 d - do not apply this hunk or any of the later hunks in the file"),
1291 worktree_head => N__(
1292 "y - discard this hunk from worktree
1293 n - do not discard this hunk from worktree
1294 q - quit; do not discard this hunk or any of the remaining ones
1295 a - discard this hunk and all later hunks in the file
1296 d - do not discard this hunk or any of the later hunks in the file"),
1297 worktree_nothead => N__(
1298 "y - apply this hunk to worktree
1299 n - do not apply this hunk to worktree
1300 q - quit; do not apply this hunk or any of the remaining ones
1301 a - apply this hunk and all later hunks in the file
1302 d - do not apply this hunk or any of the later hunks in the file"),
1305 sub help_patch_cmd {
1306 local $_;
1307 my $other = $_[0] . ",?";
1308 print colored $help_color, __($help_patch_modes{$patch_mode}), "\n",
1309 map { "$_\n" } grep {
1310 my $c = quotemeta(substr($_, 0, 1));
1311 $other =~ /,$c/
1312 } split "\n", __ <<EOF ;
1313 g - select a hunk to go to
1314 / - search for a hunk matching the given regex
1315 j - leave this hunk undecided, see next undecided hunk
1316 J - leave this hunk undecided, see next hunk
1317 k - leave this hunk undecided, see previous undecided hunk
1318 K - leave this hunk undecided, see previous hunk
1319 s - split the current hunk into smaller hunks
1320 e - manually edit the current hunk
1321 ? - print help
1325 sub apply_patch {
1326 my $cmd = shift;
1327 my $ret = run_git_apply $cmd, @_;
1328 if (!$ret) {
1329 print STDERR @_;
1331 return $ret;
1334 sub apply_patch_for_checkout_commit {
1335 my $reverse = shift;
1336 my $applies_index = run_git_apply 'apply '.$reverse.' --cached --check', @_;
1337 my $applies_worktree = run_git_apply 'apply '.$reverse.' --check', @_;
1339 if ($applies_worktree && $applies_index) {
1340 run_git_apply 'apply '.$reverse.' --cached', @_;
1341 run_git_apply 'apply '.$reverse, @_;
1342 return 1;
1343 } elsif (!$applies_index) {
1344 print colored $error_color, __("The selected hunks do not apply to the index!\n");
1345 if (prompt_yesno __("Apply them to the worktree anyway? ")) {
1346 return run_git_apply 'apply '.$reverse, @_;
1347 } else {
1348 print colored $error_color, __("Nothing was applied.\n");
1349 return 0;
1351 } else {
1352 print STDERR @_;
1353 return 0;
1357 sub patch_update_cmd {
1358 my @all_mods = list_modified($patch_mode_flavour{FILTER});
1359 error_msg sprintf(__("ignoring unmerged: %s\n"), $_->{VALUE})
1360 for grep { $_->{UNMERGED} } @all_mods;
1361 @all_mods = grep { !$_->{UNMERGED} } @all_mods;
1363 my @mods = grep { !($_->{BINARY}) } @all_mods;
1364 my @them;
1366 if (!@mods) {
1367 if (@all_mods) {
1368 print STDERR __("Only binary files changed.\n");
1369 } else {
1370 print STDERR __("No changes.\n");
1372 return 0;
1374 if ($patch_mode_only) {
1375 @them = @mods;
1377 else {
1378 @them = list_and_choose({ PROMPT => __('Patch update'),
1379 HEADER => $status_head, },
1380 @mods);
1382 for (@them) {
1383 return 0 if patch_update_file($_->{VALUE});
1387 # Generate a one line summary of a hunk.
1388 sub summarize_hunk {
1389 my $rhunk = shift;
1390 my $summary = $rhunk->{TEXT}[0];
1392 # Keep the line numbers, discard extra context.
1393 $summary =~ s/@@(.*?)@@.*/$1 /s;
1394 $summary .= " " x (20 - length $summary);
1396 # Add some user context.
1397 for my $line (@{$rhunk->{TEXT}}) {
1398 if ($line =~ m/^[+-].*\w/) {
1399 $summary .= $line;
1400 last;
1404 chomp $summary;
1405 return substr($summary, 0, 80) . "\n";
1409 # Print a one-line summary of each hunk in the array ref in
1410 # the first argument, starting with the index in the 2nd.
1411 sub display_hunks {
1412 my ($hunks, $i) = @_;
1413 my $ctr = 0;
1414 $i ||= 0;
1415 for (; $i < @$hunks && $ctr < 20; $i++, $ctr++) {
1416 my $status = " ";
1417 if (defined $hunks->[$i]{USE}) {
1418 $status = $hunks->[$i]{USE} ? "+" : "-";
1420 printf "%s%2d: %s",
1421 $status,
1422 $i + 1,
1423 summarize_hunk($hunks->[$i]);
1425 return $i;
1428 my %patch_update_prompt_modes = (
1429 stage => {
1430 mode => N__("Stage mode change [y,n,q,a,d%s,?]? "),
1431 deletion => N__("Stage deletion [y,n,q,a,d%s,?]? "),
1432 addition => N__("Stage addition [y,n,q,a,d%s,?]? "),
1433 hunk => N__("Stage this hunk [y,n,q,a,d%s,?]? "),
1435 stash => {
1436 mode => N__("Stash mode change [y,n,q,a,d%s,?]? "),
1437 deletion => N__("Stash deletion [y,n,q,a,d%s,?]? "),
1438 addition => N__("Stash addition [y,n,q,a,d%s,?]? "),
1439 hunk => N__("Stash this hunk [y,n,q,a,d%s,?]? "),
1441 reset_head => {
1442 mode => N__("Unstage mode change [y,n,q,a,d%s,?]? "),
1443 deletion => N__("Unstage deletion [y,n,q,a,d%s,?]? "),
1444 addition => N__("Unstage addition [y,n,q,a,d%s,?]? "),
1445 hunk => N__("Unstage this hunk [y,n,q,a,d%s,?]? "),
1447 reset_nothead => {
1448 mode => N__("Apply mode change to index [y,n,q,a,d%s,?]? "),
1449 deletion => N__("Apply deletion to index [y,n,q,a,d%s,?]? "),
1450 addition => N__("Apply addition to index [y,n,q,a,d%s,?]? "),
1451 hunk => N__("Apply this hunk to index [y,n,q,a,d%s,?]? "),
1453 checkout_index => {
1454 mode => N__("Discard mode change from worktree [y,n,q,a,d%s,?]? "),
1455 deletion => N__("Discard deletion from worktree [y,n,q,a,d%s,?]? "),
1456 addition => N__("Discard addition from worktree [y,n,q,a,d%s,?]? "),
1457 hunk => N__("Discard this hunk from worktree [y,n,q,a,d%s,?]? "),
1459 checkout_head => {
1460 mode => N__("Discard mode change from index and worktree [y,n,q,a,d%s,?]? "),
1461 deletion => N__("Discard deletion from index and worktree [y,n,q,a,d%s,?]? "),
1462 addition => N__("Discard addition from index and worktree [y,n,q,a,d%s,?]? "),
1463 hunk => N__("Discard this hunk from index and worktree [y,n,q,a,d%s,?]? "),
1465 checkout_nothead => {
1466 mode => N__("Apply mode change to index and worktree [y,n,q,a,d%s,?]? "),
1467 deletion => N__("Apply deletion to index and worktree [y,n,q,a,d%s,?]? "),
1468 addition => N__("Apply addition to index and worktree [y,n,q,a,d%s,?]? "),
1469 hunk => N__("Apply this hunk to index and worktree [y,n,q,a,d%s,?]? "),
1471 worktree_head => {
1472 mode => N__("Discard mode change from worktree [y,n,q,a,d%s,?]? "),
1473 deletion => N__("Discard deletion from worktree [y,n,q,a,d%s,?]? "),
1474 addition => N__("Discard addition from worktree [y,n,q,a,d%s,?]? "),
1475 hunk => N__("Discard this hunk from worktree [y,n,q,a,d%s,?]? "),
1477 worktree_nothead => {
1478 mode => N__("Apply mode change to worktree [y,n,q,a,d%s,?]? "),
1479 deletion => N__("Apply deletion to worktree [y,n,q,a,d%s,?]? "),
1480 addition => N__("Apply addition to worktree [y,n,q,a,d%s,?]? "),
1481 hunk => N__("Apply this hunk to worktree [y,n,q,a,d%s,?]? "),
1485 sub patch_update_file {
1486 my $quit = 0;
1487 my ($ix, $num);
1488 my $path = shift;
1489 my ($head, @hunk) = parse_diff($path);
1490 ($head, my $mode, my $deletion, my $addition) = parse_diff_header($head);
1491 for (@{$head->{DISPLAY}}) {
1492 print;
1495 if (@{$mode->{TEXT}}) {
1496 unshift @hunk, $mode;
1498 if (@{$deletion->{TEXT}}) {
1499 foreach my $hunk (@hunk) {
1500 push @{$deletion->{TEXT}}, @{$hunk->{TEXT}};
1501 push @{$deletion->{DISPLAY}}, @{$hunk->{DISPLAY}};
1503 @hunk = ($deletion);
1504 } elsif (@{$addition->{TEXT}}) {
1505 foreach my $hunk (@hunk) {
1506 push @{$addition->{TEXT}}, @{$hunk->{TEXT}};
1507 push @{$addition->{DISPLAY}}, @{$hunk->{DISPLAY}};
1509 @hunk = ($addition);
1512 $num = scalar @hunk;
1513 $ix = 0;
1515 while (1) {
1516 my ($prev, $next, $other, $undecided, $i);
1517 $other = '';
1519 if ($num <= $ix) {
1520 $ix = 0;
1522 for ($i = 0; $i < $ix; $i++) {
1523 if (!defined $hunk[$i]{USE}) {
1524 $prev = 1;
1525 $other .= ',k';
1526 last;
1529 if ($ix) {
1530 $other .= ',K';
1532 for ($i = $ix + 1; $i < $num; $i++) {
1533 if (!defined $hunk[$i]{USE}) {
1534 $next = 1;
1535 $other .= ',j';
1536 last;
1539 if ($ix < $num - 1) {
1540 $other .= ',J';
1542 if ($num > 1) {
1543 $other .= ',g,/';
1545 for ($i = 0; $i < $num; $i++) {
1546 if (!defined $hunk[$i]{USE}) {
1547 $undecided = 1;
1548 last;
1551 last if (!$undecided);
1553 if ($hunk[$ix]{TYPE} eq 'hunk' &&
1554 hunk_splittable($hunk[$ix]{TEXT})) {
1555 $other .= ',s';
1557 if ($hunk[$ix]{TYPE} eq 'hunk') {
1558 $other .= ',e';
1560 for (@{$hunk[$ix]{DISPLAY}}) {
1561 print;
1563 print colored $prompt_color, "(", ($ix+1), "/$num) ",
1564 sprintf(__($patch_update_prompt_modes{$patch_mode}{$hunk[$ix]{TYPE}}), $other);
1566 my $line = prompt_single_character;
1567 last unless defined $line;
1568 if ($line) {
1569 if ($line =~ /^y/i) {
1570 $hunk[$ix]{USE} = 1;
1572 elsif ($line =~ /^n/i) {
1573 $hunk[$ix]{USE} = 0;
1575 elsif ($line =~ /^a/i) {
1576 while ($ix < $num) {
1577 if (!defined $hunk[$ix]{USE}) {
1578 $hunk[$ix]{USE} = 1;
1580 $ix++;
1582 next;
1584 elsif ($line =~ /^g(.*)/) {
1585 my $response = $1;
1586 unless ($other =~ /g/) {
1587 error_msg __("No other hunks to goto\n");
1588 next;
1590 my $no = $ix > 10 ? $ix - 10 : 0;
1591 while ($response eq '') {
1592 $no = display_hunks(\@hunk, $no);
1593 if ($no < $num) {
1594 print __("go to which hunk (<ret> to see more)? ");
1595 } else {
1596 print __("go to which hunk? ");
1598 $response = <STDIN>;
1599 if (!defined $response) {
1600 $response = '';
1602 chomp $response;
1604 if ($response !~ /^\s*\d+\s*$/) {
1605 error_msg sprintf(__("Invalid number: '%s'\n"),
1606 $response);
1607 } elsif (0 < $response && $response <= $num) {
1608 $ix = $response - 1;
1609 } else {
1610 error_msg sprintf(__n("Sorry, only %d hunk available.\n",
1611 "Sorry, only %d hunks available.\n", $num), $num);
1613 next;
1615 elsif ($line =~ /^d/i) {
1616 while ($ix < $num) {
1617 if (!defined $hunk[$ix]{USE}) {
1618 $hunk[$ix]{USE} = 0;
1620 $ix++;
1622 next;
1624 elsif ($line =~ /^q/i) {
1625 for ($i = 0; $i < $num; $i++) {
1626 if (!defined $hunk[$i]{USE}) {
1627 $hunk[$i]{USE} = 0;
1630 $quit = 1;
1631 last;
1633 elsif ($line =~ m|^/(.*)|) {
1634 my $regex = $1;
1635 unless ($other =~ m|/|) {
1636 error_msg __("No other hunks to search\n");
1637 next;
1639 if ($regex eq "") {
1640 print colored $prompt_color, __("search for regex? ");
1641 $regex = <STDIN>;
1642 if (defined $regex) {
1643 chomp $regex;
1646 my $search_string;
1647 eval {
1648 $search_string = qr{$regex}m;
1650 if ($@) {
1651 my ($err,$exp) = ($@, $1);
1652 $err =~ s/ at .*git-add--interactive line \d+, <STDIN> line \d+.*$//;
1653 error_msg sprintf(__("Malformed search regexp %s: %s\n"), $exp, $err);
1654 next;
1656 my $iy = $ix;
1657 while (1) {
1658 my $text = join ("", @{$hunk[$iy]{TEXT}});
1659 last if ($text =~ $search_string);
1660 $iy++;
1661 $iy = 0 if ($iy >= $num);
1662 if ($ix == $iy) {
1663 error_msg __("No hunk matches the given pattern\n");
1664 last;
1667 $ix = $iy;
1668 next;
1670 elsif ($line =~ /^K/) {
1671 if ($other =~ /K/) {
1672 $ix--;
1674 else {
1675 error_msg __("No previous hunk\n");
1677 next;
1679 elsif ($line =~ /^J/) {
1680 if ($other =~ /J/) {
1681 $ix++;
1683 else {
1684 error_msg __("No next hunk\n");
1686 next;
1688 elsif ($line =~ /^k/) {
1689 if ($other =~ /k/) {
1690 while (1) {
1691 $ix--;
1692 last if (!$ix ||
1693 !defined $hunk[$ix]{USE});
1696 else {
1697 error_msg __("No previous hunk\n");
1699 next;
1701 elsif ($line =~ /^j/) {
1702 if ($other !~ /j/) {
1703 error_msg __("No next hunk\n");
1704 next;
1707 elsif ($line =~ /^s/) {
1708 unless ($other =~ /s/) {
1709 error_msg __("Sorry, cannot split this hunk\n");
1710 next;
1712 my @split = split_hunk($hunk[$ix]{TEXT}, $hunk[$ix]{DISPLAY});
1713 if (1 < @split) {
1714 print colored $header_color, sprintf(
1715 __n("Split into %d hunk.\n",
1716 "Split into %d hunks.\n",
1717 scalar(@split)), scalar(@split));
1719 splice (@hunk, $ix, 1, @split);
1720 $num = scalar @hunk;
1721 next;
1723 elsif ($line =~ /^e/) {
1724 unless ($other =~ /e/) {
1725 error_msg __("Sorry, cannot edit this hunk\n");
1726 next;
1728 my $newhunk = edit_hunk_loop($head, \@hunk, $ix);
1729 if (defined $newhunk) {
1730 splice @hunk, $ix, 1, $newhunk;
1733 else {
1734 help_patch_cmd($other);
1735 next;
1737 # soft increment
1738 while (1) {
1739 $ix++;
1740 last if ($ix >= $num ||
1741 !defined $hunk[$ix]{USE});
1746 @hunk = coalesce_overlapping_hunks(@hunk);
1748 my $n_lofs = 0;
1749 my @result = ();
1750 for (@hunk) {
1751 if ($_->{USE}) {
1752 push @result, @{$_->{TEXT}};
1756 if (@result) {
1757 my @patch = reassemble_patch($head->{TEXT}, @result);
1758 my $apply_routine = $patch_mode_flavour{APPLY};
1759 &$apply_routine(@patch);
1760 refresh();
1763 print "\n";
1764 return $quit;
1767 sub diff_cmd {
1768 my @mods = list_modified('index-only');
1769 @mods = grep { !($_->{BINARY}) } @mods;
1770 return if (!@mods);
1771 my (@them) = list_and_choose({ PROMPT => __('Review diff'),
1772 IMMEDIATE => 1,
1773 HEADER => $status_head, },
1774 @mods);
1775 return if (!@them);
1776 my $reference = (is_initial_commit()) ? get_empty_tree() : 'HEAD';
1777 system(qw(git diff -p --cached), $reference, '--',
1778 map { $_->{VALUE} } @them);
1781 sub quit_cmd {
1782 print __("Bye.\n");
1783 exit(0);
1786 sub help_cmd {
1787 # TRANSLATORS: please do not translate the command names
1788 # 'status', 'update', 'revert', etc.
1789 print colored $help_color, __ <<'EOF' ;
1790 status - show paths with changes
1791 update - add working tree state to the staged set of changes
1792 revert - revert staged set of changes back to the HEAD version
1793 patch - pick hunks and update selectively
1794 diff - view diff between HEAD and index
1795 add untracked - add contents of untracked files to the staged set of changes
1799 sub process_args {
1800 return unless @ARGV;
1801 my $arg = shift @ARGV;
1802 if ($arg =~ /--patch(?:=(.*))?/) {
1803 if (defined $1) {
1804 if ($1 eq 'reset') {
1805 $patch_mode = 'reset_head';
1806 $patch_mode_revision = 'HEAD';
1807 $arg = shift @ARGV or die __("missing --");
1808 if ($arg ne '--') {
1809 $patch_mode_revision = $arg;
1810 $patch_mode = ($arg eq 'HEAD' ?
1811 'reset_head' : 'reset_nothead');
1812 $arg = shift @ARGV or die __("missing --");
1814 } elsif ($1 eq 'checkout') {
1815 $arg = shift @ARGV or die __("missing --");
1816 if ($arg eq '--') {
1817 $patch_mode = 'checkout_index';
1818 } else {
1819 $patch_mode_revision = $arg;
1820 $patch_mode = ($arg eq 'HEAD' ?
1821 'checkout_head' : 'checkout_nothead');
1822 $arg = shift @ARGV or die __("missing --");
1824 } elsif ($1 eq 'worktree') {
1825 $arg = shift @ARGV or die __("missing --");
1826 if ($arg eq '--') {
1827 $patch_mode = 'checkout_index';
1828 } else {
1829 $patch_mode_revision = $arg;
1830 $patch_mode = ($arg eq 'HEAD' ?
1831 'worktree_head' : 'worktree_nothead');
1832 $arg = shift @ARGV or die __("missing --");
1834 } elsif ($1 eq 'stage' or $1 eq 'stash') {
1835 $patch_mode = $1;
1836 $arg = shift @ARGV or die __("missing --");
1837 } else {
1838 die sprintf(__("unknown --patch mode: %s"), $1);
1840 } else {
1841 $patch_mode = 'stage';
1842 $arg = shift @ARGV or die __("missing --");
1844 die sprintf(__("invalid argument %s, expecting --"),
1845 $arg) unless $arg eq "--";
1846 %patch_mode_flavour = %{$patch_modes{$patch_mode}};
1847 $patch_mode_only = 1;
1849 elsif ($arg ne "--") {
1850 die sprintf(__("invalid argument %s, expecting --"), $arg);
1854 sub main_loop {
1855 my @cmd = ([ 'status', \&status_cmd, ],
1856 [ 'update', \&update_cmd, ],
1857 [ 'revert', \&revert_cmd, ],
1858 [ 'add untracked', \&add_untracked_cmd, ],
1859 [ 'patch', \&patch_update_cmd, ],
1860 [ 'diff', \&diff_cmd, ],
1861 [ 'quit', \&quit_cmd, ],
1862 [ 'help', \&help_cmd, ],
1864 while (1) {
1865 my ($it) = list_and_choose({ PROMPT => __('What now'),
1866 SINGLETON => 1,
1867 LIST_FLAT => 4,
1868 HEADER => __('*** Commands ***'),
1869 ON_EOF => \&quit_cmd,
1870 IMMEDIATE => 1 }, @cmd);
1871 if ($it) {
1872 eval {
1873 $it->[1]->();
1875 if ($@) {
1876 print "$@";
1882 process_args();
1883 refresh();
1884 if ($patch_mode_only) {
1885 patch_update_cmd();
1887 else {
1888 status_cmd();
1889 main_loop();