diff --git a/Documentation/git-replay.adoc b/Documentation/git-replay.adoc index 997097e420..a32f72aead 100644 --- a/Documentation/git-replay.adoc +++ b/Documentation/git-replay.adoc @@ -9,7 +9,8 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t SYNOPSIS -------- [verse] -(EXPERIMENTAL!) 'git replay' ([--contained] --onto | --advance | --revert ) [--ref-action[=]] +(EXPERIMENTAL!) 'git replay' ([--contained] --onto= | --advance= | --revert=) + [--ref=] [--ref-action=] DESCRIPTION ----------- @@ -26,7 +27,7 @@ THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE. OPTIONS ------- ---onto :: +--onto=:: Starting point at which to create the new commits. May be any valid commit, and not just an existing branch name. + @@ -34,7 +35,7 @@ When `--onto` is specified, the branch(es) in the revision range will be updated to point at the new commits, similar to the way `git rebase --update-refs` updates multiple branches in the affected range. ---advance :: +--advance=:: Starting point at which to create the new commits; must be a branch name. + @@ -42,7 +43,7 @@ The history is replayed on top of the and is updated to point at the tip of the resulting history. This is different from `--onto`, which uses the target only as a starting point without updating it. ---revert :: +--revert=:: Starting point at which to create the reverted commits; must be a branch name. + @@ -65,6 +66,16 @@ incompatible with `--contained` (which is a modifier for `--onto` only). Update all branches that point at commits in . Requires `--onto`. +--ref=:: + Override which reference is updated with the result of the replay. + The ref must be fully qualified. + When used with `--onto`, the `` should have a + single tip and only the specified reference is updated instead of + inferring refs from the revision range. + When used with `--advance` or `--revert`, the specified reference is + updated instead of the branch given to those options. + This option is incompatible with `--contained`. + --ref-action[=]:: Control how references are updated. The mode can be: + @@ -79,8 +90,8 @@ The default mode can be configured via the `replay.refAction` configuration vari :: Range of commits to replay; see "Specifying Ranges" in - linkgit:git-rev-parse[1]. In `--advance ` or - `--revert ` mode, the range should have a single tip, + linkgit:git-rev-parse[1]. In `--advance=` or + `--revert=` mode, the range should have a single tip, so that it's clear to which tip the advanced or reverted should point. Any commits in the range whose changes are already present in the branch the commits are being @@ -127,7 +138,7 @@ EXAMPLES To simply rebase `mybranch` onto `target`: ------------ -$ git replay --onto target origin/main..mybranch +$ git replay --onto=target origin/main..mybranch ------------ The refs are updated atomically and no output is produced on success. @@ -135,14 +146,14 @@ The refs are updated atomically and no output is produced on success. To see what would be updated without actually updating: ------------ -$ git replay --ref-action=print --onto target origin/main..mybranch +$ git replay --ref-action=print --onto=target origin/main..mybranch update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH} ------------ To cherry-pick the commits from mybranch onto target: ------------ -$ git replay --advance target origin/main..mybranch +$ git replay --advance=target origin/main..mybranch ------------ Note that the first two examples replay the exact same commits and on @@ -154,7 +165,7 @@ What if you have a stack of branches, one depending upon another, and you'd really like to rebase the whole set? ------------ -$ git replay --contained --onto origin/main origin/main..tipbranch +$ git replay --contained --onto=origin/main origin/main..tipbranch ------------ All three branches (`branch1`, `branch2`, and `tipbranch`) are updated @@ -165,7 +176,7 @@ commits to replay using the syntax `A..B`; any range expression will do: ------------ -$ git replay --onto origin/main ^base branch1 branch2 branch3 +$ git replay --onto=origin/main ^base branch1 branch2 branch3 ------------ This will simultaneously rebase `branch1`, `branch2`, and `branch3`, @@ -176,7 +187,7 @@ that they have in common, but that does not need to be the case. To revert commits on a branch: ------------ -$ git replay --revert main topic~2..topic +$ git replay --revert=main topic~2..topic ------------ This reverts the last two commits from `topic`, creating revert commits on @@ -188,6 +199,16 @@ NOTE: For reverting an entire merge request as a single commit (rather than commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE` which can avoid unnecessary merge conflicts. +To replay onto a specific commit while updating a different reference: + +------------ +$ git replay --onto=112233 --ref=refs/heads/mybranch aabbcc..ddeeff +------------ + +This replays the range `aabbcc..ddeeff` onto commit `112233` and updates +`refs/heads/mybranch` to point at the result. This can be useful when you want +to use bare commit IDs instead of branch names. + GIT --- Part of the linkgit:git[1] suite diff --git a/builtin/replay.c b/builtin/replay.c index a0879b020f..39e3a86f6c 100644 --- a/builtin/replay.c +++ b/builtin/replay.c @@ -84,25 +84,33 @@ int cmd_replay(int argc, const char *const replay_usage[] = { N_("(EXPERIMENTAL!) git replay " - "([--contained] --onto | --advance | --revert ) " - "[--ref-action[=]] "), + "([--contained] --onto= | --advance= | --revert=)\n" + "[--ref=] [--ref-action=] "), NULL }; struct option replay_options[] = { - OPT_STRING(0, "advance", &opts.advance, - N_("branch"), - N_("make replay advance given branch")), - OPT_STRING(0, "onto", &opts.onto, - N_("revision"), - N_("replay onto given commit")), OPT_BOOL(0, "contained", &opts.contained, N_("update all branches that point at commits in ")), - OPT_STRING(0, "revert", &opts.revert, - N_("branch"), - N_("revert commits onto given branch")), - OPT_STRING(0, "ref-action", &ref_action, - N_("mode"), - N_("control ref update behavior (update|print)")), + OPT_STRING_F(0, "onto", &opts.onto, + N_("revision"), + N_("replay onto given commit"), + PARSE_OPT_NONEG), + OPT_STRING_F(0, "advance", &opts.advance, + N_("branch"), + N_("make replay advance given branch"), + PARSE_OPT_NONEG), + OPT_STRING_F(0, "revert", &opts.revert, + N_("branch"), + N_("revert commits onto given branch"), + PARSE_OPT_NONEG), + OPT_STRING_F(0, "ref", &opts.ref, + N_("branch"), + N_("reference to update with result"), + PARSE_OPT_NONEG), + OPT_STRING_F(0, "ref-action", &ref_action, + N_("mode"), + N_("control ref update behavior (update|print)"), + PARSE_OPT_NONEG), OPT_END() }; @@ -122,6 +130,8 @@ int cmd_replay(int argc, opts.contained, "--contained"); die_for_incompatible_opt2(!!opts.revert, "--revert", opts.contained, "--contained"); + die_for_incompatible_opt2(!!opts.ref, "--ref", + !!opts.contained, "--contained"); /* Parse ref action mode from command line or config */ ref_mode = get_ref_action_mode(repo, ref_action); diff --git a/replay.c b/replay.c index cf1f0bcba0..f96f1f6551 100644 --- a/replay.c +++ b/replay.c @@ -358,13 +358,15 @@ int replay_revisions(struct rev_info *revs, struct commit *last_commit = NULL; struct commit *commit; struct commit *onto = NULL; - struct merge_options merge_opt; + struct merge_options merge_opt = { 0 }; struct merge_result result = { .clean = 1, }; bool detached_head; char *advance; char *revert; + const char *ref; + struct object_id old_oid; enum replay_mode mode = REPLAY_MODE_PICK; int ret; @@ -375,6 +377,27 @@ int replay_revisions(struct rev_info *revs, set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto, &detached_head, &advance, &revert, &onto, &update_refs); + if (opts->ref) { + struct object_id oid; + + if (update_refs && strset_get_size(update_refs) > 1) { + ret = error(_("'--ref' cannot be used with multiple revision ranges")); + goto out; + } + if (check_refname_format(opts->ref, 0) || !starts_with(opts->ref, "refs/")) { + ret = error(_("'%s' is not a valid refname"), opts->ref); + goto out; + } + ref = opts->ref; + if (!refs_read_ref(get_main_ref_store(revs->repo), opts->ref, &oid)) + oidcpy(&old_oid, &oid); + else + oidclr(&old_oid, revs->repo->hash_algo); + } else { + ref = advance ? advance : revert; + oidcpy(&old_oid, &onto->object.oid); + } + if (prepare_revision_walk(revs) < 0) { ret = error(_("error preparing revisions")); goto out; @@ -406,7 +429,7 @@ int replay_revisions(struct rev_info *revs, kh_value(replayed_commits, pos) = last_commit; /* Update any necessary branches */ - if (advance || revert) + if (ref) continue; for (decoration = get_name_decoration(&commit->object); @@ -440,13 +463,9 @@ int replay_revisions(struct rev_info *revs, goto out; } - /* In --advance or --revert mode, update the target ref */ - if (advance || revert) { - const char *ref = advance ? advance : revert; - replay_result_queue_update(out, ref, - &onto->object.oid, + if (ref) + replay_result_queue_update(out, ref, &old_oid, &last_commit->object.oid); - } ret = 0; diff --git a/replay.h b/replay.h index e916a5f975..0ab74b9805 100644 --- a/replay.h +++ b/replay.h @@ -24,6 +24,13 @@ struct replay_revisions_options { */ const char *onto; + /* + * Reference to update with the result of the replay. This will not + * update any refs from `onto`, `advance`, or `revert`. Ignores + * `contained`. + */ + const char *ref; + /* * Starting point at which to create revert commits; must be a branch * name. The branch will be updated to point to the revert commits. diff --git a/t/t3650-replay-basics.sh b/t/t3650-replay-basics.sh index 89819ad4d2..3353bc4a4d 100755 --- a/t/t3650-replay-basics.sh +++ b/t/t3650-replay-basics.sh @@ -499,4 +499,70 @@ test_expect_success 'git replay --revert incompatible with --advance' ' test_grep "cannot be used together" error ' +test_expect_success 'using --onto with --ref' ' + git branch test-ref-onto topic2 && + test_when_finished "git branch -D test-ref-onto" && + + git replay --ref-action=print --onto=main --ref=refs/heads/test-ref-onto topic1..topic2 >result && + + test_line_count = 1 result && + test_grep "^update refs/heads/test-ref-onto " result && + + git log --format=%s $(cut -f 3 -d " " result) >actual && + test_write_lines E D M L B A >expect && + test_cmp expect actual +' + +test_expect_success 'using --advance with --ref' ' + git branch test-ref-advance main && + git branch test-ref-target main && + test_when_finished "git branch -D test-ref-advance test-ref-target" && + + git replay --ref-action=print --advance=test-ref-advance --ref=refs/heads/test-ref-target topic1..topic2 >result && + + test_line_count = 1 result && + test_grep "^update refs/heads/test-ref-target " result +' + +test_expect_success 'using --revert with --ref' ' + git branch test-ref-revert topic4 && + git branch test-ref-revert-target topic4 && + test_when_finished "git branch -D test-ref-revert test-ref-revert-target" && + + git replay --ref-action=print --revert=test-ref-revert --ref=refs/heads/test-ref-revert-target topic4~1..topic4 >result && + + test_line_count = 1 result && + test_grep "^update refs/heads/test-ref-revert-target " result +' + +test_expect_success '--ref is incompatible with --contained' ' + test_must_fail git replay --onto=main --ref=refs/heads/main --contained topic1..topic2 2>err && + test_grep "cannot be used together" err +' + +test_expect_success '--ref with nonexistent fully-qualified ref' ' + test_when_finished "git update-ref -d refs/heads/new-branch" && + + git replay --onto=main --ref=refs/heads/new-branch topic1..topic2 && + + git log --format=%s -2 new-branch >actual && + test_write_lines E D >expect && + test_cmp expect actual +' + +test_expect_success '--ref must be a valid refname' ' + test_must_fail git replay --onto=main --ref="refs/heads/bad..ref" topic1..topic2 2>err && + test_grep "is not a valid refname" err +' + +test_expect_success '--ref requires fully qualified ref' ' + test_must_fail git replay --onto=main --ref=main topic1..topic2 2>err && + test_grep "is not a valid refname" err +' + +test_expect_success '--onto with --ref rejects multiple revision ranges' ' + test_must_fail git replay --onto=main --ref=refs/heads/topic2 ^topic1 topic2 topic4 2>err && + test_grep "cannot be used with multiple revision ranges" err +' + test_done