--------
[verse]
(EXPERIMENTAL!) 'git replay' ([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)
- [--ref=<ref>] [--ref-action=<mode>] [--linearize] <revision-range>
+ [--ref=<ref>] [--ref-action=<mode>] <revision-range>
DESCRIPTION
-----------
+
The default mode can be configured via the `replay.refAction` configuration variable.
---linearize::
- In this mode, each replayed commit is stacked on top of the
- previously replayed one, so all replayed commits are flattened into
- a single linear history.
-+
-When a merge commit is encountered, the behavior of git-rebase(1)'s
-option `--no-rebase-merges` is imitated. All commits in the range
-reachable from the merge commit are replayed into a linear history, and
-the merge commit itself is dropped. A ref that pointed to a merge commit
-is updated to the merge's last replayed ancestor.
-+
-This flattens the `<revision-range>` as a whole. When multiple revision
-ranges are given they are stacked on top of each other into one linear
-history. Each of their refs is updated to point to its position in that
-history. To linearize ranges separately, replay them in separate `git
-replay` invocations.
-
<revision-range>::
Range of commits to replay; see "Specifying Ranges" in
linkgit:git-rev-parse[1]. In `--advance=<branch>` or
const char *const replay_usage[] = {
N_("(EXPERIMENTAL!) git replay "
"([--contained] --onto=<newbase> | --advance=<branch> | --revert=<branch>)\n"
- "[--ref=<ref>] [--ref-action=<mode>] [--linearize] <revision-range>"),
+ "[--ref=<ref>] [--ref-action=<mode>] <revision-range>"),
NULL
};
struct option replay_options[] = {
N_("mode"),
N_("control ref update behavior (update|print)"),
PARSE_OPT_NONEG),
- OPT_BOOL(0, "linearize", &opts.linearize,
- N_("drop merge commits, replaying only non-merge commits")),
OPT_END()
};
strset_clear(&rinfo.positive_refs);
}
-static struct commit *get_mapped_commit(kh_oid_map_t *replayed_commits,
- struct commit *commit,
- struct commit *fallback)
+static struct commit *mapped_commit(kh_oid_map_t *replayed_commits,
+ struct commit *commit,
+ struct commit *fallback)
{
khint_t pos;
if (!commit)
return kh_value(replayed_commits, pos);
}
-static void put_mapped_commit(kh_oid_map_t *replayed_commits,
- struct commit *commit,
- struct commit *new_commit)
-{
- khint_t pos;
- int ret;
-
- pos = kh_put_oid_map(replayed_commits, commit->object.oid, &ret);
- if (ret == 0)
- BUG("Duplicate rewritten commit: %s",
- oid_to_hex(&commit->object.oid));
-
- kh_value(replayed_commits, pos) = new_commit;
-}
-
static struct commit *pick_regular_commit(struct repository *repo,
struct commit *pickme,
- struct commit *replayed_base,
+ kh_oid_map_t *replayed_commits,
+ struct commit *onto,
struct merge_options *merge_opt,
struct merge_result *result,
enum replay_mode mode,
enum replay_empty_commit_action empty)
{
+ struct commit *base, *replayed_base;
struct tree *pickme_tree, *base_tree, *replayed_base_tree;
- if (pickme->parents)
- base_tree = repo_get_commit_tree(repo, pickme->parents->item);
- else
+ if (pickme->parents) {
+ base = pickme->parents->item;
+ base_tree = repo_get_commit_tree(repo, base);
+ } else {
+ base = NULL;
base_tree = lookup_tree(repo, repo->hash_algo->empty_tree);
+ }
+ replayed_base = mapped_commit(replayed_commits, base, onto);
replayed_base_tree = repo_get_commit_tree(repo, replayed_base);
pickme_tree = repo_get_commit_tree(repo, pickme);
replayed_commits = kh_init_oid_map();
while ((commit = get_revision(revs))) {
const struct name_decoration *decoration;
+ khint_t pos;
+ int hr;
- if (commit->parents && commit->parents->next) {
- if (!opts->linearize)
- die(_("replaying merge commits is not supported yet!"));
- /*
- * Drop the merge commit: do not pick it, leave
- * `last_commit` unchanged, and fall through to the
- * rest of the loop. As a result:
- * - refs pointing to the merge commit will be updated
- * to `last_commit`.
- * - the next replayed commit uses `last_commit` as its
- * `base`.
- */
- } else {
- /*
- * Decide where to replay this commit onto.
- * If the parent commit was replayed already, the replayed result
- * can be found in `replayed_commits`. Otherwise fall back to `onto`.
- * When reverting, commits are replayed in reverse order and thus
- * its parent isn't replayed yet. Therefore revert commits are
- * always replayed onto `last_commit`.
- * Also when opts->linearize is true, set the base to
- * `last_commit` to create a single linear history.
- */
- struct commit *parent = commit->parents ? commit->parents->item : NULL;
- struct commit *base = get_mapped_commit(replayed_commits, parent, onto);
-
- if (opts->linearize || mode == REPLAY_MODE_REVERT)
- base = last_commit;
-
- last_commit = pick_regular_commit(revs->repo, commit, base,
- &merge_opt, &result,
- mode, opts->empty);
- }
+ if (commit->parents && commit->parents->next)
+ die(_("replaying merge commits is not supported yet!"));
+ last_commit = pick_regular_commit(revs->repo, commit, replayed_commits,
+ mode == REPLAY_MODE_REVERT ? last_commit : onto,
+ &merge_opt, &result, mode, opts->empty);
if (!last_commit)
break;
/* Record commit -> last_commit mapping */
- put_mapped_commit(replayed_commits, commit, last_commit);
+ pos = kh_put_oid_map(replayed_commits, commit->object.oid, &hr);
+ if (hr == 0)
+ BUG("Duplicate rewritten commit: %s\n",
+ oid_to_hex(&commit->object.oid));
+ kh_value(replayed_commits, pos) = last_commit;
/* Update any necessary branches */
if (ref)
* Defaults to REPLAY_EMPTY_COMMIT_DROP.
*/
enum replay_empty_commit_action empty;
-
- /*
- * Whether to linearize the commits (i.e. drop merge commits).
- */
- int linearize;
};
/* This struct is used as an out-parameter by `replay_revisions()`. */
test_merge P O --no-ff &&
git switch main &&
- git switch --orphan unrelated &&
- test_commit unrelated-root &&
-
git switch -c conflict B &&
- test_commit C.conflict C.t conflict &&
- git branch -D unrelated &&
-
- git switch -c divergent-x main &&
- test_commit X &&
- git switch -c divergent-y main &&
- test_commit Y &&
- git switch divergent-x &&
- test_merge Z divergent-y --no-ff
+ test_commit C.conflict C.t conflict
'
test_expect_success 'setup bare' '
test_grep "cannot be used with multiple revision ranges" err
'
-test_expect_success 'replay to rebase merge commit with --linearize' '
- git replay --ref-action=print --linearize \
- --onto main I..topic-with-merge >result &&
-
- test_line_count = 1 result &&
-
- git log --format=%s $(cut -f 3 -d " " result) >actual &&
- test_write_lines O N J M L B A >expect &&
- test_cmp expect actual
-'
-
-test_expect_success 'replay to rebase merge commit with --linearize down to the root commit' '
- git replay --ref-action=print --linearize \
- --onto unrelated-root topic-with-merge >result &&
-
- test_line_count = 1 result &&
-
- git log --format=%s $(cut -f 3 -d " " result) >actual &&
- test_write_lines O N J I B A unrelated-root >expect &&
- test_cmp expect actual
-'
-
-test_expect_success 'replay to cherry-pick merge commit with --linearize' '
- git replay --ref-action=print --linearize \
- --advance main I..topic-with-merge >result &&
-
- test_line_count = 1 result &&
-
- git log --format=%s $(cut -f 3 -d " " result) >actual &&
- test_write_lines O N J M L B A >expect &&
- test_cmp expect actual &&
-
- printf "update refs/heads/main " >expect &&
- printf "%s " $(cut -f 3 -d " " result) >>expect &&
- git rev-parse main >>expect &&
- test_cmp expect result
-'
-
-test_expect_success 'replay --linearize produces the same patches' '
- git replay --ref-action=print --linearize \
- --onto main I..topic-with-merge >result &&
-
- test_line_count = 1 result &&
- tip=$(cut -f 3 -d " " result) &&
-
- # range-diff does not care about the dropped merge,
- # so the original commits (I..topic-with-merge)
- # and the replayed chain (main..tip) must produce identical patches.
- git range-diff I..topic-with-merge main..$tip >out &&
- test_file_not_empty out &&
- test_grep ! -v "=" out &&
-
- git log --oneline main..$tip >out &&
- test_line_count = 3 out
-'
-
-test_expect_success 'replay with --linearize rebase multiple divergent branches into a single line' '
- git replay --ref-action=print --linearize \
- --onto main ^B topic2 topic3 topic4 >result &&
-
- test_line_count = 3 result &&
- cut -f 3 -d " " result >new-branch-tips &&
-
- >expect &&
- for i in 2 3 4
- do
- printf "update refs/heads/topic$i " >>expect &&
- printf "%s " $(grep topic$i result | cut -f 3 -d " ") >>expect &&
- git rev-parse topic$i >>expect || return 1
- done &&
-
- test_cmp expect result &&
-
- test_write_lines E D C M L B A >expect2 &&
- test_write_lines H G F E D C M L B A >expect3 &&
- test_write_lines J I H G F E D C M L B A >expect4 &&
-
- for i in 2 3 4
- do
- git log --format=%s $(grep topic$i result | cut -f 3 -d " ") >actual &&
- test_cmp expect$i actual || return 1
- done
-'
-
-test_expect_success 'replay with --linearize of a divergent merge keeps both sides' '
- git replay --ref-action=print --linearize \
- --onto main main..divergent-x >result &&
- test_line_count = 1 result &&
- tip=$(cut -f 3 -d " " result) &&
-
- # The merge Z is dropped, but both X and Y are linearized onto main;
- # neither side is lost.
- git log --format=%s main..$tip >actual &&
- test_write_lines Y X >expect &&
- test_cmp expect actual
-'
-
-test_expect_success '--linearize with --contained updates contained refs' '
- git replay --ref-action=print --linearize --contained \
- --onto main ^B topic-with-merge >result &&
-
- test_line_count = 2 result &&
-
- git log --format=%s $(head -n 1 result | cut -f 3 -d " ") >actual &&
- test_write_lines J I M L B A >expect &&
- test_cmp expect actual &&
-
- git log --format=%s $(tail -n 1 result | cut -f 3 -d " ") >actual &&
- test_write_lines O N J I M L B A >expect &&
- test_cmp expect actual
-'
-
-test_expect_success 'replay --revert with --linearize reverts a range containing a merge' '
- git replay --ref-action=print --revert=divergent-x --linearize \
- main..divergent-x >result &&
- test_line_count = 1 result &&
- tip=$(cut -f 3 -d " " result) &&
-
- git log --format=%s $tip >actual &&
- test_write_lines \
- "Revert \"X\"" "Revert \"Y\"" Z Y X M L B A >expect &&
- test_cmp expect actual &&
-
- test_must_fail git cat-file -e $tip:X.t &&
- test_must_fail git cat-file -e $tip:Y.t
-'
-
test_done