fix(build): also rerun on commits to the current branch
The earlier build-script fix only watched .git/HEAD, which catches branch switches and detached-HEAD moves but NOT git commit on the current branch — the commit updates the underlying ref file (refs/heads/<name> or packed-refs after pack-refs), and HEAD itself stays unchanged. So the embedded short-SHA in deepseek --version went stale on the same-branch-commit case the fix was supposed to cover. Resolve the symbolic ref at build time and watch: - the loose ref file (refs/heads/<branch>) - packed-refs (Cargo treats a non-existent rerun-if-changed path as always-changed, which covers the loose to packed transition after git pack-refs) Detached HEAD is unchanged: HEAD itself contains a SHA, no symbolic deref happens, and HEAD-as-watched still triggers on every move. Adds parse_symbolic_ref + 4 unit tests covering: stripped prefix, no trailing newline, detached SHA, empty input. Smoke verified: with the previous fix, an empty commit on the same branch did not bust the cache. With this commit, it does. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+85
-19
@@ -17,32 +17,98 @@ fn main() {
|
||||
}
|
||||
|
||||
/// Tell Cargo to invalidate the cached build script output when `HEAD`
|
||||
/// moves, so the embedded short-SHA stays in sync with the checkout. With
|
||||
/// only `rerun-if-env-changed` lines, Cargo otherwise caches the script
|
||||
/// across commits and the SHA goes stale.
|
||||
/// moves, so the embedded short-SHA stays in sync with the checkout.
|
||||
///
|
||||
/// `.git/HEAD` only changes on branch switches and detached-HEAD moves —
|
||||
/// `git commit` on the current branch updates the underlying ref file
|
||||
/// (loose `refs/heads/<name>`, or `packed-refs` after `git pack-refs`)
|
||||
/// without touching `HEAD` itself. So when `HEAD` is a symbolic ref we
|
||||
/// also watch the resolved target and `packed-refs`. A non-existent
|
||||
/// `rerun-if-changed` path is treated as "always changed" by Cargo, which
|
||||
/// covers the loose→packed transition.
|
||||
fn declare_git_head_rerun() {
|
||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let workspace_root = manifest_dir.join("..").join("..");
|
||||
let git_meta = workspace_root.join(".git");
|
||||
if git_meta.is_dir() {
|
||||
println!("cargo:rerun-if-changed={}", git_meta.join("HEAD").display());
|
||||
|
||||
let gitdir = if git_meta.is_dir() {
|
||||
git_meta
|
||||
} else if git_meta.is_file() {
|
||||
// Worktree checkout: `.git` is a pointer file with `gitdir: <path>`.
|
||||
// Worktree pointer file: watch it directly, then follow `gitdir:`.
|
||||
println!("cargo:rerun-if-changed={}", git_meta.display());
|
||||
if let Ok(contents) = std::fs::read_to_string(&git_meta) {
|
||||
for line in contents.lines() {
|
||||
if let Some(rest) = line.strip_prefix("gitdir:") {
|
||||
let trimmed = rest.trim();
|
||||
let gitdir = if Path::new(trimmed).is_absolute() {
|
||||
PathBuf::from(trimmed)
|
||||
} else {
|
||||
workspace_root.join(trimmed)
|
||||
};
|
||||
println!("cargo:rerun-if-changed={}", gitdir.join("HEAD").display());
|
||||
break;
|
||||
}
|
||||
}
|
||||
let Ok(contents) = std::fs::read_to_string(&git_meta) else {
|
||||
return;
|
||||
};
|
||||
let Some(rest) = contents.lines().find_map(|l| l.strip_prefix("gitdir:")) else {
|
||||
return;
|
||||
};
|
||||
let trimmed = rest.trim();
|
||||
if Path::new(trimmed).is_absolute() {
|
||||
PathBuf::from(trimmed)
|
||||
} else {
|
||||
workspace_root.join(trimmed)
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let head = gitdir.join("HEAD");
|
||||
println!("cargo:rerun-if-changed={}", head.display());
|
||||
|
||||
if let Ok(contents) = std::fs::read_to_string(&head)
|
||||
&& let Some(target) = parse_symbolic_ref(&contents)
|
||||
{
|
||||
println!("cargo:rerun-if-changed={}", gitdir.join(target).display());
|
||||
println!(
|
||||
"cargo:rerun-if-changed={}",
|
||||
gitdir.join("packed-refs").display()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// If `.git/HEAD` is a symbolic ref (`ref: refs/heads/...`) return the
|
||||
/// target ref path. Returns `None` for a detached HEAD (raw SHA).
|
||||
fn parse_symbolic_ref(head_contents: &str) -> Option<&str> {
|
||||
head_contents
|
||||
.lines()
|
||||
.next()
|
||||
.and_then(|line| line.strip_prefix("ref:"))
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::parse_symbolic_ref;
|
||||
|
||||
#[test]
|
||||
fn symbolic_ref_strips_prefix_and_whitespace() {
|
||||
assert_eq!(
|
||||
parse_symbolic_ref("ref: refs/heads/main\n"),
|
||||
Some("refs/heads/main")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symbolic_ref_handles_no_trailing_newline() {
|
||||
assert_eq!(
|
||||
parse_symbolic_ref("ref: refs/heads/work/v0.8.26-security"),
|
||||
Some("refs/heads/work/v0.8.26-security")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detached_head_is_not_a_symbolic_ref() {
|
||||
assert_eq!(
|
||||
parse_symbolic_ref("506343f44e48b9c2c8d6b2d3e8e8e8e8e8e8e8e8\n"),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_input_returns_none() {
|
||||
assert_eq!(parse_symbolic_ref(""), None);
|
||||
assert_eq!(parse_symbolic_ref("ref: \n"), None);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+85
-19
@@ -18,32 +18,98 @@ fn main() {
|
||||
}
|
||||
|
||||
/// Tell Cargo to invalidate the cached build script output when `HEAD`
|
||||
/// moves, so the embedded short-SHA stays in sync with the checkout. With
|
||||
/// only `rerun-if-env-changed` lines, Cargo otherwise caches the script
|
||||
/// across commits and the SHA goes stale.
|
||||
/// moves, so the embedded short-SHA stays in sync with the checkout.
|
||||
///
|
||||
/// `.git/HEAD` only changes on branch switches and detached-HEAD moves —
|
||||
/// `git commit` on the current branch updates the underlying ref file
|
||||
/// (loose `refs/heads/<name>`, or `packed-refs` after `git pack-refs`)
|
||||
/// without touching `HEAD` itself. So when `HEAD` is a symbolic ref we
|
||||
/// also watch the resolved target and `packed-refs`. A non-existent
|
||||
/// `rerun-if-changed` path is treated as "always changed" by Cargo, which
|
||||
/// covers the loose→packed transition.
|
||||
fn declare_git_head_rerun() {
|
||||
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
let workspace_root = manifest_dir.join("..").join("..");
|
||||
let git_meta = workspace_root.join(".git");
|
||||
if git_meta.is_dir() {
|
||||
println!("cargo:rerun-if-changed={}", git_meta.join("HEAD").display());
|
||||
|
||||
let gitdir = if git_meta.is_dir() {
|
||||
git_meta
|
||||
} else if git_meta.is_file() {
|
||||
// Worktree checkout: `.git` is a pointer file with `gitdir: <path>`.
|
||||
// Worktree pointer file: watch it directly, then follow `gitdir:`.
|
||||
println!("cargo:rerun-if-changed={}", git_meta.display());
|
||||
if let Ok(contents) = std::fs::read_to_string(&git_meta) {
|
||||
for line in contents.lines() {
|
||||
if let Some(rest) = line.strip_prefix("gitdir:") {
|
||||
let trimmed = rest.trim();
|
||||
let gitdir = if Path::new(trimmed).is_absolute() {
|
||||
PathBuf::from(trimmed)
|
||||
} else {
|
||||
workspace_root.join(trimmed)
|
||||
};
|
||||
println!("cargo:rerun-if-changed={}", gitdir.join("HEAD").display());
|
||||
break;
|
||||
}
|
||||
}
|
||||
let Ok(contents) = std::fs::read_to_string(&git_meta) else {
|
||||
return;
|
||||
};
|
||||
let Some(rest) = contents.lines().find_map(|l| l.strip_prefix("gitdir:")) else {
|
||||
return;
|
||||
};
|
||||
let trimmed = rest.trim();
|
||||
if Path::new(trimmed).is_absolute() {
|
||||
PathBuf::from(trimmed)
|
||||
} else {
|
||||
workspace_root.join(trimmed)
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
let head = gitdir.join("HEAD");
|
||||
println!("cargo:rerun-if-changed={}", head.display());
|
||||
|
||||
if let Ok(contents) = std::fs::read_to_string(&head)
|
||||
&& let Some(target) = parse_symbolic_ref(&contents)
|
||||
{
|
||||
println!("cargo:rerun-if-changed={}", gitdir.join(target).display());
|
||||
println!(
|
||||
"cargo:rerun-if-changed={}",
|
||||
gitdir.join("packed-refs").display()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// If `.git/HEAD` is a symbolic ref (`ref: refs/heads/...`) return the
|
||||
/// target ref path. Returns `None` for a detached HEAD (raw SHA).
|
||||
fn parse_symbolic_ref(head_contents: &str) -> Option<&str> {
|
||||
head_contents
|
||||
.lines()
|
||||
.next()
|
||||
.and_then(|line| line.strip_prefix("ref:"))
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::parse_symbolic_ref;
|
||||
|
||||
#[test]
|
||||
fn symbolic_ref_strips_prefix_and_whitespace() {
|
||||
assert_eq!(
|
||||
parse_symbolic_ref("ref: refs/heads/main\n"),
|
||||
Some("refs/heads/main")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symbolic_ref_handles_no_trailing_newline() {
|
||||
assert_eq!(
|
||||
parse_symbolic_ref("ref: refs/heads/work/v0.8.26-security"),
|
||||
Some("refs/heads/work/v0.8.26-security")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detached_head_is_not_a_symbolic_ref() {
|
||||
assert_eq!(
|
||||
parse_symbolic_ref("506343f44e48b9c2c8d6b2d3e8e8e8e8e8e8e8e8\n"),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_input_returns_none() {
|
||||
assert_eq!(parse_symbolic_ref(""), None);
|
||||
assert_eq!(parse_symbolic_ref("ref: \n"), None);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user