Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
remote-helper: improve canonical handling
Fintan Halpenny committed 11 months ago
commit 1b8623954357e2df46e477eb1be5249864a4d1e8
parent fc2af6f9cee1dbfa15af31ce8c4490f0eaadc61e
3 files changed +282 -57
modified crates/radicle-remote-helper/src/push.rs
@@ -1,5 +1,6 @@
#![allow(clippy::too_many_arguments)]

+
mod canonical;
mod error;

use std::collections::HashMap;
@@ -17,7 +18,6 @@ use radicle::cob::patch;
use radicle::cob::patch::cache::Patches as _;
use radicle::crypto;
use radicle::explorer::ExplorerResource;
-
use radicle::git::canonical;
use radicle::git::canonical::Canonical;
use radicle::identity::Did;
use radicle::node;
@@ -30,7 +30,7 @@ use radicle::{git, rad};
use radicle_cli as cli;
use radicle_cli::terminal as term;

-
use crate::{hint, read_line, warn, Options};
+
use crate::{hint, read_line, Options};

#[derive(Debug, Error)]
pub enum Error {
@@ -44,9 +44,6 @@ pub enum Error {
    /// No public key is given
    #[error("no public key given as a remote namespace, perhaps you are attempting to push to restricted refs")]
    NoKey,
-
    /// Head being pushed diverges from canonical head.
-
    #[error("refusing to update branch to commit that is not a descendant of canonical head")]
-
    HeadsDiverge(git::Oid, git::Oid),
    /// User tried to delete the canonical branch.
    #[error("refusing to delete default branch ref '{0}'")]
    DeleteForbidden(git::RefString),
@@ -118,6 +115,8 @@ pub enum Error {
    Quorum(#[from] radicle::git::canonical::QuorumError),
    #[error(transparent)]
    PushAction(#[from] error::PushAction),
+
    #[error(transparent)]
+
    Canonical(#[from] error::CanonicalUnrecoverable),
}

/// Push command.
@@ -308,63 +307,16 @@ pub fn run(
                        if dst == canonical_ref && delegates.contains(&me) && delegates.len() > 1 {
                            let head = working.find_reference(src.as_str())?;
                            let head = head.peel_to_commit()?.id();
-

-
                            let mut canonical = Canonical::default_branch(
+
                            let canonical = Canonical::default_branch(
                                stored,
                                &project,
                                identity.delegates().as_ref(),
                                identity.threshold(),
                            )?;
-
                            let converges = canonical::converges(
-
                                canonical
-
                                    .tips()
-
                                    .filter_map(|(did, tip)| (*did != me).then_some(tip)),
-
                                head.into(),
-
                                &working,
-
                            )?;
-
                            if converges {
-
                                canonical.modify_vote(me, head.into());
+
                            let canonical = canonical::Canonical::new(me, head.into(), canonical);
+
                            if let Err(e) = canonical.quorum(&working) {
+
                                canonical::io::handle_error(e, canonical_ref, hints)?;
                            }
-

-
                            match canonical.quorum(&working) {
-
                                Ok(canonical_oid) => {
-
                                    // Canonical head is an ancestor of head.
-
                                    let is_ff = head == *canonical_oid
-
                                        || working.graph_descendant_of(head, *canonical_oid)?;
-

-
                                    if !is_ff && !converges {
-
                                        if hints {
-
                                            hint(
-
                                                "you are attempting to push a commit that would cause \
-
                                                 your upstream to diverge from the canonical head",
-
                                            );
-
                                            hint(
-
                                                "to integrate the remote changes, run `git pull --rebase` \
-
                                                 and try again",
-
                                            );
-
                                        }
-
                                        return Err(Error::HeadsDiverge(
-
                                            head.into(),
-
                                            canonical_oid,
-
                                        ));
-
                                    }
-
                                }
-
                                Err(canonical::QuorumError::Diverging(e)) => {
-
                                    warn(format!(
-
                                        "could not determine canonical tip for `{canonical_ref}`"
-
                                    ));
-
                                    warn(e.to_string());
-
                                    warn("it is recommended to find a commit to agree upon");
-
                                }
-
                                Err(canonical::QuorumError::NoCandidates(e)) => {
-
                                    warn(format!(
-
                                        "could not determine canonical tip for `{canonical_ref}`"
-
                                    ));
-
                                    warn(e.to_string());
-
                                    warn("it is recommended to find a commit to agree upon");
-
                                }
-
                                Err(e) => return Err(e.into()),
-
                            };
                        }
                        push(src, &dst, *force, &nid, &working, stored, patches, &signer)
                    }
added crates/radicle-remote-helper/src/push/canonical.rs
@@ -0,0 +1,153 @@
+
use radicle::git;
+
use radicle::git::canonical::converges;
+
use radicle::git::raw::Repository;
+
use radicle::prelude::Did;
+

+
use super::error;
+

+
/// Compute the canonical commit for a Radicle repository.
+
pub struct Canonical {
+
    me: Did,
+
    head: git::Oid,
+
    canonical: git::canonical::Canonical,
+
}
+

+
impl Canonical {
+
    pub fn new(me: Did, head: git::Oid, canonical: git::canonical::Canonical) -> Self {
+
        Self {
+
            me,
+
            head,
+
            canonical,
+
        }
+
    }
+

+
    /// Calculates the quorum of the [`git::canonical::Canonical`] provided.
+
    ///
+
    /// In some cases, it ensures that the [`head`] is attempting to converge
+
    /// with the set of commits of the other [`Did`]s.
+
    ///
+
    /// If a quorum is found, then it is also ensured that the new [`head`] is a
+
    /// descendant of the current canonical commit, otherwise the commits are
+
    /// considered diverging.
+
    ///
+
    /// # Errors
+
    ///
+
    /// Ensures that the commits of the other [`Did`]s are in the working
+
    /// copy, and that checks that any two commits are related in the graph.
+
    ///
+
    /// Ensures that the new head and the canonical commit do not diverge.
+
    ///
+
    /// [`head`]: crate::push::canonical::Canonical::head
+
    pub fn quorum(mut self, working: &Repository) -> Result<git::Oid, error::Canonical> {
+
        let heads = {
+
            let mut heads = self.canonical.tips();
+
            heads.try_fold(
+
                Vec::with_capacity(heads.size_hint().0),
+
                |mut heads, (did, head)| {
+
                    if *did != self.me {
+
                        heads.push(Self::ensure_commit(*did, *head, working)?)
+
                    }
+
                    Ok::<_, error::Canonical>(heads)
+
                },
+
            )?
+
        };
+
        let converges = converges(heads.iter(), self.head, working)
+
            .map_err(|err| error::Canonical::converges(self.head, err))?;
+
        if converges {
+
            self.canonical.modify_vote(self.me, self.head);
+
        }
+

+
        match self.canonical.quorum(working) {
+
            Ok(canonical_oid) => {
+
                // Canonical head is an ancestor of head.
+
                let is_ff = self.head == canonical_oid
+
                    || working
+
                        .graph_descendant_of(*self.head, *canonical_oid)
+
                        .map_err(|err| {
+
                            error::Canonical::graph_descendant(self.head, canonical_oid, err)
+
                        })?;
+

+
                if !is_ff && !converges {
+
                    Err(error::Canonical::heads_diverge(self.head, canonical_oid))
+
                } else {
+
                    Ok(canonical_oid)
+
                }
+
            }
+
            Err(err) => Err(err.into()),
+
        }
+
    }
+

+
    fn ensure_commit(
+
        from: Did,
+
        commit: git::Oid,
+
        working: &Repository,
+
    ) -> Result<git::Oid, error::Canonical> {
+
        match working.find_commit(*commit).map(|_| commit) {
+
            Ok(oid) => Ok(oid),
+
            Err(err) if err.code() == git::raw::ErrorCode::NotFound => Err(
+
                error::Canonical::missing_commit(working.path().to_path_buf(), from, commit, err),
+
            ),
+
            Err(err) => Err(error::Canonical::invalid_commit(
+
                working.path().to_path_buf(),
+
                from,
+
                commit,
+
                err,
+
            )),
+
        }
+
    }
+
}
+

+
pub mod io {
+
    use radicle::git::{self, canonical};
+

+
    use crate::push::error;
+
    use crate::{hint, warn};
+

+
    /// Handle recoverable errors, printing relevant information to the
+
    /// terminal. Otherwise, convert the error into an unrecoverable error
+
    /// [`error::CanonicalUnrecoverable`].
+
    pub fn handle_error(
+
        e: error::Canonical,
+
        canonical: git::Qualified,
+
        hints: bool,
+
    ) -> Result<(), error::CanonicalUnrecoverable> {
+
        match e {
+
            error::Canonical::MissingCommit(e) => Err(e.into()),
+
            error::Canonical::InvalidCommit(e) => Err(e.into()),
+
            error::Canonical::GraphDescendant(e) => Err(e.into()),
+
            error::Canonical::Converges(e) => Err(e.into()),
+
            error::Canonical::HeadsDiverge(e) => {
+
                if hints {
+
                    hint(
+
                        "you are attempting to push a commit that would cause \
+
                                                 your upstream to diverge from the canonical head",
+
                    );
+
                    hint(
+
                        "to integrate the remote changes, run `git pull --rebase` \
+
                                                 and try again",
+
                    );
+
                }
+
                Err(e.into())
+
            }
+
            error::Canonical::Quorum(e) => match e {
+
                canonical::QuorumError::Diverging(e) => {
+
                    warn(format!(
+
                        "could not determine canonical tip for `{canonical}`"
+
                    ));
+
                    warn(e.to_string());
+
                    warn("it is recommended to find a commit to agree upon");
+
                    Ok(())
+
                }
+
                canonical::QuorumError::NoCandidates(e) => {
+
                    warn(format!(
+
                        "could not determine canonical tip for `{canonical}`"
+
                    ));
+
                    warn(e.to_string());
+
                    warn("it is recommended to find a commit to agree upon");
+
                    Ok(())
+
                }
+
                canonical::QuorumError::Git(err) => Err(error::CanonicalUnrecoverable::Git { err }),
+
            },
+
        }
+
    }
+
}
modified crates/radicle-remote-helper/src/push/error.rs
@@ -1,7 +1,127 @@
-
use radicle::storage::git;
+
use std::path::PathBuf;
+

+
use radicle::git;
+
use radicle::git::canonical;
+
use radicle::prelude::Did;
use thiserror::Error;

#[derive(Debug, Error)]
+
pub enum CanonicalUnrecoverable {
+
    #[error(transparent)]
+
    GraphDescendant(#[from] GraphDescendant),
+
    #[error(transparent)]
+
    Converges(#[from] Converges),
+
    #[error(transparent)]
+
    HeadsDiverge(#[from] HeadsDiverge),
+
    #[error(transparent)]
+
    MissingCommit(#[from] MissingCommit),
+
    #[error(transparent)]
+
    InvalidCommit(#[from] InvalidCommit),
+
    #[error("failure while computing canonical reference: {err}")]
+
    Git {
+
        #[source]
+
        err: git::raw::Error,
+
    },
+
}
+

+
#[derive(Debug, Error)]
+
pub enum Canonical {
+
    #[error(transparent)]
+
    GraphDescendant(GraphDescendant),
+
    #[error(transparent)]
+
    Converges(Converges),
+
    #[error(transparent)]
+
    HeadsDiverge(HeadsDiverge),
+
    #[error(transparent)]
+
    Quorum(#[from] canonical::QuorumError),
+
    #[error(transparent)]
+
    MissingCommit(MissingCommit),
+
    #[error(transparent)]
+
    InvalidCommit(InvalidCommit),
+
}
+

+
impl Canonical {
+
    pub fn converges(head: git::Oid, err: git::raw::Error) -> Self {
+
        Self::Converges(Converges { head, err })
+
    }
+

+
    pub fn graph_descendant(head: git::Oid, canonical: git::Oid, err: git::raw::Error) -> Self {
+
        Self::GraphDescendant(GraphDescendant {
+
            head,
+
            canonical,
+
            err,
+
        })
+
    }
+

+
    pub fn heads_diverge(head: git::Oid, canonical: git::Oid) -> Self {
+
        Self::HeadsDiverge(HeadsDiverge { head, canonical })
+
    }
+

+
    pub fn missing_commit(repo: PathBuf, did: Did, oid: git::Oid, err: git::raw::Error) -> Self {
+
        Self::MissingCommit(MissingCommit {
+
            repo,
+
            did,
+
            oid,
+
            err,
+
        })
+
    }
+

+
    pub fn invalid_commit(repo: PathBuf, did: Did, oid: git::Oid, err: git::raw::Error) -> Self {
+
        Self::InvalidCommit(InvalidCommit {
+
            repo,
+
            did,
+
            oid,
+
            err,
+
        })
+
    }
+
}
+

+
#[derive(Debug, Error)]
+
#[error("the commit {oid} for {did} is missing from the repository {repo:?}")]
+
pub struct MissingCommit {
+
    repo: PathBuf,
+
    did: Did,
+
    oid: git::Oid,
+
    #[source]
+
    err: git::raw::Error,
+
}
+

+
#[derive(Debug, Error)]
+
#[error("could not determine the commit {oid} for {did} is part of the repository {repo:?}")]
+
pub struct InvalidCommit {
+
    repo: PathBuf,
+
    did: Did,
+
    oid: git::Oid,
+
    #[source]
+
    err: git::raw::Error,
+
}
+

+
#[derive(Debug, Error)]
+
#[error("failed to check if {head} is an ancestor of {canonical} due to: {err}")]
+
pub struct GraphDescendant {
+
    head: git::Oid,
+
    canonical: git::Oid,
+
    #[source]
+
    err: git::raw::Error,
+
}
+

+
#[derive(Debug, Error)]
+
#[error("failed to see if {head} converges with other commits due to: {err}")]
+
pub struct Converges {
+
    head: git::Oid,
+
    #[source]
+
    err: git::raw::Error,
+
}
+

+
#[derive(Debug, Error)]
+
/// Head being pushed diverges from canonical head.
+
#[error("refusing to update branch to commit that is not a descendant of canonical head")]
+
pub struct HeadsDiverge {
+
    head: git::Oid,
+
    canonical: git::Oid,
+
}
+

+
#[derive(Debug, Error)]
pub enum PushAction {
    #[error("invalid reference {refname}, expected qualified reference starting with `refs/`")]
    InvalidRef { refname: git::RefString },