mirror of
https://seed.radicle.dev/rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt.git
synced 2026-11-02 18:00:29 +08:00
Merge remote-tracking branch 'origin/surf/git-ref-format'
* origin/surf/git-ref-format: radicle-surf: rework to use git-ref-format radicle-surf: move vcs::git to git radicle-surf: remove RefScope radicle-surf: remove deny pragma Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com>
This commit is contained in:
@@ -4,7 +4,7 @@ description = "A code surfing library for VCS file systems"
|
||||
readme = "README.md"
|
||||
version = "0.8.0"
|
||||
authors = ["The Radicle Team <dev@radicle.xyz>"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
homepage = "https://github.com/radicle-dev/radicle-surf"
|
||||
repository = "https://github.com/radicle-dev/radicle-surf"
|
||||
license = "GPL-3.0-or-later"
|
||||
@@ -44,6 +44,7 @@ features = ["vendored-libgit2"]
|
||||
[dependencies.git-ref-format]
|
||||
version = "0.1.0"
|
||||
path = "../git-ref-format"
|
||||
features = ["macro", "serde"]
|
||||
|
||||
[dependencies.radicle-git-ext]
|
||||
version = "0.2.0"
|
||||
@@ -56,9 +57,6 @@ pretty_assertions = "1.3.0"
|
||||
proptest = "0.9"
|
||||
serde_json = "1"
|
||||
|
||||
[dev-dependencies.test-helpers]
|
||||
path = "../test/test-helpers"
|
||||
|
||||
[build-dependencies]
|
||||
anyhow = "1.0"
|
||||
flate2 = "1"
|
||||
|
||||
@@ -16,15 +16,16 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||
use git_ref_format::refname;
|
||||
use radicle_surf::{
|
||||
file_system::{unsound, Path},
|
||||
vcs::git::{Branch, Repository},
|
||||
git::{Branch, Repository},
|
||||
};
|
||||
|
||||
fn last_commit_comparison(c: &mut Criterion) {
|
||||
let repo = Repository::open("./data/git-platinum")
|
||||
.expect("Could not retrieve ./data/git-platinum as git repository");
|
||||
let rev = Branch::local("master");
|
||||
let rev = Branch::local(refname!("master"));
|
||||
|
||||
let mut group = c.benchmark_group("Last Commit");
|
||||
for path in [
|
||||
|
||||
@@ -20,7 +20,7 @@ extern crate radicle_surf;
|
||||
use std::{env::Args, str::FromStr, time::Instant};
|
||||
|
||||
use radicle_git_ext::Oid;
|
||||
use radicle_surf::{diff::Diff, vcs::git};
|
||||
use radicle_surf::{diff::Diff, git};
|
||||
|
||||
fn main() {
|
||||
let options = get_options_or_exit();
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
//! Represents a commit.
|
||||
|
||||
use git_ref_format::RefString;
|
||||
#[cfg(feature = "serialize")]
|
||||
use serde::{
|
||||
ser::{SerializeStruct as _, Serializer},
|
||||
@@ -26,10 +27,9 @@ use serde::{
|
||||
use crate::{
|
||||
diff,
|
||||
file_system,
|
||||
git::Glob,
|
||||
git::{self, Glob, RepositoryRef},
|
||||
person::Person,
|
||||
revision::Revision,
|
||||
vcs::git::{self, BranchName, RepositoryRef},
|
||||
};
|
||||
|
||||
use radicle_git_ext::Oid;
|
||||
@@ -55,7 +55,7 @@ pub struct Commit {
|
||||
/// The changeset introduced by this commit.
|
||||
pub diff: diff::Diff,
|
||||
/// The list of branches this commit belongs to.
|
||||
pub branches: Vec<BranchName>,
|
||||
pub branches: Vec<RefString>,
|
||||
}
|
||||
|
||||
/// Representation of a code commit.
|
||||
@@ -196,7 +196,7 @@ pub fn commit<R: git::Revision>(repo: &RepositoryRef, rev: R) -> Result<Commit,
|
||||
let branches = repo
|
||||
.revision_branches(&sha1, &Glob::heads("*")?.and_remotes("*")?)?
|
||||
.into_iter()
|
||||
.map(|b| b.name)
|
||||
.map(|b| b.refname().into())
|
||||
.collect();
|
||||
|
||||
Ok(Commit {
|
||||
@@ -227,13 +227,7 @@ pub fn header(repo: &RepositoryRef, sha1: Oid) -> Result<Header, Error> {
|
||||
///
|
||||
/// Will return [`Error`] if the project doesn't exist or the surf interaction
|
||||
/// fails.
|
||||
pub fn commits<P>(
|
||||
repo: &RepositoryRef,
|
||||
maybe_revision: Option<Revision<P>>,
|
||||
) -> Result<Commits, Error>
|
||||
where
|
||||
P: ToString,
|
||||
{
|
||||
pub fn commits(repo: &RepositoryRef, maybe_revision: Option<Revision>) -> Result<Commits, Error> {
|
||||
let rev = match maybe_revision {
|
||||
Some(revision) => revision,
|
||||
None => Revision::Sha {
|
||||
|
||||
@@ -24,7 +24,7 @@ use serde::{ser, Serialize, Serializer};
|
||||
|
||||
use crate::{
|
||||
file_system::{Directory, Path},
|
||||
vcs::git::{Error, RepositoryRef},
|
||||
git::{Error, RepositoryRef},
|
||||
};
|
||||
|
||||
pub mod git;
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
use crate::{
|
||||
file_system::{error::LabelError, path::*, Error},
|
||||
vcs::git::{self, RepositoryRef, Revision},
|
||||
git::{self, RepositoryRef, Revision},
|
||||
};
|
||||
use git2::Blob;
|
||||
use radicle_git_ext::Oid;
|
||||
|
||||
@@ -67,6 +67,7 @@ use std::str::FromStr;
|
||||
|
||||
// Re-export git2 as sub-module
|
||||
pub use git2::{self, Error as Git2Error, Time};
|
||||
use git_ref_format::{name::Components, Component, Qualified, RefString};
|
||||
pub use radicle_git_ext::Oid;
|
||||
|
||||
mod repo;
|
||||
@@ -81,15 +82,13 @@ pub use history::History;
|
||||
pub mod error;
|
||||
pub use error::Error;
|
||||
|
||||
pub mod ext;
|
||||
|
||||
/// Provides the data for talking about branches.
|
||||
pub mod branch;
|
||||
pub use branch::{Branch, BranchName, BranchType};
|
||||
pub use branch::{Branch, Local, Remote};
|
||||
|
||||
/// Provides the data for talking about tags.
|
||||
pub mod tag;
|
||||
pub use tag::{Tag, TagName};
|
||||
pub use tag::Tag;
|
||||
|
||||
/// Provides the data for talking about commits.
|
||||
pub mod commit;
|
||||
@@ -115,41 +114,36 @@ impl From<git2::Buf> for Signature {
|
||||
}
|
||||
}
|
||||
|
||||
/// Determines whether to look for local or remote references or both.
|
||||
pub enum RefScope {
|
||||
/// List all branches by default.
|
||||
All,
|
||||
/// List only local branches.
|
||||
Local,
|
||||
/// List only remote branches.
|
||||
Remote {
|
||||
/// Name of the remote. If `None`, then get the reference from all
|
||||
/// remotes.
|
||||
name: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
/// Turn an `Option<P>` into a [`RefScope`]. If the `P` is present then
|
||||
/// this is set as the remote of the `RefScope`. Otherwise, it's local
|
||||
/// branch.
|
||||
impl<P> From<Option<P>> for RefScope
|
||||
where
|
||||
P: ToString,
|
||||
{
|
||||
fn from(peer_id: Option<P>) -> Self {
|
||||
peer_id.map_or(RefScope::Local, |peer_id| RefScope::Remote {
|
||||
// We qualify the remotes as the PeerId + heads, otherwise we would grab the tags too.
|
||||
name: Some(format!("{}/heads", peer_id.to_string())),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Supports various ways to specify a revision used in Git.
|
||||
pub trait Revision {
|
||||
/// Returns the object id of this revision in `repo`.
|
||||
fn object_id(&self, repo: &RepositoryRef) -> Result<Oid, Error>;
|
||||
}
|
||||
|
||||
impl Revision for RefString {
|
||||
fn object_id(&self, repo: &RepositoryRef) -> Result<Oid, Error> {
|
||||
repo.refname_to_oid(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl Revision for &RefString {
|
||||
fn object_id(&self, repo: &RepositoryRef) -> Result<Oid, Error> {
|
||||
repo.refname_to_oid(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl Revision for Qualified<'_> {
|
||||
fn object_id(&self, repo: &RepositoryRef) -> Result<Oid, Error> {
|
||||
repo.refname_to_oid(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl Revision for &Qualified<'_> {
|
||||
fn object_id(&self, repo: &RepositoryRef) -> Result<Oid, Error> {
|
||||
repo.refname_to_oid(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl Revision for Oid {
|
||||
fn object_id(&self, _repo: &RepositoryRef) -> Result<Oid, Error> {
|
||||
Ok(*self)
|
||||
@@ -176,9 +170,6 @@ impl Revision for &Tag {
|
||||
}
|
||||
}
|
||||
|
||||
impl Revision for &TagName {
|
||||
fn object_id(&self, repo: &RepositoryRef) -> Result<Oid, Error> {
|
||||
let refname = repo.namespaced_refname(&self.refname())?;
|
||||
Ok(repo.repo_ref.refname_to_id(&refname).map(Oid::from)?)
|
||||
}
|
||||
pub(crate) fn refstr_join<'a>(c: Component<'a>, cs: Components<'a>) -> RefString {
|
||||
std::iter::once(c).chain(cs).collect::<RefString>()
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
use std::{
|
||||
convert::TryFrom,
|
||||
str::{self, FromStr},
|
||||
};
|
||||
|
||||
use crate::git::refstr_join;
|
||||
use git_ref_format::{component, lit, Component, Qualified, RefStr, RefString};
|
||||
|
||||
/// A `Branch` represents any git branch. This can either be a reference
|
||||
/// that is under the `refs/heads` or `refs/remotes` namespace.
|
||||
///
|
||||
/// Note that if a `Branch` is created from a [`git2::Reference`] then
|
||||
/// any `refs/namespaces` will be stripped.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub enum Branch {
|
||||
Local(Local),
|
||||
Remote(Remote),
|
||||
}
|
||||
|
||||
impl Branch {
|
||||
/// Construct a [`Local`] branch.
|
||||
pub fn local<R>(name: R) -> Self
|
||||
where
|
||||
R: AsRef<RefStr>,
|
||||
{
|
||||
Self::Local(Local::new(name))
|
||||
}
|
||||
|
||||
/// Construct a [`Remote`] branch.
|
||||
/// The `remote` is the remote name of the reference name while
|
||||
/// the `name` is the suffix, i.e. `refs/remotes/<remote>/<name>`.
|
||||
pub fn remote<R>(remote: Component<'_>, name: R) -> Self
|
||||
where
|
||||
R: AsRef<RefStr>,
|
||||
{
|
||||
Self::Remote(Remote::new(remote, name))
|
||||
}
|
||||
|
||||
pub fn refname(&self) -> Qualified {
|
||||
match self {
|
||||
Branch::Local(local) => local.refname(),
|
||||
Branch::Remote(remote) => remote.refname(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&git2::Reference<'_>> for Branch {
|
||||
type Error = error::Branch;
|
||||
|
||||
fn try_from(reference: &git2::Reference<'_>) -> Result<Self, Self::Error> {
|
||||
let name = str::from_utf8(reference.name_bytes())?;
|
||||
Self::from_str(name)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Branch {
|
||||
type Error = error::Branch;
|
||||
|
||||
fn try_from(name: &str) -> Result<Self, Self::Error> {
|
||||
Self::from_str(name)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Branch {
|
||||
type Err = error::Branch;
|
||||
|
||||
fn from_str(name: &str) -> Result<Self, Self::Err> {
|
||||
let name = RefStr::try_from_str(name)?;
|
||||
let name = match name.to_namespaced() {
|
||||
None => name
|
||||
.qualified()
|
||||
.ok_or_else(|| error::Branch::NotQualified(name.to_string()))?,
|
||||
Some(name) => name.strip_namespace_recursive(),
|
||||
};
|
||||
|
||||
let (_ref, category, c, cs) = name.non_empty_components();
|
||||
|
||||
if category == component::HEADS {
|
||||
Ok(Self::Local(Local::new(refstr_join(c, cs))))
|
||||
} else if category == component::REMOTES {
|
||||
Ok(Self::Remote(Remote::new(c, cs.collect::<RefString>())))
|
||||
} else {
|
||||
Err(error::Branch::NotBranch(name.into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A `Local` represents a local branch, i.e. it is a reference under
|
||||
/// `refs/heads`.
|
||||
///
|
||||
/// Note that if a `Local` is created from a [`git2::Reference`] then
|
||||
/// any `refs/namespaces` will be stripped.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Local {
|
||||
name: RefString,
|
||||
}
|
||||
|
||||
impl Local {
|
||||
/// Construct a new `Local` with the given `name`.
|
||||
///
|
||||
/// If the name is qualified with `refs/heads`, this will be
|
||||
/// shortened to the suffix. To get the `Qualified` name again,
|
||||
/// use [`Local::refname`].
|
||||
pub fn new<R>(name: R) -> Self
|
||||
where
|
||||
R: AsRef<RefStr>,
|
||||
{
|
||||
match name.as_ref().qualified() {
|
||||
None => Self {
|
||||
name: name.as_ref().to_ref_string(),
|
||||
},
|
||||
Some(qualified) => {
|
||||
let (_refs, heads, c, cs) = qualified.non_empty_components();
|
||||
if heads == component::HEADS {
|
||||
Self {
|
||||
name: refstr_join(c, cs),
|
||||
}
|
||||
} else {
|
||||
Self {
|
||||
name: name.as_ref().to_ref_string(),
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the fully qualified `Local` refname,
|
||||
/// e.g. `refs/heads/fix/ref-format`.
|
||||
pub fn refname(&self) -> Qualified {
|
||||
lit::refs_heads(&self.name).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&git2::Reference<'_>> for Local {
|
||||
type Error = error::Local;
|
||||
|
||||
fn try_from(reference: &git2::Reference) -> Result<Self, Self::Error> {
|
||||
let name = str::from_utf8(reference.name_bytes())?;
|
||||
Self::from_str(name)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Local {
|
||||
type Error = error::Local;
|
||||
|
||||
fn try_from(name: &str) -> Result<Self, Self::Error> {
|
||||
Self::from_str(name)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Local {
|
||||
type Err = error::Local;
|
||||
|
||||
fn from_str(name: &str) -> Result<Self, Self::Err> {
|
||||
let name = RefStr::try_from_str(name)?;
|
||||
let name = match name.to_namespaced() {
|
||||
None => name
|
||||
.qualified()
|
||||
.ok_or_else(|| error::Local::NotQualified(name.to_string()))?,
|
||||
Some(name) => name.strip_namespace_recursive(),
|
||||
};
|
||||
|
||||
let (_ref, heads, c, cs) = name.non_empty_components();
|
||||
if heads == component::HEADS {
|
||||
Ok(Self::new(refstr_join(c, cs)))
|
||||
} else {
|
||||
Err(error::Local::NotHeads(name.into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A `Remote` represents a remote branch, i.e. it is a reference under
|
||||
/// `refs/remotes`.
|
||||
///
|
||||
/// Note that if a `Remote` is created from a [`git2::Reference`] then
|
||||
/// any `refs/namespaces` will be stripped.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
|
||||
pub struct Remote {
|
||||
remote: RefString,
|
||||
name: RefString,
|
||||
}
|
||||
|
||||
impl Remote {
|
||||
/// Construct a new `Remote` with the given `name` and `remote`.
|
||||
///
|
||||
/// ## Note
|
||||
/// `name` is expected to be in short form, i.e. not begin with
|
||||
/// `refs`.
|
||||
///
|
||||
/// If you are creating a `Remote` with a name that begins with
|
||||
/// `refs/remotes`, use [`Remote::from_refs_remotes`] instead.
|
||||
///
|
||||
/// To get the `Qualified` name, use [`Remote::refname`].
|
||||
pub fn new<R>(remote: Component, name: R) -> Self
|
||||
where
|
||||
R: AsRef<RefStr>,
|
||||
{
|
||||
Self {
|
||||
name: name.as_ref().to_ref_string(),
|
||||
remote: remote.to_ref_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the `name` from the form `refs/remotes/<remote>/<rest>`.
|
||||
///
|
||||
/// If the `name` is not of this form, then `None` is returned.
|
||||
pub fn from_refs_remotes<R>(name: R) -> Option<Self>
|
||||
where
|
||||
R: AsRef<RefStr>,
|
||||
{
|
||||
let qualified = name.as_ref().qualified()?;
|
||||
let (_refs, remotes, remote, cs) = qualified.non_empty_components();
|
||||
(remotes == component::REMOTES).then_some(Self {
|
||||
name: cs.collect(),
|
||||
remote: remote.to_ref_string(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Give back the fully qualified `Remote` refname,
|
||||
/// e.g. `refs/remotes/origin/fix/ref-format`.
|
||||
pub fn refname(&self) -> Qualified {
|
||||
lit::refs_remotes(self.remote.join(&self.name)).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&git2::Reference<'_>> for Remote {
|
||||
type Error = error::Remote;
|
||||
|
||||
fn try_from(reference: &git2::Reference) -> Result<Self, Self::Error> {
|
||||
let name = str::from_utf8(reference.name_bytes())?;
|
||||
Self::from_str(name)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Remote {
|
||||
type Error = error::Remote;
|
||||
|
||||
fn try_from(name: &str) -> Result<Self, Self::Error> {
|
||||
Self::from_str(name)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Remote {
|
||||
type Err = error::Remote;
|
||||
|
||||
fn from_str(name: &str) -> Result<Self, Self::Err> {
|
||||
let name = RefStr::try_from_str(name)?;
|
||||
let name = match name.to_namespaced() {
|
||||
None => name
|
||||
.qualified()
|
||||
.ok_or_else(|| error::Remote::NotQualified(name.to_string()))?,
|
||||
Some(name) => name.strip_namespace_recursive(),
|
||||
};
|
||||
|
||||
let (_ref, remotes, remote, cs) = name.non_empty_components();
|
||||
if remotes == component::REMOTES {
|
||||
Ok(Self::new(remote, cs.collect::<RefString>()))
|
||||
} else {
|
||||
Err(error::Remote::NotRemotes(name.into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod error {
|
||||
use git_ref_format::RefString;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Branch {
|
||||
#[error("the refname '{0}' did not begin with 'refs/heads' or 'refs/remotes'")]
|
||||
NotBranch(RefString),
|
||||
#[error("the refname '{0}' did not begin with 'refs/heads' or 'refs/remotes'")]
|
||||
NotQualified(String),
|
||||
#[error(transparent)]
|
||||
RefFormat(#[from] git_ref_format::Error),
|
||||
#[error(transparent)]
|
||||
Utf8(#[from] std::str::Utf8Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Local {
|
||||
#[error("the refname '{0}' did not begin with 'refs/heads'")]
|
||||
NotHeads(RefString),
|
||||
#[error("the refname '{0}' did not begin with 'refs/heads'")]
|
||||
NotQualified(String),
|
||||
#[error(transparent)]
|
||||
RefFormat(#[from] git_ref_format::Error),
|
||||
#[error(transparent)]
|
||||
Utf8(#[from] std::str::Utf8Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Remote {
|
||||
#[error("the refname '{0}' did not begin with 'refs/remotes'")]
|
||||
NotQualified(String),
|
||||
#[error("the refname '{0}' did not begin with 'refs/remotes'")]
|
||||
NotRemotes(RefString),
|
||||
#[error(transparent)]
|
||||
RefFormat(#[from] git_ref_format::Error),
|
||||
#[error(transparent)]
|
||||
Utf8(#[from] std::str::Utf8Error),
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,9 @@
|
||||
|
||||
use crate::{
|
||||
file_system::{self, directory},
|
||||
vcs::git::{error::Error, Branch, RepositoryRef, Tag, TagName},
|
||||
git::{error::Error, Branch, RepositoryRef, Tag},
|
||||
};
|
||||
use git_ref_format::{Qualified, RefString};
|
||||
use radicle_git_ext::Oid;
|
||||
use std::{convert::TryFrom, str};
|
||||
|
||||
@@ -216,7 +217,13 @@ impl ToCommit for &Tag {
|
||||
}
|
||||
}
|
||||
|
||||
impl ToCommit for &TagName {
|
||||
impl ToCommit for &RefString {
|
||||
fn to_commit(self, repo: &RepositoryRef) -> Result<Commit, Error> {
|
||||
repo.commit(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl ToCommit for &Qualified<'_> {
|
||||
fn to_commit(self, repo: &RepositoryRef) -> Result<Commit, Error> {
|
||||
repo.commit(self)
|
||||
}
|
||||
@@ -16,33 +16,20 @@
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! Collection of errors and helper instances that can occur when performing
|
||||
//! operations from [`crate::vcs::git`].
|
||||
//! operations from [`crate::git`].
|
||||
|
||||
use crate::{
|
||||
diff,
|
||||
file_system,
|
||||
vcs::git::{BranchName, Namespace, TagName},
|
||||
};
|
||||
use crate::{diff, file_system, git, git::Namespace};
|
||||
use std::str;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Enumeration of errors that can occur in operations from [`crate::vcs::git`].
|
||||
#[derive(Debug, PartialEq, Error)]
|
||||
/// Enumeration of errors that can occur in operations from [`crate::git`].
|
||||
#[derive(Debug, Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
/// The user tried to fetch a branch, but the name provided does not
|
||||
/// exist as a branch. This could mean that the branch does not exist
|
||||
/// or that a tag or commit was provided by accident.
|
||||
#[error("provided branch name does not exist: {0}")]
|
||||
NotBranch(BranchName),
|
||||
/// We tried to convert a name into its remote and branch name parts.
|
||||
#[error("could not parse '{0}' into a remote name and branch name")]
|
||||
ParseRemoteBranch(BranchName),
|
||||
/// The user tried to fetch a tag, but the name provided does not
|
||||
/// exist as a tag. This could mean that the tag does not exist
|
||||
/// or that a branch or commit was provided by accident.
|
||||
#[error("provided tag name does not exist: {0}")]
|
||||
NotTag(TagName),
|
||||
#[error(transparent)]
|
||||
BranchIter(#[from] git::repo::iter::error::Branch),
|
||||
#[error(transparent)]
|
||||
TagIter(#[from] git::repo::iter::error::Tag),
|
||||
/// A `revspec` was provided that could not be parsed into a branch, tag, or
|
||||
/// commit object.
|
||||
#[error("provided revspec '{rev}' could not be parsed into a git object")]
|
||||
@@ -15,7 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::vcs::git::Error;
|
||||
use crate::git::Error;
|
||||
use git_ref_format::refspec::PatternString;
|
||||
use std::{convert::TryFrom, marker::PhantomData, str};
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
use crate::{
|
||||
file_system,
|
||||
vcs::git::{commit::ToCommit, Commit, Error, RepositoryRef},
|
||||
git::{commit::ToCommit, Commit, Error, RepositoryRef},
|
||||
};
|
||||
use std::convert::TryFrom;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::vcs::git::error::Error;
|
||||
use crate::git::error::Error;
|
||||
use nonempty::NonEmpty;
|
||||
pub use radicle_git_ext::Oid;
|
||||
use std::{convert::TryFrom, fmt, str};
|
||||
@@ -18,26 +18,16 @@
|
||||
use crate::{
|
||||
diff::*,
|
||||
file_system,
|
||||
file_system::{directory, DirectoryEntry, Label},
|
||||
vcs::git::{
|
||||
error::*,
|
||||
Branch,
|
||||
BranchName,
|
||||
Commit,
|
||||
Glob,
|
||||
History,
|
||||
Namespace,
|
||||
Revision,
|
||||
Signature,
|
||||
Stats,
|
||||
Tag,
|
||||
TagName,
|
||||
file_system::{
|
||||
directory::{self, Directory, DirectoryEntry, FileContent},
|
||||
Label,
|
||||
},
|
||||
git::{error::*, Branch, Commit, Glob, History, Namespace, Revision, Signature, Stats, Tag},
|
||||
};
|
||||
use directory::{Directory, FileContent};
|
||||
use git_ref_format::RefString;
|
||||
use radicle_git_ext::Oid;
|
||||
use std::{
|
||||
collections::{btree_set, BTreeMap, BTreeSet},
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
convert::TryFrom,
|
||||
path::PathBuf,
|
||||
str,
|
||||
@@ -45,6 +35,9 @@ use std::{
|
||||
|
||||
use super::commit::ToCommit;
|
||||
|
||||
pub mod iter;
|
||||
pub use iter::{Branches, Namespaces, Tags};
|
||||
|
||||
/// Wrapper around the `git2`'s `git2::Repository` type.
|
||||
/// This is to to limit the functionality that we can do
|
||||
/// on the underlying object.
|
||||
@@ -74,67 +67,6 @@ impl<'a> From<&'a git2::Repository> for RepositoryRef<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
// I think the following `Tags` and `Branches` would be merged
|
||||
// using Generic associated types supported in Rust 1.65.0.
|
||||
|
||||
/// An iterator for tags.
|
||||
pub struct Tags<'a> {
|
||||
references: Vec<git2::References<'a>>,
|
||||
current: usize,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Tags<'a> {
|
||||
type Item = Result<Tag, Error>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
while self.current < self.references.len() {
|
||||
match self.references.get_mut(self.current) {
|
||||
Some(refs) => match refs.next() {
|
||||
Some(res) => return Some(res.map_err(Error::Git).and_then(Tag::try_from)),
|
||||
None => self.current += 1,
|
||||
},
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator for branches.
|
||||
pub struct Branches<'a> {
|
||||
references: Vec<git2::References<'a>>,
|
||||
current: usize,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Branches<'a> {
|
||||
type Item = Result<Branch, Error>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
while self.current < self.references.len() {
|
||||
match self.references.get_mut(self.current) {
|
||||
Some(refs) => match refs.next() {
|
||||
Some(res) => return Some(res.map_err(Error::Git).and_then(Branch::try_from)),
|
||||
None => self.current += 1,
|
||||
},
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator for namespaces.
|
||||
pub struct Namespaces {
|
||||
namespaces: btree_set::IntoIter<Namespace>,
|
||||
}
|
||||
|
||||
impl Iterator for Namespaces {
|
||||
type Item = Namespace;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.namespaces.next()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> RepositoryRef<'a> {
|
||||
/// What is the current namespace we're browsing in.
|
||||
pub fn which_namespace(&self) -> Result<Option<Namespace>, Error> {
|
||||
@@ -146,28 +78,22 @@ impl<'a> RepositoryRef<'a> {
|
||||
|
||||
/// Returns an iterator of branches that match `pattern`.
|
||||
pub fn branches(&self, pattern: &Glob<Branch>) -> Result<Branches, Error> {
|
||||
let mut branches = Branches {
|
||||
references: vec![],
|
||||
current: 0,
|
||||
};
|
||||
let mut branches = Branches::default();
|
||||
for glob in pattern.globs().iter() {
|
||||
let namespaced = self.namespaced_refname(glob)?;
|
||||
let references = self.repo_ref.references_glob(&namespaced)?;
|
||||
branches.references.push(references);
|
||||
branches.push(references);
|
||||
}
|
||||
Ok(branches)
|
||||
}
|
||||
|
||||
/// Returns an iterator of tags that match `pattern`.
|
||||
pub fn tags(&self, pattern: &Glob<Tag>) -> Result<Tags, Error> {
|
||||
let mut tags = Tags {
|
||||
references: vec![],
|
||||
current: 0,
|
||||
};
|
||||
let mut tags = Tags::default();
|
||||
for glob in pattern.globs().iter() {
|
||||
let namespaced = self.namespaced_refname(glob)?;
|
||||
let references = self.repo_ref.references_glob(&namespaced)?;
|
||||
tags.references.push(references);
|
||||
tags.push(references);
|
||||
}
|
||||
Ok(tags)
|
||||
}
|
||||
@@ -187,9 +113,7 @@ impl<'a> RepositoryRef<'a> {
|
||||
.collect::<Result<BTreeSet<Namespace>, Error>>()?;
|
||||
set.extend(new_set);
|
||||
}
|
||||
Ok(Namespaces {
|
||||
namespaces: set.into_iter(),
|
||||
})
|
||||
Ok(Namespaces::new(set))
|
||||
}
|
||||
|
||||
/// Get the [`Diff`] between two commits.
|
||||
@@ -341,21 +265,22 @@ impl<'a> RepositoryRef<'a> {
|
||||
}
|
||||
|
||||
/// Lists branch names with `filter`.
|
||||
pub fn branch_names(&self, filter: &Glob<Branch>) -> Result<Vec<BranchName>, Error> {
|
||||
let branches: Result<Vec<BranchName>, Error> =
|
||||
self.branches(filter)?.map(|b| b.map(|b| b.name)).collect();
|
||||
let mut branches = branches?;
|
||||
pub fn branch_names(&self, filter: &Glob<Branch>) -> Result<Vec<RefString>, Error> {
|
||||
let mut branches = self
|
||||
.branches(filter)?
|
||||
.map(|b| b.map(|b| b.refname().into()))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
branches.sort();
|
||||
|
||||
Ok(branches)
|
||||
}
|
||||
|
||||
/// Lists tag names in the local RefScope.
|
||||
pub fn tag_names(&self) -> Result<Vec<TagName>, Error> {
|
||||
pub fn tag_names(&self) -> Result<Vec<RefString>, Error> {
|
||||
let mut tags = self
|
||||
.tags(&Glob::tags("*")?)?
|
||||
.map(|t| t.map(|t| t.name()))
|
||||
.collect::<Result<Vec<TagName>, Error>>()?;
|
||||
.map(|t| t.map_err(Error::from).map(|t| t.refname().into()))
|
||||
.collect::<Result<Vec<_>, Error>>()?;
|
||||
tags.sort();
|
||||
|
||||
Ok(tags)
|
||||
@@ -0,0 +1,122 @@
|
||||
// I think the following `Tags` and `Branches` would be merged
|
||||
// using Generic associated types supported in Rust 1.65.0.
|
||||
|
||||
use std::{
|
||||
collections::{btree_set, BTreeSet},
|
||||
convert::TryFrom as _,
|
||||
};
|
||||
|
||||
use crate::git::{Branch, Namespace, Tag};
|
||||
|
||||
/// An iterator for tags.
|
||||
#[derive(Default)]
|
||||
pub struct Tags<'a> {
|
||||
references: Vec<git2::References<'a>>,
|
||||
current: usize,
|
||||
}
|
||||
|
||||
impl<'a> Tags<'a> {
|
||||
pub(super) fn push(&mut self, references: git2::References<'a>) {
|
||||
self.references.push(references)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Tags<'a> {
|
||||
type Item = Result<Tag, error::Tag>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
while self.current < self.references.len() {
|
||||
match self.references.get_mut(self.current) {
|
||||
Some(refs) => match refs.next() {
|
||||
Some(res) => {
|
||||
return Some(
|
||||
res.map_err(error::Tag::from)
|
||||
.and_then(|r| Tag::try_from(&r).map_err(error::Tag::from)),
|
||||
);
|
||||
},
|
||||
None => self.current += 1,
|
||||
},
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator for branches.
|
||||
#[derive(Default)]
|
||||
pub struct Branches<'a> {
|
||||
references: Vec<git2::References<'a>>,
|
||||
current: usize,
|
||||
}
|
||||
|
||||
impl<'a> Branches<'a> {
|
||||
pub(super) fn push(&mut self, references: git2::References<'a>) {
|
||||
self.references.push(references)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for Branches<'a> {
|
||||
type Item = Result<Branch, error::Branch>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
while self.current < self.references.len() {
|
||||
match self.references.get_mut(self.current) {
|
||||
Some(refs) => match refs.next() {
|
||||
Some(res) => {
|
||||
return Some(
|
||||
res.map_err(error::Branch::from)
|
||||
.and_then(|r| Branch::try_from(&r).map_err(error::Branch::from)),
|
||||
)
|
||||
},
|
||||
None => self.current += 1,
|
||||
},
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: not sure this buys us much
|
||||
/// An iterator for namespaces.
|
||||
pub struct Namespaces {
|
||||
namespaces: btree_set::IntoIter<Namespace>,
|
||||
}
|
||||
|
||||
impl Namespaces {
|
||||
pub(super) fn new(namespaces: BTreeSet<Namespace>) -> Self {
|
||||
Self {
|
||||
namespaces: namespaces.into_iter(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Namespaces {
|
||||
type Item = Namespace;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.namespaces.next()
|
||||
}
|
||||
}
|
||||
|
||||
pub mod error {
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::git::{branch, tag};
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Branch {
|
||||
#[error(transparent)]
|
||||
Git(#[from] git2::Error),
|
||||
#[error(transparent)]
|
||||
Branch(#[from] branch::error::Branch),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Tag {
|
||||
#[error(transparent)]
|
||||
Git(#[from] git2::Error),
|
||||
#[error(transparent)]
|
||||
Tag(#[from] tag::error::FromReference),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
use std::{convert::TryFrom, str};
|
||||
|
||||
use git_ext::Oid;
|
||||
use git_ref_format::{component, lit, Qualified, RefStr, RefString};
|
||||
|
||||
use crate::git::{refstr_join, Author};
|
||||
|
||||
/// The static information of a [`git2::Tag`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum Tag {
|
||||
/// A light-weight git tag.
|
||||
Light {
|
||||
/// The Object ID for the `Tag`, i.e the SHA1 digest.
|
||||
id: Oid,
|
||||
/// The reference name for this `Tag`.
|
||||
name: RefString,
|
||||
},
|
||||
/// An annotated git tag.
|
||||
Annotated {
|
||||
/// The Object ID for the `Tag`, i.e the SHA1 digest.
|
||||
id: Oid,
|
||||
/// The Object ID for the object that is tagged.
|
||||
target: Oid,
|
||||
/// The reference name for this `Tag`.
|
||||
name: RefString,
|
||||
/// The named author of this `Tag`, if the `Tag` was annotated.
|
||||
tagger: Option<Author>,
|
||||
/// The message with this `Tag`, if the `Tag` was annotated.
|
||||
message: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Tag {
|
||||
/// Get the `Oid` of the tag, regardless of its type.
|
||||
pub fn id(&self) -> Oid {
|
||||
match self {
|
||||
Self::Light { id, .. } => *id,
|
||||
Self::Annotated { id, .. } => *id,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the fully qualified `Tag` refname,
|
||||
/// e.g. `refs/tags/release/v1`.
|
||||
pub fn refname(&self) -> Qualified {
|
||||
lit::refs_tags(self.name()).into()
|
||||
}
|
||||
|
||||
fn name(&self) -> &RefString {
|
||||
match &self {
|
||||
Tag::Light { name, .. } => name,
|
||||
Tag::Annotated { name, .. } => name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod error {
|
||||
use std::str;
|
||||
|
||||
use git_ref_format::RefString;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum FromTag {
|
||||
#[error(transparent)]
|
||||
RefFormat(#[from] git_ref_format::Error),
|
||||
#[error(transparent)]
|
||||
Utf8(#[from] str::Utf8Error),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum FromReference {
|
||||
#[error(transparent)]
|
||||
FromTag(#[from] FromTag),
|
||||
#[error(transparent)]
|
||||
Git(#[from] git2::Error),
|
||||
#[error("the refname '{0}' did not begin with 'refs/tags'")]
|
||||
NotQualified(String),
|
||||
#[error("the refname '{0}' did not begin with 'refs/tags'")]
|
||||
NotTag(RefString),
|
||||
#[error(transparent)]
|
||||
RefFormat(#[from] git_ref_format::Error),
|
||||
#[error(transparent)]
|
||||
Utf8(#[from] str::Utf8Error),
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&git2::Tag<'_>> for Tag {
|
||||
type Error = error::FromTag;
|
||||
|
||||
fn try_from(tag: &git2::Tag) -> Result<Self, Self::Error> {
|
||||
let id = tag.id().into();
|
||||
let target = tag.target_id().into();
|
||||
let name = {
|
||||
let name = str::from_utf8(tag.name_bytes())?;
|
||||
RefStr::try_from_str(name)?.to_ref_string()
|
||||
};
|
||||
let tagger = tag.tagger().map(Author::try_from).transpose()?;
|
||||
let message = tag
|
||||
.message_bytes()
|
||||
.map(str::from_utf8)
|
||||
.transpose()?
|
||||
.map(|message| message.into());
|
||||
|
||||
Ok(Tag::Annotated {
|
||||
id,
|
||||
target,
|
||||
name,
|
||||
tagger,
|
||||
message,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&git2::Reference<'_>> for Tag {
|
||||
type Error = error::FromReference;
|
||||
|
||||
fn try_from(reference: &git2::Reference) -> Result<Self, Self::Error> {
|
||||
let name = {
|
||||
let name = str::from_utf8(reference.name_bytes())?;
|
||||
RefStr::try_from_str(name)?
|
||||
.qualified()
|
||||
.ok_or_else(|| error::FromReference::NotQualified(name.to_string()))?
|
||||
};
|
||||
|
||||
let (_refs, tags, c, cs) = name.non_empty_components();
|
||||
|
||||
if tags == component::TAGS {
|
||||
match reference.peel_to_tag() {
|
||||
Ok(tag) => Tag::try_from(&tag).map_err(error::FromReference::from),
|
||||
// If we get an error peeling to a tag _BUT_ we also have confirmed the
|
||||
// reference is a tag, that means we have a lightweight tag,
|
||||
// i.e. a commit SHA and name.
|
||||
Err(err)
|
||||
if err.class() == git2::ErrorClass::Object
|
||||
&& err.code() == git2::ErrorCode::InvalidSpec =>
|
||||
{
|
||||
let commit = reference.peel_to_commit()?;
|
||||
Ok(Tag::Light {
|
||||
id: commit.id().into(),
|
||||
name: refstr_join(c, cs),
|
||||
})
|
||||
},
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
} else {
|
||||
Err(error::FromReference::NotTag(name.into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,6 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#![deny(missing_docs, unused_import_braces, unused_qualifications, warnings)]
|
||||
|
||||
//! Welcome to `radicle-surf`!
|
||||
//!
|
||||
//! `radicle-surf` is a system to describe a file-system in a VCS world.
|
||||
@@ -82,9 +80,14 @@
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
pub extern crate git_ref_format;
|
||||
|
||||
extern crate radicle_git_ext as git_ext;
|
||||
|
||||
pub mod diff;
|
||||
pub mod file_system;
|
||||
pub mod vcs;
|
||||
pub mod git;
|
||||
|
||||
pub mod commit;
|
||||
pub use commit::{commit, commits, Commit};
|
||||
@@ -105,5 +108,3 @@ pub use syntax::SYNTAX_SET;
|
||||
|
||||
// Private modules
|
||||
mod nonempty;
|
||||
|
||||
pub use crate::vcs::git;
|
||||
|
||||
@@ -114,25 +114,21 @@ impl Serialize for BlobContent {
|
||||
///
|
||||
/// Will return [`Error`] if the project doesn't exist or a surf interaction
|
||||
/// fails.
|
||||
pub fn blob<P>(
|
||||
pub fn blob(
|
||||
repo: &RepositoryRef,
|
||||
maybe_revision: Option<Revision<P>>,
|
||||
maybe_revision: Option<Revision>,
|
||||
path: &str,
|
||||
) -> Result<Blob, Error>
|
||||
where
|
||||
P: ToString,
|
||||
{
|
||||
) -> Result<Blob, Error> {
|
||||
make_blob(repo, maybe_revision, path, content)
|
||||
}
|
||||
|
||||
fn make_blob<P, C>(
|
||||
fn make_blob<C>(
|
||||
repo: &RepositoryRef,
|
||||
maybe_revision: Option<Revision<P>>,
|
||||
maybe_revision: Option<Revision>,
|
||||
path: &str,
|
||||
content: C,
|
||||
) -> Result<Blob, Error>
|
||||
where
|
||||
P: ToString,
|
||||
C: FnOnce(&[u8]) -> BlobContent,
|
||||
{
|
||||
let revision = maybe_revision.unwrap();
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
use std::str::FromStr as _;
|
||||
|
||||
use git_ref_format::refname;
|
||||
#[cfg(feature = "serialize")]
|
||||
use serde::{
|
||||
ser::{SerializeStruct as _, Serializer},
|
||||
@@ -85,20 +86,17 @@ impl Serialize for TreeEntry {
|
||||
/// # Errors
|
||||
///
|
||||
/// Will return [`Error`] if any of the surf interactions fail.
|
||||
pub fn tree<P>(
|
||||
pub fn tree(
|
||||
repo: &RepositoryRef,
|
||||
maybe_revision: Option<Revision<P>>,
|
||||
maybe_revision: Option<Revision>,
|
||||
maybe_prefix: Option<String>,
|
||||
) -> Result<Tree, Error>
|
||||
where
|
||||
P: ToString,
|
||||
{
|
||||
) -> Result<Tree, Error> {
|
||||
let prefix = maybe_prefix.unwrap_or_default();
|
||||
let rev = match maybe_revision {
|
||||
Some(r) => r,
|
||||
None => Revision::Branch {
|
||||
name: "main".to_string(),
|
||||
peer_id: None,
|
||||
name: refname!("main"),
|
||||
remote: None,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
//! Represents revisions
|
||||
|
||||
use git_ref_format::{lit, Qualified, RefString};
|
||||
use nonempty::NonEmpty;
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
@@ -24,10 +25,7 @@ use serde::{Deserialize, Serialize};
|
||||
|
||||
use radicle_git_ext::Oid;
|
||||
|
||||
use crate::{
|
||||
git::{commit::ToCommit, BranchName, Glob, RepositoryRef},
|
||||
vcs::git::{self, error::Error, TagName},
|
||||
};
|
||||
use crate::git::{self, commit::ToCommit, error::Error, Glob, RepositoryRef};
|
||||
|
||||
/// Types of a peer.
|
||||
pub enum Category<P, U> {
|
||||
@@ -54,20 +52,20 @@ pub enum Category<P, U> {
|
||||
serde(rename_all = "camelCase", tag = "type")
|
||||
)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Revision<P> {
|
||||
pub enum Revision {
|
||||
/// Select a tag under the name provided.
|
||||
#[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
|
||||
Tag {
|
||||
/// Name of the tag.
|
||||
name: String,
|
||||
name: RefString,
|
||||
},
|
||||
/// Select a branch under the name provided.
|
||||
#[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
|
||||
Branch {
|
||||
/// Name of the branch.
|
||||
name: String,
|
||||
name: RefString,
|
||||
/// The remote peer, if specified.
|
||||
peer_id: Option<P>,
|
||||
remote: Option<RefString>,
|
||||
},
|
||||
/// Select a SHA1 under the name provided.
|
||||
#[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
|
||||
@@ -77,39 +75,31 @@ pub enum Revision<P> {
|
||||
},
|
||||
}
|
||||
|
||||
impl<P> git::Revision for &Revision<P>
|
||||
where
|
||||
P: ToString,
|
||||
{
|
||||
impl git::Revision for &Revision {
|
||||
fn object_id(&self, repo: &RepositoryRef) -> Result<Oid, Error> {
|
||||
match self {
|
||||
Revision::Tag { name } => {
|
||||
repo.refname_to_oid(git::TagName::new(name)?.refname().as_str())
|
||||
Revision::Tag { name } => match name.qualified() {
|
||||
None => Qualified::from(lit::refs_tags(name)).object_id(repo),
|
||||
Some(name) => name.object_id(repo),
|
||||
},
|
||||
Revision::Branch { name, peer_id } => {
|
||||
let refname = match peer_id {
|
||||
Some(peer) => {
|
||||
git::Branch::remote(&format!("heads/{}", name), &peer.to_string()).refname()
|
||||
},
|
||||
None => git::Branch::local(name).refname(),
|
||||
};
|
||||
repo.refname_to_oid(&refname)
|
||||
Revision::Branch { name, remote } => match remote {
|
||||
Some(remote) => {
|
||||
Qualified::from(lit::refs_remotes(remote.join(name))).object_id(repo)
|
||||
},
|
||||
None => git::Branch::local(name).refname().object_id(repo),
|
||||
},
|
||||
Revision::Sha { sha } => Ok(*sha),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<P> ToCommit for &Revision<P>
|
||||
where
|
||||
P: ToString,
|
||||
{
|
||||
impl ToCommit for &Revision {
|
||||
fn to_commit(self, repo: &RepositoryRef) -> Result<git::Commit, Error> {
|
||||
repo.commit(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Bundled response to retrieve both [`BranchName`]es and [`TagName`]s for
|
||||
/// Bundled response to retrieve both branches and tags for
|
||||
/// a user's repo.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct Revisions<P, U> {
|
||||
@@ -117,10 +107,10 @@ pub struct Revisions<P, U> {
|
||||
pub peer_id: P,
|
||||
/// The user who owns these revisions.
|
||||
pub user: U,
|
||||
/// List of [`git::BranchName`].
|
||||
pub branches: NonEmpty<BranchName>,
|
||||
/// List of [`git::TagName`].
|
||||
pub tags: Vec<TagName>,
|
||||
/// List of branch reference names.
|
||||
pub branches: NonEmpty<RefString>,
|
||||
/// List of tag reference names.
|
||||
pub tags: Vec<RefString>,
|
||||
}
|
||||
|
||||
/// Provide the [`Revisions`] for the given `peer_id`, looking for the
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
// This file is part of radicle-surf
|
||||
// <https://github.com/radicle-dev/radicle-surf>
|
||||
//
|
||||
// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License version 3 or
|
||||
// later as published by the Free Software Foundation.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
//! A model of a general VCS.
|
||||
|
||||
pub mod git;
|
||||
@@ -1,186 +0,0 @@
|
||||
// This file is part of radicle-surf
|
||||
// <https://github.com/radicle-dev/radicle-surf>
|
||||
//
|
||||
// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License version 3 or
|
||||
// later as published by the Free Software Foundation.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::vcs::git::{self, error::Error, ext};
|
||||
#[cfg(feature = "serialize")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{cmp::Ordering, convert::TryFrom, fmt, str};
|
||||
|
||||
/// The branch type we want to filter on.
|
||||
#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))]
|
||||
#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Hash)]
|
||||
pub enum BranchType {
|
||||
/// Local branches that are under `refs/heads/*`
|
||||
Local,
|
||||
/// Remote branches that are under `refs/remotes/<name>/*` if the name is
|
||||
/// provided, otherwise `refs/remotes/**/*`.
|
||||
Remote {
|
||||
/// Name of the remote.
|
||||
name: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<BranchType> for git2::BranchType {
|
||||
fn from(other: BranchType) -> Self {
|
||||
match other {
|
||||
BranchType::Local => git2::BranchType::Local,
|
||||
BranchType::Remote { .. } => git2::BranchType::Remote,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<git2::BranchType> for BranchType {
|
||||
fn from(other: git2::BranchType) -> Self {
|
||||
match other {
|
||||
git2::BranchType::Local => BranchType::Local,
|
||||
git2::BranchType::Remote => BranchType::Remote { name: None },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A newtype wrapper over `String` to separate out the fact that a caller wants
|
||||
/// to fetch a branch.
|
||||
#[cfg_attr(feature = "serialize", derive(Deserialize, Serialize))]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct BranchName(String);
|
||||
|
||||
impl fmt::Display for BranchName {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for BranchName {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(name: &[u8]) -> Result<Self, Self::Error> {
|
||||
let name = str::from_utf8(name)?;
|
||||
let short_name = match git::ext::try_extract_refname(name) {
|
||||
Ok(stripped) => stripped,
|
||||
Err(original) => original,
|
||||
};
|
||||
Ok(Self(short_name))
|
||||
}
|
||||
}
|
||||
|
||||
impl BranchName {
|
||||
/// Create a new `BranchName`.
|
||||
pub fn new(name: &str) -> Self {
|
||||
Self(name.into())
|
||||
}
|
||||
|
||||
/// Access the string value of the `BranchName`.
|
||||
pub fn name(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// The static information of a `git2::Branch`.
|
||||
///
|
||||
/// **Note**: The `PartialOrd` and `Ord` implementations compare on `BranchName`
|
||||
/// only.
|
||||
#[cfg_attr(feature = "serialize", derive(Deserialize, Serialize))]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Branch {
|
||||
/// Name identifier of the `Branch`.
|
||||
pub name: BranchName,
|
||||
/// Whether the `Branch` is `Remote` or `Local`.
|
||||
pub locality: BranchType,
|
||||
}
|
||||
|
||||
impl PartialOrd for Branch {
|
||||
fn partial_cmp(&self, other: &Branch) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Branch {
|
||||
fn cmp(&self, other: &Branch) -> Ordering {
|
||||
self.name.cmp(&other.name)
|
||||
}
|
||||
}
|
||||
|
||||
impl Branch {
|
||||
/// Helper to create a remote `Branch` with a name
|
||||
pub fn remote(name: &str, remote: &str) -> Self {
|
||||
Self {
|
||||
name: BranchName(name.to_string()),
|
||||
locality: BranchType::Remote {
|
||||
name: Some(remote.to_string()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper to create a remote `Branch` with a name
|
||||
pub fn local(name: &str) -> Self {
|
||||
Self {
|
||||
name: BranchName(name.to_string()),
|
||||
locality: BranchType::Local,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the name of the `Branch`.
|
||||
pub fn refname(&self) -> String {
|
||||
let branch_name = &self.name.0;
|
||||
match self.locality {
|
||||
BranchType::Local => format!("refs/heads/{}", branch_name),
|
||||
BranchType::Remote { ref name } => match name {
|
||||
None => branch_name.to_string(),
|
||||
Some(remote_name) => format!("refs/remotes/{}/{}", remote_name, branch_name),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'repo> TryFrom<git2::Reference<'repo>> for Branch {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(reference: git2::Reference) -> Result<Self, Self::Error> {
|
||||
let is_remote = ext::is_remote(&reference);
|
||||
let is_tag = reference.is_tag();
|
||||
let is_note = reference.is_note();
|
||||
let name = BranchName::try_from(reference.name_bytes())?;
|
||||
|
||||
// Best effort to not return tags or notes. Assuming everything after that is a
|
||||
// branch.
|
||||
if is_tag || is_note {
|
||||
return Err(Error::NotBranch(name));
|
||||
}
|
||||
|
||||
if is_remote {
|
||||
let mut split = name.0.splitn(2, '/');
|
||||
let remote_name = split
|
||||
.next()
|
||||
.ok_or_else(|| Error::ParseRemoteBranch(name.clone()))?;
|
||||
let name = split
|
||||
.next()
|
||||
.ok_or_else(|| Error::ParseRemoteBranch(name.clone()))?;
|
||||
|
||||
Ok(Self {
|
||||
name: BranchName(name.to_string()),
|
||||
locality: BranchType::Remote {
|
||||
name: Some(remote_name.to_string()),
|
||||
},
|
||||
})
|
||||
} else {
|
||||
Ok(Self {
|
||||
name,
|
||||
locality: BranchType::Local,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
// This file is part of radicle-surf
|
||||
// <https://github.com/radicle-dev/radicle-surf>
|
||||
//
|
||||
// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License version 3 or
|
||||
// later as published by the Free Software Foundation.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#![allow(missing_docs)]
|
||||
|
||||
/// Try to strip any refs/namespaces, refs/heads, refs/remotes, and
|
||||
/// refs/tags. If this fails we return the original string.
|
||||
pub fn try_extract_refname(spec: &str) -> Result<String, String> {
|
||||
let re = regex::Regex::new(r"(refs/namespaces/.*?/)*refs/(remotes/(.*?)/)?(heads/|tags/)?(.*)")
|
||||
.unwrap();
|
||||
|
||||
re.captures(spec)
|
||||
.and_then(|c| {
|
||||
let mut result = String::new();
|
||||
if let Some(remote) = c.get(3).map(|m| m.as_str()) {
|
||||
result.push_str(remote);
|
||||
result.push('/');
|
||||
}
|
||||
result.push_str(c.get(5).map(|m| m.as_str())?);
|
||||
Some(result)
|
||||
})
|
||||
.ok_or_else(|| spec.to_string())
|
||||
}
|
||||
|
||||
/// [`git2::Reference::is_tag`] just does a check for the prefix of `tags/`.
|
||||
/// The issue with that is, as soon as we're in 'namespaces' ref that
|
||||
/// is a tag it will say that it's not a tag. Instead we do a regex check on
|
||||
/// `refs/tags/.*`.
|
||||
pub fn is_tag(reference: &git2::Reference) -> bool {
|
||||
let re = regex::Regex::new(r"refs/tags/.*").unwrap();
|
||||
// If we couldn't parse the name we say it's not a tag.
|
||||
match reference.name() {
|
||||
Some(name) => re.is_match(name),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_branch(reference: &git2::Reference) -> bool {
|
||||
let re = regex::Regex::new(r"refs/heads/.*|refs/remotes/.*/.*").unwrap();
|
||||
// If we couldn't parse the name we say it's not a branch.
|
||||
match reference.name() {
|
||||
Some(name) => re.is_match(name),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_remote(reference: &git2::Reference) -> bool {
|
||||
let re = regex::Regex::new(r"refs/remotes/.*/.*").unwrap();
|
||||
// If we couldn't parse the name we say it's not a remote branch.
|
||||
match reference.name() {
|
||||
Some(name) => re.is_match(name),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
// This file is part of radicle-surf
|
||||
// <https://github.com/radicle-dev/radicle-surf>
|
||||
//
|
||||
// Copyright (C) 2019-2020 The Radicle Team <dev@radicle.xyz>
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License version 3 or
|
||||
// later as published by the Free Software Foundation.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use crate::vcs::git::{self, error::Error, Author};
|
||||
use git_ref_format::RefString;
|
||||
use radicle_git_ext::Oid;
|
||||
use std::{convert::TryFrom, fmt, str};
|
||||
|
||||
/// A newtype wrapper over `String` to separate out the fact that a caller wants
|
||||
/// to fetch a tag.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct TagName(RefString);
|
||||
|
||||
impl fmt::Display for TagName {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for TagName {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(name: &[u8]) -> Result<Self, Self::Error> {
|
||||
let name = str::from_utf8(name)?;
|
||||
let short_name = match git::ext::try_extract_refname(name) {
|
||||
Ok(stripped) => stripped,
|
||||
Err(original) => original,
|
||||
};
|
||||
let refstring = RefString::try_from(short_name)?;
|
||||
Ok(Self(refstring))
|
||||
}
|
||||
}
|
||||
|
||||
impl TagName {
|
||||
/// Create a new `TagName`.
|
||||
pub fn new(name: &str) -> Result<Self, Error> {
|
||||
let refstring = RefString::try_from(name)?;
|
||||
Ok(Self(refstring))
|
||||
}
|
||||
|
||||
/// Access the string value of the `TagName`.
|
||||
pub fn name(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
/// Returns the full ref name of the tag.
|
||||
pub fn refname(&self) -> String {
|
||||
format!("refs/tags/{}", self.name())
|
||||
}
|
||||
}
|
||||
|
||||
/// The static information of a [`git2::Tag`].
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum Tag {
|
||||
/// A light-weight git tag.
|
||||
Light {
|
||||
/// The Object ID for the `Tag`, i.e the SHA1 digest.
|
||||
id: Oid,
|
||||
/// The name that references this `Tag`.
|
||||
name: TagName,
|
||||
/// If the tag is provided this holds the remote’s name.
|
||||
remote: Option<String>,
|
||||
},
|
||||
/// An annotated git tag.
|
||||
Annotated {
|
||||
/// The Object ID for the `Tag`, i.e the SHA1 digest.
|
||||
id: Oid,
|
||||
/// The Object ID for the object that is tagged.
|
||||
target_id: Oid,
|
||||
/// The name that references this `Tag`.
|
||||
name: TagName,
|
||||
/// The named author of this `Tag`, if the `Tag` was annotated.
|
||||
tagger: Option<Author>,
|
||||
/// The message with this `Tag`, if the `Tag` was annotated.
|
||||
message: Option<String>,
|
||||
/// If the tag is provided this holds the remote’s name.
|
||||
remote: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Tag {
|
||||
/// Get the `Oid` of the tag, regardless of its type.
|
||||
pub fn id(&self) -> Oid {
|
||||
match self {
|
||||
Self::Light { id, .. } => *id,
|
||||
Self::Annotated { id, .. } => *id,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the `TagName` of the tag, regardless of its type.
|
||||
pub fn name(&self) -> TagName {
|
||||
match self {
|
||||
Self::Light { name, .. } => name.clone(),
|
||||
Self::Annotated { name, .. } => name.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the full ref name of the tag.
|
||||
pub fn refname(&self) -> String {
|
||||
self.name().refname()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'repo> TryFrom<git2::Tag<'repo>> for Tag {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(tag: git2::Tag) -> Result<Self, Self::Error> {
|
||||
let id = tag.id().into();
|
||||
|
||||
let target_id = tag.target_id().into();
|
||||
|
||||
let name = TagName::try_from(tag.name_bytes())?;
|
||||
|
||||
let tagger = tag.tagger().map(Author::try_from).transpose()?;
|
||||
|
||||
let message = tag
|
||||
.message_bytes()
|
||||
.map(str::from_utf8)
|
||||
.transpose()?
|
||||
.map(|message| message.into());
|
||||
|
||||
Ok(Tag::Annotated {
|
||||
id,
|
||||
target_id,
|
||||
name,
|
||||
tagger,
|
||||
message,
|
||||
remote: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<'repo> TryFrom<git2::Reference<'repo>> for Tag {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(reference: git2::Reference) -> Result<Self, Self::Error> {
|
||||
let name = TagName::try_from(reference.name_bytes())?;
|
||||
|
||||
let (remote, name) = if git::ext::is_remote(&reference) {
|
||||
let mut split = name.0.splitn(2, '/');
|
||||
let remote = split.next().map(|x| x.to_owned());
|
||||
let name = split.next().unwrap();
|
||||
(remote, TagName::new(name)?)
|
||||
} else {
|
||||
(None, name)
|
||||
};
|
||||
|
||||
match reference.peel_to_tag() {
|
||||
Ok(tag) => Ok(Tag::try_from(tag)?),
|
||||
Err(err) => {
|
||||
// If we get an error peeling to a tag _BUT_ we also have confirmed the
|
||||
// reference is a tag, that means we have a lightweight tag,
|
||||
// i.e. a commit SHA and name.
|
||||
if err.class() == git2::ErrorClass::Object
|
||||
&& err.code() == git2::ErrorCode::InvalidSpec
|
||||
{
|
||||
let commit = reference.peel_to_commit()?;
|
||||
Ok(Tag::Light {
|
||||
id: commit.id().into(),
|
||||
name,
|
||||
remote,
|
||||
})
|
||||
} else {
|
||||
Err(err.into())
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ test = []
|
||||
[dev-dependencies]
|
||||
nonempty = "0.5"
|
||||
pretty_assertions = "1.3.0"
|
||||
proptest = "0.9"
|
||||
proptest = "1"
|
||||
serde_json = "1"
|
||||
|
||||
[dev-dependencies.git2]
|
||||
@@ -24,6 +24,14 @@ version = "0.15.0"
|
||||
default-features = false
|
||||
features = ["vendored-libgit2"]
|
||||
|
||||
[dev-dependencies.git-ref-format]
|
||||
path = "../../git-ref-format"
|
||||
features = ["macro"]
|
||||
|
||||
[dev-dependencies.git-ref-format-test]
|
||||
path = "../../git-ref-format/t"
|
||||
features = ["test"]
|
||||
|
||||
[dev-dependencies.radicle-git-ext]
|
||||
path = "../../radicle-git-ext"
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ mod path {
|
||||
|
||||
#[cfg(test)]
|
||||
mod directory {
|
||||
use git_ref_format::refname;
|
||||
use radicle_surf::{
|
||||
file_system::DirectoryEntry,
|
||||
git::{Branch, Repository},
|
||||
@@ -42,7 +43,7 @@ mod directory {
|
||||
fn directory_get_path() {
|
||||
let repo = Repository::open(GIT_PLATINUM).unwrap();
|
||||
let repo = repo.as_ref();
|
||||
let root = repo.root_dir(&Branch::local("master")).unwrap();
|
||||
let root = repo.root_dir(&Branch::local(refname!("master"))).unwrap();
|
||||
|
||||
// get_path for a file.
|
||||
let path = Path::new("src/memory.rs");
|
||||
@@ -79,7 +80,7 @@ mod directory {
|
||||
fn directory_size() {
|
||||
let repo = Repository::open(GIT_PLATINUM).unwrap();
|
||||
let repo = repo.as_ref();
|
||||
let root = repo.root_dir(&Branch::local("master")).unwrap();
|
||||
let root = repo.root_dir(&Branch::local(refname!("master"))).unwrap();
|
||||
|
||||
/*
|
||||
git-platinum (master) $ ls -l src
|
||||
|
||||
+98
-109
@@ -1,14 +1,14 @@
|
||||
// Copyright © 2022 The Radicle Git Contributors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
//! Unit tests for radicle_surf::vcs::git and its submodules.
|
||||
//! Unit tests for radicle_surf::git and its submodules.
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
use radicle_surf::git::{Author, BranchType, Commit};
|
||||
use radicle_surf::git::{Author, Commit};
|
||||
use radicle_surf::{
|
||||
diff::*,
|
||||
file_system::{unsound, DirectoryEntry, Path},
|
||||
git::{error::Error, Branch, Glob, Namespace, Oid, Repository, TagName},
|
||||
git::{error::Error, Branch, Glob, Namespace, Oid, Repository},
|
||||
};
|
||||
|
||||
const GIT_PLATINUM: &str = "../data/git-platinum";
|
||||
@@ -17,22 +17,27 @@ const GIT_PLATINUM: &str = "../data/git-platinum";
|
||||
#[test]
|
||||
// An issue with submodules, see: https://github.com/radicle-dev/radicle-surf/issues/54
|
||||
fn test_submodule_failure() {
|
||||
use git_ref_format::refname;
|
||||
|
||||
let repo = Repository::discover(".").unwrap();
|
||||
repo.as_ref().root_dir(&Branch::local("main")).unwrap();
|
||||
repo.as_ref()
|
||||
.root_dir(&Branch::local(refname!("main")))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod namespace {
|
||||
use super::*;
|
||||
use git_ref_format::{name::component, refname};
|
||||
use pretty_assertions::{assert_eq, assert_ne};
|
||||
|
||||
#[test]
|
||||
fn switch_to_banana() -> Result<(), Error> {
|
||||
let repo = Repository::open(GIT_PLATINUM)?;
|
||||
let repo = repo.as_ref();
|
||||
let history_master = repo.history(&Branch::local("master"))?;
|
||||
let history_master = repo.history(&Branch::local(refname!("master")))?;
|
||||
repo.switch_namespace("golden")?;
|
||||
let history_banana = repo.history(&Branch::local("banana"))?;
|
||||
let history_banana = repo.history(&Branch::local(refname!("banana")))?;
|
||||
|
||||
assert_ne!(history_master.head(), history_banana.head());
|
||||
|
||||
@@ -43,28 +48,34 @@ mod namespace {
|
||||
fn me_namespace() -> Result<(), Error> {
|
||||
let repo = Repository::open(GIT_PLATINUM)?;
|
||||
let repo = repo.as_ref();
|
||||
let history = repo.history(&Branch::local("master"))?;
|
||||
let history = repo.history(&Branch::local(refname!("master")))?;
|
||||
|
||||
assert_eq!(repo.which_namespace(), Ok(None));
|
||||
assert_eq!(repo.which_namespace().unwrap(), None);
|
||||
|
||||
repo.switch_namespace("me")?;
|
||||
assert_eq!(repo.which_namespace(), Ok(Some(Namespace::try_from("me")?)));
|
||||
assert_eq!(
|
||||
repo.which_namespace().unwrap(),
|
||||
Some(Namespace::try_from("me")?)
|
||||
);
|
||||
|
||||
let history_feature = repo.history(&Branch::local("feature/#1194"))?;
|
||||
let history_feature = repo.history(&Branch::local(refname!("feature/#1194")))?;
|
||||
assert_eq!(history.head(), history_feature.head());
|
||||
|
||||
let expected_branches: Vec<Branch> = vec![Branch::local("feature/#1194")];
|
||||
let expected_branches: Vec<Branch> = vec![Branch::local(refname!("feature/#1194"))];
|
||||
let mut branches = repo
|
||||
.branches(&Glob::heads("*")?)?
|
||||
.collect::<Result<Vec<Branch>, Error>>()?;
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
branches.sort();
|
||||
|
||||
assert_eq!(expected_branches, branches);
|
||||
|
||||
let expected_branches: Vec<Branch> = vec![Branch::remote("feature/#1194", "fein")];
|
||||
let expected_branches: Vec<Branch> = vec![Branch::remote(
|
||||
component!("fein"),
|
||||
refname!("heads/feature/#1194"),
|
||||
)];
|
||||
let mut branches = repo
|
||||
.branches(&Glob::remotes("fein/*")?)?
|
||||
.collect::<Result<Vec<Branch>, Error>>()?;
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
branches.sort();
|
||||
|
||||
assert_eq!(expected_branches, branches);
|
||||
@@ -76,36 +87,42 @@ mod namespace {
|
||||
fn golden_namespace() -> Result<(), Error> {
|
||||
let repo = Repository::open(GIT_PLATINUM)?;
|
||||
let repo = repo.as_ref();
|
||||
let history = repo.history(&Branch::local("master"))?;
|
||||
let history = repo.history(&Branch::local(refname!("master")))?;
|
||||
|
||||
assert_eq!(repo.which_namespace(), Ok(None));
|
||||
assert_eq!(repo.which_namespace().unwrap(), None);
|
||||
|
||||
repo.switch_namespace("golden")?;
|
||||
|
||||
assert_eq!(
|
||||
repo.which_namespace(),
|
||||
Ok(Some(Namespace::try_from("golden")?))
|
||||
repo.which_namespace().unwrap(),
|
||||
Some(Namespace::try_from("golden")?)
|
||||
);
|
||||
|
||||
let golden_history = repo.history(&Branch::local("master"))?;
|
||||
let golden_history = repo.history(&Branch::local(refname!("master")))?;
|
||||
assert_eq!(history.head(), golden_history.head());
|
||||
|
||||
let expected_branches: Vec<Branch> = vec![Branch::local("banana"), Branch::local("master")];
|
||||
let expected_branches: Vec<Branch> = vec![
|
||||
Branch::local(refname!("banana")),
|
||||
Branch::local(refname!("master")),
|
||||
];
|
||||
let mut branches = repo
|
||||
.branches(&Glob::heads("*")?)?
|
||||
.collect::<Result<Vec<Branch>, Error>>()?;
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
branches.sort();
|
||||
|
||||
assert_eq!(expected_branches, branches);
|
||||
|
||||
// NOTE: these tests used to remove the categories, i.e. heads & tags, but that
|
||||
// was specialised logic based on the radicle-link storage layout.
|
||||
let remote = component!("kickflip");
|
||||
let expected_branches: Vec<Branch> = vec![
|
||||
Branch::remote("fakie/bigspin", "kickflip"),
|
||||
Branch::remote("heelflip", "kickflip"),
|
||||
Branch::remote("v0.1.0", "kickflip"),
|
||||
Branch::remote(remote.clone(), refname!("heads/fakie/bigspin")),
|
||||
Branch::remote(remote.clone(), refname!("heads/heelflip")),
|
||||
Branch::remote(remote, refname!("tags/v0.1.0")),
|
||||
];
|
||||
let mut branches = repo
|
||||
.branches(&Glob::remotes("kickflip/*")?)?
|
||||
.collect::<Result<Vec<Branch>, Error>>()?;
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
branches.sort();
|
||||
|
||||
assert_eq!(expected_branches, branches);
|
||||
@@ -117,22 +134,22 @@ mod namespace {
|
||||
fn silver_namespace() -> Result<(), Error> {
|
||||
let repo = Repository::open(GIT_PLATINUM)?;
|
||||
let repo = repo.as_ref();
|
||||
let history = repo.history(&Branch::local("master"))?;
|
||||
let history = repo.history(&Branch::local(refname!("master")))?;
|
||||
|
||||
assert_eq!(repo.which_namespace(), Ok(None));
|
||||
assert_eq!(repo.which_namespace().unwrap(), None);
|
||||
|
||||
repo.switch_namespace("golden/silver")?;
|
||||
assert_eq!(
|
||||
repo.which_namespace(),
|
||||
Ok(Some(Namespace::try_from("golden/silver")?))
|
||||
repo.which_namespace().unwrap(),
|
||||
Some(Namespace::try_from("golden/silver")?)
|
||||
);
|
||||
let silver_history = repo.history(&Branch::local("master"))?;
|
||||
let silver_history = repo.history(&Branch::local(refname!("master")))?;
|
||||
assert_ne!(history.head(), silver_history.head());
|
||||
|
||||
let expected_branches: Vec<Branch> = vec![Branch::local("master")];
|
||||
let expected_branches: Vec<Branch> = vec![Branch::local(refname!("master"))];
|
||||
let mut branches = repo
|
||||
.branches(&Glob::heads("*")?.and_remotes("*")?)?
|
||||
.collect::<Result<Vec<Branch>, Error>>()?;
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
branches.sort();
|
||||
|
||||
assert_eq!(expected_branches, branches);
|
||||
@@ -143,6 +160,8 @@ mod namespace {
|
||||
|
||||
#[cfg(test)]
|
||||
mod rev {
|
||||
use git_ref_format::{name::component, refname};
|
||||
|
||||
use super::*;
|
||||
use std::str::FromStr;
|
||||
|
||||
@@ -161,7 +180,8 @@ mod rev {
|
||||
fn _master() -> Result<(), Error> {
|
||||
let repo = Repository::open(GIT_PLATINUM)?;
|
||||
let repo = repo.as_ref();
|
||||
let mut history = repo.history(&Branch::remote("master", "origin"))?;
|
||||
let mut history =
|
||||
repo.history(&Branch::remote(component!("origin"), refname!("master")))?;
|
||||
|
||||
let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
|
||||
assert!(
|
||||
@@ -228,7 +248,7 @@ mod rev {
|
||||
fn tag() -> Result<(), Error> {
|
||||
let repo = Repository::open(GIT_PLATINUM)?;
|
||||
let repo = repo.as_ref();
|
||||
let rev = TagName::new("v0.2.0")?;
|
||||
let rev = refname!("refs/tags/v0.2.0");
|
||||
let history = repo.history(&rev)?;
|
||||
|
||||
let commit1 = Oid::from_str("2429f097664f9af0c5b7b389ab998b2199ffa977")?;
|
||||
@@ -240,6 +260,8 @@ mod rev {
|
||||
|
||||
#[cfg(test)]
|
||||
mod last_commit {
|
||||
use git_ref_format::refname;
|
||||
|
||||
use super::*;
|
||||
use std::str::FromStr;
|
||||
|
||||
@@ -344,7 +366,7 @@ mod last_commit {
|
||||
fn root() {
|
||||
let repo = Repository::open(GIT_PLATINUM)
|
||||
.expect("Could not retrieve ./data/git-platinum as git repository");
|
||||
let rev = Branch::local("master");
|
||||
let rev = Branch::local(refname!("master"));
|
||||
let root_last_commit_id = repo
|
||||
.as_ref()
|
||||
.last_commit(Path::root(), &rev)
|
||||
@@ -353,7 +375,7 @@ mod last_commit {
|
||||
|
||||
let expected_oid = repo
|
||||
.as_ref()
|
||||
.history(&Branch::local("master"))
|
||||
.history(&Branch::local(refname!("master")))
|
||||
.unwrap()
|
||||
.head()
|
||||
.id;
|
||||
@@ -365,7 +387,7 @@ mod last_commit {
|
||||
let repo = Repository::open(GIT_PLATINUM)
|
||||
.expect("Could not retrieve ./data/git-platinum as git repository");
|
||||
let repo = repo.as_ref();
|
||||
let history = repo.history(&Branch::local("dev")).unwrap();
|
||||
let history = repo.history(&Branch::local(refname!("dev"))).unwrap();
|
||||
let file_commit = history.by_path(unsound::path::new("~/bin/cat")).next();
|
||||
assert!(file_commit.is_some());
|
||||
println!("file commit: {:?}", &file_commit);
|
||||
@@ -375,6 +397,7 @@ mod last_commit {
|
||||
#[cfg(test)]
|
||||
mod diff {
|
||||
use super::*;
|
||||
use git_ref_format::refname;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::str::FromStr;
|
||||
|
||||
@@ -463,7 +486,10 @@ mod diff {
|
||||
fn test_branch_diff() -> Result<(), Error> {
|
||||
let repo = Repository::open(GIT_PLATINUM)?;
|
||||
let repo = repo.as_ref();
|
||||
let diff = repo.diff(&Branch::local("master"), &Branch::local("dev"))?;
|
||||
let diff = repo.diff(
|
||||
&Branch::local(refname!("master")),
|
||||
&Branch::local(refname!("dev")),
|
||||
)?;
|
||||
|
||||
println!("Diff two branches: master -> dev");
|
||||
println!(
|
||||
@@ -569,6 +595,7 @@ mod diff {
|
||||
|
||||
#[cfg(test)]
|
||||
mod threading {
|
||||
use git_ref_format::{name::component, refname};
|
||||
use radicle_surf::git::Glob;
|
||||
|
||||
use super::*;
|
||||
@@ -581,19 +608,21 @@ mod threading {
|
||||
let mut branches = locked_repo
|
||||
.as_ref()
|
||||
.branches(&Glob::heads("*")?.and_remotes("*")?)?
|
||||
.collect::<Result<Vec<Branch>, Error>>()?;
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
branches.sort();
|
||||
|
||||
let origin = component!("origin");
|
||||
let banana = component!("banana");
|
||||
assert_eq!(
|
||||
branches,
|
||||
vec![
|
||||
Branch::remote("HEAD", "origin"),
|
||||
Branch::local("dev"),
|
||||
Branch::remote("dev", "origin"),
|
||||
Branch::local("master"),
|
||||
Branch::remote("master", "origin"),
|
||||
Branch::remote("orange/pineapple", "banana"),
|
||||
Branch::remote("pineapple", "banana"),
|
||||
Branch::local(refname!("dev")),
|
||||
Branch::local(refname!("master")),
|
||||
Branch::remote(banana.clone(), refname!("orange/pineapple")),
|
||||
Branch::remote(banana, refname!("pineapple")),
|
||||
Branch::remote(origin.clone(), refname!("HEAD")),
|
||||
Branch::remote(origin.clone(), refname!("dev")),
|
||||
Branch::remote(origin, refname!("master")),
|
||||
]
|
||||
);
|
||||
|
||||
@@ -641,82 +670,38 @@ mod commit {
|
||||
#[cfg(feature = "serialize")]
|
||||
#[cfg(test)]
|
||||
mod branch {
|
||||
use super::*;
|
||||
use git_ref_format::{RefStr, RefString};
|
||||
use git_ref_format_test::gen;
|
||||
use proptest::prelude::*;
|
||||
use test_helpers::roundtrip;
|
||||
|
||||
use super::*;
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn prop_test_branch(branch in branch_strategy()) {
|
||||
fn prop_test_branch(branch in gen_branch()) {
|
||||
roundtrip::json(branch)
|
||||
}
|
||||
}
|
||||
|
||||
fn branch_strategy() -> impl Strategy<Value = Branch> {
|
||||
fn gen_branch() -> impl Strategy<Value = Branch> {
|
||||
prop_oneof![
|
||||
any::<String>().prop_map(|name| Branch {
|
||||
name: BranchName::new(&name),
|
||||
locality: BranchType::Local
|
||||
}),
|
||||
(any::<String>(), any::<String>()).prop_map(|(name, remote_name)| Branch {
|
||||
name: BranchName::new(&name),
|
||||
locality: BranchType::Remote {
|
||||
name: Some(remote_name),
|
||||
},
|
||||
gen::valid().prop_map(|name| Branch::local(RefString::try_from(name).unwrap())),
|
||||
(gen::valid(), gen::valid()).prop_map(|(remote, name): (String, String)| {
|
||||
let remote =
|
||||
RefStr::try_from_str(&remote).expect("BUG: reference strings should be valid");
|
||||
let name =
|
||||
RefStr::try_from_str(&name).expect("BUG: reference strings should be valid");
|
||||
Branch::remote(remote.head(), name)
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod ext {
|
||||
use radicle_surf::vcs::git::ext::*;
|
||||
|
||||
#[test]
|
||||
fn test_try_extract_refname() {
|
||||
assert_eq!(try_extract_refname("refs/heads/dev"), Ok("dev".to_string()));
|
||||
|
||||
assert_eq!(
|
||||
try_extract_refname("refs/heads/master"),
|
||||
Ok("master".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
try_extract_refname("refs/remotes/banana/pineapple"),
|
||||
Ok("banana/pineapple".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
try_extract_refname("refs/remotes/origin/master"),
|
||||
Ok("origin/master".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
try_extract_refname("refs/namespaces/golden/refs/heads/banana"),
|
||||
Ok("banana".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
try_extract_refname("refs/namespaces/golden/refs/tags/v0.1.0"),
|
||||
Ok("v0.1.0".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
try_extract_refname("refs/namespaces/golden/refs/namespaces/silver/refs/heads/master"),
|
||||
Ok("master".to_string())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
try_extract_refname("refs/namespaces/golden/refs/remotes/kickflip/heads/heelflip"),
|
||||
Ok("kickflip/heelflip".to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod reference {
|
||||
use super::*;
|
||||
use radicle_surf::vcs::git::{Glob, Tag};
|
||||
use radicle_surf::git::Glob;
|
||||
|
||||
#[test]
|
||||
fn test_branches() {
|
||||
@@ -724,7 +709,7 @@ mod reference {
|
||||
let repo = repo.as_ref();
|
||||
let branches = repo.branches(&Glob::heads("*").unwrap()).unwrap();
|
||||
for b in branches {
|
||||
println!("{}", b.unwrap().name);
|
||||
println!("{}", b.unwrap().refname());
|
||||
}
|
||||
let branches = repo
|
||||
.branches(&Glob::heads("*").unwrap().and_remotes("banana/*").unwrap())
|
||||
@@ -741,7 +726,7 @@ mod reference {
|
||||
let tags = repo_ref
|
||||
.tags(&Glob::tags("*").unwrap())
|
||||
.unwrap()
|
||||
.collect::<Result<Vec<Tag>, Error>>()
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.unwrap();
|
||||
assert_eq!(tags.len(), 6);
|
||||
let root_dir = repo_ref.root_dir(&tags[0]).unwrap();
|
||||
@@ -767,14 +752,18 @@ mod reference {
|
||||
|
||||
mod code_browsing {
|
||||
use super::*;
|
||||
|
||||
use git_ref_format::refname;
|
||||
use radicle_surf::{file_system::Directory, git::RepositoryRef};
|
||||
|
||||
#[test]
|
||||
fn iterate_root_dir_recursive() {
|
||||
let repo = Repository::open(GIT_PLATINUM).unwrap();
|
||||
let repo = repo.as_ref();
|
||||
let root_dir = repo.root_dir(&Branch::local("master")).unwrap();
|
||||
|
||||
let root_dir = repo.root_dir(&Branch::local(refname!("master"))).unwrap();
|
||||
let count = println_dir(&root_dir, &repo, 0);
|
||||
|
||||
assert_eq!(count, 36); // Check total file count.
|
||||
|
||||
/// Prints items in `dir` with `indent_level`.
|
||||
@@ -798,7 +787,7 @@ mod code_browsing {
|
||||
fn browse_repo_lazily() {
|
||||
let repo = Repository::open(GIT_PLATINUM).unwrap();
|
||||
let repo = repo.as_ref();
|
||||
let root_dir = repo.root_dir(&Branch::local("master")).unwrap();
|
||||
let root_dir = repo.root_dir(&Branch::local(refname!("master"))).unwrap();
|
||||
let count = root_dir.contents(&repo).unwrap().iter().count();
|
||||
assert_eq!(count, 8);
|
||||
let count = traverse(&root_dir, &repo);
|
||||
@@ -820,7 +809,7 @@ mod code_browsing {
|
||||
fn test_file_history() {
|
||||
let repo = Repository::open(GIT_PLATINUM).unwrap();
|
||||
let repo = repo.as_ref();
|
||||
let history = repo.history(&Branch::local("dev")).unwrap();
|
||||
let history = repo.history(&Branch::local(refname!("dev"))).unwrap();
|
||||
let path = unsound::path::new("README.md");
|
||||
let mut file_history = history.by_path(path);
|
||||
let commit = file_history.next().unwrap().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user