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