Skip to content

Commit e48cf33

Browse files
pks-tgitster
authored andcommitted
update-ref: implement interactive transaction handling
The git-update-ref(1) command can only handle queueing transactions right now via its "--stdin" parameter, but there is no way for users to handle the transaction itself in a more explicit way. E.g. in a replicated scenario, one may imagine a coordinator that spawns git-update-ref(1) for multiple repositories and only if all agree that an update is possible will the coordinator send a commit. Such a transactional session could look like > start < start: ok > update refs/heads/master $OLD $NEW > prepare < prepare: ok # All nodes have returned "ok" > commit < commit: ok or > start < start: ok > create refs/heads/master $OLD $NEW > prepare < fatal: cannot lock ref 'refs/heads/master': reference already exists # On all other nodes: > abort < abort: ok In order to allow for such transactional sessions, this commit introduces four new commands for git-update-ref(1), which matches those we have internally already with the exception of "start": - start: start a new transaction - prepare: prepare the transaction, that is try to lock all references and verify their current value matches the expected one - commit: explicitly commit a session, that is update references to match their new expected state - abort: abort a session and roll back all changes By design, git-update-ref(1) will commit as soon as standard input is being closed. While fine in a non-transactional world, it is definitely unexpected in a transactional world. Because of this, as soon as any of the new transactional commands is used, the default will change to aborting without an explicit "commit". To avoid a race between queueing updates and the first "prepare" that starts a transaction, the "start" command has been added to start an explicit transaction. Add some tests to exercise this new functionality. Signed-off-by: Patrick Steinhardt <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 94fd491 commit e48cf33

File tree

3 files changed

+255
-8
lines changed

3 files changed

+255
-8
lines changed

Documentation/git-update-ref.txt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ performs all modifications together. Specify commands of the form:
6666
delete SP <ref> [SP <oldvalue>] LF
6767
verify SP <ref> [SP <oldvalue>] LF
6868
option SP <opt> LF
69+
start LF
70+
prepare LF
71+
commit LF
72+
abort LF
6973

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

8795
In this format, use 40 "0" to specify a zero value, and use the empty
8896
string to specify a missing value.
@@ -114,6 +122,24 @@ option::
114122
The only valid option is `no-deref` to avoid dereferencing
115123
a symbolic ref.
116124

125+
start::
126+
Start a transaction. In contrast to a non-transactional session, a
127+
transaction will automatically abort if the session ends without an
128+
explicit commit.
129+
130+
prepare::
131+
Prepare to commit the transaction. This will create lock files for all
132+
queued reference updates. If one reference could not be locked, the
133+
transaction will be aborted.
134+
135+
commit::
136+
Commit all reference updates queued for the transaction, ending the
137+
transaction.
138+
139+
abort::
140+
Abort the transaction, releasing all locks if the transaction is in
141+
prepared state.
142+
117143
If all <ref>s can be locked with matching <oldvalue>s
118144
simultaneously, all modifications are performed. Otherwise, no
119145
modifications are performed. Note that while each individual

builtin/update-ref.c

Lines changed: 98 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -312,21 +312,80 @@ static void parse_cmd_option(struct ref_transaction *transaction,
312312
die("option unknown: %s", next);
313313
}
314314

315+
static void parse_cmd_start(struct ref_transaction *transaction,
316+
const char *next, const char *end)
317+
{
318+
if (*next != line_termination)
319+
die("start: extra input: %s", next);
320+
puts("start: ok");
321+
}
322+
323+
static void parse_cmd_prepare(struct ref_transaction *transaction,
324+
const char *next, const char *end)
325+
{
326+
struct strbuf error = STRBUF_INIT;
327+
if (*next != line_termination)
328+
die("prepare: extra input: %s", next);
329+
if (ref_transaction_prepare(transaction, &error))
330+
die("prepare: %s", error.buf);
331+
puts("prepare: ok");
332+
}
333+
334+
static void parse_cmd_abort(struct ref_transaction *transaction,
335+
const char *next, const char *end)
336+
{
337+
struct strbuf error = STRBUF_INIT;
338+
if (*next != line_termination)
339+
die("abort: extra input: %s", next);
340+
if (ref_transaction_abort(transaction, &error))
341+
die("abort: %s", error.buf);
342+
puts("abort: ok");
343+
}
344+
345+
static void parse_cmd_commit(struct ref_transaction *transaction,
346+
const char *next, const char *end)
347+
{
348+
struct strbuf error = STRBUF_INIT;
349+
if (*next != line_termination)
350+
die("commit: extra input: %s", next);
351+
if (ref_transaction_commit(transaction, &error))
352+
die("commit: %s", error.buf);
353+
puts("commit: ok");
354+
ref_transaction_free(transaction);
355+
}
356+
357+
enum update_refs_state {
358+
/* Non-transactional state open for updates. */
359+
UPDATE_REFS_OPEN,
360+
/* A transaction has been started. */
361+
UPDATE_REFS_STARTED,
362+
/* References are locked and ready for commit */
363+
UPDATE_REFS_PREPARED,
364+
/* Transaction has been committed or closed. */
365+
UPDATE_REFS_CLOSED,
366+
};
367+
315368
static const struct parse_cmd {
316369
const char *prefix;
317370
void (*fn)(struct ref_transaction *, const char *, const char *);
318371
unsigned args;
372+
enum update_refs_state state;
319373
} command[] = {
320-
{ "update", parse_cmd_update, 3 },
321-
{ "create", parse_cmd_create, 2 },
322-
{ "delete", parse_cmd_delete, 2 },
323-
{ "verify", parse_cmd_verify, 2 },
324-
{ "option", parse_cmd_option, 1 },
374+
{ "update", parse_cmd_update, 3, UPDATE_REFS_OPEN },
375+
{ "create", parse_cmd_create, 2, UPDATE_REFS_OPEN },
376+
{ "delete", parse_cmd_delete, 2, UPDATE_REFS_OPEN },
377+
{ "verify", parse_cmd_verify, 2, UPDATE_REFS_OPEN },
378+
{ "option", parse_cmd_option, 1, UPDATE_REFS_OPEN },
379+
{ "start", parse_cmd_start, 0, UPDATE_REFS_STARTED },
380+
{ "prepare", parse_cmd_prepare, 0, UPDATE_REFS_PREPARED },
381+
{ "abort", parse_cmd_abort, 0, UPDATE_REFS_CLOSED },
382+
{ "commit", parse_cmd_commit, 0, UPDATE_REFS_CLOSED },
325383
};
326384

327385
static void update_refs_stdin(void)
328386
{
329387
struct strbuf input = STRBUF_INIT, err = STRBUF_INIT;
388+
enum update_refs_state state = UPDATE_REFS_OPEN;
330389
struct ref_transaction *transaction;
331390
int i, j;
332391

@@ -374,14 +433,45 @@ static void update_refs_stdin(void)
374433
if (strbuf_appendwholeline(&input, stdin, line_termination))
375434
break;
376435

436+
switch (state) {
437+
case UPDATE_REFS_OPEN:
438+
case UPDATE_REFS_STARTED:
439+
/* Do not downgrade a transaction to a non-transaction. */
440+
if (cmd->state >= state)
441+
state = cmd->state;
442+
break;
443+
case UPDATE_REFS_PREPARED:
444+
if (cmd->state != UPDATE_REFS_CLOSED)
445+
die("prepared transactions can only be closed");
446+
state = cmd->state;
447+
break;
448+
case UPDATE_REFS_CLOSED:
449+
die("transaction is closed");
450+
break;
451+
}
452+
377453
cmd->fn(transaction, input.buf + strlen(cmd->prefix) + !!cmd->args,
378454
input.buf + input.len);
379455
}
380456

381-
if (ref_transaction_commit(transaction, &err))
382-
die("%s", err.buf);
457+
switch (state) {
458+
case UPDATE_REFS_OPEN:
459+
/* Commit by default if no transaction was requested. */
460+
if (ref_transaction_commit(transaction, &err))
461+
die("%s", err.buf);
462+
ref_transaction_free(transaction);
463+
break;
464+
case UPDATE_REFS_STARTED:
465+
case UPDATE_REFS_PREPARED:
466+
/* If using a transaction, we want to abort it. */
467+
if (ref_transaction_abort(transaction, &err))
468+
die("%s", err.buf);
469+
break;
470+
case UPDATE_REFS_CLOSED:
471+
/* Otherwise no need to do anything, the transaction was closed already. */
472+
break;
473+
}
383474

384-
ref_transaction_free(transaction);
385475
strbuf_release(&err);
386476
strbuf_release(&input);
387477
}

t/t1400-update-ref.sh

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1404,4 +1404,135 @@ test_expect_success 'handle per-worktree refs in refs/bisect' '
14041404
! test_cmp main-head worktree-head
14051405
'
14061406

1407+
test_expect_success 'transaction handles empty commit' '
1408+
cat >stdin <<-EOF &&
1409+
start
1410+
prepare
1411+
commit
1412+
EOF
1413+
git update-ref --stdin <stdin >actual &&
1414+
printf "%s: ok\n" start prepare commit >expect &&
1415+
test_cmp expect actual
1416+
'
1417+
1418+
test_expect_success 'transaction handles empty commit with missing prepare' '
1419+
cat >stdin <<-EOF &&
1420+
start
1421+
commit
1422+
EOF
1423+
git update-ref --stdin <stdin >actual &&
1424+
printf "%s: ok\n" start commit >expect &&
1425+
test_cmp expect actual
1426+
'
1427+
1428+
test_expect_success 'transaction handles sole commit' '
1429+
cat >stdin <<-EOF &&
1430+
commit
1431+
EOF
1432+
git update-ref --stdin <stdin >actual &&
1433+
printf "%s: ok\n" commit >expect &&
1434+
test_cmp expect actual
1435+
'
1436+
1437+
test_expect_success 'transaction handles empty abort' '
1438+
cat >stdin <<-EOF &&
1439+
start
1440+
prepare
1441+
abort
1442+
EOF
1443+
git update-ref --stdin <stdin >actual &&
1444+
printf "%s: ok\n" start prepare abort >expect &&
1445+
test_cmp expect actual
1446+
'
1447+
1448+
test_expect_success 'transaction exits on multiple aborts' '
1449+
cat >stdin <<-EOF &&
1450+
abort
1451+
abort
1452+
EOF
1453+
test_must_fail git update-ref --stdin <stdin >actual 2>err &&
1454+
printf "%s: ok\n" abort >expect &&
1455+
test_cmp expect actual &&
1456+
grep "fatal: transaction is closed" err
1457+
'
1458+
1459+
test_expect_success 'transaction exits on start after prepare' '
1460+
cat >stdin <<-EOF &&
1461+
prepare
1462+
start
1463+
EOF
1464+
test_must_fail git update-ref --stdin <stdin 2>err >actual &&
1465+
printf "%s: ok\n" prepare >expect &&
1466+
test_cmp expect actual &&
1467+
grep "fatal: prepared transactions can only be closed" err
1468+
'
1469+
1470+
test_expect_success 'transaction handles empty abort with missing prepare' '
1471+
cat >stdin <<-EOF &&
1472+
start
1473+
abort
1474+
EOF
1475+
git update-ref --stdin <stdin >actual &&
1476+
printf "%s: ok\n" start abort >expect &&
1477+
test_cmp expect actual
1478+
'
1479+
1480+
test_expect_success 'transaction handles sole abort' '
1481+
cat >stdin <<-EOF &&
1482+
abort
1483+
EOF
1484+
git update-ref --stdin <stdin >actual &&
1485+
printf "%s: ok\n" abort >expect &&
1486+
test_cmp expect actual
1487+
'
1488+
1489+
test_expect_success 'transaction can handle commit' '
1490+
cat >stdin <<-EOF &&
1491+
start
1492+
create $a HEAD
1493+
commit
1494+
EOF
1495+
git update-ref --stdin <stdin >actual &&
1496+
printf "%s: ok\n" start commit >expect &&
1497+
test_cmp expect actual &&
1498+
git rev-parse HEAD >expect &&
1499+
git rev-parse $a >actual &&
1500+
test_cmp expect actual
1501+
'
1502+
1503+
test_expect_success 'transaction can handle abort' '
1504+
cat >stdin <<-EOF &&
1505+
start
1506+
create $b HEAD
1507+
abort
1508+
EOF
1509+
git update-ref --stdin <stdin >actual &&
1510+
printf "%s: ok\n" start abort >expect &&
1511+
test_cmp expect actual &&
1512+
test_path_is_missing .git/$b
1513+
'
1514+
1515+
test_expect_success 'transaction aborts by default' '
1516+
cat >stdin <<-EOF &&
1517+
start
1518+
create $b HEAD
1519+
EOF
1520+
git update-ref --stdin <stdin >actual &&
1521+
printf "%s: ok\n" start >expect &&
1522+
test_cmp expect actual &&
1523+
test_path_is_missing .git/$b
1524+
'
1525+
1526+
test_expect_success 'transaction with prepare aborts by default' '
1527+
cat >stdin <<-EOF &&
1528+
start
1529+
create $b HEAD
1530+
prepare
1531+
EOF
1532+
git update-ref --stdin <stdin >actual &&
1533+
printf "%s: ok\n" start prepare >expect &&
1534+
test_cmp expect actual &&
1535+
test_path_is_missing .git/$b
1536+
'
1537+
14071538
test_done

0 commit comments

Comments
 (0)