# History rewrite

> Plan in-memory history edits, then apply ref and worktree updates atomically.

The [`history`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/index.html) module supports stateless history editing: build a [`RewritePlan`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/struct.RewritePlan.html) in memory, inspect or dry-run it, then [`apply_rewrite`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/fn.apply_rewrite.html) to move refs and align the worktree.

## Model

1. **Plan** — [`plan_rewrite`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/fn.plan_rewrite.html) walks affected commits, writes replacement objects into the ODB, and records ref movements in a [`RewritePlan`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/struct.RewritePlan.html). Refs, the index, and the worktree are untouched during planning.
2. **Apply** — [`apply_rewrite`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/fn.apply_rewrite.html) writes a crash journal under `.git/grit/rewrite-apply/` when a worktree checkout is required, runs a compare-and-swap ref batch, then (in non-bare repos) checks out from the old `HEAD` tree to the new one while preserving unrelated staged and unstaged changes. On success the journal is removed. If the process dies after refs move but before checkout finishes, [`recover_interrupted_rewrite_apply`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/fn.recover_interrupted_rewrite_apply.html) (also run from [`status`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/status/fn.status.html) and before [`create_commit`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/commit/fn.create_commit.html)) finishes the checkout when refs already match the plan, or rolls refs and the index/worktree back from the journal. Recovery holds `index.lock` for its whole run: when grit recorded a pid sidecar for the lock (the default; see [`grit_lockfile_pid_enabled`](https://docs.rs/grit-lib/latest/grit_lib/lockfile/fn.grit_lockfile_pid_enabled.html)) and that process has exited, [`LockHolder::remove_if_stale`](https://docs.rs/grit-lib/latest/grit_lib/lockfile/struct.LockHolder.html#method.remove_if_stale) removes the lock first; any other existing lock returns [`Error::LockHeld`](https://docs.rs/grit-lib/latest/grit_lib/error/enum.Error.html#variant.LockHeld) without touching refs. To finish, it first restores the journal's index and the transition paths it captured (a kill mid-checkout can leave files half written), then checks out the new tree again, so the partial checkout is not mistaken for local edits. Only if that checkout still fails are refs rolled back, and the call returns [`RewriteApplyRecovery::RolledBack`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/enum.RewriteApplyRecovery.html#variant.RolledBack) rather than the checkout error. A journal directory without its `meta` file (killed while the snapshots were written, before any ref moved) is removed. [`status`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/status/fn.status.html) reports what happened in [`StatusModel::interrupted_rewrite`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/status/struct.StatusModel.html#structfield.interrupted_rewrite), with [`InterruptedRewrite::Pending`](https://docs.rs/grit-lib/latest/grit_lib/porcelain/status/enum.InterruptedRewrite.html#variant.Pending) when another process holds the index lock. While the journal remains, [`HistoryError::RewriteApplyIncomplete`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/enum.HistoryError.html#variant.RewriteApplyIncomplete) blocks new commits; `grit rebase --abort` calls [`abort_interrupted_rewrite_apply`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/fn.abort_interrupted_rewrite_apply.html) to roll back explicitly.

[`CommitEdit`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/enum.CommitEdit.html) describes per-commit rewrites, drops, and insert-before commits. [`RewriteOptions`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/struct.RewriteOptions.html) controls scope, empty-commit policy, the committer [`Signature`](https://docs.rs/grit-lib/latest/grit_lib/ident/struct.Signature.html) used for new objects (usually `repo.identity(IdentRole::Committer, now)`), and whether replacement commits are signed ([`RewriteOptions::sign`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/struct.RewriteOptions.html#structfield.sign) — set `false` for dry-run planning so signers are not invoked).

When a rewrite replays descendant **merge** commits, [`plan_rewrite`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/fn.plan_rewrite.html) remaps every parent and recomputes the merge tree (first-parent transplant when side branches are unchanged, otherwise a fresh merge of the mapped parents). Dropping or reordering a merge commit itself is still rejected ([`HistoryError::MergeCommitInReplay`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/enum.HistoryError.html#variant.MergeCommitInReplay)).

For a single message change, [`reword`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/fn.reword.html) builds the [`CommitEdit`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/enum.CommitEdit.html) and runs [`plan_rewrite`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/fn.plan_rewrite.html) after [`normalize_rewrite_message`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/fn.normalize_rewrite_message.html) (stripspace, trailing newline, reject empty).

To reorder one commit on the linear first-parent chain of `HEAD`, [`move_commit`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/fn.move_commit.html) takes a [`Placement`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/enum.Placement.html) (`Before` / `After` another commit), replays the affected segment in the new order, and returns a [`RewritePlan`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/struct.RewritePlan.html). Commits must sit on the same first-parent chain with no merge commits in the segment; conflicts and no-op moves surface as [`HistoryError`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/enum.HistoryError.html) variants.

For rebasing a branch tip onto a new base with fork-point detection and bounded cherry patch-id lookup, see [Rebase planning](https://grit-scm.com/docs/library/history/rebase.md/index.md) ([`plan_rebase`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/fn.plan_rebase.html) / [`RebaseRequest`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/struct.RebaseRequest.html)).

To assign staged hunks automatically, [`absorb`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/fn.absorb.html) blame-maps each hunk to a candidate commit between [`AbsorbOptions::base`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/struct.AbsorbOptions.html#structfield.base) and `HEAD`, verifies commutation with later commits, builds one combined [`RewritePlan`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/struct.RewritePlan.html), and returns [`AbsorbPlan`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/struct.AbsorbPlan.html) with [`HunkAssignment`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/struct.HunkAssignment.html) and [`UnassignedHunk`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/struct.UnassignedHunk.html) detail. When `--base` is omitted, [`AbsorbPlan::excluded_refs`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/struct.AbsorbPlan.html#structfield.excluded_refs) lists other local branch tips that bound the stack (another ref at the checked-out tip is not treated as a boundary). [`UnassignedReason::HiddenByRef`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/enum.UnassignedReason.html#variant.HiddenByRef) with [`UnassignedHunk::detail`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/struct.UnassignedHunk.html#structfield.detail) explains when a hunk’s introducing commit sits outside the stack because of one of those tips. Call [`apply_absorb_rewrite`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/fn.apply_absorb_rewrite.html) so only assigned hunks are removed from the index.

## Apply options and result

[`ApplyOptions`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/struct.ApplyOptions.html) supplies the reflog identity and message written for every ref update in the batch.

[`AppliedRewrite`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/struct.AppliedRewrite.html) returns the ref updates that were committed and the commit OID `HEAD` resolves to afterward.

## Undo

When [`ApplyOptions::record_operation`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/struct.ApplyOptions.html#structfield.record_operation) is set, the apply records its ref plan in `.git/grit/history-last`, and [`undo_last_rewrite`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/fn.undo_last_rewrite.html) inverts it while every recorded ref is still at its post-rewrite tip. Undo writes the inverse as the new record, so calling it again redoes the rewrite.

[`apply_fixup_rewrite`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/fn.apply_fixup_rewrite.html), [`apply_fixup_hunk_selection`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/fn.apply_fixup_hunk_selection.html), and [`apply_absorb_rewrite`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/fn.apply_absorb_rewrite.html) consume edits from the index (or selected unstaged hunks). A recorded apply also saves, in `.git/grit/history-last-work`, each consumed path's index entry and work tree file from before the command, plus a fingerprint of the same paths after it. Undo checks that those paths still match the fingerprint (otherwise [`LocalChangesWouldBeOverwritten`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/enum.HistoryError.html#variant.LocalChangesWouldBeOverwritten)), runs the inverse checkout, then puts the saved entries and files back. A restored entry takes the stat data of the file just written when that file cleans (through `core.autocrlf`, `eol`, `ident` and clean filters) to the restored blob, so neither Git nor grit sees a phantom unstaged change; when the file holds a real unstaged edit, the entry's stat cache stays unset so both rehash it. [`UndoOutcome::restored_paths`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/struct.UndoOutcome.html#structfield.restored_paths) lists them. A record written without saved state still moves refs, and [`UndoOutcome::unrestored_edits`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/struct.UndoOutcome.html#structfield.unrestored_edits) names the commit that holds the consumed edits.

## Inspecting a plan

[`RewritePlan::ref_update_lines`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/struct.RewritePlan.html#method.ref_update_lines) returns `update <ref> <new> <old>` lines suitable for `git update-ref --stdin`, so callers can preview ref motion without applying.

[`RewritePlan::replacement`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/struct.RewritePlan.html#method.replacement) maps one original commit to its rewritten OID (`None` when dropped), and [`RewritePlan::head_change`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/struct.RewritePlan.html#method.head_change) reports `HEAD`'s commit before and after the rewrite, or `None` when `HEAD` does not move. `grit history` builds its `--json` `head` field from it.

## Change ids

History planning preserves [`change-id`](https://docs.rs/grit-lib/latest/grit_lib/change_id/fn.change_id.html) and legacy `gitbutler-change-id` headers on rewritten commits (signatures are stripped). When [`ChangeIdPolicy`](https://docs.rs/grit-lib/latest/grit_lib/change_id/struct.ChangeIdPolicy.html) is enabled and the source commit has no stored id, the planner writes the Jujutsu-derived id for the original commit oid into the replacement object. Commits inserted during a rewrite (for example the parent created by [`split`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/fn.split.html)) receive a new random id via [`ChangeIdPolicy::id_for_new_commit`](https://docs.rs/grit-lib/latest/grit_lib/change_id/struct.ChangeIdPolicy.html#method.id_for_new_commit). With `grit.changeIds=false`, existing headers are still copied but no derived or random ids are added. See [Change ids](https://grit-scm.com/docs/library/history/change-ids.md/index.md) for header format and config.

## Errors

[`HistoryError`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/enum.HistoryError.html) covers planning failures (conflicts, out-of-scope commits, branch checked out elsewhere) and apply failures:

- [`LocalChangesWouldBeOverwritten`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/enum.HistoryError.html#variant.LocalChangesWouldBeOverwritten) — staged, unstaged, or untracked paths would be clobbered by the planned tree checkout.
- [`RefCasMismatch`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/enum.HistoryError.html#variant.RefCasMismatch) — a ref tip changed after planning; nothing is written.
- [`HeadMovedSincePlan`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/enum.HistoryError.html#variant.HeadMovedSincePlan) — `HEAD` no longer points at the ref or commit recorded when the plan was built; nothing is written.
- [`RefApply`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/enum.HistoryError.html#variant.RefApply) / [`WorktreeApply`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/enum.HistoryError.html#variant.WorktreeApply) — ref batch or post-ref checkout failed (apply rolls back ref updates when checkout fails).
- [`RewriteApplyIncomplete`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/enum.HistoryError.html#variant.RewriteApplyIncomplete) — refs moved but the worktree checkout did not finish; run recovery or `grit rebase --abort` before committing.
- [`ApplyRollbackIncomplete`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/enum.HistoryError.html#variant.ApplyRollbackIncomplete) — checkout failed after refs moved and ref rollback or worktree restore also failed.
- [`EmptyMessage`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/enum.HistoryError.html#variant.EmptyMessage) — reword message cleanup left an empty body.
- [`Signing`](https://docs.rs/grit-lib/latest/grit_lib/revwalk/history/enum.HistoryError.html#variant.Signing) — `commit.gpgsign` was enabled but signing a replacement commit failed; refs are not updated.

## Example

Reword one commit on the current branch, plan, and apply:

```rust
//! Plan and apply a single-commit message rewrite (library history API demo).
//!
//! Source for the library guide "History rewrite" page.

use grit_lib::config::environment::RepositoryOptions;
use grit_lib::objects::ObjectId;
use grit_lib::repo::Repository;
use grit_lib::revwalk::history::{
    apply_rewrite, reword, ApplyOptions, EmptyPolicy, RefScope, RewriteOptions,
};
use grit_lib::revwalk::rev_parse::resolve_revision_as_commit;
use std::path::{Path, PathBuf};

fn open_repo(root: &Path) -> Result<Repository, grit_lib::error::Error> {
    Repository::discover(Some(root), &RepositoryOptions::from_process())
}

fn main() -> Result<(), grit_lib::error::Error> {
    let mut args = std::env::args().skip(1);
    let root: PathBuf = args
        .next()
        .map(PathBuf::from)
        .or_else(|| std::env::current_dir().ok())
        .ok_or_else(|| grit_lib::error::Error::Message("missing repository path".into()))?;
    let spec = args
        .next()
        .ok_or_else(|| grit_lib::error::Error::Message("missing commit spec".into()))?;
    let message = args
        .next()
        .ok_or_else(|| grit_lib::error::Error::Message("missing new message".into()))?;

    let repo = open_repo(&root)?;
    let commit: ObjectId = resolve_revision_as_commit(&repo, &spec)?;

    let plan = reword(
        &repo,
        commit,
        message,
        &RewriteOptions {
            scope: RefScope::Head,
            empty: EmptyPolicy::Drop,
            committer: "History Guide <guide@grit.test> 1 +0000".parse()?,
            preserve_committer: false,
            sign: true,
        },
    )
    .map_err(grit_lib::error::Error::History)?;

    for line in plan.ref_update_lines() {
        println!("dry-run: {line}");
    }

    let applied = apply_rewrite(
        &repo,
        &plan,
        &ApplyOptions {
            identity: grit_lib::ident::parse_signature("History Guide <guide@grit.test> 1 +0000")
                .unwrap_or_else(|| panic!("guide identity must parse as a signature")),
            reflog_message: "history reword".into(),
            record_operation: None,
            hooks: grit_lib::PostOperationHooks::Run,
        },
        &mut grit_lib::progress::NullProgress,
        &grit_lib::progress::NeverCancel,
    )
    .map_err(grit_lib::error::Error::History)?;

    println!("head={}", applied.head.to_hex());
    Ok(())
}
```

```bash
cargo run --bin history_reword /path/to/repo <commit-ish> "New message"
```
