radicle: namespace peers
The previous iteration of the storage layout for projects organised peer's subtrees by the `refs/remotes` category. This layout leads to problems when linking a working copy to the storage project. The fetch refspec would look something like: fetch = +refs/remotes/abc/heads/*:refs/remotes/rad/* This works perfectly fine when running: git fetch rad but will fail when one tries to specify: git fetch rad main Instead, the command would need to be: git fetch remotes/abc/heads/main This patch proposes to move the use of `refs/namespaces` for each peer. The layout looks similar in that each peer has a subtree, but instead it is under `refs/namespaces/<peer>/refs/...`. The previous refspec would look more familiar in its new form: fetch = +refs/heads/*:refs/remotes/rad/* With this in place running: git --namespace=abc rad main Will work as expected and place the reference under `refs/remotes/rad/main`. Similary, `git --namespace=abc push` will work as expected. Another benefit of this approach is that the use of namespacing means that a tag is pushed it will be placed under the respective namespace rather than in the global `refs/tags` category. The actual implementation needed to work around the fact that `git2`/`libgit2` does not have any options for fetching or pushing using namespaces. The working copy code uses processes to shell out to `git` for namespacing needs. Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com>
This commit is contained in:
parent
50dd817762
commit
df15b497ab
|
|
@ -216,12 +216,11 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.11"
|
||||
version = "0.8.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51887d4adc7b564537b15adcfb307936f8075dfcd5f00dde9a9f1d29383682bc"
|
||||
checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -287,9 +286,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "ed25519-compact"
|
||||
version = "1.0.12"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c25036262e9b9c81fe4c6decb438f753f66a8f06aac5dbe9eb2b28355c85c3f5"
|
||||
checksum = "bee9df587982575886a8682edcee11877894349a805f25629c27f63abe3e9ae8"
|
||||
dependencies = [
|
||||
"ct-codecs",
|
||||
"getrandom",
|
||||
|
|
@ -417,14 +416,13 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.48"
|
||||
version = "0.1.50"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "237a0714f28b1ee39ccec0770ccb544eb02c9ef2c82bb096230eefcffa6468b0"
|
||||
checksum = "fd911b35d940d2bd0bea0f9100068e5b97b51a1cbe13d13382f132e0365257a0"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"js-sys",
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
"winapi",
|
||||
]
|
||||
|
|
@ -466,9 +464,9 @@ checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"
|
|||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.24"
|
||||
version = "0.1.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa"
|
||||
checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
|
@ -502,9 +500,9 @@ checksum = "478ee9e62aaeaf5b140bd4138753d1f109765488581444218d3ddda43234f3e8"
|
|||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.133"
|
||||
version = "0.2.134"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966"
|
||||
checksum = "329c933548736bc49fd575ee68c89e8be4d260064184389a5b77517cddd99ffb"
|
||||
|
||||
[[package]]
|
||||
name = "libgit2-sys"
|
||||
|
|
@ -641,9 +639,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "olpc-cjson"
|
||||
version = "0.1.1"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72ca49fe685014bbf124ee547da94ed7bb65a6eb9dc9c4711773c081af96a39c"
|
||||
checksum = "87dc75cf72208cd853671c1abccc5d5d1e43b1e378dde67340ef933219a8c13c"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
|
@ -670,9 +668,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
|||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.75"
|
||||
version = "0.9.76"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5f9bd0c2710541a3cda73d6f9ac4f1b240de4ae261065d309dbe73d9dceb42f"
|
||||
checksum = "5230151e44c0f05157effb743e8d517472843121cf9243e8b81393edb5acd9ce"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"cc",
|
||||
|
|
@ -728,9 +726,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.43"
|
||||
version = "1.0.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab"
|
||||
checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
|
@ -910,18 +908,18 @@ checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
|
|||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.144"
|
||||
version = "1.0.145"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860"
|
||||
checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.144"
|
||||
version = "1.0.145"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00"
|
||||
checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
@ -1037,9 +1035,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.100"
|
||||
version = "1.0.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52205623b1b0f064a4e71182c3b18ae902267282930c6d5462c91b859668426e"
|
||||
checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
@ -1062,18 +1060,18 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.35"
|
||||
version = "1.0.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c53f98874615aea268107765aa1ed8f6116782501d18e53d08b471733bea6c85"
|
||||
checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.35"
|
||||
version = "1.0.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8b463991b4eab2d801e724172285ec4195c650e8ec79b149e6c2a8e6dd3f783"
|
||||
checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ fn main() -> anyhow::Result<()> {
|
|||
let profile = radicle::Profile::load()?;
|
||||
let (_, id) = radicle::rad::remote(&repo)?;
|
||||
|
||||
let output = radicle::git::run(&cwd, &["push", "rad"])?;
|
||||
let output = radicle::git::run::<_, _, &str, &str>(&cwd, &["push", "rad"], None)?;
|
||||
println!("{}", output);
|
||||
|
||||
let project = profile.storage.repository(id)?;
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ pub use ext::NotFound;
|
|||
pub use ext::Oid;
|
||||
pub use git2 as raw;
|
||||
pub use git_ref_format as fmt;
|
||||
pub use git_ref_format::{refname, Component, Qualified, RefStr, RefString};
|
||||
pub use git_ref_format::{lit, name, refname, Component, Namespaced, Qualified, RefStr, RefString};
|
||||
pub use git_url as url;
|
||||
pub use git_url::Url;
|
||||
pub use radicle_git_ext as ext;
|
||||
|
|
@ -33,6 +33,14 @@ pub enum RefError {
|
|||
InvalidName(format::RefString),
|
||||
#[error("invalid ref format: {0}")]
|
||||
Format(#[from] format::Error),
|
||||
#[error("expected ref to begin with 'refs/namespaces' but found '{0}'")]
|
||||
MissingNamespace(format::RefString),
|
||||
#[error("ref name contains invalid namespace identifier '{name}'")]
|
||||
Id {
|
||||
name: format::RefString,
|
||||
#[source]
|
||||
err: Box<dyn std::error::Error + Send + Sync + 'static>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
|
|
@ -43,9 +51,11 @@ pub enum ListRefsError {
|
|||
InvalidRef(#[from] RefError),
|
||||
}
|
||||
|
||||
impl From<&RemoteId> for RefString {
|
||||
impl<'a> From<&RemoteId> for Component<'a> {
|
||||
fn from(id: &RemoteId) -> Self {
|
||||
RefString::try_from(id.to_string()).expect("encoded public keys are valid ref strings")
|
||||
let refstr =
|
||||
RefString::try_from(id.to_string()).expect("encoded public keys are valid ref strings");
|
||||
Component::from_refstring(refstr).expect("encoded public keys are valid refname components")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -56,17 +66,19 @@ pub mod refs {
|
|||
pub static IDENTITY_BRANCH: Lazy<RefString> = Lazy::new(|| refname!("radicle/id"));
|
||||
|
||||
pub mod storage {
|
||||
|
||||
use super::*;
|
||||
|
||||
pub fn branch(remote: &RemoteId, branch: &RefStr) -> RefString {
|
||||
refname!("refs/remotes")
|
||||
.and::<RefString>(remote.into())
|
||||
.and(refname!("heads"))
|
||||
.and(branch)
|
||||
/// Create the [`Namespaced`] `branch` under the `remote` namespace, i.e.
|
||||
/// `refs/namespaces/<remote>/refs/heads/<branch>`
|
||||
pub fn branch<'a>(remote: &RemoteId, branch: &RefStr) -> Namespaced<'a> {
|
||||
Qualified::from(git_ref_format::lit::refs_heads(branch)).add_namespace(remote.into())
|
||||
}
|
||||
|
||||
/// Get the branch used to track project information.
|
||||
pub fn id(remote: &RemoteId) -> RefString {
|
||||
///
|
||||
/// `refs/namespaces/<remote>/refs/heads/radicle/id`
|
||||
pub fn id(remote: &RemoteId) -> Namespaced {
|
||||
branch(remote, &IDENTITY_BRANCH)
|
||||
}
|
||||
}
|
||||
|
|
@ -74,18 +86,22 @@ pub mod refs {
|
|||
pub mod workdir {
|
||||
use super::*;
|
||||
|
||||
/// Create a [`RefString`] that corresponds to `refs/heads/<branch>`.
|
||||
pub fn branch(branch: &RefStr) -> RefString {
|
||||
refname!("refs/heads").join(branch)
|
||||
}
|
||||
|
||||
/// Create a [`RefString`] that corresponds to `refs/notes/<name>`.
|
||||
pub fn note(name: &RefStr) -> RefString {
|
||||
refname!("refs/notes").join(name)
|
||||
}
|
||||
|
||||
/// Create a [`RefString`] that corresponds to `refs/remotes/<remote>/<branch>`.
|
||||
pub fn remote_branch(remote: &RefStr, branch: &RefStr) -> RefString {
|
||||
refname!("refs/remotes").and(remote).and(branch)
|
||||
}
|
||||
|
||||
/// Create a [`RefString`] that corresponds to `refs/tags/<branch>`.
|
||||
pub fn tag(name: &RefStr) -> RefString {
|
||||
refname!("refs/tags").join(name)
|
||||
}
|
||||
|
|
@ -105,27 +121,34 @@ pub fn remote_refs(url: &Url) -> Result<HashMap<RemoteId, Refs>, ListRefsError>
|
|||
let (id, refname) = parse_ref::<PublicKey>(r.name())?;
|
||||
let entry = remotes.entry(id).or_insert_with(Refs::default);
|
||||
|
||||
entry.insert(refname, r.oid().into());
|
||||
entry.insert(refname.into(), r.oid().into());
|
||||
}
|
||||
|
||||
Ok(remotes)
|
||||
}
|
||||
|
||||
/// Parse a ref string.
|
||||
pub fn parse_ref<T: FromStr>(s: &str) -> Result<(T, format::RefString), RefError> {
|
||||
pub fn parse_ref<T>(s: &str) -> Result<(T, format::Qualified), RefError>
|
||||
where
|
||||
T: FromStr,
|
||||
T::Err: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
let input = format::RefStr::try_from_str(s)?;
|
||||
let suffix = input
|
||||
.strip_prefix(format::refname!("refs/remotes"))
|
||||
.ok_or_else(|| RefError::InvalidName(input.to_owned()))?;
|
||||
|
||||
let mut components = suffix.components();
|
||||
let id = components
|
||||
.next()
|
||||
.ok_or_else(|| RefError::InvalidName(input.to_owned()))?;
|
||||
let id = T::from_str(&id.to_string()).map_err(|_| RefError::InvalidName(input.to_owned()))?;
|
||||
let refstr = components.collect::<format::RefString>();
|
||||
|
||||
Ok((id, refstr))
|
||||
match input.namespaced() {
|
||||
None => Err(RefError::MissingNamespace(input.to_owned())),
|
||||
Some(ns) => {
|
||||
let id = ns
|
||||
.namespace()
|
||||
.as_str()
|
||||
.parse()
|
||||
.map_err(|err| RefError::Id {
|
||||
name: input.to_owned(),
|
||||
err: Box::new(err),
|
||||
})?;
|
||||
let rest = ns.strip_namespace();
|
||||
Ok((id, rest))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an initial empty commit.
|
||||
|
|
@ -158,24 +181,6 @@ pub fn commit<'a>(
|
|||
Ok(commit)
|
||||
}
|
||||
|
||||
/// Push the refs to the radicle remote.
|
||||
pub fn push(repo: &git2::Repository) -> Result<(), git2::Error> {
|
||||
// NOTE: This function is going away soon.
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
let mut remote = repo.find_remote("rad")?;
|
||||
let refspecs = remote.push_refspecs()?;
|
||||
let refspec = refspecs.into_iter().next().unwrap().unwrap();
|
||||
|
||||
// The `git2` crate doesn't seem to support push refspecs with '*' in them,
|
||||
// so we manually replace it with the current branch.
|
||||
let head = repo.head()?;
|
||||
let branch = head.shorthand().unwrap();
|
||||
let refspec = refspec.replace('*', branch);
|
||||
|
||||
remote.push::<&str>(&[&refspec], None)
|
||||
}
|
||||
|
||||
/// Get the repository head.
|
||||
pub fn head(repo: &git2::Repository) -> Result<git2::Commit, git2::Error> {
|
||||
let head = repo.head()?.peel_to_commit()?;
|
||||
|
|
@ -201,22 +206,62 @@ pub fn write_tree<'r>(
|
|||
|
||||
/// Configure a repository's radicle remote.
|
||||
///
|
||||
/// Takes the repository in which to configure the remote, the name of the remote, the public
|
||||
/// key of the remote, and the path to the remote repository on the filesystem.
|
||||
/// The entry for this remote will be:
|
||||
/// ```text
|
||||
/// [remote.<name>]
|
||||
/// url = <url>
|
||||
/// fetch +refs/heads/*:refs/remotes/<name>/*
|
||||
/// ```
|
||||
pub fn configure_remote<'r>(
|
||||
repo: &'r git2::Repository,
|
||||
remote_name: &str,
|
||||
remote_id: &RemoteId,
|
||||
remote_url: &Url,
|
||||
name: &str,
|
||||
url: &Url,
|
||||
) -> Result<git2::Remote<'r>, git2::Error> {
|
||||
let fetch = format!("+refs/remotes/{remote_id}/heads/*:refs/remotes/rad/*");
|
||||
let push = format!("refs/heads/*:refs/remotes/{remote_id}/heads/*");
|
||||
let remote = repo.remote_with_fetch(remote_name, remote_url.to_string().as_str(), &fetch)?;
|
||||
repo.remote_add_push(remote_name, &push)?;
|
||||
let fetch = format!("+refs/heads/*:refs/remotes/{name}/*");
|
||||
let remote = repo.remote_with_fetch(name, url.to_string().as_str(), &fetch)?;
|
||||
|
||||
Ok(remote)
|
||||
}
|
||||
|
||||
/// Fetch from the given `remote` using the provided `namespace`.
|
||||
///
|
||||
/// This uses [`Command`] under the hood and is the equivalent to:
|
||||
///
|
||||
/// `GIT_NAMESPACE=<namespace> git fetch <remote>`
|
||||
pub fn fetch(repo: &git2::Repository, remote: &str, namespace: &RemoteId) -> io::Result<String> {
|
||||
run(
|
||||
&repo.path(),
|
||||
["fetch", remote],
|
||||
[("GIT_NAMESPACE", Component::from(namespace).as_str())],
|
||||
)
|
||||
}
|
||||
|
||||
/// Push `refspecs` to the given `remote` using the provided `namespace`.
|
||||
///
|
||||
/// This uses [`Command`] under the hood and is the equivalent to:
|
||||
///
|
||||
/// `GIT_NAMESPACE=<namespace> git push <remote> [<refspecs>]`
|
||||
pub fn push<Ref>(
|
||||
repo: &git2::Repository,
|
||||
remote: &str,
|
||||
namespace: &RemoteId,
|
||||
refspecs: impl IntoIterator<Item = (Ref, Ref)>,
|
||||
) -> io::Result<String>
|
||||
where
|
||||
Ref: AsRef<RefStr>,
|
||||
{
|
||||
let mut args = vec!["push".to_owned(), remote.to_owned()];
|
||||
let refspecs = refspecs
|
||||
.into_iter()
|
||||
.map(|(src, dst)| format!("{}:{}", src.as_ref().as_str(), dst.as_ref().as_str()));
|
||||
args.extend(refspecs);
|
||||
run(
|
||||
&repo.path(),
|
||||
args,
|
||||
[("GIT_NAMESPACE", Component::from(namespace).as_str())],
|
||||
)
|
||||
}
|
||||
|
||||
/// Set the upstream of the given branch to the given remote.
|
||||
///
|
||||
/// This writes to the `config` directly. The entry will look like the
|
||||
|
|
@ -258,11 +303,22 @@ pub fn set_upstream(
|
|||
}
|
||||
|
||||
/// Execute a git command by spawning a child process.
|
||||
pub fn run<P: AsRef<Path>, S: AsRef<std::ffi::OsStr>>(
|
||||
pub fn run<P, S, K, V>(
|
||||
repo: &P,
|
||||
args: impl IntoIterator<Item = S>,
|
||||
) -> Result<String, io::Error> {
|
||||
let output = Command::new("git").current_dir(repo).args(args).output()?;
|
||||
envs: impl IntoIterator<Item = (K, V)>,
|
||||
) -> Result<String, io::Error>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
S: AsRef<std::ffi::OsStr>,
|
||||
K: AsRef<std::ffi::OsStr>,
|
||||
V: AsRef<std::ffi::OsStr>,
|
||||
{
|
||||
let output = Command::new("git")
|
||||
.current_dir(repo)
|
||||
.envs(envs)
|
||||
.args(args)
|
||||
.output()?;
|
||||
|
||||
if output.status.success() {
|
||||
let out = if output.stdout.is_empty() {
|
||||
|
|
|
|||
|
|
@ -358,8 +358,8 @@ impl Doc<Unverified> {
|
|||
|
||||
impl<V> Doc<V> {
|
||||
pub fn head<R: ReadRepository>(remote: &RemoteId, repo: &R) -> Result<Oid, DocError> {
|
||||
let head = &git::refname!("heads").join(&*git::refs::IDENTITY_BRANCH);
|
||||
repo.reference_oid(remote, head).map_err(DocError::from)
|
||||
let head = git::Qualified::from(git::lit::refs_heads(&*git::refs::IDENTITY_BRANCH));
|
||||
repo.reference_oid(remote, &head).map_err(DocError::from)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ use std::io;
|
|||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
|
||||
use git_ref_format::{lit, Qualified};
|
||||
use once_cell::sync::Lazy;
|
||||
use thiserror::Error;
|
||||
|
||||
|
|
@ -68,20 +69,11 @@ pub fn init<G: Signer, S: storage::WriteStorage>(
|
|||
repo,
|
||||
&REMOTE_NAME,
|
||||
&default_branch,
|
||||
&git::refs::storage::branch(pk, &default_branch),
|
||||
&git::refs::workdir::branch(&default_branch),
|
||||
)?;
|
||||
|
||||
// TODO: Note that you'll likely want to use `RemoteCallbacks` and set
|
||||
// `push_update_reference` to test whether all the references were pushed
|
||||
// successfully.
|
||||
git::configure_remote(repo, &REMOTE_NAME, pk, &url)?.push::<&str>(
|
||||
&[&format!(
|
||||
"{}:{}",
|
||||
&git::refs::workdir::branch(&default_branch),
|
||||
&git::refs::storage::branch(pk, &default_branch),
|
||||
)],
|
||||
None,
|
||||
)?;
|
||||
git::configure_remote(repo, &REMOTE_NAME, &url)?;
|
||||
git::push(repo, &REMOTE_NAME, pk, [(&default_branch, &default_branch)])?;
|
||||
let signed = storage.sign_refs(&project, signer)?;
|
||||
|
||||
Ok((id, signed))
|
||||
|
|
@ -171,10 +163,8 @@ pub fn fork<G: Signer, S: storage::WriteStorage>(
|
|||
let canonical_head = {
|
||||
let mut heads = Vec::new();
|
||||
for delegate in project.delegates.iter() {
|
||||
let name = format!("heads/{}", &project.default_branch);
|
||||
let refname = git::RefString::try_from(name.as_str())?;
|
||||
let refname = Qualified::from(lit::refs_heads(&project.default_branch));
|
||||
let r = repository.reference_oid(&delegate.id, &refname)?.into();
|
||||
|
||||
heads.push(r);
|
||||
}
|
||||
|
||||
|
|
@ -258,6 +248,8 @@ pub fn clone_url<P: AsRef<Path>, G: Signer, S: storage::WriteStorage>(
|
|||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum CheckoutError {
|
||||
#[error("failed to fetch to working copy")]
|
||||
Fetch(#[source] io::Error),
|
||||
#[error("git: {0}")]
|
||||
Git(#[from] git2::Error),
|
||||
#[error("storage: {0}")]
|
||||
|
|
@ -289,12 +281,14 @@ pub fn checkout<P: AsRef<Path>, S: storage::ReadStorage>(
|
|||
let url = storage.url(&proj);
|
||||
|
||||
// Configure and fetch all refs from remote.
|
||||
git::configure_remote(&repo, &REMOTE_NAME, remote, &url)?.fetch::<&str>(&[], None, None)?;
|
||||
git::configure_remote(&repo, &REMOTE_NAME, &url)?;
|
||||
git::fetch(&repo, &REMOTE_NAME, remote).map_err(CheckoutError::Fetch)?;
|
||||
|
||||
{
|
||||
// Setup default branch.
|
||||
let remote_head_ref =
|
||||
git::refs::workdir::remote_branch(&REMOTE_NAME, &project.default_branch);
|
||||
|
||||
let remote_head_commit = repo.find_reference(&remote_head_ref)?.peel_to_commit()?;
|
||||
let _ = repo.branch(&project.default_branch, &remote_head_commit, true)?;
|
||||
|
||||
|
|
@ -303,7 +297,7 @@ pub fn checkout<P: AsRef<Path>, S: storage::ReadStorage>(
|
|||
&repo,
|
||||
&REMOTE_NAME,
|
||||
&project.default_branch,
|
||||
&git::refs::storage::branch(remote, &project.default_branch),
|
||||
&git::refs::workdir::branch(&project.default_branch),
|
||||
)?;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ use crate::crypto;
|
|||
use crate::crypto::{PublicKey, Signer, Unverified, Verified};
|
||||
use crate::git::ext as git_ext;
|
||||
use crate::git::Url;
|
||||
use crate::git::{RefError, RefStr, RefString};
|
||||
use crate::git::{Qualified, RefError, RefString};
|
||||
use crate::identity;
|
||||
use crate::identity::{Id, IdError};
|
||||
use crate::storage::refs::Refs;
|
||||
|
|
@ -54,6 +54,8 @@ pub enum FetchError {
|
|||
Git(#[from] git2::Error),
|
||||
#[error("i/o: {0}")]
|
||||
Io(#[from] io::Error),
|
||||
#[error(transparent)]
|
||||
Refs(#[from] refs::Error),
|
||||
#[error("verify: {0}")]
|
||||
Verify(#[from] git::VerifyError),
|
||||
#[error(transparent)]
|
||||
|
|
@ -174,13 +176,16 @@ pub struct Remote<V> {
|
|||
pub id: PublicKey,
|
||||
/// Git references published under this remote, and their hashes.
|
||||
pub refs: SignedRefs<V>,
|
||||
/// Whether this remote is of a project delegate.
|
||||
/// Whether this remote is a delegate for the project.
|
||||
pub delegate: bool,
|
||||
/// Whether the remote is verified or not, ie. whether its signed refs were checked.
|
||||
verified: PhantomData<V>,
|
||||
}
|
||||
|
||||
impl<V> Remote<V> {
|
||||
// TODO(finto): This function seems out of place in the API for a couple of reasons:
|
||||
// * The SignedRefs aren't guaranteed to be by the `id`
|
||||
// * I could write `Remote::<Verified>::new(id, refs) and because of the above, it's a LIE
|
||||
pub fn new(id: PublicKey, refs: impl Into<SignedRefs<V>>) -> Self {
|
||||
Self {
|
||||
id,
|
||||
|
|
@ -239,17 +244,34 @@ pub trait WriteStorage: ReadStorage {
|
|||
}
|
||||
|
||||
pub trait ReadRepository {
|
||||
/// Returns `true` if there are no references in the repository.
|
||||
fn is_empty(&self) -> Result<bool, git2::Error>;
|
||||
|
||||
/// The [`Path`] to the git repository.
|
||||
fn path(&self) -> &Path;
|
||||
|
||||
fn blob_at<'a>(&'a self, oid: Oid, path: &'a Path) -> Result<git2::Blob<'a>, git_ext::Error>;
|
||||
|
||||
/// Get the `reference` for the given `remote`.
|
||||
///
|
||||
/// Returns `None` is the reference did not exist.
|
||||
fn reference(
|
||||
&self,
|
||||
remote: &RemoteId,
|
||||
reference: &RefStr,
|
||||
reference: &Qualified,
|
||||
) -> Result<git2::Reference, git_ext::Error>;
|
||||
|
||||
/// Get the [`git2::Commit`] found using its `oid`.
|
||||
///
|
||||
/// Returns `None` if the commit did not exist.
|
||||
fn commit(&self, oid: Oid) -> Result<git2::Commit, git_ext::Error>;
|
||||
|
||||
fn revwalk(&self, head: Oid) -> Result<git2::Revwalk, git2::Error>;
|
||||
fn reference_oid(&self, remote: &RemoteId, reference: &RefStr) -> Result<Oid, git_ext::Error>;
|
||||
fn reference_oid(
|
||||
&self,
|
||||
remote: &RemoteId,
|
||||
reference: &Qualified,
|
||||
) -> Result<Oid, git_ext::Error>;
|
||||
fn references(&self, remote: &RemoteId) -> Result<Refs, Error>;
|
||||
fn remote(&self, remote: &RemoteId) -> Result<Remote<Verified>, refs::Error>;
|
||||
fn remotes(&self) -> Result<Remotes<Verified>, refs::Error>;
|
||||
|
|
|
|||
|
|
@ -24,9 +24,9 @@ pub use crate::git::*;
|
|||
use super::{RefUpdate, RemoteId};
|
||||
|
||||
pub static REMOTES_GLOB: Lazy<refspec::PatternString> =
|
||||
Lazy::new(|| refspec::pattern!("refs/remotes/*"));
|
||||
Lazy::new(|| refspec::pattern!("refs/namespaces/*"));
|
||||
pub static SIGNATURES_GLOB: Lazy<refspec::PatternString> =
|
||||
Lazy::new(|| refspec::pattern!("refs/remotes/*/radicle/signature"));
|
||||
Lazy::new(|| refspec::pattern!("refs/namespaces/*/radicle/signature"));
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ProjectError {
|
||||
|
|
@ -254,6 +254,7 @@ impl Repository {
|
|||
let remote = remotes
|
||||
.get_mut(&remote_id)
|
||||
.ok_or(VerifyError::InvalidRemote(remote_id))?;
|
||||
let refname = RefString::from(refname);
|
||||
let signed_oid = remote
|
||||
.remove(&refname)
|
||||
.ok_or_else(|| VerifyError::UnknownRef(remote_id, refname.clone()))?;
|
||||
|
|
@ -396,8 +397,7 @@ impl Repository {
|
|||
|
||||
impl ReadRepository for Repository {
|
||||
fn is_empty(&self) -> Result<bool, git2::Error> {
|
||||
let some = self.remotes()?.next().is_some();
|
||||
Ok(!some)
|
||||
Ok(self.remotes()?.next().is_none())
|
||||
}
|
||||
|
||||
fn path(&self) -> &Path {
|
||||
|
|
@ -415,11 +415,9 @@ impl ReadRepository for Repository {
|
|||
fn reference(
|
||||
&self,
|
||||
remote: &RemoteId,
|
||||
name: &git::RefStr,
|
||||
name: &git::Qualified,
|
||||
) -> Result<git2::Reference, git::Error> {
|
||||
let name = name.strip_prefix(git::refname!("refs")).unwrap_or(name);
|
||||
let name = format!("refs/remotes/{remote}/{name}");
|
||||
|
||||
let name = name.add_namespace(remote.into());
|
||||
self.backend.find_reference(&name).map_err(git::Error::from)
|
||||
}
|
||||
|
||||
|
|
@ -436,7 +434,11 @@ impl ReadRepository for Repository {
|
|||
Ok(revwalk)
|
||||
}
|
||||
|
||||
fn reference_oid(&self, remote: &RemoteId, reference: &git::RefStr) -> Result<Oid, git::Error> {
|
||||
fn reference_oid(
|
||||
&self,
|
||||
remote: &RemoteId,
|
||||
reference: &git::Qualified,
|
||||
) -> Result<Oid, git::Error> {
|
||||
let oid = self
|
||||
.reference(remote, reference)?
|
||||
.target()
|
||||
|
|
@ -454,7 +456,7 @@ impl ReadRepository for Repository {
|
|||
// TODO: Only return known refs, eg. heads/ rad/ tags/ etc..
|
||||
let entries = self
|
||||
.backend
|
||||
.references_glob(format!("refs/remotes/{remote}/*").as_str())?;
|
||||
.references_glob(format!("refs/namespaces/{remote}/*").as_str())?;
|
||||
let mut refs = BTreeMap::new();
|
||||
|
||||
for e in entries {
|
||||
|
|
@ -463,7 +465,7 @@ impl ReadRepository for Repository {
|
|||
let (_, refname) = git::parse_ref::<RemoteId>(name)?;
|
||||
let oid = e.target().ok_or(Error::InvalidRef)?;
|
||||
|
||||
refs.insert(refname, oid.into());
|
||||
refs.insert(refname.into(), oid.into());
|
||||
}
|
||||
Ok(refs.into())
|
||||
}
|
||||
|
|
@ -523,7 +525,7 @@ impl WriteRepository for Repository {
|
|||
// local <- git-fetch -- staging # fetch from staging copy
|
||||
//
|
||||
let url = url.to_string();
|
||||
let refs: &[&str] = &["refs/remotes/*:refs/remotes/*"];
|
||||
let refs: &[&str] = &["refs/namespaces/*:refs/namespaces/*"];
|
||||
let mut updates = Vec::new();
|
||||
let mut callbacks = git2::RemoteCallbacks::new();
|
||||
let tempdir = tempfile::tempdir()?;
|
||||
|
|
@ -692,7 +694,8 @@ mod tests {
|
|||
|
||||
// Strip the remote refs of sigrefs so we can compare them.
|
||||
for remote in refs.values_mut() {
|
||||
remote.remove(&*SIGNATURE_REF).unwrap();
|
||||
let sigref = (*SIGNATURE_REF).to_ref_string();
|
||||
remote.remove(&sigref).unwrap();
|
||||
}
|
||||
|
||||
let remotes = remotes
|
||||
|
|
@ -713,7 +716,7 @@ mod tests {
|
|||
let proj = *inventory.first().unwrap();
|
||||
let repo = alice.repository(proj).unwrap();
|
||||
let remotes = repo.remotes().unwrap().collect::<Vec<_>>();
|
||||
let refname = git::refname!("heads/master");
|
||||
let refname = Qualified::from_refstr(git::refname!("refs/heads/master")).unwrap();
|
||||
|
||||
// Have Bob fetch Alice's refs.
|
||||
let updates = bob
|
||||
|
|
@ -735,7 +738,7 @@ mod tests {
|
|||
for update in updates {
|
||||
assert_matches!(
|
||||
update,
|
||||
RefUpdate::Created { name, .. } if name.starts_with("refs/remotes")
|
||||
RefUpdate::Created { name, .. } if name.starts_with("refs/namespaces")
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -762,7 +765,7 @@ mod tests {
|
|||
let (proj_id, _, proj_repo, alice_head) =
|
||||
fixtures::project(tmp.path().join("alice/project"), &alice, &alice_signer).unwrap();
|
||||
|
||||
let refname = git::refname!("refs/heads/master");
|
||||
let refname = Qualified::from_refstr(git::refname!("refs/heads/master")).unwrap();
|
||||
let alice_url = git::Url {
|
||||
scheme: git_url::Scheme::File,
|
||||
path: paths::repository(&alice, &proj_id)
|
||||
|
|
@ -782,7 +785,7 @@ mod tests {
|
|||
let alice_head = git::commit(&proj_repo, &alice_head, &refname, "Making changes", "Alice")
|
||||
.unwrap()
|
||||
.id();
|
||||
git::push(&proj_repo).unwrap();
|
||||
git::push(&proj_repo, "rad", alice_id, [(&refname, &refname)]).unwrap();
|
||||
alice.sign_refs(&alice_proj_storage, &alice_signer).unwrap();
|
||||
|
||||
// Have Bob fetch Alice's new commit.
|
||||
|
|
@ -894,9 +897,9 @@ mod tests {
|
|||
assert_eq!(
|
||||
updates,
|
||||
vec![
|
||||
format!("refs/remotes/{remote}/heads/master"),
|
||||
format!("refs/remotes/{remote}/heads/radicle/id"),
|
||||
format!("refs/remotes/{remote}/radicle/signature")
|
||||
format!("refs/namespaces/{remote}/refs/heads/master"),
|
||||
format!("refs/namespaces/{remote}/refs/heads/radicle/id"),
|
||||
format!("refs/namespaces/{remote}/refs/radicle/signature")
|
||||
]
|
||||
);
|
||||
}
|
||||
|
|
@ -928,7 +931,8 @@ mod tests {
|
|||
let mut unsigned = project.references(&alice).unwrap();
|
||||
|
||||
// The signed refs doesn't contain the signature ref itself.
|
||||
unsigned.remove(&*SIGNATURE_REF).unwrap();
|
||||
let sigref = (*SIGNATURE_REF).to_ref_string();
|
||||
unsigned.remove(&sigref).unwrap();
|
||||
|
||||
assert_eq!(remote.refs, signed);
|
||||
assert_eq!(*remote.refs, unsigned);
|
||||
|
|
|
|||
|
|
@ -18,7 +18,13 @@ use crate::git::Oid;
|
|||
use crate::storage;
|
||||
use crate::storage::{ReadRepository, RemoteId, WriteRepository};
|
||||
|
||||
pub static SIGNATURE_REF: Lazy<git::RefString> = Lazy::new(|| git::refname!("radicle/signature"));
|
||||
pub static SIGNATURE_REF: Lazy<git::Qualified> = Lazy::new(|| {
|
||||
git::Qualified::from_components(
|
||||
git::name::component!("radicle"),
|
||||
git::name::component!("signature"),
|
||||
None,
|
||||
)
|
||||
});
|
||||
pub const REFS_BLOB_PATH: &str = "refs";
|
||||
pub const SIGNATURE_BLOB_PATH: &str = "signature";
|
||||
|
||||
|
|
@ -124,7 +130,7 @@ impl Refs {
|
|||
let mut buf = String::new();
|
||||
let refs = self
|
||||
.iter()
|
||||
.filter(|(name, oid)| *name != &*SIGNATURE_REF && !oid.is_zero());
|
||||
.filter(|(name, oid)| name.as_refstr() != (*SIGNATURE_REF).as_ref() && !oid.is_zero());
|
||||
|
||||
for (name, oid) in refs {
|
||||
buf.push_str(&oid.to_string());
|
||||
|
|
@ -291,7 +297,7 @@ impl SignedRefs<Verified> {
|
|||
}
|
||||
}
|
||||
|
||||
let sigref = format!("refs/remotes/{remote}/{sigref}");
|
||||
let sigref = sigref.add_namespace(remote.into());
|
||||
let author = repo.raw().signature()?;
|
||||
let commit = repo.raw().commit(
|
||||
Some(&sigref),
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ impl ReadRepository for MockRepository {
|
|||
fn reference(
|
||||
&self,
|
||||
_remote: &RemoteId,
|
||||
_reference: &git::RefStr,
|
||||
_reference: &git::Qualified,
|
||||
) -> Result<git2::Reference, git_ext::Error> {
|
||||
todo!()
|
||||
}
|
||||
|
|
@ -124,7 +124,7 @@ impl ReadRepository for MockRepository {
|
|||
fn reference_oid(
|
||||
&self,
|
||||
_remote: &RemoteId,
|
||||
_reference: &git::RefStr,
|
||||
_reference: &git::Qualified,
|
||||
) -> Result<git_ext::Oid, git_ext::Error> {
|
||||
todo!()
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue