Imported Upstream version 2.30.0
[platform/upstream/git.git] / builtin / update-ref.c
index 51d2684..6029a80 100644 (file)
 #include "cache.h"
+#include "config.h"
 #include "refs.h"
 #include "builtin.h"
 #include "parse-options.h"
+#include "quote.h"
+#include "strvec.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>] -d <refname> [<old-val>]"),
+       N_("git update-ref [<options>]    <refname> <new-val> [<old-val>]"),
+       N_("git update-ref [<options>] --stdin [-z]"),
        NULL
 };
 
+static char line_termination = '\n';
+static unsigned int update_flags;
+static unsigned int default_flags;
+static unsigned create_reflog_flag;
+static const char *msg;
+
+/*
+ * Parse one whitespace- or NUL-terminated, possibly C-quoted argument
+ * and append the result to arg.  Return a pointer to the terminator.
+ * Die if there is an error in how the argument is C-quoted.  This
+ * function is only used if not -z.
+ */
+static const char *parse_arg(const char *next, struct strbuf *arg)
+{
+       if (*next == '"') {
+               const char *orig = next;
+
+               if (unquote_c_style(arg, next, &next))
+                       die("badly quoted argument: %s", orig);
+               if (*next && !isspace(*next))
+                       die("unexpected character after quoted argument: %s", orig);
+       } else {
+               while (*next && !isspace(*next))
+                       strbuf_addch(arg, *next++);
+       }
+
+       return next;
+}
+
+/*
+ * Parse the reference name immediately after "command SP".  If not
+ * -z, then handle C-quoting.  Return a pointer to a newly allocated
+ * string containing the name of the reference, or NULL if there was
+ * an error.  Update *next to point at the character that terminates
+ * the argument.  Die if C-quoting is malformed or the reference name
+ * is invalid.
+ */
+static char *parse_refname(const char **next)
+{
+       struct strbuf ref = STRBUF_INIT;
+
+       if (line_termination) {
+               /* Without -z, use the next argument */
+               *next = parse_arg(*next, &ref);
+       } else {
+               /* With -z, use everything up to the next NUL */
+               strbuf_addstr(&ref, *next);
+               *next += ref.len;
+       }
+
+       if (!ref.len) {
+               strbuf_release(&ref);
+               return NULL;
+       }
+
+       if (check_refname_format(ref.buf, REFNAME_ALLOW_ONELEVEL))
+               die("invalid ref format: %s", ref.buf);
+
+       return strbuf_detach(&ref, NULL);
+}
+
+/*
+ * The value being parsed is <oldvalue> (as opposed to <newvalue>; the
+ * difference affects which error messages are generated):
+ */
+#define PARSE_SHA1_OLD 0x01
+
+/*
+ * For backwards compatibility, accept an empty string for update's
+ * <newvalue> in binary mode to be equivalent to specifying zeros.
+ */
+#define PARSE_SHA1_ALLOW_EMPTY 0x02
+
+/*
+ * Parse an argument separator followed by the next argument, if any.
+ * If there is an argument, convert it to a SHA-1, write it to sha1,
+ * set *next to point at the character terminating the argument, and
+ * return 0.  If there is no argument at all (not even the empty
+ * string), return 1 and leave *next unchanged.  If the value is
+ * provided but cannot be converted to a SHA-1, die.  flags can
+ * include PARSE_SHA1_OLD and/or PARSE_SHA1_ALLOW_EMPTY.
+ */
+static int parse_next_oid(const char **next, const char *end,
+                         struct object_id *oid,
+                         const char *command, const char *refname,
+                         int flags)
+{
+       struct strbuf arg = STRBUF_INIT;
+       int ret = 0;
+
+       if (*next == end)
+               goto eof;
+
+       if (line_termination) {
+               /* Without -z, consume SP and use next argument */
+               if (!**next || **next == line_termination)
+                       return 1;
+               if (**next != ' ')
+                       die("%s %s: expected SP but got: %s",
+                           command, refname, *next);
+               (*next)++;
+               *next = parse_arg(*next, &arg);
+               if (arg.len) {
+                       if (get_oid(arg.buf, oid))
+                               goto invalid;
+               } else {
+                       /* Without -z, an empty value means all zeros: */
+                       oidclr(oid);
+               }
+       } else {
+               /* With -z, read the next NUL-terminated line */
+               if (**next)
+                       die("%s %s: expected NUL but got: %s",
+                           command, refname, *next);
+               (*next)++;
+               if (*next == end)
+                       goto eof;
+               strbuf_addstr(&arg, *next);
+               *next += arg.len;
+
+               if (arg.len) {
+                       if (get_oid(arg.buf, oid))
+                               goto invalid;
+               } else if (flags & PARSE_SHA1_ALLOW_EMPTY) {
+                       /* With -z, treat an empty value as all zeros: */
+                       warning("%s %s: missing <newvalue>, treating as zero",
+                               command, refname);
+                       oidclr(oid);
+               } else {
+                       /*
+                        * With -z, an empty non-required value means
+                        * unspecified:
+                        */
+                       ret = 1;
+               }
+       }
+
+       strbuf_release(&arg);
+
+       return ret;
+
+ invalid:
+       die(flags & PARSE_SHA1_OLD ?
+           "%s %s: invalid <oldvalue>: %s" :
+           "%s %s: invalid <newvalue>: %s",
+           command, refname, arg.buf);
+
+ eof:
+       die(flags & PARSE_SHA1_OLD ?
+           "%s %s: unexpected end of input when reading <oldvalue>" :
+           "%s %s: unexpected end of input when reading <newvalue>",
+           command, refname);
+}
+
+
+/*
+ * The following five parse_cmd_*() functions parse the corresponding
+ * command.  In each case, next points at the character following the
+ * command name and the following space.  They each return a pointer
+ * to the character terminating the command, and die with an
+ * explanatory message if there are any parsing problems.  All of
+ * these functions handle either text or binary format input,
+ * depending on how line_termination is set.
+ */
+
+static void parse_cmd_update(struct ref_transaction *transaction,
+                            const char *next, const char *end)
+{
+       struct strbuf err = STRBUF_INIT;
+       char *refname;
+       struct object_id new_oid, old_oid;
+       int have_old;
+
+       refname = parse_refname(&next);
+       if (!refname)
+               die("update: missing <ref>");
+
+       if (parse_next_oid(&next, end, &new_oid, "update", refname,
+                          PARSE_SHA1_ALLOW_EMPTY))
+               die("update %s: missing <newvalue>", refname);
+
+       have_old = !parse_next_oid(&next, end, &old_oid, "update", refname,
+                                  PARSE_SHA1_OLD);
+
+       if (*next != line_termination)
+               die("update %s: extra input: %s", refname, next);
+
+       if (ref_transaction_update(transaction, refname,
+                                  &new_oid, have_old ? &old_oid : NULL,
+                                  update_flags | create_reflog_flag,
+                                  msg, &err))
+               die("%s", err.buf);
+
+       update_flags = default_flags;
+       free(refname);
+       strbuf_release(&err);
+}
+
+static void parse_cmd_create(struct ref_transaction *transaction,
+                            const char *next, const char *end)
+{
+       struct strbuf err = STRBUF_INIT;
+       char *refname;
+       struct object_id new_oid;
+
+       refname = parse_refname(&next);
+       if (!refname)
+               die("create: missing <ref>");
+
+       if (parse_next_oid(&next, end, &new_oid, "create", refname, 0))
+               die("create %s: missing <newvalue>", refname);
+
+       if (is_null_oid(&new_oid))
+               die("create %s: zero <newvalue>", refname);
+
+       if (*next != line_termination)
+               die("create %s: extra input: %s", refname, next);
+
+       if (ref_transaction_create(transaction, refname, &new_oid,
+                                  update_flags | create_reflog_flag,
+                                  msg, &err))
+               die("%s", err.buf);
+
+       update_flags = default_flags;
+       free(refname);
+       strbuf_release(&err);
+}
+
+static void parse_cmd_delete(struct ref_transaction *transaction,
+                            const char *next, const char *end)
+{
+       struct strbuf err = STRBUF_INIT;
+       char *refname;
+       struct object_id old_oid;
+       int have_old;
+
+       refname = parse_refname(&next);
+       if (!refname)
+               die("delete: missing <ref>");
+
+       if (parse_next_oid(&next, end, &old_oid, "delete", refname,
+                          PARSE_SHA1_OLD)) {
+               have_old = 0;
+       } else {
+               if (is_null_oid(&old_oid))
+                       die("delete %s: zero <oldvalue>", refname);
+               have_old = 1;
+       }
+
+       if (*next != line_termination)
+               die("delete %s: extra input: %s", refname, next);
+
+       if (ref_transaction_delete(transaction, refname,
+                                  have_old ? &old_oid : NULL,
+                                  update_flags, msg, &err))
+               die("%s", err.buf);
+
+       update_flags = default_flags;
+       free(refname);
+       strbuf_release(&err);
+}
+
+static void parse_cmd_verify(struct ref_transaction *transaction,
+                            const char *next, const char *end)
+{
+       struct strbuf err = STRBUF_INIT;
+       char *refname;
+       struct object_id old_oid;
+
+       refname = parse_refname(&next);
+       if (!refname)
+               die("verify: missing <ref>");
+
+       if (parse_next_oid(&next, end, &old_oid, "verify", refname,
+                          PARSE_SHA1_OLD))
+               oidclr(&old_oid);
+
+       if (*next != line_termination)
+               die("verify %s: extra input: %s", refname, next);
+
+       if (ref_transaction_verify(transaction, refname, &old_oid,
+                                  update_flags, &err))
+               die("%s", err.buf);
+
+       update_flags = default_flags;
+       free(refname);
+       strbuf_release(&err);
+}
+
+static void parse_cmd_option(struct ref_transaction *transaction,
+                            const char *next, const char *end)
+{
+       const char *rest;
+       if (skip_prefix(next, "no-deref", &rest) && *rest == line_termination)
+               update_flags |= REF_NO_DEREF;
+       else
+               die("option unknown: %s", next);
+}
+
+static void parse_cmd_start(struct ref_transaction *transaction,
+                           const char *next, const char *end)
+{
+       if (*next != line_termination)
+               die("start: extra input: %s", next);
+       puts("start: ok");
+}
+
+static void parse_cmd_prepare(struct ref_transaction *transaction,
+                             const char *next, const char *end)
+{
+       struct strbuf error = STRBUF_INIT;
+       if (*next != line_termination)
+               die("prepare: extra input: %s", next);
+       if (ref_transaction_prepare(transaction, &error))
+               die("prepare: %s", error.buf);
+       puts("prepare: ok");
+}
+
+static void parse_cmd_abort(struct ref_transaction *transaction,
+                           const char *next, const char *end)
+{
+       struct strbuf error = STRBUF_INIT;
+       if (*next != line_termination)
+               die("abort: extra input: %s", next);
+       if (ref_transaction_abort(transaction, &error))
+               die("abort: %s", error.buf);
+       puts("abort: ok");
+}
+
+static void parse_cmd_commit(struct ref_transaction *transaction,
+                            const char *next, const char *end)
+{
+       struct strbuf error = STRBUF_INIT;
+       if (*next != line_termination)
+               die("commit: extra input: %s", next);
+       if (ref_transaction_commit(transaction, &error))
+               die("commit: %s", error.buf);
+       puts("commit: ok");
+       ref_transaction_free(transaction);
+}
+
+enum update_refs_state {
+       /* Non-transactional state open for updates. */
+       UPDATE_REFS_OPEN,
+       /* A transaction has been started. */
+       UPDATE_REFS_STARTED,
+       /* References are locked and ready for commit */
+       UPDATE_REFS_PREPARED,
+       /* Transaction has been committed or closed. */
+       UPDATE_REFS_CLOSED,
+};
+
+static const struct parse_cmd {
+       const char *prefix;
+       void (*fn)(struct ref_transaction *, const char *, const char *);
+       unsigned args;
+       enum update_refs_state state;
+} command[] = {
+       { "update",  parse_cmd_update,  3, UPDATE_REFS_OPEN },
+       { "create",  parse_cmd_create,  2, UPDATE_REFS_OPEN },
+       { "delete",  parse_cmd_delete,  2, UPDATE_REFS_OPEN },
+       { "verify",  parse_cmd_verify,  2, UPDATE_REFS_OPEN },
+       { "option",  parse_cmd_option,  1, UPDATE_REFS_OPEN },
+       { "start",   parse_cmd_start,   0, UPDATE_REFS_STARTED },
+       { "prepare", parse_cmd_prepare, 0, UPDATE_REFS_PREPARED },
+       { "abort",   parse_cmd_abort,   0, UPDATE_REFS_CLOSED },
+       { "commit",  parse_cmd_commit,  0, UPDATE_REFS_CLOSED },
+};
+
+static void update_refs_stdin(void)
+{
+       struct strbuf input = STRBUF_INIT, err = STRBUF_INIT;
+       enum update_refs_state state = UPDATE_REFS_OPEN;
+       struct ref_transaction *transaction;
+       int i, j;
+
+       transaction = ref_transaction_begin(&err);
+       if (!transaction)
+               die("%s", err.buf);
+
+       /* Read each line dispatch its command */
+       while (!strbuf_getwholeline(&input, stdin, line_termination)) {
+               const struct parse_cmd *cmd = NULL;
+
+               if (*input.buf == line_termination)
+                       die("empty command in input");
+               else if (isspace(*input.buf))
+                       die("whitespace before command: %s", input.buf);
+
+               for (i = 0; i < ARRAY_SIZE(command); i++) {
+                       const char *prefix = command[i].prefix;
+                       char c;
+
+                       if (!starts_with(input.buf, prefix))
+                               continue;
+
+                       /*
+                        * If the command has arguments, verify that it's
+                        * followed by a space. Otherwise, it shall be followed
+                        * by a line terminator.
+                        */
+                       c = command[i].args ? ' ' : line_termination;
+                       if (input.buf[strlen(prefix)] != c)
+                               continue;
+
+                       cmd = &command[i];
+                       break;
+               }
+               if (!cmd)
+                       die("unknown command: %s", input.buf);
+
+               /*
+                * Read additional arguments if NUL-terminated. Do not raise an
+                * error in case there is an early EOF to let the command
+                * handle missing arguments with a proper error message.
+                */
+               for (j = 1; line_termination == '\0' && j < cmd->args; j++)
+                       if (strbuf_appendwholeline(&input, stdin, line_termination))
+                               break;
+
+               switch (state) {
+               case UPDATE_REFS_OPEN:
+               case UPDATE_REFS_STARTED:
+                       if (state == UPDATE_REFS_STARTED && cmd->state == UPDATE_REFS_STARTED)
+                               die("cannot restart ongoing transaction");
+                       /* Do not downgrade a transaction to a non-transaction. */
+                       if (cmd->state >= state)
+                               state = cmd->state;
+                       break;
+               case UPDATE_REFS_PREPARED:
+                       if (cmd->state != UPDATE_REFS_CLOSED)
+                               die("prepared transactions can only be closed");
+                       state = cmd->state;
+                       break;
+               case UPDATE_REFS_CLOSED:
+                       if (cmd->state != UPDATE_REFS_STARTED)
+                               die("transaction is closed");
+
+                       /*
+                        * Open a new transaction if we're currently closed and
+                        * get a "start".
+                        */
+                       state = cmd->state;
+                       transaction = ref_transaction_begin(&err);
+                       if (!transaction)
+                               die("%s", err.buf);
+
+                       break;
+               }
+
+               cmd->fn(transaction, input.buf + strlen(cmd->prefix) + !!cmd->args,
+                       input.buf + input.len);
+       }
+
+       switch (state) {
+       case UPDATE_REFS_OPEN:
+               /* Commit by default if no transaction was requested. */
+               if (ref_transaction_commit(transaction, &err))
+                       die("%s", err.buf);
+               ref_transaction_free(transaction);
+               break;
+       case UPDATE_REFS_STARTED:
+       case UPDATE_REFS_PREPARED:
+               /* If using a transaction, we want to abort it. */
+               if (ref_transaction_abort(transaction, &err))
+                       die("%s", err.buf);
+               break;
+       case UPDATE_REFS_CLOSED:
+               /* Otherwise no need to do anything, the transaction was closed already. */
+               break;
+       }
+
+       strbuf_release(&err);
+       strbuf_release(&input);
+}
+
 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;
+       const char *refname, *oldval;
+       struct object_id oid, oldoid;
+       int delete = 0, no_deref = 0, read_stdin = 0, end_null = 0;
+       int create_reflog = 0;
        struct option options[] = {
                OPT_STRING( 'm', NULL, &msg, N_("reason"), N_("reason of the update")),
-               OPT_BOOLEAN('d', NULL, &delete, N_("delete the reference")),
-               OPT_BOOLEAN( 0 , "no-deref", &no_deref,
+               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_BOOL( 0 , "create-reflog", &create_reflog, N_("create a reflog")),
                OPT_END(),
        };
 
@@ -28,6 +512,25 @@ int cmd_update_ref(int argc, const char **argv, const char *prefix)
        if (msg && !*msg)
                die("Refusing to perform update with empty message.");
 
+       create_reflog_flag = create_reflog ? REF_FORCE_CREATE_REFLOG : 0;
+
+       if (no_deref) {
+               default_flags = REF_NO_DEREF;
+               update_flags = default_flags;
+       }
+
+       if (read_stdin) {
+               if (delete || argc > 0)
+                       usage_with_options(git_update_ref_usage, options);
+               if (end_null)
+                       line_termination = '\0';
+               update_refs_stdin();
+               return 0;
+       }
+
+       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);
@@ -40,19 +543,31 @@ int cmd_update_ref(int argc, const char **argv, const char *prefix)
                refname = argv[0];
                value = argv[1];
                oldval = argv[2];
-               if (get_sha1(value, sha1))
+               if (get_oid(value, &oid))
                        die("%s: not a valid SHA1", value);
        }
 
-       hashclr(oldsha1); /* all-zero hash in case oldval is the empty string */
-       if (oldval && *oldval && get_sha1(oldval, oldsha1))
-               die("%s: not a valid old SHA1", oldval);
+       if (oldval) {
+               if (!*oldval)
+                       /*
+                        * The empty string implies that the reference
+                        * must not already exist:
+                        */
+                       oidclr(&oldoid);
+               else if (get_oid(oldval, &oldoid))
+                       die("%s: not a valid old SHA1", oldval);
+       }
 
-       if (no_deref)
-               flags = REF_NODEREF;
        if (delete)
-               return delete_ref(refname, oldval ? oldsha1 : NULL, flags);
+               /*
+                * For purposes of backwards compatibility, we treat
+                * NULL_SHA1 as "don't care" here:
+                */
+               return delete_ref(msg, refname,
+                                 (oldval && !is_null_oid(&oldoid)) ? &oldoid : NULL,
+                                 default_flags);
        else
-               return update_ref(msg, refname, sha1, oldval ? oldsha1 : NULL,
-                                 flags, DIE_ON_ERR);
+               return update_ref(msg, refname, &oid, oldval ? &oldoid : NULL,
+                                 default_flags | create_reflog_flag,
+                                 UPDATE_REFS_DIE_ON_ERR);
 }