Skip to content

Commit 6c4325a

Browse files
committed
cargo-rail: added metadata cache via FNV-1a instead of adding sha2/hex deps. extensive testing updates and fixes across the codebase for those end-to-end test bugs.
1 parent 23cb6d3 commit 6c4325a

File tree

14 files changed

+675
-31
lines changed

14 files changed

+675
-31
lines changed

src/cargo/metadata.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::error::RailResult;
22
use cargo_metadata::{Dependency, DependencyKind, MetadataCommand, Package, Resolve, Target, TargetKind};
33
use semver::Version;
4+
use serde::{Deserialize, Serialize};
45
use std::collections::{BTreeMap, HashSet};
56
use std::path::Path;
67

@@ -11,7 +12,7 @@ use std::path::Path;
1112
/// - Resolved dependency graph with actual enabled features
1213
/// - MSRV and edition information
1314
/// - Platform-specific and optional dependencies
14-
#[derive(Clone)]
15+
#[derive(Clone, Serialize, Deserialize)]
1516
pub struct WorkspaceMetadata {
1617
metadata: cargo_metadata::Metadata,
1718
}

src/commands/affected.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,10 +73,10 @@ fn get_changed_files(
7373
// Determine git range
7474
let changes = if let (Some(from_ref), Some(to_ref)) = (from, to) {
7575
// SHA pair mode: from..to
76-
ctx.git.git().get_changed_files_between(from_ref, to_ref)?
76+
ctx.git.git().get_changed_files_between(from_ref, Some(to_ref))?
7777
} else {
78-
// Single ref mode: since..HEAD
79-
ctx.git.git().get_changed_files_between(since, "HEAD")?
78+
// Single ref mode: since..working tree
79+
ctx.git.git().get_changed_files_between(since, None)?
8080
};
8181

8282
// Extract just the file paths (ignore status char)

src/commands/config_sync.rs

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,19 @@ impl ConfigSyncer for ToolchainSyncer {
139139
fn check(&self, workspace_root: &Path, config: &crate::config::RailConfig) -> RailResult<SyncStatus> {
140140
let toolchain_path = workspace_root.join("rust-toolchain.toml");
141141

142+
// If not managed by rail, skip sync
143+
if !config.toolchain.managed_by_rail {
144+
if toolchain_path.exists() {
145+
return Ok(SyncStatus::InSync(
146+
"rust-toolchain.toml exists but not managed by rail (managed_by_rail = false)".to_string(),
147+
));
148+
} else {
149+
return Ok(SyncStatus::OutOfSync(
150+
"rust-toolchain.toml missing (set managed_by_rail = true to enable sync)".to_string(),
151+
));
152+
}
153+
}
154+
142155
// If rust-toolchain.toml doesn't exist, it's out of sync
143156
if !toolchain_path.exists() {
144157
return Ok(SyncStatus::OutOfSync(
@@ -166,6 +179,19 @@ impl ConfigSyncer for ToolchainSyncer {
166179
fn sync(&self, workspace_root: &Path, config: &crate::config::RailConfig) -> RailResult<SyncStatus> {
167180
let toolchain_path = workspace_root.join("rust-toolchain.toml");
168181

182+
// If not managed by rail, skip sync
183+
if !config.toolchain.managed_by_rail {
184+
if toolchain_path.exists() {
185+
return Ok(SyncStatus::InSync(
186+
"rust-toolchain.toml not managed by rail (skipping sync)".to_string(),
187+
));
188+
} else {
189+
return Ok(SyncStatus::InSync(
190+
"rust-toolchain.toml sync disabled (set managed_by_rail = true to enable)".to_string(),
191+
));
192+
}
193+
}
194+
169195
// Check if already in sync
170196
let check_status = self.check(workspace_root, config)?;
171197
if matches!(check_status, SyncStatus::InSync(_)) {
@@ -183,6 +209,62 @@ impl ConfigSyncer for ToolchainSyncer {
183209
}
184210
}
185211

212+
/// Parse existing rust-toolchain.toml and import into ToolchainConfig
213+
///
214+
/// Returns None if file doesn't exist or can't be parsed
215+
pub fn import_rust_toolchain_toml(workspace_root: &Path) -> Option<crate::config::ToolchainConfig> {
216+
let toolchain_path = workspace_root.join("rust-toolchain.toml");
217+
218+
if !toolchain_path.exists() {
219+
return None;
220+
}
221+
222+
let content = std::fs::read_to_string(&toolchain_path).ok()?;
223+
224+
// Parse TOML using toml_edit which is already a dependency
225+
let parsed: toml_edit::DocumentMut = content.parse().ok()?;
226+
let toolchain_table = parsed.get("toolchain")?.as_table()?;
227+
228+
// Extract fields
229+
let channel = toolchain_table
230+
.get("channel")
231+
.and_then(|v| v.as_str())
232+
.unwrap_or("stable")
233+
.to_string();
234+
235+
let path = toolchain_table
236+
.get("path")
237+
.and_then(|v| v.as_str())
238+
.map(|s| s.to_string());
239+
240+
let profile = toolchain_table
241+
.get("profile")
242+
.and_then(|v| v.as_str())
243+
.unwrap_or("default")
244+
.to_string();
245+
246+
let components = toolchain_table
247+
.get("components")
248+
.and_then(|v| v.as_array())
249+
.map(|arr| arr.iter().filter_map(|v| v.as_str()).map(|s| s.to_string()).collect())
250+
.unwrap_or_default();
251+
252+
let targets = toolchain_table
253+
.get("targets")
254+
.and_then(|v| v.as_array())
255+
.map(|arr| arr.iter().filter_map(|v| v.as_str()).map(|s| s.to_string()).collect())
256+
.unwrap_or_default();
257+
258+
Some(crate::config::ToolchainConfig {
259+
channel,
260+
path,
261+
profile,
262+
components,
263+
targets,
264+
managed_by_rail: true, // Set to true when importing
265+
})
266+
}
267+
186268
/// Generate rust-toolchain.toml content from ToolchainConfig
187269
///
188270
/// Supports all rust-toolchain.toml fields:
@@ -245,6 +327,7 @@ mod tests {
245327
profile: "default".to_string(),
246328
components: vec![],
247329
targets: vec![],
330+
managed_by_rail: false,
248331
};
249332

250333
let content = generate_rust_toolchain_toml(&config);
@@ -266,6 +349,7 @@ mod tests {
266349
"x86_64-unknown-linux-gnu".to_string(),
267350
"aarch64-apple-darwin".to_string(),
268351
],
352+
managed_by_rail: false,
269353
};
270354

271355
let content = generate_rust_toolchain_toml(&config);
@@ -285,6 +369,7 @@ mod tests {
285369
profile: "default".to_string(), // Ignored in path mode
286370
components: vec!["clippy".to_string()], // Ignored in path mode
287371
targets: vec!["x86_64-unknown-linux-gnu".to_string()], // Ignored in path mode
372+
managed_by_rail: false,
288373
};
289374

290375
let content = generate_rust_toolchain_toml(&config);

src/commands/init.rs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -199,13 +199,19 @@ fn detect_workspace_patterns(ctx: &WorkspaceContext) -> WorkspacePatternInfo {
199199
}
200200

201201
/// Detect toolchain configuration from existing rust-toolchain.toml
202+
///
203+
/// If rust-toolchain.toml exists, imports it and sets managed_by_rail = true.
204+
/// This enables bidirectional sync: rail.toml becomes the source of truth,
205+
/// and future syncs will update rust-toolchain.toml from rail.toml.
202206
fn detect_toolchain_config(workspace_root: &Path) -> RailResult<ToolchainConfig> {
203207
let toolchain_path = workspace_root.join("rust-toolchain.toml");
204208

205209
if !toolchain_path.exists() {
206210
// Try rust-toolchain without .toml extension
207211
let alt_path = workspace_root.join("rust-toolchain");
208212
if !alt_path.exists() {
213+
// No existing toolchain file - use default and don't enable management yet
214+
// User can enable managed_by_rail = true manually if they want rail to create it
209215
return Ok(ToolchainConfig::default());
210216
}
211217

@@ -216,6 +222,7 @@ fn detect_toolchain_config(workspace_root: &Path) -> RailResult<ToolchainConfig>
216222

217223
return Ok(ToolchainConfig {
218224
channel,
225+
managed_by_rail: true, // Imported from existing file
219226
..ToolchainConfig::default()
220227
});
221228
}
@@ -251,6 +258,7 @@ fn detect_toolchain_config(workspace_root: &Path) -> RailResult<ToolchainConfig>
251258
profile: parsed.toolchain.profile.unwrap_or_else(|| "default".to_string()),
252259
components: parsed.toolchain.components.unwrap_or_default(),
253260
targets: parsed.toolchain.targets.unwrap_or_default(),
261+
managed_by_rail: true, // Imported from existing file - rail now manages it
254262
})
255263
}
256264

@@ -397,11 +405,12 @@ fn serialize_config_with_comments(config: &RailConfig) -> RailResult<String> {
397405
output.push_str("# rust-toolchain.toml automatically.\n");
398406
output.push_str("#\n");
399407
output.push_str("# Fields:\n");
400-
output.push_str("# channel - Rust release channel (stable, beta, nightly, or version)\n");
401-
output.push_str("# path - Path to custom toolchain (mutually exclusive with channel)\n");
402-
output.push_str("# profile - Toolchain profile (minimal, default, complete)\n");
403-
output.push_str("# components - Additional components (clippy, rustfmt, rust-src, etc.)\n");
404-
output.push_str("# targets - Cross-compilation targets\n\n");
408+
output.push_str("# channel - Rust release channel (stable, beta, nightly, or version)\n");
409+
output.push_str("# path - Path to custom toolchain (mutually exclusive with channel)\n");
410+
output.push_str("# profile - Toolchain profile (minimal, default, complete)\n");
411+
output.push_str("# components - Additional components (clippy, rustfmt, rust-src, etc.)\n");
412+
output.push_str("# targets - Cross-compilation targets\n");
413+
output.push_str("# managed_by_rail - Enable rust-toolchain.toml sync (true if imported)\n\n");
405414

406415
output.push_str("[toolchain]\n");
407416
if let Some(ref path) = config.toolchain.path {
@@ -443,6 +452,13 @@ fn serialize_config_with_comments(config: &RailConfig) -> RailResult<String> {
443452
output.push_str("targets = [] # e.g., [\"x86_64-unknown-linux-gnu\", \"aarch64-apple-darwin\"]\n");
444453
}
445454

455+
// Add managed_by_rail field
456+
if config.toolchain.managed_by_rail {
457+
output.push_str("managed_by_rail = true # rust-toolchain.toml imported - rail now manages it\n");
458+
} else {
459+
output.push_str("managed_by_rail = false # Set to true to enable rust-toolchain.toml sync\n");
460+
}
461+
446462
output.push('\n');
447463

448464
// Dependency Unification

src/commands/unify.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -501,17 +501,18 @@ pub fn run_unify_check(
501501

502502
// Run optional per-target validation if CLI flag is set
503503
if validate_targets_flag {
504-
// Get targets from config
504+
// Get targets from config - use [unify].validate_targets which is the source of truth
505505
let targets = if let Some(cfg) = ctx.config.as_ref() {
506-
if cfg.toolchain.targets.is_empty() {
507-
println!("\n⚠️ --validate-targets flag set but no targets configured in rail.toml [toolchain.targets]");
508-
println!("Add targets to .config/rail.toml to enable validation.");
506+
if cfg.unify.validate_targets.is_empty() {
507+
println!("\n⚠️ --validate-targets flag set but no targets configured in rail.toml [unify.validate_targets]");
508+
println!("Add targets to .config/rail.toml under [unify] section to enable validation.");
509+
println!("Example: validate_targets = [\"x86_64-unknown-linux-gnu\", \"wasm32-unknown-unknown\"]");
509510
return Ok(());
510511
}
511-
cfg.toolchain.targets.clone()
512+
cfg.unify.validate_targets.clone()
512513
} else {
513514
println!("\n⚠️ --validate-targets flag set but no rail.toml found");
514-
println!("Run 'cargo rail init' to create a configuration file with [toolchain.targets].");
515+
println!("Run 'cargo rail init' to create a configuration file with [unify.validate_targets].");
515516
return Ok(());
516517
};
517518

src/config.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,10 +188,21 @@ pub struct ToolchainConfig {
188188
pub components: Vec<String>,
189189

190190
/// Target triples for cross-compilation
191-
/// Used by unify for optional validation and by config sync for rust-toolchain.toml
191+
/// Used by config sync for rust-toolchain.toml
192192
/// The host platform is automatically included. No effect if `path` is set.
193193
#[serde(default)]
194194
pub targets: Vec<String>,
195+
196+
/// Whether rust-toolchain.toml is managed by rail
197+
///
198+
/// When true, cargo-rail will sync rail.toml `[toolchain]` → rust-toolchain.toml
199+
/// When false, rust-toolchain.toml is assumed to be managed externally
200+
///
201+
/// Set automatically during `cargo rail init` based on whether rust-toolchain.toml exists:
202+
/// - If exists: imports values and sets to true
203+
/// - If doesn't exist: creates it and sets to true
204+
#[serde(default)]
205+
pub managed_by_rail: bool,
195206
}
196207

197208
fn default_channel() -> String {
@@ -210,6 +221,7 @@ impl Default for ToolchainConfig {
210221
profile: default_profile(),
211222
components: vec![],
212223
targets: vec![],
224+
managed_by_rail: false, // Default to false for safety
213225
}
214226
}
215227
}
@@ -702,6 +714,7 @@ mod tests {
702714
profile: "minimal".to_string(),
703715
components: vec!["clippy".to_string(), "rustfmt".to_string()],
704716
targets: vec!["x86_64-unknown-linux-gnu".to_string()],
717+
managed_by_rail: false,
705718
};
706719
assert!(toolchain.validate().is_ok());
707720
}
@@ -714,6 +727,7 @@ mod tests {
714727
profile: "default".to_string(),
715728
components: vec![],
716729
targets: vec![],
730+
managed_by_rail: false,
717731
};
718732
assert!(toolchain.validate().is_ok());
719733
}
@@ -726,6 +740,7 @@ mod tests {
726740
profile: "default".to_string(),
727741
components: vec![],
728742
targets: vec![],
743+
managed_by_rail: false,
729744
};
730745
assert!(toolchain.validate().is_err());
731746
}
@@ -738,6 +753,7 @@ mod tests {
738753
profile: "invalid".to_string(),
739754
components: vec![],
740755
targets: vec![],
756+
managed_by_rail: false,
741757
};
742758
assert!(toolchain.validate().is_err());
743759
}
@@ -750,6 +766,7 @@ mod tests {
750766
profile: "default".to_string(),
751767
components: vec![],
752768
targets: vec![],
769+
managed_by_rail: false,
753770
};
754771
assert!(toolchain.validate().is_err());
755772
}

src/git/ops.rs

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -114,17 +114,24 @@ impl SystemGit {
114114
/// # Performance
115115
/// Uses `git diff --name-status` which is optimized for listing changes.
116116
/// Typically <100ms even for large diffs with 1000s of files.
117-
pub fn get_changed_files_between(&self, base_ref: &str, head_ref: &str) -> RailResult<Vec<(PathBuf, char)>> {
118-
let output = self
119-
.git_cmd()
120-
.args(["diff", "--name-status", base_ref, head_ref])
121-
.output()
122-
.context("Failed to get changed files between refs")?;
117+
pub fn get_changed_files_between(&self, base_ref: &str, head_ref: Option<&str>) -> RailResult<Vec<(PathBuf, char)>> {
118+
let mut cmd = self.git_cmd();
119+
cmd.args(["diff", "--name-status", base_ref]);
120+
121+
if let Some(head) = head_ref {
122+
cmd.arg(head);
123+
}
124+
125+
let output = cmd.output().context("Failed to get changed files between refs")?;
123126

124127
if !output.status.success() {
125128
let stderr = String::from_utf8_lossy(&output.stderr);
126129
return Err(RailError::Git(GitError::CommandFailed {
127-
command: format!("git diff --name-status {} {}", base_ref, head_ref),
130+
command: format!(
131+
"git diff --name-status {} {}",
132+
base_ref,
133+
head_ref.unwrap_or("working tree")
134+
),
128135
stderr: stderr.to_string(),
129136
}));
130137
}
@@ -346,8 +353,9 @@ impl SystemGit {
346353
// Read all files in one batch (100x+ faster than loop)
347354
let contents = self.read_files_bulk(&items)?;
348355

349-
// Combine paths with contents
350-
let results: Vec<(PathBuf, Vec<u8>)> = files.into_iter().zip(contents).collect();
356+
// Combine full paths (with crate prefix) with contents
357+
// Use paths from items (which include the crate prefix) not files (which are relative)
358+
let results: Vec<(PathBuf, Vec<u8>)> = items.into_iter().map(|(_, path)| path).zip(contents).collect();
351359

352360
Ok(results)
353361
}

0 commit comments

Comments
 (0)