aboutsummaryrefslogtreecommitdiff
path: root/builtin
diff options
context:
space:
mode:
authorJunio C Hamano <gitster@pobox.com>2026-02-09 12:09:09 -0800
committerJunio C Hamano <gitster@pobox.com>2026-02-09 12:09:09 -0800
commit7bf3785d0973d229fa21a76122c7e4735a2b1ffb (patch)
tree9ee2a41b83f2f18bf0cb63feec23320eda20cc5e /builtin
parent3e0db84c88c57e70ac8be8c196dfa92c5d656fbc (diff)
parentd205234cb05a5e330c0f7f5b3ea764533a74d69e (diff)
downloadgit-7bf3785d0973d229fa21a76122c7e4735a2b1ffb.tar.xz
Merge branch 'ps/history'
"git history" history rewriting UI. * ps/history: builtin/history: implement "reword" subcommand builtin: add new "history" command wt-status: provide function to expose status for trees replay: support updating detached HEAD replay: support empty commit ranges replay: small set of cleanups builtin/replay: move core logic into "libgit.a" builtin/replay: extract core logic to replay revisions
Diffstat (limited to 'builtin')
-rw-r--r--builtin/history.c427
-rw-r--r--builtin/replay.c373
2 files changed, 459 insertions, 341 deletions
diff --git a/builtin/history.c b/builtin/history.c
new file mode 100644
index 0000000000..8dcb9a6046
--- /dev/null
+++ b/builtin/history.c
@@ -0,0 +1,427 @@
+#define USE_THE_REPOSITORY_VARIABLE
+
+#include "builtin.h"
+#include "commit.h"
+#include "commit-reach.h"
+#include "config.h"
+#include "editor.h"
+#include "environment.h"
+#include "gettext.h"
+#include "hex.h"
+#include "parse-options.h"
+#include "refs.h"
+#include "replay.h"
+#include "revision.h"
+#include "sequencer.h"
+#include "strvec.h"
+#include "tree.h"
+#include "wt-status.h"
+
+#define GIT_HISTORY_REWORD_USAGE \
+ N_("git history reword <commit> [--ref-action=(branches|head|print)]")
+
+static void change_data_free(void *util, const char *str UNUSED)
+{
+ struct wt_status_change_data *d = util;
+ free(d->rename_source);
+ free(d);
+}
+
+static int fill_commit_message(struct repository *repo,
+ const struct object_id *old_tree,
+ const struct object_id *new_tree,
+ const char *default_message,
+ const char *action,
+ struct strbuf *out)
+{
+ const char *path = git_path_commit_editmsg();
+ const char *hint =
+ _("Please enter the commit message for the %s changes."
+ " Lines starting\nwith '%s' will be ignored, and an"
+ " empty message aborts the commit.\n");
+ struct wt_status s;
+
+ strbuf_addstr(out, default_message);
+ strbuf_addch(out, '\n');
+ strbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);
+ write_file_buf(path, out->buf, out->len);
+
+ wt_status_prepare(repo, &s);
+ FREE_AND_NULL(s.branch);
+ s.ahead_behind_flags = AHEAD_BEHIND_QUICK;
+ s.commit_template = 1;
+ s.colopts = 0;
+ s.display_comment_prefix = 1;
+ s.hints = 0;
+ s.use_color = 0;
+ s.whence = FROM_COMMIT;
+ s.committable = 1;
+
+ s.fp = fopen(git_path_commit_editmsg(), "a");
+ if (!s.fp)
+ return error_errno(_("could not open '%s'"), git_path_commit_editmsg());
+
+ wt_status_collect_changes_trees(&s, old_tree, new_tree);
+ wt_status_print(&s);
+ wt_status_collect_free_buffers(&s);
+ string_list_clear_func(&s.change, change_data_free);
+
+ strbuf_reset(out);
+ if (launch_editor(path, out, NULL)) {
+ fprintf(stderr, _("Aborting commit as launching the editor failed.\n"));
+ return -1;
+ }
+ strbuf_stripspace(out, comment_line_str);
+
+ cleanup_message(out, COMMIT_MSG_CLEANUP_ALL, 0);
+
+ if (!out->len) {
+ fprintf(stderr, _("Aborting commit due to empty commit message.\n"));
+ return -1;
+ }
+
+ return 0;
+}
+
+static int commit_tree_with_edited_message(struct repository *repo,
+ const char *action,
+ struct commit *original,
+ struct commit **out)
+{
+ const char *exclude_gpgsig[] = {
+ /* We reencode the message, so the encoding needs to be stripped. */
+ "encoding",
+ /* We need to strip signatures as those will become invalid. */
+ "gpgsig",
+ "gpgsig-sha256",
+ NULL,
+ };
+ const char *original_message, *original_body, *ptr;
+ struct commit_extra_header *original_extra_headers = NULL;
+ struct strbuf commit_message = STRBUF_INIT;
+ struct object_id rewritten_commit_oid;
+ struct object_id original_tree_oid;
+ struct object_id parent_tree_oid;
+ char *original_author = NULL;
+ struct commit *parent;
+ size_t len;
+ int ret;
+
+ original_tree_oid = repo_get_commit_tree(repo, original)->object.oid;
+
+ parent = original->parents ? original->parents->item : NULL;
+ if (parent) {
+ if (repo_parse_commit(repo, parent)) {
+ ret = error(_("unable to parse parent commit %s"),
+ oid_to_hex(&parent->object.oid));
+ goto out;
+ }
+
+ parent_tree_oid = repo_get_commit_tree(repo, parent)->object.oid;
+ } else {
+ oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
+ }
+
+ /* We retain authorship of the original commit. */
+ original_message = repo_logmsg_reencode(repo, original, NULL, NULL);
+ ptr = find_commit_header(original_message, "author", &len);
+ if (ptr)
+ original_author = xmemdupz(ptr, len);
+ find_commit_subject(original_message, &original_body);
+
+ ret = fill_commit_message(repo, &parent_tree_oid, &original_tree_oid,
+ original_body, action, &commit_message);
+ if (ret < 0)
+ goto out;
+
+ original_extra_headers = read_commit_extra_headers(original, exclude_gpgsig);
+
+ ret = commit_tree_extended(commit_message.buf, commit_message.len, &original_tree_oid,
+ original->parents, &rewritten_commit_oid, original_author,
+ NULL, NULL, original_extra_headers);
+ if (ret < 0)
+ goto out;
+
+ *out = lookup_commit_or_die(&rewritten_commit_oid, "rewritten commit");
+
+out:
+ free_commit_extra_headers(original_extra_headers);
+ strbuf_release(&commit_message);
+ free(original_author);
+ return ret;
+}
+
+enum ref_action {
+ REF_ACTION_DEFAULT,
+ REF_ACTION_BRANCHES,
+ REF_ACTION_HEAD,
+ REF_ACTION_PRINT,
+};
+
+static int parse_ref_action(const struct option *opt, const char *value, int unset)
+{
+ enum ref_action *action = opt->value;
+
+ BUG_ON_OPT_NEG_NOARG(unset, value);
+ if (!strcmp(value, "branches")) {
+ *action = REF_ACTION_BRANCHES;
+ } else if (!strcmp(value, "head")) {
+ *action = REF_ACTION_HEAD;
+ } else if (!strcmp(value, "print")) {
+ *action = REF_ACTION_PRINT;
+ } else {
+ return error(_("%s expects one of 'branches', 'head' or 'print'"),
+ opt->long_name);
+ }
+
+ return 0;
+}
+
+static int handle_reference_updates(enum ref_action action,
+ struct repository *repo,
+ struct commit *original,
+ struct commit *rewritten,
+ const char *reflog_msg)
+{
+ const struct name_decoration *decoration;
+ struct replay_revisions_options opts = { 0 };
+ struct replay_result result = { 0 };
+ struct ref_transaction *transaction = NULL;
+ struct strvec args = STRVEC_INIT;
+ struct strbuf err = STRBUF_INIT;
+ struct commit *head = NULL;
+ struct rev_info revs;
+ char hex[GIT_MAX_HEXSZ + 1];
+ bool detached_head;
+ int head_flags = 0;
+ int ret;
+
+ refs_read_ref_full(get_main_ref_store(repo), "HEAD",
+ RESOLVE_REF_NO_RECURSE, NULL, &head_flags);
+ detached_head = !(head_flags & REF_ISSYMREF);
+
+ repo_init_revisions(repo, &revs, NULL);
+ strvec_push(&args, "ignored");
+ strvec_push(&args, "--reverse");
+ strvec_push(&args, "--topo-order");
+ strvec_push(&args, "--full-history");
+
+ /* We only want to see commits that are descendants of the old commit. */
+ strvec_pushf(&args, "--ancestry-path=%s",
+ oid_to_hex(&original->object.oid));
+
+ /*
+ * Ancestry path may also show ancestors of the old commit, but we
+ * don't want to see those, either.
+ */
+ strvec_pushf(&args, "^%s", oid_to_hex(&original->object.oid));
+
+ /*
+ * When we're asked to update HEAD we need to verify that the commit
+ * that we want to rewrite is actually an ancestor of it and, if so,
+ * update it. Otherwise we'll update (or print) all descendant
+ * branches.
+ */
+ if (action == REF_ACTION_HEAD) {
+ struct commit_list *from_list = NULL;
+
+ head = lookup_commit_reference_by_name("HEAD");
+ if (!head) {
+ ret = error(_("cannot look up HEAD"));
+ goto out;
+ }
+
+ commit_list_insert(original, &from_list);
+ ret = repo_is_descendant_of(repo, head, from_list);
+ free_commit_list(from_list);
+
+ if (ret < 0) {
+ ret = error(_("cannot determine descendance"));
+ goto out;
+ } else if (!ret) {
+ ret = error(_("rewritten commit must be an ancestor "
+ "of HEAD when using --ref-action=head"));
+ goto out;
+ }
+
+ strvec_push(&args, "HEAD");
+ } else {
+ strvec_push(&args, "--branches");
+ strvec_push(&args, "HEAD");
+ }
+
+ setup_revisions_from_strvec(&args, &revs, NULL);
+ if (args.nr != 1)
+ BUG("revisions were set up with invalid argument");
+
+ opts.onto = oid_to_hex_r(hex, &rewritten->object.oid);
+
+ ret = replay_revisions(&revs, &opts, &result);
+ if (ret)
+ goto out;
+
+ switch (action) {
+ case REF_ACTION_BRANCHES:
+ case REF_ACTION_HEAD:
+ transaction = ref_store_transaction_begin(get_main_ref_store(repo), 0, &err);
+ if (!transaction) {
+ ret = error(_("failed to begin ref transaction: %s"), err.buf);
+ goto out;
+ }
+
+ for (size_t i = 0; i < result.updates_nr; i++) {
+ ret = ref_transaction_update(transaction,
+ result.updates[i].refname,
+ &result.updates[i].new_oid,
+ &result.updates[i].old_oid,
+ NULL, NULL, 0, reflog_msg, &err);
+ if (ret) {
+ ret = error(_("failed to update ref '%s': %s"),
+ result.updates[i].refname, err.buf);
+ goto out;
+ }
+ }
+
+ /*
+ * `replay_revisions()` only updates references that are
+ * ancestors of `rewritten`, so we need to manually
+ * handle updating references that point to `original`.
+ */
+ for (decoration = get_name_decoration(&original->object);
+ decoration;
+ decoration = decoration->next)
+ {
+ if (decoration->type != DECORATION_REF_LOCAL &&
+ decoration->type != DECORATION_REF_HEAD)
+ continue;
+
+ if (action == REF_ACTION_HEAD &&
+ decoration->type != DECORATION_REF_HEAD)
+ continue;
+
+ /*
+ * We only need to update HEAD separately in case it's
+ * detached. If it's not we'd already update the branch
+ * it is pointing to.
+ */
+ if (action == REF_ACTION_BRANCHES &&
+ decoration->type == DECORATION_REF_HEAD &&
+ !detached_head)
+ continue;
+
+ ret = ref_transaction_update(transaction,
+ decoration->name,
+ &rewritten->object.oid,
+ &original->object.oid,
+ NULL, NULL, 0, reflog_msg, &err);
+ if (ret) {
+ ret = error(_("failed to update ref '%s': %s"),
+ decoration->name, err.buf);
+ goto out;
+ }
+ }
+
+ if (ref_transaction_commit(transaction, &err)) {
+ ret = error(_("failed to commit ref transaction: %s"), err.buf);
+ goto out;
+ }
+
+ break;
+ case REF_ACTION_PRINT:
+ for (size_t i = 0; i < result.updates_nr; i++)
+ printf("update %s %s %s\n",
+ result.updates[i].refname,
+ oid_to_hex(&result.updates[i].new_oid),
+ oid_to_hex(&result.updates[i].old_oid));
+ break;
+ default:
+ BUG("unsupported ref action %d", action);
+ }
+
+ ret = 0;
+
+out:
+ ref_transaction_free(transaction);
+ replay_result_release(&result);
+ release_revisions(&revs);
+ strbuf_release(&err);
+ strvec_clear(&args);
+ return ret;
+}
+
+static int cmd_history_reword(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo)
+{
+ const char * const usage[] = {
+ GIT_HISTORY_REWORD_USAGE,
+ NULL,
+ };
+ enum ref_action action = REF_ACTION_DEFAULT;
+ struct option options[] = {
+ OPT_CALLBACK_F(0, "ref-action", &action, N_("<action>"),
+ N_("control ref update behavior (branches|head|print)"),
+ PARSE_OPT_NONEG, parse_ref_action),
+ OPT_END(),
+ };
+ struct strbuf reflog_msg = STRBUF_INIT;
+ struct commit *original, *rewritten;
+ int ret;
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ if (argc != 1) {
+ ret = error(_("command expects a single revision"));
+ goto out;
+ }
+ repo_config(repo, git_default_config, NULL);
+
+ if (action == REF_ACTION_DEFAULT)
+ action = REF_ACTION_BRANCHES;
+
+ original = lookup_commit_reference_by_name(argv[0]);
+ if (!original) {
+ ret = error(_("commit cannot be found: %s"), argv[0]);
+ goto out;
+ }
+
+ ret = commit_tree_with_edited_message(repo, "reworded", original, &rewritten);
+ if (ret < 0) {
+ ret = error(_("failed writing reworded commit"));
+ goto out;
+ }
+
+ strbuf_addf(&reflog_msg, "reword: updating %s", argv[0]);
+
+ ret = handle_reference_updates(action, repo, original, rewritten,
+ reflog_msg.buf);
+ if (ret < 0) {
+ ret = error(_("failed replaying descendants"));
+ goto out;
+ }
+
+ ret = 0;
+
+out:
+ strbuf_release(&reflog_msg);
+ return ret;
+}
+
+int cmd_history(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo)
+{
+ const char * const usage[] = {
+ GIT_HISTORY_REWORD_USAGE,
+ NULL,
+ };
+ parse_opt_subcommand_fn *fn = NULL;
+ struct option options[] = {
+ OPT_SUBCOMMAND("reword", &fn, cmd_history_reword),
+ OPT_END(),
+ };
+
+ argc = parse_options(argc, argv, prefix, options, usage, 0);
+ return fn(argc, argv, prefix, repo);
+}
diff --git a/builtin/replay.c b/builtin/replay.c
index 1960bbbee8..2cdde830a8 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"))
@@ -306,21 +71,11 @@ int cmd_replay(int argc,
const char *prefix,
struct repository *repo)
{
- const char *advance_name_opt = NULL;
- char *advance_name = NULL;
- struct commit *onto = NULL;
- const char *onto_name = NULL;
- int contained = 0;
+ 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 commit *last_commit = NULL;
- struct commit *commit;
- struct merge_options merge_opt;
- struct merge_result result;
- struct strset *update_refs = NULL;
- kh_oid_map_t *replayed_commits;
struct ref_transaction *transaction = NULL;
struct strbuf transaction_err = STRBUF_INIT;
struct strbuf reflog_msg = STRBUF_INIT;
@@ -333,13 +88,13 @@ int cmd_replay(int argc,
NULL
};
struct option replay_options[] = {
- OPT_STRING(0, "advance", &advance_name_opt,
+ OPT_STRING(0, "advance", &opts.advance,
N_("branch"),
N_("make replay advance given branch")),
- OPT_STRING(0, "onto", &onto_name,
+ OPT_STRING(0, "onto", &opts.onto,
N_("revision"),
N_("replay onto given commit")),
- OPT_BOOL(0, "contained", &contained,
+ OPT_BOOL(0, "contained", &opts.contained,
N_("update all branches that point at commits in <revision-range>")),
OPT_STRING(0, "ref-action", &ref_action,
N_("mode"),
@@ -350,19 +105,19 @@ int cmd_replay(int argc,
argc = parse_options(argc, argv, prefix, replay_options, replay_usage,
PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
- if (!onto_name && !advance_name_opt) {
+ if (!opts.onto && !opts.advance) {
error(_("option --onto or --advance is mandatory"));
usage_with_options(replay_usage, replay_options);
}
- die_for_incompatible_opt2(!!advance_name_opt, "--advance",
- contained, "--contained");
+ die_for_incompatible_opt2(!!opts.advance, "--advance",
+ opts.contained, "--contained");
+ die_for_incompatible_opt2(!!opts.advance, "--advance",
+ !!opts.onto, "--onto");
/* Parse ref action mode from command line or config */
ref_mode = get_ref_action_mode(repo, ref_action);
- advance_name = xstrdup_or_null(advance_name_opt);
-
repo_init_revisions(repo, &revs, prefix);
/*
@@ -414,18 +169,19 @@ int cmd_replay(int argc,
revs.simplify_history = 0;
}
- set_up_replay_mode(repo, &revs.cmdline,
- onto_name, &advance_name,
- &onto, &update_refs);
-
- /* FIXME: Should allow replaying commits with the first as a root commit */
+ ret = replay_revisions(&revs, &opts, &result);
+ if (ret)
+ goto cleanup;
/* Build reflog message */
- if (advance_name_opt)
- strbuf_addf(&reflog_msg, "replay --advance %s", advance_name_opt);
- else
- strbuf_addf(&reflog_msg, "replay --onto %s",
- oid_to_hex(&onto->object.oid));
+ 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) {
@@ -438,78 +194,19 @@ int cmd_replay(int argc,
}
}
- if (prepare_revision_walk(&revs) < 0) {
- ret = error(_("error preparing revisions"));
- goto cleanup;
- }
-
- 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_name)
- continue;
- decoration = get_name_decoration(&commit->object);
- if (!decoration)
- continue;
- while (decoration) {
- if (decoration->type == DECORATION_REF_LOCAL &&
- (contained || strset_contains(update_refs,
- decoration->name))) {
- if (handle_ref_update(ref_mode, transaction,
- decoration->name,
- &last_commit->object.oid,
- &commit->object.oid,
- reflog_msg.buf,
- &transaction_err) < 0) {
- ret = error(_("failed to update ref '%s': %s"),
- decoration->name, transaction_err.buf);
- goto cleanup;
- }
- }
- decoration = decoration->next;
- }
- }
-
- /* In --advance mode, advance the target ref */
- if (result.clean == 1 && advance_name) {
- if (handle_ref_update(ref_mode, transaction, advance_name,
- &last_commit->object.oid,
- &onto->object.oid,
- reflog_msg.buf,
- &transaction_err) < 0) {
+ 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"),
- advance_name, transaction_err.buf);
+ result.updates[i].refname, transaction_err.buf);
goto cleanup;
}
}
/* Commit the ref transaction if we have one */
- if (transaction && result.clean == 1) {
+ if (transaction) {
if (ref_transaction_commit(transaction, &transaction_err)) {
ret = error(_("failed to commit ref transaction: %s"),
transaction_err.buf);
@@ -517,24 +214,18 @@ int cmd_replay(int argc,
}
}
- merge_finalize(&merge_opt, &result);
- kh_destroy_oid_map(replayed_commits);
- if (update_refs) {
- strset_clear(update_refs);
- free(update_refs);
- }
- ret = result.clean;
+ ret = 0;
cleanup:
if (transaction)
ref_transaction_free(transaction);
+ replay_result_release(&result);
strbuf_release(&transaction_err);
strbuf_release(&reflog_msg);
release_revisions(&revs);
- free(advance_name);
/* Return */
if (ret < 0)
exit(128);
- return ret ? 0 : 1;
+ return ret;
}