git/list[1] front-page[2] threads[3] people[4] search[5] about
 

[PATCH v8 2/7] builtin/replay: move core logic into "libgit.a"

From
Patrick Steinhardt <ps@pks.im>
Date
Jan 7, 2026, 10:10 UTC
Message-ID
<20260107-b4-pks-history-builtin-v8-2-18e9779e3a26@pks.im>
In-Reply-To
<20260107-b4-pks-history-builtin-v8-0-18e9779e3a26@pks.im>

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>
---
 Makefile         |   1 +
 builtin/replay.c | 358 +------------------------------------------------------
 meson.build      |   1 +
 replay.c         | 347 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 replay.h         |  61 ++++++++++
 5 files changed, 411 insertions(+), 357 deletions(-)
diff --git a/Makefile b/Makefile
index b7eba509c6..1c64a5d2ae 100644
--- 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
diff --git a/builtin/replay.c b/builtin/replay.c
index d7523fdbc2..24f0b5f050 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -2,257 +2,22 @@
  * "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,
diff --git a/meson.build b/meson.build
index dd52efd1c8..a5a4e99b25 100644
--- a/meson.build
+++ b/meson.build
@@ -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
index 0000000000..1926fca4b9
--- /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
index 0000000000..bc7a321a5e
--- /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
-- 
2.52.0.542.g9473a8513b.dirty
Previous: Patrick SteinhardtNext: Patrick Steinhardt
Message 5 of 18 in “Introduce git-history(1) command for easy history editing”
  1. 0/7 Introduce git-history(1) command for easy history editingPatrick Steinhardt, Jan 7, 2026
  2. 1/7 builtin/replay: extract core logic to replay revisionsPatrick Steinhardt, Jan 7, 2026
  3. D. Ben KnobleJan 7, 2026
  4. Patrick SteinhardtJan 9, 2026
  5. 2/7 builtin/replay: move core logic into "libgit.a"Patrick Steinhardt, Jan 7, 2026
  6. 3/7 replay: small set of cleanupsPatrick Steinhardt, Jan 7, 2026
  7. 4/7 replay: yield the object ID of the final rewritten commitPatrick Steinhardt, Jan 7, 2026
  8. 5/7 wt-status: provide function to expose status for treesPatrick Steinhardt, Jan 7, 2026
  9. 6/7 builtin: add new "history" commandPatrick Steinhardt, Jan 7, 2026
  10. 7/7 builtin/history: implement "reword" subcommandPatrick Steinhardt, Jan 7, 2026
  11. D. Ben KnobleJan 7, 2026
  12. Patrick SteinhardtJan 9, 2026
  13. D. Ben KnobleJan 9, 2026
  14. Elijah NewrenJan 10, 2026
  15. Patrick SteinhardtJan 12, 2026
  16. D. Ben KnobleJan 7, 2026
  17. Patrick SteinhardtJan 9, 2026
  18. D. Ben KnobleJan 9, 2026

Read the whole thread, see it on lore, or plain text.

$ cat FOOTERMessages come from the public archive at lore.kernel.org/git, fetched every hour. The front page is chosen and written each morning by an AI editor and can be wrong; the threads themselves are the record. About and API. For agents: an MCP server at https://gitlist.dev/mcp, and any thread, story or person page as Markdown by adding .md to its URL (or sending Accept: text/markdown). Details in /llms.txt.