@@ -8,7 +8,7 @@ pub struct GitResult {
88 pub error : Option < String > ,
99}
1010
11- fn run_git ( cwd : & str , args : & [ & str ] ) -> GitResult {
11+ pub ( crate ) fn run_git ( cwd : & str , args : & [ & str ] ) -> GitResult {
1212 match Command :: new ( "git" ) . args ( args) . current_dir ( cwd) . output ( ) {
1313 Ok ( output) => {
1414 let stdout = String :: from_utf8_lossy ( & output. stdout ) . trim ( ) . to_string ( ) ;
@@ -257,6 +257,103 @@ pub fn detect_default_branch(cwd: &str) -> String {
257257 "main" . to_string ( )
258258}
259259
260+ /// Clone a repository. Runs in the parent directory of `destination`.
261+ /// Uses GIT_TERMINAL_PROMPT=0 to prevent interactive credential prompts
262+ /// from hanging the process. Skips LFS to avoid downloading large
263+ /// binary files that aren't needed for code review.
264+ pub fn clone_repo ( url : & str , destination : & str ) -> GitResult {
265+ let dest_path = std:: path:: Path :: new ( destination) ;
266+
267+ // Create parent directory if needed
268+ if let Some ( parent) = dest_path. parent ( ) {
269+ let _ = std:: fs:: create_dir_all ( parent) ;
270+ }
271+
272+ match Command :: new ( "git" )
273+ . args ( [ "clone" , url, destination] )
274+ . env ( "GIT_TERMINAL_PROMPT" , "0" )
275+ . env ( "GIT_LFS_SKIP_SMUDGE" , "1" )
276+ . output ( )
277+ {
278+ Ok ( output) => {
279+ let stdout = String :: from_utf8_lossy ( & output. stdout ) . trim ( ) . to_string ( ) ;
280+ let stderr = String :: from_utf8_lossy ( & output. stderr ) . trim ( ) . to_string ( ) ;
281+ if output. status . success ( ) {
282+ GitResult {
283+ success : true ,
284+ output : destination. to_string ( ) ,
285+ error : None ,
286+ }
287+ } else {
288+ GitResult {
289+ success : false ,
290+ output : stdout,
291+ error : Some ( stderr) ,
292+ }
293+ }
294+ }
295+ Err ( e) => GitResult {
296+ success : false ,
297+ output : String :: new ( ) ,
298+ error : Some ( format ! ( "Failed to execute git clone: {}" , e) ) ,
299+ } ,
300+ }
301+ }
302+
303+ /// Fetch a specific branch from origin and create a local branch.
304+ /// Tries the branch name first, then falls back to `pull/<number>/head`
305+ /// for fork-based PRs where the branch doesn't exist on the upstream remote.
306+ pub fn fetch_branch ( cwd : & str , branch_name : & str ) -> GitResult {
307+ fetch_branch_with_pr ( cwd, branch_name, None )
308+ }
309+
310+ /// Fetch a branch, with an optional PR number for fork-based PR fallback.
311+ pub fn fetch_branch_with_pr ( cwd : & str , branch_name : & str , pr_number : Option < u32 > ) -> GitResult {
312+ // Try fetching by branch name first (works for same-repo PRs)
313+ let refspec = format ! (
314+ "refs/heads/{}:refs/remotes/origin/{}" ,
315+ branch_name, branch_name
316+ ) ;
317+ let fetch = run_git ( cwd, & [ "fetch" , "origin" , & refspec] ) ;
318+
319+ if !fetch. success {
320+ if let Some ( pr_num) = pr_number {
321+ // Fallback: fetch via GitHub's PR ref (works for fork-based PRs)
322+ println ! (
323+ "[git] branch {} not found on origin, trying pull/{}/head" ,
324+ branch_name, pr_num
325+ ) ;
326+ let pr_refspec = format ! (
327+ "refs/pull/{}/head:refs/remotes/origin/{}" ,
328+ pr_num, branch_name
329+ ) ;
330+ let pr_fetch = run_git ( cwd, & [ "fetch" , "origin" , & pr_refspec] ) ;
331+ if !pr_fetch. success {
332+ return pr_fetch;
333+ }
334+ } else {
335+ return fetch;
336+ }
337+ }
338+
339+ // Delete stale local branch if it exists (may point to wrong commit)
340+ let _ = run_git ( cwd, & [ "branch" , "-D" , branch_name] ) ;
341+ // Create local branch from the fetched ref
342+ run_git (
343+ cwd,
344+ & [
345+ "branch" ,
346+ branch_name,
347+ & format ! ( "origin/{}" , branch_name) ,
348+ ] ,
349+ )
350+ }
351+
352+ /// Get the remote URL for origin.
353+ pub fn get_remote_url ( cwd : & str ) -> GitResult {
354+ run_git ( cwd, & [ "remote" , "get-url" , "origin" ] )
355+ }
356+
260357/// Generate a branch name for a task.
261358pub fn task_branch_name ( task_id : & str ) -> String {
262359 // Use first 8 chars of UUID for readability
0 commit comments