#![allow(clippy::unwrap_used)]
use std::collections::HashMap;
use panproto_gat::Name;
use panproto_schema::{Constraint, Edge, Schema, Vertex};
use panproto_vcs::dag;
use panproto_vcs::merge::{MergeConflict, MergeOptions, Side};
use panproto_vcs::reset::ResetMode;
use panproto_vcs::store::{self, HeadState};
use panproto_vcs::{ObjectId, Repository, Store, VcsError, refs};
fn make_schema(vertices: &[(&str, &str)]) -> Schema {
let mut vert_map = HashMap::new();
for (id, kind) in vertices {
vert_map.insert(
Name::from(*id),
Vertex {
id: Name::from(*id),
kind: Name::from(*kind),
nsid: None,
},
);
}
Schema {
protocol: "test".into(),
vertices: vert_map,
edges: HashMap::new(),
hyper_edges: HashMap::new(),
constraints: HashMap::new(),
required: HashMap::new(),
nsids: HashMap::new(),
entries: Vec::new(),
variants: HashMap::new(),
orderings: HashMap::new(),
recursion_points: HashMap::new(),
spans: HashMap::new(),
usage_modes: HashMap::new(),
nominal: HashMap::new(),
coercions: HashMap::new(),
mergers: HashMap::new(),
defaults: HashMap::new(),
policies: HashMap::new(),
outgoing: HashMap::new(),
incoming: HashMap::new(),
between: HashMap::new(),
}
}
fn make_schema_with_edges(vertices: &[(&str, &str)], edges: &[(&str, &str, &str)]) -> Schema {
let mut s = make_schema(vertices);
for (src, tgt, kind) in edges {
let edge = Edge {
src: (*src).into(),
tgt: (*tgt).into(),
kind: Name::from(*kind),
name: None,
};
s.edges.insert(edge, Name::from(*kind));
}
s
}
fn make_schema_with_named_edges(
vertices: &[(&str, &str)],
edges: &[(&str, &str, &str, &str)],
) -> Schema {
let mut s = make_schema(vertices);
for (src, tgt, kind, name) in edges {
let edge = Edge {
src: (*src).into(),
tgt: (*tgt).into(),
kind: Name::from(*kind),
name: Some(Name::from(*name)),
};
s.edges.insert(edge.clone(), Name::from(*kind));
s.outgoing
.entry(Name::from(*src))
.or_default()
.push(edge.clone());
s.incoming
.entry(Name::from(*tgt))
.or_default()
.push(edge.clone());
s.between
.entry((Name::from(*src), Name::from(*tgt)))
.or_default()
.push(edge);
}
s
}
fn make_schema_with_constraints(
vertices: &[(&str, &str)],
constraints: &[(&str, &str, &str)],
) -> Schema {
let mut s = make_schema(vertices);
for (vid, sort, value) in constraints {
s.constraints
.entry(Name::from(*vid))
.or_default()
.push(Constraint {
sort: Name::from(*sort),
value: value.to_string(),
});
}
s
}
fn init_with_schema(
dir: &std::path::Path,
vertices: &[(&str, &str)],
msg: &str,
author: &str,
) -> Result<(Repository, ObjectId), Box<dyn std::error::Error>> {
let mut repo = Repository::init(dir)?;
let s = make_schema(vertices);
repo.add(&s)?;
let cid = repo.commit(msg, author)?;
Ok((repo, cid))
}
#[test]
fn init_creates_panproto_dir() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let _repo = Repository::init(dir.path())?;
assert!(dir.path().join(".panproto").exists());
assert!(dir.path().join(".panproto/objects").exists());
assert!(dir.path().join(".panproto/refs/heads").exists());
Ok(())
}
#[test]
fn open_nonexistent_fails() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let result = Repository::open(dir.path());
assert!(result.is_err());
Ok(())
}
#[test]
fn double_init_fails() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let _repo = Repository::init(dir.path())?;
let repo2 = Repository::init(dir.path())?;
assert_eq!(repo2.store().get_head()?, HeadState::Branch("main".into()));
assert!(repo2.log(None).is_err());
Ok(())
}
#[test]
fn empty_repo_log_fails() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let repo = Repository::init(dir.path())?;
let result = repo.log(None);
assert!(result.is_err());
Ok(())
}
#[test]
fn commit_without_add_fails() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let mut repo = Repository::init(dir.path())?;
let result = repo.commit("empty", "alice");
assert!(matches!(result, Err(VcsError::NothingStaged)));
Ok(())
}
#[test]
fn add_unchanged_schema_fails() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, _) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
let s = make_schema(&[("a", "object")]);
let result = repo.add(&s);
assert!(result.is_err());
Ok(())
}
#[test]
fn linear_three_commits() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let mut repo = Repository::init(dir.path())?;
let s1 = make_schema(&[("a", "object")]);
repo.add(&s1)?;
repo.commit("first", "alice")?;
let s2 = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&s2)?;
repo.commit("second", "alice")?;
let s3 = make_schema(&[("a", "object"), ("b", "string"), ("c", "integer")]);
repo.add(&s3)?;
repo.commit("third", "alice")?;
let log = repo.log(None)?;
assert_eq!(log.len(), 3);
assert_eq!(log[0].message, "third");
assert_eq!(log[1].message, "second");
assert_eq!(log[2].message, "first");
Ok(())
}
#[test]
fn log_with_limit() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let mut repo = Repository::init(dir.path())?;
let s1 = make_schema(&[("a", "object")]);
repo.add(&s1)?;
repo.commit("first", "alice")?;
let s2 = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&s2)?;
repo.commit("second", "alice")?;
let s3 = make_schema(&[("a", "object"), ("b", "string"), ("c", "integer")]);
repo.add(&s3)?;
repo.commit("third", "alice")?;
let log = repo.log(Some(2))?;
assert_eq!(log.len(), 2);
assert_eq!(log[0].message, "third");
assert_eq!(log[1].message, "second");
Ok(())
}
#[test]
fn head_advances_each_commit() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let mut repo = Repository::init(dir.path())?;
let s1 = make_schema(&[("a", "object")]);
repo.add(&s1)?;
let c1 = repo.commit("first", "alice")?;
assert_eq!(store::resolve_head(repo.store())?, Some(c1));
let s2 = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&s2)?;
let c2 = repo.commit("second", "alice")?;
assert_eq!(store::resolve_head(repo.store())?, Some(c2));
assert_ne!(c1, c2);
Ok(())
}
#[test]
fn commit_preserves_schema() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let mut repo = Repository::init(dir.path())?;
let s = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&s)?;
repo.commit("initial", "alice")?;
let log = repo.log(None)?;
let commit = log[0].clone();
let stored = panproto_vcs::tree::resolve_commit_schema(repo.store(), &commit)?;
assert!(stored.vertices.contains_key("a"));
assert!(stored.vertices.contains_key("b"));
assert_eq!(stored.vertices.len(), 2);
Ok(())
}
#[test]
fn create_branch_and_list() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
refs::create_branch(repo.store_mut(), "feature", c1)?;
let branches = refs::list_branches(repo.store())?;
let names: Vec<&str> = branches.iter().map(|(n, _)| n.as_str()).collect();
assert!(names.contains(&"main"));
assert!(names.contains(&"feature"));
Ok(())
}
#[test]
fn checkout_switches_head() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
refs::create_branch(repo.store_mut(), "dev", c1)?;
refs::checkout_branch(repo.store_mut(), "dev")?;
assert_eq!(repo.store().get_head()?, HeadState::Branch("dev".into()));
Ok(())
}
#[test]
fn delete_branch() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
refs::create_branch(repo.store_mut(), "feature", c1)?;
refs::delete_branch(repo.store_mut(), "feature")?;
let branches = refs::list_branches(repo.store())?;
assert!(!branches.iter().any(|(n, _)| n == "feature"));
Ok(())
}
#[test]
fn checkout_nonexistent_fails() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let _repo = Repository::init(dir.path())?;
let mut repo = Repository::open(dir.path())?;
let result = refs::checkout_branch(repo.store_mut(), "nonexistent");
assert!(result.is_err());
Ok(())
}
#[test]
fn create_duplicate_branch_fails() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
refs::create_branch(repo.store_mut(), "feature", c1)?;
let result = refs::create_branch(repo.store_mut(), "feature", c1);
assert!(matches!(result, Err(VcsError::BranchExists { .. })));
Ok(())
}
#[test]
fn checkout_detached() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
refs::checkout_detached(repo.store_mut(), c1)?;
assert_eq!(repo.store().get_head()?, HeadState::Detached(c1));
Ok(())
}
#[test]
fn force_delete_unmerged_branch() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
refs::create_branch(repo.store_mut(), "feature", c1)?;
refs::checkout_branch(repo.store_mut(), "feature")?;
let s2 = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&s2)?;
repo.commit("feature work", "bob")?;
refs::checkout_branch(repo.store_mut(), "main")?;
let result = refs::delete_branch(repo.store_mut(), "feature");
assert!(matches!(result, Err(VcsError::BranchNotMerged { .. })));
refs::force_delete_branch(repo.store_mut(), "feature")?;
let branches = refs::list_branches(repo.store())?;
assert!(!branches.iter().any(|(n, _)| n == "feature"));
Ok(())
}
#[test]
fn rename_branch() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
refs::create_branch(repo.store_mut(), "old-name", c1)?;
refs::rename_branch(repo.store_mut(), "old-name", "new-name")?;
let branches = refs::list_branches(repo.store())?;
let names: Vec<&str> = branches.iter().map(|(n, _)| n.as_str()).collect();
assert!(!names.contains(&"old-name"));
assert!(names.contains(&"new-name"));
let resolved = refs::resolve_ref(repo.store(), "new-name")?;
assert_eq!(resolved, c1);
Ok(())
}
#[test]
fn create_tag_and_list() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
refs::create_tag(repo.store_mut(), "v1.0", c1)?;
let tags = refs::list_tags(repo.store())?;
assert_eq!(tags.len(), 1);
assert_eq!(tags[0].0, "v1.0");
assert_eq!(tags[0].1, c1);
Ok(())
}
#[test]
fn delete_tag() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
refs::create_tag(repo.store_mut(), "v1.0", c1)?;
refs::delete_tag(repo.store_mut(), "v1.0")?;
let tags = refs::list_tags(repo.store())?;
assert!(tags.is_empty());
Ok(())
}
#[test]
fn resolve_tag() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
refs::create_tag(repo.store_mut(), "v1.0", c1)?;
let resolved = refs::resolve_ref(repo.store(), "v1.0")?;
assert_eq!(resolved, c1);
Ok(())
}
#[test]
fn create_annotated_tag() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
let tag_id = refs::create_annotated_tag(repo.store_mut(), "v2.0", c1, "alice", "release 2.0")?;
let tags = refs::list_tags(repo.store())?;
assert_eq!(tags.len(), 1);
assert_eq!(tags[0].0, "v2.0");
assert_eq!(tags[0].1, tag_id);
let resolved = refs::resolve_ref(repo.store(), "v2.0")?;
assert_eq!(resolved, c1);
let obj = repo.store().get(&tag_id)?;
match obj {
panproto_vcs::Object::Tag(tag) => {
assert_eq!(tag.target, c1);
assert_eq!(tag.tagger, "alice");
assert_eq!(tag.message, "release 2.0");
}
_ => panic!("expected tag object"),
}
Ok(())
}
#[test]
fn force_overwrite_tag() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
refs::create_tag(repo.store_mut(), "v1.0", c1)?;
let s2 = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&s2)?;
let c2 = repo.commit("second", "alice")?;
let result = refs::create_tag(repo.store_mut(), "v1.0", c2);
assert!(matches!(result, Err(VcsError::TagExists { .. })));
refs::create_tag_force(repo.store_mut(), "v1.0", c2)?;
let resolved = refs::resolve_ref(repo.store(), "v1.0")?;
assert_eq!(resolved, c2);
Ok(())
}
#[test]
fn merge_fast_forward() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
refs::create_branch(repo.store_mut(), "feature", c1)?;
refs::checkout_branch(repo.store_mut(), "feature")?;
let s2 = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&s2)?;
repo.commit("add b", "bob")?;
refs::checkout_branch(repo.store_mut(), "main")?;
let result = repo.merge("feature", "alice")?;
assert!(result.conflicts.is_empty());
assert!(result.merged_schema.vertices.contains_key("b"));
Ok(())
}
#[test]
fn merge_fast_forward_multiple_commits() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
refs::create_branch(repo.store_mut(), "feature", c1)?;
refs::checkout_branch(repo.store_mut(), "feature")?;
let s2 = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&s2)?;
repo.commit("add b", "bob")?;
let s3 = make_schema(&[("a", "object"), ("b", "string"), ("c", "integer")]);
repo.add(&s3)?;
repo.commit("add c", "bob")?;
refs::checkout_branch(repo.store_mut(), "main")?;
let result = repo.merge("feature", "alice")?;
assert!(result.conflicts.is_empty());
let log = repo.log(None)?;
assert_eq!(log.len(), 3); assert!(result.merged_schema.vertices.contains_key("b"));
assert!(result.merged_schema.vertices.contains_key("c"));
Ok(())
}
#[test]
fn merge_three_way_clean() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
refs::create_branch(repo.store_mut(), "feature", c1)?;
refs::checkout_branch(repo.store_mut(), "feature")?;
let sf = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&sf)?;
repo.commit("add b", "bob")?;
refs::checkout_branch(repo.store_mut(), "main")?;
let sm = make_schema(&[("a", "object"), ("c", "integer")]);
repo.add(&sm)?;
repo.commit("add c", "alice")?;
let result = repo.merge("feature", "alice")?;
assert!(result.conflicts.is_empty());
assert!(result.merged_schema.vertices.contains_key("a"));
assert!(result.merged_schema.vertices.contains_key("b"));
assert!(result.merged_schema.vertices.contains_key("c"));
Ok(())
}
#[test]
fn merge_identical_additions() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
refs::create_branch(repo.store_mut(), "feature", c1)?;
refs::checkout_branch(repo.store_mut(), "feature")?;
let sf = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&sf)?;
repo.commit("add b on feature", "bob")?;
refs::checkout_branch(repo.store_mut(), "main")?;
let sm = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&sm)?;
repo.commit("add b on main", "alice")?;
let result = repo.merge("feature", "alice")?;
assert!(result.conflicts.is_empty());
assert!(result.merged_schema.vertices.contains_key("b"));
Ok(())
}
#[test]
fn merge_auto_commits() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
refs::create_branch(repo.store_mut(), "feature", c1)?;
refs::checkout_branch(repo.store_mut(), "feature")?;
let sf = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&sf)?;
repo.commit("add b", "bob")?;
refs::checkout_branch(repo.store_mut(), "main")?;
let sm = make_schema(&[("a", "object"), ("c", "integer")]);
repo.add(&sm)?;
repo.commit("add c", "alice")?;
repo.merge("feature", "alice")?;
let log = repo.log(None)?;
assert_eq!(log[0].parents.len(), 2);
Ok(())
}
#[test]
fn merge_no_commit_leaves_staged() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
refs::create_branch(repo.store_mut(), "feature", c1)?;
refs::checkout_branch(repo.store_mut(), "feature")?;
let sf = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&sf)?;
repo.commit("add b", "bob")?;
refs::checkout_branch(repo.store_mut(), "main")?;
let sm = make_schema(&[("a", "object"), ("c", "integer")]);
repo.add(&sm)?;
let main_head = repo.commit("add c", "alice")?;
let opts = MergeOptions {
no_commit: true,
..Default::default()
};
let result = repo.merge_with_options("feature", "alice", &opts)?;
assert!(result.conflicts.is_empty());
let current_head = store::resolve_head(repo.store())?.unwrap();
assert_eq!(current_head, main_head);
Ok(())
}
#[test]
fn merge_ff_only_fails_on_diverge() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
refs::create_branch(repo.store_mut(), "feature", c1)?;
refs::checkout_branch(repo.store_mut(), "feature")?;
let sf = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&sf)?;
repo.commit("add b", "bob")?;
refs::checkout_branch(repo.store_mut(), "main")?;
let sm = make_schema(&[("a", "object"), ("c", "integer")]);
repo.add(&sm)?;
repo.commit("add c", "alice")?;
let opts = MergeOptions {
ff_only: true,
..Default::default()
};
let result = repo.merge_with_options("feature", "alice", &opts);
assert!(matches!(result, Err(VcsError::FastForwardOnly)));
Ok(())
}
#[test]
fn merge_no_ff_creates_commit() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
refs::create_branch(repo.store_mut(), "feature", c1)?;
refs::checkout_branch(repo.store_mut(), "feature")?;
let sf = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&sf)?;
repo.commit("add b", "bob")?;
refs::checkout_branch(repo.store_mut(), "main")?;
let opts = MergeOptions {
no_ff: true,
..Default::default()
};
let result = repo.merge_with_options("feature", "alice", &opts)?;
assert!(result.conflicts.is_empty());
let log = repo.log(None)?;
assert_eq!(log[0].parents.len(), 2);
Ok(())
}
#[test]
fn merge_squash() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
refs::create_branch(repo.store_mut(), "feature", c1)?;
refs::checkout_branch(repo.store_mut(), "feature")?;
let sf = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&sf)?;
repo.commit("add b", "bob")?;
refs::checkout_branch(repo.store_mut(), "main")?;
let sm = make_schema(&[("a", "object"), ("c", "integer")]);
repo.add(&sm)?;
let main_head = repo.commit("add c", "alice")?;
let opts = MergeOptions {
squash: true,
..Default::default()
};
let result = repo.merge_with_options("feature", "alice", &opts)?;
assert!(result.conflicts.is_empty());
let current_head = store::resolve_head(repo.store())?.unwrap();
assert_eq!(current_head, main_head);
Ok(())
}
#[test]
fn conflict_both_modified_vertex() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
refs::create_branch(repo.store_mut(), "feature", c1)?;
refs::checkout_branch(repo.store_mut(), "feature")?;
let sf = make_schema(&[("a", "string")]);
repo.add(&sf)?;
repo.commit("change a to string", "bob")?;
refs::checkout_branch(repo.store_mut(), "main")?;
let sm = make_schema(&[("a", "integer")]);
repo.add(&sm)?;
repo.commit("change a to integer", "alice")?;
let result = repo.merge("feature", "alice")?;
assert!(!result.conflicts.is_empty());
assert!(result.conflicts.iter().any(|c| matches!(c,
MergeConflict::BothModifiedVertex { vertex_id, .. } if vertex_id == "a"
)));
Ok(())
}
#[test]
fn conflict_both_added_vertex_differently() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
refs::create_branch(repo.store_mut(), "feature", c1)?;
refs::checkout_branch(repo.store_mut(), "feature")?;
let sf = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&sf)?;
repo.commit("add b as string", "bob")?;
refs::checkout_branch(repo.store_mut(), "main")?;
let sm = make_schema(&[("a", "object"), ("b", "integer")]);
repo.add(&sm)?;
repo.commit("add b as integer", "alice")?;
let result = repo.merge("feature", "alice")?;
assert!(!result.conflicts.is_empty());
assert!(result.conflicts.iter().any(|c| matches!(c,
MergeConflict::BothAddedVertexDifferently { vertex_id, .. } if vertex_id == "b"
)));
Ok(())
}
#[test]
fn conflict_delete_modify_vertex_ours() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(
dir.path(),
&[("a", "object"), ("b", "string")],
"init",
"alice",
)?;
refs::create_branch(repo.store_mut(), "feature", c1)?;
refs::checkout_branch(repo.store_mut(), "feature")?;
let sf = make_schema(&[("a", "object"), ("b", "integer")]);
repo.add(&sf)?;
repo.commit("change b to integer", "bob")?;
refs::checkout_branch(repo.store_mut(), "main")?;
let sm = make_schema(&[("a", "object")]);
repo.add(&sm)?;
repo.commit("delete b", "alice")?;
let result = repo.merge("feature", "alice")?;
assert!(!result.conflicts.is_empty());
assert!(result.conflicts.iter().any(|c| matches!(c,
MergeConflict::DeleteModifyVertex { vertex_id, deleted_by: Side::Ours } if vertex_id == "b"
)));
Ok(())
}
#[test]
fn conflict_delete_modify_vertex_theirs() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(
dir.path(),
&[("a", "object"), ("b", "string")],
"init",
"alice",
)?;
refs::create_branch(repo.store_mut(), "feature", c1)?;
refs::checkout_branch(repo.store_mut(), "feature")?;
let sf = make_schema(&[("a", "object")]);
repo.add(&sf)?;
repo.commit("delete b", "bob")?;
refs::checkout_branch(repo.store_mut(), "main")?;
let sm = make_schema(&[("a", "object"), ("b", "integer")]);
repo.add(&sm)?;
repo.commit("change b to integer", "alice")?;
let result = repo.merge("feature", "alice")?;
assert!(!result.conflicts.is_empty());
assert!(result.conflicts.iter().any(|c| matches!(c,
MergeConflict::DeleteModifyVertex { vertex_id, deleted_by: Side::Theirs } if vertex_id == "b"
)));
Ok(())
}
#[test]
fn edge_removal_one_side_is_clean() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let mut repo = Repository::init(dir.path())?;
let s_base = make_schema_with_edges(&[("a", "object"), ("b", "string")], &[("a", "b", "prop")]);
repo.add(&s_base)?;
let c1 = repo.commit("init", "alice")?;
refs::create_branch(repo.store_mut(), "feature", c1)?;
refs::checkout_branch(repo.store_mut(), "feature")?;
let sf = make_schema(&[("a", "object"), ("b", "string"), ("c", "integer")]);
repo.add(&sf)?;
repo.commit("remove edge, add c", "bob")?;
refs::checkout_branch(repo.store_mut(), "main")?;
let sm = make_schema_with_edges(
&[("a", "object"), ("b", "string"), ("d", "boolean")],
&[("a", "b", "prop")],
);
repo.add(&sm)?;
repo.commit("add d, keep edge", "alice")?;
let result = repo.merge("feature", "alice")?;
assert!(result.conflicts.is_empty());
assert!(result.merged_schema.vertices.contains_key("c"));
assert!(result.merged_schema.vertices.contains_key("d"));
assert!(result.merged_schema.edges.is_empty());
Ok(())
}
#[test]
fn conflict_both_modified_constraint() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let mut repo = Repository::init(dir.path())?;
let s_base = make_schema_with_constraints(&[("a", "string")], &[("a", "maxLength", "100")]);
repo.add(&s_base)?;
let c1 = repo.commit("init", "alice")?;
refs::create_branch(repo.store_mut(), "feature", c1)?;
refs::checkout_branch(repo.store_mut(), "feature")?;
let sf = make_schema_with_constraints(&[("a", "string")], &[("a", "maxLength", "200")]);
repo.add(&sf)?;
repo.commit("change maxLength to 200", "bob")?;
refs::checkout_branch(repo.store_mut(), "main")?;
let sm = make_schema_with_constraints(&[("a", "string")], &[("a", "maxLength", "300")]);
repo.add(&sm)?;
repo.commit("change maxLength to 300", "alice")?;
let result = repo.merge("feature", "alice")?;
assert!(!result.conflicts.is_empty());
assert!(result.conflicts.iter().any(|c| matches!(c,
MergeConflict::BothModifiedConstraint { vertex_id, sort, .. }
if vertex_id == "a" && sort == "maxLength"
)));
Ok(())
}
#[test]
fn conflict_both_added_constraint_differently() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "string")], "init", "alice")?;
refs::create_branch(repo.store_mut(), "feature", c1)?;
refs::checkout_branch(repo.store_mut(), "feature")?;
let sf = make_schema_with_constraints(&[("a", "string")], &[("a", "format", "email")]);
repo.add(&sf)?;
repo.commit("add format email", "bob")?;
refs::checkout_branch(repo.store_mut(), "main")?;
let sm = make_schema_with_constraints(&[("a", "string")], &[("a", "format", "uri")]);
repo.add(&sm)?;
repo.commit("add format uri", "alice")?;
let result = repo.merge("feature", "alice")?;
assert!(!result.conflicts.is_empty());
assert!(result.conflicts.iter().any(|c| matches!(c,
MergeConflict::BothAddedConstraintDifferently { vertex_id, sort, .. }
if vertex_id == "a" && sort == "format"
)));
Ok(())
}
#[test]
fn conflict_delete_modify_constraint() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let mut repo = Repository::init(dir.path())?;
let s_base = make_schema_with_constraints(&[("a", "string")], &[("a", "maxLength", "100")]);
repo.add(&s_base)?;
let c1 = repo.commit("init", "alice")?;
refs::create_branch(repo.store_mut(), "feature", c1)?;
refs::checkout_branch(repo.store_mut(), "feature")?;
let sf = make_schema_with_constraints(&[("a", "string")], &[("a", "maxLength", "200")]);
repo.add(&sf)?;
repo.commit("change maxLength to 200", "bob")?;
refs::checkout_branch(repo.store_mut(), "main")?;
let sm = make_schema(&[("a", "string")]);
repo.add(&sm)?;
repo.commit("remove constraint", "alice")?;
let result = repo.merge("feature", "alice")?;
assert!(!result.conflicts.is_empty());
assert!(result.conflicts.iter().any(|c| matches!(c,
MergeConflict::DeleteModifyConstraint { vertex_id, sort, .. }
if vertex_id == "a" && sort == "maxLength"
)));
Ok(())
}
#[test]
fn conflict_both_modified_nsid() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let mut repo = Repository::init(dir.path())?;
let mut s_base = make_schema(&[("a", "object")]);
s_base
.nsids
.insert(Name::from("a"), Name::from("com.example.base"));
repo.add(&s_base)?;
let c1 = repo.commit("init", "alice")?;
refs::create_branch(repo.store_mut(), "feature", c1)?;
refs::checkout_branch(repo.store_mut(), "feature")?;
let mut sf = make_schema(&[("a", "object")]);
sf.nsids
.insert(Name::from("a"), Name::from("com.example.feature"));
repo.add(&sf)?;
repo.commit("change nsid to feature", "bob")?;
refs::checkout_branch(repo.store_mut(), "main")?;
let mut sm = make_schema(&[("a", "object")]);
sm.nsids
.insert(Name::from("a"), Name::from("com.example.main"));
repo.add(&sm)?;
repo.commit("change nsid to main", "alice")?;
let result = repo.merge("feature", "alice")?;
assert!(!result.conflicts.is_empty());
assert!(result.conflicts.iter().any(|c| matches!(c,
MergeConflict::BothModifiedNsid { vertex_id, .. } if vertex_id == "a"
)));
Ok(())
}
#[test]
fn conflict_both_modified_ordering() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let mut repo = Repository::init(dir.path())?;
let mut s_base =
make_schema_with_edges(&[("a", "object"), ("b", "string")], &[("a", "b", "prop")]);
let edge = Edge {
src: "a".into(),
tgt: "b".into(),
kind: "prop".into(),
name: None,
};
s_base.orderings.insert(edge.clone(), 0);
repo.add(&s_base)?;
let c1 = repo.commit("init", "alice")?;
refs::create_branch(repo.store_mut(), "feature", c1)?;
refs::checkout_branch(repo.store_mut(), "feature")?;
let mut sf = make_schema_with_edges(&[("a", "object"), ("b", "string")], &[("a", "b", "prop")]);
sf.orderings.insert(edge.clone(), 5);
repo.add(&sf)?;
repo.commit("set ordering to 5", "bob")?;
refs::checkout_branch(repo.store_mut(), "main")?;
let mut sm = make_schema_with_edges(&[("a", "object"), ("b", "string")], &[("a", "b", "prop")]);
sm.orderings.insert(edge, 10);
repo.add(&sm)?;
repo.commit("set ordering to 10", "alice")?;
let result = repo.merge("feature", "alice")?;
assert!(!result.conflicts.is_empty());
assert!(
result
.conflicts
.iter()
.any(|c| matches!(c, MergeConflict::BothModifiedOrdering { .. }))
);
Ok(())
}
#[test]
fn conflict_multiple_simultaneous() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(
dir.path(),
&[("a", "object"), ("b", "string")],
"init",
"alice",
)?;
refs::create_branch(repo.store_mut(), "feature", c1)?;
refs::checkout_branch(repo.store_mut(), "feature")?;
let sf = make_schema(&[("a", "integer"), ("b", "integer")]);
repo.add(&sf)?;
repo.commit("change both to integer", "bob")?;
refs::checkout_branch(repo.store_mut(), "main")?;
let sm = make_schema(&[("a", "array"), ("b", "array")]);
repo.add(&sm)?;
repo.commit("change both to array", "alice")?;
let result = repo.merge("feature", "alice")?;
assert!(result.conflicts.len() >= 2);
Ok(())
}
#[test]
fn cherry_pick_applies_change() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
refs::create_branch(repo.store_mut(), "feature", c1)?;
refs::checkout_branch(repo.store_mut(), "feature")?;
let sf = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&sf)?;
let feature_commit = repo.commit("add b", "bob")?;
refs::checkout_branch(repo.store_mut(), "main")?;
let new_id = repo.cherry_pick(feature_commit, "alice")?;
let obj = repo.store().get(&new_id)?;
match obj {
panproto_vcs::Object::Commit(c) => {
let s = panproto_vcs::tree::resolve_commit_schema(repo.store(), &c)?;
assert!(s.vertices.contains_key("b"));
assert!(s.vertices.contains_key("a"));
}
_ => panic!("expected commit"),
}
Ok(())
}
#[test]
fn cherry_pick_conflict_fails() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
refs::create_branch(repo.store_mut(), "feature", c1)?;
refs::checkout_branch(repo.store_mut(), "feature")?;
let sf = make_schema(&[("a", "string")]);
repo.add(&sf)?;
let feature_commit = repo.commit("change a to string", "bob")?;
refs::checkout_branch(repo.store_mut(), "main")?;
let sm = make_schema(&[("a", "integer")]);
repo.add(&sm)?;
repo.commit("change a to integer", "alice")?;
let result = repo.cherry_pick(feature_commit, "alice");
assert!(matches!(result, Err(VcsError::MergeConflicts { .. })));
Ok(())
}
#[test]
fn cherry_pick_preserves_branch() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
refs::create_branch(repo.store_mut(), "feature", c1)?;
refs::checkout_branch(repo.store_mut(), "feature")?;
let sf = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&sf)?;
let feature_commit = repo.commit("add b", "bob")?;
refs::checkout_branch(repo.store_mut(), "main")?;
let _main_head_before = store::resolve_head(repo.store())?.unwrap();
repo.cherry_pick(feature_commit, "alice")?;
assert_eq!(repo.store().get_head()?, HeadState::Branch("main".into()));
Ok(())
}
#[test]
fn cherry_pick_no_commit() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
refs::create_branch(repo.store_mut(), "feature", c1)?;
refs::checkout_branch(repo.store_mut(), "feature")?;
let sf = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&sf)?;
let feature_commit = repo.commit("add b", "bob")?;
refs::checkout_branch(repo.store_mut(), "main")?;
let main_head = store::resolve_head(repo.store())?.unwrap();
let opts = panproto_vcs::cherry_pick::CherryPickOptions {
no_commit: true,
record_origin: false,
};
let _schema_id = panproto_vcs::cherry_pick::cherry_pick_with_options(
repo.store_mut(),
feature_commit,
"alice",
&opts,
)?;
let current_head = store::resolve_head(repo.store())?.unwrap();
assert_eq!(current_head, main_head);
Ok(())
}
#[test]
fn rebase_diverged_branch() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
let sm = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&sm)?;
let main_tip = repo.commit("add b on main", "alice")?;
refs::create_branch(repo.store_mut(), "feature", c1)?;
refs::checkout_branch(repo.store_mut(), "feature")?;
let sf = make_schema(&[("a", "object"), ("c", "integer")]);
repo.add(&sf)?;
repo.commit("add c on feature", "bob")?;
let new_tip = repo.rebase(main_tip, "bob")?;
let obj = repo.store().get(&new_tip)?;
match obj {
panproto_vcs::Object::Commit(c) => {
let s = panproto_vcs::tree::resolve_commit_schema(repo.store(), &c)?;
assert!(s.vertices.contains_key("a"));
assert!(s.vertices.contains_key("b"));
assert!(s.vertices.contains_key("c"));
}
_ => panic!("expected commit"),
}
Ok(())
}
#[test]
fn rebase_multiple_commits() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
let sm = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&sm)?;
let main_tip = repo.commit("add b", "alice")?;
refs::create_branch(repo.store_mut(), "feature", c1)?;
refs::checkout_branch(repo.store_mut(), "feature")?;
let s_c = make_schema(&[("a", "object"), ("c", "integer")]);
repo.add(&s_c)?;
repo.commit("add c", "bob")?;
let s_d = make_schema(&[("a", "object"), ("c", "integer"), ("d", "boolean")]);
repo.add(&s_d)?;
repo.commit("add d", "bob")?;
let new_tip = repo.rebase(main_tip, "bob")?;
let obj = repo.store().get(&new_tip)?;
match obj {
panproto_vcs::Object::Commit(c) => {
let s = panproto_vcs::tree::resolve_commit_schema(repo.store(), &c)?;
assert!(s.vertices.contains_key("a"));
assert!(s.vertices.contains_key("b"));
assert!(s.vertices.contains_key("c"));
assert!(s.vertices.contains_key("d"));
}
_ => panic!("expected commit"),
}
Ok(())
}
#[test]
fn rebase_conflict_fails() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
let sm = make_schema(&[("a", "string")]);
repo.add(&sm)?;
let main_tip = repo.commit("change a to string", "alice")?;
refs::create_branch(repo.store_mut(), "feature", c1)?;
refs::checkout_branch(repo.store_mut(), "feature")?;
let sf = make_schema(&[("a", "integer")]);
repo.add(&sf)?;
repo.commit("change a to integer", "bob")?;
let result = repo.rebase(main_tip, "bob");
assert!(matches!(result, Err(VcsError::MergeConflicts { .. })));
Ok(())
}
#[test]
fn amend_changes_message() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, _c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
let new_id = repo.amend("amended message", "alice")?;
let log = repo.log(None)?;
assert_eq!(log.len(), 1);
assert_eq!(log[0].message, "amended message");
let head = store::resolve_head(repo.store())?.unwrap();
assert_eq!(head, new_id);
Ok(())
}
#[test]
fn amend_changes_schema() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, _c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
let s2 = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&s2)?;
repo.amend("amended with b", "alice")?;
let log = repo.log(None)?;
assert_eq!(log.len(), 1);
assert_eq!(log[0].message, "amended with b");
let s = panproto_vcs::tree::resolve_commit_schema(repo.store(), &log[0])?;
assert!(s.vertices.contains_key("b"));
Ok(())
}
#[test]
fn amend_no_commits_fails() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let mut repo = Repository::init(dir.path())?;
let result = repo.amend("nothing here", "alice");
assert!(matches!(result, Err(VcsError::NothingToAmend)));
Ok(())
}
#[test]
fn reset_soft() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
let s2 = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&s2)?;
let _c2 = repo.commit("second", "alice")?;
let outcome = repo.reset(c1, ResetMode::Soft, "alice")?;
assert!(!outcome.should_clear_index);
assert!(!outcome.should_write_working);
assert_eq!(outcome.new_head, c1);
let head = store::resolve_head(repo.store())?.unwrap();
assert_eq!(head, c1);
Ok(())
}
#[test]
fn reset_mixed() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
let s2 = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&s2)?;
let _c2 = repo.commit("second", "alice")?;
let outcome = repo.reset(c1, ResetMode::Mixed, "alice")?;
assert!(outcome.should_clear_index);
assert!(!outcome.should_write_working);
assert_eq!(outcome.new_head, c1);
Ok(())
}
#[test]
fn reset_hard() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
let s2 = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&s2)?;
let _c2 = repo.commit("second", "alice")?;
let outcome = repo.reset(c1, ResetMode::Hard, "alice")?;
assert!(outcome.should_clear_index);
assert!(outcome.should_write_working);
assert_eq!(outcome.new_head, c1);
Ok(())
}
#[test]
fn reset_records_reflog() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
let s2 = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&s2)?;
let _c2 = repo.commit("second", "alice")?;
repo.reset(c1, ResetMode::Soft, "alice")?;
let reflog = repo.store().read_reflog("HEAD", None)?;
assert!(reflog.iter().any(|e| e.message.contains("reset")));
Ok(())
}
#[test]
fn stash_push_pop() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, _c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
let s2 = make_schema(&[("a", "object"), ("b", "string")]);
let s2_id = panproto_vcs::tree::store_schema_as_tree(repo.store_mut(), s2)?;
panproto_vcs::stash::stash_push(repo.store_mut(), s2_id, "alice", Some("wip"))?;
let popped = panproto_vcs::stash::stash_pop(repo.store_mut())?;
assert_eq!(popped, s2_id);
Ok(())
}
#[test]
fn stash_multiple_lifo() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, _c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
let s1 = make_schema(&[("a", "object"), ("b", "string")]);
let s1_id = panproto_vcs::tree::store_schema_as_tree(repo.store_mut(), s1)?;
let s2 = make_schema(&[("a", "object"), ("c", "integer")]);
let s2_id = panproto_vcs::tree::store_schema_as_tree(repo.store_mut(), s2)?;
panproto_vcs::stash::stash_push(repo.store_mut(), s1_id, "alice", Some("first"))?;
panproto_vcs::stash::stash_push(repo.store_mut(), s2_id, "alice", Some("second"))?;
let popped = panproto_vcs::stash::stash_pop(repo.store_mut())?;
assert_eq!(popped, s2_id);
Ok(())
}
#[test]
fn stash_pop_empty_fails() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, _c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
let result = panproto_vcs::stash::stash_pop(repo.store_mut());
assert!(result.is_err());
Ok(())
}
#[test]
fn stash_list() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, _c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
let s1 = make_schema(&[("a", "object"), ("b", "string")]);
let s1_id = panproto_vcs::tree::store_schema_as_tree(repo.store_mut(), s1)?;
let s2 = make_schema(&[("a", "object"), ("c", "integer")]);
let s2_id = panproto_vcs::tree::store_schema_as_tree(repo.store_mut(), s2)?;
panproto_vcs::stash::stash_push(repo.store_mut(), s1_id, "alice", Some("first"))?;
panproto_vcs::stash::stash_push(repo.store_mut(), s2_id, "alice", Some("second"))?;
let entries = panproto_vcs::stash::stash_list(repo.store())?;
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].message, "second");
assert_eq!(entries[1].message, "first");
Ok(())
}
#[test]
fn stash_apply_preserves_entry() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, _c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
let s1 = make_schema(&[("a", "object"), ("b", "string")]);
let s1_id = panproto_vcs::tree::store_schema_as_tree(repo.store_mut(), s1)?;
panproto_vcs::stash::stash_push(repo.store_mut(), s1_id, "alice", Some("stashed"))?;
let applied = panproto_vcs::stash::stash_apply(repo.store(), 0)?;
assert_eq!(applied, s1_id);
let entries = panproto_vcs::stash::stash_list(repo.store())?;
assert_eq!(entries.len(), 1);
Ok(())
}
#[test]
fn stash_clear() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, _c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
let s1 = make_schema(&[("a", "object"), ("b", "string")]);
let s1_id = panproto_vcs::tree::store_schema_as_tree(repo.store_mut(), s1)?;
panproto_vcs::stash::stash_push(repo.store_mut(), s1_id, "alice", Some("stash1"))?;
panproto_vcs::stash::stash_push(repo.store_mut(), s1_id, "alice", Some("stash2"))?;
panproto_vcs::stash::stash_clear(repo.store_mut())?;
let result = panproto_vcs::stash::stash_pop(repo.store_mut());
assert!(result.is_err());
Ok(())
}
#[test]
fn blame_vertex_finds_introducer() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, _c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
let s2 = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&s2)?;
let c2 = repo.commit("add b", "bob")?;
let entry = panproto_vcs::blame::blame_vertex(repo.store(), c2, "b")?;
assert_eq!(entry.commit_id, c2);
assert_eq!(entry.author, "bob");
Ok(())
}
#[test]
fn blame_vertex_root() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
let entry = panproto_vcs::blame::blame_vertex(repo.store(), c1, "a")?;
assert_eq!(entry.commit_id, c1);
assert_eq!(entry.author, "alice");
Ok(())
}
#[test]
fn blame_nonexistent_fails() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
let result = panproto_vcs::blame::blame_vertex(repo.store(), c1, "nonexistent");
assert!(result.is_err());
Ok(())
}
#[test]
fn bisect_finds_breaking_commit() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let mut repo = Repository::init(dir.path())?;
let mut ids = Vec::new();
let s0 = make_schema(&[("a", "object")]);
repo.add(&s0)?;
ids.push(repo.commit("commit 0", "alice")?);
let s1 = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&s1)?;
ids.push(repo.commit("commit 1", "alice")?);
let s2 = make_schema(&[("a", "object"), ("b", "string"), ("c", "integer")]);
repo.add(&s2)?;
ids.push(repo.commit("commit 2", "alice")?);
let s3 = make_schema(&[
("a", "object"),
("b", "string"),
("c", "integer"),
("d", "broken"),
]);
repo.add(&s3)?;
ids.push(repo.commit("commit 3 (breaking)", "alice")?);
let s4 = make_schema(&[
("a", "object"),
("b", "string"),
("c", "integer"),
("d", "broken"),
("e", "extra"),
]);
repo.add(&s4)?;
ids.push(repo.commit("commit 4", "alice")?);
let s5 = make_schema(&[
("a", "object"),
("b", "string"),
("c", "integer"),
("d", "broken"),
("e", "extra"),
("f", "another"),
]);
repo.add(&s5)?;
ids.push(repo.commit("commit 5", "alice")?);
let breaking_index = 3;
let (mut state, step) = panproto_vcs::bisect::bisect_start(repo.store(), ids[0], ids[5])?;
let mut current_step = step;
let mut steps = 0;
loop {
match current_step {
panproto_vcs::bisect::BisectStep::Found(id) => {
assert_eq!(id, ids[breaking_index]);
break;
}
panproto_vcs::bisect::BisectStep::Test(id) => {
let idx = ids.iter().position(|i| *i == id).unwrap();
let is_good = idx < breaking_index;
current_step = panproto_vcs::bisect::bisect_step(&mut state, is_good);
steps += 1;
assert!(steps <= 10, "bisect should converge");
}
}
}
Ok(())
}
#[test]
fn bisect_adjacent_found_immediately() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let mut repo = Repository::init(dir.path())?;
let s1 = make_schema(&[("a", "object")]);
repo.add(&s1)?;
let c1 = repo.commit("good", "alice")?;
let s2 = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&s2)?;
let c2 = repo.commit("bad", "alice")?;
let (_state, step) = panproto_vcs::bisect::bisect_start(repo.store(), c1, c2)?;
assert!(matches!(step, panproto_vcs::bisect::BisectStep::Found(id) if id == c2));
Ok(())
}
#[test]
fn gc_after_reset() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
let s2 = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&s2)?;
let c2 = repo.commit("second", "alice")?;
repo.reset(c1, ResetMode::Hard, "alice")?;
assert!(repo.store().has(&c2));
let report = repo.gc()?;
assert!(!report.deleted.is_empty());
assert!(!repo.store().has(&c2));
Ok(())
}
#[test]
fn gc_preserves_tagged() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
let s2 = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&s2)?;
let c2 = repo.commit("second", "alice")?;
refs::create_tag(repo.store_mut(), "v1.0", c2)?;
repo.reset(c1, ResetMode::Hard, "alice")?;
let report = repo.gc()?;
assert!(repo.store().has(&c2));
assert!(!report.deleted.contains(&c2));
Ok(())
}
#[test]
fn gc_preserves_branches() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
refs::create_branch(repo.store_mut(), "feature", c1)?;
refs::checkout_branch(repo.store_mut(), "feature")?;
let s2 = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&s2)?;
let feature_commit = repo.commit("feature work", "bob")?;
refs::checkout_branch(repo.store_mut(), "main")?;
let report = repo.gc()?;
assert!(repo.store().has(&feature_commit));
assert!(!report.deleted.contains(&feature_commit));
Ok(())
}
#[test]
fn reflog_records_commits() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, _c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
let s2 = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&s2)?;
repo.commit("second", "alice")?;
let reflog = repo.store().read_reflog("HEAD", None)?;
assert!(!reflog.is_empty());
assert!(reflog.iter().any(|e| e.message.contains("commit")));
Ok(())
}
#[test]
fn reflog_records_merge() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
refs::create_branch(repo.store_mut(), "feature", c1)?;
refs::checkout_branch(repo.store_mut(), "feature")?;
let sf = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&sf)?;
repo.commit("add b", "bob")?;
refs::checkout_branch(repo.store_mut(), "main")?;
repo.merge("feature", "alice")?;
let reflog = repo.store().read_reflog("HEAD", None)?;
assert!(reflog.iter().any(|e| e.message.contains("merge")));
Ok(())
}
#[test]
fn feature_branch_full_workflow() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
refs::create_and_checkout_branch(repo.store_mut(), "feature", c1)?;
assert_eq!(
repo.store().get_head()?,
HeadState::Branch("feature".into())
);
let s2 = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&s2)?;
repo.commit("add b", "bob")?;
let s3 = make_schema(&[("a", "object"), ("b", "string"), ("c", "integer")]);
repo.add(&s3)?;
repo.commit("add c", "bob")?;
refs::checkout_branch(repo.store_mut(), "main")?;
let result = repo.merge("feature", "alice")?;
assert!(result.conflicts.is_empty());
let log = repo.log(None)?;
let s = panproto_vcs::tree::resolve_commit_schema(repo.store(), &log[0])?;
assert!(s.vertices.contains_key("a"));
assert!(s.vertices.contains_key("b"));
assert!(s.vertices.contains_key("c"));
let head = store::resolve_head(repo.store())?.unwrap();
refs::create_tag(repo.store_mut(), "v1.0", head)?;
refs::delete_branch(repo.store_mut(), "feature")?;
let branches = refs::list_branches(repo.store())?;
let names: Vec<&str> = branches.iter().map(|(n, _)| n.as_str()).collect();
assert!(names.contains(&"main"));
assert!(!names.contains(&"feature"));
let tags = refs::list_tags(repo.store())?;
assert_eq!(tags.len(), 1);
assert_eq!(tags[0].0, "v1.0");
Ok(())
}
#[test]
fn stash_across_branch_switch() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
refs::create_branch(repo.store_mut(), "feature", c1)?;
let s_wip = make_schema(&[("a", "object"), ("wip", "string")]);
let wip_id = panproto_vcs::tree::store_schema_as_tree(repo.store_mut(), s_wip)?;
panproto_vcs::stash::stash_push(repo.store_mut(), wip_id, "alice", Some("wip on main"))?;
refs::checkout_branch(repo.store_mut(), "feature")?;
let sf = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&sf)?;
repo.commit("feature work", "bob")?;
refs::checkout_branch(repo.store_mut(), "main")?;
let popped = panproto_vcs::stash::stash_pop(repo.store_mut())?;
assert_eq!(popped, wip_id);
let result = panproto_vcs::stash::stash_pop(repo.store_mut());
assert!(result.is_err());
Ok(())
}
#[test]
fn rebase_then_fast_forward_merge() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
let sm = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&sm)?;
let main_tip = repo.commit("add b on main", "alice")?;
refs::create_branch(repo.store_mut(), "feature", c1)?;
refs::checkout_branch(repo.store_mut(), "feature")?;
let sf = make_schema(&[("a", "object"), ("c", "integer")]);
repo.add(&sf)?;
repo.commit("add c on feature", "bob")?;
let _rebased_tip = repo.rebase(main_tip, "bob")?;
refs::checkout_branch(repo.store_mut(), "main")?;
let result = repo.merge("feature", "alice")?;
assert!(result.conflicts.is_empty());
let log = repo.log(None)?;
let s = panproto_vcs::tree::resolve_commit_schema(repo.store(), &log[0])?;
assert!(s.vertices.contains_key("a"));
assert!(s.vertices.contains_key("b"));
assert!(s.vertices.contains_key("c"));
Ok(())
}
#[test]
fn reset_then_recommit_then_gc() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
let s2 = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&s2)?;
let c2 = repo.commit("add b", "alice")?;
let s3 = make_schema(&[("a", "object"), ("b", "string"), ("c", "integer")]);
repo.add(&s3)?;
let c3 = repo.commit("add c", "alice")?;
repo.reset(c1, ResetMode::Hard, "alice")?;
let s_new = make_schema(&[("a", "object"), ("d", "boolean")]);
repo.add(&s_new)?;
let c_new = repo.commit("add d instead", "alice")?;
let report = repo.gc()?;
assert!(!report.deleted.is_empty());
assert!(!repo.store().has(&c2));
assert!(!repo.store().has(&c3));
assert!(repo.store().has(&c_new));
let log = repo.log(None)?;
assert_eq!(log.len(), 2);
let s = panproto_vcs::tree::resolve_commit_schema(repo.store(), &log[0])?;
assert!(s.vertices.contains_key("a"));
assert!(s.vertices.contains_key("d"));
assert!(!s.vertices.contains_key("b"));
assert!(!s.vertices.contains_key("c"));
Ok(())
}
#[test]
fn blame_edge_finds_introducer() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, _c1) = init_with_schema(
dir.path(),
&[("a", "object"), ("b", "string")],
"init",
"alice",
)?;
let s2 = make_schema_with_edges(&[("a", "object"), ("b", "string")], &[("a", "b", "prop")]);
repo.add(&s2)?;
let c2 = repo.commit("add edge", "alice")?;
let edge = Edge {
src: "a".into(),
tgt: "b".into(),
kind: "prop".into(),
name: None,
};
let entry = panproto_vcs::blame::blame_edge(repo.store(), c2, &edge)?;
assert_eq!(entry.commit_id, c2);
Ok(())
}
#[test]
fn blame_constraint_finds_introducer() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, _c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
let s2 = make_schema_with_constraints(&[("a", "object")], &[("a", "maxLength", "100")]);
repo.add(&s2)?;
let c2 = repo.commit("add constraint", "alice")?;
let entry = panproto_vcs::blame::blame_constraint(repo.store(), c2, "a", "maxLength")?;
assert_eq!(entry.commit_id, c2);
Ok(())
}
#[test]
fn dag_is_ancestor() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c0) = init_with_schema(dir.path(), &[("a", "object")], "c0", "alice")?;
let s1 = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&s1)?;
let c1 = repo.commit("c1", "alice")?;
let s2 = make_schema(&[("a", "object"), ("b", "string"), ("c", "integer")]);
repo.add(&s2)?;
let c2 = repo.commit("c2", "alice")?;
assert!(panproto_vcs::dag::is_ancestor(repo.store(), c0, c2)?);
assert!(!panproto_vcs::dag::is_ancestor(repo.store(), c2, c0)?);
assert!(panproto_vcs::dag::is_ancestor(repo.store(), c0, c0)?);
assert!(panproto_vcs::dag::is_ancestor(repo.store(), c1, c2)?);
Ok(())
}
#[test]
fn dag_merge_base_diamond() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c0) = init_with_schema(dir.path(), &[("a", "object")], "base", "alice")?;
let sm = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&sm)?;
let c1 = repo.commit("main work", "alice")?;
refs::create_branch(repo.store_mut(), "feature", c0)?;
refs::checkout_branch(repo.store_mut(), "feature")?;
let sf = make_schema(&[("a", "object"), ("c", "integer")]);
repo.add(&sf)?;
let c2 = repo.commit("feature work", "bob")?;
let base = panproto_vcs::dag::merge_base(repo.store(), c1, c2)?;
assert_eq!(base, Some(c0));
Ok(())
}
#[test]
fn dag_commit_count_linear() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c0) = init_with_schema(dir.path(), &[("a", "object")], "c0", "alice")?;
let s1 = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&s1)?;
let _c1 = repo.commit("c1", "alice")?;
let s2 = make_schema(&[("a", "object"), ("b", "string"), ("c", "integer")]);
repo.add(&s2)?;
let _c2 = repo.commit("c2", "alice")?;
let s3 = make_schema(&[
("a", "object"),
("b", "string"),
("c", "integer"),
("d", "boolean"),
]);
repo.add(&s3)?;
let c3 = repo.commit("c3", "alice")?;
let count = panproto_vcs::dag::commit_count(repo.store(), c0, c3)?;
assert_eq!(count, 3);
Ok(())
}
#[test]
fn stash_drop_removes_entry() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, _c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
let s1 = make_schema(&[("a", "object"), ("b", "string")]);
let s1_id = panproto_vcs::tree::store_schema_as_tree(repo.store_mut(), s1)?;
let s2 = make_schema(&[("a", "object"), ("c", "integer")]);
let s2_id = panproto_vcs::tree::store_schema_as_tree(repo.store_mut(), s2)?;
panproto_vcs::stash::stash_push(repo.store_mut(), s1_id, "alice", Some("first"))?;
panproto_vcs::stash::stash_push(repo.store_mut(), s2_id, "alice", Some("second"))?;
panproto_vcs::stash::stash_drop(repo.store_mut(), 0)?;
let remaining = panproto_vcs::stash::stash_pop(repo.store_mut())?;
assert_eq!(remaining, s1_id);
panproto_vcs::stash::stash_push(repo.store_mut(), s2_id, "alice", Some("re-push"))?;
let result = panproto_vcs::stash::stash_drop(repo.store_mut(), 1);
assert!(result.is_err());
Ok(())
}
#[test]
fn create_and_checkout_branch_switches_head() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
refs::create_and_checkout_branch(repo.store_mut(), "feature", c1)?;
let head = repo.store().get_head()?;
assert_eq!(head, HeadState::Branch("feature".into()));
let branches = refs::list_branches(repo.store())?;
assert!(branches.iter().any(|(name, _)| name == "feature"));
Ok(())
}
#[test]
fn annotated_tag_peels_on_resolve() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
let tag_obj_id =
refs::create_annotated_tag(repo.store_mut(), "v3.0", c1, "alice", "annotated release")?;
let tags = refs::list_tags(repo.store())?;
let tag_entry = tags.iter().find(|(n, _)| n == "v3.0").unwrap();
assert_eq!(tag_entry.1, tag_obj_id);
assert_ne!(tag_obj_id, c1);
let resolved = refs::resolve_ref(repo.store(), "v3.0")?;
assert_eq!(resolved, c1);
Ok(())
}
#[test]
fn merge_with_custom_message() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
refs::create_branch(repo.store_mut(), "feature", c1)?;
refs::checkout_branch(repo.store_mut(), "feature")?;
let sf = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&sf)?;
repo.commit("add b", "bob")?;
refs::checkout_branch(repo.store_mut(), "main")?;
let sm = make_schema(&[("a", "object"), ("c", "integer")]);
repo.add(&sm)?;
repo.commit("add c", "alice")?;
let opts = MergeOptions {
message: Some("custom msg".into()),
..Default::default()
};
repo.merge_with_options("feature", "alice", &opts)?;
let log = repo.log(None)?;
assert_eq!(log[0].message, "custom msg");
assert_eq!(log[0].parents.len(), 2);
Ok(())
}
#[test]
fn gc_dry_run_reports_but_preserves() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
let s2 = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&s2)?;
let c2 = repo.commit("second", "alice")?;
repo.reset(c1, ResetMode::Hard, "alice")?;
assert!(repo.store().has(&c2));
let options = panproto_vcs::gc::GcOptions { dry_run: true };
let report = panproto_vcs::gc::gc_with_options(repo.store_mut(), &options)?;
assert!(!report.deleted.is_empty());
assert!(repo.store().has(&c2));
Ok(())
}
#[test]
fn reset_to_specific_commit_then_recommit() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c0) = init_with_schema(dir.path(), &[("a", "object")], "c0", "alice")?;
let s1 = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&s1)?;
let c1 = repo.commit("c1", "alice")?;
let s2 = make_schema(&[("a", "object"), ("b", "string"), ("c", "integer")]);
repo.add(&s2)?;
let _c2 = repo.commit("c2", "alice")?;
repo.reset(c1, ResetMode::Mixed, "alice")?;
let head = store::resolve_head(repo.store())?.unwrap();
assert_eq!(head, c1);
let s_new = make_schema(&[("a", "object"), ("b", "string"), ("d", "boolean")]);
repo.add(&s_new)?;
let c_new = repo.commit("add d", "alice")?;
let log = repo.log(None)?;
assert_eq!(log[0].message, "add d");
assert_eq!(log[0].parents, vec![c1]);
assert!(panproto_vcs::dag::is_ancestor(repo.store(), c0, c_new)?);
Ok(())
}
#[test]
fn cherry_pick_record_origin() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let (mut repo, c1) = init_with_schema(dir.path(), &[("a", "object")], "init", "alice")?;
refs::create_branch(repo.store_mut(), "feature", c1)?;
refs::checkout_branch(repo.store_mut(), "feature")?;
let sf = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&sf)?;
let feature_commit = repo.commit("add b on feature", "bob")?;
refs::checkout_branch(repo.store_mut(), "main")?;
let options = panproto_vcs::cherry_pick::CherryPickOptions {
record_origin: true,
no_commit: false,
};
let new_id = panproto_vcs::cherry_pick::cherry_pick_with_options(
repo.store_mut(),
feature_commit,
"alice",
&options,
)?;
let obj = repo.store().get(&new_id)?;
match obj {
panproto_vcs::Object::Commit(c) => {
assert!(
c.message.contains("(cherry picked from commit"),
"expected origin annotation, got: {}",
c.message
);
}
_ => panic!("expected commit"),
}
Ok(())
}
#[test]
fn dag_compose_path_two_steps() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let mut repo = Repository::init(dir.path())?;
let s0 = make_schema_with_named_edges(
&[("root", "object"), ("root.name", "string")],
&[("root", "root.name", "prop", "name")],
);
repo.add(&s0)?;
let c0 = repo.commit("v0: name only", "alice")?;
let s1 = make_schema_with_named_edges(
&[
("root", "object"),
("root.name", "string"),
("root.email", "string"),
],
&[
("root", "root.name", "prop", "name"),
("root", "root.email", "prop", "email"),
],
);
repo.add(&s1)?;
let c1 = repo.commit("v1: add email", "alice")?;
let s2 = make_schema_with_named_edges(
&[
("root", "object"),
("root.name", "string"),
("root.email", "string"),
("root.role", "string"),
],
&[
("root", "root.name", "prop", "name"),
("root", "root.email", "prop", "email"),
("root", "root.role", "prop", "role"),
],
);
repo.add(&s2)?;
let c2 = repo.commit("v2: add role", "alice")?;
let path = vec![c0, c1, c2];
let composed = dag::compose_path(repo.store(), &path)?;
assert_eq!(
composed.vertex_map.get("root"),
Some(&Name::from("root")),
"root vertex should survive composition"
);
assert_eq!(
composed.vertex_map.get("root.name"),
Some(&Name::from("root.name")),
"root.name vertex should survive composition"
);
assert!(
!composed.vertex_map.contains_key("root.email"),
"root.email was not in c0, should not be in composed domain"
);
assert!(
!composed.vertex_map.contains_key("root.role"),
"root.role was not in c0, should not be in composed domain"
);
Ok(())
}
#[test]
fn dag_compose_path_three_steps() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let mut repo = Repository::init(dir.path())?;
let s0 = make_schema_with_named_edges(
&[("root", "object"), ("root.name", "string")],
&[("root", "root.name", "prop", "name")],
);
repo.add(&s0)?;
let c0 = repo.commit("v0", "alice")?;
let s1 = make_schema_with_named_edges(
&[
("root", "object"),
("root.name", "string"),
("root.email", "string"),
],
&[
("root", "root.name", "prop", "name"),
("root", "root.email", "prop", "email"),
],
);
repo.add(&s1)?;
let c1 = repo.commit("v1: add email", "alice")?;
let s2 = make_schema_with_named_edges(
&[
("root", "object"),
("root.name", "string"),
("root.email", "string"),
("root.role", "string"),
],
&[
("root", "root.name", "prop", "name"),
("root", "root.email", "prop", "email"),
("root", "root.role", "prop", "role"),
],
);
repo.add(&s2)?;
let c2 = repo.commit("v2: add role", "alice")?;
let s3 = make_schema_with_named_edges(
&[
("root", "object"),
("root.name", "string"),
("root.role", "string"),
],
&[
("root", "root.name", "prop", "name"),
("root", "root.role", "prop", "role"),
],
);
repo.add(&s3)?;
let c3 = repo.commit("v3: drop email", "alice")?;
let path = vec![c0, c1, c2, c3];
let composed = dag::compose_path(repo.store(), &path)?;
assert_eq!(composed.vertex_map.get("root"), Some(&Name::from("root")));
assert_eq!(
composed.vertex_map.get("root.name"),
Some(&Name::from("root.name"))
);
assert!(!composed.vertex_map.contains_key("root.email"));
assert!(!composed.vertex_map.contains_key("root.role"));
Ok(())
}
#[test]
fn dag_find_path_then_compose() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let mut repo = Repository::init(dir.path())?;
let s0 = make_schema_with_named_edges(
&[("root", "object"), ("root.x", "string")],
&[("root", "root.x", "prop", "x")],
);
repo.add(&s0)?;
let c0 = repo.commit("v0", "alice")?;
let s1 = make_schema_with_named_edges(
&[
("root", "object"),
("root.x", "string"),
("root.y", "string"),
],
&[
("root", "root.x", "prop", "x"),
("root", "root.y", "prop", "y"),
],
);
repo.add(&s1)?;
let _c1 = repo.commit("v1: add y", "alice")?;
let s2 = make_schema_with_named_edges(
&[
("root", "object"),
("root.x", "string"),
("root.y", "string"),
("root.z", "integer"),
],
&[
("root", "root.x", "prop", "x"),
("root", "root.y", "prop", "y"),
("root", "root.z", "prop", "z"),
],
);
repo.add(&s2)?;
let c2 = repo.commit("v2: add z", "alice")?;
let path = dag::find_path(repo.store(), c0, c2)?;
assert_eq!(path.len(), 3, "path should have 3 commits");
assert_eq!(path[0], c0);
assert_eq!(path[2], c2);
let composed = dag::compose_path(repo.store(), &path)?;
assert_eq!(composed.vertex_map.get("root"), Some(&Name::from("root")));
assert_eq!(
composed.vertex_map.get("root.x"),
Some(&Name::from("root.x"))
);
assert!(!composed.vertex_map.contains_key("root.y"));
assert!(!composed.vertex_map.contains_key("root.z"));
let src_edge = Edge {
src: "root".into(),
tgt: "root.x".into(),
kind: "prop".into(),
name: Some("x".into()),
};
assert!(
composed.edge_map.contains_key(&src_edge),
"prop edge root->root.x should survive in composed migration"
);
Ok(())
}
#[test]
fn dag_compose_path_single_step() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let mut repo = Repository::init(dir.path())?;
let s0 = make_schema_with_named_edges(
&[("root", "object"), ("root.a", "string")],
&[("root", "root.a", "prop", "a")],
);
repo.add(&s0)?;
let c0 = repo.commit("v0", "alice")?;
let s1 = make_schema_with_named_edges(
&[
("root", "object"),
("root.a", "string"),
("root.b", "string"),
],
&[
("root", "root.a", "prop", "a"),
("root", "root.b", "prop", "b"),
],
);
repo.add(&s1)?;
let c1 = repo.commit("v1: add b", "alice")?;
let path = vec![c0, c1];
let composed = dag::compose_path(repo.store(), &path)?;
assert_eq!(composed.vertex_map.get("root"), Some(&Name::from("root")));
assert_eq!(
composed.vertex_map.get("root.a"),
Some(&Name::from("root.a"))
);
assert!(
!composed.vertex_map.contains_key("root.b"),
"root.b is new in c1, should not be in migration domain"
);
Ok(())
}