mirror of
https://github.com/git/git.git
synced 2026-01-11 13:23:12 +09:00
Merge branch 'ps/history' into seen
"git history" history rewriting UI. Comments? * ps/history: builtin/history: implement "reword" subcommand builtin: add new "history" command wt-status: provide function to expose status for trees replay: yield the object ID of the final rewritten commit replay: small set of cleanups builtin/replay: move core logic into "libgit.a" builtin/replay: extract core logic to replay revisions
This commit is contained in:
commit
276ca121e0
1
.gitignore
vendored
1
.gitignore
vendored
@ -79,6 +79,7 @@
|
|||||||
/git-grep
|
/git-grep
|
||||||
/git-hash-object
|
/git-hash-object
|
||||||
/git-help
|
/git-help
|
||||||
|
/git-history
|
||||||
/git-hook
|
/git-hook
|
||||||
/git-http-backend
|
/git-http-backend
|
||||||
/git-http-fetch
|
/git-http-fetch
|
||||||
|
|||||||
73
Documentation/git-history.adoc
Normal file
73
Documentation/git-history.adoc
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
git-history(1)
|
||||||
|
==============
|
||||||
|
|
||||||
|
NAME
|
||||||
|
----
|
||||||
|
git-history - EXPERIMENTAL: Rewrite history
|
||||||
|
|
||||||
|
SYNOPSIS
|
||||||
|
--------
|
||||||
|
[synopsis]
|
||||||
|
git history reword <commit> [--ref-action=(branches|head|print)]
|
||||||
|
|
||||||
|
DESCRIPTION
|
||||||
|
-----------
|
||||||
|
|
||||||
|
Rewrite history by rearranging or modifying specific commits in the
|
||||||
|
history.
|
||||||
|
|
||||||
|
THIS COMMAND IS EXPERIMENTAL. THE BEHAVIOR MAY CHANGE.
|
||||||
|
|
||||||
|
This command is related to linkgit:git-rebase[1] in that both commands can be
|
||||||
|
used to rewrite history. There are a couple of major differences though:
|
||||||
|
|
||||||
|
* linkgit:git-history[1] can work in a bare repository as it does not need to
|
||||||
|
touch either the index or the worktree.
|
||||||
|
* linkgit:git-history[1] does not execute any linkgit:githooks[5] at the
|
||||||
|
current point in time. This may change in the future.
|
||||||
|
* linkgit:git-history[1] by default updates all branches that are descendants
|
||||||
|
of the original commit to point to the rewritten commit.
|
||||||
|
|
||||||
|
Overall, linkgit:git-history[1] aims to provide a more opinionated way to modify
|
||||||
|
your commit history that is simpler to use compared to linkgit:git-rebase[1] in
|
||||||
|
general.
|
||||||
|
|
||||||
|
Use linkgit:git-rebase[1] if you want to reapply a range of commits onto a
|
||||||
|
different base, or interactive rebases if you want to edit a range of commits
|
||||||
|
at once.
|
||||||
|
|
||||||
|
LIMITATIONS
|
||||||
|
-----------
|
||||||
|
|
||||||
|
This command does not (yet) work with histories that contain merges. You
|
||||||
|
should use linkgit:git-rebase[1] with the `--rebase-merges` flag instead.
|
||||||
|
|
||||||
|
Furthermore, the command does not support operations that can result in merge
|
||||||
|
conflicts. This limitation is by design as history rewrites are not intended to
|
||||||
|
be stateful operations. The limitation can be lifted once (if) Git learns about
|
||||||
|
first-class conflicts.
|
||||||
|
|
||||||
|
COMMANDS
|
||||||
|
--------
|
||||||
|
|
||||||
|
Several commands are available to rewrite history in different ways:
|
||||||
|
|
||||||
|
`reword <commit>`::
|
||||||
|
Rewrite the commit message of the specified commit. All the other
|
||||||
|
details of this commit remain unchanged. This command will spawn an
|
||||||
|
editor with the current message of that commit.
|
||||||
|
|
||||||
|
OPTIONS
|
||||||
|
-------
|
||||||
|
|
||||||
|
`--ref-action=(branches|head|print)`::
|
||||||
|
Control which references will be updated by the command, if any. With
|
||||||
|
`branches`, all local branches that point to commits which are
|
||||||
|
decendants of the original commit will be rewritten. With `head`, only
|
||||||
|
the current `HEAD` reference will be rewritten. With `print`, all
|
||||||
|
updates as they would be performed with `branches` are printed in a
|
||||||
|
format that can be consumed by linkgit:git-update-ref[1].
|
||||||
|
|
||||||
|
GIT
|
||||||
|
---
|
||||||
|
Part of the linkgit:git[1] suite
|
||||||
@ -64,6 +64,7 @@ manpages = {
|
|||||||
'git-gui.adoc' : 1,
|
'git-gui.adoc' : 1,
|
||||||
'git-hash-object.adoc' : 1,
|
'git-hash-object.adoc' : 1,
|
||||||
'git-help.adoc' : 1,
|
'git-help.adoc' : 1,
|
||||||
|
'git-history.adoc' : 1,
|
||||||
'git-hook.adoc' : 1,
|
'git-hook.adoc' : 1,
|
||||||
'git-http-backend.adoc' : 1,
|
'git-http-backend.adoc' : 1,
|
||||||
'git-http-fetch.adoc' : 1,
|
'git-http-fetch.adoc' : 1,
|
||||||
|
|||||||
2
Makefile
2
Makefile
@ -1286,6 +1286,7 @@ LIB_OBJS += repack-geometry.o
|
|||||||
LIB_OBJS += repack-midx.o
|
LIB_OBJS += repack-midx.o
|
||||||
LIB_OBJS += repack-promisor.o
|
LIB_OBJS += repack-promisor.o
|
||||||
LIB_OBJS += replace-object.o
|
LIB_OBJS += replace-object.o
|
||||||
|
LIB_OBJS += replay.o
|
||||||
LIB_OBJS += repo-settings.o
|
LIB_OBJS += repo-settings.o
|
||||||
LIB_OBJS += repository.o
|
LIB_OBJS += repository.o
|
||||||
LIB_OBJS += rerere.o
|
LIB_OBJS += rerere.o
|
||||||
@ -1418,6 +1419,7 @@ BUILTIN_OBJS += builtin/get-tar-commit-id.o
|
|||||||
BUILTIN_OBJS += builtin/grep.o
|
BUILTIN_OBJS += builtin/grep.o
|
||||||
BUILTIN_OBJS += builtin/hash-object.o
|
BUILTIN_OBJS += builtin/hash-object.o
|
||||||
BUILTIN_OBJS += builtin/help.o
|
BUILTIN_OBJS += builtin/help.o
|
||||||
|
BUILTIN_OBJS += builtin/history.o
|
||||||
BUILTIN_OBJS += builtin/hook.o
|
BUILTIN_OBJS += builtin/hook.o
|
||||||
BUILTIN_OBJS += builtin/index-pack.o
|
BUILTIN_OBJS += builtin/index-pack.o
|
||||||
BUILTIN_OBJS += builtin/init-db.o
|
BUILTIN_OBJS += builtin/init-db.o
|
||||||
|
|||||||
@ -196,6 +196,7 @@ int cmd_get_tar_commit_id(int argc, const char **argv, const char *prefix, struc
|
|||||||
int cmd_grep(int argc, const char **argv, const char *prefix, struct repository *repo);
|
int cmd_grep(int argc, const char **argv, const char *prefix, struct repository *repo);
|
||||||
int cmd_hash_object(int argc, const char **argv, const char *prefix, struct repository *repo);
|
int cmd_hash_object(int argc, const char **argv, const char *prefix, struct repository *repo);
|
||||||
int cmd_help(int argc, const char **argv, const char *prefix, struct repository *repo);
|
int cmd_help(int argc, const char **argv, const char *prefix, struct repository *repo);
|
||||||
|
int cmd_history(int argc, const char **argv, const char *prefix, struct repository *repo);
|
||||||
int cmd_hook(int argc, const char **argv, const char *prefix, struct repository *repo);
|
int cmd_hook(int argc, const char **argv, const char *prefix, struct repository *repo);
|
||||||
int cmd_index_pack(int argc, const char **argv, const char *prefix, struct repository *repo);
|
int cmd_index_pack(int argc, const char **argv, const char *prefix, struct repository *repo);
|
||||||
int cmd_init_db(int argc, const char **argv, const char *prefix, struct repository *repo);
|
int cmd_init_db(int argc, const char **argv, const char *prefix, struct repository *repo);
|
||||||
|
|||||||
404
builtin/history.c
Normal file
404
builtin/history.c
Normal file
@ -0,0 +1,404 @@
|
|||||||
|
#define USE_THE_REPOSITORY_VARIABLE
|
||||||
|
|
||||||
|
#include "builtin.h"
|
||||||
|
#include "commit.h"
|
||||||
|
#include "commit-reach.h"
|
||||||
|
#include "config.h"
|
||||||
|
#include "editor.h"
|
||||||
|
#include "environment.h"
|
||||||
|
#include "gettext.h"
|
||||||
|
#include "hex.h"
|
||||||
|
#include "parse-options.h"
|
||||||
|
#include "refs.h"
|
||||||
|
#include "replay.h"
|
||||||
|
#include "revision.h"
|
||||||
|
#include "sequencer.h"
|
||||||
|
#include "strvec.h"
|
||||||
|
#include "tree.h"
|
||||||
|
#include "wt-status.h"
|
||||||
|
|
||||||
|
#define GIT_HISTORY_REWORD_USAGE \
|
||||||
|
N_("git history reword <commit> [--ref-action=(branches|head|print)]")
|
||||||
|
|
||||||
|
static void change_data_free(void *util, const char *str UNUSED)
|
||||||
|
{
|
||||||
|
struct wt_status_change_data *d = util;
|
||||||
|
free(d->rename_source);
|
||||||
|
free(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int fill_commit_message(struct repository *repo,
|
||||||
|
const struct object_id *old_tree,
|
||||||
|
const struct object_id *new_tree,
|
||||||
|
const char *default_message,
|
||||||
|
const char *action,
|
||||||
|
struct strbuf *out)
|
||||||
|
{
|
||||||
|
const char *path = git_path_commit_editmsg();
|
||||||
|
const char *hint =
|
||||||
|
_("Please enter the commit message for the %s changes."
|
||||||
|
" Lines starting\nwith '%s' will be ignored, and an"
|
||||||
|
" empty message aborts the commit.\n");
|
||||||
|
struct wt_status s;
|
||||||
|
|
||||||
|
strbuf_addstr(out, default_message);
|
||||||
|
strbuf_addch(out, '\n');
|
||||||
|
strbuf_commented_addf(out, comment_line_str, hint, action, comment_line_str);
|
||||||
|
write_file_buf(path, out->buf, out->len);
|
||||||
|
|
||||||
|
wt_status_prepare(repo, &s);
|
||||||
|
FREE_AND_NULL(s.branch);
|
||||||
|
s.ahead_behind_flags = AHEAD_BEHIND_QUICK;
|
||||||
|
s.commit_template = 1;
|
||||||
|
s.colopts = 0;
|
||||||
|
s.display_comment_prefix = 1;
|
||||||
|
s.hints = 0;
|
||||||
|
s.use_color = 0;
|
||||||
|
s.whence = FROM_COMMIT;
|
||||||
|
s.committable = 1;
|
||||||
|
|
||||||
|
s.fp = fopen(git_path_commit_editmsg(), "a");
|
||||||
|
if (!s.fp)
|
||||||
|
return error_errno(_("could not open '%s'"), git_path_commit_editmsg());
|
||||||
|
|
||||||
|
wt_status_collect_changes_trees(&s, old_tree, new_tree);
|
||||||
|
wt_status_print(&s);
|
||||||
|
wt_status_collect_free_buffers(&s);
|
||||||
|
string_list_clear_func(&s.change, change_data_free);
|
||||||
|
|
||||||
|
strbuf_reset(out);
|
||||||
|
if (launch_editor(path, out, NULL)) {
|
||||||
|
fprintf(stderr, _("Aborting commit as launching the editor failed.\n"));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
strbuf_stripspace(out, comment_line_str);
|
||||||
|
|
||||||
|
cleanup_message(out, COMMIT_MSG_CLEANUP_ALL, 0);
|
||||||
|
|
||||||
|
if (!out->len) {
|
||||||
|
fprintf(stderr, _("Aborting commit due to empty commit message.\n"));
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int commit_tree_with_edited_message(struct repository *repo,
|
||||||
|
const char *action,
|
||||||
|
struct commit *original,
|
||||||
|
struct commit **out)
|
||||||
|
{
|
||||||
|
const char *exclude_gpgsig[] = { "gpgsig", "gpgsig-sha256", NULL };
|
||||||
|
const char *original_message, *original_body, *ptr;
|
||||||
|
struct commit_extra_header *original_extra_headers = NULL;
|
||||||
|
struct strbuf commit_message = STRBUF_INIT;
|
||||||
|
struct object_id rewritten_commit_oid;
|
||||||
|
struct object_id original_tree_oid;
|
||||||
|
struct object_id parent_tree_oid;
|
||||||
|
char *original_author = NULL;
|
||||||
|
struct commit *parent;
|
||||||
|
size_t len;
|
||||||
|
int ret;
|
||||||
|
|
||||||
|
original_tree_oid = repo_get_commit_tree(repo, original)->object.oid;
|
||||||
|
|
||||||
|
parent = original->parents ? original->parents->item : NULL;
|
||||||
|
if (parent) {
|
||||||
|
if (repo_parse_commit(repo, parent)) {
|
||||||
|
ret = error(_("unable to parse parent commit %s"),
|
||||||
|
oid_to_hex(&parent->object.oid));
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
parent_tree_oid = repo_get_commit_tree(repo, parent)->object.oid;
|
||||||
|
} else {
|
||||||
|
oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* We retain authorship of the original commit. */
|
||||||
|
original_message = repo_logmsg_reencode(repo, original, NULL, NULL);
|
||||||
|
ptr = find_commit_header(original_message, "author", &len);
|
||||||
|
if (ptr)
|
||||||
|
original_author = xmemdupz(ptr, len);
|
||||||
|
find_commit_subject(original_message, &original_body);
|
||||||
|
|
||||||
|
ret = fill_commit_message(repo, &parent_tree_oid, &original_tree_oid,
|
||||||
|
original_body, action, &commit_message);
|
||||||
|
if (ret < 0)
|
||||||
|
goto out;
|
||||||
|
|
||||||
|
original_extra_headers = read_commit_extra_headers(original, exclude_gpgsig);
|
||||||
|
|
||||||
|
ret = commit_tree_extended(commit_message.buf, commit_message.len, &original_tree_oid,
|
||||||
|
original->parents, &rewritten_commit_oid, original_author,
|
||||||
|
NULL, NULL, original_extra_headers);
|
||||||
|
if (ret < 0)
|
||||||
|
goto out;
|
||||||
|
|
||||||
|
*out = lookup_commit_or_die(&rewritten_commit_oid, "rewritten commit");
|
||||||
|
|
||||||
|
out:
|
||||||
|
free_commit_extra_headers(original_extra_headers);
|
||||||
|
strbuf_release(&commit_message);
|
||||||
|
free(original_author);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ref_action {
|
||||||
|
REF_ACTION_DEFAULT,
|
||||||
|
REF_ACTION_BRANCHES,
|
||||||
|
REF_ACTION_HEAD,
|
||||||
|
REF_ACTION_PRINT,
|
||||||
|
};
|
||||||
|
|
||||||
|
static int parse_ref_action(const struct option *opt, const char *value, int unset)
|
||||||
|
{
|
||||||
|
enum ref_action *action = opt->value;
|
||||||
|
|
||||||
|
BUG_ON_OPT_NEG_NOARG(unset, value);
|
||||||
|
if (!strcmp(value, "branches")) {
|
||||||
|
*action = REF_ACTION_BRANCHES;
|
||||||
|
} else if (!strcmp(value, "head")) {
|
||||||
|
*action = REF_ACTION_HEAD;
|
||||||
|
} else if (!strcmp(value, "print")) {
|
||||||
|
*action = REF_ACTION_PRINT;
|
||||||
|
} else {
|
||||||
|
return error(_("%s expects one of 'branches', 'head' or 'print'"),
|
||||||
|
opt->long_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int handle_reference_updates(enum ref_action action,
|
||||||
|
struct repository *repo,
|
||||||
|
struct commit *original,
|
||||||
|
struct commit *rewritten,
|
||||||
|
const char *reflog_msg)
|
||||||
|
{
|
||||||
|
const struct name_decoration *decoration;
|
||||||
|
struct replay_revisions_options opts = { 0 };
|
||||||
|
struct replay_result result = {
|
||||||
|
.final_oid = rewritten->object.oid,
|
||||||
|
};
|
||||||
|
struct ref_transaction *transaction = NULL;
|
||||||
|
struct strvec args = STRVEC_INIT;
|
||||||
|
struct strbuf err = STRBUF_INIT;
|
||||||
|
struct commit *head = NULL;
|
||||||
|
struct rev_info revs;
|
||||||
|
char hex[GIT_MAX_HEXSZ + 1];
|
||||||
|
int ret;
|
||||||
|
|
||||||
|
repo_init_revisions(repo, &revs, NULL);
|
||||||
|
strvec_push(&args, "ignored");
|
||||||
|
strvec_push(&args, "--reverse");
|
||||||
|
strvec_push(&args, "--topo-order");
|
||||||
|
strvec_push(&args, "--full-history");
|
||||||
|
|
||||||
|
/* We only want to see commits that are descendants of the old commit. */
|
||||||
|
strvec_pushf(&args, "--ancestry-path=%s",
|
||||||
|
oid_to_hex(&original->object.oid));
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Ancestry path may also show ancestors of the old commit, but we
|
||||||
|
* don't want to see those, either.
|
||||||
|
*/
|
||||||
|
strvec_pushf(&args, "^%s", oid_to_hex(&original->object.oid));
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When we're asked to update HEAD we need to verify that the commit
|
||||||
|
* that we want to rewrite is actually an ancestor of it and, if so,
|
||||||
|
* update it. Otherwise we'll update (or print) all descendant
|
||||||
|
* branches.
|
||||||
|
*/
|
||||||
|
if (action == REF_ACTION_HEAD) {
|
||||||
|
struct commit_list *from_list = NULL;
|
||||||
|
|
||||||
|
head = lookup_commit_reference_by_name("HEAD");
|
||||||
|
if (!head) {
|
||||||
|
ret = error(_("cannot look up HEAD"));
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
commit_list_insert(original, &from_list);
|
||||||
|
ret = repo_is_descendant_of(repo, head, from_list);
|
||||||
|
free_commit_list(from_list);
|
||||||
|
|
||||||
|
if (ret < 0) {
|
||||||
|
ret = error(_("cannot determine descendance"));
|
||||||
|
goto out;
|
||||||
|
} else if (!ret) {
|
||||||
|
ret = error(_("rewritten commit must be an ancestor "
|
||||||
|
"of HEAD when using --ref-action=head"));
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
strvec_push(&args, oid_to_hex(&head->object.oid));
|
||||||
|
} else {
|
||||||
|
strvec_push(&args, "--branches");
|
||||||
|
}
|
||||||
|
|
||||||
|
setup_revisions_from_strvec(&args, &revs, NULL);
|
||||||
|
if (revs.nr)
|
||||||
|
BUG("revisions were set up with invalid argument '%s'", args.v[0]);
|
||||||
|
|
||||||
|
opts.onto = oid_to_hex_r(hex, &rewritten->object.oid);
|
||||||
|
|
||||||
|
ret = replay_revisions(repo, &revs, &opts, &result);
|
||||||
|
if (ret)
|
||||||
|
goto out;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case REF_ACTION_DEFAULT:
|
||||||
|
case REF_ACTION_BRANCHES:
|
||||||
|
transaction = ref_store_transaction_begin(get_main_ref_store(repo), 0, &err);
|
||||||
|
if (!transaction) {
|
||||||
|
ret = error(_("failed to begin ref transaction: %s"), err.buf);
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < result.updates_nr; i++) {
|
||||||
|
ret = ref_transaction_update(transaction,
|
||||||
|
result.updates[i].refname,
|
||||||
|
&result.updates[i].new_oid,
|
||||||
|
&result.updates[i].old_oid,
|
||||||
|
NULL, NULL, 0, reflog_msg, &err);
|
||||||
|
if (ret) {
|
||||||
|
ret = error(_("failed to update ref '%s': %s"),
|
||||||
|
result.updates[i].refname, err.buf);
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* `replay_revisions()` only updates references that are
|
||||||
|
* ancestors of `rewritten`, so we need to manually
|
||||||
|
* handle updating references that point to `original`.
|
||||||
|
*/
|
||||||
|
for (decoration = get_name_decoration(&original->object);
|
||||||
|
decoration;
|
||||||
|
decoration = decoration->next)
|
||||||
|
{
|
||||||
|
if (decoration->type != DECORATION_REF_LOCAL)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
ret = ref_transaction_update(transaction,
|
||||||
|
decoration->name,
|
||||||
|
&rewritten->object.oid,
|
||||||
|
&original->object.oid,
|
||||||
|
NULL, NULL, 0, reflog_msg, &err);
|
||||||
|
if (ret) {
|
||||||
|
ret = error(_("failed to update ref '%s': %s"),
|
||||||
|
decoration->name, err.buf);
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ref_transaction_commit(transaction, &err)) {
|
||||||
|
ret = error(_("failed to commit ref transaction: %s"), err.buf);
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case REF_ACTION_HEAD:
|
||||||
|
ret = refs_update_ref(get_main_ref_store(repo), reflog_msg, "HEAD",
|
||||||
|
&result.final_oid, &head->object.oid, 0,
|
||||||
|
UPDATE_REFS_MSG_ON_ERR);
|
||||||
|
if (ret)
|
||||||
|
goto out;
|
||||||
|
break;
|
||||||
|
case REF_ACTION_PRINT:
|
||||||
|
for (size_t i = 0; i < result.updates_nr; i++)
|
||||||
|
printf("update %s %s %s\n",
|
||||||
|
result.updates[i].refname,
|
||||||
|
oid_to_hex(&result.updates[i].new_oid),
|
||||||
|
oid_to_hex(&result.updates[i].old_oid));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
BUG("unsupported ref action %d", action);
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = 0;
|
||||||
|
|
||||||
|
out:
|
||||||
|
ref_transaction_free(transaction);
|
||||||
|
replay_result_release(&result);
|
||||||
|
release_revisions(&revs);
|
||||||
|
strbuf_release(&err);
|
||||||
|
strvec_clear(&args);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int cmd_history_reword(int argc,
|
||||||
|
const char **argv,
|
||||||
|
const char *prefix,
|
||||||
|
struct repository *repo)
|
||||||
|
{
|
||||||
|
const char * const usage[] = {
|
||||||
|
GIT_HISTORY_REWORD_USAGE,
|
||||||
|
NULL,
|
||||||
|
};
|
||||||
|
enum ref_action action = REF_ACTION_DEFAULT;
|
||||||
|
struct option options[] = {
|
||||||
|
OPT_CALLBACK_F(0, "ref-action", &action, N_("<action>"),
|
||||||
|
N_("control ref update behavior (branches|head|print)"),
|
||||||
|
PARSE_OPT_NONEG, parse_ref_action),
|
||||||
|
OPT_END(),
|
||||||
|
};
|
||||||
|
struct strbuf reflog_msg = STRBUF_INIT;
|
||||||
|
struct commit *original, *rewritten;
|
||||||
|
int ret;
|
||||||
|
|
||||||
|
argc = parse_options(argc, argv, prefix, options, usage, 0);
|
||||||
|
if (argc != 1) {
|
||||||
|
ret = error(_("command expects a single revision"));
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
repo_config(repo, git_default_config, NULL);
|
||||||
|
|
||||||
|
original = lookup_commit_reference_by_name(argv[0]);
|
||||||
|
if (!original) {
|
||||||
|
ret = error(_("commit cannot be found: %s"), argv[0]);
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = commit_tree_with_edited_message(repo, "reworded", original, &rewritten);
|
||||||
|
if (ret < 0) {
|
||||||
|
ret = error(_("failed writing reworded commit"));
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
strbuf_addf(&reflog_msg, "reword: updating %s", argv[0]);
|
||||||
|
|
||||||
|
ret = handle_reference_updates(action, repo, original, rewritten,
|
||||||
|
reflog_msg.buf);
|
||||||
|
if (ret < 0) {
|
||||||
|
ret = error(_("failed replaying descendants"));
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = 0;
|
||||||
|
|
||||||
|
out:
|
||||||
|
strbuf_release(&reflog_msg);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cmd_history(int argc,
|
||||||
|
const char **argv,
|
||||||
|
const char *prefix,
|
||||||
|
struct repository *repo)
|
||||||
|
{
|
||||||
|
const char * const usage[] = {
|
||||||
|
GIT_HISTORY_REWORD_USAGE,
|
||||||
|
NULL,
|
||||||
|
};
|
||||||
|
parse_opt_subcommand_fn *fn = NULL;
|
||||||
|
struct option options[] = {
|
||||||
|
OPT_SUBCOMMAND("reword", &fn, cmd_history_reword),
|
||||||
|
OPT_END(),
|
||||||
|
};
|
||||||
|
|
||||||
|
argc = parse_options(argc, argv, prefix, options, usage, 0);
|
||||||
|
return fn(argc, argv, prefix, repo);
|
||||||
|
}
|
||||||
380
builtin/replay.c
380
builtin/replay.c
@ -2,257 +2,22 @@
|
|||||||
* "git replay" builtin command
|
* "git replay" builtin command
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#define USE_THE_REPOSITORY_VARIABLE
|
|
||||||
#define DISABLE_SIGN_COMPARE_WARNINGS
|
|
||||||
|
|
||||||
#include "git-compat-util.h"
|
#include "git-compat-util.h"
|
||||||
|
|
||||||
#include "builtin.h"
|
#include "builtin.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "environment.h"
|
|
||||||
#include "hex.h"
|
#include "hex.h"
|
||||||
#include "lockfile.h"
|
|
||||||
#include "merge-ort.h"
|
|
||||||
#include "object-name.h"
|
#include "object-name.h"
|
||||||
#include "parse-options.h"
|
#include "parse-options.h"
|
||||||
#include "refs.h"
|
#include "refs.h"
|
||||||
|
#include "replay.h"
|
||||||
#include "revision.h"
|
#include "revision.h"
|
||||||
#include "strmap.h"
|
|
||||||
#include <oidset.h>
|
|
||||||
#include <tree.h>
|
|
||||||
|
|
||||||
enum ref_action_mode {
|
enum ref_action_mode {
|
||||||
REF_ACTION_UPDATE,
|
REF_ACTION_UPDATE,
|
||||||
REF_ACTION_PRINT,
|
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,
|
|
||||||
const char *mode)
|
|
||||||
{
|
|
||||||
struct object *obj;
|
|
||||||
struct object_id oid;
|
|
||||||
|
|
||||||
if (repo_get_oid(repo, name, &oid))
|
|
||||||
die(_("'%s' is not a valid commit-ish for %s"), name, mode);
|
|
||||||
obj = parse_object_or_die(repo, &oid, name);
|
|
||||||
return (struct commit *)repo_peel_to_type(repo, name, 0, obj,
|
|
||||||
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", "gpgsig-sha256", 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;
|
|
||||||
struct strset negative_refs;
|
|
||||||
int positive_refexprs;
|
|
||||||
int negative_refexprs;
|
|
||||||
};
|
|
||||||
|
|
||||||
static void get_ref_information(struct repository *repo,
|
|
||||||
struct rev_cmdline_info *cmd_info,
|
|
||||||
struct ref_info *ref_info)
|
|
||||||
{
|
|
||||||
int i;
|
|
||||||
|
|
||||||
ref_info->onto = NULL;
|
|
||||||
strset_init(&ref_info->positive_refs);
|
|
||||||
strset_init(&ref_info->negative_refs);
|
|
||||||
ref_info->positive_refexprs = 0;
|
|
||||||
ref_info->negative_refexprs = 0;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* When the user specifies e.g.
|
|
||||||
* git replay origin/main..mybranch
|
|
||||||
* git replay ^origin/next mybranch1 mybranch2
|
|
||||||
* we want to be able to determine where to replay the commits. In
|
|
||||||
* these examples, the branches are probably based on an old version
|
|
||||||
* of either origin/main or origin/next, so we want to replay on the
|
|
||||||
* newest version of that branch. In contrast we would want to error
|
|
||||||
* out if they ran
|
|
||||||
* git replay ^origin/master ^origin/next mybranch
|
|
||||||
* git replay mybranch~2..mybranch
|
|
||||||
* the first of those because there's no unique base to choose, and
|
|
||||||
* the second because they'd likely just be replaying commits on top
|
|
||||||
* of the same commit and not making any difference.
|
|
||||||
*/
|
|
||||||
for (i = 0; i < cmd_info->nr; i++) {
|
|
||||||
struct rev_cmdline_entry *e = cmd_info->rev + i;
|
|
||||||
struct object_id oid;
|
|
||||||
const char *refexpr = e->name;
|
|
||||||
char *fullname = NULL;
|
|
||||||
int can_uniquely_dwim = 1;
|
|
||||||
|
|
||||||
if (*refexpr == '^')
|
|
||||||
refexpr++;
|
|
||||||
if (repo_dwim_ref(repo, refexpr, strlen(refexpr), &oid, &fullname, 0) != 1)
|
|
||||||
can_uniquely_dwim = 0;
|
|
||||||
|
|
||||||
if (e->flags & BOTTOM) {
|
|
||||||
if (can_uniquely_dwim)
|
|
||||||
strset_add(&ref_info->negative_refs, fullname);
|
|
||||||
if (!ref_info->negative_refexprs)
|
|
||||||
ref_info->onto = lookup_commit_reference_gently(repo,
|
|
||||||
&e->item->oid, 1);
|
|
||||||
ref_info->negative_refexprs++;
|
|
||||||
} else {
|
|
||||||
if (can_uniquely_dwim)
|
|
||||||
strset_add(&ref_info->positive_refs, fullname);
|
|
||||||
ref_info->positive_refexprs++;
|
|
||||||
}
|
|
||||||
|
|
||||||
free(fullname);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void set_up_replay_mode(struct repository *repo,
|
|
||||||
struct rev_cmdline_info *cmd_info,
|
|
||||||
const char *onto_name,
|
|
||||||
char **advance_name,
|
|
||||||
struct commit **onto,
|
|
||||||
struct strset **update_refs)
|
|
||||||
{
|
|
||||||
struct ref_info rinfo;
|
|
||||||
|
|
||||||
get_ref_information(repo, cmd_info, &rinfo);
|
|
||||||
if (!rinfo.positive_refexprs)
|
|
||||||
die(_("need some commits to replay"));
|
|
||||||
|
|
||||||
die_for_incompatible_opt2(!!onto_name, "--onto",
|
|
||||||
!!*advance_name, "--advance");
|
|
||||||
if (onto_name) {
|
|
||||||
*onto = peel_committish(repo, onto_name, "--onto");
|
|
||||||
if (rinfo.positive_refexprs <
|
|
||||||
strset_get_size(&rinfo.positive_refs))
|
|
||||||
die(_("all positive revisions given must be references"));
|
|
||||||
*update_refs = xcalloc(1, sizeof(**update_refs));
|
|
||||||
**update_refs = rinfo.positive_refs;
|
|
||||||
memset(&rinfo.positive_refs, 0, sizeof(**update_refs));
|
|
||||||
} else {
|
|
||||||
struct object_id oid;
|
|
||||||
char *fullname = NULL;
|
|
||||||
|
|
||||||
if (!*advance_name)
|
|
||||||
BUG("expected either onto_name or *advance_name in this function");
|
|
||||||
|
|
||||||
if (repo_dwim_ref(repo, *advance_name, strlen(*advance_name),
|
|
||||||
&oid, &fullname, 0) == 1) {
|
|
||||||
free(*advance_name);
|
|
||||||
*advance_name = fullname;
|
|
||||||
} else {
|
|
||||||
die(_("argument to --advance must be a reference"));
|
|
||||||
}
|
|
||||||
*onto = peel_committish(repo, *advance_name, "--advance");
|
|
||||||
if (rinfo.positive_refexprs > 1)
|
|
||||||
die(_("cannot advance target with multiple sources because ordering would be ill-defined"));
|
|
||||||
}
|
|
||||||
strset_clear(&rinfo.negative_refs);
|
|
||||||
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)
|
static enum ref_action_mode parse_ref_action_mode(const char *ref_action, const char *source)
|
||||||
{
|
{
|
||||||
if (!ref_action || !strcmp(ref_action, "update"))
|
if (!ref_action || !strcmp(ref_action, "update"))
|
||||||
@ -306,21 +71,11 @@ int cmd_replay(int argc,
|
|||||||
const char *prefix,
|
const char *prefix,
|
||||||
struct repository *repo)
|
struct repository *repo)
|
||||||
{
|
{
|
||||||
const char *advance_name_opt = NULL;
|
struct replay_revisions_options opts = { 0 };
|
||||||
char *advance_name = NULL;
|
struct replay_result result = { 0 };
|
||||||
struct commit *onto = NULL;
|
|
||||||
const char *onto_name = NULL;
|
|
||||||
int contained = 0;
|
|
||||||
const char *ref_action = NULL;
|
const char *ref_action = NULL;
|
||||||
enum ref_action_mode ref_mode;
|
enum ref_action_mode ref_mode;
|
||||||
|
|
||||||
struct rev_info revs;
|
struct rev_info revs;
|
||||||
struct commit *last_commit = NULL;
|
|
||||||
struct commit *commit;
|
|
||||||
struct merge_options merge_opt;
|
|
||||||
struct merge_result result;
|
|
||||||
struct strset *update_refs = NULL;
|
|
||||||
kh_oid_map_t *replayed_commits;
|
|
||||||
struct ref_transaction *transaction = NULL;
|
struct ref_transaction *transaction = NULL;
|
||||||
struct strbuf transaction_err = STRBUF_INIT;
|
struct strbuf transaction_err = STRBUF_INIT;
|
||||||
struct strbuf reflog_msg = STRBUF_INIT;
|
struct strbuf reflog_msg = STRBUF_INIT;
|
||||||
@ -333,13 +88,13 @@ int cmd_replay(int argc,
|
|||||||
NULL
|
NULL
|
||||||
};
|
};
|
||||||
struct option replay_options[] = {
|
struct option replay_options[] = {
|
||||||
OPT_STRING(0, "advance", &advance_name_opt,
|
OPT_STRING(0, "advance", &opts.advance,
|
||||||
N_("branch"),
|
N_("branch"),
|
||||||
N_("make replay advance given branch")),
|
N_("make replay advance given branch")),
|
||||||
OPT_STRING(0, "onto", &onto_name,
|
OPT_STRING(0, "onto", &opts.onto,
|
||||||
N_("revision"),
|
N_("revision"),
|
||||||
N_("replay onto given commit")),
|
N_("replay onto given commit")),
|
||||||
OPT_BOOL(0, "contained", &contained,
|
OPT_BOOL(0, "contained", &opts.contained,
|
||||||
N_("update all branches that point at commits in <revision-range>")),
|
N_("update all branches that point at commits in <revision-range>")),
|
||||||
OPT_STRING(0, "ref-action", &ref_action,
|
OPT_STRING(0, "ref-action", &ref_action,
|
||||||
N_("mode"),
|
N_("mode"),
|
||||||
@ -350,19 +105,17 @@ int cmd_replay(int argc,
|
|||||||
argc = parse_options(argc, argv, prefix, replay_options, replay_usage,
|
argc = parse_options(argc, argv, prefix, replay_options, replay_usage,
|
||||||
PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
|
PARSE_OPT_KEEP_ARGV0 | PARSE_OPT_KEEP_UNKNOWN_OPT);
|
||||||
|
|
||||||
if (!onto_name && !advance_name_opt) {
|
if (!opts.onto && !opts.advance) {
|
||||||
error(_("option --onto or --advance is mandatory"));
|
error(_("option --onto or --advance is mandatory"));
|
||||||
usage_with_options(replay_usage, replay_options);
|
usage_with_options(replay_usage, replay_options);
|
||||||
}
|
}
|
||||||
|
|
||||||
die_for_incompatible_opt2(!!advance_name_opt, "--advance",
|
die_for_incompatible_opt2(!!opts.advance, "--advance",
|
||||||
contained, "--contained");
|
opts.contained, "--contained");
|
||||||
|
|
||||||
/* Parse ref action mode from command line or config */
|
/* Parse ref action mode from command line or config */
|
||||||
ref_mode = get_ref_action_mode(repo, ref_action);
|
ref_mode = get_ref_action_mode(repo, ref_action);
|
||||||
|
|
||||||
advance_name = xstrdup_or_null(advance_name_opt);
|
|
||||||
|
|
||||||
repo_init_revisions(repo, &revs, prefix);
|
repo_init_revisions(repo, &revs, prefix);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -414,18 +167,19 @@ int cmd_replay(int argc,
|
|||||||
revs.simplify_history = 0;
|
revs.simplify_history = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
set_up_replay_mode(repo, &revs.cmdline,
|
ret = replay_revisions(repo, &revs, &opts, &result);
|
||||||
onto_name, &advance_name,
|
if (ret)
|
||||||
&onto, &update_refs);
|
goto cleanup;
|
||||||
|
|
||||||
/* FIXME: Should allow replaying commits with the first as a root commit */
|
|
||||||
|
|
||||||
/* Build reflog message */
|
/* Build reflog message */
|
||||||
if (advance_name_opt)
|
if (opts.advance) {
|
||||||
strbuf_addf(&reflog_msg, "replay --advance %s", advance_name_opt);
|
strbuf_addf(&reflog_msg, "replay --advance %s", opts.advance);
|
||||||
else
|
} else {
|
||||||
strbuf_addf(&reflog_msg, "replay --onto %s",
|
struct object_id oid;
|
||||||
oid_to_hex(&onto->object.oid));
|
if (repo_get_oid_committish(repo, opts.onto, &oid))
|
||||||
|
BUG("--onto commit should have been resolved beforehand already");
|
||||||
|
strbuf_addf(&reflog_msg, "replay --onto %s", oid_to_hex(&oid));
|
||||||
|
}
|
||||||
|
|
||||||
/* Initialize ref transaction if using update mode */
|
/* Initialize ref transaction if using update mode */
|
||||||
if (ref_mode == REF_ACTION_UPDATE) {
|
if (ref_mode == REF_ACTION_UPDATE) {
|
||||||
@ -438,78 +192,19 @@ int cmd_replay(int argc,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (prepare_revision_walk(&revs) < 0) {
|
for (size_t i = 0; i < result.updates_nr; i++) {
|
||||||
ret = error(_("error preparing revisions"));
|
ret = handle_ref_update(ref_mode, transaction, result.updates[i].refname,
|
||||||
goto cleanup;
|
&result.updates[i].new_oid, &result.updates[i].old_oid,
|
||||||
}
|
reflog_msg.buf, &transaction_err);
|
||||||
|
if (ret) {
|
||||||
init_basic_merge_options(&merge_opt, repo);
|
|
||||||
memset(&result, 0, sizeof(result));
|
|
||||||
merge_opt.show_rename_progress = 0;
|
|
||||||
last_commit = onto;
|
|
||||||
replayed_commits = kh_init_oid_map();
|
|
||||||
while ((commit = get_revision(&revs))) {
|
|
||||||
const struct name_decoration *decoration;
|
|
||||||
khint_t pos;
|
|
||||||
int hr;
|
|
||||||
|
|
||||||
if (!commit->parents)
|
|
||||||
die(_("replaying down from root commit is not supported yet!"));
|
|
||||||
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);
|
|
||||||
if (!last_commit)
|
|
||||||
break;
|
|
||||||
|
|
||||||
/* Record commit -> last_commit mapping */
|
|
||||||
pos = kh_put_oid_map(replayed_commits, commit->object.oid, &hr);
|
|
||||||
if (hr == 0)
|
|
||||||
BUG("Duplicate rewritten commit: %s\n",
|
|
||||||
oid_to_hex(&commit->object.oid));
|
|
||||||
kh_value(replayed_commits, pos) = last_commit;
|
|
||||||
|
|
||||||
/* Update any necessary branches */
|
|
||||||
if (advance_name)
|
|
||||||
continue;
|
|
||||||
decoration = get_name_decoration(&commit->object);
|
|
||||||
if (!decoration)
|
|
||||||
continue;
|
|
||||||
while (decoration) {
|
|
||||||
if (decoration->type == DECORATION_REF_LOCAL &&
|
|
||||||
(contained || strset_contains(update_refs,
|
|
||||||
decoration->name))) {
|
|
||||||
if (handle_ref_update(ref_mode, transaction,
|
|
||||||
decoration->name,
|
|
||||||
&last_commit->object.oid,
|
|
||||||
&commit->object.oid,
|
|
||||||
reflog_msg.buf,
|
|
||||||
&transaction_err) < 0) {
|
|
||||||
ret = error(_("failed to update ref '%s': %s"),
|
|
||||||
decoration->name, transaction_err.buf);
|
|
||||||
goto cleanup;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
decoration = decoration->next;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* In --advance mode, advance the target ref */
|
|
||||||
if (result.clean == 1 && advance_name) {
|
|
||||||
if (handle_ref_update(ref_mode, transaction, advance_name,
|
|
||||||
&last_commit->object.oid,
|
|
||||||
&onto->object.oid,
|
|
||||||
reflog_msg.buf,
|
|
||||||
&transaction_err) < 0) {
|
|
||||||
ret = error(_("failed to update ref '%s': %s"),
|
ret = error(_("failed to update ref '%s': %s"),
|
||||||
advance_name, transaction_err.buf);
|
result.updates[i].refname, transaction_err.buf);
|
||||||
goto cleanup;
|
goto cleanup;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Commit the ref transaction if we have one */
|
/* Commit the ref transaction if we have one */
|
||||||
if (transaction && result.clean == 1) {
|
if (transaction) {
|
||||||
if (ref_transaction_commit(transaction, &transaction_err)) {
|
if (ref_transaction_commit(transaction, &transaction_err)) {
|
||||||
ret = error(_("failed to commit ref transaction: %s"),
|
ret = error(_("failed to commit ref transaction: %s"),
|
||||||
transaction_err.buf);
|
transaction_err.buf);
|
||||||
@ -517,24 +212,19 @@ int cmd_replay(int argc,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
merge_finalize(&merge_opt, &result);
|
|
||||||
kh_destroy_oid_map(replayed_commits);
|
|
||||||
if (update_refs) {
|
|
||||||
strset_clear(update_refs);
|
|
||||||
free(update_refs);
|
|
||||||
}
|
|
||||||
ret = result.clean;
|
|
||||||
|
|
||||||
cleanup:
|
cleanup:
|
||||||
if (transaction)
|
if (transaction)
|
||||||
ref_transaction_free(transaction);
|
ref_transaction_free(transaction);
|
||||||
|
replay_result_release(&result);
|
||||||
strbuf_release(&transaction_err);
|
strbuf_release(&transaction_err);
|
||||||
strbuf_release(&reflog_msg);
|
strbuf_release(&reflog_msg);
|
||||||
release_revisions(&revs);
|
release_revisions(&revs);
|
||||||
free(advance_name);
|
|
||||||
|
|
||||||
/* Return */
|
if (ret) {
|
||||||
if (ret < 0)
|
if (result.merge_conflict)
|
||||||
exit(128);
|
return 1;
|
||||||
return ret ? 0 : 1;
|
return 128;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -115,6 +115,7 @@ git-grep mainporcelain info
|
|||||||
git-gui mainporcelain
|
git-gui mainporcelain
|
||||||
git-hash-object plumbingmanipulators
|
git-hash-object plumbingmanipulators
|
||||||
git-help ancillaryinterrogators complete
|
git-help ancillaryinterrogators complete
|
||||||
|
git-history mainporcelain history
|
||||||
git-hook purehelpers
|
git-hook purehelpers
|
||||||
git-http-backend synchingrepositories
|
git-http-backend synchingrepositories
|
||||||
git-http-fetch synchelpers
|
git-http-fetch synchelpers
|
||||||
|
|||||||
1
git.c
1
git.c
@ -586,6 +586,7 @@ static struct cmd_struct commands[] = {
|
|||||||
{ "grep", cmd_grep, RUN_SETUP_GENTLY },
|
{ "grep", cmd_grep, RUN_SETUP_GENTLY },
|
||||||
{ "hash-object", cmd_hash_object },
|
{ "hash-object", cmd_hash_object },
|
||||||
{ "help", cmd_help },
|
{ "help", cmd_help },
|
||||||
|
{ "history", cmd_history, RUN_SETUP },
|
||||||
{ "hook", cmd_hook, RUN_SETUP },
|
{ "hook", cmd_hook, RUN_SETUP },
|
||||||
{ "index-pack", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },
|
{ "index-pack", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },
|
||||||
{ "init", cmd_init_db },
|
{ "init", cmd_init_db },
|
||||||
|
|||||||
@ -472,6 +472,7 @@ libgit_sources = [
|
|||||||
'repack-midx.c',
|
'repack-midx.c',
|
||||||
'repack-promisor.c',
|
'repack-promisor.c',
|
||||||
'replace-object.c',
|
'replace-object.c',
|
||||||
|
'replay.c',
|
||||||
'repo-settings.c',
|
'repo-settings.c',
|
||||||
'repository.c',
|
'repository.c',
|
||||||
'rerere.c',
|
'rerere.c',
|
||||||
@ -610,6 +611,7 @@ builtin_sources = [
|
|||||||
'builtin/grep.c',
|
'builtin/grep.c',
|
||||||
'builtin/hash-object.c',
|
'builtin/hash-object.c',
|
||||||
'builtin/help.c',
|
'builtin/help.c',
|
||||||
|
'builtin/history.c',
|
||||||
'builtin/hook.c',
|
'builtin/hook.c',
|
||||||
'builtin/index-pack.c',
|
'builtin/index-pack.c',
|
||||||
'builtin/init-db.c',
|
'builtin/init-db.c',
|
||||||
|
|||||||
355
replay.c
Normal file
355
replay.c
Normal file
@ -0,0 +1,355 @@
|
|||||||
|
#define USE_THE_REPOSITORY_VARIABLE
|
||||||
|
|
||||||
|
#include "git-compat-util.h"
|
||||||
|
#include "environment.h"
|
||||||
|
#include "hex.h"
|
||||||
|
#include "merge-ort.h"
|
||||||
|
#include "object-name.h"
|
||||||
|
#include "oidset.h"
|
||||||
|
#include "parse-options.h"
|
||||||
|
#include "refs.h"
|
||||||
|
#include "replay.h"
|
||||||
|
#include "revision.h"
|
||||||
|
#include "tree.h"
|
||||||
|
|
||||||
|
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,
|
||||||
|
const char *mode)
|
||||||
|
{
|
||||||
|
struct object *obj;
|
||||||
|
struct object_id oid;
|
||||||
|
|
||||||
|
if (repo_get_oid(repo, name, &oid))
|
||||||
|
die(_("'%s' is not a valid commit-ish for %s"), name, mode);
|
||||||
|
obj = parse_object_or_die(repo, &oid, name);
|
||||||
|
return (struct commit *)repo_peel_to_type(repo, name, 0, obj,
|
||||||
|
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", "gpgsig-sha256", 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(repo, 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;
|
||||||
|
struct strset negative_refs;
|
||||||
|
size_t positive_refexprs;
|
||||||
|
size_t negative_refexprs;
|
||||||
|
};
|
||||||
|
|
||||||
|
static void get_ref_information(struct repository *repo,
|
||||||
|
struct rev_cmdline_info *cmd_info,
|
||||||
|
struct ref_info *ref_info)
|
||||||
|
{
|
||||||
|
ref_info->onto = NULL;
|
||||||
|
strset_init(&ref_info->positive_refs);
|
||||||
|
strset_init(&ref_info->negative_refs);
|
||||||
|
ref_info->positive_refexprs = 0;
|
||||||
|
ref_info->negative_refexprs = 0;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When the user specifies e.g.
|
||||||
|
* git replay origin/main..mybranch
|
||||||
|
* git replay ^origin/next mybranch1 mybranch2
|
||||||
|
* we want to be able to determine where to replay the commits. In
|
||||||
|
* these examples, the branches are probably based on an old version
|
||||||
|
* of either origin/main or origin/next, so we want to replay on the
|
||||||
|
* newest version of that branch. In contrast we would want to error
|
||||||
|
* out if they ran
|
||||||
|
* git replay ^origin/master ^origin/next mybranch
|
||||||
|
* git replay mybranch~2..mybranch
|
||||||
|
* the first of those because there's no unique base to choose, and
|
||||||
|
* the second because they'd likely just be replaying commits on top
|
||||||
|
* of the same commit and not making any difference.
|
||||||
|
*/
|
||||||
|
for (size_t i = 0; i < cmd_info->nr; i++) {
|
||||||
|
struct rev_cmdline_entry *e = cmd_info->rev + i;
|
||||||
|
struct object_id oid;
|
||||||
|
const char *refexpr = e->name;
|
||||||
|
char *fullname = NULL;
|
||||||
|
int can_uniquely_dwim = 1;
|
||||||
|
|
||||||
|
if (*refexpr == '^')
|
||||||
|
refexpr++;
|
||||||
|
if (repo_dwim_ref(repo, refexpr, strlen(refexpr), &oid, &fullname, 0) != 1)
|
||||||
|
can_uniquely_dwim = 0;
|
||||||
|
|
||||||
|
if (e->flags & BOTTOM) {
|
||||||
|
if (can_uniquely_dwim)
|
||||||
|
strset_add(&ref_info->negative_refs, fullname);
|
||||||
|
if (!ref_info->negative_refexprs)
|
||||||
|
ref_info->onto = lookup_commit_reference_gently(repo,
|
||||||
|
&e->item->oid, 1);
|
||||||
|
ref_info->negative_refexprs++;
|
||||||
|
} else {
|
||||||
|
if (can_uniquely_dwim)
|
||||||
|
strset_add(&ref_info->positive_refs, fullname);
|
||||||
|
ref_info->positive_refexprs++;
|
||||||
|
}
|
||||||
|
|
||||||
|
free(fullname);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void set_up_replay_mode(struct repository *repo,
|
||||||
|
struct rev_cmdline_info *cmd_info,
|
||||||
|
const char *onto_name,
|
||||||
|
char **advance_name,
|
||||||
|
struct commit **onto,
|
||||||
|
struct strset **update_refs)
|
||||||
|
{
|
||||||
|
struct ref_info rinfo;
|
||||||
|
|
||||||
|
get_ref_information(repo, cmd_info, &rinfo);
|
||||||
|
if (!rinfo.positive_refexprs)
|
||||||
|
die(_("need some commits to replay"));
|
||||||
|
|
||||||
|
die_for_incompatible_opt2(!!onto_name, "--onto",
|
||||||
|
!!*advance_name, "--advance");
|
||||||
|
if (onto_name) {
|
||||||
|
*onto = peel_committish(repo, onto_name, "--onto");
|
||||||
|
if (rinfo.positive_refexprs <
|
||||||
|
strset_get_size(&rinfo.positive_refs))
|
||||||
|
die(_("all positive revisions given must be references"));
|
||||||
|
*update_refs = xcalloc(1, sizeof(**update_refs));
|
||||||
|
**update_refs = rinfo.positive_refs;
|
||||||
|
memset(&rinfo.positive_refs, 0, sizeof(**update_refs));
|
||||||
|
} else {
|
||||||
|
struct object_id oid;
|
||||||
|
char *fullname = NULL;
|
||||||
|
|
||||||
|
if (!*advance_name)
|
||||||
|
BUG("expected either onto_name or *advance_name in this function");
|
||||||
|
|
||||||
|
if (repo_dwim_ref(repo, *advance_name, strlen(*advance_name),
|
||||||
|
&oid, &fullname, 0) == 1) {
|
||||||
|
free(*advance_name);
|
||||||
|
*advance_name = fullname;
|
||||||
|
} else {
|
||||||
|
die(_("argument to --advance must be a reference"));
|
||||||
|
}
|
||||||
|
*onto = peel_committish(repo, *advance_name, "--advance");
|
||||||
|
if (rinfo.positive_refexprs > 1)
|
||||||
|
die(_("cannot advance target with multiple sources because ordering would be ill-defined"));
|
||||||
|
}
|
||||||
|
strset_clear(&rinfo.negative_refs);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
void replay_result_release(struct replay_result *result)
|
||||||
|
{
|
||||||
|
for (size_t i = 0; i < result->updates_nr; i++)
|
||||||
|
free(result->updates[i].refname);
|
||||||
|
free(result->updates);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void replay_result_queue_update(struct replay_result *result,
|
||||||
|
const char *refname,
|
||||||
|
const struct object_id *old_oid,
|
||||||
|
const struct object_id *new_oid)
|
||||||
|
{
|
||||||
|
ALLOC_GROW(result->updates, result->updates_nr + 1, result->updates_alloc);
|
||||||
|
result->updates[result->updates_nr].refname = xstrdup(refname);
|
||||||
|
result->updates[result->updates_nr].old_oid = *old_oid;
|
||||||
|
result->updates[result->updates_nr].new_oid = *new_oid;
|
||||||
|
result->updates_nr++;
|
||||||
|
}
|
||||||
|
|
||||||
|
int replay_revisions(struct repository *repo, struct rev_info *revs,
|
||||||
|
struct replay_revisions_options *opts,
|
||||||
|
struct replay_result *out)
|
||||||
|
{
|
||||||
|
kh_oid_map_t *replayed_commits = NULL;
|
||||||
|
struct strset *update_refs = NULL;
|
||||||
|
struct commit *last_commit = NULL;
|
||||||
|
struct commit *commit;
|
||||||
|
struct commit *onto = NULL;
|
||||||
|
struct merge_options merge_opt;
|
||||||
|
struct merge_result result = {
|
||||||
|
.clean = 1,
|
||||||
|
};
|
||||||
|
char *advance;
|
||||||
|
int ret;
|
||||||
|
|
||||||
|
advance = xstrdup_or_null(opts->advance);
|
||||||
|
set_up_replay_mode(repo, &revs->cmdline, opts->onto, &advance,
|
||||||
|
&onto, &update_refs);
|
||||||
|
|
||||||
|
/* FIXME: Should allow replaying commits with the first as a root commit */
|
||||||
|
|
||||||
|
if (prepare_revision_walk(revs) < 0) {
|
||||||
|
ret = error(_("error preparing revisions"));
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
init_basic_merge_options(&merge_opt, repo);
|
||||||
|
merge_opt.show_rename_progress = 0;
|
||||||
|
last_commit = onto;
|
||||||
|
replayed_commits = kh_init_oid_map();
|
||||||
|
while ((commit = get_revision(revs))) {
|
||||||
|
const struct name_decoration *decoration;
|
||||||
|
khint_t pos;
|
||||||
|
int hr;
|
||||||
|
|
||||||
|
if (!commit->parents)
|
||||||
|
die(_("replaying down from root commit is not supported yet!"));
|
||||||
|
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);
|
||||||
|
if (!last_commit)
|
||||||
|
break;
|
||||||
|
|
||||||
|
/* Record commit -> last_commit mapping */
|
||||||
|
pos = kh_put_oid_map(replayed_commits, commit->object.oid, &hr);
|
||||||
|
if (hr == 0)
|
||||||
|
BUG("Duplicate rewritten commit: %s\n",
|
||||||
|
oid_to_hex(&commit->object.oid));
|
||||||
|
kh_value(replayed_commits, pos) = last_commit;
|
||||||
|
|
||||||
|
/* Update any necessary branches */
|
||||||
|
if (advance)
|
||||||
|
continue;
|
||||||
|
decoration = get_name_decoration(&commit->object);
|
||||||
|
if (!decoration)
|
||||||
|
continue;
|
||||||
|
while (decoration) {
|
||||||
|
if (decoration->type == DECORATION_REF_LOCAL &&
|
||||||
|
(opts->contained || strset_contains(update_refs,
|
||||||
|
decoration->name))) {
|
||||||
|
replay_result_queue_update(out, decoration->name,
|
||||||
|
&commit->object.oid,
|
||||||
|
&last_commit->object.oid);
|
||||||
|
}
|
||||||
|
decoration = decoration->next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.clean) {
|
||||||
|
out->merge_conflict = true;
|
||||||
|
ret = -1;
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* In --advance mode, advance the target ref */
|
||||||
|
if (advance)
|
||||||
|
replay_result_queue_update(out, advance,
|
||||||
|
&onto->object.oid,
|
||||||
|
&last_commit->object.oid);
|
||||||
|
|
||||||
|
out->final_oid = last_commit->object.oid;
|
||||||
|
|
||||||
|
ret = 0;
|
||||||
|
|
||||||
|
out:
|
||||||
|
if (update_refs) {
|
||||||
|
strset_clear(update_refs);
|
||||||
|
free(update_refs);
|
||||||
|
}
|
||||||
|
kh_destroy_oid_map(replayed_commits);
|
||||||
|
merge_finalize(&merge_opt, &result);
|
||||||
|
free(advance);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
80
replay.h
Normal file
80
replay.h
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
#ifndef REPLAY_H
|
||||||
|
#define REPLAY_H
|
||||||
|
|
||||||
|
#include "hash.h"
|
||||||
|
|
||||||
|
struct repository;
|
||||||
|
struct rev_info;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* A set of options that can be passed to `replay_revisions()`.
|
||||||
|
*/
|
||||||
|
struct replay_revisions_options {
|
||||||
|
/*
|
||||||
|
* Starting point at which to create the new commits; must be a branch
|
||||||
|
* name. The branch will be updated to point to the rewritten commits.
|
||||||
|
* This option is mutually exclusive with `onto`.
|
||||||
|
*/
|
||||||
|
const char *advance;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Starting point at which to create the new commits; must be a
|
||||||
|
* committish. References pointing at decendants of `onto` will be
|
||||||
|
* updated to point to the new commits.
|
||||||
|
*/
|
||||||
|
const char *onto;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Update branches that point at commits in the given revision range.
|
||||||
|
* Requires `onto` to be set.
|
||||||
|
*/
|
||||||
|
int contained;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* This struct is used as an out-parameter by `replay_revisions()`. */
|
||||||
|
struct replay_result {
|
||||||
|
/*
|
||||||
|
* The set of reference updates that are caused by replaying the
|
||||||
|
* commits.
|
||||||
|
*/
|
||||||
|
struct replay_ref_update {
|
||||||
|
char *refname;
|
||||||
|
struct object_id old_oid;
|
||||||
|
struct object_id new_oid;
|
||||||
|
} *updates;
|
||||||
|
size_t updates_nr, updates_alloc;
|
||||||
|
|
||||||
|
/* Set to true in case the replay failed with a merge conflict. */
|
||||||
|
bool merge_conflict;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The final object ID that was rewritten. Note that this field has
|
||||||
|
* somewhat special semantics and may or may not be what you want:
|
||||||
|
*
|
||||||
|
* - If no commits were rewritten it will remain uninitialized.
|
||||||
|
*
|
||||||
|
* - If a thicket of branches is rewritten it is undefined in which
|
||||||
|
* order those branches will be rewritten, and thus the final object
|
||||||
|
* ID may point to a different commit than you'd expect.
|
||||||
|
*
|
||||||
|
* That being said, this field can still be useful when you know that
|
||||||
|
* you only replay a single strand of commits. In that case, the final
|
||||||
|
* commit will point to the tip of the rewritten strand of commits.
|
||||||
|
*/
|
||||||
|
struct object_id final_oid;
|
||||||
|
};
|
||||||
|
|
||||||
|
void replay_result_release(struct replay_result *result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Replay a set of commits onto a new location. Leaves both the working tree,
|
||||||
|
* index and references untouched. Reference updates caused by the replay will
|
||||||
|
* be recorded in the `updates` out pointer.
|
||||||
|
*
|
||||||
|
* Returns 0 on success, a negative error code otherwise.
|
||||||
|
*/
|
||||||
|
int replay_revisions(struct repository *repo, struct rev_info *revs,
|
||||||
|
struct replay_revisions_options *opts,
|
||||||
|
struct replay_result *out);
|
||||||
|
|
||||||
|
#endif
|
||||||
@ -392,6 +392,8 @@ integration_tests = [
|
|||||||
't3437-rebase-fixup-options.sh',
|
't3437-rebase-fixup-options.sh',
|
||||||
't3438-rebase-broken-files.sh',
|
't3438-rebase-broken-files.sh',
|
||||||
't3440-rebase-trailer.sh',
|
't3440-rebase-trailer.sh',
|
||||||
|
't3450-history.sh',
|
||||||
|
't3451-history-reword.sh',
|
||||||
't3500-cherry.sh',
|
't3500-cherry.sh',
|
||||||
't3501-revert-cherry-pick.sh',
|
't3501-revert-cherry-pick.sh',
|
||||||
't3502-cherry-pick-merge.sh',
|
't3502-cherry-pick-merge.sh',
|
||||||
|
|||||||
17
t/t3450-history.sh
Executable file
17
t/t3450-history.sh
Executable file
@ -0,0 +1,17 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
test_description='tests for git-history command'
|
||||||
|
|
||||||
|
. ./test-lib.sh
|
||||||
|
|
||||||
|
test_expect_success 'does nothing without any arguments' '
|
||||||
|
test_must_fail git history 2>err &&
|
||||||
|
test_grep "need a subcommand" err
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_success 'raises an error with unknown argument' '
|
||||||
|
test_must_fail git history garbage 2>err &&
|
||||||
|
test_grep "unknown subcommand: .garbage." err
|
||||||
|
'
|
||||||
|
|
||||||
|
test_done
|
||||||
344
t/t3451-history-reword.sh
Executable file
344
t/t3451-history-reword.sh
Executable file
@ -0,0 +1,344 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
test_description='tests for git-history reword subcommand'
|
||||||
|
|
||||||
|
. ./test-lib.sh
|
||||||
|
. "$TEST_DIRECTORY/lib-log-graph.sh"
|
||||||
|
|
||||||
|
reword_with_message () {
|
||||||
|
cat >message &&
|
||||||
|
write_script fake-editor.sh <<-\EOF &&
|
||||||
|
cp message "$1"
|
||||||
|
EOF
|
||||||
|
test_set_editor "$(pwd)"/fake-editor.sh &&
|
||||||
|
git history reword "$@" &&
|
||||||
|
rm fake-editor.sh message
|
||||||
|
}
|
||||||
|
|
||||||
|
expect_graph () {
|
||||||
|
cat >expect &&
|
||||||
|
lib_test_cmp_graph --graph --format=%s "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
expect_log () {
|
||||||
|
git log --format="%s" "$@" >actual &&
|
||||||
|
cat >expect &&
|
||||||
|
test_cmp expect actual
|
||||||
|
}
|
||||||
|
|
||||||
|
test_expect_success 'can reword tip of a branch' '
|
||||||
|
test_when_finished "rm -rf repo" &&
|
||||||
|
git init repo &&
|
||||||
|
(
|
||||||
|
cd repo &&
|
||||||
|
test_commit first &&
|
||||||
|
test_commit second &&
|
||||||
|
test_commit third &&
|
||||||
|
|
||||||
|
git symbolic-ref HEAD >expect &&
|
||||||
|
reword_with_message HEAD <<-EOF &&
|
||||||
|
third reworded
|
||||||
|
EOF
|
||||||
|
git symbolic-ref HEAD >actual &&
|
||||||
|
test_cmp expect actual &&
|
||||||
|
|
||||||
|
expect_log <<-\EOF &&
|
||||||
|
third reworded
|
||||||
|
second
|
||||||
|
first
|
||||||
|
EOF
|
||||||
|
|
||||||
|
git reflog >reflog &&
|
||||||
|
test_grep "reword: updating HEAD" reflog
|
||||||
|
)
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_success 'can reword commit in the middle' '
|
||||||
|
test_when_finished "rm -rf repo" &&
|
||||||
|
git init repo &&
|
||||||
|
(
|
||||||
|
cd repo &&
|
||||||
|
test_commit first &&
|
||||||
|
test_commit second &&
|
||||||
|
test_commit third &&
|
||||||
|
|
||||||
|
git symbolic-ref HEAD >expect &&
|
||||||
|
reword_with_message HEAD~ <<-EOF &&
|
||||||
|
second reworded
|
||||||
|
EOF
|
||||||
|
git symbolic-ref HEAD >actual &&
|
||||||
|
test_cmp expect actual &&
|
||||||
|
|
||||||
|
expect_log <<-\EOF
|
||||||
|
third
|
||||||
|
second reworded
|
||||||
|
first
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_success 'can reword root commit' '
|
||||||
|
test_when_finished "rm -rf repo" &&
|
||||||
|
git init repo &&
|
||||||
|
(
|
||||||
|
cd repo &&
|
||||||
|
test_commit first &&
|
||||||
|
test_commit second &&
|
||||||
|
test_commit third &&
|
||||||
|
reword_with_message HEAD~2 <<-EOF &&
|
||||||
|
first reworded
|
||||||
|
EOF
|
||||||
|
|
||||||
|
expect_log <<-\EOF
|
||||||
|
third
|
||||||
|
second
|
||||||
|
first reworded
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_success 'can reword in a bare repo' '
|
||||||
|
test_when_finished "rm -rf repo repo.git" &&
|
||||||
|
git init repo &&
|
||||||
|
test_commit -C repo first &&
|
||||||
|
git clone --bare repo repo.git &&
|
||||||
|
(
|
||||||
|
cd repo.git &&
|
||||||
|
reword_with_message HEAD <<-EOF &&
|
||||||
|
reworded
|
||||||
|
EOF
|
||||||
|
|
||||||
|
expect_log <<-\EOF
|
||||||
|
reworded
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_success 'can reword a commit on a different branch' '
|
||||||
|
test_when_finished "rm -rf repo" &&
|
||||||
|
git init repo &&
|
||||||
|
(
|
||||||
|
cd repo &&
|
||||||
|
test_commit base &&
|
||||||
|
git branch theirs &&
|
||||||
|
test_commit ours &&
|
||||||
|
git switch theirs &&
|
||||||
|
test_commit theirs &&
|
||||||
|
|
||||||
|
git rev-parse ours >ours-before &&
|
||||||
|
reword_with_message theirs <<-EOF &&
|
||||||
|
Reworded theirs
|
||||||
|
EOF
|
||||||
|
git rev-parse ours >ours-after &&
|
||||||
|
test_cmp ours-before ours-after &&
|
||||||
|
|
||||||
|
expect_graph --branches <<-\EOF
|
||||||
|
* Reworded theirs
|
||||||
|
| * ours
|
||||||
|
|/
|
||||||
|
* base
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_success 'can reword a merge commit' '
|
||||||
|
test_when_finished "rm -rf repo" &&
|
||||||
|
git init repo &&
|
||||||
|
(
|
||||||
|
cd repo &&
|
||||||
|
test_commit base &&
|
||||||
|
git branch branch &&
|
||||||
|
test_commit ours &&
|
||||||
|
git switch branch &&
|
||||||
|
test_commit theirs &&
|
||||||
|
git switch - &&
|
||||||
|
git merge theirs &&
|
||||||
|
|
||||||
|
# It is not possible to replay merge commits embedded in the
|
||||||
|
# history (yet).
|
||||||
|
test_must_fail git history reword HEAD~ 2>err &&
|
||||||
|
test_grep "replaying merge commits is not supported yet" err &&
|
||||||
|
|
||||||
|
# But it is possible to reword a merge commit directly.
|
||||||
|
reword_with_message HEAD <<-EOF &&
|
||||||
|
Reworded merge commit
|
||||||
|
EOF
|
||||||
|
expect_graph <<-\EOF
|
||||||
|
* Reworded merge commit
|
||||||
|
|\
|
||||||
|
| * theirs
|
||||||
|
* | ours
|
||||||
|
|/
|
||||||
|
* base
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_success '--ref-action=print prints ref updates without modifying repo' '
|
||||||
|
test_when_finished "rm -rf repo" &&
|
||||||
|
git init repo --initial-branch=main &&
|
||||||
|
(
|
||||||
|
cd repo &&
|
||||||
|
test_commit base &&
|
||||||
|
git branch branch &&
|
||||||
|
test_commit ours &&
|
||||||
|
git switch branch &&
|
||||||
|
test_commit theirs &&
|
||||||
|
|
||||||
|
git refs list >refs-expect &&
|
||||||
|
reword_with_message --ref-action=print base >updates <<-\EOF &&
|
||||||
|
reworded commit
|
||||||
|
EOF
|
||||||
|
git refs list >refs-actual &&
|
||||||
|
test_cmp refs-expect refs-actual &&
|
||||||
|
|
||||||
|
test_grep "update refs/heads/branch" updates &&
|
||||||
|
test_grep "update refs/heads/main" updates &&
|
||||||
|
git update-ref --stdin <updates &&
|
||||||
|
expect_log --branches <<-\EOF
|
||||||
|
theirs
|
||||||
|
ours
|
||||||
|
reworded commit
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_success '--ref-action=head updates only HEAD' '
|
||||||
|
test_when_finished "rm -rf repo" &&
|
||||||
|
git init repo --initial-branch=main &&
|
||||||
|
(
|
||||||
|
cd repo &&
|
||||||
|
test_commit base &&
|
||||||
|
git branch branch &&
|
||||||
|
test_commit theirs &&
|
||||||
|
git switch branch &&
|
||||||
|
test_commit ours &&
|
||||||
|
|
||||||
|
# When told to update HEAD, only, the command will refuse to
|
||||||
|
# rewrite commits that are not an ancestor of HEAD.
|
||||||
|
test_must_fail git history reword --ref-action=head theirs 2>err &&
|
||||||
|
test_grep "rewritten commit must be an ancestor of HEAD" err &&
|
||||||
|
|
||||||
|
reword_with_message --ref-action=head base >updates <<-\EOF &&
|
||||||
|
reworded base
|
||||||
|
EOF
|
||||||
|
expect_log HEAD <<-\EOF &&
|
||||||
|
ours
|
||||||
|
reworded base
|
||||||
|
EOF
|
||||||
|
expect_log main <<-\EOF
|
||||||
|
theirs
|
||||||
|
base
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_success 'editor shows proper status' '
|
||||||
|
test_when_finished "rm -rf repo" &&
|
||||||
|
git init repo &&
|
||||||
|
(
|
||||||
|
cd repo &&
|
||||||
|
test_commit first &&
|
||||||
|
|
||||||
|
write_script fake-editor.sh <<-\EOF &&
|
||||||
|
cp "$1" . &&
|
||||||
|
printf "\namend a comment\n" >>"$1"
|
||||||
|
EOF
|
||||||
|
test_set_editor "$(pwd)"/fake-editor.sh &&
|
||||||
|
git history reword HEAD &&
|
||||||
|
|
||||||
|
cat >expect <<-EOF &&
|
||||||
|
first
|
||||||
|
|
||||||
|
# Please enter the commit message for the reworded changes. Lines starting
|
||||||
|
# with ${SQ}#${SQ} will be ignored, and an empty message aborts the commit.
|
||||||
|
# Changes to be committed:
|
||||||
|
# new file: first.t
|
||||||
|
#
|
||||||
|
EOF
|
||||||
|
test_cmp expect COMMIT_EDITMSG &&
|
||||||
|
|
||||||
|
test_commit_message HEAD <<-\EOF
|
||||||
|
first
|
||||||
|
|
||||||
|
amend a comment
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
'
|
||||||
|
|
||||||
|
# For now, git-history(1) does not yet execute any hooks. This is subject to
|
||||||
|
# change in the future, and if it does this test here is expected to start
|
||||||
|
# failing. In other words, this test is not an endorsement of the current
|
||||||
|
# status quo.
|
||||||
|
test_expect_success 'hooks are not executed for rewritten commits' '
|
||||||
|
test_when_finished "rm -rf repo" &&
|
||||||
|
git init repo &&
|
||||||
|
(
|
||||||
|
cd repo &&
|
||||||
|
test_commit first &&
|
||||||
|
test_commit second &&
|
||||||
|
test_commit third &&
|
||||||
|
|
||||||
|
ORIG_PATH="$(pwd)" &&
|
||||||
|
export ORIG_PATH &&
|
||||||
|
for hook in prepare-commit-msg pre-commit post-commit post-rewrite commit-msg
|
||||||
|
do
|
||||||
|
write_script .git/hooks/$hook <<-\EOF || exit 1
|
||||||
|
touch "$ORIG_PATH/hooks.log
|
||||||
|
EOF
|
||||||
|
done &&
|
||||||
|
|
||||||
|
reword_with_message HEAD~ <<-EOF &&
|
||||||
|
second reworded
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat >expect <<-EOF &&
|
||||||
|
third
|
||||||
|
second reworded
|
||||||
|
first
|
||||||
|
EOF
|
||||||
|
git log --format=%s >actual &&
|
||||||
|
test_cmp expect actual &&
|
||||||
|
|
||||||
|
test_path_is_missing hooks.log
|
||||||
|
)
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_success 'aborts with empty commit message' '
|
||||||
|
test_when_finished "rm -rf repo" &&
|
||||||
|
git init repo &&
|
||||||
|
(
|
||||||
|
cd repo &&
|
||||||
|
test_commit first &&
|
||||||
|
|
||||||
|
! reword_with_message HEAD 2>err </dev/null &&
|
||||||
|
test_grep "Aborting commit due to empty commit message." err
|
||||||
|
)
|
||||||
|
'
|
||||||
|
|
||||||
|
test_expect_success 'retains changes in the worktree and index' '
|
||||||
|
test_when_finished "rm -rf repo" &&
|
||||||
|
git init repo &&
|
||||||
|
(
|
||||||
|
cd repo &&
|
||||||
|
touch a b &&
|
||||||
|
git add . &&
|
||||||
|
git commit -m "initial commit" &&
|
||||||
|
echo foo >a &&
|
||||||
|
echo bar >b &&
|
||||||
|
git add b &&
|
||||||
|
reword_with_message HEAD <<-EOF &&
|
||||||
|
message
|
||||||
|
EOF
|
||||||
|
cat >expect <<-\EOF &&
|
||||||
|
M a
|
||||||
|
M b
|
||||||
|
?? actual
|
||||||
|
?? expect
|
||||||
|
EOF
|
||||||
|
git status --porcelain >actual &&
|
||||||
|
test_cmp expect actual
|
||||||
|
)
|
||||||
|
'
|
||||||
|
|
||||||
|
test_done
|
||||||
24
wt-status.c
24
wt-status.c
@ -612,6 +612,30 @@ static void wt_status_collect_updated_cb(struct diff_queue_struct *q,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void wt_status_collect_changes_trees(struct wt_status *s,
|
||||||
|
const struct object_id *old_treeish,
|
||||||
|
const struct object_id *new_treeish)
|
||||||
|
{
|
||||||
|
struct diff_options opts = { 0 };
|
||||||
|
|
||||||
|
repo_diff_setup(s->repo, &opts);
|
||||||
|
opts.output_format = DIFF_FORMAT_CALLBACK;
|
||||||
|
opts.format_callback = wt_status_collect_updated_cb;
|
||||||
|
opts.format_callback_data = s;
|
||||||
|
opts.detect_rename = s->detect_rename >= 0 ? s->detect_rename : opts.detect_rename;
|
||||||
|
opts.rename_limit = s->rename_limit >= 0 ? s->rename_limit : opts.rename_limit;
|
||||||
|
opts.rename_score = s->rename_score >= 0 ? s->rename_score : opts.rename_score;
|
||||||
|
opts.flags.recursive = 1;
|
||||||
|
diff_setup_done(&opts);
|
||||||
|
|
||||||
|
diff_tree_oid(old_treeish, new_treeish, "", &opts);
|
||||||
|
diffcore_std(&opts);
|
||||||
|
diff_flush(&opts);
|
||||||
|
wt_status_get_state(s->repo, &s->state, 0);
|
||||||
|
|
||||||
|
diff_free(&opts);
|
||||||
|
}
|
||||||
|
|
||||||
static void wt_status_collect_changes_worktree(struct wt_status *s)
|
static void wt_status_collect_changes_worktree(struct wt_status *s)
|
||||||
{
|
{
|
||||||
struct rev_info rev;
|
struct rev_info rev;
|
||||||
|
|||||||
@ -153,6 +153,15 @@ void wt_status_add_cut_line(struct wt_status *s);
|
|||||||
void wt_status_prepare(struct repository *r, struct wt_status *s);
|
void wt_status_prepare(struct repository *r, struct wt_status *s);
|
||||||
void wt_status_print(struct wt_status *s);
|
void wt_status_print(struct wt_status *s);
|
||||||
void wt_status_collect(struct wt_status *s);
|
void wt_status_collect(struct wt_status *s);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Collect all changes between the two trees. Changes will be displayed as if
|
||||||
|
* they were staged into the index.
|
||||||
|
*/
|
||||||
|
void wt_status_collect_changes_trees(struct wt_status *s,
|
||||||
|
const struct object_id *old_treeish,
|
||||||
|
const struct object_id *new_treeish);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Frees the buffers allocated by wt_status_collect.
|
* Frees the buffers allocated by wt_status_collect.
|
||||||
*/
|
*/
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user