use std::pin::Pin;
use std::task::{Context, Poll};
use futures_core::Stream;
use tokio::sync::mpsc;
use super::{Repo, RepoError};
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct CommitInfo {
pub id: String,
pub summary: String,
pub message: String,
pub author_name: String,
pub author_email: String,
pub time_seconds: i64,
}
pub struct CommitWalk {
rx: mpsc::Receiver<Result<CommitInfo, RepoError>>,
}
impl std::fmt::Debug for CommitWalk {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CommitWalk").field("rx", &"<mpsc::Receiver>").finish()
}
}
impl Stream for CommitWalk {
type Item = Result<CommitInfo, RepoError>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
self.rx.poll_recv(cx)
}
}
impl Repo {
pub fn walk(&self, revspec: &str) -> Result<CommitWalk, RepoError> {
let inner = self.thread_safe();
let (tips, boundary) = {
let repo = inner.to_thread_local();
let spec = repo
.rev_parse(revspec)
.map_err(|_| RepoError::RevspecNotFound { revspec: revspec.to_string() })?;
tips_and_boundary(spec.detach())?
};
let (tx, rx) = mpsc::channel(64);
let inner_for_task = inner;
tokio::task::spawn_blocking(move || {
run_walk(&inner_for_task, tips, boundary, &tx);
});
Ok(CommitWalk { rx })
}
}
fn tips_and_boundary(
spec: gix::revision::plumbing::Spec,
) -> Result<(Vec<gix::ObjectId>, Vec<gix::ObjectId>), RepoError> {
use gix::revision::plumbing::Spec as S;
match spec {
S::Include(oid) => Ok((vec![oid], vec![])),
S::Range { from, to } => Ok((vec![to], vec![from])),
S::Merge { theirs, ours } => Ok((vec![theirs, ours], vec![])),
other => {
Err(RepoError::WalkFailed { cause: format!("unsupported revspec kind: {other:?}") })
}
}
}
fn run_walk(
inner: &gix::ThreadSafeRepository,
tips: Vec<gix::ObjectId>,
boundary: Vec<gix::ObjectId>,
tx: &mpsc::Sender<Result<CommitInfo, RepoError>>,
) {
let repo = inner.to_thread_local();
let mut platform = repo.rev_walk(tips);
if !boundary.is_empty() {
platform = platform.with_boundary(boundary);
}
let walk = match platform.all() {
Ok(w) => w,
Err(e) => {
let _ = tx
.blocking_send(Err(RepoError::WalkFailed { cause: format!("rev_walk init: {e}") }));
return;
}
};
for item in walk {
let info = match item {
Ok(i) => i,
Err(e) => {
let _ = tx
.blocking_send(Err(RepoError::WalkFailed { cause: format!("walk item: {e}") }));
return;
}
};
let commit = match info.object() {
Ok(c) => c,
Err(e) => {
let _ = tx.blocking_send(Err(RepoError::WalkFailed {
cause: format!("commit object: {e}"),
}));
return;
}
};
let payload = match make_commit_info(&commit) {
Ok(p) => p,
Err(e) => {
let _ = tx.blocking_send(Err(e));
return;
}
};
if tx.blocking_send(Ok(payload)).is_err() {
return;
}
}
}
fn make_commit_info(commit: &gix::Commit<'_>) -> Result<CommitInfo, RepoError> {
let id = commit.id().to_string();
let message_ref = commit
.message()
.map_err(|e| RepoError::WalkFailed { cause: format!("commit message: {e}") })?;
let summary = message_ref.summary().to_string();
let message_full = message_ref.body.map_or_else(
|| message_ref.title.to_string(),
|body| format!("{}\n\n{}", message_ref.title, body),
);
let author = commit
.author()
.map_err(|e| RepoError::WalkFailed { cause: format!("commit author: {e}") })?;
let author_name = author.name.to_string();
let author_email = author.email.to_string();
let time_seconds = author.time().ok().map_or(0, |t| t.seconds);
Ok(CommitInfo { id, summary, message: message_full, author_name, author_email, time_seconds })
}