git-add--interactive.perlon commit add_cache_entry(): removal of file foo does not conflict with foo/bar (21cd8d0)
   1#!/usr/bin/perl -w
   2
   3use strict;
   4
   5sub run_cmd_pipe {
   6        my $fh = undef;
   7        open($fh, '-|', @_) or die;
   8        return <$fh>;
   9}
  10
  11my ($GIT_DIR) = run_cmd_pipe(qw(git rev-parse --git-dir));
  12
  13if (!defined $GIT_DIR) {
  14        exit(1); # rev-parse would have already said "not a git repo"
  15}
  16chomp($GIT_DIR);
  17
  18sub refresh {
  19        my $fh;
  20        open $fh, '-|', qw(git update-index --refresh)
  21            or die;
  22        while (<$fh>) {
  23                ;# ignore 'needs update'
  24        }
  25        close $fh;
  26}
  27
  28sub list_untracked {
  29        map {
  30                chomp $_;
  31                $_;
  32        }
  33        run_cmd_pipe(qw(git ls-files --others
  34                        --exclude-per-directory=.gitignore),
  35                     "--exclude-from=$GIT_DIR/info/exclude",
  36                     '--', @_);
  37}
  38
  39my $status_fmt = '%12s %12s %s';
  40my $status_head = sprintf($status_fmt, 'staged', 'unstaged', 'path');
  41
  42# Returns list of hashes, contents of each of which are:
  43# PRINT:        print message
  44# VALUE:        pathname
  45# BINARY:       is a binary path
  46# INDEX:        is index different from HEAD?
  47# FILE:         is file different from index?
  48# INDEX_ADDDEL: is it add/delete between HEAD and index?
  49# FILE_ADDDEL:  is it add/delete between index and file?
  50
  51sub list_modified {
  52        my ($only) = @_;
  53        my (%data, @return);
  54        my ($add, $del, $adddel, $file);
  55
  56        for (run_cmd_pipe(qw(git diff-index --cached
  57                             --numstat --summary HEAD))) {
  58                if (($add, $del, $file) =
  59                    /^([-\d]+)  ([-\d]+)        (.*)/) {
  60                        my ($change, $bin);
  61                        if ($add eq '-' && $del eq '-') {
  62                                $change = 'binary';
  63                                $bin = 1;
  64                        }
  65                        else {
  66                                $change = "+$add/-$del";
  67                        }
  68                        $data{$file} = {
  69                                INDEX => $change,
  70                                BINARY => $bin,
  71                                FILE => 'nothing',
  72                        }
  73                }
  74                elsif (($adddel, $file) =
  75                       /^ (create|delete) mode [0-7]+ (.*)$/) {
  76                        $data{$file}{INDEX_ADDDEL} = $adddel;
  77                }
  78        }
  79
  80        for (run_cmd_pipe(qw(git diff-files --numstat --summary))) {
  81                if (($add, $del, $file) =
  82                    /^([-\d]+)  ([-\d]+)        (.*)/) {
  83                        if (!exists $data{$file}) {
  84                                $data{$file} = +{
  85                                        INDEX => 'unchanged',
  86                                        BINARY => 0,
  87                                };
  88                        }
  89                        my ($change, $bin);
  90                        if ($add eq '-' && $del eq '-') {
  91                                $change = 'binary';
  92                                $bin = 1;
  93                        }
  94                        else {
  95                                $change = "+$add/-$del";
  96                        }
  97                        $data{$file}{FILE} = $change;
  98                        if ($bin) {
  99                                $data{$file}{BINARY} = 1;
 100                        }
 101                }
 102                elsif (($adddel, $file) =
 103                       /^ (create|delete) mode [0-7]+ (.*)$/) {
 104                        $data{$file}{FILE_ADDDEL} = $adddel;
 105                }
 106        }
 107
 108        for (sort keys %data) {
 109                my $it = $data{$_};
 110
 111                if ($only) {
 112                        if ($only eq 'index-only') {
 113                                next if ($it->{INDEX} eq 'unchanged');
 114                        }
 115                        if ($only eq 'file-only') {
 116                                next if ($it->{FILE} eq 'nothing');
 117                        }
 118                }
 119                push @return, +{
 120                        VALUE => $_,
 121                        PRINT => (sprintf $status_fmt,
 122                                  $it->{INDEX}, $it->{FILE}, $_),
 123                        %$it,
 124                };
 125        }
 126        return @return;
 127}
 128
 129sub find_unique {
 130        my ($string, @stuff) = @_;
 131        my $found = undef;
 132        for (my $i = 0; $i < @stuff; $i++) {
 133                my $it = $stuff[$i];
 134                my $hit = undef;
 135                if (ref $it) {
 136                        if ((ref $it) eq 'ARRAY') {
 137                                $it = $it->[0];
 138                        }
 139                        else {
 140                                $it = $it->{VALUE};
 141                        }
 142                }
 143                eval {
 144                        if ($it =~ /^$string/) {
 145                                $hit = 1;
 146                        };
 147                };
 148                if (defined $hit && defined $found) {
 149                        return undef;
 150                }
 151                if ($hit) {
 152                        $found = $i + 1;
 153                }
 154        }
 155        return $found;
 156}
 157
 158sub list_and_choose {
 159        my ($opts, @stuff) = @_;
 160        my (@chosen, @return);
 161        my $i;
 162
 163      TOPLOOP:
 164        while (1) {
 165                my $last_lf = 0;
 166
 167                if ($opts->{HEADER}) {
 168                        if (!$opts->{LIST_FLAT}) {
 169                                print "     ";
 170                        }
 171                        print "$opts->{HEADER}\n";
 172                }
 173                for ($i = 0; $i < @stuff; $i++) {
 174                        my $chosen = $chosen[$i] ? '*' : ' ';
 175                        my $print = $stuff[$i];
 176                        if (ref $print) {
 177                                if ((ref $print) eq 'ARRAY') {
 178                                        $print = $print->[0];
 179                                }
 180                                else {
 181                                        $print = $print->{PRINT};
 182                                }
 183                        }
 184                        printf("%s%2d: %s", $chosen, $i+1, $print);
 185                        if (($opts->{LIST_FLAT}) &&
 186                            (($i + 1) % ($opts->{LIST_FLAT}))) {
 187                                print "\t";
 188                                $last_lf = 0;
 189                        }
 190                        else {
 191                                print "\n";
 192                                $last_lf = 1;
 193                        }
 194                }
 195                if (!$last_lf) {
 196                        print "\n";
 197                }
 198
 199                return if ($opts->{LIST_ONLY});
 200
 201                print $opts->{PROMPT};
 202                if ($opts->{SINGLETON}) {
 203                        print "> ";
 204                }
 205                else {
 206                        print ">> ";
 207                }
 208                my $line = <STDIN>;
 209                last if (!$line);
 210                chomp $line;
 211                my $donesomething = 0;
 212                for my $choice (split(/[\s,]+/, $line)) {
 213                        my $choose = 1;
 214                        my ($bottom, $top);
 215
 216                        # Input that begins with '-'; unchoose
 217                        if ($choice =~ s/^-//) {
 218                                $choose = 0;
 219                        }
 220                        # A range can be specified like 5-7
 221                        if ($choice =~ /^(\d+)-(\d+)$/) {
 222                                ($bottom, $top) = ($1, $2);
 223                        }
 224                        elsif ($choice =~ /^\d+$/) {
 225                                $bottom = $top = $choice;
 226                        }
 227                        elsif ($choice eq '*') {
 228                                $bottom = 1;
 229                                $top = 1 + @stuff;
 230                        }
 231                        else {
 232                                $bottom = $top = find_unique($choice, @stuff);
 233                                if (!defined $bottom) {
 234                                        print "Huh ($choice)?\n";
 235                                        next TOPLOOP;
 236                                }
 237                        }
 238                        if ($opts->{SINGLETON} && $bottom != $top) {
 239                                print "Huh ($choice)?\n";
 240                                next TOPLOOP;
 241                        }
 242                        for ($i = $bottom-1; $i <= $top-1; $i++) {
 243                                next if (@stuff <= $i);
 244                                $chosen[$i] = $choose;
 245                                $donesomething++;
 246                        }
 247                }
 248                last if (!$donesomething || $opts->{IMMEDIATE});
 249        }
 250        for ($i = 0; $i < @stuff; $i++) {
 251                if ($chosen[$i]) {
 252                        push @return, $stuff[$i];
 253                }
 254        }
 255        return @return;
 256}
 257
 258sub status_cmd {
 259        list_and_choose({ LIST_ONLY => 1, HEADER => $status_head },
 260                        list_modified());
 261        print "\n";
 262}
 263
 264sub say_n_paths {
 265        my $did = shift @_;
 266        my $cnt = scalar @_;
 267        print "$did ";
 268        if (1 < $cnt) {
 269                print "$cnt paths\n";
 270        }
 271        else {
 272                print "one path\n";
 273        }
 274}
 275
 276sub update_cmd {
 277        my @mods = list_modified('file-only');
 278        return if (!@mods);
 279
 280        my @update = list_and_choose({ PROMPT => 'Update',
 281                                       HEADER => $status_head, },
 282                                     @mods);
 283        if (@update) {
 284                system(qw(git update-index --add --remove --),
 285                       map { $_->{VALUE} } @update);
 286                say_n_paths('updated', @update);
 287        }
 288        print "\n";
 289}
 290
 291sub revert_cmd {
 292        my @update = list_and_choose({ PROMPT => 'Revert',
 293                                       HEADER => $status_head, },
 294                                     list_modified());
 295        if (@update) {
 296                my @lines = run_cmd_pipe(qw(git ls-tree HEAD --),
 297                                         map { $_->{VALUE} } @update);
 298                my $fh;
 299                open $fh, '|-', qw(git update-index --index-info)
 300                    or die;
 301                for (@lines) {
 302                        print $fh $_;
 303                }
 304                close($fh);
 305                for (@update) {
 306                        if ($_->{INDEX_ADDDEL} &&
 307                            $_->{INDEX_ADDDEL} eq 'create') {
 308                                system(qw(git update-index --force-remove --),
 309                                       $_->{VALUE});
 310                                print "note: $_->{VALUE} is untracked now.\n";
 311                        }
 312                }
 313                refresh();
 314                say_n_paths('reverted', @update);
 315        }
 316        print "\n";
 317}
 318
 319sub add_untracked_cmd {
 320        my @add = list_and_choose({ PROMPT => 'Add untracked' },
 321                                  list_untracked());
 322        if (@add) {
 323                system(qw(git update-index --add --), @add);
 324                say_n_paths('added', @add);
 325        }
 326        print "\n";
 327}
 328
 329sub parse_diff {
 330        my ($path) = @_;
 331        my @diff = run_cmd_pipe(qw(git diff-files -p --), $path);
 332        my (@hunk) = { TEXT => [] };
 333
 334        for (@diff) {
 335                if (/^@@ /) {
 336                        push @hunk, { TEXT => [] };
 337                }
 338                push @{$hunk[-1]{TEXT}}, $_;
 339        }
 340        return @hunk;
 341}
 342
 343sub hunk_splittable {
 344        my ($text) = @_;
 345
 346        my @s = split_hunk($text);
 347        return (1 < @s);
 348}
 349
 350sub parse_hunk_header {
 351        my ($line) = @_;
 352        my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
 353            $line =~ /^@@ -(\d+)(?:,(\d+)) \+(\d+)(?:,(\d+)) @@/;
 354        return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
 355}
 356
 357sub split_hunk {
 358        my ($text) = @_;
 359        my @split = ();
 360
 361        # If there are context lines in the middle of a hunk,
 362        # it can be split, but we would need to take care of
 363        # overlaps later.
 364
 365        my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = parse_hunk_header($text->[0]);
 366        my $hunk_start = 1;
 367        my $next_hunk_start;
 368
 369      OUTER:
 370        while (1) {
 371                my $next_hunk_start = undef;
 372                my $i = $hunk_start - 1;
 373                my $this = +{
 374                        TEXT => [],
 375                        OLD => $o_ofs,
 376                        NEW => $n_ofs,
 377                        OCNT => 0,
 378                        NCNT => 0,
 379                        ADDDEL => 0,
 380                        POSTCTX => 0,
 381                };
 382
 383                while (++$i < @$text) {
 384                        my $line = $text->[$i];
 385                        if ($line =~ /^ /) {
 386                                if ($this->{ADDDEL} &&
 387                                    !defined $next_hunk_start) {
 388                                        # We have seen leading context and
 389                                        # adds/dels and then here is another
 390                                        # context, which is trailing for this
 391                                        # split hunk and leading for the next
 392                                        # one.
 393                                        $next_hunk_start = $i;
 394                                }
 395                                push @{$this->{TEXT}}, $line;
 396                                $this->{OCNT}++;
 397                                $this->{NCNT}++;
 398                                if (defined $next_hunk_start) {
 399                                        $this->{POSTCTX}++;
 400                                }
 401                                next;
 402                        }
 403
 404                        # add/del
 405                        if (defined $next_hunk_start) {
 406                                # We are done with the current hunk and
 407                                # this is the first real change for the
 408                                # next split one.
 409                                $hunk_start = $next_hunk_start;
 410                                $o_ofs = $this->{OLD} + $this->{OCNT};
 411                                $n_ofs = $this->{NEW} + $this->{NCNT};
 412                                $o_ofs -= $this->{POSTCTX};
 413                                $n_ofs -= $this->{POSTCTX};
 414                                push @split, $this;
 415                                redo OUTER;
 416                        }
 417                        push @{$this->{TEXT}}, $line;
 418                        $this->{ADDDEL}++;
 419                        if ($line =~ /^-/) {
 420                                $this->{OCNT}++;
 421                        }
 422                        else {
 423                                $this->{NCNT}++;
 424                        }
 425                }
 426
 427                push @split, $this;
 428                last;
 429        }
 430
 431        for my $hunk (@split) {
 432                $o_ofs = $hunk->{OLD};
 433                $n_ofs = $hunk->{NEW};
 434                $o_cnt = $hunk->{OCNT};
 435                $n_cnt = $hunk->{NCNT};
 436
 437                my $head = ("@@ -$o_ofs" .
 438                            (($o_cnt != 1) ? ",$o_cnt" : '') .
 439                            " +$n_ofs" .
 440                            (($n_cnt != 1) ? ",$n_cnt" : '') .
 441                            " @@\n");
 442                unshift @{$hunk->{TEXT}}, $head;
 443        }
 444        return map { $_->{TEXT} } @split;
 445}
 446
 447sub find_last_o_ctx {
 448        my ($it) = @_;
 449        my $text = $it->{TEXT};
 450        my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = parse_hunk_header($text->[0]);
 451        my $i = @{$text};
 452        my $last_o_ctx = $o_ofs + $o_cnt;
 453        while (0 < --$i) {
 454                my $line = $text->[$i];
 455                if ($line =~ /^ /) {
 456                        $last_o_ctx--;
 457                        next;
 458                }
 459                last;
 460        }
 461        return $last_o_ctx;
 462}
 463
 464sub merge_hunk {
 465        my ($prev, $this) = @_;
 466        my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) =
 467            parse_hunk_header($prev->{TEXT}[0]);
 468        my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) =
 469            parse_hunk_header($this->{TEXT}[0]);
 470
 471        my (@line, $i, $ofs, $o_cnt, $n_cnt);
 472        $ofs = $o0_ofs;
 473        $o_cnt = $n_cnt = 0;
 474        for ($i = 1; $i < @{$prev->{TEXT}}; $i++) {
 475                my $line = $prev->{TEXT}[$i];
 476                if ($line =~ /^\+/) {
 477                        $n_cnt++;
 478                        push @line, $line;
 479                        next;
 480                }
 481
 482                last if ($o1_ofs <= $ofs);
 483
 484                $o_cnt++;
 485                $ofs++;
 486                if ($line =~ /^ /) {
 487                        $n_cnt++;
 488                }
 489                push @line, $line;
 490        }
 491
 492        for ($i = 1; $i < @{$this->{TEXT}}; $i++) {
 493                my $line = $this->{TEXT}[$i];
 494                if ($line =~ /^\+/) {
 495                        $n_cnt++;
 496                        push @line, $line;
 497                        next;
 498                }
 499                $ofs++;
 500                $o_cnt++;
 501                if ($line =~ /^ /) {
 502                        $n_cnt++;
 503                }
 504                push @line, $line;
 505        }
 506        my $head = ("@@ -$o0_ofs" .
 507                    (($o_cnt != 1) ? ",$o_cnt" : '') .
 508                    " +$n0_ofs" .
 509                    (($n_cnt != 1) ? ",$n_cnt" : '') .
 510                    " @@\n");
 511        @{$prev->{TEXT}} = ($head, @line);
 512}
 513
 514sub coalesce_overlapping_hunks {
 515        my (@in) = @_;
 516        my @out = ();
 517
 518        my ($last_o_ctx);
 519
 520        for (grep { $_->{USE} } @in) {
 521                my $text = $_->{TEXT};
 522                my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
 523                    parse_hunk_header($text->[0]);
 524                if (defined $last_o_ctx &&
 525                    $o_ofs <= $last_o_ctx) {
 526                        merge_hunk($out[-1], $_);
 527                }
 528                else {
 529                        push @out, $_;
 530                }
 531                $last_o_ctx = find_last_o_ctx($out[-1]);
 532        }
 533        return @out;
 534}
 535
 536sub help_patch_cmd {
 537        print <<\EOF ;
 538y - stage this hunk
 539n - do not stage this hunk
 540a - stage this and all the remaining hunks
 541d - do not stage this hunk nor any of the remaining hunks
 542j - leave this hunk undecided, see next undecided hunk
 543J - leave this hunk undecided, see next hunk
 544k - leave this hunk undecided, see previous undecided hunk
 545K - leave this hunk undecided, see previous hunk
 546s - split the current hunk into smaller hunks
 547EOF
 548}
 549
 550sub patch_update_cmd {
 551        my @mods = list_modified('file-only');
 552        @mods = grep { !($_->{BINARY}) } @mods;
 553        return if (!@mods);
 554
 555        my ($it) = list_and_choose({ PROMPT => 'Patch update',
 556                                     SINGLETON => 1,
 557                                     IMMEDIATE => 1,
 558                                     HEADER => $status_head, },
 559                                   @mods);
 560        return if (!$it);
 561
 562        my ($ix, $num);
 563        my $path = $it->{VALUE};
 564        my ($head, @hunk) = parse_diff($path);
 565        for (@{$head->{TEXT}}) {
 566                print;
 567        }
 568        $num = scalar @hunk;
 569        $ix = 0;
 570
 571        while (1) {
 572                my ($prev, $next, $other, $undecided, $i);
 573                $other = '';
 574
 575                if ($num <= $ix) {
 576                        $ix = 0;
 577                }
 578                for ($i = 0; $i < $ix; $i++) {
 579                        if (!defined $hunk[$i]{USE}) {
 580                                $prev = 1;
 581                                $other .= '/k';
 582                                last;
 583                        }
 584                }
 585                if ($ix) {
 586                        $other .= '/K';
 587                }
 588                for ($i = $ix + 1; $i < $num; $i++) {
 589                        if (!defined $hunk[$i]{USE}) {
 590                                $next = 1;
 591                                $other .= '/j';
 592                                last;
 593                        }
 594                }
 595                if ($ix < $num - 1) {
 596                        $other .= '/J';
 597                }
 598                for ($i = 0; $i < $num; $i++) {
 599                        if (!defined $hunk[$i]{USE}) {
 600                                $undecided = 1;
 601                                last;
 602                        }
 603                }
 604                last if (!$undecided);
 605
 606                if (hunk_splittable($hunk[$ix]{TEXT})) {
 607                        $other .= '/s';
 608                }
 609                for (@{$hunk[$ix]{TEXT}}) {
 610                        print;
 611                }
 612                print "Stage this hunk [y/n/a/d$other/?]? ";
 613                my $line = <STDIN>;
 614                if ($line) {
 615                        if ($line =~ /^y/i) {
 616                                $hunk[$ix]{USE} = 1;
 617                        }
 618                        elsif ($line =~ /^n/i) {
 619                                $hunk[$ix]{USE} = 0;
 620                        }
 621                        elsif ($line =~ /^a/i) {
 622                                while ($ix < $num) {
 623                                        if (!defined $hunk[$ix]{USE}) {
 624                                                $hunk[$ix]{USE} = 1;
 625                                        }
 626                                        $ix++;
 627                                }
 628                                next;
 629                        }
 630                        elsif ($line =~ /^d/i) {
 631                                while ($ix < $num) {
 632                                        if (!defined $hunk[$ix]{USE}) {
 633                                                $hunk[$ix]{USE} = 0;
 634                                        }
 635                                        $ix++;
 636                                }
 637                                next;
 638                        }
 639                        elsif ($other =~ /K/ && $line =~ /^K/) {
 640                                $ix--;
 641                                next;
 642                        }
 643                        elsif ($other =~ /J/ && $line =~ /^J/) {
 644                                $ix++;
 645                                next;
 646                        }
 647                        elsif ($other =~ /k/ && $line =~ /^k/) {
 648                                while (1) {
 649                                        $ix--;
 650                                        last if (!$ix ||
 651                                                 !defined $hunk[$ix]{USE});
 652                                }
 653                                next;
 654                        }
 655                        elsif ($other =~ /j/ && $line =~ /^j/) {
 656                                while (1) {
 657                                        $ix++;
 658                                        last if ($ix >= $num ||
 659                                                 !defined $hunk[$ix]{USE});
 660                                }
 661                                next;
 662                        }
 663                        elsif ($other =~ /s/ && $line =~ /^s/) {
 664                                my @split = split_hunk($hunk[$ix]{TEXT});
 665                                if (1 < @split) {
 666                                        print "Split into ",
 667                                        scalar(@split), " hunks.\n";
 668                                }
 669                                splice(@hunk, $ix, 1,
 670                                       map { +{ TEXT => $_, USE => undef } }
 671                                       @split);
 672                                $num = scalar @hunk;
 673                                next;
 674                        }
 675                        else {
 676                                help_patch_cmd($other);
 677                                next;
 678                        }
 679                        # soft increment
 680                        while (1) {
 681                                $ix++;
 682                                last if ($ix >= $num ||
 683                                         !defined $hunk[$ix]{USE});
 684                        }
 685                }
 686        }
 687
 688        @hunk = coalesce_overlapping_hunks(@hunk);
 689
 690        my ($o_lofs, $n_lofs) = (0, 0);
 691        my @result = ();
 692        for (@hunk) {
 693                my $text = $_->{TEXT};
 694                my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
 695                    parse_hunk_header($text->[0]);
 696
 697                if (!$_->{USE}) {
 698                        if (!defined $o_cnt) { $o_cnt = 1; }
 699                        if (!defined $n_cnt) { $n_cnt = 1; }
 700
 701                        # We would have added ($n_cnt - $o_cnt) lines
 702                        # to the postimage if we were to use this hunk,
 703                        # but we didn't.  So the line number that the next
 704                        # hunk starts at would be shifted by that much.
 705                        $n_lofs -= ($n_cnt - $o_cnt);
 706                        next;
 707                }
 708                else {
 709                        if ($n_lofs) {
 710                                $n_ofs += $n_lofs;
 711                                $text->[0] = ("@@ -$o_ofs" .
 712                                              ((defined $o_cnt)
 713                                               ? ",$o_cnt" : '') .
 714                                              " +$n_ofs" .
 715                                              ((defined $n_cnt)
 716                                               ? ",$n_cnt" : '') .
 717                                              " @@\n");
 718                        }
 719                        for (@$text) {
 720                                push @result, $_;
 721                        }
 722                }
 723        }
 724
 725        if (@result) {
 726                my $fh;
 727
 728                open $fh, '|-', qw(git apply --cached);
 729                for (@{$head->{TEXT}}, @result) {
 730                        print $fh $_;
 731                }
 732                if (!close $fh) {
 733                        for (@{$head->{TEXT}}, @result) {
 734                                print STDERR $_;
 735                        }
 736                }
 737                refresh();
 738        }
 739
 740        print "\n";
 741}
 742
 743sub diff_cmd {
 744        my @mods = list_modified('index-only');
 745        @mods = grep { !($_->{BINARY}) } @mods;
 746        return if (!@mods);
 747        my (@them) = list_and_choose({ PROMPT => 'Review diff',
 748                                     IMMEDIATE => 1,
 749                                     HEADER => $status_head, },
 750                                   @mods);
 751        return if (!@them);
 752        system(qw(git diff-index -p --cached HEAD --),
 753               map { $_->{VALUE} } @them);
 754}
 755
 756sub quit_cmd {
 757        print "Bye.\n";
 758        exit(0);
 759}
 760
 761sub help_cmd {
 762        print <<\EOF ;
 763status        - show paths with changes
 764update        - add working tree state to the staged set of changes
 765revert        - revert staged set of changes back to the HEAD version
 766patch         - pick hunks and update selectively
 767diff          - view diff between HEAD and index
 768add untracked - add contents of untracked files to the staged set of changes
 769EOF
 770}
 771
 772sub main_loop {
 773        my @cmd = ([ 'status', \&status_cmd, ],
 774                   [ 'update', \&update_cmd, ],
 775                   [ 'revert', \&revert_cmd, ],
 776                   [ 'add untracked', \&add_untracked_cmd, ],
 777                   [ 'patch', \&patch_update_cmd, ],
 778                   [ 'diff', \&diff_cmd, ],
 779                   [ 'quit', \&quit_cmd, ],
 780                   [ 'help', \&help_cmd, ],
 781        );
 782        while (1) {
 783                my ($it) = list_and_choose({ PROMPT => 'What now',
 784                                             SINGLETON => 1,
 785                                             LIST_FLAT => 4,
 786                                             HEADER => '*** Commands ***',
 787                                             IMMEDIATE => 1 }, @cmd);
 788                if ($it) {
 789                        eval {
 790                                $it->[1]->();
 791                        };
 792                        if ($@) {
 793                                print "$@";
 794                        }
 795                }
 796        }
 797}
 798
 799my @z;
 800
 801refresh();
 802status_cmd();
 803main_loop();