diff options
| -rw-r--r-- | Documentation/git-add.txt | 17 | ||||
| -rw-r--r-- | Documentation/git-rm.txt | 51 | ||||
| -rw-r--r-- | builtin-add.c | 42 | ||||
| -rw-r--r-- | builtin-rm.c | 123 | ||||
| -rw-r--r-- | dir.c | 70 | ||||
| -rw-r--r-- | dir.h | 10 | ||||
| -rwxr-xr-x | t/t3600-rm.sh | 78 |
7 files changed, 311 insertions, 80 deletions
diff --git a/Documentation/git-add.txt b/Documentation/git-add.txt index 8710b3a75e..95bea66374 100644 --- a/Documentation/git-add.txt +++ b/Documentation/git-add.txt @@ -7,7 +7,7 @@ git-add - Add file contents to the changeset to be committed next SYNOPSIS -------- -'git-add' [-n] [-v] [--interactive] [--] <file>... +'git-add' [-n] [-v] [-f] [--interactive] [--] <file>... DESCRIPTION ----------- @@ -25,8 +25,10 @@ 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 with `-f` (force) +option, 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 +37,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 +49,9 @@ OPTIONS -v:: Be verbose. +-f:: + Allow adding otherwise ignored files. + \--interactive:: Add modified contents in the working tree interactively to the index. diff --git a/Documentation/git-rm.txt b/Documentation/git-rm.txt index 66fc478f57..3a8f279e1a 100644 --- a/Documentation/git-rm.txt +++ b/Documentation/git-rm.txt @@ -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 -------- diff --git a/builtin-add.c b/builtin-add.c index 17641b433d..8ed4a6a9f3 100644 --- a/builtin-add.c +++ b/builtin-add.c @@ -10,7 +10,7 @@ #include "cache-tree.h" static const char builtin_add_usage[] = -"git-add [-n] [-v] [--interactive] [--] <filepattern>..."; +"git-add [-n] [-v] [-f] [--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)) @@ -82,10 +91,13 @@ static void fill_directory(struct dir_struct *dir, const char **pathspec) static struct lock_file lock_file; +static const char ignore_warning[] = +"The following paths are ignored by one of your .gitignore files:\n"; + int cmd_add(int argc, const char **argv, const char *prefix) { int i, newfd; - int verbose = 0, show_only = 0; + int verbose = 0, show_only = 0, ignored_too = 0; const char **pathspec; struct dir_struct dir; int add_interactive = 0; @@ -120,6 +132,10 @@ int cmd_add(int argc, const char **argv, const char *prefix) show_only = 1; continue; } + if (!strcmp(arg, "-f")) { + ignored_too = 1; + continue; + } if (!strcmp(arg, "-v")) { verbose = 1; continue; @@ -138,6 +154,8 @@ int cmd_add(int argc, const char **argv, const char *prefix) if (show_only) { const char *sep = "", *eof = ""; for (i = 0; i < dir.nr; i++) { + if (!ignored_too && dir.entries[i]->ignored_entry) + continue; printf("%s%s", sep, dir.entries[i]->name); sep = " "; eof = "\n"; @@ -149,6 +167,24 @@ int cmd_add(int argc, const char **argv, const char *prefix) if (read_cache() < 0) die("index file corrupt"); + if (!ignored_too) { + int has_ignored = -1; + for (i = 0; has_ignored < 0 && i < dir.nr; i++) + if (dir.entries[i]->ignored_entry) + has_ignored = i; + if (0 <= has_ignored) { + fprintf(stderr, ignore_warning); + for (i = has_ignored; i < dir.nr; i++) { + if (!dir.entries[i]->ignored_entry) + continue; + fprintf(stderr, "%s\n", dir.entries[i]->name); + } + fprintf(stderr, + "Use -f if you really want to add them.\n"); + exit(1); + } + } + for (i = 0; i < dir.nr; i++) add_file_to_index(dir.entries[i]->name, verbose); diff --git a/builtin-rm.c b/builtin-rm.c index 33d04bd015..5b078c4194 100644 --- a/builtin-rm.c +++ b/builtin-rm.c @@ -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]; @@ -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); @@ -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); diff --git a/t/t3600-rm.sh b/t/t3600-rm.sh index 201d1642da..e31cf93a00 100755 --- a/t/t3600-rm.sh +++ b/t/t3600-rm.sh @@ -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 |
