flams_git/gl/
mod.rs

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  /*fn split(url:&str) -> (&str,bool) {
137    if let Some(r) = url.strip_prefix("https://") {
138      return (r,false)
139    }
140    if let Some(r) = url.strip_prefix("http://") {
141      return (r,true)
142    }
143    (url,false)
144  }*/
145}
146
147impl GitLab {
148  /// #### Errors
149	pub async fn new(cfg:GitlabConfig) -> Result<Self,gitlab::GitlabError> {
150		let GitlabConfig { url, token, app_id, app_secret } = cfg;
151		//let (split_url,http) = GitlabConfig::split(&url);
152    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  /// #### Errors
177#[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    //let raw = gitlab::api::raw(q).query_async(&self.inner).await?;
196    //Ok(std::str::from_utf8(raw.as_ref())?.to_string())
197  }
198
199  /// #### Errors
200#[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}