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