Skip to main content

crate_seq_git/
date.rs

1//! Tag date extraction from annotated and lightweight tags.
2
3use chrono::{DateTime, TimeZone, Utc};
4
5use crate::Error;
6
7/// Resolves the author/tagger timestamp for `tag_name`.
8///
9/// For annotated tags, returns the tagger date. For lightweight tags
10/// (which point directly to a commit), returns the committer date.
11///
12/// # Errors
13///
14/// Returns `Error::OpenRepo` if the repository cannot be opened, `Error::TagNotFound`
15/// if the tag does not exist, or `Error::Object` if the timestamp cannot be decoded.
16pub fn tag_date(
17    repo_path: &std::path::Path,
18    tag_name: &str,
19) -> Result<DateTime<Utc>, Error> {
20    let repo = gix::discover(repo_path).map_err(|e| Error::OpenRepo {
21        path: repo_path.to_owned(),
22        source: Box::new(e),
23    })?;
24
25    let ref_name = format!("refs/tags/{tag_name}");
26    let tag_ref = repo
27        .try_find_reference(&ref_name)
28        .map_err(|e| Error::Object(e.to_string()))?
29        .ok_or_else(|| Error::TagNotFound(tag_name.to_owned()))?;
30
31    let target_id = tag_ref.id();
32    let obj = repo
33        .find_object(target_id)
34        .map_err(|e| Error::Object(e.to_string()))?;
35
36    let seconds = resolve_seconds(&repo, obj)?;
37    Utc.timestamp_opt(seconds, 0)
38        .single()
39        .ok_or_else(|| Error::Object("invalid timestamp".into()))
40}
41
42/// Extracts the unix timestamp from either an annotated tag or a commit object.
43///
44/// Annotated tags carry a tagger field; lightweight tags point directly to a
45/// commit, so we fall through to the committer time.
46fn resolve_seconds(
47    repo: &gix::Repository,
48    obj: gix::Object<'_>,
49) -> Result<i64, Error> {
50    match obj.kind {
51        gix::object::Kind::Tag => {
52            let tag = obj
53                .try_into_tag()
54                .map_err(|e| Error::Object(e.to_string()))?;
55            let tagger = tag
56                .tagger()
57                .map_err(|e| Error::Object(e.to_string()))?
58                .ok_or_else(|| Error::Object("annotated tag has no tagger field".into()))?;
59            let time = tagger
60                .time()
61                .map_err(|e| Error::Object(e.to_string()))?;
62            Ok(time.seconds)
63        }
64        gix::object::Kind::Commit => {
65            let commit = obj
66                .try_into_commit()
67                .map_err(|e| Error::Object(e.to_string()))?;
68            let time = commit
69                .time()
70                .map_err(|e| Error::Object(e.to_string()))?;
71            Ok(time.seconds)
72        }
73        _ => {
74            let commit = repo
75                .find_object(obj.id)
76                .map_err(|e| Error::Object(e.to_string()))?
77                .peel_to_commit()
78                .map_err(|e| Error::Object(e.to_string()))?;
79            let time = commit
80                .time()
81                .map_err(|e| Error::Object(e.to_string()))?;
82            Ok(time.seconds)
83        }
84    }
85}