Codex Handbook
core-plugins/src/marketplace_upgrade/git.rs 285 lines
use std::path::Path;use std::path::PathBuf;use std::process::Command;use std::process::Output;use std::process::Stdio;use std::time::Duration;pub(super) fn git_remote_revision(    source: &str,    ref_name: Option<&str>,    timeout: Duration,) -> Result<String, String> {    if let Some(ref_name) = ref_name        && is_full_git_sha(ref_name)    {        return Ok(ref_name.to_string());    }    let ref_name = ref_name.unwrap_or("HEAD");    let output = run_git_command_with_timeout(        git_command().arg("ls-remote").arg(source).arg(ref_name),        "git ls-remote marketplace source",        timeout,    )?;    ensure_git_success(&output, "git ls-remote marketplace source")?;    let stdout = String::from_utf8_lossy(&output.stdout);    let Some(first_line) = stdout.lines().next() else {        return Err("git ls-remote returned empty output for marketplace source".to_string());    };    let Some((revision, _)) = first_line.split_once('\t') else {        return Err(format!(            "unexpected git ls-remote output for marketplace source: {first_line}"        ));    };    let revision = revision.trim();    if revision.is_empty() {        return Err("git ls-remote returned empty revision for marketplace source".to_string());    }    Ok(revision.to_string())}pub(super) fn clone_git_source(    source: &str,    ref_name: Option<&str>,    sparse_paths: &[String],    destination: &Path,    timeout: Duration,) -> Result<String, String> {    let git_destination = git_path_arg(destination);    if sparse_paths.is_empty() {        let output = run_git_command_with_timeout(            git_command().arg("clone").arg(source).arg(&git_destination),            "git clone marketplace source",            timeout,        )?;        ensure_git_success(&output, "git clone marketplace source")?;        if let Some(ref_name) = ref_name {            let output = run_git_command_with_timeout(                git_command()                    .arg("-C")                    .arg(&git_destination)                    .arg("checkout")                    .arg(ref_name),                "git checkout marketplace ref",                timeout,            )?;            ensure_git_success(&output, "git checkout marketplace ref")?;        }        return git_worktree_revision(&git_destination, timeout);    }    let output = run_git_command_with_timeout(        git_command()            .arg("clone")            .arg("--filter=blob:none")            .arg("--no-checkout")            .arg(source)            .arg(&git_destination),        "git clone marketplace source",        timeout,    )?;    ensure_git_success(&output, "git clone marketplace source")?;    let mut sparse_checkout = git_command();    sparse_checkout        .arg("-C")        .arg(&git_destination)        .arg("sparse-checkout")        .arg("set")        .args(sparse_paths);    let output = run_git_command_with_timeout(        &mut sparse_checkout,        "git sparse-checkout marketplace source",        timeout,    )?;    ensure_git_success(&output, "git sparse-checkout marketplace source")?;    let output = run_git_command_with_timeout(        git_command()            .arg("-C")            .arg(&git_destination)            .arg("checkout")            .arg(ref_name.unwrap_or("HEAD")),        "git checkout marketplace ref",        timeout,    )?;    ensure_git_success(&output, "git checkout marketplace ref")?;    git_worktree_revision(&git_destination, timeout)}fn git_worktree_revision(destination: &Path, timeout: Duration) -> Result<String, String> {    let output = run_git_command_with_timeout(        git_command()            .arg("-C")            .arg(destination)            .arg("rev-parse")            .arg("HEAD"),        "git rev-parse marketplace revision",        timeout,    )?;    ensure_git_success(&output, "git rev-parse marketplace revision")?;    let revision = String::from_utf8_lossy(&output.stdout).trim().to_string();    if revision.is_empty() {        Err("git rev-parse returned empty revision for marketplace source".to_string())    } else {        Ok(revision)    }}fn is_full_git_sha(value: &str) -> bool {    value.len() == 40 && value.chars().all(|ch| ch.is_ascii_hexdigit())}fn git_command() -> Command {    let mut command = Command::new("git");    command        .env("GIT_OPTIONAL_LOCKS", "0")        .env("GIT_TERMINAL_PROMPT", "0");    command}#[cfg(windows)]fn git_path_arg(path: &Path) -> PathBuf {    strip_windows_verbatim_path_prefix(&path.to_string_lossy())        .map(PathBuf::from)        .unwrap_or_else(|| path.to_path_buf())}#[cfg(not(windows))]fn git_path_arg(path: &Path) -> PathBuf {    path.to_path_buf()}#[cfg(any(windows, test))]fn strip_windows_verbatim_path_prefix(path: &str) -> Option<String> {    let stripped = path.strip_prefix(r"\\?\")?;    let stripped = stripped        .strip_prefix(r"UNC\")        .map(|unc_path| format!(r"\\{unc_path}"))        .unwrap_or_else(|| stripped.to_string());    Some(stripped)}fn run_git_command_with_timeout(    command: &mut Command,    context: &str,    timeout: Duration,) -> Result<Output, String> {    let mut child = command        .stdin(Stdio::null())        .stdout(Stdio::piped())        .stderr(Stdio::piped())        .spawn()        .map_err(|err| format!("failed to run {context}: {err}"))?;    let start = std::time::Instant::now();    loop {        match child.try_wait() {            Ok(Some(_)) => {                return child                    .wait_with_output()                    .map_err(|err| format!("failed to wait for {context}: {err}"));            }            Ok(None) => {}            Err(err) => return Err(format!("failed to poll {context}: {err}")),        }        if start.elapsed() >= timeout {            let _ = child.kill();            let output = child                .wait_with_output()                .map_err(|err| format!("failed to wait for {context} after timeout: {err}"))?;            let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();            return if stderr.is_empty() {                Err(format!("{context} timed out after {}s", timeout.as_secs()))            } else {                Err(format!(                    "{context} timed out after {}s: {stderr}",                    timeout.as_secs()                ))            };        }        std::thread::sleep(Duration::from_millis(100));    }}fn ensure_git_success(output: &Output, context: &str) -> Result<(), String> {    if output.status.success() {        return Ok(());    }    let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();    if stderr.is_empty() {        Err(format!("{context} failed with status {}", output.status))    } else {        Err(format!(            "{context} failed with status {}: {stderr}",            output.status        ))    }}#[cfg(test)]mod tests {    use super::git_command;    use super::is_full_git_sha;    use super::strip_windows_verbatim_path_prefix;    use pretty_assertions::assert_eq;    use std::ffi::OsStr;    #[test]    fn full_git_sha_ref_is_already_a_remote_revision() {        assert!(is_full_git_sha("0123456789abcdef0123456789abcdef01234567"));        assert!(!is_full_git_sha("main"));        assert!(!is_full_git_sha("0123456"));    }    #[test]    fn git_command_uses_path_lookup_with_stable_noninteractive_env() {        let command = git_command();        assert_eq!(command.get_program(), OsStr::new("git"));        assert_eq!(            command_env(&command, "GIT_OPTIONAL_LOCKS"),            Some(Some(OsStr::new("0")))        );        assert_eq!(            command_env(&command, "GIT_TERMINAL_PROMPT"),            Some(Some(OsStr::new("0")))        );        assert_eq!(command_env(&command, "PATH"), None);    }    #[test]    fn strips_windows_verbatim_disk_prefix_for_git() {        assert_eq!(            strip_windows_verbatim_path_prefix(r"\\?\C:\Users\alice\marketplace"),            Some(r"C:\Users\alice\marketplace".to_string())        );    }    #[test]    fn strips_windows_verbatim_unc_prefix_for_git() {        assert_eq!(            strip_windows_verbatim_path_prefix(r"\\?\UNC\server\share\marketplace"),            Some(r"\\server\share\marketplace".to_string())        );    }    #[test]    fn leaves_non_verbatim_path_without_rewrite() {        assert_eq!(strip_windows_verbatim_path_prefix(r"C:\Users\alice"), None);    }    fn command_env<'a>(        command: &'a std::process::Command,        name: &str,    ) -> Option<Option<&'a OsStr>> {        command            .get_envs()            .find(|(key, _)| key == &OsStr::new(name))            .map(|(_, value)| value)    }}