[PATCH 1/2] replay: add --update-refs option
- From
Siddharth Asthana <siddharthasthana31@gmail.com>
- Date
- Sep 8, 2025, 04:36 UTC
- Message-ID
- <20250908043620.57848-2-siddharthasthana31@gmail.com>
- In-Reply-To
- <20250908043620.57848-1-siddharthasthana31@gmail.com>
Currently, git replay outputs "update" commands that need to be piped to `git update-ref --stdin`:
git replay --onto main topic1..topic2 | git update-ref --stdin
While this works, it requires users to run a two-command pipeline and doesn't provide atomic transaction guarantees by default.
This patch adds --update-refs option that performs ref updates directly using Git's ref transaction API instead of outputting update commands:
git replay --update-refs --onto main topic1..topic2
The implementation uses atomic transactions by default (all updates succeed or all fail) and supports an optional --batch flag for partial failure tolerance, similar to `git update-ref --stdin`.
The --update-refs option: - Uses ref_store_transaction_begin() with atomic mode by default - Supports --batch mode with REF_TRANSACTION_ALLOW_FAILURE flag - Works with all existing options: --onto, --advance, --contained - Follows the same patterns as builtin/update-ref.c - Works with bare repositories (important for server-side operations) - Produces no output on successful completion
Option validation ensures --update-refs cannot be used with the existing --update option, and --batch can only be used with --update-refs.
This particularly benefits server-side Git operations (like Gitaly) that need atomic ref updates, and users who want to avoid the two-command pipeline for performance or reliability reasons.
Signed-off-by: Siddharth Asthana <siddharthasthana31@gmail.com> --- builtin/replay.c | 134 +++++++++++++- t/meson.build | 1 + t/t3650-replay-basics.sh | 323 ++++++++++++++++++++++++++++++++++ t/t3651-replay-update-refs.sh | 273 ++++++++++++++++++++++++++++ 4 files changed, 722 insertions(+), 9 deletions(-) create mode 100755 t/t3651-replay-update-refs.sh
diff --git a/builtin/replay.c b/builtin/replay.c index 6172c8aacc..a33c9887cf 100644 --- a/builtin/replay.c +++ b/builtin/replay.c @@ -284,6 +284,37 @@ static struct commit *pick_regular_commit(struct repository *repo, return create_commit(repo, result->tree, pickme, replayed_base); } +static int update_ref_direct(struct repository *repo, const char *refname, + const struct object_id *new_oid, + const struct object_id *old_oid) +{ + const char *msg = "replay"; + return refs_update_ref(get_main_ref_store(repo), msg, refname, + new_oid, old_oid, 0, UPDATE_REFS_MSG_ON_ERR); +} + +static int add_ref_to_transaction(struct ref_transaction *transaction, + const char *refname, + const struct object_id *new_oid, + const struct object_id *old_oid, + struct strbuf *err) +{ + return ref_transaction_update(transaction, refname, new_oid, old_oid, + NULL, NULL, 0, "git replay", err); +} + +static void print_rejected_update(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) +{ + const char *reason = ref_transaction_error_msg(err); + warning(_("failed to update %s: %s"), refname, reason); +} + int cmd_replay(int argc, const char **argv, const char *prefix, @@ -294,6 +325,9 @@ int cmd_replay(int argc, struct commit *onto = NULL; const char *onto_name = NULL; int contained = 0; + int update_directly = 0; + int update_refs_flag = 0; + int batch_mode = 0; struct rev_info revs; struct commit *last_commit = NULL; @@ -302,12 +336,14 @@ int cmd_replay(int argc, struct merge_result result; struct strset *update_refs = NULL; kh_oid_map_t *replayed_commits; + struct ref_transaction *transaction = NULL; + struct strbuf transaction_err = STRBUF_INIT; int ret = 0; const char * const replay_usage[] = { N_("(EXPERIMENTAL!) git replay " "([--contained] --onto <newbase> | --advance <branch>) " - "<revision-range>..."), + "[--update | --update-refs [--batch]] <revision-range>..."), NULL }; struct option replay_options[] = { @@ -319,6 +355,12 @@ int cmd_replay(int argc, N_("replay onto given commit")), OPT_BOOL(0, "contained", &contained, N_("advance all branches contained in revision-range")), + OPT_BOOL(0, "update", &update_directly, + N_("update branches directly instead of outputting update commands")), + OPT_BOOL(0, "update-refs", &update_refs_flag, + N_("update branches using ref transactions")), + OPT_BOOL(0, "batch", &batch_mode, + N_("allow partial ref updates in batch mode")), OPT_END() }; @@ -333,6 +375,14 @@ int cmd_replay(int argc, if (advance_name_opt && contained) die(_("options '%s' and '%s' cannot be used together"), "--advance", "--contained"); + + if (update_directly && update_refs_flag) + die(_("options '%s' and '%s' cannot be used together"), + "--update", "--update-refs"); + + if (batch_mode && !update_refs_flag) + die(_("option '%s' can only be used with '%s'"), + "--batch", "--update-refs"); advance_name = xstrdup_or_null(advance_name_opt); repo_init_revisions(repo, &revs, prefix); @@ -389,6 +439,18 @@ int cmd_replay(int argc, determine_replay_mode(repo, &revs.cmdline, onto_name, &advance_name, &onto, &update_refs); + /* Initialize ref transaction if using --update-refs */ + if (update_refs_flag) { + unsigned int transaction_flags = batch_mode ? REF_TRANSACTION_ALLOW_FAILURE : 0; + transaction = ref_store_transaction_begin(get_main_ref_store(repo), + transaction_flags, + &transaction_err); + if (!transaction) { + ret = error(_("failed to begin ref transaction: %s"), transaction_err.buf); + goto cleanup; + } + } + if (!onto) /* FIXME: Should handle replaying down to root commit */ die("Replaying down to root commit is not supported yet!"); @@ -399,6 +461,7 @@ int cmd_replay(int argc, init_basic_merge_options(&merge_opt, repo); memset(&result, 0, sizeof(result)); + result.clean = 1; /* Assume clean until proven otherwise */ merge_opt.show_rename_progress = 0; last_commit = onto; replayed_commits = kh_init_oid_map(); @@ -434,10 +497,27 @@ int cmd_replay(int argc, if (decoration->type == DECORATION_REF_LOCAL && (contained || strset_contains(update_refs, decoration->name))) { - printf("update %s %s %s\n", - decoration->name, - oid_to_hex(&last_commit->object.oid), - oid_to_hex(&commit->object.oid)); + if (update_directly) { + if (update_ref_direct(repo, decoration->name, + &last_commit->object.oid, + &commit->object.oid) < 0) { + ret = -1; + goto cleanup; + } + } else if (transaction) { + if (add_ref_to_transaction(transaction, decoration->name, + &last_commit->object.oid, + &commit->object.oid, + &transaction_err) < 0) { + ret = error(_("failed to add ref update to transaction: %s"), transaction_err.buf); + goto cleanup; + } + } else { + printf("update %s %s %s\n", + decoration->name, + oid_to_hex(&last_commit->object.oid), + oid_to_hex(&commit->object.oid)); + } } decoration = decoration->next; } @@ -445,10 +525,43 @@ int cmd_replay(int argc, /* In --advance mode, advance the target ref */ if (result.clean == 1 && advance_name) { - printf("update %s %s %s\n", - advance_name, - oid_to_hex(&last_commit->object.oid), - oid_to_hex(&onto->object.oid)); + if (update_directly) { + if (update_ref_direct(repo, advance_name, + &last_commit->object.oid, + &onto->object.oid) < 0) { + ret = -1; + goto cleanup; + } + } else if (transaction) { + if (add_ref_to_transaction(transaction, advance_name, + &last_commit->object.oid, + &onto->object.oid, + &transaction_err) < 0) { + ret = error(_("failed to add ref update to transaction: %s"), transaction_err.buf); + goto cleanup; + } + } else { + printf("update %s %s %s\n", + advance_name, + oid_to_hex(&last_commit->object.oid), + oid_to_hex(&onto->object.oid)); + } + } + + /* Commit the ref transaction if we have one */ + if (transaction && result.clean == 1) { + if (ref_transaction_commit(transaction, &transaction_err)) { + if (batch_mode) { + /* Print failed updates in batch mode */ + warning(_("some ref updates failed: %s"), transaction_err.buf); + ref_transaction_for_each_rejected_update(transaction, + print_rejected_update, NULL); + } else { + /* In atomic mode, all updates failed */ + ret = error(_("failed to update refs: %s"), transaction_err.buf); + goto cleanup; + } + } } merge_finalize(&merge_opt, &result); @@ -460,6 +573,9 @@ int cmd_replay(int argc, ret = result.clean; cleanup: + if (transaction) + ref_transaction_free(transaction); + strbuf_release(&transaction_err); release_revisions(&revs); free(advance_name); diff --git a/t/meson.build b/t/meson.build index daf01fb5d0..966b9d1b1f 100644 --- a/t/meson.build +++ b/t/meson.build @@ -397,6 +397,7 @@ integration_tests = [ 't3601-rm-pathspec-file.sh', 't3602-rm-sparse-checkout.sh', 't3650-replay-basics.sh', + 't3651-replay-update-refs.sh', 't3700-add.sh', 't3701-add-interactive.sh', 't3702-add-edit.sh', diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh index 58b3759935..b5aac8c566 100755 --- a/t/t3650-replay-basics.sh +++ b/t/t3650-replay-basics.sh @@ -217,4 +217,327 @@ test_expect_success 'merge.directoryRenames=false' ' --onto rename-onto rename-onto..rename-from ' +test_expect_success 'using replay with --update to rebase a branch' ' + # Store original branch tips + git rev-parse topic2 >topic2.old && + + # Use --update to directly update the refs + git replay --update --onto main topic1..topic2 && + + # Verify the branch was actually updated + git rev-parse topic2 >topic2.new && + ! test_cmp topic2.old topic2.new && + + # Verify the history is correct + git log --format=%s topic2 >actual && + test_write_lines E D M L B A >expect && + test_cmp expect actual +' + +test_expect_success 'using replay with --update in advance mode' ' + # Reset topic2 first + git branch -f topic2 $(cat topic2.old) && + + # Store original main tip + git rev-parse main >main.old && + + # Use --update with --advance + git replay --update --advance main topic1..topic2 && + + # Verify main was updated + git rev-parse main >main.new && + ! test_cmp main.old main.new && + + # Verify the history is correct + git log --format=%s main >actual && + test_write_lines E D M L B A >expect && + test_cmp expect actual && + + # Reset main back + git branch -f main $(cat main.old) +' + +test_expect_success 'using replay with --update and --contained' ' + # Store original branch tips + git rev-parse topic1 >topic1.old && + git rev-parse topic3 >topic3.old && + + # Use --update with --contained + git replay --update --contained --onto main main..topic3 && + + # Verify both branches were updated + git rev-parse topic1 >topic1.new && + git rev-parse topic3 >topic3.new && + ! test_cmp topic1.old topic1.new && + ! test_cmp topic3.old topic3.new && + + # Reset branches back + git branch -f topic1 $(cat topic1.old) && + git branch -f topic3 $(cat topic3.old) +' + +test_expect_success 'replay with --update should not produce output when successful' ' + git replay --update --onto main topic1..topic2 >output && + test_must_be_empty output +' + +test_expect_success 'using replay with --update-refs to rebase a branch (atomic mode)' ' + # Store original branch tip + git rev-parse topic2 >topic2.old && + + # Use --update-refs to directly update refs with transactions + git replay --update-refs --onto main topic1..topic2 && + + # Verify the branch was actually updated + git rev-parse topic2 >topic2.new && + ! test_cmp topic2.old topic2.new && + + # Verify the history is correct + git log --format=%s topic2 >actual && + test_write_lines E D M L B A >expect && + test_cmp expect actual +' + +test_expect_success 'using replay with --update-refs in advance mode' ' + # Store original main tip + git rev-parse main >main.old && + + # Use --update-refs with --advance + git replay --update-refs --advance main topic1..topic2 && + + # Verify main was updated + git rev-parse main >main.new && + ! test_cmp main.old main.new && + + # Verify the history is correct + git log --format=%s main >actual && + test_write_lines E D M L B A >expect && + test_cmp expect actual +' + +test_expect_success 'using replay with --update-refs and --contained' ' + # Store original branch tips + git rev-parse topic1 >topic1.old && + git rev-parse topic3 >topic3.old && + + # Use --update-refs with --contained + git replay --update-refs --contained --onto main main..topic3 && + + # Verify both branches were updated + git rev-parse topic1 >topic1.new && + git rev-parse topic3 >topic3.new && + ! test_cmp topic1.old topic1.new && + ! test_cmp topic3.old topic3.new && + + # Reset branches back + git branch -f topic1 $(cat topic1.old) && + git branch -f topic3 $(cat topic3.old) +' + +test_expect_success 'replay with --update-refs should not produce output when successful' ' + git replay --update-refs --onto main topic1..topic2 >output && + test_must_be_empty output +' + +test_expect_success 'replay with --update-refs --batch should not produce output when successful' ' + git replay --update-refs --batch --onto main topic1..topic2 >output && + test_must_be_empty output +' + +test_expect_success 'replay fails when --update and --update-refs are used together' ' + test_must_fail git replay --update --update-refs --onto main topic1..topic2 2>error && + grep "cannot be used together" error +' + +test_expect_success 'replay fails when --batch is used without --update-refs' ' + test_must_fail git replay --batch --onto main topic1..topic2 2>error && + grep "can only be used with.*--update-refs" error +' + +# Edge cases and comprehensive testing for --update-refs + +test_expect_success 'setup for edge case tests' ' + # Create some additional branches for testing + git checkout -b edge1 main && + test_commit Edge1 && + git checkout -b edge2 main && + test_commit Edge2 && + git checkout main +' + +test_expect_success '--update-refs with conflicting replay (atomic mode fails completely)' ' + # Create a conflict scenario + git checkout -b conflict-test main && + echo "conflict content" > C.t && + git add C.t && + git commit -m "Conflicting change" && + + # Store original branch state + git rev-parse conflict-test >conflict-test.old && + + # This should fail due to conflict, and branch should remain unchanged + test_expect_code 1 git replay --update-refs --onto topic1 main..conflict-test && + + # Verify branch was not updated (atomic transaction rolled back) + git rev-parse conflict-test >conflict-test.new && + test_cmp conflict-test.old conflict-test.new +' + +test_expect_success '--update-refs --batch with conflicting replay (partial success)' ' + # Create scenario with one good commit and one conflicting commit + git checkout -b batch-test main && + test_commit GoodCommit && + echo "conflict" > C.t && + git add C.t && + git commit -m "Bad commit" && + + # Store original states + git rev-parse batch-test >batch-test.old && + + # Batch mode should handle partial failures gracefully + # Note: This test might need adjustment based on actual conflict behavior + test_expect_code 1 git replay --update-refs --batch --onto topic1 main..batch-test 2>batch-error && + + # In batch mode, we should get warnings rather than hard failures + test_path_is_file batch-error +' + +test_expect_success '--update-refs with no commits to replay (empty transaction)' ' + # Try to replay an empty range + git rev-parse topic1 >topic1.before && + + # This should succeed but do nothing + git replay --update-refs --onto main topic1..topic1 && + + # Branch should be unchanged + git rev-parse topic1 >topic1.after && + test_cmp topic1.before topic1.after +' + +test_expect_success '--update-refs with multiple branches (atomic success)' ' + # Store original states + git rev-parse edge1 >edge1.old && + git rev-parse edge2 >edge2.old && + + # Replay multiple branches atomically + git replay --update-refs --contained --onto main main..edge1 && + git replay --update-refs --contained --onto main main..edge2 && + + # Both should be updated + git rev-parse edge1 >edge1.new && + git rev-parse edge2 >edge2.new && + ! test_cmp edge1.old edge1.new && + ! test_cmp edge2.old edge2.new +' + +test_expect_success '--update-refs atomic vs batch behavior comparison' ' + # Create a branch for comparison + git checkout -b compare-test main && + test_commit CompareCommit && + + # Test atomic mode first + git replay --update-refs --onto main main..compare-test && + git rev-parse compare-test >atomic-result && + + # Reset and test batch mode + git branch -f compare-test main && + test_commit CompareCommit && + git replay --update-refs --batch --onto main main..compare-test && + git rev-parse compare-test >batch-result && + + # Results should be identical for successful cases + test_cmp atomic-result batch-result +' + +test_expect_success '--update-refs preserves ref transaction semantics' ' + # Create branch for testing + git checkout -b transaction-test main && + test_commit TransactionCommit && + + # Store original state + git rev-parse transaction-test >before-transaction && + + # Use --update-refs (should be atomic) + git replay --update-refs --onto main main..transaction-test && + + # Verify ref was updated + git rev-parse transaction-test >after-transaction && + ! test_cmp before-transaction after-transaction && + + # Verify commit history is correct + git log --format=%s transaction-test >actual-history && + test_write_lines TransactionCommit M L B A >expected-history && + test_cmp expected-history actual-history +' + +test_expect_success '--update-refs with --advance preserves branch history' ' + # Test that --advance with --update-refs works correctly + git checkout -b advance-test main && + test_commit AdvanceCommit && + + # Store original main state + git rev-parse main >main-before-advance && + + # Use --advance with --update-refs + git replay --update-refs --advance main main..advance-test && + + # Main should be updated + git rev-parse main >main-after-advance && + ! test_cmp main-before-advance main-after-advance && + + # Verify main has the right commits + git log --format=%s main >main-history && + test_write_lines AdvanceCommit M L B A >expected-main && + test_cmp expected-main main-history +' + +test_expect_success '--update-refs handles ref updates consistently with traditional method' ' + # Create test scenario + git checkout -b consistency-test main && + test_commit ConsistencyTest && + + # Method 1: Traditional output piped to update-ref + git checkout -b trad-test consistency-test && + git replay --onto main main..consistency-test >update-commands && + git update-ref --stdin <update-commands && + git rev-parse trad-test >traditional-result && + + # Method 2: Direct --update-refs + git branch -f consistency-test main && + test_commit ConsistencyTest && + git checkout -b direct-test consistency-test && + git replay --update-refs --onto main main..consistency-test && + git rev-parse direct-test >direct-result && + + # Results should be identical + test_cmp traditional-result direct-result +' + +test_expect_success '--update-refs error messages are helpful' ' + # Test that error messages are clear and helpful + git checkout -b error-test main && + test_commit ErrorTest && + + # Test conflicting options + test_must_fail git replay --update --update-refs --onto main main..error-test 2>conflict-error && + grep "cannot be used together" conflict-error && + + # Test batch without update-refs + test_must_fail git replay --batch --onto main main..error-test 2>batch-error && + grep "can only be used with" batch-error +' + +test_expect_success '--update-refs with bare repository works correctly' ' + # Test that --update-refs works in bare repositories (important for Gitaly) + git checkout -b bare-test main && + test_commit BareTest && + + # Test with bare repo (using existing bare setup) + git -C bare replay --update-refs --onto main main..bare-test && + + # Verify the bare repo was updated correctly + git -C bare rev-parse bare-test >bare-result && + test -s bare-result +' + test_done diff --git a/t/t3651-replay-update-refs.sh b/t/t3651-replay-update-refs.sh new file mode 100755 index 0000000000..fcd4d36721 --- /dev/null +++ b/t/t3651-replay-update-refs.sh @@ -0,0 +1,273 @@ +#!/bin/sh + +test_description='git replay --update-refs edge cases and comprehensive testing' + +GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME=main +export GIT_TEST_DEFAULT_INITIAL_BRANCH_NAME + +. ./test-lib.sh + +GIT_AUTHOR_NAME=author@name +GIT_AUTHOR_EMAIL=bogus@email@address +export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL + +test_expect_success 'setup for update-refs tests' ' + test_commit A && + test_commit B && + + git switch -c topic1 && + test_commit C && + git switch -c topic2 && + test_commit D && + test_commit E && + git switch topic1 && + test_commit F && + + git switch main && + test_commit L && + test_commit M && + + git switch -c conflict B && + test_commit C.conflict C.t conflict +' + +test_expect_success 'setup bare repo' ' + git clone --bare . bare +' + +# Basic functionality tests + +test_expect_success '--update-refs works in atomic mode (basic)' ' + # Store original branch tip + git rev-parse topic2 >topic2.old && + + # Use --update-refs to directly update refs with transactions + git replay --update-refs --onto main topic1..topic2 && + + # Verify the branch was actually updated + git rev-parse topic2 >topic2.new && + ! test_cmp topic2.old topic2.new && + + # Verify the history is correct + git log --format=%s topic2 >actual && + test_write_lines E D M L B A >expect && + test_cmp expect actual +' + +test_expect_success '--update-refs works with --advance' ' + # Store original main tip + git rev-parse main >main.old && + + # Use --update-refs with --advance + git replay --update-refs --advance main topic1..topic2 && + + # Verify main was updated + git rev-parse main >main.new && + ! test_cmp main.old main.new && + + # Verify the history is correct + git log --format=%s main >actual && + test_write_lines E D M L B A >expect && + test_cmp expect actual +' + +test_expect_success '--update-refs produces no output on success' ' + git checkout -b quiet-test topic1 && + git replay --update-refs --onto main topic1..quiet-test >output && + test_must_be_empty output +' + +test_expect_success '--update-refs --batch produces no output on success' ' + git checkout -b batch-quiet-test topic1 && + git replay --update-refs --batch --onto main topic1..batch-quiet-test >output && + test_must_be_empty output +' + +# Edge case tests + +test_expect_success '--update-refs with empty range (no-op)' ' + # Store original branch tip + git rev-parse topic1 >topic1.before && + + # Try to replay an empty range - should succeed but do nothing + git replay --update-refs --onto main topic1..topic1 && + + # Branch should be unchanged + git rev-parse topic1 >topic1.after && + test_cmp topic1.before topic1.after +' + +test_expect_success '--update-refs atomic vs batch mode comparison' ' + # Create branch for comparison + git checkout -b compare1 topic1 && + test_commit Compare1 && + git checkout -b compare2 topic1 && + test_commit Compare2 && + + # Test atomic mode + git replay --update-refs --onto main topic1..compare1 && + git rev-parse compare1 >atomic-result && + + # Test batch mode - should give same result for successful case + git replay --update-refs --batch --onto main topic1..compare2 && + git rev-parse compare2 >batch-result && + + # The OIDs will be different since commits are different, + # but both should have been updated (not equal to original) + git rev-parse topic1 >original && + ! test_cmp atomic-result original && + ! test_cmp batch-result original +' + +test_expect_success '--update-refs handles conflict gracefully in atomic mode' ' + # Create a branch that will conflict + git checkout -b atomic-conflict B && + echo "different content" >C.t && + git add C.t && + git commit -m "Conflicting C" && + + # Store original state + git rev-parse atomic-conflict >conflict-before && + + # This should fail due to conflict + test_expect_code 1 git replay --update-refs --onto conflict atomic-conflict^..atomic-conflict && + + # In atomic mode, branch should remain unchanged + git rev-parse atomic-conflict >conflict-after && + test_cmp conflict-before conflict-after +' + +test_expect_success '--update-refs preserves transaction semantics' ' + # Create test branch + git checkout -b transaction-test topic1 && + test_commit TransactionTest && + + # Store original state + git rev-parse transaction-test >before-transaction && + + # Use --update-refs (should be atomic) + git replay --update-refs --onto main topic1..transaction-test && + + # Verify ref was updated + git rev-parse transaction-test >after-transaction && + ! test_cmp before-transaction after-transaction && + + # Verify commit history is preserved correctly + git log --format=%s transaction-test >actual-history && + test_write_lines TransactionTest M L B A >expected-history && + test_cmp expected-history actual-history +' + +test_expect_success '--update-refs vs traditional method equivalence' ' + # Create test branches + git checkout -b traditional topic1 && + test_commit Traditional && + git checkout -b direct topic1 && + test_commit Direct && + + # Method 1: Traditional output + update-ref + git replay --onto main topic1..traditional >update-commands && + git update-ref --stdin <update-commands && + git rev-parse traditional >traditional-result && + + # Method 2: Direct --update-refs + git replay --update-refs --onto main topic1..direct && + git rev-parse direct >direct-result && + + # Both methods should produce equivalent results + # (OIDs will be different due to different commits, but both should be updated) + git rev-parse topic1 >original && + ! test_cmp traditional-result original && + ! test_cmp direct-result original +' + +# Error handling and validation tests + +test_expect_success 'error messages are helpful and clear' ' + # Test conflicting options + test_must_fail git replay --update --update-refs --onto main topic1..topic2 2>error1 && + grep "cannot be used together" error1 && + + # Test batch without update-refs + test_must_fail git replay --batch --onto main topic1..topic2 2>error2 && + grep "can only be used with.*--update-refs" error2 +' + +test_expect_success '--update-refs works correctly with bare repositories' ' + # Create branch for bare repo testing + git checkout -b bare-test topic1 && + test_commit BareTest && + + # Test with bare repo (important for Gitaly use case) + git -C bare fetch .. bare-test:bare-test && + git -C bare replay --update-refs --onto main topic1..bare-test && + + # Verify the bare repo was updated correctly + git -C bare rev-parse bare-test >bare-result && + test -s bare-result && + + # Verify it is different from original + git rev-parse topic1 >original && + ! test_cmp bare-result original +' + +test_expect_success '--update-refs maintains ref update ordering' ' + # Create multiple branches to test ordering + git checkout -b order1 topic1 && + test_commit Order1 && + git checkout -b order2 topic1 && + test_commit Order2 && + + # Store original states + git rev-parse order1 >order1-before && + git rev-parse order2 >order2-before && + + # Update both branches + git replay --update-refs --onto main topic1..order1 && + git replay --update-refs --onto main topic1..order2 && + + # Verify both were updated + git rev-parse order1 >order1-after && + git rev-parse order2 >order2-after && + ! test_cmp order1-before order1-after && + ! test_cmp order2-before order2-after +' + +test_expect_success '--update-refs handles ref transaction cleanup properly' ' + # This test ensures no ref transaction leaks occur + git checkout -b cleanup-test topic1 && + test_commit CleanupTest && + + # Run multiple operations to test cleanup + git replay --update-refs --onto main topic1..cleanup-test && + git replay --update-refs --batch --onto main topic1..cleanup-test && + + # If cleanup is working properly, these should succeed without errors + test_path_is_file .git/refs/heads/cleanup-test +' + +# Performance and stress tests + +test_expect_success '--update-refs performance is reasonable' ' + # Create several commits to test performance + git checkout -b perf-test topic1 && + for i in 1 2 3 4 5; do + test_commit "Perf$i" || return 1 + done && + + # Time the traditional method + time git replay --onto main topic1..perf-test >perf-commands && + time git update-ref --stdin <perf-commands && + + # Reset and time the new method + git branch -f perf-test topic1 && + for i in 1 2 3 4 5; do + test_commit "Perf$i" || return 1 + done && + time git replay --update-refs --onto main topic1..perf-test && + + # Test completed successfully if we got here + true +' + +test_done \ No newline at end of file
-- 2.51.0