0.0.2-alpha.3
This commit is contained in:
@@ -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)]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/knowledge-git-lib/src/error.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! Recoverable errors for controlled Git process execution.
|
||||
|
||||
@@ -10,6 +10,14 @@ pub enum GitOperation {
|
||||
CloneMirror,
|
||||
/// Inspect whether a controlled repository is a valid bare mirror.
|
||||
InspectMirror,
|
||||
/// Refresh refs in one controlled mirror.
|
||||
RefreshMirror,
|
||||
/// Resolve one configured reference to a commit revision.
|
||||
ResolveRevision,
|
||||
/// Materialize one detached revision-addressed snapshot.
|
||||
CreateSnapshot,
|
||||
/// Validate an existing detached snapshot before reuse.
|
||||
InspectSnapshot,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for crate::GitOperation {
|
||||
@@ -17,6 +25,10 @@ impl std::fmt::Display for crate::GitOperation {
|
||||
match self {
|
||||
crate::GitOperation::CloneMirror => return formatter.write_str("clone mirror"),
|
||||
crate::GitOperation::InspectMirror => return formatter.write_str("inspect mirror"),
|
||||
crate::GitOperation::RefreshMirror => return formatter.write_str("refresh mirror"),
|
||||
crate::GitOperation::ResolveRevision => return formatter.write_str("resolve revision"),
|
||||
crate::GitOperation::CreateSnapshot => return formatter.write_str("create snapshot"),
|
||||
crate::GitOperation::InspectSnapshot => return formatter.write_str("inspect snapshot"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,6 +45,10 @@ pub enum GitBackendError {
|
||||
EmptyStateRoot,
|
||||
/// A local source path or remote source string is empty.
|
||||
EmptySourceLocation,
|
||||
/// One logical source identifier was registered more than once in the same backend.
|
||||
DuplicateSource,
|
||||
/// No physical source configuration is registered for the requested logical source.
|
||||
SourceNotConfigured,
|
||||
/// The backend could not prepare its generated-state directories.
|
||||
StatePreparationFailed,
|
||||
/// The Git executable could not be started for the requested operation.
|
||||
@@ -49,6 +65,12 @@ pub enum GitBackendError {
|
||||
},
|
||||
/// An existing controlled repository is not a valid bare mirror.
|
||||
InvalidMirror,
|
||||
/// A controlled mirror required by a read operation does not exist yet.
|
||||
MirrorMissing,
|
||||
/// A revision supplied to snapshot materialization is not a safe Git object identity.
|
||||
InvalidRevision,
|
||||
/// An existing snapshot does not match the requested revision or is not clean.
|
||||
InvalidSnapshot,
|
||||
/// Git returned output that does not satisfy the backend contract.
|
||||
UnexpectedGitOutput {
|
||||
/// Operation whose output could not be validated.
|
||||
@@ -62,12 +84,17 @@ impl std::fmt::Display for crate::GitBackendError {
|
||||
crate::GitBackendError::EmptyGitExecutable => return formatter.write_str("git executable must not be empty"),
|
||||
crate::GitBackendError::EmptyStateRoot => return formatter.write_str("git state root must not be empty"),
|
||||
crate::GitBackendError::EmptySourceLocation => return formatter.write_str("git source location must not be empty"),
|
||||
crate::GitBackendError::DuplicateSource => return formatter.write_str("git source identifier is already configured"),
|
||||
crate::GitBackendError::SourceNotConfigured => return formatter.write_str("git source identifier is not configured"),
|
||||
crate::GitBackendError::StatePreparationFailed => return formatter.write_str("git state directories could not be prepared"),
|
||||
crate::GitBackendError::GitUnavailable { operation } => return write!(formatter, "git is unavailable while attempting to {operation}"),
|
||||
crate::GitBackendError::GitCommandFailed { operation, exit_code } => {
|
||||
return write!(formatter, "git failed while attempting to {operation}; exit code {exit_code:?}");
|
||||
},
|
||||
crate::GitBackendError::InvalidMirror => return formatter.write_str("controlled git repository is not a valid bare mirror"),
|
||||
crate::GitBackendError::MirrorMissing => return formatter.write_str("controlled git mirror does not exist"),
|
||||
crate::GitBackendError::InvalidRevision => return formatter.write_str("git revision is not a safe object identity"),
|
||||
crate::GitBackendError::InvalidSnapshot => return formatter.write_str("controlled git snapshot is invalid or dirty"),
|
||||
crate::GitBackendError::UnexpectedGitOutput { operation } => {
|
||||
return write!(formatter, "git returned unexpected output while attempting to {operation}");
|
||||
},
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
// file: crates/knowledge-git-lib/src/lib.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
//! Controlled Git process backend for Knowledge content sources.
|
||||
//!
|
||||
//! This crate owns physical Git source configuration, generated-state layout and process execution.
|
||||
//! It deliberately contains no forge API, persistence, scheduler or application orchestration.
|
||||
//! This crate owns physical Git source configuration, generated-state layout, provider-neutral read
|
||||
//! materialization and process execution. It deliberately contains no forge API, persistence,
|
||||
//! scheduler or application orchestration.
|
||||
|
||||
mod backend;
|
||||
mod error;
|
||||
|
||||
/// Controlled Git backend that owns local mirror initialization and inspection.
|
||||
/// Controlled Git backend that owns configured sources and local read materialization.
|
||||
pub use self::backend::GitBackend;
|
||||
/// Explicit physical configuration for one controlled Git backend instance.
|
||||
pub use self::backend::GitBackendConfig;
|
||||
|
||||
Reference in New Issue
Block a user