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_indent_heuristic = $repo->config_bool('diff.indentheuristic');
50 my $diff_filter = $repo->config('interactive.difffilter');
58 if ($repo->config_bool("interactive.singlekey")) {
60 require Term
::ReadKey
;
61 Term
::ReadKey
->import;
65 print STDERR
"missing Term::ReadKey, disabling interactive.singlekey\n";
69 my $termcap = Term
::Cap
->Tgetent;
70 foreach (values %$termcap) {
71 $term_escapes{$_} = 1 if /^\e/;
79 my $string = join("", @_);
82 # Put a color code at the beginning of each line, a reset at the end
83 # color after newlines that are not at the end of the string
84 $string =~ s/(\n+)(.)/$1$color$2/g;
85 # reset before newlines
86 $string =~ s/(\n+)/$normal_color$1/g;
87 # codes at beginning and end (if necessary):
88 $string =~ s/^/$color/;
89 $string =~ s/$/$normal_color/ unless $string =~ /\n$/;
94 # command line options
97 my $patch_mode_revision;
100 sub apply_patch_for_checkout_commit
;
101 sub apply_patch_for_stash
;
105 DIFF
=> 'diff-files -p',
106 APPLY
=> sub { apply_patch
'apply --cached', @_; },
107 APPLY_CHECK
=> 'apply --cached',
108 FILTER
=> 'file-only',
112 DIFF
=> 'diff-index -p HEAD',
113 APPLY
=> sub { apply_patch
'apply --cached', @_; },
114 APPLY_CHECK
=> 'apply --cached',
119 DIFF
=> 'diff-index -p --cached',
120 APPLY
=> sub { apply_patch
'apply -R --cached', @_; },
121 APPLY_CHECK
=> 'apply -R --cached',
122 FILTER
=> 'index-only',
126 DIFF
=> 'diff-index -R -p --cached',
127 APPLY
=> sub { apply_patch
'apply --cached', @_; },
128 APPLY_CHECK
=> 'apply --cached',
129 FILTER
=> 'index-only',
132 'checkout_index' => {
133 DIFF
=> 'diff-files -p',
134 APPLY
=> sub { apply_patch
'apply -R', @_; },
135 APPLY_CHECK
=> 'apply -R',
136 FILTER
=> 'file-only',
140 DIFF
=> 'diff-index -p',
141 APPLY
=> sub { apply_patch_for_checkout_commit
'-R', @_ },
142 APPLY_CHECK
=> 'apply -R',
146 'checkout_nothead' => {
147 DIFF
=> 'diff-index -R -p',
148 APPLY
=> sub { apply_patch_for_checkout_commit
'', @_ },
149 APPLY_CHECK
=> 'apply',
155 $patch_mode = 'stage';
156 my %patch_mode_flavour = %{$patch_modes{$patch_mode}};
159 if ($^O
eq 'MSWin32') {
160 my @invalid = grep {m/[":*]/} @_;
161 die "$^O does not support: @invalid\n" if @invalid;
162 my @args = map { m/ /o ?
"\"$_\"": $_ } @_;
166 open($fh, '-|', @_) or die;
171 my ($GIT_DIR) = run_cmd_pipe
(qw(git rev-parse --git-dir));
173 if (!defined $GIT_DIR) {
174 exit(1); # rev-parse would have already said "not a git repo"
191 my ($retval, $remainder);
192 if (!/^\042(.*)\042$/) {
195 ($_, $retval) = ($1, "");
196 while (/^([^\\]*)\\(.*)$/) {
200 if (/^([0-3][0-7][0-7])(.*)$/) {
201 $retval .= chr(oct($1));
205 if (/^([\\\042btnvfr])(.*)$/) {
206 $retval .= $cquote_map{$1};
210 # This is malformed -- just return it as-is for now.
221 open $fh, 'git update-index --refresh |'
224 ;# ignore 'needs update'
234 run_cmd_pipe
(qw(git ls-files --others --exclude-standard --), @ARGV);
237 # TRANSLATORS: you can adjust this to align "git add -i" status menu
238 my $status_fmt = __
('%12s %12s %s');
239 my $status_head = sprintf($status_fmt, __
('staged'), __
('unstaged'), __
('path'));
243 sub is_initial_commit
{
244 $initial = system('git rev-parse HEAD -- >/dev/null 2>&1') != 0
245 unless defined $initial;
251 return '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
254 sub get_diff_reference
{
256 if (defined $ref and $ref ne 'HEAD') {
258 } elsif (is_initial_commit
()) {
259 return get_empty_tree
();
265 # Returns list of hashes, contents of each of which are:
267 # BINARY: is a binary path
268 # INDEX: is index different from HEAD?
269 # FILE: is file different from index?
270 # INDEX_ADDDEL: is it add/delete between HEAD and index?
271 # FILE_ADDDEL: is it add/delete between index and file?
272 # UNMERGED: is the path unmerged
277 my ($add, $del, $adddel, $file);
279 my $reference = get_diff_reference
($patch_mode_revision);
280 for (run_cmd_pipe
(qw(git diff-index --cached
281 --numstat --summary), $reference,
283 if (($add, $del, $file) =
284 /^([-\d]+) ([-\d]+) (.*)/) {
286 $file = unquote_path
($file);
287 if ($add eq '-' && $del eq '-') {
288 $change = __
('binary');
292 $change = "+$add/-$del";
297 FILE
=> __
('nothing'),
300 elsif (($adddel, $file) =
301 /^ (create|delete) mode [0-7]+ (.*)$/) {
302 $file = unquote_path
($file);
303 $data{$file}{INDEX_ADDDEL
} = $adddel;
307 for (run_cmd_pipe
(qw(git diff-files --numstat --summary --raw --), @ARGV)) {
308 if (($add, $del, $file) =
309 /^([-\d]+) ([-\d]+) (.*)/) {
310 $file = unquote_path
($file);
312 if ($add eq '-' && $del eq '-') {
313 $change = __
('binary');
317 $change = "+$add/-$del";
319 $data{$file}{FILE
} = $change;
321 $data{$file}{BINARY
} = 1;
324 elsif (($adddel, $file) =
325 /^ (create|delete) mode [0-7]+ (.*)$/) {
326 $file = unquote_path
($file);
327 $data{$file}{FILE_ADDDEL
} = $adddel;
329 elsif (/^:[0-7]+ [0-7]+ [0-9a-f]+ [0-9a-f]+ (.) (.*)$/) {
330 $file = unquote_path
($2);
331 if (!exists $data{$file}) {
333 INDEX
=> __
('unchanged'),
338 $data{$file}{UNMERGED
} = 1;
343 for (sort keys %data) {
347 if ($only eq 'index-only') {
348 next if ($it->{INDEX
} eq __
('unchanged'));
350 if ($only eq 'file-only') {
351 next if ($it->{FILE
} eq __
('nothing'));
363 my ($string, @stuff) = @_;
365 for (my $i = 0; $i < @stuff; $i++) {
369 if ((ref $it) eq 'ARRAY') {
377 if ($it =~ /^$string/) {
381 if (defined $hit && defined $found) {
391 # inserts string into trie and updates count for each character
393 my ($trie, $string) = @_;
394 foreach (split //, $string) {
395 $trie = $trie->{$_} ||= {COUNT
=> 0};
400 # returns an array of tuples (prefix, remainder)
401 sub find_unique_prefixes
{
405 # any single prefix exceeding the soft limit is omitted
406 # if any prefix exceeds the hard limit all are omitted
407 # 0 indicates no limit
411 # build a trie modelling all possible options
413 foreach my $print (@stuff) {
414 if ((ref $print) eq 'ARRAY') {
415 $print = $print->[0];
417 elsif ((ref $print) eq 'HASH') {
418 $print = $print->{VALUE
};
420 update_trie
(\
%trie, $print);
421 push @return, $print;
424 # use the trie to find the unique prefixes
425 for (my $i = 0; $i < @return; $i++) {
426 my $ret = $return[$i];
427 my @letters = split //, $ret;
429 my ($prefix, $remainder);
431 for ($j = 0; $j < @letters; $j++) {
432 my $letter = $letters[$j];
433 if ($search{$letter}{COUNT
} == 1) {
434 $prefix = substr $ret, 0, $j + 1;
435 $remainder = substr $ret, $j + 1;
439 my $prefix = substr $ret, 0, $j;
441 if ($hard_limit && $j + 1 > $hard_limit);
443 %search = %{$search{$letter}};
445 if (ord($letters[0]) > 127 ||
446 ($soft_limit && $j + 1 > $soft_limit)) {
450 $return[$i] = [$prefix, $remainder];
455 # filters out prefixes which have special meaning to list_and_choose()
456 sub is_valid_prefix
{
458 return (defined $prefix) &&
459 !($prefix =~ /[\s,]/) && # separators
460 !($prefix =~ /^-/) && # deselection
461 !($prefix =~ /^\d+/) && # selection
462 ($prefix ne '*') && # "all" wildcard
463 ($prefix ne '?'); # prompt help
466 # given a prefix/remainder tuple return a string with the prefix highlighted
467 # for now use square brackets; later might use ANSI colors (underline, bold)
468 sub highlight_prefix
{
470 my $remainder = shift;
472 if (!defined $prefix) {
476 if (!is_valid_prefix
($prefix)) {
477 return "$prefix$remainder";
480 if (!$menu_use_color) {
481 return "[$prefix]$remainder";
484 return "$prompt_color$prefix$normal_color$remainder";
488 print STDERR colored
$error_color, @_;
491 sub list_and_choose
{
492 my ($opts, @stuff) = @_;
493 my (@chosen, @return);
498 my @prefixes = find_unique_prefixes
(@stuff) unless $opts->{LIST_ONLY
};
504 if ($opts->{HEADER
}) {
505 if (!$opts->{LIST_FLAT
}) {
508 print colored
$header_color, "$opts->{HEADER}\n";
510 for ($i = 0; $i < @stuff; $i++) {
511 my $chosen = $chosen[$i] ?
'*' : ' ';
512 my $print = $stuff[$i];
513 my $ref = ref $print;
514 my $highlighted = highlight_prefix
(@
{$prefixes[$i]})
516 if ($ref eq 'ARRAY') {
517 $print = $highlighted || $print->[0];
519 elsif ($ref eq 'HASH') {
520 my $value = $highlighted || $print->{VALUE
};
521 $print = sprintf($status_fmt,
527 $print = $highlighted || $print;
529 printf("%s%2d: %s", $chosen, $i+1, $print);
530 if (($opts->{LIST_FLAT
}) &&
531 (($i + 1) % ($opts->{LIST_FLAT
}))) {
544 return if ($opts->{LIST_ONLY
});
546 print colored
$prompt_color, $opts->{PROMPT
};
547 if ($opts->{SINGLETON
}) {
556 $opts->{ON_EOF
}->() if $opts->{ON_EOF
};
563 singleton_prompt_help_cmd
() :
567 for my $choice (split(/[\s,]+/, $line)) {
571 # Input that begins with '-'; unchoose
572 if ($choice =~ s/^-//) {
575 # A range can be specified like 5-7 or 5-.
576 if ($choice =~ /^(\d+)-(\d*)$/) {
577 ($bottom, $top) = ($1, length($2) ?
$2 : 1 + @stuff);
579 elsif ($choice =~ /^\d+$/) {
580 $bottom = $top = $choice;
582 elsif ($choice eq '*') {
587 $bottom = $top = find_unique
($choice, @stuff);
588 if (!defined $bottom) {
589 error_msg
sprintf(__
("Huh (%s)?\n"), $choice);
593 if ($opts->{SINGLETON
} && $bottom != $top) {
594 error_msg
sprintf(__
("Huh (%s)?\n"), $choice);
597 for ($i = $bottom-1; $i <= $top-1; $i++) {
598 next if (@stuff <= $i || $i < 0);
599 $chosen[$i] = $choose;
602 last if ($opts->{IMMEDIATE
} || $line eq '*');
604 for ($i = 0; $i < @stuff; $i++) {
606 push @return, $stuff[$i];
612 sub singleton_prompt_help_cmd
{
613 print colored
$help_color, __
<<'EOF' ;
615 1 - select a numbered item
616 foo - select item based on unique prefix
617 - (empty) select nothing
621 sub prompt_help_cmd
{
622 print colored
$help_color, __
<<'EOF' ;
624 1 - select a single item
625 3-5 - select a range of items
626 2-3,6-9 - select multiple ranges
627 foo - select item based on unique prefix
628 -... - unselect specified items
630 - (empty) finish selecting
635 list_and_choose
({ LIST_ONLY
=> 1, HEADER
=> $status_head },
643 if ($did eq 'added') {
644 printf(__n
("added %d path\n", "added %d paths\n",
646 } elsif ($did eq 'updated') {
647 printf(__n
("updated %d path\n", "updated %d paths\n",
649 } elsif ($did eq 'reverted') {
650 printf(__n
("reverted %d path\n", "reverted %d paths\n",
653 printf(__n
("touched %d path\n", "touched %d paths\n",
659 my @mods = list_modified
('file-only');
662 my @update = list_and_choose
({ PROMPT
=> __
('Update'),
663 HEADER
=> $status_head, },
666 system(qw(git update-index --add --remove --),
667 map { $_->{VALUE
} } @update);
668 say_n_paths
('updated', @update);
674 my @update = list_and_choose
({ PROMPT
=> __
('Revert'),
675 HEADER
=> $status_head, },
678 if (is_initial_commit
()) {
679 system(qw(git rm --cached),
680 map { $_->{VALUE
} } @update);
683 my @lines = run_cmd_pipe
(qw(git ls-tree HEAD --),
684 map { $_->{VALUE
} } @update);
686 open $fh, '| git update-index --index-info'
693 if ($_->{INDEX_ADDDEL
} &&
694 $_->{INDEX_ADDDEL
} eq 'create') {
695 system(qw(git update-index --force-remove --),
697 printf(__
("note: %s is untracked now.\n"), $_->{VALUE
});
702 say_n_paths
('reverted', @update);
707 sub add_untracked_cmd
{
708 my @add = list_and_choose
({ PROMPT
=> __
('Add untracked') },
711 system(qw(git update-index --add --), @add);
712 say_n_paths
('added', @add);
714 print __
("No untracked files.\n");
722 open $fh, '| git ' . $cmd . " --recount --allow-overlap";
729 my @diff_cmd = split(" ", $patch_mode_flavour{DIFF
});
730 if (defined $diff_algorithm) {
731 splice @diff_cmd, 1, 0, "--diff-algorithm=${diff_algorithm}";
733 if ($diff_indent_heuristic) {
734 splice @diff_cmd, 1, 0, "--indent-heuristic";
736 if (defined $patch_mode_revision) {
737 push @diff_cmd, get_diff_reference
($patch_mode_revision);
739 my @diff = run_cmd_pipe
("git", @diff_cmd, "--", $path);
741 if ($diff_use_color) {
742 my @display_cmd = ("git", @diff_cmd, qw(--color --), $path);
743 if (defined $diff_filter) {
744 # quotemeta is overkill, but sufficient for shell-quoting
745 my $diff = join(' ', map { quotemeta } @display_cmd);
746 @display_cmd = ("$diff | $diff_filter");
749 @colored = run_cmd_pipe
(@display_cmd);
751 my (@hunk) = { TEXT
=> [], DISPLAY
=> [], TYPE
=> 'header' };
753 for (my $i = 0; $i < @diff; $i++) {
754 if ($diff[$i] =~ /^@@ /) {
755 push @hunk, { TEXT
=> [], DISPLAY
=> [],
758 push @
{$hunk[-1]{TEXT
}}, $diff[$i];
759 push @
{$hunk[-1]{DISPLAY
}},
760 (@colored ?
$colored[$i] : $diff[$i]);
765 sub parse_diff_header
{
768 my $head = { TEXT
=> [], DISPLAY
=> [], TYPE
=> 'header' };
769 my $mode = { TEXT
=> [], DISPLAY
=> [], TYPE
=> 'mode' };
770 my $deletion = { TEXT
=> [], DISPLAY
=> [], TYPE
=> 'deletion' };
772 for (my $i = 0; $i < @
{$src->{TEXT
}}; $i++) {
774 $src->{TEXT
}->[$i] =~ /^(old|new) mode (\d+)$/ ?
$mode :
775 $src->{TEXT
}->[$i] =~ /^deleted file/ ?
$deletion :
777 push @
{$dest->{TEXT
}}, $src->{TEXT
}->[$i];
778 push @
{$dest->{DISPLAY
}}, $src->{DISPLAY
}->[$i];
780 return ($head, $mode, $deletion);
783 sub hunk_splittable
{
786 my @s = split_hunk
($text);
790 sub parse_hunk_header
{
792 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
793 $line =~ /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
794 $o_cnt = 1 unless defined $o_cnt;
795 $n_cnt = 1 unless defined $n_cnt;
796 return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
800 my ($text, $display) = @_;
802 if (!defined $display) {
805 # If there are context lines in the middle of a hunk,
806 # it can be split, but we would need to take care of
809 my ($o_ofs, undef, $n_ofs) = parse_hunk_header
($text->[0]);
814 my $next_hunk_start = undef;
815 my $i = $hunk_start - 1;
829 while (++$i < @
$text) {
830 my $line = $text->[$i];
831 my $display = $display->[$i];
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
840 $next_hunk_start = $i;
842 push @
{$this->{TEXT
}}, $line;
843 push @
{$this->{DISPLAY
}}, $display;
846 if (defined $next_hunk_start) {
853 if (defined $next_hunk_start) {
854 # We are done with the current hunk and
855 # this is the first real change for the
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
};
865 push @
{$this->{TEXT
}}, $line;
866 push @
{$this->{DISPLAY
}}, $display;
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
};
886 my $head = ("@@ -$o_ofs" .
887 (($o_cnt != 1) ?
",$o_cnt" : '') .
889 (($n_cnt != 1) ?
",$n_cnt" : '') .
891 my $display_head = $head;
892 unshift @
{$hunk->{TEXT
}}, $head;
893 if ($diff_use_color) {
894 $display_head = colored
($fraginfo_color, $head);
896 unshift @
{$hunk->{DISPLAY
}}, $display_head;
901 sub find_last_o_ctx
{
903 my $text = $it->{TEXT
};
904 my ($o_ofs, $o_cnt) = parse_hunk_header
($text->[0]);
906 my $last_o_ctx = $o_ofs + $o_cnt;
908 my $line = $text->[$i];
919 my ($prev, $this) = @_;
920 my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) =
921 parse_hunk_header
($prev->{TEXT
}[0]);
922 my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) =
923 parse_hunk_header
($this->{TEXT
}[0]);
925 my (@line, $i, $ofs, $o_cnt, $n_cnt);
928 for ($i = 1; $i < @
{$prev->{TEXT
}}; $i++) {
929 my $line = $prev->{TEXT
}[$i];
930 if ($line =~ /^\+/) {
936 last if ($o1_ofs <= $ofs);
946 for ($i = 1; $i < @
{$this->{TEXT
}}; $i++) {
947 my $line = $this->{TEXT
}[$i];
948 if ($line =~ /^\+/) {
960 my $head = ("@@ -$o0_ofs" .
961 (($o_cnt != 1) ?
",$o_cnt" : '') .
963 (($n_cnt != 1) ?
",$n_cnt" : '') .
965 @
{$prev->{TEXT
}} = ($head, @line);
968 sub coalesce_overlapping_hunks
{
972 my ($last_o_ctx, $last_was_dirty);
974 for (grep { $_->{USE
} } @in) {
975 if ($_->{TYPE
} ne 'hunk') {
979 my $text = $_->{TEXT
};
980 my ($o_ofs) = parse_hunk_header
($text->[0]);
981 if (defined $last_o_ctx &&
982 $o_ofs <= $last_o_ctx &&
985 merge_hunk
($out[-1], $_);
990 $last_o_ctx = find_last_o_ctx
($out[-1]);
991 $last_was_dirty = $_->{DIRTY
};
996 sub reassemble_patch
{
1000 # Include everything in the header except the beginning of the diff.
1001 push @patch, (grep { !/^[-+]{3}/ } @
$head);
1003 # Then include any headers from the hunk lines, which must
1004 # come before any actual hunk.
1005 while (@_ && $_[0] !~ /^@/) {
1009 # Then begin the diff.
1010 push @patch, grep { /^[-+]{3}/ } @
$head;
1012 # And then the actual hunks.
1020 colored
((/^@/ ?
$fraginfo_color :
1021 /^\+/ ?
$diff_new_color :
1022 /^-/ ?
$diff_old_color :
1028 my %edit_hunk_manually_modes = (
1030 "If the patch applies cleanly, the edited hunk will immediately be
1031 marked for staging."),
1033 "If the patch applies cleanly, the edited hunk will immediately be
1034 marked for stashing."),
1036 "If the patch applies cleanly, the edited hunk will immediately be
1037 marked for unstaging."),
1038 reset_nothead
=> N__
(
1039 "If the patch applies cleanly, the edited hunk will immediately be
1040 marked for applying."),
1041 checkout_index
=> N__
(
1042 "If the patch applies cleanly, the edited hunk will immediately be
1043 marked for discarding"),
1044 checkout_head
=> N__
(
1045 "If the patch applies cleanly, the edited hunk will immediately be
1046 marked for discarding."),
1047 checkout_nothead
=> N__
(
1048 "If the patch applies cleanly, the edited hunk will immediately be
1049 marked for applying."),
1052 sub edit_hunk_manually
{
1055 my $hunkfile = $repo->repo_path . "/addp-hunk-edit.diff";
1057 open $fh, '>', $hunkfile
1058 or die sprintf(__
("failed to open hunk edit file for writing: %s"), $!);
1059 print $fh Git
::comment_lines __
("Manual hunk edit mode -- see bottom for a quick guide.\n");
1060 print $fh @
$oldtext;
1061 my $is_reverse = $patch_mode_flavour{IS_REVERSE
};
1062 my ($remove_plus, $remove_minus) = $is_reverse ?
('-', '+') : ('+', '-');
1063 my $comment_line_char = Git
::get_comment_line_char
;
1064 print $fh Git
::comment_lines
sprintf(__
<<EOF, $remove_minus, $remove_plus, $comment_line_char),
1066 To remove '%s' lines, make them ' ' lines (context).
1067 To remove '%s' lines, delete them.
1068 Lines starting with %s will be removed.
1070 __
($edit_hunk_manually_modes{$patch_mode}),
1071 # TRANSLATORS: 'it' refers to the patch mentioned in the previous messages.
1073 If it does not apply cleanly, you will be given an opportunity to
1074 edit again. If all lines of the hunk are removed, then the edit is
1075 aborted and the hunk is left unchanged.
1079 chomp(my $editor = run_cmd_pipe(qw(git var GIT_EDITOR)));
1080 system('sh', '-c', $editor.' "$@"', $editor, $hunkfile);
1086 open $fh, '<', $hunkfile
1087 or die sprintf(__
("failed to open hunk edit file for reading: %s"), $!);
1088 my @newtext = grep { !/^$comment_line_char/ } <$fh>;
1092 # Abort if nothing remains
1093 if (!grep { /\S/ } @newtext) {
1097 # Reinsert the first hunk header if the user accidentally deleted it
1098 if ($newtext[0] !~ /^@/) {
1099 unshift @newtext, $oldtext->[0];
1105 return run_git_apply
($patch_mode_flavour{APPLY_CHECK
} . ' --check',
1106 map { @
{$_->{TEXT
}} } @_);
1109 sub _restore_terminal_and_die
{
1115 sub prompt_single_character
{
1117 local $SIG{TERM
} = \
&_restore_terminal_and_die
;
1118 local $SIG{INT
} = \
&_restore_terminal_and_die
;
1120 my $key = ReadKey
0;
1122 if ($use_termcap and $key eq "\e") {
1123 while (!defined $term_escapes{$key}) {
1124 my $next = ReadKey
0.5;
1125 last if (!defined $next);
1130 print "$key" if defined $key;
1141 print colored
$prompt_color, $prompt;
1142 my $line = prompt_single_character
;
1143 return 0 if $line =~ /^n/i;
1144 return 1 if $line =~ /^y/i;
1148 sub edit_hunk_loop
{
1149 my ($head, $hunk, $ix) = @_;
1150 my $text = $hunk->[$ix]->{TEXT
};
1153 $text = edit_hunk_manually
($text);
1154 if (!defined $text) {
1159 TYPE
=> $hunk->[$ix]->{TYPE
},
1163 if (diff_applies
($head,
1166 @
{$hunk}[$ix+1..$#{$hunk}])) {
1167 $newhunk->{DISPLAY
} = [color_diff
(@
{$text})];
1172 # TRANSLATORS: do not translate [y/n]
1173 # The program will only accept that input
1175 # Consider translating (saying "no" discards!) as
1176 # (saying "n" for "no" discards!) if the translation
1177 # of the word "no" does not start with n.
1178 __
('Your edited hunk does not apply. Edit again '
1179 . '(saying "no" discards!) [y/n]? ')
1185 my %help_patch_modes = (
1187 "y - stage this hunk
1188 n - do not stage this hunk
1189 q - quit; do not stage this hunk or any of the remaining ones
1190 a - stage this hunk and all later hunks in the file
1191 d - do not stage this hunk or any of the later hunks in the file"),
1193 "y - stash this hunk
1194 n - do not stash this hunk
1195 q - quit; do not stash this hunk or any of the remaining ones
1196 a - stash this hunk and all later hunks in the file
1197 d - do not stash this hunk or any of the later hunks in the file"),
1199 "y - unstage this hunk
1200 n - do not unstage this hunk
1201 q - quit; do not unstage this hunk or any of the remaining ones
1202 a - unstage this hunk and all later hunks in the file
1203 d - do not unstage this hunk or any of the later hunks in the file"),
1204 reset_nothead
=> N__
(
1205 "y - apply this hunk to index
1206 n - do not apply this hunk to index
1207 q - quit; do not apply this hunk or any of the remaining ones
1208 a - apply this hunk and all later hunks in the file
1209 d - do not apply this hunk or any of the later hunks in the file"),
1210 checkout_index
=> N__
(
1211 "y - discard this hunk from worktree
1212 n - do not discard this hunk from worktree
1213 q - quit; do not discard this hunk or any of the remaining ones
1214 a - discard this hunk and all later hunks in the file
1215 d - do not discard this hunk or any of the later hunks in the file"),
1216 checkout_head
=> N__
(
1217 "y - discard this hunk from index and worktree
1218 n - do not discard this hunk from index and worktree
1219 q - quit; do not discard this hunk or any of the remaining ones
1220 a - discard this hunk and all later hunks in the file
1221 d - do not discard this hunk or any of the later hunks in the file"),
1222 checkout_nothead
=> N__
(
1223 "y - apply this hunk to index and worktree
1224 n - do not apply this hunk to index and worktree
1225 q - quit; do not apply this hunk or any of the remaining ones
1226 a - apply this hunk and all later hunks in the file
1227 d - do not apply this hunk or any of the later hunks in the file"),
1230 sub help_patch_cmd
{
1231 print colored
$help_color, __
($help_patch_modes{$patch_mode}), "\n", __
<<EOF ;
1232 g - select a hunk to go to
1233 / - search for a hunk matching the given regex
1234 j - leave this hunk undecided, see next undecided hunk
1235 J - leave this hunk undecided, see next hunk
1236 k - leave this hunk undecided, see previous undecided hunk
1237 K - leave this hunk undecided, see previous hunk
1238 s - split the current hunk into smaller hunks
1239 e - manually edit the current hunk
1246 my $ret = run_git_apply
$cmd, @_;
1253 sub apply_patch_for_checkout_commit
{
1254 my $reverse = shift;
1255 my $applies_index = run_git_apply
'apply '.$reverse.' --cached --check', @_;
1256 my $applies_worktree = run_git_apply
'apply '.$reverse.' --check', @_;
1258 if ($applies_worktree && $applies_index) {
1259 run_git_apply
'apply '.$reverse.' --cached', @_;
1260 run_git_apply
'apply '.$reverse, @_;
1262 } elsif (!$applies_index) {
1263 print colored
$error_color, __
("The selected hunks do not apply to the index!\n");
1264 if (prompt_yesno __
("Apply them to the worktree anyway? ")) {
1265 return run_git_apply
'apply '.$reverse, @_;
1267 print colored
$error_color, __
("Nothing was applied.\n");
1276 sub patch_update_cmd
{
1277 my @all_mods = list_modified
($patch_mode_flavour{FILTER
});
1278 error_msg
sprintf(__
("ignoring unmerged: %s\n"), $_->{VALUE
})
1279 for grep { $_->{UNMERGED
} } @all_mods;
1280 @all_mods = grep { !$_->{UNMERGED
} } @all_mods;
1282 my @mods = grep { !($_->{BINARY
}) } @all_mods;
1287 print STDERR __
("Only binary files changed.\n");
1289 print STDERR __
("No changes.\n");
1293 if ($patch_mode_only) {
1297 @them = list_and_choose
({ PROMPT
=> __
('Patch update'),
1298 HEADER
=> $status_head, },
1302 return 0 if patch_update_file
($_->{VALUE
});
1306 # Generate a one line summary of a hunk.
1307 sub summarize_hunk
{
1309 my $summary = $rhunk->{TEXT
}[0];
1311 # Keep the line numbers, discard extra context.
1312 $summary =~ s/@@(.*?)@@.*/$1 /s;
1313 $summary .= " " x
(20 - length $summary);
1315 # Add some user context.
1316 for my $line (@
{$rhunk->{TEXT
}}) {
1317 if ($line =~ m/^[+-].*\w/) {
1324 return substr($summary, 0, 80) . "\n";
1328 # Print a one-line summary of each hunk in the array ref in
1329 # the first argument, starting with the index in the 2nd.
1331 my ($hunks, $i) = @_;
1334 for (; $i < @
$hunks && $ctr < 20; $i++, $ctr++) {
1336 if (defined $hunks->[$i]{USE
}) {
1337 $status = $hunks->[$i]{USE
} ?
"+" : "-";
1342 summarize_hunk
($hunks->[$i]);
1347 my %patch_update_prompt_modes = (
1349 mode
=> N__
("Stage mode change [y,n,q,a,d,/%s,?]? "),
1350 deletion
=> N__
("Stage deletion [y,n,q,a,d,/%s,?]? "),
1351 hunk
=> N__
("Stage this hunk [y,n,q,a,d,/%s,?]? "),
1354 mode
=> N__
("Stash mode change [y,n,q,a,d,/%s,?]? "),
1355 deletion
=> N__
("Stash deletion [y,n,q,a,d,/%s,?]? "),
1356 hunk
=> N__
("Stash this hunk [y,n,q,a,d,/%s,?]? "),
1359 mode
=> N__
("Unstage mode change [y,n,q,a,d,/%s,?]? "),
1360 deletion
=> N__
("Unstage deletion [y,n,q,a,d,/%s,?]? "),
1361 hunk
=> N__
("Unstage this hunk [y,n,q,a,d,/%s,?]? "),
1364 mode
=> N__
("Apply mode change to index [y,n,q,a,d,/%s,?]? "),
1365 deletion
=> N__
("Apply deletion to index [y,n,q,a,d,/%s,?]? "),
1366 hunk
=> N__
("Apply this hunk to index [y,n,q,a,d,/%s,?]? "),
1369 mode
=> N__
("Discard mode change from worktree [y,n,q,a,d,/%s,?]? "),
1370 deletion
=> N__
("Discard deletion from worktree [y,n,q,a,d,/%s,?]? "),
1371 hunk
=> N__
("Discard this hunk from worktree [y,n,q,a,d,/%s,?]? "),
1374 mode
=> N__
("Discard mode change from index and worktree [y,n,q,a,d,/%s,?]? "),
1375 deletion
=> N__
("Discard deletion from index and worktree [y,n,q,a,d,/%s,?]? "),
1376 hunk
=> N__
("Discard this hunk from index and worktree [y,n,q,a,d,/%s,?]? "),
1378 checkout_nothead
=> {
1379 mode
=> N__
("Apply mode change to index and worktree [y,n,q,a,d,/%s,?]? "),
1380 deletion
=> N__
("Apply deletion to index and worktree [y,n,q,a,d,/%s,?]? "),
1381 hunk
=> N__
("Apply this hunk to index and worktree [y,n,q,a,d,/%s,?]? "),
1385 sub patch_update_file
{
1389 my ($head, @hunk) = parse_diff
($path);
1390 ($head, my $mode, my $deletion) = parse_diff_header
($head);
1391 for (@
{$head->{DISPLAY
}}) {
1395 if (@
{$mode->{TEXT
}}) {
1396 unshift @hunk, $mode;
1398 if (@
{$deletion->{TEXT
}}) {
1399 foreach my $hunk (@hunk) {
1400 push @
{$deletion->{TEXT
}}, @
{$hunk->{TEXT
}};
1401 push @
{$deletion->{DISPLAY
}}, @
{$hunk->{DISPLAY
}};
1403 @hunk = ($deletion);
1406 $num = scalar @hunk;
1410 my ($prev, $next, $other, $undecided, $i);
1416 for ($i = 0; $i < $ix; $i++) {
1417 if (!defined $hunk[$i]{USE
}) {
1426 for ($i = $ix + 1; $i < $num; $i++) {
1427 if (!defined $hunk[$i]{USE
}) {
1433 if ($ix < $num - 1) {
1439 for ($i = 0; $i < $num; $i++) {
1440 if (!defined $hunk[$i]{USE
}) {
1445 last if (!$undecided);
1447 if ($hunk[$ix]{TYPE
} eq 'hunk' &&
1448 hunk_splittable
($hunk[$ix]{TEXT
})) {
1451 if ($hunk[$ix]{TYPE
} eq 'hunk') {
1454 for (@
{$hunk[$ix]{DISPLAY
}}) {
1457 print colored
$prompt_color,
1458 sprintf(__
($patch_update_prompt_modes{$patch_mode}{$hunk[$ix]{TYPE
}}), $other);
1460 my $line = prompt_single_character
;
1461 last unless defined $line;
1463 if ($line =~ /^y/i) {
1464 $hunk[$ix]{USE
} = 1;
1466 elsif ($line =~ /^n/i) {
1467 $hunk[$ix]{USE
} = 0;
1469 elsif ($line =~ /^a/i) {
1470 while ($ix < $num) {
1471 if (!defined $hunk[$ix]{USE
}) {
1472 $hunk[$ix]{USE
} = 1;
1478 elsif ($other =~ /g/ && $line =~ /^g(.*)/) {
1480 my $no = $ix > 10 ?
$ix - 10 : 0;
1481 while ($response eq '') {
1482 $no = display_hunks
(\
@hunk, $no);
1484 print __
("go to which hunk (<ret> to see more)? ");
1486 print __
("go to which hunk? ");
1488 $response = <STDIN
>;
1489 if (!defined $response) {
1494 if ($response !~ /^\s*\d+\s*$/) {
1495 error_msg
sprintf(__
("Invalid number: '%s'\n"),
1497 } elsif (0 < $response && $response <= $num) {
1498 $ix = $response - 1;
1500 error_msg
sprintf(__n
("Sorry, only %d hunk available.\n",
1501 "Sorry, only %d hunks available.\n", $num), $num);
1505 elsif ($line =~ /^d/i) {
1506 while ($ix < $num) {
1507 if (!defined $hunk[$ix]{USE
}) {
1508 $hunk[$ix]{USE
} = 0;
1514 elsif ($line =~ /^q/i) {
1515 for ($i = 0; $i < $num; $i++) {
1516 if (!defined $hunk[$i]{USE
}) {
1523 elsif ($line =~ m
|^/(.*)|) {
1526 print colored
$prompt_color, __
("search for regex? ");
1528 if (defined $regex) {
1534 $search_string = qr{$regex}m;
1537 my ($err,$exp) = ($@
, $1);
1538 $err =~ s/ at .*git-add--interactive line \d+, <STDIN> line \d+.*$//;
1539 error_msg
sprintf(__
("Malformed search regexp %s: %s\n"), $exp, $err);
1544 my $text = join ("", @
{$hunk[$iy]{TEXT
}});
1545 last if ($text =~ $search_string);
1547 $iy = 0 if ($iy >= $num);
1549 error_msg __
("No hunk matches the given pattern\n");
1556 elsif ($line =~ /^K/) {
1557 if ($other =~ /K/) {
1561 error_msg __
("No previous hunk\n");
1565 elsif ($line =~ /^J/) {
1566 if ($other =~ /J/) {
1570 error_msg __
("No next hunk\n");
1574 elsif ($line =~ /^k/) {
1575 if ($other =~ /k/) {
1579 !defined $hunk[$ix]{USE
});
1583 error_msg __
("No previous hunk\n");
1587 elsif ($line =~ /^j/) {
1588 if ($other !~ /j/) {
1589 error_msg __
("No next hunk\n");
1593 elsif ($other =~ /s/ && $line =~ /^s/) {
1594 my @split = split_hunk
($hunk[$ix]{TEXT
}, $hunk[$ix]{DISPLAY
});
1596 print colored
$header_color, sprintf(
1597 __n
("Split into %d hunk.\n",
1598 "Split into %d hunks.\n",
1599 scalar(@split)), scalar(@split));
1601 splice (@hunk, $ix, 1, @split);
1602 $num = scalar @hunk;
1605 elsif ($other =~ /e/ && $line =~ /^e/) {
1606 my $newhunk = edit_hunk_loop
($head, \
@hunk, $ix);
1607 if (defined $newhunk) {
1608 splice @hunk, $ix, 1, $newhunk;
1612 help_patch_cmd
($other);
1618 last if ($ix >= $num ||
1619 !defined $hunk[$ix]{USE
});
1624 @hunk = coalesce_overlapping_hunks
(@hunk);
1630 push @result, @
{$_->{TEXT
}};
1635 my @patch = reassemble_patch
($head->{TEXT
}, @result);
1636 my $apply_routine = $patch_mode_flavour{APPLY
};
1637 &$apply_routine(@patch);
1646 my @mods = list_modified
('index-only');
1647 @mods = grep { !($_->{BINARY
}) } @mods;
1649 my (@them) = list_and_choose
({ PROMPT
=> __
('Review diff'),
1651 HEADER
=> $status_head, },
1654 my $reference = (is_initial_commit
()) ? get_empty_tree
() : 'HEAD';
1655 system(qw(git diff -p --cached), $reference, '--',
1656 map { $_->{VALUE
} } @them);
1665 # TRANSLATORS: please do not translate the command names
1666 # 'status', 'update', 'revert', etc.
1667 print colored
$help_color, __
<<'EOF' ;
1668 status - show paths with changes
1669 update - add working tree state to the staged set of changes
1670 revert - revert staged set of changes back to the HEAD version
1671 patch - pick hunks and update selectively
1672 diff - view diff between HEAD and index
1673 add untracked - add contents of untracked files to the staged set of changes
1678 return unless @ARGV;
1679 my $arg = shift @ARGV;
1680 if ($arg =~ /--patch(?:=(.*))?/) {
1682 if ($1 eq 'reset') {
1683 $patch_mode = 'reset_head';
1684 $patch_mode_revision = 'HEAD';
1685 $arg = shift @ARGV or die __
("missing --");
1687 $patch_mode_revision = $arg;
1688 $patch_mode = ($arg eq 'HEAD' ?
1689 'reset_head' : 'reset_nothead');
1690 $arg = shift @ARGV or die __
("missing --");
1692 } elsif ($1 eq 'checkout') {
1693 $arg = shift @ARGV or die __
("missing --");
1695 $patch_mode = 'checkout_index';
1697 $patch_mode_revision = $arg;
1698 $patch_mode = ($arg eq 'HEAD' ?
1699 'checkout_head' : 'checkout_nothead');
1700 $arg = shift @ARGV or die __
("missing --");
1702 } elsif ($1 eq 'stage' or $1 eq 'stash') {
1704 $arg = shift @ARGV or die __
("missing --");
1706 die sprintf(__
("unknown --patch mode: %s"), $1);
1709 $patch_mode = 'stage';
1710 $arg = shift @ARGV or die __
("missing --");
1712 die sprintf(__
("invalid argument %s, expecting --"),
1713 $arg) unless $arg eq "--";
1714 %patch_mode_flavour = %{$patch_modes{$patch_mode}};
1715 $patch_mode_only = 1;
1717 elsif ($arg ne "--") {
1718 die sprintf(__
("invalid argument %s, expecting --"), $arg);
1723 my @cmd = ([ 'status', \
&status_cmd
, ],
1724 [ 'update', \
&update_cmd
, ],
1725 [ 'revert', \
&revert_cmd
, ],
1726 [ 'add untracked', \
&add_untracked_cmd
, ],
1727 [ 'patch', \
&patch_update_cmd
, ],
1728 [ 'diff', \
&diff_cmd
, ],
1729 [ 'quit', \
&quit_cmd
, ],
1730 [ 'help', \
&help_cmd
, ],
1733 my ($it) = list_and_choose
({ PROMPT
=> __
('What now'),
1736 HEADER
=> __
('*** Commands ***'),
1737 ON_EOF
=> \
&quit_cmd
,
1738 IMMEDIATE
=> 1 }, @cmd);
1752 if ($patch_mode_only) {