huskies: merge 978
This commit is contained in:
@@ -85,10 +85,14 @@ pub trait LanguageAdapter {
|
|||||||
/// Reads the existing map, updates only the entries for the provided files, and
|
/// 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.
|
/// writes back. Entries for files not in `passing_files` are preserved unchanged.
|
||||||
/// Running twice with the same input produces identical file content (idempotent).
|
/// 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(
|
fn update_source_map(
|
||||||
&self,
|
&self,
|
||||||
passing_files: &[&Path],
|
passing_files: &[&Path],
|
||||||
source_map_path: &Path,
|
source_map_path: &Path,
|
||||||
|
root: Option<&Path>,
|
||||||
) -> Result<(), String>;
|
) -> Result<(), String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,7 +232,14 @@ pub fn check_files(files: &[&Path]) -> CheckResult {
|
|||||||
///
|
///
|
||||||
/// Dispatches each file to the appropriate [`LanguageAdapter`] based on extension.
|
/// Dispatches each file to the appropriate [`LanguageAdapter`] based on extension.
|
||||||
/// Files with unsupported extensions are silently skipped.
|
/// 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();
|
let mut by_ext: HashMap<String, Vec<&Path>> = HashMap::new();
|
||||||
for &file in passing_files {
|
for &file in passing_files {
|
||||||
if let Some(ext) = file.extension().and_then(|e| e.to_str()) {
|
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 {
|
for (ext, ext_files) in &by_ext {
|
||||||
if let Some(adapter) = adapter_for_ext(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(())
|
Ok(())
|
||||||
@@ -295,7 +306,20 @@ pub fn update_for_worktree(
|
|||||||
std::fs::create_dir_all(parent).map_err(|e| format!("create_dir_all: {e}"))?;
|
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.
|
/// 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 map_path = tmp.path().join("source-map.json");
|
||||||
let files: &[&Path] = &[&rs_path];
|
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();
|
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();
|
let second = std::fs::read_to_string(&map_path).unwrap();
|
||||||
|
|
||||||
assert_eq!(first, second, "update_source_map must be idempotent");
|
assert_eq!(first, second, "update_source_map must be idempotent");
|
||||||
@@ -468,7 +492,7 @@ mod tests {
|
|||||||
"new.rs",
|
"new.rs",
|
||||||
"//! Module doc.\n\n/// A function.\npub fn bar() {}\n",
|
"//! 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();
|
let content = std::fs::read_to_string(&map_path).unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
@@ -736,4 +760,72 @@ mod tests {
|
|||||||
"map must list the documented function"
|
"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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use crate::{CheckFailure, CheckResult, LanguageAdapter};
|
use crate::{CheckFailure, CheckResult, LanguageAdapter, relative_key};
|
||||||
|
|
||||||
/// Rust documentation coverage adapter.
|
/// Rust documentation coverage adapter.
|
||||||
pub struct RustAdapter;
|
pub struct RustAdapter;
|
||||||
@@ -79,10 +79,11 @@ impl LanguageAdapter for RustAdapter {
|
|||||||
&self,
|
&self,
|
||||||
passing_files: &[&Path],
|
passing_files: &[&Path],
|
||||||
source_map_path: &Path,
|
source_map_path: &Path,
|
||||||
|
root: Option<&Path>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let mut map = crate::read_map(source_map_path)?;
|
let mut map = crate::read_map(source_map_path)?;
|
||||||
for &file in passing_files {
|
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)
|
let items: Vec<serde_json::Value> = Self::extract_items(file)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(serde_json::Value::String)
|
.map(serde_json::Value::String)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use crate::{CheckFailure, CheckResult, LanguageAdapter};
|
use crate::{CheckFailure, CheckResult, LanguageAdapter, relative_key};
|
||||||
|
|
||||||
/// TypeScript documentation coverage adapter.
|
/// TypeScript documentation coverage adapter.
|
||||||
pub struct TypeScriptAdapter;
|
pub struct TypeScriptAdapter;
|
||||||
@@ -80,10 +80,11 @@ impl LanguageAdapter for TypeScriptAdapter {
|
|||||||
&self,
|
&self,
|
||||||
passing_files: &[&Path],
|
passing_files: &[&Path],
|
||||||
source_map_path: &Path,
|
source_map_path: &Path,
|
||||||
|
root: Option<&Path>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let mut map = crate::read_map(source_map_path)?;
|
let mut map = crate::read_map(source_map_path)?;
|
||||||
for &file in passing_files {
|
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)
|
let items: Vec<serde_json::Value> = Self::extract_items(file)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(serde_json::Value::String)
|
.map(serde_json::Value::String)
|
||||||
|
|||||||
Reference in New Issue
Block a user