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 "config/local.json",
16 "config/local.yaml",
17 "config/local.yml",
18 ".vscode/settings.json",
19];
20
21pub 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
28pub fn mark_cwshare_prompted(repo: &Path) {
30 let _ = git::set_config("cwshare.prompted", "true", Some(repo));
31}
32
33pub fn has_cwshare_file(repo: &Path) -> bool {
35 repo.join(".cwshare").exists()
36}
37
38pub 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
47pub 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
75pub fn prompt_cwshare_setup() {
77 let repo = match git::get_repo_root(None) {
79 Ok(r) => r,
80 Err(_) => return,
81 };
82
83 if git::is_non_interactive() {
85 return;
86 }
87
88 if has_cwshare_file(&repo) {
90 if !is_cwshare_prompted(&repo) {
91 mark_cwshare_prompted(&repo);
92 }
93 return;
94 }
95
96 if is_cwshare_prompted(&repo) {
98 return;
99 }
100
101 let detected_files = detect_common_files(&repo);
103
104 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 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_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 let dir = TempDir::new().unwrap();
265 assert!(!is_cwshare_prompted(dir.path()));
266 }
267}