Files
Chris Werner Rau b094cf961f fix(build): stop watching .git/index for the build-version rerun trigger (#3432)
Drops `.git/index` from the build-version rerun watch: git rewrites its stat cache on a plain `git status`, so background git activity forced a full lib plus binary recompile on builds with no source change.

Adds `logs/HEAD` alongside `HEAD` so the embedded `AOE_BUILD_VERSION` still refreshes on commit, pull, merge, rebase, and reset, which `HEAD` alone misses because it holds `ref: refs/heads/<branch>` and is not rewritten when the branch advances. Without it the #1754 respawn gate would match a worker still running the previous binary.

Includes a test pinning both halves of the trigger contract: a commit must disturb a watched path, a plain `git status` must not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 13:27:45 -04:00

76 lines
3.4 KiB
Rust

// Shared between `build.rs` (via `include!`) and the regression test in
// `tests/build_version_rerun.rs` (via `#[path] mod`). Keep it free of
// dependencies on the rest of the crate: build scripts compile in isolation.
/// The git files cargo should watch so `AOE_BUILD_VERSION` is recomputed when
/// the checkout's revision changes: `HEAD` for a checkout or a detached-HEAD
/// move, and the per-worktree reflog `logs/HEAD` for everything that advances
/// the branch `HEAD` points at (commit, pull, merge, rebase, reset), which
/// leaves the `HEAD` file itself untouched.
///
/// `index` is deliberately not watched. It looks like the natural trigger for
/// the dirty-flag suffix, but git rewrites its stat cache on a plain
/// `git status` (racily-clean revalidation), not just on real staging, so a
/// shell prompt or editor git integration running in the background made
/// cargo recompile the lib + binary for a build with zero source changes.
/// The cost is that the dirty suffix now lags: staging or editing files no
/// longer refreshes it on its own, so a binary rebuilt after an edit can
/// still report the flag computed at the last revision change. That was
/// already true for unstaged edits before, and the flag is documented as
/// coarse (see `emit_build_version` in `build.rs`).
///
/// Paths are resolved for the repository rooted at `dir` via
/// `git rev-parse --git-path`, which is correct for both a normal checkout
/// (`.git/HEAD`) and a git worktree, where `.git` is a file pointing at
/// `<main>/.git/worktrees/<name>/` and the literal `.git/HEAD` path does not
/// exist.
///
/// Only paths that exist on disk are returned. Cargo treats a missing
/// `rerun-if-changed` input as perpetually stale, so handing it a path that
/// does not exist rebuilds the lib + binary on every invocation; that is the
/// exact failure that hardcoding `.git/HEAD` caused in a worktree (issue
/// #1962). Returns an empty vec when git is unavailable or `dir` is not a git
/// checkout (e.g. a source tarball), leaving the build version pinned to
/// `CARGO_PKG_VERSION` with no spurious rerun trigger.
pub fn git_watch_paths(dir: &std::path::Path) -> Vec<String> {
["HEAD", "logs/HEAD"]
.iter()
.filter_map(|file| git_path(dir, file))
.filter(|path| watched_path_exists(dir, path))
.collect()
}
/// Resolve a single per-worktree git file path via `git rev-parse --git-path`,
/// run inside `dir`. `None` when git fails or the output is empty.
fn git_path(dir: &std::path::Path, file: &str) -> Option<String> {
let out = std::process::Command::new("git")
.arg("-C")
.arg(dir)
.args(["rev-parse", "--git-path", file])
.output()
.ok()?;
if !out.status.success() {
return None;
}
let path = String::from_utf8(out.stdout).ok()?.trim().to_string();
if path.is_empty() {
None
} else {
Some(path)
}
}
/// True when `path` points at an existing file. `git rev-parse --git-path`
/// returns an absolute path in a worktree and a path relative to `dir` (e.g.
/// `.git/HEAD`) in a normal checkout, so relative paths are resolved against
/// `dir`. `build.rs` runs with `dir = "."` and cargo's cwd at the package
/// root, so the relative path it emits resolves identically there.
fn watched_path_exists(dir: &std::path::Path, path: &str) -> bool {
let p = std::path::Path::new(path);
if p.is_absolute() {
p.exists()
} else {
dir.join(p).exists()
}
}