flams_git/gl/
mod.rs

1use flams_utils::prelude::HSet;
2use ftml_uris::ArchiveId;
3use gitlab::api::AsyncQuery;
4use tracing::{instrument, Instrument};
5
6pub mod auth;
7
8static GITLAB: std::sync::LazyLock<GLInstance> = std::sync::LazyLock::new(GLInstance::default);
9
10#[derive(Debug)]
11struct ProjectWithId {
12    pub project: flams_backend_types::git::Project,
13    #[allow(clippy::option_option)]
14    pub id: Option<Option<ArchiveId>>,
15}
16impl std::borrow::Borrow<u64> for ProjectWithId {
17    #[inline]
18    fn borrow(&self) -> &u64 {
19        &self.project.id
20    }
21}
22impl std::hash::Hash for ProjectWithId {
23    #[inline]
24    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
25        self.project.id.hash(state);
26    }
27}
28impl PartialEq for ProjectWithId {
29    #[inline]
30    fn eq(&self, other: &Self) -> bool {
31        self.project.id == other.project.id
32    }
33}
34impl Eq for ProjectWithId {}
35
36#[derive(Debug)]
37struct GitLabI {
38    inner: gitlab::AsyncGitlab,
39    url: url::Url,
40    id: Option<Box<str>>,
41    secret: Option<Box<str>>,
42    projects: parking_lot::Mutex<HSet<ProjectWithId>>,
43}
44
45#[derive(Debug, Clone)]
46pub struct GitLab(std::sync::Arc<GitLabI>);
47
48#[derive(Debug, Clone, Default)]
49enum MaybeGitlab {
50    #[default]
51    None,
52    Loading,
53    Loaded(GitLab),
54    Failed,
55}
56#[derive(Clone, Debug, Default)]
57pub struct GLInstance {
58    inner: std::sync::Arc<parking_lot::RwLock<MaybeGitlab>>,
59}
60
61impl GLInstance {
62    #[inline]
63    #[must_use]
64    pub fn global() -> &'static Self {
65        &GITLAB
66    }
67    pub fn load(self, cfg: GitlabConfig) {
68        *self.inner.write() = MaybeGitlab::Loading;
69        let span = tracing::info_span!(target:"git","loading gitlab");
70        tokio::spawn(
71            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 {
87                                    gl.get_archive_id(p.id, &d).instrument(span).await
88                                };
89                                let _ = js.spawn(f);
90                            }
91                        }
92                        let _ = js.join_all().in_current_span().await;
93                    }
94                    Err(e) => {
95                        tracing::error!("Failed to load gitlab: {e}");
96                        *self.inner.write() = MaybeGitlab::Failed;
97                    }
98                }
99            }
100            .instrument(span),
101        );
102    }
103
104    pub async fn get(&self) -> Option<GitLab> {
105        loop {
106            match &*self.inner.read() {
107                MaybeGitlab::None | MaybeGitlab::Failed => return None,
108                MaybeGitlab::Loading => (),
109                MaybeGitlab::Loaded(gl) => return Some(gl.clone()),
110            }
111            tokio::time::sleep(std::time::Duration::from_secs_f32(0.1)).await;
112        }
113    }
114
115    #[inline]
116    #[must_use]
117    pub fn exists(&self) -> bool {
118        !matches!(&*self.inner.read(), MaybeGitlab::None)
119    }
120
121    #[inline]
122    #[must_use]
123    pub fn has_loaded(&self) -> bool {
124        matches!(&*self.inner.read(), MaybeGitlab::Loaded(_))
125    }
126}
127
128#[derive(Debug, Clone)]
129pub struct GitlabConfig {
130    url: url::Url,
131    token: Option<String>,
132    app_id: Option<String>,
133    app_secret: Option<String>,
134}
135impl GitlabConfig {
136    #[inline]
137    #[must_use]
138    pub const fn new(
139        url: url::Url,
140        token: Option<String>,
141        app_id: Option<String>,
142        app_secret: Option<String>,
143    ) -> Self {
144        Self {
145            url,
146            token,
147            app_id,
148            app_secret,
149        }
150    }
151
152    /*fn split(url:&str) -> (&str,bool) {
153      if let Some(r) = url.strip_prefix("https://") {
154        return (r,false)
155      }
156      if let Some(r) = url.strip_prefix("http://") {
157        return (r,true)
158      }
159      (url,false)
160    }*/
161}
162
163impl GitLab {
164    /// #### Errors
165    pub async fn new(cfg: GitlabConfig) -> Result<Self, gitlab::GitlabError> {
166        let GitlabConfig {
167            url,
168            token,
169            app_id,
170            app_secret,
171        } = cfg;
172        //let (split_url,http) = GitlabConfig::split(&url);
173        let Some(url_str) = url.host_str() else {
174            return Err(gitlab::GitlabError::UrlParse {
175                source: url::ParseError::EmptyHost,
176            });
177        };
178        let mut builder = token.map_or_else(
179            || gitlab::GitlabBuilder::new_unauthenticated(url_str),
180            |token| gitlab::GitlabBuilder::new(url_str, token),
181        );
182        if matches!(url.scheme(), "http") {
183            builder.insecure();
184        }
185        Ok(Self(std::sync::Arc::new(GitLabI {
186            inner: builder.build_async().in_current_span().await?,
187            url,
188            id: app_id.map(Into::into),
189            secret: app_secret.map(Into::into),
190            projects: parking_lot::Mutex::new(HSet::default()),
191        })))
192    }
193
194    #[must_use]
195    pub fn new_background(cfg: GitlabConfig) -> GLInstance {
196        let r = GLInstance {
197            inner: std::sync::Arc::new(parking_lot::RwLock::new(MaybeGitlab::Loading)),
198        };
199        r.clone().load(cfg);
200        r
201    }
202
203    /// #### Errors
204    #[instrument(
205        level = "debug",
206        target = "git",
207        name = "getting all gitlab projects",
208        skip_all
209    )]
210    pub async fn get_projects(&self) -> Result<Vec<flams_backend_types::git::Project>, Err> {
211        use gitlab::api::AsyncQuery;
212        let q = gitlab::api::projects::Projects::builder()
213            .simple(true)
214            .build()
215            .unwrap_or_else(|_| unreachable!());
216        let v: Vec<flams_backend_types::git::Project> =
217            gitlab::api::paged(q, gitlab::api::Pagination::All)
218                .query_async(&self.0.inner)
219                .await
220                .map_err(|e| {
221                    tracing::error!("Failed to load projects: {e}");
222                    e
223                })?;
224        let mut prs = self.0.projects.lock();
225        for p in &v {
226            if !prs.contains(&p.id) {
227                prs.insert(ProjectWithId {
228                    project: p.clone(),
229                    id: None,
230                });
231            }
232        }
233        drop(prs);
234        Ok(v)
235        //let raw = gitlab::api::raw(q).query_async(&self.inner).await?;
236        //Ok(std::str::from_utf8(raw.as_ref())?.to_string())
237    }
238
239    /// #### Errors
240    #[instrument(
241        level = "debug",
242        target = "git",
243        name = "getting archive id",
244        skip(self)
245    )]
246    pub async fn get_archive_id(&self, id: u64, branch: &str) -> Result<Option<ArchiveId>, Err> {
247        {
248            let vs = self.0.projects.lock();
249            if let Some(ProjectWithId { id: Some(id), .. }) = vs.get(&id) {
250                return Ok(id.clone());
251            }
252        }
253
254        macro_rules! ret {
255            ($v:expr) => {{
256                tracing::info!("Found {:?}", $v);
257                let mut lock = self.0.projects.lock();
258                if let Some(mut v) = lock.take(&id) {
259                    v.id = Some($v.clone());
260                    lock.insert(v);
261                }
262                drop(lock);
263                return Ok($v);
264            }};
265        }
266        let r = gitlab::api::projects::repository::TreeBuilder::default()
267            .project(id)
268            .ref_(branch)
269            .recursive(false)
270            .build()
271            .unwrap_or_else(|_| unreachable!());
272        let r: Vec<flams_backend_types::git::TreeEntry> = r.query_async(&self.0.inner).await?;
273        let Some(p) = r.into_iter().find_map(|e| {
274            if e.path.eq_ignore_ascii_case("meta-inf")
275                && matches!(e.tp, flams_backend_types::git::DirOrFile::Dir)
276            {
277                Some(e.path)
278            } else {
279                None
280            }
281        }) else {
282            ret!(None::<ArchiveId>)
283        };
284
285        let r = gitlab::api::projects::repository::TreeBuilder::default()
286            .project(id)
287            .ref_(branch)
288            .path(p)
289            .recursive(false)
290            .build()
291            .unwrap_or_else(|_| unreachable!());
292        let r: Vec<flams_backend_types::git::TreeEntry> = r.query_async(&self.0.inner).await?;
293        let Some(p) = r.into_iter().find_map(|e| {
294            if e.name.eq_ignore_ascii_case("manifest.mf")
295                && matches!(e.tp, flams_backend_types::git::DirOrFile::File)
296            {
297                Some(e.path)
298            } else {
299                None
300            }
301        }) else {
302            ret!(None::<ArchiveId>)
303        };
304
305        let blob = gitlab::api::projects::repository::files::FileRaw::builder()
306            .project(id)
307            .file_path(p)
308            .ref_(branch)
309            .build()
310            .unwrap_or_else(|_| unreachable!());
311        let r = gitlab::api::raw(blob).query_async(&self.0.inner).await?;
312        let r = std::str::from_utf8(&r)?;
313        let r = r.split('\n').find_map(|line| {
314            let line = line.trim();
315            line.strip_prefix("id:")
316                .and_then(|rest| ArchiveId::new(rest.trim()).ok())
317        });
318        ret!(r)
319    }
320}
321
322#[derive(Debug)]
323pub enum Err {
324    Api(gitlab::api::ApiError<gitlab::RestError>),
325    Str(std::str::Utf8Error),
326    Gitlab(gitlab::GitlabError),
327    Other(String),
328}
329impl From<gitlab::api::ApiError<gitlab::RestError>> for Err {
330    #[inline]
331    fn from(e: gitlab::api::ApiError<gitlab::RestError>) -> Self {
332        Self::Api(e)
333    }
334}
335impl From<std::str::Utf8Error> for Err {
336    #[inline]
337    fn from(e: std::str::Utf8Error) -> Self {
338        Self::Str(e)
339    }
340}
341impl From<gitlab::GitlabError> for Err {
342    #[inline]
343    fn from(e: gitlab::GitlabError) -> Self {
344        Self::Gitlab(e)
345    }
346}
347impl std::fmt::Display for Err {
348    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
349        match self {
350            Self::Api(e) => e.fmt(f),
351            Self::Str(e) => e.fmt(f),
352            Self::Gitlab(e) => e.fmt(f),
353            Self::Other(s) => s.fmt(f),
354        }
355    }
356}