mbox series

[v2,0/7] update-ref: add symref oriented commands

Message ID 20240412095908.1134387-1-knayak@gitlab.com (mailing list archive)
Headers show
Series update-ref: add symref oriented commands | expand

Message

Karthik Nayak April 12, 2024, 9:59 a.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.

At GitLab, we've been using the manual process till date, to allow users
to set and change their default branch. But with the introduction of
reftables as a reference backend, this becomes a necessity to be solved
within git.

This patch series goes about introducing a set of commands
symref-{create,verify,delete,update} to work with symrefs complimenting
the existing commands for the regular refs within 'git-update-ref(1)'.

The 'symref-verify' command can be used to verify if a symref exists and
its existing value.

The 'symref-create' command can be used to create a new symref.

The 'symref-delete' command can be used to delete an existing symref while
optionally checking its existing value.

The 'symref-update' command can be used to update a symref, create a symref,
delete a symref or even convert an existing regular ref to a symref. Wherein
like the regular 'update' command, the zero OID can be used to create/delete
a symref.

V1 of the patch series can be found here:
https://lore.kernel.org/git/20240330224623.579457-1-knayak@gitlab.com/

I'm not adding a range diff here, cause I redid the whole series, things which
have changed:
1. The earlier series simply propagated a 'symref_target' and only supported the
'symref-update' command, without checks for existing values and such. Now we
support the entire fleet of commands with support for checking old_values.
2. The flow is now changedc to send an old_ref, new_ref pair in supplement to
the existing old_oid, new_oid pair to the reference backends. This allows the
backends to simply do a combination of changes based on what values are set.
This allows us to do symref-update's where we change a regular ref to a symref
while also validating its old OID.
3. I added a lot more tests to cover reflog checks and also '-z' input.
4. The entered <old-ref> and <new-ref> values are checked within update-ref, to
ensure we don't create dangling refs. This could be extended in the future with
a flag, maybe "REF_ALLOW_DANGLING_SYMREF" and a corresponding option within
update-ref.
5. Removed some commits where reftable backend code was reused for symref creation.
This actually caused issues since the reused code also created a reflog along with
the symref but we should defer reflog creations only after all ref creations have
taken place. There is a bit of DRY here, but I think overall its still much cleaner.

Thanks all for the review and discussion in the previous version. Patrick, I did
incorporate the changes you suggested, but I just noticed that I didn't reply to
your emails.

Karthik Nayak (7):
  refs: accept symref values in `ref_transaction[_add]_update`
  update-ref: add support for symref-verify
  update-ref: add support for symref-delete
  files-backend: extract out `create_symref_lock`
  update-ref: add support for symref-create
  update-ref: add support for symref-update
  refs: support symrefs in 'reference-transaction' hook

 Documentation/git-update-ref.txt |  25 +++
 Documentation/githooks.txt       |  13 +-
 branch.c                         |   2 +-
 builtin/clone.c                  |   2 +-
 builtin/fast-import.c            |   5 +-
 builtin/fetch.c                  |   4 +-
 builtin/receive-pack.c           |   4 +-
 builtin/replace.c                |   2 +-
 builtin/tag.c                    |   1 +
 builtin/update-ref.c             | 202 +++++++++++++++++--
 refs.c                           |  74 +++++--
 refs.h                           |  15 +-
 refs/files-backend.c             | 143 +++++++++++---
 refs/refs-internal.h             |  21 ++
 refs/reftable-backend.c          |  51 ++++-
 sequencer.c                      |   9 +-
 t/t0600-reffiles-backend.sh      |  32 ++++
 t/t1400-update-ref.sh            | 320 ++++++++++++++++++++++++++++++-
 t/t1416-ref-transaction-hooks.sh |  41 ++++
 walker.c                         |   2 +-
 20 files changed, 886 insertions(+), 82 deletions(-)

Comments

Junio C Hamano April 12, 2024, 6:01 p.m. UTC | #1
"make refs.sp" should have told you that you should not write NULL
as just 0.  I'll queue a SQUASH??? fix-up on top before merging it
into 'seen' for today's integration.

diff --git c/refs.c w/refs.c
index 010f426def..d578a2823b 100644
--- c/refs.c
+++ w/refs.c
@@ -2758,7 +2758,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, 0, msg, &err);
+					     NULL, flags, NULL, msg, &err);
 		if (ret) {
 			warning(_("could not delete reference %s: %s"),
 				item->string, err.buf);




Also there are quite many whitespace breakages.

$ git am -s ./+kn7-v2-update-ref-symref
Applying: refs: accept symref values in `ref_transaction[_add]_update`
Applying: update-ref: add support for symref-verify
.git/rebase-apply/patch:59: indent with spaces.
        if (line_termination) {
.git/rebase-apply/patch:60: indent with spaces.
                /* Without -z, consume SP and use next argument */
.git/rebase-apply/patch:61: indent with spaces.
                if (!**next || **next == line_termination)
.git/rebase-apply/patch:62: indent with spaces.
                        return NULL;
.git/rebase-apply/patch:63: indent with spaces.
                if (**next != ' ')
warning: squelched 10 whitespace errors
warning: 15 lines applied after fixing whitespace errors.
Applying: update-ref: add support for symref-delete
.git/rebase-apply/patch:95: indent with spaces.
                die("symref-delete: cannot operate with deref mode");
.git/rebase-apply/patch:101: indent with spaces.
        old_ref = parse_next_refname(&next);
warning: 2 lines applied after fixing whitespace errors.
Applying: files-backend: extract out `create_symref_lock`
Applying: update-ref: add support for symref-create
.git/rebase-apply/patch:81: indent with spaces.
                die("symref-create: cannot operate with deref mode");
warning: 1 line applied after fixing whitespace errors.
Applying: update-ref: add support for symref-update
Applying: refs: support symrefs in 'reference-transaction' hook
Karthik Nayak April 12, 2024, 6:49 p.m. UTC | #2
Junio C Hamano <gitster@pobox.com> writes:

> "make refs.sp" should have told you that you should not write NULL
> as just 0.  I'll queue a SQUASH??? fix-up on top before merging it
> into 'seen' for today's integration.
>
> diff --git c/refs.c w/refs.c
> index 010f426def..d578a2823b 100644
> --- c/refs.c
> +++ w/refs.c
> @@ -2758,7 +2758,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, 0, msg, &err);
> +					     NULL, flags, NULL, msg, &err);
>  		if (ret) {
>  			warning(_("could not delete reference %s: %s"),
>  				item->string, err.buf);
>
>

Thanks Junio, I'll apply it locally too.

>
>
> Also there are quite many whitespace breakages.
>
> $ git am -s ./+kn7-v2-update-ref-symref
> Applying: refs: accept symref values in `ref_transaction[_add]_update`
> Applying: update-ref: add support for symref-verify
> .git/rebase-apply/patch:59: indent with spaces.
>         if (line_termination) {
> .git/rebase-apply/patch:60: indent with spaces.
>                 /* Without -z, consume SP and use next argument */
> .git/rebase-apply/patch:61: indent with spaces.
>                 if (!**next || **next == line_termination)
> .git/rebase-apply/patch:62: indent with spaces.
>                         return NULL;
> .git/rebase-apply/patch:63: indent with spaces.
>                 if (**next != ' ')
> warning: squelched 10 whitespace errors
> warning: 15 lines applied after fixing whitespace errors.
> Applying: update-ref: add support for symref-delete
> .git/rebase-apply/patch:95: indent with spaces.
>                 die("symref-delete: cannot operate with deref mode");
> .git/rebase-apply/patch:101: indent with spaces.
>         old_ref = parse_next_refname(&next);
> warning: 2 lines applied after fixing whitespace errors.
> Applying: files-backend: extract out `create_symref_lock`
> Applying: update-ref: add support for symref-create
> .git/rebase-apply/patch:81: indent with spaces.
>                 die("symref-create: cannot operate with deref mode");
> warning: 1 line applied after fixing whitespace errors.
> Applying: update-ref: add support for symref-update
> Applying: refs: support symrefs in 'reference-transaction' hook

Sigh. Even with editorconfig, this seems to be problematic for me on
Emacs. I'll just start applying locally from now before sending it.

As such I've applied this locally and will refrain from sending a new
version until a review happens.

1:  3269d0e91e = 1:  3269d0e91e refs: accept symref values in
`ref_transaction[_add]_update`
2:  a8cb0e0a1d ! 2:  2293b514a8 update-ref: add support for symref-verify
    @@ builtin/update-ref.c: static char *parse_refname(const char **next)
      	return strbuf_detach(&ref, NULL);
      }

    -+
    -+
     +/*
     + * Wrapper around parse_refname which skips the next delimiter.
     + */
     +static char *parse_next_refname(const char **next)
     +{
    -+        if (line_termination) {
    -+                /* Without -z, consume SP and use next argument */
    -+                if (!**next || **next == line_termination)
    -+                        return NULL;
    -+                if (**next != ' ')
    -+                        die("expected SP but got: %s", *next);
    -+        } else {
    -+                /* With -z, read the next NUL-terminated line */
    -+                if (**next)
    -+                        return NULL;
    -+        }
    -+        /* Skip the delimiter */
    -+        (*next)++;
    ++	if (line_termination) {
    ++		/* Without -z, consume SP and use next argument */
    ++		if (!**next || **next == line_termination)
    ++			return NULL;
    ++		if (**next != ' ')
    ++			die("expected SP but got: %s", *next);
    ++	} else {
    ++		/* With -z, read the next NUL-terminated line */
    ++		if (**next)
    ++			return NULL;
    ++	}
    ++	/* Skip the delimiter */
    ++	(*next)++;
     +
    -+        return parse_refname(next);
    ++	return parse_refname(next);
     +}
     +
      /*
    @@ builtin/update-ref.c: static void parse_cmd_verify(struct
ref_transaction *trans
     +}
     +
     +static void parse_cmd_symref_verify(struct ref_transaction *transaction,
    -+                                    const char *next, const char *end)
    ++				    const char *next, const char *end)
     +{
     +	struct strbuf err = STRBUF_INIT;
     +	struct object_id old_oid;
3:  37c3e006da ! 3:  62d05e7cb7 update-ref: add support for symref-delete
    @@ builtin/update-ref.c: static void parse_cmd_delete(struct
ref_transaction *trans
     +	char *refname, *old_ref;
     +
     +	if (!(update_flags & REF_NO_DEREF))
    -+                die("symref-delete: cannot operate with deref mode");
    ++		die("symref-delete: cannot operate with deref mode");
     +
     +	refname = parse_refname(&next);
     +	if (!refname)
     +		die("symref-delete: missing <ref>");
     +
    -+        old_ref = parse_next_refname(&next);
    ++	old_ref = parse_next_refname(&next);
     +	if (old_ref && read_ref(old_ref, NULL))
     +		die("symref-delete %s: invalid <old-ref>", refname);
     +
    @@ refs.c: 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, 0, msg, &err);
    ++					     NULL, flags, NULL, msg, &err);
      		if (ret) {
      			warning(_("could not delete reference %s: %s"),
      				item->string, err.buf);
4:  53fdb408ef = 4:  1bd39a38dc files-backend: extract out `create_symref_lock`
5:  8fa0151f94 ! 5:  732d31b43d update-ref: add support for symref-create
    @@ builtin/update-ref.c: static void parse_cmd_create(struct
ref_transaction *trans
     +	char *refname, *new_ref;
     +
     +	if (!(update_flags & REF_NO_DEREF))
    -+                die("symref-create: cannot operate with deref mode");
    ++		die("symref-create: cannot operate with deref mode");
     +
     +	refname = parse_refname(&next);
     +	if (!refname)
6:  714492ede3 = 6:  eb3d239b4b update-ref: add support for symref-update
7:  c483104562 = 7:  3cdfb9e528 refs: support symrefs in
'reference-transaction' hook
Christian Couder April 18, 2024, 3:05 p.m. UTC | #3
On Fri, Apr 12, 2024 at 12:11 PM Karthik Nayak <karthik.188@gmail.com> wrote:

> The 'symref-update' command can be used to update a symref, create a symref,
> delete a symref or even convert an existing regular ref to a symref. Wherein
> like the regular 'update' command, the zero OID can be used to create/delete
> a symref.

Is it possible to also convert a symref to a regular ref, like when
HEAD becomes detached?

> 2. The flow is now changedc to send an old_ref, new_ref pair in supplement to

s/changedc/changed/

> the existing old_oid, new_oid pair to the reference backends. This allows the
> backends to simply do a combination of changes based on what values are set.

I had a number of comments and questions when reading the first few
patches in this series. I also took a very quick look at the rest of
the series, but I think answering my questions about the first few
patches would make reading the rest of the series easier, so I will
take a deeper look later.

Thanks.
Patrick Steinhardt April 20, 2024, 6:16 a.m. UTC | #4
On Fri, Apr 12, 2024 at 11:59:01AM +0200, Karthik Nayak wrote:
> 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.
> 
> At GitLab, we've been using the manual process till date, to allow users
> to set and change their default branch. But with the introduction of
> reftables as a reference backend, this becomes a necessity to be solved
> within git.
> 
> This patch series goes about introducing a set of commands
> symref-{create,verify,delete,update} to work with symrefs complimenting
> the existing commands for the regular refs within 'git-update-ref(1)'.

One more thought crossed my mind this night: is it even necessary to
introduce new commands for this? As far as I can see, it should be
feasible to introduce symref support for all existing commands without
breaking backwards compatibility. This can be achieved by using a prefix
that cannot ever be an object ID, like for example "ref:".

Thus, all of the following should work (with 1234 being an OID and 0000
being the null OID):

    update HEAD ref:refs/heads/main ref:refs/heads/master
    update HEAD ref:refs/heads/main 1234
    update HEAD ref:refs/heads/main 0000
    update HEAD 1234 ref:refs/heads/main
    update HEAD 0000 ref:refs/heads/main
    create HEAD ref:refs/heads/main
    delete HEAD ref:refs/heads/main
    verify HEAD ref:refs/heads/mains

Parsing is unambiguous because we can use `starts_with("ref:")` for an
otherwise invalid object ID. Furthermore, because refs cannot have
spaces, we also don't have an issue with the SP separator.

I have a hunch that this variant might lead to less code duplication,
lead to less confusing behaviour and also makes for an easier user
interface.

Patrick
Karthik Nayak April 21, 2024, 7:06 p.m. UTC | #5
Christian Couder <christian.couder@gmail.com> writes:

> On Fri, Apr 12, 2024 at 12:11 PM Karthik Nayak <karthik.188@gmail.com> wrote:
>
>> The 'symref-update' command can be used to update a symref, create a symref,
>> delete a symref or even convert an existing regular ref to a symref. Wherein
>> like the regular 'update' command, the zero OID can be used to create/delete
>> a symref.
>
> Is it possible to also convert a symref to a regular ref, like when
> HEAD becomes detached?
>

Currently not, that could be a follow up, since that would involve
updating the regular commands. A quick way to think about it is that
this series adds commands whose end product is a symref. Since what
you're talking about is the inverse, it makes sense to add it to the
existing commands.

>> 2. The flow is now changedc to send an old_ref, new_ref pair in supplement to
>
> s/changedc/changed/
>
>> the existing old_oid, new_oid pair to the reference backends. This allows the
>> backends to simply do a combination of changes based on what values are set.
>
> I had a number of comments and questions when reading the first few
> patches in this series. I also took a very quick look at the rest of
> the series, but I think answering my questions about the first few
> patches would make reading the rest of the series easier, so I will
> take a deeper look later.
>
> Thanks.

Thanks for the review. Really appreciate it.
Karthik Nayak April 21, 2024, 7:11 p.m. UTC | #6
Patrick Steinhardt <ps@pks.im> writes:

> On Fri, Apr 12, 2024 at 11:59:01AM +0200, Karthik Nayak wrote:
>> 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.
>>
>> At GitLab, we've been using the manual process till date, to allow users
>> to set and change their default branch. But with the introduction of
>> reftables as a reference backend, this becomes a necessity to be solved
>> within git.
>>
>> This patch series goes about introducing a set of commands
>> symref-{create,verify,delete,update} to work with symrefs complimenting
>> the existing commands for the regular refs within 'git-update-ref(1)'.
>
> One more thought crossed my mind this night: is it even necessary to
> introduce new commands for this? As far as I can see, it should be
> feasible to introduce symref support for all existing commands without
> breaking backwards compatibility. This can be achieved by using a prefix
> that cannot ever be an object ID, like for example "ref:".
>

I couldn't think of any reason that we can't do this. I think it would
work. I'll try to patch something with my existing tests and see if
breaks something.

> Thus, all of the following should work (with 1234 being an OID and 0000
> being the null OID):
>
>     update HEAD ref:refs/heads/main ref:refs/heads/master
>     update HEAD ref:refs/heads/main 1234
>     update HEAD ref:refs/heads/main 0000
>     update HEAD 1234 ref:refs/heads/main
>     update HEAD 0000 ref:refs/heads/main
>     create HEAD ref:refs/heads/main
>     delete HEAD ref:refs/heads/main
>     verify HEAD ref:refs/heads/mains
>
> Parsing is unambiguous because we can use `starts_with("ref:")` for an
> otherwise invalid object ID. Furthermore, because refs cannot have
> spaces, we also don't have an issue with the SP separator.
>
> I have a hunch that this variant might lead to less code duplication,
> lead to less confusing behaviour and also makes for an easier user
> interface.

I'm certain that the duplication in update-ref.c will go away with this.
Regarding other code changes, I think they'll stay the same.

But I do agree, this seems much nicer in terms of UX and also allows us
to introduce `symref => regular ref` conversion in this series itself.