git_workspace/providers/
gitea.rs

1use crate::providers::{
2    create_exclude_regex_set, create_include_regex_set, Provider, APP_USER_AGENT,
3};
4use crate::repository::Repository;
5use anyhow::Context;
6use console::style;
7use serde::{Deserialize, Serialize};
8use std::env;
9use std::fmt;
10
11#[derive(Deserialize, Debug)]
12struct GiteaRepository {
13    full_name: String,
14    clone_url: String,
15    ssh_url: String,
16    default_branch: String,
17    archived: bool,
18    fork: bool,
19}
20
21fn default_env_var() -> String {
22    String::from("GITEA_TOKEN")
23}
24
25static DEFAULT_GITEA_URL: &str = "https://gitea.com";
26
27fn public_gitea_url() -> String {
28    DEFAULT_GITEA_URL.to_string()
29}
30
31#[derive(Deserialize, Serialize, Debug, Eq, Ord, PartialEq, PartialOrd, clap::Parser)]
32#[serde(rename_all = "lowercase")]
33#[command(about = "Add a Gitea user or organization by name")]
34pub struct GiteaProvider {
35    /// The name of the user or organisation to add
36    pub name: String,
37
38    #[arg(long = "path", default_value = "gitea")]
39    /// Clone repos to a specific path
40    path: String,
41
42    #[arg(long = "env-name", short = 'e', default_value = "GITEA_TOKEN")]
43    #[serde(default = "default_env_var")]
44    /// Environment variable containing the auth token
45    env_var: String,
46
47    #[arg(long = "skip-forks")]
48    #[serde(default)]
49    /// Don't clone forked repositories
50    skip_forks: bool,
51
52    #[arg(long = "include")]
53    #[serde(default)]
54    /// Only clone repositories that match these regular expressions
55    include: Vec<String>,
56
57    #[arg(long = "auth-http")]
58    #[serde(default)]
59    /// Use HTTP authentication instead of SSH
60    auth_http: bool,
61
62    #[arg(long = "exclude")]
63    #[serde(default)]
64    /// Don't clone repositories that match these regular expressions
65    exclude: Vec<String>,
66
67    #[arg(long = "url", default_value = DEFAULT_GITEA_URL)]
68    #[serde(default = "public_gitea_url")]
69    /// Gitea instance URL
70    pub url: String,
71}
72
73impl fmt::Display for GiteaProvider {
74    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
75        write!(
76            f,
77            "Gitea user/org {} at {} in directory {}, using the token stored in {}",
78            style(&self.name.to_lowercase()).green(),
79            style(&self.url).green(),
80            style(&self.path).green(),
81            style(&self.env_var).green(),
82        )
83    }
84}
85
86impl Provider for GiteaProvider {
87    fn correctly_configured(&self) -> bool {
88        let token = env::var(&self.env_var);
89        if token.is_err() {
90            println!(
91                "{}",
92                style(format!(
93                    "Error: {} environment variable is not defined",
94                    self.env_var
95                ))
96                .red()
97            );
98            println!("Create an access token in your Gitea Settings -> Applications");
99            println!(
100                "Then set a {} environment variable with the value",
101                style(&self.env_var).green()
102            );
103            return false;
104        }
105        if self.name.ends_with('/') {
106            println!(
107                "{}",
108                style("Error: Ensure that names do not end in forward slashes").red()
109            );
110            println!("You specified: {}", self.name);
111            return false;
112        }
113        true
114    }
115
116    fn fetch_repositories(&self) -> anyhow::Result<Vec<Repository>> {
117        let gitea_token = env::var(&self.env_var)
118            .with_context(|| format!("Missing {} environment variable", self.env_var))?;
119
120        let include_regex_set = create_include_regex_set(&self.include)?;
121        let exclude_regex_set = create_exclude_regex_set(&self.exclude)?;
122
123        let agent = ureq::AgentBuilder::new()
124            .https_only(true)
125            .user_agent(APP_USER_AGENT)
126            .build();
127
128        let mut page = 1;
129        let mut repositories = Vec::new();
130
131        loop {
132            let url = format!(
133                "{}/api/v1/users/{}/repos?page={}&limit=50",
134                self.url, self.name, page
135            );
136
137            let response = agent
138                .get(&url)
139                .set("Authorization", &format!("token {}", gitea_token))
140                .call()?;
141
142            let repos: Vec<GiteaRepository> = response.into_json()?;
143            if repos.is_empty() {
144                break;
145            }
146
147            repositories.extend(
148                repos
149                    .into_iter()
150                    .filter(|r| !r.archived)
151                    .filter(|r| !self.skip_forks || !r.fork)
152                    .filter(|r| include_regex_set.is_match(&r.full_name))
153                    .filter(|r| !exclude_regex_set.is_match(&r.full_name))
154                    .map(|r| {
155                        Repository::new(
156                            format!("{}/{}", self.path, r.full_name),
157                            if self.auth_http {
158                                r.clone_url
159                            } else {
160                                r.ssh_url
161                            },
162                            Some(r.default_branch),
163                            None,
164                        )
165                    }),
166            );
167
168            page += 1;
169        }
170
171        Ok(repositories)
172    }
173}