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