git_worktree_manager/
cwshare_setup.rs1use std::path::Path;
4
5use console::style;
6
7use crate::git;
8
9const 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
22pub 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
29pub fn mark_cwshare_prompted(repo: &Path) {
31 let _ = git::set_config("cwshare.prompted", "true", Some(repo));
32}
33
34pub fn has_cwshare_file(repo: &Path) -> bool {
36 repo.join(".cwshare").exists()
37}
38
39pub 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
48pub 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
76pub fn prompt_cwshare_setup() {
78 let repo = match git::get_repo_root(None) {
80 Ok(r) => r,
81 Err(_) => return,
82 };
83
84 if git::is_non_interactive() {
86 return;
87 }
88
89 if has_cwshare_file(&repo) {
91 if !is_cwshare_prompted(&repo) {
92 mark_cwshare_prompted(&repo);
93 }
94 return;
95 }
96
97 if is_cwshare_prompted(&repo) {
99 return;
100 }
101
102 let detected_files = detect_common_files(&repo);
104
105 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 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_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 let dir = TempDir::new().unwrap();
266 assert!(!is_cwshare_prompted(dir.path()));
267 }
268}