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 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}