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;
209 return '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
212 sub get_diff_reference
{
214 if (defined $ref and $ref ne 'HEAD') {
216 } elsif (is_initial_commit
()) {
217 return get_empty_tree
();
223 # Returns list of hashes, contents of each of which are:
225 # BINARY: is a binary path
226 # INDEX: is index different from HEAD?
227 # FILE: is file different from index?
228 # INDEX_ADDDEL: is it add/delete between HEAD and index?
229 # FILE_ADDDEL: is it add/delete between index and file?
230 # UNMERGED: is the path unmerged
235 my ($add, $del, $adddel, $file);
237 my $reference = get_diff_reference
($patch_mode_revision);
238 for (run_cmd_pipe
(qw(git diff-index --cached
239 --numstat --summary), $reference,
241 if (($add, $del, $file) =
242 /^([-\d]+) ([-\d]+) (.*)/) {
244 $file = unquote_path
($file);
245 if ($add eq '-' && $del eq '-') {
246 $change = __
('binary');
250 $change = "+$add/-$del";
255 FILE
=> __
('nothing'),
258 elsif (($adddel, $file) =
259 /^ (create|delete) mode [0-7]+ (.*)$/) {
260 $file = unquote_path
($file);
261 $data{$file}{INDEX_ADDDEL
} = $adddel;
265 for (run_cmd_pipe
(qw(git diff-files --ignore-submodules=dirty --numstat --summary --raw --), @ARGV)) {
266 if (($add, $del, $file) =
267 /^([-\d]+) ([-\d]+) (.*)/) {
268 $file = unquote_path
($file);
270 if ($add eq '-' && $del eq '-') {
271 $change = __
('binary');
275 $change = "+$add/-$del";
277 $data{$file}{FILE
} = $change;
279 $data{$file}{BINARY
} = 1;
282 elsif (($adddel, $file) =
283 /^ (create|delete) mode [0-7]+ (.*)$/) {
284 $file = unquote_path
($file);
285 $data{$file}{FILE_ADDDEL
} = $adddel;
287 elsif (/^:[0-7]+ [0-7]+ [0-9a-f]+ [0-9a-f]+ (.) (.*)$/) {
288 $file = unquote_path
($2);
289 if (!exists $data{$file}) {
291 INDEX
=> __
('unchanged'),
296 $data{$file}{UNMERGED
} = 1;
301 for (sort keys %data) {
305 if ($only eq 'index-only') {
306 next if ($it->{INDEX
} eq __
('unchanged'));
308 if ($only eq 'file-only') {
309 next if ($it->{FILE
} eq __
('nothing'));
321 my ($string, @stuff) = @_;
323 for (my $i = 0; $i < @stuff; $i++) {
327 if ((ref $it) eq 'ARRAY') {
335 if ($it =~ /^$string/) {
339 if (defined $hit && defined $found) {
349 # inserts string into trie and updates count for each character
351 my ($trie, $string) = @_;
352 foreach (split //, $string) {
353 $trie = $trie->{$_} ||= {COUNT
=> 0};
358 # returns an array of tuples (prefix, remainder)
359 sub find_unique_prefixes
{
363 # any single prefix exceeding the soft limit is omitted
364 # if any prefix exceeds the hard limit all are omitted
365 # 0 indicates no limit
369 # build a trie modelling all possible options
371 foreach my $print (@stuff) {
372 if ((ref $print) eq 'ARRAY') {
373 $print = $print->[0];
375 elsif ((ref $print) eq 'HASH') {
376 $print = $print->{VALUE
};
378 update_trie
(\
%trie, $print);
379 push @return, $print;
382 # use the trie to find the unique prefixes
383 for (my $i = 0; $i < @return; $i++) {
384 my $ret = $return[$i];
385 my @letters = split //, $ret;
387 my ($prefix, $remainder);
389 for ($j = 0; $j < @letters; $j++) {
390 my $letter = $letters[$j];
391 if ($search{$letter}{COUNT
} == 1) {
392 $prefix = substr $ret, 0, $j + 1;
393 $remainder = substr $ret, $j + 1;
397 my $prefix = substr $ret, 0, $j;
399 if ($hard_limit && $j + 1 > $hard_limit);
401 %search = %{$search{$letter}};
403 if (ord($letters[0]) > 127 ||
404 ($soft_limit && $j + 1 > $soft_limit)) {
408 $return[$i] = [$prefix, $remainder];
413 # filters out prefixes which have special meaning to list_and_choose()
414 sub is_valid_prefix
{
416 return (defined $prefix) &&
417 !($prefix =~ /[\s,]/) && # separators
418 !($prefix =~ /^-/) && # deselection
419 !($prefix =~ /^\d+/) && # selection
420 ($prefix ne '*') && # "all" wildcard
421 ($prefix ne '?'); # prompt help
424 # given a prefix/remainder tuple return a string with the prefix highlighted
425 # for now use square brackets; later might use ANSI colors (underline, bold)
426 sub highlight_prefix
{
428 my $remainder = shift;
430 if (!defined $prefix) {
434 if (!is_valid_prefix
($prefix)) {
435 return "$prefix$remainder";
438 if (!$menu_use_color) {
439 return "[$prefix]$remainder";
442 return "$prompt_color$prefix$normal_color$remainder";
446 print STDERR colored
$error_color, @_;
449 sub list_and_choose
{
450 my ($opts, @stuff) = @_;
451 my (@chosen, @return);
456 my @prefixes = find_unique_prefixes
(@stuff) unless $opts->{LIST_ONLY
};
462 if ($opts->{HEADER
}) {
463 if (!$opts->{LIST_FLAT
}) {
466 print colored
$header_color, "$opts->{HEADER}\n";
468 for ($i = 0; $i < @stuff; $i++) {
469 my $chosen = $chosen[$i] ?
'*' : ' ';
470 my $print = $stuff[$i];
471 my $ref = ref $print;
472 my $highlighted = highlight_prefix
(@
{$prefixes[$i]})
474 if ($ref eq 'ARRAY') {
475 $print = $highlighted || $print->[0];
477 elsif ($ref eq 'HASH') {
478 my $value = $highlighted || $print->{VALUE
};
479 $print = sprintf($status_fmt,
485 $print = $highlighted || $print;
487 printf("%s%2d: %s", $chosen, $i+1, $print);
488 if (($opts->{LIST_FLAT
}) &&
489 (($i + 1) % ($opts->{LIST_FLAT
}))) {
502 return if ($opts->{LIST_ONLY
});
504 print colored
$prompt_color, $opts->{PROMPT
};
505 if ($opts->{SINGLETON
}) {
514 $opts->{ON_EOF
}->() if $opts->{ON_EOF
};
521 singleton_prompt_help_cmd
() :
525 for my $choice (split(/[\s,]+/, $line)) {
529 # Input that begins with '-'; unchoose
530 if ($choice =~ s/^-//) {
533 # A range can be specified like 5-7 or 5-.
534 if ($choice =~ /^(\d+)-(\d*)$/) {
535 ($bottom, $top) = ($1, length($2) ?
$2 : 1 + @stuff);
537 elsif ($choice =~ /^\d+$/) {
538 $bottom = $top = $choice;
540 elsif ($choice eq '*') {
545 $bottom = $top = find_unique
($choice, @stuff);
546 if (!defined $bottom) {
547 error_msg
sprintf(__
("Huh (%s)?\n"), $choice);
551 if ($opts->{SINGLETON
} && $bottom != $top) {
552 error_msg
sprintf(__
("Huh (%s)?\n"), $choice);
555 for ($i = $bottom-1; $i <= $top-1; $i++) {
556 next if (@stuff <= $i || $i < 0);
557 $chosen[$i] = $choose;
560 last if ($opts->{IMMEDIATE
} || $line eq '*');
562 for ($i = 0; $i < @stuff; $i++) {
564 push @return, $stuff[$i];
570 sub singleton_prompt_help_cmd
{
571 print colored
$help_color, __
<<'EOF' ;
573 1 - select a numbered item
574 foo - select item based on unique prefix
575 - (empty) select nothing
579 sub prompt_help_cmd
{
580 print colored
$help_color, __
<<'EOF' ;
582 1 - select a single item
583 3-5 - select a range of items
584 2-3,6-9 - select multiple ranges
585 foo - select item based on unique prefix
586 -... - unselect specified items
588 - (empty) finish selecting
593 list_and_choose
({ LIST_ONLY
=> 1, HEADER
=> $status_head },
601 if ($did eq 'added') {
602 printf(__n
("added %d path\n", "added %d paths\n",
604 } elsif ($did eq 'updated') {
605 printf(__n
("updated %d path\n", "updated %d paths\n",
607 } elsif ($did eq 'reverted') {
608 printf(__n
("reverted %d path\n", "reverted %d paths\n",
611 printf(__n
("touched %d path\n", "touched %d paths\n",
617 my @mods = list_modified
('file-only');
620 my @update = list_and_choose
({ PROMPT
=> __
('Update'),
621 HEADER
=> $status_head, },
624 system(qw(git update-index --add --remove --),
625 map { $_->{VALUE
} } @update);
626 say_n_paths
('updated', @update);
632 my @update = list_and_choose
({ PROMPT
=> __
('Revert'),
633 HEADER
=> $status_head, },
636 if (is_initial_commit
()) {
637 system(qw(git rm --cached),
638 map { $_->{VALUE
} } @update);
641 my @lines = run_cmd_pipe
(qw(git ls-tree HEAD --),
642 map { $_->{VALUE
} } @update);
644 open $fh, '| git update-index --index-info'
651 if ($_->{INDEX_ADDDEL
} &&
652 $_->{INDEX_ADDDEL
} eq 'create') {
653 system(qw(git update-index --force-remove --),
655 printf(__
("note: %s is untracked now.\n"), $_->{VALUE
});
660 say_n_paths
('reverted', @update);
665 sub add_untracked_cmd
{
666 my @add = list_and_choose
({ PROMPT
=> __
('Add untracked') },
669 system(qw(git update-index --add --), @add);
670 say_n_paths
('added', @add);
672 print __
("No untracked files.\n");
680 open $fh, '| git ' . $cmd . " --allow-overlap";
687 my @diff_cmd = split(" ", $patch_mode_flavour{DIFF
});
688 if (defined $diff_algorithm) {
689 splice @diff_cmd, 1, 0, "--diff-algorithm=${diff_algorithm}";
691 if (defined $patch_mode_revision) {
692 push @diff_cmd, get_diff_reference
($patch_mode_revision);
694 my @diff = run_cmd_pipe
("git", @diff_cmd, "--", $path);
696 if ($diff_use_color) {
697 my @display_cmd = ("git", @diff_cmd, qw(--color --), $path);
698 if (defined $diff_filter) {
699 # quotemeta is overkill, but sufficient for shell-quoting
700 my $diff = join(' ', map { quotemeta } @display_cmd);
701 @display_cmd = ("$diff | $diff_filter");
704 @colored = run_cmd_pipe
(@display_cmd);
706 my (@hunk) = { TEXT
=> [], DISPLAY
=> [], TYPE
=> 'header' };
708 for (my $i = 0; $i < @diff; $i++) {
709 if ($diff[$i] =~ /^@@ /) {
710 push @hunk, { TEXT
=> [], DISPLAY
=> [],
713 push @
{$hunk[-1]{TEXT
}}, $diff[$i];
714 push @
{$hunk[-1]{DISPLAY
}},
715 (@colored ?
$colored[$i] : $diff[$i]);
720 sub parse_diff_header
{
723 my $head = { TEXT
=> [], DISPLAY
=> [], TYPE
=> 'header' };
724 my $mode = { TEXT
=> [], DISPLAY
=> [], TYPE
=> 'mode' };
725 my $deletion = { TEXT
=> [], DISPLAY
=> [], TYPE
=> 'deletion' };
727 for (my $i = 0; $i < @
{$src->{TEXT
}}; $i++) {
729 $src->{TEXT
}->[$i] =~ /^(old|new) mode (\d+)$/ ?
$mode :
730 $src->{TEXT
}->[$i] =~ /^deleted file/ ?
$deletion :
732 push @
{$dest->{TEXT
}}, $src->{TEXT
}->[$i];
733 push @
{$dest->{DISPLAY
}}, $src->{DISPLAY
}->[$i];
735 return ($head, $mode, $deletion);
738 sub hunk_splittable
{
741 my @s = split_hunk
($text);
745 sub parse_hunk_header
{
747 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
748 $line =~ /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
749 $o_cnt = 1 unless defined $o_cnt;
750 $n_cnt = 1 unless defined $n_cnt;
751 return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
754 sub format_hunk_header
{
755 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = @_;
756 return ("@@ -$o_ofs" .
757 (($o_cnt != 1) ?
",$o_cnt" : '') .
759 (($n_cnt != 1) ?
",$n_cnt" : '') .
764 my ($text, $display) = @_;
766 if (!defined $display) {
769 # If there are context lines in the middle of a hunk,
770 # it can be split, but we would need to take care of
773 my ($o_ofs, undef, $n_ofs) = parse_hunk_header
($text->[0]);
778 my $next_hunk_start = undef;
779 my $i = $hunk_start - 1;
793 while (++$i < @
$text) {
794 my $line = $text->[$i];
795 my $display = $display->[$i];
796 if ($line =~ /^\\/) {
797 push @
{$this->{TEXT
}}, $line;
798 push @
{$this->{DISPLAY
}}, $display;
802 if ($this->{ADDDEL
} &&
803 !defined $next_hunk_start) {
804 # We have seen leading context and
805 # adds/dels and then here is another
806 # context, which is trailing for this
807 # split hunk and leading for the next
809 $next_hunk_start = $i;
811 push @
{$this->{TEXT
}}, $line;
812 push @
{$this->{DISPLAY
}}, $display;
815 if (defined $next_hunk_start) {
822 if (defined $next_hunk_start) {
823 # We are done with the current hunk and
824 # this is the first real change for the
826 $hunk_start = $next_hunk_start;
827 $o_ofs = $this->{OLD
} + $this->{OCNT
};
828 $n_ofs = $this->{NEW
} + $this->{NCNT
};
829 $o_ofs -= $this->{POSTCTX
};
830 $n_ofs -= $this->{POSTCTX
};
834 push @
{$this->{TEXT
}}, $line;
835 push @
{$this->{DISPLAY
}}, $display;
849 for my $hunk (@split) {
850 $o_ofs = $hunk->{OLD
};
851 $n_ofs = $hunk->{NEW
};
852 my $o_cnt = $hunk->{OCNT
};
853 my $n_cnt = $hunk->{NCNT
};
855 my $head = format_hunk_header
($o_ofs, $o_cnt, $n_ofs, $n_cnt);
856 my $display_head = $head;
857 unshift @
{$hunk->{TEXT
}}, $head;
858 if ($diff_use_color) {
859 $display_head = colored
($fraginfo_color, $head);
861 unshift @
{$hunk->{DISPLAY
}}, $display_head;
866 sub find_last_o_ctx
{
868 my $text = $it->{TEXT
};
869 my ($o_ofs, $o_cnt) = parse_hunk_header
($text->[0]);
871 my $last_o_ctx = $o_ofs + $o_cnt;
873 my $line = $text->[$i];
884 my ($prev, $this) = @_;
885 my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) =
886 parse_hunk_header
($prev->{TEXT
}[0]);
887 my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) =
888 parse_hunk_header
($this->{TEXT
}[0]);
890 my (@line, $i, $ofs, $o_cnt, $n_cnt);
893 for ($i = 1; $i < @
{$prev->{TEXT
}}; $i++) {
894 my $line = $prev->{TEXT
}[$i];
895 if ($line =~ /^\+/) {
899 } elsif ($line =~ /^\\/) {
904 last if ($o1_ofs <= $ofs);
914 for ($i = 1; $i < @
{$this->{TEXT
}}; $i++) {
915 my $line = $this->{TEXT
}[$i];
916 if ($line =~ /^\+/) {
920 } elsif ($line =~ /^\\/) {
931 my $head = format_hunk_header
($o0_ofs, $o_cnt, $n0_ofs, $n_cnt);
932 @
{$prev->{TEXT
}} = ($head, @line);
935 sub coalesce_overlapping_hunks
{
939 my ($last_o_ctx, $last_was_dirty);
943 if ($_->{TYPE
} ne 'hunk') {
947 my $text = $_->{TEXT
};
948 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
949 parse_hunk_header
($text->[0]);
951 $ofs_delta += $o_cnt - $n_cnt;
952 # If this hunk has been edited then subtract
953 # the delta that is due to the edit.
954 if ($_->{OFS_DELTA
}) {
955 $ofs_delta -= $_->{OFS_DELTA
};
960 $n_ofs += $ofs_delta;
961 $_->{TEXT
}->[0] = format_hunk_header
($o_ofs, $o_cnt,
964 # If this hunk was edited then adjust the offset delta
965 # to reflect the edit.
966 if ($_->{OFS_DELTA
}) {
967 $ofs_delta += $_->{OFS_DELTA
};
969 if (defined $last_o_ctx &&
970 $o_ofs <= $last_o_ctx &&
973 merge_hunk
($out[-1], $_);
978 $last_o_ctx = find_last_o_ctx
($out[-1]);
979 $last_was_dirty = $_->{DIRTY
};
984 sub reassemble_patch
{
988 # Include everything in the header except the beginning of the diff.
989 push @patch, (grep { !/^[-+]{3}/ } @
$head);
991 # Then include any headers from the hunk lines, which must
992 # come before any actual hunk.
993 while (@_ && $_[0] !~ /^@/) {
997 # Then begin the diff.
998 push @patch, grep { /^[-+]{3}/ } @
$head;
1000 # And then the actual hunks.
1008 colored
((/^@/ ?
$fraginfo_color :
1009 /^\+/ ?
$diff_new_color :
1010 /^-/ ?
$diff_old_color :
1016 my %edit_hunk_manually_modes = (
1018 "If the patch applies cleanly, the edited hunk will immediately be
1019 marked for staging."),
1021 "If the patch applies cleanly, the edited hunk will immediately be
1022 marked for stashing."),
1024 "If the patch applies cleanly, the edited hunk will immediately be
1025 marked for unstaging."),
1026 reset_nothead
=> N__
(
1027 "If the patch applies cleanly, the edited hunk will immediately be
1028 marked for applying."),
1029 checkout_index
=> N__
(
1030 "If the patch applies cleanly, the edited hunk will immediately be
1031 marked for discarding."),
1032 checkout_head
=> N__
(
1033 "If the patch applies cleanly, the edited hunk will immediately be
1034 marked for discarding."),
1035 checkout_nothead
=> N__
(
1036 "If the patch applies cleanly, the edited hunk will immediately be
1037 marked for applying."),
1040 sub recount_edited_hunk
{
1042 my ($oldtext, $newtext) = @_;
1043 my ($o_cnt, $n_cnt) = (0, 0);
1044 for (@
{$newtext}[1..$#{$newtext}]) {
1045 my $mode = substr($_, 0, 1);
1048 } elsif ($mode eq '+') {
1050 } elsif ($mode eq ' ') {
1055 my ($o_ofs, undef, $n_ofs, undef) =
1056 parse_hunk_header
($newtext->[0]);
1057 $newtext->[0] = format_hunk_header
($o_ofs, $o_cnt, $n_ofs, $n_cnt);
1058 my (undef, $orig_o_cnt, undef, $orig_n_cnt) =
1059 parse_hunk_header
($oldtext->[0]);
1060 # Return the change in the number of lines inserted by this hunk
1061 return $orig_o_cnt - $orig_n_cnt - $o_cnt + $n_cnt;
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 { !/^\Q$comment_line_char\E/ } <$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, $hunks, $ix) = @_;
1163 my $hunk = $hunks->[$ix];
1164 my $text = $hunk->{TEXT
};
1167 my $newtext = edit_hunk_manually
($text);
1168 if (!defined $newtext) {
1173 TYPE
=> $hunk->{TYPE
},
1177 $newhunk->{OFS_DELTA
} = recount_edited_hunk
($text, $newtext);
1178 # If this hunk has already been edited then add the
1179 # offset delta of the previous edit to get the real
1180 # delta from the original unedited hunk.
1181 $hunk->{OFS_DELTA
} and
1182 $newhunk->{OFS_DELTA
} += $hunk->{OFS_DELTA
};
1183 if (diff_applies
($head,
1184 @
{$hunks}[0..$ix-1],
1186 @
{$hunks}[$ix+1..$#{$hunks}])) {
1187 $newhunk->{DISPLAY
} = [color_diff
(@
{$newtext})];
1192 # TRANSLATORS: do not translate [y/n]
1193 # The program will only accept that input
1195 # Consider translating (saying "no" discards!) as
1196 # (saying "n" for "no" discards!) if the translation
1197 # of the word "no" does not start with n.
1198 __
('Your edited hunk does not apply. Edit again '
1199 . '(saying "no" discards!) [y/n]? ')
1205 my %help_patch_modes = (
1207 "y - stage this hunk
1208 n - do not stage this hunk
1209 q - quit; do not stage this hunk or any of the remaining ones
1210 a - stage this hunk and all later hunks in the file
1211 d - do not stage this hunk or any of the later hunks in the file"),
1213 "y - stash this hunk
1214 n - do not stash this hunk
1215 q - quit; do not stash this hunk or any of the remaining ones
1216 a - stash this hunk and all later hunks in the file
1217 d - do not stash this hunk or any of the later hunks in the file"),
1219 "y - unstage this hunk
1220 n - do not unstage this hunk
1221 q - quit; do not unstage this hunk or any of the remaining ones
1222 a - unstage this hunk and all later hunks in the file
1223 d - do not unstage this hunk or any of the later hunks in the file"),
1224 reset_nothead
=> N__
(
1225 "y - apply this hunk to index
1226 n - do not apply this hunk to index
1227 q - quit; do not apply this hunk or any of the remaining ones
1228 a - apply this hunk and all later hunks in the file
1229 d - do not apply this hunk or any of the later hunks in the file"),
1230 checkout_index
=> N__
(
1231 "y - discard this hunk from worktree
1232 n - do not discard this hunk from worktree
1233 q - quit; do not discard this hunk or any of the remaining ones
1234 a - discard this hunk and all later hunks in the file
1235 d - do not discard this hunk or any of the later hunks in the file"),
1236 checkout_head
=> N__
(
1237 "y - discard this hunk from index and worktree
1238 n - do not discard this hunk from index and worktree
1239 q - quit; do not discard this hunk or any of the remaining ones
1240 a - discard this hunk and all later hunks in the file
1241 d - do not discard this hunk or any of the later hunks in the file"),
1242 checkout_nothead
=> N__
(
1243 "y - apply this hunk to index and worktree
1244 n - do not apply this hunk to index and worktree
1245 q - quit; do not apply this hunk or any of the remaining ones
1246 a - apply this hunk and all later hunks in the file
1247 d - do not apply this hunk or any of the later hunks in the file"),
1250 sub help_patch_cmd
{
1252 my $other = $_[0] . ",?";
1253 print colored
$help_color, __
($help_patch_modes{$patch_mode}), "\n",
1254 map { "$_\n" } grep {
1255 my $c = quotemeta(substr($_, 0, 1));
1257 } split "\n", __
<<EOF ;
1258 g - select a hunk to go to
1259 / - search for a hunk matching the given regex
1260 j - leave this hunk undecided, see next undecided hunk
1261 J - leave this hunk undecided, see next hunk
1262 k - leave this hunk undecided, see previous undecided hunk
1263 K - leave this hunk undecided, see previous hunk
1264 s - split the current hunk into smaller hunks
1265 e - manually edit the current hunk
1272 my $ret = run_git_apply
$cmd, @_;
1279 sub apply_patch_for_checkout_commit
{
1280 my $reverse = shift;
1281 my $applies_index = run_git_apply
'apply '.$reverse.' --cached --check', @_;
1282 my $applies_worktree = run_git_apply
'apply '.$reverse.' --check', @_;
1284 if ($applies_worktree && $applies_index) {
1285 run_git_apply
'apply '.$reverse.' --cached', @_;
1286 run_git_apply
'apply '.$reverse, @_;
1288 } elsif (!$applies_index) {
1289 print colored
$error_color, __
("The selected hunks do not apply to the index!\n");
1290 if (prompt_yesno __
("Apply them to the worktree anyway? ")) {
1291 return run_git_apply
'apply '.$reverse, @_;
1293 print colored
$error_color, __
("Nothing was applied.\n");
1302 sub patch_update_cmd
{
1303 my @all_mods = list_modified
($patch_mode_flavour{FILTER
});
1304 error_msg
sprintf(__
("ignoring unmerged: %s\n"), $_->{VALUE
})
1305 for grep { $_->{UNMERGED
} } @all_mods;
1306 @all_mods = grep { !$_->{UNMERGED
} } @all_mods;
1308 my @mods = grep { !($_->{BINARY
}) } @all_mods;
1313 print STDERR __
("Only binary files changed.\n");
1315 print STDERR __
("No changes.\n");
1319 if ($patch_mode_only) {
1323 @them = list_and_choose
({ PROMPT
=> __
('Patch update'),
1324 HEADER
=> $status_head, },
1328 return 0 if patch_update_file
($_->{VALUE
});
1332 # Generate a one line summary of a hunk.
1333 sub summarize_hunk
{
1335 my $summary = $rhunk->{TEXT
}[0];
1337 # Keep the line numbers, discard extra context.
1338 $summary =~ s/@@(.*?)@@.*/$1 /s;
1339 $summary .= " " x
(20 - length $summary);
1341 # Add some user context.
1342 for my $line (@
{$rhunk->{TEXT
}}) {
1343 if ($line =~ m/^[+-].*\w/) {
1350 return substr($summary, 0, 80) . "\n";
1354 # Print a one-line summary of each hunk in the array ref in
1355 # the first argument, starting with the index in the 2nd.
1357 my ($hunks, $i) = @_;
1360 for (; $i < @
$hunks && $ctr < 20; $i++, $ctr++) {
1362 if (defined $hunks->[$i]{USE
}) {
1363 $status = $hunks->[$i]{USE
} ?
"+" : "-";
1368 summarize_hunk
($hunks->[$i]);
1373 my %patch_update_prompt_modes = (
1375 mode
=> N__
("Stage mode change [y,n,q,a,d%s,?]? "),
1376 deletion
=> N__
("Stage deletion [y,n,q,a,d%s,?]? "),
1377 hunk
=> N__
("Stage this hunk [y,n,q,a,d%s,?]? "),
1380 mode
=> N__
("Stash mode change [y,n,q,a,d%s,?]? "),
1381 deletion
=> N__
("Stash deletion [y,n,q,a,d%s,?]? "),
1382 hunk
=> N__
("Stash this hunk [y,n,q,a,d%s,?]? "),
1385 mode
=> N__
("Unstage mode change [y,n,q,a,d%s,?]? "),
1386 deletion
=> N__
("Unstage deletion [y,n,q,a,d%s,?]? "),
1387 hunk
=> N__
("Unstage this hunk [y,n,q,a,d%s,?]? "),
1390 mode
=> N__
("Apply mode change to index [y,n,q,a,d%s,?]? "),
1391 deletion
=> N__
("Apply deletion to index [y,n,q,a,d%s,?]? "),
1392 hunk
=> N__
("Apply this hunk to index [y,n,q,a,d%s,?]? "),
1395 mode
=> N__
("Discard mode change from worktree [y,n,q,a,d%s,?]? "),
1396 deletion
=> N__
("Discard deletion from worktree [y,n,q,a,d%s,?]? "),
1397 hunk
=> N__
("Discard this hunk from worktree [y,n,q,a,d%s,?]? "),
1400 mode
=> N__
("Discard mode change from index and worktree [y,n,q,a,d%s,?]? "),
1401 deletion
=> N__
("Discard deletion from index and worktree [y,n,q,a,d%s,?]? "),
1402 hunk
=> N__
("Discard this hunk from index and worktree [y,n,q,a,d%s,?]? "),
1404 checkout_nothead
=> {
1405 mode
=> N__
("Apply mode change to index and worktree [y,n,q,a,d%s,?]? "),
1406 deletion
=> N__
("Apply deletion to index and worktree [y,n,q,a,d%s,?]? "),
1407 hunk
=> N__
("Apply this hunk to index and worktree [y,n,q,a,d%s,?]? "),
1411 sub patch_update_file
{
1415 my ($head, @hunk) = parse_diff
($path);
1416 ($head, my $mode, my $deletion) = parse_diff_header
($head);
1417 for (@
{$head->{DISPLAY
}}) {
1421 if (@
{$mode->{TEXT
}}) {
1422 unshift @hunk, $mode;
1424 if (@
{$deletion->{TEXT
}}) {
1425 foreach my $hunk (@hunk) {
1426 push @
{$deletion->{TEXT
}}, @
{$hunk->{TEXT
}};
1427 push @
{$deletion->{DISPLAY
}}, @
{$hunk->{DISPLAY
}};
1429 @hunk = ($deletion);
1432 $num = scalar @hunk;
1436 my ($prev, $next, $other, $undecided, $i);
1442 for ($i = 0; $i < $ix; $i++) {
1443 if (!defined $hunk[$i]{USE
}) {
1452 for ($i = $ix + 1; $i < $num; $i++) {
1453 if (!defined $hunk[$i]{USE
}) {
1459 if ($ix < $num - 1) {
1465 for ($i = 0; $i < $num; $i++) {
1466 if (!defined $hunk[$i]{USE
}) {
1471 last if (!$undecided);
1473 if ($hunk[$ix]{TYPE
} eq 'hunk' &&
1474 hunk_splittable
($hunk[$ix]{TEXT
})) {
1477 if ($hunk[$ix]{TYPE
} eq 'hunk') {
1480 for (@
{$hunk[$ix]{DISPLAY
}}) {
1483 print colored
$prompt_color,
1484 sprintf(__
($patch_update_prompt_modes{$patch_mode}{$hunk[$ix]{TYPE
}}), $other);
1486 my $line = prompt_single_character
;
1487 last unless defined $line;
1489 if ($line =~ /^y/i) {
1490 $hunk[$ix]{USE
} = 1;
1492 elsif ($line =~ /^n/i) {
1493 $hunk[$ix]{USE
} = 0;
1495 elsif ($line =~ /^a/i) {
1496 while ($ix < $num) {
1497 if (!defined $hunk[$ix]{USE
}) {
1498 $hunk[$ix]{USE
} = 1;
1504 elsif ($line =~ /^g(.*)/) {
1506 unless ($other =~ /g/) {
1507 error_msg __
("No other hunks to goto\n");
1510 my $no = $ix > 10 ?
$ix - 10 : 0;
1511 while ($response eq '') {
1512 $no = display_hunks
(\
@hunk, $no);
1514 print __
("go to which hunk (<ret> to see more)? ");
1516 print __
("go to which hunk? ");
1518 $response = <STDIN
>;
1519 if (!defined $response) {
1524 if ($response !~ /^\s*\d+\s*$/) {
1525 error_msg
sprintf(__
("Invalid number: '%s'\n"),
1527 } elsif (0 < $response && $response <= $num) {
1528 $ix = $response - 1;
1530 error_msg
sprintf(__n
("Sorry, only %d hunk available.\n",
1531 "Sorry, only %d hunks available.\n", $num), $num);
1535 elsif ($line =~ /^d/i) {
1536 while ($ix < $num) {
1537 if (!defined $hunk[$ix]{USE
}) {
1538 $hunk[$ix]{USE
} = 0;
1544 elsif ($line =~ /^q/i) {
1545 for ($i = 0; $i < $num; $i++) {
1546 if (!defined $hunk[$i]{USE
}) {
1553 elsif ($line =~ m
|^/(.*)|) {
1555 unless ($other =~ m
|/|) {
1556 error_msg __
("No other hunks to search\n");
1560 print colored
$prompt_color, __
("search for regex? ");
1562 if (defined $regex) {
1568 $search_string = qr{$regex}m;
1571 my ($err,$exp) = ($@
, $1);
1572 $err =~ s/ at .*git-add--interactive line \d+, <STDIN> line \d+.*$//;
1573 error_msg
sprintf(__
("Malformed search regexp %s: %s\n"), $exp, $err);
1578 my $text = join ("", @
{$hunk[$iy]{TEXT
}});
1579 last if ($text =~ $search_string);
1581 $iy = 0 if ($iy >= $num);
1583 error_msg __
("No hunk matches the given pattern\n");
1590 elsif ($line =~ /^K/) {
1591 if ($other =~ /K/) {
1595 error_msg __
("No previous hunk\n");
1599 elsif ($line =~ /^J/) {
1600 if ($other =~ /J/) {
1604 error_msg __
("No next hunk\n");
1608 elsif ($line =~ /^k/) {
1609 if ($other =~ /k/) {
1613 !defined $hunk[$ix]{USE
});
1617 error_msg __
("No previous hunk\n");
1621 elsif ($line =~ /^j/) {
1622 if ($other !~ /j/) {
1623 error_msg __
("No next hunk\n");
1627 elsif ($line =~ /^s/) {
1628 unless ($other =~ /s/) {
1629 error_msg __
("Sorry, cannot split this hunk\n");
1632 my @split = split_hunk
($hunk[$ix]{TEXT
}, $hunk[$ix]{DISPLAY
});
1634 print colored
$header_color, sprintf(
1635 __n
("Split into %d hunk.\n",
1636 "Split into %d hunks.\n",
1637 scalar(@split)), scalar(@split));
1639 splice (@hunk, $ix, 1, @split);
1640 $num = scalar @hunk;
1643 elsif ($line =~ /^e/) {
1644 unless ($other =~ /e/) {
1645 error_msg __
("Sorry, cannot edit this hunk\n");
1648 my $newhunk = edit_hunk_loop
($head, \
@hunk, $ix);
1649 if (defined $newhunk) {
1650 splice @hunk, $ix, 1, $newhunk;
1654 help_patch_cmd
($other);
1660 last if ($ix >= $num ||
1661 !defined $hunk[$ix]{USE
});
1666 @hunk = coalesce_overlapping_hunks
(@hunk);
1672 push @result, @
{$_->{TEXT
}};
1677 my @patch = reassemble_patch
($head->{TEXT
}, @result);
1678 my $apply_routine = $patch_mode_flavour{APPLY
};
1679 &$apply_routine(@patch);
1688 my @mods = list_modified
('index-only');
1689 @mods = grep { !($_->{BINARY
}) } @mods;
1691 my (@them) = list_and_choose
({ PROMPT
=> __
('Review diff'),
1693 HEADER
=> $status_head, },
1696 my $reference = (is_initial_commit
()) ? get_empty_tree
() : 'HEAD';
1697 system(qw(git diff -p --cached), $reference, '--',
1698 map { $_->{VALUE
} } @them);
1707 # TRANSLATORS: please do not translate the command names
1708 # 'status', 'update', 'revert', etc.
1709 print colored
$help_color, __
<<'EOF' ;
1710 status - show paths with changes
1711 update - add working tree state to the staged set of changes
1712 revert - revert staged set of changes back to the HEAD version
1713 patch - pick hunks and update selectively
1714 diff - view diff between HEAD and index
1715 add untracked - add contents of untracked files to the staged set of changes
1720 return unless @ARGV;
1721 my $arg = shift @ARGV;
1722 if ($arg =~ /--patch(?:=(.*))?/) {
1724 if ($1 eq 'reset') {
1725 $patch_mode = 'reset_head';
1726 $patch_mode_revision = 'HEAD';
1727 $arg = shift @ARGV or die __
("missing --");
1729 $patch_mode_revision = $arg;
1730 $patch_mode = ($arg eq 'HEAD' ?
1731 'reset_head' : 'reset_nothead');
1732 $arg = shift @ARGV or die __
("missing --");
1734 } elsif ($1 eq 'checkout') {
1735 $arg = shift @ARGV or die __
("missing --");
1737 $patch_mode = 'checkout_index';
1739 $patch_mode_revision = $arg;
1740 $patch_mode = ($arg eq 'HEAD' ?
1741 'checkout_head' : 'checkout_nothead');
1742 $arg = shift @ARGV or die __
("missing --");
1744 } elsif ($1 eq 'stage' or $1 eq 'stash') {
1746 $arg = shift @ARGV or die __
("missing --");
1748 die sprintf(__
("unknown --patch mode: %s"), $1);
1751 $patch_mode = 'stage';
1752 $arg = shift @ARGV or die __
("missing --");
1754 die sprintf(__
("invalid argument %s, expecting --"),
1755 $arg) unless $arg eq "--";
1756 %patch_mode_flavour = %{$patch_modes{$patch_mode}};
1757 $patch_mode_only = 1;
1759 elsif ($arg ne "--") {
1760 die sprintf(__
("invalid argument %s, expecting --"), $arg);
1765 my @cmd = ([ 'status', \
&status_cmd
, ],
1766 [ 'update', \
&update_cmd
, ],
1767 [ 'revert', \
&revert_cmd
, ],
1768 [ 'add untracked', \
&add_untracked_cmd
, ],
1769 [ 'patch', \
&patch_update_cmd
, ],
1770 [ 'diff', \
&diff_cmd
, ],
1771 [ 'quit', \
&quit_cmd
, ],
1772 [ 'help', \
&help_cmd
, ],
1775 my ($it) = list_and_choose
({ PROMPT
=> __
('What now'),
1778 HEADER
=> __
('*** Commands ***'),
1779 ON_EOF
=> \
&quit_cmd
,
1780 IMMEDIATE
=> 1 }, @cmd);
1794 if ($patch_mode_only) {