1
8use askama::Template;
9use askama_web::WebTemplate;
10use axum::extract::{Path, State};
11use axum::response::IntoResponse;
12use gix::bstr::ByteSlice;
13use serde::Deserialize;
14
15use crate::error::{Error, Result};
16use crate::git::{self, GitResultExt as _, commit_date, commit_timestamp};
17use crate::routes::{AppState, RepoName};
18
19#[derive(Deserialize)]
20pub(super) struct TagPath {
21 tag: String,
22}
23
24struct TagSummary {
26 name: String,
27 date: String,
28 timestamp: i64,
29}
30
31#[derive(Template, WebTemplate)]
33#[template(path = "tag_list.html")]
34struct TagList {
35 repo: String,
36 tags: Vec<TagSummary>,
37}
38
39#[derive(Template, WebTemplate)]
41#[template(path = "tag_detail.html")]
42struct TagDetail {
43 repo: String,
44 tag: String,
45 tagger: Option<String>,
47 tag_date: Option<String>,
48 message: Option<String>,
50 commit_sha: String,
51 commit_author: String,
52 commit_date: String,
53}
54
55pub async fn list(
57 State(state): State<AppState>,
58 RepoName(repo): RepoName,
59) -> Result<impl IntoResponse> {
60 let git_repo = git::open_repo(&state.root, &repo)?;
61
62 let mut tags: Vec<TagSummary> = git::flatten_refs(git_repo.references().corrupt()?.tags())
66 .filter_map(|mut tag| {
67 let commit = tag.peel_to_commit().ok()?;
68 let date = commit_date(&commit)?;
69 let timestamp = commit_timestamp(&commit)?;
70 let name = tag.name().shorten().to_string();
71
72 Some(TagSummary {
73 name,
74 date,
75 timestamp,
76 })
77 })
78 .collect();
79
80 tags.sort_by_key(|b| std::cmp::Reverse(b.timestamp));
81
82 Ok(TagList { repo, tags })
83}
84
85pub async fn detail(
87 State(state): State<AppState>,
88 RepoName(repo): RepoName,
89 Path(params): Path<TagPath>,
90) -> Result<impl IntoResponse> {
91 let tag_name = params.tag;
92
93 let git_repo = git::open_repo(&state.root, &repo)?;
94
95 let ref_name = format!("refs/tags/{tag_name}");
96 let mut reference = git_repo
97 .find_reference(&ref_name)
98 .map_err(|_| Error::NotFound(format!("tag {tag_name}")))?;
99
100 let (tagger, tag_date, message) = reference
102 .peel_to_tag()
103 .ok()
104 .and_then(|t| {
105 let decoded = t.decode().ok()?;
106 let sig = decoded.tagger().ok().flatten();
107 let tagger = sig.as_ref().map(|s| s.name.to_str_lossy().into_owned());
108 let tag_date = sig.as_ref().and_then(|s| {
109 let mut parts = s.time.splitn(2, ' ');
111 let seconds: i64 = parts.next()?.parse().ok()?;
112 let offset_str = parts.next().unwrap_or("+0000");
113 let sign = if offset_str.starts_with('-') {
114 -1i32
115 } else {
116 1
117 };
118 let hhmm = offset_str.trim_start_matches(['+', '-']);
119 let h: i32 = hhmm.get(..2)?.parse().ok()?;
120 let m: i32 = hhmm.get(2..4)?.parse().ok()?;
121 let offset_secs = sign * (h * 3600 + m * 60);
122 let zone =
123 jiff::tz::TimeZone::fixed(jiff::tz::Offset::from_seconds(offset_secs).ok()?);
124 jiff::Timestamp::from_second(seconds)
125 .map(|ts| ts.to_zoned(zone).date().to_string())
126 .ok()
127 });
128 let message = decoded.message.to_str_lossy().trim().to_string();
129 let message = if message.is_empty() {
130 None
131 } else {
132 Some(message)
133 };
134 Some((tagger, tag_date, message))
135 })
136 .unwrap_or((None, None, None));
137
138 let commit = reference.peel_to_commit().corrupt()?;
139
140 let commit_sha = commit.id().to_string();
141 let commit_author = commit
142 .author()
143 .map(|a| a.name.to_string())
144 .unwrap_or_default();
145 let commit_date = commit_date(&commit).unwrap_or_default();
146
147 Ok(TagDetail {
148 repo,
149 tag: tag_name,
150 tagger,
151 tag_date,
152 message,
153 commit_sha,
154 commit_author,
155 commit_date,
156 })
157}