From bd89db27803f55329f57518e2dab7f2f9831996f Mon Sep 17 00:00:00 2001 From: Fabro Date: Mon, 16 Mar 2026 05:38:58 +0000 Subject: [PATCH] fabro(01KKTJ9PFAH95DTPCN7NMC3AV6): solve (success) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fabro-Run: 01KKTJ9PFAH95DTPCN7NMC3AV6 Fabro-Completed: 3 Fabro-Checkpoint: 16fdfaebfc9fa67e019492d784d6f114cd3c75a0 ⚒️ Generated with [Fabro](https://fabro.sh) --- lib/crates/fabro-workflows/src/cli/run.rs | 120 +++++++++++++++++++++- 1 file changed, 117 insertions(+), 3 deletions(-) diff --git a/lib/crates/fabro-workflows/src/cli/run.rs b/lib/crates/fabro-workflows/src/cli/run.rs index 749f8882d..43ef1e91c 100644 --- a/lib/crates/fabro-workflows/src/cli/run.rs +++ b/lib/crates/fabro-workflows/src/cli/run.rs @@ -313,6 +313,78 @@ struct CostAccumulator { has_pricing: bool, } +/// Extract the git clone URL from a git clone command string. +/// +/// Handles formats like: `git clone `, `git clone --branch `, etc. +fn parse_git_clone_url(cmd: &str) -> Option { + // Find "git clone" and extract the URL that comes after it + if let Some(clone_pos) = cmd.find("git clone") { + let after_clone = &cmd[clone_pos + 9..]; // Skip "git clone" + + for part in after_clone.split_whitespace() { + // Skip flags + if part.starts_with('-') { + continue; + } + // Check if this looks like a URL + if part.contains("://") || part.starts_with("git@") { + return Some(part.to_string()); + } + } + } + None +} + +/// Extract the git branch from a git clone command if specified with --branch. +fn extract_git_branch(cmd: &str) -> Option { + if let Some(branch_pos) = cmd.find("--branch") { + let after_flag = &cmd[branch_pos + 8..]; // Skip "--branch" + let remaining = after_flag.trim_start(); + + // Handle both "--branch " and "--branch=" + if remaining.starts_with('=') { + remaining[1..].split_whitespace().next().map(|s| s.to_string()) + } else { + remaining.split_whitespace().next().map(|s| s.to_string()) + } + } else { + None + } +} + +/// Extract the target directory from a git clone command. +/// Returns "." if cloning to current directory or if target dir is not specified. +fn extract_git_target_dir(cmd: &str) -> Option { + // If command ends with " .", it's cloning to current directory + if cmd.trim_end().ends_with(" .") || cmd.trim_end().ends_with("' .") { + return Some(".".to_string()); + } + + // Try to find the target directory - it's typically the last argument after the URL + if let Some(clone_pos) = cmd.find("git clone") { + let after_clone = &cmd[clone_pos + 9..]; + let parts: Vec<&str> = after_clone.split_whitespace().collect(); + + let mut found_url = false; + for part in parts.iter() { + if found_url { + // If this part doesn't start with -, it's likely the target directory + if !part.starts_with('-') && !part.contains("://") && !part.starts_with("git@") { + // Check if this is part of chained commands (&&, ||, etc.) + if !part.starts_with("&&") && !part.starts_with("||") && !part.starts_with("|") { + return Some(part.to_string()); + } + } + } + + if (part.contains("://") || part.starts_with("git@")) && !part.starts_with('-') { + found_url = true; + } + } + } + None +} + /// Execute a full workflow run. /// /// # Errors @@ -1040,11 +1112,53 @@ pub async fn run_command( index, }); let cmd_start = Instant::now(); - let result = sandbox + let mut result = sandbox .exec_command(cmd, 300_000, None, None, None) .await .map_err(|e| anyhow::anyhow!("Setup command failed: {e}"))?; - let cmd_duration = crate::millis_u64(cmd_start.elapsed()); + let mut cmd_duration = crate::millis_u64(cmd_start.elapsed()); + + // If git clone fails due to non-empty directory, try fallback + if result.exit_code != 0 { + let stderr = String::from_utf8_lossy(&result.stderr); + if cmd.contains("git clone") && + (stderr.contains("not an empty directory") || stderr.contains("already exists and is not an empty")) { + // Try fallback: git init + remote add + fetch + checkout + if let Some(clone_url) = parse_git_clone_url(cmd) { + let branch = extract_git_branch(cmd).unwrap_or_else(|| "main".to_string()); + let target_dir = extract_git_target_dir(cmd).unwrap_or_else(|| ".".to_string()); + + // Extract any chained commands after the git clone (e.g., && ...) + let rest_of_cmd = if let Some(amp_pos) = cmd.find("&&") { + &cmd[amp_pos..] + } else { + "" + }; + + let fallback_cmd = if target_dir == "." { + // Current directory - don't cd + format!( + "git init && git remote add origin '{}' && git fetch origin && git checkout '{}' {}", + clone_url, branch, rest_of_cmd + ) + } else { + // Specific directory - cd first + format!( + "cd '{}' && git init && git remote add origin '{}' && git fetch origin && git checkout '{}' {}", + target_dir, clone_url, branch, rest_of_cmd + ) + }; + + let fallback_start = Instant::now(); + result = sandbox + .exec_command(&fallback_cmd, 300_000, None, None, None) + .await + .map_err(|e| anyhow::anyhow!("Setup command fallback failed: {e}"))?; + cmd_duration = crate::millis_u64(fallback_start.elapsed()); + } + } + } + if result.exit_code != 0 { emitter.emit(&crate::event::WorkflowRunEvent::SetupFailed { command: cmd.clone(), @@ -3150,4 +3264,4 @@ mod tests { "third field must be event, got: {fields:?}" ); } -} +} \ No newline at end of file