* 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
Родитель 31cbb5d961 7e018be2ad
Коммит ab7d707669
5 изменённых файлов: 242 добавлений и 46 удалений

Просмотреть файл

@ -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.

Просмотреть файл

@ -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);

Просмотреть файл

@ -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();

Просмотреть файл

@ -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)

Просмотреть файл

@ -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 @@ add untracked - add contents of untracked files to the staged set of changes
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();
}