Merge branch 'jc/rm' into next

* jc/rm:
  git-add: add ignored files when asked explicitly.
  read_directory: show_both option.
  git-rm: Documentation
  t3600: update the test for updated git rm
  git-rm: update to saner semantics
  match_pathspec() -- return how well the spec matched
  git-add --interactive: add documentation
This commit is contained in:
Junio C Hamano
2006-12-25 03:29:13 -08:00
7 changed files with 396 additions and 79 deletions

View File

@@ -7,7 +7,7 @@ git-add - Add file contents to the changeset to be committed next
SYNOPSIS
--------
'git-add' [-n] [-v] [--] <file>...
'git-add' [-n] [-v] [--interactive] [--] <file>...
DESCRIPTION
-----------
@@ -25,8 +25,9 @@ the commit.
The 'git status' command can be used to obtain a summary of what is included
for the next commit.
This command only adds non-ignored files, to add ignored files use
"git update-index --add".
This command can be used to add ignored files, but they have to be
explicitly and exactly specified from the command line. File globbing
and recursive behaviour do not add ignored files.
Please see gitlink:git-commit[1] for alternative ways to add content to a
commit.
@@ -35,7 +36,11 @@ commit.
OPTIONS
-------
<file>...::
Files to add content from.
Files to add content from. Fileglobs (e.g. `*.c`) can
be given to add all matching files. Also a
leading directory name (e.g. `dir` to add `dir/file1`
and `dir/file2`) can be given to add all files in the
directory, recursively.
-n::
Don't actually add the file(s), just show if they exist.
@@ -43,6 +48,10 @@ OPTIONS
-v::
Be verbose.
\--interactive::
Add modified contents in the working tree interactively to
the index.
\--::
This option can be used to separate command-line options from
the list of files, (useful when filenames might be mistaken
@@ -67,6 +76,119 @@ git-add git-*.sh::
(i.e. you are listing the files explicitly), it does not
consider `subdir/git-foo.sh`.
Interactive mode
----------------
When the command enters the interactive mode, it shows the
output of the 'status' subcommand, and then goes into ints
interactive command loop.
The command loop shows the list of subcommands available, and
gives a prompt "What now> ". In general, when the prompt ends
with a single '>', you can pick only one of the choices given
and type return, like this:
------------
*** Commands ***
1: status 2: update 3: revert 4: add untracked
5: patch 6: diff 7: quit 8: help
What now> 1
------------
You also could say "s" or "sta" or "status" above as long as the
choice is unique.
The main command loop has 6 subcommands (plus help and quit).
status::
This shows the change between HEAD and index (i.e. what will be
committed if you say "git commit"), and between index and
working tree files (i.e. what you could stage further before
"git commit" using "git-add") for each path. A sample output
looks like this:
+
------------
staged unstaged path
1: binary nothing foo.png
2: +403/-35 +1/-1 git-add--interactive.perl
------------
+
It shows that foo.png has differences from HEAD (but that is
binary so line count cannot be shown) and there is no
difference between indexed copy and the working tree
version (if the working tree version were also different,
'binary' would have been shown in place of 'nothing'). The
other file, git-add--interactive.perl, has 403 lines added
and 35 lines deleted if you commit what is in the index, but
working tree file has further modifications (one addition and
one deletion).
update::
This shows the status information and gives prompt
"Update>>". When the prompt ends with double '>>', you can
make more than one selection, concatenated with whitespace or
comma. Also you can say ranges. E.g. "2-5 7,9" to choose
2,3,4,5,7,9 from the list. You can say '*' to choose
everything.
+
What you chose are then highlighted with '*',
like this:
+
------------
staged unstaged path
1: binary nothing foo.png
* 2: +403/-35 +1/-1 git-add--interactive.perl
------------
+
To remove selection, prefix the input with `-`
like this:
+
------------
Update>> -2
------------
+
After making the selection, answer with an empty line to stage the
contents of working tree files for selected paths in the index.
revert::
This has a very similar UI to 'update', and the staged
information for selected paths are reverted to that of the
HEAD version. Reverting new paths makes them untracked.
add untracked::
This has a very similar UI to 'update' and
'revert', and lets you add untracked paths to the index.
patch::
This lets you choose one path out of 'status' like selection.
After choosing the path, it presents diff between the index
and the working tree file and asks you if you want to stage
the change of each hunk. You can say:
y - add the change from that hunk to index
n - do not add the change from that hunk to index
a - add the change from that hunk and all the rest to index
d - do not the change from that hunk nor any of the rest to index
j - do not decide on this hunk now, and view the next
undecided hunk
J - do not decide on this hunk now, and view the next hunk
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
+
After deciding the fate for all hunks, if there is any hunk
that was chosen, the index is updated with the selected hunks.
diff::
This lets you review what will be committed (i.e. between
HEAD and index).
See Also
--------
gitlink:git-status[1]

View File

@@ -7,51 +7,54 @@ git-rm - Remove files from the working tree and from the index
SYNOPSIS
--------
'git-rm' [-f] [-n] [-v] [--] <file>...
'git-rm' [-f] [-n] [-r] [--cached] [--] <file>...
DESCRIPTION
-----------
A convenience wrapper for git-update-index --remove. For those coming
from cvs, git-rm provides an operation similar to "cvs rm" or "cvs
remove".
Remove files from the working tree and from the index. The
files have to be identical to the tip of the branch, and no
updates to its contents must have been placed in the staging
area (aka index).
OPTIONS
-------
<file>...::
Files to remove from the index and optionally, from the
working tree as well.
Files to remove. Fileglobs (e.g. `*.c`) can be given to
remove all matching files. Also a leading directory name
(e.g. `dir` to add `dir/file1` and `dir/file2`) can be
given to remove all files in the directory, recursively,
but this requires `-r` option to be given for safety.
-f::
Remove files from the working tree as well as from the index.
Override the up-to-date check.
-n::
Don't actually remove the file(s), just show if they exist in
the index.
-v::
Be verbose.
-r::
Allow recursive removal when a leading directory name is
given.
\--::
This option can be used to separate command-line options from
the list of files, (useful when filenames might be mistaken
for command-line options).
\--cached::
This option can be used to tell the command to remove
the paths only from the index, leaving working tree
files.
DISCUSSION
----------
The list of <file> given to the command is fed to `git-ls-files`
command to list files that are registered in the index and
are not ignored/excluded by `$GIT_DIR/info/exclude` file or
`.gitignore` file in each directory. This means two things:
. You can put the name of a directory on the command line, and the
command will remove all files in it and its subdirectories (the
directories themselves are never removed from the working tree);
. Giving the name of a file that is not in the index does not
remove that file.
The list of <file> given to the command can be exact pathnames,
file glob patterns, or leading directory name. The command
removes only the paths that is known to git. Giving the name of
a file that you have not told git about does not remove that file.
EXAMPLES
@@ -69,10 +72,10 @@ subdirectories of `Documentation/` directory.
git-rm -f git-*.sh::
Remove all git-*.sh scripts that are in the index. The files
are removed from the index, and (because of the -f option),
from the working tree as well. Because this example lets the
shell expand the asterisk (i.e. you are listing the files
explicitly), it does not remove `subdir/git-foo.sh`.
are removed from the index, and from the working
tree. Because this example lets the shell expand the
asterisk (i.e. you are listing the files explicitly), it
does not remove `subdir/git-foo.sh`.
See Also
--------

View File

@@ -10,7 +10,7 @@
#include "cache-tree.h"
static const char builtin_add_usage[] =
"git-add [-n] [-v] <filepattern>...";
"git-add [-n] [-v] [--interactive] [--] <filepattern>...";
static void prune_directory(struct dir_struct *dir, const char **pathspec, int prefix)
{
@@ -26,7 +26,14 @@ static void prune_directory(struct dir_struct *dir, const char **pathspec, int p
i = dir->nr;
while (--i >= 0) {
struct dir_entry *entry = *src++;
if (!match_pathspec(pathspec, entry->name, entry->len, prefix, seen)) {
int how = match_pathspec(pathspec, entry->name, entry->len,
prefix, seen);
/*
* ignored entries can be added with exact match,
* but not with glob nor recursive.
*/
if (!how ||
(entry->ignored_entry && how != MATCHED_EXACTLY)) {
free(entry);
continue;
}
@@ -55,6 +62,8 @@ static void fill_directory(struct dir_struct *dir, const char **pathspec)
/* Set up the default git porcelain excludes */
memset(dir, 0, sizeof(*dir));
if (pathspec)
dir->show_both = 1;
dir->exclude_per_dir = ".gitignore";
path = git_path("info/exclude");
if (!access(path, R_OK))

View File

@@ -7,9 +7,10 @@
#include "builtin.h"
#include "dir.h"
#include "cache-tree.h"
#include "tree-walk.h"
static const char builtin_rm_usage[] =
"git-rm [-n] [-v] [-f] <filepattern>...";
"git-rm [-n] [-f] [--cached] <filepattern>...";
static struct {
int nr, alloc;
@@ -41,12 +42,75 @@ static int remove_file(const char *name)
return ret;
}
static int check_local_mod(unsigned char *head)
{
/* items in list are already sorted in the cache order,
* so we could do this a lot more efficiently by using
* tree_desc based traversal if we wanted to, but I am
* lazy, and who cares if removal of files is a tad
* slower than the theoretical maximum speed?
*/
int i, no_head;
int errs = 0;
no_head = is_null_sha1(head);
for (i = 0; i < list.nr; i++) {
struct stat st;
int pos;
struct cache_entry *ce;
const char *name = list.name[i];
unsigned char sha1[20];
unsigned mode;
pos = cache_name_pos(name, strlen(name));
if (pos < 0)
continue; /* removing unmerged entry */
ce = active_cache[pos];
if (lstat(ce->name, &st) < 0) {
if (errno != ENOENT)
fprintf(stderr, "warning: '%s': %s",
ce->name, strerror(errno));
/* It already vanished from the working tree */
continue;
}
else if (S_ISDIR(st.st_mode)) {
/* if a file was removed and it is now a
* directory, that is the same as ENOENT as
* far as git is concerned; we do not track
* directories.
*/
continue;
}
if (ce_match_stat(ce, &st, 0))
errs = error("'%s' has local modifications "
"(hint: try -f)", ce->name);
if (no_head)
continue;
/*
* It is Ok to remove a newly added path, as long as
* it is cache-clean.
*/
if (get_tree_entry(head, name, sha1, &mode))
continue;
/*
* Otherwise make sure the version from the HEAD
* matches the index.
*/
if (ce->ce_mode != create_ce_mode(mode) ||
hashcmp(ce->sha1, sha1))
errs = error("'%s' has changes staged in the index "
"(hint: try -f)", name);
}
return errs;
}
static struct lock_file lock_file;
int cmd_rm(int argc, const char **argv, const char *prefix)
{
int i, newfd;
int verbose = 0, show_only = 0, force = 0;
int show_only = 0, force = 0, index_only = 0, recursive = 0;
const char **pathspec;
char *seen;
@@ -62,23 +126,20 @@ int cmd_rm(int argc, const char **argv, const char *prefix)
if (*arg != '-')
break;
if (!strcmp(arg, "--")) {
else if (!strcmp(arg, "--")) {
i++;
break;
}
if (!strcmp(arg, "-n")) {
else if (!strcmp(arg, "-n"))
show_only = 1;
continue;
}
if (!strcmp(arg, "-v")) {
verbose = 1;
continue;
}
if (!strcmp(arg, "-f")) {
else if (!strcmp(arg, "--cached"))
index_only = 1;
else if (!strcmp(arg, "-f"))
force = 1;
continue;
}
usage(builtin_rm_usage);
else if (!strcmp(arg, "-r"))
recursive = 1;
else
usage(builtin_rm_usage);
}
if (argc <= i)
usage(builtin_rm_usage);
@@ -99,14 +160,36 @@ int cmd_rm(int argc, const char **argv, const char *prefix)
if (pathspec) {
const char *match;
for (i = 0; (match = pathspec[i]) != NULL ; i++) {
if (*match && !seen[i])
die("pathspec '%s' did not match any files", match);
if (!seen[i])
die("pathspec '%s' did not match any files",
match);
if (!recursive && seen[i] == MATCHED_RECURSIVELY)
die("not removing '%s' recursively without -r",
*match ? match : ".");
}
}
/*
* If not forced, the file, the index and the HEAD (if exists)
* must match; but the file can already been removed, since
* this sequence is a natural "novice" way:
*
* rm F; git fm F
*
* Further, if HEAD commit exists, "diff-index --cached" must
* report no changes unless forced.
*/
if (!force) {
unsigned char sha1[20];
if (get_sha1("HEAD", sha1))
hashclr(sha1);
if (check_local_mod(sha1))
exit(1);
}
/*
* First remove the names from the index: we won't commit
* the index unless all of them succeed
* the index unless all of them succeed.
*/
for (i = 0; i < list.nr; i++) {
const char *path = list.name[i];
@@ -121,14 +204,14 @@ int cmd_rm(int argc, const char **argv, const char *prefix)
return 0;
/*
* Then, if we used "-f", remove the filenames from the
* workspace. If we fail to remove the first one, we
* Then, unless we used "--cache", remove the filenames from
* the workspace. If we fail to remove the first one, we
* abort the "git rm" (but once we've successfully removed
* any file at all, we'll go ahead and commit to it all:
* by then we've already committed ourselves and can't fail
* in the middle)
*/
if (force) {
if (!index_only) {
int removed = 0;
for (i = 0; i < list.nr; i++) {
const char *path = list.name[i];

70
dir.c
View File

@@ -40,6 +40,18 @@ int common_prefix(const char **pathspec)
return prefix;
}
/*
* Does 'match' matches the given name?
* A match is found if
*
* (1) the 'match' string is leading directory of 'name', or
* (2) the 'match' string is a wildcard and matches 'name', or
* (3) the 'match' string is exactly the same as 'name'.
*
* and the return value tells which case it was.
*
* It returns 0 when there is no match.
*/
static int match_one(const char *match, const char *name, int namelen)
{
int matchlen;
@@ -47,27 +59,30 @@ static int match_one(const char *match, const char *name, int namelen)
/* If the match was just the prefix, we matched */
matchlen = strlen(match);
if (!matchlen)
return 1;
return MATCHED_RECURSIVELY;
/*
* If we don't match the matchstring exactly,
* we need to match by fnmatch
*/
if (strncmp(match, name, matchlen))
return !fnmatch(match, name, 0);
return !fnmatch(match, name, 0) ? MATCHED_FNMATCH : 0;
/*
* If we did match the string exactly, we still
* need to make sure that it happened on a path
* component boundary (ie either the last character
* of the match was '/', or the next character of
* the name was '/' or the terminating NUL.
*/
return match[matchlen-1] == '/' ||
name[matchlen] == '/' ||
!name[matchlen];
if (!name[matchlen])
return MATCHED_EXACTLY;
if (match[matchlen-1] == '/' || name[matchlen] == '/')
return MATCHED_RECURSIVELY;
return 0;
}
/*
* Given a name and a list of pathspecs, see if the name matches
* any of the pathspecs. The caller is also interested in seeing
* all pathspec matches some names it calls this function with
* (otherwise the user could have mistyped the unmatched pathspec),
* and a mark is left in seen[] array for pathspec element that
* actually matched anything.
*/
int match_pathspec(const char **pathspec, const char *name, int namelen, int prefix, char *seen)
{
int retval;
@@ -77,12 +92,16 @@ int match_pathspec(const char **pathspec, const char *name, int namelen, int pre
namelen -= prefix;
for (retval = 0; (match = *pathspec++) != NULL; seen++) {
if (retval & *seen)
int how;
if (retval && *seen == MATCHED_EXACTLY)
continue;
match += prefix;
if (match_one(match, name, namelen)) {
retval = 1;
*seen = 1;
how = match_one(match, name, namelen);
if (how) {
if (retval < how)
retval = how;
if (*seen < how)
*seen = how;
}
}
return retval;
@@ -241,7 +260,8 @@ int excluded(struct dir_struct *dir, const char *pathname)
return 0;
}
static void add_name(struct dir_struct *dir, const char *pathname, int len)
static void add_name(struct dir_struct *dir, const char *pathname, int len,
int ignored_entry)
{
struct dir_entry *ent;
@@ -254,6 +274,7 @@ static void add_name(struct dir_struct *dir, const char *pathname, int len)
dir->entries = xrealloc(dir->entries, alloc*sizeof(ent));
}
ent = xmalloc(sizeof(*ent) + len + 1);
ent->ignored_entry = ignored_entry;
ent->len = len;
memcpy(ent->name, pathname, len);
ent->name[len] = 0;
@@ -295,6 +316,7 @@ static int read_directory_recursive(struct dir_struct *dir, const char *path, co
while ((de = readdir(fdir)) != NULL) {
int len;
int ignored_entry;
if ((de->d_name[0] == '.') &&
(de->d_name[1] == 0 ||
@@ -303,11 +325,12 @@ static int read_directory_recursive(struct dir_struct *dir, const char *path, co
continue;
len = strlen(de->d_name);
memcpy(fullname + baselen, de->d_name, len+1);
if (excluded(dir, fullname) != dir->show_ignored) {
if (!dir->show_ignored || DTYPE(de) != DT_DIR) {
continue;
}
}
ignored_entry = excluded(dir, fullname);
if (!dir->show_both &&
(ignored_entry != dir->show_ignored) &&
(!dir->show_ignored || DTYPE(de) != DT_DIR))
continue;
switch (DTYPE(de)) {
struct stat st;
@@ -345,7 +368,8 @@ static int read_directory_recursive(struct dir_struct *dir, const char *path, co
if (check_only)
goto exit_early;
else
add_name(dir, fullname, baselen + len);
add_name(dir, fullname, baselen + len,
ignored_entry);
}
exit_early:
closedir(fdir);

10
dir.h
View File

@@ -13,7 +13,8 @@
struct dir_entry {
int len;
unsigned ignored_entry : 1;
unsigned int len : 15;
char name[FLEX_ARRAY]; /* more */
};
@@ -29,7 +30,8 @@ struct exclude_list {
struct dir_struct {
int nr, alloc;
unsigned int show_ignored:1,
unsigned int show_both: 1,
show_ignored:1,
show_other_directories:1,
hide_empty_directories:1;
struct dir_entry **entries;
@@ -40,6 +42,10 @@ struct dir_struct {
};
extern int common_prefix(const char **pathspec);
#define MATCHED_RECURSIVELY 1
#define MATCHED_FNMATCH 2
#define MATCHED_EXACTLY 3
extern int match_pathspec(const char **pathspec, const char *name, int namelen, int prefix, char *seen);
extern int read_directory(struct dir_struct *, const char *path, const char *base, int baselen);

View File

@@ -43,19 +43,19 @@ test_expect_success \
test_expect_success \
'Test that git-rm foo succeeds' \
'git-rm foo'
'git-rm --cached foo'
test_expect_success \
'Post-check that foo exists but is not in index after git-rm foo' \
'[ -f foo ] && ! git-ls-files --error-unmatch foo'
test_expect_success \
'Pre-check that bar exists and is in index before "git-rm -f bar"' \
'Pre-check that bar exists and is in index before "git-rm bar"' \
'[ -f bar ] && git-ls-files --error-unmatch bar'
test_expect_success \
'Test that "git-rm -f bar" succeeds' \
'git-rm -f bar'
'Test that "git-rm bar" succeeds' \
'git-rm bar'
test_expect_success \
'Post-check that bar does not exist and is not in index after "git-rm -f bar"' \
@@ -84,4 +84,74 @@ test_expect_success \
'When the rm in "git-rm -f" fails, it should not remove the file from the index' \
'git-ls-files --error-unmatch baz'
# Now, failure cases.
test_expect_success 'Re-add foo and baz' '
git add foo baz &&
git ls-files --error-unmatch foo baz
'
test_expect_success 'Modify foo -- rm should refuse' '
echo >>foo &&
! git rm foo baz &&
test -f foo &&
test -f baz &&
git ls-files --error-unmatch foo baz
'
test_expect_success 'Modified foo -- rm -f should work' '
git rm -f foo baz &&
test ! -f foo &&
test ! -f baz &&
! git ls-files --error-unmatch foo &&
! git ls-files --error-unmatch bar
'
test_expect_success 'Re-add foo and baz for HEAD tests' '
echo frotz >foo &&
git checkout HEAD -- baz &&
git add foo baz &&
git ls-files --error-unmatch foo baz
'
test_expect_success 'foo is different in index from HEAD -- rm should refuse' '
! git rm foo baz &&
test -f foo &&
test -f baz &&
git ls-files --error-unmatch foo baz
'
test_expect_success 'but with -f it should work.' '
git rm -f foo baz &&
test ! -f foo &&
test ! -f baz &&
! git ls-files --error-unmatch foo
! git ls-files --error-unmatch baz
'
test_expect_success 'Recursive test setup' '
mkdir -p frotz &&
echo qfwfq >frotz/nitfol &&
git add frotz &&
git commit -m "subdir test"
'
test_expect_success 'Recursive without -r fails' '
! git rm frotz &&
test -d frotz &&
test -f frotz/nitfol
'
test_expect_success 'Recursive with -r but dirty' '
echo qfwfq >>frotz/nitfol
! git rm -r frotz &&
test -d frotz &&
test -f frotz/nitfol
'
test_expect_success 'Recursive with -r -f' '
git rm -f -r frotz &&
! test -f frotz/nitfol &&
! test -d frotz
'
test_done