use std::path::Path;
use console::style;
use crate::git;
const COMMON_SHARED_FILES: &[&str] = &[
".env",
".env.local",
".env.development",
".env.test",
".claude/settings.local.json",
"config/local.json",
"config/local.yaml",
"config/local.yml",
".vscode/settings.json",
];
pub fn is_cwshare_prompted(repo: &Path) -> bool {
git::get_config("cwshare.prompted", Some(repo))
.map(|v| v == "true")
.unwrap_or(false)
}
pub fn mark_cwshare_prompted(repo: &Path) {
let _ = git::set_config("cwshare.prompted", "true", Some(repo));
}
pub fn has_cwshare_file(repo: &Path) -> bool {
repo.join(".cwshare").exists()
}
pub fn detect_common_files(repo: &Path) -> Vec<String> {
COMMON_SHARED_FILES
.iter()
.filter(|f| repo.join(f).exists())
.map(|f| f.to_string())
.collect()
}
pub fn create_cwshare_template(repo: &Path, suggested_files: &[String]) {
let cwshare_path = repo.join(".cwshare");
let mut template = String::from(
"# .cwshare - Files to copy to new worktrees\n\
#\n\
# Files listed here will be automatically copied when you run 'gw new'.\n\
# Useful for environment files and local configs not tracked in git.\n\
#\n\
# Format:\n\
# - One file/directory path per line (relative to repo root)\n\
# - Lines starting with # are comments\n\
# - Empty lines are ignored\n",
);
if !suggested_files.is_empty() {
template.push_str("#\n# Detected files in this repository (uncomment to enable):\n\n");
for file in suggested_files {
template.push_str(&format!("# {}\n", file));
}
} else {
template.push_str("#\n# No common files detected. Add your own below:\n\n");
}
let _ = std::fs::write(&cwshare_path, template);
}
pub fn prompt_cwshare_setup() {
let repo = match git::get_repo_root(None) {
Ok(r) => r,
Err(_) => return,
};
if git::is_non_interactive() {
return;
}
if has_cwshare_file(&repo) {
if !is_cwshare_prompted(&repo) {
mark_cwshare_prompted(&repo);
}
return;
}
if is_cwshare_prompted(&repo) {
return;
}
let detected_files = detect_common_files(&repo);
println!("\n{}", style(".cwshare File Setup").cyan().bold());
println!(
"\nWould you like to create a {} file?",
style(".cwshare").cyan()
);
println!("This lets you automatically copy files to new worktrees (like .env, configs).\n");
if !detected_files.is_empty() {
println!(
"{}",
style("Detected files that you might want to share:").bold()
);
for file in &detected_files {
println!(" {} {}", style("•").dim(), file);
}
println!();
}
use std::io::Write;
print!("Create .cwshare file? [Y/n]: ");
let _ = std::io::stdout().flush();
let mut input = String::new();
match std::io::stdin().read_line(&mut input) {
Ok(_) => {}
Err(_) => {
mark_cwshare_prompted(&repo);
println!(
"\n{}\n",
style("You can create .cwshare manually anytime.").dim()
);
return;
}
}
let input = input.trim().to_lowercase();
mark_cwshare_prompted(&repo);
if input.is_empty() || input == "y" || input == "yes" {
create_cwshare_template(&repo, &detected_files);
println!(
"\n{} Created {}",
style("*").green().bold(),
repo.join(".cwshare").display()
);
println!("\n{}", style("Next steps:").bold());
println!(" 1. Review and edit .cwshare to uncomment files you want to share");
println!(
" 2. Add to git: {}",
style("git add .cwshare && git commit").cyan()
);
println!(
" 3. Files will be copied when you run: {}",
style("gw new <branch>").cyan()
);
println!();
} else {
println!(
"\n{}\n",
style("You can create .cwshare manually anytime.").dim()
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_has_cwshare_file_returns_false_when_missing() {
let dir = TempDir::new().unwrap();
assert!(!has_cwshare_file(dir.path()));
}
#[test]
fn test_has_cwshare_file_returns_true_when_present() {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join(".cwshare"), "# test").unwrap();
assert!(has_cwshare_file(dir.path()));
}
#[test]
fn test_detect_common_files_empty_dir() {
let dir = TempDir::new().unwrap();
let detected = detect_common_files(dir.path());
assert!(detected.is_empty());
}
#[test]
fn test_detect_common_files_finds_env() {
let dir = TempDir::new().unwrap();
std::fs::write(dir.path().join(".env"), "SECRET=123").unwrap();
std::fs::write(dir.path().join(".env.local"), "LOCAL=1").unwrap();
let detected = detect_common_files(dir.path());
assert_eq!(detected, vec![".env", ".env.local"]);
}
#[test]
fn test_detect_common_files_finds_nested() {
let dir = TempDir::new().unwrap();
std::fs::create_dir_all(dir.path().join("config")).unwrap();
std::fs::write(dir.path().join("config/local.yaml"), "key: val").unwrap();
let detected = detect_common_files(dir.path());
assert_eq!(detected, vec!["config/local.yaml"]);
}
#[test]
fn test_detect_common_files_finds_vscode_settings() {
let dir = TempDir::new().unwrap();
std::fs::create_dir_all(dir.path().join(".vscode")).unwrap();
std::fs::write(dir.path().join(".vscode/settings.json"), "{}").unwrap();
let detected = detect_common_files(dir.path());
assert_eq!(detected, vec![".vscode/settings.json"]);
}
#[test]
fn test_create_cwshare_template_no_suggestions() {
let dir = TempDir::new().unwrap();
create_cwshare_template(dir.path(), &[]);
let content = std::fs::read_to_string(dir.path().join(".cwshare")).unwrap();
assert!(content.contains("# .cwshare - Files to copy to new worktrees"));
assert!(content.contains("# No common files detected. Add your own below:"));
assert!(!content.contains("Detected files"));
}
#[test]
fn test_create_cwshare_template_with_suggestions() {
let dir = TempDir::new().unwrap();
let files = vec![".env".to_string(), ".env.local".to_string()];
create_cwshare_template(dir.path(), &files);
let content = std::fs::read_to_string(dir.path().join(".cwshare")).unwrap();
assert!(content.contains("# .cwshare - Files to copy to new worktrees"));
assert!(content.contains("# Detected files in this repository (uncomment to enable):"));
assert!(content.contains("# .env\n"));
assert!(content.contains("# .env.local\n"));
assert!(!content.contains("No common files detected"));
}
#[test]
fn test_create_cwshare_template_creates_file() {
let dir = TempDir::new().unwrap();
assert!(!dir.path().join(".cwshare").exists());
create_cwshare_template(dir.path(), &[]);
assert!(dir.path().join(".cwshare").exists());
}
#[test]
fn test_is_cwshare_prompted_false_without_git() {
let dir = TempDir::new().unwrap();
assert!(!is_cwshare_prompted(dir.path()));
}
}