6 use Git
qw(unquote_path);
9 binmode(STDOUT
, ":raw");
11 my $repo = Git
->repository();
13 my $menu_use_color = $repo->get_colorbool('color.interactive');
14 my ($prompt_color, $header_color, $help_color) =
16 $repo->get_color('color.interactive.prompt', 'bold blue'),
17 $repo->get_color('color.interactive.header', 'bold'),
18 $repo->get_color('color.interactive.help', 'red bold'),
21 if ($menu_use_color) {
22 my $help_color_spec = ($repo->config('color.interactive.help') or
24 $error_color = $repo->get_color('color.interactive.error',
28 my $diff_use_color = $repo->get_colorbool('color.diff');
29 my ($fraginfo_color) =
31 $repo->get_color('color.diff.frag', 'cyan'),
33 my ($diff_plain_color) =
35 $repo->get_color('color.diff.plain', ''),
37 my ($diff_old_color) =
39 $repo->get_color('color.diff.old', 'red'),
41 my ($diff_new_color) =
43 $repo->get_color('color.diff.new', 'green'),
46 my $normal_color = $repo->get_color("", "reset");
48 my $diff_algorithm = $repo->config('diff.algorithm');
49 my $diff_filter = $repo->config('interactive.difffilter');
57 if ($repo->config_bool("interactive.singlekey")) {
59 require Term
::ReadKey
;
60 Term
::ReadKey
->import;
64 print STDERR
"missing Term::ReadKey, disabling interactive.singlekey\n";
68 my $termcap = Term
::Cap
->Tgetent;
69 foreach (values %$termcap) {
70 $term_escapes{$_} = 1 if /^\e/;
78 my $string = join("", @_);
81 # Put a color code at the beginning of each line, a reset at the end
82 # color after newlines that are not at the end of the string
83 $string =~ s/(\n+)(.)/$1$color$2/g;
84 # reset before newlines
85 $string =~ s/(\n+)/$normal_color$1/g;
86 # codes at beginning and end (if necessary):
87 $string =~ s/^/$color/;
88 $string =~ s/$/$normal_color/ unless $string =~ /\n$/;
93 # command line options
96 my $patch_mode_revision;
99 sub apply_patch_for_checkout_commit
;
100 sub apply_patch_for_stash
;
104 DIFF
=> 'diff-files -p',
105 APPLY
=> sub { apply_patch
'apply --cached', @_; },
106 APPLY_CHECK
=> 'apply --cached',
107 FILTER
=> 'file-only',
111 DIFF
=> 'diff-index -p HEAD',
112 APPLY
=> sub { apply_patch
'apply --cached', @_; },
113 APPLY_CHECK
=> 'apply --cached',
118 DIFF
=> 'diff-index -p --cached',
119 APPLY
=> sub { apply_patch
'apply -R --cached', @_; },
120 APPLY_CHECK
=> 'apply -R --cached',
121 FILTER
=> 'index-only',
125 DIFF
=> 'diff-index -R -p --cached',
126 APPLY
=> sub { apply_patch
'apply --cached', @_; },
127 APPLY_CHECK
=> 'apply --cached',
128 FILTER
=> 'index-only',
131 'checkout_index' => {
132 DIFF
=> 'diff-files -p',
133 APPLY
=> sub { apply_patch
'apply -R', @_; },
134 APPLY_CHECK
=> 'apply -R',
135 FILTER
=> 'file-only',
139 DIFF
=> 'diff-index -p',
140 APPLY
=> sub { apply_patch_for_checkout_commit
'-R', @_ },
141 APPLY_CHECK
=> 'apply -R',
145 'checkout_nothead' => {
146 DIFF
=> 'diff-index -R -p',
147 APPLY
=> sub { apply_patch_for_checkout_commit
'', @_ },
148 APPLY_CHECK
=> 'apply',
154 $patch_mode = 'stage';
155 my %patch_mode_flavour = %{$patch_modes{$patch_mode}};
158 if ($^O
eq 'MSWin32') {
159 my @invalid = grep {m/[":*]/} @_;
160 die "$^O does not support: @invalid\n" if @invalid;
161 my @args = map { m/ /o ?
"\"$_\"": $_ } @_;
165 open($fh, '-|', @_) or die;
170 my ($GIT_DIR) = run_cmd_pipe
(qw(git rev-parse --git-dir));
172 if (!defined $GIT_DIR) {
173 exit(1); # rev-parse would have already said "not a git repo"
179 open $fh, 'git update-index --refresh |'
182 ;# ignore 'needs update'
192 run_cmd_pipe
(qw(git ls-files --others --exclude-standard --), @ARGV);
195 # TRANSLATORS: you can adjust this to align "git add -i" status menu
196 my $status_fmt = __
('%12s %12s %s');
197 my $status_head = sprintf($status_fmt, __
('staged'), __
('unstaged'), __
('path'));
201 sub is_initial_commit
{
202 $initial = system('git rev-parse HEAD -- >/dev/null 2>&1') != 0
203 unless defined $initial;
211 return $empty_tree if defined $empty_tree;
213 $empty_tree = run_cmd_pipe
(qw(git hash-object -t tree /dev/null));
219 sub get_diff_reference
{
221 if (defined $ref and $ref ne 'HEAD') {
223 } elsif (is_initial_commit
()) {
224 return get_empty_tree
();
230 # Returns list of hashes, contents of each of which are:
232 # BINARY: is a binary path
233 # INDEX: is index different from HEAD?
234 # FILE: is file different from index?
235 # INDEX_ADDDEL: is it add/delete between HEAD and index?
236 # FILE_ADDDEL: is it add/delete between index and file?
237 # UNMERGED: is the path unmerged
242 my ($add, $del, $adddel, $file);
244 my $reference = get_diff_reference
($patch_mode_revision);
245 for (run_cmd_pipe
(qw(git diff-index --cached
246 --numstat --summary), $reference,
248 if (($add, $del, $file) =
249 /^([-\d]+) ([-\d]+) (.*)/) {
251 $file = unquote_path
($file);
252 if ($add eq '-' && $del eq '-') {
253 $change = __
('binary');
257 $change = "+$add/-$del";
262 FILE
=> __
('nothing'),
265 elsif (($adddel, $file) =
266 /^ (create|delete) mode [0-7]+ (.*)$/) {
267 $file = unquote_path
($file);
268 $data{$file}{INDEX_ADDDEL
} = $adddel;
272 for (run_cmd_pipe
(qw(git diff-files --ignore-submodules=dirty --numstat --summary --raw --), @ARGV)) {
273 if (($add, $del, $file) =
274 /^([-\d]+) ([-\d]+) (.*)/) {
275 $file = unquote_path
($file);
277 if ($add eq '-' && $del eq '-') {
278 $change = __
('binary');
282 $change = "+$add/-$del";
284 $data{$file}{FILE
} = $change;
286 $data{$file}{BINARY
} = 1;
289 elsif (($adddel, $file) =
290 /^ (create|delete) mode [0-7]+ (.*)$/) {
291 $file = unquote_path
($file);
292 $data{$file}{FILE_ADDDEL
} = $adddel;
294 elsif (/^:[0-7]+ [0-7]+ [0-9a-f]+ [0-9a-f]+ (.) (.*)$/) {
295 $file = unquote_path
($2);
296 if (!exists $data{$file}) {
298 INDEX
=> __
('unchanged'),
303 $data{$file}{UNMERGED
} = 1;
308 for (sort keys %data) {
312 if ($only eq 'index-only') {
313 next if ($it->{INDEX
} eq __
('unchanged'));
315 if ($only eq 'file-only') {
316 next if ($it->{FILE
} eq __
('nothing'));
328 my ($string, @stuff) = @_;
330 for (my $i = 0; $i < @stuff; $i++) {
334 if ((ref $it) eq 'ARRAY') {
342 if ($it =~ /^$string/) {
346 if (defined $hit && defined $found) {
356 # inserts string into trie and updates count for each character
358 my ($trie, $string) = @_;
359 foreach (split //, $string) {
360 $trie = $trie->{$_} ||= {COUNT
=> 0};
365 # returns an array of tuples (prefix, remainder)
366 sub find_unique_prefixes
{
370 # any single prefix exceeding the soft limit is omitted
371 # if any prefix exceeds the hard limit all are omitted
372 # 0 indicates no limit
376 # build a trie modelling all possible options
378 foreach my $print (@stuff) {
379 if ((ref $print) eq 'ARRAY') {
380 $print = $print->[0];
382 elsif ((ref $print) eq 'HASH') {
383 $print = $print->{VALUE
};
385 update_trie
(\
%trie, $print);
386 push @return, $print;
389 # use the trie to find the unique prefixes
390 for (my $i = 0; $i < @return; $i++) {
391 my $ret = $return[$i];
392 my @letters = split //, $ret;
394 my ($prefix, $remainder);
396 for ($j = 0; $j < @letters; $j++) {
397 my $letter = $letters[$j];
398 if ($search{$letter}{COUNT
} == 1) {
399 $prefix = substr $ret, 0, $j + 1;
400 $remainder = substr $ret, $j + 1;
404 my $prefix = substr $ret, 0, $j;
406 if ($hard_limit && $j + 1 > $hard_limit);
408 %search = %{$search{$letter}};
410 if (ord($letters[0]) > 127 ||
411 ($soft_limit && $j + 1 > $soft_limit)) {
415 $return[$i] = [$prefix, $remainder];
420 # filters out prefixes which have special meaning to list_and_choose()
421 sub is_valid_prefix
{
423 return (defined $prefix) &&
424 !($prefix =~ /[\s,]/) && # separators
425 !($prefix =~ /^-/) && # deselection
426 !($prefix =~ /^\d+/) && # selection
427 ($prefix ne '*') && # "all" wildcard
428 ($prefix ne '?'); # prompt help
431 # given a prefix/remainder tuple return a string with the prefix highlighted
432 # for now use square brackets; later might use ANSI colors (underline, bold)
433 sub highlight_prefix
{
435 my $remainder = shift;
437 if (!defined $prefix) {
441 if (!is_valid_prefix
($prefix)) {
442 return "$prefix$remainder";
445 if (!$menu_use_color) {
446 return "[$prefix]$remainder";
449 return "$prompt_color$prefix$normal_color$remainder";
453 print STDERR colored
$error_color, @_;
456 sub list_and_choose
{
457 my ($opts, @stuff) = @_;
458 my (@chosen, @return);
463 my @prefixes = find_unique_prefixes
(@stuff) unless $opts->{LIST_ONLY
};
469 if ($opts->{HEADER
}) {
470 if (!$opts->{LIST_FLAT
}) {
473 print colored
$header_color, "$opts->{HEADER}\n";
475 for ($i = 0; $i < @stuff; $i++) {
476 my $chosen = $chosen[$i] ?
'*' : ' ';
477 my $print = $stuff[$i];
478 my $ref = ref $print;
479 my $highlighted = highlight_prefix
(@
{$prefixes[$i]})
481 if ($ref eq 'ARRAY') {
482 $print = $highlighted || $print->[0];
484 elsif ($ref eq 'HASH') {
485 my $value = $highlighted || $print->{VALUE
};
486 $print = sprintf($status_fmt,
492 $print = $highlighted || $print;
494 printf("%s%2d: %s", $chosen, $i+1, $print);
495 if (($opts->{LIST_FLAT
}) &&
496 (($i + 1) % ($opts->{LIST_FLAT
}))) {
509 return if ($opts->{LIST_ONLY
});
511 print colored
$prompt_color, $opts->{PROMPT
};
512 if ($opts->{SINGLETON
}) {
521 $opts->{ON_EOF
}->() if $opts->{ON_EOF
};
528 singleton_prompt_help_cmd
() :
532 for my $choice (split(/[\s,]+/, $line)) {
536 # Input that begins with '-'; unchoose
537 if ($choice =~ s/^-//) {
540 # A range can be specified like 5-7 or 5-.
541 if ($choice =~ /^(\d+)-(\d*)$/) {
542 ($bottom, $top) = ($1, length($2) ?
$2 : 1 + @stuff);
544 elsif ($choice =~ /^\d+$/) {
545 $bottom = $top = $choice;
547 elsif ($choice eq '*') {
552 $bottom = $top = find_unique
($choice, @stuff);
553 if (!defined $bottom) {
554 error_msg
sprintf(__
("Huh (%s)?\n"), $choice);
558 if ($opts->{SINGLETON
} && $bottom != $top) {
559 error_msg
sprintf(__
("Huh (%s)?\n"), $choice);
562 for ($i = $bottom-1; $i <= $top-1; $i++) {
563 next if (@stuff <= $i || $i < 0);
564 $chosen[$i] = $choose;
567 last if ($opts->{IMMEDIATE
} || $line eq '*');
569 for ($i = 0; $i < @stuff; $i++) {
571 push @return, $stuff[$i];
577 sub singleton_prompt_help_cmd
{
578 print colored
$help_color, __
<<'EOF' ;
580 1 - select a numbered item
581 foo - select item based on unique prefix
582 - (empty) select nothing
586 sub prompt_help_cmd
{
587 print colored
$help_color, __
<<'EOF' ;
589 1 - select a single item
590 3-5 - select a range of items
591 2-3,6-9 - select multiple ranges
592 foo - select item based on unique prefix
593 -... - unselect specified items
595 - (empty) finish selecting
600 list_and_choose
({ LIST_ONLY
=> 1, HEADER
=> $status_head },
608 if ($did eq 'added') {
609 printf(__n
("added %d path\n", "added %d paths\n",
611 } elsif ($did eq 'updated') {
612 printf(__n
("updated %d path\n", "updated %d paths\n",
614 } elsif ($did eq 'reverted') {
615 printf(__n
("reverted %d path\n", "reverted %d paths\n",
618 printf(__n
("touched %d path\n", "touched %d paths\n",
624 my @mods = list_modified
('file-only');
627 my @update = list_and_choose
({ PROMPT
=> __
('Update'),
628 HEADER
=> $status_head, },
631 system(qw(git update-index --add --remove --),
632 map { $_->{VALUE
} } @update);
633 say_n_paths
('updated', @update);
639 my @update = list_and_choose
({ PROMPT
=> __
('Revert'),
640 HEADER
=> $status_head, },
643 if (is_initial_commit
()) {
644 system(qw(git rm --cached),
645 map { $_->{VALUE
} } @update);
648 my @lines = run_cmd_pipe
(qw(git ls-tree HEAD --),
649 map { $_->{VALUE
} } @update);
651 open $fh, '| git update-index --index-info'
658 if ($_->{INDEX_ADDDEL
} &&
659 $_->{INDEX_ADDDEL
} eq 'create') {
660 system(qw(git update-index --force-remove --),
662 printf(__
("note: %s is untracked now.\n"), $_->{VALUE
});
667 say_n_paths
('reverted', @update);
672 sub add_untracked_cmd
{
673 my @add = list_and_choose
({ PROMPT
=> __
('Add untracked') },
676 system(qw(git update-index --add --), @add);
677 say_n_paths
('added', @add);
679 print __
("No untracked files.\n");
687 open $fh, '| git ' . $cmd . " --allow-overlap";
694 my @diff_cmd = split(" ", $patch_mode_flavour{DIFF
});
695 if (defined $diff_algorithm) {
696 splice @diff_cmd, 1, 0, "--diff-algorithm=${diff_algorithm}";
698 if (defined $patch_mode_revision) {
699 push @diff_cmd, get_diff_reference
($patch_mode_revision);
701 my @diff = run_cmd_pipe
("git", @diff_cmd, "--", $path);
703 if ($diff_use_color) {
704 my @display_cmd = ("git", @diff_cmd, qw(--color --), $path);
705 if (defined $diff_filter) {
706 # quotemeta is overkill, but sufficient for shell-quoting
707 my $diff = join(' ', map { quotemeta } @display_cmd);
708 @display_cmd = ("$diff | $diff_filter");
711 @colored = run_cmd_pipe
(@display_cmd);
713 my (@hunk) = { TEXT
=> [], DISPLAY
=> [], TYPE
=> 'header' };
715 if (@colored && @colored != @diff) {
717 "fatal: mismatched output from interactive.diffFilter\n",
718 "hint: Your filter must maintain a one-to-one correspondence\n",
719 "hint: between its input and output lines.\n";
723 for (my $i = 0; $i < @diff; $i++) {
724 if ($diff[$i] =~ /^@@ /) {
725 push @hunk, { TEXT
=> [], DISPLAY
=> [],
728 push @
{$hunk[-1]{TEXT
}}, $diff[$i];
729 push @
{$hunk[-1]{DISPLAY
}},
730 (@colored ?
$colored[$i] : $diff[$i]);
735 sub parse_diff_header
{
738 my $head = { TEXT
=> [], DISPLAY
=> [], TYPE
=> 'header' };
739 my $mode = { TEXT
=> [], DISPLAY
=> [], TYPE
=> 'mode' };
740 my $deletion = { TEXT
=> [], DISPLAY
=> [], TYPE
=> 'deletion' };
742 for (my $i = 0; $i < @
{$src->{TEXT
}}; $i++) {
744 $src->{TEXT
}->[$i] =~ /^(old|new) mode (\d+)$/ ?
$mode :
745 $src->{TEXT
}->[$i] =~ /^deleted file/ ?
$deletion :
747 push @
{$dest->{TEXT
}}, $src->{TEXT
}->[$i];
748 push @
{$dest->{DISPLAY
}}, $src->{DISPLAY
}->[$i];
750 return ($head, $mode, $deletion);
753 sub hunk_splittable
{
756 my @s = split_hunk
($text);
760 sub parse_hunk_header
{
762 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
763 $line =~ /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
764 $o_cnt = 1 unless defined $o_cnt;
765 $n_cnt = 1 unless defined $n_cnt;
766 return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
769 sub format_hunk_header
{
770 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = @_;
771 return ("@@ -$o_ofs" .
772 (($o_cnt != 1) ?
",$o_cnt" : '') .
774 (($n_cnt != 1) ?
",$n_cnt" : '') .
779 my ($text, $display) = @_;
781 if (!defined $display) {
784 # If there are context lines in the middle of a hunk,
785 # it can be split, but we would need to take care of
788 my ($o_ofs, undef, $n_ofs) = parse_hunk_header
($text->[0]);
793 my $next_hunk_start = undef;
794 my $i = $hunk_start - 1;
808 while (++$i < @
$text) {
809 my $line = $text->[$i];
810 my $display = $display->[$i];
811 if ($line =~ /^\\/) {
812 push @
{$this->{TEXT
}}, $line;
813 push @
{$this->{DISPLAY
}}, $display;
817 if ($this->{ADDDEL
} &&
818 !defined $next_hunk_start) {
819 # We have seen leading context and
820 # adds/dels and then here is another
821 # context, which is trailing for this
822 # split hunk and leading for the next
824 $next_hunk_start = $i;
826 push @
{$this->{TEXT
}}, $line;
827 push @
{$this->{DISPLAY
}}, $display;
830 if (defined $next_hunk_start) {
837 if (defined $next_hunk_start) {
838 # We are done with the current hunk and
839 # this is the first real change for the
841 $hunk_start = $next_hunk_start;
842 $o_ofs = $this->{OLD
} + $this->{OCNT
};
843 $n_ofs = $this->{NEW
} + $this->{NCNT
};
844 $o_ofs -= $this->{POSTCTX
};
845 $n_ofs -= $this->{POSTCTX
};
849 push @
{$this->{TEXT
}}, $line;
850 push @
{$this->{DISPLAY
}}, $display;
864 for my $hunk (@split) {
865 $o_ofs = $hunk->{OLD
};
866 $n_ofs = $hunk->{NEW
};
867 my $o_cnt = $hunk->{OCNT
};
868 my $n_cnt = $hunk->{NCNT
};
870 my $head = format_hunk_header
($o_ofs, $o_cnt, $n_ofs, $n_cnt);
871 my $display_head = $head;
872 unshift @
{$hunk->{TEXT
}}, $head;
873 if ($diff_use_color) {
874 $display_head = colored
($fraginfo_color, $head);
876 unshift @
{$hunk->{DISPLAY
}}, $display_head;
881 sub find_last_o_ctx
{
883 my $text = $it->{TEXT
};
884 my ($o_ofs, $o_cnt) = parse_hunk_header
($text->[0]);
886 my $last_o_ctx = $o_ofs + $o_cnt;
888 my $line = $text->[$i];
899 my ($prev, $this) = @_;
900 my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) =
901 parse_hunk_header
($prev->{TEXT
}[0]);
902 my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) =
903 parse_hunk_header
($this->{TEXT
}[0]);
905 my (@line, $i, $ofs, $o_cnt, $n_cnt);
908 for ($i = 1; $i < @
{$prev->{TEXT
}}; $i++) {
909 my $line = $prev->{TEXT
}[$i];
910 if ($line =~ /^\+/) {
914 } elsif ($line =~ /^\\/) {
919 last if ($o1_ofs <= $ofs);
929 for ($i = 1; $i < @
{$this->{TEXT
}}; $i++) {
930 my $line = $this->{TEXT
}[$i];
931 if ($line =~ /^\+/) {
935 } elsif ($line =~ /^\\/) {
946 my $head = format_hunk_header
($o0_ofs, $o_cnt, $n0_ofs, $n_cnt);
947 @
{$prev->{TEXT
}} = ($head, @line);
950 sub coalesce_overlapping_hunks
{
954 my ($last_o_ctx, $last_was_dirty);
958 if ($_->{TYPE
} ne 'hunk') {
962 my $text = $_->{TEXT
};
963 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
964 parse_hunk_header
($text->[0]);
966 $ofs_delta += $o_cnt - $n_cnt;
967 # If this hunk has been edited then subtract
968 # the delta that is due to the edit.
969 if ($_->{OFS_DELTA
}) {
970 $ofs_delta -= $_->{OFS_DELTA
};
975 if ($patch_mode_flavour{IS_REVERSE
}) {
976 $o_ofs -= $ofs_delta;
978 $n_ofs += $ofs_delta;
980 $_->{TEXT
}->[0] = format_hunk_header
($o_ofs, $o_cnt,
983 # If this hunk was edited then adjust the offset delta
984 # to reflect the edit.
985 if ($_->{OFS_DELTA
}) {
986 $ofs_delta += $_->{OFS_DELTA
};
988 if (defined $last_o_ctx &&
989 $o_ofs <= $last_o_ctx &&
992 merge_hunk
($out[-1], $_);
997 $last_o_ctx = find_last_o_ctx
($out[-1]);
998 $last_was_dirty = $_->{DIRTY
};
1003 sub reassemble_patch
{
1007 # Include everything in the header except the beginning of the diff.
1008 push @patch, (grep { !/^[-+]{3}/ } @
$head);
1010 # Then include any headers from the hunk lines, which must
1011 # come before any actual hunk.
1012 while (@_ && $_[0] !~ /^@/) {
1016 # Then begin the diff.
1017 push @patch, grep { /^[-+]{3}/ } @
$head;
1019 # And then the actual hunks.
1027 colored
((/^@/ ?
$fraginfo_color :
1028 /^\+/ ?
$diff_new_color :
1029 /^-/ ?
$diff_old_color :
1035 my %edit_hunk_manually_modes = (
1037 "If the patch applies cleanly, the edited hunk will immediately be
1038 marked for staging."),
1040 "If the patch applies cleanly, the edited hunk will immediately be
1041 marked for stashing."),
1043 "If the patch applies cleanly, the edited hunk will immediately be
1044 marked for unstaging."),
1045 reset_nothead
=> N__
(
1046 "If the patch applies cleanly, the edited hunk will immediately be
1047 marked for applying."),
1048 checkout_index
=> N__
(
1049 "If the patch applies cleanly, the edited hunk will immediately be
1050 marked for discarding."),
1051 checkout_head
=> N__
(
1052 "If the patch applies cleanly, the edited hunk will immediately be
1053 marked for discarding."),
1054 checkout_nothead
=> N__
(
1055 "If the patch applies cleanly, the edited hunk will immediately be
1056 marked for applying."),
1059 sub recount_edited_hunk
{
1061 my ($oldtext, $newtext) = @_;
1062 my ($o_cnt, $n_cnt) = (0, 0);
1063 for (@
{$newtext}[1..$#{$newtext}]) {
1064 my $mode = substr($_, 0, 1);
1067 } elsif ($mode eq '+') {
1069 } elsif ($mode eq ' ' or $mode eq "\n") {
1074 my ($o_ofs, undef, $n_ofs, undef) =
1075 parse_hunk_header
($newtext->[0]);
1076 $newtext->[0] = format_hunk_header
($o_ofs, $o_cnt, $n_ofs, $n_cnt);
1077 my (undef, $orig_o_cnt, undef, $orig_n_cnt) =
1078 parse_hunk_header
($oldtext->[0]);
1079 # Return the change in the number of lines inserted by this hunk
1080 return $orig_o_cnt - $orig_n_cnt - $o_cnt + $n_cnt;
1083 sub edit_hunk_manually
{
1086 my $hunkfile = $repo->repo_path . "/addp-hunk-edit.diff";
1088 open $fh, '>', $hunkfile
1089 or die sprintf(__
("failed to open hunk edit file for writing: %s"), $!);
1090 print $fh Git
::comment_lines __
("Manual hunk edit mode -- see bottom for a quick guide.\n");
1091 print $fh @
$oldtext;
1092 my $is_reverse = $patch_mode_flavour{IS_REVERSE
};
1093 my ($remove_plus, $remove_minus) = $is_reverse ?
('-', '+') : ('+', '-');
1094 my $comment_line_char = Git
::get_comment_line_char
;
1095 print $fh Git
::comment_lines
sprintf(__
<<EOF, $remove_minus, $remove_plus, $comment_line_char),
1097 To remove '%s' lines, make them ' ' lines (context).
1098 To remove '%s' lines, delete them.
1099 Lines starting with %s will be removed.
1101 __
($edit_hunk_manually_modes{$patch_mode}),
1102 # TRANSLATORS: 'it' refers to the patch mentioned in the previous messages.
1104 If it does not apply cleanly, you will be given an opportunity to
1105 edit again. If all lines of the hunk are removed, then the edit is
1106 aborted and the hunk is left unchanged.
1110 chomp(my $editor = run_cmd_pipe(qw(git var GIT_EDITOR)));
1111 system('sh', '-c', $editor.' "$@"', $editor, $hunkfile);
1117 open $fh, '<', $hunkfile
1118 or die sprintf(__
("failed to open hunk edit file for reading: %s"), $!);
1119 my @newtext = grep { !/^\Q$comment_line_char\E/ } <$fh>;
1123 # Abort if nothing remains
1124 if (!grep { /\S/ } @newtext) {
1128 # Reinsert the first hunk header if the user accidentally deleted it
1129 if ($newtext[0] !~ /^@/) {
1130 unshift @newtext, $oldtext->[0];
1136 return run_git_apply
($patch_mode_flavour{APPLY_CHECK
} . ' --check',
1137 map { @
{$_->{TEXT
}} } @_);
1140 sub _restore_terminal_and_die
{
1146 sub prompt_single_character
{
1148 local $SIG{TERM
} = \
&_restore_terminal_and_die
;
1149 local $SIG{INT
} = \
&_restore_terminal_and_die
;
1151 my $key = ReadKey
0;
1153 if ($use_termcap and $key eq "\e") {
1154 while (!defined $term_escapes{$key}) {
1155 my $next = ReadKey
0.5;
1156 last if (!defined $next);
1161 print "$key" if defined $key;
1172 print colored
$prompt_color, $prompt;
1173 my $line = prompt_single_character
;
1174 return undef unless defined $line;
1175 return 0 if $line =~ /^n/i;
1176 return 1 if $line =~ /^y/i;
1180 sub edit_hunk_loop
{
1181 my ($head, $hunks, $ix) = @_;
1182 my $hunk = $hunks->[$ix];
1183 my $text = $hunk->{TEXT
};
1186 my $newtext = edit_hunk_manually
($text);
1187 if (!defined $newtext) {
1192 TYPE
=> $hunk->{TYPE
},
1196 $newhunk->{OFS_DELTA
} = recount_edited_hunk
($text, $newtext);
1197 # If this hunk has already been edited then add the
1198 # offset delta of the previous edit to get the real
1199 # delta from the original unedited hunk.
1200 $hunk->{OFS_DELTA
} and
1201 $newhunk->{OFS_DELTA
} += $hunk->{OFS_DELTA
};
1202 if (diff_applies
($head,
1203 @
{$hunks}[0..$ix-1],
1205 @
{$hunks}[$ix+1..$#{$hunks}])) {
1206 $newhunk->{DISPLAY
} = [color_diff
(@
{$newtext})];
1211 # TRANSLATORS: do not translate [y/n]
1212 # The program will only accept that input
1214 # Consider translating (saying "no" discards!) as
1215 # (saying "n" for "no" discards!) if the translation
1216 # of the word "no" does not start with n.
1217 __
('Your edited hunk does not apply. Edit again '
1218 . '(saying "no" discards!) [y/n]? ')
1224 my %help_patch_modes = (
1226 "y - stage this hunk
1227 n - do not stage this hunk
1228 q - quit; do not stage this hunk or any of the remaining ones
1229 a - stage this hunk and all later hunks in the file
1230 d - do not stage this hunk or any of the later hunks in the file"),
1232 "y - stash this hunk
1233 n - do not stash this hunk
1234 q - quit; do not stash this hunk or any of the remaining ones
1235 a - stash this hunk and all later hunks in the file
1236 d - do not stash this hunk or any of the later hunks in the file"),
1238 "y - unstage this hunk
1239 n - do not unstage this hunk
1240 q - quit; do not unstage this hunk or any of the remaining ones
1241 a - unstage this hunk and all later hunks in the file
1242 d - do not unstage this hunk or any of the later hunks in the file"),
1243 reset_nothead
=> N__
(
1244 "y - apply this hunk to index
1245 n - do not apply this hunk to index
1246 q - quit; do not apply this hunk or any of the remaining ones
1247 a - apply this hunk and all later hunks in the file
1248 d - do not apply this hunk or any of the later hunks in the file"),
1249 checkout_index
=> N__
(
1250 "y - discard this hunk from worktree
1251 n - do not discard this hunk from worktree
1252 q - quit; do not discard this hunk or any of the remaining ones
1253 a - discard this hunk and all later hunks in the file
1254 d - do not discard this hunk or any of the later hunks in the file"),
1255 checkout_head
=> N__
(
1256 "y - discard this hunk from index and worktree
1257 n - do not discard this hunk from index and worktree
1258 q - quit; do not discard this hunk or any of the remaining ones
1259 a - discard this hunk and all later hunks in the file
1260 d - do not discard this hunk or any of the later hunks in the file"),
1261 checkout_nothead
=> N__
(
1262 "y - apply this hunk to index and worktree
1263 n - do not apply this hunk to index and worktree
1264 q - quit; do not apply this hunk or any of the remaining ones
1265 a - apply this hunk and all later hunks in the file
1266 d - do not apply this hunk or any of the later hunks in the file"),
1269 sub help_patch_cmd
{
1271 my $other = $_[0] . ",?";
1272 print colored
$help_color, __
($help_patch_modes{$patch_mode}), "\n",
1273 map { "$_\n" } grep {
1274 my $c = quotemeta(substr($_, 0, 1));
1276 } split "\n", __
<<EOF ;
1277 g - select a hunk to go to
1278 / - search for a hunk matching the given regex
1279 j - leave this hunk undecided, see next undecided hunk
1280 J - leave this hunk undecided, see next hunk
1281 k - leave this hunk undecided, see previous undecided hunk
1282 K - leave this hunk undecided, see previous hunk
1283 s - split the current hunk into smaller hunks
1284 e - manually edit the current hunk
1291 my $ret = run_git_apply
$cmd, @_;
1298 sub apply_patch_for_checkout_commit
{
1299 my $reverse = shift;
1300 my $applies_index = run_git_apply
'apply '.$reverse.' --cached --check', @_;
1301 my $applies_worktree = run_git_apply
'apply '.$reverse.' --check', @_;
1303 if ($applies_worktree && $applies_index) {
1304 run_git_apply
'apply '.$reverse.' --cached', @_;
1305 run_git_apply
'apply '.$reverse, @_;
1307 } elsif (!$applies_index) {
1308 print colored
$error_color, __
("The selected hunks do not apply to the index!\n");
1309 if (prompt_yesno __
("Apply them to the worktree anyway? ")) {
1310 return run_git_apply
'apply '.$reverse, @_;
1312 print colored
$error_color, __
("Nothing was applied.\n");
1321 sub patch_update_cmd
{
1322 my @all_mods = list_modified
($patch_mode_flavour{FILTER
});
1323 error_msg
sprintf(__
("ignoring unmerged: %s\n"), $_->{VALUE
})
1324 for grep { $_->{UNMERGED
} } @all_mods;
1325 @all_mods = grep { !$_->{UNMERGED
} } @all_mods;
1327 my @mods = grep { !($_->{BINARY
}) } @all_mods;
1332 print STDERR __
("Only binary files changed.\n");
1334 print STDERR __
("No changes.\n");
1338 if ($patch_mode_only) {
1342 @them = list_and_choose
({ PROMPT
=> __
('Patch update'),
1343 HEADER
=> $status_head, },
1347 return 0 if patch_update_file
($_->{VALUE
});
1351 # Generate a one line summary of a hunk.
1352 sub summarize_hunk
{
1354 my $summary = $rhunk->{TEXT
}[0];
1356 # Keep the line numbers, discard extra context.
1357 $summary =~ s/@@(.*?)@@.*/$1 /s;
1358 $summary .= " " x
(20 - length $summary);
1360 # Add some user context.
1361 for my $line (@
{$rhunk->{TEXT
}}) {
1362 if ($line =~ m/^[+-].*\w/) {
1369 return substr($summary, 0, 80) . "\n";
1373 # Print a one-line summary of each hunk in the array ref in
1374 # the first argument, starting with the index in the 2nd.
1376 my ($hunks, $i) = @_;
1379 for (; $i < @
$hunks && $ctr < 20; $i++, $ctr++) {
1381 if (defined $hunks->[$i]{USE
}) {
1382 $status = $hunks->[$i]{USE
} ?
"+" : "-";
1387 summarize_hunk
($hunks->[$i]);
1392 my %patch_update_prompt_modes = (
1394 mode
=> N__
("Stage mode change [y,n,q,a,d%s,?]? "),
1395 deletion
=> N__
("Stage deletion [y,n,q,a,d%s,?]? "),
1396 hunk
=> N__
("Stage this hunk [y,n,q,a,d%s,?]? "),
1399 mode
=> N__
("Stash mode change [y,n,q,a,d%s,?]? "),
1400 deletion
=> N__
("Stash deletion [y,n,q,a,d%s,?]? "),
1401 hunk
=> N__
("Stash this hunk [y,n,q,a,d%s,?]? "),
1404 mode
=> N__
("Unstage mode change [y,n,q,a,d%s,?]? "),
1405 deletion
=> N__
("Unstage deletion [y,n,q,a,d%s,?]? "),
1406 hunk
=> N__
("Unstage this hunk [y,n,q,a,d%s,?]? "),
1409 mode
=> N__
("Apply mode change to index [y,n,q,a,d%s,?]? "),
1410 deletion
=> N__
("Apply deletion to index [y,n,q,a,d%s,?]? "),
1411 hunk
=> N__
("Apply this hunk to index [y,n,q,a,d%s,?]? "),
1414 mode
=> N__
("Discard mode change from worktree [y,n,q,a,d%s,?]? "),
1415 deletion
=> N__
("Discard deletion from worktree [y,n,q,a,d%s,?]? "),
1416 hunk
=> N__
("Discard this hunk from worktree [y,n,q,a,d%s,?]? "),
1419 mode
=> N__
("Discard mode change from index and worktree [y,n,q,a,d%s,?]? "),
1420 deletion
=> N__
("Discard deletion from index and worktree [y,n,q,a,d%s,?]? "),
1421 hunk
=> N__
("Discard this hunk from index and worktree [y,n,q,a,d%s,?]? "),
1423 checkout_nothead
=> {
1424 mode
=> N__
("Apply mode change to index and worktree [y,n,q,a,d%s,?]? "),
1425 deletion
=> N__
("Apply deletion to index and worktree [y,n,q,a,d%s,?]? "),
1426 hunk
=> N__
("Apply this hunk to index and worktree [y,n,q,a,d%s,?]? "),
1430 sub patch_update_file
{
1434 my ($head, @hunk) = parse_diff
($path);
1435 ($head, my $mode, my $deletion) = parse_diff_header
($head);
1436 for (@
{$head->{DISPLAY
}}) {
1440 if (@
{$mode->{TEXT
}}) {
1441 unshift @hunk, $mode;
1443 if (@
{$deletion->{TEXT
}}) {
1444 foreach my $hunk (@hunk) {
1445 push @
{$deletion->{TEXT
}}, @
{$hunk->{TEXT
}};
1446 push @
{$deletion->{DISPLAY
}}, @
{$hunk->{DISPLAY
}};
1448 @hunk = ($deletion);
1451 $num = scalar @hunk;
1455 my ($prev, $next, $other, $undecided, $i);
1461 for ($i = 0; $i < $ix; $i++) {
1462 if (!defined $hunk[$i]{USE
}) {
1471 for ($i = $ix + 1; $i < $num; $i++) {
1472 if (!defined $hunk[$i]{USE
}) {
1478 if ($ix < $num - 1) {
1484 for ($i = 0; $i < $num; $i++) {
1485 if (!defined $hunk[$i]{USE
}) {
1490 last if (!$undecided);
1492 if ($hunk[$ix]{TYPE
} eq 'hunk' &&
1493 hunk_splittable
($hunk[$ix]{TEXT
})) {
1496 if ($hunk[$ix]{TYPE
} eq 'hunk') {
1499 for (@
{$hunk[$ix]{DISPLAY
}}) {
1502 print colored
$prompt_color,
1503 sprintf(__
($patch_update_prompt_modes{$patch_mode}{$hunk[$ix]{TYPE
}}), $other);
1505 my $line = prompt_single_character
;
1506 last unless defined $line;
1508 if ($line =~ /^y/i) {
1509 $hunk[$ix]{USE
} = 1;
1511 elsif ($line =~ /^n/i) {
1512 $hunk[$ix]{USE
} = 0;
1514 elsif ($line =~ /^a/i) {
1515 while ($ix < $num) {
1516 if (!defined $hunk[$ix]{USE
}) {
1517 $hunk[$ix]{USE
} = 1;
1523 elsif ($line =~ /^g(.*)/) {
1525 unless ($other =~ /g/) {
1526 error_msg __
("No other hunks to goto\n");
1529 my $no = $ix > 10 ?
$ix - 10 : 0;
1530 while ($response eq '') {
1531 $no = display_hunks
(\
@hunk, $no);
1533 print __
("go to which hunk (<ret> to see more)? ");
1535 print __
("go to which hunk? ");
1537 $response = <STDIN
>;
1538 if (!defined $response) {
1543 if ($response !~ /^\s*\d+\s*$/) {
1544 error_msg
sprintf(__
("Invalid number: '%s'\n"),
1546 } elsif (0 < $response && $response <= $num) {
1547 $ix = $response - 1;
1549 error_msg
sprintf(__n
("Sorry, only %d hunk available.\n",
1550 "Sorry, only %d hunks available.\n", $num), $num);
1554 elsif ($line =~ /^d/i) {
1555 while ($ix < $num) {
1556 if (!defined $hunk[$ix]{USE
}) {
1557 $hunk[$ix]{USE
} = 0;
1563 elsif ($line =~ /^q/i) {
1564 for ($i = 0; $i < $num; $i++) {
1565 if (!defined $hunk[$i]{USE
}) {
1572 elsif ($line =~ m
|^/(.*)|) {
1574 unless ($other =~ m
|/|) {
1575 error_msg __
("No other hunks to search\n");
1579 print colored
$prompt_color, __
("search for regex? ");
1581 if (defined $regex) {
1587 $search_string = qr{$regex}m;
1590 my ($err,$exp) = ($@
, $1);
1591 $err =~ s/ at .*git-add--interactive line \d+, <STDIN> line \d+.*$//;
1592 error_msg
sprintf(__
("Malformed search regexp %s: %s\n"), $exp, $err);
1597 my $text = join ("", @
{$hunk[$iy]{TEXT
}});
1598 last if ($text =~ $search_string);
1600 $iy = 0 if ($iy >= $num);
1602 error_msg __
("No hunk matches the given pattern\n");
1609 elsif ($line =~ /^K/) {
1610 if ($other =~ /K/) {
1614 error_msg __
("No previous hunk\n");
1618 elsif ($line =~ /^J/) {
1619 if ($other =~ /J/) {
1623 error_msg __
("No next hunk\n");
1627 elsif ($line =~ /^k/) {
1628 if ($other =~ /k/) {
1632 !defined $hunk[$ix]{USE
});
1636 error_msg __
("No previous hunk\n");
1640 elsif ($line =~ /^j/) {
1641 if ($other !~ /j/) {
1642 error_msg __
("No next hunk\n");
1646 elsif ($line =~ /^s/) {
1647 unless ($other =~ /s/) {
1648 error_msg __
("Sorry, cannot split this hunk\n");
1651 my @split = split_hunk
($hunk[$ix]{TEXT
}, $hunk[$ix]{DISPLAY
});
1653 print colored
$header_color, sprintf(
1654 __n
("Split into %d hunk.\n",
1655 "Split into %d hunks.\n",
1656 scalar(@split)), scalar(@split));
1658 splice (@hunk, $ix, 1, @split);
1659 $num = scalar @hunk;
1662 elsif ($line =~ /^e/) {
1663 unless ($other =~ /e/) {
1664 error_msg __
("Sorry, cannot edit this hunk\n");
1667 my $newhunk = edit_hunk_loop
($head, \
@hunk, $ix);
1668 if (defined $newhunk) {
1669 splice @hunk, $ix, 1, $newhunk;
1673 help_patch_cmd
($other);
1679 last if ($ix >= $num ||
1680 !defined $hunk[$ix]{USE
});
1685 @hunk = coalesce_overlapping_hunks
(@hunk);
1691 push @result, @
{$_->{TEXT
}};
1696 my @patch = reassemble_patch
($head->{TEXT
}, @result);
1697 my $apply_routine = $patch_mode_flavour{APPLY
};
1698 &$apply_routine(@patch);
1707 my @mods = list_modified
('index-only');
1708 @mods = grep { !($_->{BINARY
}) } @mods;
1710 my (@them) = list_and_choose
({ PROMPT
=> __
('Review diff'),
1712 HEADER
=> $status_head, },
1715 my $reference = (is_initial_commit
()) ? get_empty_tree
() : 'HEAD';
1716 system(qw(git diff -p --cached), $reference, '--',
1717 map { $_->{VALUE
} } @them);
1726 # TRANSLATORS: please do not translate the command names
1727 # 'status', 'update', 'revert', etc.
1728 print colored
$help_color, __
<<'EOF' ;
1729 status - show paths with changes
1730 update - add working tree state to the staged set of changes
1731 revert - revert staged set of changes back to the HEAD version
1732 patch - pick hunks and update selectively
1733 diff - view diff between HEAD and index
1734 add untracked - add contents of untracked files to the staged set of changes
1739 return unless @ARGV;
1740 my $arg = shift @ARGV;
1741 if ($arg =~ /--patch(?:=(.*))?/) {
1743 if ($1 eq 'reset') {
1744 $patch_mode = 'reset_head';
1745 $patch_mode_revision = 'HEAD';
1746 $arg = shift @ARGV or die __
("missing --");
1748 $patch_mode_revision = $arg;
1749 $patch_mode = ($arg eq 'HEAD' ?
1750 'reset_head' : 'reset_nothead');
1751 $arg = shift @ARGV or die __
("missing --");
1753 } elsif ($1 eq 'checkout') {
1754 $arg = shift @ARGV or die __
("missing --");
1756 $patch_mode = 'checkout_index';
1758 $patch_mode_revision = $arg;
1759 $patch_mode = ($arg eq 'HEAD' ?
1760 'checkout_head' : 'checkout_nothead');
1761 $arg = shift @ARGV or die __
("missing --");
1763 } elsif ($1 eq 'stage' or $1 eq 'stash') {
1765 $arg = shift @ARGV or die __
("missing --");
1767 die sprintf(__
("unknown --patch mode: %s"), $1);
1770 $patch_mode = 'stage';
1771 $arg = shift @ARGV or die __
("missing --");
1773 die sprintf(__
("invalid argument %s, expecting --"),
1774 $arg) unless $arg eq "--";
1775 %patch_mode_flavour = %{$patch_modes{$patch_mode}};
1776 $patch_mode_only = 1;
1778 elsif ($arg ne "--") {
1779 die sprintf(__
("invalid argument %s, expecting --"), $arg);
1784 my @cmd = ([ 'status', \
&status_cmd
, ],
1785 [ 'update', \
&update_cmd
, ],
1786 [ 'revert', \
&revert_cmd
, ],
1787 [ 'add untracked', \
&add_untracked_cmd
, ],
1788 [ 'patch', \
&patch_update_cmd
, ],
1789 [ 'diff', \
&diff_cmd
, ],
1790 [ 'quit', \
&quit_cmd
, ],
1791 [ 'help', \
&help_cmd
, ],
1794 my ($it) = list_and_choose
({ PROMPT
=> __
('What now'),
1797 HEADER
=> __
('*** Commands ***'),
1798 ON_EOF
=> \
&quit_cmd
,
1799 IMMEDIATE
=> 1 }, @cmd);
1813 if ($patch_mode_only) {