GitoxideLabs/gitoxide · error

BUG: packed refs cannot contain symbolic refs, catch that…

Error message

BUG: packed refs cannot contain symbolic refs, catch that in prepare(…)

What it means

An `unreachable!()` guard in the packed-refs edit writer: packed-refs files cannot represent symbolic references, so when the transaction applies an `Change::Update` whose new target is `Target::Symbolic` against the packed buffer, the code panics with a message directing you to `prepare(…)`. The transaction preparation step is responsible for rejecting or splitting such updates before they reach this code path.

Solutions

  1. Ensure all ref updates go through `gix_ref::transaction::Transaction` and its `prepare(…)` step, which filters symbolic updates away from packed storage.
  2. If hit, file a bug with the reproducing transaction changes; symbolic targets must be written as loose refs instead.
  3. As a workaround, split the update: delete/log the packed ref and write the symbolic ref as a loose ref.
Defensive patterns

Strategy: validation

Validate before calling

// Before committing a transaction, ensure no symbolic-ref updates target packed storage:
let has_symbolic_update = changes.iter().any(|c| matches!(c, gix_ref::transaction::Change::Update { new: gix_ref::Target::Symbolic(_), .. }));
if has_symbolic_update { /* route that ref to loose storage or handle before prepare/commit */ }

Try / catch

match transaction.commit(commit_resource) {
    Ok(edit) => { /* apply */ }
    Err(err) if err.to_string().contains("packed refs cannot contain symbolic refs") => {
        // library bug: split the symbolic update into a loose-ref write and retry
    }
    Err(err) => return Err(err.into()),
}

Prevention

When it happens

Trigger: Calling `Transaction::commit` (which invokes `write_edit`) with a prepared transaction that still contains an `Update` change targeting a symbolic ref destined for the packed-refs store. This indicates `prepare(…)` failed to catch it — a gitoxide bug, not a user error.

Common situations: Only reachable by users through a bug in gix-ref's transaction preparation, or when writing custom transaction plumbing that bypasses `prepare`.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of GitoxideLabs/gitoxide@e73179060b (2026-09-08). Data as JSON: /api/errors/273a76b9f02eb9d8. Report an issue: GitHub.

Appendix: source

Thrown at gix-ref/src/store/packed/transaction.rs:256

fn write_edit(out: &mut dyn std::io::Write, edit: &Edit, lines_written: &mut i32) -> std::io::Result<()> {
    match edit.inner.change {
        Change::Delete { .. } => {}
        Change::Update {
            new: Target::Object(target_oid),
            ..
        } => {
            write!(out, "{target_oid} ")?;
            out.write_all(edit.inner.name.as_bstr())?;
            out.write_all(b"\n")?;
            if let Some(object) = edit.peeled {
                writeln!(out, "^{object}")?;
            }
            *lines_written += 1;
        }
        Change::Update {
            new: Target::Symbolic(_),
            ..
        } => unreachable!("BUG: packed refs cannot contain symbolic refs, catch that in prepare(…)"),
    }
    Ok(())
}

/// Convert this buffer to be used as the basis for a transaction.
pub(crate) fn buffer_into_transaction(
    buffer: file::packed::SharedBufferSnapshot,
    lock_mode: gix_lock::acquire::Fail,
    precompose_unicode: bool,
    namespace: Option<Namespace>,
) -> Result<packed::Transaction, gix_lock::acquire::Error> {
    let lock = gix_lock::File::acquire_to_update_resource(&buffer.path, lock_mode, None)?;
    Ok(packed::Transaction {
        buffer: Some(buffer),
        lock: Some(lock),
        closed_lock: None,
        edits: None,
        precompose_unicode,

View on GitHub (pinned to e73179060b)