]> git.ipfire.org Git - thirdparty/git.git/commitdiff
update-ref: add --batch-updates flag for stdin mode
authorKarthik Nayak <karthik.188@gmail.com>
Tue, 8 Apr 2025 08:51:12 +0000 (10:51 +0200)
committerJunio C Hamano <gitster@pobox.com>
Tue, 8 Apr 2025 14:59:49 +0000 (07:59 -0700)
When updating multiple references through stdin, Git's update-ref
command normally aborts the entire transaction if any single update
fails. This atomic behavior prevents partial updates. Introduce a new
batch update system, where the updates the performed together similar
but individual updates are allowed to fail.

Add a new `--batch-updates` flag that allows the transaction to continue
even when individual reference updates fail. This flag can only be used
in `--stdin` mode and builds upon the batch update support added to the
refs subsystem in the previous commits. When enabled, failed updates are
reported in the following format:

  rejected SP (<old-oid> | <old-target>) SP (<new-oid> | <new-target>) SP <rejection-reason> LF

Update the documentation to reflect this change and also tests to cover
different scenarios where an update could be rejected.

Signed-off-by: Karthik Nayak <karthik.188@gmail.com>
Acked-by: Patrick Steinhardt <ps@pks.im>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
Documentation/git-update-ref.adoc
builtin/update-ref.c
t/t1400-update-ref.sh

index 9e6935d38d031b4890135e0cce36fffcc349ac1d..9310ce9768320972c3a6547b18b920b3d7eff89d 100644 (file)
@@ -7,8 +7,10 @@ git-update-ref - Update the object name stored in a ref safely
 
 SYNOPSIS
 --------
-[verse]
-'git update-ref' [-m <reason>] [--no-deref] (-d <ref> [<old-oid>] | [--create-reflog] <ref> <new-oid> [<old-oid>] | --stdin [-z])
+[synopsis]
+git update-ref [-m <reason>] [--no-deref] -d <ref> [<old-oid>]
+git update-ref [-m <reason>] [--no-deref] [--create-reflog] <ref> <new-oid> [<old-oid>]
+git update-ref [-m <reason>] [--no-deref] --stdin [-z] [--batch-updates]
 
 DESCRIPTION
 -----------
@@ -57,6 +59,14 @@ performs all modifications together.  Specify commands of the form:
 With `--create-reflog`, update-ref will create a reflog for each ref
 even if one would not ordinarily be created.
 
+With `--batch-updates`, update-ref executes the updates in a batch but allows
+individual updates to fail due to invalid or incorrect user input, applying only
+the successful updates. However, system-related errors—such as I/O failures or
+memory issues—will result in a full failure of all batched updates. Any failed
+updates will be reported in the following format:
+
+       rejected SP (<old-oid> | <old-target>) SP (<new-oid> | <new-target>) SP <rejection-reason> LF
+
 Quote fields containing whitespace as if they were strings in C source
 code; i.e., surrounded by double-quotes and with backslash escapes.
 Use 40 "0" characters or the empty string to specify a zero value.  To
index 1d541e13adebe40b162858659040daecd7cc10dd..111d6473ad53f033f249dd676f3fd62a9b756ffb 100644 (file)
@@ -5,6 +5,7 @@
 #include "config.h"
 #include "gettext.h"
 #include "hash.h"
+#include "hex.h"
 #include "refs.h"
 #include "object-name.h"
 #include "parse-options.h"
@@ -13,7 +14,7 @@
 static const char * const git_update_ref_usage[] = {
        N_("git update-ref [<options>] -d <refname> [<old-oid>]"),
        N_("git update-ref [<options>]    <refname> <new-oid> [<old-oid>]"),
-       N_("git update-ref [<options>] --stdin [-z]"),
+       N_("git update-ref [<options>] --stdin [-z] [--batch-updates]"),
        NULL
 };
 
@@ -565,6 +566,49 @@ static void parse_cmd_abort(struct ref_transaction *transaction,
        report_ok("abort");
 }
 
+static void print_rejected_refs(const char *refname,
+                               const struct object_id *old_oid,
+                               const struct object_id *new_oid,
+                               const char *old_target,
+                               const char *new_target,
+                               enum ref_transaction_error err,
+                               void *cb_data UNUSED)
+{
+       struct strbuf sb = STRBUF_INIT;
+       const char *reason = "";
+
+       switch (err) {
+       case REF_TRANSACTION_ERROR_NAME_CONFLICT:
+               reason = "refname conflict";
+               break;
+       case REF_TRANSACTION_ERROR_CREATE_EXISTS:
+               reason = "reference already exists";
+               break;
+       case REF_TRANSACTION_ERROR_NONEXISTENT_REF:
+               reason = "reference does not exist";
+               break;
+       case REF_TRANSACTION_ERROR_INCORRECT_OLD_VALUE:
+               reason = "incorrect old value provided";
+               break;
+       case REF_TRANSACTION_ERROR_INVALID_NEW_VALUE:
+               reason = "invalid new value provided";
+               break;
+       case REF_TRANSACTION_ERROR_EXPECTED_SYMREF:
+               reason = "expected symref but found regular ref";
+               break;
+       default:
+               reason = "unkown failure";
+       }
+
+       strbuf_addf(&sb, "rejected %s %s %s %s\n", refname,
+                   new_oid ? oid_to_hex(new_oid) : new_target,
+                   old_oid ? oid_to_hex(old_oid) : old_target,
+                   reason);
+
+       fwrite(sb.buf, sb.len, 1, stdout);
+       strbuf_release(&sb);
+}
+
 static void parse_cmd_commit(struct ref_transaction *transaction,
                             const char *next, const char *end UNUSED)
 {
@@ -573,6 +617,10 @@ static void parse_cmd_commit(struct ref_transaction *transaction,
                die("commit: extra input: %s", next);
        if (ref_transaction_commit(transaction, &error))
                die("commit: %s", error.buf);
+
+       ref_transaction_for_each_rejected_update(transaction,
+                                                print_rejected_refs, NULL);
+
        report_ok("commit");
        ref_transaction_free(transaction);
 }
@@ -609,7 +657,7 @@ static const struct parse_cmd {
        { "commit",        parse_cmd_commit,        0, UPDATE_REFS_CLOSED },
 };
 
-static void update_refs_stdin(void)
+static void update_refs_stdin(unsigned int flags)
 {
        struct strbuf input = STRBUF_INIT, err = STRBUF_INIT;
        enum update_refs_state state = UPDATE_REFS_OPEN;
@@ -617,7 +665,7 @@ static void update_refs_stdin(void)
        int i, j;
 
        transaction = ref_store_transaction_begin(get_main_ref_store(the_repository),
-                                                 0, &err);
+                                                 flags, &err);
        if (!transaction)
                die("%s", err.buf);
 
@@ -685,7 +733,7 @@ static void update_refs_stdin(void)
                         */
                        state = cmd->state;
                        transaction = ref_store_transaction_begin(get_main_ref_store(the_repository),
-                                                                 0, &err);
+                                                                 flags, &err);
                        if (!transaction)
                                die("%s", err.buf);
 
@@ -701,6 +749,8 @@ static void update_refs_stdin(void)
                /* Commit by default if no transaction was requested. */
                if (ref_transaction_commit(transaction, &err))
                        die("%s", err.buf);
+               ref_transaction_for_each_rejected_update(transaction,
+                                                print_rejected_refs, NULL);
                ref_transaction_free(transaction);
                break;
        case UPDATE_REFS_STARTED:
@@ -727,6 +777,8 @@ int cmd_update_ref(int argc,
        struct object_id oid, oldoid;
        int delete = 0, no_deref = 0, read_stdin = 0, end_null = 0;
        int create_reflog = 0;
+       unsigned int flags = 0;
+
        struct option options[] = {
                OPT_STRING( 'm', NULL, &msg, N_("reason"), N_("reason of the update")),
                OPT_BOOL('d', NULL, &delete, N_("delete the reference")),
@@ -735,6 +787,8 @@ int cmd_update_ref(int argc,
                OPT_BOOL('z', NULL, &end_null, N_("stdin has NUL-terminated arguments")),
                OPT_BOOL( 0 , "stdin", &read_stdin, N_("read updates from stdin")),
                OPT_BOOL( 0 , "create-reflog", &create_reflog, N_("create a reflog")),
+               OPT_BIT('0', "batch-updates", &flags, N_("batch reference updates"),
+                       REF_TRANSACTION_ALLOW_FAILURE),
                OPT_END(),
        };
 
@@ -756,8 +810,10 @@ int cmd_update_ref(int argc,
                        usage_with_options(git_update_ref_usage, options);
                if (end_null)
                        line_termination = '\0';
-               update_refs_stdin();
+               update_refs_stdin(flags);
                return 0;
+       } else if (flags & REF_TRANSACTION_ALLOW_FAILURE) {
+               die("--batch-updates can only be used with --stdin");
        }
 
        if (end_null)
index 29045aad43906fce3f64fb82ee98fb5f80d4796b..d29d23cb8905f865e68da0e782c3cbe1948c6c3f 100755 (executable)
@@ -2066,6 +2066,239 @@ do
                grep "$(git rev-parse $a) $(git rev-parse $a)" actual
        '
 
+       test_expect_success "stdin $type batch-updates" '
+               git init repo &&
+               test_when_finished "rm -fr repo" &&
+               (
+                       cd repo &&
+                       test_commit commit &&
+                       head=$(git rev-parse HEAD) &&
+
+                       format_command $type "update refs/heads/ref1" "$head" "$Z" >stdin &&
+                       format_command $type "update refs/heads/ref2" "$head" "$Z" >>stdin &&
+                       git update-ref $type --stdin --batch-updates <stdin &&
+                       echo $head >expect &&
+                       git rev-parse refs/heads/ref1 >actual &&
+                       test_cmp expect actual &&
+                       git rev-parse refs/heads/ref2 >actual &&
+                       test_cmp expect actual
+               )
+       '
+
+       test_expect_success "stdin $type batch-updates with invalid new_oid" '
+               git init repo &&
+               test_when_finished "rm -fr repo" &&
+               (
+                       cd repo &&
+                       test_commit one &&
+                       old_head=$(git rev-parse HEAD) &&
+                       test_commit two &&
+                       head=$(git rev-parse HEAD) &&
+                       git update-ref refs/heads/ref1 $head &&
+                       git update-ref refs/heads/ref2 $head &&
+
+                       format_command $type "update refs/heads/ref1" "$old_head" "$head" >stdin &&
+                       format_command $type "update refs/heads/ref2" "$(test_oid 001)" "$head" >>stdin &&
+                       git update-ref $type --stdin --batch-updates <stdin >stdout &&
+                       echo $old_head >expect &&
+                       git rev-parse refs/heads/ref1 >actual &&
+                       test_cmp expect actual &&
+                       echo $head >expect &&
+                       git rev-parse refs/heads/ref2 >actual &&
+                       test_cmp expect actual &&
+                       test_grep -q "invalid new value provided" stdout
+               )
+       '
+
+       test_expect_success "stdin $type batch-updates with non-commit new_oid" '
+               git init repo &&
+               test_when_finished "rm -fr repo" &&
+               (
+                       cd repo &&
+                       test_commit one &&
+                       old_head=$(git rev-parse HEAD) &&
+                       test_commit two &&
+                       head=$(git rev-parse HEAD) &&
+                       head_tree=$(git rev-parse HEAD^{tree}) &&
+                       git update-ref refs/heads/ref1 $head &&
+                       git update-ref refs/heads/ref2 $head &&
+
+                       format_command $type "update refs/heads/ref1" "$old_head" "$head" >stdin &&
+                       format_command $type "update refs/heads/ref2" "$head_tree" "$head" >>stdin &&
+                       git update-ref $type --stdin --batch-updates <stdin >stdout &&
+                       echo $old_head >expect &&
+                       git rev-parse refs/heads/ref1 >actual &&
+                       test_cmp expect actual &&
+                       echo $head >expect &&
+                       git rev-parse refs/heads/ref2 >actual &&
+                       test_cmp expect actual &&
+                       test_grep -q "invalid new value provided" stdout
+               )
+       '
+
+       test_expect_success "stdin $type batch-updates with non-existent ref" '
+               git init repo &&
+               test_when_finished "rm -fr repo" &&
+               (
+                       cd repo &&
+                       test_commit one &&
+                       old_head=$(git rev-parse HEAD) &&
+                       test_commit two &&
+                       head=$(git rev-parse HEAD) &&
+                       git update-ref refs/heads/ref1 $head &&
+
+                       format_command $type "update refs/heads/ref1" "$old_head" "$head" >stdin &&
+                       format_command $type "update refs/heads/ref2" "$old_head" "$head" >>stdin &&
+                       git update-ref $type --stdin --batch-updates <stdin >stdout &&
+                       echo $old_head >expect &&
+                       git rev-parse refs/heads/ref1 >actual &&
+                       test_cmp expect actual &&
+                       test_must_fail git rev-parse refs/heads/ref2 &&
+                       test_grep -q "reference does not exist" stdout
+               )
+       '
+
+       test_expect_success "stdin $type batch-updates with dangling symref" '
+               git init repo &&
+               test_when_finished "rm -fr repo" &&
+               (
+                       cd repo &&
+                       test_commit one &&
+                       old_head=$(git rev-parse HEAD) &&
+                       test_commit two &&
+                       head=$(git rev-parse HEAD) &&
+                       git update-ref refs/heads/ref1 $head &&
+                       git symbolic-ref refs/heads/ref2 refs/heads/nonexistent &&
+
+                       format_command $type "update refs/heads/ref1" "$old_head" "$head" >stdin &&
+                       format_command $type "update refs/heads/ref2" "$old_head" "$head" >>stdin &&
+                       git update-ref $type --no-deref --stdin --batch-updates <stdin >stdout &&
+                       echo $old_head >expect &&
+                       git rev-parse refs/heads/ref1 >actual &&
+                       test_cmp expect actual &&
+                       echo $head >expect &&
+                       test_must_fail git rev-parse refs/heads/ref2 &&
+                       test_grep -q "reference does not exist" stdout
+               )
+       '
+
+       test_expect_success "stdin $type batch-updates with regular ref as symref" '
+               git init repo &&
+               test_when_finished "rm -fr repo" &&
+               (
+                       cd repo &&
+                       test_commit one &&
+                       old_head=$(git rev-parse HEAD) &&
+                       test_commit two &&
+                       head=$(git rev-parse HEAD) &&
+                       git update-ref refs/heads/ref1 $head &&
+                       git update-ref refs/heads/ref2 $head &&
+
+                       format_command $type "update refs/heads/ref1" "$old_head" "$head" >stdin &&
+                       format_command $type "symref-update refs/heads/ref2" "$old_head" "ref" "refs/heads/nonexistent" >>stdin &&
+                       git update-ref $type --no-deref --stdin --batch-updates <stdin >stdout &&
+                       echo $old_head >expect &&
+                       git rev-parse refs/heads/ref1 >actual &&
+                       test_cmp expect actual &&
+                       echo $head >expect &&
+                       echo $head >expect &&
+                       git rev-parse refs/heads/ref2 >actual &&
+                       test_cmp expect actual &&
+                       test_grep -q "expected symref but found regular ref" stdout
+               )
+       '
+
+       test_expect_success "stdin $type batch-updates with invalid old_oid" '
+               git init repo &&
+               test_when_finished "rm -fr repo" &&
+               (
+                       cd repo &&
+                       test_commit one &&
+                       old_head=$(git rev-parse HEAD) &&
+                       test_commit two &&
+                       head=$(git rev-parse HEAD) &&
+                       git update-ref refs/heads/ref1 $head &&
+                       git update-ref refs/heads/ref2 $head &&
+
+                       format_command $type "update refs/heads/ref1" "$old_head" "$head" >stdin &&
+                       format_command $type "update refs/heads/ref2" "$old_head" "$Z" >>stdin &&
+                       git update-ref $type --stdin --batch-updates <stdin >stdout &&
+                       echo $old_head >expect &&
+                       git rev-parse refs/heads/ref1 >actual &&
+                       test_cmp expect actual &&
+                       echo $head >expect &&
+                       git rev-parse refs/heads/ref2 >actual &&
+                       test_cmp expect actual &&
+                       test_grep -q "reference already exists" stdout
+               )
+       '
+
+       test_expect_success "stdin $type batch-updates with incorrect old oid" '
+               git init repo &&
+               test_when_finished "rm -fr repo" &&
+               (
+                       cd repo &&
+                       test_commit one &&
+                       old_head=$(git rev-parse HEAD) &&
+                       test_commit two &&
+                       head=$(git rev-parse HEAD) &&
+                       git update-ref refs/heads/ref1 $head &&
+                       git update-ref refs/heads/ref2 $head &&
+
+                       format_command $type "update refs/heads/ref1" "$old_head" "$head" >stdin &&
+                       format_command $type "update refs/heads/ref2" "$head" "$old_head" >>stdin &&
+                       git update-ref $type --stdin --batch-updates <stdin >stdout &&
+                       echo $old_head >expect &&
+                       git rev-parse refs/heads/ref1 >actual &&
+                       test_cmp expect actual &&
+                       echo $head >expect &&
+                       git rev-parse refs/heads/ref2 >actual &&
+                       test_cmp expect actual &&
+                       test_grep -q "incorrect old value provided" stdout
+               )
+       '
+
+       test_expect_success "stdin $type batch-updates refname conflict" '
+               git init repo &&
+               test_when_finished "rm -fr repo" &&
+               (
+                       cd repo &&
+                       test_commit one &&
+                       old_head=$(git rev-parse HEAD) &&
+                       test_commit two &&
+                       head=$(git rev-parse HEAD) &&
+                       git update-ref refs/heads/ref/foo $head &&
+
+                       format_command $type "update refs/heads/ref/foo" "$old_head" "$head" >stdin &&
+                       format_command $type "update refs/heads/ref" "$old_head" "" >>stdin &&
+                       git update-ref $type --stdin --batch-updates <stdin >stdout &&
+                       echo $old_head >expect &&
+                       git rev-parse refs/heads/ref/foo >actual &&
+                       test_cmp expect actual &&
+                       test_grep -q "refname conflict" stdout
+               )
+       '
+
+       test_expect_success "stdin $type batch-updates refname conflict new ref" '
+               git init repo &&
+               test_when_finished "rm -fr repo" &&
+               (
+                       cd repo &&
+                       test_commit one &&
+                       old_head=$(git rev-parse HEAD) &&
+                       test_commit two &&
+                       head=$(git rev-parse HEAD) &&
+                       git update-ref refs/heads/ref/foo $head &&
+
+                       format_command $type "update refs/heads/foo" "$old_head" "" >stdin &&
+                       format_command $type "update refs/heads/ref" "$old_head" "" >>stdin &&
+                       git update-ref $type --stdin --batch-updates <stdin >stdout &&
+                       echo $old_head >expect &&
+                       git rev-parse refs/heads/foo >actual &&
+                       test_cmp expect actual &&
+                       test_grep -q "refname conflict" stdout
+               )
+       '
 done
 
 test_expect_success 'update-ref should also create reflog for HEAD' '