Skip to content

Commit 7cc7ac1

Browse files
committed
cargo-rail: fixed CI issue; added testing and gitnotes robustness. cleaned some todo items up
1 parent 72d5236 commit 7cc7ac1

File tree

14 files changed

+335
-195
lines changed

14 files changed

+335
-195
lines changed

src/cargo/metadata.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,9 @@ impl WorkspaceMetadata {
119119
/// - Feature dependency analysis: Understand feature activation chains
120120
/// - Feature optimization: Identify minimal feature sets for build times
121121
///
122+
/// TODO: Use this for `cargo rail audit` or `cargo rail graph` to inspect specific packages.
123+
/// For global analysis (like unification), use `cargo_metadata` directly to avoid O(N) lookups.
124+
///
122125
/// # Example
123126
/// ```ignore
124127
/// let features = metadata.package_features("serde")?;
@@ -138,6 +141,8 @@ impl WorkspaceMetadata {
138141
/// - `cargo rail build --lib`: Build only library targets
139142
/// - Target coverage: Ensure all crates have appropriate targets
140143
///
144+
/// TODO: Use this for `cargo rail test` filtering or `cargo rail check` to validate targets.
145+
///
141146
/// # Example
142147
/// ```ignore
143148
/// let targets = metadata.package_targets("cargo-rail");
@@ -159,6 +164,8 @@ impl WorkspaceMetadata {
159164
/// - Migration planning: Identify crates stuck on old editions
160165
/// - Compatibility matrix: Track edition usage across workspace
161166
///
167+
/// TODO: Integrate into `unify` pre-checks or `cargo rail lint` to ensure consistent editions.
168+
///
162169
/// # Example
163170
/// ```ignore
164171
/// if metadata.package_edition("lib-core")? == "2021" {
@@ -178,6 +185,8 @@ impl WorkspaceMetadata {
178185
/// - Workspace MSRV policy: Enforce minimum Rust version across crates
179186
/// - CI matrix: Generate test matrix based on supported Rust versions
180187
///
188+
/// TODO: Integrate into `unify` pre-checks or `cargo rail lint` to enforce MSRV policy.
189+
///
181190
/// # Example
182191
/// ```ignore
183192
/// let msrv = metadata.package_rust_version("lib-core")?;
@@ -201,6 +210,9 @@ impl WorkspaceMetadata {
201210
/// - License compliance: Check all dependency licenses
202211
/// - Duplicate detection: Find dependencies listed multiple times
203212
///
213+
/// TODO: Use this for `cargo rail graph` or `cargo rail audit` to inspect dependencies of a specific crate.
214+
/// For global analysis, iterate `metadata.packages` directly.
215+
///
204216
/// # Example
205217
/// ```ignore
206218
/// let deps = metadata.package_dependencies("cargo-rail");

src/commands/config_sync.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,12 @@ pub struct SyncRegistry {
113113
syncers: Vec<Box<dyn ConfigSyncer>>,
114114
}
115115

116+
impl Default for SyncRegistry {
117+
fn default() -> Self {
118+
Self::new()
119+
}
120+
}
121+
116122
impl SyncRegistry {
117123
pub fn new() -> Self {
118124
Self { syncers: Vec::new() }

src/commands/sync.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use std::io::IsTerminal;
2+
use std::str::FromStr;
23
use std::sync::Arc;
34

45
use crate::commands::common::SplitSyncConfigBuilder;

src/config.rs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -389,13 +389,22 @@ impl RailConfig {
389389
// We do this by checking if a de-canonicalized version exists.
390390
#[cfg(target_os = "windows")]
391391
{
392-
// Try to find the config by reconstructing paths from the parent
393-
// This handles cases where:
394-
// 1. cargo metadata returns \\?\C:\Users\RUNNER~1\AppData\Local\Temp\...
395-
// 2. But test wrote file to C:\Users\RUNNER~1\AppData\Local\Temp\...
396-
// Or the short name (8.3) vs long name issue
392+
// 1. Try canonicalizing the path and searching there
393+
// This handles 8.3 short paths vs long paths issues (RUNNER~1 vs runneradmin)
394+
if let Ok(canonical) = path.canonicalize() {
395+
let canonical_candidates = [
396+
canonical.join("rail.toml"),
397+
canonical.join(".rail.toml"),
398+
canonical.join(".cargo").join("rail.toml"),
399+
canonical.join(".config").join("rail.toml"),
400+
];
401+
if let Some(found) = canonical_candidates.iter().find(|p| p.exists()) {
402+
return Some(found.to_path_buf());
403+
}
404+
}
397405

398-
// Check if we can read the directory to find config files
406+
// 2. Try to find the config by reading the directory entries
407+
// This handles cases where exact path string matching fails but the file is in the directory
399408
if let Ok(entries) = std::fs::read_dir(path) {
400409
for entry in entries.flatten() {
401410
let file_name = entry.file_name();
@@ -408,7 +417,7 @@ impl RailConfig {
408417
}
409418
}
410419

411-
// Also check subdirectories .cargo and .config
420+
// Also check subdirectories .cargo and .config via read_dir
412421
for subdir in &[".cargo", ".config"] {
413422
let subdir_path = path.join(subdir);
414423
if let Ok(entries) = std::fs::read_dir(&subdir_path) {

src/git/mappings.rs

Lines changed: 155 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use crate::error::{GitError, RailError, RailResult, ResultExt};
22
use std::collections::HashMap;
33
use std::path::Path;
4+
use std::thread;
5+
use std::time::Duration;
46

57
/// Commit mapping store using git-notes
68
/// Maps commits between monorepo and split repos
@@ -163,19 +165,22 @@ impl MappingStore {
163165

164166
println!(" Pushing git-notes to remote '{}'...", remote);
165167

166-
let output = Command::new("git")
167-
.current_dir(repo_path)
168-
.args(["push", remote, &notes_ref])
169-
.output()
170-
.context("Failed to push git-notes")?;
171-
172-
if !output.status.success() {
173-
let stderr = String::from_utf8_lossy(&output.stderr);
174-
return Err(RailError::Git(GitError::CommandFailed {
175-
command: "git push notes".to_string(),
176-
stderr: stderr.to_string(),
177-
}));
178-
}
168+
retry_operation(|| {
169+
let output = Command::new("git")
170+
.current_dir(repo_path)
171+
.args(["push", remote, &notes_ref])
172+
.output()
173+
.context("Failed to push git-notes")?;
174+
175+
if !output.status.success() {
176+
let stderr = String::from_utf8_lossy(&output.stderr);
177+
return Err(RailError::Git(GitError::CommandFailed {
178+
command: "git push notes".to_string(),
179+
stderr: stderr.to_string(),
180+
}));
181+
}
182+
Ok(())
183+
})?;
179184

180185
println!(" ✅ Pushed git-notes");
181186
Ok(())
@@ -190,91 +195,154 @@ impl MappingStore {
190195

191196
println!(" Fetching git-notes from remote '{}'...", remote);
192197

193-
let output = Command::new("git")
194-
.current_dir(repo_path)
195-
.args(["fetch", remote, &refspec])
196-
.output()
197-
.context("Failed to fetch git-notes")?;
198+
// Retry the fetch operation
199+
let result = retry_operation(|| {
200+
let output = Command::new("git")
201+
.current_dir(repo_path)
202+
.args(["fetch", remote, &refspec])
203+
.output()
204+
.context("Failed to fetch git-notes")?;
205+
206+
if !output.status.success() {
207+
let stderr = String::from_utf8_lossy(&output.stderr);
198208

199-
if !output.status.success() {
200-
let stderr = String::from_utf8_lossy(&output.stderr);
209+
// Ignore "couldn't find remote ref" - notes may not exist yet
210+
if stderr.contains("couldn't find remote ref") {
211+
return Ok(()); // Treat as success (no notes yet)
212+
}
201213

202-
// Ignore "couldn't find remote ref" - notes may not exist yet
203-
if stderr.contains("couldn't find remote ref") {
204-
println!(" ℹ️ No remote git-notes found yet (this is normal for first sync)");
205-
return Ok(());
214+
return Err(RailError::Git(GitError::CommandFailed {
215+
command: "git fetch notes".to_string(),
216+
stderr: stderr.to_string(),
217+
}));
206218
}
219+
Ok(())
220+
});
207221

208-
// Handle non-fast-forward (notes conflict)
209-
if stderr.contains("non-fast-forward") || stderr.contains("rejected") {
210-
println!(" ⚠️ Git-notes conflict detected (local and remote notes diverged)");
211-
println!(" 🔄 Attempting automatic merge with union strategy...");
212-
213-
// Fetch to FETCH_HEAD without updating the ref
214-
let fetch_output = Command::new("git")
215-
.current_dir(repo_path)
216-
.args(["fetch", remote, &notes_ref])
217-
.output()
218-
.context("Failed to fetch notes to FETCH_HEAD")?;
219-
220-
if !fetch_output.status.success() {
221-
let fetch_stderr = String::from_utf8_lossy(&fetch_output.stderr);
222-
if !fetch_stderr.contains("couldn't find remote ref") {
223-
return Err(RailError::Git(GitError::CommandFailed {
224-
command: "git fetch notes to FETCH_HEAD".to_string(),
225-
stderr: fetch_stderr.to_string(),
226-
}));
222+
match result {
223+
Ok(_) => {
224+
println!(" ✅ Fetched git-notes");
225+
Ok(())
226+
}
227+
Err(e) => {
228+
// Check if it's a conflict (non-fast-forward)
229+
let is_conflict = if let RailError::Git(GitError::CommandFailed { stderr, .. }) = &e {
230+
stderr.contains("non-fast-forward") || stderr.contains("rejected")
231+
} else {
232+
false
233+
};
234+
235+
if is_conflict {
236+
println!(" ⚠️ Git-notes conflict detected (local and remote notes diverged)");
237+
println!(" 🔄 Attempting automatic merge with union strategy...");
238+
239+
// Fetch to FETCH_HEAD without updating the ref
240+
let fetch_output = Command::new("git")
241+
.current_dir(repo_path)
242+
.args(["fetch", remote, &notes_ref])
243+
.output()
244+
.context("Failed to fetch notes to FETCH_HEAD")?;
245+
246+
if !fetch_output.status.success() {
247+
let fetch_stderr = String::from_utf8_lossy(&fetch_output.stderr);
248+
if !fetch_stderr.contains("couldn't find remote ref") {
249+
return Err(RailError::Git(GitError::CommandFailed {
250+
command: "git fetch notes to FETCH_HEAD".to_string(),
251+
stderr: fetch_stderr.to_string(),
252+
}));
253+
}
254+
return Ok(()); // No remote notes
227255
}
228-
return Ok(()); // No remote notes
256+
257+
// Merge notes using union strategy (combines both without conflict)
258+
let merge_output = Command::new("git")
259+
.current_dir(repo_path)
260+
.args(["notes", "--ref", &notes_ref, "merge", "--strategy=union", "FETCH_HEAD"])
261+
.output()
262+
.context("Failed to merge git-notes")?;
263+
264+
if !merge_output.status.success() {
265+
let merge_stderr = String::from_utf8_lossy(&merge_output.stderr);
266+
267+
// If union merge fails, provide clear guidance
268+
eprintln!(" ❌ Automatic git-notes merge failed");
269+
eprintln!(" 📋 Manual resolution required:");
270+
eprintln!(" 1. cd {}", repo_path.display());
271+
eprintln!(" 2. git notes --ref={} merge FETCH_HEAD", notes_ref);
272+
eprintln!(" 3. Resolve conflicts manually");
273+
eprintln!(" 4. git notes --ref={} merge --commit", notes_ref);
274+
eprintln!();
275+
return Err(RailError::with_help(
276+
format!("git notes merge failed: {}", merge_stderr),
277+
format!(
278+
"This usually happens when the same commit has different mappings on different machines.\n\
279+
Manual resolution steps:\n \
280+
1. cd {}\n \
281+
2. git notes --ref={} merge FETCH_HEAD\n \
282+
3. Resolve conflicts manually\n \
283+
4. git notes --ref={} merge --commit",
284+
repo_path.display(),
285+
notes_ref,
286+
notes_ref
287+
),
288+
));
289+
}
290+
291+
println!(" ✅ Git-notes merged successfully (union strategy)");
292+
Ok(())
293+
} else {
294+
// Propagate other errors
295+
Err(e)
296+
}
297+
}
298+
}
299+
}
300+
}
301+
302+
/// Retry an operation with exponential backoff
303+
fn retry_operation<F, T>(mut operation: F) -> RailResult<T>
304+
where
305+
F: FnMut() -> RailResult<T>,
306+
{
307+
let max_retries = 3;
308+
let mut attempt = 0;
309+
let mut delay = Duration::from_millis(500);
310+
311+
loop {
312+
match operation() {
313+
Ok(result) => return Ok(result),
314+
Err(e) => {
315+
attempt += 1;
316+
if attempt > max_retries {
317+
return Err(e);
229318
}
230319

231-
// Merge notes using union strategy (combines both without conflict)
232-
let merge_output = Command::new("git")
233-
.current_dir(repo_path)
234-
.args(["notes", "--ref", &notes_ref, "merge", "--strategy=union", "FETCH_HEAD"])
235-
.output()
236-
.context("Failed to merge git-notes")?;
237-
238-
if !merge_output.status.success() {
239-
let merge_stderr = String::from_utf8_lossy(&merge_output.stderr);
240-
241-
// If union merge fails, provide clear guidance
242-
eprintln!(" ❌ Automatic git-notes merge failed");
243-
eprintln!(" 📋 Manual resolution required:");
244-
eprintln!(" 1. cd {}", repo_path.display());
245-
eprintln!(" 2. git notes --ref={} merge FETCH_HEAD", notes_ref);
246-
eprintln!(" 3. Resolve conflicts manually");
247-
eprintln!(" 4. git notes --ref={} merge --commit", notes_ref);
248-
eprintln!();
249-
return Err(RailError::with_help(
250-
format!("git notes merge failed: {}", merge_stderr),
251-
format!(
252-
"This usually happens when the same commit has different mappings on different machines.\n\
253-
Manual resolution steps:\n \
254-
1. cd {}\n \
255-
2. git notes --ref={} merge FETCH_HEAD\n \
256-
3. Resolve conflicts manually\n \
257-
4. git notes --ref={} merge --commit",
258-
repo_path.display(),
259-
notes_ref,
260-
notes_ref
261-
),
262-
));
320+
// Only retry on network/lock errors, not logical errors
321+
// For now, we assume most git command failures in this context are transient or lock-related
322+
// unless it's a conflict which we handle separately in fetch_notes
323+
let should_retry = match &e {
324+
RailError::Git(GitError::CommandFailed { stderr, .. }) => {
325+
stderr.contains("lock")
326+
|| stderr.contains("temporarily unavailable")
327+
|| stderr.contains("Connection timed out")
328+
|| stderr.contains("Could not resolve host")
329+
|| stderr.contains("Failed to connect")
330+
}
331+
_ => false,
332+
};
333+
334+
if !should_retry {
335+
return Err(e);
263336
}
264337

265-
println!(" ✅ Git-notes merged successfully (union strategy)");
266-
return Ok(());
338+
println!(
339+
" ⚠️ Operation failed. Retrying in {:?}... (Attempt {}/{})",
340+
delay, attempt, max_retries
341+
);
342+
thread::sleep(delay);
343+
delay *= 2;
267344
}
268-
269-
// Unknown error
270-
return Err(RailError::Git(GitError::CommandFailed {
271-
command: "git fetch notes".to_string(),
272-
stderr: stderr.to_string(),
273-
}));
274345
}
275-
276-
println!(" ✅ Fetched git-notes");
277-
Ok(())
278346
}
279347
}
280348

0 commit comments

Comments
 (0)