huskies: merge 978

This commit is contained in:
dave
2026-05-13 13:45:16 +00:00
parent 6fc6c9fcb2
commit 51aa649ce4
3 changed files with 104 additions and 10 deletions
+98 -6
View File
@@ -85,10 +85,14 @@ pub trait LanguageAdapter {
/// Reads the existing map, updates only the entries for the provided files, and
/// writes back. Entries for files not in `passing_files` are preserved unchanged.
/// Running twice with the same input produces identical file content (idempotent).
///
/// When `root` is `Some`, keys are written as paths relative to `root` so the
/// map stays portable across machines and worktree locations.
fn update_source_map(
&self,
passing_files: &[&Path],
source_map_path: &Path,
root: Option<&Path>,
) -> Result<(), String>;
}
@@ -228,7 +232,14 @@ pub fn check_files(files: &[&Path]) -> CheckResult {
///
/// Dispatches each file to the appropriate [`LanguageAdapter`] based on extension.
/// Files with unsupported extensions are silently skipped.
pub fn update_source_map(passing_files: &[&Path], source_map_path: &Path) -> Result<(), String> {
///
/// When `root` is `Some`, keys in the map are written relative to `root` so the
/// map stays portable across machines and worktree locations.
pub fn update_source_map(
passing_files: &[&Path],
source_map_path: &Path,
root: Option<&Path>,
) -> Result<(), String> {
let mut by_ext: HashMap<String, Vec<&Path>> = HashMap::new();
for &file in passing_files {
if let Some(ext) = file.extension().and_then(|e| e.to_str()) {
@@ -237,7 +248,7 @@ pub fn update_source_map(passing_files: &[&Path], source_map_path: &Path) -> Res
}
for (ext, ext_files) in &by_ext {
if let Some(adapter) = adapter_for_ext(ext) {
adapter.update_source_map(ext_files, source_map_path)?;
adapter.update_source_map(ext_files, source_map_path, root)?;
}
}
Ok(())
@@ -295,7 +306,20 @@ pub fn update_for_worktree(
std::fs::create_dir_all(parent).map_err(|e| format!("create_dir_all: {e}"))?;
}
update_source_map(&passing, source_map_path)
update_source_map(&passing, source_map_path, Some(worktree_path))
}
/// Compute the map key for a file, stripping `root` when present.
///
/// Returns a root-relative path string when `root` is `Some` and the file is
/// under that root; falls back to the file's own path string otherwise.
pub(crate) fn relative_key(file: &Path, root: Option<&Path>) -> String {
if let Some(r) = root
&& let Ok(rel) = file.strip_prefix(r)
{
return rel.to_string_lossy().to_string();
}
file.to_string_lossy().to_string()
}
/// Read the existing source map from `path` as a JSON object.
@@ -444,10 +468,10 @@ mod tests {
let map_path = tmp.path().join("source-map.json");
let files: &[&Path] = &[&rs_path];
update_source_map(files, &map_path).unwrap();
update_source_map(files, &map_path, None).unwrap();
let first = std::fs::read_to_string(&map_path).unwrap();
update_source_map(files, &map_path).unwrap();
update_source_map(files, &map_path, None).unwrap();
let second = std::fs::read_to_string(&map_path).unwrap();
assert_eq!(first, second, "update_source_map must be idempotent");
@@ -468,7 +492,7 @@ mod tests {
"new.rs",
"//! Module doc.\n\n/// A function.\npub fn bar() {}\n",
);
update_source_map(&[&rs_path], &map_path).unwrap();
update_source_map(&[&rs_path], &map_path, None).unwrap();
let content = std::fs::read_to_string(&map_path).unwrap();
assert!(
@@ -736,4 +760,72 @@ mod tests {
"map must list the documented function"
);
}
/// AC2/AC3: keys written by `update_for_worktree` are project-root-relative,
/// not absolute paths into the worktree directory.
#[test]
fn update_for_worktree_writes_relative_keys() {
let tmp = TempDir::new().unwrap();
init_git_repo(tmp.path());
write_rs(
tmp.path(),
"lib.rs",
"//! Module doc.\n\n/// A function.\npub fn greet() {}\n",
);
Command::new("git")
.args(["add", "lib.rs"])
.current_dir(tmp.path())
.output()
.expect("git add");
Command::new("git")
.args(["commit", "-m", "add lib.rs"])
.current_dir(tmp.path())
.output()
.expect("git commit");
let huskies_dir = tmp.path().join(".huskies");
std::fs::create_dir_all(&huskies_dir).unwrap();
let map_path = huskies_dir.join("source-map.json");
update_for_worktree(tmp.path(), "HEAD~1", &map_path).unwrap();
let content = std::fs::read_to_string(&map_path).unwrap();
let map: serde_json::Value = serde_json::from_str(&content).unwrap();
let obj = map.as_object().unwrap();
// Every key must be relative — no absolute path prefix.
for key in obj.keys() {
assert!(
!key.starts_with('/'),
"key must be relative, got absolute path: {key}"
);
assert!(
!key.contains("/.huskies/worktrees/"),
"key must not contain worktree path infix: {key}"
);
}
// The key for lib.rs must be exactly "lib.rs".
assert!(
obj.contains_key("lib.rs"),
"expected key 'lib.rs', got keys: {:?}",
obj.keys().collect::<Vec<_>>()
);
}
/// `relative_key` strips the root prefix from an absolute path.
#[test]
fn relative_key_strips_root_prefix() {
let root = Path::new("/workspace/.huskies/worktrees/978");
let file = Path::new("/workspace/.huskies/worktrees/978/server/src/foo.rs");
assert_eq!(relative_key(file, Some(root)), "server/src/foo.rs");
}
/// `relative_key` falls back to the full path when root is `None`.
#[test]
fn relative_key_none_root_returns_full_path() {
let file = Path::new("/absolute/path/foo.rs");
assert_eq!(relative_key(file, None), "/absolute/path/foo.rs");
}
}
+3 -2
View File
@@ -8,7 +8,7 @@
use std::fs;
use std::path::Path;
use crate::{CheckFailure, CheckResult, LanguageAdapter};
use crate::{CheckFailure, CheckResult, LanguageAdapter, relative_key};
/// Rust documentation coverage adapter.
pub struct RustAdapter;
@@ -79,10 +79,11 @@ impl LanguageAdapter for RustAdapter {
&self,
passing_files: &[&Path],
source_map_path: &Path,
root: Option<&Path>,
) -> Result<(), String> {
let mut map = crate::read_map(source_map_path)?;
for &file in passing_files {
let key = file.to_string_lossy().to_string();
let key = relative_key(file, root);
let items: Vec<serde_json::Value> = Self::extract_items(file)
.into_iter()
.map(serde_json::Value::String)
+3 -2
View File
@@ -9,7 +9,7 @@
use std::fs;
use std::path::Path;
use crate::{CheckFailure, CheckResult, LanguageAdapter};
use crate::{CheckFailure, CheckResult, LanguageAdapter, relative_key};
/// TypeScript documentation coverage adapter.
pub struct TypeScriptAdapter;
@@ -80,10 +80,11 @@ impl LanguageAdapter for TypeScriptAdapter {
&self,
passing_files: &[&Path],
source_map_path: &Path,
root: Option<&Path>,
) -> Result<(), String> {
let mut map = crate::read_map(source_map_path)?;
for &file in passing_files {
let key = file.to_string_lossy().to_string();
let key = relative_key(file, root);
let items: Vec<serde_json::Value> = Self::extract_items(file)
.into_iter()
.map(serde_json::Value::String)