use tame_index::external::gix;
use crate::{
error::{Error, ErrorKind},
repository::{
git::{CommitHash, Repository},
signature::Signature,
},
};
use std::time::{Duration, SystemTime};
const STALE_AFTER: Duration = Duration::from_secs(90 * 86400);
#[cfg_attr(docsrs, doc(cfg(feature = "git")))]
#[derive(Debug)]
pub struct Commit {
pub commit_id: CommitHash,
pub author: String,
pub summary: String,
pub timestamp: time::OffsetDateTime,
pub signature: Option<Signature>,
signed_data: Option<Vec<u8>>,
}
impl Commit {
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))?;
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() {
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,
})
}
pub fn is_fresh(&self) -> bool {
self.timestamp > SystemTime::now().checked_sub(STALE_AFTER).unwrap()
}
pub fn raw_signed_bytes(&self) -> Option<&[u8]> {
self.signed_data.as_ref().map(|bytes| bytes.as_ref())
}
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 all_validations_for_max_safety =
gix::worktree::validate::path::component::Options::default();
let index =
gix::index::State::from_tree(&root_tree, &repo.objects, all_validations_for_max_safety)
.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(())
}
}