1
7use askama::Template;
8use askama_web::WebTemplate;
9use axum::extract::{Path, Query, State};
10use axum::response::IntoResponse;
11use serde::Deserialize;
12
13use crate::error::{Error, Result};
14use crate::routes::{AppState, RepoName, commit_date, commit_timestamp};
15
16const PAGE_SIZE: usize = 100;
17
18#[derive(Deserialize)]
19pub(super) struct BranchPath {
20 branch: String,
21}
22
23#[derive(Deserialize)]
24pub(super) struct Pagination {
25 #[serde(default)]
26 page: usize,
27}
28
29struct BranchSummary {
31 name: String,
32 date: String,
33 timestamp: i64,
34 is_default: bool,
35}
36
37#[derive(Template, WebTemplate)]
39#[template(path = "branch_list.html")]
40struct BranchList {
41 repo: String,
42 branches: Vec<BranchSummary>,
43}
44
45struct CommitSummary {
47 sha: String,
48 message: String,
49 author: String,
50 date: String,
51}
52
53#[derive(Template, WebTemplate)]
55#[template(path = "branch_detail.html")]
56struct BranchDetail {
57 repo: String,
58 branch: String,
59 tip_sha: String,
60 commits: Vec<CommitSummary>,
61 page: usize,
62 has_prev: bool,
63 has_next: bool,
64}
65
66pub async fn list(
68 State(state): State<AppState>,
69 RepoName(repo): RepoName,
70) -> Result<impl IntoResponse> {
71 let git_repo =
72 gix::open(state.root.join(&repo)).map_err(|_| Error::RepoNotFound(repo.clone()))?;
73
74 let head_name = git_repo.head_name().ok().flatten();
75
76 let mut branches: Vec<BranchSummary> = git_repo
80 .references()
81 .map_err(|e| Error::GitCorrupt(e.to_string()))?
82 .local_branches()
83 .into_iter()
84 .flatten()
85 .flatten()
86 .filter_map(|mut branch| {
87 let commit = branch.peel_to_commit().ok()?;
88 let date = commit_date(&commit)?;
89 let timestamp = commit_timestamp(&commit)?;
90 let name = branch.name().shorten().to_string();
91 let is_default = head_name
92 .as_ref()
93 .is_some_and(|h| h.as_ref() == branch.name());
94
95 Some(BranchSummary {
96 name,
97 date,
98 timestamp,
99 is_default,
100 })
101 })
102 .collect();
103
104 branches.sort_by(|a, b| {
105 b.is_default
106 .cmp(&a.is_default)
107 .then(b.timestamp.cmp(&a.timestamp))
108 });
109
110 Ok(BranchList { repo, branches })
111}
112
113pub async fn detail(
115 State(state): State<AppState>,
116 RepoName(repo): RepoName,
117 Path(params): Path<BranchPath>,
118 Query(pagination): Query<Pagination>,
119) -> Result<impl IntoResponse> {
120 let branch = params.branch;
121 let page = pagination.page;
122
123 let git_repo =
124 gix::open(state.root.join(&repo)).map_err(|_| Error::RepoNotFound(repo.clone()))?;
125
126 let ref_name = format!("refs/heads/{branch}");
127 let tip = git_repo
128 .find_reference(&ref_name)
129 .map_err(|_| Error::NotFound(format!("branch {branch}")))?
130 .peel_to_commit()
131 .map_err(|e| Error::GitCorrupt(e.to_string()))?;
132
133 let tip_sha = tip.id().to_string();
134
135 let mut commits: Vec<CommitSummary> = tip
137 .id()
138 .ancestors()
139 .all()
140 .map_err(|e| Error::GitCorrupt(e.to_string()))?
141 .filter_map(|info| {
142 let info = info.ok()?;
143 let commit = info.id().object().ok()?.try_into_commit().ok()?;
144
145 let sha = info.id().to_string();
146 let message = commit.message().ok()?.summary().to_string();
147 let author = commit.author().ok()?.name.to_string();
148 let date = commit_date(&commit)?;
149
150 Some(CommitSummary {
151 sha,
152 message,
153 author,
154 date,
155 })
156 })
157 .skip(page * PAGE_SIZE)
158 .take(PAGE_SIZE + 1)
159 .collect();
160
161 let has_next = commits.len() > PAGE_SIZE;
164 if has_next {
165 commits.pop();
166 }
167
168 Ok(BranchDetail {
169 repo,
170 branch,
171 tip_sha,
172 commits,
173 page,
174 has_prev: page > 0,
175 has_next,
176 })
177}