use crate::pkg;
use anyhow::{anyhow, Result};
use forc_util::{println_green, println_red};
use petgraph::{visit::EdgeRef, Direction};
use serde::{Deserialize, Serialize};
use std::{
borrow::Cow,
collections::{BTreeSet, HashMap, HashSet},
fs,
path::Path,
};
#[derive(Debug, Default, Deserialize, Serialize)]
pub struct Lock {
pub(crate) package: BTreeSet<PkgLock>,
}
pub struct Diff<'a> {
pub removed: BTreeSet<&'a PkgLock>,
pub added: BTreeSet<&'a PkgLock>,
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Deserialize, Serialize)]
pub struct PkgLock {
pub(crate) name: String,
version: Option<semver::Version>,
source: String,
dependencies: Vec<PkgDepLine>,
}
pub type PkgDepLine = String;
impl PkgLock {
pub fn from_node(graph: &pkg::Graph, node: pkg::NodeIx, disambiguate: &HashSet<&str>) -> Self {
let pinned = &graph[node];
let name = pinned.name.clone();
let version = match &pinned.source {
pkg::SourcePinned::Registry(reg) => Some(reg.source.version.clone()),
_ => None,
};
let source = pinned.source.to_string();
let mut dependencies: Vec<String> = graph
.edges_directed(node, Direction::Outgoing)
.map(|edge| {
let dep_name = edge.weight();
let dep_node = edge.target();
let dep_pkg = &graph[dep_node];
let dep_name = if *dep_name != dep_pkg.name {
Some(&dep_name[..])
} else {
None
};
let disambiguate = disambiguate.contains(&dep_pkg.name[..]);
pkg_dep_line(dep_name, &dep_pkg.name, &dep_pkg.source, disambiguate)
})
.collect();
dependencies.sort();
Self {
name,
version,
source,
dependencies,
}
}
pub fn unique_string(&self) -> String {
pkg_unique_string(&self.name, &self.source)
}
pub fn name_disambiguated(&self, disambiguate: &HashSet<&str>) -> Cow<str> {
let disambiguate = disambiguate.contains(&self.name[..]);
pkg_name_disambiguated(&self.name, &self.source, disambiguate)
}
}
impl Lock {
pub fn from_path(path: &Path) -> Result<Self> {
let string = fs::read_to_string(&path)
.map_err(|e| anyhow!("failed to read {}: {}", path.display(), e))?;
toml::de::from_str(&string).map_err(|e| anyhow!("failed to parse lock file: {}", e))
}
pub fn from_graph(graph: &pkg::Graph) -> Self {
let names = graph.node_indices().map(|n| &graph[n].name[..]);
let disambiguate: HashSet<_> = names_requiring_disambiguation(names).collect();
let package: BTreeSet<_> = graph
.node_indices()
.map(|node| PkgLock::from_node(graph, node, &disambiguate))
.collect();
Self { package }
}
pub fn to_graph(&self) -> Result<pkg::Graph> {
let mut graph = pkg::Graph::new();
let names = self.package.iter().map(|pkg| &pkg.name[..]);
let disambiguate: HashSet<_> = names_requiring_disambiguation(names).collect();
let mut pkg_to_node: HashMap<String, pkg::NodeIx> = HashMap::new();
for pkg in &self.package {
let key = pkg.name_disambiguated(&disambiguate).into_owned();
let name = pkg.name.clone();
let source: pkg::SourcePinned = pkg.source.parse().map_err(|e| {
anyhow!("invalid 'source' entry for package {} lock: {:?}", name, e)
})?;
let pkg = pkg::Pinned { name, source };
let node = graph.add_node(pkg);
pkg_to_node.insert(key, node);
}
for pkg in &self.package {
let key = pkg.name_disambiguated(&disambiguate);
let node = pkg_to_node[&key[..]];
for dep_line in &pkg.dependencies {
let (dep_name, dep_key) = parse_pkg_dep_line(dep_line)
.map_err(|e| anyhow!("failed to parse dependency \"{}\": {}", dep_line, e))?;
let dep_node = pkg_to_node
.get(dep_key)
.cloned()
.ok_or_else(|| anyhow!("found dep {} without node entry in graph", dep_key))?;
let dep_name = dep_name.unwrap_or(&graph[dep_node].name).to_string();
graph.update_edge(node, dep_node, dep_name);
}
}
Ok(graph)
}
pub fn diff<'a>(&'a self, old: &'a Self) -> Diff<'a> {
let added = self.package.difference(&old.package).collect();
let removed = old.package.difference(&self.package).collect();
Diff { added, removed }
}
}
fn names_requiring_disambiguation<'a, I>(names: I) -> impl Iterator<Item = &'a str>
where
I: IntoIterator<Item = &'a str>,
{
let mut visited = BTreeSet::default();
names.into_iter().filter(move |&name| !visited.insert(name))
}
fn pkg_name_disambiguated<'a>(name: &'a str, source: &'a str, disambiguate: bool) -> Cow<'a, str> {
match disambiguate {
true => Cow::Owned(pkg_unique_string(name, source)),
false => Cow::Borrowed(name),
}
}
fn pkg_unique_string(name: &str, source: &str) -> String {
format!("{} {}", name, source)
}
fn pkg_dep_line(
dep_name: Option<&str>,
name: &str,
source: &pkg::SourcePinned,
disambiguate: bool,
) -> PkgDepLine {
let source_string = source.to_string();
let pkg_string = pkg_name_disambiguated(name, &source_string, disambiguate);
match dep_name {
None => pkg_string.into_owned(),
Some(dep_name) => format!("({}) {}", dep_name, pkg_string),
}
}
fn parse_pkg_dep_line(pkg_dep_line: &str) -> anyhow::Result<(Option<&str>, &str)> {
let s = pkg_dep_line.trim();
if !s.starts_with('(') {
return Ok((None, s));
}
let s = &s["(".len()..];
let mut iter = s.split(')');
let dep_name = iter
.next()
.ok_or_else(|| anyhow!("missing closing parenthesis"))?;
let s = &s[dep_name.len() + ")".len()..];
let pkg_str = s.trim_start();
Ok((Some(dep_name), pkg_str))
}
pub fn print_diff(proj_name: &str, diff: &Diff) {
print_removed_pkgs(proj_name, diff.removed.iter().cloned());
print_added_pkgs(proj_name, diff.added.iter().cloned());
}
pub fn print_removed_pkgs<'a, I>(proj_name: &str, removed: I)
where
I: IntoIterator<Item = &'a PkgLock>,
{
for pkg in removed {
if pkg.name != proj_name {
let name = name_or_git_unique_string(pkg);
println_red(&format!(" Removing {}", name));
}
}
}
pub fn print_added_pkgs<'a, I>(proj_name: &str, removed: I)
where
I: IntoIterator<Item = &'a PkgLock>,
{
for pkg in removed {
if pkg.name != proj_name {
let name = name_or_git_unique_string(pkg);
println_green(&format!(" Adding {}", name));
}
}
}
fn name_or_git_unique_string(pkg: &PkgLock) -> Cow<str> {
match pkg.source.starts_with(pkg::SourceGitPinned::PREFIX) {
true => Cow::Owned(pkg.unique_string()),
false => Cow::Borrowed(&pkg.name),
}
}