use crate::lineage::PackageLineage;
use crate::lineage::UpstreamState;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PackageState {
Latest,
PendingCommit,
PendingChanges { files: usize },
Behind,
Diverged,
Unpublished,
NoRemote,
Unknown,
}
impl PackageState {
#[must_use]
pub fn resolve(
upstream: UpstreamState,
has_local_commit: bool,
has_remote: bool,
changed_files: Option<usize>,
) -> Self {
let pending_changes = match changed_files {
Some(files) if files > 0 => Some(Self::PendingChanges { files }),
_ => None,
};
match upstream {
UpstreamState::Local if has_remote => Self::Unpublished,
UpstreamState::Local => Self::NoRemote,
UpstreamState::Behind => Self::Behind,
UpstreamState::Diverged => Self::Diverged,
UpstreamState::Error => Self::Unknown,
UpstreamState::Ahead => pending_changes.unwrap_or(Self::PendingCommit),
UpstreamState::UpToDate => pending_changes.unwrap_or({
if has_local_commit {
Self::PendingCommit
} else {
Self::Latest
}
}),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PackageStateReport {
pub state: PackageState,
pub upstream_state: UpstreamState,
pub changed_files: Option<usize>,
pub latest_refreshed: bool,
}
impl PackageStateReport {
#[must_use]
pub(crate) fn without_remote(lineage: &PackageLineage) -> Option<Self> {
if lineage.misconfigured_remote() {
return Some(Self {
state: PackageState::Unknown,
upstream_state: UpstreamState::Error,
changed_files: None,
latest_refreshed: false,
});
}
if lineage.remote_uri.is_none() {
let upstream_state = UpstreamState::from(lineage.clone());
return Some(Self {
state: PackageState::resolve(upstream_state, lineage.commit.is_some(), false, None),
upstream_state,
changed_files: None,
latest_refreshed: false,
});
}
None
}
}
impl PackageLineage {
#[must_use]
pub fn misconfigured_remote(&self) -> bool {
self.remote_uri
.as_ref()
.is_some_and(|uri| uri.origin.is_none())
}
}
#[cfg(test)]
mod tests {
use super::*;
use quilt_uri::Host;
use quilt_uri::ManifestUri;
use quilt_uri::fixtures;
fn resolve(
upstream: UpstreamState,
has_local_commit: bool,
has_remote: bool,
changed_files: Option<usize>,
) -> PackageState {
PackageState::resolve(upstream, has_local_commit, has_remote, changed_files)
}
#[test]
fn local_with_no_bucket_is_no_remote() {
assert_eq!(
resolve(UpstreamState::Local, false, false, None),
PackageState::NoRemote
);
}
#[test]
fn local_with_a_bucket_is_unpublished() {
assert_eq!(
resolve(UpstreamState::Local, false, true, None),
PackageState::Unpublished
);
}
#[test]
fn up_to_date_with_nothing_local_is_latest() {
assert_eq!(
resolve(UpstreamState::UpToDate, false, true, None),
PackageState::Latest
);
}
#[test]
fn up_to_date_with_a_local_revision_is_pending_commit() {
assert_eq!(
resolve(UpstreamState::UpToDate, true, true, None),
PackageState::PendingCommit
);
}
#[test]
fn ahead_is_pending_commit() {
assert_eq!(
resolve(UpstreamState::Ahead, false, true, None),
PackageState::PendingCommit
);
}
#[test]
fn behind_carries_no_count() {
assert_eq!(
resolve(UpstreamState::Behind, false, true, None),
PackageState::Behind,
"a hash inequality is not a distance; no revision count is derivable"
);
}
#[test]
fn diverged_maps_straight_through() {
assert_eq!(
resolve(UpstreamState::Diverged, false, true, None),
PackageState::Diverged
);
}
#[test]
fn error_becomes_the_fallback() {
assert_eq!(
resolve(UpstreamState::Error, false, true, None),
PackageState::Unknown
);
}
#[test]
fn a_measured_working_tree_beats_an_unpushed_revision() {
assert_eq!(
resolve(UpstreamState::UpToDate, true, true, Some(3)),
PackageState::PendingChanges { files: 3 }
);
assert_eq!(
resolve(UpstreamState::Ahead, false, true, Some(2)),
PackageState::PendingChanges { files: 2 }
);
}
#[test]
fn a_measured_clean_tree_falls_through_to_the_revision_state() {
assert_eq!(
resolve(UpstreamState::UpToDate, false, true, Some(0)),
PackageState::Latest
);
assert_eq!(
resolve(UpstreamState::UpToDate, true, true, Some(0)),
PackageState::PendingCommit
);
assert_eq!(
resolve(UpstreamState::Ahead, false, true, Some(0)),
PackageState::PendingCommit
);
}
#[test]
fn a_count_never_outranks_a_state_above_it() {
assert_eq!(
resolve(UpstreamState::Behind, false, true, Some(4)),
PackageState::Behind
);
assert_eq!(
resolve(UpstreamState::Diverged, false, true, Some(4)),
PackageState::Diverged
);
assert_eq!(
resolve(UpstreamState::Local, false, false, Some(4)),
PackageState::NoRemote
);
assert_eq!(
resolve(UpstreamState::Local, false, true, Some(4)),
PackageState::Unpublished
);
}
#[test]
fn an_unmeasured_tree_agrees_with_a_measured_empty_one() {
for upstream in [
UpstreamState::UpToDate,
UpstreamState::Ahead,
UpstreamState::Behind,
UpstreamState::Diverged,
UpstreamState::Local,
UpstreamState::Error,
] {
for has_local_commit in [true, false] {
assert_eq!(
resolve(upstream, has_local_commit, true, None),
resolve(upstream, has_local_commit, true, Some(0)),
"{upstream:?} / commit={has_local_commit} disagreed"
);
}
}
}
fn manifest_uri(origin: Option<Host>) -> ManifestUri {
ManifestUri {
origin,
bucket: "acme-research".to_string(),
namespace: ("team", "one").into(),
hash: "abcdef".to_string(),
}
}
#[test]
fn a_remote_with_no_catalog_host_is_not_read_through_the_hashes() {
let mut lineage = PackageLineage::from_remote(manifest_uri(None), "abcdef".to_string());
lineage.latest_hash = "abcdef".to_string();
assert!(lineage.misconfigured_remote());
assert!(
!PackageLineage::from_remote(
manifest_uri(Some(fixtures::host())),
"abcdef".to_string()
)
.misconfigured_remote()
);
assert!(!PackageLineage::default().misconfigured_remote());
}
#[test]
fn without_remote_answers_only_when_the_remote_is_not_asked() {
let misconfigured = PackageLineage::from_remote(manifest_uri(None), "abcdef".to_string());
assert_eq!(
PackageStateReport::without_remote(&misconfigured),
Some(PackageStateReport {
state: PackageState::Unknown,
upstream_state: UpstreamState::Error,
changed_files: None,
latest_refreshed: false,
})
);
assert_eq!(
PackageStateReport::without_remote(&PackageLineage::default()),
Some(PackageStateReport {
state: PackageState::NoRemote,
upstream_state: UpstreamState::Local,
changed_files: None,
latest_refreshed: false,
})
);
let configured =
PackageLineage::from_remote(manifest_uri(Some(fixtures::host())), "abcdef".to_string());
assert_eq!(PackageStateReport::without_remote(&configured), None);
}
}