diff --git a/radicle-surf/Cargo.toml b/radicle-surf/Cargo.toml index 242bdef..498dfa5 100644 --- a/radicle-surf/Cargo.toml +++ b/radicle-surf/Cargo.toml @@ -4,7 +4,7 @@ description = "A code surfing library for VCS file systems" readme = "README.md" version = "0.8.0" authors = ["The Radicle Team "] -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" diff --git a/radicle-surf/benches/last_commit.rs b/radicle-surf/benches/last_commit.rs index 409cf11..2b35100 100644 --- a/radicle-surf/benches/last_commit.rs +++ b/radicle-surf/benches/last_commit.rs @@ -16,6 +16,7 @@ // along with this program. If not, see . use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use git_ref_format::refname; use radicle_surf::{ file_system::{unsound, Path}, git::{Branch, Repository}, @@ -24,7 +25,7 @@ use radicle_surf::{ 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 [ diff --git a/radicle-surf/src/commit.rs b/radicle-surf/src/commit.rs index 41436b3..68af3d7 100644 --- a/radicle-surf/src/commit.rs +++ b/radicle-surf/src/commit.rs @@ -17,6 +17,7 @@ //! Represents a commit. +use git_ref_format::RefString; #[cfg(feature = "serialize")] use serde::{ ser::{SerializeStruct as _, Serializer}, @@ -26,7 +27,7 @@ use serde::{ use crate::{ diff, file_system, - git::{self, BranchName, Glob, RepositoryRef}, + git::{self, Glob, RepositoryRef}, person::Person, revision::Revision, }; @@ -54,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, + pub branches: Vec, } /// Representation of a code commit. @@ -195,7 +196,7 @@ pub fn commit(repo: &RepositoryRef, rev: R) -> Result Result { /// /// Will return [`Error`] if the project doesn't exist or the surf interaction /// fails. -pub fn commits

( - repo: &RepositoryRef, - maybe_revision: Option>, -) -> Result -where - P: ToString, -{ +pub fn commits(repo: &RepositoryRef, maybe_revision: Option) -> Result { let rev = match maybe_revision { Some(revision) => revision, None => Revision::Sha { diff --git a/radicle-surf/src/git.rs b/radicle-surf/src/git.rs index 1979f23..f3482de 100644 --- a/radicle-surf/src/git.rs +++ b/radicle-surf/src/git.rs @@ -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; @@ -121,6 +120,30 @@ pub trait Revision { fn object_id(&self, repo: &RepositoryRef) -> Result; } +impl Revision for RefString { + fn object_id(&self, repo: &RepositoryRef) -> Result { + repo.refname_to_oid(self.as_str()) + } +} + +impl Revision for &RefString { + fn object_id(&self, repo: &RepositoryRef) -> Result { + repo.refname_to_oid(self.as_str()) + } +} + +impl Revision for Qualified<'_> { + fn object_id(&self, repo: &RepositoryRef) -> Result { + repo.refname_to_oid(self.as_str()) + } +} + +impl Revision for &Qualified<'_> { + fn object_id(&self, repo: &RepositoryRef) -> Result { + repo.refname_to_oid(self.as_str()) + } +} + impl Revision for Oid { fn object_id(&self, _repo: &RepositoryRef) -> Result { Ok(*self) @@ -147,9 +170,6 @@ impl Revision for &Tag { } } -impl Revision for &TagName { - fn object_id(&self, repo: &RepositoryRef) -> Result { - 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::() } diff --git a/radicle-surf/src/git/branch.rs b/radicle-surf/src/git/branch.rs index 6bfac0d..988f667 100644 --- a/radicle-surf/src/git/branch.rs +++ b/radicle-surf/src/git/branch.rs @@ -1,186 +1,306 @@ -// This file is part of radicle-surf -// -// -// Copyright (C) 2019-2020 The Radicle Team -// -// 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 . +use std::{ + convert::TryFrom, + str::{self, FromStr}, +}; -use crate::git::{self, error::Error, ext}; -#[cfg(feature = "serialize")] -use serde::{Deserialize, Serialize}; -use std::{cmp::Ordering, convert::TryFrom, fmt, str}; +use crate::git::refstr_join; +use git_ref_format::{component, lit, Component, Qualified, RefStr, RefString}; -/// 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//*` if the name is - /// provided, otherwise `refs/remotes/**/*`. - Remote { - /// Name of the remote. - name: Option, - }, -} - -impl From for git2::BranchType { - fn from(other: BranchType) -> Self { - match other { - BranchType::Local => git2::BranchType::Local, - BranchType::Remote { .. } => git2::BranchType::Remote, - } - } -} - -impl From 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 { - 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`. +/// A `Branch` represents any git branch. This can either be a reference +/// that is under the `refs/heads` or `refs/remotes` namespace. /// -/// **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 { - Some(self.cmp(other)) - } -} - -impl Ord for Branch { - fn cmp(&self, other: &Branch) -> Ordering { - self.name.cmp(&other.name) - } +/// 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 { - /// 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()), - }, - } + /// Construct a [`Local`] branch. + pub fn local(name: R) -> Self + where + R: AsRef, + { + Self::Local(Local::new(name)) } - /// Helper to create a remote `Branch` with a name - pub fn local(name: &str) -> Self { - Self { - name: BranchName(name.to_string()), - locality: BranchType::Local, - } + /// Construct a [`Remote`] branch. + /// The `remote` is the remote name of the reference name while + /// the `name` is the suffix, i.e. `refs/remotes//`. + pub fn remote(remote: Component<'_>, name: R) -> Self + where + R: AsRef, + { + Self::Remote(Remote::new(remote, name)) } - /// 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), - }, + pub fn refname(&self) -> Qualified { + match self { + Branch::Local(local) => local.refname(), + Branch::Remote(remote) => remote.refname(), } } } -impl<'repo> TryFrom> for Branch { - type Error = Error; +impl TryFrom<&git2::Reference<'_>> for Branch { + type Error = error::Branch; - fn try_from(reference: git2::Reference) -> Result { - 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())?; + fn try_from(reference: &git2::Reference<'_>) -> Result { + let name = str::from_utf8(reference.name_bytes())?; + Self::from_str(name) + } +} - // 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)); - } +impl TryFrom<&str> for Branch { + type Error = error::Branch; - 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()))?; + fn try_from(name: &str) -> Result { + Self::from_str(name) + } +} - Ok(Self { - name: BranchName(name.to_string()), - locality: BranchType::Remote { - name: Some(remote_name.to_string()), - }, - }) +impl FromStr for Branch { + type Err = error::Branch; + + fn from_str(name: &str) -> Result { + 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::()))) } else { - Ok(Self { - name, - locality: BranchType::Local, - }) + 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(name: R) -> Self + where + R: AsRef, + { + 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 { + 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::from_str(name) + } +} + +impl FromStr for Local { + type Err = error::Local; + + fn from_str(name: &str) -> Result { + 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(remote: Component, name: R) -> Self + where + R: AsRef, + { + Self { + name: name.as_ref().to_ref_string(), + remote: remote.to_ref_string(), + } + } + + /// Parse the `name` from the form `refs/remotes//`. + /// + /// If the `name` is not of this form, then `None` is returned. + pub fn from_refs_remotes(name: R) -> Option + where + R: AsRef, + { + 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 { + 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::from_str(name) + } +} + +impl FromStr for Remote { + type Err = error::Remote; + + fn from_str(name: &str) -> Result { + 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::())) + } 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), + } +} diff --git a/radicle-surf/src/git/commit.rs b/radicle-surf/src/git/commit.rs index 3164ff1..0fdfd6c 100644 --- a/radicle-surf/src/git/commit.rs +++ b/radicle-surf/src/git/commit.rs @@ -17,8 +17,9 @@ use crate::{ file_system::{self, directory}, - 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 { + repo.commit(self) + } +} + +impl ToCommit for &Qualified<'_> { fn to_commit(self, repo: &RepositoryRef) -> Result { repo.commit(self) } diff --git a/radicle-surf/src/git/error.rs b/radicle-surf/src/git/error.rs index 31f855d..565fcf1 100644 --- a/radicle-surf/src/git/error.rs +++ b/radicle-surf/src/git/error.rs @@ -18,31 +18,18 @@ //! Collection of errors and helper instances that can occur when performing //! operations from [`crate::git`]. -use crate::{ - diff, - file_system, - 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::git`]. -#[derive(Debug, PartialEq, Error)] +#[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")] diff --git a/radicle-surf/src/git/ext.rs b/radicle-surf/src/git/ext.rs deleted file mode 100644 index d065881..0000000 --- a/radicle-surf/src/git/ext.rs +++ /dev/null @@ -1,68 +0,0 @@ -// This file is part of radicle-surf -// -// -// Copyright (C) 2019-2020 The Radicle Team -// -// 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 . - -#![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 { - 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, - } -} diff --git a/radicle-surf/src/git/repo.rs b/radicle-surf/src/git/repo.rs index 6d4ab51..5457265 100644 --- a/radicle-surf/src/git/repo.rs +++ b/radicle-surf/src/git/repo.rs @@ -18,26 +18,16 @@ use crate::{ diff::*, file_system, - file_system::{directory, DirectoryEntry, Label}, - 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>, - current: usize, -} - -impl<'a> Iterator for Tags<'a> { - type Item = Result; - - fn next(&mut self) -> Option { - 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>, - current: usize, -} - -impl<'a> Iterator for Branches<'a> { - type Item = Result; - - fn next(&mut self) -> Option { - 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, -} - -impl Iterator for Namespaces { - type Item = Namespace; - fn next(&mut self) -> Option { - self.namespaces.next() - } -} - impl<'a> RepositoryRef<'a> { /// What is the current namespace we're browsing in. pub fn which_namespace(&self) -> Result, Error> { @@ -146,28 +78,22 @@ impl<'a> RepositoryRef<'a> { /// Returns an iterator of branches that match `pattern`. pub fn branches(&self, pattern: &Glob) -> Result { - 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) -> Result { - 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::, 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) -> Result, Error> { - let branches: Result, Error> = - self.branches(filter)?.map(|b| b.map(|b| b.name)).collect(); - let mut branches = branches?; + pub fn branch_names(&self, filter: &Glob) -> Result, Error> { + let mut branches = self + .branches(filter)? + .map(|b| b.map(|b| b.refname().into())) + .collect::, _>>()?; branches.sort(); Ok(branches) } /// Lists tag names in the local RefScope. - pub fn tag_names(&self) -> Result, Error> { + pub fn tag_names(&self) -> Result, Error> { let mut tags = self .tags(&Glob::tags("*")?)? - .map(|t| t.map(|t| t.name())) - .collect::, Error>>()?; + .map(|t| t.map_err(Error::from).map(|t| t.refname().into())) + .collect::, Error>>()?; tags.sort(); Ok(tags) diff --git a/radicle-surf/src/git/repo/iter.rs b/radicle-surf/src/git/repo/iter.rs new file mode 100644 index 0000000..9bd17b9 --- /dev/null +++ b/radicle-surf/src/git/repo/iter.rs @@ -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>, + 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; + + fn next(&mut self) -> Option { + 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>, + 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; + + fn next(&mut self) -> Option { + 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, +} + +impl Namespaces { + pub(super) fn new(namespaces: BTreeSet) -> Self { + Self { + namespaces: namespaces.into_iter(), + } + } +} + +impl Iterator for Namespaces { + type Item = Namespace; + fn next(&mut self) -> Option { + 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), + } +} diff --git a/radicle-surf/src/git/tag.rs b/radicle-surf/src/git/tag.rs index 1d23957..743599b 100644 --- a/radicle-surf/src/git/tag.rs +++ b/radicle-surf/src/git/tag.rs @@ -1,67 +1,9 @@ -// This file is part of radicle-surf -// -// -// Copyright (C) 2019-2020 The Radicle Team -// -// 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 . +use std::{convert::TryFrom, str}; -use crate::git::{self, error::Error, Author}; -use git_ref_format::RefString; -use radicle_git_ext::Oid; -use std::{convert::TryFrom, fmt, str}; +use git_ext::Oid; +use git_ref_format::{component, lit, Qualified, RefStr, RefString}; -/// 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 { - 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 { - 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()) - } -} +use crate::git::{refstr_join, Author}; /// The static information of a [`git2::Tag`]. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] @@ -70,25 +12,21 @@ pub enum 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, + /// 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_id: Oid, - /// The name that references this `Tag`. - name: TagName, + target: Oid, + /// The reference name for this `Tag`. + name: RefString, /// The named author of this `Tag`, if the `Tag` was annotated. tagger: Option, /// The message with this `Tag`, if the `Tag` was annotated. message: Option, - /// If the tag is provided this holds the remote’s name. - remote: Option, }, } @@ -101,32 +39,62 @@ impl Tag { } } - /// 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(), - } + /// Return the fully qualified `Tag` refname, + /// e.g. `refs/tags/release/v1`. + pub fn refname(&self) -> Qualified { + lit::refs_tags(self.name()).into() } - /// Returns the full ref name of the tag. - pub fn refname(&self) -> String { - self.name().refname() + fn name(&self) -> &RefString { + match &self { + Tag::Light { name, .. } => name, + Tag::Annotated { name, .. } => name, + } } } -impl<'repo> TryFrom> for Tag { - type Error = Error; +pub mod error { + use std::str; - fn try_from(tag: git2::Tag) -> Result { + 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 { let id = tag.id().into(); - - let target_id = tag.target_id().into(); - - let name = TagName::try_from(tag.name_bytes())?; - + 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) @@ -135,49 +103,47 @@ impl<'repo> TryFrom> for Tag { Ok(Tag::Annotated { id, - target_id, + target, name, tagger, message, - remote: None, }) } } -impl<'repo> TryFrom> for Tag { - type Error = Error; +impl TryFrom<&git2::Reference<'_>> for Tag { + type Error = error::FromReference; - fn try_from(reference: git2::Reference) -> Result { - 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) + fn try_from(reference: &git2::Reference) -> Result { + 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()))? }; - match reference.peel_to_tag() { - Ok(tag) => Ok(Tag::try_from(tag)?), - Err(err) => { + 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. - if err.class() == git2::ErrorClass::Object - && err.code() == git2::ErrorCode::InvalidSpec + 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, - remote, + name: refstr_join(c, cs), }) - } else { - Err(err.into()) - } - }, + }, + Err(err) => Err(err.into()), + } + } else { + Err(error::FromReference::NotTag(name.into())) } } } diff --git a/radicle-surf/src/lib.rs b/radicle-surf/src/lib.rs index dfa3db9..24a6139 100644 --- a/radicle-surf/src/lib.rs +++ b/radicle-surf/src/lib.rs @@ -80,6 +80,11 @@ //! # Ok(()) //! # } //! ``` + +pub extern crate git_ref_format; + +extern crate radicle_git_ext as git_ext; + pub mod diff; pub mod file_system; pub mod git; diff --git a/radicle-surf/src/object/blob.rs b/radicle-surf/src/object/blob.rs index 756df96..4d61beb 100644 --- a/radicle-surf/src/object/blob.rs +++ b/radicle-surf/src/object/blob.rs @@ -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

( +pub fn blob( repo: &RepositoryRef, - maybe_revision: Option>, + maybe_revision: Option, path: &str, -) -> Result -where - P: ToString, -{ +) -> Result { make_blob(repo, maybe_revision, path, content) } -fn make_blob( +fn make_blob( repo: &RepositoryRef, - maybe_revision: Option>, + maybe_revision: Option, path: &str, content: C, ) -> Result where - P: ToString, C: FnOnce(&[u8]) -> BlobContent, { let revision = maybe_revision.unwrap(); diff --git a/radicle-surf/src/object/tree.rs b/radicle-surf/src/object/tree.rs index 2077527..aaac499 100644 --- a/radicle-surf/src/object/tree.rs +++ b/radicle-surf/src/object/tree.rs @@ -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

( +pub fn tree( repo: &RepositoryRef, - maybe_revision: Option>, + maybe_revision: Option, maybe_prefix: Option, -) -> Result -where - P: ToString, -{ +) -> Result { 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, }, }; diff --git a/radicle-surf/src/revision.rs b/radicle-surf/src/revision.rs index 8805983..8f3ae76 100644 --- a/radicle-surf/src/revision.rs +++ b/radicle-surf/src/revision.rs @@ -17,6 +17,7 @@ //! Represents revisions +use git_ref_format::{lit, Qualified, RefString}; use nonempty::NonEmpty; #[cfg(feature = "serialize")] @@ -24,7 +25,7 @@ use serde::{Deserialize, Serialize}; use radicle_git_ext::Oid; -use crate::git::{self, commit::ToCommit, error::Error, BranchName, Glob, RepositoryRef, TagName}; +use crate::git::{self, commit::ToCommit, error::Error, Glob, RepositoryRef}; /// Types of a peer. pub enum Category { @@ -51,20 +52,20 @@ pub enum Category { serde(rename_all = "camelCase", tag = "type") )] #[derive(Debug, Clone)] -pub enum Revision

{ +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

, + remote: Option, }, /// Select a SHA1 under the name provided. #[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))] @@ -74,39 +75,31 @@ pub enum Revision

{ }, } -impl

git::Revision for &Revision

-where - P: ToString, -{ +impl git::Revision for &Revision { fn object_id(&self, repo: &RepositoryRef) -> Result { 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

ToCommit for &Revision

-where - P: ToString, -{ +impl ToCommit for &Revision { fn to_commit(self, repo: &RepositoryRef) -> Result { 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 { @@ -114,10 +107,10 @@ pub struct Revisions { pub peer_id: P, /// The user who owns these revisions. pub user: U, - /// List of [`git::BranchName`]. - pub branches: NonEmpty, - /// List of [`git::TagName`]. - pub tags: Vec, + /// List of branch reference names. + pub branches: NonEmpty, + /// List of tag reference names. + pub tags: Vec, } /// Provide the [`Revisions`] for the given `peer_id`, looking for the diff --git a/radicle-surf/t/Cargo.toml b/radicle-surf/t/Cargo.toml index 19ce3f2..efda92d 100644 --- a/radicle-surf/t/Cargo.toml +++ b/radicle-surf/t/Cargo.toml @@ -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" diff --git a/radicle-surf/t/src/file_system.rs b/radicle-surf/t/src/file_system.rs index 4cef3d8..2d807a3 100644 --- a/radicle-surf/t/src/file_system.rs +++ b/radicle-surf/t/src/file_system.rs @@ -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 diff --git a/radicle-surf/t/src/git.rs b/radicle-surf/t/src/git.rs index 9d4ec9e..0c86e75 100644 --- a/radicle-surf/t/src/git.rs +++ b/radicle-surf/t/src/git.rs @@ -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 = vec![Branch::local("feature/#1194")]; + let expected_branches: Vec = vec![Branch::local(refname!("feature/#1194"))]; let mut branches = repo .branches(&Glob::heads("*")?)? - .collect::, Error>>()?; + .collect::, _>>()?; branches.sort(); assert_eq!(expected_branches, branches); - let expected_branches: Vec = vec![Branch::remote("feature/#1194", "fein")]; + let expected_branches: Vec = vec![Branch::remote( + component!("fein"), + refname!("heads/feature/#1194"), + )]; let mut branches = repo .branches(&Glob::remotes("fein/*")?)? - .collect::, Error>>()?; + .collect::, _>>()?; 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 = vec![Branch::local("banana"), Branch::local("master")]; + let expected_branches: Vec = vec![ + Branch::local(refname!("banana")), + Branch::local(refname!("master")), + ]; let mut branches = repo .branches(&Glob::heads("*")?)? - .collect::, Error>>()?; + .collect::, _>>()?; 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 = 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::, Error>>()?; + .collect::, _>>()?; 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 = vec![Branch::local("master")]; + let expected_branches: Vec = vec![Branch::local(refname!("master"))]; let mut branches = repo .branches(&Glob::heads("*")?.and_remotes("*")?)? - .collect::, Error>>()?; + .collect::, _>>()?; 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::, Error>>()?; + .collect::, _>>()?; 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,28 +670,29 @@ 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 { + fn gen_branch() -> impl Strategy { prop_oneof![ - any::().prop_map(|name| Branch { - name: BranchName::new(&name), - locality: BranchType::Local - }), - (any::(), any::()).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) }) ] } @@ -671,7 +701,7 @@ mod branch { #[cfg(test)] mod reference { use super::*; - use radicle_surf::git::{Glob, Tag}; + use radicle_surf::git::Glob; #[test] fn test_branches() { @@ -679,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()) @@ -696,7 +726,7 @@ mod reference { let tags = repo_ref .tags(&Glob::tags("*").unwrap()) .unwrap() - .collect::, Error>>() + .collect::, _>>() .unwrap(); assert_eq!(tags.len(), 6); let root_dir = repo_ref.root_dir(&tags[0]).unwrap(); @@ -722,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`. @@ -753,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); @@ -775,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();