Merge branch 'wc/add-i'
authorJunio C Hamano <gitster@pobox.com>
Wed, 5 Dec 2007 05:38:28 +0000 (21:38 -0800)
committerJunio C Hamano <gitster@pobox.com>
Wed, 5 Dec 2007 05:38:28 +0000 (21:38 -0800)
* wc/add-i:
git-add -i: add help text for list-and-choose UI
add -i: allow prefix highlighting for "Add untracked" as well.
Highlight keyboard shortcuts in git-add--interactive
Document all help keys in "git add -i" patch mode.
Add "--patch" option to git-add--interactive
add -i: Fix running from a subdirectory
builtin-add: fix command line building to call interactive
git-add -i: allow multiple selection in patch subcommand
Add path-limiting to git-add--interactive
Teach builtin-add to pass multiple paths to git-add--interactive

Documentation/git-add.txt
builtin-add.c
builtin-commit.c
commit.h
git-add--interactive.perl
index 63829d93cc827255355aa07a7db061b1a3a9e4d9..e74f83b47ad7453ea3fda994f067de81f04baa9d 100644 (file)
@@ -61,7 +61,14 @@ OPTIONS
 
 -i, \--interactive::
        Add modified contents in the working tree interactively to
-       the index.
+       the index. Optional path arguments may be supplied to limit
+       operation to a subset of the working tree. See ``Interactive
+       mode'' for details.
+
+-p, \--patch:
+       Similar to Interactive mode but the initial command loop is
+       bypassed and the 'patch' subcommand is invoked using each of
+       the specified filepatterns before exiting.
 
 -u::
        Update only files that git already knows about. This is similar
@@ -210,6 +217,8 @@ patch::
        k - do not decide on this hunk now, and view the previous
            undecided hunk
        K - do not decide on this hunk now, and view the previous hunk
+       s - split the current hunk into smaller hunks
+       ? - print help
 +
 After deciding the fate for all hunks, if there is any hunk
 that was chosen, the index is updated with the selected hunks.
index 03508d3dcb18d161f799001067d5caf3e54fa160..5c29cc2f3f680e4dee19b040e647dd8b014cf10d 100644 (file)
@@ -19,7 +19,7 @@ static const char * const builtin_add_usage[] = {
        "git-add [options] [--] <filepattern>...",
        NULL
 };
-
+static int patch_interactive = 0, add_interactive = 0;
 static int take_worktree_changes;
 
 static void prune_directory(struct dir_struct *dir, const char **pathspec, int prefix)
@@ -135,11 +135,40 @@ static void refresh(int verbose, const char **pathspec)
         free(seen);
 }
 
-int interactive_add(void)
+static const char **validate_pathspec(int argc, const char **argv, const char *prefix)
 {
-       const char *argv[2] = { "add--interactive", NULL };
+       const char **pathspec = get_pathspec(prefix, argv);
 
-       return run_command_v_opt(argv, RUN_GIT_CMD);
+       return pathspec;
+}
+
+int interactive_add(int argc, const char **argv, const char *prefix)
+{
+       int status, ac;
+       const char **args;
+       const char **pathspec = NULL;
+
+       if (argc) {
+               pathspec = validate_pathspec(argc, argv, prefix);
+               if (!pathspec)
+                       return -1;
+       }
+
+       args = xcalloc(sizeof(const char *), (argc + 4));
+       ac = 0;
+       args[ac++] = "add--interactive";
+       if (patch_interactive)
+               args[ac++] = "--patch";
+       args[ac++] = "--";
+       if (argc) {
+               memcpy(&(args[ac]), pathspec, sizeof(const char *) * argc);
+               ac += argc;
+       }
+       args[ac] = NULL;
+
+       status = run_command_v_opt(args, RUN_GIT_CMD);
+       free(args);
+       return status;
 }
 
 static struct lock_file lock_file;
@@ -148,13 +177,13 @@ static const char ignore_error[] =
 "The following paths are ignored by one of your .gitignore files:\n";
 
 static int verbose = 0, show_only = 0, ignored_too = 0, refresh_only = 0;
-static int add_interactive = 0;
 
 static struct option builtin_add_options[] = {
        OPT__DRY_RUN(&show_only),
        OPT__VERBOSE(&verbose),
        OPT_GROUP(""),
        OPT_BOOLEAN('i', "interactive", &add_interactive, "interactive picking"),
+       OPT_BOOLEAN('p', "patch", &patch_interactive, "interactive patching"),
        OPT_BOOLEAN('f', NULL, &ignored_too, "allow adding otherwise ignored files"),
        OPT_BOOLEAN('u', NULL, &take_worktree_changes, "update tracked files"),
        OPT_BOOLEAN( 0 , "refresh", &refresh_only, "don't add, only refresh the index"),
@@ -163,17 +192,16 @@ static struct option builtin_add_options[] = {
 
 int cmd_add(int argc, const char **argv, const char *prefix)
 {
-       int i, newfd, orig_argc = argc;
+       int i, newfd;
        const char **pathspec;
        struct dir_struct dir;
 
        argc = parse_options(argc, argv, builtin_add_options,
                          builtin_add_usage, 0);
-       if (add_interactive) {
-               if (add_interactive != 1 || orig_argc != 2)
-                       die("add --interactive does not take any parameters");
-               exit(interactive_add());
-       }
+       if (patch_interactive)
+               add_interactive = 1;
+       if (add_interactive)
+               exit(interactive_add(argc, argv, prefix));
 
        git_config(git_default_config);
 
index e635d9963b6ff6ade7742e801bc1cb0583059027..2ec8223132807e1df77512d3d77efd71b790496a 100644 (file)
@@ -163,7 +163,7 @@ static void add_remove_files(struct path_list *list)
        }
 }
 
-static char *prepare_index(const char **files, const char *prefix)
+static char *prepare_index(int argc, const char **argv, const char *prefix)
 {
        int fd;
        struct tree *tree;
@@ -171,7 +171,7 @@ static char *prepare_index(const char **files, const char *prefix)
        const char **pathspec = NULL;
 
        if (interactive) {
-               interactive_add();
+               interactive_add(argc, argv, prefix);
                commit_style = COMMIT_AS_IS;
                return get_index_file();
        }
@@ -179,8 +179,8 @@ static char *prepare_index(const char **files, const char *prefix)
        if (read_cache() < 0)
                die("index file corrupt");
 
-       if (*files)
-               pathspec = get_pathspec(prefix, files);
+       if (*argv)
+               pathspec = get_pathspec(prefix, argv);
 
        /*
         * Non partial, non as-is commit.
@@ -603,7 +603,7 @@ int cmd_status(int argc, const char **argv, const char *prefix)
 
        argc = parse_and_validate_options(argc, argv, builtin_status_usage);
 
-       index_file = prepare_index(argv, prefix);
+       index_file = prepare_index(argc, argv, prefix);
 
        commitable = run_status(stdout, index_file, prefix);
 
@@ -703,7 +703,7 @@ int cmd_commit(int argc, const char **argv, const char *prefix)
 
        argc = parse_and_validate_options(argc, argv, builtin_commit_usage);
 
-       index_file = prepare_index(argv, prefix);
+       index_file = prepare_index(argc, argv, prefix);
 
        if (!no_verify && run_hook(index_file, "pre-commit", NULL)) {
                rollback_index_files();
index f450aae8aa38cfc96bce8a55e418a090c44c216d..10e2b5d4cfdc7ac129ead711421ccc51d2667f02 100644 (file)
--- a/commit.h
+++ b/commit.h
@@ -113,7 +113,7 @@ extern struct commit_list *get_shallow_commits(struct object_array *heads,
 
 int in_merge_bases(struct commit *, struct commit **, int);
 
-extern int interactive_add(void);
+extern int interactive_add(int argc, const char **argv, const char *prefix);
 extern int rerere(void);
 
 static inline int single_parent(struct commit *commit)
index fb1e92a7664f77aa5ca4ca30b4711bafaf155466..335c2c6b56875b97ad6cf8f4406218833afc53ef 100755 (executable)
@@ -2,6 +2,9 @@
 
 use strict;
 
+# command line options
+my $patch_mode;
+
 sub run_cmd_pipe {
        if ($^O eq 'MSWin32') {
                my @invalid = grep {m/[":*]/} @_;
@@ -37,14 +40,13 @@ sub list_untracked {
                chomp $_;
                $_;
        }
-       run_cmd_pipe(qw(git ls-files --others --exclude-standard --), @_);
+       run_cmd_pipe(qw(git ls-files --others --exclude-standard --), @ARGV);
 }
 
 my $status_fmt = '%12s %12s %s';
 my $status_head = sprintf($status_fmt, 'staged', 'unstaged', 'path');
 
 # Returns list of hashes, contents of each of which are:
-# PRINT:       print message
 # VALUE:       pathname
 # BINARY:      is a binary path
 # INDEX:       is index different from HEAD?
@@ -56,9 +58,17 @@ sub list_modified {
        my ($only) = @_;
        my (%data, @return);
        my ($add, $del, $adddel, $file);
+       my @tracked = ();
+
+       if (@ARGV) {
+               @tracked = map {
+                       chomp $_; $_;
+               } run_cmd_pipe(qw(git ls-files --exclude-standard --), @ARGV);
+               return if (!@tracked);
+       }
 
        for (run_cmd_pipe(qw(git diff-index --cached
-                            --numstat --summary HEAD))) {
+                            --numstat --summary HEAD --), @tracked)) {
                if (($add, $del, $file) =
                    /^([-\d]+)  ([-\d]+)        (.*)/) {
                        my ($change, $bin);
@@ -81,7 +91,7 @@ sub list_modified {
                }
        }
 
-       for (run_cmd_pipe(qw(git diff-files --numstat --summary))) {
+       for (run_cmd_pipe(qw(git diff-files --numstat --summary --), @tracked)) {
                if (($add, $del, $file) =
                    /^([-\d]+)  ([-\d]+)        (.*)/) {
                        if (!exists $data{$file}) {
@@ -122,8 +132,6 @@ sub list_modified {
                }
                push @return, +{
                        VALUE => $_,
-                       PRINT => (sprintf $status_fmt,
-                                 $it->{INDEX}, $it->{FILE}, $_),
                        %$it,
                };
        }
@@ -159,10 +167,96 @@ sub find_unique {
        return $found;
 }
 
+# inserts string into trie and updates count for each character
+sub update_trie {
+       my ($trie, $string) = @_;
+       foreach (split //, $string) {
+               $trie = $trie->{$_} ||= {COUNT => 0};
+               $trie->{COUNT}++;
+       }
+}
+
+# returns an array of tuples (prefix, remainder)
+sub find_unique_prefixes {
+       my @stuff = @_;
+       my @return = ();
+
+       # any single prefix exceeding the soft limit is omitted
+       # if any prefix exceeds the hard limit all are omitted
+       # 0 indicates no limit
+       my $soft_limit = 0;
+       my $hard_limit = 3;
+
+       # build a trie modelling all possible options
+       my %trie;
+       foreach my $print (@stuff) {
+               if ((ref $print) eq 'ARRAY') {
+                       $print = $print->[0];
+               }
+               elsif ((ref $print) eq 'HASH') {
+                       $print = $print->{VALUE};
+               }
+               update_trie(\%trie, $print);
+               push @return, $print;
+       }
+
+       # use the trie to find the unique prefixes
+       for (my $i = 0; $i < @return; $i++) {
+               my $ret = $return[$i];
+               my @letters = split //, $ret;
+               my %search = %trie;
+               my ($prefix, $remainder);
+               my $j;
+               for ($j = 0; $j < @letters; $j++) {
+                       my $letter = $letters[$j];
+                       if ($search{$letter}{COUNT} == 1) {
+                               $prefix = substr $ret, 0, $j + 1;
+                               $remainder = substr $ret, $j + 1;
+                               last;
+                       }
+                       else {
+                               my $prefix = substr $ret, 0, $j;
+                               return ()
+                                   if ($hard_limit && $j + 1 > $hard_limit);
+                       }
+                       %search = %{$search{$letter}};
+               }
+               if ($soft_limit && $j + 1 > $soft_limit) {
+                       $prefix = undef;
+                       $remainder = $ret;
+               }
+               $return[$i] = [$prefix, $remainder];
+       }
+       return @return;
+}
+
+# filters out prefixes which have special meaning to list_and_choose()
+sub is_valid_prefix {
+       my $prefix = shift;
+       return (defined $prefix) &&
+           !($prefix =~ /[\s,]/) && # separators
+           !($prefix =~ /^-/) &&    # deselection
+           !($prefix =~ /^\d+/) &&  # selection
+           ($prefix ne '*') &&      # "all" wildcard
+           ($prefix ne '?');        # prompt help
+}
+
+# given a prefix/remainder tuple return a string with the prefix highlighted
+# for now use square brackets; later might use ANSI colors (underline, bold)
+sub highlight_prefix {
+       my $prefix = shift;
+       my $remainder = shift;
+       return $remainder unless defined $prefix;
+       return is_valid_prefix($prefix) ?
+           "[$prefix]$remainder" :
+           "$prefix$remainder";
+}
+
 sub list_and_choose {
        my ($opts, @stuff) = @_;
        my (@chosen, @return);
        my $i;
+       my @prefixes = find_unique_prefixes(@stuff) unless $opts->{LIST_ONLY};
 
       TOPLOOP:
        while (1) {
@@ -177,13 +271,21 @@ sub list_and_choose {
                for ($i = 0; $i < @stuff; $i++) {
                        my $chosen = $chosen[$i] ? '*' : ' ';
                        my $print = $stuff[$i];
-                       if (ref $print) {
-                               if ((ref $print) eq 'ARRAY') {
-                                       $print = $print->[0];
-                               }
-                               else {
-                                       $print = $print->{PRINT};
-                               }
+                       my $ref = ref $print;
+                       my $highlighted = highlight_prefix(@{$prefixes[$i]})
+                           if @prefixes;
+                       if ($ref eq 'ARRAY') {
+                               $print = $highlighted || $print->[0];
+                       }
+                       elsif ($ref eq 'HASH') {
+                               my $value = $highlighted || $print->{VALUE};
+                               $print = sprintf($status_fmt,
+                                   $print->{INDEX},
+                                   $print->{FILE},
+                                   $value);
+                       }
+                       else {
+                               $print = $highlighted || $print;
                        }
                        printf("%s%2d: %s", $chosen, $i+1, $print);
                        if (($opts->{LIST_FLAT}) &&
@@ -217,6 +319,12 @@ sub list_and_choose {
                }
                chomp $line;
                last if $line eq '';
+               if ($line eq '?') {
+                       $opts->{SINGLETON} ?
+                           singleton_prompt_help_cmd() :
+                           prompt_help_cmd();
+                       next TOPLOOP;
+               }
                for my $choice (split(/[\s,]+/, $line)) {
                        my $choose = 1;
                        my ($bottom, $top);
@@ -252,7 +360,7 @@ sub list_and_choose {
                                $chosen[$i] = $choose;
                        }
                }
-               last if ($opts->{IMMEDIATE});
+               last if ($opts->{IMMEDIATE} || $line eq '*');
        }
        for ($i = 0; $i < @stuff; $i++) {
                if ($chosen[$i]) {
@@ -262,6 +370,28 @@ sub list_and_choose {
        return @return;
 }
 
+sub singleton_prompt_help_cmd {
+       print <<\EOF ;
+Prompt help:
+1          - select a numbered item
+foo        - select item based on unique prefix
+           - (empty) select nothing
+EOF
+}
+
+sub prompt_help_cmd {
+       print <<\EOF ;
+Prompt help:
+1          - select a single item
+3-5        - select a range of items
+2-3,6-9    - select multiple ranges
+foo        - select item based on unique prefix
+-...       - unselect specified items
+*          - choose all items
+           - (empty) finish selecting
+EOF
+}
+
 sub status_cmd {
        list_and_choose({ LIST_ONLY => 1, HEADER => $status_head },
                        list_modified());
@@ -544,27 +674,36 @@ sub help_patch_cmd {
        print <<\EOF ;
 y - stage this hunk
 n - do not stage this hunk
-a - stage this and all the remaining hunks
-d - do not stage this hunk nor any of the remaining hunks
+a - stage this and all the remaining hunks in the file
+d - do not stage this hunk nor any of the remaining hunks in the file
 j - leave this hunk undecided, see next undecided hunk
 J - leave this hunk undecided, see next hunk
 k - leave this hunk undecided, see previous undecided hunk
 K - leave this hunk undecided, see previous hunk
 s - split the current hunk into smaller hunks
+? - print help
 EOF
 }
 
 sub patch_update_cmd {
-       my @mods = list_modified('file-only');
-       @mods = grep { !($_->{BINARY}) } @mods;
-       return if (!@mods);
+       my @mods = grep { !($_->{BINARY}) } list_modified('file-only');
+       my @them;
 
-       my ($it) = list_and_choose({ PROMPT => 'Patch update',
-                                    SINGLETON => 1,
-                                    IMMEDIATE => 1,
-                                    HEADER => $status_head, },
-                                  @mods);
-       patch_update_file($it->{VALUE}) if ($it);
+       if (!@mods) {
+               print STDERR "No changes.\n";
+               return 0;
+       }
+       if ($patch_mode) {
+               @them = @mods;
+       }
+       else {
+               @them = list_and_choose({ PROMPT => 'Patch update',
+                                         HEADER => $status_head, },
+                                       @mods);
+       }
+       for (@them) {
+               patch_update_file($_->{VALUE});
+       }
 }
 
 sub patch_update_file {
@@ -775,6 +914,20 @@ sub help_cmd {
 EOF
 }
 
+sub process_args {
+       return unless @ARGV;
+       my $arg = shift @ARGV;
+       if ($arg eq "--patch") {
+               $patch_mode = 1;
+               $arg = shift @ARGV or die "missing --";
+               die "invalid argument $arg, expecting --"
+                   unless $arg eq "--";
+       }
+       elsif ($arg ne "--") {
+               die "invalid argument $arg, expecting --";
+       }
+}
+
 sub main_loop {
        my @cmd = ([ 'status', \&status_cmd, ],
                   [ 'update', \&update_cmd, ],
@@ -803,6 +956,12 @@ sub main_loop {
        }
 }
 
+process_args();
 refresh();
-status_cmd();
-main_loop();
+if ($patch_mode) {
+       patch_update_cmd();
+}
+else {
+       status_cmd();
+       main_loop();
+}