use std::path::Path;
use anyhow::Result;
use serde::Serialize;
use crate::domain::dependency_status::{
display_dependency_alias, find_locked_package, load_lockfile,
};
use crate::lockfile::LockedPackage;
use crate::manifest::{
DependencyComponent, DependencyKind, DependencySourceKind, RequestedGitRef, load_root_from_dir,
};
use crate::paths::display_path;
use crate::report::Reporter;
#[derive(Debug, Clone, Serialize)]
pub struct DependencyList {
pub dependencies: Vec<DependencyListEntry>,
}
#[derive(Debug, Clone, Serialize)]
pub struct DependencyListEntry {
pub alias: String,
pub kind: DependencyKind,
#[serde(skip_serializing_if = "is_true")]
pub enabled: bool,
pub source: DependencyListSource,
#[serde(skip_serializing_if = "Option::is_none")]
pub requested_ref: Option<DependencyListRequestedRef>,
#[serde(skip_serializing_if = "Option::is_none")]
pub selected_components: Option<Vec<DependencyComponent>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub locked: Option<DependencyListLocked>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum DependencyListSource {
Path {
path: String,
},
Git {
url: String,
#[serde(skip_serializing_if = "Option::is_none")]
subpath: Option<String>,
},
}
#[derive(Debug, Clone, Serialize)]
pub struct DependencyListRequestedRef {
pub kind: &'static str,
pub value: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct DependencyListLocked {
#[serde(skip_serializing_if = "Option::is_none")]
pub version_tag: Option<String>,
pub digest: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub rev: Option<String>,
}
pub fn list_dependencies_in_dir(cwd: &Path, reporter: &Reporter) -> Result<()> {
let list = list_dependencies_json_in_dir(cwd)?;
if list.dependencies.is_empty() {
reporter.note("no dependencies configured")?;
return Ok(());
}
for dependency in &list.dependencies {
reporter.line(render_dependency_line(dependency))?;
}
Ok(())
}
pub fn list_dependencies_json_in_dir(cwd: &Path) -> Result<DependencyList> {
let root = load_root_from_dir(cwd)?;
let lockfile = load_lockfile(cwd)?;
let mut dependencies = root
.manifest
.all_dependency_entries()
.into_iter()
.map(|entry| {
let alias = entry.alias;
let spec = entry.spec;
let source = match spec.source_kind()? {
DependencySourceKind::Path => DependencyListSource::Path {
path: display_path(spec.path.as_deref().ok_or_else(|| {
anyhow::anyhow!("dependency `{alias}` must declare `path`")
})?),
},
DependencySourceKind::Git => DependencyListSource::Git {
url: spec.resolved_git_url()?,
subpath: spec.subpath.as_deref().map(display_path),
},
};
let requested_ref = match spec.source_kind()? {
DependencySourceKind::Path => None,
DependencySourceKind::Git => Some(match spec.requested_git_ref()? {
RequestedGitRef::Tag(tag) => DependencyListRequestedRef {
kind: "tag",
value: tag.to_string(),
},
RequestedGitRef::Branch(branch) => DependencyListRequestedRef {
kind: "branch",
value: branch.to_string(),
},
RequestedGitRef::Revision(revision) => DependencyListRequestedRef {
kind: "revision",
value: revision.to_string(),
},
RequestedGitRef::VersionReq(version) => DependencyListRequestedRef {
kind: "version",
value: version.to_string(),
},
}),
};
Ok(DependencyListEntry {
alias: alias.to_string(),
kind: entry.kind,
enabled: spec.is_enabled(),
source,
requested_ref,
selected_components: spec.effective_selected_components(),
locked: lockfile
.as_ref()
.and_then(|lockfile| find_locked_package(lockfile, alias))
.map(DependencyListLocked::from),
})
})
.collect::<Result<Vec<_>>>()?;
dependencies.sort_by(|left, right| left.alias.cmp(&right.alias));
Ok(DependencyList { dependencies })
}
impl From<&LockedPackage> for DependencyListLocked {
fn from(value: &LockedPackage) -> Self {
Self {
version_tag: value.version_tag.clone(),
digest: value.digest.clone(),
rev: value.source.rev.clone(),
}
}
}
fn render_dependency_line(dependency: &DependencyListEntry) -> String {
format!(
"{:<20} {}",
display_alias(dependency),
dependency_summary(dependency)
)
}
fn display_alias(dependency: &DependencyListEntry) -> String {
display_dependency_alias(&dependency.alias, dependency.kind)
}
fn dependency_summary(dependency: &DependencyListEntry) -> String {
let mut parts = Vec::new();
if !dependency.enabled {
parts.push("disabled".to_string());
}
parts.push(match &dependency.source {
DependencyListSource::Path { path } => format!("path {path}"),
DependencyListSource::Git { url, subpath } => match subpath {
Some(subpath) => format!("git {url} (subpath {subpath})"),
None => format!("git {url}"),
},
});
if let Some(requested_ref) = &dependency.requested_ref {
parts.push(format!("{} {}", requested_ref.kind, requested_ref.value));
}
parts.push(format!(
"components {}",
render_components(dependency.selected_components.as_deref())
));
parts.push(render_locked_summary(dependency.locked.as_ref()));
parts.join("; ")
}
fn render_components(components: Option<&[DependencyComponent]>) -> String {
match components {
Some(components) => components
.iter()
.map(|component| component.as_str())
.collect::<Vec<_>>()
.join(", "),
None => "all".into(),
}
}
fn render_locked_summary(locked: Option<&DependencyListLocked>) -> String {
match locked {
Some(locked) => {
if let Some(rev) = &locked.rev {
format!("locked rev {}", short_value(rev))
} else {
format!("locked digest {}", short_value(&locked.digest))
}
}
None => "unlocked".into(),
}
}
fn short_value(value: &str) -> String {
value.chars().take(12).collect()
}
fn is_true(value: &bool) -> bool {
*value
}