Files
signed/crates/signed_git/src/history.rs
T
reya c76e909f7f
Rust / build (macos-latest, stable) (push) Canceled after 0s
Rust / build (ubuntu-latest, stable) (push) Canceled after 0s
Rust / build (windows-latest, stable) (push) Canceled after 0s
chore: refactor backend around domain types (#26)
Reviewed-on: #26
2026-10-03 10:35:48 +00:00

197 lines
6.0 KiB
Rust

use std::collections::HashSet;
use std::path::{Path, PathBuf};
use anyhow::Result;
use crate::repo::Repo;
#[derive(Debug, Clone)]
pub struct FileCommit {
pub id: String,
pub summary: String,
pub description: Option<String>,
pub author: String,
pub time: i64,
}
impl FileCommit {
fn from_commit(commit: &gix::Commit<'_>) -> Result<FileCommit> {
Self::from_commit_with_description(commit, true)
}
// History lists never display the body,
// skipping it saves an allocation per listed commit.
fn from_commit_summary(commit: &gix::Commit<'_>) -> Result<FileCommit> {
Self::from_commit_with_description(commit, false)
}
fn from_commit_with_description(
commit: &gix::Commit<'_>,
include_description: bool,
) -> Result<FileCommit> {
let author = commit.author()?;
let message = commit.message()?;
Ok(FileCommit {
id: commit.id().shorten_or_id().to_string(),
summary: String::from_utf8_lossy(message.title).trim().to_string(),
description: if include_description {
message
.body
.map(|body| String::from_utf8_lossy(body).trim().to_string())
.filter(|body| !body.is_empty())
} else {
None
},
author: String::from_utf8_lossy(author.name).trim().to_string(),
time: author.time()?.seconds,
})
}
}
impl Repo {
// Paths without any commit, like untracked files, are absent from the result.
pub fn last_commits(&self, rels: &[PathBuf]) -> Result<Vec<(PathBuf, FileCommit)>> {
use gix::traverse::commit::simple::CommitTimeOrder;
let Some(head) = self.inner.head_id().ok() else {
return Ok(Vec::new());
};
let mut pending: Vec<PathBuf> = Vec::with_capacity(rels.len());
let mut seen: HashSet<&Path> = HashSet::with_capacity(rels.len());
for rel in rels {
if seen.insert(rel.as_path()) {
pending.push(rel.clone());
}
}
let walk = self
.inner
.rev_walk([head])
.sorting(gix::revision::walk::Sorting::ByCommitTime(
CommitTimeOrder::NewestFirst,
));
let mut found = Vec::new();
for info in walk.all()? {
if pending.is_empty() {
break;
}
let info = info?;
let commit = info.object()?;
let tree = commit.tree()?;
let parent_tree = match info.parent_ids().next() {
Some(parent) => Some(parent.object()?.into_commit().tree()?),
None => None,
};
// Compare each unresolved path against this commit and its first parent.
let mut ix = 0;
while ix < pending.len() {
let rel = &pending[ix];
let blob = tree.lookup_entry_by_path(rel)?;
let parent_blob = match &parent_tree {
Some(tree) => tree.lookup_entry_by_path(rel)?,
None => None,
};
if blob.map(|entry| entry.id().detach())
!= parent_blob.map(|entry| entry.id().detach())
{
found.push((rel.clone(), FileCommit::from_commit(&commit)?));
pending.swap_remove(ix);
} else {
ix += 1;
}
}
}
Ok(found)
}
pub fn all_commits(&self) -> Result<CommitList> {
use gix::traverse::commit::simple::CommitTimeOrder;
let Some(head) = self.inner.head_id().ok() else {
return Ok(CommitList {
total: 0,
commits: Vec::new(),
});
};
let walk = self
.inner
.rev_walk([head])
.sorting(gix::revision::walk::Sorting::ByCommitTime(
CommitTimeOrder::NewestFirst,
));
let mut commits = Vec::new();
let mut total = 0;
for info in walk.all()? {
let info = info?;
total += 1;
if commits.len() < MAX_LISTED_COMMITS {
commits.push(FileCommit::from_commit_summary(&info.object()?)?);
}
}
Ok(CommitList { total, commits })
}
pub fn commit_range(&self, base: &str, tip: &str) -> Result<Vec<FileCommit>> {
use gix::traverse::commit::simple::CommitTimeOrder;
let base_id = self.inner.rev_parse_single(base.as_bytes())?;
let tip_id = self.inner.rev_parse_single(tip.as_bytes())?;
let walk = self
.inner
.rev_walk([tip_id])
.sorting(gix::revision::walk::Sorting::ByCommitTime(
CommitTimeOrder::NewestFirst,
))
.with_hidden([base_id]);
let mut commits = Vec::new();
for info in walk.all()? {
let info = info?;
commits.push(FileCommit::from_commit_summary(&info.object()?)?);
}
Ok(commits)
}
// `Ok(None)` for an unborn HEAD.
pub fn head_commit(&self) -> Result<Option<FileCommit>> {
let Some(head) = self.inner.head_id().ok() else {
return Ok(None);
};
let commit = head.object()?.into_commit();
Ok(Some(FileCommit::from_commit(&commit)?))
}
// `Ok(None)` when the id cannot be resolved.
pub fn commit(&self, id: &str) -> Result<Option<FileCommit>> {
match self.inner.rev_parse_single(id.as_bytes()) {
Ok(commit_id) => {
let commit = commit_id.object()?.into_commit();
Ok(Some(FileCommit::from_commit(&commit)?))
}
Err(_) => Ok(None),
}
}
}
// The virtual list renders a window at a time, the tab badge shows the real
// count: a huge history is never fully materialized in memory.
pub const MAX_LISTED_COMMITS: usize = 20_000;
pub struct CommitList {
pub total: usize,
pub commits: Vec<FileCommit>,
}