{"thread":{"id":"25543","subject":"[PATCHv5 04/23] notes.c: Reorder functions in preparation for next commit","startedAt":"2010-10-25T00:08:30Z","lastAt":"2010-11-01T00:09:38Z","messageCount":31,"participants":["Johan Herland","Jonathan Nieder","Junio C Hamano"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"154358","messageId":"1287965333-5099-1-git-send-email-johan@herland.net","threadId":"25543","inReplyTo":null,"subject":"[PATCHv5 00/23] git notes merge","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-10-25T00:08:30Z","receivedAt":"2010-10-25T00:08:30Z","isPatch":false,"sender":{"key":"johan@herland.net","avatar":"https://avatars.githubusercontent.com/u/547031?v=4"},"body":"Hi,\n\nThis is the 5th iteration of the 'git notes merge' patch series.\n\nIn short, patch #5 has an updated commit message, patch #6 has a\nminor improvement, patch #16 has renames \"--reset\" to \"--abort\",\nand there are 2 new patches at the end (totally independent from\nthe other patches) implementing 'git merge --abort'. Feel free to\nconsider the two last patches separately from the rest of the series.\n\nChanges between v4 and v5:\n\n- (Jonathan Nieder) Improved commit messages\n\n- (Jonathan Nieder) Improve clarity of final if-condition in\n  note_tree_insert()\n\n- (Sverre Rabbelier) Rename 'git notes merge --reset' to\n  'git notes merge --abort'.\n\n- Introduce 'git merge --abort' as a synonym of 'git reset --merge'\n  (when MERGE_HEAD is present).\n\n\nSome open questions (not already being discussed in other threads):\n\n- Should we refuse to finalize a notes merge when conflict markers\n  present in .git/NOTES_MERGE_WORKTREE? Since add/commit in regular\n  merges does NOT do this, I have not implemented it for notes merge.\n\n\nHave fun! :)\n\n...Johan\n\n\nJohan Herland (23):\n  notes.c: Hexify SHA1 in die() message from init_notes()\n  (trivial) notes.h: Minor documentation fixes to copy_notes()\n  notes.h: Make default_notes_ref() available in notes API\n  notes.c: Reorder functions in preparation for next commit\n  notes.h/c: Allow combine_notes functions to remove notes\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  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  git notes merge: Manual conflict resolution, part 1/2\n  git notes merge: Manual conflict resolution, part 2/2\n  git notes merge: List conflicting notes in notes merge commit message\n  git notes merge: --commit should fail if underlying notes ref has moved\n  git notes merge: Add another auto-resolving strategy: \"cat_sort_uniq\"\n  git notes merge: Add testcases for merging notes trees at different fanouts\n  Provide 'git notes get-ref' to easily retrieve current notes ref\n  cmd_merge(): Parse options before checking MERGE_HEAD\n  Provide 'git merge --abort' as a synonym to 'git reset --merge'\n\n Documentation/git-merge.txt           |   29 ++-\n Documentation/git-notes.txt           |   85 ++++-\n Makefile                              |    2 +\n builtin.h                             |    2 +-\n builtin/merge.c                       |   47 ++-\n builtin/notes.c                       |  268 +++++++++++--\n notes-cache.c                         |    3 +-\n notes-merge.c                         |  735 +++++++++++++++++++++++++++++++++\n notes-merge.h                         |   98 +++++\n notes.c                               |  272 ++++++++----\n notes.h                               |   47 ++-\n t/t3301-notes.sh                      |   23 +\n t/t3303-notes-subtrees.sh             |   19 +-\n t/t3308-notes-merge.sh                |  368 +++++++++++++++++\n t/t3309-notes-merge-auto-resolve.sh   |  647 +++++++++++++++++++++++++++++\n t/t3310-notes-merge-manual-resolve.sh |  556 +++++++++++++++++++++++++\n t/t3311-notes-merge-fanout.sh         |  436 +++++++++++++++++++\n t/t3404-rebase-interactive.sh         |    1 +\n t/t7609-merge-abort.sh                |  273 ++++++++++++\n t/t9301-fast-import-notes.sh          |    5 +\n 20 files changed, 3754 insertions(+), 162 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 create mode 100755 t/t3310-notes-merge-manual-resolve.sh\n create mode 100755 t/t3311-notes-merge-fanout.sh\n create mode 100755 t/t7609-merge-abort.sh\n\n--\n1.7.3.98.g5ad7d9\n"},{"id":"154376","messageId":"1287965333-5099-2-git-send-email-johan@herland.net","threadId":"25543","inReplyTo":"1287965333-5099-1-git-send-email-johan@herland.net","subject":"[PATCHv5 01/23] notes.c: Hexify SHA1 in die() message from init_notes()","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-10-25T00:08:31Z","receivedAt":"2010-10-25T00:08:31Z","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 1 files changed, 1 insertions(+), 1 deletions(-)\n\ndiff --git a/notes.c b/notes.c\nindex 1978244..bb03eb0 100644\n--- a/notes.c\n+++ b/notes.c\n@@ -940,7 +940,7 @@ void init_notes(struct notes_tree *t, const char *notes_ref,\n \t\treturn;\n \tif (get_tree_entry(object_sha1, \"\", sha1, &mode))\n \t\tdie(\"Failed to read notes tree referenced by %s (%s)\",\n-\t\t    notes_ref, object_sha1);\n+\t\t    notes_ref, sha1_to_hex(object_sha1));\n \n \thashclr(root_tree.key_sha1);\n \thashcpy(root_tree.val_sha1, sha1);\n-- \n1.7.3.98.g5ad7d9\n"},{"id":"154355","messageId":"1287965333-5099-3-git-send-email-johan@herland.net","threadId":"25543","inReplyTo":"1287965333-5099-1-git-send-email-johan@herland.net","subject":"[PATCHv5 02/23] (trivial) notes.h: Minor documentation fixes to copy_notes()","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-10-25T00:08:32Z","receivedAt":"2010-10-25T00:08:32Z","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.3.98.g5ad7d9\n"},{"id":"154356","messageId":"1287965333-5099-4-git-send-email-johan@herland.net","threadId":"25543","inReplyTo":"1287965333-5099-1-git-send-email-johan@herland.net","subject":"[PATCHv5 03/23] notes.h: Make default_notes_ref() available in notes API","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-10-25T00:08:33Z","receivedAt":"2010-10-25T00:08:33Z","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 bb03eb0..d71c0a3 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.3.98.g5ad7d9\n"},{"id":"154354","messageId":"1287965333-5099-5-git-send-email-johan@herland.net","threadId":"25543","inReplyTo":"1287965333-5099-1-git-send-email-johan@herland.net","subject":"[PATCHv5 04/23] notes.c: Reorder functions in preparation for next commit","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-10-25T00:08:34Z","receivedAt":"2010-10-25T00:08:34Z","isPatch":false,"sender":{"key":"johan@herland.net","avatar":"https://avatars.githubusercontent.com/u/547031?v=4"},"body":"This patch introduces no functional change. It consists solely of reordering\nfunctions in notes.c to avoid use-before-declaration errors after applying\nthe next commit in this series.\n\nSigned-off-by: Johan Herland <johan@herland.net>\n---\n notes.c |  146 +++++++++++++++++++++++++++++++-------------------------------\n 1 files changed, 73 insertions(+), 73 deletions(-)\n\ndiff --git a/notes.c b/notes.c\nindex d71c0a3..bfb3ea5 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@@ -229,79 +302,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 {\n-- \n1.7.3.98.g5ad7d9\n"},{"id":"154359","messageId":"1287965333-5099-6-git-send-email-johan@herland.net","threadId":"25543","inReplyTo":"1287965333-5099-1-git-send-email-johan@herland.net","subject":"[PATCHv5 05/23] notes.h/c: Allow combine_notes functions to remove notes","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-10-25T00:08:35Z","receivedAt":"2010-10-25T00:08:35Z","isPatch":false,"sender":{"key":"johan@herland.net","avatar":"https://avatars.githubusercontent.com/u/547031?v=4"},"body":"Allow combine_notes functions to request that a note be removed, by setting\nthe resulting note SHA1 to null_sha1 (0000000...).\n\nFor consistency, also teach note_tree_insert() to skip insertion of an empty\nnote (a note with entry->val_sha1 equal to null_sha1) when there is no note\nto combine it with.\n\nIn general, an empty note (null_sha1) is treated identically to no note at\nall, but when adding an empty note where there already exists a non-empty\nnote, we allow the combine_notes function to potentially record a new/changed\nnote. Document this behaviour, and clearly specify how combine_notes functions\nare expected to handle null_sha1 in input.\n\nBefore this patch, storing null_sha1s in the notes tree were silently allowed,\ncausing an invalid notes tree (referring to blobs with null_sha1) to be\nproduced by write_notes_tree().\n\nSigned-off-by: Johan Herland <johan@herland.net>\n---\n notes.c |   12 +++++++++++-\n notes.h |   16 +++++++++++++++-\n 2 files changed, 26 insertions(+), 2 deletions(-)\n\ndiff --git a/notes.c b/notes.c\nindex bfb3ea5..0c13a36 100644\n--- a/notes.c\n+++ b/notes.c\n@@ -248,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@@ -264,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@@ -295,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);\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.3.98.g5ad7d9\n"},{"id":"154375","messageId":"1287965333-5099-7-git-send-email-johan@herland.net","threadId":"25543","inReplyTo":"1287965333-5099-1-git-send-email-johan@herland.net","subject":"[PATCHv5 06/23] notes.h/c: Propagate combine_notes_fn return value to add_note() and beyond","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-10-25T00:08:36Z","receivedAt":"2010-10-25T00:08:36Z","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\nExisting add_note() callers are updated to die() upon failure, thus\npreserving the current behaviour. The only exceptions are copy_note()\nand notes_cache_put() where we are able to propagate the add_note()\nreturn value instead.\n\nThis patch has been improved by the following contributions:\n- Jonathan Nieder: Future-proof by always checking add_note() return value\n- Jonathan Nieder: Improve clarity of final if-condition in note_tree_insert()\n\nThanks-to: Jonathan Nieder <jrnieder@gmail.com>\nSigned-off-by: Johan Herland <johan@herland.net>\n---\n builtin/notes.c |   11 ++++++-----\n notes-cache.c   |    3 +--\n notes.c         |   55 ++++++++++++++++++++++++++++---------------------------\n notes.h         |   11 ++++++++---\n 4 files changed, 43 insertions(+), 37 deletions(-)\n\ndiff --git a/builtin/notes.c b/builtin/notes.c\nindex fbc347c..35f6eb6 100644\n--- a/builtin/notes.c\n+++ b/builtin/notes.c\n@@ -573,8 +573,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\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@@ -653,7 +653,8 @@ static int copy(int argc, const char **argv, const char *prefix)\n \t\tgoto out;\n \t}\n \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@@ -712,8 +713,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\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-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 0c13a36..1674391 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,15 @@ 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+\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\treturn ret;\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+\treturn note_tree_insert(t, new_node, n + 1, entry, type, combine_notes);\n }\n \n /* Free the entire notes data contained in the given tree */\n@@ -452,8 +449,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 +1015,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 +1029,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 +1205,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 +1214,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.3.98.g5ad7d9\n"},{"id":"154357","messageId":"1287965333-5099-8-git-send-email-johan@herland.net","threadId":"25543","inReplyTo":"1287965333-5099-1-git-send-email-johan@herland.net","subject":"[PATCHv5 07/23] (trivial) t3303: Indent with tabs instead of spaces for consistency","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-10-25T00:08:37Z","receivedAt":"2010-10-25T00:08:37Z","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.3.98.g5ad7d9\n"},{"id":"154377","messageId":"1287965333-5099-9-git-send-email-johan@herland.net","threadId":"25543","inReplyTo":"1287965333-5099-1-git-send-email-johan@herland.net","subject":"[PATCHv5 08/23] notes.c: Use two newlines (instead of one) when concatenating notes","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-10-25T00:08:38Z","receivedAt":"2010-10-25T00:08:38Z","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 1674391..09a93ab 100644\n--- a/notes.c\n+++ b/notes.c\n@@ -812,16 +812,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 9f03ce6..9ed70dc 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.3.98.g5ad7d9\n"},{"id":"154360","messageId":"1287965333-5099-10-git-send-email-johan@herland.net","threadId":"25543","inReplyTo":"1287965333-5099-1-git-send-email-johan@herland.net","subject":"[PATCHv5 09/23] builtin/notes.c: Split notes ref DWIMmery into a separate function","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-10-25T00:08:39Z","receivedAt":"2010-10-25T00:08:39Z","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 35f6eb6..9c91c59 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@@ -839,13 +849,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.3.98.g5ad7d9\n"},{"id":"154361","messageId":"1287965333-5099-11-git-send-email-johan@herland.net","threadId":"25543","inReplyTo":"1287965333-5099-1-git-send-email-johan@herland.net","subject":"[PATCHv5 10/23] git notes merge: Initial implementation handling trivial merges only","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-10-25T00:08:40Z","receivedAt":"2010-10-25T00:08:40Z","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\nThis patch has been improved by the following contributions:\n- Stephen Boyd: Simplify argc logic\n- Stephen Boyd: Use test_commit\n- Ævar Arnfjörð Bjarmason: Don't use C99 comments.\n- Jonathan Nieder: Add constants for common verbosity values\n- Jonathan Nieder: Use trace_printf(...) instead of OUTPUT(o, 5, ...)\n- Jonathan Nieder: Remove extraneous show() function\n- Jonathan Nieder: Clarify handling of empty/missing notes ref in notes_merge()\n\nThanks-to: Stephen Boyd <bebarino@gmail.com>\nThanks-to: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\nThanks-to: Jonathan Nieder <jrnieder@gmail.com>\nSigned-off-by: Johan Herland <johan@herland.net>\n---\n Makefile               |    2 +\n builtin/notes.c        |   54 ++++++++++++++\n notes-merge.c          |  120 ++++++++++++++++++++++++++++++++\n notes-merge.h          |   36 ++++++++++\n t/t3308-notes-merge.sh |  180 ++++++++++++++++++++++++++++++++++++++++++++++++\n 5 files changed, 392 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 f33648d..14c0ff1 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 9c91c59..fbabdc7 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@@ -772,6 +779,51 @@ 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 (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+\tinit_notes_merge_options(&o);\n+\to.verbosity = verbosity + NOTES_MERGE_VERBOSITY_DEFAULT;\n+\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@@ -865,6 +917,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..cd917f9\n--- /dev/null\n+++ b/notes-merge.c\n@@ -0,0 +1,120 @@\n+#include \"cache.h\"\n+#include \"commit.h\"\n+#include \"refs.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 = NOTES_MERGE_VERBOSITY_DEFAULT;\n+}\n+\n+#define OUTPUT(o, v, ...) \\\n+\tdo { \\\n+\t\tif ((o)->verbosity >= (v)) { \\\n+\t\t\tprintf(__VA_ARGS__); \\\n+\t\t\tputs(\"\"); \\\n+\t\t} \\\n+\t} 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+\tassert(o->local_ref && o->remote_ref);\n+\thashclr(result_sha1);\n+\n+\ttrace_printf(\"notes_merge(o->local_ref = %s, o->remote_ref = %s)\\n\",\n+\t       o->local_ref, o->remote_ref);\n+\n+\t/* Dereference o->local_ref into local_sha1 */\n+\tif (!resolve_ref(o->local_ref, local_sha1, 0, NULL))\n+\t\tdie(\"Failed to resolve local notes ref '%s'\", o->local_ref);\n+\telse if (!check_ref_format(o->local_ref) && is_null_sha1(local_sha1))\n+\t\tlocal = NULL; /* local_sha1 == null_sha1 indicates unborn ref */\n+\telse if (!(local = lookup_commit_reference(local_sha1)))\n+\t\tdie(\"Could not parse local commit %s (%s)\",\n+\t\t    sha1_to_hex(local_sha1), o->local_ref);\n+\ttrace_printf(\"\\tlocal commit: %.7s\\n\", sha1_to_hex(local_sha1));\n+\n+\t/* Dereference o->remote_ref into remote_sha1 */\n+\tif (get_sha1(o->remote_ref, remote_sha1)) {\n+\t\t/*\n+\t\t * Failed to get remote_sha1. If o->remote_ref looks like an\n+\t\t * unborn ref, perform the merge using an empty notes tree.\n+\t\t */\n+\t\tif (!check_ref_format(o->remote_ref)) {\n+\t\t\thashclr(remote_sha1);\n+\t\t\tremote = NULL;\n+\t\t}\n+\t\telse\n+\t\t\tdie(\"Failed to resolve remote notes ref '%s'\",\n+\t\t\t    o->remote_ref);\n+\t}\n+\telse if (!(remote = lookup_commit_reference(remote_sha1)))\n+\t\tdie(\"Could not parse remote commit %s (%s)\",\n+\t\t    sha1_to_hex(remote_sha1), o->remote_ref);\n+\ttrace_printf(\"\\tremote commit: %.7s\\n\", sha1_to_hex(remote_sha1));\n+\n+\tif (!local && !remote)\n+\t\tdie(\"Cannot merge empty notes ref (%s) into empty notes ref \"\n+\t\t    \"(%s)\", o->remote_ref, o->local_ref);\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+\ttrace_printf(\"notes_merge(): result = %i, result_sha1 = %.7s\\n\",\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..fd572ac\n--- /dev/null\n+++ b/notes-merge.h\n@@ -0,0 +1,36 @@\n+#ifndef NOTES_MERGE_H\n+#define NOTES_MERGE_H\n+\n+enum notes_merge_verbosity {\n+\tNOTES_MERGE_VERBOSITY_DEFAULT = 2,\n+\tNOTES_MERGE_VERBOSITY_MAX = 5\n+};\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+ * Both o->local_ref and o->remote_ref must be given (non-NULL), but either ref\n+ * (although not both) may refer to a non-existing notes ref, in which case\n+ * that notes ref is interpreted as an empty notes tree, and the merge\n+ * trivially results in 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..9acb684\n--- /dev/null\n+++ b/t/t3308-notes-merge.sh\n@@ -0,0 +1,180 @@\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+test_expect_success setup '\n+\ttest_commit 1st &&\n+\ttest_commit 2nd &&\n+\ttest_commit 3rd &&\n+\ttest_commit 4th &&\n+\ttest_commit 5th &&\n+\t# Create notes on 4 first commits\n+\tgit config core.notesRef refs/notes/x &&\n+\tgit notes add -m \"Notes on 1st commit\" 1st &&\n+\tgit notes add -m \"Notes on 2nd commit\" 2nd &&\n+\tgit notes add -m \"Notes on 3rd commit\" 3rd &&\n+\tgit notes add -m \"Notes on 4th commit\" 4th\n+'\n+\n+commit_sha1=$(git rev-parse 1st^{commit})\n+commit_sha2=$(git rev-parse 2nd^{commit})\n+commit_sha3=$(git rev-parse 3rd^{commit})\n+commit_sha4=$(git rev-parse 4th^{commit})\n+commit_sha5=$(git rev-parse 5th^{commit})\n+\n+verify_notes () {\n+\tnotes_ref=\"$1\"\n+\tgit -c core.notesRef=\"refs/notes/$notes_ref\" notes |\n+\t\tsort >\"output_notes_$notes_ref\" &&\n+\ttest_cmp \"expect_notes_$notes_ref\" \"output_notes_$notes_ref\" &&\n+\tgit -c core.notesRef=\"refs/notes/$notes_ref\" log --format=\"%H %s%n%N\" \\\n+\t\t>\"output_log_$notes_ref\" &&\n+\ttest_cmp \"expect_log_$notes_ref\" \"output_log_$notes_ref\"\n+}\n+\n+cat <<EOF | sort >expect_notes_x\n+5e93d24084d32e1cb61f7070505b9d2530cca987 $commit_sha4\n+8366731eeee53787d2bdf8fc1eff7d94757e8da0 $commit_sha3\n+eede89064cd42441590d6afec6c37b321ada3389 $commit_sha2\n+daa55ffad6cb99bf64226532147ffcaf5ce8bdd1 $commit_sha1\n+EOF\n+\n+cat >expect_log_x <<EOF\n+$commit_sha5 5th\n+\n+$commit_sha4 4th\n+Notes on 4th commit\n+\n+$commit_sha3 3rd\n+Notes on 3rd commit\n+\n+$commit_sha2 2nd\n+Notes on 2nd commit\n+\n+$commit_sha1 1st\n+Notes on 1st commit\n+\n+EOF\n+\n+test_expect_success 'verify initial notes (x)' '\n+\tverify_notes x\n+'\n+\n+cp expect_notes_x expect_notes_y\n+cp expect_log_x expect_log_y\n+\n+test_expect_success 'fail to merge empty notes ref into empty notes ref (z => y)' '\n+\ttest_must_fail git -c \"core.notesRef=refs/notes/y\" notes merge z\n+'\n+\n+test_expect_success 'fail to merge into various non-notes refs' '\n+\ttest_must_fail git -c \"core.notesRef=refs/notes\" notes merge x &&\n+\ttest_must_fail git -c \"core.notesRef=refs/notes/\" notes merge x &&\n+\tmkdir -p .git/refs/notes/dir &&\n+\ttest_must_fail git -c \"core.notesRef=refs/notes/dir\" notes merge x &&\n+\ttest_must_fail git -c \"core.notesRef=refs/notes/dir/\" notes merge x &&\n+\ttest_must_fail git -c \"core.notesRef=refs/heads/master\" notes merge x &&\n+\ttest_must_fail git -c \"core.notesRef=refs/notes/y:\" notes merge x &&\n+\ttest_must_fail git -c \"core.notesRef=refs/notes/y:foo\" notes merge x &&\n+\ttest_must_fail git -c \"core.notesRef=refs/notes/foo^{bar\" notes merge x\n+'\n+\n+test_expect_success 'fail to merge various non-note-trees' '\n+\tgit config core.notesRef refs/notes/y &&\n+\ttest_must_fail git notes merge refs/notes &&\n+\ttest_must_fail git notes merge refs/notes/ &&\n+\ttest_must_fail git notes merge refs/notes/dir &&\n+\ttest_must_fail git notes merge refs/notes/dir/ &&\n+\ttest_must_fail git notes merge refs/heads/master &&\n+\ttest_must_fail git notes merge x: &&\n+\ttest_must_fail git notes merge x:foo &&\n+\ttest_must_fail git notes merge foo^{bar\n+'\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+\tverify_notes 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 'merge empty notes ref (z => y)' '\n+\tgit notes merge z &&\n+\t# y should not change (still == 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# Not touching notes to 1st commit\n+\tgit notes remove 2nd &&\n+\tgit notes append -m \"More notes on 3rd commit\" 3rd &&\n+\tgit notes add -f -m \"New notes on 4th commit\" 4th &&\n+\tgit notes add -m \"Notes on 5th commit\" 5th\n+'\n+\n+test_expect_success 'merge previous notes commit (y^ => y) => No-op' '\n+\tpre_state=\"$(git rev-parse refs/notes/y)\" &&\n+\tgit notes merge y^ &&\n+\t# y should not move\n+\ttest \"$pre_state\" = \"$(git rev-parse refs/notes/y)\"\n+'\n+\n+cat <<EOF | sort >expect_notes_y\n+0f2efbd00262f2fd41dfae33df8765618eeacd99 $commit_sha5\n+dec2502dac3ea161543f71930044deff93fa945c $commit_sha4\n+4069cdb399fd45463ec6eef8e051a16a03592d91 $commit_sha3\n+daa55ffad6cb99bf64226532147ffcaf5ce8bdd1 $commit_sha1\n+EOF\n+\n+cat >expect_log_y <<EOF\n+$commit_sha5 5th\n+Notes on 5th commit\n+\n+$commit_sha4 4th\n+New notes on 4th commit\n+\n+$commit_sha3 3rd\n+Notes on 3rd commit\n+\n+More notes on 3rd commit\n+\n+$commit_sha2 2nd\n+\n+$commit_sha1 1st\n+Notes on 1st commit\n+\n+EOF\n+\n+test_expect_success 'verify changed notes on other notes ref (y)' '\n+\tverify_notes y\n+'\n+\n+test_expect_success 'verify unchanged notes on original notes ref (x)' '\n+\tverify_notes x\n+'\n+\n+test_expect_success 'merge original notes (x) into changed notes (y) => No-op' '\n+\tgit notes merge -vvv x &&\n+\tverify_notes y &&\n+\tverify_notes x\n+'\n+\n+cp expect_notes_y expect_notes_x\n+cp expect_log_y expect_log_x\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+\tverify_notes x &&\n+\tverify_notes 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.3.98.g5ad7d9\n"},{"id":"154362","messageId":"1287965333-5099-12-git-send-email-johan@herland.net","threadId":"25543","inReplyTo":"1287965333-5099-1-git-send-email-johan@herland.net","subject":"[PATCHv5 11/23] builtin/notes.c: Refactor creation of notes commits.","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-10-25T00:08:41Z","receivedAt":"2010-10-25T00:08:41Z","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 fbabdc7..97d8baa 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 cd917f9..6ffa6e7 100644\n--- a/notes-merge.c\n+++ b/notes-merge.c\n@@ -1,6 +1,7 @@\n #include \"cache.h\"\n #include \"commit.h\"\n #include \"refs.h\"\n+#include \"notes.h\"\n #include \"notes-merge.h\"\n \n void init_notes_merge_options(struct notes_merge_options *o)\n@@ -17,6 +18,32 @@ void init_notes_merge_options(struct notes_merge_options *o)\n \t\t} \\\n \t} 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 fd572ac..49e1b3a 100644\n--- a/notes-merge.h\n+++ b/notes-merge.h\n@@ -15,6 +15,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.3.98.g5ad7d9\n"},{"id":"154372","messageId":"1287965333-5099-13-git-send-email-johan@herland.net","threadId":"25543","inReplyTo":"1287965333-5099-1-git-send-email-johan@herland.net","subject":"[PATCHv5 12/23] git notes merge: Handle real, non-conflicting notes merges","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-10-25T00:08:42Z","receivedAt":"2010-10-25T00:08:42Z","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\nThis patch has been improved by the following contributions:\n- Jonathan Nieder: Future-proof by always checking add_note() return value\n- Stephen Boyd: Use test_commit\n- Jonathan Nieder: Use trace_printf(...) instead of OUTPUT(o, 5, ...)\n\nThanks-to: Jonathan Nieder <jrnieder@gmail.com>\nThanks-to: Stephen Boyd <bebarino@gmail.com>\nSigned-off-by: Johan Herland <johan@herland.net>\n---\n builtin/notes.c        |   15 ++-\n notes-merge.c          |  325 +++++++++++++++++++++++++++++++++++++++++++++++-\n notes-merge.h          |   15 ++-\n t/t3308-notes-merge.sh |  188 ++++++++++++++++++++++++++++\n 4 files changed, 532 insertions(+), 11 deletions(-)\n\ndiff --git a/builtin/notes.c b/builtin/notes.c\nindex 97d8baa..c967b23 100644\n--- a/builtin/notes.c\n+++ b/builtin/notes.c\n@@ -765,6 +765,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@@ -788,19 +789,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 6ffa6e7..20db0b6 100644\n--- a/notes-merge.c\n+++ b/notes-merge.c\n@@ -1,9 +1,15 @@\n #include \"cache.h\"\n #include \"commit.h\"\n #include \"refs.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@@ -18,6 +24,305 @@ void init_notes_merge_options(struct notes_merge_options *o)\n \t\t} \\\n \t} 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+\ttrace_printf(\"\\tdiff_tree_remote(base = %.7s, remote = %.7s)\\n\",\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\ttrace_printf(\"\\t\\tCannot merge entry '%s' (%c): \"\n+\t\t\t       \"%.7s -> %.7s. Skipping!\\n\", 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\ttrace_printf(\"\\t\\tStored remote change for %s: %.7s -> %.7s\\n\",\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+\ttrace_printf(\"\\tdiff_tree_local(len = %i, base = %.7s, local = %.7s)\\n\",\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\ttrace_printf(\"\\t\\tCannot merge entry '%s' (%c): \"\n+\t\t\t       \"%.7s -> %.7s. Skipping!\\n\", 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\ttrace_printf(\"\\t\\tIgnoring local-only change for %s: \"\n+\t\t\t       \"%.7s -> %.7s\\n\", 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\ttrace_printf(\"\\t\\tStored local change for %s: %.7s -> %.7s\\n\",\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+\ttrace_printf(\"\\tmerge_changes(num_changes = %i)\\n\", *num_changes);\n+\tfor (i = 0; i < *num_changes; i++) {\n+\t\tstruct notes_merge_pair *p = changes + i;\n+\t\ttrace_printf(\"\\t\\t%.7s: %.7s -> %.7s/%.7s\\n\",\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\ttrace_printf(\"\\t\\t\\tskipping (no remote change)\\n\");\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\ttrace_printf(\"\\t\\t\\tskipping (local == remote)\\n\");\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\ttrace_printf(\"\\t\\t\\tno local change, adopted remote\\n\");\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\ttrace_printf(\"\\t\\t\\tneed content-level merge\\n\");\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+\ttrace_printf(\"\\tmerge_from_diffs(base = %.7s, local = %.7s, \"\n+\t       \"remote = %.7s)\\n\", 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@@ -45,15 +350,17 @@ 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(o->local_ref && o->remote_ref);\n+\tassert(!strcmp(o->local_ref, local_tree->ref));\n \thashclr(result_sha1);\n \n \ttrace_printf(\"notes_merge(o->local_ref = %s, o->remote_ref = %s)\\n\",\n@@ -107,14 +414,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@@ -136,8 +446,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 49e1b3a..577cfb3 100644\n--- a/notes-merge.h\n+++ b/notes-merge.h\n@@ -9,6 +9,7 @@ enum notes_merge_verbosity {\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@@ -31,13 +32,20 @@ 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  * Both o->local_ref and o->remote_ref must be given (non-NULL), but either ref\n  * (although not both) may refer to a non-existing notes ref, in which case\n@@ -45,6 +53,7 @@ void create_notes_commit(struct notes_tree *t, struct commit_list *parents,\n  * trivially results in 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 9acb684..24d82b4 100755\n--- a/t/t3308-notes-merge.sh\n+++ b/t/t3308-notes-merge.sh\n@@ -177,4 +177,192 @@ 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+\tverify_notes 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+cat <<EOF | sort >expect_notes_y\n+0f2efbd00262f2fd41dfae33df8765618eeacd99 $commit_sha5\n+dec2502dac3ea161543f71930044deff93fa945c $commit_sha4\n+4069cdb399fd45463ec6eef8e051a16a03592d91 $commit_sha3\n+d000d30e6ddcfce3a8122c403226a2ce2fd04d9d $commit_sha2\n+43add6bd0c8c0bc871ac7991e0f5573cfba27804 $commit_sha1\n+EOF\n+\n+cat >expect_log_y <<EOF\n+$commit_sha5 5th\n+Notes on 5th commit\n+\n+$commit_sha4 4th\n+New notes on 4th commit\n+\n+$commit_sha3 3rd\n+Notes on 3rd commit\n+\n+More notes on 3rd commit\n+\n+$commit_sha2 2nd\n+New notes on 2nd commit\n+\n+$commit_sha1 1st\n+Notes on 1st commit\n+\n+More notes on 1st commit\n+\n+EOF\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\" 1st &&\n+\t# Add new notes to 2nd commit\n+\tgit notes add -m \"New notes on 2nd commit\" 2nd &&\n+\tverify_notes y\n+'\n+\n+cat <<EOF | sort >expect_notes_x\n+0f2efbd00262f2fd41dfae33df8765618eeacd99 $commit_sha5\n+1f257a3a90328557c452f0817d6cc50c89d315d4 $commit_sha4\n+daa55ffad6cb99bf64226532147ffcaf5ce8bdd1 $commit_sha1\n+EOF\n+\n+cat >expect_log_x <<EOF\n+$commit_sha5 5th\n+Notes on 5th commit\n+\n+$commit_sha4 4th\n+New notes on 4th commit\n+\n+More notes on 4th commit\n+\n+$commit_sha3 3rd\n+\n+$commit_sha2 2nd\n+\n+$commit_sha1 1st\n+Notes on 1st commit\n+\n+EOF\n+\n+test_expect_success 'change notes on notes ref (x)' '\n+\tgit config core.notesRef refs/notes/x &&\n+\tgit notes remove 3rd &&\n+\tgit notes append -m \"More notes on 4th commit\" 4th &&\n+\tverify_notes x\n+'\n+\n+cat <<EOF | sort >expect_notes_x\n+0f2efbd00262f2fd41dfae33df8765618eeacd99 $commit_sha5\n+1f257a3a90328557c452f0817d6cc50c89d315d4 $commit_sha4\n+d000d30e6ddcfce3a8122c403226a2ce2fd04d9d $commit_sha2\n+43add6bd0c8c0bc871ac7991e0f5573cfba27804 $commit_sha1\n+EOF\n+\n+cat >expect_log_x <<EOF\n+$commit_sha5 5th\n+Notes on 5th commit\n+\n+$commit_sha4 4th\n+New notes on 4th commit\n+\n+More notes on 4th commit\n+\n+$commit_sha3 3rd\n+\n+$commit_sha2 2nd\n+New notes on 2nd commit\n+\n+$commit_sha1 1st\n+Notes on 1st commit\n+\n+More notes on 1st commit\n+\n+EOF\n+\n+test_expect_success 'merge y into x => Non-conflicting 3-way merge' '\n+\tgit notes merge y &&\n+\tverify_notes x &&\n+\tverify_notes y\n+'\n+\n+cat <<EOF | sort >expect_notes_w\n+05a4927951bcef347f51486575b878b2b60137f2 $commit_sha3\n+d000d30e6ddcfce3a8122c403226a2ce2fd04d9d $commit_sha2\n+EOF\n+\n+cat >expect_log_w <<EOF\n+$commit_sha5 5th\n+\n+$commit_sha4 4th\n+\n+$commit_sha3 3rd\n+New notes on 3rd commit\n+\n+$commit_sha2 2nd\n+New notes on 2nd commit\n+\n+$commit_sha1 1st\n+\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\" 2nd &&\n+\t# Add new note on 3rd commit (non-conflicting)\n+\tgit notes add -m \"New notes on 3rd commit\" 3rd &&\n+\t# Verify state of notes on new, separate notes ref (w)\n+\tverify_notes w\n+'\n+\n+cat <<EOF | sort >expect_notes_x\n+0f2efbd00262f2fd41dfae33df8765618eeacd99 $commit_sha5\n+1f257a3a90328557c452f0817d6cc50c89d315d4 $commit_sha4\n+05a4927951bcef347f51486575b878b2b60137f2 $commit_sha3\n+d000d30e6ddcfce3a8122c403226a2ce2fd04d9d $commit_sha2\n+43add6bd0c8c0bc871ac7991e0f5573cfba27804 $commit_sha1\n+EOF\n+\n+cat >expect_log_x <<EOF\n+$commit_sha5 5th\n+Notes on 5th commit\n+\n+$commit_sha4 4th\n+New notes on 4th commit\n+\n+More notes on 4th commit\n+\n+$commit_sha3 3rd\n+New notes on 3rd commit\n+\n+$commit_sha2 2nd\n+New notes on 2nd commit\n+\n+$commit_sha1 1st\n+Notes on 1st commit\n+\n+More notes on 1st commit\n+\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+\tverify_notes x &&\n+\t# Also verify that nothing changed on other notes refs (y and w)\n+\tverify_notes y &&\n+\tverify_notes w\n+'\n+\n test_done\n-- \n1.7.3.98.g5ad7d9\n"},{"id":"154370","messageId":"1287965333-5099-14-git-send-email-johan@herland.net","threadId":"25543","inReplyTo":"1287965333-5099-1-git-send-email-johan@herland.net","subject":"[PATCHv5 13/23] git notes merge: Add automatic conflict resolvers (ours, theirs, union)","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-10-25T00:08:43Z","receivedAt":"2010-10-25T00:08:43Z","isPatch":false,"sender":{"key":"johan@herland.net","avatar":"https://avatars.githubusercontent.com/u/547031?v=4"},"body":"The new -s/--strategy command-line option to 'git notes merge' allow the user\nto choose how notes merge conflicts should be resolved. There are four valid\nstrategies 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 strategies are implemented using the combine_notes_* functions from the\nnotes.h API.\n\nThe patch also includes testcases verifying the correct implementation of\nthese strategies.\n\nThis patch has been improved by the following contributions:\n- Jonathan Nieder: Future-proof by always checking add_note() return value\n- Stephen Boyd: Use test_commit\n- Stephen Boyd: Use correct option name\n\nThanks-to: Jonathan Nieder <jrnieder@gmail.com>\nThanks-to: Stephen Boyd <bebarino@gmail.com>\nSigned-off-by: Johan Herland <johan@herland.net>\n---\n builtin/notes.c                     |   21 ++-\n notes-merge.c                       |   31 ++-\n notes-merge.h                       |    6 +\n t/t3309-notes-merge-auto-resolve.sh |  502 +++++++++++++++++++++++++++++++++++\n 4 files changed, 558 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 c967b23..a249f19 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] [-s <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@@ -768,8 +768,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 *strategy = NULL;\n \tstruct option options[] = {\n \t\tOPT__VERBOSITY(&verbosity),\n+\t\tOPT_STRING('s', \"strategy\", &strategy, \"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@@ -789,6 +793,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 (strategy) {\n+\t\tif (!strcmp(strategy, \"manual\"))\n+\t\t\to.strategy = NOTES_MERGE_RESOLVE_MANUAL;\n+\t\telse if (!strcmp(strategy, \"ours\"))\n+\t\t\to.strategy = NOTES_MERGE_RESOLVE_OURS;\n+\t\telse if (!strcmp(strategy, \"theirs\"))\n+\t\t\to.strategy = NOTES_MERGE_RESOLVE_THEIRS;\n+\t\telse if (!strcmp(strategy, \"union\"))\n+\t\t\to.strategy = NOTES_MERGE_RESOLVE_UNION;\n+\t\telse {\n+\t\t\terror(\"Unknown -s/--strategy: %s\", strategy);\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 20db0b6..f2fa12d 100644\n--- a/notes-merge.c\n+++ b/notes-merge.c\n@@ -263,6 +263,35 @@ 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->strategy) {\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\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\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 strategy (%i).\", o->strategy);\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@@ -292,7 +321,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\ttrace_printf(\"\\t\\t\\tneed content-level merge\\n\");\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 577cfb3..ed356ae 100644\n--- a/notes-merge.h\n+++ b/notes-merge.h\n@@ -11,6 +11,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} strategy;\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..1a1fc7e\n--- /dev/null\n+++ b/t/t3309-notes-merge-auto-resolve.sh\n@@ -0,0 +1,502 @@\n+#!/bin/sh\n+#\n+# Copyright (c) 2010 Johan Herland\n+#\n+\n+test_description='Test notes merging with auto-resolving strategies'\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+\ttest_commit 1st &&\n+\ttest_commit 2nd &&\n+\ttest_commit 3rd &&\n+\ttest_commit 4th &&\n+\ttest_commit 5th &&\n+\ttest_commit 6th &&\n+\ttest_commit 7th &&\n+\ttest_commit 8th &&\n+\ttest_commit 9th &&\n+\ttest_commit 10th &&\n+\ttest_commit 11th &&\n+\ttest_commit 12th &&\n+\ttest_commit 13th &&\n+\ttest_commit 14th &&\n+\ttest_commit 15th\n+'\n+\n+commit_sha1=$(git rev-parse 1st^{commit})\n+commit_sha2=$(git rev-parse 2nd^{commit})\n+commit_sha3=$(git rev-parse 3rd^{commit})\n+commit_sha4=$(git rev-parse 4th^{commit})\n+commit_sha5=$(git rev-parse 5th^{commit})\n+commit_sha6=$(git rev-parse 6th^{commit})\n+commit_sha7=$(git rev-parse 7th^{commit})\n+commit_sha8=$(git rev-parse 8th^{commit})\n+commit_sha9=$(git rev-parse 9th^{commit})\n+commit_sha10=$(git rev-parse 10th^{commit})\n+commit_sha11=$(git rev-parse 11th^{commit})\n+commit_sha12=$(git rev-parse 12th^{commit})\n+commit_sha13=$(git rev-parse 13th^{commit})\n+commit_sha14=$(git rev-parse 14th^{commit})\n+commit_sha15=$(git rev-parse 15th^{commit})\n+\n+verify_notes () {\n+\tnotes_ref=\"$1\"\n+\tsuffix=\"$2\"\n+\tgit -c core.notesRef=\"refs/notes/$notes_ref\" notes |\n+\t\tsort >\"output_notes_$suffix\" &&\n+\ttest_cmp \"expect_notes_$suffix\" \"output_notes_$suffix\" &&\n+\tgit -c core.notesRef=\"refs/notes/$notes_ref\" log --format=\"%H %s%n%N\" \\\n+\t\t>\"output_log_$suffix\" &&\n+\ttest_cmp \"expect_log_$suffix\" \"output_log_$suffix\"\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 <<EOF | sort >expect_notes_x\n+457a85d6c814ea208550f15fcc48f804ac8dc023 $commit_sha15\n+b0c95b954301d69da2bc3723f4cb1680d355937c $commit_sha14\n+5d30216a129eeffa97d9694ffe8c74317a560315 $commit_sha13\n+dd161bc149470fd890dd4ab52a4cbd79bbd18c36 $commit_sha12\n+7abbc45126d680336fb24294f013a7cdfa3ed545 $commit_sha11\n+b8d03e173f67f6505a76f6e00cf93440200dd9be $commit_sha10\n+20c613c835011c48a5abe29170a2402ca6354910 $commit_sha9\n+a3daf8a1e4e5dc3409a303ad8481d57bfea7f5d6 $commit_sha8\n+897003322b53bc6ca098e9324ee508362347e734 $commit_sha7\n+11d97fdebfa5ceee540a3da07bce6fa0222bc082 $commit_sha6\n+EOF\n+\n+cat >expect_log_x <<EOF\n+$commit_sha15 15th\n+x notes on 15th commit\n+\n+$commit_sha14 14th\n+x notes on 14th commit\n+\n+$commit_sha13 13th\n+x notes on 13th commit\n+\n+$commit_sha12 12th\n+x notes on 12th commit\n+\n+$commit_sha11 11th\n+x notes on 11th commit\n+\n+$commit_sha10 10th\n+x notes on 10th commit\n+\n+$commit_sha9 9th\n+x notes on 9th commit\n+\n+$commit_sha8 8th\n+x notes on 8th commit\n+\n+$commit_sha7 7th\n+x notes on 7th commit\n+\n+$commit_sha6 6th\n+x notes on 6th commit\n+\n+$commit_sha5 5th\n+\n+$commit_sha4 4th\n+\n+$commit_sha3 3rd\n+\n+$commit_sha2 2nd\n+\n+$commit_sha1 1st\n+\n+EOF\n+\n+test_expect_success 'verify state of merge base (x)' 'verify_notes x x'\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 <<EOF | sort >expect_notes_y\n+68b8630d25516028bed862719855b3d6768d7833 $commit_sha15\n+5de7ea7ad4f47e7ff91989fb82234634730f75df $commit_sha14\n+3a631fdb6f41b05b55d8f4baf20728ba8f6fccbc $commit_sha13\n+a66055fa82f7a03fe0c02a6aba3287a85abf7c62 $commit_sha12\n+7abbc45126d680336fb24294f013a7cdfa3ed545 $commit_sha11\n+b8d03e173f67f6505a76f6e00cf93440200dd9be $commit_sha10\n+20c613c835011c48a5abe29170a2402ca6354910 $commit_sha9\n+154508c7a0bcad82b6fe4b472bc4c26b3bf0825b $commit_sha5\n+e2bfd06a37dd2031684a59a6e2b033e212239c78 $commit_sha4\n+5772f42408c0dd6f097a7ca2d24de0e78d1c46b1 $commit_sha3\n+EOF\n+\n+cat >expect_log_y <<EOF\n+$commit_sha15 15th\n+y notes on 15th commit\n+\n+$commit_sha14 14th\n+y notes on 14th commit\n+\n+$commit_sha13 13th\n+y notes on 13th commit\n+\n+$commit_sha12 12th\n+y notes on 12th commit\n+\n+$commit_sha11 11th\n+x notes on 11th commit\n+\n+$commit_sha10 10th\n+x notes on 10th commit\n+\n+$commit_sha9 9th\n+x notes on 9th commit\n+\n+$commit_sha8 8th\n+\n+$commit_sha7 7th\n+\n+$commit_sha6 6th\n+\n+$commit_sha5 5th\n+y notes on 5th commit\n+\n+$commit_sha4 4th\n+y notes on 4th commit\n+\n+$commit_sha3 3rd\n+y notes on 3rd commit\n+\n+$commit_sha2 2nd\n+\n+$commit_sha1 1st\n+\n+EOF\n+\n+test_expect_success 'verify state of local branch (y)' 'verify_notes y y'\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 <<EOF | sort >expect_notes_z\n+9b4b2c61f0615412da3c10f98ff85b57c04ec765 $commit_sha15\n+5de7ea7ad4f47e7ff91989fb82234634730f75df $commit_sha14\n+5d30216a129eeffa97d9694ffe8c74317a560315 $commit_sha13\n+7e3c53503a3db8dd996cb62e37c66e070b44b54d $commit_sha11\n+b8d03e173f67f6505a76f6e00cf93440200dd9be $commit_sha10\n+851e1638784a884c7dd26c5d41f3340f6387413a $commit_sha8\n+897003322b53bc6ca098e9324ee508362347e734 $commit_sha7\n+99fc34adfc400b95c67b013115e37e31aa9a6d23 $commit_sha5\n+e2bfd06a37dd2031684a59a6e2b033e212239c78 $commit_sha4\n+283b48219aee9a4105f6cab337e789065c82c2b9 $commit_sha2\n+EOF\n+\n+cat >expect_log_z <<EOF\n+$commit_sha15 15th\n+z notes on 15th commit\n+\n+$commit_sha14 14th\n+y notes on 14th commit\n+\n+$commit_sha13 13th\n+x notes on 13th commit\n+\n+$commit_sha12 12th\n+\n+$commit_sha11 11th\n+z notes on 11th commit\n+\n+$commit_sha10 10th\n+x notes on 10th commit\n+\n+$commit_sha9 9th\n+\n+$commit_sha8 8th\n+z notes on 8th commit\n+\n+$commit_sha7 7th\n+x notes on 7th commit\n+\n+$commit_sha6 6th\n+\n+$commit_sha5 5th\n+z notes on 5th commit\n+\n+$commit_sha4 4th\n+y notes on 4th commit\n+\n+$commit_sha3 3rd\n+\n+$commit_sha2 2nd\n+z notes on 2nd commit\n+\n+$commit_sha1 1st\n+\n+EOF\n+\n+test_expect_success 'verify state of remote branch (z)' 'verify_notes z z'\n+\n+# At this point, before merging z into y, we have the following status:\n+#\n+# commit | base/x  | local/y | remote/z | diff from x to y/z         | result\n+# -------|---------|---------|----------|----------------------------|-------\n+# 1st    | [none]  | [none]  | [none]   | unchanged / unchanged      | [none]\n+# 2nd    | [none]  | [none]  | 283b482  | unchanged / added          | 283b482\n+# 3rd    | [none]  | 5772f42 | [none]   | added     / unchanged      | 5772f42\n+# 4th    | [none]  | e2bfd06 | e2bfd06  | added     / added (same)   | e2bfd06\n+# 5th    | [none]  | 154508c | 99fc34a  | added     / added (diff)   | ???\n+# 6th    | 11d97fd | [none]  | [none]   | removed   / removed        | [none]\n+# 7th    | 8970033 | [none]  | 8970033  | removed   / unchanged      | [none]\n+# 8th    | a3daf8a | [none]  | 851e163  | removed   / changed        | ???\n+# 9th    | 20c613c | 20c613c | [none]   | unchanged / removed        | [none]\n+# 10th   | b8d03e1 | b8d03e1 | b8d03e1  | unchanged / unchanged      | b8d03e1\n+# 11th   | 7abbc45 | 7abbc45 | 7e3c535  | unchanged / changed        | 7e3c535\n+# 12th   | dd161bc | a66055f | [none]   | changed   / removed        | ???\n+# 13th   | 5d30216 | 3a631fd | 5d30216  | changed   / unchanged      | 3a631fd\n+# 14th   | b0c95b9 | 5de7ea7 | 5de7ea7  | changed   / changed (same) | 5de7ea7\n+# 15th   | 457a85d | 68b8630 | 9b4b2c6  | changed   / changed (diff) | ???\n+\n+test_expect_success 'merge z into y with invalid strategy => Fail/No changes' '\n+\tgit config core.notesRef refs/notes/y &&\n+\ttest_must_fail git notes merge --strategy=foo z &&\n+\t# Verify no changes (y)\n+\tverify_notes y y\n+'\n+\n+cat <<EOF | sort >expect_notes_ours\n+68b8630d25516028bed862719855b3d6768d7833 $commit_sha15\n+5de7ea7ad4f47e7ff91989fb82234634730f75df $commit_sha14\n+3a631fdb6f41b05b55d8f4baf20728ba8f6fccbc $commit_sha13\n+a66055fa82f7a03fe0c02a6aba3287a85abf7c62 $commit_sha12\n+7e3c53503a3db8dd996cb62e37c66e070b44b54d $commit_sha11\n+b8d03e173f67f6505a76f6e00cf93440200dd9be $commit_sha10\n+154508c7a0bcad82b6fe4b472bc4c26b3bf0825b $commit_sha5\n+e2bfd06a37dd2031684a59a6e2b033e212239c78 $commit_sha4\n+5772f42408c0dd6f097a7ca2d24de0e78d1c46b1 $commit_sha3\n+283b48219aee9a4105f6cab337e789065c82c2b9 $commit_sha2\n+EOF\n+\n+cat >expect_log_ours <<EOF\n+$commit_sha15 15th\n+y notes on 15th commit\n+\n+$commit_sha14 14th\n+y notes on 14th commit\n+\n+$commit_sha13 13th\n+y notes on 13th commit\n+\n+$commit_sha12 12th\n+y notes on 12th commit\n+\n+$commit_sha11 11th\n+z notes on 11th commit\n+\n+$commit_sha10 10th\n+x notes on 10th commit\n+\n+$commit_sha9 9th\n+\n+$commit_sha8 8th\n+\n+$commit_sha7 7th\n+\n+$commit_sha6 6th\n+\n+$commit_sha5 5th\n+y notes on 5th commit\n+\n+$commit_sha4 4th\n+y notes on 4th commit\n+\n+$commit_sha3 3rd\n+y notes on 3rd commit\n+\n+$commit_sha2 2nd\n+z notes on 2nd commit\n+\n+$commit_sha1 1st\n+\n+EOF\n+\n+test_expect_success 'merge z into y with \"ours\" strategy => Non-conflicting 3-way merge' '\n+\tgit notes merge --strategy=ours z &&\n+\tverify_notes y 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+\tverify_notes y y\n+'\n+\n+cat <<EOF | sort >expect_notes_theirs\n+9b4b2c61f0615412da3c10f98ff85b57c04ec765 $commit_sha15\n+5de7ea7ad4f47e7ff91989fb82234634730f75df $commit_sha14\n+3a631fdb6f41b05b55d8f4baf20728ba8f6fccbc $commit_sha13\n+7e3c53503a3db8dd996cb62e37c66e070b44b54d $commit_sha11\n+b8d03e173f67f6505a76f6e00cf93440200dd9be $commit_sha10\n+851e1638784a884c7dd26c5d41f3340f6387413a $commit_sha8\n+99fc34adfc400b95c67b013115e37e31aa9a6d23 $commit_sha5\n+e2bfd06a37dd2031684a59a6e2b033e212239c78 $commit_sha4\n+5772f42408c0dd6f097a7ca2d24de0e78d1c46b1 $commit_sha3\n+283b48219aee9a4105f6cab337e789065c82c2b9 $commit_sha2\n+EOF\n+\n+cat >expect_log_theirs <<EOF\n+$commit_sha15 15th\n+z notes on 15th commit\n+\n+$commit_sha14 14th\n+y notes on 14th commit\n+\n+$commit_sha13 13th\n+y notes on 13th commit\n+\n+$commit_sha12 12th\n+\n+$commit_sha11 11th\n+z notes on 11th commit\n+\n+$commit_sha10 10th\n+x notes on 10th commit\n+\n+$commit_sha9 9th\n+\n+$commit_sha8 8th\n+z notes on 8th commit\n+\n+$commit_sha7 7th\n+\n+$commit_sha6 6th\n+\n+$commit_sha5 5th\n+z notes on 5th commit\n+\n+$commit_sha4 4th\n+y notes on 4th commit\n+\n+$commit_sha3 3rd\n+y notes on 3rd commit\n+\n+$commit_sha2 2nd\n+z notes on 2nd commit\n+\n+$commit_sha1 1st\n+\n+EOF\n+\n+test_expect_success 'merge z into y with \"theirs\" strategy => Non-conflicting 3-way merge' '\n+\tgit notes merge --strategy=theirs z &&\n+\tverify_notes y 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+\tverify_notes y y\n+'\n+\n+cat <<EOF | sort >expect_notes_union\n+7c4e546efd0fe939f876beb262ece02797880b54 $commit_sha15\n+5de7ea7ad4f47e7ff91989fb82234634730f75df $commit_sha14\n+3a631fdb6f41b05b55d8f4baf20728ba8f6fccbc $commit_sha13\n+a66055fa82f7a03fe0c02a6aba3287a85abf7c62 $commit_sha12\n+7e3c53503a3db8dd996cb62e37c66e070b44b54d $commit_sha11\n+b8d03e173f67f6505a76f6e00cf93440200dd9be $commit_sha10\n+851e1638784a884c7dd26c5d41f3340f6387413a $commit_sha8\n+6c841cc36ea496027290967ca96bd2bef54dbb47 $commit_sha5\n+e2bfd06a37dd2031684a59a6e2b033e212239c78 $commit_sha4\n+5772f42408c0dd6f097a7ca2d24de0e78d1c46b1 $commit_sha3\n+283b48219aee9a4105f6cab337e789065c82c2b9 $commit_sha2\n+EOF\n+\n+cat >expect_log_union <<EOF\n+$commit_sha15 15th\n+y notes on 15th commit\n+\n+z notes on 15th commit\n+\n+$commit_sha14 14th\n+y notes on 14th commit\n+\n+$commit_sha13 13th\n+y notes on 13th commit\n+\n+$commit_sha12 12th\n+y notes on 12th commit\n+\n+$commit_sha11 11th\n+z notes on 11th commit\n+\n+$commit_sha10 10th\n+x notes on 10th commit\n+\n+$commit_sha9 9th\n+\n+$commit_sha8 8th\n+z notes on 8th commit\n+\n+$commit_sha7 7th\n+\n+$commit_sha6 6th\n+\n+$commit_sha5 5th\n+y notes on 5th commit\n+\n+z notes on 5th commit\n+\n+$commit_sha4 4th\n+y notes on 4th commit\n+\n+$commit_sha3 3rd\n+y notes on 3rd commit\n+\n+$commit_sha2 2nd\n+z notes on 2nd commit\n+\n+$commit_sha1 1st\n+\n+EOF\n+\n+test_expect_success 'merge z into y with \"union\" strategy => Non-conflicting 3-way merge' '\n+\tgit notes merge --strategy=union z &&\n+\tverify_notes y union\n+'\n+\n+test_done\n-- \n1.7.3.98.g5ad7d9\n"},{"id":"154363","messageId":"1287965333-5099-15-git-send-email-johan@herland.net","threadId":"25543","inReplyTo":"1287965333-5099-1-git-send-email-johan@herland.net","subject":"[PATCHv5 14/23] Documentation: Preliminary docs on 'git notes merge'","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-10-25T00:08:44Z","receivedAt":"2010-10-25T00:08:44Z","isPatch":false,"sender":{"key":"johan@herland.net","avatar":"https://avatars.githubusercontent.com/u/547031?v=4"},"body":"This patch has been improved by the following contributions:\n- Stephen Boyd: Use \"automatically resolves\" instead of \"auto-resolves\"\n- Stephen Boyd: Remove unbalanced '('\n\nThanks-to: Stephen Boyd <bebarino@gmail.com>\nSigned-off-by: Johan Herland <johan@herland.net>\n---\n Documentation/git-notes.txt |   44 ++++++++++++++++++++++++++++++++++++++++++-\n 1 files changed, 43 insertions(+), 1 deletions(-)\n\ndiff --git a/Documentation/git-notes.txt b/Documentation/git-notes.txt\nindex 2981d8c..07a5042 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] [-s <strategy> ] <notes_ref>\n 'git notes' remove [<object>]\n 'git notes' prune [-n | -v]\n \n@@ -83,6 +84,16 @@ 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 automatically resolving\n+conflicting notes (see the -s/--strategy option) is not given,\n+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@@ -133,9 +144,23 @@ OPTIONS\n \tDo not remove anything; just report the object names whose notes\n \twould be removed.\n \n+-s <strategy>::\n+--strategy=<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\".\n+\tSee the \"NOTES MERGE STRATEGIES\" section below for more\n+\tinformation on each notes merge strategy.\n+\n+-q::\n+--quiet::\n+\tWhen merging notes, operate quietly.\n+\n -v::\n --verbose::\n-\tReport all object names whose notes are removed.\n+\tWhen merging notes, be more verbose.\n+\tWhen pruning notes, report all object names whose notes are\n+\tremoved.\n \n \n DISCUSSION\n@@ -163,6 +188,23 @@ object, in which case the history of the notes can be read with\n `git log -p -g <refname>`.\n \n \n+NOTES MERGE STRATEGIES\n+----------------------\n+\n+The default notes merge strategy is \"manual\", which is not yet\n+implemented (TODO).\n+\n+\"ours\" automatically resolves conflicting notes in favor of the local\n+version (i.e. the current notes ref).\n+\n+\"theirs\" automatically resolves notes conflicts in favor of the remote\n+version (i.e. the given notes ref being merged into the current notes\n+ref).\n+\n+\"union\" automatically resolves notes conflicts by concatenating the\n+local and remote versions.\n+\n+\n EXAMPLES\n --------\n \n-- \n1.7.3.98.g5ad7d9\n"},{"id":"154364","messageId":"1287965333-5099-16-git-send-email-johan@herland.net","threadId":"25543","inReplyTo":"1287965333-5099-1-git-send-email-johan@herland.net","subject":"[PATCHv5 15/23] git notes merge: Manual conflict resolution, part 1/2","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-10-25T00:08:45Z","receivedAt":"2010-10-25T00:08:45Z","isPatch":false,"sender":{"key":"johan@herland.net","avatar":"https://avatars.githubusercontent.com/u/547031?v=4"},"body":"Conflicts (that are to be resolved manually) are written into a special-\npurpose working tree, located at .git/NOTES_MERGE_WORKTREE. Within this\ndirectory, conflicting notes entries are stored (with conflict markers\nproduced by ll_merge()) using the SHA1 of the annotated object. The\n.git/NOTES_MERGE_WORKTREE directory will only contain the _conflicting_\nnote entries. The non-conflicting note entries (aka. the partial merge\nresult) are stored in 'local_tree', and the SHA1 of the resulting commit\nis written to 'result_sha1'. The return value from notes_merge() is -1.\n\nThe user is told to edit the files within the .git/NOTES_MERGE_WORKTREE\ndirectory in order to resolve the conflicts.\n\nThe patch also contains documentation and testcases for the correct setup\nof .git/NOTES_MERGE_WORKTREE.\n\nThe next part will recombine the partial notes merge result with the\nresolved conflicts in .git/NOTES_MERGE_WORKTREE to produce the complete\nmerge result.\n\nThis patch has been improved by the following contributions:\n- Jonathan Nieder: Use trace_printf(...) instead of OUTPUT(o, 5, ...)\n\nThanks-to: Jonathan Nieder <jrnieder@gmail.com>\nSigned-off-by: Johan Herland <johan@herland.net>\n---\n Documentation/git-notes.txt           |   10 +-\n builtin/notes.c                       |    8 +-\n notes-merge.c                         |  166 ++++++++++++++++++-\n notes-merge.h                         |   11 +-\n t/t3310-notes-merge-manual-resolve.sh |  292 +++++++++++++++++++++++++++++++++\n 5 files changed, 474 insertions(+), 13 deletions(-)\n create mode 100755 t/t3310-notes-merge-manual-resolve.sh\n\ndiff --git a/Documentation/git-notes.txt b/Documentation/git-notes.txt\nindex 07a5042..f003b7c 100644\n--- a/Documentation/git-notes.txt\n+++ b/Documentation/git-notes.txt\n@@ -92,7 +92,9 @@ merge::\n +\n If conflicts arise and a strategy for automatically resolving\n conflicting notes (see the -s/--strategy option) is not given,\n-the merge fails (TODO).\n+the \"manual\" resolver is used. This resolver checks out the\n+conflicting notes in a special worktree (`.git/NOTES_MERGE_WORKTREE`),\n+and instructs the user to manually resolve the conflicts there.\n \n remove::\n \tRemove the notes for a given object (defaults to HEAD).\n@@ -191,8 +193,10 @@ object, in which case the history of the notes can be read with\n NOTES MERGE STRATEGIES\n ----------------------\n \n-The default notes merge strategy is \"manual\", which is not yet\n-implemented (TODO).\n+The default notes merge strategy is \"manual\", which checks out\n+conflicting notes in a special work tree for resolving notes conflicts\n+(`.git/NOTES_MERGE_WORKTREE`), and instructs the user to resolve the\n+conflicts in that work tree.\n \n \"ours\" automatically resolves conflicting notes in favor of the local\n version (i.e. the current notes ref).\ndiff --git a/builtin/notes.c b/builtin/notes.c\nindex a249f19..7a2a288 100644\n--- a/builtin/notes.c\n+++ b/builtin/notes.c\n@@ -820,14 +820,14 @@ static int merge(int argc, const char **argv, const char *prefix)\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-\telse\n-\t\t/* TODO: */\n-\t\tdie(\"'git notes merge' cannot yet handle conflicts!\");\n+\telse /* Merge has unresolved conflicts */\n+\t\tprintf(\"Automatic notes merge failed. Fix conflicts in %s.\\n\",\n+\t\t       git_path(NOTES_MERGE_WORKTREE));\n \n \tfree_notes(t);\n \tstrbuf_release(&remote_ref);\n \tstrbuf_release(&msg);\n-\treturn 0;\n+\treturn result < 0; /* return non-zero on conflicts */\n }\n \n static int remove_cmd(int argc, const char **argv, const char *prefix)\ndiff --git a/notes-merge.c b/notes-merge.c\nindex f2fa12d..5734d0b 100644\n--- a/notes-merge.c\n+++ b/notes-merge.c\n@@ -3,6 +3,9 @@\n #include \"refs.h\"\n #include \"diff.h\"\n #include \"diffcore.h\"\n+#include \"xdiff-interface.h\"\n+#include \"ll-merge.h\"\n+#include \"dir.h\"\n #include \"notes.h\"\n #include \"notes-merge.h\"\n \n@@ -263,16 +266,169 @@ static void diff_tree_local(struct notes_merge_options *o,\n \tdiff_tree_release_paths(&opt);\n }\n \n+static void check_notes_merge_worktree(struct notes_merge_options *o)\n+{\n+\tif (!o->has_worktree) {\n+\t\t/*\n+\t\t * Must establish NOTES_MERGE_WORKTREE.\n+\t\t * Abort if NOTES_MERGE_WORKTREE already exists\n+\t\t */\n+\t\tif (file_exists(git_path(NOTES_MERGE_WORKTREE))) {\n+\t\t\tif (advice_resolve_conflict)\n+\t\t\t\tdie(\"You have not concluded your previous \"\n+\t\t\t\t    \"notes merge (%s exists).\\nPlease, use \"\n+\t\t\t\t    \"'git notes merge --commit' or 'git notes \"\n+\t\t\t\t    \"merge --reset' to commit/abort the \"\n+\t\t\t\t    \"previous merge before you start a new \"\n+\t\t\t\t    \"notes merge.\", git_path(\"NOTES_MERGE_*\"));\n+\t\t\telse\n+\t\t\t\tdie(\"You have not concluded your notes merge \"\n+\t\t\t\t    \"(%s exists).\", git_path(\"NOTES_MERGE_*\"));\n+\t\t}\n+\n+\t\tif (safe_create_leading_directories(git_path(\n+\t\t\t\tNOTES_MERGE_WORKTREE \"/.test\")))\n+\t\t\tdie_errno(\"unable to create directory %s\",\n+\t\t\t\t  git_path(NOTES_MERGE_WORKTREE));\n+\t\to->has_worktree = 1;\n+\t} else if (!file_exists(git_path(NOTES_MERGE_WORKTREE)))\n+\t\t/* NOTES_MERGE_WORKTREE should already be established */\n+\t\tdie(\"missing '%s'. This should not happen\",\n+\t\t    git_path(NOTES_MERGE_WORKTREE));\n+}\n+\n+static void write_buf_to_worktree(const unsigned char *obj,\n+\t\t\t\t  const char *buf, unsigned long size)\n+{\n+\tint fd;\n+\tchar *path = git_path(NOTES_MERGE_WORKTREE \"/%s\", sha1_to_hex(obj));\n+\tif (safe_create_leading_directories(path))\n+\t\tdie_errno(\"unable to create directory for '%s'\", path);\n+\tif (file_exists(path))\n+\t\tdie(\"found existing file at '%s'\", path);\n+\n+\tfd = open(path, O_WRONLY | O_TRUNC | O_CREAT, 0666);\n+\tif (fd < 0)\n+\t\tdie_errno(\"failed to open '%s'\", path);\n+\n+\twhile (size > 0) {\n+\t\tlong ret = write_in_full(fd, buf, size);\n+\t\tif (ret < 0) {\n+\t\t\t/* Ignore epipe */\n+\t\t\tif (errno == EPIPE)\n+\t\t\t\tbreak;\n+\t\t\tdie_errno(\"notes-merge\");\n+\t\t} else if (!ret) {\n+\t\t\tdie(\"notes-merge: disk full?\");\n+\t\t}\n+\t\tsize -= ret;\n+\t\tbuf += ret;\n+\t}\n+\n+\tclose(fd);\n+}\n+\n+static void write_note_to_worktree(const unsigned char *obj,\n+\t\t\t\t   const unsigned char *note)\n+{\n+\tenum object_type type;\n+\tunsigned long size;\n+\tvoid *buf = read_sha1_file(note, &type, &size);\n+\n+\tif (!buf)\n+\t\tdie(\"cannot read note %s for object %s\",\n+\t\t    sha1_to_hex(note), sha1_to_hex(obj));\n+\tif (type != OBJ_BLOB)\n+\t\tdie(\"blob expected in note %s for object %s\",\n+\t\t    sha1_to_hex(note), sha1_to_hex(obj));\n+\twrite_buf_to_worktree(obj, buf, size);\n+\tfree(buf);\n+}\n+\n+static int ll_merge_in_worktree(struct notes_merge_options *o,\n+\t\t\t\tstruct notes_merge_pair *p)\n+{\n+\tmmbuffer_t result_buf;\n+\tmmfile_t base, local, remote;\n+\tint status;\n+\n+\tread_mmblob(&base, p->base);\n+\tread_mmblob(&local, p->local);\n+\tread_mmblob(&remote, p->remote);\n+\n+\tstatus = ll_merge(&result_buf, sha1_to_hex(p->obj), &base, NULL,\n+\t\t\t  &local, o->local_ref, &remote, o->remote_ref, 0);\n+\n+\tfree(base.ptr);\n+\tfree(local.ptr);\n+\tfree(remote.ptr);\n+\n+\tif ((status < 0) || !result_buf.ptr)\n+\t\tdie(\"Failed to execute internal merge\");\n+\n+\twrite_buf_to_worktree(p->obj, result_buf.ptr, result_buf.size);\n+\tfree(result_buf.ptr);\n+\n+\treturn status;\n+}\n+\n+static int merge_one_change_manual(struct notes_merge_options *o,\n+\t\t\t\t   struct notes_merge_pair *p,\n+\t\t\t\t   struct notes_tree *t)\n+{\n+\tconst char *lref = o->local_ref ? o->local_ref : \"local version\";\n+\tconst char *rref = o->remote_ref ? o->remote_ref : \"remote version\";\n+\n+\ttrace_printf(\"\\t\\t\\tmerge_one_change_manual(obj = %.7s, base = %.7s, \"\n+\t       \"local = %.7s, remote = %.7s)\\n\",\n+\t       sha1_to_hex(p->obj), sha1_to_hex(p->base),\n+\t       sha1_to_hex(p->local), sha1_to_hex(p->remote));\n+\n+\tOUTPUT(o, 2, \"Auto-merging notes for %s\", sha1_to_hex(p->obj));\n+\tcheck_notes_merge_worktree(o);\n+\tif (is_null_sha1(p->local)) {\n+\t\t/* D/F conflict, checkout p->remote */\n+\t\tassert(!is_null_sha1(p->remote));\n+\t\tOUTPUT(o, 1, \"CONFLICT (delete/modify): Notes for object %s \"\n+\t\t       \"deleted in %s and modified in %s. Version from %s \"\n+\t\t       \"left in tree.\", sha1_to_hex(p->obj), lref, rref, rref);\n+\t\twrite_note_to_worktree(p->obj, p->remote);\n+\t} else if (is_null_sha1(p->remote)) {\n+\t\t/* D/F conflict, checkout p->local */\n+\t\tassert(!is_null_sha1(p->local));\n+\t\tOUTPUT(o, 1, \"CONFLICT (delete/modify): Notes for object %s \"\n+\t\t       \"deleted in %s and modified in %s. Version from %s \"\n+\t\t       \"left in tree.\", sha1_to_hex(p->obj), rref, lref, lref);\n+\t\twrite_note_to_worktree(p->obj, p->local);\n+\t} else {\n+\t\t/* \"regular\" conflict, checkout result of ll_merge() */\n+\t\tconst char *reason = \"content\";\n+\t\tif (is_null_sha1(p->base))\n+\t\t\treason = \"add/add\";\n+\t\tassert(!is_null_sha1(p->local));\n+\t\tassert(!is_null_sha1(p->remote));\n+\t\tOUTPUT(o, 1, \"CONFLICT (%s): Merge conflict in notes for \"\n+\t\t       \"object %s\", reason, sha1_to_hex(p->obj));\n+\t\tll_merge_in_worktree(o, p);\n+\t}\n+\n+\ttrace_printf(\"\\t\\t\\tremoving from partial merge result\\n\");\n+\tremove_note(t, p->obj);\n+\n+\treturn 1;\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 * Return 0 if change is successfully resolved (stored in notes_tree).\n+\t * Return 1 is change results in a conflict (NOT stored in notes_tree,\n+\t * but instead written to NOTES_MERGE_WORKTREE with conflict markers).\n \t */\n \tswitch (o->strategy) {\n \tcase NOTES_MERGE_RESOLVE_MANUAL:\n-\t\treturn 1;\n+\t\treturn merge_one_change_manual(o, p, t);\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@@ -478,8 +634,8 @@ int notes_merge(struct notes_merge_options *o,\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+\tif (result != 0) { /* non-trivial merge (with or without conflicts) */\n+\t\t/* Commit (partial) result */\n \t\tstruct commit_list *parents = NULL;\n \t\tcommit_list_insert(remote, &parents); /* LIFO order */\n \t\tcommit_list_insert(local, &parents);\ndiff --git a/notes-merge.h b/notes-merge.h\nindex ed356ae..55ef3d9 100644\n--- a/notes-merge.h\n+++ b/notes-merge.h\n@@ -1,6 +1,8 @@\n #ifndef NOTES_MERGE_H\n #define NOTES_MERGE_H\n \n+#define NOTES_MERGE_WORKTREE \"NOTES_MERGE_WORKTREE\"\n+\n enum notes_merge_verbosity {\n \tNOTES_MERGE_VERBOSITY_DEFAULT = 2,\n \tNOTES_MERGE_VERBOSITY_MAX = 5\n@@ -17,6 +19,7 @@ struct notes_merge_options {\n \t\tNOTES_MERGE_RESOLVE_THEIRS,\n \t\tNOTES_MERGE_RESOLVE_UNION\n \t} strategy;\n+\tunsigned has_worktree:1;\n };\n \n void init_notes_merge_options(struct notes_merge_options *o);\n@@ -51,7 +54,13 @@ void create_notes_commit(struct notes_tree *t, struct commit_list *parents,\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+ * 3. The merge results in conflicts. This is similar to #2 in that the\n+ *    partial merge result (i.e. merge result minus the unmerged entries)\n+ *    are stored in 'local_tree', and the SHA1 or the resulting commit\n+ *    (to be amended when the conflicts have been resolved) is written into\n+ *    'result_sha1'. The unmerged entries are written into the\n+ *    .git/NOTES_MERGE_WORKTREE directory with conflict markers.\n+ *    -1 is returned.\n  *\n  * Both o->local_ref and o->remote_ref must be given (non-NULL), but either ref\n  * (although not both) may refer to a non-existing notes ref, in which case\ndiff --git a/t/t3310-notes-merge-manual-resolve.sh b/t/t3310-notes-merge-manual-resolve.sh\nnew file mode 100755\nindex 0000000..eadb6b7\n--- /dev/null\n+++ b/t/t3310-notes-merge-manual-resolve.sh\n@@ -0,0 +1,292 @@\n+#!/bin/sh\n+#\n+# Copyright (c) 2010 Johan Herland\n+#\n+\n+test_description='Test notes merging with manual conflict resolution'\n+\n+. ./test-lib.sh\n+\n+# Set up a notes merge scenario with different kinds of conflicts\n+test_expect_success 'setup commits' '\n+\ttest_commit 1st &&\n+\ttest_commit 2nd &&\n+\ttest_commit 3rd &&\n+\ttest_commit 4th &&\n+\ttest_commit 5th\n+'\n+\n+commit_sha1=$(git rev-parse 1st^{commit})\n+commit_sha2=$(git rev-parse 2nd^{commit})\n+commit_sha3=$(git rev-parse 3rd^{commit})\n+commit_sha4=$(git rev-parse 4th^{commit})\n+commit_sha5=$(git rev-parse 5th^{commit})\n+\n+verify_notes () {\n+\tnotes_ref=\"$1\"\n+\tgit -c core.notesRef=\"refs/notes/$notes_ref\" notes |\n+\t\tsort >\"output_notes_$notes_ref\" &&\n+\ttest_cmp \"expect_notes_$notes_ref\" \"output_notes_$notes_ref\" &&\n+\tgit -c core.notesRef=\"refs/notes/$notes_ref\" log --format=\"%H %s%n%N\" \\\n+\t\t>\"output_log_$notes_ref\" &&\n+\ttest_cmp \"expect_log_$notes_ref\" \"output_log_$notes_ref\"\n+}\n+\n+cat <<EOF | sort >expect_notes_x\n+6e8e3febca3c2bb896704335cc4d0c34cb2f8715 $commit_sha4\n+e5388c10860456ee60673025345fe2e153eb8cf8 $commit_sha3\n+ceefa674873670e7ecd131814d909723cce2b669 $commit_sha2\n+EOF\n+\n+cat >expect_log_x <<EOF\n+$commit_sha5 5th\n+\n+$commit_sha4 4th\n+x notes on 4th commit\n+\n+$commit_sha3 3rd\n+x notes on 3rd commit\n+\n+$commit_sha2 2nd\n+x notes on 2nd commit\n+\n+$commit_sha1 1st\n+\n+EOF\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 2nd commit\" 2nd &&\n+\tgit notes add -m \"x notes on 3rd commit\" 3rd &&\n+\tgit notes add -m \"x notes on 4th commit\" 4th &&\n+\tverify_notes x\n+'\n+\n+cat <<EOF | sort >expect_notes_y\n+e2bfd06a37dd2031684a59a6e2b033e212239c78 $commit_sha4\n+5772f42408c0dd6f097a7ca2d24de0e78d1c46b1 $commit_sha3\n+b0a6021ec006d07e80e9b20ec9b444cbd9d560d3 $commit_sha1\n+EOF\n+\n+cat >expect_log_y <<EOF\n+$commit_sha5 5th\n+\n+$commit_sha4 4th\n+y notes on 4th commit\n+\n+$commit_sha3 3rd\n+y notes on 3rd commit\n+\n+$commit_sha2 2nd\n+\n+$commit_sha1 1st\n+y notes on 1st commit\n+\n+EOF\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 1st commit\" 1st &&\n+\tgit notes remove 2nd &&\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+\tverify_notes y\n+'\n+\n+cat <<EOF | sort >expect_notes_z\n+cff59c793c20bb49a4e01bc06fb06bad642e0d54 $commit_sha4\n+283b48219aee9a4105f6cab337e789065c82c2b9 $commit_sha2\n+0a81da8956346e19bcb27a906f04af327e03e31b $commit_sha1\n+EOF\n+\n+cat >expect_log_z <<EOF\n+$commit_sha5 5th\n+\n+$commit_sha4 4th\n+z notes on 4th commit\n+\n+$commit_sha3 3rd\n+\n+$commit_sha2 2nd\n+z notes on 2nd commit\n+\n+$commit_sha1 1st\n+z notes on 1st commit\n+\n+EOF\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 1st commit\" 1st &&\n+\tgit notes add -f -m \"z notes on 2nd commit\" 2nd &&\n+\tgit notes remove 3rd &&\n+\tgit notes add -f -m \"z notes on 4th commit\" 4th &&\n+\tverify_notes z\n+'\n+\n+# At this point, before merging z into y, we have the following status:\n+#\n+# commit | base/x  | local/y | remote/z | diff from x to y/z\n+# -------|---------|---------|----------|---------------------------\n+# 1st    | [none]  | b0a6021 | 0a81da8  | added     / added (diff)\n+# 2nd    | ceefa67 | [none]  | 283b482  | removed   / changed\n+# 3rd    | e5388c1 | 5772f42 | [none]   | changed   / removed\n+# 4th    | 6e8e3fe | e2bfd06 | cff59c7  | changed   / changed (diff)\n+# 5th    | [none]  | [none]  | [none]   | [none]\n+\n+cat <<EOF | sort >expect_conflicts\n+$commit_sha1\n+$commit_sha2\n+$commit_sha3\n+$commit_sha4\n+EOF\n+\n+cat >expect_conflict_$commit_sha1 <<EOF\n+<<<<<<< refs/notes/m\n+y notes on 1st commit\n+=======\n+z notes on 1st commit\n+>>>>>>> refs/notes/z\n+EOF\n+\n+cat >expect_conflict_$commit_sha2 <<EOF\n+z notes on 2nd commit\n+EOF\n+\n+cat >expect_conflict_$commit_sha3 <<EOF\n+y notes on 3rd commit\n+EOF\n+\n+cat >expect_conflict_$commit_sha4 <<EOF\n+<<<<<<< refs/notes/m\n+y notes on 4th commit\n+=======\n+z notes on 4th commit\n+>>>>>>> refs/notes/z\n+EOF\n+\n+cp expect_notes_y expect_notes_m\n+cp expect_log_y expect_log_m\n+\n+git rev-parse refs/notes/y > pre_merge_y\n+\n+test_expect_success 'merge z into m (== y) with default (\"manual\") resolver => Conflicting 3-way merge' '\n+\tgit update-ref refs/notes/m refs/notes/y &&\n+\tgit config core.notesRef refs/notes/m &&\n+\ttest_must_fail git notes merge z >output &&\n+\t# Output should point to where to resolve conflicts\n+\tgrep -q \"\\\\.git/NOTES_MERGE_WORKTREE\" output &&\n+\t# Inspect merge conflicts\n+\tls .git/NOTES_MERGE_WORKTREE >output_conflicts &&\n+\ttest_cmp expect_conflicts output_conflicts &&\n+\t( for f in $(cat expect_conflicts); do\n+\t\ttest_cmp \"expect_conflict_$f\" \".git/NOTES_MERGE_WORKTREE/$f\" ||\n+\t\texit 1\n+\tdone ) &&\n+\t# Verify that current notes tree (pre-merge) has not changed (m == y)\n+\tverify_notes y &&\n+\tverify_notes m &&\n+\ttest \"$(git rev-parse refs/notes/m)\" = \"$(cat pre_merge_y)\"\n+'\n+\n+cat <<EOF | sort >expect_notes_z\n+00494adecf2d9635a02fa431308d67993f853968 $commit_sha4\n+283b48219aee9a4105f6cab337e789065c82c2b9 $commit_sha2\n+0a81da8956346e19bcb27a906f04af327e03e31b $commit_sha1\n+EOF\n+\n+cat >expect_log_z <<EOF\n+$commit_sha5 5th\n+\n+$commit_sha4 4th\n+z notes on 4th commit\n+\n+More z notes on 4th commit\n+\n+$commit_sha3 3rd\n+\n+$commit_sha2 2nd\n+z notes on 2nd commit\n+\n+$commit_sha1 1st\n+z notes on 1st commit\n+\n+EOF\n+\n+test_expect_success 'change notes in z' '\n+\tgit notes --ref z append -m \"More z notes on 4th commit\" 4th &&\n+\tverify_notes z\n+'\n+\n+test_expect_success 'cannot do merge w/conflicts when previous merge is unfinished' '\n+\ttest -d .git/NOTES_MERGE_WORKTREE &&\n+\ttest_must_fail git notes merge z >output 2>&1 &&\n+\t# Output should indicate what is wrong\n+\tgrep -q \"\\\\.git/NOTES_MERGE_\\\\* exists\" output\n+'\n+\n+# Setup non-conflicting merge between x and new notes ref w\n+\n+cat <<EOF | sort >expect_notes_w\n+ceefa674873670e7ecd131814d909723cce2b669 $commit_sha2\n+f75d1df88cbfe4258d49852f26cfc83f2ad4494b $commit_sha1\n+EOF\n+\n+cat >expect_log_w <<EOF\n+$commit_sha5 5th\n+\n+$commit_sha4 4th\n+\n+$commit_sha3 3rd\n+\n+$commit_sha2 2nd\n+x notes on 2nd commit\n+\n+$commit_sha1 1st\n+w notes on 1st commit\n+\n+EOF\n+\n+test_expect_success 'setup unrelated notes ref (w)' '\n+\tgit config core.notesRef refs/notes/w &&\n+\tgit notes add -m \"w notes on 1st commit\" 1st &&\n+\tgit notes add -m \"x notes on 2nd commit\" 2nd &&\n+\tverify_notes w\n+'\n+\n+cat <<EOF | sort >expect_notes_w\n+6e8e3febca3c2bb896704335cc4d0c34cb2f8715 $commit_sha4\n+e5388c10860456ee60673025345fe2e153eb8cf8 $commit_sha3\n+ceefa674873670e7ecd131814d909723cce2b669 $commit_sha2\n+f75d1df88cbfe4258d49852f26cfc83f2ad4494b $commit_sha1\n+EOF\n+\n+cat >expect_log_w <<EOF\n+$commit_sha5 5th\n+\n+$commit_sha4 4th\n+x notes on 4th commit\n+\n+$commit_sha3 3rd\n+x notes on 3rd commit\n+\n+$commit_sha2 2nd\n+x notes on 2nd commit\n+\n+$commit_sha1 1st\n+w notes on 1st commit\n+\n+EOF\n+\n+test_expect_success 'can do merge without conflicts even if previous merge is unfinished (x => w)' '\n+\ttest -d .git/NOTES_MERGE_WORKTREE &&\n+\tgit notes merge x &&\n+\tverify_notes w &&\n+\t# Verify that other notes refs has not changed (x and y)\n+\tverify_notes x &&\n+\tverify_notes y\n+'\n+\n+test_done\n-- \n1.7.3.98.g5ad7d9\n"},{"id":"154373","messageId":"1287965333-5099-17-git-send-email-johan@herland.net","threadId":"25543","inReplyTo":"1287965333-5099-1-git-send-email-johan@herland.net","subject":"[PATCHv5 16/23] git notes merge: Manual conflict resolution, part 2/2","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-10-25T00:08:46Z","receivedAt":"2010-10-25T00:08:46Z","isPatch":false,"sender":{"key":"johan@herland.net","avatar":"https://avatars.githubusercontent.com/u/547031?v=4"},"body":"When the notes merge conflicts in .git/NOTES_MERGE_WORKTREE have been\nresolved, we need to record a new notes commit on the appropriate notes\nref with the resolved notes.\n\nThis patch implements 'git notes merge --commit' which the user should\nrun after resolving conflicts in the notes merge worktree. This command\nfinalizes the notes merge by recombining the partial notes tree from\npart 1 with the now-resolved conflicts in the notes merge worktree in a\nmerge commit, and updating the appropriate ref to this merge commit.\n\nIn order to correctly finalize the merge, we need to keep track of three\nthings:\n\n- The partial merge result from part 1, containing the auto-merged notes.\n  This is now stored into a ref called .git/NOTES_MERGE_PARTIAL.\n- The unmerged notes. These are already stored in\n  .git/NOTES_MERGE_WORKTREE, thanks to part 1.\n- The notes ref to be updated by the finalized merge result. This is now\n  stored in a symref called .git/NOTES_MERGE_REF.\n\nIn addition to \"git notes merge --commit\", which uses the above details\nto create the finalized notes merge commit, this patch also implements\n\"git notes merge --reset\", which aborts the ongoing notes merge by simply\nremoving the files/directory described above.\n\nFTR, \"git notes merge --commit\" reuses \"git notes merge --reset\" to remove\nthe information described above (.git/NOTES_MERGE_*) after the notes merge\nhave been successfully finalized.\n\nThe patch also contains documentation and testcases for the two new options.\n\nThis patch has been improved by the following contributions:\n- Ævar Arnfjörð Bjarmason: Fix nonsense sentence in --commit description\n- Sverre Rabbelier: Rename --reset to --abort\n\nThanks-to: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\nThanks-to: Sverre Rabbelier <srabbelier@gmail.com>\nSigned-off-by: Johan Herland <johan@herland.net>\n---\n Documentation/git-notes.txt           |   22 ++++\n builtin/notes.c                       |  106 +++++++++++++++++++-\n notes-merge.c                         |   71 +++++++++++++-\n notes-merge.h                         |   23 +++++\n t/t3310-notes-merge-manual-resolve.sh |  176 +++++++++++++++++++++++++++++++++\n 5 files changed, 394 insertions(+), 4 deletions(-)\n\ndiff --git a/Documentation/git-notes.txt b/Documentation/git-notes.txt\nindex f003b7c..a822551 100644\n--- a/Documentation/git-notes.txt\n+++ b/Documentation/git-notes.txt\n@@ -15,6 +15,8 @@ SYNOPSIS\n 'git notes' edit [<object>]\n 'git notes' show [<object>]\n 'git notes' merge [-v | -q] [-s <strategy> ] <notes_ref>\n+'git notes' merge --commit [-v | -q]\n+'git notes' merge --abort [-v | -q]\n 'git notes' remove [<object>]\n 'git notes' prune [-n | -v]\n \n@@ -95,6 +97,9 @@ conflicting notes (see the -s/--strategy option) is not given,\n the \"manual\" resolver is used. This resolver checks out the\n conflicting notes in a special worktree (`.git/NOTES_MERGE_WORKTREE`),\n and instructs the user to manually resolve the conflicts there.\n+When done, the user can either finalize the merge with\n+'git notes merge --commit', or abort the merge with\n+'git notes merge --abort'.\n \n remove::\n \tRemove the notes for a given object (defaults to HEAD).\n@@ -154,6 +159,20 @@ OPTIONS\n \tSee the \"NOTES MERGE STRATEGIES\" section below for more\n \tinformation on each notes merge strategy.\n \n+--commit::\n+\tFinalize an in-progress 'git notes merge'. Use this option\n+\twhen you have resolved the conflicts that 'git notes merge'\n+\tstored in .git/NOTES_MERGE_WORKTREE. This amends the partial\n+\tmerge commit created by 'git notes merge' (stored in\n+\t.git/NOTES_MERGE_PARTIAL) by adding the notes in\n+\t.git/NOTES_MERGE_WORKTREE. The notes ref stored in the\n+\t.git/NOTES_MERGE_REF symref is updated to the resulting commit.\n+\n+--abort::\n+\tAbort/reset a in-progress 'git notes merge', i.e. a notes merge\n+\twith conflicts. This simply removes all files related to the\n+\tnotes merge.\n+\n -q::\n --quiet::\n \tWhen merging notes, operate quietly.\n@@ -197,6 +216,9 @@ The default notes merge strategy is \"manual\", which checks out\n conflicting notes in a special work tree for resolving notes conflicts\n (`.git/NOTES_MERGE_WORKTREE`), and instructs the user to resolve the\n conflicts in that work tree.\n+When done, the user can either finalize the merge with\n+'git notes merge --commit', or abort the merge with\n+'git notes merge --abort'.\n \n \"ours\" automatically resolves conflicting notes in favor of the local\n version (i.e. the current notes ref).\ndiff --git a/builtin/notes.c b/builtin/notes.c\nindex 7a2a288..db60ead 100644\n--- a/builtin/notes.c\n+++ b/builtin/notes.c\n@@ -27,6 +27,8 @@ static const char * const git_notes_usage[] = {\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] [-s <strategy> ] <notes_ref>\",\n+\t\"git notes merge --commit [-v | -q]\",\n+\t\"git notes merge --abort [-v | -q]\",\n \t\"git notes [--ref <notes_ref>] remove [<object>]\",\n \t\"git notes [--ref <notes_ref>] prune [-n | -v]\",\n \tNULL\n@@ -65,6 +67,8 @@ static const char * const git_notes_show_usage[] = {\n \n static const char * const git_notes_merge_usage[] = {\n \t\"git notes merge [<options>] <notes_ref>\",\n+\t\"git notes merge --commit [<options>]\",\n+\t\"git notes merge --abort [<options>]\",\n \tNULL\n };\n \n@@ -761,33 +765,119 @@ static int show(int argc, const char **argv, const char *prefix)\n \treturn retval;\n }\n \n+static int merge_abort(struct notes_merge_options *o)\n+{\n+\tint ret = 0;\n+\n+\t/*\n+\t * Remove .git/NOTES_MERGE_PARTIAL and .git/NOTES_MERGE_REF, and call\n+\t * notes_merge_abort() to remove .git/NOTES_MERGE_WORKTREE.\n+\t */\n+\n+\tif (delete_ref(\"NOTES_MERGE_PARTIAL\", NULL, 0))\n+\t\tret += error(\"Failed to delete ref NOTES_MERGE_PARTIAL\");\n+\tif (delete_ref(\"NOTES_MERGE_REF\", NULL, REF_NODEREF))\n+\t\tret += error(\"Failed to delete ref NOTES_MERGE_REF\");\n+\tif (notes_merge_abort(o))\n+\t\tret += error(\"Failed to remove 'git notes merge' worktree\");\n+\treturn ret;\n+}\n+\n+static int merge_commit(struct notes_merge_options *o)\n+{\n+\tstruct strbuf msg = STRBUF_INIT;\n+\tunsigned char sha1[20];\n+\tstruct notes_tree *t;\n+\tstruct commit *partial;\n+\tstruct pretty_print_context pretty_ctx;\n+\n+\t/*\n+\t * Read partial merge result from .git/NOTES_MERGE_PARTIAL,\n+\t * and target notes ref from .git/NOTES_MERGE_REF.\n+\t */\n+\n+\tif (get_sha1(\"NOTES_MERGE_PARTIAL\", sha1))\n+\t\tdie(\"Failed to read ref NOTES_MERGE_PARTIAL\");\n+\telse if (!(partial = lookup_commit_reference(sha1)))\n+\t\tdie(\"Could not find commit from NOTES_MERGE_PARTIAL.\");\n+\telse if (parse_commit(partial))\n+\t\tdie(\"Could not parse commit from NOTES_MERGE_PARTIAL.\");\n+\n+\tt = xcalloc(1, sizeof(struct notes_tree));\n+\tinit_notes(t, \"NOTES_MERGE_PARTIAL\", combine_notes_overwrite, 0);\n+\n+\to->local_ref = resolve_ref(\"NOTES_MERGE_REF\", sha1, 0, 0);\n+\tif (!o->local_ref)\n+\t\tdie(\"Failed to resolve NOTES_MERGE_REF\");\n+\n+\tif (notes_merge_commit(o, t, partial, sha1))\n+\t\tdie(\"Failed to finalize notes merge\");\n+\n+\t/* Reuse existing commit message in reflog message */\n+\tmemset(&pretty_ctx, 0, sizeof(pretty_ctx));\n+\tformat_commit_message(partial, \"%s\", &msg, &pretty_ctx);\n+\tstrbuf_trim(&msg);\n+\tstrbuf_insert(&msg, 0, \"notes: \", 7);\n+\tupdate_ref(msg.buf, o->local_ref, sha1, NULL, 0, DIE_ON_ERR);\n+\n+\tfree_notes(t);\n+\tstrbuf_release(&msg);\n+\treturn merge_abort(o);\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_tree *t;\n \tstruct notes_merge_options o;\n+\tint do_merge = 0, do_commit = 0, do_abort = 0;\n \tint verbosity = 0, result;\n \tconst char *strategy = NULL;\n \tstruct option options[] = {\n+\t\tOPT_GROUP(\"General options\"),\n \t\tOPT__VERBOSITY(&verbosity),\n+\t\tOPT_GROUP(\"Merge options\"),\n \t\tOPT_STRING('s', \"strategy\", &strategy, \"strategy\",\n \t\t\t   \"resolve notes conflicts using the given \"\n \t\t\t   \"strategy (manual/ours/theirs/union)\"),\n+\t\tOPT_GROUP(\"Committing unmerged notes\"),\n+\t\t{ OPTION_BOOLEAN, 0, \"commit\", &do_commit, NULL,\n+\t\t\t\"finalize notes merge by committing unmerged notes\",\n+\t\t\tPARSE_OPT_NOARG | PARSE_OPT_NONEG },\n+\t\tOPT_GROUP(\"Aborting notes merge resolution\"),\n+\t\t{ OPTION_BOOLEAN, 0, \"abort\", &do_abort, NULL,\n+\t\t\t\"abort notes merge\",\n+\t\t\tPARSE_OPT_NOARG | PARSE_OPT_NONEG },\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 (argc != 1) {\n+\tif (strategy || do_commit + do_abort == 0)\n+\t\tdo_merge = 1;\n+\tif (do_merge + do_commit + do_abort != 1) {\n+\t\terror(\"cannot mix --commit, --abort or -s/--strategy\");\n+\t\tusage_with_options(git_notes_merge_usage, options);\n+\t}\n+\n+\tif (do_merge && argc != 1) {\n \t\terror(\"Must specify a notes ref to merge\");\n \t\tusage_with_options(git_notes_merge_usage, options);\n+\t} else if (!do_merge && argc) {\n+\t\terror(\"too many parameters\");\n+\t\tusage_with_options(git_notes_merge_usage, options);\n \t}\n \n \tinit_notes_merge_options(&o);\n \to.verbosity = verbosity + NOTES_MERGE_VERBOSITY_DEFAULT;\n \n+\tif (do_abort)\n+\t\treturn merge_abort(&o);\n+\tif (do_commit)\n+\t\treturn merge_commit(&o);\n+\n \to.local_ref = default_notes_ref();\n \tstrbuf_addstr(&remote_ref, argv[0]);\n \texpand_notes_ref(&remote_ref);\n@@ -820,9 +910,19 @@ static int merge(int argc, const char **argv, const char *prefix)\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-\telse /* Merge has unresolved conflicts */\n-\t\tprintf(\"Automatic notes merge failed. Fix conflicts in %s.\\n\",\n+\telse { /* Merge has unresolved conflicts */\n+\t\t/* Update .git/NOTES_MERGE_PARTIAL with partial merge result */\n+\t\tupdate_ref(msg.buf, \"NOTES_MERGE_PARTIAL\", result_sha1, NULL,\n+\t\t\t   0, DIE_ON_ERR);\n+\t\t/* Store ref-to-be-updated into .git/NOTES_MERGE_REF */\n+\t\tif (create_symref(\"NOTES_MERGE_REF\", default_notes_ref(), NULL))\n+\t\t\tdie(\"Failed to store link to current notes ref (%s)\",\n+\t\t\t    default_notes_ref());\n+\t\tprintf(\"Automatic notes merge failed. Fix conflicts in %s and \"\n+\t\t       \"commit the result with 'git notes merge --commit', or \"\n+\t\t       \"abort the merge with 'git notes merge --abort'.\\n\",\n \t\t       git_path(NOTES_MERGE_WORKTREE));\n+\t}\n \n \tfree_notes(t);\n \tstrbuf_release(&remote_ref);\ndiff --git a/notes-merge.c b/notes-merge.c\nindex 5734d0b..b44273c 100644\n--- a/notes-merge.c\n+++ b/notes-merge.c\n@@ -278,7 +278,7 @@ static void check_notes_merge_worktree(struct notes_merge_options *o)\n \t\t\t\tdie(\"You have not concluded your previous \"\n \t\t\t\t    \"notes merge (%s exists).\\nPlease, use \"\n \t\t\t\t    \"'git notes merge --commit' or 'git notes \"\n-\t\t\t\t    \"merge --reset' to commit/abort the \"\n+\t\t\t\t    \"merge --abort' to commit/abort the \"\n \t\t\t\t    \"previous merge before you start a new \"\n \t\t\t\t    \"notes merge.\", git_path(\"NOTES_MERGE_*\"));\n \t\t\telse\n@@ -649,3 +649,72 @@ found_result:\n \t       result, sha1_to_hex(result_sha1));\n \treturn result;\n }\n+\n+int notes_merge_commit(struct notes_merge_options *o,\n+\t\t       struct notes_tree *partial_tree,\n+\t\t       struct commit *partial_commit,\n+\t\t       unsigned char *result_sha1)\n+{\n+\t/*\n+\t * Iterate through files in .git/NOTES_MERGE_WORKTREE and add all\n+\t * found notes to 'partial_tree'. Write the updates notes tree to\n+\t * the DB, and commit the resulting tree object while reusing the\n+\t * commit message and parents from 'partial_commit'.\n+\t * Finally store the new commit object SHA1 into 'result_sha1'.\n+\t */\n+\tstruct dir_struct dir;\n+\tconst char *path = git_path(NOTES_MERGE_WORKTREE \"/\");\n+\tint path_len = strlen(path), i;\n+\tconst char *msg = strstr(partial_commit->buffer, \"\\n\\n\");\n+\n+\tOUTPUT(o, 3, \"Committing notes in notes merge worktree at %.*s\",\n+\t       path_len - 1, path);\n+\n+\tif (!msg || msg[2] == '\\0')\n+\t\tdie(\"partial notes commit has empty message\");\n+\tmsg += 2;\n+\n+\tmemset(&dir, 0, sizeof(dir));\n+\tread_directory(&dir, path, path_len, NULL);\n+\tfor (i = 0; i < dir.nr; i++) {\n+\t\tstruct dir_entry *ent = dir.entries[i];\n+\t\tstruct stat st;\n+\t\tconst char *relpath = ent->name + path_len;\n+\t\tunsigned char obj_sha1[20], blob_sha1[20];\n+\n+\t\tif (ent->len - path_len != 40 || get_sha1_hex(relpath, obj_sha1)) {\n+\t\t\tOUTPUT(o, 3, \"Skipping non-SHA1 entry '%s'\", ent->name);\n+\t\t\tcontinue;\n+\t\t}\n+\n+\t\t/* write file as blob, and add to partial_tree */\n+\t\tif (stat(ent->name, &st))\n+\t\t\tdie_errno(\"Failed to stat '%s'\", ent->name);\n+\t\tif (index_path(blob_sha1, ent->name, &st, 1))\n+\t\t\tdie(\"Failed to write blob object from '%s'\", ent->name);\n+\t\tif (add_note(partial_tree, obj_sha1, blob_sha1, NULL))\n+\t\t\tdie(\"Failed to add resolved note '%s' to notes tree\",\n+\t\t\t    ent->name);\n+\t\tOUTPUT(o, 4, \"Added resolved note for object %s: %s\",\n+\t\t       sha1_to_hex(obj_sha1), sha1_to_hex(blob_sha1));\n+\t}\n+\n+\tcreate_notes_commit(partial_tree, partial_commit->parents, msg,\n+\t\t\t    result_sha1);\n+\tOUTPUT(o, 4, \"Finalized notes merge commit: %s\",\n+\t       sha1_to_hex(result_sha1));\n+\treturn 0;\n+}\n+\n+int notes_merge_abort(struct notes_merge_options *o)\n+{\n+\t/* Remove .git/NOTES_MERGE_WORKTREE directory and all files within */\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tint ret;\n+\n+\tstrbuf_addstr(&buf, git_path(NOTES_MERGE_WORKTREE));\n+\tOUTPUT(o, 3, \"Removing notes merge worktree at %s\", buf.buf);\n+\tret = remove_dir_recursively(&buf, 0);\n+\tstrbuf_release(&buf);\n+\treturn ret;\n+}\ndiff --git a/notes-merge.h b/notes-merge.h\nindex 55ef3d9..aa75693 100644\n--- a/notes-merge.h\n+++ b/notes-merge.h\n@@ -71,4 +71,27 @@ int notes_merge(struct notes_merge_options *o,\n \t\tstruct notes_tree *local_tree,\n \t\tunsigned char *result_sha1);\n \n+/*\n+ * Finalize conflict resolution from an earlier notes_merge()\n+ *\n+ * The given notes tree 'partial_tree' must be the notes_tree corresponding to\n+ * the given 'partial_commit', the partial result commit created by a previous\n+ * call to notes_merge().\n+ *\n+ * This function will add the (now resolved) notes in .git/NOTES_MERGE_WORKTREE\n+ * to 'partial_tree', and create a final notes merge commit, the SHA1 of which\n+ * will be stored in 'result_sha1'.\n+ */\n+int notes_merge_commit(struct notes_merge_options *o,\n+\t\t       struct notes_tree *partial_tree,\n+\t\t       struct commit *partial_commit,\n+\t\t       unsigned char *result_sha1);\n+\n+/*\n+ * Abort conflict resolution from an earlier notes_merge()\n+ *\n+ * Removes the notes merge worktree in .git/NOTES_MERGE_WORKTREE.\n+ */\n+int notes_merge_abort(struct notes_merge_options *o);\n+\n #endif\ndiff --git a/t/t3310-notes-merge-manual-resolve.sh b/t/t3310-notes-merge-manual-resolve.sh\nindex eadb6b7..0ec315a 100755\n--- a/t/t3310-notes-merge-manual-resolve.sh\n+++ b/t/t3310-notes-merge-manual-resolve.sh\n@@ -171,6 +171,7 @@ cp expect_notes_y expect_notes_m\n cp expect_log_y expect_log_m\n \n git rev-parse refs/notes/y > pre_merge_y\n+git rev-parse refs/notes/z > pre_merge_z\n \n test_expect_success 'merge z into m (== y) with default (\"manual\") resolver => Conflicting 3-way merge' '\n \tgit update-ref refs/notes/m refs/notes/y &&\n@@ -289,4 +290,179 @@ test_expect_success 'can do merge without conflicts even if previous merge is un\n \tverify_notes y\n '\n \n+cat <<EOF | sort >expect_notes_m\n+021faa20e931fb48986ffc6282b4bb05553ac946 $commit_sha4\n+5772f42408c0dd6f097a7ca2d24de0e78d1c46b1 $commit_sha3\n+283b48219aee9a4105f6cab337e789065c82c2b9 $commit_sha2\n+0a59e787e6d688aa6309e56e8c1b89431a0fc1c1 $commit_sha1\n+EOF\n+\n+cat >expect_log_m <<EOF\n+$commit_sha5 5th\n+\n+$commit_sha4 4th\n+y and z notes on 4th commit\n+\n+$commit_sha3 3rd\n+y notes on 3rd commit\n+\n+$commit_sha2 2nd\n+z notes on 2nd commit\n+\n+$commit_sha1 1st\n+y and z notes on 1st commit\n+\n+EOF\n+\n+test_expect_success 'finalize conflicting merge (z => m)' '\n+\t# Resolve conflicts and finalize merge\n+\tcat >.git/NOTES_MERGE_WORKTREE/$commit_sha1 <<EOF &&\n+y and z notes on 1st commit\n+EOF\n+\tcat >.git/NOTES_MERGE_WORKTREE/$commit_sha4 <<EOF &&\n+y and z notes on 4th commit\n+EOF\n+\tgit notes merge --commit &&\n+\t# No .git/NOTES_MERGE_* files left\n+\ttest_must_fail ls .git/NOTES_MERGE_* >output 2>/dev/null &&\n+\ttest_cmp /dev/null output &&\n+\t# Merge commit has pre-merge y and pre-merge z as parents\n+\ttest \"$(git rev-parse refs/notes/m^1)\" = \"$(cat pre_merge_y)\" &&\n+\ttest \"$(git rev-parse refs/notes/m^2)\" = \"$(cat pre_merge_z)\" &&\n+\t# Merge commit mentions the notes refs merged\n+\tgit log -1 --format=%B refs/notes/m > merge_commit_msg &&\n+\tgrep -q refs/notes/m merge_commit_msg &&\n+\tgrep -q refs/notes/z merge_commit_msg &&\n+\t# Verify contents of merge result\n+\tverify_notes m &&\n+\t# Verify that other notes refs has not changed (w, x, y and z)\n+\tverify_notes w &&\n+\tverify_notes x &&\n+\tverify_notes y &&\n+\tverify_notes z\n+'\n+\n+cat >expect_conflict_$commit_sha4 <<EOF\n+<<<<<<< refs/notes/m\n+y notes on 4th commit\n+=======\n+z notes on 4th commit\n+\n+More z notes on 4th commit\n+>>>>>>> refs/notes/z\n+EOF\n+\n+cp expect_notes_y expect_notes_m\n+cp expect_log_y expect_log_m\n+\n+git rev-parse refs/notes/y > pre_merge_y\n+git rev-parse refs/notes/z > pre_merge_z\n+\n+test_expect_success 'redo merge of z into m (== y) with default (\"manual\") resolver => Conflicting 3-way merge' '\n+\tgit update-ref refs/notes/m refs/notes/y &&\n+\tgit config core.notesRef refs/notes/m &&\n+\ttest_must_fail git notes merge z >output &&\n+\t# Output should point to where to resolve conflicts\n+\tgrep -q \"\\\\.git/NOTES_MERGE_WORKTREE\" output &&\n+\t# Inspect merge conflicts\n+\tls .git/NOTES_MERGE_WORKTREE >output_conflicts &&\n+\ttest_cmp expect_conflicts output_conflicts &&\n+\t( for f in $(cat expect_conflicts); do\n+\t\ttest_cmp \"expect_conflict_$f\" \".git/NOTES_MERGE_WORKTREE/$f\" ||\n+\t\texit 1\n+\tdone ) &&\n+\t# Verify that current notes tree (pre-merge) has not changed (m == y)\n+\tverify_notes y &&\n+\tverify_notes m &&\n+\ttest \"$(git rev-parse refs/notes/m)\" = \"$(cat pre_merge_y)\"\n+'\n+\n+test_expect_success 'abort notes merge' '\n+\tgit notes merge --abort &&\n+\t# No .git/NOTES_MERGE_* files left\n+\ttest_must_fail ls .git/NOTES_MERGE_* >output 2>/dev/null &&\n+\ttest_cmp /dev/null output &&\n+\t# m has not moved (still == y)\n+\ttest \"$(git rev-parse refs/notes/m)\" = \"$(cat pre_merge_y)\"\n+\t# Verify that other notes refs has not changed (w, x, y and z)\n+\tverify_notes w &&\n+\tverify_notes x &&\n+\tverify_notes y &&\n+\tverify_notes z\n+'\n+\n+git rev-parse refs/notes/y > pre_merge_y\n+git rev-parse refs/notes/z > pre_merge_z\n+\n+test_expect_success 'redo merge of z into m (== y) with default (\"manual\") resolver => Conflicting 3-way merge' '\n+\ttest_must_fail git notes merge z >output &&\n+\t# Output should point to where to resolve conflicts\n+\tgrep -q \"\\\\.git/NOTES_MERGE_WORKTREE\" output &&\n+\t# Inspect merge conflicts\n+\tls .git/NOTES_MERGE_WORKTREE >output_conflicts &&\n+\ttest_cmp expect_conflicts output_conflicts &&\n+\t( for f in $(cat expect_conflicts); do\n+\t\ttest_cmp \"expect_conflict_$f\" \".git/NOTES_MERGE_WORKTREE/$f\" ||\n+\t\texit 1\n+\tdone ) &&\n+\t# Verify that current notes tree (pre-merge) has not changed (m == y)\n+\tverify_notes y &&\n+\tverify_notes m &&\n+\ttest \"$(git rev-parse refs/notes/m)\" = \"$(cat pre_merge_y)\"\n+'\n+\n+cat <<EOF | sort >expect_notes_m\n+304dfb4325cf243025b9957486eb605a9b51c199 $commit_sha5\n+283b48219aee9a4105f6cab337e789065c82c2b9 $commit_sha2\n+0a59e787e6d688aa6309e56e8c1b89431a0fc1c1 $commit_sha1\n+EOF\n+\n+cat >expect_log_m <<EOF\n+$commit_sha5 5th\n+new note on 5th commit\n+\n+$commit_sha4 4th\n+\n+$commit_sha3 3rd\n+\n+$commit_sha2 2nd\n+z notes on 2nd commit\n+\n+$commit_sha1 1st\n+y and z notes on 1st commit\n+\n+EOF\n+\n+test_expect_success 'add + remove notes in finalized merge (z => m)' '\n+\t# Resolve one conflict\n+\tcat >.git/NOTES_MERGE_WORKTREE/$commit_sha1 <<EOF &&\n+y and z notes on 1st commit\n+EOF\n+\t# Remove another conflict\n+\trm .git/NOTES_MERGE_WORKTREE/$commit_sha4 &&\n+\t# Remove a D/F conflict\n+\trm .git/NOTES_MERGE_WORKTREE/$commit_sha3 &&\n+\t# Add a new note\n+\techo \"new note on 5th commit\" > .git/NOTES_MERGE_WORKTREE/$commit_sha5 &&\n+\t# Finalize merge\n+\tgit notes merge --commit &&\n+\t# No .git/NOTES_MERGE_* files left\n+\ttest_must_fail ls .git/NOTES_MERGE_* >output 2>/dev/null &&\n+\ttest_cmp /dev/null output &&\n+\t# Merge commit has pre-merge y and pre-merge z as parents\n+\ttest \"$(git rev-parse refs/notes/m^1)\" = \"$(cat pre_merge_y)\" &&\n+\ttest \"$(git rev-parse refs/notes/m^2)\" = \"$(cat pre_merge_z)\" &&\n+\t# Merge commit mentions the notes refs merged\n+\tgit log -1 --format=%B refs/notes/m > merge_commit_msg &&\n+\tgrep -q refs/notes/m merge_commit_msg &&\n+\tgrep -q refs/notes/z merge_commit_msg &&\n+\t# Verify contents of merge result\n+\tverify_notes m &&\n+\t# Verify that other notes refs has not changed (w, x, y and z)\n+\tverify_notes w &&\n+\tverify_notes x &&\n+\tverify_notes y &&\n+\tverify_notes z\n+'\n+\n test_done\n-- \n1.7.3.98.g5ad7d9\n"},{"id":"154369","messageId":"1287965333-5099-18-git-send-email-johan@herland.net","threadId":"25543","inReplyTo":"1287965333-5099-1-git-send-email-johan@herland.net","subject":"[PATCHv5 17/23] git notes merge: List conflicting notes in notes merge commit message","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-10-25T00:08:47Z","receivedAt":"2010-10-25T00:08:47Z","isPatch":false,"sender":{"key":"johan@herland.net","avatar":"https://avatars.githubusercontent.com/u/547031?v=4"},"body":"This brings notes merge in line with regular merge's behaviour.\n\nThis patch has been improved by the following contributions:\n- Ævar Arnfjörð Bjarmason: Don't use C99 comments.\n\nThanks-to: Ævar Arnfjörð Bjarmason <avarab@gmail.com>\nSigned-off-by: Johan Herland <johan@herland.net>\n---\n builtin/notes.c                       |    2 +-\n notes-merge.c                         |   11 ++++++++++-\n notes-merge.h                         |    2 +-\n t/t3310-notes-merge-manual-resolve.sh |   12 ++++++++++++\n 4 files changed, 24 insertions(+), 3 deletions(-)\n\ndiff --git a/builtin/notes.c b/builtin/notes.c\nindex db60ead..b440378 100644\n--- a/builtin/notes.c\n+++ b/builtin/notes.c\n@@ -902,7 +902,7 @@ static int merge(int argc, const char **argv, const char *prefix)\n \n \tstrbuf_addf(&msg, \"notes: Merged notes from %s into %s\",\n \t\t    remote_ref.buf, default_notes_ref());\n-\to.commit_msg = msg.buf + 7; // skip \"notes: \" prefix\n+\tstrbuf_add(&(o.commit_msg), msg.buf + 7, msg.len - 7); /* skip \"notes: \" */\n \n \tresult = notes_merge(&o, t, result_sha1);\n \ndiff --git a/notes-merge.c b/notes-merge.c\nindex b44273c..db4fe08 100644\n--- a/notes-merge.c\n+++ b/notes-merge.c\n@@ -8,6 +8,7 @@\n #include \"dir.h\"\n #include \"notes.h\"\n #include \"notes-merge.h\"\n+#include \"strbuf.h\"\n \n struct notes_merge_pair {\n \tunsigned char obj[20], base[20], local[20], remote[20];\n@@ -16,6 +17,7 @@ struct notes_merge_pair {\n void init_notes_merge_options(struct notes_merge_options *o)\n {\n \tmemset(o, 0, sizeof(struct notes_merge_options));\n+\tstrbuf_init(&(o->commit_msg), 0);\n \to->verbosity = NOTES_MERGE_VERBOSITY_DEFAULT;\n }\n \n@@ -384,6 +386,12 @@ static int merge_one_change_manual(struct notes_merge_options *o,\n \t       sha1_to_hex(p->obj), sha1_to_hex(p->base),\n \t       sha1_to_hex(p->local), sha1_to_hex(p->remote));\n \n+\t/* add \"Conflicts:\" section to commit message first time through */\n+\tif (!o->has_worktree)\n+\t\tstrbuf_addstr(&(o->commit_msg), \"\\n\\nConflicts:\\n\");\n+\n+\tstrbuf_addf(&(o->commit_msg), \"\\t%s\\n\", sha1_to_hex(p->obj));\n+\n \tOUTPUT(o, 2, \"Auto-merging notes for %s\", sha1_to_hex(p->obj));\n \tcheck_notes_merge_worktree(o);\n \tif (is_null_sha1(p->local)) {\n@@ -639,12 +647,13 @@ int notes_merge(struct notes_merge_options *o,\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\tcreate_notes_commit(local_tree, parents, o->commit_msg.buf,\n \t\t\t\t    result_sha1);\n \t}\n \n found_result:\n \tfree_commit_list(bases);\n+\tstrbuf_release(&(o->commit_msg));\n \ttrace_printf(\"notes_merge(): result = %i, result_sha1 = %.7s\\n\",\n \t       result, sha1_to_hex(result_sha1));\n \treturn result;\ndiff --git a/notes-merge.h b/notes-merge.h\nindex aa75693..195222f 100644\n--- a/notes-merge.h\n+++ b/notes-merge.h\n@@ -11,7 +11,7 @@ enum notes_merge_verbosity {\n struct notes_merge_options {\n \tconst char *local_ref;\n \tconst char *remote_ref;\n-\tconst char *commit_msg;\n+\tstruct strbuf commit_msg;\n \tint verbosity;\n \tenum {\n \t\tNOTES_MERGE_RESOLVE_MANUAL = 0,\ndiff --git a/t/t3310-notes-merge-manual-resolve.sh b/t/t3310-notes-merge-manual-resolve.sh\nindex 0ec315a..287fab8 100755\n--- a/t/t3310-notes-merge-manual-resolve.sh\n+++ b/t/t3310-notes-merge-manual-resolve.sh\n@@ -333,6 +333,12 @@ EOF\n \tgit log -1 --format=%B refs/notes/m > merge_commit_msg &&\n \tgrep -q refs/notes/m merge_commit_msg &&\n \tgrep -q refs/notes/z merge_commit_msg &&\n+\t# Merge commit mentions conflicting notes\n+\tgrep -q \"Conflicts\" merge_commit_msg &&\n+\t( for sha1 in $(cat expect_conflicts); do\n+\t\tgrep -q \"$sha1\" merge_commit_msg ||\n+\t\texit 1\n+\tdone ) &&\n \t# Verify contents of merge result\n \tverify_notes m &&\n \t# Verify that other notes refs has not changed (w, x, y and z)\n@@ -456,6 +462,12 @@ EOF\n \tgit log -1 --format=%B refs/notes/m > merge_commit_msg &&\n \tgrep -q refs/notes/m merge_commit_msg &&\n \tgrep -q refs/notes/z merge_commit_msg &&\n+\t# Merge commit mentions conflicting notes\n+\tgrep -q \"Conflicts\" merge_commit_msg &&\n+\t( for sha1 in $(cat expect_conflicts); do\n+\t\tgrep -q \"$sha1\" merge_commit_msg ||\n+\t\texit 1\n+\tdone ) &&\n \t# Verify contents of merge result\n \tverify_notes m &&\n \t# Verify that other notes refs has not changed (w, x, y and z)\n-- \n1.7.3.98.g5ad7d9\n"},{"id":"154365","messageId":"1287965333-5099-19-git-send-email-johan@herland.net","threadId":"25543","inReplyTo":"1287965333-5099-1-git-send-email-johan@herland.net","subject":"[PATCHv5 18/23] git notes merge: --commit should fail if underlying notes ref has moved","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-10-25T00:08:48Z","receivedAt":"2010-10-25T00:08:48Z","isPatch":false,"sender":{"key":"johan@herland.net","avatar":"https://avatars.githubusercontent.com/u/547031?v=4"},"body":"When manually resolving a notes merge, if the merging ref has moved since\nthe merge started, we should fail to complete the merge, and alert the user\nto what's going on.\n\nThis situation may arise if you start a 'git notes merge' which results in\nconflicts, and you then update the current notes ref (using for example\n'git notes add/copy/amend/edit/remove/prune', 'git update-ref', etc.),\nbefore you get around to resolving the notes conflicts and calling\n'git notes merge --commit'.\n\nWe detect this situation by comparing the first parent of the partial merge\ncommit (which was created when the merge started) to the current value of the\nmerging notes ref (pointed to by the .git/NOTES_MERGE_REF symref).\n\nIf we don't fail in this situation, the notes merge commit would overwrite\nthe updated notes ref, thus losing the changes that happened in the meantime.\n\nThe patch includes a testcase verifying that we fail correctly in this\nsituation.\n\nSigned-off-by: Johan Herland <johan@herland.net>\n---\n builtin/notes.c                       |   11 ++++-\n t/t3310-notes-merge-manual-resolve.sh |   76 +++++++++++++++++++++++++++++++++\n 2 files changed, 85 insertions(+), 2 deletions(-)\n\ndiff --git a/builtin/notes.c b/builtin/notes.c\nindex b440378..ca09d45 100644\n--- a/builtin/notes.c\n+++ b/builtin/notes.c\n@@ -786,7 +786,7 @@ static int merge_abort(struct notes_merge_options *o)\n static int merge_commit(struct notes_merge_options *o)\n {\n \tstruct strbuf msg = STRBUF_INIT;\n-\tunsigned char sha1[20];\n+\tunsigned char sha1[20], parent_sha1[20];\n \tstruct notes_tree *t;\n \tstruct commit *partial;\n \tstruct pretty_print_context pretty_ctx;\n@@ -803,6 +803,11 @@ static int merge_commit(struct notes_merge_options *o)\n \telse if (parse_commit(partial))\n \t\tdie(\"Could not parse commit from NOTES_MERGE_PARTIAL.\");\n \n+\tif (partial->parents)\n+\t\thashcpy(parent_sha1, partial->parents->item->object.sha1);\n+\telse\n+\t\thashclr(parent_sha1);\n+\n \tt = xcalloc(1, sizeof(struct notes_tree));\n \tinit_notes(t, \"NOTES_MERGE_PARTIAL\", combine_notes_overwrite, 0);\n \n@@ -818,7 +823,9 @@ static int merge_commit(struct notes_merge_options *o)\n \tformat_commit_message(partial, \"%s\", &msg, &pretty_ctx);\n \tstrbuf_trim(&msg);\n \tstrbuf_insert(&msg, 0, \"notes: \", 7);\n-\tupdate_ref(msg.buf, o->local_ref, sha1, NULL, 0, DIE_ON_ERR);\n+\tupdate_ref(msg.buf, o->local_ref, sha1,\n+\t\t   is_null_sha1(parent_sha1) ? NULL : parent_sha1,\n+\t\t   0, DIE_ON_ERR);\n \n \tfree_notes(t);\n \tstrbuf_release(&msg);\ndiff --git a/t/t3310-notes-merge-manual-resolve.sh b/t/t3310-notes-merge-manual-resolve.sh\nindex 287fab8..4ec4d11 100755\n--- a/t/t3310-notes-merge-manual-resolve.sh\n+++ b/t/t3310-notes-merge-manual-resolve.sh\n@@ -477,4 +477,80 @@ EOF\n \tverify_notes z\n '\n \n+cp expect_notes_y expect_notes_m\n+cp expect_log_y expect_log_m\n+\n+test_expect_success 'redo merge of z into m (== y) with default (\"manual\") resolver => Conflicting 3-way merge' '\n+\tgit update-ref refs/notes/m refs/notes/y &&\n+\ttest_must_fail git notes merge z >output &&\n+\t# Output should point to where to resolve conflicts\n+\tgrep -q \"\\\\.git/NOTES_MERGE_WORKTREE\" output &&\n+\t# Inspect merge conflicts\n+\tls .git/NOTES_MERGE_WORKTREE >output_conflicts &&\n+\ttest_cmp expect_conflicts output_conflicts &&\n+\t( for f in $(cat expect_conflicts); do\n+\t\ttest_cmp \"expect_conflict_$f\" \".git/NOTES_MERGE_WORKTREE/$f\" ||\n+\t\texit 1\n+\tdone ) &&\n+\t# Verify that current notes tree (pre-merge) has not changed (m == y)\n+\tverify_notes y &&\n+\tverify_notes m &&\n+\ttest \"$(git rev-parse refs/notes/m)\" = \"$(cat pre_merge_y)\"\n+'\n+\n+cp expect_notes_w expect_notes_m\n+cp expect_log_w expect_log_m\n+\n+test_expect_success 'reset notes ref m to somewhere else (w)' '\n+\tgit update-ref refs/notes/m refs/notes/w &&\n+\tverify_notes m &&\n+\ttest \"$(git rev-parse refs/notes/m)\" = \"$(git rev-parse refs/notes/w)\"\n+'\n+\n+test_expect_success 'fail to finalize conflicting merge if underlying ref has moved in the meantime (m != NOTES_MERGE_PARTIAL^1)' '\n+\t# Resolve conflicts\n+\tcat >.git/NOTES_MERGE_WORKTREE/$commit_sha1 <<EOF &&\n+y and z notes on 1st commit\n+EOF\n+\tcat >.git/NOTES_MERGE_WORKTREE/$commit_sha4 <<EOF &&\n+y and z notes on 4th commit\n+EOF\n+\t# Fail to finalize merge\n+\ttest_must_fail git notes merge --commit >output 2>&1 &&\n+\t# .git/NOTES_MERGE_* must remain\n+\ttest -f .git/NOTES_MERGE_PARTIAL &&\n+\ttest -f .git/NOTES_MERGE_REF &&\n+\ttest -f .git/NOTES_MERGE_WORKTREE/$commit_sha1 &&\n+\ttest -f .git/NOTES_MERGE_WORKTREE/$commit_sha2 &&\n+\ttest -f .git/NOTES_MERGE_WORKTREE/$commit_sha3 &&\n+\ttest -f .git/NOTES_MERGE_WORKTREE/$commit_sha4 &&\n+\t# Refs are unchanged\n+\ttest \"$(git rev-parse refs/notes/m)\" = \"$(git rev-parse refs/notes/w)\"\n+\ttest \"$(git rev-parse refs/notes/y)\" = \"$(git rev-parse NOTES_MERGE_PARTIAL^1)\"\n+\ttest \"$(git rev-parse refs/notes/m)\" != \"$(git rev-parse NOTES_MERGE_PARTIAL^1)\"\n+\t# Mention refs/notes/m, and its current and expected value in output\n+\tgrep -q \"refs/notes/m\" output &&\n+\tgrep -q \"$(git rev-parse refs/notes/m)\" output &&\n+\tgrep -q \"$(git rev-parse NOTES_MERGE_PARTIAL^1)\" output &&\n+\t# Verify that other notes refs has not changed (w, x, y and z)\n+\tverify_notes w &&\n+\tverify_notes x &&\n+\tverify_notes y &&\n+\tverify_notes z\n+'\n+\n+test_expect_success 'resolve situation by aborting the notes merge' '\n+\tgit notes merge --abort &&\n+\t# No .git/NOTES_MERGE_* files left\n+\ttest_must_fail ls .git/NOTES_MERGE_* >output 2>/dev/null &&\n+\ttest_cmp /dev/null output &&\n+\t# m has not moved (still == w)\n+\ttest \"$(git rev-parse refs/notes/m)\" = \"$(git rev-parse refs/notes/w)\"\n+\t# Verify that other notes refs has not changed (w, x, y and z)\n+\tverify_notes w &&\n+\tverify_notes x &&\n+\tverify_notes y &&\n+\tverify_notes z\n+'\n+\n test_done\n-- \n1.7.3.98.g5ad7d9\n"},{"id":"154371","messageId":"1287965333-5099-20-git-send-email-johan@herland.net","threadId":"25543","inReplyTo":"1287965333-5099-1-git-send-email-johan@herland.net","subject":"[PATCHv5 19/23] git notes merge: Add another auto-resolving strategy: \"cat_sort_uniq\"","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-10-25T00:08:49Z","receivedAt":"2010-10-25T00:08:49Z","isPatch":false,"sender":{"key":"johan@herland.net","avatar":"https://avatars.githubusercontent.com/u/547031?v=4"},"body":"This new strategy is similar to \"concatenate\", but in addition to\nconcatenating the two note candidates, this strategy sorts the resulting\nlines, and removes duplicate lines from the result. This is equivalent to\napplying the \"cat | sort | uniq\" shell pipeline to the two note candidates.\n\nThis strategy is useful if the notes follow a line-based format where one\nwants to avoid duplicate lines in the merge result.\n\nNote that if either of the note candidates contain duplicate lines _prior_\nto the merge, these will also be removed by this merge strategy.\n\nThe patch also contains tests and documentation for the new strategy.\n\nSigned-off-by: Johan Herland <johan@herland.net>\n---\n Documentation/git-notes.txt         |   12 +++-\n builtin/notes.c                     |    8 ++-\n notes-merge.c                       |    6 ++\n notes-merge.h                       |    3 +-\n notes.c                             |   76 ++++++++++++++++++\n notes.h                             |    1 +\n t/t3309-notes-merge-auto-resolve.sh |  145 +++++++++++++++++++++++++++++++++++\n 7 files changed, 247 insertions(+), 4 deletions(-)\n\ndiff --git a/Documentation/git-notes.txt b/Documentation/git-notes.txt\nindex a822551..1de1417 100644\n--- a/Documentation/git-notes.txt\n+++ b/Documentation/git-notes.txt\n@@ -155,7 +155,7 @@ OPTIONS\n --strategy=<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\".\n+\t(default), \"ours\", \"theirs\", \"union\" and \"cat_sort_uniq\".\n \tSee the \"NOTES MERGE STRATEGIES\" section below for more\n \tinformation on each notes merge strategy.\n \n@@ -230,6 +230,16 @@ ref).\n \"union\" automatically resolves notes conflicts by concatenating the\n local and remote versions.\n \n+\"cat_sort_uniq\" is similar to \"union\", but in addition to concatenating\n+the local and remote versions, this strategy also sorts the resulting\n+lines, and removes duplicate lines from the result. This is equivalent\n+to applying the \"cat | sort | uniq\" shell pipeline to the local and\n+remote versions. This strategy is useful if the notes follow a line-based\n+format where one wants to avoid duplicated lines in the merge result.\n+Note that if either the local or remote version contain duplicate lines\n+prior to the merge, these will also be removed by this notes merge\n+strategy.\n+\n \n EXAMPLES\n --------\ndiff --git a/builtin/notes.c b/builtin/notes.c\nindex ca09d45..79783a5 100644\n--- a/builtin/notes.c\n+++ b/builtin/notes.c\n@@ -324,6 +324,8 @@ combine_notes_fn parse_combine_notes_fn(const char *v)\n \t\treturn combine_notes_ignore;\n \telse if (!strcasecmp(v, \"concatenate\"))\n \t\treturn combine_notes_concatenate;\n+\telse if (!strcasecmp(v, \"cat_sort_uniq\"))\n+\t\treturn combine_notes_cat_sort_uniq;\n \telse\n \t\treturn NULL;\n }\n@@ -846,8 +848,8 @@ static int merge(int argc, const char **argv, const char *prefix)\n \t\tOPT__VERBOSITY(&verbosity),\n \t\tOPT_GROUP(\"Merge options\"),\n \t\tOPT_STRING('s', \"strategy\", &strategy, \"strategy\",\n-\t\t\t   \"resolve notes conflicts using the given \"\n-\t\t\t   \"strategy (manual/ours/theirs/union)\"),\n+\t\t\t   \"resolve notes conflicts using the given strategy \"\n+\t\t\t   \"(manual/ours/theirs/union/cat_sort_uniq)\"),\n \t\tOPT_GROUP(\"Committing unmerged notes\"),\n \t\t{ OPTION_BOOLEAN, 0, \"commit\", &do_commit, NULL,\n \t\t\t\"finalize notes merge by committing unmerged notes\",\n@@ -899,6 +901,8 @@ static int merge(int argc, const char **argv, const char *prefix)\n \t\t\to.strategy = NOTES_MERGE_RESOLVE_THEIRS;\n \t\telse if (!strcmp(strategy, \"union\"))\n \t\t\to.strategy = NOTES_MERGE_RESOLVE_UNION;\n+\t\telse if (!strcmp(strategy, \"cat_sort_uniq\"))\n+\t\t\to.strategy = NOTES_MERGE_RESOLVE_CAT_SORT_UNIQ;\n \t\telse {\n \t\t\terror(\"Unknown -s/--strategy: %s\", strategy);\n \t\t\tusage_with_options(git_notes_merge_usage, options);\ndiff --git a/notes-merge.c b/notes-merge.c\nindex db4fe08..d112a91 100644\n--- a/notes-merge.c\n+++ b/notes-merge.c\n@@ -452,6 +452,12 @@ static int merge_one_change(struct notes_merge_options *o,\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+\tcase NOTES_MERGE_RESOLVE_CAT_SORT_UNIQ:\n+\t\tOUTPUT(o, 2, \"Concatenating unique lines in local and remote \"\n+\t\t       \"notes for %s\", sha1_to_hex(p->obj));\n+\t\tif (add_note(t, p->obj, p->remote, combine_notes_cat_sort_uniq))\n+\t\t\tdie(\"confused: combine_notes_cat_sort_uniq failed\");\n+\t\treturn 0;\n \t}\n \tdie(\"Unknown strategy (%i).\", o->strategy);\n }\ndiff --git a/notes-merge.h b/notes-merge.h\nindex 195222f..168a672 100644\n--- a/notes-merge.h\n+++ b/notes-merge.h\n@@ -17,7 +17,8 @@ struct notes_merge_options {\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\tNOTES_MERGE_RESOLVE_UNION,\n+\t\tNOTES_MERGE_RESOLVE_CAT_SORT_UNIQ\n \t} strategy;\n \tunsigned has_worktree:1;\n };\ndiff --git a/notes.c b/notes.c\nindex 09a93ab..96cde42 100644\n--- a/notes.c\n+++ b/notes.c\n@@ -845,6 +845,82 @@ int combine_notes_ignore(unsigned char *cur_sha1,\n \treturn 0;\n }\n \n+static int string_list_add_note_lines(struct string_list *sort_uniq_list,\n+\t\t\t\t      const unsigned char *sha1)\n+{\n+\tchar *data;\n+\tunsigned long len;\n+\tenum object_type t;\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tstruct strbuf **lines = NULL;\n+\tint i, list_index;\n+\n+\tif (is_null_sha1(sha1))\n+\t\treturn 0;\n+\n+\t/* read_sha1_file NUL-terminates */\n+\tdata = read_sha1_file(sha1, &t, &len);\n+\tif (t != OBJ_BLOB || !data || !len) {\n+\t\tfree(data);\n+\t\treturn t != OBJ_BLOB || !data;\n+\t}\n+\n+\tstrbuf_attach(&buf, data, len, len + 1);\n+\tlines = strbuf_split(&buf, '\\n');\n+\n+\tfor (i = 0; lines[i]; i++) {\n+\t\tif (lines[i]->buf[lines[i]->len - 1] == '\\n')\n+\t\t\tstrbuf_setlen(lines[i], lines[i]->len - 1);\n+\t\tif (!lines[i]->len)\n+\t\t\tcontinue; /* skip empty lines */\n+\t\tlist_index = string_list_find_insert_index(sort_uniq_list,\n+\t\t\t\t\t\t\t   lines[i]->buf, 0);\n+\t\tif (list_index < 0)\n+\t\t\tcontinue; /* skip duplicate lines */\n+\t\tstring_list_insert_at_index(sort_uniq_list, list_index,\n+\t\t\t\t\t    lines[i]->buf);\n+\t}\n+\n+\tstrbuf_list_free(lines);\n+\tstrbuf_release(&buf);\n+\treturn 0;\n+}\n+\n+static int string_list_join_lines_helper(struct string_list_item *item,\n+\t\t\t\t\t void *cb_data)\n+{\n+\tstruct strbuf *buf = cb_data;\n+\tstrbuf_addstr(buf, item->string);\n+\tstrbuf_addch(buf, '\\n');\n+\treturn 0;\n+}\n+\n+int combine_notes_cat_sort_uniq(unsigned char *cur_sha1,\n+\t\tconst unsigned char *new_sha1)\n+{\n+\tstruct string_list sort_uniq_list = { NULL, 0, 0, 1 };\n+\tstruct strbuf buf = STRBUF_INIT;\n+\tint ret = 1;\n+\n+\t/* read both note blob objects into unique_lines */\n+\tif (string_list_add_note_lines(&sort_uniq_list, cur_sha1))\n+\t\tgoto out;\n+\tif (string_list_add_note_lines(&sort_uniq_list, new_sha1))\n+\t\tgoto out;\n+\n+\t/* create a new blob object from sort_uniq_list */\n+\tif (for_each_string_list(&sort_uniq_list,\n+\t\t\t\t string_list_join_lines_helper, &buf))\n+\t\tgoto out;\n+\n+\tret = write_sha1_file(buf.buf, buf.len, blob_type, cur_sha1);\n+\n+out:\n+\tstrbuf_release(&buf);\n+\tstring_list_clear(&sort_uniq_list, 0);\n+\treturn ret;\n+}\n+\n static int string_list_add_one_ref(const char *path, const unsigned char *sha1,\n \t\t\t\t   int flag, void *cb)\n {\ndiff --git a/notes.h b/notes.h\nindex b372575..93a7365 100644\n--- a/notes.h\n+++ b/notes.h\n@@ -27,6 +27,7 @@ typedef int (*combine_notes_fn)(unsigned char *cur_sha1, const unsigned char *ne\n int combine_notes_concatenate(unsigned char *cur_sha1, const unsigned char *new_sha1);\n int combine_notes_overwrite(unsigned char *cur_sha1, const unsigned char *new_sha1);\n int combine_notes_ignore(unsigned char *cur_sha1, const unsigned char *new_sha1);\n+int combine_notes_cat_sort_uniq(unsigned char *cur_sha1, const unsigned char *new_sha1);\n \n /*\n  * Notes tree object\ndiff --git a/t/t3309-notes-merge-auto-resolve.sh b/t/t3309-notes-merge-auto-resolve.sh\nindex 1a1fc7e..461fd84 100755\n--- a/t/t3309-notes-merge-auto-resolve.sh\n+++ b/t/t3309-notes-merge-auto-resolve.sh\n@@ -499,4 +499,149 @@ test_expect_success 'merge z into y with \"union\" strategy => Non-conflicting 3-w\n \tverify_notes y union\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+\tverify_notes y y\n+'\n+\n+cat <<EOF | sort >expect_notes_union2\n+d682107b8bf7a7aea1e537a8d5cb6a12b60135f1 $commit_sha15\n+5de7ea7ad4f47e7ff91989fb82234634730f75df $commit_sha14\n+3a631fdb6f41b05b55d8f4baf20728ba8f6fccbc $commit_sha13\n+a66055fa82f7a03fe0c02a6aba3287a85abf7c62 $commit_sha12\n+7e3c53503a3db8dd996cb62e37c66e070b44b54d $commit_sha11\n+b8d03e173f67f6505a76f6e00cf93440200dd9be $commit_sha10\n+851e1638784a884c7dd26c5d41f3340f6387413a $commit_sha8\n+357b6ca14c7afd59b7f8b8aaaa6b8b723771135b $commit_sha5\n+e2bfd06a37dd2031684a59a6e2b033e212239c78 $commit_sha4\n+5772f42408c0dd6f097a7ca2d24de0e78d1c46b1 $commit_sha3\n+283b48219aee9a4105f6cab337e789065c82c2b9 $commit_sha2\n+EOF\n+\n+cat >expect_log_union2 <<EOF\n+$commit_sha15 15th\n+z notes on 15th commit\n+\n+y notes on 15th commit\n+\n+$commit_sha14 14th\n+y notes on 14th commit\n+\n+$commit_sha13 13th\n+y notes on 13th commit\n+\n+$commit_sha12 12th\n+y notes on 12th commit\n+\n+$commit_sha11 11th\n+z notes on 11th commit\n+\n+$commit_sha10 10th\n+x notes on 10th commit\n+\n+$commit_sha9 9th\n+\n+$commit_sha8 8th\n+z notes on 8th commit\n+\n+$commit_sha7 7th\n+\n+$commit_sha6 6th\n+\n+$commit_sha5 5th\n+z notes on 5th commit\n+\n+y notes on 5th commit\n+\n+$commit_sha4 4th\n+y notes on 4th commit\n+\n+$commit_sha3 3rd\n+y notes on 3rd commit\n+\n+$commit_sha2 2nd\n+z notes on 2nd commit\n+\n+$commit_sha1 1st\n+\n+EOF\n+\n+test_expect_success 'merge y into z with \"union\" strategy => Non-conflicting 3-way merge' '\n+\tgit config core.notesRef refs/notes/z &&\n+\tgit notes merge --strategy=union y &&\n+\tverify_notes z union2\n+'\n+\n+test_expect_success 'reset to pre-merge state (z)' '\n+\tgit update-ref refs/notes/z refs/notes/z^1 &&\n+\t# Verify pre-merge state\n+\tverify_notes z z\n+'\n+\n+cat <<EOF | sort >expect_notes_cat_sort_uniq\n+6be90240b5f54594203e25d9f2f64b7567175aee $commit_sha15\n+5de7ea7ad4f47e7ff91989fb82234634730f75df $commit_sha14\n+3a631fdb6f41b05b55d8f4baf20728ba8f6fccbc $commit_sha13\n+a66055fa82f7a03fe0c02a6aba3287a85abf7c62 $commit_sha12\n+7e3c53503a3db8dd996cb62e37c66e070b44b54d $commit_sha11\n+b8d03e173f67f6505a76f6e00cf93440200dd9be $commit_sha10\n+851e1638784a884c7dd26c5d41f3340f6387413a $commit_sha8\n+660311d7f78dc53db12ac373a43fca7465381a7e $commit_sha5\n+e2bfd06a37dd2031684a59a6e2b033e212239c78 $commit_sha4\n+5772f42408c0dd6f097a7ca2d24de0e78d1c46b1 $commit_sha3\n+283b48219aee9a4105f6cab337e789065c82c2b9 $commit_sha2\n+EOF\n+\n+cat >expect_log_cat_sort_uniq <<EOF\n+$commit_sha15 15th\n+y notes on 15th commit\n+z notes on 15th commit\n+\n+$commit_sha14 14th\n+y notes on 14th commit\n+\n+$commit_sha13 13th\n+y notes on 13th commit\n+\n+$commit_sha12 12th\n+y notes on 12th commit\n+\n+$commit_sha11 11th\n+z notes on 11th commit\n+\n+$commit_sha10 10th\n+x notes on 10th commit\n+\n+$commit_sha9 9th\n+\n+$commit_sha8 8th\n+z notes on 8th commit\n+\n+$commit_sha7 7th\n+\n+$commit_sha6 6th\n+\n+$commit_sha5 5th\n+y notes on 5th commit\n+z notes on 5th commit\n+\n+$commit_sha4 4th\n+y notes on 4th commit\n+\n+$commit_sha3 3rd\n+y notes on 3rd commit\n+\n+$commit_sha2 2nd\n+z notes on 2nd commit\n+\n+$commit_sha1 1st\n+\n+EOF\n+\n+test_expect_success 'merge y into z with \"cat_sort_uniq\" strategy => Non-conflicting 3-way merge' '\n+\tgit notes merge --strategy=cat_sort_uniq y &&\n+\tverify_notes z cat_sort_uniq\n+'\n+\n test_done\n-- \n1.7.3.98.g5ad7d9\n"},{"id":"154367","messageId":"1287965333-5099-21-git-send-email-johan@herland.net","threadId":"25543","inReplyTo":"1287965333-5099-1-git-send-email-johan@herland.net","subject":"[PATCHv5 20/23] git notes merge: Add testcases for merging notes trees at different fanouts","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-10-25T00:08:50Z","receivedAt":"2010-10-25T00:08:50Z","isPatch":false,"sender":{"key":"johan@herland.net","avatar":"https://avatars.githubusercontent.com/u/547031?v=4"},"body":"Notes trees may exist at different fanout levels internally. This\nimplementation detail should not be visible to the user, and it should\ncertainly not affect the merging of notes tree.\n\nThis patch adds testcases verifying the correctness of 'git notes merge'\nwhen merging notes trees at different fanout levels.\n\nSigned-off-by: Johan Herland <johan@herland.net>\n---\n t/t3311-notes-merge-fanout.sh |  436 +++++++++++++++++++++++++++++++++++++++++\n 1 files changed, 436 insertions(+), 0 deletions(-)\n create mode 100755 t/t3311-notes-merge-fanout.sh\n\ndiff --git a/t/t3311-notes-merge-fanout.sh b/t/t3311-notes-merge-fanout.sh\nnew file mode 100755\nindex 0000000..d1c7b69\n--- /dev/null\n+++ b/t/t3311-notes-merge-fanout.sh\n@@ -0,0 +1,436 @@\n+#!/bin/sh\n+#\n+# Copyright (c) 2010 Johan Herland\n+#\n+\n+test_description='Test notes merging at various fanout levels'\n+\n+. ./test-lib.sh\n+\n+verify_notes () {\n+\tnotes_ref=\"$1\"\n+\tcommit=\"$2\"\n+\tif test -f \"expect_notes_$notes_ref\"\n+\tthen\n+\t\tgit -c core.notesRef=\"refs/notes/$notes_ref\" notes |\n+\t\t\tsort >\"output_notes_$notes_ref\" &&\n+\t\ttest_cmp \"expect_notes_$notes_ref\" \"output_notes_$notes_ref\" ||\n+\t\t\treturn 1\n+\tfi &&\n+\tgit -c core.notesRef=\"refs/notes/$notes_ref\" log --format=\"%H %s%n%N\" \\\n+\t\t\"$commit\" >\"output_log_$notes_ref\" &&\n+\ttest_cmp \"expect_log_$notes_ref\" \"output_log_$notes_ref\"\n+}\n+\n+verify_fanout () {\n+\tnotes_ref=\"$1\"\n+\t# Expect entire notes tree to have a fanout == 1\n+\tgit rev-parse --quiet --verify \"refs/notes/$notes_ref\" >/dev/null &&\n+\tgit ls-tree -r --name-only \"refs/notes/$notes_ref\" |\n+\twhile read path\n+\tdo\n+\t\tcase \"$path\" in\n+\t\t??/??????????????????????????????????????)\n+\t\t\t: true\n+\t\t\t;;\n+\t\t*)\n+\t\t\techo \"Invalid path \\\"$path\\\"\" &&\n+\t\t\treturn 1\n+\t\t\t;;\n+\t\tesac\n+\tdone\n+}\n+\n+verify_no_fanout () {\n+\tnotes_ref=\"$1\"\n+\t# Expect entire notes tree to have a fanout == 0\n+\tgit rev-parse --quiet --verify \"refs/notes/$notes_ref\" >/dev/null &&\n+\tgit ls-tree -r --name-only \"refs/notes/$notes_ref\" |\n+\twhile read path\n+\tdo\n+\t\tcase \"$path\" in\n+\t\t????????????????????????????????????????)\n+\t\t\t: true\n+\t\t\t;;\n+\t\t*)\n+\t\t\techo \"Invalid path \\\"$path\\\"\" &&\n+\t\t\treturn 1\n+\t\t\t;;\n+\t\tesac\n+\tdone\n+}\n+\n+# Set up a notes merge scenario with different kinds of conflicts\n+test_expect_success 'setup a few initial commits with notes (notes ref: x)' '\n+\tgit config core.notesRef refs/notes/x &&\n+\tfor i in 1 2 3 4 5\n+\tdo\n+\t\ttest_commit \"commit$i\" >/dev/null &&\n+\t\tgit notes add -m \"notes for commit$i\" || return 1\n+\tdone\n+'\n+\n+commit_sha1=$(git rev-parse commit1^{commit})\n+commit_sha2=$(git rev-parse commit2^{commit})\n+commit_sha3=$(git rev-parse commit3^{commit})\n+commit_sha4=$(git rev-parse commit4^{commit})\n+commit_sha5=$(git rev-parse commit5^{commit})\n+\n+cat <<EOF | sort >expect_notes_x\n+aed91155c7a72c2188e781fdf40e0f3761b299db $commit_sha5\n+99fab268f9d7ee7b011e091a436c78def8eeee69 $commit_sha4\n+953c20ae26c7aa0b428c20693fe38bc687f9d1a9 $commit_sha3\n+6358796131b8916eaa2dde6902642942a1cb37e1 $commit_sha2\n+b02d459c32f0e68f2fe0981033bb34f38776ba47 $commit_sha1\n+EOF\n+\n+cat >expect_log_x <<EOF\n+$commit_sha5 commit5\n+notes for commit5\n+\n+$commit_sha4 commit4\n+notes for commit4\n+\n+$commit_sha3 commit3\n+notes for commit3\n+\n+$commit_sha2 commit2\n+notes for commit2\n+\n+$commit_sha1 commit1\n+notes for commit1\n+\n+EOF\n+\n+test_expect_success 'sanity check (x)' '\n+\tverify_notes x commit5 &&\n+\tverify_no_fanout x\n+'\n+\n+num=300\n+\n+cp expect_log_x expect_log_y\n+\n+test_expect_success 'Add a few hundred commits w/notes to trigger fanout (x -> y)' '\n+\tgit update-ref refs/notes/y refs/notes/x &&\n+\tgit config core.notesRef refs/notes/y &&\n+\ti=5 &&\n+\twhile test $i -lt $num\n+\tdo\n+\t\ti=$(($i + 1)) &&\n+\t\ttest_commit \"commit$i\" >/dev/null &&\n+\t\tgit notes add -m \"notes for commit$i\" || return 1\n+\tdone &&\n+\ttest \"$(git rev-parse refs/notes/y)\" != \"$(git rev-parse refs/notes/x)\" &&\n+\t# Expected number of commits and notes\n+\ttest \"$(git rev-list HEAD | wc -l)\" = \"$num\" &&\n+\ttest \"$(git notes list | wc -l)\" = \"$num\" &&\n+\t# 5 first notes unchanged\n+\tverify_notes y commit5\n+'\n+\n+test_expect_success 'notes tree has fanout (y)' 'verify_fanout y'\n+\n+test_expect_success 'No-op merge (already included) (x => y)' '\n+\tgit update-ref refs/notes/m refs/notes/y &&\n+\tgit config core.notesRef refs/notes/m &&\n+\tgit notes merge x &&\n+\ttest \"$(git rev-parse refs/notes/m)\" = \"$(git rev-parse refs/notes/y)\"\n+'\n+\n+test_expect_success 'Fast-forward merge (y => x)' '\n+\tgit update-ref refs/notes/m refs/notes/x &&\n+\tgit notes merge y &&\n+\ttest \"$(git rev-parse refs/notes/m)\" = \"$(git rev-parse refs/notes/y)\"\n+'\n+\n+cat <<EOF | sort >expect_notes_z\n+9f506ee70e20379d7f78204c77b334f43d77410d $commit_sha3\n+23a47d6ea7d589895faf800752054818e1e7627b $commit_sha2\n+b02d459c32f0e68f2fe0981033bb34f38776ba47 $commit_sha1\n+EOF\n+\n+cat >expect_log_z <<EOF\n+$commit_sha5 commit5\n+\n+$commit_sha4 commit4\n+\n+$commit_sha3 commit3\n+notes for commit3\n+\n+appended notes for commit3\n+\n+$commit_sha2 commit2\n+new notes for commit2\n+\n+$commit_sha1 commit1\n+notes for commit1\n+\n+EOF\n+\n+test_expect_success 'change some of the initial 5 notes (x -> 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 \"new notes for commit2\" commit2 &&\n+\tgit notes append -m \"appended notes for commit3\" commit3 &&\n+\tgit notes remove commit4 &&\n+\tgit notes remove commit5 &&\n+\tverify_notes z commit5\n+'\n+\n+test_expect_success 'notes tree has no fanout (z)' 'verify_no_fanout z'\n+\n+cp expect_log_z expect_log_m\n+\n+test_expect_success 'successful merge without conflicts (y => z)' '\n+\tgit update-ref refs/notes/m refs/notes/z &&\n+\tgit config core.notesRef refs/notes/m &&\n+\tgit notes merge y &&\n+\tverify_notes m commit5 &&\n+\t# x/y/z unchanged\n+\tverify_notes x commit5 &&\n+\tverify_notes y commit5 &&\n+\tverify_notes z commit5\n+'\n+\n+test_expect_success 'notes tree still has fanout after merge (m)' 'verify_fanout m'\n+\n+cat >expect_log_w <<EOF\n+$commit_sha5 commit5\n+\n+$commit_sha4 commit4\n+other notes for commit4\n+\n+$commit_sha3 commit3\n+other notes for commit3\n+\n+$commit_sha2 commit2\n+notes for commit2\n+\n+$commit_sha1 commit1\n+other notes for commit1\n+\n+EOF\n+\n+test_expect_success 'introduce conflicting changes (y -> w)' '\n+\tgit update-ref refs/notes/w refs/notes/y &&\n+\tgit config core.notesRef refs/notes/w &&\n+\tgit notes add -f -m \"other notes for commit1\" commit1 &&\n+\tgit notes add -f -m \"other notes for commit3\" commit3 &&\n+\tgit notes add -f -m \"other notes for commit4\" commit4 &&\n+\tgit notes remove commit5 &&\n+\tverify_notes w commit5\n+'\n+\n+cat >expect_log_m <<EOF\n+$commit_sha5 commit5\n+\n+$commit_sha4 commit4\n+other notes for commit4\n+\n+$commit_sha3 commit3\n+other notes for commit3\n+\n+$commit_sha2 commit2\n+new notes for commit2\n+\n+$commit_sha1 commit1\n+other notes for commit1\n+\n+EOF\n+\n+test_expect_success 'successful merge using \"ours\" strategy (z => w)' '\n+\tgit update-ref refs/notes/m refs/notes/w &&\n+\tgit config core.notesRef refs/notes/m &&\n+\tgit notes merge -s ours z &&\n+\tverify_notes m commit5 &&\n+\t# w/x/y/z unchanged\n+\tverify_notes w commit5 &&\n+\tverify_notes x commit5 &&\n+\tverify_notes y commit5 &&\n+\tverify_notes z commit5\n+'\n+\n+test_expect_success 'notes tree still has fanout after merge (m)' 'verify_fanout m'\n+\n+cat >expect_log_m <<EOF\n+$commit_sha5 commit5\n+\n+$commit_sha4 commit4\n+\n+$commit_sha3 commit3\n+notes for commit3\n+\n+appended notes for commit3\n+\n+$commit_sha2 commit2\n+new notes for commit2\n+\n+$commit_sha1 commit1\n+other notes for commit1\n+\n+EOF\n+\n+test_expect_success 'successful merge using \"theirs\" strategy (z => w)' '\n+\tgit update-ref refs/notes/m refs/notes/w &&\n+\tgit notes merge -s theirs z &&\n+\tverify_notes m commit5 &&\n+\t# w/x/y/z unchanged\n+\tverify_notes w commit5 &&\n+\tverify_notes x commit5 &&\n+\tverify_notes y commit5 &&\n+\tverify_notes z commit5\n+'\n+\n+test_expect_success 'notes tree still has fanout after merge (m)' 'verify_fanout m'\n+\n+cat >expect_log_m <<EOF\n+$commit_sha5 commit5\n+\n+$commit_sha4 commit4\n+other notes for commit4\n+\n+$commit_sha3 commit3\n+other notes for commit3\n+\n+notes for commit3\n+\n+appended notes for commit3\n+\n+$commit_sha2 commit2\n+new notes for commit2\n+\n+$commit_sha1 commit1\n+other notes for commit1\n+\n+EOF\n+\n+test_expect_success 'successful merge using \"union\" strategy (z => w)' '\n+\tgit update-ref refs/notes/m refs/notes/w &&\n+\tgit notes merge -s union z &&\n+\tverify_notes m commit5 &&\n+\t# w/x/y/z unchanged\n+\tverify_notes w commit5 &&\n+\tverify_notes x commit5 &&\n+\tverify_notes y commit5 &&\n+\tverify_notes z commit5\n+'\n+\n+test_expect_success 'notes tree still has fanout after merge (m)' 'verify_fanout m'\n+\n+cat >expect_log_m <<EOF\n+$commit_sha5 commit5\n+\n+$commit_sha4 commit4\n+other notes for commit4\n+\n+$commit_sha3 commit3\n+appended notes for commit3\n+notes for commit3\n+other notes for commit3\n+\n+$commit_sha2 commit2\n+new notes for commit2\n+\n+$commit_sha1 commit1\n+other notes for commit1\n+\n+EOF\n+\n+test_expect_success 'successful merge using \"cat_sort_uniq\" strategy (z => w)' '\n+\tgit update-ref refs/notes/m refs/notes/w &&\n+\tgit notes merge -s cat_sort_uniq z &&\n+\tverify_notes m commit5 &&\n+\t# w/x/y/z unchanged\n+\tverify_notes w commit5 &&\n+\tverify_notes x commit5 &&\n+\tverify_notes y commit5 &&\n+\tverify_notes z commit5\n+'\n+\n+test_expect_success 'notes tree still has fanout after merge (m)' 'verify_fanout m'\n+\n+# We're merging z into w. Here are the conflicts we expect:\n+#\n+# commit | x -> w    | x -> z    | conflict?\n+# -------|-----------|-----------|----------\n+# 1      | changed   | unchanged | no, use w\n+# 2      | unchanged | changed   | no, use z\n+# 3      | changed   | changed   | yes (w, then z in conflict markers)\n+# 4      | changed   | deleted   | yes (w)\n+# 5      | deleted   | deleted   | no, deleted\n+\n+test_expect_success 'fails to merge using \"manual\" strategy (z => w)' '\n+\tgit update-ref refs/notes/m refs/notes/w &&\n+\ttest_must_fail git notes merge z\n+'\n+\n+test_expect_success 'notes tree still has fanout after merge (m)' 'verify_fanout m'\n+\n+cat <<EOF | sort >expect_conflicts\n+$commit_sha3\n+$commit_sha4\n+EOF\n+\n+cat >expect_conflict_$commit_sha3 <<EOF\n+<<<<<<< refs/notes/m\n+other notes for commit3\n+=======\n+notes for commit3\n+\n+appended notes for commit3\n+>>>>>>> refs/notes/z\n+EOF\n+\n+cat >expect_conflict_$commit_sha4 <<EOF\n+other notes for commit4\n+EOF\n+\n+test_expect_success 'verify conflict entries (with no fanout)' '\n+\tls .git/NOTES_MERGE_WORKTREE >output_conflicts &&\n+\ttest_cmp expect_conflicts output_conflicts &&\n+\t( for f in $(cat expect_conflicts); do\n+\t\ttest_cmp \"expect_conflict_$f\" \".git/NOTES_MERGE_WORKTREE/$f\" ||\n+\t\texit 1\n+\tdone ) &&\n+\t# Verify that current notes tree (pre-merge) has not changed (m == w)\n+\ttest \"$(git rev-parse refs/notes/m)\" = \"$(git rev-parse refs/notes/w)\"\n+'\n+\n+cat >expect_log_m <<EOF\n+$commit_sha5 commit5\n+\n+$commit_sha4 commit4\n+other notes for commit4\n+\n+$commit_sha3 commit3\n+other notes for commit3\n+\n+appended notes for commit3\n+\n+$commit_sha2 commit2\n+new notes for commit2\n+\n+$commit_sha1 commit1\n+other notes for commit1\n+\n+EOF\n+\n+test_expect_success 'resolve and finalize merge (z => w)' '\n+\tcat >.git/NOTES_MERGE_WORKTREE/$commit_sha3 <<EOF &&\n+other notes for commit3\n+\n+appended notes for commit3\n+EOF\n+\tgit notes merge --commit &&\n+\tverify_notes m commit5 &&\n+\t# w/x/y/z unchanged\n+\tverify_notes w commit5 &&\n+\tverify_notes x commit5 &&\n+\tverify_notes y commit5 &&\n+\tverify_notes z commit5\n+'\n+\n+test_expect_success 'notes tree still has fanout after merge (m)' 'verify_fanout m'\n+\n+test_done\n-- \n1.7.3.98.g5ad7d9\n"},{"id":"154374","messageId":"1287965333-5099-22-git-send-email-johan@herland.net","threadId":"25543","inReplyTo":"1287965333-5099-1-git-send-email-johan@herland.net","subject":"[PATCHv5 21/23] Provide 'git notes get-ref' to easily retrieve current notes ref","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-10-25T00:08:51Z","receivedAt":"2010-10-25T00:08:51Z","isPatch":false,"sender":{"key":"johan@herland.net","avatar":"https://avatars.githubusercontent.com/u/547031?v=4"},"body":"Script may use 'git notes get-ref' to easily retrieve the current notes ref.\n\nSuggested-by: Jonathan Nieder <jrnieder@gmail.com>\nSigned-off-by: Johan Herland <johan@herland.net>\n---\n Documentation/git-notes.txt |    5 +++++\n builtin/notes.c             |   23 +++++++++++++++++++++++\n t/t3301-notes.sh            |   19 +++++++++++++++++++\n 3 files changed, 47 insertions(+), 0 deletions(-)\n\ndiff --git a/Documentation/git-notes.txt b/Documentation/git-notes.txt\nindex 1de1417..296f314 100644\n--- a/Documentation/git-notes.txt\n+++ b/Documentation/git-notes.txt\n@@ -19,6 +19,7 @@ SYNOPSIS\n 'git notes' merge --abort [-v | -q]\n 'git notes' remove [<object>]\n 'git notes' prune [-n | -v]\n+'git notes' get-ref\n \n \n DESCRIPTION\n@@ -109,6 +110,10 @@ remove::\n prune::\n \tRemove all notes for non-existing/unreachable objects.\n \n+get-ref::\n+\tPrint the current notes ref. This provides an easy way to\n+\tretrieve the current notes ref (e.g. from scripts).\n+\n OPTIONS\n -------\n -f::\ndiff --git a/builtin/notes.c b/builtin/notes.c\nindex 79783a5..ef52ffd 100644\n--- a/builtin/notes.c\n+++ b/builtin/notes.c\n@@ -31,6 +31,7 @@ static const char * const git_notes_usage[] = {\n \t\"git notes merge --abort [-v | -q]\",\n \t\"git notes [--ref <notes_ref>] remove [<object>]\",\n \t\"git notes [--ref <notes_ref>] prune [-n | -v]\",\n+\t\"git notes [--ref <notes_ref>] get-ref\",\n \tNULL\n };\n \n@@ -82,6 +83,11 @@ static const char * const git_notes_prune_usage[] = {\n \tNULL\n };\n \n+static const char * const git_notes_get_ref_usage[] = {\n+\t\"git notes get-ref\",\n+\tNULL\n+};\n+\n static const char note_template[] =\n \t\"\\n\"\n \t\"#\\n\"\n@@ -1002,6 +1008,21 @@ static int prune(int argc, const char **argv, const char *prefix)\n \treturn 0;\n }\n \n+static int get_ref(int argc, const char **argv, const char *prefix)\n+{\n+\tstruct option options[] = { OPT_END() };\n+\targc = parse_options(argc, argv, prefix, options,\n+\t\t\t     git_notes_get_ref_usage, 0);\n+\n+\tif (argc) {\n+\t\terror(\"too many parameters\");\n+\t\tusage_with_options(git_notes_get_ref_usage, options);\n+\t}\n+\n+\tputs(default_notes_ref());\n+\treturn 0;\n+}\n+\n int cmd_notes(int argc, const char **argv, const char *prefix)\n {\n \tint result;\n@@ -1040,6 +1061,8 @@ int cmd_notes(int argc, const char **argv, const char *prefix)\n \t\tresult = remove_cmd(argc, argv, prefix);\n \telse if (!strcmp(argv[0], \"prune\"))\n \t\tresult = prune(argc, argv, prefix);\n+\telse if (!strcmp(argv[0], \"get-ref\"))\n+\t\tresult = get_ref(argc, argv, prefix);\n \telse {\n \t\tresult = error(\"Unknown subcommand: %s\", argv[0]);\n \t\tusage_with_options(git_notes_usage, options);\ndiff --git a/t/t3301-notes.sh b/t/t3301-notes.sh\nindex 4bf4e52..f5b72c7 100755\n--- a/t/t3301-notes.sh\n+++ b/t/t3301-notes.sh\n@@ -1058,4 +1058,23 @@ test_expect_success 'git notes copy diagnoses too many or too few parameters' '\n \ttest_must_fail git notes copy one two three\n '\n \n+test_expect_success 'git notes get-ref (no overrides)' '\n+\tgit config --unset core.notesRef &&\n+\tunset GIT_NOTES_REF &&\n+\ttest \"$(git notes get-ref)\" = \"refs/notes/commits\"\n+'\n+\n+test_expect_success 'git notes get-ref (core.notesRef)' '\n+\tgit config core.notesRef refs/notes/foo &&\n+\ttest \"$(git notes get-ref)\" = \"refs/notes/foo\"\n+'\n+\n+test_expect_success 'git notes get-ref (GIT_NOTES_REF)' '\n+\ttest \"$(GIT_NOTES_REF=refs/notes/bar git notes get-ref)\" = \"refs/notes/bar\"\n+'\n+\n+test_expect_success 'git notes get-ref (--ref)' '\n+\ttest \"$(GIT_NOTES_REF=refs/notes/bar git notes --ref=baz get-ref)\" = \"refs/notes/baz\"\n+'\n+\n test_done\n-- \n1.7.3.98.g5ad7d9\n"},{"id":"154368","messageId":"1287965333-5099-23-git-send-email-johan@herland.net","threadId":"25543","inReplyTo":"1287965333-5099-1-git-send-email-johan@herland.net","subject":"[PATCHv5 22/23] cmd_merge(): Parse options before checking MERGE_HEAD","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-10-25T00:08:52Z","receivedAt":"2010-10-25T00:08:52Z","isPatch":false,"sender":{"key":"johan@herland.net","avatar":"https://avatars.githubusercontent.com/u/547031?v=4"},"body":"Reorder the initial part of builtin/merge.c:cmd_merge() so that command-line\noptions are parsed _before_ we load the index and check for MERGE_HEAD\n(and exits if it exists). This does not change the behaviour of 'git merge',\nbut is needed in preparation for the implementation of 'git merge --abort'\n(which requires MERGE_HEAD to be present).\n\nSigned-off-by: Johan Herland <johan@herland.net>\n---\n builtin/merge.c        |   33 ++++++++--------\n t/t7609-merge-abort.sh |   97 ++++++++++++++++++++++++++++++++++++++++++++++++\n 2 files changed, 114 insertions(+), 16 deletions(-)\n create mode 100755 t/t7609-merge-abort.sh\n\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex 37ce4f5..702f399 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -895,22 +895,6 @@ int cmd_merge(int argc, const char **argv, const char *prefix)\n \tconst char *best_strategy = NULL, *wt_strategy = NULL;\n \tstruct commit_list **remotes = &remoteheads;\n \n-\tif (read_cache_unmerged()) {\n-\t\tdie_resolve_conflict(\"merge\");\n-\t}\n-\tif (file_exists(git_path(\"MERGE_HEAD\"))) {\n-\t\t/*\n-\t\t * There is no unmerged entry, don't advise 'git\n-\t\t * add/rm <file>', just 'git commit'.\n-\t\t */\n-\t\tif (advice_resolve_conflict)\n-\t\t\tdie(\"You have not concluded your merge (MERGE_HEAD exists).\\n\"\n-\t\t\t    \"Please, commit your changes before you can merge.\");\n-\t\telse\n-\t\t\tdie(\"You have not concluded your merge (MERGE_HEAD exists).\");\n-\t}\n-\n-\tresolve_undo_clear();\n \t/*\n \t * Check if we are _not_ on a detached HEAD, i.e. if there is a\n \t * current branch.\n@@ -929,6 +913,23 @@ int cmd_merge(int argc, const char **argv, const char *prefix)\n \n \targc = parse_options(argc, argv, prefix, builtin_merge_options,\n \t\t\tbuiltin_merge_usage, 0);\n+\n+\tif (read_cache_unmerged()) {\n+\t\tdie_resolve_conflict(\"merge\");\n+\t}\n+\tif (file_exists(git_path(\"MERGE_HEAD\"))) {\n+\t\t/*\n+\t\t * There is no unmerged entry, don't advise 'git\n+\t\t * add/rm <file>', just 'git commit'.\n+\t\t */\n+\t\tif (advice_resolve_conflict)\n+\t\t\tdie(\"You have not concluded your merge (MERGE_HEAD exists).\\n\"\n+\t\t\t    \"Please, commit your changes before you can merge.\");\n+\t\telse\n+\t\t\tdie(\"You have not concluded your merge (MERGE_HEAD exists).\");\n+\t}\n+\tresolve_undo_clear();\n+\n \tif (verbosity < 0)\n \t\tshow_diffstat = 0;\n \ndiff --git a/t/t7609-merge-abort.sh b/t/t7609-merge-abort.sh\nnew file mode 100755\nindex 0000000..88d76e1\n--- /dev/null\n+++ b/t/t7609-merge-abort.sh\n@@ -0,0 +1,97 @@\n+#!/bin/sh\n+\n+test_description='test aborting in-progress merges'\n+. ./test-lib.sh\n+\n+# Test git merge --abort with the following variables:\n+# - before/after successful merge (i.e. should fail if not in merge context)\n+# - with/without conflicts\n+# - clean/dirty worktree before merge (may fail to reconstruct dirty worktree)\n+# - clean/dirty index before merge (merge should fail on dirty index)\n+# - changed/unchanged worktree after merge\n+# - changed/unchanged index after merge\n+\n+test_done\n+\n+test_expect_success 'fails without MERGE_HEAD (unstarted merge)' '\n+\ttest_must_fail git merge --abort 2>output &&\n+\tgrep -q MERGE_HEAD output\n+'\n+\n+test_expect_success 'fails without MERGE_HEAD (completed merge)' '\n+\ttest_commit master-1 &&\n+\ttest_commit master-2 &&\n+\tgit checkout -b side HEAD^ &&\n+\ttest_commit side-1 &&\n+\tgit checkout master &&\n+\tgit merge side &&\n+\t# Merge successfully completed\n+\ttest_must_fail git merge --abort 2>output &&\n+\tgrep -q MERGE_HEAD output\n+'\n+\n+test_expect_success 'Abort successfully after --no-commit' '\n+\t# Forget previous merge\n+\tgit reset --hard master^ &&\n+\thead=$(git rev-parse HEAD) &&\n+\tgit merge --no-commit side &&\n+\ttest -f .git/MERGE_HEAD &&\n+\tgit merge --abort &&\n+\ttest \"$head\" = \"$(git rev-parse HEAD)\" &&\n+\ttest -z \"$(git diff HEAD)\" &&\n+\ttest ! -f .git/MERGE_HEAD\n+'\n+\n+\n+\n+test_done\n+\n+test_expect_success 'merge local branch' '\n+\ttest_commit master-1 &&\n+\tgit checkout -b local-branch &&\n+\ttest_commit branch-1 &&\n+\tgit checkout master &&\n+\ttest_commit master-2 &&\n+\tgit merge local-branch &&\n+\tcheck_oneline \"Merge branch Qlocal-branchQ\"\n+'\n+\n+test_expect_success 'merge octopus branches' '\n+\tgit checkout -b octopus-a master &&\n+\ttest_commit octopus-1 &&\n+\tgit checkout -b octopus-b master &&\n+\ttest_commit octopus-2 &&\n+\tgit checkout master &&\n+\tgit merge octopus-a octopus-b &&\n+\tcheck_oneline \"Merge branches Qoctopus-aQ and Qoctopus-bQ\"\n+'\n+\n+test_expect_success 'merge tag' '\n+\tgit checkout -b tag-branch master &&\n+\ttest_commit tag-1 &&\n+\tgit checkout master &&\n+\ttest_commit master-3 &&\n+\tgit merge tag-1 &&\n+\tcheck_oneline \"Merge commit Qtag-1Q\"\n+'\n+\n+test_expect_success 'ambiguous tag' '\n+\tgit checkout -b ambiguous master &&\n+\ttest_commit ambiguous &&\n+\tgit checkout master &&\n+\ttest_commit master-4 &&\n+\tgit merge ambiguous &&\n+\tcheck_oneline \"Merge commit QambiguousQ\"\n+'\n+\n+test_expect_success 'remote branch' '\n+\tgit checkout -b remote master &&\n+\ttest_commit remote-1 &&\n+\tgit update-ref refs/remotes/origin/master remote &&\n+\tgit checkout master &&\n+\ttest_commit master-5 &&\n+\tgit merge origin/master &&\n+\tcheck_oneline \"Merge remote branch Qorigin/masterQ\"\n+'\n+\n+test_done\n-- \n1.7.3.98.g5ad7d9\n"},{"id":"154366","messageId":"1287965333-5099-24-git-send-email-johan@herland.net","threadId":"25543","inReplyTo":"1287965333-5099-1-git-send-email-johan@herland.net","subject":"[PATCHv5 23/23] Provide 'git merge --abort' as a synonym to 'git reset --merge'","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-10-25T00:08:53Z","receivedAt":"2010-10-25T00:08:53Z","isPatch":false,"sender":{"key":"johan@herland.net","avatar":"https://avatars.githubusercontent.com/u/547031?v=4"},"body":"Teach 'git merge' the --abort option, which verifies the existence of\nMERGE_HEAD and then invokes 'git reset --merge' to abort the current\nin-progress merge and attempt to reconstruct the pre-merge state.\n\nThe reason for adding this option is to provide a user interface for\naborting an in-progress merge that is consistent with the interface\nfor aborting a rebase ('git rebase --abort'), aborting the application\nof a patch series ('git am --abort'), and aborting an in-progress notes\nmerge ('git notes merge --abort').\n\nThe patch includes documentation and testcases that explain and verify\nthe various scenarios in which 'git merge --abort' can run. The\ntestcases also document the cases in which 'git merge --abort' is\nunable to correctly restore the pre-merge state (look for the '###'\ncomments towards the bottom of t/t7609-merge-abort.sh).\n\nSigned-off-by: Johan Herland <johan@herland.net>\n---\n Documentation/git-merge.txt |   29 ++++-\n builtin/merge.c             |   14 ++\n t/t7609-merge-abort.sh      |  290 ++++++++++++++++++++++++++++++++++---------\n 3 files changed, 273 insertions(+), 60 deletions(-)\n\ndiff --git a/Documentation/git-merge.txt b/Documentation/git-merge.txt\nindex 84043cc..d0099d3 100644\n--- a/Documentation/git-merge.txt\n+++ b/Documentation/git-merge.txt\n@@ -13,6 +13,7 @@ SYNOPSIS\n \t[-s <strategy>] [-X <strategy-option>]\n \t[--[no-]rerere-autoupdate] [-m <msg>] <commit>...\n 'git merge' <msg> HEAD <commit>...\n+'git merge' --abort\n \n DESCRIPTION\n -----------\n@@ -47,6 +48,15 @@ The second syntax (<msg> `HEAD` <commit>...) is supported for\n historical reasons.  Do not use it from the command line or in\n new scripts.  It is the same as `git merge -m <msg> <commit>...`.\n \n+The third syntax (\"`git merge --abort`\") can only be run after the\n+merge has resulted in conflicts. 'git merge --abort' will abort the\n+merge process and reconstruct the pre-merge state. However, if there\n+were uncommitted changes when the merge started (and these changes\n+did not interfere with the merge itself, otherwise the merge would\n+have refused to start), and then additional modifications were made\n+to these uncommitted changes, 'git merge --abort' will not be able\n+reconstruct the original (pre-merge) uncommitted changes. Therefore:\n+\n *Warning*: Running 'git merge' with uncommitted changes is\n discouraged: while possible, it leaves you in a state that is hard to\n back out of in the case of a conflict.\n@@ -72,6 +82,19 @@ include::merge-options.txt[]\n \tAllow the rerere mechanism to update the index with the\n \tresult of auto-conflict resolution if possible.\n \n+--abort::\n+\tAbort the current conflict resolution process, and\n+\treconstruct the pre-merge state.\n++\n+Any uncommitted worktree changes present when the merge started,\n+will only be preserved if they have not been further modified\n+since the merge started. Otherwise, git is unable to reconstruct\n+uncommitted changes. It is therefore recommended to always commit\n+or stash your changes before running 'git merge'.\n++\n+'git merge --abort' is equivalent to 'git reset --merge' when\n+`MERGE_HEAD` is present.\n+\n <commit>...::\n \tCommits, usually other branch heads, to merge into our branch.\n \tYou need at least one <commit>.  Specifying more than one\n@@ -142,7 +165,7 @@ happens:\n    i.e. matching `HEAD`.\n \n If you tried a merge which resulted in complex conflicts and\n-want to start over, you can recover with `git reset --merge`.\n+want to start over, you can recover with `git merge --abort`.\n \n HOW CONFLICTS ARE PRESENTED\n ---------------------------\n@@ -213,8 +236,8 @@ After seeing a conflict, you can do two things:\n \n  * Decide not to merge.  The only clean-ups you need are to reset\n    the index file to the `HEAD` commit to reverse 2. and to clean\n-   up working tree changes made by 2. and 3.; `git-reset --hard` can\n-   be used for this.\n+   up working tree changes made by 2. and 3.; `git merge --abort`\n+   can be used for this.\n \n  * Resolve the conflicts.  Git will mark the conflicts in\n    the working tree.  Edit the files into shape and\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex 702f399..fbe342f 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -56,6 +56,7 @@ static size_t xopts_nr, xopts_alloc;\n static const char *branch;\n static int verbosity;\n static int allow_rerere_auto;\n+static int abort_current_merge;\n \n static struct strategy all_strategy[] = {\n \t{ \"recursive\",  DEFAULT_TWOHEAD | NO_TRIVIAL },\n@@ -194,6 +195,8 @@ static struct option builtin_merge_options[] = {\n \t\t\"message to be used for the merge commit (if any)\",\n \t\toption_parse_message),\n \tOPT__VERBOSITY(&verbosity),\n+\tOPT_BOOLEAN(0, \"abort\", &abort_current_merge,\n+\t\t\"abort the current in-progress merge\"),\n \tOPT_END()\n };\n \n@@ -914,6 +917,17 @@ int cmd_merge(int argc, const char **argv, const char *prefix)\n \targc = parse_options(argc, argv, prefix, builtin_merge_options,\n \t\t\tbuiltin_merge_usage, 0);\n \n+\tif (abort_current_merge) {\n+\t\tint nargc = 2;\n+\t\tconst char *nargv[] = {\"reset\", \"--merge\", NULL};\n+\n+\t\tif (!file_exists(git_path(\"MERGE_HEAD\")))\n+\t\t\tdie(\"There is no merge to abort (MERGE_HEAD missing).\");\n+\n+\t\t/* Invoke 'git reset --merge' */\n+\t\treturn cmd_reset(nargc, nargv, prefix);\n+\t}\n+\n \tif (read_cache_unmerged()) {\n \t\tdie_resolve_conflict(\"merge\");\n \t}\ndiff --git a/t/t7609-merge-abort.sh b/t/t7609-merge-abort.sh\nindex 88d76e1..011a4c0 100755\n--- a/t/t7609-merge-abort.sh\n+++ b/t/t7609-merge-abort.sh\n@@ -3,95 +3,271 @@\n test_description='test aborting in-progress merges'\n . ./test-lib.sh\n \n+# Set up repo with conflicting and non-conflicting branches:\n+#\n+# master1---master2---foo_foo  <-- master\n+#        \\\n+#         --clean1             <-- clean_branch\n+#                 \\\n+#                  --foo_bar   <-- conflict_branch\n+\n+test_expect_success 'setup' '\n+\ttest_commit master1 &&\n+\tgit checkout -b clean_branch &&\n+\ttest_commit clean1 &&\n+\tgit checkout -b conflict_branch &&\n+\techo bar > foo &&\n+\tgit add foo &&\n+\tgit commit -m \"foo_bar\" &&\n+\tgit tag foo_bar &&\n+\tgit checkout master &&\n+\ttest_commit master2 &&\n+\techo foo > foo &&\n+\tgit add foo &&\n+\tgit commit -m \"foo_foo\" &&\n+\tgit tag foo_foo\n+'\n+\n # Test git merge --abort with the following variables:\n # - before/after successful merge (i.e. should fail if not in merge context)\n # - with/without conflicts\n+# - clean/dirty index before merge (merge fails on dirty index)\n # - clean/dirty worktree before merge (may fail to reconstruct dirty worktree)\n-# - clean/dirty index before merge (merge should fail on dirty index)\n+# - dirty worktree before merge matches contents on remote branch\n # - changed/unchanged worktree after merge\n # - changed/unchanged index after merge\n \n-test_done\n+pre_merge_head=\"$(git rev-parse HEAD)\"\n \n test_expect_success 'fails without MERGE_HEAD (unstarted merge)' '\n \ttest_must_fail git merge --abort 2>output &&\n-\tgrep -q MERGE_HEAD output\n+\tgrep -q MERGE_HEAD output &&\n+\ttest ! -f .git/MERGE_HEAD &&\n+\ttest \"$pre_merge_head\" = \"$(git rev-parse HEAD)\"\n '\n \n test_expect_success 'fails without MERGE_HEAD (completed merge)' '\n-\ttest_commit master-1 &&\n-\ttest_commit master-2 &&\n-\tgit checkout -b side HEAD^ &&\n-\ttest_commit side-1 &&\n-\tgit checkout master &&\n-\tgit merge side &&\n+\tgit merge clean_branch &&\n+\ttest ! -f .git/MERGE_HEAD &&\n \t# Merge successfully completed\n+\tpost_merge_head=\"$(git rev-parse HEAD)\" &&\n \ttest_must_fail git merge --abort 2>output &&\n-\tgrep -q MERGE_HEAD output\n+\tgrep -q MERGE_HEAD output &&\n+\ttest ! -f .git/MERGE_HEAD &&\n+\ttest \"$post_merge_head\" = \"$(git rev-parse HEAD)\"\n '\n \n-test_expect_success 'Abort successfully after --no-commit' '\n-\t# Forget previous merge\n-\tgit reset --hard master^ &&\n-\thead=$(git rev-parse HEAD) &&\n-\tgit merge --no-commit side &&\n+test_expect_success 'Forget previous merge' '\n+\tgit reset --hard \"$pre_merge_head\"\n+'\n+\n+test_expect_success 'Abort after --no-commit' '\n+\t# Redo merge, but stop before creating merge commit\n+\tgit merge --no-commit clean_branch &&\n \ttest -f .git/MERGE_HEAD &&\n+\t# Abort non-conflicting merge\n \tgit merge --abort &&\n-\ttest \"$head\" = \"$(git rev-parse HEAD)\" &&\n-\ttest -z \"$(git diff HEAD)\" &&\n-\ttest ! -f .git/MERGE_HEAD\n+\ttest ! -f .git/MERGE_HEAD &&\n+\ttest \"$pre_merge_head\" = \"$(git rev-parse HEAD)\" &&\n+\ttest -z \"$(git diff)\" &&\n+\ttest -z \"$(git diff --staged)\"\n '\n \n+test_expect_success 'Abort after conflicts' '\n+\t# Create conflicting merge\n+\ttest_must_fail git merge conflict_branch &&\n+\ttest -f .git/MERGE_HEAD &&\n+\t# Abort conflicting merge\n+\tgit merge --abort &&\n+\ttest ! -f .git/MERGE_HEAD &&\n+\ttest \"$pre_merge_head\" = \"$(git rev-parse HEAD)\" &&\n+\ttest -z \"$(git diff)\" &&\n+\ttest -z \"$(git diff --staged)\"\n+'\n \n+test_expect_success 'Clean merge with dirty index fails' '\n+\techo xyzzy >> foo &&\n+\tgit add foo &&\n+\tgit diff --staged > expect &&\n+\ttest_must_fail git merge clean_branch &&\n+\ttest ! -f .git/MERGE_HEAD &&\n+\ttest \"$pre_merge_head\" = \"$(git rev-parse HEAD)\" &&\n+\ttest -z \"$(git diff)\" &&\n+\tgit diff --staged > actual &&\n+\ttest_cmp expect actual\n+'\n \n-test_done\n+test_expect_success 'Conflicting merge with dirty index fails' '\n+\ttest_must_fail git merge conflict_branch &&\n+\ttest ! -f .git/MERGE_HEAD &&\n+\ttest \"$pre_merge_head\" = \"$(git rev-parse HEAD)\" &&\n+\ttest -z \"$(git diff)\" &&\n+\tgit diff --staged > actual &&\n+\ttest_cmp expect actual\n+'\n \n-test_expect_success 'merge local branch' '\n-\ttest_commit master-1 &&\n-\tgit checkout -b local-branch &&\n-\ttest_commit branch-1 &&\n-\tgit checkout master &&\n-\ttest_commit master-2 &&\n-\tgit merge local-branch &&\n-\tcheck_oneline \"Merge branch Qlocal-branchQ\"\n+test_expect_success 'Reset index (but preserve worktree changes)' '\n+\tgit reset \"$pre_merge_head\" &&\n+\tgit diff > actual &&\n+\ttest_cmp expect actual\n '\n \n-test_expect_success 'merge octopus branches' '\n-\tgit checkout -b octopus-a master &&\n-\ttest_commit octopus-1 &&\n-\tgit checkout -b octopus-b master &&\n-\ttest_commit octopus-2 &&\n-\tgit checkout master &&\n-\tgit merge octopus-a octopus-b &&\n-\tcheck_oneline \"Merge branches Qoctopus-aQ and Qoctopus-bQ\"\n+test_expect_success 'Abort clean merge with non-conflicting dirty worktree' '\n+\tgit merge --no-commit clean_branch &&\n+\ttest -f .git/MERGE_HEAD &&\n+\t# Abort merge\n+\tgit merge --abort &&\n+\ttest ! -f .git/MERGE_HEAD &&\n+\ttest \"$pre_merge_head\" = \"$(git rev-parse HEAD)\" &&\n+\ttest -z \"$(git diff --staged)\" &&\n+\tgit diff > actual &&\n+\ttest_cmp expect actual\n '\n \n-test_expect_success 'merge tag' '\n-\tgit checkout -b tag-branch master &&\n-\ttest_commit tag-1 &&\n-\tgit checkout master &&\n-\ttest_commit master-3 &&\n-\tgit merge tag-1 &&\n-\tcheck_oneline \"Merge commit Qtag-1Q\"\n+test_expect_success 'Reset worktree changes' '\n+\tgit reset --hard \"$pre_merge_head\" &&\n+\tgit clean -f\n '\n \n-test_expect_success 'ambiguous tag' '\n-\tgit checkout -b ambiguous master &&\n-\ttest_commit ambiguous &&\n-\tgit checkout master &&\n-\ttest_commit master-4 &&\n-\tgit merge ambiguous &&\n-\tcheck_oneline \"Merge commit QambiguousQ\"\n+test_expect_success 'Abort conflicting merge with non-conflicting dirty worktree' '\n+\techo xyzzy >> master1.t &&\n+\tgit diff > expect &&\n+\ttest_must_fail git merge conflict_branch &&\n+\ttest -f .git/MERGE_HEAD &&\n+\t# Abort merge\n+\tgit merge --abort &&\n+\ttest ! -f .git/MERGE_HEAD &&\n+\ttest \"$pre_merge_head\" = \"$(git rev-parse HEAD)\" &&\n+\ttest -z \"$(git diff --staged)\" &&\n+\tgit diff > actual &&\n+\ttest_cmp expect actual\n '\n \n-test_expect_success 'remote branch' '\n-\tgit checkout -b remote master &&\n-\ttest_commit remote-1 &&\n-\tgit update-ref refs/remotes/origin/master remote &&\n-\tgit checkout master &&\n-\ttest_commit master-5 &&\n-\tgit merge origin/master &&\n-\tcheck_oneline \"Merge remote branch Qorigin/masterQ\"\n+test_expect_success 'Reset worktree changes' '\n+\tgit reset --hard \"$pre_merge_head\" &&\n+\tgit clean -f\n+'\n+\n+test_expect_success 'Fail clean merge with conflicting dirty worktree' '\n+\techo xyzzy >> clean1.t &&\n+\tgit diff > expect &&\n+\ttest_must_fail git merge --no-commit clean_branch &&\n+\ttest ! -f .git/MERGE_HEAD &&\n+\ttest \"$pre_merge_head\" = \"$(git rev-parse HEAD)\" &&\n+\ttest -z \"$(git diff --staged)\" &&\n+\tgit diff > actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'Reset worktree changes' '\n+\tgit reset --hard \"$pre_merge_head\" &&\n+\tgit clean -f\n+'\n+\n+test_expect_success 'Fail clean merge with matching dirty worktree' '\n+\techo clean1 > clean1.t &&\n+\tgit diff > expect &&\n+\ttest_must_fail git merge --no-commit clean_branch &&\n+\ttest ! -f .git/MERGE_HEAD &&\n+\ttest \"$pre_merge_head\" = \"$(git rev-parse HEAD)\" &&\n+\ttest -z \"$(git diff --staged)\" &&\n+\tgit diff > actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'Reset worktree changes' '\n+\tgit reset --hard \"$pre_merge_head\" &&\n+\tgit clean -f\n+'\n+\n+test_expect_success 'Fail conflicting merge with conflicting dirty worktree' '\n+\techo xyzzy >> foo &&\n+\tgit diff > expect &&\n+\ttest_must_fail git merge conflict_branch &&\n+\ttest ! -f .git/MERGE_HEAD &&\n+\ttest \"$pre_merge_head\" = \"$(git rev-parse HEAD)\" &&\n+\ttest -z \"$(git diff --staged)\" &&\n+\tgit diff > actual &&\n+\ttest_cmp expect actual\n+'\n+test_expect_success 'Reset worktree changes' '\n+\tgit reset --hard \"$pre_merge_head\" &&\n+\tgit clean -f\n+'\n+\n+test_expect_success 'Fail conflicting merge with matching dirty worktree' '\n+\techo bar > foo &&\n+\tgit diff > expect &&\n+\ttest_must_fail git merge conflict_branch &&\n+\ttest ! -f .git/MERGE_HEAD &&\n+\ttest \"$pre_merge_head\" = \"$(git rev-parse HEAD)\" &&\n+\ttest -z \"$(git diff --staged)\" &&\n+\tgit diff > actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'Reset worktree changes' '\n+\tgit reset --hard \"$pre_merge_head\" &&\n+\tgit clean -f\n+'\n+\n+test_expect_success 'Abort merge with pre- and post-merge worktree changes' '\n+\t# Pre-merge worktree changes\n+\techo xyzzy >> master1.t &&\n+\tgit diff > expect &&\n+\t# Perform merge\n+\ttest_must_fail git merge conflict_branch &&\n+\ttest -f .git/MERGE_HEAD &&\n+\t# Post-merge worktree changes\n+\techo barf >> master1.t &&\n+\techo barf > foo &&\n+\t### When aborting the merge, git can reconstruct foo, but for non-\n+\t### conflicting files it cannot tell pre-merge changes apart from\n+\t### post-merge changes. Therefore, the master1.t file will reflect\n+\t### the post-merge state, while the foo file will reflect the\n+\t### pre-merge (unchanged) state. Change expectations accordingly:\n+\tgit diff master1.t > expect &&\n+\t# Abort merge\n+\tgit merge --abort &&\n+\ttest ! -f .git/MERGE_HEAD &&\n+\ttest \"$pre_merge_head\" = \"$(git rev-parse HEAD)\" &&\n+\ttest -z \"$(git diff --staged)\" &&\n+\tgit diff > actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'Reset worktree changes' '\n+\tgit reset --hard \"$pre_merge_head\" &&\n+\tgit clean -f\n+'\n+\n+test_expect_success 'Abort merge with pre-merge worktree changes and post-merge index changes' '\n+\t# Pre-merge worktree changes\n+\techo xyzzy >> master1.t &&\n+\techo xyzzy >> master2.t &&\n+\t### See explanation below for why we only expect the diff of master2.t\n+\tgit diff master2.t > expect &&\n+\t# Perform merge\n+\ttest_must_fail git merge conflict_branch &&\n+\ttest -f .git/MERGE_HEAD &&\n+\t# Post-merge worktree changes\n+\techo barf >> master1.t &&\n+\techo barf > foo &&\n+\tgit add master1.t foo &&\n+\t### When aborting the merge, git will remove all staged changes,\n+\t### including those that were staged post-merge. For any post-merge\n+\t### changes that have overwritten pre-merge changes, git cannot\n+\t### reconstruct the pre-merge changes. In this scenario, foo and\n+\t### master1.t are reset to a clean state (i.e. losing the pre-merge\n+\t### changes in master1.t), but the pre-merge changes in master2.t are\n+\t### preserved (since it was not changed post-merge).\n+\t# Abort merge\n+\tgit merge --abort &&\n+\ttest ! -f .git/MERGE_HEAD &&\n+\ttest \"$pre_merge_head\" = \"$(git rev-parse HEAD)\" &&\n+\ttest -z \"$(git diff --staged)\" &&\n+\tgit diff > actual &&\n+\ttest_cmp expect actual\n '\n \n test_done\n-- \n1.7.3.98.g5ad7d9\n"},{"id":"154378","messageId":"20101025013217.GA25355@burratino","threadId":"25543","inReplyTo":"1287965333-5099-24-git-send-email-johan@herland.net","subject":"Re: [PATCHv5 23/23] Provide 'git merge --abort' as a synonym to 'git reset --merge'","fromName":"Jonathan Nieder","fromEmail":"jrnieder@gmail.com","sentAt":"2010-10-25T01:32:18Z","receivedAt":"2010-10-25T01:32:18Z","isPatch":false,"sender":{"key":"jrnieder@gmail.com","avatar":"https://avatars.githubusercontent.com/u/281595?v=4"},"body":"Johan Herland wrote:\n\n> --- a/Documentation/git-merge.txt\n> +++ b/Documentation/git-merge.txt\n> @@ -13,6 +13,7 @@ SYNOPSIS\n>                                                     However, if there\n> +were uncommitted changes when the merge started (and these changes\n> +did not interfere with the merge itself, otherwise the merge would\n> +have refused to start), and then additional modifications were made\n> +to these uncommitted changes, 'git merge --abort' will not be able\n> +reconstruct the original (pre-merge) uncommitted changes. Therefore:\n\nI do not find this clear.  Could you give an example?\n\nReferences:\n\n http://thread.gmane.org/gmane.comp.version-control.git/136356/focus=136773\n http://thread.gmane.org/gmane.comp.version-control.git/151799/focus=151812\n\n> +--abort::\n> +\tAbort the current conflict resolution process, and\n> +\treconstruct the pre-merge state.\n> ++\n> +Any uncommitted worktree changes present when the merge started,\n> +will only be preserved if they have not been further modified\n> +since the merge started.\n\nAh, maybe I see: is the problem this procedure?\n\n 1. Make changes to file foo.c (without staging them).\n 2. Try a merge (which cannot touch foo.c, or the merge would have\n    been aborted automatically) which fails with conflicts.\n 3. As a result of semantic conflicts, make some changes to foo.c.\n 4. Wish to return to the state from the end of step 1.\n\nBut I find the following more likely:\n\n 1. Make changes to file foo.c (without staging them).\n 2. Try a merge (which cannot touch foo.c, or the merge would have\n    been aborted automatically) which fails with conflicts.\n 3. Walk away in disgust.\n 4. Return, make some more changes to foo.c.\n 5. Notice the merge in progress --- oh! --- and abort it.\n\n> --- a/t/t7609-merge-abort.sh\n> +++ b/t/t7609-merge-abort.sh\n> @@ -3,95 +3,271 @@\n>  test_description='test aborting in-progress merges'\n>  . ./test-lib.sh\n>  \n> +# Set up repo with conflicting and non-conflicting branches:\n> +#\n> +# master1---master2---foo_foo  <-- master\n> +#        \\\n> +#         --clean1             <-- clean_branch\n> +#                 \\\n> +#                  --foo_bar   <-- conflict_branch\n\n[It might be nice to include this in test_description for use by\n \"./t7609-merge-abort.sh --help\".]\n\n> +# - dirty worktree before merge matches contents on remote branch\n\nOr maybe this was the example.  Here was Junio's explanation of it:\n\n| It will discard the change, the one you independently picked up, but the\n| change agreed with what was done by the the trash history that you are\n| cancelling merge with.  You wouldn't miss losing the same change as in\n| that trash history.\n\nIn other words, if the change is also on a remote branch that you want\nnot to merge with anyway, it is not likely to be terribly important to\npreserve it in the local tree.  (This is a trade-off between\nconvenience in two different scenarios.)\n\nHope that helps (sorry for the ramble).\n"},{"id":"154393","messageId":"201010251203.00389.johan@herland.net","threadId":"25543","inReplyTo":"20101025013217.GA25355@burratino","subject":"Re: [PATCHv5 23/23] Provide 'git merge --abort' as a synonym to 'git reset --merge'","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-10-25T10:02:59Z","receivedAt":"2010-10-25T10:02:59Z","isPatch":false,"sender":{"key":"johan@herland.net","avatar":"https://avatars.githubusercontent.com/u/547031?v=4"},"body":"On Monday 25 October 2010, Jonathan Nieder wrote:\n> Johan Herland wrote:\n> > --- a/Documentation/git-merge.txt\n> > +++ b/Documentation/git-merge.txt\n> > @@ -13,6 +13,7 @@ SYNOPSIS\n> > \n> >                                                     However, if there\n> > +were uncommitted changes when the merge started (and these changes\n> > +did not interfere with the merge itself, otherwise the merge would\n> > +have refused to start), and then additional modifications were made\n> > +to these uncommitted changes, 'git merge --abort' will not be able\n> > +reconstruct the original (pre-merge) uncommitted changes. Therefore:\n>\n> I do not find this clear.  Could you give an example?\n\nI'll try. Given these commands:\n\n  git init\n  echo foo > foo\n  echo bar > bar\n  echo baz > baz\n  git add foo bar baz\n  git commit -m initial\n  git checkout -b side\n  echo blarg > bar\n  echo blazg > baz\n  git commit -a -m \"side\"\n  git checkout master\n  echo xyzzy > bar\n  git commit -a -m \"xyzzy\"\n\nWe now have the following repo, in terms of bar/baz's contents:\n\n  bar/baz---xyzzy/baz    <-- master\n         \\\n          --blarg/blazg  <-- side\n\nNow, before we merge side into master, consider what uncommitted changes we \ncan make before starting the merge:\n\n  A. (no changes)\n\n  B. Change foo to something different [echo blech > foo]\n\n  C. Same as (B), plus stage foo [git add foo]\n\n  D. Change bar to something different [echo blech > bar]\n\n  E. Same as (D), plus stage bar [git add bar]\n\n  F. Change baz to something different [echo blech > baz]\n\n  G. Same as (F), plus stage baz [git add baz]\n\n  H. Change baz to \"blazg\" [echo blazg > baz]\n\n  I. Same as (H), plus stage baz [git add baz]\n\nAFAICS, this is a complete list of pre-merge uncommitted changes we can \nmake. Everything else is either uninteresting, or can be classified in terms \nof (a combination of) the above 9 cases.\n\nNow, we can immediately disregard cases (C), (D), (E), (F), (G) and (H), \nsince 'git merge' refuses to start merging:\n\n  $ git merge side\n  error: Your local changes to the following files would be overwritten by\n  merge:\n          foo/bar/baz\n  Please, commit your changes or stash them before you can merge.\n  Aborting\n\nSo we're left with considering cases (A), (B) and (I). These cases are \npreserved by the merge, in that:\n\n  A. Non-changes are still non-changes after the merge.\n\n  B. Uncommitted changes are still uncommitted changes.\n\n  I. Staged changes (matching the other branch) are still staged changes.\n\nNow, my experiments in t7609 (although I now see that I have failed to \nproperly recreate case (I); this will be fixed in the next iteration) \ndemonstrate that 'git merge --abort' will preserve uncommitted changes, and \ndiscard staged changes. As such cases (A) and (B) will be preserved by 'git \nmerge --abort', while case (I) will be lost.\n\nFurthermore, experiments (in t7609) show that 'git merge --abort' is unable \nto identify any changes done after the merge attempt and before 'git merge \n--abort'. As such, if you make any more staged or unstaged changes, they \nwill be handled by 'git merge --abort' as though they happened before the \nmerge (i.e. unstaged changes are preserved, staged changes are discarded).\n\nI should therefore update the documentation to be a little broader in its \nwarning against merging with uncommitted changes. Maybe something like:\n\n  However, if there were uncommitted changes when the merge started,\n  'git merge --abort' will in some cases be unable to recreate these\n  changes. Especially if further changes were made after the merge was\n  started, 'git merge --abort' is unable to reconstruct the original\n  (pre-merge) changes. Therefore:\n\n> References:\n> \n> http://thread.gmane.org/gmane.comp.version-control.git/136356/focus=136773\n> http://thread.gmane.org/gmane.comp.version-control.git/151799/focus=151812\n\nThanks. I hadn't read those before preparing these. Instead I tried to \ndocument the current behaviour by testing all scenarios (that I could think \nof) in t7609, and documenting the results, extensively in t7609 and somewhat \nmore abbreviated in the man page.\n\n> > +--abort::\n> > +\tAbort the current conflict resolution process, and\n> > +\treconstruct the pre-merge state.\n> > ++\n> > +Any uncommitted worktree changes present when the merge started,\n> > +will only be preserved if they have not been further modified\n> > +since the merge started.\n> \n> Ah, maybe I see: is the problem this procedure?\n> \n>  1. Make changes to file foo.c (without staging them).\n>  2. Try a merge (which cannot touch foo.c, or the merge would have\n>     been aborted automatically) which fails with conflicts.\n>  3. As a result of semantic conflicts, make some changes to foo.c.\n>  4. Wish to return to the state from the end of step 1.\n\nExactly. That's one of the cases presented above.\n\n> But I find the following more likely:\n> \n>  1. Make changes to file foo.c (without staging them).\n>  2. Try a merge (which cannot touch foo.c, or the merge would have\n>     been aborted automatically) which fails with conflicts.\n>  3. Walk away in disgust.\n>  4. Return, make some more changes to foo.c.\n>  5. Notice the merge in progress --- oh! --- and abort it.\n\nYeah, in that case the current behaviour will actually work for you, as long \nas you DON'T stage your changes (staged + abort => changes lost, d'oh).\n\n> > --- a/t/t7609-merge-abort.sh\n> > +++ b/t/t7609-merge-abort.sh\n> > @@ -3,95 +3,271 @@\n> > \n> >  test_description='test aborting in-progress merges'\n> >  . ./test-lib.sh\n> > \n> > +# Set up repo with conflicting and non-conflicting branches:\n> > +#\n> > +# master1---master2---foo_foo  <-- master\n> > +#        \\\n> > +#         --clean1             <-- clean_branch\n> > +#                 \\\n> > +#                  --foo_bar   <-- conflict_branch\n> \n> [It might be nice to include this in test_description for use by\n>  \"./t7609-merge-abort.sh --help\".]\n\nAgreed. Will fix.\n\n> > +# - dirty worktree before merge matches contents on remote branch\n> \n> Or maybe this was the example.  Here was Junio's explanation of it:\n> | It will discard the change, the one you independently picked up, but\n> | the change agreed with what was done by the the trash history that you\n> | are cancelling merge with.  You wouldn't miss losing the same change\n> | as in that trash history.\n\nYeah, but I've now found that I failed to test that case. Will be fixed in \nthe next iteration.\n\n> In other words, if the change is also on a remote branch that you want\n> not to merge with anyway, it is not likely to be terribly important to\n> preserve it in the local tree.  (This is a trade-off between\n> convenience in two different scenarios.)\n\nYeah, it's an argument that will work in some situations, but not all. Say \nyou happend to stage something after the merge, but before you 'git merge --\nabort'. That change will be obliterated by the --abort, which is likely not \nwhat you wanted.\n\n> Hope that helps (sorry for the ramble).\n\nThat was _immensely_ helpful, since it cleared my own jumbled thoughts on \nthe matter, and identified a missing/miscreated case in t7609. Thanks!\n\n\n...Johan\n\n-- \nJohan Herland, <johan@herland.net>\nwww.herland.net\n"},{"id":"154431","messageId":"201010260326.33200.johan@herland.net","threadId":"25543","inReplyTo":"201010251203.00389.johan@herland.net","subject":"Re: [PATCHv5 23/23] Provide 'git merge --abort' as a synonym to 'git reset --merge'","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-10-26T01:26:32Z","receivedAt":"2010-10-26T01:26:32Z","isPatch":false,"sender":{"key":"johan@herland.net","avatar":"https://avatars.githubusercontent.com/u/547031?v=4"},"body":"On Monday 25 October 2010, Johan Herland wrote:\n> On Monday 25 October 2010, Jonathan Nieder wrote:\n> > Johan Herland wrote:\n> > >                                                  However, if there\n> > > +were uncommitted changes when the merge started (and these changes\n> > > +did not interfere with the merge itself, otherwise the merge would\n> > > +have refused to start), and then additional modifications were made\n> > > +to these uncommitted changes, 'git merge --abort' will not be able\n> > \n> > > +reconstruct the original (pre-merge) uncommitted changes. Therefore:\n> > I do not find this clear.  Could you give an example?\n\n[...]\n\n> > > +# - dirty worktree before merge matches contents on remote branch\n> > \n> > Or maybe this was the example.  Here was Junio's explanation of it:\n> > | It will discard the change, the one you independently picked up, but\n> > | the change agreed with what was done by the the trash history that\n> > | you are cancelling merge with.  You wouldn't miss losing the same\n> > | change as in that trash history.\n> \n> Yeah, but I've now found that I failed to test that case. Will be fixed\n> in the next iteration.\n\nInstead of sending an entire new patch series when I've only changed the \nlast two patches, I'm gonna send only those two patches, marked \"PATCHv5.1\", \nas a reply to this email.\n\n\nHope this is ok,\n\n...Johan\n\n-- \nJohan Herland, <johan@herland.net>\nwww.herland.net\n"},{"id":"154432","messageId":"201010260330.29194.johan@herland.net","threadId":"25543","inReplyTo":"201010260326.33200.johan@herland.net","subject":"[PATCHv5.1 22/23] cmd_merge(): Parse options before checking MERGE_HEAD","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-10-26T01:30:27Z","receivedAt":"2010-10-26T01:30:27Z","isPatch":false,"sender":{"key":"johan@herland.net","avatar":"https://avatars.githubusercontent.com/u/547031?v=4"},"body":"Reorder the initial part of builtin/merge.c:cmd_merge() so that command-line\noptions are parsed _before_ we load the index and check for MERGE_HEAD\n(and exits if it exists). This does not change the behaviour of 'git merge',\nbut is needed in preparation for the implementation of 'git merge --abort'\n(which requires MERGE_HEAD to be present).\n\nSigned-off-by: Johan Herland <johan@herland.net>\n---\n\nIn the previous version of this patch, I somehow managed to misplace\nparts of t7609 here instead of in the next commit. Fixed now.\n\nOtherwise, no changes.\n\n\n...Johan\n\n builtin/merge.c |   33 +++++++++++++++++----------------\n 1 files changed, 17 insertions(+), 16 deletions(-)\n\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex 37ce4f5..702f399 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -895,22 +895,6 @@ int cmd_merge(int argc, const char **argv, const char *prefix)\n \tconst char *best_strategy = NULL, *wt_strategy = NULL;\n \tstruct commit_list **remotes = &remoteheads;\n \n-\tif (read_cache_unmerged()) {\n-\t\tdie_resolve_conflict(\"merge\");\n-\t}\n-\tif (file_exists(git_path(\"MERGE_HEAD\"))) {\n-\t\t/*\n-\t\t * There is no unmerged entry, don't advise 'git\n-\t\t * add/rm <file>', just 'git commit'.\n-\t\t */\n-\t\tif (advice_resolve_conflict)\n-\t\t\tdie(\"You have not concluded your merge (MERGE_HEAD exists).\\n\"\n-\t\t\t    \"Please, commit your changes before you can merge.\");\n-\t\telse\n-\t\t\tdie(\"You have not concluded your merge (MERGE_HEAD exists).\");\n-\t}\n-\n-\tresolve_undo_clear();\n \t/*\n \t * Check if we are _not_ on a detached HEAD, i.e. if there is a\n \t * current branch.\n@@ -929,6 +913,23 @@ int cmd_merge(int argc, const char **argv, const char *prefix)\n \n \targc = parse_options(argc, argv, prefix, builtin_merge_options,\n \t\t\tbuiltin_merge_usage, 0);\n+\n+\tif (read_cache_unmerged()) {\n+\t\tdie_resolve_conflict(\"merge\");\n+\t}\n+\tif (file_exists(git_path(\"MERGE_HEAD\"))) {\n+\t\t/*\n+\t\t * There is no unmerged entry, don't advise 'git\n+\t\t * add/rm <file>', just 'git commit'.\n+\t\t */\n+\t\tif (advice_resolve_conflict)\n+\t\t\tdie(\"You have not concluded your merge (MERGE_HEAD exists).\\n\"\n+\t\t\t    \"Please, commit your changes before you can merge.\");\n+\t\telse\n+\t\t\tdie(\"You have not concluded your merge (MERGE_HEAD exists).\");\n+\t}\n+\tresolve_undo_clear();\n+\n \tif (verbosity < 0)\n \t\tshow_diffstat = 0;\n \n-- \n1.7.3.2.173.gab1c9.dirty\n"},{"id":"154433","messageId":"201010260334.51162.johan@herland.net","threadId":"25543","inReplyTo":"201010260326.33200.johan@herland.net","subject":"[PATCHv5.1 23/23] Provide 'git merge --abort' as a synonym to 'git reset --merge'","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-10-26T01:34:50Z","receivedAt":"2010-10-26T01:34:50Z","isPatch":false,"sender":{"key":"johan@herland.net","avatar":"https://avatars.githubusercontent.com/u/547031?v=4"},"body":"Teach 'git merge' the --abort option, which verifies the existence of\nMERGE_HEAD and then invokes 'git reset --merge' to abort the current\nin-progress merge and attempt to reconstruct the pre-merge state.\n\nThe reason for adding this option is to provide a user interface for\naborting an in-progress merge that is consistent with the interface\nfor aborting a rebase ('git rebase --abort'), aborting the application\nof a patch series ('git am --abort'), and aborting an in-progress notes\nmerge ('git notes merge --abort').\n\nThe patch includes documentation and testcases that explain and verify\nthe various scenarios in which 'git merge --abort' can run. The\ntestcases also document the cases in which 'git merge --abort' is\nunable to correctly restore the pre-merge state (look for the '###'\ncomments towards the bottom of t/t7609-merge-abort.sh).\n\nThis patch has been improved by the following contributions:\n- Jonathan Nieder: Move test documentation into test_description\n\nThanks-to: Jonathan Nieder <jrnieder@gmail.com>\nSigned-off-by: Johan Herland <johan@herland.net>\n---\n\nHi,\n\nThe documentation and test cases in this patch has been extensively\nupdated. The code in builtin/merge.s is unchanged.\n\nAgain, thanks to Jonathan Nieder for extremely helpful reviews.\n\n\nHave fun! :)\n\n...Johan\n\n Documentation/git-merge.txt |   27 ++++-\n builtin/merge.c             |   14 ++\n t/t7609-merge-abort.sh      |  313 +++++++++++++++++++++++++++++++++++++++++++\n 3 files changed, 351 insertions(+), 3 deletions(-)\n create mode 100755 t/t7609-merge-abort.sh\n\ndiff --git a/Documentation/git-merge.txt b/Documentation/git-merge.txt\nindex 84043cc..47b59a5 100644\n--- a/Documentation/git-merge.txt\n+++ b/Documentation/git-merge.txt\n@@ -13,6 +13,7 @@ SYNOPSIS\n \t[-s <strategy>] [-X <strategy-option>]\n \t[--[no-]rerere-autoupdate] [-m <msg>] <commit>...\n 'git merge' <msg> HEAD <commit>...\n+'git merge' --abort\n \n DESCRIPTION\n -----------\n@@ -47,6 +48,14 @@ The second syntax (<msg> `HEAD` <commit>...) is supported for\n historical reasons.  Do not use it from the command line or in\n new scripts.  It is the same as `git merge -m <msg> <commit>...`.\n \n+The third syntax (\"`git merge --abort`\") can only be run after the\n+merge has resulted in conflicts. 'git merge --abort' will abort the\n+merge process and try to reconstruct the pre-merge state. However,\n+if there were uncommitted changes when the merge started (and\n+especially if those changes were further modified after the merge\n+was started), 'git merge --abort' will in some cases be unable to\n+reconstruct the original (pre-merge) changes. Therefore:\n+\n *Warning*: Running 'git merge' with uncommitted changes is\n discouraged: while possible, it leaves you in a state that is hard to\n back out of in the case of a conflict.\n@@ -72,6 +81,18 @@ include::merge-options.txt[]\n \tAllow the rerere mechanism to update the index with the\n \tresult of auto-conflict resolution if possible.\n \n+--abort::\n+\tAbort the current conflict resolution process, and\n+\ttry to reconstruct the pre-merge state.\n++\n+If there were uncommitted worktree changes present when the merge\n+started, 'git merge --abort' will in some cases be unable to\n+reconstruct these changes. It is therefore recommended to always\n+commit or stash your changes before running 'git merge'.\n++\n+'git merge --abort' is equivalent to 'git reset --merge' when\n+`MERGE_HEAD` is present.\n+\n <commit>...::\n \tCommits, usually other branch heads, to merge into our branch.\n \tYou need at least one <commit>.  Specifying more than one\n@@ -142,7 +163,7 @@ happens:\n    i.e. matching `HEAD`.\n \n If you tried a merge which resulted in complex conflicts and\n-want to start over, you can recover with `git reset --merge`.\n+want to start over, you can recover with `git merge --abort`.\n \n HOW CONFLICTS ARE PRESENTED\n ---------------------------\n@@ -213,8 +234,8 @@ After seeing a conflict, you can do two things:\n \n  * Decide not to merge.  The only clean-ups you need are to reset\n    the index file to the `HEAD` commit to reverse 2. and to clean\n-   up working tree changes made by 2. and 3.; `git-reset --hard` can\n-   be used for this.\n+   up working tree changes made by 2. and 3.; `git merge --abort`\n+   can be used for this.\n \n  * Resolve the conflicts.  Git will mark the conflicts in\n    the working tree.  Edit the files into shape and\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex 702f399..fbe342f 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -56,6 +56,7 @@ static size_t xopts_nr, xopts_alloc;\n static const char *branch;\n static int verbosity;\n static int allow_rerere_auto;\n+static int abort_current_merge;\n \n static struct strategy all_strategy[] = {\n \t{ \"recursive\",  DEFAULT_TWOHEAD | NO_TRIVIAL },\n@@ -194,6 +195,8 @@ static struct option builtin_merge_options[] = {\n \t\t\"message to be used for the merge commit (if any)\",\n \t\toption_parse_message),\n \tOPT__VERBOSITY(&verbosity),\n+\tOPT_BOOLEAN(0, \"abort\", &abort_current_merge,\n+\t\t\"abort the current in-progress merge\"),\n \tOPT_END()\n };\n \n@@ -914,6 +917,17 @@ int cmd_merge(int argc, const char **argv, const char *prefix)\n \targc = parse_options(argc, argv, prefix, builtin_merge_options,\n \t\t\tbuiltin_merge_usage, 0);\n \n+\tif (abort_current_merge) {\n+\t\tint nargc = 2;\n+\t\tconst char *nargv[] = {\"reset\", \"--merge\", NULL};\n+\n+\t\tif (!file_exists(git_path(\"MERGE_HEAD\")))\n+\t\t\tdie(\"There is no merge to abort (MERGE_HEAD missing).\");\n+\n+\t\t/* Invoke 'git reset --merge' */\n+\t\treturn cmd_reset(nargc, nargv, prefix);\n+\t}\n+\n \tif (read_cache_unmerged()) {\n \t\tdie_resolve_conflict(\"merge\");\n \t}\ndiff --git a/t/t7609-merge-abort.sh b/t/t7609-merge-abort.sh\nnew file mode 100755\nindex 0000000..61890bc\n--- /dev/null\n+++ b/t/t7609-merge-abort.sh\n@@ -0,0 +1,313 @@\n+#!/bin/sh\n+\n+test_description='test aborting in-progress merges\n+\n+Set up repo with conflicting and non-conflicting branches:\n+\n+There are three files foo/bar/baz, and the following graph illustrates the\n+content of these files in each commit:\n+\n+# foo/bar/baz --- foo/bar/bazz     <-- master\n+#             \\\n+#              --- foo/barf/bazf   <-- conflict_branch\n+#               \\\n+#                --- foo/bart/baz  <-- clean_branch\n+\n+Next, test git merge --abort with the following variables:\n+- before/after successful merge (should fail when not in merge context)\n+- with/without conflicts\n+- clean/dirty index before merge\n+- clean/dirty worktree before merge\n+- dirty index before merge matches contents on remote branch\n+- changed/unchanged worktree after merge\n+- changed/unchanged index after merge\n+'\n+. ./test-lib.sh\n+\n+test_expect_success 'setup' '\n+\t# Create the above repo\n+\techo foo > foo &&\n+\techo bar > bar &&\n+\techo baz > baz &&\n+\tgit add foo bar baz &&\n+\tgit commit -m initial &&\n+\techo bazz > baz &&\n+\tgit commit -a -m \"second\" &&\n+\tgit checkout -b conflict_branch HEAD^ &&\n+\techo barf > bar &&\n+\techo bazf > baz &&\n+\tgit commit -a -m \"conflict\" &&\n+\tgit checkout -b clean_branch HEAD^ &&\n+\techo bart > bar &&\n+\tgit commit -a -m \"clean\" &&\n+\tgit checkout master\n+'\n+\n+pre_merge_head=\"$(git rev-parse HEAD)\"\n+\n+test_expect_success 'fails without MERGE_HEAD (unstarted merge)' '\n+\ttest_must_fail git merge --abort 2>output &&\n+\tgrep -q MERGE_HEAD output &&\n+\ttest ! -f .git/MERGE_HEAD &&\n+\ttest \"$pre_merge_head\" = \"$(git rev-parse HEAD)\"\n+'\n+\n+test_expect_success 'fails without MERGE_HEAD (completed merge)' '\n+\tgit merge clean_branch &&\n+\ttest ! -f .git/MERGE_HEAD &&\n+\t# Merge successfully completed\n+\tpost_merge_head=\"$(git rev-parse HEAD)\" &&\n+\ttest_must_fail git merge --abort 2>output &&\n+\tgrep -q MERGE_HEAD output &&\n+\ttest ! -f .git/MERGE_HEAD &&\n+\ttest \"$post_merge_head\" = \"$(git rev-parse HEAD)\"\n+'\n+\n+test_expect_success 'Forget previous merge' '\n+\tgit reset --hard \"$pre_merge_head\"\n+'\n+\n+test_expect_success 'Abort after --no-commit' '\n+\t# Redo merge, but stop before creating merge commit\n+\tgit merge --no-commit clean_branch &&\n+\ttest -f .git/MERGE_HEAD &&\n+\t# Abort non-conflicting merge\n+\tgit merge --abort &&\n+\ttest ! -f .git/MERGE_HEAD &&\n+\ttest \"$pre_merge_head\" = \"$(git rev-parse HEAD)\" &&\n+\ttest -z \"$(git diff)\" &&\n+\ttest -z \"$(git diff --staged)\"\n+'\n+\n+test_expect_success 'Abort after conflicts' '\n+\t# Create conflicting merge\n+\ttest_must_fail git merge conflict_branch &&\n+\ttest -f .git/MERGE_HEAD &&\n+\t# Abort conflicting merge\n+\tgit merge --abort &&\n+\ttest ! -f .git/MERGE_HEAD &&\n+\ttest \"$pre_merge_head\" = \"$(git rev-parse HEAD)\" &&\n+\ttest -z \"$(git diff)\" &&\n+\ttest -z \"$(git diff --staged)\"\n+'\n+\n+test_expect_success 'Clean merge with dirty index fails' '\n+\techo xyzzy >> foo &&\n+\tgit add foo &&\n+\tgit diff --staged > expect &&\n+\ttest_must_fail git merge clean_branch &&\n+\ttest ! -f .git/MERGE_HEAD &&\n+\ttest \"$pre_merge_head\" = \"$(git rev-parse HEAD)\" &&\n+\ttest -z \"$(git diff)\" &&\n+\tgit diff --staged > actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'Conflicting merge with dirty index fails' '\n+\ttest_must_fail git merge conflict_branch &&\n+\ttest ! -f .git/MERGE_HEAD &&\n+\ttest \"$pre_merge_head\" = \"$(git rev-parse HEAD)\" &&\n+\ttest -z \"$(git diff)\" &&\n+\tgit diff --staged > actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'Reset index (but preserve worktree changes)' '\n+\tgit reset \"$pre_merge_head\" &&\n+\tgit diff > actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'Abort clean merge with non-conflicting dirty worktree' '\n+\tgit merge --no-commit clean_branch &&\n+\ttest -f .git/MERGE_HEAD &&\n+\t# Abort merge\n+\tgit merge --abort &&\n+\ttest ! -f .git/MERGE_HEAD &&\n+\ttest \"$pre_merge_head\" = \"$(git rev-parse HEAD)\" &&\n+\ttest -z \"$(git diff --staged)\" &&\n+\tgit diff > actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'Abort conflicting merge with non-conflicting dirty worktree' '\n+\ttest_must_fail git merge conflict_branch &&\n+\ttest -f .git/MERGE_HEAD &&\n+\t# Abort merge\n+\tgit merge --abort &&\n+\ttest ! -f .git/MERGE_HEAD &&\n+\ttest \"$pre_merge_head\" = \"$(git rev-parse HEAD)\" &&\n+\ttest -z \"$(git diff --staged)\" &&\n+\tgit diff > actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'Reset worktree changes' '\n+\tgit reset --hard \"$pre_merge_head\"\n+'\n+\n+test_expect_success 'Fail clean merge with conflicting dirty worktree' '\n+\techo xyzzy >> bar &&\n+\tgit diff > expect &&\n+\ttest_must_fail git merge --no-commit clean_branch &&\n+\ttest ! -f .git/MERGE_HEAD &&\n+\ttest \"$pre_merge_head\" = \"$(git rev-parse HEAD)\" &&\n+\ttest -z \"$(git diff --staged)\" &&\n+\tgit diff > actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'Fail conflicting merge with conflicting dirty worktree' '\n+\ttest_must_fail git merge conflict_branch &&\n+\ttest ! -f .git/MERGE_HEAD &&\n+\ttest \"$pre_merge_head\" = \"$(git rev-parse HEAD)\" &&\n+\ttest -z \"$(git diff --staged)\" &&\n+\tgit diff > actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'Reset worktree changes' '\n+\tgit reset --hard \"$pre_merge_head\"\n+'\n+\n+test_expect_success 'Fail clean merge with matching dirty worktree' '\n+\techo bart > bar &&\n+\tgit diff > expect &&\n+\ttest_must_fail git merge --no-commit clean_branch &&\n+\ttest ! -f .git/MERGE_HEAD &&\n+\ttest \"$pre_merge_head\" = \"$(git rev-parse HEAD)\" &&\n+\ttest -z \"$(git diff --staged)\" &&\n+\tgit diff > actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'Abort clean merge with matching dirty index' '\n+\tgit add bar &&\n+\tgit diff --staged > expect &&\n+\tgit merge --no-commit clean_branch &&\n+\ttest -f .git/MERGE_HEAD &&\n+\t### When aborting the merge, git will discard all staged changes,\n+\t### including those that were staged pre-merge. In other words,\n+\t### --abort will LOSE any staged changes (the staged changes that\n+\t### are lost must match the merge result, or the merge would not\n+\t### have been allowed to start). Change expectations accordingly:\n+\trm expect &&\n+\ttouch expect &&\n+\t# Abort merge\n+\tgit merge --abort &&\n+\ttest ! -f .git/MERGE_HEAD &&\n+\ttest \"$pre_merge_head\" = \"$(git rev-parse HEAD)\" &&\n+\tgit diff --staged > actual &&\n+\ttest_cmp expect actual &&\n+\ttest -z \"$(git diff)\"\n+'\n+\n+test_expect_success 'Reset worktree changes' '\n+\tgit reset --hard \"$pre_merge_head\"\n+'\n+\n+test_expect_success 'Fail conflicting merge with matching dirty worktree' '\n+\techo barf > bar &&\n+\tgit diff > expect &&\n+\ttest_must_fail git merge conflict_branch &&\n+\ttest ! -f .git/MERGE_HEAD &&\n+\ttest \"$pre_merge_head\" = \"$(git rev-parse HEAD)\" &&\n+\ttest -z \"$(git diff --staged)\" &&\n+\tgit diff > actual &&\n+\ttest_cmp expect actual\n+'\n+\n+test_expect_success 'Abort conflicting merge with matching dirty index' '\n+\tgit add bar &&\n+\tgit diff --staged > expect &&\n+\ttest_must_fail git merge conflict_branch &&\n+\ttest -f .git/MERGE_HEAD &&\n+\t### When aborting the merge, git will discard all staged changes,\n+\t### including those that were staged pre-merge. In other words,\n+\t### --abort will LOSE any staged changes (the staged changes that\n+\t### are lost must match the merge result, or the merge would not\n+\t### have been allowed to start). Change expectations accordingly:\n+\trm expect &&\n+\ttouch expect &&\n+\t# Abort merge\n+\tgit merge --abort &&\n+\ttest ! -f .git/MERGE_HEAD &&\n+\ttest \"$pre_merge_head\" = \"$(git rev-parse HEAD)\" &&\n+\tgit diff --staged > actual &&\n+\ttest_cmp expect actual &&\n+\ttest -z \"$(git diff)\"\n+'\n+\n+test_expect_success 'Reset worktree changes' '\n+\tgit reset --hard \"$pre_merge_head\"\n+'\n+\n+test_expect_success 'Abort merge with pre- and post-merge worktree changes' '\n+\t# Pre-merge worktree changes\n+\techo xyzzy > foo &&\n+\techo barf > bar &&\n+\tgit add bar &&\n+\tgit diff > expect &&\n+\tgit diff --staged > expect-staged &&\n+\t# Perform merge\n+\ttest_must_fail git merge conflict_branch &&\n+\ttest -f .git/MERGE_HEAD &&\n+\t# Post-merge worktree changes\n+\techo yzxxz > foo &&\n+\techo blech > baz &&\n+\t### When aborting the merge, git will discard staged changes (bar)\n+\t### and unmerged changes (baz). Other changes that are neither\n+\t### staged nor marked as unmerged (foo), will be preserved. For\n+\t### these changed, git cannot tell pre-merge changes apart from\n+\t### post-merge changes, so the post-merge changes will be\n+\t### preserved. Change expectations accordingly:\n+\tgit diff -- foo > expect &&\n+\trm expect-staged &&\n+\ttouch expect-staged &&\n+\t# Abort merge\n+\tgit merge --abort &&\n+\ttest ! -f .git/MERGE_HEAD &&\n+\ttest \"$pre_merge_head\" = \"$(git rev-parse HEAD)\" &&\n+\tgit diff > actual &&\n+\ttest_cmp expect actual &&\n+\tgit diff --staged > actual-staged &&\n+\ttest_cmp expect-staged actual-staged\n+'\n+\n+test_expect_success 'Reset worktree changes' '\n+\tgit reset --hard \"$pre_merge_head\"\n+'\n+\n+test_expect_success 'Abort merge with pre- and post-merge index changes' '\n+\t# Pre-merge worktree changes\n+\techo xyzzy > foo &&\n+\techo barf > bar &&\n+\tgit add bar &&\n+\tgit diff > expect &&\n+\tgit diff --staged > expect-staged &&\n+\t# Perform merge\n+\ttest_must_fail git merge conflict_branch &&\n+\ttest -f .git/MERGE_HEAD &&\n+\t# Post-merge worktree changes\n+\techo yzxxz > foo &&\n+\techo blech > baz &&\n+\tgit add foo bar &&\n+\t### When aborting the merge, git will discard all staged changes\n+\t### (foo, bar and baz), and no changes will be preserved. Whether\n+\t### the changes were staged pre- or post-merge does not matter\n+\t### (except for not preventing starting the merge).\n+\t### Change expectations accordingly:\n+\trm expect expect-staged &&\n+\ttouch expect &&\n+\ttouch expect-staged &&\n+\t# Abort merge\n+\tgit merge --abort &&\n+\ttest ! -f .git/MERGE_HEAD &&\n+\ttest \"$pre_merge_head\" = \"$(git rev-parse HEAD)\" &&\n+\tgit diff > actual &&\n+\ttest_cmp expect actual &&\n+\tgit diff --staged > actual-staged &&\n+\ttest_cmp expect-staged actual-staged\n+'\n+\n+test_done\n-- \n1.7.3.2.173.gab1c9.dirty\n"},{"id":"154783","messageId":"7vwrp16o21.fsf@alter.siamese.dyndns.org","threadId":"25543","inReplyTo":"1287965333-5099-21-git-send-email-johan@herland.net","subject":"Re: [PATCHv5 20/23] git notes merge: Add testcases for merging notes trees at different fanouts","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2010-10-29T15:01:10Z","receivedAt":"2010-10-29T15:01:10Z","isPatch":false,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Subject: [PATCH] portability fix for [20/23]\n\n    test \"$(line generator | wc -l)\" = $expected_number_of_lines (bad)\n\nis not portable, as \"wc -l\" can prefix the number with whitespaces.\n\nEither write the $(... | wc -l) without enclosing in a dq pair, i.e.\n\n    test $(line generator | wc -l) = $expected_number_of_lines (good)\n\nor compare them numerically with \n\n    test \"$(... | wc -l)\" -eq $num (ok)\n\nThe former is preferred for readability.\n\n---\n t/t3311-notes-merge-fanout.sh |    4 ++--\n 1 files changed, 2 insertions(+), 2 deletions(-)\n\ndiff --git a/t/t3311-notes-merge-fanout.sh b/t/t3311-notes-merge-fanout.sh\nindex d1c7b69..93516ef 100755\n--- a/t/t3311-notes-merge-fanout.sh\n+++ b/t/t3311-notes-merge-fanout.sh\n@@ -123,8 +123,8 @@ test_expect_success 'Add a few hundred commits w/notes to trigger fanout (x -> y\n \tdone &&\n \ttest \"$(git rev-parse refs/notes/y)\" != \"$(git rev-parse refs/notes/x)\" &&\n \t# Expected number of commits and notes\n-\ttest \"$(git rev-list HEAD | wc -l)\" = \"$num\" &&\n-\ttest \"$(git notes list | wc -l)\" = \"$num\" &&\n+\ttest $(git rev-list HEAD | wc -l) = $num &&\n+\ttest $(git notes list | wc -l) = $num &&\n \t# 5 first notes unchanged\n \tverify_notes y commit5\n '\n-- \n1.7.3.2.193.g78bbb\n"},{"id":"154905","messageId":"201011010109.38459.johan@herland.net","threadId":"25543","inReplyTo":"7vwrp16o21.fsf@alter.siamese.dyndns.org","subject":"Re: [PATCHv5 20/23] git notes merge: Add testcases for merging notes trees at different fanouts","fromName":"Johan Herland","fromEmail":"johan@herland.net","sentAt":"2010-11-01T00:09:38Z","receivedAt":"2010-11-01T00:09:38Z","isPatch":false,"sender":{"key":"johan@herland.net","avatar":"https://avatars.githubusercontent.com/u/547031?v=4"},"body":"On Friday 29 October 2010, Junio C Hamano wrote:\n> Subject: [PATCH] portability fix for [20/23]\n> \n>     test \"$(line generator | wc -l)\" = $expected_number_of_lines (bad)\n> \n> is not portable, as \"wc -l\" can prefix the number with whitespaces.\n> \n> Either write the $(... | wc -l) without enclosing in a dq pair, i.e.\n> \n>     test $(line generator | wc -l) = $expected_number_of_lines (good)\n> \n> or compare them numerically with\n> \n>     test \"$(... | wc -l)\" -eq $num (ok)\n> \n> The former is preferred for readability.\n\nAck. I'll fold this fix into the next iteration.\n\nThanks!\n\n...Johan\n\n-- \nJohan Herland, <johan@herland.net>\nwww.herland.net\n"}]}