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::routes::{AppState, RepoName, commit_date, commit_timestamp};
17
18#[derive(Deserialize)]
19pub(super) struct TagPath {
20 tag: String,
21}
22
23struct TagSummary {
25 name: String,
26 date: String,
27 timestamp: i64,
28}
29
30#[derive(Template, WebTemplate)]
32#[template(path = "tag_list.html")]
33struct TagList {
34 repo: String,
35 tags: Vec<TagSummary>,
36}
37
38#[derive(Template, WebTemplate)]
40#[template(path = "tag_detail.html")]
41struct TagDetail {
42 repo: String,
43 tag: String,
44 tagger: Option<String>,
46 tag_date: Option<String>,
47 message: Option<String>,
49 commit_sha: String,
50 commit_author: String,
51 commit_date: String,
52}
53
54pub async fn list(
56 State(state): State<AppState>,
57 RepoName(repo): RepoName,
58) -> Result<impl IntoResponse> {
59 let git_repo =
60 gix::open(state.root.join(&repo)).map_err(|_| Error::RepoNotFound(repo.clone()))?;
61
62 let mut tags: Vec<TagSummary> = git_repo
66 .references()
67 .map_err(|e| Error::GitCorrupt(e.to_string()))?
68 .tags()
69 .into_iter()
70 .flatten()
71 .flatten()
72 .filter_map(|mut tag| {
73 let commit = tag.peel_to_commit().ok()?;
74 let date = commit_date(&commit)?;
75 let timestamp = commit_timestamp(&commit)?;
76 let name = tag.name().shorten().to_string();
77
78 Some(TagSummary {
79 name,
80 date,
81 timestamp,
82 })
83 })
84 .collect();
85
86 tags.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
87
88 Ok(TagList { repo, tags })
89}
90
91pub async fn detail(
93 State(state): State<AppState>,
94 RepoName(repo): RepoName,
95 Path(params): Path<TagPath>,
96) -> Result<impl IntoResponse> {
97 let tag_name = params.tag;
98
99 let git_repo =
100 gix::open(state.root.join(&repo)).map_err(|_| Error::RepoNotFound(repo.clone()))?;
101
102 let ref_name = format!("refs/tags/{tag_name}");
103 let mut reference = git_repo
104 .find_reference(&ref_name)
105 .map_err(|_| Error::NotFound(format!("tag {tag_name}")))?;
106
107 let (tagger, tag_date, message) = match reference.peel_to_tag().ok() {
109 None => (None, None, None),
110 Some(t) => match t.decode() {
111 Err(_) => (None, None, None),
112 Ok(decoded) => {
113 let sig = decoded.tagger().ok().flatten();
114 let tagger = sig.as_ref().map(|s| s.name.to_str_lossy().into_owned());
115 let tag_date = sig.as_ref().and_then(|s| {
116 let mut parts = s.time.splitn(2, ' ');
118 let seconds: i64 = parts.next()?.parse().ok()?;
119 let offset_str = parts.next().unwrap_or("+0000");
120 let sign = if offset_str.starts_with('-') {
121 -1i32
122 } else {
123 1
124 };
125 let hhmm = offset_str.trim_start_matches(['+', '-']);
126 let h: i32 = hhmm.get(..2)?.parse().ok()?;
127 let m: i32 = hhmm.get(2..4)?.parse().ok()?;
128 let offset_secs = sign * (h * 3600 + m * 60);
129 let zone = jiff::tz::TimeZone::fixed(
130 jiff::tz::Offset::from_seconds(offset_secs).ok()?,
131 );
132 jiff::Timestamp::from_second(seconds)
133 .map(|ts| ts.to_zoned(zone).date().to_string())
134 .ok()
135 });
136 let message = decoded.message.to_str_lossy().trim().to_string();
137 let message = if message.is_empty() {
138 None
139 } else {
140 Some(message)
141 };
142 (tagger, tag_date, message)
143 }
144 },
145 };
146
147 let commit = reference
148 .peel_to_commit()
149 .map_err(|e| Error::GitCorrupt(e.to_string()))?;
150
151 let commit_sha = commit.id().to_string();
152 let commit_author = commit
153 .author()
154 .map(|a| a.name.to_string())
155 .unwrap_or_default();
156 let commit_date = commit_date(&commit).unwrap_or_default();
157
158 Ok(TagDetail {
159 repo,
160 tag: tag_name,
161 tagger,
162 tag_date,
163 message,
164 commit_sha,
165 commit_author,
166 commit_date,
167 })
168}