mirror of
https://seed.radicle.dev/rad:z6cFWeWpnZNHh9rUW8phgA3b5yGt.git
synced 2026-10-18 19:10:31 +08:00
git-commit: introduce git-commit crate
Porting the commit code that is found in heartwood[0]. It represents a parsed git commit object -- sans the id for the commit itself. [0]: https://github.com/radicle-dev/heartwood/blob/16b22d9178fa6a1aaf2ecb01ec97e067399c5286/radicle/src/git.rs#L392-L575 Signed-off-by: Fintan Halpenny <fintan.halpenny@gmail.com> X-Clacks-Overhead: GNU Terry Pratchett
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
[workspace]
|
||||
members = [
|
||||
"git-ref-format",
|
||||
"git-commit",
|
||||
"git-storage",
|
||||
"git-trailers",
|
||||
"link-git",
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "git-commit"
|
||||
version = "0.1.0"
|
||||
license = "MIT OR Apache-2.0"
|
||||
edition = "2021"
|
||||
authors = [
|
||||
"Alexis Sellier <alexis@radicle.xyz>",
|
||||
"Fintan Halpenny <fintan.halpenny@gmail.com>",
|
||||
]
|
||||
keywords = ["git", "radicle"]
|
||||
|
||||
[dependencies]
|
||||
thiserror = "1"
|
||||
|
||||
[dependencies.git2]
|
||||
version = "0.15.0"
|
||||
default-features = false
|
||||
features = ["vendored-libgit2"]
|
||||
@@ -0,0 +1,119 @@
|
||||
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 }
|
||||
}
|
||||
}
|
||||
|
||||
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 })
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
//! 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};
|
||||
|
||||
/// 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,
|
||||
}
|
||||
|
||||
impl Commit {
|
||||
pub fn new(
|
||||
tree: Oid,
|
||||
parents: Vec<Oid>,
|
||||
author: Author,
|
||||
committer: Author,
|
||||
headers: Headers,
|
||||
message: String,
|
||||
) -> Self {
|
||||
Self {
|
||||
tree,
|
||||
parents,
|
||||
author,
|
||||
committer,
|
||||
headers,
|
||||
message,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 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("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(transparent)]
|
||||
Author(#[from] author::ParseError),
|
||||
#[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;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
tree,
|
||||
parents,
|
||||
author: author.ok_or(error::Parse::Missing("author"))?,
|
||||
committer: committer.ok_or(error::Parse::Missing("committer"))?,
|
||||
headers,
|
||||
message: message.to_owned(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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).ok();
|
||||
buf
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "git-commit-test"
|
||||
version = "0.1.0"
|
||||
license = "MIT OR Apache-2.0"
|
||||
edition = "2021"
|
||||
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
doctest = false
|
||||
test = true
|
||||
doc = false
|
||||
|
||||
[features]
|
||||
test = []
|
||||
|
||||
[dependencies.git-commit]
|
||||
path = ".."
|
||||
|
||||
[dev-dependencies.git2]
|
||||
version = "0.15.0"
|
||||
default-features = false
|
||||
features = ["vendored-libgit2"]
|
||||
|
||||
[dev-dependencies.test-helpers]
|
||||
path = "../../test/test-helpers"
|
||||
@@ -0,0 +1,107 @@
|
||||
use std::str::FromStr as _;
|
||||
|
||||
use git_commit::Commit;
|
||||
|
||||
const UNSIGNED: &str = "\
|
||||
tree c66cc435f83ed0fba90ed4500e9b4b96e9bd001b
|
||||
parent af06ad645133f580a87895353508053c5de60716
|
||||
author Alexis Sellier <alexis@radicle.xyz> 1664467633 +0200
|
||||
committer Alexis Sellier <alexis@radicle.xyz> 1664786099 -0200
|
||||
|
||||
Add SSH functionality with new `radicle-ssh`
|
||||
|
||||
We borrow code from `thrussh`, refactored to be runtime-less.
|
||||
";
|
||||
|
||||
const SSH_SIGNATURE: &str = "\
|
||||
-----BEGIN SSH SIGNATURE-----
|
||||
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgvjrQogRxxLjzzWns8+mKJAGzEX
|
||||
4fm2ALoN7pyvD2ttQAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
|
||||
AAAAQIQvhIewOgGfnXLgR5Qe1ZEr2vjekYXTdOfNWICi6ZiosgfZnIqV0enCPC4arVqQg+
|
||||
GPp0HqxaB911OnSAr6bwU=
|
||||
-----END SSH SIGNATURE-----";
|
||||
|
||||
const PGP_SIGNATURE: &str = "\
|
||||
-----BEGIN PGP SIGNATURE-----
|
||||
iQIzBAABCAAdFiEEHe7BWIo9taTY6TIiJVL7b2QGbLcFAmNcDhsACgkQJVL7b2QG
|
||||
bLcc9Q//RgKf5N4enta9AuszGJZvdFhMPfIDUdw+WAZA6Z8zDPb/aAXZrPP/KIOM
|
||||
zmX08FTqjP9B9YeWrEcFuAtxsRNqbDKrfpko9Y6bTsdrAJg3WIypBb9F8YDKJ6BO
|
||||
CORJJqWOsLW129jW+mJDhcE0YTvPlcMiMI2qjVXKhU6Ag11W8IRZyTb9tvEaDjBR
|
||||
YUnkPvgubv61K9BeUKexE2MakPBldaQtl0MF1Dk7/zo5btLd+KP0SOUKEhuMEu5b
|
||||
LATHHdiYjt/2Xz7q8EcrFxXUaipxZe89dfTdi2ooJQw3ZDqjDHsGTHpDeBuzuSaJ
|
||||
9fKVRwFz/78onfHPhmU4wfUhh+Fcl90p5/T+4dt2K6cr+7rq078e+aJYxkX2d0MG
|
||||
PG0xGP0RN4g+X92K1kGuzoe4870xAnRTNh5nUB+X9snO8tVqQZTb0M2yI+sTsKrv
|
||||
w/f+uiqL6e9DgIxlO5dgiNHCVoCs1QJ900jUGisrlzS4+n6GzMsG6s3c01X4yY9G
|
||||
Ou/kGkMsn7tqejqC9RufygcchCFZqYwaHQwPkiYhfYGMarMpoCFvll0h8tSparpS
|
||||
nnpAQXVdu8m3v1YdPUuTg5ksxSOe9HCIlVXGFhxy3iqCVRn+51FRnUI63rMTOm9/
|
||||
LBqzvji02lDUPGqPgXfcCS0ty8FM2flBIXnwb8TDzCaPYhf53+U=
|
||||
=6dw2
|
||||
-----END PGP SIGNATURE-----";
|
||||
|
||||
const SIGNED: &str = "\
|
||||
tree c66cc435f83ed0fba90ed4500e9b4b96e9bd001b
|
||||
parent af06ad645133f580a87895353508053c5de60716
|
||||
author Alexis Sellier <alexis@radicle.xyz> 1664467633 +0200
|
||||
committer Alexis Sellier <alexis@radicle.xyz> 1664786099 -0200
|
||||
other e6fe3c97619deb8ab4198620f9a7eb79d98363dd
|
||||
gpgsig -----BEGIN SSH SIGNATURE-----
|
||||
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgvjrQogRxxLjzzWns8+mKJAGzEX
|
||||
4fm2ALoN7pyvD2ttQAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
|
||||
AAAAQIQvhIewOgGfnXLgR5Qe1ZEr2vjekYXTdOfNWICi6ZiosgfZnIqV0enCPC4arVqQg+
|
||||
GPp0HqxaB911OnSAr6bwU=
|
||||
-----END SSH SIGNATURE-----
|
||||
gpgsig -----BEGIN PGP SIGNATURE-----
|
||||
iQIzBAABCAAdFiEEHe7BWIo9taTY6TIiJVL7b2QGbLcFAmNcDhsACgkQJVL7b2QG
|
||||
bLcc9Q//RgKf5N4enta9AuszGJZvdFhMPfIDUdw+WAZA6Z8zDPb/aAXZrPP/KIOM
|
||||
zmX08FTqjP9B9YeWrEcFuAtxsRNqbDKrfpko9Y6bTsdrAJg3WIypBb9F8YDKJ6BO
|
||||
CORJJqWOsLW129jW+mJDhcE0YTvPlcMiMI2qjVXKhU6Ag11W8IRZyTb9tvEaDjBR
|
||||
YUnkPvgubv61K9BeUKexE2MakPBldaQtl0MF1Dk7/zo5btLd+KP0SOUKEhuMEu5b
|
||||
LATHHdiYjt/2Xz7q8EcrFxXUaipxZe89dfTdi2ooJQw3ZDqjDHsGTHpDeBuzuSaJ
|
||||
9fKVRwFz/78onfHPhmU4wfUhh+Fcl90p5/T+4dt2K6cr+7rq078e+aJYxkX2d0MG
|
||||
PG0xGP0RN4g+X92K1kGuzoe4870xAnRTNh5nUB+X9snO8tVqQZTb0M2yI+sTsKrv
|
||||
w/f+uiqL6e9DgIxlO5dgiNHCVoCs1QJ900jUGisrlzS4+n6GzMsG6s3c01X4yY9G
|
||||
Ou/kGkMsn7tqejqC9RufygcchCFZqYwaHQwPkiYhfYGMarMpoCFvll0h8tSparpS
|
||||
nnpAQXVdu8m3v1YdPUuTg5ksxSOe9HCIlVXGFhxy3iqCVRn+51FRnUI63rMTOm9/
|
||||
LBqzvji02lDUPGqPgXfcCS0ty8FM2flBIXnwb8TDzCaPYhf53+U=
|
||||
=6dw2
|
||||
-----END PGP SIGNATURE-----
|
||||
|
||||
Add SSH functionality with new `radicle-ssh`
|
||||
|
||||
We borrow code from `thrussh`, refactored to be runtime-less.
|
||||
";
|
||||
|
||||
#[test]
|
||||
fn test_push_header() {
|
||||
let mut commit = Commit::from_str(UNSIGNED).unwrap();
|
||||
|
||||
commit.push_header("other", "e6fe3c97619deb8ab4198620f9a7eb79d98363dd");
|
||||
commit.push_header("gpgsig", SSH_SIGNATURE);
|
||||
commit.push_header("gpgsig", PGP_SIGNATURE);
|
||||
|
||||
assert_eq!(commit.to_string(), SIGNED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_header() {
|
||||
let commit = Commit::from_str(SIGNED).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
commit
|
||||
.signatures()
|
||||
.map(|sig| sig.to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
vec![SSH_SIGNATURE.to_owned(), PGP_SIGNATURE.to_owned()]
|
||||
);
|
||||
assert_eq!(
|
||||
commit.values("other").collect::<Vec<_>>(),
|
||||
vec![String::from("e6fe3c97619deb8ab4198620f9a7eb79d98363dd")],
|
||||
);
|
||||
assert!(commit.values("unknown").next().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conversion() {
|
||||
assert_eq!(Commit::from_str(SIGNED).unwrap().to_string(), SIGNED);
|
||||
assert_eq!(Commit::from_str(UNSIGNED).unwrap().to_string(), UNSIGNED);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
use std::io;
|
||||
|
||||
use git_commit::Commit;
|
||||
use test_helpers::tempdir::WithTmpDir;
|
||||
|
||||
#[test]
|
||||
fn valid_commits() {
|
||||
let radicle_git = format!(
|
||||
"file://{}",
|
||||
git2::Repository::discover(".").unwrap().path().display()
|
||||
);
|
||||
let repo = WithTmpDir::new(|path| {
|
||||
let repo = git2::Repository::clone(&radicle_git, path)
|
||||
.map_err(|err| io::Error::new(io::ErrorKind::Other, err))?;
|
||||
Ok::<_, io::Error>(repo)
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let mut walk = repo.revwalk().unwrap();
|
||||
walk.push_head().unwrap();
|
||||
|
||||
// take the first 20 commits and make sure we can parse them
|
||||
for oid in walk.take(20) {
|
||||
let oid = oid.unwrap();
|
||||
let commit = Commit::read(&repo, oid);
|
||||
assert!(commit.is_ok(), "Oid: {}, Error: {:?}", oid, commit)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#[cfg(test)]
|
||||
mod commit;
|
||||
|
||||
#[cfg(test)]
|
||||
mod integration;
|
||||
@@ -11,6 +11,10 @@ doctest = false
|
||||
test = true
|
||||
doc = false
|
||||
|
||||
[dev-dependencies.git-commit-test]
|
||||
path = "../git-commit/t"
|
||||
features = ["test"]
|
||||
|
||||
[dev-dependencies.git-ext-test]
|
||||
path = "../radicle-git-ext/t"
|
||||
features = ["test"]
|
||||
|
||||
Reference in New Issue
Block a user