Merge branch 'ps/transactional-update-ref-stdin'

"git update-ref --stdin" learned a handful of new verbs to let the
user control ref update transactions more explicitly, which helps
as an ingredient to implement two-phase commit-style atomic
ref-updates across multiple repositories.

* ps/transactional-update-ref-stdin:
  update-ref: implement interactive transaction handling
  update-ref: read commands in a line-wise fashion
  update-ref: move transaction handling into `update_refs_stdin()`
  update-ref: pass end pointer instead of strbuf
  update-ref: drop unused argument for `parse_refname`
  update-ref: organize commands in an array
  strbuf: provide function to append whole lines
  git-update-ref.txt: add missing word
  refs: fix segfault when aborting empty transaction
maint
Junio C Hamano 2020-04-29 16:15:31 -07:00
commit d2ea03ddee
6 changed files with 364 additions and 76 deletions

View File

@ -66,6 +66,10 @@ performs all modifications together. Specify commands of the form:
delete SP <ref> [SP <oldvalue>] LF delete SP <ref> [SP <oldvalue>] LF
verify SP <ref> [SP <oldvalue>] LF verify SP <ref> [SP <oldvalue>] LF
option SP <opt> LF option SP <opt> LF
start LF
prepare LF
commit LF
abort LF


With `--create-reflog`, update-ref will create a reflog for each ref With `--create-reflog`, update-ref will create a reflog for each ref
even if one would not ordinarily be created. even if one would not ordinarily be created.
@ -83,6 +87,10 @@ quoting:
delete SP <ref> NUL [<oldvalue>] NUL delete SP <ref> NUL [<oldvalue>] NUL
verify SP <ref> NUL [<oldvalue>] NUL verify SP <ref> NUL [<oldvalue>] NUL
option SP <opt> NUL option SP <opt> NUL
start NUL
prepare NUL
commit NUL
abort NUL


In this format, use 40 "0" to specify a zero value, and use the empty In this format, use 40 "0" to specify a zero value, and use the empty
string to specify a missing value. string to specify a missing value.
@ -107,13 +115,31 @@ delete::


verify:: verify::
Verify <ref> against <oldvalue> but do not change it. If Verify <ref> against <oldvalue> but do not change it. If
<oldvalue> zero or missing, the ref must not exist. <oldvalue> is zero or missing, the ref must not exist.


option:: option::
Modify behavior of the next command naming a <ref>. Modify behavior of the next command naming a <ref>.
The only valid option is `no-deref` to avoid dereferencing The only valid option is `no-deref` to avoid dereferencing
a symbolic ref. a symbolic ref.


start::
Start a transaction. In contrast to a non-transactional session, a
transaction will automatically abort if the session ends without an
explicit commit.

prepare::
Prepare to commit the transaction. This will create lock files for all
queued reference updates. If one reference could not be locked, the
transaction will be aborted.

commit::
Commit all reference updates queued for the transaction, ending the
transaction.

abort::
Abort the transaction, releasing all locks if the transaction is in
prepared state.

If all <ref>s can be locked with matching <oldvalue>s If all <ref>s can be locked with matching <oldvalue>s
simultaneously, all modifications are performed. Otherwise, no simultaneously, all modifications are performed. Otherwise, no
modifications are performed. Note that while each individual modifications are performed. Note that while each individual

View File

@ -50,7 +50,7 @@ static const char *parse_arg(const char *next, struct strbuf *arg)
* the argument. Die if C-quoting is malformed or the reference name * the argument. Die if C-quoting is malformed or the reference name
* is invalid. * is invalid.
*/ */
static char *parse_refname(struct strbuf *input, const char **next) static char *parse_refname(const char **next)
{ {
struct strbuf ref = STRBUF_INIT; struct strbuf ref = STRBUF_INIT;


@ -95,7 +95,7 @@ static char *parse_refname(struct strbuf *input, const char **next)
* provided but cannot be converted to a SHA-1, die. flags can * provided but cannot be converted to a SHA-1, die. flags can
* include PARSE_SHA1_OLD and/or PARSE_SHA1_ALLOW_EMPTY. * include PARSE_SHA1_OLD and/or PARSE_SHA1_ALLOW_EMPTY.
*/ */
static int parse_next_oid(struct strbuf *input, const char **next, static int parse_next_oid(const char **next, const char *end,
struct object_id *oid, struct object_id *oid,
const char *command, const char *refname, const char *command, const char *refname,
int flags) int flags)
@ -103,7 +103,7 @@ static int parse_next_oid(struct strbuf *input, const char **next,
struct strbuf arg = STRBUF_INIT; struct strbuf arg = STRBUF_INIT;
int ret = 0; int ret = 0;


if (*next == input->buf + input->len) if (*next == end)
goto eof; goto eof;


if (line_termination) { if (line_termination) {
@ -128,7 +128,7 @@ static int parse_next_oid(struct strbuf *input, const char **next,
die("%s %s: expected NUL but got: %s", die("%s %s: expected NUL but got: %s",
command, refname, *next); command, refname, *next);
(*next)++; (*next)++;
if (*next == input->buf + input->len) if (*next == end)
goto eof; goto eof;
strbuf_addstr(&arg, *next); strbuf_addstr(&arg, *next);
*next += arg.len; *next += arg.len;
@ -178,23 +178,23 @@ static int parse_next_oid(struct strbuf *input, const char **next,
* depending on how line_termination is set. * depending on how line_termination is set.
*/ */


static const char *parse_cmd_update(struct ref_transaction *transaction, static void parse_cmd_update(struct ref_transaction *transaction,
struct strbuf *input, const char *next) const char *next, const char *end)
{ {
struct strbuf err = STRBUF_INIT; struct strbuf err = STRBUF_INIT;
char *refname; char *refname;
struct object_id new_oid, old_oid; struct object_id new_oid, old_oid;
int have_old; int have_old;


refname = parse_refname(input, &next); refname = parse_refname(&next);
if (!refname) if (!refname)
die("update: missing <ref>"); die("update: missing <ref>");


if (parse_next_oid(input, &next, &new_oid, "update", refname, if (parse_next_oid(&next, end, &new_oid, "update", refname,
PARSE_SHA1_ALLOW_EMPTY)) PARSE_SHA1_ALLOW_EMPTY))
die("update %s: missing <newvalue>", refname); die("update %s: missing <newvalue>", refname);


have_old = !parse_next_oid(input, &next, &old_oid, "update", refname, have_old = !parse_next_oid(&next, end, &old_oid, "update", refname,
PARSE_SHA1_OLD); PARSE_SHA1_OLD);


if (*next != line_termination) if (*next != line_termination)
@ -209,22 +209,20 @@ static const char *parse_cmd_update(struct ref_transaction *transaction,
update_flags = default_flags; update_flags = default_flags;
free(refname); free(refname);
strbuf_release(&err); strbuf_release(&err);

return next;
} }


static const char *parse_cmd_create(struct ref_transaction *transaction, static void parse_cmd_create(struct ref_transaction *transaction,
struct strbuf *input, const char *next) const char *next, const char *end)
{ {
struct strbuf err = STRBUF_INIT; struct strbuf err = STRBUF_INIT;
char *refname; char *refname;
struct object_id new_oid; struct object_id new_oid;


refname = parse_refname(input, &next); refname = parse_refname(&next);
if (!refname) if (!refname)
die("create: missing <ref>"); die("create: missing <ref>");


if (parse_next_oid(input, &next, &new_oid, "create", refname, 0)) if (parse_next_oid(&next, end, &new_oid, "create", refname, 0))
die("create %s: missing <newvalue>", refname); die("create %s: missing <newvalue>", refname);


if (is_null_oid(&new_oid)) if (is_null_oid(&new_oid))
@ -241,23 +239,21 @@ static const char *parse_cmd_create(struct ref_transaction *transaction,
update_flags = default_flags; update_flags = default_flags;
free(refname); free(refname);
strbuf_release(&err); strbuf_release(&err);

return next;
} }


static const char *parse_cmd_delete(struct ref_transaction *transaction, static void parse_cmd_delete(struct ref_transaction *transaction,
struct strbuf *input, const char *next) const char *next, const char *end)
{ {
struct strbuf err = STRBUF_INIT; struct strbuf err = STRBUF_INIT;
char *refname; char *refname;
struct object_id old_oid; struct object_id old_oid;
int have_old; int have_old;


refname = parse_refname(input, &next); refname = parse_refname(&next);
if (!refname) if (!refname)
die("delete: missing <ref>"); die("delete: missing <ref>");


if (parse_next_oid(input, &next, &old_oid, "delete", refname, if (parse_next_oid(&next, end, &old_oid, "delete", refname,
PARSE_SHA1_OLD)) { PARSE_SHA1_OLD)) {
have_old = 0; have_old = 0;
} else { } else {
@ -277,22 +273,20 @@ static const char *parse_cmd_delete(struct ref_transaction *transaction,
update_flags = default_flags; update_flags = default_flags;
free(refname); free(refname);
strbuf_release(&err); strbuf_release(&err);

return next;
} }


static const char *parse_cmd_verify(struct ref_transaction *transaction, static void parse_cmd_verify(struct ref_transaction *transaction,
struct strbuf *input, const char *next) const char *next, const char *end)
{ {
struct strbuf err = STRBUF_INIT; struct strbuf err = STRBUF_INIT;
char *refname; char *refname;
struct object_id old_oid; struct object_id old_oid;


refname = parse_refname(input, &next); refname = parse_refname(&next);
if (!refname) if (!refname)
die("verify: missing <ref>"); die("verify: missing <ref>");


if (parse_next_oid(input, &next, &old_oid, "verify", refname, if (parse_next_oid(&next, end, &old_oid, "verify", refname,
PARSE_SHA1_OLD)) PARSE_SHA1_OLD))
oidclr(&old_oid); oidclr(&old_oid);


@ -306,50 +300,179 @@ static const char *parse_cmd_verify(struct ref_transaction *transaction,
update_flags = default_flags; update_flags = default_flags;
free(refname); free(refname);
strbuf_release(&err); strbuf_release(&err);

return next;
} }


static const char *parse_cmd_option(struct strbuf *input, const char *next) static void parse_cmd_option(struct ref_transaction *transaction,
const char *next, const char *end)
{ {
const char *rest; const char *rest;
if (skip_prefix(next, "no-deref", &rest) && *rest == line_termination) if (skip_prefix(next, "no-deref", &rest) && *rest == line_termination)
update_flags |= REF_NO_DEREF; update_flags |= REF_NO_DEREF;
else else
die("option unknown: %s", next); die("option unknown: %s", next);
return rest;
} }


static void update_refs_stdin(struct ref_transaction *transaction) static void parse_cmd_start(struct ref_transaction *transaction,
const char *next, const char *end)
{ {
struct strbuf input = STRBUF_INIT; if (*next != line_termination)
const char *next; 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);


if (strbuf_read(&input, 0, 1000) < 0)
die_errno("could not read from stdin");
next = input.buf;
/* Read each line dispatch its command */ /* Read each line dispatch its command */
while (next < input.buf + input.len) { while (!strbuf_getwholeline(&input, stdin, line_termination)) {
if (*next == line_termination) const struct parse_cmd *cmd = NULL;
die("empty command in input");
else if (isspace(*next))
die("whitespace before command: %s", next);
else if (skip_prefix(next, "update ", &next))
next = parse_cmd_update(transaction, &input, next);
else if (skip_prefix(next, "create ", &next))
next = parse_cmd_create(transaction, &input, next);
else if (skip_prefix(next, "delete ", &next))
next = parse_cmd_delete(transaction, &input, next);
else if (skip_prefix(next, "verify ", &next))
next = parse_cmd_verify(transaction, &input, next);
else if (skip_prefix(next, "option ", &next))
next = parse_cmd_option(&input, next);
else
die("unknown command: %s", next);


next++; 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:
/* 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:
die("transaction is closed");
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); strbuf_release(&input);
} }


@ -384,21 +507,11 @@ int cmd_update_ref(int argc, const char **argv, const char *prefix)
} }


if (read_stdin) { if (read_stdin) {
struct strbuf err = STRBUF_INIT;
struct ref_transaction *transaction;

transaction = ref_transaction_begin(&err);
if (!transaction)
die("%s", err.buf);
if (delete || argc > 0) if (delete || argc > 0)
usage_with_options(git_update_ref_usage, options); usage_with_options(git_update_ref_usage, options);
if (end_null) if (end_null)
line_termination = '\0'; line_termination = '\0';
update_refs_stdin(transaction); update_refs_stdin();
if (ref_transaction_commit(transaction, &err))
die("%s", err.buf);
ref_transaction_free(transaction);
strbuf_release(&err);
return 0; return 0;
} }



View File

@ -2565,6 +2565,7 @@ static void files_transaction_cleanup(struct files_ref_store *refs,
} }
} }


if (backend_data) {
if (backend_data->packed_transaction && if (backend_data->packed_transaction &&
ref_transaction_abort(backend_data->packed_transaction, &err)) { ref_transaction_abort(backend_data->packed_transaction, &err)) {
error("error aborting transaction: %s", err.buf); error("error aborting transaction: %s", err.buf);
@ -2575,6 +2576,7 @@ static void files_transaction_cleanup(struct files_ref_store *refs,
packed_refs_unlock(refs->packed_ref_store); packed_refs_unlock(refs->packed_ref_store);


free(backend_data); free(backend_data);
}


transaction->state = REF_TRANSACTION_CLOSED; transaction->state = REF_TRANSACTION_CLOSED;
} }

View File

@ -690,6 +690,16 @@ int strbuf_getwholeline(struct strbuf *sb, FILE *fp, int term)
} }
#endif #endif


int strbuf_appendwholeline(struct strbuf *sb, FILE *fp, int term)
{
struct strbuf line = STRBUF_INIT;
if (strbuf_getwholeline(&line, fp, term))
return EOF;
strbuf_addbuf(sb, &line);
strbuf_release(&line);
return 0;
}

static int strbuf_getdelim(struct strbuf *sb, FILE *fp, int term) static int strbuf_getdelim(struct strbuf *sb, FILE *fp, int term)
{ {
if (strbuf_getwholeline(sb, fp, term)) if (strbuf_getwholeline(sb, fp, term))

View File

@ -502,6 +502,12 @@ int strbuf_getline(struct strbuf *sb, FILE *file);
*/ */
int strbuf_getwholeline(struct strbuf *sb, FILE *file, int term); int strbuf_getwholeline(struct strbuf *sb, FILE *file, int term);


/**
* Like `strbuf_getwholeline`, but appends the line instead of
* resetting the buffer first.
*/
int strbuf_appendwholeline(struct strbuf *sb, FILE *file, int term);

/** /**
* Like `strbuf_getwholeline`, but operates on a file descriptor. * Like `strbuf_getwholeline`, but operates on a file descriptor.
* It reads one character at a time, so it is very slow. Do not * It reads one character at a time, so it is very slow. Do not

View File

@ -1404,4 +1404,135 @@ test_expect_success 'handle per-worktree refs in refs/bisect' '
! test_cmp main-head worktree-head ! test_cmp main-head worktree-head
' '


test_expect_success 'transaction handles empty commit' '
cat >stdin <<-EOF &&
start
prepare
commit
EOF
git update-ref --stdin <stdin >actual &&
printf "%s: ok\n" start prepare commit >expect &&
test_cmp expect actual
'

test_expect_success 'transaction handles empty commit with missing prepare' '
cat >stdin <<-EOF &&
start
commit
EOF
git update-ref --stdin <stdin >actual &&
printf "%s: ok\n" start commit >expect &&
test_cmp expect actual
'

test_expect_success 'transaction handles sole commit' '
cat >stdin <<-EOF &&
commit
EOF
git update-ref --stdin <stdin >actual &&
printf "%s: ok\n" commit >expect &&
test_cmp expect actual
'

test_expect_success 'transaction handles empty abort' '
cat >stdin <<-EOF &&
start
prepare
abort
EOF
git update-ref --stdin <stdin >actual &&
printf "%s: ok\n" start prepare abort >expect &&
test_cmp expect actual
'

test_expect_success 'transaction exits on multiple aborts' '
cat >stdin <<-EOF &&
abort
abort
EOF
test_must_fail git update-ref --stdin <stdin >actual 2>err &&
printf "%s: ok\n" abort >expect &&
test_cmp expect actual &&
grep "fatal: transaction is closed" err
'

test_expect_success 'transaction exits on start after prepare' '
cat >stdin <<-EOF &&
prepare
start
EOF
test_must_fail git update-ref --stdin <stdin 2>err >actual &&
printf "%s: ok\n" prepare >expect &&
test_cmp expect actual &&
grep "fatal: prepared transactions can only be closed" err
'

test_expect_success 'transaction handles empty abort with missing prepare' '
cat >stdin <<-EOF &&
start
abort
EOF
git update-ref --stdin <stdin >actual &&
printf "%s: ok\n" start abort >expect &&
test_cmp expect actual
'

test_expect_success 'transaction handles sole abort' '
cat >stdin <<-EOF &&
abort
EOF
git update-ref --stdin <stdin >actual &&
printf "%s: ok\n" abort >expect &&
test_cmp expect actual
'

test_expect_success 'transaction can handle commit' '
cat >stdin <<-EOF &&
start
create $a HEAD
commit
EOF
git update-ref --stdin <stdin >actual &&
printf "%s: ok\n" start commit >expect &&
test_cmp expect actual &&
git rev-parse HEAD >expect &&
git rev-parse $a >actual &&
test_cmp expect actual
'

test_expect_success 'transaction can handle abort' '
cat >stdin <<-EOF &&
start
create $b HEAD
abort
EOF
git update-ref --stdin <stdin >actual &&
printf "%s: ok\n" start abort >expect &&
test_cmp expect actual &&
test_path_is_missing .git/$b
'

test_expect_success 'transaction aborts by default' '
cat >stdin <<-EOF &&
start
create $b HEAD
EOF
git update-ref --stdin <stdin >actual &&
printf "%s: ok\n" start >expect &&
test_cmp expect actual &&
test_path_is_missing .git/$b
'

test_expect_success 'transaction with prepare aborts by default' '
cat >stdin <<-EOF &&
start
create $b HEAD
prepare
EOF
git update-ref --stdin <stdin >actual &&
printf "%s: ok\n" start prepare >expect &&
test_cmp expect actual &&
test_path_is_missing .git/$b
'

test_done test_done