1mod about;
2mod commit;
3mod diff;
4mod log;
5mod refs;
6mod smart_git;
7mod snapshot;
8mod summary;
9mod tag;
10mod tree;
11
12use std::{
13 collections::BTreeMap,
14 ops::Deref,
15 path::{Path, PathBuf},
16 sync::{Arc, LazyLock},
17};
18
19use axum::{
20 body::Body,
21 handler::Handler,
22 http::{Request, StatusCode},
23 response::{IntoResponse, Response},
24};
25use path_clean::PathClean;
26
27use self::{
28 about::handle as handle_about,
29 commit::handle as handle_commit,
30 diff::{handle as handle_diff, handle_plain as handle_patch},
31 log::handle as handle_log,
32 refs::handle as handle_refs,
33 smart_git::handle as handle_smart_git,
34 snapshot::handle as handle_snapshot,
35 summary::handle as handle_summary,
36 tag::handle as handle_tag,
37 tree::handle as handle_tree,
38};
39use crate::database::schema::tag::YokedString;
40use crate::database::schema::{commit::YokedCommit, tag::YokedTag};
41
42pub const DEFAULT_BRANCHES: [&str; 2] = ["refs/heads/master", "refs/heads/main"];
43
44// this is some wicked, wicked abuse of axum right here...
45#[allow(clippy::trait_duplication_in_bounds)] // clippy seems a bit.. lost
46pub async fn service(mut request: Request<Body>) -> Response {
47 let scan_path = request
48 .extensions()
49 .get::<Arc<PathBuf>>()
50 .expect("scan_path missing");
51
52 let ParsedUri {
53 uri,
54 child_path,
55 action,
56 } = parse_uri(request.uri().path().trim_matches('/'));
57
58 let uri = Path::new(uri).clean();
59 let path = scan_path.join(&uri);
60
61 let db = request
62 .extensions()
63 .get::<Arc<rocksdb::DB>>()
64 .expect("db extension missing");
65 if path.as_os_str().is_empty()
66 || !crate::database::schema::repository::Repository::exists(db, &uri).unwrap_or_default()
67 {
68 return RepositoryNotFound.into_response();
69 }
70
71 request.extensions_mut().insert(ChildPath(child_path));
72 request.extensions_mut().insert(Repository(uri));
73 request.extensions_mut().insert(RepositoryPath(path));
74
75 match action {
76 HandlerAction::About => handle_about.call(request, None::<()>).await,
77 HandlerAction::SmartGit => handle_smart_git.call(request, None::<()>).await,
78 HandlerAction::Refs => handle_refs.call(request, None::<()>).await,
79 HandlerAction::Log => handle_log.call(request, None::<()>).await,
80 HandlerAction::Tree => handle_tree.call(request, None::<()>).await,
81 HandlerAction::Commit => handle_commit.call(request, None::<()>).await,
82 HandlerAction::Diff => handle_diff.call(request, None::<()>).await,
83 HandlerAction::Patch => handle_patch.call(request, None::<()>).await,
84 HandlerAction::Tag => handle_tag.call(request, None::<()>).await,
85 HandlerAction::Snapshot => handle_snapshot.call(request, None::<()>).await,
86 HandlerAction::Summary => handle_summary.call(request, None::<()>).await,
87 }
88}
89
90#[derive(Debug, PartialEq, Eq)]
91struct ParsedUri<'a> {
92 action: HandlerAction,
93 uri: &'a str,
94 child_path: Option<PathBuf>,
95}
96
97fn parse_uri(uri: &str) -> ParsedUri<'_> {
98 let mut uri_parts = memchr::memchr_iter(b'/', uri.as_bytes());
99
100 let original_uri = uri;
101 let (action, mut uri) = if let Some(idx) = uri_parts.next_back() {
102 (uri.get(idx + 1..), &uri[..idx])
103 } else {
104 (None, uri)
105 };
106
107 match action {
108 Some("about") => ParsedUri {
109 action: HandlerAction::About,
110 uri,
111 child_path: None,
112 },
113 Some("git-upload-pack") => ParsedUri {
114 action: HandlerAction::SmartGit,
115 uri,
116 child_path: None,
117 },
118 Some("refs") => {
119 if let Some(idx) = uri_parts.next_back() {
120 if uri.get(idx + 1..) == Some("info") {
121 ParsedUri {
122 action: HandlerAction::SmartGit,
123 uri: &uri[..idx],
124 child_path: None,
125 }
126 } else {
127 ParsedUri {
128 action: HandlerAction::Refs,
129 uri,
130 child_path: None,
131 }
132 }
133 } else {
134 ParsedUri {
135 action: HandlerAction::Refs,
136 uri,
137 child_path: None,
138 }
139 }
140 }
141 Some("log") => ParsedUri {
142 action: HandlerAction::Log,
143 uri,
144 child_path: None,
145 },
146 Some("tree") => ParsedUri {
147 action: HandlerAction::Tree,
148 uri,
149 child_path: None,
150 },
151 Some("commit") => ParsedUri {
152 action: HandlerAction::Commit,
153 uri,
154 child_path: None,
155 },
156 Some("diff") => ParsedUri {
157 action: HandlerAction::Diff,
158 uri,
159 child_path: None,
160 },
161 Some("patch") => ParsedUri {
162 action: HandlerAction::Patch,
163 uri,
164 child_path: None,
165 },
166 Some("tag") => ParsedUri {
167 action: HandlerAction::Tag,
168 uri,
169 child_path: None,
170 },
171 Some("snapshot") => ParsedUri {
172 action: HandlerAction::Snapshot,
173 uri,
174 child_path: None,
175 },
176 Some(_) => {
177 static TREE_FINDER: LazyLock<memchr::memmem::Finder> =
178 LazyLock::new(|| memchr::memmem::Finder::new(b"/tree/"));
179
180 uri = original_uri;
181
182 // match tree children
183 if let Some(idx) = TREE_FINDER.find(uri.as_bytes()) {
184 ParsedUri {
185 action: HandlerAction::Tree,
186 uri: &uri[..idx],
187 // 6 is the length of /tree/
188 child_path: Some(Path::new(&uri[idx + 6..]).clean()),
189 }
190 } else {
191 ParsedUri {
192 action: HandlerAction::Summary,
193 uri,
194 child_path: None,
195 }
196 }
197 }
198 None => ParsedUri {
199 action: HandlerAction::Summary,
200 uri,
201 child_path: None,
202 },
203 }
204}
205
206#[derive(Copy, Clone, Debug, PartialEq, Eq)]
207enum HandlerAction {
208 About,
209 SmartGit,
210 Refs,
211 Log,
212 Tree,
213 Commit,
214 Diff,
215 Patch,
216 Tag,
217 Snapshot,
218 Summary,
219}
220
221#[derive(Clone)]
222pub struct Repository(pub PathBuf);
223
224impl Deref for Repository {
225 type Target = Path;
226
227 fn deref(&self) -> &Self::Target {
228 &self.0
229 }
230}
231
232#[derive(Clone)]
233pub struct RepositoryPath(pub PathBuf);
234
235#[derive(Clone)]
236pub struct ChildPath(pub Option<PathBuf>);
237
238impl Deref for RepositoryPath {
239 type Target = Path;
240
241 fn deref(&self) -> &Self::Target {
242 &self.0
243 }
244}
245
246pub type Result<T, E = Error> = std::result::Result<T, E>;
247
248pub struct InvalidRequest;
249
250impl IntoResponse for InvalidRequest {
251 fn into_response(self) -> Response {
252 (StatusCode::NOT_FOUND, "Invalid request").into_response()
253 }
254}
255
256pub struct RepositoryNotFound;
257
258impl IntoResponse for RepositoryNotFound {
259 fn into_response(self) -> Response {
260 (StatusCode::NOT_FOUND, "Repository not found").into_response()
261 }
262}
263
264pub struct Error(anyhow::Error);
265
266impl From<Arc<anyhow::Error>> for Error {
267 fn from(e: Arc<anyhow::Error>) -> Self {
268 Self(anyhow::Error::msg(format!("{e:?}")))
269 }
270}
271
272impl From<anyhow::Error> for Error {
273 fn from(e: anyhow::Error) -> Self {
274 Self(e)
275 }
276}
277
278impl IntoResponse for Error {
279 fn into_response(self) -> Response {
280 (StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", self.0)).into_response()
281 }
282}
283
284pub struct Refs {
285 heads: BTreeMap<String, YokedCommit>,
286 tags: Vec<(YokedString, YokedTag)>,
287}