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