aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJunio C Hamano <gitster@pobox.com>2024-04-26 09:28:43 -0700
committerJunio C Hamano <gitster@pobox.com>2024-04-26 09:28:43 -0700
commita9f388adb7d0e35de975a1ff643d0678ad8920d2 (patch)
tree55fb7e212261838570bdbb2560c1664cdc26d73a
parentMerge branch 'rh/complete-symbolic-ref' into seen (diff)
parentSQUASH??? (sparse fix) (diff)
downloadgit-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.txt41
-rw-r--r--Documentation/githooks.txt14
-rw-r--r--branch.c2
-rw-r--r--builtin/clone.c2
-rw-r--r--builtin/fast-import.c5
-rw-r--r--builtin/fetch.c4
-rw-r--r--builtin/receive-pack.c4
-rw-r--r--builtin/replace.c2
-rw-r--r--builtin/tag.c1
-rw-r--r--builtin/update-ref.c106
-rw-r--r--refs.c78
-rw-r--r--refs.h23
-rw-r--r--refs/files-backend.c141
-rw-r--r--refs/refs-internal.h20
-rw-r--r--refs/reftable-backend.c49
-rw-r--r--sequencer.c9
-rwxr-xr-xt/t0600-reffiles-backend.sh32
-rwxr-xr-xt/t1400-update-ref.sh346
-rwxr-xr-xt/t1416-ref-transaction-hooks.sh41
-rw-r--r--walker.c2
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
diff --git a/branch.c b/branch.c
index e4a738fc7be..48af4c3ceba 100644
--- a/branch.c
+++ b/branch.c
@@ -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)
diff --git a/refs.c b/refs.c
index 55d2e0b2cb9..9a510744a73 100644
--- a/refs.c
+++ b/refs.c
@@ -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);
+}
diff --git a/refs.h b/refs.h
index d278775e086..bde86062134 100644
--- a/refs.h
+++ b/refs.h
@@ -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,
&current_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(&current_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(&current_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
diff --git a/walker.c b/walker.c
index c0fd632d921..1b3df439061 100644
--- a/walker.c
+++ b/walker.c
@@ -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);