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',
153 DIFF
=> 'diff-index -p',
154 APPLY
=> sub { apply_patch
'apply -R', @_ },
155 APPLY_CHECK
=> 'apply -R',
159 'worktree_nothead' => {
160 DIFF
=> 'diff-index -R -p',
161 APPLY
=> sub { apply_patch
'apply', @_ },
162 APPLY_CHECK
=> 'apply',
168 $patch_mode = 'stage';
169 my %patch_mode_flavour = %{$patch_modes{$patch_mode}};
172 if ($^O
eq 'MSWin32') {
173 my @invalid = grep {m/[":*]/} @_;
174 die "$^O does not support: @invalid\n" if @invalid;
175 my @args = map { m/ /o ?
"\"$_\"": $_ } @_;
179 open($fh, '-|', @_) or die;
181 close $fh || die "Cannot close @_ ($!)";
186 my ($GIT_DIR) = run_cmd_pipe
(qw(git rev-parse --git-dir));
188 if (!defined $GIT_DIR) {
189 exit(1); # rev-parse would have already said "not a git repo"
195 open $fh, 'git update-index --refresh |'
198 ;# ignore 'needs update'
208 run_cmd_pipe
(qw(git ls-files --others --exclude-standard --), @ARGV);
211 # TRANSLATORS: you can adjust this to align "git add -i" status menu
212 my $status_fmt = __
('%12s %12s %s');
213 my $status_head = sprintf($status_fmt, __
('staged'), __
('unstaged'), __
('path'));
217 sub is_initial_commit
{
218 $initial = system('git rev-parse HEAD -- >/dev/null 2>&1') != 0
219 unless defined $initial;
227 return $empty_tree if defined $empty_tree;
229 ($empty_tree) = run_cmd_pipe
(qw(git hash-object -t tree /dev/null));
235 sub get_diff_reference
{
237 if (defined $ref and $ref ne 'HEAD') {
239 } elsif (is_initial_commit
()) {
240 return get_empty_tree
();
246 # Returns list of hashes, contents of each of which are:
248 # BINARY: is a binary path
249 # INDEX: is index different from HEAD?
250 # FILE: is file different from index?
251 # INDEX_ADDDEL: is it add/delete between HEAD and index?
252 # FILE_ADDDEL: is it add/delete between index and file?
253 # UNMERGED: is the path unmerged
258 my ($add, $del, $adddel, $file);
260 my $reference = get_diff_reference
($patch_mode_revision);
261 for (run_cmd_pipe
(qw(git diff-index --cached
262 --numstat --summary), $reference,
264 if (($add, $del, $file) =
265 /^([-\d]+) ([-\d]+) (.*)/) {
267 $file = unquote_path
($file);
268 if ($add eq '-' && $del eq '-') {
269 $change = __
('binary');
273 $change = "+$add/-$del";
278 FILE
=> __
('nothing'),
281 elsif (($adddel, $file) =
282 /^ (create|delete) mode [0-7]+ (.*)$/) {
283 $file = unquote_path
($file);
284 $data{$file}{INDEX_ADDDEL
} = $adddel;
288 for (run_cmd_pipe
(qw(git diff-files --ignore-submodules=dirty --numstat --summary --raw --), @ARGV)) {
289 if (($add, $del, $file) =
290 /^([-\d]+) ([-\d]+) (.*)/) {
291 $file = unquote_path
($file);
293 if ($add eq '-' && $del eq '-') {
294 $change = __
('binary');
298 $change = "+$add/-$del";
300 $data{$file}{FILE
} = $change;
302 $data{$file}{BINARY
} = 1;
305 elsif (($adddel, $file) =
306 /^ (create|delete) mode [0-7]+ (.*)$/) {
307 $file = unquote_path
($file);
308 $data{$file}{FILE_ADDDEL
} = $adddel;
310 elsif (/^:[0-7]+ [0-7]+ [0-9a-f]+ [0-9a-f]+ (.) (.*)$/) {
311 $file = unquote_path
($2);
312 if (!exists $data{$file}) {
314 INDEX
=> __
('unchanged'),
319 $data{$file}{UNMERGED
} = 1;
324 for (sort keys %data) {
328 if ($only eq 'index-only') {
329 next if ($it->{INDEX
} eq __
('unchanged'));
331 if ($only eq 'file-only') {
332 next if ($it->{FILE
} eq __
('nothing'));
344 my ($string, @stuff) = @_;
346 for (my $i = 0; $i < @stuff; $i++) {
350 if ((ref $it) eq 'ARRAY') {
358 if ($it =~ /^$string/) {
362 if (defined $hit && defined $found) {
372 # inserts string into trie and updates count for each character
374 my ($trie, $string) = @_;
375 foreach (split //, $string) {
376 $trie = $trie->{$_} ||= {COUNT
=> 0};
381 # returns an array of tuples (prefix, remainder)
382 sub find_unique_prefixes
{
386 # any single prefix exceeding the soft limit is omitted
387 # if any prefix exceeds the hard limit all are omitted
388 # 0 indicates no limit
392 # build a trie modelling all possible options
394 foreach my $print (@stuff) {
395 if ((ref $print) eq 'ARRAY') {
396 $print = $print->[0];
398 elsif ((ref $print) eq 'HASH') {
399 $print = $print->{VALUE
};
401 update_trie
(\
%trie, $print);
402 push @return, $print;
405 # use the trie to find the unique prefixes
406 for (my $i = 0; $i < @return; $i++) {
407 my $ret = $return[$i];
408 my @letters = split //, $ret;
410 my ($prefix, $remainder);
412 for ($j = 0; $j < @letters; $j++) {
413 my $letter = $letters[$j];
414 if ($search{$letter}{COUNT
} == 1) {
415 $prefix = substr $ret, 0, $j + 1;
416 $remainder = substr $ret, $j + 1;
420 my $prefix = substr $ret, 0, $j;
422 if ($hard_limit && $j + 1 > $hard_limit);
424 %search = %{$search{$letter}};
426 if (ord($letters[0]) > 127 ||
427 ($soft_limit && $j + 1 > $soft_limit)) {
431 $return[$i] = [$prefix, $remainder];
436 # filters out prefixes which have special meaning to list_and_choose()
437 sub is_valid_prefix
{
439 return (defined $prefix) &&
440 !($prefix =~ /[\s,]/) && # separators
441 !($prefix =~ /^-/) && # deselection
442 !($prefix =~ /^\d+/) && # selection
443 ($prefix ne '*') && # "all" wildcard
444 ($prefix ne '?'); # prompt help
447 # given a prefix/remainder tuple return a string with the prefix highlighted
448 # for now use square brackets; later might use ANSI colors (underline, bold)
449 sub highlight_prefix
{
451 my $remainder = shift;
453 if (!defined $prefix) {
457 if (!is_valid_prefix
($prefix)) {
458 return "$prefix$remainder";
461 if (!$menu_use_color) {
462 return "[$prefix]$remainder";
465 return "$prompt_color$prefix$normal_color$remainder";
469 print STDERR colored
$error_color, @_;
472 sub list_and_choose
{
473 my ($opts, @stuff) = @_;
474 my (@chosen, @return);
479 my @prefixes = find_unique_prefixes
(@stuff) unless $opts->{LIST_ONLY
};
485 if ($opts->{HEADER
}) {
486 if (!$opts->{LIST_FLAT
}) {
489 print colored
$header_color, "$opts->{HEADER}\n";
491 for ($i = 0; $i < @stuff; $i++) {
492 my $chosen = $chosen[$i] ?
'*' : ' ';
493 my $print = $stuff[$i];
494 my $ref = ref $print;
495 my $highlighted = highlight_prefix
(@
{$prefixes[$i]})
497 if ($ref eq 'ARRAY') {
498 $print = $highlighted || $print->[0];
500 elsif ($ref eq 'HASH') {
501 my $value = $highlighted || $print->{VALUE
};
502 $print = sprintf($status_fmt,
508 $print = $highlighted || $print;
510 printf("%s%2d: %s", $chosen, $i+1, $print);
511 if (($opts->{LIST_FLAT
}) &&
512 (($i + 1) % ($opts->{LIST_FLAT
}))) {
525 return if ($opts->{LIST_ONLY
});
527 print colored
$prompt_color, $opts->{PROMPT
};
528 if ($opts->{SINGLETON
}) {
537 $opts->{ON_EOF
}->() if $opts->{ON_EOF
};
544 singleton_prompt_help_cmd
() :
548 for my $choice (split(/[\s,]+/, $line)) {
552 # Input that begins with '-'; unchoose
553 if ($choice =~ s/^-//) {
556 # A range can be specified like 5-7 or 5-.
557 if ($choice =~ /^(\d+)-(\d*)$/) {
558 ($bottom, $top) = ($1, length($2) ?
$2 : 1 + @stuff);
560 elsif ($choice =~ /^\d+$/) {
561 $bottom = $top = $choice;
563 elsif ($choice eq '*') {
568 $bottom = $top = find_unique
($choice, @stuff);
569 if (!defined $bottom) {
570 error_msg
sprintf(__
("Huh (%s)?\n"), $choice);
574 if ($opts->{SINGLETON
} && $bottom != $top) {
575 error_msg
sprintf(__
("Huh (%s)?\n"), $choice);
578 for ($i = $bottom-1; $i <= $top-1; $i++) {
579 next if (@stuff <= $i || $i < 0);
580 $chosen[$i] = $choose;
583 last if ($opts->{IMMEDIATE
} || $line eq '*');
585 for ($i = 0; $i < @stuff; $i++) {
587 push @return, $stuff[$i];
593 sub singleton_prompt_help_cmd
{
594 print colored
$help_color, __
<<'EOF' ;
596 1 - select a numbered item
597 foo - select item based on unique prefix
598 - (empty) select nothing
602 sub prompt_help_cmd
{
603 print colored
$help_color, __
<<'EOF' ;
605 1 - select a single item
606 3-5 - select a range of items
607 2-3,6-9 - select multiple ranges
608 foo - select item based on unique prefix
609 -... - unselect specified items
611 - (empty) finish selecting
616 list_and_choose
({ LIST_ONLY
=> 1, HEADER
=> $status_head },
624 if ($did eq 'added') {
625 printf(__n
("added %d path\n", "added %d paths\n",
627 } elsif ($did eq 'updated') {
628 printf(__n
("updated %d path\n", "updated %d paths\n",
630 } elsif ($did eq 'reverted') {
631 printf(__n
("reverted %d path\n", "reverted %d paths\n",
634 printf(__n
("touched %d path\n", "touched %d paths\n",
640 my @mods = list_modified
('file-only');
643 my @update = list_and_choose
({ PROMPT
=> __
('Update'),
644 HEADER
=> $status_head, },
647 system(qw(git update-index --add --remove --),
648 map { $_->{VALUE
} } @update);
649 say_n_paths
('updated', @update);
655 my @update = list_and_choose
({ PROMPT
=> __
('Revert'),
656 HEADER
=> $status_head, },
659 if (is_initial_commit
()) {
660 system(qw(git rm --cached),
661 map { $_->{VALUE
} } @update);
664 my @lines = run_cmd_pipe
(qw(git ls-tree HEAD --),
665 map { $_->{VALUE
} } @update);
667 open $fh, '| git update-index --index-info'
674 if ($_->{INDEX_ADDDEL
} &&
675 $_->{INDEX_ADDDEL
} eq 'create') {
676 system(qw(git update-index --force-remove --),
678 printf(__
("note: %s is untracked now.\n"), $_->{VALUE
});
683 say_n_paths
('reverted', @update);
688 sub add_untracked_cmd
{
689 my @add = list_and_choose
({ PROMPT
=> __
('Add untracked') },
692 system(qw(git update-index --add --), @add);
693 say_n_paths
('added', @add);
695 print __
("No untracked files.\n");
703 open $fh, '| git ' . $cmd . " --allow-overlap";
710 my @diff_cmd = split(" ", $patch_mode_flavour{DIFF
});
711 if (defined $diff_algorithm) {
712 splice @diff_cmd, 1, 0, "--diff-algorithm=${diff_algorithm}";
714 if (defined $patch_mode_revision) {
715 push @diff_cmd, get_diff_reference
($patch_mode_revision);
717 my @diff = run_cmd_pipe
("git", @diff_cmd, "--", $path);
719 if ($diff_use_color) {
720 my @display_cmd = ("git", @diff_cmd, qw(--color --), $path);
721 if (defined $diff_filter) {
722 # quotemeta is overkill, but sufficient for shell-quoting
723 my $diff = join(' ', map { quotemeta } @display_cmd);
724 @display_cmd = ("$diff | $diff_filter");
727 @colored = run_cmd_pipe
(@display_cmd);
729 my (@hunk) = { TEXT
=> [], DISPLAY
=> [], TYPE
=> 'header' };
731 if (@colored && @colored != @diff) {
733 "fatal: mismatched output from interactive.diffFilter\n",
734 "hint: Your filter must maintain a one-to-one correspondence\n",
735 "hint: between its input and output lines.\n";
739 for (my $i = 0; $i < @diff; $i++) {
740 if ($diff[$i] =~ /^@@ /) {
741 push @hunk, { TEXT
=> [], DISPLAY
=> [],
744 push @
{$hunk[-1]{TEXT
}}, $diff[$i];
745 push @
{$hunk[-1]{DISPLAY
}},
746 (@colored ?
$colored[$i] : $diff[$i]);
751 sub parse_diff_header
{
754 my $head = { TEXT
=> [], DISPLAY
=> [], TYPE
=> 'header' };
755 my $mode = { TEXT
=> [], DISPLAY
=> [], TYPE
=> 'mode' };
756 my $deletion = { TEXT
=> [], DISPLAY
=> [], TYPE
=> 'deletion' };
758 for (my $i = 0; $i < @
{$src->{TEXT
}}; $i++) {
760 $src->{TEXT
}->[$i] =~ /^(old|new) mode (\d+)$/ ?
$mode :
761 $src->{TEXT
}->[$i] =~ /^deleted file/ ?
$deletion :
763 push @
{$dest->{TEXT
}}, $src->{TEXT
}->[$i];
764 push @
{$dest->{DISPLAY
}}, $src->{DISPLAY
}->[$i];
766 return ($head, $mode, $deletion);
769 sub hunk_splittable
{
772 my @s = split_hunk
($text);
776 sub parse_hunk_header
{
778 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
779 $line =~ /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
780 $o_cnt = 1 unless defined $o_cnt;
781 $n_cnt = 1 unless defined $n_cnt;
782 return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
785 sub format_hunk_header
{
786 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = @_;
787 return ("@@ -$o_ofs" .
788 (($o_cnt != 1) ?
",$o_cnt" : '') .
790 (($n_cnt != 1) ?
",$n_cnt" : '') .
795 my ($text, $display) = @_;
797 if (!defined $display) {
800 # If there are context lines in the middle of a hunk,
801 # it can be split, but we would need to take care of
804 my ($o_ofs, undef, $n_ofs) = parse_hunk_header
($text->[0]);
809 my $next_hunk_start = undef;
810 my $i = $hunk_start - 1;
824 while (++$i < @
$text) {
825 my $line = $text->[$i];
826 my $display = $display->[$i];
827 if ($line =~ /^\\/) {
828 push @
{$this->{TEXT
}}, $line;
829 push @
{$this->{DISPLAY
}}, $display;
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 = format_hunk_header
($o_ofs, $o_cnt, $n_ofs, $n_cnt);
887 my $display_head = $head;
888 unshift @
{$hunk->{TEXT
}}, $head;
889 if ($diff_use_color) {
890 $display_head = colored
($fraginfo_color, $head);
892 unshift @
{$hunk->{DISPLAY
}}, $display_head;
897 sub find_last_o_ctx
{
899 my $text = $it->{TEXT
};
900 my ($o_ofs, $o_cnt) = parse_hunk_header
($text->[0]);
902 my $last_o_ctx = $o_ofs + $o_cnt;
904 my $line = $text->[$i];
915 my ($prev, $this) = @_;
916 my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) =
917 parse_hunk_header
($prev->{TEXT
}[0]);
918 my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) =
919 parse_hunk_header
($this->{TEXT
}[0]);
921 my (@line, $i, $ofs, $o_cnt, $n_cnt);
924 for ($i = 1; $i < @
{$prev->{TEXT
}}; $i++) {
925 my $line = $prev->{TEXT
}[$i];
926 if ($line =~ /^\+/) {
930 } elsif ($line =~ /^\\/) {
935 last if ($o1_ofs <= $ofs);
945 for ($i = 1; $i < @
{$this->{TEXT
}}; $i++) {
946 my $line = $this->{TEXT
}[$i];
947 if ($line =~ /^\+/) {
951 } elsif ($line =~ /^\\/) {
962 my $head = format_hunk_header
($o0_ofs, $o_cnt, $n0_ofs, $n_cnt);
963 @
{$prev->{TEXT
}} = ($head, @line);
966 sub coalesce_overlapping_hunks
{
970 my ($last_o_ctx, $last_was_dirty);
974 if ($_->{TYPE
} ne 'hunk') {
978 my $text = $_->{TEXT
};
979 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
980 parse_hunk_header
($text->[0]);
982 $ofs_delta += $o_cnt - $n_cnt;
983 # If this hunk has been edited then subtract
984 # the delta that is due to the edit.
985 if ($_->{OFS_DELTA
}) {
986 $ofs_delta -= $_->{OFS_DELTA
};
991 if ($patch_mode_flavour{IS_REVERSE
}) {
992 $o_ofs -= $ofs_delta;
994 $n_ofs += $ofs_delta;
996 $_->{TEXT
}->[0] = format_hunk_header
($o_ofs, $o_cnt,
999 # If this hunk was edited then adjust the offset delta
1000 # to reflect the edit.
1001 if ($_->{OFS_DELTA
}) {
1002 $ofs_delta += $_->{OFS_DELTA
};
1004 if (defined $last_o_ctx &&
1005 $o_ofs <= $last_o_ctx &&
1008 merge_hunk
($out[-1], $_);
1013 $last_o_ctx = find_last_o_ctx
($out[-1]);
1014 $last_was_dirty = $_->{DIRTY
};
1019 sub reassemble_patch
{
1023 # Include everything in the header except the beginning of the diff.
1024 push @patch, (grep { !/^[-+]{3}/ } @
$head);
1026 # Then include any headers from the hunk lines, which must
1027 # come before any actual hunk.
1028 while (@_ && $_[0] !~ /^@/) {
1032 # Then begin the diff.
1033 push @patch, grep { /^[-+]{3}/ } @
$head;
1035 # And then the actual hunks.
1043 colored
((/^@/ ?
$fraginfo_color :
1044 /^\+/ ?
$diff_new_color :
1045 /^-/ ?
$diff_old_color :
1051 my %edit_hunk_manually_modes = (
1053 "If the patch applies cleanly, the edited hunk will immediately be
1054 marked for staging."),
1056 "If the patch applies cleanly, the edited hunk will immediately be
1057 marked for stashing."),
1059 "If the patch applies cleanly, the edited hunk will immediately be
1060 marked for unstaging."),
1061 reset_nothead
=> N__
(
1062 "If the patch applies cleanly, the edited hunk will immediately be
1063 marked for applying."),
1064 checkout_index
=> N__
(
1065 "If the patch applies cleanly, the edited hunk will immediately be
1066 marked for discarding."),
1067 checkout_head
=> N__
(
1068 "If the patch applies cleanly, the edited hunk will immediately be
1069 marked for discarding."),
1070 checkout_nothead
=> N__
(
1071 "If the patch applies cleanly, the edited hunk will immediately be
1072 marked for applying."),
1073 worktree_head
=> N__
(
1074 "If the patch applies cleanly, the edited hunk will immediately be
1075 marked for discarding."),
1076 worktree_nothead
=> N__
(
1077 "If the patch applies cleanly, the edited hunk will immediately be
1078 marked for applying."),
1081 sub recount_edited_hunk
{
1083 my ($oldtext, $newtext) = @_;
1084 my ($o_cnt, $n_cnt) = (0, 0);
1085 for (@
{$newtext}[1..$#{$newtext}]) {
1086 my $mode = substr($_, 0, 1);
1089 } elsif ($mode eq '+') {
1091 } elsif ($mode eq ' ' or $mode eq "\n") {
1096 my ($o_ofs, undef, $n_ofs, undef) =
1097 parse_hunk_header
($newtext->[0]);
1098 $newtext->[0] = format_hunk_header
($o_ofs, $o_cnt, $n_ofs, $n_cnt);
1099 my (undef, $orig_o_cnt, undef, $orig_n_cnt) =
1100 parse_hunk_header
($oldtext->[0]);
1101 # Return the change in the number of lines inserted by this hunk
1102 return $orig_o_cnt - $orig_n_cnt - $o_cnt + $n_cnt;
1105 sub edit_hunk_manually
{
1108 my $hunkfile = $repo->repo_path . "/addp-hunk-edit.diff";
1110 open $fh, '>', $hunkfile
1111 or die sprintf(__
("failed to open hunk edit file for writing: %s"), $!);
1112 print $fh Git
::comment_lines __
("Manual hunk edit mode -- see bottom for a quick guide.\n");
1113 print $fh @
$oldtext;
1114 my $is_reverse = $patch_mode_flavour{IS_REVERSE
};
1115 my ($remove_plus, $remove_minus) = $is_reverse ?
('-', '+') : ('+', '-');
1116 my $comment_line_char = Git
::get_comment_line_char
;
1117 print $fh Git
::comment_lines
sprintf(__
<<EOF, $remove_minus, $remove_plus, $comment_line_char),
1119 To remove '%s' lines, make them ' ' lines (context).
1120 To remove '%s' lines, delete them.
1121 Lines starting with %s will be removed.
1123 __
($edit_hunk_manually_modes{$patch_mode}),
1124 # TRANSLATORS: 'it' refers to the patch mentioned in the previous messages.
1126 If it does not apply cleanly, you will be given an opportunity to
1127 edit again. If all lines of the hunk are removed, then the edit is
1128 aborted and the hunk is left unchanged.
1132 chomp(my ($editor) = run_cmd_pipe(qw(git var GIT_EDITOR)));
1133 system('sh', '-c', $editor.' "$@"', $editor, $hunkfile);
1139 open $fh, '<', $hunkfile
1140 or die sprintf(__
("failed to open hunk edit file for reading: %s"), $!);
1141 my @newtext = grep { !/^\Q$comment_line_char\E/ } <$fh>;
1145 # Abort if nothing remains
1146 if (!grep { /\S/ } @newtext) {
1150 # Reinsert the first hunk header if the user accidentally deleted it
1151 if ($newtext[0] !~ /^@/) {
1152 unshift @newtext, $oldtext->[0];
1158 return run_git_apply
($patch_mode_flavour{APPLY_CHECK
} . ' --check',
1159 map { @
{$_->{TEXT
}} } @_);
1162 sub _restore_terminal_and_die
{
1168 sub prompt_single_character
{
1170 local $SIG{TERM
} = \
&_restore_terminal_and_die
;
1171 local $SIG{INT
} = \
&_restore_terminal_and_die
;
1173 my $key = ReadKey
0;
1175 if ($use_termcap and $key eq "\e") {
1176 while (!defined $term_escapes{$key}) {
1177 my $next = ReadKey
0.5;
1178 last if (!defined $next);
1183 print "$key" if defined $key;
1194 print colored
$prompt_color, $prompt;
1195 my $line = prompt_single_character
;
1196 return undef unless defined $line;
1197 return 0 if $line =~ /^n/i;
1198 return 1 if $line =~ /^y/i;
1202 sub edit_hunk_loop
{
1203 my ($head, $hunks, $ix) = @_;
1204 my $hunk = $hunks->[$ix];
1205 my $text = $hunk->{TEXT
};
1208 my $newtext = edit_hunk_manually
($text);
1209 if (!defined $newtext) {
1214 TYPE
=> $hunk->{TYPE
},
1218 $newhunk->{OFS_DELTA
} = recount_edited_hunk
($text, $newtext);
1219 # If this hunk has already been edited then add the
1220 # offset delta of the previous edit to get the real
1221 # delta from the original unedited hunk.
1222 $hunk->{OFS_DELTA
} and
1223 $newhunk->{OFS_DELTA
} += $hunk->{OFS_DELTA
};
1224 if (diff_applies
($head,
1225 @
{$hunks}[0..$ix-1],
1227 @
{$hunks}[$ix+1..$#{$hunks}])) {
1228 $newhunk->{DISPLAY
} = [color_diff
(@
{$newtext})];
1233 # TRANSLATORS: do not translate [y/n]
1234 # The program will only accept that input
1236 # Consider translating (saying "no" discards!) as
1237 # (saying "n" for "no" discards!) if the translation
1238 # of the word "no" does not start with n.
1239 __
('Your edited hunk does not apply. Edit again '
1240 . '(saying "no" discards!) [y/n]? ')
1246 my %help_patch_modes = (
1248 "y - stage this hunk
1249 n - do not stage this hunk
1250 q - quit; do not stage this hunk or any of the remaining ones
1251 a - stage this hunk and all later hunks in the file
1252 d - do not stage this hunk or any of the later hunks in the file"),
1254 "y - stash this hunk
1255 n - do not stash this hunk
1256 q - quit; do not stash this hunk or any of the remaining ones
1257 a - stash this hunk and all later hunks in the file
1258 d - do not stash this hunk or any of the later hunks in the file"),
1260 "y - unstage this hunk
1261 n - do not unstage this hunk
1262 q - quit; do not unstage this hunk or any of the remaining ones
1263 a - unstage this hunk and all later hunks in the file
1264 d - do not unstage this hunk or any of the later hunks in the file"),
1265 reset_nothead
=> N__
(
1266 "y - apply this hunk to index
1267 n - do not apply this hunk to index
1268 q - quit; do not apply this hunk or any of the remaining ones
1269 a - apply this hunk and all later hunks in the file
1270 d - do not apply this hunk or any of the later hunks in the file"),
1271 checkout_index
=> N__
(
1272 "y - discard this hunk from worktree
1273 n - do not discard this hunk from worktree
1274 q - quit; do not discard this hunk or any of the remaining ones
1275 a - discard this hunk and all later hunks in the file
1276 d - do not discard this hunk or any of the later hunks in the file"),
1277 checkout_head
=> N__
(
1278 "y - discard this hunk from index and worktree
1279 n - do not discard this hunk from index and worktree
1280 q - quit; do not discard this hunk or any of the remaining ones
1281 a - discard this hunk and all later hunks in the file
1282 d - do not discard this hunk or any of the later hunks in the file"),
1283 checkout_nothead
=> N__
(
1284 "y - apply this hunk to index and worktree
1285 n - do not apply this hunk to index and worktree
1286 q - quit; do not apply this hunk or any of the remaining ones
1287 a - apply this hunk and all later hunks in the file
1288 d - do not apply this hunk or any of the later hunks in the file"),
1289 worktree_head
=> N__
(
1290 "y - discard this hunk from worktree
1291 n - do not discard this hunk from worktree
1292 q - quit; do not discard this hunk or any of the remaining ones
1293 a - discard this hunk and all later hunks in the file
1294 d - do not discard this hunk or any of the later hunks in the file"),
1295 worktree_nothead
=> N__
(
1296 "y - apply this hunk to worktree
1297 n - do not apply this hunk to worktree
1298 q - quit; do not apply this hunk or any of the remaining ones
1299 a - apply this hunk and all later hunks in the file
1300 d - do not apply this hunk or any of the later hunks in the file"),
1303 sub help_patch_cmd
{
1305 my $other = $_[0] . ",?";
1306 print colored
$help_color, __
($help_patch_modes{$patch_mode}), "\n",
1307 map { "$_\n" } grep {
1308 my $c = quotemeta(substr($_, 0, 1));
1310 } split "\n", __
<<EOF ;
1311 g - select a hunk to go to
1312 / - search for a hunk matching the given regex
1313 j - leave this hunk undecided, see next undecided hunk
1314 J - leave this hunk undecided, see next hunk
1315 k - leave this hunk undecided, see previous undecided hunk
1316 K - leave this hunk undecided, see previous hunk
1317 s - split the current hunk into smaller hunks
1318 e - manually edit the current hunk
1325 my $ret = run_git_apply
$cmd, @_;
1332 sub apply_patch_for_checkout_commit
{
1333 my $reverse = shift;
1334 my $applies_index = run_git_apply
'apply '.$reverse.' --cached --check', @_;
1335 my $applies_worktree = run_git_apply
'apply '.$reverse.' --check', @_;
1337 if ($applies_worktree && $applies_index) {
1338 run_git_apply
'apply '.$reverse.' --cached', @_;
1339 run_git_apply
'apply '.$reverse, @_;
1341 } elsif (!$applies_index) {
1342 print colored
$error_color, __
("The selected hunks do not apply to the index!\n");
1343 if (prompt_yesno __
("Apply them to the worktree anyway? ")) {
1344 return run_git_apply
'apply '.$reverse, @_;
1346 print colored
$error_color, __
("Nothing was applied.\n");
1355 sub patch_update_cmd
{
1356 my @all_mods = list_modified
($patch_mode_flavour{FILTER
});
1357 error_msg
sprintf(__
("ignoring unmerged: %s\n"), $_->{VALUE
})
1358 for grep { $_->{UNMERGED
} } @all_mods;
1359 @all_mods = grep { !$_->{UNMERGED
} } @all_mods;
1361 my @mods = grep { !($_->{BINARY
}) } @all_mods;
1366 print STDERR __
("Only binary files changed.\n");
1368 print STDERR __
("No changes.\n");
1372 if ($patch_mode_only) {
1376 @them = list_and_choose
({ PROMPT
=> __
('Patch update'),
1377 HEADER
=> $status_head, },
1381 return 0 if patch_update_file
($_->{VALUE
});
1385 # Generate a one line summary of a hunk.
1386 sub summarize_hunk
{
1388 my $summary = $rhunk->{TEXT
}[0];
1390 # Keep the line numbers, discard extra context.
1391 $summary =~ s/@@(.*?)@@.*/$1 /s;
1392 $summary .= " " x
(20 - length $summary);
1394 # Add some user context.
1395 for my $line (@
{$rhunk->{TEXT
}}) {
1396 if ($line =~ m/^[+-].*\w/) {
1403 return substr($summary, 0, 80) . "\n";
1407 # Print a one-line summary of each hunk in the array ref in
1408 # the first argument, starting with the index in the 2nd.
1410 my ($hunks, $i) = @_;
1413 for (; $i < @
$hunks && $ctr < 20; $i++, $ctr++) {
1415 if (defined $hunks->[$i]{USE
}) {
1416 $status = $hunks->[$i]{USE
} ?
"+" : "-";
1421 summarize_hunk
($hunks->[$i]);
1426 my %patch_update_prompt_modes = (
1428 mode
=> N__
("Stage mode change [y,n,q,a,d%s,?]? "),
1429 deletion
=> N__
("Stage deletion [y,n,q,a,d%s,?]? "),
1430 hunk
=> N__
("Stage this hunk [y,n,q,a,d%s,?]? "),
1433 mode
=> N__
("Stash mode change [y,n,q,a,d%s,?]? "),
1434 deletion
=> N__
("Stash deletion [y,n,q,a,d%s,?]? "),
1435 hunk
=> N__
("Stash this hunk [y,n,q,a,d%s,?]? "),
1438 mode
=> N__
("Unstage mode change [y,n,q,a,d%s,?]? "),
1439 deletion
=> N__
("Unstage deletion [y,n,q,a,d%s,?]? "),
1440 hunk
=> N__
("Unstage this hunk [y,n,q,a,d%s,?]? "),
1443 mode
=> N__
("Apply mode change to index [y,n,q,a,d%s,?]? "),
1444 deletion
=> N__
("Apply deletion to index [y,n,q,a,d%s,?]? "),
1445 hunk
=> N__
("Apply this hunk to index [y,n,q,a,d%s,?]? "),
1448 mode
=> N__
("Discard mode change from worktree [y,n,q,a,d%s,?]? "),
1449 deletion
=> N__
("Discard deletion from worktree [y,n,q,a,d%s,?]? "),
1450 hunk
=> N__
("Discard this hunk from worktree [y,n,q,a,d%s,?]? "),
1453 mode
=> N__
("Discard mode change from index and worktree [y,n,q,a,d%s,?]? "),
1454 deletion
=> N__
("Discard deletion from index and worktree [y,n,q,a,d%s,?]? "),
1455 hunk
=> N__
("Discard this hunk from index and worktree [y,n,q,a,d%s,?]? "),
1457 checkout_nothead
=> {
1458 mode
=> N__
("Apply mode change to index and worktree [y,n,q,a,d%s,?]? "),
1459 deletion
=> N__
("Apply deletion to index and worktree [y,n,q,a,d%s,?]? "),
1460 hunk
=> N__
("Apply this hunk to index and worktree [y,n,q,a,d%s,?]? "),
1463 mode
=> N__
("Discard mode change from worktree [y,n,q,a,d%s,?]? "),
1464 deletion
=> N__
("Discard deletion from worktree [y,n,q,a,d%s,?]? "),
1465 hunk
=> N__
("Discard this hunk from worktree [y,n,q,a,d%s,?]? "),
1467 worktree_nothead
=> {
1468 mode
=> N__
("Apply mode change to worktree [y,n,q,a,d%s,?]? "),
1469 deletion
=> N__
("Apply deletion to worktree [y,n,q,a,d%s,?]? "),
1470 hunk
=> N__
("Apply this hunk to worktree [y,n,q,a,d%s,?]? "),
1474 sub patch_update_file
{
1478 my ($head, @hunk) = parse_diff
($path);
1479 ($head, my $mode, my $deletion) = parse_diff_header
($head);
1480 for (@
{$head->{DISPLAY
}}) {
1484 if (@
{$mode->{TEXT
}}) {
1485 unshift @hunk, $mode;
1487 if (@
{$deletion->{TEXT
}}) {
1488 foreach my $hunk (@hunk) {
1489 push @
{$deletion->{TEXT
}}, @
{$hunk->{TEXT
}};
1490 push @
{$deletion->{DISPLAY
}}, @
{$hunk->{DISPLAY
}};
1492 @hunk = ($deletion);
1495 $num = scalar @hunk;
1499 my ($prev, $next, $other, $undecided, $i);
1505 for ($i = 0; $i < $ix; $i++) {
1506 if (!defined $hunk[$i]{USE
}) {
1515 for ($i = $ix + 1; $i < $num; $i++) {
1516 if (!defined $hunk[$i]{USE
}) {
1522 if ($ix < $num - 1) {
1528 for ($i = 0; $i < $num; $i++) {
1529 if (!defined $hunk[$i]{USE
}) {
1534 last if (!$undecided);
1536 if ($hunk[$ix]{TYPE
} eq 'hunk' &&
1537 hunk_splittable
($hunk[$ix]{TEXT
})) {
1540 if ($hunk[$ix]{TYPE
} eq 'hunk') {
1543 for (@
{$hunk[$ix]{DISPLAY
}}) {
1546 print colored
$prompt_color, "(", ($ix+1), "/$num) ",
1547 sprintf(__
($patch_update_prompt_modes{$patch_mode}{$hunk[$ix]{TYPE
}}), $other);
1549 my $line = prompt_single_character
;
1550 last unless defined $line;
1552 if ($line =~ /^y/i) {
1553 $hunk[$ix]{USE
} = 1;
1555 elsif ($line =~ /^n/i) {
1556 $hunk[$ix]{USE
} = 0;
1558 elsif ($line =~ /^a/i) {
1559 while ($ix < $num) {
1560 if (!defined $hunk[$ix]{USE
}) {
1561 $hunk[$ix]{USE
} = 1;
1567 elsif ($line =~ /^g(.*)/) {
1569 unless ($other =~ /g/) {
1570 error_msg __
("No other hunks to goto\n");
1573 my $no = $ix > 10 ?
$ix - 10 : 0;
1574 while ($response eq '') {
1575 $no = display_hunks
(\
@hunk, $no);
1577 print __
("go to which hunk (<ret> to see more)? ");
1579 print __
("go to which hunk? ");
1581 $response = <STDIN
>;
1582 if (!defined $response) {
1587 if ($response !~ /^\s*\d+\s*$/) {
1588 error_msg
sprintf(__
("Invalid number: '%s'\n"),
1590 } elsif (0 < $response && $response <= $num) {
1591 $ix = $response - 1;
1593 error_msg
sprintf(__n
("Sorry, only %d hunk available.\n",
1594 "Sorry, only %d hunks available.\n", $num), $num);
1598 elsif ($line =~ /^d/i) {
1599 while ($ix < $num) {
1600 if (!defined $hunk[$ix]{USE
}) {
1601 $hunk[$ix]{USE
} = 0;
1607 elsif ($line =~ /^q/i) {
1608 for ($i = 0; $i < $num; $i++) {
1609 if (!defined $hunk[$i]{USE
}) {
1616 elsif ($line =~ m
|^/(.*)|) {
1618 unless ($other =~ m
|/|) {
1619 error_msg __
("No other hunks to search\n");
1623 print colored
$prompt_color, __
("search for regex? ");
1625 if (defined $regex) {
1631 $search_string = qr{$regex}m;
1634 my ($err,$exp) = ($@
, $1);
1635 $err =~ s/ at .*git-add--interactive line \d+, <STDIN> line \d+.*$//;
1636 error_msg
sprintf(__
("Malformed search regexp %s: %s\n"), $exp, $err);
1641 my $text = join ("", @
{$hunk[$iy]{TEXT
}});
1642 last if ($text =~ $search_string);
1644 $iy = 0 if ($iy >= $num);
1646 error_msg __
("No hunk matches the given pattern\n");
1653 elsif ($line =~ /^K/) {
1654 if ($other =~ /K/) {
1658 error_msg __
("No previous hunk\n");
1662 elsif ($line =~ /^J/) {
1663 if ($other =~ /J/) {
1667 error_msg __
("No next hunk\n");
1671 elsif ($line =~ /^k/) {
1672 if ($other =~ /k/) {
1676 !defined $hunk[$ix]{USE
});
1680 error_msg __
("No previous hunk\n");
1684 elsif ($line =~ /^j/) {
1685 if ($other !~ /j/) {
1686 error_msg __
("No next hunk\n");
1690 elsif ($line =~ /^s/) {
1691 unless ($other =~ /s/) {
1692 error_msg __
("Sorry, cannot split this hunk\n");
1695 my @split = split_hunk
($hunk[$ix]{TEXT
}, $hunk[$ix]{DISPLAY
});
1697 print colored
$header_color, sprintf(
1698 __n
("Split into %d hunk.\n",
1699 "Split into %d hunks.\n",
1700 scalar(@split)), scalar(@split));
1702 splice (@hunk, $ix, 1, @split);
1703 $num = scalar @hunk;
1706 elsif ($line =~ /^e/) {
1707 unless ($other =~ /e/) {
1708 error_msg __
("Sorry, cannot edit this hunk\n");
1711 my $newhunk = edit_hunk_loop
($head, \
@hunk, $ix);
1712 if (defined $newhunk) {
1713 splice @hunk, $ix, 1, $newhunk;
1717 help_patch_cmd
($other);
1723 last if ($ix >= $num ||
1724 !defined $hunk[$ix]{USE
});
1729 @hunk = coalesce_overlapping_hunks
(@hunk);
1735 push @result, @
{$_->{TEXT
}};
1740 my @patch = reassemble_patch
($head->{TEXT
}, @result);
1741 my $apply_routine = $patch_mode_flavour{APPLY
};
1742 &$apply_routine(@patch);
1751 my @mods = list_modified
('index-only');
1752 @mods = grep { !($_->{BINARY
}) } @mods;
1754 my (@them) = list_and_choose
({ PROMPT
=> __
('Review diff'),
1756 HEADER
=> $status_head, },
1759 my $reference = (is_initial_commit
()) ? get_empty_tree
() : 'HEAD';
1760 system(qw(git diff -p --cached), $reference, '--',
1761 map { $_->{VALUE
} } @them);
1770 # TRANSLATORS: please do not translate the command names
1771 # 'status', 'update', 'revert', etc.
1772 print colored
$help_color, __
<<'EOF' ;
1773 status - show paths with changes
1774 update - add working tree state to the staged set of changes
1775 revert - revert staged set of changes back to the HEAD version
1776 patch - pick hunks and update selectively
1777 diff - view diff between HEAD and index
1778 add untracked - add contents of untracked files to the staged set of changes
1783 return unless @ARGV;
1784 my $arg = shift @ARGV;
1785 if ($arg =~ /--patch(?:=(.*))?/) {
1787 if ($1 eq 'reset') {
1788 $patch_mode = 'reset_head';
1789 $patch_mode_revision = 'HEAD';
1790 $arg = shift @ARGV or die __
("missing --");
1792 $patch_mode_revision = $arg;
1793 $patch_mode = ($arg eq 'HEAD' ?
1794 'reset_head' : 'reset_nothead');
1795 $arg = shift @ARGV or die __
("missing --");
1797 } elsif ($1 eq 'checkout') {
1798 $arg = shift @ARGV or die __
("missing --");
1800 $patch_mode = 'checkout_index';
1802 $patch_mode_revision = $arg;
1803 $patch_mode = ($arg eq 'HEAD' ?
1804 'checkout_head' : 'checkout_nothead');
1805 $arg = shift @ARGV or die __
("missing --");
1807 } elsif ($1 eq 'worktree') {
1808 $arg = shift @ARGV or die __
("missing --");
1810 $patch_mode = 'checkout_index';
1812 $patch_mode_revision = $arg;
1813 $patch_mode = ($arg eq 'HEAD' ?
1814 'worktree_head' : 'worktree_nothead');
1815 $arg = shift @ARGV or die __
("missing --");
1817 } elsif ($1 eq 'stage' or $1 eq 'stash') {
1819 $arg = shift @ARGV or die __
("missing --");
1821 die sprintf(__
("unknown --patch mode: %s"), $1);
1824 $patch_mode = 'stage';
1825 $arg = shift @ARGV or die __
("missing --");
1827 die sprintf(__
("invalid argument %s, expecting --"),
1828 $arg) unless $arg eq "--";
1829 %patch_mode_flavour = %{$patch_modes{$patch_mode}};
1830 $patch_mode_only = 1;
1832 elsif ($arg ne "--") {
1833 die sprintf(__
("invalid argument %s, expecting --"), $arg);
1838 my @cmd = ([ 'status', \
&status_cmd
, ],
1839 [ 'update', \
&update_cmd
, ],
1840 [ 'revert', \
&revert_cmd
, ],
1841 [ 'add untracked', \
&add_untracked_cmd
, ],
1842 [ 'patch', \
&patch_update_cmd
, ],
1843 [ 'diff', \
&diff_cmd
, ],
1844 [ 'quit', \
&quit_cmd
, ],
1845 [ 'help', \
&help_cmd
, ],
1848 my ($it) = list_and_choose
({ PROMPT
=> __
('What now'),
1851 HEADER
=> __
('*** Commands ***'),
1852 ON_EOF
=> \
&quit_cmd
,
1853 IMMEDIATE
=> 1 }, @cmd);
1867 if ($patch_mode_only) {