mirror of
https://github.com/git-for-windows/git.git
synced 2026-06-11 08:30:32 -05:00
When replaying commits it may happen that some of the commits become empty relative to their parent. Such commits are for now automatically dropped by the replay subsystem without much control from the user. Introduce a new enum that allows the caller to drop, keep or abort in this case. Signed-off-by: Patrick Steinhardt <ps@pks.im> Signed-off-by: Junio C Hamano <gitster@pobox.com>
502 lines
14 KiB
C
502 lines
14 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 "refs.h"
|
|
#include "replay.h"
|
|
#include "revision.h"
|
|
#include "sequencer.h"
|
|
#include "strmap.h"
|
|
#include "tree.h"
|
|
|
|
/*
|
|
* We technically need USE_THE_REPOSITORY_VARIABLE for DEFAULT_ABBREV, but
|
|
* do not want to use the_repository.
|
|
*/
|
|
#define the_repository DO_NOT_USE_THE_REPOSITORY
|
|
|
|
enum replay_mode {
|
|
REPLAY_MODE_PICK,
|
|
REPLAY_MODE_REVERT,
|
|
};
|
|
|
|
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 void generate_revert_message(struct strbuf *msg,
|
|
struct commit *commit,
|
|
struct repository *repo)
|
|
{
|
|
const char *out_enc = get_commit_output_encoding();
|
|
const char *message = repo_logmsg_reencode(repo, commit, NULL, out_enc);
|
|
const char *subject_start;
|
|
int subject_len;
|
|
char *subject;
|
|
|
|
subject_len = find_commit_subject(message, &subject_start);
|
|
subject = xmemdupz(subject_start, subject_len);
|
|
|
|
sequencer_format_revert_message(repo, subject, commit,
|
|
commit->parents ? commit->parents->item : NULL,
|
|
false, msg);
|
|
|
|
free(subject);
|
|
repo_unuse_commit_buffer(repo, commit, message);
|
|
}
|
|
|
|
static struct commit *create_commit(struct repository *repo,
|
|
struct tree *tree,
|
|
struct commit *based_on,
|
|
struct commit *parent,
|
|
enum replay_mode mode)
|
|
{
|
|
struct object_id ret;
|
|
struct object *obj = NULL;
|
|
struct commit_list *parents = NULL;
|
|
char *author = NULL;
|
|
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);
|
|
if (mode == REPLAY_MODE_REVERT) {
|
|
generate_revert_message(&msg, based_on, repo);
|
|
/* For revert, use current user as author (NULL = use default) */
|
|
} else if (mode == REPLAY_MODE_PICK) {
|
|
find_commit_subject(message, &orig_message);
|
|
strbuf_addstr(&msg, orig_message);
|
|
author = get_author(message);
|
|
} else {
|
|
BUG("unexpected replay mode %d", mode);
|
|
}
|
|
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_branch_mode(struct repository *repo,
|
|
char **branch_name,
|
|
const char *option_name,
|
|
struct ref_info *rinfo,
|
|
struct commit **onto)
|
|
{
|
|
struct object_id oid;
|
|
char *fullname = NULL;
|
|
|
|
if (repo_dwim_ref(repo, *branch_name, strlen(*branch_name),
|
|
&oid, &fullname, 0) == 1) {
|
|
free(*branch_name);
|
|
*branch_name = fullname;
|
|
} else {
|
|
die(_("argument to %s must be a reference"), option_name);
|
|
}
|
|
*onto = peel_committish(repo, *branch_name, option_name);
|
|
if (rinfo->positive_refexprs > 1)
|
|
die(_("'%s' cannot be used with multiple revision ranges "
|
|
"because the ordering would be ill-defined"),
|
|
option_name);
|
|
}
|
|
|
|
static void set_up_replay_mode(struct repository *repo,
|
|
struct rev_cmdline_info *cmd_info,
|
|
const char *onto_name,
|
|
bool *detached_head,
|
|
char **advance_name,
|
|
char **revert_name,
|
|
struct commit **onto,
|
|
struct strset **update_refs)
|
|
{
|
|
struct ref_info rinfo;
|
|
int head_flags = 0;
|
|
|
|
refs_read_ref_full(get_main_ref_store(repo), "HEAD",
|
|
RESOLVE_REF_NO_RECURSE, NULL, &head_flags);
|
|
*detached_head = !(head_flags & REF_ISSYMREF);
|
|
|
|
get_ref_information(repo, cmd_info, &rinfo);
|
|
if (!rinfo.positive_refexprs)
|
|
die(_("need some commits to replay"));
|
|
|
|
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 if (*advance_name) {
|
|
set_up_branch_mode(repo, advance_name, "--advance", &rinfo, onto);
|
|
} else if (*revert_name) {
|
|
set_up_branch_mode(repo, revert_name, "--revert", &rinfo, onto);
|
|
} else {
|
|
BUG("expected one of onto_name, *advance_name, or *revert_name");
|
|
}
|
|
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;
|
|
if (!commit)
|
|
return fallback;
|
|
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,
|
|
enum replay_mode mode,
|
|
enum replay_empty_commit_action empty)
|
|
{
|
|
struct commit *base, *replayed_base;
|
|
struct tree *pickme_tree, *base_tree, *replayed_base_tree;
|
|
|
|
if (pickme->parents) {
|
|
base = pickme->parents->item;
|
|
base_tree = repo_get_commit_tree(repo, base);
|
|
} else {
|
|
base = NULL;
|
|
base_tree = lookup_tree(repo, repo->hash_algo->empty_tree);
|
|
}
|
|
|
|
replayed_base = mapped_commit(replayed_commits, base, onto);
|
|
replayed_base_tree = repo_get_commit_tree(repo, replayed_base);
|
|
pickme_tree = repo_get_commit_tree(repo, pickme);
|
|
|
|
if (mode == REPLAY_MODE_PICK) {
|
|
/* Cherry-pick: normal order */
|
|
merge_opt->branch1 = short_commit_name(repo, replayed_base);
|
|
merge_opt->branch2 = short_commit_name(repo, pickme);
|
|
if (pickme->parents)
|
|
merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
|
|
else
|
|
merge_opt->ancestor = xstrdup("empty tree");
|
|
|
|
merge_incore_nonrecursive(merge_opt,
|
|
base_tree,
|
|
replayed_base_tree,
|
|
pickme_tree,
|
|
result);
|
|
|
|
free((char *)merge_opt->ancestor);
|
|
} else if (mode == REPLAY_MODE_REVERT) {
|
|
/* Revert: swap base and pickme to reverse the diff */
|
|
const char *pickme_name = short_commit_name(repo, pickme);
|
|
merge_opt->branch1 = short_commit_name(repo, replayed_base);
|
|
merge_opt->branch2 = xstrfmt("parent of %s", pickme_name);
|
|
merge_opt->ancestor = pickme_name;
|
|
|
|
merge_incore_nonrecursive(merge_opt,
|
|
pickme_tree,
|
|
replayed_base_tree,
|
|
base_tree,
|
|
result);
|
|
|
|
free((char *)merge_opt->branch2);
|
|
} else {
|
|
BUG("unexpected replay mode %d", mode);
|
|
}
|
|
merge_opt->ancestor = NULL;
|
|
merge_opt->branch2 = NULL;
|
|
|
|
if (!result->clean)
|
|
return NULL;
|
|
|
|
/* Handle commits that become empty */
|
|
if (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&
|
|
!oideq(&pickme_tree->object.oid, &base_tree->object.oid)) {
|
|
switch (empty) {
|
|
case REPLAY_EMPTY_COMMIT_DROP:
|
|
return replayed_base;
|
|
case REPLAY_EMPTY_COMMIT_KEEP:
|
|
break;
|
|
case REPLAY_EMPTY_COMMIT_ABORT:
|
|
result->clean = error(_("commit %s became empty after replay"),
|
|
oid_to_hex(&pickme->object.oid));
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
return create_commit(repo, result->tree, pickme, replayed_base, mode);
|
|
}
|
|
|
|
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 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 = { 0 };
|
|
struct merge_result result = {
|
|
.clean = 1,
|
|
};
|
|
bool detached_head;
|
|
char *advance;
|
|
char *revert;
|
|
const char *ref;
|
|
struct object_id old_oid;
|
|
enum replay_mode mode = REPLAY_MODE_PICK;
|
|
int ret;
|
|
|
|
advance = xstrdup_or_null(opts->advance);
|
|
revert = xstrdup_or_null(opts->revert);
|
|
if (revert)
|
|
mode = REPLAY_MODE_REVERT;
|
|
set_up_replay_mode(revs->repo, &revs->cmdline, opts->onto,
|
|
&detached_head, &advance, &revert, &onto, &update_refs);
|
|
|
|
if (opts->ref) {
|
|
struct object_id oid;
|
|
|
|
if (update_refs && strset_get_size(update_refs) > 1) {
|
|
ret = error(_("'--ref' cannot be used with multiple revision ranges"));
|
|
goto out;
|
|
}
|
|
if (check_refname_format(opts->ref, 0) || !starts_with(opts->ref, "refs/")) {
|
|
ret = error(_("'%s' is not a valid refname"), opts->ref);
|
|
goto out;
|
|
}
|
|
ref = opts->ref;
|
|
if (!refs_read_ref(get_main_ref_store(revs->repo), opts->ref, &oid))
|
|
oidcpy(&old_oid, &oid);
|
|
else
|
|
oidclr(&old_oid, revs->repo->hash_algo);
|
|
} else {
|
|
ref = advance ? advance : revert;
|
|
oidcpy(&old_oid, &onto->object.oid);
|
|
}
|
|
|
|
if (prepare_revision_walk(revs) < 0) {
|
|
ret = error(_("error preparing revisions"));
|
|
goto out;
|
|
}
|
|
|
|
init_basic_merge_options(&merge_opt, revs->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 && commit->parents->next)
|
|
die(_("replaying merge commits is not supported yet!"));
|
|
|
|
last_commit = pick_regular_commit(revs->repo, commit, replayed_commits,
|
|
mode == REPLAY_MODE_REVERT ? last_commit : onto,
|
|
&merge_opt, &result, mode, opts->empty);
|
|
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 (ref)
|
|
continue;
|
|
|
|
for (decoration = get_name_decoration(&commit->object);
|
|
decoration;
|
|
decoration = decoration->next)
|
|
{
|
|
if (decoration->type != DECORATION_REF_LOCAL &&
|
|
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 (decoration->type == DECORATION_REF_HEAD && !detached_head)
|
|
continue;
|
|
|
|
if (!opts->contained &&
|
|
!strset_contains(update_refs, decoration->name))
|
|
continue;
|
|
|
|
replay_result_queue_update(out, decoration->name,
|
|
&commit->object.oid,
|
|
&last_commit->object.oid);
|
|
}
|
|
}
|
|
|
|
if (result.clean < 0) {
|
|
ret = -1;
|
|
goto out;
|
|
}
|
|
|
|
if (!result.clean) {
|
|
ret = 1;
|
|
goto out;
|
|
}
|
|
|
|
if (ref)
|
|
replay_result_queue_update(out, ref, &old_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);
|
|
free(revert);
|
|
return ret;
|
|
}
|