mirror of
https://seed.radicle.dev/rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt.git
synced 2026-11-04 19:00:36 +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:
+3
-7
@@ -1,10 +1,6 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
target
|
||||
.vscode
|
||||
tags
|
||||
Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
max_width = 100
|
||||
# yeap
|
||||
comment_width = 80
|
||||
wrap_comments = true
|
||||
hard_tabs = false
|
||||
tab_spaces = 4
|
||||
imports_layout = "HorizontalVertical"
|
||||
imports_granularity = "Crate"
|
||||
|
||||
newline_style = "Unix"
|
||||
use_small_heuristics = "Default"
|
||||
|
||||
reorder_imports = true
|
||||
reorder_modules = true
|
||||
|
||||
remove_nested_parens = true
|
||||
|
||||
fn_args_layout = "Tall"
|
||||
|
||||
edition = "2018"
|
||||
|
||||
match_block_trailing_comma = true
|
||||
|
||||
merge_derives = true
|
||||
|
||||
use_try_shorthand = false
|
||||
use_field_init_shorthand = false
|
||||
|
||||
force_explicit_abi = true
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"git-ext",
|
||||
"git-ref-format",
|
||||
"git-trailers",
|
||||
"link-git",
|
||||
"macros",
|
||||
# TODO: port gitd-lib over
|
||||
# "cli/gitd-lib",
|
||||
"std-ext",
|
||||
"test",
|
||||
]
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
{ sources ? import ./nix/sources.nix
|
||||
, pkgs ? import sources.nixpkgs {
|
||||
overlays = [ (import sources.rust-overlay) ];
|
||||
}
|
||||
, rust-overlay ? pkgs.rust-bin.stable.latest.default
|
||||
}:
|
||||
let
|
||||
# TODO: remove once cargo-nextest is available in nixpkgs stable
|
||||
cargo-nextest = (pkgs.callPackage ./nix/cargo-nextest/default.nix { });
|
||||
in
|
||||
with pkgs;
|
||||
mkShell {
|
||||
name = "build";
|
||||
buildInputs = [
|
||||
# cargo tooling
|
||||
cargo-deny
|
||||
cargo-nextest
|
||||
cargo-watch
|
||||
pkgs.rust-bin.nightly."2022-07-01".rustfmt
|
||||
|
||||
# hard dependencies
|
||||
cmake
|
||||
openssl
|
||||
pkgconfig
|
||||
rust-overlay
|
||||
|
||||
# testing utilities
|
||||
gettext # for `envsubst`
|
||||
socat
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
# This section is considered when running `cargo deny check advisories`
|
||||
# More documentation for the advisories section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
|
||||
[advisories]
|
||||
# The path where the advisory database is cloned/fetched into
|
||||
db-path = "~/cargo/advisory-db"
|
||||
# The url of the advisory database to use
|
||||
db-urls = [ "https://github.com/rustsec/advisory-db" ]
|
||||
# The lint level for security vulnerabilities
|
||||
vulnerability = "deny"
|
||||
# The lint level for unmaintained crates
|
||||
unmaintained = "warn"
|
||||
# The lint level for crates that have been yanked from their source registry
|
||||
yanked = "warn"
|
||||
# The lint level for crates with security notices. Note that as of
|
||||
# 2019-12-17 there are no security notice advisories in
|
||||
# https://github.com/rustsec/advisory-db
|
||||
notice = "warn"
|
||||
# A list of advisory IDs to ignore. Note that ignored advisories will still
|
||||
# output a note when they are encountered.
|
||||
ignore = [
|
||||
#"RUSTSEC-0000-0000",
|
||||
]
|
||||
# Threshold for security vulnerabilities, any vulnerability with a CVSS score
|
||||
# lower than the range specified will be ignored. Note that ignored advisories
|
||||
# will still output a note when they are encountered.
|
||||
# * None - CVSS Score 0.0
|
||||
# * Low - CVSS Score 0.1 - 3.9
|
||||
# * Medium - CVSS Score 4.0 - 6.9
|
||||
# * High - CVSS Score 7.0 - 8.9
|
||||
# * Critical - CVSS Score 9.0 - 10.0
|
||||
#severity-threshold =
|
||||
|
||||
# This section is considered when running `cargo deny check licenses`
|
||||
# More documentation for the licenses section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
|
||||
[licenses]
|
||||
# The lint level for crates which do not have a detectable license
|
||||
unlicensed = "deny"
|
||||
# List of explictly allowed licenses
|
||||
# See https://spdx.org/licenses/ for list of possible licenses
|
||||
# [possible values: any SPDX 3.7 short identifier (+ optional exception)].
|
||||
allow = [
|
||||
"Apache-2.0",
|
||||
"BlueOak-1.0.0",
|
||||
"BSD-2-Clause",
|
||||
"CC0-1.0",
|
||||
"GPL-3.0",
|
||||
"MIT",
|
||||
"Unlicense",
|
||||
]
|
||||
# List of explictly disallowed licenses
|
||||
# See https://spdx.org/licenses/ for list of possible licenses
|
||||
# [possible values: any SPDX 3.7 short identifier (+ optional exception)].
|
||||
deny = []
|
||||
# Lint level for licenses considered copyleft
|
||||
copyleft = "allow"
|
||||
# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses
|
||||
# * both - The license will be approved if it is both OSI-approved *AND* FSF
|
||||
# * either - The license will be approved if it is either OSI-approved *OR* FSF
|
||||
# * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF
|
||||
# * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved
|
||||
# * neither - This predicate is ignored and the default lint level is used
|
||||
allow-osi-fsf-free = "both"
|
||||
# Lint level used when no other predicates are matched
|
||||
# 1. License isn't in the allow or deny lists
|
||||
# 2. License isn't copyleft
|
||||
# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither"
|
||||
default = "deny"
|
||||
# The confidence threshold for detecting a license from license text.
|
||||
# The higher the value, the more closely the license text must be to the
|
||||
# canonical license text of a valid SPDX license file.
|
||||
# [possible values: any between 0.0 and 1.0].
|
||||
confidence-threshold = 0.8
|
||||
# Allow 1 or more licenses on a per-crate basis, so that particular licenses
|
||||
# aren't accepted for every possible crate as with the normal allow list
|
||||
exceptions = [
|
||||
# Technically, the `ring` crate is "ISC AND MIT AND OpenSSL", but the rust
|
||||
# code is ISC (mostly). The OpenSSL license is not compatible with GPLv3.
|
||||
# For the sake of sanity, we will need to either get rid of TLS, or
|
||||
# re-license.
|
||||
{ allow = ["ISC", "MIT", "OpenSSL"], name = "ring" },
|
||||
|
||||
# The Unicode-DFS--2016 license is necessary for unicode-ident because they
|
||||
# use data from the unicode tables to generate the tables which are
|
||||
# included in the application. We do not distribute those data files so
|
||||
# this is not a problem for us. See
|
||||
# https://github.com/dtolnay/unicode-ident/pull/9/files for more details.
|
||||
{ allow = ["MIT", "Apache-2.0", "Unicode-DFS-2016"], name = "unicode-ident" },
|
||||
]
|
||||
|
||||
# Some crates don't have (easily) machine readable licensing information,
|
||||
# adding a clarification entry for it allows you to manually specify the
|
||||
# licensing information
|
||||
[[licenses.clarify]]
|
||||
name = "ring"
|
||||
expression = "ISC AND MIT AND OpenSSL"
|
||||
license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }]
|
||||
|
||||
[licenses.private]
|
||||
# If true, ignores workspace crates that aren't published, or are only
|
||||
# published to private registries
|
||||
ignore = false
|
||||
# One or more private registries that you might publish crates to, if a crate
|
||||
# is only published to private registries, and ignore is true, the crate will
|
||||
# not have its license(s) checked
|
||||
registries = [
|
||||
#"https://sekretz.com/registry
|
||||
]
|
||||
|
||||
# This section is considered when running `cargo deny check bans`.
|
||||
# More documentation about the 'bans' section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
|
||||
[bans]
|
||||
# Lint level for when multiple versions of the same crate are detected
|
||||
multiple-versions = "warn"
|
||||
# The graph highlighting used when creating dotgraphs for crates
|
||||
# with multiple versions
|
||||
# * lowest-version - The path to the lowest versioned duplicate is highlighted
|
||||
# * simplest-path - The path to the version with the fewest edges is highlighted
|
||||
# * all - Both lowest-version and simplest-path are used
|
||||
highlight = "all"
|
||||
# List of crates that are allowed. Use with care!
|
||||
allow = [
|
||||
#{ name = "ansi_term", version = "=0.11.0" },
|
||||
]
|
||||
# List of crates to deny
|
||||
deny = [
|
||||
# Each entry the name of a crate and a version range. If version is
|
||||
# not specified, all versions will be matched.
|
||||
#{ name = "ansi_term", version = "=0.11.0" },
|
||||
{ name = "openssl-probe" },
|
||||
{ name = "openssl-sys" },
|
||||
|
||||
# Pinned crypto libs
|
||||
# See radicle-keystore @ 00f8fb6135f8e4cd097a48e6f0700e08ce4abb04
|
||||
{ name = "chacha20poly1305", version = "> 0.9.0" },
|
||||
{ name = "ed25519-zebra", version = "> 3.0.0" },
|
||||
{ name = "curve25519-dalek", version = "> 3.2.1" },
|
||||
{ name = "scrypt", version = "> 0.8.0" },
|
||||
]
|
||||
# Certain crates/versions that will be skipped when doing duplicate detection.
|
||||
skip = [
|
||||
#{ name = "ansi_term", version = "=0.11.0" },
|
||||
]
|
||||
# Similarly to `skip` allows you to skip certain crates during duplicate
|
||||
# detection. Unlike skip, it also includes the entire tree of transitive
|
||||
# dependencies starting at the specified crate, up to a certain depth, which is
|
||||
# by default infinite
|
||||
skip-tree = [
|
||||
#{ name = "ansi_term", version = "=0.11.0", depth = 20 },
|
||||
]
|
||||
|
||||
# This section is considered when running `cargo deny check sources`.
|
||||
# More documentation about the 'sources' section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
|
||||
[sources]
|
||||
# Lint level for what to happen when a crate from a crate registry that is not
|
||||
# in the allow list is encountered
|
||||
unknown-registry = "deny"
|
||||
# Lint level for what to happen when a crate from a git repository that is not
|
||||
# in the allow list is encountered
|
||||
unknown-git = "deny"
|
||||
# List of URLs for allowed crate registries. Defaults to the crates.io index
|
||||
# if not specified. If it is specified but empty, no registries are allowed.
|
||||
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||
# List of URLs for allowed Git repositories
|
||||
allow-git = []
|
||||
@@ -0,0 +1,41 @@
|
||||
[package]
|
||||
name = "radicle-git-ext"
|
||||
version = "0.1.0"
|
||||
authors = ["The Radicle Team <dev@radicle.xyz>"]
|
||||
edition = "2018"
|
||||
license = "GPL-3.0-or-later"
|
||||
description = "Utilities and extensions to the git2 crate"
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
test = false
|
||||
|
||||
[dependencies]
|
||||
multihash = "0.11"
|
||||
percent-encoding = "2"
|
||||
thiserror = "1"
|
||||
|
||||
[dependencies.git2]
|
||||
version = "0.13.24"
|
||||
default-features = false
|
||||
features = ["vendored-libgit2"]
|
||||
|
||||
[dependencies.git-ref-format]
|
||||
path = "../git-ref-format"
|
||||
|
||||
[dependencies.link-git]
|
||||
path = "../link-git"
|
||||
optional = true
|
||||
|
||||
[dependencies.minicbor]
|
||||
version = "0.13"
|
||||
features = ["std"]
|
||||
optional = true
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1"
|
||||
features = ["derive"]
|
||||
optional = true
|
||||
|
||||
[dependencies.radicle-std-ext]
|
||||
path = "../std-ext"
|
||||
@@ -0,0 +1,146 @@
|
||||
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
|
||||
//
|
||||
// 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::{borrow::Cow, path::Path};
|
||||
|
||||
use radicle_std_ext::result::ResultExt as _;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{error::is_not_found_err, revwalk};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
NotFound(#[from] NotFound),
|
||||
|
||||
#[error(transparent)]
|
||||
Git(#[from] git2::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum NotFound {
|
||||
#[error("blob with path {0} not found")]
|
||||
NoSuchBlob(String),
|
||||
|
||||
#[error("branch {0} not found")]
|
||||
NoSuchBranch(String),
|
||||
|
||||
#[error("object {0} not found")]
|
||||
NoSuchObject(git2::Oid),
|
||||
|
||||
#[error("the supplied git2::Reference does not have a target")]
|
||||
NoRefTarget,
|
||||
}
|
||||
|
||||
pub enum Branch<'a> {
|
||||
Name(Cow<'a, str>),
|
||||
Ref(git2::Reference<'a>),
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for Branch<'a> {
|
||||
fn from(s: &'a str) -> Self {
|
||||
Self::Name(Cow::Borrowed(s))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<String> for Branch<'a> {
|
||||
fn from(s: String) -> Self {
|
||||
Self::Name(Cow::Owned(s))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<git2::Reference<'a>> for Branch<'a> {
|
||||
fn from(r: git2::Reference<'a>) -> Self {
|
||||
Self::Ref(r)
|
||||
}
|
||||
}
|
||||
|
||||
/// Conveniently read a [`git2::Blob`] from a starting point.
|
||||
pub enum Blob<'a> {
|
||||
/// Look up the tip of the reference specified by [`Branch`], peel until a
|
||||
/// tree is found, and traverse the tree along the given [`Path`] until
|
||||
/// the blob is found.
|
||||
Tip { branch: Branch<'a>, path: &'a Path },
|
||||
/// Traverse the history from the tip of [`Branch`] along the first parent
|
||||
/// until a commit without parents is found. Try to get the blob in that
|
||||
/// commit's tree at [`Path`].
|
||||
Init { branch: Branch<'a>, path: &'a Path },
|
||||
/// Look up `object`, peel until a tree is found, and try to get at the blob
|
||||
/// at [`Path`].
|
||||
At { object: git2::Oid, path: &'a Path },
|
||||
}
|
||||
|
||||
impl<'a> Blob<'a> {
|
||||
pub fn get(self, git: &'a git2::Repository) -> Result<git2::Blob<'a>, Error> {
|
||||
match self {
|
||||
Self::Tip { branch, path } => {
|
||||
let reference = match branch {
|
||||
Branch::Name(name) => {
|
||||
git.find_reference(&name).or_matches(is_not_found_err, || {
|
||||
Err(Error::NotFound(NotFound::NoSuchBranch(
|
||||
name.to_owned().to_string(),
|
||||
)))
|
||||
})
|
||||
},
|
||||
|
||||
Branch::Ref(reference) => Ok(reference),
|
||||
}?;
|
||||
let tree = reference.peel_to_tree()?;
|
||||
blob(git, tree, path)
|
||||
},
|
||||
|
||||
Self::Init { branch, path } => {
|
||||
let start = match branch {
|
||||
Branch::Name(name) => Ok(revwalk::Start::Ref(name.to_string())),
|
||||
Branch::Ref(reference) => {
|
||||
match (reference.target(), reference.symbolic_target()) {
|
||||
(Some(oid), _) => Ok(revwalk::Start::Oid(oid)),
|
||||
(_, Some(sym)) => Ok(revwalk::Start::Ref(sym.to_string())),
|
||||
(_, _) => Err(Error::NotFound(NotFound::NoRefTarget)),
|
||||
}
|
||||
},
|
||||
}?;
|
||||
|
||||
let revwalk = revwalk::FirstParent::new(git, start)?.reverse()?;
|
||||
match revwalk.into_iter().next() {
|
||||
None => Err(Error::NotFound(NotFound::NoSuchBlob(
|
||||
path.display().to_string(),
|
||||
))),
|
||||
Some(oid) => {
|
||||
let oid = oid?;
|
||||
let tree = git.find_commit(oid)?.tree()?;
|
||||
blob(git, tree, path)
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
Self::At { object, path } => {
|
||||
let tree = git
|
||||
.find_object(object, None)
|
||||
.or_matches(is_not_found_err, || {
|
||||
Err(Error::NotFound(NotFound::NoSuchObject(object)))
|
||||
})
|
||||
.and_then(|obj| Ok(obj.peel_to_tree()?))?;
|
||||
blob(git, tree, path)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn blob<'a>(
|
||||
repo: &'a git2::Repository,
|
||||
tree: git2::Tree<'a>,
|
||||
path: &'a Path,
|
||||
) -> Result<git2::Blob<'a>, Error> {
|
||||
let entry = tree.get_path(path).or_matches(is_not_found_err, || {
|
||||
Err(Error::NotFound(NotFound::NoSuchBlob(
|
||||
path.display().to_string(),
|
||||
)))
|
||||
})?;
|
||||
|
||||
entry.to_object(repo)?.peel_to_blob().map_err(Error::from)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
|
||||
//
|
||||
// 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::{fmt::Display, io};
|
||||
|
||||
pub fn is_not_found_err(e: &git2::Error) -> bool {
|
||||
e.code() == git2::ErrorCode::NotFound
|
||||
}
|
||||
|
||||
pub fn is_exists_err(e: &git2::Error) -> bool {
|
||||
e.code() == git2::ErrorCode::Exists
|
||||
}
|
||||
|
||||
pub fn into_git_err<E: Display>(e: E) -> git2::Error {
|
||||
git2::Error::from_str(&e.to_string())
|
||||
}
|
||||
|
||||
pub fn into_io_err(e: git2::Error) -> io::Error {
|
||||
io::Error::new(io::ErrorKind::Other, e)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
//! Extensions and wrappers for `git2` types
|
||||
|
||||
pub mod blob;
|
||||
pub mod error;
|
||||
pub mod oid;
|
||||
pub mod reference;
|
||||
pub mod revwalk;
|
||||
pub mod transport;
|
||||
pub mod tree;
|
||||
|
||||
pub use blob::*;
|
||||
pub use error::*;
|
||||
pub use oid::*;
|
||||
pub use reference::*;
|
||||
pub use revwalk::*;
|
||||
pub use transport::*;
|
||||
pub use tree::Tree;
|
||||
@@ -0,0 +1,233 @@
|
||||
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
|
||||
//
|
||||
// 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::{
|
||||
convert::TryFrom,
|
||||
fmt::{self, Display},
|
||||
ops::Deref,
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use multihash::{Multihash, MultihashRef};
|
||||
use thiserror::Error;
|
||||
|
||||
#[cfg(feature = "link-git")]
|
||||
use link_git::hash as git_hash;
|
||||
|
||||
/// Serializable [`git2::Oid`]
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub struct Oid(git2::Oid);
|
||||
|
||||
impl Oid {
|
||||
pub fn into_multihash(self) -> Multihash {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
mod serde_impls {
|
||||
use super::*;
|
||||
use serde::{de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
impl Serialize for Oid {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
self.0.to_string().serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Oid {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
struct OidVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for OidVisitor {
|
||||
type Value = Oid;
|
||||
|
||||
fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "a hexidecimal git2::Oid")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: serde::de::Error,
|
||||
{
|
||||
s.parse().map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_str(OidVisitor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "minicbor")]
|
||||
mod minicbor_impls {
|
||||
use super::*;
|
||||
use minicbor::{
|
||||
decode,
|
||||
encode::{self, Write},
|
||||
Decode,
|
||||
Decoder,
|
||||
Encode,
|
||||
Encoder,
|
||||
};
|
||||
|
||||
impl Encode for Oid {
|
||||
fn encode<W: Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
|
||||
e.bytes(Multihash::from(self).as_bytes())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'b> Decode<'b> for Oid {
|
||||
fn decode(d: &mut Decoder) -> Result<Self, decode::Error> {
|
||||
let bytes = d.bytes()?;
|
||||
let mhash = MultihashRef::from_slice(bytes)
|
||||
.or(Err(decode::Error::Message("not a multihash")))?;
|
||||
Self::try_from(mhash).or(Err(decode::Error::Message("not a git oid")))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Oid {
|
||||
type Target = git2::Oid;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<git2::Oid> for Oid {
|
||||
fn as_ref(&self) -> &git2::Oid {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for Oid {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.as_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "link-git")]
|
||||
impl AsRef<git_hash::oid> for Oid {
|
||||
fn as_ref(&self) -> &git_hash::oid {
|
||||
// SAFETY: checks the length of the slice, which we know is correct
|
||||
git_hash::oid::try_from(self.as_bytes()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<git2::Oid> for Oid {
|
||||
fn from(oid: git2::Oid) -> Self {
|
||||
Self(oid)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Oid> for git2::Oid {
|
||||
fn from(oid: Oid) -> Self {
|
||||
oid.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "link-git")]
|
||||
impl From<git_hash::ObjectId> for Oid {
|
||||
fn from(git_hash::ObjectId::Sha1(bs): git_hash::ObjectId) -> Self {
|
||||
// SAFETY: checks the length of the slice, which we statically know
|
||||
Self(git2::Oid::from_bytes(&bs).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "link-git")]
|
||||
impl From<Oid> for git_hash::ObjectId {
|
||||
fn from(oid: Oid) -> Self {
|
||||
Self::from_20_bytes(oid.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "link-git")]
|
||||
impl<'a> From<&'a Oid> for &'a git_hash::oid {
|
||||
fn from(oid: &'a Oid) -> Self {
|
||||
oid.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Oid {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Oid {
|
||||
type Error = git2::Error;
|
||||
|
||||
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
||||
s.parse().map(Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Oid {
|
||||
type Err = git2::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Self::try_from(s)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum FromMultihashError {
|
||||
#[error("invalid hash algorithm: expected Sha1, got {actual:?}")]
|
||||
AlgorithmMismatch { actual: multihash::Code },
|
||||
|
||||
#[error(transparent)]
|
||||
Git(#[from] git2::Error),
|
||||
}
|
||||
|
||||
impl TryFrom<Multihash> for Oid {
|
||||
type Error = FromMultihashError;
|
||||
|
||||
fn try_from(mhash: Multihash) -> Result<Self, Self::Error> {
|
||||
Self::try_from(mhash.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<MultihashRef<'_>> for Oid {
|
||||
type Error = FromMultihashError;
|
||||
|
||||
fn try_from(mhash: MultihashRef) -> Result<Self, Self::Error> {
|
||||
if mhash.algorithm() != multihash::Code::Sha1 {
|
||||
return Err(Self::Error::AlgorithmMismatch {
|
||||
actual: mhash.algorithm(),
|
||||
});
|
||||
}
|
||||
|
||||
Self::try_from(mhash.digest()).map_err(Self::Error::from)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for Oid {
|
||||
type Error = git2::Error;
|
||||
|
||||
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
|
||||
git2::Oid::from_bytes(bytes).map(Self)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Oid> for Multihash {
|
||||
fn from(oid: Oid) -> Self {
|
||||
Self::from(&oid)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Oid> for Multihash {
|
||||
fn from(oid: &Oid) -> Self {
|
||||
multihash::wrap(multihash::Code::Sha1, oid.as_ref())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
|
||||
//
|
||||
// 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::convert::TryFrom as _;
|
||||
|
||||
mod iter;
|
||||
pub use iter::{ReferenceNames, References};
|
||||
|
||||
pub mod name;
|
||||
pub use name::{OneLevel, Qualified, RefLike, RefspecPattern};
|
||||
|
||||
pub mod check {
|
||||
pub use git_ref_format::{check_ref_format as ref_format, Error, Options};
|
||||
}
|
||||
|
||||
pub fn peeled(head: git2::Reference) -> Option<(String, git2::Oid)> {
|
||||
head.name()
|
||||
.and_then(|name| head.target().map(|target| (name.to_owned(), target)))
|
||||
}
|
||||
|
||||
pub fn refined((name, oid): (&str, git2::Oid)) -> Result<(OneLevel, crate::Oid), name::Error> {
|
||||
let name = RefLike::try_from(name)?;
|
||||
Ok((OneLevel::from(name), oid.into()))
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
/// Iterator chaining multiple [`git2::References`]
|
||||
#[must_use = "iterators are lazy and do nothing unless consumed"]
|
||||
pub struct References<'a> {
|
||||
inner: Vec<git2::References<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> References<'a> {
|
||||
pub fn new(refs: impl IntoIterator<Item = git2::References<'a>>) -> Self {
|
||||
Self {
|
||||
inner: refs.into_iter().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_globs(
|
||||
repo: &'a git2::Repository,
|
||||
globs: impl IntoIterator<Item = impl AsRef<str>>,
|
||||
) -> Result<Self, git2::Error> {
|
||||
let globs = globs.into_iter();
|
||||
let mut iters = globs
|
||||
.size_hint()
|
||||
.1
|
||||
.map(Vec::with_capacity)
|
||||
.unwrap_or_else(Vec::new);
|
||||
for glob in globs {
|
||||
let iter = repo.references_glob(glob.as_ref())?;
|
||||
iters.push(iter);
|
||||
}
|
||||
|
||||
Ok(Self::new(iters))
|
||||
}
|
||||
|
||||
pub fn names<'b>(&'b mut self) -> ReferenceNames<'a, 'b> {
|
||||
ReferenceNames {
|
||||
inner: self.inner.iter_mut().map(|refs| refs.names()).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn peeled(self) -> impl Iterator<Item = (String, git2::Oid)> + 'a {
|
||||
self.filter_map(|reference| {
|
||||
reference.ok().and_then(|head| {
|
||||
head.name().and_then(|name| {
|
||||
head.target()
|
||||
.map(|target| (name.to_owned(), target.to_owned()))
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for References<'a> {
|
||||
type Item = Result<git2::Reference<'a>, git2::Error>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.inner.pop().and_then(|mut iter| match iter.next() {
|
||||
None => self.next(),
|
||||
Some(item) => {
|
||||
self.inner.push(iter);
|
||||
Some(item)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator chaining multiple [`git2::ReferenceNames`]
|
||||
#[must_use = "iterators are lazy and do nothing unless consumed"]
|
||||
pub struct ReferenceNames<'repo, 'references> {
|
||||
inner: Vec<git2::ReferenceNames<'repo, 'references>>,
|
||||
}
|
||||
|
||||
impl<'a, 'b> Iterator for ReferenceNames<'a, 'b> {
|
||||
type Item = Result<&'b str, git2::Error>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.inner.pop().and_then(|mut iter| match iter.next() {
|
||||
None => self.next(),
|
||||
Some(item) => {
|
||||
self.inner.push(iter);
|
||||
Some(item)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,711 @@
|
||||
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
|
||||
//
|
||||
// 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::{
|
||||
convert::TryFrom,
|
||||
fmt::{self, Display},
|
||||
iter::FromIterator,
|
||||
ops::Deref,
|
||||
path::Path,
|
||||
str::{self, FromStr},
|
||||
};
|
||||
|
||||
pub use percent_encoding::PercentEncode;
|
||||
use thiserror::Error;
|
||||
|
||||
use super::check;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
#[error("invalid utf8")]
|
||||
Utf8,
|
||||
|
||||
#[error("not a valid git ref name or pattern")]
|
||||
RefFormat(#[from] check::Error),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub const fn empty() -> Self {
|
||||
Self::RefFormat(check::Error::Empty)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum StripPrefixError {
|
||||
#[error("prefix is equal to path")]
|
||||
ImproperPrefix,
|
||||
|
||||
#[error("not prefixed by given path")]
|
||||
NotPrefix,
|
||||
}
|
||||
|
||||
/// An owned path-like value which is a valid git refname.
|
||||
///
|
||||
/// See [`git-check-ref-format`] for what the rules for refnames are --
|
||||
/// conversion functions behave as if `--allow-onelevel` was given.
|
||||
/// Additionally, we impose the rule that the name must consist of valid utf8.
|
||||
///
|
||||
/// Note that refspec patterns (eg. "refs/heads/*") are not allowed (see
|
||||
/// [`RefspecPattern`]), and that the maximum length of the name is 1024 bytes.
|
||||
///
|
||||
/// [`git-check-ref-format`]: https://git-scm.com/docs/git-check-ref-format
|
||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
derive(serde::Serialize, serde::Deserialize),
|
||||
serde(into = "String", try_from = "String")
|
||||
)]
|
||||
pub struct RefLike(String);
|
||||
|
||||
impl RefLike {
|
||||
/// Append the path in `Other` to `self.
|
||||
pub fn join<Other: Into<Self>>(&self, other: Other) -> Self {
|
||||
Self(format!("{}/{}", self.0, other.into().0))
|
||||
}
|
||||
|
||||
/// Append a [`RefspecPattern`], yielding a [`RefspecPattern`]
|
||||
pub fn with_pattern_suffix<Suf: Into<RefspecPattern>>(&self, suf: Suf) -> RefspecPattern {
|
||||
RefspecPattern(format!("{}/{}", self.0, suf.into().0))
|
||||
}
|
||||
|
||||
/// Returns a [`RefLike`] that, when joined onto `base`, yields `self`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// If `base` is not a prefix of `self`, or `base` equals the path in `self`
|
||||
/// (ie. the result would be the empty path, which is not a valid
|
||||
/// [`RefLike`]).
|
||||
pub fn strip_prefix<P: AsRef<str>>(&self, base: P) -> Result<Self, StripPrefixError> {
|
||||
let base = base.as_ref();
|
||||
let base = format!("{}/", base.strip_suffix('/').unwrap_or(base));
|
||||
self.0
|
||||
.strip_prefix(&base)
|
||||
.ok_or(StripPrefixError::NotPrefix)
|
||||
.and_then(|path| {
|
||||
if path.is_empty() {
|
||||
Err(StripPrefixError::ImproperPrefix)
|
||||
} else {
|
||||
Ok(Self(path.into()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.as_ref()
|
||||
}
|
||||
|
||||
pub fn percent_encode(&self) -> PercentEncode {
|
||||
/// https://url.spec.whatwg.org/#fragment-percent-encode-set
|
||||
const FRAGMENT_PERCENT_ENCODE_SET: &percent_encoding::AsciiSet =
|
||||
&percent_encoding::CONTROLS
|
||||
.add(b' ')
|
||||
.add(b'"')
|
||||
.add(b'<')
|
||||
.add(b'>')
|
||||
.add(b'`');
|
||||
|
||||
/// https://url.spec.whatwg.org/#path-percent-encode-set
|
||||
const PATH_PERCENT_ENCODE_SET: &percent_encoding::AsciiSet = &FRAGMENT_PERCENT_ENCODE_SET
|
||||
.add(b'#')
|
||||
.add(b'?')
|
||||
.add(b'{')
|
||||
.add(b'}');
|
||||
|
||||
percent_encoding::utf8_percent_encode(self.as_str(), PATH_PERCENT_ENCODE_SET)
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for RefLike {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for RefLike {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for RefLike {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
||||
check::ref_format(
|
||||
check::Options {
|
||||
allow_onelevel: true,
|
||||
allow_pattern: false,
|
||||
},
|
||||
s,
|
||||
)?;
|
||||
Ok(Self(s.to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for RefLike {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
|
||||
str::from_utf8(bytes)
|
||||
.or(Err(Error::Utf8))
|
||||
.and_then(Self::try_from)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for RefLike {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Self::try_from(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for RefLike {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||
Self::try_from(s.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&Path> for RefLike {
|
||||
type Error = Error;
|
||||
|
||||
#[cfg(target_family = "windows")]
|
||||
fn try_from(p: &Path) -> Result<Self, Self::Error> {
|
||||
use std::{convert::TryInto as _, path::Component::Normal};
|
||||
|
||||
p.components()
|
||||
.filter_map(|comp| match comp {
|
||||
Normal(s) => Some(s),
|
||||
_ => None,
|
||||
})
|
||||
.map(|os| os.to_str().ok_or(Error::Utf8))
|
||||
.collect::<Result<Vec<_>, Self::Error>>()?
|
||||
.join("/")
|
||||
.try_into()
|
||||
}
|
||||
|
||||
#[cfg(target_family = "unix")]
|
||||
fn try_from(p: &Path) -> Result<Self, Self::Error> {
|
||||
Self::try_from(p.to_str().ok_or(Error::Utf8)?)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&RefLike> for RefLike {
|
||||
fn from(me: &RefLike) -> Self {
|
||||
me.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<git_ref_format::RefString> for RefLike {
|
||||
#[inline]
|
||||
fn from(r: git_ref_format::RefString) -> Self {
|
||||
Self(r.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&git_ref_format::RefString> for RefLike {
|
||||
#[inline]
|
||||
fn from(r: &git_ref_format::RefString) -> Self {
|
||||
Self::from(r.as_refstr())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&git_ref_format::RefStr> for RefLike {
|
||||
#[inline]
|
||||
fn from(r: &git_ref_format::RefStr) -> Self {
|
||||
Self(r.to_owned().into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RefLike> for String {
|
||||
fn from(RefLike(path): RefLike) -> Self {
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<Self> for RefLike {
|
||||
fn from_iter<T>(iter: T) -> Self
|
||||
where
|
||||
T: IntoIterator<Item = Self>,
|
||||
{
|
||||
Self(iter.into_iter().map(|x| x.0).collect::<Vec<_>>().join("/"))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for RefLike {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`RefLike`] without a "refs/" prefix.
|
||||
///
|
||||
/// Conversion functions strip the first **two** path components iff the path
|
||||
/// starts with `refs/`.
|
||||
///
|
||||
/// Note that the [`serde::Deserialize`] impl thusly implies that input in
|
||||
/// [`Qualified`] form is accepted, and silently converted.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use std::convert::TryFrom;
|
||||
/// use radicle_git_ext::reference::name::*;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// &*OneLevel::from(RefLike::try_from("refs/heads/next").unwrap()),
|
||||
/// "next"
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// &*OneLevel::from(RefLike::try_from("refs/remotes/origin/it").unwrap()),
|
||||
/// "origin/it"
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// &*OneLevel::from(RefLike::try_from("mistress").unwrap()),
|
||||
/// "mistress"
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// OneLevel::from_qualified(Qualified::from(RefLike::try_from("refs/tags/grace").unwrap())),
|
||||
/// (
|
||||
/// OneLevel::from(RefLike::try_from("grace").unwrap()),
|
||||
/// Some(RefLike::try_from("tags").unwrap())
|
||||
/// ),
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// OneLevel::from_qualified(Qualified::from(RefLike::try_from("refs/remotes/origin/hopper").unwrap())),
|
||||
/// (
|
||||
/// OneLevel::from(RefLike::try_from("origin/hopper").unwrap()),
|
||||
/// Some(RefLike::try_from("remotes").unwrap())
|
||||
/// ),
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// OneLevel::from_qualified(Qualified::from(RefLike::try_from("refs/HEAD").unwrap())),
|
||||
/// (OneLevel::from(RefLike::try_from("HEAD").unwrap()), None)
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// &*OneLevel::from(RefLike::try_from("origin/hopper").unwrap()).into_qualified(
|
||||
/// RefLike::try_from("remotes").unwrap()
|
||||
/// ),
|
||||
/// "refs/remotes/origin/hopper",
|
||||
/// );
|
||||
/// ```
|
||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
derive(serde::Serialize, serde::Deserialize),
|
||||
serde(into = "String", try_from = "RefLike")
|
||||
)]
|
||||
pub struct OneLevel(String);
|
||||
|
||||
impl OneLevel {
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.as_ref()
|
||||
}
|
||||
|
||||
pub fn from_qualified(Qualified(path): Qualified) -> (Self, Option<RefLike>) {
|
||||
let mut path = path.strip_prefix("refs/").unwrap_or(&path).split('/');
|
||||
match path.next() {
|
||||
Some(category) => {
|
||||
let category = RefLike(category.into());
|
||||
// check that the "category" is not the only component of the path
|
||||
match path.next() {
|
||||
Some(head) => (
|
||||
Self(
|
||||
std::iter::once(head)
|
||||
.chain(path)
|
||||
.collect::<Vec<_>>()
|
||||
.join("/"),
|
||||
),
|
||||
Some(category),
|
||||
),
|
||||
None => (Self::from(category), None),
|
||||
}
|
||||
},
|
||||
None => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_qualified(self, category: RefLike) -> Qualified {
|
||||
Qualified(format!("refs/{}/{}", category, self))
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for OneLevel {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for OneLevel {
|
||||
fn as_ref(&self) -> &str {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RefLike> for OneLevel {
|
||||
fn from(RefLike(path): RefLike) -> Self {
|
||||
if path.starts_with("refs/") {
|
||||
Self(path.split('/').skip(2).collect::<Vec<_>>().join("/"))
|
||||
} else {
|
||||
Self(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Qualified> for OneLevel {
|
||||
fn from(Qualified(path): Qualified) -> Self {
|
||||
Self::from(RefLike(path))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OneLevel> for RefLike {
|
||||
fn from(OneLevel(path): OneLevel) -> Self {
|
||||
Self(path)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OneLevel> for String {
|
||||
fn from(OneLevel(path): OneLevel) -> Self {
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for OneLevel {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`RefLike`] **with** a "refs/" prefix.
|
||||
///
|
||||
/// Conversion functions will assume `refs/heads/` if the input was not
|
||||
/// qualified.
|
||||
///
|
||||
/// Note that the [`serde::Deserialize`] impl thusly implies that input in
|
||||
/// [`OneLevel`] form is accepted, and silently converted.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use std::convert::TryFrom;
|
||||
/// use radicle_git_ext::reference::name::*;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// &*Qualified::from(RefLike::try_from("laplace").unwrap()),
|
||||
/// "refs/heads/laplace"
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// &*Qualified::from(RefLike::try_from("refs/heads/pu").unwrap()),
|
||||
/// "refs/heads/pu"
|
||||
/// );
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// &*Qualified::from(RefLike::try_from("refs/tags/v6.6.6").unwrap()),
|
||||
/// "refs/tags/v6.6.6"
|
||||
/// );
|
||||
/// ```
|
||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
derive(serde::Serialize, serde::Deserialize),
|
||||
serde(into = "String", try_from = "RefLike")
|
||||
)]
|
||||
pub struct Qualified(String);
|
||||
|
||||
impl Qualified {
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Qualified {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for Qualified {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RefLike> for Qualified {
|
||||
fn from(RefLike(path): RefLike) -> Self {
|
||||
if path.starts_with("refs/") {
|
||||
Self(path)
|
||||
} else {
|
||||
Self(format!("refs/heads/{}", path))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OneLevel> for Qualified {
|
||||
fn from(OneLevel(path): OneLevel) -> Self {
|
||||
Self::from(RefLike(path))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Qualified> for RefLike {
|
||||
fn from(Qualified(path): Qualified) -> Self {
|
||||
Self(path)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Qualified> for String {
|
||||
fn from(Qualified(path): Qualified) -> Self {
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Qualified {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// An owned, path-like value which is a valid refspec pattern.
|
||||
///
|
||||
/// Conversion functions behave as if `--allow-onelevel --refspec-pattern` where
|
||||
/// given to [`git-check-ref-format`]. That is, most of the rules of [`RefLike`]
|
||||
/// apply, but the path _may_ contain exactly one `*` character.
|
||||
///
|
||||
/// [`git-check-ref-format`]: https://git-scm.com/docs/git-check-ref-format
|
||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
derive(serde::Serialize, serde::Deserialize),
|
||||
serde(into = "String", try_from = "String")
|
||||
)]
|
||||
pub struct RefspecPattern(String);
|
||||
|
||||
impl RefspecPattern {
|
||||
/// Append the `RefLike` to the `RefspecPattern`. This allows the creation
|
||||
/// of patterns where the `*` appears in the middle of the path, e.g.
|
||||
/// `refs/remotes/*/mfdoom`
|
||||
pub fn append(&self, refl: impl Into<RefLike>) -> Self {
|
||||
RefspecPattern(format!("{}/{}", self.0, refl.into()))
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&RefspecPattern> for RefspecPattern {
|
||||
fn from(pat: &RefspecPattern) -> Self {
|
||||
pat.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for RefspecPattern {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for RefspecPattern {
|
||||
fn as_ref(&self) -> &str {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for RefspecPattern {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
||||
check::ref_format(
|
||||
check::Options {
|
||||
allow_onelevel: true,
|
||||
allow_pattern: true,
|
||||
},
|
||||
s,
|
||||
)?;
|
||||
Ok(Self(s.to_owned()))
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for RefspecPattern {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
|
||||
str::from_utf8(bytes)
|
||||
.or(Err(Error::Utf8))
|
||||
.and_then(Self::try_from)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for RefspecPattern {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Self::try_from(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for RefspecPattern {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||
Self::try_from(s.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RefspecPattern> for String {
|
||||
fn from(RefspecPattern(path): RefspecPattern) -> Self {
|
||||
path
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for RefspecPattern {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
// `RefLike`-likes can be coerced into `RefspecPattern`s
|
||||
|
||||
impl From<RefLike> for RefspecPattern {
|
||||
fn from(RefLike(path): RefLike) -> Self {
|
||||
Self(path)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&RefLike> for RefspecPattern {
|
||||
fn from(RefLike(path): &RefLike) -> Self {
|
||||
Self(path.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OneLevel> for RefspecPattern {
|
||||
fn from(OneLevel(path): OneLevel) -> Self {
|
||||
Self(path)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&OneLevel> for RefspecPattern {
|
||||
fn from(OneLevel(path): &OneLevel) -> Self {
|
||||
Self(path.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Qualified> for RefspecPattern {
|
||||
fn from(Qualified(path): Qualified) -> Self {
|
||||
Self(path)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Qualified> for RefspecPattern {
|
||||
fn from(Qualified(path): &Qualified) -> Self {
|
||||
Self(path.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<git_ref_format::refspec::PatternString> for RefspecPattern {
|
||||
#[inline]
|
||||
fn from(r: git_ref_format::refspec::PatternString) -> Self {
|
||||
Self(r.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&git_ref_format::refspec::PatternStr> for RefspecPattern {
|
||||
#[inline]
|
||||
fn from(r: &git_ref_format::refspec::PatternStr) -> Self {
|
||||
Self(r.to_owned().into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "minicbor")]
|
||||
mod minicbor_impls {
|
||||
use super::*;
|
||||
use minicbor::{
|
||||
decode,
|
||||
encode::{self, Write},
|
||||
Decode,
|
||||
Decoder,
|
||||
Encode,
|
||||
Encoder,
|
||||
};
|
||||
|
||||
impl Encode for RefLike {
|
||||
fn encode<W: Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
|
||||
e.str(self.as_str())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'b> Decode<'b> for RefLike {
|
||||
fn decode(d: &mut Decoder) -> Result<Self, decode::Error> {
|
||||
let path = d.str()?;
|
||||
Self::try_from(path).or(Err(decode::Error::Message("invalid reflike")))
|
||||
}
|
||||
}
|
||||
|
||||
impl minicbor::Encode for OneLevel {
|
||||
fn encode<W: Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
|
||||
e.str(self.as_str())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'b> Decode<'b> for OneLevel {
|
||||
fn decode(d: &mut Decoder) -> Result<Self, decode::Error> {
|
||||
let refl: RefLike = Decode::decode(d)?;
|
||||
Ok(Self::from(refl))
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode for Qualified {
|
||||
fn encode<W: encode::Write>(
|
||||
&self,
|
||||
e: &mut Encoder<W>,
|
||||
) -> Result<(), encode::Error<W::Error>> {
|
||||
e.str(self.as_str())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'b> Decode<'b> for Qualified {
|
||||
fn decode(d: &mut Decoder) -> Result<Self, decode::Error> {
|
||||
let refl: RefLike = Decode::decode(d)?;
|
||||
Ok(Self::from(refl))
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode for RefspecPattern {
|
||||
fn encode<W: encode::Write>(
|
||||
&self,
|
||||
e: &mut Encoder<W>,
|
||||
) -> Result<(), encode::Error<W::Error>> {
|
||||
e.str(self.as_str())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'b> Decode<'b> for RefspecPattern {
|
||||
fn decode(d: &mut Decoder) -> Result<Self, decode::Error> {
|
||||
Self::try_from(d.str()?).or(Err(decode::Error::Message("invalid refspec pattern")))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
pub enum Start {
|
||||
Oid(git2::Oid),
|
||||
Ref(String),
|
||||
}
|
||||
|
||||
pub struct FirstParent<'a> {
|
||||
inner: git2::Revwalk<'a>,
|
||||
}
|
||||
|
||||
impl<'a> FirstParent<'a> {
|
||||
pub fn new(repo: &'a git2::Repository, start: Start) -> Result<Self, git2::Error> {
|
||||
let mut revwalk = repo.revwalk()?;
|
||||
revwalk.set_sorting(git2::Sort::TOPOLOGICAL)?;
|
||||
revwalk.simplify_first_parent()?;
|
||||
|
||||
match start {
|
||||
Start::Oid(oid) => revwalk.push(oid),
|
||||
Start::Ref(name) => revwalk.push_ref(&name),
|
||||
}?;
|
||||
|
||||
Ok(Self { inner: revwalk })
|
||||
}
|
||||
|
||||
pub fn reverse(mut self) -> Result<Self, git2::Error> {
|
||||
let mut sort = git2::Sort::TOPOLOGICAL;
|
||||
sort.insert(git2::Sort::REVERSE);
|
||||
self.inner.set_sorting(sort)?;
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for FirstParent<'a> {
|
||||
type Item = Result<git2::Oid, git2::Error>;
|
||||
type IntoIter = git2::Revwalk<'a>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.inner
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
pub const UPLOAD_PACK_HEADER: &[u8] = b"001e# service=git-upload-pack\n0000";
|
||||
pub const RECEIVE_PACK_HEADER: &[u8] = b"001f# service=git-receive-pack\n0000";
|
||||
@@ -0,0 +1,147 @@
|
||||
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
|
||||
//
|
||||
// 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::{borrow::Cow, collections::BTreeMap, iter::FromIterator};
|
||||
|
||||
/// A simplified representation of a git tree, intended mainly to be created
|
||||
/// from literals.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use radicle_git_ext::tree::{Tree, blob};
|
||||
///
|
||||
/// let my_tree = vec![
|
||||
/// ("README", blob(b"awe")),
|
||||
/// ("src", vec![("main.rs", blob(b"fn main() {}"))].into_iter().collect()),
|
||||
/// ]
|
||||
/// .into_iter()
|
||||
/// .collect::<Tree>();
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// format!("{:?}", my_tree),
|
||||
/// "Tree(\
|
||||
/// {\
|
||||
/// \"README\": Blob(\
|
||||
/// [\
|
||||
/// 97, \
|
||||
/// 119, \
|
||||
/// 101\
|
||||
/// ]\
|
||||
/// ), \
|
||||
/// \"src\": Tree(\
|
||||
/// Tree(\
|
||||
/// {\
|
||||
/// \"main.rs\": Blob(\
|
||||
/// [\
|
||||
/// 102, \
|
||||
/// 110, \
|
||||
/// 32, \
|
||||
/// 109, \
|
||||
/// 97, \
|
||||
/// 105, \
|
||||
/// 110, \
|
||||
/// 40, \
|
||||
/// 41, \
|
||||
/// 32, \
|
||||
/// 123, \
|
||||
/// 125\
|
||||
/// ]\
|
||||
/// )\
|
||||
/// }\
|
||||
/// )\
|
||||
/// )\
|
||||
/// }\
|
||||
/// )"
|
||||
/// )
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Tree<'a>(BTreeMap<Cow<'a, str>, Node<'a>>);
|
||||
|
||||
impl Tree<'_> {
|
||||
pub fn write(&self, repo: &git2::Repository) -> Result<git2::Oid, git2::Error> {
|
||||
use Node::*;
|
||||
|
||||
let mut builder = repo.treebuilder(None)?;
|
||||
for (name, node) in &self.0 {
|
||||
match node {
|
||||
Blob(data) => {
|
||||
let oid = repo.blob(data)?;
|
||||
builder.insert(name.as_ref(), oid, git2::FileMode::Blob.into())?;
|
||||
},
|
||||
Tree(sub) => {
|
||||
let oid = sub.write(repo)?;
|
||||
builder.insert(name.as_ref(), oid, git2::FileMode::Tree.into())?;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
builder.write()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<BTreeMap<Cow<'a, str>, Node<'a>>> for Tree<'a> {
|
||||
fn from(map: BTreeMap<Cow<'a, str>, Node<'a>>) -> Self {
|
||||
Self(map)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, K, N> FromIterator<(K, N)> for Tree<'a>
|
||||
where
|
||||
K: Into<Cow<'a, str>>,
|
||||
N: Into<Node<'a>>,
|
||||
{
|
||||
fn from_iter<T>(iter: T) -> Self
|
||||
where
|
||||
T: IntoIterator<Item = (K, N)>,
|
||||
{
|
||||
Self(
|
||||
iter.into_iter()
|
||||
.map(|(k, v)| (k.into(), v.into()))
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum Node<'a> {
|
||||
Blob(Cow<'a, [u8]>),
|
||||
Tree(Tree<'a>),
|
||||
}
|
||||
|
||||
pub fn blob(slice: &[u8]) -> Node {
|
||||
Node::from(slice)
|
||||
}
|
||||
|
||||
impl<'a> From<&'a [u8]> for Node<'a> {
|
||||
fn from(slice: &'a [u8]) -> Self {
|
||||
Self::from(Cow::Borrowed(slice))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Cow<'a, [u8]>> for Node<'a> {
|
||||
fn from(bytes: Cow<'a, [u8]>) -> Self {
|
||||
Self::Blob(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Tree<'a>> for Node<'a> {
|
||||
fn from(tree: Tree<'a>) -> Self {
|
||||
Self::Tree(tree)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, K, N> FromIterator<(K, N)> for Node<'a>
|
||||
where
|
||||
K: Into<Cow<'a, str>>,
|
||||
N: Into<Node<'a>>,
|
||||
{
|
||||
fn from_iter<T>(iter: T) -> Self
|
||||
where
|
||||
T: IntoIterator<Item = (K, N)>,
|
||||
{
|
||||
Self::Tree(iter.into_iter().collect())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "git-ext-test"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
test = true
|
||||
doc = false
|
||||
|
||||
[features]
|
||||
test = []
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches = "1.5"
|
||||
minicbor = "0.13"
|
||||
serde = "1"
|
||||
serde_json = "1"
|
||||
|
||||
[dev-dependencies.radicle-git-ext]
|
||||
path = ".."
|
||||
features = ["minicbor", "serde"]
|
||||
|
||||
[dev-dependencies.test-helpers]
|
||||
path = "../../test/test-helpers"
|
||||
@@ -0,0 +1,9 @@
|
||||
// Copyright © 2022 The Radicle Link Contributors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_use]
|
||||
extern crate assert_matches;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,255 @@
|
||||
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use radicle_git_ext::reference::{check, name::*};
|
||||
use test_helpers::roundtrip;
|
||||
|
||||
mod common {
|
||||
use super::*;
|
||||
use std::fmt::Debug;
|
||||
|
||||
pub fn invalid<T>()
|
||||
where
|
||||
T: TryFrom<&'static str, Error = Error> + Debug,
|
||||
{
|
||||
const INVALID: [&str; 16] = [
|
||||
".hidden",
|
||||
"/etc/shadow",
|
||||
"@",
|
||||
"@{",
|
||||
"C:",
|
||||
"\\WORKGROUP",
|
||||
"foo.lock",
|
||||
"head^",
|
||||
"here/../../etc/shadow",
|
||||
"refs//heads/main",
|
||||
"refs/heads/",
|
||||
"shawn/ white",
|
||||
"the/dotted./quad",
|
||||
"wh?t",
|
||||
"x[a-z]",
|
||||
"~ommij",
|
||||
];
|
||||
|
||||
for v in INVALID {
|
||||
assert_matches!(T::try_from(v), Err(Error::RefFormat(_)), "input: {}", v)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn valid<T>()
|
||||
where
|
||||
T: TryFrom<&'static str, Error = Error> + AsRef<str> + Debug,
|
||||
{
|
||||
const VALID: [&str; 5] = [
|
||||
"\u{1F32F}",
|
||||
"cl@wn",
|
||||
"foo/bar",
|
||||
"master",
|
||||
"refs/heads/mistress",
|
||||
];
|
||||
|
||||
for v in VALID {
|
||||
assert_matches!(T::try_from(v), Ok(x) if x.as_ref() == v, "input: {}", v)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn empty<T>()
|
||||
where
|
||||
T: TryFrom<&'static str, Error = Error> + Debug,
|
||||
{
|
||||
assert_matches!(T::try_from(""), Err(Error::RefFormat(check::Error::Empty)))
|
||||
}
|
||||
|
||||
pub fn nulsafe<T>()
|
||||
where
|
||||
T: TryFrom<&'static str, Error = Error> + Debug,
|
||||
{
|
||||
assert_matches!(
|
||||
T::try_from("jeff\0"),
|
||||
Err(Error::RefFormat(check::Error::InvalidChar('\0')))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
mod reflike {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
common::empty::<RefLike>()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid() {
|
||||
common::valid::<RefLike>()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid() {
|
||||
common::invalid::<RefLike>()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nulsafe() {
|
||||
common::nulsafe::<RefLike>()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn globstar_invalid() {
|
||||
assert_matches!(
|
||||
RefLike::try_from("refs/heads/*"),
|
||||
Err(Error::RefFormat(check::Error::InvalidChar('*')))
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn into_onelevel() {
|
||||
assert_eq!(
|
||||
&*OneLevel::from(RefLike::try_from("refs/heads/next").unwrap()),
|
||||
"next"
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn into_heads() {
|
||||
assert_eq!(
|
||||
&*Qualified::from(RefLike::try_from("pu").unwrap()),
|
||||
"refs/heads/pu"
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serde() {
|
||||
let refl = RefLike::try_from("pu").unwrap();
|
||||
roundtrip::json(refl.clone());
|
||||
roundtrip::json(OneLevel::from(refl.clone()));
|
||||
roundtrip::json(Qualified::from(refl))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serde_invalid() {
|
||||
let json = serde_json::to_string("HEAD^").unwrap();
|
||||
assert!(serde_json::from_str::<RefLike>(&json).is_err());
|
||||
assert!(serde_json::from_str::<OneLevel>(&json).is_err());
|
||||
assert!(serde_json::from_str::<Qualified>(&json).is_err())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cbor() {
|
||||
let refl = RefLike::try_from("pu").unwrap();
|
||||
roundtrip::cbor(refl.clone());
|
||||
roundtrip::cbor(OneLevel::from(refl.clone()));
|
||||
roundtrip::cbor(Qualified::from(refl))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cbor_invalid() {
|
||||
let cbor = minicbor::to_vec("HEAD^").unwrap();
|
||||
assert!(minicbor::decode::<RefLike>(&cbor).is_err());
|
||||
assert!(minicbor::decode::<OneLevel>(&cbor).is_err());
|
||||
assert!(minicbor::decode::<Qualified>(&cbor).is_err())
|
||||
}
|
||||
}
|
||||
|
||||
mod pattern {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
common::empty::<RefspecPattern>()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid() {
|
||||
common::valid::<RefspecPattern>()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid() {
|
||||
common::invalid::<RefspecPattern>()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nulsafe() {
|
||||
common::nulsafe::<RefspecPattern>()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn globstar_ok() {
|
||||
const GLOBBED: [&str; 7] = [
|
||||
"*",
|
||||
"fo*",
|
||||
"fo*/bar",
|
||||
"foo/*/bar",
|
||||
"foo/ba*",
|
||||
"foo/bar/*",
|
||||
"foo/b*r",
|
||||
];
|
||||
|
||||
for v in GLOBBED {
|
||||
assert_matches!(
|
||||
RefspecPattern::try_from(v),
|
||||
Ok(ref x) if x.as_str() == v,
|
||||
"input: {}", v
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn globstar_invalid() {
|
||||
const GLOBBED: [&str; 12] = [
|
||||
"**",
|
||||
"***",
|
||||
"*/*",
|
||||
"*/L/*",
|
||||
"fo*/*/bar",
|
||||
"fo*/ba*",
|
||||
"fo*/ba*/baz",
|
||||
"fo*/ba*/ba*",
|
||||
"fo*/bar/*",
|
||||
"foo/*/bar/*",
|
||||
"foo/*/bar/*/baz*",
|
||||
"foo/*/bar/*/baz/*",
|
||||
];
|
||||
|
||||
for v in GLOBBED {
|
||||
assert_matches!(
|
||||
RefspecPattern::try_from(v),
|
||||
Err(Error::RefFormat(check::Error::Pattern))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serde() {
|
||||
roundtrip::json(RefspecPattern::try_from("refs/heads/*").unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serde_invalid() {
|
||||
let json = serde_json::to_string("HEAD^").unwrap();
|
||||
assert!(serde_json::from_str::<RefspecPattern>(&json).is_err())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cbor() {
|
||||
roundtrip::cbor(RefspecPattern::try_from("refs/heads/*").unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cbor_invalid() {
|
||||
let cbor = minicbor::to_vec("HEAD^").unwrap();
|
||||
assert!(minicbor::decode::<RefspecPattern>(&cbor).is_err())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strip_prefix_works_for_different_ends() {
|
||||
let refl = RefLike::try_from("refs/heads/next").unwrap();
|
||||
assert_eq!(
|
||||
refl.strip_prefix("refs/heads").unwrap(),
|
||||
refl.strip_prefix("refs/heads/").unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "git-ref-format"
|
||||
version = "0.1.0"
|
||||
authors = ["Kim Altintop <kim@eagain.st>"]
|
||||
edition = "2018"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
test = false
|
||||
|
||||
[features]
|
||||
bstr = ["git-ref-format-core/bstr"]
|
||||
link-literals = ["git-ref-format-core/link-literals"]
|
||||
macro = ["git-ref-format-macro"]
|
||||
minicbor = ["git-ref-format-core/minicbor"]
|
||||
percent-encoding = ["git-ref-format-core/percent-encoding"]
|
||||
serde = ["git-ref-format-core/serde"]
|
||||
|
||||
[dependencies.git-ref-format-core]
|
||||
path = "./core"
|
||||
|
||||
[dependencies.git-ref-format-macro]
|
||||
path = "./macro"
|
||||
optional = true
|
||||
@@ -0,0 +1,34 @@
|
||||
[package]
|
||||
name = "git-ref-format-core"
|
||||
version = "0.1.0"
|
||||
authors = ["Kim Altintop <kim@eagain.st>"]
|
||||
edition = "2018"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
test = false
|
||||
|
||||
[features]
|
||||
link-literals = []
|
||||
|
||||
[dependencies]
|
||||
thiserror = "1.0"
|
||||
|
||||
[dependencies.bstr]
|
||||
version = "0.2"
|
||||
optional = true
|
||||
|
||||
[dependencies.minicbor]
|
||||
version = "0.13"
|
||||
features = ["std"]
|
||||
optional = true
|
||||
|
||||
[dependencies.percent-encoding]
|
||||
version = "2.1.0"
|
||||
optional = true
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0"
|
||||
features = ["derive"]
|
||||
optional = true
|
||||
@@ -0,0 +1,117 @@
|
||||
// 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::convert::TryFrom;
|
||||
|
||||
use minicbor::{
|
||||
decode,
|
||||
encode::{self, Write},
|
||||
Decode,
|
||||
Decoder,
|
||||
Encode,
|
||||
Encoder,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
refspec::{PatternStr, PatternString},
|
||||
Namespaced,
|
||||
Qualified,
|
||||
RefStr,
|
||||
RefString,
|
||||
};
|
||||
|
||||
impl<'de: 'a, 'a> Decode<'de> for &'a RefStr {
|
||||
#[inline]
|
||||
fn decode(d: &mut Decoder<'de>) -> Result<Self, decode::Error> {
|
||||
d.str()
|
||||
.and_then(|s| Self::try_from(s).map_err(|e| decode::Error::Custom(Box::new(e))))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Encode for &'a RefStr {
|
||||
#[inline]
|
||||
fn encode<W: Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
|
||||
e.str(self.as_str())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Decode<'de> for RefString {
|
||||
#[inline]
|
||||
fn decode(d: &mut Decoder<'de>) -> Result<Self, decode::Error> {
|
||||
Decode::decode(d).map(|s: &RefStr| s.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode for RefString {
|
||||
#[inline]
|
||||
fn encode<W: Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
|
||||
self.as_refstr().encode(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de: 'a, 'a> Decode<'de> for &'a PatternStr {
|
||||
#[inline]
|
||||
fn decode(d: &mut Decoder<'de>) -> Result<Self, decode::Error> {
|
||||
d.str()
|
||||
.and_then(|s| Self::try_from(s).map_err(|e| decode::Error::Custom(Box::new(e))))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Encode for &'a PatternStr {
|
||||
#[inline]
|
||||
fn encode<W: Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
|
||||
e.str(self.as_str())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Decode<'de> for PatternString {
|
||||
#[inline]
|
||||
fn decode(d: &mut Decoder<'de>) -> Result<Self, decode::Error> {
|
||||
Decode::decode(d).map(|s: &PatternStr| s.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl Encode for PatternString {
|
||||
#[inline]
|
||||
fn encode<W: Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
|
||||
self.as_pattern_str().encode(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de: 'a, 'a> Decode<'de> for Qualified<'a> {
|
||||
#[inline]
|
||||
fn decode(d: &mut Decoder<'de>) -> Result<Self, decode::Error> {
|
||||
Decode::decode(d).and_then(|s: &RefStr| {
|
||||
s.qualified()
|
||||
.ok_or(decode::Error::Message("not a qualified ref"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Encode for Qualified<'a> {
|
||||
#[inline]
|
||||
fn encode<W: Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
|
||||
self.as_str().encode(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de: 'a, 'a> Decode<'de> for Namespaced<'a> {
|
||||
#[inline]
|
||||
fn decode(d: &mut Decoder<'de>) -> Result<Self, decode::Error> {
|
||||
Decode::decode(d).and_then(|s: &RefStr| {
|
||||
s.namespaced()
|
||||
.ok_or(decode::Error::Message("not a namespaced ref"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Encode for Namespaced<'a> {
|
||||
#[inline]
|
||||
fn encode<W: Write>(&self, e: &mut Encoder<W>) -> Result<(), encode::Error<W::Error>> {
|
||||
self.as_str().encode(e)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
// 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 thiserror::Error;
|
||||
|
||||
pub struct Options {
|
||||
/// If `false`, the refname must contain at least one `/`.
|
||||
pub allow_onelevel: bool,
|
||||
/// If `true`, the refname may contain exactly one `*` character.
|
||||
pub allow_pattern: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
#[error("empty input")]
|
||||
Empty,
|
||||
#[error("lone '@' character")]
|
||||
LoneAt,
|
||||
#[error("consecutive or trailing slash")]
|
||||
Slash,
|
||||
#[error("ends with '.lock'")]
|
||||
DotLock,
|
||||
#[error("consecutive dots ('..')")]
|
||||
DotDot,
|
||||
#[error("at-open-brace ('@{{')")]
|
||||
AtOpenBrace,
|
||||
#[error("invalid character {0:?}")]
|
||||
InvalidChar(char),
|
||||
#[error("component starts with '.'")]
|
||||
StartsDot,
|
||||
#[error("component ends with '.'")]
|
||||
EndsDot,
|
||||
#[error("control character")]
|
||||
Control,
|
||||
#[error("whitespace")]
|
||||
Space,
|
||||
#[error("must contain at most one '*'")]
|
||||
Pattern,
|
||||
#[error("must contain at least one '/'")]
|
||||
OneLevel,
|
||||
}
|
||||
|
||||
/// Validate that a string slice is a valid refname.
|
||||
pub fn ref_format(opts: Options, s: &str) -> Result<(), Error> {
|
||||
match s {
|
||||
"" => Err(Error::Empty),
|
||||
"@" => Err(Error::LoneAt),
|
||||
"." => Err(Error::StartsDot),
|
||||
_ => {
|
||||
let mut globs = 0usize;
|
||||
let mut parts = 0usize;
|
||||
|
||||
for x in s.split('/') {
|
||||
if x.is_empty() {
|
||||
return Err(Error::Slash);
|
||||
}
|
||||
|
||||
parts += 1;
|
||||
|
||||
if x.ends_with(".lock") {
|
||||
return Err(Error::DotLock);
|
||||
}
|
||||
|
||||
let last_char = x.chars().count() - 1;
|
||||
for (i, y) in x.chars().zip(x.chars().cycle().skip(1)).enumerate() {
|
||||
match y {
|
||||
('.', '.') => return Err(Error::DotDot),
|
||||
('@', '{') => return Err(Error::AtOpenBrace),
|
||||
|
||||
('\0', _) => return Err(Error::InvalidChar('\0')),
|
||||
('\\', _) => return Err(Error::InvalidChar('\\')),
|
||||
('~', _) => return Err(Error::InvalidChar('~')),
|
||||
('^', _) => return Err(Error::InvalidChar('^')),
|
||||
(':', _) => return Err(Error::InvalidChar(':')),
|
||||
('?', _) => return Err(Error::InvalidChar('?')),
|
||||
('[', _) => return Err(Error::InvalidChar('[')),
|
||||
|
||||
('*', _) => globs += 1,
|
||||
|
||||
('.', _) if i == 0 => return Err(Error::StartsDot),
|
||||
('.', _) if i == last_char => return Err(Error::EndsDot),
|
||||
|
||||
(' ', _) => return Err(Error::Space),
|
||||
|
||||
(z, _) if z.is_ascii_control() => return Err(Error::Control),
|
||||
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if parts < 2 && !opts.allow_onelevel {
|
||||
Err(Error::OneLevel)
|
||||
} else if globs > 1 && opts.allow_pattern {
|
||||
Err(Error::Pattern)
|
||||
} else if globs > 0 && !opts.allow_pattern {
|
||||
Err(Error::InvalidChar('*'))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,437 @@
|
||||
// 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::{
|
||||
borrow::Cow,
|
||||
fmt::{self, Display},
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
use crate::{lit, name, Component, RefStr, RefString};
|
||||
|
||||
/// A fully-qualified refname.
|
||||
///
|
||||
/// A refname is qualified _iff_ it starts with "refs/" and has at least three
|
||||
/// components. This implies that a [`Qualified`] ref has a category, such as
|
||||
/// "refs/heads/main".
|
||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub struct Qualified<'a>(pub(crate) Cow<'a, RefStr>);
|
||||
|
||||
impl<'a> Qualified<'a> {
|
||||
/// Infallibly create a [`Qualified`] from components.
|
||||
///
|
||||
/// Note that the "refs/" prefix is implicitly added, so `a` is the second
|
||||
/// [`Component`]. Mirroring [`Self::non_empty_components`], providing
|
||||
/// two [`Component`]s guarantees well-formedness of the [`Qualified`].
|
||||
/// `tail` may be empty.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// use git_ref_format::{component, Qualified};
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// "refs/heads/main",
|
||||
/// Qualified::from_components(component::HEADS, component::MAIN, None).as_str()
|
||||
/// )
|
||||
/// ```
|
||||
pub fn from_components<'b, 'c, 'd, A, B, C>(a: A, b: B, tail: C) -> Self
|
||||
where
|
||||
A: Into<Component<'b>>,
|
||||
B: Into<Component<'c>>,
|
||||
C: IntoIterator<Item = Component<'d>>,
|
||||
{
|
||||
let mut inner = name::REFS.join(a.into()).and(b.into());
|
||||
inner.extend(tail);
|
||||
|
||||
Self(inner.into())
|
||||
}
|
||||
|
||||
pub fn from_refstr(r: impl Into<Cow<'a, RefStr>>) -> Option<Self> {
|
||||
Self::_from_refstr(r.into())
|
||||
}
|
||||
|
||||
fn _from_refstr(r: Cow<'a, RefStr>) -> Option<Self> {
|
||||
let mut iter = r.iter();
|
||||
match (iter.next()?, iter.next()?, iter.next()?) {
|
||||
("refs", _, _) => Some(Qualified(r)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.as_ref()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn join<'b, R>(&self, other: R) -> Qualified<'b>
|
||||
where
|
||||
R: AsRef<RefStr>,
|
||||
{
|
||||
Qualified(self.0.join(other).into())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn namespaced(&self) -> Option<Namespaced> {
|
||||
self.0.as_ref().into()
|
||||
}
|
||||
|
||||
/// Add a namespace.
|
||||
///
|
||||
/// Creates a new [`Namespaced`] by prefxing `self` with
|
||||
/// "refs/namespaces/<ns>".
|
||||
pub fn add_namespace<'b>(&self, ns: Component<'b>) -> Namespaced<'a> {
|
||||
Namespaced(Cow::Owned(
|
||||
IntoIterator::into_iter([lit::Refs.into(), lit::Namespaces.into(), ns])
|
||||
.chain(self.0.components())
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Like [`Self::non_empty_components`], but with string slices.
|
||||
pub fn non_empty_iter(&self) -> (&str, &str, &str, name::Iter) {
|
||||
let mut iter = self.iter();
|
||||
(
|
||||
iter.next().unwrap(),
|
||||
iter.next().unwrap(),
|
||||
iter.next().unwrap(),
|
||||
iter,
|
||||
)
|
||||
}
|
||||
|
||||
/// Return the first three [`Component`]s, and a possibly empty iterator
|
||||
/// over the remaining ones.
|
||||
///
|
||||
/// A qualified ref is guaranteed to have at least three components, which
|
||||
/// this method provides a witness of. This is useful eg. for pattern
|
||||
/// matching on the prefix.
|
||||
pub fn non_empty_components(&self) -> (Component, Component, Component, name::Components) {
|
||||
let mut cs = self.components();
|
||||
(
|
||||
cs.next().unwrap(),
|
||||
cs.next().unwrap(),
|
||||
cs.next().unwrap(),
|
||||
cs,
|
||||
)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn to_owned<'b>(&self) -> Qualified<'b> {
|
||||
Qualified(Cow::Owned(self.0.clone().into_owned()))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn into_owned<'b>(self) -> Qualified<'b> {
|
||||
Qualified(Cow::Owned(self.0.into_owned()))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn into_refstring(self) -> RefString {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Qualified<'_> {
|
||||
type Target = RefStr;
|
||||
|
||||
#[inline]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<RefStr> for Qualified<'_> {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &RefStr {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for Qualified<'_> {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Self> for Qualified<'_> {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Qualified<'a>> for Cow<'a, RefStr> {
|
||||
#[inline]
|
||||
fn from(q: Qualified<'a>) -> Self {
|
||||
q.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Qualified<'_>> for RefString {
|
||||
#[inline]
|
||||
fn from(q: Qualified) -> Self {
|
||||
q.0.into_owned()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U> From<(lit::Refs, T, U)> for Qualified<'_>
|
||||
where
|
||||
T: AsRef<RefStr>,
|
||||
U: AsRef<RefStr>,
|
||||
{
|
||||
#[inline]
|
||||
fn from((refs, cat, name): (lit::Refs, T, U)) -> Self {
|
||||
let refs: &RefStr = refs.into();
|
||||
Self(Cow::Owned(refs.join(cat).and(name)))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<lit::RefsHeads<T>> for Qualified<'_>
|
||||
where
|
||||
T: AsRef<RefStr>,
|
||||
{
|
||||
#[inline]
|
||||
fn from((refs, heads, name): lit::RefsHeads<T>) -> Self {
|
||||
Self(Cow::Owned(
|
||||
IntoIterator::into_iter([Component::from(refs), heads.into()])
|
||||
.collect::<RefString>()
|
||||
.and(name),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<lit::RefsTags<T>> for Qualified<'_>
|
||||
where
|
||||
T: AsRef<RefStr>,
|
||||
{
|
||||
#[inline]
|
||||
fn from((refs, tags, name): lit::RefsTags<T>) -> Self {
|
||||
Self(Cow::Owned(
|
||||
IntoIterator::into_iter([Component::from(refs), tags.into()])
|
||||
.collect::<RefString>()
|
||||
.and(name),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<lit::RefsNotes<T>> for Qualified<'_>
|
||||
where
|
||||
T: AsRef<RefStr>,
|
||||
{
|
||||
#[inline]
|
||||
fn from((refs, notes, name): lit::RefsNotes<T>) -> Self {
|
||||
Self(Cow::Owned(
|
||||
IntoIterator::into_iter([Component::from(refs), notes.into()])
|
||||
.collect::<RefString>()
|
||||
.and(name),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<lit::RefsRemotes<T>> for Qualified<'_>
|
||||
where
|
||||
T: AsRef<RefStr>,
|
||||
{
|
||||
#[inline]
|
||||
fn from((refs, remotes, name): lit::RefsRemotes<T>) -> Self {
|
||||
Self(Cow::Owned(
|
||||
IntoIterator::into_iter([Component::from(refs), remotes.into()])
|
||||
.collect::<RefString>()
|
||||
.and(name),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Qualified<'_> {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "link-literals")]
|
||||
mod link {
|
||||
use super::*;
|
||||
|
||||
impl From<lit::RefsRadId> for Qualified<'_> {
|
||||
#[inline]
|
||||
fn from((refs, rad, id): lit::RefsRadId) -> Self {
|
||||
Self(Cow::Owned(
|
||||
IntoIterator::into_iter([Component::from(refs), rad.into(), id.into()]).collect(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lit::RefsRadSelf> for Qualified<'_> {
|
||||
#[inline]
|
||||
fn from((refs, rad, selv): lit::RefsRadSelf) -> Self {
|
||||
Self(Cow::Owned(
|
||||
IntoIterator::into_iter([Component::from(refs), rad.into(), selv.into()]).collect(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<lit::RefsRadSignedRefs> for Qualified<'_> {
|
||||
#[inline]
|
||||
fn from((refs, rad, sig): lit::RefsRadSignedRefs) -> Self {
|
||||
Self(Cow::Owned(
|
||||
IntoIterator::into_iter([Component::from(refs), rad.into(), sig.into()]).collect(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Into<Component<'a>>> From<lit::RefsRadIds<T>> for Qualified<'_> {
|
||||
#[inline]
|
||||
fn from((refs, rad, ids, id): lit::RefsRadIds<T>) -> Self {
|
||||
Self(Cow::Owned(
|
||||
IntoIterator::into_iter([Component::from(refs), rad.into(), ids.into(), id.into()])
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T: Into<Component<'a>>, I: Into<Component<'a>>> From<lit::RefsCobs<T, I>>
|
||||
for Qualified<'_>
|
||||
{
|
||||
#[inline]
|
||||
fn from((refs, cobs, ty, id): lit::RefsCobs<T, I>) -> Self {
|
||||
Self(Cow::Owned(
|
||||
IntoIterator::into_iter([Component::from(refs), cobs.into(), ty.into(), id.into()])
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A [`Qualified`] ref under a git namespace.
|
||||
///
|
||||
/// A ref is namespaced if it starts with "refs/namespaces/", another path
|
||||
/// component, and "refs/". Eg.
|
||||
///
|
||||
/// refs/namespaces/xyz/refs/heads/main
|
||||
///
|
||||
/// Note that namespaces can be nested, so the result of
|
||||
/// [`Namespaced::strip_namespace`] may be convertible to a [`Namespaced`]
|
||||
/// again. For example:
|
||||
///
|
||||
/// ```no_run
|
||||
/// let full = refname!("refs/namespaces/a/refs/namespaces/b/refs/heads/main");
|
||||
/// let namespaced = full.namespaced().unwrap();
|
||||
/// let strip_first = namespaced.strip_namespace();
|
||||
/// let nested = strip_first.namespaced().unwrap();
|
||||
/// let strip_second = nested.strip_namespace();
|
||||
///
|
||||
/// assert_eq!("a", namespaced.namespace().as_str());
|
||||
/// assert_eq!("b", nested.namespace().as_str());
|
||||
/// assert_eq!("refs/namespaces/b/refs/heads/main", strip_first.as_str());
|
||||
/// assert_eq!("refs/heads/main", strip_second.as_str());
|
||||
/// ```
|
||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub struct Namespaced<'a>(Cow<'a, RefStr>);
|
||||
|
||||
impl<'a> Namespaced<'a> {
|
||||
pub fn namespace(&self) -> Component {
|
||||
self.components().nth(2).unwrap()
|
||||
}
|
||||
|
||||
pub fn strip_namespace<'b>(&self) -> Qualified<'b> {
|
||||
const REFS_NAMESPACES: &RefStr = RefStr::from_str("refs/namespaces");
|
||||
|
||||
Qualified(Cow::Owned(
|
||||
self.strip_prefix(REFS_NAMESPACES)
|
||||
.unwrap()
|
||||
.components()
|
||||
.skip(1)
|
||||
.collect(),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn strip_namespace_recursive<'b>(&self) -> Qualified<'b> {
|
||||
let mut strip = self.strip_namespace();
|
||||
while let Some(ns) = strip.namespaced() {
|
||||
strip = ns.strip_namespace();
|
||||
}
|
||||
strip
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn to_owned<'b>(&self) -> Namespaced<'b> {
|
||||
Namespaced(Cow::Owned(self.0.clone().into_owned()))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn into_owned<'b>(self) -> Namespaced<'b> {
|
||||
Namespaced(Cow::Owned(self.0.into_owned()))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn into_qualified(self) -> Qualified<'a> {
|
||||
self.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Namespaced<'_> {
|
||||
type Target = RefStr;
|
||||
|
||||
#[inline]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<RefStr> for Namespaced<'_> {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &RefStr {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for Namespaced<'_> {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Namespaced<'a>> for Qualified<'a> {
|
||||
#[inline]
|
||||
fn from(ns: Namespaced<'a>) -> Self {
|
||||
Self(ns.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a RefStr> for Option<Namespaced<'a>> {
|
||||
fn from(rs: &'a RefStr) -> Self {
|
||||
let mut cs = rs.iter();
|
||||
match (cs.next()?, cs.next()?, cs.next()?, cs.next()?) {
|
||||
("refs", "namespaces", _, "refs") => Some(Namespaced(Cow::from(rs))),
|
||||
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> From<lit::RefsNamespaces<'_, T>> for Namespaced<'static>
|
||||
where
|
||||
T: Into<Component<'a>>,
|
||||
{
|
||||
#[inline]
|
||||
fn from((refs, namespaces, namespace, name): lit::RefsNamespaces<T>) -> Self {
|
||||
Self(Cow::Owned(
|
||||
IntoIterator::into_iter([refs.into(), namespaces.into(), namespace.into()])
|
||||
.collect::<RefString>()
|
||||
.and(name),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Namespaced<'_> {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// 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.
|
||||
|
||||
mod check;
|
||||
pub use check::{ref_format as check_ref_format, Error, Options};
|
||||
|
||||
mod deriv;
|
||||
pub use deriv::{Namespaced, Qualified};
|
||||
|
||||
pub mod lit;
|
||||
|
||||
pub mod name;
|
||||
#[cfg(feature = "percent-encoding")]
|
||||
pub use name::PercentEncode;
|
||||
pub use name::{Component, RefStr, RefString};
|
||||
|
||||
pub mod refspec;
|
||||
pub use refspec::DuplicateGlob;
|
||||
|
||||
#[cfg(feature = "minicbor")]
|
||||
mod cbor;
|
||||
#[cfg(feature = "serde")]
|
||||
mod serde;
|
||||
@@ -0,0 +1,313 @@
|
||||
// Copyright © 2022 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 crate::{name, Qualified, RefStr};
|
||||
|
||||
/// A literal [`RefStr`].
|
||||
///
|
||||
/// Types implementing [`Lit`] must be [`name::Component`]s, and provide a
|
||||
/// conversion from a component _iff_ the component's [`RefStr`] representation
|
||||
/// is equal to [`Lit::NAME`]. Because these morphisms can only be guaranteed
|
||||
/// axiomatically, the trait can not currently be implemented by types outside
|
||||
/// of this crate.
|
||||
///
|
||||
/// [`Lit`] types are useful for efficiently creating known-valid [`Qualified`]
|
||||
/// refs, and sometimes for pattern matching.
|
||||
pub trait Lit: Sized + sealed::Sealed {
|
||||
const SELF: Self;
|
||||
const NAME: &'static RefStr;
|
||||
|
||||
#[inline]
|
||||
fn from_component(c: &name::Component) -> Option<Self> {
|
||||
(c.as_ref() == Self::NAME).then(|| Self::SELF)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Lit> From<T> for &'static RefStr {
|
||||
#[inline]
|
||||
fn from(_: T) -> Self {
|
||||
T::NAME
|
||||
}
|
||||
}
|
||||
|
||||
mod sealed {
|
||||
pub trait Sealed {}
|
||||
}
|
||||
|
||||
/// All known literal [`RefStr`]s.
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub enum KnownLit {
|
||||
Refs,
|
||||
Heads,
|
||||
Namespaces,
|
||||
Remotes,
|
||||
Tags,
|
||||
Notes,
|
||||
|
||||
#[cfg(feature = "link-literals")]
|
||||
Rad,
|
||||
#[cfg(feature = "link-literals")]
|
||||
Id,
|
||||
#[cfg(feature = "link-literals")]
|
||||
Ids,
|
||||
#[cfg(feature = "link-literals")]
|
||||
Selv,
|
||||
#[cfg(feature = "link-literals")]
|
||||
SignedRefs,
|
||||
#[cfg(feature = "link-literals")]
|
||||
Cobs,
|
||||
}
|
||||
|
||||
impl KnownLit {
|
||||
#[inline]
|
||||
pub fn from_component(c: &name::Component) -> Option<Self> {
|
||||
let c: &RefStr = c.as_ref();
|
||||
if c == Refs::NAME {
|
||||
Some(Self::Refs)
|
||||
} else if c == Heads::NAME {
|
||||
Some(Self::Heads)
|
||||
} else if c == Namespaces::NAME {
|
||||
Some(Self::Namespaces)
|
||||
} else if c == Remotes::NAME {
|
||||
Some(Self::Remotes)
|
||||
} else if c == Tags::NAME {
|
||||
Some(Self::Tags)
|
||||
} else if c == Notes::NAME {
|
||||
Some(Self::Notes)
|
||||
} else {
|
||||
#[cfg(feature = "link-literals")]
|
||||
{
|
||||
if c == Rad::NAME {
|
||||
Some(Self::Rad)
|
||||
} else if c == Id::NAME {
|
||||
Some(Self::Id)
|
||||
} else if c == Ids::NAME {
|
||||
Some(Self::Ids)
|
||||
} else if c == Selv::NAME {
|
||||
Some(Self::Selv)
|
||||
} else if c == SignedRefs::NAME {
|
||||
Some(Self::SignedRefs)
|
||||
} else if c == Cobs::NAME {
|
||||
Some(Self::Cobs)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "link-literals"))]
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KnownLit> for name::Component<'_> {
|
||||
#[inline]
|
||||
fn from(k: KnownLit) -> Self {
|
||||
match k {
|
||||
KnownLit::Refs => Refs.into(),
|
||||
KnownLit::Heads => Heads.into(),
|
||||
KnownLit::Namespaces => Namespaces.into(),
|
||||
KnownLit::Remotes => Remotes.into(),
|
||||
KnownLit::Tags => Tags.into(),
|
||||
KnownLit::Notes => Notes.into(),
|
||||
#[cfg(feature = "link-literals")]
|
||||
KnownLit::Rad => Rad.into(),
|
||||
#[cfg(feature = "link-literals")]
|
||||
KnownLit::Id => Id.into(),
|
||||
#[cfg(feature = "link-literals")]
|
||||
KnownLit::Ids => Ids.into(),
|
||||
#[cfg(feature = "link-literals")]
|
||||
KnownLit::Selv => Selv.into(),
|
||||
#[cfg(feature = "link-literals")]
|
||||
KnownLit::SignedRefs => SignedRefs.into(),
|
||||
#[cfg(feature = "link-literals")]
|
||||
KnownLit::Cobs => Cobs.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Either a [`KnownLit`] or a [`name::Component`]
|
||||
pub enum SomeLit<'a> {
|
||||
Known(KnownLit),
|
||||
Any(name::Component<'a>),
|
||||
}
|
||||
|
||||
impl SomeLit<'_> {
|
||||
pub fn known(self) -> Option<KnownLit> {
|
||||
match self {
|
||||
Self::Known(k) => Some(k),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<name::Component<'a>> for SomeLit<'a> {
|
||||
#[inline]
|
||||
fn from(c: name::Component<'a>) -> Self {
|
||||
match KnownLit::from_component(&c) {
|
||||
Some(k) => Self::Known(k),
|
||||
None => Self::Any(c),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type RefsHeads<T> = (Refs, Heads, T);
|
||||
pub type RefsTags<T> = (Refs, Tags, T);
|
||||
pub type RefsNotes<T> = (Refs, Notes, T);
|
||||
pub type RefsRemotes<T> = (Refs, Remotes, T);
|
||||
pub type RefsNamespaces<'a, T> = (Refs, Namespaces, T, Qualified<'a>);
|
||||
|
||||
#[inline]
|
||||
pub fn refs_heads<T: AsRef<RefStr>>(name: T) -> RefsHeads<T> {
|
||||
(Refs, Heads, name)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn refs_tags<T: AsRef<RefStr>>(name: T) -> RefsTags<T> {
|
||||
(Refs, Tags, name)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn refs_notes<T: AsRef<RefStr>>(name: T) -> RefsNotes<T> {
|
||||
(Refs, Notes, name)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn refs_remotes<T: AsRef<RefStr>>(name: T) -> RefsRemotes<T> {
|
||||
(Refs, Remotes, name)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn refs_namespaces<'a, 'b, T>(namespace: T, name: Qualified<'b>) -> RefsNamespaces<'b, T>
|
||||
where
|
||||
T: Into<name::Component<'a>>,
|
||||
{
|
||||
(Refs, Namespaces, namespace, name)
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub struct Refs;
|
||||
|
||||
impl Lit for Refs {
|
||||
const SELF: Self = Self;
|
||||
const NAME: &'static RefStr = name::REFS;
|
||||
}
|
||||
impl sealed::Sealed for Refs {}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub struct Heads;
|
||||
|
||||
impl Lit for Heads {
|
||||
const SELF: Self = Self;
|
||||
const NAME: &'static RefStr = name::HEADS;
|
||||
}
|
||||
impl sealed::Sealed for Heads {}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub struct Namespaces;
|
||||
|
||||
impl Lit for Namespaces {
|
||||
const SELF: Self = Self;
|
||||
const NAME: &'static RefStr = name::NAMESPACES;
|
||||
}
|
||||
impl sealed::Sealed for Namespaces {}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub struct Remotes;
|
||||
|
||||
impl Lit for Remotes {
|
||||
const SELF: Self = Self;
|
||||
const NAME: &'static RefStr = name::REMOTES;
|
||||
}
|
||||
impl sealed::Sealed for Remotes {}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub struct Tags;
|
||||
|
||||
impl Lit for Tags {
|
||||
const SELF: Self = Self;
|
||||
const NAME: &'static RefStr = name::TAGS;
|
||||
}
|
||||
impl sealed::Sealed for Tags {}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub struct Notes;
|
||||
|
||||
impl Lit for Notes {
|
||||
const SELF: Self = Self;
|
||||
const NAME: &'static RefStr = name::NOTES;
|
||||
}
|
||||
impl sealed::Sealed for Notes {}
|
||||
|
||||
#[cfg(feature = "link-literals")]
|
||||
mod link {
|
||||
use super::*;
|
||||
|
||||
pub type RefsRadId = (Refs, Rad, Id);
|
||||
pub type RefsRadSelf = (Refs, Rad, Selv);
|
||||
pub type RefsRadSignedRefs = (Refs, Rad, SignedRefs);
|
||||
pub type RefsRadIds<T> = (Refs, Rad, Ids, T);
|
||||
pub type RefsCobs<T, I> = (Refs, Cobs, T, I);
|
||||
|
||||
pub const REFS_RAD_ID: (Refs, Rad, Id) = (Refs, Rad, Id);
|
||||
pub const REFS_RAD_SELF: (Refs, Rad, Selv) = (Refs, Rad, Selv);
|
||||
pub const REFS_RAD_SIGNED_REFS: (Refs, Rad, SignedRefs) = (Refs, Rad, SignedRefs);
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub struct Rad;
|
||||
|
||||
impl Lit for Rad {
|
||||
const SELF: Self = Self;
|
||||
const NAME: &'static RefStr = RefStr::from_str("rad");
|
||||
}
|
||||
impl sealed::Sealed for Rad {}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub struct Id;
|
||||
|
||||
impl Lit for Id {
|
||||
const SELF: Self = Self;
|
||||
const NAME: &'static RefStr = RefStr::from_str("id");
|
||||
}
|
||||
impl sealed::Sealed for Id {}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub struct Ids;
|
||||
|
||||
impl Lit for Ids {
|
||||
const SELF: Self = Self;
|
||||
const NAME: &'static RefStr = RefStr::from_str("ids");
|
||||
}
|
||||
impl sealed::Sealed for Ids {}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub struct Selv;
|
||||
|
||||
impl Lit for Selv {
|
||||
const SELF: Self = Self;
|
||||
const NAME: &'static RefStr = RefStr::from_str("self");
|
||||
}
|
||||
impl sealed::Sealed for Selv {}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub struct SignedRefs;
|
||||
|
||||
impl Lit for SignedRefs {
|
||||
const SELF: Self = Self;
|
||||
const NAME: &'static RefStr = RefStr::from_str("signed_refs");
|
||||
}
|
||||
impl sealed::Sealed for SignedRefs {}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub struct Cobs;
|
||||
|
||||
impl Lit for Cobs {
|
||||
const SELF: Self = Self;
|
||||
const NAME: &'static RefStr = RefStr::from_str("cobs");
|
||||
}
|
||||
impl sealed::Sealed for Cobs {}
|
||||
}
|
||||
|
||||
#[cfg(feature = "link-literals")]
|
||||
pub use link::*;
|
||||
@@ -0,0 +1,522 @@
|
||||
// Copyright © 2022 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::{
|
||||
borrow::{Borrow, Cow},
|
||||
convert::TryFrom,
|
||||
fmt::{self, Display},
|
||||
iter::{Extend, FromIterator},
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
check,
|
||||
refspec::{PatternStr, PatternString},
|
||||
Namespaced,
|
||||
Qualified,
|
||||
};
|
||||
|
||||
mod iter;
|
||||
pub use iter::{component, Component, Components, Iter};
|
||||
|
||||
#[cfg(feature = "percent-encoding")]
|
||||
pub use percent_encoding::PercentEncode;
|
||||
|
||||
pub const HEADS: &RefStr = RefStr::from_str(str::HEADS);
|
||||
pub const MAIN: &RefStr = RefStr::from_str(str::MAIN);
|
||||
pub const MASTER: &RefStr = RefStr::from_str(str::MASTER);
|
||||
pub const NAMESPACES: &RefStr = RefStr::from_str(str::NAMESPACES);
|
||||
pub const NOTES: &RefStr = RefStr::from_str(str::NOTES);
|
||||
pub const ORIGIN: &RefStr = RefStr::from_str(str::ORIGIN);
|
||||
pub const REFS: &RefStr = RefStr::from_str(str::REFS);
|
||||
pub const REMOTES: &RefStr = RefStr::from_str(str::REMOTES);
|
||||
pub const TAGS: &RefStr = RefStr::from_str(str::TAGS);
|
||||
|
||||
pub const REFS_HEADS_MAIN: &RefStr = RefStr::from_str(str::REFS_HEADS_MAIN);
|
||||
pub const REFS_HEADS_MASTER: &RefStr = RefStr::from_str(str::REFS_HEADS_MASTER);
|
||||
|
||||
pub mod str {
|
||||
pub const HEADS: &str = "heads";
|
||||
pub const MAIN: &str = "main";
|
||||
pub const MASTER: &str = "master";
|
||||
pub const NAMESPACES: &str = "namespaces";
|
||||
pub const NOTES: &str = "notes";
|
||||
pub const ORIGIN: &str = "origin";
|
||||
pub const REFS: &str = "refs";
|
||||
pub const REMOTES: &str = "remotes";
|
||||
pub const TAGS: &str = "tags";
|
||||
|
||||
pub const REFS_HEADS_MAIN: &str = "refs/heads/main";
|
||||
pub const REFS_HEADS_MASTER: &str = "refs/heads/master";
|
||||
|
||||
#[cfg(feature = "link-literals")]
|
||||
mod link {
|
||||
pub const RAD: &str = "rad";
|
||||
pub const ID: &str = "id";
|
||||
pub const IDS: &str = "ids";
|
||||
pub const SELF: &str = "self";
|
||||
pub const SIGNED_REFS: &str = "signed_refs";
|
||||
pub const COBS: &str = "cobs";
|
||||
|
||||
pub const REFS_RAD_ID: &str = "refs/rad/id";
|
||||
pub const REFS_RAD_SELF: &str = "refs/rad/self";
|
||||
pub const REFS_RAD_SIGNED_REFS: &str = "refs/rad/signed_refs";
|
||||
}
|
||||
#[cfg(feature = "link-literals")]
|
||||
pub use link::*;
|
||||
}
|
||||
|
||||
pub mod bytes {
|
||||
use super::str;
|
||||
|
||||
pub const HEADS: &[u8] = str::HEADS.as_bytes();
|
||||
pub const MAIN: &[u8] = str::MAIN.as_bytes();
|
||||
pub const MASTER: &[u8] = str::MASTER.as_bytes();
|
||||
pub const NAMESPACES: &[u8] = str::NAMESPACES.as_bytes();
|
||||
pub const NOTES: &[u8] = str::NOTES.as_bytes();
|
||||
pub const ORIGIN: &[u8] = str::ORIGIN.as_bytes();
|
||||
pub const REFS: &[u8] = str::REFS.as_bytes();
|
||||
pub const REMOTES: &[u8] = str::REMOTES.as_bytes();
|
||||
pub const TAGS: &[u8] = str::TAGS.as_bytes();
|
||||
|
||||
pub const REFS_HEADS_MAIN: &[u8] = str::REFS_HEADS_MAIN.as_bytes();
|
||||
pub const REFS_HEADS_MASTER: &[u8] = str::REFS_HEADS_MASTER.as_bytes();
|
||||
|
||||
#[cfg(feature = "link-literals")]
|
||||
mod link {
|
||||
use super::str;
|
||||
|
||||
pub const RAD: &[u8] = str::RAD.as_bytes();
|
||||
pub const ID: &[u8] = str::ID.as_bytes();
|
||||
pub const IDS: &[u8] = str::IDS.as_bytes();
|
||||
pub const SELF: &[u8] = str::SELF.as_bytes();
|
||||
pub const SIGNED_REFS: &[u8] = str::SIGNED_REFS.as_bytes();
|
||||
pub const COBS: &[u8] = str::COBS.as_bytes();
|
||||
|
||||
pub const REFS_RAD_ID: &[u8] = str::REFS_RAD_ID.as_bytes();
|
||||
pub const REFS_RAD_SELF: &[u8] = str::REFS_RAD_SELF.as_bytes();
|
||||
pub const REFS_RAD_SIGNED_REFS: &[u8] = str::REFS_RAD_SIGNED_REFS.as_bytes();
|
||||
}
|
||||
#[cfg(feature = "link-literals")]
|
||||
pub use link::*;
|
||||
}
|
||||
|
||||
#[cfg(feature = "link-literals")]
|
||||
mod link {
|
||||
use super::{str, RefStr};
|
||||
|
||||
pub const RAD: &RefStr = RefStr::from_str(str::RAD);
|
||||
pub const ID: &RefStr = RefStr::from_str(str::ID);
|
||||
pub const IDS: &RefStr = RefStr::from_str(str::IDS);
|
||||
pub const SELF: &RefStr = RefStr::from_str(str::SELF);
|
||||
pub const SIGNED_REFS: &RefStr = RefStr::from_str(str::SIGNED_REFS);
|
||||
pub const COBS: &RefStr = RefStr::from_str(str::COBS);
|
||||
|
||||
pub const REFS_RAD_ID: &RefStr = RefStr::from_str(str::REFS_RAD_ID);
|
||||
pub const REFS_RAD_SELF: &RefStr = RefStr::from_str(str::REFS_RAD_SELF);
|
||||
pub const REFS_RAD_SIGNED_REFS: &RefStr = RefStr::from_str(str::REFS_RAD_SIGNED_REFS);
|
||||
}
|
||||
#[cfg(feature = "link-literals")]
|
||||
pub use link::*;
|
||||
|
||||
const CHECK_OPTS: check::Options = check::Options {
|
||||
allow_pattern: false,
|
||||
allow_onelevel: true,
|
||||
};
|
||||
|
||||
#[repr(transparent)]
|
||||
#[derive(Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub struct RefStr(str);
|
||||
|
||||
impl RefStr {
|
||||
pub fn try_from_str(s: &str) -> Result<&RefStr, check::Error> {
|
||||
TryFrom::try_from(s)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn as_str(&self) -> &str {
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn to_ref_string(&self) -> RefString {
|
||||
self.to_owned()
|
||||
}
|
||||
|
||||
pub fn strip_prefix<P>(&self, base: P) -> Option<&RefStr>
|
||||
where
|
||||
P: AsRef<RefStr>,
|
||||
{
|
||||
self._strip_prefix(base.as_ref())
|
||||
}
|
||||
|
||||
fn _strip_prefix(&self, base: &RefStr) -> Option<&RefStr> {
|
||||
self.0
|
||||
.strip_prefix(base.as_str())
|
||||
.and_then(|s| s.strip_prefix('/'))
|
||||
.map(Self::from_str)
|
||||
}
|
||||
|
||||
/// Join `other` onto `self`, yielding a new [`RefString`].
|
||||
///
|
||||
/// Consider to use [`RefString::and`] when chaining multiple fragments
|
||||
/// together, and the intermediate values are not needed.
|
||||
pub fn join<R>(&self, other: R) -> RefString
|
||||
where
|
||||
R: AsRef<RefStr>,
|
||||
{
|
||||
self._join(other.as_ref())
|
||||
}
|
||||
|
||||
fn _join(&self, other: &RefStr) -> RefString {
|
||||
let mut buf = self.to_ref_string();
|
||||
buf.push(other);
|
||||
buf
|
||||
}
|
||||
|
||||
pub fn to_pattern<P>(&self, pattern: P) -> PatternString
|
||||
where
|
||||
P: AsRef<PatternStr>,
|
||||
{
|
||||
self._to_pattern(pattern.as_ref())
|
||||
}
|
||||
|
||||
fn _to_pattern(&self, pattern: &PatternStr) -> PatternString {
|
||||
self.to_owned().with_pattern(pattern)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn qualified(&self) -> Option<Qualified> {
|
||||
Qualified::from_refstr(self)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn namespaced(&self) -> Option<Namespaced> {
|
||||
self.into()
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> Iter {
|
||||
self.0.split('/')
|
||||
}
|
||||
|
||||
pub fn components(&self) -> Components {
|
||||
Components::from(self)
|
||||
}
|
||||
|
||||
pub fn head(&self) -> Component {
|
||||
self.components().next().expect("`RefStr` cannot be empty")
|
||||
}
|
||||
|
||||
#[cfg(feature = "percent-encoding")]
|
||||
pub fn percent_encode(&self) -> PercentEncode {
|
||||
/// https://url.spec.whatwg.org/#fragment-percent-encode-set
|
||||
const FRAGMENT_PERCENT_ENCODE_SET: &percent_encoding::AsciiSet =
|
||||
&percent_encoding::CONTROLS
|
||||
.add(b' ')
|
||||
.add(b'"')
|
||||
.add(b'<')
|
||||
.add(b'>')
|
||||
.add(b'`');
|
||||
|
||||
/// https://url.spec.whatwg.org/#path-percent-encode-set
|
||||
const PATH_PERCENT_ENCODE_SET: &percent_encoding::AsciiSet = &FRAGMENT_PERCENT_ENCODE_SET
|
||||
.add(b'#')
|
||||
.add(b'?')
|
||||
.add(b'{')
|
||||
.add(b'}');
|
||||
|
||||
percent_encoding::utf8_percent_encode(self.as_str(), PATH_PERCENT_ENCODE_SET)
|
||||
}
|
||||
|
||||
#[cfg(feature = "bstr")]
|
||||
#[inline]
|
||||
pub fn as_bstr(&self) -> &bstr::BStr {
|
||||
self.as_ref()
|
||||
}
|
||||
|
||||
pub(crate) const fn from_str(s: &str) -> &RefStr {
|
||||
unsafe { &*(s as *const str as *const RefStr) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for RefStr {
|
||||
type Target = str;
|
||||
|
||||
#[inline]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for RefStr {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &str {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "bstr")]
|
||||
impl AsRef<bstr::BStr> for RefStr {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &bstr::BStr {
|
||||
use bstr::ByteSlice as _;
|
||||
self.as_str().as_bytes().as_bstr()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> AsRef<RefStr> for &'a RefStr {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &RefStr {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a str> for &'a RefStr {
|
||||
type Error = check::Error;
|
||||
|
||||
#[inline]
|
||||
fn try_from(s: &'a str) -> Result<Self, Self::Error> {
|
||||
check::ref_format(CHECK_OPTS, s).map(|()| RefStr::from_str(s))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a RefStr> for Cow<'a, RefStr> {
|
||||
#[inline]
|
||||
fn from(rs: &'a RefStr) -> Cow<'a, RefStr> {
|
||||
Cow::Borrowed(rs)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for RefStr {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub struct RefString(String);
|
||||
|
||||
impl RefString {
|
||||
#[inline]
|
||||
pub fn as_refstr(&self) -> &RefStr {
|
||||
self
|
||||
}
|
||||
|
||||
/// Join `other` onto `self` in place.
|
||||
///
|
||||
/// This is a consuming version of [`RefString::push`] which can be chained.
|
||||
/// Prefer this over chaining calls to [`RefStr::join`] if the
|
||||
/// intermediate values are not neede.
|
||||
pub fn and<R>(self, other: R) -> Self
|
||||
where
|
||||
R: AsRef<RefStr>,
|
||||
{
|
||||
self._and(other.as_ref())
|
||||
}
|
||||
|
||||
fn _and(mut self, other: &RefStr) -> Self {
|
||||
self.push(other);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn push<R>(&mut self, other: R)
|
||||
where
|
||||
R: AsRef<RefStr>,
|
||||
{
|
||||
self.0.push('/');
|
||||
self.0.push_str(other.as_ref().as_str());
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn pop(&mut self) -> bool {
|
||||
match self.0.rfind('/') {
|
||||
None => false,
|
||||
Some(idx) => {
|
||||
self.0.truncate(idx);
|
||||
true
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Append a [`PatternStr`], turning self into a new [`PatternString`].
|
||||
pub fn with_pattern<P>(self, pattern: P) -> PatternString
|
||||
where
|
||||
P: AsRef<PatternStr>,
|
||||
{
|
||||
self._with_pattern(pattern.as_ref())
|
||||
}
|
||||
|
||||
fn _with_pattern(self, pattern: &PatternStr) -> PatternString {
|
||||
let mut buf = self.0;
|
||||
buf.push('/');
|
||||
buf.push_str(pattern.as_str());
|
||||
|
||||
PatternString(buf)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn into_qualified<'a>(self) -> Option<Qualified<'a>> {
|
||||
Qualified::from_refstr(self)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn reserve(&mut self, additional: usize) {
|
||||
self.0.reserve(additional)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn shrink_to_fit(&mut self) {
|
||||
self.0.shrink_to_fit()
|
||||
}
|
||||
|
||||
#[cfg(feature = "bstr")]
|
||||
#[inline]
|
||||
pub fn into_bstring(self) -> bstr::BString {
|
||||
self.into()
|
||||
}
|
||||
|
||||
#[cfg(feature = "bstr")]
|
||||
#[inline]
|
||||
pub fn as_bstr(&self) -> &bstr::BStr {
|
||||
self.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for RefString {
|
||||
type Target = RefStr;
|
||||
|
||||
#[inline]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.borrow()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<RefStr> for RefString {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &RefStr {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for RefString {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "bstr")]
|
||||
impl AsRef<bstr::BStr> for RefString {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &bstr::BStr {
|
||||
use bstr::ByteSlice as _;
|
||||
self.as_str().as_bytes().as_bstr()
|
||||
}
|
||||
}
|
||||
|
||||
impl Borrow<RefStr> for RefString {
|
||||
#[inline]
|
||||
fn borrow(&self) -> &RefStr {
|
||||
RefStr::from_str(self.0.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl ToOwned for RefStr {
|
||||
type Owned = RefString;
|
||||
|
||||
#[inline]
|
||||
fn to_owned(&self) -> Self::Owned {
|
||||
RefString(self.0.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for RefString {
|
||||
type Error = check::Error;
|
||||
|
||||
#[inline]
|
||||
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
||||
RefStr::try_from_str(s).map(ToOwned::to_owned)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for RefString {
|
||||
type Error = check::Error;
|
||||
|
||||
#[inline]
|
||||
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||
check::ref_format(CHECK_OPTS, s.as_str()).map(|()| RefString(s))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a RefString> for Cow<'a, RefStr> {
|
||||
#[inline]
|
||||
fn from(rs: &'a RefString) -> Cow<'a, RefStr> {
|
||||
Cow::Borrowed(rs.as_refstr())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<RefString> for Cow<'a, RefStr> {
|
||||
#[inline]
|
||||
fn from(rs: RefString) -> Cow<'a, RefStr> {
|
||||
Cow::Owned(rs)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RefString> for String {
|
||||
#[inline]
|
||||
fn from(rs: RefString) -> Self {
|
||||
rs.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "bstr")]
|
||||
impl From<RefString> for bstr::BString {
|
||||
#[inline]
|
||||
fn from(rs: RefString) -> Self {
|
||||
bstr::BString::from(rs.0.into_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for RefString {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<A> FromIterator<A> for RefString
|
||||
where
|
||||
A: AsRef<RefStr>,
|
||||
{
|
||||
fn from_iter<T>(iter: T) -> Self
|
||||
where
|
||||
T: IntoIterator<Item = A>,
|
||||
{
|
||||
let mut buf = String::new();
|
||||
for c in iter {
|
||||
buf.push_str(c.as_ref().as_str());
|
||||
buf.push('/');
|
||||
}
|
||||
assert!(!buf.is_empty(), "empty iterator");
|
||||
buf.truncate(buf.len() - 1);
|
||||
|
||||
Self(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl<A> Extend<A> for RefString
|
||||
where
|
||||
A: AsRef<RefStr>,
|
||||
{
|
||||
fn extend<T>(&mut self, iter: T)
|
||||
where
|
||||
T: IntoIterator<Item = A>,
|
||||
{
|
||||
for x in iter {
|
||||
self.push(x)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
// 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::{
|
||||
borrow::Cow,
|
||||
fmt::{self, Display},
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
use super::{RefStr, RefString};
|
||||
use crate::lit;
|
||||
|
||||
pub type Iter<'a> = std::str::Split<'a, char>;
|
||||
|
||||
/// Iterator created by the [`RefStr::iter`] method.
|
||||
#[must_use = "iterators are lazy and do nothing unless consumed"]
|
||||
#[derive(Clone)]
|
||||
pub struct Components<'a> {
|
||||
inner: std::str::Split<'a, char>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Components<'a> {
|
||||
type Item = Component<'a>;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.inner
|
||||
.next()
|
||||
.map(RefStr::from_str)
|
||||
.map(Cow::from)
|
||||
.map(Component)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a RefStr> for Components<'a> {
|
||||
#[inline]
|
||||
fn from(rs: &'a RefStr) -> Self {
|
||||
Self {
|
||||
inner: rs.as_str().split('/'),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A path component of a [`RefStr`].
|
||||
///
|
||||
/// A [`Component`] is a valid [`RefStr`] which does not contain any '/'
|
||||
/// separators.
|
||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub struct Component<'a>(Cow<'a, RefStr>);
|
||||
|
||||
impl<'a> Component<'a> {
|
||||
#[inline]
|
||||
pub fn from_refstring(r: RefString) -> Option<Self> {
|
||||
if !r.contains('/') {
|
||||
Some(Self(Cow::Owned(r)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn as_lit<T: lit::Lit>(&self) -> Option<T> {
|
||||
T::from_component(self)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn into_inner(self) -> Cow<'a, RefStr> {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Deref for Component<'a> {
|
||||
type Target = RefStr;
|
||||
|
||||
#[inline]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<RefStr> for Component<'_> {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &RefStr {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a RefStr> for Option<Component<'a>> {
|
||||
#[inline]
|
||||
fn from(r: &'a RefStr) -> Self {
|
||||
if !r.contains('/') {
|
||||
Some(Component(Cow::from(r)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Component<'a>> for Cow<'a, RefStr> {
|
||||
#[inline]
|
||||
fn from(c: Component<'a>) -> Self {
|
||||
c.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: lit::Lit> From<T> for Component<'static> {
|
||||
#[inline]
|
||||
fn from(_: T) -> Self {
|
||||
Component(Cow::from(T::NAME))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<lit::SomeLit<'a>> for Component<'a> {
|
||||
#[inline]
|
||||
fn from(s: lit::SomeLit<'a>) -> Self {
|
||||
use lit::SomeLit::*;
|
||||
|
||||
match s {
|
||||
Known(k) => k.into(),
|
||||
Any(c) => c,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Component<'_> {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str(self.0.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
pub mod component {
|
||||
use super::Component;
|
||||
use crate::name;
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub const HEADS: Component = Component(Cow::Borrowed(name::HEADS));
|
||||
pub const MAIN: Component = Component(Cow::Borrowed(name::MAIN));
|
||||
pub const MASTER: Component = Component(Cow::Borrowed(name::MASTER));
|
||||
pub const NAMESPACES: Component = Component(Cow::Borrowed(name::NAMESPACES));
|
||||
pub const NOTES: Component = Component(Cow::Borrowed(name::NOTES));
|
||||
pub const ORIGIN: Component = Component(Cow::Borrowed(name::ORIGIN));
|
||||
pub const REFS: Component = Component(Cow::Borrowed(name::REFS));
|
||||
pub const REMOTES: Component = Component(Cow::Borrowed(name::REMOTES));
|
||||
pub const TAGS: Component = Component(Cow::Borrowed(name::TAGS));
|
||||
|
||||
#[cfg(feature = "link-literals")]
|
||||
mod link {
|
||||
use super::*;
|
||||
|
||||
pub const RAD: Component = Component(Cow::Borrowed(name::RAD));
|
||||
pub const ID: Component = Component(Cow::Borrowed(name::ID));
|
||||
pub const IDS: Component = Component(Cow::Borrowed(name::IDS));
|
||||
pub const SELF: Component = Component(Cow::Borrowed(name::SELF));
|
||||
pub const SIGNED_REFS: Component = Component(Cow::Borrowed(name::SIGNED_REFS));
|
||||
pub const COBS: Component = Component(Cow::Borrowed(name::COBS));
|
||||
}
|
||||
#[cfg(feature = "link-literals")]
|
||||
pub use link::*;
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
// 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::{
|
||||
borrow::{Borrow, Cow},
|
||||
convert::TryFrom,
|
||||
fmt::{self, Display},
|
||||
iter::FromIterator,
|
||||
ops::Deref,
|
||||
};
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{check, RefStr, RefString};
|
||||
|
||||
mod iter;
|
||||
pub use iter::{Component, Components, Iter};
|
||||
|
||||
pub const STAR: &PatternStr = PatternStr::from_str("*");
|
||||
|
||||
const CHECK_OPTS: check::Options = check::Options {
|
||||
allow_onelevel: true,
|
||||
allow_pattern: true,
|
||||
};
|
||||
|
||||
#[repr(transparent)]
|
||||
#[derive(Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub struct PatternStr(str);
|
||||
|
||||
impl PatternStr {
|
||||
#[inline]
|
||||
pub fn try_from_str(s: &str) -> Result<&Self, check::Error> {
|
||||
TryFrom::try_from(s)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn as_str(&self) -> &str {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn join<R>(&self, other: R) -> PatternString
|
||||
where
|
||||
R: AsRef<RefStr>,
|
||||
{
|
||||
self._join(other.as_ref())
|
||||
}
|
||||
|
||||
fn _join(&self, other: &RefStr) -> PatternString {
|
||||
let mut buf = self.to_owned();
|
||||
buf.push(other);
|
||||
buf
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn iter(&self) -> Iter {
|
||||
self.0.split('/')
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn components(&self) -> Components {
|
||||
Components::from(self)
|
||||
}
|
||||
|
||||
pub(crate) const fn from_str(s: &str) -> &PatternStr {
|
||||
unsafe { &*(s as *const str as *const PatternStr) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for PatternStr {
|
||||
type Target = str;
|
||||
|
||||
#[inline]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for PatternStr {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &str {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Self> for PatternStr {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a str> for &'a PatternStr {
|
||||
type Error = check::Error;
|
||||
|
||||
#[inline]
|
||||
fn try_from(s: &'a str) -> Result<Self, Self::Error> {
|
||||
check::ref_format(CHECK_OPTS, s).map(|()| PatternStr::from_str(s))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a RefStr> for &'a PatternStr {
|
||||
#[inline]
|
||||
fn from(rs: &'a RefStr) -> Self {
|
||||
PatternStr::from_str(rs.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a PatternStr> for Cow<'a, PatternStr> {
|
||||
#[inline]
|
||||
fn from(p: &'a PatternStr) -> Cow<'a, PatternStr> {
|
||||
Cow::Borrowed(p)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for PatternStr {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Hash)]
|
||||
pub struct PatternString(pub(crate) String);
|
||||
|
||||
impl PatternString {
|
||||
#[inline]
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.as_ref()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn as_pattern_str(&self) -> &PatternStr {
|
||||
self.as_ref()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn from_components<'a, T>(iter: T) -> Result<Self, DuplicateGlob>
|
||||
where
|
||||
T: IntoIterator<Item = Component<'a>>,
|
||||
{
|
||||
iter.into_iter().collect()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn and<R>(mut self, other: R) -> Self
|
||||
where
|
||||
R: AsRef<RefStr>,
|
||||
{
|
||||
self._push(other.as_ref());
|
||||
self
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn push<R>(&mut self, other: R)
|
||||
where
|
||||
R: AsRef<RefStr>,
|
||||
{
|
||||
self._push(other.as_ref())
|
||||
}
|
||||
|
||||
fn _push(&mut self, other: &RefStr) {
|
||||
self.0.push('/');
|
||||
self.0.push_str(other.as_str());
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn pop(&mut self) -> bool {
|
||||
match self.0.rfind('/') {
|
||||
None => false,
|
||||
Some(idx) => {
|
||||
self.0.truncate(idx);
|
||||
true
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for PatternString {
|
||||
type Target = PatternStr;
|
||||
|
||||
#[inline]
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.borrow()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<PatternStr> for PatternString {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &PatternStr {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for PatternString {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &str {
|
||||
self.0.as_str()
|
||||
}
|
||||
}
|
||||
|
||||
impl Borrow<PatternStr> for PatternString {
|
||||
#[inline]
|
||||
fn borrow(&self) -> &PatternStr {
|
||||
PatternStr::from_str(self.0.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl ToOwned for PatternStr {
|
||||
type Owned = PatternString;
|
||||
|
||||
#[inline]
|
||||
fn to_owned(&self) -> Self::Owned {
|
||||
PatternString(self.0.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RefString> for PatternString {
|
||||
#[inline]
|
||||
fn from(rs: RefString) -> Self {
|
||||
Self(rs.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a PatternString> for Cow<'a, PatternStr> {
|
||||
#[inline]
|
||||
fn from(p: &'a PatternString) -> Cow<'a, PatternStr> {
|
||||
Cow::Borrowed(p.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PatternString> for String {
|
||||
#[inline]
|
||||
fn from(p: PatternString) -> Self {
|
||||
p.0
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for PatternString {
|
||||
type Error = check::Error;
|
||||
|
||||
#[inline]
|
||||
fn try_from(s: &str) -> Result<Self, Self::Error> {
|
||||
PatternStr::try_from_str(s).map(ToOwned::to_owned)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for PatternString {
|
||||
type Error = check::Error;
|
||||
|
||||
#[inline]
|
||||
fn try_from(s: String) -> Result<Self, Self::Error> {
|
||||
check::ref_format(CHECK_OPTS, s.as_str()).map(|()| PatternString(s))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("more than one '*' encountered")]
|
||||
pub struct DuplicateGlob;
|
||||
|
||||
impl<'a> FromIterator<Component<'a>> for Result<PatternString, DuplicateGlob> {
|
||||
fn from_iter<T>(iter: T) -> Self
|
||||
where
|
||||
T: IntoIterator<Item = Component<'a>>,
|
||||
{
|
||||
use Component::*;
|
||||
|
||||
let mut buf = String::new();
|
||||
let mut seen_glob = false;
|
||||
for c in iter {
|
||||
if let Glob(_) = c {
|
||||
if seen_glob {
|
||||
return Err(DuplicateGlob);
|
||||
}
|
||||
|
||||
seen_glob = true;
|
||||
}
|
||||
|
||||
buf.push_str(c.as_str());
|
||||
buf.push('/');
|
||||
}
|
||||
buf.truncate(buf.len() - 1);
|
||||
|
||||
Ok(PatternString(buf))
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for PatternString {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str(&self.0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// 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::fmt::{self, Display};
|
||||
|
||||
use super::PatternStr;
|
||||
use crate::RefStr;
|
||||
|
||||
pub type Iter<'a> = std::str::Split<'a, char>;
|
||||
|
||||
pub enum Component<'a> {
|
||||
Glob(Option<&'a PatternStr>),
|
||||
Normal(&'a RefStr),
|
||||
}
|
||||
|
||||
impl Component<'_> {
|
||||
#[inline]
|
||||
pub fn as_str(&self) -> &str {
|
||||
self.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for Component<'_> {
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &str {
|
||||
match self {
|
||||
Self::Glob(None) => "*",
|
||||
Self::Glob(Some(x)) => x.as_str(),
|
||||
Self::Normal(x) => x.as_str(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Component<'_> {
|
||||
#[inline]
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
f.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use = "iterators are lazy and do nothing unless consumed"]
|
||||
#[derive(Clone)]
|
||||
pub struct Components<'a> {
|
||||
inner: Iter<'a>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Components<'a> {
|
||||
type Item = Component<'a>;
|
||||
|
||||
#[inline]
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.inner.next().map(|next| match next {
|
||||
"*" => Component::Glob(None),
|
||||
x if x.contains('*') => Component::Glob(Some(PatternStr::from_str(x))),
|
||||
x => Component::Normal(RefStr::from_str(x)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a PatternStr> for Components<'a> {
|
||||
#[inline]
|
||||
fn from(p: &'a PatternStr) -> Self {
|
||||
Self {
|
||||
inner: p.as_str().split('/'),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// 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::convert::TryFrom;
|
||||
|
||||
use ::serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
use crate::{
|
||||
refspec::{PatternStr, PatternString},
|
||||
Namespaced,
|
||||
Qualified,
|
||||
RefStr,
|
||||
RefString,
|
||||
};
|
||||
|
||||
impl<'de: 'a, 'a> Deserialize<'de> for &'a RefStr {
|
||||
#[inline]
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Deserialize::deserialize(deserializer)
|
||||
.and_then(|s: &str| Self::try_from(s).map_err(de::Error::custom))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Serialize for &'a RefStr {
|
||||
#[inline]
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for RefString {
|
||||
#[inline]
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Deserialize::deserialize(deserializer).map(|x: &RefStr| x.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for RefString {
|
||||
#[inline]
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de: 'a, 'a> Deserialize<'de> for &'a PatternStr {
|
||||
#[inline]
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Deserialize::deserialize(deserializer)
|
||||
.and_then(|s: &str| Self::try_from(s).map_err(de::Error::custom))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Serialize for &'a PatternStr {
|
||||
#[inline]
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for PatternString {
|
||||
#[inline]
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Deserialize::deserialize(deserializer).map(|x: &PatternStr| x.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for PatternString {
|
||||
#[inline]
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de: 'a, 'a> Deserialize<'de> for Qualified<'a> {
|
||||
#[inline]
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Deserialize::deserialize(deserializer).and_then(|s: &RefStr| {
|
||||
s.qualified()
|
||||
.ok_or_else(|| de::Error::custom("not a qualified ref"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Qualified<'_> {
|
||||
#[inline]
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de: 'a, 'a> Deserialize<'de> for Namespaced<'a> {
|
||||
#[inline]
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Deserialize::deserialize(deserializer).and_then(|s: &RefStr| {
|
||||
s.namespaced()
|
||||
.ok_or_else(|| de::Error::custom("not a namespaced ref"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for Namespaced<'_> {
|
||||
#[inline]
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_str())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "git-ref-format-macro"
|
||||
version = "0.1.0"
|
||||
authors = ["Kim Altintop <kim@eagain.st>"]
|
||||
edition = "2018"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
proc-macro = true
|
||||
test = false
|
||||
|
||||
[dependencies]
|
||||
proc-macro-error = "1.0.4"
|
||||
quote = "1"
|
||||
syn = "1"
|
||||
|
||||
[dependencies.git-ref-format-core]
|
||||
path = "../core"
|
||||
@@ -0,0 +1,122 @@
|
||||
// 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.
|
||||
|
||||
#[macro_use]
|
||||
extern crate proc_macro_error;
|
||||
|
||||
use std::convert::TryInto;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro_error::abort;
|
||||
use quote::quote;
|
||||
use syn::{parse_macro_input, LitStr};
|
||||
|
||||
use git_ref_format_core::{refspec::PatternStr, Component, Error, RefStr};
|
||||
|
||||
/// Create a [`git_ref_format_core::RefString`] from a string literal.
|
||||
///
|
||||
/// The string is validated at compile time, and an unsafe conversion is
|
||||
/// emitted.
|
||||
#[proc_macro_error]
|
||||
#[proc_macro]
|
||||
pub fn refname(input: TokenStream) -> TokenStream {
|
||||
let lit = parse_macro_input!(input as LitStr);
|
||||
let val = lit.value();
|
||||
|
||||
let parsed: Result<&RefStr, Error> = val.as_str().try_into();
|
||||
match parsed {
|
||||
Ok(safe) => {
|
||||
let safe: &str = safe.as_str();
|
||||
let expand = quote! {
|
||||
unsafe {
|
||||
use ::std::mem::transmute;
|
||||
use ::git_ref_format::RefString;
|
||||
|
||||
transmute::<_, RefString>(#safe.to_owned())
|
||||
}
|
||||
};
|
||||
TokenStream::from(expand)
|
||||
},
|
||||
|
||||
Err(e) => {
|
||||
abort!(lit.span(), "invalid refname literal: {}", e);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a [`git_ref_format_core::Component`] from a string literal.
|
||||
///
|
||||
/// The string is validated at compile time, and an unsafe conversion is
|
||||
/// emitted.
|
||||
#[proc_macro_error]
|
||||
#[proc_macro]
|
||||
pub fn component(input: TokenStream) -> TokenStream {
|
||||
let lit = parse_macro_input!(input as LitStr);
|
||||
let val = lit.value();
|
||||
|
||||
let name: Result<&RefStr, Error> = val.as_str().try_into();
|
||||
match name {
|
||||
Ok(name) => {
|
||||
let comp: Option<Component> = name.into();
|
||||
match comp {
|
||||
Some(safe) => {
|
||||
let safe: &str = safe.as_ref().as_str();
|
||||
let expand = quote! {
|
||||
unsafe {
|
||||
use ::std::{borrow::Cow, mem::transmute};
|
||||
use ::git_ref_format::{Component, RefStr, RefString};
|
||||
|
||||
let inner: RefString = transmute(#safe.to_owned());
|
||||
let cow: Cow<'static, RefStr> = Cow::Owned(inner);
|
||||
transmute::<_, Component>(cow)
|
||||
}
|
||||
};
|
||||
|
||||
TokenStream::from(expand)
|
||||
},
|
||||
|
||||
None => {
|
||||
abort!(lit.span(), "component contains a '/'");
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
Err(e) => {
|
||||
abort!(lit.span(), "invalid refname literal: {}", e);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a [`git_ref_format_core::refspec::PatternString`] from a string
|
||||
/// literal.
|
||||
///
|
||||
/// The string is validated at compile time, and an unsafe conversion is
|
||||
/// emitted.
|
||||
#[proc_macro_error]
|
||||
#[proc_macro]
|
||||
pub fn pattern(input: TokenStream) -> TokenStream {
|
||||
let lit = parse_macro_input!(input as LitStr);
|
||||
let val = lit.value();
|
||||
|
||||
let parsed: Result<&PatternStr, Error> = val.as_str().try_into();
|
||||
match parsed {
|
||||
Ok(safe) => {
|
||||
let safe: &str = safe.as_str();
|
||||
let expand = quote! {
|
||||
unsafe {
|
||||
use ::std::mem::transmute;
|
||||
use ::git_ref_format::refspec::PatternString;
|
||||
|
||||
transmute::<_, PatternString>(#safe.to_owned())
|
||||
}
|
||||
};
|
||||
TokenStream::from(expand)
|
||||
},
|
||||
|
||||
Err(e) => {
|
||||
abort!(lit.span(), "invalid refspec pattern literal: {}", e);
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
// 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.
|
||||
|
||||
//! Everything you never knew you wanted for handling git ref names.
|
||||
//!
|
||||
//! # Overview
|
||||
//!
|
||||
//! This crate provides a number of types which allow to validate git ref names,
|
||||
//! create new ones which are valid by construction, make assertions about their
|
||||
//! structure, and deconstruct them into their components.
|
||||
//!
|
||||
//! ## Basic Types
|
||||
//!
|
||||
//! The basic types are:
|
||||
//!
|
||||
//! * [`RefStr`]
|
||||
//! * [`RefString`]
|
||||
//!
|
||||
//! They are wrappers around [`str`] and [`String`] respectively, with the
|
||||
//! additional guarantee that they are also valid ref names as per
|
||||
//! [`git-check-ref-format`] (which is also exposed directly as
|
||||
//! [`check_ref_format`]). Both types are referred to as "ref strings".
|
||||
//!
|
||||
//! Note that this implies that ref names must be valid UTF-8, which git itself
|
||||
//! doesn't require.
|
||||
//!
|
||||
//! Ref strings can be iterated over, either yielding `&str` or [`Component`]. A
|
||||
//! [`Component`] is guaranteed to not contain a '/' separator, and can thus
|
||||
//! also be used to conveniently construct known-valid ref strings. The [`lit`]
|
||||
//! module contains a number of types (and `const` values thereof) which can be
|
||||
//! coerced into [`Component`], and thus can be used to construct known-valid
|
||||
//! ref strings.
|
||||
//!
|
||||
//! The [`name`] module also provides a number of constant values of commonly
|
||||
//! used ref strings / components, which are useful for pattern matching.
|
||||
//!
|
||||
//! The `"macro"` feature enables the `refstring!` and `component!` macros,
|
||||
//! which can be convenient to construct compile-time validated [`RefString`]s
|
||||
//! respectively [`Component`]s.
|
||||
//!
|
||||
//! ## Refspec Patterns
|
||||
//!
|
||||
//! The types
|
||||
//!
|
||||
//! * [`refspec::PatternStr`]
|
||||
//! * [`refspec::PatternString`]
|
||||
//!
|
||||
//! guarantee that their values are valid ref strings but additionally _may_
|
||||
//! contain at most one "*" character. It is thus possible to convert a ref
|
||||
//! string to a refspec pattern, but not the other way round. Refspec patterns
|
||||
//! are commonly used for mapping remote to local refs (cf. [`git-fetch`]).
|
||||
//!
|
||||
//! The `"macro"` feature enables the `refspec::pattern!` macro, which
|
||||
//! constructs a compile-time validated [`refspec::PatternString`].
|
||||
//!
|
||||
//! ## Structured Ref Strings
|
||||
//!
|
||||
//! Ref strings may be [`Qualified`], which essentially means that they start
|
||||
//! with "refs/". [`Qualified`] ref string also require at least three
|
||||
//! components (eg. "refs/heads/main"), which makes it easier to deal with
|
||||
//! common naming conventions.
|
||||
//!
|
||||
//! [`Qualified`] refs may be [`Namespaced`], or can be given a namespace
|
||||
//! (namespaces can be nested). [`Namespaced`] refs are also [`Qualified`], and
|
||||
//! can have their namespace(s) stripped.
|
||||
//!
|
||||
//! # On Git Ref Name Conventions
|
||||
//!
|
||||
//! Git references are essentially path names pointing to their traditional
|
||||
//! storage location in a the repository (`$GIT_DIR/refs`). Unlike (UNIX) file
|
||||
//! paths, they are subject to a few restrictions, as described in
|
||||
//! [`git-check-ref-format`].
|
||||
//!
|
||||
//! On top of that, there are a number of conventions around the hierarchical
|
||||
//! naming, _some_ of which are treated specially by tools such as the `git`
|
||||
//! CLI. For example:
|
||||
//!
|
||||
//! * `refs/heads/..` are also called "branches".
|
||||
//!
|
||||
//! Omitting the "refs/heads/" prefix is typically accepted. Such a branch
|
||||
//! name is also referred to as a "shorthand" ref.
|
||||
//!
|
||||
//! * `refs/tags/..` are assumed to contain tags.
|
||||
//!
|
||||
//! `git` treats tags specially, specifically it insists that they be globally
|
||||
//! unique across all copies of the repository.
|
||||
//!
|
||||
//! * `refs/remotes/../..` is where "remote tracking branches" are stored.
|
||||
//!
|
||||
//! In `git`, the first element after "remotes" is considered the name of the
|
||||
//! [remote][git-remote] (as it appears in the config file), while everything
|
||||
//! after that is considered a shorthand branch. Note, however, that the
|
||||
//! remote name may itself contain '/' separators, so it is not generally
|
||||
//! possible to extract the branch name without access to the config.
|
||||
//!
|
||||
//! * `refs/namespaces/..` is hidden unless [`gitnamespaces`] are in effect.
|
||||
//!
|
||||
//! The structure of namespaces is recursive: they contain full refs, which
|
||||
//! can themselves be namespaces (eg.
|
||||
//! `refs/namespaces/a/refs/namespaces/b/refs/heads/branch`). Note that,
|
||||
//! unlike remote names, namespace names can **not** contain forward slashes
|
||||
//! but there is no tooling which would enforce that.
|
||||
//!
|
||||
//! There are also other such ref hierachies `git` knows about, and this crate
|
||||
//! doesn't attempt to cover all of them. More importantly, `git` does not
|
||||
//! impose any restrictions on ref hierarchies: as long as they don't collide
|
||||
//! with convential ones, applications can introduce any hierchies they want.
|
||||
//!
|
||||
//! This restricts the transformations between conventional refs which can be
|
||||
//! made without additional information besides the ref name: for example, it is
|
||||
//! not generally possible to turn a remote tracking branch into a branch (or a
|
||||
//! shorthand) without knowning about all possible remote names.
|
||||
//!
|
||||
//! Therefore, this crate doesn't attempt to interpret all possible semantics
|
||||
//! associated with refs, and instead tries to make it easy for library
|
||||
//! consumers to do so.
|
||||
//!
|
||||
//! [`git-check-ref-format`]: https://git-scm.com/docs/git-check-ref-format
|
||||
//! [`git-fetch`]: https://git-scm.com/docs/git-fetch
|
||||
//! [git-remote]: https://git-scm.com/docs/git-remote
|
||||
//! [`gitnamespaces`]: https://git-scm.com/docs/gitnamespaces
|
||||
#[cfg(feature = "percent-encoding")]
|
||||
pub use git_ref_format_core::PercentEncode;
|
||||
pub use git_ref_format_core::{
|
||||
check_ref_format,
|
||||
lit,
|
||||
name::component,
|
||||
Component,
|
||||
DuplicateGlob,
|
||||
Error,
|
||||
Namespaced,
|
||||
Options,
|
||||
Qualified,
|
||||
RefStr,
|
||||
RefString,
|
||||
};
|
||||
|
||||
pub mod name {
|
||||
pub use git_ref_format_core::name::*;
|
||||
|
||||
#[cfg(any(feature = "macro", feature = "git-ref-format-macro"))]
|
||||
pub use git_ref_format_macro::component;
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "macro", feature = "git-ref-format-macro"))]
|
||||
pub use git_ref_format_macro::refname;
|
||||
|
||||
pub mod refspec {
|
||||
pub use git_ref_format_core::refspec::*;
|
||||
|
||||
#[cfg(any(feature = "macro", feature = "git-ref-format-macro"))]
|
||||
pub use git_ref_format_macro::pattern;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "git-ref-format-test"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
test = true
|
||||
doc = false
|
||||
|
||||
[features]
|
||||
test = []
|
||||
|
||||
[dependencies]
|
||||
proptest = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches = "1.5"
|
||||
|
||||
[dev-dependencies.git-ref-format]
|
||||
path = ".."
|
||||
features = ["macro", "minicbor", "serde"]
|
||||
|
||||
[dev-dependencies.test-helpers]
|
||||
path = "../../test/test-helpers"
|
||||
@@ -0,0 +1,101 @@
|
||||
// Copyright © 2022 The Radicle Link Contributors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
use proptest::prelude::*;
|
||||
|
||||
/// Any unicode "word" is trivially a valid refname.
|
||||
pub fn trivial() -> impl Strategy<Value = String> {
|
||||
"\\w+"
|
||||
}
|
||||
|
||||
pub fn valid() -> impl Strategy<Value = String> {
|
||||
prop::collection::vec(trivial(), 1..20).prop_map(|xs| xs.join("/"))
|
||||
}
|
||||
|
||||
pub fn invalid_char() -> impl Strategy<Value = char> {
|
||||
prop_oneof![
|
||||
Just('\0'),
|
||||
Just('\\'),
|
||||
Just('~'),
|
||||
Just('^'),
|
||||
Just(':'),
|
||||
Just('?'),
|
||||
Just('[')
|
||||
]
|
||||
}
|
||||
|
||||
pub fn with_invalid_char() -> impl Strategy<Value = String> {
|
||||
("\\w*", invalid_char(), "\\w*").prop_map(|(mut pre, invalid, suf)| {
|
||||
pre.push(invalid);
|
||||
pre.push_str(&suf);
|
||||
pre
|
||||
})
|
||||
}
|
||||
|
||||
pub fn ends_with_dot_lock() -> impl Strategy<Value = String> {
|
||||
"\\w*\\.lock"
|
||||
}
|
||||
|
||||
pub fn with_double_dot() -> impl Strategy<Value = String> {
|
||||
"\\w*\\.\\.\\w*"
|
||||
}
|
||||
|
||||
pub fn starts_with_dot() -> impl Strategy<Value = String> {
|
||||
"\\.\\w*"
|
||||
}
|
||||
|
||||
pub fn ends_with_dot() -> impl Strategy<Value = String> {
|
||||
"\\w+\\."
|
||||
}
|
||||
|
||||
pub fn with_control_char() -> impl Strategy<Value = String> {
|
||||
"\\w*[\x01-\x1F\x7F]+\\w*"
|
||||
}
|
||||
|
||||
pub fn with_space() -> impl Strategy<Value = String> {
|
||||
"\\w* +\\w*"
|
||||
}
|
||||
|
||||
pub fn with_consecutive_slashes() -> impl Strategy<Value = String> {
|
||||
"\\w*//\\w*"
|
||||
}
|
||||
|
||||
pub fn with_glob() -> impl Strategy<Value = String> {
|
||||
"\\w*\\*\\w*"
|
||||
}
|
||||
|
||||
pub fn multi_glob() -> impl Strategy<Value = String> {
|
||||
(
|
||||
prop::collection::vec(with_glob(), 2..5),
|
||||
prop::collection::vec(trivial(), 0..5),
|
||||
)
|
||||
.prop_map(|(mut globs, mut valids)| {
|
||||
globs.append(&mut valids);
|
||||
globs
|
||||
})
|
||||
.prop_shuffle()
|
||||
.prop_map(|xs| xs.join("/"))
|
||||
}
|
||||
|
||||
pub fn invalid() -> impl Strategy<Value = String> {
|
||||
fn path(s: impl Strategy<Value = String>) -> impl Strategy<Value = String> {
|
||||
prop::collection::vec(s, 1..20).prop_map(|xs| xs.join("/"))
|
||||
}
|
||||
|
||||
prop_oneof![
|
||||
Just(String::from("")),
|
||||
Just(String::from("@")),
|
||||
path(with_invalid_char()),
|
||||
path(ends_with_dot_lock()),
|
||||
path(with_double_dot()),
|
||||
path(starts_with_dot()),
|
||||
path(ends_with_dot()),
|
||||
path(with_control_char()),
|
||||
path(with_space()),
|
||||
path(with_consecutive_slashes()),
|
||||
path(trivial()).prop_map(|mut p| {
|
||||
p.push('/');
|
||||
p
|
||||
}),
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// Copyright © 2022 The Radicle Link Contributors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_use]
|
||||
extern crate assert_matches;
|
||||
|
||||
#[cfg(any(test, feature = "test"))]
|
||||
pub mod gen;
|
||||
#[cfg(test)]
|
||||
mod properties;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright © 2022 The Radicle Link Contributors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
use git_ref_format::{check_ref_format, Error, Options};
|
||||
use proptest::prelude::*;
|
||||
|
||||
use crate::gen;
|
||||
|
||||
mod name;
|
||||
mod pattern;
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn disallow_onelevel(input in gen::trivial(), allow_pattern in any::<bool>()) {
|
||||
assert_matches!(
|
||||
check_ref_format(Options {
|
||||
allow_onelevel: false,
|
||||
allow_pattern,
|
||||
},
|
||||
&input
|
||||
),
|
||||
Err(Error::OneLevel)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright © 2022 The Radicle Link Contributors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use git_ref_format::{Error, RefStr, RefString};
|
||||
use proptest::prelude::*;
|
||||
use test_helpers::roundtrip;
|
||||
|
||||
use crate::gen;
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn valid(input in gen::valid()) {
|
||||
assert_eq!(input.as_str(), RefStr::try_from_str(&input).unwrap().as_str())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_char(input in gen::with_invalid_char()) {
|
||||
assert_matches!(RefString::try_from(input), Err(Error::InvalidChar(_)))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dot_lock(input in gen::ends_with_dot_lock()) {
|
||||
assert_matches!(RefString::try_from(input), Err(Error::DotLock))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_dot(input in gen::with_double_dot()) {
|
||||
assert_matches!(RefString::try_from(input), Err(Error::DotDot))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn starts_dot(input in gen::starts_with_dot()) {
|
||||
assert_matches!(RefString::try_from(input), Err(Error::StartsDot))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ends_dot(input in gen::ends_with_dot()) {
|
||||
assert_matches!(RefString::try_from(input), Err(Error::EndsDot))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn control_char(input in gen::with_control_char()) {
|
||||
assert_matches!(RefString::try_from(input), Err(Error::Control))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn space(input in gen::with_space()) {
|
||||
assert_matches!(RefString::try_from(input), Err(Error::Space))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn consecutive_slashes(input in gen::with_consecutive_slashes()) {
|
||||
assert_matches!(RefString::try_from(input), Err(Error::Slash))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glob(input in gen::with_glob()) {
|
||||
assert_matches!(RefString::try_from(input), Err(Error::InvalidChar('*')))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid(input in gen::invalid()) {
|
||||
assert_matches!(RefString::try_from(input), Err(_))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_components(input in gen::valid()) {
|
||||
assert_eq!(
|
||||
input.as_str(),
|
||||
RefStr::try_from_str(&input).unwrap().components().collect::<RefString>().as_str()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json(input in gen::valid()) {
|
||||
roundtrip::json(RefString::try_from(input).unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cbor(input in gen::valid()) {
|
||||
roundtrip::cbor(RefString::try_from(input).unwrap())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright © 2022 The Radicle Link Contributors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use git_ref_format::{refspec, Error};
|
||||
use proptest::prelude::*;
|
||||
use test_helpers::roundtrip;
|
||||
|
||||
use crate::gen;
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn valid(input in gen::with_glob()) {
|
||||
assert_eq!(input.as_str(), refspec::PatternStr::try_from_str(&input).unwrap().as_str())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refname_is_pattern(input in gen::valid()) {
|
||||
assert_eq!(input.as_str(), refspec::PatternStr::try_from_str(&input).unwrap().as_str())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_more_than_one_star(input in gen::multi_glob()) {
|
||||
assert_matches!(refspec::PatternString::try_from(input), Err(Error::Pattern))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_refname_is_invalid_pattern(input in gen::invalid()) {
|
||||
assert_matches!(refspec::PatternString::try_from(input), Err(_))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip_components(input in gen::with_glob()) {
|
||||
assert_eq!(
|
||||
input.as_str(),
|
||||
refspec::PatternStr::try_from_str(&input)
|
||||
.unwrap()
|
||||
.components()
|
||||
.collect::<Result<refspec::PatternString, _>>()
|
||||
.unwrap()
|
||||
.as_str()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json(input in gen::with_glob()) {
|
||||
roundtrip::json(refspec::PatternString::try_from(input).unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cbor(input in gen::with_glob()) {
|
||||
roundtrip::cbor(refspec::PatternString::try_from(input).unwrap())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
// Copyright © 2022 The Radicle Link Contributors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
use git_ref_format::{component, name, refname, refspec, Error, Qualified, RefStr, RefString};
|
||||
|
||||
#[test]
|
||||
fn refname_macro_works() {
|
||||
assert_eq!("refs/heads/main", refname!("refs/heads/main").as_str())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn component_macro_works() {
|
||||
assert_eq!("self", name::component!("self").as_str())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pattern_macro_works() {
|
||||
assert_eq!("refs/heads/*", refspec::pattern!("refs/heads/*").as_str())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
assert_matches!(RefStr::try_from_str(""), Err(Error::Empty));
|
||||
assert_matches!(RefString::try_from("".to_owned()), Err(Error::Empty));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn join() {
|
||||
let s = name::REFS.join(name::HEADS);
|
||||
let t = s.join(name::MAIN);
|
||||
assert_eq!("refs/heads", s.as_str());
|
||||
assert_eq!("refs/heads/main", t.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn join_and() {
|
||||
assert_eq!(
|
||||
"refs/heads/this/that",
|
||||
name::REFS
|
||||
.join(name::HEADS)
|
||||
.and(refname!("this"))
|
||||
.and(refname!("that"))
|
||||
.as_str()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strip_prefix() {
|
||||
assert_eq!(
|
||||
"main",
|
||||
name::REFS_HEADS_MAIN
|
||||
.strip_prefix(refname!("refs/heads"))
|
||||
.unwrap()
|
||||
.as_str()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strip_prefix_not_prefix() {
|
||||
assert!(name::REFS_HEADS_MAIN
|
||||
.strip_prefix(refname!("refs/tags"))
|
||||
.is_none())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qualified() {
|
||||
assert_eq!(
|
||||
"refs/heads/main",
|
||||
name::REFS_HEADS_MAIN.qualified().unwrap().as_str()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qualified_tag() {
|
||||
assert_eq!(
|
||||
"refs/tags/v1",
|
||||
refname!("refs/tags/v1").qualified().unwrap().as_str()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qualified_remote_tracking() {
|
||||
assert_eq!(
|
||||
"refs/remotes/origin/master",
|
||||
refname!("refs/remotes/origin/master")
|
||||
.qualified()
|
||||
.unwrap()
|
||||
.as_str()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_qualified() {
|
||||
assert!(name::MAIN.qualified().is_none())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qualified_from_components() {
|
||||
assert_eq!(
|
||||
"refs/heads/main",
|
||||
Qualified::from_components(component::HEADS, component::MAIN, None).as_str()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qualified_from_components_with_iter() {
|
||||
assert_eq!(
|
||||
"refs/heads/foo/bar/baz",
|
||||
Qualified::from_components(
|
||||
component::HEADS,
|
||||
name::component!("foo"),
|
||||
[name::component!("bar"), name::component!("baz")]
|
||||
)
|
||||
.as_str()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qualified_from_components_non_empty_iter() {
|
||||
let q = Qualified::from_components(component::HEADS, component::MAIN, None);
|
||||
let (refs, heads, main, mut empty) = q.non_empty_iter();
|
||||
assert!(empty.next().is_none());
|
||||
assert_eq!(("refs", "heads", "main"), (refs, heads, main))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qualified_from_components_non_empty_components() {
|
||||
let q = Qualified::from_components(component::HEADS, component::MAIN, Some(component::MASTER));
|
||||
let (refs, heads, main, mut master) = q.non_empty_components();
|
||||
assert_eq!(
|
||||
(
|
||||
component::REFS,
|
||||
component::HEADS,
|
||||
component::MAIN,
|
||||
component::MASTER
|
||||
),
|
||||
(refs, heads, main, master.next().unwrap())
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn namespaced() {
|
||||
assert_eq!(
|
||||
"refs/namespaces/foo/refs/heads/main",
|
||||
refname!("refs/namespaces/foo/refs/heads/main")
|
||||
.namespaced()
|
||||
.unwrap()
|
||||
.as_str()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_namespaced() {
|
||||
assert!(name::REFS_HEADS_MAIN.namespaced().is_none())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_namespaced_because_not_qualified() {
|
||||
assert!(refname!("refs/namespaces/foo/banana")
|
||||
.namespaced()
|
||||
.is_none())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strip_namespace() {
|
||||
assert_eq!(
|
||||
"refs/rad/id",
|
||||
refname!("refs/namespaces/xyz/refs/rad/id")
|
||||
.namespaced()
|
||||
.unwrap()
|
||||
.strip_namespace()
|
||||
.as_str()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strip_nested_namespaces() {
|
||||
let full = refname!("refs/namespaces/a/refs/namespaces/b/refs/heads/main");
|
||||
let namespaced = full.namespaced().unwrap();
|
||||
let strip_first = namespaced.strip_namespace();
|
||||
let nested = strip_first.namespaced().unwrap();
|
||||
let strip_second = nested.strip_namespace();
|
||||
|
||||
assert_eq!("a", namespaced.namespace().as_str());
|
||||
assert_eq!("b", nested.namespace().as_str());
|
||||
assert_eq!("refs/namespaces/b/refs/heads/main", strip_first.as_str());
|
||||
assert_eq!("refs/heads/main", strip_second.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_namespace() {
|
||||
assert_eq!(
|
||||
"refs/namespaces/foo/refs/heads/main",
|
||||
name::REFS_HEADS_MAIN
|
||||
.qualified()
|
||||
.unwrap()
|
||||
.add_namespace(refname!("foo").head())
|
||||
.as_str()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iter() {
|
||||
assert_eq!(
|
||||
vec!["refs", "heads", "main"],
|
||||
name::REFS_HEADS_MAIN.iter().collect::<Vec<_>>()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_pop() {
|
||||
let mut s = name::REFS.to_owned();
|
||||
s.push(name::HEADS);
|
||||
s.push(name::MAIN);
|
||||
|
||||
assert_eq!("refs/heads/main", s.as_str());
|
||||
assert!(s.pop());
|
||||
assert!(s.pop());
|
||||
assert_eq!("refs", s.as_str());
|
||||
assert!(!s.pop());
|
||||
assert_eq!("refs", s.as_str());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_pattern() {
|
||||
assert_eq!(
|
||||
"refs/heads/*",
|
||||
refname!("refs/heads")
|
||||
.to_pattern(refspec::pattern!("*"))
|
||||
.as_str()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_pattern() {
|
||||
assert_eq!(
|
||||
"refs/heads/*",
|
||||
refname!("refs/heads").with_pattern(refspec::STAR).as_str()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_pattern_and() {
|
||||
assert_eq!(
|
||||
"refs/*/heads",
|
||||
refname!("refs")
|
||||
.with_pattern(refspec::STAR)
|
||||
.and(name::HEADS)
|
||||
.as_str()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect() {
|
||||
assert_eq!(
|
||||
"refs/heads/main",
|
||||
IntoIterator::into_iter([name::REFS, name::HEADS, name::MAIN])
|
||||
.collect::<RefString>()
|
||||
.as_str()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect_components() {
|
||||
let a = name::REFS_HEADS_MAIN.to_owned();
|
||||
let b = a.components().collect();
|
||||
assert_eq!(a, b)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect_pattern_duplicate_glob() {
|
||||
assert_matches!(
|
||||
IntoIterator::into_iter([
|
||||
refspec::Component::Normal(name::REFS),
|
||||
refspec::Component::Glob(None),
|
||||
refspec::Component::Glob(Some(refspec::pattern!("fo*").as_ref()))
|
||||
])
|
||||
.collect::<Result<_, _>>(),
|
||||
Err(refspec::DuplicateGlob)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "git-trailers"
|
||||
version = "0.1.0"
|
||||
authors = ["Nuno Alexandre <hi@nunoalexandre.com>", "Kim Altintop <kim@eagain.st>"]
|
||||
edition = "2018"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
test = false
|
||||
|
||||
[dependencies]
|
||||
nom = "7.1"
|
||||
thiserror = "1.0"
|
||||
@@ -0,0 +1,290 @@
|
||||
// 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::{borrow::Cow, convert::TryFrom, fmt, ops::Deref};
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
#[error("the trailers paragraph is missing in the given message")]
|
||||
MissingParagraph,
|
||||
|
||||
#[error("trailing data after trailers section: '{0}")]
|
||||
Trailing(String),
|
||||
|
||||
#[error(transparent)]
|
||||
Parse(#[from] nom::Err<nom::error::Error<String>>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct Trailer<'a> {
|
||||
pub token: Token<'a>,
|
||||
pub values: Vec<Cow<'a, str>>,
|
||||
}
|
||||
|
||||
impl<'a> Trailer<'a> {
|
||||
pub fn display(&'a self, separator: &'a str) -> Display<'a> {
|
||||
Display {
|
||||
trailer: self,
|
||||
separator,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_owned(&self) -> OwnedTrailer {
|
||||
OwnedTrailer::from(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct Token<'a>(&'a str);
|
||||
|
||||
/// A version of the Trailer<'a> which owns it's token and values. Useful for
|
||||
/// when you need to carry trailers around in a long lived data structure.
|
||||
pub struct OwnedTrailer {
|
||||
token: OwnedToken,
|
||||
values: Vec<String>,
|
||||
}
|
||||
|
||||
pub struct OwnedToken(String);
|
||||
|
||||
impl<'a> From<&Trailer<'a>> for OwnedTrailer {
|
||||
fn from(t: &Trailer<'a>) -> Self {
|
||||
OwnedTrailer {
|
||||
token: OwnedToken(t.token.0.to_string()),
|
||||
values: t.values.iter().map(|v| v.to_string()).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<Trailer<'a>> for OwnedTrailer {
|
||||
fn from(t: Trailer<'a>) -> Self {
|
||||
(&t).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a OwnedTrailer> for Trailer<'a> {
|
||||
fn from(t: &'a OwnedTrailer) -> Self {
|
||||
Trailer {
|
||||
token: Token(t.token.0.as_str()),
|
||||
values: t.values.iter().map(Cow::from).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum InvalidToken {
|
||||
#[error("trailing characters: '{0}'")]
|
||||
Trailing(String),
|
||||
|
||||
#[error(transparent)]
|
||||
Parse(#[from] nom::Err<nom::error::Error<String>>),
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a str> for Token<'a> {
|
||||
type Error = InvalidToken;
|
||||
|
||||
fn try_from(s: &'a str) -> Result<Self, Self::Error> {
|
||||
match parser::token(s) {
|
||||
Ok((rest, token)) if rest.is_empty() => Ok(token),
|
||||
Ok((trailing, _)) => Err(InvalidToken::Trailing(trailing.to_owned())),
|
||||
Err(e) => Err(e.to_owned().into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Token<'_> {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Display<'a> {
|
||||
trailer: &'a Trailer<'a>,
|
||||
separator: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for Display<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}{}{}",
|
||||
self.trailer.token.deref(),
|
||||
self.separator,
|
||||
self.trailer.values.join("\n ")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Separator<'a> {
|
||||
fn sep_for(&self, token: &Token) -> &'a str;
|
||||
}
|
||||
|
||||
impl<'a> Separator<'a> for &'a str {
|
||||
fn sep_for(&self, _: &Token) -> &'a str {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, F> Separator<'a> for F
|
||||
where
|
||||
F: Fn(&Token) -> &'a str,
|
||||
{
|
||||
fn sep_for(&self, token: &Token) -> &'a str {
|
||||
self(token)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DisplayMany<'a, S> {
|
||||
separator: S,
|
||||
trailers: &'a [Trailer<'a>],
|
||||
}
|
||||
|
||||
impl<'a, S> fmt::Display for DisplayMany<'a, S>
|
||||
where
|
||||
S: Separator<'a>,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
for (i, trailer) in self.trailers.iter().enumerate() {
|
||||
if i > 0 {
|
||||
writeln!(f)?
|
||||
}
|
||||
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
trailer.display(self.separator.sep_for(&trailer.token))
|
||||
)?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the trailers of the given message. It looks up the last paragraph
|
||||
/// of the message and attempts to parse each of its lines as a [Trailer].
|
||||
/// Fails if no trailers paragraph is found or if at least one trailer
|
||||
/// fails to be parsed.
|
||||
pub fn parse<'a>(message: &'a str, separators: &'a str) -> Result<Vec<Trailer<'a>>, Error> {
|
||||
let trailers_paragraph =
|
||||
match parser::paragraphs(message.trim_end()).map(|(_, ps)| ps.last().cloned()) {
|
||||
Ok(None) | Err(_) => return Err(Error::MissingParagraph),
|
||||
Ok(Some(p)) => {
|
||||
if p.is_empty() {
|
||||
return Err(Error::MissingParagraph);
|
||||
}
|
||||
p
|
||||
},
|
||||
};
|
||||
|
||||
match parser::trailers(trailers_paragraph, separators) {
|
||||
Ok((rest, trailers)) if rest.is_empty() => Ok(trailers),
|
||||
Ok((unparseable, _)) => Err(Error::Trailing(unparseable.to_owned())),
|
||||
Err(e) => Err(e.to_owned().into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Render a slice of trailers.
|
||||
///
|
||||
/// The `separator` can be either a string slice, or a closure which may choose
|
||||
/// a different separator for each [`Token`] encountered. Note that multiline
|
||||
/// trailers are rendered with a fixed indent, so the result is not
|
||||
/// layout-preserving.
|
||||
pub fn display<'a, S>(separator: S, trailers: &'a [Trailer<'a>]) -> DisplayMany<'a, S>
|
||||
where
|
||||
S: Separator<'a>,
|
||||
{
|
||||
DisplayMany {
|
||||
separator,
|
||||
trailers,
|
||||
}
|
||||
}
|
||||
|
||||
pub mod parser {
|
||||
use std::borrow::Cow;
|
||||
|
||||
use super::{Token, Trailer};
|
||||
use nom::{
|
||||
branch::alt,
|
||||
bytes::complete::{tag, take_until, take_while1},
|
||||
character::complete::{line_ending, not_line_ending, one_of, space0, space1},
|
||||
combinator::{map, rest},
|
||||
multi::{many0, separated_list1},
|
||||
sequence::{delimited, preceded, separated_pair, terminated},
|
||||
IResult,
|
||||
};
|
||||
|
||||
const EMPTY_LINE: &str = "\n\n";
|
||||
|
||||
pub fn paragraphs(s: &str) -> IResult<&str, Vec<&str>> {
|
||||
separated_list1(tag(EMPTY_LINE), paragraph)(s)
|
||||
}
|
||||
|
||||
pub fn paragraph(s: &str) -> IResult<&str, &str> {
|
||||
alt((take_until(EMPTY_LINE), rest))(s)
|
||||
}
|
||||
|
||||
/// Parse all the possible trailers.
|
||||
/// It stops when it can no longer parse valid trailers.
|
||||
pub fn trailers<'a>(s: &'a str, separators: &'a str) -> IResult<&'a str, Vec<Trailer<'a>>> {
|
||||
many0(|s| trailer(s, separators))(s)
|
||||
}
|
||||
|
||||
/// Parse a trailer, which can have an inlined or multilined value.
|
||||
pub fn trailer<'a>(s: &'a str, separators: &'a str) -> IResult<&'a str, Trailer<'a>> {
|
||||
let mut parser = separated_pair(token, |s| separator(separators, s), values);
|
||||
let (rest, (token, values)) = parser(s)?;
|
||||
Ok((rest, Trailer { token, values }))
|
||||
}
|
||||
|
||||
/// Parse a trailer token.
|
||||
pub(super) fn token(s: &str) -> IResult<&str, Token> {
|
||||
take_while1(|c: char| c.is_alphanumeric() || c == '-')(s)
|
||||
.map(|(i, token_str)| (i, Token(token_str)))
|
||||
}
|
||||
|
||||
/// Parse the trailer separator, which can be delimited by spaces.
|
||||
fn separator<'a>(separators: &'a str, s: &'a str) -> IResult<&'a str, char> {
|
||||
delimited(space0, one_of(separators), space0)(s)
|
||||
}
|
||||
|
||||
/// Parse the trailer values, which gathers the value after the separator
|
||||
/// (if any) and possible following multilined values, indented by a
|
||||
/// space.
|
||||
fn values(s: &str) -> IResult<&str, Vec<Cow<'_, str>>> {
|
||||
let (r, opt_inline_value) = until_eol_or_eof(s)?;
|
||||
let (r, mut values) = multiline_values(r)?;
|
||||
if !opt_inline_value.is_empty() {
|
||||
values.insert(0, opt_inline_value.into())
|
||||
}
|
||||
Ok((r, values))
|
||||
}
|
||||
|
||||
fn multiline_values(s: &str) -> IResult<&str, Vec<Cow<'_, str>>> {
|
||||
many0(map(indented_line_contents, Cow::from))(s)
|
||||
}
|
||||
|
||||
fn until_eol_or_eof(s: &str) -> IResult<&str, &str> {
|
||||
alt((until_eol, rest))(s)
|
||||
}
|
||||
|
||||
/// Parse an indented line, i.e, a line that starts with a space.
|
||||
/// Extracts the line contents, ignoring the indentation and the
|
||||
/// new line character.
|
||||
fn indented_line_contents(s: &str) -> IResult<&str, &str> {
|
||||
preceded(space1, until_eol_or_eof)(s)
|
||||
}
|
||||
|
||||
/// Consume the input until the end of the line, ignoring the new line
|
||||
/// character.
|
||||
fn until_eol(s: &str) -> IResult<&str, &str> {
|
||||
terminated(not_line_ending, line_ending)(s)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "git-trailers-test"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
test = true
|
||||
doc = false
|
||||
|
||||
[features]
|
||||
test = []
|
||||
|
||||
[dev-dependencies]
|
||||
assert_matches = "1.5"
|
||||
pretty_assertions = "1.1"
|
||||
|
||||
[dev-dependencies.git-trailers]
|
||||
path = ".."
|
||||
@@ -0,0 +1,2 @@
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,155 @@
|
||||
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
|
||||
//
|
||||
// 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::{borrow::Cow, convert::TryFrom as _, ops::Deref as _};
|
||||
|
||||
use assert_matches::assert_matches;
|
||||
use git_trailers::{display, parse, Error, Token, Trailer};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn parse_message_with_valid_trailers() {
|
||||
let msg = r#"Subject
|
||||
|
||||
A multiline
|
||||
description.
|
||||
|
||||
Co-authored-by: John Doe <john.doe@test.com>
|
||||
Ticket: #42
|
||||
Tested-by:
|
||||
John <john@test.com>
|
||||
Jane <jane@test.com>
|
||||
Just-a-token:
|
||||
|
||||
"#;
|
||||
assert_eq!(
|
||||
parse(msg, ":").unwrap(),
|
||||
vec![
|
||||
new_trailer("Co-authored-by", &["John Doe <john.doe@test.com>"]),
|
||||
new_trailer("Ticket", &["#42"]),
|
||||
new_trailer(
|
||||
"Tested-by",
|
||||
&["John <john@test.com>", "Jane <jane@test.com>"]
|
||||
),
|
||||
new_trailer("Just-a-token", &[]),
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_message_trailers_with_custom_separators() {
|
||||
let separators = ":=$";
|
||||
let msg = r#"Subject
|
||||
|
||||
A multiline
|
||||
description.
|
||||
|
||||
Co-authored-by: John Doe <john.doe@test.com>
|
||||
Ticket = #42
|
||||
Tested-by $User <user@test.com>
|
||||
John <john@test.com>
|
||||
Jane <jane@test.com>
|
||||
"#;
|
||||
assert_eq!(
|
||||
parse(msg, separators).unwrap(),
|
||||
vec![
|
||||
new_trailer("Co-authored-by", &["John Doe <john.doe@test.com>"]),
|
||||
new_trailer("Ticket", &["#42"]),
|
||||
new_trailer(
|
||||
"Tested-by",
|
||||
&[
|
||||
"User <user@test.com>",
|
||||
"John <john@test.com>",
|
||||
"Jane <jane@test.com>"
|
||||
]
|
||||
),
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_message_trailers_with_missing_token() {
|
||||
let msg = r#"Subject
|
||||
|
||||
Good-trailer: true
|
||||
John Doe <john.doe@test.com> # Unparsable token due to missing token"#;
|
||||
assert_matches!(
|
||||
parse(msg, ":"),
|
||||
Err(Error::Trailing(s))
|
||||
if s == "John Doe <john.doe@test.com> # Unparsable token due to missing token"
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_message_trailers_with_invalid_token() {
|
||||
let msg = r#"Subject
|
||||
|
||||
Good-trailer: true
|
||||
&!#: John Doe <john.doe@test.com> # Unparsable token due to invalid token"#;
|
||||
assert_matches!(
|
||||
parse(msg, ":"),
|
||||
Err(Error::Trailing(s))
|
||||
if s == "&!#: John Doe <john.doe@test.com> # Unparsable token due to invalid token"
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_message_with_only_trailers() {
|
||||
let msg = r#"Co-authored-by: John Doe <john.doe@test.com>
|
||||
Ticket: #42
|
||||
Tested-by: Tester <tester@test.com>
|
||||
"#;
|
||||
assert_eq!(
|
||||
parse(msg, ":").unwrap(),
|
||||
vec![
|
||||
new_trailer("Co-authored-by", &["John Doe <john.doe@test.com>"]),
|
||||
new_trailer("Ticket", &["#42"]),
|
||||
new_trailer("Tested-by", &["Tester <tester@test.com>"]),
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_empty_message() {
|
||||
let msg = "";
|
||||
assert_matches!(parse(msg, ":"), Err(Error::MissingParagraph))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_static() {
|
||||
let msg = r#"Tested-by: Alice
|
||||
Bob
|
||||
Carol
|
||||
Dylan
|
||||
Acked-by: Eve"#;
|
||||
|
||||
let parsed = parse(msg, ":").unwrap();
|
||||
let rendered = format!("{}", display(": ", &parsed));
|
||||
assert_eq!(&rendered, msg);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_dynamic() {
|
||||
let msg = r#"Co-authored-by: John Doe <john.doe@test.com>
|
||||
Tested-by: Tester <tester@test.com>
|
||||
Fixes #42"#;
|
||||
|
||||
let parsed = parse(msg, ":#").unwrap();
|
||||
let rendered = format!(
|
||||
"{}",
|
||||
display(
|
||||
|t: &Token| if t.deref() == "Fixes" { " #" } else { ": " },
|
||||
&parsed
|
||||
)
|
||||
);
|
||||
assert_eq!(rendered, msg)
|
||||
}
|
||||
|
||||
fn new_trailer<'a>(token: &'a str, values: &[&'a str]) -> Trailer<'a> {
|
||||
Trailer {
|
||||
token: Token::try_from(token).unwrap(),
|
||||
values: values.iter().map(|s| Cow::from(*s)).collect(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
[package]
|
||||
name = "link-git"
|
||||
version = "0.1.0"
|
||||
authors = ["Kim Altintop <kim@eagain.st>"]
|
||||
edition = "2018"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
description = "Core git types and functionality"
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
test = false
|
||||
|
||||
[dependencies]
|
||||
arc-swap = "1.4.0"
|
||||
async-process = "1.1.0"
|
||||
async-trait = "0.1"
|
||||
blocking = "1.0.2"
|
||||
bstr = "0.2"
|
||||
futures-lite = "1.12.0"
|
||||
futures-util = "0.3.15"
|
||||
lazy_static = "1.4.0"
|
||||
im = "15.0.0"
|
||||
once_cell = "1.10"
|
||||
parking_lot = "0.12"
|
||||
pin-project = "1.0.7"
|
||||
regex = "1.5.4"
|
||||
rustc-hash = "1.1.0"
|
||||
tempfile = "3.3"
|
||||
thiserror = "1.0.30"
|
||||
tracing = "0.1"
|
||||
versions = "3.0.2"
|
||||
|
||||
# gitoxide
|
||||
git-actor = "^0.6.0"
|
||||
git-hash = "^0.8.0"
|
||||
git-lock = "^1.0.1"
|
||||
git-object = "^0.15.1"
|
||||
git-odb = "^0.24.0"
|
||||
git-ref = "^0.9.0"
|
||||
git-traverse = "^0.10.0"
|
||||
|
||||
[dependencies.git-features]
|
||||
version = "^0.17.0"
|
||||
features = ["progress", "parallel", "zlib-ng-compat"]
|
||||
|
||||
[dependencies.git-pack]
|
||||
version = "^0.14.0"
|
||||
features = ["object-cache-dynamic", "pack-cache-lru-static", "pack-cache-lru-dynamic"]
|
||||
|
||||
[dependencies.git-packetline]
|
||||
version = "^0.12.0"
|
||||
features = ["async-io"]
|
||||
|
||||
[dependencies.git-protocol]
|
||||
version = "^0.12.0"
|
||||
features = ["async-client"]
|
||||
|
||||
# compat
|
||||
[dependencies.git2]
|
||||
version = "0.13.24"
|
||||
default-features = false
|
||||
features = ["vendored-libgit2"]
|
||||
optional = true
|
||||
@@ -0,0 +1,20 @@
|
||||
// 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.
|
||||
|
||||
#[macro_use]
|
||||
extern crate async_trait;
|
||||
|
||||
pub mod odb;
|
||||
pub mod protocol;
|
||||
pub mod refs;
|
||||
pub use refs::db as refdb;
|
||||
#[cfg(feature = "git2")]
|
||||
pub mod service;
|
||||
|
||||
pub use git_actor as actor;
|
||||
pub use git_hash as hash;
|
||||
pub use git_lock as lock;
|
||||
pub use git_object as object;
|
||||
pub use git_traverse as traverse;
|
||||
@@ -0,0 +1,51 @@
|
||||
// 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 git_hash::oid;
|
||||
use thiserror::Error;
|
||||
|
||||
pub mod backend;
|
||||
pub mod index;
|
||||
pub mod pack;
|
||||
pub mod window;
|
||||
|
||||
pub use git_pack::{cache, data::Object};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Error {
|
||||
#[error(transparent)]
|
||||
Packed(#[from] index::error::Lookup<pack::error::Data>),
|
||||
|
||||
#[error(transparent)]
|
||||
Loose(#[from] git_odb::loose::find::Error),
|
||||
}
|
||||
|
||||
pub struct Odb<I, D> {
|
||||
pub loose: backend::Loose,
|
||||
pub packed: backend::Packed<I, D>,
|
||||
}
|
||||
|
||||
impl<I, D> Odb<I, D>
|
||||
where
|
||||
I: index::Index,
|
||||
D: window::Cache,
|
||||
{
|
||||
pub fn contains(&self, id: impl AsRef<oid>) -> bool {
|
||||
self.packed.contains(id.as_ref()) || self.loose.contains(id)
|
||||
}
|
||||
|
||||
pub fn find<'a>(
|
||||
&self,
|
||||
id: impl AsRef<oid>,
|
||||
buf: &'a mut Vec<u8>,
|
||||
cache: &mut impl cache::DecodeEntry,
|
||||
) -> Result<Option<Object<'a>>, Error> {
|
||||
let id = id.as_ref();
|
||||
if self.packed.contains(id) {
|
||||
return self.packed.find(id, buf, cache).map_err(Into::into);
|
||||
}
|
||||
self.loose.try_find(id, buf).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// 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 git_hash::oid;
|
||||
use git_pack::{cache::DecodeEntry, data::Object};
|
||||
|
||||
use super::{index, pack, window};
|
||||
|
||||
pub type Loose = git_odb::loose::Store;
|
||||
|
||||
pub struct Packed<I, D> {
|
||||
pub index: I,
|
||||
pub data: D,
|
||||
}
|
||||
|
||||
impl<I, D> Packed<I, D>
|
||||
where
|
||||
I: index::Index,
|
||||
D: window::Cache,
|
||||
{
|
||||
pub fn contains(&self, id: impl AsRef<oid>) -> bool {
|
||||
self.index.contains(id)
|
||||
}
|
||||
|
||||
pub fn find<'a>(
|
||||
&self,
|
||||
id: impl AsRef<oid>,
|
||||
buf: &'a mut Vec<u8>,
|
||||
cache: &mut impl DecodeEntry,
|
||||
) -> Result<Option<Object<'a>>, index::error::Lookup<pack::error::Data>> {
|
||||
self.index
|
||||
.lookup(|info| self.data.get(info), id, buf, cache)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
// 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::{
|
||||
fs,
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use git_hash::oid;
|
||||
use git_pack::{
|
||||
cache::DecodeEntry,
|
||||
data::{Object, ResolvedBase},
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use tracing::trace;
|
||||
|
||||
use super::pack;
|
||||
|
||||
pub use git_pack::index::File as IndexFile;
|
||||
|
||||
mod metrics;
|
||||
pub use metrics::{Metrics, Stats, StatsView};
|
||||
|
||||
pub mod error {
|
||||
use super::*;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Discover {
|
||||
#[error(transparent)]
|
||||
Index(#[from] pack::error::Index),
|
||||
|
||||
#[error(transparent)]
|
||||
Io(#[from] io::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Lookup<E> {
|
||||
#[error(transparent)]
|
||||
Lookup(E),
|
||||
|
||||
#[error(transparent)]
|
||||
Reload(#[from] Discover),
|
||||
|
||||
#[error(transparent)]
|
||||
Decode(#[from] git_pack::data::decode_entry::Error),
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Index {
|
||||
fn contains(&self, id: impl AsRef<oid>) -> bool;
|
||||
|
||||
fn lookup<'a, F, E>(
|
||||
&self,
|
||||
pack_cache: F,
|
||||
id: impl AsRef<oid>,
|
||||
buf: &'a mut Vec<u8>,
|
||||
cache: &mut impl DecodeEntry,
|
||||
) -> Result<Option<Object<'a>>, error::Lookup<E>>
|
||||
where
|
||||
F: FnOnce(&pack::Info) -> Result<Arc<pack::Data>, E>;
|
||||
}
|
||||
|
||||
/// An [`Index`] which can be shared between threads.
|
||||
///
|
||||
/// [`Shared`] assumes that:
|
||||
///
|
||||
/// * newer packs are likely to contain recent objects
|
||||
/// * lookups tend to favour recent objects
|
||||
/// * lookups tend to expect the object to be found (the object id is either
|
||||
/// pointed to by a ref, or linked to by an existing object)
|
||||
///
|
||||
/// Thus, it:
|
||||
///
|
||||
/// * orders indices found in `GIT_DIR/objects/pack` by modification time, and
|
||||
/// queries the more recent ones first
|
||||
/// * attempts to rescan `GIT_DIR/objects/pack` when an object id was _not_
|
||||
/// found (assuming that this is due to a compaction)
|
||||
///
|
||||
/// Unless a reload occurs, lookups are lock-free and mostly wait-free. Writes
|
||||
/// ([`Shared::push`], [`Shared::reload`]) are guarded by a [`Mutex`].
|
||||
// TODO: consecutive lookups also tend to resolve to the same pack, so we could
|
||||
// remember the index into the `im::Vector` where we found a match and look
|
||||
// there first. This is what libgit2 does, but the heuristic is not necessarily
|
||||
// true when `Shared` is shared across multiple concurrent link replication
|
||||
// tasks; per-namespace packs are independent pre-compaction.
|
||||
pub struct Shared<M> {
|
||||
pack_dir: PathBuf,
|
||||
indices: ArcSwap<im::Vector<Arc<pack::Index>>>,
|
||||
write: Mutex<()>,
|
||||
stats: M,
|
||||
}
|
||||
|
||||
impl Shared<()> {
|
||||
pub fn open(git_dir: impl AsRef<Path>) -> Result<Self, error::Discover> {
|
||||
let pack_dir = git_dir.as_ref().join("objects").join("pack");
|
||||
let indices = discover(&pack_dir)?;
|
||||
|
||||
Ok(Self {
|
||||
pack_dir,
|
||||
indices: ArcSwap::new(Arc::new(indices)),
|
||||
write: Mutex::new(()),
|
||||
stats: (),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<M> Shared<M>
|
||||
where
|
||||
M: Metrics,
|
||||
{
|
||||
pub fn with_stats(self) -> Shared<Stats> {
|
||||
self.with_metrics(Stats::default())
|
||||
}
|
||||
|
||||
pub fn with_metrics<N: Metrics>(self, m: N) -> Shared<N> {
|
||||
Shared {
|
||||
pack_dir: self.pack_dir,
|
||||
indices: self.indices,
|
||||
write: self.write,
|
||||
stats: m,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stats(&self) -> M::Snapshot {
|
||||
self.stats.snapshot(self.len())
|
||||
}
|
||||
|
||||
/// Add a newly discovered [`pack::Index`].
|
||||
///
|
||||
/// This index will be considered first by subsequent lookups. Note that it
|
||||
/// is only guaranteed that the index will be visible to readers if it
|
||||
/// resides in the `git_dir` this [`Shared`] was initialised with.
|
||||
pub fn push(&self, idx: pack::Index) {
|
||||
let lock = self.write.lock();
|
||||
let mut new = self.indices.load_full();
|
||||
Arc::make_mut(&mut new).push_front(Arc::new(idx));
|
||||
self.indices.store(new);
|
||||
drop(lock);
|
||||
|
||||
self.stats.record_push()
|
||||
}
|
||||
|
||||
/// Re-scan the packs directory and replace the in-memory indices with the
|
||||
/// result.
|
||||
///
|
||||
/// If the application can intercept compaction events, this method can be
|
||||
/// used to release memory early. Otherwise it is not required to call this
|
||||
/// method, as [`Shared`] manages reloads automatically.
|
||||
pub fn reload(&self) -> Result<(), error::Discover> {
|
||||
let lock = self.write.lock();
|
||||
let indices = discover(&self.pack_dir)?;
|
||||
self.indices.store(Arc::new(indices));
|
||||
drop(lock);
|
||||
|
||||
self.stats.record_reload();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.indices.load().is_empty()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.indices.load().len()
|
||||
}
|
||||
|
||||
fn contains(&self, id: impl AsRef<oid>) -> bool {
|
||||
for i in 0..2 {
|
||||
for idx in self.indices.load().iter() {
|
||||
if idx.contains(&id) {
|
||||
self.stats.record_hit();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if i == 0 && self.reload().is_err() {
|
||||
self.stats.record_miss();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
self.stats.record_miss();
|
||||
false
|
||||
}
|
||||
|
||||
fn lookup<'a, F, E>(
|
||||
&self,
|
||||
pack_cache: F,
|
||||
id: impl AsRef<oid>,
|
||||
buf: &'a mut Vec<u8>,
|
||||
cache: &mut impl DecodeEntry,
|
||||
) -> Result<Option<Object<'a>>, error::Lookup<E>>
|
||||
where
|
||||
F: FnOnce(&pack::Info) -> Result<Arc<pack::Data>, E>,
|
||||
{
|
||||
for i in 0..2 {
|
||||
for idx in self.indices.load().iter() {
|
||||
if let Some(ofs) = idx.ofs(&id) {
|
||||
self.stats.record_hit();
|
||||
return load_obj(ofs, idx, pack_cache, buf, cache).map(Some);
|
||||
}
|
||||
}
|
||||
|
||||
if i == 0 {
|
||||
self.reload()?;
|
||||
}
|
||||
}
|
||||
|
||||
self.stats.record_miss();
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn load_obj<'a, F, E>(
|
||||
ofs: u64,
|
||||
idx: &pack::Index,
|
||||
pack_cache: F,
|
||||
buf: &'a mut Vec<u8>,
|
||||
cache: &mut impl DecodeEntry,
|
||||
) -> Result<Object<'a>, error::Lookup<E>>
|
||||
where
|
||||
F: FnOnce(&pack::Info) -> Result<Arc<pack::Data>, E>,
|
||||
{
|
||||
let data = pack_cache(&idx.info).map_err(error::Lookup::Lookup)?;
|
||||
let pack = data.file();
|
||||
let entry = pack.entry(ofs);
|
||||
let obj = pack
|
||||
.decode_entry(
|
||||
entry,
|
||||
buf,
|
||||
|id, _| idx.ofs(id).map(|ofs| ResolvedBase::InPack(pack.entry(ofs))),
|
||||
cache,
|
||||
)
|
||||
.map(move |out| Object {
|
||||
kind: out.kind,
|
||||
data: buf.as_slice(),
|
||||
pack_location: None,
|
||||
})?;
|
||||
|
||||
Ok(obj)
|
||||
}
|
||||
|
||||
fn discover(pack_dir: impl AsRef<Path>) -> Result<im::Vector<Arc<pack::Index>>, error::Discover> {
|
||||
let pack_dir = pack_dir.as_ref();
|
||||
let pack_dir_disp = pack_dir.display();
|
||||
trace!("discovering packs at {}", pack_dir_disp);
|
||||
match fs::read_dir(&pack_dir) {
|
||||
Ok(iter) => {
|
||||
let mut paths = Vec::new();
|
||||
for entry in iter {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
trace!("{}", path.display());
|
||||
let meta = entry.metadata()?;
|
||||
if meta.file_type().is_file() && path.extension().unwrap_or_default() == "idx" {
|
||||
let mtime = meta.modified()?;
|
||||
paths.push((path, mtime));
|
||||
}
|
||||
}
|
||||
paths.sort_by(|(_, mtime_a), (_, mtime_b)| mtime_a.cmp(mtime_b));
|
||||
|
||||
let indices = paths
|
||||
.into_iter()
|
||||
.rev()
|
||||
.map(|(path, _)| Ok(pack::Index::open(path).map(Arc::new)?))
|
||||
.collect::<Result<_, error::Discover>>()?;
|
||||
|
||||
Ok(indices)
|
||||
},
|
||||
// It's not an error if the directory doesn't exist, the repository
|
||||
// could contain only loose objects
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => {
|
||||
trace!("not a directory: {}", pack_dir_disp);
|
||||
Ok(im::Vector::new())
|
||||
},
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
impl<M> Index for Shared<M>
|
||||
where
|
||||
M: Metrics,
|
||||
{
|
||||
fn contains(&self, id: impl AsRef<oid>) -> bool {
|
||||
self.contains(id)
|
||||
}
|
||||
|
||||
fn lookup<'a, F, E>(
|
||||
&self,
|
||||
pack_cache: F,
|
||||
id: impl AsRef<oid>,
|
||||
buf: &'a mut Vec<u8>,
|
||||
cache: &mut impl DecodeEntry,
|
||||
) -> Result<Option<Object<'a>>, error::Lookup<E>>
|
||||
where
|
||||
F: FnOnce(&pack::Info) -> Result<Arc<pack::Data>, E>,
|
||||
{
|
||||
self.lookup(pack_cache, id, buf, cache)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// 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::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
pub struct StatsView {
|
||||
/// Total number of times a lookup was successful.
|
||||
pub hits: usize,
|
||||
/// Total number of times a lookup was unsuccessful.
|
||||
pub misses: usize,
|
||||
/// Total number of times an index was added explicitly via
|
||||
/// [`super::Shared::push`].
|
||||
pub pushes: usize,
|
||||
/// Total number of reloads via [`super::Shared::reload`].
|
||||
pub reloads: usize,
|
||||
/// Number of [`crate::odb::pack::Index`]es currently held.
|
||||
pub indices: usize,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Stats {
|
||||
hits: AtomicUsize,
|
||||
misses: AtomicUsize,
|
||||
pushes: AtomicUsize,
|
||||
reloads: AtomicUsize,
|
||||
}
|
||||
|
||||
pub trait Metrics {
|
||||
type Snapshot;
|
||||
|
||||
fn record_hit(&self);
|
||||
fn record_miss(&self);
|
||||
fn record_push(&self);
|
||||
fn record_reload(&self);
|
||||
|
||||
fn snapshot(&self, indices: usize) -> Self::Snapshot;
|
||||
}
|
||||
|
||||
impl Metrics for Stats {
|
||||
type Snapshot = StatsView;
|
||||
|
||||
fn record_hit(&self) {
|
||||
self.hits.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn record_miss(&self) {
|
||||
self.misses.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn record_push(&self) {
|
||||
self.pushes.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn record_reload(&self) {
|
||||
self.reloads.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn snapshot(&self, indices: usize) -> Self::Snapshot {
|
||||
StatsView {
|
||||
hits: self.hits.load(Ordering::Relaxed),
|
||||
misses: self.misses.load(Ordering::Relaxed),
|
||||
pushes: self.pushes.load(Ordering::Relaxed),
|
||||
reloads: self.reloads.load(Ordering::Relaxed),
|
||||
indices,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Metrics for () {
|
||||
type Snapshot = ();
|
||||
|
||||
fn record_hit(&self) {}
|
||||
fn record_miss(&self) {}
|
||||
fn record_push(&self) {}
|
||||
fn record_reload(&self) {}
|
||||
|
||||
fn snapshot(&self, _: usize) -> Self::Snapshot {}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
// 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::{
|
||||
path::{Path, PathBuf},
|
||||
sync::atomic::{AtomicUsize, Ordering},
|
||||
};
|
||||
|
||||
use git_hash::{oid, ObjectId};
|
||||
use git_pack::{data, index};
|
||||
use rustc_hash::FxHasher;
|
||||
use tracing::warn;
|
||||
|
||||
pub mod error {
|
||||
use super::*;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("failed to load pack data from {path:?}")]
|
||||
pub struct Data {
|
||||
pub path: PathBuf,
|
||||
pub source: data::header::decode::Error,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("failed to load pack index from {path:?}")]
|
||||
pub struct Index {
|
||||
pub path: PathBuf,
|
||||
pub source: index::init::Error,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Data {
|
||||
pub hash: u64,
|
||||
hits: AtomicUsize,
|
||||
file: data::File,
|
||||
}
|
||||
|
||||
impl Data {
|
||||
pub fn hit(&self) {
|
||||
self.hits.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn hits(&self) -> usize {
|
||||
self.hits.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn file(&self) -> &data::File {
|
||||
&self.file
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<data::File> for Data {
|
||||
fn as_ref(&self) -> &data::File {
|
||||
self.file()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct Info {
|
||||
pub(super) hash: u64,
|
||||
pub data_path: PathBuf,
|
||||
}
|
||||
|
||||
impl Info {
|
||||
pub fn data(&self) -> Result<Data, error::Data> {
|
||||
let file = data::File::at(&self.data_path).map_err(|source| error::Data {
|
||||
path: self.data_path.clone(),
|
||||
source,
|
||||
})?;
|
||||
Ok(Data {
|
||||
hash: self.hash,
|
||||
hits: AtomicUsize::new(0),
|
||||
file,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Index {
|
||||
pub info: Info,
|
||||
file: index::File,
|
||||
}
|
||||
|
||||
impl Index {
|
||||
pub fn open(path: impl AsRef<Path>) -> Result<Self, error::Index> {
|
||||
let path = path.as_ref();
|
||||
let file = index::File::at(path).map_err(|source| error::Index {
|
||||
path: path.to_path_buf(),
|
||||
source,
|
||||
})?;
|
||||
let data_path = path.with_extension("pack");
|
||||
let hash = {
|
||||
let file_name = path
|
||||
.file_name()
|
||||
.expect("must have a file name, we opened it")
|
||||
.to_string_lossy();
|
||||
// XXX: inexplicably, gitoxide omits the "pack-" prefix
|
||||
let sha_hex = file_name.strip_prefix("pack-").unwrap_or(&file_name);
|
||||
match ObjectId::from_hex(&sha_hex.as_bytes()[..40]) {
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"unconventional pack name {:?}, falling back to fxhash: {}",
|
||||
path, e
|
||||
);
|
||||
hash(path)
|
||||
},
|
||||
Ok(oid) => {
|
||||
let mut buf = [0u8; 8];
|
||||
buf.copy_from_slice(&oid.sha1()[..8]);
|
||||
u64::from_be_bytes(buf)
|
||||
},
|
||||
}
|
||||
};
|
||||
let info = Info { hash, data_path };
|
||||
|
||||
Ok(Self { file, info })
|
||||
}
|
||||
|
||||
pub fn contains(&self, id: impl AsRef<oid>) -> bool {
|
||||
self.file.lookup(id).is_some()
|
||||
}
|
||||
|
||||
pub fn ofs(&self, id: impl AsRef<oid>) -> Option<u64> {
|
||||
self.file
|
||||
.lookup(id)
|
||||
.map(|idx| self.file.pack_offset_at_index(idx))
|
||||
}
|
||||
}
|
||||
|
||||
fn hash(p: &Path) -> u64 {
|
||||
use std::hash::{Hash as _, Hasher as _};
|
||||
|
||||
let mut hasher = FxHasher::default();
|
||||
p.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
// 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::sync::Arc;
|
||||
|
||||
use arc_swap::{ArcSwap, Guard};
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use super::pack;
|
||||
|
||||
mod metrics;
|
||||
pub use metrics::{Metrics, Stats, StatsView};
|
||||
|
||||
/// A threadsafe, shareable cache of packfiles.
|
||||
pub trait Cache {
|
||||
type Stats;
|
||||
|
||||
fn stats(&self) -> Self::Stats;
|
||||
|
||||
fn get(&self, info: &pack::Info) -> Result<Arc<pack::Data>, pack::error::Data>;
|
||||
}
|
||||
|
||||
impl<M, const B: usize, const S: usize> Cache for Fixed<M, B, S>
|
||||
where
|
||||
M: Metrics,
|
||||
{
|
||||
type Stats = M::Snapshot;
|
||||
|
||||
fn stats(&self) -> Self::Stats {
|
||||
self.stats()
|
||||
}
|
||||
|
||||
fn get(&self, info: &pack::Info) -> Result<Arc<pack::Data>, pack::error::Data> {
|
||||
self.get(info)
|
||||
}
|
||||
}
|
||||
|
||||
/// 128 open files
|
||||
pub type Small<S> = Fixed<S, 16, 8>;
|
||||
/// 512 open files
|
||||
pub type Medium<S> = Fixed<S, 32, 16>;
|
||||
/// 1024 open files
|
||||
pub type Large<S> = Fixed<S, 64, 16>;
|
||||
/// 2048 open files
|
||||
pub type XLarge<S> = Fixed<S, 128, 16>;
|
||||
|
||||
/// A fixed-size [`Cache`].
|
||||
///
|
||||
/// [`Fixed`] is essentially a very simple, fixed-capacity hashtable. When a
|
||||
/// pack (data-) file is requested via [`Cache::get`], the file is loaded
|
||||
/// (typically `mmap`ed) from disk if it is not already in the cache. Otherwise,
|
||||
/// a pointer to the already loaded file is returned. Old entries are replaced
|
||||
/// on an approximate LRU basis when the cache becomes full (this means that old
|
||||
/// entries are **not** evicted when there is still space).
|
||||
///
|
||||
/// The implementation is a somewhat dumbed-down version of JGit's
|
||||
/// `WindowCache`. The main differences are that the table buckets are of fixed
|
||||
/// size (`SLOTS`), instead of a linked list. This means that the cache does not
|
||||
/// allow to (temporarily) commit more entries than its nominal capacity.
|
||||
///
|
||||
/// Reading cached values is lock-free and mostly wait-free. Modifications are
|
||||
/// guarded by locks on individual buckets; if a cache miss occurs, multiple
|
||||
/// threads requesting the same entry will be blocked until one of them
|
||||
/// succeeded loading the data and updating the cache. Writers will _not_,
|
||||
/// however, contend with readers (unlike `RwLock`).
|
||||
///
|
||||
/// This favours usage patterns where different threads tend to request disjoint
|
||||
/// sets of packfiles, and of course their hashes colliding relatively
|
||||
/// infrequently.
|
||||
pub struct Fixed<M, const BUCKETS: usize, const SLOTS: usize> {
|
||||
entries: [ArcSwap<[Option<Arc<pack::Data>>; SLOTS]>; BUCKETS],
|
||||
locks: [Mutex<()>; BUCKETS],
|
||||
stats: M,
|
||||
}
|
||||
|
||||
trait AssertSendSync: Send + Sync {}
|
||||
impl<M, const B: usize, const S: usize> AssertSendSync for Fixed<M, B, S> where M: Send + Sync {}
|
||||
|
||||
impl<M, const B: usize, const S: usize> AsRef<Fixed<M, B, S>> for Fixed<M, B, S> {
|
||||
fn as_ref(&self) -> &Fixed<M, B, S> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<const B: usize, const S: usize> Default for Fixed<(), B, S> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
entries: [(); B].map(|_| ArcSwap::new(Arc::new([(); S].map(|_| None)))),
|
||||
locks: [(); B].map(|_| Mutex::new(())),
|
||||
stats: (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<M, const B: usize, const S: usize> Fixed<M, B, S>
|
||||
where
|
||||
M: Metrics,
|
||||
{
|
||||
pub fn with_stats(self) -> Fixed<Stats, B, S> {
|
||||
self.with_metrics(Stats::default())
|
||||
}
|
||||
|
||||
pub fn with_metrics<N: Metrics>(self, m: N) -> Fixed<N, B, S> {
|
||||
Fixed {
|
||||
entries: self.entries,
|
||||
locks: self.locks,
|
||||
stats: m,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stats(&self) -> M::Snapshot {
|
||||
let open_files = self
|
||||
.entries
|
||||
.iter()
|
||||
.map(|bucket| bucket.load().iter().flatten().count())
|
||||
.sum();
|
||||
self.stats.snapshot(open_files)
|
||||
}
|
||||
|
||||
pub fn get(&self, info: &pack::Info) -> Result<Arc<pack::Data>, pack::error::Data> {
|
||||
let idx = info.hash as usize % self.entries.len();
|
||||
|
||||
let bucket = self.entries[idx].load();
|
||||
for entry in bucket.iter().flatten() {
|
||||
if entry.hash == info.hash {
|
||||
self.stats.record_hit();
|
||||
entry.hit();
|
||||
return Ok(Arc::clone(entry));
|
||||
}
|
||||
}
|
||||
drop(bucket);
|
||||
|
||||
self.stats.record_miss();
|
||||
|
||||
// Cache miss, try to load the data file
|
||||
let lock = self.locks[idx].lock();
|
||||
// Did someone else win the race for the lock?
|
||||
let bucket = self.entries[idx].load();
|
||||
for entry in bucket.iter().flatten() {
|
||||
if entry.hash == info.hash {
|
||||
self.stats.record_hit();
|
||||
entry.hit();
|
||||
return Ok(Arc::clone(entry));
|
||||
}
|
||||
}
|
||||
// No, proceed
|
||||
self.stats.record_load();
|
||||
let data = Arc::new(info.data()?);
|
||||
|
||||
// Find an empty slot, or swap with the least popular
|
||||
let mut access = usize::MAX;
|
||||
let mut evict = 0;
|
||||
for (i, e) in bucket.iter().enumerate() {
|
||||
match e {
|
||||
Some(entry) => {
|
||||
let hits = entry.hits();
|
||||
if hits < access {
|
||||
access = hits;
|
||||
evict = i;
|
||||
}
|
||||
},
|
||||
None => {
|
||||
evict = i;
|
||||
break;
|
||||
},
|
||||
}
|
||||
}
|
||||
let mut entries = Guard::into_inner(bucket);
|
||||
{
|
||||
// This costs `SLOTS` refcount increments if the slot is currently
|
||||
// borrowed.
|
||||
let mutti = Arc::make_mut(&mut entries);
|
||||
mutti[evict] = Some(Arc::clone(&data));
|
||||
}
|
||||
self.entries[idx].store(entries);
|
||||
drop(lock);
|
||||
|
||||
data.hit();
|
||||
Ok(data)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// 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::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use tracing::trace;
|
||||
|
||||
pub struct StatsView {
|
||||
/// Total number of times the requested data was found in the cache.
|
||||
pub cache_hits: usize,
|
||||
/// Total number of times the requested data was not found in the cache.
|
||||
///
|
||||
/// Note that a cache hit can occur after a miss if another thread was
|
||||
/// faster to fill in the missing entry. Thus, `cache_hits + cache_misses`
|
||||
/// does not necessarily sum up to the number of cache accesses.
|
||||
pub cache_misses: usize,
|
||||
/// Total number of times a pack file was attempted to be loaded from disk
|
||||
/// (incl. failed attempts).
|
||||
pub file_loads: usize,
|
||||
/// Total number of pack files the cache holds on to.
|
||||
pub open_files: usize,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Stats {
|
||||
hits: AtomicUsize,
|
||||
miss: AtomicUsize,
|
||||
load: AtomicUsize,
|
||||
}
|
||||
|
||||
pub trait Metrics {
|
||||
type Snapshot;
|
||||
|
||||
fn record_hit(&self);
|
||||
fn record_miss(&self);
|
||||
fn record_load(&self);
|
||||
|
||||
fn snapshot(&self, open_files: usize) -> Self::Snapshot;
|
||||
}
|
||||
|
||||
impl Metrics for Stats {
|
||||
type Snapshot = StatsView;
|
||||
|
||||
fn record_hit(&self) {
|
||||
trace!("cache hit");
|
||||
self.hits.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn record_miss(&self) {
|
||||
trace!("cache miss");
|
||||
self.miss.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn record_load(&self) {
|
||||
trace!("pack load");
|
||||
self.load.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn snapshot(&self, open_files: usize) -> Self::Snapshot {
|
||||
StatsView {
|
||||
cache_hits: self.hits.load(Ordering::Relaxed),
|
||||
cache_misses: self.miss.load(Ordering::Relaxed),
|
||||
file_loads: self.load.load(Ordering::Relaxed),
|
||||
open_files,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Metrics for () {
|
||||
type Snapshot = ();
|
||||
|
||||
fn record_hit(&self) {}
|
||||
fn record_miss(&self) {}
|
||||
fn record_load(&self) {}
|
||||
|
||||
fn snapshot(&self, _: usize) -> Self::Snapshot {}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// 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::ByteSlice as _;
|
||||
use git_protocol::transport::client;
|
||||
use versions::Version;
|
||||
|
||||
pub mod fetch;
|
||||
pub mod ls;
|
||||
pub mod packwriter;
|
||||
pub mod take;
|
||||
pub mod transport;
|
||||
pub mod upload_pack;
|
||||
|
||||
pub use fetch::{fetch, Ref};
|
||||
pub use ls::ls_refs;
|
||||
pub use packwriter::PackWriter;
|
||||
pub use upload_pack::upload_pack;
|
||||
|
||||
pub use git_hash::{oid, ObjectId};
|
||||
|
||||
fn remote_git_version(caps: &client::Capabilities) -> Option<Version> {
|
||||
let agent = caps.capability("agent").and_then(|cap| {
|
||||
cap.value()
|
||||
.and_then(|bs| bs.to_str().map(|s| s.to_owned()).ok())
|
||||
})?;
|
||||
Version::new(agent.strip_prefix("git/")?)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// 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.
|
||||
|
||||
pub mod db;
|
||||
pub use git_ref::*;
|
||||
@@ -0,0 +1,269 @@
|
||||
// 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::{
|
||||
collections::BTreeSet,
|
||||
convert::TryInto,
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
use git_ref::{
|
||||
file::{self, iter::LooseThenPacked, Transaction, WriteReflog},
|
||||
packed,
|
||||
FullName,
|
||||
PartialNameRef,
|
||||
Reference,
|
||||
Target,
|
||||
};
|
||||
use parking_lot::RwLock;
|
||||
|
||||
pub mod error {
|
||||
use super::*;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Open {
|
||||
#[error("failed to take a snapshot of packed-refs")]
|
||||
Snapshot(#[from] Snapshot),
|
||||
|
||||
#[error(transparent)]
|
||||
Io(#[from] io::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Snapshot {
|
||||
#[error("failed to lock packed-refs")]
|
||||
Lock(#[from] git_lock::acquire::Error),
|
||||
|
||||
#[error("failed to open packed-refs")]
|
||||
Open(#[from] packed::buffer::open::Error),
|
||||
|
||||
#[error(transparent)]
|
||||
Io(#[from] io::Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Follow {
|
||||
#[error("cyclic symref: {0:?}")]
|
||||
Cycle(FullName),
|
||||
|
||||
#[error("reference {0:?} not found")]
|
||||
NotFound(FullName),
|
||||
|
||||
#[error("max symref depth {0} exceeded")]
|
||||
DepthLimitExceeded(usize),
|
||||
|
||||
#[error(transparent)]
|
||||
Find(#[from] file::find::Error),
|
||||
}
|
||||
}
|
||||
|
||||
/// Threadsafe refdb with shareable `packed-refs` memory buffer.
|
||||
///
|
||||
/// Packed refs are a delicate business: they are written by an external
|
||||
/// process, [`git-pack-refs`], _or_ when a packed ref is deleted. It may also
|
||||
/// be that no `packed-refs` currently exist.
|
||||
///
|
||||
/// The only way we can be certain to operate on a consistent view of what is
|
||||
/// committed to disk is to check if the `packed-refs` file has changed since we
|
||||
/// last read it. This would be quite expensive to do for small operations.
|
||||
/// Thus, the caller is responsible for determining just how much they can
|
||||
/// afford to see possibly out-of-date data: the [`Refdb::snapshot`] method
|
||||
/// checks if the previously loaded `packed-refs` appear to be out-of-date, and
|
||||
/// reloads them if necessary. The resulting [`Snapshot`] contains a pointer to
|
||||
/// an immutable memory buffer of the packed refs which can be shared between
|
||||
/// threads, or cloned.
|
||||
///
|
||||
/// [`git-pack-refs`]: https://git-scm.com/docs/git-pack-refs
|
||||
#[derive(Clone)]
|
||||
pub struct Refdb {
|
||||
store: file::Store,
|
||||
packed: Arc<RwLock<Option<Packed>>>,
|
||||
}
|
||||
|
||||
impl Refdb {
|
||||
pub fn open(git_dir: impl Into<PathBuf>) -> Result<Self, error::Open> {
|
||||
let store = file::Store::at(git_dir, WriteReflog::Normal);
|
||||
let packed = Arc::new(RwLock::new(Packed::open(store.packed_refs_path())?));
|
||||
Ok(Self { store, packed })
|
||||
}
|
||||
|
||||
pub fn snapshot(&self) -> Result<Snapshot, error::Snapshot> {
|
||||
let read = self.packed.read();
|
||||
match &*read {
|
||||
None => {
|
||||
drop(read);
|
||||
// always modified, because it was None and now is Some
|
||||
self.reload(|_| true)
|
||||
},
|
||||
|
||||
Some(packed) => {
|
||||
if packed.is_modified()? {
|
||||
let mtime = packed.mtime;
|
||||
drop(read);
|
||||
// we don't care what the mtime is, only that we have a
|
||||
// different value than before
|
||||
self.reload(|packed1| packed1.mtime != mtime)
|
||||
} else {
|
||||
Ok(Snapshot {
|
||||
store: self.store.clone(),
|
||||
packed: Some(packed.buf.clone()),
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn reload<F>(&self, modified_while_blocked: F) -> Result<Snapshot, error::Snapshot>
|
||||
where
|
||||
F: FnOnce(&Packed) -> bool,
|
||||
{
|
||||
let mut write = self.packed.write();
|
||||
if let Some(packed) = &*write {
|
||||
if modified_while_blocked(packed) {
|
||||
return Ok(Snapshot {
|
||||
store: self.store.clone(),
|
||||
packed: Some(packed.buf.clone()),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
match Packed::open(self.store.packed_refs_path())? {
|
||||
Some(packed) => {
|
||||
let buf = packed.buf.clone();
|
||||
*write = Some(packed);
|
||||
Ok(Snapshot {
|
||||
store: self.store.clone(),
|
||||
packed: Some(buf),
|
||||
})
|
||||
},
|
||||
|
||||
None => {
|
||||
*write = None;
|
||||
Ok(Snapshot {
|
||||
store: self.store.clone(),
|
||||
packed: None,
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Snapshot {
|
||||
store: file::Store,
|
||||
packed: Option<Arc<packed::Buffer>>,
|
||||
}
|
||||
|
||||
impl Snapshot {
|
||||
pub fn find<'a, N, E>(&self, name: N) -> Result<Option<Reference>, file::find::Error>
|
||||
where
|
||||
N: TryInto<PartialNameRef<'a>, Error = E>,
|
||||
file::find::Error: From<E>,
|
||||
{
|
||||
self.store.try_find(name, self.packed.as_deref())
|
||||
}
|
||||
|
||||
pub fn transaction(&self) -> Transaction {
|
||||
self.store.transaction()
|
||||
}
|
||||
|
||||
pub fn iter(&self, prefix: Option<impl AsRef<Path>>) -> io::Result<LooseThenPacked> {
|
||||
let packed = self.packed.as_deref();
|
||||
match prefix {
|
||||
None => self.store.iter(packed),
|
||||
Some(p) => self.store.iter_prefixed(packed, p),
|
||||
}
|
||||
}
|
||||
|
||||
/// Follow a symbolic reference until a direct reference is found.
|
||||
///
|
||||
/// If `symref` is a direct reference, a copy of it is returned. No more
|
||||
/// than five symbolic references will be followed, and cyclic
|
||||
/// references are detected. Both result in an error to be returned.
|
||||
///
|
||||
/// Note that following is not the same as "peeling": no access to the
|
||||
/// object database is made, and thus no assumptions about the kind of
|
||||
/// object the reference ultimately points to can be made.
|
||||
pub fn follow(&self, symref: &Reference) -> Result<Reference, error::Follow> {
|
||||
match &symref.target {
|
||||
Target::Peeled(_) => Ok(symref.clone()),
|
||||
Target::Symbolic(name) => {
|
||||
let mut seen = BTreeSet::new();
|
||||
seen.insert(symref.name.clone());
|
||||
|
||||
let mut next = self
|
||||
.find(name.to_partial())?
|
||||
.ok_or_else(|| error::Follow::NotFound(name.clone()))?;
|
||||
seen.insert(name.clone());
|
||||
|
||||
const MAX_DEPTH: usize = 5;
|
||||
loop {
|
||||
match next.target {
|
||||
Target::Peeled(_) => return Ok(next),
|
||||
Target::Symbolic(sym) => {
|
||||
if seen.len() + 1 > MAX_DEPTH {
|
||||
return Err(error::Follow::DepthLimitExceeded(MAX_DEPTH));
|
||||
}
|
||||
|
||||
if seen.contains(&sym) {
|
||||
return Err(error::Follow::Cycle(sym));
|
||||
}
|
||||
next = self
|
||||
.find(sym.to_partial())?
|
||||
.ok_or_else(|| error::Follow::NotFound(sym.clone()))?;
|
||||
seen.insert(sym);
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Packed {
|
||||
buf: Arc<packed::Buffer>,
|
||||
path: PathBuf,
|
||||
mtime: SystemTime,
|
||||
}
|
||||
|
||||
impl Packed {
|
||||
fn open(path: PathBuf) -> Result<Option<Self>, error::Snapshot> {
|
||||
use git_lock::{acquire, Marker};
|
||||
|
||||
let _lock = Marker::acquire_to_hold_resource(
|
||||
&path,
|
||||
acquire::Fail::AfterDurationWithBackoff(Duration::from_millis(500)),
|
||||
None,
|
||||
)?;
|
||||
match path.metadata() {
|
||||
// `git-lock` will happily lock a non-existent file
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
|
||||
Err(e) => Err(e.into()),
|
||||
|
||||
Ok(meta) => {
|
||||
let mtime = meta.modified()?;
|
||||
let buf = Arc::new(packed::Buffer::open(&path, 32 * 1024)?);
|
||||
Ok(Some(Self { buf, path, mtime }))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn is_modified(&self) -> io::Result<bool> {
|
||||
match self.path.metadata() {
|
||||
// it existed before, so gone is modified
|
||||
Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(true),
|
||||
Err(e) => Err(e),
|
||||
|
||||
Ok(meta) => {
|
||||
let mtime = meta.modified()?;
|
||||
Ok(self.mtime == mtime)
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
// 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::{fmt::Debug, ops::Deref, str::FromStr};
|
||||
|
||||
use git2::transport::Service as GitService;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
lazy_static! {
|
||||
static ref SERVICE_REGEX: regex::Regex = regex::Regex::new(r"(\S+) '/?(.+)'").unwrap();
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub struct Service(pub GitService);
|
||||
|
||||
/// A service and URN as passed to the exec_request of an SSH server by git when
|
||||
/// talking to an SSH remote. The `FromStr` implementation for this type expects
|
||||
/// a string of the form:
|
||||
///
|
||||
/// <request type> /<path>
|
||||
///
|
||||
/// Where the request type is either `upload-pack` or `receive-pack`, the
|
||||
/// leading slash before the urn is optional, and the `path` is whatever the
|
||||
/// `FromStr` of `Path` provides.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SshService<Path> {
|
||||
pub service: Service,
|
||||
pub path: Path,
|
||||
}
|
||||
|
||||
impl<Path> SshService<Path> {
|
||||
pub fn is_upload(&self) -> bool {
|
||||
match self.service.0 {
|
||||
GitService::UploadPackLs | GitService::UploadPack => true,
|
||||
GitService::ReceivePackLs | GitService::ReceivePack => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_receive(&self) -> bool {
|
||||
!self.is_upload()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GitService> for Service {
|
||||
fn from(g: GitService) -> Self {
|
||||
Service(g)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Service> for GitService {
|
||||
fn from(s: Service) -> Self {
|
||||
s.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ParseService {
|
||||
#[error("the exec str must be in the form <service> <urn>")]
|
||||
Format,
|
||||
#[error(transparent)]
|
||||
Namespace(Box<dyn std::error::Error + Send + Sync + 'static>),
|
||||
#[error("unknown service {0}")]
|
||||
UnknownService(String),
|
||||
}
|
||||
|
||||
impl Debug for Service {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
f.debug_tuple("Service")
|
||||
.field(match self.0 {
|
||||
GitService::UploadPackLs => &"UploadPackLs",
|
||||
GitService::UploadPack => &"UploadPack",
|
||||
GitService::ReceivePackLs => &"ReceivePackLs",
|
||||
GitService::ReceivePack => &"ReceivePack",
|
||||
})
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Service {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self.0 {
|
||||
GitService::UploadPack => write!(f, "upload-pack"),
|
||||
GitService::UploadPackLs => write!(f, "upload-pack-ls"),
|
||||
GitService::ReceivePack => write!(f, "receive-pack"),
|
||||
GitService::ReceivePackLs => write!(f, "receive-pack-ls"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Service {
|
||||
type Target = GitService;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<Path> FromStr for SshService<Path>
|
||||
where
|
||||
Path: FromStr,
|
||||
Path::Err: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
type Err = ParseService;
|
||||
|
||||
fn from_str(exec_str: &str) -> Result<Self, Self::Err> {
|
||||
let cap = SERVICE_REGEX
|
||||
.captures_iter(exec_str)
|
||||
.next()
|
||||
.ok_or(ParseService::Format)?;
|
||||
debug_assert!(cap.len() == 3);
|
||||
let service_str: &str = &cap[1];
|
||||
let urn_str = &cap[2];
|
||||
|
||||
let path = urn_str
|
||||
.parse()
|
||||
.map_err(|err| ParseService::Namespace(Box::new(err)))?;
|
||||
let service = match service_str {
|
||||
"git-upload-pack" => Ok(Service(GitService::UploadPack)),
|
||||
"git-receive-pack" => Ok(Service(GitService::ReceivePack)),
|
||||
other => Err(ParseService::UnknownService(other.to_string())),
|
||||
}?;
|
||||
Ok(Self { service, path })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
[package]
|
||||
name = "link-git-test"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
test = true
|
||||
doc = false
|
||||
|
||||
[features]
|
||||
test = []
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1"
|
||||
bstr = "0.2"
|
||||
futures = "0.3"
|
||||
futures_ringbuf = "0.3"
|
||||
tempfile = "3.3"
|
||||
|
||||
[dev-dependencies.git2]
|
||||
version = "0.13.24"
|
||||
default-features = false
|
||||
features = ["vendored-libgit2"]
|
||||
|
||||
[dev-dependencies.link-git]
|
||||
path = ".."
|
||||
features = ["git2"]
|
||||
@@ -0,0 +1,6 @@
|
||||
// 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.
|
||||
|
||||
mod protocol;
|
||||
@@ -0,0 +1,382 @@
|
||||
// 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::{
|
||||
collections::BTreeSet,
|
||||
io,
|
||||
path::Path,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
};
|
||||
|
||||
use bstr::ByteSlice as _;
|
||||
use futures::{AsyncReadExt as _, TryFutureExt as _};
|
||||
use link_git::protocol::{fetch, ls, packwriter, upload_pack, ObjectId, PackWriter, Ref};
|
||||
use tempfile::{tempdir, TempDir};
|
||||
|
||||
fn upstream() -> TempDir {
|
||||
let tmp = tempdir().unwrap();
|
||||
|
||||
let repo = git2::Repository::init_bare(&tmp).unwrap();
|
||||
let auth = git2::Signature::now("apollo", "apollo@cree.de").unwrap();
|
||||
|
||||
let tree = {
|
||||
let empty = repo.treebuilder(None).unwrap();
|
||||
let oid = empty.write().unwrap();
|
||||
repo.find_tree(oid).unwrap()
|
||||
};
|
||||
let base = {
|
||||
let oid = repo
|
||||
.commit(
|
||||
Some("refs/namespaces/foo/refs/heads/main"),
|
||||
&auth,
|
||||
&auth,
|
||||
"initial",
|
||||
&tree,
|
||||
&[],
|
||||
)
|
||||
.unwrap();
|
||||
repo.find_commit(oid).unwrap()
|
||||
};
|
||||
let next = repo
|
||||
.commit(
|
||||
Some("refs/namespaces/foo/refs/heads/next"),
|
||||
&auth,
|
||||
&auth,
|
||||
"ng",
|
||||
&tree,
|
||||
&[&base],
|
||||
)
|
||||
.unwrap();
|
||||
repo.reference(
|
||||
"refs/namespaces/foo/refs/pulls/1/head",
|
||||
next,
|
||||
true,
|
||||
"pee arrr",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
tmp
|
||||
}
|
||||
|
||||
fn collect_refs(repo: &git2::Repository) -> Result<Vec<(String, git2::Oid)>, git2::Error> {
|
||||
repo.references()?
|
||||
.map(|x| x.map(|r| (r.name().unwrap().to_owned(), r.target().unwrap())))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn update_tips<'a, T>(repo: &git2::Repository, tips: T) -> Result<(), anyhow::Error>
|
||||
where
|
||||
T: IntoIterator<Item = &'a Ref>,
|
||||
{
|
||||
for r in tips {
|
||||
match r {
|
||||
Ref::Direct { path, object } => {
|
||||
repo.reference(
|
||||
path.to_str()?,
|
||||
git2::Oid::from_bytes(object.as_slice())?,
|
||||
true,
|
||||
"",
|
||||
)?;
|
||||
},
|
||||
x => anyhow::bail!("unexpected ref variant: {:?}", x),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn collect_history(repo: &git2::Repository, tip: &str) -> Result<Vec<git2::Oid>, git2::Error> {
|
||||
let mut revwalk = repo.revwalk()?;
|
||||
revwalk.push_ref(tip)?;
|
||||
revwalk.collect()
|
||||
}
|
||||
|
||||
fn run_ls_refs<R: AsRef<Path>>(remote: R, opt: ls::Options) -> io::Result<Vec<Ref>> {
|
||||
let (client, server) = futures_ringbuf::Endpoint::pair(256, 256);
|
||||
let client = async move {
|
||||
let (recv, send) = client.split();
|
||||
ls::ls_refs(opt, recv, send).await
|
||||
};
|
||||
let server = {
|
||||
let (recv, send) = server.split();
|
||||
upload_pack::upload_pack(&remote, recv, send).and_then(|(_hdr, run)| run)
|
||||
};
|
||||
|
||||
let (client_out, server_out) =
|
||||
futures::executor::block_on(futures::future::try_join(client, server))?;
|
||||
assert!(server_out.success());
|
||||
Ok(client_out)
|
||||
}
|
||||
|
||||
fn run_fetch<R, B, P>(
|
||||
remote: R,
|
||||
opt: fetch::Options,
|
||||
build_pack_writer: B,
|
||||
) -> io::Result<fetch::Outputs<P::Output>>
|
||||
where
|
||||
R: AsRef<Path>,
|
||||
B: FnOnce(Arc<AtomicBool>) -> P,
|
||||
P: PackWriter + Send + 'static,
|
||||
P::Output: Send + 'static,
|
||||
{
|
||||
let (client, server) = futures_ringbuf::Endpoint::pair(256, 256);
|
||||
let client = async move {
|
||||
let (recv, send) = client.split();
|
||||
fetch::fetch(opt, build_pack_writer, recv, send).await
|
||||
};
|
||||
let server = {
|
||||
let (recv, send) = server.split();
|
||||
upload_pack::upload_pack(&remote, recv, send).and_then(|(_hdr, run)| run)
|
||||
};
|
||||
|
||||
let (client_out, server_out) =
|
||||
futures::executor::block_on(futures::future::try_join(client, server))?;
|
||||
assert!(server_out.success());
|
||||
Ok(client_out)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn smoke() {
|
||||
let remote = upstream();
|
||||
let refs = run_ls_refs(
|
||||
&remote,
|
||||
ls::Options {
|
||||
repo: "foo".into(),
|
||||
extra_params: vec![],
|
||||
ref_prefixes: vec!["refs/heads/".into(), "refs/pulls/".into()],
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
refs.iter().map(|r| r.unpack().0).collect::<BTreeSet<_>>(),
|
||||
[
|
||||
"refs/heads/main".into(),
|
||||
"refs/heads/next".into(),
|
||||
"refs/pulls/1/head".into()
|
||||
]
|
||||
.iter()
|
||||
.collect::<BTreeSet<_>>()
|
||||
);
|
||||
|
||||
let out = run_fetch(
|
||||
&remote,
|
||||
fetch::Options {
|
||||
repo: "foo".into(),
|
||||
extra_params: vec![],
|
||||
haves: vec![],
|
||||
wants: vec![],
|
||||
want_refs: refs.iter().map(|r| r.unpack().0.clone()).collect(),
|
||||
},
|
||||
|_| packwriter::Discard,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(out.pack.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn want_ref() {
|
||||
let remote = upstream();
|
||||
let out = run_fetch(
|
||||
&remote,
|
||||
fetch::Options {
|
||||
repo: "foo".into(),
|
||||
extra_params: vec![],
|
||||
haves: vec![],
|
||||
wants: vec![],
|
||||
want_refs: vec!["refs/heads/main".into(), "refs/pulls/1/head".into()],
|
||||
},
|
||||
|_| packwriter::Discard,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(out.pack.is_some());
|
||||
assert_eq!(
|
||||
out.wanted_refs
|
||||
.iter()
|
||||
.map(|r| r.unpack().0)
|
||||
.collect::<BTreeSet<_>>(),
|
||||
["refs/heads/main".into(), "refs/pulls/1/head".into(),]
|
||||
.iter()
|
||||
.collect::<BTreeSet<_>>()
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "`fetch` is empty")]
|
||||
fn empty_fetch() {
|
||||
let remote = upstream();
|
||||
run_fetch(
|
||||
&remote,
|
||||
fetch::Options {
|
||||
repo: "foo".into(),
|
||||
extra_params: vec![],
|
||||
haves: vec![],
|
||||
wants: vec![],
|
||||
want_refs: vec![],
|
||||
},
|
||||
|_| packwriter::Discard,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn clone_with<R, L, B, P>(remote: R, local: L, build_pack_writer: B)
|
||||
where
|
||||
R: AsRef<Path>,
|
||||
L: AsRef<Path>,
|
||||
B: FnOnce(Arc<AtomicBool>) -> P,
|
||||
P: PackWriter + Send + 'static,
|
||||
P::Output: Send + 'static,
|
||||
{
|
||||
let refs = run_ls_refs(
|
||||
&remote,
|
||||
ls::Options {
|
||||
repo: "foo".into(),
|
||||
extra_params: vec![],
|
||||
ref_prefixes: vec!["refs/heads/".into(), "refs/pulls/".into()],
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let out = run_fetch(
|
||||
&remote,
|
||||
fetch::Options {
|
||||
repo: "foo".into(),
|
||||
extra_params: vec![],
|
||||
haves: vec![],
|
||||
wants: vec![],
|
||||
want_refs: refs.iter().map(|r| r.unpack().0.clone()).collect(),
|
||||
},
|
||||
build_pack_writer,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(out.pack.is_some());
|
||||
|
||||
let remote_repo = git2::Repository::open(remote).unwrap();
|
||||
remote_repo.set_namespace("foo").unwrap();
|
||||
let local_repo = git2::Repository::open(&local).unwrap();
|
||||
|
||||
update_tips(&local_repo, &out.wanted_refs).unwrap();
|
||||
|
||||
let mut remote_refs = collect_refs(&remote_repo).unwrap();
|
||||
let mut local_refs = collect_refs(&local_repo).unwrap();
|
||||
|
||||
remote_refs.sort();
|
||||
local_refs.sort();
|
||||
|
||||
assert_eq!(remote_refs, local_refs);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clone_libgit() {
|
||||
let remote = upstream();
|
||||
let local = tempdir().unwrap();
|
||||
let local_repo = git2::Repository::init(&local).unwrap();
|
||||
|
||||
clone_with(&remote, &local, move |stop| {
|
||||
packwriter::Libgit::new(packwriter::Options::default(), local_repo, stop)
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clone_gitoxide() {
|
||||
let remote = upstream();
|
||||
let local = tempdir().unwrap();
|
||||
let local_repo = git2::Repository::init(&local).unwrap();
|
||||
|
||||
clone_with(&remote, &local, move |stop| {
|
||||
packwriter::Standard::new(
|
||||
local_repo.path(),
|
||||
packwriter::Options::default(),
|
||||
packwriter::StandardThickener::new(local_repo.path()),
|
||||
stop,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn thin_pack_with<R, L, B, P>(remote: R, local: L, build_pack_writer: B)
|
||||
where
|
||||
R: AsRef<Path>,
|
||||
L: AsRef<Path>,
|
||||
B: Fn(Arc<AtomicBool>) -> P,
|
||||
P: PackWriter + Send + 'static,
|
||||
P::Output: Send + 'static,
|
||||
{
|
||||
// Clone main only
|
||||
{
|
||||
let out = run_fetch(
|
||||
&remote,
|
||||
fetch::Options {
|
||||
repo: "foo".into(),
|
||||
extra_params: vec![],
|
||||
haves: vec![],
|
||||
wants: vec![],
|
||||
want_refs: vec!["refs/heads/main".into()],
|
||||
},
|
||||
&build_pack_writer,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(out.pack.is_some());
|
||||
}
|
||||
|
||||
let remote_repo = git2::Repository::open(&remote).unwrap();
|
||||
remote_repo.set_namespace("foo").unwrap();
|
||||
let local_repo = git2::Repository::open(&local).unwrap();
|
||||
|
||||
// Fetch next, which is ahead of main
|
||||
{
|
||||
let head = remote_repo.refname_to_id("refs/heads/main").unwrap();
|
||||
let out = run_fetch(
|
||||
&remote,
|
||||
fetch::Options {
|
||||
repo: "foo".into(),
|
||||
extra_params: vec![],
|
||||
haves: vec![ObjectId::from_20_bytes(head.as_bytes())],
|
||||
wants: vec![],
|
||||
want_refs: vec!["refs/heads/next".into()],
|
||||
},
|
||||
build_pack_writer,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(out.pack.is_some());
|
||||
|
||||
update_tips(&local_repo, &out.wanted_refs).unwrap();
|
||||
}
|
||||
|
||||
let remote_history = collect_history(&remote_repo, "refs/heads/next").unwrap();
|
||||
let local_history = collect_history(&local_repo, "refs/heads/next").unwrap();
|
||||
|
||||
assert!(!remote_history.is_empty());
|
||||
assert_eq!(remote_history, local_history)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thin_pack_libgit() {
|
||||
let remote = upstream();
|
||||
let local = tempdir().unwrap();
|
||||
|
||||
thin_pack_with(&remote, &local, |stop| {
|
||||
let local_repo = git2::Repository::init(&local).unwrap();
|
||||
packwriter::Libgit::new(packwriter::Options::default(), local_repo, stop)
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thin_pack_gitoxide() {
|
||||
let remote = upstream();
|
||||
let local = tempdir().unwrap();
|
||||
let local_repo = git2::Repository::init(&local).unwrap();
|
||||
let git_dir = local_repo.path().to_owned();
|
||||
|
||||
thin_pack_with(&remote, &local, move |stop| {
|
||||
packwriter::Standard::new(
|
||||
&git_dir,
|
||||
packwriter::Options::default(),
|
||||
packwriter::StandardThickener::new(&git_dir),
|
||||
stop,
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// Copyright © 2022 The Radicle Link Contributors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#[cfg(test)]
|
||||
mod integration;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
@@ -0,0 +1,6 @@
|
||||
// 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.
|
||||
|
||||
mod protocol;
|
||||
@@ -0,0 +1,7 @@
|
||||
// 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.
|
||||
|
||||
mod take;
|
||||
mod upload_pack;
|
||||
@@ -0,0 +1,47 @@
|
||||
// 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 futures::{executor::block_on, io::Cursor, AsyncReadExt as _};
|
||||
use link_git::protocol::take::TryTake;
|
||||
use std::io;
|
||||
|
||||
#[test]
|
||||
fn when_within_limit() {
|
||||
let input = b"the world is everything that is the case";
|
||||
let output = block_on(async move {
|
||||
let mut buf = Vec::with_capacity(input.len());
|
||||
TryTake::new(Cursor::new(input), input.len() as u64 + 1)
|
||||
.read_to_end(&mut buf)
|
||||
.await?;
|
||||
Ok::<_, io::Error>(buf)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(input, output.as_slice())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn when_limit_exceeded() {
|
||||
let input = b"what is the case, the fact, is the existence of atomic facts";
|
||||
let output =
|
||||
block_on(TryTake::new(Cursor::new(input), 10).read_to_end(&mut Vec::new())).unwrap_err();
|
||||
|
||||
assert_eq!(output.to_string(), "max input size exceeded")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn excess_bytes_remain() {
|
||||
let input = b"whereof one cannot speak, thereof one must be silent";
|
||||
let output = block_on(async move {
|
||||
let mut buf = Vec::with_capacity(input.len());
|
||||
let res = TryTake::new(Cursor::new(input), input.len() as u64)
|
||||
.read_to_end(&mut buf)
|
||||
.await;
|
||||
assert!(res.is_err());
|
||||
buf
|
||||
});
|
||||
|
||||
assert_eq!(input, output.as_slice())
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// 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 link_git::protocol::upload_pack;
|
||||
|
||||
mod header {
|
||||
use super::*;
|
||||
use std::str::FromStr as _;
|
||||
|
||||
#[test]
|
||||
fn service_must_be_upload_pack() {
|
||||
assert_eq!(
|
||||
upload_pack::Header::from_str("git-receive-pack "),
|
||||
Err("unsupported service")
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_path() {
|
||||
assert_eq!(
|
||||
upload_pack::Header::from_str("git-upload-pack "),
|
||||
Err("missing path")
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_path() {
|
||||
assert_eq!(
|
||||
upload_pack::Header::from_str("git-upload-pack \0host=lolhost:123\0"),
|
||||
Err("empty path")
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_and_port() {
|
||||
assert_eq!(
|
||||
upload_pack::Header::from_str("git-upload-pack /git.git\0host=lolhost:123\0").unwrap(),
|
||||
upload_pack::Header {
|
||||
path: "/git.git".to_owned(),
|
||||
host: Some(("lolhost".to_owned(), Some(123))),
|
||||
extra: vec![]
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_without_port() {
|
||||
assert_eq!(
|
||||
upload_pack::Header::from_str("git-upload-pack /git.git\0host=lolhost\0").unwrap(),
|
||||
upload_pack::Header {
|
||||
path: "/git.git".to_owned(),
|
||||
host: Some(("lolhost".to_owned(), None)),
|
||||
extra: vec![]
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_host() {
|
||||
assert_eq!(
|
||||
upload_pack::Header::from_str("git-upload-pack /git.git\0").unwrap(),
|
||||
upload_pack::Header {
|
||||
path: "/git.git".to_owned(),
|
||||
host: None,
|
||||
extra: vec![]
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_host() {
|
||||
assert_eq!(
|
||||
upload_pack::Header::from_str("git-upload-pack /git.git\0\0").unwrap(),
|
||||
upload_pack::Header {
|
||||
path: "/git.git".to_owned(),
|
||||
host: None,
|
||||
extra: vec![]
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_host_extra() {
|
||||
assert_eq!(
|
||||
upload_pack::Header::from_str("git-upload-pack /git.git\0\0version=42\0").unwrap(),
|
||||
upload_pack::Header {
|
||||
path: "/git.git".to_owned(),
|
||||
host: None,
|
||||
extra: vec![("version".to_owned(), Some("42".to_owned()))]
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_port_extra() {
|
||||
assert_eq!(
|
||||
upload_pack::Header::from_str(
|
||||
"git-upload-pack /git.git\0host=lolhost:123\0\0version=42\0"
|
||||
)
|
||||
.unwrap(),
|
||||
upload_pack::Header {
|
||||
path: "/git.git".to_owned(),
|
||||
host: Some(("lolhost".to_owned(), Some(123))),
|
||||
extra: vec![("version".to_owned(), Some("42".to_owned()))]
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_extra_extra() {
|
||||
assert_eq!(
|
||||
upload_pack::Header::from_str(
|
||||
"git-upload-pack /git.git\0host=lolhost\0\0version=42\0foo\0n=69\0"
|
||||
)
|
||||
.unwrap(),
|
||||
upload_pack::Header {
|
||||
path: "/git.git".to_owned(),
|
||||
host: Some(("lolhost".to_owned(), None)),
|
||||
extra: vec![
|
||||
("version".to_owned(), Some("42".to_owned())),
|
||||
("foo".to_owned(), None),
|
||||
("n".to_owned(), Some("69".to_owned()))
|
||||
]
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "radicle-macros"
|
||||
version = "0.1.0"
|
||||
authors = ["The Radicle Team <dev@radicle.xyz>"]
|
||||
edition = "2018"
|
||||
license = "GPL-3.0-or-later"
|
||||
description = "Radicle procedural macros"
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
proc-macro = true
|
||||
test = false
|
||||
|
||||
[dependencies]
|
||||
proc-macro-error = "1.0.4"
|
||||
quote = "1"
|
||||
syn = "1"
|
||||
|
||||
[dependencies.radicle-git-ext]
|
||||
path = "../git-ext"
|
||||
@@ -0,0 +1,72 @@
|
||||
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
#[macro_use]
|
||||
extern crate proc_macro_error;
|
||||
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use proc_macro_error::abort;
|
||||
use quote::quote;
|
||||
use syn::{parse_macro_input, LitStr};
|
||||
|
||||
use radicle_git_ext::reference::name::{RefLike, RefspecPattern};
|
||||
|
||||
/// Create `RefLike` from a string literal.
|
||||
///
|
||||
/// The string is validated at compile time, and an unsafe conversion is
|
||||
/// emitted.
|
||||
///
|
||||
/// ```rust
|
||||
/// use radicle_macros::reflike;
|
||||
///
|
||||
/// assert_eq!("lolek/bolek", reflike!("lolek/bolek").as_str())
|
||||
/// ```
|
||||
#[proc_macro_error]
|
||||
#[proc_macro]
|
||||
pub fn reflike(input: TokenStream) -> TokenStream {
|
||||
let lit = parse_macro_input!(input as LitStr);
|
||||
|
||||
match RefLike::try_from(lit.value()) {
|
||||
Ok(safe) => {
|
||||
let safe: &str = &*safe;
|
||||
let expand = quote! { unsafe { ::std::mem::transmute::<_, ::radicle_git_ext::RefLike>(#safe.to_owned()) }};
|
||||
TokenStream::from(expand)
|
||||
},
|
||||
|
||||
Err(e) => {
|
||||
abort!(lit.span(), "invalid RefLike literal: {}", e);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a `RefspecPattern` from a string literal.
|
||||
///
|
||||
/// The string is validated at compile time, and an unsafe conversion is
|
||||
/// emitted.
|
||||
///
|
||||
/// ```rust
|
||||
/// use radicle_macros::refspec_pattern;
|
||||
///
|
||||
/// assert_eq!("refs/heads/*", refspec_pattern!("refs/heads/*").as_str())
|
||||
/// ```
|
||||
#[proc_macro_error]
|
||||
#[proc_macro]
|
||||
pub fn refspec_pattern(input: TokenStream) -> TokenStream {
|
||||
let lit = parse_macro_input!(input as LitStr);
|
||||
|
||||
match RefspecPattern::try_from(lit.value()) {
|
||||
Ok(safe) => {
|
||||
let safe: &str = &*safe;
|
||||
let expand = quote! { unsafe { ::std::mem::transmute::<_, ::radicle_git_ext::RefspecPattern>(#safe.to_owned()) }};
|
||||
TokenStream::from(expand)
|
||||
},
|
||||
|
||||
Err(e) => {
|
||||
abort!(lit.span(), "invalid RefspecPattern literal: {}", e);
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{ sources ? import ../sources.nix
|
||||
, pkgs ? import sources.nixpkgs
|
||||
}:
|
||||
with pkgs;
|
||||
rustPlatform.buildRustPackage rec {
|
||||
pname = "cargo-nextest";
|
||||
version = "0.9.9";
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "nextest-rs";
|
||||
repo = "nextest";
|
||||
rev = "cargo-nextest-${version}";
|
||||
sha256 = "sha256-1s1N126S51kg7aOgAb8oMts1zJcO6QRn1fwbQf6ZaJ8=";
|
||||
};
|
||||
|
||||
cargoSha256 = "sha256-JxZyl5Hti3Hh33e7H/pXhM6WkU0kDDml0naBPYzvNy4=";
|
||||
|
||||
nativeBuildInputs = [
|
||||
pkg-config
|
||||
];
|
||||
|
||||
buildInputs = [
|
||||
openssl
|
||||
libiconv
|
||||
] ++ lib.optionals stdenv.isDarwin [
|
||||
Security
|
||||
];
|
||||
|
||||
doCheck = false;
|
||||
}
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eoux pipefail
|
||||
|
||||
cargo fmt -- --check
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eou pipefail
|
||||
|
||||
./nix/ci/fmt
|
||||
./scripts/ci/lint
|
||||
./scripts/ci/build
|
||||
./scripts/ci/test
|
||||
./scripts/ci/docs
|
||||
./scripts/ci/advisory
|
||||
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"niv": {
|
||||
"branch": "master",
|
||||
"description": "Easy dependency management for Nix projects",
|
||||
"homepage": "https://github.com/nmattia/niv",
|
||||
"owner": "nmattia",
|
||||
"repo": "niv",
|
||||
"rev": "82e5cd1ad3c387863f0545d7591512e76ab0fc41",
|
||||
"sha256": "090l219mzc0gi33i3psgph6s2pwsc8qy4lyrqjdj4qzkvmaj65a7",
|
||||
"type": "tarball",
|
||||
"url": "https://github.com/nmattia/niv/archive/82e5cd1ad3c387863f0545d7591512e76ab0fc41.tar.gz",
|
||||
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
|
||||
},
|
||||
"nixpkgs": {
|
||||
"branch": "release-21.11",
|
||||
"description": "Nix Packages collection",
|
||||
"homepage": "",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "eabc38219184cc3e04a974fe31857d8e0eac098d",
|
||||
"sha256": "04ffwp2gzq0hhz7siskw6qh9ys8ragp7285vi1zh8xjksxn1msc5",
|
||||
"type": "tarball",
|
||||
"url": "https://github.com/NixOS/nixpkgs/archive/eabc38219184cc3e04a974fe31857d8e0eac098d.tar.gz",
|
||||
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
|
||||
},
|
||||
"rust-overlay": {
|
||||
"branch": "master",
|
||||
"description": "Pure and reproducible nix overlay of binary distributed rust toolchains",
|
||||
"homepage": "",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "c97cf9d581e09b767f5e3503b43dc3e4cd91bd99",
|
||||
"sha256": "02r3y1x4sdzdy0qzds6286v51406jk7ywks6fjivmb3c5mlhvq8x",
|
||||
"type": "tarball",
|
||||
"url": "https://github.com/oxalica/rust-overlay/archive/c97cf9d581e09b767f5e3503b43dc3e4cd91bd99.tar.gz",
|
||||
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
|
||||
}
|
||||
}
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
# This file has been generated by Niv.
|
||||
|
||||
let
|
||||
|
||||
#
|
||||
# The fetchers. fetch_<type> fetches specs of type <type>.
|
||||
#
|
||||
|
||||
fetch_file = pkgs: name: spec:
|
||||
let
|
||||
name' = sanitizeName name + "-src";
|
||||
in
|
||||
if spec.builtin or true then
|
||||
builtins_fetchurl { inherit (spec) url sha256; name = name'; }
|
||||
else
|
||||
pkgs.fetchurl { inherit (spec) url sha256; name = name'; };
|
||||
|
||||
fetch_tarball = pkgs: name: spec:
|
||||
let
|
||||
name' = sanitizeName name + "-src";
|
||||
in
|
||||
if spec.builtin or true then
|
||||
builtins_fetchTarball { name = name'; inherit (spec) url sha256; }
|
||||
else
|
||||
pkgs.fetchzip { name = name'; inherit (spec) url sha256; };
|
||||
|
||||
fetch_git = name: spec:
|
||||
let
|
||||
ref =
|
||||
if spec ? ref then spec.ref else
|
||||
if spec ? branch then "refs/heads/${spec.branch}" else
|
||||
if spec ? tag then "refs/tags/${spec.tag}" else
|
||||
abort "In git source '${name}': Please specify `ref`, `tag` or `branch`!";
|
||||
in
|
||||
builtins.fetchGit { url = spec.repo; inherit (spec) rev; inherit ref; };
|
||||
|
||||
fetch_local = spec: spec.path;
|
||||
|
||||
fetch_builtin-tarball = name: throw
|
||||
''[${name}] The niv type "builtin-tarball" is deprecated. You should instead use `builtin = true`.
|
||||
$ niv modify ${name} -a type=tarball -a builtin=true'';
|
||||
|
||||
fetch_builtin-url = name: throw
|
||||
''[${name}] The niv type "builtin-url" will soon be deprecated. You should instead use `builtin = true`.
|
||||
$ niv modify ${name} -a type=file -a builtin=true'';
|
||||
|
||||
#
|
||||
# Various helpers
|
||||
#
|
||||
|
||||
# https://github.com/NixOS/nixpkgs/pull/83241/files#diff-c6f540a4f3bfa4b0e8b6bafd4cd54e8bR695
|
||||
sanitizeName = name:
|
||||
(
|
||||
concatMapStrings (s: if builtins.isList s then "-" else s)
|
||||
(
|
||||
builtins.split "[^[:alnum:]+._?=-]+"
|
||||
((x: builtins.elemAt (builtins.match "\\.*(.*)" x) 0) name)
|
||||
)
|
||||
);
|
||||
|
||||
# The set of packages used when specs are fetched using non-builtins.
|
||||
mkPkgs = sources: system:
|
||||
let
|
||||
sourcesNixpkgs =
|
||||
import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; }) { inherit system; };
|
||||
hasNixpkgsPath = builtins.any (x: x.prefix == "nixpkgs") builtins.nixPath;
|
||||
hasThisAsNixpkgsPath = <nixpkgs> == ./.;
|
||||
in
|
||||
if builtins.hasAttr "nixpkgs" sources
|
||||
then sourcesNixpkgs
|
||||
else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then
|
||||
import <nixpkgs> {}
|
||||
else
|
||||
abort
|
||||
''
|
||||
Please specify either <nixpkgs> (through -I or NIX_PATH=nixpkgs=...) or
|
||||
add a package called "nixpkgs" to your sources.json.
|
||||
'';
|
||||
|
||||
# The actual fetching function.
|
||||
fetch = pkgs: name: spec:
|
||||
|
||||
if ! builtins.hasAttr "type" spec then
|
||||
abort "ERROR: niv spec ${name} does not have a 'type' attribute"
|
||||
else if spec.type == "file" then fetch_file pkgs name spec
|
||||
else if spec.type == "tarball" then fetch_tarball pkgs name spec
|
||||
else if spec.type == "git" then fetch_git name spec
|
||||
else if spec.type == "local" then fetch_local spec
|
||||
else if spec.type == "builtin-tarball" then fetch_builtin-tarball name
|
||||
else if spec.type == "builtin-url" then fetch_builtin-url name
|
||||
else
|
||||
abort "ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}";
|
||||
|
||||
# If the environment variable NIV_OVERRIDE_${name} is set, then use
|
||||
# the path directly as opposed to the fetched source.
|
||||
replace = name: drv:
|
||||
let
|
||||
saneName = stringAsChars (c: if isNull (builtins.match "[a-zA-Z0-9]" c) then "_" else c) name;
|
||||
ersatz = builtins.getEnv "NIV_OVERRIDE_${saneName}";
|
||||
in
|
||||
if ersatz == "" then drv else
|
||||
# this turns the string into an actual Nix path (for both absolute and
|
||||
# relative paths)
|
||||
if builtins.substring 0 1 ersatz == "/" then /. + ersatz else /. + builtins.getEnv "PWD" + "/${ersatz}";
|
||||
|
||||
# Ports of functions for older nix versions
|
||||
|
||||
# a Nix version of mapAttrs if the built-in doesn't exist
|
||||
mapAttrs = builtins.mapAttrs or (
|
||||
f: set: with builtins;
|
||||
listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set))
|
||||
);
|
||||
|
||||
# https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/lists.nix#L295
|
||||
range = first: last: if first > last then [] else builtins.genList (n: first + n) (last - first + 1);
|
||||
|
||||
# https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L257
|
||||
stringToCharacters = s: map (p: builtins.substring p 1 s) (range 0 (builtins.stringLength s - 1));
|
||||
|
||||
# https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L269
|
||||
stringAsChars = f: s: concatStrings (map f (stringToCharacters s));
|
||||
concatMapStrings = f: list: concatStrings (map f list);
|
||||
concatStrings = builtins.concatStringsSep "";
|
||||
|
||||
# https://github.com/NixOS/nixpkgs/blob/8a9f58a375c401b96da862d969f66429def1d118/lib/attrsets.nix#L331
|
||||
optionalAttrs = cond: as: if cond then as else {};
|
||||
|
||||
# fetchTarball version that is compatible between all the versions of Nix
|
||||
builtins_fetchTarball = { url, name ? null, sha256 }@attrs:
|
||||
let
|
||||
inherit (builtins) lessThan nixVersion fetchTarball;
|
||||
in
|
||||
if lessThan nixVersion "1.12" then
|
||||
fetchTarball ({ inherit url; } // (optionalAttrs (!isNull name) { inherit name; }))
|
||||
else
|
||||
fetchTarball attrs;
|
||||
|
||||
# fetchurl version that is compatible between all the versions of Nix
|
||||
builtins_fetchurl = { url, name ? null, sha256 }@attrs:
|
||||
let
|
||||
inherit (builtins) lessThan nixVersion fetchurl;
|
||||
in
|
||||
if lessThan nixVersion "1.12" then
|
||||
fetchurl ({ inherit url; } // (optionalAttrs (!isNull name) { inherit name; }))
|
||||
else
|
||||
fetchurl attrs;
|
||||
|
||||
# Create the final "sources" from the config
|
||||
mkSources = config:
|
||||
mapAttrs (
|
||||
name: spec:
|
||||
if builtins.hasAttr "outPath" spec
|
||||
then abort
|
||||
"The values in sources.json should not have an 'outPath' attribute"
|
||||
else
|
||||
spec // { outPath = replace name (fetch config.pkgs name spec); }
|
||||
) config.sources;
|
||||
|
||||
# The "config" used by the fetchers
|
||||
mkConfig =
|
||||
{ sourcesFile ? if builtins.pathExists ./sources.json then ./sources.json else null
|
||||
, sources ? if isNull sourcesFile then {} else builtins.fromJSON (builtins.readFile sourcesFile)
|
||||
, system ? builtins.currentSystem
|
||||
, pkgs ? mkPkgs sources system
|
||||
}: rec {
|
||||
# The sources, i.e. the attribute set of spec name to spec
|
||||
inherit sources;
|
||||
|
||||
# The "pkgs" (evaluated nixpkgs) to use for e.g. non-builtin fetchers
|
||||
inherit pkgs;
|
||||
};
|
||||
|
||||
in
|
||||
mkSources (mkConfig {}) // { __functor = _: settings: mkSources (mkConfig settings); }
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eoux pipefail
|
||||
|
||||
cargo deny --version
|
||||
cargo deny check advisories
|
||||
cargo deny check licenses
|
||||
cargo deny check bans
|
||||
cargo deny check sources
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eoux pipefail
|
||||
|
||||
cargo build --tests --workspace
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eoux pipefail
|
||||
|
||||
RUSTDOCFLAGS="-D rustdoc::broken-intra-doc-links -D warnings" \
|
||||
cargo doc --no-deps --workspace --document-private-items
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eoux pipefail
|
||||
|
||||
cargo +nightly fmt -- --check
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eoux pipefail
|
||||
|
||||
# Force clippy to consider all local sources
|
||||
# https://github.com/rust-lang/rust-clippy/issues/4612
|
||||
find . -name "*.rs" -not -path "./target/*" -exec touch "{}" +
|
||||
cargo clippy --all-targets -- -D warnings
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
if brew ls --versions gnu-tar > /dev/null; then
|
||||
echo "gnu-tar is already installed"
|
||||
else
|
||||
brew install gnu-tar
|
||||
fi
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eou pipefail
|
||||
|
||||
./scripts/ci/fmt
|
||||
./scripts/ci/lint
|
||||
./scripts/ci/advisory
|
||||
./scripts/ci/build
|
||||
./scripts/ci/test
|
||||
./scripts/ci/docs
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eoux pipefail
|
||||
|
||||
RUST_LOG=error ssh-agent cargo nextest run \
|
||||
--status-level all \
|
||||
--failure-output immediate-final \
|
||||
--no-fail-fast \
|
||||
--retries 2
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eoux pipefail
|
||||
|
||||
cargo nextest run --no-fail-fast tests properties
|
||||
@@ -0,0 +1,22 @@
|
||||
{ sources ? import ./nix/sources.nix
|
||||
, pkgs ? import sources.nixpkgs {
|
||||
overlays = [ (import sources.rust-overlay) ];
|
||||
}
|
||||
}:
|
||||
let
|
||||
stable = pkgs.rust-bin.stable.latest.default;
|
||||
rust-overlay = stable.override {
|
||||
extensions = [ "rust-src" "rust-analysis" ];
|
||||
};
|
||||
devault = (pkgs.callPackage ./default.nix {});
|
||||
in
|
||||
with pkgs;
|
||||
mkShell {
|
||||
name = "development";
|
||||
buildInputs = devault.buildInputs ++ [
|
||||
clang
|
||||
lld
|
||||
|
||||
ripgrep
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "radicle-std-ext"
|
||||
version = "0.1.0"
|
||||
authors = ["The Radicle Team <dev@radicle.xyz>"]
|
||||
edition = "2018"
|
||||
license = "GPL-3.0-or-later"
|
||||
description = "Monkey patches of std types"
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
test = false
|
||||
|
||||
[features]
|
||||
default = []
|
||||
nightly = []
|
||||
@@ -0,0 +1,19 @@
|
||||
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
#![cfg_attr(feature = "nightly", feature(try_trait_v2))]
|
||||
|
||||
pub mod ops;
|
||||
pub mod result;
|
||||
|
||||
pub type Void = std::convert::Infallible;
|
||||
|
||||
pub mod prelude {
|
||||
use super::*;
|
||||
|
||||
pub use super::Void;
|
||||
pub use ops::{FromResidual, Try};
|
||||
pub use result::ResultExt;
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
// 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.
|
||||
|
||||
//! Provides Try-trait for stable rust
|
||||
//!
|
||||
//! Probably doesn't work with `?`-desugaring. If the `nightly` feature is
|
||||
//! enabled for this crate, the `std` version is enabled.
|
||||
|
||||
#[cfg(not(feature = "nightly"))]
|
||||
pub use stable::{FromResidual, Try};
|
||||
#[cfg(feature = "nightly")]
|
||||
pub use std::ops::{FromResidual, Try};
|
||||
|
||||
mod stable {
|
||||
use std::{convert, ops::ControlFlow, task::Poll};
|
||||
|
||||
pub trait Try: FromResidual {
|
||||
type Output;
|
||||
type Residual;
|
||||
|
||||
fn from_output(output: Self::Output) -> Self;
|
||||
fn branch(self) -> ControlFlow<Self::Residual, Self::Output>;
|
||||
}
|
||||
|
||||
pub trait FromResidual<R = <Self as Try>::Residual> {
|
||||
fn from_residual(residual: R) -> Self;
|
||||
}
|
||||
|
||||
impl<B, C> Try for ControlFlow<B, C> {
|
||||
type Output = C;
|
||||
type Residual = ControlFlow<B, convert::Infallible>;
|
||||
|
||||
#[inline]
|
||||
fn from_output(output: Self::Output) -> Self {
|
||||
ControlFlow::Continue(output)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn branch(self) -> ControlFlow<Self::Residual, Self::Output> {
|
||||
match self {
|
||||
ControlFlow::Continue(c) => ControlFlow::Continue(c),
|
||||
ControlFlow::Break(b) => ControlFlow::Break(ControlFlow::Break(b)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B, C> FromResidual for ControlFlow<B, C> {
|
||||
#[inline]
|
||||
fn from_residual(residual: ControlFlow<B, convert::Infallible>) -> Self {
|
||||
match residual {
|
||||
ControlFlow::Break(b) => ControlFlow::Break(b),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Try for Option<T> {
|
||||
type Output = T;
|
||||
type Residual = Option<convert::Infallible>;
|
||||
|
||||
#[inline]
|
||||
fn from_output(output: Self::Output) -> Self {
|
||||
Some(output)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn branch(self) -> ControlFlow<Self::Residual, Self::Output> {
|
||||
match self {
|
||||
Some(v) => ControlFlow::Continue(v),
|
||||
None => ControlFlow::Break(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> FromResidual for Option<T> {
|
||||
#[inline]
|
||||
fn from_residual(residual: Option<convert::Infallible>) -> Self {
|
||||
match residual {
|
||||
None => None,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> Try for Result<T, E> {
|
||||
type Output = T;
|
||||
type Residual = Result<convert::Infallible, E>;
|
||||
|
||||
#[inline]
|
||||
fn from_output(output: Self::Output) -> Self {
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn branch(self) -> ControlFlow<Self::Residual, Self::Output> {
|
||||
match self {
|
||||
Ok(v) => ControlFlow::Continue(v),
|
||||
Err(e) => ControlFlow::Break(Err(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E, F: From<E>> FromResidual<Result<convert::Infallible, E>> for Result<T, F> {
|
||||
#[inline]
|
||||
fn from_residual(residual: Result<convert::Infallible, E>) -> Self {
|
||||
match residual {
|
||||
Err(e) => Err(From::from(e)),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> Try for Poll<Option<Result<T, E>>> {
|
||||
type Output = Poll<Option<T>>;
|
||||
type Residual = Result<convert::Infallible, E>;
|
||||
|
||||
#[inline]
|
||||
fn from_output(c: Self::Output) -> Self {
|
||||
c.map(|x| x.map(Ok))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn branch(self) -> ControlFlow<Self::Residual, Self::Output> {
|
||||
match self {
|
||||
Poll::Ready(Some(Ok(x))) => ControlFlow::Continue(Poll::Ready(Some(x))),
|
||||
Poll::Ready(Some(Err(e))) => ControlFlow::Break(Err(e)),
|
||||
Poll::Ready(None) => ControlFlow::Continue(Poll::Ready(None)),
|
||||
Poll::Pending => ControlFlow::Continue(Poll::Pending),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E, F: From<E>> FromResidual<Result<convert::Infallible, E>> for Poll<Option<Result<T, F>>> {
|
||||
#[inline]
|
||||
fn from_residual(x: Result<convert::Infallible, E>) -> Self {
|
||||
match x {
|
||||
Err(e) => Poll::Ready(Some(Err(From::from(e)))),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E> Try for Poll<Result<T, E>> {
|
||||
type Output = Poll<T>;
|
||||
type Residual = Result<convert::Infallible, E>;
|
||||
|
||||
#[inline]
|
||||
fn from_output(c: Self::Output) -> Self {
|
||||
c.map(Ok)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn branch(self) -> ControlFlow<Self::Residual, Self::Output> {
|
||||
match self {
|
||||
Poll::Ready(Ok(x)) => ControlFlow::Continue(Poll::Ready(x)),
|
||||
Poll::Ready(Err(e)) => ControlFlow::Break(Err(e)),
|
||||
Poll::Pending => ControlFlow::Continue(Poll::Pending),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, E, F: From<E>> FromResidual<Result<convert::Infallible, E>> for Poll<Result<T, F>> {
|
||||
#[inline]
|
||||
fn from_residual(x: Result<convert::Infallible, E>) -> Self {
|
||||
match x {
|
||||
Err(e) => Poll::Ready(Err(From::from(e))),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
pub trait ResultExt<T, E> {
|
||||
/// Calls `f` if the result is [`Err`], **and** the predicate `pred` on the
|
||||
/// error value returns true. Otherwise returns the [`Ok`] value of
|
||||
/// `self`. Note that `f` may change the error type, so as long as the
|
||||
/// target type can be converted from the original one.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use std::io;
|
||||
/// use radicle_std_ext::result::ResultExt as _;
|
||||
///
|
||||
/// let res = Err(io::Error::new(io::ErrorKind::Other, "crashbug"))
|
||||
/// .or_matches::<io::Error, _, _>(|e| matches!(e.kind(), io::ErrorKind::Other), || Ok(()))
|
||||
/// .unwrap();
|
||||
///
|
||||
/// assert_eq!((), res)
|
||||
/// ```
|
||||
fn or_matches<E2, P, F>(self, pred: P, f: F) -> Result<T, E2>
|
||||
where
|
||||
E2: From<E>,
|
||||
P: FnOnce(&E) -> bool,
|
||||
F: FnOnce() -> Result<T, E2>;
|
||||
}
|
||||
|
||||
impl<T, E> ResultExt<T, E> for Result<T, E> {
|
||||
fn or_matches<E2, P, F>(self, pred: P, f: F) -> Result<T, E2>
|
||||
where
|
||||
E2: From<E>,
|
||||
P: FnOnce(&E) -> bool,
|
||||
F: FnOnce() -> Result<T, E2>,
|
||||
{
|
||||
self.or_else(|e| if pred(&e) { f() } else { Err(e.into()) })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
proptest-regressions
|
||||
@@ -0,0 +1,31 @@
|
||||
[package]
|
||||
name = "tests"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
test = true
|
||||
doc = false
|
||||
|
||||
[dev-dependencies.git-ext-test]
|
||||
path = "../git-ext/t"
|
||||
features = ["test"]
|
||||
|
||||
[dev-dependencies.git-ref-format-test]
|
||||
path = "../git-ref-format/t"
|
||||
features = ["test"]
|
||||
|
||||
[dev-dependencies.git-trailers-test]
|
||||
path = "../git-trailers/t"
|
||||
features = ["test"]
|
||||
|
||||
# [dev-dependencies.gitd-lib-test]
|
||||
# path = "../cli/gitd-lib/t"
|
||||
|
||||
[dev-dependencies.link-git-test]
|
||||
path = "../link-git/t"
|
||||
features = ["test"]
|
||||
@@ -0,0 +1,52 @@
|
||||
# Test root crate
|
||||
|
||||
Organisation of the test code for the `radicle-git` project crates deviates
|
||||
from the `cargo` conventions in order to work around some of the limitations of
|
||||
the current `cargo` / Rust testing infrastructure.
|
||||
|
||||
Here is how:
|
||||
|
||||
- Project crates are set to `test = false` by default, ie. no `#[cfg(test)]` /
|
||||
`#[test]` annotated tests are run.
|
||||
|
||||
- Instead, project crates are tested via an accompanying `<crate>-test` crate
|
||||
located in a `t/` directory relative to the crate root.
|
||||
|
||||
- This is similar to what `cargo` calls "integration tests", in that only the
|
||||
public API of the crate under test is available. Test crates are, however,
|
||||
meant to contain all kinds of tests.
|
||||
|
||||
- Conventionally, tests are split into module hierarchies, mainly to support
|
||||
convenient filtering.
|
||||
|
||||
`tests`
|
||||
: Unit tests. Example-based, preferably-pure.
|
||||
|
||||
`properties`
|
||||
: Property tests. Randomized, preferably-pure.
|
||||
|
||||
`integration`
|
||||
: Stateful tests, scenario-based. May have all kinds of effects.
|
||||
|
||||
- Additionally, test crates may export helpers (such as mocks or fixtures) and
|
||||
`proptest` generators through `gen` and `helpers` modules. Test crates may
|
||||
depend on each other to make those types / functions available, possibly
|
||||
mirroring the dependency relationships of their respective "parent" crates.
|
||||
|
||||
- `gen` and `helpers` modules are guarded behind a feature flag "test", ie.
|
||||
|
||||
#[cfg(any(test, feature = "test"))]
|
||||
|
||||
- Additional helpers can be found in the `test-helpers` (preferably-pure) and
|
||||
`it-helpers` (stateful) crates.
|
||||
|
||||
- This crate (`test`) does not contain any code, but depends on all other test
|
||||
crates in the workspace (which are themselves not proper workspace members).
|
||||
This prevents unnecessary compilation of test crates if no test target is
|
||||
being built, but still makes each test crate available to be executed
|
||||
individually via the `-p` flag, eg.
|
||||
|
||||
cargo test -p link-replication-test
|
||||
|
||||
- It is recommended to use [`cargo-nextest`](https://nexte.st) instead of `cargo
|
||||
test` for maximising parallelism.
|
||||
@@ -0,0 +1,48 @@
|
||||
[package]
|
||||
name = "it-helpers"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "GPL-3.0-or-later"
|
||||
publish = false
|
||||
|
||||
description = "Integration test helpers"
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
test = false
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
futures = "0.3"
|
||||
once_cell = "1.10"
|
||||
tempfile = "3.3"
|
||||
tokio = "1.13"
|
||||
tracing = "0.1"
|
||||
either = "1.6"
|
||||
|
||||
[dependencies.git2]
|
||||
version = "0.13.24"
|
||||
default-features = false
|
||||
features = ["vendored-libgit2"]
|
||||
|
||||
#
|
||||
# workspace dependencies
|
||||
#
|
||||
|
||||
[dependencies.git-ref-format]
|
||||
path = "../../git-ref-format"
|
||||
|
||||
[dependencies.librad]
|
||||
path = "../../librad"
|
||||
|
||||
[dependencies.link-async]
|
||||
path = "../../link-async"
|
||||
|
||||
[dependencies.lnk-clib]
|
||||
path = "../../cli/lnk-clib"
|
||||
|
||||
[dependencies.radicle-git-ext]
|
||||
path = "../../git-ext"
|
||||
|
||||
[dependencies.test-helpers]
|
||||
path = "../test-helpers"
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
|
||||
//
|
||||
// This file is part of radicle-link, distributed under the GPLv3 with Radicle
|
||||
// Linking Exception. For full terms see the included LICENSE file.
|
||||
|
||||
use git_ref_format::Qualified;
|
||||
|
||||
#[tracing::instrument(skip(repo))]
|
||||
pub fn create_commit(
|
||||
repo: &git2::Repository,
|
||||
on_branch: Qualified,
|
||||
) -> Result<git2::Oid, git2::Error> {
|
||||
let empty_tree = {
|
||||
let mut index = repo.index()?;
|
||||
let oid = index.write_tree()?;
|
||||
repo.find_tree(oid).unwrap()
|
||||
};
|
||||
let author = git2::Signature::now("The Animal", "animal@muppets.com").unwrap();
|
||||
repo.commit(
|
||||
Some(on_branch.as_str()),
|
||||
&author,
|
||||
&author,
|
||||
"Initial commit",
|
||||
&empty_tree,
|
||||
&[],
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user