Merge branch 'rf/web'
* rf/web:
gitweb: Use rev-list --skip option.
gitweb: Change history action to use parse_commits.
gitweb: Change atom, rss actions to use parse_commits.
gitweb: Change header search action to use parse_commits.
gitweb: Change log action to use parse_commits.
gitweb: Change summary, shortlog actions to use parse_commits.
gitweb: We do longer need the --parents flag in rev-list.
gitweb: Add parse_commits, used to bulk load commit objects.
diff --git a/Documentation/git-add.txt b/Documentation/git-add.txt
index 8710b3a..95bea66 100644
--- a/Documentation/git-add.txt
+++ b/Documentation/git-add.txt
@@ -7,7 +7,7 @@
SYNOPSIS
--------
-'git-add' [-n] [-v] [--interactive] [--] <file>...
+'git-add' [-n] [-v] [-f] [--interactive] [--] <file>...
DESCRIPTION
-----------
@@ -25,8 +25,10 @@
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 @@
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 @@
-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 66fc478..3a8f279 100644
--- a/Documentation/git-rm.txt
+++ b/Documentation/git-rm.txt
@@ -7,51 +7,54 @@
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 @@
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 17641b4..8ed4a6a 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 @@
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 @@
/* 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 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 @@
show_only = 1;
continue;
}
+ if (!strcmp(arg, "-f")) {
+ ignored_too = 1;
+ continue;
+ }
if (!strcmp(arg, "-v")) {
verbose = 1;
continue;
@@ -138,6 +154,8 @@
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 @@
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 33d04bd..5b078c4 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 @@
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 @@
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 @@
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 @@
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];
diff --git a/dir.c b/dir.c
index 16401d8..dd188a8 100644
--- a/dir.c
+++ b/dir.c
@@ -40,6 +40,18 @@
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 @@
/* 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 @@
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 @@
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 @@
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 @@
while ((de = readdir(fdir)) != NULL) {
int len;
+ int ignored_entry;
if ((de->d_name[0] == '.') &&
(de->d_name[1] == 0 ||
@@ -303,11 +325,12 @@
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 @@
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);
diff --git a/dir.h b/dir.h
index 550551a..08c6345 100644
--- a/dir.h
+++ b/dir.h
@@ -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 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 @@
};
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 201d164..e31cf93 100755
--- a/t/t3600-rm.sh
+++ b/t/t3600-rm.sh
@@ -43,19 +43,19 @@
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 @@
'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