From 7f7592e5bfa8338b978a4ea76662ab6e8444cd94 Mon Sep 17 00:00:00 2001 From: Louis Knight-Webb Date: Mon, 16 Jun 2025 17:15:51 -0400 Subject: [PATCH] new worktree on attempt create --- AGENT.md | 5 +++ backend/Cargo.toml | 1 + backend/src/models/task.rs | 12 +++++ backend/src/models/task_attempt.rs | 70 ++++++++++++++++++++++++++++-- 4 files changed, 85 insertions(+), 3 deletions(-) diff --git a/AGENT.md b/AGENT.md index 71bf66be..64033489 100644 --- a/AGENT.md +++ b/AGENT.md @@ -68,3 +68,8 @@ When working on any task that involves changes to the backend and the frontend, # Testing your work Try to build the Typescript project after any frontend changes `npm run build` + +# Backend data models + +SQLX queries should be located in backend/src/models/\* +Use getters and setters instead of raw SQL queries where possible. diff --git a/backend/Cargo.toml b/backend/Cargo.toml index e31b386c..3bcc2775 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -26,6 +26,7 @@ bcrypt = "0.15" jsonwebtoken = "9.2" ts-rs = { version = "9.0", features = ["uuid-impl", "chrono-impl"] } dirs = "5.0" +git2 = "0.18" [build-dependencies] ts-rs = { version = "9.0", features = ["uuid-impl", "chrono-impl"] } diff --git a/backend/src/models/task.rs b/backend/src/models/task.rs index 830208a5..b1842e98 100644 --- a/backend/src/models/task.rs +++ b/backend/src/models/task.rs @@ -58,6 +58,18 @@ impl Task { .await } + pub async fn find_by_id(pool: &PgPool, id: Uuid) -> Result, sqlx::Error> { + sqlx::query_as!( + Task, + r#"SELECT id, project_id, title, description, status as "status!: TaskStatus", created_at, updated_at + FROM tasks + WHERE id = $1"#, + id + ) + .fetch_optional(pool) + .await + } + pub async fn find_by_id_and_project_id(pool: &PgPool, id: Uuid, project_id: Uuid) -> Result, sqlx::Error> { sqlx::query_as!( Task, diff --git a/backend/src/models/task_attempt.rs b/backend/src/models/task_attempt.rs index ef891124..0da2a59c 100644 --- a/backend/src/models/task_attempt.rs +++ b/backend/src/models/task_attempt.rs @@ -3,6 +3,44 @@ use serde::{Deserialize, Serialize}; use sqlx::{FromRow, Type, PgPool}; use ts_rs::TS; use uuid::Uuid; +use git2::{Repository, Error as GitError}; +use std::path::Path; + +use super::task::Task; +use super::project::Project; + +#[derive(Debug)] +pub enum TaskAttemptError { + Database(sqlx::Error), + Git(GitError), + TaskNotFound, + ProjectNotFound, +} + +impl std::fmt::Display for TaskAttemptError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TaskAttemptError::Database(e) => write!(f, "Database error: {}", e), + TaskAttemptError::Git(e) => write!(f, "Git error: {}", e), + TaskAttemptError::TaskNotFound => write!(f, "Task not found"), + TaskAttemptError::ProjectNotFound => write!(f, "Project not found"), + } + } +} + +impl std::error::Error for TaskAttemptError {} + +impl From for TaskAttemptError { + fn from(err: sqlx::Error) -> Self { + TaskAttemptError::Database(err) + } +} + +impl From for TaskAttemptError { + fn from(err: GitError) -> Self { + TaskAttemptError::Git(err) + } +} #[derive(Debug, Clone, Type, Serialize, Deserialize, PartialEq, TS)] #[sqlx(type_name = "task_attempt_status", rename_all = "lowercase")] @@ -57,10 +95,34 @@ impl TaskAttempt { .await } - pub async fn create(pool: &PgPool, data: &CreateTaskAttempt, attempt_id: Uuid) -> Result { + pub async fn create(pool: &PgPool, data: &CreateTaskAttempt, attempt_id: Uuid) -> Result { let now = Utc::now(); - sqlx::query_as!( + // First, get the task to get the project_id + let task = Task::find_by_id(pool, data.task_id) + .await? + .ok_or(TaskAttemptError::TaskNotFound)?; + + // Then get the project using the project_id + let project = Project::find_by_id(pool, task.project_id) + .await? + .ok_or(TaskAttemptError::ProjectNotFound)?; + + // Create the worktree using git2 + let repo = Repository::open(&project.git_repo_path)?; + let worktree_path = Path::new(&data.worktree_path); + + // Create the worktree directory if it doesn't exist + if let Some(parent) = worktree_path.parent() { + std::fs::create_dir_all(parent).map_err(|e| TaskAttemptError::Git(GitError::from_str(&e.to_string())))?; + } + + // Create the worktree at the specified path + let branch_name = format!("attempt-{}", attempt_id); + repo.worktree(&branch_name, worktree_path, None)?; + + // Insert the record into the database + let task_attempt = sqlx::query_as!( TaskAttempt, r#"INSERT INTO task_attempts (id, task_id, worktree_path, base_commit, merge_commit, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7) @@ -74,7 +136,9 @@ impl TaskAttempt { now ) .fetch_one(pool) - .await + .await?; + + Ok(task_attempt) } pub async fn exists_for_task(pool: &PgPool, attempt_id: Uuid, task_id: Uuid, project_id: Uuid) -> Result {