gitbutlerapp/gitbutler · warning
changed while preparing the rewrite; leaving it alone
Error message
{} changed while preparing the rewrite; leaving it alone What it means
replace_file_contents rewrites agent-related files via a temp file + atomic persist. To avoid silently discarding concurrent edits, it re-reads the file before the rename; if the content changed since the original read, it backs off and leaves the file alone so a later sweep re-runs on the fresh content.
Solutions
- Treat this as an expected race: simply re-run the cleanup — the error is designed so a later sweep processes the fresh content
- Serialize edits: hold a lock (or the same editor/session) so no concurrent writer touches the file during cleanup
- Disable auto-save/formatters for files under sweep, or run the sweep while those tools are idle
- Check mtime before initiating a rewrite and skip files modified recently
Example fix
// before: blind retry that may keep failing
loop { match cleanup_file(path) { Ok(_) => break, Err(_) => continue } }
// after: back off and retry on fresh content after a delay
match cleanup_file(path) {
Err(e) if e.to_string().contains("changed while preparing the rewrite") => {
std::thread::sleep(Duration::from_millis(250));
cleanup_file(path)?; // re-reads the fresh content
}
r => r,
} Defensive patterns
Strategy: retry
Validate before calling
// re-check mtime before starting the rewrite
let before = std::fs::metadata(path)?.modified()?;
if before.elapsed()?.as_secs() < 1 { /* recently written — wait or skip */ } Try / catch
match replace_file_contents(path, new_content) {
Err(e) if e.to_string().contains("changed while preparing the rewrite") => {
std::thread::sleep(Duration::from_millis(250));
replace_file_contents(path, compute_new_content(path)?) // re-read fresh
}
r => r,
} Prevention
- Recompute the new content from a fresh read on each attempt
- Serialize writers with a file lock or single cleanup owner
- Disable autosave/formatters for files under automated sweeps
- Back off and retry rather than forcing the write
When it happens
Trigger: cleanup_file-driven rewrite of a file that was modified between the initial read_to_string and the pre-rename check — a concurrent process, editor auto-save, formatter, or another cleanup invocation wrote to the path in that window.
Common situations: An agent session editing the same instructions/notes file while the cleanup sweep runs; linters-on-save firing during the rewrite; two cleanup processes racing on the same file; users editing files by hand while an automated sweep is in flight.
Understand the failure class
Background: "failed to write file", "Could not save figure", "Error saving remote file" — file write failed: causes and fixes across languages and libraries — this error's family across 38 libraries.
Related errors
- another pre-commit hook is already using the repository…
- Could not read settings
- Could not restore native GitHub stack membership after the…
- diffing-thread crashed
- Failed to fetch review
AI-assisted analysis of gitbutlerapp/gitbutler@58e5313667 (2026-09-18).
Data as JSON: /api/errors/cb67cbbc163aa80d.
Report an issue: GitHub.
Appendix: source
Thrown at crates/but/src/command/agent/cleanup.rs:133
path: &Path,
read_content: &str,
new_content: &str,
original: &std::fs::Metadata,
) -> Result<()> {
use std::io::Write as _;
let dir = path
.parent()
.with_context(|| format!("No parent directory for {}", path.display()))?;
let mut temp = tempfile::NamedTempFile::new_in(dir)?;
temp.write_all(new_content.as_bytes())?;
// Temp files default to owner-only permissions; keep the original mode.
temp.as_file().set_permissions(original.permissions())?;
temp.as_file().sync_all()?;
// The rename below replaces the whole file, so an edit that landed after
// our read would be silently thrown away with it. Back off if the content
// moved under us — a later command re-runs the sweep on the fresh content.
if std::fs::read_to_string(path)? != read_content {
anyhow::bail!(
"{} changed while preparing the rewrite; leaving it alone",
path.display()
);
}
temp.persist(path)
.with_context(|| format!("Failed to replace {}", path.display()))?;
Ok(())
}
/// Replace every line-anchored occurrence of the retired bullet that sits
/// inside a managed block. Returns `None` when nothing needs rewriting.
fn replace_retired_bullet(content: &str) -> Option<String> {
// The cheap check first: on the steady-state clean file this skips the
// marker scan and allocation entirely.
if !content.contains(RETIRED_FAST_PATH_BULLET) {
return None;
}
// A malformed block (unmatched marker) is not ours to interpret: skip.View on GitHub (pinned to 58e5313667)