mirror of
https://github.com/git/git.git
synced 2026-10-19 10:18:52 +02:00
The experimental 'git history' command has been taught a new 'squash' subcommand to fold a range of commits into a single commit, with any descendants replayed on top. * hn/history-squash: history: support editing squashed commit messages history: create squashed commits without editing history: protect branches when squashing a range history: validate squash revision ranges history: add skeleton for squash subcommand sequencer: share the squash message marker helpers and flags history: give commit_tree_ext a message template history: extract helper for a commit's parent tree
1860 lines
50 KiB
C
1860 lines
50 KiB
C
#define USE_THE_REPOSITORY_VARIABLE
|
|
|
|
#include "builtin.h"
|
|
#include "advice.h"
|
|
#include "cache-tree.h"
|
|
#include "commit.h"
|
|
#include "commit-reach.h"
|
|
#include "config.h"
|
|
#include "editor.h"
|
|
#include "environment.h"
|
|
#include "gettext.h"
|
|
#include "hex.h"
|
|
#include "lockfile.h"
|
|
#include "merge-ort.h"
|
|
#include "oidmap.h"
|
|
#include "parse-options.h"
|
|
#include "path.h"
|
|
#include "read-cache.h"
|
|
#include "refs.h"
|
|
#include "ref-filter.h"
|
|
#include "replay.h"
|
|
#include "reset.h"
|
|
#include "revision.h"
|
|
#include "sequencer.h"
|
|
#include "string-list.h"
|
|
#include "strvec.h"
|
|
#include "tree.h"
|
|
#include "tree-walk.h"
|
|
#include "unpack-trees.h"
|
|
#include "wt-status.h"
|
|
|
|
#define GIT_HISTORY_DROP_USAGE \
|
|
N_("git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)]")
|
|
#define GIT_HISTORY_FIXUP_USAGE \
|
|
N_("git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]")
|
|
#define GIT_HISTORY_REWORD_USAGE \
|
|
N_("git history reword <commit> [--dry-run] [--update-refs=(branches|head)]")
|
|
#define GIT_HISTORY_SPLIT_USAGE \
|
|
N_("git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]")
|
|
#define GIT_HISTORY_SQUASH_USAGE \
|
|
N_("git history squash [--dry-run] [--update-refs=(branches|head)] [--[no-]edit] <revision-range>")
|
|
|
|
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;
|
|
|
|
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(path, "w");
|
|
if (!s.fp)
|
|
return error_errno(_("could not open '%s'"), path);
|
|
|
|
strbuf_addstr(out, default_message);
|
|
strbuf_addch(out, '\n');
|
|
strbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);
|
|
if (fwrite(out->buf, 1, out->len, s.fp) != out->len)
|
|
die_errno(_("could not write to '%s'"), path);
|
|
|
|
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);
|
|
if (fclose(s.fp))
|
|
die_errno(_("could not write to '%s'"), path);
|
|
|
|
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;
|
|
}
|
|
|
|
enum commit_tree_flags {
|
|
COMMIT_TREE_EDIT_MESSAGE = (1 << 0),
|
|
};
|
|
|
|
static int commit_tree_ext(struct repository *repo,
|
|
const char *action,
|
|
struct commit *commit_with_message,
|
|
const char *message_template,
|
|
const struct commit_list *parents,
|
|
const struct object_id *old_tree,
|
|
const struct object_id *new_tree,
|
|
struct commit **out,
|
|
enum commit_tree_flags flags)
|
|
{
|
|
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;
|
|
char *original_author = NULL;
|
|
size_t len;
|
|
int ret;
|
|
|
|
/* We retain authorship of the original commit. */
|
|
original_message = repo_logmsg_reencode(repo, commit_with_message, NULL, NULL);
|
|
ptr = find_commit_header(original_message, "author", &len);
|
|
if (ptr)
|
|
original_author = xmemdupz(ptr, len);
|
|
find_commit_subject(original_message, &original_body);
|
|
|
|
if (!message_template)
|
|
message_template = original_body;
|
|
|
|
if (flags & COMMIT_TREE_EDIT_MESSAGE) {
|
|
ret = fill_commit_message(repo, old_tree, new_tree,
|
|
message_template, action, &commit_message);
|
|
if (ret < 0)
|
|
goto out;
|
|
} else {
|
|
strbuf_addstr(&commit_message, message_template);
|
|
}
|
|
|
|
original_extra_headers = read_commit_extra_headers(commit_with_message,
|
|
exclude_gpgsig);
|
|
|
|
ret = commit_tree_extended(commit_message.buf, commit_message.len, new_tree,
|
|
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);
|
|
repo_unuse_commit_buffer(repo, commit_with_message, original_message);
|
|
return ret;
|
|
}
|
|
|
|
static int first_parent_tree_oid(struct repository *repo,
|
|
struct commit *commit,
|
|
struct object_id *out)
|
|
{
|
|
struct commit *parent = commit->parents ? commit->parents->item : NULL;
|
|
|
|
if (!parent) {
|
|
oidcpy(out, repo->hash_algo->empty_tree);
|
|
return 0;
|
|
}
|
|
|
|
if (repo_parse_commit(repo, parent))
|
|
return error(_("unable to parse parent commit %s"),
|
|
oid_to_hex(&parent->object.oid));
|
|
|
|
oidcpy(out, &repo_get_commit_tree(repo, parent)->object.oid);
|
|
return 0;
|
|
}
|
|
|
|
static int commit_tree_with_edited_message(struct repository *repo,
|
|
const char *action,
|
|
struct commit *original,
|
|
struct commit **out)
|
|
{
|
|
struct object_id parent_tree_oid;
|
|
const struct object_id *tree_oid;
|
|
|
|
tree_oid = &repo_get_commit_tree(repo, original)->object.oid;
|
|
|
|
if (first_parent_tree_oid(repo, original, &parent_tree_oid) < 0)
|
|
return -1;
|
|
|
|
return commit_tree_ext(repo, action, original, NULL, original->parents,
|
|
&parent_tree_oid, tree_oid, out, COMMIT_TREE_EDIT_MESSAGE);
|
|
}
|
|
|
|
enum ref_action {
|
|
REF_ACTION_DEFAULT,
|
|
REF_ACTION_BRANCHES,
|
|
REF_ACTION_HEAD,
|
|
};
|
|
|
|
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 {
|
|
return error(_("%s expects one of 'branches' or 'head'"),
|
|
opt->long_name);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int revwalk_contains_merges(struct repository *repo,
|
|
const struct strvec *revwalk_args)
|
|
{
|
|
struct strvec args = STRVEC_INIT;
|
|
struct rev_info revs;
|
|
int ret;
|
|
|
|
strvec_pushv(&args, revwalk_args->v);
|
|
strvec_push(&args, "--min-parents=2");
|
|
|
|
repo_init_revisions(repo, &revs, NULL);
|
|
|
|
setup_revisions_from_strvec(&args, &revs, NULL);
|
|
if (args.nr != 1)
|
|
BUG("revisions were set up with invalid argument");
|
|
|
|
if (prepare_revision_walk(&revs) < 0) {
|
|
ret = error(_("error preparing revisions"));
|
|
goto out;
|
|
}
|
|
|
|
if (get_revision(&revs)) {
|
|
ret = error(_("replaying merge commits is not supported yet!"));
|
|
goto out;
|
|
}
|
|
|
|
reset_revision_walk();
|
|
ret = 0;
|
|
|
|
out:
|
|
release_revisions(&revs);
|
|
strvec_clear(&args);
|
|
return ret;
|
|
}
|
|
|
|
static int setup_revwalk(struct repository *repo,
|
|
enum ref_action action,
|
|
struct commit *original,
|
|
struct rev_info *revs)
|
|
{
|
|
struct strvec args = STRVEC_INIT;
|
|
int ret;
|
|
|
|
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;
|
|
struct commit *head;
|
|
|
|
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);
|
|
commit_list_free(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 --update-refs=head"));
|
|
goto out;
|
|
}
|
|
|
|
strvec_push(&args, "HEAD");
|
|
} else {
|
|
strvec_push(&args, "--branches");
|
|
strvec_push(&args, "HEAD");
|
|
}
|
|
|
|
ret = revwalk_contains_merges(repo, &args);
|
|
if (ret < 0)
|
|
goto out;
|
|
|
|
setup_revisions_from_strvec(&args, revs, NULL);
|
|
if (args.nr != 1)
|
|
BUG("revisions were set up with invalid argument");
|
|
|
|
ret = 0;
|
|
|
|
out:
|
|
strvec_clear(&args);
|
|
return ret;
|
|
}
|
|
|
|
static int handle_ref_update(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)
|
|
{
|
|
if (!transaction) {
|
|
printf("update %s %s %s\n",
|
|
refname, oid_to_hex(new_oid), oid_to_hex(old_oid));
|
|
return 0;
|
|
}
|
|
|
|
return ref_transaction_update(transaction, refname, new_oid, old_oid,
|
|
NULL, NULL, 0, reflog_msg, err);
|
|
}
|
|
|
|
static int compute_pending_ref_updates(struct rev_info *revs,
|
|
enum ref_action action,
|
|
struct commit *original,
|
|
struct commit *rewritten,
|
|
enum replay_empty_commit_action empty,
|
|
struct replay_result *result)
|
|
{
|
|
const struct name_decoration *decoration;
|
|
struct replay_revisions_options opts = {
|
|
.empty = empty,
|
|
};
|
|
char hex[GIT_MAX_HEXSZ + 1];
|
|
bool detached_head;
|
|
int head_flags = 0;
|
|
int ret;
|
|
|
|
refs_read_ref_full(get_main_ref_store(revs->repo), "HEAD",
|
|
RESOLVE_REF_NO_RECURSE, NULL, &head_flags);
|
|
detached_head = !(head_flags & REF_ISSYMREF);
|
|
|
|
opts.onto = oid_to_hex_r(hex, &rewritten->object.oid);
|
|
|
|
ret = replay_revisions(revs, &opts, result);
|
|
if (ret)
|
|
return ret;
|
|
|
|
if (action != REF_ACTION_BRANCHES && action != REF_ACTION_HEAD)
|
|
BUG("unsupported ref action %d", action);
|
|
|
|
/*
|
|
* `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;
|
|
|
|
replay_result_queue_update(result, decoration->name,
|
|
&original->object.oid,
|
|
&rewritten->object.oid);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int apply_pending_ref_updates(struct repository *repo,
|
|
const struct replay_result *result,
|
|
const char *reflog_msg,
|
|
int dry_run)
|
|
{
|
|
struct ref_transaction *transaction = NULL;
|
|
struct strbuf err = STRBUF_INIT;
|
|
int ret;
|
|
|
|
if (!dry_run) {
|
|
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 = handle_ref_update(transaction,
|
|
result->updates[i].refname,
|
|
&result->updates[i].new_oid,
|
|
&result->updates[i].old_oid,
|
|
reflog_msg, &err);
|
|
if (ret) {
|
|
ret = error(_("failed to update ref '%s': %s"),
|
|
result->updates[i].refname, err.buf);
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
if (transaction && ref_transaction_commit(transaction, &err)) {
|
|
ret = error(_("failed to commit ref transaction: %s"), err.buf);
|
|
goto out;
|
|
}
|
|
|
|
ret = 0;
|
|
|
|
out:
|
|
ref_transaction_free(transaction);
|
|
strbuf_release(&err);
|
|
return ret;
|
|
}
|
|
|
|
static int handle_reference_updates(struct rev_info *revs,
|
|
enum ref_action action,
|
|
struct commit *original,
|
|
struct commit *rewritten,
|
|
const char *reflog_msg,
|
|
int dry_run,
|
|
enum replay_empty_commit_action empty)
|
|
{
|
|
struct replay_result result = { 0 };
|
|
int ret;
|
|
|
|
ret = compute_pending_ref_updates(revs, action, original, rewritten,
|
|
empty, &result);
|
|
if (ret)
|
|
goto out;
|
|
|
|
ret = apply_pending_ref_updates(revs->repo, &result, reflog_msg, dry_run);
|
|
|
|
out:
|
|
replay_result_release(&result);
|
|
return ret;
|
|
}
|
|
|
|
static int commit_became_empty(struct repository *repo,
|
|
struct commit *original,
|
|
struct tree *result)
|
|
{
|
|
struct object_id parent_tree_oid;
|
|
|
|
if (first_parent_tree_oid(repo, original, &parent_tree_oid) < 0)
|
|
return -1;
|
|
|
|
return oideq(&result->object.oid, &parent_tree_oid);
|
|
}
|
|
|
|
static int parse_opt_empty(const struct option *opt, const char *arg, int unset)
|
|
{
|
|
enum replay_empty_commit_action *value = opt->value;
|
|
|
|
BUG_ON_OPT_NEG(unset);
|
|
|
|
if (!strcmp(arg, "drop"))
|
|
*value = REPLAY_EMPTY_COMMIT_DROP;
|
|
else if (!strcmp(arg, "keep"))
|
|
*value = REPLAY_EMPTY_COMMIT_KEEP;
|
|
else if (!strcmp(arg, "abort"))
|
|
*value = REPLAY_EMPTY_COMMIT_ABORT;
|
|
else
|
|
die(_("unrecognized '--empty=' action '%s'; "
|
|
"valid values are \"drop\", \"keep\", and \"abort\"."), arg);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cmd_history_fixup(int argc,
|
|
const char **argv,
|
|
const char *prefix,
|
|
struct repository *repo)
|
|
{
|
|
const char * const usage[] = {
|
|
GIT_HISTORY_FIXUP_USAGE,
|
|
NULL,
|
|
};
|
|
enum replay_empty_commit_action empty = REPLAY_EMPTY_COMMIT_DROP;
|
|
enum ref_action action = REF_ACTION_DEFAULT;
|
|
enum commit_tree_flags flags = 0;
|
|
int dry_run = 0;
|
|
struct option options[] = {
|
|
OPT_CALLBACK_F(0, "update-refs", &action, "(branches|head)",
|
|
N_("control which refs should be updated"),
|
|
PARSE_OPT_NONEG, parse_ref_action),
|
|
OPT_BOOL('n', "dry-run", &dry_run,
|
|
N_("perform a dry-run without updating any refs")),
|
|
OPT_BIT(0, "reedit-message", &flags,
|
|
N_("open an editor to modify the commit message"),
|
|
COMMIT_TREE_EDIT_MESSAGE),
|
|
OPT_CALLBACK_F(0, "empty", &empty, "(drop|keep|abort)",
|
|
N_("how to handle commits that become empty"),
|
|
PARSE_OPT_NONEG, parse_opt_empty),
|
|
OPT_END(),
|
|
};
|
|
struct merge_result merge_result = { 0 };
|
|
struct merge_options merge_opts = { 0 };
|
|
struct strbuf reflog_msg = STRBUF_INIT;
|
|
struct commit *head_commit, *original, *rewritten;
|
|
struct tree *head_tree, *original_tree, *index_tree;
|
|
struct rev_info revs = { 0 };
|
|
bool skip_commit = false;
|
|
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;
|
|
|
|
if (is_bare_repository(repo)) {
|
|
ret = error(_("cannot run fixup in a bare repository"));
|
|
goto out;
|
|
}
|
|
|
|
/* Resolve the original commit, which is the one we want to fix up. */
|
|
original = lookup_commit_reference_by_name(argv[0]);
|
|
if (!original) {
|
|
ret = error(_("commit cannot be found: %s"), argv[0]);
|
|
goto out;
|
|
}
|
|
|
|
/*
|
|
* Resolve HEAD so we can use its tree as the merge base: the staged
|
|
* changes are expressed as a diff from HEAD's tree to the index tree.
|
|
*/
|
|
head_commit = lookup_commit_reference_by_name("HEAD");
|
|
if (!head_commit) {
|
|
ret = error(_("cannot look up HEAD"));
|
|
goto out;
|
|
}
|
|
|
|
head_tree = repo_get_commit_tree(repo, head_commit);
|
|
if (!head_tree) {
|
|
ret = error(_("cannot get tree for HEAD"));
|
|
goto out;
|
|
}
|
|
|
|
if (repo_read_index(repo) < 0) {
|
|
ret = error(_("unable to read index"));
|
|
goto out;
|
|
}
|
|
|
|
if (!repo_index_has_changes(repo, head_tree, NULL)) {
|
|
ret = error(_("nothing to fixup: no staged changes"));
|
|
goto out;
|
|
}
|
|
|
|
/*
|
|
* Write the index as a tree object. This is the "theirs" side of the
|
|
* three-way merge: it is HEAD's tree with the staged changes applied.
|
|
*/
|
|
index_tree = write_in_core_index_as_tree(repo, repo->index);
|
|
if (!index_tree) {
|
|
ret = error(_("unable to write index as a tree"));
|
|
goto out;
|
|
}
|
|
|
|
original_tree = repo_get_commit_tree(repo, original);
|
|
if (!original_tree) {
|
|
ret = error(_("cannot get tree for commit %s"), argv[0]);
|
|
goto out;
|
|
}
|
|
|
|
/*
|
|
* Perform the three-way merge to reapply changes in the index onto the
|
|
* target commit. This is using basically the same logic as a
|
|
* cherry-pick, where the base commit is our HEAD, ours is the original
|
|
* tree and theirs is the index tree.
|
|
*/
|
|
init_basic_merge_options(&merge_opts, repo);
|
|
merge_opts.ancestor = "HEAD";
|
|
merge_opts.branch1 = argv[0];
|
|
merge_opts.branch2 = "staged";
|
|
merge_incore_nonrecursive(&merge_opts, head_tree,
|
|
original_tree, index_tree, &merge_result);
|
|
|
|
if (merge_result.clean < 0) {
|
|
ret = error(_("merge failed while applying fixup"));
|
|
goto out;
|
|
}
|
|
|
|
if (!merge_result.clean) {
|
|
ret = error(_("fixup would produce conflicts; aborting"));
|
|
goto out;
|
|
}
|
|
|
|
ret = commit_became_empty(repo, original, merge_result.tree);
|
|
if (ret < 0)
|
|
goto out;
|
|
if (ret > 0) {
|
|
switch (empty) {
|
|
case REPLAY_EMPTY_COMMIT_DROP:
|
|
/*
|
|
* Drop the target commit by replaying its descendants
|
|
* directly onto its parent.
|
|
*/
|
|
rewritten = original->parents ? original->parents->item : NULL;
|
|
|
|
/*
|
|
* TODO: we don't yet have the ability to drop root
|
|
* commits, but there's ultimately no good reason for
|
|
* this restriction to exist other than a technical
|
|
* limitation.
|
|
*/
|
|
if (!rewritten) {
|
|
ret = error(_("cannot drop root commit %s: "
|
|
"it has no parent to replay onto"),
|
|
argv[0]);
|
|
goto out;
|
|
}
|
|
|
|
skip_commit = true;
|
|
break;
|
|
case REPLAY_EMPTY_COMMIT_KEEP:
|
|
/* Proceed and record the empty commit. */
|
|
break;
|
|
case REPLAY_EMPTY_COMMIT_ABORT:
|
|
ret = error(_("fixup makes commit %s empty"), argv[0]);
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
ret = setup_revwalk(repo, action, original, &revs);
|
|
if (ret)
|
|
goto out;
|
|
|
|
if (!skip_commit) {
|
|
ret = commit_tree_ext(repo, "fixup", original, NULL, original->parents,
|
|
&original_tree->object.oid, &merge_result.tree->object.oid,
|
|
&rewritten, flags);
|
|
if (ret < 0) {
|
|
ret = error(_("failed writing fixed-up commit"));
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
strbuf_addf(&reflog_msg, "fixup: updating %s", argv[0]);
|
|
|
|
ret = handle_reference_updates(&revs, action, original, rewritten,
|
|
reflog_msg.buf, dry_run, empty);
|
|
if (ret < 0) {
|
|
ret = error(_("failed replaying descendants"));
|
|
goto out;
|
|
}
|
|
|
|
ret = 0;
|
|
|
|
out:
|
|
merge_finalize(&merge_opts, &merge_result);
|
|
strbuf_release(&reflog_msg);
|
|
release_revisions(&revs);
|
|
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;
|
|
int dry_run = 0;
|
|
struct option options[] = {
|
|
OPT_CALLBACK_F(0, "update-refs", &action, "(branches|head)",
|
|
N_("control which refs should be updated"),
|
|
PARSE_OPT_NONEG, parse_ref_action),
|
|
OPT_BOOL('n', "dry-run", &dry_run,
|
|
N_("perform a dry-run without updating any refs")),
|
|
OPT_END(),
|
|
};
|
|
struct strbuf reflog_msg = STRBUF_INIT;
|
|
struct commit *original, *rewritten;
|
|
struct rev_info revs = { 0 };
|
|
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 = setup_revwalk(repo, action, original, &revs);
|
|
if (ret)
|
|
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(&revs, action, original, rewritten,
|
|
reflog_msg.buf, dry_run, REPLAY_EMPTY_COMMIT_ABORT);
|
|
if (ret < 0) {
|
|
ret = error(_("failed replaying descendants"));
|
|
goto out;
|
|
}
|
|
|
|
ret = 0;
|
|
|
|
out:
|
|
strbuf_release(&reflog_msg);
|
|
release_revisions(&revs);
|
|
return ret;
|
|
}
|
|
|
|
static int write_ondisk_index(struct repository *repo,
|
|
struct object_id *oid,
|
|
const char *path)
|
|
{
|
|
struct unpack_trees_options opts = { 0 };
|
|
struct lock_file lock = LOCK_INIT;
|
|
struct tree_desc tree_desc;
|
|
struct index_state index;
|
|
struct tree *tree;
|
|
int ret;
|
|
|
|
index_state_init(&index, repo);
|
|
|
|
opts.head_idx = -1;
|
|
opts.src_index = &index;
|
|
opts.dst_index = &index;
|
|
|
|
tree = repo_parse_tree_indirect(repo, oid);
|
|
if (!tree) {
|
|
ret = error(_("unable to parse tree %s"), oid_to_hex(oid));
|
|
goto out;
|
|
}
|
|
init_tree_desc(&tree_desc, &tree->object.oid, tree->buffer, tree->size);
|
|
|
|
if (unpack_trees(1, &tree_desc, &opts)) {
|
|
ret = error(_("unable to populate index with tree"));
|
|
goto out;
|
|
}
|
|
|
|
prime_cache_tree(repo, &index, tree);
|
|
|
|
if (repo_hold_lock_file_for_update(repo, &lock, path, 0) < 0) {
|
|
ret = error_errno(_("unable to acquire index lock"));
|
|
goto out;
|
|
}
|
|
|
|
if (write_locked_index(&index, &lock, COMMIT_LOCK)) {
|
|
ret = error(_("unable to write new index file"));
|
|
goto out;
|
|
}
|
|
|
|
ret = 0;
|
|
|
|
out:
|
|
rollback_lock_file(&lock);
|
|
release_index(&index);
|
|
return ret;
|
|
}
|
|
|
|
static int split_commit(struct repository *repo,
|
|
struct commit *original,
|
|
struct pathspec *pathspec,
|
|
struct commit **out)
|
|
{
|
|
struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
|
|
struct strbuf index_file = STRBUF_INIT;
|
|
struct index_state index = INDEX_STATE_INIT(repo);
|
|
const struct object_id *original_commit_tree_oid;
|
|
const struct object_id *old_tree_oid, *new_tree_oid;
|
|
struct object_id parent_tree_oid;
|
|
char original_commit_oid[GIT_MAX_HEXSZ + 1];
|
|
struct commit *first_commit, *second_commit;
|
|
struct commit_list *parents = NULL;
|
|
struct tree *split_tree;
|
|
int ret;
|
|
|
|
if (first_parent_tree_oid(repo, original, &parent_tree_oid) < 0) {
|
|
ret = -1;
|
|
goto out;
|
|
}
|
|
original_commit_tree_oid = get_commit_tree_oid(original);
|
|
|
|
/*
|
|
* Construct the first commit. This is done by taking the original
|
|
* commit parent's tree and selectively patching changes from the diff
|
|
* between that parent and its child.
|
|
*/
|
|
repo_git_path_replace(repo, &index_file, "%s", "history-split.index");
|
|
|
|
ret = write_ondisk_index(repo, &parent_tree_oid, index_file.buf);
|
|
if (ret < 0)
|
|
goto out;
|
|
|
|
ret = read_index_from(&index, index_file.buf, repo->gitdir);
|
|
if (ret < 0) {
|
|
ret = error(_("failed reading temporary index"));
|
|
goto out;
|
|
}
|
|
|
|
oid_to_hex_r(original_commit_oid, &original->object.oid);
|
|
ret = run_add_p_index(repo, &index, index_file.buf, &interactive_opts,
|
|
original_commit_oid, pathspec, ADD_P_DISALLOW_EDIT);
|
|
if (ret < 0)
|
|
goto out;
|
|
|
|
split_tree = write_in_core_index_as_tree(repo, &index);
|
|
if (!split_tree) {
|
|
ret = error(_("failed split tree"));
|
|
goto out;
|
|
}
|
|
|
|
unlink(index_file.buf);
|
|
strbuf_release(&index_file);
|
|
|
|
/*
|
|
* We disallow the cases where either the split-out commit or the
|
|
* original commit would become empty. Consequently, if we see that the
|
|
* new tree ID matches either of those trees we abort.
|
|
*/
|
|
if (oideq(&split_tree->object.oid, &parent_tree_oid)) {
|
|
ret = error(_("split commit is empty"));
|
|
goto out;
|
|
} else if (oideq(&split_tree->object.oid, original_commit_tree_oid)) {
|
|
ret = error(_("split commit tree matches original commit"));
|
|
goto out;
|
|
}
|
|
|
|
/*
|
|
* The first commit is constructed from the split-out tree. The base
|
|
* that shall be diffed against is the parent of the original commit.
|
|
*/
|
|
ret = commit_tree_ext(repo, "split-out", original, NULL, original->parents, &parent_tree_oid,
|
|
&split_tree->object.oid, &first_commit, COMMIT_TREE_EDIT_MESSAGE);
|
|
if (ret < 0) {
|
|
ret = error(_("failed writing first commit"));
|
|
goto out;
|
|
}
|
|
|
|
/*
|
|
* The second commit is constructed from the original tree. The base to
|
|
* diff against and the parent in this case is the first split-out
|
|
* commit.
|
|
*/
|
|
commit_list_append(first_commit, &parents);
|
|
|
|
old_tree_oid = &repo_get_commit_tree(repo, first_commit)->object.oid;
|
|
new_tree_oid = &repo_get_commit_tree(repo, original)->object.oid;
|
|
|
|
ret = commit_tree_ext(repo, "split-out", original, NULL, parents, old_tree_oid,
|
|
new_tree_oid, &second_commit, COMMIT_TREE_EDIT_MESSAGE);
|
|
if (ret < 0) {
|
|
ret = error(_("failed writing second commit"));
|
|
goto out;
|
|
}
|
|
|
|
*out = second_commit;
|
|
ret = 0;
|
|
|
|
out:
|
|
if (index_file.len)
|
|
unlink(index_file.buf);
|
|
strbuf_release(&index_file);
|
|
commit_list_free(parents);
|
|
release_index(&index);
|
|
return ret;
|
|
}
|
|
|
|
static int cmd_history_split(int argc,
|
|
const char **argv,
|
|
const char *prefix,
|
|
struct repository *repo)
|
|
{
|
|
const char * const usage[] = {
|
|
GIT_HISTORY_SPLIT_USAGE,
|
|
NULL,
|
|
};
|
|
enum ref_action action = REF_ACTION_DEFAULT;
|
|
int dry_run = 0;
|
|
struct option options[] = {
|
|
OPT_CALLBACK_F(0, "update-refs", &action, "(branches|head)",
|
|
N_("control ref update behavior"),
|
|
PARSE_OPT_NONEG, parse_ref_action),
|
|
OPT_BOOL('n', "dry-run", &dry_run,
|
|
N_("perform a dry-run without updating any refs")),
|
|
OPT_END(),
|
|
};
|
|
struct commit *original, *rewritten = NULL;
|
|
struct strbuf reflog_msg = STRBUF_INIT;
|
|
struct pathspec pathspec = { 0 };
|
|
struct rev_info revs = { 0 };
|
|
int ret;
|
|
|
|
argc = parse_options(argc, argv, prefix, options, usage, 0);
|
|
if (argc < 1) {
|
|
ret = error(_("command expects a committish"));
|
|
goto out;
|
|
}
|
|
repo_config(repo, git_default_config, NULL);
|
|
|
|
if (action == REF_ACTION_DEFAULT)
|
|
action = REF_ACTION_BRANCHES;
|
|
|
|
parse_pathspec(&pathspec, 0,
|
|
PATHSPEC_PREFER_FULL |
|
|
PATHSPEC_SYMLINK_LEADING_PATH |
|
|
PATHSPEC_PREFIX_ORIGIN,
|
|
prefix, argv + 1);
|
|
|
|
original = lookup_commit_reference_by_name(argv[0]);
|
|
if (!original) {
|
|
ret = error(_("commit cannot be found: %s"), argv[0]);
|
|
goto out;
|
|
}
|
|
|
|
ret = setup_revwalk(repo, action, original, &revs);
|
|
if (ret < 0)
|
|
goto out;
|
|
|
|
if (original->parents && original->parents->next) {
|
|
ret = error(_("cannot split up merge commit"));
|
|
goto out;
|
|
}
|
|
|
|
ret = split_commit(repo, original, &pathspec, &rewritten);
|
|
if (ret < 0)
|
|
goto out;
|
|
|
|
strbuf_addf(&reflog_msg, "split: updating %s", argv[0]);
|
|
|
|
ret = handle_reference_updates(&revs, action, original, rewritten,
|
|
reflog_msg.buf, dry_run, REPLAY_EMPTY_COMMIT_ABORT);
|
|
if (ret < 0) {
|
|
ret = error(_("failed replaying descendants"));
|
|
goto out;
|
|
}
|
|
|
|
ret = 0;
|
|
|
|
out:
|
|
strbuf_release(&reflog_msg);
|
|
clear_pathspec(&pathspec);
|
|
release_revisions(&revs);
|
|
return ret;
|
|
}
|
|
|
|
#define SQUASH_SEEN (1u << 11)
|
|
#define SQUASH_TIP (1u << 12)
|
|
#define SQUASH_AMEND_TARGET (1u << 13)
|
|
|
|
static bool is_autosquash_subject(const char *s)
|
|
{
|
|
return starts_with(s, "amend!") || starts_with(s, "fixup!") ||
|
|
starts_with(s, "squash!");
|
|
}
|
|
|
|
static bool skip_one_autosquash_prefix(const char *s, const char **out)
|
|
{
|
|
if (skip_prefix(s, "amend!", out) || skip_prefix(s, "fixup!", out) ||
|
|
skip_prefix(s, "squash!", out)) {
|
|
while (**out == ' ')
|
|
(*out)++;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static void truncate_message_to_subject(struct strbuf *msg)
|
|
{
|
|
const char *eos = strstr(msg->buf, "\n\n");
|
|
|
|
if (eos)
|
|
strbuf_setlen(msg, eos - msg->buf + 1);
|
|
}
|
|
|
|
struct subject_data {
|
|
struct strintmap subjects;
|
|
struct strbuf subject;
|
|
struct strbuf squash_message;
|
|
const char *message;
|
|
bool edit_message;
|
|
};
|
|
|
|
#define SUBJECT_DATA_INIT { \
|
|
.subjects = STRINTMAP_INIT, \
|
|
.subject = STRBUF_INIT, \
|
|
.squash_message = STRBUF_INIT, \
|
|
}
|
|
|
|
static void subject_data_clear(struct subject_data *data)
|
|
{
|
|
strintmap_clear(&data->subjects);
|
|
strbuf_release(&data->subject);
|
|
strbuf_release(&data->squash_message);
|
|
}
|
|
|
|
static int squash_amend_message(struct repository *repo,
|
|
struct commit *commit,
|
|
struct subject_data *data,
|
|
unsigned flags)
|
|
{
|
|
const char *body = data->message + data->subject.len;
|
|
|
|
while (isspace(*body))
|
|
body++;
|
|
|
|
if (!*body) {
|
|
warning(_("ignoring %s (%s): message body is empty"),
|
|
repo_find_unique_abbrev(repo, &commit->object.oid,
|
|
DEFAULT_ABBREV),
|
|
data->subject.buf);
|
|
return 0;
|
|
}
|
|
|
|
if (data->edit_message) {
|
|
return 0;
|
|
} else if (flags & SQUASH_AMEND_TARGET) {
|
|
if (starts_with(data->squash_message.buf, "squash!"))
|
|
return error(_("squashing %s (%s) would overwrite "
|
|
"'squash!' message, please combine them "
|
|
"using '--edit'"),
|
|
repo_find_unique_abbrev(repo,
|
|
&commit->object.oid,
|
|
DEFAULT_ABBREV),
|
|
data->subject.buf);
|
|
if (starts_with(data->squash_message.buf, "fixup!"))
|
|
strbuf_splice(&data->squash_message, 0, 5, "amend!", 5);
|
|
if (starts_with(data->squash_message.buf, "amend!")) {
|
|
truncate_message_to_subject(&data->squash_message);
|
|
strbuf_addch(&data->squash_message, '\n');
|
|
} else {
|
|
strbuf_reset(&data->squash_message);
|
|
}
|
|
strbuf_addstr(&data->squash_message, body);
|
|
strbuf_complete_line(&data->squash_message);
|
|
} else {
|
|
return error(_("cannot squash %s (%s) that does not target "
|
|
"base commit without '--edit'"),
|
|
repo_find_unique_abbrev(repo, &commit->object.oid,
|
|
DEFAULT_ABBREV),
|
|
data->subject.buf);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int squash_squash_message(struct repository *repo,
|
|
struct commit *commit,
|
|
struct subject_data *data,
|
|
unsigned flags)
|
|
{
|
|
const char *body = data->message + data->subject.len;
|
|
|
|
while (isspace(*body))
|
|
body++;
|
|
|
|
if (data->edit_message) {
|
|
return 0;
|
|
} else if (flags & SQUASH_AMEND_TARGET) {
|
|
if (starts_with(data->squash_message.buf, "fixup!")) {
|
|
truncate_message_to_subject(&data->squash_message);
|
|
strbuf_splice(&data->squash_message, 0, 5, "squash", 6);
|
|
}
|
|
if (starts_with(data->squash_message.buf, "squash!")) {
|
|
strbuf_addch(&data->squash_message, '\n');
|
|
strbuf_addstr(&data->squash_message, body);
|
|
strbuf_complete_line(&data->squash_message);
|
|
} else {
|
|
return error(_("squashing %s (%s) would discard its "
|
|
"message, please combine them using "
|
|
"'--edit'"),
|
|
repo_find_unique_abbrev(repo,
|
|
&commit->object.oid,
|
|
DEFAULT_ABBREV),
|
|
data->subject.buf);
|
|
}
|
|
} else {
|
|
return error(_("cannot squash %s (%s) that does not target "
|
|
"base commit without '--edit'"),
|
|
repo_find_unique_abbrev(repo, &commit->object.oid,
|
|
DEFAULT_ABBREV),
|
|
data->subject.buf);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
static int squash_check_can_autosquash(struct repository *repo,
|
|
struct commit *commit,
|
|
struct subject_data *data,
|
|
unsigned flags)
|
|
{
|
|
commit->object.flags |= flags & SQUASH_AMEND_TARGET;
|
|
if (starts_with(data->subject.buf, "amend!"))
|
|
return squash_amend_message(repo, commit, data, flags);
|
|
else if (starts_with(data->subject.buf, "squash!"))
|
|
return squash_squash_message(repo, commit, data, flags);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int squash_check_autosquash_subject(struct repository *repo,
|
|
struct commit *commit,
|
|
struct subject_data *data)
|
|
{
|
|
const char* s = data->subject.buf;
|
|
struct commit *target;
|
|
struct hashmap_iter iter;
|
|
struct strmap_entry *entry;
|
|
/* Try skipping autosquash prefixes one at a time to allow
|
|
* squashing
|
|
* a commit
|
|
* fixup! fixup! a commit
|
|
*
|
|
* where we may have started with
|
|
* a commit
|
|
* fixup! a commit
|
|
* fixup! fixup! a commit
|
|
*
|
|
* and squashed the first fixup separately from the second
|
|
*/
|
|
while (skip_one_autosquash_prefix(s, &s)) {
|
|
unsigned flags = strintmap_get(&data->subjects, s);
|
|
if (flags)
|
|
return squash_check_can_autosquash(repo, commit, data, flags);
|
|
}
|
|
/*
|
|
* Allow "fixup! <hex object id>", but not "fixup! HEAD^" or
|
|
* "fixup! main". If the target is not being squashed check the subject
|
|
* to allow "fixup! abc123" and "fixup! <subject of abc123>" to be
|
|
* squashed together.
|
|
*/
|
|
target = lookup_commit_reference_by_name(s);
|
|
if (target && starts_with(oid_to_hex(&target->object.oid), s)) {
|
|
unsigned flags =
|
|
target->object.flags & (SQUASH_SEEN | SQUASH_AMEND_TARGET);
|
|
if (!flags) {
|
|
const char *subject_start;
|
|
const char *buffer = repo_logmsg_reencode(repo, target,
|
|
NULL, NULL);
|
|
size_t subject_len = find_commit_subject(buffer,
|
|
&subject_start);
|
|
char *subject = xmemdupz(subject_start, subject_len);
|
|
|
|
flags = strintmap_get(&data->subjects, subject);
|
|
free(subject);
|
|
repo_unuse_commit_buffer(repo, target, buffer);
|
|
}
|
|
if (flags)
|
|
return squash_check_can_autosquash(repo, commit,
|
|
data, flags);
|
|
}
|
|
/* Try subject prefix matches */
|
|
strintmap_for_each_entry(&data->subjects, &iter, entry) {
|
|
s = data->subject.buf;
|
|
while(skip_one_autosquash_prefix(s, &s)) {
|
|
if (starts_with(entry->key, s)) {
|
|
unsigned value = (intptr_t)entry->value;
|
|
|
|
return squash_check_can_autosquash(repo, commit,
|
|
data, value);
|
|
}
|
|
}
|
|
}
|
|
return error(_("cannot squash %s (%s): its target is not being "
|
|
"squashed"),
|
|
repo_find_unique_abbrev(repo, &commit->object.oid,
|
|
DEFAULT_ABBREV),
|
|
data->subject.buf);
|
|
}
|
|
|
|
static int squash_check_subject(struct repository *repo,
|
|
struct commit *commit,
|
|
struct subject_data *data)
|
|
{
|
|
int ret = 0;
|
|
const char *buf = repo_logmsg_reencode(repo, commit, NULL, NULL);
|
|
size_t subject_len = find_commit_subject(buf, &data->message);
|
|
|
|
strbuf_reset(&data->subject);
|
|
strbuf_add(&data->subject, data->message, subject_len);
|
|
|
|
if (!strintmap_get_size(&data->subjects)) {
|
|
const char *s;
|
|
|
|
strbuf_addstr(&data->squash_message, data->message);
|
|
strbuf_complete_line(&data->squash_message);
|
|
/*
|
|
* Strip a single autosquash prefix to allow squashing
|
|
* fixup! base
|
|
* amend! base
|
|
*/
|
|
s = data->subject.buf;
|
|
skip_one_autosquash_prefix(s, &s);
|
|
strintmap_set(&data->subjects, s, SQUASH_AMEND_TARGET | SQUASH_SEEN);
|
|
commit->object.flags |= SQUASH_AMEND_TARGET;
|
|
} else if (is_autosquash_subject(data->subject.buf)) {
|
|
ret = squash_check_autosquash_subject(repo, commit, data);
|
|
} else {
|
|
strintmap_set(&data->subjects, data->subject.buf, SQUASH_SEEN);
|
|
}
|
|
repo_unuse_commit_buffer(repo, commit, buf);
|
|
return ret;
|
|
}
|
|
|
|
static int build_squash_message(struct repository *repo,
|
|
const struct strbuf *todo_buf,
|
|
struct strbuf *out);
|
|
|
|
static int setup_squash_revisions(struct repository *repo,
|
|
int argc, const char **argv,
|
|
struct rev_info *revs)
|
|
{
|
|
repo_init_revisions(repo, revs, NULL);
|
|
revs->reverse = 1;
|
|
revs->topo_order = 1;
|
|
revs->sort_order = REV_SORT_IN_GRAPH_ORDER;
|
|
revs->simplify_history = 0;
|
|
revs->ancestry_path = 1;
|
|
revs->limited = 1;
|
|
revs->ancestry_path_implicit_bottoms = 1;
|
|
|
|
argc = setup_revisions(argc, argv, revs, NULL);
|
|
if (argc > 1)
|
|
return error(_("unrecognized argument: %s"), argv[1]);
|
|
|
|
if (revs->reverse != 1 || revs->topo_order != 1 ||
|
|
revs->sort_order != REV_SORT_IN_GRAPH_ORDER ||
|
|
revs->simplify_history != 0 || revs->boundary == 1 ||
|
|
revs->ancestry_path != 1 || revs->limited != 1 ||
|
|
revs->ancestry_path_implicit_bottoms != 1) {
|
|
warning(_("ignoring rev-list options that would change how the "
|
|
"range is walked"));
|
|
revs->reverse = 1;
|
|
revs->topo_order = 1;
|
|
revs->sort_order = REV_SORT_IN_GRAPH_ORDER;
|
|
revs->simplify_history = 0;
|
|
revs->boundary = 0;
|
|
revs->ancestry_path = 1;
|
|
revs->limited = 1;
|
|
revs->ancestry_path_implicit_bottoms = 1;
|
|
}
|
|
|
|
/*
|
|
* A squash needs a base to reparent onto, so the range has to exclude
|
|
* something, as in "<base>..<tip>". A revision range with no such
|
|
* bottom commit cannot be squashed.
|
|
*/
|
|
for (size_t i = 0; i < revs->cmdline.nr; i++)
|
|
if (revs->cmdline.rev[i].flags & BOTTOM)
|
|
return 0;
|
|
|
|
return error(_("not a '<base>..<tip>' revision range"));
|
|
}
|
|
|
|
/*
|
|
* Resolve a revision range into its oldest commit and single tip. Every
|
|
* parent after the oldest commit must either be selected or also be a parent
|
|
* of the oldest commit.
|
|
*/
|
|
static int resolve_squash_range(struct repository *repo,
|
|
bool update_branches,
|
|
bool edit_message,
|
|
int argc, const char **argv,
|
|
struct commit **oldest_out,
|
|
struct commit **tip_out,
|
|
char **message_out)
|
|
{
|
|
struct rev_info revs;
|
|
struct subject_data subject_data = SUBJECT_DATA_INIT;
|
|
struct commit *commit, *oldest = NULL, *tip = NULL;
|
|
struct strbuf todo_buf = STRBUF_INIT;
|
|
int ret, tip_count = 0;
|
|
bool walk_started = false;
|
|
struct ref_filter filter = REF_FILTER_INIT;
|
|
struct ref_array refs = { 0 };
|
|
|
|
subject_data.edit_message = edit_message;
|
|
ret = setup_squash_revisions(repo, argc, argv, &revs);
|
|
if (ret < 0)
|
|
goto out;
|
|
|
|
if (prepare_revision_walk(&revs) < 0) {
|
|
ret = error(_("error preparing revisions"));
|
|
goto out;
|
|
}
|
|
walk_started = true;
|
|
while ((commit = get_revision(&revs))) {
|
|
struct commit_list *p;
|
|
|
|
if (edit_message)
|
|
strbuf_addf(&todo_buf, "pick %s\n",
|
|
oid_to_hex(&commit->object.oid));
|
|
|
|
if (!commit->parents) {
|
|
ret = error(_("cannot squash down to root commit"));
|
|
goto out;
|
|
}
|
|
for (p = commit->parents; oldest && p; p = p->next) {
|
|
struct commit_list *q;
|
|
struct object *o;
|
|
bool seen;
|
|
|
|
if (repo_parse_commit(repo, p->item)) {
|
|
ret = error(_("cannot parse commit"));
|
|
goto out;
|
|
}
|
|
o = &p->item->object;
|
|
seen = o->flags & SQUASH_SEEN;
|
|
/*
|
|
* Allow parents that match the parents of the
|
|
* squashed commit.
|
|
*/
|
|
for (q = oldest->parents; !seen && q; q = q->next) {
|
|
if (p->item == q->item) {
|
|
seen = true;
|
|
commit_list_insert(commit, &filter.with_commit);
|
|
}
|
|
}
|
|
if (!seen) {
|
|
ret = error(_("parent %s of commit %s is "
|
|
"outside the revision range"),
|
|
repo_find_unique_abbrev(repo, &o->oid,
|
|
DEFAULT_ABBREV),
|
|
repo_find_unique_abbrev(repo,
|
|
&commit->object.oid,
|
|
DEFAULT_ABBREV));
|
|
goto out;
|
|
}
|
|
if (o->flags & SQUASH_TIP) {
|
|
tip_count--;
|
|
o->flags &= ~SQUASH_TIP;
|
|
}
|
|
}
|
|
if (!oldest) {
|
|
commit_list_insert(commit, &filter.with_commit);
|
|
oldest = commit;
|
|
}
|
|
if (squash_check_subject(repo, commit, &subject_data)) {
|
|
ret = -1;
|
|
goto out;
|
|
}
|
|
tip = commit;
|
|
tip->object.flags |= SQUASH_SEEN | SQUASH_TIP;
|
|
tip_count++;
|
|
}
|
|
clear_object_flags(repo, SQUASH_SEEN | SQUASH_TIP);
|
|
reset_revision_walk();
|
|
walk_started = false;
|
|
|
|
if (!tip_count) {
|
|
ret = error(_("the revision range is empty"));
|
|
goto out;
|
|
} else if (tip_count != 1) {
|
|
ret = error(_("the revision range contains more than one tip "
|
|
"commit"));
|
|
goto out;
|
|
} else if (oldest == tip) {
|
|
ret = error(_("the revision range holds a single commit; "
|
|
"nothing to squash"));
|
|
goto out;
|
|
} else if (!oldest->parents) {
|
|
BUG("an in-range commit must have a parent");
|
|
}
|
|
|
|
commit_list_insert(tip, &filter.no_commit);
|
|
filter.kind = FILTER_REFS_BRANCHES;
|
|
if (update_branches &&
|
|
filter_refs(&refs, &filter, filter.kind)) {
|
|
ret = error(_("could not filter refs"));
|
|
goto out;
|
|
}
|
|
if (refs.nr) {
|
|
struct ref_format format = REF_FORMAT_INIT;
|
|
struct ref_sorting *sorting;
|
|
struct string_list sorting_options = STRING_LIST_INIT_DUP;
|
|
struct strbuf branches = STRBUF_INIT;
|
|
struct strbuf err = STRBUF_INIT;
|
|
|
|
format.format = "%(refname:short)";
|
|
if (verify_ref_format(&format))
|
|
BUG("invalid branch format");
|
|
string_list_append(&sorting_options, "refname");
|
|
sorting = ref_sorting_options(&sorting_options);
|
|
ref_array_sort(sorting, &refs);
|
|
for (int i = 0; i < refs.nr; i++) {
|
|
strbuf_reset(&err);
|
|
strbuf_addstr(&branches, "\n ");
|
|
if (format_ref_array_item(refs.items[i], &format,
|
|
&branches, &err))
|
|
BUG("could not format branch name: %s", err.buf);
|
|
}
|
|
/*
|
|
* TODO: also check HEADS from other worktrees.
|
|
*/
|
|
ret = error(_("the following branches cannot be rewritten as "
|
|
"descendants of the squashed commit:%s"), branches.buf);
|
|
advise_if_enabled(ADVICE_HISTORY_UPDATE_REFS,
|
|
_("Use --update-refs=head to rewrite only "
|
|
"the current branch and leave such branches "
|
|
"untouched."));
|
|
strbuf_release(&err);
|
|
strbuf_release(&branches);
|
|
ref_sorting_release(sorting);
|
|
string_list_clear(&sorting_options, 0);
|
|
goto out;
|
|
}
|
|
if (edit_message) {
|
|
strbuf_reset(&subject_data.squash_message);
|
|
ret = build_squash_message(repo, &todo_buf,
|
|
&subject_data.squash_message);
|
|
if (ret < 0)
|
|
goto out;
|
|
}
|
|
|
|
*oldest_out = oldest;
|
|
*tip_out = tip;
|
|
*message_out = strbuf_detach(&subject_data.squash_message, NULL);
|
|
ret = 0;
|
|
|
|
out:
|
|
strbuf_release(&todo_buf);
|
|
clear_object_flags(repo, SQUASH_SEEN | SQUASH_TIP |
|
|
SQUASH_AMEND_TARGET);
|
|
if (walk_started)
|
|
reset_revision_walk();
|
|
subject_data_clear(&subject_data);
|
|
release_revisions(&revs);
|
|
ref_filter_clear(&filter);
|
|
ref_array_clear(&refs);
|
|
return ret;
|
|
}
|
|
|
|
static bool amend_replaces_target(struct todo_list *todo, int target)
|
|
{
|
|
for (int i = target + 1; i < todo->nr &&
|
|
todo->items[i].command != TODO_PICK; i++) {
|
|
if (todo->items[i].command == TODO_SQUASH)
|
|
return false;
|
|
if (todo->items[i].flags & TODO_REPLACE_FIXUP_MSG)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static int build_squash_message(struct repository *repo,
|
|
const struct strbuf *todo_buf,
|
|
struct strbuf *out)
|
|
{
|
|
struct todo_list todo = TODO_LIST_INIT;
|
|
struct replay_opts opts = REPLAY_OPTS_INIT;
|
|
int nr_commits, ret;
|
|
|
|
if (todo_list_parse_insn_buffer(repo, &opts, todo_buf->buf, &todo) < 0 ||
|
|
todo_list_rearrange_squash(&todo) < 0) {
|
|
ret = error(_("could not prepare the squash message"));
|
|
goto out;
|
|
}
|
|
|
|
nr_commits = todo.nr;
|
|
for (int i = 0; i < nr_commits; i++) {
|
|
struct todo_item *item = &todo.items[i];
|
|
const char *message, *body;
|
|
size_t commented_len;
|
|
bool skip, squashing;
|
|
|
|
squashing = item->command == TODO_SQUASH ||
|
|
(item->flags & TODO_REPLACE_FIXUP_MSG);
|
|
if (item->command == TODO_PICK)
|
|
skip = amend_replaces_target(&todo, i);
|
|
else
|
|
skip = !squashing;
|
|
|
|
message = repo_logmsg_reencode(repo, item->commit, NULL, NULL);
|
|
find_commit_subject(message, &body);
|
|
|
|
if (skip)
|
|
commented_len = strlen(body);
|
|
else if (squashing)
|
|
commented_len = squash_subject_comment_len(body, 1);
|
|
else
|
|
commented_len = 0;
|
|
|
|
if (!i)
|
|
add_squash_combination_header(out, nr_commits);
|
|
strbuf_addch(out, '\n');
|
|
add_squash_message_header(out, i + 1, skip);
|
|
strbuf_addstr(out, "\n\n");
|
|
strbuf_add_commented_lines(out, body, commented_len, comment_line_str);
|
|
strbuf_addstr(out, body + commented_len);
|
|
strbuf_complete_line(out);
|
|
|
|
repo_unuse_commit_buffer(repo, item->commit, message);
|
|
}
|
|
|
|
ret = 0;
|
|
|
|
out:
|
|
todo_list_release(&todo);
|
|
replay_opts_release(&opts);
|
|
return ret;
|
|
}
|
|
|
|
static int cmd_history_squash(int argc,
|
|
const char **argv,
|
|
const char *prefix,
|
|
struct repository *repo)
|
|
{
|
|
const char * const usage[] = {
|
|
GIT_HISTORY_SQUASH_USAGE,
|
|
NULL,
|
|
};
|
|
enum ref_action action = REF_ACTION_DEFAULT;
|
|
int dry_run = 0;
|
|
int edit = 1;
|
|
struct option options[] = {
|
|
OPT_CALLBACK_F(0, "update-refs", &action, "(branches|head)",
|
|
N_("control which refs should be updated"),
|
|
PARSE_OPT_NONEG, parse_ref_action),
|
|
OPT_BOOL('n', "dry-run", &dry_run,
|
|
N_("perform a dry-run without updating any refs")),
|
|
OPT_BOOL('e', "edit", &edit,
|
|
N_("edit the commit message")),
|
|
OPT_END(),
|
|
};
|
|
struct strbuf reflog_msg = STRBUF_INIT;
|
|
struct commit *oldest, *tip, *rewritten;
|
|
const struct object_id *base_tree_oid, *tip_tree_oid;
|
|
char *message_template = NULL;
|
|
struct rev_info revs = { 0 };
|
|
int ret;
|
|
|
|
argc = parse_options(argc, argv, prefix, options, usage,
|
|
PARSE_OPT_KEEP_UNKNOWN_OPT | PARSE_OPT_KEEP_ARGV0);
|
|
if (argc < 2) {
|
|
ret = error(_("command expects a revision range"));
|
|
goto out;
|
|
}
|
|
repo_config(repo, git_default_config, NULL);
|
|
|
|
if (action == REF_ACTION_DEFAULT)
|
|
action = REF_ACTION_BRANCHES;
|
|
|
|
strbuf_addstr(&reflog_msg, "squash: updating ");
|
|
strbuf_join_argv(&reflog_msg, argc - 1, argv + 1, ' ');
|
|
|
|
ret = resolve_squash_range(repo, action == REF_ACTION_BRANCHES,
|
|
edit,
|
|
argc, argv, &oldest, &tip,
|
|
&message_template);
|
|
if (ret < 0)
|
|
goto out;
|
|
|
|
ret = setup_revwalk(repo, action, tip, &revs);
|
|
if (ret < 0)
|
|
goto out;
|
|
|
|
base_tree_oid = &repo_get_commit_tree(repo,
|
|
oldest->parents->item)->object.oid;
|
|
tip_tree_oid = &repo_get_commit_tree(repo, tip)->object.oid;
|
|
|
|
ret = commit_tree_ext(repo, "squash", oldest, message_template,
|
|
oldest->parents, base_tree_oid, tip_tree_oid,
|
|
&rewritten,
|
|
edit ? COMMIT_TREE_EDIT_MESSAGE : 0);
|
|
if (ret < 0) {
|
|
ret = error(_("failed writing squashed commit"));
|
|
goto out;
|
|
}
|
|
|
|
ret = handle_reference_updates(&revs, action, tip, rewritten,
|
|
reflog_msg.buf, dry_run,
|
|
REPLAY_EMPTY_COMMIT_ABORT);
|
|
if (ret < 0) {
|
|
ret = error(_("failed replaying descendants"));
|
|
goto out;
|
|
}
|
|
|
|
ret = 0;
|
|
|
|
out:
|
|
strbuf_release(&reflog_msg);
|
|
release_revisions(&revs);
|
|
free(message_template);
|
|
return ret;
|
|
}
|
|
|
|
static int update_worktree(struct repository *repo,
|
|
const struct commit *old_head,
|
|
const struct commit *new_head,
|
|
bool dry_run)
|
|
{
|
|
struct reset_working_tree_options opts = {
|
|
.oid_from = &old_head->object.oid,
|
|
.oid = &new_head->object.oid,
|
|
};
|
|
if (dry_run)
|
|
opts.flags |= RESET_WORKING_TREE_DRY_RUN;
|
|
return reset_working_tree(repo, &opts);
|
|
}
|
|
|
|
static int find_head_tree_change(struct repository *repo,
|
|
const struct replay_result *result,
|
|
struct commit **old_head,
|
|
struct commit **new_head,
|
|
bool *changed)
|
|
{
|
|
const struct replay_ref_update *head_update = NULL;
|
|
struct commit *old_head_commit, *new_head_commit;
|
|
struct tree *old_head_tree, *new_head_tree;
|
|
const char *head_target;
|
|
int head_flags;
|
|
|
|
*changed = false;
|
|
|
|
head_target = refs_resolve_ref_unsafe(get_main_ref_store(repo), "HEAD",
|
|
RESOLVE_REF_NO_RECURSE | RESOLVE_REF_READING,
|
|
NULL, &head_flags);
|
|
if (!head_target)
|
|
return error(_("cannot look up HEAD"));
|
|
|
|
for (size_t i = 0; i < result->updates_nr; i++) {
|
|
if (!strcmp(result->updates[i].refname, head_target)) {
|
|
head_update = &result->updates[i];
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!head_update)
|
|
return 0;
|
|
|
|
old_head_commit = lookup_commit_reference(repo, &head_update->old_oid);
|
|
new_head_commit = lookup_commit_reference(repo, &head_update->new_oid);
|
|
if (!old_head_commit || !new_head_commit)
|
|
return error(_("cannot resolve HEAD commit"));
|
|
|
|
old_head_tree = repo_get_commit_tree(repo, old_head_commit);
|
|
new_head_tree = repo_get_commit_tree(repo, new_head_commit);
|
|
if (!old_head_tree || !new_head_tree)
|
|
return error(_("cannot resolve tree for HEAD"));
|
|
|
|
if (oideq(&old_head_tree->object.oid, &new_head_tree->object.oid))
|
|
return 0;
|
|
|
|
*old_head = old_head_commit;
|
|
*new_head = new_head_commit;
|
|
*changed = true;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int cmd_history_drop(int argc,
|
|
const char **argv,
|
|
const char *prefix,
|
|
struct repository *repo)
|
|
{
|
|
const char * const usage[] = {
|
|
GIT_HISTORY_DROP_USAGE,
|
|
NULL,
|
|
};
|
|
enum replay_empty_commit_action empty = REPLAY_EMPTY_COMMIT_DROP;
|
|
enum ref_action action = REF_ACTION_DEFAULT;
|
|
int dry_run = 0;
|
|
struct option options[] = {
|
|
OPT_CALLBACK_F(0, "update-refs", &action, "(branches|head)",
|
|
N_("control which refs should be updated"),
|
|
PARSE_OPT_NONEG, parse_ref_action),
|
|
OPT_BOOL('n', "dry-run", &dry_run,
|
|
N_("perform a dry-run without updating any refs")),
|
|
OPT_CALLBACK_F(0, "empty", &empty, "(drop|keep|abort)",
|
|
N_("how to handle descendants that become empty"),
|
|
PARSE_OPT_NONEG, parse_opt_empty),
|
|
OPT_END(),
|
|
};
|
|
struct strbuf reflog_msg = STRBUF_INIT;
|
|
struct commit *original, *rewritten;
|
|
struct rev_info revs = { 0 };
|
|
struct replay_result result = { 0 };
|
|
struct commit *old_head, *new_head;
|
|
bool head_moves = false;
|
|
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;
|
|
}
|
|
|
|
if (!original->parents) {
|
|
ret = error(_("cannot drop root commit %s: "
|
|
"it has no parent to replay onto"),
|
|
argv[0]);
|
|
goto out;
|
|
} else if (original->parents->next) {
|
|
ret = error(_("cannot drop merge commit: %s"), argv[0]);
|
|
goto out;
|
|
}
|
|
|
|
ret = setup_revwalk(repo, action, original, &revs);
|
|
if (ret)
|
|
goto out;
|
|
|
|
rewritten = original->parents->item;
|
|
|
|
ret = compute_pending_ref_updates(&revs, action, original, rewritten,
|
|
empty, &result);
|
|
if (ret) {
|
|
ret = error(_("failed replaying descendants"));
|
|
goto out;
|
|
}
|
|
|
|
/*
|
|
* If HEAD will move as a result of the rewrite then we'll have to
|
|
* merge in the changes into the worktree and index. This merge can of
|
|
* course conflict, which will cause the whole operation to abort.
|
|
*
|
|
* If we had already updated the refs at that point then we'd have an
|
|
* inconsistent repository state. So we first perform a dry-run merge
|
|
* here before updating refs.
|
|
*/
|
|
if (!is_bare_repository(repo)) {
|
|
ret = find_head_tree_change(repo, &result, &old_head,
|
|
&new_head, &head_moves);
|
|
if (ret < 0)
|
|
goto out;
|
|
|
|
if (head_moves && update_worktree(repo, old_head, new_head, true) < 0) {
|
|
ret = error(_("dropping this commit would "
|
|
"overwrite local changes; aborting"));
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
strbuf_addf(&reflog_msg, "drop: dropping %s", argv[0]);
|
|
ret = apply_pending_ref_updates(repo, &result, reflog_msg.buf, dry_run);
|
|
if (ret < 0) {
|
|
ret = error(_("failed to update references"));
|
|
goto out;
|
|
}
|
|
|
|
if (!dry_run && head_moves && update_worktree(repo, old_head, new_head, false) < 0) {
|
|
ret = error(_("could not update working tree to new commit %s"),
|
|
oid_to_hex(&new_head->object.oid));
|
|
goto out;
|
|
}
|
|
|
|
ret = 0;
|
|
|
|
out:
|
|
replay_result_release(&result);
|
|
strbuf_release(&reflog_msg);
|
|
release_revisions(&revs);
|
|
return ret;
|
|
}
|
|
|
|
int cmd_history(int argc,
|
|
const char **argv,
|
|
const char *prefix,
|
|
struct repository *repo)
|
|
{
|
|
const char * const usage[] = {
|
|
GIT_HISTORY_DROP_USAGE,
|
|
GIT_HISTORY_FIXUP_USAGE,
|
|
GIT_HISTORY_REWORD_USAGE,
|
|
GIT_HISTORY_SPLIT_USAGE,
|
|
GIT_HISTORY_SQUASH_USAGE,
|
|
NULL,
|
|
};
|
|
parse_opt_subcommand_fn *fn = NULL;
|
|
struct option options[] = {
|
|
OPT_SUBCOMMAND("drop", &fn, cmd_history_drop),
|
|
OPT_SUBCOMMAND("fixup", &fn, cmd_history_fixup),
|
|
OPT_SUBCOMMAND("reword", &fn, cmd_history_reword),
|
|
OPT_SUBCOMMAND("split", &fn, cmd_history_split),
|
|
OPT_SUBCOMMAND("squash", &fn, cmd_history_squash),
|
|
OPT_END(),
|
|
};
|
|
|
|
argc = parse_options(argc, argv, prefix, options, usage, 0);
|
|
return fn(argc, argv, prefix, repo);
|
|
}
|