Skip to content

Commit 7374e03

Browse files
committed
cargo-rail: fixing the codex-rs bug for nested workspaces under change detection; updating the readme/docs/etc.
1 parent 08ec44a commit 7374e03

File tree

7 files changed

+103
-29
lines changed

7 files changed

+103
-29
lines changed

README.md

Lines changed: 38 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@ cargo-rail is a single tool that replaces a fragmented ecosystem:
3737
| **MSRV guessing** | `cargo-msrv`, compile-and-fail loops | `cargo rail unify` |
3838
| **CI waste** | `paths-filter` + 1k LoC shell | `cargo rail test` |
3939
| **Crate extraction** | `git subtree`, Copybara (Java) | `cargo rail split` |
40-
| **Release chaos** | `release-plz` (600+ deps), `git-cliff` | `cargo rail release` |
40+
| **Release chaos** | `release-plz` (hundreds of deps), `git-cliff` | `cargo rail release` |
4141

42-
**11 core dependencies. 77 resolved. One config file.**
42+
**11 core dependencies. 55 resolved. One config file.**
4343

4444
---
4545

@@ -58,13 +58,15 @@ Pre-built binaries: [Releases](https://github.com/loadingalias/cargo-rail/releas
5858
Drop into any Rust workspace:
5959

6060
```bash
61-
cargo rail init # Generate .config/rail.toml
62-
cargo rail unify --check # Preview dependency unification
63-
cargo rail unify # Apply (auto-backups on first run)
64-
cargo rail test # Test only affected crates
61+
cargo install cargo-rail && cargo rail init
62+
cargo rail unify --check # See what would change (read-only)
6563
```
6664

67-
That's it. Your workspace is now unified, and you're only testing what changed.
65+
That's it. See what cargo-rail would unify — no changes made until you run `cargo rail unify`.
66+
67+
https://github.com/user-attachments/assets/93f34633-aa0e-4cde-8723-c81f3f474bac
68+
69+
<sub>*`cargo rail unify` on ripgrep — 9 deps unified, 6 dead features pruned*</sub>
6870

6971
---
7072

@@ -102,7 +104,7 @@ cargo rail test --all # Override: test everything
102104
**For CI:** Use [`cargo-rail-action`](https://github.com/loadingalias/cargo-rail-action):
103105

104106
```yaml
105-
- uses: loadingalias/cargo-rail-action@v1
107+
- uses: loadingalias/cargo-rail-action@latest
106108
id: rail
107109
with:
108110
since: ${{ github.event.before }}
@@ -135,11 +137,11 @@ What it does:
135137
Extract crates with full git history. Keep them in sync:
136138

137139
```bash
138-
cargo rail split init my_crate # Configure
139-
cargo rail split run my_crate # Extract with history
140-
cargo rail sync my_crate # Bidirectional sync
141-
cargo rail sync my_crate --to-remote # Push to split repo
142-
cargo rail sync my_crate --from-remote # Pull (creates PR branch)
140+
cargo rail split init my-crate # Configure
141+
cargo rail split run my-crate # Extract with history
142+
cargo rail sync my-crate # Bidirectional sync
143+
cargo rail sync my-crate --to-remote # Push to split repo
144+
cargo rail sync my-crate --from-remote # Pull (creates PR branch)
143145
```
144146

145147
Three modes: single crate → new repo, multiple crates → new repo, or multiple crates → new workspace.
@@ -168,7 +170,7 @@ Tested on production Rust workspaces:
168170
| **[helix](https://github.com/loadingalias/helix)** | 12 | 16 | 1 | Editor workspace |
169171
| **[tokio](https://github.com/loadingalias/tokio)** | 10 | 10 | 0 | Core ecosystem |
170172
| **[ripgrep](https://github.com/loadingalias/ripgrep)** | 10 | 9 | 6 | CLI baseline |
171-
| **[polars](https://github.com/loadingalias/polars)** | 8 | 2 | 9 | Already clean |
173+
| **[polars](https://github.com/loadingalias/polars)** | 33 | 2 | 9 | Already clean |
172174
| **[ruff](https://github.com/loadingalias/ruff)** | 43 | 0 | 0 | Already unified |
173175
| **[codex](https://github.com/loadingalias/codex)** | 49 | 0 | 0 | Already unified |
174176

@@ -203,7 +205,27 @@ Full guide: [docs/migrate-hakari.md](docs/migrate-hakari.md)
203205

204206
**Lossless TOML** — Uses `toml_edit` to preserve comments and formatting. Your manifests stay readable.
205207

206-
**Supply-Chain Safety** — 11 core dependencies. I built the release workflow specifically because I was uncomfortable with 600+ deps for release automation.
208+
**Supply-Chain Safety** — 11 core dependencies. I built the release workflow specifically because I was uncomfortable with hundreds of deps for release automation.
209+
210+
---
211+
212+
## FAQ
213+
214+
**How is this different from cargo-hakari?**
215+
216+
cargo-hakari creates a workspace-hack crate. cargo-rail writes unified versions directly to `[workspace.dependencies]` — no extra crate, no `hakari generate` step. Enable `pin_transitives = true` for equivalent behavior. See the [migration guide](docs/migrate-hakari.md).
217+
218+
**Does it work with workspace inheritance?**
219+
220+
Yes. cargo-rail writes to `[workspace.dependencies]` and converts member manifests to `{ workspace = true }`.
221+
222+
**What about virtual workspaces?**
223+
224+
Supported. For `pin_transitives`, cargo-rail auto-selects a workspace member as the transitive host (or set `transitive_host` explicitly).
225+
226+
**Private registries?**
227+
228+
Works via `cargo metadata`, which respects `.cargo/config.toml`. Private registry deps are pinned normally.
207229

208230
---
209231

@@ -220,7 +242,7 @@ Full guide: [docs/migrate-hakari.md](docs/migrate-hakari.md)
220242

221243
Issues, PRs, and feedback welcome. This is built for the Rust community.
222244

223-
If cargo-rail helps your workflow, consider [starring the repo](https://github.com/loadingalias/cargo-rail) — it helps others find it.
245+
Found this useful? [Star the repo](https://github.com/loadingalias/cargo-rail) to help other Rust teams find it.
224246

225247
---
226248

examples/release/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# release demo
22

3-
Demonstrates release validation and planning using tikv (70+ component workspace).
3+
Demonstrates release validation and planning using tikv (72 component workspace).
44

55
## Workflow
66

examples/unify/polars/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# polars unify demo
22

3-
Large data processing workspace (50+ crates, pyo3 bindings). Aggressive settings.
3+
Large data processing workspace (33 crates, pyo3 bindings). Aggressive settings.
44

55
## Config choices
66

@@ -31,7 +31,7 @@ major_version_conflict = "bump"
3131

3232
## What this shows
3333

34-
- Handling large workspace with 50+ crates
34+
- Handling large workspace with 33 crates
3535
- `pin_transitives` as cargo-hakari replacement
3636
- Aggressive `major_version_conflict = "bump"` for leaner graph
3737
- polars already uses `[workspace.dependencies]` - cargo-rail enhances it

examples/unify/ripgrep/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ripgrep unify demo
22

3-
Small, focused CLI workspace (9 crates). Demonstrates baseline unification.
3+
Small, focused CLI workspace (10 crates). Demonstrates baseline unification.
44

55
## Config choices
66

src/commands/affected.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -255,24 +255,28 @@ fn write_output(content: &str, output_file: Option<&PathBuf>) -> RailResult<()>
255255
Ok(())
256256
}
257257

258-
/// Get changed files from git
258+
/// Get changed files from git, normalized to workspace-relative paths
259259
fn get_changed_files(
260260
ctx: &WorkspaceContext,
261261
since: &str,
262262
from: Option<&str>,
263263
to: Option<&str>,
264264
) -> RailResult<Vec<PathBuf>> {
265265
// Determine git range
266-
let changes = if let (Some(from_ref), Some(to_ref)) = (from, to) {
266+
let git_changes = if let (Some(from_ref), Some(to_ref)) = (from, to) {
267267
// SHA pair mode: from..to
268268
ctx.git.git().get_changed_files_between(from_ref, Some(to_ref))?
269269
} else {
270270
// Single ref mode: since..working tree
271271
ctx.git.git().get_changed_files_between(since, None)?
272272
};
273273

274-
// Extract just the file paths (ignore status char)
275-
let files = changes.into_iter().map(|(path, _status)| path).collect();
274+
// Convert git-relative paths to workspace-relative paths
275+
// This handles nested workspaces (e.g., git at /repo, workspace at /repo/rust)
276+
let files = git_changes
277+
.into_iter()
278+
.filter_map(|(git_path, _status)| ctx.to_workspace_path(&git_path))
279+
.collect();
276280

277281
Ok(files)
278282
}

src/workspace/change_analyzer.rs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,13 +122,25 @@ impl<'a> ChangeImpact<'a> {
122122
///
123123
/// If `to` is None, compares against the working tree (uncommitted changes).
124124
pub fn analyze_changes(&self, from: &str, to: Option<&str>) -> RailResult<ImpactReport> {
125-
// 1. Get changed files from git
126-
let changed_files = self.ctx.git.git().get_changed_files_between(from, to)?;
127-
128-
// 2. Categorize changes by file type using new classification system
125+
// 1. Get changed files from git (paths are relative to git root)
126+
let git_changed_files = self.ctx.git.git().get_changed_files_between(from, to)?;
127+
128+
// 2. Convert git-relative paths to workspace-relative paths
129+
// This handles nested workspaces (e.g., git at /repo, workspace at /repo/rust)
130+
let changed_files: Vec<(PathBuf, char)> = git_changed_files
131+
.into_iter()
132+
.filter_map(|(git_path, change_type)| {
133+
self
134+
.ctx
135+
.to_workspace_path(&git_path)
136+
.map(|ws_path| (ws_path, change_type))
137+
})
138+
.collect();
139+
140+
// 3. Categorize changes by file type using new classification system
129141
let categories = self.categorize_changes(&changed_files);
130142

131-
// 3. Use graph to find affected crates
143+
// 4. Use graph to find affected crates
132144
let file_paths: Vec<PathBuf> = changed_files.iter().map(|(p, _)| p.clone()).collect();
133145
let analysis = crate::graph::analyze(&self.ctx.graph, &file_paths)?;
134146

src/workspace/context.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,42 @@ impl WorkspaceContext {
420420
pub fn workspace_root(&self) -> &Path {
421421
&self.workspace_root
422422
}
423+
424+
/// Get the relative path from git root to workspace root.
425+
///
426+
/// Returns `Some(prefix)` if workspace is nested inside git repo (e.g., "codex-rs"),
427+
/// or `None` if they're the same directory.
428+
///
429+
/// This is used to strip the prefix from git-relative paths so they can be
430+
/// matched against workspace-relative paths (e.g., for crate membership).
431+
pub fn workspace_prefix(&self) -> Option<PathBuf> {
432+
let git_root = self.git.repo_root();
433+
434+
// If workspace is nested inside git repo, compute the relative prefix
435+
if let Ok(prefix) = self.workspace_root.strip_prefix(git_root) {
436+
if prefix.as_os_str().is_empty() {
437+
None // Same directory
438+
} else {
439+
Some(prefix.to_path_buf())
440+
}
441+
} else {
442+
None
443+
}
444+
}
445+
446+
/// Convert a git-relative path to a workspace-relative path.
447+
///
448+
/// If the workspace is nested inside the git repo (e.g., git at `/repo`, workspace at `/repo/rust`),
449+
/// git returns paths like `rust/src/lib.rs` but the workspace expects `src/lib.rs`.
450+
///
451+
/// Returns `None` if the path doesn't belong to this workspace.
452+
pub fn to_workspace_path(&self, git_path: &Path) -> Option<PathBuf> {
453+
if let Some(prefix) = self.workspace_prefix() {
454+
git_path.strip_prefix(&prefix).ok().map(|p| p.to_path_buf())
455+
} else {
456+
Some(git_path.to_path_buf())
457+
}
458+
}
423459
}
424460

425461
#[cfg(test)]

0 commit comments

Comments
 (0)