]> git.ipfire.org Git - thirdparty/git.git/commitdiff
builtin/replay: move core logic into "libgit.a"
authorPatrick Steinhardt <ps@pks.im>
Wed, 7 Jan 2026 10:10:10 +0000 (11:10 +0100)
committerJunio C Hamano <gitster@pobox.com>
Thu, 8 Jan 2026 02:01:58 +0000 (11:01 +0900)
Move the core logic used to replay commits into "libgit.a" so that it
can be easily reused by other commands. It will be used in a subsequent
commit where we're about to introduce a new git-history(1) command.

Note that with this change we have no sign-comparison warnings anymore,
and neither do we depend on `the_repository`.

Signed-off-by: Patrick Steinhardt <ps@pks.im>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
Makefile
builtin/replay.c
meson.build
replay.c [new file with mode: 0644]
replay.h [new file with mode: 0644]

index b7eba509c6a0ca3a8346c81e89edc46d018fbe02..1c64a5d2aea3e3403893617c06f03e1aa6ba839c 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -1285,6 +1285,7 @@ LIB_OBJS += repack-geometry.o
 LIB_OBJS += repack-midx.o
 LIB_OBJS += repack-promisor.o
 LIB_OBJS += replace-object.o
+LIB_OBJS += replay.o
 LIB_OBJS += repo-settings.o
 LIB_OBJS += repository.o
 LIB_OBJS += rerere.o
index d7523fdbc240d6964aede20809014174acec58c7..24f0b5f05022f33dd44f76d0bd56bdb50ed622aa 100644 (file)
  * "git replay" builtin command
  */
 
-#define USE_THE_REPOSITORY_VARIABLE
-#define DISABLE_SIGN_COMPARE_WARNINGS
-
 #include "git-compat-util.h"
 
 #include "builtin.h"
 #include "config.h"
-#include "environment.h"
 #include "hex.h"
-#include "lockfile.h"
-#include "merge-ort.h"
 #include "object-name.h"
 #include "parse-options.h"
 #include "refs.h"
+#include "replay.h"
 #include "revision.h"
-#include "strmap.h"
-#include <oidset.h>
-#include <tree.h>
 
 enum ref_action_mode {
        REF_ACTION_UPDATE,
        REF_ACTION_PRINT,
 };
 
-static const char *short_commit_name(struct repository *repo,
-                                    struct commit *commit)
-{
-       return repo_find_unique_abbrev(repo, &commit->object.oid,
-                                      DEFAULT_ABBREV);
-}
-
-static struct commit *peel_committish(struct repository *repo,
-                                     const char *name,
-                                     const char *mode)
-{
-       struct object *obj;
-       struct object_id oid;
-
-       if (repo_get_oid(repo, name, &oid))
-               die(_("'%s' is not a valid commit-ish for %s"), name, mode);
-       obj = parse_object_or_die(repo, &oid, name);
-       return (struct commit *)repo_peel_to_type(repo, name, 0, obj,
-                                                 OBJ_COMMIT);
-}
-
-static char *get_author(const char *message)
-{
-       size_t len;
-       const char *a;
-
-       a = find_commit_header(message, "author", &len);
-       if (a)
-               return xmemdupz(a, len);
-
-       return NULL;
-}
-
-static struct commit *create_commit(struct repository *repo,
-                                   struct tree *tree,
-                                   struct commit *based_on,
-                                   struct commit *parent)
-{
-       struct object_id ret;
-       struct object *obj = NULL;
-       struct commit_list *parents = NULL;
-       char *author;
-       char *sign_commit = NULL; /* FIXME: cli users might want to sign again */
-       struct commit_extra_header *extra = NULL;
-       struct strbuf msg = STRBUF_INIT;
-       const char *out_enc = get_commit_output_encoding();
-       const char *message = repo_logmsg_reencode(repo, based_on,
-                                                  NULL, out_enc);
-       const char *orig_message = NULL;
-       const char *exclude_gpgsig[] = { "gpgsig", "gpgsig-sha256", NULL };
-
-       commit_list_insert(parent, &parents);
-       extra = read_commit_extra_headers(based_on, exclude_gpgsig);
-       find_commit_subject(message, &orig_message);
-       strbuf_addstr(&msg, orig_message);
-       author = get_author(message);
-       reset_ident_date();
-       if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,
-                                &ret, author, NULL, sign_commit, extra)) {
-               error(_("failed to write commit object"));
-               goto out;
-       }
-
-       obj = parse_object(repo, &ret);
-
-out:
-       repo_unuse_commit_buffer(the_repository, based_on, message);
-       free_commit_extra_headers(extra);
-       free_commit_list(parents);
-       strbuf_release(&msg);
-       free(author);
-       return (struct commit *)obj;
-}
-
-struct ref_info {
-       struct commit *onto;
-       struct strset positive_refs;
-       struct strset negative_refs;
-       int positive_refexprs;
-       int negative_refexprs;
-};
-
-static void get_ref_information(struct repository *repo,
-                               struct rev_cmdline_info *cmd_info,
-                               struct ref_info *ref_info)
-{
-       int i;
-
-       ref_info->onto = NULL;
-       strset_init(&ref_info->positive_refs);
-       strset_init(&ref_info->negative_refs);
-       ref_info->positive_refexprs = 0;
-       ref_info->negative_refexprs = 0;
-
-       /*
-        * When the user specifies e.g.
-        *   git replay origin/main..mybranch
-        *   git replay ^origin/next mybranch1 mybranch2
-        * we want to be able to determine where to replay the commits.  In
-        * these examples, the branches are probably based on an old version
-        * of either origin/main or origin/next, so we want to replay on the
-        * newest version of that branch.  In contrast we would want to error
-        * out if they ran
-        *   git replay ^origin/master ^origin/next mybranch
-        *   git replay mybranch~2..mybranch
-        * the first of those because there's no unique base to choose, and
-        * the second because they'd likely just be replaying commits on top
-        * of the same commit and not making any difference.
-        */
-       for (i = 0; i < cmd_info->nr; i++) {
-               struct rev_cmdline_entry *e = cmd_info->rev + i;
-               struct object_id oid;
-               const char *refexpr = e->name;
-               char *fullname = NULL;
-               int can_uniquely_dwim = 1;
-
-               if (*refexpr == '^')
-                       refexpr++;
-               if (repo_dwim_ref(repo, refexpr, strlen(refexpr), &oid, &fullname, 0) != 1)
-                       can_uniquely_dwim = 0;
-
-               if (e->flags & BOTTOM) {
-                       if (can_uniquely_dwim)
-                               strset_add(&ref_info->negative_refs, fullname);
-                       if (!ref_info->negative_refexprs)
-                               ref_info->onto = lookup_commit_reference_gently(repo,
-                                                                               &e->item->oid, 1);
-                       ref_info->negative_refexprs++;
-               } else {
-                       if (can_uniquely_dwim)
-                               strset_add(&ref_info->positive_refs, fullname);
-                       ref_info->positive_refexprs++;
-               }
-
-               free(fullname);
-       }
-}
-
-static void set_up_replay_mode(struct repository *repo,
-                              struct rev_cmdline_info *cmd_info,
-                              const char *onto_name,
-                              char **advance_name,
-                              struct commit **onto,
-                              struct strset **update_refs)
-{
-       struct ref_info rinfo;
-
-       get_ref_information(repo, cmd_info, &rinfo);
-       if (!rinfo.positive_refexprs)
-               die(_("need some commits to replay"));
-
-       die_for_incompatible_opt2(!!onto_name, "--onto",
-                                 !!*advance_name, "--advance");
-       if (onto_name) {
-               *onto = peel_committish(repo, onto_name, "--onto");
-               if (rinfo.positive_refexprs <
-                   strset_get_size(&rinfo.positive_refs))
-                       die(_("all positive revisions given must be references"));
-               *update_refs = xcalloc(1, sizeof(**update_refs));
-               **update_refs = rinfo.positive_refs;
-               memset(&rinfo.positive_refs, 0, sizeof(**update_refs));
-       } else {
-               struct object_id oid;
-               char *fullname = NULL;
-
-               if (!*advance_name)
-                       BUG("expected either onto_name or *advance_name in this function");
-
-               if (repo_dwim_ref(repo, *advance_name, strlen(*advance_name),
-                            &oid, &fullname, 0) == 1) {
-                       free(*advance_name);
-                       *advance_name = fullname;
-               } else {
-                       die(_("argument to --advance must be a reference"));
-               }
-               *onto = peel_committish(repo, *advance_name, "--advance");
-               if (rinfo.positive_refexprs > 1)
-                       die(_("cannot advance target with multiple sources because ordering would be ill-defined"));
-       }
-       strset_clear(&rinfo.negative_refs);
-       strset_clear(&rinfo.positive_refs);
-}
-
-static struct commit *mapped_commit(kh_oid_map_t *replayed_commits,
-                                   struct commit *commit,
-                                   struct commit *fallback)
-{
-       khint_t pos = kh_get_oid_map(replayed_commits, commit->object.oid);
-       if (pos == kh_end(replayed_commits))
-               return fallback;
-       return kh_value(replayed_commits, pos);
-}
-
-static struct commit *pick_regular_commit(struct repository *repo,
-                                         struct commit *pickme,
-                                         kh_oid_map_t *replayed_commits,
-                                         struct commit *onto,
-                                         struct merge_options *merge_opt,
-                                         struct merge_result *result)
-{
-       struct commit *base, *replayed_base;
-       struct tree *pickme_tree, *base_tree;
-
-       base = pickme->parents->item;
-       replayed_base = mapped_commit(replayed_commits, base, onto);
-
-       result->tree = repo_get_commit_tree(repo, replayed_base);
-       pickme_tree = repo_get_commit_tree(repo, pickme);
-       base_tree = repo_get_commit_tree(repo, base);
-
-       merge_opt->branch1 = short_commit_name(repo, replayed_base);
-       merge_opt->branch2 = short_commit_name(repo, pickme);
-       merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
-
-       merge_incore_nonrecursive(merge_opt,
-                                 base_tree,
-                                 result->tree,
-                                 pickme_tree,
-                                 result);
-
-       free((char*)merge_opt->ancestor);
-       merge_opt->ancestor = NULL;
-       if (!result->clean)
-               return NULL;
-       return create_commit(repo, result->tree, pickme, replayed_base);
-}
-
 static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)
 {
        if (!ref_action || !strcmp(ref_action, "update"))
@@ -278,127 +43,6 @@ static enum ref_action_mode get_ref_action_mode(struct repository *repo, const c
        return REF_ACTION_UPDATE;
 }
 
-struct replay_revisions_options {
-       const char *advance;
-       const char *onto;
-       int contained;
-};
-
-struct replay_ref_updates {
-       struct replay_ref_update {
-               char *refname;
-               struct object_id old_oid;
-               struct object_id new_oid;
-       } *items;
-       size_t nr, alloc;
-};
-
-static void replay_ref_updates_release(struct replay_ref_updates *updates)
-{
-       for (size_t i = 0; i < updates->nr; i++)
-               free(updates->items[i].refname);
-       free(updates->items);
-}
-
-static int replay_revisions(struct repository *repo, struct rev_info *revs,
-                           struct replay_revisions_options *opts,
-                           struct replay_ref_updates *updates)
-{
-       kh_oid_map_t *replayed_commits = NULL;
-       struct strset *update_refs = NULL;
-       struct commit *last_commit = NULL;
-       struct commit *commit;
-       struct commit *onto = NULL;
-       struct merge_options merge_opt;
-       struct merge_result result;
-       char *advance;
-       int ret;
-
-       advance = xstrdup_or_null(opts->advance);
-       set_up_replay_mode(repo, &revs->cmdline, opts->onto, &advance,
-                          &onto, &update_refs);
-
-       /* FIXME: Should allow replaying commits with the first as a root commit */
-
-       if (prepare_revision_walk(revs) < 0) {
-               ret = error(_("error preparing revisions"));
-               goto out;
-       }
-
-       init_basic_merge_options(&merge_opt, repo);
-       memset(&result, 0, sizeof(result));
-       merge_opt.show_rename_progress = 0;
-       last_commit = onto;
-       replayed_commits = kh_init_oid_map();
-       while ((commit = get_revision(revs))) {
-               const struct name_decoration *decoration;
-               khint_t pos;
-               int hr;
-
-               if (!commit->parents)
-                       die(_("replaying down from root commit is not supported yet!"));
-               if (commit->parents->next)
-                       die(_("replaying merge commits is not supported yet!"));
-
-               last_commit = pick_regular_commit(repo, commit, replayed_commits,
-                                                 onto, &merge_opt, &result);
-               if (!last_commit)
-                       break;
-
-               /* Record commit -> last_commit mapping */
-               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 (advance)
-                       continue;
-               decoration = get_name_decoration(&commit->object);
-               if (!decoration)
-                       continue;
-               while (decoration) {
-                       if (decoration->type == DECORATION_REF_LOCAL &&
-                           (opts->contained || strset_contains(update_refs,
-                                                               decoration->name))) {
-                               ALLOC_GROW(updates->items, updates->nr + 1, updates->alloc);
-                               updates->items[updates->nr].refname = xstrdup(decoration->name);
-                               updates->items[updates->nr].old_oid = commit->object.oid;
-                               updates->items[updates->nr].new_oid = last_commit->object.oid;
-                               updates->nr++;
-                       }
-                       decoration = decoration->next;
-               }
-       }
-
-       if (!result.clean) {
-               ret = -1;
-               goto out;
-       }
-
-       /* In --advance mode, advance the target ref */
-       if (advance) {
-               ALLOC_GROW(updates->items, updates->nr + 1, updates->alloc);
-               updates->items[updates->nr].refname = xstrdup(advance);
-               updates->items[updates->nr].old_oid = onto->object.oid;
-               updates->items[updates->nr].new_oid = last_commit->object.oid;
-               updates->nr++;
-       }
-
-       ret = 0;
-
-out:
-       if (update_refs) {
-               strset_clear(update_refs);
-               free(update_refs);
-       }
-       kh_destroy_oid_map(replayed_commits);
-       merge_finalize(&merge_opt, &result);
-       free(advance);
-       return ret;
-}
-
 static int handle_ref_update(enum ref_action_mode mode,
                             struct ref_transaction *transaction,
                             const char *refname,
index dd52efd1c875746304c717d8fc4a572d1f26a0cc..a5a4e99b259cf98f9b808d0ebf9dfc64a7f09621 100644 (file)
@@ -471,6 +471,7 @@ libgit_sources = [
   'repack-midx.c',
   'repack-promisor.c',
   'replace-object.c',
+  'replay.c',
   'repo-settings.c',
   'repository.c',
   'rerere.c',
diff --git a/replay.c b/replay.c
new file mode 100644 (file)
index 0000000..1926fca
--- /dev/null
+++ b/replay.c
@@ -0,0 +1,347 @@
+#define USE_THE_REPOSITORY_VARIABLE
+#define DISABLE_SIGN_COMPARE_WARNINGS
+
+#include "git-compat-util.h"
+#include "environment.h"
+#include "hex.h"
+#include "merge-ort.h"
+#include "object-name.h"
+#include "oidset.h"
+#include "parse-options.h"
+#include "refs.h"
+#include "replay.h"
+#include "revision.h"
+#include "tree.h"
+
+static const char *short_commit_name(struct repository *repo,
+                                    struct commit *commit)
+{
+       return repo_find_unique_abbrev(repo, &commit->object.oid,
+                                      DEFAULT_ABBREV);
+}
+
+static struct commit *peel_committish(struct repository *repo,
+                                     const char *name,
+                                     const char *mode)
+{
+       struct object *obj;
+       struct object_id oid;
+
+       if (repo_get_oid(repo, name, &oid))
+               die(_("'%s' is not a valid commit-ish for %s"), name, mode);
+       obj = parse_object_or_die(repo, &oid, name);
+       return (struct commit *)repo_peel_to_type(repo, name, 0, obj,
+                                                 OBJ_COMMIT);
+}
+
+static char *get_author(const char *message)
+{
+       size_t len;
+       const char *a;
+
+       a = find_commit_header(message, "author", &len);
+       if (a)
+               return xmemdupz(a, len);
+
+       return NULL;
+}
+
+static struct commit *create_commit(struct repository *repo,
+                                   struct tree *tree,
+                                   struct commit *based_on,
+                                   struct commit *parent)
+{
+       struct object_id ret;
+       struct object *obj = NULL;
+       struct commit_list *parents = NULL;
+       char *author;
+       char *sign_commit = NULL; /* FIXME: cli users might want to sign again */
+       struct commit_extra_header *extra = NULL;
+       struct strbuf msg = STRBUF_INIT;
+       const char *out_enc = get_commit_output_encoding();
+       const char *message = repo_logmsg_reencode(repo, based_on,
+                                                  NULL, out_enc);
+       const char *orig_message = NULL;
+       const char *exclude_gpgsig[] = { "gpgsig", "gpgsig-sha256", NULL };
+
+       commit_list_insert(parent, &parents);
+       extra = read_commit_extra_headers(based_on, exclude_gpgsig);
+       find_commit_subject(message, &orig_message);
+       strbuf_addstr(&msg, orig_message);
+       author = get_author(message);
+       reset_ident_date();
+       if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,
+                                &ret, author, NULL, sign_commit, extra)) {
+               error(_("failed to write commit object"));
+               goto out;
+       }
+
+       obj = parse_object(repo, &ret);
+
+out:
+       repo_unuse_commit_buffer(the_repository, based_on, message);
+       free_commit_extra_headers(extra);
+       free_commit_list(parents);
+       strbuf_release(&msg);
+       free(author);
+       return (struct commit *)obj;
+}
+
+struct ref_info {
+       struct commit *onto;
+       struct strset positive_refs;
+       struct strset negative_refs;
+       int positive_refexprs;
+       int negative_refexprs;
+};
+
+static void get_ref_information(struct repository *repo,
+                               struct rev_cmdline_info *cmd_info,
+                               struct ref_info *ref_info)
+{
+       int i;
+
+       ref_info->onto = NULL;
+       strset_init(&ref_info->positive_refs);
+       strset_init(&ref_info->negative_refs);
+       ref_info->positive_refexprs = 0;
+       ref_info->negative_refexprs = 0;
+
+       /*
+        * When the user specifies e.g.
+        *   git replay origin/main..mybranch
+        *   git replay ^origin/next mybranch1 mybranch2
+        * we want to be able to determine where to replay the commits.  In
+        * these examples, the branches are probably based on an old version
+        * of either origin/main or origin/next, so we want to replay on the
+        * newest version of that branch.  In contrast we would want to error
+        * out if they ran
+        *   git replay ^origin/master ^origin/next mybranch
+        *   git replay mybranch~2..mybranch
+        * the first of those because there's no unique base to choose, and
+        * the second because they'd likely just be replaying commits on top
+        * of the same commit and not making any difference.
+        */
+       for (i = 0; i < cmd_info->nr; i++) {
+               struct rev_cmdline_entry *e = cmd_info->rev + i;
+               struct object_id oid;
+               const char *refexpr = e->name;
+               char *fullname = NULL;
+               int can_uniquely_dwim = 1;
+
+               if (*refexpr == '^')
+                       refexpr++;
+               if (repo_dwim_ref(repo, refexpr, strlen(refexpr), &oid, &fullname, 0) != 1)
+                       can_uniquely_dwim = 0;
+
+               if (e->flags & BOTTOM) {
+                       if (can_uniquely_dwim)
+                               strset_add(&ref_info->negative_refs, fullname);
+                       if (!ref_info->negative_refexprs)
+                               ref_info->onto = lookup_commit_reference_gently(repo,
+                                                                               &e->item->oid, 1);
+                       ref_info->negative_refexprs++;
+               } else {
+                       if (can_uniquely_dwim)
+                               strset_add(&ref_info->positive_refs, fullname);
+                       ref_info->positive_refexprs++;
+               }
+
+               free(fullname);
+       }
+}
+
+static void set_up_replay_mode(struct repository *repo,
+                              struct rev_cmdline_info *cmd_info,
+                              const char *onto_name,
+                              char **advance_name,
+                              struct commit **onto,
+                              struct strset **update_refs)
+{
+       struct ref_info rinfo;
+
+       get_ref_information(repo, cmd_info, &rinfo);
+       if (!rinfo.positive_refexprs)
+               die(_("need some commits to replay"));
+
+       die_for_incompatible_opt2(!!onto_name, "--onto",
+                                 !!*advance_name, "--advance");
+       if (onto_name) {
+               *onto = peel_committish(repo, onto_name, "--onto");
+               if (rinfo.positive_refexprs <
+                   strset_get_size(&rinfo.positive_refs))
+                       die(_("all positive revisions given must be references"));
+               *update_refs = xcalloc(1, sizeof(**update_refs));
+               **update_refs = rinfo.positive_refs;
+               memset(&rinfo.positive_refs, 0, sizeof(**update_refs));
+       } else {
+               struct object_id oid;
+               char *fullname = NULL;
+
+               if (!*advance_name)
+                       BUG("expected either onto_name or *advance_name in this function");
+
+               if (repo_dwim_ref(repo, *advance_name, strlen(*advance_name),
+                            &oid, &fullname, 0) == 1) {
+                       free(*advance_name);
+                       *advance_name = fullname;
+               } else {
+                       die(_("argument to --advance must be a reference"));
+               }
+               *onto = peel_committish(repo, *advance_name, "--advance");
+               if (rinfo.positive_refexprs > 1)
+                       die(_("cannot advance target with multiple sources because ordering would be ill-defined"));
+       }
+       strset_clear(&rinfo.negative_refs);
+       strset_clear(&rinfo.positive_refs);
+}
+
+static struct commit *mapped_commit(kh_oid_map_t *replayed_commits,
+                                   struct commit *commit,
+                                   struct commit *fallback)
+{
+       khint_t pos = kh_get_oid_map(replayed_commits, commit->object.oid);
+       if (pos == kh_end(replayed_commits))
+               return fallback;
+       return kh_value(replayed_commits, pos);
+}
+
+static struct commit *pick_regular_commit(struct repository *repo,
+                                         struct commit *pickme,
+                                         kh_oid_map_t *replayed_commits,
+                                         struct commit *onto,
+                                         struct merge_options *merge_opt,
+                                         struct merge_result *result)
+{
+       struct commit *base, *replayed_base;
+       struct tree *pickme_tree, *base_tree;
+
+       base = pickme->parents->item;
+       replayed_base = mapped_commit(replayed_commits, base, onto);
+
+       result->tree = repo_get_commit_tree(repo, replayed_base);
+       pickme_tree = repo_get_commit_tree(repo, pickme);
+       base_tree = repo_get_commit_tree(repo, base);
+
+       merge_opt->branch1 = short_commit_name(repo, replayed_base);
+       merge_opt->branch2 = short_commit_name(repo, pickme);
+       merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
+
+       merge_incore_nonrecursive(merge_opt,
+                                 base_tree,
+                                 result->tree,
+                                 pickme_tree,
+                                 result);
+
+       free((char*)merge_opt->ancestor);
+       merge_opt->ancestor = NULL;
+       if (!result->clean)
+               return NULL;
+       return create_commit(repo, result->tree, pickme, replayed_base);
+}
+
+void replay_ref_updates_release(struct replay_ref_updates *updates)
+{
+       for (size_t i = 0; i < updates->nr; i++)
+               free(updates->items[i].refname);
+       free(updates->items);
+}
+
+int replay_revisions(struct repository *repo, struct rev_info *revs,
+                    struct replay_revisions_options *opts,
+                    struct replay_ref_updates *updates)
+{
+       kh_oid_map_t *replayed_commits = NULL;
+       struct strset *update_refs = NULL;
+       struct commit *last_commit = NULL;
+       struct commit *commit;
+       struct commit *onto = NULL;
+       struct merge_options merge_opt;
+       struct merge_result result;
+       char *advance;
+       int ret;
+
+       advance = xstrdup_or_null(opts->advance);
+       set_up_replay_mode(repo, &revs->cmdline, opts->onto, &advance,
+                          &onto, &update_refs);
+
+       /* FIXME: Should allow replaying commits with the first as a root commit */
+
+       if (prepare_revision_walk(revs) < 0) {
+               ret = error(_("error preparing revisions"));
+               goto out;
+       }
+
+       init_basic_merge_options(&merge_opt, repo);
+       memset(&result, 0, sizeof(result));
+       merge_opt.show_rename_progress = 0;
+       last_commit = onto;
+       replayed_commits = kh_init_oid_map();
+       while ((commit = get_revision(revs))) {
+               const struct name_decoration *decoration;
+               khint_t pos;
+               int hr;
+
+               if (!commit->parents)
+                       die(_("replaying down from root commit is not supported yet!"));
+               if (commit->parents->next)
+                       die(_("replaying merge commits is not supported yet!"));
+
+               last_commit = pick_regular_commit(repo, commit, replayed_commits,
+                                                 onto, &merge_opt, &result);
+               if (!last_commit)
+                       break;
+
+               /* Record commit -> last_commit mapping */
+               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 (advance)
+                       continue;
+               decoration = get_name_decoration(&commit->object);
+               if (!decoration)
+                       continue;
+               while (decoration) {
+                       if (decoration->type == DECORATION_REF_LOCAL &&
+                           (opts->contained || strset_contains(update_refs,
+                                                               decoration->name))) {
+                               ALLOC_GROW(updates->items, updates->nr + 1, updates->alloc);
+                               updates->items[updates->nr].refname = xstrdup(decoration->name);
+                               updates->items[updates->nr].old_oid = commit->object.oid;
+                               updates->items[updates->nr].new_oid = last_commit->object.oid;
+                               updates->nr++;
+                       }
+                       decoration = decoration->next;
+               }
+       }
+
+       if (!result.clean) {
+               ret = -1;
+               goto out;
+       }
+
+       /* In --advance mode, advance the target ref */
+       if (advance) {
+               ALLOC_GROW(updates->items, updates->nr + 1, updates->alloc);
+               updates->items[updates->nr].refname = xstrdup(advance);
+               updates->items[updates->nr].old_oid = onto->object.oid;
+               updates->items[updates->nr].new_oid = last_commit->object.oid;
+               updates->nr++;
+       }
+
+       ret = 0;
+
+out:
+       if (update_refs) {
+               strset_clear(update_refs);
+               free(update_refs);
+       }
+       kh_destroy_oid_map(replayed_commits);
+       merge_finalize(&merge_opt, &result);
+       free(advance);
+       return ret;
+}
diff --git a/replay.h b/replay.h
new file mode 100644 (file)
index 0000000..bc7a321
--- /dev/null
+++ b/replay.h
@@ -0,0 +1,61 @@
+#ifndef REPLAY_H
+#define REPLAY_H
+
+#include "hash.h"
+
+struct repository;
+struct rev_info;
+
+/*
+ * A set of options that can be passed to `replay_revisions()`.
+ */
+struct replay_revisions_options {
+       /*
+        * Starting point at which to create the new commits; must be a branch
+        * name. The branch will be updated to point to the rewritten commits.
+        * This option is mutually exclusive with `onto`.
+        */
+       const char *advance;
+
+       /*
+        * Starting point at which to create the new commits; must be a
+        * committish. References pointing at decendants of `onto` will be
+        * updated to point to the new commits.
+        */
+        const char *onto;
+
+       /*
+        * Update branches that point at commits in the given revision range.
+        * Requires `onto` to be set.
+        */
+       int contained;
+};
+
+/* This struct is used as an out-parameter by `replay_revisions()`. */
+struct replay_ref_updates {
+       /*
+        * The set of reference updates that are caused by replaying the
+        * commits.
+        */
+       struct replay_ref_update {
+               char *refname;
+               struct object_id old_oid;
+               struct object_id new_oid;
+       } *items;
+       size_t nr, alloc;
+};
+
+void replay_ref_updates_release(struct replay_ref_updates *updates);
+
+/*
+ * Replay a set of commits onto a new location. Leaves both the working tree,
+ * index and references untouched. Reference updates caused by the replay will
+ * be recorded in the `updates` out pointer.
+ *
+ * Returns 0 on success, a negative error code otherwise.
+ */
+int replay_revisions(struct repository *repo, struct rev_info *revs,
+                    struct replay_revisions_options *opts,
+                    struct replay_ref_updates *updates);
+
+#endif