1#!/usr/bin/perl
2
3use 5.008;
4use strict;
5use warnings;
6use Git qw(unquote_path);
7use Git::I18N;
8
9binmode(STDOUT, ":raw");
10
11my $repo = Git->repository();
12
13my $menu_use_color = $repo->get_colorbool('color.interactive');
14my ($prompt_color, $header_color, $help_color) =
15 $menu_use_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'),
19 ) : ();
20my $error_color = ();
21if ($menu_use_color) {
22 my $help_color_spec = ($repo->config('color.interactive.help') or
23 'red bold');
24 $error_color = $repo->get_color('color.interactive.error',
25 $help_color_spec);
26}
27
28my $diff_use_color = $repo->get_colorbool('color.diff');
29my ($fraginfo_color) =
30 $diff_use_color ? (
31 $repo->get_color('color.diff.frag', 'cyan'),
32 ) : ();
33my ($diff_plain_color) =
34 $diff_use_color ? (
35 $repo->get_color('color.diff.plain', ''),
36 ) : ();
37my ($diff_old_color) =
38 $diff_use_color ? (
39 $repo->get_color('color.diff.old', 'red'),
40 ) : ();
41my ($diff_new_color) =
42 $diff_use_color ? (
43 $repo->get_color('color.diff.new', 'green'),
44 ) : ();
45
46my $normal_color = $repo->get_color("", "reset");
47
48my $diff_algorithm = $repo->config('diff.algorithm');
49my $diff_filter = $repo->config('interactive.difffilter');
50
51my $use_readkey = 0;
52my $use_termcap = 0;
53my %term_escapes;
54
55sub ReadMode;
56sub ReadKey;
57if ($repo->config_bool("interactive.singlekey")) {
58 eval {
59 require Term::ReadKey;
60 Term::ReadKey->import;
61 $use_readkey = 1;
62 };
63 if (!$use_readkey) {
64 print STDERR "missing Term::ReadKey, disabling interactive.singlekey\n";
65 }
66 eval {
67 require Term::Cap;
68 my $termcap = Term::Cap->Tgetent;
69 foreach (values %$termcap) {
70 $term_escapes{$_} = 1 if /^\e/;
71 }
72 $use_termcap = 1;
73 };
74}
75
76sub colored {
77 my $color = shift;
78 my $string = join("", @_);
79
80 if (defined $color) {
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$/;
89 }
90 return $string;
91}
92
93# command line options
94my $patch_mode_only;
95my $patch_mode;
96my $patch_mode_revision;
97
98sub apply_patch;
99sub apply_patch_for_checkout_commit;
100sub apply_patch_for_stash;
101
102my %patch_modes = (
103 'stage' => {
104 DIFF => 'diff-files -p',
105 APPLY => sub { apply_patch 'apply --cached', @_; },
106 APPLY_CHECK => 'apply --cached',
107 FILTER => 'file-only',
108 IS_REVERSE => 0,
109 },
110 'stash' => {
111 DIFF => 'diff-index -p HEAD',
112 APPLY => sub { apply_patch 'apply --cached', @_; },
113 APPLY_CHECK => 'apply --cached',
114 FILTER => undef,
115 IS_REVERSE => 0,
116 },
117 'reset_head' => {
118 DIFF => 'diff-index -p --cached',
119 APPLY => sub { apply_patch 'apply -R --cached', @_; },
120 APPLY_CHECK => 'apply -R --cached',
121 FILTER => 'index-only',
122 IS_REVERSE => 1,
123 },
124 'reset_nothead' => {
125 DIFF => 'diff-index -R -p --cached',
126 APPLY => sub { apply_patch 'apply --cached', @_; },
127 APPLY_CHECK => 'apply --cached',
128 FILTER => 'index-only',
129 IS_REVERSE => 0,
130 },
131 'checkout_index' => {
132 DIFF => 'diff-files -p',
133 APPLY => sub { apply_patch 'apply -R', @_; },
134 APPLY_CHECK => 'apply -R',
135 FILTER => 'file-only',
136 IS_REVERSE => 1,
137 },
138 'checkout_head' => {
139 DIFF => 'diff-index -p',
140 APPLY => sub { apply_patch_for_checkout_commit '-R', @_ },
141 APPLY_CHECK => 'apply -R',
142 FILTER => undef,
143 IS_REVERSE => 1,
144 },
145 'checkout_nothead' => {
146 DIFF => 'diff-index -R -p',
147 APPLY => sub { apply_patch_for_checkout_commit '', @_ },
148 APPLY_CHECK => 'apply',
149 FILTER => undef,
150 IS_REVERSE => 0,
151 },
152);
153
154$patch_mode = 'stage';
155my %patch_mode_flavour = %{$patch_modes{$patch_mode}};
156
157sub run_cmd_pipe {
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 ? "\"$_\"": $_ } @_;
162 return qx{@args};
163 } else {
164 my $fh = undef;
165 open($fh, '-|', @_) or die;
166 return <$fh>;
167 }
168}
169
170my ($GIT_DIR) = run_cmd_pipe(qw(git rev-parse --git-dir));
171
172if (!defined $GIT_DIR) {
173 exit(1); # rev-parse would have already said "not a git repo"
174}
175chomp($GIT_DIR);
176
177sub refresh {
178 my $fh;
179 open $fh, 'git update-index --refresh |'
180 or die;
181 while (<$fh>) {
182 ;# ignore 'needs update'
183 }
184 close $fh;
185}
186
187sub list_untracked {
188 map {
189 chomp $_;
190 unquote_path($_);
191 }
192 run_cmd_pipe(qw(git ls-files --others --exclude-standard --), @ARGV);
193}
194
195# TRANSLATORS: you can adjust this to align "git add -i" status menu
196my $status_fmt = __('%12s %12s %s');
197my $status_head = sprintf($status_fmt, __('staged'), __('unstaged'), __('path'));
198
199{
200 my $initial;
201 sub is_initial_commit {
202 $initial = system('git rev-parse HEAD -- >/dev/null 2>&1') != 0
203 unless defined $initial;
204 return $initial;
205 }
206}
207
208sub get_empty_tree {
209 return '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
210}
211
212sub get_diff_reference {
213 my $ref = shift;
214 if (defined $ref and $ref ne 'HEAD') {
215 return $ref;
216 } elsif (is_initial_commit()) {
217 return get_empty_tree();
218 } else {
219 return 'HEAD';
220 }
221}
222
223# Returns list of hashes, contents of each of which are:
224# VALUE: pathname
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
231
232sub list_modified {
233 my ($only) = @_;
234 my (%data, @return);
235 my ($add, $del, $adddel, $file);
236
237 my $reference = get_diff_reference($patch_mode_revision);
238 for (run_cmd_pipe(qw(git diff-index --cached
239 --numstat --summary), $reference,
240 '--', @ARGV)) {
241 if (($add, $del, $file) =
242 /^([-\d]+) ([-\d]+) (.*)/) {
243 my ($change, $bin);
244 $file = unquote_path($file);
245 if ($add eq '-' && $del eq '-') {
246 $change = __('binary');
247 $bin = 1;
248 }
249 else {
250 $change = "+$add/-$del";
251 }
252 $data{$file} = {
253 INDEX => $change,
254 BINARY => $bin,
255 FILE => __('nothing'),
256 }
257 }
258 elsif (($adddel, $file) =
259 /^ (create|delete) mode [0-7]+ (.*)$/) {
260 $file = unquote_path($file);
261 $data{$file}{INDEX_ADDDEL} = $adddel;
262 }
263 }
264
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);
269 my ($change, $bin);
270 if ($add eq '-' && $del eq '-') {
271 $change = __('binary');
272 $bin = 1;
273 }
274 else {
275 $change = "+$add/-$del";
276 }
277 $data{$file}{FILE} = $change;
278 if ($bin) {
279 $data{$file}{BINARY} = 1;
280 }
281 }
282 elsif (($adddel, $file) =
283 /^ (create|delete) mode [0-7]+ (.*)$/) {
284 $file = unquote_path($file);
285 $data{$file}{FILE_ADDDEL} = $adddel;
286 }
287 elsif (/^:[0-7]+ [0-7]+ [0-9a-f]+ [0-9a-f]+ (.) (.*)$/) {
288 $file = unquote_path($2);
289 if (!exists $data{$file}) {
290 $data{$file} = +{
291 INDEX => __('unchanged'),
292 BINARY => 0,
293 };
294 }
295 if ($1 eq 'U') {
296 $data{$file}{UNMERGED} = 1;
297 }
298 }
299 }
300
301 for (sort keys %data) {
302 my $it = $data{$_};
303
304 if ($only) {
305 if ($only eq 'index-only') {
306 next if ($it->{INDEX} eq __('unchanged'));
307 }
308 if ($only eq 'file-only') {
309 next if ($it->{FILE} eq __('nothing'));
310 }
311 }
312 push @return, +{
313 VALUE => $_,
314 %$it,
315 };
316 }
317 return @return;
318}
319
320sub find_unique {
321 my ($string, @stuff) = @_;
322 my $found = undef;
323 for (my $i = 0; $i < @stuff; $i++) {
324 my $it = $stuff[$i];
325 my $hit = undef;
326 if (ref $it) {
327 if ((ref $it) eq 'ARRAY') {
328 $it = $it->[0];
329 }
330 else {
331 $it = $it->{VALUE};
332 }
333 }
334 eval {
335 if ($it =~ /^$string/) {
336 $hit = 1;
337 };
338 };
339 if (defined $hit && defined $found) {
340 return undef;
341 }
342 if ($hit) {
343 $found = $i + 1;
344 }
345 }
346 return $found;
347}
348
349# inserts string into trie and updates count for each character
350sub update_trie {
351 my ($trie, $string) = @_;
352 foreach (split //, $string) {
353 $trie = $trie->{$_} ||= {COUNT => 0};
354 $trie->{COUNT}++;
355 }
356}
357
358# returns an array of tuples (prefix, remainder)
359sub find_unique_prefixes {
360 my @stuff = @_;
361 my @return = ();
362
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
366 my $soft_limit = 0;
367 my $hard_limit = 3;
368
369 # build a trie modelling all possible options
370 my %trie;
371 foreach my $print (@stuff) {
372 if ((ref $print) eq 'ARRAY') {
373 $print = $print->[0];
374 }
375 elsif ((ref $print) eq 'HASH') {
376 $print = $print->{VALUE};
377 }
378 update_trie(\%trie, $print);
379 push @return, $print;
380 }
381
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;
386 my %search = %trie;
387 my ($prefix, $remainder);
388 my $j;
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;
394 last;
395 }
396 else {
397 my $prefix = substr $ret, 0, $j;
398 return ()
399 if ($hard_limit && $j + 1 > $hard_limit);
400 }
401 %search = %{$search{$letter}};
402 }
403 if (ord($letters[0]) > 127 ||
404 ($soft_limit && $j + 1 > $soft_limit)) {
405 $prefix = undef;
406 $remainder = $ret;
407 }
408 $return[$i] = [$prefix, $remainder];
409 }
410 return @return;
411}
412
413# filters out prefixes which have special meaning to list_and_choose()
414sub is_valid_prefix {
415 my $prefix = shift;
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
422}
423
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)
426sub highlight_prefix {
427 my $prefix = shift;
428 my $remainder = shift;
429
430 if (!defined $prefix) {
431 return $remainder;
432 }
433
434 if (!is_valid_prefix($prefix)) {
435 return "$prefix$remainder";
436 }
437
438 if (!$menu_use_color) {
439 return "[$prefix]$remainder";
440 }
441
442 return "$prompt_color$prefix$normal_color$remainder";
443}
444
445sub error_msg {
446 print STDERR colored $error_color, @_;
447}
448
449sub list_and_choose {
450 my ($opts, @stuff) = @_;
451 my (@chosen, @return);
452 if (!@stuff) {
453 return @return;
454 }
455 my $i;
456 my @prefixes = find_unique_prefixes(@stuff) unless $opts->{LIST_ONLY};
457
458 TOPLOOP:
459 while (1) {
460 my $last_lf = 0;
461
462 if ($opts->{HEADER}) {
463 if (!$opts->{LIST_FLAT}) {
464 print " ";
465 }
466 print colored $header_color, "$opts->{HEADER}\n";
467 }
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]})
473 if @prefixes;
474 if ($ref eq 'ARRAY') {
475 $print = $highlighted || $print->[0];
476 }
477 elsif ($ref eq 'HASH') {
478 my $value = $highlighted || $print->{VALUE};
479 $print = sprintf($status_fmt,
480 $print->{INDEX},
481 $print->{FILE},
482 $value);
483 }
484 else {
485 $print = $highlighted || $print;
486 }
487 printf("%s%2d: %s", $chosen, $i+1, $print);
488 if (($opts->{LIST_FLAT}) &&
489 (($i + 1) % ($opts->{LIST_FLAT}))) {
490 print "\t";
491 $last_lf = 0;
492 }
493 else {
494 print "\n";
495 $last_lf = 1;
496 }
497 }
498 if (!$last_lf) {
499 print "\n";
500 }
501
502 return if ($opts->{LIST_ONLY});
503
504 print colored $prompt_color, $opts->{PROMPT};
505 if ($opts->{SINGLETON}) {
506 print "> ";
507 }
508 else {
509 print ">> ";
510 }
511 my $line = <STDIN>;
512 if (!$line) {
513 print "\n";
514 $opts->{ON_EOF}->() if $opts->{ON_EOF};
515 last;
516 }
517 chomp $line;
518 last if $line eq '';
519 if ($line eq '?') {
520 $opts->{SINGLETON} ?
521 singleton_prompt_help_cmd() :
522 prompt_help_cmd();
523 next TOPLOOP;
524 }
525 for my $choice (split(/[\s,]+/, $line)) {
526 my $choose = 1;
527 my ($bottom, $top);
528
529 # Input that begins with '-'; unchoose
530 if ($choice =~ s/^-//) {
531 $choose = 0;
532 }
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);
536 }
537 elsif ($choice =~ /^\d+$/) {
538 $bottom = $top = $choice;
539 }
540 elsif ($choice eq '*') {
541 $bottom = 1;
542 $top = 1 + @stuff;
543 }
544 else {
545 $bottom = $top = find_unique($choice, @stuff);
546 if (!defined $bottom) {
547 error_msg sprintf(__("Huh (%s)?\n"), $choice);
548 next TOPLOOP;
549 }
550 }
551 if ($opts->{SINGLETON} && $bottom != $top) {
552 error_msg sprintf(__("Huh (%s)?\n"), $choice);
553 next TOPLOOP;
554 }
555 for ($i = $bottom-1; $i <= $top-1; $i++) {
556 next if (@stuff <= $i || $i < 0);
557 $chosen[$i] = $choose;
558 }
559 }
560 last if ($opts->{IMMEDIATE} || $line eq '*');
561 }
562 for ($i = 0; $i < @stuff; $i++) {
563 if ($chosen[$i]) {
564 push @return, $stuff[$i];
565 }
566 }
567 return @return;
568}
569
570sub singleton_prompt_help_cmd {
571 print colored $help_color, __ <<'EOF' ;
572Prompt help:
5731 - select a numbered item
574foo - select item based on unique prefix
575 - (empty) select nothing
576EOF
577}
578
579sub prompt_help_cmd {
580 print colored $help_color, __ <<'EOF' ;
581Prompt help:
5821 - select a single item
5833-5 - select a range of items
5842-3,6-9 - select multiple ranges
585foo - select item based on unique prefix
586-... - unselect specified items
587* - choose all items
588 - (empty) finish selecting
589EOF
590}
591
592sub status_cmd {
593 list_and_choose({ LIST_ONLY => 1, HEADER => $status_head },
594 list_modified());
595 print "\n";
596}
597
598sub say_n_paths {
599 my $did = shift @_;
600 my $cnt = scalar @_;
601 if ($did eq 'added') {
602 printf(__n("added %d path\n", "added %d paths\n",
603 $cnt), $cnt);
604 } elsif ($did eq 'updated') {
605 printf(__n("updated %d path\n", "updated %d paths\n",
606 $cnt), $cnt);
607 } elsif ($did eq 'reverted') {
608 printf(__n("reverted %d path\n", "reverted %d paths\n",
609 $cnt), $cnt);
610 } else {
611 printf(__n("touched %d path\n", "touched %d paths\n",
612 $cnt), $cnt);
613 }
614}
615
616sub update_cmd {
617 my @mods = list_modified('file-only');
618 return if (!@mods);
619
620 my @update = list_and_choose({ PROMPT => __('Update'),
621 HEADER => $status_head, },
622 @mods);
623 if (@update) {
624 system(qw(git update-index --add --remove --),
625 map { $_->{VALUE} } @update);
626 say_n_paths('updated', @update);
627 }
628 print "\n";
629}
630
631sub revert_cmd {
632 my @update = list_and_choose({ PROMPT => __('Revert'),
633 HEADER => $status_head, },
634 list_modified());
635 if (@update) {
636 if (is_initial_commit()) {
637 system(qw(git rm --cached),
638 map { $_->{VALUE} } @update);
639 }
640 else {
641 my @lines = run_cmd_pipe(qw(git ls-tree HEAD --),
642 map { $_->{VALUE} } @update);
643 my $fh;
644 open $fh, '| git update-index --index-info'
645 or die;
646 for (@lines) {
647 print $fh $_;
648 }
649 close($fh);
650 for (@update) {
651 if ($_->{INDEX_ADDDEL} &&
652 $_->{INDEX_ADDDEL} eq 'create') {
653 system(qw(git update-index --force-remove --),
654 $_->{VALUE});
655 printf(__("note: %s is untracked now.\n"), $_->{VALUE});
656 }
657 }
658 }
659 refresh();
660 say_n_paths('reverted', @update);
661 }
662 print "\n";
663}
664
665sub add_untracked_cmd {
666 my @add = list_and_choose({ PROMPT => __('Add untracked') },
667 list_untracked());
668 if (@add) {
669 system(qw(git update-index --add --), @add);
670 say_n_paths('added', @add);
671 } else {
672 print __("No untracked files.\n");
673 }
674 print "\n";
675}
676
677sub run_git_apply {
678 my $cmd = shift;
679 my $fh;
680 open $fh, '| git ' . $cmd . " --allow-overlap";
681 print $fh @_;
682 return close $fh;
683}
684
685sub parse_diff {
686 my ($path) = @_;
687 my @diff_cmd = split(" ", $patch_mode_flavour{DIFF});
688 if (defined $diff_algorithm) {
689 splice @diff_cmd, 1, 0, "--diff-algorithm=${diff_algorithm}";
690 }
691 if (defined $patch_mode_revision) {
692 push @diff_cmd, get_diff_reference($patch_mode_revision);
693 }
694 my @diff = run_cmd_pipe("git", @diff_cmd, "--", $path);
695 my @colored = ();
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");
702 }
703
704 @colored = run_cmd_pipe(@display_cmd);
705 }
706 my (@hunk) = { TEXT => [], DISPLAY => [], TYPE => 'header' };
707
708 for (my $i = 0; $i < @diff; $i++) {
709 if ($diff[$i] =~ /^@@ /) {
710 push @hunk, { TEXT => [], DISPLAY => [],
711 TYPE => 'hunk' };
712 }
713 push @{$hunk[-1]{TEXT}}, $diff[$i];
714 push @{$hunk[-1]{DISPLAY}},
715 (@colored ? $colored[$i] : $diff[$i]);
716 }
717 return @hunk;
718}
719
720sub parse_diff_header {
721 my $src = shift;
722
723 my $head = { TEXT => [], DISPLAY => [], TYPE => 'header' };
724 my $mode = { TEXT => [], DISPLAY => [], TYPE => 'mode' };
725 my $deletion = { TEXT => [], DISPLAY => [], TYPE => 'deletion' };
726
727 for (my $i = 0; $i < @{$src->{TEXT}}; $i++) {
728 my $dest =
729 $src->{TEXT}->[$i] =~ /^(old|new) mode (\d+)$/ ? $mode :
730 $src->{TEXT}->[$i] =~ /^deleted file/ ? $deletion :
731 $head;
732 push @{$dest->{TEXT}}, $src->{TEXT}->[$i];
733 push @{$dest->{DISPLAY}}, $src->{DISPLAY}->[$i];
734 }
735 return ($head, $mode, $deletion);
736}
737
738sub hunk_splittable {
739 my ($text) = @_;
740
741 my @s = split_hunk($text);
742 return (1 < @s);
743}
744
745sub parse_hunk_header {
746 my ($line) = @_;
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);
752}
753
754sub format_hunk_header {
755 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = @_;
756 return ("@@ -$o_ofs" .
757 (($o_cnt != 1) ? ",$o_cnt" : '') .
758 " +$n_ofs" .
759 (($n_cnt != 1) ? ",$n_cnt" : '') .
760 " @@\n");
761}
762
763sub split_hunk {
764 my ($text, $display) = @_;
765 my @split = ();
766 if (!defined $display) {
767 $display = $text;
768 }
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
771 # overlaps later.
772
773 my ($o_ofs, undef, $n_ofs) = parse_hunk_header($text->[0]);
774 my $hunk_start = 1;
775
776 OUTER:
777 while (1) {
778 my $next_hunk_start = undef;
779 my $i = $hunk_start - 1;
780 my $this = +{
781 TEXT => [],
782 DISPLAY => [],
783 TYPE => 'hunk',
784 OLD => $o_ofs,
785 NEW => $n_ofs,
786 OCNT => 0,
787 NCNT => 0,
788 ADDDEL => 0,
789 POSTCTX => 0,
790 USE => undef,
791 };
792
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;
799 next;
800 }
801 if ($line =~ /^ /) {
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
808 # one.
809 $next_hunk_start = $i;
810 }
811 push @{$this->{TEXT}}, $line;
812 push @{$this->{DISPLAY}}, $display;
813 $this->{OCNT}++;
814 $this->{NCNT}++;
815 if (defined $next_hunk_start) {
816 $this->{POSTCTX}++;
817 }
818 next;
819 }
820
821 # add/del
822 if (defined $next_hunk_start) {
823 # We are done with the current hunk and
824 # this is the first real change for the
825 # next split one.
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};
831 push @split, $this;
832 redo OUTER;
833 }
834 push @{$this->{TEXT}}, $line;
835 push @{$this->{DISPLAY}}, $display;
836 $this->{ADDDEL}++;
837 if ($line =~ /^-/) {
838 $this->{OCNT}++;
839 }
840 else {
841 $this->{NCNT}++;
842 }
843 }
844
845 push @split, $this;
846 last;
847 }
848
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};
854
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);
860 }
861 unshift @{$hunk->{DISPLAY}}, $display_head;
862 }
863 return @split;
864}
865
866sub find_last_o_ctx {
867 my ($it) = @_;
868 my $text = $it->{TEXT};
869 my ($o_ofs, $o_cnt) = parse_hunk_header($text->[0]);
870 my $i = @{$text};
871 my $last_o_ctx = $o_ofs + $o_cnt;
872 while (0 < --$i) {
873 my $line = $text->[$i];
874 if ($line =~ /^ /) {
875 $last_o_ctx--;
876 next;
877 }
878 last;
879 }
880 return $last_o_ctx;
881}
882
883sub merge_hunk {
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]);
889
890 my (@line, $i, $ofs, $o_cnt, $n_cnt);
891 $ofs = $o0_ofs;
892 $o_cnt = $n_cnt = 0;
893 for ($i = 1; $i < @{$prev->{TEXT}}; $i++) {
894 my $line = $prev->{TEXT}[$i];
895 if ($line =~ /^\+/) {
896 $n_cnt++;
897 push @line, $line;
898 next;
899 } elsif ($line =~ /^\\/) {
900 push @line, $line;
901 next;
902 }
903
904 last if ($o1_ofs <= $ofs);
905
906 $o_cnt++;
907 $ofs++;
908 if ($line =~ /^ /) {
909 $n_cnt++;
910 }
911 push @line, $line;
912 }
913
914 for ($i = 1; $i < @{$this->{TEXT}}; $i++) {
915 my $line = $this->{TEXT}[$i];
916 if ($line =~ /^\+/) {
917 $n_cnt++;
918 push @line, $line;
919 next;
920 } elsif ($line =~ /^\\/) {
921 push @line, $line;
922 next;
923 }
924 $ofs++;
925 $o_cnt++;
926 if ($line =~ /^ /) {
927 $n_cnt++;
928 }
929 push @line, $line;
930 }
931 my $head = format_hunk_header($o0_ofs, $o_cnt, $n0_ofs, $n_cnt);
932 @{$prev->{TEXT}} = ($head, @line);
933}
934
935sub coalesce_overlapping_hunks {
936 my (@in) = @_;
937 my @out = ();
938
939 my ($last_o_ctx, $last_was_dirty);
940 my $ofs_delta = 0;
941
942 for (@in) {
943 if ($_->{TYPE} ne 'hunk') {
944 push @out, $_;
945 next;
946 }
947 my $text = $_->{TEXT};
948 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
949 parse_hunk_header($text->[0]);
950 unless ($_->{USE}) {
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};
956 }
957 next;
958 }
959 if ($ofs_delta) {
960 if ($patch_mode_flavour{IS_REVERSE}) {
961 $o_ofs -= $ofs_delta;
962 } else {
963 $n_ofs += $ofs_delta;
964 }
965 $_->{TEXT}->[0] = format_hunk_header($o_ofs, $o_cnt,
966 $n_ofs, $n_cnt);
967 }
968 # If this hunk was edited then adjust the offset delta
969 # to reflect the edit.
970 if ($_->{OFS_DELTA}) {
971 $ofs_delta += $_->{OFS_DELTA};
972 }
973 if (defined $last_o_ctx &&
974 $o_ofs <= $last_o_ctx &&
975 !$_->{DIRTY} &&
976 !$last_was_dirty) {
977 merge_hunk($out[-1], $_);
978 }
979 else {
980 push @out, $_;
981 }
982 $last_o_ctx = find_last_o_ctx($out[-1]);
983 $last_was_dirty = $_->{DIRTY};
984 }
985 return @out;
986}
987
988sub reassemble_patch {
989 my $head = shift;
990 my @patch;
991
992 # Include everything in the header except the beginning of the diff.
993 push @patch, (grep { !/^[-+]{3}/ } @$head);
994
995 # Then include any headers from the hunk lines, which must
996 # come before any actual hunk.
997 while (@_ && $_[0] !~ /^@/) {
998 push @patch, shift;
999 }
1000
1001 # Then begin the diff.
1002 push @patch, grep { /^[-+]{3}/ } @$head;
1003
1004 # And then the actual hunks.
1005 push @patch, @_;
1006
1007 return @patch;
1008}
1009
1010sub color_diff {
1011 return map {
1012 colored((/^@/ ? $fraginfo_color :
1013 /^\+/ ? $diff_new_color :
1014 /^-/ ? $diff_old_color :
1015 $diff_plain_color),
1016 $_);
1017 } @_;
1018}
1019
1020my %edit_hunk_manually_modes = (
1021 stage => N__(
1022"If the patch applies cleanly, the edited hunk will immediately be
1023marked for staging."),
1024 stash => N__(
1025"If the patch applies cleanly, the edited hunk will immediately be
1026marked for stashing."),
1027 reset_head => N__(
1028"If the patch applies cleanly, the edited hunk will immediately be
1029marked for unstaging."),
1030 reset_nothead => N__(
1031"If the patch applies cleanly, the edited hunk will immediately be
1032marked for applying."),
1033 checkout_index => N__(
1034"If the patch applies cleanly, the edited hunk will immediately be
1035marked for discarding."),
1036 checkout_head => N__(
1037"If the patch applies cleanly, the edited hunk will immediately be
1038marked for discarding."),
1039 checkout_nothead => N__(
1040"If the patch applies cleanly, the edited hunk will immediately be
1041marked for applying."),
1042);
1043
1044sub recount_edited_hunk {
1045 local $_;
1046 my ($oldtext, $newtext) = @_;
1047 my ($o_cnt, $n_cnt) = (0, 0);
1048 for (@{$newtext}[1..$#{$newtext}]) {
1049 my $mode = substr($_, 0, 1);
1050 if ($mode eq '-') {
1051 $o_cnt++;
1052 } elsif ($mode eq '+') {
1053 $n_cnt++;
1054 } elsif ($mode eq ' ' or $mode eq "\n") {
1055 $o_cnt++;
1056 $n_cnt++;
1057 }
1058 }
1059 my ($o_ofs, undef, $n_ofs, undef) =
1060 parse_hunk_header($newtext->[0]);
1061 $newtext->[0] = format_hunk_header($o_ofs, $o_cnt, $n_ofs, $n_cnt);
1062 my (undef, $orig_o_cnt, undef, $orig_n_cnt) =
1063 parse_hunk_header($oldtext->[0]);
1064 # Return the change in the number of lines inserted by this hunk
1065 return $orig_o_cnt - $orig_n_cnt - $o_cnt + $n_cnt;
1066}
1067
1068sub edit_hunk_manually {
1069 my ($oldtext) = @_;
1070
1071 my $hunkfile = $repo->repo_path . "/addp-hunk-edit.diff";
1072 my $fh;
1073 open $fh, '>', $hunkfile
1074 or die sprintf(__("failed to open hunk edit file for writing: %s"), $!);
1075 print $fh Git::comment_lines __("Manual hunk edit mode -- see bottom for a quick guide.\n");
1076 print $fh @$oldtext;
1077 my $is_reverse = $patch_mode_flavour{IS_REVERSE};
1078 my ($remove_plus, $remove_minus) = $is_reverse ? ('-', '+') : ('+', '-');
1079 my $comment_line_char = Git::get_comment_line_char;
1080 print $fh Git::comment_lines sprintf(__ <<EOF, $remove_minus, $remove_plus, $comment_line_char),
1081---
1082To remove '%s' lines, make them ' ' lines (context).
1083To remove '%s' lines, delete them.
1084Lines starting with %s will be removed.
1085EOF
1086__($edit_hunk_manually_modes{$patch_mode}),
1087# TRANSLATORS: 'it' refers to the patch mentioned in the previous messages.
1088__ <<EOF2 ;
1089If it does not apply cleanly, you will be given an opportunity to
1090edit again. If all lines of the hunk are removed, then the edit is
1091aborted and the hunk is left unchanged.
1092EOF2
1093 close $fh;
1094
1095 chomp(my $editor = run_cmd_pipe(qw(git var GIT_EDITOR)));
1096 system('sh', '-c', $editor.' "$@"', $editor, $hunkfile);
1097
1098 if ($? != 0) {
1099 return undef;
1100 }
1101
1102 open $fh, '<', $hunkfile
1103 or die sprintf(__("failed to open hunk edit file for reading: %s"), $!);
1104 my @newtext = grep { !/^\Q$comment_line_char\E/ } <$fh>;
1105 close $fh;
1106 unlink $hunkfile;
1107
1108 # Abort if nothing remains
1109 if (!grep { /\S/ } @newtext) {
1110 return undef;
1111 }
1112
1113 # Reinsert the first hunk header if the user accidentally deleted it
1114 if ($newtext[0] !~ /^@/) {
1115 unshift @newtext, $oldtext->[0];
1116 }
1117 return \@newtext;
1118}
1119
1120sub diff_applies {
1121 return run_git_apply($patch_mode_flavour{APPLY_CHECK} . ' --check',
1122 map { @{$_->{TEXT}} } @_);
1123}
1124
1125sub _restore_terminal_and_die {
1126 ReadMode 'restore';
1127 print "\n";
1128 exit 1;
1129}
1130
1131sub prompt_single_character {
1132 if ($use_readkey) {
1133 local $SIG{TERM} = \&_restore_terminal_and_die;
1134 local $SIG{INT} = \&_restore_terminal_and_die;
1135 ReadMode 'cbreak';
1136 my $key = ReadKey 0;
1137 ReadMode 'restore';
1138 if ($use_termcap and $key eq "\e") {
1139 while (!defined $term_escapes{$key}) {
1140 my $next = ReadKey 0.5;
1141 last if (!defined $next);
1142 $key .= $next;
1143 }
1144 $key =~ s/\e/^[/;
1145 }
1146 print "$key" if defined $key;
1147 print "\n";
1148 return $key;
1149 } else {
1150 return <STDIN>;
1151 }
1152}
1153
1154sub prompt_yesno {
1155 my ($prompt) = @_;
1156 while (1) {
1157 print colored $prompt_color, $prompt;
1158 my $line = prompt_single_character;
1159 return undef unless defined $line;
1160 return 0 if $line =~ /^n/i;
1161 return 1 if $line =~ /^y/i;
1162 }
1163}
1164
1165sub edit_hunk_loop {
1166 my ($head, $hunks, $ix) = @_;
1167 my $hunk = $hunks->[$ix];
1168 my $text = $hunk->{TEXT};
1169
1170 while (1) {
1171 my $newtext = edit_hunk_manually($text);
1172 if (!defined $newtext) {
1173 return undef;
1174 }
1175 my $newhunk = {
1176 TEXT => $newtext,
1177 TYPE => $hunk->{TYPE},
1178 USE => 1,
1179 DIRTY => 1,
1180 };
1181 $newhunk->{OFS_DELTA} = recount_edited_hunk($text, $newtext);
1182 # If this hunk has already been edited then add the
1183 # offset delta of the previous edit to get the real
1184 # delta from the original unedited hunk.
1185 $hunk->{OFS_DELTA} and
1186 $newhunk->{OFS_DELTA} += $hunk->{OFS_DELTA};
1187 if (diff_applies($head,
1188 @{$hunks}[0..$ix-1],
1189 $newhunk,
1190 @{$hunks}[$ix+1..$#{$hunks}])) {
1191 $newhunk->{DISPLAY} = [color_diff(@{$newtext})];
1192 return $newhunk;
1193 }
1194 else {
1195 prompt_yesno(
1196 # TRANSLATORS: do not translate [y/n]
1197 # The program will only accept that input
1198 # at this point.
1199 # Consider translating (saying "no" discards!) as
1200 # (saying "n" for "no" discards!) if the translation
1201 # of the word "no" does not start with n.
1202 __('Your edited hunk does not apply. Edit again '
1203 . '(saying "no" discards!) [y/n]? ')
1204 ) or return undef;
1205 }
1206 }
1207}
1208
1209my %help_patch_modes = (
1210 stage => N__(
1211"y - stage this hunk
1212n - do not stage this hunk
1213q - quit; do not stage this hunk or any of the remaining ones
1214a - stage this hunk and all later hunks in the file
1215d - do not stage this hunk or any of the later hunks in the file"),
1216 stash => N__(
1217"y - stash this hunk
1218n - do not stash this hunk
1219q - quit; do not stash this hunk or any of the remaining ones
1220a - stash this hunk and all later hunks in the file
1221d - do not stash this hunk or any of the later hunks in the file"),
1222 reset_head => N__(
1223"y - unstage this hunk
1224n - do not unstage this hunk
1225q - quit; do not unstage this hunk or any of the remaining ones
1226a - unstage this hunk and all later hunks in the file
1227d - do not unstage this hunk or any of the later hunks in the file"),
1228 reset_nothead => N__(
1229"y - apply this hunk to index
1230n - do not apply this hunk to index
1231q - quit; do not apply this hunk or any of the remaining ones
1232a - apply this hunk and all later hunks in the file
1233d - do not apply this hunk or any of the later hunks in the file"),
1234 checkout_index => N__(
1235"y - discard this hunk from worktree
1236n - do not discard this hunk from worktree
1237q - quit; do not discard this hunk or any of the remaining ones
1238a - discard this hunk and all later hunks in the file
1239d - do not discard this hunk or any of the later hunks in the file"),
1240 checkout_head => N__(
1241"y - discard this hunk from index and worktree
1242n - do not discard this hunk from index and worktree
1243q - quit; do not discard this hunk or any of the remaining ones
1244a - discard this hunk and all later hunks in the file
1245d - do not discard this hunk or any of the later hunks in the file"),
1246 checkout_nothead => N__(
1247"y - apply this hunk to index and worktree
1248n - do not apply this hunk to index and worktree
1249q - quit; do not apply this hunk or any of the remaining ones
1250a - apply this hunk and all later hunks in the file
1251d - do not apply this hunk or any of the later hunks in the file"),
1252);
1253
1254sub help_patch_cmd {
1255 print colored $help_color, __($help_patch_modes{$patch_mode}), "\n", __ <<EOF ;
1256g - select a hunk to go to
1257/ - search for a hunk matching the given regex
1258j - leave this hunk undecided, see next undecided hunk
1259J - leave this hunk undecided, see next hunk
1260k - leave this hunk undecided, see previous undecided hunk
1261K - leave this hunk undecided, see previous hunk
1262s - split the current hunk into smaller hunks
1263e - manually edit the current hunk
1264? - print help
1265EOF
1266}
1267
1268sub apply_patch {
1269 my $cmd = shift;
1270 my $ret = run_git_apply $cmd, @_;
1271 if (!$ret) {
1272 print STDERR @_;
1273 }
1274 return $ret;
1275}
1276
1277sub apply_patch_for_checkout_commit {
1278 my $reverse = shift;
1279 my $applies_index = run_git_apply 'apply '.$reverse.' --cached --check', @_;
1280 my $applies_worktree = run_git_apply 'apply '.$reverse.' --check', @_;
1281
1282 if ($applies_worktree && $applies_index) {
1283 run_git_apply 'apply '.$reverse.' --cached', @_;
1284 run_git_apply 'apply '.$reverse, @_;
1285 return 1;
1286 } elsif (!$applies_index) {
1287 print colored $error_color, __("The selected hunks do not apply to the index!\n");
1288 if (prompt_yesno __("Apply them to the worktree anyway? ")) {
1289 return run_git_apply 'apply '.$reverse, @_;
1290 } else {
1291 print colored $error_color, __("Nothing was applied.\n");
1292 return 0;
1293 }
1294 } else {
1295 print STDERR @_;
1296 return 0;
1297 }
1298}
1299
1300sub patch_update_cmd {
1301 my @all_mods = list_modified($patch_mode_flavour{FILTER});
1302 error_msg sprintf(__("ignoring unmerged: %s\n"), $_->{VALUE})
1303 for grep { $_->{UNMERGED} } @all_mods;
1304 @all_mods = grep { !$_->{UNMERGED} } @all_mods;
1305
1306 my @mods = grep { !($_->{BINARY}) } @all_mods;
1307 my @them;
1308
1309 if (!@mods) {
1310 if (@all_mods) {
1311 print STDERR __("Only binary files changed.\n");
1312 } else {
1313 print STDERR __("No changes.\n");
1314 }
1315 return 0;
1316 }
1317 if ($patch_mode_only) {
1318 @them = @mods;
1319 }
1320 else {
1321 @them = list_and_choose({ PROMPT => __('Patch update'),
1322 HEADER => $status_head, },
1323 @mods);
1324 }
1325 for (@them) {
1326 return 0 if patch_update_file($_->{VALUE});
1327 }
1328}
1329
1330# Generate a one line summary of a hunk.
1331sub summarize_hunk {
1332 my $rhunk = shift;
1333 my $summary = $rhunk->{TEXT}[0];
1334
1335 # Keep the line numbers, discard extra context.
1336 $summary =~ s/@@(.*?)@@.*/$1 /s;
1337 $summary .= " " x (20 - length $summary);
1338
1339 # Add some user context.
1340 for my $line (@{$rhunk->{TEXT}}) {
1341 if ($line =~ m/^[+-].*\w/) {
1342 $summary .= $line;
1343 last;
1344 }
1345 }
1346
1347 chomp $summary;
1348 return substr($summary, 0, 80) . "\n";
1349}
1350
1351
1352# Print a one-line summary of each hunk in the array ref in
1353# the first argument, starting with the index in the 2nd.
1354sub display_hunks {
1355 my ($hunks, $i) = @_;
1356 my $ctr = 0;
1357 $i ||= 0;
1358 for (; $i < @$hunks && $ctr < 20; $i++, $ctr++) {
1359 my $status = " ";
1360 if (defined $hunks->[$i]{USE}) {
1361 $status = $hunks->[$i]{USE} ? "+" : "-";
1362 }
1363 printf "%s%2d: %s",
1364 $status,
1365 $i + 1,
1366 summarize_hunk($hunks->[$i]);
1367 }
1368 return $i;
1369}
1370
1371my %patch_update_prompt_modes = (
1372 stage => {
1373 mode => N__("Stage mode change [y,n,q,a,d,/%s,?]? "),
1374 deletion => N__("Stage deletion [y,n,q,a,d,/%s,?]? "),
1375 hunk => N__("Stage this hunk [y,n,q,a,d,/%s,?]? "),
1376 },
1377 stash => {
1378 mode => N__("Stash mode change [y,n,q,a,d,/%s,?]? "),
1379 deletion => N__("Stash deletion [y,n,q,a,d,/%s,?]? "),
1380 hunk => N__("Stash this hunk [y,n,q,a,d,/%s,?]? "),
1381 },
1382 reset_head => {
1383 mode => N__("Unstage mode change [y,n,q,a,d,/%s,?]? "),
1384 deletion => N__("Unstage deletion [y,n,q,a,d,/%s,?]? "),
1385 hunk => N__("Unstage this hunk [y,n,q,a,d,/%s,?]? "),
1386 },
1387 reset_nothead => {
1388 mode => N__("Apply mode change to index [y,n,q,a,d,/%s,?]? "),
1389 deletion => N__("Apply deletion to index [y,n,q,a,d,/%s,?]? "),
1390 hunk => N__("Apply this hunk to index [y,n,q,a,d,/%s,?]? "),
1391 },
1392 checkout_index => {
1393 mode => N__("Discard mode change from worktree [y,n,q,a,d,/%s,?]? "),
1394 deletion => N__("Discard deletion from worktree [y,n,q,a,d,/%s,?]? "),
1395 hunk => N__("Discard this hunk from worktree [y,n,q,a,d,/%s,?]? "),
1396 },
1397 checkout_head => {
1398 mode => N__("Discard mode change from index and worktree [y,n,q,a,d,/%s,?]? "),
1399 deletion => N__("Discard deletion from index and worktree [y,n,q,a,d,/%s,?]? "),
1400 hunk => N__("Discard this hunk from index and worktree [y,n,q,a,d,/%s,?]? "),
1401 },
1402 checkout_nothead => {
1403 mode => N__("Apply mode change to index and worktree [y,n,q,a,d,/%s,?]? "),
1404 deletion => N__("Apply deletion to index and worktree [y,n,q,a,d,/%s,?]? "),
1405 hunk => N__("Apply this hunk to index and worktree [y,n,q,a,d,/%s,?]? "),
1406 },
1407);
1408
1409sub patch_update_file {
1410 my $quit = 0;
1411 my ($ix, $num);
1412 my $path = shift;
1413 my ($head, @hunk) = parse_diff($path);
1414 ($head, my $mode, my $deletion) = parse_diff_header($head);
1415 for (@{$head->{DISPLAY}}) {
1416 print;
1417 }
1418
1419 if (@{$mode->{TEXT}}) {
1420 unshift @hunk, $mode;
1421 }
1422 if (@{$deletion->{TEXT}}) {
1423 foreach my $hunk (@hunk) {
1424 push @{$deletion->{TEXT}}, @{$hunk->{TEXT}};
1425 push @{$deletion->{DISPLAY}}, @{$hunk->{DISPLAY}};
1426 }
1427 @hunk = ($deletion);
1428 }
1429
1430 $num = scalar @hunk;
1431 $ix = 0;
1432
1433 while (1) {
1434 my ($prev, $next, $other, $undecided, $i);
1435 $other = '';
1436
1437 if ($num <= $ix) {
1438 $ix = 0;
1439 }
1440 for ($i = 0; $i < $ix; $i++) {
1441 if (!defined $hunk[$i]{USE}) {
1442 $prev = 1;
1443 $other .= ',k';
1444 last;
1445 }
1446 }
1447 if ($ix) {
1448 $other .= ',K';
1449 }
1450 for ($i = $ix + 1; $i < $num; $i++) {
1451 if (!defined $hunk[$i]{USE}) {
1452 $next = 1;
1453 $other .= ',j';
1454 last;
1455 }
1456 }
1457 if ($ix < $num - 1) {
1458 $other .= ',J';
1459 }
1460 if ($num > 1) {
1461 $other .= ',g';
1462 }
1463 for ($i = 0; $i < $num; $i++) {
1464 if (!defined $hunk[$i]{USE}) {
1465 $undecided = 1;
1466 last;
1467 }
1468 }
1469 last if (!$undecided);
1470
1471 if ($hunk[$ix]{TYPE} eq 'hunk' &&
1472 hunk_splittable($hunk[$ix]{TEXT})) {
1473 $other .= ',s';
1474 }
1475 if ($hunk[$ix]{TYPE} eq 'hunk') {
1476 $other .= ',e';
1477 }
1478 for (@{$hunk[$ix]{DISPLAY}}) {
1479 print;
1480 }
1481 print colored $prompt_color,
1482 sprintf(__($patch_update_prompt_modes{$patch_mode}{$hunk[$ix]{TYPE}}), $other);
1483
1484 my $line = prompt_single_character;
1485 last unless defined $line;
1486 if ($line) {
1487 if ($line =~ /^y/i) {
1488 $hunk[$ix]{USE} = 1;
1489 }
1490 elsif ($line =~ /^n/i) {
1491 $hunk[$ix]{USE} = 0;
1492 }
1493 elsif ($line =~ /^a/i) {
1494 while ($ix < $num) {
1495 if (!defined $hunk[$ix]{USE}) {
1496 $hunk[$ix]{USE} = 1;
1497 }
1498 $ix++;
1499 }
1500 next;
1501 }
1502 elsif ($other =~ /g/ && $line =~ /^g(.*)/) {
1503 my $response = $1;
1504 my $no = $ix > 10 ? $ix - 10 : 0;
1505 while ($response eq '') {
1506 $no = display_hunks(\@hunk, $no);
1507 if ($no < $num) {
1508 print __("go to which hunk (<ret> to see more)? ");
1509 } else {
1510 print __("go to which hunk? ");
1511 }
1512 $response = <STDIN>;
1513 if (!defined $response) {
1514 $response = '';
1515 }
1516 chomp $response;
1517 }
1518 if ($response !~ /^\s*\d+\s*$/) {
1519 error_msg sprintf(__("Invalid number: '%s'\n"),
1520 $response);
1521 } elsif (0 < $response && $response <= $num) {
1522 $ix = $response - 1;
1523 } else {
1524 error_msg sprintf(__n("Sorry, only %d hunk available.\n",
1525 "Sorry, only %d hunks available.\n", $num), $num);
1526 }
1527 next;
1528 }
1529 elsif ($line =~ /^d/i) {
1530 while ($ix < $num) {
1531 if (!defined $hunk[$ix]{USE}) {
1532 $hunk[$ix]{USE} = 0;
1533 }
1534 $ix++;
1535 }
1536 next;
1537 }
1538 elsif ($line =~ /^q/i) {
1539 for ($i = 0; $i < $num; $i++) {
1540 if (!defined $hunk[$i]{USE}) {
1541 $hunk[$i]{USE} = 0;
1542 }
1543 }
1544 $quit = 1;
1545 last;
1546 }
1547 elsif ($line =~ m|^/(.*)|) {
1548 my $regex = $1;
1549 if ($1 eq "") {
1550 print colored $prompt_color, __("search for regex? ");
1551 $regex = <STDIN>;
1552 if (defined $regex) {
1553 chomp $regex;
1554 }
1555 }
1556 my $search_string;
1557 eval {
1558 $search_string = qr{$regex}m;
1559 };
1560 if ($@) {
1561 my ($err,$exp) = ($@, $1);
1562 $err =~ s/ at .*git-add--interactive line \d+, <STDIN> line \d+.*$//;
1563 error_msg sprintf(__("Malformed search regexp %s: %s\n"), $exp, $err);
1564 next;
1565 }
1566 my $iy = $ix;
1567 while (1) {
1568 my $text = join ("", @{$hunk[$iy]{TEXT}});
1569 last if ($text =~ $search_string);
1570 $iy++;
1571 $iy = 0 if ($iy >= $num);
1572 if ($ix == $iy) {
1573 error_msg __("No hunk matches the given pattern\n");
1574 last;
1575 }
1576 }
1577 $ix = $iy;
1578 next;
1579 }
1580 elsif ($line =~ /^K/) {
1581 if ($other =~ /K/) {
1582 $ix--;
1583 }
1584 else {
1585 error_msg __("No previous hunk\n");
1586 }
1587 next;
1588 }
1589 elsif ($line =~ /^J/) {
1590 if ($other =~ /J/) {
1591 $ix++;
1592 }
1593 else {
1594 error_msg __("No next hunk\n");
1595 }
1596 next;
1597 }
1598 elsif ($line =~ /^k/) {
1599 if ($other =~ /k/) {
1600 while (1) {
1601 $ix--;
1602 last if (!$ix ||
1603 !defined $hunk[$ix]{USE});
1604 }
1605 }
1606 else {
1607 error_msg __("No previous hunk\n");
1608 }
1609 next;
1610 }
1611 elsif ($line =~ /^j/) {
1612 if ($other !~ /j/) {
1613 error_msg __("No next hunk\n");
1614 next;
1615 }
1616 }
1617 elsif ($other =~ /s/ && $line =~ /^s/) {
1618 my @split = split_hunk($hunk[$ix]{TEXT}, $hunk[$ix]{DISPLAY});
1619 if (1 < @split) {
1620 print colored $header_color, sprintf(
1621 __n("Split into %d hunk.\n",
1622 "Split into %d hunks.\n",
1623 scalar(@split)), scalar(@split));
1624 }
1625 splice (@hunk, $ix, 1, @split);
1626 $num = scalar @hunk;
1627 next;
1628 }
1629 elsif ($other =~ /e/ && $line =~ /^e/) {
1630 my $newhunk = edit_hunk_loop($head, \@hunk, $ix);
1631 if (defined $newhunk) {
1632 splice @hunk, $ix, 1, $newhunk;
1633 }
1634 }
1635 else {
1636 help_patch_cmd($other);
1637 next;
1638 }
1639 # soft increment
1640 while (1) {
1641 $ix++;
1642 last if ($ix >= $num ||
1643 !defined $hunk[$ix]{USE});
1644 }
1645 }
1646 }
1647
1648 @hunk = coalesce_overlapping_hunks(@hunk);
1649
1650 my $n_lofs = 0;
1651 my @result = ();
1652 for (@hunk) {
1653 if ($_->{USE}) {
1654 push @result, @{$_->{TEXT}};
1655 }
1656 }
1657
1658 if (@result) {
1659 my @patch = reassemble_patch($head->{TEXT}, @result);
1660 my $apply_routine = $patch_mode_flavour{APPLY};
1661 &$apply_routine(@patch);
1662 refresh();
1663 }
1664
1665 print "\n";
1666 return $quit;
1667}
1668
1669sub diff_cmd {
1670 my @mods = list_modified('index-only');
1671 @mods = grep { !($_->{BINARY}) } @mods;
1672 return if (!@mods);
1673 my (@them) = list_and_choose({ PROMPT => __('Review diff'),
1674 IMMEDIATE => 1,
1675 HEADER => $status_head, },
1676 @mods);
1677 return if (!@them);
1678 my $reference = (is_initial_commit()) ? get_empty_tree() : 'HEAD';
1679 system(qw(git diff -p --cached), $reference, '--',
1680 map { $_->{VALUE} } @them);
1681}
1682
1683sub quit_cmd {
1684 print __("Bye.\n");
1685 exit(0);
1686}
1687
1688sub help_cmd {
1689# TRANSLATORS: please do not translate the command names
1690# 'status', 'update', 'revert', etc.
1691 print colored $help_color, __ <<'EOF' ;
1692status - show paths with changes
1693update - add working tree state to the staged set of changes
1694revert - revert staged set of changes back to the HEAD version
1695patch - pick hunks and update selectively
1696diff - view diff between HEAD and index
1697add untracked - add contents of untracked files to the staged set of changes
1698EOF
1699}
1700
1701sub process_args {
1702 return unless @ARGV;
1703 my $arg = shift @ARGV;
1704 if ($arg =~ /--patch(?:=(.*))?/) {
1705 if (defined $1) {
1706 if ($1 eq 'reset') {
1707 $patch_mode = 'reset_head';
1708 $patch_mode_revision = 'HEAD';
1709 $arg = shift @ARGV or die __("missing --");
1710 if ($arg ne '--') {
1711 $patch_mode_revision = $arg;
1712 $patch_mode = ($arg eq 'HEAD' ?
1713 'reset_head' : 'reset_nothead');
1714 $arg = shift @ARGV or die __("missing --");
1715 }
1716 } elsif ($1 eq 'checkout') {
1717 $arg = shift @ARGV or die __("missing --");
1718 if ($arg eq '--') {
1719 $patch_mode = 'checkout_index';
1720 } else {
1721 $patch_mode_revision = $arg;
1722 $patch_mode = ($arg eq 'HEAD' ?
1723 'checkout_head' : 'checkout_nothead');
1724 $arg = shift @ARGV or die __("missing --");
1725 }
1726 } elsif ($1 eq 'stage' or $1 eq 'stash') {
1727 $patch_mode = $1;
1728 $arg = shift @ARGV or die __("missing --");
1729 } else {
1730 die sprintf(__("unknown --patch mode: %s"), $1);
1731 }
1732 } else {
1733 $patch_mode = 'stage';
1734 $arg = shift @ARGV or die __("missing --");
1735 }
1736 die sprintf(__("invalid argument %s, expecting --"),
1737 $arg) unless $arg eq "--";
1738 %patch_mode_flavour = %{$patch_modes{$patch_mode}};
1739 $patch_mode_only = 1;
1740 }
1741 elsif ($arg ne "--") {
1742 die sprintf(__("invalid argument %s, expecting --"), $arg);
1743 }
1744}
1745
1746sub main_loop {
1747 my @cmd = ([ 'status', \&status_cmd, ],
1748 [ 'update', \&update_cmd, ],
1749 [ 'revert', \&revert_cmd, ],
1750 [ 'add untracked', \&add_untracked_cmd, ],
1751 [ 'patch', \&patch_update_cmd, ],
1752 [ 'diff', \&diff_cmd, ],
1753 [ 'quit', \&quit_cmd, ],
1754 [ 'help', \&help_cmd, ],
1755 );
1756 while (1) {
1757 my ($it) = list_and_choose({ PROMPT => __('What now'),
1758 SINGLETON => 1,
1759 LIST_FLAT => 4,
1760 HEADER => __('*** Commands ***'),
1761 ON_EOF => \&quit_cmd,
1762 IMMEDIATE => 1 }, @cmd);
1763 if ($it) {
1764 eval {
1765 $it->[1]->();
1766 };
1767 if ($@) {
1768 print "$@";
1769 }
1770 }
1771 }
1772}
1773
1774process_args();
1775refresh();
1776if ($patch_mode_only) {
1777 patch_update_cmd();
1778}
1779else {
1780 status_cmd();
1781 main_loop();
1782}