pub mod analysis;
pub mod bump;
pub mod risk;
pub use analysis::{analyze_commits, build_breaking_changes, build_features, build_fixes};
pub use bump::{calculate_confidence, generate_reasoning};
pub use risk::calculate_risk_assessment;
use crate::cli::{AnalyzeOpts, OutputFormat};
use crate::error::{CommandExitCode, Result};
use governor_core::{
domain::version::{BumpType, SemanticVersion},
traits::source_control::SourceControl,
};
use governor_git::GitAdapter;
use serde_json::json;
pub struct AnalyzeService {
workspace_path: String,
opts: AnalyzeOpts,
}
impl AnalyzeService {
pub const fn new(workspace_path: String, opts: AnalyzeOpts) -> 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_workspace_metadata()?;
let commits = self.fetch_commits().await?;
let analysis = analyze_commits(&commits);
let bump_type = self.determine_bump(&analysis)?;
let current = Self::parse_current_version(&metadata.current_version)?;
let recommended = bump_type.apply_to(¤t);
let confidence = calculate_confidence(&analysis, bump_type);
let reasoning = generate_reasoning(&analysis, bump_type, ¤t, &recommended);
let breaking_changes = build_breaking_changes(analysis.breaking, &metadata.package_name);
let features = build_features(analysis.features, &metadata.package_name);
let fixes = build_fixes(analysis.fixes, &metadata.package_name);
let risk_assessment = if self.opts.risk_analysis {
Some(calculate_risk_assessment(
&breaking_changes,
&features,
&fixes,
))
} else {
None
};
Self::print_response(
metadata,
&recommended,
bump_type,
confidence,
reasoning,
&commits,
&breaking_changes,
&features,
&fixes,
risk_assessment,
start_time,
);
Ok(CommandExitCode::Success)
}
async fn fetch_commits(&self) -> Result<Vec<governor_core::domain::commit::Commit>> {
let git = GitAdapter::open(governor_git::GitAdapterConfig {
repository_path: Some(std::path::PathBuf::from(&self.workspace_path)),
..Default::default()
})
.map_err(|e| crate::error::Error::Config(format!("Failed to open git repository: {e}")))?;
let tag_ref = self.opts.since.as_deref();
git.get_commits_since(tag_ref)
.await
.map_err(|e| crate::error::Error::Git(format!("Failed to get commits: {e}")))
}
fn determine_bump(&self, analysis: &analysis::Analysis) -> Result<BumpType> {
use bump::analyze_bump_type;
if let Some(ref bump) = self.opts.bump {
match bump.as_str() {
"major" => Ok(BumpType::Major),
"minor" => Ok(BumpType::Minor),
"patch" => Ok(BumpType::Patch),
"auto" => Ok(analyze_bump_type(analysis)),
_ => Err(crate::error::Error::InvalidArguments(format!(
"Invalid bump strategy: {bump}"
))),
}
} else {
Ok(analyze_bump_type(analysis))
}
}
fn parse_current_version(version: &str) -> Result<SemanticVersion> {
SemanticVersion::parse(version).map_err(|e| {
crate::error::Error::Version(format!("Failed to parse current version: {e}"))
})
}
fn parse_workspace_metadata(&self) -> Result<WorkspaceMetadata> {
let manifest_path = std::path::PathBuf::from(&self.workspace_path).join("Cargo.toml");
if !manifest_path.exists() {
return Err(crate::error::Error::Config(format!(
"Cargo.toml not found at {}",
manifest_path.display()
)));
}
let content = std::fs::read_to_string(&manifest_path)
.map_err(|e| crate::error::Error::Io(format!("Failed to read Cargo.toml: {e}")))?;
let value: toml::Value = toml::from_str(&content)
.map_err(|e| crate::error::Error::Config(format!("Failed to parse Cargo.toml: {e}")))?;
let package = value
.get("workspace")
.and_then(|w| w.get("package"))
.or_else(|| value.get("package"));
let name = package
.and_then(|p| p.get("name"))
.and_then(|n| n.as_str())
.unwrap_or("unknown")
.to_string();
let version = package
.and_then(|p| p.get("version"))
.and_then(|v| v.as_str())
.unwrap_or("0.0.0")
.to_string();
Ok(WorkspaceMetadata {
package_name: name,
current_version: version,
})
}
fn print_response(
metadata: WorkspaceMetadata,
recommended: &SemanticVersion,
bump_type: BumpType,
confidence: f64,
reasoning: String,
commits: &[governor_core::domain::commit::Commit],
breaking_changes: &[governor_core::domain::version::BreakingChange],
features: &[governor_core::domain::version::Feature],
fixes: &[governor_core::domain::version::Fix],
risk_assessment: Option<risk::RiskAssessment>,
start_time: std::time::Instant,
) {
let response = json!({
"success": true,
"command": "analyze",
"workspace": metadata.package_name,
"result": {
"current_version": metadata.current_version,
"recommended_bump": serde_json::to_value(bump_type).unwrap(),
"new_version": recommended.to_string(),
"confidence": confidence,
"reasoning": reasoning,
"commits_analyzed": commits.len(),
"breaking_changes": breaking_changes,
"features": features,
"fixes": fixes,
"changelog_entries": features.len() + fixes.len(),
"risk_assessment": risk_assessment,
},
"metrics": {
"execution_time_ms": start_time.elapsed().as_millis(),
"git_operations": 2,
"api_calls": 0,
}
});
println!("{}", serde_json::to_string_pretty(&response).unwrap());
}
}
#[derive(Debug, Clone)]
struct WorkspaceMetadata {
package_name: String,
current_version: String,
}