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_compaction_heuristic = $repo->config_bool('diff.compactionheuristic');
51 my $diff_filter = $repo->config('interactive.difffilter');
59 if ($repo->config_bool("interactive.singlekey")) {
61 require Term
::ReadKey
;
62 Term
::ReadKey
->import;
66 print STDERR
"missing Term::ReadKey, disabling interactive.singlekey\n";
70 my $termcap = Term
::Cap
->Tgetent;
71 foreach (values %$termcap) {
72 $term_escapes{$_} = 1 if /^\e/;
80 my $string = join("", @_);
83 # Put a color code at the beginning of each line, a reset at the end
84 # color after newlines that are not at the end of the string
85 $string =~ s/(\n+)(.)/$1$color$2/g;
86 # reset before newlines
87 $string =~ s/(\n+)/$normal_color$1/g;
88 # codes at beginning and end (if necessary):
89 $string =~ s/^/$color/;
90 $string =~ s/$/$normal_color/ unless $string =~ /\n$/;
95 # command line options
98 my $patch_mode_revision;
101 sub apply_patch_for_checkout_commit
;
102 sub apply_patch_for_stash
;
106 DIFF
=> 'diff-files -p',
107 APPLY
=> sub { apply_patch
'apply --cached', @_; },
108 APPLY_CHECK
=> 'apply --cached',
109 FILTER
=> 'file-only',
113 DIFF
=> 'diff-index -p HEAD',
114 APPLY
=> sub { apply_patch
'apply --cached', @_; },
115 APPLY_CHECK
=> 'apply --cached',
120 DIFF
=> 'diff-index -p --cached',
121 APPLY
=> sub { apply_patch
'apply -R --cached', @_; },
122 APPLY_CHECK
=> 'apply -R --cached',
123 FILTER
=> 'index-only',
127 DIFF
=> 'diff-index -R -p --cached',
128 APPLY
=> sub { apply_patch
'apply --cached', @_; },
129 APPLY_CHECK
=> 'apply --cached',
130 FILTER
=> 'index-only',
133 'checkout_index' => {
134 DIFF
=> 'diff-files -p',
135 APPLY
=> sub { apply_patch
'apply -R', @_; },
136 APPLY_CHECK
=> 'apply -R',
137 FILTER
=> 'file-only',
141 DIFF
=> 'diff-index -p',
142 APPLY
=> sub { apply_patch_for_checkout_commit
'-R', @_ },
143 APPLY_CHECK
=> 'apply -R',
147 'checkout_nothead' => {
148 DIFF
=> 'diff-index -R -p',
149 APPLY
=> sub { apply_patch_for_checkout_commit
'', @_ },
150 APPLY_CHECK
=> 'apply',
156 $patch_mode = 'stage';
157 my %patch_mode_flavour = %{$patch_modes{$patch_mode}};
160 if ($^O
eq 'MSWin32') {
161 my @invalid = grep {m/[":*]/} @_;
162 die "$^O does not support: @invalid\n" if @invalid;
163 my @args = map { m/ /o ?
"\"$_\"": $_ } @_;
167 open($fh, '-|', @_) or die;
172 my ($GIT_DIR) = run_cmd_pipe
(qw(git rev-parse --git-dir));
174 if (!defined $GIT_DIR) {
175 exit(1); # rev-parse would have already said "not a git repo"
192 my ($retval, $remainder);
193 if (!/^\042(.*)\042$/) {
196 ($_, $retval) = ($1, "");
197 while (/^([^\\]*)\\(.*)$/) {
201 if (/^([0-3][0-7][0-7])(.*)$/) {
202 $retval .= chr(oct($1));
206 if (/^([\\\042btnvfr])(.*)$/) {
207 $retval .= $cquote_map{$1};
211 # This is malformed -- just return it as-is for now.
222 open $fh, 'git update-index --refresh |'
225 ;# ignore 'needs update'
235 run_cmd_pipe
(qw(git ls-files --others --exclude-standard --), @ARGV);
238 # TRANSLATORS: you can adjust this to align "git add -i" status menu
239 my $status_fmt = __
('%12s %12s %s');
240 my $status_head = sprintf($status_fmt, __
('staged'), __
('unstaged'), __
('path'));
244 sub is_initial_commit
{
245 $initial = system('git rev-parse HEAD -- >/dev/null 2>&1') != 0
246 unless defined $initial;
252 return '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
255 sub get_diff_reference
{
257 if (defined $ref and $ref ne 'HEAD') {
259 } elsif (is_initial_commit
()) {
260 return get_empty_tree
();
266 # Returns list of hashes, contents of each of which are:
268 # BINARY: is a binary path
269 # INDEX: is index different from HEAD?
270 # FILE: is file different from index?
271 # INDEX_ADDDEL: is it add/delete between HEAD and index?
272 # FILE_ADDDEL: is it add/delete between index and file?
273 # UNMERGED: is the path unmerged
278 my ($add, $del, $adddel, $file);
285 } run_cmd_pipe
(qw(git ls-files --), @ARGV);
286 return if (!@tracked);
289 my $reference = get_diff_reference
($patch_mode_revision);
290 for (run_cmd_pipe
(qw(git diff-index --cached
291 --numstat --summary), $reference,
293 if (($add, $del, $file) =
294 /^([-\d]+) ([-\d]+) (.*)/) {
296 $file = unquote_path
($file);
297 if ($add eq '-' && $del eq '-') {
298 $change = __
('binary');
302 $change = "+$add/-$del";
307 FILE
=> __
('nothing'),
310 elsif (($adddel, $file) =
311 /^ (create|delete) mode [0-7]+ (.*)$/) {
312 $file = unquote_path
($file);
313 $data{$file}{INDEX_ADDDEL
} = $adddel;
317 for (run_cmd_pipe
(qw(git diff-files --numstat --summary --raw --), @tracked)) {
318 if (($add, $del, $file) =
319 /^([-\d]+) ([-\d]+) (.*)/) {
320 $file = unquote_path
($file);
322 if ($add eq '-' && $del eq '-') {
323 $change = __
('binary');
327 $change = "+$add/-$del";
329 $data{$file}{FILE
} = $change;
331 $data{$file}{BINARY
} = 1;
334 elsif (($adddel, $file) =
335 /^ (create|delete) mode [0-7]+ (.*)$/) {
336 $file = unquote_path
($file);
337 $data{$file}{FILE_ADDDEL
} = $adddel;
339 elsif (/^:[0-7]+ [0-7]+ [0-9a-f]+ [0-9a-f]+ (.) (.*)$/) {
340 $file = unquote_path
($2);
341 if (!exists $data{$file}) {
343 INDEX
=> __
('unchanged'),
348 $data{$file}{UNMERGED
} = 1;
353 for (sort keys %data) {
357 if ($only eq 'index-only') {
358 next if ($it->{INDEX
} eq __
('unchanged'));
360 if ($only eq 'file-only') {
361 next if ($it->{FILE
} eq __
('nothing'));
373 my ($string, @stuff) = @_;
375 for (my $i = 0; $i < @stuff; $i++) {
379 if ((ref $it) eq 'ARRAY') {
387 if ($it =~ /^$string/) {
391 if (defined $hit && defined $found) {
401 # inserts string into trie and updates count for each character
403 my ($trie, $string) = @_;
404 foreach (split //, $string) {
405 $trie = $trie->{$_} ||= {COUNT
=> 0};
410 # returns an array of tuples (prefix, remainder)
411 sub find_unique_prefixes
{
415 # any single prefix exceeding the soft limit is omitted
416 # if any prefix exceeds the hard limit all are omitted
417 # 0 indicates no limit
421 # build a trie modelling all possible options
423 foreach my $print (@stuff) {
424 if ((ref $print) eq 'ARRAY') {
425 $print = $print->[0];
427 elsif ((ref $print) eq 'HASH') {
428 $print = $print->{VALUE
};
430 update_trie
(\
%trie, $print);
431 push @return, $print;
434 # use the trie to find the unique prefixes
435 for (my $i = 0; $i < @return; $i++) {
436 my $ret = $return[$i];
437 my @letters = split //, $ret;
439 my ($prefix, $remainder);
441 for ($j = 0; $j < @letters; $j++) {
442 my $letter = $letters[$j];
443 if ($search{$letter}{COUNT
} == 1) {
444 $prefix = substr $ret, 0, $j + 1;
445 $remainder = substr $ret, $j + 1;
449 my $prefix = substr $ret, 0, $j;
451 if ($hard_limit && $j + 1 > $hard_limit);
453 %search = %{$search{$letter}};
455 if (ord($letters[0]) > 127 ||
456 ($soft_limit && $j + 1 > $soft_limit)) {
460 $return[$i] = [$prefix, $remainder];
465 # filters out prefixes which have special meaning to list_and_choose()
466 sub is_valid_prefix
{
468 return (defined $prefix) &&
469 !($prefix =~ /[\s,]/) && # separators
470 !($prefix =~ /^-/) && # deselection
471 !($prefix =~ /^\d+/) && # selection
472 ($prefix ne '*') && # "all" wildcard
473 ($prefix ne '?'); # prompt help
476 # given a prefix/remainder tuple return a string with the prefix highlighted
477 # for now use square brackets; later might use ANSI colors (underline, bold)
478 sub highlight_prefix
{
480 my $remainder = shift;
482 if (!defined $prefix) {
486 if (!is_valid_prefix
($prefix)) {
487 return "$prefix$remainder";
490 if (!$menu_use_color) {
491 return "[$prefix]$remainder";
494 return "$prompt_color$prefix$normal_color$remainder";
498 print STDERR colored
$error_color, @_;
501 sub list_and_choose
{
502 my ($opts, @stuff) = @_;
503 my (@chosen, @return);
508 my @prefixes = find_unique_prefixes
(@stuff) unless $opts->{LIST_ONLY
};
514 if ($opts->{HEADER
}) {
515 if (!$opts->{LIST_FLAT
}) {
518 print colored
$header_color, "$opts->{HEADER}\n";
520 for ($i = 0; $i < @stuff; $i++) {
521 my $chosen = $chosen[$i] ?
'*' : ' ';
522 my $print = $stuff[$i];
523 my $ref = ref $print;
524 my $highlighted = highlight_prefix
(@
{$prefixes[$i]})
526 if ($ref eq 'ARRAY') {
527 $print = $highlighted || $print->[0];
529 elsif ($ref eq 'HASH') {
530 my $value = $highlighted || $print->{VALUE
};
531 $print = sprintf($status_fmt,
537 $print = $highlighted || $print;
539 printf("%s%2d: %s", $chosen, $i+1, $print);
540 if (($opts->{LIST_FLAT
}) &&
541 (($i + 1) % ($opts->{LIST_FLAT
}))) {
554 return if ($opts->{LIST_ONLY
});
556 print colored
$prompt_color, $opts->{PROMPT
};
557 if ($opts->{SINGLETON
}) {
566 $opts->{ON_EOF
}->() if $opts->{ON_EOF
};
573 singleton_prompt_help_cmd
() :
577 for my $choice (split(/[\s,]+/, $line)) {
581 # Input that begins with '-'; unchoose
582 if ($choice =~ s/^-//) {
585 # A range can be specified like 5-7 or 5-.
586 if ($choice =~ /^(\d+)-(\d*)$/) {
587 ($bottom, $top) = ($1, length($2) ?
$2 : 1 + @stuff);
589 elsif ($choice =~ /^\d+$/) {
590 $bottom = $top = $choice;
592 elsif ($choice eq '*') {
597 $bottom = $top = find_unique
($choice, @stuff);
598 if (!defined $bottom) {
599 error_msg
sprintf(__
("Huh (%s)?\n"), $choice);
603 if ($opts->{SINGLETON
} && $bottom != $top) {
604 error_msg
sprintf(__
("Huh (%s)?\n"), $choice);
607 for ($i = $bottom-1; $i <= $top-1; $i++) {
608 next if (@stuff <= $i || $i < 0);
609 $chosen[$i] = $choose;
612 last if ($opts->{IMMEDIATE
} || $line eq '*');
614 for ($i = 0; $i < @stuff; $i++) {
616 push @return, $stuff[$i];
622 sub singleton_prompt_help_cmd
{
623 print colored
$help_color, __
<<'EOF' ;
625 1 - select a numbered item
626 foo - select item based on unique prefix
627 - (empty) select nothing
631 sub prompt_help_cmd
{
632 print colored
$help_color, __
<<'EOF' ;
634 1 - select a single item
635 3-5 - select a range of items
636 2-3,6-9 - select multiple ranges
637 foo - select item based on unique prefix
638 -... - unselect specified items
640 - (empty) finish selecting
645 list_and_choose
({ LIST_ONLY
=> 1, HEADER
=> $status_head },
653 if ($did eq 'added') {
654 printf(__n
("added %d path\n", "added %d paths\n",
656 } elsif ($did eq 'updated') {
657 printf(__n
("updated %d path\n", "updated %d paths\n",
659 } elsif ($did eq 'reverted') {
660 printf(__n
("reverted %d path\n", "reverted %d paths\n",
663 printf(__n
("touched %d path\n", "touched %d paths\n",
669 my @mods = list_modified
('file-only');
672 my @update = list_and_choose
({ PROMPT
=> __
('Update'),
673 HEADER
=> $status_head, },
676 system(qw(git update-index --add --remove --),
677 map { $_->{VALUE
} } @update);
678 say_n_paths
('updated', @update);
684 my @update = list_and_choose
({ PROMPT
=> __
('Revert'),
685 HEADER
=> $status_head, },
688 if (is_initial_commit
()) {
689 system(qw(git rm --cached),
690 map { $_->{VALUE
} } @update);
693 my @lines = run_cmd_pipe
(qw(git ls-tree HEAD --),
694 map { $_->{VALUE
} } @update);
696 open $fh, '| git update-index --index-info'
703 if ($_->{INDEX_ADDDEL
} &&
704 $_->{INDEX_ADDDEL
} eq 'create') {
705 system(qw(git update-index --force-remove --),
707 printf(__
("note: %s is untracked now.\n"), $_->{VALUE
});
712 say_n_paths
('reverted', @update);
717 sub add_untracked_cmd
{
718 my @add = list_and_choose
({ PROMPT
=> __
('Add untracked') },
721 system(qw(git update-index --add --), @add);
722 say_n_paths
('added', @add);
724 print __
("No untracked files.\n");
732 open $fh, '| git ' . $cmd . " --recount --allow-overlap";
739 my @diff_cmd = split(" ", $patch_mode_flavour{DIFF
});
740 if (defined $diff_algorithm) {
741 splice @diff_cmd, 1, 0, "--diff-algorithm=${diff_algorithm}";
743 if ($diff_indent_heuristic) {
744 splice @diff_cmd, 1, 0, "--indent-heuristic";
745 } elsif ($diff_compaction_heuristic) {
746 splice @diff_cmd, 1, 0, "--compaction-heuristic";
748 if (defined $patch_mode_revision) {
749 push @diff_cmd, get_diff_reference
($patch_mode_revision);
751 my @diff = run_cmd_pipe
("git", @diff_cmd, "--", $path);
753 if ($diff_use_color) {
754 my @display_cmd = ("git", @diff_cmd, qw(--color --), $path);
755 if (defined $diff_filter) {
756 # quotemeta is overkill, but sufficient for shell-quoting
757 my $diff = join(' ', map { quotemeta } @display_cmd);
758 @display_cmd = ("$diff | $diff_filter");
761 @colored = run_cmd_pipe
(@display_cmd);
763 my (@hunk) = { TEXT
=> [], DISPLAY
=> [], TYPE
=> 'header' };
765 for (my $i = 0; $i < @diff; $i++) {
766 if ($diff[$i] =~ /^@@ /) {
767 push @hunk, { TEXT
=> [], DISPLAY
=> [],
770 push @
{$hunk[-1]{TEXT
}}, $diff[$i];
771 push @
{$hunk[-1]{DISPLAY
}},
772 (@colored ?
$colored[$i] : $diff[$i]);
777 sub parse_diff_header
{
780 my $head = { TEXT
=> [], DISPLAY
=> [], TYPE
=> 'header' };
781 my $mode = { TEXT
=> [], DISPLAY
=> [], TYPE
=> 'mode' };
782 my $deletion = { TEXT
=> [], DISPLAY
=> [], TYPE
=> 'deletion' };
784 for (my $i = 0; $i < @
{$src->{TEXT
}}; $i++) {
786 $src->{TEXT
}->[$i] =~ /^(old|new) mode (\d+)$/ ?
$mode :
787 $src->{TEXT
}->[$i] =~ /^deleted file/ ?
$deletion :
789 push @
{$dest->{TEXT
}}, $src->{TEXT
}->[$i];
790 push @
{$dest->{DISPLAY
}}, $src->{DISPLAY
}->[$i];
792 return ($head, $mode, $deletion);
795 sub hunk_splittable
{
798 my @s = split_hunk
($text);
802 sub parse_hunk_header
{
804 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
805 $line =~ /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
806 $o_cnt = 1 unless defined $o_cnt;
807 $n_cnt = 1 unless defined $n_cnt;
808 return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
812 my ($text, $display) = @_;
814 if (!defined $display) {
817 # If there are context lines in the middle of a hunk,
818 # it can be split, but we would need to take care of
821 my ($o_ofs, undef, $n_ofs) = parse_hunk_header
($text->[0]);
826 my $next_hunk_start = undef;
827 my $i = $hunk_start - 1;
841 while (++$i < @
$text) {
842 my $line = $text->[$i];
843 my $display = $display->[$i];
845 if ($this->{ADDDEL
} &&
846 !defined $next_hunk_start) {
847 # We have seen leading context and
848 # adds/dels and then here is another
849 # context, which is trailing for this
850 # split hunk and leading for the next
852 $next_hunk_start = $i;
854 push @
{$this->{TEXT
}}, $line;
855 push @
{$this->{DISPLAY
}}, $display;
858 if (defined $next_hunk_start) {
865 if (defined $next_hunk_start) {
866 # We are done with the current hunk and
867 # this is the first real change for the
869 $hunk_start = $next_hunk_start;
870 $o_ofs = $this->{OLD
} + $this->{OCNT
};
871 $n_ofs = $this->{NEW
} + $this->{NCNT
};
872 $o_ofs -= $this->{POSTCTX
};
873 $n_ofs -= $this->{POSTCTX
};
877 push @
{$this->{TEXT
}}, $line;
878 push @
{$this->{DISPLAY
}}, $display;
892 for my $hunk (@split) {
893 $o_ofs = $hunk->{OLD
};
894 $n_ofs = $hunk->{NEW
};
895 my $o_cnt = $hunk->{OCNT
};
896 my $n_cnt = $hunk->{NCNT
};
898 my $head = ("@@ -$o_ofs" .
899 (($o_cnt != 1) ?
",$o_cnt" : '') .
901 (($n_cnt != 1) ?
",$n_cnt" : '') .
903 my $display_head = $head;
904 unshift @
{$hunk->{TEXT
}}, $head;
905 if ($diff_use_color) {
906 $display_head = colored
($fraginfo_color, $head);
908 unshift @
{$hunk->{DISPLAY
}}, $display_head;
913 sub find_last_o_ctx
{
915 my $text = $it->{TEXT
};
916 my ($o_ofs, $o_cnt) = parse_hunk_header
($text->[0]);
918 my $last_o_ctx = $o_ofs + $o_cnt;
920 my $line = $text->[$i];
931 my ($prev, $this) = @_;
932 my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) =
933 parse_hunk_header
($prev->{TEXT
}[0]);
934 my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) =
935 parse_hunk_header
($this->{TEXT
}[0]);
937 my (@line, $i, $ofs, $o_cnt, $n_cnt);
940 for ($i = 1; $i < @
{$prev->{TEXT
}}; $i++) {
941 my $line = $prev->{TEXT
}[$i];
942 if ($line =~ /^\+/) {
948 last if ($o1_ofs <= $ofs);
958 for ($i = 1; $i < @
{$this->{TEXT
}}; $i++) {
959 my $line = $this->{TEXT
}[$i];
960 if ($line =~ /^\+/) {
972 my $head = ("@@ -$o0_ofs" .
973 (($o_cnt != 1) ?
",$o_cnt" : '') .
975 (($n_cnt != 1) ?
",$n_cnt" : '') .
977 @
{$prev->{TEXT
}} = ($head, @line);
980 sub coalesce_overlapping_hunks
{
984 my ($last_o_ctx, $last_was_dirty);
986 for (grep { $_->{USE
} } @in) {
987 if ($_->{TYPE
} ne 'hunk') {
991 my $text = $_->{TEXT
};
992 my ($o_ofs) = parse_hunk_header
($text->[0]);
993 if (defined $last_o_ctx &&
994 $o_ofs <= $last_o_ctx &&
997 merge_hunk
($out[-1], $_);
1002 $last_o_ctx = find_last_o_ctx
($out[-1]);
1003 $last_was_dirty = $_->{DIRTY
};
1008 sub reassemble_patch
{
1012 # Include everything in the header except the beginning of the diff.
1013 push @patch, (grep { !/^[-+]{3}/ } @
$head);
1015 # Then include any headers from the hunk lines, which must
1016 # come before any actual hunk.
1017 while (@_ && $_[0] !~ /^@/) {
1021 # Then begin the diff.
1022 push @patch, grep { /^[-+]{3}/ } @
$head;
1024 # And then the actual hunks.
1032 colored
((/^@/ ?
$fraginfo_color :
1033 /^\+/ ?
$diff_new_color :
1034 /^-/ ?
$diff_old_color :
1040 my %edit_hunk_manually_modes = (
1042 "If the patch applies cleanly, the edited hunk will immediately be
1043 marked for staging."),
1045 "If the patch applies cleanly, the edited hunk will immediately be
1046 marked for stashing."),
1048 "If the patch applies cleanly, the edited hunk will immediately be
1049 marked for unstaging."),
1050 reset_nothead
=> N__
(
1051 "If the patch applies cleanly, the edited hunk will immediately be
1052 marked for applying."),
1053 checkout_index
=> N__
(
1054 "If the patch applies cleanly, the edited hunk will immediately be
1055 marked for discarding"),
1056 checkout_head
=> N__
(
1057 "If the patch applies cleanly, the edited hunk will immediately be
1058 marked for discarding."),
1059 checkout_nothead
=> N__
(
1060 "If the patch applies cleanly, the edited hunk will immediately be
1061 marked for applying."),
1064 sub edit_hunk_manually
{
1067 my $hunkfile = $repo->repo_path . "/addp-hunk-edit.diff";
1069 open $fh, '>', $hunkfile
1070 or die sprintf(__
("failed to open hunk edit file for writing: %s"), $!);
1071 print $fh Git
::comment_lines __
("Manual hunk edit mode -- see bottom for a quick guide.\n");
1072 print $fh @
$oldtext;
1073 my $is_reverse = $patch_mode_flavour{IS_REVERSE
};
1074 my ($remove_plus, $remove_minus) = $is_reverse ?
('-', '+') : ('+', '-');
1075 my $comment_line_char = Git
::get_comment_line_char
;
1076 print $fh Git
::comment_lines
sprintf(__
<<EOF, $remove_minus, $remove_plus, $comment_line_char),
1078 To remove '%s' lines, make them ' ' lines (context).
1079 To remove '%s' lines, delete them.
1080 Lines starting with %s will be removed.
1082 __
($edit_hunk_manually_modes{$patch_mode}),
1083 # TRANSLATORS: 'it' refers to the patch mentioned in the previous messages.
1085 If it does not apply cleanly, you will be given an opportunity to
1086 edit again. If all lines of the hunk are removed, then the edit is
1087 aborted and the hunk is left unchanged.
1091 chomp(my $editor = run_cmd_pipe(qw(git var GIT_EDITOR)));
1092 system('sh', '-c', $editor.' "$@"', $editor, $hunkfile);
1098 open $fh, '<', $hunkfile
1099 or die sprintf(__
("failed to open hunk edit file for reading: %s"), $!);
1100 my @newtext = grep { !/^$comment_line_char/ } <$fh>;
1104 # Abort if nothing remains
1105 if (!grep { /\S/ } @newtext) {
1109 # Reinsert the first hunk header if the user accidentally deleted it
1110 if ($newtext[0] !~ /^@/) {
1111 unshift @newtext, $oldtext->[0];
1117 return run_git_apply
($patch_mode_flavour{APPLY_CHECK
} . ' --check',
1118 map { @
{$_->{TEXT
}} } @_);
1121 sub _restore_terminal_and_die
{
1127 sub prompt_single_character
{
1129 local $SIG{TERM
} = \
&_restore_terminal_and_die
;
1130 local $SIG{INT
} = \
&_restore_terminal_and_die
;
1132 my $key = ReadKey
0;
1134 if ($use_termcap and $key eq "\e") {
1135 while (!defined $term_escapes{$key}) {
1136 my $next = ReadKey
0.5;
1137 last if (!defined $next);
1142 print "$key" if defined $key;
1153 print colored
$prompt_color, $prompt;
1154 my $line = prompt_single_character
;
1155 return undef unless defined $line;
1156 return 0 if $line =~ /^n/i;
1157 return 1 if $line =~ /^y/i;
1161 sub edit_hunk_loop
{
1162 my ($head, $hunk, $ix) = @_;
1163 my $text = $hunk->[$ix]->{TEXT
};
1166 $text = edit_hunk_manually
($text);
1167 if (!defined $text) {
1172 TYPE
=> $hunk->[$ix]->{TYPE
},
1176 if (diff_applies
($head,
1179 @
{$hunk}[$ix+1..$#{$hunk}])) {
1180 $newhunk->{DISPLAY
} = [color_diff
(@
{$text})];
1185 # TRANSLATORS: do not translate [y/n]
1186 # The program will only accept that input
1188 # Consider translating (saying "no" discards!) as
1189 # (saying "n" for "no" discards!) if the translation
1190 # of the word "no" does not start with n.
1191 __
('Your edited hunk does not apply. Edit again '
1192 . '(saying "no" discards!) [y/n]? ')
1198 my %help_patch_modes = (
1200 "y - stage this hunk
1201 n - do not stage this hunk
1202 q - quit; do not stage this hunk or any of the remaining ones
1203 a - stage this hunk and all later hunks in the file
1204 d - do not stage this hunk or any of the later hunks in the file"),
1206 "y - stash this hunk
1207 n - do not stash this hunk
1208 q - quit; do not stash this hunk or any of the remaining ones
1209 a - stash this hunk and all later hunks in the file
1210 d - do not stash this hunk or any of the later hunks in the file"),
1212 "y - unstage this hunk
1213 n - do not unstage this hunk
1214 q - quit; do not unstage this hunk or any of the remaining ones
1215 a - unstage this hunk and all later hunks in the file
1216 d - do not unstage this hunk or any of the later hunks in the file"),
1217 reset_nothead
=> N__
(
1218 "y - apply this hunk to index
1219 n - do not apply this hunk to index
1220 q - quit; do not apply this hunk or any of the remaining ones
1221 a - apply this hunk and all later hunks in the file
1222 d - do not apply this hunk or any of the later hunks in the file"),
1223 checkout_index
=> N__
(
1224 "y - discard this hunk from worktree
1225 n - do not discard this hunk from worktree
1226 q - quit; do not discard this hunk or any of the remaining ones
1227 a - discard this hunk and all later hunks in the file
1228 d - do not discard this hunk or any of the later hunks in the file"),
1229 checkout_head
=> N__
(
1230 "y - discard this hunk from index and worktree
1231 n - do not discard this hunk from index and worktree
1232 q - quit; do not discard this hunk or any of the remaining ones
1233 a - discard this hunk and all later hunks in the file
1234 d - do not discard this hunk or any of the later hunks in the file"),
1235 checkout_nothead
=> N__
(
1236 "y - apply this hunk to index and worktree
1237 n - do not apply this hunk to index and worktree
1238 q - quit; do not apply this hunk or any of the remaining ones
1239 a - apply this hunk and all later hunks in the file
1240 d - do not apply this hunk or any of the later hunks in the file"),
1243 sub help_patch_cmd
{
1244 print colored
$help_color, __
($help_patch_modes{$patch_mode}), "\n", __
<<EOF ;
1245 g - select a hunk to go to
1246 / - search for a hunk matching the given regex
1247 j - leave this hunk undecided, see next undecided hunk
1248 J - leave this hunk undecided, see next hunk
1249 k - leave this hunk undecided, see previous undecided hunk
1250 K - leave this hunk undecided, see previous hunk
1251 s - split the current hunk into smaller hunks
1252 e - manually edit the current hunk
1259 my $ret = run_git_apply
$cmd, @_;
1266 sub apply_patch_for_checkout_commit
{
1267 my $reverse = shift;
1268 my $applies_index = run_git_apply
'apply '.$reverse.' --cached --check', @_;
1269 my $applies_worktree = run_git_apply
'apply '.$reverse.' --check', @_;
1271 if ($applies_worktree && $applies_index) {
1272 run_git_apply
'apply '.$reverse.' --cached', @_;
1273 run_git_apply
'apply '.$reverse, @_;
1275 } elsif (!$applies_index) {
1276 print colored
$error_color, __
("The selected hunks do not apply to the index!\n");
1277 if (prompt_yesno __
("Apply them to the worktree anyway? ")) {
1278 return run_git_apply
'apply '.$reverse, @_;
1280 print colored
$error_color, __
("Nothing was applied.\n");
1289 sub patch_update_cmd
{
1290 my @all_mods = list_modified
($patch_mode_flavour{FILTER
});
1291 error_msg
sprintf(__
("ignoring unmerged: %s\n"), $_->{VALUE
})
1292 for grep { $_->{UNMERGED
} } @all_mods;
1293 @all_mods = grep { !$_->{UNMERGED
} } @all_mods;
1295 my @mods = grep { !($_->{BINARY
}) } @all_mods;
1300 print STDERR __
("Only binary files changed.\n");
1302 print STDERR __
("No changes.\n");
1310 @them = list_and_choose
({ PROMPT
=> __
('Patch update'),
1311 HEADER
=> $status_head, },
1315 return 0 if patch_update_file
($_->{VALUE
});
1319 # Generate a one line summary of a hunk.
1320 sub summarize_hunk
{
1322 my $summary = $rhunk->{TEXT
}[0];
1324 # Keep the line numbers, discard extra context.
1325 $summary =~ s/@@(.*?)@@.*/$1 /s;
1326 $summary .= " " x
(20 - length $summary);
1328 # Add some user context.
1329 for my $line (@
{$rhunk->{TEXT
}}) {
1330 if ($line =~ m/^[+-].*\w/) {
1337 return substr($summary, 0, 80) . "\n";
1341 # Print a one-line summary of each hunk in the array ref in
1342 # the first argument, starting with the index in the 2nd.
1344 my ($hunks, $i) = @_;
1347 for (; $i < @
$hunks && $ctr < 20; $i++, $ctr++) {
1349 if (defined $hunks->[$i]{USE
}) {
1350 $status = $hunks->[$i]{USE
} ?
"+" : "-";
1355 summarize_hunk
($hunks->[$i]);
1360 my %patch_update_prompt_modes = (
1362 mode
=> N__
("Stage mode change [y,n,q,a,d,/%s,?]? "),
1363 deletion
=> N__
("Stage deletion [y,n,q,a,d,/%s,?]? "),
1364 hunk
=> N__
("Stage this hunk [y,n,q,a,d,/%s,?]? "),
1367 mode
=> N__
("Stash mode change [y,n,q,a,d,/%s,?]? "),
1368 deletion
=> N__
("Stash deletion [y,n,q,a,d,/%s,?]? "),
1369 hunk
=> N__
("Stash this hunk [y,n,q,a,d,/%s,?]? "),
1372 mode
=> N__
("Unstage mode change [y,n,q,a,d,/%s,?]? "),
1373 deletion
=> N__
("Unstage deletion [y,n,q,a,d,/%s,?]? "),
1374 hunk
=> N__
("Unstage this hunk [y,n,q,a,d,/%s,?]? "),
1377 mode
=> N__
("Apply mode change to index [y,n,q,a,d,/%s,?]? "),
1378 deletion
=> N__
("Apply deletion to index [y,n,q,a,d,/%s,?]? "),
1379 hunk
=> N__
("Apply this hunk to index [y,n,q,a,d,/%s,?]? "),
1382 mode
=> N__
("Discard mode change from worktree [y,n,q,a,d,/%s,?]? "),
1383 deletion
=> N__
("Discard deletion from worktree [y,n,q,a,d,/%s,?]? "),
1384 hunk
=> N__
("Discard this hunk from worktree [y,n,q,a,d,/%s,?]? "),
1387 mode
=> N__
("Discard mode change from index and worktree [y,n,q,a,d,/%s,?]? "),
1388 deletion
=> N__
("Discard deletion from index and worktree [y,n,q,a,d,/%s,?]? "),
1389 hunk
=> N__
("Discard this hunk from index and worktree [y,n,q,a,d,/%s,?]? "),
1391 checkout_nothead
=> {
1392 mode
=> N__
("Apply mode change to index and worktree [y,n,q,a,d,/%s,?]? "),
1393 deletion
=> N__
("Apply deletion to index and worktree [y,n,q,a,d,/%s,?]? "),
1394 hunk
=> N__
("Apply this hunk to index and worktree [y,n,q,a,d,/%s,?]? "),
1398 sub patch_update_file
{
1402 my ($head, @hunk) = parse_diff
($path);
1403 ($head, my $mode, my $deletion) = parse_diff_header
($head);
1404 for (@
{$head->{DISPLAY
}}) {
1408 if (@
{$mode->{TEXT
}}) {
1409 unshift @hunk, $mode;
1411 if (@
{$deletion->{TEXT
}}) {
1412 foreach my $hunk (@hunk) {
1413 push @
{$deletion->{TEXT
}}, @
{$hunk->{TEXT
}};
1414 push @
{$deletion->{DISPLAY
}}, @
{$hunk->{DISPLAY
}};
1416 @hunk = ($deletion);
1419 $num = scalar @hunk;
1423 my ($prev, $next, $other, $undecided, $i);
1429 for ($i = 0; $i < $ix; $i++) {
1430 if (!defined $hunk[$i]{USE
}) {
1439 for ($i = $ix + 1; $i < $num; $i++) {
1440 if (!defined $hunk[$i]{USE
}) {
1446 if ($ix < $num - 1) {
1452 for ($i = 0; $i < $num; $i++) {
1453 if (!defined $hunk[$i]{USE
}) {
1458 last if (!$undecided);
1460 if ($hunk[$ix]{TYPE
} eq 'hunk' &&
1461 hunk_splittable
($hunk[$ix]{TEXT
})) {
1464 if ($hunk[$ix]{TYPE
} eq 'hunk') {
1467 for (@
{$hunk[$ix]{DISPLAY
}}) {
1470 print colored
$prompt_color,
1471 sprintf(__
($patch_update_prompt_modes{$patch_mode}{$hunk[$ix]{TYPE
}}), $other);
1473 my $line = prompt_single_character
;
1474 last unless defined $line;
1476 if ($line =~ /^y/i) {
1477 $hunk[$ix]{USE
} = 1;
1479 elsif ($line =~ /^n/i) {
1480 $hunk[$ix]{USE
} = 0;
1482 elsif ($line =~ /^a/i) {
1483 while ($ix < $num) {
1484 if (!defined $hunk[$ix]{USE
}) {
1485 $hunk[$ix]{USE
} = 1;
1491 elsif ($other =~ /g/ && $line =~ /^g(.*)/) {
1493 my $no = $ix > 10 ?
$ix - 10 : 0;
1494 while ($response eq '') {
1495 $no = display_hunks
(\
@hunk, $no);
1497 print __
("go to which hunk (<ret> to see more)? ");
1499 print __
("go to which hunk? ");
1501 $response = <STDIN
>;
1502 if (!defined $response) {
1507 if ($response !~ /^\s*\d+\s*$/) {
1508 error_msg
sprintf(__
("Invalid number: '%s'\n"),
1510 } elsif (0 < $response && $response <= $num) {
1511 $ix = $response - 1;
1513 error_msg
sprintf(__n
("Sorry, only %d hunk available.\n",
1514 "Sorry, only %d hunks available.\n", $num), $num);
1518 elsif ($line =~ /^d/i) {
1519 while ($ix < $num) {
1520 if (!defined $hunk[$ix]{USE
}) {
1521 $hunk[$ix]{USE
} = 0;
1527 elsif ($line =~ /^q/i) {
1528 for ($i = 0; $i < $num; $i++) {
1529 if (!defined $hunk[$i]{USE
}) {
1536 elsif ($line =~ m
|^/(.*)|) {
1539 print colored
$prompt_color, __
("search for regex? ");
1541 if (defined $regex) {
1547 $search_string = qr{$regex}m;
1550 my ($err,$exp) = ($@
, $1);
1551 $err =~ s/ at .*git-add--interactive line \d+, <STDIN> line \d+.*$//;
1552 error_msg
sprintf(__
("Malformed search regexp %s: %s\n"), $exp, $err);
1557 my $text = join ("", @
{$hunk[$iy]{TEXT
}});
1558 last if ($text =~ $search_string);
1560 $iy = 0 if ($iy >= $num);
1562 error_msg __
("No hunk matches the given pattern\n");
1569 elsif ($line =~ /^K/) {
1570 if ($other =~ /K/) {
1574 error_msg __
("No previous hunk\n");
1578 elsif ($line =~ /^J/) {
1579 if ($other =~ /J/) {
1583 error_msg __
("No next hunk\n");
1587 elsif ($line =~ /^k/) {
1588 if ($other =~ /k/) {
1592 !defined $hunk[$ix]{USE
});
1596 error_msg __
("No previous hunk\n");
1600 elsif ($line =~ /^j/) {
1601 if ($other !~ /j/) {
1602 error_msg __
("No next hunk\n");
1606 elsif ($other =~ /s/ && $line =~ /^s/) {
1607 my @split = split_hunk
($hunk[$ix]{TEXT
}, $hunk[$ix]{DISPLAY
});
1609 print colored
$header_color, sprintf(
1610 __n
("Split into %d hunk.\n",
1611 "Split into %d hunks.\n",
1612 scalar(@split)), scalar(@split));
1614 splice (@hunk, $ix, 1, @split);
1615 $num = scalar @hunk;
1618 elsif ($other =~ /e/ && $line =~ /^e/) {
1619 my $newhunk = edit_hunk_loop
($head, \
@hunk, $ix);
1620 if (defined $newhunk) {
1621 splice @hunk, $ix, 1, $newhunk;
1625 help_patch_cmd
($other);
1631 last if ($ix >= $num ||
1632 !defined $hunk[$ix]{USE
});
1637 @hunk = coalesce_overlapping_hunks
(@hunk);
1643 push @result, @
{$_->{TEXT
}};
1648 my @patch = reassemble_patch
($head->{TEXT
}, @result);
1649 my $apply_routine = $patch_mode_flavour{APPLY
};
1650 &$apply_routine(@patch);
1659 my @mods = list_modified
('index-only');
1660 @mods = grep { !($_->{BINARY
}) } @mods;
1662 my (@them) = list_and_choose
({ PROMPT
=> __
('Review diff'),
1664 HEADER
=> $status_head, },
1667 my $reference = (is_initial_commit
()) ? get_empty_tree
() : 'HEAD';
1668 system(qw(git diff -p --cached), $reference, '--',
1669 map { $_->{VALUE
} } @them);
1678 # TRANSLATORS: please do not translate the command names
1679 # 'status', 'update', 'revert', etc.
1680 print colored
$help_color, __
<<'EOF' ;
1681 status - show paths with changes
1682 update - add working tree state to the staged set of changes
1683 revert - revert staged set of changes back to the HEAD version
1684 patch - pick hunks and update selectively
1685 diff - view diff between HEAD and index
1686 add untracked - add contents of untracked files to the staged set of changes
1691 return unless @ARGV;
1692 my $arg = shift @ARGV;
1693 if ($arg =~ /--patch(?:=(.*))?/) {
1695 if ($1 eq 'reset') {
1696 $patch_mode = 'reset_head';
1697 $patch_mode_revision = 'HEAD';
1698 $arg = shift @ARGV or die __
("missing --");
1700 $patch_mode_revision = $arg;
1701 $patch_mode = ($arg eq 'HEAD' ?
1702 'reset_head' : 'reset_nothead');
1703 $arg = shift @ARGV or die __
("missing --");
1705 } elsif ($1 eq 'checkout') {
1706 $arg = shift @ARGV or die __
("missing --");
1708 $patch_mode = 'checkout_index';
1710 $patch_mode_revision = $arg;
1711 $patch_mode = ($arg eq 'HEAD' ?
1712 'checkout_head' : 'checkout_nothead');
1713 $arg = shift @ARGV or die __
("missing --");
1715 } elsif ($1 eq 'stage' or $1 eq 'stash') {
1717 $arg = shift @ARGV or die __
("missing --");
1719 die sprintf(__
("unknown --patch mode: %s"), $1);
1722 $patch_mode = 'stage';
1723 $arg = shift @ARGV or die __
("missing --");
1725 die sprintf(__
("invalid argument %s, expecting --"),
1726 $arg) unless $arg eq "--";
1727 %patch_mode_flavour = %{$patch_modes{$patch_mode}};
1730 elsif ($arg ne "--") {
1731 die sprintf(__
("invalid argument %s, expecting --"), $arg);
1736 my @cmd = ([ 'status', \
&status_cmd
, ],
1737 [ 'update', \
&update_cmd
, ],
1738 [ 'revert', \
&revert_cmd
, ],
1739 [ 'add untracked', \
&add_untracked_cmd
, ],
1740 [ 'patch', \
&patch_update_cmd
, ],
1741 [ 'diff', \
&diff_cmd
, ],
1742 [ 'quit', \
&quit_cmd
, ],
1743 [ 'help', \
&help_cmd
, ],
1746 my ($it) = list_and_choose
({ PROMPT
=> __
('What now'),
1749 HEADER
=> __
('*** Commands ***'),
1750 ON_EOF
=> \
&quit_cmd
,
1751 IMMEDIATE
=> 1 }, @cmd);