mirror of
https://github.com/git/git.git
synced 2026-05-25 11:25:06 +02:00
Merge branch 'ps/history-fixup'
"git history" learned "fixup" command. * ps/history-fixup: builtin/history: introduce "fixup" subcommand builtin/history: generalize function to commit trees replay: allow callers to control what happens with empty commits
This commit is contained in:
@@ -8,6 +8,7 @@ git-history - EXPERIMENTAL: Rewrite history
|
||||
SYNOPSIS
|
||||
--------
|
||||
[synopsis]
|
||||
git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]
|
||||
git history reword <commit> [--dry-run] [--update-refs=(branches|head)]
|
||||
git history split <commit> [--dry-run] [--update-refs=(branches|head)] [--] [<pathspec>...]
|
||||
|
||||
@@ -22,8 +23,9 @@ 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.
|
||||
* Most subcommands of linkgit:git-history[1] can work in a bare repository as
|
||||
they do not need to touch either the index or the worktree. The `fixup`
|
||||
subcommand is an exception to this, as it reads staged changes from the index.
|
||||
* 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
|
||||
@@ -48,11 +50,28 @@ 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.
|
||||
|
||||
When using `fixup` with `--empty=drop`, dropping the root commit is not yet
|
||||
supported.
|
||||
|
||||
COMMANDS
|
||||
--------
|
||||
|
||||
The following commands are available to rewrite history in different ways:
|
||||
|
||||
`fixup <commit>`::
|
||||
Apply the currently staged changes to the specified commit. This is
|
||||
similar in nature to `git commit --fixup=<commit>` followed by `git
|
||||
rebase --autosquash <commit>~`. Changes are applied to the target
|
||||
commit by performing a three-way merge between the HEAD commit, the
|
||||
target commit and the tree generated from staged changes.
|
||||
+
|
||||
The commit message and authorship of the target commit are preserved by
|
||||
default, unless you specify `--reedit-message`.
|
||||
+
|
||||
If applying the staged changes would result in a conflict, the command
|
||||
aborts with an error. All branches that are descendants of the original
|
||||
commit are updated to point to the rewritten history.
|
||||
|
||||
`reword <commit>`::
|
||||
Rewrite the commit message of the specified commit. All the other
|
||||
details of this commit remain unchanged. This command will spawn an
|
||||
@@ -87,6 +106,31 @@ OPTIONS
|
||||
objects will be written into the repository, so applying these printed
|
||||
ref updates is generally safe.
|
||||
|
||||
`--reedit-message`::
|
||||
Open an editor to modify the target commit's message.
|
||||
|
||||
`--empty=(drop|keep|abort)`::
|
||||
Control what happens when a commit becomes empty as a result of the
|
||||
fixup. This can happen in two situations:
|
||||
+
|
||||
--
|
||||
* The fixup target itself becomes empty because the staged changes exactly
|
||||
cancel out all changes introduced by that commit.
|
||||
|
||||
* A descendant commit becomes empty during replay because it introduced the
|
||||
same change that was just fixed up into an ancestor.
|
||||
--
|
||||
+
|
||||
With `drop` (the default), empty commits are removed from the rewritten
|
||||
history. Descendants of a dropped target commit are replayed directly onto
|
||||
the target's parent. Note that dropping the root commit is not supported;
|
||||
see LIMITATIONS.
|
||||
+
|
||||
With `keep`, empty commits are retained in the rewritten history as-is.
|
||||
+
|
||||
With `abort`, the command stops with an error if any commit would become
|
||||
empty.
|
||||
|
||||
`--update-refs=(branches|head)`::
|
||||
Control which references will be updated by the command, if any. With
|
||||
`branches`, all local branches that point to commits which are
|
||||
@@ -96,6 +140,36 @@ OPTIONS
|
||||
EXAMPLES
|
||||
--------
|
||||
|
||||
Fixup a commit
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
----------
|
||||
$ git log --oneline --stat
|
||||
abc1234 (HEAD -> main) third
|
||||
third.txt | 1 +
|
||||
def5678 second
|
||||
second.txt | 1 +
|
||||
ghi9012 first
|
||||
first.txt | 1 +
|
||||
|
||||
$ echo "change" >>unrelated.txt
|
||||
$ git add unrelated.txt
|
||||
$ git history fixup ghi9012
|
||||
|
||||
$ git log --oneline --stat
|
||||
jkl3456 (HEAD -> main) third
|
||||
third.txt | 1 +
|
||||
mno7890 second
|
||||
second.txt | 1 +
|
||||
pqr1234 first
|
||||
first.txt | 1 +
|
||||
unrelated.txt | 1 +
|
||||
----------
|
||||
|
||||
The staged addition of `unrelated.txt` has been incorporated into the `first`
|
||||
commit. All descendant commits have been replayed on top of the rewritten
|
||||
history.
|
||||
|
||||
Split a commit
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
+268
-23
@@ -10,6 +10,7 @@
|
||||
#include "gettext.h"
|
||||
#include "hex.h"
|
||||
#include "lockfile.h"
|
||||
#include "merge-ort.h"
|
||||
#include "oidmap.h"
|
||||
#include "parse-options.h"
|
||||
#include "path.h"
|
||||
@@ -23,6 +24,8 @@
|
||||
#include "unpack-trees.h"
|
||||
#include "wt-status.h"
|
||||
|
||||
#define GIT_HISTORY_FIXUP_USAGE \
|
||||
N_("git history fixup <commit> [--dry-run] [--update-refs=(branches|head)] [--reedit-message] [--empty=(drop|keep|abort)]")
|
||||
#define GIT_HISTORY_REWORD_USAGE \
|
||||
N_("git history reword <commit> [--dry-run] [--update-refs=(branches|head)]")
|
||||
#define GIT_HISTORY_SPLIT_USAGE \
|
||||
@@ -91,13 +94,18 @@ static int fill_commit_message(struct repository *repo,
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int commit_tree_with_edited_message_ext(struct repository *repo,
|
||||
const char *action,
|
||||
struct commit *commit_with_message,
|
||||
const struct commit_list *parents,
|
||||
const struct object_id *old_tree,
|
||||
const struct object_id *new_tree,
|
||||
struct commit **out)
|
||||
enum commit_tree_flags {
|
||||
COMMIT_TREE_EDIT_MESSAGE = (1 << 0),
|
||||
};
|
||||
|
||||
static int commit_tree_ext(struct repository *repo,
|
||||
const char *action,
|
||||
struct commit *commit_with_message,
|
||||
const struct commit_list *parents,
|
||||
const struct object_id *old_tree,
|
||||
const struct object_id *new_tree,
|
||||
struct commit **out,
|
||||
enum commit_tree_flags flags)
|
||||
{
|
||||
const char *exclude_gpgsig[] = {
|
||||
/* We reencode the message, so the encoding needs to be stripped. */
|
||||
@@ -122,10 +130,14 @@ static int commit_tree_with_edited_message_ext(struct repository *repo,
|
||||
original_author = xmemdupz(ptr, len);
|
||||
find_commit_subject(original_message, &original_body);
|
||||
|
||||
ret = fill_commit_message(repo, old_tree, new_tree,
|
||||
original_body, action, &commit_message);
|
||||
if (ret < 0)
|
||||
goto out;
|
||||
if (flags & COMMIT_TREE_EDIT_MESSAGE) {
|
||||
ret = fill_commit_message(repo, old_tree, new_tree,
|
||||
original_body, action, &commit_message);
|
||||
if (ret < 0)
|
||||
goto out;
|
||||
} else {
|
||||
strbuf_addstr(&commit_message, original_body);
|
||||
}
|
||||
|
||||
original_extra_headers = read_commit_extra_headers(commit_with_message,
|
||||
exclude_gpgsig);
|
||||
@@ -168,8 +180,8 @@ static int commit_tree_with_edited_message(struct repository *repo,
|
||||
oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
|
||||
}
|
||||
|
||||
return commit_tree_with_edited_message_ext(repo, action, original, original->parents,
|
||||
&parent_tree_oid, tree_oid, out);
|
||||
return commit_tree_ext(repo, action, original, original->parents,
|
||||
&parent_tree_oid, tree_oid, out, COMMIT_TREE_EDIT_MESSAGE);
|
||||
}
|
||||
|
||||
enum ref_action {
|
||||
@@ -326,10 +338,13 @@ static int handle_reference_updates(struct rev_info *revs,
|
||||
struct commit *original,
|
||||
struct commit *rewritten,
|
||||
const char *reflog_msg,
|
||||
int dry_run)
|
||||
int dry_run,
|
||||
enum replay_empty_commit_action empty)
|
||||
{
|
||||
const struct name_decoration *decoration;
|
||||
struct replay_revisions_options opts = { 0 };
|
||||
struct replay_revisions_options opts = {
|
||||
.empty = empty,
|
||||
};
|
||||
struct replay_result result = { 0 };
|
||||
struct ref_transaction *transaction = NULL;
|
||||
struct strbuf err = STRBUF_INIT;
|
||||
@@ -425,6 +440,236 @@ out:
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int commit_became_empty(struct repository *repo,
|
||||
struct commit *original,
|
||||
struct tree *result)
|
||||
{
|
||||
struct commit *parent = original->parents ? original->parents->item : NULL;
|
||||
struct object_id parent_tree_oid;
|
||||
|
||||
if (parent) {
|
||||
if (repo_parse_commit(repo, parent))
|
||||
return error(_("unable to parse parent of %s"),
|
||||
oid_to_hex(&original->object.oid));
|
||||
|
||||
parent_tree_oid = repo_get_commit_tree(repo, parent)->object.oid;
|
||||
} else {
|
||||
oidcpy(&parent_tree_oid, repo->hash_algo->empty_tree);
|
||||
}
|
||||
|
||||
return oideq(&result->object.oid, &parent_tree_oid);
|
||||
}
|
||||
|
||||
static int parse_opt_empty(const struct option *opt, const char *arg, int unset)
|
||||
{
|
||||
enum replay_empty_commit_action *value = opt->value;
|
||||
|
||||
BUG_ON_OPT_NEG(unset);
|
||||
|
||||
if (!strcmp(arg, "drop"))
|
||||
*value = REPLAY_EMPTY_COMMIT_DROP;
|
||||
else if (!strcmp(arg, "keep"))
|
||||
*value = REPLAY_EMPTY_COMMIT_KEEP;
|
||||
else if (!strcmp(arg, "abort"))
|
||||
*value = REPLAY_EMPTY_COMMIT_ABORT;
|
||||
else
|
||||
die(_("unrecognized '--empty=' action '%s'; "
|
||||
"valid values are \"drop\", \"keep\", and \"abort\"."), arg);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cmd_history_fixup(int argc,
|
||||
const char **argv,
|
||||
const char *prefix,
|
||||
struct repository *repo)
|
||||
{
|
||||
const char * const usage[] = {
|
||||
GIT_HISTORY_FIXUP_USAGE,
|
||||
NULL,
|
||||
};
|
||||
enum replay_empty_commit_action empty = REPLAY_EMPTY_COMMIT_DROP;
|
||||
enum ref_action action = REF_ACTION_DEFAULT;
|
||||
enum commit_tree_flags flags = 0;
|
||||
int dry_run = 0;
|
||||
struct option options[] = {
|
||||
OPT_CALLBACK_F(0, "update-refs", &action, "(branches|head)",
|
||||
N_("control which refs should be updated"),
|
||||
PARSE_OPT_NONEG, parse_ref_action),
|
||||
OPT_BOOL('n', "dry-run", &dry_run,
|
||||
N_("perform a dry-run without updating any refs")),
|
||||
OPT_BIT(0, "reedit-message", &flags,
|
||||
N_("open an editor to modify the commit message"),
|
||||
COMMIT_TREE_EDIT_MESSAGE),
|
||||
OPT_CALLBACK_F(0, "empty", &empty, "(drop|keep|abort)",
|
||||
N_("how to handle commits that become empty"),
|
||||
PARSE_OPT_NONEG, parse_opt_empty),
|
||||
OPT_END(),
|
||||
};
|
||||
struct merge_result merge_result = { 0 };
|
||||
struct merge_options merge_opts = { 0 };
|
||||
struct strbuf reflog_msg = STRBUF_INIT;
|
||||
struct commit *head_commit, *original, *rewritten;
|
||||
struct tree *head_tree, *original_tree, *index_tree;
|
||||
struct rev_info revs = { 0 };
|
||||
bool skip_commit = false;
|
||||
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);
|
||||
|
||||
if (action == REF_ACTION_DEFAULT)
|
||||
action = REF_ACTION_BRANCHES;
|
||||
|
||||
if (is_bare_repository()) {
|
||||
ret = error(_("cannot run fixup in a bare repository"));
|
||||
goto out;
|
||||
}
|
||||
|
||||
/* Resolve the original commit, which is the one we want to fix up. */
|
||||
original = lookup_commit_reference_by_name(argv[0]);
|
||||
if (!original) {
|
||||
ret = error(_("commit cannot be found: %s"), argv[0]);
|
||||
goto out;
|
||||
}
|
||||
|
||||
/*
|
||||
* Resolve HEAD so we can use its tree as the merge base: the staged
|
||||
* changes are expressed as a diff from HEAD's tree to the index tree.
|
||||
*/
|
||||
head_commit = lookup_commit_reference_by_name("HEAD");
|
||||
if (!head_commit) {
|
||||
ret = error(_("cannot look up HEAD"));
|
||||
goto out;
|
||||
}
|
||||
|
||||
head_tree = repo_get_commit_tree(repo, head_commit);
|
||||
if (!head_tree) {
|
||||
ret = error(_("cannot get tree for HEAD"));
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (repo_read_index(repo) < 0) {
|
||||
ret = error(_("unable to read index"));
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (!repo_index_has_changes(repo, head_tree, NULL)) {
|
||||
ret = error(_("nothing to fixup: no staged changes"));
|
||||
goto out;
|
||||
}
|
||||
|
||||
/*
|
||||
* Write the index as a tree object. This is the "theirs" side of the
|
||||
* three-way merge: it is HEAD's tree with the staged changes applied.
|
||||
*/
|
||||
index_tree = write_in_core_index_as_tree(repo, repo->index);
|
||||
if (!index_tree) {
|
||||
ret = error(_("unable to write index as a tree"));
|
||||
goto out;
|
||||
}
|
||||
|
||||
original_tree = repo_get_commit_tree(repo, original);
|
||||
if (!original_tree) {
|
||||
ret = error(_("cannot get tree for commit %s"), argv[0]);
|
||||
goto out;
|
||||
}
|
||||
|
||||
/*
|
||||
* Perform the three-way merge to reapply changes in the index onto the
|
||||
* target commit. This is using basically the same logic as a
|
||||
* cherry-pick, where the base commit is our HEAD, ours is the original
|
||||
* tree and theirs is the index tree.
|
||||
*/
|
||||
init_basic_merge_options(&merge_opts, repo);
|
||||
merge_opts.ancestor = "HEAD";
|
||||
merge_opts.branch1 = argv[0];
|
||||
merge_opts.branch2 = "staged";
|
||||
merge_incore_nonrecursive(&merge_opts, head_tree,
|
||||
original_tree, index_tree, &merge_result);
|
||||
|
||||
if (merge_result.clean < 0) {
|
||||
ret = error(_("merge failed while applying fixup"));
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (!merge_result.clean) {
|
||||
ret = error(_("fixup would produce conflicts; aborting"));
|
||||
goto out;
|
||||
}
|
||||
|
||||
ret = commit_became_empty(repo, original, merge_result.tree);
|
||||
if (ret < 0)
|
||||
goto out;
|
||||
if (ret > 0) {
|
||||
switch (empty) {
|
||||
case REPLAY_EMPTY_COMMIT_DROP:
|
||||
/*
|
||||
* Drop the target commit by replaying its descendants
|
||||
* directly onto its parent.
|
||||
*/
|
||||
rewritten = original->parents ? original->parents->item : NULL;
|
||||
|
||||
/*
|
||||
* TODO: we don't yet have the ability to drop root
|
||||
* commits, but there's ultimately no good reason for
|
||||
* this restriction to exist other than a technical
|
||||
* limitation.
|
||||
*/
|
||||
if (!rewritten) {
|
||||
ret = error(_("cannot drop root commit %s: "
|
||||
"it has no parent to replay onto"),
|
||||
argv[0]);
|
||||
goto out;
|
||||
}
|
||||
|
||||
skip_commit = true;
|
||||
break;
|
||||
case REPLAY_EMPTY_COMMIT_KEEP:
|
||||
/* Proceed and record the empty commit. */
|
||||
break;
|
||||
case REPLAY_EMPTY_COMMIT_ABORT:
|
||||
ret = error(_("fixup makes commit %s empty"), argv[0]);
|
||||
goto out;
|
||||
}
|
||||
}
|
||||
|
||||
ret = setup_revwalk(repo, action, original, &revs);
|
||||
if (ret)
|
||||
goto out;
|
||||
|
||||
if (!skip_commit) {
|
||||
ret = commit_tree_ext(repo, "fixup", original, original->parents,
|
||||
&original_tree->object.oid, &merge_result.tree->object.oid,
|
||||
&rewritten, flags);
|
||||
if (ret < 0) {
|
||||
ret = error(_("failed writing fixed-up commit"));
|
||||
goto out;
|
||||
}
|
||||
}
|
||||
|
||||
strbuf_addf(&reflog_msg, "fixup: updating %s", argv[0]);
|
||||
|
||||
ret = handle_reference_updates(&revs, action, original, rewritten,
|
||||
reflog_msg.buf, dry_run, empty);
|
||||
if (ret < 0) {
|
||||
ret = error(_("failed replaying descendants"));
|
||||
goto out;
|
||||
}
|
||||
|
||||
ret = 0;
|
||||
|
||||
out:
|
||||
merge_finalize(&merge_opts, &merge_result);
|
||||
strbuf_release(&reflog_msg);
|
||||
release_revisions(&revs);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int cmd_history_reword(int argc,
|
||||
const char **argv,
|
||||
const char *prefix,
|
||||
@@ -478,7 +723,7 @@ static int cmd_history_reword(int argc,
|
||||
strbuf_addf(&reflog_msg, "reword: updating %s", argv[0]);
|
||||
|
||||
ret = handle_reference_updates(&revs, action, original, rewritten,
|
||||
reflog_msg.buf, dry_run);
|
||||
reflog_msg.buf, dry_run, REPLAY_EMPTY_COMMIT_ABORT);
|
||||
if (ret < 0) {
|
||||
ret = error(_("failed replaying descendants"));
|
||||
goto out;
|
||||
@@ -616,9 +861,8 @@ static int split_commit(struct repository *repo,
|
||||
* The first commit is constructed from the split-out tree. The base
|
||||
* that shall be diffed against is the parent of the original commit.
|
||||
*/
|
||||
ret = commit_tree_with_edited_message_ext(repo, "split-out", original,
|
||||
original->parents, &parent_tree_oid,
|
||||
&split_tree->object.oid, &first_commit);
|
||||
ret = commit_tree_ext(repo, "split-out", original, original->parents, &parent_tree_oid,
|
||||
&split_tree->object.oid, &first_commit, COMMIT_TREE_EDIT_MESSAGE);
|
||||
if (ret < 0) {
|
||||
ret = error(_("failed writing first commit"));
|
||||
goto out;
|
||||
@@ -634,9 +878,8 @@ static int split_commit(struct repository *repo,
|
||||
old_tree_oid = &repo_get_commit_tree(repo, first_commit)->object.oid;
|
||||
new_tree_oid = &repo_get_commit_tree(repo, original)->object.oid;
|
||||
|
||||
ret = commit_tree_with_edited_message_ext(repo, "split-out", original,
|
||||
parents, old_tree_oid,
|
||||
new_tree_oid, &second_commit);
|
||||
ret = commit_tree_ext(repo, "split-out", original, parents, old_tree_oid,
|
||||
new_tree_oid, &second_commit, COMMIT_TREE_EDIT_MESSAGE);
|
||||
if (ret < 0) {
|
||||
ret = error(_("failed writing second commit"));
|
||||
goto out;
|
||||
@@ -717,7 +960,7 @@ static int cmd_history_split(int argc,
|
||||
strbuf_addf(&reflog_msg, "split: updating %s", argv[0]);
|
||||
|
||||
ret = handle_reference_updates(&revs, action, original, rewritten,
|
||||
reflog_msg.buf, dry_run);
|
||||
reflog_msg.buf, dry_run, REPLAY_EMPTY_COMMIT_ABORT);
|
||||
if (ret < 0) {
|
||||
ret = error(_("failed replaying descendants"));
|
||||
goto out;
|
||||
@@ -738,12 +981,14 @@ int cmd_history(int argc,
|
||||
struct repository *repo)
|
||||
{
|
||||
const char * const usage[] = {
|
||||
GIT_HISTORY_FIXUP_USAGE,
|
||||
GIT_HISTORY_REWORD_USAGE,
|
||||
GIT_HISTORY_SPLIT_USAGE,
|
||||
NULL,
|
||||
};
|
||||
parse_opt_subcommand_fn *fn = NULL;
|
||||
struct option options[] = {
|
||||
OPT_SUBCOMMAND("fixup", &fn, cmd_history_fixup),
|
||||
OPT_SUBCOMMAND("reword", &fn, cmd_history_reword),
|
||||
OPT_SUBCOMMAND("split", &fn, cmd_history_split),
|
||||
OPT_END(),
|
||||
|
||||
@@ -269,7 +269,8 @@ static struct commit *pick_regular_commit(struct repository *repo,
|
||||
struct commit *onto,
|
||||
struct merge_options *merge_opt,
|
||||
struct merge_result *result,
|
||||
enum replay_mode mode)
|
||||
enum replay_mode mode,
|
||||
enum replay_empty_commit_action empty)
|
||||
{
|
||||
struct commit *base, *replayed_base;
|
||||
struct tree *pickme_tree, *base_tree, *replayed_base_tree;
|
||||
@@ -321,12 +322,25 @@ static struct commit *pick_regular_commit(struct repository *repo,
|
||||
}
|
||||
merge_opt->ancestor = NULL;
|
||||
merge_opt->branch2 = NULL;
|
||||
|
||||
if (!result->clean)
|
||||
return NULL;
|
||||
/* Drop commits that become empty */
|
||||
|
||||
/* Handle commits that become empty */
|
||||
if (oideq(&replayed_base_tree->object.oid, &result->tree->object.oid) &&
|
||||
!oideq(&pickme_tree->object.oid, &base_tree->object.oid))
|
||||
return replayed_base;
|
||||
!oideq(&pickme_tree->object.oid, &base_tree->object.oid)) {
|
||||
switch (empty) {
|
||||
case REPLAY_EMPTY_COMMIT_DROP:
|
||||
return replayed_base;
|
||||
case REPLAY_EMPTY_COMMIT_KEEP:
|
||||
break;
|
||||
case REPLAY_EMPTY_COMMIT_ABORT:
|
||||
result->clean = error(_("commit %s became empty after replay"),
|
||||
oid_to_hex(&pickme->object.oid));
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
return create_commit(repo, result->tree, pickme, replayed_base, mode);
|
||||
}
|
||||
|
||||
@@ -417,7 +431,7 @@ int replay_revisions(struct rev_info *revs,
|
||||
|
||||
last_commit = pick_regular_commit(revs->repo, commit, replayed_commits,
|
||||
mode == REPLAY_MODE_REVERT ? last_commit : onto,
|
||||
&merge_opt, &result, mode);
|
||||
&merge_opt, &result, mode, opts->empty);
|
||||
if (!last_commit)
|
||||
break;
|
||||
|
||||
@@ -458,6 +472,11 @@ int replay_revisions(struct rev_info *revs,
|
||||
}
|
||||
}
|
||||
|
||||
if (result.clean < 0) {
|
||||
ret = -1;
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (!result.clean) {
|
||||
ret = 1;
|
||||
goto out;
|
||||
|
||||
@@ -6,6 +6,19 @@
|
||||
struct repository;
|
||||
struct rev_info;
|
||||
|
||||
/*
|
||||
* Controls what happens when a replayed commit becomes empty (i.e. its tree
|
||||
* is identical to its parent's tree after the replay).
|
||||
*/
|
||||
enum replay_empty_commit_action {
|
||||
/* Silently discard the empty commit. */
|
||||
REPLAY_EMPTY_COMMIT_DROP,
|
||||
/* Keep the empty commit as-is. */
|
||||
REPLAY_EMPTY_COMMIT_KEEP,
|
||||
/* Abort with an error. */
|
||||
REPLAY_EMPTY_COMMIT_ABORT,
|
||||
};
|
||||
|
||||
/*
|
||||
* A set of options that can be passed to `replay_revisions()`.
|
||||
*/
|
||||
@@ -43,6 +56,12 @@ struct replay_revisions_options {
|
||||
* Requires `onto` to be set.
|
||||
*/
|
||||
int contained;
|
||||
|
||||
/*
|
||||
* Controls what to do when a replayed commit becomes empty.
|
||||
* Defaults to REPLAY_EMPTY_COMMIT_DROP.
|
||||
*/
|
||||
enum replay_empty_commit_action empty;
|
||||
};
|
||||
|
||||
/* This struct is used as an out-parameter by `replay_revisions()`. */
|
||||
|
||||
@@ -397,6 +397,7 @@ integration_tests = [
|
||||
't3450-history.sh',
|
||||
't3451-history-reword.sh',
|
||||
't3452-history-split.sh',
|
||||
't3453-history-fixup.sh',
|
||||
't3500-cherry.sh',
|
||||
't3501-revert-cherry-pick.sh',
|
||||
't3502-cherry-pick-merge.sh',
|
||||
|
||||
Executable
+680
@@ -0,0 +1,680 @@
|
||||
#!/bin/sh
|
||||
|
||||
test_description='tests for git-history fixup subcommand'
|
||||
|
||||
. ./test-lib.sh
|
||||
|
||||
fixup_with_message () {
|
||||
cat >message &&
|
||||
write_script fake-editor.sh <<-\EOF &&
|
||||
cp message "$1"
|
||||
EOF
|
||||
test_set_editor "$(pwd)"/fake-editor.sh &&
|
||||
git history fixup --reedit-message "$@" &&
|
||||
rm fake-editor.sh message
|
||||
}
|
||||
|
||||
expect_changes () {
|
||||
git log --format="%s" --numstat "$@" >actual.raw &&
|
||||
sed '/^$/d' <actual.raw >actual &&
|
||||
cat >expect &&
|
||||
test_cmp expect actual
|
||||
}
|
||||
|
||||
test_expect_success 'errors on missing commit argument' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo &&
|
||||
(
|
||||
cd repo &&
|
||||
test_commit initial &&
|
||||
test_must_fail git history fixup 2>err &&
|
||||
test_grep "command expects a single revision" err
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'errors on too many arguments' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo &&
|
||||
(
|
||||
cd repo &&
|
||||
test_commit initial &&
|
||||
test_must_fail git history fixup HEAD HEAD 2>err &&
|
||||
test_grep "command expects a single revision" err
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'errors on unknown revision' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo &&
|
||||
(
|
||||
cd repo &&
|
||||
test_commit initial &&
|
||||
test_must_fail git history fixup does-not-exist 2>err &&
|
||||
test_grep "commit cannot be found: does-not-exist" err
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'errors when nothing is staged' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo &&
|
||||
(
|
||||
cd repo &&
|
||||
test_commit initial &&
|
||||
test_must_fail git history fixup HEAD 2>err &&
|
||||
test_grep "nothing to fixup: no staged changes" err
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'errors in a bare repository' '
|
||||
test_when_finished "rm -rf repo repo.git" &&
|
||||
git init repo &&
|
||||
test_commit -C repo initial &&
|
||||
git clone --bare repo repo.git &&
|
||||
test_must_fail git -C repo.git history fixup HEAD 2>err &&
|
||||
test_grep "cannot run fixup in a bare repository" err
|
||||
'
|
||||
|
||||
test_expect_success 'errors with invalid --empty= value' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo &&
|
||||
test_must_fail git -C repo history fixup --empty=bogus HEAD 2>err &&
|
||||
test_grep "unrecognized.*--empty.*bogus" err
|
||||
'
|
||||
|
||||
test_expect_success 'can fixup the tip commit' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo &&
|
||||
(
|
||||
cd repo &&
|
||||
test_commit initial &&
|
||||
echo content >file.txt &&
|
||||
git add file.txt &&
|
||||
git commit -m "add file" &&
|
||||
|
||||
echo fix >>file.txt &&
|
||||
git add file.txt &&
|
||||
|
||||
expect_changes <<-\EOF &&
|
||||
add file
|
||||
1 0 file.txt
|
||||
initial
|
||||
1 0 initial.t
|
||||
EOF
|
||||
|
||||
git symbolic-ref HEAD >branch-expect &&
|
||||
git history fixup HEAD &&
|
||||
git symbolic-ref HEAD >branch-actual &&
|
||||
test_cmp branch-expect branch-actual &&
|
||||
|
||||
expect_changes <<-\EOF &&
|
||||
add file
|
||||
2 0 file.txt
|
||||
initial
|
||||
1 0 initial.t
|
||||
EOF
|
||||
|
||||
# Verify the fix is in the tip commit tree
|
||||
git show HEAD:file.txt >actual &&
|
||||
printf "content\nfix\n" >expect &&
|
||||
test_cmp expect actual &&
|
||||
|
||||
git reflog >reflog &&
|
||||
test_grep "fixup: updating HEAD" reflog
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'can fixup a commit in the middle of history' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo &&
|
||||
(
|
||||
cd repo &&
|
||||
test_commit first &&
|
||||
echo content >file.txt &&
|
||||
git add file.txt &&
|
||||
git commit -m "add file" &&
|
||||
test_commit third &&
|
||||
|
||||
echo fix >>file.txt &&
|
||||
git add file.txt &&
|
||||
|
||||
expect_changes <<-\EOF &&
|
||||
third
|
||||
1 0 third.t
|
||||
add file
|
||||
1 0 file.txt
|
||||
first
|
||||
1 0 first.t
|
||||
EOF
|
||||
|
||||
git history fixup HEAD~ &&
|
||||
|
||||
expect_changes <<-\EOF &&
|
||||
third
|
||||
1 0 third.t
|
||||
add file
|
||||
2 0 file.txt
|
||||
first
|
||||
1 0 first.t
|
||||
EOF
|
||||
|
||||
# Verify the fix landed in the "add file" commit.
|
||||
git show HEAD~:file.txt >actual &&
|
||||
printf "content\nfix\n" >expect &&
|
||||
test_cmp expect actual &&
|
||||
|
||||
# And verify that the replayed commit also has the change.
|
||||
git show HEAD:file.txt >actual &&
|
||||
printf "content\nfix\n" >expect &&
|
||||
test_cmp expect actual
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'can fixup root commit' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo &&
|
||||
(
|
||||
cd repo &&
|
||||
echo initial >root.txt &&
|
||||
git add root.txt &&
|
||||
git commit -m "root" &&
|
||||
test_commit second &&
|
||||
|
||||
expect_changes <<-\EOF &&
|
||||
second
|
||||
1 0 second.t
|
||||
root
|
||||
1 0 root.txt
|
||||
EOF
|
||||
|
||||
echo fix >>root.txt &&
|
||||
git add root.txt &&
|
||||
git history fixup HEAD~ &&
|
||||
|
||||
expect_changes <<-\EOF &&
|
||||
second
|
||||
1 0 second.t
|
||||
root
|
||||
2 0 root.txt
|
||||
EOF
|
||||
|
||||
git show HEAD~:root.txt >actual &&
|
||||
printf "initial\nfix\n" >expect &&
|
||||
test_cmp expect actual
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'preserves commit message and authorship' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo &&
|
||||
(
|
||||
cd repo &&
|
||||
test_commit initial &&
|
||||
echo content >file.txt &&
|
||||
git add file.txt &&
|
||||
git commit --author="Original <original@example.com>" -m "original message" &&
|
||||
|
||||
echo fix >>file.txt &&
|
||||
git add file.txt &&
|
||||
git history fixup HEAD &&
|
||||
|
||||
# Message preserved
|
||||
git log -1 --format="%s" >actual &&
|
||||
echo "original message" >expect &&
|
||||
test_cmp expect actual &&
|
||||
|
||||
# Authorship preserved
|
||||
git log -1 --format="%an <%ae>" >actual &&
|
||||
echo "Original <original@example.com>" >expect &&
|
||||
test_cmp expect actual
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'updates all descendant branches by default' '
|
||||
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 switch main &&
|
||||
|
||||
echo fix >fix.txt &&
|
||||
git add fix.txt &&
|
||||
git history fixup base &&
|
||||
|
||||
expect_changes --branches <<-\EOF &&
|
||||
theirs
|
||||
1 0 theirs.t
|
||||
ours
|
||||
1 0 ours.t
|
||||
base
|
||||
1 0 base.t
|
||||
1 0 fix.txt
|
||||
EOF
|
||||
|
||||
# Both branches should have the fix in the base
|
||||
git show main~:fix.txt >actual &&
|
||||
echo fix >expect &&
|
||||
test_cmp expect actual &&
|
||||
git show branch~:fix.txt >actual &&
|
||||
test_cmp expect actual
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'can fixup 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 &&
|
||||
|
||||
# Stage a change while on "theirs"
|
||||
echo fix >fix.txt &&
|
||||
git add fix.txt &&
|
||||
|
||||
# Ensure that "ours" does not change, as it does not contain
|
||||
# the commit in question.
|
||||
git rev-parse ours >ours-before &&
|
||||
git history fixup theirs &&
|
||||
git rev-parse ours >ours-after &&
|
||||
test_cmp ours-before ours-after &&
|
||||
|
||||
git show HEAD:fix.txt >actual &&
|
||||
echo fix >expect &&
|
||||
test_cmp expect actual
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success '--dry-run 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 main-tip &&
|
||||
git switch branch &&
|
||||
test_commit branch-tip &&
|
||||
git switch main &&
|
||||
|
||||
echo fix >fix.txt &&
|
||||
git add fix.txt &&
|
||||
|
||||
git refs list >refs-before &&
|
||||
git history fixup --dry-run base >updates &&
|
||||
git refs list >refs-after &&
|
||||
test_cmp refs-before refs-after &&
|
||||
|
||||
test_grep "update refs/heads/main" updates &&
|
||||
test_grep "update refs/heads/branch" updates &&
|
||||
|
||||
expect_changes --branches <<-\EOF &&
|
||||
branch-tip
|
||||
1 0 branch-tip.t
|
||||
main-tip
|
||||
1 0 main-tip.t
|
||||
base
|
||||
1 0 base.t
|
||||
EOF
|
||||
|
||||
git update-ref --stdin <updates &&
|
||||
expect_changes --branches <<-\EOF
|
||||
branch-tip
|
||||
1 0 branch-tip.t
|
||||
main-tip
|
||||
1 0 main-tip.t
|
||||
base
|
||||
1 0 base.t
|
||||
1 0 fix.txt
|
||||
EOF
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success '--update-refs=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 main-tip &&
|
||||
git switch branch &&
|
||||
test_commit branch-tip &&
|
||||
|
||||
echo fix >fix.txt &&
|
||||
git add fix.txt &&
|
||||
|
||||
# Only HEAD (branch) should be updated
|
||||
git history fixup --update-refs=head base &&
|
||||
|
||||
# The main branch should be unaffected.
|
||||
expect_changes main <<-\EOF &&
|
||||
main-tip
|
||||
1 0 main-tip.t
|
||||
base
|
||||
1 0 base.t
|
||||
EOF
|
||||
|
||||
# But the currently checked out branch should be modified.
|
||||
expect_changes branch <<-\EOF
|
||||
branch-tip
|
||||
1 0 branch-tip.t
|
||||
base
|
||||
1 0 base.t
|
||||
1 0 fix.txt
|
||||
EOF
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success '--update-refs=head refuses to rewrite commits not in HEAD ancestry' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo --initial-branch=main &&
|
||||
(
|
||||
cd repo &&
|
||||
test_commit base &&
|
||||
git branch other &&
|
||||
test_commit main-tip &&
|
||||
git switch other &&
|
||||
test_commit other-tip &&
|
||||
|
||||
echo fix >fix.txt &&
|
||||
git add fix.txt &&
|
||||
|
||||
test_must_fail git history fixup --update-refs=head main-tip 2>err &&
|
||||
test_grep "rewritten commit must be an ancestor of HEAD" err
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'aborts when fixup would produce conflicts' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo &&
|
||||
(
|
||||
cd repo &&
|
||||
|
||||
echo "line one" >file.txt &&
|
||||
git add file.txt &&
|
||||
git commit -m "first" &&
|
||||
|
||||
echo "line two" >file.txt &&
|
||||
git add file.txt &&
|
||||
git commit -m "second" &&
|
||||
|
||||
echo "conflicting change" >file.txt &&
|
||||
git add file.txt &&
|
||||
|
||||
git refs list >refs-before &&
|
||||
test_must_fail git history fixup HEAD~ 2>err &&
|
||||
test_grep "fixup would produce conflicts" err &&
|
||||
git refs list >refs-after &&
|
||||
test_cmp refs-before refs-after
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success '--reedit-message opens editor for the commit message' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo &&
|
||||
(
|
||||
cd repo &&
|
||||
test_commit initial &&
|
||||
echo content >file.txt &&
|
||||
git add file.txt &&
|
||||
git commit -m "add file" &&
|
||||
|
||||
echo fix >>file.txt &&
|
||||
git add file.txt &&
|
||||
|
||||
fixup_with_message HEAD <<-\EOF &&
|
||||
add file with fix
|
||||
EOF
|
||||
|
||||
expect_changes --branches <<-\EOF
|
||||
add file with fix
|
||||
2 0 file.txt
|
||||
initial
|
||||
1 0 initial.t
|
||||
EOF
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'retains unstaged working tree changes after fixup' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo &&
|
||||
(
|
||||
cd repo &&
|
||||
touch a b &&
|
||||
git add . &&
|
||||
git commit -m "initial commit" &&
|
||||
echo staged >a &&
|
||||
echo unstaged >b &&
|
||||
git add a &&
|
||||
git history fixup HEAD &&
|
||||
|
||||
# b is still modified in the worktree but not staged
|
||||
cat >expect <<-\EOF &&
|
||||
M b
|
||||
EOF
|
||||
git status --porcelain --untracked-files=no >actual &&
|
||||
test_cmp expect actual
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'index is clean after fixup when target is HEAD' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo &&
|
||||
(
|
||||
cd repo &&
|
||||
|
||||
test_commit initial &&
|
||||
echo fix >fix.txt &&
|
||||
git add fix.txt &&
|
||||
git history fixup HEAD &&
|
||||
|
||||
git status --porcelain --untracked-files=no >actual &&
|
||||
test_must_be_empty actual
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'index is unchanged on conflict' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo &&
|
||||
(
|
||||
cd repo &&
|
||||
|
||||
echo base >file.txt &&
|
||||
git add file.txt &&
|
||||
git commit -m base &&
|
||||
echo change >file.txt &&
|
||||
git add file.txt &&
|
||||
git commit -m change &&
|
||||
|
||||
echo conflict >file.txt &&
|
||||
git add file.txt &&
|
||||
|
||||
git diff --cached >index-before &&
|
||||
test_must_fail git history fixup HEAD~ &&
|
||||
git diff --cached >index-after &&
|
||||
test_cmp index-before index-after
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success '--empty=drop removes target commit and replays descendants onto its parent' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo --initial-branch=main &&
|
||||
(
|
||||
cd repo &&
|
||||
|
||||
test_commit first &&
|
||||
test_commit second &&
|
||||
test_commit third &&
|
||||
|
||||
git rm second.t &&
|
||||
git history fixup --empty=drop HEAD~ &&
|
||||
|
||||
expect_changes <<-\EOF &&
|
||||
third
|
||||
1 0 third.t
|
||||
first
|
||||
1 0 first.t
|
||||
EOF
|
||||
test_must_fail git show HEAD:second.t
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success '--empty=drop errors out when dropping the root commit' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo &&
|
||||
(
|
||||
cd repo &&
|
||||
|
||||
test_commit first &&
|
||||
test_commit second &&
|
||||
|
||||
git rm first.t &&
|
||||
test_must_fail git history fixup --empty=drop HEAD~ 2>err &&
|
||||
test_grep "cannot drop root commit" err
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success '--empty=drop can drop the HEAD commit' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo &&
|
||||
(
|
||||
cd repo &&
|
||||
|
||||
test_commit first &&
|
||||
test_commit second &&
|
||||
|
||||
git rm second.t &&
|
||||
git history fixup --empty=drop HEAD &&
|
||||
|
||||
expect_changes <<-\EOF
|
||||
first
|
||||
1 0 first.t
|
||||
EOF
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success '--empty=drop drops empty replayed commits' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo &&
|
||||
(
|
||||
cd repo &&
|
||||
|
||||
touch base remove-me &&
|
||||
git add . &&
|
||||
git commit -m "base" &&
|
||||
git rm remove-me &&
|
||||
git commit -m "remove" &&
|
||||
touch reintroduce remove-me &&
|
||||
git add . &&
|
||||
git commit -m "reintroduce" &&
|
||||
|
||||
git rm remove-me &&
|
||||
git history fixup --empty=drop HEAD~2 &&
|
||||
|
||||
expect_changes <<-\EOF
|
||||
reintroduce
|
||||
0 0 reintroduce
|
||||
0 0 remove-me
|
||||
base
|
||||
0 0 base
|
||||
EOF
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success '--empty=keep keeps commit when fixup target becomes empty' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo &&
|
||||
(
|
||||
cd repo &&
|
||||
|
||||
test_commit first &&
|
||||
test_commit second &&
|
||||
test_commit third &&
|
||||
|
||||
git rm second.t &&
|
||||
git history fixup --empty=keep HEAD~ &&
|
||||
|
||||
expect_changes <<-\EOF
|
||||
third
|
||||
1 0 third.t
|
||||
second
|
||||
first
|
||||
1 0 first.t
|
||||
EOF
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success '--empty=keep keeps commit when replayed commit becomes empty' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo &&
|
||||
(
|
||||
cd repo &&
|
||||
|
||||
touch base remove-me &&
|
||||
git add . &&
|
||||
git commit -m "base" &&
|
||||
git rm remove-me &&
|
||||
git commit -m "remove" &&
|
||||
touch reintroduce remove-me &&
|
||||
git add . &&
|
||||
git commit -m "reintroduce" &&
|
||||
|
||||
git rm remove-me &&
|
||||
git history fixup --empty=keep HEAD~2 &&
|
||||
|
||||
expect_changes <<-\EOF
|
||||
reintroduce
|
||||
0 0 reintroduce
|
||||
0 0 remove-me
|
||||
remove
|
||||
base
|
||||
0 0 base
|
||||
EOF
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success '--empty=abort errors out when fixup target becomes empty' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo &&
|
||||
(
|
||||
cd repo &&
|
||||
|
||||
test_commit first &&
|
||||
test_commit second &&
|
||||
|
||||
git rm first.t &&
|
||||
test_must_fail git history fixup --empty=abort HEAD~ 2>err &&
|
||||
test_grep "fixup makes commit.*empty" err
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success '--empty=abort errors out when a descendant becomes empty during replay' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo --initial-branch=main &&
|
||||
(
|
||||
cd repo &&
|
||||
|
||||
touch base remove-me &&
|
||||
git add . &&
|
||||
git commit -m "base" &&
|
||||
git rm remove-me &&
|
||||
git commit -m "remove" &&
|
||||
touch reintroduce remove-me &&
|
||||
git add . &&
|
||||
git commit -m "reintroduce" &&
|
||||
|
||||
git rm remove-me &&
|
||||
test_must_fail git history fixup --empty=abort HEAD~2 2>err &&
|
||||
test_grep "became empty after replay" err
|
||||
)
|
||||
'
|
||||
|
||||
test_done
|
||||
Reference in New Issue
Block a user