pub(crate) mod applied_packs;
mod envelope_signer;
mod packmap;
use mkit_core::layout::RepoLayout;
use std::path::Path;
use std::sync::Arc;
use applied_packs::AppliedPacks;
use mkit_core::hash::{HASH_LEN, Hash};
use mkit_core::object::Object;
use mkit_core::ops::merge::is_ancestor;
use mkit_core::ops::restore;
use mkit_core::pack::{self, PackError, PackWriter};
use mkit_core::protocol::{PackKey, Transport, TransportError};
use mkit_core::refs::{self, Head};
use mkit_core::store::{ObjectStore, StoreError};
use mkit_core::transfer::{self, PackListError};
use mkit_transport_connect::ConnectTransport;
use mkit_transport_file::FileTransport;
use mkit_transport_s3::S3Transport;
use mkit_transport_ssh::{SshInitError, SshOptions, SshTransport, parse_mkit_ssh_url};
use packmap::{
ChainAction, advance_packmap, apply_fetched_chain, commit_head, packmap_ref, probe_chain,
rebaseline_depth, resolve_and_download_chain,
};
const DEFAULT_REMOTE: &str = "default";
#[derive(Debug, thiserror::Error)]
pub enum DispatchError {
#[error("unsupported URL scheme: {0}")]
UnsupportedScheme(String),
#[error("malformed URL: {0}")]
MalformedUrl(String),
#[error("no HEAD branch to push")]
NoHead,
#[error("interrupted")]
Interrupted,
#[error("transport: {0}")]
Transport(#[from] TransportError),
#[error("refs: {0}")]
Refs(#[from] refs::RefError),
#[error("repo lock: {0}")]
RepoLock(#[from] mkit_core::repo_lock::LockError),
#[error("worktree discovery: {0}")]
Discover(#[from] mkit_core::layout::DiscoverError),
#[error("io: {0}")]
Io(#[from] std::io::Error),
#[error("store: {0}")]
Store(#[from] StoreError),
#[error("pack: {0}")]
Pack(#[from] PackError),
#[error("packlist: {0}")]
PackList(#[from] PackListError),
#[error("ssh init: {0}")]
SshInit(#[from] SshInitError),
#[error("pull requires HEAD to point at a branch")]
DetachedHead,
#[error("remote branch '{0}' not found")]
RemoteBranchMissing(String),
#[error("pull would not fast-forward branch '{branch}'; merge or rebase first")]
NonFastForwardPull { branch: String },
#[error("restore safety: {0}")]
RestoreSafety(String),
#[error("object is not a commit")]
NotCommit,
#[error("restore: {0}")]
Restore(#[from] restore::RestoreError),
#[error("{0}")]
UntrustedRemote(String),
#[error(
"updates were rejected for branch '{branch}' (non-fast-forward); fetch and merge first, or re-run with --force-with-lease / --force"
)]
NonFastForwardPush { branch: String },
#[error(
"could not establish the pack map for branch '{branch}' under concurrent pushes; retry"
)]
PackmapContended { branch: String },
#[error("pack map chain for branch '{branch}' is malformed (too deep, cyclic, or unreadable)")]
PackChainInvalid { branch: String },
#[error("remote advertised branch '{0}' but no pack map to reconstruct it")]
PackmapMissing(String),
#[error("remote advertised pack {pack} for branch '{branch}' but does not hold it")]
AdvertisedPackMissing { branch: String, pack: String },
#[error("remote is missing object {0} needed to reconstruct the ref")]
RemoteMissingObject(String),
#[error(
"fetched history is too large to verify (closure exceeds the {0}-object cap); refusing to publish an unverified ref"
)]
ClosureTooLarge(usize),
#[error("object {hash} failed signature verification: {reason}")]
UnsignedOrInvalidObject { hash: String, reason: String },
#[error("invalid remote name for applied-packs record: '{0}'")]
InvalidRemoteName(String),
}
pub fn open_trusted(
endpoint: &str,
repo_chosen: bool,
cfg: &crate::config::LayeredConfig,
layout: &RepoLayout,
) -> Result<Arc<dyn Transport>, DispatchError> {
crate::config::endpoint_credential_trust(cfg, endpoint, repo_chosen)
.map_err(DispatchError::UntrustedRemote)?;
open_with_config(endpoint, &cfg.merged, layout)
}
pub(crate) fn open_with_config(
url: &str,
cfg: &crate::config::Config,
layout: &RepoLayout,
) -> Result<Arc<dyn Transport>, DispatchError> {
let envelope_signer = if url.starts_with("mkit+https://") || url.starts_with("mkit+http://") {
envelope_signer_from_config(cfg, layout)?
} else {
None
};
open_with_ssh_options(url, &ssh_options_from_config(cfg), envelope_signer)
}
pub(crate) fn envelope_signer_from_config(
cfg: &crate::config::Config,
layout: &RepoLayout,
) -> Result<Option<Arc<dyn mkit_transport_connect::EnvelopeSigner>>, DispatchError> {
if !cfg.transport_auth_envelope() {
return Ok(None);
}
let remote_error = |msg: String| DispatchError::Transport(TransportError::RemoteError(msg));
match cfg.signer.as_str() {
"" | "legacy" => {
let key_path =
crate::config::resolve_key_path(layout, &cfg.signing_key).map_err(|e| {
remote_error(format!("transport_auth = envelope: signing_key: {e}"))
})?;
if !key_path.exists() {
return Err(remote_error(format!(
"transport_auth = envelope requires a signing key at {} — run `mkit keygen` first",
key_path.display()
)));
}
let kp = mkit_core::sign::load_key(&key_path)
.map_err(|e| remote_error(format!("transport_auth = envelope: load key: {e}")))?;
Ok(Some(
Arc::new(envelope_signer::RepoKeyEnvelopeSigner::new(kp))
as Arc<dyn mkit_transport_connect::EnvelopeSigner>,
))
}
"keystore" => {
let signer = envelope_signer::KeystoreEnvelopeSigner::open(cfg)
.map_err(|e| remote_error(format!("transport_auth = envelope: {e}")))?;
Ok(Some(
Arc::new(signer) as Arc<dyn mkit_transport_connect::EnvelopeSigner>
))
}
other => Err(remote_error(format!(
"transport_auth = envelope: unknown signer `{other}` — expected `legacy` or `keystore`"
))),
}
}
fn ssh_options_from_config(cfg: &crate::config::Config) -> SshOptions {
SshOptions {
strict_host_key_checking: cfg.ssh_strict_host_key_checking.clone(),
user_known_hosts_file: cfg.ssh_user_known_hosts_file.clone(),
identity_file: cfg.ssh_identity_file.clone(),
}
}
pub fn open(url: &str) -> Result<Arc<dyn Transport>, DispatchError> {
open_with_ssh_options(url, &SshOptions::default(), None)
}
fn open_with_ssh_options(
url: &str,
ssh_options: &SshOptions,
envelope_signer: Option<Arc<dyn mkit_transport_connect::EnvelopeSigner>>,
) -> Result<Arc<dyn Transport>, DispatchError> {
if url.starts_with("git+") {
return Err(DispatchError::UnsupportedScheme(format!(
"'{url}' is a git-bridge remote — native push/pull/fetch/clone do not \
speak git transports; use `mkit git export` / `mkit git import` / \
`mkit git pull` (feature git-bridge)"
)));
}
if let Some(rest) = url.strip_prefix("mkit+file://") {
let path = Path::new(rest);
return Ok(Arc::new(FileTransport::new(path)));
}
if url.starts_with("mkit+memory://") {
return Err(DispatchError::UnsupportedScheme(
"mkit+memory:// must be driven via in-process harness (see tests)".to_string(),
));
}
if url.starts_with("mkit+https://") || url.starts_with("mkit+http://") {
let tx = ConnectTransport::connect_with_signer(url, envelope_signer)?;
return Ok(Arc::new(tx));
}
if url.starts_with("mkit+s3://") {
let tx = S3Transport::connect(url)?;
return Ok(Arc::new(tx));
}
if url.starts_with("mkit+ssh://") {
let target = parse_mkit_ssh_url(url).map_err(SshInitError::from)?;
let tx = SshTransport::connect_with_options(&target, ssh_options)?;
return Ok(Arc::new(tx));
}
#[cfg(feature = "enc-transport")]
if url.starts_with("mkit+enc://") {
return open_enc(url);
}
Err(DispatchError::MalformedUrl(url.to_string()))
}
#[cfg(feature = "enc-transport")]
const ENC_CLIENT_KEY_ENV: &str = "MKIT_ENC_CLIENT_KEY";
#[cfg(feature = "enc-transport")]
fn open_enc(url: &str) -> Result<Arc<dyn Transport>, DispatchError> {
use mkit_transport_enc::url::parse_enc_url;
let target = parse_enc_url(url).map_err(DispatchError::Transport)?;
let sk = load_or_ephemeral_client_key()?;
let tx = mkit_transport_enc::connect_tcp(&target.host, target.port, &target.server_pubkey, sk)
.map_err(|e| DispatchError::Transport(TransportError::RemoteError(e.to_string())))?;
Ok(Arc::new(tx))
}
#[cfg(feature = "enc-transport")]
fn load_or_ephemeral_client_key()
-> Result<commonware_cryptography::ed25519::PrivateKey, DispatchError> {
use commonware_codec::DecodeExt as _;
use commonware_cryptography::ed25519::PrivateKey;
use zeroize::Zeroizing;
let map_err = |e: String| DispatchError::Transport(TransportError::RemoteError(e));
if let Some(path) = std::env::var_os(ENC_CLIENT_KEY_ENV).filter(|s| !s.is_empty()) {
let seed = mkit_core::sign::load_raw_32(std::path::Path::new(&path))
.map_err(|e| map_err(format!("load {ENC_CLIENT_KEY_ENV}: {e}")))?;
return PrivateKey::decode(seed.as_ref())
.map_err(|e| map_err(format!("client key construction failed: {e}")));
}
let mut secret = Zeroizing::new([0u8; 32]);
getrandom::fill(secret.as_mut()).map_err(|e| map_err(e.to_string()))?;
PrivateKey::decode(secret.as_ref()).map_err(|e| map_err(e.to_string()))
}
pub fn push_all(cwd: &Path, tx: &dyn Transport) -> Result<usize, DispatchError> {
push_all_with(cwd, tx, None, false)
}
pub fn push_all_with(
cwd: &Path,
tx: &dyn Transport,
remote: Option<&str>,
force: bool,
) -> Result<usize, DispatchError> {
let layout = mkit_core::layout::discover(cwd)?;
let store = crate::commands::open_store_configured(&layout)?;
let refs_list = refs::list_refs(&layout)?;
let remote = remote.unwrap_or(DEFAULT_REMOTE);
let mut n = 0;
let mut tracking = refs::RemoteRefBatch::new(&layout, remote)?;
let result: Result<(), DispatchError> = (|| {
for r in refs_list {
if crate::signal::is_shutdown() {
return Err(DispatchError::Interrupted);
}
let Some(h) = r.hash else { continue };
let condition = if force {
refs::RefWriteCondition::Any
} else {
match refs::read_remote_ref(&layout, remote, &r.name)? {
Some(tracked) => refs::RefWriteCondition::Match(tracked),
None => refs::RefWriteCondition::Missing,
}
};
push_branch(tx, &store, &r.name, h, condition)?;
tracking.write(&r.name, &h)?;
n += 1;
}
Ok(())
})();
tracking.commit()?;
result?;
Ok(n)
}
pub fn is_fast_forward(cwd: &Path, old: Option<Hash>, new: Hash) -> Result<bool, DispatchError> {
match old {
None => Ok(true),
Some(o) if o == new => Ok(true),
Some(o) => {
let layout = mkit_core::layout::discover(cwd)?;
let store = crate::commands::open_store_configured(&layout)?;
Ok(is_ancestor(&store, o, new)?)
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum PushLease {
Force,
WithLease,
FastForward,
}
pub fn lease_condition(
cwd: &Path,
remote: &str,
branch: &str,
lease: PushLease,
) -> Result<refs::RefWriteCondition, DispatchError> {
if matches!(lease, PushLease::Force) {
return Ok(refs::RefWriteCondition::Any);
}
let layout = mkit_core::layout::discover(cwd)?;
Ok(match refs::read_remote_ref(&layout, remote, branch)? {
Some(tracked) => refs::RefWriteCondition::Match(tracked),
None => refs::RefWriteCondition::Missing,
})
}
pub fn push_branch_tracked(
cwd: &Path,
tx: &dyn Transport,
remote: &str,
branch: &str,
remote_branch: &str,
lease: PushLease,
) -> Result<Hash, DispatchError> {
let layout = mkit_core::layout::discover(cwd)?;
let store = crate::commands::open_store_configured(&layout)?;
let tip = refs::read_ref(&layout, branch)?
.ok_or_else(|| DispatchError::RemoteBranchMissing(branch.to_owned()))?;
if matches!(lease, PushLease::FastForward)
&& let Some(tracked) = refs::read_remote_ref(&layout, remote, remote_branch)?
&& !is_ancestor(&store, tracked, tip)?
{
return Err(DispatchError::NonFastForwardPush {
branch: remote_branch.to_owned(),
});
}
let condition = lease_condition(cwd, remote, remote_branch, lease)?;
push_branch(tx, &store, remote_branch, tip, condition)?;
refs::write_remote_ref(&layout, remote, remote_branch, &tip)?;
Ok(tip)
}
pub fn push_branch(
tx: &dyn Transport,
store: &ObjectStore,
branch: &str,
tip: Hash,
condition: refs::RefWriteCondition,
) -> Result<(), DispatchError> {
push_branch_with_depth(tx, store, branch, tip, condition, rebaseline_depth())
}
pub fn push_branch_with_depth(
tx: &dyn Transport,
store: &ObjectStore,
branch: &str,
tip: Hash,
condition: refs::RefWriteCondition,
rebaseline_threshold: usize,
) -> Result<(), DispatchError> {
push_branch_with_limits(
tx,
store,
branch,
tip,
condition,
rebaseline_threshold,
pack::MAX_TOTAL_PAYLOAD,
)
}
pub fn push_branch_with_limits(
tx: &dyn Transport,
store: &ObjectStore,
branch: &str,
tip: Hash,
condition: refs::RefWriteCondition,
rebaseline_threshold: usize,
pack_payload_cap: u64,
) -> Result<(), DispatchError> {
let remote_tip = tx.read_ref(&format!("refs/heads/{branch}"))?;
let mut plan = transfer::plan_pack(store, tip, remote_tip)?;
if plan.is_empty() {
return commit_head(tx, &format!("refs/heads/{branch}"), condition, &tip, branch);
}
if crate::signal::is_shutdown() {
return Err(DispatchError::Interrupted);
}
let mut rebaseline = false;
let mut resolved_chain = None;
if rebaseline_threshold > 0
&& let Some(pm) = tx.read_ref(&packmap_ref(branch))?
{
match probe_chain(tx, branch, pm) {
Ok(chain)
if chain.depth + 1 > rebaseline_threshold
&& tx.supports_atomic_advance()
&& !matches!(condition, refs::RefWriteCondition::Any) =>
{
rebaseline = true;
}
Ok(chain) => resolved_chain = Some(chain),
Err(DispatchError::PackChainInvalid { .. }) => {}
Err(e) => return Err(e),
}
}
if rebaseline {
plan = transfer::plan_pack(store, tip, None)?;
}
let pack_keys = build_and_upload_packs(tx, store, &plan, pack_payload_cap)?;
let action = if rebaseline {
ChainAction::ResetSelfContained
} else {
ChainAction::Append {
self_contained: plan.self_contained,
}
};
advance_packmap(
tx,
branch,
&pack_keys,
action,
resolved_chain,
condition,
tip,
)
}
fn build_and_upload_packs(
tx: &dyn Transport,
store: &ObjectStore,
plan: &transfer::PackPlan,
payload_cap: u64,
) -> Result<Vec<Hash>, DispatchError> {
let mut pack_keys = Vec::new();
let mut w = PackWriter::new();
for h in &plan.raw {
let bytes = store.read(h)?;
if should_seal(&w, bytes.len() as u64, payload_cap) {
seal_pack(tx, &mut w, &mut pack_keys)?;
}
w.push_raw(*h, &bytes)?;
crate::progress::report(crate::progress::Event::ObjectsPacked(1));
}
for d in &plan.deltas {
let bound = (HASH_LEN + d.stream.len()) as u64;
if should_seal(&w, bound, payload_cap) {
seal_pack(tx, &mut w, &mut pack_keys)?;
}
w.push_delta(&d.base, &d.stream)?;
crate::progress::report(crate::progress::Event::ObjectsPacked(1));
}
seal_pack(tx, &mut w, &mut pack_keys)?;
Ok(pack_keys)
}
fn should_seal(w: &PackWriter, add_bound: u64, payload_cap: u64) -> bool {
w.entry_count() > 0 && w.total_payload().saturating_add(add_bound) > payload_cap
}
fn seal_pack(
tx: &dyn Transport,
w: &mut PackWriter,
pack_keys: &mut Vec<Hash>,
) -> Result<(), DispatchError> {
if crate::signal::is_shutdown() {
return Err(DispatchError::Interrupted);
}
let sealed = std::mem::replace(w, PackWriter::new());
let pack = sealed.finish()?;
let pack_key = pack::pack_key(&pack);
tx.upload_pack(&pack, &PackKey::from_hash(pack_key))?;
crate::progress::report(crate::progress::Event::PackUploaded(pack.len() as u64));
pack_keys.push(pack_key);
Ok(())
}
pub fn pull_all(
cwd: &Path,
tx: &dyn Transport,
remote: &str,
target_branch: Option<&str>,
) -> Result<usize, DispatchError> {
pull_all_with(cwd, tx, remote, target_branch, true)
}
pub fn pull_all_with(
cwd: &Path,
tx: &dyn Transport,
remote: &str,
target_branch: Option<&str>,
require_signed: bool,
) -> Result<usize, DispatchError> {
let layout = mkit_core::layout::discover(cwd)?;
let store = crate::commands::open_store_configured(&layout)?;
let n = fetch_objects(&store, &layout, tx, remote, require_signed)?;
let remote_refs = refs::list_remote_refs(&layout, remote)?
.into_iter()
.filter_map(|r| r.hash.map(|hash| (r.name, hash)))
.collect::<Vec<_>>();
if remote_refs.is_empty() {
return Ok(n);
}
let _lock = mkit_core::repo_lock::acquire_default(
layout.worktree_state_dir(),
crate::commands::WORKTREE_LOCK,
)?;
let original_head = refs::read_head(&layout).ok();
let (branch, local_tip, remote_tip) = match &original_head {
Some(Head::Branch(head_branch)) => {
let want_branch = target_branch.unwrap_or(head_branch.as_str());
let local_tip = refs::read_ref(&layout, want_branch)?;
let selected = if local_tip.is_some() || target_branch.is_some() {
remote_refs
.iter()
.find(|(name, _)| name == want_branch)
.ok_or_else(|| DispatchError::RemoteBranchMissing(want_branch.to_owned()))?
} else {
remote_refs
.iter()
.find(|(name, _)| name == want_branch)
.unwrap_or(&remote_refs[0])
};
(selected.0.clone(), local_tip, selected.1)
}
Some(Head::Detached(_)) => return Err(DispatchError::DetachedHead),
None => (remote_refs[0].0.clone(), None, remote_refs[0].1),
};
let ref_condition = if let Some(local_tip) = local_tip {
if local_tip == remote_tip {
return Ok(n);
}
if !is_ancestor(&store, local_tip, remote_tip)? {
return Err(DispatchError::NonFastForwardPull { branch });
}
refs::RefWriteCondition::Match(local_tip)
} else {
refs::RefWriteCondition::Missing
};
let tree = load_tree_hash(&store, remote_tip)?;
crate::commands::ensure_restore_safe(&layout, &store, tree)
.map_err(DispatchError::RestoreSafety)?;
crate::commands::write_ref_recording_history(&layout, &branch, ref_condition, &remote_tip)?;
if let Err(e) = refs::write_head_branch(&layout, &branch) {
rollback_pull_ref(&layout, &branch, local_tip, remote_tip)?;
return Err(e.into());
}
if let Err(e) = crate::commands::restore_worktree_and_index(&layout, &store, tree) {
if let Err(rollback) =
rollback_pull_ref_and_head(&layout, &branch, local_tip, remote_tip, original_head)
{
return Err(DispatchError::RestoreSafety(format!(
"{e}; additionally failed to roll back ref: {rollback}"
)));
}
return Err(DispatchError::RestoreSafety(e));
}
Ok(n)
}
fn rollback_pull_ref_and_head(
layout: &RepoLayout,
branch: &str,
local_tip: Option<Hash>,
remote_tip: Hash,
original_head: Option<Head>,
) -> Result<(), String> {
rollback_pull_ref(layout, branch, local_tip, remote_tip).map_err(|e| e.to_string())?;
match original_head {
Some(Head::Branch(name)) => refs::write_head_branch(layout, &name),
Some(Head::Detached(hash)) => refs::write_head_detached(layout, &hash),
None => Ok(()),
}
.map_err(|e| e.to_string())
}
fn rollback_pull_ref(
layout: &RepoLayout,
branch: &str,
local_tip: Option<Hash>,
remote_tip: Hash,
) -> Result<(), refs::RefError> {
if let Some(local_tip) = local_tip {
crate::commands::write_ref_recording_history(
layout,
branch,
refs::RefWriteCondition::Match(remote_tip),
&local_tip,
)
} else if refs::read_ref(layout, branch)? == Some(remote_tip) {
refs::delete_ref(layout, branch)
} else {
Ok(())
}
}
pub fn fetch_all(cwd: &Path, tx: &dyn Transport, remote: &str) -> Result<usize, DispatchError> {
fetch_all_with(cwd, tx, remote, true)
}
pub fn fetch_all_with(
cwd: &Path,
tx: &dyn Transport,
remote: &str,
require_signed: bool,
) -> Result<usize, DispatchError> {
let layout = mkit_core::layout::discover(cwd)?;
let store = crate::commands::open_store_configured(&layout)?;
fetch_objects(&store, &layout, tx, remote, require_signed)
}
fn fetch_objects(
store: &ObjectStore,
layout: &RepoLayout,
tx: &dyn Transport,
remote: &str,
require_signed: bool,
) -> Result<usize, DispatchError> {
let mut applied = AppliedPacks::load_or_empty(layout, remote);
let result = fetch_objects_inner(store, layout, tx, remote, &mut applied, require_signed);
persist_record(&mut applied, remote);
result
}
fn fetch_objects_inner(
store: &ObjectStore,
layout: &RepoLayout,
tx: &dyn Transport,
remote: &str,
applied: &mut AppliedPacks,
require_signed: bool,
) -> Result<usize, DispatchError> {
let remote_refs = tx.list_refs("refs/heads/")?;
let mut n = 0;
let mut tracking = refs::RemoteRefBatch::new(layout, remote)?;
let result: Result<(), DispatchError> = (|| {
for r in remote_refs {
if crate::signal::is_shutdown() {
return Err(DispatchError::Interrupted);
}
let Some(h) = r.hash else { continue };
let Some(chain_head) = tx.read_ref(&packmap_ref(&r.name))? else {
return Err(DispatchError::PackmapMissing(r.name.clone()));
};
let fetched = resolve_and_download_chain(tx, &r.name, chain_head, applied)?;
let lock = mkit_core::repo_lock::acquire_default(
layout.worktree_state_dir(),
crate::commands::WORKTREE_LOCK,
)?;
let (published_tip, _lock) = match apply_fetched_chain(
store,
tx,
remote,
&r.name,
fetched,
h,
applied,
require_signed,
) {
Ok(()) => (h, lock),
Err(e @ DispatchError::RemoteMissingObject(_)) => {
let (Some(fresh_h), Some(fresh_head)) = (
tx.read_ref(&format!("refs/heads/{}", r.name))?,
tx.read_ref(&packmap_ref(&r.name))?,
) else {
return Err(e);
};
drop(lock);
let fresh_fetched =
resolve_and_download_chain(tx, &r.name, fresh_head, applied)?;
let lock = mkit_core::repo_lock::acquire_default(
layout.worktree_state_dir(),
crate::commands::WORKTREE_LOCK,
)?;
apply_fetched_chain(
store,
tx,
remote,
&r.name,
fresh_fetched,
fresh_h,
applied,
require_signed,
)?;
(fresh_h, lock)
}
Err(e) => return Err(e),
};
tracking.write(&r.name, &published_tip)?;
n += 1;
}
Ok(())
})();
tracking.commit()?;
result?;
Ok(n)
}
fn persist_record(applied: &mut AppliedPacks, remote: &str) {
if let Err(e) = applied.persist() {
eprintln!(
"warning: could not persist applied-packs record for remote '{remote}' ({e}); it will be rebuilt on the next fetch"
);
}
}
pub(crate) fn verify_closure_present(store: &ObjectStore, tip: &Hash) -> Result<(), DispatchError> {
match mkit_core::ops::reachable_closure_checked(store, std::iter::once(tip)) {
Ok((_, false)) => Ok(()),
Ok((_, true)) => Err(DispatchError::ClosureTooLarge(
mkit_core::ops::graph::MAX_REACHABLE,
)),
Err(StoreError::ObjectNotFound(hex)) => Err(DispatchError::RemoteMissingObject(hex)),
Err(e) => Err(e.into()),
}
}
fn load_tree_hash(store: &ObjectStore, commit_hash: Hash) -> Result<Hash, DispatchError> {
match store.read_object(&commit_hash)? {
Object::Commit(c) => Ok(c.tree_hash),
Object::Remix(r) => Ok(r.tree_hash),
_ => Err(DispatchError::NotCommit),
}
}
#[cfg(test)]
mod tests {
use super::ssh_options_from_config;
use crate::config::Config;
#[test]
fn populated_config_maps_to_ssh_options() {
let cfg = Config {
ssh_strict_host_key_checking: "yes".to_string(),
ssh_user_known_hosts_file: "/path/to/project.known_hosts".to_string(),
ssh_identity_file: "/path/to/id_ed25519".to_string(),
..Config::default()
};
let opts = ssh_options_from_config(&cfg);
assert_eq!(opts.strict_host_key_checking, "yes");
assert_eq!(opts.user_known_hosts_file, "/path/to/project.known_hosts");
assert_eq!(opts.identity_file, "/path/to/id_ed25519");
}
#[test]
fn empty_config_maps_to_empty_ssh_options() {
let opts = ssh_options_from_config(&Config::default());
assert!(opts.strict_host_key_checking.is_empty());
assert!(opts.user_known_hosts_file.is_empty());
assert!(opts.identity_file.is_empty());
}
}