mirror of
https://github.com/git/git.git
synced 2026-01-11 21:33:13 +09:00
Implement a new "reword" subcommand for git-history(1). This subcommand
is similar to the user performing an interactive rebase with a single
commit changed to use the "reword" instruction.
The "reword" subcommand is built on top of the replay subsystem
instead of the sequencer. This leads to some major differences compared
to git-rebase(1):
- We do not check out the commit that is to be reworded and instead
perform the operation in-memory. This has the obvious benefit of
being significantly faster compared to git-rebase(1), but even more
importantly it allows the user to rewrite history even if there are
local changes in the working tree or in the index.
- We do not execute any hooks, even though we leave some room for
changing this in the future.
- By default, all local branches that contain the commit will be
rewritten. This especially helps with workflows that use stacked
branches.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
356 lines
10 KiB
C
356 lines
10 KiB
C
#define USE_THE_REPOSITORY_VARIABLE
|
|
|
|
#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(repo, 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;
|
|
size_t positive_refexprs;
|
|
size_t negative_refexprs;
|
|
};
|
|
|
|
static void get_ref_information(struct repository *repo,
|
|
struct rev_cmdline_info *cmd_info,
|
|
struct ref_info *ref_info)
|
|
{
|
|
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 (size_t 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_result_release(struct replay_result *result)
|
|
{
|
|
for (size_t i = 0; i < result->updates_nr; i++)
|
|
free(result->updates[i].refname);
|
|
free(result->updates);
|
|
}
|
|
|
|
static void replay_result_queue_update(struct replay_result *result,
|
|
const char *refname,
|
|
const struct object_id *old_oid,
|
|
const struct object_id *new_oid)
|
|
{
|
|
ALLOC_GROW(result->updates, result->updates_nr + 1, result->updates_alloc);
|
|
result->updates[result->updates_nr].refname = xstrdup(refname);
|
|
result->updates[result->updates_nr].old_oid = *old_oid;
|
|
result->updates[result->updates_nr].new_oid = *new_oid;
|
|
result->updates_nr++;
|
|
}
|
|
|
|
int replay_revisions(struct repository *repo, struct rev_info *revs,
|
|
struct replay_revisions_options *opts,
|
|
struct replay_result *out)
|
|
{
|
|
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 = {
|
|
.clean = 1,
|
|
};
|
|
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);
|
|
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))) {
|
|
replay_result_queue_update(out, decoration->name,
|
|
&commit->object.oid,
|
|
&last_commit->object.oid);
|
|
}
|
|
decoration = decoration->next;
|
|
}
|
|
}
|
|
|
|
if (!result.clean) {
|
|
out->merge_conflict = true;
|
|
ret = -1;
|
|
goto out;
|
|
}
|
|
|
|
/* In --advance mode, advance the target ref */
|
|
if (advance)
|
|
replay_result_queue_update(out, advance,
|
|
&onto->object.oid,
|
|
&last_commit->object.oid);
|
|
|
|
out->final_oid = last_commit->object.oid;
|
|
|
|
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;
|
|
}
|