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