git_repos/
config.rs

1use std::{path::Path, process};
2
3use serde::{Deserialize, Serialize};
4
5use crate::{
6    output::*,
7    path, provider,
8    provider::{Filter, Provider},
9    repo,
10    token::AuthToken,
11    tree,
12};
13
14pub type RemoteProvider = provider::RemoteProvider;
15pub type RemoteType = repo::RemoteType;
16
17fn worktree_setup_default() -> bool {
18    false
19}
20
21#[derive(Debug, Serialize, Deserialize)]
22#[serde(untagged)]
23pub enum Config {
24    ConfigTrees(ConfigTrees),
25    ConfigProvider(ConfigProvider),
26}
27
28#[derive(Debug, Serialize, Deserialize)]
29#[serde(deny_unknown_fields)]
30pub struct ConfigTrees {
31    pub trees: Vec<ConfigTree>,
32}
33
34#[derive(Debug, Serialize, Deserialize)]
35#[serde(deny_unknown_fields)]
36pub struct ConfigProviderFilter {
37    pub access: Option<bool>,
38    pub owner: Option<bool>,
39    pub users: Option<Vec<String>>,
40    pub groups: Option<Vec<String>>,
41}
42
43#[derive(Debug, Serialize, Deserialize)]
44#[serde(deny_unknown_fields)]
45pub struct ConfigProvider {
46    pub provider: RemoteProvider,
47    pub token_file: String,
48    pub root: String,
49    pub filters: Option<ConfigProviderFilter>,
50    pub force_ssh: Option<bool>,
51    pub api_url: Option<String>,
52    pub worktree: Option<bool>,
53    pub remote_name: Option<String>,
54}
55
56#[derive(Debug, Serialize, Deserialize)]
57#[serde(deny_unknown_fields)]
58pub struct RemoteConfig {
59    pub name: String,
60    pub url: String,
61    #[serde(rename = "type")]
62    pub remote_type: RemoteType,
63}
64
65impl RemoteConfig {
66    pub fn from_remote(remote: repo::Remote) -> Self {
67        Self { name: remote.name, url: remote.url, remote_type: remote.remote_type }
68    }
69
70    pub fn into_remote(self) -> repo::Remote {
71        repo::Remote { name: self.name, url: self.url, remote_type: self.remote_type }
72    }
73}
74
75#[derive(Debug, Serialize, Deserialize)]
76pub struct RepoConfig {
77    pub name: String,
78
79    #[serde(default)]
80    pub init: bool,
81
82    pub remotes: Option<Vec<RemoteConfig>>,
83}
84
85impl RepoConfig {
86    pub fn from_repo(repo: repo::Repo) -> Self {
87        Self {
88            name: repo.name,
89            init: repo.worktree_setup,
90            remotes: repo
91                .remotes
92                .map(|remotes| remotes.into_iter().map(RemoteConfig::from_remote).collect()),
93        }
94    }
95
96    pub fn into_repo(self) -> repo::Repo {
97        let (namespace, name) = if let Some((namespace, name)) = self.name.rsplit_once('/') {
98            (Some(namespace.to_string()), name.to_string())
99        } else {
100            (None, self.name)
101        };
102
103        repo::Repo {
104            name,
105            namespace,
106            worktree_setup: self.init,
107            remotes: self
108                .remotes
109                .map(|remotes| remotes.into_iter().map(|remote| remote.into_remote()).collect()),
110        }
111    }
112}
113
114impl ConfigTrees {
115    pub fn to_config(self) -> Config {
116        Config::ConfigTrees(self)
117    }
118
119    pub fn from_vec(vec: Vec<ConfigTree>) -> Self {
120        ConfigTrees { trees: vec }
121    }
122
123    pub fn from_trees(vec: Vec<tree::Tree>) -> Self {
124        ConfigTrees { trees: vec.into_iter().map(ConfigTree::from_tree).collect() }
125    }
126
127    pub fn trees(self) -> Vec<ConfigTree> {
128        self.trees
129    }
130
131    pub fn trees_mut(&mut self) -> &mut Vec<ConfigTree> {
132        &mut self.trees
133    }
134
135    pub fn trees_ref(&self) -> &Vec<ConfigTree> {
136        self.trees.as_ref()
137    }
138}
139
140impl Config {
141    pub fn trees(self) -> Result<Vec<ConfigTree>, String> {
142        match self {
143            Config::ConfigTrees(config) => Ok(config.trees),
144            Config::ConfigProvider(config) => {
145                let token = match AuthToken::from_file(&config.token_file) {
146                    Ok(token) => token,
147                    Err(error) => {
148                        print_error(&format!("Getting token from command failed: {}", error));
149                        process::exit(1);
150                    },
151                };
152
153                let filters = config.filters.unwrap_or(ConfigProviderFilter {
154                    access: Some(false),
155                    owner: Some(false),
156                    users: Some(vec![]),
157                    groups: Some(vec![]),
158                });
159
160                let filter = Filter::new(
161                    filters.users.unwrap_or_default(),
162                    filters.groups.unwrap_or_default(),
163                    filters.owner.unwrap_or(false),
164                    filters.access.unwrap_or(false),
165                );
166
167                if filter.empty() {
168                    print_warning(
169                        "The configuration does not contain any filters, so no repos will match",
170                    );
171                }
172
173                let repos = match config.provider {
174                    RemoteProvider::Github => {
175                        match provider::Github::new(filter, token, config.api_url) {
176                            Ok(provider) => provider,
177                            Err(error) => {
178                                print_error(&format!("Error: {}", error));
179                                process::exit(1);
180                            },
181                        }
182                        .get_repos(
183                            config.worktree.unwrap_or(false),
184                            config.force_ssh.unwrap_or(false),
185                            config.remote_name,
186                        )?
187                    },
188                    RemoteProvider::Gitlab => {
189                        match provider::Gitlab::new(filter, token, config.api_url) {
190                            Ok(provider) => provider,
191                            Err(error) => {
192                                print_error(&format!("Error: {}", error));
193                                process::exit(1);
194                            },
195                        }
196                        .get_repos(
197                            config.worktree.unwrap_or(false),
198                            config.force_ssh.unwrap_or(false),
199                            config.remote_name,
200                        )?
201                    },
202                };
203
204                let mut trees = vec![];
205
206                for (namespace, namespace_repos) in repos {
207                    let repos = namespace_repos.into_iter().map(RepoConfig::from_repo).collect();
208                    let tree = ConfigTree {
209                        root: if let Some(namespace) = namespace {
210                            path::path_as_string(&Path::new(&config.root).join(namespace))
211                        } else {
212                            path::path_as_string(Path::new(&config.root))
213                        },
214                        repos: Some(repos),
215                    };
216                    trees.push(tree);
217                }
218                Ok(trees)
219            },
220        }
221    }
222
223    pub fn from_trees(trees: Vec<ConfigTree>) -> Self {
224        Config::ConfigTrees(ConfigTrees { trees })
225    }
226
227    pub fn normalize(&mut self) {
228        if let Config::ConfigTrees(config) = self {
229            let home = "~";
230            for tree in &mut config.trees_mut().iter_mut() {
231                if tree.root.starts_with(&home) {
232                    // The tilde is not handled differently, it's just a normal path component for `Path`.
233                    // Therefore we can treat it like that during **output**.
234                    //
235                    // The `unwrap()` is safe here as we are testing via `starts_with()`
236                    // beforehand
237                    let mut path = tree.root.strip_prefix(&home).unwrap();
238                    if path.starts_with('/') {
239                        path = path.strip_prefix('/').unwrap();
240                    }
241
242                    tree.root = Path::new("~").join(path).display().to_string();
243                }
244            }
245        }
246    }
247
248    pub fn as_toml(&self) -> Result<String, String> {
249        match toml::to_string(self) {
250            Ok(toml) => Ok(toml),
251            Err(error) => Err(error.to_string()),
252        }
253    }
254
255    pub fn as_yaml(&self) -> Result<String, String> {
256        todo!()
257    }
258}
259
260#[derive(Debug, Serialize, Deserialize)]
261#[serde(deny_unknown_fields)]
262pub struct ConfigTree {
263    pub root: String,
264    pub repos: Option<Vec<RepoConfig>>,
265}
266
267impl ConfigTree {
268    pub fn from_repos(root: String, repos: Vec<repo::Repo>) -> Self {
269        Self { root, repos: Some(repos.into_iter().map(RepoConfig::from_repo).collect()) }
270    }
271
272    pub fn from_tree(tree: tree::Tree) -> Self {
273        Self {
274            root: tree.root,
275            repos: Some(tree.repos.into_iter().map(RepoConfig::from_repo).collect()),
276        }
277    }
278}
279
280pub fn read_config<'a, T>(path: &str) -> Result<T, String>
281where
282    T: for<'de> serde::Deserialize<'de>,
283{
284    let content =
285        match std::fs::read_to_string(path) {
286            Ok(s) => s,
287            Err(e) => {
288                return Err(format!("Error reading configuration file \"{}\": {}", path, match e
289                    .kind()
290                {
291                    std::io::ErrorKind::NotFound => String::from("not found"),
292                    _ => e.to_string(),
293                }));
294            },
295        };
296
297    let config: T = match toml::from_str(&content) {
298        Ok(c) => c,
299        Err(_) => {
300            todo!()
301        },
302    };
303
304    Ok(config)
305}