Files
Junio C Hamano 5cfa270bc4 Merge branch 'hn/history-squash'
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
2026-10-01 11:26:39 -07:00

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);
}