blob: f1d42c36d73ff17ff993de6cdb56afdb3cac56ef [file] [log] [blame]
//! Commits to the advisory DB git repository
use tame_index::external::gix;
use crate::{
error::{Error, ErrorKind},
repository::{
git::{CommitHash, Repository},
signature::Signature,
},
};
use std::time::{Duration, SystemTime};
/// Number of days after which the repo will be considered stale
/// (90 days)
const STALE_AFTER: Duration = Duration::from_secs(90 * 86400);
/// Information about a commit to the Git repository
#[cfg_attr(docsrs, doc(cfg(feature = "git")))]
#[derive(Debug)]
pub struct Commit {
/// ID (i.e. SHA-1 hash) of the latest commit
pub commit_id: CommitHash,
/// Information about the author of a commit
pub author: String,
/// Summary message for the commit
pub summary: String,
/// Commit time in number of seconds since the UNIX epoch
pub timestamp: time::OffsetDateTime,
/// Signature on the commit (mandatory for Repository::fetch)
// TODO: actually verify signatures
pub signature: Option<Signature>,
/// Signed data to verify along with this commit
signed_data: Option<Vec<u8>>,
}
impl Commit {
/// Get information about HEAD
pub(crate) fn from_repo_head(repo: &Repository) -> Result<Self, Error> {
let commit = repo
.repo
.head_commit()
.map_err(|err| format_err!(ErrorKind::Repo, "unable to locate head commit: {}", err))?;
// Since we are pulling multiple pieces from the commit it's better to do this once
let cref = commit.decode().map_err(|err| {
format_err!(
ErrorKind::Repo,
"unable to decode commit information: {}",
err
)
})?;
let commit_id = commit.id;
let timestamp = crate::repository::git::gix_time_to_time(cref.committer.time);
let author = {
let sig = cref.author();
format!("{} <{}>", sig.name, sig.email)
};
let summary = cref.message_summary().to_string();
if summary.is_empty() {
return Err(format_err!(
ErrorKind::Repo,
"no commit summary for {}",
commit_id
));
}
let commit_id = CommitHash::from_gix(commit_id);
let (signature, signed_data) = if let Some(sig) = cref.extra_headers().pgp_signature() {
// Note this is inefficient as gix doesn't yet support signature extraction natively.
// TODO: convert this to native methods once https://github.com/Byron/gitoxide/pull/973 ships in a stable release.
let signed_data = {
let mut commit_without_signature = cref.clone();
let pos = commit_without_signature
.extra_headers
.iter()
.position(|eh| eh.0 == "gpgsig")
.unwrap();
commit_without_signature.extra_headers.remove(pos);
let mut signed_data = Vec::new();
use gix::objs::WriteTo;
commit_without_signature.write_to(&mut signed_data)?;
signed_data
};
(Some(Signature::from_bytes(sig)?), Some(signed_data))
} else {
(None, None)
};
Ok(Self {
commit_id,
author,
summary,
timestamp,
signature,
signed_data,
})
}
/// Is the commit timestamp "fresh" as in the database has been updated
/// recently? (i.e. 90 days, per the `STALE_AFTER` constant)
pub fn is_fresh(&self) -> bool {
self.timestamp > SystemTime::now().checked_sub(STALE_AFTER).unwrap()
}
/// Get the raw bytes to be verified when verifying a commit signature
pub fn raw_signed_bytes(&self) -> Option<&[u8]> {
self.signed_data.as_ref().map(|bytes| bytes.as_ref())
}
/// Reset the repository's state to match this commit
pub(crate) fn reset(&self, repo: &Repository) -> Result<(), Error> {
let repo = &repo.repo;
let workdir = repo.work_dir().ok_or_else(|| {
format_err!(ErrorKind::Repo, "unable to checkout, repository is bare")
})?;
let root_tree = repo
.find_object(self.commit_id.to_gix())
.map_err(|err| format_err!(ErrorKind::Repo, "unable to locate commit: {}", err))?
.peel_to_tree()
.map_err(|err| format_err!(ErrorKind::Repo, "unable to peel to tree: {}", err))?
.id;
let index = gix::index::State::from_tree(&root_tree, &repo.objects).map_err(|err| {
format_err!(
ErrorKind::Repo,
"failed to create index from tree '{}': {}",
root_tree,
err
)
})?;
let mut index = gix::index::File::from_state(index, repo.index_path());
let opts = gix::worktree::state::checkout::Options {
destination_is_initially_empty: false,
overwrite_existing: true,
..Default::default()
};
gix::worktree::state::checkout(
&mut index,
workdir,
repo.objects.clone(),
&gix::progress::Discard,
&gix::progress::Discard,
&gix::interrupt::IS_INTERRUPTED,
opts,
)
.map_err(|err| format_err!(ErrorKind::Repo, "failed to checkout: {}", err))?;
index
.write(Default::default())
.map_err(|err| format_err!(ErrorKind::Repo, "failed to write index: {}", err))?;
Ok(())
}
}