Merge branch 'tc/replay-ref'
The experimental `git replay` command learned the `--ref=<ref>` option to allow specifying which ref to update, overriding the default behavior. * tc/replay-ref: replay: allow to specify a ref with option --ref replay: use stuck form in documentation and help message builtin/replay: mark options as not negatablemaint
commit
37a4780f2c
|
|
@ -9,7 +9,8 @@ git-replay - EXPERIMENTAL: Replay commits on a new base, works with bare repos t
|
||||||
SYNOPSIS
|
SYNOPSIS
|
||||||
--------
|
--------
|
||||||
[verse]
|
[verse]
|
||||||
(EXPERIMENTAL!) 'git replay' ([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) [--ref-action[=<mode>]] <revision-range>
|
(EXPERIMENTAL!) 'git replay' ([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)
|
||||||
|
[--ref=<ref>] [--ref-action=<mode>] <revision-range>
|
||||||
|
|
||||||
DESCRIPTION
|
DESCRIPTION
|
||||||
-----------
|
-----------
|
||||||
|
|
@ -26,7 +27,7 @@ THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
|
||||||
OPTIONS
|
OPTIONS
|
||||||
-------
|
-------
|
||||||
|
|
||||||
--onto <newbase>::
|
--onto=<newbase>::
|
||||||
Starting point at which to create the new commits. May be any
|
Starting point at which to create the new commits. May be any
|
||||||
valid commit, and not just an existing branch name.
|
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`
|
updated to point at the new commits, similar to the way `git rebase --update-refs`
|
||||||
updates multiple branches in the affected range.
|
updates multiple branches in the affected range.
|
||||||
|
|
||||||
--advance <branch>::
|
--advance=<branch>::
|
||||||
Starting point at which to create the new commits; must be a
|
Starting point at which to create the new commits; must be a
|
||||||
branch name.
|
branch name.
|
||||||
+
|
+
|
||||||
|
|
@ -42,7 +43,7 @@ The history is replayed on top of the <branch> and <branch> is updated to
|
||||||
point at the tip of the resulting history. This is different from `--onto`,
|
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.
|
which uses the target only as a starting point without updating it.
|
||||||
|
|
||||||
--revert <branch>::
|
--revert=<branch>::
|
||||||
Starting point at which to create the reverted commits; must be a
|
Starting point at which to create the reverted commits; must be a
|
||||||
branch name.
|
branch name.
|
||||||
+
|
+
|
||||||
|
|
@ -65,6 +66,16 @@ incompatible with `--contained` (which is a modifier for `--onto` only).
|
||||||
Update all branches that point at commits in
|
Update all branches that point at commits in
|
||||||
<revision-range>. Requires `--onto`.
|
<revision-range>. Requires `--onto`.
|
||||||
|
|
||||||
|
--ref=<ref>::
|
||||||
|
Override which reference is updated with the result of the replay.
|
||||||
|
The ref must be fully qualified.
|
||||||
|
When used with `--onto`, the `<revision-range>` 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[=<mode>]::
|
--ref-action[=<mode>]::
|
||||||
Control how references are updated. The mode can be:
|
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
|
||||||
|
|
||||||
<revision-range>::
|
<revision-range>::
|
||||||
Range of commits to replay; see "Specifying Ranges" in
|
Range of commits to replay; see "Specifying Ranges" in
|
||||||
linkgit:git-rev-parse[1]. In `--advance <branch>` or
|
linkgit:git-rev-parse[1]. In `--advance=<branch>` or
|
||||||
`--revert <branch>` mode, the range should have a single tip,
|
`--revert=<branch>` mode, the range should have a single tip,
|
||||||
so that it's clear to which tip the advanced or reverted
|
so that it's clear to which tip the advanced or reverted
|
||||||
<branch> should point. Any commits in the range whose changes
|
<branch> should point. Any commits in the range whose changes
|
||||||
are already present in the branch the commits are being
|
are already present in the branch the commits are being
|
||||||
|
|
@ -127,7 +138,7 @@ EXAMPLES
|
||||||
To simply rebase `mybranch` onto `target`:
|
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.
|
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:
|
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}
|
update refs/heads/mybranch ${NEW_mybranch_HASH} ${OLD_mybranch_HASH}
|
||||||
------------
|
------------
|
||||||
|
|
||||||
To cherry-pick the commits from mybranch onto target:
|
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
|
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?
|
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
|
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:
|
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`,
|
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:
|
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
|
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`
|
commit-by-commit), consider using `git merge-tree --merge-base $TIP HEAD $BASE`
|
||||||
which can avoid unnecessary merge conflicts.
|
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
|
GIT
|
||||||
---
|
---
|
||||||
Part of the linkgit:git[1] suite
|
Part of the linkgit:git[1] suite
|
||||||
|
|
|
||||||
|
|
@ -84,25 +84,33 @@ int cmd_replay(int argc,
|
||||||
|
|
||||||
const char *const replay_usage[] = {
|
const char *const replay_usage[] = {
|
||||||
N_("(EXPERIMENTAL!) git replay "
|
N_("(EXPERIMENTAL!) git replay "
|
||||||
"([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) "
|
"([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)\n"
|
||||||
"[--ref-action[=<mode>]] <revision-range>"),
|
"[--ref=<ref>] [--ref-action=<mode>] <revision-range>"),
|
||||||
NULL
|
NULL
|
||||||
};
|
};
|
||||||
struct option replay_options[] = {
|
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,
|
OPT_BOOL(0, "contained", &opts.contained,
|
||||||
N_("update all branches that point at commits in <revision-range>")),
|
N_("update all branches that point at commits in <revision-range>")),
|
||||||
OPT_STRING(0, "revert", &opts.revert,
|
OPT_STRING_F(0, "onto", &opts.onto,
|
||||||
N_("branch"),
|
N_("revision"),
|
||||||
N_("revert commits onto given branch")),
|
N_("replay onto given commit"),
|
||||||
OPT_STRING(0, "ref-action", &ref_action,
|
PARSE_OPT_NONEG),
|
||||||
N_("mode"),
|
OPT_STRING_F(0, "advance", &opts.advance,
|
||||||
N_("control ref update behavior (update|print)")),
|
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()
|
OPT_END()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -122,6 +130,8 @@ int cmd_replay(int argc,
|
||||||
opts.contained, "--contained");
|
opts.contained, "--contained");
|
||||||
die_for_incompatible_opt2(!!opts.revert, "--revert",
|
die_for_incompatible_opt2(!!opts.revert, "--revert",
|
||||||
opts.contained, "--contained");
|
opts.contained, "--contained");
|
||||||
|
die_for_incompatible_opt2(!!opts.ref, "--ref",
|
||||||
|
!!opts.contained, "--contained");
|
||||||
|
|
||||||
/* Parse ref action mode from command line or config */
|
/* Parse ref action mode from command line or config */
|
||||||
ref_mode = get_ref_action_mode(repo, ref_action);
|
ref_mode = get_ref_action_mode(repo, ref_action);
|
||||||
|
|
|
||||||
35
replay.c
35
replay.c
|
|
@ -358,13 +358,15 @@ int replay_revisions(struct rev_info *revs,
|
||||||
struct commit *last_commit = NULL;
|
struct commit *last_commit = NULL;
|
||||||
struct commit *commit;
|
struct commit *commit;
|
||||||
struct commit *onto = NULL;
|
struct commit *onto = NULL;
|
||||||
struct merge_options merge_opt;
|
struct merge_options merge_opt = { 0 };
|
||||||
struct merge_result result = {
|
struct merge_result result = {
|
||||||
.clean = 1,
|
.clean = 1,
|
||||||
};
|
};
|
||||||
bool detached_head;
|
bool detached_head;
|
||||||
char *advance;
|
char *advance;
|
||||||
char *revert;
|
char *revert;
|
||||||
|
const char *ref;
|
||||||
|
struct object_id old_oid;
|
||||||
enum replay_mode mode = REPLAY_MODE_PICK;
|
enum replay_mode mode = REPLAY_MODE_PICK;
|
||||||
int ret;
|
int ret;
|
||||||
|
|
||||||
|
|
@ -375,6 +377,27 @@ int replay_revisions(struct rev_info *revs,
|
||||||
set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto,
|
set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto,
|
||||||
&detached_head, &advance, &revert, &onto, &update_refs);
|
&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) {
|
if (prepare_revision_walk(revs) < 0) {
|
||||||
ret = error(_("error preparing revisions"));
|
ret = error(_("error preparing revisions"));
|
||||||
goto out;
|
goto out;
|
||||||
|
|
@ -406,7 +429,7 @@ int replay_revisions(struct rev_info *revs,
|
||||||
kh_value(replayed_commits, pos) = last_commit;
|
kh_value(replayed_commits, pos) = last_commit;
|
||||||
|
|
||||||
/* Update any necessary branches */
|
/* Update any necessary branches */
|
||||||
if (advance || revert)
|
if (ref)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
for (decoration = get_name_decoration(&commit->object);
|
for (decoration = get_name_decoration(&commit->object);
|
||||||
|
|
@ -440,13 +463,9 @@ int replay_revisions(struct rev_info *revs,
|
||||||
goto out;
|
goto out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* In --advance or --revert mode, update the target ref */
|
if (ref)
|
||||||
if (advance || revert) {
|
replay_result_queue_update(out, ref, &old_oid,
|
||||||
const char *ref = advance ? advance : revert;
|
|
||||||
replay_result_queue_update(out, ref,
|
|
||||||
&onto->object.oid,
|
|
||||||
&last_commit->object.oid);
|
&last_commit->object.oid);
|
||||||
}
|
|
||||||
|
|
||||||
ret = 0;
|
ret = 0;
|
||||||
|
|
||||||
|
|
|
||||||
7
replay.h
7
replay.h
|
|
@ -24,6 +24,13 @@ struct replay_revisions_options {
|
||||||
*/
|
*/
|
||||||
const char *onto;
|
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
|
* Starting point at which to create revert commits; must be a branch
|
||||||
* name. The branch will be updated to point to the revert commits.
|
* name. The branch will be updated to point to the revert commits.
|
||||||
|
|
|
||||||
|
|
@ -499,4 +499,70 @@ test_expect_success 'git replay --revert incompatible with --advance' '
|
||||||
test_grep "cannot be used together" error
|
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
|
test_done
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue