Merge branch 'wc/add-i'

* 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
This commit is contained in:
Junio C Hamano 2007-12-04 21:38:28 -08:00
commit ab7d707669
5 changed files with 242 additions and 46 deletions

View File

@ -61,7 +61,14 @@ OPTIONS
-i, \--interactive:: -i, \--interactive::
Add modified contents in the working tree interactively to 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:: -u::
Update only files that git already knows about. This is similar 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 k - do not decide on this hunk now, and view the previous
undecided hunk undecided hunk
K - do not decide on this hunk now, and view the previous 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 After deciding the fate for all hunks, if there is any hunk
that was chosen, the index is updated with the selected hunks. that was chosen, the index is updated with the selected hunks.

View File

@ -19,7 +19,7 @@ static const char * const builtin_add_usage[] = {
"git-add [options] [--] <filepattern>...", "git-add [options] [--] <filepattern>...",
NULL NULL
}; };
static int patch_interactive = 0, add_interactive = 0;
static int take_worktree_changes; static int take_worktree_changes;
static void prune_directory(struct dir_struct *dir, const char **pathspec, int prefix) 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); 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; 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"; "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 verbose = 0, show_only = 0, ignored_too = 0, refresh_only = 0;
static int add_interactive = 0;
static struct option builtin_add_options[] = { static struct option builtin_add_options[] = {
OPT__DRY_RUN(&show_only), OPT__DRY_RUN(&show_only),
OPT__VERBOSE(&verbose), OPT__VERBOSE(&verbose),
OPT_GROUP(""), OPT_GROUP(""),
OPT_BOOLEAN('i', "interactive", &add_interactive, "interactive picking"), 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('f', NULL, &ignored_too, "allow adding otherwise ignored files"),
OPT_BOOLEAN('u', NULL, &take_worktree_changes, "update tracked files"), OPT_BOOLEAN('u', NULL, &take_worktree_changes, "update tracked files"),
OPT_BOOLEAN( 0 , "refresh", &refresh_only, "don't add, only refresh the index"), 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 cmd_add(int argc, const char **argv, const char *prefix)
{ {
int i, newfd, orig_argc = argc; int i, newfd;
const char **pathspec; const char **pathspec;
struct dir_struct dir; struct dir_struct dir;
argc = parse_options(argc, argv, builtin_add_options, argc = parse_options(argc, argv, builtin_add_options,
builtin_add_usage, 0); builtin_add_usage, 0);
if (add_interactive) { if (patch_interactive)
if (add_interactive != 1 || orig_argc != 2) add_interactive = 1;
die("add --interactive does not take any parameters"); if (add_interactive)
exit(interactive_add()); exit(interactive_add(argc, argv, prefix));
}
git_config(git_default_config); git_config(git_default_config);

View 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; int fd;
struct tree *tree; struct tree *tree;
@ -171,7 +171,7 @@ static char *prepare_index(const char **files, const char *prefix)
const char **pathspec = NULL; const char **pathspec = NULL;
if (interactive) { if (interactive) {
interactive_add(); interactive_add(argc, argv, prefix);
commit_style = COMMIT_AS_IS; commit_style = COMMIT_AS_IS;
return get_index_file(); return get_index_file();
} }
@ -179,8 +179,8 @@ static char *prepare_index(const char **files, const char *prefix)
if (read_cache() < 0) if (read_cache() < 0)
die("index file corrupt"); die("index file corrupt");
if (*files) if (*argv)
pathspec = get_pathspec(prefix, files); pathspec = get_pathspec(prefix, argv);
/* /*
* Non partial, non as-is commit. * 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); 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); 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); 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)) { if (!no_verify && run_hook(index_file, "pre-commit", NULL)) {
rollback_index_files(); rollback_index_files();

View File

@ -113,7 +113,7 @@ extern struct commit_list *get_shallow_commits(struct object_array *heads,
int in_merge_bases(struct commit *, struct commit **, int); 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); extern int rerere(void);
static inline int single_parent(struct commit *commit) static inline int single_parent(struct commit *commit)

View File

@ -2,6 +2,9 @@
use strict; use strict;
# command line options
my $patch_mode;
sub run_cmd_pipe { sub run_cmd_pipe {
if ($^O eq 'MSWin32') { if ($^O eq 'MSWin32') {
my @invalid = grep {m/[":*]/} @_; my @invalid = grep {m/[":*]/} @_;
@ -37,14 +40,13 @@ sub list_untracked {
chomp $_; 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_fmt = '%12s %12s %s';
my $status_head = sprintf($status_fmt, 'staged', 'unstaged', 'path'); my $status_head = sprintf($status_fmt, 'staged', 'unstaged', 'path');
# Returns list of hashes, contents of each of which are: # Returns list of hashes, contents of each of which are:
# PRINT: print message
# VALUE: pathname # VALUE: pathname
# BINARY: is a binary path # BINARY: is a binary path
# INDEX: is index different from HEAD? # INDEX: is index different from HEAD?
@ -56,9 +58,17 @@ sub list_modified {
my ($only) = @_; my ($only) = @_;
my (%data, @return); my (%data, @return);
my ($add, $del, $adddel, $file); 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 for (run_cmd_pipe(qw(git diff-index --cached
--numstat --summary HEAD))) { --numstat --summary HEAD --), @tracked)) {
if (($add, $del, $file) = if (($add, $del, $file) =
/^([-\d]+) ([-\d]+) (.*)/) { /^([-\d]+) ([-\d]+) (.*)/) {
my ($change, $bin); 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) = if (($add, $del, $file) =
/^([-\d]+) ([-\d]+) (.*)/) { /^([-\d]+) ([-\d]+) (.*)/) {
if (!exists $data{$file}) { if (!exists $data{$file}) {
@ -122,8 +132,6 @@ sub list_modified {
} }
push @return, +{ push @return, +{
VALUE => $_, VALUE => $_,
PRINT => (sprintf $status_fmt,
$it->{INDEX}, $it->{FILE}, $_),
%$it, %$it,
}; };
} }
@ -159,10 +167,96 @@ sub find_unique {
return $found; 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 { sub list_and_choose {
my ($opts, @stuff) = @_; my ($opts, @stuff) = @_;
my (@chosen, @return); my (@chosen, @return);
my $i; my $i;
my @prefixes = find_unique_prefixes(@stuff) unless $opts->{LIST_ONLY};
TOPLOOP: TOPLOOP:
while (1) { while (1) {
@ -177,13 +271,21 @@ sub list_and_choose {
for ($i = 0; $i < @stuff; $i++) { for ($i = 0; $i < @stuff; $i++) {
my $chosen = $chosen[$i] ? '*' : ' '; my $chosen = $chosen[$i] ? '*' : ' ';
my $print = $stuff[$i]; my $print = $stuff[$i];
if (ref $print) { my $ref = ref $print;
if ((ref $print) eq 'ARRAY') { my $highlighted = highlight_prefix(@{$prefixes[$i]})
$print = $print->[0]; 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 { else {
$print = $print->{PRINT}; $print = $highlighted || $print;
}
} }
printf("%s%2d: %s", $chosen, $i+1, $print); printf("%s%2d: %s", $chosen, $i+1, $print);
if (($opts->{LIST_FLAT}) && if (($opts->{LIST_FLAT}) &&
@ -217,6 +319,12 @@ sub list_and_choose {
} }
chomp $line; chomp $line;
last if $line eq ''; last if $line eq '';
if ($line eq '?') {
$opts->{SINGLETON} ?
singleton_prompt_help_cmd() :
prompt_help_cmd();
next TOPLOOP;
}
for my $choice (split(/[\s,]+/, $line)) { for my $choice (split(/[\s,]+/, $line)) {
my $choose = 1; my $choose = 1;
my ($bottom, $top); my ($bottom, $top);
@ -252,7 +360,7 @@ sub list_and_choose {
$chosen[$i] = $choose; $chosen[$i] = $choose;
} }
} }
last if ($opts->{IMMEDIATE}); last if ($opts->{IMMEDIATE} || $line eq '*');
} }
for ($i = 0; $i < @stuff; $i++) { for ($i = 0; $i < @stuff; $i++) {
if ($chosen[$i]) { if ($chosen[$i]) {
@ -262,6 +370,28 @@ sub list_and_choose {
return @return; 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 { sub status_cmd {
list_and_choose({ LIST_ONLY => 1, HEADER => $status_head }, list_and_choose({ LIST_ONLY => 1, HEADER => $status_head },
list_modified()); list_modified());
@ -544,27 +674,36 @@ sub help_patch_cmd {
print <<\EOF ; print <<\EOF ;
y - stage this hunk y - stage this hunk
n - do not stage this hunk n - do not stage this hunk
a - stage this and all 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 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 undecided hunk
J - leave this hunk undecided, see next 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 undecided hunk
K - leave this hunk undecided, see previous hunk K - leave this hunk undecided, see previous hunk
s - split the current hunk into smaller hunks s - split the current hunk into smaller hunks
? - print help
EOF EOF
} }
sub patch_update_cmd { sub patch_update_cmd {
my @mods = list_modified('file-only'); my @mods = grep { !($_->{BINARY}) } list_modified('file-only');
@mods = grep { !($_->{BINARY}) } @mods; my @them;
return if (!@mods);
my ($it) = list_and_choose({ PROMPT => 'Patch update', if (!@mods) {
SINGLETON => 1, print STDERR "No changes.\n";
IMMEDIATE => 1, return 0;
}
if ($patch_mode) {
@them = @mods;
}
else {
@them = list_and_choose({ PROMPT => 'Patch update',
HEADER => $status_head, }, HEADER => $status_head, },
@mods); @mods);
patch_update_file($it->{VALUE}) if ($it); }
for (@them) {
patch_update_file($_->{VALUE});
}
} }
sub patch_update_file { sub patch_update_file {
@ -775,6 +914,20 @@ add untracked - add contents of untracked files to the staged set of changes
EOF 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 { sub main_loop {
my @cmd = ([ 'status', \&status_cmd, ], my @cmd = ([ 'status', \&status_cmd, ],
[ 'update', \&update_cmd, ], [ 'update', \&update_cmd, ],
@ -803,6 +956,12 @@ sub main_loop {
} }
} }
process_args();
refresh(); refresh();
if ($patch_mode) {
patch_update_cmd();
}
else {
status_cmd(); status_cmd();
main_loop(); main_loop();
}