615 lines
17 KiB
C
615 lines
17 KiB
C
#define USE_THE_REPOSITORY_VARIABLE
|
|
|
|
#include "builtin.h"
|
|
#include "cache-tree.h"
|
|
#include "commit-reach.h"
|
|
#include "commit.h"
|
|
#include "config.h"
|
|
#include "editor.h"
|
|
#include "environment.h"
|
|
#include "gettext.h"
|
|
#include "hex.h"
|
|
#include "oidmap.h"
|
|
#include "parse-options.h"
|
|
#include "path.h"
|
|
#include "read-cache.h"
|
|
#include "refs.h"
|
|
#include "replay.h"
|
|
#include "reset.h"
|
|
#include "revision.h"
|
|
#include "run-command.h"
|
|
#include "sequencer.h"
|
|
#include "strvec.h"
|
|
#include "tree.h"
|
|
#include "wt-status.h"
|
|
|
|
static int collect_commits(struct repository *repo,
|
|
struct commit *old_commit,
|
|
struct commit *new_commit,
|
|
struct strvec *out)
|
|
{
|
|
struct setup_revision_opt revision_opts = {
|
|
.assume_dashdash = 1,
|
|
};
|
|
struct strvec revisions = STRVEC_INIT;
|
|
struct commit_list *from_list = NULL;
|
|
struct commit *child;
|
|
struct rev_info rev = { 0 };
|
|
int ret;
|
|
|
|
/*
|
|
* Check that the old commit actually is an ancestor of HEAD. If not
|
|
* the whole request becomes nonsensical.
|
|
*/
|
|
if (old_commit) {
|
|
commit_list_insert(old_commit, &from_list);
|
|
if (!repo_is_descendant_of(repo, new_commit, from_list)) {
|
|
ret = error(_("commit must be reachable from current HEAD commit"));
|
|
goto out;
|
|
}
|
|
}
|
|
|
|
repo_init_revisions(repo, &rev, NULL);
|
|
strvec_push(&revisions, "");
|
|
strvec_push(&revisions, oid_to_hex(&new_commit->object.oid));
|
|
if (old_commit)
|
|
strvec_pushf(&revisions, "^%s", oid_to_hex(&old_commit->object.oid));
|
|
|
|
setup_revisions_from_strvec(&revisions, &rev, &revision_opts);
|
|
if (revisions.nr != 1 || prepare_revision_walk(&rev)) {
|
|
ret = error(_("revision walk setup failed"));
|
|
goto out;
|
|
}
|
|
|
|
while ((child = get_revision(&rev))) {
|
|
if (old_commit && !child->parents)
|
|
BUG("revision walk did not find child commit");
|
|
if (child->parents && child->parents->next) {
|
|
ret = error(_("cannot rearrange commit history with merges"));
|
|
goto out;
|
|
}
|
|
|
|
strvec_push(out, oid_to_hex(&child->object.oid));
|
|
|
|
if (child->parents && old_commit &&
|
|
commit_list_contains(old_commit, child->parents))
|
|
break;
|
|
}
|
|
|
|
/*
|
|
* Revisions are in newest-order-first. We have to reverse the
|
|
* array though so that we pick the oldest commits first.
|
|
*/
|
|
for (size_t i = 0, j = out->nr - 1; i < j; i++, j--)
|
|
SWAP(out->v[i], out->v[j]);
|
|
|
|
ret = 0;
|
|
|
|
out:
|
|
free_commit_list(from_list);
|
|
strvec_clear(&revisions);
|
|
release_revisions(&rev);
|
|
reset_revision_walk();
|
|
return ret;
|
|
}
|
|
|
|
static void replace_commits(struct strvec *commits,
|
|
const struct object_id *commit_to_replace,
|
|
const struct object_id *replacements,
|
|
size_t replacements_nr)
|
|
{
|
|
char commit_to_replace_oid[GIT_MAX_HEXSZ + 1];
|
|
struct strvec replacement_oids = STRVEC_INIT;
|
|
bool found = false;
|
|
|
|
oid_to_hex_r(commit_to_replace_oid, commit_to_replace);
|
|
for (size_t i = 0; i < replacements_nr; i++)
|
|
strvec_push(&replacement_oids, oid_to_hex(&replacements[i]));
|
|
|
|
for (size_t i = 0; i < commits->nr; i++) {
|
|
if (strcmp(commits->v[i], commit_to_replace_oid))
|
|
continue;
|
|
strvec_splice(commits, i, 1, replacement_oids.v, replacement_oids.nr);
|
|
found = true;
|
|
break;
|
|
}
|
|
if (!found)
|
|
BUG("could not find commit to replace");
|
|
|
|
strvec_clear(&replacement_oids);
|
|
}
|
|
|
|
static int apply_commits(struct repository *repo,
|
|
const struct strvec *commits,
|
|
struct commit *onto,
|
|
struct commit *orig_head,
|
|
const char *action)
|
|
{
|
|
struct reset_head_opts reset_opts = { 0 };
|
|
struct merge_options merge_opts = { 0 };
|
|
struct merge_result result = { 0 };
|
|
struct strbuf buf = STRBUF_INIT;
|
|
kh_oid_map_t *replayed_commits;
|
|
int ret;
|
|
|
|
replayed_commits = kh_init_oid_map();
|
|
|
|
init_basic_merge_options(&merge_opts, repo);
|
|
merge_opts.show_rename_progress = 0;
|
|
|
|
for (size_t i = 0; i < commits->nr; i++) {
|
|
struct object_id commit_id;
|
|
struct commit *commit;
|
|
const char *end;
|
|
int hash_result;
|
|
khint_t pos;
|
|
|
|
if (parse_oid_hex_algop(commits->v[i], &commit_id, &end,
|
|
repo->hash_algo)) {
|
|
ret = error(_("invalid object ID: %s"), commits->v[i]);
|
|
goto out;
|
|
}
|
|
|
|
commit = lookup_commit(repo, &commit_id);
|
|
if (!commit || repo_parse_commit(repo, commit)) {
|
|
ret = error(_("failed to look up commit: %s"), oid_to_hex(&commit_id));
|
|
goto out;
|
|
}
|
|
|
|
if (!onto) {
|
|
onto = commit;
|
|
result.clean = 1;
|
|
result.tree = repo_get_commit_tree(repo, commit);
|
|
} else {
|
|
onto = replay_pick_regular_commit(repo, commit, replayed_commits,
|
|
onto, &merge_opts, &result);
|
|
if (!onto)
|
|
break;
|
|
}
|
|
|
|
pos = kh_put_oid_map(replayed_commits, commit->object.oid, &hash_result);
|
|
if (hash_result == 0) {
|
|
ret = error(_("duplicate rewritten commit: %s\n"),
|
|
oid_to_hex(&commit->object.oid));
|
|
goto out;
|
|
}
|
|
kh_value(replayed_commits, pos) = onto;
|
|
}
|
|
|
|
if (!result.clean) {
|
|
ret = error(_("could not merge"));
|
|
goto out;
|
|
}
|
|
|
|
reset_opts.oid = &onto->object.oid;
|
|
strbuf_addf(&buf, "%s: switch to rewritten %s", action, oid_to_hex(reset_opts.oid));
|
|
reset_opts.flags = RESET_HEAD_REFS_ONLY | RESET_ORIG_HEAD;
|
|
reset_opts.orig_head = &orig_head->object.oid;
|
|
reset_opts.default_reflog_action = action;
|
|
if (reset_head(repo, &reset_opts) < 0) {
|
|
ret = error(_("could not switch to %s"), oid_to_hex(reset_opts.oid));
|
|
goto out;
|
|
}
|
|
|
|
ret = 0;
|
|
|
|
out:
|
|
kh_destroy_oid_map(replayed_commits);
|
|
merge_finalize(&merge_opts, &result);
|
|
strbuf_release(&buf);
|
|
return ret;
|
|
}
|
|
|
|
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 *provided_message,
|
|
const char *action,
|
|
struct strbuf *out)
|
|
{
|
|
if (!provided_message) {
|
|
const char *path = git_path_commit_editmsg();
|
|
const char *hint =
|
|
_("Please enter the commit message for the %s changes. Lines starting\n"
|
|
"with '%s' will be kept; you may remove them yourself if you want to.\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, _("Please supply the message using the -m option.\n"));
|
|
return -1;
|
|
}
|
|
strbuf_stripspace(out, comment_line_str);
|
|
} else {
|
|
strbuf_addstr(out, provided_message);
|
|
}
|
|
|
|
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 cmd_history_reword(int argc,
|
|
const char **argv,
|
|
const char *prefix,
|
|
struct repository *repo)
|
|
{
|
|
const char * const usage[] = {
|
|
N_("git history reword [<options>] <commit>"),
|
|
NULL,
|
|
};
|
|
const char *commit_message = NULL;
|
|
struct option options[] = {
|
|
OPT_STRING('m', "message", &commit_message, N_("message"), N_("commit message")),
|
|
OPT_END(),
|
|
};
|
|
struct strbuf final_message = STRBUF_INIT;
|
|
struct commit *original_commit, *parent, *head;
|
|
struct strvec commits = STRVEC_INIT;
|
|
struct object_id parent_tree_oid, original_commit_tree_oid;
|
|
struct object_id rewritten_commit;
|
|
const char *original_message, *original_body, *ptr;
|
|
char *original_author = NULL;
|
|
size_t len;
|
|
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_commit = lookup_commit_reference_by_name(argv[0]);
|
|
if (!original_commit) {
|
|
ret = error(_("commit to be reworded cannot be found: %s"), argv[0]);
|
|
goto out;
|
|
}
|
|
if (repo_parse_commit(repo, original_commit)) {
|
|
ret = error(_("unable to parse commit %s"),
|
|
oid_to_hex(&original_commit->object.oid));
|
|
goto out;
|
|
}
|
|
original_commit_tree_oid = repo_get_commit_tree(repo, original_commit)->object.oid;
|
|
|
|
parent = original_commit->parents ? original_commit->parents->item : NULL;
|
|
if (parent) {
|
|
if (repo_parse_commit(repo, parent)) {
|
|
ret = error(_("unable to parse 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);
|
|
}
|
|
|
|
head = lookup_commit_reference_by_name("HEAD");
|
|
if (!head) {
|
|
ret = error(_("could not resolve HEAD to a commit"));
|
|
goto out;
|
|
}
|
|
|
|
/*
|
|
* Collect the list of commits that we'll have to reapply now already.
|
|
* This ensures that we'll abort early on in case the range of commits
|
|
* contains merges, which we do not yet handle.
|
|
*/
|
|
ret = collect_commits(repo, parent, head, &commits);
|
|
if (ret < 0)
|
|
goto out;
|
|
|
|
/* We retain authorship of the original commit. */
|
|
original_message = repo_logmsg_reencode(repo, original_commit, 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_commit_tree_oid,
|
|
original_body, commit_message, "reworded", &final_message);
|
|
if (ret < 0)
|
|
goto out;
|
|
|
|
ret = commit_tree(final_message.buf, final_message.len,
|
|
&repo_get_commit_tree(repo, original_commit)->object.oid,
|
|
original_commit->parents, &rewritten_commit, original_author, NULL);
|
|
if (ret < 0) {
|
|
ret = error(_("failed writing reworded commit"));
|
|
goto out;
|
|
}
|
|
|
|
replace_commits(&commits, &original_commit->object.oid, &rewritten_commit, 1);
|
|
|
|
ret = apply_commits(repo, &commits, parent, head, "reword");
|
|
if (ret < 0)
|
|
goto out;
|
|
|
|
ret = 0;
|
|
|
|
out:
|
|
strbuf_release(&final_message);
|
|
strvec_clear(&commits);
|
|
free(original_author);
|
|
return ret;
|
|
}
|
|
|
|
static int split_commit(struct repository *repo,
|
|
struct commit *original_commit,
|
|
struct pathspec *pathspec,
|
|
const char *commit_message,
|
|
struct object_id *out)
|
|
{
|
|
struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
|
|
struct strbuf index_file = STRBUF_INIT, split_message = STRBUF_INIT;
|
|
struct child_process read_tree_cmd = CHILD_PROCESS_INIT;
|
|
struct index_state index = INDEX_STATE_INIT(repo);
|
|
struct object_id original_commit_tree_oid, parent_tree_oid;
|
|
const char *original_message, *original_body, *ptr;
|
|
char original_commit_oid[GIT_MAX_HEXSZ + 1];
|
|
char *original_author = NULL;
|
|
struct commit_list *parents = NULL;
|
|
struct commit *first_commit;
|
|
struct tree *split_tree;
|
|
size_t len;
|
|
int ret;
|
|
|
|
if (original_commit->parents)
|
|
parent_tree_oid = *get_commit_tree_oid(original_commit->parents->item);
|
|
else
|
|
oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
|
|
original_commit_tree_oid = *get_commit_tree_oid(original_commit);
|
|
|
|
/*
|
|
* 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");
|
|
|
|
read_tree_cmd.git_cmd = 1;
|
|
strvec_pushf(&read_tree_cmd.env, "GIT_INDEX_FILE=%s", index_file.buf);
|
|
strvec_push(&read_tree_cmd.args, "read-tree");
|
|
strvec_push(&read_tree_cmd.args, oid_to_hex(&parent_tree_oid));
|
|
ret = run_command(&read_tree_cmd);
|
|
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_commit->object.oid);
|
|
ret = run_add_p_index(repo, &index, index_file.buf, &interactive_opts,
|
|
original_commit_oid, pathspec);
|
|
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);
|
|
|
|
/*
|
|
* 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;
|
|
}
|
|
|
|
/* We retain authorship of the original commit. */
|
|
original_message = repo_logmsg_reencode(repo, original_commit, NULL, NULL);
|
|
ptr = find_commit_header(original_message, "author", &len);
|
|
if (ptr)
|
|
original_author = xmemdupz(ptr, len);
|
|
|
|
ret = fill_commit_message(repo, &parent_tree_oid, &split_tree->object.oid,
|
|
"", commit_message, "split-out", &split_message);
|
|
if (ret < 0)
|
|
goto out;
|
|
|
|
ret = commit_tree(split_message.buf, split_message.len, &split_tree->object.oid,
|
|
original_commit->parents, &out[0], original_author, NULL);
|
|
if (ret < 0) {
|
|
ret = error(_("failed writing split-out commit"));
|
|
goto out;
|
|
}
|
|
|
|
/*
|
|
* The second commit is much simpler to construct, as we can simply use
|
|
* the original commit details, except that we adjust its parent to be
|
|
* the newly split-out commit.
|
|
*/
|
|
find_commit_subject(original_message, &original_body);
|
|
first_commit = lookup_commit_reference(repo, &out[0]);
|
|
commit_list_append(first_commit, &parents);
|
|
|
|
ret = commit_tree(original_body, strlen(original_body), &original_commit_tree_oid,
|
|
parents, &out[1], original_author, NULL);
|
|
if (ret < 0) {
|
|
ret = error(_("failed writing second commit"));
|
|
goto out;
|
|
}
|
|
|
|
ret = 0;
|
|
|
|
out:
|
|
if (index_file.len)
|
|
unlink(index_file.buf);
|
|
strbuf_release(&split_message);
|
|
strbuf_release(&index_file);
|
|
free_commit_list(parents);
|
|
free(original_author);
|
|
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[] = {
|
|
N_("git history split [<options>] <commit>"),
|
|
NULL,
|
|
};
|
|
const char *commit_message = NULL;
|
|
struct option options[] = {
|
|
OPT_STRING('m', "message", &commit_message, N_("message"), N_("commit message")),
|
|
OPT_END(),
|
|
};
|
|
struct oidmap rewritten_commits = OIDMAP_INIT;
|
|
struct commit *original_commit, *parent, *head;
|
|
struct strvec commits = STRVEC_INIT;
|
|
struct commit_list *list = NULL;
|
|
struct object_id split_commits[2];
|
|
struct pathspec pathspec = { 0 };
|
|
int ret;
|
|
|
|
argc = parse_options(argc, argv, prefix, options, usage, 0);
|
|
if (argc < 1) {
|
|
ret = error(_("command expects a revision"));
|
|
goto out;
|
|
}
|
|
repo_config(repo, git_default_config, NULL);
|
|
|
|
original_commit = lookup_commit_reference_by_name(argv[0]);
|
|
if (!original_commit) {
|
|
ret = error(_("commit to be split cannot be found: %s"), argv[0]);
|
|
goto out;
|
|
}
|
|
|
|
if (original_commit->parents && original_commit->parents->next) {
|
|
ret = error(_("commit to be split must not be a merge commit"));
|
|
goto out;
|
|
}
|
|
|
|
parent = original_commit->parents ? original_commit->parents->item : NULL;
|
|
if (parent && repo_parse_commit(repo, parent)) {
|
|
ret = error(_("unable to parse commit %s"),
|
|
oid_to_hex(&parent->object.oid));
|
|
goto out;
|
|
}
|
|
|
|
head = lookup_commit_reference_by_name("HEAD");
|
|
if (!head) {
|
|
ret = error(_("could not resolve HEAD to a commit"));
|
|
goto out;
|
|
}
|
|
|
|
commit_list_append(original_commit, &list);
|
|
if (!repo_is_descendant_of(repo, original_commit, list)) {
|
|
ret = error (_("split commit must be reachable from current HEAD commit"));
|
|
goto out;
|
|
}
|
|
|
|
parse_pathspec(&pathspec, 0,
|
|
PATHSPEC_PREFER_FULL | PATHSPEC_SYMLINK_LEADING_PATH | PATHSPEC_PREFIX_ORIGIN,
|
|
prefix, argv + 1);
|
|
|
|
/*
|
|
* Collect the list of commits that we'll have to reapply now already.
|
|
* This ensures that we'll abort early on in case the range of commits
|
|
* contains merges, which we do not yet handle.
|
|
*/
|
|
ret = collect_commits(repo, parent, head, &commits);
|
|
if (ret < 0)
|
|
goto out;
|
|
|
|
/*
|
|
* Then we split up the commit and replace the original commit with the
|
|
* new new ones.
|
|
*/
|
|
ret = split_commit(repo, original_commit, &pathspec,
|
|
commit_message, split_commits);
|
|
if (ret < 0)
|
|
goto out;
|
|
|
|
replace_commits(&commits, &original_commit->object.oid,
|
|
split_commits, ARRAY_SIZE(split_commits));
|
|
|
|
ret = apply_commits(repo, &commits, parent, head, "split");
|
|
if (ret < 0)
|
|
goto out;
|
|
|
|
ret = 0;
|
|
|
|
out:
|
|
oidmap_clear(&rewritten_commits, 0);
|
|
clear_pathspec(&pathspec);
|
|
strvec_clear(&commits);
|
|
free_commit_list(list);
|
|
return ret;
|
|
}
|
|
|
|
int cmd_history(int argc,
|
|
const char **argv,
|
|
const char *prefix,
|
|
struct repository *repo)
|
|
{
|
|
const char * const usage[] = {
|
|
N_("git history [<options>]"),
|
|
N_("git history reword [<options>] <commit>"),
|
|
N_("git history split [<options>] <commit> [--] [<pathspec>...]"),
|
|
NULL,
|
|
};
|
|
parse_opt_subcommand_fn *fn = NULL;
|
|
struct option options[] = {
|
|
OPT_SUBCOMMAND("reword", &fn, cmd_history_reword),
|
|
OPT_SUBCOMMAND("split", &fn, cmd_history_split),
|
|
OPT_END(),
|
|
};
|
|
|
|
argc = parse_options(argc, argv, prefix, options, usage, 0);
|
|
return fn(argc, argv, prefix, repo);
|
|
}
|