ef0b83a
use std::path::{Path, PathBuf};
use crate::rpcgit::commit::types::ListCommitsOptions;
use serde::{Deserialize, Serialize};
use crate::context::facade::GitContext;
use crate::service::GitService;
use crate::service::cache::{self, IMMUTABLE_TTL};
use crate::share::errors::{GitError, map_baby};
#[derive(Debug, Clone, Serialize, Deserialize, utoipa::ToSchema)]
pub struct PathLogEntryDto {
pub path: String,
pub short_sha: String,
pub date: String,
pub author: String,
pub subject: String,
}
fn validate_target_dir(path: &Path) -> Result<(), GitError> {
if path.as_os_str().is_empty() {
return Err(GitError::InvalidArgument(
"target directory path is empty".to_string(),
));
}
if path.is_absolute() || path.has_root() {
return Err(GitError::InvalidArgument(format!(
"target directory must be relative: `{}`",
path.display()
)));
}
if path
.components()
.any(|c| matches!(c, std::path::Component::ParentDir))
{
return Err(GitError::InvalidArgument(format!(
"target directory may not contain `..`: `{}`",
path.display()
)));
}
Ok(())
}
fn iso_date(t: &time::OffsetDateTime) -> String {
let secs = t.offset().whole_seconds();
let sign = if secs >= 0 { '+' } else { '-' };
let abs = secs.unsigned_abs();
format!(
"{:04}-{:02}-{:02} {:02}:{:02}:{:02} {sign}{:02}{:02}",
t.year(),
u8::from(t.month()),
t.day(),
t.hour(),
t.minute(),
t.second(),
abs / 3600,
(abs % 3600) / 60,
)
}
fn first_line(s: &str) -> String {
s.trim_end()
.lines()
.next()
.unwrap_or("")
.trim_end()
.to_string()
}
impl GitService {
pub async fn path_log(
&self,
ctx: &GitContext,
commit_hash: String,
target_dir: PathBuf,
) -> Result<Vec<PathLogEntryDto>, GitError> {
validate_target_dir(&target_dir)?;
if commit_hash.trim().is_empty() {
return Err(GitError::InvalidArgument(
"commit hash must not be empty".to_string(),
));
}
let baby = self.baby(&ctx);
let oid = baby
.rev_parse(&commit_hash)
.await
.map_err(map_baby)?
.to_string();
let key = format!(
"repo.{}.path_log.{}.{}",
ctx.model.id,
oid,
target_dir.display()
);
cache::cached_fetch(&ctx.cache, &key, IMMUTABLE_TTL, || async {
let entries = baby
.get_tree_entries(&oid, &target_dir)
.await
.map_err(map_baby)?;
let mut out = Vec::with_capacity(entries.len());
for entry in &entries {
if entry.name.starts_with('.') {
continue;
}
let entry_path = match target_dir.to_str() {
Some("") | Some(".") => entry.name.clone(),
Some(d) if d.ends_with('/') => format!("{d}{}", entry.name),
Some(d) => format!("{d}/{}", entry.name),
None => continue,
};
let commits = baby
.list_commits(ListCommitsOptions {
revision: oid.clone(),
path: Some(PathBuf::from(&entry_path)),
max_count: Some(1),
..Default::default()
})
.await
.map_err(map_baby)?;
let Some(commit) = commits.into_iter().next() else {
continue;
};
let hex = commit.id.to_string();
let short_sha = hex.get(..7).unwrap_or("").to_string();
out.push(PathLogEntryDto {
path: entry_path,
short_sha,
date: iso_date(&commit.committer.time),
author: commit.author.name.clone(),
subject: first_line(&commit.message),
});
}
Ok(out)
})
.await
}
}