use crate::db::Database;
use crate::models::TaskRepo;
use crate::utils::{Result, TrackError};
use chrono::Utc;
use rusqlite::{params, OptionalExtension};
use std::path::{Path, PathBuf};
pub struct RepoService<'a> {
db: &'a Database,
}
impl<'a> RepoService<'a> {
pub fn new(db: &'a Database) -> Self {
RepoService { db }
}
pub fn add_repo(
&self,
task_id: i64,
repo_path: &str,
base_branch: Option<String>,
base_commit_hash: Option<String>,
) -> Result<TaskRepo> {
let abs_path = self.resolve_absolute_path(repo_path)?;
if !self.is_jj_repository(&abs_path)? {
return Err(TrackError::Other(format!(
"{} is not a JJ repository",
abs_path.display()
)));
}
let path_str = abs_path.to_string_lossy().to_string();
let created_at = Utc::now().to_rfc3339();
self.db.with_transaction(|| {
let existing: Option<i64> = self
.db
.get_connection()
.query_row(
"SELECT id FROM task_repos WHERE task_id = ?1 AND repo_path = ?2",
params![task_id, path_str],
|row| row.get(0),
)
.optional()?;
if existing.is_some() {
return Err(TrackError::Other(
"Repository already registered for this task".to_string(),
));
}
let next_index: i64 = self.db.get_connection().query_row(
"SELECT COALESCE(MAX(task_index), 0) + 1 FROM task_repos WHERE task_id = ?1",
params![task_id],
|row| row.get(0),
)?;
self.db.get_connection().execute(
"INSERT INTO task_repos (task_id, task_index, repo_path, base_branch, base_commit_hash, created_at) VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
params![task_id, next_index, path_str, base_branch, base_commit_hash, created_at],
)?;
let id = self.db.get_connection().last_insert_rowid();
self.db.increment_rev("repos")?;
Ok(TaskRepo {
id,
task_id,
task_index: next_index,
repo_path: path_str.clone(),
base_branch,
base_commit_hash,
created_at: Utc::now(),
})
})
}
pub fn list_repos(&self, task_id: i64) -> Result<Vec<TaskRepo>> {
let mut stmt = self.db.get_connection().prepare(
"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"
)?;
let repos = stmt
.query_map(params![task_id], |row| {
Ok(TaskRepo {
id: row.get(0)?,
task_id: row.get(1)?,
task_index: row.get(2)?,
repo_path: row.get(3)?,
base_branch: row.get(4)?,
base_commit_hash: row.get(5)?,
created_at: row
.get::<_, String>(6)?
.parse()
.unwrap_or_else(|_| Utc::now()),
})
})?
.collect::<std::result::Result<Vec<_>, _>>()?;
Ok(repos)
}
pub fn remove_repo(&self, repo_id: i64) -> Result<()> {
let rows_affected = self
.db
.get_connection()
.execute("DELETE FROM task_repos WHERE id = ?1", params![repo_id])?;
if rows_affected == 0 {
return Err(TrackError::Other(format!(
"Repository #{} not found",
repo_id
)));
}
self.db.increment_rev("repos")?;
Ok(())
}
fn resolve_absolute_path(&self, path: &str) -> Result<PathBuf> {
let path_buf = PathBuf::from(path);
if path_buf.is_absolute() {
Ok(path_buf)
} else {
std::env::current_dir()?
.join(path_buf)
.canonicalize()
.map_err(|e| TrackError::Other(format!("Failed to resolve path: {}", e)))
}
}
fn is_jj_repository(&self, path: &Path) -> Result<bool> {
let jj_dir = path.join(".jj");
Ok(jj_dir.exists())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::services::task_service::TaskService;
fn setup_db() -> Database {
Database::new_in_memory().unwrap()
}
#[test]
fn test_add_repo_success() {
let db = setup_db();
let task_service = TaskService::new(&db);
let repo_service = RepoService::new(&db);
let task = task_service
.create_task("Test Task", None, None, None)
.unwrap();
let temp_dir = std::env::temp_dir().join(format!("test_repo_{}", std::process::id()));
std::fs::create_dir_all(&temp_dir).unwrap();
std::fs::create_dir(temp_dir.join(".jj")).unwrap();
let repo = repo_service
.add_repo(task.id, temp_dir.to_str().unwrap(), None, None)
.unwrap();
assert_eq!(repo.task_id, task.id);
assert!(repo.repo_path.contains("test_repo"));
std::fs::remove_dir_all(&temp_dir).ok();
}
#[test]
fn test_add_repo_not_git() {
let db = setup_db();
let task_service = TaskService::new(&db);
let repo_service = RepoService::new(&db);
let task = task_service
.create_task("Test Task", None, None, None)
.unwrap();
let temp_dir = std::env::temp_dir().join(format!("test_not_git_{}", std::process::id()));
std::fs::create_dir_all(&temp_dir).unwrap();
let result = repo_service.add_repo(task.id, temp_dir.to_str().unwrap(), None, None);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("not a JJ repository"));
std::fs::remove_dir_all(&temp_dir).ok();
}
#[test]
fn test_add_repo_duplicate() {
let db = setup_db();
let task_service = TaskService::new(&db);
let repo_service = RepoService::new(&db);
let task = task_service
.create_task("Test Task", None, None, None)
.unwrap();
let temp_dir = std::env::temp_dir().join(format!("test_dup_repo_{}", std::process::id()));
std::fs::create_dir_all(&temp_dir).unwrap();
std::fs::create_dir(temp_dir.join(".jj")).unwrap();
repo_service
.add_repo(task.id, temp_dir.to_str().unwrap(), None, None)
.unwrap();
let result = repo_service.add_repo(task.id, temp_dir.to_str().unwrap(), None, None);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("already registered"));
std::fs::remove_dir_all(&temp_dir).ok();
}
#[test]
fn test_list_repos() {
let db = setup_db();
let task_service = TaskService::new(&db);
let repo_service = RepoService::new(&db);
let task = task_service
.create_task("Test Task", None, None, None)
.unwrap();
let temp_dir1 =
std::env::temp_dir().join(format!("test_list_repo1_{}", std::process::id()));
let temp_dir2 =
std::env::temp_dir().join(format!("test_list_repo2_{}", std::process::id()));
std::fs::create_dir_all(&temp_dir1).unwrap();
std::fs::create_dir_all(&temp_dir2).unwrap();
std::fs::create_dir(temp_dir1.join(".jj")).unwrap();
std::fs::create_dir(temp_dir2.join(".jj")).unwrap();
repo_service
.add_repo(task.id, temp_dir1.to_str().unwrap(), None, None)
.unwrap();
repo_service
.add_repo(task.id, temp_dir2.to_str().unwrap(), None, None)
.unwrap();
let repos = repo_service.list_repos(task.id).unwrap();
assert_eq!(repos.len(), 2);
std::fs::remove_dir_all(&temp_dir1).ok();
std::fs::remove_dir_all(&temp_dir2).ok();
}
#[test]
fn test_remove_repo() {
let db = setup_db();
let task_service = TaskService::new(&db);
let repo_service = RepoService::new(&db);
let task = task_service
.create_task("Test Task", None, None, None)
.unwrap();
let temp_dir =
std::env::temp_dir().join(format!("test_remove_repo_{}", std::process::id()));
std::fs::create_dir_all(&temp_dir).unwrap();
std::fs::create_dir(temp_dir.join(".jj")).unwrap();
let repo = repo_service
.add_repo(task.id, temp_dir.to_str().unwrap(), None, None)
.unwrap();
repo_service.remove_repo(repo.id).unwrap();
let repos = repo_service.list_repos(task.id).unwrap();
assert_eq!(repos.len(), 0);
}
#[test]
fn test_add_repo_with_base_info() {
let db = setup_db();
let task_service = TaskService::new(&db);
let repo_service = RepoService::new(&db);
let task = task_service
.create_task("Test Task", None, None, None)
.unwrap();
let temp_dir = std::env::temp_dir().join(format!("test_repo_base_{}", std::process::id()));
std::fs::create_dir_all(&temp_dir).unwrap();
std::fs::create_dir(temp_dir.join(".jj")).unwrap();
let base_branch = "main".to_string();
let base_hash = "abc123def456".to_string();
let repo = repo_service
.add_repo(
task.id,
temp_dir.to_str().unwrap(),
Some(base_branch.clone()),
Some(base_hash.clone()),
)
.unwrap();
assert_eq!(repo.base_branch, Some(base_branch.clone()));
assert_eq!(repo.base_commit_hash, Some(base_hash.clone()));
let repos = repo_service.list_repos(task.id).unwrap();
assert_eq!(repos.len(), 1);
assert_eq!(repos[0].base_branch, Some(base_branch));
assert_eq!(repos[0].base_commit_hash, Some(base_hash));
std::fs::remove_dir_all(&temp_dir).ok();
}
}