53b979f
//! gitcli: run the system `git` via `duct` for operations gix does not expose
//! (gc, prune, pack, patch apply, archive, remote fetch/push/clone).
//!
//! All invocations are scoped to the repository's `--git-dir` and never use a
//! shell, so there is no shell-injection surface. Arguments are passed as
//! separate argv elements.
use std::path::Path;
use crate::Error;
use crate::remote::types::RemoteAuth;
/// Run `git` with the given args against `repo_path`, returning stdout.
pub fn run(repo_path: &Path, args: &[&str]) -> Result<String, Error> {
let out = duct::cmd("git", args)
.env("GIT_DIR", repo_path)
.env("GIT_TERMINAL_PROMPT", "0")
.read()
.map_err(|e| Error::Other(format!("git command failed: {e}")))?;
Ok(out)
}
/// Run `git` with the given args against `repo_path`, returning raw bytes
/// (binary-safe, e.g. for `git archive`).
pub fn run_bytes(repo_path: &Path, args: &[&str]) -> Result<Vec<u8>, Error> {
let out = duct::cmd("git", args)
.env("GIT_DIR", repo_path)
.env("GIT_TERMINAL_PROMPT", "0")
.stdout_capture()
.stderr_capture()
.run()
.map_err(|e| Error::Other(format!("git command failed: {e}")))?;
if !out.status.success() {
return Err(Error::Other(format!(
"git command failed: {}",
String::from_utf8_lossy(&out.stderr)
)));
}
Ok(out.stdout)
}
/// Run `git` with the given args against `repo_path`, returning exit status.
pub fn run_status(repo_path: &Path, args: &[&str]) -> Result<bool, Error> {
let output = duct::cmd("git", args)
.env("GIT_DIR", repo_path)
.env("GIT_TERMINAL_PROMPT", "0")
.run()
.map_err(|e| Error::Other(format!("git command failed: {e}")))?;
Ok(output.status.success())
}
/// Run a remote `git` command (fetch/push/clone) with the given auth.
///
/// Token auth is supplied via `GIT_ASKPASS` pointing to a temp helper that
/// writes the token to stdout, so the token never appears in argv or in the
/// URL. The helper is removed afterwards.
pub fn run_remote(repo_path: &Path, args: &[&str], auth: &RemoteAuth) -> Result<String, Error> {
let mut cmd = duct::cmd("git", args)
.env("GIT_DIR", repo_path)
.env("GIT_TERMINAL_PROMPT", "0");
let mut askpass = None;
match auth {
RemoteAuth::None | RemoteAuth::SshKey(_) => {}
RemoteAuth::Token(token) => {
let helper = write_askpass_helper(token)?;
let helper_path = helper.path.clone();
cmd = cmd
.env("GIT_ASKPASS", &helper_path)
.env("GIT_TERMINAL_PROMPT", "0");
askpass = Some(helper);
}
}
let out = cmd
.read()
.map_err(|e| Error::Other(format!("git command failed: {e}")))?;
if let Some(h) = askpass.as_mut() {
let _ = std::fs::remove_file(&h.path);
}
Ok(out)
}
/// A temporary GIT_ASKPASS helper on disk.
struct AskPassHelper {
path: std::path::PathBuf,
}
impl Drop for AskPassHelper {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.path);
}
}
/// Write a temp script that echoes the token when git prompts for credentials.
fn write_askpass_helper(token: &str) -> Result<AskPassHelper, Error> {
let dir = std::env::temp_dir();
let path = dir.join(format!("git-askpass-{}.sh", std::process::id()));
let script = format!(
"#!/bin/sh\nprintf '%s\\n' '{}'\n",
token.replace('\'', "'\\''")
);
std::fs::write(&path, script).map_err(|e| Error::Io(e.to_string()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o700));
}
Ok(AskPassHelper { path })
}