Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
radicle: Handle `GIT_DIR` more uniformly
Lorenz Leutgeb committed 7 months ago
commit f857a310df1e11da3bac97da48b1e481335138ab
parent f765395c834acc259df3df1fed0eca2aa38aee1a
7 files changed +100 -94
modified crates/radicle-cli/src/commands/patch/checkout.rs
@@ -125,14 +125,26 @@ fn find_patch_commit<'a>(
    working: &'a git::raw::Repository,
) -> anyhow::Result<git::raw::Commit<'a>> {
    let head = *revision.head();
-
    let workdir = working
-
        .workdir()
-
        .ok_or(anyhow::anyhow!("repository is a bare git repository "))?;

    match working.find_commit(head) {
        Ok(commit) => Ok(commit),
        Err(e) if git::ext::is_not_found_err(&e) => {
-
            git::process::fetch_local(workdir, stored, [head.into()], git::Verbosity::default())?;
+
            let output = git::process::fetch_local(
+
                Some(working.path()),
+
                stored,
+
                [head.into()],
+
                git::Verbosity::default(),
+
            )?;
+

+
            if !output.status.success() {
+
                anyhow::bail!(
+
                    "`git fetch` exited with status {}, stderr and stdout follow:\n{}\n{}\n",
+
                    output.status,
+
                    String::from_utf8_lossy(&output.stderr),
+
                    String::from_utf8_lossy(&output.stdout)
+
                );
+
            }
+

            working.find_commit(head).map_err(|e| e.into())
        }
        Err(e) => Err(e.into()),
modified crates/radicle-cli/src/git.rs
@@ -137,7 +137,7 @@ pub fn git<S: AsRef<std::ffi::OsStr>>(
    repo: &std::path::Path,
    args: impl IntoIterator<Item = S>,
) -> anyhow::Result<std::process::Output> {
-
    let output = radicle::git::run::<_, _, &str, &str>(repo, args, [])?;
+
    let output = radicle::git::run(Some(repo), args)?;

    if !output.status.success() {
        anyhow::bail!(
modified crates/radicle-remote-helper/src/fetch.rs
@@ -1,5 +1,4 @@
use std::io;
-
use std::path::Path;
use std::str::FromStr;

use thiserror::Error;
@@ -28,7 +27,6 @@ pub enum Error {
/// Run a git fetch command.
pub fn run<R: ReadRepository>(
    mut refs: Vec<(git::Oid, git::RefString)>,
-
    working: &Path,
    stored: R,
    stdin: &io::Stdin,
    verbosity: Verbosity,
@@ -54,6 +52,9 @@ pub fn run<R: ReadRepository>(
    // Verify them and prepare the final refspecs.
    let oids = refs.into_iter().map(|(oid, _)| oid);

+
    // Rely on the environment variable `GIT_DIR` pointing at the repository.
+
    let working = None;
+

    // N.b. we shell out to `git`, avoiding using `git2`. This is to
    // avoid an issue where somewhere within the fetch there is an
    // attempt to lookup a `rad/sigrefs` object, which says that the
modified crates/radicle-remote-helper/src/main.rs
@@ -243,14 +243,7 @@ pub fn run(profile: radicle::Profile) -> Result<(), Error> {
                let oid = git::Oid::from_str(oid)?;
                let refstr = git::RefString::try_from(*refstr)?;

-
                // N.b. `working` is the `.git` folder and `fetch::run`
-
                // requires the working directory.
-
                let working = dunce::canonicalize(working.map_err(|_| Error::NoGitDir)?)?;
-
                let working = working.parent().ok_or_else(|| Error::NoWorkingCopy {
-
                    path: working.clone(),
-
                })?;
-

-
                return fetch::run(vec![(oid, refstr)], working, stored, &stdin, opts.verbosity)
+
                return fetch::run(vec![(oid, refstr)], stored, &stdin, opts.verbosity)
                    .map_err(Error::from);
            }
            ["push", refspec] => {
modified crates/radicle-remote-helper/src/push.rs
@@ -502,7 +502,7 @@ where
    //
    // In case the reference is not properly deleted, the next attempt to open a patch should
    // not fail, since the reference will already exist with the correct OID.
-
    push_ref(src, &dst, false, working, stored.raw(), opts.verbosity)?;
+
    push_ref(src, &dst, false, stored.raw(), opts.verbosity)?;

    let (_, target) = stored.canonical_head()?;
    let base = if let Some(base) = opts.base {
@@ -616,7 +616,7 @@ where
    let commit = *src;
    let dst = dst.with_namespace(nid.into());

-
    push_ref(src, &dst, force, working, stored.raw(), opts.verbosity)?;
+
    push_ref(src, &dst, force, stored.raw(), opts.verbosity)?;

    let Ok(Some(patch)) = patches.get(&patch_id) else {
        return Err(Error::NotFound(patch_id));
@@ -696,7 +696,7 @@ where
    // It's ok for the destination reference to be unknown, eg. when pushing a new branch.
    let old = stored.backend.find_reference(dst.as_str()).ok();

-
    push_ref(src, &dst, force, working, stored.raw(), verbosity)?;
+
    push_ref(src, &dst, force, stored.raw(), verbosity)?;

    if let Some(old) = old {
        let proj = stored.project()?;
@@ -877,7 +877,6 @@ fn push_ref(
    src: &git::Oid,
    dst: &git::Namespaced,
    force: bool,
-
    working: &git::raw::Repository,
    stored: &git::raw::Repository,
    verbosity: Verbosity,
) -> Result<(), Error> {
@@ -885,7 +884,6 @@ fn push_ref(
    // Nb. The *force* indicator (`+`) is processed by Git tooling before we even reach this code.
    // This happens during the `list for-push` phase.
    let refspec = git::Refspec { src, dst, force };
-
    let repo = working.workdir().unwrap_or_else(|| working.path());

    let mut args = vec![
        "push".to_string(),
@@ -904,7 +902,10 @@ fn push_ref(

    args.extend([url.to_string(), refspec.to_string()]);

-
    let output = radicle::git::run::<_, _, &str, &str>(repo, args, [])?;
+
    // Rely on the environment variable `GIT_DIR`.
+
    let working = None;
+

+
    let output = radicle::git::run(working, args)?;

    if !output.status.success() {
        return Err(Error::InternalPushFailed {
modified crates/radicle/src/git.rs
@@ -756,23 +756,24 @@ pub fn head_refname(repo: &git2::Repository) -> Result<Option<String>, git2::Err
    }
}

-
/// Execute a git command by spawning a child process.
-
pub fn run<P, S, K, V>(
-
    repo: P,
+
/// Execute a `git` command by spawning a child process and collect its output.
+
/// If `working` is [`Some`], the command is run as if `git` was started in
+
/// `working` instead of the current working directory, by prepending
+
/// `-C <working>` to the command line.
+
pub fn run<S>(
+
    working: Option<&std::path::Path>,
    args: impl IntoIterator<Item = S>,
-
    envs: impl IntoIterator<Item = (K, V)>,
) -> io::Result<std::process::Output>
where
-
    P: AsRef<Path>,
    S: AsRef<std::ffi::OsStr>,
-
    K: AsRef<std::ffi::OsStr>,
-
    V: AsRef<std::ffi::OsStr>,
{
-
    Command::new("git")
-
        .current_dir(repo)
-
        .envs(envs)
-
        .args(args)
-
        .output()
+
    let mut cmd = Command::new("git");
+

+
    if let Some(working) = working {
+
        cmd.arg("-C").arg(dunce::canonicalize(working)?);
+
    }
+

+
    cmd.args(args).output()
}

/// Functions that call to the `git` CLI instead of `git2`.
@@ -789,7 +790,7 @@ pub mod process {
    /// `oids` are the set of [`Oid`]s that are being fetched from the
    /// `storage`.
    pub fn fetch_local<R>(
-
        working: &Path,
+
        working: Option<&Path>,
        storage: &R,
        oids: impl IntoIterator<Item = Oid>,
        verbosity: Verbosity,
@@ -797,16 +798,15 @@ pub mod process {
    where
        R: ReadRepository,
    {
-
        let mut fetch = vec![
+
        let mut args = vec![
            "fetch".to_string(),
            // Avoid writing fetch head since we're only fetching objects
            "--no-write-fetch-head".to_string(),
        ];
-
        fetch.extend(verbosity.into_flag());
-
        fetch.push(url::File::new(storage.path()).to_string());
-
        fetch.extend(oids.into_iter().map(|oid| oid.to_string()));
-
        // N.b. `.` is used since we're fetching within the working copy
-
        run::<_, _, &str, &str>(working, fetch, [])
+
        args.extend(verbosity.into_flag());
+
        args.push(url::File::new(storage.path()).to_string());
+
        args.extend(oids.into_iter().map(|oid| oid.to_string()));
+
        run(working, args)
    }
}

modified crates/radicle/src/rad.rs
@@ -1,6 +1,6 @@
#![allow(clippy::let_unit_value)]
use std::io;
-
use std::path::{Path, PathBuf};
+
use std::path::Path;
use std::str::FromStr;
use std::sync::LazyLock;

@@ -32,10 +32,6 @@ pub static PATCHES_REFNAME: LazyLock<git::RefString> =

#[derive(Error, Debug)]
pub enum InitError {
-
    #[error(
-
        "the Git repository found at {path:?} is a bare repository, expected a working directory"
-
    )]
-
    BareRepository { path: PathBuf },
    #[error("doc: {0}")]
    Doc(#[from] DocError),
    #[error("repository: {0}")]
@@ -115,23 +111,22 @@ where
    git::configure_repository(repo)?;
    git::configure_remote(repo, &REMOTE_NAME, url, &url.clone().with_namespace(*pk))?;
    let branch = git::Qualified::from(git::fmt::lit::refs_heads(default_branch));
-
    // Pushes to default branch to the namespace of the `signer`
-
    let pushspec = git::Refspec {
-
        src: branch.clone(),
-
        dst: branch.with_namespace(git::Component::from(pk)),
-
        force: false,
-
    };
-
    git::run::<_, _, &str, &str>(
-
        repo.workdir().ok_or(InitError::BareRepository {
-
            path: repo.path().to_path_buf(),
-
        })?,
-
        [
-
            "push",
-
            &format!("{}", dunce::canonicalize(stored.path())?.display()),
-
            &pushspec.to_string(),
-
        ],
-
        [],
-
    )?;
+

+
    {
+
        // Push branch to storage.
+
        let stored = dunce::canonicalize(stored.path())?.display().to_string();
+

+
        // Pushes to default branch to the namespace of the `signer`.
+
        let pushspec = git::Refspec {
+
            src: branch.clone(),
+
            dst: branch.with_namespace(git::Component::from(pk)),
+
            force: false,
+
        }
+
        .to_string();
+

+
        git::run(Some(repo.path()), ["push".to_string(), stored, pushspec])?;
+
    }
+

    // N.b. we need to create the remote branch for the default branch
    let rad_remote =
        git::Qualified::from(git::lit::refs_remotes(&*REMOTE_COMPONENT)).join(default_branch);
@@ -231,12 +226,14 @@ where

#[derive(Error, Debug)]
pub enum CheckoutError {
-
    #[error(
-
        "the Git repository found at {path:?} is a bare repository, expected a working directory"
-
    )]
-
    BareRepository { path: PathBuf },
-
    #[error("failed to fetch to working copy")]
-
    Fetch(#[source] std::io::Error),
+
    #[error("failed to fetch to working copy: {0}")]
+
    FetchIo(#[source] std::io::Error),
+
    #[error("internal fetch failed with exit status {status}, stderr and stdout follow:\n{stderr}\n{stdout}")]
+
    FetchGit {
+
        status: std::process::ExitStatus,
+
        stderr: String,
+
        stdout: String,
+
    },
    #[error("git: {0}")]
    Git(#[from] git2::Error),
    #[error("payload: {0}")]
@@ -277,33 +274,35 @@ pub fn checkout<P: AsRef<Path>, S: storage::ReadStorage>(
        &url,
        &url.clone().with_namespace(*remote),
    )?;
-
    let fetchspec = git::Refspec {
-
        src: git::refspec::pattern!("refs/heads/*"),
-
        dst: git::Qualified::from(git::lit::refs_remotes(&*REMOTE_NAME))
-
            .to_pattern(git::refspec::STAR)
-
            .into_patternstring(),
-
        force: false,
-
    };
-
    let stored = storage.repository(proj)?;
-
    let workdir = repo.workdir().ok_or(CheckoutError::BareRepository {
-
        path: repo.path().to_path_buf(),
-
    })?;

-
    git::run::<_, _, &str, &str>(
-
        workdir,
-
        [
-
            "fetch",
-
            &format!(
-
                "{}",
-
                dunce::canonicalize(stored.path())
-
                    .map_err(CheckoutError::Fetch)?
-
                    .display()
-
            ),
-
            &fetchspec.to_string(),
-
        ],
-
        [],
-
    )
-
    .map_err(CheckoutError::Fetch)?;
+
    {
+
        // Fetch remote head to working copy.
+

+
        let fetchspec = git::Refspec {
+
            src: git::refspec::pattern!("refs/heads/*"),
+
            dst: git::Qualified::from(git::lit::refs_remotes(&*REMOTE_NAME))
+
                .to_pattern(git::refspec::STAR)
+
                .into_patternstring(),
+
            force: false,
+
        }
+
        .to_string();
+

+
        let stored = dunce::canonicalize(storage.repository(proj)?.path())
+
            .map_err(CheckoutError::FetchIo)?
+
            .display()
+
            .to_string();
+

+
        let output = git::run(Some(repo.path()), ["fetch", &stored, &fetchspec])
+
            .map_err(CheckoutError::FetchIo)?;
+

+
        if !output.status.success() {
+
            return Err(CheckoutError::FetchGit {
+
                status: output.status,
+
                stdout: String::from_utf8_lossy(&output.stdout).to_string(),
+
                stderr: String::from_utf8_lossy(&output.stderr).to_string(),
+
            });
+
        }
+
    }

    {
        // Setup default branch.