git_workspace/
config.rs

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    // Find all config files in workspace
25    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 files
152        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        // Test with no config files
169        let result = Config::from_workspace(dir_path);
170        assert!(result.is_err());
171
172        // Test with config file
173        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 invalid config
224        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}