GitoxideLabs/gitoxide · error

cannot restore an unborn checkout after review setup failed

Error message

cannot restore an unborn checkout after review setup failed

What it means

If review setup fails midway, `restore_checkout` attempts to return the worktree to its previous position. When the repository had an unborn HEAD (a fresh repo with no commits — no branch target and no commit id recorded), there is nowhere to check out, so the restore is impossible and this error is raised during rollback.

Solutions

  1. Create an initial commit first (`git commit --allow-empty -m init`) so HEAD is born, then retry the review.
  2. Manually restore the unborn-HEAD state: delete the branch HEAD was moved to (`git update-ref -d <branch>`) if setup partially completed.
  3. Re-initialize the repository from scratch if it is disposable.

Example fix

// before: review in an empty repo fails and cannot roll back
// shell: git commit --allow-empty -m "initial"
// after: retry the review command
Defensive patterns

Strategy: validation

Validate before calling

let has_commits = repo.head_id().is_ok() || repo.head().ok().and_then(|h| h.try_into_referenced().ok()).is_some();
if !has_commits {
    return Err(anyhow::anyhow!("create an initial commit before starting a review"));
}

Try / catch

match review::start(repo, params) {
    Err(e) if e.to_string().contains("unborn checkout") => {
        eprintln!("repository has no commits; run: git commit --allow-empty -m init");
    }
    other => other?,
}

Prevention

When it happens

Trigger: Review `start` failing on a repository with zero commits (HEAD points to a not-yet-created branch), triggering the rollback path in `restore_checkout` with `(None, None)`.

Common situations: Running `tix review` in a freshly `git init`ed repository before the first commit; a repo where all branches were deleted.

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/dddbdc1ad9d08497. Report an issue: GitHub.

Appendix: source

Thrown at gix-tix/src/edit/review.rs:377

fn remove_new_departure_pin(repository_path: &Path, bare: bool, pin: Option<&(history::Pin, bool)>) -> Result<()> {
    let Some((pin, true)) = pin else { return Ok(()) };
    let repo =
        open_repository(repository_path, bare, false).context("could not reopen repository to remove review pin")?;
    super::time_travel::delete_pin(&repo, pin).context("could not remove review departure pin")
}

fn restore_checkout(workdir: &Path, restore: &(Option<gix::refs::FullName>, Option<ObjectId>)) -> Result<()> {
    let mut command = Command::new("git");
    command.arg("-C").arg(workdir).args(["checkout", "--quiet", "--force"]);
    match restore {
        (Some(name), _) => {
            let name = gix::path::from_bstr(name.as_bstr());
            command.arg(name.as_ref());
        }
        (None, Some(id)) => {
            command.args(["--detach", &id.to_string()]);
        }
        (None, None) => anyhow::bail!("cannot restore an unborn checkout after review setup failed"),
    }
    let output = command
        .output()
        .context("could not restore checkout after review setup failed")?;
    if output.status.success() {
        Ok(())
    } else {
        anyhow::bail!("{}", String::from_utf8_lossy(&output.stderr).trim())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn run(path: &Path, args: &[&str]) -> gix_testtools::Result<Vec<u8>> {
        let output = Command::new("git")
            .arg("-C")

View on GitHub (pinned to e73179060b)