
How Bark Rewrites Thousands of Files Without Breaking One
Bark is a Rust CLI whose entire job is mutating source files β every file in your repository, in parallel. This post covers the three things that make that trustworthy: an idempotent header engine, an atomic write path with backups, and 210 tests for code whose failure mode is destroying your work.
Table of Contents
Bark is a small Rust CLI I built with one job: stamp a standardized path header β like // File: src/main.rs β onto every source file in a project, and keep a directory-tree snapshot fresh. That sounds harmless until you notice what it actually means: a tool that opens every file in your repository, rewrites it, and does so in parallel across all cores. The failure modes aren't 'a flag didn't parse'; they're 'your uncommitted work is gone.'
So the interesting engineering in bark isn't the header text β it's the three properties that make a file-rewriting tool boring to run: it must be idempotent (running it twice changes nothing), it must be atomic (a crash mid-run corrupts nothing), and it must be tested like the stakes are real (because they are). This post walks through how each one is built, with the actual code.
Act one: the idempotent header engine
Idempotence sounds like an implementation detail. It's actually a specification problem: before you can guarantee 'running twice is a no-op', you have to define β precisely β what counts as 'this file is already tagged'. Bark's answer lives in a single function that classifies every file into one of three states.
pub enum HeaderAction {
AlreadyCurrent,
UpdateExisting,
AddNew,
}
/// Determine what action should be taken on a file's header.
pub fn analyze(content: &str, desired_header: &str, style: CommentStyle) -> HeaderAction {
let re = style.detect_regex();
let candidate = candidate_line(content);
match candidate {
Some(line) if re.is_match(line) => {
if line.trim() == desired_header.trim() {
HeaderAction::AlreadyCurrent
} else {
HeaderAction::UpdateExisting
}
}
_ => HeaderAction::AddNew,
}
}
/// Return the line to inspect for an existing header.
/// Skips shebangs so the header candidate is always line 0 or 1.
fn candidate_line(content: &str) -> Option<&str> {
let mut lines = content.lines();
let first = lines.next()?;
if first.starts_with("#!") {
lines.next()
} else {
Some(first)
}
}Three states, no fourth: the header is exactly what we want (touch nothing), the header slot is occupied by something else (replace it in place), or there's no header (insert one). Because the classification is total and the rewrite is deterministic, the second run of bark always lands in AlreadyCurrent for every file. That's the whole idempotence proof, and it's small enough to hold in your head.
The subtle decision is hiding in detect_regex(). What pattern identifies 'a bark header'? The header text is user-configurable β templates like File: {{file}} | {{author}} | {{date}} render differently per file, per user, per day. If detection matched the current template's shape, changing the template would strand every existing header: bark would stop recognizing them and stack a second header on top. So detection is deliberately broad:
/// Regex that broadly matches ANY bark header for this comment style.
/// Intentionally broad so it catches headers written with any template.
pub fn detect_regex(&self) -> Regex {
let pattern = match self {
Self::Slash => r"^[[:space:]]*//[[:space:]]+\S",
Self::Hash => r"^[[:space:]]*#[[:space:]]+\S",
Self::Css => r"^[[:space:]]*/\*[[:space:]]+\S",
Self::Html => r"^[[:space:]]*<!--[[:space:]]+\S",
};
Regex::new(pattern).expect("valid regex")
}The tradeoff, stated honestly
Any comment on the first content line is treated as the managed header β including a comment bark never wrote. Point bark at a file that opens with // Copyright 2019 Some Corp and that line becomes the header slot and gets replaced. That's the price of surviving template changes and renames. Two mitigations make it acceptable: every modified file is backed up first, and the header_skip config marks files whose first line must never be touched. Broad-but-recoverable beat clever-but-fragile.
The rewrite itself (apply_tag) carries the rest of the correctness burden, and it's all edge cases: a shebang line must stay at line 0 with the header inserted after it; files with CRLF line endings keep CRLF (bark detects the ending and joins with it); a trailing newline is preserved if and only if one existed; and however many blank lines followed the old header, exactly one follows the new one. None of this is hard individually β the discipline is that every one of these is a named unit test, because every one is a way to corrupt a file politely.
Act two: never corrupt a file
Deciding the new content correctly is half the job. Getting it onto disk is the other half, and the naive way is a trap: std::fs::write truncates the file to zero bytes and then writes. Kill the process between those two steps β crash, power loss, Ctrl-C at the wrong millisecond, disk full β and the file is empty. For a tool touching thousands of files per run, 'rare' multiplied by 'thousands' stops being rare.
Bark routes every mutation β tag, strip, everything β through one function:
/// Atomically write `content` to `target`.
/// Creates the tempfile in the same directory as the target so the
/// rename (persist) is guaranteed to be on the same filesystem.
pub fn write_atomic(target: &Path, content: &str) -> Result<()> {
let parent = target
.parent()
.with_context(|| format!("no parent for: {}", target.display()))?;
// Capture original permissions if the file already exists
let original_perms = if target.exists() {
Some(std::fs::metadata(target)?.permissions())
} else {
None
};
let mut tmp = NamedTempFile::new_in(parent)
.with_context(|| format!("creating tempfile near: {}", target.display()))?;
tmp.write_all(content.as_bytes())
.with_context(|| "writing to tempfile")?;
tmp.flush()?;
// Restore permissions before rename
if let Some(perms) = original_perms {
std::fs::set_permissions(tmp.path(), perms)?;
}
tmp.persist(target)
.with_context(|| format!("persisting to: {}", target.display()))?;
Ok(())
}Three details carry all the weight. First, NamedTempFile::new_in(parent) β the tempfile is created in the target's own directory, not in /tmp. Rename is only atomic within a single filesystem; /tmp is frequently a different mount, and crossing filesystems silently degrades rename into copy-then-delete, which reintroduces the exact partial-write window we're eliminating. Second, permissions are captured from the original and stamped onto the tempfile before the rename β otherwise every executable script bark touches would come out mode 600 and stop being executable. Third, persist() is the rename: at every instant, the path on disk points to either the complete old file or the complete new file. There is no in-between state to observe.
Atomicity protects against crashes. It does nothing against the other failure mode: bark doing exactly what you asked on a config you got wrong. For that, the layer above write_atomic takes a timestamped backup of every file before modifying it, mirroring the source tree under .barks/:
.barks/
βββ src/
βββ main.rs.20260325_142022.bak
βββ main.rs.20260318_091044.bak
βββ lib.rs.20260318_091044.bak
# <relative/path>.<YYYYMMDD_HHMMSS>.bak β mirrors the source treebark restore --latest reverts every file to its most recent backup; interactive restore picks from a numbered list; bark clean --keep N stops the directory growing forever. The combination changes the tool's personality: a first run on a legacy codebase goes from 'deep breath' to 'worst case, one command undoes it'. Safety features aren't just protection β they're what makes people actually adopt a tool that mutates their files.
Act three: 210 tests for a filesystem-heavy CLI
Bark has 210 tests β 104 unit tests next to the code and 106 integration tests that exercise the compiled binary end-to-end β for about 4,200 lines of source. Line coverage sits at 90.16%, measured with cargo-tarpaulin. Those numbers aren't the point, though; the point is what a filesystem-heavy CLI forces you to figure out about testability.
The first enabler is an architectural choice from act one: the header engine is pure. analyze() and apply_tag() take strings and return strings β no file handles, no paths, no I/O. Every corrupting edge case (shebang plus existing header plus multiple blank lines; CRLF preservation; empty files) is a plain unit test with no filesystem setup. The code most capable of destroying data is the easiest code in the project to test exhaustively.
The integration layer then tests what unit tests structurally can't: flag parsing, exit codes, config file resolution, and real directory trees. Each test builds a throwaway project in a tempfile::TempDir and runs the actual bark binary against it with assert_cmd. This is the test that guards the core promise of the whole tool:
#[test]
fn tag_is_idempotent() {
let dir = TempDir::new().unwrap();
init_git(&dir);
fs::write(dir.path().join("main.go"), "package main\n").unwrap();
// First run tags the file
bark().current_dir(dir.path()).assert().success();
let after_first = fs::read_to_string(dir.path().join("main.go")).unwrap();
// Second run must change nothing
bark().current_dir(dir.path()).assert().success();
let after_second = fs::read_to_string(dir.path().join("main.go")).unwrap();
assert_eq!(after_first, after_second, "second run must be a no-op");
}The hardest thing to test was watch mode, for a blunt reason: it's an infinite loop. bark watch subscribes to OS file events via the notify crate, debounces editor burst-saves, tags what changed, and runs until Ctrl-C. An integration test can't send Ctrl-C to a function call β so the loop is designed with its own off-switch:
/// Run forever (normal CLI use).
pub fn run(&self, root: &Path) -> anyhow::Result<()> {
self.run_until_stopped(root, None)
}
/// Run until `stop` is set to true (useful for tests).
/// The loop checks the flag every 100 ms when idle.
pub fn run_until_stopped(
&self,
root: &Path,
stop: Option<Arc<AtomicBool>>,
) -> anyhow::Result<()> {
// ...
loop {
if stop.as_ref().is_some_and(|s| s.load(Ordering::Relaxed)) {
break;
}
// wait for events with a 100ms timeout, debounce, tag...
}
Ok(())
}Production passes None and loops forever. A test spawns the watcher on a thread with a shared AtomicBool, saves some files, waits for them to be tagged, flips the flag, and joins the thread. The same design solves watch mode's other landmine: tagging a file emits a change event for that file, which would trigger re-tagging, forever. Before writing, the watcher records the path in a recently_written set; when the event for bark's own write arrives, it's consumed and skipped. Both behaviors β clean shutdown and no self-tagging loop β are covered by real integration tests instead of being the untested corner every long-running feature tends to become.
One last reliability habit, easy to miss: bark shells out to git β user.name feeds the {{author}} template variable, and --staged mode asks git for the staged file list. A subprocess call inherits the worst-case behavior of whatever it calls, and git can hang on a stuck credential helper or a slow network filesystem. So every git call runs on a spawned thread with an mpsc receive timeout:
fn get_git_author() -> anyhow::Result<String> {
let (tx, rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let result = std::process::Command::new("git")
.args(["config", "user.name"])
.output();
let _ = tx.send(result);
});
let out = rx
.recv_timeout(std::time::Duration::from_secs(2))
.map_err(|_| anyhow::anyhow!("git config timed out"))??;
let name = String::from_utf8_lossy(&out.stdout).trim().to_string();
if name.is_empty() {
anyhow::bail!("empty git user.name");
}
Ok(name)
}Two seconds and then bark falls back to "unknown" and keeps going. A header-stamping tool that hangs at startup because of a git credential prompt would be absurd β but it's the default behavior of Command::output(), and it's the kind of bug you only meet in the field. CI runs the whole suite with cargo test, clippy -D warnings, and rustfmt --check on Ubuntu, macOS, and Windows, because a tool that rewrites files on people's machines doesn't get to have platform-specific surprises with path separators or line endings.
What generalizes
- Idempotence is a spec before it's code: define 'already done' precisely, make the classification total, and write the run-twice test first.
- Atomicity lives in the rename, and the rename only counts on one filesystem β put the tempfile next to the target, and remember permissions don't come along for free.
- Backups change adoption, not just outcomes: a destructive tool with a one-command undo is a different product from the same tool without it.
- Keep the dangerous logic pure and I/O-free β it becomes the most-tested code in the project instead of the least.
- Design infinite loops and subprocess calls for their tests: a stop flag and a timeout cost a few lines and buy you coverage of the code that otherwise never gets any.
Bark is open source under MIT β the code is on GitHub, the full command reference lives in the docs section of this site, and the fastest way to see all of this in action is bark tag --dry-run on a repo you care about, followed by the run you no longer have to be nervous about.
AI-readable content
Learn more βThis content is available via the AI Content API as JSON or token-efficient Markdown. Feed it directly into LLM workflows.
/api/content/blogs/how-bark-rewrites-your-codebase-without-breaking-itRelated Articles
Continue your learning journey with these handpicked articles.


Related Content
Explore related articles, projects, and tools.