relay-knowledge 1.1.10

Graph-database-based knowledge graph project.
Documentation
use std::{collections::BTreeMap, path::Path};

#[cfg(test)]
use std::{fs, path::PathBuf, sync::Mutex};

use crate::domain::{
    CodeIndexSnapshot, CodeRepositoryRegistration, CodeRepositorySelector,
    CodeWorkspaceDetectionConfig,
};

use super::{
    CodeIndexError,
    parser::parse_indexed_file,
    scope::{ScopedSourceSnapshot, scoped_source_snapshot},
    snapshot::{SnapshotBuild, SnapshotScopeFilters},
    source::{
        ensure_filesystem_blobs_match_content_hashes, filesystem_content_hashes_for_paths,
        filesystem_tree_hash_from_path_hashes, source_snapshot_bytes,
    },
};

#[cfg(test)]
struct FullSnapshotReadMutation {
    root: PathBuf,
    path: String,
    content: Vec<u8>,
}

#[cfg(test)]
static FULL_SNAPSHOT_READ_MUTATION: Mutex<Option<FullSnapshotReadMutation>> = Mutex::new(None);

#[cfg(test)]
pub(crate) fn mutate_next_filesystem_full_snapshot_read(root: PathBuf, path: &str, content: &[u8]) {
    *FULL_SNAPSHOT_READ_MUTATION
        .lock()
        .expect("full snapshot mutation should lock") = Some(FullSnapshotReadMutation {
        root,
        path: path.to_owned(),
        content: content.to_vec(),
    });
}

pub(super) fn build_full_snapshot(
    registration: &CodeRepositoryRegistration,
    selector: &CodeRepositorySelector,
    root: &Path,
    workspace_detection: &CodeWorkspaceDetectionConfig,
) -> Result<CodeIndexSnapshot, CodeIndexError> {
    let snapshot = scoped_source_snapshot(registration, selector, root, &selector.ref_selector)?;
    let filesystem_path_hashes = filesystem_full_snapshot_path_hashes(&snapshot)?;
    let mut build = SnapshotBuild::new_with_scope_filters(
        registration,
        snapshot.resolved_commit_sha.clone(),
        snapshot.tree_hash.clone(),
        SnapshotScopeFilters {
            path_filters: snapshot.path_filters.clone(),
            language_filters: snapshot.language_filters.clone(),
        },
        true,
        snapshot.entries.len(),
        0,
    );

    #[cfg(test)]
    apply_filesystem_full_snapshot_read_mutation(&snapshot)?;

    build.detect_and_fill_workspaces(
        &snapshot.root,
        snapshot.kind,
        &snapshot.entries,
        workspace_detection,
    );

    for entry in snapshot.entries {
        let bytes =
            source_snapshot_bytes(&snapshot.root, snapshot.kind, &build.commit, &entry.path)?;
        ensure_filesystem_blobs_match_content_hashes(
            &build.commit,
            std::slice::from_ref(&entry.path),
            std::slice::from_ref(&bytes),
            &filesystem_path_hashes,
        )?;
        parse_indexed_file(&mut build, &entry.path, &bytes)?;
    }

    Ok(build.finish())
}

fn filesystem_full_snapshot_path_hashes(
    snapshot: &ScopedSourceSnapshot,
) -> Result<BTreeMap<String, String>, CodeIndexError> {
    if !snapshot.kind.is_filesystem() {
        return Ok(BTreeMap::new());
    }
    let paths = snapshot
        .entries
        .iter()
        .map(|entry| entry.path.clone())
        .collect::<Vec<_>>();
    let path_hashes = filesystem_content_hashes_for_paths(&snapshot.root, &paths)?;
    let tree_hash = filesystem_tree_hash_from_path_hashes(&path_hashes);
    if tree_hash != snapshot.tree_hash {
        return Err(CodeIndexError::InvalidInput(format!(
            "filesystem source snapshot {} no longer matches planned filesystem content {tree_hash}",
            snapshot.resolved_commit_sha
        )));
    }

    Ok(path_hashes)
}

#[cfg(test)]
fn apply_filesystem_full_snapshot_read_mutation(
    snapshot: &ScopedSourceSnapshot,
) -> Result<(), CodeIndexError> {
    if !snapshot.kind.is_filesystem() {
        return Ok(());
    }
    let mut mutation = FULL_SNAPSHOT_READ_MUTATION
        .lock()
        .expect("full snapshot mutation should lock");
    let Some(next) = mutation.take() else {
        return Ok(());
    };
    if next.root != snapshot.root {
        *mutation = Some(next);
        return Ok(());
    }
    fs::write(snapshot.root.join(next.path), next.content).map_err(CodeIndexError::Io)
}

#[cfg(test)]
mod tests {
    use std::{
        path::PathBuf,
        process::Command,
        time::{SystemTime, UNIX_EPOCH},
    };

    use crate::domain::CodeMonorepoWorkspaceFormat;

    use super::*;

    #[test]
    fn workspace_detection_reads_manifests_from_indexed_ref() {
        let repo = TestRepo::create("workspace-indexed-ref");
        repo.write("pnpm-workspace.yaml", "packages:\n  - 'packages/*'\n");
        repo.write(
            "packages/core/package.json",
            "{\n  \"name\": \"@scope/core\",\n  \"version\": \"1.0.0\"\n}\n",
        );
        repo.write(
            "packages/core/src/index.ts",
            "export const core = 'committed';\n",
        );
        repo.git(["add", "."]);
        repo.git(["commit", "-m", "initial"]);
        repo.write(
            "packages/core/package.json",
            "{\n  \"name\": \"@scope/worktree-core\",\n  \"version\": \"1.0.0\"\n}\n",
        );

        let registration = CodeRepositoryRegistration::new(
            "repo",
            "fixture",
            repo.path.to_string_lossy().into_owned(),
            Vec::new(),
            Vec::new(),
        )
        .expect("registration");
        let selector =
            CodeRepositorySelector::new("fixture", "HEAD", Vec::new(), Vec::new()).expect("ref");

        let snapshot = build_full_snapshot(
            &registration,
            &selector,
            &repo.path,
            &CodeWorkspaceDetectionConfig::enabled_all(),
        )
        .expect("snapshot");

        let workspace = snapshot
            .workspaces
            .iter()
            .find(|workspace| workspace.format == CodeMonorepoWorkspaceFormat::Pnpm)
            .expect("pnpm workspace");
        let package_names = workspace
            .members
            .iter()
            .map(|member| member.package_name.as_str())
            .collect::<Vec<_>>();

        assert!(package_names.contains(&"@scope/core"));
        assert!(!package_names.contains(&"@scope/worktree-core"));
    }

    #[test]
    fn workspace_detection_reads_only_indexed_scope_entries() {
        let repo = TestRepo::create("workspace-indexed-scope");
        repo.write("pnpm-workspace.yaml", "packages:\n  - 'packages/*'\n");
        repo.write("packages/core/package.json", "{\"name\":\"@scope/core\"}\n");
        repo.write("packages/core/src/index.ts", "export const core = 1;\n");
        repo.write("src/app.ts", "export const app = 1;\n");
        repo.git(["add", "."]);
        repo.git(["commit", "-m", "initial"]);

        let registration = CodeRepositoryRegistration::new(
            "repo",
            "fixture",
            repo.path.to_string_lossy().into_owned(),
            Vec::new(),
            Vec::new(),
        )
        .expect("registration");
        let selector =
            CodeRepositorySelector::new("fixture", "HEAD", vec!["src".to_owned()], Vec::new())
                .expect("ref");

        let snapshot = build_full_snapshot(
            &registration,
            &selector,
            &repo.path,
            &CodeWorkspaceDetectionConfig::enabled_all(),
        )
        .expect("snapshot");

        assert!(snapshot.workspaces.is_empty());
    }

    #[test]
    fn workspace_detection_reads_manifests_for_language_filtered_scope() {
        let repo = TestRepo::create("workspace-language-scope");
        repo.write("pnpm-workspace.yaml", "packages:\n  - 'packages/*'\n");
        repo.write("packages/core/package.json", "{\"name\":\"@scope/core\"}\n");
        repo.write("packages/core/src/index.ts", "export const core = 1;\n");
        repo.git(["add", "."]);
        repo.git(["commit", "-m", "initial"]);

        let registration = CodeRepositoryRegistration::new(
            "repo",
            "fixture",
            repo.path.to_string_lossy().into_owned(),
            Vec::new(),
            Vec::new(),
        )
        .expect("registration");
        let selector = CodeRepositorySelector::new(
            "fixture",
            "HEAD",
            Vec::new(),
            vec!["typescript".to_owned()],
        )
        .expect("ref");

        let snapshot = build_full_snapshot(
            &registration,
            &selector,
            &repo.path,
            &CodeWorkspaceDetectionConfig::enabled_all(),
        )
        .expect("snapshot");

        let package_names = snapshot
            .workspaces
            .iter()
            .flat_map(|workspace| workspace.members.iter())
            .map(|member| member.package_name.as_str())
            .collect::<Vec<_>>();

        assert!(package_names.contains(&"@scope/core"));
    }

    struct TestRepo {
        path: PathBuf,
    }

    impl TestRepo {
        fn create(name: &str) -> Self {
            let millis = SystemTime::now()
                .duration_since(UNIX_EPOCH)
                .unwrap_or_default()
                .as_millis();
            let path = std::env::temp_dir().join(format!("rk-{name}-{millis}"));
            let _ = std::fs::remove_dir_all(&path);
            std::fs::create_dir_all(&path).expect("create repo dir");
            let repo = Self { path };
            repo.git(["init"]);
            repo.git(["config", "user.email", "codex@example.invalid"]);
            repo.git(["config", "user.name", "Codex"]);
            repo
        }

        fn write(&self, relative_path: &str, content: &str) {
            let path = self.path.join(relative_path);
            if let Some(parent) = path.parent() {
                std::fs::create_dir_all(parent).expect("create parent");
            }
            std::fs::write(path, content).expect("write fixture file");
        }

        fn git<const N: usize>(&self, args: [&str; N]) {
            let output = Command::new("git")
                .args(args)
                .current_dir(&self.path)
                .output()
                .expect("run git");
            assert!(
                output.status.success(),
                "git failed: {}",
                String::from_utf8_lossy(&output.stderr)
            );
        }
    }

    impl Drop for TestRepo {
        fn drop(&mut self) {
            let _ = std::fs::remove_dir_all(&self.path);
        }
    }
}