Implement 'git reset --patch'
[git] / git-add--interactive.perl
1 #!/usr/bin/perl -w
2
3 use strict;
4 use Git;
5
6 binmode(STDOUT, ":raw");
7
8 my $repo = Git->repository();
9
10 my $menu_use_color = $repo->get_colorbool('color.interactive');
11 my ($prompt_color, $header_color, $help_color) =
12         $menu_use_color ? (
13                 $repo->get_color('color.interactive.prompt', 'bold blue'),
14                 $repo->get_color('color.interactive.header', 'bold'),
15                 $repo->get_color('color.interactive.help', 'red bold'),
16         ) : ();
17 my $error_color = ();
18 if ($menu_use_color) {
19         my $help_color_spec = ($repo->config('color.interactive.help') or
20                                 'red bold');
21         $error_color = $repo->get_color('color.interactive.error',
22                                         $help_color_spec);
23 }
24
25 my $diff_use_color = $repo->get_colorbool('color.diff');
26 my ($fraginfo_color) =
27         $diff_use_color ? (
28                 $repo->get_color('color.diff.frag', 'cyan'),
29         ) : ();
30 my ($diff_plain_color) =
31         $diff_use_color ? (
32                 $repo->get_color('color.diff.plain', ''),
33         ) : ();
34 my ($diff_old_color) =
35         $diff_use_color ? (
36                 $repo->get_color('color.diff.old', 'red'),
37         ) : ();
38 my ($diff_new_color) =
39         $diff_use_color ? (
40                 $repo->get_color('color.diff.new', 'green'),
41         ) : ();
42
43 my $normal_color = $repo->get_color("", "reset");
44
45 my $use_readkey = 0;
46 sub ReadMode;
47 sub ReadKey;
48 if ($repo->config_bool("interactive.singlekey")) {
49         eval {
50                 require Term::ReadKey;
51                 Term::ReadKey->import;
52                 $use_readkey = 1;
53         };
54 }
55
56 sub colored {
57         my $color = shift;
58         my $string = join("", @_);
59
60         if (defined $color) {
61                 # Put a color code at the beginning of each line, a reset at the end
62                 # color after newlines that are not at the end of the string
63                 $string =~ s/(\n+)(.)/$1$color$2/g;
64                 # reset before newlines
65                 $string =~ s/(\n+)/$normal_color$1/g;
66                 # codes at beginning and end (if necessary):
67                 $string =~ s/^/$color/;
68                 $string =~ s/$/$normal_color/ unless $string =~ /\n$/;
69         }
70         return $string;
71 }
72
73 # command line options
74 my $patch_mode;
75 my $patch_mode_revision;
76
77 sub apply_patch;
78
79 my %patch_modes = (
80         'stage' => {
81                 DIFF => 'diff-files -p',
82                 APPLY => sub { apply_patch 'apply --cached', @_; },
83                 APPLY_CHECK => 'apply --cached',
84                 VERB => 'Stage',
85                 TARGET => '',
86                 PARTICIPLE => 'staging',
87                 FILTER => 'file-only',
88         },
89         'reset_head' => {
90                 DIFF => 'diff-index -p --cached',
91                 APPLY => sub { apply_patch 'apply -R --cached', @_; },
92                 APPLY_CHECK => 'apply -R --cached',
93                 VERB => 'Unstage',
94                 TARGET => '',
95                 PARTICIPLE => 'unstaging',
96                 FILTER => 'index-only',
97         },
98         'reset_nothead' => {
99                 DIFF => 'diff-index -R -p --cached',
100                 APPLY => sub { apply_patch 'apply --cached', @_; },
101                 APPLY_CHECK => 'apply --cached',
102                 VERB => 'Apply',
103                 TARGET => ' to index',
104                 PARTICIPLE => 'applying',
105                 FILTER => 'index-only',
106         },
107 );
108
109 my %patch_mode_flavour = %{$patch_modes{stage}};
110
111 sub run_cmd_pipe {
112         if ($^O eq 'MSWin32' || $^O eq 'msys') {
113                 my @invalid = grep {m/[":*]/} @_;
114                 die "$^O does not support: @invalid\n" if @invalid;
115                 my @args = map { m/ /o ? "\"$_\"": $_ } @_;
116                 return qx{@args};
117         } else {
118                 my $fh = undef;
119                 open($fh, '-|', @_) or die;
120                 return <$fh>;
121         }
122 }
123
124 my ($GIT_DIR) = run_cmd_pipe(qw(git rev-parse --git-dir));
125
126 if (!defined $GIT_DIR) {
127         exit(1); # rev-parse would have already said "not a git repo"
128 }
129 chomp($GIT_DIR);
130
131 my %cquote_map = (
132  "b" => chr(8),
133  "t" => chr(9),
134  "n" => chr(10),
135  "v" => chr(11),
136  "f" => chr(12),
137  "r" => chr(13),
138  "\\" => "\\",
139  "\042" => "\042",
140 );
141
142 sub unquote_path {
143         local ($_) = @_;
144         my ($retval, $remainder);
145         if (!/^\042(.*)\042$/) {
146                 return $_;
147         }
148         ($_, $retval) = ($1, "");
149         while (/^([^\\]*)\\(.*)$/) {
150                 $remainder = $2;
151                 $retval .= $1;
152                 for ($remainder) {
153                         if (/^([0-3][0-7][0-7])(.*)$/) {
154                                 $retval .= chr(oct($1));
155                                 $_ = $2;
156                                 last;
157                         }
158                         if (/^([\\\042btnvfr])(.*)$/) {
159                                 $retval .= $cquote_map{$1};
160                                 $_ = $2;
161                                 last;
162                         }
163                         # This is malformed -- just return it as-is for now.
164                         return $_[0];
165                 }
166                 $_ = $remainder;
167         }
168         $retval .= $_;
169         return $retval;
170 }
171
172 sub refresh {
173         my $fh;
174         open $fh, 'git update-index --refresh |'
175             or die;
176         while (<$fh>) {
177                 ;# ignore 'needs update'
178         }
179         close $fh;
180 }
181
182 sub list_untracked {
183         map {
184                 chomp $_;
185                 unquote_path($_);
186         }
187         run_cmd_pipe(qw(git ls-files --others --exclude-standard --), @ARGV);
188 }
189
190 my $status_fmt = '%12s %12s %s';
191 my $status_head = sprintf($status_fmt, 'staged', 'unstaged', 'path');
192
193 {
194         my $initial;
195         sub is_initial_commit {
196                 $initial = system('git rev-parse HEAD -- >/dev/null 2>&1') != 0
197                         unless defined $initial;
198                 return $initial;
199         }
200 }
201
202 sub get_empty_tree {
203         return '4b825dc642cb6eb9a060e54bf8d69288fbee4904';
204 }
205
206 # Returns list of hashes, contents of each of which are:
207 # VALUE:        pathname
208 # BINARY:       is a binary path
209 # INDEX:        is index different from HEAD?
210 # FILE:         is file different from index?
211 # INDEX_ADDDEL: is it add/delete between HEAD and index?
212 # FILE_ADDDEL:  is it add/delete between index and file?
213
214 sub list_modified {
215         my ($only) = @_;
216         my (%data, @return);
217         my ($add, $del, $adddel, $file);
218         my @tracked = ();
219
220         if (@ARGV) {
221                 @tracked = map {
222                         chomp $_;
223                         unquote_path($_);
224                 } run_cmd_pipe(qw(git ls-files --exclude-standard --), @ARGV);
225                 return if (!@tracked);
226         }
227
228         my $reference;
229         if (defined $patch_mode_revision and $patch_mode_revision ne 'HEAD') {
230                 $reference = $patch_mode_revision;
231         } elsif (is_initial_commit()) {
232                 $reference = get_empty_tree();
233         } else {
234                 $reference = 'HEAD';
235         }
236         for (run_cmd_pipe(qw(git diff-index --cached
237                              --numstat --summary), $reference,
238                              '--', @tracked)) {
239                 if (($add, $del, $file) =
240                     /^([-\d]+)  ([-\d]+)        (.*)/) {
241                         my ($change, $bin);
242                         $file = unquote_path($file);
243                         if ($add eq '-' && $del eq '-') {
244                                 $change = 'binary';
245                                 $bin = 1;
246                         }
247                         else {
248                                 $change = "+$add/-$del";
249                         }
250                         $data{$file} = {
251                                 INDEX => $change,
252                                 BINARY => $bin,
253                                 FILE => 'nothing',
254                         }
255                 }
256                 elsif (($adddel, $file) =
257                        /^ (create|delete) mode [0-7]+ (.*)$/) {
258                         $file = unquote_path($file);
259                         $data{$file}{INDEX_ADDDEL} = $adddel;
260                 }
261         }
262
263         for (run_cmd_pipe(qw(git diff-files --numstat --summary --), @tracked)) {
264                 if (($add, $del, $file) =
265                     /^([-\d]+)  ([-\d]+)        (.*)/) {
266                         $file = unquote_path($file);
267                         if (!exists $data{$file}) {
268                                 $data{$file} = +{
269                                         INDEX => 'unchanged',
270                                         BINARY => 0,
271                                 };
272                         }
273                         my ($change, $bin);
274                         if ($add eq '-' && $del eq '-') {
275                                 $change = 'binary';
276                                 $bin = 1;
277                         }
278                         else {
279                                 $change = "+$add/-$del";
280                         }
281                         $data{$file}{FILE} = $change;
282                         if ($bin) {
283                                 $data{$file}{BINARY} = 1;
284                         }
285                 }
286                 elsif (($adddel, $file) =
287                        /^ (create|delete) mode [0-7]+ (.*)$/) {
288                         $file = unquote_path($file);
289                         $data{$file}{FILE_ADDDEL} = $adddel;
290                 }
291         }
292
293         for (sort keys %data) {
294                 my $it = $data{$_};
295
296                 if ($only) {
297                         if ($only eq 'index-only') {
298                                 next if ($it->{INDEX} eq 'unchanged');
299                         }
300                         if ($only eq 'file-only') {
301                                 next if ($it->{FILE} eq 'nothing');
302                         }
303                 }
304                 push @return, +{
305                         VALUE => $_,
306                         %$it,
307                 };
308         }
309         return @return;
310 }
311
312 sub find_unique {
313         my ($string, @stuff) = @_;
314         my $found = undef;
315         for (my $i = 0; $i < @stuff; $i++) {
316                 my $it = $stuff[$i];
317                 my $hit = undef;
318                 if (ref $it) {
319                         if ((ref $it) eq 'ARRAY') {
320                                 $it = $it->[0];
321                         }
322                         else {
323                                 $it = $it->{VALUE};
324                         }
325                 }
326                 eval {
327                         if ($it =~ /^$string/) {
328                                 $hit = 1;
329                         };
330                 };
331                 if (defined $hit && defined $found) {
332                         return undef;
333                 }
334                 if ($hit) {
335                         $found = $i + 1;
336                 }
337         }
338         return $found;
339 }
340
341 # inserts string into trie and updates count for each character
342 sub update_trie {
343         my ($trie, $string) = @_;
344         foreach (split //, $string) {
345                 $trie = $trie->{$_} ||= {COUNT => 0};
346                 $trie->{COUNT}++;
347         }
348 }
349
350 # returns an array of tuples (prefix, remainder)
351 sub find_unique_prefixes {
352         my @stuff = @_;
353         my @return = ();
354
355         # any single prefix exceeding the soft limit is omitted
356         # if any prefix exceeds the hard limit all are omitted
357         # 0 indicates no limit
358         my $soft_limit = 0;
359         my $hard_limit = 3;
360
361         # build a trie modelling all possible options
362         my %trie;
363         foreach my $print (@stuff) {
364                 if ((ref $print) eq 'ARRAY') {
365                         $print = $print->[0];
366                 }
367                 elsif ((ref $print) eq 'HASH') {
368                         $print = $print->{VALUE};
369                 }
370                 update_trie(\%trie, $print);
371                 push @return, $print;
372         }
373
374         # use the trie to find the unique prefixes
375         for (my $i = 0; $i < @return; $i++) {
376                 my $ret = $return[$i];
377                 my @letters = split //, $ret;
378                 my %search = %trie;
379                 my ($prefix, $remainder);
380                 my $j;
381                 for ($j = 0; $j < @letters; $j++) {
382                         my $letter = $letters[$j];
383                         if ($search{$letter}{COUNT} == 1) {
384                                 $prefix = substr $ret, 0, $j + 1;
385                                 $remainder = substr $ret, $j + 1;
386                                 last;
387                         }
388                         else {
389                                 my $prefix = substr $ret, 0, $j;
390                                 return ()
391                                     if ($hard_limit && $j + 1 > $hard_limit);
392                         }
393                         %search = %{$search{$letter}};
394                 }
395                 if (ord($letters[0]) > 127 ||
396                     ($soft_limit && $j + 1 > $soft_limit)) {
397                         $prefix = undef;
398                         $remainder = $ret;
399                 }
400                 $return[$i] = [$prefix, $remainder];
401         }
402         return @return;
403 }
404
405 # filters out prefixes which have special meaning to list_and_choose()
406 sub is_valid_prefix {
407         my $prefix = shift;
408         return (defined $prefix) &&
409             !($prefix =~ /[\s,]/) && # separators
410             !($prefix =~ /^-/) &&    # deselection
411             !($prefix =~ /^\d+/) &&  # selection
412             ($prefix ne '*') &&      # "all" wildcard
413             ($prefix ne '?');        # prompt help
414 }
415
416 # given a prefix/remainder tuple return a string with the prefix highlighted
417 # for now use square brackets; later might use ANSI colors (underline, bold)
418 sub highlight_prefix {
419         my $prefix = shift;
420         my $remainder = shift;
421
422         if (!defined $prefix) {
423                 return $remainder;
424         }
425
426         if (!is_valid_prefix($prefix)) {
427                 return "$prefix$remainder";
428         }
429
430         if (!$menu_use_color) {
431                 return "[$prefix]$remainder";
432         }
433
434         return "$prompt_color$prefix$normal_color$remainder";
435 }
436
437 sub error_msg {
438         print STDERR colored $error_color, @_;
439 }
440
441 sub list_and_choose {
442         my ($opts, @stuff) = @_;
443         my (@chosen, @return);
444         my $i;
445         my @prefixes = find_unique_prefixes(@stuff) unless $opts->{LIST_ONLY};
446
447       TOPLOOP:
448         while (1) {
449                 my $last_lf = 0;
450
451                 if ($opts->{HEADER}) {
452                         if (!$opts->{LIST_FLAT}) {
453                                 print "     ";
454                         }
455                         print colored $header_color, "$opts->{HEADER}\n";
456                 }
457                 for ($i = 0; $i < @stuff; $i++) {
458                         my $chosen = $chosen[$i] ? '*' : ' ';
459                         my $print = $stuff[$i];
460                         my $ref = ref $print;
461                         my $highlighted = highlight_prefix(@{$prefixes[$i]})
462                             if @prefixes;
463                         if ($ref eq 'ARRAY') {
464                                 $print = $highlighted || $print->[0];
465                         }
466                         elsif ($ref eq 'HASH') {
467                                 my $value = $highlighted || $print->{VALUE};
468                                 $print = sprintf($status_fmt,
469                                     $print->{INDEX},
470                                     $print->{FILE},
471                                     $value);
472                         }
473                         else {
474                                 $print = $highlighted || $print;
475                         }
476                         printf("%s%2d: %s", $chosen, $i+1, $print);
477                         if (($opts->{LIST_FLAT}) &&
478                             (($i + 1) % ($opts->{LIST_FLAT}))) {
479                                 print "\t";
480                                 $last_lf = 0;
481                         }
482                         else {
483                                 print "\n";
484                                 $last_lf = 1;
485                         }
486                 }
487                 if (!$last_lf) {
488                         print "\n";
489                 }
490
491                 return if ($opts->{LIST_ONLY});
492
493                 print colored $prompt_color, $opts->{PROMPT};
494                 if ($opts->{SINGLETON}) {
495                         print "> ";
496                 }
497                 else {
498                         print ">> ";
499                 }
500                 my $line = <STDIN>;
501                 if (!$line) {
502                         print "\n";
503                         $opts->{ON_EOF}->() if $opts->{ON_EOF};
504                         last;
505                 }
506                 chomp $line;
507                 last if $line eq '';
508                 if ($line eq '?') {
509                         $opts->{SINGLETON} ?
510                             singleton_prompt_help_cmd() :
511                             prompt_help_cmd();
512                         next TOPLOOP;
513                 }
514                 for my $choice (split(/[\s,]+/, $line)) {
515                         my $choose = 1;
516                         my ($bottom, $top);
517
518                         # Input that begins with '-'; unchoose
519                         if ($choice =~ s/^-//) {
520                                 $choose = 0;
521                         }
522                         # A range can be specified like 5-7 or 5-.
523                         if ($choice =~ /^(\d+)-(\d*)$/) {
524                                 ($bottom, $top) = ($1, length($2) ? $2 : 1 + @stuff);
525                         }
526                         elsif ($choice =~ /^\d+$/) {
527                                 $bottom = $top = $choice;
528                         }
529                         elsif ($choice eq '*') {
530                                 $bottom = 1;
531                                 $top = 1 + @stuff;
532                         }
533                         else {
534                                 $bottom = $top = find_unique($choice, @stuff);
535                                 if (!defined $bottom) {
536                                         error_msg "Huh ($choice)?\n";
537                                         next TOPLOOP;
538                                 }
539                         }
540                         if ($opts->{SINGLETON} && $bottom != $top) {
541                                 error_msg "Huh ($choice)?\n";
542                                 next TOPLOOP;
543                         }
544                         for ($i = $bottom-1; $i <= $top-1; $i++) {
545                                 next if (@stuff <= $i || $i < 0);
546                                 $chosen[$i] = $choose;
547                         }
548                 }
549                 last if ($opts->{IMMEDIATE} || $line eq '*');
550         }
551         for ($i = 0; $i < @stuff; $i++) {
552                 if ($chosen[$i]) {
553                         push @return, $stuff[$i];
554                 }
555         }
556         return @return;
557 }
558
559 sub singleton_prompt_help_cmd {
560         print colored $help_color, <<\EOF ;
561 Prompt help:
562 1          - select a numbered item
563 foo        - select item based on unique prefix
564            - (empty) select nothing
565 EOF
566 }
567
568 sub prompt_help_cmd {
569         print colored $help_color, <<\EOF ;
570 Prompt help:
571 1          - select a single item
572 3-5        - select a range of items
573 2-3,6-9    - select multiple ranges
574 foo        - select item based on unique prefix
575 -...       - unselect specified items
576 *          - choose all items
577            - (empty) finish selecting
578 EOF
579 }
580
581 sub status_cmd {
582         list_and_choose({ LIST_ONLY => 1, HEADER => $status_head },
583                         list_modified());
584         print "\n";
585 }
586
587 sub say_n_paths {
588         my $did = shift @_;
589         my $cnt = scalar @_;
590         print "$did ";
591         if (1 < $cnt) {
592                 print "$cnt paths\n";
593         }
594         else {
595                 print "one path\n";
596         }
597 }
598
599 sub update_cmd {
600         my @mods = list_modified('file-only');
601         return if (!@mods);
602
603         my @update = list_and_choose({ PROMPT => 'Update',
604                                        HEADER => $status_head, },
605                                      @mods);
606         if (@update) {
607                 system(qw(git update-index --add --remove --),
608                        map { $_->{VALUE} } @update);
609                 say_n_paths('updated', @update);
610         }
611         print "\n";
612 }
613
614 sub revert_cmd {
615         my @update = list_and_choose({ PROMPT => 'Revert',
616                                        HEADER => $status_head, },
617                                      list_modified());
618         if (@update) {
619                 if (is_initial_commit()) {
620                         system(qw(git rm --cached),
621                                 map { $_->{VALUE} } @update);
622                 }
623                 else {
624                         my @lines = run_cmd_pipe(qw(git ls-tree HEAD --),
625                                                  map { $_->{VALUE} } @update);
626                         my $fh;
627                         open $fh, '| git update-index --index-info'
628                             or die;
629                         for (@lines) {
630                                 print $fh $_;
631                         }
632                         close($fh);
633                         for (@update) {
634                                 if ($_->{INDEX_ADDDEL} &&
635                                     $_->{INDEX_ADDDEL} eq 'create') {
636                                         system(qw(git update-index --force-remove --),
637                                                $_->{VALUE});
638                                         print "note: $_->{VALUE} is untracked now.\n";
639                                 }
640                         }
641                 }
642                 refresh();
643                 say_n_paths('reverted', @update);
644         }
645         print "\n";
646 }
647
648 sub add_untracked_cmd {
649         my @add = list_and_choose({ PROMPT => 'Add untracked' },
650                                   list_untracked());
651         if (@add) {
652                 system(qw(git update-index --add --), @add);
653                 say_n_paths('added', @add);
654         }
655         print "\n";
656 }
657
658 sub run_git_apply {
659         my $cmd = shift;
660         my $fh;
661         open $fh, '| git ' . $cmd;
662         print $fh @_;
663         return close $fh;
664 }
665
666 sub parse_diff {
667         my ($path) = @_;
668         my @diff_cmd = split(" ", $patch_mode_flavour{DIFF});
669         if (defined $patch_mode_revision) {
670                 push @diff_cmd, $patch_mode_revision;
671         }
672         my @diff = run_cmd_pipe("git", @diff_cmd, "--", $path);
673         my @colored = ();
674         if ($diff_use_color) {
675                 @colored = run_cmd_pipe("git", @diff_cmd, qw(--color --), $path);
676         }
677         my (@hunk) = { TEXT => [], DISPLAY => [], TYPE => 'header' };
678
679         for (my $i = 0; $i < @diff; $i++) {
680                 if ($diff[$i] =~ /^@@ /) {
681                         push @hunk, { TEXT => [], DISPLAY => [],
682                                 TYPE => 'hunk' };
683                 }
684                 push @{$hunk[-1]{TEXT}}, $diff[$i];
685                 push @{$hunk[-1]{DISPLAY}},
686                         ($diff_use_color ? $colored[$i] : $diff[$i]);
687         }
688         return @hunk;
689 }
690
691 sub parse_diff_header {
692         my $src = shift;
693
694         my $head = { TEXT => [], DISPLAY => [], TYPE => 'header' };
695         my $mode = { TEXT => [], DISPLAY => [], TYPE => 'mode' };
696
697         for (my $i = 0; $i < @{$src->{TEXT}}; $i++) {
698                 my $dest = $src->{TEXT}->[$i] =~ /^(old|new) mode (\d+)$/ ?
699                         $mode : $head;
700                 push @{$dest->{TEXT}}, $src->{TEXT}->[$i];
701                 push @{$dest->{DISPLAY}}, $src->{DISPLAY}->[$i];
702         }
703         return ($head, $mode);
704 }
705
706 sub hunk_splittable {
707         my ($text) = @_;
708
709         my @s = split_hunk($text);
710         return (1 < @s);
711 }
712
713 sub parse_hunk_header {
714         my ($line) = @_;
715         my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
716             $line =~ /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
717         $o_cnt = 1 unless defined $o_cnt;
718         $n_cnt = 1 unless defined $n_cnt;
719         return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
720 }
721
722 sub split_hunk {
723         my ($text, $display) = @_;
724         my @split = ();
725         if (!defined $display) {
726                 $display = $text;
727         }
728         # If there are context lines in the middle of a hunk,
729         # it can be split, but we would need to take care of
730         # overlaps later.
731
732         my ($o_ofs, undef, $n_ofs) = parse_hunk_header($text->[0]);
733         my $hunk_start = 1;
734
735       OUTER:
736         while (1) {
737                 my $next_hunk_start = undef;
738                 my $i = $hunk_start - 1;
739                 my $this = +{
740                         TEXT => [],
741                         DISPLAY => [],
742                         TYPE => 'hunk',
743                         OLD => $o_ofs,
744                         NEW => $n_ofs,
745                         OCNT => 0,
746                         NCNT => 0,
747                         ADDDEL => 0,
748                         POSTCTX => 0,
749                         USE => undef,
750                 };
751
752                 while (++$i < @$text) {
753                         my $line = $text->[$i];
754                         my $display = $display->[$i];
755                         if ($line =~ /^ /) {
756                                 if ($this->{ADDDEL} &&
757                                     !defined $next_hunk_start) {
758                                         # We have seen leading context and
759                                         # adds/dels and then here is another
760                                         # context, which is trailing for this
761                                         # split hunk and leading for the next
762                                         # one.
763                                         $next_hunk_start = $i;
764                                 }
765                                 push @{$this->{TEXT}}, $line;
766                                 push @{$this->{DISPLAY}}, $display;
767                                 $this->{OCNT}++;
768                                 $this->{NCNT}++;
769                                 if (defined $next_hunk_start) {
770                                         $this->{POSTCTX}++;
771                                 }
772                                 next;
773                         }
774
775                         # add/del
776                         if (defined $next_hunk_start) {
777                                 # We are done with the current hunk and
778                                 # this is the first real change for the
779                                 # next split one.
780                                 $hunk_start = $next_hunk_start;
781                                 $o_ofs = $this->{OLD} + $this->{OCNT};
782                                 $n_ofs = $this->{NEW} + $this->{NCNT};
783                                 $o_ofs -= $this->{POSTCTX};
784                                 $n_ofs -= $this->{POSTCTX};
785                                 push @split, $this;
786                                 redo OUTER;
787                         }
788                         push @{$this->{TEXT}}, $line;
789                         push @{$this->{DISPLAY}}, $display;
790                         $this->{ADDDEL}++;
791                         if ($line =~ /^-/) {
792                                 $this->{OCNT}++;
793                         }
794                         else {
795                                 $this->{NCNT}++;
796                         }
797                 }
798
799                 push @split, $this;
800                 last;
801         }
802
803         for my $hunk (@split) {
804                 $o_ofs = $hunk->{OLD};
805                 $n_ofs = $hunk->{NEW};
806                 my $o_cnt = $hunk->{OCNT};
807                 my $n_cnt = $hunk->{NCNT};
808
809                 my $head = ("@@ -$o_ofs" .
810                             (($o_cnt != 1) ? ",$o_cnt" : '') .
811                             " +$n_ofs" .
812                             (($n_cnt != 1) ? ",$n_cnt" : '') .
813                             " @@\n");
814                 my $display_head = $head;
815                 unshift @{$hunk->{TEXT}}, $head;
816                 if ($diff_use_color) {
817                         $display_head = colored($fraginfo_color, $head);
818                 }
819                 unshift @{$hunk->{DISPLAY}}, $display_head;
820         }
821         return @split;
822 }
823
824 sub find_last_o_ctx {
825         my ($it) = @_;
826         my $text = $it->{TEXT};
827         my ($o_ofs, $o_cnt) = parse_hunk_header($text->[0]);
828         my $i = @{$text};
829         my $last_o_ctx = $o_ofs + $o_cnt;
830         while (0 < --$i) {
831                 my $line = $text->[$i];
832                 if ($line =~ /^ /) {
833                         $last_o_ctx--;
834                         next;
835                 }
836                 last;
837         }
838         return $last_o_ctx;
839 }
840
841 sub merge_hunk {
842         my ($prev, $this) = @_;
843         my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) =
844             parse_hunk_header($prev->{TEXT}[0]);
845         my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) =
846             parse_hunk_header($this->{TEXT}[0]);
847
848         my (@line, $i, $ofs, $o_cnt, $n_cnt);
849         $ofs = $o0_ofs;
850         $o_cnt = $n_cnt = 0;
851         for ($i = 1; $i < @{$prev->{TEXT}}; $i++) {
852                 my $line = $prev->{TEXT}[$i];
853                 if ($line =~ /^\+/) {
854                         $n_cnt++;
855                         push @line, $line;
856                         next;
857                 }
858
859                 last if ($o1_ofs <= $ofs);
860
861                 $o_cnt++;
862                 $ofs++;
863                 if ($line =~ /^ /) {
864                         $n_cnt++;
865                 }
866                 push @line, $line;
867         }
868
869         for ($i = 1; $i < @{$this->{TEXT}}; $i++) {
870                 my $line = $this->{TEXT}[$i];
871                 if ($line =~ /^\+/) {
872                         $n_cnt++;
873                         push @line, $line;
874                         next;
875                 }
876                 $ofs++;
877                 $o_cnt++;
878                 if ($line =~ /^ /) {
879                         $n_cnt++;
880                 }
881                 push @line, $line;
882         }
883         my $head = ("@@ -$o0_ofs" .
884                     (($o_cnt != 1) ? ",$o_cnt" : '') .
885                     " +$n0_ofs" .
886                     (($n_cnt != 1) ? ",$n_cnt" : '') .
887                     " @@\n");
888         @{$prev->{TEXT}} = ($head, @line);
889 }
890
891 sub coalesce_overlapping_hunks {
892         my (@in) = @_;
893         my @out = ();
894
895         my ($last_o_ctx, $last_was_dirty);
896
897         for (grep { $_->{USE} } @in) {
898                 my $text = $_->{TEXT};
899                 my ($o_ofs) = parse_hunk_header($text->[0]);
900                 if (defined $last_o_ctx &&
901                     $o_ofs <= $last_o_ctx &&
902                     !$_->{DIRTY} &&
903                     !$last_was_dirty) {
904                         merge_hunk($out[-1], $_);
905                 }
906                 else {
907                         push @out, $_;
908                 }
909                 $last_o_ctx = find_last_o_ctx($out[-1]);
910                 $last_was_dirty = $_->{DIRTY};
911         }
912         return @out;
913 }
914
915 sub color_diff {
916         return map {
917                 colored((/^@/  ? $fraginfo_color :
918                          /^\+/ ? $diff_new_color :
919                          /^-/  ? $diff_old_color :
920                          $diff_plain_color),
921                         $_);
922         } @_;
923 }
924
925 sub edit_hunk_manually {
926         my ($oldtext) = @_;
927
928         my $hunkfile = $repo->repo_path . "/addp-hunk-edit.diff";
929         my $fh;
930         open $fh, '>', $hunkfile
931                 or die "failed to open hunk edit file for writing: " . $!;
932         print $fh "# Manual hunk edit mode -- see bottom for a quick guide\n";
933         print $fh @$oldtext;
934         my $participle = $patch_mode_flavour{PARTICIPLE};
935         print $fh <<EOF;
936 # ---
937 # To remove '-' lines, make them ' ' lines (context).
938 # To remove '+' lines, delete them.
939 # Lines starting with # will be removed.
940 #
941 # If the patch applies cleanly, the edited hunk will immediately be
942 # marked for $participle. If it does not apply cleanly, you will be given
943 # an opportunity to edit again. If all lines of the hunk are removed,
944 # then the edit is aborted and the hunk is left unchanged.
945 EOF
946         close $fh;
947
948         my $editor = $ENV{GIT_EDITOR} || $repo->config("core.editor")
949                 || $ENV{VISUAL} || $ENV{EDITOR} || "vi";
950         system('sh', '-c', $editor.' "$@"', $editor, $hunkfile);
951
952         if ($? != 0) {
953                 return undef;
954         }
955
956         open $fh, '<', $hunkfile
957                 or die "failed to open hunk edit file for reading: " . $!;
958         my @newtext = grep { !/^#/ } <$fh>;
959         close $fh;
960         unlink $hunkfile;
961
962         # Abort if nothing remains
963         if (!grep { /\S/ } @newtext) {
964                 return undef;
965         }
966
967         # Reinsert the first hunk header if the user accidentally deleted it
968         if ($newtext[0] !~ /^@/) {
969                 unshift @newtext, $oldtext->[0];
970         }
971         return \@newtext;
972 }
973
974 sub diff_applies {
975         my $fh;
976         return run_git_apply($patch_mode_flavour{APPLY_CHECK} . ' --recount --check',
977                              map { @{$_->{TEXT}} } @_);
978 }
979
980 sub _restore_terminal_and_die {
981         ReadMode 'restore';
982         print "\n";
983         exit 1;
984 }
985
986 sub prompt_single_character {
987         if ($use_readkey) {
988                 local $SIG{TERM} = \&_restore_terminal_and_die;
989                 local $SIG{INT} = \&_restore_terminal_and_die;
990                 ReadMode 'cbreak';
991                 my $key = ReadKey 0;
992                 ReadMode 'restore';
993                 print "$key" if defined $key;
994                 print "\n";
995                 return $key;
996         } else {
997                 return <STDIN>;
998         }
999 }
1000
1001 sub prompt_yesno {
1002         my ($prompt) = @_;
1003         while (1) {
1004                 print colored $prompt_color, $prompt;
1005                 my $line = prompt_single_character;
1006                 return 0 if $line =~ /^n/i;
1007                 return 1 if $line =~ /^y/i;
1008         }
1009 }
1010
1011 sub edit_hunk_loop {
1012         my ($head, $hunk, $ix) = @_;
1013         my $text = $hunk->[$ix]->{TEXT};
1014
1015         while (1) {
1016                 $text = edit_hunk_manually($text);
1017                 if (!defined $text) {
1018                         return undef;
1019                 }
1020                 my $newhunk = {
1021                         TEXT => $text,
1022                         TYPE => $hunk->[$ix]->{TYPE},
1023                         USE => 1,
1024                         DIRTY => 1,
1025                 };
1026                 if (diff_applies($head,
1027                                  @{$hunk}[0..$ix-1],
1028                                  $newhunk,
1029                                  @{$hunk}[$ix+1..$#{$hunk}])) {
1030                         $newhunk->{DISPLAY} = [color_diff(@{$text})];
1031                         return $newhunk;
1032                 }
1033                 else {
1034                         prompt_yesno(
1035                                 'Your edited hunk does not apply. Edit again '
1036                                 . '(saying "no" discards!) [y/n]? '
1037                                 ) or return undef;
1038                 }
1039         }
1040 }
1041
1042 sub help_patch_cmd {
1043         my $verb = lc $patch_mode_flavour{VERB};
1044         my $target = $patch_mode_flavour{TARGET};
1045         print colored $help_color, <<EOF ;
1046 y - $verb this hunk$target
1047 n - do not $verb this hunk$target
1048 q - quit, do not $verb this hunk nor any of the remaining ones
1049 a - $verb this and all the remaining hunks in the file
1050 d - do not $verb this hunk nor any of the remaining hunks in the file
1051 g - select a hunk to go to
1052 / - search for a hunk matching the given regex
1053 j - leave this hunk undecided, see next undecided hunk
1054 J - leave this hunk undecided, see next hunk
1055 k - leave this hunk undecided, see previous undecided hunk
1056 K - leave this hunk undecided, see previous hunk
1057 s - split the current hunk into smaller hunks
1058 e - manually edit the current hunk
1059 ? - print help
1060 EOF
1061 }
1062
1063 sub apply_patch {
1064         my $cmd = shift;
1065         my $ret = run_git_apply $cmd . ' --recount', @_;
1066         if (!$ret) {
1067                 print STDERR @_;
1068         }
1069         return $ret;
1070 }
1071
1072 sub patch_update_cmd {
1073         my @all_mods = list_modified($patch_mode_flavour{FILTER});
1074         my @mods = grep { !($_->{BINARY}) } @all_mods;
1075         my @them;
1076
1077         if (!@mods) {
1078                 if (@all_mods) {
1079                         print STDERR "Only binary files changed.\n";
1080                 } else {
1081                         print STDERR "No changes.\n";
1082                 }
1083                 return 0;
1084         }
1085         if ($patch_mode) {
1086                 @them = @mods;
1087         }
1088         else {
1089                 @them = list_and_choose({ PROMPT => 'Patch update',
1090                                           HEADER => $status_head, },
1091                                         @mods);
1092         }
1093         for (@them) {
1094                 return 0 if patch_update_file($_->{VALUE});
1095         }
1096 }
1097
1098 # Generate a one line summary of a hunk.
1099 sub summarize_hunk {
1100         my $rhunk = shift;
1101         my $summary = $rhunk->{TEXT}[0];
1102
1103         # Keep the line numbers, discard extra context.
1104         $summary =~ s/@@(.*?)@@.*/$1 /s;
1105         $summary .= " " x (20 - length $summary);
1106
1107         # Add some user context.
1108         for my $line (@{$rhunk->{TEXT}}) {
1109                 if ($line =~ m/^[+-].*\w/) {
1110                         $summary .= $line;
1111                         last;
1112                 }
1113         }
1114
1115         chomp $summary;
1116         return substr($summary, 0, 80) . "\n";
1117 }
1118
1119
1120 # Print a one-line summary of each hunk in the array ref in
1121 # the first argument, starting wih the index in the 2nd.
1122 sub display_hunks {
1123         my ($hunks, $i) = @_;
1124         my $ctr = 0;
1125         $i ||= 0;
1126         for (; $i < @$hunks && $ctr < 20; $i++, $ctr++) {
1127                 my $status = " ";
1128                 if (defined $hunks->[$i]{USE}) {
1129                         $status = $hunks->[$i]{USE} ? "+" : "-";
1130                 }
1131                 printf "%s%2d: %s",
1132                         $status,
1133                         $i + 1,
1134                         summarize_hunk($hunks->[$i]);
1135         }
1136         return $i;
1137 }
1138
1139 sub patch_update_file {
1140         my $quit = 0;
1141         my ($ix, $num);
1142         my $path = shift;
1143         my ($head, @hunk) = parse_diff($path);
1144         ($head, my $mode) = parse_diff_header($head);
1145         for (@{$head->{DISPLAY}}) {
1146                 print;
1147         }
1148
1149         if (@{$mode->{TEXT}}) {
1150                 unshift @hunk, $mode;
1151         }
1152
1153         $num = scalar @hunk;
1154         $ix = 0;
1155
1156         while (1) {
1157                 my ($prev, $next, $other, $undecided, $i);
1158                 $other = '';
1159
1160                 if ($num <= $ix) {
1161                         $ix = 0;
1162                 }
1163                 for ($i = 0; $i < $ix; $i++) {
1164                         if (!defined $hunk[$i]{USE}) {
1165                                 $prev = 1;
1166                                 $other .= ',k';
1167                                 last;
1168                         }
1169                 }
1170                 if ($ix) {
1171                         $other .= ',K';
1172                 }
1173                 for ($i = $ix + 1; $i < $num; $i++) {
1174                         if (!defined $hunk[$i]{USE}) {
1175                                 $next = 1;
1176                                 $other .= ',j';
1177                                 last;
1178                         }
1179                 }
1180                 if ($ix < $num - 1) {
1181                         $other .= ',J';
1182                 }
1183                 if ($num > 1) {
1184                         $other .= ',g';
1185                 }
1186                 for ($i = 0; $i < $num; $i++) {
1187                         if (!defined $hunk[$i]{USE}) {
1188                                 $undecided = 1;
1189                                 last;
1190                         }
1191                 }
1192                 last if (!$undecided);
1193
1194                 if ($hunk[$ix]{TYPE} eq 'hunk' &&
1195                     hunk_splittable($hunk[$ix]{TEXT})) {
1196                         $other .= ',s';
1197                 }
1198                 if ($hunk[$ix]{TYPE} eq 'hunk') {
1199                         $other .= ',e';
1200                 }
1201                 for (@{$hunk[$ix]{DISPLAY}}) {
1202                         print;
1203                 }
1204                 print colored $prompt_color, $patch_mode_flavour{VERB},
1205                   ($hunk[$ix]{TYPE} eq 'mode' ? ' mode change' : ' this hunk'),
1206                   $patch_mode_flavour{TARGET},
1207                   " [y,n,q,a,d,/$other,?]? ";
1208                 my $line = prompt_single_character;
1209                 if ($line) {
1210                         if ($line =~ /^y/i) {
1211                                 $hunk[$ix]{USE} = 1;
1212                         }
1213                         elsif ($line =~ /^n/i) {
1214                                 $hunk[$ix]{USE} = 0;
1215                         }
1216                         elsif ($line =~ /^a/i) {
1217                                 while ($ix < $num) {
1218                                         if (!defined $hunk[$ix]{USE}) {
1219                                                 $hunk[$ix]{USE} = 1;
1220                                         }
1221                                         $ix++;
1222                                 }
1223                                 next;
1224                         }
1225                         elsif ($other =~ /g/ && $line =~ /^g(.*)/) {
1226                                 my $response = $1;
1227                                 my $no = $ix > 10 ? $ix - 10 : 0;
1228                                 while ($response eq '') {
1229                                         my $extra = "";
1230                                         $no = display_hunks(\@hunk, $no);
1231                                         if ($no < $num) {
1232                                                 $extra = " (<ret> to see more)";
1233                                         }
1234                                         print "go to which hunk$extra? ";
1235                                         $response = <STDIN>;
1236                                         if (!defined $response) {
1237                                                 $response = '';
1238                                         }
1239                                         chomp $response;
1240                                 }
1241                                 if ($response !~ /^\s*\d+\s*$/) {
1242                                         error_msg "Invalid number: '$response'\n";
1243                                 } elsif (0 < $response && $response <= $num) {
1244                                         $ix = $response - 1;
1245                                 } else {
1246                                         error_msg "Sorry, only $num hunks available.\n";
1247                                 }
1248                                 next;
1249                         }
1250                         elsif ($line =~ /^d/i) {
1251                                 while ($ix < $num) {
1252                                         if (!defined $hunk[$ix]{USE}) {
1253                                                 $hunk[$ix]{USE} = 0;
1254                                         }
1255                                         $ix++;
1256                                 }
1257                                 next;
1258                         }
1259                         elsif ($line =~ /^q/i) {
1260                                 while ($ix < $num) {
1261                                         if (!defined $hunk[$ix]{USE}) {
1262                                                 $hunk[$ix]{USE} = 0;
1263                                         }
1264                                         $ix++;
1265                                 }
1266                                 $quit = 1;
1267                                 next;
1268                         }
1269                         elsif ($line =~ m|^/(.*)|) {
1270                                 my $regex = $1;
1271                                 if ($1 eq "") {
1272                                         print colored $prompt_color, "search for regex? ";
1273                                         $regex = <STDIN>;
1274                                         if (defined $regex) {
1275                                                 chomp $regex;
1276                                         }
1277                                 }
1278                                 my $search_string;
1279                                 eval {
1280                                         $search_string = qr{$regex}m;
1281                                 };
1282                                 if ($@) {
1283                                         my ($err,$exp) = ($@, $1);
1284                                         $err =~ s/ at .*git-add--interactive line \d+, <STDIN> line \d+.*$//;
1285                                         error_msg "Malformed search regexp $exp: $err\n";
1286                                         next;
1287                                 }
1288                                 my $iy = $ix;
1289                                 while (1) {
1290                                         my $text = join ("", @{$hunk[$iy]{TEXT}});
1291                                         last if ($text =~ $search_string);
1292                                         $iy++;
1293                                         $iy = 0 if ($iy >= $num);
1294                                         if ($ix == $iy) {
1295                                                 error_msg "No hunk matches the given pattern\n";
1296                                                 last;
1297                                         }
1298                                 }
1299                                 $ix = $iy;
1300                                 next;
1301                         }
1302                         elsif ($line =~ /^K/) {
1303                                 if ($other =~ /K/) {
1304                                         $ix--;
1305                                 }
1306                                 else {
1307                                         error_msg "No previous hunk\n";
1308                                 }
1309                                 next;
1310                         }
1311                         elsif ($line =~ /^J/) {
1312                                 if ($other =~ /J/) {
1313                                         $ix++;
1314                                 }
1315                                 else {
1316                                         error_msg "No next hunk\n";
1317                                 }
1318                                 next;
1319                         }
1320                         elsif ($line =~ /^k/) {
1321                                 if ($other =~ /k/) {
1322                                         while (1) {
1323                                                 $ix--;
1324                                                 last if (!$ix ||
1325                                                          !defined $hunk[$ix]{USE});
1326                                         }
1327                                 }
1328                                 else {
1329                                         error_msg "No previous hunk\n";
1330                                 }
1331                                 next;
1332                         }
1333                         elsif ($line =~ /^j/) {
1334                                 if ($other !~ /j/) {
1335                                         error_msg "No next hunk\n";
1336                                         next;
1337                                 }
1338                         }
1339                         elsif ($other =~ /s/ && $line =~ /^s/) {
1340                                 my @split = split_hunk($hunk[$ix]{TEXT}, $hunk[$ix]{DISPLAY});
1341                                 if (1 < @split) {
1342                                         print colored $header_color, "Split into ",
1343                                         scalar(@split), " hunks.\n";
1344                                 }
1345                                 splice (@hunk, $ix, 1, @split);
1346                                 $num = scalar @hunk;
1347                                 next;
1348                         }
1349                         elsif ($other =~ /e/ && $line =~ /^e/) {
1350                                 my $newhunk = edit_hunk_loop($head, \@hunk, $ix);
1351                                 if (defined $newhunk) {
1352                                         splice @hunk, $ix, 1, $newhunk;
1353                                 }
1354                         }
1355                         else {
1356                                 help_patch_cmd($other);
1357                                 next;
1358                         }
1359                         # soft increment
1360                         while (1) {
1361                                 $ix++;
1362                                 last if ($ix >= $num ||
1363                                          !defined $hunk[$ix]{USE});
1364                         }
1365                 }
1366         }
1367
1368         @hunk = coalesce_overlapping_hunks(@hunk);
1369
1370         my $n_lofs = 0;
1371         my @result = ();
1372         for (@hunk) {
1373                 if ($_->{USE}) {
1374                         push @result, @{$_->{TEXT}};
1375                 }
1376         }
1377
1378         if (@result) {
1379                 my $fh;
1380                 my @patch = (@{$head->{TEXT}}, @result);
1381                 my $apply_routine = $patch_mode_flavour{APPLY};
1382                 &$apply_routine(@patch);
1383                 refresh();
1384         }
1385
1386         print "\n";
1387         return $quit;
1388 }
1389
1390 sub diff_cmd {
1391         my @mods = list_modified('index-only');
1392         @mods = grep { !($_->{BINARY}) } @mods;
1393         return if (!@mods);
1394         my (@them) = list_and_choose({ PROMPT => 'Review diff',
1395                                      IMMEDIATE => 1,
1396                                      HEADER => $status_head, },
1397                                    @mods);
1398         return if (!@them);
1399         my $reference = is_initial_commit() ? get_empty_tree() : 'HEAD';
1400         system(qw(git diff -p --cached), $reference, '--',
1401                 map { $_->{VALUE} } @them);
1402 }
1403
1404 sub quit_cmd {
1405         print "Bye.\n";
1406         exit(0);
1407 }
1408
1409 sub help_cmd {
1410         print colored $help_color, <<\EOF ;
1411 status        - show paths with changes
1412 update        - add working tree state to the staged set of changes
1413 revert        - revert staged set of changes back to the HEAD version
1414 patch         - pick hunks and update selectively
1415 diff          - view diff between HEAD and index
1416 add untracked - add contents of untracked files to the staged set of changes
1417 EOF
1418 }
1419
1420 sub process_args {
1421         return unless @ARGV;
1422         my $arg = shift @ARGV;
1423         if ($arg =~ /--patch(?:=(.*))?/) {
1424                 if (defined $1) {
1425                         if ($1 eq 'reset') {
1426                                 $patch_mode = 'reset_head';
1427                                 $patch_mode_revision = 'HEAD';
1428                                 $arg = shift @ARGV or die "missing --";
1429                                 if ($arg ne '--') {
1430                                         $patch_mode_revision = $arg;
1431                                         $patch_mode = ($arg eq 'HEAD' ?
1432                                                        'reset_head' : 'reset_nothead');
1433                                         $arg = shift @ARGV or die "missing --";
1434                                 }
1435                         } elsif ($1 eq 'stage') {
1436                                 $patch_mode = 'stage';
1437                                 $arg = shift @ARGV or die "missing --";
1438                         } else {
1439                                 die "unknown --patch mode: $1";
1440                         }
1441                 } else {
1442                         $patch_mode = 'stage';
1443                         $arg = shift @ARGV or die "missing --";
1444                 }
1445                 die "invalid argument $arg, expecting --"
1446                     unless $arg eq "--";
1447                 %patch_mode_flavour = %{$patch_modes{$patch_mode}};
1448         }
1449         elsif ($arg ne "--") {
1450                 die "invalid argument $arg, expecting --";
1451         }
1452 }
1453
1454 sub main_loop {
1455         my @cmd = ([ 'status', \&status_cmd, ],
1456                    [ 'update', \&update_cmd, ],
1457                    [ 'revert', \&revert_cmd, ],
1458                    [ 'add untracked', \&add_untracked_cmd, ],
1459                    [ 'patch', \&patch_update_cmd, ],
1460                    [ 'diff', \&diff_cmd, ],
1461                    [ 'quit', \&quit_cmd, ],
1462                    [ 'help', \&help_cmd, ],
1463         );
1464         while (1) {
1465                 my ($it) = list_and_choose({ PROMPT => 'What now',
1466                                              SINGLETON => 1,
1467                                              LIST_FLAT => 4,
1468                                              HEADER => '*** Commands ***',
1469                                              ON_EOF => \&quit_cmd,
1470                                              IMMEDIATE => 1 }, @cmd);
1471                 if ($it) {
1472                         eval {
1473                                 $it->[1]->();
1474                         };
1475                         if ($@) {
1476                                 print "$@";
1477                         }
1478                 }
1479         }
1480 }
1481
1482 process_args();
1483 refresh();
1484 if ($patch_mode) {
1485         patch_update_cmd();
1486 }
1487 else {
1488         status_cmd();
1489         main_loop();
1490 }