use crate::package_repair_request::RepairRequest;
use crate::package_repair_command::{ToolCommand, ToolEnd, ToolLimits, ToolRun, run_tool};
use crate::transaction_git::{GitRequest, run_git};
use std::ffi::OsString;
use std::path::{Path, PathBuf};
pub const MANIFEST: &str = "Cargo.toml";
fn is_nul_byte(byte: &u8) -> bool {
return *byte == 0;
}
fn compare_by_root(left: &Workspace, right: &Workspace) -> std::cmp::Ordering {
return left.root.cmp(&right.root);
}
fn same_root(later: &mut Workspace, earlier: &mut Workspace) -> bool {
return later.root == earlier.root;
}
fn text_field(metadata: &serde_json::Value, key: &str) -> Option<PathBuf> {
return metadata.get(key).and_then(serde_json::Value::as_str).map(PathBuf::from);
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Workspace {
pub root: PathBuf,
pub build_directory: PathBuf,
pub target_directory: PathBuf,
pub members: Vec<String>,
}
pub fn parse_metadata(text: &str) -> Result<Workspace, String> {
let metadata: serde_json::Value = match serde_json::from_str(text) {
Ok(parsed) => parsed,
Err(error) => return Err(format!("the output of cargo metadata is not JSON ({error})")),
};
let Some(root) = text_field(&metadata, "workspace_root") else {
return Err(String::from("cargo metadata names no workspace_root"));
};
let Some(target_directory) = text_field(&metadata, "target_directory") else {
return Err(String::from("cargo metadata names no target_directory"));
};
let build_directory: PathBuf = match text_field(&metadata, "build_directory") {
Some(directory) => directory,
None => target_directory.clone(),
};
let Some(packages) = metadata.get("packages").and_then(serde_json::Value::as_array) else {
return Err(String::from("cargo metadata names no packages"));
};
let mut members: Vec<String> = Vec::new();
for package in packages {
if let Some(name) = package.get("name").and_then(serde_json::Value::as_str) {
members.push(name.to_owned());
}
}
return Ok(Workspace { root, build_directory, target_directory, members });
}
pub fn distinct_workspaces(mut found: Vec<Workspace>) -> Vec<Workspace> {
found.sort_by(compare_by_root);
found.dedup_by(same_root);
return found;
}
pub fn manifest_directories(request: &RepairRequest<'_>) -> Result<Vec<PathBuf>, String> {
use std::os::unix::ffi::OsStrExt;
let listing = GitRequest::new(request.destination, &["ls-files", "-z", "--", "*Cargo.toml"]);
let output = match run_git(request.git, &listing) {
Ok(done) => done,
Err(error) => return Err(format!("git could not list the tracked manifests ({error})")),
};
if !output.succeeded() {
return Err(format!("git could not list the tracked manifests: {}", output.error_text()));
}
let mut directories: Vec<PathBuf> = Vec::new();
for entry in output.stdout.split(is_nul_byte) {
if entry.is_empty() {
continue;
}
let path: PathBuf = request.destination.join(std::ffi::OsStr::from_bytes(entry));
let is_manifest: bool = match path.file_name() {
Some(name) => name == MANIFEST,
None => false,
};
if !(is_manifest && path.is_file()) {
continue;
}
if let Some(parent) = path.parent() {
directories.push(parent.to_path_buf());
}
}
directories.sort();
directories.dedup();
return Ok(directories);
}
pub fn read_workspace(cargo: &Path, directory: &Path, limit: ToolLimits) -> Result<Workspace, String> {
let arguments: Vec<OsString> = ["metadata", "--locked", "--offline", "--no-deps", "--format-version", "1"].iter().map(OsString::from).collect();
let command: ToolCommand = ToolCommand { arguments, directory: PathBuf::from(directory), set: Vec::new() };
let run: ToolRun = run_tool(cargo, &command, limit);
return match run.end {
ToolEnd::Succeeded if !run.notes.is_empty() => Err(format!("cargo metadata in {} wrote output that could not be taken whole ({}), so it was not read", directory.display(), run.notes.join("; "))),
ToolEnd::Succeeded => parse_metadata(String::from_utf8_lossy(run.stdout.as_slice()).as_ref()),
ToolEnd::Failed(reason) => Err(format!("cargo metadata in {} failed: {reason}", directory.display())),
ToolEnd::TimedOut => Err(format!("cargo metadata in {} showed no sign of work for the quiet window and was stopped", directory.display())),
ToolEnd::CeilingReached(ceiling) => Err(format!("cargo metadata in {} ran for {} seconds, the longest the wrapper lets one repair tool run, and was stopped with its whole process group", directory.display(), ceiling.as_secs())),
};
}