diff options
author | Junio C Hamano <gitster@pobox.com> | 2024-04-26 09:28:43 -0700 |
---|---|---|
committer | Junio C Hamano <gitster@pobox.com> | 2024-04-26 09:28:43 -0700 |
commit | a9f388adb7d0e35de975a1ff643d0678ad8920d2 (patch) | |
tree | 55fb7e212261838570bdbb2560c1664cdc26d73a | |
parent | Merge branch 'rh/complete-symbolic-ref' into seen (diff) | |
parent | SQUASH??? (sparse fix) (diff) | |
download | git-a9f388adb7d0e35de975a1ff643d0678ad8920d2.tar.xz git-a9f388adb7d0e35de975a1ff643d0678ad8920d2.zip |
Merge branch 'kn/update-ref-symrefs' into seen
"update-ref" learns to also handle symbolic refs.
The design adds unnecessary ambiguities and needs rethought.
cf. <20240423220308.GC1172807@coredump.intra.peff.net>
* kn/update-ref-symrefs:
SQUASH??? (sparse fix)
ref: support symrefs in 'reference-transaction' hook
update-ref: support symrefs in the update command
update-ref: support symrefs in the create command
update-ref: support symrefs in the delete command
update-ref: support symrefs in the verify command
files-backend: extract out `create_symref_lock`
update-ref: support parsing ref targets in `parse_next_oid`
refs: accept symref values in `ref_transaction[_add]_update`
-rw-r--r-- | Documentation/git-update-ref.txt | 41 | ||||
-rw-r--r-- | Documentation/githooks.txt | 14 | ||||
-rw-r--r-- | branch.c | 2 | ||||
-rw-r--r-- | builtin/clone.c | 2 | ||||
-rw-r--r-- | builtin/fast-import.c | 5 | ||||
-rw-r--r-- | builtin/fetch.c | 4 | ||||
-rw-r--r-- | builtin/receive-pack.c | 4 | ||||
-rw-r--r-- | builtin/replace.c | 2 | ||||
-rw-r--r-- | builtin/tag.c | 1 | ||||
-rw-r--r-- | builtin/update-ref.c | 106 | ||||
-rw-r--r-- | refs.c | 78 | ||||
-rw-r--r-- | refs.h | 23 | ||||
-rw-r--r-- | refs/files-backend.c | 141 | ||||
-rw-r--r-- | refs/refs-internal.h | 20 | ||||
-rw-r--r-- | refs/reftable-backend.c | 49 | ||||
-rw-r--r-- | sequencer.c | 9 | ||||
-rwxr-xr-x | t/t0600-reffiles-backend.sh | 32 | ||||
-rwxr-xr-x | t/t1400-update-ref.sh | 346 | ||||
-rwxr-xr-x | t/t1416-ref-transaction-hooks.sh | 41 | ||||
-rw-r--r-- | walker.c | 2 |
20 files changed, 817 insertions, 105 deletions
diff --git a/Documentation/git-update-ref.txt b/Documentation/git-update-ref.txt index 374a2ebd2b0..79e29fead61 100644 --- a/Documentation/git-update-ref.txt +++ b/Documentation/git-update-ref.txt @@ -61,10 +61,10 @@ still contains <old-oid>. With `--stdin`, update-ref reads instructions from standard input and performs all modifications together. Specify commands of the form: - update SP <ref> SP <new-oid> [SP <old-oid>] LF - create SP <ref> SP <new-oid> LF - delete SP <ref> [SP <old-oid>] LF - verify SP <ref> [SP <old-oid>] LF + update SP <ref> SP (<new-oid> | ref:<new-target>) [SP (<old-oid> | ref:<old-target>)] LF + create SP <ref> SP (<new-oid> | ref:<new-target>) LF + delete SP <ref> [SP (<old-oid> | ref:<old-target>)] LF + verify SP <ref> [SP (<old-oid> | ref:<old-target>)] LF option SP <opt> LF start LF prepare LF @@ -82,10 +82,10 @@ specify a missing value, omit the value and its preceding SP entirely. Alternatively, use `-z` to specify in NUL-terminated format, without quoting: - update SP <ref> NUL <new-oid> NUL [<old-oid>] NUL - create SP <ref> NUL <new-oid> NUL - delete SP <ref> NUL [<old-oid>] NUL - verify SP <ref> NUL [<old-oid>] NUL + update SP <ref> NUL (<new-oid> | ref:<new-target>) NUL [(<old-oid> | ref:<old-target>)] NUL + create SP <ref> NUL (<new-oid> | ref:<new-target>) NUL + delete SP <ref> NUL [(<old-oid> | ref:<old-target>)] NUL + verify SP <ref> NUL [(<old-oid> | ref:<old-target>)] NUL option SP <opt> NUL start NUL prepare NUL @@ -95,6 +95,12 @@ quoting: In this format, use 40 "0" to specify a zero value, and use the empty string to specify a missing value. +For commands which support it, substituting the <old-oid> value with +ref:<old-target> will ensure that the <ref> targets the specified +old-target before the update. Similarly, substituting the <new-oid> +with ref:<new-target> will ensure that the <ref> is a symbolic ref +targeting the new-target after the update. + In either format, values can be specified in any form that Git recognizes as an object name. Commands in any other format or a repeated <ref> produce an error. Command meanings are: @@ -103,19 +109,28 @@ update:: Set <ref> to <new-oid> after verifying <old-oid>, if given. Specify a zero <new-oid> to ensure the ref does not exist after the update and/or a zero <old-oid> to make sure the - ref does not exist before the update. + ref does not exist before the update. If ref:<old-target> + is provided, we verify that the <ref> is an existing symbolic + ref which targets <old-target>. If ref:<new-target> is given, + the update ensures <ref> is a symbolic ref which targets + <new-target>. create:: Create <ref> with <new-oid> after verifying it does not - exist. The given <new-oid> may not be zero. + exist. The given <new-oid> may not be zero. If instead + ref:<new-target> is provided, a symbolic ref is created + which targets <new-target>. delete:: - Delete <ref> after verifying it exists with <old-oid>, if - given. If given, <old-oid> may not be zero. + Delete <ref> after verifying it exists with <old-oid>, if given. + If given, <old-oid> may not be zero. If instead, ref:<old-target> + is provided, verify that the symbolic ref <ref> targets + <old-target> before deleting it. verify:: Verify <ref> against <old-oid> but do not change it. If - <old-oid> is zero or missing, the ref must not exist. + <old-oid> is zero or missing, the ref must not exist. For + verifying symbolic refs, provide ref:<old-target>. option:: Modify the behavior of the next command naming a <ref>. diff --git a/Documentation/githooks.txt b/Documentation/githooks.txt index ee9b92c90da..0bf8ca87a6d 100644 --- a/Documentation/githooks.txt +++ b/Documentation/githooks.txt @@ -486,7 +486,7 @@ reference-transaction This hook is invoked by any Git command that performs reference updates. It executes whenever a reference transaction is prepared, committed or aborted and may thus get called multiple times. The hook -does not cover symbolic references (but that may change in the future). +also cover symbolic references. The hook takes exactly one argument, which is the current state the given reference transaction is in: @@ -503,16 +503,20 @@ given reference transaction is in: For each reference update that was added to the transaction, the hook receives on standard input a line of the format: - <old-oid> SP <new-oid> SP <ref-name> LF + <old-value> SP <new-value> SP <ref-name> LF -where `<old-oid>` is the old object name passed into the reference -transaction, `<new-oid>` is the new object name to be stored in the +where `<old-value>` is the old object name passed into the reference +transaction, `<new-value>` is the new object name to be stored in the ref and `<ref-name>` is the full name of the ref. When force updating the reference regardless of its current value or when the reference is -to be created anew, `<old-oid>` is the all-zeroes object name. To +to be created anew, `<old-value>` is the all-zeroes object name. To distinguish these cases, you can inspect the current value of `<ref-name>` via `git rev-parse`. +For symbolic reference updates the `<old_value>` and `<new-value>` +fields could denote references instead of objects, denoted via the +`ref:<ref-target>` format. + The exit status of the hook is ignored for any state except for the "prepared" state. In the "prepared" state, a non-zero exit status will cause the transaction to be aborted. The hook will not be called with @@ -627,7 +627,7 @@ void create_branch(struct repository *r, if (!transaction || ref_transaction_update(transaction, ref.buf, &oid, forcing ? NULL : null_oid(), - 0, msg, &err) || + NULL, NULL, 0, msg, &err) || ref_transaction_commit(transaction, &err)) die("%s", err.buf); ref_transaction_free(transaction); diff --git a/builtin/clone.c b/builtin/clone.c index 93fdfc945a2..ac1e131d8b4 100644 --- a/builtin/clone.c +++ b/builtin/clone.c @@ -546,7 +546,7 @@ static void write_remote_refs(const struct ref *local_refs) if (!r->peer_ref) continue; if (ref_transaction_create(t, r->peer_ref->name, &r->old_oid, - 0, NULL, &err)) + NULL, 0, NULL, &err)) die("%s", err.buf); } diff --git a/builtin/fast-import.c b/builtin/fast-import.c index 279d97b7310..f3c37f2c862 100644 --- a/builtin/fast-import.c +++ b/builtin/fast-import.c @@ -1634,7 +1634,7 @@ static int update_branch(struct branch *b) transaction = ref_transaction_begin(&err); if (!transaction || ref_transaction_update(transaction, b->name, &b->oid, &old_oid, - 0, msg, &err) || + NULL, NULL, 0, msg, &err) || ref_transaction_commit(transaction, &err)) { ref_transaction_free(transaction); error("%s", err.buf); @@ -1675,7 +1675,8 @@ static void dump_tags(void) strbuf_addf(&ref_name, "refs/tags/%s", t->name); if (ref_transaction_update(transaction, ref_name.buf, - &t->oid, NULL, 0, msg, &err)) { + &t->oid, NULL, NULL, NULL, + 0, msg, &err)) { failure |= error("%s", err.buf); goto cleanup; } diff --git a/builtin/fetch.c b/builtin/fetch.c index 5857d860dbf..d02592efca2 100644 --- a/builtin/fetch.c +++ b/builtin/fetch.c @@ -668,7 +668,7 @@ static int s_update_ref(const char *action, ret = ref_transaction_update(transaction, ref->name, &ref->new_oid, check_old ? &ref->old_oid : NULL, - 0, msg, &err); + NULL, NULL, 0, msg, &err); if (ret) { ret = STORE_REF_ERROR_OTHER; goto out; @@ -1383,7 +1383,7 @@ static int prune_refs(struct display_state *display_state, if (transaction) { for (ref = stale_refs; ref; ref = ref->next) { result = ref_transaction_delete(transaction, ref->name, NULL, 0, - "fetch: prune", &err); + NULL, "fetch: prune", &err); if (result) goto cleanup; } diff --git a/builtin/receive-pack.c b/builtin/receive-pack.c index e8d7df14b6c..9a4667d57d7 100644 --- a/builtin/receive-pack.c +++ b/builtin/receive-pack.c @@ -1576,7 +1576,8 @@ static const char *update(struct command *cmd, struct shallow_info *si) if (ref_transaction_delete(transaction, namespaced_name, old_oid, - 0, "push", &err)) { + 0, NULL, + "push", &err)) { rp_error("%s", err.buf); ret = "failed to delete"; } else { @@ -1595,6 +1596,7 @@ static const char *update(struct command *cmd, struct shallow_info *si) if (ref_transaction_update(transaction, namespaced_name, new_oid, old_oid, + NULL, NULL, 0, "push", &err)) { rp_error("%s", err.buf); diff --git a/builtin/replace.c b/builtin/replace.c index da59600ad22..7690687b0ed 100644 --- a/builtin/replace.c +++ b/builtin/replace.c @@ -201,7 +201,7 @@ static int replace_object_oid(const char *object_ref, transaction = ref_transaction_begin(&err); if (!transaction || ref_transaction_update(transaction, ref.buf, repl, &prev, - 0, NULL, &err) || + NULL, NULL, 0, NULL, &err) || ref_transaction_commit(transaction, &err)) res = error("%s", err.buf); diff --git a/builtin/tag.c b/builtin/tag.c index 9a33cb50b45..40a65fdebce 100644 --- a/builtin/tag.c +++ b/builtin/tag.c @@ -660,6 +660,7 @@ int cmd_tag(int argc, const char **argv, const char *prefix) transaction = ref_transaction_begin(&err); if (!transaction || ref_transaction_update(transaction, ref.buf, &object, &prev, + NULL, NULL, create_reflog ? REF_FORCE_CREATE_REFLOG : 0, reflog_msg.buf, &err) || ref_transaction_commit(transaction, &err)) { diff --git a/builtin/update-ref.c b/builtin/update-ref.c index e46afbc46d9..1cdafc33f39 100644 --- a/builtin/update-ref.c +++ b/builtin/update-ref.c @@ -89,16 +89,24 @@ static char *parse_refname(const char **next) #define PARSE_SHA1_ALLOW_EMPTY 0x02 /* + * Parse refname targets using the ref:<ref_target> format. + */ +#define PARSE_REFNAME_TARGETS 0x04 + +/* * 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. + * include PARSE_SHA1_OLD and/or PARSE_SHA1_ALLOW_EMPTY and/or + * PARSE_REFNAME_TARGETS. When PARSE_REFNAME_TARGETS is set, parse + * the argument as `ref:<refname>` and store the refname into + * the target strbuf. */ -static int parse_next_oid(const char **next, const char *end, - struct object_id *oid, +static int parse_next_arg(const char **next, const char *end, + struct object_id *oid, struct strbuf *target, const char *command, const char *refname, int flags) { @@ -118,8 +126,17 @@ static int parse_next_oid(const char **next, const char *end, (*next)++; *next = parse_arg(*next, &arg); if (arg.len) { - if (repo_get_oid(the_repository, arg.buf, oid)) - goto invalid; + if (repo_get_oid(the_repository, arg.buf, oid)) { + const char *value; + if (flags & PARSE_REFNAME_TARGETS && + skip_prefix(arg.buf, "ref:", &value)) { + if (check_refname_format(value, REFNAME_ALLOW_ONELEVEL)) + die("invalid ref format: %s", value); + strbuf_addstr(target, value); + } else { + goto invalid; + } + } } else { /* Without -z, an empty value means all zeros: */ oidclr(oid); @@ -136,8 +153,17 @@ static int parse_next_oid(const char **next, const char *end, *next += arg.len; if (arg.len) { - if (repo_get_oid(the_repository, arg.buf, oid)) - goto invalid; + if (repo_get_oid(the_repository, arg.buf, oid)) { + const char *value; + if (flags & PARSE_REFNAME_TARGETS && + skip_prefix(arg.buf, "ref:", &value)) { + if (check_refname_format(value, REFNAME_ALLOW_ONELEVEL)) + die("invalid ref format: %s", value); + strbuf_addstr(target, value); + } else { + goto invalid; + } + } } else if (flags & PARSE_SHA1_ALLOW_EMPTY) { /* With -z, treat an empty value as all zeros: */ warning("%s %s: missing <new-oid>, treating as zero", @@ -184,6 +210,8 @@ static void parse_cmd_update(struct ref_transaction *transaction, const char *next, const char *end) { struct strbuf err = STRBUF_INIT; + struct strbuf new_target = STRBUF_INIT; + struct strbuf old_target = STRBUF_INIT; char *refname; struct object_id new_oid, old_oid; int have_old; @@ -192,18 +220,24 @@ static void parse_cmd_update(struct ref_transaction *transaction, if (!refname) die("update: missing <ref>"); - if (parse_next_oid(&next, end, &new_oid, "update", refname, - PARSE_SHA1_ALLOW_EMPTY)) + if (parse_next_arg(&next, end, &new_oid, + &new_target, "update", refname, + PARSE_SHA1_ALLOW_EMPTY | PARSE_REFNAME_TARGETS)) die("update %s: missing <new-oid>", refname); - have_old = !parse_next_oid(&next, end, &old_oid, "update", refname, - PARSE_SHA1_OLD); + have_old = !parse_next_arg(&next, end, &old_oid, + &old_target, "update", refname, + PARSE_SHA1_OLD | PARSE_REFNAME_TARGETS); + have_old = have_old && !old_target.len; 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, + new_target.len ? NULL : &new_oid, + have_old ? &old_oid : NULL, + new_target.len ? new_target.buf : NULL, + old_target.len ? old_target.buf : NULL, update_flags | create_reflog_flag, msg, &err)) die("%s", err.buf); @@ -211,12 +245,15 @@ static void parse_cmd_update(struct ref_transaction *transaction, update_flags = default_flags; free(refname); strbuf_release(&err); + strbuf_release(&old_target); + strbuf_release(&new_target); } static void parse_cmd_create(struct ref_transaction *transaction, const char *next, const char *end) { struct strbuf err = STRBUF_INIT; + struct strbuf new_target = STRBUF_INIT; char *refname; struct object_id new_oid; @@ -224,16 +261,22 @@ static void parse_cmd_create(struct ref_transaction *transaction, if (!refname) die("create: missing <ref>"); - if (parse_next_oid(&next, end, &new_oid, "create", refname, 0)) + if (parse_next_arg(&next, end, &new_oid, &new_target, + "create", refname, PARSE_REFNAME_TARGETS)) die("create %s: missing <new-oid>", refname); - if (is_null_oid(&new_oid)) + if (!new_target.len && is_null_oid(&new_oid)) die("create %s: zero <new-oid>", refname); + if (new_target.len && !(update_flags & REF_NO_DEREF)) + die("create %s: cannot create symrefs in deref mode", refname); + if (*next != line_termination) die("create %s: extra input: %s", refname, next); - if (ref_transaction_create(transaction, refname, &new_oid, + if (ref_transaction_create(transaction, refname, + new_target.len ? NULL : &new_oid , + new_target.len ? new_target.buf : NULL, update_flags | create_reflog_flag, msg, &err)) die("%s", err.buf); @@ -241,12 +284,14 @@ static void parse_cmd_create(struct ref_transaction *transaction, update_flags = default_flags; free(refname); strbuf_release(&err); + strbuf_release(&new_target); } static void parse_cmd_delete(struct ref_transaction *transaction, const char *next, const char *end) { struct strbuf err = STRBUF_INIT; + struct strbuf old_target = STRBUF_INIT; char *refname; struct object_id old_oid; int have_old; @@ -255,32 +300,40 @@ static void parse_cmd_delete(struct ref_transaction *transaction, if (!refname) die("delete: missing <ref>"); - if (parse_next_oid(&next, end, &old_oid, "delete", refname, - PARSE_SHA1_OLD)) { + if (parse_next_arg(&next, end, &old_oid, &old_target, + "delete", refname, PARSE_SHA1_OLD | + PARSE_REFNAME_TARGETS)) { have_old = 0; } else { - if (is_null_oid(&old_oid)) + if (!old_target.len && is_null_oid(&old_oid)) die("delete %s: zero <old-oid>", refname); - have_old = 1; + have_old = 1 && !old_target.len; } + if (old_target.len && !(update_flags & REF_NO_DEREF)) + die("delete %s: cannot operate on symrefs in deref mode", refname); + 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)) + update_flags, + old_target.len ? old_target.buf : NULL, + msg, &err)) die("%s", err.buf); update_flags = default_flags; free(refname); strbuf_release(&err); + strbuf_release(&old_target); } static void parse_cmd_verify(struct ref_transaction *transaction, const char *next, const char *end) { struct strbuf err = STRBUF_INIT; + struct strbuf old_target = STRBUF_INIT; char *refname; struct object_id old_oid; @@ -288,20 +341,27 @@ static void parse_cmd_verify(struct ref_transaction *transaction, if (!refname) die("verify: missing <ref>"); - if (parse_next_oid(&next, end, &old_oid, "verify", refname, - PARSE_SHA1_OLD)) + if (parse_next_arg(&next, end, &old_oid, &old_target, + "verify", refname, + PARSE_SHA1_OLD | PARSE_REFNAME_TARGETS)) oidclr(&old_oid); + if (old_target.len && !(update_flags & REF_NO_DEREF)) + die("verify %s: cannot operate on symrefs in deref mode", refname); + if (*next != line_termination) die("verify %s: extra input: %s", refname, next); - if (ref_transaction_verify(transaction, refname, &old_oid, + if (ref_transaction_verify(transaction, refname, + old_target.len ? NULL : &old_oid, + old_target.len ? old_target.buf : NULL, update_flags, &err)) die("%s", err.buf); update_flags = default_flags; free(refname); strbuf_release(&err); + strbuf_release(&old_target); } static void report_ok(const char *command) @@ -979,7 +979,7 @@ int refs_delete_ref(struct ref_store *refs, const char *msg, transaction = ref_store_transaction_begin(refs, &err); if (!transaction || ref_transaction_delete(transaction, refname, old_oid, - flags, msg, &err) || + flags, NULL, msg, &err) || ref_transaction_commit(transaction, &err)) { error("%s", err.buf); ref_transaction_free(transaction); @@ -1217,6 +1217,8 @@ void ref_transaction_free(struct ref_transaction *transaction) for (i = 0; i < transaction->nr; i++) { free(transaction->updates[i]->msg); + free((void *)transaction->updates[i]->old_target); + free((void *)transaction->updates[i]->new_target); free(transaction->updates[i]); } free(transaction->updates); @@ -1228,6 +1230,7 @@ struct ref_update *ref_transaction_add_update( const char *refname, unsigned int flags, const struct object_id *new_oid, const struct object_id *old_oid, + const char *new_target, const char *old_target, const char *msg) { struct ref_update *update; @@ -1235,15 +1238,24 @@ struct ref_update *ref_transaction_add_update( if (transaction->state != REF_TRANSACTION_OPEN) BUG("update called for transaction that is not open"); + if (old_oid && !is_null_oid(old_oid) && old_target) + BUG("Only one of old_oid and old_target should be non NULL"); + if (new_oid && !is_null_oid(new_oid) && new_target) + BUG("Only one of new_oid and new_target should be non NULL"); + FLEX_ALLOC_STR(update, refname, refname); ALLOC_GROW(transaction->updates, transaction->nr + 1, transaction->alloc); transaction->updates[transaction->nr++] = update; update->flags = flags; - if (flags & REF_HAVE_NEW) + if (new_target) + update->new_target = xstrdup(new_target); + if (old_target) + update->old_target = xstrdup(old_target); + if (new_oid && flags & REF_HAVE_NEW) oidcpy(&update->new_oid, new_oid); - if (flags & REF_HAVE_OLD) + if (old_oid && flags & REF_HAVE_OLD) oidcpy(&update->old_oid, old_oid); update->msg = normalize_reflog_message(msg); return update; @@ -1253,6 +1265,8 @@ int ref_transaction_update(struct ref_transaction *transaction, const char *refname, const struct object_id *new_oid, const struct object_id *old_oid, + const char *new_target, + const char *old_target, unsigned int flags, const char *msg, struct strbuf *err) { @@ -1278,49 +1292,64 @@ int ref_transaction_update(struct ref_transaction *transaction, flags &= REF_TRANSACTION_UPDATE_ALLOWED_FLAGS; flags |= (new_oid ? REF_HAVE_NEW : 0) | (old_oid ? REF_HAVE_OLD : 0); + flags |= (new_target ? REF_HAVE_NEW : 0) | (old_target ? REF_HAVE_OLD : 0); ref_transaction_add_update(transaction, refname, flags, - new_oid, old_oid, msg); + new_oid, old_oid, new_target, + old_target, msg); return 0; } int ref_transaction_create(struct ref_transaction *transaction, const char *refname, const struct object_id *new_oid, + const char *new_target, unsigned int flags, const char *msg, struct strbuf *err) { - if (!new_oid || is_null_oid(new_oid)) { - strbuf_addf(err, "'%s' has a null OID", refname); + if ((!new_oid || is_null_oid(new_oid)) && !new_target) { + strbuf_addf(err, "'%s' has a null OID or no new target", refname); return 1; } + if (new_target && !(flags & REF_NO_DEREF)) + BUG("create cannot operate on symrefs with deref mode"); return ref_transaction_update(transaction, refname, new_oid, - null_oid(), flags, msg, err); + null_oid(), new_target, NULL, flags, + msg, err); } int ref_transaction_delete(struct ref_transaction *transaction, const char *refname, const struct object_id *old_oid, - unsigned int flags, const char *msg, + unsigned int flags, + const char *old_target, + const char *msg, struct strbuf *err) { if (old_oid && is_null_oid(old_oid)) BUG("delete called with old_oid set to zeros"); + if (old_target && !(flags & REF_NO_DEREF)) + BUG("delete cannot operate on symrefs with deref mode"); return ref_transaction_update(transaction, refname, null_oid(), old_oid, - flags, msg, err); + NULL, old_target, flags, + msg, err); } int ref_transaction_verify(struct ref_transaction *transaction, const char *refname, const struct object_id *old_oid, + const char *old_target, unsigned int flags, struct strbuf *err) { - if (!old_oid) - BUG("verify called with old_oid set to NULL"); + if (!old_target && !old_oid) + BUG("verify called with old_oid and old_target set to NULL"); + if (old_target && !(flags & REF_NO_DEREF)) + BUG("verify cannot operate on symrefs with deref mode"); return ref_transaction_update(transaction, refname, NULL, old_oid, + NULL, old_target, flags, NULL, err); } @@ -1335,8 +1364,8 @@ int refs_update_ref(struct ref_store *refs, const char *msg, t = ref_store_transaction_begin(refs, &err); if (!t || - ref_transaction_update(t, refname, new_oid, old_oid, flags, msg, - &err) || + ref_transaction_update(t, refname, new_oid, old_oid, NULL, NULL, + flags, msg, &err) || ref_transaction_commit(t, &err)) { ret = 1; ref_transaction_free(t); @@ -2336,12 +2365,19 @@ static int run_transaction_hook(struct ref_transaction *transaction, for (i = 0; i < transaction->nr; i++) { struct ref_update *update = transaction->updates[i]; - strbuf_reset(&buf); - strbuf_addf(&buf, "%s %s %s\n", - oid_to_hex(&update->old_oid), - oid_to_hex(&update->new_oid), - update->refname); + + if (update->flags & REF_HAVE_OLD && update->old_target) + strbuf_addf(&buf, "ref:%s ", update->old_target); + else + strbuf_addf(&buf, "%s ", oid_to_hex(&update->old_oid)); + + if (update->flags & REF_HAVE_NEW && update->new_target) + strbuf_addf(&buf, "ref:%s ", update->new_target); + else + strbuf_addf(&buf, "%s ", oid_to_hex(&update->new_oid)); + + strbuf_addf(&buf, "%s\n", update->refname); if (write_in_full(proc.in, buf.buf, buf.len) < 0) { if (errno != EPIPE) { @@ -2724,7 +2760,7 @@ int refs_delete_refs(struct ref_store *refs, const char *logmsg, for_each_string_list_item(item, refnames) { ret = ref_transaction_delete(transaction, item->string, - NULL, flags, msg, &err); + NULL, flags, NULL, msg, &err); if (ret) { warning(_("could not delete reference %s: %s"), item->string, err.buf); @@ -2790,3 +2826,7 @@ int copy_existing_ref(const char *oldref, const char *newref, const char *logmsg { return refs_copy_existing_ref(get_main_ref_store(the_repository), oldref, newref, logmsg); } + +int ref_update_is_null_new_value(struct ref_update *update) { + return !update->new_target && is_null_oid(&update->new_oid); +} @@ -648,6 +648,15 @@ struct ref_transaction *ref_transaction_begin(struct strbuf *err); * before the update. A copy of this value is made in the * transaction. * + * new_target -- the target reference that the reference will be + * update to point to. This takes precedence over new_oid when + * set. If the reference is a regular reference, it will be + * converted to a symbolic reference. + * + * old_target -- the reference that the reference must be pointing to. + * Will only be taken into account when the reference is a symbolic + * reference. + * * flags -- flags affecting the update, passed to * update_ref_lock(). Possible flags: REF_NO_DEREF, * REF_FORCE_CREATE_REFLOG. See those constants for more @@ -713,7 +722,11 @@ struct ref_transaction *ref_transaction_begin(struct strbuf *err); * beforehand. The old value is checked after the lock is taken to * prevent races. If the old value doesn't agree with old_oid, the * whole transaction fails. If old_oid is NULL, then the previous - * value is not checked. + * value is not checked. If `old_target` is not NULL, treat the reference + * as a symbolic ref and validate that its target before the update is + * `old_target`. If the `new_target` is not NULL, then the reference + * will be updated to a symbolic ref which targets `new_target`. + * Together, these allow us to update between regular refs and symrefs. * * See the above comment "Reference transaction updates" for more * information. @@ -722,6 +735,8 @@ int ref_transaction_update(struct ref_transaction *transaction, const char *refname, const struct object_id *new_oid, const struct object_id *old_oid, + const char *new_target, + const char *old_target, unsigned int flags, const char *msg, struct strbuf *err); @@ -737,6 +752,7 @@ int ref_transaction_update(struct ref_transaction *transaction, int ref_transaction_create(struct ref_transaction *transaction, const char *refname, const struct object_id *new_oid, + const char *new_target, unsigned int flags, const char *msg, struct strbuf *err); @@ -751,7 +767,9 @@ int ref_transaction_create(struct ref_transaction *transaction, int ref_transaction_delete(struct ref_transaction *transaction, const char *refname, const struct object_id *old_oid, - unsigned int flags, const char *msg, + unsigned int flags, + const char *old_target, + const char *msg, struct strbuf *err); /* @@ -765,6 +783,7 @@ int ref_transaction_delete(struct ref_transaction *transaction, int ref_transaction_verify(struct ref_transaction *transaction, const char *refname, const struct object_id *old_oid, + const char *old_target, unsigned int flags, struct strbuf *err); diff --git a/refs/files-backend.c b/refs/files-backend.c index a098d14ea00..59d1ab3eeb1 100644 --- a/refs/files-backend.c +++ b/refs/files-backend.c @@ -1198,7 +1198,7 @@ static void prune_ref(struct files_ref_store *refs, struct ref_to_prune *r) ref_transaction_add_update( transaction, r->name, REF_NO_DEREF | REF_HAVE_NEW | REF_HAVE_OLD | REF_IS_PRUNING, - null_oid(), &r->oid, NULL); + null_oid(), &r->oid, NULL, NULL, NULL); if (ref_transaction_commit(transaction, &err)) goto cleanup; @@ -1292,7 +1292,7 @@ static int files_pack_refs(struct ref_store *ref_store, * packed-refs transaction: */ if (ref_transaction_update(transaction, iter->refname, - iter->oid, NULL, + iter->oid, NULL, NULL, NULL, REF_NO_DEREF, NULL, &err)) die("failure preparing to create packed reference %s: %s", iter->refname, err.buf); @@ -1920,26 +1920,39 @@ static void update_symref_reflog(struct files_ref_store *refs, } } -static int create_symref_locked(struct files_ref_store *refs, - struct ref_lock *lock, const char *refname, - const char *target, const char *logmsg) +static int create_symref_lock(struct files_ref_store *refs, + struct ref_lock *lock, const char *refname, + const char *target) { + if (!fdopen_lock_file(&lock->lk, "w")) + return error("unable to fdopen %s: %s", + get_lock_file_path(&lock->lk), strerror(errno)); + + /* no error check; commit_ref will check ferror */ + fprintf(get_lock_file_fp(&lock->lk), "ref: %s\n", target); + return 0; +} + +static int create_and_commit_symref(struct files_ref_store *refs, + struct ref_lock *lock, const char *refname, + const char *target, const char *logmsg) +{ + int ret; + if (prefer_symlink_refs && !create_ref_symlink(lock, target)) { update_symref_reflog(refs, lock, refname, target, logmsg); return 0; } - if (!fdopen_lock_file(&lock->lk, "w")) - return error("unable to fdopen %s: %s", - get_lock_file_path(&lock->lk), strerror(errno)); + ret = create_symref_lock(refs, lock, refname, target); + if (!ret) { + update_symref_reflog(refs, lock, refname, target, logmsg); - update_symref_reflog(refs, lock, refname, target, logmsg); + if (commit_ref(lock) < 0) + return error("unable to write symref for %s: %s", refname, + strerror(errno)); + } - /* no error check; commit_ref will check ferror */ - fprintf(get_lock_file_fp(&lock->lk), "ref: %s\n", target); - if (commit_ref(lock) < 0) - return error("unable to write symref for %s: %s", refname, - strerror(errno)); return 0; } @@ -1960,7 +1973,8 @@ static int files_create_symref(struct ref_store *ref_store, return -1; } - ret = create_symref_locked(refs, lock, refname, target, logmsg); + ret = create_and_commit_symref(refs, lock, refname, target, logmsg); + unlock_ref(lock); return ret; } @@ -2309,7 +2323,7 @@ static int split_head_update(struct ref_update *update, transaction, "HEAD", update->flags | REF_LOG_ONLY | REF_NO_DEREF, &update->new_oid, &update->old_oid, - update->msg); + NULL, NULL, update->msg); /* * Add "HEAD". This insertion is O(N) in the transaction @@ -2372,6 +2386,7 @@ static int split_symref_update(struct ref_update *update, new_update = ref_transaction_add_update( transaction, referent, new_flags, &update->new_oid, &update->old_oid, + update->new_target, update->old_target, update->msg); new_update->parent_update = update; @@ -2412,6 +2427,37 @@ static const char *original_update_refname(struct ref_update *update) } /* + * Check whether the REF_HAVE_OLD and old_target values stored in + * update are consistent with ref, which is the symbolic reference's + * current value. If everything is OK, return 0; otherwise, write an + * error message to err and return -1. + */ +static int check_old_target(struct ref_update *update, char *ref, + struct strbuf *err) +{ + if (!(update->flags & REF_HAVE_OLD) || + !strcmp(update->old_target, ref)) + return 0; + + if (!strcmp(update->old_target, "")) + strbuf_addf(err, "cannot lock ref '%s': " + "reference already exists", + original_update_refname(update)); + else if (!strcmp(ref, "")) + strbuf_addf(err, "cannot lock ref '%s': " + "reference is missing but expected %s", + original_update_refname(update), + update->old_target); + else + strbuf_addf(err, "cannot lock ref '%s': " + "is at %s but expected %s", + original_update_refname(update), + ref, update->old_target); + + return -1; +} + +/* * Check whether the REF_HAVE_OLD and old_oid values stored in update * are consistent with oid, which is the reference's current value. If * everything is OK, return 0; otherwise, write an error message to @@ -2471,7 +2517,7 @@ static int lock_ref_for_update(struct files_ref_store *refs, files_assert_main_repository(refs, "lock_ref_for_update"); - if ((update->flags & REF_HAVE_NEW) && is_null_oid(&update->new_oid)) + if ((update->flags & REF_HAVE_NEW) && ref_update_is_null_new_value(update)) update->flags |= REF_DELETING; if (head_ref) { @@ -2514,6 +2560,18 @@ static int lock_ref_for_update(struct files_ref_store *refs, ret = TRANSACTION_GENERIC_ERROR; goto out; } + } + + /* + * For symref verification, we need to check the reference value + * rather than the oid. If we're dealing with regular refs or we're + * verifying a dereferenced symref, we then check the oid. + */ + if (update->old_target) { + if (check_old_target(update, referent.buf, err)) { + ret = TRANSACTION_GENERIC_ERROR; + goto out; + } } else if (check_old_oid(update, &lock->old_oid, err)) { ret = TRANSACTION_GENERIC_ERROR; goto out; @@ -2553,9 +2611,27 @@ static int lock_ref_for_update(struct files_ref_store *refs, } } - if ((update->flags & REF_HAVE_NEW) && - !(update->flags & REF_DELETING) && - !(update->flags & REF_LOG_ONLY)) { + if (update->new_target && !(update->flags & REF_LOG_ONLY)) { + if (create_symref_lock(refs, lock, update->refname, update->new_target)) { + ret = TRANSACTION_GENERIC_ERROR; + goto out; + } + + if (close_ref_gently(lock)) { + strbuf_addf(err, "couldn't close '%s.lock'", + update->refname); + ret = TRANSACTION_GENERIC_ERROR; + goto out; + } + + /* + * Once we have created the symref lock, the commit + * phase of the transaction only needs to commit the lock. + */ + update->flags |= REF_NEEDS_COMMIT; + } else if ((update->flags & REF_HAVE_NEW) && + !(update->flags & REF_DELETING) && + !(update->flags & REF_LOG_ONLY)) { if (!(update->type & REF_ISSYMREF) && oideq(&lock->old_oid, &update->new_oid)) { /* @@ -2763,7 +2839,7 @@ static int files_transaction_prepare(struct ref_store *ref_store, packed_transaction, update->refname, REF_HAVE_NEW | REF_NO_DEREF, &update->new_oid, NULL, - NULL); + NULL, NULL, NULL); } } @@ -2848,6 +2924,18 @@ static int files_transaction_finish(struct ref_store *ref_store, if (update->flags & REF_NEEDS_COMMIT || update->flags & REF_LOG_ONLY) { + if (update->new_target) { + /* + * We want to get the resolved OID for the target, to ensure + * that the correct value is added to the reflog. + */ + if (!refs_resolve_ref_unsafe(&refs->base, update->new_target, + RESOLVE_REF_READING, &update->new_oid, NULL)) { + /* for dangling symrefs we gracefully set the oid to zero */ + update->new_oid = *null_oid(); + } + } + if (files_log_ref_write(refs, lock->ref_name, &lock->old_oid, @@ -2865,6 +2953,15 @@ static int files_transaction_finish(struct ref_store *ref_store, goto cleanup; } } + + /* + * We try creating a symlink, if that succeeds we continue to the + * next updated. If not, we try and create a regular symref. + */ + if (update->new_target && prefer_symlink_refs) + if (!create_ref_symlink(lock, update->new_target)) + continue; + if (update->flags & REF_NEEDS_COMMIT) { clear_loose_ref_cache(refs); if (commit_ref(lock)) { @@ -3048,7 +3145,7 @@ static int files_initial_transaction_commit(struct ref_store *ref_store, ref_transaction_add_update(packed_transaction, update->refname, update->flags & ~REF_HAVE_OLD, &update->new_oid, &update->old_oid, - NULL); + NULL, NULL, NULL); } if (packed_refs_lock(refs->packed_ref_store, 0, err)) { diff --git a/refs/refs-internal.h b/refs/refs-internal.h index 56641aa57a1..23e65f65e84 100644 --- a/refs/refs-internal.h +++ b/refs/refs-internal.h @@ -125,6 +125,18 @@ struct ref_update { struct object_id old_oid; /* + * If set, point the reference to this value. This can also be + * used to convert regular references to become symbolic refs. + */ + const char *new_target; + + /* + * If set and the reference is a symbolic ref, check that the + * reference previously pointed to this value. + */ + const char *old_target; + + /* * One or more of REF_NO_DEREF, REF_FORCE_CREATE_REFLOG, * REF_HAVE_NEW, REF_HAVE_OLD, or backend-specific flags. */ @@ -173,6 +185,7 @@ struct ref_update *ref_transaction_add_update( const char *refname, unsigned int flags, const struct object_id *new_oid, const struct object_id *old_oid, + const char *new_target, const char *old_target, const char *msg); /* @@ -735,4 +748,11 @@ void base_ref_store_init(struct ref_store *refs, struct repository *repo, */ struct ref_store *maybe_debug_wrap_ref_store(const char *gitdir, struct ref_store *store); +/* + * Helper function to check if the new value is null, this + * takes into consideration that the update could be a regular + * ref or a symbolic ref. + */ +int ref_update_is_null_new_value(struct ref_update *update); + #endif /* REFS_REFS_INTERNAL_H */ diff --git a/refs/reftable-backend.c b/refs/reftable-backend.c index 010ef811b64..580fc80eee6 100644 --- a/refs/reftable-backend.c +++ b/refs/reftable-backend.c @@ -827,7 +827,7 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store, new_update = ref_transaction_add_update( transaction, "HEAD", u->flags | REF_LOG_ONLY | REF_NO_DEREF, - &u->new_oid, &u->old_oid, u->msg); + &u->new_oid, &u->old_oid, NULL, NULL, u->msg); string_list_insert(&affected_refnames, new_update->refname); } @@ -854,7 +854,7 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store, * There is no need to write the reference deletion * when the reference in question doesn't exist. */ - if (u->flags & REF_HAVE_NEW && !is_null_oid(&u->new_oid)) { + if (u->flags & REF_HAVE_NEW && !ref_update_is_null_new_value(u)) { ret = queue_transaction_update(refs, tx_data, u, ¤t_oid, err); if (ret) @@ -906,7 +906,8 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store, */ new_update = ref_transaction_add_update( transaction, referent.buf, new_flags, - &u->new_oid, &u->old_oid, u->msg); + &u->new_oid, &u->old_oid, u->new_target, + u->old_target, u->msg); new_update->parent_update = u; /* @@ -936,7 +937,26 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store, * individual refs. But the error messages match what the files * backend returns, which keeps our tests happy. */ - if (u->flags & REF_HAVE_OLD && !oideq(¤t_oid, &u->old_oid)) { + if ((u->flags & REF_HAVE_OLD) && u->old_target) { + if (strcmp(referent.buf, u->old_target)) { + if (!strcmp(u->old_target, "")) + strbuf_addf(err, "verifying symref target: '%s': " + "provided target is empty", + original_update_refname(u)); + else if (!strcmp(referent.buf, "")) + strbuf_addf(err, "verifying symref target: '%s': " + "reference is missing but expected %s", + original_update_refname(u), + u->old_target); + else + strbuf_addf(err, "verifying symref target: '%s': " + "is at %s but expected %s", + original_update_refname(u), + referent.buf, u->old_target); + ret = -1; + goto done; + } + } else if (u->flags & REF_HAVE_OLD && !oideq(¤t_oid, &u->old_oid)) { if (is_null_oid(&u->old_oid)) strbuf_addf(err, _("cannot lock ref '%s': " "reference already exists"), @@ -1047,7 +1067,7 @@ static int write_transaction_table(struct reftable_writer *writer, void *cb_data * - `core.logAllRefUpdates` tells us to create the reflog for * the given ref. */ - if (u->flags & REF_HAVE_NEW && !(u->type & REF_ISSYMREF) && is_null_oid(&u->new_oid)) { + if (u->flags & REF_HAVE_NEW && !(u->type & REF_ISSYMREF) && ref_update_is_null_new_value(u)) { struct reftable_log_record log = {0}; struct reftable_iterator it = {0}; @@ -1089,6 +1109,12 @@ static int write_transaction_table(struct reftable_writer *writer, void *cb_data should_write_log(&arg->refs->base, u->refname))) { struct reftable_log_record *log; + if (u->new_target) + if (!refs_resolve_ref_unsafe(&arg->refs->base, u->new_target, + RESOLVE_REF_READING, &u->new_oid, NULL)) + /* for dangling symrefs we gracefully set the oid to zero */ + u->new_oid = *null_oid(); + ALLOC_GROW(logs, logs_nr + 1, logs_alloc); log = &logs[logs_nr++]; memset(log, 0, sizeof(*log)); @@ -1105,7 +1131,18 @@ static int write_transaction_table(struct reftable_writer *writer, void *cb_data if (u->flags & REF_LOG_ONLY) continue; - if (u->flags & REF_HAVE_NEW && is_null_oid(&u->new_oid)) { + if (u->flags & REF_HAVE_NEW && u->new_target) { + struct reftable_ref_record ref = { + .refname = (char *)u->refname, + .value_type = REFTABLE_REF_SYMREF, + .value.symref = (char *)u->new_target, + .update_index = ts, + }; + + ret = reftable_writer_add_ref(writer, &ref); + if (ret < 0) + goto done; + } else if (u->flags & REF_HAVE_NEW && ref_update_is_null_new_value(u)) { struct reftable_ref_record ref = { .refname = (char *)u->refname, .update_index = ts, diff --git a/sequencer.c b/sequencer.c index 612622eb67b..ab4cb5630d1 100644 --- a/sequencer.c +++ b/sequencer.c @@ -662,7 +662,7 @@ static int fast_forward_to(struct repository *r, if (!transaction || ref_transaction_update(transaction, "HEAD", to, unborn && !is_rebase_i(opts) ? - null_oid() : from, + null_oid() : from, NULL, NULL, 0, sb.buf, &err) || ref_transaction_commit(transaction, &err)) { ref_transaction_free(transaction); @@ -1295,7 +1295,7 @@ int update_head_with_reflog(const struct commit *old_head, if (!transaction || ref_transaction_update(transaction, "HEAD", new_head, old_head ? &old_head->object.oid : null_oid(), - 0, sb.buf, err) || + NULL, NULL, 0, sb.buf, err) || ref_transaction_commit(transaction, err)) { ret = -1; } @@ -3863,8 +3863,9 @@ static int do_label(struct repository *r, const char *name, int len) } else if (repo_get_oid(r, "HEAD", &head_oid)) { error(_("could not read HEAD")); ret = -1; - } else if (ref_transaction_update(transaction, ref_name.buf, &head_oid, - NULL, 0, msg.buf, &err) < 0 || + } else if (ref_transaction_update(transaction, ref_name.buf, + &head_oid, NULL, NULL, NULL, + 0, msg.buf, &err) < 0 || ref_transaction_commit(transaction, &err)) { error("%s", err.buf); ret = -1; diff --git a/t/t0600-reffiles-backend.sh b/t/t0600-reffiles-backend.sh index a390cffc800..13dc6901d5e 100755 --- a/t/t0600-reffiles-backend.sh +++ b/t/t0600-reffiles-backend.sh @@ -468,4 +468,36 @@ test_expect_success POSIXPERM 'git reflog expire honors core.sharedRepository' ' esac ' +test_expect_success SYMLINKS 'symref transaction supports symlinks' ' + test_when_finished "git symbolic-ref -d TESTSYMREFONE" && + git update-ref refs/heads/new @ && + test_config core.prefersymlinkrefs true && + cat >stdin <<-EOF && + start + create TESTSYMREFONE ref:refs/heads/new + prepare + commit + EOF + git update-ref --no-deref --stdin <stdin && + test_path_is_symlink .git/TESTSYMREFONE && + test "$(test_readlink .git/TESTSYMREFONE)" = refs/heads/new +' + +test_expect_success 'symref transaction supports false symlink config' ' + test_when_finished "git symbolic-ref -d TESTSYMREFONE" && + git update-ref refs/heads/new @ && + test_config core.prefersymlinkrefs false && + cat >stdin <<-EOF && + start + create TESTSYMREFONE ref:refs/heads/new + prepare + commit + EOF + git update-ref --no-deref --stdin <stdin && + test_path_is_file .git/TESTSYMREFONE && + git symbolic-ref TESTSYMREFONE >actual && + echo refs/heads/new >expect && + test_cmp expect actual +' + test_done diff --git a/t/t1400-update-ref.sh b/t/t1400-update-ref.sh index ec3443cc878..5b2d23da37e 100755 --- a/t/t1400-update-ref.sh +++ b/t/t1400-update-ref.sh @@ -890,17 +890,23 @@ test_expect_success 'stdin update/create/verify combination works' ' ' test_expect_success 'stdin verify succeeds for correct value' ' + test-tool ref-store main for-each-reflog-ent $m >before && git rev-parse $m >expect && echo "verify $m $m" >stdin && git update-ref --stdin <stdin && git rev-parse $m >actual && - test_cmp expect actual + test_cmp expect actual && + test-tool ref-store main for-each-reflog-ent $m >after && + test_cmp before after ' test_expect_success 'stdin verify succeeds for missing reference' ' + test-tool ref-store main for-each-reflog-ent $m >before && echo "verify refs/heads/missing $Z" >stdin && git update-ref --stdin <stdin && - test_must_fail git rev-parse --verify -q refs/heads/missing + test_must_fail git rev-parse --verify -q refs/heads/missing && + test-tool ref-store main for-each-reflog-ent $m >after && + test_cmp before after ' test_expect_success 'stdin verify treats no value as missing' ' @@ -1354,6 +1360,7 @@ test_expect_success 'fails with duplicate HEAD update' ' ' test_expect_success 'fails with duplicate ref update via symref' ' + test_when_finished "git symbolic-ref -d refs/heads/symref2" && git branch target2 $A && git symbolic-ref refs/heads/symref2 refs/heads/target2 && cat >stdin <<-EOF && @@ -1641,4 +1648,339 @@ test_expect_success PIPE 'transaction flushes status updates' ' test_cmp expected actual ' +create_stdin_buf () { + if test "$1" = "-z" + then + shift + printf "$F" "$@" >stdin + else + echo "$@" >stdin + fi +} + +for type in "" "-z" +do + + test_expect_success "stdin ${type} verify symref fails without --no-deref" ' + git symbolic-ref refs/heads/symref $a && + create_stdin_buf ${type} "verify refs/heads/symref" "ref:$a" && + test_must_fail git update-ref --stdin ${type} <stdin 2>err && + grep "fatal: verify refs/heads/symref: cannot operate on symrefs in deref mode" err + ' + + test_expect_success "stdin ${type} verify symref fails with too many arguments" ' + create_stdin_buf ${type} "verify refs/heads/symref" "ref:$a" "ref:$a" && + test_must_fail git update-ref --stdin ${type} --no-deref <stdin 2>err && + if test "$type" = "-z" + then + grep "fatal: unknown command: ref:$a" err + else + grep "fatal: verify refs/heads/symref: extra input: ref:$a" err + fi + ' + + test_expect_success "stdin ${type} verify symref succeeds for correct value" ' + git symbolic-ref refs/heads/symref >expect && + test-tool ref-store main for-each-reflog-ent refs/heads/symref >before && + create_stdin_buf ${type} "verify refs/heads/symref" "ref:$a" && + git update-ref --stdin ${type} --no-deref <stdin && + git symbolic-ref refs/heads/symref >actual && + test_cmp expect actual && + test-tool ref-store main for-each-reflog-ent refs/heads/symref >after && + test_cmp before after + ' + + test_expect_success "stdin ${type} verify symref succeeds for missing reference" ' + test-tool ref-store main for-each-reflog-ent refs/heads/symref >before && + create_stdin_buf ${type} "verify refs/heads/missing" "$Z" && + git update-ref --stdin ${type} --no-deref <stdin && + test_must_fail git rev-parse --verify -q refs/heads/missing && + test-tool ref-store main for-each-reflog-ent refs/heads/symref >after && + test_cmp before after + ' + + test_expect_success "stdin ${type} verify symref fails for wrong value" ' + git symbolic-ref refs/heads/symref >expect && + create_stdin_buf ${type} "verify refs/heads/symref" "$b" && + test_must_fail git update-ref --stdin ${type} --no-deref <stdin && + git symbolic-ref refs/heads/symref >actual && + test_cmp expect actual + ' + + test_expect_success "stdin ${type} verify symref fails for mistaken null value" ' + git symbolic-ref refs/heads/symref >expect && + create_stdin_buf ${type} "verify refs/heads/symref" "$Z" && + test_must_fail git update-ref --stdin ${type} --no-deref <stdin && + git symbolic-ref refs/heads/symref >actual && + test_cmp expect actual + ' + + test_expect_success "stdin ${type} delete symref fails without --no-deref" ' + git symbolic-ref refs/heads/symref $a && + create_stdin_buf ${type} "delete refs/heads/symref" "ref:$a" && + test_must_fail git update-ref --stdin ${type} <stdin 2>err && + grep "fatal: delete refs/heads/symref: cannot operate on symrefs in deref mode" err + ' + + test_expect_success "stdin ${type} delete symref fails with no ref" ' + create_stdin_buf ${type} "delete " && + test_must_fail git update-ref --stdin ${type} --no-deref <stdin 2>err && + grep "fatal: delete: missing <ref>" err + ' + + test_expect_success "stdin ${type} delete symref fails with too many arguments" ' + create_stdin_buf ${type} "delete refs/heads/symref" "ref:$a" "ref:$a" && + test_must_fail git update-ref --stdin ${type} --no-deref <stdin 2>err && + if test "$type" = "-z" + then + grep "fatal: unknown command: ref:$a" err + else + grep "fatal: delete refs/heads/symref: extra input: ref:$a" err + fi + ' + + test_expect_success "stdin ${type} delete symref fails with wrong old value" ' + create_stdin_buf ${type} "delete refs/heads/symref" "ref:$m" && + test_must_fail git update-ref --stdin ${type} --no-deref <stdin 2>err && + if test_have_prereq REFTABLE + then + grep "fatal: verifying symref target: ${SQ}refs/heads/symref${SQ}: is at $a but expected refs/heads/main" err + else + grep "fatal: cannot lock ref ${SQ}refs/heads/symref${SQ}" err + fi && + git symbolic-ref refs/heads/symref >expect && + echo $a >actual && + test_cmp expect actual + ' + + test_expect_success "stdin ${type} delete symref works with right old value" ' + create_stdin_buf ${type} "delete refs/heads/symref" "ref:$a" && + git update-ref --stdin ${type} --no-deref <stdin && + test_must_fail git rev-parse --verify -q $b + ' + + test_expect_success "stdin ${type} create symref fails without --no-deref" ' + create_stdin_buf ${type} "create refs/heads/symref" "ref:$a" && + test_must_fail git update-ref --stdin ${type} <stdin 2>err && + grep "fatal: create refs/heads/symref: cannot create symrefs in deref mode" err + ' + + test_expect_success "stdin ${type} create symref fails with too many arguments" ' + create_stdin_buf ${type} "create refs/heads/symref" "ref:$a" "ref:$a" >stdin && + test_must_fail git update-ref --stdin ${type} --no-deref <stdin 2>err && + if test "$type" = "-z" + then + grep "fatal: unknown command: ref:$a" err + else + grep "fatal: create refs/heads/symref: extra input: ref:$a" err + fi + ' + + test_expect_success "stdin ${type} create symref ref works" ' + test_when_finished "git symbolic-ref -d refs/heads/symref" && + create_stdin_buf ${type} "create refs/heads/symref" "ref:$a" >stdin && + git update-ref --stdin ${type} --no-deref <stdin && + git symbolic-ref refs/heads/symref >expect && + echo $a >actual && + test_cmp expect actual + ' + + test_expect_success "stdin ${type} create dangling symref ref works" ' + test_when_finished "git symbolic-ref -d refs/heads/symref" && + create_stdin_buf ${type} "create refs/heads/symref" "ref:refs/heads/unkown" >stdin && + git update-ref --stdin ${type} --no-deref <stdin && + git symbolic-ref refs/heads/symref >expect && + echo refs/heads/unkown >actual && + test_cmp expect actual + ' + + test_expect_success "stdin ${type} create symref does not create reflogs by default" ' + test_when_finished "git symbolic-ref -d refs/symref" && + create_stdin_buf ${type} "create refs/symref" "ref:$a" >stdin && + git update-ref --stdin ${type} --no-deref <stdin && + git symbolic-ref refs/symref >expect && + echo $a >actual && + test_cmp expect actual && + test_must_fail git reflog exists refs/symref + ' + + test_expect_success "stdin ${type} create symref reflogs with --create-reflog" ' + test_when_finished "git symbolic-ref -d refs/heads/symref" && + create_stdin_buf ${type} "create refs/heads/symref" "ref:$a" >stdin && + git update-ref --create-reflog --stdin ${type} --no-deref <stdin && + git symbolic-ref refs/heads/symref >expect && + echo $a >actual && + test_cmp expect actual && + git reflog exists refs/heads/symref + ' + + test_expect_success "stdin ${type} update symref fails with too many arguments" ' + create_stdin_buf ${type} "update refs/heads/symref" "ref:$a" "ref:$a" "ref:$a" >stdin && + test_must_fail git update-ref --stdin ${type} --no-deref <stdin 2>err && + if test "$type" = "-z" + then + grep "fatal: unknown command: ref:$a" err + else + grep "fatal: update refs/heads/symref: extra input: ref:$a" err + fi + ' + + test_expect_success "stdin ${type} update creates symref with zero old value" ' + test_when_finished "git symbolic-ref -d refs/heads/symref" && + create_stdin_buf ${type} "update refs/heads/symref" "ref:$a" "$Z" >stdin && + git update-ref --stdin ${type} --no-deref <stdin && + echo $a >expect && + git symbolic-ref refs/heads/symref >actual && + test_cmp expect actual + ' + + test_expect_success "stdin ${type} update creates symref with empty old value" ' + test_when_finished "git symbolic-ref -d refs/heads/symref" && + create_stdin_buf ${type} "update refs/heads/symref" "ref:$a" "" >stdin && + git update-ref --stdin ${type} --no-deref <stdin && + echo $a >expect && + git symbolic-ref refs/heads/symref >actual && + test_cmp expect actual + ' + + test_expect_success "stdin ${type} update symref fails with wrong old value" ' + test_when_finished "git symbolic-ref -d refs/heads/symref" && + git symbolic-ref refs/heads/symref $a && + create_stdin_buf ${type} "update refs/heads/symref" "ref:$m" "ref:$b" >stdin && + test_must_fail git update-ref --stdin ${type} --no-deref <stdin 2>err && + if test_have_prereq REFTABLE + then + grep "fatal: verifying symref target: ${SQ}refs/heads/symref${SQ}: is at $a but expected $b" err + else + grep "fatal: cannot lock ref ${SQ}refs/heads/symref${SQ}: is at $a but expected $b" err + fi && + test_must_fail git rev-parse --verify -q $c + ' + + test_expect_success "stdin ${type} update symref works with right old value" ' + test_when_finished "git symbolic-ref -d refs/heads/symref" && + git symbolic-ref refs/heads/symref $a && + create_stdin_buf ${type} "update refs/heads/symref" "ref:$m" "ref:$a" >stdin && + git update-ref --stdin ${type} --no-deref <stdin && + echo $m >expect && + git symbolic-ref refs/heads/symref >actual && + test_cmp expect actual + ' + + test_expect_success "stdin ${type} update creates symref (with deref)" ' + test_when_finished "git symbolic-ref -d refs/heads/symref" && + create_stdin_buf ${type} "update refs/heads/symref" "ref:$a" "" >stdin && + git update-ref --stdin ${type} <stdin && + echo $a >expect && + git symbolic-ref --no-recurse refs/heads/symref >actual && + test_cmp expect actual && + test-tool ref-store main for-each-reflog-ent refs/heads/symref >actual && + grep "$Z $(git rev-parse $a)" actual + ' + + test_expect_success "stdin ${type} update regular ref to symref with correct old-oid" ' + test_when_finished "git symbolic-ref -d --no-recurse refs/heads/regularref" && + git update-ref --no-deref refs/heads/regularref $a && + create_stdin_buf ${type} "update refs/heads/regularref" "ref:$a" "$(git rev-parse $a)" >stdin && + git update-ref --stdin ${type} <stdin && + echo $a >expect && + git symbolic-ref --no-recurse refs/heads/regularref >actual && + test_cmp expect actual && + test-tool ref-store main for-each-reflog-ent refs/heads/regularref >actual && + grep "$(git rev-parse $a) $(git rev-parse $a)" actual + ' + + test_expect_success "stdin ${type} update regular ref to symref fails with wrong old-oid" ' + test_when_finished "git update-ref -d refs/heads/regularref" && + git update-ref --no-deref refs/heads/regularref $a && + create_stdin_buf ${type} "update refs/heads/regularref" "ref:$a" "$(git rev-parse refs/heads/target2)" >stdin && + test_must_fail git update-ref --stdin ${type} <stdin 2>err && + echo $(git rev-parse $a) >expect && + git rev-parse refs/heads/regularref >actual && + test_cmp expect actual + ' + + test_expect_success "stdin ${type} update existing symref with zero old-oid" ' + test_when_finished "git symbolic-ref -d --no-recurse refs/heads/symref" && + git symbolic-ref refs/heads/symref refs/heads/target2 && + create_stdin_buf ${type} "update refs/heads/symref" "ref:$a" "$Z" >stdin && + test_must_fail git update-ref --stdin ${type} <stdin 2>err && + grep "fatal: cannot lock ref ${SQ}refs/heads/symref${SQ}: reference already exists" err && + echo refs/heads/target2 >expect && + git symbolic-ref refs/heads/symref >actual && + test_cmp expect actual + ' + + test_expect_success "stdin ${type} update existing symref to regular ref" ' + test_when_finished "git update-ref -d refs/heads/symref" && + git symbolic-ref refs/heads/symref refs/heads/target2 && + create_stdin_buf ${type} "update refs/heads/symref" "$(git rev-parse $a)" "ref:refs/heads/target2" >stdin && + git update-ref --stdin ${type} --no-deref <stdin && + echo $(git rev-parse $a) >expect && + git rev-parse refs/heads/symref >actual && + test_cmp expect actual && + test-tool ref-store main for-each-reflog-ent refs/heads/symref >actual && + grep "$(git rev-parse refs/heads/target2) $(git rev-parse $a)" actual + ' + +done + +test_expect_success "stdin update symref (with deref)" ' + test_when_finished "git symbolic-ref -d refs/heads/symref" && + test_when_finished "git update-ref -d --no-deref refs/heads/symref2" && + git update-ref refs/heads/symref2 $a && + git symbolic-ref --no-recurse refs/heads/symref refs/heads/symref2 && + echo "update refs/heads/symref" "ref:$a" >stdin && + git update-ref --stdin <stdin && + echo $a >expect && + git symbolic-ref --no-recurse refs/heads/symref2 >actual && + test_cmp expect actual && + echo refs/heads/symref2 >expect && + git symbolic-ref --no-recurse refs/heads/symref >actual && + test_cmp expect actual && + test-tool ref-store main for-each-reflog-ent refs/heads/symref >actual && + grep "$(git rev-parse $a) $(git rev-parse $a)" actual +' + +test_expect_success "stdin -z update symref (with deref)" ' + test_when_finished "git symbolic-ref -d refs/heads/symref" && + test_when_finished "git update-ref -d --no-deref refs/heads/symref2" && + git update-ref refs/heads/symref2 $a && + git symbolic-ref --no-recurse refs/heads/symref refs/heads/symref2 && + printf "$F" "update refs/heads/symref" "ref:$a" "" >stdin && + git update-ref --stdin -z <stdin && + echo $a >expect && + git symbolic-ref --no-recurse refs/heads/symref2 >actual && + test_cmp expect actual && + echo refs/heads/symref2 >expect && + git symbolic-ref --no-recurse refs/heads/symref >actual && + test_cmp expect actual && + test-tool ref-store main for-each-reflog-ent refs/heads/symref >actual && + grep "$(git rev-parse $a) $(git rev-parse $a)" actual +' + +test_expect_success "stdin update regular ref to symref" ' + test_when_finished "git symbolic-ref -d --no-recurse refs/heads/regularref" && + git update-ref --no-deref refs/heads/regularref $a && + echo "update refs/heads/regularref" "ref:$a" >stdin && + git update-ref --stdin <stdin && + echo $a >expect && + git symbolic-ref --no-recurse refs/heads/regularref >actual && + test_cmp expect actual && + test-tool ref-store main for-each-reflog-ent refs/heads/regularref >actual && + grep "$(git rev-parse $a) $(git rev-parse $a)" actual +' + +test_expect_success "stdin -z update regular ref to symref" ' + test_when_finished "git symbolic-ref -d --no-recurse refs/heads/regularref" && + git update-ref --no-deref refs/heads/regularref $a && + printf "$F" "update refs/heads/regularref" "ref:$a" "" >stdin && + git update-ref --stdin -z <stdin && + echo $a >expect && + git symbolic-ref --no-recurse refs/heads/regularref >actual && + test_cmp expect actual && + test-tool ref-store main for-each-reflog-ent refs/heads/regularref >actual && + grep "$(git rev-parse $a) $(git rev-parse $a)" actual +' + test_done diff --git a/t/t1416-ref-transaction-hooks.sh b/t/t1416-ref-transaction-hooks.sh index 2092488090d..abd47778192 100755 --- a/t/t1416-ref-transaction-hooks.sh +++ b/t/t1416-ref-transaction-hooks.sh @@ -134,4 +134,45 @@ test_expect_success 'interleaving hook calls succeed' ' test_cmp expect target-repo.git/actual ' +# This test doesn't add a check for symref 'delete' since there is a +# variation between the ref backends WRT 'delete'. In the files backend, +# 'delete' also triggers an additional transaction update on the +# packed-refs backend, which constitutes additional reflog entries. +test_expect_success 'hook gets all queued symref updates' ' + test_when_finished "rm actual" && + + git update-ref refs/heads/branch $POST_OID && + git symbolic-ref refs/heads/symref refs/heads/main && + git symbolic-ref refs/heads/symrefu refs/heads/main && + + test_hook reference-transaction <<-\EOF && + echo "$*" >>actual + while read -r line + do + printf "%s\n" "$line" + done >>actual + EOF + + cat >expect <<-EOF && + prepared + ref:refs/heads/main $ZERO_OID refs/heads/symref + $ZERO_OID ref:refs/heads/main refs/heads/symrefc + ref:refs/heads/main ref:refs/heads/branch refs/heads/symrefu + committed + ref:refs/heads/main $ZERO_OID refs/heads/symref + $ZERO_OID ref:refs/heads/main refs/heads/symrefc + ref:refs/heads/main ref:refs/heads/branch refs/heads/symrefu + EOF + + git update-ref --no-deref --stdin <<-EOF && + start + verify refs/heads/symref ref:refs/heads/main + create refs/heads/symrefc ref:refs/heads/main + update refs/heads/symrefu ref:refs/heads/branch ref:refs/heads/main + prepare + commit + EOF + test_cmp expect actual +' + test_done @@ -324,7 +324,7 @@ int walker_fetch(struct walker *walker, int targets, char **target, strbuf_reset(&refname); strbuf_addf(&refname, "refs/%s", write_ref[i]); if (ref_transaction_update(transaction, refname.buf, - oids + i, NULL, 0, + oids + i, NULL, NULL, NULL, 0, msg ? msg : "fetch (unknown)", &err)) { error("%s", err.buf); |