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:
Junio C Hamano 2026-01-08 16:41:18 +09:00
commit e62a1bc8b3
17 changed files with 1335 additions and 345 deletions

1
.gitignore vendored
View File

@ -79,6 +79,7 @@
/git-grep
/git-hash-object
/git-help
/git-history
/git-hook
/git-http-backend
/git-http-fetch

View 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

View File

@ -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,

View File

@ -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

View File

@ -172,6 +172,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
View 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_ref_updates updates = {
.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, &updates);
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 < updates.nr; i++) {
ret = ref_transaction_update(transaction,
updates.items[i].refname,
&updates.items[i].new_oid,
&updates.items[i].old_oid,
NULL, NULL, 0, reflog_msg, &err);
if (ret) {
ret = error(_("failed to update ref '%s': %s"),
updates.items[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",
&updates.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 < updates.nr; i++)
printf("update %s %s %s\n",
updates.items[i].refname,
oid_to_hex(&updates.items[i].new_oid),
oid_to_hex(&updates.items[i].old_oid));
break;
default:
BUG("unsupported ref action %d", action);
}
ret = 0;
out:
replay_ref_updates_release(&updates);
ref_transaction_free(transaction);
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);
}

View File

@ -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_ref_updates updates = { 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, &updates);
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 < updates.nr; i++) {
ret = handle_ref_update(ref_mode, transaction, updates.items[i].refname,
&updates.items[i].new_oid, &updates.items[i].old_oid,
reflog_msg.buf, &transaction_err);
if (ret) {
ret = error(_("failed to update ref '%s': %s"),
advance_name, transaction_err.buf);
updates.items[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,13 @@ 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_ref_updates_release(&updates);
strbuf_release(&transaction_err);
strbuf_release(&reflog_msg);
release_revisions(&revs);
free(advance_name);
/* Return */
if (ret < 0)
exit(128);
return ret ? 0 : 1;
return ret ? 1 : 0;
}

View File

@ -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
View File

@ -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 },

View File

@ -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',

347
replay.c Normal file
View File

@ -0,0 +1,347 @@
#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_ref_updates_release(struct replay_ref_updates *updates)
{
for (size_t i = 0; i < updates->nr; i++)
free(updates->items[i].refname);
free(updates->items);
}
int replay_revisions(struct repository *repo, struct rev_info *revs,
struct replay_revisions_options *opts,
struct replay_ref_updates *updates)
{
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))) {
ALLOC_GROW(updates->items, updates->nr + 1, updates->alloc);
updates->items[updates->nr].refname = xstrdup(decoration->name);
updates->items[updates->nr].old_oid = commit->object.oid;
updates->items[updates->nr].new_oid = last_commit->object.oid;
updates->nr++;
}
decoration = decoration->next;
}
}
if (!result.clean) {
ret = -1;
goto out;
}
/* In --advance mode, advance the target ref */
if (advance) {
ALLOC_GROW(updates->items, updates->nr + 1, updates->alloc);
updates->items[updates->nr].refname = xstrdup(advance);
updates->items[updates->nr].old_oid = onto->object.oid;
updates->items[updates->nr].new_oid = last_commit->object.oid;
updates->nr++;
}
updates->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;
}

77
replay.h Normal file
View File

@ -0,0 +1,77 @@
#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_ref_updates {
/*
* 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;
} *items;
size_t nr, alloc;
/*
* 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_ref_updates_release(struct replay_ref_updates *updates);
/*
* 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_ref_updates *updates);
#endif

View File

@ -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
View 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
View 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

View File

@ -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;

View File

@ -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.
*/