зеркало из https://github.com/microsoft/git.git
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:
Коммит
ab7d707669
|
@ -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();
|
||||
|
|
2
commit.h
2
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)
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче