gitbutlerapp/gitbutler · error

Found a GitButler managed block start marker without a…

Error message

Found a GitButler managed block start marker without a matching end marker. Refusing to edit a partial managed block.

What it means

The managed-block editor scans a file for GitButler MANAGED_BLOCK_START/END marker pairs and records the spans it owns. If a start marker exists without a matching end marker, the block is partial/corrupt; editing it could truncate or duplicate user content, so the operation refuses.

Solutions

  1. Restore the missing end marker (MANAGED_BLOCK_END) so the block is complete, then retry
  2. If the block is unwanted, remove the entire start marker (and any orphaned body) instead of leaving a partial block
  3. Regenerate the file section from a known-good version (e.g. `git checkout -- <file>` if committed state was intact)
  4. Avoid hand-editing between the marker lines; edit outside the managed span

Example fix

// before: crashing on a partial block
upsert_managed_block(&mut content, "## Tasks", new_bullets)?;
// after: repair or reject the partial block first
if content.contains(MANAGED_BLOCK_START) && !content.contains(MANAGED_BLOCK_END) {
    anyhow::bail!("repair the partial managed block (missing end marker) before updating");
}
upsert_managed_block(&mut content, "## Tasks", new_bullets)?;
Defensive patterns

Strategy: validation

Validate before calling

fn has_complete_managed_block(content: &str) -> bool {
    let starts = content.matches(MANAGED_BLOCK_START).count();
    let ends = content.matches(MANAGED_BLOCK_END).count();
    starts == 0 || starts == ends
}

Try / catch

match upsert_managed_block(&mut content, heading, bullets) {
    Err(e) if e.to_string().contains("without a matching end marker") => {
        repair_or_regenerate_file_before_retry()?;
        upsert_managed_block(&mut content, heading, bullets)
    }
    r => r,
}

Prevention

When it happens

Trigger: Calling upsert_managed_block or replace_retired_bullet on a file whose content contains an opening managed-block marker with no closing marker before EOF — typically because a previous write was truncated, a user manually deleted the end marker, or a formatter/tool stripped it.

Common situations: Manual editing of agent-maintained markdown where a user removed marker lines they thought were decoration; a crash/disk-full during a prior write leaving a truncated file; templating tools that rewrite the file and drop unknown comment lines; partially committed merges of these files.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of gitbutlerapp/gitbutler@58e5313667 (2026-09-18). Data as JSON: /api/errors/8eb123fee4659b95. Report an issue: GitHub.

Appendix: source

Thrown at crates/but/src/command/agent/files.rs:75

    }
    open
}

/// Byte spans of each managed block, from the first byte of the start marker
/// to just past the last byte of the end marker (both markers lie inside the
/// end-exclusive `Range`). Pairs every line-anchored start with the first
/// line-anchored end after it. Errors when a start marker has no matching end,
/// so callers refuse to touch a malformed block.
pub(super) fn managed_block_spans(existing: &str) -> Result<Vec<std::ops::Range<usize>>> {
    let mut spans = Vec::new();
    let mut pos = 0;
    while let Some(start) = find_line_anchored(existing, MANAGED_BLOCK_START, pos) {
        let Some(end) = find_line_anchored(
            existing,
            MANAGED_BLOCK_END,
            start + MANAGED_BLOCK_START.len(),
        ) else {
            anyhow::bail!(
                "Found a GitButler managed block start marker without a matching end marker. Refusing to edit a partial managed block."
            );
        };
        let span_end = end + MANAGED_BLOCK_END.len();
        spans.push(start..span_end);
        pos = span_end;
    }
    Ok(spans)
}

/// Rewrite `block` to use CRLF line endings when `existing` already does, so a
/// replaced or appended block does not introduce mixed line endings.
fn match_line_endings(existing: &str, block: &str) -> String {
    if existing.contains("\r\n") {
        block.replace("\r\n", "\n").replace('\n', "\r\n")
    } else {
        block.to_string()
    }

View on GitHub (pinned to 58e5313667)