Subject: [PATCH/RFC/GSoC 06/17] rebase-am: introduce am backend for builtin rebase

From: Paul Tan <[email-masked]>
Date: Sat, 12 Mar 2016 18:46:26 +0800

Since 7f59dbb (Rewrite rebase to use git-format-patch piped to git-am.,
2005-11-14), git-rebase will by default use "git am" to rebase commits.
This is done by first checking out to the new base commit, generating a
series of patches with the commits to replay, and then applying them
with git-am. Finally, if orig_head is a branch, it is updated to point
to the tip of the new rebased commit history.

Implement a skeletal version of this method of rebasing commits by
introducing a new rebase-am backend for our builtin-rebase. This
skeletal version can only call git-format-patch and git-am to perform a
rebase, and is unable to resume from a failed rebase. Subsequent
patches will re-implement all the missing features.

The symmetric difference between upstream...orig_head is used because in
a later patch, we will add an additional exclusion revision in order to
handle fork points correctly.  See b6266dc (rebase--am: use
--cherry-pick instead of --ignore-if-in-upstream, 2014-07-15).

The initial steps of checking out the new base commit, and the final
cleanup steps of updating refs are common between the am backend and
merge backend. As such, we implement the common setup and teardown
sequence in the shared functions rebase_common_setup() and
rebase_common_finish(), so we can share code with the merge backend when
it is implemented in a later patch.

Signed-off-by: Paul Tan <[email-masked]>
---
 Makefile         |   1 +
 builtin/rebase.c |  25 +++++++++++++
 rebase-am.c      | 110 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
 rebase-am.h      |  22 +++++++++++
 rebase-common.c  |  81 ++++++++++++++++++++++++++++++++++++++++
 rebase-common.h  |   6 +++
 6 files changed, 245 insertions(+)
 create mode 100644 rebase-am.c
 create mode 100644 rebase-am.h

diff --git a/Makefile b/Makefile
index b29c672..a2618ea 100644
--- a/Makefile
+++ b/Makefile
@@ -779,6 +779,7 @@ LIB_OBJS += prompt.o
 LIB_OBJS += quote.o
 LIB_OBJS += reachable.o
 LIB_OBJS += read-cache.o
+LIB_OBJS += rebase-am.o
 LIB_OBJS += rebase-common.o
 LIB_OBJS += reflog-walk.o
 LIB_OBJS += refs.o
diff --git a/builtin/rebase.c b/builtin/rebase.c
index 40176ca..ec63d3b 100644
--- a/builtin/rebase.c
+++ b/builtin/rebase.c
@@ -8,6 +8,22 @@
 #include "remote.h"
 #include "branch.h"
 #include "refs.h"
+#include "rebase-am.h"
+
+enum rebase_type {
+	REBASE_TYPE_NONE = 0,
+	REBASE_TYPE_AM
+};
+
+static const char *rebase_dir(enum rebase_type type)
+{
+	switch (type) {
+	case REBASE_TYPE_AM:
+		return git_path_rebase_am_dir();
+	default:
+		die("BUG: invalid rebase_type %d", type);
+	}
+}
 
 /**
  * Used by get_curr_branch_upstream_name() as a for_each_remote() callback to
@@ -208,6 +224,15 @@ int cmd_rebase(int argc, const char **argv, const char *prefix)
 			die(_("Failed to resolve '%s' as a valid revision."), "HEAD");
 	}
 
+	/* Run the appropriate rebase backend */
+	{
+		struct rebase_am state;
+		rebase_am_init(&state, rebase_dir(REBASE_TYPE_AM));
+		rebase_options_swap(&state.opts, &rebase_opts);
+		rebase_am_run(&state);
+		rebase_am_release(&state);
+	}
+
 	rebase_options_release(&rebase_opts);
 	return 0;
 }
diff --git a/rebase-am.c b/rebase-am.c
new file mode 100644
index 0000000..53e8798
--- /dev/null
+++ b/rebase-am.c
@@ -0,0 +1,110 @@
+#include "cache.h"
+#include "rebase-am.h"
+#include "run-command.h"
+
+GIT_PATH_FUNC(git_path_rebase_am_dir, "rebase-apply");
+
+void rebase_am_init(struct rebase_am *state, const char *dir)
+{
+	if (!dir)
+		dir = git_path_rebase_am_dir();
+	rebase_options_init(&state->opts);
+	state->dir = xstrdup(dir);
+}
+
+void rebase_am_release(struct rebase_am *state)
+{
+	rebase_options_release(&state->opts);
+	free(state->dir);
+}
+
+int rebase_am_in_progress(const struct rebase_am *state)
+{
+	const char *dir = state ? state->dir : git_path_rebase_am_dir();
+	struct stat st;
+
+	return !lstat(dir, &st) && S_ISDIR(st.st_mode);
+}
+
+int rebase_am_load(struct rebase_am *state)
+{
+	if (rebase_options_load(&state->opts, state->dir) < 0)
+		return -1;
+
+	return 0;
+}
+
+static int run_format_patch(const char *patches, const struct object_id *left,
+		const struct object_id *right)
+{
+	struct child_process cp = CHILD_PROCESS_INIT;
+	int ret;
+
+	cp.git_cmd = 1;
+	cp.out = xopen(patches, O_WRONLY | O_CREAT, 0777);
+	argv_array_push(&cp.args, "format-patch");
+	argv_array_push(&cp.args, "-k");
+	argv_array_push(&cp.args, "--stdout");
+	argv_array_push(&cp.args, "--full-index");
+	argv_array_push(&cp.args, "--cherry-pick");
+	argv_array_push(&cp.args, "--right-only");
+	argv_array_push(&cp.args, "--src-prefix=a/");
+	argv_array_push(&cp.args, "--dst-prefix=b/");
+	argv_array_push(&cp.args, "--no-renames");
+	argv_array_push(&cp.args, "--no-cover-letter");
+	argv_array_pushf(&cp.args, "%s...%s", oid_to_hex(left), oid_to_hex(right));
+
+	ret = run_command(&cp);
+	close(cp.out);
+	return ret;
+}
+
+static int run_am(const struct rebase_am *state, const char *patches)
+{
+	struct child_process cp = CHILD_PROCESS_INIT;
+	int ret;
+
+	cp.git_cmd = 1;
+	cp.in = xopen(patches, O_RDONLY);
+	argv_array_push(&cp.args, "am");
+	argv_array_push(&cp.args, "--rebasing");
+	if (state->opts.resolvemsg)
+		argv_array_pushf(&cp.args, "--resolvemsg=%s", state->opts.resolvemsg);
+
+	ret = run_command(&cp);
+	close(cp.in);
+	return ret;
+}
+
+void rebase_am_run(struct rebase_am *state)
+{
+	char *patches;
+	int ret;
+
+	rebase_common_setup(&state->opts, state->dir);
+
+	patches = git_pathdup("rebased-patches");
+	ret = run_format_patch(patches, &state->opts.upstream, &state->opts.orig_head);
+	if (ret) {
+		unlink_or_warn(patches);
+		fprintf_ln(stderr, _("\ngit encountered an error while preparing the patches to replay\n"
+			"these revisions:\n"
+			"\n"
+			"    %s...%s\n"
+			"\n"
+			"As a result, git cannot rebase them."),
+				oid_to_hex(&state->opts.upstream),
+				oid_to_hex(&state->opts.orig_head));
+		exit(ret);
+	}
+
+	ret = run_am(state, patches);
+	unlink_or_warn(patches);
+	if (ret) {
+		rebase_options_save(&state->opts, state->dir);
+		exit(ret);
+	}
+
+	free(patches);
+	rebase_common_finish(&state->opts, state->dir);
+}
diff --git a/rebase-am.h b/rebase-am.h
new file mode 100644
index 0000000..0b4348c
--- /dev/null
+++ b/rebase-am.h
@@ -0,0 +1,22 @@
+#ifndef REBASE_AM_H
+#define REBASE_AM_H
+#include "rebase-common.h"
+
+const char *git_path_rebase_am_dir(void);
+
+struct rebase_am {
+	struct rebase_options opts;
+	char *dir;
+};
+
+void rebase_am_init(struct rebase_am *, const char *dir);
+
+void rebase_am_release(struct rebase_am *);
+
+int rebase_am_in_progress(const struct rebase_am *);
+
+int rebase_am_load(struct rebase_am *);
+
+void rebase_am_run(struct rebase_am *);
+
+#endif /* REBASE_AM_H */
diff --git a/rebase-common.c b/rebase-common.c
index 1835f08..8169fb6 100644
--- a/rebase-common.c
+++ b/rebase-common.c
@@ -1,5 +1,8 @@
 #include "cache.h"
 #include "rebase-common.h"
+#include "dir.h"
+#include "run-command.h"
+#include "refs.h"
 
 void rebase_options_init(struct rebase_options *opts)
 {
@@ -95,3 +98,81 @@ void rebase_options_save(const struct rebase_options *opts, const char *dir)
 	write_state_text(dir, "onto", oid_to_hex(&opts->onto));
 	write_state_text(dir, "orig-head", oid_to_hex(&opts->orig_head));
 }
+
+static int detach_head(const struct object_id *commit, const char *onto_name)
+{
+	struct child_process cp = CHILD_PROCESS_INIT;
+	int status;
+	const char *reflog_action = getenv("GIT_REFLOG_ACTION");
+	if (!reflog_action || !*reflog_action)
+		reflog_action = "rebase";
+	cp.git_cmd = 1;
+	argv_array_pushf(&cp.env_array, "GIT_REFLOG_ACTION=%s: checkout %s",
+			reflog_action, onto_name ? onto_name : oid_to_hex(commit));
+	argv_array_push(&cp.args, "checkout");
+	argv_array_push(&cp.args, "-q");
+	argv_array_push(&cp.args, "--detach");
+	argv_array_push(&cp.args, oid_to_hex(commit));
+	status = run_command(&cp);
+
+	/* reload cache as checkout will have modified it */
+	discard_cache();
+	read_cache();
+
+	return status;
+}
+
+void rebase_common_setup(struct rebase_options *opts, const char *dir)
+{
+	/* Detach HEAD and reset the tree */
+	printf_ln(_("First, rewinding head to replay your work on top of it..."));
+	if (detach_head(&opts->onto, opts->onto_name))
+		die(_("could not detach HEAD"));
+	update_ref("rebase", "ORIG_HEAD", opts->orig_head.hash, NULL, 0,
+			UPDATE_REFS_DIE_ON_ERR);
+}
+
+void rebase_common_destroy(struct rebase_options *opts, const char *dir)
+{
+	struct strbuf sb = STRBUF_INIT;
+	strbuf_addstr(&sb, dir);
+	remove_dir_recursively(&sb, 0);
+	strbuf_release(&sb);
+}
+
+static void move_to_original_branch(struct rebase_options *opts)
+{
+	struct strbuf sb = STRBUF_INIT;
+	struct object_id curr_head;
+
+	if (!opts->orig_refname || !starts_with(opts->orig_refname, "refs/"))
+		return;
+
+	if (get_sha1("HEAD", curr_head.hash) < 0)
+		die("get_sha1() failed");
+
+	strbuf_addf(&sb, "rebase finished: %s onto %s", opts->orig_refname, oid_to_hex(&opts->onto));
+	if (update_ref(sb.buf, opts->orig_refname, curr_head.hash, opts->orig_head.hash, 0, UPDATE_REFS_MSG_ON_ERR))
+		goto fail;
+
+	strbuf_reset(&sb);
+	strbuf_addf(&sb, "rebase finished: returning to %s", opts->orig_refname);
+	if (create_symref("HEAD", opts->orig_refname, sb.buf))
+		goto fail;
+
+	strbuf_release(&sb);
+
+	return;
+fail:
+	die(_("Could not move back to %s"), opts->orig_refname);
+}
+
+void rebase_common_finish(struct rebase_options *opts, const char *dir)
+{
+	const char *argv_gc_auto[] = {"gc", "--auto", NULL};
+
+	move_to_original_branch(opts);
+	close_all_packs();
+	run_command_v_opt(argv_gc_auto, RUN_GIT_CMD);
+	rebase_common_destroy(opts, dir);
+}
diff --git a/rebase-common.h b/rebase-common.h
index 051c056..067ad0b 100644
--- a/rebase-common.h
+++ b/rebase-common.h
@@ -24,4 +24,10 @@ int rebase_options_load(struct rebase_options *, const char *dir);
 
 void rebase_options_save(const struct rebase_options *, const char *dir);
 
+void rebase_common_setup(struct rebase_options *, const char *dir);
+
+void rebase_common_destroy(struct rebase_options *, const char *dir);
+
+void rebase_common_finish(struct rebase_options *, const char *dir);
+
 #endif /* REBASE_COMMON_H */
-- 
2.7.0