git_worktree_cli/
config.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::path::{Path, PathBuf};
5
6use crate::cli::Provider;
7use crate::error::{Error, Result};
8
9#[derive(Debug, Serialize, Deserialize)]
10#[serde(rename_all = "camelCase")]
11pub struct GitWorktreeConfig {
12    pub repository_url: String,
13    pub main_branch: String,
14    pub created_at: DateTime<Utc>,
15    pub source_control: String,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub bitbucket_email: Option<String>,
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub hooks: Option<Hooks>,
20}
21
22#[derive(Debug, Serialize, Deserialize)]
23#[serde(rename_all = "camelCase")]
24pub struct Hooks {
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub post_add: Option<Vec<String>>,
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub pre_remove: Option<Vec<String>>,
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub post_remove: Option<Vec<String>>,
31}
32
33impl GitWorktreeConfig {
34    pub fn new(repository_url: String, main_branch: String, provider: Provider) -> Self {
35        // Convert provider enum to string
36        let source_control = match provider {
37            Provider::Github => "github".to_string(),
38            Provider::BitbucketCloud => "bitbucket-cloud".to_string(),
39            Provider::BitbucketDataCenter => "bitbucket-data-center".to_string(),
40        };
41
42        Self {
43            repository_url,
44            main_branch,
45            created_at: Utc::now(),
46            source_control,
47            bitbucket_email: None,
48            hooks: Some(Hooks {
49                post_add: Some(vec![]),
50                pre_remove: Some(vec![]),
51                post_remove: Some(vec![]),
52            }),
53        }
54    }
55
56    pub fn save(&self, path: &Path) -> Result<()> {
57        let json_string = serde_json::to_string_pretty(self)?;
58
59        fs::write(path, json_string).map_err(|e| Error::config(format!("Failed to write config file: {}", e)))?;
60
61        Ok(())
62    }
63
64    pub fn load(path: &Path) -> Result<Self> {
65        let content =
66            fs::read_to_string(path).map_err(|e| Error::config(format!("Failed to read config file: {}", e)))?;
67
68        let config: Self = json5::from_str(&content)?;
69
70        Ok(config)
71    }
72
73    pub fn find_config() -> Result<Option<(PathBuf, Self)>> {
74        let mut current_dir = std::env::current_dir()?;
75
76        loop {
77            // First check in current directory
78            let config_path = current_dir.join("git-worktree-config.jsonc");
79            if config_path.exists() {
80                let config = Self::load(&config_path)?;
81                return Ok(Some((config_path, config)));
82            }
83
84            // Then check in ./main/ subdirectory
85            let main_config_path = current_dir.join("main").join("git-worktree-config.jsonc");
86            if main_config_path.exists() {
87                let config = Self::load(&main_config_path)?;
88                return Ok(Some((main_config_path, config)));
89            }
90
91            if !current_dir.pop() {
92                break;
93            }
94        }
95
96        Ok(None)
97    }
98}
99
100pub const CONFIG_FILENAME: &str = "git-worktree-config.jsonc";
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use tempfile::tempdir;
106
107    #[test]
108    fn test_config_creation() {
109        let config = GitWorktreeConfig::new(
110            "git@github.com:test/repo.git".to_string(),
111            "main".to_string(),
112            Provider::Github,
113        );
114
115        assert_eq!(config.repository_url, "git@github.com:test/repo.git");
116        assert_eq!(config.main_branch, "main");
117        assert_eq!(config.source_control, "github");
118        assert_eq!(config.bitbucket_email, None);
119        assert!(config.hooks.is_some());
120
121        let hooks = config.hooks.unwrap();
122        assert!(hooks.post_add.is_some());
123        assert!(hooks.pre_remove.is_some());
124        assert!(hooks.post_remove.is_some());
125    }
126
127    #[test]
128    fn test_config_creation_bitbucket() {
129        let config = GitWorktreeConfig::new(
130            "https://bitbucket.org/workspace/repo.git".to_string(),
131            "main".to_string(),
132            Provider::BitbucketCloud,
133        );
134
135        assert_eq!(config.repository_url, "https://bitbucket.org/workspace/repo.git");
136        assert_eq!(config.main_branch, "main");
137        assert_eq!(config.source_control, "bitbucket-cloud");
138        assert_eq!(config.bitbucket_email, None);
139    }
140
141    #[test]
142    fn test_config_creation_bitbucket_data_center() {
143        let config = GitWorktreeConfig::new(
144            "https://bitbucket.company.com/scm/project/repo.git".to_string(),
145            "main".to_string(),
146            Provider::BitbucketDataCenter,
147        );
148
149        assert_eq!(
150            config.repository_url,
151            "https://bitbucket.company.com/scm/project/repo.git"
152        );
153        assert_eq!(config.main_branch, "main");
154        assert_eq!(config.source_control, "bitbucket-data-center");
155        assert_eq!(config.bitbucket_email, None);
156    }
157
158    #[test]
159    fn test_config_save_and_load() {
160        let temp_dir = tempdir().unwrap();
161        let config_path = temp_dir.path().join("test-config.jsonc");
162
163        let original_config = GitWorktreeConfig::new(
164            "git@github.com:test/repo.git".to_string(),
165            "develop".to_string(),
166            Provider::Github,
167        );
168
169        // Save config
170        original_config.save(&config_path).unwrap();
171        assert!(config_path.exists());
172
173        // Load config
174        let loaded_config = GitWorktreeConfig::load(&config_path).unwrap();
175        assert_eq!(loaded_config.repository_url, original_config.repository_url);
176        assert_eq!(loaded_config.main_branch, original_config.main_branch);
177    }
178
179    #[test]
180    fn test_config_find_in_current_dir() {
181        let temp_dir = tempdir().unwrap();
182        let original_cwd = std::env::current_dir().unwrap();
183
184        // Create config in temp directory first
185        let config = GitWorktreeConfig::new(
186            "git@github.com:test/repo.git".to_string(),
187            "main".to_string(),
188            Provider::Github,
189        );
190        config.save(&temp_dir.path().join(CONFIG_FILENAME)).unwrap();
191
192        // Change to temp directory
193        std::env::set_current_dir(temp_dir.path()).unwrap();
194
195        // Find config should return the config
196        let result = GitWorktreeConfig::find_config().unwrap();
197        assert!(result.is_some());
198
199        let (_found_path, found_config) = result.unwrap();
200        assert_eq!(found_config.repository_url, "git@github.com:test/repo.git");
201        assert_eq!(found_config.main_branch, "main");
202
203        // Restore original directory before temp_dir is dropped
204        // Use unwrap_or_else to handle case where original_cwd may not exist
205        if original_cwd.exists() {
206            std::env::set_current_dir(&original_cwd).unwrap();
207        } else {
208            // Fallback to a directory that should exist
209            std::env::set_current_dir("/").unwrap();
210        }
211    }
212
213    #[test]
214    fn test_config_not_found() {
215        let temp_dir = tempdir().unwrap();
216        let original_cwd = std::env::current_dir().unwrap();
217
218        // Change to empty temp directory
219        std::env::set_current_dir(temp_dir.path()).unwrap();
220
221        // Find config should return None
222        let result = GitWorktreeConfig::find_config().unwrap();
223        assert!(result.is_none());
224
225        // Restore original directory before temp_dir is dropped
226        // Use unwrap_or_else to handle case where original_cwd may not exist
227        if original_cwd.exists() {
228            std::env::set_current_dir(&original_cwd).unwrap();
229        } else {
230            // Fallback to a directory that should exist
231            std::env::set_current_dir("/").unwrap();
232        }
233    }
234}