use std::path::{Path, PathBuf};
use grpc::heddle::v1::{EdgeSkip, MonorepoNode};
use objects::object::ChangeId;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MonorepoCloneOp {
pub spool_id: String,
pub content_state: Option<ChangeId>,
pub rel_path: PathBuf,
}
impl MonorepoCloneOp {
pub fn dest_path(&self, clone_root: &Path) -> PathBuf {
if self.rel_path.as_os_str().is_empty() {
clone_root.to_path_buf()
} else {
clone_root.join(&self.rel_path)
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SkippedChild {
pub child_spool_id: String,
pub mount_name: String,
pub rel_path: PathBuf,
pub reason: EdgeSkip,
}
impl SkippedChild {
pub fn reason_label(&self) -> &'static str {
match self.reason {
EdgeSkip::Unspecified => "unspecified",
EdgeSkip::Unreadable => "unreadable",
EdgeSkip::Cycle => "cycle",
EdgeSkip::DepthBounded => "depth-bounded",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct MonorepoClonePlan {
pub ops: Vec<MonorepoCloneOp>,
pub skipped: Vec<SkippedChild>,
}
impl MonorepoClonePlan {
pub fn from_resolved(root: &MonorepoNode) -> Self {
let mut plan = MonorepoClonePlan::default();
plan.walk(root, PathBuf::new());
plan
}
fn walk(&mut self, node: &MonorepoNode, rel_path: PathBuf) {
let content_state = node
.content_state
.as_deref()
.and_then(|bytes| ChangeId::try_from_slice(bytes).ok());
self.ops.push(MonorepoCloneOp {
spool_id: node.spool_id.clone(),
content_state,
rel_path: rel_path.clone(),
});
for edge in &node.edges {
let child_rel = rel_path.join(&edge.mount_name);
match (&edge.subtree, edge.skipped) {
(Some(subtree), _) => self.walk(subtree, child_rel),
(None, skipped) => {
let reason = skipped
.and_then(|s| EdgeSkip::try_from(s).ok())
.unwrap_or(EdgeSkip::Unspecified);
self.skipped.push(SkippedChild {
child_spool_id: edge.child_spool_id.clone(),
mount_name: edge.mount_name.clone(),
rel_path: child_rel,
reason,
});
}
}
}
}
}
#[cfg(test)]
mod tests {
use grpc::heddle::v1::{ChildEdgeStatus, MonorepoEdge};
use super::*;
fn cid(seed: u8) -> ChangeId {
ChangeId::from_bytes([seed; 16])
}
fn cid_bytes(seed: u8) -> Vec<u8> {
cid(seed).as_bytes().to_vec()
}
fn leaf(spool_id: &str, content: u8) -> MonorepoNode {
MonorepoNode {
spool_id: spool_id.to_string(),
content_state: Some(cid_bytes(content)),
edges: vec![],
}
}
fn descended_edge(
mount: &str,
child_id: &str,
anchor: u8,
subtree: MonorepoNode,
) -> MonorepoEdge {
MonorepoEdge {
mount_name: mount.to_string(),
child_spool_id: child_id.to_string(),
anchored_state_id: cid_bytes(anchor),
child_head: Some(cid_bytes(anchor)),
status: ChildEdgeStatus::UpToDate as i32,
subtree: Some(subtree),
skipped: None,
}
}
fn skipped_edge(mount: &str, child_id: &str, anchor: u8, reason: EdgeSkip) -> MonorepoEdge {
MonorepoEdge {
mount_name: mount.to_string(),
child_spool_id: child_id.to_string(),
anchored_state_id: cid_bytes(anchor),
child_head: None,
status: ChildEdgeStatus::Unspecified as i32,
subtree: None,
skipped: Some(reason as i32),
}
}
fn fixture_tree() -> MonorepoNode {
let grandchild = leaf("acme/grandchild", 3);
let child_a = MonorepoNode {
spool_id: "acme/child-a".to_string(),
content_state: Some(cid_bytes(2)),
edges: vec![descended_edge("vendor", "acme/grandchild", 3, grandchild)],
};
MonorepoNode {
spool_id: "acme/root".to_string(),
content_state: Some(cid_bytes(1)),
edges: vec![
descended_edge("libs", "acme/child-a", 2, child_a),
skipped_edge("secret", "acme/child-b", 9, EdgeSkip::Unreadable),
],
}
}
#[test]
fn planner_places_each_spool_at_its_mount_path_and_anchored_state() {
let plan = MonorepoClonePlan::from_resolved(&fixture_tree());
assert_eq!(plan.ops.len(), 3, "root + child-a + grandchild");
assert_eq!(plan.ops[0].spool_id, "acme/root");
assert_eq!(plan.ops[0].rel_path, PathBuf::new());
assert_eq!(plan.ops[0].content_state, Some(cid(1)));
assert_eq!(plan.ops[1].spool_id, "acme/child-a");
assert_eq!(plan.ops[1].rel_path, PathBuf::from("libs"));
assert_eq!(plan.ops[1].content_state, Some(cid(2)));
assert_eq!(plan.ops[2].spool_id, "acme/grandchild");
assert_eq!(plan.ops[2].rel_path, PathBuf::from("libs").join("vendor"));
assert_eq!(plan.ops[2].content_state, Some(cid(3)));
}
#[test]
fn planner_reports_the_skipped_child_and_does_not_clone_it() {
let plan = MonorepoClonePlan::from_resolved(&fixture_tree());
assert_eq!(plan.skipped.len(), 1, "exactly one withheld edge");
let sk = &plan.skipped[0];
assert_eq!(sk.child_spool_id, "acme/child-b");
assert_eq!(sk.mount_name, "secret");
assert_eq!(sk.rel_path, PathBuf::from("secret"));
assert_eq!(sk.reason, EdgeSkip::Unreadable);
assert_eq!(sk.reason_label(), "unreadable");
assert!(
plan.ops.iter().all(|op| op.spool_id != "acme/child-b"),
"skipped child must not be cloned"
);
}
#[test]
fn dest_path_joins_root_for_children_and_returns_root_itself_for_the_root_op() {
let plan = MonorepoClonePlan::from_resolved(&fixture_tree());
let root = Path::new("/tmp/mono");
assert_eq!(plan.ops[0].dest_path(root), PathBuf::from("/tmp/mono"));
assert_eq!(plan.ops[1].dest_path(root), PathBuf::from("/tmp/mono/libs"));
assert_eq!(
plan.ops[2].dest_path(root),
PathBuf::from("/tmp/mono/libs/vendor")
);
}
#[test]
fn node_without_content_head_yields_an_op_with_no_state_but_still_walks_its_edges() {
let child = leaf("acme/child", 5);
let root = MonorepoNode {
spool_id: "acme/root".to_string(),
content_state: None,
edges: vec![descended_edge("sub", "acme/child", 5, child)],
};
let plan = MonorepoClonePlan::from_resolved(&root);
assert_eq!(plan.ops.len(), 2);
assert_eq!(plan.ops[0].spool_id, "acme/root");
assert_eq!(plan.ops[0].content_state, None);
assert_eq!(plan.ops[1].spool_id, "acme/child");
assert_eq!(plan.ops[1].rel_path, PathBuf::from("sub"));
assert_eq!(plan.ops[1].content_state, Some(cid(5)));
}
#[test]
fn every_edge_skip_variant_maps_to_a_stable_label() {
for (reason, label) in [
(EdgeSkip::Unreadable, "unreadable"),
(EdgeSkip::Cycle, "cycle"),
(EdgeSkip::DepthBounded, "depth-bounded"),
] {
let root = MonorepoNode {
spool_id: "root".to_string(),
content_state: Some(cid_bytes(1)),
edges: vec![skipped_edge("m", "child", 2, reason)],
};
let plan = MonorepoClonePlan::from_resolved(&root);
assert_eq!(plan.skipped.len(), 1);
assert_eq!(plan.skipped[0].reason, reason);
assert_eq!(plan.skipped[0].reason_label(), label);
}
}
}