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

[RFC] introducing git replay

From
Edmundo Carmona Antoranz <eantoranz@gmail.com>
Date
Apr 13, 2022, 16:43 UTC
Message-ID
<20220413164336.101390-1-eantoranz@gmail.com>
Let me explain with an easy-to-follow example:
$ git checkout v2.35.0
.
.
.
HEAD is now at 89bece5c8c Git 2.35
$ git commit --amend --no-edit
[detached HEAD c58a5e5621] Git 2.35
 Author: Junio C Hamano <someone@somewhere>
 Date: Mon Jan 24 09:25:25 2022 -0800
 2 files changed, 11 insertions(+), 1 deletion(-)
$ git rebase --rebase-merges --onto HEAD v2.35.0 v2.36.0-rc1
Auto-merging GIT-VERSION-GEN
CONFLICT (content): Merge conflict in GIT-VERSION-GEN
CONFLICT (content): Merge conflict in RelNotes
error: could not apply 4c53a8c20f... Git 2.35.1
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply 4c53a8c20f... Git 2.35.1

If HEAD and v2.35.0 share the same tree, it _should_ be possible to recreate the commits that make up the range v2.35.0..v2.36.0-rc1 on top of HEAD without requiring any real "rebasing". Just creating new revisions with the same information except for different parents (and possibly a committer?).

This is what git replay does. To achieve the same in this example:
$ git checkout v2.35.0
.
.
.
HEAD is now at 89bece5c8c Git 2.35
$ git commit --amend --no-edit
[detached HEAD c682d8a22e] Git 2.35
 Author: Junio C Hamano <someone@somewhere>
 Date: Mon Jan 24 09:25:25 2022 -0800
 2 files changed, 11 insertions(+), 1 deletion(-)
$ git replay HEAD v2.35.0 v2.36.0-rc1
8312ecf6404ab1bacd5521a2d8681a2410d13ede

The ID that is printed out is the equivalent of V2.36.0-rc1 after replaying.

This is a RFC because:
- Perhaps it is already possible to do it with git rebase
  to achieve the same? But I haven't seen a recipe that
  gets it done in stackoverflow, at least.
- If it is not possible, I think getting this logic in rebase
  (with a flag, for example --replay) makes sense.
Let me know what you think.
Interesting? Not?
Keep it as a builtin (polishing it, it's just a rough cut
at the time) or get it into rebase?
---
 Makefile         |   1 +
 builtin.h        |   1 +
 builtin/replay.c | 148 +++++++++++++++++++++++++++++++++++++++++++++++
 git.c            |   1 +
 4 files changed, 151 insertions(+)
 create mode 100644 builtin/replay.c
diff --git a/Makefile b/Makefile
index f8bccfab5e..71924d0c43 100644
--- a/Makefile
+++ b/Makefile
@@ -1194,6 +1194,7 @@ BUILTIN_OBJS += builtin/remote-fd.o
 BUILTIN_OBJS += builtin/remote.o
 BUILTIN_OBJS += builtin/repack.o
 BUILTIN_OBJS += builtin/replace.o
+BUILTIN_OBJS += builtin/replay.o
 BUILTIN_OBJS += builtin/rerere.o
 BUILTIN_OBJS += builtin/reset.o
 BUILTIN_OBJS += builtin/rev-list.o
diff --git a/builtin.h b/builtin.h
index 40e9ecc848..0c1915c4c9 100644
--- a/builtin.h
+++ b/builtin.h
@@ -207,6 +207,7 @@ int cmd_remote(int argc, const char **argv, const char *prefix);
 int cmd_remote_ext(int argc, const char **argv, const char *prefix);
 int cmd_remote_fd(int argc, const char **argv, const char *prefix);
 int cmd_repack(int argc, const char **argv, const char *prefix);
+int cmd_replay(int argc, const char **argv, const char *prefix);
 int cmd_rerere(int argc, const char **argv, const char *prefix);
 int cmd_reset(int argc, const char **argv, const char *prefix);
 int cmd_restore(int argc, const char **argv, const char *prefix);
diff --git a/builtin/replay.c b/builtin/replay.c
new file mode 100644
index 0000000000..ed970fa057
--- /dev/null
+++ b/builtin/replay.c
@@ -0,0 +1,148 @@
+/*
+ * "git replay" builtin command
+ *
+ * Copyright (c) 2022 Edmundo Carmona Antoranz
+ * Released under the terms of GPLv2
+ */
+
+#include "builtin.h"
+#include "revision.h"
+#include "commit.h"
+#include "cache.h"
+
+static struct commit **new_commits;
+static unsigned long mappings_size = 0;
+static struct commit_list *old_commits = NULL;
+
+static unsigned int replay_indexof(struct commit *commit,
+				   struct commit_list *list)
+{
+	int res;
+	if (list == NULL)
+		return -1;
+	if (!oidcmp(&list->item->object.oid,
+		    &commit->object.oid))
+		return 0;
+	res = replay_indexof(commit, list->next);
+	return res < 0 ? res : res + 1;
+}
+
+static struct commit *replay_find_commit(const char *name)
+{
+	struct commit *commit = lookup_commit_reference_by_name(name);
+	if (!commit)
+		die(_("no such branch/commit '%s'"), name);
+	return commit;
+}
+
+static struct commit* replay_commit(struct commit * orig_commit)
+{
+	struct pretty_print_context ctx = {0};
+	struct strbuf body = STRBUF_INIT;
+	struct strbuf author = STRBUF_INIT;
+	struct strbuf committer = STRBUF_INIT;
+	struct object_id new_commit_oid;
+	struct commit *new_commit;
+
+	struct commit_list *new_parents_head = NULL;
+	struct commit_list **new_parents = &new_parents_head;
+	struct commit_list *parents = orig_commit->parents;
+	while (parents) {
+		struct commit *parent = parents->item;
+		int commit_index;
+		struct commit *new_parent;
+
+		commit_index = replay_indexof(parent, old_commits);
+
+		if (commit_index < 0)
+			 // won't be replayed, use the original parent
+			new_parent = parent;
+		else {
+			// it might have been translated already
+			if (!new_commits[commit_index])
+				new_commits[commit_index] = replay_commit(parent);
+			new_parent = new_commits[commit_index];
+		}
+		new_parents = commit_list_append(new_parent, new_parents);
+		parents = parents->next;
+	}
+
+	format_commit_message(orig_commit, "%B", &body, &ctx);
+	// TODO timezones
+	format_commit_message(orig_commit, "%an <%ae> %at +0000", &author, &ctx);
+	// TODO consider committer (control with an option)
+	format_commit_message(orig_commit, "%cn <%ce> %ct +0000", &committer, &ctx);
+
+	commit_tree_extended(body.buf,
+			     body.len,
+			     get_commit_tree_oid(orig_commit),
+			     new_parents_head,
+			     &new_commit_oid,
+			     author.buf,
+			     committer.buf,
+			     NULL, NULL);
+
+	new_commit = lookup_commit_or_die(&new_commit_oid,
+					  "new commit");
+
+	strbuf_release(&author);
+	strbuf_release(&body);
+	strbuf_release(&committer);
+
+	return new_commit;
+}
+
+static struct commit* replay(struct commit *new_base, struct commit *old_base,
+		      struct commit *tip)
+{
+	struct rev_info revs;
+	struct commit *commit;
+
+	init_revisions(&revs, NULL);
+
+	old_base->object.flags |= UNINTERESTING;
+	add_pending_object(&revs, &old_base->object, "old-base");
+	add_pending_object(&revs, &tip->object, "tip");
+
+	if (prepare_revision_walk(&revs))
+		die("Could not get revisions to replay");
+
+	while ((commit = get_revision(&revs)) != NULL)
+		commit_list_insert(commit, &old_commits);
+
+	// save the mapping between the new and the old base
+	commit_list_insert(old_base, &old_commits);
+	mappings_size = commit_list_count(old_commits);
+	new_commits = calloc(mappings_size, sizeof(struct commit));
+	new_commits[replay_indexof(old_base, old_commits)] = new_base;
+
+	return replay_commit(tip);
+}
+
+
+int cmd_replay(int argc, const char **argv, const char *prefix)
+{
+	struct commit *new_base;
+	struct commit *old_base;
+	struct commit *tip;
+	struct commit *new_tip;
+
+	if (argc < 4) {
+		die("Not enough parameters");
+	}
+
+	new_base = replay_find_commit(argv[1]);
+	old_base = replay_find_commit(argv[2]);
+	tip = replay_find_commit(argv[3]);
+
+	if (oidcmp(get_commit_tree_oid(new_base),
+		   get_commit_tree_oid(old_base)))
+		die("The old and the new base do not have the same tree");
+
+	// get the list of revisions between old_base and tip
+	new_tip = replay(new_base, old_base, tip);
+
+	printf("%s\n", oid_to_hex(&new_tip->object.oid));
+
+	return 0;
+}
diff --git a/git.c b/git.c
index 3d8e48cf55..14de8d666f 100644
--- a/git.c
+++ b/git.c
@@ -590,6 +590,7 @@ static struct cmd_struct commands[] = {
 	{ "remote-fd", cmd_remote_fd, NO_PARSEOPT },
 	{ "repack", cmd_repack, RUN_SETUP },
 	{ "replace", cmd_replace, RUN_SETUP },
+	{ "replay", cmd_replay, RUN_SETUP },
 	{ "rerere", cmd_rerere, RUN_SETUP },
 	{ "reset", cmd_reset, RUN_SETUP },
 	{ "restore", cmd_restore, RUN_SETUP | NEED_WORK_TREE },
-- 
2.35.1
Next: Junio C Hamano
Message 1 of 25 in “[RFC] introducing git replay”
  1. Edmundo Carmona AntoranzApr 13, 2022
  2. Junio C HamanoApr 13, 2022
  3. Edmundo Carmona AntoranzApr 15, 2022
  4. Junio C HamanoApr 15, 2022
  5. Edmundo Carmona AntoranzApr 16, 2022
  6. Junio C HamanoApr 16, 2022
  7. Edmundo Carmona AntoranzApr 16, 2022
  8. Elijah NewrenApr 17, 2022
  9. Edmundo Carmona AntoranzApr 17, 2022
  10. Martin von ZweigbergkApr 17, 2022
  11. Edmundo Carmona AntoranzApr 18, 2022
  12. Sergey OrganovApr 18, 2022
  13. Elijah NewrenApr 18, 2022
  14. Sergey OrganovApr 18, 2022
  15. Tao KlerksApr 20, 2022
  16. Elijah NewrenApr 21, 2022
  17. rsbecker@nexbridge.comApr 13, 2022
  18. Edmundo Carmona AntoranzApr 13, 2022
  19. Edmundo Carmona AntoranzApr 13, 2022
  20. Phillip SusiApr 13, 2022
  21. Edmundo Carmona AntoranzApr 13, 2022
  22. Ævar Arnfjörð BjarmasonApr 13, 2022
  23. Junio C HamanoApr 13, 2022
  24. Edmundo Carmona AntoranzApr 13, 2022
  25. Eric SunshineApr 13, 2022

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.