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 --ignore-submodules=dirty --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 . " --recount --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 if (@colored && @colored != @diff) {
709 print STDERR
710 "fatal: mismatched output from interactive.diffFilter\n",
711 "hint: Your filter must maintain a one-to-one correspondence\n",
712 "hint: between its input and output lines.\n";
713 exit 1;
714 }
715
716 for (my $i = 0; $i < @diff; $i++) {
717 if ($diff[$i] =~ /^@@ /) {
718 push @hunk, { TEXT => [], DISPLAY => [],
719 TYPE => 'hunk' };
720 }
721 push @{$hunk[-1]{TEXT}}, $diff[$i];
722 push @{$hunk[-1]{DISPLAY}},
723 (@colored ? $colored[$i] : $diff[$i]);
724 }
725 return @hunk;
726}
727
728sub parse_diff_header {
729 my $src = shift;
730
731 my $head = { TEXT => [], DISPLAY => [], TYPE => 'header' };
732 my $mode = { TEXT => [], DISPLAY => [], TYPE => 'mode' };
733 my $deletion = { TEXT => [], DISPLAY => [], TYPE => 'deletion' };
734
735 for (my $i = 0; $i < @{$src->{TEXT}}; $i++) {
736 my $dest =
737 $src->{TEXT}->[$i] =~ /^(old|new) mode (\d+)$/ ? $mode :
738 $src->{TEXT}->[$i] =~ /^deleted file/ ? $deletion :
739 $head;
740 push @{$dest->{TEXT}}, $src->{TEXT}->[$i];
741 push @{$dest->{DISPLAY}}, $src->{DISPLAY}->[$i];
742 }
743 return ($head, $mode, $deletion);
744}
745
746sub hunk_splittable {
747 my ($text) = @_;
748
749 my @s = split_hunk($text);
750 return (1 < @s);
751}
752
753sub parse_hunk_header {
754 my ($line) = @_;
755 my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
756 $line =~ /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
757 $o_cnt = 1 unless defined $o_cnt;
758 $n_cnt = 1 unless defined $n_cnt;
759 return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
760}
761
762sub split_hunk {
763 my ($text, $display) = @_;
764 my @split = ();
765 if (!defined $display) {
766 $display = $text;
767 }
768 # If there are context lines in the middle of a hunk,
769 # it can be split, but we would need to take care of
770 # overlaps later.
771
772 my ($o_ofs, undef, $n_ofs) = parse_hunk_header($text->[0]);
773 my $hunk_start = 1;
774
775 OUTER:
776 while (1) {
777 my $next_hunk_start = undef;
778 my $i = $hunk_start - 1;
779 my $this = +{
780 TEXT => [],
781 DISPLAY => [],
782 TYPE => 'hunk',
783 OLD => $o_ofs,
784 NEW => $n_ofs,
785 OCNT => 0,
786 NCNT => 0,
787 ADDDEL => 0,
788 POSTCTX => 0,
789 USE => undef,
790 };
791
792 while (++$i < @$text) {
793 my $line = $text->[$i];
794 my $display = $display->[$i];
795 if ($line =~ /^ /) {
796 if ($this->{ADDDEL} &&
797 !defined $next_hunk_start) {
798 # We have seen leading context and
799 # adds/dels and then here is another
800 # context, which is trailing for this
801 # split hunk and leading for the next
802 # one.
803 $next_hunk_start = $i;
804 }
805 push @{$this->{TEXT}}, $line;
806 push @{$this->{DISPLAY}}, $display;
807 $this->{OCNT}++;
808 $this->{NCNT}++;
809 if (defined $next_hunk_start) {
810 $this->{POSTCTX}++;
811 }
812 next;
813 }
814
815 # add/del
816 if (defined $next_hunk_start) {
817 # We are done with the current hunk and
818 # this is the first real change for the
819 # next split one.
820 $hunk_start = $next_hunk_start;
821 $o_ofs = $this->{OLD} + $this->{OCNT};
822 $n_ofs = $this->{NEW} + $this->{NCNT};
823 $o_ofs -= $this->{POSTCTX};
824 $n_ofs -= $this->{POSTCTX};
825 push @split, $this;
826 redo OUTER;
827 }
828 push @{$this->{TEXT}}, $line;
829 push @{$this->{DISPLAY}}, $display;
830 $this->{ADDDEL}++;
831 if ($line =~ /^-/) {
832 $this->{OCNT}++;
833 }
834 else {
835 $this->{NCNT}++;
836 }
837 }
838
839 push @split, $this;
840 last;
841 }
842
843 for my $hunk (@split) {
844 $o_ofs = $hunk->{OLD};
845 $n_ofs = $hunk->{NEW};
846 my $o_cnt = $hunk->{OCNT};
847 my $n_cnt = $hunk->{NCNT};
848
849 my $head = ("@@ -$o_ofs" .
850 (($o_cnt != 1) ? ",$o_cnt" : '') .
851 " +$n_ofs" .
852 (($n_cnt != 1) ? ",$n_cnt" : '') .
853 " @@\n");
854 my $display_head = $head;
855 unshift @{$hunk->{TEXT}}, $head;
856 if ($diff_use_color) {
857 $display_head = colored($fraginfo_color, $head);
858 }
859 unshift @{$hunk->{DISPLAY}}, $display_head;
860 }
861 return @split;
862}
863
864sub find_last_o_ctx {
865 my ($it) = @_;
866 my $text = $it->{TEXT};
867 my ($o_ofs, $o_cnt) = parse_hunk_header($text->[0]);
868 my $i = @{$text};
869 my $last_o_ctx = $o_ofs + $o_cnt;
870 while (0 < --$i) {
871 my $line = $text->[$i];
872 if ($line =~ /^ /) {
873 $last_o_ctx--;
874 next;
875 }
876 last;
877 }
878 return $last_o_ctx;
879}
880
881sub merge_hunk {
882 my ($prev, $this) = @_;
883 my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) =
884 parse_hunk_header($prev->{TEXT}[0]);
885 my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) =
886 parse_hunk_header($this->{TEXT}[0]);
887
888 my (@line, $i, $ofs, $o_cnt, $n_cnt);
889 $ofs = $o0_ofs;
890 $o_cnt = $n_cnt = 0;
891 for ($i = 1; $i < @{$prev->{TEXT}}; $i++) {
892 my $line = $prev->{TEXT}[$i];
893 if ($line =~ /^\+/) {
894 $n_cnt++;
895 push @line, $line;
896 next;
897 }
898
899 last if ($o1_ofs <= $ofs);
900
901 $o_cnt++;
902 $ofs++;
903 if ($line =~ /^ /) {
904 $n_cnt++;
905 }
906 push @line, $line;
907 }
908
909 for ($i = 1; $i < @{$this->{TEXT}}; $i++) {
910 my $line = $this->{TEXT}[$i];
911 if ($line =~ /^\+/) {
912 $n_cnt++;
913 push @line, $line;
914 next;
915 }
916 $ofs++;
917 $o_cnt++;
918 if ($line =~ /^ /) {
919 $n_cnt++;
920 }
921 push @line, $line;
922 }
923 my $head = ("@@ -$o0_ofs" .
924 (($o_cnt != 1) ? ",$o_cnt" : '') .
925 " +$n0_ofs" .
926 (($n_cnt != 1) ? ",$n_cnt" : '') .
927 " @@\n");
928 @{$prev->{TEXT}} = ($head, @line);
929}
930
931sub coalesce_overlapping_hunks {
932 my (@in) = @_;
933 my @out = ();
934
935 my ($last_o_ctx, $last_was_dirty);
936
937 for (grep { $_->{USE} } @in) {
938 if ($_->{TYPE} ne 'hunk') {
939 push @out, $_;
940 next;
941 }
942 my $text = $_->{TEXT};
943 my ($o_ofs) = parse_hunk_header($text->[0]);
944 if (defined $last_o_ctx &&
945 $o_ofs <= $last_o_ctx &&
946 !$_->{DIRTY} &&
947 !$last_was_dirty) {
948 merge_hunk($out[-1], $_);
949 }
950 else {
951 push @out, $_;
952 }
953 $last_o_ctx = find_last_o_ctx($out[-1]);
954 $last_was_dirty = $_->{DIRTY};
955 }
956 return @out;
957}
958
959sub reassemble_patch {
960 my $head = shift;
961 my @patch;
962
963 # Include everything in the header except the beginning of the diff.
964 push @patch, (grep { !/^[-+]{3}/ } @$head);
965
966 # Then include any headers from the hunk lines, which must
967 # come before any actual hunk.
968 while (@_ && $_[0] !~ /^@/) {
969 push @patch, shift;
970 }
971
972 # Then begin the diff.
973 push @patch, grep { /^[-+]{3}/ } @$head;
974
975 # And then the actual hunks.
976 push @patch, @_;
977
978 return @patch;
979}
980
981sub color_diff {
982 return map {
983 colored((/^@/ ? $fraginfo_color :
984 /^\+/ ? $diff_new_color :
985 /^-/ ? $diff_old_color :
986 $diff_plain_color),
987 $_);
988 } @_;
989}
990
991my %edit_hunk_manually_modes = (
992 stage => N__(
993"If the patch applies cleanly, the edited hunk will immediately be
994marked for staging."),
995 stash => N__(
996"If the patch applies cleanly, the edited hunk will immediately be
997marked for stashing."),
998 reset_head => N__(
999"If the patch applies cleanly, the edited hunk will immediately be
1000marked for unstaging."),
1001 reset_nothead => N__(
1002"If the patch applies cleanly, the edited hunk will immediately be
1003marked for applying."),
1004 checkout_index => N__(
1005"If the patch applies cleanly, the edited hunk will immediately be
1006marked for discarding."),
1007 checkout_head => N__(
1008"If the patch applies cleanly, the edited hunk will immediately be
1009marked for discarding."),
1010 checkout_nothead => N__(
1011"If the patch applies cleanly, the edited hunk will immediately be
1012marked for applying."),
1013);
1014
1015sub edit_hunk_manually {
1016 my ($oldtext) = @_;
1017
1018 my $hunkfile = $repo->repo_path . "/addp-hunk-edit.diff";
1019 my $fh;
1020 open $fh, '>', $hunkfile
1021 or die sprintf(__("failed to open hunk edit file for writing: %s"), $!);
1022 print $fh Git::comment_lines __("Manual hunk edit mode -- see bottom for a quick guide.\n");
1023 print $fh @$oldtext;
1024 my $is_reverse = $patch_mode_flavour{IS_REVERSE};
1025 my ($remove_plus, $remove_minus) = $is_reverse ? ('-', '+') : ('+', '-');
1026 my $comment_line_char = Git::get_comment_line_char;
1027 print $fh Git::comment_lines sprintf(__ <<EOF, $remove_minus, $remove_plus, $comment_line_char),
1028---
1029To remove '%s' lines, make them ' ' lines (context).
1030To remove '%s' lines, delete them.
1031Lines starting with %s will be removed.
1032EOF
1033__($edit_hunk_manually_modes{$patch_mode}),
1034# TRANSLATORS: 'it' refers to the patch mentioned in the previous messages.
1035__ <<EOF2 ;
1036If it does not apply cleanly, you will be given an opportunity to
1037edit again. If all lines of the hunk are removed, then the edit is
1038aborted and the hunk is left unchanged.
1039EOF2
1040 close $fh;
1041
1042 chomp(my $editor = run_cmd_pipe(qw(git var GIT_EDITOR)));
1043 system('sh', '-c', $editor.' "$@"', $editor, $hunkfile);
1044
1045 if ($? != 0) {
1046 return undef;
1047 }
1048
1049 open $fh, '<', $hunkfile
1050 or die sprintf(__("failed to open hunk edit file for reading: %s"), $!);
1051 my @newtext = grep { !/^\Q$comment_line_char\E/ } <$fh>;
1052 close $fh;
1053 unlink $hunkfile;
1054
1055 # Abort if nothing remains
1056 if (!grep { /\S/ } @newtext) {
1057 return undef;
1058 }
1059
1060 # Reinsert the first hunk header if the user accidentally deleted it
1061 if ($newtext[0] !~ /^@/) {
1062 unshift @newtext, $oldtext->[0];
1063 }
1064 return \@newtext;
1065}
1066
1067sub diff_applies {
1068 return run_git_apply($patch_mode_flavour{APPLY_CHECK} . ' --check',
1069 map { @{$_->{TEXT}} } @_);
1070}
1071
1072sub _restore_terminal_and_die {
1073 ReadMode 'restore';
1074 print "\n";
1075 exit 1;
1076}
1077
1078sub prompt_single_character {
1079 if ($use_readkey) {
1080 local $SIG{TERM} = \&_restore_terminal_and_die;
1081 local $SIG{INT} = \&_restore_terminal_and_die;
1082 ReadMode 'cbreak';
1083 my $key = ReadKey 0;
1084 ReadMode 'restore';
1085 if ($use_termcap and $key eq "\e") {
1086 while (!defined $term_escapes{$key}) {
1087 my $next = ReadKey 0.5;
1088 last if (!defined $next);
1089 $key .= $next;
1090 }
1091 $key =~ s/\e/^[/;
1092 }
1093 print "$key" if defined $key;
1094 print "\n";
1095 return $key;
1096 } else {
1097 return <STDIN>;
1098 }
1099}
1100
1101sub prompt_yesno {
1102 my ($prompt) = @_;
1103 while (1) {
1104 print colored $prompt_color, $prompt;
1105 my $line = prompt_single_character;
1106 return undef unless defined $line;
1107 return 0 if $line =~ /^n/i;
1108 return 1 if $line =~ /^y/i;
1109 }
1110}
1111
1112sub edit_hunk_loop {
1113 my ($head, $hunk, $ix) = @_;
1114 my $text = $hunk->[$ix]->{TEXT};
1115
1116 while (1) {
1117 $text = edit_hunk_manually($text);
1118 if (!defined $text) {
1119 return undef;
1120 }
1121 my $newhunk = {
1122 TEXT => $text,
1123 TYPE => $hunk->[$ix]->{TYPE},
1124 USE => 1,
1125 DIRTY => 1,
1126 };
1127 if (diff_applies($head,
1128 @{$hunk}[0..$ix-1],
1129 $newhunk,
1130 @{$hunk}[$ix+1..$#{$hunk}])) {
1131 $newhunk->{DISPLAY} = [color_diff(@{$text})];
1132 return $newhunk;
1133 }
1134 else {
1135 prompt_yesno(
1136 # TRANSLATORS: do not translate [y/n]
1137 # The program will only accept that input
1138 # at this point.
1139 # Consider translating (saying "no" discards!) as
1140 # (saying "n" for "no" discards!) if the translation
1141 # of the word "no" does not start with n.
1142 __('Your edited hunk does not apply. Edit again '
1143 . '(saying "no" discards!) [y/n]? ')
1144 ) or return undef;
1145 }
1146 }
1147}
1148
1149my %help_patch_modes = (
1150 stage => N__(
1151"y - stage this hunk
1152n - do not stage this hunk
1153q - quit; do not stage this hunk or any of the remaining ones
1154a - stage this hunk and all later hunks in the file
1155d - do not stage this hunk or any of the later hunks in the file"),
1156 stash => N__(
1157"y - stash this hunk
1158n - do not stash this hunk
1159q - quit; do not stash this hunk or any of the remaining ones
1160a - stash this hunk and all later hunks in the file
1161d - do not stash this hunk or any of the later hunks in the file"),
1162 reset_head => N__(
1163"y - unstage this hunk
1164n - do not unstage this hunk
1165q - quit; do not unstage this hunk or any of the remaining ones
1166a - unstage this hunk and all later hunks in the file
1167d - do not unstage this hunk or any of the later hunks in the file"),
1168 reset_nothead => N__(
1169"y - apply this hunk to index
1170n - do not apply this hunk to index
1171q - quit; do not apply this hunk or any of the remaining ones
1172a - apply this hunk and all later hunks in the file
1173d - do not apply this hunk or any of the later hunks in the file"),
1174 checkout_index => N__(
1175"y - discard this hunk from worktree
1176n - do not discard this hunk from worktree
1177q - quit; do not discard this hunk or any of the remaining ones
1178a - discard this hunk and all later hunks in the file
1179d - do not discard this hunk or any of the later hunks in the file"),
1180 checkout_head => N__(
1181"y - discard this hunk from index and worktree
1182n - do not discard this hunk from index and worktree
1183q - quit; do not discard this hunk or any of the remaining ones
1184a - discard this hunk and all later hunks in the file
1185d - do not discard this hunk or any of the later hunks in the file"),
1186 checkout_nothead => N__(
1187"y - apply this hunk to index and worktree
1188n - do not apply this hunk to index and worktree
1189q - quit; do not apply this hunk or any of the remaining ones
1190a - apply this hunk and all later hunks in the file
1191d - do not apply this hunk or any of the later hunks in the file"),
1192);
1193
1194sub help_patch_cmd {
1195 print colored $help_color, __($help_patch_modes{$patch_mode}), "\n", __ <<EOF ;
1196g - select a hunk to go to
1197/ - search for a hunk matching the given regex
1198j - leave this hunk undecided, see next undecided hunk
1199J - leave this hunk undecided, see next hunk
1200k - leave this hunk undecided, see previous undecided hunk
1201K - leave this hunk undecided, see previous hunk
1202s - split the current hunk into smaller hunks
1203e - manually edit the current hunk
1204? - print help
1205EOF
1206}
1207
1208sub apply_patch {
1209 my $cmd = shift;
1210 my $ret = run_git_apply $cmd, @_;
1211 if (!$ret) {
1212 print STDERR @_;
1213 }
1214 return $ret;
1215}
1216
1217sub apply_patch_for_checkout_commit {
1218 my $reverse = shift;
1219 my $applies_index = run_git_apply 'apply '.$reverse.' --cached --check', @_;
1220 my $applies_worktree = run_git_apply 'apply '.$reverse.' --check', @_;
1221
1222 if ($applies_worktree && $applies_index) {
1223 run_git_apply 'apply '.$reverse.' --cached', @_;
1224 run_git_apply 'apply '.$reverse, @_;
1225 return 1;
1226 } elsif (!$applies_index) {
1227 print colored $error_color, __("The selected hunks do not apply to the index!\n");
1228 if (prompt_yesno __("Apply them to the worktree anyway? ")) {
1229 return run_git_apply 'apply '.$reverse, @_;
1230 } else {
1231 print colored $error_color, __("Nothing was applied.\n");
1232 return 0;
1233 }
1234 } else {
1235 print STDERR @_;
1236 return 0;
1237 }
1238}
1239
1240sub patch_update_cmd {
1241 my @all_mods = list_modified($patch_mode_flavour{FILTER});
1242 error_msg sprintf(__("ignoring unmerged: %s\n"), $_->{VALUE})
1243 for grep { $_->{UNMERGED} } @all_mods;
1244 @all_mods = grep { !$_->{UNMERGED} } @all_mods;
1245
1246 my @mods = grep { !($_->{BINARY}) } @all_mods;
1247 my @them;
1248
1249 if (!@mods) {
1250 if (@all_mods) {
1251 print STDERR __("Only binary files changed.\n");
1252 } else {
1253 print STDERR __("No changes.\n");
1254 }
1255 return 0;
1256 }
1257 if ($patch_mode_only) {
1258 @them = @mods;
1259 }
1260 else {
1261 @them = list_and_choose({ PROMPT => __('Patch update'),
1262 HEADER => $status_head, },
1263 @mods);
1264 }
1265 for (@them) {
1266 return 0 if patch_update_file($_->{VALUE});
1267 }
1268}
1269
1270# Generate a one line summary of a hunk.
1271sub summarize_hunk {
1272 my $rhunk = shift;
1273 my $summary = $rhunk->{TEXT}[0];
1274
1275 # Keep the line numbers, discard extra context.
1276 $summary =~ s/@@(.*?)@@.*/$1 /s;
1277 $summary .= " " x (20 - length $summary);
1278
1279 # Add some user context.
1280 for my $line (@{$rhunk->{TEXT}}) {
1281 if ($line =~ m/^[+-].*\w/) {
1282 $summary .= $line;
1283 last;
1284 }
1285 }
1286
1287 chomp $summary;
1288 return substr($summary, 0, 80) . "\n";
1289}
1290
1291
1292# Print a one-line summary of each hunk in the array ref in
1293# the first argument, starting with the index in the 2nd.
1294sub display_hunks {
1295 my ($hunks, $i) = @_;
1296 my $ctr = 0;
1297 $i ||= 0;
1298 for (; $i < @$hunks && $ctr < 20; $i++, $ctr++) {
1299 my $status = " ";
1300 if (defined $hunks->[$i]{USE}) {
1301 $status = $hunks->[$i]{USE} ? "+" : "-";
1302 }
1303 printf "%s%2d: %s",
1304 $status,
1305 $i + 1,
1306 summarize_hunk($hunks->[$i]);
1307 }
1308 return $i;
1309}
1310
1311my %patch_update_prompt_modes = (
1312 stage => {
1313 mode => N__("Stage mode change [y,n,q,a,d,/%s,?]? "),
1314 deletion => N__("Stage deletion [y,n,q,a,d,/%s,?]? "),
1315 hunk => N__("Stage this hunk [y,n,q,a,d,/%s,?]? "),
1316 },
1317 stash => {
1318 mode => N__("Stash mode change [y,n,q,a,d,/%s,?]? "),
1319 deletion => N__("Stash deletion [y,n,q,a,d,/%s,?]? "),
1320 hunk => N__("Stash this hunk [y,n,q,a,d,/%s,?]? "),
1321 },
1322 reset_head => {
1323 mode => N__("Unstage mode change [y,n,q,a,d,/%s,?]? "),
1324 deletion => N__("Unstage deletion [y,n,q,a,d,/%s,?]? "),
1325 hunk => N__("Unstage this hunk [y,n,q,a,d,/%s,?]? "),
1326 },
1327 reset_nothead => {
1328 mode => N__("Apply mode change to index [y,n,q,a,d,/%s,?]? "),
1329 deletion => N__("Apply deletion to index [y,n,q,a,d,/%s,?]? "),
1330 hunk => N__("Apply this hunk to index [y,n,q,a,d,/%s,?]? "),
1331 },
1332 checkout_index => {
1333 mode => N__("Discard mode change from worktree [y,n,q,a,d,/%s,?]? "),
1334 deletion => N__("Discard deletion from worktree [y,n,q,a,d,/%s,?]? "),
1335 hunk => N__("Discard this hunk from worktree [y,n,q,a,d,/%s,?]? "),
1336 },
1337 checkout_head => {
1338 mode => N__("Discard mode change from index and worktree [y,n,q,a,d,/%s,?]? "),
1339 deletion => N__("Discard deletion from index and worktree [y,n,q,a,d,/%s,?]? "),
1340 hunk => N__("Discard this hunk from index and worktree [y,n,q,a,d,/%s,?]? "),
1341 },
1342 checkout_nothead => {
1343 mode => N__("Apply mode change to index and worktree [y,n,q,a,d,/%s,?]? "),
1344 deletion => N__("Apply deletion to index and worktree [y,n,q,a,d,/%s,?]? "),
1345 hunk => N__("Apply this hunk to index and worktree [y,n,q,a,d,/%s,?]? "),
1346 },
1347);
1348
1349sub patch_update_file {
1350 my $quit = 0;
1351 my ($ix, $num);
1352 my $path = shift;
1353 my ($head, @hunk) = parse_diff($path);
1354 ($head, my $mode, my $deletion) = parse_diff_header($head);
1355 for (@{$head->{DISPLAY}}) {
1356 print;
1357 }
1358
1359 if (@{$mode->{TEXT}}) {
1360 unshift @hunk, $mode;
1361 }
1362 if (@{$deletion->{TEXT}}) {
1363 foreach my $hunk (@hunk) {
1364 push @{$deletion->{TEXT}}, @{$hunk->{TEXT}};
1365 push @{$deletion->{DISPLAY}}, @{$hunk->{DISPLAY}};
1366 }
1367 @hunk = ($deletion);
1368 }
1369
1370 $num = scalar @hunk;
1371 $ix = 0;
1372
1373 while (1) {
1374 my ($prev, $next, $other, $undecided, $i);
1375 $other = '';
1376
1377 if ($num <= $ix) {
1378 $ix = 0;
1379 }
1380 for ($i = 0; $i < $ix; $i++) {
1381 if (!defined $hunk[$i]{USE}) {
1382 $prev = 1;
1383 $other .= ',k';
1384 last;
1385 }
1386 }
1387 if ($ix) {
1388 $other .= ',K';
1389 }
1390 for ($i = $ix + 1; $i < $num; $i++) {
1391 if (!defined $hunk[$i]{USE}) {
1392 $next = 1;
1393 $other .= ',j';
1394 last;
1395 }
1396 }
1397 if ($ix < $num - 1) {
1398 $other .= ',J';
1399 }
1400 if ($num > 1) {
1401 $other .= ',g';
1402 }
1403 for ($i = 0; $i < $num; $i++) {
1404 if (!defined $hunk[$i]{USE}) {
1405 $undecided = 1;
1406 last;
1407 }
1408 }
1409 last if (!$undecided);
1410
1411 if ($hunk[$ix]{TYPE} eq 'hunk' &&
1412 hunk_splittable($hunk[$ix]{TEXT})) {
1413 $other .= ',s';
1414 }
1415 if ($hunk[$ix]{TYPE} eq 'hunk') {
1416 $other .= ',e';
1417 }
1418 for (@{$hunk[$ix]{DISPLAY}}) {
1419 print;
1420 }
1421 print colored $prompt_color,
1422 sprintf(__($patch_update_prompt_modes{$patch_mode}{$hunk[$ix]{TYPE}}), $other);
1423
1424 my $line = prompt_single_character;
1425 last unless defined $line;
1426 if ($line) {
1427 if ($line =~ /^y/i) {
1428 $hunk[$ix]{USE} = 1;
1429 }
1430 elsif ($line =~ /^n/i) {
1431 $hunk[$ix]{USE} = 0;
1432 }
1433 elsif ($line =~ /^a/i) {
1434 while ($ix < $num) {
1435 if (!defined $hunk[$ix]{USE}) {
1436 $hunk[$ix]{USE} = 1;
1437 }
1438 $ix++;
1439 }
1440 next;
1441 }
1442 elsif ($other =~ /g/ && $line =~ /^g(.*)/) {
1443 my $response = $1;
1444 my $no = $ix > 10 ? $ix - 10 : 0;
1445 while ($response eq '') {
1446 $no = display_hunks(\@hunk, $no);
1447 if ($no < $num) {
1448 print __("go to which hunk (<ret> to see more)? ");
1449 } else {
1450 print __("go to which hunk? ");
1451 }
1452 $response = <STDIN>;
1453 if (!defined $response) {
1454 $response = '';
1455 }
1456 chomp $response;
1457 }
1458 if ($response !~ /^\s*\d+\s*$/) {
1459 error_msg sprintf(__("Invalid number: '%s'\n"),
1460 $response);
1461 } elsif (0 < $response && $response <= $num) {
1462 $ix = $response - 1;
1463 } else {
1464 error_msg sprintf(__n("Sorry, only %d hunk available.\n",
1465 "Sorry, only %d hunks available.\n", $num), $num);
1466 }
1467 next;
1468 }
1469 elsif ($line =~ /^d/i) {
1470 while ($ix < $num) {
1471 if (!defined $hunk[$ix]{USE}) {
1472 $hunk[$ix]{USE} = 0;
1473 }
1474 $ix++;
1475 }
1476 next;
1477 }
1478 elsif ($line =~ /^q/i) {
1479 for ($i = 0; $i < $num; $i++) {
1480 if (!defined $hunk[$i]{USE}) {
1481 $hunk[$i]{USE} = 0;
1482 }
1483 }
1484 $quit = 1;
1485 last;
1486 }
1487 elsif ($line =~ m|^/(.*)|) {
1488 my $regex = $1;
1489 if ($1 eq "") {
1490 print colored $prompt_color, __("search for regex? ");
1491 $regex = <STDIN>;
1492 if (defined $regex) {
1493 chomp $regex;
1494 }
1495 }
1496 my $search_string;
1497 eval {
1498 $search_string = qr{$regex}m;
1499 };
1500 if ($@) {
1501 my ($err,$exp) = ($@, $1);
1502 $err =~ s/ at .*git-add--interactive line \d+, <STDIN> line \d+.*$//;
1503 error_msg sprintf(__("Malformed search regexp %s: %s\n"), $exp, $err);
1504 next;
1505 }
1506 my $iy = $ix;
1507 while (1) {
1508 my $text = join ("", @{$hunk[$iy]{TEXT}});
1509 last if ($text =~ $search_string);
1510 $iy++;
1511 $iy = 0 if ($iy >= $num);
1512 if ($ix == $iy) {
1513 error_msg __("No hunk matches the given pattern\n");
1514 last;
1515 }
1516 }
1517 $ix = $iy;
1518 next;
1519 }
1520 elsif ($line =~ /^K/) {
1521 if ($other =~ /K/) {
1522 $ix--;
1523 }
1524 else {
1525 error_msg __("No previous hunk\n");
1526 }
1527 next;
1528 }
1529 elsif ($line =~ /^J/) {
1530 if ($other =~ /J/) {
1531 $ix++;
1532 }
1533 else {
1534 error_msg __("No next hunk\n");
1535 }
1536 next;
1537 }
1538 elsif ($line =~ /^k/) {
1539 if ($other =~ /k/) {
1540 while (1) {
1541 $ix--;
1542 last if (!$ix ||
1543 !defined $hunk[$ix]{USE});
1544 }
1545 }
1546 else {
1547 error_msg __("No previous hunk\n");
1548 }
1549 next;
1550 }
1551 elsif ($line =~ /^j/) {
1552 if ($other !~ /j/) {
1553 error_msg __("No next hunk\n");
1554 next;
1555 }
1556 }
1557 elsif ($other =~ /s/ && $line =~ /^s/) {
1558 my @split = split_hunk($hunk[$ix]{TEXT}, $hunk[$ix]{DISPLAY});
1559 if (1 < @split) {
1560 print colored $header_color, sprintf(
1561 __n("Split into %d hunk.\n",
1562 "Split into %d hunks.\n",
1563 scalar(@split)), scalar(@split));
1564 }
1565 splice (@hunk, $ix, 1, @split);
1566 $num = scalar @hunk;
1567 next;
1568 }
1569 elsif ($other =~ /e/ && $line =~ /^e/) {
1570 my $newhunk = edit_hunk_loop($head, \@hunk, $ix);
1571 if (defined $newhunk) {
1572 splice @hunk, $ix, 1, $newhunk;
1573 }
1574 }
1575 else {
1576 help_patch_cmd($other);
1577 next;
1578 }
1579 # soft increment
1580 while (1) {
1581 $ix++;
1582 last if ($ix >= $num ||
1583 !defined $hunk[$ix]{USE});
1584 }
1585 }
1586 }
1587
1588 @hunk = coalesce_overlapping_hunks(@hunk);
1589
1590 my $n_lofs = 0;
1591 my @result = ();
1592 for (@hunk) {
1593 if ($_->{USE}) {
1594 push @result, @{$_->{TEXT}};
1595 }
1596 }
1597
1598 if (@result) {
1599 my @patch = reassemble_patch($head->{TEXT}, @result);
1600 my $apply_routine = $patch_mode_flavour{APPLY};
1601 &$apply_routine(@patch);
1602 refresh();
1603 }
1604
1605 print "\n";
1606 return $quit;
1607}
1608
1609sub diff_cmd {
1610 my @mods = list_modified('index-only');
1611 @mods = grep { !($_->{BINARY}) } @mods;
1612 return if (!@mods);
1613 my (@them) = list_and_choose({ PROMPT => __('Review diff'),
1614 IMMEDIATE => 1,
1615 HEADER => $status_head, },
1616 @mods);
1617 return if (!@them);
1618 my $reference = (is_initial_commit()) ? get_empty_tree() : 'HEAD';
1619 system(qw(git diff -p --cached), $reference, '--',
1620 map { $_->{VALUE} } @them);
1621}
1622
1623sub quit_cmd {
1624 print __("Bye.\n");
1625 exit(0);
1626}
1627
1628sub help_cmd {
1629# TRANSLATORS: please do not translate the command names
1630# 'status', 'update', 'revert', etc.
1631 print colored $help_color, __ <<'EOF' ;
1632status - show paths with changes
1633update - add working tree state to the staged set of changes
1634revert - revert staged set of changes back to the HEAD version
1635patch - pick hunks and update selectively
1636diff - view diff between HEAD and index
1637add untracked - add contents of untracked files to the staged set of changes
1638EOF
1639}
1640
1641sub process_args {
1642 return unless @ARGV;
1643 my $arg = shift @ARGV;
1644 if ($arg =~ /--patch(?:=(.*))?/) {
1645 if (defined $1) {
1646 if ($1 eq 'reset') {
1647 $patch_mode = 'reset_head';
1648 $patch_mode_revision = 'HEAD';
1649 $arg = shift @ARGV or die __("missing --");
1650 if ($arg ne '--') {
1651 $patch_mode_revision = $arg;
1652 $patch_mode = ($arg eq 'HEAD' ?
1653 'reset_head' : 'reset_nothead');
1654 $arg = shift @ARGV or die __("missing --");
1655 }
1656 } elsif ($1 eq 'checkout') {
1657 $arg = shift @ARGV or die __("missing --");
1658 if ($arg eq '--') {
1659 $patch_mode = 'checkout_index';
1660 } else {
1661 $patch_mode_revision = $arg;
1662 $patch_mode = ($arg eq 'HEAD' ?
1663 'checkout_head' : 'checkout_nothead');
1664 $arg = shift @ARGV or die __("missing --");
1665 }
1666 } elsif ($1 eq 'stage' or $1 eq 'stash') {
1667 $patch_mode = $1;
1668 $arg = shift @ARGV or die __("missing --");
1669 } else {
1670 die sprintf(__("unknown --patch mode: %s"), $1);
1671 }
1672 } else {
1673 $patch_mode = 'stage';
1674 $arg = shift @ARGV or die __("missing --");
1675 }
1676 die sprintf(__("invalid argument %s, expecting --"),
1677 $arg) unless $arg eq "--";
1678 %patch_mode_flavour = %{$patch_modes{$patch_mode}};
1679 $patch_mode_only = 1;
1680 }
1681 elsif ($arg ne "--") {
1682 die sprintf(__("invalid argument %s, expecting --"), $arg);
1683 }
1684}
1685
1686sub main_loop {
1687 my @cmd = ([ 'status', \&status_cmd, ],
1688 [ 'update', \&update_cmd, ],
1689 [ 'revert', \&revert_cmd, ],
1690 [ 'add untracked', \&add_untracked_cmd, ],
1691 [ 'patch', \&patch_update_cmd, ],
1692 [ 'diff', \&diff_cmd, ],
1693 [ 'quit', \&quit_cmd, ],
1694 [ 'help', \&help_cmd, ],
1695 );
1696 while (1) {
1697 my ($it) = list_and_choose({ PROMPT => __('What now'),
1698 SINGLETON => 1,
1699 LIST_FLAT => 4,
1700 HEADER => __('*** Commands ***'),
1701 ON_EOF => \&quit_cmd,
1702 IMMEDIATE => 1 }, @cmd);
1703 if ($it) {
1704 eval {
1705 $it->[1]->();
1706 };
1707 if ($@) {
1708 print "$@";
1709 }
1710 }
1711 }
1712}
1713
1714process_args();
1715refresh();
1716if ($patch_mode_only) {
1717 patch_update_cmd();
1718}
1719else {
1720 status_cmd();
1721 main_loop();
1722}