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