use std::collections::{BTreeMap, BTreeSet};
use std::io::{Read, Write};
use std::num::NonZeroU32;
use std::ops::ControlFlow;
use std::path::Path;
use std::sync::atomic::AtomicBool;
use super::{
Advertisement, FetchError, FetchInstalled, FetchLimits, FetchUpdateLimits, KnownHistory,
ReceivedFetch,
};
use crate::refs::{Expected, RefEdit, RefEditOutcome, RefName, ReferenceError, Reflog, Target};
use crate::remote::{Direction, Mapping, MappingError, Refspecs};
use crate::transport::TransportControl;
use crate::{ObjectId, Repository};
#[derive(Debug)]
pub struct FetchRequest {
repository: Repository,
specs: Refspecs,
snapshot: BTreeMap<RefName, Target>,
authorized_force: BTreeSet<RefName>,
reflog: Reflog,
namespace_reflogs: Vec<(RefName, Reflog, Reflog)>,
kind_reflogs: Vec<(RefName, FetchUpdateKind, Reflog)>,
additional_tag_namespaces: Vec<RefName>,
retained_missing_sources: BTreeSet<Vec<u8>>,
prune: bool,
pub(super) depth: Option<NonZeroU32>,
shallow_before: Vec<ObjectId>,
}
impl FetchRequest {
pub fn prepare(
repository: Repository,
specs: Refspecs,
authorized_force: BTreeSet<RefName>,
reflog: Reflog,
) -> Result<Self, FetchPlanError> {
Self::prepare_from(repository, specs, authorized_force, reflog, |repository| {
Ok(repository.references()?.list()?)
})
}
pub(crate) fn prepare_listed(
repository: Repository,
specs: Refspecs,
authorized_force: BTreeSet<RefName>,
reflog: Reflog,
references: Vec<crate::refs::Reference>,
) -> Result<Self, FetchPlanError> {
Self::prepare_from(repository, specs, authorized_force, reflog, |_| {
Ok(references)
})
}
fn prepare_from(
repository: Repository,
specs: Refspecs,
authorized_force: BTreeSet<RefName>,
reflog: Reflog,
list: impl FnOnce(&Repository) -> Result<Vec<crate::refs::Reference>, FetchPlanError>,
) -> Result<Self, FetchPlanError> {
if specs.direction() != Direction::Fetch {
return Err(FetchPlanError::Mapping(MappingError::Direction));
}
super::worktree::check(&repository)?;
let references = list(&repository)?;
let snapshot = references
.into_iter()
.map(|reference| (reference.name, reference.target))
.collect();
let shallow_before = repository.shallow_roots().iter().collect();
Ok(Self {
repository,
specs,
snapshot,
authorized_force,
reflog,
namespace_reflogs: Vec::new(),
kind_reflogs: Vec::new(),
additional_tag_namespaces: Vec::new(),
retained_missing_sources: BTreeSet::new(),
prune: false,
depth: None,
shallow_before,
})
}
pub fn with_tag_destination_namespace(mut self, namespace: RefName) -> Self {
self.additional_tag_namespaces.push(namespace);
self
}
pub fn with_reflog_for_namespace(
mut self,
namespace: RefName,
update: Reflog,
prune: Reflog,
) -> Self {
self.namespace_reflogs.push((namespace, update, prune));
self
}
pub fn with_reflog_for_update_kind(
mut self,
namespace: RefName,
kind: FetchUpdateKind,
reflog: Reflog,
) -> Self {
self.kind_reflogs.push((namespace, kind, reflog));
self
}
fn reflog_for(&self, name: &RefName, kind: FetchUpdateKind) -> Reflog {
if let Some((_, _, reflog)) = self
.kind_reflogs
.iter()
.filter(|(namespace, selected, _)| *selected == kind && is_descendant(name, namespace))
.max_by_key(|(namespace, _, _)| namespace.as_bytes().len())
{
return reflog.clone();
}
self.namespace_reflogs
.iter()
.filter(|(namespace, _, _)| is_descendant(name, namespace))
.max_by_key(|(namespace, _, _)| namespace.as_bytes().len())
.map(|(_, update, prune)| {
if kind == FetchUpdateKind::Prune {
prune
} else {
update
}
})
.unwrap_or(&self.reflog)
.clone()
}
fn additional_tag_destination(&self, name: &RefName) -> bool {
self.additional_tag_namespaces
.iter()
.any(|namespace| is_descendant(name, namespace))
}
pub fn with_depth(mut self, depth: NonZeroU32) -> Self {
self.depth = Some(depth);
self
}
pub fn with_prune(mut self) -> Self {
self.prune = true;
self
}
pub fn with_retained_missing_sources(
mut self,
sources: impl IntoIterator<Item = RefName>,
) -> Self {
self.retained_missing_sources
.extend(sources.into_iter().map(|source| source.as_bytes().to_vec()));
self
}
pub fn plan(&self, advertisement: &Advertisement) -> Result<Vec<FetchUpdate>, FetchPlanError> {
let mut updates: Vec<FetchUpdate> = self
.specs
.map_advertisement_with_missing(advertisement)?
.0
.into_iter()
.map(|mapping| {
let Some(name) = &mapping.destination else {
return Ok(FetchUpdate {
mapping,
previous: None,
kind: FetchUpdateKind::SourceOnly,
});
};
let bytes = name.as_bytes();
let tag_destination =
bytes.starts_with(b"refs/tags/") || self.additional_tag_destination(name);
if !bytes.starts_with(b"refs/remotes/") && !tag_destination {
return Err(FetchPlanError::Destination(name.clone()));
}
let previous = match self.snapshot.get(name) {
Some(Target::Symbolic(_)) => {
return Err(FetchPlanError::Symbolic(name.clone()));
}
Some(Target::Direct(id)) => Some(*id),
None => None,
};
let new = mapping
.source
.as_ref()
.expect("fetch mapping has a source")
.id;
let kind = update_kind(
name,
previous,
new,
mapping.force,
self.authorized_force.contains(name),
tag_destination,
)?;
Ok(FetchUpdate {
mapping,
previous,
kind,
})
})
.collect::<Result<_, _>>()?;
if self.prune {
let advertised: BTreeSet<_> = advertisement
.refs
.iter()
.filter(|reference| !reference.peeled)
.map(|reference| reference.name.as_bytes().to_vec())
.collect();
let selected: BTreeSet<_> = updates
.iter()
.filter_map(|update| update.mapping.destination.as_ref())
.cloned()
.collect();
for (name, target) in &self.snapshot {
if !(name.as_bytes().starts_with(b"refs/remotes/")
|| self.additional_tag_destination(name))
|| selected.contains(name)
{
continue;
}
let sources = self.specs.prune_sources(name);
if sources.is_empty()
|| sources.iter().any(|source| {
advertised.contains(source)
|| self.retained_missing_sources.contains(source.as_slice())
})
{
continue;
}
let Target::Direct(previous) = target else {
continue;
};
updates.push(FetchUpdate {
mapping: Mapping {
source: None,
destination: Some(name.clone()),
force: false,
},
previous: Some(*previous),
kind: FetchUpdateKind::Prune,
});
}
}
Ok(updates)
}
pub fn receive_local(
self,
source: impl AsRef<Path>,
known: &KnownHistory,
limits: FetchLimits,
control: TransportControl<'_>,
progress: impl FnMut(&[u8]) -> ControlFlow<()>,
) -> Result<FetchReady, FetchWorkflowError> {
self.receive_local_with_progress(source, known, limits, control, progress, |_| {})
}
pub fn receive_local_with_progress(
self,
source: impl AsRef<Path>,
known: &KnownHistory,
limits: FetchLimits,
control: TransportControl<'_>,
progress: impl FnMut(&[u8]) -> ControlFlow<()>,
observe: impl FnMut(super::LocalFetchProgress),
) -> Result<FetchReady, FetchWorkflowError> {
self.receive_local_observed(source, known, limits, control, progress, observe, |_| {})
}
pub fn receive_local_with_validation_progress(
self,
source: impl AsRef<Path>,
known: &KnownHistory,
limits: FetchLimits,
control: TransportControl<'_>,
progress: impl FnMut(&[u8]) -> ControlFlow<()>,
validation: impl FnMut(super::ValidationProgress),
) -> Result<FetchReady, FetchWorkflowError> {
self.receive_local_observed(source, known, limits, control, progress, |_| {}, validation)
}
#[expect(
clippy::too_many_arguments,
reason = "independent construction and validation observers"
)]
fn receive_local_observed(
self,
source: impl AsRef<Path>,
known: &KnownHistory,
limits: FetchLimits,
control: TransportControl<'_>,
progress: impl FnMut(&[u8]) -> ControlFlow<()>,
mut observe: impl FnMut(super::LocalFetchProgress),
validation: impl FnMut(super::ValidationProgress),
) -> Result<FetchReady, FetchWorkflowError> {
self.check_known(known)?;
let mut plan = None;
let received = super::local::receive_local_observed(
source,
|advertisement| select(self.plan(advertisement), &mut plan),
known,
self.depth,
limits,
control,
progress,
|event| {
if event != super::LocalFetchProgress::Complete {
observe(event);
}
},
validation,
);
let updates = selected(plan, &received)?;
let ready = FetchReady {
request: self,
updates,
received: received?,
};
control.check().map_err(FetchError::from)?;
observe(super::LocalFetchProgress::Complete);
Ok(ready)
}
pub fn receive_session(
self,
reader: &mut impl Read,
writer: &mut impl Write,
known: &KnownHistory,
limits: FetchLimits,
cancel: &AtomicBool,
progress: impl FnMut(&[u8]) -> ControlFlow<()>,
) -> Result<FetchReady, FetchWorkflowError> {
self.check_known(known)?;
let mut plan = None;
let received = super::receive_with_known_depth(
reader,
writer,
|advertisement| select(self.plan(advertisement), &mut plan),
known,
super::FetchOptions {
limits,
depth: self.depth,
},
cancel,
progress,
);
let updates = selected(plan, &received)?;
Ok(FetchReady {
request: self,
updates,
received: received?,
})
}
pub(super) fn check_known(&self, known: &KnownHistory) -> Result<(), FetchError> {
if known.shallow != self.shallow_before {
return Err(FetchError::Unsupported(
"known shallow boundaries differ from destination",
));
}
Ok(())
}
}
fn is_descendant(name: &RefName, namespace: &RefName) -> bool {
name.as_bytes()
.strip_prefix(namespace.as_bytes())
.is_some_and(|suffix| suffix.starts_with(b"/") && suffix.len() > 1)
}
fn update_kind(
name: &RefName,
previous: Option<ObjectId>,
new: ObjectId,
force: bool,
authorized: bool,
tag_destination: bool,
) -> Result<FetchUpdateKind, FetchPlanError> {
match previous {
None => Ok(FetchUpdateKind::Create),
Some(old) if old == new => Ok(FetchUpdateKind::Unchanged),
Some(_) if tag_destination => {
if !force || !authorized {
return Err(FetchPlanError::TagReplacement(name.clone()));
}
Ok(FetchUpdateKind::ForcedTag)
}
Some(_) => Ok(FetchUpdateKind::Replace),
}
}
pub(super) fn select(
result: Result<Vec<FetchUpdate>, FetchPlanError>,
saved: &mut Option<Result<Vec<FetchUpdate>, FetchPlanError>>,
) -> Vec<ObjectId> {
let wants = result
.as_ref()
.map(|updates| {
updates
.iter()
.filter_map(|update| update.mapping.source.as_ref().map(|s| s.id))
.collect()
})
.unwrap_or_default();
*saved = Some(result);
wants
}
pub(super) fn selected<T>(
plan: Option<Result<Vec<FetchUpdate>, FetchPlanError>>,
transfer: &Result<T, FetchError>,
) -> Result<Vec<FetchUpdate>, FetchWorkflowError> {
match plan {
Some(plan) => Ok(plan?),
None if transfer.is_err() => Ok(Vec::new()),
None => Err(FetchError::Protocol("selection callback was not invoked").into()),
}
}
#[derive(Debug)]
pub struct FetchReady {
pub(super) request: FetchRequest,
pub(super) updates: Vec<FetchUpdate>,
pub(super) received: ReceivedFetch,
}
impl FetchReady {
pub(crate) fn for_clone(
request: FetchRequest,
received: ReceivedFetch,
) -> Result<Self, FetchPlanError> {
let updates = request.plan(received.advertisement())?;
Ok(Self {
request,
updates,
received,
})
}
pub fn updates(&self) -> &[FetchUpdate] {
&self.updates
}
pub fn received(&self) -> &ReceivedFetch {
&self.received
}
pub fn install_retained(
mut self,
limits: FetchUpdateLimits,
cancel: &AtomicBool,
) -> Result<RetainedFetchReady, RetainedFetchFinishError> {
let mut report = self.take_report();
let prepare = || {
let checksum = self
.received
.retention_checksum(&self.request.repository, cancel)
.map_err(FetchFinishFailure::Installation)?;
let shallow_lock = self.lock_shallow(true, cancel)?;
Ok((checksum, shallow_lock))
};
let (checksum, shallow_lock) = match prepare() {
Ok(prepared) => prepared,
Err(source) => {
return Err(RetainedFetchFinishError {
report: Box::new(report),
source,
retention: None,
});
}
};
let installation =
super::retention::install_with(&self.request.repository, checksum, || {
self.received.install_for_workflow(
&self.request.repository,
limits.snapshot,
cancel,
shallow_lock.is_some(),
)
});
let (installed, retention) = match installation {
Ok(result) => result,
Err(error) => {
return Err(RetainedFetchFinishError {
report: Box::new(report),
source: FetchFinishFailure::Installation(*error.source).into(),
retention: error.retention,
});
}
};
report.installed = Some(installed);
Ok(RetainedFetchReady {
ready: self,
report,
limits,
shallow_lock,
retention,
})
}
pub fn finish(
mut self,
limits: FetchUpdateLimits,
cancel: &AtomicBool,
) -> Result<FetchReport, FetchFinishError> {
#[cfg(feature = "tracing")]
let span = tracing::debug_span!(
target: "girt",
"fetch.finish",
outcome = "incomplete",
failure_class = tracing::field::Empty,
effects = tracing::field::Empty,
);
let operation = || {
let mut report = self.take_report();
let result = self.install_publish(limits, cancel, &mut report);
match result {
Ok(()) => Ok(report),
Err(source) => Err(FetchFinishError {
report: Box::new(report),
source,
}),
}
};
#[cfg(feature = "tracing")]
let result = span.in_scope(operation);
#[cfg(not(feature = "tracing"))]
let result = { operation }();
#[cfg(feature = "tracing")]
crate::trace::finish(&span, &result, |error| {
crate::trace::fetch_finish(error, &span)
});
result
}
fn install_publish(
&self,
limits: FetchUpdateLimits,
cancel: &AtomicBool,
report: &mut FetchReport,
) -> Result<(), Box<FetchFinishFailure>> {
let mut shallow_lock = self.lock_shallow(false, cancel)?;
report.installed = Some(
self.received
.install_for_workflow(
&self.request.repository,
limits.snapshot,
cancel,
shallow_lock.is_some(),
)
.map_err(FetchFinishFailure::Installation)?,
);
self.publish_installed(limits, cancel, &mut shallow_lock, report)
}
fn take_report(&mut self) -> FetchReport {
FetchReport {
updates: std::mem::take(&mut self.updates),
pack_bytes: self.received.pack_bytes(),
objects: self.received.object_count(),
installed: None,
references: Vec::new(),
shallow_published: false,
missing: self
.request
.specs
.map_advertisement_with_missing(self.received.advertisement())
.expect("validated fetch mapping")
.1,
}
}
fn lock_shallow(
&self,
retained: bool,
cancel: &AtomicBool,
) -> Result<Option<super::shallow::Lock>, Box<FetchFinishFailure>> {
let resulting_roots = self.received.shallow_roots();
if self.received.wants().is_empty() && self.request.shallow_before != resulting_roots {
return Err(FetchFinishFailure::Safety(FetchPlanError::Shallow).into());
}
if self.request.depth.is_none() && self.request.shallow_before != resulting_roots {
return Err(FetchFinishFailure::Safety(FetchPlanError::Shallow).into());
}
let shallow_lock =
if retained || !self.request.shallow_before.is_empty() || !resulting_roots.is_empty() {
Some(
super::shallow::Lock::acquire(
self.request.repository.common_dir(),
self.request.repository.object_format(),
&self.request.shallow_before,
cancel,
)
.map_err(FetchFinishFailure::Shallow)?,
)
} else {
None
};
Ok(shallow_lock)
}
fn publish_installed(
&self,
limits: FetchUpdateLimits,
cancel: &AtomicBool,
shallow_lock: &mut Option<super::shallow::Lock>,
report: &mut FetchReport,
) -> Result<(), Box<FetchFinishFailure>> {
let resulting_roots = self.received.shallow_roots();
if !self.received.wants().is_empty() && self.request.shallow_before != resulting_roots {
let objects = self
.request
.repository
.objects(limits.snapshot)
.map_err(|error| {
FetchFinishFailure::BeforePublication(FetchError::Destination(error))
})?;
let mut verify_roots = self.received.wants().to_vec();
verify_roots.extend(self.request.shallow_before.iter().copied());
let verified = KnownHistory::new_with_boundaries(
&objects,
&verify_roots,
resulting_roots,
limits.verification,
cancel,
)
.map_err(FetchFinishFailure::BeforePublication)?;
if resulting_roots
.iter()
.any(|root| !verified.objects.contains_key(root))
{
return Err(FetchFinishFailure::BeforePublication(FetchError::Protocol(
"unrelated shallow boundary",
))
.into());
}
}
if let Some(lock) = shallow_lock {
report.shallow_published = lock
.publish(resulting_roots)
.map_err(FetchFinishFailure::Shallow)?;
}
super::check_cancelled(cancel).map_err(FetchFinishFailure::BeforePublication)?;
super::worktree::check(&self.request.repository).map_err(FetchFinishFailure::Safety)?;
if !self.received.wants().is_empty() {
let reopened = Repository::open(self.request.repository.git_dir())
.map_err(FetchFinishFailure::Reopen)?;
let objects = reopened.objects(limits.snapshot).map_err(|error| {
FetchFinishFailure::BeforePublication(FetchError::Destination(error))
})?;
let mut verify_roots = self.received.wants().to_vec();
verify_roots.extend(self.request.shallow_before.iter().copied());
let published = self
.request
.snapshot
.values()
.filter_map(|target| match target {
Target::Direct(id) => Some(*id),
Target::Symbolic(_) => None,
})
.collect();
let shallow: Vec<_> = objects.shallow_roots().iter().collect();
let verified =
super::verified::InstalledObjects::bind(&objects, &self.received, cancel)
.map_err(FetchFinishFailure::BeforePublication)?;
KnownHistory::verify_installed(
&verified,
&verify_roots,
self.received.dependencies(),
&shallow,
&published,
limits.verification,
cancel,
)
.map_err(FetchFinishFailure::BeforePublication)?;
let validation = super::update::validate(
&mut report.updates,
&verified,
&self.request.authorized_force,
limits,
cancel,
);
validation.map_err(FetchFinishFailure::Update)?;
}
super::check_cancelled(cancel).map_err(FetchFinishFailure::BeforePublication)?;
let edits: Vec<_> = report
.updates
.iter()
.filter(|update| update.kind.changes_ref())
.map(|update| {
let name = update
.mapping
.destination
.as_ref()
.expect("changed destination");
RefEdit {
name: name.clone(),
dereference: false,
target: update
.mapping
.source
.as_ref()
.map(|source| Target::Direct(source.id)),
expected: update
.previous
.map(|id| Expected::Value(Target::Direct(id)))
.unwrap_or(Expected::Absent),
reflog: self.request.reflog_for(name, update.kind),
}
})
.collect();
let refs = self
.request
.repository
.references()
.map_err(FetchPlanError::from)
.map_err(FetchFinishFailure::Safety)?;
let (prunes, updates): (Vec<_>, Vec<_>) =
edits.into_iter().partition(|edit| edit.target.is_none());
report.references = refs
.transaction(&prunes)
.map_err(FetchFinishFailure::Publication)?;
match refs.transaction(&updates) {
Ok(outcomes) => report.references.extend(outcomes),
Err(crate::refs::TransactionError::Prepare { source, .. })
if !report.references.is_empty() =>
{
return Err(Box::new(FetchFinishFailure::Publication(
crate::refs::TransactionError::Publish {
outcomes: std::mem::take(&mut report.references),
source,
},
)));
}
Err(crate::refs::TransactionError::Publish {
mut outcomes,
source,
}) => {
let mut all = std::mem::take(&mut report.references);
all.append(&mut outcomes);
return Err(Box::new(FetchFinishFailure::Publication(
crate::refs::TransactionError::Publish {
outcomes: all,
source,
},
)));
}
Err(error) => return Err(Box::new(FetchFinishFailure::Publication(error))),
}
Ok(())
}
}
#[derive(Debug)]
#[must_use = "publish references or recover the retained installation explicitly"]
pub struct RetainedFetchReady {
ready: FetchReady,
report: FetchReport,
limits: FetchUpdateLimits,
shallow_lock: Option<super::shallow::Lock>,
retention: super::FetchRetention,
}
impl RetainedFetchReady {
pub fn updates(&self) -> &[FetchUpdate] {
&self.report.updates
}
pub fn report(&self) -> &FetchReport {
&self.report
}
pub fn retention(&self) -> &super::FetchRetention {
&self.retention
}
pub fn finish(
mut self,
cancel: &AtomicBool,
) -> Result<(FetchReport, super::FetchRetention), RetainedFetchFinishError> {
let result = self.ready.publish_installed(
self.limits,
cancel,
&mut self.shallow_lock,
&mut self.report,
);
match result {
Ok(()) => Ok((self.report, self.retention)),
Err(source) => Err(RetainedFetchFinishError {
report: Box::new(self.report),
source,
retention: Some(self.retention),
}),
}
}
pub fn into_recovery(self) -> (FetchReport, super::FetchRetention) {
(self.report, self.retention)
}
}
#[derive(Debug, thiserror::Error)]
#[error("fetch finish failed")]
pub struct RetainedFetchFinishError {
pub report: Box<FetchReport>,
#[source]
pub source: Box<FetchFinishFailure>,
pub retention: Option<super::FetchRetention>,
}
#[derive(Clone, Debug)]
pub struct FetchUpdate {
pub mapping: Mapping,
pub previous: Option<ObjectId>,
pub kind: FetchUpdateKind,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum FetchUpdateKind {
SourceOnly,
Create,
Unchanged,
Replace,
FastForward,
ForcedTracking,
ForcedTag,
Prune,
}
impl FetchUpdateKind {
fn changes_ref(self) -> bool {
matches!(
self,
Self::Create
| Self::Replace
| Self::FastForward
| Self::ForcedTracking
| Self::ForcedTag
| Self::Prune
)
}
}
#[derive(Debug)]
pub struct FetchReport {
pub updates: Vec<FetchUpdate>,
pub pack_bytes: usize,
pub objects: usize,
pub installed: Option<FetchInstalled>,
pub shallow_published: bool,
pub references: Vec<RefEditOutcome>,
pub missing: Vec<Vec<u8>>,
}
#[derive(Debug, thiserror::Error)]
pub enum FetchPlanError {
#[error("shallow boundary change requires explicit depth request")]
Shallow,
#[error(transparent)]
Mapping(#[from] MappingError),
#[error("unsupported fetch destination: {0:?}")]
Destination(RefName),
#[error("symbolic fetch destination: {0:?}")]
Symbolic(RefName),
#[error("tag replacement requires force intent and authorization: {0:?}")]
TagReplacement(RefName),
#[error(transparent)]
Reference(#[from] ReferenceError),
#[error(transparent)]
Worktree(#[from] crate::OpenError),
#[error("worktree enumeration")]
Io(#[from] std::io::Error),
#[error("unsupported HEAD chain at {0:?}")]
Head(RefName),
}
#[derive(Debug, thiserror::Error)]
pub enum FetchWorkflowError {
#[error(transparent)]
Plan(#[from] FetchPlanError),
#[error(transparent)]
Transfer(#[from] FetchError),
}
#[derive(Debug, thiserror::Error)]
#[error("fetch finish failed")]
pub struct FetchFinishError {
pub report: Box<FetchReport>,
#[source]
pub source: Box<FetchFinishFailure>,
}
#[derive(Debug, thiserror::Error)]
pub enum FetchFinishFailure {
#[error("fetch installation")]
Installation(#[source] FetchError),
#[error("fetch shallow publication")]
Shallow(#[from] super::FetchShallowError),
#[error("fetch repository reopen")]
Reopen(#[from] crate::OpenError),
#[error("before fetch publication")]
BeforePublication(#[source] FetchError),
#[error("fetch publication safety")]
Safety(#[from] FetchPlanError),
#[error("fetch update validation")]
Update(#[from] super::FetchUpdateError),
#[error("fetch reference publication")]
Publication(#[from] crate::refs::TransactionError),
}
#[cfg(test)]
#[path = "workflow_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "single_pass_tests.rs"]
mod single_pass_tests;