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::HandlerWithoutStateExt,
22 http::{Request, StatusCode},
23 response::{IntoResponse, Response},
24};
25use path_clean::PathClean;
26use tower::{util::BoxCloneService, Service};
27
28use self::{
29 about::handle as handle_about,
30 commit::handle as handle_commit,
31 diff::{handle as handle_diff, handle_plain as handle_patch},
32 log::handle as handle_log,
33 refs::handle as handle_refs,
34 smart_git::handle as handle_smart_git,
35 snapshot::handle as handle_snapshot,
36 summary::handle as handle_summary,
37 tag::handle as handle_tag,
38 tree::handle as handle_tree,
39};
40use crate::database::schema::tag::YokedString;
41use crate::{
42 database::schema::{commit::YokedCommit, tag::YokedTag},
43 layers::UnwrapInfallible,
44};
45
46pub const DEFAULT_BRANCHES: [&str; 2] = ["refs/heads/master", "refs/heads/main"];
47
48// this is some wicked, wicked abuse of axum right here...
49#[allow(clippy::trait_duplication_in_bounds)] // clippy seems a bit.. lost
50pub async fn service(mut request: Request<Body>) -> Response {
51 let scan_path = request
52 .extensions()
53 .get::<Arc<PathBuf>>()
54 .expect("scan_path missing");
55
56 let mut child_path = None;
57
58 macro_rules! h {
59 ($handler:ident) => {
60 BoxCloneService::new($handler.into_service())
61 };
62 }
63
64 let uri = request
65 .uri()
66 .path()
67 .trim_start_matches('/')
68 .trim_end_matches('/');
69 let mut uri_parts = memchr::memchr_iter(b'/', uri.as_bytes());
70
71 let original_uri = uri;
72 let (action, mut uri) = if let Some(idx) = uri_parts.next_back() {
73 (uri.get(idx + 1..), &uri[..idx])
74 } else {
75 (None, uri)
76 };
77
78 let mut service = match action {
79 Some("about") => h!(handle_about),
80 Some("git-upload-pack") => h!(handle_smart_git),
81 Some("refs") => {
82 if let Some(idx) = uri_parts.next_back() {
83 if uri.get(idx + 1..) == Some("info") {
84 uri = &uri[..idx];
85 h!(handle_smart_git)
86 } else {
87 h!(handle_refs)
88 }
89 } else {
90 h!(handle_refs)
91 }
92 }
93 Some("log") => h!(handle_log),
94 Some("tree") => h!(handle_tree),
95 Some("commit") => h!(handle_commit),
96 Some("diff") => h!(handle_diff),
97 Some("patch") => h!(handle_patch),
98 Some("tag") => h!(handle_tag),
99 Some("snapshot") => h!(handle_snapshot),
100 Some(_) => {
101 static TREE_FINDER: LazyLock<memchr::memmem::Finder> =
102 LazyLock::new(|| memchr::memmem::Finder::new(b"/tree/"));
103
104 uri = original_uri;
105
106 // match tree children
107 if let Some(idx) = TREE_FINDER.find(uri.as_bytes()) {
108 // 6 is the length of /tree/
109 child_path = Some(Path::new(&uri[idx + 6..]).clean());
110 uri = &uri[..idx];
111 h!(handle_tree)
112 } else {
113 h!(handle_summary)
114 }
115 }
116 None => h!(handle_summary),
117 };
118
119 let uri = Path::new(uri).clean();
120 let path = scan_path.join(&uri);
121
122 let db = request
123 .extensions()
124 .get::<Arc<rocksdb::DB>>()
125 .expect("db extension missing");
126 if path.as_os_str().is_empty()
127 || !crate::database::schema::repository::Repository::exists(db, &uri).unwrap_or_default()
128 {
129 return RepositoryNotFound.into_response();
130 }
131
132 request.extensions_mut().insert(ChildPath(child_path));
133 request.extensions_mut().insert(Repository(uri));
134 request.extensions_mut().insert(RepositoryPath(path));
135
136 service
137 .call(request)
138 .await
139 .unwrap_infallible()
140 .into_response()
141}
142
143#[derive(Clone)]
144pub struct Repository(pub PathBuf);
145
146impl Deref for Repository {
147 type Target = Path;
148
149 fn deref(&self) -> &Self::Target {
150 &self.0
151 }
152}
153
154#[derive(Clone)]
155pub struct RepositoryPath(pub PathBuf);
156
157#[derive(Clone)]
158pub struct ChildPath(pub Option<PathBuf>);
159
160impl Deref for RepositoryPath {
161 type Target = Path;
162
163 fn deref(&self) -> &Self::Target {
164 &self.0
165 }
166}
167
168pub type Result<T, E = Error> = std::result::Result<T, E>;
169
170pub struct InvalidRequest;
171
172impl IntoResponse for InvalidRequest {
173 fn into_response(self) -> Response {
174 (StatusCode::NOT_FOUND, "Invalid request").into_response()
175 }
176}
177
178pub struct RepositoryNotFound;
179
180impl IntoResponse for RepositoryNotFound {
181 fn into_response(self) -> Response {
182 (StatusCode::NOT_FOUND, "Repository not found").into_response()
183 }
184}
185
186pub struct Error(anyhow::Error);
187
188impl From<Arc<anyhow::Error>> for Error {
189 fn from(e: Arc<anyhow::Error>) -> Self {
190 Self(anyhow::Error::msg(format!("{e:?}")))
191 }
192}
193
194impl From<anyhow::Error> for Error {
195 fn from(e: anyhow::Error) -> Self {
196 Self(e)
197 }
198}
199
200impl IntoResponse for Error {
201 fn into_response(self) -> Response {
202 (StatusCode::INTERNAL_SERVER_ERROR, format!("{:?}", self.0)).into_response()
203 }
204}
205
206pub struct Refs {
207 heads: BTreeMap<String, YokedCommit>,
208 tags: Vec<(YokedString, YokedTag)>,
209}