1use flams_ontology::uris::ArchiveId;
2use flams_utils::prelude::HSet;
3use gitlab::api::AsyncQuery;
4use tracing::{Instrument,instrument};
5
6pub mod auth;
7
8lazy_static::lazy_static!{
9 static ref GITLAB: GLInstance = GLInstance::default();
10}
11
12#[derive(Debug)]
13struct ProjectWithId {
14 pub project:super::Project,
15 #[allow(clippy::option_option)]
16 pub id:Option<Option<ArchiveId>>
17}
18impl std::borrow::Borrow<u64> for ProjectWithId {
19 #[inline]
20 fn borrow(&self) -> &u64 {
21 &self.project.id
22 }
23}
24impl std::hash::Hash for ProjectWithId {
25 #[inline]
26 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
27 self.project.id.hash(state);
28 }
29}
30impl PartialEq for ProjectWithId {
31 #[inline]
32 fn eq(&self, other: &Self) -> bool {
33 self.project.id == other.project.id
34 }
35}
36impl Eq for ProjectWithId {}
37
38#[derive(Debug)]
39struct GitLabI {
40 inner: gitlab::AsyncGitlab,
41 url:url::Url,
42 id:Option<Box<str>>,
43 secret:Option<Box<str>>,
44 projects:parking_lot::Mutex<HSet<ProjectWithId>>
45}
46
47#[derive(Debug,Clone)]
48pub struct GitLab(std::sync::Arc<GitLabI>);
49
50#[derive(Debug,Clone,Default)]
51enum MaybeGitlab {
52 #[default]
53 None,
54 Loading,
55 Loaded(GitLab),
56 Failed
57}
58#[derive(Clone,Debug,Default)]
59pub struct GLInstance{
60 inner:std::sync::Arc<parking_lot::RwLock<MaybeGitlab>>
61}
62
63impl GLInstance {
64 #[inline]#[must_use]
65 pub fn global() -> &'static Self {
66 &GITLAB
67 }
68 pub fn load(self,cfg:GitlabConfig) {
69 *self.inner.write() = MaybeGitlab::Loading;
70 let span = tracing::info_span!(target:"git","loading gitlab");
71 tokio::spawn(async move {
72 match GitLab::new(cfg).in_current_span().await {
73 Ok(gl) => {
74 *self.inner.write() = MaybeGitlab::Loaded(gl.clone());
75 let Ok(ps) = gl.get_projects().in_current_span().await else {
76 tracing::error!("Failed to load projects");
77 return
78 };
79 tracing::info!("Loaded {} projects",ps.len());
80 let span2 = tracing::info_span!("loading archive IDs");
81 let mut js = tokio::task::JoinSet::new();
82 for p in ps {
83 if let Some(d) = p.default_branch {
84 let gl = gl.clone();
85 let span = span2.clone();
86 let f = async move {gl.get_archive_id(p.id, &d).instrument(span).await};
87 let _ = js.spawn(f);
88 }
89 }
90 let _ = js.join_all().in_current_span().await;
91 }
92 Err(e) => {
93 tracing::error!("Failed to load gitlab: {e}");
94 *self.inner.write() = MaybeGitlab::Failed;
95 }
96 }
97 }.instrument(span));
98 }
99
100 pub async fn get(&self) -> Option<GitLab> {
101 loop {
102 match &*self.inner.read() {
103 MaybeGitlab::None | MaybeGitlab::Failed => return None,
104 MaybeGitlab::Loading => (),
105 MaybeGitlab::Loaded(gl) => return Some(gl.clone())
106 }
107 tokio::time::sleep(std::time::Duration::from_secs_f32(0.1)).await;
108 }
109 }
110
111 #[inline]#[must_use]
112 pub fn exists(&self) -> bool {
113
114 !matches!(&*self.inner.read(),MaybeGitlab::None)
115 }
116
117 #[inline]#[must_use]
118 pub fn has_loaded(&self) -> bool {
119 matches!(&*self.inner.read(),MaybeGitlab::Loaded(_))
120 }
121}
122
123#[derive(Debug,Clone)]
124pub struct GitlabConfig {
125 url:url::Url,
126 token:Option<String>,
127 app_id:Option<String>,
128 app_secret:Option<String>
129}
130impl GitlabConfig {
131 #[inline]#[must_use]
132 pub const fn new(url:url::Url,token:Option<String>,app_id:Option<String>,app_secret:Option<String>) -> Self {
133 Self { url, token, app_id, app_secret }
134 }
135
136 }
146
147impl GitLab {
148 pub async fn new(cfg:GitlabConfig) -> Result<Self,gitlab::GitlabError> {
150 let GitlabConfig { url, token, app_id, app_secret } = cfg;
151 let Some(url_str) = url.host_str() else {
153 return Err(gitlab::GitlabError::UrlParse{source:url::ParseError::EmptyHost});
154 };
155 let mut builder = token.map_or_else(
156 || gitlab::GitlabBuilder::new_unauthenticated(url_str),
157 |token| gitlab::GitlabBuilder::new(url_str,token)
158 );
159 if matches!(url.scheme(),"http") { builder.insecure(); }
160 Ok(Self(std::sync::Arc::new(GitLabI {
161 inner: builder.build_async().in_current_span().await?,
162 url,
163 id:app_id.map(Into::into),
164 secret:app_secret.map(Into::into),
165 projects:parking_lot::Mutex::new(HSet::default())
166 })))
167 }
168
169 #[must_use]
170 pub fn new_background(cfg:GitlabConfig) -> GLInstance {
171 let r = GLInstance {inner:std::sync::Arc::new(parking_lot::RwLock::new(MaybeGitlab::Loading))};
172 r.clone().load(cfg);
173 r
174 }
175
176 #[instrument(level = "debug",
178 target = "git",
179 name = "getting all gitlab projects",
180 skip_all
181)]
182 pub async fn get_projects(&self) -> Result<Vec<crate::Project>,Err> {
183 use gitlab::api::AsyncQuery;
184 let q = gitlab::api::projects::Projects::builder().simple(true).build().unwrap_or_else(|_| unreachable!());
185 let v: Vec<super::Project> = gitlab::api::paged(q,gitlab::api::Pagination::All).query_async(&self.0.inner).await
186 .map_err(|e| {tracing::error!("Failed to load projects: {e}"); e})?;
187 let mut prs = self.0.projects.lock();
188 for p in &v {
189 if !prs.contains(&p.id) {
190 prs.insert(ProjectWithId { project: p.clone(), id: None });
191 }
192 }
193 drop(prs);
194 Ok(v)
195 }
198
199 #[instrument(level = "debug",
201 target = "git",
202 name = "getting archive id",
203 skip(self),
204)]
205 pub async fn get_archive_id(&self,id:u64,branch:&str) -> Result<Option<ArchiveId>,Err> {
206 {
207 let vs = self.0.projects.lock();
208 if let Some(ProjectWithId{id:Some(id),..}) = vs.get(&id) {
209 return Ok(id.clone())
210 }
211 }
212
213 macro_rules! ret {
214 ($v:expr) => {{
215 tracing::info!("Found {:?}",$v);
216 let mut lock = self.0.projects.lock();
217 if let Some(mut v) = lock.take(&id) {
218 v.id = Some($v.clone());
219 lock.insert(v);
220 }
221 drop(lock);
222 return Ok($v);
223 }}
224 }
225 let r = gitlab::api::projects::repository::TreeBuilder::default()
226 .project(id).ref_(branch).recursive(false).build().unwrap_or_else(|_| unreachable!());
227 let r:Vec<crate::TreeEntry> = r.query_async(&self.0.inner).await?;
228 let Some(p) = r.into_iter().find_map(|e| if e.path.eq_ignore_ascii_case("meta-inf") && matches!(e.kind, crate::DirOrFile::Dir) { Some(e.path) } else {None}) else {
229 ret!(None::<ArchiveId>)
230 };
231
232 let r = gitlab::api::projects::repository::TreeBuilder::default()
233 .project(id).ref_(branch).path(p).recursive(false).build().unwrap_or_else(|_| unreachable!());
234 let r:Vec<crate::TreeEntry> = r.query_async(&self.0.inner).await?;
235 let Some(p) = r.into_iter().find_map(|e| if e.name.eq_ignore_ascii_case("manifest.mf") && matches!(e.kind,crate::DirOrFile::File) { Some(e.path) } else {None}) else {
236 ret!(None::<ArchiveId>)
237 };
238
239 let blob = gitlab::api::projects::repository::files::FileRaw::builder()
240 .project(id).file_path(p).ref_(branch).build().unwrap_or_else(|_| unreachable!());
241 let r = gitlab::api::raw(blob).query_async(&self.0.inner).await?;
242 let r = std::str::from_utf8(&r)?;
243 let r = r.split('\n').find_map(|line| {
244 let line = line.trim();
245 line.strip_prefix("id:").map(|rest| {
246 ArchiveId::new(rest.trim())
247 })
248 });
249 ret!(r)
250 }
251}
252
253#[derive(Debug)]
254pub enum Err {
255 Api(gitlab::api::ApiError<gitlab::RestError>),
256 Str(std::str::Utf8Error),
257 Gitlab(gitlab::GitlabError)
258}
259impl From<gitlab::api::ApiError<gitlab::RestError>> for Err {
260 #[inline]
261 fn from(e: gitlab::api::ApiError<gitlab::RestError>) -> Self {
262 Self::Api(e)
263 }
264}
265impl From<std::str::Utf8Error> for Err {
266 #[inline]
267 fn from(e: std::str::Utf8Error) -> Self {
268 Self::Str(e)
269 }
270}
271impl From<gitlab::GitlabError> for Err {
272 #[inline]
273 fn from(e: gitlab::GitlabError) -> Self {
274 Self::Gitlab(e)
275 }
276}
277impl std::fmt::Display for Err {
278 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
279 match self {
280 Self::Api(e) => e.fmt(f),
281 Self::Str(e) => e.fmt(f),
282 Self::Gitlab(e) => e.fmt(f)
283 }
284 }
285}