use petgraph::{
algo::toposort,
graph::{DefaultIx, DiGraph, NodeIndex},
visit::EdgeRef,
};
use std::collections::{HashMap, HashSet};
use crate::{
errors::{Error, Result},
project::{Project, ProjectBuilder, ProjectId},
repository::{CommitAvailability, CommitId, ReleaseCommitInfo, RepoHistory, Repository},
};
type OurNodeIndex = NodeIndex<DefaultIx>;
#[derive(Debug, Default)]
pub struct ProjectGraph {
projects: Vec<Project>,
node_ixs: Vec<OurNodeIndex>,
graph: DiGraph<ProjectId, Option<CommitId>>,
name_to_id: HashMap<String, ProjectId>,
}
impl ProjectGraph {
pub fn len(&self) -> usize {
self.projects.len()
}
pub fn add_project<'a>(&'a mut self) -> ProjectBuilder<'a> {
if self.name_to_id.len() != 0 {
panic!("cannot add projects after finalizing initialization");
}
ProjectBuilder::new(self)
}
#[doc(hidden)]
pub fn finalize_project_addition<F>(&mut self, f: F) -> ProjectId
where
F: FnOnce(ProjectId) -> Project,
{
let id = self.projects.len();
self.projects.push(f(id));
self.node_ixs.push(self.graph.add_node(id));
id
}
pub fn lookup(&self, ident: ProjectId) -> &Project {
&self.projects[ident]
}
pub fn lookup_mut(&mut self, ident: ProjectId) -> &mut Project {
&mut self.projects[ident]
}
pub fn lookup_ident<S: AsRef<str>>(&self, name: S) -> Option<ProjectId> {
self.name_to_id.get(name.as_ref()).map(|id| *id)
}
pub fn add_dependency(
&mut self,
depender_id: ProjectId,
dependee_id: ProjectId,
min_version: Option<CommitId>,
) {
let depender_nix = self.node_ixs[depender_id];
let dependee_nix = self.node_ixs[dependee_id];
self.graph.add_edge(dependee_nix, depender_nix, min_version);
}
pub fn complete_loading(&mut self) -> Result<()> {
let node_ixs = toposort(&self.graph, None).map_err(|cycle| {
let ident = self.graph[cycle.node_id()];
Error::Cycle(self.projects[ident].user_facing_name.to_owned())
})?;
let name_to_id = &mut self.name_to_id;
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
struct NamingState {
pub n_narrow: usize,
}
impl Default for NamingState {
fn default() -> Self {
NamingState { n_narrow: 1 }
}
}
impl NamingState {
fn compute_name(&self, proj: &Project) -> String {
let mut s = String::new();
let qnames = proj.qualified_names();
const SEP: char = ':';
for i in 0..self.n_narrow {
if i != 0 {
s.push(SEP);
}
s.push_str(&qnames[self.n_narrow - 1 - i]);
}
s
}
}
let mut states = vec![NamingState::default(); self.projects.len()];
let mut need_another_pass = true;
while need_another_pass {
name_to_id.clear();
need_another_pass = false;
for node_ix in &node_ixs {
use std::collections::hash_map::Entry;
let ident1 = self.graph[*node_ix];
let proj1 = &self.projects[ident1];
let candidate_name = states[ident1].compute_name(proj1);
let ident2: ProjectId = match name_to_id.entry(candidate_name) {
Entry::Vacant(o) => {
o.insert(ident1);
continue;
}
Entry::Occupied(o) => o.remove(),
};
let proj2 = &self.projects[ident2];
let qn1 = proj1.qualified_names();
let qn2 = proj2.qualified_names();
let n1 = qn1.len();
let n2 = qn2.len();
let mut success = false;
for i in 0..std::cmp::min(n1, n2) {
if qn1[i] != qn2[i] {
success = true;
states[ident1].n_narrow = std::cmp::max(states[ident1].n_narrow, i + 1);
states[ident2].n_narrow = std::cmp::max(states[ident2].n_narrow, i + 1);
break;
}
}
if !success {
if n1 > n2 {
states[ident1].n_narrow = std::cmp::max(states[ident1].n_narrow, n2 + 1);
} else if n2 > n1 {
states[ident2].n_narrow = std::cmp::max(states[ident2].n_narrow, n1 + 1);
} else {
return Err(Error::NamingClash(states[ident1].compute_name(proj1)));
}
}
if name_to_id
.insert(states[ident1].compute_name(proj1), ident1)
.is_some()
{
need_another_pass = true; }
if name_to_id
.insert(states[ident2].compute_name(proj2), ident2)
.is_some()
{
need_another_pass = true; }
}
}
for (name, ident) in name_to_id {
self.projects[*ident].user_facing_name = name.clone();
}
for index1 in 1..self.projects.len() {
let (left, right) = self.projects.split_at_mut(index1);
let litem = &mut left[index1 - 1];
for ritem in right {
litem.repo_paths.make_disjoint(&ritem.repo_paths);
ritem.repo_paths.make_disjoint(&litem.repo_paths);
}
}
Ok(())
}
pub fn projects(&self) -> GraphIter {
GraphIter {
graph: self,
node_idxs_iter: self
.graph
.node_indices()
.collect::<Vec<OurNodeIndex>>()
.into_iter(),
}
}
pub fn toposort_idents(&self) -> Result<impl IntoIterator<Item = ProjectId>> {
let idents = toposort(&self.graph, None)
.map_err(|cycle| {
let ident = self.graph[cycle.node_id()];
Error::Cycle(self.projects[ident].user_facing_name.to_owned())
})?
.iter()
.map(|ix| self.graph[*ix])
.collect::<Vec<_>>();
Ok(idents)
}
pub fn toposort(&self) -> Result<GraphIter> {
let node_idxs = toposort(&self.graph, None).map_err(|cycle| {
let ident = self.graph[cycle.node_id()];
Error::Cycle(self.projects[ident].user_facing_name.to_owned())
})?;
Ok(GraphIter {
graph: self,
node_idxs_iter: node_idxs.into_iter(),
})
}
pub fn toposort_mut(&mut self) -> Result<GraphIterMut> {
let node_idxs = toposort(&self.graph, None).map_err(|cycle| {
let ident = self.graph[cycle.node_id()];
Error::Cycle(self.projects[ident].user_facing_name.to_owned())
})?;
Ok(GraphIterMut {
graph: self,
node_idxs_iter: node_idxs.into_iter(),
})
}
pub fn query(&self, query: GraphQueryBuilder) -> Result<Vec<ProjectId>> {
let mut matched_idents = Vec::new();
let mut seen_ids = HashSet::new();
let root_idents = if query.no_names() {
toposort(&self.graph, None)
.map_err(|cycle| {
let ident = self.graph[cycle.node_id()];
Error::Cycle(self.projects[ident].user_facing_name.to_owned())
})?
.iter()
.map(|ix| self.graph[*ix])
.collect::<Vec<_>>()
} else {
let mut root_idents = Vec::new();
for name in query.names {
if let Some(id) = self.name_to_id.get(&name) {
root_idents.push(*id);
} else {
return Err(Error::NoSuchProject(name));
}
}
root_idents
};
for id in root_idents {
let proj = &self.projects[id];
if let Some(ref rel_info) = query.release_info {
if rel_info.lookup_if_released(proj).is_none() {
continue;
}
}
if let Some(ref ptype) = query.project_type {
let qnames = proj.qualified_names();
let n = qnames.len();
if n < 2 {
continue;
}
if &qnames[n - 1] != ptype {
continue;
}
}
if seen_ids.insert(id) {
matched_idents.push(id);
}
}
Ok(matched_idents)
}
pub fn analyze_histories(&self, repo: &Repository) -> Result<RepoHistories> {
Ok(RepoHistories {
histories: repo.analyze_histories(&self.projects[..])?,
})
}
pub fn resolve_direct_dependencies(
&self,
repo: &Repository,
ident: ProjectId,
) -> Result<Vec<ResolvedDependency>> {
let mut deps = Vec::new();
for edge in self
.graph
.edges_directed(self.node_ixs[ident], petgraph::Direction::Incoming)
{
let dependee_id = self.graph[edge.source()];
let dependee_proj = &self.projects[dependee_id];
let maybe_cid = edge.weight();
let availability = if let Some(cid) = maybe_cid {
repo.find_earliest_release_containing(dependee_proj, cid)?
} else {
CommitAvailability::NotAvailable
};
deps.push(ResolvedDependency {
ident: dependee_id,
min_commit: maybe_cid.clone(),
availability,
});
}
Ok(deps)
}
}
#[derive(Clone, Debug)]
pub struct RepoHistories {
histories: Vec<RepoHistory>,
}
impl RepoHistories {
pub fn lookup(&self, projid: ProjectId) -> &RepoHistory {
&self.histories[projid]
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ResolvedDependency {
pub ident: ProjectId,
pub min_commit: Option<CommitId>,
pub availability: CommitAvailability,
}
#[derive(Debug)]
pub struct GraphQueryBuilder {
names: Vec<String>,
release_info: Option<ReleaseCommitInfo>,
project_type: Option<String>,
}
impl Default for GraphQueryBuilder {
fn default() -> Self {
GraphQueryBuilder {
names: Vec::new(),
release_info: None,
project_type: None,
}
}
}
impl GraphQueryBuilder {
pub fn names<T: std::fmt::Display>(&mut self, names: impl IntoIterator<Item = T>) -> &mut Self {
self.names = names.into_iter().map(|s| s.to_string()).collect();
self
}
pub fn only_new_releases(&mut self, rel_info: ReleaseCommitInfo) -> &mut Self {
self.release_info = Some(rel_info);
self
}
pub fn only_project_type<T: std::fmt::Display>(&mut self, ptype: T) -> &mut Self {
self.project_type = Some(ptype.to_string());
self
}
pub fn no_names(&self) -> bool {
self.names.len() == 0
}
}
pub struct GraphIter<'a> {
graph: &'a ProjectGraph,
node_idxs_iter: std::vec::IntoIter<OurNodeIndex>,
}
impl<'a> Iterator for GraphIter<'a> {
type Item = &'a Project;
fn next(&mut self) -> Option<&'a Project> {
let node_ix = self.node_idxs_iter.next()?;
let ident = self.graph.graph[node_ix];
Some(self.graph.lookup(ident))
}
}
pub struct GraphIterMut<'a> {
graph: &'a mut ProjectGraph,
node_idxs_iter: std::vec::IntoIter<OurNodeIndex>,
}
impl<'a> Iterator for GraphIterMut<'a> {
type Item = &'a mut Project;
fn next(&mut self) -> Option<&'a mut Project> {
let node_ix = self.node_idxs_iter.next()?;
let ident = self.graph.graph[node_ix];
Some(unsafe { &mut *(self.graph.lookup_mut(ident) as *mut _) })
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{repository::RepoPathBuf, version::Version};
fn do_name_assignment_test(spec: &[(&[&str], &str)]) -> Result<()> {
let mut graph = ProjectGraph::default();
let mut ids = HashMap::new();
for (qnames, user_facing) in spec {
let mut b = graph.add_project();
b.qnames(*qnames);
b.version(Version::Semver(semver::Version::new(0, 0, 0)));
b.prefix(RepoPathBuf::new(b""));
let projid = b.finish_init();
ids.insert(projid, user_facing);
}
graph.complete_loading()?;
for (projid, user_facing) in ids {
assert_eq!(graph.lookup(projid).user_facing_name, *user_facing);
}
Ok(())
}
#[test]
fn name_assignment_1() {
do_name_assignment_test(&[(&["A", "B"], "A")]).unwrap();
}
#[test]
fn name_assignment_2() {
do_name_assignment_test(&[(&["A", "B"], "B:A"), (&["A", "C"], "C:A")]).unwrap();
}
#[test]
fn name_assignment_3() {
do_name_assignment_test(&[
(&["A", "B"], "B:A"),
(&["A", "C"], "C:A"),
(&["D", "B"], "D"),
(&["E"], "E"),
])
.unwrap();
}
#[test]
fn name_assignment_4() {
do_name_assignment_test(&[(&["A", "A"], "A:A"), (&["A"], "A")]).unwrap();
}
#[test]
fn name_assignment_5() {
do_name_assignment_test(&[
(&["A"], "A"),
(&["A", "B"], "B:A"),
(&["A", "B", "C"], "C:B:A"),
(&["A", "B", "C", "D"], "D:C:B:A"),
])
.unwrap();
}
}