0.0.2-alpha.3

This commit is contained in:
2026-09-28 19:23:12 +02:00
parent 38025266b9
commit 721e8a6d9c
18 changed files with 1103 additions and 38 deletions

View File

@@ -1,5 +1,5 @@
// file: crates/knowledge-git-lib/src/backend.rs
// version: 2
// version: 3
//! Physical source configuration, generated-state layout and controlled Git process runner.
@@ -200,21 +200,22 @@ impl GitCommandRunner for SystemGitRunner {
}
}
/// Controlled Git backend that owns local mirror initialization and inspection.
/// Controlled Git backend that owns configured sources and local read materialization.
pub struct GitBackend {
config: crate::GitBackendConfig,
runner: Box<dyn GitCommandRunner>,
sources: Vec<crate::GitSourceConfig>,
}
impl crate::GitBackend {
/// Create a backend using the configured real Git executable.
/// Create a backend using the configured real Git executable and no registered source yet.
pub fn new(config: crate::GitBackendConfig) -> crate::GitBackend {
return crate::GitBackend { config, runner: Box::new(SystemGitRunner) };
return crate::GitBackend { config, runner: Box::new(SystemGitRunner), sources: Vec::new() };
}
#[cfg(test)]
fn with_runner(config: crate::GitBackendConfig, runner: Box<dyn GitCommandRunner>) -> crate::GitBackend {
return crate::GitBackend { config, runner };
return crate::GitBackend { config, runner, sources: Vec::new() };
}
/// Return the immutable backend configuration.
@@ -222,12 +223,36 @@ impl crate::GitBackend {
return &self.config;
}
/// Register one physical source configuration for provider-neutral operations.
pub fn register_source(&mut self, source: crate::GitSourceConfig) -> Result<(), crate::GitBackendError> {
for existing in &self.sources {
if existing.source_id() == source.source_id() {
return Err(crate::GitBackendError::DuplicateSource);
}
}
self.sources.push(source);
return Ok(());
}
/// Derive the controlled mirror path without interpolating the raw source identifier.
pub fn repository_path(&self, source_id: &knowledge_domain_lib::SourceId) -> std::path::PathBuf {
let source_key = crate::GitSourceKey::from_source_id(source_id);
return self.config.repositories_path().join(format!("{}.git", source_key.as_str()));
}
/// Derive the revision-addressed snapshot path after validating the revision as a safe object identity.
pub fn snapshot_path(
&self,
source_id: &knowledge_domain_lib::SourceId,
revision: &knowledge_git_api::GitRevision,
) -> Result<std::path::PathBuf, crate::GitBackendError> {
if !is_safe_revision(revision.as_str()) {
return Err(crate::GitBackendError::InvalidRevision);
}
let source_key = crate::GitSourceKey::from_source_id(source_id);
return Ok(self.config.snapshots_path().join(source_key.as_str()).join(revision.as_str()));
}
/// Inspect one mirror when it already exists without creating it.
pub fn inspect_mirror(&self, source_id: &knowledge_domain_lib::SourceId) -> Result<Option<crate::GitMirror>, crate::GitBackendError> {
let source_key = crate::GitSourceKey::from_source_id(source_id);
@@ -276,6 +301,15 @@ impl crate::GitBackend {
}
}
fn configured_source(&self, source_id: &knowledge_domain_lib::SourceId) -> Result<&crate::GitSourceConfig, crate::GitBackendError> {
for source in &self.sources {
if source.source_id() == source_id {
return Ok(source);
}
}
return Err(crate::GitBackendError::SourceNotConfigured);
}
fn prepare_state_directories(&self) -> Result<(), crate::GitBackendError> {
for path in [self.config.hooks_path(), self.config.repositories_path(), self.config.snapshots_path()] {
let create_result = std::fs::create_dir_all(path);
@@ -336,6 +370,238 @@ impl crate::GitBackend {
}
return Ok(());
}
fn refresh_registered_source(&self, source_id: &knowledge_domain_lib::SourceId) -> Result<(), crate::GitBackendError> {
let source_result = self.configured_source(source_id);
let source = match source_result {
Ok(value) => value,
Err(error) => return Err(error),
};
let mirror_result = self.ensure_mirror(source);
let mirror = match mirror_result {
Ok(value) => value,
Err(error) => return Err(error),
};
let specification = GitCommandSpec {
arguments: vec![
std::ffi::OsString::from("--git-dir"),
mirror.repository_path().as_os_str().to_os_string(),
std::ffi::OsString::from("remote"),
std::ffi::OsString::from("update"),
std::ffi::OsString::from("--prune"),
],
executable: self.config.git_executable().to_path_buf(),
hooks_path: self.config.hooks_path(),
operation: crate::GitOperation::RefreshMirror,
};
let run_result = self.runner.run(&specification);
match run_result {
Ok(_) => return Ok(()),
Err(error) => return Err(error),
}
}
fn resolve_registered_revision(
&self,
source_id: &knowledge_domain_lib::SourceId,
reference: &knowledge_git_api::GitReference,
) -> Result<knowledge_git_api::GitRevision, crate::GitBackendError> {
let configured_result = self.configured_source(source_id);
match configured_result {
Ok(_) => (),
Err(error) => return Err(error),
}
let mirror_result = self.inspect_mirror(source_id);
let mirror = match mirror_result {
Ok(Some(value)) => value,
Ok(None) => return Err(crate::GitBackendError::MirrorMissing),
Err(error) => return Err(error),
};
let revision_expression = format!("{}^{{commit}}", reference.as_str());
let specification = GitCommandSpec {
arguments: vec![
std::ffi::OsString::from("--git-dir"),
mirror.repository_path().as_os_str().to_os_string(),
std::ffi::OsString::from("rev-parse"),
std::ffi::OsString::from("--verify"),
std::ffi::OsString::from("--end-of-options"),
std::ffi::OsString::from(revision_expression),
],
executable: self.config.git_executable().to_path_buf(),
hooks_path: self.config.hooks_path(),
operation: crate::GitOperation::ResolveRevision,
};
let run_result = self.runner.run(&specification);
let output = match run_result {
Ok(value) => value,
Err(error) => return Err(error),
};
let stdout_result = std::str::from_utf8(&output.stdout);
let stdout = match stdout_result {
Ok(value) => value.trim(),
Err(_) => {
return Err(crate::GitBackendError::UnexpectedGitOutput { operation: crate::GitOperation::ResolveRevision });
},
};
if !is_safe_revision(stdout) {
return Err(crate::GitBackendError::UnexpectedGitOutput { operation: crate::GitOperation::ResolveRevision });
}
return Ok(knowledge_git_api::GitRevision::new(stdout));
}
fn prepare_registered_snapshot(
&self,
source_id: &knowledge_domain_lib::SourceId,
revision: &knowledge_git_api::GitRevision,
) -> Result<knowledge_git_api::GitSnapshot, crate::GitBackendError> {
let configured_result = self.configured_source(source_id);
match configured_result {
Ok(_) => (),
Err(error) => return Err(error),
}
if !is_safe_revision(revision.as_str()) {
return Err(crate::GitBackendError::InvalidRevision);
}
let mirror_result = self.inspect_mirror(source_id);
let mirror = match mirror_result {
Ok(Some(value)) => value,
Ok(None) => return Err(crate::GitBackendError::MirrorMissing),
Err(error) => return Err(error),
};
let snapshot_path_result = self.snapshot_path(source_id, revision);
let snapshot_path = match snapshot_path_result {
Ok(value) => value,
Err(error) => return Err(error),
};
if snapshot_path.exists() {
let validation_result = self.validate_snapshot(&snapshot_path, revision);
match validation_result {
Ok(()) => return Ok(knowledge_git_api::GitSnapshot::new(revision.clone(), snapshot_path)),
Err(error) => return Err(error),
}
}
let parent = match snapshot_path.parent() {
Some(value) => value,
None => return Err(crate::GitBackendError::StatePreparationFailed),
};
let create_result = std::fs::create_dir_all(parent);
if create_result.is_err() {
return Err(crate::GitBackendError::StatePreparationFailed);
}
let specification = GitCommandSpec {
arguments: vec![
std::ffi::OsString::from("--git-dir"),
mirror.repository_path().as_os_str().to_os_string(),
std::ffi::OsString::from("worktree"),
std::ffi::OsString::from("add"),
std::ffi::OsString::from("--detach"),
std::ffi::OsString::from("--"),
snapshot_path.as_os_str().to_os_string(),
std::ffi::OsString::from(revision.as_str()),
],
executable: self.config.git_executable().to_path_buf(),
hooks_path: self.config.hooks_path(),
operation: crate::GitOperation::CreateSnapshot,
};
let run_result = self.runner.run(&specification);
match run_result {
Ok(_) => (),
Err(error) => return Err(error),
}
let validation_result = self.validate_snapshot(&snapshot_path, revision);
match validation_result {
Ok(()) => return Ok(knowledge_git_api::GitSnapshot::new(revision.clone(), snapshot_path)),
Err(error) => return Err(error),
}
}
fn validate_snapshot(&self, snapshot_path: &std::path::Path, revision: &knowledge_git_api::GitRevision) -> Result<(), crate::GitBackendError> {
if !snapshot_path.is_dir() {
return Err(crate::GitBackendError::InvalidSnapshot);
}
let head_specification = GitCommandSpec {
arguments: vec![
std::ffi::OsString::from("-C"),
snapshot_path.as_os_str().to_os_string(),
std::ffi::OsString::from("rev-parse"),
std::ffi::OsString::from("--verify"),
std::ffi::OsString::from("HEAD"),
],
executable: self.config.git_executable().to_path_buf(),
hooks_path: self.config.hooks_path(),
operation: crate::GitOperation::InspectSnapshot,
};
let head_result = self.runner.run(&head_specification);
let head_output = match head_result {
Ok(value) => value,
Err(_) => return Err(crate::GitBackendError::InvalidSnapshot),
};
let head_text_result = std::str::from_utf8(&head_output.stdout);
let head_text = match head_text_result {
Ok(value) => value.trim(),
Err(_) => return Err(crate::GitBackendError::InvalidSnapshot),
};
if head_text != revision.as_str() {
return Err(crate::GitBackendError::InvalidSnapshot);
}
let status_specification = GitCommandSpec {
arguments: vec![
std::ffi::OsString::from("-C"),
snapshot_path.as_os_str().to_os_string(),
std::ffi::OsString::from("status"),
std::ffi::OsString::from("--porcelain=v1"),
std::ffi::OsString::from("--untracked-files=all"),
],
executable: self.config.git_executable().to_path_buf(),
hooks_path: self.config.hooks_path(),
operation: crate::GitOperation::InspectSnapshot,
};
let status_result = self.runner.run(&status_specification);
let status_output = match status_result {
Ok(value) => value,
Err(_) => return Err(crate::GitBackendError::InvalidSnapshot),
};
if !status_output.stdout.is_empty() {
return Err(crate::GitBackendError::InvalidSnapshot);
}
return Ok(());
}
}
impl knowledge_git_api::GitRepository for crate::GitBackend {
type Error = crate::GitBackendError;
fn refresh(&mut self, source_id: &knowledge_domain_lib::SourceId) -> Result<(), Self::Error> {
return self.refresh_registered_source(source_id);
}
fn resolve_revision(
&self,
source_id: &knowledge_domain_lib::SourceId,
reference: &knowledge_git_api::GitReference,
) -> Result<knowledge_git_api::GitRevision, Self::Error> {
return self.resolve_registered_revision(source_id, reference);
}
fn prepare_snapshot(
&self,
source_id: &knowledge_domain_lib::SourceId,
revision: &knowledge_git_api::GitRevision,
) -> Result<knowledge_git_api::GitSnapshot, Self::Error> {
return self.prepare_registered_snapshot(source_id, revision);
}
}
fn is_safe_revision(value: &str) -> bool {
if value.is_empty() {
return false;
}
for character in value.chars() {
if !character.is_ascii_hexdigit() {
return false;
}
}
return true;
}
#[cfg(test)]