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