/*
 * "git replay" builtin command
 */

#include "git-compat-util.h"

#include "builtin.h"
#include "config.h"
#include "hex.h"
#include "object-name.h"
#include "parse-options.h"
#include "refs.h"
#include "replay.h"
#include "revision.h"

enum ref_action_mode {
	REF_ACTION_UPDATE,
	REF_ACTION_PRINT,
};

static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)
{
	if (!ref_action || !strcmp(ref_action, "update"))
		return REF_ACTION_UPDATE;
	if (!strcmp(ref_action, "print"))
		return REF_ACTION_PRINT;
	die(_("invalid %s value: '%s'"), source, ref_action);
}

static enum ref_action_mode get_ref_action_mode(struct repository *repo, const char *ref_action)
{
	const char *config_value = NULL;

	/* Command line option takes precedence */
	if (ref_action)
		return parse_ref_action_mode(ref_action, "--ref-action");

	/* Check config value */
	if (!repo_config_get_string_tmp(repo, "replay.refAction", &config_value))
		return parse_ref_action_mode(config_value, "replay.refAction");

	/* Default to update mode */
	return REF_ACTION_UPDATE;
}

static int handle_ref_update(enum ref_action_mode mode,
			     struct ref_transaction *transaction,
			     const char *refname,
			     const struct object_id *new_oid,
			     const struct object_id *old_oid,
			     const char *reflog_msg,
			     struct strbuf *err)
{
	switch (mode) {
	case REF_ACTION_PRINT:
		printf("update %s %s %s\n",
		       refname,
		       oid_to_hex(new_oid),
		       oid_to_hex(old_oid));
		return 0;
	case REF_ACTION_UPDATE:
		return ref_transaction_update(transaction, refname, new_oid, old_oid,
					      NULL, NULL, 0, reflog_msg, err);
	default:
		BUG("unknown ref_action_mode %d", mode);
	}
}

int cmd_replay(int argc,
	       const char **argv,
	       const char *prefix,
	       struct repository *repo)
{
	struct replay_revisions_options opts = { 0 };
	struct replay_result result = { 0 };
	const char *ref_action = NULL;
	enum ref_action_mode ref_mode;
	struct rev_info revs;
	struct ref_transaction *transaction = NULL;
	struct strbuf transaction_err = STRBUF_INIT;
	struct strbuf reflog_msg = STRBUF_INIT;
	int desired_reverse;
	int ret = 0;

	const char *const replay_usage[] = {
		N_("(EXPERIMENTAL!) git replay "
		   "([--contained] --onto <newbase> | --advance <branch> | --revert <branch>) "
		   "[--ref-action[=<mode>]] <revision-range>"),
		NULL
	};
	struct option replay_options[] = {
		OPT_STRING(0, "advance", &opts.advance,
			   N_("branch"),
			   N_("make replay advance given branch")),
		OPT_STRING(0, "onto", &opts.onto,
			   N_("revision"),
			   N_("replay onto given commit")),
		OPT_BOOL(0, "contained", &opts.contained,
			 N_("update all branches that point at commits in <revision-range>")),
		OPT_STRING(0, "revert", &opts.revert,
			   N_("branch"),
			   N_("revert commits onto given branch")),
		OPT_STRING(0, "ref-action", &ref_action,
			   N_("mode"),
			   N_("control ref update behavior (update|print)")),
		OPT_END()
	};

	argc = parse_options(argc, argv, prefix, replay_options, replay_usage,
			     PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);

	/* Exactly one mode must be specified */
	if (!opts.onto && !opts.advance && !opts.revert) {
		error(_("exactly one of --onto, --advance, or --revert is required"));
		usage_with_options(replay_usage, replay_options);
	}

	die_for_incompatible_opt3(!!opts.onto, "--onto",
				  !!opts.advance, "--advance",
				  !!opts.revert, "--revert");
	die_for_incompatible_opt2(!!opts.advance, "--advance",
				  opts.contained, "--contained");
	die_for_incompatible_opt2(!!opts.revert, "--revert",
				  opts.contained, "--contained");

	/* Parse ref action mode from command line or config */
	ref_mode = get_ref_action_mode(repo, ref_action);

	/*
	 * Cherry-pick/rebase need oldest-first ordering so that each
	 * replayed commit can build on its already-replayed parent.
	 * Revert needs newest-first ordering (like git revert) to
	 * reduce conflicts by peeling off changes from the top.
	 */
	desired_reverse = !opts.revert;

	repo_init_revisions(repo, &revs, prefix);

	/*
	 * Set desired values for rev walking options here. If they
	 * are changed by some user specified option in setup_revisions()
	 * below, we will detect that below and then warn.
	 *
	 * TODO: In the future we might want to either die(), or allow
	 * some options changing these values if we think they could
	 * be useful.
	 */
	revs.reverse = desired_reverse;
	revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
	revs.topo_order = 1;
	revs.simplify_history = 0;

	argc = setup_revisions(argc, argv, &revs, NULL);
	if (argc > 1) {
		ret = error(_("unrecognized argument: %s"), argv[1]);
		goto cleanup;
	}

	/*
	 * Detect and warn if we override some user specified rev
	 * walking options.
	 */
	if (revs.reverse != desired_reverse) {
		warning(_("some rev walking options will be overridden as "
			  "'%s' bit in 'struct rev_info' will be forced"),
			"reverse");
		revs.reverse = desired_reverse;
	}
	if (revs.sort_order != REV_SORT_IN_GRAPH_ORDER) {
		warning(_("some rev walking options will be overridden as "
			  "'%s' bit in 'struct rev_info' will be forced"),
			"sort_order");
		revs.sort_order = REV_SORT_IN_GRAPH_ORDER;
	}
	if (revs.topo_order != 1) {
		warning(_("some rev walking options will be overridden as "
			  "'%s' bit in 'struct rev_info' will be forced"),
			"topo_order");
		revs.topo_order = 1;
	}
	if (revs.simplify_history != 0) {
		warning(_("some rev walking options will be overridden as "
			  "'%s' bit in 'struct rev_info' will be forced"),
			"simplify_history");
		revs.simplify_history = 0;
	}

	ret = replay_revisions(&revs, &opts, &result);
	if (ret)
		goto cleanup;

	/* Build reflog message */
	if (opts.revert) {
		strbuf_addf(&reflog_msg, "replay --revert %s", opts.revert);
	} else if (opts.advance) {
		strbuf_addf(&reflog_msg, "replay --advance %s", opts.advance);
	} else {
		struct object_id oid;
		if (repo_get_oid_committish(repo, opts.onto, &oid))
			BUG("--onto commit should have been resolved beforehand already");
		strbuf_addf(&reflog_msg, "replay --onto %s", oid_to_hex(&oid));
	}

	/* Initialize ref transaction if using update mode */
	if (ref_mode == REF_ACTION_UPDATE) {
		transaction = ref_store_transaction_begin(get_main_ref_store(repo),
							  0, &transaction_err);
		if (!transaction) {
			ret = error(_("failed to begin ref transaction: %s"),
				    transaction_err.buf);
			goto cleanup;
		}
	}

	for (size_t i = 0; i < result.updates_nr; i++) {
		ret = handle_ref_update(ref_mode, transaction, result.updates[i].refname,
					&result.updates[i].new_oid, &result.updates[i].old_oid,
					reflog_msg.buf, &transaction_err);
		if (ret) {
			ret = error(_("failed to update ref '%s': %s"),
				    result.updates[i].refname, transaction_err.buf);
			goto cleanup;
		}
	}

	/* Commit the ref transaction if we have one */
	if (transaction) {
		if (ref_transaction_commit(transaction, &transaction_err)) {
			ret = error(_("failed to commit ref transaction: %s"),
				    transaction_err.buf);
			goto cleanup;
		}
	}

	ret = 0;

cleanup:
	if (transaction)
		ref_transaction_free(transaction);
	replay_result_release(&result);
	strbuf_release(&transaction_err);
	strbuf_release(&reflog_msg);
	release_revisions(&revs);

	/* Return */
	if (ret < 0)
		exit(128);
	return ret;
}
