Merge branch 'hv/remote-end-hung-up'
[gitweb.git] / builtin / notes.c
index 6d07aac80cc2bfd042eee905abb5086a29e1d90a..3644d140ece9f2cfc6381e1a60c5b4fc8f645e2a 100644 (file)
@@ -17,6 +17,7 @@
 #include "run-command.h"
 #include "parse-options.h"
 #include "string-list.h"
+#include "notes-merge.h"
 
 static const char * const git_notes_usage[] = {
        "git notes [--ref <notes_ref>] [list [<object>]]",
@@ -25,8 +26,12 @@ static const char * const git_notes_usage[] = {
        "git notes [--ref <notes_ref>] append [-m <msg> | -F <file> | (-c | -C) <object>] [<object>]",
        "git notes [--ref <notes_ref>] edit [<object>]",
        "git notes [--ref <notes_ref>] show [<object>]",
-       "git notes [--ref <notes_ref>] remove [<object>]",
+       "git notes [--ref <notes_ref>] merge [-v | -q] [-s <strategy> ] <notes_ref>",
+       "git notes merge --commit [-v | -q]",
+       "git notes merge --abort [-v | -q]",
+       "git notes [--ref <notes_ref>] remove [<object>...]",
        "git notes [--ref <notes_ref>] prune [-n | -v]",
+       "git notes [--ref <notes_ref>] get-ref",
        NULL
 };
 
@@ -61,6 +66,13 @@ static const char * const git_notes_show_usage[] = {
        NULL
 };
 
+static const char * const git_notes_merge_usage[] = {
+       "git notes merge [<options>] <notes_ref>",
+       "git notes merge --commit [<options>]",
+       "git notes merge --abort [<options>]",
+       NULL
+};
+
 static const char * const git_notes_remove_usage[] = {
        "git notes remove [<object>]",
        NULL
@@ -71,6 +83,11 @@ static const char * const git_notes_prune_usage[] = {
        NULL
 };
 
+static const char * const git_notes_get_ref_usage[] = {
+       "git notes get-ref",
+       NULL
+};
+
 static const char note_template[] =
        "\n"
        "#\n"
@@ -119,13 +136,13 @@ static void write_commented_object(int fd, const unsigned char *object)
        show.err = 0;
        show.git_cmd = 1;
        if (start_command(&show))
-               die("unable to start 'show' for object '%s'",
+               die(_("unable to start 'show' for object '%s'"),
                    sha1_to_hex(object));
 
        /* Open the output as FILE* so strbuf_getline() can be used. */
        show_out = xfdopen(show.out, "r");
        if (show_out == NULL)
-               die_errno("can't fdopen 'show' output fd");
+               die_errno(_("can't fdopen 'show' output fd"));
 
        /* Prepend "# " to each output line and write result to 'fd' */
        while (strbuf_getline(&buf, show_out, '\n') != EOF) {
@@ -135,10 +152,10 @@ static void write_commented_object(int fd, const unsigned char *object)
        }
        strbuf_release(&buf);
        if (fclose(show_out))
-               die_errno("failed to close pipe to 'show' for object '%s'",
+               die_errno(_("failed to close pipe to 'show' for object '%s'"),
                          sha1_to_hex(object));
        if (finish_command(&show))
-               die("failed to finish 'show' for object '%s'",
+               die(_("failed to finish 'show' for object '%s'"),
                    sha1_to_hex(object));
 }
 
@@ -155,7 +172,7 @@ static void create_note(const unsigned char *object, struct msg_arg *msg,
                path = git_pathdup("NOTES_EDITMSG");
                fd = open(path, O_CREAT | O_TRUNC | O_WRONLY, 0600);
                if (fd < 0)
-                       die_errno("could not create file '%s'", path);
+                       die_errno(_("could not create file '%s'"), path);
 
                if (msg->given)
                        write_or_die(fd, msg->buf.buf, msg->buf.len);
@@ -169,8 +186,8 @@ static void create_note(const unsigned char *object, struct msg_arg *msg,
                strbuf_reset(&(msg->buf));
 
                if (launch_editor(path, &(msg->buf), NULL)) {
-                       die("Please supply the note contents using either -m" \
-                           " or -F option");
+                       die(_("Please supply the note contents using either -m" \
+                           " or -F option"));
                }
                stripspace(&(msg->buf), 1);
        }
@@ -190,14 +207,14 @@ static void create_note(const unsigned char *object, struct msg_arg *msg,
        }
 
        if (!msg->buf.len) {
-               fprintf(stderr, "Removing note for object %s\n",
+               fprintf(stderr, _("Removing note for object %s\n"),
                        sha1_to_hex(object));
                hashclr(result);
        } else {
                if (write_sha1_file(msg->buf.buf, msg->buf.len, blob_type, result)) {
-                       error("unable to write note object");
+                       error(_("unable to write note object"));
                        if (path)
-                               error("The note contents has been left in %s",
+                               error(_("The note contents has been left in %s"),
                                      path);
                        exit(128);
                }
@@ -231,9 +248,9 @@ static int parse_file_arg(const struct option *opt, const char *arg, int unset)
                strbuf_addch(&(msg->buf), '\n');
        if (!strcmp(arg, "-")) {
                if (strbuf_read(&(msg->buf), 0, 1024) < 0)
-                       die_errno("cannot read '%s'", arg);
+                       die_errno(_("cannot read '%s'"), arg);
        } else if (strbuf_read_file(&(msg->buf), arg, 1024) < 0)
-               die_errno("could not open or read '%s'", arg);
+               die_errno(_("could not open or read '%s'"), arg);
        stripspace(&(msg->buf), 0);
 
        msg->given = 1;
@@ -252,10 +269,10 @@ static int parse_reuse_arg(const struct option *opt, const char *arg, int unset)
                strbuf_addch(&(msg->buf), '\n');
 
        if (get_sha1(arg, object))
-               die("Failed to resolve '%s' as a valid ref.", arg);
+               die(_("Failed to resolve '%s' as a valid ref."), arg);
        if (!(buf = read_sha1_file(object, &type, &len)) || !len) {
                free(buf);
-               die("Failed to read object '%s'.", arg);;
+               die(_("Failed to read object '%s'."), arg);;
        }
        strbuf_add(&(msg->buf), buf, len);
        free(buf);
@@ -271,46 +288,28 @@ static int parse_reedit_arg(const struct option *opt, const char *arg, int unset
        return parse_reuse_arg(opt, arg, unset);
 }
 
-int commit_notes(struct notes_tree *t, const char *msg)
+void commit_notes(struct notes_tree *t, const char *msg)
 {
-       struct commit_list *parent;
-       unsigned char tree_sha1[20], prev_commit[20], new_commit[20];
        struct strbuf buf = STRBUF_INIT;
+       unsigned char commit_sha1[20];
 
        if (!t)
                t = &default_notes_tree;
        if (!t->initialized || !t->ref || !*t->ref)
-               die("Cannot commit uninitialized/unreferenced notes tree");
+               die(_("Cannot commit uninitialized/unreferenced notes tree"));
        if (!t->dirty)
-               return 0; /* don't have to commit an unchanged tree */
+               return; /* don't have to commit an unchanged tree */
 
        /* Prepare commit message and reflog message */
-       strbuf_addstr(&buf, "notes: "); /* commit message starts at index 7 */
        strbuf_addstr(&buf, msg);
        if (buf.buf[buf.len - 1] != '\n')
                strbuf_addch(&buf, '\n'); /* Make sure msg ends with newline */
 
-       /* Convert notes tree to tree object */
-       if (write_notes_tree(t, tree_sha1))
-               die("Failed to write current notes tree to database");
-
-       /* Create new commit for the tree object */
-       if (!read_ref(t->ref, prev_commit)) { /* retrieve parent commit */
-               parent = xmalloc(sizeof(*parent));
-               parent->item = lookup_commit(prev_commit);
-               parent->next = NULL;
-       } else {
-               hashclr(prev_commit);
-               parent = NULL;
-       }
-       if (commit_tree(buf.buf + 7, tree_sha1, parent, new_commit, NULL))
-               die("Failed to commit notes tree to database");
-
-       /* Update notes ref with new commit */
-       update_ref(buf.buf, t->ref, new_commit, prev_commit, 0, DIE_ON_ERR);
+       create_notes_commit(t, NULL, &buf, commit_sha1);
+       strbuf_insert(&buf, 0, "notes: ", 7); /* commit message starts at index 7 */
+       update_ref(buf.buf, t->ref, commit_sha1, NULL, 0, DIE_ON_ERR);
 
        strbuf_release(&buf);
-       return 0;
 }
 
 combine_notes_fn parse_combine_notes_fn(const char *v)
@@ -321,6 +320,8 @@ combine_notes_fn parse_combine_notes_fn(const char *v)
                return combine_notes_ignore;
        else if (!strcasecmp(v, "concatenate"))
                return combine_notes_concatenate;
+       else if (!strcasecmp(v, "cat_sort_uniq"))
+               return combine_notes_cat_sort_uniq;
        else
                return NULL;
 }
@@ -336,7 +337,7 @@ static int notes_rewrite_config(const char *k, const char *v, void *cb)
                        config_error_nonbool(k);
                c->combine = parse_combine_notes_fn(v);
                if (!c->combine) {
-                       error("Bad notes.rewriteMode value: '%s'", v);
+                       error(_("Bad notes.rewriteMode value: '%s'"), v);
                        return 1;
                }
                return 0;
@@ -346,8 +347,8 @@ static int notes_rewrite_config(const char *k, const char *v, void *cb)
                if (!prefixcmp(v, "refs/notes/"))
                        string_list_add_refs_by_glob(c->refs, v);
                else
-                       warning("Refusing to rewrite notes in %s"
-                               " (outside of refs/notes/)", v);
+                       warning(_("Refusing to rewrite notes in %s"
+                               " (outside of refs/notes/)"), v);
                return 0;
        }
 
@@ -371,8 +372,10 @@ struct notes_rewrite_cfg *init_copy_notes_for_rewrite(const char *cmd)
                c->mode_from_env = 1;
                c->combine = parse_combine_notes_fn(rewrite_mode_env);
                if (!c->combine)
-                       error("Bad " GIT_NOTES_REWRITE_MODE_ENVIRONMENT
-                             " value: '%s'", rewrite_mode_env);
+                       /* TRANSLATORS: The first %s is the name of the
+                          environment variable, the second %s is its value */
+                       error(_("Bad %s value: '%s'"), GIT_NOTES_REWRITE_MODE_ENVIRONMENT,
+                                       rewrite_mode_env);
        }
        if (rewrite_refs_env) {
                c->refs_from_env = 1;
@@ -412,7 +415,7 @@ void finish_copy_notes_for_rewrite(struct notes_rewrite_cfg *c)
        free(c);
 }
 
-int notes_copy_from_stdin(int force, const char *rewrite_cmd)
+static int notes_copy_from_stdin(int force, const char *rewrite_cmd)
 {
        struct strbuf buf = STRBUF_INIT;
        struct notes_rewrite_cfg *c = NULL;
@@ -435,13 +438,13 @@ int notes_copy_from_stdin(int force, const char *rewrite_cmd)
 
                split = strbuf_split(&buf, ' ');
                if (!split[0] || !split[1])
-                       die("Malformed input line: '%s'.", buf.buf);
+                       die(_("Malformed input line: '%s'."), buf.buf);
                strbuf_rtrim(split[0]);
                strbuf_rtrim(split[1]);
                if (get_sha1(split[0]->buf, from_obj))
-                       die("Failed to resolve '%s' as a valid ref.", split[0]->buf);
+                       die(_("Failed to resolve '%s' as a valid ref."), split[0]->buf);
                if (get_sha1(split[1]->buf, to_obj))
-                       die("Failed to resolve '%s' as a valid ref.", split[1]->buf);
+                       die(_("Failed to resolve '%s' as a valid ref."), split[1]->buf);
 
                if (rewrite_cmd)
                        err = copy_note_for_rewrite(c, from_obj, to_obj);
@@ -450,7 +453,7 @@ int notes_copy_from_stdin(int force, const char *rewrite_cmd)
                                        combine_notes_overwrite);
 
                if (err) {
-                       error("Failed to copy notes from '%s' to '%s'",
+                       error(_("Failed to copy notes from '%s' to '%s'"),
                              split[0]->buf, split[1]->buf);
                        ret = 1;
                }
@@ -494,20 +497,20 @@ static int list(int argc, const char **argv, const char *prefix)
                                     git_notes_list_usage, 0);
 
        if (1 < argc) {
-               error("too many parameters");
+               error(_("too many parameters"));
                usage_with_options(git_notes_list_usage, options);
        }
 
        t = init_notes_check("list");
        if (argc) {
                if (get_sha1(argv[0], object))
-                       die("Failed to resolve '%s' as a valid ref.", argv[0]);
+                       die(_("Failed to resolve '%s' as a valid ref."), argv[0]);
                note = get_note(t, object);
                if (note) {
                        puts(sha1_to_hex(note));
                        retval = 0;
                } else
-                       retval = error("No note found for object %s.",
+                       retval = error(_("No note found for object %s."),
                                       sha1_to_hex(object));
        } else
                retval = for_each_note(t, 0, list_each_note, NULL);
@@ -516,6 +519,8 @@ static int list(int argc, const char **argv, const char *prefix)
        return retval;
 }
 
+static int append_edit(int argc, const char **argv, const char *prefix);
+
 static int add(int argc, const char **argv, const char *prefix)
 {
        int retval = 0, force = 0;
@@ -526,46 +531,58 @@ static int add(int argc, const char **argv, const char *prefix)
        const unsigned char *note;
        struct msg_arg msg = { 0, 0, STRBUF_INIT };
        struct option options[] = {
-               { OPTION_CALLBACK, 'm', "message", &msg, "MSG",
+               { OPTION_CALLBACK, 'm', "message", &msg, "msg",
                        "note contents as a string", PARSE_OPT_NONEG,
                        parse_msg_arg},
-               { OPTION_CALLBACK, 'F', "file", &msg, "FILE",
+               { OPTION_CALLBACK, 'F', "file", &msg, "file",
                        "note contents in a file", PARSE_OPT_NONEG,
                        parse_file_arg},
-               { OPTION_CALLBACK, 'c', "reedit-message", &msg, "OBJECT",
+               { OPTION_CALLBACK, 'c', "reedit-message", &msg, "object",
                        "reuse and edit specified note object", PARSE_OPT_NONEG,
                        parse_reedit_arg},
-               { OPTION_CALLBACK, 'C', "reuse-message", &msg, "OBJECT",
+               { OPTION_CALLBACK, 'C', "reuse-message", &msg, "object",
                        "reuse specified note object", PARSE_OPT_NONEG,
                        parse_reuse_arg},
-               OPT_BOOLEAN('f', "force", &force, "replace existing notes"),
+               OPT__FORCE(&force, "replace existing notes"),
                OPT_END()
        };
 
        argc = parse_options(argc, argv, prefix, options, git_notes_add_usage,
-                            0);
+                            PARSE_OPT_KEEP_ARGV0);
 
-       if (1 < argc) {
-               error("too many parameters");
+       if (2 < argc) {
+               error(_("too many parameters"));
                usage_with_options(git_notes_add_usage, options);
        }
 
-       object_ref = argc ? argv[0] : "HEAD";
+       object_ref = argc > 1 ? argv[1] : "HEAD";
 
        if (get_sha1(object_ref, object))
-               die("Failed to resolve '%s' as a valid ref.", object_ref);
+               die(_("Failed to resolve '%s' as a valid ref."), object_ref);
 
        t = init_notes_check("add");
        note = get_note(t, object);
 
        if (note) {
                if (!force) {
-                       retval = error("Cannot add notes. Found existing notes "
+                       if (!msg.given) {
+                               /*
+                                * Redirect to "edit" subcommand.
+                                *
+                                * We only end up here if none of -m/-F/-c/-C
+                                * or -f are given. The original args are
+                                * therefore still in argv[0-1].
+                                */
+                               argv[0] = "edit";
+                               free_notes(t);
+                               return append_edit(argc, argv, prefix);
+                       }
+                       retval = error(_("Cannot add notes. Found existing notes "
                                       "for object %s. Use '-f' to overwrite "
-                                      "existing notes", sha1_to_hex(object));
+                                      "existing notes"), sha1_to_hex(object));
                        goto out;
                }
-               fprintf(stderr, "Overwriting existing notes for object %s\n",
+               fprintf(stderr, _("Overwriting existing notes for object %s\n"),
                        sha1_to_hex(object));
        }
 
@@ -573,8 +590,8 @@ static int add(int argc, const char **argv, const char *prefix)
 
        if (is_null_sha1(new_note))
                remove_note(t, object);
-       else
-               add_note(t, object, new_note, combine_notes_overwrite);
+       else if (add_note(t, object, new_note, combine_notes_overwrite))
+               die("BUG: combine_notes_overwrite failed");
 
        snprintf(logmsg, sizeof(logmsg), "Notes %s by 'git notes %s'",
                 is_null_sha1(new_note) ? "removed" : "added", "add");
@@ -594,7 +611,7 @@ static int copy(int argc, const char **argv, const char *prefix)
        struct notes_tree *t;
        const char *rewrite_cmd = NULL;
        struct option options[] = {
-               OPT_BOOLEAN('f', "force", &force, "replace existing notes"),
+               OPT__FORCE(&force, "replace existing notes"),
                OPT_BOOLEAN(0, "stdin", &from_stdin, "read objects from stdin"),
                OPT_STRING(0, "for-rewrite", &rewrite_cmd, "command",
                           "load rewriting config for <command> (implies "
@@ -607,7 +624,7 @@ static int copy(int argc, const char **argv, const char *prefix)
 
        if (from_stdin || rewrite_cmd) {
                if (argc) {
-                       error("too many parameters");
+                       error(_("too many parameters"));
                        usage_with_options(git_notes_copy_usage, options);
                } else {
                        return notes_copy_from_stdin(force, rewrite_cmd);
@@ -615,45 +632,46 @@ static int copy(int argc, const char **argv, const char *prefix)
        }
 
        if (argc < 2) {
-               error("too few parameters");
+               error(_("too few parameters"));
                usage_with_options(git_notes_copy_usage, options);
        }
        if (2 < argc) {
-               error("too many parameters");
+               error(_("too many parameters"));
                usage_with_options(git_notes_copy_usage, options);
        }
 
        if (get_sha1(argv[0], from_obj))
-               die("Failed to resolve '%s' as a valid ref.", argv[0]);
+               die(_("Failed to resolve '%s' as a valid ref."), argv[0]);
 
        object_ref = 1 < argc ? argv[1] : "HEAD";
 
        if (get_sha1(object_ref, object))
-               die("Failed to resolve '%s' as a valid ref.", object_ref);
+               die(_("Failed to resolve '%s' as a valid ref."), object_ref);
 
        t = init_notes_check("copy");
        note = get_note(t, object);
 
        if (note) {
                if (!force) {
-                       retval = error("Cannot copy notes. Found existing "
+                       retval = error(_("Cannot copy notes. Found existing "
                                       "notes for object %s. Use '-f' to "
-                                      "overwrite existing notes",
+                                      "overwrite existing notes"),
                                       sha1_to_hex(object));
                        goto out;
                }
-               fprintf(stderr, "Overwriting existing notes for object %s\n",
+               fprintf(stderr, _("Overwriting existing notes for object %s\n"),
                        sha1_to_hex(object));
        }
 
        from_note = get_note(t, from_obj);
        if (!from_note) {
-               retval = error("Missing notes on source object %s. Cannot "
-                              "copy.", sha1_to_hex(from_obj));
+               retval = error(_("Missing notes on source object %s. Cannot "
+                              "copy."), sha1_to_hex(from_obj));
                goto out;
        }
 
-       add_note(t, object, from_note, combine_notes_overwrite);
+       if (add_note(t, object, from_note, combine_notes_overwrite))
+               die("BUG: combine_notes_overwrite failed");
        commit_notes(t, "Notes added by 'git notes copy'");
 out:
        free_notes(t);
@@ -670,16 +688,16 @@ static int append_edit(int argc, const char **argv, const char *prefix)
        const char * const *usage;
        struct msg_arg msg = { 0, 0, STRBUF_INIT };
        struct option options[] = {
-               { OPTION_CALLBACK, 'm', "message", &msg, "MSG",
+               { OPTION_CALLBACK, 'm', "message", &msg, "msg",
                        "note contents as a string", PARSE_OPT_NONEG,
                        parse_msg_arg},
-               { OPTION_CALLBACK, 'F', "file", &msg, "FILE",
+               { OPTION_CALLBACK, 'F', "file", &msg, "file",
                        "note contents in a file", PARSE_OPT_NONEG,
                        parse_file_arg},
-               { OPTION_CALLBACK, 'c', "reedit-message", &msg, "OBJECT",
+               { OPTION_CALLBACK, 'c', "reedit-message", &msg, "object",
                        "reuse and edit specified note object", PARSE_OPT_NONEG,
                        parse_reedit_arg},
-               { OPTION_CALLBACK, 'C', "reuse-message", &msg, "OBJECT",
+               { OPTION_CALLBACK, 'C', "reuse-message", &msg, "object",
                        "reuse specified note object", PARSE_OPT_NONEG,
                        parse_reuse_arg},
                OPT_END()
@@ -691,19 +709,19 @@ static int append_edit(int argc, const char **argv, const char *prefix)
                             PARSE_OPT_KEEP_ARGV0);
 
        if (2 < argc) {
-               error("too many parameters");
+               error(_("too many parameters"));
                usage_with_options(usage, options);
        }
 
        if (msg.given && edit)
-               fprintf(stderr, "The -m/-F/-c/-C options have been deprecated "
+               fprintf(stderr, _("The -m/-F/-c/-C options have been deprecated "
                        "for the 'edit' subcommand.\n"
-                       "Please use 'git notes add -f -m/-F/-c/-C' instead.\n");
+                       "Please use 'git notes add -f -m/-F/-c/-C' instead.\n"));
 
        object_ref = 1 < argc ? argv[1] : "HEAD";
 
        if (get_sha1(object_ref, object))
-               die("Failed to resolve '%s' as a valid ref.", object_ref);
+               die(_("Failed to resolve '%s' as a valid ref."), object_ref);
 
        t = init_notes_check(argv[0]);
        note = get_note(t, object);
@@ -712,8 +730,8 @@ static int append_edit(int argc, const char **argv, const char *prefix)
 
        if (is_null_sha1(new_note))
                remove_note(t, object);
-       else
-               add_note(t, object, new_note, combine_notes_overwrite);
+       else if (add_note(t, object, new_note, combine_notes_overwrite))
+               die("BUG: combine_notes_overwrite failed");
 
        snprintf(logmsg, sizeof(logmsg), "Notes %s by 'git notes %s'",
                 is_null_sha1(new_note) ? "removed" : "added", argv[0]);
@@ -738,20 +756,20 @@ static int show(int argc, const char **argv, const char *prefix)
                             0);
 
        if (1 < argc) {
-               error("too many parameters");
+               error(_("too many parameters"));
                usage_with_options(git_notes_show_usage, options);
        }
 
        object_ref = argc ? argv[0] : "HEAD";
 
        if (get_sha1(object_ref, object))
-               die("Failed to resolve '%s' as a valid ref.", object_ref);
+               die(_("Failed to resolve '%s' as a valid ref."), object_ref);
 
        t = init_notes_check("show");
        note = get_note(t, object);
 
        if (!note)
-               retval = error("No note found for object %s.",
+               retval = error(_("No note found for object %s."),
                               sha1_to_hex(object));
        else {
                const char *show_args[3] = {"show", sha1_to_hex(note), NULL};
@@ -761,40 +779,239 @@ static int show(int argc, const char **argv, const char *prefix)
        return retval;
 }
 
-static int remove_cmd(int argc, const char **argv, const char *prefix)
+static int merge_abort(struct notes_merge_options *o)
 {
+       int ret = 0;
+
+       /*
+        * Remove .git/NOTES_MERGE_PARTIAL and .git/NOTES_MERGE_REF, and call
+        * notes_merge_abort() to remove .git/NOTES_MERGE_WORKTREE.
+        */
+
+       if (delete_ref("NOTES_MERGE_PARTIAL", NULL, 0))
+               ret += error("Failed to delete ref NOTES_MERGE_PARTIAL");
+       if (delete_ref("NOTES_MERGE_REF", NULL, REF_NODEREF))
+               ret += error("Failed to delete ref NOTES_MERGE_REF");
+       if (notes_merge_abort(o))
+               ret += error("Failed to remove 'git notes merge' worktree");
+       return ret;
+}
+
+static int merge_commit(struct notes_merge_options *o)
+{
+       struct strbuf msg = STRBUF_INIT;
+       unsigned char sha1[20], parent_sha1[20];
+       struct notes_tree *t;
+       struct commit *partial;
+       struct pretty_print_context pretty_ctx;
+       void *local_ref_to_free;
+       int ret;
+
+       /*
+        * Read partial merge result from .git/NOTES_MERGE_PARTIAL,
+        * and target notes ref from .git/NOTES_MERGE_REF.
+        */
+
+       if (get_sha1("NOTES_MERGE_PARTIAL", sha1))
+               die("Failed to read ref NOTES_MERGE_PARTIAL");
+       else if (!(partial = lookup_commit_reference(sha1)))
+               die("Could not find commit from NOTES_MERGE_PARTIAL.");
+       else if (parse_commit(partial))
+               die("Could not parse commit from NOTES_MERGE_PARTIAL.");
+
+       if (partial->parents)
+               hashcpy(parent_sha1, partial->parents->item->object.sha1);
+       else
+               hashclr(parent_sha1);
+
+       t = xcalloc(1, sizeof(struct notes_tree));
+       init_notes(t, "NOTES_MERGE_PARTIAL", combine_notes_overwrite, 0);
+
+       o->local_ref = local_ref_to_free =
+               resolve_refdup("NOTES_MERGE_REF", sha1, 0, NULL);
+       if (!o->local_ref)
+               die("Failed to resolve NOTES_MERGE_REF");
+
+       if (notes_merge_commit(o, t, partial, sha1))
+               die("Failed to finalize notes merge");
+
+       /* Reuse existing commit message in reflog message */
+       memset(&pretty_ctx, 0, sizeof(pretty_ctx));
+       format_commit_message(partial, "%s", &msg, &pretty_ctx);
+       strbuf_trim(&msg);
+       strbuf_insert(&msg, 0, "notes: ", 7);
+       update_ref(msg.buf, o->local_ref, sha1,
+                  is_null_sha1(parent_sha1) ? NULL : parent_sha1,
+                  0, DIE_ON_ERR);
+
+       free_notes(t);
+       strbuf_release(&msg);
+       ret = merge_abort(o);
+       free(local_ref_to_free);
+       return ret;
+}
+
+static int merge(int argc, const char **argv, const char *prefix)
+{
+       struct strbuf remote_ref = STRBUF_INIT, msg = STRBUF_INIT;
+       unsigned char result_sha1[20];
+       struct notes_tree *t;
+       struct notes_merge_options o;
+       int do_merge = 0, do_commit = 0, do_abort = 0;
+       int verbosity = 0, result;
+       const char *strategy = NULL;
        struct option options[] = {
+               OPT_GROUP("General options"),
+               OPT__VERBOSITY(&verbosity),
+               OPT_GROUP("Merge options"),
+               OPT_STRING('s', "strategy", &strategy, "strategy",
+                          "resolve notes conflicts using the given strategy "
+                          "(manual/ours/theirs/union/cat_sort_uniq)"),
+               OPT_GROUP("Committing unmerged notes"),
+               { OPTION_BOOLEAN, 0, "commit", &do_commit, NULL,
+                       "finalize notes merge by committing unmerged notes",
+                       PARSE_OPT_NOARG | PARSE_OPT_NONEG },
+               OPT_GROUP("Aborting notes merge resolution"),
+               { OPTION_BOOLEAN, 0, "abort", &do_abort, NULL,
+                       "abort notes merge",
+                       PARSE_OPT_NOARG | PARSE_OPT_NONEG },
                OPT_END()
        };
-       const char *object_ref;
-       struct notes_tree *t;
-       unsigned char object[20];
-       int retval;
 
        argc = parse_options(argc, argv, prefix, options,
-                            git_notes_remove_usage, 0);
+                            git_notes_merge_usage, 0);
 
-       if (1 < argc) {
+       if (strategy || do_commit + do_abort == 0)
+               do_merge = 1;
+       if (do_merge + do_commit + do_abort != 1) {
+               error("cannot mix --commit, --abort or -s/--strategy");
+               usage_with_options(git_notes_merge_usage, options);
+       }
+
+       if (do_merge && argc != 1) {
+               error("Must specify a notes ref to merge");
+               usage_with_options(git_notes_merge_usage, options);
+       } else if (!do_merge && argc) {
                error("too many parameters");
-               usage_with_options(git_notes_remove_usage, options);
+               usage_with_options(git_notes_merge_usage, options);
        }
 
-       object_ref = argc ? argv[0] : "HEAD";
+       init_notes_merge_options(&o);
+       o.verbosity = verbosity + NOTES_MERGE_VERBOSITY_DEFAULT;
+
+       if (do_abort)
+               return merge_abort(&o);
+       if (do_commit)
+               return merge_commit(&o);
+
+       o.local_ref = default_notes_ref();
+       strbuf_addstr(&remote_ref, argv[0]);
+       expand_notes_ref(&remote_ref);
+       o.remote_ref = remote_ref.buf;
+
+       if (strategy) {
+               if (!strcmp(strategy, "manual"))
+                       o.strategy = NOTES_MERGE_RESOLVE_MANUAL;
+               else if (!strcmp(strategy, "ours"))
+                       o.strategy = NOTES_MERGE_RESOLVE_OURS;
+               else if (!strcmp(strategy, "theirs"))
+                       o.strategy = NOTES_MERGE_RESOLVE_THEIRS;
+               else if (!strcmp(strategy, "union"))
+                       o.strategy = NOTES_MERGE_RESOLVE_UNION;
+               else if (!strcmp(strategy, "cat_sort_uniq"))
+                       o.strategy = NOTES_MERGE_RESOLVE_CAT_SORT_UNIQ;
+               else {
+                       error("Unknown -s/--strategy: %s", strategy);
+                       usage_with_options(git_notes_merge_usage, options);
+               }
+       }
 
-       if (get_sha1(object_ref, object))
-               die("Failed to resolve '%s' as a valid ref.", object_ref);
+       t = init_notes_check("merge");
+
+       strbuf_addf(&msg, "notes: Merged notes from %s into %s",
+                   remote_ref.buf, default_notes_ref());
+       strbuf_add(&(o.commit_msg), msg.buf + 7, msg.len - 7); /* skip "notes: " */
+
+       result = notes_merge(&o, t, result_sha1);
+
+       if (result >= 0) /* Merge resulted (trivially) in result_sha1 */
+               /* Update default notes ref with new commit */
+               update_ref(msg.buf, default_notes_ref(), result_sha1, NULL,
+                          0, DIE_ON_ERR);
+       else { /* Merge has unresolved conflicts */
+               /* Update .git/NOTES_MERGE_PARTIAL with partial merge result */
+               update_ref(msg.buf, "NOTES_MERGE_PARTIAL", result_sha1, NULL,
+                          0, DIE_ON_ERR);
+               /* Store ref-to-be-updated into .git/NOTES_MERGE_REF */
+               if (create_symref("NOTES_MERGE_REF", default_notes_ref(), NULL))
+                       die("Failed to store link to current notes ref (%s)",
+                           default_notes_ref());
+               printf("Automatic notes merge failed. Fix conflicts in %s and "
+                      "commit the result with 'git notes merge --commit', or "
+                      "abort the merge with 'git notes merge --abort'.\n",
+                      git_path(NOTES_MERGE_WORKTREE));
+       }
 
-       t = init_notes_check("remove");
+       free_notes(t);
+       strbuf_release(&remote_ref);
+       strbuf_release(&msg);
+       return result < 0; /* return non-zero on conflicts */
+}
 
-       retval = remove_note(t, object);
-       if (retval)
-               fprintf(stderr, "Object %s has no note\n", sha1_to_hex(object));
-       else {
-               fprintf(stderr, "Removing note for object %s\n",
-                       sha1_to_hex(object));
+#define IGNORE_MISSING 1
 
-               commit_notes(t, "Notes removed by 'git notes remove'");
+static int remove_one_note(struct notes_tree *t, const char *name, unsigned flag)
+{
+       int status;
+       unsigned char sha1[20];
+       if (get_sha1(name, sha1))
+               return error(_("Failed to resolve '%s' as a valid ref."), name);
+       status = remove_note(t, sha1);
+       if (status)
+               fprintf(stderr, _("Object %s has no note\n"), name);
+       else
+               fprintf(stderr, _("Removing note for object %s\n"), name);
+       return (flag & IGNORE_MISSING) ? 0 : status;
+}
+
+static int remove_cmd(int argc, const char **argv, const char *prefix)
+{
+       unsigned flag = 0;
+       int from_stdin = 0;
+       struct option options[] = {
+               OPT_BIT(0, "ignore-missing", &flag,
+                       "attempt to remove non-existent note is not an error",
+                       IGNORE_MISSING),
+               OPT_BOOLEAN(0, "stdin", &from_stdin,
+                           "read object names from the standard input"),
+               OPT_END()
+       };
+       struct notes_tree *t;
+       int retval = 0;
+
+       argc = parse_options(argc, argv, prefix, options,
+                            git_notes_remove_usage, 0);
+
+       t = init_notes_check("remove");
+
+       if (!argc && !from_stdin) {
+               retval = remove_one_note(t, "HEAD", flag);
+       } else {
+               while (*argv) {
+                       retval |= remove_one_note(t, *argv, flag);
+                       argv++;
+               }
+       }
+       if (from_stdin) {
+               struct strbuf sb = STRBUF_INIT;
+               while (strbuf_getwholeline(&sb, stdin, '\n') != EOF) {
+                       strbuf_rtrim(&sb);
+                       retval |= remove_one_note(t, sb.buf, flag);
+               }
+               strbuf_release(&sb);
        }
+       if (!retval)
+               commit_notes(t, "Notes removed by 'git notes remove'");
        free_notes(t);
        return retval;
 }
@@ -804,9 +1021,8 @@ static int prune(int argc, const char **argv, const char *prefix)
        struct notes_tree *t;
        int show_only = 0, verbose = 0;
        struct option options[] = {
-               OPT_BOOLEAN('n', "dry-run", &show_only,
-                           "do not remove, show only"),
-               OPT_BOOLEAN('v', "verbose", &verbose, "report pruned notes"),
+               OPT__DRY_RUN(&show_only, "do not remove, show only"),
+               OPT__VERBOSE(&verbose, "report pruned notes"),
                OPT_END()
        };
 
@@ -814,7 +1030,7 @@ static int prune(int argc, const char **argv, const char *prefix)
                             0);
 
        if (argc) {
-               error("too many parameters");
+               error(_("too many parameters"));
                usage_with_options(git_notes_prune_usage, options);
        }
 
@@ -828,6 +1044,21 @@ static int prune(int argc, const char **argv, const char *prefix)
        return 0;
 }
 
+static int get_ref(int argc, const char **argv, const char *prefix)
+{
+       struct option options[] = { OPT_END() };
+       argc = parse_options(argc, argv, prefix, options,
+                            git_notes_get_ref_usage, 0);
+
+       if (argc) {
+               error("too many parameters");
+               usage_with_options(git_notes_get_ref_usage, options);
+       }
+
+       puts(default_notes_ref());
+       return 0;
+}
+
 int cmd_notes(int argc, const char **argv, const char *prefix)
 {
        int result;
@@ -844,13 +1075,8 @@ int cmd_notes(int argc, const char **argv, const char *prefix)
 
        if (override_notes_ref) {
                struct strbuf sb = STRBUF_INIT;
-               if (!prefixcmp(override_notes_ref, "refs/notes/"))
-                       /* we're happy */;
-               else if (!prefixcmp(override_notes_ref, "notes/"))
-                       strbuf_addstr(&sb, "refs/");
-               else
-                       strbuf_addstr(&sb, "refs/notes/");
                strbuf_addstr(&sb, override_notes_ref);
+               expand_notes_ref(&sb);
                setenv("GIT_NOTES_REF", sb.buf, 1);
                strbuf_release(&sb);
        }
@@ -865,12 +1091,16 @@ int cmd_notes(int argc, const char **argv, const char *prefix)
                result = append_edit(argc, argv, prefix);
        else if (!strcmp(argv[0], "show"))
                result = show(argc, argv, prefix);
+       else if (!strcmp(argv[0], "merge"))
+               result = merge(argc, argv, prefix);
        else if (!strcmp(argv[0], "remove"))
                result = remove_cmd(argc, argv, prefix);
        else if (!strcmp(argv[0], "prune"))
                result = prune(argc, argv, prefix);
+       else if (!strcmp(argv[0], "get-ref"))
+               result = get_ref(argc, argv, prefix);
        else {
-               result = error("Unknown subcommand: %s", argv[0]);
+               result = error(_("Unknown subcommand: %s"), argv[0]);
                usage_with_options(git_notes_usage, options);
        }