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