GitoxideLabs/gitoxide · error

user error: multiple calls are allowed only until it…

Error message

user error: multiple calls are allowed only until it succeeds

What it means

This `.expect("user error: multiple calls are allowed only until it succeeds")` fires when `PrepareFetch::fetch_only` (gix clone/fetch) is invoked more than once, or after the prepare state was already consumed by a successful fetch. Internally the builder holds `repo` in an `Option` that is taken on success; a second call finds `None` and the process panics. The message explicitly frames this as a user error: the API permits exactly one fetch attempt.

Solutions

  1. Call `fetch_only`/`fetch_then_checkout` exactly once per `PrepareFetch`; create a new `PrepareFetch` for retries.
  2. Use the returned values immediately; don't stash the prepare handle for later reuse.
  3. Restructure control flow so success paths never fall through to a second fetch call.
  4. Match on the first call's `Option` result explicitly instead of re-invoking.

Example fix

// before
let prep = gix::clone::PrepareFetch::new(...)?;
let _ = prep.fetch_only(PackCache, Interrupts)?;
let _ = prep.fetch_only(PackCache, Interrupts)?; // panics
// after
let prep = gix::clone::PrepareFetch::new(...)?;
let remote = prep.fetch_only(PackCache, Interrupts)?; // single call
// for retries, rebuild:
// let prep = gix::clone::PrepareFetch::new(...)?;
Defensive patterns

Strategy: validation

Validate before calling

// PrepareFetch cannot be queried directly; track consumption yourself
if fetch_already_attempted {
    anyhow::bail!("PrepareFetch::fetch_only must be called at most once; rebuild the prepare state to retry");
}

Try / catch

let result = std::panic::catch_unwind(AssertUnwindSafe(|| prep.fetch_only(cache, interrupts)));
match result {
    Ok(inner) => inner?,
    Err(_) => anyhow::bail!("fetch prepare state already consumed; create a new PrepareFetch"),
}

Prevention

When it happens

Trigger: Calling `fetch_only()` (or `fetch_then_checkout()`) twice on the same `PrepareFetch` value, or calling `fetch_only` after `fetch_then_checkout` already succeeded and consumed the state.

Common situations: Retry loops that re-call `fetch_only` on the same prepared handle after a failure consumed intermediate state or after success; conditional code paths that accidentally invoke fetch twice.

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

Appendix: source

Thrown at gix/src/clone/fetch/mod.rs:120

    ///
    /// Even though `async` is technically supported, it will still be blocking in nature as it uses a lot of non-async writes
    /// and computation under the hood. Thus it should be spawned into a runtime which can handle blocking futures.
    #[gix_protocol::bisync::bisync]
    pub async fn fetch_only<P>(
        &mut self,
        mut progress: P,
        should_interrupt: &std::sync::atomic::AtomicBool,
    ) -> Result<(crate::Repository, crate::remote::fetch::Outcome), Error>
    where
        P: crate::NestedProgress,
        P::SubProgress: 'static,
    {
        use crate::{bstr::ByteVec, remote, remote::fetch::RefLogMessage};

        let mut repo = self
            .repo
            .as_ref()
            .expect("user error: multiple calls are allowed only until it succeeds")
            .clone();

        repo.committer_or_set_generic_fallback()?;

        if !self.config_overrides.is_empty() {
            let mut snapshot = repo.config_snapshot_mut();
            snapshot.append_config(&self.config_overrides, gix_config::Source::Api)?;
        }

        let remote_name = match self.remote_name.as_ref() {
            Some(name) => name.to_owned(),
            None => repo
                .config
                .resolved
                .string(crate::config::tree::Clone::DEFAULT_REMOTE_NAME)
                .map(|n| crate::config::tree::Clone::DEFAULT_REMOTE_NAME.try_into_symbolic_name(n))
                .transpose()?
                .unwrap_or_else(|| {

View on GitHub (pinned to e73179060b)