use crate::cli::{OutputFormat, StatusOpts};
use crate::error::{CommandExitCode, Result};
use cargo_metadata::MetadataCommand;
use governor_core::{
domain::{commit::Commit, dependency::DependencyGraph, workspace::WorkingTreeStatus},
traits::source_control::SourceControl,
};
use governor_git::GitAdapter;
use serde_json::json;
use std::path::PathBuf;
pub struct StatusService {
workspace_path: String,
opts: StatusOpts,
}
impl StatusService {
pub const fn new(workspace_path: String, opts: StatusOpts) -> Self {
Self {
workspace_path,
opts,
}
}
pub async fn execute(&self, _format: OutputFormat) -> Result<CommandExitCode> {
let start_time = std::time::Instant::now();
let metadata = self.parse_metadata()?;
let workspace_packages = self.get_workspace_packages(&metadata);
let (current_branch, last_tag, commits_since_tag, working_tree_status) =
self.fetch_git_status().await?;
let workspace_name = self.get_workspace_name(&metadata);
let current_version = self.get_current_version(&workspace_packages);
let dep_graph = self.build_dependency_graph(&metadata, &workspace_packages);
let publish_order = dep_graph.publish_order().ok().unwrap_or_default();
let crates = self.build_crates_list(&workspace_packages, &commits_since_tag);
let response = json!({
"success": true,
"command": "status",
"workspace": workspace_name,
"result": {
"workspace": {
"name": workspace_name,
"current_version": current_version,
"branch": current_branch,
"last_tag": last_tag,
"commits_since_tag": commits_since_tag.len(),
},
"git": {
"clean": !working_tree_status.has_changes,
"modified": working_tree_status.modified,
"added": working_tree_status.added,
"deleted": working_tree_status.deleted,
"untracked": working_tree_status.untracked,
},
"crates": crates,
"dependency_graph": if self.opts.show_deps {
Some(json!({
"crates": publish_order.len(),
"publish_order": publish_order,
"has_cycles": dep_graph.has_cycles(),
}))
} else {
None
},
},
"metrics": {
"execution_time_ms": start_time.elapsed().as_millis(),
"git_operations": 3,
"api_calls": 0,
}
});
println!("{}", serde_json::to_string_pretty(&response).unwrap());
Ok(CommandExitCode::Success)
}
fn parse_metadata(&self) -> Result<cargo_metadata::Metadata> {
MetadataCommand::new()
.current_dir(PathBuf::from(&self.workspace_path))
.no_deps()
.exec()
.map_err(|e| crate::error::Error::Config(format!("Failed to read cargo metadata: {e}")))
}
fn get_workspace_packages(
&self,
metadata: &cargo_metadata::Metadata,
) -> Vec<cargo_metadata::Package> {
let workspace_members: Vec<_> = metadata
.workspace_members
.iter()
.map(std::string::ToString::to_string)
.collect();
metadata
.packages
.clone()
.into_iter()
.filter(|p| workspace_members.contains(&p.id.to_string()))
.collect()
}
async fn fetch_git_status(
&self,
) -> Result<(String, Option<String>, Vec<Commit>, WorkingTreeStatus)> {
let git = GitAdapter::open(governor_git::GitAdapterConfig {
repository_path: Some(PathBuf::from(&self.workspace_path)),
..Default::default()
})
.map_err(|e| crate::error::Error::Config(format!("Failed to open git repository: {e}")))?;
let current_branch = git
.get_current_branch()
.await
.map_err(|e| crate::error::Error::Git(format!("Failed to get current branch: {e}")))?;
let last_tag = git
.get_last_tag(Some("v*"))
.await
.map_err(|e| crate::error::Error::Git(format!("Failed to get last tag: {e}")))?;
let commits_since_tag = if let Some(ref tag) = last_tag {
git.get_commits_since(Some(tag))
.await
.map_err(|e| crate::error::Error::Git(format!("Failed to get commits: {e}")))?
} else {
Vec::new()
};
let working_tree_status = git.get_working_tree_status().await.map_err(|e| {
crate::error::Error::Git(format!("Failed to get working tree status: {e}"))
})?;
Ok((
current_branch,
last_tag,
commits_since_tag,
working_tree_status,
))
}
fn get_workspace_name(&self, metadata: &cargo_metadata::Metadata) -> String {
metadata
.workspace_root
.file_name()
.map_or_else(|| "workspace".to_string(), std::string::ToString::to_string)
}
fn get_current_version(&self, workspace_packages: &[cargo_metadata::Package]) -> String {
workspace_packages
.first()
.map_or_else(|| "0.0.0".to_string(), |p| p.version.to_string())
}
fn build_dependency_graph(
&self,
metadata: &cargo_metadata::Metadata,
workspace_packages: &[cargo_metadata::Package],
) -> DependencyGraph {
let mut dep_graph = DependencyGraph::new();
let workspace_root = &metadata.workspace_root;
for pkg in workspace_packages {
for dep in &pkg.dependencies {
if let Some(ref dep_path) = dep.path
&& dep_path.starts_with(workspace_root)
{
dep_graph.add(governor_core::domain::dependency::WorkspaceDependency::new(
pkg.name.clone(),
dep.name.clone(),
dep.req.to_string(),
));
}
}
}
dep_graph
}
fn build_crates_list(
&self,
workspace_packages: &[cargo_metadata::Package],
commits_since_tag: &[Commit],
) -> Vec<serde_json::Value> {
if self.opts.all {
workspace_packages
.iter()
.map(|p| {
let version = p.version.to_string();
json!({
"name": p.name.as_str(),
"version": version,
"published": false,
"status": "unpublished",
})
})
.collect()
} else {
let changed_crates: std::collections::HashSet<String> = commits_since_tag
.iter()
.filter_map(|c| c.scope.clone())
.collect();
if changed_crates.is_empty() {
vec![]
} else {
changed_crates
.iter()
.map(|name| {
json!({
"name": name,
"status": "changed",
})
})
.collect()
}
}
}
}