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,
ids::stable_hash64,
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,
},
};
struct PersistedSnapshotIdentity {
resolved_commit_sha: String,
tree_hash: String,
base_resolved_commit_sha: Option<String>,
}
#[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 identity = PersistedSnapshotIdentity {
resolved_commit_sha: snapshot.resolved_commit_sha.clone(),
tree_hash: snapshot.tree_hash.clone(),
base_resolved_commit_sha: None,
};
build_full_snapshot_from_scoped_source(registration, snapshot, identity, workspace_detection)
}
fn build_full_snapshot_with_identity(
registration: &CodeRepositoryRegistration,
selector: &CodeRepositorySelector,
root: &Path,
source_ref_selector: &str,
identity: PersistedSnapshotIdentity,
workspace_detection: &CodeWorkspaceDetectionConfig,
) -> Result<CodeIndexSnapshot, CodeIndexError> {
let snapshot = scoped_source_snapshot(registration, selector, root, source_ref_selector)?;
build_full_snapshot_from_scoped_source(registration, snapshot, identity, workspace_detection)
}
pub(super) fn build_full_snapshot_as_worktree_overlay(
registration: &CodeRepositoryRegistration,
selector: &CodeRepositorySelector,
root: &Path,
source_ref_selector: &str,
base_commit: &str,
workspace_detection: &CodeWorkspaceDetectionConfig,
) -> Result<CodeIndexSnapshot, CodeIndexError> {
let overlay_hash = clean_worktree_overlay_hash(base_commit);
let identity = PersistedSnapshotIdentity {
resolved_commit_sha: format!("worktree:{base_commit}:{overlay_hash}"),
tree_hash: format!("worktree:{overlay_hash}"),
base_resolved_commit_sha: Some(base_commit.to_owned()),
};
build_full_snapshot_with_identity(
registration,
selector,
root,
source_ref_selector,
identity,
workspace_detection,
)
}
pub(crate) fn clean_worktree_overlay_hash(base_commit: &str) -> String {
let mut input = Vec::new();
append_hash_part(&mut input, "clean-worktree-overlay");
append_hash_part(&mut input, base_commit);
format!("{:016x}", stable_hash64(&input))
}
fn append_hash_part(input: &mut Vec<u8>, value: &str) {
input.extend_from_slice(&(value.len() as u64).to_le_bytes());
input.extend_from_slice(value.as_bytes());
}
fn build_full_snapshot_from_scoped_source(
registration: &CodeRepositoryRegistration,
snapshot: ScopedSourceSnapshot,
identity: PersistedSnapshotIdentity,
workspace_detection: &CodeWorkspaceDetectionConfig,
) -> Result<CodeIndexSnapshot, CodeIndexError> {
let filesystem_path_hashes = filesystem_full_snapshot_path_hashes(&snapshot)?;
let source_commit = snapshot.resolved_commit_sha.clone();
let mut build = SnapshotBuild::new_with_scope_filters(
registration,
identity.resolved_commit_sha,
identity.tree_hash,
SnapshotScopeFilters {
path_filters: snapshot.path_filters.clone(),
language_filters: snapshot.language_filters.clone(),
},
true,
snapshot.entries.len(),
0,
);
build.base_resolved_commit_sha = identity.base_resolved_commit_sha;
#[cfg(test)]
apply_filesystem_full_snapshot_read_mutation(&snapshot)?;
build.detect_and_fill_workspaces_at_commit(
&snapshot.root,
snapshot.kind,
&source_commit,
&snapshot.entries,
workspace_detection,
);
for entry in snapshot.entries {
let bytes =
source_snapshot_bytes(&snapshot.root, snapshot.kind, &source_commit, &entry.path)?;
ensure_filesystem_blobs_match_content_hashes(
&source_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)]
#[path = "mod_tests.rs"]
mod tests;