Thread (114 messages) 114 messages, 6 authors, 1d ago
WARM1d
Revisions (8)
  1. v1 [diff vs current]
  2. v2 [diff vs current]
  3. v3 current
  4. v4 [diff vs current]
  5. v5 [diff vs current]
  6. v6 [diff vs current]
  7. v7 [diff vs current]
  8. v8 [diff vs current]

[PATCH v3 0/9] builtin/history: introduce "drop" subcommand

From: Patrick Steinhardt <hidden>
Date: 2026-06-08 10:23:35

Hi,

this small patch series introduces the new "drop" subcommand for
git-history(1). As a reader might guess, the command does exactly that:
given a commit, it will drop that commit from the commit history and
replay descendant branches on top of it.

Changes in v3:
  - Fix commit message typos.
  - Make `update_orig_head` and `skip_ref_updates` mutually exclusive.
  - Use fancy revisions to specify the commit to drop in the example
    section.
  - Detect conflicting changes in the index/working tree in dry-run
    mode.
  - Consistently use a subshell.
  - Rename `RESET_HEAD_ORIG_HEAD` to `RESET_HEAD_UPDATE_ORIG_HEAD`.
  - 
  - Link to v2: https://patch.msgid.link/20260603-b4-pks-history-drop-v2-0-742cb5b5176d@pks.im

Changes in v2:
  - Reworked `update_worktree()` to use `reset_head()`, which required a
    bunch of changes to `reset_head()`.
  - Consistently mention the commit that cannot be dropped as part of
    error messages.
  - Adapt error message to not use backticks anymore.
  - Drop redundant "--graph" flag in a test helper.
  - Link to v1: https://patch.msgid.link/20260601-b4-pks-history-drop-v1-0-643e32340d55@pks.im

Thanks!

Patrick

---
Patrick Steinhardt (9):
      read-cache: split out function to drop unmerged entries to stage 0
      reset: drop `USE_THE_REPOSITORY_VARIABLE`
      reset: modernize flags passed to `reset_head()`
      reset: introduce dry-run mode
      reset: introduce ability to skip reference updates
      reset: allow the caller to specify the current HEAD object
      reset: stop assuming that the caller passes in a clean index
      builtin/history: split handling of ref updates into two phases
      builtin/history: implement "drop" subcommand

 Documentation/git-history.adoc |  38 ++-
 builtin/history.c              | 289 +++++++++++++++++++---
 builtin/rebase.c               |   2 +-
 read-cache-ll.h                |   1 +
 read-cache.c                   |  12 +-
 reset.c                        |  91 ++++---
 reset.h                        |  44 +++-
 sequencer.c                    |   2 +-
 t/meson.build                  |   1 +
 t/t3454-history-drop.sh        | 537 +++++++++++++++++++++++++++++++++++++++++
 10 files changed, 929 insertions(+), 88 deletions(-)

Range-diff versus v2:

 1:  a93e804936 =  1:  41a723b3d0 read-cache: split out function to drop unmerged entries to stage 0
 2:  d8f39e7dc4 =  2:  db850730ef reset: drop `USE_THE_REPOSITORY_VARIABLE`
 3:  fdec5a57b4 !  3:  bd18736141 reset: modernize flags passed to `reset_head()`
    @@ builtin/rebase.c: int cmd_rebase(int argc,
      	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 |
    ++	ropts.flags = RESET_HEAD_DETACH | RESET_HEAD_UPDATE_ORIG_HEAD |
      			RESET_HEAD_RUN_POST_CHECKOUT_HOOK;
      	ropts.head_msg = msg.buf;
      	ropts.default_reflog_action = options.reflog_action;
    @@ reset.c: static int update_refs(struct repository *repo,
      	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;
    ++	unsigned update_orig_head = opts->flags & RESET_HEAD_UPDATE_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;
    @@ reset.c: int reset_head(struct repository *r, const struct reset_head_opts *opts
      	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 update_orig_head = opts->flags & RESET_HEAD_UPDATE_ORIG_HEAD;
      	struct object_id *head = NULL, head_oid;
      	struct tree_desc desc[2] = { { NULL }, { NULL } };
      	struct lock_file lock = LOCK_INIT;
    @@ reset.h
     +	RESET_HEAD_REFS_ONLY = (1 << 3),
     +
     +	/* Update ORIG_HEAD as well as HEAD */
    -+	RESET_HEAD_ORIG_HEAD = (1 << 4),
    ++	RESET_HEAD_UPDATE_ORIG_HEAD = (1 << 4),
     +};
      
      struct reset_head_opts {
    @@ reset.h: struct reset_head_opts {
      	/*
      	 * 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.
    ++	 * contains RESET_HEAD_UPDATE_ORIG_HEAD then default_reflog_action must be given.
      	 */
      	const char *orig_head_msg;
      	/*
    @@ sequencer.c: static int checkout_onto(struct repository *r, struct replay_opts *
      		.oid = onto,
      		.orig_head = orig_head,
     -		.flags = RESET_HEAD_DETACH | RESET_ORIG_HEAD |
    -+		.flags = RESET_HEAD_DETACH | RESET_HEAD_ORIG_HEAD |
    ++		.flags = RESET_HEAD_DETACH | RESET_HEAD_UPDATE_ORIG_HEAD |
      				RESET_HEAD_RUN_POST_CHECKOUT_HOOK,
      		.head_msg = reflog_message(opts, "start", "checkout %s",
      					   onto_name),
 4:  3af7c9a4fd !  4:  8c79e56076 reset: introduce dry-run mode
    @@ Metadata
      ## Commit message ##
         reset: introduce dry-run mode
     
    -    In a subsequent commit we'll add add another caller to `reset_head()`
    -    that wants to perform a dry-run check of whether it would be possible to
    -    udpate the index and working tree when moving to a new commit. Introduce
    +    In a subsequent commit we'll add another caller to `reset_head()` that
    +    wants to perform a dry-run check of whether it would be possible to
    +    update the index and working tree when moving to a new commit. Introduce
         a new flag that lets the caller perform this operation.
     
         Signed-off-by: Patrick Steinhardt [off-list ref]
    @@ reset.c
     @@ reset.c: int reset_head(struct repository *r, const struct reset_head_opts *opts)
      	unsigned reset_hard = opts->flags & RESET_HEAD_HARD;
      	unsigned refs_only = opts->flags & RESET_HEAD_REFS_ONLY;
    - 	unsigned update_orig_head = opts->flags & RESET_HEAD_ORIG_HEAD;
    + 	unsigned update_orig_head = opts->flags & RESET_HEAD_UPDATE_ORIG_HEAD;
     +	unsigned dry_run = opts->flags & RESET_HEAD_DRY_RUN;
      	struct object_id *head = NULL, head_oid;
      	struct tree_desc desc[2] = { { NULL }, { NULL } };
    @@ reset.h
     @@ reset.h: enum reset_head_flags {
      
      	/* Update ORIG_HEAD as well as HEAD */
    - 	RESET_HEAD_ORIG_HEAD = (1 << 4),
    + 	RESET_HEAD_UPDATE_ORIG_HEAD = (1 << 4),
     +
     +	/*
     +	 * Perform a dry-run by performing the operation without updating
 5:  31d1ff1d4c !  5:  c112fbbd14 reset: introduce ability to skip reference updates
    @@ Commit message
      ## reset.c ##
     @@ reset.c: int reset_head(struct repository *r, const struct reset_head_opts *opts)
      	unsigned refs_only = opts->flags & RESET_HEAD_REFS_ONLY;
    - 	unsigned update_orig_head = opts->flags & RESET_HEAD_ORIG_HEAD;
    + 	unsigned update_orig_head = opts->flags & RESET_HEAD_UPDATE_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;
    @@ reset.c: 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 (skip_ref_updates && (opts->branch || refs_only))
    ++	if (skip_ref_updates && (opts->branch || refs_only || update_orig_head))
     +		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) {
 6:  21b2d4d281 =  6:  9550257dd1 reset: allow the caller to specify the current HEAD object
 7:  7c032ca1e3 =  7:  8a3d517020 reset: stop assuming that the caller passes in a clean index
 8:  fcd4479178 =  8:  a09be2ffc0 builtin/history: split handling of ref updates into two phases
 9:  6b1c17a8df !  9:  682b11af93 builtin/history: implement "drop" subcommand
    @@ Documentation/git-history.adoc: The staged addition of `unrelated.txt` has been
     +def5678 second
     +ghi9012 first
     +
    -+$ git history drop def5678
    ++$ git history drop 'main^{/second}'
     +
     +$ git log --oneline
     +jkl3456 (HEAD -> main) third
    @@ builtin/history.c: static int cmd_history_split(int argc,
     +	 * inconsistent repository state. So we first perform a dry-run merge
     +	 * here before updating refs.
     +	 */
    -+	if (!dry_run && !is_bare_repository()) {
    ++	if (!is_bare_repository()) {
     +		ret = find_head_tree_change(repo, &result, &old_head,
     +					    &new_head, &head_moves);
     +		if (ret < 0)
    @@ builtin/history.c: static int cmd_history_split(int argc,
     +		goto out;
     +	}
     +
    -+	if (head_moves && update_worktree(repo, old_head, new_head, false) < 0) {
    ++	if (!dry_run && 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;
    @@ t/t3454-history-drop.sh (new)
     +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
    ++	(
    ++		cd repo &&
    ++		test_commit initial &&
    ++		test_commit second &&
    ++		test_must_fail git 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' '
    @@ t/t3454-history-drop.sh (new)
     +	)
     +'
     +
    ++test_expect_success '--dry-run detects conflicts with modified working tree' '
    ++	test_when_finished "rm -rf repo" &&
    ++	git init repo --initial-branch=main &&
    ++	(
    ++		cd repo &&
    ++		test_commit first &&
    ++		test_commit second modify-me &&
    ++		echo modified >modify-me &&
    ++
    ++		git refs list >refs-expect &&
    ++		git diff >diff-expect &&
    ++		test_must_fail git history drop --dry-run HEAD 2>err &&
    ++		test_grep "dropping this commit would overwrite local changes" err &&
    ++		git diff >diff-actual &&
    ++		git refs list >refs-actual &&
    ++
    ++		test_cmp diff-expect diff-actual &&
    ++		test_cmp refs-expect refs-actual
    ++	)
    ++'
    ++
     +test_expect_success '--update-refs=head updates only HEAD' '
     +	test_when_finished "rm -rf repo" &&
     +	git init repo --initial-branch=main &&

---
base-commit: 1666c1265231b0bc5f613fbbf3f0a9896cdef76e
change-id: 20260601-b4-pks-history-drop-28f6c6399e7b
Keyboard shortcuts
hback out one level
jnext message in thread
kprevious message in thread
ldrill in
Escclose help / fold thread tree
?toggle this help