mirror of
https://seed.radicle.dev/rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt.git
synced 2026-10-24 13:50:36 +08:00
Merge remote-tracking branch 'han/ref-format'
* han/ref-format: Remove Ref, RefGlob and Rev, add Glob that uses git-ref-format. Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com>
This commit is contained in:
@@ -12,7 +12,7 @@ pub struct Options {
|
||||
pub allow_pattern: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[derive(Debug, PartialEq, Eq, Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
#[error("empty input")]
|
||||
|
||||
@@ -32,7 +32,6 @@ base64 = "0.13"
|
||||
either = "1.5"
|
||||
nom = "6"
|
||||
nonempty = "0.5"
|
||||
radicle-git-ext = { path = "../radicle-git-ext", features = ["serde"] }
|
||||
regex = ">= 1.5.5"
|
||||
serde = { features = ["serde_derive"], optional = true, version = "1" }
|
||||
thiserror = "1.0"
|
||||
@@ -42,6 +41,15 @@ version = "0.15.0"
|
||||
default-features = false
|
||||
features = ["vendored-libgit2"]
|
||||
|
||||
[dependencies.git-ref-format]
|
||||
version = "0.1.0"
|
||||
path = "../git-ref-format"
|
||||
|
||||
[dependencies.radicle-git-ext]
|
||||
version = "0.2.0"
|
||||
path = "../radicle-git-ext"
|
||||
features = ["serde"]
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = "0.3"
|
||||
pretty_assertions = "1.3.0"
|
||||
|
||||
@@ -18,13 +18,13 @@
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||
use radicle_surf::{
|
||||
file_system::{unsound, Path},
|
||||
vcs::git::{Branch, Repository, Rev},
|
||||
vcs::git::{Branch, Repository},
|
||||
};
|
||||
|
||||
fn last_commit_comparison(c: &mut Criterion) {
|
||||
let repo = Repository::new("./data/git-platinum")
|
||||
.expect("Could not retrieve ./data/git-platinum as git repository");
|
||||
let rev: Rev = Branch::local("master").into();
|
||||
let rev = Branch::local("master");
|
||||
|
||||
let mut group = c.benchmark_group("Last Commit");
|
||||
for path in [
|
||||
|
||||
@@ -17,8 +17,6 @@
|
||||
|
||||
//! Represents a commit.
|
||||
|
||||
use std::convert::TryFrom as _;
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
use serde::{
|
||||
ser::{SerializeStruct as _, Serializer},
|
||||
@@ -28,9 +26,10 @@ use serde::{
|
||||
use crate::{
|
||||
diff,
|
||||
file_system,
|
||||
git::Glob,
|
||||
person::Person,
|
||||
revision::Revision,
|
||||
vcs::git::{self, BranchName, RepositoryRef, Rev},
|
||||
vcs::git::{self, BranchName, RepositoryRef},
|
||||
};
|
||||
|
||||
use radicle_git_ext::Oid;
|
||||
@@ -147,7 +146,7 @@ pub struct Commits {
|
||||
///
|
||||
/// Will return [`Error`] if the project doesn't exist or the surf interaction
|
||||
/// fails.
|
||||
pub fn commit(repo: &RepositoryRef, rev: &Rev) -> Result<Commit, Error> {
|
||||
pub fn commit<R: git::Revision>(repo: &RepositoryRef, rev: R) -> Result<Commit, Error> {
|
||||
let commit = repo.commit(rev)?;
|
||||
let sha1 = commit.id;
|
||||
let header = Header::from(&commit);
|
||||
@@ -195,7 +194,7 @@ pub fn commit(repo: &RepositoryRef, rev: &Rev) -> Result<Commit, Error> {
|
||||
}
|
||||
|
||||
let branches = repo
|
||||
.revision_branches(&sha1)?
|
||||
.revision_branches(&sha1, &Glob::heads("*")?.and_remotes("*")?)?
|
||||
.into_iter()
|
||||
.map(|b| b.name)
|
||||
.collect();
|
||||
@@ -235,11 +234,11 @@ pub fn commits<P>(
|
||||
where
|
||||
P: ToString,
|
||||
{
|
||||
let maybe_revision = maybe_revision.map(Rev::try_from).transpose()?;
|
||||
|
||||
let rev: Rev = match maybe_revision {
|
||||
let rev = match maybe_revision {
|
||||
Some(revision) => revision,
|
||||
None => repo.head_oid()?.into(),
|
||||
None => Revision::Sha {
|
||||
sha: repo.head_oid()?,
|
||||
},
|
||||
};
|
||||
|
||||
let stats = repo.get_commit_stats(&rev)?;
|
||||
|
||||
@@ -18,10 +18,7 @@
|
||||
//! Represents git object type 'blob', i.e. actual file contents.
|
||||
//! See git [doc](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects) for more details.
|
||||
|
||||
use std::{
|
||||
convert::TryFrom as _,
|
||||
str::{self, FromStr as _},
|
||||
};
|
||||
use std::str::{self, FromStr as _};
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
use serde::{
|
||||
@@ -35,7 +32,6 @@ use crate::{
|
||||
git::RepositoryRef,
|
||||
object::{Error, Info, ObjectType},
|
||||
revision::Revision,
|
||||
vcs::git::Rev,
|
||||
};
|
||||
|
||||
#[cfg(feature = "syntax")]
|
||||
@@ -139,7 +135,6 @@ where
|
||||
P: ToString,
|
||||
C: FnOnce(&[u8]) -> BlobContent,
|
||||
{
|
||||
let maybe_revision = maybe_revision.map(Rev::try_from).transpose()?;
|
||||
let revision = maybe_revision.unwrap();
|
||||
let root = repo.snapshot(&revision)?;
|
||||
let p = file_system::Path::from_str(path)?;
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
//! Represents git object type 'tree', i.e. like directory entries in Unix.
|
||||
//! See git [doc](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects) for more details.
|
||||
|
||||
use std::{convert::TryFrom as _, str::FromStr as _};
|
||||
use std::str::FromStr as _;
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
use serde::{
|
||||
@@ -32,7 +32,6 @@ use crate::{
|
||||
git::RepositoryRef,
|
||||
object::{Error, Info, ObjectType},
|
||||
revision::Revision,
|
||||
vcs::git::{Branch, Rev},
|
||||
};
|
||||
|
||||
/// Result of a directory listing, carries other trees and blobs.
|
||||
@@ -94,12 +93,13 @@ pub fn tree<P>(
|
||||
where
|
||||
P: ToString,
|
||||
{
|
||||
let maybe_revision = maybe_revision.map(Rev::try_from).transpose()?;
|
||||
let prefix = maybe_prefix.unwrap_or_default();
|
||||
|
||||
let rev = match maybe_revision {
|
||||
Some(r) => r,
|
||||
None => Branch::local("main").into(),
|
||||
None => Revision::Branch {
|
||||
name: "main".to_string(),
|
||||
peer_id: None,
|
||||
},
|
||||
};
|
||||
|
||||
let path = if prefix == "/" || prefix.is_empty() {
|
||||
|
||||
@@ -17,8 +17,6 @@
|
||||
|
||||
//! Represents revisions
|
||||
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use nonempty::NonEmpty;
|
||||
|
||||
#[cfg(feature = "serialize")]
|
||||
@@ -27,8 +25,8 @@ use serde::{Deserialize, Serialize};
|
||||
use radicle_git_ext::Oid;
|
||||
|
||||
use crate::{
|
||||
git::{BranchName, RepositoryRef},
|
||||
vcs::git::{self, error::Error, RefScope, Rev, TagName},
|
||||
git::{commit::ToCommit, BranchName, Glob, RepositoryRef},
|
||||
vcs::git::{self, error::Error, TagName},
|
||||
};
|
||||
|
||||
/// Types of a peer.
|
||||
@@ -79,26 +77,38 @@ pub enum Revision<P> {
|
||||
},
|
||||
}
|
||||
|
||||
impl<P> TryFrom<Revision<P>> for Rev
|
||||
impl<P> git::Revision for &Revision<P>
|
||||
where
|
||||
P: ToString,
|
||||
{
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(other: Revision<P>) -> Result<Self, Self::Error> {
|
||||
match other {
|
||||
Revision::Tag { name } => Ok(git::TagName::new(&name).into()),
|
||||
Revision::Branch { name, peer_id } => Ok(match peer_id {
|
||||
Some(peer) => {
|
||||
git::Branch::remote(&format!("heads/{}", name), &peer.to_string()).into()
|
||||
},
|
||||
None => git::Branch::local(&name).into(),
|
||||
}),
|
||||
Revision::Sha { sha } => Ok(sha.into()),
|
||||
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::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::Sha { sha } => Ok(*sha),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<P> ToCommit for &Revision<P>
|
||||
where
|
||||
P: ToString,
|
||||
{
|
||||
fn to_commit(self, repo: &RepositoryRef) -> Result<git::Commit, Error> {
|
||||
repo.commit(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Bundled response to retrieve both [`BranchName`]es and [`TagName`]s for
|
||||
/// a user's repo.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -114,7 +124,7 @@ pub struct Revisions<P, U> {
|
||||
}
|
||||
|
||||
/// Provide the [`Revisions`] for the given `peer_id`, looking for the
|
||||
/// branches as [`RefScope::Remote`].
|
||||
/// remote branches.
|
||||
///
|
||||
/// If there are no branches then this returns `None`.
|
||||
///
|
||||
@@ -129,7 +139,8 @@ pub fn remote<P, U>(
|
||||
where
|
||||
P: Clone + ToString,
|
||||
{
|
||||
let remote_branches = repo.branch_names(Some(peer_id.clone()).into())?;
|
||||
let remote_branches =
|
||||
repo.branch_names(&Glob::remotes(&format!("{}/*", peer_id.to_string()))?)?;
|
||||
Ok(
|
||||
NonEmpty::from_vec(remote_branches).map(|branches| Revisions {
|
||||
peer_id,
|
||||
@@ -143,7 +154,7 @@ where
|
||||
}
|
||||
|
||||
/// Provide the [`Revisions`] for the given `peer_id`, looking for the
|
||||
/// branches as [`RefScope::Local`].
|
||||
/// local branches.
|
||||
///
|
||||
/// If there are no branches then this returns `None`.
|
||||
///
|
||||
@@ -158,7 +169,7 @@ pub fn local<P, U>(
|
||||
where
|
||||
P: Clone + ToString,
|
||||
{
|
||||
let local_branches = repo.branch_names(RefScope::Local)?;
|
||||
let local_branches = repo.branch_names(&Glob::heads("*")?)?;
|
||||
let tags = repo.tag_names()?;
|
||||
Ok(
|
||||
NonEmpty::from_vec(local_branches).map(|branches| Revisions {
|
||||
|
||||
@@ -69,13 +69,12 @@ use std::str::FromStr;
|
||||
pub use git2::{self, Error as Git2Error, Time};
|
||||
pub use radicle_git_ext::Oid;
|
||||
|
||||
/// Provides ways of selecting a particular reference/revision.
|
||||
mod reference;
|
||||
pub use reference::{ParseError, Ref, Rev};
|
||||
|
||||
mod repo;
|
||||
pub use repo::{Repository, RepositoryRef};
|
||||
|
||||
mod glob;
|
||||
pub use glob::Glob;
|
||||
|
||||
mod history;
|
||||
pub use history::History;
|
||||
|
||||
@@ -177,21 +176,9 @@ impl Revision for &Tag {
|
||||
}
|
||||
}
|
||||
|
||||
impl Revision for &Rev {
|
||||
impl Revision for &TagName {
|
||||
fn object_id(&self, repo: &RepositoryRef) -> Result<Oid, Error> {
|
||||
match *self {
|
||||
Rev::Oid(oid) => Ok(*oid),
|
||||
Rev::Ref(r) => {
|
||||
let r = match repo.which_namespace()? {
|
||||
None => r.clone(),
|
||||
Some(namespace) => match r {
|
||||
Ref::Namespace { .. } => r.clone(),
|
||||
_ => r.clone().namespaced(namespace),
|
||||
},
|
||||
};
|
||||
let refname = format!("{}", r);
|
||||
Ok(repo.repo_ref.refname_to_id(&refname).map(Oid::from)?)
|
||||
},
|
||||
}
|
||||
let refname = repo.namespaced_refname(&self.refname())?;
|
||||
Ok(repo.repo_ref.refname_to_id(&refname).map(Oid::from)?)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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::{self, error::Error, ext, reference::Ref};
|
||||
use crate::vcs::git::{self, error::Error, ext};
|
||||
#[cfg(feature = "serialize")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{cmp::Ordering, convert::TryFrom, fmt, str};
|
||||
@@ -56,7 +56,7 @@ impl From<git2::BranchType> for BranchType {
|
||||
/// to fetch a branch.
|
||||
#[cfg_attr(feature = "serialize", derive(Deserialize, Serialize))]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct BranchName(pub(crate) String);
|
||||
pub struct BranchName(String);
|
||||
|
||||
impl fmt::Display for BranchName {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
@@ -65,7 +65,7 @@ impl fmt::Display for BranchName {
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for BranchName {
|
||||
type Error = str::Utf8Error;
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(name: &[u8]) -> Result<Self, Self::Error> {
|
||||
let name = str::from_utf8(name)?;
|
||||
@@ -114,18 +114,6 @@ impl Ord for Branch {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Branch> for Ref {
|
||||
fn from(other: Branch) -> Self {
|
||||
match other.locality {
|
||||
BranchType::Local => Self::LocalBranch { name: other.name },
|
||||
BranchType::Remote { name } => Self::RemoteBranch {
|
||||
name: other.name,
|
||||
remote: name.unwrap_or_else(|| "**".to_string()),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Branch {
|
||||
/// Helper to create a remote `Branch` with a name
|
||||
pub fn remote(name: &str, remote: &str) -> Self {
|
||||
@@ -147,11 +135,11 @@ impl Branch {
|
||||
|
||||
/// Get the name of the `Branch`.
|
||||
pub fn refname(&self) -> String {
|
||||
let branch_name = self.name.0.clone();
|
||||
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,
|
||||
None => branch_name.to_string(),
|
||||
Some(remote_name) => format!("refs/remotes/{}/{}", remote_name, branch_name),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
use crate::{
|
||||
file_system::{self, directory},
|
||||
vcs::git::{error::Error, Branch, RepositoryRef, Rev, Tag},
|
||||
vcs::git::{error::Error, Branch, RepositoryRef, Tag, TagName},
|
||||
};
|
||||
use radicle_git_ext::Oid;
|
||||
use std::{convert::TryFrom, str};
|
||||
@@ -216,7 +216,7 @@ impl ToCommit for &Tag {
|
||||
}
|
||||
}
|
||||
|
||||
impl ToCommit for &Rev {
|
||||
impl ToCommit for &TagName {
|
||||
fn to_commit(self, repo: &RepositoryRef) -> Result<Commit, Error> {
|
||||
repo.commit(self)
|
||||
}
|
||||
|
||||
@@ -88,4 +88,7 @@ pub enum Error {
|
||||
/// A wrapper around the generic [`git2::Error`].
|
||||
#[error(transparent)]
|
||||
Git(#[from] git2::Error),
|
||||
/// A wrapper around git-ref-format::Error
|
||||
#[error(transparent)]
|
||||
RefFormat(#[from] git_ref_format::Error),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
// This file is part of radicle-git
|
||||
// <https://github.com/radicle-dev/radicle-git>
|
||||
//
|
||||
// Copyright (C) 2022 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::Error;
|
||||
use git_ref_format::refspec::PatternString;
|
||||
use std::{convert::TryFrom, marker::PhantomData, str};
|
||||
|
||||
/// A collection of globs for T (a git reference type).
|
||||
pub struct Glob<T> {
|
||||
globs: Vec<PatternString>,
|
||||
glob_type: PhantomData<T>, // To support different methods for different T.
|
||||
}
|
||||
|
||||
impl<T> Glob<T> {
|
||||
/// Returns the globs.
|
||||
pub fn globs(&self) -> Vec<&str> {
|
||||
self.globs.iter().map(|g| g.as_str()).collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl<Namespace> Glob<Namespace> {
|
||||
/// Creates a `Glob` for namespaces.
|
||||
pub fn namespaces(glob: &str) -> Result<Self, Error> {
|
||||
let pattern = PatternString::try_from(format!("refs/namespaces/{}", glob))?;
|
||||
let globs = vec![pattern];
|
||||
Ok(Self {
|
||||
globs,
|
||||
glob_type: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
/// Adds namespaces patterns to existing `Glob`.
|
||||
pub fn and(mut self, glob: &str) -> Result<Self, Error> {
|
||||
let pattern = PatternString::try_from(format!("refs/namespaces/{}", glob))?;
|
||||
self.globs.push(pattern);
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Tag> Glob<Tag> {
|
||||
/// Creates a `Glob` for local tags.
|
||||
pub fn tags(glob: &str) -> Result<Self, Error> {
|
||||
let pattern = PatternString::try_from(format!("refs/tags/{}", glob))?;
|
||||
let globs = vec![pattern];
|
||||
Ok(Self {
|
||||
globs,
|
||||
glob_type: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
/// Updates a `Glob` to include other tags.
|
||||
pub fn and_tags(mut self, glob: &str) -> Result<Self, Error> {
|
||||
let pattern = PatternString::try_from(format!("refs/tags/{}", glob))?;
|
||||
self.globs.push(pattern);
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<Branch> Glob<Branch> {
|
||||
/// Creates a `Glob` for local branches.
|
||||
pub fn heads(glob: &str) -> Result<Self, Error> {
|
||||
let pattern = PatternString::try_from(format!("refs/heads/{}", glob))?;
|
||||
let globs = vec![pattern];
|
||||
Ok(Self {
|
||||
globs,
|
||||
glob_type: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a `Glob` for remote branches.
|
||||
pub fn remotes(glob: &str) -> Result<Self, Error> {
|
||||
let pattern = PatternString::try_from(format!("refs/remotes/{}", glob))?;
|
||||
let globs = vec![pattern];
|
||||
Ok(Self {
|
||||
globs,
|
||||
glob_type: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
/// Updates a `Glob` to include local branches.
|
||||
pub fn and_heads(mut self, glob: &str) -> Result<Self, Error> {
|
||||
let pattern = PatternString::try_from(format!("refs/heads/{}", glob))?;
|
||||
self.globs.push(pattern);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Updates a `Glob` to include remote branches.
|
||||
pub fn and_remotes(mut self, glob: &str) -> Result<Self, Error> {
|
||||
let pattern = PatternString::try_from(format!("refs/remotes/{}", glob))?;
|
||||
self.globs.push(pattern);
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
@@ -1,191 +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 std::{fmt, str};
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::vcs::git::{BranchName, Namespace, TagName};
|
||||
use radicle_git_ext::Oid;
|
||||
pub(super) mod glob;
|
||||
|
||||
/// A revision within the repository.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Rev {
|
||||
/// A reference to a branch or tag.
|
||||
Ref(Ref),
|
||||
/// A particular commit identifier.
|
||||
Oid(Oid),
|
||||
}
|
||||
|
||||
impl<R> From<R> for Rev
|
||||
where
|
||||
R: Into<Ref>,
|
||||
{
|
||||
fn from(other: R) -> Self {
|
||||
Self::Ref(other.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Oid> for Rev {
|
||||
fn from(other: Oid) -> Self {
|
||||
Self::Oid(other)
|
||||
}
|
||||
}
|
||||
|
||||
/// A structured way of referring to a git reference.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Ref {
|
||||
/// A git tag, which can be found under `.git/refs/tags/`.
|
||||
Tag {
|
||||
/// The name of the tag, e.g. `v1.0.0`.
|
||||
name: TagName,
|
||||
},
|
||||
/// A git branch, which can be found under `.git/refs/heads/`.
|
||||
LocalBranch {
|
||||
/// The name of the branch, e.g. `master`.
|
||||
name: BranchName,
|
||||
},
|
||||
/// A git branch, which can be found under `.git/refs/remotes/`.
|
||||
RemoteBranch {
|
||||
/// The remote name, e.g. `origin`.
|
||||
remote: String,
|
||||
/// The name of the branch, e.g. `master`.
|
||||
name: BranchName,
|
||||
},
|
||||
/// A git namespace, which can be found under `.git/refs/namespaces/`.
|
||||
///
|
||||
/// Note that namespaces can be nested.
|
||||
Namespace {
|
||||
/// The name value of the namespace.
|
||||
namespace: String,
|
||||
/// The reference under that namespace, e.g. The
|
||||
/// `refs/remotes/origin/master/ portion of `refs/namespaces/
|
||||
/// moi/refs/remotes/origin/master`.
|
||||
reference: Box<Ref>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Ref {
|
||||
/// Add a [`Namespace`] to a `Ref`.
|
||||
pub fn namespaced(self, Namespace { values: namespaces }: Namespace) -> Self {
|
||||
let mut ref_namespace = self;
|
||||
for namespace in namespaces.into_iter().rev() {
|
||||
ref_namespace = Self::Namespace {
|
||||
namespace,
|
||||
reference: Box::new(ref_namespace.clone()),
|
||||
};
|
||||
}
|
||||
|
||||
ref_namespace
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Ref {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Tag { name } => write!(f, "refs/tags/{}", name),
|
||||
Self::LocalBranch { name } => write!(f, "refs/heads/{}", name),
|
||||
Self::RemoteBranch { remote, name } => write!(f, "refs/remotes/{}/{}", remote, name),
|
||||
Self::Namespace {
|
||||
namespace,
|
||||
reference,
|
||||
} => write!(f, "refs/namespaces/{}/{}", namespace, reference),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error when parsing a ref.
|
||||
#[derive(Debug, PartialEq, Eq, Error)]
|
||||
pub enum ParseError {
|
||||
/// The parsed ref is malformed.
|
||||
#[error("the ref provided '{0}' was malformed")]
|
||||
MalformedRef(String),
|
||||
}
|
||||
|
||||
pub mod parser {
|
||||
use nom::{bytes, named, tag, IResult};
|
||||
|
||||
use crate::vcs::git::{BranchName, TagName};
|
||||
|
||||
use super::Ref;
|
||||
|
||||
const HEADS: &str = "refs/heads/";
|
||||
const REMOTES: &str = "refs/remotes/";
|
||||
const TAGS: &str = "refs/tags/";
|
||||
const NAMESPACES: &str = "refs/namespaces/";
|
||||
|
||||
named!(heads, tag!(HEADS));
|
||||
named!(remotes, tag!(REMOTES));
|
||||
named!(tags, tag!(TAGS));
|
||||
named!(namsespaces, tag!(NAMESPACES));
|
||||
|
||||
type Error<'a> = nom::Err<nom::error::Error<&'a str>>;
|
||||
|
||||
pub fn component(s: &str) -> IResult<&str, &str> {
|
||||
bytes::complete::take_till(|c| c == '/')(s).and_then(|(rest, component)| {
|
||||
bytes::complete::take(1u8)(rest).map(|(rest, _)| (rest, component))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn local(s: &str) -> Result<Ref, Error> {
|
||||
bytes::complete::tag(HEADS)(s).map(|(name, _)| Ref::LocalBranch {
|
||||
name: BranchName::new(name),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn remote(s: &str) -> Result<Ref, Error> {
|
||||
bytes::complete::tag(REMOTES)(s).and_then(|(rest, _)| {
|
||||
component(rest).map(|(rest, remote)| Ref::RemoteBranch {
|
||||
remote: remote.to_owned(),
|
||||
name: BranchName::new(rest),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn tag(s: &str) -> Result<Ref, Error> {
|
||||
bytes::complete::tag(TAGS)(s).map(|(name, _)| Ref::Tag {
|
||||
name: TagName::new(name),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn namespace(s: &str) -> Result<Ref, Error> {
|
||||
bytes::complete::tag(NAMESPACES)(s).and_then(|(rest, _)| {
|
||||
component(rest).and_then(|(rest, namespace)| {
|
||||
Ok(Ref::Namespace {
|
||||
namespace: namespace.to_owned(),
|
||||
reference: Box::new(parse(rest)?),
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse(s: &str) -> Result<Ref, nom::Err<nom::error::Error<&str>>> {
|
||||
local(s)
|
||||
.or_else(|_| remote(s))
|
||||
.or_else(|_| tag(s))
|
||||
.or_else(|_| namespace(s))
|
||||
}
|
||||
}
|
||||
|
||||
impl str::FromStr for Ref {
|
||||
type Err = ParseError;
|
||||
|
||||
fn from_str(reference: &str) -> Result<Self, Self::Err> {
|
||||
parser::parse(reference).map_err(|_| ParseError::MalformedRef(reference.to_owned()))
|
||||
}
|
||||
}
|
||||
@@ -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::{
|
||||
git::RefScope,
|
||||
vcs::git::{error, repo::RepositoryRef},
|
||||
};
|
||||
use either::Either;
|
||||
use std::fmt::{self, Write as _};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum RefGlob {
|
||||
/// When calling [`RefGlob::references`] this will return the references via
|
||||
/// the globs `refs/heads/*` and `refs/remotes/**/*`.
|
||||
Branch,
|
||||
/// When calling [`RefGlob::references`] this will return the references via
|
||||
/// the glob `refs/heads/*`.
|
||||
LocalBranch,
|
||||
/// When calling [`RefGlob::references`] this will return the references via
|
||||
/// either of the following globs:
|
||||
/// * `refs/remotes/**/*`
|
||||
/// * `refs/remotes/{remote}/*`
|
||||
RemoteBranch {
|
||||
/// If `remote` is `None` then the `**` wildcard will be used, otherwise
|
||||
/// the provided remote name will be used.
|
||||
remote: Option<String>,
|
||||
},
|
||||
/// When calling [`RefGlob::references`] this will return the references via
|
||||
/// the globs `refs/tags/*` and `refs/remotes/*/tags`
|
||||
Tag,
|
||||
/// When calling [`RefGlob::references`] this will return the references via
|
||||
/// the glob `refs/tags/*`.
|
||||
LocalTag,
|
||||
/// When calling [`RefGlob::references`] this will return the references via
|
||||
/// either of the following globs:
|
||||
/// * `refs/remotes/*/tags/*`
|
||||
/// * `refs/remotes/{remote}/tags/*`
|
||||
RemoteTag {
|
||||
/// If `remote` is `None` then the `*` wildcard will be used, otherwise
|
||||
/// the provided remote name will be used.
|
||||
remote: Option<String>,
|
||||
},
|
||||
/// refs/namespaces/**
|
||||
Namespace,
|
||||
}
|
||||
|
||||
/// Iterator chaining multiple [`git2::References`]
|
||||
#[must_use = "iterators are lazy and do nothing unless consumed"]
|
||||
pub struct References<'a> {
|
||||
inner: Vec<git2::References<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> References<'a> {
|
||||
pub fn iter(self) -> impl Iterator<Item = Result<git2::Reference<'a>, git2::Error>> {
|
||||
self.inner.into_iter().flatten()
|
||||
}
|
||||
}
|
||||
|
||||
impl RefGlob {
|
||||
pub fn branch(scope: RefScope) -> Self {
|
||||
match scope {
|
||||
RefScope::All => Self::Branch,
|
||||
RefScope::Local => Self::LocalBranch,
|
||||
RefScope::Remote { name } => Self::RemoteBranch { remote: name },
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tag(scope: RefScope) -> Self {
|
||||
match scope {
|
||||
RefScope::All => Self::Tag,
|
||||
RefScope::Local => Self::LocalTag,
|
||||
RefScope::Remote { name } => Self::RemoteTag { remote: name },
|
||||
}
|
||||
}
|
||||
|
||||
pub fn references<'a>(&self, repo: &RepositoryRef<'a>) -> Result<References<'a>, error::Error> {
|
||||
let namespace = repo
|
||||
.which_namespace()?
|
||||
.map_or(Either::Left(std::iter::empty()), |namespace| {
|
||||
Either::Right(namespace.values.into_iter())
|
||||
});
|
||||
self.with_namespace_glob(namespace, repo)
|
||||
}
|
||||
|
||||
fn with_namespace_glob<'a>(
|
||||
&self,
|
||||
namespace: impl Iterator<Item = String>,
|
||||
repo: &RepositoryRef<'a>,
|
||||
) -> Result<References<'a>, error::Error> {
|
||||
let mut namespace_glob = "".to_string();
|
||||
for n in namespace {
|
||||
let _ = write!(namespace_glob, "refs/namespaces/{n}/");
|
||||
}
|
||||
|
||||
Ok(match self {
|
||||
Self::Branch => {
|
||||
let remotes = repo.repo_ref.references_glob(&format!(
|
||||
"{}{}",
|
||||
namespace_glob,
|
||||
Self::RemoteBranch { remote: None }
|
||||
))?;
|
||||
|
||||
let locals = repo.repo_ref.references_glob(&format!(
|
||||
"{}{}",
|
||||
namespace_glob,
|
||||
&Self::LocalBranch
|
||||
))?;
|
||||
References {
|
||||
inner: vec![remotes, locals],
|
||||
}
|
||||
},
|
||||
Self::Tag => {
|
||||
let remotes = repo.repo_ref.references_glob(&format!(
|
||||
"{}{}",
|
||||
namespace_glob,
|
||||
Self::RemoteTag { remote: None }
|
||||
))?;
|
||||
|
||||
let locals = repo.repo_ref.references_glob(&format!(
|
||||
"{}{}",
|
||||
namespace_glob,
|
||||
&Self::LocalTag
|
||||
))?;
|
||||
References {
|
||||
inner: vec![remotes, locals],
|
||||
}
|
||||
},
|
||||
other => References {
|
||||
inner: vec![repo
|
||||
.repo_ref
|
||||
.references_glob(&format!("{}{}", namespace_glob, other,))?],
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for RefGlob {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::LocalBranch => write!(f, "refs/heads/*"),
|
||||
Self::RemoteBranch { remote } => {
|
||||
write!(f, "refs/remotes/")?;
|
||||
match remote {
|
||||
None => write!(f, "**/*"),
|
||||
Some(remote) => write!(f, "{}/*", remote),
|
||||
}
|
||||
},
|
||||
Self::LocalTag => write!(f, "refs/tags/*"),
|
||||
Self::RemoteTag { remote } => {
|
||||
let remote = match remote {
|
||||
Some(remote) => remote.as_ref(),
|
||||
None => "*",
|
||||
};
|
||||
write!(f, "refs/remotes/{}/tags/*", remote)
|
||||
},
|
||||
// Note: the glob below would be used, but libgit doesn't care for union globs.
|
||||
// write!(f, "refs/{{remotes/**/*,heads/*}}")
|
||||
Self::Branch | Self::Tag => {
|
||||
panic!("{}",
|
||||
"fatal: `Display` should not be called on `RefGlob::Branch` or `RefGlob::Tag` Since
|
||||
this `enum` is private to the repository, it should not be called from the outside.
|
||||
Unfortunately, libgit does not support union of globs otherwise this would display
|
||||
refs/{remotes/**/*,heads/*}"
|
||||
)
|
||||
},
|
||||
Self::Namespace => write!(f, "refs/namespaces/**"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,13 +21,12 @@ use crate::{
|
||||
file_system::{directory, DirectoryContents, Label},
|
||||
vcs::git::{
|
||||
error::*,
|
||||
reference::{glob::RefGlob, Rev},
|
||||
Branch,
|
||||
BranchName,
|
||||
Commit,
|
||||
Glob,
|
||||
History,
|
||||
Namespace,
|
||||
RefScope,
|
||||
Revision,
|
||||
Signature,
|
||||
Stats,
|
||||
@@ -38,7 +37,7 @@ use crate::{
|
||||
use directory::Directory;
|
||||
use radicle_git_ext::Oid;
|
||||
use std::{
|
||||
collections::{BTreeSet, HashSet},
|
||||
collections::{btree_set, BTreeSet},
|
||||
convert::TryFrom,
|
||||
path::PathBuf,
|
||||
str,
|
||||
@@ -75,6 +74,67 @@ 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> {
|
||||
@@ -84,56 +144,52 @@ impl<'a> RepositoryRef<'a> {
|
||||
.transpose()
|
||||
}
|
||||
|
||||
/// List the branches within a repository, filtering out ones that do not
|
||||
/// parse correctly.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// * [`Error::Git`]
|
||||
pub fn list_branches(&self, scope: RefScope) -> Result<Vec<Branch>, Error> {
|
||||
RefGlob::branch(scope)
|
||||
.references(self)?
|
||||
.iter()
|
||||
.try_fold(vec![], |mut acc, reference| {
|
||||
let branch = Branch::try_from(reference?)?;
|
||||
acc.push(branch);
|
||||
Ok(acc)
|
||||
})
|
||||
/// 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,
|
||||
};
|
||||
for glob in pattern.globs().iter() {
|
||||
let namespaced = self.namespaced_refname(glob)?;
|
||||
let references = self.repo_ref.references_glob(&namespaced)?;
|
||||
branches.references.push(references);
|
||||
}
|
||||
Ok(branches)
|
||||
}
|
||||
|
||||
/// List the tags within a repository, filtering out ones that do not parse
|
||||
/// correctly.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// * [`Error::Git`]
|
||||
pub fn list_tags(&self, scope: RefScope) -> Result<Vec<Tag>, Error> {
|
||||
RefGlob::tag(scope)
|
||||
.references(self)?
|
||||
.iter()
|
||||
.try_fold(vec![], |mut acc, reference| {
|
||||
let tag = Tag::try_from(reference?)?;
|
||||
acc.push(tag);
|
||||
Ok(acc)
|
||||
})
|
||||
/// 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,
|
||||
};
|
||||
for glob in pattern.globs().iter() {
|
||||
let namespaced = self.namespaced_refname(glob)?;
|
||||
let references = self.repo_ref.references_glob(&namespaced)?;
|
||||
tags.references.push(references);
|
||||
}
|
||||
Ok(tags)
|
||||
}
|
||||
|
||||
/// List the namespaces within a repository, filtering out ones that do not
|
||||
/// parse correctly.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// * [`Error::Git`]
|
||||
pub fn list_namespaces(&self) -> Result<Vec<Namespace>, Error> {
|
||||
let namespaces: Result<HashSet<Namespace>, Error> = RefGlob::Namespace
|
||||
.references(self)?
|
||||
.iter()
|
||||
.try_fold(HashSet::new(), |mut acc, reference| {
|
||||
let namespace = Namespace::try_from(reference?)?;
|
||||
acc.insert(namespace);
|
||||
Ok(acc)
|
||||
});
|
||||
Ok(namespaces?.into_iter().collect())
|
||||
/// Returns an iterator of namespaces that match `pattern`.
|
||||
pub fn namespaces(&self, pattern: &Glob<Namespace>) -> Result<Namespaces, Error> {
|
||||
let mut set = BTreeSet::new();
|
||||
for glob in pattern.globs().iter() {
|
||||
let new_set = self
|
||||
.repo_ref
|
||||
.references_glob(glob)?
|
||||
.map(|reference| {
|
||||
reference
|
||||
.map_err(Error::Git)
|
||||
.and_then(|r| Namespace::try_from(r).map_err(|_| Error::EmptyNamespace))
|
||||
})
|
||||
.collect::<Result<BTreeSet<Namespace>, Error>>()?;
|
||||
set.extend(new_set);
|
||||
}
|
||||
Ok(Namespaces {
|
||||
namespaces: set.into_iter(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the [`Diff`] between two commits.
|
||||
@@ -174,7 +230,11 @@ impl<'a> RepositoryRef<'a> {
|
||||
|
||||
/// Returns the last commit, if exists, for a `path` in the history of
|
||||
/// `rev`.
|
||||
pub fn last_commit(&self, path: file_system::Path, rev: &Rev) -> Result<Option<Commit>, Error> {
|
||||
pub fn last_commit<C: ToCommit>(
|
||||
&self,
|
||||
path: file_system::Path,
|
||||
rev: C,
|
||||
) -> Result<Option<Commit>, Error> {
|
||||
let history = self.history(rev)?;
|
||||
history.by_path(path).next().transpose()
|
||||
}
|
||||
@@ -190,7 +250,7 @@ impl<'a> RepositoryRef<'a> {
|
||||
|
||||
/// Gets stats of `commit`.
|
||||
pub fn get_commit_stats<C: ToCommit>(&self, commit: C) -> Result<Stats, Error> {
|
||||
let branches = self.list_branches(RefScope::Local)?.len();
|
||||
let branches = self.branches(&Glob::heads("*")?)?.count();
|
||||
let history = self.history(commit)?;
|
||||
let mut commits = 0;
|
||||
|
||||
@@ -212,13 +272,10 @@ impl<'a> RepositoryRef<'a> {
|
||||
}
|
||||
|
||||
/// Lists branch names with `filter`.
|
||||
pub fn branch_names(&self, filter: RefScope) -> Result<Vec<BranchName>, Error> {
|
||||
let mut branches = self
|
||||
.list_branches(filter)?
|
||||
.into_iter()
|
||||
.map(|b| b.name)
|
||||
.collect::<Vec<BranchName>>();
|
||||
|
||||
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?;
|
||||
branches.sort();
|
||||
|
||||
Ok(branches)
|
||||
@@ -226,12 +283,10 @@ impl<'a> RepositoryRef<'a> {
|
||||
|
||||
/// Lists tag names in the local RefScope.
|
||||
pub fn tag_names(&self) -> Result<Vec<TagName>, Error> {
|
||||
let tag_names = self.list_tags(RefScope::Local)?;
|
||||
let mut tags: Vec<TagName> = tag_names
|
||||
.into_iter()
|
||||
.map(|tag_name| tag_name.name())
|
||||
.collect();
|
||||
|
||||
let mut tags = self
|
||||
.tags(&Glob::tags("*")?)?
|
||||
.map(|t| t.map(|t| t.name()))
|
||||
.collect::<Result<Vec<TagName>, Error>>()?;
|
||||
tags.sort();
|
||||
|
||||
Ok(tags)
|
||||
@@ -258,6 +313,11 @@ impl<'a> RepositoryRef<'a> {
|
||||
Ok(fullname)
|
||||
}
|
||||
|
||||
pub(crate) fn refname_to_oid(&self, refname: &str) -> Result<Oid, Error> {
|
||||
let oid = self.repo_ref.refname_to_id(refname)?;
|
||||
Ok(oid.into())
|
||||
}
|
||||
|
||||
/// Get a particular `git2::Commit` of `oid`.
|
||||
pub(crate) fn get_git2_commit(&self, oid: Oid) -> Result<git2::Commit, Error> {
|
||||
self.repo_ref.find_commit(oid.into()).map_err(Error::Git)
|
||||
@@ -292,23 +352,16 @@ impl<'a> RepositoryRef<'a> {
|
||||
}
|
||||
|
||||
/// Lists branches that are reachable from `oid`.
|
||||
pub fn revision_branches(&self, oid: &Oid) -> Result<Vec<Branch>, Error> {
|
||||
let local = RefGlob::LocalBranch.references(self)?;
|
||||
let remote = RefGlob::RemoteBranch { remote: None }.references(self)?;
|
||||
let mut references = local.iter().chain(remote.iter());
|
||||
|
||||
pub fn revision_branches(&self, oid: &Oid, glob: &Glob<Branch>) -> Result<Vec<Branch>, Error> {
|
||||
let mut contained_branches = vec![];
|
||||
|
||||
references.try_for_each(|reference| {
|
||||
let reference = reference?;
|
||||
self.reachable_from(&reference, oid).and_then(|contains| {
|
||||
if contains {
|
||||
let branch = Branch::try_from(reference)?;
|
||||
contained_branches.push(branch);
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
})?;
|
||||
for branch in self.branches(glob)? {
|
||||
let branch = branch?;
|
||||
let namespaced = self.namespaced_refname(&branch.refname())?;
|
||||
let reference = self.repo_ref.find_reference(&namespaced)?;
|
||||
if self.reachable_from(&reference, oid)? {
|
||||
contained_branches.push(branch);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(contained_branches)
|
||||
}
|
||||
|
||||
@@ -15,14 +15,15 @@
|
||||
// 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, reference::Ref, Author};
|
||||
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(String);
|
||||
pub struct TagName(RefString);
|
||||
|
||||
impl fmt::Display for TagName {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
@@ -31,7 +32,7 @@ impl fmt::Display for TagName {
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for TagName {
|
||||
type Error = str::Utf8Error;
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(name: &[u8]) -> Result<Self, Self::Error> {
|
||||
let name = str::from_utf8(name)?;
|
||||
@@ -39,26 +40,27 @@ impl TryFrom<&[u8]> for TagName {
|
||||
Ok(stripped) => stripped,
|
||||
Err(original) => original,
|
||||
};
|
||||
Ok(Self(short_name))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TagName> for Ref {
|
||||
fn from(other: TagName) -> Self {
|
||||
Self::Tag { name: other }
|
||||
let refstring = RefString::try_from(short_name)?;
|
||||
Ok(Self(refstring))
|
||||
}
|
||||
}
|
||||
|
||||
impl TagName {
|
||||
/// Create a new `TagName`.
|
||||
pub fn new(name: &str) -> Self {
|
||||
TagName(name.into())
|
||||
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`].
|
||||
@@ -109,12 +111,12 @@ impl Tag {
|
||||
|
||||
/// Returns the full ref name of the tag.
|
||||
pub fn refname(&self) -> String {
|
||||
format!("refs/tags/{}", self.name().name())
|
||||
self.name().refname()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'repo> TryFrom<git2::Tag<'repo>> for Tag {
|
||||
type Error = str::Utf8Error;
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(tag: git2::Tag) -> Result<Self, Self::Error> {
|
||||
let id = tag.id().into();
|
||||
@@ -152,7 +154,7 @@ impl<'repo> TryFrom<git2::Reference<'repo>> for Tag {
|
||||
let mut split = name.0.splitn(2, '/');
|
||||
let remote = split.next().map(|x| x.to_owned());
|
||||
let name = split.next().unwrap();
|
||||
(remote, TagName(name.to_owned()))
|
||||
(remote, TagName::new(name)?)
|
||||
} else {
|
||||
(None, name)
|
||||
};
|
||||
|
||||
+80
-136
@@ -8,7 +8,7 @@ use radicle_surf::git::{Author, BranchType, Commit};
|
||||
use radicle_surf::{
|
||||
diff::*,
|
||||
file_system::{unsound, DirectoryContents, Path},
|
||||
git::{error::Error, Branch, BranchName, Namespace, Oid, RefScope, Repository, Rev, TagName},
|
||||
git::{error::Error, Branch, Glob, Namespace, Oid, Repository, TagName},
|
||||
};
|
||||
|
||||
const GIT_PLATINUM: &str = "../data/git-platinum";
|
||||
@@ -54,15 +54,17 @@ mod namespace {
|
||||
assert_eq!(history.head(), history_feature.head());
|
||||
|
||||
let expected_branches: Vec<Branch> = vec![Branch::local("feature/#1194")];
|
||||
let mut branches = repo.list_branches(RefScope::Local)?;
|
||||
let mut branches = repo
|
||||
.branches(&Glob::heads("*")?)?
|
||||
.collect::<Result<Vec<Branch>, Error>>()?;
|
||||
branches.sort();
|
||||
|
||||
assert_eq!(expected_branches, branches);
|
||||
|
||||
let expected_branches: Vec<Branch> = vec![Branch::remote("feature/#1194", "fein")];
|
||||
let mut branches = repo.list_branches(RefScope::Remote {
|
||||
name: Some("fein".to_string()),
|
||||
})?;
|
||||
let mut branches = repo
|
||||
.branches(&Glob::remotes("fein/*")?)?
|
||||
.collect::<Result<Vec<Branch>, Error>>()?;
|
||||
branches.sort();
|
||||
|
||||
assert_eq!(expected_branches, branches);
|
||||
@@ -89,7 +91,9 @@ mod namespace {
|
||||
assert_eq!(history.head(), golden_history.head());
|
||||
|
||||
let expected_branches: Vec<Branch> = vec![Branch::local("banana"), Branch::local("master")];
|
||||
let mut branches = repo.list_branches(RefScope::Local)?;
|
||||
let mut branches = repo
|
||||
.branches(&Glob::heads("*")?)?
|
||||
.collect::<Result<Vec<Branch>, Error>>()?;
|
||||
branches.sort();
|
||||
|
||||
assert_eq!(expected_branches, branches);
|
||||
@@ -99,9 +103,9 @@ mod namespace {
|
||||
Branch::remote("heelflip", "kickflip"),
|
||||
Branch::remote("v0.1.0", "kickflip"),
|
||||
];
|
||||
let mut branches = repo.list_branches(RefScope::Remote {
|
||||
name: Some("kickflip".to_string()),
|
||||
})?;
|
||||
let mut branches = repo
|
||||
.branches(&Glob::remotes("kickflip/*")?)?
|
||||
.collect::<Result<Vec<Branch>, Error>>()?;
|
||||
branches.sort();
|
||||
|
||||
assert_eq!(expected_branches, branches);
|
||||
@@ -126,7 +130,9 @@ mod namespace {
|
||||
assert_ne!(history.head(), silver_history.head());
|
||||
|
||||
let expected_branches: Vec<Branch> = vec![Branch::local("master")];
|
||||
let mut branches = repo.list_branches(RefScope::All)?;
|
||||
let mut branches = repo
|
||||
.branches(&Glob::heads("*")?.and_remotes("*")?)?
|
||||
.collect::<Result<Vec<Branch>, Error>>()?;
|
||||
branches.sort();
|
||||
|
||||
assert_eq!(expected_branches, branches);
|
||||
@@ -180,8 +186,8 @@ mod rev {
|
||||
fn commit() -> Result<(), Error> {
|
||||
let repo = Repository::new(GIT_PLATINUM)?;
|
||||
let repo = repo.as_ref();
|
||||
let rev: Rev = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?.into();
|
||||
let mut history = repo.history(&rev)?;
|
||||
let rev = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
|
||||
let mut history = repo.history(rev)?;
|
||||
|
||||
let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
|
||||
assert!(history.any(|commit| commit.unwrap().id == commit1));
|
||||
@@ -193,8 +199,8 @@ mod rev {
|
||||
fn commit_parents() -> Result<(), Error> {
|
||||
let repo = Repository::new(GIT_PLATINUM)?;
|
||||
let repo = repo.as_ref();
|
||||
let rev: Rev = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?.into();
|
||||
let history = repo.history(&rev)?;
|
||||
let rev = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
|
||||
let history = repo.history(rev)?;
|
||||
let commit = history.head();
|
||||
|
||||
assert_eq!(
|
||||
@@ -209,8 +215,8 @@ mod rev {
|
||||
fn commit_short() -> Result<(), Error> {
|
||||
let repo = Repository::new(GIT_PLATINUM)?;
|
||||
let repo = repo.as_ref();
|
||||
let rev: Rev = repo.oid("3873745c8")?.into();
|
||||
let mut history = repo.history(&rev)?;
|
||||
let rev = repo.oid("3873745c8")?;
|
||||
let mut history = repo.history(rev)?;
|
||||
|
||||
let commit1 = Oid::from_str("3873745c8f6ffb45c990eb23b491d4b4b6182f95")?;
|
||||
assert!(history.any(|commit| commit.unwrap().id == commit1));
|
||||
@@ -222,7 +228,7 @@ mod rev {
|
||||
fn tag() -> Result<(), Error> {
|
||||
let repo = Repository::new(GIT_PLATINUM)?;
|
||||
let repo = repo.as_ref();
|
||||
let rev: Rev = TagName::new("v0.2.0").into();
|
||||
let rev = TagName::new("v0.2.0")?;
|
||||
let history = repo.history(&rev)?;
|
||||
|
||||
let commit1 = Oid::from_str("2429f097664f9af0c5b7b389ab998b2199ffa977")?;
|
||||
@@ -245,12 +251,11 @@ mod last_commit {
|
||||
Oid::from_str("d3464e33d75c75c99bfb90fa2e9d16efc0b7d0e3").expect("Failed to parse SHA");
|
||||
|
||||
// memory.rs is commited later so it should not exist here.
|
||||
let rev: Rev = oid.into();
|
||||
let memory_last_commit_oid = repo
|
||||
.as_ref()
|
||||
.last_commit(
|
||||
Path::with_root(&[unsound::label::new("src"), unsound::label::new("memory.rs")]),
|
||||
&rev,
|
||||
oid,
|
||||
)
|
||||
.expect("Failed to get last commit")
|
||||
.map(|commit| commit.id);
|
||||
@@ -260,7 +265,7 @@ mod last_commit {
|
||||
// README.md exists in this commit.
|
||||
let readme_last_commit = repo
|
||||
.as_ref()
|
||||
.last_commit(Path::with_root(&[unsound::label::new("README.md")]), &rev)
|
||||
.last_commit(Path::with_root(&[unsound::label::new("README.md")]), oid)
|
||||
.expect("Failed to get last commit")
|
||||
.map(|commit| commit.id);
|
||||
|
||||
@@ -274,13 +279,12 @@ mod last_commit {
|
||||
// Check that last commit is the actual last commit even if head commit differs.
|
||||
let oid =
|
||||
Oid::from_str("19bec071db6474af89c866a1bd0e4b1ff76e2b97").expect("Could not parse SHA");
|
||||
let rev: Rev = oid.into();
|
||||
|
||||
let expected_commit_id = Oid::from_str("f3a089488f4cfd1a240a9c01b3fcc4c34a4e97b2").unwrap();
|
||||
|
||||
let folder_svelte = repo
|
||||
.as_ref()
|
||||
.last_commit(unsound::path::new("~/examples/Folder.svelte"), &rev)
|
||||
.last_commit(unsound::path::new("~/examples/Folder.svelte"), oid)
|
||||
.expect("Failed to get last commit")
|
||||
.map(|commit| commit.id);
|
||||
|
||||
@@ -294,7 +298,6 @@ mod last_commit {
|
||||
// Check that last commit is the actual last commit even if head commit differs.
|
||||
let oid =
|
||||
Oid::from_str("19bec071db6474af89c866a1bd0e4b1ff76e2b97").expect("Failed to parse SHA");
|
||||
let rev: Rev = oid.into();
|
||||
|
||||
let expected_commit_id = Oid::from_str("2429f097664f9af0c5b7b389ab998b2199ffa977").unwrap();
|
||||
|
||||
@@ -302,7 +305,7 @@ mod last_commit {
|
||||
.as_ref()
|
||||
.last_commit(
|
||||
unsound::path::new("~/this/is/a/really/deeply/nested/directory/tree"),
|
||||
&rev,
|
||||
oid,
|
||||
)
|
||||
.expect("Failed to get last commit")
|
||||
.map(|commit| commit.id);
|
||||
@@ -319,20 +322,19 @@ mod last_commit {
|
||||
// Check that last commit is the actual last commit even if head commit differs.
|
||||
let oid =
|
||||
Oid::from_str("a0dd9122d33dff2a35f564d564db127152c88e02").expect("Failed to parse SHA");
|
||||
let rev: Rev = oid.into();
|
||||
|
||||
let expected_commit_id = Oid::from_str("a0dd9122d33dff2a35f564d564db127152c88e02").unwrap();
|
||||
|
||||
let backslash_commit_id = repo
|
||||
.as_ref()
|
||||
.last_commit(unsound::path::new("~/special/faux\\path"), &rev)
|
||||
.last_commit(unsound::path::new("~/special/faux\\path"), oid)
|
||||
.expect("Failed to get last commit")
|
||||
.map(|commit| commit.id);
|
||||
assert_eq!(backslash_commit_id, Some(expected_commit_id));
|
||||
|
||||
let ogre_commit_id = repo
|
||||
.as_ref()
|
||||
.last_commit(unsound::path::new("~/special/👹👹👹"), &rev)
|
||||
.last_commit(unsound::path::new("~/special/👹👹👹"), oid)
|
||||
.expect("Failed to get last commit")
|
||||
.map(|commit| commit.id);
|
||||
assert_eq!(ogre_commit_id, Some(expected_commit_id));
|
||||
@@ -342,7 +344,7 @@ mod last_commit {
|
||||
fn root() {
|
||||
let repo = Repository::new(GIT_PLATINUM)
|
||||
.expect("Could not retrieve ./data/git-platinum as git repository");
|
||||
let rev: Rev = Branch::local("master").into();
|
||||
let rev = Branch::local("master");
|
||||
let root_last_commit_id = repo
|
||||
.as_ref()
|
||||
.last_commit(Path::root(), &rev)
|
||||
@@ -567,6 +569,8 @@ mod diff {
|
||||
|
||||
#[cfg(test)]
|
||||
mod threading {
|
||||
use radicle_surf::git::Glob;
|
||||
|
||||
use super::*;
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
|
||||
@@ -574,17 +578,20 @@ mod threading {
|
||||
fn basic_test() -> Result<(), Error> {
|
||||
let shared_repo = Mutex::new(Repository::new(GIT_PLATINUM)?);
|
||||
let locked_repo: MutexGuard<Repository> = shared_repo.lock().unwrap();
|
||||
let mut branches = locked_repo.as_ref().list_branches(RefScope::All)?;
|
||||
let mut branches = locked_repo
|
||||
.as_ref()
|
||||
.branches(&Glob::heads("*")?.and_remotes("*")?)?
|
||||
.collect::<Result<Vec<Branch>, Error>>()?;
|
||||
branches.sort();
|
||||
|
||||
assert_eq!(
|
||||
branches,
|
||||
vec![
|
||||
Branch::remote("HEAD", "origin"),
|
||||
Branch::remote("dev", "origin"),
|
||||
Branch::local("dev"),
|
||||
Branch::remote("master", "origin"),
|
||||
Branch::remote("dev", "origin"),
|
||||
Branch::local("master"),
|
||||
Branch::remote("master", "origin"),
|
||||
Branch::remote("orange/pineapple", "banana"),
|
||||
Branch::remote("pineapple", "banana"),
|
||||
]
|
||||
@@ -709,105 +716,52 @@ mod ext {
|
||||
#[cfg(test)]
|
||||
mod reference {
|
||||
use super::*;
|
||||
use radicle_surf::vcs::git::{ParseError, Ref};
|
||||
use std::str::FromStr;
|
||||
use radicle_surf::vcs::git::{Glob, Tag};
|
||||
|
||||
#[test]
|
||||
fn parse_ref() -> Result<(), ParseError> {
|
||||
assert_eq!(
|
||||
Ref::from_str("refs/remotes/origin/master"),
|
||||
Ok(Ref::RemoteBranch {
|
||||
remote: "origin".to_string(),
|
||||
name: BranchName::new("master")
|
||||
})
|
||||
);
|
||||
fn test_branches() {
|
||||
let repo = Repository::new(GIT_PLATINUM).unwrap();
|
||||
let repo = repo.as_ref();
|
||||
let branches = repo.branches(&Glob::heads("*").unwrap()).unwrap();
|
||||
for b in branches {
|
||||
println!("{}", b.unwrap().name);
|
||||
}
|
||||
let branches = repo
|
||||
.branches(&Glob::heads("*").unwrap().and_remotes("banana/*").unwrap())
|
||||
.unwrap();
|
||||
for b in branches {
|
||||
println!("{}", b.unwrap().refname());
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
Ref::from_str("refs/heads/master"),
|
||||
Ok(Ref::LocalBranch {
|
||||
name: BranchName::new("master"),
|
||||
})
|
||||
);
|
||||
#[test]
|
||||
fn test_tag_snapshot() {
|
||||
let repo = Repository::new(GIT_PLATINUM).unwrap();
|
||||
let repo_ref = repo.as_ref();
|
||||
let tags = repo_ref
|
||||
.tags(&Glob::tags("*").unwrap())
|
||||
.unwrap()
|
||||
.collect::<Result<Vec<Tag>, Error>>()
|
||||
.unwrap();
|
||||
assert_eq!(tags.len(), 6);
|
||||
let root_dir = repo_ref.snapshot(&tags[0]).unwrap();
|
||||
assert_eq!(root_dir.contents().count(), 1);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
Ref::from_str("refs/heads/i-am-hyphenated"),
|
||||
Ok(Ref::LocalBranch {
|
||||
name: BranchName::new("i-am-hyphenated"),
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Ref::from_str("refs/heads/prefix/i-am-hyphenated"),
|
||||
Ok(Ref::LocalBranch {
|
||||
name: BranchName::new("prefix/i-am-hyphenated"),
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Ref::from_str("refs/tags/v0.0.1"),
|
||||
Ok(Ref::Tag {
|
||||
name: TagName::new("v0.0.1")
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Ref::from_str("refs/namespaces/moi/refs/remotes/origin/master"),
|
||||
Ok(Ref::Namespace {
|
||||
namespace: "moi".to_string(),
|
||||
reference: Box::new(Ref::RemoteBranch {
|
||||
remote: "origin".to_string(),
|
||||
name: BranchName::new("master")
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Ref::from_str("refs/namespaces/moi/refs/namespaces/toi/refs/tags/v1.0.0"),
|
||||
Ok(Ref::Namespace {
|
||||
namespace: "moi".to_string(),
|
||||
reference: Box::new(Ref::Namespace {
|
||||
namespace: "toi".to_string(),
|
||||
reference: Box::new(Ref::Tag {
|
||||
name: TagName::new("v1.0.0")
|
||||
})
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Ref::from_str("refs/namespaces/me/refs/heads/feature/#1194"),
|
||||
Ok(Ref::Namespace {
|
||||
namespace: "me".to_string(),
|
||||
reference: Box::new(Ref::LocalBranch {
|
||||
name: BranchName::new("feature/#1194"),
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Ref::from_str("refs/namespaces/me/refs/remotes/fein/heads/feature/#1194"),
|
||||
Ok(Ref::Namespace {
|
||||
namespace: "me".to_string(),
|
||||
reference: Box::new(Ref::RemoteBranch {
|
||||
remote: "fein".to_string(),
|
||||
name: BranchName::new("heads/feature/#1194"),
|
||||
})
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Ref::from_str("refs/remotes/master"),
|
||||
Err(ParseError::MalformedRef("refs/remotes/master".to_owned())),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Ref::from_str("refs/namespaces/refs/remotes/origin/master"),
|
||||
Err(ParseError::MalformedRef(
|
||||
"refs/namespaces/refs/remotes/origin/master".to_owned()
|
||||
)),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
#[test]
|
||||
fn test_namespaces() {
|
||||
let repo = Repository::new(GIT_PLATINUM).unwrap();
|
||||
let repo = repo.as_ref();
|
||||
let namespaces = repo.namespaces(&Glob::namespaces("*").unwrap()).unwrap();
|
||||
assert_eq!(namespaces.count(), 3);
|
||||
let namespaces = repo
|
||||
.namespaces(&Glob::namespaces("golden/*").unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(namespaces.count(), 2);
|
||||
let namespaces = repo
|
||||
.namespaces(&Glob::namespaces("golden/*").unwrap().and("me/*").unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(namespaces.count(), 3);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -840,16 +794,6 @@ mod code_browsing {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tag_snapshot() {
|
||||
let repo = Repository::new(GIT_PLATINUM).unwrap();
|
||||
let repo_ref = repo.as_ref();
|
||||
let tags = repo_ref.list_tags(RefScope::Local).unwrap();
|
||||
assert_eq!(tags.len(), 6);
|
||||
let root_dir = repo_ref.snapshot(&tags[0]).unwrap();
|
||||
assert_eq!(root_dir.contents().count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_file_history() {
|
||||
let repo = Repository::new(GIT_PLATINUM).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user