use crate::providers::{
create_exclude_regex_set, create_include_regex_set, Provider, APP_USER_AGENT,
};
use crate::repository::Repository;
use anyhow::{anyhow, Context};
use console::style;
use graphql_client::{GraphQLQuery, Response};
use serde::{Deserialize, Serialize};
use serde_json::json;
use std::env;
use std::fmt;
#[derive(GraphQLQuery)]
#[graphql(
schema_path = "src/providers/graphql/gitlab/schema.json",
query_path = "src/providers/graphql/gitlab/projects.graphql",
response_derives = "Debug"
)]
pub struct Repositories;
struct ProjectNode {
archived: bool,
full_path: String,
ssh_url: String,
http_url: String,
root_ref: Option<String>,
}
impl From<repositories::RepositoriesGroupProjectsEdgesNode> for ProjectNode {
fn from(item: repositories::RepositoriesGroupProjectsEdgesNode) -> Self {
Self {
archived: item.archived.unwrap(),
root_ref: item.repository.and_then(|r| r.root_ref),
ssh_url: item.ssh_url_to_repo.expect("Unknown SSH URL"),
http_url: item.http_url_to_repo.expect("Unknown HTTP URL"),
full_path: item.full_path,
}
}
}
impl From<repositories::RepositoriesNamespaceProjectsEdgesNode> for ProjectNode {
fn from(item: repositories::RepositoriesNamespaceProjectsEdgesNode) -> Self {
Self {
archived: item.archived.unwrap(),
root_ref: item.repository.and_then(|r| r.root_ref),
ssh_url: item.ssh_url_to_repo.expect("Unknown SSH URL"),
http_url: item.http_url_to_repo.expect("Unknown HTTP URL"),
full_path: item.full_path,
}
}
}
static DEFAULT_GITLAB_URL: &str = "https://gitlab.com";
fn public_gitlab_url() -> String {
DEFAULT_GITLAB_URL.to_string()
}
fn default_env_var() -> String {
String::from("GITHUB_TOKEN")
}
#[derive(Deserialize, Serialize, Default, Debug, Eq, Ord, PartialEq, PartialOrd, clap::Parser)]
#[serde(rename_all = "lowercase")]
#[command(about = "Add a Gitlab user or group by name")]
pub struct GitlabProvider {
pub name: String,
#[serde(default = "public_gitlab_url")]
#[arg(long = "url", default_value = DEFAULT_GITLAB_URL)]
pub url: String,
#[arg(long = "path", default_value = "gitlab")]
path: String,
#[arg(long = "env-name", short = 'e', default_value = "GITLAB_TOKEN")]
#[serde(default = "default_env_var")]
env_var: String,
#[arg(long = "include")]
#[serde(default)]
include: Vec<String>,
#[arg(long = "auth-http")]
#[serde(default)]
auth_http: bool,
#[arg(long = "exclude")]
#[serde(default)]
exclude: Vec<String>,
}
impl fmt::Display for GitlabProvider {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"Gitlab user/group {} at {} in directory {}, using the token stored in {}",
style(&self.name.to_lowercase()).green(),
style(&self.url).green(),
style(&self.path).green(),
style(&self.env_var).green(),
)
}
}
impl Provider for GitlabProvider {
fn correctly_configured(&self) -> bool {
let token = env::var(&self.env_var);
if token.is_err() {
println!(
"{}",
style(format!(
"Error: {} environment variable is not defined",
self.env_var
))
.red()
);
println!("Create a personal access token here:");
println!("{}/profile/personal_access_tokens", self.url);
println!(
"Set an environment variable called {} with the value",
self.env_var
);
return false;
}
if self.name.ends_with('/') {
println!(
"{}",
style("Error: Ensure that names do not end in forward slashes").red()
);
println!("You specified: {}", self.name);
return false;
}
true
}
fn fetch_repositories(&self) -> anyhow::Result<Vec<Repository>> {
let gitlab_token = env::var(&self.env_var)
.with_context(|| format!("Missing {} environment variable", self.env_var))?;
let mut repositories = vec![];
let mut after = Some("".to_string());
let name = self.name.to_string().to_lowercase();
let include_regex_set = create_include_regex_set(&self.include)?;
let exclude_regex_set = create_exclude_regex_set(&self.exclude)?;
let agent = ureq::AgentBuilder::new()
.https_only(true)
.user_agent(APP_USER_AGENT)
.build();
loop {
let q = Repositories::build_query(repositories::Variables {
name: name.clone(),
after,
});
let res = agent
.post(format!("{}/api/graphql", self.url).as_str())
.set("Authorization", format!("Bearer {}", gitlab_token).as_str())
.set("Content-Type", "application/json")
.send_json(json!(&q))?;
let json = res.into_json()?;
let response_body: Response<repositories::ResponseData> = serde_json::from_value(json)?;
let data = response_body.data.expect("Missing data");
let temp_repositories: Vec<ProjectNode>;
if let Some(group) = data.group {
let group_data = group.projects;
temp_repositories = group_data
.edges
.expect("missing edges")
.into_iter()
.flatten()
.filter_map(|x| x.node)
.map(ProjectNode::from)
.collect();
after = group_data.page_info.end_cursor;
} else if let Some(namespace) = data.namespace {
let namespace_data = namespace.projects;
temp_repositories = namespace_data
.edges
.expect("missing edges")
.into_iter()
.flatten()
.filter_map(|x| x.node)
.map(ProjectNode::from)
.collect();
after = namespace_data.page_info.end_cursor;
} else {
return Err(anyhow!(
"Gitlab group/user {} could not be found. Are you sure you have access?",
name
));
}
repositories.extend(
temp_repositories
.into_iter()
.filter(|r| !r.archived)
.filter(|r| include_regex_set.is_match(&r.full_path))
.filter(|r| !exclude_regex_set.is_match(&r.full_path))
.map(|r| {
Repository::new(
format!("{}/{}", self.path, r.full_path),
if self.auth_http {
r.http_url
} else {
r.ssh_url
},
r.root_ref,
None,
)
}),
);
if after.is_none() {
break;
}
}
Ok(repositories)
}
}