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::{str::ParseOidError, Oid};
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;
pub const PROTOCOL_PORT: u16 = 9418;
pub const VERSION_REQUIRED: Version = Version {
major: 2,
minor: 31,
patch: 0,
};
#[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)
}
}
#[derive(Default, Clone, Copy)]
pub struct Verbosity(i8);
impl Verbosity {
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))
}
fn clamp(self, min: i8, max: i8) -> Self {
Self(self.0.clamp(min, max))
}
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,
})
}
}
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::*;
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()))
}
pub fn branch<'a>(branch: &RefStr) -> Qualified<'a> {
Qualified::from(lit::refs_heads(branch))
}
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::*;
pub static IDENTITY_BRANCH: LazyLock<Qualified> =
LazyLock::new(|| Qualified::from_components(component!("rad"), component!("id"), None));
pub static IDENTITY_ROOT: LazyLock<Qualified> = LazyLock::new(|| {
Qualified::from_components(component!("rad"), component!("root"), None)
});
pub static SIGREFS_BRANCH: LazyLock<Qualified> = LazyLock::new(|| {
Qualified::from_components(component!("rad"), component!("sigrefs"), None)
});
pub static SIGREFS_PARENT: LazyLock<Qualified> = LazyLock::new(|| {
Qualified::from_components(component!("rad"), component!("sigrefs-parent"), None)
});
#[derive(Clone, Copy, Debug)]
pub enum Special {
Id,
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
}
}
}
pub fn branch_of<'a>(remote: &RemoteId, branch: &RefStr) -> Namespaced<'a> {
Qualified::from(lit::refs_heads(branch)).with_namespace(remote.into())
}
pub fn id(remote: &RemoteId) -> Namespaced<'_> {
IDENTITY_BRANCH.with_namespace(remote.into())
}
pub fn id_root(remote: &RemoteId) -> Namespaced<'_> {
IDENTITY_ROOT.with_namespace(remote.into())
}
pub fn sigrefs(remote: &RemoteId) -> Namespaced<'_> {
SIGREFS_BRANCH.with_namespace(remote.into())
}
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())
}
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))
}
pub mod draft {
use super::*;
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())
}
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())
}
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))
}
}
pub mod staging {
use super::*;
pub fn patch<'a>(remote: &RemoteId, oid: impl Into<Oid>) -> Namespaced<'a> {
#[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::*;
pub fn branch(branch: &RefStr) -> RefString {
refname!("refs/heads").join(branch)
}
pub fn note(name: &RefStr) -> RefString {
refname!("refs/notes").join(name)
}
pub fn remote_branch(remote: &RefStr, branch: &RefStr) -> RefString {
refname!("refs/remotes").and(remote).and(branch)
}
pub fn tag(name: &RefStr) -> RefString {
refname!("refs/tags").join(name)
}
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()],
)
}
}
}
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),
}
}
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))
}
}
}
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)
}
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)
}
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)
}
pub fn head(repo: &raw::Repository) -> Result<raw::Commit<'_>, raw::Error> {
let head = repo.head()?.peel_to_commit()?;
Ok(head)
}
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)
}
pub fn configure_repository(repo: &raw::Repository) -> Result<(), raw::Error> {
let mut cfg = repo.config()?;
cfg.set_str("push.default", "upstream")?;
Ok(())
}
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)?;
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)
}
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,
)
}
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(())
}
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),
}
}
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()
}
pub mod process {
use std::io;
use std::path::Path;
use crate::storage::ReadRepository;
use super::{run, Oid, Verbosity};
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)
}
}
pub mod url {
use std::path::PathBuf;
use crate::prelude::RepoId;
pub struct File {
pub path: PathBuf,
}
impl File {
pub fn new(path: impl Into<PathBuf>) -> Self {
Self { path: path.into() }
}
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())
}
}
}
pub mod env {
pub const GIT_DEFAULT_CONFIG: [(&str, &str); 2] = [
("GIT_CONFIG_GLOBAL", "/dev/null"),
("GIT_CONFIG_NOSYSTEM", "1"),
];
}
#[derive(Debug, Clone)]
pub struct UserInfo {
pub alias: Alias,
pub key: PublicKey,
}
impl UserInfo {
pub fn name(&self) -> Alias {
self.alias.clone()
}
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());
}
}