mirror of
https://seed.radicle.dev/rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt.git
synced 2026-10-30 08:20:48 +08:00
radicle-git init
Port over all git relevant parts of the radicle-link repository[0]. The packages that are kept are: * git-ext * git-ref-format * git-trailers * link-git * macros * std-ext * test -- only the portions that test the above The development and testing scripts are also added for local and CI testing. [0]: https://github.com/radicle-dev/radicle-link/ Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com>
This commit is contained in:
@@ -0,0 +1,290 @@
|
||||
// Copyright © 2021 The Radicle Link Contributors
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
use std::{
|
||||
future::Future,
|
||||
io,
|
||||
mem,
|
||||
pin::Pin,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use bstr::{BString, ByteSlice as _};
|
||||
use futures_lite::{
|
||||
future,
|
||||
io::{AsyncBufRead, AsyncRead, AsyncWrite},
|
||||
};
|
||||
use git_features::progress::{self, Progress};
|
||||
use git_protocol::{
|
||||
fetch::{response, Action, Arguments, Delegate, DelegateBlocking, LsRefsAction, Response},
|
||||
transport::client,
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
use pin_project::{pin_project, pinned_drop};
|
||||
use versions::Version;
|
||||
|
||||
pub use git_hash::ObjectId;
|
||||
pub use git_protocol::fetch::Ref;
|
||||
|
||||
use super::{packwriter::PackWriter, remote_git_version, transport};
|
||||
|
||||
// Work around `git-upload-pack` not handling namespaces properly,
|
||||
//
|
||||
// cf. https://lore.kernel.org/git/CD2XNXHACAXS.13J6JTWZPO1JA@schmidt/
|
||||
// Fixed in `git.git` 1ab13eb, which should land in 2.34
|
||||
//
|
||||
// Based on testing with git 2.25.1 in Ubuntu 20.04, this workaround is
|
||||
// not needed. Hence the checked version is lowered to 2.25.0.
|
||||
fn must_namespace_want_ref(caps: &client::Capabilities) -> bool {
|
||||
static FIXED_AFTER: Lazy<Version> = Lazy::new(|| Version::new("2.25.0").unwrap());
|
||||
|
||||
remote_git_version(caps)
|
||||
.map(|version| version <= *FIXED_AFTER)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Options {
|
||||
/// The remote (logical) repository to fetch from.
|
||||
///
|
||||
/// Normally, this is the path to a repo on the remote side (eg.
|
||||
/// `/git.git`). `radicle-link` serves only a single namespaced repo, so
|
||||
/// this value should be the name of a namespace.
|
||||
pub repo: BString,
|
||||
|
||||
/// [Extra Parameters][extra] to send with the initial transport header.
|
||||
///
|
||||
/// [extra]: https://git.kernel.org/pub/scm/git/git.git/tree/Documentation/technical/pack-protocol.txt#n52
|
||||
pub extra_params: Vec<(String, Option<String>)>,
|
||||
|
||||
/// [`ObjectId`]s to send as `want` lines.
|
||||
pub wants: Vec<ObjectId>,
|
||||
|
||||
/// [`ObjectId`]s to send as `have` lines.
|
||||
pub haves: Vec<ObjectId>,
|
||||
|
||||
/// Known refs to ask the server to include in the packfile.
|
||||
pub want_refs: Vec<BString>,
|
||||
}
|
||||
|
||||
/// Result of a succesful [`fetch`].
|
||||
#[derive(Debug)]
|
||||
pub struct Outputs<T> {
|
||||
/// The `wanted-refs` as acknowledged by the server.
|
||||
pub wanted_refs: Vec<Ref>,
|
||||
/// If a packfile was received successfully, some info about it.
|
||||
pub pack: Option<T>,
|
||||
}
|
||||
|
||||
impl<T> Default for Outputs<T> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
wanted_refs: Vec::new(),
|
||||
pack: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// [`Delegate`] driving the fetch end of the [pack protocol].
|
||||
///
|
||||
/// [pack protocol]: https://git.kernel.org/pub/scm/git/git.git/tree/Documentation/technical/pack-protocol.txt
|
||||
pub struct Fetch<P, O> {
|
||||
opt: Options,
|
||||
pack_writer: P,
|
||||
out: Outputs<O>,
|
||||
need_namespaced_want_ref: bool,
|
||||
}
|
||||
|
||||
impl<P, O> Fetch<P, O> {
|
||||
pub fn new(opt: Options, pack_writer: P) -> Self {
|
||||
Self {
|
||||
opt,
|
||||
pack_writer,
|
||||
out: Outputs::default(),
|
||||
need_namespaced_want_ref: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn outputs(&self) -> &Outputs<O> {
|
||||
&self.out
|
||||
}
|
||||
|
||||
pub fn take_outputs(&mut self) -> Outputs<O> {
|
||||
mem::take(&mut self.out)
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: PackWriter> DelegateBlocking for Fetch<P, P::Output> {
|
||||
fn handshake_extra_parameters(&self) -> Vec<(String, Option<String>)> {
|
||||
self.opt.extra_params.clone()
|
||||
}
|
||||
|
||||
fn prepare_ls_refs(
|
||||
&mut self,
|
||||
_: &client::Capabilities,
|
||||
_: &mut Vec<BString>,
|
||||
_: &mut Vec<(&str, Option<&str>)>,
|
||||
) -> io::Result<LsRefsAction> {
|
||||
Ok(LsRefsAction::Skip)
|
||||
}
|
||||
|
||||
fn prepare_fetch(
|
||||
&mut self,
|
||||
_: git_protocol::transport::Protocol,
|
||||
caps: &client::Capabilities,
|
||||
_: &mut Vec<(&str, Option<&str>)>,
|
||||
_: &[Ref],
|
||||
) -> io::Result<Action> {
|
||||
if !self.opt.want_refs.is_empty() && !remote_supports_ref_in_want(caps) {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
"`want-ref`s given, but server does not support `ref-in-want`",
|
||||
));
|
||||
}
|
||||
|
||||
if self.opt.wants.is_empty() && self.opt.want_refs.is_empty() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"`fetch` is empty",
|
||||
));
|
||||
}
|
||||
|
||||
self.need_namespaced_want_ref = must_namespace_want_ref(caps);
|
||||
|
||||
Ok(Action::Continue)
|
||||
}
|
||||
|
||||
fn negotiate(
|
||||
&mut self,
|
||||
_: &[Ref],
|
||||
args: &mut Arguments,
|
||||
_: Option<&Response>,
|
||||
) -> io::Result<Action> {
|
||||
for oid in &self.opt.wants {
|
||||
args.want(oid);
|
||||
}
|
||||
|
||||
for oid in &self.opt.haves {
|
||||
args.have(oid)
|
||||
}
|
||||
|
||||
for name in &self.opt.want_refs {
|
||||
if self.need_namespaced_want_ref {
|
||||
let want_ref = format!("refs/namespaces/{}/{}", self.opt.repo, name);
|
||||
args.want_ref(BString::from(want_ref).as_bstr());
|
||||
} else {
|
||||
args.want_ref(name.as_bstr());
|
||||
}
|
||||
}
|
||||
|
||||
// send done, as we don't bother with further negotiation
|
||||
Ok(Action::Cancel)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl<P: PackWriter> Delegate for Fetch<P, P::Output> {
|
||||
async fn receive_pack(
|
||||
&mut self,
|
||||
pack: impl AsyncBufRead + Unpin + 'async_trait,
|
||||
prog: impl Progress,
|
||||
_: &[Ref],
|
||||
resp: &Response,
|
||||
) -> io::Result<()> {
|
||||
// Strip any namespaces leaked by the other end due to workarounds
|
||||
let namespace = format!("refs/namespaces/{}/", self.opt.repo);
|
||||
self.out.wanted_refs.extend(resp.wanted_refs().iter().map(
|
||||
|response::WantedRef { id, path }| {
|
||||
Ref::Direct {
|
||||
path: path
|
||||
.strip_prefix(namespace.as_bytes())
|
||||
.map(BString::from)
|
||||
.unwrap_or_else(|| path.clone()),
|
||||
object: *id,
|
||||
}
|
||||
},
|
||||
));
|
||||
let out = self.pack_writer.write_pack(pack, prog)?;
|
||||
self.out.pack = Some(out);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Future created by the [`fetch`] function.
|
||||
///
|
||||
/// Ensures that a running inner [`PackWriter`] is cancelled when the
|
||||
/// [`Fetching`] future is dropped without also dropping the [`AsyncRead`] data
|
||||
/// source.
|
||||
#[pin_project(PinnedDrop)]
|
||||
struct Fetching<T> {
|
||||
stop: Arc<AtomicBool>,
|
||||
#[pin]
|
||||
task: T,
|
||||
}
|
||||
|
||||
#[pinned_drop]
|
||||
impl<T> PinnedDrop for Fetching<T> {
|
||||
fn drop(self: Pin<&mut Self>) {
|
||||
self.stop.store(true, Ordering::Release)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Future for Fetching<T>
|
||||
where
|
||||
T: Future,
|
||||
{
|
||||
type Output = T::Output;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
|
||||
self.project().task.poll(cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fetch<B, P, R, W>(
|
||||
opt: Options,
|
||||
build_pack_writer: B,
|
||||
recv: R,
|
||||
send: W,
|
||||
) -> impl Future<Output = io::Result<Outputs<P::Output>>>
|
||||
where
|
||||
B: FnOnce(Arc<AtomicBool>) -> P,
|
||||
P: PackWriter + Send + 'static,
|
||||
P::Output: Send + 'static,
|
||||
R: AsyncRead + Unpin + Send + 'static,
|
||||
W: AsyncWrite + Unpin + Send + 'static,
|
||||
{
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
let task = blocking::unblock({
|
||||
let mut conn = transport::Stateless::new(opt.repo.clone(), recv, send);
|
||||
let pack_writer = build_pack_writer(Arc::clone(&stop));
|
||||
|
||||
move || {
|
||||
let mut delegate = Fetch::new(opt, pack_writer);
|
||||
future::block_on(git_protocol::fetch(
|
||||
&mut conn,
|
||||
&mut delegate,
|
||||
|_| unreachable!("credentials helper requested"),
|
||||
progress::Discard,
|
||||
git_protocol::FetchConnection::AllowReuse,
|
||||
))
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
||||
|
||||
Ok(delegate.out)
|
||||
}
|
||||
});
|
||||
|
||||
Fetching { stop, task }
|
||||
}
|
||||
|
||||
fn remote_supports_ref_in_want(caps: &client::Capabilities) -> bool {
|
||||
caps.capability("fetch")
|
||||
.and_then(|cap| cap.supports("ref-in-want"))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
// Copyright © 2021 The Radicle Link Contributors
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
use std::io;
|
||||
|
||||
use bstr::{BString, ByteVec as _};
|
||||
use futures_lite::io::{AsyncBufRead, AsyncRead, AsyncWrite};
|
||||
use git_features::progress::{self, Progress};
|
||||
use git_protocol::{
|
||||
fetch::{Action, Arguments, Delegate, DelegateBlocking, LsRefsAction, Response},
|
||||
transport::client,
|
||||
};
|
||||
use once_cell::sync::Lazy;
|
||||
use versions::Version;
|
||||
|
||||
pub use git_protocol::fetch::Ref;
|
||||
|
||||
use super::{remote_git_version, transport};
|
||||
|
||||
// Work around `git-upload-pack` not handling namespaces properly
|
||||
//
|
||||
// cf. https://lore.kernel.org/git/pMV5dJabxOBTD8kJBaPuWK0aS6OJhRQ7YFGwfhPCeSJEbPDrIFBza36nXBCgUCeUJWGmpjPI1rlOGvZJEh71Ruz4SqljndUwOCoBUDRHRDU=@eagain.st/
|
||||
//
|
||||
// Based on testing with git 2.25.1 in Ubuntu 20.04, this workaround is
|
||||
// not needed. Hence the checked version is lowered to 2.25.0.
|
||||
fn must_namespace(caps: &client::Capabilities) -> bool {
|
||||
static MIN_GIT_VERSION_NAMESPACES: Lazy<Version> =
|
||||
Lazy::new(|| Version::new("2.25.0").unwrap());
|
||||
|
||||
remote_git_version(caps)
|
||||
.map(|version| version < *MIN_GIT_VERSION_NAMESPACES)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Options {
|
||||
/// The remote (logical) repository to fetch from.
|
||||
///
|
||||
/// Normally, this is the path to a repo on the remote side (eg.
|
||||
/// `/git.git`). `radicle-link` serves only a single namespaced repo, so
|
||||
/// this value should be the name of a namespace.
|
||||
pub repo: BString,
|
||||
|
||||
/// [Extra Parameters][extra] to send with the initial transport header.
|
||||
///
|
||||
/// [extra]: https://git.kernel.org/pub/scm/git/git.git/tree/Documentation/technical/pack-protocol.txt#n52
|
||||
pub extra_params: Vec<(String, Option<String>)>,
|
||||
|
||||
/// Prefixes of refs to ask the server to advertise via `ls-refs`.
|
||||
///
|
||||
/// If the [`Vec`] is empty, the server is asked to return all refs it knows
|
||||
/// about. Otherwise, the server is asked to only return refs matching
|
||||
/// the given prefixes.
|
||||
pub ref_prefixes: Vec<BString>,
|
||||
}
|
||||
|
||||
/// [`Delegate`] for running a stateless `ls-refs` command.
|
||||
pub struct LsRefs {
|
||||
opt: Options,
|
||||
out: Vec<Ref>,
|
||||
}
|
||||
|
||||
impl LsRefs {
|
||||
pub fn new(opt: Options) -> Self {
|
||||
Self {
|
||||
opt,
|
||||
out: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DelegateBlocking for LsRefs {
|
||||
fn handshake_extra_parameters(&self) -> Vec<(String, Option<String>)> {
|
||||
self.opt.extra_params.clone()
|
||||
}
|
||||
|
||||
fn prepare_ls_refs(
|
||||
&mut self,
|
||||
caps: &client::Capabilities,
|
||||
args: &mut Vec<BString>,
|
||||
_: &mut Vec<(&str, Option<&str>)>,
|
||||
) -> io::Result<LsRefsAction> {
|
||||
let must_namespace = must_namespace(caps);
|
||||
for prefix in &self.opt.ref_prefixes {
|
||||
let mut arg = BString::from("ref-prefix ");
|
||||
if must_namespace {
|
||||
arg.push_str("refs/namespaces/");
|
||||
arg.push_str(&self.opt.repo);
|
||||
arg.push_char('/');
|
||||
}
|
||||
arg.push_str(prefix);
|
||||
args.push(arg)
|
||||
}
|
||||
Ok(LsRefsAction::Continue)
|
||||
}
|
||||
|
||||
fn prepare_fetch(
|
||||
&mut self,
|
||||
_: git_protocol::transport::Protocol,
|
||||
_: &client::Capabilities,
|
||||
_: &mut Vec<(&str, Option<&str>)>,
|
||||
refs: &[Ref],
|
||||
) -> io::Result<Action> {
|
||||
self.out.extend_from_slice(refs);
|
||||
Ok(Action::Cancel)
|
||||
}
|
||||
|
||||
fn negotiate(
|
||||
&mut self,
|
||||
_: &[Ref],
|
||||
_: &mut Arguments,
|
||||
_: Option<&Response>,
|
||||
) -> io::Result<Action> {
|
||||
unreachable!("`negotiate` called even though no `fetch` command was sent")
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl Delegate for LsRefs {
|
||||
async fn receive_pack(
|
||||
&mut self,
|
||||
_: impl AsyncBufRead + Unpin + 'async_trait,
|
||||
_: impl Progress,
|
||||
_: &[Ref],
|
||||
_: &Response,
|
||||
) -> io::Result<()> {
|
||||
unreachable!("`receive_pack` called even though no `fetch` command was sent")
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn ls_refs<R, W>(opt: Options, recv: R, send: W) -> io::Result<Vec<Ref>>
|
||||
where
|
||||
R: AsyncRead + Unpin,
|
||||
W: AsyncWrite + Unpin,
|
||||
{
|
||||
let mut conn = transport::Stateless::new(opt.repo.clone(), recv, send);
|
||||
let mut delegate = LsRefs::new(opt);
|
||||
git_protocol::fetch(
|
||||
&mut conn,
|
||||
&mut delegate,
|
||||
|_| unreachable!("credentials helper requested"),
|
||||
progress::Discard,
|
||||
git_protocol::FetchConnection::AllowReuse,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
||||
|
||||
Ok(delegate.out)
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
// Copyright © 2021 The Radicle Link Contributors
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
use std::{
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
|
||||
use futures_lite::io::{AsyncBufRead, BlockOn};
|
||||
use git_features::progress::Progress;
|
||||
use git_hash::ObjectId;
|
||||
use git_odb::{self as odb, pack};
|
||||
|
||||
use super::take::TryTake;
|
||||
|
||||
#[cfg(feature = "git2")]
|
||||
pub use libgit::Libgit;
|
||||
|
||||
/// What to do with the `packfile` response.
|
||||
///
|
||||
/// _This is mostly the same as [`git_protocol::fetch::Delegate`], but without
|
||||
/// incurring the [`git_protocol::fetch::DelegateBlocking`] super-trait
|
||||
/// constraint. We can simply make [`crate::protocol::fetch::Fetch`] parametric
|
||||
/// over the packfile sink._
|
||||
pub trait PackWriter {
|
||||
type Output;
|
||||
|
||||
fn write_pack(
|
||||
&self,
|
||||
pack: impl AsyncBufRead + Unpin,
|
||||
progress: impl Progress,
|
||||
) -> io::Result<Self::Output>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Options {
|
||||
/// How many threads the packfile indexer is allowed to spawn. `None` means
|
||||
/// unlimited.
|
||||
pub max_indexer_threads: Option<usize>,
|
||||
/// The maximum size in bytes of the packfile.
|
||||
///
|
||||
/// If the remote sends a larger file, the transfer will be aborted.
|
||||
pub max_pack_bytes: u64,
|
||||
}
|
||||
|
||||
impl Default for Options {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_indexer_threads: Some(1),
|
||||
max_pack_bytes: u64::MAX,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "git2")]
|
||||
pub mod libgit {
|
||||
use super::*;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct PackReceived {
|
||||
pub objects: usize,
|
||||
pub local_objects: usize,
|
||||
pub deltas: usize,
|
||||
}
|
||||
|
||||
impl From<git2::Progress<'_>> for PackReceived {
|
||||
fn from(p: git2::Progress<'_>) -> Self {
|
||||
Self {
|
||||
objects: p.indexed_objects(),
|
||||
local_objects: p.local_objects(),
|
||||
deltas: p.indexed_deltas(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Libgit {
|
||||
opt: Options,
|
||||
repo: git2::Repository,
|
||||
stop: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl Libgit {
|
||||
pub fn new(opt: Options, repo: git2::Repository, stop: Arc<AtomicBool>) -> Self {
|
||||
Self { opt, repo, stop }
|
||||
}
|
||||
|
||||
fn guard_cancelled(&self) -> io::Result<()> {
|
||||
if self.stop.load(Ordering::Acquire) {
|
||||
Err(io::Error::new(io::ErrorKind::Interrupted, "cancelled"))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PackWriter for Libgit {
|
||||
type Output = Option<PackReceived>;
|
||||
|
||||
fn write_pack(
|
||||
&self,
|
||||
pack: impl AsyncBufRead + Unpin,
|
||||
_: impl Progress,
|
||||
) -> io::Result<Self::Output> {
|
||||
let mut out = None;
|
||||
|
||||
let odb = self.repo.odb().map_err(io_error)?;
|
||||
let mut writer = odb.packwriter().map_err(io_error)?;
|
||||
|
||||
self.guard_cancelled()?;
|
||||
io::copy(
|
||||
&mut BlockOn::new(TryTake::new(pack, self.opt.max_pack_bytes)),
|
||||
&mut writer,
|
||||
)?;
|
||||
|
||||
self.guard_cancelled()?;
|
||||
writer
|
||||
.progress(|p| {
|
||||
out = Some(p.to_owned());
|
||||
true
|
||||
})
|
||||
.commit()
|
||||
.map(|_| ())
|
||||
.map_err(io_error)?;
|
||||
// Convince borrowchk that `out` can not possibly be borrowed anymore
|
||||
drop(writer);
|
||||
|
||||
Ok(out.map(Into::into))
|
||||
}
|
||||
}
|
||||
|
||||
fn io_error(e: git2::Error) -> io::Error {
|
||||
io::Error::new(io::ErrorKind::Other, e)
|
||||
}
|
||||
}
|
||||
|
||||
pub type PackReceived = pack::bundle::write::Outcome;
|
||||
|
||||
/// A lookup function to help "thicken" thin packs by finding missing base
|
||||
/// objects.
|
||||
///
|
||||
/// The impl provided for [`odb::linked::Store`] does not use any pack caching.
|
||||
pub trait Thickener {
|
||||
fn find_object<'a>(&self, id: ObjectId, buf: &'a mut Vec<u8>)
|
||||
-> Option<pack::data::Object<'a>>;
|
||||
}
|
||||
|
||||
impl Thickener for odb::linked::Store {
|
||||
fn find_object<'a>(
|
||||
&self,
|
||||
id: ObjectId,
|
||||
buf: &'a mut Vec<u8>,
|
||||
) -> Option<pack::data::Object<'a>> {
|
||||
use git_odb::FindExt as _;
|
||||
|
||||
self.find(id, buf, &mut pack::cache::Never).ok()
|
||||
}
|
||||
}
|
||||
|
||||
/// A factory spewing out new [`Thickener`]s with static lifetimes.
|
||||
///
|
||||
/// `gitoxide` doesn't currently allow us to initialise thickening lazily (the
|
||||
/// pack file may not be thin after all), but requires a static lookup function.
|
||||
/// Instead of initialising a new [`odb::linked::Store`] for every pack stream,
|
||||
/// users may share a pre-initialised object database provided appropriate
|
||||
/// thread safety measures.
|
||||
pub trait BuildThickener {
|
||||
type Error: std::error::Error + Send + Sync + 'static;
|
||||
type Thick: Thickener + 'static;
|
||||
|
||||
fn build_thickener(&self) -> Result<Self::Thick, Self::Error>;
|
||||
}
|
||||
|
||||
pub struct StandardThickener {
|
||||
git_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl StandardThickener {
|
||||
pub fn new(git_dir: impl Into<PathBuf>) -> Self {
|
||||
let git_dir = git_dir.into();
|
||||
Self { git_dir }
|
||||
}
|
||||
}
|
||||
|
||||
impl BuildThickener for StandardThickener {
|
||||
type Error = odb::linked::init::Error;
|
||||
type Thick = odb::linked::Store;
|
||||
|
||||
fn build_thickener(&self) -> Result<Self::Thick, Self::Error> {
|
||||
odb::linked::Store::at(self.git_dir.join("objects"))
|
||||
}
|
||||
}
|
||||
|
||||
/// The default [`PackWriter`].
|
||||
///
|
||||
/// Writes the packfile into the given output directory, along with a v2
|
||||
/// index. The packfile is verified.
|
||||
pub struct Standard<F> {
|
||||
git_dir: PathBuf,
|
||||
opt: Options,
|
||||
thick: F,
|
||||
stop: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl<F> Standard<F> {
|
||||
pub fn new(git_dir: impl AsRef<Path>, opt: Options, thick: F, stop: Arc<AtomicBool>) -> Self {
|
||||
Self {
|
||||
git_dir: git_dir.as_ref().to_owned(),
|
||||
opt,
|
||||
thick,
|
||||
stop,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> Drop for Standard<F> {
|
||||
fn drop(&mut self) {
|
||||
self.stop.store(true, Ordering::Release);
|
||||
}
|
||||
}
|
||||
|
||||
impl<F: BuildThickener> PackWriter for Standard<F> {
|
||||
type Output = PackReceived;
|
||||
|
||||
fn write_pack(
|
||||
&self,
|
||||
pack: impl AsyncBufRead + Unpin,
|
||||
prog: impl Progress,
|
||||
) -> io::Result<Self::Output> {
|
||||
use pack::{bundle::write::Options, data::input::Mode, index::Version, Bundle};
|
||||
|
||||
let opts = Options {
|
||||
thread_limit: self.opt.max_indexer_threads,
|
||||
index_kind: Version::V2,
|
||||
iteration_mode: Mode::Verify,
|
||||
};
|
||||
let thickener = self
|
||||
.thick
|
||||
.build_thickener()
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
||||
Bundle::write_to_directory(
|
||||
BlockOn::new(TryTake::new(pack, self.opt.max_pack_bytes)),
|
||||
Some(self.git_dir.join("objects").join("pack")),
|
||||
prog,
|
||||
&self.stop,
|
||||
Some(Box::new(move |oid, buf| thickener.find_object(oid, buf))),
|
||||
opts,
|
||||
)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))
|
||||
}
|
||||
}
|
||||
|
||||
/// No-op [`PackWriter`] which just drains the input.
|
||||
pub struct Discard;
|
||||
|
||||
impl PackWriter for Discard {
|
||||
type Output = u64;
|
||||
|
||||
fn write_pack(
|
||||
&self,
|
||||
pack: impl AsyncBufRead + Unpin,
|
||||
_: impl Progress,
|
||||
) -> io::Result<Self::Output> {
|
||||
io::copy(&mut BlockOn::new(pack), &mut io::sink())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright © 2021 The Radicle Link Contributors
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
use std::{
|
||||
io,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use futures_lite::io::{AsyncBufRead, AsyncRead};
|
||||
|
||||
/// Like [`futures_lite::io::Take`], but returns an error if and when the
|
||||
/// `limit` is exceeded.
|
||||
///
|
||||
/// Note that, unlike [`futures_lite::io::Take`], if a single poll reads past
|
||||
/// the limit, the excess bytes are _not_ discarded. Instead, an error is
|
||||
/// returned on the next poll.
|
||||
pub struct TryTake<R> {
|
||||
limit: u64,
|
||||
inner: R,
|
||||
}
|
||||
|
||||
impl<R> TryTake<R> {
|
||||
pub fn new(inner: R, limit: u64) -> Self {
|
||||
Self { limit, inner }
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> AsyncRead for TryTake<R>
|
||||
where
|
||||
R: AsyncRead + Unpin,
|
||||
{
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context,
|
||||
buf: &mut [u8],
|
||||
) -> Poll<Result<usize, io::Error>> {
|
||||
if self.limit == 0 {
|
||||
return Poll::Ready(Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"max input size exceeded",
|
||||
)));
|
||||
}
|
||||
|
||||
let this = self.get_mut();
|
||||
Pin::new(&mut this.inner).poll_read(cx, buf).map(|ready| {
|
||||
if let Ok(siz) = ready {
|
||||
this.limit = this.limit.saturating_sub(siz as u64);
|
||||
}
|
||||
|
||||
ready
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> AsyncBufRead for TryTake<R>
|
||||
where
|
||||
R: AsyncBufRead + Unpin,
|
||||
{
|
||||
fn poll_fill_buf(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Result<&[u8], io::Error>> {
|
||||
if self.limit == 0 {
|
||||
return Poll::Ready(Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"max input size exceeded",
|
||||
)));
|
||||
}
|
||||
|
||||
Pin::new(&mut self.get_mut().inner).poll_fill_buf(cx)
|
||||
}
|
||||
|
||||
fn consume(self: Pin<&mut Self>, amt: usize) {
|
||||
Pin::new(&mut self.get_mut().inner).consume(amt)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// Copyright © 2021 The Radicle Link Contributors
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
use bstr::BString;
|
||||
use futures_lite::io::{AsyncRead, AsyncWrite};
|
||||
use git_protocol::transport::{
|
||||
client::{
|
||||
self,
|
||||
git::{ConnectMode, Connection},
|
||||
SetServiceResponse,
|
||||
Transport,
|
||||
TransportWithoutIO,
|
||||
},
|
||||
Protocol,
|
||||
Service,
|
||||
};
|
||||
|
||||
pub struct Stateless<R, W> {
|
||||
inner: Connection<R, W>,
|
||||
}
|
||||
|
||||
impl<R, W> Stateless<R, W>
|
||||
where
|
||||
R: AsyncRead + Unpin,
|
||||
W: AsyncWrite + Unpin,
|
||||
{
|
||||
pub fn new(repo: BString, recv: R, send: W) -> Self {
|
||||
let url = format!("rad://{}", repo);
|
||||
let inner = Connection::new(
|
||||
recv,
|
||||
send,
|
||||
Protocol::V2,
|
||||
repo,
|
||||
None::<(String, Option<u16>)>,
|
||||
ConnectMode::Daemon,
|
||||
)
|
||||
.custom_url(Some(url));
|
||||
|
||||
Self { inner }
|
||||
}
|
||||
}
|
||||
|
||||
impl<R, W> TransportWithoutIO for Stateless<R, W>
|
||||
where
|
||||
R: AsyncRead + Unpin,
|
||||
W: AsyncWrite + Unpin,
|
||||
{
|
||||
fn request(
|
||||
&mut self,
|
||||
write_mode: client::WriteMode,
|
||||
on_into_read: client::MessageKind,
|
||||
) -> Result<client::RequestWriter<'_>, client::Error> {
|
||||
self.inner.request(write_mode, on_into_read)
|
||||
}
|
||||
|
||||
fn to_url(&self) -> String {
|
||||
self.inner.to_url()
|
||||
}
|
||||
|
||||
fn supported_protocol_versions(&self) -> &[Protocol] {
|
||||
&[Protocol::V2]
|
||||
}
|
||||
|
||||
fn connection_persists_across_multiple_requests(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl<R, W> Transport for Stateless<R, W>
|
||||
where
|
||||
R: AsyncRead + Unpin,
|
||||
W: AsyncWrite + Unpin,
|
||||
{
|
||||
async fn handshake<'a>(
|
||||
&mut self,
|
||||
service: Service,
|
||||
extra_parameters: &'a [(&'a str, Option<&'a str>)],
|
||||
) -> Result<SetServiceResponse<'_>, client::Error> {
|
||||
self.inner.handshake(service, extra_parameters).await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
// Copyright © 2021 The Radicle Link Contributors
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
use std::{future::Future, io, path::Path, process::ExitStatus, str::FromStr};
|
||||
|
||||
use async_process::{Command, Stdio};
|
||||
use futures_lite::io::{copy, AsyncBufReadExt as _, AsyncRead, AsyncWrite, BufReader};
|
||||
use futures_util::try_join;
|
||||
use git_packetline::{self as packetline, PacketLineRef};
|
||||
use once_cell::sync::Lazy;
|
||||
use versions::Version;
|
||||
|
||||
mod legacy;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct Header {
|
||||
pub path: String,
|
||||
pub host: Option<(String, Option<u16>)>,
|
||||
pub extra: Vec<(String, Option<String>)>,
|
||||
}
|
||||
|
||||
impl FromStr for Header {
|
||||
type Err = &'static str;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let mut parts = s
|
||||
.strip_prefix("git-upload-pack ")
|
||||
.ok_or("unsupported service")?
|
||||
.split_terminator('\0');
|
||||
|
||||
let path = parts.next().ok_or("missing path").and_then(|path| {
|
||||
if path.is_empty() {
|
||||
Err("empty path")
|
||||
} else {
|
||||
Ok(path.to_owned())
|
||||
}
|
||||
})?;
|
||||
let host = match parts.next() {
|
||||
None | Some("") => None,
|
||||
Some(host) => match host.strip_prefix("host=") {
|
||||
None => return Err("invalid host"),
|
||||
Some(host) => match host.split_once(':') {
|
||||
None => Some((host.to_owned(), None)),
|
||||
Some((host, port)) => {
|
||||
let port = port.parse::<u16>().or(Err("invalid port"))?;
|
||||
Some((host.to_owned(), Some(port)))
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
let extra = parts
|
||||
.skip_while(|part| part.is_empty())
|
||||
.map(|part| match part.split_once('=') {
|
||||
None => (part.to_owned(), None),
|
||||
Some((k, v)) => (k.to_owned(), Some(v.to_owned())),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Self { path, host, extra })
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn upload_pack<R, W>(
|
||||
git_dir: impl AsRef<Path>,
|
||||
recv: R,
|
||||
mut send: W,
|
||||
) -> io::Result<(Header, impl Future<Output = io::Result<ExitStatus>>)>
|
||||
where
|
||||
R: AsyncRead + Unpin,
|
||||
W: AsyncWrite + Unpin,
|
||||
{
|
||||
let mut recv = BufReader::new(recv);
|
||||
let header: Header = match recv.fill_buf().await?.first() {
|
||||
// legacy clients don't send a proper pktline header :(
|
||||
Some(b'g') => {
|
||||
let mut buf = String::with_capacity(256);
|
||||
recv.read_line(&mut buf).await?;
|
||||
buf.parse().map_err(invalid_data)?
|
||||
},
|
||||
Some(_) => {
|
||||
let mut pktline = packetline::StreamingPeekableIter::new(recv, &[]);
|
||||
let pkt = pktline
|
||||
.read_line()
|
||||
.await
|
||||
.ok_or_else(|| invalid_data("missing header"))?
|
||||
.map_err(invalid_data)?
|
||||
.map_err(invalid_data)?;
|
||||
let hdr = match pkt {
|
||||
PacketLineRef::Data(data) => std::str::from_utf8(data)
|
||||
.map_err(invalid_data)?
|
||||
.parse()
|
||||
.map_err(invalid_data),
|
||||
_ => Err(invalid_data("not a header packet")),
|
||||
}?;
|
||||
recv = pktline.into_inner();
|
||||
|
||||
hdr
|
||||
},
|
||||
None => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::UnexpectedEof,
|
||||
"expected header",
|
||||
))
|
||||
},
|
||||
};
|
||||
|
||||
let namespace = header
|
||||
.path
|
||||
// legacy clients redundantly send a full URN
|
||||
.strip_prefix("rad:git:")
|
||||
.map(ToOwned::to_owned)
|
||||
.unwrap_or_else(|| header.path.clone());
|
||||
let protocol_version = header
|
||||
.extra
|
||||
.iter()
|
||||
.find_map(|kv| match kv {
|
||||
(ref k, Some(v)) if k == "version" => {
|
||||
let version = match v.as_str() {
|
||||
"2" => 2,
|
||||
"1" => 1,
|
||||
_ => 0,
|
||||
};
|
||||
Some(version)
|
||||
},
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(0);
|
||||
// legacy
|
||||
let stateless_ls = header.extra.iter().any(|(k, _)| k == "ls");
|
||||
|
||||
let fut = async move {
|
||||
if protocol_version < 2 {
|
||||
if stateless_ls {
|
||||
return legacy::advertise_refs(git_dir, &namespace, recv, send).await;
|
||||
}
|
||||
} else {
|
||||
advertise_capabilities(&mut send).await?;
|
||||
}
|
||||
|
||||
let mut child = {
|
||||
let mut cmd = Command::new("git");
|
||||
cmd.current_dir(git_dir)
|
||||
.env_clear()
|
||||
.envs(
|
||||
std::env::vars()
|
||||
.filter(|(key, _)| key == "PATH" || key.starts_with("GIT_TRACE")),
|
||||
)
|
||||
.env("GIT_PROTOCOL", format!("version={}", protocol_version))
|
||||
.env("GIT_NAMESPACE", namespace)
|
||||
.args(&[
|
||||
"-c",
|
||||
"uploadpack.allowanysha1inwant=true",
|
||||
"-c",
|
||||
"uploadpack.allowrefinwant=true",
|
||||
"-c",
|
||||
"lsrefs.unborn=ignore",
|
||||
"upload-pack",
|
||||
"--strict",
|
||||
"--stateless-rpc",
|
||||
".",
|
||||
])
|
||||
.stdout(Stdio::piped())
|
||||
.stdin(Stdio::piped())
|
||||
.stderr(Stdio::inherit())
|
||||
.kill_on_drop(true)
|
||||
.reap_on_drop(true)
|
||||
.spawn()?
|
||||
};
|
||||
|
||||
let mut stdin = child.stdin.take().unwrap();
|
||||
let mut stdout = child.stdout.take().unwrap();
|
||||
|
||||
try_join!(
|
||||
copy(&mut recv, &mut stdin),
|
||||
copy(&mut stdout, &mut send),
|
||||
child.status(),
|
||||
)
|
||||
.map(|(_, _, status)| status)
|
||||
};
|
||||
|
||||
Ok((header, fut))
|
||||
}
|
||||
|
||||
async fn advertise_capabilities<W>(mut send: W) -> io::Result<()>
|
||||
where
|
||||
W: AsyncWrite + Unpin,
|
||||
{
|
||||
// Thou shallt not upgrade your `git` installation while a link instance is
|
||||
// running!
|
||||
static GIT_VERSION: Lazy<Version> = Lazy::new(|| git_version().unwrap());
|
||||
static AGENT: Lazy<Vec<u8>> = Lazy::new(|| format!("agent=git/{}", *GIT_VERSION).into_bytes());
|
||||
static CAPABILITIES: Lazy<[&[u8]; 4]> = Lazy::new(|| {
|
||||
[
|
||||
b"version 2",
|
||||
AGENT.as_slice(),
|
||||
b"object-format=sha1",
|
||||
b"fetch=ref-in-want",
|
||||
]
|
||||
});
|
||||
|
||||
for cap in *CAPABILITIES {
|
||||
packetline::encode::text_to_write(cap, &mut send).await?;
|
||||
}
|
||||
packetline::encode::flush_to_write(&mut send).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn git_version() -> io::Result<Version> {
|
||||
let out = std::process::Command::new("git")
|
||||
.arg("--version")
|
||||
.output()?;
|
||||
if !out.status.success() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"failed to read `git` version",
|
||||
));
|
||||
}
|
||||
|
||||
// parse: git version 2.30.1 <other optional tokens>
|
||||
out.stdout
|
||||
.split(|x| x == &b' ')
|
||||
.nth(2)
|
||||
.and_then(|s| {
|
||||
let s = std::str::from_utf8(s).ok()?;
|
||||
Version::new(s.trim())
|
||||
})
|
||||
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "failed to parse `git` version"))
|
||||
}
|
||||
|
||||
fn invalid_data<E>(inner: E) -> io::Error
|
||||
where
|
||||
E: Into<Box<dyn std::error::Error + Sync + Send>>,
|
||||
{
|
||||
io::Error::new(io::ErrorKind::InvalidData, inner)
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
|
||||
// Copyright © 2021 The Radicle Link Contributors
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
use std::{io, path::Path, process::ExitStatus};
|
||||
|
||||
use async_process::{Command, Stdio};
|
||||
use futures_lite::io::{copy, AsyncRead, AsyncReadExt as _, AsyncWrite, AsyncWriteExt as _};
|
||||
use futures_util::try_join;
|
||||
use git_ref::{
|
||||
file::{Store as Refdb, WriteReflog},
|
||||
FullName,
|
||||
Reference,
|
||||
};
|
||||
|
||||
pub(super) async fn advertise_refs<R, W>(
|
||||
git_dir: impl AsRef<Path>,
|
||||
namespace: &str,
|
||||
mut recv: R,
|
||||
mut send: W,
|
||||
) -> io::Result<ExitStatus>
|
||||
where
|
||||
R: AsyncRead + Unpin,
|
||||
W: AsyncWrite + Unpin,
|
||||
{
|
||||
let unhide = blocking::unblock({
|
||||
let git_dir = git_dir.as_ref().to_path_buf();
|
||||
let prefix = Path::new("refs")
|
||||
.join("namespaces")
|
||||
.join(namespace)
|
||||
.join("refs");
|
||||
move || -> io::Result<Vec<FullName>> {
|
||||
let refdb = Refdb::at(git_dir, WriteReflog::Disable);
|
||||
let packed = refdb
|
||||
.packed_buffer()
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
||||
|
||||
let refs = refdb
|
||||
.iter_prefixed(packed.as_ref(), prefix)?
|
||||
.filter_map(|r| r.ok().map(|Reference { name, .. }| name))
|
||||
.filter(|name| {
|
||||
const PATTERN: &[u8] = b"rad/ids/any";
|
||||
const SEPARAT: u8 = b'/';
|
||||
name.as_bstr()
|
||||
.rsplit(|b| b == &SEPARAT)
|
||||
.zip(PATTERN.rsplit(|b| b == &SEPARAT))
|
||||
.skip(1)
|
||||
.all(|(a, b)| a == b)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(refs)
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
let mut child = {
|
||||
let mut cmd = Command::new("git");
|
||||
cmd.current_dir(git_dir)
|
||||
.env_clear()
|
||||
.envs(std::env::vars().filter(|(key, _)| key == "PATH" || key.starts_with("GIT_TRACE")))
|
||||
.arg("-c")
|
||||
.arg("uploadpack.hiderefs=refs/")
|
||||
.arg("-c")
|
||||
.arg(format!(
|
||||
"uploadpack.hiderefs=!refs/namespaces/{}",
|
||||
namespace
|
||||
));
|
||||
|
||||
for r in unhide {
|
||||
cmd.arg("-c")
|
||||
.arg(format!("uploadpack.hiderefs=!{}", r.as_bstr()));
|
||||
}
|
||||
|
||||
cmd.args(&[
|
||||
"upload-pack",
|
||||
"--strict",
|
||||
"--timeout=5",
|
||||
"--stateless-rpc",
|
||||
"--advertise-refs",
|
||||
".",
|
||||
])
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::inherit())
|
||||
.kill_on_drop(true)
|
||||
.reap_on_drop(true)
|
||||
.spawn()?
|
||||
};
|
||||
let mut stdout = child.stdout.take().unwrap();
|
||||
|
||||
const HEADER: &[u8] = b"001e# service=git-upload-pack\n0000";
|
||||
send.write_all(HEADER).await?;
|
||||
let status = try_join!(copy(&mut stdout, &mut send), child.status()).map(|x| x.1);
|
||||
|
||||
// Read one byte off the read stream to ensure it is driven to completion
|
||||
// (we expect EOF immediately). Failure to do so may cause resource leaks.
|
||||
//
|
||||
// Cf. 900b6cf6 (replication: Ensure git stream is closed, 2021-04-26)
|
||||
let mut buf = [0; 1];
|
||||
recv.read(&mut buf).await?;
|
||||
|
||||
status
|
||||
}
|
||||
Reference in New Issue
Block a user