]> git.ipfire.org Git - thirdparty/git.git/commitdiff
Merge branch 'ps/history-drop'
authorJunio C Hamano <gitster@pobox.com>
Wed, 15 Jul 2026 20:24:18 +0000 (13:24 -0700)
committerJunio C Hamano <gitster@pobox.com>
Wed, 15 Jul 2026 20:24:19 +0000 (13:24 -0700)
The experimental 'git history' command has been taught a new 'drop'
subcommand to remove a commit, with its descendants replayed onto its
parent.

* ps/history-drop:
  builtin/history: implement "drop" subcommand
  builtin/history: split handling of ref updates into two phases
  replay: expose `replay_result_queue_update()`
  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 updating HEAD
  reset: introduce dry-run mode
  reset: modernize flags passed to `reset_working_tree()`
  reset: rename `reset_head()`
  reset: drop `USE_THE_REPOSITORY_VARIABLE`
  read-cache: split out function to drop unmerged entries to stage 0

1  2 
builtin/history.c
read-cache.c
replay.c
replay.h
sequencer.c
t/meson.build

index c8d249d2786b9eae45095a17a3396b99e21e56d6,dd11ce2fa81256c93d666e552debe51e88baa252..d28c1f08bb66ea3db09a5c7e7aee904cf8fd7e8c
        return ret;
  }
  
 -      if (!is_bare_repository()) {
+ static int update_worktree(struct repository *repo,
+                          const struct commit *old_head,
+                          const struct commit *new_head,
+                          bool dry_run)
+ {
+       struct reset_working_tree_options opts = {
+               .oid_from = &old_head->object.oid,
+               .oid = &new_head->object.oid,
+       };
+       if (dry_run)
+               opts.flags |= RESET_WORKING_TREE_DRY_RUN;
+       return reset_working_tree(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 | RESOLVE_REF_READING,
+                                             NULL, &head_flags);
+       if (!head_target)
+               return error(_("cannot look up 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 (!is_bare_repository(repo)) {
+               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 (!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;
+       }
+       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,
diff --cc read-cache.c
Simple merge
diff --cc replay.c
Simple merge
diff --cc replay.h
Simple merge
diff --cc sequencer.c
Simple merge
diff --cc t/meson.build
Simple merge