mirror of
https://github.com/git-for-windows/git.git
synced 2026-06-05 00:20:47 -05:00
Merge branch 'ps/history-drop' into seen
The experimental "git history" command has been taught a new "drop" subcommand to remove a commit and replay its descendants onto its parent. * ps/history-drop: builtin/history: implement "drop" subcommand builtin/history: split handling of ref updates into two phases reset: stop assuming that the caller passes in a clean index reset: allow the caller to specify the current HEAD object reset: introduce ability to skip reference updates reset: introduce dry-run mode reset: modernize flags passed to `reset_head()` reset: drop `USE_THE_REPOSITORY_VARIABLE` read-cache: split out function to drop unmerged entries to stage 0
This commit is contained in:
@@ -8,6 +8,7 @@ git-history - EXPERIMENTAL: Rewrite history
|
||||
SYNOPSIS
|
||||
--------
|
||||
[synopsis]
|
||||
git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)]
|
||||
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>...]
|
||||
@@ -51,13 +52,28 @@ 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.
|
||||
supported. Likewise, `drop` cannot remove the root commit or a merge commit.
|
||||
|
||||
COMMANDS
|
||||
--------
|
||||
|
||||
The following commands are available to rewrite history in different ways:
|
||||
|
||||
`drop <commit>`::
|
||||
Remove the specified commit from the history. All descendants of the
|
||||
commit are replayed directly onto its parent.
|
||||
+
|
||||
The root commit cannot be dropped as that may lead to edge cases where refs
|
||||
end up with no commits anymore. Merge commits cannot be dropped either; see
|
||||
LIMITATIONS.
|
||||
+
|
||||
If `HEAD` points at a commit that is to be rewritten, the index and working
|
||||
tree are updated to match the new `HEAD`. The command aborts before any
|
||||
references are updated in case local modifications would be overwritten.
|
||||
+
|
||||
If replaying any descendant would result in a conflict, the command aborts
|
||||
with an error.
|
||||
|
||||
`fixup <commit>`::
|
||||
Apply the currently staged changes to the specified commit. This is
|
||||
similar in nature to `git commit --fixup=<commit>` followed by `git
|
||||
@@ -170,6 +186,26 @@ 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.
|
||||
|
||||
Drop a commit
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
----------
|
||||
$ git log --oneline
|
||||
abc1234 (HEAD -> main) third
|
||||
def5678 second
|
||||
ghi9012 first
|
||||
|
||||
$ git history drop def5678
|
||||
|
||||
$ git log --oneline
|
||||
jkl3456 (HEAD -> main) third
|
||||
ghi9012 first
|
||||
----------
|
||||
|
||||
The `second` commit has been removed from the history, and `third` has been
|
||||
replayed directly on top of `first`. All branches that pointed at the dropped
|
||||
commit have been moved to its parent.
|
||||
|
||||
Split a commit
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
@@ -17,13 +17,17 @@
|
||||
#include "read-cache.h"
|
||||
#include "refs.h"
|
||||
#include "replay.h"
|
||||
#include "reset.h"
|
||||
#include "revision.h"
|
||||
#include "sequencer.h"
|
||||
#include "strvec.h"
|
||||
#include "tree.h"
|
||||
#include "tree-walk.h"
|
||||
#include "unpack-trees.h"
|
||||
#include "wt-status.h"
|
||||
|
||||
#define GIT_HISTORY_DROP_USAGE \
|
||||
N_("git history drop <commit> [--dry-run] [--update-refs=(branches|head)] [--empty=(drop|keep|abort)]")
|
||||
#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 \
|
||||
@@ -333,21 +337,17 @@ static int handle_ref_update(struct ref_transaction *transaction,
|
||||
NULL, NULL, 0, reflog_msg, err);
|
||||
}
|
||||
|
||||
static int handle_reference_updates(struct rev_info *revs,
|
||||
enum ref_action action,
|
||||
struct commit *original,
|
||||
struct commit *rewritten,
|
||||
const char *reflog_msg,
|
||||
int dry_run,
|
||||
enum replay_empty_commit_action empty)
|
||||
static int compute_pending_ref_updates(struct rev_info *revs,
|
||||
enum ref_action action,
|
||||
struct commit *original,
|
||||
struct commit *rewritten,
|
||||
enum replay_empty_commit_action empty,
|
||||
struct replay_result *result)
|
||||
{
|
||||
const struct name_decoration *decoration;
|
||||
struct replay_revisions_options opts = {
|
||||
.empty = empty,
|
||||
};
|
||||
struct replay_result result = { 0 };
|
||||
struct ref_transaction *transaction = NULL;
|
||||
struct strbuf err = STRBUF_INIT;
|
||||
char hex[GIT_MAX_HEXSZ + 1];
|
||||
bool detached_head;
|
||||
int head_flags = 0;
|
||||
@@ -359,34 +359,13 @@ static int handle_reference_updates(struct rev_info *revs,
|
||||
|
||||
opts.onto = oid_to_hex_r(hex, &rewritten->object.oid);
|
||||
|
||||
ret = replay_revisions(revs, &opts, &result);
|
||||
ret = replay_revisions(revs, &opts, result);
|
||||
if (ret)
|
||||
goto out;
|
||||
return ret;
|
||||
|
||||
if (action != REF_ACTION_BRANCHES && action != REF_ACTION_HEAD)
|
||||
BUG("unsupported ref action %d", action);
|
||||
|
||||
if (!dry_run) {
|
||||
transaction = ref_store_transaction_begin(get_main_ref_store(revs->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 = handle_ref_update(transaction,
|
||||
result.updates[i].refname,
|
||||
&result.updates[i].new_oid,
|
||||
&result.updates[i].old_oid,
|
||||
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
|
||||
@@ -414,14 +393,43 @@ static int handle_reference_updates(struct rev_info *revs,
|
||||
!detached_head)
|
||||
continue;
|
||||
|
||||
ALLOC_GROW(result->updates, result->updates_nr + 1, result->updates_alloc);
|
||||
result->updates[result->updates_nr].refname = xstrdup(decoration->name);
|
||||
result->updates[result->updates_nr].old_oid = original->object.oid;
|
||||
result->updates[result->updates_nr].new_oid = rewritten->object.oid;
|
||||
result->updates_nr++;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int apply_pending_ref_updates(struct repository *repo,
|
||||
const struct replay_result *result,
|
||||
const char *reflog_msg,
|
||||
int dry_run)
|
||||
{
|
||||
struct ref_transaction *transaction = NULL;
|
||||
struct strbuf err = STRBUF_INIT;
|
||||
int ret;
|
||||
|
||||
if (!dry_run) {
|
||||
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 = handle_ref_update(transaction,
|
||||
decoration->name,
|
||||
&rewritten->object.oid,
|
||||
&original->object.oid,
|
||||
result->updates[i].refname,
|
||||
&result->updates[i].new_oid,
|
||||
&result->updates[i].old_oid,
|
||||
reflog_msg, &err);
|
||||
if (ret) {
|
||||
ret = error(_("failed to update ref '%s': %s"),
|
||||
decoration->name, err.buf);
|
||||
result->updates[i].refname, err.buf);
|
||||
goto out;
|
||||
}
|
||||
}
|
||||
@@ -435,11 +443,33 @@ static int handle_reference_updates(struct rev_info *revs,
|
||||
|
||||
out:
|
||||
ref_transaction_free(transaction);
|
||||
replay_result_release(&result);
|
||||
strbuf_release(&err);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int handle_reference_updates(struct rev_info *revs,
|
||||
enum ref_action action,
|
||||
struct commit *original,
|
||||
struct commit *rewritten,
|
||||
const char *reflog_msg,
|
||||
int dry_run,
|
||||
enum replay_empty_commit_action empty)
|
||||
{
|
||||
struct replay_result result = { 0 };
|
||||
int ret;
|
||||
|
||||
ret = compute_pending_ref_updates(revs, action, original, rewritten,
|
||||
empty, &result);
|
||||
if (ret)
|
||||
goto out;
|
||||
|
||||
ret = apply_pending_ref_updates(revs->repo, &result, reflog_msg, dry_run);
|
||||
|
||||
out:
|
||||
replay_result_release(&result);
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int commit_became_empty(struct repository *repo,
|
||||
struct commit *original,
|
||||
struct tree *result)
|
||||
@@ -975,12 +1005,194 @@ out:
|
||||
return ret;
|
||||
}
|
||||
|
||||
static int update_worktree(struct repository *repo,
|
||||
const struct commit *old_head,
|
||||
const struct commit *new_head,
|
||||
bool dry_run)
|
||||
{
|
||||
struct reset_head_opts opts = {
|
||||
.oid_from = &old_head->object.oid,
|
||||
.oid = &new_head->object.oid,
|
||||
.flags = RESET_HEAD_SKIP_REF_UPDATES,
|
||||
};
|
||||
if (dry_run)
|
||||
opts.flags |= RESET_HEAD_DRY_RUN;
|
||||
return reset_head(repo, &opts);
|
||||
}
|
||||
|
||||
static int find_head_tree_change(struct repository *repo,
|
||||
const struct replay_result *result,
|
||||
struct commit **old_head,
|
||||
struct commit **new_head,
|
||||
bool *changed)
|
||||
{
|
||||
const struct replay_ref_update *head_update = NULL;
|
||||
struct commit *old_head_commit, *new_head_commit;
|
||||
struct tree *old_head_tree, *new_head_tree;
|
||||
const char *head_target;
|
||||
int head_flags;
|
||||
|
||||
*changed = false;
|
||||
|
||||
head_target = refs_resolve_ref_unsafe(get_main_ref_store(repo),
|
||||
"HEAD", RESOLVE_REF_NO_RECURSE,
|
||||
NULL, &head_flags);
|
||||
if (!head_target)
|
||||
return error(_("cannot look up HEAD"));
|
||||
if (!(head_flags & REF_ISSYMREF))
|
||||
head_target = "HEAD";
|
||||
|
||||
for (size_t i = 0; i < result->updates_nr; i++) {
|
||||
if (!strcmp(result->updates[i].refname, head_target)) {
|
||||
head_update = &result->updates[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!head_update)
|
||||
return 0;
|
||||
|
||||
old_head_commit = lookup_commit_reference(repo, &head_update->old_oid);
|
||||
new_head_commit = lookup_commit_reference(repo, &head_update->new_oid);
|
||||
if (!old_head_commit || !new_head_commit)
|
||||
return error(_("cannot resolve HEAD commit"));
|
||||
|
||||
old_head_tree = repo_get_commit_tree(repo, old_head_commit);
|
||||
new_head_tree = repo_get_commit_tree(repo, new_head_commit);
|
||||
if (!old_head_tree || !new_head_tree)
|
||||
return error(_("cannot resolve tree for HEAD"));
|
||||
|
||||
if (oideq(&old_head_tree->object.oid, &new_head_tree->object.oid))
|
||||
return 0;
|
||||
|
||||
*old_head = old_head_commit;
|
||||
*new_head = new_head_commit;
|
||||
*changed = true;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int cmd_history_drop(int argc,
|
||||
const char **argv,
|
||||
const char *prefix,
|
||||
struct repository *repo)
|
||||
{
|
||||
const char * const usage[] = {
|
||||
GIT_HISTORY_DROP_USAGE,
|
||||
NULL,
|
||||
};
|
||||
enum replay_empty_commit_action empty = REPLAY_EMPTY_COMMIT_DROP;
|
||||
enum ref_action action = REF_ACTION_DEFAULT;
|
||||
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_CALLBACK_F(0, "empty", &empty, "(drop|keep|abort)",
|
||||
N_("how to handle descendants that become empty"),
|
||||
PARSE_OPT_NONEG, parse_opt_empty),
|
||||
OPT_END(),
|
||||
};
|
||||
struct strbuf reflog_msg = STRBUF_INIT;
|
||||
struct commit *original, *rewritten;
|
||||
struct rev_info revs = { 0 };
|
||||
struct replay_result result = { 0 };
|
||||
struct commit *old_head, *new_head;
|
||||
bool head_moves = 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;
|
||||
|
||||
original = lookup_commit_reference_by_name(argv[0]);
|
||||
if (!original) {
|
||||
ret = error(_("commit cannot be found: %s"), argv[0]);
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (!original->parents) {
|
||||
ret = error(_("cannot drop root commit %s: "
|
||||
"it has no parent to replay onto"),
|
||||
argv[0]);
|
||||
goto out;
|
||||
} else if (original->parents->next) {
|
||||
ret = error(_("cannot drop merge commit: %s"), argv[0]);
|
||||
goto out;
|
||||
}
|
||||
|
||||
ret = setup_revwalk(repo, action, original, &revs);
|
||||
if (ret)
|
||||
goto out;
|
||||
|
||||
rewritten = original->parents->item;
|
||||
|
||||
ret = compute_pending_ref_updates(&revs, action, original, rewritten,
|
||||
empty, &result);
|
||||
if (ret) {
|
||||
ret = error(_("failed replaying descendants"));
|
||||
goto out;
|
||||
}
|
||||
|
||||
/*
|
||||
* If HEAD will move as a result of the rewrite then we'll have to
|
||||
* merge in the changes into the worktree and index. This merge can of
|
||||
* course conflict, which will cause the whole operation to abort.
|
||||
*
|
||||
* If we had already updated the refs at that point then we'd have an
|
||||
* inconsistent repository state. So we first perform a dry-run merge
|
||||
* here before updating refs.
|
||||
*/
|
||||
if (!dry_run && !is_bare_repository()) {
|
||||
ret = find_head_tree_change(repo, &result, &old_head,
|
||||
&new_head, &head_moves);
|
||||
if (ret < 0)
|
||||
goto out;
|
||||
|
||||
if (head_moves && update_worktree(repo, old_head, new_head, true) < 0) {
|
||||
ret = error(_("dropping this commit would "
|
||||
"overwrite local changes; aborting"));
|
||||
goto out;
|
||||
}
|
||||
}
|
||||
|
||||
strbuf_addf(&reflog_msg, "drop: dropping %s", argv[0]);
|
||||
ret = apply_pending_ref_updates(repo, &result, reflog_msg.buf, dry_run);
|
||||
if (ret < 0) {
|
||||
ret = error(_("failed to update references"));
|
||||
goto out;
|
||||
}
|
||||
|
||||
if (head_moves && update_worktree(repo, old_head, new_head, false) < 0) {
|
||||
ret = error(_("could not update working tree to new commit %s"),
|
||||
oid_to_hex(&new_head->object.oid));
|
||||
goto out;
|
||||
}
|
||||
|
||||
ret = 0;
|
||||
|
||||
out:
|
||||
replay_result_release(&result);
|
||||
strbuf_release(&reflog_msg);
|
||||
release_revisions(&revs);
|
||||
return ret;
|
||||
}
|
||||
|
||||
int cmd_history(int argc,
|
||||
const char **argv,
|
||||
const char *prefix,
|
||||
struct repository *repo)
|
||||
{
|
||||
const char * const usage[] = {
|
||||
GIT_HISTORY_DROP_USAGE,
|
||||
GIT_HISTORY_FIXUP_USAGE,
|
||||
GIT_HISTORY_REWORD_USAGE,
|
||||
GIT_HISTORY_SPLIT_USAGE,
|
||||
@@ -988,6 +1200,7 @@ int cmd_history(int argc,
|
||||
};
|
||||
parse_opt_subcommand_fn *fn = NULL;
|
||||
struct option options[] = {
|
||||
OPT_SUBCOMMAND("drop", &fn, cmd_history_drop),
|
||||
OPT_SUBCOMMAND("fixup", &fn, cmd_history_fixup),
|
||||
OPT_SUBCOMMAND("reword", &fn, cmd_history_reword),
|
||||
OPT_SUBCOMMAND("split", &fn, cmd_history_split),
|
||||
|
||||
@@ -1876,7 +1876,7 @@ int cmd_rebase(int argc,
|
||||
options.reflog_action, options.onto_name);
|
||||
ropts.oid = &options.onto->object.oid;
|
||||
ropts.orig_head = &options.orig_head->object.oid;
|
||||
ropts.flags = RESET_HEAD_DETACH | RESET_ORIG_HEAD |
|
||||
ropts.flags = RESET_HEAD_DETACH | RESET_HEAD_ORIG_HEAD |
|
||||
RESET_HEAD_RUN_POST_CHECKOUT_HOOK;
|
||||
ropts.head_msg = msg.buf;
|
||||
ropts.default_reflog_action = options.reflog_action;
|
||||
|
||||
@@ -309,6 +309,7 @@ int write_locked_index(struct index_state *, struct lock_file *lock, unsigned fl
|
||||
void discard_index(struct index_state *);
|
||||
void move_index_extensions(struct index_state *dst, struct index_state *src);
|
||||
int unmerged_index(const struct index_state *);
|
||||
int index_state_unmerged_to_stage0(struct index_state *istate);
|
||||
|
||||
/**
|
||||
* Returns 1 if istate differs from tree, 0 otherwise. If tree is NULL,
|
||||
|
||||
12
read-cache.c
12
read-cache.c
@@ -3428,13 +3428,15 @@ out:
|
||||
*/
|
||||
int repo_read_index_unmerged(struct repository *repo)
|
||||
{
|
||||
struct index_state *istate;
|
||||
int i;
|
||||
repo_read_index(repo);
|
||||
return index_state_unmerged_to_stage0(repo->index);
|
||||
}
|
||||
|
||||
int index_state_unmerged_to_stage0(struct index_state *istate)
|
||||
{
|
||||
int unmerged = 0;
|
||||
|
||||
repo_read_index(repo);
|
||||
istate = repo->index;
|
||||
for (i = 0; i < istate->cache_nr; i++) {
|
||||
for (unsigned int i = 0; i < istate->cache_nr; i++) {
|
||||
struct cache_entry *ce = istate->cache[i];
|
||||
struct cache_entry *new_ce;
|
||||
int len;
|
||||
|
||||
91
reset.c
91
reset.c
@@ -1,5 +1,3 @@
|
||||
#define USE_THE_REPOSITORY_VARIABLE
|
||||
|
||||
#include "git-compat-util.h"
|
||||
#include "cache-tree.h"
|
||||
#include "gettext.h"
|
||||
@@ -13,13 +11,14 @@
|
||||
#include "unpack-trees.h"
|
||||
#include "hook.h"
|
||||
|
||||
static int update_refs(const struct reset_head_opts *opts,
|
||||
static int update_refs(struct repository *repo,
|
||||
const struct reset_head_opts *opts,
|
||||
const struct object_id *oid,
|
||||
const struct object_id *head)
|
||||
{
|
||||
unsigned detach_head = opts->flags & RESET_HEAD_DETACH;
|
||||
unsigned run_hook = opts->flags & RESET_HEAD_RUN_POST_CHECKOUT_HOOK;
|
||||
unsigned update_orig_head = opts->flags & RESET_ORIG_HEAD;
|
||||
unsigned update_orig_head = opts->flags & RESET_HEAD_ORIG_HEAD;
|
||||
const struct object_id *orig_head = opts->orig_head;
|
||||
const char *switch_to_branch = opts->branch;
|
||||
const char *reflog_branch = opts->branch_msg;
|
||||
@@ -42,19 +41,19 @@ static int update_refs(const struct reset_head_opts *opts,
|
||||
prefix_len = msg.len;
|
||||
|
||||
if (update_orig_head) {
|
||||
if (!repo_get_oid(the_repository, "ORIG_HEAD", &oid_old_orig))
|
||||
if (!repo_get_oid(repo, "ORIG_HEAD", &oid_old_orig))
|
||||
old_orig = &oid_old_orig;
|
||||
if (head) {
|
||||
if (!reflog_orig_head) {
|
||||
strbuf_addstr(&msg, "updating ORIG_HEAD");
|
||||
reflog_orig_head = msg.buf;
|
||||
}
|
||||
refs_update_ref(get_main_ref_store(the_repository),
|
||||
refs_update_ref(get_main_ref_store(repo),
|
||||
reflog_orig_head, "ORIG_HEAD",
|
||||
orig_head ? orig_head : head,
|
||||
old_orig, 0, UPDATE_REFS_MSG_ON_ERR);
|
||||
} else if (old_orig)
|
||||
refs_delete_ref(get_main_ref_store(the_repository),
|
||||
refs_delete_ref(get_main_ref_store(repo),
|
||||
NULL, "ORIG_HEAD", old_orig, 0);
|
||||
}
|
||||
|
||||
@@ -64,23 +63,23 @@ static int update_refs(const struct reset_head_opts *opts,
|
||||
reflog_head = msg.buf;
|
||||
}
|
||||
if (!switch_to_branch)
|
||||
ret = refs_update_ref(get_main_ref_store(the_repository),
|
||||
ret = refs_update_ref(get_main_ref_store(repo),
|
||||
reflog_head, "HEAD", oid, head,
|
||||
detach_head ? REF_NO_DEREF : 0,
|
||||
UPDATE_REFS_MSG_ON_ERR);
|
||||
else {
|
||||
ret = refs_update_ref(get_main_ref_store(the_repository),
|
||||
ret = refs_update_ref(get_main_ref_store(repo),
|
||||
reflog_branch ? reflog_branch : reflog_head,
|
||||
switch_to_branch, oid, NULL, 0,
|
||||
UPDATE_REFS_MSG_ON_ERR);
|
||||
if (!ret)
|
||||
ret = refs_update_symref(get_main_ref_store(the_repository),
|
||||
ret = refs_update_symref(get_main_ref_store(repo),
|
||||
"HEAD", switch_to_branch,
|
||||
reflog_head);
|
||||
}
|
||||
if (!ret && run_hook)
|
||||
run_hooks_l(the_repository, "post-checkout",
|
||||
oid_to_hex(head ? head : null_oid(the_hash_algo)),
|
||||
run_hooks_l(repo, "post-checkout",
|
||||
oid_to_hex(head ? head : null_oid(repo->hash_algo)),
|
||||
oid_to_hex(oid), "1", NULL);
|
||||
strbuf_release(&msg);
|
||||
return ret;
|
||||
@@ -92,12 +91,16 @@ int reset_head(struct repository *r, const struct reset_head_opts *opts)
|
||||
const char *switch_to_branch = opts->branch;
|
||||
unsigned reset_hard = opts->flags & RESET_HEAD_HARD;
|
||||
unsigned refs_only = opts->flags & RESET_HEAD_REFS_ONLY;
|
||||
unsigned update_orig_head = opts->flags & RESET_ORIG_HEAD;
|
||||
unsigned update_orig_head = opts->flags & RESET_HEAD_ORIG_HEAD;
|
||||
unsigned dry_run = opts->flags & RESET_HEAD_DRY_RUN;
|
||||
unsigned skip_ref_updates = opts->flags & RESET_HEAD_SKIP_REF_UPDATES;
|
||||
struct object_id *head = NULL, head_oid;
|
||||
struct tree_desc desc[2] = { { NULL }, { NULL } };
|
||||
struct lock_file lock = LOCK_INIT;
|
||||
struct unpack_trees_options unpack_tree_opts = { 0 };
|
||||
struct tree *tree;
|
||||
struct index_state scratch_index = INDEX_STATE_INIT(r);
|
||||
struct index_state *istate;
|
||||
const char *action;
|
||||
int ret = 0, nr = 0;
|
||||
|
||||
@@ -110,12 +113,18 @@ int reset_head(struct repository *r, const struct reset_head_opts *opts)
|
||||
if (opts->branch_msg && !opts->branch)
|
||||
BUG("branch reflog message given without a branch");
|
||||
|
||||
if (!refs_only && repo_hold_locked_index(r, &lock, LOCK_REPORT_ON_ERROR) < 0) {
|
||||
if (skip_ref_updates && (opts->branch || refs_only))
|
||||
BUG("asked to perform ref updates and skip them at the same time");
|
||||
|
||||
if (!refs_only && !dry_run && repo_hold_locked_index(r, &lock, LOCK_REPORT_ON_ERROR) < 0) {
|
||||
ret = -1;
|
||||
goto leave_reset_head;
|
||||
}
|
||||
|
||||
if (!repo_get_oid(r, "HEAD", &head_oid)) {
|
||||
if (opts->oid_from) {
|
||||
oidcpy(&head_oid, opts->oid_from);
|
||||
head = &head_oid;
|
||||
} else if (!repo_get_oid(r, "HEAD", &head_oid)) {
|
||||
head = &head_oid;
|
||||
} else if (!oid || !reset_hard) {
|
||||
ret = error(_("could not determine HEAD revision"));
|
||||
@@ -125,26 +134,42 @@ int reset_head(struct repository *r, const struct reset_head_opts *opts)
|
||||
if (!oid)
|
||||
oid = &head_oid;
|
||||
|
||||
if (refs_only)
|
||||
return update_refs(opts, oid, head);
|
||||
if (refs_only) {
|
||||
if (!dry_run)
|
||||
return update_refs(r, opts, oid, head);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (dry_run) {
|
||||
if (read_index_from(&scratch_index, r->index_file, r->gitdir) < 0 ||
|
||||
index_state_unmerged_to_stage0(&scratch_index) < 0) {
|
||||
ret = error(_("could not read index"));
|
||||
goto leave_reset_head;
|
||||
}
|
||||
|
||||
istate = &scratch_index;
|
||||
} else {
|
||||
if (repo_read_index_unmerged(r) < 0) {
|
||||
ret = error(_("could not read index"));
|
||||
goto leave_reset_head;
|
||||
}
|
||||
istate = r->index;
|
||||
}
|
||||
|
||||
action = reset_hard ? "reset" : "checkout";
|
||||
setup_unpack_trees_porcelain(&unpack_tree_opts, action);
|
||||
unpack_tree_opts.head_idx = 1;
|
||||
unpack_tree_opts.src_index = r->index;
|
||||
unpack_tree_opts.dst_index = r->index;
|
||||
unpack_tree_opts.src_index = istate;
|
||||
unpack_tree_opts.dst_index = istate;
|
||||
unpack_tree_opts.fn = reset_hard ? oneway_merge : twoway_merge;
|
||||
unpack_tree_opts.update = 1;
|
||||
unpack_tree_opts.update = !dry_run;
|
||||
unpack_tree_opts.dry_run = dry_run;
|
||||
unpack_tree_opts.merge = 1;
|
||||
unpack_tree_opts.preserve_ignored = 0; /* FIXME: !overwrite_ignore */
|
||||
unpack_tree_opts.skip_cache_tree_update = 1;
|
||||
init_checkout_metadata(&unpack_tree_opts.meta, switch_to_branch, oid, NULL);
|
||||
if (reset_hard)
|
||||
if (reset_hard) {
|
||||
unpack_tree_opts.skip_cache_tree_update = 1;
|
||||
unpack_tree_opts.reset = UNPACK_RESET_PROTECT_UNTRACKED;
|
||||
|
||||
if (repo_read_index_unmerged(r) < 0) {
|
||||
ret = error(_("could not read index"));
|
||||
goto leave_reset_head;
|
||||
}
|
||||
|
||||
if (!reset_hard && !fill_tree_descriptor(r, &desc[nr++], &head_oid)) {
|
||||
@@ -163,25 +188,31 @@ int reset_head(struct repository *r, const struct reset_head_opts *opts)
|
||||
goto leave_reset_head;
|
||||
}
|
||||
|
||||
tree = repo_parse_tree_indirect(the_repository, oid);
|
||||
if (dry_run)
|
||||
goto leave_reset_head;
|
||||
|
||||
tree = repo_parse_tree_indirect(r, oid);
|
||||
if (!tree) {
|
||||
ret = error(_("unable to read tree (%s)"), oid_to_hex(oid));
|
||||
goto leave_reset_head;
|
||||
}
|
||||
|
||||
prime_cache_tree(r, r->index, tree);
|
||||
if (reset_hard)
|
||||
prime_cache_tree(r, r->index, tree);
|
||||
|
||||
if (write_locked_index(r->index, &lock, COMMIT_LOCK) < 0) {
|
||||
ret = error(_("could not write index"));
|
||||
goto leave_reset_head;
|
||||
}
|
||||
|
||||
if (oid != &head_oid || update_orig_head || switch_to_branch)
|
||||
ret = update_refs(opts, oid, head);
|
||||
if (!skip_ref_updates &&
|
||||
(oid != &head_oid || update_orig_head || switch_to_branch))
|
||||
ret = update_refs(r, opts, oid, head);
|
||||
|
||||
leave_reset_head:
|
||||
rollback_lock_file(&lock);
|
||||
clear_unpack_trees_porcelain(&unpack_tree_opts);
|
||||
release_index(&scratch_index);
|
||||
while (nr)
|
||||
free((void *)desc[--nr].buffer);
|
||||
return ret;
|
||||
|
||||
44
reset.h
44
reset.h
@@ -6,22 +6,42 @@
|
||||
|
||||
#define GIT_REFLOG_ACTION_ENVIRONMENT "GIT_REFLOG_ACTION"
|
||||
|
||||
/* Request a detached checkout */
|
||||
#define RESET_HEAD_DETACH (1<<0)
|
||||
/* Request a reset rather than a checkout */
|
||||
#define RESET_HEAD_HARD (1<<1)
|
||||
/* Run the post-checkout hook */
|
||||
#define RESET_HEAD_RUN_POST_CHECKOUT_HOOK (1<<2)
|
||||
/* Only update refs, do not touch the worktree */
|
||||
#define RESET_HEAD_REFS_ONLY (1<<3)
|
||||
/* Update ORIG_HEAD as well as HEAD */
|
||||
#define RESET_ORIG_HEAD (1<<4)
|
||||
enum reset_head_flags {
|
||||
/* Request a detached checkout */
|
||||
RESET_HEAD_DETACH = (1 << 0),
|
||||
|
||||
/* Request a reset rather than a checkout */
|
||||
RESET_HEAD_HARD = (1 << 1),
|
||||
|
||||
/* Run the post-checkout hook */
|
||||
RESET_HEAD_RUN_POST_CHECKOUT_HOOK = (1 << 2),
|
||||
|
||||
/* Only update refs, do not touch the worktree */
|
||||
RESET_HEAD_REFS_ONLY = (1 << 3),
|
||||
|
||||
/* Update ORIG_HEAD as well as HEAD */
|
||||
RESET_HEAD_ORIG_HEAD = (1 << 4),
|
||||
|
||||
/*
|
||||
* Perform a dry-run by performing the operation without updating
|
||||
* any user-visible state.
|
||||
*/
|
||||
RESET_HEAD_DRY_RUN = (1 << 5),
|
||||
|
||||
/* Skip updating any references, only update the worktree and index. */
|
||||
RESET_HEAD_SKIP_REF_UPDATES = (1 << 6),
|
||||
};
|
||||
|
||||
struct reset_head_opts {
|
||||
/*
|
||||
* The commit to checkout/reset to. Defaults to HEAD.
|
||||
*/
|
||||
const struct object_id *oid;
|
||||
/*
|
||||
* The commit to checkout/reset from when doing a two-way merge. This
|
||||
* is used as one of the sides to merge.
|
||||
*/
|
||||
const struct object_id *oid_from;
|
||||
/*
|
||||
* Optional value to set ORIG_HEAD. Defaults to HEAD.
|
||||
*/
|
||||
@@ -33,7 +53,7 @@ struct reset_head_opts {
|
||||
/*
|
||||
* Flags defined above.
|
||||
*/
|
||||
unsigned flags;
|
||||
enum reset_head_flags flags;
|
||||
/*
|
||||
* Optional reflog message for branch, defaults to head_msg.
|
||||
*/
|
||||
@@ -45,7 +65,7 @@ struct reset_head_opts {
|
||||
const char *head_msg;
|
||||
/*
|
||||
* Optional reflog message for ORIG_HEAD, if this omitted and flags
|
||||
* contains RESET_ORIG_HEAD then default_reflog_action must be given.
|
||||
* contains RESET_HEAD_ORIG_HEAD then default_reflog_action must be given.
|
||||
*/
|
||||
const char *orig_head_msg;
|
||||
/*
|
||||
|
||||
@@ -4885,7 +4885,7 @@ static int checkout_onto(struct repository *r, struct replay_opts *opts,
|
||||
struct reset_head_opts ropts = {
|
||||
.oid = onto,
|
||||
.orig_head = orig_head,
|
||||
.flags = RESET_HEAD_DETACH | RESET_ORIG_HEAD |
|
||||
.flags = RESET_HEAD_DETACH | RESET_HEAD_ORIG_HEAD |
|
||||
RESET_HEAD_RUN_POST_CHECKOUT_HOOK,
|
||||
.head_msg = reflog_message(opts, "start", "checkout %s",
|
||||
onto_name),
|
||||
|
||||
@@ -400,6 +400,7 @@ integration_tests = [
|
||||
't3451-history-reword.sh',
|
||||
't3452-history-split.sh',
|
||||
't3453-history-fixup.sh',
|
||||
't3454-history-drop.sh',
|
||||
't3500-cherry.sh',
|
||||
't3501-revert-cherry-pick.sh',
|
||||
't3502-cherry-pick-merge.sh',
|
||||
|
||||
513
t/t3454-history-drop.sh
Executable file
513
t/t3454-history-drop.sh
Executable file
@@ -0,0 +1,513 @@
|
||||
#!/bin/sh
|
||||
|
||||
test_description='tests for git-history drop subcommand'
|
||||
|
||||
. ./test-lib.sh
|
||||
. "$TEST_DIRECTORY/lib-log-graph.sh"
|
||||
|
||||
expect_graph () {
|
||||
cat >expect &&
|
||||
lib_test_cmp_graph --format=%s "$@"
|
||||
}
|
||||
|
||||
expect_log () {
|
||||
git log --format="%s" "$@" >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 drop 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 drop 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 drop does-not-exist 2>err &&
|
||||
test_grep "commit cannot be found: does-not-exist" err
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'errors with invalid --empty= value' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo &&
|
||||
test_commit -C repo initial &&
|
||||
test_commit -C repo second &&
|
||||
test_must_fail git -C repo history drop --empty=bogus HEAD 2>err &&
|
||||
test_grep "unrecognized.*--empty.*bogus" err
|
||||
'
|
||||
|
||||
test_expect_success 'drops a commit in the middle and replays descendants' '
|
||||
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 &&
|
||||
git history drop HEAD~ &&
|
||||
git symbolic-ref HEAD >actual &&
|
||||
test_cmp expect actual &&
|
||||
|
||||
expect_log <<-\EOF &&
|
||||
third
|
||||
first
|
||||
EOF
|
||||
|
||||
test_must_fail git show HEAD:second.t &&
|
||||
test_path_is_missing second.t &&
|
||||
|
||||
git reflog >reflog &&
|
||||
test_grep "drop: dropping HEAD~" reflog
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'drops the HEAD commit' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo &&
|
||||
(
|
||||
cd repo &&
|
||||
test_commit first &&
|
||||
test_commit second &&
|
||||
|
||||
git history drop HEAD &&
|
||||
|
||||
expect_log <<-\EOF
|
||||
first
|
||||
EOF
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'drops a commit on detached HEAD' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo &&
|
||||
(
|
||||
cd repo &&
|
||||
test_commit first &&
|
||||
test_commit second &&
|
||||
test_commit third &&
|
||||
git checkout --detach HEAD &&
|
||||
|
||||
git history drop HEAD~ &&
|
||||
|
||||
expect_log <<-\EOF
|
||||
third
|
||||
first
|
||||
EOF
|
||||
)
|
||||
'
|
||||
|
||||
# Note: in this case it would actually be fine to drop the root commit, as we
|
||||
# do have a descendant commit, and no reference points to the root commit
|
||||
# directly. So this is something that we may relax eventually.
|
||||
test_expect_success 'refuses to drop the root commit' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo &&
|
||||
(
|
||||
cd repo &&
|
||||
test_commit first &&
|
||||
test_commit second &&
|
||||
|
||||
test_must_fail git history drop HEAD~ 2>err &&
|
||||
test_grep "cannot drop root commit" err
|
||||
)
|
||||
'
|
||||
|
||||
# In contrast to the above case, we actually don't want to drop the root commit
|
||||
# here as that would cause us to end up with an empty commit graph.
|
||||
test_expect_success 'refuses to drop the root commit when branch becomes empty' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo &&
|
||||
(
|
||||
cd repo &&
|
||||
test_commit first &&
|
||||
|
||||
test_must_fail git history drop HEAD 2>err &&
|
||||
test_grep "cannot drop root commit" err
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'refuses to drop 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 &&
|
||||
|
||||
test_must_fail git history drop HEAD 2>err &&
|
||||
test_grep "cannot drop merge commit" err
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'refuses when descendants contain a merge commit' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo &&
|
||||
(
|
||||
cd repo &&
|
||||
test_commit base &&
|
||||
test_commit middle &&
|
||||
git branch branch &&
|
||||
test_commit ours &&
|
||||
git switch branch &&
|
||||
test_commit theirs &&
|
||||
git switch - &&
|
||||
git merge theirs &&
|
||||
|
||||
test_must_fail git history drop middle 2>err &&
|
||||
test_grep "replaying merge commits is not supported yet" err
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'works in a bare repository' '
|
||||
test_when_finished "rm -rf repo repo.git" &&
|
||||
|
||||
git init repo &&
|
||||
test_commit -C repo first &&
|
||||
test_commit -C repo second &&
|
||||
test_commit -C repo third &&
|
||||
|
||||
git clone --bare repo repo.git &&
|
||||
(
|
||||
cd repo.git &&
|
||||
|
||||
git history drop HEAD~ &&
|
||||
expect_log <<-\EOF
|
||||
third
|
||||
first
|
||||
EOF
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'updates branches on other lines of descent' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo &&
|
||||
(
|
||||
cd repo &&
|
||||
test_commit base &&
|
||||
test_commit target &&
|
||||
git branch theirs &&
|
||||
test_commit ours &&
|
||||
git switch theirs &&
|
||||
test_commit theirs &&
|
||||
|
||||
expect_graph --branches <<-\EOF &&
|
||||
* theirs
|
||||
| * ours
|
||||
|/
|
||||
* target
|
||||
* base
|
||||
EOF
|
||||
|
||||
git history drop target &&
|
||||
|
||||
expect_graph --branches <<-\EOF
|
||||
* ours
|
||||
| * theirs
|
||||
|/
|
||||
* base
|
||||
EOF
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'moves branch pointing at dropped commit to its parent' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo --initial-branch=main &&
|
||||
(
|
||||
cd repo &&
|
||||
test_commit first &&
|
||||
test_commit second &&
|
||||
git branch points-at-second &&
|
||||
test_commit third &&
|
||||
|
||||
git rev-parse first >expect &&
|
||||
git history drop second &&
|
||||
git rev-parse points-at-second >actual &&
|
||||
test_cmp expect actual &&
|
||||
|
||||
expect_log --format="%s %D" --branches <<-\EOF
|
||||
third HEAD -> main
|
||||
first tag: first, points-at-second
|
||||
EOF
|
||||
)
|
||||
'
|
||||
|
||||
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 middle &&
|
||||
test_commit ours &&
|
||||
git switch branch &&
|
||||
test_commit theirs &&
|
||||
|
||||
git refs list >refs-expect &&
|
||||
git history drop --dry-run main~ >updates &&
|
||||
git refs list >refs-actual &&
|
||||
test_cmp refs-expect refs-actual &&
|
||||
test_grep "update refs/heads/main" updates &&
|
||||
|
||||
git update-ref --stdin <updates &&
|
||||
expect_log main <<-\EOF
|
||||
ours
|
||||
base
|
||||
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 &&
|
||||
test_commit target &&
|
||||
git branch theirs &&
|
||||
test_commit ours &&
|
||||
git switch theirs &&
|
||||
test_commit theirs &&
|
||||
|
||||
# When told to update HEAD only, the command refuses to
|
||||
# rewrite commits that are not an ancestor of HEAD.
|
||||
test_must_fail git history drop --update-refs=head main 2>err &&
|
||||
test_grep "rewritten commit must be an ancestor of HEAD" err &&
|
||||
|
||||
expect_graph --branches <<-\EOF &&
|
||||
* theirs
|
||||
| * ours
|
||||
|/
|
||||
* target
|
||||
* base
|
||||
EOF
|
||||
|
||||
git switch main &&
|
||||
git history drop --update-refs=head target &&
|
||||
|
||||
expect_graph --branches <<-\EOF
|
||||
* ours
|
||||
| * theirs
|
||||
| * target
|
||||
|/
|
||||
* base
|
||||
EOF
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'conflict with replayed commit aborts cleanly' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo &&
|
||||
(
|
||||
cd repo &&
|
||||
test_commit base &&
|
||||
test_commit conflict-a file &&
|
||||
test_commit conflict-b file &&
|
||||
|
||||
git refs list >refs-expect &&
|
||||
test_must_fail git history drop HEAD~ 2>err &&
|
||||
test_grep "failed replaying descendants" err &&
|
||||
git refs list >refs-actual &&
|
||||
test_cmp refs-expect refs-actual
|
||||
)
|
||||
'
|
||||
|
||||
# Build a history where a descendant of the drop target reverts the change
|
||||
# introduced by the drop target. After dropping, the descendant's diff applies
|
||||
# against a tree that already lacks the change, so it becomes empty.
|
||||
setup_empty_descendant_repo () {
|
||||
git init "$1" &&
|
||||
(
|
||||
cd "$1" &&
|
||||
echo C1 >file &&
|
||||
git add file &&
|
||||
git commit -m "base" &&
|
||||
git tag base &&
|
||||
echo C2 >file &&
|
||||
git add file &&
|
||||
git commit -m "drop-me" &&
|
||||
git tag drop-me &&
|
||||
test_commit middle &&
|
||||
echo C1 >file &&
|
||||
git add file &&
|
||||
git commit -m "revert-drop-me" &&
|
||||
git tag revert-drop-me
|
||||
)
|
||||
}
|
||||
|
||||
test_expect_success '--empty=drop drops descendants that become empty' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
setup_empty_descendant_repo repo &&
|
||||
(
|
||||
cd repo &&
|
||||
|
||||
git history drop --empty=drop drop-me &&
|
||||
|
||||
expect_log <<-\EOF
|
||||
middle
|
||||
base
|
||||
EOF
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success '--empty=keep keeps descendants that become empty' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
setup_empty_descendant_repo repo &&
|
||||
(
|
||||
cd repo &&
|
||||
|
||||
git history drop --empty=keep drop-me &&
|
||||
|
||||
expect_log <<-\EOF &&
|
||||
revert-drop-me
|
||||
middle
|
||||
base
|
||||
EOF
|
||||
git diff HEAD~ HEAD >diff &&
|
||||
test_must_be_empty diff
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success '--empty=abort errors out when a descendant becomes empty' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
setup_empty_descendant_repo repo &&
|
||||
(
|
||||
cd repo &&
|
||||
|
||||
test_must_fail git history drop --empty=abort drop-me 2>err &&
|
||||
test_grep "became empty after replay" err
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'updates index and worktree when HEAD moves' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo &&
|
||||
(
|
||||
cd repo &&
|
||||
test_commit first &&
|
||||
test_commit second &&
|
||||
test_commit third &&
|
||||
|
||||
git history drop second &&
|
||||
|
||||
# Worktree should no longer contain second.t.
|
||||
test_path_is_missing second.t &&
|
||||
test_path_is_file first.t &&
|
||||
test_path_is_file third.t &&
|
||||
|
||||
# Index and worktree should both match the new HEAD.
|
||||
git status --porcelain --untracked-files=no >status &&
|
||||
test_must_be_empty status
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'updates worktree when dropping HEAD itself' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo &&
|
||||
(
|
||||
cd repo &&
|
||||
test_commit first &&
|
||||
test_commit second &&
|
||||
|
||||
git history drop HEAD &&
|
||||
|
||||
test_path_is_missing second.t &&
|
||||
test_path_is_file first.t &&
|
||||
|
||||
git status --porcelain --untracked-files=no >status &&
|
||||
test_must_be_empty status
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'preserves unrelated unstaged modifications' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo &&
|
||||
(
|
||||
cd repo &&
|
||||
test_commit first &&
|
||||
echo first-content >unrelated.txt &&
|
||||
git add unrelated.txt &&
|
||||
git commit -m "add unrelated" &&
|
||||
test_commit second &&
|
||||
test_commit third &&
|
||||
|
||||
echo locally-modified >unrelated.txt &&
|
||||
|
||||
git diff >diff-expect &&
|
||||
git history drop second &&
|
||||
git diff >diff-actual &&
|
||||
test_cmp diff-expect diff-actual &&
|
||||
test_path_is_missing second.t
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'preserves unrelated staged changes' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo &&
|
||||
(
|
||||
cd repo &&
|
||||
test_commit first &&
|
||||
echo first-content >unrelated.txt &&
|
||||
git add unrelated.txt &&
|
||||
git commit -m "add unrelated" &&
|
||||
test_commit second &&
|
||||
test_commit third &&
|
||||
|
||||
echo staged-change >unrelated.txt &&
|
||||
git add unrelated.txt &&
|
||||
|
||||
git diff --cached >diff-expect &&
|
||||
git history drop second &&
|
||||
git diff --cached >diff-actual &&
|
||||
test_cmp diff-expect diff-actual &&
|
||||
test_path_is_missing second.t
|
||||
)
|
||||
'
|
||||
|
||||
test_expect_success 'aborts when local modifications would be overwritten' '
|
||||
test_when_finished "rm -rf repo" &&
|
||||
git init repo &&
|
||||
(
|
||||
cd repo &&
|
||||
test_commit base &&
|
||||
test_commit conflict &&
|
||||
|
||||
echo local-edit >conflict.t &&
|
||||
git diff >diff-expect &&
|
||||
test_must_fail git history drop HEAD 2>err &&
|
||||
test_grep "would overwrite local changes" err &&
|
||||
git diff >diff-actual &&
|
||||
test_cmp diff-expect diff-actual
|
||||
)
|
||||
'
|
||||
|
||||
test_done
|
||||
Reference in New Issue
Block a user