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