use std::path::{Path, PathBuf};
use panproto_check::diff;
use panproto_mig::hom_search::{SearchOptions, find_best_morphism, morphism_to_migration};
use panproto_schema::Schema;
use crate::auto_mig;
use crate::cherry_pick::{self, advance_head};
use crate::dag;
use crate::error::VcsError;
use crate::fs_store::FsStore;
use crate::gat_validate;
use crate::gc;
use crate::hash::ObjectId;
use crate::index::{Index, StagedData, StagedSchema, ValidationStatus};
use crate::merge;
use crate::object::{CommitObject, DataSetObject, Object};
use crate::refs;
use crate::store::{self, HeadState, Store};
#[derive(Clone, Debug, Default)]
pub struct CommitOptions {
pub skip_verify: bool,
}
#[allow(dead_code)]
pub struct Repository {
store: FsStore,
working_dir: PathBuf,
}
impl Repository {
pub fn init(path: &Path) -> Result<Self, VcsError> {
let store = FsStore::init(path)?;
Ok(Self {
store,
working_dir: path.to_owned(),
})
}
pub fn open(path: &Path) -> Result<Self, VcsError> {
let store = FsStore::open(path)?;
Ok(Self {
store,
working_dir: path.to_owned(),
})
}
pub fn add(&mut self, schema: &Schema) -> Result<Index, VcsError> {
let schema_id = crate::tree::store_schema_as_tree(&mut self.store, schema.clone())?;
let (migration_id, auto_derived, validation, gat_diagnostics) =
match store::resolve_head(&self.store)? {
None => {
(None, false, ValidationStatus::Valid, None)
}
Some(head_id) => {
let head_commit = self.load_commit(head_id)?;
let head_schema = self.load_schema(head_commit.schema_id)?;
let schema_diff = diff::diff(&head_schema, schema);
if schema_diff.is_empty() {
return Err(VcsError::ValidationFailed {
reasons: vec!["no changes detected".to_owned()],
});
}
let mut migration =
auto_mig::derive_migration(&head_schema, schema, &schema_diff);
let old_vertex_count = head_schema.vertex_count();
if old_vertex_count > 0 && migration.vertex_map.len() * 2 < old_vertex_count {
let opts = SearchOptions::default();
if let Some(best) = find_best_morphism(&head_schema, schema, &opts) {
if best.vertex_map.len() > migration.vertex_map.len() {
let mut hom_mig = morphism_to_migration(&best);
hom_mig.hyper_edge_map = migration.hyper_edge_map;
hom_mig.label_map = migration.label_map;
migration = hom_mig;
}
}
}
let gat_diag =
gat_validate::validate_migration(&head_schema, schema, &migration);
let mig_src_id = self.store.put(&Object::FlatSchema(Box::new(head_schema)))?;
let mig_tgt_id = self
.store
.put(&Object::FlatSchema(Box::new(schema.clone())))?;
let migration_id = self.store.put(&Object::Migration {
src: mig_src_id,
tgt: mig_tgt_id,
mapping: migration,
})?;
let validation = if gat_diag.has_errors() {
ValidationStatus::Invalid(gat_diag.all_errors())
} else {
ValidationStatus::Valid
};
(Some(migration_id), true, validation, Some(gat_diag))
}
};
let mut index = self.read_index()?;
index.staged = Some(StagedSchema {
schema_id,
migration_id,
auto_derived,
validation,
gat_diagnostics,
});
self.write_index(&index)?;
Ok(index)
}
pub fn commit(&mut self, message: &str, author: &str) -> Result<ObjectId, VcsError> {
self.commit_with_options(message, author, &CommitOptions::default())
}
pub fn commit_with_options(
&mut self,
message: &str,
author: &str,
options: &CommitOptions,
) -> Result<ObjectId, VcsError> {
let index = self.read_index()?;
let staged = index.staged.ok_or(VcsError::NothingStaged)?;
if !options.skip_verify {
if let ValidationStatus::Invalid(reasons) = &staged.validation {
return Err(VcsError::ValidationFailed {
reasons: reasons.clone(),
});
}
if let Some(ref diag) = staged.gat_diagnostics {
if diag.has_errors() {
return Err(VcsError::ValidationFailed {
reasons: diag.all_errors(),
});
}
}
}
let head_id = store::resolve_head(&self.store)?;
let schema = self.load_schema(staged.schema_id)?;
let theory = crate::gat_validate::schema_to_theory(&schema.protocol, &schema);
let theory_id = self.store.put(&Object::Theory(Box::new(theory)))?;
let mut theory_ids = std::collections::BTreeMap::new();
theory_ids.insert(schema.protocol.clone(), theory_id);
let parents: Vec<ObjectId> = head_id.into_iter().collect();
let data_ids: Vec<ObjectId> = index.staged_data.iter().map(|sd| sd.data_id).collect();
let mut builder = CommitObject::builder(staged.schema_id, schema.protocol, author, message)
.theory_ids(theory_ids);
if !parents.is_empty() {
builder = builder.parents(parents);
}
if let Some(mid) = staged.migration_id {
builder = builder.migration_id(mid);
}
if let Some(pid) = index.staged_protocol {
builder = builder.protocol_id(pid);
}
if !data_ids.is_empty() {
builder = builder.data_ids(data_ids);
}
let commit = builder.build();
let commit_id = self.store.put(&Object::Commit(commit))?;
if let Some(old) = head_id {
advance_head(
&mut self.store,
old,
commit_id,
author,
&format!("commit: {message}"),
)?;
} else {
match self.store.get_head()? {
HeadState::Branch(name) => {
let ref_name = format!("refs/heads/{name}");
self.store.set_ref(&ref_name, commit_id)?;
}
HeadState::Detached(_) => {
self.store.set_head(HeadState::Detached(commit_id))?;
}
}
}
self.write_index(&Index::default())?;
Ok(commit_id)
}
pub fn merge(&mut self, branch: &str, author: &str) -> Result<merge::MergeResult, VcsError> {
self.merge_with_options(branch, author, &merge::MergeOptions::default())
}
pub fn merge_with_options(
&mut self,
branch: &str,
author: &str,
options: &merge::MergeOptions,
) -> Result<merge::MergeResult, VcsError> {
let ours_id = store::resolve_head(&self.store)?.ok_or_else(|| VcsError::RefNotFound {
name: "HEAD".to_owned(),
})?;
let theirs_id = refs::resolve_ref(&self.store, branch)?;
if dag::is_ancestor(&self.store, ours_id, theirs_id)? {
if options.no_ff {
} else {
advance_head(
&mut self.store,
ours_id,
theirs_id,
author,
&format!("merge {branch}: fast-forward"),
)?;
let theirs_commit = self.load_commit(theirs_id)?;
let theirs_schema = self.load_schema(theirs_commit.schema_id)?;
return Ok(merge::MergeResult {
merged_schema: theirs_schema,
conflicts: Vec::new(),
migration_from_ours: panproto_mig::Migration::empty(),
migration_from_theirs: panproto_mig::Migration::empty(),
pullback_overlap: None,
});
}
} else if options.ff_only {
return Err(VcsError::FastForwardOnly);
}
let base_id =
dag::merge_base(&self.store, ours_id, theirs_id)?.ok_or(VcsError::NoCommonAncestor)?;
let base_commit = self.load_commit(base_id)?;
let ours_commit = self.load_commit(ours_id)?;
let theirs_commit = self.load_commit(theirs_id)?;
let base_schema = self.load_schema(base_commit.schema_id)?;
let ours_schema = self.load_schema(ours_commit.schema_id)?;
let theirs_schema = self.load_schema(theirs_commit.schema_id)?;
let result = merge::three_way_merge(&base_schema, &ours_schema, &theirs_schema);
if result.conflicts.is_empty() && !options.no_commit && !options.squash {
let merged_schema_id =
crate::tree::store_schema_as_tree(&mut self.store, result.merged_schema.clone())?;
let mig_src = self.store.put(&Object::FlatSchema(Box::new(ours_schema)))?;
let mig_tgt = self
.store
.put(&Object::FlatSchema(Box::new(result.merged_schema.clone())))?;
let migration_id = self.store.put(&Object::Migration {
src: mig_src,
tgt: mig_tgt,
mapping: result.migration_from_ours.clone(),
})?;
let msg = options
.message
.clone()
.unwrap_or_else(|| format!("merge branch '{branch}'"));
let mut data_ids = ours_commit.data_ids;
for id in &theirs_commit.data_ids {
if !data_ids.contains(id) {
data_ids.push(*id);
}
}
let mut complement_ids = ours_commit.complement_ids;
for id in &theirs_commit.complement_ids {
if !complement_ids.contains(id) {
complement_ids.push(*id);
}
}
let merged_schema = self.load_schema(merged_schema_id)?;
let merged_theory =
crate::gat_validate::schema_to_theory(&merged_schema.protocol, &merged_schema);
let merged_theory_id = self.store.put(&Object::Theory(Box::new(merged_theory)))?;
let merged_protocol = merged_schema.protocol;
let mut merge_theory_ids = std::collections::BTreeMap::new();
merge_theory_ids.insert(merged_protocol.clone(), merged_theory_id);
let mut merge_builder =
CommitObject::builder(merged_schema_id, merged_protocol, author, msg)
.parents(vec![ours_id, theirs_id])
.migration_id(migration_id)
.theory_ids(merge_theory_ids);
if !data_ids.is_empty() {
merge_builder = merge_builder.data_ids(data_ids);
}
if !complement_ids.is_empty() {
merge_builder = merge_builder.complement_ids(complement_ids);
}
let merge_commit = merge_builder.build();
let merge_id = self.store.put(&Object::Commit(merge_commit))?;
advance_head(
&mut self.store,
ours_id,
merge_id,
author,
&format!("merge {branch}"),
)?;
}
Ok(result)
}
pub fn amend(&mut self, message: &str, author: &str) -> Result<ObjectId, VcsError> {
let head_id = store::resolve_head(&self.store)?.ok_or(VcsError::NothingToAmend)?;
let old_commit = self.load_commit(head_id)?;
let index = self.read_index()?;
let (schema_id, migration_id) = if let Some(staged) = index.staged {
(staged.schema_id, staged.migration_id)
} else {
(old_commit.schema_id, old_commit.migration_id)
};
let mut builder = CommitObject::builder(schema_id, old_commit.protocol, author, message);
if !old_commit.parents.is_empty() {
builder = builder.parents(old_commit.parents);
}
if let Some(mid) = migration_id {
builder = builder.migration_id(mid);
}
if let Some(pid) = old_commit.protocol_id {
builder = builder.protocol_id(pid);
}
if !old_commit.data_ids.is_empty() {
builder = builder.data_ids(old_commit.data_ids);
}
if !old_commit.complement_ids.is_empty() {
builder = builder.complement_ids(old_commit.complement_ids);
}
if !old_commit.edit_log_ids.is_empty() {
builder = builder.edit_log_ids(old_commit.edit_log_ids);
}
let new_commit = builder.build();
let new_id = self.store.put(&Object::Commit(new_commit))?;
advance_head(
&mut self.store,
head_id,
new_id,
author,
&format!("commit (amend): {message}"),
)?;
self.write_index(&Index::default())?;
Ok(new_id)
}
pub fn log(&self, limit: Option<usize>) -> Result<Vec<CommitObject>, VcsError> {
let head_id = store::resolve_head(&self.store)?.ok_or_else(|| VcsError::RefNotFound {
name: "HEAD".to_owned(),
})?;
dag::log_walk(&self.store, head_id, limit)
}
pub fn cherry_pick(&mut self, commit_id: ObjectId, author: &str) -> Result<ObjectId, VcsError> {
cherry_pick::cherry_pick(&mut self.store, commit_id, author)
}
pub fn rebase(&mut self, onto: ObjectId, author: &str) -> Result<ObjectId, VcsError> {
crate::rebase::rebase(&mut self.store, onto, author)
}
pub fn reset(
&mut self,
target: ObjectId,
mode: crate::reset::ResetMode,
author: &str,
) -> Result<crate::reset::ResetOutcome, VcsError> {
let outcome = crate::reset::reset(&mut self.store, target, mode, author)?;
if outcome.should_clear_index {
self.write_index(&Index::default())?;
}
Ok(outcome)
}
pub fn gc(&mut self) -> Result<gc::GcReport, VcsError> {
gc::gc(&mut self.store)
}
#[must_use]
pub const fn store(&self) -> &FsStore {
&self.store
}
pub const fn store_mut(&mut self) -> &mut FsStore {
&mut self.store
}
pub fn add_data(&mut self, path: &Path) -> Result<Index, VcsError> {
let data_bytes = std::fs::read(path)?;
let index = self.read_index()?;
let schema_id = if let Some(ref staged) = index.staged {
staged.schema_id
} else {
let head_id = store::resolve_head(&self.store)?.ok_or(VcsError::NothingStaged)?;
let commit = self.load_commit(head_id)?;
commit.schema_id
};
let record_count = count_records(&data_bytes);
let dataset = DataSetObject {
schema_id,
data: data_bytes,
record_count,
};
let data_id = self.store.put(&Object::DataSet(dataset))?;
let mut updated_index = index;
updated_index.staged_data.push(StagedData {
source_path: path.to_owned(),
data_id,
schema_id,
});
self.write_index(&updated_index)?;
Ok(updated_index)
}
pub fn add_protocol(
&mut self,
protocol: &panproto_schema::Protocol,
) -> Result<Index, VcsError> {
let protocol_id = self
.store
.put(&Object::Protocol(Box::new(protocol.clone())))?;
let mut index = self.read_index()?;
index.staged_protocol = Some(protocol_id);
self.write_index(&index)?;
Ok(index)
}
pub fn checkout_with_data(&mut self, target: &str, data_dir: &Path) -> Result<(), VcsError> {
let current_id = store::resolve_head(&self.store)?.ok_or(VcsError::NothingStaged)?;
let current_commit = self.load_commit(current_id)?;
let current_schema = self.load_schema(current_commit.schema_id)?;
let target_id = refs::resolve_ref(&self.store, target)?;
let target_commit = self.load_commit(target_id)?;
let target_schema = self.load_schema(target_commit.schema_id)?;
refs::checkout_branch(&mut self.store, target)?;
if current_commit.schema_id != target_commit.schema_id && data_dir.is_dir() {
let protocol = crate::data_mig::protocol_for_schema(¤t_schema);
crate::data_mig::migrate_data_directory(
&mut self.store,
data_dir,
¤t_schema,
&target_schema,
&protocol,
)?;
}
Ok(())
}
pub fn merge_with_data(
&mut self,
branch: &str,
author: &str,
data_dir: &Path,
opts: &merge::MergeOptions,
) -> Result<merge::MergeResult, VcsError> {
let pre_merge_id =
store::resolve_head(&self.store)?.ok_or_else(|| VcsError::RefNotFound {
name: "HEAD".to_owned(),
})?;
let pre_merge_commit = self.load_commit(pre_merge_id)?;
let pre_merge_schema = self.load_schema(pre_merge_commit.schema_id)?;
let result = self.merge_with_options(branch, author, opts)?;
if data_dir.is_dir() {
let head_id = store::resolve_head(&self.store)?.ok_or(VcsError::NothingStaged)?;
let head_commit = self.load_commit(head_id)?;
if pre_merge_commit.schema_id != head_commit.schema_id {
let merged_schema = self.load_schema(head_commit.schema_id)?;
let protocol = crate::data_mig::protocol_for_schema(&pre_merge_schema);
crate::data_mig::migrate_data_directory(
&mut self.store,
data_dir,
&pre_merge_schema,
&merged_schema,
&protocol,
)?;
}
}
Ok(result)
}
fn load_commit(&self, id: ObjectId) -> Result<CommitObject, VcsError> {
match self.store.get(&id)? {
Object::Commit(c) => Ok(c),
other => Err(VcsError::WrongObjectType {
expected: "commit",
found: other.type_name(),
}),
}
}
fn load_schema(&self, id: ObjectId) -> Result<Schema, VcsError> {
let proto = crate::tree::project_coproduct_protocol();
crate::tree::assemble_schema(&self.store, &id, &proto)
}
fn index_path(&self) -> PathBuf {
self.store.root().join("index.json")
}
fn read_index(&self) -> Result<Index, VcsError> {
let path = self.index_path();
if !path.exists() {
return Ok(Index::default());
}
let json = std::fs::read_to_string(&path)?;
serde_json::from_str(&json)
.map_err(|e| VcsError::Serialization(crate::error::SerializationError(e.to_string())))
}
fn write_index(&self, index: &Index) -> Result<(), VcsError> {
let json = serde_json::to_string_pretty(index).map_err(|e| {
VcsError::Serialization(crate::error::SerializationError(e.to_string()))
})?;
std::fs::write(self.index_path(), json)?;
Ok(())
}
}
fn count_records(data: &[u8]) -> u64 {
serde_json::from_slice::<serde_json::Value>(data).map_or(1, |value| match &value {
serde_json::Value::Array(arr) => arr.len() as u64,
_ => 1,
})
}
#[cfg(test)]
mod tests {
use super::*;
use panproto_gat::Name;
use panproto_schema::Vertex;
use std::collections::HashMap;
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(),
}
}
#[test]
fn init_add_commit() -> 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)?;
let commit_id = repo.commit("initial commit", "alice")?;
let log = repo.log(None)?;
assert_eq!(log.len(), 1);
assert_eq!(log[0].message, "initial commit");
assert_eq!(log[0].author, "alice");
let head = store::resolve_head(repo.store())?;
assert_eq!(head, Some(commit_id));
Ok(())
}
#[test]
fn add_commit_second_schema() -> 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 log = repo.log(None)?;
assert_eq!(log.len(), 2);
assert_eq!(log[0].message, "second");
assert_eq!(log[1].message, "first");
Ok(())
}
#[test]
fn merge_fast_forward() -> 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("initial", "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());
let log = repo.log(None)?;
let head_schema = repo.load_schema(log[0].schema_id)?;
assert!(head_schema.vertices.contains_key("b"));
Ok(())
}
#[test]
fn nothing_staged_errors() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let mut repo = Repository::init(dir.path())?;
assert!(matches!(
repo.commit("empty", "alice"),
Err(VcsError::NothingStaged)
));
Ok(())
}
#[test]
fn commit_blocked_by_gat_errors() -> Result<(), Box<dyn std::error::Error>> {
use crate::gat_validate::GatDiagnostics;
use crate::index::{Index, StagedSchema, ValidationStatus};
let dir = tempfile::tempdir()?;
let mut repo = Repository::init(dir.path())?;
let s = make_schema(&[("a", "object")]);
repo.add(&s)?;
repo.commit("initial", "alice")?;
let staged_schema = make_schema(&[("a", "object"), ("b", "string")]);
let schema_id = crate::tree::store_schema_as_tree(&mut repo.store, staged_schema)?;
let diag = GatDiagnostics {
type_errors: vec!["sort mismatch: expected Ob, got Hom".to_owned()],
equation_errors: vec![],
migration_warnings: vec![],
};
let index = Index {
staged: Some(StagedSchema {
schema_id,
migration_id: None,
auto_derived: false,
validation: ValidationStatus::Invalid(diag.all_errors()),
gat_diagnostics: Some(diag),
}),
staged_data: vec![],
staged_protocol: None,
};
repo.write_index(&index)?;
let Err(err) = repo.commit("should fail", "alice") else {
panic!("commit should fail when validation status is invalid");
};
assert!(
matches!(&err, VcsError::ValidationFailed { reasons } if !reasons.is_empty()),
"expected ValidationFailed, got: {err:?}"
);
let opts = CommitOptions { skip_verify: true };
let commit_id = repo.commit_with_options("forced commit", "alice", &opts)?;
let log = repo.log(None)?;
assert_eq!(log[0].message, "forced commit");
assert_eq!(store::resolve_head(repo.store())?, Some(commit_id));
Ok(())
}
#[test]
fn commit_blocked_by_gat_diagnostics_only() -> Result<(), Box<dyn std::error::Error>> {
use crate::gat_validate::GatDiagnostics;
use crate::index::{Index, StagedSchema, ValidationStatus};
let dir = tempfile::tempdir()?;
let mut repo = Repository::init(dir.path())?;
let s = make_schema(&[("a", "object")]);
repo.add(&s)?;
repo.commit("initial", "alice")?;
let staged_schema = make_schema(&[("a", "object"), ("c", "number")]);
let schema_id = crate::tree::store_schema_as_tree(&mut repo.store, staged_schema)?;
let diag = GatDiagnostics {
type_errors: vec![],
equation_errors: vec!["equation 'assoc' violated when f=id: LHS=a, RHS=b".to_owned()],
migration_warnings: vec![],
};
let index = Index {
staged: Some(StagedSchema {
schema_id,
migration_id: None,
auto_derived: false,
validation: ValidationStatus::Valid,
gat_diagnostics: Some(diag),
}),
staged_data: vec![],
staged_protocol: None,
};
repo.write_index(&index)?;
let Err(err) = repo.commit("should fail", "alice") else {
panic!("commit should fail when GAT diagnostics has equation errors");
};
assert!(
matches!(&err, VcsError::ValidationFailed { reasons } if reasons.iter().any(|r| r.contains("equation violation"))),
"expected ValidationFailed with equation violation, got: {err:?}"
);
let opts = CommitOptions { skip_verify: true };
let id = repo.commit_with_options("bypassed", "alice", &opts)?;
assert_eq!(store::resolve_head(repo.store())?, Some(id));
Ok(())
}
#[test]
fn add_data_and_commit() -> 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 schema", "alice")?;
let data_path = dir.path().join("data.json");
std::fs::write(&data_path, r#"[{"a": 1}, {"a": 2}, {"a": 3}]"#)?;
let index = repo.add_data(&data_path)?;
assert_eq!(index.staged_data.len(), 1);
assert_eq!(index.staged_data[0].source_path, data_path);
let s2 = make_schema(&[("a", "object"), ("b", "string"), ("c", "number")]);
repo.add(&s2)?;
let commit_id = repo.commit("add data", "alice")?;
let log = repo.log(None)?;
assert_eq!(log[0].message, "add data");
assert_eq!(log[0].data_ids.len(), 1);
let data_obj = repo.store().get(&log[0].data_ids[0])?;
match data_obj {
Object::DataSet(ds) => {
assert_eq!(ds.record_count, 3);
}
_ => panic!("expected DataSet object"),
}
assert_eq!(store::resolve_head(repo.store())?, Some(commit_id));
Ok(())
}
#[test]
fn add_protocol_and_commit() -> Result<(), Box<dyn std::error::Error>> {
let dir = tempfile::tempdir()?;
let mut repo = Repository::init(dir.path())?;
let s = make_schema(&[("a", "object")]);
repo.add(&s)?;
repo.commit("initial", "alice")?;
let protocol = panproto_schema::Protocol {
name: "test-protocol".into(),
schema_theory: "ThGraph".into(),
instance_theory: "ThInst".into(),
..Default::default()
};
let index = repo.add_protocol(&protocol)?;
assert!(index.staged_protocol.is_some());
let s2 = make_schema(&[("a", "object"), ("b", "string")]);
repo.add(&s2)?;
let commit_id = repo.commit("add protocol", "alice")?;
let log = repo.log(None)?;
assert_eq!(log[0].message, "add protocol");
assert!(log[0].protocol_id.is_some());
let protocol_id = log[0].protocol_id.ok_or("missing protocol_id")?;
let proto_obj = repo.store().get(&protocol_id)?;
match proto_obj {
Object::Protocol(p) => {
assert_eq!(p.name, "test-protocol");
}
_ => panic!("expected Protocol object"),
}
assert_eq!(store::resolve_head(repo.store())?, Some(commit_id));
Ok(())
}
#[test]
fn count_records_json_array() {
assert_eq!(count_records(b"[1, 2, 3]"), 3);
}
#[test]
fn count_records_json_object() {
assert_eq!(count_records(b"{\"a\": 1}"), 1);
}
#[test]
fn count_records_non_json() {
assert_eq!(count_records(b"not json"), 1);
}
}