]> git.ipfire.org Git - thirdparty/git.git/commitdiff
refs: add support for transactional symref updates
authorKarthik Nayak <karthik.188@gmail.com>
Tue, 7 May 2024 12:58:56 +0000 (14:58 +0200)
committerJunio C Hamano <gitster@pobox.com>
Tue, 7 May 2024 15:51:49 +0000 (08:51 -0700)
The reference backends currently support transactional reference
updates. While this is exposed to users via 'git-update-ref' and its
'--stdin' mode, it is also used internally within various commands.

However, we do not support transactional updates of symrefs. This commit
adds support for symrefs in both the 'files' and the 'reftable' backend.

Here, we add and use `ref_update_has_null_new_value()`, a helper
function which is used to check if there is a new_value in a reference
update. The new value could either be a symref target `new_target` or a
OID `new_oid`.

We also add another common function `ref_update_check_old_target` which
will be used to check if the update's old_target corresponds to a
reference's current target.

Now transactional updates (verify, create, delete, update) can be used
for:
- regular refs
- symbolic refs
- conversion of regular to symbolic refs and vice versa

This also allows us to expose this to users via new commands in
'git-update-ref' in the future.

Note that a dangling symref update does not record a new reflog entry,
which is unchanged before and after this commit.

Signed-off-by: Karthik Nayak <karthik.188@gmail.com>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
refs.c
refs/files-backend.c
refs/refs-internal.h
refs/reftable-backend.c

diff --git a/refs.c b/refs.c
index 990703ed16886a432e5e6c4955d15fe69b7ef568..d0ea7573d853af9085e38fb0dec169414008489a 100644 (file)
--- a/refs.c
+++ b/refs.c
@@ -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((char *)transaction->updates[i]->new_target);
+               free((char *)transaction->updates[i]->old_target);
                free(transaction->updates[i]);
        }
        free(transaction->updates);
@@ -1247,10 +1249,13 @@ struct ref_update *ref_transaction_add_update(
 
        update->flags = flags;
 
-       if (flags & REF_HAVE_NEW)
+       update->new_target = xstrdup_or_null(new_target);
+       update->old_target = xstrdup_or_null(old_target);
+       if ((flags & REF_HAVE_NEW) && new_oid)
                oidcpy(&update->new_oid, new_oid);
-       if (flags & REF_HAVE_OLD)
+       if ((flags & REF_HAVE_OLD) && old_oid)
                oidcpy(&update->old_oid, old_oid);
+
        update->msg = normalize_reflog_message(msg);
        return update;
 }
@@ -1286,6 +1291,7 @@ 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, new_target,
@@ -2822,3 +2828,30 @@ const char *ref_update_original_update_refname(struct ref_update *update)
 
        return update->refname;
 }
+
+int ref_update_has_null_new_value(struct ref_update *update)
+{
+       return !update->new_target && is_null_oid(&update->new_oid);
+}
+
+int ref_update_check_old_target(const char *referent, struct ref_update *update,
+                               struct strbuf *err)
+{
+       if (!update->old_target)
+               BUG("called without old_target set");
+
+       if (!strcmp(referent, update->old_target))
+               return 0;
+
+       if (!strcmp(referent, ""))
+               strbuf_addf(err, "verifying symref target: '%s': "
+                           "reference is missing but expected %s",
+                           ref_update_original_update_refname(update),
+                           update->old_target);
+       else
+               strbuf_addf(err, "verifying symref target: '%s': "
+                           "is at %s but expected %s",
+                           ref_update_original_update_refname(update),
+                           referent, update->old_target);
+       return -1;
+}
index 25e5d03496e8a01bb310babf9da69e2e8eff1eea..2d1525b240ec8d5ce73c06f0413dab04f948e0cc 100644 (file)
@@ -2394,8 +2394,9 @@ 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,
-                       NULL, NULL, update->msg);
+                       update->new_target ? NULL : &update->new_oid,
+                       update->old_target ? NULL : &update->old_oid,
+                       update->new_target, update->old_target, update->msg);
 
        new_update->parent_update = update;
 
@@ -2483,7 +2484,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_has_null_new_value(update))
                update->flags |= REF_DELETING;
 
        if (head_ref) {
@@ -2526,7 +2527,14 @@ static int lock_ref_for_update(struct files_ref_store *refs,
                                        ret = TRANSACTION_GENERIC_ERROR;
                                        goto out;
                                }
-                       } else if (check_old_oid(update, &lock->old_oid, err)) {
+                       }
+
+                       if (update->old_target) {
+                               if (ref_update_check_old_target(referent.buf, update, err)) {
+                                       ret = TRANSACTION_GENERIC_ERROR;
+                                       goto out;
+                               }
+                       } else if  (check_old_oid(update, &lock->old_oid, err)) {
                                ret = TRANSACTION_GENERIC_ERROR;
                                goto out;
                        }
@@ -2547,7 +2555,17 @@ static int lock_ref_for_update(struct files_ref_store *refs,
        } else {
                struct ref_update *parent_update;
 
-               if (check_old_oid(update, &lock->old_oid, err)) {
+               /*
+                * Even if the ref is a regular ref, if `old_target` is set, we
+                * check the referent value. Ideally `old_target` should only
+                * be set for symrefs, but we're strict about its usage.
+                */
+               if (update->old_target) {
+                       if (ref_update_check_old_target(referent.buf, update, err)) {
+                               ret = TRANSACTION_GENERIC_ERROR;
+                               goto out;
+                       }
+               } else if  (check_old_oid(update, &lock->old_oid, err)) {
                        ret = TRANSACTION_GENERIC_ERROR;
                        goto out;
                }
@@ -2565,9 +2583,28 @@ 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, err)) {
+                       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)) {
                        /*
@@ -2830,6 +2867,43 @@ cleanup:
        return ret;
 }
 
+static int parse_and_write_reflog(struct files_ref_store *refs,
+                                 struct ref_update *update,
+                                 struct ref_lock *lock,
+                                 struct strbuf *err)
+{
+       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)) {
+                       /*
+                        * TODO: currently we skip creating reflogs for dangling
+                        * symref updates. It would be nice to capture this as
+                        * zero oid updates however.
+                        */
+                       return 0;
+               }
+       }
+
+       if (files_log_ref_write(refs, lock->ref_name, &lock->old_oid,
+                               &update->new_oid, update->msg, update->flags, err)) {
+               char *old_msg = strbuf_detach(err, NULL);
+
+               strbuf_addf(err, "cannot update the ref '%s': %s",
+                           lock->ref_name, old_msg);
+               free(old_msg);
+               unlock_ref(lock);
+               update->backend_data = NULL;
+               return -1;
+       }
+
+       return 0;
+}
+
 static int files_transaction_finish(struct ref_store *ref_store,
                                    struct ref_transaction *transaction,
                                    struct strbuf *err)
@@ -2860,23 +2934,20 @@ static int files_transaction_finish(struct ref_store *ref_store,
 
                if (update->flags & REF_NEEDS_COMMIT ||
                    update->flags & REF_LOG_ONLY) {
-                       if (files_log_ref_write(refs,
-                                               lock->ref_name,
-                                               &lock->old_oid,
-                                               &update->new_oid,
-                                               update->msg, update->flags,
-                                               err)) {
-                               char *old_msg = strbuf_detach(err, NULL);
-
-                               strbuf_addf(err, "cannot update the ref '%s': %s",
-                                           lock->ref_name, old_msg);
-                               free(old_msg);
-                               unlock_ref(lock);
-                               update->backend_data = NULL;
+                       if (parse_and_write_reflog(refs, update, lock, err)) {
                                ret = TRANSACTION_GENERIC_ERROR;
                                goto cleanup;
                        }
                }
+
+               /*
+                * We try creating a symlink, if that succeeds we continue to the
+                * next update. 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)) {
index 617b93a6c854bceaf9da67051e4b2e8b31f7b4df..819157256e95e4e35466aa8add703884ba50d728 100644 (file)
@@ -754,4 +754,20 @@ struct ref_store *maybe_debug_wrap_ref_store(const char *gitdir, struct ref_stor
  */
 const char *ref_update_original_update_refname(struct ref_update *update);
 
+/*
+ * 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_has_null_new_value(struct ref_update *update);
+
+/*
+ * Check whether the old_target values stored in update are consistent
+ * with the referent, which is the symbolic reference's current value.
+ * If everything is OK, return 0; otherwise, write an error message to
+ * err and return -1.
+ */
+int ref_update_check_old_target(const char *referent, struct ref_update *update,
+                               struct strbuf *err);
+
 #endif /* REFS_REFS_INTERNAL_H */
index fe4a7681a0944ec7fff17237252e4f10a939dd66..4ad0d8137164ab141fe4d25ca2bdee413adaec77 100644 (file)
@@ -843,7 +843,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_has_null_new_value(u)) {
                                 ret = queue_transaction_update(refs, tx_data, u,
                                                                &current_oid, err);
                                 if (ret)
@@ -894,8 +894,10 @@ static int reftable_be_transaction_prepare(struct ref_store *ref_store,
                                 * intertwined with the locking in files-backend.c.
                                 */
                                new_update = ref_transaction_add_update(
-                                               transaction, referent.buf, new_flags,
-                                               &u->new_oid, &u->old_oid, NULL, NULL, u->msg);
+                                       transaction, referent.buf, new_flags,
+                                       &u->new_oid, &u->old_oid, u->new_target,
+                                       u->old_target, u->msg);
+
                                new_update->parent_update = u;
 
                                /*
@@ -925,7 +927,12 @@ 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->old_target) {
+                       if (ref_update_check_old_target(referent.buf, u, err)) {
+                               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"),
@@ -1030,7 +1037,9 @@ 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_has_null_new_value(u)) {
                        struct reftable_log_record log = {0};
                        struct reftable_iterator it = {0};
 
@@ -1071,24 +1080,52 @@ static int write_transaction_table(struct reftable_writer *writer, void *cb_data
                           (u->flags & REF_FORCE_CREATE_REFLOG ||
                            should_write_log(&arg->refs->base, u->refname))) {
                        struct reftable_log_record *log;
+                       int create_reflog = 1;
+
+                       if (u->new_target) {
+                               if (!refs_resolve_ref_unsafe(&arg->refs->base, u->new_target,
+                                                            RESOLVE_REF_READING, &u->new_oid, NULL)) {
+                                       /*
+                                        * TODO: currently we skip creating reflogs for dangling
+                                        * symref updates. It would be nice to capture this as
+                                        * zero oid updates however.
+                                        */
+                                       create_reflog = 0;
+                               }
+                       }
 
-                       ALLOC_GROW(logs, logs_nr + 1, logs_alloc);
-                       log = &logs[logs_nr++];
-                       memset(log, 0, sizeof(*log));
-
-                       fill_reftable_log_record(log);
-                       log->update_index = ts;
-                       log->refname = xstrdup(u->refname);
-                       memcpy(log->value.update.new_hash, u->new_oid.hash, GIT_MAX_RAWSZ);
-                       memcpy(log->value.update.old_hash, tx_update->current_oid.hash, GIT_MAX_RAWSZ);
-                       log->value.update.message =
-                               xstrndup(u->msg, arg->refs->write_options.block_size / 2);
+                       if (create_reflog) {
+                               ALLOC_GROW(logs, logs_nr + 1, logs_alloc);
+                               log = &logs[logs_nr++];
+                               memset(log, 0, sizeof(*log));
+
+                               fill_reftable_log_record(log);
+                               log->update_index = ts;
+                               log->refname = xstrdup(u->refname);
+                               memcpy(log->value.update.new_hash,
+                                      u->new_oid.hash, GIT_MAX_RAWSZ);
+                               memcpy(log->value.update.old_hash,
+                                      tx_update->current_oid.hash, GIT_MAX_RAWSZ);
+                               log->value.update.message =
+                                       xstrndup(u->msg, arg->refs->write_options.block_size / 2);
+                       }
                }
 
                if (u->flags & REF_LOG_ONLY)
                        continue;
 
-               if (u->flags & REF_HAVE_NEW && is_null_oid(&u->new_oid)) {
+               if (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_has_null_new_value(u)) {
                        struct reftable_ref_record ref = {
                                .refname = (char *)u->refname,
                                .update_index = ts,