GitoxideLabs/gitoxide · error
a squash is outside the editable history
Error message
a squash is outside the editable history
What it means
Validation inside `edit::todo::parse` of gix-tix. Commands prefixed with `@` (e.g. `@squash`) mark a position in the editable history, but a `squash` verb is only legal when it sits within the history that the todo is allowed to edit. When a squash command's marked position resolves outside that editable history, parsing bails rather than producing a plan that would rewrite commits the tool doesn't own. Triggered by hand-edited todos that move a squash to a foreign segment of history; fix by removing the squash or placing it inside the editable range.
Solutions
- Use a commit ID that is part of the editable history range
- Extend the plan's scope if the commit legitimately belongs to the edit
- Remove the squash line if the commit should not be folded
Example fix
// before squash 9999999 # commit outside the edited range // after squash abc1234 # commit inside the edited range
Defensive patterns
Strategy: validation
Validate before calling
let id = repo.rev_parse_single(value)?;
if !scope.contains(&id) {
anyhow::bail!("squash target {} is outside the editable history", id.shorten_or_id());
} Type guard
fn squash_in_scope(id: gix::Id<'_>, scope: &HashSet<gix::oid::ObjectId>) -> bool {
scope.contains(id.object_id())
} Try / catch
match parse_plan(...) {
Err(e) if e.to_string().contains("a squash is outside the editable history") => { /* fix the squash line */ }
other => other?,
} Prevention
- Only squash commits that appear in the forked range of the plan
- Copy commit IDs from the plan's own history, not from other branches
- Resolve abbreviated IDs and check scope membership before writing the todo
When it happens
Trigger: `parse_plan` reads a `squash <oid>` line whose resolved commit is not in the plan's scope set.
Common situations: Typing a wrong or truncated commit ID; squashing a commit that lives outside the range forked for editing; copying a squash line from another rebase session.
Understand the failure class
Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.
Related errors
- the squash target must be a strict ancestor of the source
- the squash source must have exactly one parent
- the squash target must have exactly one parent
- descendant merge commits cannot be squashed
- a pick is outside the editable history
AI-assisted analysis of GitoxideLabs/gitoxide@e73179060b (2026-09-08).
Data as JSON: /api/errors/1a6af13172ae2089.
Report an issue: GitHub.
Appendix: source
Thrown at gix-tix/src/edit/todo.rs:941
let (verb, value) = command.split_once(char::is_whitespace).unwrap_or((command, ""));
let marked = verb.starts_with('@');
let verb = verb.strip_prefix('@').unwrap_or(verb);
if marked {
if std::mem::replace(&mut command_marker, true) {
anyhow::bail!("the rebase todo contains more than one @ command");
}
if !state.checkout_allowed || repo.workdir().is_none() {
anyhow::bail!("the rebase todo cannot select a checkout without a worktree");
}
}
if verb == "squash" {
let index = section_last_step.context("a squash must follow a command in the same fork")?;
let id = resolve_commit(
repo,
value.split_whitespace().next().context("a squash needs a commit ID")?,
)?;
if !scope.contains(&id) {
anyhow::bail!("a squash is outside the editable history");
}
if picked.insert(id, index).is_some() {
anyhow::bail!("a commit is picked more than once");
}
steps[index].squash.push(id);
if marked {
let target = rebase::PlanParent::Step(index);
if checkout_target.is_some_and(|checkout| checkout != target) {
anyhow::bail!("the @ command and @ reference point to different results");
}
checkout_target = Some(target);
}
section_has_commit = true;
continue;
}
let parent = cursor.context("the first todo command must follow a fork heading")?;
let commit = match verb {
"pick" => {View on GitHub (pinned to e73179060b)