Merge branch 'jc/bm'

* jc/bm:
  Allow branch.*.merge to talk about remote tracking branches.
diff --git a/.gitignore b/.gitignore
index d706dd9..91e6966 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
 GIT-VERSION-FILE
 git
 git-add
+git-add--interactive
 git-am
 git-annotate
 git-apply
diff --git a/Documentation/git-add.txt b/Documentation/git-add.txt
index d86c0e7..95bea66 100644
--- a/Documentation/git-add.txt
+++ b/Documentation/git-add.txt
@@ -7,7 +7,7 @@
 
 SYNOPSIS
 --------
-'git-add' [-n] [-v] [--] <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,13 @@
 -v::
         Be verbose.
 
+-f::
+	Allow adding otherwise ignored files.
+
+\--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 +80,119 @@
 	(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]
diff --git a/Documentation/git-rev-list.txt b/Documentation/git-rev-list.txt
index ec43c0b..9e0dcf8 100644
--- a/Documentation/git-rev-list.txt
+++ b/Documentation/git-rev-list.txt
@@ -10,6 +10,7 @@
 --------
 [verse]
 'git-rev-list' [ \--max-count=number ]
+	     [ \--skip=number ]
 	     [ \--max-age=timestamp ]
 	     [ \--min-age=timestamp ]
 	     [ \--sparse ]
@@ -139,6 +140,10 @@
 
 	Limit the number of commits output.
 
+--skip='number'::
+
+	Skip 'number' commits before starting to show the commit output.
+
 --since='date', --after='date'::
 
 	Show commits more recent than a specific date.
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/Makefile b/Makefile
index 8827dee..475047f 100644
--- a/Makefile
+++ b/Makefile
@@ -170,6 +170,7 @@
 	git-lost-found.sh git-quiltimport.sh
 
 SCRIPT_PERL = \
+	git-add--interactive.perl \
 	git-archimport.perl git-cvsimport.perl git-relink.perl \
 	git-cvsserver.perl \
 	git-svnimport.perl git-cvsexportcommit.perl \
diff --git a/builtin-add.c b/builtin-add.c
index f306f82..8ed4a6a 100644
--- a/builtin-add.c
+++ b/builtin-add.c
@@ -6,10 +6,11 @@
 #include "cache.h"
 #include "builtin.h"
 #include "dir.h"
+#include "exec_cmd.h"
 #include "cache-tree.h"
 
 static const char builtin_add_usage[] =
-"git-add [-n] [-v] <filepattern>...";
+"git-add [-n] [-v] [-f] [--interactive] [--] <filepattern>...";
 
 static void prune_directory(struct dir_struct *dir, const char **pathspec, int prefix)
 {
@@ -25,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;
 		}
@@ -54,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))
@@ -81,12 +91,29 @@
 
 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;
+
+	for (i = 1; i < argc; i++) {
+		if (!strcmp("--interactive", argv[i]))
+			add_interactive++;
+	}
+	if (add_interactive) {
+		const char *args[] = { "add--interactive", NULL };
+
+		if (add_interactive != 1 || argc != 2)
+			die("add --interactive does not take any parameters");
+		execv_git_cmd(args);
+		exit(1);
+	}
 
 	git_config(git_default_config);
 
@@ -105,6 +132,10 @@
 			show_only = 1;
 			continue;
 		}
+		if (!strcmp(arg, "-f")) {
+			ignored_too = 1;
+			continue;
+		}
 		if (!strcmp(arg, "-v")) {
 			verbose = 1;
 			continue;
@@ -123,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";
@@ -134,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/git-add--interactive.perl b/git-add--interactive.perl
new file mode 100755
index 0000000..0057f86
--- /dev/null
+++ b/git-add--interactive.perl
@@ -0,0 +1,804 @@
+#!/usr/bin/perl -w
+
+
+use strict;
+
+sub run_cmd_pipe {
+	my $fh = undef;
+	open($fh, '-|', @_) or die;
+	return <$fh>;
+}
+
+my ($GIT_DIR) = run_cmd_pipe(qw(git rev-parse --git-dir));
+
+if (!defined $GIT_DIR) {
+	exit(1); # rev-parse would have already said "not a git repo"
+}
+chomp($GIT_DIR);
+
+sub refresh {
+	my $fh;
+	open $fh, '-|', qw(git update-index --refresh)
+	    or die;
+	while (<$fh>) {
+		;# ignore 'needs update'
+	}
+	close $fh;
+}
+
+sub list_untracked {
+	map {
+		chomp $_;
+		$_;
+	}
+	run_cmd_pipe(qw(git ls-files --others
+			--exclude-per-directory=.gitignore),
+		     "--exclude-from=$GIT_DIR/info/exclude",
+		     '--', @_);
+}
+
+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?
+# FILE:		is file different from index?
+# INDEX_ADDDEL:	is it add/delete between HEAD and index?
+# FILE_ADDDEL:	is it add/delete between index and file?
+
+sub list_modified {
+	my ($only) = @_;
+	my (%data, @return);
+	my ($add, $del, $adddel, $file);
+
+	for (run_cmd_pipe(qw(git diff-index --cached
+			     --numstat --summary HEAD))) {
+		if (($add, $del, $file) =
+		    /^([-\d]+)	([-\d]+)	(.*)/) {
+			my ($change, $bin);
+			if ($add eq '-' && $del eq '-') {
+				$change = 'binary';
+				$bin = 1;
+			}
+			else {
+				$change = "+$add/-$del";
+			}
+			$data{$file} = {
+				INDEX => $change,
+				BINARY => $bin,
+				FILE => 'nothing',
+			}
+		}
+		elsif (($adddel, $file) =
+		       /^ (create|delete) mode [0-7]+ (.*)$/) {
+			$data{$file}{INDEX_ADDDEL} = $adddel;
+		}
+	}
+
+	for (run_cmd_pipe(qw(git diff-files --numstat --summary))) {
+		if (($add, $del, $file) =
+		    /^([-\d]+)	([-\d]+)	(.*)/) {
+			if (!exists $data{$file}) {
+				$data{$file} = +{
+					INDEX => 'unchanged',
+					BINARY => 0,
+				};
+			}
+			my ($change, $bin);
+			if ($add eq '-' && $del eq '-') {
+				$change = 'binary';
+				$bin = 1;
+			}
+			else {
+				$change = "+$add/-$del";
+			}
+			$data{$file}{FILE} = $change;
+			if ($bin) {
+				$data{$file}{BINARY} = 1;
+			}
+		}
+		elsif (($adddel, $file) =
+		       /^ (create|delete) mode [0-7]+ (.*)$/) {
+			$data{$file}{FILE_ADDDEL} = $adddel;
+		}
+	}
+
+	for (sort keys %data) {
+		my $it = $data{$_};
+
+		if ($only) {
+			if ($only eq 'index-only') {
+				next if ($it->{INDEX} eq 'unchanged');
+			}
+			if ($only eq 'file-only') {
+				next if ($it->{FILE} eq 'nothing');
+			}
+		}
+		push @return, +{
+			VALUE => $_,
+			PRINT => (sprintf $status_fmt,
+				  $it->{INDEX}, $it->{FILE}, $_),
+			%$it,
+		};
+	}
+	return @return;
+}
+
+sub find_unique {
+	my ($string, @stuff) = @_;
+	my $found = undef;
+	for (my $i = 0; $i < @stuff; $i++) {
+		my $it = $stuff[$i];
+		my $hit = undef;
+		if (ref $it) {
+			if ((ref $it) eq 'ARRAY') {
+				$it = $it->[0];
+			}
+			else {
+				$it = $it->{VALUE};
+			}
+		}
+		eval {
+			if ($it =~ /^$string/) {
+				$hit = 1;
+			};
+		};
+		if (defined $hit && defined $found) {
+			return undef;
+		}
+		if ($hit) {
+			$found = $i + 1;
+		}
+	}
+	return $found;
+}
+
+sub list_and_choose {
+	my ($opts, @stuff) = @_;
+	my (@chosen, @return);
+	my $i;
+
+      TOPLOOP:
+	while (1) {
+		my $last_lf = 0;
+
+		if ($opts->{HEADER}) {
+			if (!$opts->{LIST_FLAT}) {
+				print "     ";
+			}
+			print "$opts->{HEADER}\n";
+		}
+		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};
+				}
+			}
+			printf("%s%2d: %s", $chosen, $i+1, $print);
+			if (($opts->{LIST_FLAT}) &&
+			    (($i + 1) % ($opts->{LIST_FLAT}))) {
+				print "\t";
+				$last_lf = 0;
+			}
+			else {
+				print "\n";
+				$last_lf = 1;
+			}
+		}
+		if (!$last_lf) {
+			print "\n";
+		}
+
+		return if ($opts->{LIST_ONLY});
+
+		print $opts->{PROMPT};
+		if ($opts->{SINGLETON}) {
+			print "> ";
+		}
+		else {
+			print ">> ";
+		}
+		my $line = <STDIN>;
+		last if (!$line);
+		chomp $line;
+		my $donesomething = 0;
+		for my $choice (split(/[\s,]+/, $line)) {
+			my $choose = 1;
+			my ($bottom, $top);
+
+			# Input that begins with '-'; unchoose
+			if ($choice =~ s/^-//) {
+				$choose = 0;
+			}
+			# A range can be specified like 5-7
+			if ($choice =~ /^(\d+)-(\d+)$/) {
+				($bottom, $top) = ($1, $2);
+			}
+			elsif ($choice =~ /^\d+$/) {
+				$bottom = $top = $choice;
+			}
+			elsif ($choice eq '*') {
+				$bottom = 1;
+				$top = 1 + @stuff;
+			}
+			else {
+				$bottom = $top = find_unique($choice, @stuff);
+				if (!defined $bottom) {
+					print "Huh ($choice)?\n";
+					next TOPLOOP;
+				}
+			}
+			if ($opts->{SINGLETON} && $bottom != $top) {
+				print "Huh ($choice)?\n";
+				next TOPLOOP;
+			}
+			for ($i = $bottom-1; $i <= $top-1; $i++) {
+				next if (@stuff <= $i);
+				$chosen[$i] = $choose;
+				$donesomething++;
+			}
+		}
+		last if (!$donesomething || $opts->{IMMEDIATE});
+	}
+	for ($i = 0; $i < @stuff; $i++) {
+		if ($chosen[$i]) {
+			push @return, $stuff[$i];
+		}
+	}
+	return @return;
+}
+
+sub status_cmd {
+	list_and_choose({ LIST_ONLY => 1, HEADER => $status_head },
+			list_modified());
+	print "\n";
+}
+
+sub say_n_paths {
+	my $did = shift @_;
+	my $cnt = scalar @_;
+	print "$did ";
+	if (1 < $cnt) {
+		print "$cnt paths\n";
+	}
+	else {
+		print "one path\n";
+	}
+}
+
+sub update_cmd {
+	my @mods = list_modified('file-only');
+	return if (!@mods);
+
+	my @update = list_and_choose({ PROMPT => 'Update',
+				       HEADER => $status_head, },
+				     @mods);
+	if (@update) {
+		system(qw(git update-index --add --),
+		       map { $_->{VALUE} } @update);
+		say_n_paths('updated', @update);
+	}
+	print "\n";
+}
+
+sub revert_cmd {
+	my @update = list_and_choose({ PROMPT => 'Revert',
+				       HEADER => $status_head, },
+				     list_modified());
+	if (@update) {
+		my @lines = run_cmd_pipe(qw(git ls-tree HEAD --),
+					 map { $_->{VALUE} } @update);
+		my $fh;
+		open $fh, '|-', qw(git update-index --index-info)
+		    or die;
+		for (@lines) {
+			print $fh $_;
+		}
+		close($fh);
+		for (@update) {
+			if ($_->{INDEX_ADDDEL} &&
+			    $_->{INDEX_ADDDEL} eq 'create') {
+				system(qw(git update-index --force-remove --),
+				       $_->{VALUE});
+				print "note: $_->{VALUE} is untracked now.\n";
+			}
+		}
+		refresh();
+		say_n_paths('reverted', @update);
+	}
+	print "\n";
+}
+
+sub add_untracked_cmd {
+	my @add = list_and_choose({ PROMPT => 'Add untracked' },
+				  list_untracked());
+	if (@add) {
+		system(qw(git update-index --add --), @add);
+		say_n_paths('added', @add);
+	}
+	print "\n";
+}
+
+sub parse_diff {
+	my ($path) = @_;
+	my @diff = run_cmd_pipe(qw(git diff-files -p --), $path);
+	my (@hunk) = { TEXT => [] };
+
+	for (@diff) {
+		if (/^@@ /) {
+			push @hunk, { TEXT => [] };
+		}
+		push @{$hunk[-1]{TEXT}}, $_;
+	}
+	return @hunk;
+}
+
+sub hunk_splittable {
+	my ($text) = @_;
+
+	my @s = split_hunk($text);
+	return (1 < @s);
+}
+
+sub parse_hunk_header {
+	my ($line) = @_;
+	my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
+	    $line =~ /^@@ -(\d+)(?:,(\d+)) \+(\d+)(?:,(\d+)) @@/;
+	return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
+}
+
+sub split_hunk {
+	my ($text) = @_;
+	my @split = ();
+
+	# If there are context lines in the middle of a hunk,
+	# it can be split, but we would need to take care of
+	# overlaps later.
+
+	my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = parse_hunk_header($text->[0]);
+	my $hunk_start = 1;
+	my $next_hunk_start;
+
+      OUTER:
+	while (1) {
+		my $next_hunk_start = undef;
+		my $i = $hunk_start - 1;
+		my $this = +{
+			TEXT => [],
+			OLD => $o_ofs,
+			NEW => $n_ofs,
+			OCNT => 0,
+			NCNT => 0,
+			ADDDEL => 0,
+			POSTCTX => 0,
+		};
+
+		while (++$i < @$text) {
+			my $line = $text->[$i];
+			if ($line =~ /^ /) {
+				if ($this->{ADDDEL} &&
+				    !defined $next_hunk_start) {
+					# We have seen leading context and
+					# adds/dels and then here is another
+					# context, which is trailing for this
+					# split hunk and leading for the next
+					# one.
+					$next_hunk_start = $i;
+				}
+				push @{$this->{TEXT}}, $line;
+				$this->{OCNT}++;
+				$this->{NCNT}++;
+				if (defined $next_hunk_start) {
+					$this->{POSTCTX}++;
+				}
+				next;
+			}
+
+			# add/del
+			if (defined $next_hunk_start) {
+				# We are done with the current hunk and
+				# this is the first real change for the
+				# next split one.
+				$hunk_start = $next_hunk_start;
+				$o_ofs = $this->{OLD} + $this->{OCNT};
+				$n_ofs = $this->{NEW} + $this->{NCNT};
+				$o_ofs -= $this->{POSTCTX};
+				$n_ofs -= $this->{POSTCTX};
+				push @split, $this;
+				redo OUTER;
+			}
+			push @{$this->{TEXT}}, $line;
+			$this->{ADDDEL}++;
+			if ($line =~ /^-/) {
+				$this->{OCNT}++;
+			}
+			else {
+				$this->{NCNT}++;
+			}
+		}
+
+		push @split, $this;
+		last;
+	}
+
+	for my $hunk (@split) {
+		$o_ofs = $hunk->{OLD};
+		$n_ofs = $hunk->{NEW};
+		$o_cnt = $hunk->{OCNT};
+		$n_cnt = $hunk->{NCNT};
+
+		my $head = ("@@ -$o_ofs" .
+			    (($o_cnt != 1) ? ",$o_cnt" : '') .
+			    " +$n_ofs" .
+			    (($n_cnt != 1) ? ",$n_cnt" : '') .
+			    " @@\n");
+		unshift @{$hunk->{TEXT}}, $head;
+	}
+	return map { $_->{TEXT} } @split;
+}
+
+sub find_last_o_ctx {
+	my ($it) = @_;
+	my $text = $it->{TEXT};
+	my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = parse_hunk_header($text->[0]);
+	my $i = @{$text};
+	my $last_o_ctx = $o_ofs + $o_cnt;
+	while (0 < --$i) {
+		my $line = $text->[$i];
+		if ($line =~ /^ /) {
+			$last_o_ctx--;
+			next;
+		}
+		last;
+	}
+	return $last_o_ctx;
+}
+
+sub merge_hunk {
+	my ($prev, $this) = @_;
+	my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) =
+	    parse_hunk_header($prev->{TEXT}[0]);
+	my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) =
+	    parse_hunk_header($this->{TEXT}[0]);
+
+	my (@line, $i, $ofs, $o_cnt, $n_cnt);
+	$ofs = $o0_ofs;
+	$o_cnt = $n_cnt = 0;
+	for ($i = 1; $i < @{$prev->{TEXT}}; $i++) {
+		my $line = $prev->{TEXT}[$i];
+		if ($line =~ /^\+/) {
+			$n_cnt++;
+			push @line, $line;
+			next;
+		}
+
+		last if ($o1_ofs <= $ofs);
+
+		$o_cnt++;
+		$ofs++;
+		if ($line =~ /^ /) {
+			$n_cnt++;
+		}
+		push @line, $line;
+	}
+
+	for ($i = 1; $i < @{$this->{TEXT}}; $i++) {
+		my $line = $this->{TEXT}[$i];
+		if ($line =~ /^\+/) {
+			$n_cnt++;
+			push @line, $line;
+			next;
+		}
+		$ofs++;
+		$o_cnt++;
+		if ($line =~ /^ /) {
+			$n_cnt++;
+		}
+		push @line, $line;
+	}
+	my $head = ("@@ -$o0_ofs" .
+		    (($o_cnt != 1) ? ",$o_cnt" : '') .
+		    " +$n0_ofs" .
+		    (($n_cnt != 1) ? ",$n_cnt" : '') .
+		    " @@\n");
+	@{$prev->{TEXT}} = ($head, @line);
+}
+
+sub coalesce_overlapping_hunks {
+	my (@in) = @_;
+	my @out = ();
+
+	my ($last_o_ctx);
+
+	for (grep { $_->{USE} } @in) {
+		my $text = $_->{TEXT};
+		my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
+		    parse_hunk_header($text->[0]);
+		if (defined $last_o_ctx &&
+		    $o_ofs <= $last_o_ctx) {
+			merge_hunk($out[-1], $_);
+		}
+		else {
+			push @out, $_;
+		}
+		$last_o_ctx = find_last_o_ctx($out[-1]);
+	}
+	return @out;
+}
+
+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
+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
+EOF
+}
+
+sub patch_update_cmd {
+	my @mods = list_modified('file-only');
+	@mods = grep { !($_->{BINARY}) } @mods;
+	return if (!@mods);
+
+	my ($it) = list_and_choose({ PROMPT => 'Patch update',
+				     SINGLETON => 1,
+				     IMMEDIATE => 1,
+				     HEADER => $status_head, },
+				   @mods);
+	return if (!$it);
+
+	my ($ix, $num);
+	my $path = $it->{VALUE};
+	my ($head, @hunk) = parse_diff($path);
+	for (@{$head->{TEXT}}) {
+		print;
+	}
+	$num = scalar @hunk;
+	$ix = 0;
+
+	while (1) {
+		my ($prev, $next, $other, $undecided, $i);
+		$other = '';
+
+		if ($num <= $ix) {
+			$ix = 0;
+		}
+		for ($i = 0; $i < $ix; $i++) {
+			if (!defined $hunk[$i]{USE}) {
+				$prev = 1;
+				$other .= '/k';
+				last;
+			}
+		}
+		if ($ix) {
+			$other .= '/K';
+		}
+		for ($i = $ix + 1; $i < $num; $i++) {
+			if (!defined $hunk[$i]{USE}) {
+				$next = 1;
+				$other .= '/j';
+				last;
+			}
+		}
+		if ($ix < $num - 1) {
+			$other .= '/J';
+		}
+		for ($i = 0; $i < $num; $i++) {
+			if (!defined $hunk[$i]{USE}) {
+				$undecided = 1;
+				last;
+			}
+		}
+		last if (!$undecided);
+
+		if (hunk_splittable($hunk[$ix]{TEXT})) {
+			$other .= '/s';
+		}
+		for (@{$hunk[$ix]{TEXT}}) {
+			print;
+		}
+		print "Stage this hunk [y/n/a/d$other/?]? ";
+		my $line = <STDIN>;
+		if ($line) {
+			if ($line =~ /^y/i) {
+				$hunk[$ix]{USE} = 1;
+			}
+			elsif ($line =~ /^n/i) {
+				$hunk[$ix]{USE} = 0;
+			}
+			elsif ($line =~ /^a/i) {
+				while ($ix < $num) {
+					if (!defined $hunk[$ix]{USE}) {
+						$hunk[$ix]{USE} = 1;
+					}
+					$ix++;
+				}
+				next;
+			}
+			elsif ($line =~ /^d/i) {
+				while ($ix < $num) {
+					if (!defined $hunk[$ix]{USE}) {
+						$hunk[$ix]{USE} = 0;
+					}
+					$ix++;
+				}
+				next;
+			}
+			elsif ($other =~ /K/ && $line =~ /^K/) {
+				$ix--;
+				next;
+			}
+			elsif ($other =~ /J/ && $line =~ /^J/) {
+				$ix++;
+				next;
+			}
+			elsif ($other =~ /k/ && $line =~ /^k/) {
+				while (1) {
+					$ix--;
+					last if (!$ix ||
+						 !defined $hunk[$ix]{USE});
+				}
+				next;
+			}
+			elsif ($other =~ /j/ && $line =~ /^j/) {
+				while (1) {
+					$ix++;
+					last if ($ix >= $num ||
+						 !defined $hunk[$ix]{USE});
+				}
+				next;
+			}
+			elsif ($other =~ /s/ && $line =~ /^s/) {
+				my @split = split_hunk($hunk[$ix]{TEXT});
+				if (1 < @split) {
+					print "Split into ",
+					scalar(@split), " hunks.\n";
+				}
+				splice(@hunk, $ix, 1,
+				       map { +{ TEXT => $_, USE => undef } }
+				       @split);
+				$num = scalar @hunk;
+				next;
+			}
+			else {
+				help_patch_cmd($other);
+				next;
+			}
+			# soft increment
+			while (1) {
+				$ix++;
+				last if ($ix >= $num ||
+					 !defined $hunk[$ix]{USE});
+			}
+		}
+	}
+
+	@hunk = coalesce_overlapping_hunks(@hunk);
+
+	my ($o_lofs, $n_lofs) = (0, 0);
+	my @result = ();
+	for (@hunk) {
+		my $text = $_->{TEXT};
+		my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
+		    parse_hunk_header($text->[0]);
+
+		if (!$_->{USE}) {
+			if (!defined $o_cnt) { $o_cnt = 1; }
+			if (!defined $n_cnt) { $n_cnt = 1; }
+
+			# We would have added ($n_cnt - $o_cnt) lines
+			# to the postimage if we were to use this hunk,
+			# but we didn't.  So the line number that the next
+			# hunk starts at would be shifted by that much.
+			$n_lofs -= ($n_cnt - $o_cnt);
+			next;
+		}
+		else {
+			if ($n_lofs) {
+				$n_ofs += $n_lofs;
+				$text->[0] = ("@@ -$o_ofs" .
+					      ((defined $o_cnt)
+					       ? ",$o_cnt" : '') .
+					      " +$n_ofs" .
+					      ((defined $n_cnt)
+					       ? ",$n_cnt" : '') .
+					      " @@\n");
+			}
+			for (@$text) {
+				push @result, $_;
+			}
+		}
+	}
+
+	if (@result) {
+		my $fh;
+
+		open $fh, '|-', qw(git apply --cached);
+		for (@{$head->{TEXT}}, @result) {
+			print $fh $_;
+		}
+		if (!close $fh) {
+			for (@{$head->{TEXT}}, @result) {
+				print STDERR $_;
+			}
+		}
+		refresh();
+	}
+
+	print "\n";
+}
+
+sub diff_cmd {
+	my @mods = list_modified('index-only');
+	@mods = grep { !($_->{BINARY}) } @mods;
+	return if (!@mods);
+	my (@them) = list_and_choose({ PROMPT => 'Review diff',
+				     IMMEDIATE => 1,
+				     HEADER => $status_head, },
+				   @mods);
+	return if (!@them);
+	system(qw(git diff-index -p --cached HEAD --),
+	       map { $_->{VALUE} } @them);
+}
+
+sub quit_cmd {
+	print "Bye.\n";
+	exit(0);
+}
+
+sub help_cmd {
+	print <<\EOF ;
+status        - show paths with changes
+update        - add working tree state to the staged set of changes
+revert        - revert staged set of changes back to the HEAD version
+patch         - pick hunks and update selectively
+diff	      - view diff between HEAD and index
+add untracked - add contents of untracked files to the staged set of changes
+EOF
+}
+
+sub main_loop {
+	my @cmd = ([ 'status', \&status_cmd, ],
+		   [ 'update', \&update_cmd, ],
+		   [ 'revert', \&revert_cmd, ],
+		   [ 'add untracked', \&add_untracked_cmd, ],
+		   [ 'patch', \&patch_update_cmd, ],
+		   [ 'diff', \&diff_cmd, ],
+		   [ 'quit', \&quit_cmd, ],
+		   [ 'help', \&help_cmd, ],
+	);
+	while (1) {
+		my ($it) = list_and_choose({ PROMPT => 'What now',
+					     SINGLETON => 1,
+					     LIST_FLAT => 4,
+					     HEADER => '*** Commands ***',
+					     IMMEDIATE => 1 }, @cmd);
+		if ($it) {
+			eval {
+				$it->[1]->();
+			};
+			if ($@) {
+				print "$@";
+			}
+		}
+	}
+}
+
+my @z;
+
+refresh();
+status_cmd();
+main_loop();
diff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl
index d01d689..65fcdb0 100755
--- a/gitweb/gitweb.perl
+++ b/gitweb/gitweb.perl
@@ -1270,36 +1270,25 @@
 	return %tag
 }
 
-sub parse_commit {
-	my $commit_id = shift;
-	my $commit_text = shift;
-
-	my @commit_lines;
+sub parse_commit_text {
+	my ($commit_text) = @_;
+	my @commit_lines = split '\n', $commit_text;
 	my %co;
 
-	if (defined $commit_text) {
-		@commit_lines = @$commit_text;
-	} else {
-		local $/ = "\0";
-		open my $fd, "-|", git_cmd(), "rev-list",
-			"--header", "--parents", "--max-count=1",
-			$commit_id, "--"
-			or return;
-		@commit_lines = split '\n', <$fd>;
-		close $fd or return;
-		pop @commit_lines;
-	}
+	pop @commit_lines; # Remove '\0'
+
 	my $header = shift @commit_lines;
 	if (!($header =~ m/^[0-9a-fA-F]{40}/)) {
 		return;
 	}
-	($co{'id'}, my @parents) = split ' ', $header;
-	$co{'parents'} = \@parents;
-	$co{'parent'} = $parents[0];
+	$co{'id'} = $header;
+	my @parents;
 	while (my $line = shift @commit_lines) {
 		last if $line eq "\n";
 		if ($line =~ m/^tree ([0-9a-fA-F]{40})$/) {
 			$co{'tree'} = $1;
+		} elsif ($line =~ m/^parent ([0-9a-fA-F]{40})$/) {
+			push @parents, $1;
 		} elsif ($line =~ m/^author (.*) ([0-9]+) (.*)$/) {
 			$co{'author'} = $1;
 			$co{'author_epoch'} = $2;
@@ -1326,6 +1315,8 @@
 	if (!defined $co{'tree'}) {
 		return;
 	};
+	$co{'parents'} = \@parents;
+	$co{'parent'} = $parents[0];
 
 	foreach my $title (@commit_lines) {
 		$title =~ s/^    //;
@@ -1375,6 +1366,51 @@
 	return %co;
 }
 
+sub parse_commit {
+	my ($commit_id) = @_;
+	my %co;
+
+	local $/ = "\0";
+
+	open my $fd, "-|", git_cmd(), "rev-list",
+		"--header",
+		"--max-count=1",
+		$commit_id,
+		"--",
+		or die_error(undef, "Open git-rev-list failed");
+	%co = parse_commit_text(<$fd>);
+	close $fd;
+
+	return %co;
+}
+
+sub parse_commits {
+	my ($commit_id, $maxcount, $skip, $arg, $filename) = @_;
+	my @cos;
+
+	$maxcount ||= 1;
+	$skip ||= 0;
+
+	local $/ = "\0";
+
+	open my $fd, "-|", git_cmd(), "rev-list",
+		"--header",
+		($arg ? ($arg) : ()),
+		("--max-count=" . $maxcount),
+		("--skip=" . $skip),
+		$commit_id,
+		"--",
+		($filename ? ($filename) : ())
+		or die_error(undef, "Open git-rev-list failed");
+	while (my $line = <$fd>) {
+		my %co = parse_commit_text($line);
+		push @cos, \%co;
+	}
+	close $fd;
+
+	return wantarray ? @cos : \@cos;
+}
+
 # parse ref from ref_file, given by ref_id, with given type
 sub parse_ref {
 	my $ref_file = shift;
@@ -2646,20 +2682,19 @@
 
 sub git_shortlog_body {
 	# uses global variable $project
-	my ($revlist, $from, $to, $refs, $extra) = @_;
+	my ($commitlist, $from, $to, $refs, $extra) = @_;
 
 	my $have_snapshot = gitweb_have_snapshot();
 
 	$from = 0 unless defined $from;
-	$to = $#{$revlist} if (!defined $to || $#{$revlist} < $to);
+	$to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
 
 	print "<table class=\"shortlog\" cellspacing=\"0\">\n";
 	my $alternate = 1;
 	for (my $i = $from; $i <= $to; $i++) {
-		my $commit = $revlist->[$i];
-		#my $ref = defined $refs ? format_ref_marker($refs, $commit) : '';
+		my %co = %{$commitlist->[$i]};
+		my $commit = $co{'id'};
 		my $ref = format_ref_marker($refs, $commit);
-		my %co = parse_commit($commit);
 		if ($alternate) {
 			print "<tr class=\"dark\">\n";
 		} else {
@@ -2693,23 +2728,19 @@
 
 sub git_history_body {
 	# Warning: assumes constant type (blob or tree) during history
-	my ($revlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
+	my ($commitlist, $from, $to, $refs, $hash_base, $ftype, $extra) = @_;
 
 	$from = 0 unless defined $from;
-	$to = $#{$revlist} unless (defined $to && $to <= $#{$revlist});
+	$to = $#{$commitlist} unless (defined $to && $to <= $#{$commitlist});
 
 	print "<table class=\"history\" cellspacing=\"0\">\n";
 	my $alternate = 1;
 	for (my $i = $from; $i <= $to; $i++) {
-		if ($revlist->[$i] !~ m/^([0-9a-fA-F]{40})/) {
-			next;
-		}
-
-		my $commit = $1;
-		my %co = parse_commit($commit);
+		my %co = %{$commitlist->[$i]};
 		if (!%co) {
 			next;
 		}
+		my $commit = $co{'id'};
 
 		my $ref = format_ref_marker($refs, $commit);
 
@@ -2853,18 +2884,18 @@
 }
 
 sub git_search_grep_body {
-	my ($greplist, $from, $to, $extra) = @_;
+	my ($commitlist, $from, $to, $extra) = @_;
 	$from = 0 unless defined $from;
-	$to = $#{$greplist} if (!defined $to || $#{$greplist} < $to);
+	$to = $#{$commitlist} if (!defined $to || $#{$commitlist} < $to);
 
 	print "<table class=\"grep\" cellspacing=\"0\">\n";
 	my $alternate = 1;
 	for (my $i = $from; $i <= $to; $i++) {
-		my $commit = $greplist->[$i];
-		my %co = parse_commit($commit);
+		my %co = %{$commitlist->[$i]};
 		if (!%co) {
 			next;
 		}
+		my $commit = $co{'id'};
 		if ($alternate) {
 			print "<tr class=\"dark\">\n";
 		} else {
@@ -3023,14 +3054,10 @@
 
 	# we need to request one more than 16 (0..15) to check if
 	# those 16 are all
-	open my $fd, "-|", git_cmd(), "rev-list", "--max-count=17",
-		$head, "--"
-		or die_error(undef, "Open git-rev-list failed");
-	my @revlist = map { chomp; $_ } <$fd>;
-	close $fd;
+	my @commitlist = parse_commits($head, 17);
 	git_print_header_div('shortlog');
-	git_shortlog_body(\@revlist, 0, 15, $refs,
-	                  $#revlist <=  15 ? undef :
+	git_shortlog_body(\@commitlist, 0, 15, $refs,
+	                  $#commitlist <=  15 ? undef :
 	                  $cgi->a({-href => href(action=>"shortlog")}, "..."));
 
 	if (@taglist) {
@@ -3592,28 +3619,25 @@
 	}
 	my $refs = git_get_references();
 
-	my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
-	open my $fd, "-|", git_cmd(), "rev-list", $limit, $hash, "--"
-		or die_error(undef, "Open git-rev-list failed");
-	my @revlist = map { chomp; $_ } <$fd>;
-	close $fd;
+	my @commitlist = parse_commits($hash, 101, (100 * $page));
 
-	my $paging_nav = format_paging_nav('log', $hash, $head, $page, $#revlist);
+	my $paging_nav = format_paging_nav('log', $hash, $head, $page, (100 * ($page+1)));
 
 	git_header_html();
 	git_print_page_nav('log','', $hash,undef,undef, $paging_nav);
 
-	if (!@revlist) {
+	if (!@commitlist) {
 		my %co = parse_commit($hash);
 
 		git_print_header_div('summary', $project);
 		print "<div class=\"page_body\"> Last change $co{'age_string'}.<br/><br/></div>\n";
 	}
-	for (my $i = ($page * 100); $i <= $#revlist; $i++) {
-		my $commit = $revlist[$i];
-		my $ref = format_ref_marker($refs, $commit);
-		my %co = parse_commit($commit);
+	my $to = ($#commitlist >= 99) ? (99) : ($#commitlist);
+	for (my $i = 0; $i <= $to; $i++) {
+		my %co = %{$commitlist[$i]};
 		next if !%co;
+		my $commit = $co{'id'};
+		my $ref = format_ref_marker($refs, $commit);
 		my %ad = parse_date($co{'author_epoch'});
 		git_print_header_div('commit',
 		               "<span class=\"age\">$co{'age_string'}</span>" .
@@ -3635,6 +3659,12 @@
 		git_print_log($co{'comment'}, -final_empty_line=> 1);
 		print "</div>\n";
 	}
+	if ($#commitlist >= 100) {
+		print "<div class=\"page_nav\">\n";
+		print $cgi->a({-href => href(action=>"log", hash=>$hash, page=>$page+1),
+			       -accesskey => "n", -title => "Alt-n"}, "next");
+		print "</div>\n";
+	}
 	git_footer_html();
 }
 
@@ -4163,12 +4193,7 @@
 		$ftype = git_get_type($hash);
 	}
 
-	open my $fd, "-|",
-		git_cmd(), "rev-list", $limit, "--full-history", $hash_base, "--", $file_name
-			or die_error(undef, "Open git-rev-list-failed");
-	my @revlist = map { chomp; $_ } <$fd>;
-	close $fd
-		or die_error(undef, "Reading git-rev-list failed");
+	my @commitlist = parse_commits($hash_base, 101, (100 * $page), "--full-history", $file_name);
 
 	my $paging_nav = '';
 	if ($page > 0) {
@@ -4184,7 +4209,7 @@
 		$paging_nav .= "first";
 		$paging_nav .= " &sdot; prev";
 	}
-	if ($#revlist >= (100 * ($page+1)-1)) {
+	if ($#commitlist >= 100) {
 		$paging_nav .= " &sdot; " .
 			$cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
 			                       file_name=>$file_name, page=>$page+1),
@@ -4193,11 +4218,11 @@
 		$paging_nav .= " &sdot; next";
 	}
 	my $next_link = '';
-	if ($#revlist >= (100 * ($page+1)-1)) {
+	if ($#commitlist >= 100) {
 		$next_link =
 			$cgi->a({-href => href(action=>"history", hash=>$hash, hash_base=>$hash_base,
 			                       file_name=>$file_name, page=>$page+1),
-			         -title => "Alt-n"}, "next");
+			         -accesskey => "n", -title => "Alt-n"}, "next");
 	}
 
 	git_header_html();
@@ -4205,7 +4230,7 @@
 	git_print_header_div('commit', esc_html($co{'title'}), $hash_base);
 	git_print_page_path($file_name, $ftype, $hash_base);
 
-	git_history_body(\@revlist, ($page * 100), $#revlist,
+	git_history_body(\@commitlist, 0, 99,
 	                 $refs, $hash_base, $ftype, $next_link);
 
 	git_footer_html();
@@ -4251,13 +4276,8 @@
 		} elsif ($searchtype eq 'committer') {
 			$greptype = "--committer=";
 		}
-		open my $fd, "-|", git_cmd(), "rev-list",
-			("--max-count=" . (100 * ($page+1))),
-			($greptype . $searchtext),
-			$hash, "--"
-			or next;
-		my @revlist = map { chomp; $_ } <$fd>;
-		close $fd;
+		$greptype .= $searchtext;
+		my @commitlist = parse_commits($hash, 101, (100 * $page), $greptype);
 
 		my $paging_nav = '';
 		if ($page > 0) {
@@ -4274,7 +4294,7 @@
 			$paging_nav .= "first";
 			$paging_nav .= " &sdot; prev";
 		}
-		if ($#revlist >= (100 * ($page+1)-1)) {
+		if ($#commitlist >= 100) {
 			$paging_nav .= " &sdot; " .
 				$cgi->a({-href => href(action=>"search", hash=>$hash,
 						       searchtext=>$searchtext, searchtype=>$searchtype,
@@ -4284,7 +4304,7 @@
 			$paging_nav .= " &sdot; next";
 		}
 		my $next_link = '';
-		if ($#revlist >= (100 * ($page+1)-1)) {
+		if ($#commitlist >= 100) {
 			$next_link =
 				$cgi->a({-href => href(action=>"search", hash=>$hash,
 						       searchtext=>$searchtext, searchtype=>$searchtype,
@@ -4294,7 +4314,7 @@
 
 		git_print_page_nav('','', $hash,$co{'tree'},$hash, $paging_nav);
 		git_print_header_div('commit', esc_html($co{'title'}), $hash);
-		git_search_grep_body(\@revlist, ($page * 100), $#revlist, $next_link);
+		git_search_grep_body(\@commitlist, 0, 99, $next_link);
 	}
 
 	if ($searchtype eq 'pickaxe') {
@@ -4398,26 +4418,21 @@
 	}
 	my $refs = git_get_references();
 
-	my $limit = sprintf("--max-count=%i", (100 * ($page+1)));
-	open my $fd, "-|", git_cmd(), "rev-list", $limit, $hash, "--"
-		or die_error(undef, "Open git-rev-list failed");
-	my @revlist = map { chomp; $_ } <$fd>;
-	close $fd;
+	my @commitlist = parse_commits($head, 101, (100 * $page));
 
-	my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, $#revlist);
+	my $paging_nav = format_paging_nav('shortlog', $hash, $head, $page, (100 * ($page+1)));
 	my $next_link = '';
-	if ($#revlist >= (100 * ($page+1)-1)) {
+	if ($#commitlist >= 100) {
 		$next_link =
 			$cgi->a({-href => href(action=>"shortlog", hash=>$hash, page=>$page+1),
-			         -title => "Alt-n"}, "next");
+			         -accesskey => "n", -title => "Alt-n"}, "next");
 	}
 
-
 	git_header_html();
 	git_print_page_nav('shortlog','', $hash,$hash,$hash, $paging_nav);
 	git_print_header_div('summary', $project);
 
-	git_shortlog_body(\@revlist, ($page * 100), $#revlist, $refs, $next_link);
+	git_shortlog_body(\@commitlist, 0, 99, $refs, $next_link);
 
 	git_footer_html();
 }
@@ -4437,11 +4452,7 @@
 
 	# log/feed of current (HEAD) branch, log of given branch, history of file/directory
 	my $head = $hash || 'HEAD';
-	open my $fd, "-|", git_cmd(), "rev-list", "--max-count=150",
-		$head, "--", (defined $file_name ? $file_name : ())
-		or die_error(undef, "Open git-rev-list failed");
-	my @revlist = map { chomp; $_ } <$fd>;
-	close $fd or die_error(undef, "Reading git-rev-list failed");
+	my @commitlist = parse_commits($head, 150);
 
 	my %latest_commit;
 	my %latest_date;
@@ -4451,8 +4462,8 @@
 		# browser (feed reader) prefers text/xml
 		$content_type = 'text/xml';
 	}
-	if (defined($revlist[0])) {
-		%latest_commit = parse_commit($revlist[0]);
+	if (defined($commitlist[0])) {
+		%latest_commit = %{$commitlist[0]};
 		%latest_date   = parse_date($latest_commit{'author_epoch'});
 		print $cgi->header(
 			-type => $content_type,
@@ -4542,9 +4553,9 @@
 	}
 
 	# contents
-	for (my $i = 0; $i <= $#revlist; $i++) {
-		my $commit = $revlist[$i];
-		my %co = parse_commit($commit);
+	for (my $i = 0; $i <= $#commitlist; $i++) {
+		my %co = %{$commitlist[$i]};
+		my $commit = $co{'id'};
 		# we read 150, we always show 30 and the ones more recent than 48 hours
 		if (($i >= 20) && ((time - $co{'author_epoch'}) > 48*60*60)) {
 			last;
@@ -4552,7 +4563,7 @@
 		my %cd = parse_date($co{'author_epoch'});
 
 		# get list of changed files
-		open $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
+		open my $fd, "-|", git_cmd(), "diff-tree", '-r', @diff_opts,
 			$co{'parent'}, $co{'id'}, "--", (defined $file_name ? $file_name : ())
 			or next;
 		my @difftree = map { chomp; $_ } <$fd>;
diff --git a/revision.c b/revision.c
index 4dae534..e7eccd9 100644
--- a/revision.c
+++ b/revision.c
@@ -526,6 +526,7 @@
 	revs->prefix = prefix;
 	revs->max_age = -1;
 	revs->min_age = -1;
+	revs->skip_count = -1;
 	revs->max_count = -1;
 
 	revs->prune_fn = NULL;
@@ -762,6 +763,10 @@
 				revs->max_count = atoi(arg + 12);
 				continue;
 			}
+			if (!strncmp(arg, "--skip=", 7)) {
+				revs->skip_count = atoi(arg + 7);
+				continue;
+			}
 			/* accept -<digit>, like traditional "head" */
 			if ((*arg == '-') && isdigit(arg[1])) {
 				revs->max_count = atoi(arg + 1);
@@ -1129,23 +1134,11 @@
 			   commit->buffer, strlen(commit->buffer));
 }
 
-struct commit *get_revision(struct rev_info *revs)
+static struct commit *get_revision_1(struct rev_info *revs)
 {
-	struct commit_list *list = revs->commits;
-
-	if (!list)
+	if (!revs->commits)
 		return NULL;
 
-	/* Check the max_count ... */
-	switch (revs->max_count) {
-	case -1:
-		break;
-	case 0:
-		return NULL;
-	default:
-		revs->max_count--;
-	}
-
 	do {
 		struct commit_list *entry = revs->commits;
 		struct commit *commit = entry->item;
@@ -1212,3 +1205,28 @@
 	} while (revs->commits);
 	return NULL;
 }
+
+struct commit *get_revision(struct rev_info *revs)
+{
+	struct commit *c = NULL;
+
+	if (0 < revs->skip_count) {
+		while ((c = get_revision_1(revs)) != NULL) {
+			if (revs->skip_count-- <= 0)
+				break;
+		}
+	}
+
+	/* Check the max_count ... */
+	switch (revs->max_count) {
+	case -1:
+		break;
+	case 0:
+		return NULL;
+	default:
+		revs->max_count--;
+	}
+	if (c)
+		return c;
+	return get_revision_1(revs);
+}
diff --git a/revision.h b/revision.h
index 4585463..ec991e5 100644
--- a/revision.h
+++ b/revision.h
@@ -77,6 +77,7 @@
 	struct grep_opt	*grep_filter;
 
 	/* special limits */
+	int skip_count;
 	int max_count;
 	unsigned long max_age;
 	unsigned long min_age;
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
diff --git a/t/t6005-rev-list-count.sh b/t/t6005-rev-list-count.sh
new file mode 100755
index 0000000..334fccf
--- /dev/null
+++ b/t/t6005-rev-list-count.sh
@@ -0,0 +1,51 @@
+#!/bin/sh
+
+test_description='git-rev-list --max-count and --skip test'
+
+. ./test-lib.sh
+
+test_expect_success 'setup' '
+    for n in 1 2 3 4 5 ; do \
+        echo $n > a ; \
+        git add a ; \
+        git commit -m "$n" ; \
+    done
+'
+
+test_expect_success 'no options' '
+    test $(git-rev-list HEAD | wc -l) = 5
+'
+
+test_expect_success '--max-count' '
+    test $(git-rev-list HEAD --max-count=0 | wc -l) = 0 &&
+    test $(git-rev-list HEAD --max-count=3 | wc -l) = 3 &&
+    test $(git-rev-list HEAD --max-count=5 | wc -l) = 5 &&
+    test $(git-rev-list HEAD --max-count=10 | wc -l) = 5
+'
+
+test_expect_success '--max-count all forms' '
+    test $(git-rev-list HEAD --max-count=1 | wc -l) = 1 &&
+    test $(git-rev-list HEAD -1 | wc -l) = 1 &&
+    test $(git-rev-list HEAD -n1 | wc -l) = 1 &&
+    test $(git-rev-list HEAD -n 1 | wc -l) = 1
+'
+
+test_expect_success '--skip' '
+    test $(git-rev-list HEAD --skip=0 | wc -l) = 5 &&
+    test $(git-rev-list HEAD --skip=3 | wc -l) = 2 &&
+    test $(git-rev-list HEAD --skip=5 | wc -l) = 0 &&
+    test $(git-rev-list HEAD --skip=10 | wc -l) = 0
+'
+
+test_expect_success '--skip --max-count' '
+    test $(git-rev-list HEAD --skip=0 --max-count=0 | wc -l) = 0 &&
+    test $(git-rev-list HEAD --skip=0 --max-count=10 | wc -l) = 5 &&
+    test $(git-rev-list HEAD --skip=3 --max-count=0 | wc -l) = 0 &&
+    test $(git-rev-list HEAD --skip=3 --max-count=1 | wc -l) = 1 &&
+    test $(git-rev-list HEAD --skip=3 --max-count=2 | wc -l) = 2 &&
+    test $(git-rev-list HEAD --skip=3 --max-count=10 | wc -l) = 2 &&
+    test $(git-rev-list HEAD --skip=5 --max-count=10 | wc -l) = 0 &&
+    test $(git-rev-list HEAD --skip=10 --max-count=10 | wc -l) = 0
+'
+
+test_done