git-add--interactive.perlon commit Merge branch 'pw/add-p-single' (60f8b89)
   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        for (my $i = 0; $i < @diff; $i++) {
 709                if ($diff[$i] =~ /^@@ /) {
 710                        push @hunk, { TEXT => [], DISPLAY => [],
 711                                TYPE => 'hunk' };
 712                }
 713                push @{$hunk[-1]{TEXT}}, $diff[$i];
 714                push @{$hunk[-1]{DISPLAY}},
 715                        (@colored ? $colored[$i] : $diff[$i]);
 716        }
 717        return @hunk;
 718}
 719
 720sub parse_diff_header {
 721        my $src = shift;
 722
 723        my $head = { TEXT => [], DISPLAY => [], TYPE => 'header' };
 724        my $mode = { TEXT => [], DISPLAY => [], TYPE => 'mode' };
 725        my $deletion = { TEXT => [], DISPLAY => [], TYPE => 'deletion' };
 726
 727        for (my $i = 0; $i < @{$src->{TEXT}}; $i++) {
 728                my $dest =
 729                   $src->{TEXT}->[$i] =~ /^(old|new) mode (\d+)$/ ? $mode :
 730                   $src->{TEXT}->[$i] =~ /^deleted file/ ? $deletion :
 731                   $head;
 732                push @{$dest->{TEXT}}, $src->{TEXT}->[$i];
 733                push @{$dest->{DISPLAY}}, $src->{DISPLAY}->[$i];
 734        }
 735        return ($head, $mode, $deletion);
 736}
 737
 738sub hunk_splittable {
 739        my ($text) = @_;
 740
 741        my @s = split_hunk($text);
 742        return (1 < @s);
 743}
 744
 745sub parse_hunk_header {
 746        my ($line) = @_;
 747        my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
 748            $line =~ /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
 749        $o_cnt = 1 unless defined $o_cnt;
 750        $n_cnt = 1 unless defined $n_cnt;
 751        return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
 752}
 753
 754sub split_hunk {
 755        my ($text, $display) = @_;
 756        my @split = ();
 757        if (!defined $display) {
 758                $display = $text;
 759        }
 760        # If there are context lines in the middle of a hunk,
 761        # it can be split, but we would need to take care of
 762        # overlaps later.
 763
 764        my ($o_ofs, undef, $n_ofs) = parse_hunk_header($text->[0]);
 765        my $hunk_start = 1;
 766
 767      OUTER:
 768        while (1) {
 769                my $next_hunk_start = undef;
 770                my $i = $hunk_start - 1;
 771                my $this = +{
 772                        TEXT => [],
 773                        DISPLAY => [],
 774                        TYPE => 'hunk',
 775                        OLD => $o_ofs,
 776                        NEW => $n_ofs,
 777                        OCNT => 0,
 778                        NCNT => 0,
 779                        ADDDEL => 0,
 780                        POSTCTX => 0,
 781                        USE => undef,
 782                };
 783
 784                while (++$i < @$text) {
 785                        my $line = $text->[$i];
 786                        my $display = $display->[$i];
 787                        if ($line =~ /^ /) {
 788                                if ($this->{ADDDEL} &&
 789                                    !defined $next_hunk_start) {
 790                                        # We have seen leading context and
 791                                        # adds/dels and then here is another
 792                                        # context, which is trailing for this
 793                                        # split hunk and leading for the next
 794                                        # one.
 795                                        $next_hunk_start = $i;
 796                                }
 797                                push @{$this->{TEXT}}, $line;
 798                                push @{$this->{DISPLAY}}, $display;
 799                                $this->{OCNT}++;
 800                                $this->{NCNT}++;
 801                                if (defined $next_hunk_start) {
 802                                        $this->{POSTCTX}++;
 803                                }
 804                                next;
 805                        }
 806
 807                        # add/del
 808                        if (defined $next_hunk_start) {
 809                                # We are done with the current hunk and
 810                                # this is the first real change for the
 811                                # next split one.
 812                                $hunk_start = $next_hunk_start;
 813                                $o_ofs = $this->{OLD} + $this->{OCNT};
 814                                $n_ofs = $this->{NEW} + $this->{NCNT};
 815                                $o_ofs -= $this->{POSTCTX};
 816                                $n_ofs -= $this->{POSTCTX};
 817                                push @split, $this;
 818                                redo OUTER;
 819                        }
 820                        push @{$this->{TEXT}}, $line;
 821                        push @{$this->{DISPLAY}}, $display;
 822                        $this->{ADDDEL}++;
 823                        if ($line =~ /^-/) {
 824                                $this->{OCNT}++;
 825                        }
 826                        else {
 827                                $this->{NCNT}++;
 828                        }
 829                }
 830
 831                push @split, $this;
 832                last;
 833        }
 834
 835        for my $hunk (@split) {
 836                $o_ofs = $hunk->{OLD};
 837                $n_ofs = $hunk->{NEW};
 838                my $o_cnt = $hunk->{OCNT};
 839                my $n_cnt = $hunk->{NCNT};
 840
 841                my $head = ("@@ -$o_ofs" .
 842                            (($o_cnt != 1) ? ",$o_cnt" : '') .
 843                            " +$n_ofs" .
 844                            (($n_cnt != 1) ? ",$n_cnt" : '') .
 845                            " @@\n");
 846                my $display_head = $head;
 847                unshift @{$hunk->{TEXT}}, $head;
 848                if ($diff_use_color) {
 849                        $display_head = colored($fraginfo_color, $head);
 850                }
 851                unshift @{$hunk->{DISPLAY}}, $display_head;
 852        }
 853        return @split;
 854}
 855
 856sub find_last_o_ctx {
 857        my ($it) = @_;
 858        my $text = $it->{TEXT};
 859        my ($o_ofs, $o_cnt) = parse_hunk_header($text->[0]);
 860        my $i = @{$text};
 861        my $last_o_ctx = $o_ofs + $o_cnt;
 862        while (0 < --$i) {
 863                my $line = $text->[$i];
 864                if ($line =~ /^ /) {
 865                        $last_o_ctx--;
 866                        next;
 867                }
 868                last;
 869        }
 870        return $last_o_ctx;
 871}
 872
 873sub merge_hunk {
 874        my ($prev, $this) = @_;
 875        my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) =
 876            parse_hunk_header($prev->{TEXT}[0]);
 877        my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) =
 878            parse_hunk_header($this->{TEXT}[0]);
 879
 880        my (@line, $i, $ofs, $o_cnt, $n_cnt);
 881        $ofs = $o0_ofs;
 882        $o_cnt = $n_cnt = 0;
 883        for ($i = 1; $i < @{$prev->{TEXT}}; $i++) {
 884                my $line = $prev->{TEXT}[$i];
 885                if ($line =~ /^\+/) {
 886                        $n_cnt++;
 887                        push @line, $line;
 888                        next;
 889                }
 890
 891                last if ($o1_ofs <= $ofs);
 892
 893                $o_cnt++;
 894                $ofs++;
 895                if ($line =~ /^ /) {
 896                        $n_cnt++;
 897                }
 898                push @line, $line;
 899        }
 900
 901        for ($i = 1; $i < @{$this->{TEXT}}; $i++) {
 902                my $line = $this->{TEXT}[$i];
 903                if ($line =~ /^\+/) {
 904                        $n_cnt++;
 905                        push @line, $line;
 906                        next;
 907                }
 908                $ofs++;
 909                $o_cnt++;
 910                if ($line =~ /^ /) {
 911                        $n_cnt++;
 912                }
 913                push @line, $line;
 914        }
 915        my $head = ("@@ -$o0_ofs" .
 916                    (($o_cnt != 1) ? ",$o_cnt" : '') .
 917                    " +$n0_ofs" .
 918                    (($n_cnt != 1) ? ",$n_cnt" : '') .
 919                    " @@\n");
 920        @{$prev->{TEXT}} = ($head, @line);
 921}
 922
 923sub coalesce_overlapping_hunks {
 924        my (@in) = @_;
 925        my @out = ();
 926
 927        my ($last_o_ctx, $last_was_dirty);
 928
 929        for (grep { $_->{USE} } @in) {
 930                if ($_->{TYPE} ne 'hunk') {
 931                        push @out, $_;
 932                        next;
 933                }
 934                my $text = $_->{TEXT};
 935                my ($o_ofs) = parse_hunk_header($text->[0]);
 936                if (defined $last_o_ctx &&
 937                    $o_ofs <= $last_o_ctx &&
 938                    !$_->{DIRTY} &&
 939                    !$last_was_dirty) {
 940                        merge_hunk($out[-1], $_);
 941                }
 942                else {
 943                        push @out, $_;
 944                }
 945                $last_o_ctx = find_last_o_ctx($out[-1]);
 946                $last_was_dirty = $_->{DIRTY};
 947        }
 948        return @out;
 949}
 950
 951sub reassemble_patch {
 952        my $head = shift;
 953        my @patch;
 954
 955        # Include everything in the header except the beginning of the diff.
 956        push @patch, (grep { !/^[-+]{3}/ } @$head);
 957
 958        # Then include any headers from the hunk lines, which must
 959        # come before any actual hunk.
 960        while (@_ && $_[0] !~ /^@/) {
 961                push @patch, shift;
 962        }
 963
 964        # Then begin the diff.
 965        push @patch, grep { /^[-+]{3}/ } @$head;
 966
 967        # And then the actual hunks.
 968        push @patch, @_;
 969
 970        return @patch;
 971}
 972
 973sub color_diff {
 974        return map {
 975                colored((/^@/  ? $fraginfo_color :
 976                         /^\+/ ? $diff_new_color :
 977                         /^-/  ? $diff_old_color :
 978                         $diff_plain_color),
 979                        $_);
 980        } @_;
 981}
 982
 983my %edit_hunk_manually_modes = (
 984        stage => N__(
 985"If the patch applies cleanly, the edited hunk will immediately be
 986marked for staging."),
 987        stash => N__(
 988"If the patch applies cleanly, the edited hunk will immediately be
 989marked for stashing."),
 990        reset_head => N__(
 991"If the patch applies cleanly, the edited hunk will immediately be
 992marked for unstaging."),
 993        reset_nothead => N__(
 994"If the patch applies cleanly, the edited hunk will immediately be
 995marked for applying."),
 996        checkout_index => N__(
 997"If the patch applies cleanly, the edited hunk will immediately be
 998marked for discarding."),
 999        checkout_head => N__(
1000"If the patch applies cleanly, the edited hunk will immediately be
1001marked for discarding."),
1002        checkout_nothead => N__(
1003"If the patch applies cleanly, the edited hunk will immediately be
1004marked for applying."),
1005);
1006
1007sub edit_hunk_manually {
1008        my ($oldtext) = @_;
1009
1010        my $hunkfile = $repo->repo_path . "/addp-hunk-edit.diff";
1011        my $fh;
1012        open $fh, '>', $hunkfile
1013                or die sprintf(__("failed to open hunk edit file for writing: %s"), $!);
1014        print $fh Git::comment_lines __("Manual hunk edit mode -- see bottom for a quick guide.\n");
1015        print $fh @$oldtext;
1016        my $is_reverse = $patch_mode_flavour{IS_REVERSE};
1017        my ($remove_plus, $remove_minus) = $is_reverse ? ('-', '+') : ('+', '-');
1018        my $comment_line_char = Git::get_comment_line_char;
1019        print $fh Git::comment_lines sprintf(__ <<EOF, $remove_minus, $remove_plus, $comment_line_char),
1020---
1021To remove '%s' lines, make them ' ' lines (context).
1022To remove '%s' lines, delete them.
1023Lines starting with %s will be removed.
1024EOF
1025__($edit_hunk_manually_modes{$patch_mode}),
1026# TRANSLATORS: 'it' refers to the patch mentioned in the previous messages.
1027__ <<EOF2 ;
1028If it does not apply cleanly, you will be given an opportunity to
1029edit again.  If all lines of the hunk are removed, then the edit is
1030aborted and the hunk is left unchanged.
1031EOF2
1032        close $fh;
1033
1034        chomp(my $editor = run_cmd_pipe(qw(git var GIT_EDITOR)));
1035        system('sh', '-c', $editor.' "$@"', $editor, $hunkfile);
1036
1037        if ($? != 0) {
1038                return undef;
1039        }
1040
1041        open $fh, '<', $hunkfile
1042                or die sprintf(__("failed to open hunk edit file for reading: %s"), $!);
1043        my @newtext = grep { !/^\Q$comment_line_char\E/ } <$fh>;
1044        close $fh;
1045        unlink $hunkfile;
1046
1047        # Abort if nothing remains
1048        if (!grep { /\S/ } @newtext) {
1049                return undef;
1050        }
1051
1052        # Reinsert the first hunk header if the user accidentally deleted it
1053        if ($newtext[0] !~ /^@/) {
1054                unshift @newtext, $oldtext->[0];
1055        }
1056        return \@newtext;
1057}
1058
1059sub diff_applies {
1060        return run_git_apply($patch_mode_flavour{APPLY_CHECK} . ' --check',
1061                             map { @{$_->{TEXT}} } @_);
1062}
1063
1064sub _restore_terminal_and_die {
1065        ReadMode 'restore';
1066        print "\n";
1067        exit 1;
1068}
1069
1070sub prompt_single_character {
1071        if ($use_readkey) {
1072                local $SIG{TERM} = \&_restore_terminal_and_die;
1073                local $SIG{INT} = \&_restore_terminal_and_die;
1074                ReadMode 'cbreak';
1075                my $key = ReadKey 0;
1076                ReadMode 'restore';
1077                if ($use_termcap and $key eq "\e") {
1078                        while (!defined $term_escapes{$key}) {
1079                                my $next = ReadKey 0.5;
1080                                last if (!defined $next);
1081                                $key .= $next;
1082                        }
1083                        $key =~ s/\e/^[/;
1084                }
1085                print "$key" if defined $key;
1086                print "\n";
1087                return $key;
1088        } else {
1089                return <STDIN>;
1090        }
1091}
1092
1093sub prompt_yesno {
1094        my ($prompt) = @_;
1095        while (1) {
1096                print colored $prompt_color, $prompt;
1097                my $line = prompt_single_character;
1098                return undef unless defined $line;
1099                return 0 if $line =~ /^n/i;
1100                return 1 if $line =~ /^y/i;
1101        }
1102}
1103
1104sub edit_hunk_loop {
1105        my ($head, $hunk, $ix) = @_;
1106        my $text = $hunk->[$ix]->{TEXT};
1107
1108        while (1) {
1109                $text = edit_hunk_manually($text);
1110                if (!defined $text) {
1111                        return undef;
1112                }
1113                my $newhunk = {
1114                        TEXT => $text,
1115                        TYPE => $hunk->[$ix]->{TYPE},
1116                        USE => 1,
1117                        DIRTY => 1,
1118                };
1119                if (diff_applies($head,
1120                                 @{$hunk}[0..$ix-1],
1121                                 $newhunk,
1122                                 @{$hunk}[$ix+1..$#{$hunk}])) {
1123                        $newhunk->{DISPLAY} = [color_diff(@{$text})];
1124                        return $newhunk;
1125                }
1126                else {
1127                        prompt_yesno(
1128                                # TRANSLATORS: do not translate [y/n]
1129                                # The program will only accept that input
1130                                # at this point.
1131                                # Consider translating (saying "no" discards!) as
1132                                # (saying "n" for "no" discards!) if the translation
1133                                # of the word "no" does not start with n.
1134                                __('Your edited hunk does not apply. Edit again '
1135                                   . '(saying "no" discards!) [y/n]? ')
1136                                ) or return undef;
1137                }
1138        }
1139}
1140
1141my %help_patch_modes = (
1142        stage => N__(
1143"y - stage this hunk
1144n - do not stage this hunk
1145q - quit; do not stage this hunk or any of the remaining ones
1146a - stage this hunk and all later hunks in the file
1147d - do not stage this hunk or any of the later hunks in the file"),
1148        stash => N__(
1149"y - stash this hunk
1150n - do not stash this hunk
1151q - quit; do not stash this hunk or any of the remaining ones
1152a - stash this hunk and all later hunks in the file
1153d - do not stash this hunk or any of the later hunks in the file"),
1154        reset_head => N__(
1155"y - unstage this hunk
1156n - do not unstage this hunk
1157q - quit; do not unstage this hunk or any of the remaining ones
1158a - unstage this hunk and all later hunks in the file
1159d - do not unstage this hunk or any of the later hunks in the file"),
1160        reset_nothead => N__(
1161"y - apply this hunk to index
1162n - do not apply this hunk to index
1163q - quit; do not apply this hunk or any of the remaining ones
1164a - apply this hunk and all later hunks in the file
1165d - do not apply this hunk or any of the later hunks in the file"),
1166        checkout_index => N__(
1167"y - discard this hunk from worktree
1168n - do not discard this hunk from worktree
1169q - quit; do not discard this hunk or any of the remaining ones
1170a - discard this hunk and all later hunks in the file
1171d - do not discard this hunk or any of the later hunks in the file"),
1172        checkout_head => N__(
1173"y - discard this hunk from index and worktree
1174n - do not discard this hunk from index and worktree
1175q - quit; do not discard this hunk or any of the remaining ones
1176a - discard this hunk and all later hunks in the file
1177d - do not discard this hunk or any of the later hunks in the file"),
1178        checkout_nothead => N__(
1179"y - apply this hunk to index and worktree
1180n - do not apply this hunk to index and worktree
1181q - quit; do not apply this hunk or any of the remaining ones
1182a - apply this hunk and all later hunks in the file
1183d - do not apply this hunk or any of the later hunks in the file"),
1184);
1185
1186sub help_patch_cmd {
1187        local $_;
1188        my $other = $_[0] . ",?";
1189        print colored $help_color, __($help_patch_modes{$patch_mode}), "\n",
1190                map { "$_\n" } grep {
1191                        my $c = quotemeta(substr($_, 0, 1));
1192                        $other =~ /,$c/
1193                } split "\n", __ <<EOF ;
1194g - select a hunk to go to
1195/ - search for a hunk matching the given regex
1196j - leave this hunk undecided, see next undecided hunk
1197J - leave this hunk undecided, see next hunk
1198k - leave this hunk undecided, see previous undecided hunk
1199K - leave this hunk undecided, see previous hunk
1200s - split the current hunk into smaller hunks
1201e - manually edit the current hunk
1202? - print help
1203EOF
1204}
1205
1206sub apply_patch {
1207        my $cmd = shift;
1208        my $ret = run_git_apply $cmd, @_;
1209        if (!$ret) {
1210                print STDERR @_;
1211        }
1212        return $ret;
1213}
1214
1215sub apply_patch_for_checkout_commit {
1216        my $reverse = shift;
1217        my $applies_index = run_git_apply 'apply '.$reverse.' --cached --check', @_;
1218        my $applies_worktree = run_git_apply 'apply '.$reverse.' --check', @_;
1219
1220        if ($applies_worktree && $applies_index) {
1221                run_git_apply 'apply '.$reverse.' --cached', @_;
1222                run_git_apply 'apply '.$reverse, @_;
1223                return 1;
1224        } elsif (!$applies_index) {
1225                print colored $error_color, __("The selected hunks do not apply to the index!\n");
1226                if (prompt_yesno __("Apply them to the worktree anyway? ")) {
1227                        return run_git_apply 'apply '.$reverse, @_;
1228                } else {
1229                        print colored $error_color, __("Nothing was applied.\n");
1230                        return 0;
1231                }
1232        } else {
1233                print STDERR @_;
1234                return 0;
1235        }
1236}
1237
1238sub patch_update_cmd {
1239        my @all_mods = list_modified($patch_mode_flavour{FILTER});
1240        error_msg sprintf(__("ignoring unmerged: %s\n"), $_->{VALUE})
1241                for grep { $_->{UNMERGED} } @all_mods;
1242        @all_mods = grep { !$_->{UNMERGED} } @all_mods;
1243
1244        my @mods = grep { !($_->{BINARY}) } @all_mods;
1245        my @them;
1246
1247        if (!@mods) {
1248                if (@all_mods) {
1249                        print STDERR __("Only binary files changed.\n");
1250                } else {
1251                        print STDERR __("No changes.\n");
1252                }
1253                return 0;
1254        }
1255        if ($patch_mode_only) {
1256                @them = @mods;
1257        }
1258        else {
1259                @them = list_and_choose({ PROMPT => __('Patch update'),
1260                                          HEADER => $status_head, },
1261                                        @mods);
1262        }
1263        for (@them) {
1264                return 0 if patch_update_file($_->{VALUE});
1265        }
1266}
1267
1268# Generate a one line summary of a hunk.
1269sub summarize_hunk {
1270        my $rhunk = shift;
1271        my $summary = $rhunk->{TEXT}[0];
1272
1273        # Keep the line numbers, discard extra context.
1274        $summary =~ s/@@(.*?)@@.*/$1 /s;
1275        $summary .= " " x (20 - length $summary);
1276
1277        # Add some user context.
1278        for my $line (@{$rhunk->{TEXT}}) {
1279                if ($line =~ m/^[+-].*\w/) {
1280                        $summary .= $line;
1281                        last;
1282                }
1283        }
1284
1285        chomp $summary;
1286        return substr($summary, 0, 80) . "\n";
1287}
1288
1289
1290# Print a one-line summary of each hunk in the array ref in
1291# the first argument, starting with the index in the 2nd.
1292sub display_hunks {
1293        my ($hunks, $i) = @_;
1294        my $ctr = 0;
1295        $i ||= 0;
1296        for (; $i < @$hunks && $ctr < 20; $i++, $ctr++) {
1297                my $status = " ";
1298                if (defined $hunks->[$i]{USE}) {
1299                        $status = $hunks->[$i]{USE} ? "+" : "-";
1300                }
1301                printf "%s%2d: %s",
1302                        $status,
1303                        $i + 1,
1304                        summarize_hunk($hunks->[$i]);
1305        }
1306        return $i;
1307}
1308
1309my %patch_update_prompt_modes = (
1310        stage => {
1311                mode => N__("Stage mode change [y,n,q,a,d%s,?]? "),
1312                deletion => N__("Stage deletion [y,n,q,a,d%s,?]? "),
1313                hunk => N__("Stage this hunk [y,n,q,a,d%s,?]? "),
1314        },
1315        stash => {
1316                mode => N__("Stash mode change [y,n,q,a,d%s,?]? "),
1317                deletion => N__("Stash deletion [y,n,q,a,d%s,?]? "),
1318                hunk => N__("Stash this hunk [y,n,q,a,d%s,?]? "),
1319        },
1320        reset_head => {
1321                mode => N__("Unstage mode change [y,n,q,a,d%s,?]? "),
1322                deletion => N__("Unstage deletion [y,n,q,a,d%s,?]? "),
1323                hunk => N__("Unstage this hunk [y,n,q,a,d%s,?]? "),
1324        },
1325        reset_nothead => {
1326                mode => N__("Apply mode change to index [y,n,q,a,d%s,?]? "),
1327                deletion => N__("Apply deletion to index [y,n,q,a,d%s,?]? "),
1328                hunk => N__("Apply this hunk to index [y,n,q,a,d%s,?]? "),
1329        },
1330        checkout_index => {
1331                mode => N__("Discard mode change from worktree [y,n,q,a,d%s,?]? "),
1332                deletion => N__("Discard deletion from worktree [y,n,q,a,d%s,?]? "),
1333                hunk => N__("Discard this hunk from worktree [y,n,q,a,d%s,?]? "),
1334        },
1335        checkout_head => {
1336                mode => N__("Discard mode change from index and worktree [y,n,q,a,d%s,?]? "),
1337                deletion => N__("Discard deletion from index and worktree [y,n,q,a,d%s,?]? "),
1338                hunk => N__("Discard this hunk from index and worktree [y,n,q,a,d%s,?]? "),
1339        },
1340        checkout_nothead => {
1341                mode => N__("Apply mode change to index and worktree [y,n,q,a,d%s,?]? "),
1342                deletion => N__("Apply deletion to index and worktree [y,n,q,a,d%s,?]? "),
1343                hunk => N__("Apply this hunk to index and worktree [y,n,q,a,d%s,?]? "),
1344        },
1345);
1346
1347sub patch_update_file {
1348        my $quit = 0;
1349        my ($ix, $num);
1350        my $path = shift;
1351        my ($head, @hunk) = parse_diff($path);
1352        ($head, my $mode, my $deletion) = parse_diff_header($head);
1353        for (@{$head->{DISPLAY}}) {
1354                print;
1355        }
1356
1357        if (@{$mode->{TEXT}}) {
1358                unshift @hunk, $mode;
1359        }
1360        if (@{$deletion->{TEXT}}) {
1361                foreach my $hunk (@hunk) {
1362                        push @{$deletion->{TEXT}}, @{$hunk->{TEXT}};
1363                        push @{$deletion->{DISPLAY}}, @{$hunk->{DISPLAY}};
1364                }
1365                @hunk = ($deletion);
1366        }
1367
1368        $num = scalar @hunk;
1369        $ix = 0;
1370
1371        while (1) {
1372                my ($prev, $next, $other, $undecided, $i);
1373                $other = '';
1374
1375                if ($num <= $ix) {
1376                        $ix = 0;
1377                }
1378                for ($i = 0; $i < $ix; $i++) {
1379                        if (!defined $hunk[$i]{USE}) {
1380                                $prev = 1;
1381                                $other .= ',k';
1382                                last;
1383                        }
1384                }
1385                if ($ix) {
1386                        $other .= ',K';
1387                }
1388                for ($i = $ix + 1; $i < $num; $i++) {
1389                        if (!defined $hunk[$i]{USE}) {
1390                                $next = 1;
1391                                $other .= ',j';
1392                                last;
1393                        }
1394                }
1395                if ($ix < $num - 1) {
1396                        $other .= ',J';
1397                }
1398                if ($num > 1) {
1399                        $other .= ',g,/';
1400                }
1401                for ($i = 0; $i < $num; $i++) {
1402                        if (!defined $hunk[$i]{USE}) {
1403                                $undecided = 1;
1404                                last;
1405                        }
1406                }
1407                last if (!$undecided);
1408
1409                if ($hunk[$ix]{TYPE} eq 'hunk' &&
1410                    hunk_splittable($hunk[$ix]{TEXT})) {
1411                        $other .= ',s';
1412                }
1413                if ($hunk[$ix]{TYPE} eq 'hunk') {
1414                        $other .= ',e';
1415                }
1416                for (@{$hunk[$ix]{DISPLAY}}) {
1417                        print;
1418                }
1419                print colored $prompt_color,
1420                        sprintf(__($patch_update_prompt_modes{$patch_mode}{$hunk[$ix]{TYPE}}), $other);
1421
1422                my $line = prompt_single_character;
1423                last unless defined $line;
1424                if ($line) {
1425                        if ($line =~ /^y/i) {
1426                                $hunk[$ix]{USE} = 1;
1427                        }
1428                        elsif ($line =~ /^n/i) {
1429                                $hunk[$ix]{USE} = 0;
1430                        }
1431                        elsif ($line =~ /^a/i) {
1432                                while ($ix < $num) {
1433                                        if (!defined $hunk[$ix]{USE}) {
1434                                                $hunk[$ix]{USE} = 1;
1435                                        }
1436                                        $ix++;
1437                                }
1438                                next;
1439                        }
1440                        elsif ($line =~ /^g(.*)/) {
1441                                my $response = $1;
1442                                unless ($other =~ /g/) {
1443                                        error_msg __("No other hunks to goto\n");
1444                                        next;
1445                                }
1446                                my $no = $ix > 10 ? $ix - 10 : 0;
1447                                while ($response eq '') {
1448                                        $no = display_hunks(\@hunk, $no);
1449                                        if ($no < $num) {
1450                                                print __("go to which hunk (<ret> to see more)? ");
1451                                        } else {
1452                                                print __("go to which hunk? ");
1453                                        }
1454                                        $response = <STDIN>;
1455                                        if (!defined $response) {
1456                                                $response = '';
1457                                        }
1458                                        chomp $response;
1459                                }
1460                                if ($response !~ /^\s*\d+\s*$/) {
1461                                        error_msg sprintf(__("Invalid number: '%s'\n"),
1462                                                             $response);
1463                                } elsif (0 < $response && $response <= $num) {
1464                                        $ix = $response - 1;
1465                                } else {
1466                                        error_msg sprintf(__n("Sorry, only %d hunk available.\n",
1467                                                              "Sorry, only %d hunks available.\n", $num), $num);
1468                                }
1469                                next;
1470                        }
1471                        elsif ($line =~ /^d/i) {
1472                                while ($ix < $num) {
1473                                        if (!defined $hunk[$ix]{USE}) {
1474                                                $hunk[$ix]{USE} = 0;
1475                                        }
1476                                        $ix++;
1477                                }
1478                                next;
1479                        }
1480                        elsif ($line =~ /^q/i) {
1481                                for ($i = 0; $i < $num; $i++) {
1482                                        if (!defined $hunk[$i]{USE}) {
1483                                                $hunk[$i]{USE} = 0;
1484                                        }
1485                                }
1486                                $quit = 1;
1487                                last;
1488                        }
1489                        elsif ($line =~ m|^/(.*)|) {
1490                                my $regex = $1;
1491                                unless ($other =~ m|/|) {
1492                                        error_msg __("No other hunks to search\n");
1493                                        next;
1494                                }
1495                                if ($1 eq "") {
1496                                        print colored $prompt_color, __("search for regex? ");
1497                                        $regex = <STDIN>;
1498                                        if (defined $regex) {
1499                                                chomp $regex;
1500                                        }
1501                                }
1502                                my $search_string;
1503                                eval {
1504                                        $search_string = qr{$regex}m;
1505                                };
1506                                if ($@) {
1507                                        my ($err,$exp) = ($@, $1);
1508                                        $err =~ s/ at .*git-add--interactive line \d+, <STDIN> line \d+.*$//;
1509                                        error_msg sprintf(__("Malformed search regexp %s: %s\n"), $exp, $err);
1510                                        next;
1511                                }
1512                                my $iy = $ix;
1513                                while (1) {
1514                                        my $text = join ("", @{$hunk[$iy]{TEXT}});
1515                                        last if ($text =~ $search_string);
1516                                        $iy++;
1517                                        $iy = 0 if ($iy >= $num);
1518                                        if ($ix == $iy) {
1519                                                error_msg __("No hunk matches the given pattern\n");
1520                                                last;
1521                                        }
1522                                }
1523                                $ix = $iy;
1524                                next;
1525                        }
1526                        elsif ($line =~ /^K/) {
1527                                if ($other =~ /K/) {
1528                                        $ix--;
1529                                }
1530                                else {
1531                                        error_msg __("No previous hunk\n");
1532                                }
1533                                next;
1534                        }
1535                        elsif ($line =~ /^J/) {
1536                                if ($other =~ /J/) {
1537                                        $ix++;
1538                                }
1539                                else {
1540                                        error_msg __("No next hunk\n");
1541                                }
1542                                next;
1543                        }
1544                        elsif ($line =~ /^k/) {
1545                                if ($other =~ /k/) {
1546                                        while (1) {
1547                                                $ix--;
1548                                                last if (!$ix ||
1549                                                         !defined $hunk[$ix]{USE});
1550                                        }
1551                                }
1552                                else {
1553                                        error_msg __("No previous hunk\n");
1554                                }
1555                                next;
1556                        }
1557                        elsif ($line =~ /^j/) {
1558                                if ($other !~ /j/) {
1559                                        error_msg __("No next hunk\n");
1560                                        next;
1561                                }
1562                        }
1563                        elsif ($line =~ /^s/) {
1564                                unless ($other =~ /s/) {
1565                                        error_msg __("Sorry, cannot split this hunk\n");
1566                                        next;
1567                                }
1568                                my @split = split_hunk($hunk[$ix]{TEXT}, $hunk[$ix]{DISPLAY});
1569                                if (1 < @split) {
1570                                        print colored $header_color, sprintf(
1571                                                __n("Split into %d hunk.\n",
1572                                                    "Split into %d hunks.\n",
1573                                                    scalar(@split)), scalar(@split));
1574                                }
1575                                splice (@hunk, $ix, 1, @split);
1576                                $num = scalar @hunk;
1577                                next;
1578                        }
1579                        elsif ($line =~ /^e/) {
1580                                unless ($other =~ /e/) {
1581                                        error_msg __("Sorry, cannot edit this hunk\n");
1582                                        next;
1583                                }
1584                                my $newhunk = edit_hunk_loop($head, \@hunk, $ix);
1585                                if (defined $newhunk) {
1586                                        splice @hunk, $ix, 1, $newhunk;
1587                                }
1588                        }
1589                        else {
1590                                help_patch_cmd($other);
1591                                next;
1592                        }
1593                        # soft increment
1594                        while (1) {
1595                                $ix++;
1596                                last if ($ix >= $num ||
1597                                         !defined $hunk[$ix]{USE});
1598                        }
1599                }
1600        }
1601
1602        @hunk = coalesce_overlapping_hunks(@hunk);
1603
1604        my $n_lofs = 0;
1605        my @result = ();
1606        for (@hunk) {
1607                if ($_->{USE}) {
1608                        push @result, @{$_->{TEXT}};
1609                }
1610        }
1611
1612        if (@result) {
1613                my @patch = reassemble_patch($head->{TEXT}, @result);
1614                my $apply_routine = $patch_mode_flavour{APPLY};
1615                &$apply_routine(@patch);
1616                refresh();
1617        }
1618
1619        print "\n";
1620        return $quit;
1621}
1622
1623sub diff_cmd {
1624        my @mods = list_modified('index-only');
1625        @mods = grep { !($_->{BINARY}) } @mods;
1626        return if (!@mods);
1627        my (@them) = list_and_choose({ PROMPT => __('Review diff'),
1628                                     IMMEDIATE => 1,
1629                                     HEADER => $status_head, },
1630                                   @mods);
1631        return if (!@them);
1632        my $reference = (is_initial_commit()) ? get_empty_tree() : 'HEAD';
1633        system(qw(git diff -p --cached), $reference, '--',
1634                map { $_->{VALUE} } @them);
1635}
1636
1637sub quit_cmd {
1638        print __("Bye.\n");
1639        exit(0);
1640}
1641
1642sub help_cmd {
1643# TRANSLATORS: please do not translate the command names
1644# 'status', 'update', 'revert', etc.
1645        print colored $help_color, __ <<'EOF' ;
1646status        - show paths with changes
1647update        - add working tree state to the staged set of changes
1648revert        - revert staged set of changes back to the HEAD version
1649patch         - pick hunks and update selectively
1650diff          - view diff between HEAD and index
1651add untracked - add contents of untracked files to the staged set of changes
1652EOF
1653}
1654
1655sub process_args {
1656        return unless @ARGV;
1657        my $arg = shift @ARGV;
1658        if ($arg =~ /--patch(?:=(.*))?/) {
1659                if (defined $1) {
1660                        if ($1 eq 'reset') {
1661                                $patch_mode = 'reset_head';
1662                                $patch_mode_revision = 'HEAD';
1663                                $arg = shift @ARGV or die __("missing --");
1664                                if ($arg ne '--') {
1665                                        $patch_mode_revision = $arg;
1666                                        $patch_mode = ($arg eq 'HEAD' ?
1667                                                       'reset_head' : 'reset_nothead');
1668                                        $arg = shift @ARGV or die __("missing --");
1669                                }
1670                        } elsif ($1 eq 'checkout') {
1671                                $arg = shift @ARGV or die __("missing --");
1672                                if ($arg eq '--') {
1673                                        $patch_mode = 'checkout_index';
1674                                } else {
1675                                        $patch_mode_revision = $arg;
1676                                        $patch_mode = ($arg eq 'HEAD' ?
1677                                                       'checkout_head' : 'checkout_nothead');
1678                                        $arg = shift @ARGV or die __("missing --");
1679                                }
1680                        } elsif ($1 eq 'stage' or $1 eq 'stash') {
1681                                $patch_mode = $1;
1682                                $arg = shift @ARGV or die __("missing --");
1683                        } else {
1684                                die sprintf(__("unknown --patch mode: %s"), $1);
1685                        }
1686                } else {
1687                        $patch_mode = 'stage';
1688                        $arg = shift @ARGV or die __("missing --");
1689                }
1690                die sprintf(__("invalid argument %s, expecting --"),
1691                               $arg) unless $arg eq "--";
1692                %patch_mode_flavour = %{$patch_modes{$patch_mode}};
1693                $patch_mode_only = 1;
1694        }
1695        elsif ($arg ne "--") {
1696                die sprintf(__("invalid argument %s, expecting --"), $arg);
1697        }
1698}
1699
1700sub main_loop {
1701        my @cmd = ([ 'status', \&status_cmd, ],
1702                   [ 'update', \&update_cmd, ],
1703                   [ 'revert', \&revert_cmd, ],
1704                   [ 'add untracked', \&add_untracked_cmd, ],
1705                   [ 'patch', \&patch_update_cmd, ],
1706                   [ 'diff', \&diff_cmd, ],
1707                   [ 'quit', \&quit_cmd, ],
1708                   [ 'help', \&help_cmd, ],
1709        );
1710        while (1) {
1711                my ($it) = list_and_choose({ PROMPT => __('What now'),
1712                                             SINGLETON => 1,
1713                                             LIST_FLAT => 4,
1714                                             HEADER => __('*** Commands ***'),
1715                                             ON_EOF => \&quit_cmd,
1716                                             IMMEDIATE => 1 }, @cmd);
1717                if ($it) {
1718                        eval {
1719                                $it->[1]->();
1720                        };
1721                        if ($@) {
1722                                print "$@";
1723                        }
1724                }
1725        }
1726}
1727
1728process_args();
1729refresh();
1730if ($patch_mode_only) {
1731        patch_update_cmd();
1732}
1733else {
1734        status_cmd();
1735        main_loop();
1736}