Skip to content

Commit ad4703c

Browse files
committed
cargo-rail: fixing the commands and cleaning up for uniformity
1 parent 56ad568 commit ad4703c

File tree

10 files changed

+263
-55
lines changed

10 files changed

+263
-55
lines changed

src/commands/affected.rs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use crate::change_detection::classify::{ChangeKind, classify_file};
99
use crate::config::ChangeDetectionConfig;
1010
use crate::error::{RailError, RailResult};
11+
use crate::git::detect_default_base_ref;
1112
use crate::graph::AffectedAnalysis;
1213
use crate::workspace::WorkspaceContext;
1314
use glob::Pattern;
@@ -48,22 +49,28 @@ impl OutputFormat {
4849
/// Run the affected command
4950
pub fn run_affected(
5051
ctx: &WorkspaceContext,
51-
since: String,
52+
since: Option<String>,
5253
from: Option<String>,
5354
to: Option<String>,
5455
format: String,
5556
all: bool,
56-
output_file: Option<PathBuf>,
57+
output: Option<PathBuf>,
5758
) -> RailResult<()> {
5859
let output_format = OutputFormat::from_str(&format)?;
5960

6061
// If --all flag is set, show all workspace crates regardless of changes
6162
if all {
62-
return display_all_crates(ctx, output_format, output_file.as_ref());
63+
return display_all_crates(ctx, output_format, output.as_ref());
6364
}
6465

66+
// Auto-detect default branch if --since not specified
67+
let since_ref = match since {
68+
Some(s) => s,
69+
None => detect_default_base_ref(ctx.git.git())?,
70+
};
71+
6572
// Get changed files from git
66-
let changed_files = get_changed_files(ctx, &since, from.as_deref(), to.as_deref())?;
73+
let changed_files = get_changed_files(ctx, &since_ref, from.as_deref(), to.as_deref())?;
6774

6875
// Get change-detection config (from rail.toml or defaults)
6976
let cd_config = ctx
@@ -79,7 +86,7 @@ pub fn run_affected(
7986
let analysis = crate::graph::analyze(&ctx.graph, &changed_files)?;
8087

8188
// Output results
82-
display_results(&analysis, &classification, output_format, output_file.as_ref())?;
89+
display_results(&analysis, &classification, output_format, output.as_ref())?;
8390

8491
Ok(())
8592
}

src/commands/clean.rs

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@ use crate::workspace::WorkspaceContext;
55
use std::fs;
66

77
/// Run the clean command
8-
pub fn run_clean(ctx: &WorkspaceContext, cache: bool, backups: bool, reports: bool) -> RailResult<()> {
9-
let mut cleaned_any = false;
10-
8+
pub fn run_clean(ctx: &WorkspaceContext, cache: bool, backups: bool, reports: bool, check: bool) -> RailResult<()> {
119
// If no flags provided, clean everything (cache, reports, and ALL backups)
1210
let clean_all = !cache && !backups && !reports;
1311

@@ -17,6 +15,78 @@ pub fn run_clean(ctx: &WorkspaceContext, cache: bool, backups: bool, reports: bo
1715
let prune_backups = backups && !clean_all;
1816
let delete_all_backups = clean_all;
1917

18+
// Dry-run mode: show what would be cleaned
19+
if check {
20+
println!("🔍 DRY-RUN MODE - Showing what would be cleaned:\n");
21+
let mut would_clean = false;
22+
23+
if clean_cache {
24+
let cache_path = ctx
25+
.workspace_root
26+
.join("target")
27+
.join("cargo-rail")
28+
.join("metadata.json");
29+
let old_cache = ctx.workspace_root.join("target").join("rail");
30+
31+
if cache_path.exists() {
32+
println!(" 📄 {}", cache_path.display());
33+
would_clean = true;
34+
}
35+
if old_cache.exists() {
36+
println!(" 📁 {} (legacy)", old_cache.display());
37+
would_clean = true;
38+
}
39+
}
40+
41+
if clean_reports {
42+
let report_dir = ctx.workspace_root.join("target").join("cargo-rail");
43+
if report_dir.exists() {
44+
for entry in fs::read_dir(&report_dir).ok().into_iter().flatten().flatten() {
45+
let path = entry.path();
46+
if path.is_file() && path.extension().is_some_and(|ext| ext == "md") {
47+
println!(" 📄 {}", path.display());
48+
would_clean = true;
49+
}
50+
}
51+
}
52+
}
53+
54+
if prune_backups || delete_all_backups {
55+
let backup_manager = BackupManager::new(&ctx.workspace_root);
56+
if backup_manager.has_backups() {
57+
let backup_list = backup_manager.list_backups()?;
58+
if delete_all_backups {
59+
for backup in &backup_list {
60+
println!(" 📦 backup: {}", backup.id);
61+
would_clean = true;
62+
}
63+
} else {
64+
let max_backups = ctx
65+
.config
66+
.as_ref()
67+
.map(|c| c.unify.max_backups)
68+
.unwrap_or_else(|| UnifyConfig::default().max_backups);
69+
if backup_list.len() > max_backups {
70+
for backup in backup_list.iter().skip(max_backups) {
71+
println!(" 📦 backup (prune): {}", backup.id);
72+
would_clean = true;
73+
}
74+
}
75+
}
76+
}
77+
}
78+
79+
if would_clean {
80+
println!("\n✋ To execute cleanup, run: cargo rail clean");
81+
} else {
82+
println!("Nothing to clean");
83+
}
84+
return Ok(());
85+
}
86+
87+
// Execute cleanup
88+
let mut cleaned_any = false;
89+
2090
if clean_cache {
2191
clean_metadata_cache(ctx)?;
2292
cleaned_any = true;

src/commands/common.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,51 @@ use crate::split::SplitConfig;
66
use crate::sync::SyncConfig;
77
use crate::workspace::WorkspaceContext;
88
use std::path::PathBuf;
9+
use std::str::FromStr;
10+
11+
/// Standard output format for most commands
12+
///
13+
/// Use this for commands that just need text/json output.
14+
/// For commands with more formats (like `affected`), define a specialized enum.
15+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16+
pub enum OutputFormat {
17+
/// Human-readable text output (default)
18+
#[default]
19+
Text,
20+
/// Machine-readable JSON output
21+
Json,
22+
}
23+
24+
impl OutputFormat {
25+
/// Check if this format is JSON
26+
pub fn is_json(&self) -> bool {
27+
matches!(self, Self::Json)
28+
}
29+
}
30+
31+
impl FromStr for OutputFormat {
32+
type Err = RailError;
33+
34+
fn from_str(s: &str) -> Result<Self, Self::Err> {
35+
match s.to_lowercase().as_str() {
36+
"text" => Ok(Self::Text),
37+
"json" => Ok(Self::Json),
38+
_ => Err(RailError::message(format!(
39+
"Unknown format '{}'. Valid formats: text, json",
40+
s
41+
))),
42+
}
43+
}
44+
}
45+
46+
impl std::fmt::Display for OutputFormat {
47+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48+
match self {
49+
Self::Text => write!(f, "text"),
50+
Self::Json => write!(f, "json"),
51+
}
52+
}
53+
}
954

1055
/// Builder for split/sync configurations
1156
///

src/commands/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ pub mod unify;
3838

3939
pub use affected::run_affected;
4040
pub use clean::run_clean;
41+
pub use common::OutputFormat;
4142
pub use init::{run_init, run_init_standalone};
4243
pub use release::{run_release_check, run_release_init, run_release_plan, run_release_publish};
4344
pub use split::{run_split, run_split_init};

src/commands/release.rs

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//! `cargo rail release` - Release automation commands
22
3+
use crate::commands::common::OutputFormat;
34
use crate::error::{RailError, RailResult};
45
use crate::release::planner::ReleasePlanner;
56
use crate::release::publisher::ReleasePublisher;
@@ -17,8 +18,11 @@ pub fn run_release_plan(
1718
bump: String,
1819
skip_publish: bool,
1920
skip_tag: bool,
20-
json: bool,
21+
format: String,
2122
) -> RailResult<()> {
23+
let output_format: OutputFormat = format.parse()?;
24+
let json = output_format.is_json();
25+
2226
if !json {
2327
eprintln!("🔍 Planning release...\n");
2428
}
@@ -151,8 +155,18 @@ pub fn run_release_publish(
151155
/// Run release check command (CI validation)
152156
///
153157
/// Validates that crates are ready for release without making changes.
154-
pub fn run_release_check(ctx: &WorkspaceContext, crate_names: Option<Vec<String>>, all: bool) -> RailResult<()> {
155-
println!("🔍 Checking release readiness...\n");
158+
pub fn run_release_check(
159+
ctx: &WorkspaceContext,
160+
crate_names: Option<Vec<String>>,
161+
all: bool,
162+
format: String,
163+
) -> RailResult<()> {
164+
let output_format: OutputFormat = format.parse()?;
165+
let json = output_format.is_json();
166+
167+
if !json {
168+
println!("🔍 Checking release readiness...\n");
169+
}
156170

157171
// Get release config
158172
let config = ctx.config.as_ref().map(|c| &c.release);
@@ -179,13 +193,30 @@ pub fn run_release_check(ctx: &WorkspaceContext, crate_names: Option<Vec<String>
179193
let validator = ReleaseValidator::new(ctx);
180194
validator.validate(&target_crates, release_config.require_clean)?;
181195

182-
// Check publishability
196+
// Check publishability and collect results
197+
let mut results = Vec::new();
183198
for crate_name in &target_crates {
184199
validator.validate_publishable(crate_name)?;
185-
println!("✅ {} is ready for release", crate_name);
200+
results.push(crate_name.clone());
201+
if !json {
202+
println!("✅ {} is ready for release", crate_name);
203+
}
204+
}
205+
206+
if json {
207+
let output = serde_json::json!({
208+
"status": "passed",
209+
"crates": results,
210+
"count": results.len()
211+
});
212+
println!(
213+
"{}",
214+
serde_json::to_string_pretty(&output).map_err(|e| RailError::message(format!("JSON error: {}", e)))?
215+
);
216+
} else {
217+
println!("\n✅ All release checks passed!");
186218
}
187219

188-
println!("\n✅ All release checks passed!");
189220
Ok(())
190221
}
191222

src/commands/split.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::io::IsTerminal;
22

3-
use crate::commands::common::SplitSyncConfigBuilder;
3+
use crate::commands::common::{OutputFormat, SplitSyncConfigBuilder};
44
use crate::config::RailConfig;
55
use crate::error::RailResult;
66
use crate::split::SplitEngine;
@@ -18,8 +18,11 @@ pub fn run_split(
1818
all: bool,
1919
remote: Option<String>,
2020
dry_run: bool,
21-
json: bool,
21+
format: String,
2222
) -> RailResult<()> {
23+
let output_format: OutputFormat = format.parse()?;
24+
let json = output_format.is_json();
25+
2326
if !json {
2427
eprintln!("📦 Loaded configuration");
2528
}

src/commands/sync.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::io::IsTerminal;
22
use std::str::FromStr;
33

4-
use crate::commands::common::SplitSyncConfigBuilder;
4+
use crate::commands::common::{OutputFormat, SplitSyncConfigBuilder};
55
use crate::error::RailResult;
66
use crate::sync::{ConflictStrategy, SyncDirection, SyncEngine};
77
use crate::utils;
@@ -23,8 +23,11 @@ pub fn run_sync(
2323
strategy_str: String,
2424
_no_protected_branches: bool,
2525
dry_run: bool,
26-
json: bool,
26+
format: String,
2727
) -> RailResult<()> {
28+
let output_format: OutputFormat = format.parse()?;
29+
let json = output_format.is_json();
30+
2831
// Parse conflict strategy
2932
let conflict_strategy = ConflictStrategy::from_str(&strategy_str)?;
3033

src/commands/unify.rs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
//! 3. Clean unification with intersection-based features
77
88
use crate::cargo::{ManifestWriter, UnifyAnalyzer, UnifyReport};
9-
use crate::error::RailResult;
9+
use crate::commands::common::OutputFormat;
10+
use crate::error::{RailError, RailResult};
1011
use crate::workspace::WorkspaceContext;
1112

1213
/// Run dependency unification analysis (dry-run)
@@ -17,8 +18,14 @@ pub fn run_unify_analyze(
1718
pin_transitives_flag: bool,
1819
include_renamed_flag: bool,
1920
show_diff: bool,
21+
format: String,
2022
) -> RailResult<()> {
21-
eprintln!("🔍 Analyzing workspace dependencies...\n");
23+
let output_format: OutputFormat = format.parse()?;
24+
let json = output_format.is_json();
25+
26+
if !json {
27+
eprintln!("🔍 Analyzing workspace dependencies...\n");
28+
}
2229

2330
// Create analyzer
2431
let mut analyzer = UnifyAnalyzer::new(ctx)?;
@@ -40,6 +47,31 @@ pub fn run_unify_analyze(
4047
// Run analysis
4148
let plan = analyzer.analyze()?;
4249

50+
// JSON output mode
51+
if json {
52+
let output = serde_json::json!({
53+
"workspace_deps": plan.workspace_deps.iter().map(|d| serde_json::json!({
54+
"name": d.name,
55+
"version": d.version_req,
56+
"features": d.features,
57+
})).collect::<Vec<_>>(),
58+
"member_edits_count": plan.member_edits.values().map(|v| v.len()).sum::<usize>(),
59+
"members_affected": plan.member_edits.len(),
60+
"transitive_pins": plan.transitive_pins.len(),
61+
"has_blocking_issues": plan.has_blocking_issues(),
62+
"issues": plan.issues.iter().map(|i| serde_json::json!({
63+
"dep_name": i.dep_name,
64+
"severity": format!("{:?}", i.severity),
65+
"message": i.message,
66+
})).collect::<Vec<_>>(),
67+
});
68+
println!(
69+
"{}",
70+
serde_json::to_string_pretty(&output).map_err(|e| RailError::message(format!("JSON error: {}", e)))?
71+
);
72+
return Ok(());
73+
}
74+
4375
// Display summary
4476
println!("{}", plan.summary());
4577

0 commit comments

Comments
 (0)