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-api/src/lib.rs
// version: 2
// version: 3
//! Provider-neutral contracts for Git-backed Knowledge sources.
//!
@@ -38,6 +38,30 @@ impl crate::GitRevision {
}
}
/// Read-only local materialization of one authoritative Git revision.
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GitSnapshot {
path: std::path::PathBuf,
revision: crate::GitRevision,
}
impl crate::GitSnapshot {
/// Build a snapshot contract from a resolved revision and its local read root.
pub fn new(revision: crate::GitRevision, path: std::path::PathBuf) -> crate::GitSnapshot {
return crate::GitSnapshot { path, revision };
}
/// Return the local root containing the materialized repository content.
pub fn path(&self) -> &std::path::Path {
return self.path.as_path();
}
/// Return the authoritative revision represented by the snapshot.
pub fn revision(&self) -> &crate::GitRevision {
return &self.revision;
}
}
/// Provider-neutral operations required from a Git repository backend.
///
/// Implementations may use a local Git executable, a Rust Git library or another generic Git
@@ -51,6 +75,9 @@ pub trait GitRepository {
/// Resolve one opaque reference to the authoritative revision used by synchronization.
fn resolve_revision(&self, source_id: &knowledge_domain_lib::SourceId, reference: &crate::GitReference) -> Result<crate::GitRevision, Self::Error>;
/// Prepare or validate a read-only local snapshot for one resolved revision.
fn prepare_snapshot(&self, source_id: &knowledge_domain_lib::SourceId, revision: &crate::GitRevision) -> Result<crate::GitSnapshot, Self::Error>;
}
#[cfg(test)]

View File

@@ -1,5 +1,5 @@
// file: crates/knowledge-git-api/unit_tests/lib.rs
// version: 1
// version: 2
#[derive(Debug, Eq, PartialEq)]
struct FakeGitError;
@@ -16,6 +16,10 @@ impl crate::GitRepository for FakeGitRepository {
fn resolve_revision(&self, source_id: &knowledge_domain_lib::SourceId, reference: &crate::GitReference) -> Result<crate::GitRevision, Self::Error> {
return Ok(crate::GitRevision::new(&format!("{}:{}", source_id.as_str(), reference.as_str())));
}
fn prepare_snapshot(&self, _source_id: &knowledge_domain_lib::SourceId, revision: &crate::GitRevision) -> Result<crate::GitSnapshot, Self::Error> {
return Ok(crate::GitSnapshot::new(revision.clone(), std::path::PathBuf::from("/controlled/snapshot")));
}
}
#[test]
@@ -31,5 +35,14 @@ fn generic_repository_contract_does_not_require_a_forge() {
assert!(crate::GitRepository::refresh(&mut repository, &source_id).is_ok());
let revision_result = crate::GitRepository::resolve_revision(&repository, &source_id, &reference);
assert_eq!(revision_result, Ok(crate::GitRevision::new("local-pgr:refs/heads/master")));
let revision = match revision_result {
Ok(value) => value,
Err(_) => return,
};
let snapshot_result = crate::GitRepository::prepare_snapshot(&repository, &source_id, &revision);
assert_eq!(
snapshot_result,
Ok(crate::GitSnapshot::new(revision, std::path::PathBuf::from("/controlled/snapshot")))
);
return;
}

View File

@@ -1,5 +1,5 @@
# file: crates/knowledge-git-lib/Cargo.toml
# version: 1
# version: 2
[package]
name = "knowledge-git-lib"
@@ -10,6 +10,7 @@ repository.workspace = true
[dependencies]
knowledge-domain-lib.workspace = true
knowledge-git-api.workspace = true
[lints]
workspace = true

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)]

View File

@@ -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}");
},

View File

@@ -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;

View File

@@ -0,0 +1,198 @@
// file: crates/knowledge-git-lib/tests/git_read_path.rs
// version: 1
fn test_root(label: &str) -> std::path::PathBuf {
let nanos_result = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH);
let nanos = match nanos_result {
Ok(value) => value.as_nanos(),
Err(_) => 0,
};
let mut path = std::env::temp_dir();
path.push(format!("sasedev-knowledge-git-integration-{label}-{}-{nanos}", std::process::id()));
let _ = std::fs::remove_dir_all(&path);
return path;
}
fn run_git(repository: &std::path::Path, arguments: &[&str]) -> bool {
let mut command = std::process::Command::new("git");
command.current_dir(repository);
command.args(arguments);
command.env("GIT_TERMINAL_PROMPT", "0");
let status_result = command.status();
match status_result {
Ok(status) => return status.success(),
Err(_) => return false,
}
}
fn prepare_source(root: &std::path::Path) -> Option<std::path::PathBuf> {
let source = root.join("source");
if std::fs::create_dir_all(&source).is_err() {
return None;
}
if !run_git(&source, &["init", "-b", "main"]) {
return None;
}
if !run_git(&source, &["config", "user.name", "Knowledge Test"]) {
return None;
}
if !run_git(&source, &["config", "user.email", "knowledge@example.invalid"]) {
return None;
}
if !run_git(&source, &["config", "commit.gpgsign", "false"]) {
return None;
}
if std::fs::write(source.join("content.txt"), "first\n").is_err() {
return None;
}
if !run_git(&source, &["add", "content.txt"]) {
return None;
}
if !run_git(&source, &["commit", "-m", "first"]) {
return None;
}
return Some(source);
}
fn source_id() -> Option<knowledge_domain_lib::SourceId> {
let result = knowledge_domain_lib::SourceId::new("integration-source");
match result {
Ok(value) => return Some(value),
Err(_) => return None,
}
}
fn configured_backend(root: &std::path::Path, source: &std::path::Path) -> Option<(knowledge_git_lib::GitBackend, knowledge_domain_lib::SourceId)> {
let source_id_result = source_id();
let source_id = match source_id_result {
Some(value) => value,
None => return None,
};
let config_result = knowledge_git_lib::GitBackendConfig::new(root.join("state"));
let config = match config_result {
Ok(value) => value,
Err(_) => return None,
};
let source_result = knowledge_git_lib::GitSourceConfig::local(source_id.clone(), source.to_path_buf());
let source_config = match source_result {
Ok(value) => value,
Err(_) => return None,
};
let mut backend = knowledge_git_lib::GitBackend::new(config);
if backend.register_source(source_config).is_err() {
return None;
}
return Some((backend, source_id));
}
#[test]
fn real_local_repository_refreshes_resolves_and_materializes_revision_snapshots() {
let root = test_root("refresh-resolve-snapshot");
let source_result = prepare_source(&root);
assert!(source_result.is_some());
let source = match source_result {
Some(value) => value,
None => return,
};
let backend_result = configured_backend(&root, &source);
assert!(backend_result.is_some());
let (mut backend, source_id) = match backend_result {
Some(value) => value,
None => return,
};
let reference = knowledge_git_api::GitReference::new("refs/heads/main");
assert!(knowledge_git_api::GitRepository::refresh(&mut backend, &source_id).is_ok());
let first_revision_result = knowledge_git_api::GitRepository::resolve_revision(&backend, &source_id, &reference);
assert!(first_revision_result.is_ok());
let first_revision = match first_revision_result {
Ok(value) => value,
Err(_) => return,
};
let first_snapshot_result = knowledge_git_api::GitRepository::prepare_snapshot(&backend, &source_id, &first_revision);
assert!(first_snapshot_result.is_ok());
let first_snapshot = match first_snapshot_result {
Ok(value) => value,
Err(_) => return,
};
assert!(first_snapshot.path().is_dir());
let first_content_result = std::fs::read_to_string(first_snapshot.path().join("content.txt"));
assert!(first_content_result.is_ok());
let first_content = match first_content_result {
Ok(value) => value,
Err(_) => return,
};
assert_eq!(first_content, "first\n");
let repeated_snapshot_result = knowledge_git_api::GitRepository::prepare_snapshot(&backend, &source_id, &first_revision);
assert!(repeated_snapshot_result.is_ok());
let repeated_snapshot = match repeated_snapshot_result {
Ok(value) => value,
Err(_) => return,
};
assert_eq!(first_snapshot.path(), repeated_snapshot.path());
assert!(std::fs::write(source.join("content.txt"), "second\n").is_ok());
assert!(run_git(&source, &["add", "content.txt"]));
assert!(run_git(&source, &["commit", "-m", "second"]));
assert!(knowledge_git_api::GitRepository::refresh(&mut backend, &source_id).is_ok());
let second_revision_result = knowledge_git_api::GitRepository::resolve_revision(&backend, &source_id, &reference);
assert!(second_revision_result.is_ok());
let second_revision = match second_revision_result {
Ok(value) => value,
Err(_) => return,
};
assert_ne!(first_revision, second_revision);
let second_snapshot_result = knowledge_git_api::GitRepository::prepare_snapshot(&backend, &source_id, &second_revision);
assert!(second_snapshot_result.is_ok());
let second_snapshot = match second_snapshot_result {
Ok(value) => value,
Err(_) => return,
};
assert_ne!(first_snapshot.path(), second_snapshot.path());
let second_content_result = std::fs::read_to_string(second_snapshot.path().join("content.txt"));
assert!(second_content_result.is_ok());
let second_content = match second_content_result {
Ok(value) => value,
Err(_) => return,
};
assert_eq!(second_content, "second\n");
let _ = std::fs::remove_dir_all(&root);
return;
}
#[test]
fn invalid_reference_and_dirty_existing_snapshot_are_rejected() {
let root = test_root("invalid-and-dirty");
let source_result = prepare_source(&root);
assert!(source_result.is_some());
let source = match source_result {
Some(value) => value,
None => return,
};
let backend_result = configured_backend(&root, &source);
assert!(backend_result.is_some());
let (mut backend, source_id) = match backend_result {
Some(value) => value,
None => return,
};
assert!(knowledge_git_api::GitRepository::refresh(&mut backend, &source_id).is_ok());
let missing_reference = knowledge_git_api::GitReference::new("refs/heads/does-not-exist");
let missing_result = knowledge_git_api::GitRepository::resolve_revision(&backend, &source_id, &missing_reference);
assert!(missing_result.is_err());
let reference = knowledge_git_api::GitReference::new("refs/heads/main");
let revision_result = knowledge_git_api::GitRepository::resolve_revision(&backend, &source_id, &reference);
assert!(revision_result.is_ok());
let revision = match revision_result {
Ok(value) => value,
Err(_) => return,
};
let snapshot_result = knowledge_git_api::GitRepository::prepare_snapshot(&backend, &source_id, &revision);
assert!(snapshot_result.is_ok());
let snapshot = match snapshot_result {
Ok(value) => value,
Err(_) => return,
};
assert!(std::fs::write(snapshot.path().join("content.txt"), "dirty\n").is_ok());
let repeated_result = knowledge_git_api::GitRepository::prepare_snapshot(&backend, &source_id, &revision);
assert_eq!(repeated_result, Err(knowledge_git_lib::GitBackendError::InvalidSnapshot));
let _ = std::fs::remove_dir_all(&root);
return;
}

View File

@@ -1,5 +1,5 @@
// file: crates/knowledge-git-lib/unit_tests/backend.rs
// version: 3
// version: 4
#[derive(Clone)]
struct RecordingRunner {
@@ -72,6 +72,34 @@ fn backend_configuration_requires_non_empty_paths() {
return;
}
#[test]
fn duplicate_source_registration_is_rejected() {
let root = test_root("duplicate-source");
let config_result = crate::GitBackendConfig::new(root.clone());
assert!(config_result.is_ok());
let config = match config_result {
Ok(value) => value,
Err(_) => return,
};
let source_id_result = source_id("pgr");
assert!(source_id_result.is_some());
let source_id = match source_id_result {
Some(value) => value,
None => return,
};
let source_result = crate::GitSourceConfig::local(source_id, std::path::PathBuf::from("/controlled/source"));
assert!(source_result.is_ok());
let source = match source_result {
Ok(value) => value,
Err(_) => return,
};
let mut backend = crate::GitBackend::new(config);
assert_eq!(backend.register_source(source.clone()), Ok(()));
assert_eq!(backend.register_source(source), Err(crate::GitBackendError::DuplicateSource));
let _ = std::fs::remove_dir_all(root);
return;
}
#[test]
fn source_key_never_interpolates_raw_source_identifier_as_a_path() {
let source_result = source_id("../pgr/credential:value");