Subject: [PATCH 7/7] merge: add --rename-notes

From: Nguyễn Thái Ngọc Duy <[email-masked]>
Date: Wed, 20 Jan 2016 18:06:08 +0700

For merge professionals, --rename-file is as bad a nightmare as
resolving conflicts because they may have to create a rename file for
every merge. --rename-notes takes advantage of rename notes to avoid that.

Because rename notes are between commit A and A^, not between A and its
merge base, we need to convert "rename path A to path B" to "rename blob
A' to blob B'" and hope that mere base still has that blob A'.

Merging notes this way is expensive. So it's cached in $GIT_DIR/rename-cache.

Signed-off-by: Nguyễn Thái Ngọc Duy <[email-masked]>
---
 builtin/merge.c | 114 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 114 insertions(+)

diff --git a/builtin/merge.c b/builtin/merge.c
index 95a6c26..6ad2e52 100644
--- a/builtin/merge.c
+++ b/builtin/merge.c
@@ -30,6 +30,7 @@
 #include "fmt-merge-msg.h"
 #include "gpg-interface.h"
 #include "sequencer.h"
+#include "notes.h"
 
 #define DEFAULT_TWOHEAD (1<<0)
 #define DEFAULT_OCTOPUS (1<<1)
@@ -69,6 +70,7 @@ static int default_to_upstream = 1;
 static const char *sign_commit;
 static const char *rename_file;
 static struct strbuf manual_renames = STRBUF_INIT;
+static const char *rename_note_ref;
 
 static struct strategy all_strategy[] = {
 	{ "recursive",  DEFAULT_TWOHEAD | NO_TRIVIAL },
@@ -228,6 +230,7 @@ static struct option builtin_merge_options[] = {
 	  N_("GPG sign commit"), PARSE_OPT_OPTARG, NULL, (intptr_t) "" },
 	OPT_BOOL(0, "overwrite-ignore", &overwrite_ignore, N_("update ignored files (default)")),
 	OPT_FILENAME(0, "rename-file", &rename_file, N_("--rename-file to diff")),
+	OPT_STRING(0, "rename-notes", &rename_note_ref, N_("note-ref"), N_("--rename-notes to diff")),
 	OPT_END()
 };
 
@@ -1161,6 +1164,102 @@ static struct commit_list *collect_parents(struct commit *head_commit,
 	return remoteheads;
 }
 
+static int merge_rename_note(const unsigned char *object_hash,
+			     const unsigned char *note_hash,
+			     char *note_path,
+			     void *cb_data)
+{
+	struct strbuf *cache = cb_data;
+	struct strbuf sb = STRBUF_INIT;
+	const char *p, *end;
+	enum object_type type;
+	unsigned long size;
+	char *note;
+
+	note = read_sha1_file(note_hash, &type, &size);
+	if (type != OBJ_BLOB) {
+		free(note);
+		return 0;
+	}
+
+	p = note;
+	end = p + strlen(p);
+	while (p < end) {
+		const char *line_end = strchr(p, '\n');
+		const char *arrow = strstr(p, " => ");
+		const char *src = p, *dst;
+		struct object_id src_oid;
+		struct object_id dst_oid;
+
+		if (!line_end)
+			line_end = end;
+		p = line_end + 1;
+
+		if (!arrow || arrow >= line_end)
+			continue;
+
+		if (starts_with(src, "blob ") && src + 4 < arrow) {
+			strbuf_addf(cache, "%.*s\n",
+				    (int)(line_end - src), src);
+			continue;
+		}
+
+		strbuf_reset(&sb);
+		strbuf_addf(&sb, "%s:%.*s", sha1_to_hex(object_hash),
+			    (int)(arrow - src), src);
+		if (get_sha1(sb.buf, src_oid.hash))
+			continue;
+
+		dst = arrow + strlen(" => ");
+		strbuf_reset(&sb);
+		strbuf_addf(&sb, "%s:%.*s", sha1_to_hex(object_hash),
+			    (int)(line_end - dst), dst);
+		if (get_sha1(sb.buf, dst_oid.hash))
+			continue;
+
+		strbuf_addf(cache, "blob %s => %s\n",
+			    oid_to_hex(&src_oid),
+			    oid_to_hex(&dst_oid));
+	}
+
+	return 0;
+}
+
+/*
+ * Traverse through the given notes tree, convert all "path to path"
+ * rename lines into "blob to blob" and return it. If cache_file is
+ * non-NULL, return it's content if still valid. Otherwise save the
+ * new content in it.
+ */
+static void merge_rename_notes(struct strbuf *cache,
+			       struct notes_tree *t, const char *cache_file)
+{
+	struct object_id notes_oid;
+
+	if (cache_file) {
+		struct object_id cache_oid;
+
+		strbuf_reset(cache);
+		if (!resolve_ref_unsafe(t->ref, RESOLVE_REF_READING,
+					notes_oid.hash, NULL))
+			return;
+
+		if (strbuf_read_file(cache, cache_file, 0) > GIT_SHA1_HEXSZ + 1 &&
+		    cache->buf[GIT_SHA1_HEXSZ] == '\n' &&
+		    !get_oid_hex(cache->buf, &cache_oid) &&
+		    !oidcmp(&notes_oid, &cache_oid)) {
+			strbuf_remove(cache, 0, GIT_SHA1_HEXSZ + 1);
+			return;
+		}
+	}
+
+	strbuf_reset(cache);
+	for_each_note(t, 0, merge_rename_note, cache);
+	if (cache_file && cache->len)
+		write_file(cache_file, "%s\n%s",
+			   oid_to_hex(&notes_oid), cache->buf);
+}
+
 int cmd_merge(int argc, const char **argv, const char *prefix)
 {
 	unsigned char result_tree[20];
@@ -1260,10 +1359,25 @@ int cmd_merge(int argc, const char **argv, const char *prefix)
 		usage_with_options(builtin_merge_usage,
 			builtin_merge_options);
 
+	if (rename_file && rename_note_ref)
+		die(_("--rename-file and --rename-notes are incompatible"));
+
 	if (rename_file &&
 	    strbuf_read_file(&manual_renames, rename_file, 0) == -1)
 		die(_("unable to read %s"), rename_file);
 
+	if (rename_note_ref) {
+		struct notes_tree rename_notes;
+		struct strbuf ref = STRBUF_INIT;
+
+		strbuf_addstr(&ref, rename_note_ref);
+		expand_notes_ref(&ref);
+		init_notes(&rename_notes, ref.buf, NULL, 0);
+		strbuf_release(&ref);
+		merge_rename_notes(&manual_renames, &rename_notes,
+				   git_path("GIT_RENAME_CACHE"));
+	}
+
 	if (!head_commit) {
 		struct commit *remote_head;
 		/*
-- 
2.7.0.125.g9eec362