fix(core): resolve git metadata in worktrees

Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
This commit is contained in:
Matthew Grossman
2026-07-24 15:32:38 -07:00
parent 75d24688e1
commit 8055b09acd
3 changed files with 143 additions and 3 deletions
+6 -3
View File
@@ -4,16 +4,20 @@
use std::env;
use std::path::{Path, PathBuf};
#[path = "build_support/git.rs"]
mod git;
const PROTO_REL: &str = "../../proto";
fn main() -> Result<(), Box<dyn std::error::Error>> {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?);
// --- Git-derived version ---
// Compute a version from `git describe` for local builds. In Docker/CI
// builds where .git is absent, this silently does nothing and the binary
// falls back to CARGO_PKG_VERSION (which is already sed-patched by the
// build pipeline).
println!("cargo:rerun-if-changed=../../.git/HEAD");
println!("cargo:rerun-if-changed=../../.git/refs/tags");
git::emit_rerun_if_changed(&manifest_dir);
if let Some(version) = git_version() {
println!("cargo:rustc-env=OPENSHELL_GIT_VERSION={version}");
@@ -33,7 +37,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
env::set_var("PROTOC", protobuf_src::protoc());
}
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR")?);
let proto_root = manifest_dir.join(PROTO_REL);
let mut proto_files = Vec::new();
@@ -0,0 +1,45 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use std::process::Command;
const WATCH_PATHS: [&str; 2] = ["HEAD", "refs/tags"];
#[cfg(not(test))]
pub fn emit_rerun_if_changed(manifest_dir: &Path) {
for path in watch_paths(manifest_dir) {
println!("cargo:rerun-if-changed={}", path.display());
}
}
pub fn watch_paths(manifest_dir: &Path) -> Vec<PathBuf> {
WATCH_PATHS
.into_iter()
.filter_map(|path| resolve_existing_git_path(manifest_dir, path))
.collect::<BTreeSet<_>>()
.into_iter()
.collect()
}
fn resolve_existing_git_path(manifest_dir: &Path, path: &str) -> Option<PathBuf> {
let output = Command::new("git")
.args(["rev-parse", "--git-path", path])
.current_dir(manifest_dir)
.output()
.ok()?;
if !output.status.success() {
return None;
}
let path = PathBuf::from(String::from_utf8(output.stdout).ok()?.trim());
let path = if path.is_absolute() {
path
} else {
manifest_dir.join(path)
};
path.exists().then_some(path)
}
@@ -0,0 +1,92 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
#[path = "../build_support/git.rs"]
mod git;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
fn run_git(repo: &Path, args: &[&str]) -> String {
let output = Command::new("git")
.arg("-C")
.arg(repo)
.args(args)
.output()
.expect("run git");
assert!(
output.status.success(),
"git {args:?} failed: {}",
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8(output.stdout)
.expect("git output is UTF-8")
.trim()
.to_owned()
}
fn init_repository() -> (tempfile::TempDir, PathBuf) {
let temp = tempfile::tempdir().expect("create temp directory");
let repo = temp.path().join("repo");
fs::create_dir(&repo).expect("create repository directory");
run_git(&repo, &["init", "--quiet"]);
run_git(&repo, &["config", "user.name", "OpenShell Test"]);
run_git(
&repo,
&["config", "user.email", "openshell@example.invalid"],
);
fs::write(repo.join("README.md"), "test\n").expect("write tracked file");
run_git(&repo, &["add", "README.md"]);
run_git(&repo, &["commit", "--quiet", "-m", "initial"]);
run_git(&repo, &["tag", "v0.0.1"]);
(temp, repo)
}
fn git_path(repo: &Path, path: &str) -> PathBuf {
let path = PathBuf::from(run_git(repo, &["rev-parse", "--git-path", path]));
if path.is_absolute() {
path
} else {
repo.join(path)
}
}
#[test]
fn resolves_metadata_paths_in_normal_checkout() {
let (_temp, repo) = init_repository();
let paths = git::watch_paths(&repo);
assert!(paths.contains(&git_path(&repo, "HEAD")));
assert!(paths.contains(&git_path(&repo, "refs/tags")));
assert!(paths.iter().all(|path| path.exists()));
}
#[test]
fn resolves_metadata_paths_in_linked_worktree() {
let (temp, repo) = init_repository();
let worktree = temp.path().join("linked");
let worktree_arg = worktree.to_str().expect("worktree path is UTF-8");
run_git(
&repo,
&["worktree", "add", "--quiet", "-b", "linked", worktree_arg],
);
let paths = git::watch_paths(&worktree);
let head = git_path(&worktree, "HEAD");
assert!(paths.contains(&head));
assert_ne!(head, worktree.join(".git/HEAD"));
assert!(paths.contains(&git_path(&worktree, "refs/tags")));
assert!(paths.iter().all(|path| path.exists()));
}
#[test]
fn ignores_source_tree_without_git_metadata() {
let temp = tempfile::tempdir().expect("create temp directory");
assert!(git::watch_paths(temp.path()).is_empty());
}