use petgraph::{
algo::toposort,
graph::{DefaultIx, DiGraph, NodeIndex},
};
use std::collections::{HashMap, HashSet};
use thiserror::Error as ThisError;
use crate::{
a_ok_or, atry,
config::ProjectConfiguration,
errors::Result,
project::{
DepRequirement, Dependency, DependencyBuilder, DependencyTarget, Project, ProjectBuilder,
ProjectId,
},
repository::{ReleaseCommitInfo, RepoHistory, Repository},
};
type OurNodeIndex = NodeIndex<DefaultIx>;
#[derive(Debug, Default)]
pub struct ProjectGraph {
projects: Vec<Project>,
graph: DiGraph<ProjectId, ()>,
name_to_id: HashMap<String, ProjectId>,
toposorted_ids: Vec<ProjectId>,
}
#[derive(Debug, ThisError)]
#[error("no such project with the name `{0}`")]
pub struct NoSuchProjectError(pub String);
impl ProjectGraph {
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()).copied()
}
pub fn projects(&self) -> GraphIter {
GraphIter {
graph: self,
node_idxs_iter: self
.graph
.node_indices()
.collect::<Vec<OurNodeIndex>>()
.into_iter(),
}
}
pub fn toposorted(&self) -> TopoSortIdentIter {
TopoSortIdentIter {
graph: self,
index: 0,
}
}
pub fn toposorted_mut(&mut self) -> TopoSortIterMut {
TopoSortIterMut {
graph: self,
index: 0,
}
}
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() {
self.toposorted_ids.clone()
} 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(NoSuchProjectError(name).into());
}
}
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[..])?,
})
}
}
#[derive(Clone, Debug)]
pub struct RepoHistories {
histories: Vec<RepoHistory>,
}
impl RepoHistories {
pub fn lookup(&self, projid: ProjectId) -> &RepoHistory {
&self.histories[projid]
}
}
#[derive(Debug, Default)]
pub struct GraphQueryBuilder {
names: Vec<String>,
release_info: Option<ReleaseCommitInfo>,
project_type: Option<String>,
}
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.is_empty()
}
}
#[derive(Debug)]
pub struct ProjectGraphBuilder {
projects: Vec<ProjectBuilder>,
node_ixs: Vec<OurNodeIndex>,
graph: DiGraph<ProjectId, ()>,
}
#[derive(Debug, ThisError)]
#[error("detected an internal dependency cycle associated with project {0}")]
pub struct DependencyCycleError(pub String);
#[derive(Debug, ThisError)]
#[error("multiple projects with same name `{0}`")]
pub struct NamingClashError(pub String);
impl ProjectGraphBuilder {
pub(crate) fn new() -> ProjectGraphBuilder {
ProjectGraphBuilder {
projects: Vec::new(),
node_ixs: Vec::new(),
graph: DiGraph::default(),
}
}
pub fn try_add_project(
&mut self,
qnames: Vec<String>,
pconfig: &HashMap<String, ProjectConfiguration>,
) -> Option<ProjectId> {
let mut full_name = String::new();
for term in qnames.iter().rev() {
if !full_name.is_empty() {
full_name.push(':')
}
full_name.push_str(term);
}
let ignore = pconfig
.get(&full_name)
.map(|c| c.ignore)
.unwrap_or_default();
if ignore {
return None;
}
let mut pbuilder = ProjectBuilder::new();
pbuilder.qnames = qnames;
let id = self.projects.len();
self.projects.push(pbuilder);
self.node_ixs.push(self.graph.add_node(id));
Some(id)
}
pub fn lookup_mut(&mut self, ident: ProjectId) -> &mut ProjectBuilder {
&mut self.projects[ident]
}
pub fn add_dependency(
&mut self,
depender_id: ProjectId,
dependee_target: DependencyTarget,
literal: String,
req: DepRequirement,
) {
self.projects[depender_id]
.internal_deps
.push(DependencyBuilder {
target: dependee_target,
literal,
cranko_requirement: req,
resolved_version: None,
});
}
pub fn complete_loading(mut self) -> Result<ProjectGraph> {
let mut name_to_id = HashMap::new();
#[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: &ProjectBuilder) -> String {
let mut s = String::new();
const SEP: char = ':';
for i in 0..self.n_narrow {
if i != 0 {
s.push(SEP);
}
s.push_str(&proj.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 &self.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.qnames;
let qn2 = &proj2.qnames;
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 {
use std::cmp::Ordering;
match n1.cmp(&n2) {
Ordering::Greater => {
states[ident1].n_narrow =
std::cmp::max(states[ident1].n_narrow, n2 + 1);
}
Ordering::Less => {
states[ident2].n_narrow =
std::cmp::max(states[ident2].n_narrow, n1 + 1);
}
Ordering::Equal => {
return Err(NamingClashError(states[ident1].compute_name(proj1)).into());
}
}
}
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; }
}
}
let mut projects = Vec::with_capacity(self.projects.len());
for (ident, mut proj_builder) in self.projects.drain(..).enumerate() {
let mut name = None;
for (i_name, i_ident) in &name_to_id {
if *i_ident == ident {
name = Some(i_name.clone());
break;
}
}
let name = name.unwrap();
let mut internal_deps = Vec::with_capacity(proj_builder.internal_deps.len());
let depender_nix = self.node_ixs[ident];
for dep in proj_builder.internal_deps.drain(..) {
let dep_ident = match dep.target {
DependencyTarget::Ident(id) => id,
DependencyTarget::Text(ref dep_name) => *a_ok_or!(
name_to_id.get(dep_name);
["project `{}` states a dependency on an unrecognized project name: `{}`",
name, dep_name]
),
};
internal_deps.push(Dependency {
ident: dep_ident,
literal: dep.literal,
cranko_requirement: dep.cranko_requirement,
resolved_version: dep.resolved_version,
});
let dependee_nix = self.node_ixs[dep_ident];
self.graph.add_edge(dependee_nix, depender_nix, ());
}
let proj = proj_builder.finalize(ident, name, internal_deps)?;
projects.push(proj);
}
let sorted_nixs = atry!(
toposort(&self.graph, None).map_err(|cycle| {
let ident = self.graph[cycle.node_id()];
DependencyCycleError(projects[ident].user_facing_name.to_owned())
});
["the project graph contains a dependency cycle"]
);
let toposorted_ids = sorted_nixs
.iter()
.map(|node_ix| self.graph[*node_ix])
.collect();
for index1 in 1..projects.len() {
let (left, right) = 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(ProjectGraph {
projects,
name_to_id,
graph: self.graph,
toposorted_ids,
})
}
}
pub struct TopoSortIdentIter<'a> {
graph: &'a ProjectGraph,
index: usize,
}
impl<'a> Iterator for TopoSortIdentIter<'a> {
type Item = ProjectId;
fn next(&mut self) -> Option<ProjectId> {
if self.index < self.graph.toposorted_ids.len() {
let rv = self.graph.toposorted_ids[self.index];
self.index += 1;
Some(rv)
} else {
None
}
}
}
pub struct TopoSortIterMut<'a> {
graph: &'a mut ProjectGraph,
index: usize,
}
impl<'a> Iterator for TopoSortIterMut<'a> {
type Item = &'a mut Project;
fn next(&mut self) -> Option<&'a mut Project> {
if self.index < self.graph.toposorted_ids.len() {
let ident = self.graph.toposorted_ids[self.index];
self.index += 1;
Some(unsafe { &mut *(self.graph.lookup_mut(ident) as *mut _) })
} else {
None
}
}
}
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 = ProjectGraphBuilder::new();
let mut ids = HashMap::new();
let empty_config = HashMap::new();
for (qnames, user_facing) in spec {
let qnames = qnames.iter().map(|s| (*s).to_owned()).collect();
let projid = graph.try_add_project(qnames, &empty_config).unwrap();
let mut b = graph.lookup_mut(projid);
b.version = Some(Version::Semver(semver::Version::new(0, 0, 0)));
b.prefix = Some(RepoPathBuf::new(b""));
ids.insert(projid, user_facing);
}
let graph = 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();
}
}