GitoxideLabs/gitoxide · error

a fork target must be picked before it is used

Error message

a fork target must be picked before it is used

What it means

A fork heading names a target commit; if that target belongs to the rebase scope it must already have been picked by an earlier (lower in the file, since parsing is bottom-up) plan step, so the fork can attach to that step. Using an in-scope fork target before it is picked would create a plan referencing a nonexistent step, so it is rejected.

Solutions

  1. Move the fork heading below (in file order: after) the section that picks its target commit
  2. Change the fork target to a commit outside the rebase scope (an existing commit) if it should not attach to a plan step
  3. Restore the pick line for the target commit in the preceding section

Example fix

// before (fork targets in-scope commit before it is picked)
──fork def5678──
pick 2222222
──fork abc1234──
pick 1111111
// after (pick the target first, then fork onto it)
──fork abc1234──
pick 1111111
──fork def5678──
pick 2222222
Defensive patterns

Strategy: validation

Validate before calling

// fork targets inside the scope must already have a pick step above them
for fork in fork_targets(edited) {
    if scope.contains(&fork.id) && !picked_before(fork, edited) {
        return Err(format!("fork target {} must be picked first", fork.id));
    }
}

Try / catch

if let Err(e) = parse(repo, edited) {
    if e.to_string().contains("must be picked before it is used") {
        // prompt user to reorder the fork section
    }
}

Prevention

When it happens

Trigger: `parse` bails when a fork heading's resolved commit `id` is in `scope` but not in the `picked` map at that point (not `PlanParent::Step`). Happens when a user reorders fork sections so a fork targeting a scoped commit appears before the section that picks that commit.

Common situations: Manually reordering fork blocks in the todo editor; moving a fork heading to the wrong position while restructuring; deleting the pick step a fork depended on.

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

Appendix: source

Thrown at gix-tix/src/edit/todo.rs:886

            let target = line
                .trim_matches('─')
                .trim()
                .strip_prefix("fork ")
                .context("a fork separator needs a fork ID")?;
            if sections > 0 && !section_has_commit {
                anyhow::bail!("a fork section contains no commits");
            }
            let id = resolve_commit(
                repo,
                target
                    .split_whitespace()
                    .next()
                    .context("a fork heading needs a commit ID")?,
            )?;
            cursor = Some(if let Some(index) = picked.get(&id) {
                rebase::PlanParent::Step(*index)
            } else if scope.contains(&id) {
                anyhow::bail!("a fork target must be picked before it is used");
            } else {
                rebase::PlanParent::Existing(id)
            });
            sections += 1;
            section_has_commit = false;
            section_last_step = None;
            continue;
        }
        if line.starts_with('(') && line.ends_with(')') {
            let target = cursor.context("a reference line must follow a fork or command")?;
            for (marked, value) in parse_ref_line(line)? {
                let name = resolve_ref_name(repo, &mut state.expected_refs, value.as_bstr())?;
                if ref_targets.insert(name.clone(), target).is_some() {
                    anyhow::bail!("a reference is placed more than once");
                }
                if marked {
                    if !state.checkout_allowed || repo.workdir().is_none() {
                        anyhow::bail!("the rebase todo cannot select a checkout without a worktree");

View on GitHub (pinned to e73179060b)