Skip to main content

git_worktree_manager/
cwshare_setup.rs

1/// Setup prompt for .cwshare file creation.
2///
3use std::path::Path;
4
5use console::style;
6
7use crate::git;
8
9/// Common files that users might want to share across worktrees.
10const COMMON_SHARED_FILES: &[&str] = &[
11    ".env",
12    ".env.local",
13    ".env.development",
14    ".env.test",
15    "config/local.json",
16    "config/local.yaml",
17    "config/local.yml",
18    ".vscode/settings.json",
19];
20
21/// Check if user has been prompted for .cwshare in this repo.
22pub fn is_cwshare_prompted(repo: &Path) -> bool {
23    git::get_config("cwshare.prompted", Some(repo))
24        .map(|v| v == "true")
25        .unwrap_or(false)
26}
27
28/// Mark that user has been prompted for .cwshare.
29pub fn mark_cwshare_prompted(repo: &Path) {
30    let _ = git::set_config("cwshare.prompted", "true", Some(repo));
31}
32
33/// Check if .cwshare file exists.
34pub fn has_cwshare_file(repo: &Path) -> bool {
35    repo.join(".cwshare").exists()
36}
37
38/// Detect common files that exist and might be worth sharing.
39pub fn detect_common_files(repo: &Path) -> Vec<String> {
40    COMMON_SHARED_FILES
41        .iter()
42        .filter(|f| repo.join(f).exists())
43        .map(|f| f.to_string())
44        .collect()
45}
46
47/// Create a .cwshare file with template content.
48pub fn create_cwshare_template(repo: &Path, suggested_files: &[String]) {
49    let cwshare_path = repo.join(".cwshare");
50
51    let mut template = String::from(
52        "# .cwshare - Files to copy to new worktrees\n\
53         #\n\
54         # Files listed here will be automatically copied when you run 'gw new'.\n\
55         # Useful for environment files and local configs not tracked in git.\n\
56         #\n\
57         # Format:\n\
58         #   - One file/directory path per line (relative to repo root)\n\
59         #   - Lines starting with # are comments\n\
60         #   - Empty lines are ignored\n",
61    );
62
63    if !suggested_files.is_empty() {
64        template.push_str("#\n# Detected files in this repository (uncomment to enable):\n\n");
65        for file in suggested_files {
66            template.push_str(&format!("# {}\n", file));
67        }
68    } else {
69        template.push_str("#\n# No common files detected. Add your own below:\n\n");
70    }
71
72    let _ = std::fs::write(&cwshare_path, template);
73}
74
75/// Prompt user to create .cwshare file on first run in this repo.
76pub fn prompt_cwshare_setup() {
77    // Check if in git repo
78    let repo = match git::get_repo_root(None) {
79        Ok(r) => r,
80        Err(_) => return,
81    };
82
83    // Don't prompt in non-interactive environments
84    if git::is_non_interactive() {
85        return;
86    }
87
88    // Check if .cwshare already exists
89    if has_cwshare_file(&repo) {
90        if !is_cwshare_prompted(&repo) {
91            mark_cwshare_prompted(&repo);
92        }
93        return;
94    }
95
96    // Check if already prompted
97    if is_cwshare_prompted(&repo) {
98        return;
99    }
100
101    // Detect common files
102    let detected_files = detect_common_files(&repo);
103
104    // Prompt user
105    println!("\n{}", style(".cwshare File Setup").cyan().bold());
106    println!(
107        "\nWould you like to create a {} file?",
108        style(".cwshare").cyan()
109    );
110    println!("This lets you automatically copy files to new worktrees (like .env, configs).\n");
111
112    if !detected_files.is_empty() {
113        println!(
114            "{}",
115            style("Detected files that you might want to share:").bold()
116        );
117        for file in &detected_files {
118            println!("  {} {}", style("•").dim(), file);
119        }
120        println!();
121    }
122
123    // Ask user
124    use std::io::Write;
125    print!("Create .cwshare file? [Y/n]: ");
126    let _ = std::io::stdout().flush();
127
128    let mut input = String::new();
129    match std::io::stdin().read_line(&mut input) {
130        Ok(_) => {}
131        Err(_) => {
132            mark_cwshare_prompted(&repo);
133            println!(
134                "\n{}\n",
135                style("You can create .cwshare manually anytime.").dim()
136            );
137            return;
138        }
139    }
140
141    let input = input.trim().to_lowercase();
142
143    // Mark as prompted regardless of answer
144    mark_cwshare_prompted(&repo);
145
146    if input.is_empty() || input == "y" || input == "yes" {
147        create_cwshare_template(&repo, &detected_files);
148        println!(
149            "\n{} Created {}",
150            style("*").green().bold(),
151            repo.join(".cwshare").display()
152        );
153        println!("\n{}", style("Next steps:").bold());
154        println!("  1. Review and edit .cwshare to uncomment files you want to share");
155        println!(
156            "  2. Add to git: {}",
157            style("git add .cwshare && git commit").cyan()
158        );
159        println!(
160            "  3. Files will be copied when you run: {}",
161            style("gw new <branch>").cyan()
162        );
163        println!();
164    } else {
165        println!(
166            "\n{}\n",
167            style("You can create .cwshare manually anytime.").dim()
168        );
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use tempfile::TempDir;
176
177    #[test]
178    fn test_has_cwshare_file_returns_false_when_missing() {
179        let dir = TempDir::new().unwrap();
180        assert!(!has_cwshare_file(dir.path()));
181    }
182
183    #[test]
184    fn test_has_cwshare_file_returns_true_when_present() {
185        let dir = TempDir::new().unwrap();
186        std::fs::write(dir.path().join(".cwshare"), "# test").unwrap();
187        assert!(has_cwshare_file(dir.path()));
188    }
189
190    #[test]
191    fn test_detect_common_files_empty_dir() {
192        let dir = TempDir::new().unwrap();
193        let detected = detect_common_files(dir.path());
194        assert!(detected.is_empty());
195    }
196
197    #[test]
198    fn test_detect_common_files_finds_env() {
199        let dir = TempDir::new().unwrap();
200        std::fs::write(dir.path().join(".env"), "SECRET=123").unwrap();
201        std::fs::write(dir.path().join(".env.local"), "LOCAL=1").unwrap();
202
203        let detected = detect_common_files(dir.path());
204        assert_eq!(detected, vec![".env", ".env.local"]);
205    }
206
207    #[test]
208    fn test_detect_common_files_finds_nested() {
209        let dir = TempDir::new().unwrap();
210        std::fs::create_dir_all(dir.path().join("config")).unwrap();
211        std::fs::write(dir.path().join("config/local.yaml"), "key: val").unwrap();
212
213        let detected = detect_common_files(dir.path());
214        assert_eq!(detected, vec!["config/local.yaml"]);
215    }
216
217    #[test]
218    fn test_detect_common_files_finds_vscode_settings() {
219        let dir = TempDir::new().unwrap();
220        std::fs::create_dir_all(dir.path().join(".vscode")).unwrap();
221        std::fs::write(dir.path().join(".vscode/settings.json"), "{}").unwrap();
222
223        let detected = detect_common_files(dir.path());
224        assert_eq!(detected, vec![".vscode/settings.json"]);
225    }
226
227    #[test]
228    fn test_create_cwshare_template_no_suggestions() {
229        let dir = TempDir::new().unwrap();
230        create_cwshare_template(dir.path(), &[]);
231
232        let content = std::fs::read_to_string(dir.path().join(".cwshare")).unwrap();
233        assert!(content.contains("# .cwshare - Files to copy to new worktrees"));
234        assert!(content.contains("# No common files detected. Add your own below:"));
235        assert!(!content.contains("Detected files"));
236    }
237
238    #[test]
239    fn test_create_cwshare_template_with_suggestions() {
240        let dir = TempDir::new().unwrap();
241        let files = vec![".env".to_string(), ".env.local".to_string()];
242        create_cwshare_template(dir.path(), &files);
243
244        let content = std::fs::read_to_string(dir.path().join(".cwshare")).unwrap();
245        assert!(content.contains("# .cwshare - Files to copy to new worktrees"));
246        assert!(content.contains("# Detected files in this repository (uncomment to enable):"));
247        assert!(content.contains("# .env\n"));
248        assert!(content.contains("# .env.local\n"));
249        assert!(!content.contains("No common files detected"));
250    }
251
252    #[test]
253    fn test_create_cwshare_template_creates_file() {
254        let dir = TempDir::new().unwrap();
255        assert!(!dir.path().join(".cwshare").exists());
256
257        create_cwshare_template(dir.path(), &[]);
258        assert!(dir.path().join(".cwshare").exists());
259    }
260
261    #[test]
262    fn test_is_cwshare_prompted_false_without_git() {
263        // Non-git directory should return false (git config will fail)
264        let dir = TempDir::new().unwrap();
265        assert!(!is_cwshare_prompted(dir.path()));
266    }
267}