Merge branch 'bk/refs-multi-update'
authorJunio C Hamano <gitster@pobox.com>
Fri, 20 Sep 2013 19:36:12 +0000 (12:36 -0700)
committerJunio C Hamano <gitster@pobox.com>
Fri, 20 Sep 2013 19:36:12 +0000 (12:36 -0700)
Give "update-refs" a "--stdin" option to read multiple update
requests and perform them in an all-or-none fashion.

* bk/refs-multi-update:
update-ref: add test cases covering --stdin signature
update-ref: support multiple simultaneous updates
refs: add update_refs for multiple simultaneous updates
refs: add function to repack without multiple refs
refs: factor delete_ref loose ref step into a helper
refs: factor update_ref steps into helpers
refs: report ref type from lock_any_ref_for_update
reset: rename update_refs to reset_refs

15 files changed:
Documentation/git-update-ref.txt
branch.c
builtin/commit.c
builtin/fetch.c
builtin/receive-pack.c
builtin/reflog.c
builtin/replace.c
builtin/reset.c
builtin/tag.c
builtin/update-ref.c
fast-import.c
refs.c
refs.h
sequencer.c
t/t1400-update-ref.sh
index 0df13ff6f4231779a5f0663900f34447ff2c1276..0a0a5512b3c62eb1179a3d5a9687949c39b2a9ef 100644 (file)
@@ -8,7 +8,7 @@ git-update-ref - Update the object name stored in a ref safely
 SYNOPSIS
 --------
 [verse]
-'git update-ref' [-m <reason>] (-d <ref> [<oldvalue>] | [--no-deref] <ref> <newvalue> [<oldvalue>])
+'git update-ref' [-m <reason>] (-d <ref> [<oldvalue>] | [--no-deref] <ref> <newvalue> [<oldvalue>] | --stdin [-z])
 
 DESCRIPTION
 -----------
@@ -58,6 +58,58 @@ archive by creating a symlink tree).
 With `-d` flag, it deletes the named <ref> after verifying it
 still contains <oldvalue>.
 
+With `--stdin`, update-ref reads instructions from standard input and
+performs all modifications together.  Specify commands of the form:
+
+       update SP <ref> SP <newvalue> [SP <oldvalue>] LF
+       create SP <ref> SP <newvalue> LF
+       delete SP <ref> [SP <oldvalue>] LF
+       verify SP <ref> [SP <oldvalue>] LF
+       option SP <opt> LF
+
+Quote fields containing whitespace as if they were strings in C source
+code.  Alternatively, use `-z` to specify commands without quoting:
+
+       update SP <ref> NUL <newvalue> NUL [<oldvalue>] NUL
+       create SP <ref> NUL <newvalue> NUL
+       delete SP <ref> NUL [<oldvalue>] NUL
+       verify SP <ref> NUL [<oldvalue>] NUL
+       option SP <opt> NUL
+
+Lines of any other format or a repeated <ref> produce an error.
+Command meanings are:
+
+update::
+       Set <ref> to <newvalue> after verifying <oldvalue>, if given.
+       Specify a zero <newvalue> to ensure the ref does not exist
+       after the update and/or a zero <oldvalue> to make sure the
+       ref does not exist before the update.
+
+create::
+       Create <ref> with <newvalue> after verifying it does not
+       exist.  The given <newvalue> may not be zero.
+
+delete::
+       Delete <ref> after verifying it exists with <oldvalue>, if
+       given.  If given, <oldvalue> may not be zero.
+
+verify::
+       Verify <ref> against <oldvalue> but do not change it.  If
+       <oldvalue> zero or missing, the ref must not exist.
+
+option::
+       Modify behavior of the next command naming a <ref>.
+       The only valid option is `no-deref` to avoid dereferencing
+       a symbolic ref.
+
+Use 40 "0" or the empty string to specify a zero value, except that
+with `-z` an empty <oldvalue> is considered missing.
+
+If all <ref>s can be locked with matching <oldvalue>s
+simultaneously, all modifications are performed.  Otherwise, no
+modifications are performed.  Note that while each individual
+<ref> is updated or deleted atomically, a concurrent reader may
+still see a subset of the modifications.
 
 Logging Updates
 ---------------
index d31b354d487e43f710206bbafbb130ab96d87637..9e6c68edaf90f27cf447ad1522da6d7de7827141 100644 (file)
--- a/branch.c
+++ b/branch.c
@@ -290,7 +290,7 @@ void create_branch(const char *head,
        hashcpy(sha1, commit->object.sha1);
 
        if (!dont_change_ref) {
-               lock = lock_any_ref_for_update(ref.buf, NULL, 0);
+               lock = lock_any_ref_for_update(ref.buf, NULL, 0, NULL);
                if (!lock)
                        die_errno(_("Failed to lock ref for update"));
        }
index d0d8bc1b30fe4d084e32beaf4b8ee9a24bb9ac94..29d4225ff214d629eab622242ae8e0aac780ead1 100644 (file)
@@ -1631,7 +1631,7 @@ int cmd_commit(int argc, const char **argv, const char *prefix)
                                           !current_head
                                           ? NULL
                                           : current_head->object.sha1,
-                                          0);
+                                          0, NULL);
 
        nl = strchr(sb.buf, '\n');
        if (nl)
index 9e654efa3bb725edd91f6eb7059b263668e622bc..bd7a10164f4fed8aeb6e8148e2ae8c0a793e7ee0 100644 (file)
@@ -262,7 +262,8 @@ static int s_update_ref(const char *action,
                rla = default_rla.buf;
        snprintf(msg, sizeof(msg), "%s: %s", rla, action);
        lock = lock_any_ref_for_update(ref->name,
-                                      check_old ? ref->old_sha1 : NULL, 0);
+                                      check_old ? ref->old_sha1 : NULL,
+                                      0, NULL);
        if (!lock)
                return errno == ENOTDIR ? STORE_REF_ERROR_DF_CONFLICT :
                                          STORE_REF_ERROR_OTHER;
index b7e71a04f61798ed72282f683a6fcd3c156007ab..67ce1ef105d1494e7905e66750b14676e082e11c 100644 (file)
@@ -526,7 +526,8 @@ static const char *update(struct command *cmd)
                return NULL; /* good */
        }
        else {
-               lock = lock_any_ref_for_update(namespaced_name, old_sha1, 0);
+               lock = lock_any_ref_for_update(namespaced_name, old_sha1,
+                                              0, NULL);
                if (!lock) {
                        rp_error("failed to lock %s", name);
                        return "failed to lock";
index ba27f7cc614214eb32f60b684b3617c39a0066ef..6eb24c8da27d265cad38628aeaa3e040dd298aab 100644 (file)
@@ -365,7 +365,7 @@ static int expire_reflog(const char *ref, const unsigned char *sha1, int unused,
         * we take the lock for the ref itself to prevent it from
         * getting updated.
         */
-       lock = lock_any_ref_for_update(ref, sha1, 0);
+       lock = lock_any_ref_for_update(ref, sha1, 0, NULL);
        if (!lock)
                return error("cannot lock ref '%s'", ref);
        log_file = git_pathdup("logs/%s", ref);
index 11b0a55ae7f6e3d1a04eeb4026b151144d0315ba..301b45ce6a453c472566278521bb51fcbd91ee10 100644 (file)
@@ -105,7 +105,7 @@ static int replace_object(const char *object_ref, const char *replace_ref,
        else if (!force)
                die("replace ref '%s' already exists", ref);
 
-       lock = lock_any_ref_for_update(ref, prev, 0);
+       lock = lock_any_ref_for_update(ref, prev, 0, NULL);
        if (!lock)
                die("%s: cannot lock the ref", ref);
        if (write_ref_sha1(lock, repl, NULL) < 0)
index 088ccffba07a4227150290492f22dc3bbac56fdc..1a5344877212e675d685055a631aba2ac0a4c661 100644 (file)
@@ -226,7 +226,7 @@ static void parse_args(struct pathspec *pathspec,
                       prefix, argv);
 }
 
-static int update_refs(const char *rev, const unsigned char *sha1)
+static int reset_refs(const char *rev, const unsigned char *sha1)
 {
        int update_ref_status;
        struct strbuf msg = STRBUF_INIT;
@@ -354,7 +354,7 @@ int cmd_reset(int argc, const char **argv, const char *prefix)
        if (!pathspec.nr && !unborn) {
                /* Any resets without paths update HEAD to the head being
                 * switched to, saving the previous head in ORIG_HEAD before. */
-               update_ref_status = update_refs(rev, sha1);
+               update_ref_status = reset_refs(rev, sha1);
 
                if (reset_type == HARD && !update_ref_status && !quiet)
                        print_new_head_line(lookup_commit_reference(sha1));
index b577af562a4a554f43c64256e112fcbc423edd3d..ea55f1d1bdd524abd46e6baa7a02ef85d56d5eb2 100644 (file)
@@ -574,7 +574,7 @@ int cmd_tag(int argc, const char **argv, const char *prefix)
        if (annotate)
                create_tag(object, tag, &buf, &opt, prev, object);
 
-       lock = lock_any_ref_for_update(ref.buf, prev, 0);
+       lock = lock_any_ref_for_update(ref.buf, prev, 0, NULL);
        if (!lock)
                die(_("%s: cannot lock the ref"), ref.buf);
        if (write_ref_sha1(lock, object, NULL) < 0)
index 7484d36a65ba1f722f2dda33473bf509f30727c9..702e90db2a82a0eba2ff233b9ec46690da5afc3e 100644 (file)
 #include "refs.h"
 #include "builtin.h"
 #include "parse-options.h"
+#include "quote.h"
+#include "argv-array.h"
 
 static const char * const git_update_ref_usage[] = {
        N_("git update-ref [options] -d <refname> [<oldval>]"),
        N_("git update-ref [options]    <refname> <newval> [<oldval>]"),
+       N_("git update-ref [options] --stdin [-z]"),
        NULL
 };
 
+static int updates_alloc;
+static int updates_count;
+static const struct ref_update **updates;
+
+static char line_termination = '\n';
+static int update_flags;
+
+static struct ref_update *update_alloc(void)
+{
+       struct ref_update *update;
+
+       /* Allocate and zero-init a struct ref_update */
+       update = xcalloc(1, sizeof(*update));
+       ALLOC_GROW(updates, updates_count + 1, updates_alloc);
+       updates[updates_count++] = update;
+
+       /* Store and reset accumulated options */
+       update->flags = update_flags;
+       update_flags = 0;
+
+       return update;
+}
+
+static void update_store_ref_name(struct ref_update *update,
+                                 const char *ref_name)
+{
+       if (check_refname_format(ref_name, REFNAME_ALLOW_ONELEVEL))
+               die("invalid ref format: %s", ref_name);
+       update->ref_name = xstrdup(ref_name);
+}
+
+static void update_store_new_sha1(struct ref_update *update,
+                                 const char *newvalue)
+{
+       if (*newvalue && get_sha1(newvalue, update->new_sha1))
+               die("invalid new value for ref %s: %s",
+                   update->ref_name, newvalue);
+}
+
+static void update_store_old_sha1(struct ref_update *update,
+                                 const char *oldvalue)
+{
+       if (*oldvalue && get_sha1(oldvalue, update->old_sha1))
+               die("invalid old value for ref %s: %s",
+                   update->ref_name, oldvalue);
+
+       /* We have an old value if non-empty, or if empty without -z */
+       update->have_old = *oldvalue || line_termination;
+}
+
+static const char *parse_arg(const char *next, struct strbuf *arg)
+{
+       /* Parse SP-terminated, possibly C-quoted argument */
+       if (*next != '"')
+               while (*next && !isspace(*next))
+                       strbuf_addch(arg, *next++);
+       else if (unquote_c_style(arg, next, &next))
+               die("badly quoted argument: %s", next);
+
+       /* Return position after the argument */
+       return next;
+}
+
+static const char *parse_first_arg(const char *next, struct strbuf *arg)
+{
+       /* Parse argument immediately after "command SP" */
+       strbuf_reset(arg);
+       if (line_termination) {
+               /* Without -z, use the next argument */
+               next = parse_arg(next, arg);
+       } else {
+               /* With -z, use rest of first NUL-terminated line */
+               strbuf_addstr(arg, next);
+               next = next + arg->len;
+       }
+       return next;
+}
+
+static const char *parse_next_arg(const char *next, struct strbuf *arg)
+{
+       /* Parse next SP-terminated or NUL-terminated argument, if any */
+       strbuf_reset(arg);
+       if (line_termination) {
+               /* Without -z, consume SP and use next argument */
+               if (!*next)
+                       return NULL;
+               if (*next != ' ')
+                       die("expected SP but got: %s", next);
+               next = parse_arg(next + 1, arg);
+       } else {
+               /* With -z, read the next NUL-terminated line */
+               if (*next)
+                       die("expected NUL but got: %s", next);
+               if (strbuf_getline(arg, stdin, '\0') == EOF)
+                       return NULL;
+               next = arg->buf + arg->len;
+       }
+       return next;
+}
+
+static void parse_cmd_update(const char *next)
+{
+       struct strbuf ref = STRBUF_INIT;
+       struct strbuf newvalue = STRBUF_INIT;
+       struct strbuf oldvalue = STRBUF_INIT;
+       struct ref_update *update;
+
+       update = update_alloc();
+
+       if ((next = parse_first_arg(next, &ref)) != NULL && ref.buf[0])
+               update_store_ref_name(update, ref.buf);
+       else
+               die("update line missing <ref>");
+
+       if ((next = parse_next_arg(next, &newvalue)) != NULL)
+               update_store_new_sha1(update, newvalue.buf);
+       else
+               die("update %s missing <newvalue>", ref.buf);
+
+       if ((next = parse_next_arg(next, &oldvalue)) != NULL)
+               update_store_old_sha1(update, oldvalue.buf);
+       else if(!line_termination)
+               die("update %s missing [<oldvalue>] NUL", ref.buf);
+
+       if (next && *next)
+               die("update %s has extra input: %s", ref.buf, next);
+}
+
+static void parse_cmd_create(const char *next)
+{
+       struct strbuf ref = STRBUF_INIT;
+       struct strbuf newvalue = STRBUF_INIT;
+       struct ref_update *update;
+
+       update = update_alloc();
+
+       if ((next = parse_first_arg(next, &ref)) != NULL && ref.buf[0])
+               update_store_ref_name(update, ref.buf);
+       else
+               die("create line missing <ref>");
+
+       if ((next = parse_next_arg(next, &newvalue)) != NULL)
+               update_store_new_sha1(update, newvalue.buf);
+       else
+               die("create %s missing <newvalue>", ref.buf);
+       if (is_null_sha1(update->new_sha1))
+               die("create %s given zero new value", ref.buf);
+
+       if (next && *next)
+               die("create %s has extra input: %s", ref.buf, next);
+}
+
+static void parse_cmd_delete(const char *next)
+{
+       struct strbuf ref = STRBUF_INIT;
+       struct strbuf oldvalue = STRBUF_INIT;
+       struct ref_update *update;
+
+       update = update_alloc();
+
+       if ((next = parse_first_arg(next, &ref)) != NULL && ref.buf[0])
+               update_store_ref_name(update, ref.buf);
+       else
+               die("delete line missing <ref>");
+
+       if ((next = parse_next_arg(next, &oldvalue)) != NULL)
+               update_store_old_sha1(update, oldvalue.buf);
+       else if(!line_termination)
+               die("delete %s missing [<oldvalue>] NUL", ref.buf);
+       if (update->have_old && is_null_sha1(update->old_sha1))
+               die("delete %s given zero old value", ref.buf);
+
+       if (next && *next)
+               die("delete %s has extra input: %s", ref.buf, next);
+}
+
+static void parse_cmd_verify(const char *next)
+{
+       struct strbuf ref = STRBUF_INIT;
+       struct strbuf value = STRBUF_INIT;
+       struct ref_update *update;
+
+       update = update_alloc();
+
+       if ((next = parse_first_arg(next, &ref)) != NULL && ref.buf[0])
+               update_store_ref_name(update, ref.buf);
+       else
+               die("verify line missing <ref>");
+
+       if ((next = parse_next_arg(next, &value)) != NULL) {
+               update_store_old_sha1(update, value.buf);
+               update_store_new_sha1(update, value.buf);
+       } else if(!line_termination)
+               die("verify %s missing [<oldvalue>] NUL", ref.buf);
+
+       if (next && *next)
+               die("verify %s has extra input: %s", ref.buf, next);
+}
+
+static void parse_cmd_option(const char *next)
+{
+       if (!strcmp(next, "no-deref"))
+               update_flags |= REF_NODEREF;
+       else
+               die("option unknown: %s", next);
+}
+
+static void update_refs_stdin(void)
+{
+       struct strbuf cmd = STRBUF_INIT;
+
+       /* Read each line dispatch its command */
+       while (strbuf_getline(&cmd, stdin, line_termination) != EOF)
+               if (!cmd.buf[0])
+                       die("empty command in input");
+               else if (isspace(*cmd.buf))
+                       die("whitespace before command: %s", cmd.buf);
+               else if (!prefixcmp(cmd.buf, "update "))
+                       parse_cmd_update(cmd.buf + 7);
+               else if (!prefixcmp(cmd.buf, "create "))
+                       parse_cmd_create(cmd.buf + 7);
+               else if (!prefixcmp(cmd.buf, "delete "))
+                       parse_cmd_delete(cmd.buf + 7);
+               else if (!prefixcmp(cmd.buf, "verify "))
+                       parse_cmd_verify(cmd.buf + 7);
+               else if (!prefixcmp(cmd.buf, "option "))
+                       parse_cmd_option(cmd.buf + 7);
+               else
+                       die("unknown command: %s", cmd.buf);
+
+       strbuf_release(&cmd);
+}
+
 int cmd_update_ref(int argc, const char **argv, const char *prefix)
 {
        const char *refname, *oldval, *msg = NULL;
        unsigned char sha1[20], oldsha1[20];
-       int delete = 0, no_deref = 0, flags = 0;
+       int delete = 0, no_deref = 0, read_stdin = 0, end_null = 0, flags = 0;
        struct option options[] = {
                OPT_STRING( 'm', NULL, &msg, N_("reason"), N_("reason of the update")),
                OPT_BOOL('d', NULL, &delete, N_("delete the reference")),
                OPT_BOOL( 0 , "no-deref", &no_deref,
                                        N_("update <refname> not the one it points to")),
+               OPT_BOOL('z', NULL, &end_null, N_("stdin has NUL-terminated arguments")),
+               OPT_BOOL( 0 , "stdin", &read_stdin, N_("read updates from stdin")),
                OPT_END(),
        };
 
@@ -28,6 +266,18 @@ int cmd_update_ref(int argc, const char **argv, const char *prefix)
        if (msg && !*msg)
                die("Refusing to perform update with empty message.");
 
+       if (read_stdin) {
+               if (delete || no_deref || argc > 0)
+                       usage_with_options(git_update_ref_usage, options);
+               if (end_null)
+                       line_termination = '\0';
+               update_refs_stdin();
+               return update_refs(msg, updates, updates_count, DIE_ON_ERR);
+       }
+
+       if (end_null)
+               usage_with_options(git_update_ref_usage, options);
+
        if (delete) {
                if (argc < 1 || argc > 2)
                        usage_with_options(git_update_ref_usage, options);
index 45c87648677a90b293c633b02232f9a490be6833..f4d9969e5c81f9c1415f0908f090bf62b156e179 100644 (file)
@@ -1694,7 +1694,7 @@ static int update_branch(struct branch *b)
                return 0;
        if (read_ref(b->name, old_sha1))
                hashclr(old_sha1);
-       lock = lock_any_ref_for_update(b->name, old_sha1, 0);
+       lock = lock_any_ref_for_update(b->name, old_sha1, 0, NULL);
        if (!lock)
                return error("Unable to lock %s", b->name);
        if (!force_update && !is_null_sha1(old_sha1)) {
diff --git a/refs.c b/refs.c
index d78860c46d95ea9785c83c08824355275393ed99..6383813627ece441aa780c49f71feeaa41145c5c 100644 (file)
--- a/refs.c
+++ b/refs.c
@@ -2121,11 +2121,12 @@ struct ref_lock *lock_ref_sha1(const char *refname, const unsigned char *old_sha
 }
 
 struct ref_lock *lock_any_ref_for_update(const char *refname,
-                                        const unsigned char *old_sha1, int flags)
+                                        const unsigned char *old_sha1,
+                                        int flags, int *type_p)
 {
        if (check_refname_format(refname, REFNAME_ALLOW_ONELEVEL))
                return NULL;
-       return lock_ref_sha1_basic(refname, old_sha1, flags, NULL);
+       return lock_ref_sha1_basic(refname, old_sha1, flags, type_p);
 }
 
 /*
@@ -2413,60 +2414,82 @@ static int curate_packed_ref_fn(struct ref_entry *entry, void *cb_data)
        return 0;
 }
 
-static int repack_without_ref(const char *refname)
+static int repack_without_refs(const char **refnames, int n)
 {
        struct ref_dir *packed;
        struct string_list refs_to_delete = STRING_LIST_INIT_DUP;
        struct string_list_item *ref_to_delete;
+       int i, removed = 0;
+
+       /* Look for a packed ref */
+       for (i = 0; i < n; i++)
+               if (get_packed_ref(refnames[i]))
+                       break;
 
-       if (!get_packed_ref(refname))
-               return 0; /* refname does not exist in packed refs */
+       /* Avoid locking if we have nothing to do */
+       if (i == n)
+               return 0; /* no refname exists in packed refs */
 
        if (lock_packed_refs(0)) {
                unable_to_lock_error(git_path("packed-refs"), errno);
-               return error("cannot delete '%s' from packed refs", refname);
+               return error("cannot delete '%s' from packed refs", refnames[i]);
        }
        packed = get_packed_refs(&ref_cache);
 
-       /* Remove refname from the cache: */
-       if (remove_entry(packed, refname) == -1) {
+       /* Remove refnames from the cache */
+       for (i = 0; i < n; i++)
+               if (remove_entry(packed, refnames[i]) != -1)
+                       removed = 1;
+       if (!removed) {
                /*
-                * The packed entry disappeared while we were
+                * All packed entries disappeared while we were
                 * acquiring the lock.
                 */
                rollback_packed_refs();
                return 0;
        }
 
-       /* Remove any other accumulated cruft: */
+       /* Remove any other accumulated cruft */
        do_for_each_entry_in_dir(packed, 0, curate_packed_ref_fn, &refs_to_delete);
        for_each_string_list_item(ref_to_delete, &refs_to_delete) {
                if (remove_entry(packed, ref_to_delete->string) == -1)
                        die("internal error");
        }
 
-       /* Write what remains: */
+       /* Write what remains */
        return commit_packed_refs();
 }
 
-int delete_ref(const char *refname, const unsigned char *sha1, int delopt)
+static int repack_without_ref(const char *refname)
 {
-       struct ref_lock *lock;
-       int err, i = 0, ret = 0, flag = 0;
+       return repack_without_refs(&refname, 1);
+}
 
-       lock = lock_ref_sha1_basic(refname, sha1, delopt, &flag);
-       if (!lock)
-               return 1;
+static int delete_ref_loose(struct ref_lock *lock, int flag)
+{
        if (!(flag & REF_ISPACKED) || flag & REF_ISSYMREF) {
                /* loose */
-               i = strlen(lock->lk->filename) - 5; /* .lock */
+               int err, i = strlen(lock->lk->filename) - 5; /* .lock */
+
                lock->lk->filename[i] = 0;
                err = unlink_or_warn(lock->lk->filename);
-               if (err && errno != ENOENT)
-                       ret = 1;
-
                lock->lk->filename[i] = '.';
+               if (err && errno != ENOENT)
+                       return 1;
        }
+       return 0;
+}
+
+int delete_ref(const char *refname, const unsigned char *sha1, int delopt)
+{
+       struct ref_lock *lock;
+       int ret = 0, flag = 0;
+
+       lock = lock_ref_sha1_basic(refname, sha1, delopt, &flag);
+       if (!lock)
+               return 1;
+       ret |= delete_ref_loose(lock, flag);
+
        /* removing the loose one could have resurrected an earlier
         * packed one.  Also, if it was not loose we need to repack
         * without it.
@@ -3169,12 +3192,13 @@ int for_each_reflog(each_ref_fn fn, void *cb_data)
        return retval;
 }
 
-int update_ref(const char *action, const char *refname,
-               const unsigned char *sha1, const unsigned char *oldval,
-               int flags, enum action_on_err onerr)
+static struct ref_lock *update_ref_lock(const char *refname,
+                                       const unsigned char *oldval,
+                                       int flags, int *type_p,
+                                       enum action_on_err onerr)
 {
-       static struct ref_lock *lock;
-       lock = lock_any_ref_for_update(refname, oldval, flags);
+       struct ref_lock *lock;
+       lock = lock_any_ref_for_update(refname, oldval, flags, type_p);
        if (!lock) {
                const char *str = "Cannot lock the ref '%s'.";
                switch (onerr) {
@@ -3182,8 +3206,14 @@ int update_ref(const char *action, const char *refname,
                case DIE_ON_ERR: die(str, refname); break;
                case QUIET_ON_ERR: break;
                }
-               return 1;
        }
+       return lock;
+}
+
+static int update_ref_write(const char *action, const char *refname,
+                           const unsigned char *sha1, struct ref_lock *lock,
+                           enum action_on_err onerr)
+{
        if (write_ref_sha1(lock, sha1, action) < 0) {
                const char *str = "Cannot update the ref '%s'.";
                switch (onerr) {
@@ -3196,6 +3226,117 @@ int update_ref(const char *action, const char *refname,
        return 0;
 }
 
+int update_ref(const char *action, const char *refname,
+              const unsigned char *sha1, const unsigned char *oldval,
+              int flags, enum action_on_err onerr)
+{
+       struct ref_lock *lock;
+       lock = update_ref_lock(refname, oldval, flags, 0, onerr);
+       if (!lock)
+               return 1;
+       return update_ref_write(action, refname, sha1, lock, onerr);
+}
+
+static int ref_update_compare(const void *r1, const void *r2)
+{
+       const struct ref_update * const *u1 = r1;
+       const struct ref_update * const *u2 = r2;
+       return strcmp((*u1)->ref_name, (*u2)->ref_name);
+}
+
+static int ref_update_reject_duplicates(struct ref_update **updates, int n,
+                                       enum action_on_err onerr)
+{
+       int i;
+       for (i = 1; i < n; i++)
+               if (!strcmp(updates[i - 1]->ref_name, updates[i]->ref_name)) {
+                       const char *str =
+                               "Multiple updates for ref '%s' not allowed.";
+                       switch (onerr) {
+                       case MSG_ON_ERR:
+                               error(str, updates[i]->ref_name); break;
+                       case DIE_ON_ERR:
+                               die(str, updates[i]->ref_name); break;
+                       case QUIET_ON_ERR:
+                               break;
+                       }
+                       return 1;
+               }
+       return 0;
+}
+
+int update_refs(const char *action, const struct ref_update **updates_orig,
+               int n, enum action_on_err onerr)
+{
+       int ret = 0, delnum = 0, i;
+       struct ref_update **updates;
+       int *types;
+       struct ref_lock **locks;
+       const char **delnames;
+
+       if (!updates_orig || !n)
+               return 0;
+
+       /* Allocate work space */
+       updates = xmalloc(sizeof(*updates) * n);
+       types = xmalloc(sizeof(*types) * n);
+       locks = xcalloc(n, sizeof(*locks));
+       delnames = xmalloc(sizeof(*delnames) * n);
+
+       /* Copy, sort, and reject duplicate refs */
+       memcpy(updates, updates_orig, sizeof(*updates) * n);
+       qsort(updates, n, sizeof(*updates), ref_update_compare);
+       ret = ref_update_reject_duplicates(updates, n, onerr);
+       if (ret)
+               goto cleanup;
+
+       /* Acquire all locks while verifying old values */
+       for (i = 0; i < n; i++) {
+               locks[i] = update_ref_lock(updates[i]->ref_name,
+                                          (updates[i]->have_old ?
+                                           updates[i]->old_sha1 : NULL),
+                                          updates[i]->flags,
+                                          &types[i], onerr);
+               if (!locks[i]) {
+                       ret = 1;
+                       goto cleanup;
+               }
+       }
+
+       /* Perform updates first so live commits remain referenced */
+       for (i = 0; i < n; i++)
+               if (!is_null_sha1(updates[i]->new_sha1)) {
+                       ret = update_ref_write(action,
+                                              updates[i]->ref_name,
+                                              updates[i]->new_sha1,
+                                              locks[i], onerr);
+                       locks[i] = NULL; /* freed by update_ref_write */
+                       if (ret)
+                               goto cleanup;
+               }
+
+       /* Perform deletes now that updates are safely completed */
+       for (i = 0; i < n; i++)
+               if (locks[i]) {
+                       delnames[delnum++] = locks[i]->ref_name;
+                       ret |= delete_ref_loose(locks[i], types[i]);
+               }
+       ret |= repack_without_refs(delnames, delnum);
+       for (i = 0; i < delnum; i++)
+               unlink_or_warn(git_path("logs/%s", delnames[i]));
+       clear_loose_ref_cache(&ref_cache);
+
+cleanup:
+       for (i = 0; i < n; i++)
+               if (locks[i])
+                       unlock_ref(locks[i]);
+       free(updates);
+       free(types);
+       free(locks);
+       free(delnames);
+       return ret;
+}
+
 /*
  * generate a format suitable for scanf from a ref_rev_parse_rules
  * rule, that is replace the "%.*s" spec with a "%s" spec
diff --git a/refs.h b/refs.h
index 9e5db3ae26ec7898d8ee9083c37562d3af05c0ab..b113377c4806423af2cad13f1b38284b5f5e5f2f 100644 (file)
--- a/refs.h
+++ b/refs.h
@@ -10,6 +10,20 @@ struct ref_lock {
        int force_write;
 };
 
+/**
+ * Information needed for a single ref update.  Set new_sha1 to the
+ * new value or to zero to delete the ref.  To check the old value
+ * while locking the ref, set have_old to 1 and set old_sha1 to the
+ * value or to zero to ensure the ref does not exist before update.
+ */
+struct ref_update {
+       const char *ref_name;
+       unsigned char new_sha1[20];
+       unsigned char old_sha1[20];
+       int flags; /* REF_NODEREF? */
+       int have_old; /* 1 if old_sha1 is valid, 0 otherwise */
+};
+
 /*
  * Bit values set in the flags argument passed to each_ref_fn():
  */
@@ -137,7 +151,7 @@ extern struct ref_lock *lock_ref_sha1(const char *refname, const unsigned char *
 #define REF_NODEREF    0x01
 extern struct ref_lock *lock_any_ref_for_update(const char *refname,
                                                const unsigned char *old_sha1,
-                                               int flags);
+                                               int flags, int *type_p);
 
 /** Close the file descriptor owned by a lock and return the status */
 extern int close_ref(struct ref_lock *lock);
@@ -214,6 +228,12 @@ int update_ref(const char *action, const char *refname,
                const unsigned char *sha1, const unsigned char *oldval,
                int flags, enum action_on_err onerr);
 
+/**
+ * Lock all refs and then perform all modifications.
+ */
+int update_refs(const char *action, const struct ref_update **updates,
+               int n, enum action_on_err onerr);
+
 extern int parse_hide_refs_config(const char *var, const char *value, const char *);
 extern int ref_is_hidden(const char *);
 
index 351548f57db61e62b47584020464aee877b48d8d..06e52b4c83ffc0ce2b9414afe3fb1b4ee91d4335 100644 (file)
@@ -279,7 +279,8 @@ static int fast_forward_to(const unsigned char *to, const unsigned char *from,
        read_cache();
        if (checkout_fast_forward(from, to, 1))
                exit(1); /* the callee should have complained already */
-       ref_lock = lock_any_ref_for_update("HEAD", unborn ? null_sha1 : from, 0);
+       ref_lock = lock_any_ref_for_update("HEAD", unborn ? null_sha1 : from,
+                                          0, NULL);
        strbuf_addf(&sb, "%s: fast-forward", action_name(opts));
        ret = write_ref_sha1(ref_lock, to, sb.buf);
        strbuf_release(&sb);
index e415ee0bbf1dcf45f3ee771d0fefedb1c8fa4246..6ffd82fe321cd3154d6f193c21198218e762f1b2 100755 (executable)
@@ -302,4 +302,636 @@ test_expect_success \
        'git cat-file blob master@{2005-05-26 23:42}:F (expect OTHER)' \
        'test OTHER = $(git cat-file blob "master@{2005-05-26 23:42}:F")'
 
+a=refs/heads/a
+b=refs/heads/b
+c=refs/heads/c
+E='""'
+F='%s\0'
+pws='path with space'
+
+test_expect_success 'stdin test setup' '
+       echo "$pws" >"$pws" &&
+       git add -- "$pws" &&
+       git commit -m "$pws"
+'
+
+test_expect_success '-z fails without --stdin' '
+       test_must_fail git update-ref -z $m $m $m 2>err &&
+       grep "usage: git update-ref" err
+'
+
+test_expect_success 'stdin works with no input' '
+       >stdin &&
+       git update-ref --stdin <stdin &&
+       git rev-parse --verify -q $m
+'
+
+test_expect_success 'stdin fails on empty line' '
+       echo "" >stdin &&
+       test_must_fail git update-ref --stdin <stdin 2>err &&
+       grep "fatal: empty command in input" err
+'
+
+test_expect_success 'stdin fails on only whitespace' '
+       echo " " >stdin &&
+       test_must_fail git update-ref --stdin <stdin 2>err &&
+       grep "fatal: whitespace before command:  " err
+'
+
+test_expect_success 'stdin fails on leading whitespace' '
+       echo " create $a $m" >stdin &&
+       test_must_fail git update-ref --stdin <stdin 2>err &&
+       grep "fatal: whitespace before command:  create $a $m" err
+'
+
+test_expect_success 'stdin fails on unknown command' '
+       echo "unknown $a" >stdin &&
+       test_must_fail git update-ref --stdin <stdin 2>err &&
+       grep "fatal: unknown command: unknown $a" err
+'
+
+test_expect_success 'stdin fails on badly quoted input' '
+       echo "create $a \"master" >stdin &&
+       test_must_fail git update-ref --stdin <stdin 2>err &&
+       grep "fatal: badly quoted argument: \\\"master" err
+'
+
+test_expect_success 'stdin fails on arguments not separated by space' '
+       echo "create \"$a\"master" >stdin &&
+       test_must_fail git update-ref --stdin <stdin 2>err &&
+       grep "fatal: expected SP but got: master" err
+'
+
+test_expect_success 'stdin fails create with no ref' '
+       echo "create " >stdin &&
+       test_must_fail git update-ref --stdin <stdin 2>err &&
+       grep "fatal: create line missing <ref>" err
+'
+
+test_expect_success 'stdin fails create with bad ref name' '
+       echo "create ~a $m" >stdin &&
+       test_must_fail git update-ref --stdin <stdin 2>err &&
+       grep "fatal: invalid ref format: ~a" err
+'
+
+test_expect_success 'stdin fails create with no new value' '
+       echo "create $a" >stdin &&
+       test_must_fail git update-ref --stdin <stdin 2>err &&
+       grep "fatal: create $a missing <newvalue>" err
+'
+
+test_expect_success 'stdin fails create with too many arguments' '
+       echo "create $a $m $m" >stdin &&
+       test_must_fail git update-ref --stdin <stdin 2>err &&
+       grep "fatal: create $a has extra input:  $m" err
+'
+
+test_expect_success 'stdin fails update with no ref' '
+       echo "update " >stdin &&
+       test_must_fail git update-ref --stdin <stdin 2>err &&
+       grep "fatal: update line missing <ref>" err
+'
+
+test_expect_success 'stdin fails update with bad ref name' '
+       echo "update ~a $m" >stdin &&
+       test_must_fail git update-ref --stdin <stdin 2>err &&
+       grep "fatal: invalid ref format: ~a" err
+'
+
+test_expect_success 'stdin fails update with no new value' '
+       echo "update $a" >stdin &&
+       test_must_fail git update-ref --stdin <stdin 2>err &&
+       grep "fatal: update $a missing <newvalue>" err
+'
+
+test_expect_success 'stdin fails update with too many arguments' '
+       echo "update $a $m $m $m" >stdin &&
+       test_must_fail git update-ref --stdin <stdin 2>err &&
+       grep "fatal: update $a has extra input:  $m" err
+'
+
+test_expect_success 'stdin fails delete with no ref' '
+       echo "delete " >stdin &&
+       test_must_fail git update-ref --stdin <stdin 2>err &&
+       grep "fatal: delete line missing <ref>" err
+'
+
+test_expect_success 'stdin fails delete with bad ref name' '
+       echo "delete ~a $m" >stdin &&
+       test_must_fail git update-ref --stdin <stdin 2>err &&
+       grep "fatal: invalid ref format: ~a" err
+'
+
+test_expect_success 'stdin fails delete with too many arguments' '
+       echo "delete $a $m $m" >stdin &&
+       test_must_fail git update-ref --stdin <stdin 2>err &&
+       grep "fatal: delete $a has extra input:  $m" err
+'
+
+test_expect_success 'stdin fails verify with too many arguments' '
+       echo "verify $a $m $m" >stdin &&
+       test_must_fail git update-ref --stdin <stdin 2>err &&
+       grep "fatal: verify $a has extra input:  $m" err
+'
+
+test_expect_success 'stdin fails option with unknown name' '
+       echo "option unknown" >stdin &&
+       test_must_fail git update-ref --stdin <stdin 2>err &&
+       grep "fatal: option unknown: unknown" err
+'
+
+test_expect_success 'stdin fails with duplicate refs' '
+       cat >stdin <<-EOF &&
+       create $a $m
+       create $b $m
+       create $a $m
+       EOF
+       test_must_fail git update-ref --stdin <stdin 2>err &&
+       grep "fatal: Multiple updates for ref '"'"'$a'"'"' not allowed." err
+'
+
+test_expect_success 'stdin create ref works' '
+       echo "create $a $m" >stdin &&
+       git update-ref --stdin <stdin &&
+       git rev-parse $m >expect &&
+       git rev-parse $a >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'stdin update ref creates with zero old value' '
+       echo "update $b $m $Z" >stdin &&
+       git update-ref --stdin <stdin &&
+       git rev-parse $m >expect &&
+       git rev-parse $b >actual &&
+       test_cmp expect actual &&
+       git update-ref -d $b
+'
+
+test_expect_success 'stdin update ref creates with empty old value' '
+       echo "update $b $m $E" >stdin &&
+       git update-ref --stdin <stdin &&
+       git rev-parse $m >expect &&
+       git rev-parse $b >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'stdin create ref works with path with space to blob' '
+       echo "create refs/blobs/pws \"$m:$pws\"" >stdin &&
+       git update-ref --stdin <stdin &&
+       git rev-parse "$m:$pws" >expect &&
+       git rev-parse refs/blobs/pws >actual &&
+       test_cmp expect actual &&
+       git update-ref -d refs/blobs/pws
+'
+
+test_expect_success 'stdin update ref fails with wrong old value' '
+       echo "update $c $m $m~1" >stdin &&
+       test_must_fail git update-ref --stdin <stdin 2>err &&
+       grep "fatal: Cannot lock the ref '"'"'$c'"'"'" err &&
+       test_must_fail git rev-parse --verify -q $c
+'
+
+test_expect_success 'stdin update ref fails with bad old value' '
+       echo "update $c $m does-not-exist" >stdin &&
+       test_must_fail git update-ref --stdin <stdin 2>err &&
+       grep "fatal: invalid old value for ref $c: does-not-exist" err &&
+       test_must_fail git rev-parse --verify -q $c
+'
+
+test_expect_success 'stdin create ref fails with bad new value' '
+       echo "create $c does-not-exist" >stdin &&
+       test_must_fail git update-ref --stdin <stdin 2>err &&
+       grep "fatal: invalid new value for ref $c: does-not-exist" err &&
+       test_must_fail git rev-parse --verify -q $c
+'
+
+test_expect_success 'stdin create ref fails with zero new value' '
+       echo "create $c " >stdin &&
+       test_must_fail git update-ref --stdin <stdin 2>err &&
+       grep "fatal: create $c given zero new value" err &&
+       test_must_fail git rev-parse --verify -q $c
+'
+
+test_expect_success 'stdin update ref works with right old value' '
+       echo "update $b $m~1 $m" >stdin &&
+       git update-ref --stdin <stdin &&
+       git rev-parse $m~1 >expect &&
+       git rev-parse $b >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'stdin delete ref fails with wrong old value' '
+       echo "delete $a $m~1" >stdin &&
+       test_must_fail git update-ref --stdin <stdin 2>err &&
+       grep "fatal: Cannot lock the ref '"'"'$a'"'"'" err &&
+       git rev-parse $m >expect &&
+       git rev-parse $a >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'stdin delete ref fails with zero old value' '
+       echo "delete $a " >stdin &&
+       test_must_fail git update-ref --stdin <stdin 2>err &&
+       grep "fatal: delete $a given zero old value" err &&
+       git rev-parse $m >expect &&
+       git rev-parse $a >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'stdin update symref works option no-deref' '
+       git symbolic-ref TESTSYMREF $b &&
+       cat >stdin <<-EOF &&
+       option no-deref
+       update TESTSYMREF $a $b
+       EOF
+       git update-ref --stdin <stdin &&
+       git rev-parse TESTSYMREF >expect &&
+       git rev-parse $a >actual &&
+       test_cmp expect actual &&
+       git rev-parse $m~1 >expect &&
+       git rev-parse $b >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'stdin delete symref works option no-deref' '
+       git symbolic-ref TESTSYMREF $b &&
+       cat >stdin <<-EOF &&
+       option no-deref
+       delete TESTSYMREF $b
+       EOF
+       git update-ref --stdin <stdin &&
+       test_must_fail git rev-parse --verify -q TESTSYMREF &&
+       git rev-parse $m~1 >expect &&
+       git rev-parse $b >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'stdin delete ref works with right old value' '
+       echo "delete $b $m~1" >stdin &&
+       git update-ref --stdin <stdin &&
+       test_must_fail git rev-parse --verify -q $b
+'
+
+test_expect_success 'stdin update/create/verify combination works' '
+       cat >stdin <<-EOF &&
+       update $a $m
+       create $b $m
+       verify $c
+       EOF
+       git update-ref --stdin <stdin &&
+       git rev-parse $m >expect &&
+       git rev-parse $a >actual &&
+       test_cmp expect actual &&
+       git rev-parse $b >actual &&
+       test_cmp expect actual &&
+       test_must_fail git rev-parse --verify -q $c
+'
+
+test_expect_success 'stdin update refs works with identity updates' '
+       cat >stdin <<-EOF &&
+       update $a $m $m
+       update $b $m $m
+       update $c $Z $E
+       EOF
+       git update-ref --stdin <stdin &&
+       git rev-parse $m >expect &&
+       git rev-parse $a >actual &&
+       test_cmp expect actual &&
+       git rev-parse $b >actual &&
+       test_cmp expect actual &&
+       test_must_fail git rev-parse --verify -q $c
+'
+
+test_expect_success 'stdin update refs fails with wrong old value' '
+       git update-ref $c $m &&
+       cat >stdin <<-EOF &&
+       update $a $m $m
+       update $b $m $m
+       update $c  ''
+       EOF
+       test_must_fail git update-ref --stdin <stdin 2>err &&
+       grep "fatal: Cannot lock the ref '"'"'$c'"'"'" err &&
+       git rev-parse $m >expect &&
+       git rev-parse $a >actual &&
+       test_cmp expect actual &&
+       git rev-parse $b >actual &&
+       test_cmp expect actual &&
+       git rev-parse $c >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'stdin delete refs works with packed and loose refs' '
+       git pack-refs --all &&
+       git update-ref $c $m~1 &&
+       cat >stdin <<-EOF &&
+       delete $a $m
+       update $b $Z $m
+       update $c $E $m~1
+       EOF
+       git update-ref --stdin <stdin &&
+       test_must_fail git rev-parse --verify -q $a &&
+       test_must_fail git rev-parse --verify -q $b &&
+       test_must_fail git rev-parse --verify -q $c
+'
+
+test_expect_success 'stdin -z works on empty input' '
+       >stdin &&
+       git update-ref -z --stdin <stdin &&
+       git rev-parse --verify -q $m
+'
+
+test_expect_success 'stdin -z fails on empty line' '
+       echo "" >stdin &&
+       test_must_fail git update-ref -z --stdin <stdin 2>err &&
+       grep "fatal: whitespace before command: " err
+'
+
+test_expect_success 'stdin -z fails on empty command' '
+       printf $F "" >stdin &&
+       test_must_fail git update-ref -z --stdin <stdin 2>err &&
+       grep "fatal: empty command in input" err
+'
+
+test_expect_success 'stdin -z fails on only whitespace' '
+       printf $F " " >stdin &&
+       test_must_fail git update-ref -z --stdin <stdin 2>err &&
+       grep "fatal: whitespace before command:  " err
+'
+
+test_expect_success 'stdin -z fails on leading whitespace' '
+       printf $F " create $a" "$m" >stdin &&
+       test_must_fail git update-ref -z --stdin <stdin 2>err &&
+       grep "fatal: whitespace before command:  create $a" err
+'
+
+test_expect_success 'stdin -z fails on unknown command' '
+       printf $F "unknown $a" >stdin &&
+       test_must_fail git update-ref -z --stdin <stdin 2>err &&
+       grep "fatal: unknown command: unknown $a" err
+'
+
+test_expect_success 'stdin -z fails create with no ref' '
+       printf $F "create " >stdin &&
+       test_must_fail git update-ref -z --stdin <stdin 2>err &&
+       grep "fatal: create line missing <ref>" err
+'
+
+test_expect_success 'stdin -z fails create with bad ref name' '
+       printf $F "create ~a " "$m" >stdin &&
+       test_must_fail git update-ref -z --stdin <stdin 2>err &&
+       grep "fatal: invalid ref format: ~a " err
+'
+
+test_expect_success 'stdin -z fails create with no new value' '
+       printf $F "create $a" >stdin &&
+       test_must_fail git update-ref -z --stdin <stdin 2>err &&
+       grep "fatal: create $a missing <newvalue>" err
+'
+
+test_expect_success 'stdin -z fails create with too many arguments' '
+       printf $F "create $a" "$m" "$m" >stdin &&
+       test_must_fail git update-ref -z --stdin <stdin 2>err &&
+       grep "fatal: unknown command: $m" err
+'
+
+test_expect_success 'stdin -z fails update with no ref' '
+       printf $F "update " >stdin &&
+       test_must_fail git update-ref -z --stdin <stdin 2>err &&
+       grep "fatal: update line missing <ref>" err
+'
+
+test_expect_success 'stdin -z fails update with bad ref name' '
+       printf $F "update ~a" "$m" >stdin &&
+       test_must_fail git update-ref -z --stdin <stdin 2>err &&
+       grep "fatal: invalid ref format: ~a" err
+'
+
+test_expect_success 'stdin -z fails update with no new value' '
+       printf $F "update $a" >stdin &&
+       test_must_fail git update-ref -z --stdin <stdin 2>err &&
+       grep "fatal: update $a missing <newvalue>" err
+'
+
+test_expect_success 'stdin -z fails update with no old value' '
+       printf $F "update $a" "$m" >stdin &&
+       test_must_fail git update-ref -z --stdin <stdin 2>err &&
+       grep "fatal: update $a missing \\[<oldvalue>\\] NUL" err
+'
+
+test_expect_success 'stdin -z fails update with too many arguments' '
+       printf $F "update $a" "$m" "$m" "$m" >stdin &&
+       test_must_fail git update-ref -z --stdin <stdin 2>err &&
+       grep "fatal: unknown command: $m" err
+'
+
+test_expect_success 'stdin -z fails delete with no ref' '
+       printf $F "delete " >stdin &&
+       test_must_fail git update-ref -z --stdin <stdin 2>err &&
+       grep "fatal: delete line missing <ref>" err
+'
+
+test_expect_success 'stdin -z fails delete with bad ref name' '
+       printf $F "delete ~a" "$m" >stdin &&
+       test_must_fail git update-ref -z --stdin <stdin 2>err &&
+       grep "fatal: invalid ref format: ~a" err
+'
+
+test_expect_success 'stdin -z fails delete with no old value' '
+       printf $F "delete $a" >stdin &&
+       test_must_fail git update-ref -z --stdin <stdin 2>err &&
+       grep "fatal: delete $a missing \\[<oldvalue>\\] NUL" err
+'
+
+test_expect_success 'stdin -z fails delete with too many arguments' '
+       printf $F "delete $a" "$m" "$m" >stdin &&
+       test_must_fail git update-ref -z --stdin <stdin 2>err &&
+       grep "fatal: unknown command: $m" err
+'
+
+test_expect_success 'stdin -z fails verify with too many arguments' '
+       printf $F "verify $a" "$m" "$m" >stdin &&
+       test_must_fail git update-ref -z --stdin <stdin 2>err &&
+       grep "fatal: unknown command: $m" err
+'
+
+test_expect_success 'stdin -z fails verify with no old value' '
+       printf $F "verify $a" >stdin &&
+       test_must_fail git update-ref -z --stdin <stdin 2>err &&
+       grep "fatal: verify $a missing \\[<oldvalue>\\] NUL" err
+'
+
+test_expect_success 'stdin -z fails option with unknown name' '
+       printf $F "option unknown" >stdin &&
+       test_must_fail git update-ref -z --stdin <stdin 2>err &&
+       grep "fatal: option unknown: unknown" err
+'
+
+test_expect_success 'stdin -z fails with duplicate refs' '
+       printf $F "create $a" "$m" "create $b" "$m" "create $a" "$m" >stdin &&
+       test_must_fail git update-ref -z --stdin <stdin 2>err &&
+       grep "fatal: Multiple updates for ref '"'"'$a'"'"' not allowed." err
+'
+
+test_expect_success 'stdin -z create ref works' '
+       printf $F "create $a" "$m" >stdin &&
+       git update-ref -z --stdin <stdin &&
+       git rev-parse $m >expect &&
+       git rev-parse $a >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'stdin -z update ref creates with zero old value' '
+       printf $F "update $b" "$m" "$Z" >stdin &&
+       git update-ref -z --stdin <stdin &&
+       git rev-parse $m >expect &&
+       git rev-parse $b >actual &&
+       test_cmp expect actual &&
+       git update-ref -d $b
+'
+
+test_expect_success 'stdin -z update ref creates with empty old value' '
+       printf $F "update $b" "$m" "" >stdin &&
+       git update-ref -z --stdin <stdin &&
+       git rev-parse $m >expect &&
+       git rev-parse $b >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'stdin -z create ref works with path with space to blob' '
+       printf $F "create refs/blobs/pws" "$m:$pws" >stdin &&
+       git update-ref -z --stdin <stdin &&
+       git rev-parse "$m:$pws" >expect &&
+       git rev-parse refs/blobs/pws >actual &&
+       test_cmp expect actual &&
+       git update-ref -d refs/blobs/pws
+'
+
+test_expect_success 'stdin -z update ref fails with wrong old value' '
+       printf $F "update $c" "$m" "$m~1" >stdin &&
+       test_must_fail git update-ref -z --stdin <stdin 2>err &&
+       grep "fatal: Cannot lock the ref '"'"'$c'"'"'" err &&
+       test_must_fail git rev-parse --verify -q $c
+'
+
+test_expect_success 'stdin -z update ref fails with bad old value' '
+       printf $F "update $c" "$m" "does-not-exist" >stdin &&
+       test_must_fail git update-ref -z --stdin <stdin 2>err &&
+       grep "fatal: invalid old value for ref $c: does-not-exist" err &&
+       test_must_fail git rev-parse --verify -q $c
+'
+
+test_expect_success 'stdin -z create ref fails with bad new value' '
+       printf $F "create $c" "does-not-exist" >stdin &&
+       test_must_fail git update-ref -z --stdin <stdin 2>err &&
+       grep "fatal: invalid new value for ref $c: does-not-exist" err &&
+       test_must_fail git rev-parse --verify -q $c
+'
+
+test_expect_success 'stdin -z create ref fails with zero new value' '
+       printf $F "create $c" "" >stdin &&
+       test_must_fail git update-ref -z --stdin <stdin 2>err &&
+       grep "fatal: create $c given zero new value" err &&
+       test_must_fail git rev-parse --verify -q $c
+'
+
+test_expect_success 'stdin -z update ref works with right old value' '
+       printf $F "update $b" "$m~1" "$m" >stdin &&
+       git update-ref -z --stdin <stdin &&
+       git rev-parse $m~1 >expect &&
+       git rev-parse $b >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'stdin -z delete ref fails with wrong old value' '
+       printf $F "delete $a" "$m~1" >stdin &&
+       test_must_fail git update-ref -z --stdin <stdin 2>err &&
+       grep "fatal: Cannot lock the ref '"'"'$a'"'"'" err &&
+       git rev-parse $m >expect &&
+       git rev-parse $a >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'stdin -z delete ref fails with zero old value' '
+       printf $F "delete $a" "$Z" >stdin &&
+       test_must_fail git update-ref -z --stdin <stdin 2>err &&
+       grep "fatal: delete $a given zero old value" err &&
+       git rev-parse $m >expect &&
+       git rev-parse $a >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'stdin -z update symref works option no-deref' '
+       git symbolic-ref TESTSYMREF $b &&
+       printf $F "option no-deref" "update TESTSYMREF" "$a" "$b" >stdin &&
+       git update-ref -z --stdin <stdin &&
+       git rev-parse TESTSYMREF >expect &&
+       git rev-parse $a >actual &&
+       test_cmp expect actual &&
+       git rev-parse $m~1 >expect &&
+       git rev-parse $b >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'stdin -z delete symref works option no-deref' '
+       git symbolic-ref TESTSYMREF $b &&
+       printf $F "option no-deref" "delete TESTSYMREF" "$b" >stdin &&
+       git update-ref -z --stdin <stdin &&
+       test_must_fail git rev-parse --verify -q TESTSYMREF &&
+       git rev-parse $m~1 >expect &&
+       git rev-parse $b >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'stdin -z delete ref works with right old value' '
+       printf $F "delete $b" "$m~1" >stdin &&
+       git update-ref -z --stdin <stdin &&
+       test_must_fail git rev-parse --verify -q $b
+'
+
+test_expect_success 'stdin -z update/create/verify combination works' '
+       printf $F "update $a" "$m" "" "create $b" "$m" "verify $c" "" >stdin &&
+       git update-ref -z --stdin <stdin &&
+       git rev-parse $m >expect &&
+       git rev-parse $a >actual &&
+       test_cmp expect actual &&
+       git rev-parse $b >actual &&
+       test_cmp expect actual &&
+       test_must_fail git rev-parse --verify -q $c
+'
+
+test_expect_success 'stdin -z update refs works with identity updates' '
+       printf $F "update $a" "$m" "$m" "update $b" "$m" "$m" "update $c" "$Z" "" >stdin &&
+       git update-ref -z --stdin <stdin &&
+       git rev-parse $m >expect &&
+       git rev-parse $a >actual &&
+       test_cmp expect actual &&
+       git rev-parse $b >actual &&
+       test_cmp expect actual &&
+       test_must_fail git rev-parse --verify -q $c
+'
+
+test_expect_success 'stdin -z update refs fails with wrong old value' '
+       git update-ref $c $m &&
+       printf $F "update $a" "$m" "$m" "update $b" "$m" "$m" "update $c" "" "$Z" >stdin &&
+       test_must_fail git update-ref -z --stdin <stdin 2>err &&
+       grep "fatal: Cannot lock the ref '"'"'$c'"'"'" err &&
+       git rev-parse $m >expect &&
+       git rev-parse $a >actual &&
+       test_cmp expect actual &&
+       git rev-parse $b >actual &&
+       test_cmp expect actual &&
+       git rev-parse $c >actual &&
+       test_cmp expect actual
+'
+
+test_expect_success 'stdin -z delete refs works with packed and loose refs' '
+       git pack-refs --all &&
+       git update-ref $c $m~1 &&
+       printf $F "delete $a" "$m" "update $b" "$Z" "$m" "update $c" "" "$m~1" >stdin &&
+       git update-ref -z --stdin <stdin &&
+       test_must_fail git rev-parse --verify -q $a &&
+       test_must_fail git rev-parse --verify -q $b &&
+       test_must_fail git rev-parse --verify -q $c
+'
+
 test_done