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::{
collections::{BTreeSet, HashMap},
fs,
path::Path,
str::FromStr,
};
#[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: Option<String>,
dependencies: Vec<PkgDepLine>,
}
pub type PkgDepLine = String;
pub fn source_to_string(source: &pkg::SourcePinned) -> Option<String> {
match source {
pkg::SourcePinned::Path => None,
pkg::SourcePinned::Git(git) => Some(git.to_string()),
pkg::SourcePinned::Registry(_reg) => unimplemented!("pkg registries not yet implemented"),
}
}
pub fn source_from_str(s: &str) -> Result<pkg::SourcePinned> {
if let Ok(src) = pkg::SourceGitPinned::from_str(s) {
return Ok(pkg::SourcePinned::Git(src));
}
Err(anyhow!(
"Unable to parse valid pinned source from given string {}",
s
))
}
impl PkgLock {
pub fn from_node(graph: &pkg::Graph, node: pkg::NodeIx) -> 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 = source_to_string(&pinned.source);
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 source_string = source_to_string(&dep_pkg.source);
pkg_dep_line(dep_name, &dep_pkg.name, source_string.as_deref())
})
.collect();
dependencies.sort();
Self {
name,
version,
source,
dependencies,
}
}
pub fn unique_string(&self) -> String {
pkg_unique_string(&self.name, self.source.as_deref())
}
}
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 package: BTreeSet<_> = graph
.node_indices()
.map(|node| PkgLock::from_node(graph, node))
.collect();
Self { package }
}
pub fn to_graph(&self) -> Result<pkg::Graph> {
let mut graph = pkg::Graph::new();
let mut pkg_to_node: HashMap<String, pkg::NodeIx> = HashMap::new();
for pkg in &self.package {
let key = pkg.unique_string();
let name = pkg.name.clone();
let pkg_source_string = pkg.source.clone();
let source = match &pkg_source_string {
None => pkg::SourcePinned::Path,
Some(s) => source_from_str(s).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.unique_string();
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.add_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 pkg_unique_string(name: &str, source: Option<&str>) -> String {
match source {
None => name.to_string(),
Some(s) => format!("{} {}", name, s),
}
}
fn pkg_dep_line(dep_name: Option<&str>, name: &str, source: Option<&str>) -> PkgDepLine {
let pkg_string = pkg_unique_string(name, source);
match dep_name {
None => pkg_string,
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 _ = println_red(&format!(" Removing {}", pkg.unique_string()));
}
}
}
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 _ = println_green(&format!(" Adding {}", pkg.unique_string()));
}
}
}