675 lines
27 KiB
Rust
675 lines
27 KiB
Rust
// file: crates/knowledge-git-lib/src/backend.rs
|
|
// version: 4
|
|
|
|
//! 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");
|
|
}
|
|
|
|
/// Return the directory reserved for temporary isolated write worktrees.
|
|
pub fn write_worktrees_path(&self) -> std::path::PathBuf {
|
|
return self.state_root.join("write-worktrees");
|
|
}
|
|
|
|
/// Return the controlled empty global Git configuration path.
|
|
pub fn empty_global_config_path(&self) -> std::path::PathBuf {
|
|
return self.state_root.join("empty-global.gitconfig");
|
|
}
|
|
}
|
|
|
|
/// 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 Self(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>,
|
|
environment: Vec<(std::ffi::OsString, std::ffi::OsString)>,
|
|
executable: std::path::PathBuf,
|
|
global_config_path: 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_remove("GIT_AUTHOR_NAME");
|
|
command.env_remove("GIT_AUTHOR_EMAIL");
|
|
command.env_remove("GIT_COMMITTER_NAME");
|
|
command.env_remove("GIT_COMMITTER_EMAIL");
|
|
command.env_remove("GIT_CONFIG_GLOBAL");
|
|
command.env_remove("GIT_CONFIG_NOSYSTEM");
|
|
command.env("GIT_CONFIG_GLOBAL", &specification.global_config_path);
|
|
command.env("GIT_CONFIG_NOSYSTEM", "1");
|
|
command.env("GIT_CONFIG_COUNT", "1");
|
|
command.env("GIT_CONFIG_KEY_0", "core.hooksPath");
|
|
command.env("GIT_CONFIG_VALUE_0", &specification.hooks_path);
|
|
for (name, value) in &specification.environment {
|
|
command.env(name, value);
|
|
}
|
|
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 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 and no registered source yet.
|
|
pub fn new(config: crate::GitBackendConfig) -> crate::GitBackend {
|
|
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, sources: Vec::new() };
|
|
}
|
|
|
|
/// Return the immutable backend configuration.
|
|
pub fn config(&self) -> &crate::GitBackendConfig {
|
|
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 !crate::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);
|
|
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),
|
|
}
|
|
}
|
|
|
|
pub(crate) 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);
|
|
}
|
|
|
|
pub(crate) fn prepare_state_directories(&self) -> Result<(), crate::GitBackendError> {
|
|
for path in [self.config.hooks_path(), self.config.repositories_path(), self.config.snapshots_path(), self.config.write_worktrees_path()] {
|
|
let create_result = std::fs::create_dir_all(path);
|
|
if create_result.is_err() {
|
|
return Err(crate::GitBackendError::StatePreparationFailed);
|
|
}
|
|
}
|
|
let config_path = self.config.empty_global_config_path();
|
|
if !config_path.exists() {
|
|
let write_result = std::fs::write(config_path, b"");
|
|
if write_result.is_err() {
|
|
return Err(crate::GitBackendError::StatePreparationFailed);
|
|
}
|
|
}
|
|
return Ok(());
|
|
}
|
|
|
|
pub(crate) fn run_controlled_git(
|
|
&self,
|
|
operation: crate::GitOperation,
|
|
arguments: Vec<std::ffi::OsString>,
|
|
environment: Vec<(std::ffi::OsString, std::ffi::OsString)>,
|
|
) -> Result<Vec<u8>, crate::GitBackendError> {
|
|
let specification = GitCommandSpec {
|
|
arguments,
|
|
environment,
|
|
executable: self.config.git_executable().to_path_buf(),
|
|
global_config_path: self.config.empty_global_config_path(),
|
|
hooks_path: self.config.hooks_path(),
|
|
operation,
|
|
};
|
|
let result = self.runner.run(&specification);
|
|
match result {
|
|
Ok(output) => return Ok(output.stdout),
|
|
Err(error) => return Err(error),
|
|
}
|
|
}
|
|
|
|
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,
|
|
environment: Vec::new(),
|
|
executable: self.config.git_executable().to_path_buf(),
|
|
global_config_path: self.config.empty_global_config_path(),
|
|
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"),
|
|
],
|
|
environment: Vec::new(),
|
|
executable: self.config.git_executable().to_path_buf(),
|
|
global_config_path: self.config.empty_global_config_path(),
|
|
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(());
|
|
}
|
|
|
|
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"),
|
|
],
|
|
environment: Vec::new(),
|
|
executable: self.config.git_executable().to_path_buf(),
|
|
global_config_path: self.config.empty_global_config_path(),
|
|
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),
|
|
],
|
|
environment: Vec::new(),
|
|
executable: self.config.git_executable().to_path_buf(),
|
|
global_config_path: self.config.empty_global_config_path(),
|
|
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 !crate::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 !crate::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()),
|
|
],
|
|
environment: Vec::new(),
|
|
executable: self.config.git_executable().to_path_buf(),
|
|
global_config_path: self.config.empty_global_config_path(),
|
|
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"),
|
|
],
|
|
environment: Vec::new(),
|
|
executable: self.config.git_executable().to_path_buf(),
|
|
global_config_path: self.config.empty_global_config_path(),
|
|
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"),
|
|
],
|
|
environment: Vec::new(),
|
|
executable: self.config.git_executable().to_path_buf(),
|
|
global_config_path: self.config.empty_global_config_path(),
|
|
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);
|
|
}
|
|
}
|
|
|
|
pub(crate) 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)]
|
|
#[path = "../unit_tests/backend.rs"]
|
|
mod tests;
|