aboutsummaryrefslogtreecommitdiffstats
path: root/builtin
diff options
context:
space:
mode:
authorJunio C Hamano <gitster@pobox.com>2025-11-08 10:33:19 -0800
committerJunio C Hamano <gitster@pobox.com>2025-11-08 10:33:19 -0800
commit374eaa29420f6c01755fc579e08032fdb9962a7c (patch)
tree1c82d8fdfb7bf79615c95be2277388addcbb2bb7 /builtin
parentMerge branch 'ms/doc-worktree-side-by-side' into seen (diff)
parentbuiltin/history: implement "split" subcommand (diff)
downloadgit-374eaa29420f6c01755fc579e08032fdb9962a7c.tar.gz
git-374eaa29420f6c01755fc579e08032fdb9962a7c.zip
Merge branch 'ps/history' into seen
"git history" history rewriting UI. Comments? * ps/history: builtin/history: implement "split" subcommand cache-tree: allow writing in-memory index as tree add-patch: add support for in-memory index patching add-patch: remove dependency on "add-interactive" subsystem add-patch: split out `struct interactive_options` add-patch: split out header from "add-interactive.h" builtin/history: implement "reword" subcommand builtin: add new "history" command replay: stop using `the_repository` replay: extract logic to pick commits wt-status: provide function to expose status for trees
Diffstat (limited to 'builtin')
-rw-r--r--builtin/add.c22
-rw-r--r--builtin/checkout.c7
-rw-r--r--builtin/commit.c16
-rw-r--r--builtin/history.c561
-rw-r--r--builtin/replay.c110
-rw-r--r--builtin/reset.c16
-rw-r--r--builtin/stash.c46
7 files changed, 619 insertions, 159 deletions
diff --git a/builtin/add.c b/builtin/add.c
index 32709794b3..6f1e213052 100644
--- a/builtin/add.c
+++ b/builtin/add.c
@@ -31,7 +31,7 @@ static const char * const builtin_add_usage[] = {
NULL
};
static int patch_interactive, add_interactive, edit_interactive;
-static struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+static struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
static int take_worktree_changes;
static int add_renormalize;
static int pathspec_file_nul;
@@ -160,7 +160,7 @@ static int refresh(struct repository *repo, int verbose, const struct pathspec *
int interactive_add(struct repository *repo,
const char **argv,
const char *prefix,
- int patch, struct add_p_opt *add_p_opt)
+ int patch, struct interactive_options *interactive_opts)
{
struct pathspec pathspec;
int ret;
@@ -172,9 +172,9 @@ int interactive_add(struct repository *repo,
prefix, argv);
if (patch)
- ret = !!run_add_p(repo, ADD_P_ADD, add_p_opt, NULL, &pathspec);
+ ret = !!run_add_p(repo, ADD_P_ADD, interactive_opts, NULL, &pathspec);
else
- ret = !!run_add_i(repo, &pathspec, add_p_opt);
+ ret = !!run_add_i(repo, &pathspec, interactive_opts);
clear_pathspec(&pathspec);
return ret;
@@ -256,8 +256,8 @@ static struct option builtin_add_options[] = {
OPT_GROUP(""),
OPT_BOOL('i', "interactive", &add_interactive, N_("interactive picking")),
OPT_BOOL('p', "patch", &patch_interactive, N_("select hunks interactively")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT_BOOL('e', "edit", &edit_interactive, N_("edit current diff and apply")),
OPT__FORCE(&ignored_too, N_("allow adding otherwise ignored files"), 0),
OPT_BOOL('u', "update", &take_worktree_changes, N_("update tracked files")),
@@ -400,9 +400,9 @@ int cmd_add(int argc,
prepare_repo_settings(repo);
repo->settings.command_requires_full_index = 0;
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
if (patch_interactive)
@@ -412,11 +412,11 @@ int cmd_add(int argc,
die(_("options '%s' and '%s' cannot be used together"), "--dry-run", "--interactive/--patch");
if (pathspec_from_file)
die(_("options '%s' and '%s' cannot be used together"), "--pathspec-from-file", "--interactive/--patch");
- exit(interactive_add(repo, argv + 1, prefix, patch_interactive, &add_p_opt));
+ exit(interactive_add(repo, argv + 1, prefix, patch_interactive, &interactive_opts));
} else {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--interactive/--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--interactive/--patch");
}
diff --git a/builtin/checkout.c b/builtin/checkout.c
index 66b69df6e6..c09065cc61 100644
--- a/builtin/checkout.c
+++ b/builtin/checkout.c
@@ -546,7 +546,7 @@ static int checkout_paths(const struct checkout_opts *opts,
if (opts->patch_mode) {
enum add_p_mode patch_mode;
- struct add_p_opt add_p_opt = {
+ struct interactive_options interactive_opts = {
.context = opts->patch_context,
.interhunkcontext = opts->patch_interhunk_context,
};
@@ -575,7 +575,7 @@ static int checkout_paths(const struct checkout_opts *opts,
else
BUG("either flag must have been set, worktree=%d, index=%d",
opts->checkout_worktree, opts->checkout_index);
- return !!run_add_p(the_repository, patch_mode, &add_p_opt,
+ return !!run_add_p(the_repository, patch_mode, &interactive_opts,
rev, &opts->pathspec);
}
@@ -902,7 +902,8 @@ static int merge_working_tree(const struct checkout_opts *opts,
0);
init_ui_merge_options(&o, the_repository);
o.verbosity = 0;
- work = write_in_core_index_as_tree(the_repository);
+ work = write_in_core_index_as_tree(the_repository,
+ the_repository->index);
ret = reset_tree(new_tree,
opts, 1,
diff --git a/builtin/commit.c b/builtin/commit.c
index 0243f17d53..640495cc57 100644
--- a/builtin/commit.c
+++ b/builtin/commit.c
@@ -123,7 +123,7 @@ static const char *edit_message, *use_message;
static char *fixup_message, *fixup_commit, *squash_message;
static const char *fixup_prefix;
static int all, also, interactive, patch_interactive, only, amend, signoff;
-static struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+static struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
static int edit_flag = -1; /* unspecified */
static int quiet, verbose, no_verify, allow_empty, dry_run, renew_authorship;
static int config_commit_verbose = -1; /* unspecified */
@@ -356,9 +356,9 @@ static const char *prepare_index(const char **argv, const char *prefix,
const char *ret;
char *path = NULL;
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
if (is_status)
@@ -407,7 +407,7 @@ static const char *prepare_index(const char **argv, const char *prefix,
old_index_env = xstrdup_or_null(getenv(INDEX_ENVIRONMENT));
setenv(INDEX_ENVIRONMENT, the_repository->index_file, 1);
- if (interactive_add(the_repository, argv, prefix, patch_interactive, &add_p_opt) != 0)
+ if (interactive_add(the_repository, argv, prefix, patch_interactive, &interactive_opts) != 0)
die(_("interactive add failed"));
the_repository->index_file = old_repo_index_file;
@@ -432,9 +432,9 @@ static const char *prepare_index(const char **argv, const char *prefix,
ret = get_lock_file_path(&index_lock);
goto out;
} else {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--interactive/--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--interactive/--patch");
}
@@ -1742,8 +1742,8 @@ int cmd_commit(int argc,
OPT_BOOL('i', "include", &also, N_("add specified files to index for commit")),
OPT_BOOL(0, "interactive", &interactive, N_("interactively add files")),
OPT_BOOL('p', "patch", &patch_interactive, N_("interactively add changes")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT_BOOL('o', "only", &only, N_("commit only specified files")),
OPT_BOOL('n', "no-verify", &no_verify, N_("bypass pre-commit and commit-msg hooks")),
OPT_BOOL(0, "dry-run", &dry_run, N_("show what would be committed")),
diff --git a/builtin/history.c b/builtin/history.c
new file mode 100644
index 0000000000..cae841707d
--- /dev/null
+++ b/builtin/history.c
@@ -0,0 +1,561 @@
+#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"
+
+#define GIT_HISTORY_REWORD_USAGE N_("git history reword <commit>")
+#define GIT_HISTORY_SPLIT_USAGE N_("git history split <commit> [--] [<pathspec>...]")
+
+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 *child;
+ struct rev_info rev = { 0 };
+ int ret;
+
+ 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:
+ 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 strbuf buf = STRBUF_INIT;
+ int ret;
+
+ for (size_t i = 0; i < commits->nr; i++) {
+ struct object_id commit_id;
+ struct commit *commit;
+ const char *end;
+
+ 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;
+ } else {
+ struct tree *tree = repo_get_commit_tree(repo, commit);
+ onto = replay_create_commit(repo, tree, commit, onto);
+ if (!onto)
+ break;
+ }
+ }
+
+ 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:
+ 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 *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.\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);
+
+ 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[] = {
+ GIT_HISTORY_REWORD_USAGE,
+ NULL,
+ };
+ struct option options[] = {
+ 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;
+ struct commit_list *from_list = NULL;
+ 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;
+ }
+ 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;
+ }
+
+ commit_list_append(original_commit, &from_list);
+ if (!repo_is_descendant_of(repo, head, from_list)) {
+ ret = error (_("split commit must be reachable from current HEAD 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, "reworded", &final_message);
+ if (ret < 0)
+ goto out;
+
+ ret = commit_tree(final_message.buf, final_message.len, &original_commit_tree_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);
+ free_commit_list(from_list);
+ strvec_clear(&commits);
+ free(original_author);
+ return ret;
+}
+
+static int split_commit(struct repository *repo,
+ struct commit *original_commit,
+ struct pathspec *pathspec,
+ 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,
+ "", "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[] = {
+ GIT_HISTORY_SPLIT_USAGE,
+ NULL,
+ };
+ struct option options[] = {
+ OPT_END(),
+ };
+ struct oidmap rewritten_commits = OIDMAP_INIT;
+ struct commit *original_commit, *parent, *head;
+ struct strvec commits = STRVEC_INIT;
+ struct commit_list *from_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;
+ }
+
+ 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, &from_list);
+ if (!repo_is_descendant_of(repo, head, from_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 ones.
+ */
+ ret = split_commit(repo, original_commit, &pathspec, 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);
+ free_commit_list(from_list);
+ clear_pathspec(&pathspec);
+ strvec_clear(&commits);
+ return ret;
+}
+
+int cmd_history(int argc,
+ const char **argv,
+ const char *prefix,
+ struct repository *repo)
+{
+ const char * const usage[] = {
+ GIT_HISTORY_REWORD_USAGE,
+ GIT_HISTORY_SPLIT_USAGE,
+ 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);
+}
diff --git a/builtin/replay.c b/builtin/replay.c
index 6606a2c94b..f974a8c963 100644
--- a/builtin/replay.c
+++ b/builtin/replay.c
@@ -2,7 +2,6 @@
* "git replay" builtin command
*/
-#define USE_THE_REPOSITORY_VARIABLE
#define DISABLE_SIGN_COMPARE_WARNINGS
#include "git-compat-util.h"
@@ -16,6 +15,7 @@
#include "object-name.h"
#include "parse-options.h"
#include "refs.h"
+#include "replay.h"
#include "revision.h"
#include "strmap.h"
#include <oidset.h>
@@ -26,13 +26,6 @@ enum ref_action_mode {
REF_ACTION_PRINT,
};
-static const char *short_commit_name(struct repository *repo,
- struct commit *commit)
-{
- return repo_find_unique_abbrev(repo, &commit->object.oid,
- DEFAULT_ABBREV);
-}
-
static struct commit *peel_committish(struct repository *repo, const char *name)
{
struct object *obj;
@@ -45,59 +38,6 @@ static struct commit *peel_committish(struct repository *repo, const char *name)
OBJ_COMMIT);
}
-static char *get_author(const char *message)
-{
- size_t len;
- const char *a;
-
- a = find_commit_header(message, "author", &len);
- if (a)
- return xmemdupz(a, len);
-
- return NULL;
-}
-
-static struct commit *create_commit(struct repository *repo,
- struct tree *tree,
- struct commit *based_on,
- struct commit *parent)
-{
- struct object_id ret;
- struct object *obj = NULL;
- struct commit_list *parents = NULL;
- char *author;
- char *sign_commit = NULL; /* FIXME: cli users might want to sign again */
- struct commit_extra_header *extra = NULL;
- struct strbuf msg = STRBUF_INIT;
- const char *out_enc = get_commit_output_encoding();
- const char *message = repo_logmsg_reencode(repo, based_on,
- NULL, out_enc);
- const char *orig_message = NULL;
- const char *exclude_gpgsig[] = { "gpgsig", NULL };
-
- commit_list_insert(parent, &parents);
- extra = read_commit_extra_headers(based_on, exclude_gpgsig);
- find_commit_subject(message, &orig_message);
- strbuf_addstr(&msg, orig_message);
- author = get_author(message);
- reset_ident_date();
- if (commit_tree_extended(msg.buf, msg.len, &tree->object.oid, parents,
- &ret, author, NULL, sign_commit, extra)) {
- error(_("failed to write commit object"));
- goto out;
- }
-
- obj = parse_object(repo, &ret);
-
-out:
- repo_unuse_commit_buffer(the_repository, based_on, message);
- free_commit_extra_headers(extra);
- free_commit_list(parents);
- strbuf_release(&msg);
- free(author);
- return (struct commit *)obj;
-}
-
struct ref_info {
struct commit *onto;
struct strset positive_refs;
@@ -246,50 +186,6 @@ static void determine_replay_mode(struct repository *repo,
strset_clear(&rinfo.positive_refs);
}
-static struct commit *mapped_commit(kh_oid_map_t *replayed_commits,
- struct commit *commit,
- struct commit *fallback)
-{
- khint_t pos = kh_get_oid_map(replayed_commits, commit->object.oid);
- if (pos == kh_end(replayed_commits))
- return fallback;
- return kh_value(replayed_commits, pos);
-}
-
-static struct commit *pick_regular_commit(struct repository *repo,
- struct commit *pickme,
- kh_oid_map_t *replayed_commits,
- struct commit *onto,
- struct merge_options *merge_opt,
- struct merge_result *result)
-{
- struct commit *base, *replayed_base;
- struct tree *pickme_tree, *base_tree;
-
- base = pickme->parents->item;
- replayed_base = mapped_commit(replayed_commits, base, onto);
-
- result->tree = repo_get_commit_tree(repo, replayed_base);
- pickme_tree = repo_get_commit_tree(repo, pickme);
- base_tree = repo_get_commit_tree(repo, base);
-
- merge_opt->branch1 = short_commit_name(repo, replayed_base);
- merge_opt->branch2 = short_commit_name(repo, pickme);
- merge_opt->ancestor = xstrfmt("parent of %s", merge_opt->branch2);
-
- merge_incore_nonrecursive(merge_opt,
- base_tree,
- result->tree,
- pickme_tree,
- result);
-
- free((char*)merge_opt->ancestor);
- merge_opt->ancestor = NULL;
- if (!result->clean)
- return NULL;
- return create_commit(repo, result->tree, pickme, replayed_base);
-}
-
static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)
{
if (!ref_action || !strcmp(ref_action, "update"))
@@ -495,8 +391,8 @@ int cmd_replay(int argc,
if (commit->parents->next)
die(_("replaying merge commits is not supported yet!"));
- last_commit = pick_regular_commit(repo, commit, replayed_commits,
- onto, &merge_opt, &result);
+ last_commit = replay_pick_regular_commit(repo, commit, replayed_commits,
+ onto, &merge_opt, &result);
if (!last_commit)
break;
diff --git a/builtin/reset.c b/builtin/reset.c
index ed35802af1..088449e120 100644
--- a/builtin/reset.c
+++ b/builtin/reset.c
@@ -346,7 +346,7 @@ int cmd_reset(int argc,
struct object_id oid;
struct pathspec pathspec;
int intent_to_add = 0;
- struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+ struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
const struct option options[] = {
OPT__QUIET(&quiet, N_("be quiet, only report errors")),
OPT_BOOL(0, "no-refresh", &no_refresh,
@@ -371,8 +371,8 @@ int cmd_reset(int argc,
PARSE_OPT_OPTARG,
option_parse_recurse_submodules_worktree_updater),
OPT_BOOL('p', "patch", &patch_mode, N_("select hunks interactively")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT_BOOL('N', "intent-to-add", &intent_to_add,
N_("record only the fact that removed paths will be added later")),
OPT_PATHSPEC_FROM_FILE(&pathspec_from_file),
@@ -423,9 +423,9 @@ int cmd_reset(int argc,
oidcpy(&oid, &tree->object.oid);
}
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
prepare_repo_settings(the_repository);
@@ -436,12 +436,12 @@ int cmd_reset(int argc,
die(_("options '%s' and '%s' cannot be used together"), "--patch", "--{hard,mixed,soft}");
trace2_cmd_mode("patch-interactive");
update_ref_status = !!run_add_p(the_repository, ADD_P_RESET,
- &add_p_opt, rev, &pathspec);
+ &interactive_opts, rev, &pathspec);
goto cleanup;
} else {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--patch");
}
diff --git a/builtin/stash.c b/builtin/stash.c
index 948eba06fb..3b50905233 100644
--- a/builtin/stash.c
+++ b/builtin/stash.c
@@ -1306,7 +1306,7 @@ done:
static int stash_patch(struct stash_info *info, const struct pathspec *ps,
struct strbuf *out_patch, int quiet,
- struct add_p_opt *add_p_opt)
+ struct interactive_options *interactive_opts)
{
int ret = 0;
struct child_process cp_read_tree = CHILD_PROCESS_INIT;
@@ -1331,7 +1331,7 @@ static int stash_patch(struct stash_info *info, const struct pathspec *ps,
old_index_env = xstrdup_or_null(getenv(INDEX_ENVIRONMENT));
setenv(INDEX_ENVIRONMENT, the_repository->index_file, 1);
- ret = !!run_add_p(the_repository, ADD_P_STASH, add_p_opt, NULL, ps);
+ ret = !!run_add_p(the_repository, ADD_P_STASH, interactive_opts, NULL, ps);
the_repository->index_file = old_repo_index_file;
if (old_index_env && *old_index_env)
@@ -1427,7 +1427,8 @@ done:
}
static int do_create_stash(const struct pathspec *ps, struct strbuf *stash_msg_buf,
- int include_untracked, int patch_mode, struct add_p_opt *add_p_opt,
+ int include_untracked, int patch_mode,
+ struct interactive_options *interactive_opts,
int only_staged, struct stash_info *info, struct strbuf *patch,
int quiet)
{
@@ -1509,7 +1510,7 @@ static int do_create_stash(const struct pathspec *ps, struct strbuf *stash_msg_b
untracked_commit_option = 1;
}
if (patch_mode) {
- ret = stash_patch(info, ps, patch, quiet, add_p_opt);
+ ret = stash_patch(info, ps, patch, quiet, interactive_opts);
if (ret < 0) {
if (!quiet)
fprintf_ln(stderr, _("Cannot save the current "
@@ -1595,7 +1596,8 @@ static int create_stash(int argc, const char **argv, const char *prefix UNUSED,
}
static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int quiet,
- int keep_index, int patch_mode, struct add_p_opt *add_p_opt,
+ int keep_index, int patch_mode,
+ struct interactive_options *interactive_opts,
int include_untracked, int only_staged)
{
int ret = 0;
@@ -1667,7 +1669,7 @@ static int do_push_stash(const struct pathspec *ps, const char *stash_msg, int q
if (stash_msg)
strbuf_addstr(&stash_msg_buf, stash_msg);
if (do_create_stash(ps, &stash_msg_buf, include_untracked, patch_mode,
- add_p_opt, only_staged, &info, &patch, quiet)) {
+ interactive_opts, only_staged, &info, &patch, quiet)) {
ret = -1;
goto done;
}
@@ -1841,7 +1843,7 @@ static int push_stash(int argc, const char **argv, const char *prefix,
const char *stash_msg = NULL;
char *pathspec_from_file = NULL;
struct pathspec ps;
- struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+ struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
struct option options[] = {
OPT_BOOL('k', "keep-index", &keep_index,
N_("keep index")),
@@ -1849,8 +1851,8 @@ static int push_stash(int argc, const char **argv, const char *prefix,
N_("stash staged changes only")),
OPT_BOOL('p', "patch", &patch_mode,
N_("stash in patch mode")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT__QUIET(&quiet, N_("quiet mode")),
OPT_BOOL('u', "include-untracked", &include_untracked,
N_("include untracked files in stash")),
@@ -1907,19 +1909,19 @@ static int push_stash(int argc, const char **argv, const char *prefix,
}
if (!patch_mode) {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--patch");
}
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
ret = do_push_stash(&ps, stash_msg, quiet, keep_index, patch_mode,
- &add_p_opt, include_untracked, only_staged);
+ &interactive_opts, include_untracked, only_staged);
clear_pathspec(&ps);
free(pathspec_from_file);
@@ -1944,7 +1946,7 @@ static int save_stash(int argc, const char **argv, const char *prefix,
const char *stash_msg = NULL;
struct pathspec ps;
struct strbuf stash_msg_buf = STRBUF_INIT;
- struct add_p_opt add_p_opt = ADD_P_OPT_INIT;
+ struct interactive_options interactive_opts = INTERACTIVE_OPTIONS_INIT;
struct option options[] = {
OPT_BOOL('k', "keep-index", &keep_index,
N_("keep index")),
@@ -1952,8 +1954,8 @@ static int save_stash(int argc, const char **argv, const char *prefix,
N_("stash staged changes only")),
OPT_BOOL('p', "patch", &patch_mode,
N_("stash in patch mode")),
- OPT_DIFF_UNIFIED(&add_p_opt.context),
- OPT_DIFF_INTERHUNK_CONTEXT(&add_p_opt.interhunkcontext),
+ OPT_DIFF_UNIFIED(&interactive_opts.context),
+ OPT_DIFF_INTERHUNK_CONTEXT(&interactive_opts.interhunkcontext),
OPT__QUIET(&quiet, N_("quiet mode")),
OPT_BOOL('u', "include-untracked", &include_untracked,
N_("include untracked files in stash")),
@@ -1973,20 +1975,20 @@ static int save_stash(int argc, const char **argv, const char *prefix,
memset(&ps, 0, sizeof(ps));
- if (add_p_opt.context < -1)
+ if (interactive_opts.context < -1)
die(_("'%s' cannot be negative"), "--unified");
- if (add_p_opt.interhunkcontext < -1)
+ if (interactive_opts.interhunkcontext < -1)
die(_("'%s' cannot be negative"), "--inter-hunk-context");
if (!patch_mode) {
- if (add_p_opt.context != -1)
+ if (interactive_opts.context != -1)
die(_("the option '%s' requires '%s'"), "--unified", "--patch");
- if (add_p_opt.interhunkcontext != -1)
+ if (interactive_opts.interhunkcontext != -1)
die(_("the option '%s' requires '%s'"), "--inter-hunk-context", "--patch");
}
ret = do_push_stash(&ps, stash_msg, quiet, keep_index,
- patch_mode, &add_p_opt, include_untracked,
+ patch_mode, &interactive_opts, include_untracked,
only_staged);
strbuf_release(&stash_msg_buf);