1use crate::db::Database;
2use crate::models::TaskRepo;
3use crate::utils::{Result, TrackError};
4use chrono::Utc;
5use rusqlite::{params, OptionalExtension};
6use std::path::{Path, PathBuf};
7
8pub struct RepoService<'a> {
9 db: &'a Database,
10}
11
12impl<'a> RepoService<'a> {
13 pub fn new(db: &'a Database) -> Self {
14 RepoService { db }
15 }
16
17 pub fn add_repo(
19 &self,
20 task_id: i64,
21 repo_path: &str,
22 base_branch: Option<String>,
23 base_commit_hash: Option<String>,
24 ) -> Result<TaskRepo> {
25 let abs_path = self.resolve_absolute_path(repo_path)?;
27
28 if !self.is_jj_repository(&abs_path)? {
30 return Err(TrackError::Other(format!(
31 "{} is not a JJ repository",
32 abs_path.display()
33 )));
34 }
35
36 let path_str = abs_path.to_string_lossy().to_string();
37 let created_at = Utc::now().to_rfc3339();
38
39 self.db.with_transaction(|| {
41 let existing: Option<i64> = self
43 .db
44 .get_connection()
45 .query_row(
46 "SELECT id FROM task_repos WHERE task_id = ?1 AND repo_path = ?2",
47 params![task_id, path_str],
48 |row| row.get(0),
49 )
50 .optional()?;
51
52 if existing.is_some() {
53 return Err(TrackError::Other(
54 "Repository already registered for this task".to_string(),
55 ));
56 }
57
58 let next_index: i64 = self.db.get_connection().query_row(
60 "SELECT COALESCE(MAX(task_index), 0) + 1 FROM task_repos WHERE task_id = ?1",
61 params![task_id],
62 |row| row.get(0),
63 )?;
64
65 self.db.get_connection().execute(
66 "INSERT INTO task_repos (task_id, task_index, repo_path, base_branch, base_commit_hash, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
67 params![task_id, next_index, path_str, base_branch, base_commit_hash, created_at],
68 )?;
69
70 let id = self.db.get_connection().last_insert_rowid();
71
72 self.db.increment_rev("repos")?;
73 Ok(TaskRepo {
74 id,
75 task_id,
76 task_index: next_index,
77 repo_path: path_str.clone(),
78 base_branch,
79 base_commit_hash,
80 created_at: Utc::now(),
81 })
82 })
83 }
84
85 pub fn list_repos(&self, task_id: i64) -> Result<Vec<TaskRepo>> {
87 let mut stmt = self.db.get_connection().prepare(
88 "SELECT id, task_id, task_index, repo_path, base_branch, base_commit_hash, created_at FROM task_repos WHERE task_id = ?1 ORDER BY task_index"
89 )?;
90
91 let repos = stmt
92 .query_map(params![task_id], |row| {
93 Ok(TaskRepo {
94 id: row.get(0)?,
95 task_id: row.get(1)?,
96 task_index: row.get(2)?,
97 repo_path: row.get(3)?,
98 base_branch: row.get(4)?,
99 base_commit_hash: row.get(5)?,
100 created_at: row
101 .get::<_, String>(6)?
102 .parse()
103 .unwrap_or_else(|_| Utc::now()),
104 })
105 })?
106 .collect::<std::result::Result<Vec<_>, _>>()?;
107
108 Ok(repos)
109 }
110
111 pub fn remove_repo(&self, repo_id: i64) -> Result<()> {
113 let rows_affected = self
114 .db
115 .get_connection()
116 .execute("DELETE FROM task_repos WHERE id = ?1", params![repo_id])?;
117
118 if rows_affected == 0 {
119 return Err(TrackError::Other(format!(
120 "Repository #{} not found",
121 repo_id
122 )));
123 }
124
125 self.db.increment_rev("repos")?;
126 Ok(())
127 }
128
129 fn resolve_absolute_path(&self, path: &str) -> Result<PathBuf> {
131 let path_buf = PathBuf::from(path);
132
133 if path_buf.is_absolute() {
134 Ok(path_buf)
135 } else {
136 std::env::current_dir()?
137 .join(path_buf)
138 .canonicalize()
139 .map_err(|e| TrackError::Other(format!("Failed to resolve path: {}", e)))
140 }
141 }
142
143 fn is_jj_repository(&self, path: &Path) -> Result<bool> {
145 let jj_dir = path.join(".jj");
146 Ok(jj_dir.exists())
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153 use crate::services::task_service::TaskService;
154
155 fn setup_db() -> Database {
156 Database::new_in_memory().unwrap()
157 }
158
159 #[test]
160 fn test_add_repo_success() {
161 let db = setup_db();
162 let task_service = TaskService::new(&db);
163 let repo_service = RepoService::new(&db);
164
165 let task = task_service
167 .create_task("Test Task", None, None, None)
168 .unwrap();
169
170 let temp_dir = std::env::temp_dir().join(format!("test_repo_{}", std::process::id()));
172 std::fs::create_dir_all(&temp_dir).unwrap();
173 std::fs::create_dir(temp_dir.join(".jj")).unwrap();
174
175 let repo = repo_service
177 .add_repo(task.id, temp_dir.to_str().unwrap(), None, None)
178 .unwrap();
179 assert_eq!(repo.task_id, task.id);
180 assert!(repo.repo_path.contains("test_repo"));
181
182 std::fs::remove_dir_all(&temp_dir).ok();
184 }
185
186 #[test]
187 fn test_add_repo_not_git() {
188 let db = setup_db();
189 let task_service = TaskService::new(&db);
190 let repo_service = RepoService::new(&db);
191
192 let task = task_service
193 .create_task("Test Task", None, None, None)
194 .unwrap();
195
196 let temp_dir = std::env::temp_dir().join(format!("test_not_git_{}", std::process::id()));
198 std::fs::create_dir_all(&temp_dir).unwrap();
199
200 let result = repo_service.add_repo(task.id, temp_dir.to_str().unwrap(), None, None);
202 assert!(result.is_err());
203 assert!(result
204 .unwrap_err()
205 .to_string()
206 .contains("not a JJ repository"));
207
208 std::fs::remove_dir_all(&temp_dir).ok();
210 }
211
212 #[test]
213 fn test_add_repo_duplicate() {
214 let db = setup_db();
215 let task_service = TaskService::new(&db);
216 let repo_service = RepoService::new(&db);
217
218 let task = task_service
219 .create_task("Test Task", None, None, None)
220 .unwrap();
221
222 let temp_dir = std::env::temp_dir().join(format!("test_dup_repo_{}", std::process::id()));
223 std::fs::create_dir_all(&temp_dir).unwrap();
224 std::fs::create_dir(temp_dir.join(".jj")).unwrap();
225
226 repo_service
228 .add_repo(task.id, temp_dir.to_str().unwrap(), None, None)
229 .unwrap();
230 let result = repo_service.add_repo(task.id, temp_dir.to_str().unwrap(), None, None);
231
232 assert!(result.is_err());
233 assert!(result
234 .unwrap_err()
235 .to_string()
236 .contains("already registered"));
237
238 std::fs::remove_dir_all(&temp_dir).ok();
240 }
241
242 #[test]
243 fn test_list_repos() {
244 let db = setup_db();
245 let task_service = TaskService::new(&db);
246 let repo_service = RepoService::new(&db);
247
248 let task = task_service
249 .create_task("Test Task", None, None, None)
250 .unwrap();
251
252 let temp_dir1 =
254 std::env::temp_dir().join(format!("test_list_repo1_{}", std::process::id()));
255 let temp_dir2 =
256 std::env::temp_dir().join(format!("test_list_repo2_{}", std::process::id()));
257 std::fs::create_dir_all(&temp_dir1).unwrap();
258 std::fs::create_dir_all(&temp_dir2).unwrap();
259 std::fs::create_dir(temp_dir1.join(".jj")).unwrap();
260 std::fs::create_dir(temp_dir2.join(".jj")).unwrap();
261
262 repo_service
264 .add_repo(task.id, temp_dir1.to_str().unwrap(), None, None)
265 .unwrap();
266 repo_service
267 .add_repo(task.id, temp_dir2.to_str().unwrap(), None, None)
268 .unwrap();
269
270 let repos = repo_service.list_repos(task.id).unwrap();
272 assert_eq!(repos.len(), 2);
273
274 std::fs::remove_dir_all(&temp_dir1).ok();
276 std::fs::remove_dir_all(&temp_dir2).ok();
277 }
278
279 #[test]
280 fn test_remove_repo() {
281 let db = setup_db();
282 let task_service = TaskService::new(&db);
283 let repo_service = RepoService::new(&db);
284
285 let task = task_service
286 .create_task("Test Task", None, None, None)
287 .unwrap();
288
289 let temp_dir =
290 std::env::temp_dir().join(format!("test_remove_repo_{}", std::process::id()));
291 std::fs::create_dir_all(&temp_dir).unwrap();
292 std::fs::create_dir(temp_dir.join(".jj")).unwrap();
293
294 let repo = repo_service
295 .add_repo(task.id, temp_dir.to_str().unwrap(), None, None)
296 .unwrap();
297
298 repo_service.remove_repo(repo.id).unwrap();
300
301 let repos = repo_service.list_repos(task.id).unwrap();
303 assert_eq!(repos.len(), 0);
304 }
305
306 #[test]
307 fn test_add_repo_with_base_info() {
308 let db = setup_db();
309 let task_service = TaskService::new(&db);
310 let repo_service = RepoService::new(&db);
311
312 let task = task_service
313 .create_task("Test Task", None, None, None)
314 .unwrap();
315
316 let temp_dir = std::env::temp_dir().join(format!("test_repo_base_{}", std::process::id()));
317 std::fs::create_dir_all(&temp_dir).unwrap();
318 std::fs::create_dir(temp_dir.join(".jj")).unwrap();
319
320 let base_branch = "main".to_string();
322 let base_hash = "abc123def456".to_string();
323
324 let repo = repo_service
325 .add_repo(
326 task.id,
327 temp_dir.to_str().unwrap(),
328 Some(base_branch.clone()),
329 Some(base_hash.clone()),
330 )
331 .unwrap();
332
333 assert_eq!(repo.base_branch, Some(base_branch.clone()));
334 assert_eq!(repo.base_commit_hash, Some(base_hash.clone()));
335
336 let repos = repo_service.list_repos(task.id).unwrap();
338 assert_eq!(repos.len(), 1);
339 assert_eq!(repos[0].base_branch, Some(base_branch));
340 assert_eq!(repos[0].base_commit_hash, Some(base_hash));
341
342 std::fs::remove_dir_all(&temp_dir).ok();
344 }
345}