aboutsummaryrefslogtreecommitdiffstats
path: root/builtin
diff options
context:
space:
mode:
Diffstat (limited to 'builtin')
-rw-r--r--builtin/history.c218
1 files changed, 218 insertions, 0 deletions
diff --git a/builtin/history.c b/builtin/history.c
index cb251ae2e0..cae841707d 100644
--- a/builtin/history.c
+++ b/builtin/history.c
@@ -1,6 +1,7 @@
#define USE_THE_REPOSITORY_VARIABLE
#include "builtin.h"
+#include "cache-tree.h"
#include "commit-reach.h"
#include "commit.h"
#include "config.h"
@@ -8,17 +9,22 @@
#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,
@@ -323,6 +329,216 @@ out:
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,
@@ -330,11 +546,13 @@ int cmd_history(int argc,
{
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(),
};