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. * 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
708bf14c88
1
.gitignore
vendored
1
.gitignore
vendored
@ -79,6 +79,7 @@
|
||||
/git-grep
|
||||
/git-hash-object
|
||||
/git-help
|
||||
/git-history
|
||||
/git-hook
|
||||
/git-http-backend
|
||||
/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-hash-object.adoc' : 1,
|
||||
'git-help.adoc' : 1,
|
||||
'git-history.adoc' : 1,
|
||||
'git-hook.adoc' : 1,
|
||||
'git-http-backend.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-promisor.o
|
||||
LIB_OBJS += replace-object.o
|
||||
LIB_OBJS += replay.o
|
||||
LIB_OBJS += repo-settings.o
|
||||
LIB_OBJS += repository.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/hash-object.o
|
||||
BUILTIN_OBJS += builtin/help.o
|
||||
BUILTIN_OBJS += builtin/history.o
|
||||
BUILTIN_OBJS += builtin/hook.o
|
||||
BUILTIN_OBJS += builtin/index-pack.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_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_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_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);
|
||||
|
||||
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
|
||||
*/
|
||||
|
||||
#define USE_THE_REPOSITORY_VARIABLE
|
||||
#define DISABLE_SIGN_COMPARE_WARNINGS
|
||||
|
||||
#include "git-compat-util.h"
|
||||
|
||||
#include "builtin.h"
|
||||
#include "config.h"
|
||||
#include "environment.h"
|
||||
#include "hex.h"
|
||||
#include "lockfile.h"
|
||||
#include "merge-ort.h"
|
||||
#include "object-name.h"
|
||||
#include "parse-options.h"
|
||||
#include "refs.h"
|
||||
#include "replay.h"
|
||||
#include "revision.h"
|
||||
#include "strmap.h"
|
||||
#include <oidset.h>
|
||||
#include <tree.h>
|
||||
|
||||
enum ref_action_mode {
|
||||
REF_ACTION_UPDATE,
|
||||
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)
|
||||
{
|
||||
if (!ref_action || !strcmp(ref_action, "update"))
|
||||
@ -306,21 +71,11 @@ int cmd_replay(int argc,
|
||||
const char *prefix,
|
||||
struct repository *repo)
|
||||
{
|
||||
const char *advance_name_opt = NULL;
|
||||
char *advance_name = NULL;
|
||||
struct commit *onto = NULL;
|
||||
const char *onto_name = NULL;
|
||||
int contained = 0;
|
||||
struct replay_revisions_options opts = { 0 };
|
||||
struct replay_result result = { 0 };
|
||||
const char *ref_action = NULL;
|
||||
enum ref_action_mode ref_mode;
|
||||
|
||||
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 strbuf transaction_err = STRBUF_INIT;
|
||||
struct strbuf reflog_msg = STRBUF_INIT;
|
||||
@ -333,13 +88,13 @@ int cmd_replay(int argc,
|
||||
NULL
|
||||
};
|
||||
struct option replay_options[] = {
|
||||
OPT_STRING(0, "advance", &advance_name_opt,
|
||||
OPT_STRING(0, "advance", &opts.advance,
|
||||
N_("branch"),
|
||||
N_("make replay advance given branch")),
|
||||
OPT_STRING(0, "onto", &onto_name,
|
||||
OPT_STRING(0, "onto", &opts.onto,
|
||||
N_("revision"),
|
||||
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>")),
|
||||
OPT_STRING(0, "ref-action", &ref_action,
|
||||
N_("mode"),
|
||||
@ -350,19 +105,17 @@ int cmd_replay(int argc,
|
||||
argc = parse_options(argc, argv, prefix, replay_options, replay_usage,
|
||||
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"));
|
||||
usage_with_options(replay_usage, replay_options);
|
||||
}
|
||||
|
||||
die_for_incompatible_opt2(!!advance_name_opt, "--advance",
|
||||
contained, "--contained");
|
||||
die_for_incompatible_opt2(!!opts.advance, "--advance",
|
||||
opts.contained, "--contained");
|
||||
|
||||
/* Parse ref action mode from command line or config */
|
||||
ref_mode = get_ref_action_mode(repo, ref_action);
|
||||
|
||||
advance_name = xstrdup_or_null(advance_name_opt);
|
||||
|
||||
repo_init_revisions(repo, &revs, prefix);
|
||||
|
||||
/*
|
||||
@ -414,18 +167,19 @@ int cmd_replay(int argc,
|
||||
revs.simplify_history = 0;
|
||||
}
|
||||
|
||||
set_up_replay_mode(repo, &revs.cmdline,
|
||||
onto_name, &advance_name,
|
||||
&onto, &update_refs);
|
||||
|
||||
/* FIXME: Should allow replaying commits with the first as a root commit */
|
||||
ret = replay_revisions(repo, &revs, &opts, &result);
|
||||
if (ret)
|
||||
goto cleanup;
|
||||
|
||||
/* Build reflog message */
|
||||
if (advance_name_opt)
|
||||
strbuf_addf(&reflog_msg, "replay --advance %s", advance_name_opt);
|
||||
else
|
||||
strbuf_addf(&reflog_msg, "replay --onto %s",
|
||||
oid_to_hex(&onto->object.oid));
|
||||
if (opts.advance) {
|
||||
strbuf_addf(&reflog_msg, "replay --advance %s", opts.advance);
|
||||
} else {
|
||||
struct object_id 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 */
|
||||
if (ref_mode == REF_ACTION_UPDATE) {
|
||||
@ -438,78 +192,19 @@ int cmd_replay(int argc,
|
||||
}
|
||||
}
|
||||
|
||||
if (prepare_revision_walk(&revs) < 0) {
|
||||
ret = error(_("error preparing revisions"));
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
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) {
|
||||
for (size_t i = 0; i < result.updates_nr; i++) {
|
||||
ret = handle_ref_update(ref_mode, transaction, result.updates[i].refname,
|
||||
&result.updates[i].new_oid, &result.updates[i].old_oid,
|
||||
reflog_msg.buf, &transaction_err);
|
||||
if (ret) {
|
||||
ret = error(_("failed to update ref '%s': %s"),
|
||||
advance_name, transaction_err.buf);
|
||||
result.updates[i].refname, transaction_err.buf);
|
||||
goto cleanup;
|
||||
}
|
||||
}
|
||||
|
||||
/* Commit the ref transaction if we have one */
|
||||
if (transaction && result.clean == 1) {
|
||||
if (transaction) {
|
||||
if (ref_transaction_commit(transaction, &transaction_err)) {
|
||||
ret = error(_("failed to commit ref transaction: %s"),
|
||||
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:
|
||||
if (transaction)
|
||||
ref_transaction_free(transaction);
|
||||
replay_result_release(&result);
|
||||
strbuf_release(&transaction_err);
|
||||
strbuf_release(&reflog_msg);
|
||||
release_revisions(&revs);
|
||||
free(advance_name);
|
||||
|
||||
/* Return */
|
||||
if (ret < 0)
|
||||
exit(128);
|
||||
return ret ? 0 : 1;
|
||||
if (ret) {
|
||||
if (result.merge_conflict)
|
||||
return 1;
|
||||
return 128;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@ -115,6 +115,7 @@ git-grep mainporcelain info
|
||||
git-gui mainporcelain
|
||||
git-hash-object plumbingmanipulators
|
||||
git-help ancillaryinterrogators complete
|
||||
git-history mainporcelain history
|
||||
git-hook purehelpers
|
||||
git-http-backend synchingrepositories
|
||||
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 },
|
||||
{ "hash-object", cmd_hash_object },
|
||||
{ "help", cmd_help },
|
||||
{ "history", cmd_history, RUN_SETUP },
|
||||
{ "hook", cmd_hook, RUN_SETUP },
|
||||
{ "index-pack", cmd_index_pack, RUN_SETUP_GENTLY | NO_PARSEOPT },
|
||||
{ "init", cmd_init_db },
|
||||
|
||||
@ -472,6 +472,7 @@ libgit_sources = [
|
||||
'repack-midx.c',
|
||||
'repack-promisor.c',
|
||||
'replace-object.c',
|
||||
'replay.c',
|
||||
'repo-settings.c',
|
||||
'repository.c',
|
||||
'rerere.c',
|
||||
@ -610,6 +611,7 @@ builtin_sources = [
|
||||
'builtin/grep.c',
|
||||
'builtin/hash-object.c',
|
||||
'builtin/help.c',
|
||||
'builtin/history.c',
|
||||
'builtin/hook.c',
|
||||
'builtin/index-pack.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',
|
||||
't3438-rebase-broken-files.sh',
|
||||
't3440-rebase-trailer.sh',
|
||||
't3450-history.sh',
|
||||
't3451-history-reword.sh',
|
||||
't3500-cherry.sh',
|
||||
't3501-revert-cherry-pick.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)
|
||||
{
|
||||
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_print(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.
|
||||
*/
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user