Skip to main content

commit_wizard/core/usecases/version/
bump.rs

1use crate::{
2    core::{Context, CoreResult},
3    engine::{
4        capabilities::versioning,
5        constants::emoji::{ARROW, SUCCESS},
6        error::ErrorCode,
7        models::policy::{
8            commit::CommitModel,
9            enforcement::BumpLevel,
10            versioning::{Version, VersioningModel},
11        },
12    },
13};
14use std::time::Instant;
15
16pub fn run(ctx: &Context, from: Option<String>, to: String, tail: Option<u32>) -> CoreResult<()> {
17    let ui = ctx.ui();
18    let start = Instant::now();
19
20    // Get config and build policy
21    let resolved_config = ctx.config().ok_or_else(|| {
22        ErrorCode::ConfigUnreadable
23            .error()
24            .with_context("context", "Config not resolved")
25    })?;
26
27    let versioning_model = VersioningModel::from_config(resolved_config);
28    let commit_model = CommitModel::from_config(resolved_config);
29
30    // Get git adapter
31    let git = ctx.git();
32
33    // Fetch last tag (this is our current version)
34    let last_tag = git.latest_tag().map_err(|_| {
35        ErrorCode::GitCommandFailed
36            .error()
37            .with_context("operation", "get latest tag")
38    })?;
39
40    // Parse current version from tag
41    let current_version = last_tag
42        .as_ref()
43        .and_then(|tag| Version::parse(tag, &versioning_model.tag_prefix));
44
45    // Determine commit range
46    let from_ref = from.clone().or_else(|| last_tag.clone());
47
48    // Fetch commits
49    let commits = git.list_commits(from_ref.as_deref(), &to).map_err(|_| {
50        ErrorCode::GitCommandFailed
51            .error()
52            .with_context("operation", "list commits")
53    })?;
54
55    // Apply tail filter if specified
56    let commits = if let Some(n) = tail {
57        if n == 0 {
58            commits
59        } else {
60            let n = n as usize;
61            if commits.len() > n {
62                commits[commits.len() - n..].to_vec()
63            } else {
64                commits
65            }
66        }
67    } else {
68        commits
69    };
70
71    // Classify commits by bump level
72    let classified = versioning::classify_commits(&commits, &commit_model);
73
74    // Calculate next version
75    let next_version = versioning::calculate_next_version(current_version.clone(), &classified);
76
77    // Log summary with colored output
78    let current_str = current_version
79        .as_ref()
80        .map(|v| v.to_semver())
81        .unwrap_or_else(|| "initial".to_string());
82    let next_str = next_version.to_semver();
83    ui.logger().ok(&format!(
84        "{} Version bump: {} {} {}",
85        SUCCESS, current_str, ARROW, next_str
86    ));
87
88    // Build output metadata
89    let duration_ms = start.elapsed().as_millis() as u64;
90    let meta = ui
91        .new_output_meta()
92        .with_duration_ms(duration_ms)
93        .with_timestamp(chrono::Utc::now().to_string())
94        .with_command("bump".to_string())
95        .with_dry_run(ctx.dry_run());
96
97    // Build output content
98    let mut content = ui
99        .new_output_content()
100        .title("Version Bump Analysis")
101        .subtitle("Semantic version calculation from commits")
102        .data("commits_analyzed", commits.len().to_string())
103        .data("next_version", next_version.to_semver())
104        .data(
105            "next_tag",
106            next_version.format(&versioning_model.tag_prefix),
107        );
108
109    if let Some(tag) = &last_tag {
110        content = content.data(
111            "current_tag",
112            current_version
113                .as_ref()
114                .map(|v| v.format(&versioning_model.tag_prefix))
115                .unwrap_or_else(|| tag.clone()),
116        );
117        if let Some(version) = current_version.as_ref() {
118            content = content.data("current_version", version.to_semver());
119        }
120    } else {
121        content = content.data("current_version", "initial");
122    }
123
124    if let Some(from) = &from {
125        content = content.data("from", from.clone());
126    }
127    content = content.data("to", to.clone());
128    if let Some(tail) = tail {
129        content = content.data("tail", tail.to_string());
130    }
131
132    // Build commit classifications by bump level
133    let mut major_commits: Vec<String> = Vec::new();
134    let mut minor_commits: Vec<String> = Vec::new();
135    let mut patch_commits: Vec<String> = Vec::new();
136    let mut none_commits: Vec<String> = Vec::new();
137
138    for (commit, bump) in &classified {
139        let line = format!(
140            "{} {}",
141            &commit.hash[..8.min(commit.hash.len())],
142            &commit.summary
143        );
144        match bump {
145            BumpLevel::Major => major_commits.push(line),
146            BumpLevel::Minor => minor_commits.push(line),
147            BumpLevel::Patch => patch_commits.push(line),
148            BumpLevel::None => none_commits.push(line),
149        }
150    }
151
152    if !major_commits.is_empty() {
153        content = content.section(
154            "Major Changes (Breaking)",
155            major_commits.join("\n"),
156            "sh".to_string(),
157        );
158    }
159    if !minor_commits.is_empty() {
160        content = content.section(
161            "Minor Changes (Features)",
162            minor_commits.join("\n"),
163            "sh".to_string(),
164        );
165    }
166    if !patch_commits.is_empty() {
167        content = content.section(
168            "Patch Changes (Fixes)",
169            patch_commits.join("\n"),
170            "sh".to_string(),
171        );
172    }
173    if !none_commits.is_empty() {
174        content = content.section(
175            "Unclassified Commits",
176            none_commits.join("\n"),
177            "sh".to_string(),
178        );
179    }
180
181    // Plain output: next version for machine consumption
182    let plain = next_version.to_semver();
183    content = content.plain(plain);
184
185    let success = !classified.is_empty() || current_version.is_some();
186    ui.print_with_meta(&content, Some(&meta), success)
187}