Skip to content

Commit dc59279

Browse files
committed
cargo-rail: added proper TOML formatter for the codebase and cleaned out the manual TOML editing/etc. added a proper backup && undo command.
1 parent 22e8241 commit dc59279

File tree

19 files changed

+1450
-423
lines changed

19 files changed

+1450
-423
lines changed

src/cargo/manifest.rs

Lines changed: 195 additions & 195 deletions
Large diffs are not rendered by default.

src/cargo/mod.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,10 @@
1212
//! - `manifest` - Lossless Cargo.toml transformation
1313
//! - `unify` - Workspace dependency unification engine (replaces cargo-hakari)
1414
//! - `validate` - Optional per-target validation with Rayon parallelism
15-
16-
/// Lossless Cargo.toml transformation
15+
/// Workspace dependency unification engine
1716
pub mod manifest;
1817
/// Comprehensive cargo_metadata wrapper
1918
pub mod metadata;
20-
/// Workspace dependency unification engine
2119
pub mod unify;
2220
/// Per-target validation with parallel execution
2321
pub mod validate;

src/commands/init.rs

Lines changed: 31 additions & 176 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
66
use crate::config::{RailConfig, UnifyConfig, WorkspaceConfig};
77
use crate::error::{RailError, RailResult};
8+
use crate::toml::builder::RailConfigBuilder;
89
use crate::workspace::WorkspaceContext;
910
use std::fs;
1011
use std::io::{self, Write};
@@ -69,8 +70,14 @@ pub fn run_init(
6970
// 4. Build config
7071
let config = build_rail_config(workspace_root.to_path_buf(), unify);
7172

72-
// 6. Serialize with comments
73-
let toml_content = serialize_config_with_comments(&config)?;
73+
// 6. Serialize with comments using Builder
74+
let toml_content = RailConfigBuilder::new()
75+
.header()
76+
.workspace(&config.workspace.root)
77+
.unify(&config.unify)
78+
.release(&config.release)
79+
.splits_template()
80+
.build()?;
7481

7582
// 5. Write or print
7683
if dry_run {
@@ -112,178 +119,10 @@ fn build_rail_config(_workspace_root: PathBuf, unify: UnifyConfig) -> RailConfig
112119
unify,
113120
release: crate::config::ReleaseConfig::default(),
114121
splits: vec![], // Empty by default - use 'cargo rail split init' to configure
122+
formatting: crate::config::FormattingConfig::default(),
115123
}
116124
}
117125

118-
/// Serialize RailConfig to TOML string with minimal, clean comments
119-
fn serialize_config_with_comments(config: &RailConfig) -> RailResult<String> {
120-
let mut output = String::new();
121-
122-
// Header
123-
output.push_str("# cargo-rail configuration\n");
124-
output.push_str("# Generated by: cargo rail init\n");
125-
output.push_str("# Documentation: https://github.com/loadingalias/cargo-rail\n\n");
126-
127-
// Workspace
128-
output.push_str("[workspace]\n");
129-
output.push_str(&format!("root = \"{}\"\n\n", config.workspace.root.display()));
130-
131-
// Dependency Unification
132-
output.push_str("[unify]\n");
133-
output.push_str(&format!("use_all_features = {}\n", config.unify.use_all_features));
134-
output.push_str(&format!("allow_renamed = {}\n", config.unify.allow_renamed));
135-
136-
if !config.unify.exclude.is_empty() {
137-
output.push_str("exclude = [");
138-
for (i, dep) in config.unify.exclude.iter().enumerate() {
139-
if i > 0 {
140-
output.push_str(", ");
141-
}
142-
output.push_str(&format!("\"{}\"", dep));
143-
}
144-
output.push_str("]\n");
145-
} else {
146-
output.push_str("exclude = [] # Dependencies to skip unification\n");
147-
}
148-
149-
if !config.unify.include.is_empty() {
150-
output.push_str("include = [");
151-
for (i, dep) in config.unify.include.iter().enumerate() {
152-
if i > 0 {
153-
output.push_str(", ");
154-
}
155-
output.push_str(&format!("\"{}\"", dep));
156-
}
157-
output.push_str("]\n");
158-
} else {
159-
output.push_str("include = [] # Dependencies to force unification\n");
160-
}
161-
162-
output.push('\n');
163-
164-
// Conflict handling
165-
output.push_str("[unify.conflicts]\n");
166-
output.push_str(&format!(
167-
"auto_resolve = {} # Pick highest version automatically\n",
168-
config.unify.conflicts.auto_resolve
169-
));
170-
output.push_str(&format!(
171-
"resolution_mode = \"{}\" # \"permissive\" or \"strict\"\n",
172-
config.unify.conflicts.resolution_mode
173-
));
174-
output.push_str(&format!(
175-
"add_markers = {} # Add # ⚠️ comments to Cargo.toml\n\n",
176-
config.unify.conflicts.add_markers
177-
));
178-
179-
// Transitive optimization
180-
output.push_str("[unify.transitives]\n");
181-
output.push_str(&format!(
182-
"consolidate_features = {} # Add fragmented transitive deps to workspace\n",
183-
config.unify.transitives.consolidate_features
184-
));
185-
186-
// Serialize host_selection
187-
match &config.unify.transitives.host_selection {
188-
crate::config::TransitiveFeatureHost::Auto => {
189-
output.push_str("host_selection = \"auto\" # \"auto\", \"root\", \"largest\", or [\"crate-name\"]\n\n");
190-
}
191-
crate::config::TransitiveFeatureHost::Root => {
192-
output.push_str("host_selection = \"root\"\n\n");
193-
}
194-
crate::config::TransitiveFeatureHost::Largest => {
195-
output.push_str("host_selection = \"largest\"\n\n");
196-
}
197-
crate::config::TransitiveFeatureHost::Explicit(names) => {
198-
output.push_str("host_selection = [");
199-
for (i, name) in names.iter().enumerate() {
200-
if i > 0 {
201-
output.push_str(", ");
202-
}
203-
output.push_str(&format!("\"{}\"", name));
204-
}
205-
output.push_str("]\n\n");
206-
}
207-
}
208-
209-
// Validation
210-
output.push_str("[unify.validation]\n");
211-
if !config.unify.validation.targets.is_empty() {
212-
output.push_str("targets = [");
213-
for (i, target) in config.unify.validation.targets.iter().enumerate() {
214-
if i > 0 {
215-
output.push_str(", ");
216-
}
217-
output.push_str(&format!("\"{}\"", target));
218-
}
219-
output.push_str("]\n");
220-
} else {
221-
output.push_str("targets = [] # Platform-specific validation (e.g., [\"x86_64-unknown-linux-gnu\"])\n");
222-
}
223-
output.push_str(&format!(
224-
"max_parallel_jobs = {} # 0 = auto-detect\n\n",
225-
config.unify.validation.max_parallel_jobs
226-
));
227-
228-
// Output
229-
output.push_str("[unify.output]\n");
230-
output.push_str(&format!(
231-
"generate_report = {}\n\n",
232-
config.unify.output.generate_report
233-
));
234-
235-
// Release
236-
output.push_str("[release]\n");
237-
output.push_str(&format!("tag_prefix = \"{}\"\n", config.release.tag_prefix));
238-
output.push_str(&format!(
239-
"tag_format = \"{}\" # Variables: {{crate}}, {{version}}\n",
240-
config.release.tag_format
241-
));
242-
output.push_str(&format!("require_clean = {}\n", config.release.require_clean));
243-
output.push_str(&format!("publish_delay = {}\n", config.release.publish_delay));
244-
output.push_str(&format!(
245-
"create_github_release = {}\n",
246-
config.release.create_github_release
247-
));
248-
output.push_str(&format!("sign_tags = {}\n", config.release.sign_tags));
249-
output.push_str(&format!("changelog_path = \"{}\"\n", config.release.changelog_path));
250-
output.push_str(&format!(
251-
"skip_changelog_for = {}\n",
252-
if config.release.skip_changelog_for.is_empty() {
253-
"[]".to_string()
254-
} else {
255-
format!(
256-
"[{}]",
257-
config
258-
.release
259-
.skip_changelog_for
260-
.iter()
261-
.map(|s| format!("\"{}\"", s))
262-
.collect::<Vec<_>>()
263-
.join(", ")
264-
)
265-
}
266-
));
267-
output.push_str(&format!(
268-
"require_changelog_entries = {}\n\n",
269-
config.release.require_changelog_entries
270-
));
271-
272-
// Splits - Don't include in default init
273-
output.push_str("# Split/Sync: Use 'cargo rail split init <crate>' to configure individual crates\n");
274-
output.push_str("# [[splits]]\n");
275-
output.push_str("# name = \"my-crate\"\n");
276-
output.push_str("# remote = \"[email protected]:org/my-crate.git\"\n");
277-
output.push_str("# branch = \"main\"\n");
278-
output.push_str("# mode = \"single\"\n");
279-
output.push_str("# publish = true\n");
280-
output.push_str("#\n");
281-
output.push_str("# [[splits.paths]]\n");
282-
output.push_str("# crate = \"crates/my-crate\"\n");
283-
284-
Ok(output)
285-
}
286-
287126
/// Check if config file already exists at any location
288127
fn check_existing_config(workspace_root: &Path) -> Option<PathBuf> {
289128
crate::config::RailConfig::find_config_path(workspace_root)
@@ -372,10 +211,17 @@ pub fn run_init_standalone(
372211
},
373212
release: crate::config::ReleaseConfig::default(),
374213
splits: vec![],
214+
formatting: crate::config::FormattingConfig::default(),
375215
};
376216

377-
// 5. Serialize with comments
378-
let config_toml = serialize_config_with_comments(&config)?;
217+
// 5. Serialize with comments using Builder
218+
let config_toml = RailConfigBuilder::new()
219+
.header()
220+
.workspace(&config.workspace.root)
221+
.unify(&config.unify)
222+
.release(&config.release)
223+
.splits_template()
224+
.build()?;
379225

380226
// 4. Output
381227
if dry_run {
@@ -403,19 +249,28 @@ pub fn run_init_standalone(
403249
#[cfg(test)]
404250
mod tests {
405251
use super::*;
252+
use crate::config::{FormattingConfig, ReleaseConfig};
406253

407254
#[test]
408-
fn test_serialize_config_with_comments() {
255+
fn test_serialize_config_with_builder() {
409256
let config = RailConfig {
410257
workspace: WorkspaceConfig {
411258
root: PathBuf::from("."),
412259
},
413260
unify: UnifyConfig::default(),
414-
release: crate::config::ReleaseConfig::default(),
261+
release: ReleaseConfig::default(),
415262
splits: vec![],
263+
formatting: FormattingConfig::default(),
416264
};
417265

418-
let toml = serialize_config_with_comments(&config).unwrap();
266+
let toml = RailConfigBuilder::new()
267+
.header()
268+
.workspace(&config.workspace.root)
269+
.unify(&config.unify)
270+
.release(&config.release)
271+
.splits_template()
272+
.build()
273+
.unwrap();
419274

420275
// Should contain section headers
421276
assert!(toml.contains("[workspace]"));

src/commands/split.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ pub fn run_split_init(ctx: &WorkspaceContext, crates: Option<&str>, dry_run: boo
192192
unify: crate::config::UnifyConfig::default(),
193193
release: crate::config::ReleaseConfig::default(),
194194
splits: vec![],
195+
formatting: crate::config::FormattingConfig::default(),
195196
});
196197

197198
// Add new splits (avoid duplicates)

src/commands/test.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ use crate::workspace::{ChangeImpact, WorkspaceContext};
1010
pub struct TestConfig {
1111
/// Git ref to compare against (if None, auto-detect)
1212
pub since: Option<String>,
13+
/// Skip change detection and run all tests
14+
pub full: bool,
1315
/// Explain why tests are being run
1416
pub explain: bool,
1517
/// Prefer cargo-nextest if available
@@ -33,7 +35,18 @@ pub fn run_test(ctx: &WorkspaceContext, config: TestConfig) -> RailResult<()> {
3335
let impact = analyzer.analyze_changes(&base_ref, None)?;
3436

3537
// Get minimal test set
36-
let test_targets = impact.minimal_test_set();
38+
let test_targets = if config.full {
39+
println!("⚡ Full mode active - running all tests");
40+
ctx
41+
.cargo
42+
.metadata()
43+
.list_crates()
44+
.iter()
45+
.map(|p| p.name.to_string())
46+
.collect()
47+
} else {
48+
impact.minimal_test_set()
49+
};
3750

3851
if test_targets.is_empty() {
3952
println!("✓ No affected crates - all tests skipped");

src/commands/unify.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ pub fn run_unify_apply(
444444
}
445445

446446
// Create transformer (use the same metadata we used for analysis)
447-
let transformer = CargoTransform::new(metadata.clone());
447+
let transformer = CargoTransform::new(ctx.cargo.metadata().clone());
448448

449449
// 1. Write [workspace.dependencies]
450450
let workspace_toml = ctx.workspace_root().join("Cargo.toml");

src/commands/watch.rs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,12 @@ fn format_watch_test_command(config: &TestConfig) -> String {
155155
rail_cmd.push_str(&format!(" --since {}", since));
156156
}
157157

158-
if config.prefer_nextest {
159-
rail_cmd.push_str(" --nextest");
158+
if config.full {
159+
rail_cmd.push_str(" --full");
160+
}
161+
162+
if !config.prefer_nextest {
163+
rail_cmd.push_str(" --no-nextest");
160164
}
161165

162166
if !config.test_args.is_empty() {
@@ -207,6 +211,7 @@ mod tests {
207211
fn test_format_watch_test_command_includes_flags_and_args() {
208212
let cfg = TestConfig {
209213
since: Some("main".to_string()),
214+
full: false,
210215
explain: false,
211216
prefer_nextest: true,
212217
test_args: vec!["--nocapture".into(), "some::test".into()],
@@ -215,7 +220,22 @@ mod tests {
215220
let cmd = format_watch_test_command(&cfg);
216221
assert!(cmd.starts_with("rail test"));
217222
assert!(cmd.contains("--since main"));
218-
assert!(cmd.contains("--nextest"));
223+
assert!(!cmd.contains("--no-nextest"));
219224
assert!(cmd.contains("-- --nocapture some::test"));
220225
}
226+
227+
#[test]
228+
fn test_format_watch_test_command_no_nextest() {
229+
let cfg = TestConfig {
230+
since: None,
231+
full: true,
232+
explain: false,
233+
prefer_nextest: false,
234+
test_args: vec![],
235+
};
236+
237+
let cmd = format_watch_test_command(&cfg);
238+
assert!(cmd.contains("--full"));
239+
assert!(cmd.contains("--no-nextest"));
240+
}
221241
}

src/config.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ pub struct RailConfig {
1818
/// Split/sync configurations for crates
1919
#[serde(default)]
2020
pub splits: Vec<SplitConfig>,
21+
/// TOML formatting settings
22+
#[serde(default)]
23+
pub formatting: FormattingConfig,
2124
}
2225

2326
/// Workspace location configuration
@@ -441,6 +444,14 @@ impl Default for ReleaseConfig {
441444
}
442445
}
443446

447+
/// Configuration for TOML formatting
448+
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
449+
pub struct FormattingConfig {
450+
/// Add "Managed by cargo-rail" header (default: false)
451+
#[serde(default)]
452+
pub add_header: bool,
453+
}
454+
444455
fn default_tag_prefix() -> String {
445456
"v".to_string()
446457
}

0 commit comments

Comments
 (0)