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:
Fintan Halpenny
2022-11-18 10:29:45 +00:00
28 changed files with 809 additions and 803 deletions
+2 -4
View File
@@ -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"
+3 -2
View File
@@ -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 [
+1 -1
View File
@@ -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();
+5 -11
View File
@@ -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 {
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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>()
}
+306
View File
@@ -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)
+122
View File
@@ -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),
}
}
+149
View File
@@ -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()))
}
}
}
+6 -5
View File
@@ -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;
+5 -9
View File
@@ -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();
+6 -8
View File
@@ -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,
},
};
+21 -31
View File
@@ -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
-20
View File
@@ -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;
-186
View File
@@ -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,
})
}
}
}
-68
View File
@@ -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,
}
}
-183
View File
@@ -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())
}
},
}
}
}
+9 -1
View File
@@ -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"
+3 -2
View File
@@ -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
View File
@@ -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();