diff options
| author | Junio C Hamano <gitster@pobox.com> | 2025-11-08 10:33:19 -0800 |
|---|---|---|
| committer | Junio C Hamano <gitster@pobox.com> | 2025-11-08 10:33:19 -0800 |
| commit | 374eaa29420f6c01755fc579e08032fdb9962a7c (patch) | |
| tree | 1c82d8fdfb7bf79615c95be2277388addcbb2bb7 /builtin | |
| parent | Merge branch 'ms/doc-worktree-side-by-side' into seen (diff) | |
| parent | builtin/history: implement "split" subcommand (diff) | |
| download | git-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.c | 22 | ||||
| -rw-r--r-- | builtin/checkout.c | 7 | ||||
| -rw-r--r-- | builtin/commit.c | 16 | ||||
| -rw-r--r-- | builtin/history.c | 561 | ||||
| -rw-r--r-- | builtin/replay.c | 110 | ||||
| -rw-r--r-- | builtin/reset.c | 16 | ||||
| -rw-r--r-- | builtin/stash.c | 46 |
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); |
