use panproto_schema::Edge;
use crate::error::VcsError;
use crate::hash::ObjectId;
use crate::object::Object;
use crate::store::Store;
#[derive(Clone, Debug)]
pub struct BlameEntry {
pub commit_id: ObjectId,
pub author: String,
pub timestamp: u64,
pub message: String,
}
pub fn blame_vertex(
store: &dyn Store,
head: ObjectId,
vertex_id: &str,
) -> Result<BlameEntry, VcsError> {
walk_blame(store, head, |schema| {
schema.vertices.contains_key(vertex_id)
})
}
pub fn blame_edge(store: &dyn Store, head: ObjectId, edge: &Edge) -> Result<BlameEntry, VcsError> {
walk_blame(store, head, |schema| schema.edges.contains_key(edge))
}
pub fn blame_constraint(
store: &dyn Store,
head: ObjectId,
vertex_id: &str,
sort: &str,
) -> Result<BlameEntry, VcsError> {
walk_blame(store, head, |schema| {
schema
.constraints
.get(vertex_id)
.is_some_and(|constraints| constraints.iter().any(|c| c.sort == sort))
})
}
fn walk_blame(
store: &dyn Store,
head: ObjectId,
predicate: impl Fn(&panproto_schema::Schema) -> bool,
) -> Result<BlameEntry, VcsError> {
let mut current_id = head;
let mut last_present: Option<BlameEntry> = None;
loop {
let commit = match store.get(¤t_id)? {
Object::Commit(c) => c,
other => {
return Err(VcsError::WrongObjectType {
expected: "commit",
found: other.type_name(),
});
}
};
let schema = {
let proto = crate::tree::project_coproduct_protocol();
crate::tree::assemble_schema_dyn(store, &commit.schema_id, &proto)?
};
if predicate(&schema) {
last_present = Some(BlameEntry {
commit_id: current_id,
author: commit.author.clone(),
timestamp: commit.timestamp,
message: commit.message.clone(),
});
} else {
if let Some(entry) = last_present {
return Ok(entry);
}
return Err(VcsError::RefNotFound {
name: format!("element not found in commit {}", current_id.short()),
});
}
if let Some(&parent) = commit.parents.first() {
current_id = parent;
} else {
if let Some(entry) = last_present {
return Ok(entry);
}
return Err(VcsError::RefNotFound {
name: format!("element not found in commit {}", current_id.short()),
});
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::MemStore;
use crate::object::CommitObject;
use panproto_gat::Name;
use panproto_schema::{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 blame_vertex_in_root() -> Result<(), Box<dyn std::error::Error>> {
let mut store = MemStore::new();
let s = make_schema(&[("a", "object")]);
let schema_id = crate::tree::store_schema_as_tree(&mut store, s)?;
let commit = CommitObject::builder(schema_id, "test", "alice", "initial")
.timestamp(100)
.build();
let commit_id = store.put(&Object::Commit(commit))?;
let entry = blame_vertex(&store, commit_id, "a")?;
assert_eq!(entry.commit_id, commit_id);
assert_eq!(entry.author, "alice");
Ok(())
}
#[test]
fn blame_vertex_introduced_later() -> Result<(), Box<dyn std::error::Error>> {
let mut store = MemStore::new();
let s0 = make_schema(&[("a", "object")]);
let s0_id = crate::tree::store_schema_as_tree(&mut store, s0)?;
let c0 = CommitObject::builder(s0_id, "test", "alice", "initial")
.timestamp(100)
.build();
let c0_id = store.put(&Object::Commit(c0))?;
let s1 = make_schema(&[("a", "object"), ("b", "string")]);
let s1_id = crate::tree::store_schema_as_tree(&mut store, s1)?;
let c1 = CommitObject::builder(s1_id, "test", "bob", "add b")
.parents(vec![c0_id])
.timestamp(200)
.build();
let c1_id = store.put(&Object::Commit(c1))?;
let entry = blame_vertex(&store, c1_id, "b")?;
assert_eq!(entry.commit_id, c1_id);
assert_eq!(entry.author, "bob");
assert_eq!(entry.message, "add b");
Ok(())
}
#[test]
fn blame_vertex_not_found() -> Result<(), Box<dyn std::error::Error>> {
let mut store = MemStore::new();
let s = make_schema(&[("a", "object")]);
let schema_id = crate::tree::store_schema_as_tree(&mut store, s)?;
let commit = CommitObject::builder(schema_id, "test", "alice", "initial")
.timestamp(100)
.build();
let commit_id = store.put(&Object::Commit(commit))?;
assert!(blame_vertex(&store, commit_id, "nonexistent").is_err());
Ok(())
}
}