1use crate::providers::{GiteaProvider, GithubProvider, GitlabProvider, Provider};
2use crate::repository::Repository;
3use anyhow::Context;
4use serde::{Deserialize, Serialize};
5use std::fmt;
6use std::fs;
7use std::path::{Path, PathBuf};
8
9#[derive(Deserialize, Serialize, Debug)]
10struct ConfigContents {
11 #[serde(rename = "provider", default)]
12 providers: Vec<ProviderSource>,
13}
14
15pub struct Config {
16 files: Vec<PathBuf>,
17}
18
19impl Config {
20 pub fn new(files: Vec<PathBuf>) -> Config {
21 Config { files }
22 }
23
24 fn find_config_files(workspace: &Path) -> anyhow::Result<Vec<PathBuf>> {
26 let matcher = globset::GlobBuilder::new("workspace*.toml")
27 .literal_separator(true)
28 .build()?
29 .compile_matcher();
30 let entries = fs::read_dir(workspace)
31 .with_context(|| format!("Cannot list directory {}", workspace.display()))?;
32 let mut config_files: Vec<PathBuf> = entries
33 .filter_map(Result::ok)
34 .map(|e| e.path())
35 .filter(|p| {
36 p.file_name()
37 .map(|n| n != "workspace-lock.toml" && matcher.is_match(n))
38 .unwrap_or(false)
39 })
40 .collect();
41 config_files.sort();
42
43 Ok(config_files)
44 }
45
46 pub fn from_workspace(workspace: &Path) -> anyhow::Result<Self> {
47 let config_files =
48 Self::find_config_files(workspace).context("Error loading config files")?;
49 if config_files.is_empty() {
50 anyhow::bail!("No configuration files found: Are you in the right workspace?")
51 }
52 Ok(Self::new(config_files))
53 }
54
55 pub fn read(&self) -> anyhow::Result<Vec<ProviderSource>> {
56 let mut all_providers = vec![];
57
58 for path in &self.files {
59 if !path.exists() {
60 continue;
61 }
62 let file_contents = fs::read_to_string(path)
63 .with_context(|| format!("Cannot read file {}", path.display()))?;
64 let contents: ConfigContents = toml::from_str(file_contents.as_str())
65 .with_context(|| format!("Error parsing TOML in file {}", path.display()))?;
66 all_providers.extend(contents.providers);
67 }
68 Ok(all_providers)
69 }
70 pub fn write(&self, providers: Vec<ProviderSource>, config_path: &Path) -> anyhow::Result<()> {
71 let toml = toml::to_string(&ConfigContents { providers })?;
72 fs::write(config_path, toml)
73 .with_context(|| format!("Error writing to file {}", config_path.display()))?;
74 Ok(())
75 }
76}
77
78#[derive(Deserialize, Serialize, Debug, Eq, Ord, PartialEq, PartialOrd)]
79#[serde(tag = "provider")]
80#[serde(rename_all = "lowercase")]
81#[derive(clap::Subcommand)]
82pub enum ProviderSource {
83 Gitea(GiteaProvider),
84 Gitlab(GitlabProvider),
85 Github(GithubProvider),
86}
87
88impl ProviderSource {
89 pub fn provider(&self) -> &dyn Provider {
90 match self {
91 Self::Gitea(config) => config,
92 Self::Gitlab(config) => config,
93 Self::Github(config) => config,
94 }
95 }
96
97 pub fn correctly_configured(&self) -> bool {
98 self.provider().correctly_configured()
99 }
100
101 pub fn fetch_repositories(&self) -> anyhow::Result<Vec<Repository>> {
102 self.provider().fetch_repositories()
103 }
104}
105
106impl fmt::Display for ProviderSource {
107 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
108 write!(f, "{}", self.provider())
109 }
110}
111
112#[cfg(test)]
113mod tests {
114 use super::*;
115 use std::fs::File;
116 use std::io::Write;
117 use tempfile::TempDir;
118
119 const WORKSPACE_FILE_CONTENT: &str = r#"[[provider]]
120 provider = "github"
121 name = "github-group"
122 url = "https://api.github.com/graphql"
123 path = "github"
124 env_var = "GITHUB_TOKEN"
125 skip_forks = false
126 auth_http = true
127 include = []
128 exclude = []
129 [[provider]]
130 provider = "gitlab"
131 name = "gitlab-group"
132 url = "https://gitlab.com"
133 path = "gitlab"
134 env_var = "GITLAB_COM_TOKEN"
135 auth_http = true
136 include = []
137 exclude = []"#;
138
139 fn create_test_config(dir: &Path, filename: &str, content: &str) -> PathBuf {
140 let config_path = dir.join(filename);
141 let mut file = File::create(&config_path).unwrap();
142 file.write_all(content.as_bytes()).unwrap();
143 config_path
144 }
145
146 #[test]
147 fn test_find_config_files() {
148 let temp_dir = TempDir::new().unwrap();
149 let dir_path = temp_dir.path();
150
151 create_test_config(dir_path, "workspace.toml", WORKSPACE_FILE_CONTENT);
153 create_test_config(dir_path, "workspace-test.toml", WORKSPACE_FILE_CONTENT);
154 create_test_config(dir_path, "workspace-lock.toml", "File should be ignored");
155 create_test_config(dir_path, "other.toml", "File should be ignored");
156
157 let config_files = Config::find_config_files(dir_path).unwrap();
158 assert_eq!(config_files.len(), 2);
159 assert!(config_files[0].ends_with("workspace-test.toml"));
160 assert!(config_files[1].ends_with("workspace.toml"));
161 }
162
163 #[test]
164 fn test_config_from_workspace() {
165 let temp_dir = TempDir::new().unwrap();
166 let dir_path = temp_dir.path();
167
168 let result = Config::from_workspace(dir_path);
170 assert!(result.is_err());
171
172 create_test_config(dir_path, "workspace.toml", WORKSPACE_FILE_CONTENT);
174
175 let config = Config::from_workspace(dir_path).unwrap();
176 assert_eq!(config.files.len(), 1);
177 }
178
179 #[test]
180 fn test_config_read() {
181 let temp_dir = TempDir::new().unwrap();
182 let dir_path = temp_dir.path();
183
184 create_test_config(dir_path, "workspace.toml", WORKSPACE_FILE_CONTENT);
185 create_test_config(dir_path, "workspace-42.toml", WORKSPACE_FILE_CONTENT);
186
187 let config = Config::from_workspace(dir_path).unwrap();
188 let providers = config.read().unwrap();
189
190 assert_eq!(providers.len(), 4);
191 match &providers[0] {
192 ProviderSource::Github(config) => assert_eq!(config.name, "github-group"),
193 _ => panic!("Expected Github provider"),
194 }
195 match &providers[1] {
196 ProviderSource::Gitlab(config) => assert_eq!(config.name, "gitlab-group"),
197 _ => panic!("Expected Gitlab provider"),
198 }
199 }
200
201 #[test]
202 fn test_config_write() {
203 let temp_dir = TempDir::new().unwrap();
204 let config_path = temp_dir.path().join("workspace.toml");
205
206 let providers = vec![
207 ProviderSource::Github(GithubProvider::default()),
208 ProviderSource::Gitlab(GitlabProvider::default()),
209 ];
210 let config = Config::new(vec![config_path.clone()]);
211 config.write(providers, &config_path).unwrap();
212
213 let content = fs::read_to_string(&config_path).unwrap();
214 assert!(content.contains("github"));
215 assert!(content.contains("gitlab"));
216 }
217
218 #[test]
219 fn test_invalid_config_content() {
220 let temp_dir = TempDir::new().unwrap();
221 let dir_path = temp_dir.path();
222
223 create_test_config(
225 dir_path,
226 "workspace.toml",
227 r#"[[provider]]
228 invalid = "content""#,
229 );
230
231 let config = Config::from_workspace(dir_path).unwrap();
232 let result = config.read();
233 assert!(result.is_err());
234 }
235}