6 use Git qw(unquote_path);
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) =
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'),
21 if ($menu_use_color) {
22 my $help_color_spec = ($repo->config('color.interactive.help') or
24 $error_color = $repo->get_color('color.interactive.error',
28 my $diff_use_color = $repo->get_colorbool('color.diff');
29 my ($fraginfo_color) =
31 $repo->get_color('color.diff.frag', 'cyan'),
33 my ($diff_plain_color) =
35 $repo->get_color('color.diff.plain', ''),
37 my ($diff_old_color) =
39 $repo->get_color('color.diff.old', 'red'),
41 my ($diff_new_color) =
43 $repo->get_color('color.diff.new', 'green'),
46 my $normal_color = $repo->get_color("", "reset");
48 my $diff_algorithm = $repo->config('diff.algorithm');
49 my $diff_filter = $repo->config('interactive.difffilter');
57 if ($repo->config_bool("interactive.singlekey")) {
59 require Term::ReadKey;
60 Term::ReadKey->import;
64 print STDERR "missing Term::ReadKey, disabling interactive.singlekey\n";
68 my $termcap = Term::Cap->Tgetent;
69 foreach (values %$termcap) {
70 $term_escapes{$_} = 1 if /^\e/;
78 my $string = join("", @_);
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$/;
93 # command line options
96 my $patch_mode_revision;
99 sub apply_patch_for_checkout_commit;
100 sub apply_patch_for_stash;
104 DIFF => 'diff-files -p',
105 APPLY => sub { apply_patch 'apply --cached', @_; },
106 APPLY_CHECK => 'apply --cached',
107 FILTER => 'file-only',
111 DIFF => 'diff-index -p HEAD',
112 APPLY => sub { apply_patch 'apply --cached', @_; },
113 APPLY_CHECK => 'apply --cached',
118 DIFF => 'diff-index -p --cached',
119 APPLY => sub { apply_patch 'apply -R --cached', @_; },
120 APPLY_CHECK => 'apply -R --cached',
121 FILTER => 'index-only',
125 DIFF => 'diff-index -R -p --cached',
126 APPLY => sub { apply_patch 'apply --cached', @_; },
127 APPLY_CHECK => 'apply --cached',
128 FILTER => 'index-only',
131 'checkout_index' => {
132 DIFF => 'diff-files -p',
133 APPLY => sub { apply_patch 'apply -R', @_; },
134 APPLY_CHECK => 'apply -R',
135 FILTER => 'file-only',
139 DIFF => 'diff-index -p',
140 APPLY => sub { apply_patch_for_checkout_commit '-R', @_ },
141 APPLY_CHECK => 'apply -R',
145 'checkout_nothead' => {
146 DIFF => 'diff-index -R -p',
147 APPLY => sub { apply_patch_for_checkout_commit '', @_ },
148 APPLY_CHECK => 'apply',
154 $patch_mode = 'stage';
155 my %patch_mode_flavour = %{$patch_modes{$patch_mode}};
158 if ($^O eq 'MSWin32') {
159 my @invalid = grep {m/[":*]/} @_;
160 die "$^O does not support: @invalid\n" if @invalid;
161 my @args = map { m/ /o ? "\"$_\"": $_ } @_;
165 open($fh, '-|', @_) or die;
170 my ($GIT_DIR) = run_cmd_pipe(qw(git rev-parse --git-dir));
172 if (!defined $GIT_DIR) {
173 exit(1); # rev-parse would have already said "not a git repo"
179 open $fh, 'git update-index --refresh |'
182 ;# ignore 'needs update'
192 run_cmd_pipe(qw(git ls-files --others --exclude-standard --), @ARGV);
195 # TRANSLATORS: you can adjust this to align "git add -i" status menu
196 my $status_fmt = __('%12s %12s %s');
197 my $status_head = sprintf($status_fmt, __('staged'), __('unstaged'), __('path'));
201 sub is_initial_commit {
202 $initial = system('git rev-parse HEAD -- >/dev/null 2>&1') != 0
203 unless defined $initial;
211 return $empty_tree if defined $empty_tree;
213 $empty_tree = run_cmd_pipe(qw(git hash-object -t tree /dev/null));
219 sub get_diff_reference {
221 if (defined $ref and $ref ne 'HEAD') {
223 } elsif (is_initial_commit()) {
224 return get_empty_tree();
230 # Returns list of hashes, contents of each of which are:
232 # BINARY: is a binary path
233 # INDEX: is index different from HEAD?
234 # FILE: is file different from index?
235 # INDEX_ADDDEL: is it add/delete between HEAD and index?
236 # FILE_ADDDEL: is it add/delete between index and file?
237 # UNMERGED: is the path unmerged
242 my ($add, $del, $adddel, $file);
244 my $reference = get_diff_reference($patch_mode_revision);
245 for (run_cmd_pipe(qw(git diff-index --cached
246 --numstat --summary), $reference,
248 if (($add, $del, $file) =
249 /^([-\d]+) ([-\d]+) (.*)/) {
251 $file = unquote_path($file);
252 if ($add eq '-' && $del eq '-') {
253 $change = __('binary');
257 $change = "+$add/-$del";
262 FILE => __('nothing'),
265 elsif (($adddel, $file) =
266 /^ (create|delete) mode [0-7]+ (.*)$/) {
267 $file = unquote_path($file);
268 $data{$file}{INDEX_ADDDEL} = $adddel;
272 for (run_cmd_pipe(qw(git diff-files --ignore-submodules=dirty --numstat --summary --raw --), @ARGV)) {
273 if (($add, $del, $file) =
274 /^([-\d]+) ([-\d]+) (.*)/) {
275 $file = unquote_path($file);
277 if ($add eq '-' && $del eq '-') {
278 $change = __('binary');
282 $change = "+$add/-$del";
284 $data{$file}{FILE} = $change;
286 $data{$file}{BINARY} = 1;
289 elsif (($adddel, $file) =
290 /^ (create|delete) mode [0-7]+ (.*)$/) {
291 $file = unquote_path($file);
292 $data{$file}{FILE_ADDDEL} = $adddel;
294 elsif (/^:[0-7]+ [0-7]+ [0-9a-f]+ [0-9a-f]+ (.) (.*)$/) {
295 $file = unquote_path($2);
296 if (!exists $data{$file}) {
298 INDEX => __('unchanged'),
303 $data{$file}{UNMERGED} = 1;
308 for (sort keys %data) {
312 if ($only eq 'index-only') {
313 next if ($it->{INDEX} eq __('unchanged'));
315 if ($only eq 'file-only') {
316 next if ($it->{FILE} eq __('nothing'));
328 my ($string, @stuff) = @_;
330 for (my $i = 0; $i < @stuff; $i++) {
334 if ((ref $it) eq 'ARRAY') {
342 if ($it =~ /^$string/) {
346 if (defined $hit && defined $found) {
356 # inserts string into trie and updates count for each character
358 my ($trie, $string) = @_;
359 foreach (split //, $string) {
360 $trie = $trie->{$_} ||= {COUNT => 0};
365 # returns an array of tuples (prefix, remainder)
366 sub find_unique_prefixes {
370 # any single prefix exceeding the soft limit is omitted
371 # if any prefix exceeds the hard limit all are omitted
372 # 0 indicates no limit
376 # build a trie modelling all possible options
378 foreach my $print (@stuff) {
379 if ((ref $print) eq 'ARRAY') {
380 $print = $print->[0];
382 elsif ((ref $print) eq 'HASH') {
383 $print = $print->{VALUE};
385 update_trie(\%trie, $print);
386 push @return, $print;
389 # use the trie to find the unique prefixes
390 for (my $i = 0; $i < @return; $i++) {
391 my $ret = $return[$i];
392 my @letters = split //, $ret;
394 my ($prefix, $remainder);
396 for ($j = 0; $j < @letters; $j++) {
397 my $letter = $letters[$j];
398 if ($search{$letter}{COUNT} == 1) {
399 $prefix = substr $ret, 0, $j + 1;
400 $remainder = substr $ret, $j + 1;
404 my $prefix = substr $ret, 0, $j;
406 if ($hard_limit && $j + 1 > $hard_limit);
408 %search = %{$search{$letter}};
410 if (ord($letters[0]) > 127 ||
411 ($soft_limit && $j + 1 > $soft_limit)) {
415 $return[$i] = [$prefix, $remainder];
420 # filters out prefixes which have special meaning to list_and_choose()
421 sub is_valid_prefix {
423 return (defined $prefix) &&
424 !($prefix =~ /[\s,]/) && # separators
425 !($prefix =~ /^-/) && # deselection
426 !($prefix =~ /^\d+/) && # selection
427 ($prefix ne '*') && # "all" wildcard
428 ($prefix ne '?'); # prompt help
431 # given a prefix/remainder tuple return a string with the prefix highlighted
432 # for now use square brackets; later might use ANSI colors (underline, bold)
433 sub highlight_prefix {
435 my $remainder = shift;
437 if (!defined $prefix) {
441 if (!is_valid_prefix($prefix)) {
442 return "$prefix$remainder";
445 if (!$menu_use_color) {
446 return "[$prefix]$remainder";
449 return "$prompt_color$prefix$normal_color$remainder";
453 print STDERR colored $error_color, @_;
456 sub list_and_choose {
457 my ($opts, @stuff) = @_;
458 my (@chosen, @return);
463 my @prefixes = find_unique_prefixes(@stuff) unless $opts->{LIST_ONLY};
469 if ($opts->{HEADER}) {
470 if (!$opts->{LIST_FLAT}) {
473 print colored $header_color, "$opts->{HEADER}\n";
475 for ($i = 0; $i < @stuff; $i++) {
476 my $chosen = $chosen[$i] ? '*' : ' ';
477 my $print = $stuff[$i];
478 my $ref = ref $print;
479 my $highlighted = highlight_prefix(@{$prefixes[$i]})
481 if ($ref eq 'ARRAY') {
482 $print = $highlighted || $print->[0];
484 elsif ($ref eq 'HASH') {
485 my $value = $highlighted || $print->{VALUE};
486 $print = sprintf($status_fmt,
492 $print = $highlighted || $print;
494 printf("%s%2d: %s", $chosen, $i+1, $print);
495 if (($opts->{LIST_FLAT}) &&
496 (($i + 1) % ($opts->{LIST_FLAT}))) {
509 return if ($opts->{LIST_ONLY});
511 print colored $prompt_color, $opts->{PROMPT};
512 if ($opts->{SINGLETON}) {
521 $opts->{ON_EOF}->() if $opts->{ON_EOF};
528 singleton_prompt_help_cmd() :
532 for my $choice (split(/[\s,]+/, $line)) {
536 # Input that begins with '-'; unchoose
537 if ($choice =~ s/^-//) {
540 # A range can be specified like 5-7 or 5-.
541 if ($choice =~ /^(\d+)-(\d*)$/) {
542 ($bottom, $top) = ($1, length($2) ? $2 : 1 + @stuff);
544 elsif ($choice =~ /^\d+$/) {
545 $bottom = $top = $choice;
547 elsif ($choice eq '*') {
552 $bottom = $top = find_unique($choice, @stuff);
553 if (!defined $bottom) {
554 error_msg sprintf(__("Huh (%s)?\n"), $choice);
558 if ($opts->{SINGLETON} && $bottom != $top) {
559 error_msg sprintf(__("Huh (%s)?\n"), $choice);
562 for ($i = $bottom-1; $i <= $top-1; $i++) {
563 next if (@stuff <= $i || $i < 0);
564 $chosen[$i] = $choose;
567 last if ($opts->{IMMEDIATE} || $line eq '*');
569 for ($i = 0; $i < @stuff; $i++) {
571 push @return, $stuff[$i];
577 sub singleton_prompt_help_cmd {
578 print colored $help_color, __ <<'EOF' ;
580 1 - select a numbered item
581 foo - select item based on unique prefix
582 - (empty) select nothing
586 sub prompt_help_cmd {
587 print colored $help_color, __ <<'EOF' ;
589 1 - select a single item
590 3-5 - select a range of items
591 2-3,6-9 - select multiple ranges
592 foo - select item based on unique prefix
593 -... - unselect specified items
595 - (empty) finish selecting
600 list_and_choose({ LIST_ONLY => 1, HEADER => $status_head },
608 if ($did eq 'added') {
609 printf(__n("added %d path\n", "added %d paths\n",
611 } elsif ($did eq 'updated') {
612 printf(__n("updated %d path\n", "updated %d paths\n",
614 } elsif ($did eq 'reverted') {
615 printf(__n("reverted %d path\n", "reverted %d paths\n",
618 printf(__n("touched %d path\n", "touched %d paths\n",
624 my @mods = list_modified('file-only');
627 my @update = list_and_choose({ PROMPT => __('Update'),
628 HEADER => $status_head, },
631 system(qw(git update-index --add --remove --),
632 map { $_->{VALUE} } @update);
633 say_n_paths('updated', @update);
639 my @update = list_and_choose({ PROMPT => __('Revert'),
640 HEADER => $status_head, },
643 if (is_initial_commit()) {
644 system(qw(git rm --cached),
645 map { $_->{VALUE} } @update);
648 my @lines = run_cmd_pipe(qw(git ls-tree HEAD --),
649 map { $_->{VALUE} } @update);
651 open $fh, '| git update-index --index-info'
658 if ($_->{INDEX_ADDDEL} &&
659 $_->{INDEX_ADDDEL} eq 'create') {
660 system(qw(git update-index --force-remove --),
662 printf(__("note: %s is untracked now.\n"), $_->{VALUE});
667 say_n_paths('reverted', @update);
672 sub add_untracked_cmd {
673 my @add = list_and_choose({ PROMPT => __('Add untracked') },
676 system(qw(git update-index --add --), @add);
677 say_n_paths('added', @add);
679 print __("No untracked files.\n");
687 open $fh, '| git ' . $cmd . " --allow-overlap";
694 my @diff_cmd = split(" ", $patch_mode_flavour{DIFF});
695 if (defined $diff_algorithm) {
696 splice @diff_cmd, 1, 0, "--diff-algorithm=${diff_algorithm}";
698 if (defined $patch_mode_revision) {
699 push @diff_cmd, get_diff_reference($patch_mode_revision);
701 my @diff = run_cmd_pipe("git", @diff_cmd, "--", $path);
703 if ($diff_use_color) {
704 my @display_cmd = ("git", @diff_cmd, qw(--color --), $path);
705 if (defined $diff_filter) {
706 # quotemeta is overkill, but sufficient for shell-quoting
707 my $diff = join(' ', map { quotemeta } @display_cmd);
708 @display_cmd = ("$diff | $diff_filter");
711 @colored = run_cmd_pipe(@display_cmd);
713 my (@hunk) = { TEXT => [], DISPLAY => [], TYPE => 'header' };
715 if (@colored && @colored != @diff) {
717 "fatal: mismatched output from interactive.diffFilter\n",
718 "hint: Your filter must maintain a one-to-one correspondence\n",
719 "hint: between its input and output lines.\n";
723 for (my $i = 0; $i < @diff; $i++) {
724 if ($diff[$i] =~ /^@@ /) {
725 push @hunk, { TEXT => [], DISPLAY => [],
728 push @{$hunk[-1]{TEXT}}, $diff[$i];
729 push @{$hunk[-1]{DISPLAY}},
730 (@colored ? $colored[$i] : $diff[$i]);
735 sub parse_diff_header {
738 my $head = { TEXT => [], DISPLAY => [], TYPE => 'header' };
739 my $mode = { TEXT => [], DISPLAY => [], TYPE => 'mode' };
740 my $deletion = { TEXT => [], DISPLAY => [], TYPE => 'deletion' };
742 for (my $i = 0; $i < @{$src->{TEXT}}; $i++) {
744 $src->{TEXT}->[$i] =~ /^(old|new) mode (\d+)$/ ? $mode :
745 $src->{TEXT}->[$i] =~ /^deleted file/ ? $deletion :
747 push @{$dest->{TEXT}}, $src->{TEXT}->[$i];
748 push @{$dest->{DISPLAY}}, $src->{DISPLAY}->[$i];
750 return ($head, $mode, $deletion);
753 sub hunk_splittable {
756 my @s = split_hunk($text);
760 sub parse_hunk_header {
762 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
763 $line =~ /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
764 $o_cnt = 1 unless defined $o_cnt;
765 $n_cnt = 1 unless defined $n_cnt;
766 return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
769 sub format_hunk_header {
770 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = @_;
771 return ("@@ -$o_ofs" .
772 (($o_cnt != 1) ? ",$o_cnt" : '') .
774 (($n_cnt != 1) ? ",$n_cnt" : '') .
779 my ($text, $display) = @_;
781 if (!defined $display) {
784 # If there are context lines in the middle of a hunk,
785 # it can be split, but we would need to take care of
788 my ($o_ofs, undef, $n_ofs) = parse_hunk_header($text->[0]);
793 my $next_hunk_start = undef;
794 my $i = $hunk_start - 1;
808 while (++$i < @$text) {
809 my $line = $text->[$i];
810 my $display = $display->[$i];
811 if ($line =~ /^\\/) {
812 push @{$this->{TEXT}}, $line;
813 push @{$this->{DISPLAY}}, $display;
817 if ($this->{ADDDEL} &&
818 !defined $next_hunk_start) {
819 # We have seen leading context and
820 # adds/dels and then here is another
821 # context, which is trailing for this
822 # split hunk and leading for the next
824 $next_hunk_start = $i;
826 push @{$this->{TEXT}}, $line;
827 push @{$this->{DISPLAY}}, $display;
830 if (defined $next_hunk_start) {
837 if (defined $next_hunk_start) {
838 # We are done with the current hunk and
839 # this is the first real change for the
841 $hunk_start = $next_hunk_start;
842 $o_ofs = $this->{OLD} + $this->{OCNT};
843 $n_ofs = $this->{NEW} + $this->{NCNT};
844 $o_ofs -= $this->{POSTCTX};
845 $n_ofs -= $this->{POSTCTX};
849 push @{$this->{TEXT}}, $line;
850 push @{$this->{DISPLAY}}, $display;
864 for my $hunk (@split) {
865 $o_ofs = $hunk->{OLD};
866 $n_ofs = $hunk->{NEW};
867 my $o_cnt = $hunk->{OCNT};
868 my $n_cnt = $hunk->{NCNT};
870 my $head = format_hunk_header($o_ofs, $o_cnt, $n_ofs, $n_cnt);
871 my $display_head = $head;
872 unshift @{$hunk->{TEXT}}, $head;
873 if ($diff_use_color) {
874 $display_head = colored($fraginfo_color, $head);
876 unshift @{$hunk->{DISPLAY}}, $display_head;
881 sub find_last_o_ctx {
883 my $text = $it->{TEXT};
884 my ($o_ofs, $o_cnt) = parse_hunk_header($text->[0]);
886 my $last_o_ctx = $o_ofs + $o_cnt;
888 my $line = $text->[$i];
899 my ($prev, $this) = @_;
900 my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) =
901 parse_hunk_header($prev->{TEXT}[0]);
902 my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) =
903 parse_hunk_header($this->{TEXT}[0]);
905 my (@line, $i, $ofs, $o_cnt, $n_cnt);
908 for ($i = 1; $i < @{$prev->{TEXT}}; $i++) {
909 my $line = $prev->{TEXT}[$i];
910 if ($line =~ /^\+/) {
914 } elsif ($line =~ /^\\/) {
919 last if ($o1_ofs <= $ofs);
929 for ($i = 1; $i < @{$this->{TEXT}}; $i++) {
930 my $line = $this->{TEXT}[$i];
931 if ($line =~ /^\+/) {
935 } elsif ($line =~ /^\\/) {
946 my $head = format_hunk_header($o0_ofs, $o_cnt, $n0_ofs, $n_cnt);
947 @{$prev->{TEXT}} = ($head, @line);
950 sub coalesce_overlapping_hunks {
954 my ($last_o_ctx, $last_was_dirty);
958 if ($_->{TYPE} ne 'hunk') {
962 my $text = $_->{TEXT};
963 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
964 parse_hunk_header($text->[0]);
966 $ofs_delta += $o_cnt - $n_cnt;
967 # If this hunk has been edited then subtract
968 # the delta that is due to the edit.
969 if ($_->{OFS_DELTA}) {
970 $ofs_delta -= $_->{OFS_DELTA};
975 $n_ofs += $ofs_delta;
976 $_->{TEXT}->[0] = format_hunk_header($o_ofs, $o_cnt,
979 # If this hunk was edited then adjust the offset delta
980 # to reflect the edit.
981 if ($_->{OFS_DELTA}) {
982 $ofs_delta += $_->{OFS_DELTA};
984 if (defined $last_o_ctx &&
985 $o_ofs <= $last_o_ctx &&
988 merge_hunk($out[-1], $_);
993 $last_o_ctx = find_last_o_ctx($out[-1]);
994 $last_was_dirty = $_->{DIRTY};
999 sub reassemble_patch {
1003 # Include everything in the header except the beginning of the diff.
1004 push @patch, (grep { !/^[-+]{3}/ } @$head);
1006 # Then include any headers from the hunk lines, which must
1007 # come before any actual hunk.
1008 while (@_ && $_[0] !~ /^@/) {
1012 # Then begin the diff.
1013 push @patch, grep { /^[-+]{3}/ } @$head;
1015 # And then the actual hunks.
1023 colored((/^@/ ? $fraginfo_color :
1024 /^\+/ ? $diff_new_color :
1025 /^-/ ? $diff_old_color :
1031 my %edit_hunk_manually_modes = (
1033 "If the patch applies cleanly, the edited hunk will immediately be
1034 marked for staging."),
1036 "If the patch applies cleanly, the edited hunk will immediately be
1037 marked for stashing."),
1039 "If the patch applies cleanly, the edited hunk will immediately be
1040 marked for unstaging."),
1041 reset_nothead => N__(
1042 "If the patch applies cleanly, the edited hunk will immediately be
1043 marked for applying."),
1044 checkout_index => N__(
1045 "If the patch applies cleanly, the edited hunk will immediately be
1046 marked for discarding."),
1047 checkout_head => N__(
1048 "If the patch applies cleanly, the edited hunk will immediately be
1049 marked for discarding."),
1050 checkout_nothead => N__(
1051 "If the patch applies cleanly, the edited hunk will immediately be
1052 marked for applying."),
1055 sub recount_edited_hunk {
1057 my ($oldtext, $newtext) = @_;
1058 my ($o_cnt, $n_cnt) = (0, 0);
1059 for (@{$newtext}[1..$#{$newtext}]) {
1060 my $mode = substr($_, 0, 1);
1063 } elsif ($mode eq '+') {
1065 } elsif ($mode eq ' ') {
1070 my ($o_ofs, undef, $n_ofs, undef) =
1071 parse_hunk_header($newtext->[0]);
1072 $newtext->[0] = format_hunk_header($o_ofs, $o_cnt, $n_ofs, $n_cnt);
1073 my (undef, $orig_o_cnt, undef, $orig_n_cnt) =
1074 parse_hunk_header($oldtext->[0]);
1075 # Return the change in the number of lines inserted by this hunk
1076 return $orig_o_cnt - $orig_n_cnt - $o_cnt + $n_cnt;
1079 sub edit_hunk_manually {
1082 my $hunkfile = $repo->repo_path . "/addp-hunk-edit.diff";
1084 open $fh, '>', $hunkfile
1085 or die sprintf(__("failed to open hunk edit file for writing: %s"), $!);
1086 print $fh Git::comment_lines __("Manual hunk edit mode -- see bottom for a quick guide.\n");
1087 print $fh @$oldtext;
1088 my $is_reverse = $patch_mode_flavour{IS_REVERSE};
1089 my ($remove_plus, $remove_minus) = $is_reverse ? ('-', '+') : ('+', '-');
1090 my $comment_line_char = Git::get_comment_line_char;
1091 print $fh Git::comment_lines sprintf(__ <<EOF, $remove_minus, $remove_plus, $comment_line_char),
1093 To remove '%s' lines, make them ' ' lines (context).
1094 To remove '%s' lines, delete them.
1095 Lines starting with %s will be removed.
1097 __($edit_hunk_manually_modes{$patch_mode}),
1098 # TRANSLATORS: 'it' refers to the patch mentioned in the previous messages.
1100 If it does not apply cleanly, you will be given an opportunity to
1101 edit again. If all lines of the hunk are removed, then the edit is
1102 aborted and the hunk is left unchanged.
1106 chomp(my $editor = run_cmd_pipe(qw(git var GIT_EDITOR)));
1107 system('sh', '-c', $editor.' "$@"', $editor, $hunkfile);
1113 open $fh, '<', $hunkfile
1114 or die sprintf(__("failed to open hunk edit file for reading: %s"), $!);
1115 my @newtext = grep { !/^\Q$comment_line_char\E/ } <$fh>;
1119 # Abort if nothing remains
1120 if (!grep { /\S/ } @newtext) {
1124 # Reinsert the first hunk header if the user accidentally deleted it
1125 if ($newtext[0] !~ /^@/) {
1126 unshift @newtext, $oldtext->[0];
1132 return run_git_apply($patch_mode_flavour{APPLY_CHECK} . ' --check',
1133 map { @{$_->{TEXT}} } @_);
1136 sub _restore_terminal_and_die {
1142 sub prompt_single_character {
1144 local $SIG{TERM} = \&_restore_terminal_and_die;
1145 local $SIG{INT} = \&_restore_terminal_and_die;
1147 my $key = ReadKey 0;
1149 if ($use_termcap and $key eq "\e") {
1150 while (!defined $term_escapes{$key}) {
1151 my $next = ReadKey 0.5;
1152 last if (!defined $next);
1157 print "$key" if defined $key;
1168 print colored $prompt_color, $prompt;
1169 my $line = prompt_single_character;
1170 return undef unless defined $line;
1171 return 0 if $line =~ /^n/i;
1172 return 1 if $line =~ /^y/i;
1176 sub edit_hunk_loop {
1177 my ($head, $hunks, $ix) = @_;
1178 my $hunk = $hunks->[$ix];
1179 my $text = $hunk->{TEXT};
1182 my $newtext = edit_hunk_manually($text);
1183 if (!defined $newtext) {
1188 TYPE => $hunk->{TYPE},
1192 $newhunk->{OFS_DELTA} = recount_edited_hunk($text, $newtext);
1193 # If this hunk has already been edited then add the
1194 # offset delta of the previous edit to get the real
1195 # delta from the original unedited hunk.
1196 $hunk->{OFS_DELTA} and
1197 $newhunk->{OFS_DELTA} += $hunk->{OFS_DELTA};
1198 if (diff_applies($head,
1199 @{$hunks}[0..$ix-1],
1201 @{$hunks}[$ix+1..$#{$hunks}])) {
1202 $newhunk->{DISPLAY} = [color_diff(@{$newtext})];
1207 # TRANSLATORS: do not translate [y/n]
1208 # The program will only accept that input
1210 # Consider translating (saying "no" discards!) as
1211 # (saying "n" for "no" discards!) if the translation
1212 # of the word "no" does not start with n.
1213 __('Your edited hunk does not apply. Edit again '
1214 . '(saying "no" discards!) [y/n]? ')
1220 my %help_patch_modes = (
1222 "y - stage this hunk
1223 n - do not stage this hunk
1224 q - quit; do not stage this hunk or any of the remaining ones
1225 a - stage this hunk and all later hunks in the file
1226 d - do not stage this hunk or any of the later hunks in the file"),
1228 "y - stash this hunk
1229 n - do not stash this hunk
1230 q - quit; do not stash this hunk or any of the remaining ones
1231 a - stash this hunk and all later hunks in the file
1232 d - do not stash this hunk or any of the later hunks in the file"),
1234 "y - unstage this hunk
1235 n - do not unstage this hunk
1236 q - quit; do not unstage this hunk or any of the remaining ones
1237 a - unstage this hunk and all later hunks in the file
1238 d - do not unstage this hunk or any of the later hunks in the file"),
1239 reset_nothead => N__(
1240 "y - apply this hunk to index
1241 n - do not apply this hunk to index
1242 q - quit; do not apply this hunk or any of the remaining ones
1243 a - apply this hunk and all later hunks in the file
1244 d - do not apply this hunk or any of the later hunks in the file"),
1245 checkout_index => N__(
1246 "y - discard this hunk from worktree
1247 n - do not discard this hunk from worktree
1248 q - quit; do not discard this hunk or any of the remaining ones
1249 a - discard this hunk and all later hunks in the file
1250 d - do not discard this hunk or any of the later hunks in the file"),
1251 checkout_head => N__(
1252 "y - discard this hunk from index and worktree
1253 n - do not discard this hunk from index and worktree
1254 q - quit; do not discard this hunk or any of the remaining ones
1255 a - discard this hunk and all later hunks in the file
1256 d - do not discard this hunk or any of the later hunks in the file"),
1257 checkout_nothead => N__(
1258 "y - apply this hunk to index and worktree
1259 n - do not apply this hunk to index and worktree
1260 q - quit; do not apply this hunk or any of the remaining ones
1261 a - apply this hunk and all later hunks in the file
1262 d - do not apply this hunk or any of the later hunks in the file"),
1265 sub help_patch_cmd {
1267 my $other = $_[0] . ",?";
1268 print colored $help_color, __($help_patch_modes{$patch_mode}), "\n",
1269 map { "$_\n" } grep {
1270 my $c = quotemeta(substr($_, 0, 1));
1272 } split "\n", __ <<EOF ;
1273 g - select a hunk to go to
1274 / - search for a hunk matching the given regex
1275 j - leave this hunk undecided, see next undecided hunk
1276 J - leave this hunk undecided, see next hunk
1277 k - leave this hunk undecided, see previous undecided hunk
1278 K - leave this hunk undecided, see previous hunk
1279 s - split the current hunk into smaller hunks
1280 e - manually edit the current hunk
1287 my $ret = run_git_apply $cmd, @_;
1294 sub apply_patch_for_checkout_commit {
1295 my $reverse = shift;
1296 my $applies_index = run_git_apply 'apply '.$reverse.' --cached --check', @_;
1297 my $applies_worktree = run_git_apply 'apply '.$reverse.' --check', @_;
1299 if ($applies_worktree && $applies_index) {
1300 run_git_apply 'apply '.$reverse.' --cached', @_;
1301 run_git_apply 'apply '.$reverse, @_;
1303 } elsif (!$applies_index) {
1304 print colored $error_color, __("The selected hunks do not apply to the index!\n");
1305 if (prompt_yesno __("Apply them to the worktree anyway? ")) {
1306 return run_git_apply 'apply '.$reverse, @_;
1308 print colored $error_color, __("Nothing was applied.\n");
1317 sub patch_update_cmd {
1318 my @all_mods = list_modified($patch_mode_flavour{FILTER});
1319 error_msg sprintf(__("ignoring unmerged: %s\n"), $_->{VALUE})
1320 for grep { $_->{UNMERGED} } @all_mods;
1321 @all_mods = grep { !$_->{UNMERGED} } @all_mods;
1323 my @mods = grep { !($_->{BINARY}) } @all_mods;
1328 print STDERR __("Only binary files changed.\n");
1330 print STDERR __("No changes.\n");
1334 if ($patch_mode_only) {
1338 @them = list_and_choose({ PROMPT => __('Patch update'),
1339 HEADER => $status_head, },
1343 return 0 if patch_update_file($_->{VALUE});
1347 # Generate a one line summary of a hunk.
1348 sub summarize_hunk {
1350 my $summary = $rhunk->{TEXT}[0];
1352 # Keep the line numbers, discard extra context.
1353 $summary =~ s/@@(.*?)@@.*/$1 /s;
1354 $summary .= " " x (20 - length $summary);
1356 # Add some user context.
1357 for my $line (@{$rhunk->{TEXT}}) {
1358 if ($line =~ m/^[+-].*\w/) {
1365 return substr($summary, 0, 80) . "\n";
1369 # Print a one-line summary of each hunk in the array ref in
1370 # the first argument, starting with the index in the 2nd.
1372 my ($hunks, $i) = @_;
1375 for (; $i < @$hunks && $ctr < 20; $i++, $ctr++) {
1377 if (defined $hunks->[$i]{USE}) {
1378 $status = $hunks->[$i]{USE} ? "+" : "-";
1383 summarize_hunk($hunks->[$i]);
1388 my %patch_update_prompt_modes = (
1390 mode => N__("Stage mode change [y,n,q,a,d%s,?]? "),
1391 deletion => N__("Stage deletion [y,n,q,a,d%s,?]? "),
1392 hunk => N__("Stage this hunk [y,n,q,a,d%s,?]? "),
1395 mode => N__("Stash mode change [y,n,q,a,d%s,?]? "),
1396 deletion => N__("Stash deletion [y,n,q,a,d%s,?]? "),
1397 hunk => N__("Stash this hunk [y,n,q,a,d%s,?]? "),
1400 mode => N__("Unstage mode change [y,n,q,a,d%s,?]? "),
1401 deletion => N__("Unstage deletion [y,n,q,a,d%s,?]? "),
1402 hunk => N__("Unstage this hunk [y,n,q,a,d%s,?]? "),
1405 mode => N__("Apply mode change to index [y,n,q,a,d%s,?]? "),
1406 deletion => N__("Apply deletion to index [y,n,q,a,d%s,?]? "),
1407 hunk => N__("Apply this hunk to index [y,n,q,a,d%s,?]? "),
1410 mode => N__("Discard mode change from worktree [y,n,q,a,d%s,?]? "),
1411 deletion => N__("Discard deletion from worktree [y,n,q,a,d%s,?]? "),
1412 hunk => N__("Discard this hunk from worktree [y,n,q,a,d%s,?]? "),
1415 mode => N__("Discard mode change from index and worktree [y,n,q,a,d%s,?]? "),
1416 deletion => N__("Discard deletion from index and worktree [y,n,q,a,d%s,?]? "),
1417 hunk => N__("Discard this hunk from index and worktree [y,n,q,a,d%s,?]? "),
1419 checkout_nothead => {
1420 mode => N__("Apply mode change to index and worktree [y,n,q,a,d%s,?]? "),
1421 deletion => N__("Apply deletion to index and worktree [y,n,q,a,d%s,?]? "),
1422 hunk => N__("Apply this hunk to index and worktree [y,n,q,a,d%s,?]? "),
1426 sub patch_update_file {
1430 my ($head, @hunk) = parse_diff($path);
1431 ($head, my $mode, my $deletion) = parse_diff_header($head);
1432 for (@{$head->{DISPLAY}}) {
1436 if (@{$mode->{TEXT}}) {
1437 unshift @hunk, $mode;
1439 if (@{$deletion->{TEXT}}) {
1440 foreach my $hunk (@hunk) {
1441 push @{$deletion->{TEXT}}, @{$hunk->{TEXT}};
1442 push @{$deletion->{DISPLAY}}, @{$hunk->{DISPLAY}};
1444 @hunk = ($deletion);
1447 $num = scalar @hunk;
1451 my ($prev, $next, $other, $undecided, $i);
1457 for ($i = 0; $i < $ix; $i++) {
1458 if (!defined $hunk[$i]{USE}) {
1467 for ($i = $ix + 1; $i < $num; $i++) {
1468 if (!defined $hunk[$i]{USE}) {
1474 if ($ix < $num - 1) {
1480 for ($i = 0; $i < $num; $i++) {
1481 if (!defined $hunk[$i]{USE}) {
1486 last if (!$undecided);
1488 if ($hunk[$ix]{TYPE} eq 'hunk' &&
1489 hunk_splittable($hunk[$ix]{TEXT})) {
1492 if ($hunk[$ix]{TYPE} eq 'hunk') {
1495 for (@{$hunk[$ix]{DISPLAY}}) {
1498 print colored $prompt_color,
1499 sprintf(__($patch_update_prompt_modes{$patch_mode}{$hunk[$ix]{TYPE}}), $other);
1501 my $line = prompt_single_character;
1502 last unless defined $line;
1504 if ($line =~ /^y/i) {
1505 $hunk[$ix]{USE} = 1;
1507 elsif ($line =~ /^n/i) {
1508 $hunk[$ix]{USE} = 0;
1510 elsif ($line =~ /^a/i) {
1511 while ($ix < $num) {
1512 if (!defined $hunk[$ix]{USE}) {
1513 $hunk[$ix]{USE} = 1;
1519 elsif ($line =~ /^g(.*)/) {
1521 unless ($other =~ /g/) {
1522 error_msg __("No other hunks to goto\n");
1525 my $no = $ix > 10 ? $ix - 10 : 0;
1526 while ($response eq '') {
1527 $no = display_hunks(\@hunk, $no);
1529 print __("go to which hunk (<ret> to see more)? ");
1531 print __("go to which hunk? ");
1533 $response = <STDIN>;
1534 if (!defined $response) {
1539 if ($response !~ /^\s*\d+\s*$/) {
1540 error_msg sprintf(__("Invalid number: '%s'\n"),
1542 } elsif (0 < $response && $response <= $num) {
1543 $ix = $response - 1;
1545 error_msg sprintf(__n("Sorry, only %d hunk available.\n",
1546 "Sorry, only %d hunks available.\n", $num), $num);
1550 elsif ($line =~ /^d/i) {
1551 while ($ix < $num) {
1552 if (!defined $hunk[$ix]{USE}) {
1553 $hunk[$ix]{USE} = 0;
1559 elsif ($line =~ /^q/i) {
1560 for ($i = 0; $i < $num; $i++) {
1561 if (!defined $hunk[$i]{USE}) {
1568 elsif ($line =~ m|^/(.*)|) {
1570 unless ($other =~ m|/|) {
1571 error_msg __("No other hunks to search\n");
1575 print colored $prompt_color, __("search for regex? ");
1577 if (defined $regex) {
1583 $search_string = qr{$regex}m;
1586 my ($err,$exp) = ($@, $1);
1587 $err =~ s/ at .*git-add--interactive line \d+, <STDIN> line \d+.*$//;
1588 error_msg sprintf(__("Malformed search regexp %s: %s\n"), $exp, $err);
1593 my $text = join ("", @{$hunk[$iy]{TEXT}});
1594 last if ($text =~ $search_string);
1596 $iy = 0 if ($iy >= $num);
1598 error_msg __("No hunk matches the given pattern\n");
1605 elsif ($line =~ /^K/) {
1606 if ($other =~ /K/) {
1610 error_msg __("No previous hunk\n");
1614 elsif ($line =~ /^J/) {
1615 if ($other =~ /J/) {
1619 error_msg __("No next hunk\n");
1623 elsif ($line =~ /^k/) {
1624 if ($other =~ /k/) {
1628 !defined $hunk[$ix]{USE});
1632 error_msg __("No previous hunk\n");
1636 elsif ($line =~ /^j/) {
1637 if ($other !~ /j/) {
1638 error_msg __("No next hunk\n");
1642 elsif ($line =~ /^s/) {
1643 unless ($other =~ /s/) {
1644 error_msg __("Sorry, cannot split this hunk\n");
1647 my @split = split_hunk($hunk[$ix]{TEXT}, $hunk[$ix]{DISPLAY});
1649 print colored $header_color, sprintf(
1650 __n("Split into %d hunk.\n",
1651 "Split into %d hunks.\n",
1652 scalar(@split)), scalar(@split));
1654 splice (@hunk, $ix, 1, @split);
1655 $num = scalar @hunk;
1658 elsif ($line =~ /^e/) {
1659 unless ($other =~ /e/) {
1660 error_msg __("Sorry, cannot edit this hunk\n");
1663 my $newhunk = edit_hunk_loop($head, \@hunk, $ix);
1664 if (defined $newhunk) {
1665 splice @hunk, $ix, 1, $newhunk;
1669 help_patch_cmd($other);
1675 last if ($ix >= $num ||
1676 !defined $hunk[$ix]{USE});
1681 @hunk = coalesce_overlapping_hunks(@hunk);
1687 push @result, @{$_->{TEXT}};
1692 my @patch = reassemble_patch($head->{TEXT}, @result);
1693 my $apply_routine = $patch_mode_flavour{APPLY};
1694 &$apply_routine(@patch);
1703 my @mods = list_modified('index-only');
1704 @mods = grep { !($_->{BINARY}) } @mods;
1706 my (@them) = list_and_choose({ PROMPT => __('Review diff'),
1708 HEADER => $status_head, },
1711 my $reference = (is_initial_commit()) ? get_empty_tree() : 'HEAD';
1712 system(qw(git diff -p --cached), $reference, '--',
1713 map { $_->{VALUE} } @them);
1722 # TRANSLATORS: please do not translate the command names
1723 # 'status', 'update', 'revert', etc.
1724 print colored $help_color, __ <<'EOF' ;
1725 status - show paths with changes
1726 update - add working tree state to the staged set of changes
1727 revert - revert staged set of changes back to the HEAD version
1728 patch - pick hunks and update selectively
1729 diff - view diff between HEAD and index
1730 add untracked - add contents of untracked files to the staged set of changes
1735 return unless @ARGV;
1736 my $arg = shift @ARGV;
1737 if ($arg =~ /--patch(?:=(.*))?/) {
1739 if ($1 eq 'reset') {
1740 $patch_mode = 'reset_head';
1741 $patch_mode_revision = 'HEAD';
1742 $arg = shift @ARGV or die __("missing --");
1744 $patch_mode_revision = $arg;
1745 $patch_mode = ($arg eq 'HEAD' ?
1746 'reset_head' : 'reset_nothead');
1747 $arg = shift @ARGV or die __("missing --");
1749 } elsif ($1 eq 'checkout') {
1750 $arg = shift @ARGV or die __("missing --");
1752 $patch_mode = 'checkout_index';
1754 $patch_mode_revision = $arg;
1755 $patch_mode = ($arg eq 'HEAD' ?
1756 'checkout_head' : 'checkout_nothead');
1757 $arg = shift @ARGV or die __("missing --");
1759 } elsif ($1 eq 'stage' or $1 eq 'stash') {
1761 $arg = shift @ARGV or die __("missing --");
1763 die sprintf(__("unknown --patch mode: %s"), $1);
1766 $patch_mode = 'stage';
1767 $arg = shift @ARGV or die __("missing --");
1769 die sprintf(__("invalid argument %s, expecting --"),
1770 $arg) unless $arg eq "--";
1771 %patch_mode_flavour = %{$patch_modes{$patch_mode}};
1772 $patch_mode_only = 1;
1774 elsif ($arg ne "--") {
1775 die sprintf(__("invalid argument %s, expecting --"), $arg);
1780 my @cmd = ([ 'status', \&status_cmd, ],
1781 [ 'update', \&update_cmd, ],
1782 [ 'revert', \&revert_cmd, ],
1783 [ 'add untracked', \&add_untracked_cmd, ],
1784 [ 'patch', \&patch_update_cmd, ],
1785 [ 'diff', \&diff_cmd, ],
1786 [ 'quit', \&quit_cmd, ],
1787 [ 'help', \&help_cmd, ],
1790 my ($it) = list_and_choose({ PROMPT => __('What now'),
1793 HEADER => __('*** Commands ***'),
1794 ON_EOF => \&quit_cmd,
1795 IMMEDIATE => 1 }, @cmd);
1809 if ($patch_mode_only) {