1use std::fs;
7use std::path::Path;
8
9use crate::config;
10use crate::error::PawError;
11use crate::git;
12
13const GITIGNORE_ENTRY: &str = ".git-paw/logs/";
15
16pub fn run_init() -> Result<(), PawError> {
22 let cwd = std::env::current_dir()
23 .map_err(|e| PawError::InitError(format!("cannot read current directory: {e}")))?;
24 let repo_root = git::validate_repo(&cwd)?;
25
26 let paw_dir = repo_root.join(".git-paw");
27 let logs_dir = paw_dir.join("logs");
28 let config_path = paw_dir.join("config.toml");
29
30 let created_dir = create_dir_if_missing(&paw_dir)?;
32 if created_dir {
33 println!(" Created .git-paw/");
34 }
35
36 let created_logs = create_dir_if_missing(&logs_dir)?;
38 if created_logs {
39 println!(" Created .git-paw/logs/");
40 }
41
42 let created_config = write_config_if_missing(&config_path)?;
44 if created_config {
45 println!(" Created .git-paw/config.toml");
46 }
47
48 let updated_gitignore = ensure_gitignore_entry(&repo_root)?;
50 if updated_gitignore {
51 println!(" Updated .gitignore");
52 }
53
54 if !created_dir && !created_logs && !created_config && !updated_gitignore {
55 println!("Already initialized. Nothing to do.");
56 } else {
57 println!("Initialized git-paw.");
58 }
59
60 Ok(())
61}
62
63fn create_dir_if_missing(path: &Path) -> Result<bool, PawError> {
65 if path.is_dir() {
66 return Ok(false);
67 }
68 fs::create_dir_all(path)
69 .map_err(|e| PawError::InitError(format!("failed to create '{}': {e}", path.display())))?;
70 Ok(true)
71}
72
73fn write_config_if_missing(path: &Path) -> Result<bool, PawError> {
75 if path.exists() {
76 return Ok(false);
77 }
78 let content = config::generate_default_config();
79 fs::write(path, content)
80 .map_err(|e| PawError::InitError(format!("failed to write config: {e}")))?;
81 Ok(true)
82}
83
84fn ensure_gitignore_entry(repo_root: &Path) -> Result<bool, PawError> {
86 let gitignore_path = repo_root.join(".gitignore");
87
88 let existing = match fs::read_to_string(&gitignore_path) {
89 Ok(content) => content,
90 Err(e) if e.kind() == std::io::ErrorKind::NotFound => String::new(),
91 Err(e) => {
92 return Err(PawError::InitError(format!(
93 "failed to read .gitignore: {e}"
94 )));
95 }
96 };
97
98 if existing.lines().any(|line| line.trim() == GITIGNORE_ENTRY) {
100 return Ok(false);
101 }
102
103 let mut content = existing;
105 if !content.is_empty() && !content.ends_with('\n') {
106 content.push('\n');
107 }
108 content.push_str(GITIGNORE_ENTRY);
109 content.push('\n');
110
111 fs::write(&gitignore_path, content)
112 .map_err(|e| PawError::InitError(format!("failed to write .gitignore: {e}")))?;
113
114 Ok(true)
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use tempfile::TempDir;
121
122 fn setup_repo() -> TempDir {
123 let dir = TempDir::new().unwrap();
124 fs::create_dir(dir.path().join(".git")).unwrap();
126 dir
127 }
128
129 #[test]
132 fn creates_directory_when_missing() {
133 let dir = TempDir::new().unwrap();
134 let target = dir.path().join("new-dir");
135 assert!(create_dir_if_missing(&target).unwrap());
136 assert!(target.is_dir());
137 }
138
139 #[test]
140 fn skips_existing_directory() {
141 let dir = TempDir::new().unwrap();
142 let target = dir.path().join("existing");
143 fs::create_dir(&target).unwrap();
144 assert!(!create_dir_if_missing(&target).unwrap());
145 }
146
147 #[test]
150 fn writes_config_when_missing() {
151 let dir = TempDir::new().unwrap();
152 let config_path = dir.path().join("config.toml");
153 assert!(write_config_if_missing(&config_path).unwrap());
154 let content = fs::read_to_string(&config_path).unwrap();
155 assert!(content.contains("default_cli"));
156 }
157
158 #[test]
159 fn skips_existing_config() {
160 let dir = TempDir::new().unwrap();
161 let config_path = dir.path().join("config.toml");
162 fs::write(&config_path, "existing").unwrap();
163 assert!(!write_config_if_missing(&config_path).unwrap());
164 assert_eq!(fs::read_to_string(&config_path).unwrap(), "existing");
165 }
166
167 #[test]
170 fn creates_gitignore_with_entry() {
171 let dir = setup_repo();
172 assert!(ensure_gitignore_entry(dir.path()).unwrap());
173 let content = fs::read_to_string(dir.path().join(".gitignore")).unwrap();
174 assert!(content.contains(GITIGNORE_ENTRY));
175 }
176
177 #[test]
178 fn appends_to_existing_gitignore() {
179 let dir = setup_repo();
180 fs::write(dir.path().join(".gitignore"), "node_modules/\n").unwrap();
181 assert!(ensure_gitignore_entry(dir.path()).unwrap());
182 let content = fs::read_to_string(dir.path().join(".gitignore")).unwrap();
183 assert!(content.contains("node_modules/"));
184 assert!(content.contains(GITIGNORE_ENTRY));
185 }
186
187 #[test]
188 fn appends_newline_if_missing() {
189 let dir = setup_repo();
190 fs::write(dir.path().join(".gitignore"), "node_modules/").unwrap();
191 assert!(ensure_gitignore_entry(dir.path()).unwrap());
192 let content = fs::read_to_string(dir.path().join(".gitignore")).unwrap();
193 assert!(content.contains("node_modules/\n"));
194 assert!(content.contains(GITIGNORE_ENTRY));
195 }
196
197 #[test]
198 fn skips_when_entry_already_present() {
199 let dir = setup_repo();
200 fs::write(
201 dir.path().join(".gitignore"),
202 format!("node_modules/\n{GITIGNORE_ENTRY}\n"),
203 )
204 .unwrap();
205 assert!(!ensure_gitignore_entry(dir.path()).unwrap());
206 }
207
208 #[test]
211 fn idempotent_gitignore() {
212 let dir = setup_repo();
213 ensure_gitignore_entry(dir.path()).unwrap();
214 let first = fs::read_to_string(dir.path().join(".gitignore")).unwrap();
215 ensure_gitignore_entry(dir.path()).unwrap();
216 let second = fs::read_to_string(dir.path().join(".gitignore")).unwrap();
217 assert_eq!(first, second);
218 }
219}