git-add--interactive.perlon commit filter-branch: assume HEAD if no revision supplied (0f047f3)
   1#!/usr/bin/perl -w
   2
   3use strict;
   4use Git;
   5
   6my $repo = Git->repository();
   7
   8my $menu_use_color = $repo->get_colorbool('color.interactive');
   9my ($prompt_color, $header_color, $help_color) =
  10        $menu_use_color ? (
  11                $repo->get_color('color.interactive.prompt', 'bold blue'),
  12                $repo->get_color('color.interactive.header', 'bold'),
  13                $repo->get_color('color.interactive.help', 'red bold'),
  14        ) : ();
  15
  16my $diff_use_color = $repo->get_colorbool('color.diff');
  17my ($fraginfo_color) =
  18        $diff_use_color ? (
  19                $repo->get_color('color.diff.frag', 'cyan'),
  20        ) : ();
  21
  22my $normal_color = $repo->get_color("", "reset");
  23
  24sub colored {
  25        my $color = shift;
  26        my $string = join("", @_);
  27
  28        if (defined $color) {
  29                # Put a color code at the beginning of each line, a reset at the end
  30                # color after newlines that are not at the end of the string
  31                $string =~ s/(\n+)(.)/$1$color$2/g;
  32                # reset before newlines
  33                $string =~ s/(\n+)/$normal_color$1/g;
  34                # codes at beginning and end (if necessary):
  35                $string =~ s/^/$color/;
  36                $string =~ s/$/$normal_color/ unless $string =~ /\n$/;
  37        }
  38        return $string;
  39}
  40
  41# command line options
  42my $patch_mode;
  43
  44sub run_cmd_pipe {
  45        if ($^O eq 'MSWin32') {
  46                my @invalid = grep {m/[":*]/} @_;
  47                die "$^O does not support: @invalid\n" if @invalid;
  48                my @args = map { m/ /o ? "\"$_\"": $_ } @_;
  49                return qx{@args};
  50        } else {
  51                my $fh = undef;
  52                open($fh, '-|', @_) or die;
  53                return <$fh>;
  54        }
  55}
  56
  57my ($GIT_DIR) = run_cmd_pipe(qw(git rev-parse --git-dir));
  58
  59if (!defined $GIT_DIR) {
  60        exit(1); # rev-parse would have already said "not a git repo"
  61}
  62chomp($GIT_DIR);
  63
  64sub refresh {
  65        my $fh;
  66        open $fh, 'git update-index --refresh |'
  67            or die;
  68        while (<$fh>) {
  69                ;# ignore 'needs update'
  70        }
  71        close $fh;
  72}
  73
  74sub list_untracked {
  75        map {
  76                chomp $_;
  77                $_;
  78        }
  79        run_cmd_pipe(qw(git ls-files --others --exclude-standard --), @ARGV);
  80}
  81
  82my $status_fmt = '%12s %12s %s';
  83my $status_head = sprintf($status_fmt, 'staged', 'unstaged', 'path');
  84
  85# Returns list of hashes, contents of each of which are:
  86# VALUE:        pathname
  87# BINARY:       is a binary path
  88# INDEX:        is index different from HEAD?
  89# FILE:         is file different from index?
  90# INDEX_ADDDEL: is it add/delete between HEAD and index?
  91# FILE_ADDDEL:  is it add/delete between index and file?
  92
  93sub list_modified {
  94        my ($only) = @_;
  95        my (%data, @return);
  96        my ($add, $del, $adddel, $file);
  97        my @tracked = ();
  98
  99        if (@ARGV) {
 100                @tracked = map {
 101                        chomp $_; $_;
 102                } run_cmd_pipe(qw(git ls-files --exclude-standard --), @ARGV);
 103                return if (!@tracked);
 104        }
 105
 106        for (run_cmd_pipe(qw(git diff-index --cached
 107                             --numstat --summary HEAD --), @tracked)) {
 108                if (($add, $del, $file) =
 109                    /^([-\d]+)  ([-\d]+)        (.*)/) {
 110                        my ($change, $bin);
 111                        if ($add eq '-' && $del eq '-') {
 112                                $change = 'binary';
 113                                $bin = 1;
 114                        }
 115                        else {
 116                                $change = "+$add/-$del";
 117                        }
 118                        $data{$file} = {
 119                                INDEX => $change,
 120                                BINARY => $bin,
 121                                FILE => 'nothing',
 122                        }
 123                }
 124                elsif (($adddel, $file) =
 125                       /^ (create|delete) mode [0-7]+ (.*)$/) {
 126                        $data{$file}{INDEX_ADDDEL} = $adddel;
 127                }
 128        }
 129
 130        for (run_cmd_pipe(qw(git diff-files --numstat --summary --), @tracked)) {
 131                if (($add, $del, $file) =
 132                    /^([-\d]+)  ([-\d]+)        (.*)/) {
 133                        if (!exists $data{$file}) {
 134                                $data{$file} = +{
 135                                        INDEX => 'unchanged',
 136                                        BINARY => 0,
 137                                };
 138                        }
 139                        my ($change, $bin);
 140                        if ($add eq '-' && $del eq '-') {
 141                                $change = 'binary';
 142                                $bin = 1;
 143                        }
 144                        else {
 145                                $change = "+$add/-$del";
 146                        }
 147                        $data{$file}{FILE} = $change;
 148                        if ($bin) {
 149                                $data{$file}{BINARY} = 1;
 150                        }
 151                }
 152                elsif (($adddel, $file) =
 153                       /^ (create|delete) mode [0-7]+ (.*)$/) {
 154                        $data{$file}{FILE_ADDDEL} = $adddel;
 155                }
 156        }
 157
 158        for (sort keys %data) {
 159                my $it = $data{$_};
 160
 161                if ($only) {
 162                        if ($only eq 'index-only') {
 163                                next if ($it->{INDEX} eq 'unchanged');
 164                        }
 165                        if ($only eq 'file-only') {
 166                                next if ($it->{FILE} eq 'nothing');
 167                        }
 168                }
 169                push @return, +{
 170                        VALUE => $_,
 171                        %$it,
 172                };
 173        }
 174        return @return;
 175}
 176
 177sub find_unique {
 178        my ($string, @stuff) = @_;
 179        my $found = undef;
 180        for (my $i = 0; $i < @stuff; $i++) {
 181                my $it = $stuff[$i];
 182                my $hit = undef;
 183                if (ref $it) {
 184                        if ((ref $it) eq 'ARRAY') {
 185                                $it = $it->[0];
 186                        }
 187                        else {
 188                                $it = $it->{VALUE};
 189                        }
 190                }
 191                eval {
 192                        if ($it =~ /^$string/) {
 193                                $hit = 1;
 194                        };
 195                };
 196                if (defined $hit && defined $found) {
 197                        return undef;
 198                }
 199                if ($hit) {
 200                        $found = $i + 1;
 201                }
 202        }
 203        return $found;
 204}
 205
 206# inserts string into trie and updates count for each character
 207sub update_trie {
 208        my ($trie, $string) = @_;
 209        foreach (split //, $string) {
 210                $trie = $trie->{$_} ||= {COUNT => 0};
 211                $trie->{COUNT}++;
 212        }
 213}
 214
 215# returns an array of tuples (prefix, remainder)
 216sub find_unique_prefixes {
 217        my @stuff = @_;
 218        my @return = ();
 219
 220        # any single prefix exceeding the soft limit is omitted
 221        # if any prefix exceeds the hard limit all are omitted
 222        # 0 indicates no limit
 223        my $soft_limit = 0;
 224        my $hard_limit = 3;
 225
 226        # build a trie modelling all possible options
 227        my %trie;
 228        foreach my $print (@stuff) {
 229                if ((ref $print) eq 'ARRAY') {
 230                        $print = $print->[0];
 231                }
 232                elsif ((ref $print) eq 'HASH') {
 233                        $print = $print->{VALUE};
 234                }
 235                update_trie(\%trie, $print);
 236                push @return, $print;
 237        }
 238
 239        # use the trie to find the unique prefixes
 240        for (my $i = 0; $i < @return; $i++) {
 241                my $ret = $return[$i];
 242                my @letters = split //, $ret;
 243                my %search = %trie;
 244                my ($prefix, $remainder);
 245                my $j;
 246                for ($j = 0; $j < @letters; $j++) {
 247                        my $letter = $letters[$j];
 248                        if ($search{$letter}{COUNT} == 1) {
 249                                $prefix = substr $ret, 0, $j + 1;
 250                                $remainder = substr $ret, $j + 1;
 251                                last;
 252                        }
 253                        else {
 254                                my $prefix = substr $ret, 0, $j;
 255                                return ()
 256                                    if ($hard_limit && $j + 1 > $hard_limit);
 257                        }
 258                        %search = %{$search{$letter}};
 259                }
 260                if ($soft_limit && $j + 1 > $soft_limit) {
 261                        $prefix = undef;
 262                        $remainder = $ret;
 263                }
 264                $return[$i] = [$prefix, $remainder];
 265        }
 266        return @return;
 267}
 268
 269# filters out prefixes which have special meaning to list_and_choose()
 270sub is_valid_prefix {
 271        my $prefix = shift;
 272        return (defined $prefix) &&
 273            !($prefix =~ /[\s,]/) && # separators
 274            !($prefix =~ /^-/) &&    # deselection
 275            !($prefix =~ /^\d+/) &&  # selection
 276            ($prefix ne '*') &&      # "all" wildcard
 277            ($prefix ne '?');        # prompt help
 278}
 279
 280# given a prefix/remainder tuple return a string with the prefix highlighted
 281# for now use square brackets; later might use ANSI colors (underline, bold)
 282sub highlight_prefix {
 283        my $prefix = shift;
 284        my $remainder = shift;
 285
 286        if (!defined $prefix) {
 287                return $remainder;
 288        }
 289
 290        if (!is_valid_prefix($prefix)) {
 291                return "$prefix$remainder";
 292        }
 293
 294        if (!$menu_use_color) {
 295                return "[$prefix]$remainder";
 296        }
 297
 298        return "$prompt_color$prefix$normal_color$remainder";
 299}
 300
 301sub list_and_choose {
 302        my ($opts, @stuff) = @_;
 303        my (@chosen, @return);
 304        my $i;
 305        my @prefixes = find_unique_prefixes(@stuff) unless $opts->{LIST_ONLY};
 306
 307      TOPLOOP:
 308        while (1) {
 309                my $last_lf = 0;
 310
 311                if ($opts->{HEADER}) {
 312                        if (!$opts->{LIST_FLAT}) {
 313                                print "     ";
 314                        }
 315                        print colored $header_color, "$opts->{HEADER}\n";
 316                }
 317                for ($i = 0; $i < @stuff; $i++) {
 318                        my $chosen = $chosen[$i] ? '*' : ' ';
 319                        my $print = $stuff[$i];
 320                        my $ref = ref $print;
 321                        my $highlighted = highlight_prefix(@{$prefixes[$i]})
 322                            if @prefixes;
 323                        if ($ref eq 'ARRAY') {
 324                                $print = $highlighted || $print->[0];
 325                        }
 326                        elsif ($ref eq 'HASH') {
 327                                my $value = $highlighted || $print->{VALUE};
 328                                $print = sprintf($status_fmt,
 329                                    $print->{INDEX},
 330                                    $print->{FILE},
 331                                    $value);
 332                        }
 333                        else {
 334                                $print = $highlighted || $print;
 335                        }
 336                        printf("%s%2d: %s", $chosen, $i+1, $print);
 337                        if (($opts->{LIST_FLAT}) &&
 338                            (($i + 1) % ($opts->{LIST_FLAT}))) {
 339                                print "\t";
 340                                $last_lf = 0;
 341                        }
 342                        else {
 343                                print "\n";
 344                                $last_lf = 1;
 345                        }
 346                }
 347                if (!$last_lf) {
 348                        print "\n";
 349                }
 350
 351                return if ($opts->{LIST_ONLY});
 352
 353                print colored $prompt_color, $opts->{PROMPT};
 354                if ($opts->{SINGLETON}) {
 355                        print "> ";
 356                }
 357                else {
 358                        print ">> ";
 359                }
 360                my $line = <STDIN>;
 361                if (!$line) {
 362                        print "\n";
 363                        $opts->{ON_EOF}->() if $opts->{ON_EOF};
 364                        last;
 365                }
 366                chomp $line;
 367                last if $line eq '';
 368                if ($line eq '?') {
 369                        $opts->{SINGLETON} ?
 370                            singleton_prompt_help_cmd() :
 371                            prompt_help_cmd();
 372                        next TOPLOOP;
 373                }
 374                for my $choice (split(/[\s,]+/, $line)) {
 375                        my $choose = 1;
 376                        my ($bottom, $top);
 377
 378                        # Input that begins with '-'; unchoose
 379                        if ($choice =~ s/^-//) {
 380                                $choose = 0;
 381                        }
 382                        # A range can be specified like 5-7
 383                        if ($choice =~ /^(\d+)-(\d+)$/) {
 384                                ($bottom, $top) = ($1, $2);
 385                        }
 386                        elsif ($choice =~ /^\d+$/) {
 387                                $bottom = $top = $choice;
 388                        }
 389                        elsif ($choice eq '*') {
 390                                $bottom = 1;
 391                                $top = 1 + @stuff;
 392                        }
 393                        else {
 394                                $bottom = $top = find_unique($choice, @stuff);
 395                                if (!defined $bottom) {
 396                                        print "Huh ($choice)?\n";
 397                                        next TOPLOOP;
 398                                }
 399                        }
 400                        if ($opts->{SINGLETON} && $bottom != $top) {
 401                                print "Huh ($choice)?\n";
 402                                next TOPLOOP;
 403                        }
 404                        for ($i = $bottom-1; $i <= $top-1; $i++) {
 405                                next if (@stuff <= $i || $i < 0);
 406                                $chosen[$i] = $choose;
 407                        }
 408                }
 409                last if ($opts->{IMMEDIATE} || $line eq '*');
 410        }
 411        for ($i = 0; $i < @stuff; $i++) {
 412                if ($chosen[$i]) {
 413                        push @return, $stuff[$i];
 414                }
 415        }
 416        return @return;
 417}
 418
 419sub singleton_prompt_help_cmd {
 420        print colored $help_color, <<\EOF ;
 421Prompt help:
 4221          - select a numbered item
 423foo        - select item based on unique prefix
 424           - (empty) select nothing
 425EOF
 426}
 427
 428sub prompt_help_cmd {
 429        print colored $help_color, <<\EOF ;
 430Prompt help:
 4311          - select a single item
 4323-5        - select a range of items
 4332-3,6-9    - select multiple ranges
 434foo        - select item based on unique prefix
 435-...       - unselect specified items
 436*          - choose all items
 437           - (empty) finish selecting
 438EOF
 439}
 440
 441sub status_cmd {
 442        list_and_choose({ LIST_ONLY => 1, HEADER => $status_head },
 443                        list_modified());
 444        print "\n";
 445}
 446
 447sub say_n_paths {
 448        my $did = shift @_;
 449        my $cnt = scalar @_;
 450        print "$did ";
 451        if (1 < $cnt) {
 452                print "$cnt paths\n";
 453        }
 454        else {
 455                print "one path\n";
 456        }
 457}
 458
 459sub update_cmd {
 460        my @mods = list_modified('file-only');
 461        return if (!@mods);
 462
 463        my @update = list_and_choose({ PROMPT => 'Update',
 464                                       HEADER => $status_head, },
 465                                     @mods);
 466        if (@update) {
 467                system(qw(git update-index --add --remove --),
 468                       map { $_->{VALUE} } @update);
 469                say_n_paths('updated', @update);
 470        }
 471        print "\n";
 472}
 473
 474sub revert_cmd {
 475        my @update = list_and_choose({ PROMPT => 'Revert',
 476                                       HEADER => $status_head, },
 477                                     list_modified());
 478        if (@update) {
 479                my @lines = run_cmd_pipe(qw(git ls-tree HEAD --),
 480                                         map { $_->{VALUE} } @update);
 481                my $fh;
 482                open $fh, '| git update-index --index-info'
 483                    or die;
 484                for (@lines) {
 485                        print $fh $_;
 486                }
 487                close($fh);
 488                for (@update) {
 489                        if ($_->{INDEX_ADDDEL} &&
 490                            $_->{INDEX_ADDDEL} eq 'create') {
 491                                system(qw(git update-index --force-remove --),
 492                                       $_->{VALUE});
 493                                print "note: $_->{VALUE} is untracked now.\n";
 494                        }
 495                }
 496                refresh();
 497                say_n_paths('reverted', @update);
 498        }
 499        print "\n";
 500}
 501
 502sub add_untracked_cmd {
 503        my @add = list_and_choose({ PROMPT => 'Add untracked' },
 504                                  list_untracked());
 505        if (@add) {
 506                system(qw(git update-index --add --), @add);
 507                say_n_paths('added', @add);
 508        }
 509        print "\n";
 510}
 511
 512sub parse_diff {
 513        my ($path) = @_;
 514        my @diff = run_cmd_pipe(qw(git diff-files -p --), $path);
 515        my @colored = ();
 516        if ($diff_use_color) {
 517                @colored = run_cmd_pipe(qw(git diff-files -p --color --), $path);
 518        }
 519        my (@hunk) = { TEXT => [], DISPLAY => [] };
 520
 521        for (my $i = 0; $i < @diff; $i++) {
 522                if ($diff[$i] =~ /^@@ /) {
 523                        push @hunk, { TEXT => [], DISPLAY => [] };
 524                }
 525                push @{$hunk[-1]{TEXT}}, $diff[$i];
 526                push @{$hunk[-1]{DISPLAY}},
 527                        ($diff_use_color ? $colored[$i] : $diff[$i]);
 528        }
 529        return @hunk;
 530}
 531
 532sub hunk_splittable {
 533        my ($text) = @_;
 534
 535        my @s = split_hunk($text);
 536        return (1 < @s);
 537}
 538
 539sub parse_hunk_header {
 540        my ($line) = @_;
 541        my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
 542            $line =~ /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
 543        $o_cnt = 1 unless defined $o_cnt;
 544        $n_cnt = 1 unless defined $n_cnt;
 545        return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
 546}
 547
 548sub split_hunk {
 549        my ($text, $display) = @_;
 550        my @split = ();
 551        if (!defined $display) {
 552                $display = $text;
 553        }
 554        # If there are context lines in the middle of a hunk,
 555        # it can be split, but we would need to take care of
 556        # overlaps later.
 557
 558        my ($o_ofs, undef, $n_ofs) = parse_hunk_header($text->[0]);
 559        my $hunk_start = 1;
 560
 561      OUTER:
 562        while (1) {
 563                my $next_hunk_start = undef;
 564                my $i = $hunk_start - 1;
 565                my $this = +{
 566                        TEXT => [],
 567                        DISPLAY => [],
 568                        OLD => $o_ofs,
 569                        NEW => $n_ofs,
 570                        OCNT => 0,
 571                        NCNT => 0,
 572                        ADDDEL => 0,
 573                        POSTCTX => 0,
 574                        USE => undef,
 575                };
 576
 577                while (++$i < @$text) {
 578                        my $line = $text->[$i];
 579                        my $display = $display->[$i];
 580                        if ($line =~ /^ /) {
 581                                if ($this->{ADDDEL} &&
 582                                    !defined $next_hunk_start) {
 583                                        # We have seen leading context and
 584                                        # adds/dels and then here is another
 585                                        # context, which is trailing for this
 586                                        # split hunk and leading for the next
 587                                        # one.
 588                                        $next_hunk_start = $i;
 589                                }
 590                                push @{$this->{TEXT}}, $line;
 591                                push @{$this->{DISPLAY}}, $display;
 592                                $this->{OCNT}++;
 593                                $this->{NCNT}++;
 594                                if (defined $next_hunk_start) {
 595                                        $this->{POSTCTX}++;
 596                                }
 597                                next;
 598                        }
 599
 600                        # add/del
 601                        if (defined $next_hunk_start) {
 602                                # We are done with the current hunk and
 603                                # this is the first real change for the
 604                                # next split one.
 605                                $hunk_start = $next_hunk_start;
 606                                $o_ofs = $this->{OLD} + $this->{OCNT};
 607                                $n_ofs = $this->{NEW} + $this->{NCNT};
 608                                $o_ofs -= $this->{POSTCTX};
 609                                $n_ofs -= $this->{POSTCTX};
 610                                push @split, $this;
 611                                redo OUTER;
 612                        }
 613                        push @{$this->{TEXT}}, $line;
 614                        push @{$this->{DISPLAY}}, $display;
 615                        $this->{ADDDEL}++;
 616                        if ($line =~ /^-/) {
 617                                $this->{OCNT}++;
 618                        }
 619                        else {
 620                                $this->{NCNT}++;
 621                        }
 622                }
 623
 624                push @split, $this;
 625                last;
 626        }
 627
 628        for my $hunk (@split) {
 629                $o_ofs = $hunk->{OLD};
 630                $n_ofs = $hunk->{NEW};
 631                my $o_cnt = $hunk->{OCNT};
 632                my $n_cnt = $hunk->{NCNT};
 633
 634                my $head = ("@@ -$o_ofs" .
 635                            (($o_cnt != 1) ? ",$o_cnt" : '') .
 636                            " +$n_ofs" .
 637                            (($n_cnt != 1) ? ",$n_cnt" : '') .
 638                            " @@\n");
 639                my $display_head = $head;
 640                unshift @{$hunk->{TEXT}}, $head;
 641                if ($diff_use_color) {
 642                        $display_head = colored($fraginfo_color, $head);
 643                }
 644                unshift @{$hunk->{DISPLAY}}, $display_head;
 645        }
 646        return @split;
 647}
 648
 649sub find_last_o_ctx {
 650        my ($it) = @_;
 651        my $text = $it->{TEXT};
 652        my ($o_ofs, $o_cnt) = parse_hunk_header($text->[0]);
 653        my $i = @{$text};
 654        my $last_o_ctx = $o_ofs + $o_cnt;
 655        while (0 < --$i) {
 656                my $line = $text->[$i];
 657                if ($line =~ /^ /) {
 658                        $last_o_ctx--;
 659                        next;
 660                }
 661                last;
 662        }
 663        return $last_o_ctx;
 664}
 665
 666sub merge_hunk {
 667        my ($prev, $this) = @_;
 668        my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) =
 669            parse_hunk_header($prev->{TEXT}[0]);
 670        my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) =
 671            parse_hunk_header($this->{TEXT}[0]);
 672
 673        my (@line, $i, $ofs, $o_cnt, $n_cnt);
 674        $ofs = $o0_ofs;
 675        $o_cnt = $n_cnt = 0;
 676        for ($i = 1; $i < @{$prev->{TEXT}}; $i++) {
 677                my $line = $prev->{TEXT}[$i];
 678                if ($line =~ /^\+/) {
 679                        $n_cnt++;
 680                        push @line, $line;
 681                        next;
 682                }
 683
 684                last if ($o1_ofs <= $ofs);
 685
 686                $o_cnt++;
 687                $ofs++;
 688                if ($line =~ /^ /) {
 689                        $n_cnt++;
 690                }
 691                push @line, $line;
 692        }
 693
 694        for ($i = 1; $i < @{$this->{TEXT}}; $i++) {
 695                my $line = $this->{TEXT}[$i];
 696                if ($line =~ /^\+/) {
 697                        $n_cnt++;
 698                        push @line, $line;
 699                        next;
 700                }
 701                $ofs++;
 702                $o_cnt++;
 703                if ($line =~ /^ /) {
 704                        $n_cnt++;
 705                }
 706                push @line, $line;
 707        }
 708        my $head = ("@@ -$o0_ofs" .
 709                    (($o_cnt != 1) ? ",$o_cnt" : '') .
 710                    " +$n0_ofs" .
 711                    (($n_cnt != 1) ? ",$n_cnt" : '') .
 712                    " @@\n");
 713        @{$prev->{TEXT}} = ($head, @line);
 714}
 715
 716sub coalesce_overlapping_hunks {
 717        my (@in) = @_;
 718        my @out = ();
 719
 720        my ($last_o_ctx);
 721
 722        for (grep { $_->{USE} } @in) {
 723                my $text = $_->{TEXT};
 724                my ($o_ofs) = parse_hunk_header($text->[0]);
 725                if (defined $last_o_ctx &&
 726                    $o_ofs <= $last_o_ctx) {
 727                        merge_hunk($out[-1], $_);
 728                }
 729                else {
 730                        push @out, $_;
 731                }
 732                $last_o_ctx = find_last_o_ctx($out[-1]);
 733        }
 734        return @out;
 735}
 736
 737sub help_patch_cmd {
 738        print colored $help_color, <<\EOF ;
 739y - stage this hunk
 740n - do not stage this hunk
 741a - stage this and all the remaining hunks in the file
 742d - do not stage this hunk nor any of the remaining hunks in the file
 743j - leave this hunk undecided, see next undecided hunk
 744J - leave this hunk undecided, see next hunk
 745k - leave this hunk undecided, see previous undecided hunk
 746K - leave this hunk undecided, see previous hunk
 747s - split the current hunk into smaller hunks
 748? - print help
 749EOF
 750}
 751
 752sub patch_update_cmd {
 753        my @mods = grep { !($_->{BINARY}) } list_modified('file-only');
 754        my @them;
 755
 756        if (!@mods) {
 757                print STDERR "No changes.\n";
 758                return 0;
 759        }
 760        if ($patch_mode) {
 761                @them = @mods;
 762        }
 763        else {
 764                @them = list_and_choose({ PROMPT => 'Patch update',
 765                                          HEADER => $status_head, },
 766                                        @mods);
 767        }
 768        for (@them) {
 769                patch_update_file($_->{VALUE});
 770        }
 771}
 772
 773sub patch_update_file {
 774        my ($ix, $num);
 775        my $path = shift;
 776        my ($head, @hunk) = parse_diff($path);
 777        for (@{$head->{DISPLAY}}) {
 778                print;
 779        }
 780        $num = scalar @hunk;
 781        $ix = 0;
 782
 783        while (1) {
 784                my ($prev, $next, $other, $undecided, $i);
 785                $other = '';
 786
 787                if ($num <= $ix) {
 788                        $ix = 0;
 789                }
 790                for ($i = 0; $i < $ix; $i++) {
 791                        if (!defined $hunk[$i]{USE}) {
 792                                $prev = 1;
 793                                $other .= '/k';
 794                                last;
 795                        }
 796                }
 797                if ($ix) {
 798                        $other .= '/K';
 799                }
 800                for ($i = $ix + 1; $i < $num; $i++) {
 801                        if (!defined $hunk[$i]{USE}) {
 802                                $next = 1;
 803                                $other .= '/j';
 804                                last;
 805                        }
 806                }
 807                if ($ix < $num - 1) {
 808                        $other .= '/J';
 809                }
 810                for ($i = 0; $i < $num; $i++) {
 811                        if (!defined $hunk[$i]{USE}) {
 812                                $undecided = 1;
 813                                last;
 814                        }
 815                }
 816                last if (!$undecided);
 817
 818                if (hunk_splittable($hunk[$ix]{TEXT})) {
 819                        $other .= '/s';
 820                }
 821                for (@{$hunk[$ix]{DISPLAY}}) {
 822                        print;
 823                }
 824                print colored $prompt_color, "Stage this hunk [y/n/a/d$other/?]? ";
 825                my $line = <STDIN>;
 826                if ($line) {
 827                        if ($line =~ /^y/i) {
 828                                $hunk[$ix]{USE} = 1;
 829                        }
 830                        elsif ($line =~ /^n/i) {
 831                                $hunk[$ix]{USE} = 0;
 832                        }
 833                        elsif ($line =~ /^a/i) {
 834                                while ($ix < $num) {
 835                                        if (!defined $hunk[$ix]{USE}) {
 836                                                $hunk[$ix]{USE} = 1;
 837                                        }
 838                                        $ix++;
 839                                }
 840                                next;
 841                        }
 842                        elsif ($line =~ /^d/i) {
 843                                while ($ix < $num) {
 844                                        if (!defined $hunk[$ix]{USE}) {
 845                                                $hunk[$ix]{USE} = 0;
 846                                        }
 847                                        $ix++;
 848                                }
 849                                next;
 850                        }
 851                        elsif ($other =~ /K/ && $line =~ /^K/) {
 852                                $ix--;
 853                                next;
 854                        }
 855                        elsif ($other =~ /J/ && $line =~ /^J/) {
 856                                $ix++;
 857                                next;
 858                        }
 859                        elsif ($other =~ /k/ && $line =~ /^k/) {
 860                                while (1) {
 861                                        $ix--;
 862                                        last if (!$ix ||
 863                                                 !defined $hunk[$ix]{USE});
 864                                }
 865                                next;
 866                        }
 867                        elsif ($other =~ /j/ && $line =~ /^j/) {
 868                                while (1) {
 869                                        $ix++;
 870                                        last if ($ix >= $num ||
 871                                                 !defined $hunk[$ix]{USE});
 872                                }
 873                                next;
 874                        }
 875                        elsif ($other =~ /s/ && $line =~ /^s/) {
 876                                my @split = split_hunk($hunk[$ix]{TEXT}, $hunk[$ix]{DISPLAY});
 877                                if (1 < @split) {
 878                                        print colored $header_color, "Split into ",
 879                                        scalar(@split), " hunks.\n";
 880                                }
 881                                splice (@hunk, $ix, 1, @split);
 882                                $num = scalar @hunk;
 883                                next;
 884                        }
 885                        else {
 886                                help_patch_cmd($other);
 887                                next;
 888                        }
 889                        # soft increment
 890                        while (1) {
 891                                $ix++;
 892                                last if ($ix >= $num ||
 893                                         !defined $hunk[$ix]{USE});
 894                        }
 895                }
 896        }
 897
 898        @hunk = coalesce_overlapping_hunks(@hunk);
 899
 900        my $n_lofs = 0;
 901        my @result = ();
 902        for (@hunk) {
 903                my $text = $_->{TEXT};
 904                my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
 905                    parse_hunk_header($text->[0]);
 906
 907                if (!$_->{USE}) {
 908                        # We would have added ($n_cnt - $o_cnt) lines
 909                        # to the postimage if we were to use this hunk,
 910                        # but we didn't.  So the line number that the next
 911                        # hunk starts at would be shifted by that much.
 912                        $n_lofs -= ($n_cnt - $o_cnt);
 913                        next;
 914                }
 915                else {
 916                        if ($n_lofs) {
 917                                $n_ofs += $n_lofs;
 918                                $text->[0] = ("@@ -$o_ofs" .
 919                                              (($o_cnt != 1)
 920                                               ? ",$o_cnt" : '') .
 921                                              " +$n_ofs" .
 922                                              (($n_cnt != 1)
 923                                               ? ",$n_cnt" : '') .
 924                                              " @@\n");
 925                        }
 926                        for (@$text) {
 927                                push @result, $_;
 928                        }
 929                }
 930        }
 931
 932        if (@result) {
 933                my $fh;
 934
 935                open $fh, '| git apply --cached';
 936                for (@{$head->{TEXT}}, @result) {
 937                        print $fh $_;
 938                }
 939                if (!close $fh) {
 940                        for (@{$head->{TEXT}}, @result) {
 941                                print STDERR $_;
 942                        }
 943                }
 944                refresh();
 945        }
 946
 947        print "\n";
 948}
 949
 950sub diff_cmd {
 951        my @mods = list_modified('index-only');
 952        @mods = grep { !($_->{BINARY}) } @mods;
 953        return if (!@mods);
 954        my (@them) = list_and_choose({ PROMPT => 'Review diff',
 955                                     IMMEDIATE => 1,
 956                                     HEADER => $status_head, },
 957                                   @mods);
 958        return if (!@them);
 959        system(qw(git diff -p --cached HEAD --), map { $_->{VALUE} } @them);
 960}
 961
 962sub quit_cmd {
 963        print "Bye.\n";
 964        exit(0);
 965}
 966
 967sub help_cmd {
 968        print colored $help_color, <<\EOF ;
 969status        - show paths with changes
 970update        - add working tree state to the staged set of changes
 971revert        - revert staged set of changes back to the HEAD version
 972patch         - pick hunks and update selectively
 973diff          - view diff between HEAD and index
 974add untracked - add contents of untracked files to the staged set of changes
 975EOF
 976}
 977
 978sub process_args {
 979        return unless @ARGV;
 980        my $arg = shift @ARGV;
 981        if ($arg eq "--patch") {
 982                $patch_mode = 1;
 983                $arg = shift @ARGV or die "missing --";
 984                die "invalid argument $arg, expecting --"
 985                    unless $arg eq "--";
 986        }
 987        elsif ($arg ne "--") {
 988                die "invalid argument $arg, expecting --";
 989        }
 990}
 991
 992sub main_loop {
 993        my @cmd = ([ 'status', \&status_cmd, ],
 994                   [ 'update', \&update_cmd, ],
 995                   [ 'revert', \&revert_cmd, ],
 996                   [ 'add untracked', \&add_untracked_cmd, ],
 997                   [ 'patch', \&patch_update_cmd, ],
 998                   [ 'diff', \&diff_cmd, ],
 999                   [ 'quit', \&quit_cmd, ],
1000                   [ 'help', \&help_cmd, ],
1001        );
1002        while (1) {
1003                my ($it) = list_and_choose({ PROMPT => 'What now',
1004                                             SINGLETON => 1,
1005                                             LIST_FLAT => 4,
1006                                             HEADER => '*** Commands ***',
1007                                             ON_EOF => \&quit_cmd,
1008                                             IMMEDIATE => 1 }, @cmd);
1009                if ($it) {
1010                        eval {
1011                                $it->[1]->();
1012                        };
1013                        if ($@) {
1014                                print "$@";
1015                        }
1016                }
1017        }
1018}
1019
1020process_args();
1021refresh();
1022if ($patch_mode) {
1023        patch_update_cmd();
1024}
1025else {
1026        status_cmd();
1027        main_loop();
1028}