Archive crates

Move git-commit, git-trailers, and link-git to an archived directory
to hide them away -- showing the intent that radicle-git-ext will be
the main crate for git functionality.

Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com>
X-Clacks-Overhead: GNU Terry Pratchett
This commit is contained in:
Fintan Halpenny
2023-03-27 11:01:21 +01:00
parent e81ea9b558
commit 1668d9fab3
44 changed files with 0 additions and 17 deletions
+129
View File
@@ -0,0 +1,129 @@
use std::{
fmt,
num::ParseIntError,
str::{self, FromStr},
};
use thiserror::Error;
/// The data for indicating authorship of an action within a
/// [`super::Commit`].
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Author {
/// Name corresponding to `user.name` in the git config.
///
/// Note: this must not contain `<` or `>`.
pub name: String,
/// Email corresponding to `user.email` in the git config.
///
/// Note: this must not contain `<` or `>`.
pub email: String,
/// The time of this author's action.
pub time: Time,
}
/// The time of a [`Author`]'s action.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Time {
seconds: i64,
offset: i32,
}
impl Time {
pub fn new(seconds: i64, offset: i32) -> Self {
Self { seconds, offset }
}
/// Return the time, in seconds, since the epoch.
pub fn seconds(&self) -> i64 {
self.seconds
}
/// Return the timezone offset, in minutes.
pub fn offset(&self) -> i32 {
self.offset
}
}
impl From<Time> for git2::Time {
fn from(t: Time) -> Self {
Self::new(t.seconds, t.offset)
}
}
impl From<git2::Time> for Time {
fn from(t: git2::Time) -> Self {
Self::new(t.seconds(), t.offset_minutes())
}
}
impl fmt::Display for Time {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let sign = if self.offset.is_negative() { '-' } else { '+' };
write!(f, "{} {}{:0>4}", self.seconds, sign, self.offset.abs())
}
}
impl fmt::Display for Author {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} <{}> {}", self.name, self.email, self.time,)
}
}
impl TryFrom<&Author> for git2::Signature<'_> {
type Error = git2::Error;
fn try_from(person: &Author) -> Result<Self, Self::Error> {
let time = git2::Time::new(person.time.seconds, person.time.offset);
git2::Signature::new(&person.name, &person.email, &time)
}
}
impl<'a> TryFrom<&git2::Signature<'a>> for Author {
type Error = str::Utf8Error;
fn try_from(value: &git2::Signature<'a>) -> Result<Self, Self::Error> {
Ok(Self {
name: str::from_utf8(value.name_bytes())?.to_string(),
email: str::from_utf8(value.email_bytes())?.to_string(),
time: value.when().into(),
})
}
}
#[derive(Debug, Error)]
pub enum ParseError {
#[error("missing '{0}' while parsing person signature")]
Missing(&'static str),
#[error("offset was incorrect format while parsing person signature")]
Offset(#[source] ParseIntError),
#[error("time was incorrect format while parsing person signature")]
Time(#[source] ParseIntError),
#[error("time offset is expected to be '+'/'-' for a person siganture")]
UnknownOffset,
}
impl FromStr for Author {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut components = s.split(' ');
let offset = match components.next_back() {
None => return Err(ParseError::Missing("offset")),
Some(offset) => offset.parse::<i32>().map_err(ParseError::Offset)?,
};
let time = match components.next_back() {
None => return Err(ParseError::Missing("time")),
Some(time) => time.parse::<i64>().map_err(ParseError::Time)?,
};
let time = Time::new(time, offset);
let email = components
.next_back()
.ok_or(ParseError::Missing("email"))?
.trim_matches(|c| c == '<' || c == '>')
.to_owned();
let name = components.collect::<Vec<_>>().join(" ");
Ok(Self { name, email, time })
}
}
+71
View File
@@ -0,0 +1,71 @@
use std::borrow::Cow;
const BEGIN_SSH: &str = "-----BEGIN SSH SIGNATURE-----\n";
const BEGIN_PGP: &str = "-----BEGIN PGP SIGNATURE-----\n";
/// A collection of headers stored in a [`super::Commit`].
///
/// Note: these do not include `tree`, `parent`, `author`, and `committer`.
#[derive(Clone, Debug, Default)]
pub struct Headers(pub(super) Vec<(String, String)>);
/// A `gpgsig` signature stored in a [`super::Commit`].
pub enum Signature<'a> {
/// A PGP signature, i.e. starts with `-----BEGIN PGP SIGNATURE-----`.
Pgp(Cow<'a, str>),
/// A SSH signature, i.e. starts with `-----BEGIN SSH SIGNATURE-----`.
Ssh(Cow<'a, str>),
}
impl<'a> Signature<'a> {
fn from_str(s: &'a str) -> Result<Self, UnknownScheme> {
if s.starts_with(BEGIN_SSH) {
Ok(Signature::Ssh(Cow::Borrowed(s)))
} else if s.starts_with(BEGIN_PGP) {
Ok(Signature::Pgp(Cow::Borrowed(s)))
} else {
Err(UnknownScheme)
}
}
}
pub struct UnknownScheme;
impl<'a> ToString for Signature<'a> {
fn to_string(&self) -> String {
match self {
Signature::Pgp(pgp) => pgp.to_string(),
Signature::Ssh(ssh) => ssh.to_string(),
}
}
}
impl Headers {
pub fn new() -> Self {
Headers(Vec::new())
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
self.0.iter().map(|(k, v)| (k.as_str(), v.as_str()))
}
pub fn values<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a str> + '_ {
self.iter()
.filter_map(move |(k, v)| (k == name).then_some(v))
}
pub fn signatures(&self) -> impl Iterator<Item = Signature> + '_ {
self.0.iter().filter_map(|(k, v)| {
if k == "gpgsig" {
Signature::from_str(v).ok()
} else {
None
}
})
}
/// Push a header to the end of the headers section.
pub fn push(&mut self, name: &str, value: &str) {
self.0.push((name.to_owned(), value.trim().to_owned()));
}
}
+295
View File
@@ -0,0 +1,295 @@
//! The `git-commit` crate provides parsing a displaying of a [git
//! commit][git-commit].
//!
//! The [`Commit`] data can be constructed using the `FromStr`
//! implementation, or by converting from a `git2::Buf`.
//!
//! The [`Headers`] can be accessed via [`Commit::headers`]. If the
//! signatures of the commit are of particular interest, the
//! [`Commit::signatures`] method can be used, which returns a series of
//! [`Signature`]s.
//!
//! [git-commit]: https://git-scm.com/book/en/v2/Git-Internals-Git-Objects
use std::{
fmt::Write as _,
str::{self, FromStr},
};
use git2::{ObjectType, Oid};
pub mod author;
pub use author::Author;
pub mod headers;
pub use headers::{Headers, Signature};
pub mod trailers;
pub use trailers::{OwnedTrailer, Trailer, Trailers};
/// A git commit in its object description form, i.e. the output of
/// `git cat-file` for a commit object.
#[derive(Debug)]
pub struct Commit {
tree: Oid,
parents: Vec<Oid>,
author: Author,
committer: Author,
headers: Headers,
message: String,
trailers: Vec<OwnedTrailer>,
}
impl Commit {
pub fn new<I, T>(
tree: Oid,
parents: Vec<Oid>,
author: Author,
committer: Author,
headers: Headers,
message: String,
trailers: I,
) -> Self
where
I: IntoIterator<Item = T>,
OwnedTrailer: From<T>,
{
let trailers = trailers.into_iter().map(OwnedTrailer::from).collect();
Self {
tree,
parents,
author,
committer,
headers,
message,
trailers,
}
}
/// Read the [`Commit`] from the `repo` that is expected to be found at
/// `oid`.
pub fn read(repo: &git2::Repository, oid: Oid) -> Result<Self, error::Read> {
let odb = repo.odb()?;
let object = odb.read(oid)?;
Ok(Commit::try_from(object.data())?)
}
/// Write the given [`Commit`] to the `repo`. The resulting `Oid`
/// is the identifier for this commit.
pub fn write(&self, repo: &git2::Repository) -> Result<Oid, git2::Error> {
let odb = repo.odb()?;
odb.write(ObjectType::Commit, self.to_string().as_bytes())
}
/// The tree [`Oid`] this commit points to.
pub fn tree(&self) -> Oid {
self.tree
}
/// The parent [`Oid`]s of this commit.
pub fn parents(&self) -> impl Iterator<Item = Oid> + '_ {
self.parents.iter().copied()
}
/// The author of this commit, i.e. the header corresponding to `author`.
pub fn author(&self) -> &Author {
&self.author
}
/// The committer of this commit, i.e. the header corresponding to
/// `committer`.
pub fn committer(&self) -> &Author {
&self.committer
}
/// The message body of this commit.
pub fn message(&self) -> &str {
&self.message
}
/// The [`Signature`]s found in this commit, i.e. the headers corresponding
/// to `gpgsig`.
pub fn signatures(&self) -> impl Iterator<Item = Signature> + '_ {
self.headers.signatures()
}
/// The [`Headers`] found in this commit.
///
/// Note: these do not include `tree`, `parent`, `author`, and `committer`.
pub fn headers(&self) -> impl Iterator<Item = (&str, &str)> {
self.headers.iter()
}
/// Iterate over the [`Headers`] values that match the provided `name`.
pub fn values<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a str> + '_ {
self.headers.values(name)
}
/// Push a header to the end of the headers section.
pub fn push_header(&mut self, name: &str, value: &str) {
self.headers.push(name, value.trim());
}
pub fn trailers(&self) -> impl Iterator<Item = &OwnedTrailer> {
self.trailers.iter()
}
}
pub mod error {
use std::str;
use thiserror::Error;
use super::author;
#[derive(Debug, Error)]
pub enum Read {
#[error(transparent)]
Git(#[from] git2::Error),
#[error(transparent)]
Parse(#[from] Parse),
}
#[derive(Debug, Error)]
pub enum Parse {
#[error(transparent)]
Author(#[from] author::ParseError),
#[error("invalid '{header}'")]
InvalidHeader {
header: &'static str,
#[source]
err: git2::Error,
},
#[error("invalid git commit object format")]
InvalidFormat,
#[error("missing '{0}' while parsing commit")]
Missing(&'static str),
#[error("error occurred while checking for git-trailers: {0}")]
Trailers(#[source] git2::Error),
#[error(transparent)]
Utf8(#[from] str::Utf8Error),
}
}
impl TryFrom<git2::Buf> for Commit {
type Error = error::Parse;
fn try_from(value: git2::Buf) -> Result<Self, Self::Error> {
value.as_str().ok_or(error::Parse::InvalidFormat)?.parse()
}
}
impl TryFrom<&[u8]> for Commit {
type Error = error::Parse;
fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
Commit::from_str(str::from_utf8(data)?)
}
}
impl FromStr for Commit {
type Err = error::Parse;
fn from_str(buffer: &str) -> Result<Self, Self::Err> {
let (header, message) = buffer
.split_once("\n\n")
.ok_or(error::Parse::InvalidFormat)?;
let mut lines = header.lines();
let tree = match lines.next() {
Some(tree) => tree
.strip_prefix("tree ")
.map(git2::Oid::from_str)
.transpose()
.map_err(|err| error::Parse::InvalidHeader {
header: "tree",
err,
})?
.ok_or(error::Parse::Missing("tree"))?,
None => return Err(error::Parse::Missing("tree")),
};
let mut parents = Vec::new();
let mut author: Option<Author> = None;
let mut committer: Option<Author> = None;
let mut headers = Headers::new();
for line in lines {
// Check if a signature is still being parsed
if let Some(rest) = line.strip_prefix(' ') {
let value: &mut String = headers
.0
.last_mut()
.map(|(_, v)| v)
.ok_or(error::Parse::InvalidFormat)?;
value.push('\n');
value.push_str(rest);
continue;
}
if let Some((name, value)) = line.split_once(' ') {
match name {
"parent" => parents.push(git2::Oid::from_str(value).map_err(|err| {
error::Parse::InvalidHeader {
header: "parent",
err,
}
})?),
"author" => author = Some(value.parse::<Author>()?),
"committer" => committer = Some(value.parse::<Author>()?),
_ => headers.push(name, value),
}
continue;
}
}
let trailers = trailers::Trailers::parse(message).map_err(error::Parse::Trailers)?;
let message = message
.strip_suffix(&trailers.to_string(": "))
.unwrap_or(message)
.to_string();
let trailers = trailers.iter().map(OwnedTrailer::from).collect();
Ok(Self {
tree,
parents,
author: author.ok_or(error::Parse::Missing("author"))?,
committer: committer.ok_or(error::Parse::Missing("committer"))?,
headers,
message,
trailers,
})
}
}
impl ToString for Commit {
fn to_string(&self) -> String {
let mut buf = String::new();
writeln!(buf, "tree {}", self.tree).ok();
for parent in &self.parents {
writeln!(buf, "parent {parent}").ok();
}
writeln!(buf, "author {}", self.author).ok();
writeln!(buf, "committer {}", self.committer).ok();
for (name, value) in self.headers.iter() {
writeln!(buf, "{name} {}", value.replace('\n', "\n ")).ok();
}
writeln!(buf).ok();
write!(buf, "{}", self.message.trim()).ok();
writeln!(buf).ok();
if !self.trailers.is_empty() {
writeln!(buf).ok();
}
for trailer in self.trailers.iter() {
writeln!(buf, "{}", Trailer::from(trailer).display(": ")).ok();
}
buf
}
}
+214
View File
@@ -0,0 +1,214 @@
use std::{borrow::Cow, fmt, fmt::Write, ops::Deref, str::FromStr};
use git2::{MessageTrailersStrs, MessageTrailersStrsIterator};
/// A Git commit's set of trailers that are left in the commit's
/// message.
///
/// Trailers are key/value pairs in the last paragraph of a message,
/// not including any patches or conflicts that may be present.
///
/// # Usage
///
/// To construct `Trailers`, you can use [`Trailers::parse`] or its
/// `FromStr` implementation.
///
/// To iterate over the trailers, you can use [`Trailers::iter`].
///
/// To render the trailers to a `String`, you can use
/// [`Trailers::to_string`] or its `Display` implementation (note that
/// it will default to using `": "` as the separator.
///
/// # Examples
///
/// ```text
/// Add new functionality
///
/// Making code better with new functionality.
///
/// X-Signed-Off-By: Alex Sellier
/// X-Co-Authored-By: Fintan Halpenny
/// ```
///
/// The trailers in the above example are:
///
/// ```text
/// X-Signed-Off-By: Alex Sellier
/// X-Co-Authored-By: Fintan Halpenny
/// ```
pub struct Trailers {
inner: MessageTrailersStrs,
}
impl Trailers {
pub fn parse(message: &str) -> Result<Self, git2::Error> {
Ok(Self {
inner: git2::message_trailers_strs(message)?,
})
}
pub fn iter(&self) -> Iter<'_> {
Iter {
inner: self.inner.iter(),
}
}
pub fn to_string<'a, S>(&self, sep: S) -> String
where
S: Separator<'a>,
{
let mut buf = String::new();
for (i, trailer) in self.iter().enumerate() {
if i > 0 {
writeln!(buf).ok();
}
write!(buf, "{}", trailer.display(sep.sep_for(&trailer.token))).ok();
}
writeln!(buf).ok();
buf
}
}
impl fmt::Display for Trailers {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_string(": "))
}
}
pub trait Separator<'a> {
fn sep_for(&self, token: &Token) -> &'a str;
}
impl<'a> Separator<'a> for &'a str {
fn sep_for(&self, _: &Token) -> &'a str {
self
}
}
impl<'a, F> Separator<'a> for F
where
F: Fn(&Token) -> &'a str,
{
fn sep_for(&self, token: &Token) -> &'a str {
self(token)
}
}
impl FromStr for Trailers {
type Err = git2::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
pub struct Iter<'a> {
inner: MessageTrailersStrsIterator<'a>,
}
impl<'a> Iterator for Iter<'a> {
type Item = Trailer<'a>;
fn next(&mut self) -> Option<Self::Item> {
let (token, value) = self.inner.next()?;
Some(Trailer {
token: Token(token),
value: Cow::Borrowed(value),
})
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Token<'a>(&'a str);
impl Deref for Token<'_> {
type Target = str;
fn deref(&self) -> &Self::Target {
self.0
}
}
pub struct Display<'a> {
trailer: &'a Trailer<'a>,
separator: &'a str,
}
impl<'a> fmt::Display for Display<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}{}{}",
self.trailer.token.deref(),
self.separator,
self.trailer.value,
)
}
}
/// A trailer is a key/value pair found in the last paragraph of a Git
/// commit message, not including any patches or conflicts that may be
/// present.
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Trailer<'a> {
pub token: Token<'a>,
pub value: Cow<'a, str>,
}
impl<'a> Trailer<'a> {
pub fn display(&'a self, separator: &'a str) -> Display<'a> {
Display {
trailer: self,
separator,
}
}
pub fn to_owned(&self) -> OwnedTrailer {
OwnedTrailer::from(self)
}
}
/// A version of the [`Trailer`] which owns its token and
/// value. Useful for when you need to carry trailers around in a long
/// lived data structure.
#[derive(Debug)]
pub struct OwnedTrailer {
pub token: OwnedToken,
pub value: String,
}
#[derive(Debug)]
pub struct OwnedToken(String);
impl Deref for OwnedToken {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'a> From<&Trailer<'a>> for OwnedTrailer {
fn from(t: &Trailer<'a>) -> Self {
OwnedTrailer {
token: OwnedToken(t.token.0.to_string()),
value: t.value.to_string(),
}
}
}
impl<'a> From<Trailer<'a>> for OwnedTrailer {
fn from(t: Trailer<'a>) -> Self {
(&t).into()
}
}
impl<'a> From<&'a OwnedTrailer> for Trailer<'a> {
fn from(t: &'a OwnedTrailer) -> Self {
Trailer {
token: Token(t.token.0.as_str()),
value: Cow::from(&t.value),
}
}
}