diff mbox series

[7/8] refs: add 'update-symref' command to 'update-ref'

Message ID 20240330224623.579457-8-knayak@gitlab.com (mailing list archive)
State New
Headers show
Series update-ref: add support for update-symref option | expand

Commit Message

Karthik Nayak March 30, 2024, 10:46 p.m. UTC
From: Karthik Nayak <karthik.188@gmail.com>

The 'git-update-ref(1)' command allows transactional reference updates.
But currently only supports regular reference updates. Meaning, if one
wants to update HEAD (symbolic ref) in a transaction, there is no tool
to do so.

One option to obtain transactional updates for the HEAD ref is to
manually create the HEAD.lock file and commit. This is intrusive, where
the user needs to mimic internal git behavior. Also, this only works
when using the files backend.

To allow users to update the HEAD ref in a transaction, we introduce
'update-symref' command for 'git-update-ref(1)'. This command allows the
user to create symref in a transaction similar to the 'update' command
of 'git-update-ref(1)'. This command also works well with the existing
'no-deref' option.

The option can also be used to create new symrefs too. This means we
don't need a dedicated `create-symref` option. This is also because we
don't verify the old symref value when updating a symref. So in this
case update and create hold the same meaning.

The regular `delete` option can also be used to delete symrefs. So we
don't add a dedicated `delete-symref` option.

Signed-off-by: Karthik Nayak <karthik.188@gmail.com>
---
 Documentation/git-update-ref.txt |  11 ++-
 builtin/update-ref.c             |  61 ++++++++++++---
 refs.c                           |  10 +++
 t/t0600-reffiles-backend.sh      |  30 ++++++++
 t/t1400-update-ref.sh            | 127 +++++++++++++++++++++++++++++++
 5 files changed, 229 insertions(+), 10 deletions(-)

Comments

Junio C Hamano March 31, 2024, 10:08 p.m. UTC | #1
Karthik Nayak <karthik.188@gmail.com> writes:

> diff --git a/Documentation/git-update-ref.txt b/Documentation/git-update-ref.txt
> index 0561808cca..2ea8bc8167 100644
> --- a/Documentation/git-update-ref.txt
> +++ b/Documentation/git-update-ref.txt
> @@ -65,6 +65,7 @@ performs all modifications together.  Specify commands of the form:

In pre-context, there is this entry.

	update SP <ref> SP <newvalue> [SP <oldvalue>] LF

Unrelated to this patch, we probably should say <new-value> and
<old-value> to follow the (recent) documentation guideline to write
multiple words concatenated with hyphen.

If we are updating these newvalue and oldvalue anyway, we may want
to update them to <new-oid> and <old-oid>, as the existing commands
on refs are about object names, and what we are adding here are not.

I would prefer to see such an update of existing documentation as a
separate preliminary clean-up patch, so that we can cleanly add the
"update-symref" that takes <new-symref-target> on top of a
cleaned-up base.

The <newvalue> ...

>  	create SP <ref> SP <newvalue> LF
>  	delete SP <ref> [SP <oldvalue>] LF
>  	verify SP <ref> [SP <oldvalue>] LF
> +	update-symref SP <ref> SP <newvalue> LF

... we are giving to "update-symref" is not an object name, but a
refname (i.e. the target of the symref at <ref> points at), so
"newvalue" -> "new-ref" or something is needed.

The semantics of this new command is not quite in line with the
existing commands.  Existing "update" and "delete" allow you to
optionally specify <old-oid> to make sure you do not overwrite
somebody else's update in the meantime.  Your "update-symref" lacks
such safety completely, which I doubt is a good idea in the context
of adding it to an existing set that has such safety features.  We
should at least offer the same "optional" safety.  That makes the
syntax more like

	update-symref SP <ref> SP <new-ref> [SP <old-ref>] LF

probably.

Existing "update" command can be used to create (by having "zero"
<old-oid>) and to delete (by having "zero" <new-oid>), but we still
have "create" and "delete" separately.  Given that we are teaching
the command to also work with symrefs by adding command(s) for
symrefs to an existing vocabulary, we should offer "create-symref"
and "delete-symref" for consistency between refs and symrefs.

It probably is a good idea to allow "update-symref" to also create
and delete a symref in similar ways with the existing "update"
allows creation and deletion of a ref.  An "zero" <old-ref> may be
an update that can happen only when the <ref> does not exist, i.e.,
creation, and an "zero" <new-ref> may be an update that makes <ref>
disappear.

"zero" value in the context of refs is a 0{40} object name (side
note: we have an explicit mention of 40 in the doc, which needs to
be updated eventually, probably outside this series).  But in the
new context of symrefs, a safe "zero" value needs to be picked
carefully.  An empty string may not work well syntactically
especially in the `-z` format, because you cannot tell if

	update-symref NUL HEAD NUL refs/heads/main NUL NUL

wants to say that <old-ref> is an empty string, or if it is missing.
As a refname cannot have a path component that begins with a dot,
a usable "zero" value for <new-ref> and <old-ref> may be a string
like ".missing", ".detached", and perhaps ".detached-f78d7...f2d4".

To summarize:

	update-symref SP HEAD SP refs/heads/main LF

forcibly sets HEAD to point at refs/heads/main without regard to the
current state of HEAD.

	update-symref SP HEAD SP .missing LF

forcibly removes HEAD symref without regard to the current state of
HEAD.

	update-symref SP HEAD SP refs/heads/main SP .missing LF

creates HEAD symref to point at the 'main' branch but fails if HEAD
already exists.

	update-symref SP HEAD SP refs/heads/main SP .detached LF
	update-symref SP HEAD SP refs/heads/main SP .detached-f78d7...f2d4 LF

creates HEAD symref to point at the 'main' branch but fails if HEAD
is not detached (or detached at the specified commit).

	update-symref SP HEAD SP refs/heads/main SP refs/heads/next LF

creates HEAD symref to point at the 'main' branch but fails if HEAD
is not pointing at the 'next' branch.

Hmm?
Chris Torek March 31, 2024, 10:27 p.m. UTC | #2
On Sun, Mar 31, 2024 at 3:09 PM Junio C Hamano <gitster@pobox.com> wrote:
> ... we are giving to "update-symref" is not an object name, but a
> refname (i.e. the target of the symref at <ref> points at), so
> "newvalue" -> "new-ref" or something is needed.

I kept this line just to say, I agree on all of the above ...

> Existing "update" command can be used to create (by having "zero"
> <old-oid>) and to delete (by having "zero" <new-oid>), but we still
> have "create" and "delete" separately.  Given that we are teaching
> the command to also work with symrefs by adding command(s) for
> symrefs to an existing vocabulary, we should offer "create-symref"
> and "delete-symref" for consistency between refs and symrefs.

.. and on this, but:

> It probably is a good idea to allow "update-symref" to also create
> and delete a symref in similar ways with the existing "update"
> allows creation and deletion of a ref.  An "zero" <old-ref> may be
> an update that can happen only when the <ref> does not exist, i.e.,
> creation, and an "zero" <new-ref> may be an update that makes <ref>
> disappear.
>
> "zero" value in the context of refs is a 0{40} object name (side
> note: we have an explicit mention of 40 in the doc, which needs to
> be updated eventually, probably outside this series).  But in the
> new context of symrefs, a safe "zero" value needs to be picked
> carefully.  An empty string may not work well syntactically
> especially in the `-z` format, because you cannot tell if
>
>         update-symref NUL HEAD NUL refs/heads/main NUL NUL
>
> wants to say that <old-ref> is an empty string, or if it is missing.

For these reasons, I'd suggest that the all-zero hash be officially
deprecated in favor of create/delete and of course create-symref
and delete-symref. Of course, compatibility requires some sort
of support for the old system for some time.  As to whether that
means something like the suggestion of ".missing" etc, I have no
particular opinion -- but since the symref options are new, they
would not *need* symmetric options, if we just say that "update-symref"
cannot create or delete a symref.

Meanwhile, for testing purposes I was curious as to what happens
if you ask `git update-ref` to delete an existing symref, so after
creating a test repository:

$ git branch
* main
  symref -> main
$ git update-ref --stdin
delete refs/heads/symref
$ git branch

Whoops, this doesn't look good...

Restoring the branch name (I had saved the hash ID Just In Case):

$ echo d88ee82e6a5c29c95f712030f5efc9d43116ae79 > .git/refs/heads/main

brings things back, after which this works properly:

$ git branch -d symref
Deleted branch symref (was refs/heads/main).
$ git branch
* main

If this (deleting the target of the symref when using "delete")
is a bug, and I think it is, that's a separate topic of course...

Chris
Junio C Hamano March 31, 2024, 11:14 p.m. UTC | #3
Chris Torek <chris.torek@gmail.com> writes:

> For these reasons, I'd suggest that the all-zero hash be officially
> deprecated in favor of create/delete and of course create-symref
> and delete-symref. Of course, compatibility requires some sort
> of support for the old system for some time.  As to whether that
> means something like the suggestion of ".missing" etc, I have no
> particular opinion -- but since the symref options are new, they
> would not *need* symmetric options, if we just say that "update-symref"
> cannot create or delete a symref.

I love that simplicity.

> If this (deleting the target of the symref when using "delete")
> is a bug, and I think it is, that's a separate topic of course...

"git blame" may find those who can give us answers, but my gut
feeling is that they weren't thinking too deeply about what should
happen, and the code does what it happens to do.

When you update, you'd want the update go through a symref (i.e.,
update-ref HEAD would update your current branch unless you are
detached), so they must have been aware of the fact that they need
to deal with symrefs.

But when you delete a symref, what do you want that deletion affect?
I do not have a good answer offhand, and they probably didn't even
think of the need to deal with symrefs, as the most common symref
you see, HEAD, is not something you'd ever want to delete anyway.
Junio C Hamano April 1, 2024, 1:31 a.m. UTC | #4
Junio C Hamano <gitster@pobox.com> writes:

> Chris Torek <chris.torek@gmail.com> writes:
>
>> For these reasons, I'd suggest that the all-zero hash be officially
>> deprecated in favor of create/delete and of course create-symref
>> and delete-symref. Of course, compatibility requires some sort
>> of support for the old system for some time.  As to whether that
>> means something like the suggestion of ".missing" etc, I have no
>> particular opinion -- but since the symref options are new, they
>> would not *need* symmetric options, if we just say that "update-symref"
>> cannot create or delete a symref.
>
> I love that simplicity.

Having said that, the loose "update that can create or delete" may
actually be used by applications that do not care about overwriting
competing operation, so I am not sure if we can easily deprecate
that mode of operation.  Saying "update refs/heads/next to point at
this object" and have it created if it does not exist may be handy
for some loosely written applications.

So perhaps we can say "update with a concrete <old-oid> will ensure
that the <ref> poitns at <old-oid> before proceeding, but update
with 0{40} as <old-oid> to ensure creation is deprecated.  update
with 0{40} as <new-oid> as deletion is also deprecated.  Use create
and delete for these two deprecated operation modes".

This assumes that create and delete currently ensures that what is
asked to be created does not exist, and what is asked to be deleted
does exist, before the operation.  If we are loosely doing these two
operations, then we cannot easily deprecate the checking-update,
without breaking existing users.

As {create,update,delete,verify}-symref do not exist yet, we can
start with the right semantics from day one.  "update-symref" will
accept a <old-ref> only to ensure that the symref is pointing to
that ref and there is no "zero" value based creation/deletion
validation offered via "update-symref".  "create-symref" will error
out if the ref asked to be created already exists, "delete-symref"
will error out if the ref asked to be deleted does not exist.
Karthik Nayak April 1, 2024, 10:38 a.m. UTC | #5
Chris Torek <chris.torek@gmail.com> writes:

> Meanwhile, for testing purposes I was curious as to what happens
> if you ask `git update-ref` to delete an existing symref, so after
> creating a test repository:
>
> $ git branch
> * main
>   symref -> main
> $ git update-ref --stdin
> delete refs/heads/symref
> $ git branch
>
> Whoops, this doesn't look good...
>

This is expected though. Remember that `git-update-ref(1)` by default
does dereferencing of symlinks. So in your case, 'refs/heads/symref' is
dereferenced to 'refs/heads/main' and when a delete command is issued,
'main' is deleted.

Try the same with the `git update-ref --no-deref --stdin` instead.

> Restoring the branch name (I had saved the hash ID Just In Case):
>
> $ echo d88ee82e6a5c29c95f712030f5efc9d43116ae79 > .git/refs/heads/main
>
> brings things back, after which this works properly:
>
> $ git branch -d symref
> Deleted branch symref (was refs/heads/main).
> $ git branch
> * main
>

Here you switch to using 'git-branch(1)' and that doesn't dereference by
default. So there is a difference in the two attempts.
Karthik Nayak April 1, 2024, 11:48 a.m. UTC | #6
Junio C Hamano <gitster@pobox.com> writes:

> Karthik Nayak <karthik.188@gmail.com> writes:
>
>> diff --git a/Documentation/git-update-ref.txt b/Documentation/git-update-ref.txt
>> index 0561808cca..2ea8bc8167 100644
>> --- a/Documentation/git-update-ref.txt
>> +++ b/Documentation/git-update-ref.txt
>> @@ -65,6 +65,7 @@ performs all modifications together.  Specify commands of the form:
>
> In pre-context, there is this entry.
>
> 	update SP <ref> SP <newvalue> [SP <oldvalue>] LF
>
> Unrelated to this patch, we probably should say <new-value> and
> <old-value> to follow the (recent) documentation guideline to write
> multiple words concatenated with hyphen.
>
> If we are updating these newvalue and oldvalue anyway, we may want
> to update them to <new-oid> and <old-oid>, as the existing commands
> on refs are about object names, and what we are adding here are not.
>
> I would prefer to see such an update of existing documentation as a
> separate preliminary clean-up patch, so that we can cleanly add the
> "update-symref" that takes <new-symref-target> on top of a
> cleaned-up base.
>
> The <newvalue> ...
>

I agree with your synopsis here, I'll send in a patch for this,
independent of these changes as a precursor, while we discuss the final
design of this series.

>> Chris Torek <chris.torek@gmail.com> writes:
>>
>>> For these reasons, I'd suggest that the all-zero hash be officially
>>> deprecated in favor of create/delete and of course create-symref
>>> and delete-symref. Of course, compatibility requires some sort
>>> of support for the old system for some time.  As to whether that
>>> means something like the suggestion of ".missing" etc, I have no
>>> particular opinion -- but since the symref options are new, they
>>> would not *need* symmetric options, if we just say that "update-symref"
>>> cannot create or delete a symref.
>>
>> I love that simplicity.
>
> Having said that, the loose "update that can create or delete" may
> actually be used by applications that do not care about overwriting
> competing operation, so I am not sure if we can easily deprecate
> that mode of operation.  Saying "update refs/heads/next to point at
> this object" and have it created if it does not exist may be handy
> for some loosely written applications.
>
> So perhaps we can say "update with a concrete <old-oid> will ensure
> that the <ref> poitns at <old-oid> before proceeding, but update
> with 0{40} as <old-oid> to ensure creation is deprecated.  update
> with 0{40} as <new-oid> as deletion is also deprecated.  Use create
> and delete for these two deprecated operation modes".
>
> This assumes that create and delete currently ensures that what is
> asked to be created does not exist, and what is asked to be deleted
> does exist, before the operation.  If we are loosely doing these two
> operations, then we cannot easily deprecate the checking-update,
> without breaking existing users.
>
> As {create,update,delete,verify}-symref do not exist yet, we can
> start with the right semantics from day one.  "update-symref" will
> accept a <old-ref> only to ensure that the symref is pointing to
> that ref and there is no "zero" value based creation/deletion
> validation offered via "update-symref".  "create-symref" will error
> out if the ref asked to be created already exists, "delete-symref"
> will error out if the ref asked to be deleted does not exist.

This seems very reasonable to me. This ensures that each of the commands
do not spill over to the other and most importantly we don't have to
deal with "zero" values.

But this still means we need to think of the best output for the
reference transaction hook (following commit).

My current implementation of:
   <symref-target> SP <ref-name> LF
Should be changed to:
   <old-ref> SP <new-ref> LF

But this means, for creation of symrefs <old-ref> needs to be "zero"
value. Also there is no way for clients to differentiate between regular
refs and symrefs here. I wonder if it makes sense to do something like:

   symref SP <old-ref> SP <new-ref> LF

Where symref is a fixed string at the start, used as a differentiator.
This however still would leave us to deal with "zero" values for
creation and deletion.

Perhaps the best way here to actually be a lot more verbose and have the
hook output the following:

   symref-create SP <new-ref> LF
   symref-delete SP <old-ref> LF
   symref-update SP <old-ref> SP <new-ref> LF
   symref-update-forced <new-ref> LF

I lean towards the latter, because its also much easier to extend in the
future and we don't have to deal with the "zero" values.

I'm against the "zero" values mostly cause there is no nice way to
describe a zero value for a ref unlike OIDs, which is inherently baked
into the type of data.
Junio C Hamano April 1, 2024, 4:17 p.m. UTC | #7
Karthik Nayak <karthik.188@gmail.com> writes:

>> So perhaps we can say "update with a concrete <old-oid> will ensure
>> that the <ref> poitns at <old-oid> before proceeding, but update
>> with 0{40} as <old-oid> to ensure creation is deprecated.  update
>> with 0{40} as <new-oid> as deletion is also deprecated.  Use create
>> and delete for these two deprecated operation modes".
>>
>> This assumes that create and delete currently ensures that what is
>> asked to be created does not exist, and what is asked to be deleted
>> does exist, before the operation.  If we are loosely doing these two
>> operations, then we cannot easily deprecate the checking-update,
>> without breaking existing users.

Note that I did not (and do not) know if "create" and "delete" have
such checks; I expected somebody (other than me) to check before
going forward.

> But this still means we need to think of the best output for the
> reference transaction hook (following commit).
>
> My current implementation of:
>    <symref-target> SP <ref-name> LF
> Should be changed to:
>    <old-ref> SP <new-ref> LF
>
> But this means, for creation of symrefs <old-ref> needs to be "zero"
> value. Also there is no way for clients to differentiate between regular
> refs and symrefs here. I wonder if it makes sense to do something like:
>
>    symref SP <old-ref> SP <new-ref> LF

What do clients see for a regular ref?  "<old-oid> SP <new-oid> LF"?
That would be OK, as "symref" cannot be an object name, I guess?

> Where symref is a fixed string at the start, used as a differentiator.
> This however still would leave us to deal with "zero" values for
> creation and deletion.

Are these two <old-ref> and <new-ref> values optional in the context
of your discussion?  The review comment was on input from the end-user
that made it optional to validate the precondition, but this is what
you produce as a result---if these are not optional, then an empty
string can be a reasonable "zero" value.  I am confused...

> Perhaps the best way here to actually be a lot more verbose and have the
> hook output the following:
>
>    symref-create SP <new-ref> LF
>    symref-delete SP <old-ref> LF
>    symref-update SP <old-ref> SP <new-ref> LF
>    symref-update-forced <new-ref> LF

It is unfortunate that the input to the hook for a normal reference
update uses syntax different from the "--stdin" input format, i.e.,

    <old-oid> SP <new-oid> SP <ref> LF

but it is way too late to change it now.  So to be consistent,

    symref-create SP <new-ref> SP <ref> LF
    symref-delete SP <old-ref> SP <ref> LF
    symref-update SP <old-ref> SP <new-ref> SP <ref> LF

would be the three operations.

But this is not an end-user input that tells Git "I do not care
about precondition, I did not even bother to learn the current state
to give you as <old-something>, just force it".  The input to hook
is what we tell the hook what we are planning to do (so that it can
decline), and we do not need the ability to say "I do not know what
the current state is".  So I do not think you need any "zero" value
in the input to the reference-transaction hook.  And I do not see a
need for the "symref-update-forced" variant, either.

By the way, if we were to use symref-{create,delete,update} here,
wouldn't it make more sense to name the command on the "--stdin"
side the same, i.e., not "update-symref" but "symref-update"?

What I suspect that needs more thought is what should happen when
you request via "--stdin" to create, update, or delete a symref,
but <ref> is a regular ref, e.g., "symref-delete <ref>".  For
"symref-create <ref> <new-ref>", we would fail if <ref> exists,
whether it is a symref or a normal ref, so that is OK.  For
"symref-delete <ref> <old-ref>", we would fail if <ref> is *not*
a symref to <old-ref>, so the case where <ref> is a normal ref
is already covered. 

Should we support "symref-update <ref> <new-ref> <old-oid>" that
makes <ref> a symref that points at <new-ref>, but ensures that
<ref> before the operation is a normal ref that points at <old-oid>?

Or should "symref-update" work only on <ref> that is an existing
symref?

I think I am OK if the answer was "You can only give a precondition
in the form of <old-ref>, which means you can only turn an existing
symref to point at a different ref with precondition protection. If
you want to turn a normal ref into a symref, you have to force it by
not having a precondition, or delete the ref and then (re)create it
as a symref".  But we need to decide the semantics and document it.

Thanks.
Junio C Hamano April 1, 2024, 8:40 p.m. UTC | #8
Junio C Hamano <gitster@pobox.com> writes:

>> But this still means we need to think of the best output for the
>> reference transaction hook (following commit).
>> ...

One thing I missed.  We are not currently reporting symref updates
to these hooks.  Are they prepared to take such extra input?  If not,
are they going to barf when they see "symref-update" while expecting
to see <old-oid>?

We may need to make it possible for Git to tell which variant of the
hook script it was given somehow (the easiest cop-out is to introduce
ref-transaction-hook-v2 as a separate hook, and use it if exists, or
fall back to the reference-transaction hook, and report symref updates
only when we are using v2, but there may be better approaches).

> But this is not an end-user input that tells Git "I do not care
> about precondition, I did not even bother to learn the current state
> to give you as <old-something>, just force it".  The input to hook
> is what we tell the hook what we are planning to do (so that it can
> decline), and we do not need the ability to say "I do not know what
> the current state is".  So I do not think you need any "zero" value
> in the input to the reference-transaction hook.  And I do not see a
> need for the "symref-update-forced" variant, either.

I misspoke here.  We do need "zero" value to indicate that "this
update is a creation event" and "this update is a deletion event".
What I meant to say is that there is no need to make the "zero"
value distinguishable from a "missing optional" value, which was a
problem on the "--stdin" side with "-z" format, where each command
is in a format with fixed number of parameters, unlike the textual
format, where a missing optional argument can be expressed by
omitting SP before the value and the value itself and it can be
differentiated from an empty string as an optional value that is not
missing.

Thanks.
Karthik Nayak April 1, 2024, 10:37 p.m. UTC | #9
Junio C Hamano <gitster@pobox.com> writes:

> Karthik Nayak <karthik.188@gmail.com> writes:
>
>>> So perhaps we can say "update with a concrete <old-oid> will ensure
>>> that the <ref> poitns at <old-oid> before proceeding, but update
>>> with 0{40} as <old-oid> to ensure creation is deprecated.  update
>>> with 0{40} as <new-oid> as deletion is also deprecated.  Use create
>>> and delete for these two deprecated operation modes".
>>>
>>> This assumes that create and delete currently ensures that what is
>>> asked to be created does not exist, and what is asked to be deleted
>>> does exist, before the operation.  If we are loosely doing these two
>>> operations, then we cannot easily deprecate the checking-update,
>>> without breaking existing users.
>
> Note that I did not (and do not) know if "create" and "delete" have
> such checks; I expected somebody (other than me) to check before
> going forward.
>

create does:

$ git update-ref --stdin
create refs/heads/a 53eb33454ce4f3db4d8c79e9c15640c2dffc4a37
fatal: cannot lock ref 'refs/heads/a': reference already exists

delete doesn't:

$ git update-ref --stdin
delete refs/heads/b
% echo $?
0
$ ls .git/refs/heads/
a  master

>> But this still means we need to think of the best output for the
>> reference transaction hook (following commit).
>>
>> My current implementation of:
>>    <symref-target> SP <ref-name> LF
>> Should be changed to:
>>    <old-ref> SP <new-ref> LF
>>
>> But this means, for creation of symrefs <old-ref> needs to be "zero"
>> value. Also there is no way for clients to differentiate between regular
>> refs and symrefs here. I wonder if it makes sense to do something like:
>>
>>    symref SP <old-ref> SP <new-ref> LF
>
> What do clients see for a regular ref?  "<old-oid> SP <new-oid> LF"?
> That would be OK, as "symref" cannot be an object name, I guess?
>

<old-value> SP <new-value> SP <ref-name> LF

Yeah, 'symref' should work.

>> Where symref is a fixed string at the start, used as a differentiator.
>> This however still would leave us to deal with "zero" values for
>> creation and deletion.
>
> Are these two <old-ref> and <new-ref> values optional in the context
> of your discussion?  The review comment was on input from the end-user
> that made it optional to validate the precondition, but this is what
> you produce as a result---if these are not optional, then an empty
> string can be a reasonable "zero" value.  I am confused...
>> Perhaps the best way here to actually be a lot more verbose and have the
>> hook output the following:
>>
>>    symref-create SP <new-ref> LF
>>    symref-delete SP <old-ref> LF
>>    symref-update SP <old-ref> SP <new-ref> LF
>>    symref-update-forced <new-ref> LF
>
> It is unfortunate that the input to the hook for a normal reference
> update uses syntax different from the "--stdin" input format, i.e.,
>
>     <old-oid> SP <new-oid> SP <ref> LF
>
> but it is way too late to change it now.  So to be consistent,
>
>     symref-create SP <new-ref> SP <ref> LF
>     symref-delete SP <old-ref> SP <ref> LF
>     symref-update SP <old-ref> SP <new-ref> SP <ref> LF
>
> would be the three operations.
>
> But this is not an end-user input that tells Git "I do not care
> about precondition, I did not even bother to learn the current state
> to give you as <old-something>, just force it".  The input to hook
> is what we tell the hook what we are planning to do (so that it can
> decline), and we do not need the ability to say "I do not know what
> the current state is".  So I do not think you need any "zero" value
> in the input to the reference-transaction hook.  And I do not see a
> need for the "symref-update-forced" variant, either.

... also from your latest email ...

>> But this is not an end-user input that tells Git "I do not care
>> about precondition, I did not even bother to learn the current state
>> to give you as <old-something>, just force it".  The input to hook
>> is what we tell the hook what we are planning to do (so that it can
>> decline), and we do not need the ability to say "I do not know what
>> the current state is".  So I do not think you need any "zero" value
>> in the input to the reference-transaction hook.  And I do not see a
>> need for the "symref-update-forced" variant, either.
>
> I misspoke here.  We do need "zero" value to indicate that "this
> update is a creation event" and "this update is a deletion event".
> What I meant to say is that there is no need to make the "zero"
> value distinguishable from a "missing optional" value, which was a
> problem on the "--stdin" side with "-z" format, where each command
> is in a format with fixed number of parameters, unlike the textual
> format, where a missing optional argument can be expressed by
> omitting SP before the value and the value itself and it can be
> differentiated from an empty string as an optional value that is not
> missing.
>
> Thanks.

Yup. There is a slight subtlety here though, currently with the
reference-transaction hook:

    When force updating the reference regardless of its current value or
    when the reference is 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.

I'll keep the same behavior with the symref updates.

> By the way, if we were to use symref-{create,delete,update} here,
> wouldn't it make more sense to name the command on the "--stdin"
> side the same, i.e., not "update-symref" but "symref-update"?

If we were to use symref-*, definitely.

> What I suspect that needs more thought is what should happen when
> you request via "--stdin" to create, update, or delete a symref,
> but <ref> is a regular ref, e.g., "symref-delete <ref>".  For
> "symref-create <ref> <new-ref>", we would fail if <ref> exists,
> whether it is a symref or a normal ref, so that is OK.  For
> "symref-delete <ref> <old-ref>", we would fail if <ref> is *not*
> a symref to <old-ref>, so the case where <ref> is a normal ref
> is already covered.
>
> Should we support "symref-update <ref> <new-ref> <old-oid>" that
> makes <ref> a symref that points at <new-ref>, but ensures that
> <ref> before the operation is a normal ref that points at <old-oid>?
>
> Or should "symref-update" work only on <ref> that is an existing
> symref?
>
> I think I am OK if the answer was "You can only give a precondition
> in the form of <old-ref>, which means you can only turn an existing
> symref to point at a different ref with precondition protection. If
> you want to turn a normal ref into a symref, you have to force it by
> not having a precondition, or delete the ref and then (re)create it
> as a symref".  But we need to decide the semantics and document it.

I would think that doing what you mentioned in the last para to be the
way to go, unless someone thinks otherwise. This allows the ugly mess of
parsing an value as a ref and then as a oid and provides some structure
to what the input cases are.

One more case I'd add is that the <ref> argument for "symref-delete"
should be optional, similar to having forced update, we'd also want to
support forced deletion.

> Junio C Hamano <gitster@pobox.com> writes:
>
>>> But this still means we need to think of the best output for the
>>> reference transaction hook (following commit).
>>> ...
>
> One thing I missed.  We are not currently reporting symref updates
> to these hooks.  Are they prepared to take such extra input?  If not,
> are they going to barf when they see "symref-update" while expecting
> to see <old-oid>?
>
> We may need to make it possible for Git to tell which variant of the
> hook script it was given somehow (the easiest cop-out is to introduce
> ref-transaction-hook-v2 as a separate hook, and use it if exists, or
> fall back to the reference-transaction hook, and report symref updates
> only when we are using v2, but there may be better approaches).
>

Well I was hoping this is okay since reference-transaction and
update-ref never supported symrefs. So after this change:
1. If a user never updates the hook to support symrefs, but doesn't use
symref feature of update-ref, they would be fine.
2. If a user starts using symref features of update-ref, they would see
that reference transaction needs to be updated too.

This especially since the hook's documentation always claimed that
symref support might be added in the future.

   The hook does not cover symbolic references (but that may change in
   the future).

---

In summary the plan going forward from my side would be to:

Implement the following in update-ref:

    symref-create SP <ref> SP <new-ref> LF
    symref-update SP <ref> SP <new-ref> [SP <old-ref>] LF
    symref-delete SP <ref> [SP <old-ref>] LF
    symref-verify SP <ref> [SP <old-ref>] LF

Also on the reference transaction hook, we'll be adding the following
new inputs to the hook:

    symref-create SP <new-ref> SP <ref> LF
    symref-delete SP <old-ref> SP <ref> LF
    symref-update SP <old-ref> SP <new-ref> SP <ref> LF

This either will be added to the existing hook or we would support a new
hook (v2).
Patrick Steinhardt April 2, 2024, 12:20 p.m. UTC | #10
On Sun, Mar 31, 2024 at 06:31:14PM -0700, Junio C Hamano wrote:
> Junio C Hamano <gitster@pobox.com> writes:
> 
> > Chris Torek <chris.torek@gmail.com> writes:
> >
> >> For these reasons, I'd suggest that the all-zero hash be officially
> >> deprecated in favor of create/delete and of course create-symref
> >> and delete-symref. Of course, compatibility requires some sort
> >> of support for the old system for some time.  As to whether that
> >> means something like the suggestion of ".missing" etc, I have no
> >> particular opinion -- but since the symref options are new, they
> >> would not *need* symmetric options, if we just say that "update-symref"
> >> cannot create or delete a symref.
> >
> > I love that simplicity.
> 
> Having said that, the loose "update that can create or delete" may
> actually be used by applications that do not care about overwriting
> competing operation, so I am not sure if we can easily deprecate
> that mode of operation.  Saying "update refs/heads/next to point at
> this object" and have it created if it does not exist may be handy
> for some loosely written applications.

I wouldn't say "loosely written here". I certainly know that we do use
these implicit modes in Gitaly, and we have conciously chosen them
because they have been supported by Git all along. It simply makes our
lifes easier when we don't have to special-case creations and deletions
in any way.

So I'd really not want those to go away or become deprecated.

Patrick

> So perhaps we can say "update with a concrete <old-oid> will ensure
> that the <ref> poitns at <old-oid> before proceeding, but update
> with 0{40} as <old-oid> to ensure creation is deprecated.  update
> with 0{40} as <new-oid> as deletion is also deprecated.  Use create
> and delete for these two deprecated operation modes".
> 
> This assumes that create and delete currently ensures that what is
> asked to be created does not exist, and what is asked to be deleted
> does exist, before the operation.  If we are loosely doing these two
> operations, then we cannot easily deprecate the checking-update,
> without breaking existing users.
> 
> As {create,update,delete,verify}-symref do not exist yet, we can
> start with the right semantics from day one.  "update-symref" will
> accept a <old-ref> only to ensure that the symref is pointing to
> that ref and there is no "zero" value based creation/deletion
> validation offered via "update-symref".  "create-symref" will error
> out if the ref asked to be created already exists, "delete-symref"
> will error out if the ref asked to be deleted does not exist.
>
Junio C Hamano April 2, 2024, 4:40 p.m. UTC | #11
Patrick Steinhardt <ps@pks.im> writes:

> because they have been supported by Git all along. It simply makes our
> lifes easier when we don't have to special-case creations and deletions
> in any way.
>
> So I'd really not want those to go away or become deprecated.

That is a good input.

Do you have anything to add as a counter-proposal?  The "I do not
care what was there before" update mode does make it necessary to
have a "zero" value for symrefs that can be distinguishable from
not having a value at all.

Thanks.
Patrick Steinhardt April 9, 2024, 11:55 a.m. UTC | #12
On Tue, Apr 02, 2024 at 09:40:41AM -0700, Junio C Hamano wrote:
> Patrick Steinhardt <ps@pks.im> writes:
> 
> > because they have been supported by Git all along. It simply makes our
> > lifes easier when we don't have to special-case creations and deletions
> > in any way.
> >
> > So I'd really not want those to go away or become deprecated.
> 
> That is a good input.
> 
> Do you have anything to add as a counter-proposal?  The "I do not
> care what was there before" update mode does make it necessary to
> have a "zero" value for symrefs that can be distinguishable from
> not having a value at all.
> 
> Thanks.

Sorry for taking this long to answer your question.

I might have missed it while scanning through this thread, but why
exactly is the zero OID not a good enough placeholder here to say that
the ref must not exist? A symref cannot point to a ref named after the
zero OID anyway.

In my opinion, "update-symref" with an old-value must be able to accept
both object IDs and symrefs as old value. Like this it would be possible
to update a proper ref to a symref in a race-free way. So you can say:

    git update-ref SYMREF refs/heads/main 19981daefd7c147444462739375462b49412ce33

To update "SYRMEF" to "refs/heads/main", but only in case it currently
is a proper ref that points to 19981daefd7c147444462739375462b49412ce33.
Similarly...

    git update-ref SYMREF refs/heads/main refs/heads/master

would update "SYMREF" to "refs/heads/main", but only if it currently
points to the symref "refs/heads/master". And by extension I think that
the zero OID should retain its established meaning of "This ref must not
exist":

    git update-ref SYMREF refs/heads/main 0000000000000000000000000000000000000000

This would only update "SYMREF" to "refs/heads/main" if it does not yet
exist.

Patrick
Karthik Nayak April 9, 2024, 4:15 p.m. UTC | #13
Patrick Steinhardt <ps@pks.im> writes:

> On Tue, Apr 02, 2024 at 09:40:41AM -0700, Junio C Hamano wrote:
>> Patrick Steinhardt <ps@pks.im> writes:
>>
>> > because they have been supported by Git all along. It simply makes our
>> > lifes easier when we don't have to special-case creations and deletions
>> > in any way.
>> >
>> > So I'd really not want those to go away or become deprecated.
>>
>> That is a good input.
>>
>> Do you have anything to add as a counter-proposal?  The "I do not
>> care what was there before" update mode does make it necessary to
>> have a "zero" value for symrefs that can be distinguishable from
>> not having a value at all.
>>
>> Thanks.
>
> Sorry for taking this long to answer your question.
>
> I might have missed it while scanning through this thread, but why
> exactly is the zero OID not a good enough placeholder here to say that
> the ref must not exist? A symref cannot point to a ref named after the
> zero OID anyway.
>
> In my opinion, "update-symref" with an old-value must be able to accept
> both object IDs and symrefs as old value. Like this it would be possible
> to update a proper ref to a symref in a race-free way. So you can say:
>
>     git update-ref SYMREF refs/heads/main 19981daefd7c147444462739375462b49412ce33
>
> To update "SYRMEF" to "refs/heads/main", but only in case it currently
> is a proper ref that points to 19981daefd7c147444462739375462b49412ce33.
> Similarly...
>
>     git update-ref SYMREF refs/heads/main refs/heads/master
>
> would update "SYMREF" to "refs/heads/main", but only if it currently
> points to the symref "refs/heads/master". And by extension I think that
> the zero OID should retain its established meaning of "This ref must not
> exist":
>
>     git update-ref SYMREF refs/heads/main 0000000000000000000000000000000000000000
>
> This would only update "SYMREF" to "refs/heads/main" if it does not yet
> exist.
>

This is definitely nicer experience for the user. From looking at the
other commands, {verify, create, delete} I can only see this applying to
`symref-update`. Making the syntax for update something like:

    symref-update SP <ref> SP <new-ref> [SP (<old-oid> | <old-rev>)] LF

I think this makes sense to me, will incorporate this and send the next
version in the next few days.

On a side-note: This would also mean that we should somehow support
moving from symrefs to a regular ref via a transaction, so that means we
should allow

    update SP <ref> SP <new-oid> [SP (<old-oid> | <old-rev>)] LF

too, but I'm not going to tackle that in my patches.
Patrick Steinhardt April 10, 2024, 4:20 a.m. UTC | #14
On Tue, Apr 09, 2024 at 09:15:59AM -0700, Karthik Nayak wrote:
> Patrick Steinhardt <ps@pks.im> writes:
> 
> > On Tue, Apr 02, 2024 at 09:40:41AM -0700, Junio C Hamano wrote:
> >> Patrick Steinhardt <ps@pks.im> writes:
> >>
> >> > because they have been supported by Git all along. It simply makes our
> >> > lifes easier when we don't have to special-case creations and deletions
> >> > in any way.
> >> >
> >> > So I'd really not want those to go away or become deprecated.
> >>
> >> That is a good input.
> >>
> >> Do you have anything to add as a counter-proposal?  The "I do not
> >> care what was there before" update mode does make it necessary to
> >> have a "zero" value for symrefs that can be distinguishable from
> >> not having a value at all.
> >>
> >> Thanks.
> >
> > Sorry for taking this long to answer your question.
> >
> > I might have missed it while scanning through this thread, but why
> > exactly is the zero OID not a good enough placeholder here to say that
> > the ref must not exist? A symref cannot point to a ref named after the
> > zero OID anyway.
> >
> > In my opinion, "update-symref" with an old-value must be able to accept
> > both object IDs and symrefs as old value. Like this it would be possible
> > to update a proper ref to a symref in a race-free way. So you can say:
> >
> >     git update-ref SYMREF refs/heads/main 19981daefd7c147444462739375462b49412ce33
> >
> > To update "SYRMEF" to "refs/heads/main", but only in case it currently
> > is a proper ref that points to 19981daefd7c147444462739375462b49412ce33.
> > Similarly...
> >
> >     git update-ref SYMREF refs/heads/main refs/heads/master
> >
> > would update "SYMREF" to "refs/heads/main", but only if it currently
> > points to the symref "refs/heads/master". And by extension I think that
> > the zero OID should retain its established meaning of "This ref must not
> > exist":
> >
> >     git update-ref SYMREF refs/heads/main 0000000000000000000000000000000000000000
> >
> > This would only update "SYMREF" to "refs/heads/main" if it does not yet
> > exist.
> >
> 
> This is definitely nicer experience for the user. From looking at the
> other commands, {verify, create, delete} I can only see this applying to
> `symref-update`. Making the syntax for update something like:
> 
>     symref-update SP <ref> SP <new-ref> [SP (<old-oid> | <old-rev>)] LF
> 
> I think this makes sense to me, will incorporate this and send the next
> version in the next few days.
> 
> On a side-note: This would also mean that we should somehow support
> moving from symrefs to a regular ref via a transaction, so that means we
> should allow
> 
>     update SP <ref> SP <new-oid> [SP (<old-oid> | <old-rev>)] LF
> 
> too, but I'm not going to tackle that in my patches.

Yes, I think that would be a sensible idea, even though we have to be
careful with backwards compatibility here. In any case, I think it makes
sense to not extend the scope of your patch series and leave this for
the future.

Patrick
Junio C Hamano April 10, 2024, 4:06 p.m. UTC | #15
Patrick Steinhardt <ps@pks.im> writes:

>> > I might have missed it while scanning through this thread, but why
>> > exactly is the zero OID not a good enough placeholder here to say that
>> > the ref must not exist? A symref cannot point to a ref named after the
>> > zero OID anyway.
>
>> > In my opinion, "update-symref" with an old-value must be able to accept
>> > both object IDs and symrefs as old value. Like this it would be possible
>> > to update a proper ref to a symref in a race-free way. So you can say:
>> >
>> >     git update-ref SYMREF refs/heads/main 19981daefd7c147444462739375462b49412ce33
>
>> > To update "SYRMEF" to "refs/heads/main", but only in case it currently
>> > is a proper ref that points to 19981daefd7c147444462739375462b49412ce33.
>> > Similarly...
>> >
>> >     git update-ref SYMREF refs/heads/main refs/heads/master
>
>> > would update "SYMREF" to "refs/heads/main", but only if it currently
>> > points to the symref "refs/heads/master".

I think that would work well.  We need to explicitly forbid a file
$GIT_DIR/[0-9a-f]{40} to be used as a pseudoref, which I think that
is an improvement.  I do not know how the transition to move to a
world with a stricter rule would look like, though.

>> > And by extension I think that
>> > the zero OID should retain its established meaning of "This ref must not
>> > exist":
>> >
>> >     git update-ref SYMREF refs/heads/main 0000000000000000000000000000000000000000
>> >
>> > This would only update "SYMREF" to "refs/heads/main" if it does not yet
>> > exist.

We express "SYMREF must currently be a symref" by naming an old-ref,
and we say "SYMREF can currently be either a ref or symref, but it
must point at this object" by naming an old-oid.  The looseness of
the latter (i.e. we cannot express "Before making HEAD point at ref
X, ensure that it is detached at OID") looks a bit disturbing, but
otherwise looks good.  I offhand do not know how expressive we would
want the "old" requirement to be.

>> On a side-note: This would also mean that we should somehow support
>> moving from symrefs to a regular ref via a transaction, so that means we
>> should allow
>> 
>>     update SP <ref> SP <new-oid> [SP (<old-oid> | <old-rev>)] LF
>> 
>> too, but I'm not going to tackle that in my patches.
>
> Yes, I think that would be a sensible idea, even though we have to be
> careful with backwards compatibility here. In any case, I think it makes
> sense to not extend the scope of your patch series and leave this for
> the future.

Likewise, we may want to be able to express "Before making HEAD
detached at commit X, ensure that HEAD points at ref Y that points
at commit Z".  IOW, the "old" part might have to be not

	[SP (<old-oid> | <old-ref>)]

but

	[SP <old-oid> SP <old-ref>]

to specify both, perhaps?
Patrick Steinhardt April 10, 2024, 5:31 p.m. UTC | #16
On Wed, Apr 10, 2024 at 09:06:45AM -0700, Junio C Hamano wrote:
> Patrick Steinhardt <ps@pks.im> writes:
> 
> >> > I might have missed it while scanning through this thread, but why
> >> > exactly is the zero OID not a good enough placeholder here to say that
> >> > the ref must not exist? A symref cannot point to a ref named after the
> >> > zero OID anyway.
> >
> >> > In my opinion, "update-symref" with an old-value must be able to accept
> >> > both object IDs and symrefs as old value. Like this it would be possible
> >> > to update a proper ref to a symref in a race-free way. So you can say:
> >> >
> >> >     git update-ref SYMREF refs/heads/main 19981daefd7c147444462739375462b49412ce33
> >
> >> > To update "SYRMEF" to "refs/heads/main", but only in case it currently
> >> > is a proper ref that points to 19981daefd7c147444462739375462b49412ce33.
> >> > Similarly...
> >> >
> >> >     git update-ref SYMREF refs/heads/main refs/heads/master
> >
> >> > would update "SYMREF" to "refs/heads/main", but only if it currently
> >> > points to the symref "refs/heads/master".
> 
> I think that would work well.  We need to explicitly forbid a file
> $GIT_DIR/[0-9a-f]{40} to be used as a pseudoref, which I think that
> is an improvement.  I do not know how the transition to move to a
> world with a stricter rule would look like, though.

I thought that Git already refuses such refnames anyway. Otherwise it
would be impossible to distinguish a ref called [0-9a-f]{40} from the
actual object hash in much of our tooling. I certainly know that GitLab
does refuse such refnames, and thought that GitHub does, too.

But turns out that at least git-update-ref(1) is happy to write such
refs:

```
$ git update-ref 1111111111111111111111111111111111111111 HEAD
$ cat .git/1111111111111111111111111111111111111111
cf6ba211cd2fce88f5d22d9f036029d502565509
```

What does it resolve to?

```
$ git rev-parse --verify 1111111111111111111111111111111111111111
warning: refname '1111111111111111111111111111111111111111' is ambiguous.
Git normally never creates a ref that ends with 40 hex characters
because it will be ignored when you just specify 40-hex. These refs
may be created by mistake. For example,

  git switch -c $br $(git rev-parse ...)

where "$br" is somehow empty and a 40-hex ref is created. Please
examine these refs and maybe delete them. Turn this message off by
running "git config advice.objectNameWarning false"
1111111111111111111111111111111111111111
```

It resolves to 1111111111111111111111111111111111111111 because it's a
valid object ID already. And what if we try to peel it?

```
$ git rev-parse --verify 1111111111111111111111111111111111111111^{commit}
warning: refname '1111111111111111111111111111111111111111' is ambiguous.
Git normally never creates a ref that ends with 40 hex characters
because it will be ignored when you just specify 40-hex. These refs
may be created by mistake. For example,

  git switch -c $br $(git rev-parse ...)

where "$br" is somehow empty and a 40-hex ref is created. Please
examine these refs and maybe delete them. Turn this message off by
running "git config advice.objectNameWarning false"
fatal: Needed a single revision
```

Doesn't work.

So these refs essentially cannot be accessed anyway. Restricting
git-update-ref(1) such that it cannot write them in the first place thus
shouldn't be breaking much, I'd claim, and feels like a strict
improvement overall.

Patrick
diff mbox series

Patch

diff --git a/Documentation/git-update-ref.txt b/Documentation/git-update-ref.txt
index 0561808cca..2ea8bc8167 100644
--- a/Documentation/git-update-ref.txt
+++ b/Documentation/git-update-ref.txt
@@ -65,6 +65,7 @@  performs all modifications together.  Specify commands of the form:
 	create SP <ref> SP <newvalue> LF
 	delete SP <ref> [SP <oldvalue>] LF
 	verify SP <ref> [SP <oldvalue>] LF
+	update-symref SP <ref> SP <newvalue> LF
 	option SP <opt> LF
 	start LF
 	prepare LF
@@ -86,6 +87,7 @@  quoting:
 	create SP <ref> NUL <newvalue> NUL
 	delete SP <ref> NUL [<oldvalue>] NUL
 	verify SP <ref> NUL [<oldvalue>] NUL
+	update-symref NUL <ref> NUL <newvalue> NUL
 	option SP <opt> NUL
 	start NUL
 	prepare NUL
@@ -111,12 +113,19 @@  create::
 
 delete::
 	Delete <ref> after verifying it exists with <oldvalue>, if
-	given.  If given, <oldvalue> may not be zero.
+	given.  If given, <oldvalue> may not be zero. Can also delete
+	symrefs.
 
 verify::
 	Verify <ref> against <oldvalue> but do not change it.  If
 	<oldvalue> is zero or missing, the ref must not exist.
 
+update-symref::
+	Update <ref> as a symbolic reference to point to the given
+	reference <newvalue>. By default, <ref> will be recursively
+	de-referenced, unless the `no-deref` option is used. Can also
+	be used to create new symrefs.
+
 option::
 	Modify the behavior of the next command naming a <ref>.
 	The only valid option is `no-deref` to avoid dereferencing
diff --git a/builtin/update-ref.c b/builtin/update-ref.c
index 3807cf4106..357daf31b8 100644
--- a/builtin/update-ref.c
+++ b/builtin/update-ref.c
@@ -213,6 +213,48 @@  static void parse_cmd_update(struct ref_transaction *transaction,
 	strbuf_release(&err);
 }
 
+static void parse_cmd_update_symref(struct ref_transaction *transaction,
+				    const char *next, const char *end)
+{
+	struct strbuf err = STRBUF_INIT;
+	char *refname, *symref;
+
+	refname = parse_refname(&next);
+	if (!refname)
+		die("update-symref: missing <ref>");
+
+	if (line_termination) {
+		/* Without -z, consume SP and use next argument */
+		if (!*next || *next == line_termination)
+			die("update-symref %s: missing <newvalue>", refname);
+		if (*next != ' ')
+			die("update-symref %s: expected SP but got: %s",
+			    refname, next);
+	} else {
+		/* With -z, read the next NUL-terminated line */
+		if (*next)
+			die("update-symref %s: missing <newvalue>", refname);
+	}
+	next++;
+
+	symref = parse_refname(&next);
+	if (!symref)
+		die("update-symref %s: missing <newvalue>", refname);
+
+	if (*next != line_termination)
+		die("update-symref %s: extra input: %s", refname, next);
+
+	if (ref_transaction_update(transaction, refname, NULL, NULL,
+				   update_flags | create_reflog_flag | REF_UPDATE_SYMREF,
+				   msg, symref, &err))
+		die("%s", err.buf);
+
+	update_flags = default_flags;
+	free(symref);
+	free(refname);
+	strbuf_release(&err);
+}
+
 static void parse_cmd_create(struct ref_transaction *transaction,
 			     const char *next, const char *end)
 {
@@ -379,15 +421,16 @@  static const struct parse_cmd {
 	unsigned args;
 	enum update_refs_state state;
 } command[] = {
-	{ "update",  parse_cmd_update,  3, UPDATE_REFS_OPEN },
-	{ "create",  parse_cmd_create,  2, UPDATE_REFS_OPEN },
-	{ "delete",  parse_cmd_delete,  2, UPDATE_REFS_OPEN },
-	{ "verify",  parse_cmd_verify,  2, UPDATE_REFS_OPEN },
-	{ "option",  parse_cmd_option,  1, UPDATE_REFS_OPEN },
-	{ "start",   parse_cmd_start,   0, UPDATE_REFS_STARTED },
-	{ "prepare", parse_cmd_prepare, 0, UPDATE_REFS_PREPARED },
-	{ "abort",   parse_cmd_abort,   0, UPDATE_REFS_CLOSED },
-	{ "commit",  parse_cmd_commit,  0, UPDATE_REFS_CLOSED },
+	{ "update",        parse_cmd_update,        3, UPDATE_REFS_OPEN },
+	{ "update-symref", parse_cmd_update_symref, 2, UPDATE_REFS_OPEN },
+	{ "create",        parse_cmd_create,        2, UPDATE_REFS_OPEN },
+	{ "delete",        parse_cmd_delete,        2, UPDATE_REFS_OPEN },
+	{ "verify",        parse_cmd_verify,        2, UPDATE_REFS_OPEN },
+	{ "option",        parse_cmd_option,        1, UPDATE_REFS_OPEN },
+	{ "start",         parse_cmd_start,         0, UPDATE_REFS_STARTED },
+	{ "prepare",       parse_cmd_prepare,       0, UPDATE_REFS_PREPARED },
+	{ "abort",         parse_cmd_abort,         0, UPDATE_REFS_CLOSED },
+	{ "commit",        parse_cmd_commit,        0, UPDATE_REFS_CLOSED },
 };
 
 static void update_refs_stdin(void)
diff --git a/refs.c b/refs.c
index 69b89a1aa2..706dcd6deb 100644
--- a/refs.c
+++ b/refs.c
@@ -1216,6 +1216,7 @@  void ref_transaction_free(struct ref_transaction *transaction)
 	}
 
 	for (i = 0; i < transaction->nr; i++) {
+		free(transaction->updates[i]->symref_target);
 		free(transaction->updates[i]->msg);
 		free(transaction->updates[i]);
 	}
@@ -1235,6 +1236,9 @@  struct ref_update *ref_transaction_add_update(
 	if (transaction->state != REF_TRANSACTION_OPEN)
 		BUG("update called for transaction that is not open");
 
+	if ((flags & (REF_HAVE_NEW | REF_UPDATE_SYMREF)) == (REF_HAVE_NEW | REF_UPDATE_SYMREF))
+		BUG("cannot create regular ref and symref at once");
+
 	FLEX_ALLOC_STR(update, refname, refname);
 	ALLOC_GROW(transaction->updates, transaction->nr + 1, transaction->alloc);
 	transaction->updates[transaction->nr++] = update;
@@ -1245,6 +1249,8 @@  struct ref_update *ref_transaction_add_update(
 		oidcpy(&update->new_oid, new_oid);
 	if (flags & REF_HAVE_OLD)
 		oidcpy(&update->old_oid, old_oid);
+	if (flags & REF_UPDATE_SYMREF)
+		update->symref_target = xstrdup(symref);
 	update->msg = normalize_reflog_message(msg);
 	return update;
 }
@@ -2337,6 +2343,10 @@  static int run_transaction_hook(struct ref_transaction *transaction,
 	for (i = 0; i < transaction->nr; i++) {
 		struct ref_update *update = transaction->updates[i];
 
+		// Reference transaction does not support symbolic updates.
+		if (update->flags & REF_UPDATE_SYMREF)
+			continue;
+
 		strbuf_reset(&buf);
 		strbuf_addf(&buf, "%s %s %s\n",
 			    oid_to_hex(&update->old_oid),
diff --git a/t/t0600-reffiles-backend.sh b/t/t0600-reffiles-backend.sh
index 64214340e7..6d334cb477 100755
--- a/t/t0600-reffiles-backend.sh
+++ b/t/t0600-reffiles-backend.sh
@@ -472,4 +472,34 @@  test_expect_success POSIXPERM 'git reflog expire honors core.sharedRepository' '
 	esac
 '
 
+test_expect_success SYMLINKS 'symref transaction supports symlinks' '
+	git update-ref refs/heads/new @ &&
+	test_config core.prefersymlinkrefs true &&
+	cat >stdin <<-EOF &&
+	start
+	update-symref TESTSYMREFONE 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' '
+	git update-ref refs/heads/new @ &&
+	test_config core.prefersymlinkrefs false &&
+	cat >stdin <<-EOF &&
+	start
+	update-symref TESTSYMREFONE 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 6ebc3ef945..2a6036471b 100755
--- a/t/t1400-update-ref.sh
+++ b/t/t1400-update-ref.sh
@@ -868,6 +868,105 @@  test_expect_success 'stdin delete symref works flag --no-deref' '
 	test_cmp expect actual
 '
 
+test_expect_success 'stdin update-symref creates symref with --no-deref' '
+	# ensure that the symref does not already exist 
+	test_must_fail git symbolic-ref --no-recurse refs/heads/symref &&
+	cat >stdin <<-EOF &&
+	update-symref refs/heads/symref $b
+	EOF
+	git update-ref --no-deref --stdin <stdin &&
+	echo $b >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 $b)" actual
+'
+
+test_expect_success 'stdin update-symref updates symref with --no-deref' '
+	# ensure that the symref already exists
+	git symbolic-ref --no-recurse refs/heads/symref &&
+	cat >stdin <<-EOF &&
+	update-symref refs/heads/symref $a
+	EOF
+	git update-ref --no-deref --stdin <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 "$(git rev-parse $b) $(git rev-parse $a)" actual
+'
+
+test_expect_success 'stdin update-symref noop update with --no-deref' '
+	git symbolic-ref --no-recurse refs/heads/symref >actual &&
+	echo $a >expect &&
+	test_cmp expect actual &&
+	cat >stdin <<-EOF &&
+	update-symref refs/heads/symref $a
+	EOF
+	git update-ref --no-deref --stdin <stdin &&
+	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-symref regular ref with --no-deref' '
+	git update-ref refs/heads/regularref $a &&
+	cat >stdin <<-EOF &&
+	update-symref refs/heads/regularref $a
+	EOF
+	git update-ref --no-deref --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 update-symref creates symref' '
+	# delete the ref since it already exists from previous tests
+	git update-ref --no-deref -d refs/heads/symref &&
+	cat >stdin <<-EOF &&
+	update-symref refs/heads/symref $b
+	EOF
+	git update-ref --stdin <stdin &&
+	echo $b >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 $b)" actual
+'
+
+test_expect_success 'stdin update-symref updates symref' '
+	git update-ref refs/heads/symref2 $b &&
+	git symbolic-ref --no-recurse refs/heads/symref refs/heads/symref2 &&
+	cat >stdin <<-EOF &&
+	update-symref refs/heads/symref $a
+	EOF
+	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 $b) $(git rev-parse $b)" actual
+'
+
+test_expect_success 'stdin update-symref regular ref' '
+	git update-ref --no-deref refs/heads/regularref $a &&
+	cat >stdin <<-EOF &&
+	update-symref refs/heads/regularref $a
+	EOF
+	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 delete ref works with right old value' '
 	echo "delete $b $m~1" >stdin &&
 	git update-ref --stdin <stdin &&
@@ -1641,4 +1740,32 @@  test_expect_success PIPE 'transaction flushes status updates' '
 	test_cmp expected actual
 '
 
+test_expect_success 'transaction can commit symref update' '
+	git symbolic-ref TESTSYMREFONE $a &&
+	cat >stdin <<-EOF &&
+	start
+	update-symref TESTSYMREFONE refs/heads/branch
+	prepare
+	commit
+	EOF
+	git update-ref --no-deref --stdin <stdin &&
+	echo refs/heads/branch >expect &&
+	git symbolic-ref TESTSYMREFONE >actual &&
+	test_cmp expect actual
+'
+
+test_expect_success 'transaction can abort symref update' '
+	git symbolic-ref TESTSYMREFONE $a &&
+	cat >stdin <<-EOF &&
+	start
+	update-symref TESTSYMREFONE refs/heads/branch
+	prepare
+	abort
+	EOF
+	git update-ref --no-deref --stdin <stdin &&
+	echo $a >expect &&
+	git symbolic-ref TESTSYMREFONE >actual &&
+	test_cmp expect actual
+'
+
 test_done