Skip to content

Commit c489c01

Browse files
committed
cargo-rail: added changelog via winnow instead of 'git-cliff-core' and refactored split/sync for the ExecutionEngine. working on release workflow
1 parent 69c2bdd commit c489c01

File tree

14 files changed

+1664
-231
lines changed

14 files changed

+1664
-231
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ serde = { version = "1.0.228", features = ["derive"] }
3434
serde_json = "1.0.145"
3535
semver = "1.0.27"
3636

37+
# Parsing for conventional commits (zero-copy, deterministic)
38+
winnow = "0.7.13"
39+
3740
# Cryptographic hashing for plan IDs
3841
sha2 = "0.10.9"
3942

src/commands/release.rs

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
//! 2. Changelogs per-thing
66
//! 3. Every release has name + version + last_sha
77
8+
use crate::core::config::ReleaseConfig;
89
use crate::core::error::{RailError, RailResult};
10+
use crate::quality::changelog::{Changelog, ChangelogFormat, ConventionalCommit};
911
use crate::release::{ReleasePlan, ReleaseTracker, VersionBump};
1012
use std::env;
1113

@@ -126,15 +128,21 @@ pub fn run_release_apply(
126128
update_crate_version(&workspace_root, &release.crate_path, &plan.proposed_version.to_string())?;
127129
println!(" Updated Cargo.toml version");
128130

129-
// 2. Get current HEAD SHA
131+
// 2. Generate and update changelog (if configured)
132+
if let Some(changelog_path) = &release.changelog {
133+
generate_changelog(&workspace_root, &release, &plan, changelog_path)?;
134+
println!(" Updated {}", changelog_path.display());
135+
}
136+
137+
// 3. Get current HEAD SHA
130138
let head_sha = get_head_sha(&workspace_root)?;
131139

132-
// 3. Update rail.toml metadata
140+
// 4. Update rail.toml metadata
133141
tracker.update_release(&name, &plan.proposed_version.to_string(), &head_sha)?;
134142
tracker.save()?;
135143
println!(" Updated rail.toml metadata");
136144

137-
// 4. Create git tag
145+
// 5. Create git tag
138146
create_git_tag(&workspace_root, &name, &plan.proposed_version.to_string())?;
139147
println!(" Created tag: {}-v{}", name, plan.proposed_version);
140148

@@ -215,6 +223,63 @@ fn create_git_tag(workspace_root: &std::path::Path, name: &str, version: &str) -
215223
Ok(())
216224
}
217225

226+
/// Generate and update changelog
227+
fn generate_changelog(
228+
workspace_root: &std::path::Path,
229+
release: &ReleaseConfig,
230+
plan: &ReleasePlan,
231+
changelog_path: &std::path::Path,
232+
) -> RailResult<()> {
233+
// Create changelog from commits
234+
let current_date = chrono::Utc::now().format("%Y-%m-%d").to_string();
235+
let mut changelog = Changelog::new(plan.proposed_version.to_string(), current_date);
236+
237+
// Parse commits and add to changelog
238+
for commit in &plan.commits {
239+
if let Some(parsed) = ConventionalCommit::parse(&commit.message) {
240+
changelog.add_commit(parsed, commit.sha.clone());
241+
}
242+
}
243+
244+
// Generate markdown
245+
let changelog_entry = changelog
246+
.render(ChangelogFormat::Markdown)
247+
.map_err(RailError::message)?;
248+
249+
// Read existing changelog or create new
250+
let changelog_abs_path = workspace_root.join(changelog_path);
251+
let existing_content = if changelog_abs_path.exists() {
252+
std::fs::read_to_string(&changelog_abs_path)
253+
.map_err(|e| RailError::message(format!("Failed to read changelog: {}", e)))?
254+
} else {
255+
// Create header for new changelog
256+
format!(
257+
"# Changelog\n\nAll notable changes to {} will be documented in this file.\n\n",
258+
release.name
259+
)
260+
};
261+
262+
// Prepend new entry to existing content
263+
let new_content = if existing_content.contains("# Changelog") {
264+
// Find where to insert (after header)
265+
if let Some(header_end) = existing_content.find("\n\n") {
266+
let (header, rest) = existing_content.split_at(header_end + 2);
267+
format!("{}{}{}", header, changelog_entry, rest)
268+
} else {
269+
format!("{}\n\n{}", existing_content, changelog_entry)
270+
}
271+
} else {
272+
// No header, just prepend
273+
format!("{}{}", changelog_entry, existing_content)
274+
};
275+
276+
// Write updated changelog
277+
std::fs::write(&changelog_abs_path, new_content)
278+
.map_err(|e| RailError::message(format!("Failed to write changelog: {}", e)))?;
279+
280+
Ok(())
281+
}
282+
218283
fn print_release_plans(plans: &[ReleasePlan]) {
219284
if plans.is_empty() {
220285
return;

src/commands/split.rs

Lines changed: 22 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ use std::io::{self, Write};
33

44
use crate::commands::doctor;
55
use crate::core::config::RailConfig;
6+
use crate::core::context::WorkspaceContext;
67
use crate::core::error::{ConfigError, RailError, RailResult};
8+
use crate::core::executor::PlanExecutor;
79
use crate::core::plan::{Operation, OperationType, Plan};
8-
use crate::core::split::{SplitConfig, Splitter};
910
use crate::ui::progress::{FileProgress, MultiProgress};
1011
use crate::utils;
1112
use rayon::prelude::*;
@@ -119,9 +120,6 @@ pub fn run_split(
119120
}
120121
}
121122

122-
// Create splitter with security config
123-
let splitter = Splitter::new(config.workspace.root.clone(), config.security.clone())?;
124-
125123
// Build plans using the unified Plan system
126124
let mut plans = Vec::new();
127125

@@ -141,33 +139,17 @@ pub fn run_split(
141139
current_dir.join("..").join(remote_name)
142140
};
143141

144-
// Build unified Plan
142+
// Build unified Plan with ExecuteSplit operation
145143
let mut plan = Plan::new(OperationType::Split, Some(split_config.name.clone()));
146144

147-
// Add operations
148-
plan.add_operation(Operation::InitRepo {
149-
path: target_repo_path.display().to_string(),
150-
});
151-
152-
plan.add_operation(Operation::CreateCommit {
153-
message: "Walking commit history for crate paths".to_string(),
154-
files: crate_paths.iter().map(|p| p.display().to_string()).collect(),
155-
});
156-
157-
plan.add_operation(Operation::Transform {
158-
path: target_repo_path.join("Cargo.toml").display().to_string(),
159-
transform_type: "workspace_to_concrete".to_string(),
160-
});
161-
162-
plan.add_operation(Operation::Copy {
163-
from: "auxiliary_files".to_string(),
164-
to: target_repo_path.display().to_string(),
165-
});
166-
167-
plan.add_operation(Operation::Push {
168-
remote: split_config.remote.clone(),
145+
// Add high-level ExecuteSplit operation
146+
plan.add_operation(Operation::ExecuteSplit {
147+
crate_name: split_config.name.clone(),
148+
crate_paths: crate_paths.iter().map(|p| p.display().to_string()).collect(),
149+
mode: format!("{:?}", split_config.mode),
150+
target_repo_path: target_repo_path.display().to_string(),
169151
branch: split_config.branch.clone(),
170-
force: false,
152+
remote_url: Some(split_config.remote.clone()),
171153
});
172154

173155
// Add metadata
@@ -233,6 +215,10 @@ pub fn run_split(
233215
println!("\n🚀 APPLY MODE - Executing split operations\n");
234216
}
235217

218+
// Build workspace context for execution
219+
let workspace_context = WorkspaceContext::build(&current_dir)?;
220+
let executor = PlanExecutor::new(&workspace_context);
221+
236222
let plan_count = plans.len();
237223

238224
// Use parallel processing for multiple crates
@@ -245,22 +231,15 @@ pub fn run_split(
245231
.map(|(split_config, _, _, _)| multi_progress.add_bar(1, format!("Splitting {}", split_config.name)))
246232
.collect();
247233

234+
// For parallel execution, we need to build contexts per-thread
248235
let results: Vec<RailResult<()>> = plans
249236
.into_par_iter()
250237
.enumerate()
251-
.map(|(idx, (split_config, crate_paths, target_repo_path, _))| {
252-
let split_cfg = SplitConfig {
253-
crate_name: split_config.name.clone(),
254-
crate_paths,
255-
mode: split_config.mode.clone(),
256-
target_repo_path,
257-
branch: split_config.branch.clone(),
258-
remote_url: Some(split_config.remote.clone()),
259-
};
260-
261-
// Create a new splitter for this thread
262-
let thread_splitter = Splitter::new(config.workspace.root.clone(), config.security.clone())?;
263-
let result = thread_splitter.split(&split_cfg);
238+
.map(|(idx, (_, _, _, plan))| {
239+
// Build workspace context for this thread
240+
let thread_context = WorkspaceContext::build(&current_dir)?;
241+
let thread_executor = PlanExecutor::new(&thread_context);
242+
let result = thread_executor.execute(&plan);
264243

265244
multi_progress.inc(&bars[idx]);
266245
result
@@ -282,21 +261,12 @@ pub fn run_split(
282261
None
283262
};
284263

285-
for (split_config, crate_paths, target_repo_path, _) in plans {
264+
for (split_config, _, _, plan) in plans {
286265
if crate_progress.is_none() {
287266
println!("🔨 Splitting crate '{}'...", split_config.name);
288267
}
289268

290-
let split_cfg = SplitConfig {
291-
crate_name: split_config.name.clone(),
292-
crate_paths,
293-
mode: split_config.mode.clone(),
294-
target_repo_path,
295-
branch: split_config.branch.clone(),
296-
remote_url: Some(split_config.remote.clone()),
297-
};
298-
299-
splitter.split(&split_cfg)?;
269+
executor.execute(&plan)?;
300270

301271
if let Some(ref mut p) = crate_progress {
302272
p.inc();

0 commit comments

Comments
 (0)