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