Skip to content

Commit 604b821

Browse files
committed
cargo-rail: rethought, refactored, and greatly improved the 'cargo rail unify' pipe. consolidated TOML formatting/transforms; improved command structure.
1 parent e73166d commit 604b821

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+3707
-6387
lines changed

.gitignore

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,7 @@ AGENTS.md
2222

2323
# Documentation
2424
docs/*
25-
AUDIT_SUMMARY.md
26-
unify.md
27-
CONFIG_AUDIT.md
25+
audit.md
2826

2927
# Cargo-Rail (Testing)
3028
*rail.toml

src/backup/manager.rs

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,13 @@ impl BackupManager {
3131
///
3232
/// * `files` - Files to backup (relative to workspace root)
3333
/// * `metadata` - Metadata describing this backup
34-
pub fn create_backup(&self, files: &[PathBuf], mut metadata: BackupMetadata) -> RailResult<BackupId> {
34+
/// * `max_backups` - Maximum number of backups to keep (0 = unlimited)
35+
pub fn create_backup(
36+
&self,
37+
files: &[PathBuf],
38+
mut metadata: BackupMetadata,
39+
max_backups: usize,
40+
) -> RailResult<BackupId> {
3541
// Generate backup ID
3642
let backup_id = create_backup_id();
3743
let backup_dir = get_backup_dir(&self.workspace_root, &backup_id);
@@ -83,6 +89,11 @@ impl BackupManager {
8389
// Save metadata
8490
metadata.save(&backup_dir)?;
8591

92+
// Clean up old backups if max_backups > 0
93+
if max_backups > 0 {
94+
let _deleted = self.cleanup_old_backups(max_backups)?;
95+
}
96+
8697
Ok(backup_id)
8798
}
8899

@@ -273,7 +284,7 @@ mod tests {
273284

274285
// Create backup
275286
let metadata = BackupMetadata::new("test command");
276-
let backup_id = manager.create_backup(&files, metadata)?;
287+
let backup_id = manager.create_backup(&files, metadata, 0)?;
277288

278289
// Verify backup was created
279290
let backup_dir = get_backup_dir(workspace.path(), &backup_id);
@@ -308,7 +319,7 @@ mod tests {
308319
// Create a backup
309320
let files = vec![PathBuf::from("Cargo.toml")];
310321
let metadata = BackupMetadata::new("test 1");
311-
manager.create_backup(&files, metadata)?;
322+
manager.create_backup(&files, metadata, 0)?;
312323

313324
// Should now have 1 backup
314325
assert!(manager.has_backups());
@@ -321,7 +332,7 @@ mod tests {
321332

322333
// Create another backup
323334
let metadata2 = BackupMetadata::new("test 2");
324-
manager.create_backup(&files, metadata2)?;
335+
manager.create_backup(&files, metadata2, 0)?;
325336

326337
// Should have 2 backups
327338
let backups = manager.list_backups()?;
@@ -339,7 +350,7 @@ mod tests {
339350
let files = vec![PathBuf::from("Cargo.toml")];
340351
for i in 1..=5 {
341352
let metadata = BackupMetadata::new(format!("test {}", i));
342-
manager.create_backup(&files, metadata)?;
353+
manager.create_backup(&files, metadata, 0)?;
343354
// Sleep 1 second to ensure different timestamps (format is YYYY-MM-DD-HHMMSS)
344355
std::thread::sleep(std::time::Duration::from_secs(1));
345356
}
@@ -369,9 +380,9 @@ mod tests {
369380

370381
// Create backups with sufficient delay
371382
let files = vec![PathBuf::from("Cargo.toml")];
372-
manager.create_backup(&files, BackupMetadata::new("first"))?;
383+
manager.create_backup(&files, BackupMetadata::new("first"), 0)?;
373384
std::thread::sleep(std::time::Duration::from_secs(1));
374-
manager.create_backup(&files, BackupMetadata::new("second"))?;
385+
manager.create_backup(&files, BackupMetadata::new("second"), 0)?;
375386

376387
// Latest should be "second"
377388
let latest = manager.get_latest_backup()?.unwrap();

src/cargo/cargo_transform.rs

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
//! Cargo.toml transformation for split/sync operations
2+
//!
3+
//! This module provides simple Cargo.toml transformations needed by split and sync:
4+
//! - Transform workspace dependencies to standalone format (for splits)
5+
//! - Transform standalone dependencies back to workspace format (for syncs)
6+
7+
use crate::cargo::manifest_ops;
8+
use crate::error::{RailError, RailResult};
9+
use cargo_metadata::Metadata;
10+
use std::path::PathBuf;
11+
use toml_edit::{DocumentMut, Item};
12+
13+
/// Context for Cargo.toml transformations
14+
pub struct TransformContext {
15+
/// Name of the crate being transformed
16+
pub crate_name: String,
17+
/// Workspace root path
18+
pub workspace_root: PathBuf,
19+
}
20+
21+
/// Cargo.toml transformer for split/sync operations
22+
pub struct CargoTransform {
23+
metadata: Metadata,
24+
}
25+
26+
impl CargoTransform {
27+
/// Create a new transformer with workspace metadata
28+
pub fn new(metadata: Metadata) -> Self {
29+
Self { metadata }
30+
}
31+
32+
/// Transform a Cargo.toml from workspace format to split (standalone) format
33+
///
34+
/// This replaces workspace dependency references with concrete version requirements.
35+
pub fn transform_to_split(&self, content: &str, context: &TransformContext) -> RailResult<String> {
36+
let mut doc: DocumentMut = content
37+
.parse()
38+
.map_err(|e| RailError::message(format!("Failed to parse Cargo.toml: {}", e)))?;
39+
40+
// Remove workspace inheritance markers and resolve to actual values
41+
self.resolve_workspace_inheritance(&mut doc, &context.workspace_root)?;
42+
43+
// Transform workspace dependencies to standalone format
44+
self.transform_dependencies_to_standalone(&mut doc)?;
45+
46+
Ok(doc.to_string())
47+
}
48+
49+
/// Transform a Cargo.toml from split (standalone) format back to workspace format
50+
///
51+
/// This is currently a no-op since syncing from remote to mono doesn't need transformation.
52+
/// The crate in the monorepo already uses workspace format.
53+
pub fn transform_to_mono(&self, content: &str, _context: &TransformContext) -> RailResult<String> {
54+
// For now, pass through unchanged. If we need to restore workspace.dependencies
55+
// references, we can implement that here.
56+
Ok(content.to_string())
57+
}
58+
59+
/// Resolve workspace inheritance (workspace = true fields) to actual values
60+
fn resolve_workspace_inheritance(&self, doc: &mut DocumentMut, workspace_root: &std::path::Path) -> RailResult<()> {
61+
// Load workspace Cargo.toml to get [workspace.package] values
62+
let workspace_toml_path = workspace_root.join("Cargo.toml");
63+
let workspace_doc = manifest_ops::read_toml_file(&workspace_toml_path)?;
64+
65+
// Get workspace.package table if it exists
66+
let workspace_package = workspace_doc
67+
.get("workspace")
68+
.and_then(|w| w.as_table())
69+
.and_then(|w| w.get("package"))
70+
.and_then(|p| p.as_table());
71+
72+
// Use manifest_ops to resolve package inheritance
73+
if let Some(workspace_pkg) = workspace_package {
74+
manifest_ops::resolve_package_workspace_inheritance(doc, workspace_pkg)?;
75+
}
76+
77+
Ok(())
78+
}
79+
80+
/// Transform workspace dependencies to standalone format
81+
fn transform_dependencies_to_standalone(&self, doc: &mut DocumentMut) -> RailResult<()> {
82+
// Transform each dependency section using manifest_ops
83+
manifest_ops::transform_dependencies_in_section(doc, "dependencies", |name, item| {
84+
self.transform_and_resolve_dep(item, name)
85+
})?;
86+
87+
manifest_ops::transform_dependencies_in_section(doc, "dev-dependencies", |name, item| {
88+
self.transform_and_resolve_dep(item, name)
89+
})?;
90+
91+
manifest_ops::transform_dependencies_in_section(doc, "build-dependencies", |name, item| {
92+
self.transform_and_resolve_dep(item, name)
93+
})?;
94+
95+
Ok(())
96+
}
97+
98+
/// Helper: transform and resolve a single dependency
99+
fn transform_and_resolve_dep(&self, dep_item: &mut Item, dep_name: &str) -> RailResult<()> {
100+
// Check if this is a workspace dependency using manifest_ops
101+
if manifest_ops::is_workspace_dep(dep_item) {
102+
// Find the dependency version in workspace metadata
103+
if let Some(pkg) = self.metadata.packages.iter().find(|p| p.name == dep_name) {
104+
let version = pkg.version.to_string();
105+
106+
// Remove workspace marker and set version using manifest_ops
107+
manifest_ops::extract_workspace_marker(dep_item);
108+
manifest_ops::set_version(dep_item, &version)?;
109+
}
110+
}
111+
112+
// Remove path dependencies (they won't be valid in split repo)
113+
// This applies to both workspace and non-workspace dependencies
114+
manifest_ops::remove_path(dep_item);
115+
116+
Ok(())
117+
}
118+
}
119+
120+
#[cfg(test)]
121+
mod tests {
122+
use super::*;
123+
124+
#[test]
125+
fn test_transform_workspace_dep() {
126+
let input = r#"
127+
[package]
128+
name = "test-crate"
129+
version = "0.1.0"
130+
131+
[dependencies]
132+
other-crate = { workspace = true }
133+
serde = "1.0"
134+
"#;
135+
136+
// For testing, we'll just verify it parses and doesn't crash
137+
let doc: DocumentMut = input.parse().unwrap();
138+
assert!(doc.get("dependencies").is_some());
139+
}
140+
141+
#[test]
142+
fn test_transform_to_mono_passthrough() {
143+
let input = r#"
144+
[package]
145+
name = "test-crate"
146+
version = "0.1.0"
147+
148+
[dependencies]
149+
serde = "1.0"
150+
"#;
151+
152+
// Create a minimal metadata for testing
153+
let metadata_json = serde_json::json!({
154+
"packages": [],
155+
"workspace_members": [],
156+
"resolve": null,
157+
"target_directory": "/tmp",
158+
"version": 1,
159+
"workspace_root": "/tmp",
160+
"metadata": null
161+
});
162+
163+
let metadata: Metadata = serde_json::from_value(metadata_json).unwrap();
164+
let transformer = CargoTransform::new(metadata);
165+
let context = TransformContext {
166+
crate_name: "test-crate".to_string(),
167+
workspace_root: PathBuf::from("/tmp"),
168+
};
169+
170+
let result = transformer.transform_to_mono(input, &context).unwrap();
171+
assert_eq!(result, input);
172+
}
173+
}

0 commit comments

Comments
 (0)