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 --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 . " --recount --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];
797 if ($this->{ADDDEL} &&
798 !defined $next_hunk_start) {
799 # We have seen leading context and
800 # adds/dels and then here is another
801 # context, which is trailing for this
802 # split hunk and leading for the next
804 $next_hunk_start = $i;
806 push @{$this->{TEXT}}, $line;
807 push @{$this->{DISPLAY}}, $display;
810 if (defined $next_hunk_start) {
817 if (defined $next_hunk_start) {
818 # We are done with the current hunk and
819 # this is the first real change for the
821 $hunk_start = $next_hunk_start;
822 $o_ofs = $this->{OLD} + $this->{OCNT};
823 $n_ofs = $this->{NEW} + $this->{NCNT};
824 $o_ofs -= $this->{POSTCTX};
825 $n_ofs -= $this->{POSTCTX};
829 push @{$this->{TEXT}}, $line;
830 push @{$this->{DISPLAY}}, $display;
844 for my $hunk (@split) {
845 $o_ofs = $hunk->{OLD};
846 $n_ofs = $hunk->{NEW};
847 my $o_cnt = $hunk->{OCNT};
848 my $n_cnt = $hunk->{NCNT};
850 my $head = format_hunk_header($o_ofs, $o_cnt, $n_ofs, $n_cnt);
851 my $display_head = $head;
852 unshift @{$hunk->{TEXT}}, $head;
853 if ($diff_use_color) {
854 $display_head = colored($fraginfo_color, $head);
856 unshift @{$hunk->{DISPLAY}}, $display_head;
861 sub find_last_o_ctx {
863 my $text = $it->{TEXT};
864 my ($o_ofs, $o_cnt) = parse_hunk_header($text->[0]);
866 my $last_o_ctx = $o_ofs + $o_cnt;
868 my $line = $text->[$i];
879 my ($prev, $this) = @_;
880 my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) =
881 parse_hunk_header($prev->{TEXT}[0]);
882 my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) =
883 parse_hunk_header($this->{TEXT}[0]);
885 my (@line, $i, $ofs, $o_cnt, $n_cnt);
888 for ($i = 1; $i < @{$prev->{TEXT}}; $i++) {
889 my $line = $prev->{TEXT}[$i];
890 if ($line =~ /^\+/) {
896 last if ($o1_ofs <= $ofs);
906 for ($i = 1; $i < @{$this->{TEXT}}; $i++) {
907 my $line = $this->{TEXT}[$i];
908 if ($line =~ /^\+/) {
920 my $head = format_hunk_header($o0_ofs, $o_cnt, $n0_ofs, $n_cnt);
921 @{$prev->{TEXT}} = ($head, @line);
924 sub coalesce_overlapping_hunks {
928 my ($last_o_ctx, $last_was_dirty);
932 if ($_->{TYPE} ne 'hunk') {
936 my $text = $_->{TEXT};
937 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
938 parse_hunk_header($text->[0]);
940 $ofs_delta += $o_cnt - $n_cnt;
944 $n_ofs += $ofs_delta;
945 $_->{TEXT}->[0] = format_hunk_header($o_ofs, $o_cnt,
948 if (defined $last_o_ctx &&
949 $o_ofs <= $last_o_ctx &&
952 merge_hunk($out[-1], $_);
957 $last_o_ctx = find_last_o_ctx($out[-1]);
958 $last_was_dirty = $_->{DIRTY};
963 sub reassemble_patch {
967 # Include everything in the header except the beginning of the diff.
968 push @patch, (grep { !/^[-+]{3}/ } @$head);
970 # Then include any headers from the hunk lines, which must
971 # come before any actual hunk.
972 while (@_ && $_[0] !~ /^@/) {
976 # Then begin the diff.
977 push @patch, grep { /^[-+]{3}/ } @$head;
979 # And then the actual hunks.
987 colored((/^@/ ? $fraginfo_color :
988 /^\+/ ? $diff_new_color :
989 /^-/ ? $diff_old_color :
995 my %edit_hunk_manually_modes = (
997 "If the patch applies cleanly, the edited hunk will immediately be
998 marked for staging."),
1000 "If the patch applies cleanly, the edited hunk will immediately be
1001 marked for stashing."),
1003 "If the patch applies cleanly, the edited hunk will immediately be
1004 marked for unstaging."),
1005 reset_nothead => N__(
1006 "If the patch applies cleanly, the edited hunk will immediately be
1007 marked for applying."),
1008 checkout_index => N__(
1009 "If the patch applies cleanly, the edited hunk will immediately be
1010 marked for discarding."),
1011 checkout_head => N__(
1012 "If the patch applies cleanly, the edited hunk will immediately be
1013 marked for discarding."),
1014 checkout_nothead => N__(
1015 "If the patch applies cleanly, the edited hunk will immediately be
1016 marked for applying."),
1019 sub edit_hunk_manually {
1022 my $hunkfile = $repo->repo_path . "/addp-hunk-edit.diff";
1024 open $fh, '>', $hunkfile
1025 or die sprintf(__("failed to open hunk edit file for writing: %s"), $!);
1026 print $fh Git::comment_lines __("Manual hunk edit mode -- see bottom for a quick guide.\n");
1027 print $fh @$oldtext;
1028 my $is_reverse = $patch_mode_flavour{IS_REVERSE};
1029 my ($remove_plus, $remove_minus) = $is_reverse ? ('-', '+') : ('+', '-');
1030 my $comment_line_char = Git::get_comment_line_char;
1031 print $fh Git::comment_lines sprintf(__ <<EOF, $remove_minus, $remove_plus, $comment_line_char),
1033 To remove '%s' lines, make them ' ' lines (context).
1034 To remove '%s' lines, delete them.
1035 Lines starting with %s will be removed.
1037 __($edit_hunk_manually_modes{$patch_mode}),
1038 # TRANSLATORS: 'it' refers to the patch mentioned in the previous messages.
1040 If it does not apply cleanly, you will be given an opportunity to
1041 edit again. If all lines of the hunk are removed, then the edit is
1042 aborted and the hunk is left unchanged.
1046 chomp(my $editor = run_cmd_pipe(qw(git var GIT_EDITOR)));
1047 system('sh', '-c', $editor.' "$@"', $editor, $hunkfile);
1053 open $fh, '<', $hunkfile
1054 or die sprintf(__("failed to open hunk edit file for reading: %s"), $!);
1055 my @newtext = grep { !/^\Q$comment_line_char\E/ } <$fh>;
1059 # Abort if nothing remains
1060 if (!grep { /\S/ } @newtext) {
1064 # Reinsert the first hunk header if the user accidentally deleted it
1065 if ($newtext[0] !~ /^@/) {
1066 unshift @newtext, $oldtext->[0];
1072 return run_git_apply($patch_mode_flavour{APPLY_CHECK} . ' --check',
1073 map { @{$_->{TEXT}} } @_);
1076 sub _restore_terminal_and_die {
1082 sub prompt_single_character {
1084 local $SIG{TERM} = \&_restore_terminal_and_die;
1085 local $SIG{INT} = \&_restore_terminal_and_die;
1087 my $key = ReadKey 0;
1089 if ($use_termcap and $key eq "\e") {
1090 while (!defined $term_escapes{$key}) {
1091 my $next = ReadKey 0.5;
1092 last if (!defined $next);
1097 print "$key" if defined $key;
1108 print colored $prompt_color, $prompt;
1109 my $line = prompt_single_character;
1110 return undef unless defined $line;
1111 return 0 if $line =~ /^n/i;
1112 return 1 if $line =~ /^y/i;
1116 sub edit_hunk_loop {
1117 my ($head, $hunk, $ix) = @_;
1118 my $text = $hunk->[$ix]->{TEXT};
1121 $text = edit_hunk_manually($text);
1122 if (!defined $text) {
1127 TYPE => $hunk->[$ix]->{TYPE},
1131 if (diff_applies($head,
1134 @{$hunk}[$ix+1..$#{$hunk}])) {
1135 $newhunk->{DISPLAY} = [color_diff(@{$text})];
1140 # TRANSLATORS: do not translate [y/n]
1141 # The program will only accept that input
1143 # Consider translating (saying "no" discards!) as
1144 # (saying "n" for "no" discards!) if the translation
1145 # of the word "no" does not start with n.
1146 __('Your edited hunk does not apply. Edit again '
1147 . '(saying "no" discards!) [y/n]? ')
1153 my %help_patch_modes = (
1155 "y - stage this hunk
1156 n - do not stage this hunk
1157 q - quit; do not stage this hunk or any of the remaining ones
1158 a - stage this hunk and all later hunks in the file
1159 d - do not stage this hunk or any of the later hunks in the file"),
1161 "y - stash this hunk
1162 n - do not stash this hunk
1163 q - quit; do not stash this hunk or any of the remaining ones
1164 a - stash this hunk and all later hunks in the file
1165 d - do not stash this hunk or any of the later hunks in the file"),
1167 "y - unstage this hunk
1168 n - do not unstage this hunk
1169 q - quit; do not unstage this hunk or any of the remaining ones
1170 a - unstage this hunk and all later hunks in the file
1171 d - do not unstage this hunk or any of the later hunks in the file"),
1172 reset_nothead => N__(
1173 "y - apply this hunk to index
1174 n - do not apply this hunk to index
1175 q - quit; do not apply this hunk or any of the remaining ones
1176 a - apply this hunk and all later hunks in the file
1177 d - do not apply this hunk or any of the later hunks in the file"),
1178 checkout_index => N__(
1179 "y - discard this hunk from worktree
1180 n - do not discard this hunk from worktree
1181 q - quit; do not discard this hunk or any of the remaining ones
1182 a - discard this hunk and all later hunks in the file
1183 d - do not discard this hunk or any of the later hunks in the file"),
1184 checkout_head => N__(
1185 "y - discard this hunk from index and worktree
1186 n - do not discard this hunk from index and worktree
1187 q - quit; do not discard this hunk or any of the remaining ones
1188 a - discard this hunk and all later hunks in the file
1189 d - do not discard this hunk or any of the later hunks in the file"),
1190 checkout_nothead => N__(
1191 "y - apply this hunk to index and worktree
1192 n - do not apply this hunk to index and worktree
1193 q - quit; do not apply this hunk or any of the remaining ones
1194 a - apply this hunk and all later hunks in the file
1195 d - do not apply this hunk or any of the later hunks in the file"),
1198 sub help_patch_cmd {
1199 print colored $help_color, __($help_patch_modes{$patch_mode}), "\n", __ <<EOF ;
1200 g - select a hunk to go to
1201 / - search for a hunk matching the given regex
1202 j - leave this hunk undecided, see next undecided hunk
1203 J - leave this hunk undecided, see next hunk
1204 k - leave this hunk undecided, see previous undecided hunk
1205 K - leave this hunk undecided, see previous hunk
1206 s - split the current hunk into smaller hunks
1207 e - manually edit the current hunk
1214 my $ret = run_git_apply $cmd, @_;
1221 sub apply_patch_for_checkout_commit {
1222 my $reverse = shift;
1223 my $applies_index = run_git_apply 'apply '.$reverse.' --cached --check', @_;
1224 my $applies_worktree = run_git_apply 'apply '.$reverse.' --check', @_;
1226 if ($applies_worktree && $applies_index) {
1227 run_git_apply 'apply '.$reverse.' --cached', @_;
1228 run_git_apply 'apply '.$reverse, @_;
1230 } elsif (!$applies_index) {
1231 print colored $error_color, __("The selected hunks do not apply to the index!\n");
1232 if (prompt_yesno __("Apply them to the worktree anyway? ")) {
1233 return run_git_apply 'apply '.$reverse, @_;
1235 print colored $error_color, __("Nothing was applied.\n");
1244 sub patch_update_cmd {
1245 my @all_mods = list_modified($patch_mode_flavour{FILTER});
1246 error_msg sprintf(__("ignoring unmerged: %s\n"), $_->{VALUE})
1247 for grep { $_->{UNMERGED} } @all_mods;
1248 @all_mods = grep { !$_->{UNMERGED} } @all_mods;
1250 my @mods = grep { !($_->{BINARY}) } @all_mods;
1255 print STDERR __("Only binary files changed.\n");
1257 print STDERR __("No changes.\n");
1261 if ($patch_mode_only) {
1265 @them = list_and_choose({ PROMPT => __('Patch update'),
1266 HEADER => $status_head, },
1270 return 0 if patch_update_file($_->{VALUE});
1274 # Generate a one line summary of a hunk.
1275 sub summarize_hunk {
1277 my $summary = $rhunk->{TEXT}[0];
1279 # Keep the line numbers, discard extra context.
1280 $summary =~ s/@@(.*?)@@.*/$1 /s;
1281 $summary .= " " x (20 - length $summary);
1283 # Add some user context.
1284 for my $line (@{$rhunk->{TEXT}}) {
1285 if ($line =~ m/^[+-].*\w/) {
1292 return substr($summary, 0, 80) . "\n";
1296 # Print a one-line summary of each hunk in the array ref in
1297 # the first argument, starting with the index in the 2nd.
1299 my ($hunks, $i) = @_;
1302 for (; $i < @$hunks && $ctr < 20; $i++, $ctr++) {
1304 if (defined $hunks->[$i]{USE}) {
1305 $status = $hunks->[$i]{USE} ? "+" : "-";
1310 summarize_hunk($hunks->[$i]);
1315 my %patch_update_prompt_modes = (
1317 mode => N__("Stage mode change [y,n,q,a,d,/%s,?]? "),
1318 deletion => N__("Stage deletion [y,n,q,a,d,/%s,?]? "),
1319 hunk => N__("Stage this hunk [y,n,q,a,d,/%s,?]? "),
1322 mode => N__("Stash mode change [y,n,q,a,d,/%s,?]? "),
1323 deletion => N__("Stash deletion [y,n,q,a,d,/%s,?]? "),
1324 hunk => N__("Stash this hunk [y,n,q,a,d,/%s,?]? "),
1327 mode => N__("Unstage mode change [y,n,q,a,d,/%s,?]? "),
1328 deletion => N__("Unstage deletion [y,n,q,a,d,/%s,?]? "),
1329 hunk => N__("Unstage this hunk [y,n,q,a,d,/%s,?]? "),
1332 mode => N__("Apply mode change to index [y,n,q,a,d,/%s,?]? "),
1333 deletion => N__("Apply deletion to index [y,n,q,a,d,/%s,?]? "),
1334 hunk => N__("Apply this hunk to index [y,n,q,a,d,/%s,?]? "),
1337 mode => N__("Discard mode change from worktree [y,n,q,a,d,/%s,?]? "),
1338 deletion => N__("Discard deletion from worktree [y,n,q,a,d,/%s,?]? "),
1339 hunk => N__("Discard this hunk from worktree [y,n,q,a,d,/%s,?]? "),
1342 mode => N__("Discard mode change from index and worktree [y,n,q,a,d,/%s,?]? "),
1343 deletion => N__("Discard deletion from index and worktree [y,n,q,a,d,/%s,?]? "),
1344 hunk => N__("Discard this hunk from index and worktree [y,n,q,a,d,/%s,?]? "),
1346 checkout_nothead => {
1347 mode => N__("Apply mode change to index and worktree [y,n,q,a,d,/%s,?]? "),
1348 deletion => N__("Apply deletion to index and worktree [y,n,q,a,d,/%s,?]? "),
1349 hunk => N__("Apply this hunk to index and worktree [y,n,q,a,d,/%s,?]? "),
1353 sub patch_update_file {
1357 my ($head, @hunk) = parse_diff($path);
1358 ($head, my $mode, my $deletion) = parse_diff_header($head);
1359 for (@{$head->{DISPLAY}}) {
1363 if (@{$mode->{TEXT}}) {
1364 unshift @hunk, $mode;
1366 if (@{$deletion->{TEXT}}) {
1367 foreach my $hunk (@hunk) {
1368 push @{$deletion->{TEXT}}, @{$hunk->{TEXT}};
1369 push @{$deletion->{DISPLAY}}, @{$hunk->{DISPLAY}};
1371 @hunk = ($deletion);
1374 $num = scalar @hunk;
1378 my ($prev, $next, $other, $undecided, $i);
1384 for ($i = 0; $i < $ix; $i++) {
1385 if (!defined $hunk[$i]{USE}) {
1394 for ($i = $ix + 1; $i < $num; $i++) {
1395 if (!defined $hunk[$i]{USE}) {
1401 if ($ix < $num - 1) {
1407 for ($i = 0; $i < $num; $i++) {
1408 if (!defined $hunk[$i]{USE}) {
1413 last if (!$undecided);
1415 if ($hunk[$ix]{TYPE} eq 'hunk' &&
1416 hunk_splittable($hunk[$ix]{TEXT})) {
1419 if ($hunk[$ix]{TYPE} eq 'hunk') {
1422 for (@{$hunk[$ix]{DISPLAY}}) {
1425 print colored $prompt_color,
1426 sprintf(__($patch_update_prompt_modes{$patch_mode}{$hunk[$ix]{TYPE}}), $other);
1428 my $line = prompt_single_character;
1429 last unless defined $line;
1431 if ($line =~ /^y/i) {
1432 $hunk[$ix]{USE} = 1;
1434 elsif ($line =~ /^n/i) {
1435 $hunk[$ix]{USE} = 0;
1437 elsif ($line =~ /^a/i) {
1438 while ($ix < $num) {
1439 if (!defined $hunk[$ix]{USE}) {
1440 $hunk[$ix]{USE} = 1;
1446 elsif ($other =~ /g/ && $line =~ /^g(.*)/) {
1448 my $no = $ix > 10 ? $ix - 10 : 0;
1449 while ($response eq '') {
1450 $no = display_hunks(\@hunk, $no);
1452 print __("go to which hunk (<ret> to see more)? ");
1454 print __("go to which hunk? ");
1456 $response = <STDIN>;
1457 if (!defined $response) {
1462 if ($response !~ /^\s*\d+\s*$/) {
1463 error_msg sprintf(__("Invalid number: '%s'\n"),
1465 } elsif (0 < $response && $response <= $num) {
1466 $ix = $response - 1;
1468 error_msg sprintf(__n("Sorry, only %d hunk available.\n",
1469 "Sorry, only %d hunks available.\n", $num), $num);
1473 elsif ($line =~ /^d/i) {
1474 while ($ix < $num) {
1475 if (!defined $hunk[$ix]{USE}) {
1476 $hunk[$ix]{USE} = 0;
1482 elsif ($line =~ /^q/i) {
1483 for ($i = 0; $i < $num; $i++) {
1484 if (!defined $hunk[$i]{USE}) {
1491 elsif ($line =~ m|^/(.*)|) {
1494 print colored $prompt_color, __("search for regex? ");
1496 if (defined $regex) {
1502 $search_string = qr{$regex}m;
1505 my ($err,$exp) = ($@, $1);
1506 $err =~ s/ at .*git-add--interactive line \d+, <STDIN> line \d+.*$//;
1507 error_msg sprintf(__("Malformed search regexp %s: %s\n"), $exp, $err);
1512 my $text = join ("", @{$hunk[$iy]{TEXT}});
1513 last if ($text =~ $search_string);
1515 $iy = 0 if ($iy >= $num);
1517 error_msg __("No hunk matches the given pattern\n");
1524 elsif ($line =~ /^K/) {
1525 if ($other =~ /K/) {
1529 error_msg __("No previous hunk\n");
1533 elsif ($line =~ /^J/) {
1534 if ($other =~ /J/) {
1538 error_msg __("No next hunk\n");
1542 elsif ($line =~ /^k/) {
1543 if ($other =~ /k/) {
1547 !defined $hunk[$ix]{USE});
1551 error_msg __("No previous hunk\n");
1555 elsif ($line =~ /^j/) {
1556 if ($other !~ /j/) {
1557 error_msg __("No next hunk\n");
1561 elsif ($other =~ /s/ && $line =~ /^s/) {
1562 my @split = split_hunk($hunk[$ix]{TEXT}, $hunk[$ix]{DISPLAY});
1564 print colored $header_color, sprintf(
1565 __n("Split into %d hunk.\n",
1566 "Split into %d hunks.\n",
1567 scalar(@split)), scalar(@split));
1569 splice (@hunk, $ix, 1, @split);
1570 $num = scalar @hunk;
1573 elsif ($other =~ /e/ && $line =~ /^e/) {
1574 my $newhunk = edit_hunk_loop($head, \@hunk, $ix);
1575 if (defined $newhunk) {
1576 splice @hunk, $ix, 1, $newhunk;
1580 help_patch_cmd($other);
1586 last if ($ix >= $num ||
1587 !defined $hunk[$ix]{USE});
1592 @hunk = coalesce_overlapping_hunks(@hunk);
1598 push @result, @{$_->{TEXT}};
1603 my @patch = reassemble_patch($head->{TEXT}, @result);
1604 my $apply_routine = $patch_mode_flavour{APPLY};
1605 &$apply_routine(@patch);
1614 my @mods = list_modified('index-only');
1615 @mods = grep { !($_->{BINARY}) } @mods;
1617 my (@them) = list_and_choose({ PROMPT => __('Review diff'),
1619 HEADER => $status_head, },
1622 my $reference = (is_initial_commit()) ? get_empty_tree() : 'HEAD';
1623 system(qw(git diff -p --cached), $reference, '--',
1624 map { $_->{VALUE} } @them);
1633 # TRANSLATORS: please do not translate the command names
1634 # 'status', 'update', 'revert', etc.
1635 print colored $help_color, __ <<'EOF' ;
1636 status - show paths with changes
1637 update - add working tree state to the staged set of changes
1638 revert - revert staged set of changes back to the HEAD version
1639 patch - pick hunks and update selectively
1640 diff - view diff between HEAD and index
1641 add untracked - add contents of untracked files to the staged set of changes
1646 return unless @ARGV;
1647 my $arg = shift @ARGV;
1648 if ($arg =~ /--patch(?:=(.*))?/) {
1650 if ($1 eq 'reset') {
1651 $patch_mode = 'reset_head';
1652 $patch_mode_revision = 'HEAD';
1653 $arg = shift @ARGV or die __("missing --");
1655 $patch_mode_revision = $arg;
1656 $patch_mode = ($arg eq 'HEAD' ?
1657 'reset_head' : 'reset_nothead');
1658 $arg = shift @ARGV or die __("missing --");
1660 } elsif ($1 eq 'checkout') {
1661 $arg = shift @ARGV or die __("missing --");
1663 $patch_mode = 'checkout_index';
1665 $patch_mode_revision = $arg;
1666 $patch_mode = ($arg eq 'HEAD' ?
1667 'checkout_head' : 'checkout_nothead');
1668 $arg = shift @ARGV or die __("missing --");
1670 } elsif ($1 eq 'stage' or $1 eq 'stash') {
1672 $arg = shift @ARGV or die __("missing --");
1674 die sprintf(__("unknown --patch mode: %s"), $1);
1677 $patch_mode = 'stage';
1678 $arg = shift @ARGV or die __("missing --");
1680 die sprintf(__("invalid argument %s, expecting --"),
1681 $arg) unless $arg eq "--";
1682 %patch_mode_flavour = %{$patch_modes{$patch_mode}};
1683 $patch_mode_only = 1;
1685 elsif ($arg ne "--") {
1686 die sprintf(__("invalid argument %s, expecting --"), $arg);
1691 my @cmd = ([ 'status', \&status_cmd, ],
1692 [ 'update', \&update_cmd, ],
1693 [ 'revert', \&revert_cmd, ],
1694 [ 'add untracked', \&add_untracked_cmd, ],
1695 [ 'patch', \&patch_update_cmd, ],
1696 [ 'diff', \&diff_cmd, ],
1697 [ 'quit', \&quit_cmd, ],
1698 [ 'help', \&help_cmd, ],
1701 my ($it) = list_and_choose({ PROMPT => __('What now'),
1704 HEADER => __('*** Commands ***'),
1705 ON_EOF => \&quit_cmd,
1706 IMMEDIATE => 1 }, @cmd);
1720 if ($patch_mode_only) {