mirror of
https://github.com/git/git.git
synced 2026-01-11 13:23:12 +09:00
Implement a new "reword" subcommand for git-history(1). This subcommand
is similar to the user performing an interactive rebase with a single
commit changed to use the "reword" instruction.
The "reword" subcommand is built on top of the replay subsystem
instead of the sequencer. This leads to some major differences compared
to git-rebase(1):
- We do not check out the commit that is to be reworded and instead
perform the operation in-memory. This has the obvious benefit of
being significantly faster compared to git-rebase(1), but even more
importantly it allows the user to rewrite history even if there are
local changes in the working tree or in the index.
- We do not execute any hooks, even though we leave some room for
changing this in the future.
- By default, all local branches that contain the commit will be
rewritten. This especially helps with workflows that use stacked
branches.
Signed-off-by: Patrick Steinhardt <ps@pks.im>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
405 lines
11 KiB
C
405 lines
11 KiB
C
#define USE_THE_REPOSITORY_VARIABLE
|
|
|
|
#include "builtin.h"
|
|
#include "commit.h"
|
|
#include "commit-reach.h"
|
|
#include "config.h"
|
|
#include "editor.h"
|
|
#include "environment.h"
|
|
#include "gettext.h"
|
|
#include "hex.h"
|
|
#include "parse-options.h"
|
|
#include "refs.h"
|
|
#include "replay.h"
|
|
#include "revision.h"
|
|
#include "sequencer.h"
|
|
#include "strvec.h"
|
|
#include "tree.h"
|
|
#include "wt-status.h"
|
|
|
|
#define GIT_HISTORY_REWORD_USAGE \
|
|
N_("git history reword <commit> [--ref-action=(branches|head|print)]")
|
|
|
|
static void change_data_free(void *util, const char *str UNUSED)
|
|
{
|
|
struct wt_status_change_data *d = util;
|
|
free(d->rename_source);
|
|
free(d);
|
|
}
|
|
|
|
static int fill_commit_message(struct repository *repo,
|
|
const struct object_id *old_tree,
|
|
const struct object_id *new_tree,
|
|
const char *default_message,
|
|
const char *action,
|
|
struct strbuf *out)
|
|
{
|
|
const char *path = git_path_commit_editmsg();
|
|
const char *hint =
|
|
_("Please enter the commit message for the %s changes."
|
|
" Lines starting\nwith '%s' will be ignored, and an"
|
|
" empty message aborts the commit.\n");
|
|
struct wt_status s;
|
|
|
|
strbuf_addstr(out, default_message);
|
|
strbuf_addch(out, '\n');
|
|
strbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);
|
|
write_file_buf(path, out->buf, out->len);
|
|
|
|
wt_status_prepare(repo, &s);
|
|
FREE_AND_NULL(s.branch);
|
|
s.ahead_behind_flags = AHEAD_BEHIND_QUICK;
|
|
s.commit_template = 1;
|
|
s.colopts = 0;
|
|
s.display_comment_prefix = 1;
|
|
s.hints = 0;
|
|
s.use_color = 0;
|
|
s.whence = FROM_COMMIT;
|
|
s.committable = 1;
|
|
|
|
s.fp = fopen(git_path_commit_editmsg(), "a");
|
|
if (!s.fp)
|
|
return error_errno(_("could not open '%s'"), git_path_commit_editmsg());
|
|
|
|
wt_status_collect_changes_trees(&s, old_tree, new_tree);
|
|
wt_status_print(&s);
|
|
wt_status_collect_free_buffers(&s);
|
|
string_list_clear_func(&s.change, change_data_free);
|
|
|
|
strbuf_reset(out);
|
|
if (launch_editor(path, out, NULL)) {
|
|
fprintf(stderr, _("Aborting commit as launching the editor failed.\n"));
|
|
return -1;
|
|
}
|
|
strbuf_stripspace(out, comment_line_str);
|
|
|
|
cleanup_message(out, COMMIT_MSG_CLEANUP_ALL, 0);
|
|
|
|
if (!out->len) {
|
|
fprintf(stderr, _("Aborting commit due to empty commit message.\n"));
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int commit_tree_with_edited_message(struct repository *repo,
|
|
const char *action,
|
|
struct commit *original,
|
|
struct commit **out)
|
|
{
|
|
const char *exclude_gpgsig[] = { "gpgsig", "gpgsig-sha256", NULL };
|
|
const char *original_message, *original_body, *ptr;
|
|
struct commit_extra_header *original_extra_headers = NULL;
|
|
struct strbuf commit_message = STRBUF_INIT;
|
|
struct object_id rewritten_commit_oid;
|
|
struct object_id original_tree_oid;
|
|
struct object_id parent_tree_oid;
|
|
char *original_author = NULL;
|
|
struct commit *parent;
|
|
size_t len;
|
|
int ret;
|
|
|
|
original_tree_oid = repo_get_commit_tree(repo, original)->object.oid;
|
|
|
|
parent = original->parents ? original->parents->item : NULL;
|
|
if (parent) {
|
|
if (repo_parse_commit(repo, parent)) {
|
|
ret = error(_("unable to parse parent commit %s"),
|
|
oid_to_hex(&parent->object.oid));
|
|
goto out;
|
|
}
|
|
|
|
parent_tree_oid = repo_get_commit_tree(repo, parent)->object.oid;
|
|
} else {
|
|
oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
|
|
}
|
|
|
|
/* We retain authorship of the original commit. */
|
|
original_message = repo_logmsg_reencode(repo, original, NULL, NULL);
|
|
ptr = find_commit_header(original_message, "author", &len);
|
|
if (ptr)
|
|
original_author = xmemdupz(ptr, len);
|
|
find_commit_subject(original_message, &original_body);
|
|
|
|
ret = fill_commit_message(repo, &parent_tree_oid, &original_tree_oid,
|
|
original_body, action, &commit_message);
|
|
if (ret < 0)
|
|
goto out;
|
|
|
|
original_extra_headers = read_commit_extra_headers(original, exclude_gpgsig);
|
|
|
|
ret = commit_tree_extended(commit_message.buf, commit_message.len, &original_tree_oid,
|
|
original->parents, &rewritten_commit_oid, original_author,
|
|
NULL, NULL, original_extra_headers);
|
|
if (ret < 0)
|
|
goto out;
|
|
|
|
*out = lookup_commit_or_die(&rewritten_commit_oid, "rewritten commit");
|
|
|
|
out:
|
|
free_commit_extra_headers(original_extra_headers);
|
|
strbuf_release(&commit_message);
|
|
free(original_author);
|
|
return ret;
|
|
}
|
|
|
|
enum ref_action {
|
|
REF_ACTION_DEFAULT,
|
|
REF_ACTION_BRANCHES,
|
|
REF_ACTION_HEAD,
|
|
REF_ACTION_PRINT,
|
|
};
|
|
|
|
static int parse_ref_action(const struct option *opt, const char *value, int unset)
|
|
{
|
|
enum ref_action *action = opt->value;
|
|
|
|
BUG_ON_OPT_NEG_NOARG(unset, value);
|
|
if (!strcmp(value, "branches")) {
|
|
*action = REF_ACTION_BRANCHES;
|
|
} else if (!strcmp(value, "head")) {
|
|
*action = REF_ACTION_HEAD;
|
|
} else if (!strcmp(value, "print")) {
|
|
*action = REF_ACTION_PRINT;
|
|
} else {
|
|
return error(_("%s expects one of 'branches', 'head' or 'print'"),
|
|
opt->long_name);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int handle_reference_updates(enum ref_action action,
|
|
struct repository *repo,
|
|
struct commit *original,
|
|
struct commit *rewritten,
|
|
const char *reflog_msg)
|
|
{
|
|
const struct name_decoration *decoration;
|
|
struct replay_revisions_options opts = { 0 };
|
|
struct replay_ref_updates updates = {
|
|
.final_oid = rewritten->object.oid,
|
|
};
|
|
struct ref_transaction *transaction = NULL;
|
|
struct strvec args = STRVEC_INIT;
|
|
struct strbuf err = STRBUF_INIT;
|
|
struct commit *head = NULL;
|
|
struct rev_info revs;
|
|
char hex[GIT_MAX_HEXSZ + 1];
|
|
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;
|
|
|
|
head = lookup_commit_reference_by_name("HEAD");
|
|
if (!head) {
|
|
ret = error(_("cannot look up HEAD"));
|
|
goto out;
|
|
}
|
|
|
|
commit_list_insert(original, &from_list);
|
|
ret = repo_is_descendant_of(repo, head, from_list);
|
|
free_commit_list(from_list);
|
|
|
|
if (ret < 0) {
|
|
ret = error(_("cannot determine descendance"));
|
|
goto out;
|
|
} else if (!ret) {
|
|
ret = error(_("rewritten commit must be an ancestor "
|
|
"of HEAD when using --ref-action=head"));
|
|
goto out;
|
|
}
|
|
|
|
strvec_push(&args, oid_to_hex(&head->object.oid));
|
|
} else {
|
|
strvec_push(&args, "--branches");
|
|
}
|
|
|
|
setup_revisions_from_strvec(&args, &revs, NULL);
|
|
if (revs.nr)
|
|
BUG("revisions were set up with invalid argument '%s'", args.v[0]);
|
|
|
|
opts.onto = oid_to_hex_r(hex, &rewritten->object.oid);
|
|
|
|
ret = replay_revisions(repo, &revs, &opts, &updates);
|
|
if (ret)
|
|
goto out;
|
|
|
|
switch (action) {
|
|
case REF_ACTION_DEFAULT:
|
|
case REF_ACTION_BRANCHES:
|
|
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 < updates.nr; i++) {
|
|
ret = ref_transaction_update(transaction,
|
|
updates.items[i].refname,
|
|
&updates.items[i].new_oid,
|
|
&updates.items[i].old_oid,
|
|
NULL, NULL, 0, reflog_msg, &err);
|
|
if (ret) {
|
|
ret = error(_("failed to update ref '%s': %s"),
|
|
updates.items[i].refname, err.buf);
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* `replay_revisions()` only updates references that are
|
|
* ancestors of `rewritten`, so we need to manually
|
|
* handle updating references that point to `original`.
|
|
*/
|
|
for (decoration = get_name_decoration(&original->object);
|
|
decoration;
|
|
decoration = decoration->next)
|
|
{
|
|
if (decoration->type != DECORATION_REF_LOCAL)
|
|
continue;
|
|
|
|
ret = ref_transaction_update(transaction,
|
|
decoration->name,
|
|
&rewritten->object.oid,
|
|
&original->object.oid,
|
|
NULL, NULL, 0, reflog_msg, &err);
|
|
if (ret) {
|
|
ret = error(_("failed to update ref '%s': %s"),
|
|
decoration->name, err.buf);
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
if (ref_transaction_commit(transaction, &err)) {
|
|
ret = error(_("failed to commit ref transaction: %s"), err.buf);
|
|
goto out;
|
|
}
|
|
|
|
break;
|
|
case REF_ACTION_HEAD:
|
|
ret = refs_update_ref(get_main_ref_store(repo), reflog_msg, "HEAD",
|
|
&updates.final_oid, &head->object.oid, 0,
|
|
UPDATE_REFS_MSG_ON_ERR);
|
|
if (ret)
|
|
goto out;
|
|
break;
|
|
case REF_ACTION_PRINT:
|
|
for (size_t i = 0; i < updates.nr; i++)
|
|
printf("update %s %s %s\n",
|
|
updates.items[i].refname,
|
|
oid_to_hex(&updates.items[i].new_oid),
|
|
oid_to_hex(&updates.items[i].old_oid));
|
|
break;
|
|
default:
|
|
BUG("unsupported ref action %d", action);
|
|
}
|
|
|
|
ret = 0;
|
|
|
|
out:
|
|
replay_ref_updates_release(&updates);
|
|
ref_transaction_free(transaction);
|
|
release_revisions(&revs);
|
|
strbuf_release(&err);
|
|
strvec_clear(&args);
|
|
return ret;
|
|
}
|
|
|
|
static int cmd_history_reword(int argc,
|
|
const char **argv,
|
|
const char *prefix,
|
|
struct repository *repo)
|
|
{
|
|
const char * const usage[] = {
|
|
GIT_HISTORY_REWORD_USAGE,
|
|
NULL,
|
|
};
|
|
enum ref_action action = REF_ACTION_DEFAULT;
|
|
struct option options[] = {
|
|
OPT_CALLBACK_F(0, "ref-action", &action, N_("<action>"),
|
|
N_("control ref update behavior (branches|head|print)"),
|
|
PARSE_OPT_NONEG, parse_ref_action),
|
|
OPT_END(),
|
|
};
|
|
struct strbuf reflog_msg = STRBUF_INIT;
|
|
struct commit *original, *rewritten;
|
|
int ret;
|
|
|
|
argc = parse_options(argc, argv, prefix, options, usage, 0);
|
|
if (argc != 1) {
|
|
ret = error(_("command expects a single revision"));
|
|
goto out;
|
|
}
|
|
repo_config(repo, git_default_config, NULL);
|
|
|
|
original = lookup_commit_reference_by_name(argv[0]);
|
|
if (!original) {
|
|
ret = error(_("commit cannot be found: %s"), argv[0]);
|
|
goto out;
|
|
}
|
|
|
|
ret = commit_tree_with_edited_message(repo, "reworded", original, &rewritten);
|
|
if (ret < 0) {
|
|
ret = error(_("failed writing reworded commit"));
|
|
goto out;
|
|
}
|
|
|
|
strbuf_addf(&reflog_msg, "reword: updating %s", argv[0]);
|
|
|
|
ret = handle_reference_updates(action, repo, original, rewritten,
|
|
reflog_msg.buf);
|
|
if (ret < 0) {
|
|
ret = error(_("failed replaying descendants"));
|
|
goto out;
|
|
}
|
|
|
|
ret = 0;
|
|
|
|
out:
|
|
strbuf_release(&reflog_msg);
|
|
return ret;
|
|
}
|
|
|
|
int cmd_history(int argc,
|
|
const char **argv,
|
|
const char *prefix,
|
|
struct repository *repo)
|
|
{
|
|
const char * const usage[] = {
|
|
GIT_HISTORY_REWORD_USAGE,
|
|
NULL,
|
|
};
|
|
parse_opt_subcommand_fn *fn = NULL;
|
|
struct option options[] = {
|
|
OPT_SUBCOMMAND("reword", &fn, cmd_history_reword),
|
|
OPT_END(),
|
|
};
|
|
|
|
argc = parse_options(argc, argv, prefix, options, usage, 0);
|
|
return fn(argc, argv, prefix, repo);
|
|
}
|