d14f48c8379cb287e2e1bbe9c8fb7946e6b160fe
   1#!/usr/bin/perl -w
   2
   3use strict;
   4use Git;
   5
   6binmode(STDOUT, ":raw");
   7
   8my $repo = Git->repository();
   9
  10my $menu_use_color = $repo->get_colorbool('color.interactive');
  11my ($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        ) : ();
  17my $error_color = ();
  18if ($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
  25my $diff_use_color = $repo->get_colorbool('color.diff');
  26my ($fraginfo_color) =
  27        $diff_use_color ? (
  28                $repo->get_color('color.diff.frag', 'cyan'),
  29        ) : ();
  30my ($diff_plain_color) =
  31        $diff_use_color ? (
  32                $repo->get_color('color.diff.plain', ''),
  33        ) : ();
  34my ($diff_old_color) =
  35        $diff_use_color ? (
  36                $repo->get_color('color.diff.old', 'red'),
  37        ) : ();
  38my ($diff_new_color) =
  39        $diff_use_color ? (
  40                $repo->get_color('color.diff.new', 'green'),
  41        ) : ();
  42
  43my $normal_color = $repo->get_color("", "reset");
  44
  45my $use_readkey = 0;
  46sub ReadMode;
  47sub ReadKey;
  48if ($repo->config_bool("interactive.singlekey")) {
  49        eval {
  50                require Term::ReadKey;
  51                Term::ReadKey->import;
  52                $use_readkey = 1;
  53        };
  54}
  55
  56sub 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
  74my $patch_mode;
  75my $patch_mode_revision;
  76
  77sub apply_patch;
  78
  79my %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
 109my %patch_mode_flavour = %{$patch_modes{stage}};
 110
 111sub 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
 124my ($GIT_DIR) = run_cmd_pipe(qw(git rev-parse --git-dir));
 125
 126if (!defined $GIT_DIR) {
 127        exit(1); # rev-parse would have already said "not a git repo"
 128}
 129chomp($GIT_DIR);
 130
 131my %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
 142sub 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
 172sub 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
 182sub list_untracked {
 183        map {
 184                chomp $_;
 185                unquote_path($_);
 186        }
 187        run_cmd_pipe(qw(git ls-files --others --exclude-standard --), @ARGV);
 188}
 189
 190my $status_fmt = '%12s %12s %s';
 191my $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
 202sub 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
 214sub 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
 312sub 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
 342sub 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)
 351sub 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()
 406sub 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)
 418sub 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
 437sub error_msg {
 438        print STDERR colored $error_color, @_;
 439}
 440
 441sub 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
 559sub singleton_prompt_help_cmd {
 560        print colored $help_color, <<\EOF ;
 561Prompt help:
 5621          - select a numbered item
 563foo        - select item based on unique prefix
 564           - (empty) select nothing
 565EOF
 566}
 567
 568sub prompt_help_cmd {
 569        print colored $help_color, <<\EOF ;
 570Prompt help:
 5711          - select a single item
 5723-5        - select a range of items
 5732-3,6-9    - select multiple ranges
 574foo        - select item based on unique prefix
 575-...       - unselect specified items
 576*          - choose all items
 577           - (empty) finish selecting
 578EOF
 579}
 580
 581sub status_cmd {
 582        list_and_choose({ LIST_ONLY => 1, HEADER => $status_head },
 583                        list_modified());
 584        print "\n";
 585}
 586
 587sub 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
 599sub 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
 614sub 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
 648sub 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
 658sub run_git_apply {
 659        my $cmd = shift;
 660        my $fh;
 661        open $fh, '| git ' . $cmd;
 662        print $fh @_;
 663        return close $fh;
 664}
 665
 666sub 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
 691sub 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
 706sub hunk_splittable {
 707        my ($text) = @_;
 708
 709        my @s = split_hunk($text);
 710        return (1 < @s);
 711}
 712
 713sub 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
 722sub 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
 824sub 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
 841sub 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
 891sub 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
 915sub 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
 925sub 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.
 945EOF
 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
 974sub diff_applies {
 975        my $fh;
 976        return run_git_apply($patch_mode_flavour{APPLY_CHECK} . ' --recount --check',
 977                             map { @{$_->{TEXT}} } @_);
 978}
 979
 980sub _restore_terminal_and_die {
 981        ReadMode 'restore';
 982        print "\n";
 983        exit 1;
 984}
 985
 986sub 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
1001sub 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
1011sub 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
1042sub help_patch_cmd {
1043        my $verb = lc $patch_mode_flavour{VERB};
1044        my $target = $patch_mode_flavour{TARGET};
1045        print colored $help_color, <<EOF ;
1046y - $verb this hunk$target
1047n - do not $verb this hunk$target
1048q - quit, do not $verb this hunk nor any of the remaining ones
1049a - $verb this and all the remaining hunks in the file
1050d - do not $verb this hunk nor any of the remaining hunks in the file
1051g - select a hunk to go to
1052/ - search for a hunk matching the given regex
1053j - leave this hunk undecided, see next undecided hunk
1054J - leave this hunk undecided, see next hunk
1055k - leave this hunk undecided, see previous undecided hunk
1056K - leave this hunk undecided, see previous hunk
1057s - split the current hunk into smaller hunks
1058e - manually edit the current hunk
1059? - print help
1060EOF
1061}
1062
1063sub 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
1072sub 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.
1099sub 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.
1122sub 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
1139sub 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
1390sub 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
1404sub quit_cmd {
1405        print "Bye.\n";
1406        exit(0);
1407}
1408
1409sub help_cmd {
1410        print colored $help_color, <<\EOF ;
1411status        - show paths with changes
1412update        - add working tree state to the staged set of changes
1413revert        - revert staged set of changes back to the HEAD version
1414patch         - pick hunks and update selectively
1415diff          - view diff between HEAD and index
1416add untracked - add contents of untracked files to the staged set of changes
1417EOF
1418}
1419
1420sub 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
1454sub 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
1482process_args();
1483refresh();
1484if ($patch_mode) {
1485        patch_update_cmd();
1486}
1487else {
1488        status_cmd();
1489        main_loop();
1490}