GitoxideLabs/gitoxide · error

rebase worker stopped unexpectedly

Error message

rebase worker stopped unexpectedly

What it means

During interactive rebase rendering, the driver thread waits on a bounded `recv_timeout` for events from the rebase worker thread. A `mpsc::RecvTimeoutError::Disconnected` means every sender handle to the worker's event channel has been dropped, i.e. the worker thread exited without reporting `Complete`. The library raises this because a silent worker shutdown would otherwise look like a missing timeout event.

Solutions

  1. Inspect the worker closure for early `return`/`break`/`?` paths that exit without sending `RebaseWorkerEvent::Complete`; make every exit path send a terminal event or propagate the error before dropping the sender.
  2. Keep a clone of the `mpsc::Sender` alive for the worker's full lifetime (e.g. in a guard that sends `Complete`/error on drop).
  3. Check whether the rebase script/todos are malformed, causing the worker to bail out before producing events.
  4. Reproduce with logging inside the worker to see why the thread exits early.

Example fix

// before
match receiver.recv_timeout(timeout) {
    Ok(event) => Some(event),
    Err(mpsc::RecvTimeoutError::Timeout) => None,
    Err(mpsc::RecvTimeoutError::Disconnected) => {
        break Err(anyhow::anyhow!("rebase worker stopped unexpectedly"))
    }
}
// after
// In the worker: ensure the terminal event is always sent,
// even on error paths:
let result = run_rebase_steps(&mut progress_tx);
let _ = result_tx.send(RebaseWorkerEvent::Complete(result));
Defensive patterns

Strategy: try-catch

Validate before calling

// Before driving the worker, verify the channel contract:
// keep a Sender clone alive and assert the worker emits a terminal event
let keepalive = result_tx.clone(); // drop only after worker.join()
let _ = keepalive;

Type guard

fn worker_terminated(result: &Result<(), anyhow::Error>) -> bool {
    result.is_ok() || !result.as_ref().unwrap_err().to_string().contains("stopped unexpectedly")
}

Try / catch

match run_rebase_with_tui(...) {
    Err(e) if e.to_string().contains("rebase worker stopped unexpectedly") => {
        // worker exited early without Complete; inspect worker logs / retry
    }
    Err(e) => return Err(e),
    Ok(v) => return Ok(v),
}

Prevention

When it happens

Trigger: Calling the interactive rebase-with-TUI flow (the function driving `RebaseWorkerEvent`s at gix-tix/src/lib.rs:5840) when the worker thread returns early or its spawning/pump loop drops the `mpsc::Sender` before sending `Complete`.

Common situations: The worker hit an early `return`/`break` on an unrecoverable rebase error and dropped its sender; the user-initiated abort path closed the channel without sending a completion event; a bug in the worker loop exited without `result_tx.send(...)`.

Related errors


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

Appendix: source

Thrown at gix-tix/src/lib.rs:5840

        let started = Instant::now();
        let mut last_draw = started;
        let mut latest = None;
        let mut rendered = None;
        let result = loop {
            let now = Instant::now();
            let timeout = if now.duration_since(started) < TODO_PROGRESS_DELAY {
                Some(TODO_PROGRESS_DELAY.saturating_sub(now.duration_since(started)))
            } else if latest != rendered {
                Some(FRAME_INTERVAL.saturating_sub(now.duration_since(last_draw)))
            } else {
                None
            };
            let event = match timeout {
                Some(timeout) => match receiver.recv_timeout(timeout) {
                    Ok(event) => Some(event),
                    Err(mpsc::RecvTimeoutError::Timeout) => None,
                    Err(mpsc::RecvTimeoutError::Disconnected) => {
                        break Err(anyhow::anyhow!("rebase worker stopped unexpectedly"));
                    }
                },
                None => match receiver.recv() {
                    Ok(event) => Some(event),
                    Err(_) => break Err(anyhow::anyhow!("rebase worker stopped unexpectedly")),
                },
            };
            match event {
                Some(RebaseWorkerEvent::Progress(progress)) => latest = Some(progress),
                Some(RebaseWorkerEvent::Complete(result)) => break result,
                None => {}
            }
            let now = Instant::now();
            if todo_progress_visible(now.duration_since(started))
                && latest != rendered
                && now.duration_since(last_draw) >= FRAME_INTERVAL
            {
                let progress = latest.expect("a changed progress snapshot is available");

View on GitHub (pinned to e73179060b)