use crate::bare::GitBare; use crate::error::GitResult; use crate::pb::*; impl GitBare { /// Update mirror from a remote URL (fetch + update all refs). pub fn update_remote_mirror( &self, request: UpdateRemoteMirrorRequest, ) -> GitResult { crate::sanitize::validate_remote_url(&request.remote_url)?; let remote_name = if request.remote_name.is_empty() { "origin" } else { &request.remote_name }; crate::sanitize::validate_ref_name(remote_name)?; const MAX_REFSPECS: usize = 100; if request.refspecs.len() > MAX_REFSPECS { return Err(crate::error::GitError::InvalidArgument(format!( "too many refspecs (max {MAX_REFSPECS})" ))); } for rs in &request.refspecs { crate::sanitize::validate_refspec(rs)?; } let remote_exists = std::process::Command::new("git") .args([ "--git-dir", &self.bare_dir.to_string_lossy(), "remote", "get-url", remote_name, ]) .output() .map(|output| output.status.success()) .unwrap_or(false); let remote_command = if remote_exists { "set-url" } else { "add" }; let remote_output = std::process::Command::new("git") .args([ "--git-dir", &self.bare_dir.to_string_lossy(), "remote", remote_command, remote_name, &request.remote_url, ]) .output() .map_err(|e| crate::error::GitError::CommandFailed { status_code: None, stderr: e.to_string(), })?; if !remote_output.status.success() { return Ok(UpdateRemoteMirrorResponse { ok: false, error: String::from_utf8_lossy(&remote_output.stderr) .trim() .to_string(), }); } let mut fetch_args = vec![ "--git-dir".to_string(), self.bare_dir.to_string_lossy().into_owned(), "fetch".to_string(), remote_name.to_string(), ]; if request.prune { fetch_args.push("--prune".to_string()); } if request.force { fetch_args.push("--force".to_string()); } if request.refspecs.is_empty() { fetch_args.push("+refs/*:refs/*".to_string()); } else { for rs in &request.refspecs { fetch_args.push(rs.clone()); } } let output = std::process::Command::new("git") .args(&fetch_args) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .output() .map_err(|e| crate::error::GitError::CommandFailed { status_code: None, stderr: e.to_string(), })?; if !output.status.success() { return Ok(UpdateRemoteMirrorResponse { ok: false, error: String::from_utf8_lossy(&output.stderr).trim().to_string(), }); } // Update local HEAD to match remote HEAD let head_output = std::process::Command::new("git") .args([ "--git-dir", &self.bare_dir.to_string_lossy(), "remote", "set-head", remote_name, "--auto", ]) .output(); if let Ok(out) = head_output && !out.status.success() { tracing::warn!( repo = %self.bare_dir.display(), stderr = %String::from_utf8_lossy(&out.stderr).trim(), "failed to auto-set remote HEAD" ); } Ok(UpdateRemoteMirrorResponse { ok: true, error: String::new(), }) } /// Fetch from a remote URL without mirroring. pub fn fetch_remote(&self, request: FetchRemoteRequest) -> GitResult { crate::sanitize::validate_remote_url(&request.remote_url)?; let remote_name = if request.remote_name.is_empty() { "origin" } else { &request.remote_name }; crate::sanitize::validate_ref_name(remote_name)?; const MAX_FETCH_REFSPECS: usize = 100; if request.refspecs.len() > MAX_FETCH_REFSPECS { return Err(crate::error::GitError::InvalidArgument(format!( "too many refspecs (max {MAX_FETCH_REFSPECS})" ))); } for rs in &request.refspecs { crate::sanitize::validate_refspec(rs)?; } let exists = std::process::Command::new("git") .args([ "--git-dir", &self.bare_dir.to_string_lossy(), "remote", "get-url", remote_name, ]) .output() .map(|o| o.status.success()) .unwrap_or(false); if !exists { let remote_output = std::process::Command::new("git") .args([ "--git-dir", &self.bare_dir.to_string_lossy(), "remote", "add", remote_name, &request.remote_url, ]) .output() .map_err(|e| crate::error::GitError::CommandFailed { status_code: None, stderr: e.to_string(), })?; if !remote_output.status.success() { return Ok(FetchRemoteResponse { ok: false, error: String::from_utf8_lossy(&remote_output.stderr) .trim() .to_string(), }); } } let mut args = vec![ "--git-dir".to_string(), self.bare_dir.to_string_lossy().into_owned(), "fetch".to_string(), remote_name.to_string(), ]; if request.prune { args.push("--prune".to_string()); } if request.force { args.push("--force".to_string()); } if request.refspecs.is_empty() { args.push("+refs/heads/*:refs/heads/*".to_string()); args.push("+refs/tags/*:refs/tags/*".to_string()); } else { for rs in &request.refspecs { args.push(rs.clone()); } } let output = std::process::Command::new("git") .args(&args) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .output() .map_err(|e| crate::error::GitError::CommandFailed { status_code: None, stderr: e.to_string(), })?; if !output.status.success() { return Ok(FetchRemoteResponse { ok: false, error: String::from_utf8_lossy(&output.stderr).trim().to_string(), }); } Ok(FetchRemoteResponse { ok: true, error: String::new(), }) } /// Clone a repository from a remote URL (bare + mirror). pub fn create_repository_from_url(&self, remote_url: &str, mirror: bool) -> GitResult<()> { crate::sanitize::validate_remote_url(remote_url)?; let mut args = vec!["clone".to_string()]; args.push("--bare".to_string()); if mirror { args.push("--mirror".to_string()); } args.push(remote_url.to_string()); args.push(self.bare_dir.to_string_lossy().into_owned()); let result = duct::cmd("git", &args) .stdout_capture() .stderr_capture() .unchecked() .run() .map_err(|e| crate::error::GitError::CommandFailed { status_code: None, stderr: e.to_string(), })?; if !result.status.success() { return Err(crate::error::GitError::CommandFailed { status_code: result.status.code(), stderr: String::from_utf8_lossy(&result.stderr).into_owned(), }); } Ok(()) } }