0.0.2-alpha.2
This commit is contained in:
15
crates/knowledge-git-lib/Cargo.toml
Normal file
15
crates/knowledge-git-lib/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
# file: crates/knowledge-git-lib/Cargo.toml
|
||||
# version: 1
|
||||
|
||||
[package]
|
||||
name = "knowledge-git-lib"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
repository.workspace = true
|
||||
|
||||
[dependencies]
|
||||
knowledge-domain-lib.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
379
crates/knowledge-git-lib/src/backend.rs
Normal file
379
crates/knowledge-git-lib/src/backend.rs
Normal file
@@ -0,0 +1,379 @@
|
||||
// file: crates/knowledge-git-lib/src/backend.rs
|
||||
// version: 1
|
||||
|
||||
//! Physical source configuration, generated-state layout and controlled Git process runner.
|
||||
|
||||
/// Explicit physical configuration for one controlled Git backend instance.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct GitBackendConfig {
|
||||
git_executable: std::path::PathBuf,
|
||||
state_root: std::path::PathBuf,
|
||||
}
|
||||
|
||||
impl crate::GitBackendConfig {
|
||||
/// Build a backend configuration using `git` from the process environment.
|
||||
pub fn new(state_root: std::path::PathBuf) -> Result<crate::GitBackendConfig, crate::GitBackendError> {
|
||||
return crate::GitBackendConfig::with_git_executable(state_root, std::path::PathBuf::from("git"));
|
||||
}
|
||||
|
||||
/// Build a backend configuration with an explicit Git executable path.
|
||||
pub fn with_git_executable(
|
||||
state_root: std::path::PathBuf,
|
||||
git_executable: std::path::PathBuf,
|
||||
) -> Result<crate::GitBackendConfig, crate::GitBackendError> {
|
||||
if state_root.as_os_str().is_empty() {
|
||||
return Err(crate::GitBackendError::EmptyStateRoot);
|
||||
}
|
||||
if git_executable.as_os_str().is_empty() {
|
||||
return Err(crate::GitBackendError::EmptyGitExecutable);
|
||||
}
|
||||
return Ok(crate::GitBackendConfig { git_executable, state_root });
|
||||
}
|
||||
|
||||
/// Return the configured Git executable path.
|
||||
pub fn git_executable(&self) -> &std::path::Path {
|
||||
return self.git_executable.as_path();
|
||||
}
|
||||
|
||||
/// Return the exclusive generated-state root owned by this backend instance.
|
||||
pub fn state_root(&self) -> &std::path::Path {
|
||||
return self.state_root.as_path();
|
||||
}
|
||||
|
||||
/// Return the empty controlled hooks directory used to disable repository hooks.
|
||||
pub fn hooks_path(&self) -> std::path::PathBuf {
|
||||
return self.state_root.join("hooks-disabled");
|
||||
}
|
||||
|
||||
/// Return the directory that contains controlled bare mirrors.
|
||||
pub fn repositories_path(&self) -> std::path::PathBuf {
|
||||
return self.state_root.join("repositories");
|
||||
}
|
||||
|
||||
/// Return the directory reserved for revision-addressed snapshots.
|
||||
pub fn snapshots_path(&self) -> std::path::PathBuf {
|
||||
return self.state_root.join("snapshots");
|
||||
}
|
||||
}
|
||||
|
||||
/// Coarse physical source kind used by the backend.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum GitSourceKind {
|
||||
/// Source is a path on the local filesystem.
|
||||
Local,
|
||||
/// Source is a Git remote location interpreted by the Git executable.
|
||||
Remote,
|
||||
}
|
||||
|
||||
/// Physical Git source configuration associated with one logical source identifier.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct GitSourceConfig {
|
||||
location: std::ffi::OsString,
|
||||
source_id: knowledge_domain_lib::SourceId,
|
||||
source_kind: crate::GitSourceKind,
|
||||
}
|
||||
|
||||
impl crate::GitSourceConfig {
|
||||
/// Build a local Git source configuration.
|
||||
pub fn local(
|
||||
source_id: knowledge_domain_lib::SourceId,
|
||||
path: std::path::PathBuf,
|
||||
) -> Result<crate::GitSourceConfig, crate::GitBackendError> {
|
||||
if path.as_os_str().is_empty() {
|
||||
return Err(crate::GitBackendError::EmptySourceLocation);
|
||||
}
|
||||
return Ok(crate::GitSourceConfig {
|
||||
location: path.into_os_string(),
|
||||
source_id,
|
||||
source_kind: crate::GitSourceKind::Local,
|
||||
});
|
||||
}
|
||||
|
||||
/// Build a remote Git source configuration without interpreting provider-specific syntax.
|
||||
pub fn remote(source_id: knowledge_domain_lib::SourceId, location: &str) -> Result<crate::GitSourceConfig, crate::GitBackendError> {
|
||||
if location.trim().is_empty() {
|
||||
return Err(crate::GitBackendError::EmptySourceLocation);
|
||||
}
|
||||
return Ok(crate::GitSourceConfig {
|
||||
location: std::ffi::OsString::from(location),
|
||||
source_id,
|
||||
source_kind: crate::GitSourceKind::Remote,
|
||||
});
|
||||
}
|
||||
|
||||
/// Return the logical source identifier associated with this physical source.
|
||||
pub fn source_id(&self) -> &knowledge_domain_lib::SourceId {
|
||||
return &self.source_id;
|
||||
}
|
||||
|
||||
/// Return whether the source is local or remote.
|
||||
pub fn source_kind(&self) -> crate::GitSourceKind {
|
||||
return self.source_kind;
|
||||
}
|
||||
}
|
||||
|
||||
/// Path-safe storage key derived from a logical Knowledge source identifier.
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct GitSourceKey(String);
|
||||
|
||||
impl crate::GitSourceKey {
|
||||
/// Derive a reversible collision-free key from the UTF-8 bytes of a logical source identifier.
|
||||
pub fn from_source_id(source_id: &knowledge_domain_lib::SourceId) -> crate::GitSourceKey {
|
||||
let mut value = String::from("source-");
|
||||
for byte in source_id.as_str().as_bytes() {
|
||||
value.push_str(format!("{:02x}", *byte).as_str());
|
||||
}
|
||||
return crate::GitSourceKey(value);
|
||||
}
|
||||
|
||||
/// Return the path-safe key text.
|
||||
pub fn as_str(&self) -> &str {
|
||||
return self.0.as_str();
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of ensuring that a controlled mirror exists.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum GitMirrorStatus {
|
||||
/// The mirror was created by the current call.
|
||||
Created,
|
||||
/// A valid existing mirror was reused.
|
||||
Existing,
|
||||
}
|
||||
|
||||
/// Description of one initialized or already existing controlled mirror.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct GitMirror {
|
||||
repository_path: std::path::PathBuf,
|
||||
source_key: crate::GitSourceKey,
|
||||
status: crate::GitMirrorStatus,
|
||||
}
|
||||
|
||||
impl crate::GitMirror {
|
||||
/// Return the controlled bare-repository path.
|
||||
pub fn repository_path(&self) -> &std::path::Path {
|
||||
return self.repository_path.as_path();
|
||||
}
|
||||
|
||||
/// Return the path-safe key associated with the logical source identifier.
|
||||
pub fn source_key(&self) -> &crate::GitSourceKey {
|
||||
return &self.source_key;
|
||||
}
|
||||
|
||||
/// Return whether the mirror was newly created or already existed.
|
||||
pub fn status(&self) -> crate::GitMirrorStatus {
|
||||
return self.status;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
struct GitCommandSpec {
|
||||
arguments: Vec<std::ffi::OsString>,
|
||||
executable: std::path::PathBuf,
|
||||
hooks_path: std::path::PathBuf,
|
||||
operation: crate::GitOperation,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
struct GitCommandOutput {
|
||||
stdout: Vec<u8>,
|
||||
}
|
||||
|
||||
trait GitCommandRunner {
|
||||
fn run(&self, specification: &GitCommandSpec) -> Result<GitCommandOutput, crate::GitBackendError>;
|
||||
}
|
||||
|
||||
struct SystemGitRunner;
|
||||
|
||||
impl GitCommandRunner for SystemGitRunner {
|
||||
fn run(&self, specification: &GitCommandSpec) -> Result<GitCommandOutput, crate::GitBackendError> {
|
||||
let mut command = std::process::Command::new(&specification.executable);
|
||||
command.args(&specification.arguments);
|
||||
command.env("GIT_TERMINAL_PROMPT", "0");
|
||||
command.env_remove("GIT_ALTERNATE_OBJECT_DIRECTORIES");
|
||||
command.env_remove("GIT_COMMON_DIR");
|
||||
command.env_remove("GIT_CONFIG_PARAMETERS");
|
||||
command.env_remove("GIT_DIR");
|
||||
command.env_remove("GIT_INDEX_FILE");
|
||||
command.env_remove("GIT_OBJECT_DIRECTORY");
|
||||
command.env_remove("GIT_WORK_TREE");
|
||||
command.env("GIT_CONFIG_COUNT", "1");
|
||||
command.env("GIT_CONFIG_KEY_0", "core.hooksPath");
|
||||
command.env("GIT_CONFIG_VALUE_0", &specification.hooks_path);
|
||||
let output_result = command.output();
|
||||
let output = match output_result {
|
||||
Ok(value) => value,
|
||||
Err(_) => {
|
||||
return Err(crate::GitBackendError::GitUnavailable {
|
||||
operation: specification.operation,
|
||||
});
|
||||
}
|
||||
};
|
||||
if !output.status.success() {
|
||||
return Err(crate::GitBackendError::GitCommandFailed {
|
||||
operation: specification.operation,
|
||||
exit_code: output.status.code(),
|
||||
});
|
||||
}
|
||||
return Ok(GitCommandOutput { stdout: output.stdout });
|
||||
}
|
||||
}
|
||||
|
||||
/// Controlled Git backend that owns local mirror initialization and inspection.
|
||||
pub struct GitBackend {
|
||||
config: crate::GitBackendConfig,
|
||||
runner: Box<dyn GitCommandRunner>,
|
||||
}
|
||||
|
||||
impl crate::GitBackend {
|
||||
/// Create a backend using the configured real Git executable.
|
||||
pub fn new(config: crate::GitBackendConfig) -> crate::GitBackend {
|
||||
return crate::GitBackend {
|
||||
config,
|
||||
runner: Box::new(SystemGitRunner),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn with_runner(config: crate::GitBackendConfig, runner: Box<dyn GitCommandRunner>) -> crate::GitBackend {
|
||||
return crate::GitBackend { config, runner };
|
||||
}
|
||||
|
||||
/// Return the immutable backend configuration.
|
||||
pub fn config(&self) -> &crate::GitBackendConfig {
|
||||
return &self.config;
|
||||
}
|
||||
|
||||
/// 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()));
|
||||
}
|
||||
|
||||
/// 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);
|
||||
let repository_path = self.repository_path(source_id);
|
||||
if !repository_path.exists() {
|
||||
return Ok(None);
|
||||
}
|
||||
let inspection_result = self.inspect_bare_repository(&repository_path);
|
||||
match inspection_result {
|
||||
Ok(()) => {
|
||||
return Ok(Some(crate::GitMirror {
|
||||
repository_path,
|
||||
source_key,
|
||||
status: crate::GitMirrorStatus::Existing,
|
||||
}));
|
||||
}
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensure that one configured source has a valid controlled bare mirror.
|
||||
pub fn ensure_mirror(&self, source: &crate::GitSourceConfig) -> Result<crate::GitMirror, crate::GitBackendError> {
|
||||
let preparation_result = self.prepare_state_directories();
|
||||
match preparation_result {
|
||||
Ok(()) => (),
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
let source_key = crate::GitSourceKey::from_source_id(source.source_id());
|
||||
let repository_path = self.repository_path(source.source_id());
|
||||
if repository_path.exists() {
|
||||
let inspection_result = self.inspect_bare_repository(&repository_path);
|
||||
match inspection_result {
|
||||
Ok(()) => {
|
||||
return Ok(crate::GitMirror {
|
||||
repository_path,
|
||||
source_key,
|
||||
status: crate::GitMirrorStatus::Existing,
|
||||
});
|
||||
}
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
let clone_result = self.clone_mirror(source, &repository_path);
|
||||
match clone_result {
|
||||
Ok(()) => (),
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
let inspection_result = self.inspect_bare_repository(&repository_path);
|
||||
match inspection_result {
|
||||
Ok(()) => {
|
||||
return Ok(crate::GitMirror {
|
||||
repository_path,
|
||||
source_key,
|
||||
status: crate::GitMirrorStatus::Created,
|
||||
});
|
||||
}
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
if create_result.is_err() {
|
||||
return Err(crate::GitBackendError::StatePreparationFailed);
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
fn clone_mirror(&self, source: &crate::GitSourceConfig, repository_path: &std::path::Path) -> Result<(), crate::GitBackendError> {
|
||||
let mut arguments = vec![std::ffi::OsString::from("clone"), std::ffi::OsString::from("--mirror")];
|
||||
if source.source_kind == crate::GitSourceKind::Local {
|
||||
arguments.push(std::ffi::OsString::from("--no-local"));
|
||||
}
|
||||
arguments.push(std::ffi::OsString::from("--"));
|
||||
arguments.push(source.location.clone());
|
||||
arguments.push(repository_path.as_os_str().to_os_string());
|
||||
let specification = GitCommandSpec {
|
||||
arguments,
|
||||
executable: self.config.git_executable().to_path_buf(),
|
||||
hooks_path: self.config.hooks_path(),
|
||||
operation: crate::GitOperation::CloneMirror,
|
||||
};
|
||||
let run_result = self.runner.run(&specification);
|
||||
match run_result {
|
||||
Ok(_) => return Ok(()),
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
fn inspect_bare_repository(&self, repository_path: &std::path::Path) -> Result<(), crate::GitBackendError> {
|
||||
let specification = GitCommandSpec {
|
||||
arguments: vec![
|
||||
std::ffi::OsString::from("--git-dir"),
|
||||
repository_path.as_os_str().to_os_string(),
|
||||
std::ffi::OsString::from("rev-parse"),
|
||||
std::ffi::OsString::from("--is-bare-repository"),
|
||||
],
|
||||
executable: self.config.git_executable().to_path_buf(),
|
||||
hooks_path: self.config.hooks_path(),
|
||||
operation: crate::GitOperation::InspectMirror,
|
||||
};
|
||||
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::InspectMirror,
|
||||
});
|
||||
}
|
||||
};
|
||||
if stdout != "true" {
|
||||
return Err(crate::GitBackendError::InvalidMirror);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "../unit_tests/backend.rs"]
|
||||
mod tests;
|
||||
78
crates/knowledge-git-lib/src/error.rs
Normal file
78
crates/knowledge-git-lib/src/error.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
// file: crates/knowledge-git-lib/src/error.rs
|
||||
// version: 1
|
||||
|
||||
//! Recoverable errors for controlled Git process execution.
|
||||
|
||||
/// Git operation category safe to expose in diagnostics.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum GitOperation {
|
||||
/// Clone one configured source into a controlled mirror.
|
||||
CloneMirror,
|
||||
/// Inspect whether a controlled repository is a valid bare mirror.
|
||||
InspectMirror,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for crate::GitOperation {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
crate::GitOperation::CloneMirror => return formatter.write_str("clone mirror"),
|
||||
crate::GitOperation::InspectMirror => return formatter.write_str("inspect mirror"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Recoverable error returned by the controlled Git backend.
|
||||
///
|
||||
/// Process failures intentionally retain only an operation category and exit code. Source URLs,
|
||||
/// command arguments, stderr and credentials are not embedded in this public error.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum GitBackendError {
|
||||
/// The configured Git executable path is empty.
|
||||
EmptyGitExecutable,
|
||||
/// The configured state root is empty.
|
||||
EmptyStateRoot,
|
||||
/// A local source path or remote source string is empty.
|
||||
EmptySourceLocation,
|
||||
/// The backend could not prepare its generated-state directories.
|
||||
StatePreparationFailed,
|
||||
/// The Git executable could not be started for the requested operation.
|
||||
GitUnavailable {
|
||||
/// Operation that could not start.
|
||||
operation: crate::GitOperation,
|
||||
},
|
||||
/// Git returned a non-success status for the requested operation.
|
||||
GitCommandFailed {
|
||||
/// Operation that failed.
|
||||
operation: crate::GitOperation,
|
||||
/// Process exit code when the operating system exposed one.
|
||||
exit_code: Option<i32>,
|
||||
},
|
||||
/// An existing controlled repository is not a valid bare mirror.
|
||||
InvalidMirror,
|
||||
/// Git returned output that does not satisfy the backend contract.
|
||||
UnexpectedGitOutput {
|
||||
/// Operation whose output could not be validated.
|
||||
operation: crate::GitOperation,
|
||||
},
|
||||
}
|
||||
|
||||
impl std::fmt::Display for crate::GitBackendError {
|
||||
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
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::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::UnexpectedGitOutput { operation } => {
|
||||
return write!(formatter, "git returned unexpected output while attempting to {operation}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for crate::GitBackendError {}
|
||||
29
crates/knowledge-git-lib/src/lib.rs
Normal file
29
crates/knowledge-git-lib/src/lib.rs
Normal file
@@ -0,0 +1,29 @@
|
||||
// file: crates/knowledge-git-lib/src/lib.rs
|
||||
// version: 1
|
||||
|
||||
//! 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.
|
||||
|
||||
mod backend;
|
||||
mod error;
|
||||
|
||||
/// Controlled Git backend that owns local mirror initialization and inspection.
|
||||
pub use self::backend::GitBackend;
|
||||
/// Explicit physical configuration for one controlled Git backend instance.
|
||||
pub use self::backend::GitBackendConfig;
|
||||
/// Description of one initialized or already existing controlled mirror.
|
||||
pub use self::backend::GitMirror;
|
||||
/// Result of ensuring that a controlled mirror exists.
|
||||
pub use self::backend::GitMirrorStatus;
|
||||
/// Path-safe storage key derived from a logical Knowledge source identifier.
|
||||
pub use self::backend::GitSourceKey;
|
||||
/// Physical Git source configuration associated with one logical source identifier.
|
||||
pub use self::backend::GitSourceConfig;
|
||||
/// Coarse physical source kind used by the backend.
|
||||
pub use self::backend::GitSourceKind;
|
||||
/// Recoverable error returned by the controlled Git backend.
|
||||
pub use self::error::GitBackendError;
|
||||
/// Git operation category exposed by recoverable process errors without leaking command arguments.
|
||||
pub use self::error::GitOperation;
|
||||
283
crates/knowledge-git-lib/unit_tests/backend.rs
Normal file
283
crates/knowledge-git-lib/unit_tests/backend.rs
Normal file
@@ -0,0 +1,283 @@
|
||||
// file: crates/knowledge-git-lib/unit_tests/backend.rs
|
||||
// version: 1
|
||||
|
||||
#[derive(Clone)]
|
||||
struct RecordingRunner {
|
||||
calls: std::rc::Rc<std::cell::RefCell<Vec<super::GitCommandSpec>>>,
|
||||
failure: Option<crate::GitBackendError>,
|
||||
inspection_stdout: Vec<u8>,
|
||||
}
|
||||
|
||||
impl RecordingRunner {
|
||||
fn successful() -> RecordingRunner {
|
||||
return RecordingRunner {
|
||||
calls: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
|
||||
failure: None,
|
||||
inspection_stdout: b"true\n".to_vec(),
|
||||
};
|
||||
}
|
||||
|
||||
fn failing(error: crate::GitBackendError) -> RecordingRunner {
|
||||
return RecordingRunner {
|
||||
calls: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
|
||||
failure: Some(error),
|
||||
inspection_stdout: b"true\n".to_vec(),
|
||||
};
|
||||
}
|
||||
|
||||
fn with_inspection_stdout(value: &[u8]) -> RecordingRunner {
|
||||
return RecordingRunner {
|
||||
calls: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
|
||||
failure: None,
|
||||
inspection_stdout: value.to_vec(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
impl super::GitCommandRunner for RecordingRunner {
|
||||
fn run(&self, specification: &super::GitCommandSpec) -> Result<super::GitCommandOutput, crate::GitBackendError> {
|
||||
self.calls.borrow_mut().push(specification.clone());
|
||||
if let Some(error) = self.failure {
|
||||
return Err(error);
|
||||
}
|
||||
if specification.operation == crate::GitOperation::InspectMirror {
|
||||
return Ok(super::GitCommandOutput {
|
||||
stdout: self.inspection_stdout.clone(),
|
||||
});
|
||||
}
|
||||
return Ok(super::GitCommandOutput { stdout: Vec::new() });
|
||||
}
|
||||
}
|
||||
|
||||
fn source_id(value: &str) -> Option<knowledge_domain_lib::SourceId> {
|
||||
let result = knowledge_domain_lib::SourceId::new(value);
|
||||
match result {
|
||||
Ok(source_id) => return Some(source_id),
|
||||
Err(_) => return None,
|
||||
}
|
||||
}
|
||||
|
||||
fn test_root(label: &str) -> std::path::PathBuf {
|
||||
let mut path = std::env::temp_dir();
|
||||
path.push(format!("sasedev-knowledge-git-lib-{label}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&path);
|
||||
return path;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backend_configuration_requires_non_empty_paths() {
|
||||
assert_eq!(crate::GitBackendConfig::new(std::path::PathBuf::new()), Err(crate::GitBackendError::EmptyStateRoot));
|
||||
assert_eq!(
|
||||
crate::GitBackendConfig::with_git_executable(test_root("empty-executable"), std::path::PathBuf::new()),
|
||||
Err(crate::GitBackendError::EmptyGitExecutable)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_key_never_interpolates_raw_source_identifier_as_a_path() {
|
||||
let source_result = source_id("../pgr/credential:value");
|
||||
assert!(source_result.is_some());
|
||||
let source_id = match source_result {
|
||||
Some(value) => value,
|
||||
None => return,
|
||||
};
|
||||
let key = crate::GitSourceKey::from_source_id(&source_id);
|
||||
assert!(key.as_str().starts_with("source-"));
|
||||
assert!(key.as_str().chars().all(|character| character.is_ascii_alphanumeric() || character == '-'));
|
||||
assert!(!key.as_str().contains("pgr"));
|
||||
assert!(!key.as_str().contains(".."));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_source_clone_uses_mirror_no_local_and_argument_separator() {
|
||||
let root = test_root("local-arguments");
|
||||
let config_result = crate::GitBackendConfig::new(root.clone());
|
||||
assert!(config_result.is_ok());
|
||||
let config = match config_result {
|
||||
Ok(value) => value,
|
||||
Err(_) => return,
|
||||
};
|
||||
let runner = RecordingRunner::successful();
|
||||
let backend = crate::GitBackend::with_runner(config, Box::new(runner.clone()));
|
||||
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 mirror_result = backend.ensure_mirror(&source);
|
||||
assert!(mirror_result.is_ok());
|
||||
let calls = runner.calls.borrow();
|
||||
assert_eq!(calls.len(), 2);
|
||||
assert_eq!(calls[0].operation, crate::GitOperation::CloneMirror);
|
||||
assert_eq!(
|
||||
calls[0].arguments,
|
||||
vec![
|
||||
std::ffi::OsString::from("clone"),
|
||||
std::ffi::OsString::from("--mirror"),
|
||||
std::ffi::OsString::from("--no-local"),
|
||||
std::ffi::OsString::from("--"),
|
||||
std::ffi::OsString::from("/controlled/source"),
|
||||
backend.repository_path(source.source_id()).into_os_string(),
|
||||
]
|
||||
);
|
||||
assert_eq!(calls[1].operation, crate::GitOperation::InspectMirror);
|
||||
assert!(backend.config().hooks_path().is_dir());
|
||||
assert!(backend.config().repositories_path().is_dir());
|
||||
assert!(backend.config().snapshots_path().is_dir());
|
||||
let _ = std::fs::remove_dir_all(root);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_source_clone_does_not_add_no_local() {
|
||||
let root = test_root("remote-arguments");
|
||||
let config_result = crate::GitBackendConfig::new(root.clone());
|
||||
assert!(config_result.is_ok());
|
||||
let config = match config_result {
|
||||
Ok(value) => value,
|
||||
Err(_) => return,
|
||||
};
|
||||
let runner = RecordingRunner::successful();
|
||||
let backend = crate::GitBackend::with_runner(config, Box::new(runner.clone()));
|
||||
let source_id_result = source_id("howtos");
|
||||
assert!(source_id_result.is_some());
|
||||
let source_id = match source_id_result {
|
||||
Some(value) => value,
|
||||
None => return,
|
||||
};
|
||||
let source_result = crate::GitSourceConfig::remote(source_id, "ssh://git@example.invalid/howtos.git");
|
||||
assert!(source_result.is_ok());
|
||||
let source = match source_result {
|
||||
Ok(value) => value,
|
||||
Err(_) => return,
|
||||
};
|
||||
assert!(backend.ensure_mirror(&source).is_ok());
|
||||
let calls = runner.calls.borrow();
|
||||
assert_eq!(calls[0].operation, crate::GitOperation::CloneMirror);
|
||||
assert_eq!(calls[0].arguments[0], std::ffi::OsString::from("clone"));
|
||||
assert_eq!(calls[0].arguments[1], std::ffi::OsString::from("--mirror"));
|
||||
assert_eq!(calls[0].arguments[2], std::ffi::OsString::from("--"));
|
||||
assert!(!calls[0].arguments.contains(&std::ffi::OsString::from("--no-local")));
|
||||
let _ = std::fs::remove_dir_all(root);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_errors_do_not_expose_remote_location_or_credentials() {
|
||||
let root = test_root("redaction");
|
||||
let config_result = crate::GitBackendConfig::new(root.clone());
|
||||
assert!(config_result.is_ok());
|
||||
let config = match config_result {
|
||||
Ok(value) => value,
|
||||
Err(_) => return,
|
||||
};
|
||||
let runner = RecordingRunner::failing(crate::GitBackendError::GitCommandFailed {
|
||||
operation: crate::GitOperation::CloneMirror,
|
||||
exit_code: Some(128),
|
||||
});
|
||||
let backend = crate::GitBackend::with_runner(config, Box::new(runner));
|
||||
let source_id_result = source_id("snippets");
|
||||
assert!(source_id_result.is_some());
|
||||
let source_id = match source_id_result {
|
||||
Some(value) => value,
|
||||
None => return,
|
||||
};
|
||||
let source_result = crate::GitSourceConfig::remote(source_id, "https://user:secret@example.invalid/snippets.git");
|
||||
assert!(source_result.is_ok());
|
||||
let source = match source_result {
|
||||
Ok(value) => value,
|
||||
Err(_) => return,
|
||||
};
|
||||
let result = backend.ensure_mirror(&source);
|
||||
assert!(result.is_err());
|
||||
let error = match result {
|
||||
Ok(_) => return,
|
||||
Err(value) => value,
|
||||
};
|
||||
let display = error.to_string();
|
||||
assert!(!display.contains("secret"));
|
||||
assert!(!display.contains("example.invalid"));
|
||||
assert_eq!(
|
||||
error,
|
||||
crate::GitBackendError::GitCommandFailed {
|
||||
operation: crate::GitOperation::CloneMirror,
|
||||
exit_code: Some(128),
|
||||
}
|
||||
);
|
||||
let _ = std::fs::remove_dir_all(root);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn existing_mirror_is_inspected_without_clone() {
|
||||
let root = test_root("existing-mirror");
|
||||
let config_result = crate::GitBackendConfig::new(root.clone());
|
||||
assert!(config_result.is_ok());
|
||||
let config = match config_result {
|
||||
Ok(value) => value,
|
||||
Err(_) => return,
|
||||
};
|
||||
let runner = RecordingRunner::successful();
|
||||
let backend = crate::GitBackend::with_runner(config, Box::new(runner.clone()));
|
||||
let source_result = source_id("pgr");
|
||||
assert!(source_result.is_some());
|
||||
let source_id = match source_result {
|
||||
Some(value) => value,
|
||||
None => return,
|
||||
};
|
||||
let repository_path = backend.repository_path(&source_id);
|
||||
assert!(std::fs::create_dir_all(&repository_path).is_ok());
|
||||
let source_result = crate::GitSourceConfig::remote(source_id, "https://example.invalid/pgr.git");
|
||||
assert!(source_result.is_ok());
|
||||
let source = match source_result {
|
||||
Ok(value) => value,
|
||||
Err(_) => return,
|
||||
};
|
||||
let result = backend.ensure_mirror(&source);
|
||||
assert!(result.is_ok());
|
||||
let mirror = match result {
|
||||
Ok(value) => value,
|
||||
Err(_) => return,
|
||||
};
|
||||
assert_eq!(mirror.status(), crate::GitMirrorStatus::Existing);
|
||||
let calls = runner.calls.borrow();
|
||||
assert_eq!(calls.len(), 1);
|
||||
assert_eq!(calls[0].operation, crate::GitOperation::InspectMirror);
|
||||
let _ = std::fs::remove_dir_all(root);
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_existing_mirror_is_rejected_without_deleting_it() {
|
||||
let root = test_root("invalid-mirror");
|
||||
let config_result = crate::GitBackendConfig::new(root.clone());
|
||||
assert!(config_result.is_ok());
|
||||
let config = match config_result {
|
||||
Ok(value) => value,
|
||||
Err(_) => return,
|
||||
};
|
||||
let runner = RecordingRunner::with_inspection_stdout(b"false\n");
|
||||
let backend = crate::GitBackend::with_runner(config, Box::new(runner));
|
||||
let source_result = source_id("pgr");
|
||||
assert!(source_result.is_some());
|
||||
let source_id = match source_result {
|
||||
Some(value) => value,
|
||||
None => return,
|
||||
};
|
||||
let repository_path = backend.repository_path(&source_id);
|
||||
assert!(std::fs::create_dir_all(&repository_path).is_ok());
|
||||
let result = backend.inspect_mirror(&source_id);
|
||||
assert_eq!(result, Err(crate::GitBackendError::InvalidMirror));
|
||||
assert!(repository_path.is_dir());
|
||||
let _ = std::fs::remove_dir_all(root);
|
||||
return;
|
||||
}
|
||||
Reference in New Issue
Block a user