pub mod canonical;
pub mod raw;
use std::io;
use std::path::Path;
use std::process::Command;
use std::str::FromStr;
pub use radicle_oid::{Oid, str::ParseOidError};
pub extern crate radicle_git_ref_format as fmt;
use crate::crypto::PublicKey;
use crate::node::Alias;
use crate::rad;
use crate::storage::RemoteId;
pub use crate::storage::git::transport::local::Url;
use raw::ErrorExt as _;
pub type BranchName = crate::git::fmt::RefString;
/// Default port of the `git` transport protocol.
pub const PROTOCOL_PORT: u16 = 9418;
/// Minimum required git version.
pub const VERSION_REQUIRED: Version = Version {
major: 2,
minor: 31,
patch: 0,
};
/// A parsed git version.
#[derive(PartialEq, Eq, Debug, PartialOrd, Ord)]
pub struct Version {
pub major: u8,
pub minor: u8,
pub patch: u8,
}
impl std::fmt::Display for Version {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
/// Verbosity level for Git commands.
#[derive(Default, Clone, Copy)]
pub struct Verbosity(i8);
impl Verbosity {
/// Transform into a command line flag, helpful for passing to invocations
/// of `git`.
///
/// See <https://github.com/git/git/blob/c44beea485f0f2feaf460e2ac87fdd5608d63cf0/builtin/pull.c#L264-L276>
pub fn into_flag(&self) -> Option<String> {
const FLAG_PREFIX: &str = "-";
const FLAG_QUIET: &str = "q";
const FLAG_VERBOSE: &str = "v";
let repetitions = self.0.unsigned_abs() as usize;
if repetitions == 0 {
return None;
}
let flag = if self.0 > 0 { FLAG_VERBOSE } else { FLAG_QUIET };
Some(FLAG_PREFIX.to_string() + &flag.repeat(repetitions))
}
/// Clamps verbosity to a range, as some commands only accept a specific
/// number of repetitions.
fn clamp(self, min: i8, max: i8) -> Self {
Self(self.0.clamp(min, max))
}
/// Clamps verbosity to at most `-v` or `-q`, as some commands do not accept
/// repetitions.
pub fn clamp_one(self) -> Self {
self.clamp(-1, 1)
}
}
impl From<i8> for Verbosity {
fn from(v: i8) -> Self {
Self(v)
}
}
#[derive(thiserror::Error, Debug)]
pub enum VersionError {
#[error("malformed git version string")]
Malformed,
#[error("malformed git version string: {0}")]
ParseInt(#[from] std::num::ParseIntError),
#[error("malformed git version string: {0}")]
Utf8(#[from] std::string::FromUtf8Error),
#[error("error retrieving git version: {0}")]
Io(#[from] io::Error),
#[error("error retrieving git version: {0}")]
Other(String),
}
impl std::str::FromStr for Version {
type Err = VersionError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let rest = input
.strip_prefix("git version ")
.ok_or(VersionError::Malformed)?;
let rest = rest.split(' ').next().ok_or(VersionError::Malformed)?;
let rest = rest.trim_end();
let mut parts = rest.split('.');
let major = parts.next().ok_or(VersionError::Malformed)?.parse()?;
let minor = parts.next().ok_or(VersionError::Malformed)?.parse()?;
let patch = match parts.next() {
None => 0,
Some(patch) => patch.parse()?,
};
Ok(Self {
major,
minor,
patch,
})
}
}
/// Get the system's git version.
pub fn version() -> Result<Version, VersionError> {
let output = Command::new("git").arg("version").output()?;
if output.status.success() {
let output = String::from_utf8(output.stdout)?;
let version = output.parse()?;
return Ok(version);
}
Err(VersionError::Other(
String::from_utf8_lossy(&output.stderr).to_string(),
))
}
#[derive(thiserror::Error, Debug)]
pub enum RefError {
#[error("ref name is not valid UTF-8")]
InvalidName,
#[error("unexpected unqualified ref: {0}")]
Unqualified(fmt::RefString),
#[error("invalid ref format: {0}")]
Format(#[from] fmt::Error),
#[error("reference has no target")]
NoTarget,
#[error("expected ref to begin with 'refs/namespaces' but found '{0}'")]
MissingNamespace(fmt::RefString),
#[error("ref name contains invalid namespace identifier '{name}'")]
InvalidNamespace {
name: fmt::RefString,
#[source]
err: Box<dyn std::error::Error + Send + Sync + 'static>,
},
#[error(transparent)]
Other(#[from] raw::Error),
}
#[derive(thiserror::Error, Debug)]
pub enum ListRefsError {
#[error("git error: {0}")]
Git(#[from] raw::Error),
#[error("invalid ref: {0}")]
InvalidRef(#[from] RefError),
}
pub mod refs {
use std::sync::LazyLock;
use radicle_cob as cob;
use super::fmt::*;
use super::*;
/// Try to get a qualified reference from a generic reference.
pub fn qualified_from<'a>(r: &'a raw::Reference) -> Result<(Qualified<'a>, Oid), RefError> {
let name = r.name().ok_or(RefError::InvalidName)?;
let refstr = RefStr::try_from_str(name)?;
let target = r.resolve()?.target().ok_or(RefError::NoTarget)?;
let qualified = Qualified::from_refstr(refstr)
.ok_or_else(|| RefError::Unqualified(refstr.to_owned()))?;
Ok((qualified, target.into()))
}
/// Create a qualified branch reference.
///
/// `refs/heads/<branch>`
///
pub fn branch<'a>(branch: &RefStr) -> Qualified<'a> {
Qualified::from(lit::refs_heads(branch))
}
/// A patch reference.
///
/// `refs/heads/patches/<object_id>`
///
pub fn patch<'a>(object_id: &cob::ObjectId) -> Qualified<'a> {
Qualified::from_components(
component!("heads"),
component!("patches"),
Some(object_id.into()),
)
}
pub mod storage {
use super::*;
/// Where the repo's identity document is stored.
///
/// `refs/rad/id`
///
pub static IDENTITY_BRANCH: LazyLock<Qualified> =
LazyLock::new(|| Qualified::from_components(component!("rad"), component!("id"), None));
/// Where the repo's identity root document is stored.
///
/// `refs/rad/root`
///
pub static IDENTITY_ROOT: LazyLock<Qualified> = LazyLock::new(|| {
Qualified::from_components(component!("rad"), component!("root"), None)
});
/// Where the project's signed references are stored.
///
/// `refs/rad/sigrefs`
///
pub static SIGREFS_BRANCH: LazyLock<Qualified> = LazyLock::new(|| {
Qualified::from_components(component!("rad"), component!("sigrefs"), None)
});
/// A reference to the parent commit.
///
/// `refs/rad/sigrefs-parent`
///
pub static SIGREFS_PARENT: LazyLock<Qualified> = LazyLock::new(|| {
Qualified::from_components(component!("rad"), component!("sigrefs-parent"), None)
});
/// The set of special references used in the Heartwood protocol.
#[derive(Clone, Copy, Debug)]
pub enum Special {
/// `rad/id`
Id,
/// `rad/sigrefs`
SignedRefs,
}
impl From<Special> for Qualified<'_> {
fn from(s: Special) -> Self {
match s {
Special::Id => (*IDENTITY_BRANCH).clone(),
Special::SignedRefs => (*SIGREFS_BRANCH).clone(),
}
}
}
impl Special {
pub fn namespaced<'a>(&self, remote: &PublicKey) -> Namespaced<'a> {
Qualified::from(*self).with_namespace(Component::from(remote))
}
pub fn from_qualified(refname: &Qualified) -> Option<Self> {
if refname == &*IDENTITY_BRANCH {
Some(Special::Id)
} else if refname == &*SIGREFS_BRANCH {
Some(Special::SignedRefs)
} else {
None
}
}
}
/// Create the [`Namespaced`] `branch` under the `remote` namespace, i.e.
///
/// `refs/namespaces/<remote>/refs/heads/<branch>`
///
pub fn branch_of<'a>(remote: &RemoteId, branch: &RefStr) -> Namespaced<'a> {
Qualified::from(lit::refs_heads(branch)).with_namespace(remote.into())
}
/// Get the branch where the project's identity document is stored.
///
/// `refs/namespaces/<remote>/refs/rad/id`
///
pub fn id(remote: &RemoteId) -> Namespaced<'_> {
IDENTITY_BRANCH.with_namespace(remote.into())
}
/// Get the root of the branch where the project's identity document is stored.
///
/// `refs/namespaces/<remote>/refs/rad/root`
///
pub fn id_root(remote: &RemoteId) -> Namespaced<'_> {
IDENTITY_ROOT.with_namespace(remote.into())
}
/// Get the branch where the `remote`'s signed references are
/// stored.
///
/// `refs/namespaces/<remote>/refs/rad/sigrefs`
///
pub fn sigrefs(remote: &RemoteId) -> Namespaced<'_> {
SIGREFS_BRANCH.with_namespace(remote.into())
}
/// The collaborative object reference, identified by `typename` and `object_id`, under the given `remote`.
///
/// `refs/namespaces/<remote>/refs/cobs/<typename>/<object_id>`
///
pub fn cob<'a>(
remote: &RemoteId,
typename: &cob::TypeName,
object_id: &cob::ObjectId,
) -> Namespaced<'a> {
Qualified::from_components(
component!("cobs"),
Component::from(typename),
Some(object_id.into()),
)
.with_namespace(remote.into())
}
/// All collaborative objects, identified by `typename` and `object_id`, for all remotes.
///
/// `refs/namespaces/*/refs/cobs/<typename>/<object_id>`
///
pub fn cobs(typename: &cob::TypeName, object_id: &cob::ObjectId) -> refspec::PatternString {
pattern!("refs/namespaces/*")
.join(refname!("refs/cobs"))
.join(Component::from(typename))
.join(Component::from(object_id))
}
/// Draft references.
///
/// These references are not replicated or signed.
pub mod draft {
use super::*;
/// Review draft reference. Points to the non-COB part of a patch review.
///
/// `refs/namespaces/<remote>/refs/drafts/reviews/<patch-id>`
///
/// When building a patch review, we store the intermediate state in this ref.
pub fn review<'a>(remote: &RemoteId, patch: &cob::ObjectId) -> Namespaced<'a> {
Qualified::from_components(
component!("drafts"),
component!("reviews"),
Some(Component::from(patch)),
)
.with_namespace(remote.into())
}
/// A draft collaborative object. This can also be a draft operation on an existing
/// object.
///
/// `refs/namespaces/<remote>/refs/drafts/cobs/<typename>/<object_id>`
///
pub fn cob<'a>(
remote: &RemoteId,
typename: &cob::TypeName,
object_id: &cob::ObjectId,
) -> Namespaced<'a> {
Qualified::from_components(
component!("drafts"),
component!("cobs"),
[Component::from(typename), object_id.into()],
)
.with_namespace(remote.into())
}
/// All draft collaborative object, identified by `typename` and `object_id`, for all remotes.
///
/// `refs/namespaces/*/refs/drafts/cobs/<typename>/<object_id>`
///
pub fn cobs(
typename: &cob::TypeName,
object_id: &cob::ObjectId,
) -> refspec::PatternString {
pattern!("refs/namespaces/*")
.join(refname!("refs/drafts/cobs"))
.join(Component::from(typename))
.join(Component::from(object_id))
}
}
/// Staging/temporary references.
pub mod staging {
use super::*;
/// Where patch heads are pushed initially, before patch creation.
/// This is a short-lived reference, which is deleted after the patch has been opened.
/// The `<oid>` is the commit proposed in the patch.
///
/// `refs/namespaces/<remote>/refs/tmp/heads/<oid>`
///
pub fn patch<'a>(remote: &RemoteId, oid: impl Into<Oid>) -> Namespaced<'a> {
// SAFETY: OIDs are valid reference names and valid path component.
#[allow(clippy::unwrap_used)]
let oid = RefString::try_from(oid.into().to_string()).unwrap();
#[allow(clippy::unwrap_used)]
let oid = Component::from_refstr(oid).unwrap();
Qualified::from_components(component!("tmp"), component!("heads"), Some(oid))
.with_namespace(remote.into())
}
}
}
pub mod workdir {
use super::*;
/// Create a [`RefString`] that corresponds to `refs/heads/<branch>`.
pub fn branch(branch: &RefStr) -> RefString {
refname!("refs/heads").join(branch)
}
/// Create a [`RefString`] that corresponds to `refs/notes/<name>`.
pub fn note(name: &RefStr) -> RefString {
refname!("refs/notes").join(name)
}
/// Create a [`RefString`] that corresponds to `refs/remotes/<remote>/<branch>`.
pub fn remote_branch(remote: &RefStr, branch: &RefStr) -> RefString {
refname!("refs/remotes").and(remote).and(branch)
}
/// Create a [`RefString`] that corresponds to `refs/tags/<branch>`.
pub fn tag(name: &RefStr) -> RefString {
refname!("refs/tags").join(name)
}
/// A patch head.
///
/// `refs/remotes/rad/patches/<patch-id>`
///
pub fn patch_upstream<'a>(patch_id: &cob::ObjectId) -> Qualified<'a> {
Qualified::from_components(
component!("remotes"),
crate::rad::REMOTE_COMPONENT.clone(),
[component!("patches"), patch_id.into()],
)
}
}
}
/// Parse a [`fmt::Qualified`] reference string while expecting the reference
/// to start with `refs/namespaces`. If the namespace is not present, then an
/// error will be returned.
///
/// The namespace returned is the path component that is after `refs/namespaces`,
/// e.g. in the reference below, the segment is
/// `z6MkvUJtYD9dHDJfpevWRT98mzDDpdAtmUjwyDSkyqksUr7C`:
///
/// ```text, no_run
/// refs/namespaces/z6MkvUJtYD9dHDJfpevWRT98mzDDpdAtmUjwyDSkyqksUr7C/refs/heads/main
/// ```
///
/// The `T` can be specified when calling the function. For example, if you
/// wanted to parse the namespace as a `PublicKey`, then you would the function
/// like so, `parse_ref_namespaced::<PublicKey>(s)`.
pub fn parse_ref_namespaced<T>(s: &str) -> Result<(T, fmt::Qualified<'_>), RefError>
where
T: FromStr,
T::Err: std::error::Error + Send + Sync + 'static,
{
match parse_ref::<T>(s) {
Ok((None, refname)) => Err(RefError::MissingNamespace(refname.to_ref_string())),
Ok((Some(t), r)) => Ok((t, r)),
Err(err) => Err(err),
}
}
/// Parse a [`fmt::Qualified`] reference string. It will optionally return
/// the namespace, if present.
///
/// The qualified form could be of the form: `refs/heads/main`,
/// `refs/tags/v1.0`, etc.
///
/// The namespace returned is the path component that is after `refs/namespaces`,
/// e.g. in the reference below, the segment is
/// `z6MkvUJtYD9dHDJfpevWRT98mzDDpdAtmUjwyDSkyqksUr7C`:
///
/// ```text, no_run
/// refs/namespaces/z6MkvUJtYD9dHDJfpevWRT98mzDDpdAtmUjwyDSkyqksUr7C/refs/heads/main
/// ```
///
/// The `T` can be specified when calling the function. For example, if you
/// wanted to parse the namespace as a `PublicKey`, then you would the function
/// like so, `parse_ref::<PublicKey>(s)`.
pub fn parse_ref<T>(s: &str) -> Result<(Option<T>, fmt::Qualified<'_>), RefError>
where
T: FromStr,
T::Err: std::error::Error + Send + Sync + 'static,
{
let input = fmt::RefStr::try_from_str(s)?;
match input.to_namespaced() {
None => {
let refname = fmt::Qualified::from_refstr(input)
.ok_or_else(|| RefError::Unqualified(input.to_owned()))?;
Ok((None, refname))
}
Some(ns) => {
let id = ns
.namespace()
.as_str()
.parse()
.map_err(|err| RefError::InvalidNamespace {
name: input.to_owned(),
err: Box::new(err),
})?;
let rest = ns.strip_namespace();
Ok((Some(id), rest))
}
}
}
/// Create an initial empty commit.
pub fn initial_commit<'a>(
repo: &'a raw::Repository,
sig: &raw::Signature,
) -> Result<raw::Commit<'a>, raw::Error> {
let tree_id = repo.index()?.write_tree()?;
let tree = repo.find_tree(tree_id)?;
let oid = repo.commit(None, sig, sig, "Initial commit", &tree, &[])?;
let commit = repo.find_commit(oid)?;
Ok(commit)
}
/// Create a commit and update the given ref to it.
pub fn commit<'a>(
repo: &'a raw::Repository,
parent: &'a raw::Commit,
target: &fmt::RefStr,
message: &str,
sig: &raw::Signature,
tree: &raw::Tree,
) -> Result<raw::Commit<'a>, raw::Error> {
let oid = repo.commit(Some(target.as_str()), sig, sig, message, tree, &[parent])?;
let commit = repo.find_commit(oid)?;
Ok(commit)
}
/// Create an empty commit on top of the parent.
pub fn empty_commit<'a>(
repo: &'a raw::Repository,
parent: &'a raw::Commit,
target: &fmt::RefStr,
message: &str,
sig: &raw::Signature,
) -> Result<raw::Commit<'a>, raw::Error> {
let tree = parent.tree()?;
let oid = repo.commit(Some(target.as_str()), sig, sig, message, &tree, &[parent])?;
let commit = repo.find_commit(oid)?;
Ok(commit)
}
/// Get the repository head.
pub fn head(repo: &raw::Repository) -> Result<raw::Commit<'_>, raw::Error> {
let head = repo.head()?.peel_to_commit()?;
Ok(head)
}
/// Write a tree with the given blob at the given path.
pub fn write_tree<'r>(
path: &Path,
bytes: &[u8],
repo: &'r raw::Repository,
) -> Result<raw::Tree<'r>, raw::Error> {
let blob_id = repo.blob(bytes)?;
let mut builder = repo.treebuilder(None)?;
builder.insert(path, blob_id, 0o100_644)?;
let tree_id = builder.write()?;
let tree = repo.find_tree(tree_id)?;
Ok(tree)
}
/// Configure a Radicle repository.
///
/// * Sets `push.default = upstream`.
pub fn configure_repository(repo: &raw::Repository) -> Result<(), raw::Error> {
let mut cfg = repo.config()?;
cfg.set_str("push.default", "upstream")?;
Ok(())
}
/// Configure a repository's Radicle remote.
///
/// The entry for this remote will be:
/// ```text
/// [remote.<name>]
/// url = <fetch>
/// pushurl = <push>
/// fetch = +refs/heads/*:refs/remotes/<name>/*
/// fetch = +refs/tags/*:refs/remotes/<name>/tags/*
/// tagOpt = --no-tags
/// pruneTags = false
/// ```
///
/// Because of the `+refs/tags/*:…` refspec, set:
/// 1. `pruneTags = false` to ensure that `git` does not delete tags because
/// the remote does not have them. Tags for a Radicle repository are
/// synthesised by canonical refs and thus, the `rad` remote will handle
/// fetching them.
/// 2. `tagOpt = --no-tags` to ensure that tags are not fetched and stored
/// under `refs/tags`, again, because these are fetched by the `rad` remote.
pub fn configure_remote<'r>(
repo: &'r raw::Repository,
name: &str,
fetch: &Url,
push: &Url,
) -> Result<raw::Remote<'r>, raw::Error> {
let fetchspec = format!("+refs/heads/*:refs/remotes/{name}/*");
let remote = repo.remote_with_fetch(name, fetch.to_string().as_str(), &fetchspec)?;
// We want to be able fetch tags from a peer's namespace and this is
// necessary to do so, since Git assumes that tags should always be fetched
// from the top-level `refs/tags` namespace
let tags = format!("+refs/tags/*:refs/remotes/{name}/tags/*");
repo.remote_add_fetch(name, &tags)?;
if name != (*rad::REMOTE_NAME).as_str() {
let mut config = repo.config()?;
config.set_bool(&format!("remote.{name}.pruneTags"), false)?;
config.set_str(&format!("remote.{name}.tagOpt"), "--no-tags")?;
}
if push != fetch {
repo.remote_set_pushurl(name, Some(push.to_string().as_str()))?;
}
Ok(remote)
}
/// Fetch from the given `remote`.
pub fn fetch(repo: &raw::Repository, remote: &str) -> Result<(), raw::Error> {
repo.find_remote(remote)?.fetch::<&str>(
&[],
Some(
raw::FetchOptions::new()
.update_fetchhead(false)
.prune(raw::FetchPrune::On)
.download_tags(raw::AutotagOption::None),
),
None,
)
}
/// Push `refspecs` to the given `remote` using the provided `namespace`.
pub fn push<'a>(
repo: &raw::Repository,
remote: &str,
refspecs: impl IntoIterator<Item = (&'a fmt::Qualified<'a>, &'a fmt::Qualified<'a>)>,
) -> Result<(), raw::Error> {
let refspecs = refspecs
.into_iter()
.map(|(src, dst)| format!("{src}:{dst}"));
repo.find_remote(remote)?
.push(refspecs.collect::<Vec<_>>().as_slice(), None)?;
Ok(())
}
/// Set the upstream of the given branch to the given remote.
///
/// This writes to the `config` directly. The entry will look like the
/// following:
///
/// ```text
/// [branch "main"]
/// remote = rad
/// merge = refs/heads/main
/// ```
pub fn set_upstream(
repo: &raw::Repository,
remote: impl AsRef<str>,
branch: impl AsRef<str>,
merge: impl AsRef<str>,
) -> Result<(), raw::Error> {
let remote = remote.as_ref();
let branch = branch.as_ref();
let merge = merge.as_ref();
let mut config = repo.config()?;
let branch_remote = format!("branch.{branch}.remote");
let branch_merge = format!("branch.{branch}.merge");
config
.remove_multivar(&branch_remote, ".*")
.or_else(|e| if e.is_not_found() { Ok(()) } else { Err(e) })?;
config
.remove_multivar(&branch_merge, ".*")
.or_else(|e| if e.is_not_found() { Ok(()) } else { Err(e) })?;
config.set_multivar(&branch_remote, ".*", remote)?;
config.set_multivar(&branch_merge, ".*", merge)?;
Ok(())
}
pub fn init_default_branch(repo: &raw::Repository) -> Result<Option<String>, raw::Error> {
let config = repo.config().and_then(|mut c| c.snapshot())?;
let default_branch = config.get_str("init.defaultbranch")?;
let branch = repo.find_branch(default_branch, raw::BranchType::Local)?;
Ok(branch.into_reference().shorthand().map(ToOwned::to_owned))
}
pub fn head_refname(repo: &raw::Repository) -> Result<Option<String>, raw::Error> {
let head = repo.head()?;
match head.shorthand() {
Some("HEAD") => Ok(None),
Some(refname) => Ok(Some(refname.to_owned())),
None => Ok(None),
}
}
/// Execute a `git` command by spawning a child process and collect its output.
/// If `working` is [`Some`], the command is run as if `git` was started in
/// `working` instead of the current working directory, by prepending
/// `-C <working>` to the command line.
pub fn run<S>(
working: Option<&std::path::Path>,
args: impl IntoIterator<Item = S>,
) -> io::Result<std::process::Output>
where
S: AsRef<std::ffi::OsStr>,
{
let mut cmd = Command::new("git");
if let Some(working) = working {
cmd.arg("-C").arg(dunce::canonicalize(working)?);
}
cmd.args(args).output()
}
/// Functions that call to the `git` CLI instead of `git2`.
pub mod process {
use std::io;
use std::path::Path;
use crate::storage::ReadRepository;
use super::{Oid, Verbosity, run};
/// Perform a local fetch, from storage using `git fetch-pack`.
///
/// `oids` are the set of [`Oid`]s that are being fetched from the
/// `storage`.
pub fn fetch_pack<R>(
working: Option<&Path>,
storage: &R,
oids: impl IntoIterator<Item = Oid>,
verbosity: Verbosity,
) -> io::Result<std::process::Output>
where
R: ReadRepository,
{
let mut args = vec!["fetch-pack".to_string()];
args.extend(verbosity.clamp_one().into_flag());
args.push(dunce::canonicalize(storage.path())?.display().to_string());
args.extend(oids.into_iter().map(|oid| oid.to_string()));
run(working, args)
}
}
/// Git URLs.
pub mod url {
use std::path::PathBuf;
use crate::prelude::RepoId;
/// A Git URL using the `file://` scheme.
pub struct File {
pub path: PathBuf,
}
impl File {
/// Create a new file URL pointing to the given path.
pub fn new(path: impl Into<PathBuf>) -> Self {
Self { path: path.into() }
}
/// Return a URL with the given RID set.
pub fn rid(mut self, rid: RepoId) -> Self {
self.path.push(rid.canonical());
self
}
}
impl std::fmt::Display for File {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "file://{}", self.path.display())
}
}
}
/// Git environment variables.
pub mod env {
/// Set of environment vars to reset git's configuration to default.
pub const GIT_DEFAULT_CONFIG: [(&str, &str); 2] = [
("GIT_CONFIG_GLOBAL", "/dev/null"),
("GIT_CONFIG_NOSYSTEM", "1"),
];
}
/// The user information used for signing commits and configuring the
/// `name` and `email` fields in the Git config.
#[derive(Debug, Clone)]
pub struct UserInfo {
/// Alias of the local peer.
pub alias: Alias,
/// [`PublicKey`] of the local peer.
pub key: PublicKey,
}
impl UserInfo {
/// The name of the user, i.e. the `alias`.
pub fn name(&self) -> Alias {
self.alias.clone()
}
/// The "email" of the user, which is in the form
/// `<alias>@<public key>`.
pub fn email(&self) -> String {
format!("{}@{}", self.alias, self.key)
}
}
#[cfg(test)]
mod test {
use super::*;
use std::str::FromStr;
#[test]
fn test_version_ord() {
assert!(
Version {
major: 2,
minor: 34,
patch: 1
} > Version {
major: 2,
minor: 34,
patch: 0
}
);
assert!(
Version {
major: 2,
minor: 24,
patch: 12
} < Version {
major: 2,
minor: 34,
patch: 0
}
);
}
#[test]
fn test_version_from_str() {
assert_eq!(
Version::from_str("git version 2.34.1\n").ok(),
Some(Version {
major: 2,
minor: 34,
patch: 1
})
);
assert_eq!(
Version::from_str("git version 2.34.1 (macOS)").ok(),
Some(Version {
major: 2,
minor: 34,
patch: 1
})
);
assert_eq!(
Version::from_str("git version 2.34").ok(),
Some(Version {
major: 2,
minor: 34,
patch: 0
})
);
assert!(Version::from_str("2.34").is_err());
}
}