git_workspace/providers/
gitlab.rs1use crate::providers::{
2 create_exclude_regex_set, create_include_regex_set, Provider, APP_USER_AGENT,
3};
4use crate::repository::Repository;
5use anyhow::{anyhow, Context};
6use console::style;
7use graphql_client::{GraphQLQuery, Response};
8use serde::{Deserialize, Serialize};
9use serde_json::json;
10use std::env;
11use std::fmt;
12
13#[derive(GraphQLQuery)]
20#[graphql(
21 schema_path = "src/providers/graphql/gitlab/schema.json",
22 query_path = "src/providers/graphql/gitlab/projects.graphql",
23 response_derives = "Debug"
24)]
25pub struct Repositories;
26
27struct ProjectNode {
28 archived: bool,
29 full_path: String,
30 ssh_url: String,
31 http_url: String,
32 root_ref: Option<String>,
33}
34
35impl From<repositories::RepositoriesGroupProjectsEdgesNode> for ProjectNode {
36 fn from(item: repositories::RepositoriesGroupProjectsEdgesNode) -> Self {
37 Self {
38 archived: item.archived.unwrap(),
39 root_ref: item.repository.and_then(|r| r.root_ref),
40 ssh_url: item.ssh_url_to_repo.expect("Unknown SSH URL"),
41 http_url: item.http_url_to_repo.expect("Unknown HTTP URL"),
42 full_path: item.full_path,
43 }
44 }
45}
46
47impl From<repositories::RepositoriesNamespaceProjectsEdgesNode> for ProjectNode {
48 fn from(item: repositories::RepositoriesNamespaceProjectsEdgesNode) -> Self {
49 Self {
50 archived: item.archived.unwrap(),
51 root_ref: item.repository.and_then(|r| r.root_ref),
52 ssh_url: item.ssh_url_to_repo.expect("Unknown SSH URL"),
53 http_url: item.http_url_to_repo.expect("Unknown HTTP URL"),
54 full_path: item.full_path,
55 }
56 }
57}
58
59static DEFAULT_GITLAB_URL: &str = "https://gitlab.com";
60
61fn public_gitlab_url() -> String {
62 DEFAULT_GITLAB_URL.to_string()
63}
64
65fn default_env_var() -> String {
66 String::from("GITHUB_TOKEN")
67}
68
69#[derive(Deserialize, Serialize, Default, Debug, Eq, Ord, PartialEq, PartialOrd, clap::Parser)]
70#[serde(rename_all = "lowercase")]
71#[command(about = "Add a Gitlab user or group by name")]
72pub struct GitlabProvider {
73 pub name: String,
75 #[serde(default = "public_gitlab_url")]
76 #[arg(long = "url", default_value = DEFAULT_GITLAB_URL)]
77 pub url: String,
79 #[arg(long = "path", default_value = "gitlab")]
80 path: String,
82 #[arg(long = "env-name", short = 'e', default_value = "GITLAB_TOKEN")]
83 #[serde(default = "default_env_var")]
84 env_var: String,
86
87 #[arg(long = "include")]
88 #[serde(default)]
89 include: Vec<String>,
92
93 #[arg(long = "auth-http")]
94 #[serde(default)]
95 auth_http: bool,
97
98 #[arg(long = "exclude")]
99 #[serde(default)]
100 exclude: Vec<String>,
103 }
110
111impl fmt::Display for GitlabProvider {
112 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
113 write!(
114 f,
115 "Gitlab user/group {} at {} in directory {}, using the token stored in {}",
116 style(&self.name.to_lowercase()).green(),
117 style(&self.url).green(),
118 style(&self.path).green(),
119 style(&self.env_var).green(),
120 )
121 }
122}
123
124impl Provider for GitlabProvider {
125 fn correctly_configured(&self) -> bool {
126 let token = env::var(&self.env_var);
127 if token.is_err() {
128 println!(
129 "{}",
130 style(format!(
131 "Error: {} environment variable is not defined",
132 self.env_var
133 ))
134 .red()
135 );
136 println!("Create a personal access token here:");
137 println!("{}/profile/personal_access_tokens", self.url);
138 println!(
139 "Set an environment variable called {} with the value",
140 self.env_var
141 );
142 return false;
143 }
144 if self.name.ends_with('/') {
145 println!(
146 "{}",
147 style("Error: Ensure that names do not end in forward slashes").red()
148 );
149 println!("You specified: {}", self.name);
150 return false;
151 }
152 true
153 }
154 fn fetch_repositories(&self) -> anyhow::Result<Vec<Repository>> {
155 let gitlab_token = env::var(&self.env_var)
156 .with_context(|| format!("Missing {} environment variable", self.env_var))?;
157 let mut repositories = vec![];
158 let mut after = Some("".to_string());
159 let name = self.name.to_string().to_lowercase();
160
161 let include_regex_set = create_include_regex_set(&self.include)?;
162 let exclude_regex_set = create_exclude_regex_set(&self.exclude)?;
163
164 let agent = ureq::AgentBuilder::new()
165 .https_only(true)
166 .user_agent(APP_USER_AGENT)
167 .build();
168
169 loop {
170 let q = Repositories::build_query(repositories::Variables {
171 name: name.clone(),
172 after,
173 });
174 let res = agent
175 .post(format!("{}/api/graphql", self.url).as_str())
176 .set("Authorization", format!("Bearer {}", gitlab_token).as_str())
177 .set("Content-Type", "application/json")
178 .send_json(json!(&q))?;
179 let json = res.into_json()?;
180
181 let response_body: Response<repositories::ResponseData> = serde_json::from_value(json)?;
182 let data = response_body.data.expect("Missing data");
183
184 let temp_repositories: Vec<ProjectNode>;
185 if data.group.is_some() {
187 let group_data = data.group.expect("Missing group").projects;
188 temp_repositories = group_data
189 .edges
190 .expect("missing edges")
191 .into_iter()
192 .flatten()
194 .filter_map(|x| x.node)
196 .map(ProjectNode::from)
197 .collect();
198 after = group_data.page_info.end_cursor;
199 } else if data.namespace.is_some() {
200 let namespace_data = data.namespace.expect("Missing namespace").projects;
201 temp_repositories = namespace_data
202 .edges
203 .expect("missing edges")
204 .into_iter()
205 .flatten()
207 .filter_map(|x| x.node)
209 .map(ProjectNode::from)
210 .collect();
211 after = namespace_data.page_info.end_cursor;
212 } else {
213 return Err(anyhow!(
214 "Gitlab group/user {} could not be found. Are you sure you have access?",
215 name
216 ));
217 }
218
219 repositories.extend(
220 temp_repositories
221 .into_iter()
222 .filter(|r| !r.archived)
223 .filter(|r| include_regex_set.is_match(&r.full_path))
224 .filter(|r| !exclude_regex_set.is_match(&r.full_path))
225 .map(|r| {
226 Repository::new(
227 format!("{}/{}", self.path, r.full_path),
228 if self.auth_http {
229 r.http_url
230 } else {
231 r.ssh_url
232 },
233 r.root_ref,
234 None,
235 )
236 }),
237 );
238
239 if after.is_none() {
240 break;
241 }
242 }
243 Ok(repositories)
244 }
245}