Parent [>]
1//! Git repository helpers used across route handlers.
2//!
3//! Provides convenience functions for common `gix` operations such as opening a
4//! repository, formatting commit dates, validating paths, and iterating
5//! references.  The [`GitResultExt`] extension trait lets handlers convert gix
6//! errors into [`Error::GitCorrupt`] with a single `.corrupt()` call.
7
8use std::path::Path;
9
10use jiff::Timestamp;
11use jiff::tz::{Offset, TimeZone};
12
13use crate::error::{Error, Result as CrateResult};
14
15/// Opens a repository at `root/{name}` or returns [`Error::RepoNotFound`].
16pub fn open_repo(root: &Path, name: &str) -> CrateResult<gix::Repository> {
17    gix::open(root.join(name)).map_err(|_| Error::RepoNotFound(name.to_owned()))
18}
19
20/// Rejects paths containing non-normal [`Component`]s (e.g. `..`) to prevent
21/// directory traversal.
22pub fn validate_path(path: &str) -> CrateResult<()> {
23    if std::path::Path::new(path)
24        .components()
25        .any(|c| !matches!(c, std::path::Component::Normal(_)))
26    {
27        return Err(Error::BadRequest("invalid path".to_string()));
28    }
29    Ok(())
30}
31
32/// Returns the commit's author timestamp as Unix seconds.
33pub fn commit_timestamp(commit: &gix::Commit) -> Option<i64> {
34    commit.time().ok().map(|t| t.seconds)
35}
36
37/// Formats a commit's author date as a `YYYY-MM-DD` string in the commit's
38/// local timezone, returning `None` if the time cannot be parsed.
39pub fn commit_date(commit: &gix::Commit) -> Option<String> {
40    let time = commit.time().ok()?;
41    format_time(time.seconds, time.offset)
42}
43
44/// Formats a commit's committer date as a `YYYY-MM-DD` string in the
45/// commit's local timezone, returning `None` if the time cannot be parsed.
46pub fn committer_date(commit: &gix::Commit) -> Option<String> {
47    let sig = commit.committer().ok()?;
48    let time = sig.time().ok()?;
49    format_time(time.seconds, time.offset)
50}
51
52/// Converts a Unix timestamp + timezone offset into a `YYYY-MM-DD` string.
53fn format_time(seconds: i64, offset: i32) -> Option<String> {
54    let zone = TimeZone::fixed(Offset::from_seconds(offset).ok()?);
55    Timestamp::from_second(seconds)
56        .map(|ts| ts.to_zoned(zone).date().to_string())
57        .ok()
58}
59
60/// Extension trait adding `.corrupt()` to `Result<T, E>` where `E: Display`.
61///
62/// Converts an error into [`Error::GitCorrupt`] by calling `.to_string()` on
63/// the inner error value.  This removes the need for repetitive
64/// `.map_err(|e| Error::GitCorrupt(e.to_string()))` chains.
65///
66/// # Usage
67///
68/// ```ignore
69/// use crate::git::GitResultExt as _;
70/// let repo = git_repo.references().corrupt()?;
71/// ```
72pub trait GitResultExt<T> {
73    fn corrupt(self) -> CrateResult<T>;
74}
75
76impl<T, E: std::fmt::Display> GitResultExt<T> for std::result::Result<T, E> {
77    fn corrupt(self) -> CrateResult<T> {
78        self.map_err(|e| Error::GitCorrupt(e.to_string()))
79    }
80}
81
82/// Silently flattens a `Result<impl IntoIterator<Item = Result<T, E2>>, E1>` into
83/// an `Iterator<Item = T>`.
84///
85/// Both the outer and inner `Result` layers are consumed via `.into_iter()` and
86/// `.flatten()`, so errors at either level are silently skipped.  This matches
87/// the common git-ref iteration pattern where a single corrupt ref should not
88/// crash the page.
89pub fn flatten_refs<T, E1, E2, I>(result: std::result::Result<I, E1>) -> impl Iterator<Item = T>
90where
91    I: IntoIterator<Item = std::result::Result<T, E2>>,
92{
93    result.into_iter().flatten().flatten()
94}