Subject: [PATCH 2/2] git-blame: no rev means start from the working tree file.

From: Junio C Hamano <[email-masked]>
Date: Sun, 04 Feb 2007 19:00:45 -0800

Warning: this changes the semantics.

This makes "git blame" without any positive rev to start digging
from the working tree copy, which is made into a fake commit
whose sole parent is the HEAD.

It also adds --contents <file> option to pretend as if the
working tree copy has the contents of the named file.  You can
use '-' to make the command read from the standard input.

If you want the command to start annotating from the HEAD
commit, you need to explicitly give HEAD parameter.

Signed-off-by: Junio C Hamano <[email-masked]>
---

 Theodore Tso <[email-masked]> writes:

 > On Sun, Feb 04, 2007 at 11:12:34AM -0800, Linus Torvalds wrote:
 >> 
 >> On Sun, 4 Feb 2007, Jeff King wrote:
 >> > 
 >> > Just a thought, but it might be useful to blame the contents of an
 >> > arbitrary file (but starting the history at a given pathname). Something
 >> > like "git blame --contents /tmp/foo.c file.c", with contents defaulting
 >> > to "file.c". There's much discussion of editor interfaces, and this
 >> > leaves the possibility of git-blaming the contents of the editor buffer
 >> > (after writing it out to a temp file) without having to save changes to
 >> > the working tree file.
 >> 
 >> I agree, that probably would make most sense. If we do this at all. On the 
 >> other hand, I suspect that most editors would probably want to pipe the 
 >> contents to the program, not write it to a temp-file.
 >
 > ... and use it with --incremental, as well.  In emacs you can have the
 > annotation take place as it is being written out relatively easily, by
 > arranging to have a callback function get called each time more
 > information is handed back to emacs via a pipe.

 * By popular request, this is the second round of the patch.
   I've splitted out a small change to make ident.c::get_ident()
   available to this one into a separate commit, which this
   patch depends on.

 builtin-blame.c |  209 ++++++++++++++++++++++++++++++++++++++++++++++++-------
 diff-lib.c      |   44 +++++++++++-
 diff.h          |    1 +
 3 files changed, 228 insertions(+), 26 deletions(-)

diff --git a/builtin-blame.c b/builtin-blame.c
index 1c21204..897323a 100644
--- a/builtin-blame.c
+++ b/builtin-blame.c
@@ -15,9 +15,10 @@
 #include "revision.h"
 #include "quote.h"
 #include "xdiff-interface.h"
+#include "cache-tree.h"
 
 static char blame_usage[] =
-"git-blame [-c] [-l] [-t] [-f] [-n] [-p] [-L n,m] [-S <revs-file>] [-M] [-C] [-C] [commit] [--] file\n"
+"git-blame [-c] [-l] [-t] [-f] [-n] [-p] [-L n,m] [-S <revs-file>] [-M] [-C] [-C] [--contents <filename>] [commit] [--] file\n"
 "  -c, --compatibility Use the same output mode as git-annotate (Default: off)\n"
 "  -b                  Show blank SHA-1 for boundary commits (Default: off)\n"
 "  -l, --long          Show long commit SHA1 (Default: off)\n"
@@ -29,6 +30,7 @@ static char blame_usage[] =
 "  -L n,m              Process only line range n,m, counting from 1\n"
 "  -M, -C              Find line movements within and across files\n"
 "  --incremental       Show blame entries as we find them, incrementally\n"
+"  --contents file     Use <file>'s contents as the final image\n"
 "  -S revs-file        Use revisions from revs-file instead of calling git-rev-list\n";
 
 static int longest_file;
@@ -333,9 +335,13 @@ static struct origin *find_origin(struct scoreboard *sb,
 	diff_tree_setup_paths(paths, &diff_opts);
 	if (diff_setup_done(&diff_opts) < 0)
 		die("diff-setup");
-	diff_tree_sha1(parent->tree->object.sha1,
-		       origin->commit->tree->object.sha1,
-		       "", &diff_opts);
+
+	if (is_null_sha1(origin->commit->object.sha1))
+		do_diff_cache(parent->tree->object.sha1, &diff_opts);
+	else
+		diff_tree_sha1(parent->tree->object.sha1,
+			       origin->commit->tree->object.sha1,
+			       "", &diff_opts);
 	diffcore_std(&diff_opts);
 
 	/* It is either one entry that says "modified", or "created",
@@ -402,9 +408,13 @@ static struct origin *find_rename(struct scoreboard *sb,
 	diff_tree_setup_paths(paths, &diff_opts);
 	if (diff_setup_done(&diff_opts) < 0)
 		die("diff-setup");
-	diff_tree_sha1(parent->tree->object.sha1,
-		       origin->commit->tree->object.sha1,
-		       "", &diff_opts);
+
+	if (is_null_sha1(origin->commit->object.sha1))
+		do_diff_cache(parent->tree->object.sha1, &diff_opts);
+	else
+		diff_tree_sha1(parent->tree->object.sha1,
+			       origin->commit->tree->object.sha1,
+			       "", &diff_opts);
 	diffcore_std(&diff_opts);
 
 	for (i = 0; i < diff_queued_diff.nr; i++) {
@@ -1047,9 +1057,12 @@ static int find_copy_in_parent(struct scoreboard *sb,
 	    (!porigin || strcmp(target->path, porigin->path)))
 		diff_opts.find_copies_harder = 1;
 
-	diff_tree_sha1(parent->tree->object.sha1,
-		       target->commit->tree->object.sha1,
-		       "", &diff_opts);
+	if (is_null_sha1(target->commit->object.sha1))
+		do_diff_cache(parent->tree->object.sha1, &diff_opts);
+	else
+		diff_tree_sha1(parent->tree->object.sha1,
+			       target->commit->tree->object.sha1,
+			       "", &diff_opts);
 
 	if (!diff_opts.find_copies_harder)
 		diffcore_std(&diff_opts);
@@ -1336,9 +1349,9 @@ static void get_commit_info(struct commit *commit,
 	tmp += 2;
 	endp = strchr(tmp, '\n');
 	if (!endp)
-		goto error_out;
+		endp = tmp + strlen(tmp);
 	len = endp - tmp;
-	if (len >= sizeof(summary_buf))
+	if (len >= sizeof(summary_buf) || len == 0)
 		goto error_out;
 	memcpy(summary_buf, tmp, len);
 	summary_buf[len] = 0;
@@ -1910,6 +1923,137 @@ static int git_blame_config(const char *var, const char *value)
 	return git_default_config(var, value);
 }
 
+static struct commit *fake_working_tree_commit(const char *path, const char *contents_from)
+{
+	struct commit *commit;
+	struct origin *origin;
+	unsigned char head_sha1[20];
+	char *buf;
+	const char *ident;
+	int fd;
+	time_t now;
+	unsigned long fin_size;
+	int size, len;
+	struct cache_entry *ce;
+	unsigned mode;
+
+	if (get_sha1("HEAD", head_sha1))
+		die("No such ref: HEAD");
+
+	time(&now);
+	commit = xcalloc(1, sizeof(*commit));
+	commit->parents = xcalloc(1, sizeof(*commit->parents));
+	commit->parents->item = lookup_commit_reference(head_sha1);
+	commit->object.parsed = 1;
+	commit->date = now;
+	commit->object.type = OBJ_COMMIT;
+
+	origin = make_origin(commit, path);
+
+	if (!contents_from || strcmp("-", contents_from)) {
+		struct stat st;
+		const char *read_from;
+
+		if (contents_from) {
+			if (stat(contents_from, &st) < 0)
+				die("Cannot stat %s", contents_from);
+			read_from = contents_from;
+		}
+		else {
+			if (lstat(path, &st) < 0)
+				die("Cannot lstat %s", path);
+			read_from = path;
+		}
+		fin_size = st.st_size;
+		buf = xmalloc(fin_size+1);
+		mode = canon_mode(st.st_mode);
+		switch (st.st_mode & S_IFMT) {
+		case S_IFREG:
+			fd = open(read_from, O_RDONLY);
+			if (fd < 0)
+				die("cannot open %s", read_from);
+			if (read_in_full(fd, buf, fin_size) != fin_size)
+				die("cannot read %s", read_from);
+			break;
+		case S_IFLNK:
+			if (readlink(read_from, buf, fin_size+1) != fin_size)
+				die("cannot readlink %s", read_from);
+			break;
+		default:
+			die("unsupported file type %s", read_from);
+		}
+	}
+	else {
+		/* Reading from stdin */
+		contents_from = "standard input";
+		buf = NULL;
+		fin_size = 0;
+		mode = 0;
+		while (1) {
+			ssize_t cnt = 8192;
+			buf = xrealloc(buf, fin_size + cnt);
+			cnt = xread(0, buf + fin_size, cnt);
+			if (cnt < 0)
+				die("read error %s from stdin",
+				    strerror(errno));
+			if (!cnt)
+				break;
+			fin_size += cnt;
+		}
+		buf = xrealloc(buf, fin_size + 1);
+	}
+	buf[fin_size] = 0;
+	origin->file.ptr = buf;
+	origin->file.size = fin_size;
+	write_sha1_file(buf, fin_size, blob_type, origin->blob_sha1);
+	commit->util = origin;
+
+	/*
+	 * Read the current index, replace the path entry with
+	 * origin->blob_sha1 without mucking with its mode or type
+	 * bits; we are not going to write this index out -- we just
+	 * want to run "diff-index --cached".
+	 */
+	discard_cache();
+	read_cache();
+
+	len = strlen(path);
+	if (!mode) {
+		int pos = cache_name_pos(path, len);
+		if (0 <= pos)
+			mode = ntohl(active_cache[pos]->ce_mode);
+		else
+			/* Let's not bother reading from HEAD tree */
+			mode = S_IFREG | 0644;
+	}
+	size = cache_entry_size(len);
+	ce = xcalloc(1, size);
+	hashcpy(ce->sha1, origin->blob_sha1);
+	memcpy(ce->name, path, len);
+	ce->ce_flags = create_ce_flags(len, 0);
+	ce->ce_mode = create_ce_mode(mode);
+	add_cache_entry(ce, ADD_CACHE_OK_TO_ADD|ADD_CACHE_OK_TO_REPLACE);
+
+	/*
+	 * We are not going to write this out, so this does not matter
+	 * right now, but someday we might optimize diff-index --cached
+	 * with cache-tree information.
+	 */
+	cache_tree_invalidate_path(active_cache_tree, path);
+
+	commit->buffer = xmalloc(400);
+	ident = fmt_ident("Not Committed Yet", "not.committed.yet", NULL, 0);
+	sprintf(commit->buffer,
+		"tree 0000000000000000000000000000000000000000\n"
+		"parent %s\n"
+		"author %s\n"
+		"committer %s\n\n"
+		"Version of %s from %s\n",
+		sha1_to_hex(head_sha1),
+		ident, ident, path, contents_from ? contents_from : path);
+	return commit;
+}
+
 int cmd_blame(int argc, const char **argv, const char *prefix)
 {
 	struct rev_info revs;
@@ -1924,6 +2068,7 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
 	const char *final_commit_name = NULL;
 	char type[10];
 	const char *bottomtop = NULL;
+	const char *contents_from = NULL;
 
 	git_config(git_blame_config);
 	save_commit_buffer = 0;
@@ -1968,6 +2113,11 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
 				die("More than one '-L n,m' option given");
 			bottomtop = arg;
 		}
+		else if (!strcmp("--contents", arg)) {
+			if (++i >= argc)
+				usage(blame_usage);
+			contents_from = argv[i];
+		}
 		else if (!strcmp("--incremental", arg))
 			incremental = 1;
 		else if (!strcmp("--score-debug", arg))
@@ -2087,7 +2237,7 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
 	argv[unk] = NULL;
 
 	init_revisions(&revs, NULL);
-	setup_revisions(unk, argv, &revs, "HEAD");
+	setup_revisions(unk, argv, &revs, NULL);
 	memset(&sb, 0, sizeof(sb));
 
 	/*
@@ -2114,16 +2264,14 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
 	if (!sb.final) {
 		/*
 		 * "--not A B -- path" without anything positive;
-		 * default to HEAD.
+		 * do not default to HEAD, but use the working tree
+		 * or "--contents".
 		 */
-		unsigned char head_sha1[20];
-
-		final_commit_name = "HEAD";
-		if (get_sha1(final_commit_name, head_sha1))
-			die("No such ref: HEAD");
-		sb.final = lookup_commit_reference(head_sha1);
-		add_pending_object(&revs, &(sb.final->object), "HEAD");
+		sb.final = fake_working_tree_commit(path, contents_from);
+		add_pending_object(&revs, &(sb.final->object), ":");
 	}
+	else if (contents_from)
+		die("Cannot use --contents with final commit object name");
 
 	/*
 	 * If we have bottom, this will mark the ancestors of the
@@ -2132,11 +2280,22 @@ int cmd_blame(int argc, const char **argv, const char *prefix)
 	 */
 	prepare_revision_walk(&revs);
 
-	o = get_origin(&sb, sb.final, path);
-	if (fill_blob_sha1(o))
-		die("no such path %s in %s", path, final_commit_name);
+	if (is_null_sha1(sb.final->object.sha1)) {
+		char *buf;
+		o = sb.final->util;
+		buf = xmalloc(o->file.size + 1);
+		memcpy(buf, o->file.ptr, o->file.size + 1);
+		sb.final_buf = buf;
+		sb.final_buf_size = o->file.size;
+	}
+	else {
+		o = get_origin(&sb, sb.final, path);
+		if (fill_blob_sha1(o))
+			die("no such path %s in %s", path, final_commit_name);
 
-	sb.final_buf = read_sha1_file(o->blob_sha1, type, &sb.final_buf_size);
+		sb.final_buf = read_sha1_file(o->blob_sha1, type,
+					      &sb.final_buf_size);
+	}
 	num_read_blob++;
 	lno = prepare_lines(&sb);
 
diff --git a/diff-lib.c b/diff-lib.c
index 2c9be60..91cd877 100644
--- a/diff-lib.c
+++ b/diff-lib.c
@@ -7,6 +7,7 @@
 #include "diff.h"
 #include "diffcore.h"
 #include "revision.h"
+#include "cache-tree.h"
 
 /*
  * diff-files
@@ -271,7 +272,7 @@ static int diff_cache(struct rev_info *revs,
 				break;
 			}
 			/* Show difference between old and new */
-			show_modified(revs,ac[1], ce, 1,
+			show_modified(revs, ac[1], ce, 1,
 				      cached, match_missing);
 			break;
 		case 1:
@@ -372,3 +373,44 @@ int run_diff_index(struct rev_info *revs, int cached)
 	diff_flush(&revs->diffopt);
 	return ret;
 }
+
+int do_diff_cache(const unsigned char *tree_sha1, struct diff_options *opt)
+{
+	struct tree *tree;
+	struct rev_info revs;
+	int i;
+	struct cache_entry **dst;
+	struct cache_entry *last = NULL;
+
+	/*
+	 * This is used by git-blame to run diff-cache internally;
+	 * it potentially needs to repeatedly run this, so we will
+	 * start by removing the higher order entries the last round
+	 * left behind.
+	 */
+	dst = active_cache;
+	for (i = 0; i < active_nr; i++) {
+		struct cache_entry *ce = active_cache[i];
+		if (ce_stage(ce)) {
+			if (last && !strcmp(ce->name, last->name))
+				continue;
+			cache_tree_invalidate_path(active_cache_tree,
+						   ce->name);
+			last = ce;
+			ce->ce_mode = 0;
+			ce->ce_flags &= ~htons(CE_STAGEMASK);
+		}
+		*dst++ = ce;
+	}
+	active_nr = dst - active_cache;
+
+	init_revisions(&revs, NULL);
+	revs.prune_data = opt->paths;
+	tree = parse_tree_indirect(tree_sha1);
+	if (!tree)
+		die("bad tree object %s", sha1_to_hex(tree_sha1));
+	if (read_tree(tree, 1, opt->paths))
+		return error("unable to read tree %s", sha1_to_hex(tree_sha1));
+	return diff_cache(&revs, active_cache, active_nr, revs.prune_data,
+			  1, 0);
+}
diff --git a/diff.h b/diff.h
index 7a347cf..eece65d 100644
--- a/diff.h
+++ b/diff.h
@@ -222,6 +222,7 @@ extern int run_diff_files(struct rev_info *revs, int silent_on_removed);
 
 extern int run_diff_index(struct rev_info *revs, int cached);
 
+extern int do_diff_cache(const unsigned char *, struct diff_options *);
 extern int diff_flush_patch_id(struct diff_options *, unsigned char *);
 
 #endif /* DIFF_H */
-- 
1.5.0.rc3.40.g1f7d