{"thread":{"id":"24479","subject":"[WIP/RFC 02/13] notes.h: Make default_notes_ref() available in notes API","startedAt":"2010-07-23T10:14:51Z","lastAt":"2010-07-25T19:02:52Z","messageCount":24,"participants":["Johan Herland","Jonathan Nieder","Stephen Boyd","Sam Vilain"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"146051","messageId":"1279880104-29796-1-git-send-email-johan@herland.net","threadId":"24479","inReplyTo":null,"subject":"[WIP/RFC 00/13] git notes merge","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-07-23T10:14:51Z","receivedAt":"2010-07-23T10:14:51Z","isPatch":false,"sender":{"key":"johan@herland.net","avatar":"https://avatars.githubusercontent.com/u/547031?v=4"},"body":"Hi,\n\nHere are some rough patches implementing most of the 'git notes merge'\nfunctionality. There are still missing pieces in the area of manual\nconflict resolution, but what's here should be enough to start\nexperimenting with automatic notes merge resolution.\n\nPatches #1 - #8 + #10 are a combination of general cleanup and\npreparations for 'git notes merge'. Patch #9 implements handling\nof trivial notes merges (\"already up-to-date\" and \"fast-forward\").\nPatch #11 implements object-level merging of notes, using regular\nthree-way rules. Patch #12 adds a few automatic conflict resolvers\nthat can be used instead of manual conflict resolution (which has\nyet to be implemented). Patch #13 adds some preliminary docs.\n\nFor now, this is probably only ready for those with a special\ninterest in notes.\n\n\nHave fun! :)\n\n...Johan\n\n\nJohan Herland (13):\n  (trivial) notes.h: Minor documentation fixes to copy_notes()\n  notes.h: Make default_notes_ref() available in notes API\n  notes.h/c: Clarify the handling of notes objects that are == null_sha1\n  notes.h/c: Propagate combine_notes_fn return value to add_note() and beyond\n  (trivial) t3303: Indent with tabs instead of spaces for consistency\n  notes.c: Use two newlines (instead of one) when concatenating notes\n  (trivial) git notes prune: Accept --verbose in addition to -v\n  builtin/notes.c: Split notes ref DWIMmery into a separate function\n  git notes merge: Initial implementation handling trivial merges only\n  builtin/notes.c: Refactor creation of notes commits.\n  git notes merge: Handle real, non-conflicting notes merges\n  git notes merge: Add automatic conflict resolvers (ours, theirs, union)\n  Documentation: Preliminary docs on 'git notes merge'\n\n Documentation/git-notes.txt         |   30 ++-\n Makefile                            |    2 +\n builtin.h                           |    2 +-\n builtin/notes.c                     |  130 ++++++--\n notes-cache.c                       |    3 +-\n notes-merge.c                       |  474 +++++++++++++++++++++++++\n notes-merge.h                       |   59 +++\n notes.c                             |  196 ++++++-----\n notes.h                             |   46 +++-\n t/t3301-notes.sh                    |    4 +\n t/t3303-notes-subtrees.sh           |   19 +-\n t/t3308-notes-merge.sh              |  669 +++++++++++++++++++++++++++++++++++\n t/t3309-notes-merge-auto-resolve.sh |  512 +++++++++++++++++++++++++++\n t/t3404-rebase-interactive.sh       |    1 +\n t/t9301-fast-import-notes.sh        |    5 +\n 15 files changed, 2014 insertions(+), 138 deletions(-)\n create mode 100644 notes-merge.c\n create mode 100644 notes-merge.h\n create mode 100755 t/t3308-notes-merge.sh\n create mode 100755 t/t3309-notes-merge-auto-resolve.sh\n\n--\n1.7.2.220.gea1d3\n"},{"id":"146062","messageId":"1279880104-29796-2-git-send-email-johan@herland.net","threadId":"24479","inReplyTo":"1279880104-29796-1-git-send-email-johan@herland.net","subject":"[WIP/RFC 01/13] (trivial) notes.h: Minor documentation fixes to copy_notes()","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-07-23T10:14:52Z","receivedAt":"2010-07-23T10:14:52Z","isPatch":false,"sender":{"key":"johan@herland.net","avatar":"https://avatars.githubusercontent.com/u/547031?v=4"},"body":"Signed-off-by: Johan Herland <johan@herland.net>\n---\n notes.h |    5 +++++\n 1 files changed, 5 insertions(+), 0 deletions(-)\n\ndiff --git a/notes.h b/notes.h\nindex 65fc3a6..c0288b0 100644\n--- a/notes.h\n+++ b/notes.h\n@@ -104,6 +104,10 @@ const unsigned char *get_note(struct notes_tree *t,\n  * Copy a note from one object to another in the given notes_tree.\n  *\n  * Fails if the to_obj already has a note unless 'force' is true.\n+ *\n+ * IMPORTANT: The changes made by copy_note() to the given notes_tree structure\n+ * are not persistent until a subsequent call to write_notes_tree() returns\n+ * zero.\n  */\n int copy_note(struct notes_tree *t,\n \t      const unsigned char *from_obj, const unsigned char *to_obj,\n@@ -149,6 +153,7 @@ int copy_note(struct notes_tree *t,\n  * notes tree) from within the callback:\n  * - add_note()\n  * - remove_note()\n+ * - copy_note()\n  * - free_notes()\n  */\n typedef int each_note_fn(const unsigned char *object_sha1,\n-- \n1.7.2.220.gea1d3\n"},{"id":"146050","messageId":"1279880104-29796-3-git-send-email-johan@herland.net","threadId":"24479","inReplyTo":"1279880104-29796-1-git-send-email-johan@herland.net","subject":"[WIP/RFC 02/13] notes.h: Make default_notes_ref() available in notes API","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-07-23T10:14:53Z","receivedAt":"2010-07-23T10:14:53Z","isPatch":false,"sender":{"key":"johan@herland.net","avatar":"https://avatars.githubusercontent.com/u/547031?v=4"},"body":"Signed-off-by: Johan Herland <johan@herland.net>\n---\n notes.c |    2 +-\n notes.h |   14 ++++++++++++++\n 2 files changed, 15 insertions(+), 1 deletions(-)\n\ndiff --git a/notes.c b/notes.c\nindex 1978244..79fd850 100644\n--- a/notes.c\n+++ b/notes.c\n@@ -898,7 +898,7 @@ static int notes_display_config(const char *k, const char *v, void *cb)\n \treturn 0;\n }\n \n-static const char *default_notes_ref(void)\n+const char *default_notes_ref(void)\n {\n \tconst char *notes_ref = NULL;\n \tif (!notes_ref)\ndiff --git a/notes.h b/notes.h\nindex c0288b0..20db42f 100644\n--- a/notes.h\n+++ b/notes.h\n@@ -44,6 +44,20 @@ extern struct notes_tree {\n } default_notes_tree;\n \n /*\n+ * Return the default notes ref.\n+ *\n+ * The default notes ref is the notes ref that is used when notes_ref == NULL\n+ * is passed to init_notes().\n+ *\n+ * This the first of the following to be defined:\n+ * 1. The '--ref' option to 'git notes', if given\n+ * 2. The $GIT_NOTES_REF environment variable, if set\n+ * 3. The value of the core.notesRef config variable, if set\n+ * 4. GIT_NOTES_DEFAULT_REF (i.e. \"refs/notes/commits\")\n+ */\n+const char *default_notes_ref(void);\n+\n+/*\n  * Flags controlling behaviour of notes tree initialization\n  *\n  * Default behaviour is to initialize the notes tree from the tree object\n-- \n1.7.2.220.gea1d3\n"},{"id":"146063","messageId":"1279880104-29796-4-git-send-email-johan@herland.net","threadId":"24479","inReplyTo":"1279880104-29796-1-git-send-email-johan@herland.net","subject":"[WIP/RFC 03/13] notes.h/c: Clarify the handling of notes objects that are == null_sha1","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-07-23T10:14:54Z","receivedAt":"2010-07-23T10:14:54Z","isPatch":false,"sender":{"key":"johan@herland.net","avatar":"https://avatars.githubusercontent.com/u/547031?v=4"},"body":"Clearly specify how combine_notes functions are expected to handle null_sha1\nin input, and how they may set the result to null_sha1 in order to cause\nthe removal of a note.\n\nAlso document that passing note_sha1 == null_sha1 to add_note() is usually\na no-op, except in cases where combining it with an existing note yields a\nnew/changed result.\n\nThe only functional changes in this patch concern the handling of null_sha1\nin notes_tree_insert(). Otherwise the patch consists solely of reordering\nfunctions in notes.c to avoid use-before-declaration, and adding\ndocumentation to notes.h.\n\nSigned-off-by: Johan Herland <johan@herland.net>\n---\n notes.c |  158 +++++++++++++++++++++++++++++++++-----------------------------\n notes.h |   16 ++++++-\n 2 files changed, 99 insertions(+), 75 deletions(-)\n\ndiff --git a/notes.c b/notes.c\nindex 79fd850..9e92021 100644\n--- a/notes.c\n+++ b/notes.c\n@@ -150,6 +150,79 @@ static struct leaf_node *note_tree_find(struct notes_tree *t,\n }\n \n /*\n+ * How to consolidate an int_node:\n+ * If there are > 1 non-NULL entries, give up and return non-zero.\n+ * Otherwise replace the int_node at the given index in the given parent node\n+ * with the only entry (or a NULL entry if no entries) from the given tree,\n+ * and return 0.\n+ */\n+static int note_tree_consolidate(struct int_node *tree,\n+\tstruct int_node *parent, unsigned char index)\n+{\n+\tunsigned int i;\n+\tvoid *p = NULL;\n+\n+\tassert(tree && parent);\n+\tassert(CLR_PTR_TYPE(parent->a[index]) == tree);\n+\n+\tfor (i = 0; i < 16; i++) {\n+\t\tif (GET_PTR_TYPE(tree->a[i]) != PTR_TYPE_NULL) {\n+\t\t\tif (p) /* more than one entry */\n+\t\t\t\treturn -2;\n+\t\t\tp = tree->a[i];\n+\t\t}\n+\t}\n+\n+\t/* replace tree with p in parent[index] */\n+\tparent->a[index] = p;\n+\tfree(tree);\n+\treturn 0;\n+}\n+\n+/*\n+ * To remove a leaf_node:\n+ * Search to the tree location appropriate for the given leaf_node's key:\n+ * - If location does not hold a matching entry, abort and do nothing.\n+ * - Replace the matching leaf_node with a NULL entry (and free the leaf_node).\n+ * - Consolidate int_nodes repeatedly, while walking up the tree towards root.\n+ */\n+static void note_tree_remove(struct notes_tree *t, struct int_node *tree,\n+\t\tunsigned char n, struct leaf_node *entry)\n+{\n+\tstruct leaf_node *l;\n+\tstruct int_node *parent_stack[20];\n+\tunsigned char i, j;\n+\tvoid **p = note_tree_search(t, &tree, &n, entry->key_sha1);\n+\n+\tassert(GET_PTR_TYPE(entry) == 0); /* no type bits set */\n+\tif (GET_PTR_TYPE(*p) != PTR_TYPE_NOTE)\n+\t\treturn; /* type mismatch, nothing to remove */\n+\tl = (struct leaf_node *) CLR_PTR_TYPE(*p);\n+\tif (hashcmp(l->key_sha1, entry->key_sha1))\n+\t\treturn; /* key mismatch, nothing to remove */\n+\n+\t/* we have found a matching entry */\n+\tfree(l);\n+\t*p = SET_PTR_TYPE(NULL, PTR_TYPE_NULL);\n+\n+\t/* consolidate this tree level, and parent levels, if possible */\n+\tif (!n)\n+\t\treturn; /* cannot consolidate top level */\n+\t/* first, build stack of ancestors between root and current node */\n+\tparent_stack[0] = t->root;\n+\tfor (i = 0; i < n; i++) {\n+\t\tj = GET_NIBBLE(i, entry->key_sha1);\n+\t\tparent_stack[i + 1] = CLR_PTR_TYPE(parent_stack[i]->a[j]);\n+\t}\n+\tassert(i == n && parent_stack[i] == tree);\n+\t/* next, unwind stack until note_tree_consolidate() is done */\n+\twhile (i > 0 &&\n+\t       !note_tree_consolidate(parent_stack[i], parent_stack[i - 1],\n+\t\t\t\t      GET_NIBBLE(i - 1, entry->key_sha1)))\n+\t\ti--;\n+}\n+\n+/*\n  * To insert a leaf_node:\n  * Search to the tree location appropriate for the given leaf_node's key:\n  * - If location is unused (NULL), store the tweaked pointer directly there\n@@ -175,7 +248,10 @@ static void note_tree_insert(struct notes_tree *t, struct int_node *tree,\n \tswitch (GET_PTR_TYPE(*p)) {\n \tcase PTR_TYPE_NULL:\n \t\tassert(!*p);\n-\t\t*p = SET_PTR_TYPE(entry, type);\n+\t\tif (is_null_sha1(entry->val_sha1))\n+\t\t\tfree(entry);\n+\t\telse\n+\t\t\t*p = SET_PTR_TYPE(entry, type);\n \t\treturn;\n \tcase PTR_TYPE_NOTE:\n \t\tswitch (type) {\n@@ -191,6 +267,9 @@ static void note_tree_insert(struct notes_tree *t, struct int_node *tree,\n \t\t\t\t\t    sha1_to_hex(l->val_sha1),\n \t\t\t\t\t    sha1_to_hex(entry->val_sha1),\n \t\t\t\t\t    sha1_to_hex(l->key_sha1));\n+\n+\t\t\t\tif (is_null_sha1(l->val_sha1))\n+\t\t\t\t\tnote_tree_remove(t, tree, n, entry);\n \t\t\t\tfree(entry);\n \t\t\t\treturn;\n \t\t\t}\n@@ -222,6 +301,10 @@ static void note_tree_insert(struct notes_tree *t, struct int_node *tree,\n \t/* non-matching leaf_node */\n \tassert(GET_PTR_TYPE(*p) == PTR_TYPE_NOTE ||\n \t       GET_PTR_TYPE(*p) == PTR_TYPE_SUBTREE);\n+\tif (is_null_sha1(entry->val_sha1)) { /* skip insertion of empty note */\n+\t\tfree(entry);\n+\t\treturn;\n+\t}\n \tnew_node = (struct int_node *) xcalloc(sizeof(struct int_node), 1);\n \tnote_tree_insert(t, new_node, n + 1, l, GET_PTR_TYPE(*p),\n \t\t\t combine_notes);\n@@ -229,79 +312,6 @@ static void note_tree_insert(struct notes_tree *t, struct int_node *tree,\n \tnote_tree_insert(t, new_node, n + 1, entry, type, combine_notes);\n }\n \n-/*\n- * How to consolidate an int_node:\n- * If there are > 1 non-NULL entries, give up and return non-zero.\n- * Otherwise replace the int_node at the given index in the given parent node\n- * with the only entry (or a NULL entry if no entries) from the given tree,\n- * and return 0.\n- */\n-static int note_tree_consolidate(struct int_node *tree,\n-\tstruct int_node *parent, unsigned char index)\n-{\n-\tunsigned int i;\n-\tvoid *p = NULL;\n-\n-\tassert(tree && parent);\n-\tassert(CLR_PTR_TYPE(parent->a[index]) == tree);\n-\n-\tfor (i = 0; i < 16; i++) {\n-\t\tif (GET_PTR_TYPE(tree->a[i]) != PTR_TYPE_NULL) {\n-\t\t\tif (p) /* more than one entry */\n-\t\t\t\treturn -2;\n-\t\t\tp = tree->a[i];\n-\t\t}\n-\t}\n-\n-\t/* replace tree with p in parent[index] */\n-\tparent->a[index] = p;\n-\tfree(tree);\n-\treturn 0;\n-}\n-\n-/*\n- * To remove a leaf_node:\n- * Search to the tree location appropriate for the given leaf_node's key:\n- * - If location does not hold a matching entry, abort and do nothing.\n- * - Replace the matching leaf_node with a NULL entry (and free the leaf_node).\n- * - Consolidate int_nodes repeatedly, while walking up the tree towards root.\n- */\n-static void note_tree_remove(struct notes_tree *t, struct int_node *tree,\n-\t\tunsigned char n, struct leaf_node *entry)\n-{\n-\tstruct leaf_node *l;\n-\tstruct int_node *parent_stack[20];\n-\tunsigned char i, j;\n-\tvoid **p = note_tree_search(t, &tree, &n, entry->key_sha1);\n-\n-\tassert(GET_PTR_TYPE(entry) == 0); /* no type bits set */\n-\tif (GET_PTR_TYPE(*p) != PTR_TYPE_NOTE)\n-\t\treturn; /* type mismatch, nothing to remove */\n-\tl = (struct leaf_node *) CLR_PTR_TYPE(*p);\n-\tif (hashcmp(l->key_sha1, entry->key_sha1))\n-\t\treturn; /* key mismatch, nothing to remove */\n-\n-\t/* we have found a matching entry */\n-\tfree(l);\n-\t*p = SET_PTR_TYPE(NULL, PTR_TYPE_NULL);\n-\n-\t/* consolidate this tree level, and parent levels, if possible */\n-\tif (!n)\n-\t\treturn; /* cannot consolidate top level */\n-\t/* first, build stack of ancestors between root and current node */\n-\tparent_stack[0] = t->root;\n-\tfor (i = 0; i < n; i++) {\n-\t\tj = GET_NIBBLE(i, entry->key_sha1);\n-\t\tparent_stack[i + 1] = CLR_PTR_TYPE(parent_stack[i]->a[j]);\n-\t}\n-\tassert(i == n && parent_stack[i] == tree);\n-\t/* next, unwind stack until note_tree_consolidate() is done */\n-\twhile (i > 0 &&\n-\t       !note_tree_consolidate(parent_stack[i], parent_stack[i - 1],\n-\t\t\t\t      GET_NIBBLE(i - 1, entry->key_sha1)))\n-\t\ti--;\n-}\n-\n /* Free the entire notes data contained in the given tree */\n static void note_tree_free(struct int_node *tree)\n {\ndiff --git a/notes.h b/notes.h\nindex 20db42f..79ea797 100644\n--- a/notes.h\n+++ b/notes.h\n@@ -12,7 +12,10 @@\n  * resulting SHA1 into the first SHA1 argument (cur_sha1). A non-zero return\n  * value indicates failure.\n  *\n- * The two given SHA1s must both be non-NULL and different from each other.\n+ * The two given SHA1s shall both be non-NULL and different from each other.\n+ * Either of them (but not both) may be == null_sha1, which indicates an\n+ * empty/non-existent note. If the resulting SHA1 (cur_sha1) is == null_sha1,\n+ * the note will be removed from the notes tree.\n  *\n  * The default combine_notes function (you get this when passing NULL) is\n  * combine_notes_concatenate(), which appends the contents of the new note to\n@@ -90,6 +93,17 @@ void init_notes(struct notes_tree *t, const char *notes_ref,\n /*\n  * Add the given note object to the given notes_tree structure\n  *\n+ * If there already exists a note for the given object_sha1, the given\n+ * combine_notes function is invoked to break the tie. If not given (i.e.\n+ * combine_notes == NULL), the default combine_notes function for the given\n+ * notes_tree is used.\n+ *\n+ * Passing note_sha1 == null_sha1 indicates the addition of an\n+ * empty/non-existent note. This is a (potentially expensive) no-op unless\n+ * there already exists a note for the given object_sha1, AND combining that\n+ * note with the empty note (using the given combine_notes function) results\n+ * in a new/changed note.\n+ *\n  * IMPORTANT: The changes made by add_note() to the given notes_tree structure\n  * are not persistent until a subsequent call to write_notes_tree() returns\n  * zero.\n-- \n1.7.2.220.gea1d3\n"},{"id":"146052","messageId":"1279880104-29796-5-git-send-email-johan@herland.net","threadId":"24479","inReplyTo":"1279880104-29796-1-git-send-email-johan@herland.net","subject":"[WIP/RFC 04/13] notes.h/c: Propagate combine_notes_fn return value to add_note() and beyond","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-07-23T10:14:55Z","receivedAt":"2010-07-23T10:14:55Z","isPatch":false,"sender":{"key":"johan@herland.net","avatar":"https://avatars.githubusercontent.com/u/547031?v=4"},"body":"The combine_notes_fn functions uses a non-zero return value to indicate\nfailure. However, this return value was converted to a call to die()\nin note_tree_insert().\n\nInstead, propagate this return value out to add_note(), and return it\nfrom there to enable the caller to handle errors appropriately.\n\nSigned-off-by: Johan Herland <johan@herland.net>\n---\n builtin/notes.c |    3 ++\n notes-cache.c   |    3 +-\n notes.c         |   59 ++++++++++++++++++++++++++++--------------------------\n notes.h         |   11 +++++++--\n 4 files changed, 43 insertions(+), 33 deletions(-)\n\ndiff --git a/builtin/notes.c b/builtin/notes.c\nindex 190005f..f653f59 100644\n--- a/builtin/notes.c\n+++ b/builtin/notes.c\n@@ -574,6 +574,7 @@ static int add(int argc, const char **argv, const char *prefix)\n \tif (is_null_sha1(new_note))\n \t\tremove_note(t, object);\n \telse\n+\t\t/* No return value checking; c_n_overwrite always returns 0 */\n \t\tadd_note(t, object, new_note, combine_notes_overwrite);\n \n \tsnprintf(logmsg, sizeof(logmsg), \"Notes %s by 'git notes %s'\",\n@@ -653,6 +654,7 @@ static int copy(int argc, const char **argv, const char *prefix)\n \t\tgoto out;\n \t}\n \n+\t/* No return value checking; c_n_overwrite always returns 0 */\n \tadd_note(t, object, from_note, combine_notes_overwrite);\n \tcommit_notes(t, \"Notes added by 'git notes copy'\");\n out:\n@@ -713,6 +715,7 @@ static int append_edit(int argc, const char **argv, const char *prefix)\n \tif (is_null_sha1(new_note))\n \t\tremove_note(t, object);\n \telse\n+\t\t/* No return value checking; c_n_overwrite always returns 0 */\n \t\tadd_note(t, object, new_note, combine_notes_overwrite);\n \n \tsnprintf(logmsg, sizeof(logmsg), \"Notes %s by 'git notes %s'\",\ndiff --git a/notes-cache.c b/notes-cache.c\nindex dee6d62..4c8984e 100644\n--- a/notes-cache.c\n+++ b/notes-cache.c\n@@ -89,6 +89,5 @@ int notes_cache_put(struct notes_cache *c, unsigned char key_sha1[20],\n \n \tif (write_sha1_file(data, size, \"blob\", value_sha1) < 0)\n \t\treturn -1;\n-\tadd_note(&c->tree, key_sha1, value_sha1, NULL);\n-\treturn 0;\n+\treturn add_note(&c->tree, key_sha1, value_sha1, NULL);\n }\ndiff --git a/notes.c b/notes.c\nindex 9e92021..4f3d094 100644\n--- a/notes.c\n+++ b/notes.c\n@@ -235,13 +235,14 @@ static void note_tree_remove(struct notes_tree *t, struct int_node *tree,\n  * - Else, create a new int_node, holding both the node-at-location and the\n  *   node-to-be-inserted, and store the new int_node into the location.\n  */\n-static void note_tree_insert(struct notes_tree *t, struct int_node *tree,\n+static int note_tree_insert(struct notes_tree *t, struct int_node *tree,\n \t\tunsigned char n, struct leaf_node *entry, unsigned char type,\n \t\tcombine_notes_fn combine_notes)\n {\n \tstruct int_node *new_node;\n \tstruct leaf_node *l;\n \tvoid **p = note_tree_search(t, &tree, &n, entry->key_sha1);\n+\tint ret = 0;\n \n \tassert(GET_PTR_TYPE(entry) == 0); /* no type bits set */\n \tl = (struct leaf_node *) CLR_PTR_TYPE(*p);\n@@ -252,26 +253,21 @@ static void note_tree_insert(struct notes_tree *t, struct int_node *tree,\n \t\t\tfree(entry);\n \t\telse\n \t\t\t*p = SET_PTR_TYPE(entry, type);\n-\t\treturn;\n+\t\treturn 0;\n \tcase PTR_TYPE_NOTE:\n \t\tswitch (type) {\n \t\tcase PTR_TYPE_NOTE:\n \t\t\tif (!hashcmp(l->key_sha1, entry->key_sha1)) {\n \t\t\t\t/* skip concatenation if l == entry */\n \t\t\t\tif (!hashcmp(l->val_sha1, entry->val_sha1))\n-\t\t\t\t\treturn;\n+\t\t\t\t\treturn 0;\n \n-\t\t\t\tif (combine_notes(l->val_sha1, entry->val_sha1))\n-\t\t\t\t\tdie(\"failed to combine notes %s and %s\"\n-\t\t\t\t\t    \" for object %s\",\n-\t\t\t\t\t    sha1_to_hex(l->val_sha1),\n-\t\t\t\t\t    sha1_to_hex(entry->val_sha1),\n-\t\t\t\t\t    sha1_to_hex(l->key_sha1));\n-\n-\t\t\t\tif (is_null_sha1(l->val_sha1))\n+\t\t\t\tret = combine_notes(l->val_sha1,\n+\t\t\t\t\t\t    entry->val_sha1);\n+\t\t\t\tif (!ret && is_null_sha1(l->val_sha1))\n \t\t\t\t\tnote_tree_remove(t, tree, n, entry);\n \t\t\t\tfree(entry);\n-\t\t\t\treturn;\n+\t\t\t\treturn ret;\n \t\t\t}\n \t\t\tbreak;\n \t\tcase PTR_TYPE_SUBTREE:\n@@ -280,7 +276,7 @@ static void note_tree_insert(struct notes_tree *t, struct int_node *tree,\n \t\t\t\t/* unpack 'entry' */\n \t\t\t\tload_subtree(t, entry, tree, n);\n \t\t\t\tfree(entry);\n-\t\t\t\treturn;\n+\t\t\t\treturn 0;\n \t\t\t}\n \t\t\tbreak;\n \t\t}\n@@ -291,9 +287,8 @@ static void note_tree_insert(struct notes_tree *t, struct int_node *tree,\n \t\t\t*p = NULL;\n \t\t\tload_subtree(t, l, tree, n);\n \t\t\tfree(l);\n-\t\t\tnote_tree_insert(t, tree, n, entry, type,\n-\t\t\t\t\t combine_notes);\n-\t\t\treturn;\n+\t\t\treturn note_tree_insert(t, tree, n, entry, type,\n+\t\t\t\t\t\tcombine_notes);\n \t\t}\n \t\tbreak;\n \t}\n@@ -303,13 +298,17 @@ static void note_tree_insert(struct notes_tree *t, struct int_node *tree,\n \t       GET_PTR_TYPE(*p) == PTR_TYPE_SUBTREE);\n \tif (is_null_sha1(entry->val_sha1)) { /* skip insertion of empty note */\n \t\tfree(entry);\n-\t\treturn;\n+\t\treturn 0;\n \t}\n \tnew_node = (struct int_node *) xcalloc(sizeof(struct int_node), 1);\n-\tnote_tree_insert(t, new_node, n + 1, l, GET_PTR_TYPE(*p),\n-\t\t\t combine_notes);\n-\t*p = SET_PTR_TYPE(new_node, PTR_TYPE_INTERNAL);\n-\tnote_tree_insert(t, new_node, n + 1, entry, type, combine_notes);\n+\tret = note_tree_insert(t, new_node, n + 1, l, GET_PTR_TYPE(*p),\n+\t\t\t       combine_notes);\n+\tif (!ret) {\n+\t\t*p = SET_PTR_TYPE(new_node, PTR_TYPE_INTERNAL);\n+\t\tret = note_tree_insert(t, new_node, n + 1, entry, type,\n+\t\t\t\t       combine_notes);\n+\t}\n+\treturn ret;\n }\n \n /* Free the entire notes data contained in the given tree */\n@@ -452,8 +451,12 @@ static void load_subtree(struct notes_tree *t, struct leaf_node *subtree,\n \t\t\t\tl->key_sha1[19] = (unsigned char) len;\n \t\t\t\ttype = PTR_TYPE_SUBTREE;\n \t\t\t}\n-\t\t\tnote_tree_insert(t, node, n, l, type,\n-\t\t\t\t\t combine_notes_concatenate);\n+\t\t\tif (note_tree_insert(t, node, n, l, type,\n+\t\t\t\t\t     combine_notes_concatenate))\n+\t\t\t\tdie(\"Failed to load %s %s into notes tree \"\n+\t\t\t\t    \"from %s\",\n+\t\t\t\t    type == PTR_TYPE_NOTE ? \"note\" : \"subtree\",\n+\t\t\t\t    sha1_to_hex(l->key_sha1), t->ref);\n \t\t}\n \t\tcontinue;\n \n@@ -1014,7 +1017,7 @@ void init_display_notes(struct display_notes_opt *opt)\n \tstring_list_clear(&display_notes_refs, 0);\n }\n \n-void add_note(struct notes_tree *t, const unsigned char *object_sha1,\n+int add_note(struct notes_tree *t, const unsigned char *object_sha1,\n \t\tconst unsigned char *note_sha1, combine_notes_fn combine_notes)\n {\n \tstruct leaf_node *l;\n@@ -1028,7 +1031,7 @@ void add_note(struct notes_tree *t, const unsigned char *object_sha1,\n \tl = (struct leaf_node *) xmalloc(sizeof(struct leaf_node));\n \thashcpy(l->key_sha1, object_sha1);\n \thashcpy(l->val_sha1, note_sha1);\n-\tnote_tree_insert(t, t->root, 0, l, PTR_TYPE_NOTE, combine_notes);\n+\treturn note_tree_insert(t, t->root, 0, l, PTR_TYPE_NOTE, combine_notes);\n }\n \n void remove_note(struct notes_tree *t, const unsigned char *object_sha1)\n@@ -1204,7 +1207,7 @@ void format_display_notes(const unsigned char *object_sha1,\n \n int copy_note(struct notes_tree *t,\n \t      const unsigned char *from_obj, const unsigned char *to_obj,\n-\t      int force, combine_notes_fn combine_fn)\n+\t      int force, combine_notes_fn combine_notes)\n {\n \tconst unsigned char *note = get_note(t, from_obj);\n \tconst unsigned char *existing_note = get_note(t, to_obj);\n@@ -1213,9 +1216,9 @@ int copy_note(struct notes_tree *t,\n \t\treturn 1;\n \n \tif (note)\n-\t\tadd_note(t, to_obj, note, combine_fn);\n+\t\treturn add_note(t, to_obj, note, combine_notes);\n \telse if (existing_note)\n-\t\tadd_note(t, to_obj, null_sha1, combine_fn);\n+\t\treturn add_note(t, to_obj, null_sha1, combine_notes);\n \n \treturn 0;\n }\ndiff --git a/notes.h b/notes.h\nindex 79ea797..b372575 100644\n--- a/notes.h\n+++ b/notes.h\n@@ -104,11 +104,13 @@ void init_notes(struct notes_tree *t, const char *notes_ref,\n  * note with the empty note (using the given combine_notes function) results\n  * in a new/changed note.\n  *\n+ * Returns zero on success; non-zero means combine_notes failed.\n+ *\n  * IMPORTANT: The changes made by add_note() to the given notes_tree structure\n  * are not persistent until a subsequent call to write_notes_tree() returns\n  * zero.\n  */\n-void add_note(struct notes_tree *t, const unsigned char *object_sha1,\n+int add_note(struct notes_tree *t, const unsigned char *object_sha1,\n \t\tconst unsigned char *note_sha1, combine_notes_fn combine_notes);\n \n /*\n@@ -131,7 +133,10 @@ const unsigned char *get_note(struct notes_tree *t,\n /*\n  * Copy a note from one object to another in the given notes_tree.\n  *\n- * Fails if the to_obj already has a note unless 'force' is true.\n+ * Returns 1 if the to_obj already has a note and 'force' is false. Otherwise,\n+ * returns non-zero if 'force' is true, but the given combine_notes function\n+ * failed to combine from_obj's note with to_obj's existing note.\n+ * Returns zero on success.\n  *\n  * IMPORTANT: The changes made by copy_note() to the given notes_tree structure\n  * are not persistent until a subsequent call to write_notes_tree() returns\n@@ -139,7 +144,7 @@ const unsigned char *get_note(struct notes_tree *t,\n  */\n int copy_note(struct notes_tree *t,\n \t      const unsigned char *from_obj, const unsigned char *to_obj,\n-\t      int force, combine_notes_fn combine_fn);\n+\t      int force, combine_notes_fn combine_notes);\n \n /*\n  * Flags controlling behaviour of for_each_note()\n-- \n1.7.2.220.gea1d3\n"},{"id":"146056","messageId":"1279880104-29796-6-git-send-email-johan@herland.net","threadId":"24479","inReplyTo":"1279880104-29796-1-git-send-email-johan@herland.net","subject":"[WIP/RFC 05/13] (trivial) t3303: Indent with tabs instead of spaces for consistency","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-07-23T10:14:56Z","receivedAt":"2010-07-23T10:14:56Z","isPatch":false,"sender":{"key":"johan@herland.net","avatar":"https://avatars.githubusercontent.com/u/547031?v=4"},"body":"The rest of the file uses tabs for indenting. Fix the one function\nthat doesn't.\n\nSigned-off-by: Johan Herland <johan@herland.net>\n---\n t/t3303-notes-subtrees.sh |   18 +++++++++---------\n 1 files changed, 9 insertions(+), 9 deletions(-)\n\ndiff --git a/t/t3303-notes-subtrees.sh b/t/t3303-notes-subtrees.sh\nindex 75ec187..d571708 100755\n--- a/t/t3303-notes-subtrees.sh\n+++ b/t/t3303-notes-subtrees.sh\n@@ -168,15 +168,15 @@ INPUT_END\n }\n \n verify_concatenated_notes () {\n-    git log | grep \"^    \" > output &&\n-    i=$number_of_commits &&\n-    while [ $i -gt 0 ]; do\n-        echo \"    commit #$i\" &&\n-        echo \"    first note for commit #$i\" &&\n-        echo \"    second note for commit #$i\" &&\n-        i=$(($i-1));\n-    done > expect &&\n-    test_cmp expect output\n+\tgit log | grep \"^    \" > output &&\n+\ti=$number_of_commits &&\n+\twhile [ $i -gt 0 ]; do\n+\t\techo \"    commit #$i\" &&\n+\t\techo \"    first note for commit #$i\" &&\n+\t\techo \"    second note for commit #$i\" &&\n+\t\ti=$(($i-1));\n+\tdone > expect &&\n+\ttest_cmp expect output\n }\n \n test_expect_success 'test notes in no fanout concatenated with 2/38-fanout' 'test_concatenated_notes \"s|^..|&/|\" \"\"'\n-- \n1.7.2.220.gea1d3\n"},{"id":"146058","messageId":"1279880104-29796-7-git-send-email-johan@herland.net","threadId":"24479","inReplyTo":"1279880104-29796-1-git-send-email-johan@herland.net","subject":"[WIP/RFC 06/13] notes.c: Use two newlines (instead of one) when concatenating notes","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-07-23T10:14:57Z","receivedAt":"2010-07-23T10:14:57Z","isPatch":false,"sender":{"key":"johan@herland.net","avatar":"https://avatars.githubusercontent.com/u/547031?v=4"},"body":"When using combine_notes_concatenate() to concatenate notes, it currently\nensures exactly one newline character between the given notes. However,\nwhen using builtin/notes.c:create_note() to concatenate notes (e.g. by\n'git notes append'), it adds a newline character to the trailing newline\nof the preceding notes object, thus resulting in _two_ newlines (aka. a\nblank line) separating contents of the two notes.\n\nThis patch brings combine_notes_concatenate() into consistency with\nbuiltin/notes.c:create_note(), by ensuring exactly _two_ newline characters\nbetween concatenated notes.\n\nThe patch also changes a few notes-related selftests accordingly.\n\nSigned-off-by: Johan Herland <johan@herland.net>\n---\n notes.c                       |    7 ++++---\n t/t3301-notes.sh              |    4 ++++\n t/t3303-notes-subtrees.sh     |    1 +\n t/t3404-rebase-interactive.sh |    1 +\n t/t9301-fast-import-notes.sh  |    5 +++++\n 5 files changed, 15 insertions(+), 3 deletions(-)\n\ndiff --git a/notes.c b/notes.c\nindex 4f3d094..28afe2c 100644\n--- a/notes.c\n+++ b/notes.c\n@@ -814,16 +814,17 @@ int combine_notes_concatenate(unsigned char *cur_sha1,\n \t\treturn 0;\n \t}\n \n-\t/* we will separate the notes by a newline anyway */\n+\t/* we will separate the notes by two newlines anyway */\n \tif (cur_msg[cur_len - 1] == '\\n')\n \t\tcur_len--;\n \n \t/* concatenate cur_msg and new_msg into buf */\n-\tbuf_len = cur_len + 1 + new_len;\n+\tbuf_len = cur_len + 2 + new_len;\n \tbuf = (char *) xmalloc(buf_len);\n \tmemcpy(buf, cur_msg, cur_len);\n \tbuf[cur_len] = '\\n';\n-\tmemcpy(buf + cur_len + 1, new_msg, new_len);\n+\tbuf[cur_len + 1] = '\\n';\n+\tmemcpy(buf + cur_len + 2, new_msg, new_len);\n \tfree(cur_msg);\n \tfree(new_msg);\n \ndiff --git a/t/t3301-notes.sh b/t/t3301-notes.sh\nindex 1d82f79..4bf4e52 100755\n--- a/t/t3301-notes.sh\n+++ b/t/t3301-notes.sh\n@@ -955,6 +955,7 @@ Date:   Thu Apr 7 15:27:13 2005 -0700\n \n Notes (other):\n     a fresh note\n+$whitespace\n     another fresh note\n EOF\n \n@@ -976,8 +977,11 @@ Date:   Thu Apr 7 15:27:13 2005 -0700\n \n Notes (other):\n     a fresh note\n+$whitespace\n     another fresh note\n+$whitespace\n     append 1\n+$whitespace\n     append 2\n EOF\n \ndiff --git a/t/t3303-notes-subtrees.sh b/t/t3303-notes-subtrees.sh\nindex d571708..704aee8 100755\n--- a/t/t3303-notes-subtrees.sh\n+++ b/t/t3303-notes-subtrees.sh\n@@ -173,6 +173,7 @@ verify_concatenated_notes () {\n \twhile [ $i -gt 0 ]; do\n \t\techo \"    commit #$i\" &&\n \t\techo \"    first note for commit #$i\" &&\n+\t\techo \"    \" &&\n \t\techo \"    second note for commit #$i\" &&\n \t\ti=$(($i-1));\n \tdone > expect &&\ndiff --git a/t/t3404-rebase-interactive.sh b/t/t3404-rebase-interactive.sh\nindex 47ca88f..4b004bc 100755\n--- a/t/t3404-rebase-interactive.sh\n+++ b/t/t3404-rebase-interactive.sh\n@@ -593,6 +593,7 @@ test_expect_success 'rebase -i can copy notes' '\n \n cat >expect <<EOF\n an earlier note\n+\n a note\n EOF\n \ndiff --git a/t/t9301-fast-import-notes.sh b/t/t9301-fast-import-notes.sh\nindex a5c99d8..7cf8cd8 100755\n--- a/t/t9301-fast-import-notes.sh\n+++ b/t/t9301-fast-import-notes.sh\n@@ -255,13 +255,18 @@ EOF\n \n INPUT_END\n \n+whitespace=\"    \"\n+\n cat >expect <<EXPECT_END\n     fourth commit\n     pre-prefix of note for fourth commit\n+$whitespace\n     prefix of note for fourth commit\n+$whitespace\n     third note for fourth commit\n     third commit\n     prefix of note for third commit\n+$whitespace\n     third note for third commit\n     second commit\n     third note for second commit\n-- \n1.7.2.220.gea1d3\n"},{"id":"146055","messageId":"1279880104-29796-8-git-send-email-johan@herland.net","threadId":"24479","inReplyTo":"1279880104-29796-1-git-send-email-johan@herland.net","subject":"[WIP/RFC 07/13] (trivial) git notes prune: Accept --verbose in addition to -v","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-07-23T10:14:58Z","receivedAt":"2010-07-23T10:14:58Z","isPatch":false,"sender":{"key":"johan@herland.net","avatar":"https://avatars.githubusercontent.com/u/547031?v=4"},"body":"Signed-off-by: Johan Herland <johan@herland.net>\n---\n Documentation/git-notes.txt |    4 +++-\n builtin/notes.c             |    2 +-\n 2 files changed, 4 insertions(+), 2 deletions(-)\n\ndiff --git a/Documentation/git-notes.txt b/Documentation/git-notes.txt\nindex 5540af5..ce0ea03 100644\n--- a/Documentation/git-notes.txt\n+++ b/Documentation/git-notes.txt\n@@ -133,7 +133,9 @@ OPTIONS\n \twould be removed.\n \n -v::\n-\tReport all object names whose notes are removed.\n+--verbose::\n+\tWhen pruning notes, report all object names whose notes are\n+\tremoved.\n \n \n DISCUSSION\ndiff --git a/builtin/notes.c b/builtin/notes.c\nindex f653f59..888a9e3 100644\n--- a/builtin/notes.c\n+++ b/builtin/notes.c\n@@ -802,7 +802,7 @@ static int prune(int argc, const char **argv, const char *prefix)\n \tint show_only = 0, verbose = 0;\n \tstruct option options[] = {\n \t\tOPT_BOOLEAN('n', NULL, &show_only, \"do not remove, show only\"),\n-\t\tOPT_BOOLEAN('v', NULL, &verbose, \"report pruned notes\"),\n+\t\tOPT_BOOLEAN('v', \"verbose\", &verbose, \"report pruned notes\"),\n \t\tOPT_END()\n \t};\n \n-- \n1.7.2.220.gea1d3\n"},{"id":"146061","messageId":"1279880104-29796-9-git-send-email-johan@herland.net","threadId":"24479","inReplyTo":"1279880104-29796-1-git-send-email-johan@herland.net","subject":"[WIP/RFC 08/13] builtin/notes.c: Split notes ref DWIMmery into a separate function","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-07-23T10:14:59Z","receivedAt":"2010-07-23T10:14:59Z","isPatch":false,"sender":{"key":"johan@herland.net","avatar":"https://avatars.githubusercontent.com/u/547031?v=4"},"body":"expand_notes_ref() is a new function that performs the DWIM transformation\nof \"foo\" -> \"refs/notes/foo\" where notes refs are expected.\n\nThis is done in preparation for future patches which will also need this\nDWIM functionality.\n\nSigned-off-by: Johan Herland <johan@herland.net>\n---\n builtin/notes.c |   17 +++++++++++------\n 1 files changed, 11 insertions(+), 6 deletions(-)\n\ndiff --git a/builtin/notes.c b/builtin/notes.c\nindex 888a9e3..e19042c 100644\n--- a/builtin/notes.c\n+++ b/builtin/notes.c\n@@ -83,6 +83,16 @@ struct msg_arg {\n \tstruct strbuf buf;\n };\n \n+static void expand_notes_ref(struct strbuf *sb)\n+{\n+\tif (!prefixcmp(sb->buf, \"refs/notes/\"))\n+\t\treturn; /* we're happy */\n+\telse if (!prefixcmp(sb->buf, \"notes/\"))\n+\t\tstrbuf_insert(sb, 0, \"refs/\", 5);\n+\telse\n+\t\tstrbuf_insert(sb, 0, \"refs/notes/\", 11);\n+}\n+\n static int list_each_note(const unsigned char *object_sha1,\n \t\tconst unsigned char *note_sha1, char *note_path,\n \t\tvoid *cb_data)\n@@ -840,13 +850,8 @@ int cmd_notes(int argc, const char **argv, const char *prefix)\n \n \tif (override_notes_ref) {\n \t\tstruct strbuf sb = STRBUF_INIT;\n-\t\tif (!prefixcmp(override_notes_ref, \"refs/notes/\"))\n-\t\t\t/* we're happy */;\n-\t\telse if (!prefixcmp(override_notes_ref, \"notes/\"))\n-\t\t\tstrbuf_addstr(&sb, \"refs/\");\n-\t\telse\n-\t\t\tstrbuf_addstr(&sb, \"refs/notes/\");\n \t\tstrbuf_addstr(&sb, override_notes_ref);\n+\t\texpand_notes_ref(&sb);\n \t\tsetenv(\"GIT_NOTES_REF\", sb.buf, 1);\n \t\tstrbuf_release(&sb);\n \t}\n-- \n1.7.2.220.gea1d3\n"},{"id":"146060","messageId":"1279880104-29796-10-git-send-email-johan@herland.net","threadId":"24479","inReplyTo":"1279880104-29796-1-git-send-email-johan@herland.net","subject":"[WIP/RFC 09/13] git notes merge: Initial implementation handling trivial merges only","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-07-23T10:15:00Z","receivedAt":"2010-07-23T10:15:00Z","isPatch":false,"sender":{"key":"johan@herland.net","avatar":"https://avatars.githubusercontent.com/u/547031?v=4"},"body":"This initial implementation of 'git notes merge' only handles the trivial\nmerge cases (i.e. where the merge is either a no-op, or a fast-forward).\n\nThe patch includes testcases for these trivial merge cases.\n\nFuture patches will extend the functionality of 'git notes merge'.\n\nSigned-off-by: Johan Herland <johan@herland.net>\n---\n Makefile               |    2 +\n builtin/notes.c        |   56 +++++++++\n notes-merge.c          |  103 ++++++++++++++++\n notes-merge.h          |   30 +++++\n t/t3308-notes-merge.sh |  313 ++++++++++++++++++++++++++++++++++++++++++++++++\n 5 files changed, 504 insertions(+), 0 deletions(-)\n create mode 100644 notes-merge.c\n create mode 100644 notes-merge.h\n create mode 100755 t/t3308-notes-merge.sh\n\ndiff --git a/Makefile b/Makefile\nindex bc3c570..8a2a767 100644\n--- a/Makefile\n+++ b/Makefile\n@@ -503,6 +503,7 @@ LIB_H += mailmap.h\n LIB_H += merge-recursive.h\n LIB_H += notes.h\n LIB_H += notes-cache.h\n+LIB_H += notes-merge.h\n LIB_H += object.h\n LIB_H += pack.h\n LIB_H += pack-refs.h\n@@ -593,6 +594,7 @@ LIB_OBJS += merge-recursive.o\n LIB_OBJS += name-hash.o\n LIB_OBJS += notes.o\n LIB_OBJS += notes-cache.o\n+LIB_OBJS += notes-merge.o\n LIB_OBJS += object.o\n LIB_OBJS += pack-check.o\n LIB_OBJS += pack-refs.o\ndiff --git a/builtin/notes.c b/builtin/notes.c\nindex e19042c..3a15666 100644\n--- a/builtin/notes.c\n+++ b/builtin/notes.c\n@@ -17,6 +17,7 @@\n #include \"run-command.h\"\n #include \"parse-options.h\"\n #include \"string-list.h\"\n+#include \"notes-merge.h\"\n \n static const char * const git_notes_usage[] = {\n \t\"git notes [--ref <notes_ref>] [list [<object>]]\",\n@@ -25,6 +26,7 @@ static const char * const git_notes_usage[] = {\n \t\"git notes [--ref <notes_ref>] append [-m <msg> | -F <file> | (-c | -C) <object>] [<object>]\",\n \t\"git notes [--ref <notes_ref>] edit [<object>]\",\n \t\"git notes [--ref <notes_ref>] show [<object>]\",\n+\t\"git notes [--ref <notes_ref>] merge [-v | -q] <notes_ref>\",\n \t\"git notes [--ref <notes_ref>] remove [<object>]\",\n \t\"git notes [--ref <notes_ref>] prune [-n | -v]\",\n \tNULL\n@@ -61,6 +63,11 @@ static const char * const git_notes_show_usage[] = {\n \tNULL\n };\n \n+static const char * const git_notes_merge_usage[] = {\n+\t\"git notes merge [<options>] <notes_ref>\",\n+\tNULL\n+};\n+\n static const char * const git_notes_remove_usage[] = {\n \t\"git notes remove [<object>]\",\n \tNULL\n@@ -774,6 +781,53 @@ static int show(int argc, const char **argv, const char *prefix)\n \treturn retval;\n }\n \n+static int merge(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct strbuf remote_ref = STRBUF_INIT, msg = STRBUF_INIT;\n+\tunsigned char result_sha1[20];\n+\tstruct notes_merge_options o;\n+\tint verbosity = 0, result;\n+\tstruct option options[] = {\n+\t\tOPT__VERBOSITY(&verbosity),\n+\t\tOPT_END()\n+\t};\n+\n+\targc = parse_options(argc, argv, prefix, options,\n+\t\t\t     git_notes_merge_usage, 0);\n+\n+\tif (1 < argc) {\n+\t\terror(\"too many parameters\");\n+\t\tusage_with_options(git_notes_merge_usage, options);\n+\t} else if (1 > argc) {\n+\t\terror(\"too few parameters\");\n+\t\tusage_with_options(git_notes_merge_usage, options);\n+\t}\n+\n+\tinit_notes_merge_options(&o);\n+\to.verbosity = verbosity + 2; // default verbosity level is 2\n+\to.local_ref = default_notes_ref();\n+\tstrbuf_addstr(&remote_ref, argv[0]);\n+\texpand_notes_ref(&remote_ref);\n+\to.remote_ref = remote_ref.buf;\n+\n+\tresult = notes_merge(&o, result_sha1);\n+\n+\tstrbuf_addf(&msg, \"notes: Merged notes from %s into %s\",\n+\t\t    remote_ref.buf, default_notes_ref());\n+\tif (result == 0) { /* Merge resulted (trivially) in result_sha1 */\n+\t\t/* Update default notes ref with new commit */\n+\t\tupdate_ref(msg.buf, default_notes_ref(), result_sha1, NULL,\n+\t\t\t   0, DIE_ON_ERR);\n+\t} else {\n+\t\t/* TODO: */\n+\t\tdie(\"'git notes merge' cannot yet handle non-trivial merges!\");\n+\t}\n+\n+\tstrbuf_release(&remote_ref);\n+\tstrbuf_release(&msg);\n+\treturn 0;\n+}\n+\n static int remove_cmd(int argc, const char **argv, const char *prefix)\n {\n \tstruct option options[] = {\n@@ -866,6 +920,8 @@ int cmd_notes(int argc, const char **argv, const char *prefix)\n \t\tresult = append_edit(argc, argv, prefix);\n \telse if (!strcmp(argv[0], \"show\"))\n \t\tresult = show(argc, argv, prefix);\n+\telse if (!strcmp(argv[0], \"merge\"))\n+\t\tresult = merge(argc, argv, prefix);\n \telse if (!strcmp(argv[0], \"remove\"))\n \t\tresult = remove_cmd(argc, argv, prefix);\n \telse if (!strcmp(argv[0], \"prune\"))\ndiff --git a/notes-merge.c b/notes-merge.c\nnew file mode 100644\nindex 0000000..5461827\n--- /dev/null\n+++ b/notes-merge.c\n@@ -0,0 +1,103 @@\n+#include \"cache.h\"\n+#include \"commit.h\"\n+#include \"notes-merge.h\"\n+\n+void init_notes_merge_options(struct notes_merge_options *o)\n+{\n+\tmemset(o, 0, sizeof(struct notes_merge_options));\n+\to->verbosity = 2;\n+}\n+\n+static int show(struct notes_merge_options *o, int v)\n+{\n+\treturn (o->verbosity >= v) || o->verbosity >= 5;\n+}\n+\n+#define OUTPUT(o, v, ...) \\\n+\tdo { if (show((o), (v))) { printf(__VA_ARGS__); puts(\"\"); } } while (0)\n+\n+int notes_merge(struct notes_merge_options *o,\n+\t\tunsigned char *result_sha1)\n+{\n+\tunsigned char local_sha1[20], remote_sha1[20];\n+\tstruct commit *local, *remote;\n+\tstruct commit_list *bases = NULL;\n+\tconst unsigned char *base_sha1;\n+\tint result = 0;\n+\n+\thashclr(result_sha1);\n+\n+\tOUTPUT(o, 5, \"notes_merge(o->local_ref = %s, o->remote_ref = %s)\",\n+\t       o->local_ref, o->remote_ref);\n+\n+\tif (!o->local_ref || get_sha1(o->local_ref, local_sha1)) {\n+\t\t/* empty notes ref => assume empty notes tree */\n+\t\thashclr(local_sha1);\n+\t\tlocal = NULL;\n+\t} else if (!(local = lookup_commit_reference(local_sha1)))\n+\t\tdie(\"Could not parse commit '%s'.\", o->local_ref);\n+\tOUTPUT(o, 5, \"\\tlocal commit: %.7s\", sha1_to_hex(local_sha1));\n+\n+\tif (!o->remote_ref || get_sha1(o->remote_ref, remote_sha1)) {\n+\t\t/* empty notes ref => assume empty notes tree */\n+\t\thashclr(remote_sha1);\n+\t\tremote = NULL;\n+\t}\n+\tif (!(remote = lookup_commit_reference(remote_sha1)))\n+\t\tdie(\"Could not parse commit '%s'.\", o->remote_ref);\n+\tOUTPUT(o, 5, \"\\tremote commit: %.7s\", sha1_to_hex(remote_sha1));\n+\n+\tif (!local) {\n+\t\t/* result == remote commit */\n+\t\thashcpy(result_sha1, remote_sha1);\n+\t\tgoto found_result;\n+\t}\n+\tif (!remote) {\n+\t\t/* result == local commit */\n+\t\thashcpy(result_sha1, local_sha1);\n+\t\tgoto found_result;\n+\t}\n+\tassert(local && remote);\n+\n+\t/* Find merge bases */\n+\tbases = get_merge_bases(local, remote, 1);\n+\tif (!bases) {\n+\t\tbase_sha1 = null_sha1;\n+\t\tOUTPUT(o, 4, \"No merge base found; doing history-less merge\");\n+\t} else if (!bases->next) {\n+\t\tbase_sha1 = bases->item->object.sha1;\n+\t\tOUTPUT(o, 4, \"One merge base found (%.7s)\",\n+\t\t       sha1_to_hex(base_sha1));\n+\t} else {\n+\t\t/* TODO: How to handle multiple merge-bases? */\n+\t\tbase_sha1 = bases->item->object.sha1;\n+\t\tOUTPUT(o, 3, \"Multiple merge bases found. Using the first \"\n+\t\t       \"(%.7s)\", sha1_to_hex(base_sha1));\n+\t}\n+\n+\tOUTPUT(o, 4, \"Merging remote commit %.7s into local commit %.7s with \"\n+\t       \"merge-base %.7s\", sha1_to_hex(remote->object.sha1),\n+\t       sha1_to_hex(local->object.sha1), sha1_to_hex(base_sha1));\n+\n+\tif (!hashcmp(remote->object.sha1, base_sha1)) {\n+\t\t/* Already merged; result == local commit */\n+\t\tOUTPUT(o, 2, \"Already up-to-date!\");\n+\t\thashcpy(result_sha1, local->object.sha1);\n+\t\tgoto found_result;\n+\t}\n+\tif (!hashcmp(local->object.sha1, base_sha1)) {\n+\t\t/* Fast-forward; result == remote commit */\n+\t\tOUTPUT(o, 2, \"Fast-forward\");\n+\t\thashcpy(result_sha1, remote->object.sha1);\n+\t\tgoto found_result;\n+\t}\n+\n+\t/* TODO: */\n+\tresult = error(\"notes_merge() cannot yet handle real merges.\");\n+\n+found_result:\n+\tfree_commit_list(bases);\n+\tOUTPUT(o, 5, \"notes_merge(): result = %i, result_sha1 = %.7s\",\n+\t       result, sha1_to_hex(result_sha1));\n+\treturn result;\n+}\ndiff --git a/notes-merge.h b/notes-merge.h\nnew file mode 100644\nindex 0000000..c4416e1\n--- /dev/null\n+++ b/notes-merge.h\n@@ -0,0 +1,30 @@\n+#ifndef NOTES_MERGE_H\n+#define NOTES_MERGE_H\n+\n+struct notes_merge_options {\n+\tconst char *local_ref;\n+\tconst char *remote_ref;\n+\tint verbosity;\n+};\n+\n+void init_notes_merge_options(struct notes_merge_options *o);\n+\n+/*\n+ * Merge notes from o->remote_ref into o->local_ref\n+ *\n+ * The commits given by the two refs are merged, producing one of the following\n+ * outcomes:\n+ *\n+ * 1. The merge trivially results in an existing commit (e.g. fast-forward or\n+ *    already-up-to-date). The SHA1 of the result is written into 'result_sha1'\n+ *    and 0 is returned.\n+ * 2. The merge fails. result_sha1 is set to null_sha1, and non-zero returned.\n+ *\n+ * Either ref (but not both) may not exist in which case the missing ref is\n+ * interpreted as an empty notes tree, and the merge trivially results in\n+ * what the other ref points to.\n+ */\n+int notes_merge(struct notes_merge_options *o,\n+\t\tunsigned char *result_sha1);\n+\n+#endif\ndiff --git a/t/t3308-notes-merge.sh b/t/t3308-notes-merge.sh\nnew file mode 100755\nindex 0000000..a89fd11\n--- /dev/null\n+++ b/t/t3308-notes-merge.sh\n@@ -0,0 +1,313 @@\n+#!/bin/sh\n+#\n+# Copyright (c) 2010 Johan Herland\n+#\n+\n+test_description='Test merging of notes trees'\n+\n+. ./test-lib.sh\n+\n+whitespace=\"    \"\n+\n+test_expect_success setup '\n+\tgit config core.notesRef refs/notes/x &&\n+\t: > a1 &&\n+\tgit add a1 &&\n+\ttest_tick &&\n+\tgit commit -m 1st &&\n+\tgit notes add -m \"Notes on 1st commit\" &&\n+\t: > a2 &&\n+\tgit add a2 &&\n+\ttest_tick &&\n+\tgit commit -m 2nd &&\n+\tgit notes add -m \"Notes on 2nd commit\" &&\n+\t: > a3 &&\n+\tgit add a3 &&\n+\ttest_tick &&\n+\tgit commit -m 3rd &&\n+\tgit notes add -m \"Notes on 3rd commit\" &&\n+\t: > a4 &&\n+\tgit add a4 &&\n+\ttest_tick &&\n+\tgit commit -m 4th &&\n+\tgit notes add -m \"Notes on 4th commit\" &&\n+\t: > a5 &&\n+\tgit add a5 &&\n+\ttest_tick &&\n+\tgit commit -m 5th\n+'\n+\n+cat >expect_notes_x <<EOF\n+5e93d24084d32e1cb61f7070505b9d2530cca987 15023535574ded8b1a89052b32673f84cf9582b8\n+8366731eeee53787d2bdf8fc1eff7d94757e8da0 1584215f1d29c65e99c6c6848626553fdd07fd75\n+eede89064cd42441590d6afec6c37b321ada3389 268048bfb8a1fb38e703baceb8ab235421bf80c5\n+daa55ffad6cb99bf64226532147ffcaf5ce8bdd1 34b09d6ffa51a8a03203627f0e369f607227364f\n+EOF\n+\n+cat >expect_log_x <<EOF\n+commit bd1753200303d0a0344be813e504253b3d98e74d\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:17:13 2005 -0700\n+\n+    5th\n+\n+commit 15023535574ded8b1a89052b32673f84cf9582b8\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:16:13 2005 -0700\n+\n+    4th\n+\n+Notes (x):\n+    Notes on 4th commit\n+\n+commit 1584215f1d29c65e99c6c6848626553fdd07fd75\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:15:13 2005 -0700\n+\n+    3rd\n+\n+Notes (x):\n+    Notes on 3rd commit\n+\n+commit 268048bfb8a1fb38e703baceb8ab235421bf80c5\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:14:13 2005 -0700\n+\n+    2nd\n+\n+Notes (x):\n+    Notes on 2nd commit\n+\n+commit 34b09d6ffa51a8a03203627f0e369f607227364f\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:13:13 2005 -0700\n+\n+    1st\n+\n+Notes (x):\n+    Notes on 1st commit\n+EOF\n+\n+test_expect_success 'verify initial notes (x)' '\n+\tgit notes >output_notes_x &&\n+\ttest_cmp expect_notes_x output_notes_x &&\n+\tgit log >output_log_x &&\n+\ttest_cmp expect_log_x output_log_x\n+'\n+\n+cp expect_notes_x expect_notes_y\n+\n+cat >expect_log_y <<EOF\n+commit bd1753200303d0a0344be813e504253b3d98e74d\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:17:13 2005 -0700\n+\n+    5th\n+\n+commit 15023535574ded8b1a89052b32673f84cf9582b8\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:16:13 2005 -0700\n+\n+    4th\n+\n+Notes (y):\n+    Notes on 4th commit\n+\n+commit 1584215f1d29c65e99c6c6848626553fdd07fd75\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:15:13 2005 -0700\n+\n+    3rd\n+\n+Notes (y):\n+    Notes on 3rd commit\n+\n+commit 268048bfb8a1fb38e703baceb8ab235421bf80c5\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:14:13 2005 -0700\n+\n+    2nd\n+\n+Notes (y):\n+    Notes on 2nd commit\n+\n+commit 34b09d6ffa51a8a03203627f0e369f607227364f\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:13:13 2005 -0700\n+\n+    1st\n+\n+Notes (y):\n+    Notes on 1st commit\n+EOF\n+\n+test_expect_success 'merge notes into empty notes ref (x => y)' '\n+\tgit config core.notesRef refs/notes/y &&\n+\tgit notes merge x &&\n+\tgit notes >output_notes_y &&\n+\ttest_cmp expect_notes_y output_notes_y &&\n+\tgit log >output_log_y &&\n+\ttest_cmp expect_log_y output_log_y &&\n+\t# x and y should point to the same notes commit\n+\ttest \"$(git rev-parse refs/notes/x)\" = \"$(git rev-parse refs/notes/y)\"\n+'\n+\n+test_expect_success 'change notes on other notes ref (y)' '\n+\t# Leave notes to 1st commit alone\n+\t# Remove notes from 2nd commit\n+\tgit notes remove HEAD~3 &&\n+\t# Append to 3rd commit notes\n+\tgit notes append -m \"More notes on 3rd commit\" HEAD~2 &&\n+\t# Replace 4th commit notes\n+\tgit notes add -f -m \"New notes on 4th commit\" HEAD^ &&\n+\t# Add new notes to 5th commit\n+\tgit notes add -m \"Notes on 5th commit\" HEAD\n+'\n+\n+cat >expect_notes_y <<EOF\n+dec2502dac3ea161543f71930044deff93fa945c 15023535574ded8b1a89052b32673f84cf9582b8\n+4069cdb399fd45463ec6eef8e051a16a03592d91 1584215f1d29c65e99c6c6848626553fdd07fd75\n+daa55ffad6cb99bf64226532147ffcaf5ce8bdd1 34b09d6ffa51a8a03203627f0e369f607227364f\n+0f2efbd00262f2fd41dfae33df8765618eeacd99 bd1753200303d0a0344be813e504253b3d98e74d\n+EOF\n+\n+cat >expect_log_y <<EOF\n+commit bd1753200303d0a0344be813e504253b3d98e74d\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:17:13 2005 -0700\n+\n+    5th\n+\n+Notes (y):\n+    Notes on 5th commit\n+\n+commit 15023535574ded8b1a89052b32673f84cf9582b8\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:16:13 2005 -0700\n+\n+    4th\n+\n+Notes (y):\n+    New notes on 4th commit\n+\n+commit 1584215f1d29c65e99c6c6848626553fdd07fd75\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:15:13 2005 -0700\n+\n+    3rd\n+\n+Notes (y):\n+    Notes on 3rd commit\n+$whitespace\n+    More notes on 3rd commit\n+\n+commit 268048bfb8a1fb38e703baceb8ab235421bf80c5\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:14:13 2005 -0700\n+\n+    2nd\n+\n+commit 34b09d6ffa51a8a03203627f0e369f607227364f\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:13:13 2005 -0700\n+\n+    1st\n+\n+Notes (y):\n+    Notes on 1st commit\n+EOF\n+\n+test_expect_success 'verify changed notes on other notes ref (y)' '\n+\tgit notes >output_notes_y &&\n+\ttest_cmp expect_notes_y output_notes_y &&\n+\tgit log >output_log_y &&\n+\ttest_cmp expect_log_y output_log_y\n+'\n+\n+test_expect_success 'verify unchanged notes on original notes ref (x)' '\n+\tGIT_NOTES_REF=refs/notes/x git notes >output_notes_x &&\n+\ttest_cmp expect_notes_x output_notes_x &&\n+\tGIT_NOTES_REF=refs/notes/x git log >output_log_x &&\n+\ttest_cmp expect_log_x output_log_x\n+'\n+\n+test_expect_success 'merge original notes (x) into changed notes (y) => No-op' '\n+\tgit notes merge x &&\n+\t# Verify that nothing changed on notes ref (y)\n+\tgit notes >output_notes_y &&\n+\ttest_cmp expect_notes_y output_notes_y &&\n+\tgit log >output_log_y &&\n+\ttest_cmp expect_log_y output_log_y &&\n+\t# Also verify that nothing changed on original notes ref (x)\n+\tGIT_NOTES_REF=refs/notes/x git notes >output_notes_x &&\n+\ttest_cmp expect_notes_x output_notes_x &&\n+\tGIT_NOTES_REF=refs/notes/x git log >output_log_x &&\n+\ttest_cmp expect_log_x output_log_x\n+'\n+\n+cp expect_notes_y expect_notes_x\n+\n+cat >expect_log_x <<EOF\n+commit bd1753200303d0a0344be813e504253b3d98e74d\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:17:13 2005 -0700\n+\n+    5th\n+\n+Notes (x):\n+    Notes on 5th commit\n+\n+commit 15023535574ded8b1a89052b32673f84cf9582b8\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:16:13 2005 -0700\n+\n+    4th\n+\n+Notes (x):\n+    New notes on 4th commit\n+\n+commit 1584215f1d29c65e99c6c6848626553fdd07fd75\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:15:13 2005 -0700\n+\n+    3rd\n+\n+Notes (x):\n+    Notes on 3rd commit\n+$whitespace\n+    More notes on 3rd commit\n+\n+commit 268048bfb8a1fb38e703baceb8ab235421bf80c5\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:14:13 2005 -0700\n+\n+    2nd\n+\n+commit 34b09d6ffa51a8a03203627f0e369f607227364f\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:13:13 2005 -0700\n+\n+    1st\n+\n+Notes (x):\n+    Notes on 1st commit\n+EOF\n+\n+test_expect_success 'merge changed (y) into original (x) => Fast-forward' '\n+\tgit config core.notesRef refs/notes/x &&\n+\tgit notes merge y &&\n+\t# Verify new state of notes on notes ref (x)\n+\tgit notes >output_notes_x &&\n+\ttest_cmp expect_notes_x output_notes_x &&\n+\tgit log >output_log_x &&\n+\ttest_cmp expect_log_x output_log_x &&\n+\t# Also verify that nothing changed on other notes ref (y)\n+\tGIT_NOTES_REF=refs/notes/y git notes >output_notes_y &&\n+\ttest_cmp expect_notes_y output_notes_y &&\n+\tGIT_NOTES_REF=refs/notes/y git log >output_log_y &&\n+\ttest_cmp expect_log_y output_log_y &&\n+\t# x and y should point to same the notes commit\n+\ttest \"$(git rev-parse refs/notes/x)\" = \"$(git rev-parse refs/notes/y)\"\n+'\n+\n+test_done\n-- \n1.7.2.220.gea1d3\n"},{"id":"146057","messageId":"1279880104-29796-11-git-send-email-johan@herland.net","threadId":"24479","inReplyTo":"1279880104-29796-1-git-send-email-johan@herland.net","subject":"[WIP/RFC 10/13] builtin/notes.c: Refactor creation of notes commits.","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-07-23T10:15:01Z","receivedAt":"2010-07-23T10:15:01Z","isPatch":false,"sender":{"key":"johan@herland.net","avatar":"https://avatars.githubusercontent.com/u/547031?v=4"},"body":"Create new function create_notes_commit() which is slightly more general than\ncommit_notes() (accepts multiple commit parents and does not auto-update the\nnotes ref). This function will be used by the notes-merge functionality in\nfuture patches.\n\nAlso rewrite builtin/notes.c:commit_notes() to reuse this new function.\n\nSigned-off-by: Johan Herland <johan@herland.net>\n---\n builtin.h       |    2 +-\n builtin/notes.c |   28 +++++-----------------------\n notes-merge.c   |   27 +++++++++++++++++++++++++++\n notes-merge.h   |   14 ++++++++++++++\n 4 files changed, 47 insertions(+), 24 deletions(-)\n\ndiff --git a/builtin.h b/builtin.h\nindex ed6ee26..908d850 100644\n--- a/builtin.h\n+++ b/builtin.h\n@@ -17,7 +17,7 @@ extern void prune_packed_objects(int);\n extern int fmt_merge_msg(int merge_summary, struct strbuf *in,\n \tstruct strbuf *out);\n extern int fmt_merge_msg_shortlog(struct strbuf *in, struct strbuf *out);\n-extern int commit_notes(struct notes_tree *t, const char *msg);\n+extern void commit_notes(struct notes_tree *t, const char *msg);\n \n struct notes_rewrite_cfg {\n \tstruct notes_tree **trees;\ndiff --git a/builtin/notes.c b/builtin/notes.c\nindex 3a15666..8fcf207 100644\n--- a/builtin/notes.c\n+++ b/builtin/notes.c\n@@ -288,18 +288,17 @@ static int parse_reedit_arg(const struct option *opt, const char *arg, int unset\n \treturn parse_reuse_arg(opt, arg, unset);\n }\n \n-int commit_notes(struct notes_tree *t, const char *msg)\n+void commit_notes(struct notes_tree *t, const char *msg)\n {\n-\tstruct commit_list *parent;\n-\tunsigned char tree_sha1[20], prev_commit[20], new_commit[20];\n \tstruct strbuf buf = STRBUF_INIT;\n+\tunsigned char commit_sha1[20];\n \n \tif (!t)\n \t\tt = &default_notes_tree;\n \tif (!t->initialized || !t->ref || !*t->ref)\n \t\tdie(\"Cannot commit uninitialized/unreferenced notes tree\");\n \tif (!t->dirty)\n-\t\treturn 0; /* don't have to commit an unchanged tree */\n+\t\treturn; /* don't have to commit an unchanged tree */\n \n \t/* Prepare commit message and reflog message */\n \tstrbuf_addstr(&buf, \"notes: \"); /* commit message starts at index 7 */\n@@ -307,27 +306,10 @@ int commit_notes(struct notes_tree *t, const char *msg)\n \tif (buf.buf[buf.len - 1] != '\\n')\n \t\tstrbuf_addch(&buf, '\\n'); /* Make sure msg ends with newline */\n \n-\t/* Convert notes tree to tree object */\n-\tif (write_notes_tree(t, tree_sha1))\n-\t\tdie(\"Failed to write current notes tree to database\");\n-\n-\t/* Create new commit for the tree object */\n-\tif (!read_ref(t->ref, prev_commit)) { /* retrieve parent commit */\n-\t\tparent = xmalloc(sizeof(*parent));\n-\t\tparent->item = lookup_commit(prev_commit);\n-\t\tparent->next = NULL;\n-\t} else {\n-\t\thashclr(prev_commit);\n-\t\tparent = NULL;\n-\t}\n-\tif (commit_tree(buf.buf + 7, tree_sha1, parent, new_commit, NULL))\n-\t\tdie(\"Failed to commit notes tree to database\");\n-\n-\t/* Update notes ref with new commit */\n-\tupdate_ref(buf.buf, t->ref, new_commit, prev_commit, 0, DIE_ON_ERR);\n+\tcreate_notes_commit(t, NULL, buf.buf + 7, commit_sha1);\n+\tupdate_ref(buf.buf, t->ref, commit_sha1, NULL, 0, DIE_ON_ERR);\n \n \tstrbuf_release(&buf);\n-\treturn 0;\n }\n \n combine_notes_fn parse_combine_notes_fn(const char *v)\ndiff --git a/notes-merge.c b/notes-merge.c\nindex 5461827..9a4dc6d 100644\n--- a/notes-merge.c\n+++ b/notes-merge.c\n@@ -1,5 +1,6 @@\n #include \"cache.h\"\n #include \"commit.h\"\n+#include \"notes.h\"\n #include \"notes-merge.h\"\n \n void init_notes_merge_options(struct notes_merge_options *o)\n@@ -16,6 +17,32 @@ static int show(struct notes_merge_options *o, int v)\n #define OUTPUT(o, v, ...) \\\n \tdo { if (show((o), (v))) { printf(__VA_ARGS__); puts(\"\"); } } while (0)\n \n+void create_notes_commit(struct notes_tree *t, struct commit_list *parents,\n+\t\t\t const char *msg, unsigned char *result_sha1)\n+{\n+\tunsigned char tree_sha1[20];\n+\n+\tassert(t->initialized);\n+\n+\tif (write_notes_tree(t, tree_sha1))\n+\t\tdie(\"Failed to write notes tree to database\");\n+\n+\tif (!parents) {\n+\t\t/* Deduce parent commit from t->ref */\n+\t\tunsigned char parent_sha1[20];\n+\t\tif (!read_ref(t->ref, parent_sha1)) {\n+\t\t\tstruct commit *parent = lookup_commit(parent_sha1);\n+\t\t\tif (!parent || parse_commit(parent))\n+\t\t\t\tdie(\"Failed to find/parse commit %s\", t->ref);\n+\t\t\tcommit_list_insert(parent, &parents);\n+\t\t}\n+\t\t/* else: t->ref points to nothing, assume root/orphan commit */\n+\t}\n+\n+\tif (commit_tree(msg, tree_sha1, parents, result_sha1, NULL))\n+\t\tdie(\"Failed to commit notes tree to database\");\n+}\n+\n int notes_merge(struct notes_merge_options *o,\n \t\tunsigned char *result_sha1)\n {\ndiff --git a/notes-merge.h b/notes-merge.h\nindex c4416e1..b570123 100644\n--- a/notes-merge.h\n+++ b/notes-merge.h\n@@ -10,6 +10,20 @@ struct notes_merge_options {\n void init_notes_merge_options(struct notes_merge_options *o);\n \n /*\n+ * Create new notes commit from the given notes tree\n+ *\n+ * Properties of the created commit:\n+ * - tree: the result of converting t to a tree object with write_notes_tree().\n+ * - parents: the given parents OR (if NULL) the commit referenced by t->ref.\n+ * - author/committer: the default determined by commmit_tree().\n+ * - commit message: msg\n+ *\n+ * The resulting commit SHA1 is stored in result_sha1.\n+ */\n+void create_notes_commit(struct notes_tree *t, struct commit_list *parents,\n+\t\t\t const char *msg, unsigned char *result_sha1);\n+\n+/*\n  * Merge notes from o->remote_ref into o->local_ref\n  *\n  * The commits given by the two refs are merged, producing one of the following\n-- \n1.7.2.220.gea1d3\n"},{"id":"146054","messageId":"1279880104-29796-12-git-send-email-johan@herland.net","threadId":"24479","inReplyTo":"1279880104-29796-1-git-send-email-johan@herland.net","subject":"[WIP/RFC 11/13] git notes merge: Handle real, non-conflicting notes merges","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-07-23T10:15:02Z","receivedAt":"2010-07-23T10:15:02Z","isPatch":false,"sender":{"key":"johan@herland.net","avatar":"https://avatars.githubusercontent.com/u/547031?v=4"},"body":"This continuation of the 'git notes merge' implementation teaches notes-merge\nto properly do real merges between notes trees: Two diffs are performed, one\nfrom $base to $remote, and another from $base to $local. The paths in each\ndiff are normalized to SHA1 object names. The two diffs are then consolidated\ninto a single list of change pairs to be evaluated. Each change pair consist\nof:\n\n  - The annotated object's SHA1\n  - The $base SHA1 (i.e. the common ancestor notes for this object)\n  - The $local SHA1 (i.e. the current notes for this object)\n  - The $remote SHA1 (i.e. the to-be-merged notes for this object)\n\n>From the pair ($base -> $local, $base -> $remote), we can determine the merge\nresult using regular 3-way rules. If conflicts are encountered in this\nprocess, we fail loudly and exit (conflict handling to be added in a future\npatch), If we can complete the merge without conflicts, the resulting\nnotes tree is committed, and the current notes ref updated.\n\nThe patch includes added testcases verifying that we can successfully do real\nconflict-less merges.\n\nSigned-off-by: Johan Herland <johan@herland.net>\n---\n builtin/notes.c        |   15 ++-\n notes-merge.c          |  323 +++++++++++++++++++++++++++++++++++++++++++-\n notes-merge.h          |   15 ++-\n t/t3308-notes-merge.sh |  356 ++++++++++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 698 insertions(+), 11 deletions(-)\n\ndiff --git a/builtin/notes.c b/builtin/notes.c\nindex 8fcf207..1d22635 100644\n--- a/builtin/notes.c\n+++ b/builtin/notes.c\n@@ -767,6 +767,7 @@ static int merge(int argc, const char **argv, const char *prefix)\n {\n \tstruct strbuf remote_ref = STRBUF_INIT, msg = STRBUF_INIT;\n \tunsigned char result_sha1[20];\n+\tstruct notes_tree *t;\n \tstruct notes_merge_options o;\n \tint verbosity = 0, result;\n \tstruct option options[] = {\n@@ -792,19 +793,23 @@ static int merge(int argc, const char **argv, const char *prefix)\n \texpand_notes_ref(&remote_ref);\n \to.remote_ref = remote_ref.buf;\n \n-\tresult = notes_merge(&o, result_sha1);\n+\tt = init_notes_check(\"merge\");\n \n \tstrbuf_addf(&msg, \"notes: Merged notes from %s into %s\",\n \t\t    remote_ref.buf, default_notes_ref());\n-\tif (result == 0) { /* Merge resulted (trivially) in result_sha1 */\n+\to.commit_msg = msg.buf + 7; // skip \"notes: \" prefix\n+\n+\tresult = notes_merge(&o, t, result_sha1);\n+\n+\tif (result >= 0) /* Merge resulted (trivially) in result_sha1 */\n \t\t/* Update default notes ref with new commit */\n \t\tupdate_ref(msg.buf, default_notes_ref(), result_sha1, NULL,\n \t\t\t   0, DIE_ON_ERR);\n-\t} else {\n+\telse\n \t\t/* TODO: */\n-\t\tdie(\"'git notes merge' cannot yet handle non-trivial merges!\");\n-\t}\n+\t\tdie(\"'git notes merge' cannot yet handle conflicts!\");\n \n+\tfree_notes(t);\n \tstrbuf_release(&remote_ref);\n \tstrbuf_release(&msg);\n \treturn 0;\ndiff --git a/notes-merge.c b/notes-merge.c\nindex 9a4dc6d..ba9703a 100644\n--- a/notes-merge.c\n+++ b/notes-merge.c\n@@ -1,8 +1,14 @@\n #include \"cache.h\"\n #include \"commit.h\"\n+#include \"diff.h\"\n+#include \"diffcore.h\"\n #include \"notes.h\"\n #include \"notes-merge.h\"\n \n+struct notes_merge_pair {\n+\tunsigned char obj[20], base[20], local[20], remote[20];\n+};\n+\n void init_notes_merge_options(struct notes_merge_options *o)\n {\n \tmemset(o, 0, sizeof(struct notes_merge_options));\n@@ -17,6 +23,303 @@ static int show(struct notes_merge_options *o, int v)\n #define OUTPUT(o, v, ...) \\\n \tdo { if (show((o), (v))) { printf(__VA_ARGS__); puts(\"\"); } } while (0)\n \n+static int path_to_sha1(const char *path, unsigned char *sha1)\n+{\n+\tchar hex_sha1[40];\n+\tint i = 0;\n+\twhile (*path && i < 40) {\n+\t\tif (*path != '/')\n+\t\t\thex_sha1[i++] = *path;\n+\t\tpath++;\n+\t}\n+\tif (*path || i != 40)\n+\t\treturn -1;\n+\treturn get_sha1_hex(hex_sha1, sha1);\n+}\n+\n+static int verify_notes_filepair(struct diff_filepair *p, unsigned char *sha1)\n+{\n+\tswitch (p->status) {\n+\tcase DIFF_STATUS_MODIFIED:\n+\t\tassert(p->one->mode == p->two->mode);\n+\t\tassert(!is_null_sha1(p->one->sha1));\n+\t\tassert(!is_null_sha1(p->two->sha1));\n+\t\tbreak;\n+\tcase DIFF_STATUS_ADDED:\n+\t\tassert(is_null_sha1(p->one->sha1));\n+\t\tbreak;\n+\tcase DIFF_STATUS_DELETED:\n+\t\tassert(is_null_sha1(p->two->sha1));\n+\t\tbreak;\n+\tdefault:\n+\t\treturn -1;\n+\t}\n+\tassert(!strcmp(p->one->path, p->two->path));\n+\treturn path_to_sha1(p->one->path, sha1);\n+}\n+\n+static struct notes_merge_pair *find_notes_merge_pair_pos(\n+\t\tstruct notes_merge_pair *list, int len, unsigned char *obj,\n+\t\tint insert_new, int *occupied)\n+{\n+\t/*\n+\t * Both diff_tree_remote() and diff_tree_local() tend to process\n+\t * merge_pairs in ascending order. Therefore, cache last returned\n+\t * index, and search sequentially from there until the appropriate\n+\t * position is found.\n+\t *\n+\t * Since inserts only happen from diff_tree_remote() (which mainly\n+\t * _appends_), we don't care that inserting into the middle of the\n+\t * list is expensive (using memmove()).\n+\t */\n+\tstatic int last_index = 0;\n+\tint i = last_index < len ? last_index : len - 1;\n+\tint prev_cmp = 0, cmp = -1;\n+\twhile (i >= 0 && i < len) {\n+\t\tcmp = hashcmp(obj, list[i].obj);\n+\t\tif (!cmp) /* obj belongs @ i */\n+\t\t\tbreak;\n+\t\telse if (cmp < 0 && prev_cmp <= 0) /* obj belongs < i */\n+\t\t\ti--;\n+\t\telse if (cmp < 0) /* obj belongs between i-1 and i */\n+\t\t\tbreak;\n+\t\telse if (cmp > 0 && prev_cmp >= 0) /* obj belongs > i */\n+\t\t\ti++;\n+\t\telse /* if (cmp > 0) */ { /* obj belongs between i and i+1 */\n+\t\t\ti++;\n+\t\t\tbreak;\n+\t\t}\n+\t\tprev_cmp = cmp;\n+\t}\n+\tif (i < 0)\n+\t\ti = 0;\n+\t/* obj belongs at, or immediately preceding, index i (0 <= i <= len) */\n+\n+\tif (!cmp)\n+\t\t*occupied = 1;\n+\telse {\n+\t\t*occupied = 0;\n+\t\tif (insert_new && i < len) {\n+\t\t\tmemmove(list + i + 1, list + i,\n+\t\t\t\t(len - i) * sizeof(struct notes_merge_pair));\n+\t\t\tmemset(list + i, 0, sizeof(struct notes_merge_pair));\n+\t\t}\n+\t}\n+\tlast_index = i;\n+\treturn list + i;\n+}\n+\n+static unsigned char uninitialized[20] =\n+\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\" \\\n+\t\"\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\";\n+\n+static struct notes_merge_pair *diff_tree_remote(struct notes_merge_options *o,\n+\t\t\t\t\t\t const unsigned char *base,\n+\t\t\t\t\t\t const unsigned char *remote,\n+\t\t\t\t\t\t int *num_changes)\n+{\n+\tstruct diff_options opt;\n+\tstruct notes_merge_pair *changes;\n+\tint i, len = 0;\n+\n+\tOUTPUT(o, 5, \"\\tdiff_tree_remote(base = %.7s, remote = %.7s)\",\n+\t       sha1_to_hex(base), sha1_to_hex(remote));\n+\n+\tdiff_setup(&opt);\n+\tDIFF_OPT_SET(&opt, RECURSIVE);\n+\topt.output_format = DIFF_FORMAT_NO_OUTPUT;\n+\tif (diff_setup_done(&opt) < 0)\n+\t\tdie(\"diff_setup_done failed\");\n+\tdiff_tree_sha1(base, remote, \"\", &opt);\n+\tdiffcore_std(&opt);\n+\n+\tchanges = xcalloc(diff_queued_diff.nr, sizeof(struct notes_merge_pair));\n+\n+\tfor (i = 0; i < diff_queued_diff.nr; i++) {\n+\t\tstruct diff_filepair *p = diff_queued_diff.queue[i];\n+\t\tstruct notes_merge_pair *mp;\n+\t\tint occupied;\n+\t\tunsigned char obj[20];\n+\n+\t\tif (verify_notes_filepair(p, obj)) {\n+\t\t\tOUTPUT(o, 5, \"\\t\\tCannot merge entry '%s' (%c): \"\n+\t\t\t       \"%.7s -> %.7s. Skipping!\", p->one->path,\n+\t\t\t       p->status, sha1_to_hex(p->one->sha1),\n+\t\t\t       sha1_to_hex(p->two->sha1));\n+\t\t\tcontinue;\n+\t\t}\n+\t\tmp = find_notes_merge_pair_pos(changes, len, obj, 1, &occupied);\n+\t\tif (occupied) {\n+\t\t\t/* We've found an addition/deletion pair */\n+\t\t\tassert(!hashcmp(mp->obj, obj));\n+\t\t\tif (is_null_sha1(p->one->sha1)) { /* addition */\n+\t\t\t\tassert(is_null_sha1(mp->remote));\n+\t\t\t\thashcpy(mp->remote, p->two->sha1);\n+\t\t\t} else if (is_null_sha1(p->two->sha1)) { /* deletion */\n+\t\t\t\tassert(is_null_sha1(mp->base));\n+\t\t\t\thashcpy(mp->base, p->one->sha1);\n+\t\t\t} else\n+\t\t\t\tassert(!\"Invalid existing change recorded\");\n+\t\t} else {\n+\t\t\thashcpy(mp->obj, obj);\n+\t\t\thashcpy(mp->base, p->one->sha1);\n+\t\t\thashcpy(mp->local, uninitialized);\n+\t\t\thashcpy(mp->remote, p->two->sha1);\n+\t\t\tlen++;\n+\t\t}\n+\t\tOUTPUT(o, 5, \"\\t\\tRecorded remote change for %s: %.7s -> %.7s\",\n+\t\t       sha1_to_hex(mp->obj), sha1_to_hex(mp->base),\n+\t\t       sha1_to_hex(mp->remote));\n+\t}\n+\tdiff_flush(&opt);\n+\tdiff_tree_release_paths(&opt);\n+\n+\t*num_changes = len;\n+\treturn changes;\n+}\n+\n+static void diff_tree_local(struct notes_merge_options *o,\n+\t\t\t    struct notes_merge_pair *changes, int len,\n+\t\t\t    const unsigned char *base,\n+\t\t\t    const unsigned char *local)\n+{\n+\tstruct diff_options opt;\n+\tint i;\n+\n+\tOUTPUT(o, 5, \"\\tdiff_tree_local(len = %i, base = %.7s, local = %.7s)\",\n+\t       len, sha1_to_hex(base), sha1_to_hex(local));\n+\n+\tdiff_setup(&opt);\n+\tDIFF_OPT_SET(&opt, RECURSIVE);\n+\topt.output_format = DIFF_FORMAT_NO_OUTPUT;\n+\tif (diff_setup_done(&opt) < 0)\n+\t\tdie(\"diff_setup_done failed\");\n+\tdiff_tree_sha1(base, local, \"\", &opt);\n+\tdiffcore_std(&opt);\n+\n+\tfor (i = 0; i < diff_queued_diff.nr; i++) {\n+\t\tstruct diff_filepair *p = diff_queued_diff.queue[i];\n+\t\tstruct notes_merge_pair *mp;\n+\t\tint match;\n+\t\tunsigned char obj[20];\n+\n+\t\tif (verify_notes_filepair(p, obj)) {\n+\t\t\tOUTPUT(o, 5, \"\\t\\tCannot merge entry '%s' (%c): \"\n+\t\t\t       \"%.7s -> %.7s. Skipping!\", p->one->path,\n+\t\t\t       p->status, sha1_to_hex(p->one->sha1),\n+\t\t\t       sha1_to_hex(p->two->sha1));\n+\t\t\tcontinue;\n+\t\t}\n+\t\tmp = find_notes_merge_pair_pos(changes, len, obj, 0, &match);\n+\t\tif (!match) {\n+\t\t\tOUTPUT(o, 5, \"\\t\\tIgnoring local-only change for %s: \"\n+\t\t\t       \"%.7s -> %.7s\", sha1_to_hex(obj),\n+\t\t\t       sha1_to_hex(p->one->sha1),\n+\t\t\t       sha1_to_hex(p->two->sha1));\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\tassert(!hashcmp(mp->obj, obj));\n+\t\tif (is_null_sha1(p->two->sha1)) { /* deletion */\n+\t\t\t/*\n+\t\t\t * Either this is a true deletion (1), or it is part\n+\t\t\t * of an A/D pair (2), or D/A pair (3):\n+\t\t\t *\n+\t\t\t * (1) mp->local is uninitialized; set it to null_sha1\n+\t\t\t * (2) mp->local is not uninitialized; don't touch it\n+\t\t\t * (3) mp->local is uninitialized; set it to null_sha1\n+\t\t\t *     (will be overwritten by following addition)\n+\t\t\t */\n+\t\t\tif (!hashcmp(mp->local, uninitialized))\n+\t\t\t\thashclr(mp->local);\n+\t\t} else if (is_null_sha1(p->one->sha1)) { /* addition */\n+\t\t\t/*\n+\t\t\t * Either this is a true addition (1), or it is part\n+\t\t\t * of an A/D pair (2), or D/A pair (3):\n+\t\t\t *\n+\t\t\t * (1) mp->local is uninitialized; set to p->two->sha1\n+\t\t\t * (2) mp->local is uninitialized; set to p->two->sha1\n+\t\t\t * (3) mp->local is null_sha1;     set to p->two->sha1\n+\t\t\t */\n+\t\t\tassert(is_null_sha1(mp->local) ||\n+\t\t\t       !hashcmp(mp->local, uninitialized));\n+\t\t\thashcpy(mp->local, p->two->sha1);\n+\t\t} else { /* modification */\n+\t\t\t/*\n+\t\t\t * This is a true modification. p->one->sha1 shall\n+\t\t\t * match mp->base, and mp->local shall be uninitialized.\n+\t\t\t * Set mp->local to p->two->sha1.\n+\t\t\t */\n+\t\t\tassert(!hashcmp(p->one->sha1, mp->base));\n+\t\t\tassert(!hashcmp(mp->local, uninitialized));\n+\t\t\thashcpy(mp->local, p->two->sha1);\n+\t\t}\n+\t\tOUTPUT(o, 5, \"\\t\\tRecorded local change for %s: %.7s -> %.7s\",\n+\t\t       sha1_to_hex(mp->obj), sha1_to_hex(mp->base),\n+\t\t       sha1_to_hex(mp->local));\n+\t}\n+\tdiff_flush(&opt);\n+\tdiff_tree_release_paths(&opt);\n+}\n+\n+static int merge_changes(struct notes_merge_options *o,\n+\t\t\t struct notes_merge_pair *changes, int *num_changes,\n+\t\t\t struct notes_tree *t)\n+{\n+\tint i, conflicts = 0;\n+\n+\tOUTPUT(o, 5, \"\\tmerge_changes(num_changes = %i)\", *num_changes);\n+\tfor (i = 0; i < *num_changes; i++) {\n+\t\tstruct notes_merge_pair *p = changes + i;\n+\t\tOUTPUT(o, 5, \"\\t\\t%.7s: %.7s -> %.7s/%.7s\",\n+\t\t       sha1_to_hex(p->obj), sha1_to_hex(p->base),\n+\t\t       sha1_to_hex(p->local), sha1_to_hex(p->remote));\n+\n+\t\tif (!hashcmp(p->base, p->remote)) {\n+\t\t\t/* no remote change; nothing to do */\n+\t\t\tOUTPUT(o, 5, \"\\t\\t\\tskipping (no remote change)\");\n+\t\t} else if (!hashcmp(p->local, p->remote)) {\n+\t\t\t/* same change in local and remote; nothing to do */\n+\t\t\tOUTPUT(o, 5, \"\\t\\t\\tskipping (local == remote)\");\n+\t\t} else if (!hashcmp(p->local, uninitialized) ||\n+\t\t\t   !hashcmp(p->local, p->base)) {\n+\t\t\t/* no local change; adopt remote change */\n+\t\t\tOUTPUT(o, 5, \"\\t\\t\\tno local change, adopting remote\");\n+\t\t\tadd_note(t, p->obj, p->remote, combine_notes_overwrite);\n+\t\t} else {\n+\t\t\t/* need file-level merge between local and remote */\n+\t\t\tOUTPUT(o, 5, \"\\t\\t\\tneed content-level merge\");\n+\t\t\tconflicts += 1; /* TODO */\n+\t\t}\n+\t}\n+\n+\treturn conflicts;\n+}\n+\n+static int merge_from_diffs(struct notes_merge_options *o,\n+\t\t\t    const unsigned char *base,\n+\t\t\t    const unsigned char *local,\n+\t\t\t    const unsigned char *remote, struct notes_tree *t)\n+{\n+\tstruct notes_merge_pair *changes;\n+\tint num_changes, conflicts;\n+\n+\tOUTPUT(o, 5, \"\\tmerge_from_diffs(base = %.7s, local = %.7s, \"\n+\t       \"remote = %.7s)\", sha1_to_hex(base), sha1_to_hex(local),\n+\t       sha1_to_hex(remote));\n+\n+\tchanges = diff_tree_remote(o, base, remote, &num_changes);\n+\tdiff_tree_local(o, changes, num_changes, base, local);\n+\n+\tconflicts = merge_changes(o, changes, &num_changes, t);\n+\tfree(changes);\n+\n+\tOUTPUT(o, 4, \"Merge result: %i unmerged notes and a %s notes tree\",\n+\t       conflicts, t->dirty ? \"dirty\" : \"clean\");\n+\n+\treturn conflicts ? -1 : 1;\n+}\n+\n void create_notes_commit(struct notes_tree *t, struct commit_list *parents,\n \t\t\t const char *msg, unsigned char *result_sha1)\n {\n@@ -44,14 +347,16 @@ void create_notes_commit(struct notes_tree *t, struct commit_list *parents,\n }\n \n int notes_merge(struct notes_merge_options *o,\n+\t\tstruct notes_tree *local_tree,\n \t\tunsigned char *result_sha1)\n {\n \tunsigned char local_sha1[20], remote_sha1[20];\n \tstruct commit *local, *remote;\n \tstruct commit_list *bases = NULL;\n-\tconst unsigned char *base_sha1;\n+\tconst unsigned char *base_sha1, *base_tree_sha1;\n \tint result = 0;\n \n+\tassert(!strcmp(o->local_ref, local_tree->ref));\n \thashclr(result_sha1);\n \n \tOUTPUT(o, 5, \"notes_merge(o->local_ref = %s, o->remote_ref = %s)\",\n@@ -90,14 +395,17 @@ int notes_merge(struct notes_merge_options *o,\n \tbases = get_merge_bases(local, remote, 1);\n \tif (!bases) {\n \t\tbase_sha1 = null_sha1;\n+\t\tbase_tree_sha1 = (unsigned char *)EMPTY_TREE_SHA1_BIN;\n \t\tOUTPUT(o, 4, \"No merge base found; doing history-less merge\");\n \t} else if (!bases->next) {\n \t\tbase_sha1 = bases->item->object.sha1;\n+\t\tbase_tree_sha1 = bases->item->tree->object.sha1;\n \t\tOUTPUT(o, 4, \"One merge base found (%.7s)\",\n \t\t       sha1_to_hex(base_sha1));\n \t} else {\n \t\t/* TODO: How to handle multiple merge-bases? */\n \t\tbase_sha1 = bases->item->object.sha1;\n+\t\tbase_tree_sha1 = bases->item->tree->object.sha1;\n \t\tOUTPUT(o, 3, \"Multiple merge bases found. Using the first \"\n \t\t       \"(%.7s)\", sha1_to_hex(base_sha1));\n \t}\n@@ -119,8 +427,17 @@ int notes_merge(struct notes_merge_options *o,\n \t\tgoto found_result;\n \t}\n \n-\t/* TODO: */\n-\tresult = error(\"notes_merge() cannot yet handle real merges.\");\n+\tresult = merge_from_diffs(o, base_tree_sha1, local->tree->object.sha1,\n+\t\t\t\t  remote->tree->object.sha1, local_tree);\n+\n+\tif (result > 0) { /* successful non-trivial merge */\n+\t\t/* Commit result */\n+\t\tstruct commit_list *parents = NULL;\n+\t\tcommit_list_insert(remote, &parents); /* LIFO order */\n+\t\tcommit_list_insert(local, &parents);\n+\t\tcreate_notes_commit(local_tree, parents, o->commit_msg,\n+\t\t\t\t    result_sha1);\n+\t}\n \n found_result:\n \tfree_commit_list(bases);\ndiff --git a/notes-merge.h b/notes-merge.h\nindex b570123..e42ce4b 100644\n--- a/notes-merge.h\n+++ b/notes-merge.h\n@@ -4,6 +4,7 @@\n struct notes_merge_options {\n \tconst char *local_ref;\n \tconst char *remote_ref;\n+\tconst char *commit_msg;\n \tint verbosity;\n };\n \n@@ -26,19 +27,27 @@ void create_notes_commit(struct notes_tree *t, struct commit_list *parents,\n /*\n  * Merge notes from o->remote_ref into o->local_ref\n  *\n+ * The given notes_tree 'local_tree' must be the notes_tree referenced by the\n+ * o->local_ref. This is the notes_tree in which the object-level merge is\n+ * performed.\n+ *\n  * The commits given by the two refs are merged, producing one of the following\n  * outcomes:\n  *\n  * 1. The merge trivially results in an existing commit (e.g. fast-forward or\n- *    already-up-to-date). The SHA1 of the result is written into 'result_sha1'\n- *    and 0 is returned.\n- * 2. The merge fails. result_sha1 is set to null_sha1, and non-zero returned.\n+ *    already-up-to-date). 'local_tree' is untouched, the SHA1 of the result\n+ *    is written into 'result_sha1' and 0 is returned.\n+ * 2. The merge successfully completes, producing a merge commit. local_tree\n+ *    contains the updated notes tree, the SHA1 of the resulting commit is\n+ *    written into 'result_sha1', and 1 is returned.\n+ * 3. The merge fails. result_sha1 is set to null_sha1, and -1 is returned.\n  *\n  * Either ref (but not both) may not exist in which case the missing ref is\n  * interpreted as an empty notes tree, and the merge trivially results in\n  * what the other ref points to.\n  */\n int notes_merge(struct notes_merge_options *o,\n+\t\tstruct notes_tree *local_tree,\n \t\tunsigned char *result_sha1);\n \n #endif\ndiff --git a/t/t3308-notes-merge.sh b/t/t3308-notes-merge.sh\nindex a89fd11..d49df32 100755\n--- a/t/t3308-notes-merge.sh\n+++ b/t/t3308-notes-merge.sh\n@@ -310,4 +310,360 @@ test_expect_success 'merge changed (y) into original (x) => Fast-forward' '\n \ttest \"$(git rev-parse refs/notes/x)\" = \"$(git rev-parse refs/notes/y)\"\n '\n \n+test_expect_success 'merge empty notes ref (z => y)' '\n+\t# Prepare empty (but valid) notes ref (z)\n+\tgit config core.notesRef refs/notes/z &&\n+\tgit notes add -m \"foo\" &&\n+\tgit notes remove &&\n+\tgit notes >output_notes_z &&\n+\ttest_cmp /dev/null output_notes_z &&\n+\t# Do the merge (z => y)\n+\tgit config core.notesRef refs/notes/y &&\n+\tgit notes merge z &&\n+\tgit notes >output_notes_y &&\n+\ttest_cmp expect_notes_y output_notes_y &&\n+\tgit log >output_log_y &&\n+\ttest_cmp expect_log_y output_log_y &&\n+\t# y should no longer point to the same notes commit as x\n+\ttest \"$(git rev-parse refs/notes/x)\" != \"$(git rev-parse refs/notes/y)\"\n+'\n+\n+test_expect_success 'change notes on other notes ref (y)' '\n+\t# Append to 1st commit notes\n+\tgit notes append -m \"More notes on 1st commit\" HEAD~4 &&\n+\t# Add new notes to 2nd commit\n+\tgit notes add -m \"New notes on 2nd commit\" HEAD~3\n+'\n+\n+cat >expect_notes_y <<EOF\n+dec2502dac3ea161543f71930044deff93fa945c 15023535574ded8b1a89052b32673f84cf9582b8\n+4069cdb399fd45463ec6eef8e051a16a03592d91 1584215f1d29c65e99c6c6848626553fdd07fd75\n+d000d30e6ddcfce3a8122c403226a2ce2fd04d9d 268048bfb8a1fb38e703baceb8ab235421bf80c5\n+43add6bd0c8c0bc871ac7991e0f5573cfba27804 34b09d6ffa51a8a03203627f0e369f607227364f\n+0f2efbd00262f2fd41dfae33df8765618eeacd99 bd1753200303d0a0344be813e504253b3d98e74d\n+EOF\n+\n+cat >expect_log_y <<EOF\n+commit bd1753200303d0a0344be813e504253b3d98e74d\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:17:13 2005 -0700\n+\n+    5th\n+\n+Notes (y):\n+    Notes on 5th commit\n+\n+commit 15023535574ded8b1a89052b32673f84cf9582b8\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:16:13 2005 -0700\n+\n+    4th\n+\n+Notes (y):\n+    New notes on 4th commit\n+\n+commit 1584215f1d29c65e99c6c6848626553fdd07fd75\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:15:13 2005 -0700\n+\n+    3rd\n+\n+Notes (y):\n+    Notes on 3rd commit\n+$whitespace\n+    More notes on 3rd commit\n+\n+commit 268048bfb8a1fb38e703baceb8ab235421bf80c5\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:14:13 2005 -0700\n+\n+    2nd\n+\n+Notes (y):\n+    New notes on 2nd commit\n+\n+commit 34b09d6ffa51a8a03203627f0e369f607227364f\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:13:13 2005 -0700\n+\n+    1st\n+\n+Notes (y):\n+    Notes on 1st commit\n+$whitespace\n+    More notes on 1st commit\n+EOF\n+\n+test_expect_success 'verify changed notes on other notes ref (y)' '\n+\tgit notes >output_notes_y &&\n+\ttest_cmp expect_notes_y output_notes_y &&\n+\tgit log >output_log_y &&\n+\ttest_cmp expect_log_y output_log_y\n+'\n+\n+cat >expect_notes_x <<EOF\n+1f257a3a90328557c452f0817d6cc50c89d315d4 15023535574ded8b1a89052b32673f84cf9582b8\n+daa55ffad6cb99bf64226532147ffcaf5ce8bdd1 34b09d6ffa51a8a03203627f0e369f607227364f\n+0f2efbd00262f2fd41dfae33df8765618eeacd99 bd1753200303d0a0344be813e504253b3d98e74d\n+EOF\n+\n+cat >expect_log_x <<EOF\n+commit bd1753200303d0a0344be813e504253b3d98e74d\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:17:13 2005 -0700\n+\n+    5th\n+\n+Notes (x):\n+    Notes on 5th commit\n+\n+commit 15023535574ded8b1a89052b32673f84cf9582b8\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:16:13 2005 -0700\n+\n+    4th\n+\n+Notes (x):\n+    New notes on 4th commit\n+$whitespace\n+    More notes on 4th commit\n+\n+commit 1584215f1d29c65e99c6c6848626553fdd07fd75\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:15:13 2005 -0700\n+\n+    3rd\n+\n+commit 268048bfb8a1fb38e703baceb8ab235421bf80c5\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:14:13 2005 -0700\n+\n+    2nd\n+\n+commit 34b09d6ffa51a8a03203627f0e369f607227364f\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:13:13 2005 -0700\n+\n+    1st\n+\n+Notes (x):\n+    Notes on 1st commit\n+EOF\n+\n+test_expect_success 'change notes on notes ref (x)' '\n+\tgit config core.notesRef refs/notes/x &&\n+\t# Remove 3rd commit notes\n+\tgit notes remove HEAD~2 &&\n+\t# Append to 4th commit notes\n+\tgit notes append -m \"More notes on 4th commit\" HEAD^ &&\n+\t# Verify changed notes on notes ref (x)\n+\tgit notes >output_notes_x &&\n+\ttest_cmp expect_notes_x output_notes_x &&\n+\tgit log >output_log_x &&\n+\ttest_cmp expect_log_x output_log_x\n+'\n+\n+cat >expect_notes_x <<EOF\n+1f257a3a90328557c452f0817d6cc50c89d315d4 15023535574ded8b1a89052b32673f84cf9582b8\n+d000d30e6ddcfce3a8122c403226a2ce2fd04d9d 268048bfb8a1fb38e703baceb8ab235421bf80c5\n+43add6bd0c8c0bc871ac7991e0f5573cfba27804 34b09d6ffa51a8a03203627f0e369f607227364f\n+0f2efbd00262f2fd41dfae33df8765618eeacd99 bd1753200303d0a0344be813e504253b3d98e74d\n+EOF\n+\n+cat >expect_log_x <<EOF\n+commit bd1753200303d0a0344be813e504253b3d98e74d\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:17:13 2005 -0700\n+\n+    5th\n+\n+Notes (x):\n+    Notes on 5th commit\n+\n+commit 15023535574ded8b1a89052b32673f84cf9582b8\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:16:13 2005 -0700\n+\n+    4th\n+\n+Notes (x):\n+    New notes on 4th commit\n+$whitespace\n+    More notes on 4th commit\n+\n+commit 1584215f1d29c65e99c6c6848626553fdd07fd75\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:15:13 2005 -0700\n+\n+    3rd\n+\n+commit 268048bfb8a1fb38e703baceb8ab235421bf80c5\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:14:13 2005 -0700\n+\n+    2nd\n+\n+Notes (x):\n+    New notes on 2nd commit\n+\n+commit 34b09d6ffa51a8a03203627f0e369f607227364f\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:13:13 2005 -0700\n+\n+    1st\n+\n+Notes (x):\n+    Notes on 1st commit\n+$whitespace\n+    More notes on 1st commit\n+EOF\n+\n+test_expect_success 'merge y into x => Non-conflicting 3-way merge' '\n+\tgit notes merge y &&\n+\t# Verify new state of notes on other notes ref (x)\n+\tgit notes >output_notes_x &&\n+\ttest_cmp expect_notes_x output_notes_x &&\n+\tgit log >output_log_x &&\n+\ttest_cmp expect_log_x output_log_x &&\n+\t# Also verify that nothing changed on other notes ref (y)\n+\tGIT_NOTES_REF=refs/notes/y git notes >output_notes_y &&\n+\ttest_cmp expect_notes_y output_notes_y &&\n+\tGIT_NOTES_REF=refs/notes/y git log >output_log_y &&\n+\ttest_cmp expect_log_y output_log_y\n+'\n+\n+cat >expect_notes_w <<EOF\n+05a4927951bcef347f51486575b878b2b60137f2 1584215f1d29c65e99c6c6848626553fdd07fd75\n+d000d30e6ddcfce3a8122c403226a2ce2fd04d9d 268048bfb8a1fb38e703baceb8ab235421bf80c5\n+EOF\n+\n+cat >expect_log_w <<EOF\n+commit bd1753200303d0a0344be813e504253b3d98e74d\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:17:13 2005 -0700\n+\n+    5th\n+\n+commit 15023535574ded8b1a89052b32673f84cf9582b8\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:16:13 2005 -0700\n+\n+    4th\n+\n+commit 1584215f1d29c65e99c6c6848626553fdd07fd75\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:15:13 2005 -0700\n+\n+    3rd\n+\n+Notes (w):\n+    New notes on 3rd commit\n+\n+commit 268048bfb8a1fb38e703baceb8ab235421bf80c5\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:14:13 2005 -0700\n+\n+    2nd\n+\n+Notes (w):\n+    New notes on 2nd commit\n+\n+commit 34b09d6ffa51a8a03203627f0e369f607227364f\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:13:13 2005 -0700\n+\n+    1st\n+EOF\n+\n+test_expect_success 'create notes on new, separate notes ref (w)' '\n+\tgit config core.notesRef refs/notes/w &&\n+\t# Add same note as refs/notes/y on 2nd commit\n+\tgit notes add -m \"New notes on 2nd commit\" HEAD~3 &&\n+\t# Add new note on 3rd commit (non-conflicting)\n+\tgit notes add -m \"New notes on 3rd commit\" HEAD~2 &&\n+\t# Verify state of notes on new, separate notes ref (w)\n+\tgit notes >output_notes_w &&\n+\ttest_cmp expect_notes_w output_notes_w &&\n+\tgit log >output_log_w &&\n+\ttest_cmp expect_log_w output_log_w\n+'\n+\n+cat >expect_notes_x <<EOF\n+1f257a3a90328557c452f0817d6cc50c89d315d4 15023535574ded8b1a89052b32673f84cf9582b8\n+05a4927951bcef347f51486575b878b2b60137f2 1584215f1d29c65e99c6c6848626553fdd07fd75\n+d000d30e6ddcfce3a8122c403226a2ce2fd04d9d 268048bfb8a1fb38e703baceb8ab235421bf80c5\n+43add6bd0c8c0bc871ac7991e0f5573cfba27804 34b09d6ffa51a8a03203627f0e369f607227364f\n+0f2efbd00262f2fd41dfae33df8765618eeacd99 bd1753200303d0a0344be813e504253b3d98e74d\n+EOF\n+\n+cat >expect_log_x <<EOF\n+commit bd1753200303d0a0344be813e504253b3d98e74d\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:17:13 2005 -0700\n+\n+    5th\n+\n+Notes (x):\n+    Notes on 5th commit\n+\n+commit 15023535574ded8b1a89052b32673f84cf9582b8\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:16:13 2005 -0700\n+\n+    4th\n+\n+Notes (x):\n+    New notes on 4th commit\n+$whitespace\n+    More notes on 4th commit\n+\n+commit 1584215f1d29c65e99c6c6848626553fdd07fd75\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:15:13 2005 -0700\n+\n+    3rd\n+\n+Notes (x):\n+    New notes on 3rd commit\n+\n+commit 268048bfb8a1fb38e703baceb8ab235421bf80c5\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:14:13 2005 -0700\n+\n+    2nd\n+\n+Notes (x):\n+    New notes on 2nd commit\n+\n+commit 34b09d6ffa51a8a03203627f0e369f607227364f\n+Author: A U Thor <author@example.com>\n+Date:   Thu Apr 7 15:13:13 2005 -0700\n+\n+    1st\n+\n+Notes (x):\n+    Notes on 1st commit\n+$whitespace\n+    More notes on 1st commit\n+EOF\n+\n+test_expect_success 'merge w into x => Non-conflicting history-less merge' '\n+\tgit config core.notesRef refs/notes/x &&\n+\tgit notes merge w &&\n+\t# Verify new state of notes on other notes ref (x)\n+\tgit notes >output_notes_x &&\n+\ttest_cmp expect_notes_x output_notes_x &&\n+\tgit log >output_log_x &&\n+\ttest_cmp expect_log_x output_log_x &&\n+\t# Also verify that nothing changed on other notes refs (y and w)\n+\tGIT_NOTES_REF=refs/notes/y git notes >output_notes_y &&\n+\ttest_cmp expect_notes_y output_notes_y &&\n+\tGIT_NOTES_REF=refs/notes/y git log >output_log_y &&\n+\ttest_cmp expect_log_y output_log_y\n+\tGIT_NOTES_REF=refs/notes/w git notes >output_notes_w &&\n+\ttest_cmp expect_notes_w output_notes_w &&\n+\tGIT_NOTES_REF=refs/notes/w git log >output_log_w &&\n+\ttest_cmp expect_log_w output_log_w\n+'\n+\n test_done\n-- \n1.7.2.220.gea1d3\n"},{"id":"146059","messageId":"1279880104-29796-13-git-send-email-johan@herland.net","threadId":"24479","inReplyTo":"1279880104-29796-1-git-send-email-johan@herland.net","subject":"[WIP/RFC 12/13] git notes merge: Add automatic conflict resolvers (ours, theirs, union)","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-07-23T10:15:03Z","receivedAt":"2010-07-23T10:15:03Z","isPatch":false,"sender":{"key":"johan@herland.net","avatar":"https://avatars.githubusercontent.com/u/547031?v=4"},"body":"The new -X/--resolve command-line option to 'git notes merge' allow the user\nto choose how notes merge conflicts should be resolved. There are four valid\nresolvers to choose from:\n\n1. \"manual\" (the default): This will let the user manually resolve conflicts.\n   This option currently fails with an error message. It will be implemented\n   properly in future patches.\n\n2. \"ours\": This automatically chooses the local version of a conflict, and\n   discards the remote version.\n\n3. \"theirs\": This automatically chooses the remote version of a conflict, and\n   discards the local version.\n\n4. \"union\": This automatically resolves the conflict by appending the remote\n   version to the local version.\n\nThe auto-resolvers are implement using the combine_notes_* functions from the\nnotes.h API.\n\nThe patch also includes testcases verifying the correct implementation of\nthese auto-resolvers.\n\nSigned-off-by: Johan Herland <johan@herland.net>\n---\n builtin/notes.c                     |   21 ++-\n notes-merge.c                       |   29 ++-\n notes-merge.h                       |    6 +\n t/t3309-notes-merge-auto-resolve.sh |  512 +++++++++++++++++++++++++++++++++++\n 4 files changed, 566 insertions(+), 2 deletions(-)\n create mode 100755 t/t3309-notes-merge-auto-resolve.sh\n\ndiff --git a/builtin/notes.c b/builtin/notes.c\nindex 1d22635..e14c6f2 100644\n--- a/builtin/notes.c\n+++ b/builtin/notes.c\n@@ -26,7 +26,7 @@ static const char * const git_notes_usage[] = {\n \t\"git notes [--ref <notes_ref>] append [-m <msg> | -F <file> | (-c | -C) <object>] [<object>]\",\n \t\"git notes [--ref <notes_ref>] edit [<object>]\",\n \t\"git notes [--ref <notes_ref>] show [<object>]\",\n-\t\"git notes [--ref <notes_ref>] merge [-v | -q] <notes_ref>\",\n+\t\"git notes [--ref <notes_ref>] merge [-v | -q] [-X <strategy> ] <notes_ref>\",\n \t\"git notes [--ref <notes_ref>] remove [<object>]\",\n \t\"git notes [--ref <notes_ref>] prune [-n | -v]\",\n \tNULL\n@@ -770,8 +770,12 @@ static int merge(int argc, const char **argv, const char *prefix)\n \tstruct notes_tree *t;\n \tstruct notes_merge_options o;\n \tint verbosity = 0, result;\n+\tconst char *resolver = NULL;\n \tstruct option options[] = {\n \t\tOPT__VERBOSITY(&verbosity),\n+\t\tOPT_STRING('X', \"resolve\", &resolver, \"strategy\",\n+\t\t\t   \"resolve notes conflicts using the given \"\n+\t\t\t   \"strategy (manual/ours/theirs/union)\"),\n \t\tOPT_END()\n \t};\n \n@@ -793,6 +797,21 @@ static int merge(int argc, const char **argv, const char *prefix)\n \texpand_notes_ref(&remote_ref);\n \to.remote_ref = remote_ref.buf;\n \n+\tif (resolver) {\n+\t\tif (!strcmp(resolver, \"manual\"))\n+\t\t\to.resolve = NOTES_MERGE_RESOLVE_MANUAL;\n+\t\telse if (!strcmp(resolver, \"ours\"))\n+\t\t\to.resolve = NOTES_MERGE_RESOLVE_OURS;\n+\t\telse if (!strcmp(resolver, \"theirs\"))\n+\t\t\to.resolve = NOTES_MERGE_RESOLVE_THEIRS;\n+\t\telse if (!strcmp(resolver, \"union\"))\n+\t\t\to.resolve = NOTES_MERGE_RESOLVE_UNION;\n+\t\telse {\n+\t\t\terror(\"Unknown -X/--resolve strategy: %s\", resolver);\n+\t\t\tusage_with_options(git_notes_merge_usage, options);\n+\t\t}\n+\t}\n+\n \tt = init_notes_check(\"merge\");\n \n \tstrbuf_addf(&msg, \"notes: Merged notes from %s into %s\",\ndiff --git a/notes-merge.c b/notes-merge.c\nindex ba9703a..c7db60a 100644\n--- a/notes-merge.c\n+++ b/notes-merge.c\n@@ -262,6 +262,33 @@ static void diff_tree_local(struct notes_merge_options *o,\n \tdiff_tree_release_paths(&opt);\n }\n \n+static int merge_one_change(struct notes_merge_options *o,\n+\t\t\t    struct notes_merge_pair *p, struct notes_tree *t)\n+{\n+\t/*\n+\t * Return 0 if change was resolved (and added to notes_tree),\n+\t * 1 if conflict\n+\t */\n+\tswitch (o->resolve) {\n+\tcase NOTES_MERGE_RESOLVE_MANUAL:\n+\t\treturn 1;\n+\tcase NOTES_MERGE_RESOLVE_OURS:\n+\t\tOUTPUT(o, 2, \"Using local notes for %s\", sha1_to_hex(p->obj));\n+\t\t/* nothing to do */\n+\t\treturn 0;\n+\tcase NOTES_MERGE_RESOLVE_THEIRS:\n+\t\tOUTPUT(o, 2, \"Using remote notes for %s\", sha1_to_hex(p->obj));\n+\t\tadd_note(t, p->obj, p->remote, combine_notes_overwrite);\n+\t\treturn 0;\n+\tcase NOTES_MERGE_RESOLVE_UNION:\n+\t\tOUTPUT(o, 2, \"Concatenating local and remote notes for %s\",\n+\t\t       sha1_to_hex(p->obj));\n+\t\tadd_note(t, p->obj, p->remote, combine_notes_concatenate);\n+\t\treturn 0;\n+\t}\n+\tdie(\"Unknown resolve method (%i).\", o->resolve);\n+}\n+\n static int merge_changes(struct notes_merge_options *o,\n \t\t\t struct notes_merge_pair *changes, int *num_changes,\n \t\t\t struct notes_tree *t)\n@@ -289,7 +316,7 @@ static int merge_changes(struct notes_merge_options *o,\n \t\t} else {\n \t\t\t/* need file-level merge between local and remote */\n \t\t\tOUTPUT(o, 5, \"\\t\\t\\tneed content-level merge\");\n-\t\t\tconflicts += 1; /* TODO */\n+\t\t\tconflicts += merge_one_change(o, p, t);\n \t\t}\n \t}\n \ndiff --git a/notes-merge.h b/notes-merge.h\nindex e42ce4b..bd808d0 100644\n--- a/notes-merge.h\n+++ b/notes-merge.h\n@@ -6,6 +6,12 @@ struct notes_merge_options {\n \tconst char *remote_ref;\n \tconst char *commit_msg;\n \tint verbosity;\n+\tenum {\n+\t\tNOTES_MERGE_RESOLVE_MANUAL = 0,\n+\t\tNOTES_MERGE_RESOLVE_OURS,\n+\t\tNOTES_MERGE_RESOLVE_THEIRS,\n+\t\tNOTES_MERGE_RESOLVE_UNION\n+\t} resolve;\n };\n \n void init_notes_merge_options(struct notes_merge_options *o);\ndiff --git a/t/t3309-notes-merge-auto-resolve.sh b/t/t3309-notes-merge-auto-resolve.sh\nnew file mode 100755\nindex 0000000..171e795\n--- /dev/null\n+++ b/t/t3309-notes-merge-auto-resolve.sh\n@@ -0,0 +1,512 @@\n+#!/bin/sh\n+#\n+# Copyright (c) 2010 Johan Herland\n+#\n+\n+test_description='Test notes merging with automatic resolvers'\n+\n+. ./test-lib.sh\n+\n+# Set up a notes merge scenario with all kinds of potential conflicts\n+test_expect_success 'setup commits' '\n+\t# Create 15 commits with tags (\"1st\" through \"15th\")\n+\t: > a1 && git add a1 && test_tick && git commit -m 1st && git tag 1st &&\n+\t: > a2 && git add a2 && test_tick && git commit -m 2nd && git tag 2nd &&\n+\t: > a3 && git add a3 && test_tick && git commit -m 3rd && git tag 3rd &&\n+\t: > a4 && git add a4 && test_tick && git commit -m 4th && git tag 4th &&\n+\t: > a5 && git add a5 && test_tick && git commit -m 5th && git tag 5th &&\n+\t: > a6 && git add a6 && test_tick && git commit -m 6th && git tag 6th &&\n+\t: > a7 && git add a7 && test_tick && git commit -m 7th && git tag 7th &&\n+\t: > a8 && git add a8 && test_tick && git commit -m 8th && git tag 8th &&\n+\t: > a9 && git add a9 && test_tick && git commit -m 9th && git tag 9th &&\n+\t: > a10 && git add a10 && test_tick && git commit -m 10th && git tag 10th &&\n+\t: > a11 && git add a11 && test_tick && git commit -m 11th && git tag 11th &&\n+\t: > a12 && git add a12 && test_tick && git commit -m 12th && git tag 12th &&\n+\t: > a13 && git add a13 && test_tick && git commit -m 13th && git tag 13th &&\n+\t: > a14 && git add a14 && test_tick && git commit -m 14th && git tag 14th &&\n+\t: > a15 && git add a15 && test_tick && git commit -m 15th && git tag 15th\n+'\n+\n+test_expect_success 'setup merge base (x)' '\n+\tgit config core.notesRef refs/notes/x &&\n+\tgit notes add -m \"x notes on 6th commit\" 6th &&\n+\tgit notes add -m \"x notes on 7th commit\" 7th &&\n+\tgit notes add -m \"x notes on 8th commit\" 8th &&\n+\tgit notes add -m \"x notes on 9th commit\" 9th &&\n+\tgit notes add -m \"x notes on 10th commit\" 10th &&\n+\tgit notes add -m \"x notes on 11th commit\" 11th &&\n+\tgit notes add -m \"x notes on 12th commit\" 12th &&\n+\tgit notes add -m \"x notes on 13th commit\" 13th &&\n+\tgit notes add -m \"x notes on 14th commit\" 14th &&\n+\tgit notes add -m \"x notes on 15th commit\" 15th\n+'\n+\n+cat >expect_notes_x <<EOF\n+20c613c835011c48a5abe29170a2402ca6354910 016e982bad97eacdbda0fcbd7ce5b0ba87c81f1b\n+897003322b53bc6ca098e9324ee508362347e734 2ede89468182a62d0bde2583c736089bcf7d7e92\n+11d97fdebfa5ceee540a3da07bce6fa0222bc082 387a89921c73d7ed72cd94d179c1c7048ca47756\n+457a85d6c814ea208550f15fcc48f804ac8dc023 516bb9cbbfaa1be722bcf97df393a12e1e7c944d\n+7abbc45126d680336fb24294f013a7cdfa3ed545 6352c5e33dbcab725fe0579be16aa2ba8eb369be\n+dd161bc149470fd890dd4ab52a4cbd79bbd18c36 7038787dfe22a14c3867ce816dbba39845359719\n+a3daf8a1e4e5dc3409a303ad8481d57bfea7f5d6 80d796defacd5db327b7a4e50099663902fbdc5c\n+5d30216a129eeffa97d9694ffe8c74317a560315 e5d4fb5698d564ab8c73551538ecaf2b0c666185\n+b0c95b954301d69da2bc3723f4cb1680d355937c f42b4e95384566bc7b31d9d3bfcec2d1a8f3e2e8\n+b8d03e173f67f6505a76f6e00cf93440200dd9be ffed603236bfa3891c49644257a83598afe8ae5a\n+EOF\n+\n+cat >expect_log_x <<EOF\n+516bb9c 15th\n+x notes on 15th commit\n+\n+f42b4e9 14th\n+x notes on 14th commit\n+\n+e5d4fb5 13th\n+x notes on 13th commit\n+\n+7038787 12th\n+x notes on 12th commit\n+\n+6352c5e 11th\n+x notes on 11th commit\n+\n+ffed603 10th\n+x notes on 10th commit\n+\n+016e982 9th\n+x notes on 9th commit\n+\n+80d796d 8th\n+x notes on 8th commit\n+\n+2ede894 7th\n+x notes on 7th commit\n+\n+387a899 6th\n+x notes on 6th commit\n+\n+bd17532 5th\n+\n+1502353 4th\n+\n+1584215 3rd\n+\n+268048b 2nd\n+\n+34b09d6 1st\n+\n+EOF\n+\n+test_expect_success 'verify state of merge base (x)' '\n+\tgit notes >output_notes_x &&\n+\ttest_cmp expect_notes_x output_notes_x &&\n+\tgit log --format=\"%h %s%n%N\" >output_log_x &&\n+\ttest_cmp expect_log_x output_log_x\n+'\n+\n+test_expect_success 'setup local branch (y)' '\n+\tgit update-ref refs/notes/y refs/notes/x &&\n+\tgit config core.notesRef refs/notes/y &&\n+\tgit notes add -f -m \"y notes on 3rd commit\" 3rd &&\n+\tgit notes add -f -m \"y notes on 4th commit\" 4th &&\n+\tgit notes add -f -m \"y notes on 5th commit\" 5th &&\n+\tgit notes remove 6th &&\n+\tgit notes remove 7th &&\n+\tgit notes remove 8th &&\n+\tgit notes add -f -m \"y notes on 12th commit\" 12th &&\n+\tgit notes add -f -m \"y notes on 13th commit\" 13th &&\n+\tgit notes add -f -m \"y notes on 14th commit\" 14th &&\n+\tgit notes add -f -m \"y notes on 15th commit\" 15th\n+'\n+\n+cat >expect_notes_y <<EOF\n+20c613c835011c48a5abe29170a2402ca6354910 016e982bad97eacdbda0fcbd7ce5b0ba87c81f1b\n+e2bfd06a37dd2031684a59a6e2b033e212239c78 15023535574ded8b1a89052b32673f84cf9582b8\n+5772f42408c0dd6f097a7ca2d24de0e78d1c46b1 1584215f1d29c65e99c6c6848626553fdd07fd75\n+68b8630d25516028bed862719855b3d6768d7833 516bb9cbbfaa1be722bcf97df393a12e1e7c944d\n+7abbc45126d680336fb24294f013a7cdfa3ed545 6352c5e33dbcab725fe0579be16aa2ba8eb369be\n+a66055fa82f7a03fe0c02a6aba3287a85abf7c62 7038787dfe22a14c3867ce816dbba39845359719\n+154508c7a0bcad82b6fe4b472bc4c26b3bf0825b bd1753200303d0a0344be813e504253b3d98e74d\n+3a631fdb6f41b05b55d8f4baf20728ba8f6fccbc e5d4fb5698d564ab8c73551538ecaf2b0c666185\n+5de7ea7ad4f47e7ff91989fb82234634730f75df f42b4e95384566bc7b31d9d3bfcec2d1a8f3e2e8\n+b8d03e173f67f6505a76f6e00cf93440200dd9be ffed603236bfa3891c49644257a83598afe8ae5a\n+EOF\n+\n+cat >expect_log_y <<EOF\n+516bb9c 15th\n+y notes on 15th commit\n+\n+f42b4e9 14th\n+y notes on 14th commit\n+\n+e5d4fb5 13th\n+y notes on 13th commit\n+\n+7038787 12th\n+y notes on 12th commit\n+\n+6352c5e 11th\n+x notes on 11th commit\n+\n+ffed603 10th\n+x notes on 10th commit\n+\n+016e982 9th\n+x notes on 9th commit\n+\n+80d796d 8th\n+\n+2ede894 7th\n+\n+387a899 6th\n+\n+bd17532 5th\n+y notes on 5th commit\n+\n+1502353 4th\n+y notes on 4th commit\n+\n+1584215 3rd\n+y notes on 3rd commit\n+\n+268048b 2nd\n+\n+34b09d6 1st\n+\n+EOF\n+\n+test_expect_success 'verify state of local branch (y)' '\n+\tgit notes >output_notes_y &&\n+\ttest_cmp expect_notes_y output_notes_y &&\n+\tgit log --format=\"%h %s%n%N\" >output_log_y &&\n+\ttest_cmp expect_log_y output_log_y\n+'\n+\n+test_expect_success 'setup remote branch (z)' '\n+\tgit update-ref refs/notes/z refs/notes/x &&\n+\tgit config core.notesRef refs/notes/z &&\n+\tgit notes add -f -m \"z notes on 2nd commit\" 2nd &&\n+\tgit notes add -f -m \"y notes on 4th commit\" 4th &&\n+\tgit notes add -f -m \"z notes on 5th commit\" 5th &&\n+\tgit notes remove 6th &&\n+\tgit notes add -f -m \"z notes on 8th commit\" 8th &&\n+\tgit notes remove 9th &&\n+\tgit notes add -f -m \"z notes on 11th commit\" 11th &&\n+\tgit notes remove 12th &&\n+\tgit notes add -f -m \"y notes on 14th commit\" 14th &&\n+\tgit notes add -f -m \"z notes on 15th commit\" 15th\n+'\n+\n+cat >expect_notes_z <<EOF\n+e2bfd06a37dd2031684a59a6e2b033e212239c78 15023535574ded8b1a89052b32673f84cf9582b8\n+283b48219aee9a4105f6cab337e789065c82c2b9 268048bfb8a1fb38e703baceb8ab235421bf80c5\n+897003322b53bc6ca098e9324ee508362347e734 2ede89468182a62d0bde2583c736089bcf7d7e92\n+9b4b2c61f0615412da3c10f98ff85b57c04ec765 516bb9cbbfaa1be722bcf97df393a12e1e7c944d\n+7e3c53503a3db8dd996cb62e37c66e070b44b54d 6352c5e33dbcab725fe0579be16aa2ba8eb369be\n+851e1638784a884c7dd26c5d41f3340f6387413a 80d796defacd5db327b7a4e50099663902fbdc5c\n+99fc34adfc400b95c67b013115e37e31aa9a6d23 bd1753200303d0a0344be813e504253b3d98e74d\n+5d30216a129eeffa97d9694ffe8c74317a560315 e5d4fb5698d564ab8c73551538ecaf2b0c666185\n+5de7ea7ad4f47e7ff91989fb82234634730f75df f42b4e95384566bc7b31d9d3bfcec2d1a8f3e2e8\n+b8d03e173f67f6505a76f6e00cf93440200dd9be ffed603236bfa3891c49644257a83598afe8ae5a\n+EOF\n+\n+cat >expect_log_z <<EOF\n+516bb9c 15th\n+z notes on 15th commit\n+\n+f42b4e9 14th\n+y notes on 14th commit\n+\n+e5d4fb5 13th\n+x notes on 13th commit\n+\n+7038787 12th\n+\n+6352c5e 11th\n+z notes on 11th commit\n+\n+ffed603 10th\n+x notes on 10th commit\n+\n+016e982 9th\n+\n+80d796d 8th\n+z notes on 8th commit\n+\n+2ede894 7th\n+x notes on 7th commit\n+\n+387a899 6th\n+\n+bd17532 5th\n+z notes on 5th commit\n+\n+1502353 4th\n+y notes on 4th commit\n+\n+1584215 3rd\n+\n+268048b 2nd\n+z notes on 2nd commit\n+\n+34b09d6 1st\n+\n+EOF\n+\n+test_expect_success 'verify state of remote branch (z)' '\n+\tgit notes >output_notes_z &&\n+\ttest_cmp expect_notes_z output_notes_z &&\n+\tgit log --format=\"%h %s%n%N\" >output_log_z &&\n+\ttest_cmp expect_log_z output_log_z\n+'\n+\n+# At this point, before merging z into y, we have the following status:\n+#\n+# commit | object  | base/x  | local/y | remote/z |? diff from x to y/z\n+# -------|---------|---------|---------|----------||---------------------------\n+# 1st    | 34b09d6 | [none]  | [none]  | [none]   || unchanged / unchanged\n+# 2nd    | 268048b | [none]  | [none]  | 283b482  || unchanged / added\n+# 3rd    | 1584215 | [none]  | 5772f42 | [none]   || added     / unchanged\n+# 4th    | 1502353 | [none]  | e2bfd06 | e2bfd06  || added     / added (same)\n+# 5th    | bd17532 | [none]  | 154508c | 99fc34a  |+ added     / added (diff)\n+# 6th    | 387a899 | 11d97fd | [none]  | [none]   || removed   / removed\n+# 7th    | 2ede894 | 8970033 | [none]  | 8970033  || removed   / unchanged\n+# 8th    | 80d796d | a3daf8a | [none]  | 851e163  |+ removed   / changed\n+# 9th    | 016e982 | 20c613c | 20c613c | [none]   || unchanged / removed\n+# 10th   | ffed603 | b8d03e1 | b8d03e1 | b8d03e1  || unchanged / unchanged\n+# 11th   | 6352c5e | 7abbc45 | 7abbc45 | 7e3c535  || unchanged / changed\n+# 12th   | 7038787 | dd161bc | a66055f | [none]   |+ changed   / removed\n+# 13th   | e5d4fb5 | 5d30216 | 3a631fd | 5d30216  || changed   / unchanged\n+# 14th   | f42b4e9 | b0c95b9 | 5de7ea7 | 5de7ea7  || changed   / changed (same)\n+# 15th   | 516bb9c | 457a85d | 68b8630 | 9b4b2c6  |+ changed   / changed (diff)\n+\n+test_expect_success 'merge z into y with invalid resolver => Fail/No changes' '\n+\tgit config core.notesRef refs/notes/y &&\n+\ttest_must_fail git notes merge --resolve=foo z &&\n+\t# Verify no changes (y)\n+\tgit notes >output_notes_y &&\n+\ttest_cmp expect_notes_y output_notes_y &&\n+\tgit log --format=\"%h %s%n%N\" >output_log_y &&\n+\ttest_cmp expect_log_y output_log_y\n+'\n+\n+cat >expect_notes_ours <<EOF\n+e2bfd06a37dd2031684a59a6e2b033e212239c78 15023535574ded8b1a89052b32673f84cf9582b8\n+5772f42408c0dd6f097a7ca2d24de0e78d1c46b1 1584215f1d29c65e99c6c6848626553fdd07fd75\n+283b48219aee9a4105f6cab337e789065c82c2b9 268048bfb8a1fb38e703baceb8ab235421bf80c5\n+68b8630d25516028bed862719855b3d6768d7833 516bb9cbbfaa1be722bcf97df393a12e1e7c944d\n+7e3c53503a3db8dd996cb62e37c66e070b44b54d 6352c5e33dbcab725fe0579be16aa2ba8eb369be\n+a66055fa82f7a03fe0c02a6aba3287a85abf7c62 7038787dfe22a14c3867ce816dbba39845359719\n+154508c7a0bcad82b6fe4b472bc4c26b3bf0825b bd1753200303d0a0344be813e504253b3d98e74d\n+3a631fdb6f41b05b55d8f4baf20728ba8f6fccbc e5d4fb5698d564ab8c73551538ecaf2b0c666185\n+5de7ea7ad4f47e7ff91989fb82234634730f75df f42b4e95384566bc7b31d9d3bfcec2d1a8f3e2e8\n+b8d03e173f67f6505a76f6e00cf93440200dd9be ffed603236bfa3891c49644257a83598afe8ae5a\n+EOF\n+\n+cat >expect_log_ours <<EOF\n+516bb9c 15th\n+y notes on 15th commit\n+\n+f42b4e9 14th\n+y notes on 14th commit\n+\n+e5d4fb5 13th\n+y notes on 13th commit\n+\n+7038787 12th\n+y notes on 12th commit\n+\n+6352c5e 11th\n+z notes on 11th commit\n+\n+ffed603 10th\n+x notes on 10th commit\n+\n+016e982 9th\n+\n+80d796d 8th\n+\n+2ede894 7th\n+\n+387a899 6th\n+\n+bd17532 5th\n+y notes on 5th commit\n+\n+1502353 4th\n+y notes on 4th commit\n+\n+1584215 3rd\n+y notes on 3rd commit\n+\n+268048b 2nd\n+z notes on 2nd commit\n+\n+34b09d6 1st\n+\n+EOF\n+\n+test_expect_success 'merge z into y with \"ours\" resolver => Non-conflicting 3-way merge' '\n+\tgit notes merge --resolve=ours z &&\n+\t# Verify merge result\n+\tgit notes >output_notes_ours &&\n+\ttest_cmp expect_notes_ours output_notes_ours &&\n+\tgit log --format=\"%h %s%n%N\" >output_log_ours &&\n+\ttest_cmp expect_log_ours output_log_ours\n+'\n+\n+test_expect_success 'reset to pre-merge state (y)' '\n+\tgit update-ref refs/notes/y refs/notes/y^1 &&\n+\t# Verify pre-merge state\n+\tgit notes >output_notes_y &&\n+\ttest_cmp expect_notes_y output_notes_y &&\n+\tgit log --format=\"%h %s%n%N\" >output_log_y &&\n+\ttest_cmp expect_log_y output_log_y\n+'\n+\n+cat >expect_notes_theirs <<EOF\n+e2bfd06a37dd2031684a59a6e2b033e212239c78 15023535574ded8b1a89052b32673f84cf9582b8\n+5772f42408c0dd6f097a7ca2d24de0e78d1c46b1 1584215f1d29c65e99c6c6848626553fdd07fd75\n+283b48219aee9a4105f6cab337e789065c82c2b9 268048bfb8a1fb38e703baceb8ab235421bf80c5\n+9b4b2c61f0615412da3c10f98ff85b57c04ec765 516bb9cbbfaa1be722bcf97df393a12e1e7c944d\n+7e3c53503a3db8dd996cb62e37c66e070b44b54d 6352c5e33dbcab725fe0579be16aa2ba8eb369be\n+851e1638784a884c7dd26c5d41f3340f6387413a 80d796defacd5db327b7a4e50099663902fbdc5c\n+99fc34adfc400b95c67b013115e37e31aa9a6d23 bd1753200303d0a0344be813e504253b3d98e74d\n+3a631fdb6f41b05b55d8f4baf20728ba8f6fccbc e5d4fb5698d564ab8c73551538ecaf2b0c666185\n+5de7ea7ad4f47e7ff91989fb82234634730f75df f42b4e95384566bc7b31d9d3bfcec2d1a8f3e2e8\n+b8d03e173f67f6505a76f6e00cf93440200dd9be ffed603236bfa3891c49644257a83598afe8ae5a\n+EOF\n+\n+cat >expect_log_theirs <<EOF\n+516bb9c 15th\n+z notes on 15th commit\n+\n+f42b4e9 14th\n+y notes on 14th commit\n+\n+e5d4fb5 13th\n+y notes on 13th commit\n+\n+7038787 12th\n+\n+6352c5e 11th\n+z notes on 11th commit\n+\n+ffed603 10th\n+x notes on 10th commit\n+\n+016e982 9th\n+\n+80d796d 8th\n+z notes on 8th commit\n+\n+2ede894 7th\n+\n+387a899 6th\n+\n+bd17532 5th\n+z notes on 5th commit\n+\n+1502353 4th\n+y notes on 4th commit\n+\n+1584215 3rd\n+y notes on 3rd commit\n+\n+268048b 2nd\n+z notes on 2nd commit\n+\n+34b09d6 1st\n+\n+EOF\n+\n+test_expect_success 'merge z into y with \"theirs\" resolver => Non-conflicting 3-way merge' '\n+\tgit notes merge --resolve=theirs z &&\n+\t# Verify merge result\n+\tgit notes >output_notes_theirs &&\n+\ttest_cmp expect_notes_theirs output_notes_theirs &&\n+\tgit log --format=\"%h %s%n%N\" >output_log_theirs &&\n+\ttest_cmp expect_log_theirs output_log_theirs\n+'\n+\n+test_expect_success 'reset to pre-merge state (y)' '\n+\tgit update-ref refs/notes/y refs/notes/y^1 &&\n+\t# Verify pre-merge state\n+\tgit notes >output_notes_y &&\n+\ttest_cmp expect_notes_y output_notes_y &&\n+\tgit log --format=\"%h %s%n%N\" >output_log_y &&\n+\ttest_cmp expect_log_y output_log_y\n+'\n+\n+cat >expect_notes_union <<EOF\n+e2bfd06a37dd2031684a59a6e2b033e212239c78 15023535574ded8b1a89052b32673f84cf9582b8\n+5772f42408c0dd6f097a7ca2d24de0e78d1c46b1 1584215f1d29c65e99c6c6848626553fdd07fd75\n+283b48219aee9a4105f6cab337e789065c82c2b9 268048bfb8a1fb38e703baceb8ab235421bf80c5\n+7c4e546efd0fe939f876beb262ece02797880b54 516bb9cbbfaa1be722bcf97df393a12e1e7c944d\n+7e3c53503a3db8dd996cb62e37c66e070b44b54d 6352c5e33dbcab725fe0579be16aa2ba8eb369be\n+a66055fa82f7a03fe0c02a6aba3287a85abf7c62 7038787dfe22a14c3867ce816dbba39845359719\n+851e1638784a884c7dd26c5d41f3340f6387413a 80d796defacd5db327b7a4e50099663902fbdc5c\n+6c841cc36ea496027290967ca96bd2bef54dbb47 bd1753200303d0a0344be813e504253b3d98e74d\n+3a631fdb6f41b05b55d8f4baf20728ba8f6fccbc e5d4fb5698d564ab8c73551538ecaf2b0c666185\n+5de7ea7ad4f47e7ff91989fb82234634730f75df f42b4e95384566bc7b31d9d3bfcec2d1a8f3e2e8\n+b8d03e173f67f6505a76f6e00cf93440200dd9be ffed603236bfa3891c49644257a83598afe8ae5a\n+EOF\n+\n+cat >expect_log_union <<EOF\n+516bb9c 15th\n+y notes on 15th commit\n+\n+z notes on 15th commit\n+\n+f42b4e9 14th\n+y notes on 14th commit\n+\n+e5d4fb5 13th\n+y notes on 13th commit\n+\n+7038787 12th\n+y notes on 12th commit\n+\n+6352c5e 11th\n+z notes on 11th commit\n+\n+ffed603 10th\n+x notes on 10th commit\n+\n+016e982 9th\n+\n+80d796d 8th\n+z notes on 8th commit\n+\n+2ede894 7th\n+\n+387a899 6th\n+\n+bd17532 5th\n+y notes on 5th commit\n+\n+z notes on 5th commit\n+\n+1502353 4th\n+y notes on 4th commit\n+\n+1584215 3rd\n+y notes on 3rd commit\n+\n+268048b 2nd\n+z notes on 2nd commit\n+\n+34b09d6 1st\n+\n+EOF\n+\n+test_expect_success 'merge z into y with \"union\" resolver => Non-conflicting 3-way merge' '\n+\tgit notes merge --resolve=union z &&\n+\t# Verify merge result\n+\tgit notes >output_notes_union &&\n+\ttest_cmp expect_notes_union output_notes_union &&\n+\tgit log --format=\"%h %s%n%N\" >output_log_union &&\n+\ttest_cmp expect_log_union output_log_union\n+'\n+\n+test_done\n-- \n1.7.2.220.gea1d3\n"},{"id":"146053","messageId":"1279880104-29796-14-git-send-email-johan@herland.net","threadId":"24479","inReplyTo":"1279880104-29796-1-git-send-email-johan@herland.net","subject":"[WIP/RFC 13/13] Documentation: Preliminary docs on 'git notes merge'","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-07-23T10:15:04Z","receivedAt":"2010-07-23T10:15:04Z","isPatch":false,"sender":{"key":"johan@herland.net","avatar":"https://avatars.githubusercontent.com/u/547031?v=4"},"body":"Signed-off-by: Johan Herland <johan@herland.net>\n---\n Documentation/git-notes.txt |   26 ++++++++++++++++++++++++++\n 1 files changed, 26 insertions(+), 0 deletions(-)\n\ndiff --git a/Documentation/git-notes.txt b/Documentation/git-notes.txt\nindex ce0ea03..f4ad4cc 100644\n--- a/Documentation/git-notes.txt\n+++ b/Documentation/git-notes.txt\n@@ -14,6 +14,7 @@ SYNOPSIS\n 'git notes' append [-F <file> | -m <msg> | (-c | -C) <object>] [<object>]\n 'git notes' edit [<object>]\n 'git notes' show [<object>]\n+'git notes' merge [-v | -q] [-X <strategy> ] <notes_ref>\n 'git notes' remove [<object>]\n 'git notes' prune [-n | -v]\n \n@@ -83,6 +84,15 @@ edit::\n show::\n \tShow the notes for a given object (defaults to HEAD).\n \n+merge::\n+\tMerge the given notes ref into the current notes ref.\n+\tThis will try to merge the changes made by the given\n+\tnotes ref (called \"remote\") since the merge-base (if\n+\tany) into the current notes ref (called \"local\").\n++\n+If conflicts arise (and a strategy for auto-resolving conflicting notes\n+(see the -X/--resolve option) is not given, the merge fails (TODO).\n+\n remove::\n \tRemove the notes for a given object (defaults to HEAD).\n \tThis is equivalent to specifying an empty note message to\n@@ -132,8 +142,24 @@ OPTIONS\n \tDo not remove anything; just report the object names whose notes\n \twould be removed.\n \n+-X <strategy>::\n+--resolve=<strategy>::\n+\tWhen merging notes, resolve notes conflicts using the given\n+\tstrategy. The following strategies are recognized: \"manual\"\n+\t(default), \"ours\", \"theirs\" and \"union\". \"ours\" automatically\n+\tresolves conflicting notes in favor of the local version (i.e.\n+\tthe current notes ref). \"theirs\" auto-resolves notes conflicts\n+\tin favor of the remote version (i.e. the given notes ref being\n+\tmerged into the current notes ref). \"union\" auto-resolves\n+\tnotes conflicts by concatenating the local and remote versions.\n+\n+-q::\n+--quiet::\n+\tWhen merging notes, operate quietly.\n+\n -v::\n --verbose::\n+\tWhen merging notes, be more verbose.\n \tWhen pruning notes, report all object names whose notes are\n \tremoved.\n \n-- \n1.7.2.220.gea1d3\n"},{"id":"146080","messageId":"20100723152344.GA1709@burratino","threadId":"24479","inReplyTo":"1279880104-29796-5-git-send-email-johan@herland.net","subject":"Re: [WIP/RFC 04/13] notes.h/c: Propagate combine_notes_fn return value to add_note() and beyond","fromName":"Jonathan Nieder","fromEmail":"jrnieder@gmail.com","sentAt":"2010-07-23T15:23:44Z","receivedAt":"2010-07-23T15:23:44Z","isPatch":false,"sender":{"key":"jrnieder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/281595?v=4"},"body":"Hi,\n\nJohan Herland wrote:\n\n> --- a/builtin/notes.c\n> +++ b/builtin/notes.c\n> @@ -574,6 +574,7 @@ static int add(int argc, const char **argv, const char *prefix)\n>  \tif (is_null_sha1(new_note))\n>  \t\tremove_note(t, object);\n>  \telse\n> +\t\t/* No return value checking; c_n_overwrite always returns 0 */\n>  \t\tadd_note(t, object, new_note, combine_notes_overwrite);\n\nI suspect something like\n\n\tif (add_note(t, object, ...))\n\t\tdie(\"confused: combine_notes_overwrite failed\");\n\nwould be less likely to fall out of date. ;-)\n\nThis whole series is good stuff.  I look forward to trying it\nout.\n\nThanks,\nJonathan\n"},{"id":"146119","messageId":"201007240039.25005.johan@herland.net","threadId":"24479","inReplyTo":"20100723152344.GA1709@burratino","subject":"Re: [WIP/RFC 04/13] notes.h/c: Propagate combine_notes_fn return value to add_note() and beyond","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-07-23T22:39:24Z","receivedAt":"2010-07-23T22:39:24Z","isPatch":false,"sender":{"key":"johan@herland.net","avatar":"https://avatars.githubusercontent.com/u/547031?v=4"},"body":"On Friday 23 July 2010, Jonathan Nieder wrote:\n> Johan Herland wrote:\n> > +\t\t/* No return value checking; c_n_overwrite always returns 0 */\n> >  \t\tadd_note(t, object, new_note, combine_notes_overwrite);\n> \n> I suspect something like\n> \n> \tif (add_note(t, object, ...))\n> \t\tdie(\"confused: combine_notes_overwrite failed\");\n> \n> would be less likely to fall out of date. ;-)\n\nGood call, I'll squash the following into the next iteration:\n\nThanks!\n\n...Johan\n\n\ndiff --git a/builtin/notes.c b/builtin/notes.c\nindex 516401c..5aaae03 100644\n--- a/builtin/notes.c\n+++ b/builtin/notes.c\n@@ -576,9 +576,8 @@ static int add(int argc, const char **argv, const char *prefix)\n \n \tif (is_null_sha1(new_note))\n \t\tremove_note(t, object);\n-\telse\n-\t\t/* No return value checking; c_n_overwrite always returns 0 */\n-\t\tadd_note(t, object, new_note, combine_notes_overwrite);\n+\telse if (add_note(t, object, new_note, combine_notes_overwrite))\n+\t\tdie(\"confused: combine_notes_overwrite failed\");\n \n \tsnprintf(logmsg, sizeof(logmsg), \"Notes %s by 'git notes %s'\",\n \t\t is_null_sha1(new_note) ? \"removed\" : \"added\", \"add\");\n@@ -657,8 +656,8 @@ static int copy(int argc, const char **argv, const char *prefix)\n \t\tgoto out;\n \t}\n \n-\t/* No return value checking; c_n_overwrite always returns 0 */\n-\tadd_note(t, object, from_note, combine_notes_overwrite);\n+\tif (add_note(t, object, from_note, combine_notes_overwrite))\n+\t\tdie(\"confused: combine_notes_overwrite failed\");\n \tcommit_notes(t, \"Notes added by 'git notes copy'\");\n out:\n \tfree_notes(t);\n@@ -717,9 +716,8 @@ static int append_edit(int argc, const char **argv, const char *prefix)\n \n \tif (is_null_sha1(new_note))\n \t\tremove_note(t, object);\n-\telse\n-\t\t/* No return value checking; c_n_overwrite always returns 0 */\n-\t\tadd_note(t, object, new_note, combine_notes_overwrite);\n+\telse if (add_note(t, object, new_note, combine_notes_overwrite))\n+\t\tdie(\"confused: combine_notes_overwrite failed\");\n \n \tsnprintf(logmsg, sizeof(logmsg), \"Notes %s by 'git notes %s'\",\n \t\t is_null_sha1(new_note) ? \"removed\" : \"added\", argv[0]);\ndiff --git a/notes-merge.c b/notes-merge.c\nindex 122d6b9..47cd32a 100644\n--- a/notes-merge.c\n+++ b/notes-merge.c\n@@ -442,12 +442,14 @@ static int merge_one_change(struct notes_merge_options *o,\n \t\treturn 0;\n \tcase NOTES_MERGE_RESOLVE_THEIRS:\n \t\tOUTPUT(o, 2, \"Using remote notes for %s\", sha1_to_hex(p->obj));\n-\t\tadd_note(t, p->obj, p->remote, combine_notes_overwrite);\n+\t\tif (add_note(t, p->obj, p->remote, combine_notes_overwrite))\n+\t\t\tdie(\"confused: combine_notes_overwrite failed\");\n \t\treturn 0;\n \tcase NOTES_MERGE_RESOLVE_UNION:\n \t\tOUTPUT(o, 2, \"Concatenating local and remote notes for %s\",\n \t\t       sha1_to_hex(p->obj));\n-\t\tadd_note(t, p->obj, p->remote, combine_notes_concatenate);\n+\t\tif (add_note(t, p->obj, p->remote, combine_notes_concatenate))\n+\t\t\tdie(\"confused: combine_notes_concatenate failed\");\n \t\treturn 0;\n \t}\n \tdie(\"Unknown resolve method (%i).\", o->resolve);\n@@ -489,7 +491,9 @@ static int merge_changes(struct notes_merge_options *o,\n \t\t\t   !hashcmp(p->local, p->base)) {\n \t\t\t/* no local change; adopt remote change */\n \t\t\tOUTPUT(o, 5, \"\\t\\t\\tno local change, adopting remote\");\n-\t\t\tadd_note(t, p->obj, p->remote, combine_notes_overwrite);\n+\t\t\tif (add_note(t, p->obj, p->remote,\n+\t\t\t\t     combine_notes_overwrite))\n+\t\t\t\tdie(\"confused: combine_notes_overwrite failed\");\n \t\t} else {\n \t\t\t/* need file-level merge between local and remote */\n \t\t\tOUTPUT(o, 5, \"\\t\\t\\tneed content-level merge\");\n\n\n-- \nJohan Herland, <johan@herland.net>\nwww.herland.net\n"},{"id":"146142","messageId":"4C4AA1C3.8030907@gmail.com","threadId":"24479","inReplyTo":"1279880104-29796-13-git-send-email-johan@herland.net","subject":"Re: [WIP/RFC 12/13] git notes merge: Add automatic conflict resolvers (ours, theirs, union)","fromName":"Stephen Boyd","fromEmail":"bebarino@gmail.com","sentAt":"2010-07-24T08:18:11Z","receivedAt":"2010-07-24T08:18:11Z","isPatch":false,"sender":{"key":"bebarino@gmail.com","avatar":"https://avatars.githubusercontent.com/u/38832?v=4"},"body":"  On 07/23/2010 03:15 AM, Johan Herland wrote:\n> +\n> +# Set up a notes merge scenario with all kinds of potential conflicts\n> +test_expect_success 'setup commits' '\n> +\t# Create 15 commits with tags (\"1st\" through \"15th\")\n> +\t:>  a1&&  git add a1&&  test_tick&&  git commit -m 1st&&  git tag 1st&&\n> +\t:>  a2&&  git add a2&&  test_tick&&  git commit -m 2nd&&  git tag 2nd&&\n> +\t:>  a3&&  git add a3&&  test_tick&&  git commit -m 3rd&&  git tag 3rd&&\n> +\t:>  a4&&  git add a4&&  test_tick&&  git commit -m 4th&&  git tag 4th&&\n> +\t:>  a5&&  git add a5&&  test_tick&&  git commit -m 5th&&  git tag 5th&&\n> +\t:>  a6&&  git add a6&&  test_tick&&  git commit -m 6th&&  git tag 6th&&\n> +\t:>  a7&&  git add a7&&  test_tick&&  git commit -m 7th&&  git tag 7th&&\n> +\t:>  a8&&  git add a8&&  test_tick&&  git commit -m 8th&&  git tag 8th&&\n> +\t:>  a9&&  git add a9&&  test_tick&&  git commit -m 9th&&  git tag 9th&&\n> +\t:>  a10&&  git add a10&&  test_tick&&  git commit -m 10th&&  git tag 10th&&\n> +\t:>  a11&&  git add a11&&  test_tick&&  git commit -m 11th&&  git tag 11th&&\n> +\t:>  a12&&  git add a12&&  test_tick&&  git commit -m 12th&&  git tag 12th&&\n> +\t:>  a13&&  git add a13&&  test_tick&&  git commit -m 13th&&  git tag 13th&&\n> +\t:>  a14&&  git add a14&&  test_tick&&  git commit -m 14th&&  git tag 14th&&\n> +\t:>  a15&&  git add a15&&  test_tick&&  git commit -m 15th&&  git tag 15th\n> +'\n\nCan you use test_commit here?\n"},{"id":"146143","messageId":"4C4AA297.4090605@gmail.com","threadId":"24479","inReplyTo":"1279880104-29796-14-git-send-email-johan@herland.net","subject":"Re: [WIP/RFC 13/13] Documentation: Preliminary docs on 'git notes merge'","fromName":"Stephen Boyd","fromEmail":"bebarino@gmail.com","sentAt":"2010-07-24T08:21:43Z","receivedAt":"2010-07-24T08:21:43Z","isPatch":false,"sender":{"key":"bebarino@gmail.com","avatar":"https://avatars.githubusercontent.com/u/38832?v=4"},"body":"  On 07/23/2010 03:15 AM, Johan Herland wrote:\n> +-X<strategy>::\n> +--resolve=<strategy>::\n> +\tWhen merging notes, resolve notes conflicts using the given\n> +\tstrategy. The following strategies are recognized: \"manual\"\n> +\t(default), \"ours\", \"theirs\" and \"union\". \"ours\" automatically\n> +\tresolves conflicting notes in favor of the local version (i.e.\n> +\tthe current notes ref). \"theirs\" auto-resolves notes conflicts\n> +\tin favor of the remote version (i.e. the given notes ref being\n> +\tmerged into the current notes ref). \"union\" auto-resolves\n> +\tnotes conflicts by concatenating the local and remote versions.\n> +\n\nWe use both \"auto-resolves\" and \"automatically resolves\" in this paragraph. Perhaps it's better to use one term throughout?\n"},{"id":"146144","messageId":"4C4AA500.7010205@gmail.com","threadId":"24479","inReplyTo":"1279880104-29796-10-git-send-email-johan@herland.net","subject":"Re: [WIP/RFC 09/13] git notes merge: Initial implementation handling trivial merges only","fromName":"Stephen Boyd","fromEmail":"bebarino@gmail.com","sentAt":"2010-07-24T08:32:00Z","receivedAt":"2010-07-24T08:32:00Z","isPatch":false,"sender":{"key":"bebarino@gmail.com","avatar":"https://avatars.githubusercontent.com/u/38832?v=4"},"body":"  On 07/23/2010 03:15 AM, Johan Herland wrote:\n> +\tif (1<  argc) {\n> +\t\terror(\"too many parameters\");\n> +\t\tusage_with_options(git_notes_merge_usage, options);\n> +\t} else if (1>  argc) {\n> +\t\terror(\"too few parameters\");\n> +\t\tusage_with_options(git_notes_merge_usage, options);\n> +\t}\n> +\n>\n\nLooks like it only takes one <notes_ref>. In that case wouldn't it be better to say\n\n     if (argc != 1) {\n         error(\"Must specify a note ref to merge\");\n         usage_with_options(git_notes_merge_usage, options)\n     }\n"},{"id":"146294","messageId":"1280054725.2196.40.camel@arcturus","threadId":"24479","inReplyTo":"1279880104-29796-1-git-send-email-johan@herland.net","subject":"Re: [WIP/RFC 00/13] git notes merge","fromName":"Sam Vilain","fromEmail":"sam@vilain.net","sentAt":"2010-07-25T10:45:25Z","receivedAt":"2010-07-25T10:45:25Z","isPatch":false,"sender":{"key":"sam@vilain.net","avatar":"https://gravatar.com/avatar/8fc840ca854dbf6f7065b4335e3b934951c1dca3b11db688e95e471901f8f4a8?d=mp&s=160"},"body":"On Fri, 2010-07-23 at 12:14 +0200, Johan Herland wrote:\n> Hi,\n> \n> Here are some rough patches implementing most of the 'git notes merge'\n> functionality. There are still missing pieces in the area of manual\n> conflict resolution, but what's here should be enough to start\n> experimenting with automatic notes merge resolution.\n\nHaving implemented this before as a client-level solution I can applaud\nthe goal of tackling this.\n\nThere is also the issue of making the notes track automatically on 'git\nclone' or something like that, don't know if anyone's looked at it yet\nbut that's another annoyance I've had..\n\nSam\n"},{"id":"146281","messageId":"201007251614.32124.johan@herland.net","threadId":"24479","inReplyTo":"4C4AA500.7010205@gmail.com","subject":"Re: [WIP/RFC 09/13] git notes merge: Initial implementation handling trivial merges only","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-07-25T14:14:31Z","receivedAt":"2010-07-25T14:14:31Z","isPatch":false,"sender":{"key":"johan@herland.net","avatar":"https://avatars.githubusercontent.com/u/547031?v=4"},"body":"On Saturday 24 July 2010, Stephen Boyd wrote:\n>   On 07/23/2010 03:15 AM, Johan Herland wrote:\n> > +\tif (1<  argc) {\n> > +\t\terror(\"too many parameters\");\n> > +\t\tusage_with_options(git_notes_merge_usage, options);\n> > +\t} else if (1>  argc) {\n> > +\t\terror(\"too few parameters\");\n> > +\t\tusage_with_options(git_notes_merge_usage, options);\n> > +\t}\n> > +\n> \n> Looks like it only takes one <notes_ref>. In that case wouldn't it be\n> better to say\n> \n>      if (argc != 1) {\n>          error(\"Must specify a note ref to merge\");\n>          usage_with_options(git_notes_merge_usage, options)\n>      }\n\nThanks. I'll squash the following into the next iteration:\n\n...Johan\n\ndiff --git a/builtin/notes.c b/builtin/notes.c\nindex f2bc767..ca0e4d9 100644\n--- a/builtin/notes.c\n+++ b/builtin/notes.c\n@@ -780,11 +780,8 @@ static int merge(int argc, const char **argv, const char *prefix)\n \targc = parse_options(argc, argv, prefix, options,\n \t\t\t     git_notes_merge_usage, 0);\n \n-\tif (1 < argc) {\n-\t\terror(\"too many parameters\");\n-\t\tusage_with_options(git_notes_merge_usage, options);\n-\t} else if (1 > argc) {\n-\t\terror(\"too few parameters\");\n+\tif (argc != 1) {\n+\t\terror(\"Must specify a notes ref to merge\");\n \t\tusage_with_options(git_notes_merge_usage, options);\n \t}\n \n\n\n-- \nJohan Herland, <johan@herland.net>\nwww.herland.net\n"},{"id":"146296","messageId":"201007251844.32542.johan@herland.net","threadId":"24479","inReplyTo":"1280054725.2196.40.camel@arcturus","subject":"Re: [WIP/RFC 00/13] git notes merge","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-07-25T16:44:32Z","receivedAt":"2010-07-25T16:44:32Z","isPatch":false,"sender":{"key":"johan@herland.net","avatar":"https://avatars.githubusercontent.com/u/547031?v=4"},"body":"On Sunday 25 July 2010, Sam Vilain wrote:\n> On Fri, 2010-07-23 at 12:14 +0200, Johan Herland wrote:\n> > Hi,\n> > \n> > Here are some rough patches implementing most of the 'git notes merge'\n> > functionality. There are still missing pieces in the area of manual\n> > conflict resolution, but what's here should be enough to start\n> > experimenting with automatic notes merge resolution.\n> \n> Having implemented this before as a client-level solution I can applaud\n> the goal of tackling this.\n> \n> There is also the issue of making the notes track automatically on 'git\n> clone' or something like that, don't know if anyone's looked at it yet\n> but that's another annoyance I've had..\n\nAgreed, although I see the solving the merge problem as a prerequisite to \ndealing with exchanging notes between repos. One thing at a time...\n\n\n...Johan\n\n-- \nJohan Herland, <johan@herland.net>\nwww.herland.net\n"},{"id":"146315","messageId":"201007252058.02964.johan@herland.net","threadId":"24479","inReplyTo":"4C4AA1C3.8030907@gmail.com","subject":"Re: [WIP/RFC 12/13] git notes merge: Add automatic conflict resolvers (ours, theirs, union)","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-07-25T18:58:02Z","receivedAt":"2010-07-25T18:58:02Z","isPatch":false,"sender":{"key":"johan@herland.net","avatar":"https://avatars.githubusercontent.com/u/547031?v=4"},"body":"On Saturday 24 July 2010, Stephen Boyd wrote:\n>   On 07/23/2010 03:15 AM, Johan Herland wrote:\n> > +\n> > +# Set up a notes merge scenario with all kinds of potential conflicts\n> > +test_expect_success 'setup commits' '\n> > +\t# Create 15 commits with tags (\"1st\" through \"15th\")\n> > [...]\n> \n> Can you use test_commit here?\n\nYes, indeed. Thanks for noticing. I've rewritten the tests using\ntest_commit (and using variables to store commit SHA1s, so that the\ntests are more robust against those changing)\n\n\n...Johan\n\n-- \nJohan Herland, <johan@herland.net>\nwww.herland.net\n"},{"id":"146316","messageId":"201007252102.52632.johan@herland.net","threadId":"24479","inReplyTo":"4C4AA297.4090605@gmail.com","subject":"Re: [WIP/RFC 13/13] Documentation: Preliminary docs on 'git notes merge'","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-07-25T19:02:52Z","receivedAt":"2010-07-25T19:02:52Z","isPatch":false,"sender":{"key":"johan@herland.net","avatar":"https://avatars.githubusercontent.com/u/547031?v=4"},"body":"On Saturday 24 July 2010, Stephen Boyd wrote:\n>   On 07/23/2010 03:15 AM, Johan Herland wrote:\n> > +-X<strategy>::\n> > +--resolve=<strategy>::\n> > +\tWhen merging notes, resolve notes conflicts using the given\n> > +\tstrategy. The following strategies are recognized: \"manual\"\n> > +\t(default), \"ours\", \"theirs\" and \"union\". \"ours\" automatically\n> > +\tresolves conflicting notes in favor of the local version (i.e.\n> > +\tthe current notes ref). \"theirs\" auto-resolves notes conflicts\n> > +\tin favor of the remote version (i.e. the given notes ref being\n> > +\tmerged into the current notes ref). \"union\" auto-resolves\n> > +\tnotes conflicts by concatenating the local and remote versions.\n> > +\n> \n> We use both \"auto-resolves\" and \"automatically resolves\" in this\n> paragraph. Perhaps it's better to use one term throughout?\n\nAgreed. I'll squash the following into the next iteration:\n\n\n...Johan\n\n\ndiff --git a/Documentation/git-notes.txt b/Documentation/git-notes.txt\nindex f4ad4cc..9228349 100644\n--- a/Documentation/git-notes.txt\n+++ b/Documentation/git-notes.txt\n@@ -90,8 +90,9 @@ merge::\n \tnotes ref (called \"remote\") since the merge-base (if\n \tany) into the current notes ref (called \"local\").\n +\n-If conflicts arise (and a strategy for auto-resolving conflicting notes\n-(see the -X/--resolve option) is not given, the merge fails (TODO).\n+If conflicts arise (and a strategy for automatically resolving\n+conflicting notes (see the -X/--resolve option) is not given,\n+the merge fails (TODO).\n \n remove::\n \tRemove the notes for a given object (defaults to HEAD).\n@@ -146,12 +147,14 @@ OPTIONS\n --resolve=<strategy>::\n \tWhen merging notes, resolve notes conflicts using the given\n \tstrategy. The following strategies are recognized: \"manual\"\n-\t(default), \"ours\", \"theirs\" and \"union\". \"ours\" automatically\n-\tresolves conflicting notes in favor of the local version (i.e.\n-\tthe current notes ref). \"theirs\" auto-resolves notes conflicts\n-\tin favor of the remote version (i.e. the given notes ref being\n-\tmerged into the current notes ref). \"union\" auto-resolves\n-\tnotes conflicts by concatenating the local and remote versions.\n+\t(default), \"ours\", \"theirs\" and \"union\".\n+\t\"ours\" automatically resolves conflicting notes in favor of the\n+\tlocal version (i.e. the current notes ref).\n+\t\"theirs\" automatically resolves notes conflicts in favor of the\n+\tremote version (i.e. the given notes ref being merged into the\n+\tcurrent notes ref).\n+\t\"union\" automatically resolves notes conflicts by concatenating\n+\tthe local and remote versions.\n \n -q::\n --quiet::\n\n\n\n-- \nJohan Herland, <johan@herland.net>\nwww.herland.net\n"}]}