diff --git a/backend/.sqlx/query-934682901882acc939b82cb04fba66dedf5f3d4775601cdb0be9988b35438c09.json b/backend/.sqlx/query-0cb9073512165a38ad21dc1fdb88548b8e8e0a369eb18228287a19ada5ee402c.json similarity index 76% rename from backend/.sqlx/query-934682901882acc939b82cb04fba66dedf5f3d4775601cdb0be9988b35438c09.json rename to backend/.sqlx/query-0cb9073512165a38ad21dc1fdb88548b8e8e0a369eb18228287a19ada5ee402c.json index 00c550be..ef8fc9e2 100644 --- a/backend/.sqlx/query-934682901882acc939b82cb04fba66dedf5f3d4775601cdb0be9988b35438c09.json +++ b/backend/.sqlx/query-0cb9073512165a38ad21dc1fdb88548b8e8e0a369eb18228287a19ada5ee402c.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT id as \"id!: Uuid\", task_id as \"task_id!: Uuid\", worktree_path, branch, merge_commit, executor, pr_url, pr_number, pr_status, pr_merged_at as \"pr_merged_at: DateTime\", created_at as \"created_at!: DateTime\", updated_at as \"updated_at!: DateTime\"\n FROM task_attempts \n WHERE task_id = $1 \n ORDER BY created_at DESC", + "query": "SELECT id as \"id!: Uuid\", task_id as \"task_id!: Uuid\", worktree_path, branch, base_branch, merge_commit, executor, pr_url, pr_number, pr_status, pr_merged_at as \"pr_merged_at: DateTime\", created_at as \"created_at!: DateTime\", updated_at as \"updated_at!: DateTime\"\n FROM task_attempts \n WHERE id = $1", "describe": { "columns": [ { @@ -24,43 +24,48 @@ "type_info": "Text" }, { - "name": "merge_commit", + "name": "base_branch", "ordinal": 4, "type_info": "Text" }, { - "name": "executor", + "name": "merge_commit", "ordinal": 5, "type_info": "Text" }, { - "name": "pr_url", + "name": "executor", "ordinal": 6, "type_info": "Text" }, { - "name": "pr_number", + "name": "pr_url", "ordinal": 7, + "type_info": "Text" + }, + { + "name": "pr_number", + "ordinal": 8, "type_info": "Integer" }, { "name": "pr_status", - "ordinal": 8, + "ordinal": 9, "type_info": "Text" }, { "name": "pr_merged_at: DateTime", - "ordinal": 9, + "ordinal": 10, "type_info": "Datetime" }, { "name": "created_at!: DateTime", - "ordinal": 10, + "ordinal": 11, "type_info": "Text" }, { "name": "updated_at!: DateTime", - "ordinal": 11, + "ordinal": 12, "type_info": "Text" } ], @@ -72,6 +77,7 @@ false, false, false, + false, true, true, true, @@ -82,5 +88,5 @@ false ] }, - "hash": "934682901882acc939b82cb04fba66dedf5f3d4775601cdb0be9988b35438c09" + "hash": "0cb9073512165a38ad21dc1fdb88548b8e8e0a369eb18228287a19ada5ee402c" } diff --git a/backend/.sqlx/query-36f9435579ce655f583eeeb419353541c30a8b4c1e047ea89d204645293bd44e.json b/backend/.sqlx/query-5634c7b542833bc811d47391bfab0af14914e93b0dc1e9ec9de8869fe661a2d4.json similarity index 69% rename from backend/.sqlx/query-36f9435579ce655f583eeeb419353541c30a8b4c1e047ea89d204645293bd44e.json rename to backend/.sqlx/query-5634c7b542833bc811d47391bfab0af14914e93b0dc1e9ec9de8869fe661a2d4.json index d02f2cfe..a1bb2a05 100644 --- a/backend/.sqlx/query-36f9435579ce655f583eeeb419353541c30a8b4c1e047ea89d204645293bd44e.json +++ b/backend/.sqlx/query-5634c7b542833bc811d47391bfab0af14914e93b0dc1e9ec9de8869fe661a2d4.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "INSERT INTO task_attempts (id, task_id, worktree_path, branch, merge_commit, executor, pr_url, pr_number, pr_status, pr_merged_at) \n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) \n RETURNING id as \"id!: Uuid\", task_id as \"task_id!: Uuid\", worktree_path, branch, merge_commit, executor, pr_url, pr_number, pr_status, pr_merged_at as \"pr_merged_at: DateTime\", created_at as \"created_at!: DateTime\", updated_at as \"updated_at!: DateTime\"", + "query": "INSERT INTO task_attempts (id, task_id, worktree_path, branch, base_branch, merge_commit, executor, pr_url, pr_number, pr_status, pr_merged_at) \n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) \n RETURNING id as \"id!: Uuid\", task_id as \"task_id!: Uuid\", worktree_path, branch, base_branch, merge_commit, executor, pr_url, pr_number, pr_status, pr_merged_at as \"pr_merged_at: DateTime\", created_at as \"created_at!: DateTime\", updated_at as \"updated_at!: DateTime\"", "describe": { "columns": [ { @@ -24,54 +24,60 @@ "type_info": "Text" }, { - "name": "merge_commit", + "name": "base_branch", "ordinal": 4, "type_info": "Text" }, { - "name": "executor", + "name": "merge_commit", "ordinal": 5, "type_info": "Text" }, { - "name": "pr_url", + "name": "executor", "ordinal": 6, "type_info": "Text" }, { - "name": "pr_number", + "name": "pr_url", "ordinal": 7, + "type_info": "Text" + }, + { + "name": "pr_number", + "ordinal": 8, "type_info": "Integer" }, { "name": "pr_status", - "ordinal": 8, + "ordinal": 9, "type_info": "Text" }, { "name": "pr_merged_at: DateTime", - "ordinal": 9, + "ordinal": 10, "type_info": "Datetime" }, { "name": "created_at!: DateTime", - "ordinal": 10, + "ordinal": 11, "type_info": "Text" }, { "name": "updated_at!: DateTime", - "ordinal": 11, + "ordinal": 12, "type_info": "Text" } ], "parameters": { - "Right": 10 + "Right": 11 }, "nullable": [ true, false, false, false, + false, true, true, true, @@ -82,5 +88,5 @@ false ] }, - "hash": "36f9435579ce655f583eeeb419353541c30a8b4c1e047ea89d204645293bd44e" + "hash": "5634c7b542833bc811d47391bfab0af14914e93b0dc1e9ec9de8869fe661a2d4" } diff --git a/backend/.sqlx/query-c0e3402045a18d43bcbb882c9ea158f01d116d92f7f7e0fd467af8e2066dc638.json b/backend/.sqlx/query-6d3d6f10f57247d29724962eb428fee95dbdc4ae87f8c90b7bbd7f5c88d5a271.json similarity index 71% rename from backend/.sqlx/query-c0e3402045a18d43bcbb882c9ea158f01d116d92f7f7e0fd467af8e2066dc638.json rename to backend/.sqlx/query-6d3d6f10f57247d29724962eb428fee95dbdc4ae87f8c90b7bbd7f5c88d5a271.json index 54e3f7e6..a1de8c07 100644 --- a/backend/.sqlx/query-c0e3402045a18d43bcbb882c9ea158f01d116d92f7f7e0fd467af8e2066dc638.json +++ b/backend/.sqlx/query-6d3d6f10f57247d29724962eb428fee95dbdc4ae87f8c90b7bbd7f5c88d5a271.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT ta.id as \"id!: Uuid\", ta.task_id as \"task_id!: Uuid\", ta.worktree_path, ta.branch, ta.merge_commit, ta.executor, ta.pr_url, ta.pr_number, ta.pr_status, ta.pr_merged_at as \"pr_merged_at: DateTime\", ta.created_at as \"created_at!: DateTime\", ta.updated_at as \"updated_at!: DateTime\"\n FROM task_attempts ta \n JOIN tasks t ON ta.task_id = t.id \n WHERE ta.id = $1 AND t.id = $2 AND t.project_id = $3", + "query": "SELECT ta.id as \"id!: Uuid\", ta.task_id as \"task_id!: Uuid\", ta.worktree_path, ta.branch, ta.base_branch, ta.merge_commit, ta.executor, ta.pr_url, ta.pr_number, ta.pr_status, ta.pr_merged_at as \"pr_merged_at: DateTime\", ta.created_at as \"created_at!: DateTime\", ta.updated_at as \"updated_at!: DateTime\"\n FROM task_attempts ta \n JOIN tasks t ON ta.task_id = t.id \n WHERE ta.id = $1 AND t.id = $2 AND t.project_id = $3", "describe": { "columns": [ { @@ -24,43 +24,48 @@ "type_info": "Text" }, { - "name": "merge_commit", + "name": "base_branch", "ordinal": 4, "type_info": "Text" }, { - "name": "executor", + "name": "merge_commit", "ordinal": 5, "type_info": "Text" }, { - "name": "pr_url", + "name": "executor", "ordinal": 6, "type_info": "Text" }, { - "name": "pr_number", + "name": "pr_url", "ordinal": 7, + "type_info": "Text" + }, + { + "name": "pr_number", + "ordinal": 8, "type_info": "Integer" }, { "name": "pr_status", - "ordinal": 8, + "ordinal": 9, "type_info": "Text" }, { "name": "pr_merged_at: DateTime", - "ordinal": 9, + "ordinal": 10, "type_info": "Datetime" }, { "name": "created_at!: DateTime", - "ordinal": 10, + "ordinal": 11, "type_info": "Text" }, { "name": "updated_at!: DateTime", - "ordinal": 11, + "ordinal": 12, "type_info": "Text" } ], @@ -72,6 +77,7 @@ false, false, false, + false, true, true, true, @@ -82,5 +88,5 @@ false ] }, - "hash": "c0e3402045a18d43bcbb882c9ea158f01d116d92f7f7e0fd467af8e2066dc638" + "hash": "6d3d6f10f57247d29724962eb428fee95dbdc4ae87f8c90b7bbd7f5c88d5a271" } diff --git a/backend/.sqlx/query-7193dead2b112b137880482fe8e8c822c67ef6692e0456683331a438a4aa002f.json b/backend/.sqlx/query-7193dead2b112b137880482fe8e8c822c67ef6692e0456683331a438a4aa002f.json deleted file mode 100644 index 02229377..00000000 --- a/backend/.sqlx/query-7193dead2b112b137880482fe8e8c822c67ef6692e0456683331a438a4aa002f.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT id as \"id!: Uuid\", project_id as \"project_id!: Uuid\", title, description, status as \"status!: TaskStatus\", created_at as \"created_at!: DateTime\", updated_at as \"updated_at!: DateTime\"\n FROM tasks \n WHERE project_id = $1 AND title = $2\n LIMIT 1", - "describe": { - "columns": [ - { - "name": "id!: Uuid", - "ordinal": 0, - "type_info": "Blob" - }, - { - "name": "project_id!: Uuid", - "ordinal": 1, - "type_info": "Blob" - }, - { - "name": "title", - "ordinal": 2, - "type_info": "Text" - }, - { - "name": "description", - "ordinal": 3, - "type_info": "Text" - }, - { - "name": "status!: TaskStatus", - "ordinal": 4, - "type_info": "Text" - }, - { - "name": "created_at!: DateTime", - "ordinal": 5, - "type_info": "Text" - }, - { - "name": "updated_at!: DateTime", - "ordinal": 6, - "type_info": "Text" - } - ], - "parameters": { - "Right": 2 - }, - "nullable": [ - true, - false, - false, - true, - false, - false, - false - ] - }, - "hash": "7193dead2b112b137880482fe8e8c822c67ef6692e0456683331a438a4aa002f" -} diff --git a/backend/.sqlx/query-b0e812c0c8e6456e1ae11a89e8c03e29727de9d08652b61350e0555d348ecc59.json b/backend/.sqlx/query-849ce16b3d341fe420816fe91c345a44475a4383221bfe7711e1ee1b5b3b0aeb.json similarity index 74% rename from backend/.sqlx/query-b0e812c0c8e6456e1ae11a89e8c03e29727de9d08652b61350e0555d348ecc59.json rename to backend/.sqlx/query-849ce16b3d341fe420816fe91c345a44475a4383221bfe7711e1ee1b5b3b0aeb.json index 58d7d732..b4b1ba6c 100644 --- a/backend/.sqlx/query-b0e812c0c8e6456e1ae11a89e8c03e29727de9d08652b61350e0555d348ecc59.json +++ b/backend/.sqlx/query-849ce16b3d341fe420816fe91c345a44475a4383221bfe7711e1ee1b5b3b0aeb.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT id as \"id!: Uuid\", task_id as \"task_id!: Uuid\", worktree_path, branch, merge_commit, executor, pr_url, pr_number, pr_status, pr_merged_at as \"pr_merged_at: DateTime\", created_at as \"created_at!: DateTime\", updated_at as \"updated_at!: DateTime\"\n FROM task_attempts \n WHERE id = $1", + "query": "SELECT id as \"id!: Uuid\", task_id as \"task_id!: Uuid\", worktree_path, branch, base_branch, merge_commit, executor, pr_url, pr_number, pr_status, pr_merged_at as \"pr_merged_at: DateTime\", created_at as \"created_at!: DateTime\", updated_at as \"updated_at!: DateTime\"\n FROM task_attempts \n WHERE task_id = $1 \n ORDER BY created_at DESC", "describe": { "columns": [ { @@ -24,43 +24,48 @@ "type_info": "Text" }, { - "name": "merge_commit", + "name": "base_branch", "ordinal": 4, "type_info": "Text" }, { - "name": "executor", + "name": "merge_commit", "ordinal": 5, "type_info": "Text" }, { - "name": "pr_url", + "name": "executor", "ordinal": 6, "type_info": "Text" }, { - "name": "pr_number", + "name": "pr_url", "ordinal": 7, + "type_info": "Text" + }, + { + "name": "pr_number", + "ordinal": 8, "type_info": "Integer" }, { "name": "pr_status", - "ordinal": 8, + "ordinal": 9, "type_info": "Text" }, { "name": "pr_merged_at: DateTime", - "ordinal": 9, + "ordinal": 10, "type_info": "Datetime" }, { "name": "created_at!: DateTime", - "ordinal": 10, + "ordinal": 11, "type_info": "Text" }, { "name": "updated_at!: DateTime", - "ordinal": 11, + "ordinal": 12, "type_info": "Text" } ], @@ -72,6 +77,7 @@ false, false, false, + false, true, true, true, @@ -82,5 +88,5 @@ false ] }, - "hash": "b0e812c0c8e6456e1ae11a89e8c03e29727de9d08652b61350e0555d348ecc59" + "hash": "849ce16b3d341fe420816fe91c345a44475a4383221bfe7711e1ee1b5b3b0aeb" } diff --git a/backend/.sqlx/query-fe7c982685e4d98b871b03535de042e64b3b28f2c2837d064031159ea5048029.json b/backend/.sqlx/query-bb7926e3e1698b72ade97fa5f32314f0a2cf3e7eb3f472c32de2962a2cc806d4.json similarity index 66% rename from backend/.sqlx/query-fe7c982685e4d98b871b03535de042e64b3b28f2c2837d064031159ea5048029.json rename to backend/.sqlx/query-bb7926e3e1698b72ade97fa5f32314f0a2cf3e7eb3f472c32de2962a2cc806d4.json index 55cd399d..547fb4ba 100644 --- a/backend/.sqlx/query-fe7c982685e4d98b871b03535de042e64b3b28f2c2837d064031159ea5048029.json +++ b/backend/.sqlx/query-bb7926e3e1698b72ade97fa5f32314f0a2cf3e7eb3f472c32de2962a2cc806d4.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "\n SELECT ta.id AS \"id!: Uuid\",\n ta.task_id AS \"task_id!: Uuid\",\n ta.worktree_path,\n ta.branch,\n ta.merge_commit,\n ta.executor,\n ta.pr_url,\n ta.pr_number,\n ta.pr_status,\n ta.pr_merged_at AS \"pr_merged_at: DateTime\",\n ta.created_at AS \"created_at!: DateTime\",\n ta.updated_at AS \"updated_at!: DateTime\"\n FROM task_attempts ta\n JOIN tasks t ON ta.task_id = t.id\n WHERE ta.id = $1\n AND t.id = $2\n AND t.project_id = $3\n ", + "query": "\n SELECT ta.id AS \"id!: Uuid\",\n ta.task_id AS \"task_id!: Uuid\",\n ta.worktree_path,\n ta.branch,\n ta.base_branch,\n ta.merge_commit,\n ta.executor,\n ta.pr_url,\n ta.pr_number,\n ta.pr_status,\n ta.pr_merged_at AS \"pr_merged_at: DateTime\",\n ta.created_at AS \"created_at!: DateTime\",\n ta.updated_at AS \"updated_at!: DateTime\"\n FROM task_attempts ta\n JOIN tasks t ON ta.task_id = t.id\n WHERE ta.id = $1\n AND t.id = $2\n AND t.project_id = $3\n ", "describe": { "columns": [ { @@ -24,43 +24,48 @@ "type_info": "Text" }, { - "name": "merge_commit", + "name": "base_branch", "ordinal": 4, "type_info": "Text" }, { - "name": "executor", + "name": "merge_commit", "ordinal": 5, "type_info": "Text" }, { - "name": "pr_url", + "name": "executor", "ordinal": 6, "type_info": "Text" }, { - "name": "pr_number", + "name": "pr_url", "ordinal": 7, + "type_info": "Text" + }, + { + "name": "pr_number", + "ordinal": 8, "type_info": "Integer" }, { "name": "pr_status", - "ordinal": 8, + "ordinal": 9, "type_info": "Text" }, { "name": "pr_merged_at: DateTime", - "ordinal": 9, + "ordinal": 10, "type_info": "Datetime" }, { "name": "created_at!: DateTime", - "ordinal": 10, + "ordinal": 11, "type_info": "Text" }, { "name": "updated_at!: DateTime", - "ordinal": 11, + "ordinal": 12, "type_info": "Text" } ], @@ -72,6 +77,7 @@ false, false, false, + false, true, true, true, @@ -82,5 +88,5 @@ false ] }, - "hash": "fe7c982685e4d98b871b03535de042e64b3b28f2c2837d064031159ea5048029" + "hash": "bb7926e3e1698b72ade97fa5f32314f0a2cf3e7eb3f472c32de2962a2cc806d4" } diff --git a/backend/migrations/20250708000000_add_base_branch_to_task_attempts.sql b/backend/migrations/20250708000000_add_base_branch_to_task_attempts.sql new file mode 100644 index 00000000..c9e057bf --- /dev/null +++ b/backend/migrations/20250708000000_add_base_branch_to_task_attempts.sql @@ -0,0 +1,2 @@ +-- Add base_branch column to task_attempts table with default value +ALTER TABLE task_attempts ADD COLUMN base_branch TEXT NOT NULL DEFAULT 'main'; diff --git a/backend/src/models/task_attempt.rs b/backend/src/models/task_attempt.rs index 7a777590..5ecf72a0 100644 --- a/backend/src/models/task_attempt.rs +++ b/backend/src/models/task_attempt.rs @@ -67,7 +67,8 @@ pub struct TaskAttempt { pub id: Uuid, pub task_id: Uuid, // Foreign key to Task pub worktree_path: String, - pub branch: String, // Git branch name for this task attempt + pub branch: String, // Git branch name for this task attempt + pub base_branch: String, // Base branch this attempt is based on pub merge_commit: Option, pub executor: Option, // Name of the executor to use pub pr_url: Option, // GitHub PR URL @@ -175,7 +176,7 @@ impl TaskAttempt { pub async fn find_by_id(pool: &SqlitePool, id: Uuid) -> Result, sqlx::Error> { sqlx::query_as!( TaskAttempt, - r#"SELECT id as "id!: Uuid", task_id as "task_id!: Uuid", worktree_path, branch, merge_commit, executor, pr_url, pr_number, pr_status, pr_merged_at as "pr_merged_at: DateTime", created_at as "created_at!: DateTime", updated_at as "updated_at!: DateTime" + r#"SELECT id as "id!: Uuid", task_id as "task_id!: Uuid", worktree_path, branch, base_branch, merge_commit, executor, pr_url, pr_number, pr_status, pr_merged_at as "pr_merged_at: DateTime", created_at as "created_at!: DateTime", updated_at as "updated_at!: DateTime" FROM task_attempts WHERE id = $1"#, id @@ -190,7 +191,7 @@ impl TaskAttempt { ) -> Result, sqlx::Error> { sqlx::query_as!( TaskAttempt, - r#"SELECT id as "id!: Uuid", task_id as "task_id!: Uuid", worktree_path, branch, merge_commit, executor, pr_url, pr_number, pr_status, pr_merged_at as "pr_merged_at: DateTime", created_at as "created_at!: DateTime", updated_at as "updated_at!: DateTime" + r#"SELECT id as "id!: Uuid", task_id as "task_id!: Uuid", worktree_path, branch, base_branch, merge_commit, executor, pr_url, pr_number, pr_status, pr_merged_at as "pr_merged_at: DateTime", created_at as "created_at!: DateTime", updated_at as "updated_at!: DateTime" FROM task_attempts WHERE task_id = $1 ORDER BY created_at DESC"#, @@ -231,6 +232,25 @@ impl TaskAttempt { .await? .ok_or(TaskAttemptError::ProjectNotFound)?; + // Determine the resolved base branch name first + let resolved_base_branch = if let Some(ref base_branch) = data.base_branch { + base_branch.clone() + } else { + // Default to current HEAD branch name or "main" + let repo = Repository::open(&project.git_repo_path)?; + let default_branch = match repo.head() { + Ok(head_ref) => head_ref.shorthand().unwrap_or("main").to_string(), + Err(e) + if e.class() == git2::ErrorClass::Reference + && e.code() == git2::ErrorCode::UnbornBranch => + { + "main".to_string() // Repository has no commits yet + } + Err(_) => "main".to_string(), // Fallback + }; + default_branch + }; + // Solve scoping issues { // Create the worktree using git2 @@ -305,13 +325,14 @@ impl TaskAttempt { // Insert the record into the database Ok(sqlx::query_as!( TaskAttempt, - r#"INSERT INTO task_attempts (id, task_id, worktree_path, branch, merge_commit, executor, pr_url, pr_number, pr_status, pr_merged_at) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) - RETURNING id as "id!: Uuid", task_id as "task_id!: Uuid", worktree_path, branch, merge_commit, executor, pr_url, pr_number, pr_status, pr_merged_at as "pr_merged_at: DateTime", created_at as "created_at!: DateTime", updated_at as "updated_at!: DateTime""#, + r#"INSERT INTO task_attempts (id, task_id, worktree_path, branch, base_branch, merge_commit, executor, pr_url, pr_number, pr_status, pr_merged_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) + RETURNING id as "id!: Uuid", task_id as "task_id!: Uuid", worktree_path, branch, base_branch, merge_commit, executor, pr_url, pr_number, pr_status, pr_merged_at as "pr_merged_at: DateTime", created_at as "created_at!: DateTime", updated_at as "updated_at!: DateTime""#, attempt_id, task_id, worktree_path_str, task_attempt_branch, + resolved_base_branch, Option::::None, // merge_commit is always None during creation data.executor, Option::::None, // pr_url is None during creation @@ -477,7 +498,7 @@ impl TaskAttempt { // Get the task attempt with validation let attempt = sqlx::query_as!( TaskAttempt, - r#"SELECT ta.id as "id!: Uuid", ta.task_id as "task_id!: Uuid", ta.worktree_path, ta.branch, ta.merge_commit, ta.executor, ta.pr_url, ta.pr_number, ta.pr_status, ta.pr_merged_at as "pr_merged_at: DateTime", ta.created_at as "created_at!: DateTime", ta.updated_at as "updated_at!: DateTime" + r#"SELECT ta.id as "id!: Uuid", ta.task_id as "task_id!: Uuid", ta.worktree_path, ta.branch, ta.base_branch, ta.merge_commit, ta.executor, ta.pr_url, ta.pr_number, ta.pr_status, ta.pr_merged_at as "pr_merged_at: DateTime", ta.created_at as "created_at!: DateTime", ta.updated_at as "updated_at!: DateTime" FROM task_attempts ta JOIN tasks t ON ta.task_id = t.id WHERE ta.id = $1 AND t.id = $2 AND t.project_id = $3"#, @@ -1122,7 +1143,7 @@ impl TaskAttempt { // Get the task attempt with validation let attempt = sqlx::query_as!( TaskAttempt, - r#"SELECT ta.id as "id!: Uuid", ta.task_id as "task_id!: Uuid", ta.worktree_path, ta.branch, ta.merge_commit, ta.executor, ta.pr_url, ta.pr_number, ta.pr_status, ta.pr_merged_at as "pr_merged_at: DateTime", ta.created_at as "created_at!: DateTime", ta.updated_at as "updated_at!: DateTime" + r#"SELECT ta.id as "id!: Uuid", ta.task_id as "task_id!: Uuid", ta.worktree_path, ta.branch, ta.base_branch, ta.merge_commit, ta.executor, ta.pr_url, ta.pr_number, ta.pr_status, ta.pr_merged_at as "pr_merged_at: DateTime", ta.created_at as "created_at!: DateTime", ta.updated_at as "updated_at!: DateTime" FROM task_attempts ta JOIN tasks t ON ta.task_id = t.id WHERE ta.id = $1 AND t.id = $2 AND t.project_id = $3"#, @@ -1568,6 +1589,7 @@ impl TaskAttempt { ta.task_id AS "task_id!: Uuid", ta.worktree_path, ta.branch, + ta.base_branch, ta.merge_commit, ta.executor, ta.pr_url, @@ -1608,47 +1630,33 @@ impl TaskAttempt { let attempt_oid = attempt_ref.target().unwrap(); // ── determine the base branch & ahead/behind counts ───────────────────────── - let mut base_branch_name = String::from("main"); // sensible default - let mut commits_ahead: usize = 0; - let mut commits_behind: usize = 0; - let mut best_distance: usize = usize::MAX; + let base_branch_name = attempt.base_branch.clone(); // 1. prefer the branch’s configured upstream, if any if let Ok(local_branch) = main_repo.find_branch(&attempt_branch, BranchType::Local) { if let Ok(upstream) = local_branch.upstream() { - if let Some(name) = upstream.name()? { + if let Some(_name) = upstream.name()? { if let Some(base_oid) = upstream.get().target() { - let (ahead, behind) = + let (_ahead, _behind) = main_repo.graph_ahead_behind(attempt_oid, base_oid)?; - base_branch_name = name.to_owned(); - commits_ahead = ahead; - commits_behind = behind; - best_distance = ahead + behind; + // Ignore upstream since we use stored base branch } } } } - // 2. otherwise, take the branch with the smallest ahead+behind distance - if best_distance == usize::MAX { - for br in main_repo.branches(None)? { - let (br, _) = br?; - let name = br.name()?.unwrap_or_default(); - if name == attempt_branch { - continue; // skip comparing the branch to itself + // Calculate ahead/behind counts using the stored base branch + let (commits_ahead, commits_behind) = + if let Ok(base_branch) = main_repo.find_branch(&base_branch_name, BranchType::Local) { + if let Some(base_oid) = base_branch.get().target() { + main_repo.graph_ahead_behind(attempt_oid, base_oid)? + } else { + (0, 0) // Base branch has no commits } - if let Some(base_oid) = br.get().target() { - let (ahead, behind) = main_repo.graph_ahead_behind(attempt_oid, base_oid)?; - let distance = ahead + behind; - if distance < best_distance { - best_distance = distance; - base_branch_name = name.to_owned(); - commits_ahead = ahead; - commits_behind = behind; - } - } - } - } + } else { + // Base branch doesn't exist, assume no relationship + (0, 0) + }; // ── detect any uncommitted / untracked changes ─────────────────────────────── let repo_for_status = Repository::open(&project.git_repo_path)?; @@ -1687,7 +1695,7 @@ impl TaskAttempt { // Get the task attempt with validation let attempt = sqlx::query_as!( TaskAttempt, - r#"SELECT ta.id as "id!: Uuid", ta.task_id as "task_id!: Uuid", ta.worktree_path, ta.branch, ta.merge_commit, ta.executor, ta.pr_url, ta.pr_number, ta.pr_status, ta.pr_merged_at as "pr_merged_at: DateTime", ta.created_at as "created_at!: DateTime", ta.updated_at as "updated_at!: DateTime" + r#"SELECT ta.id as "id!: Uuid", ta.task_id as "task_id!: Uuid", ta.worktree_path, ta.branch, ta.base_branch, ta.merge_commit, ta.executor, ta.pr_url, ta.pr_number, ta.pr_status, ta.pr_merged_at as "pr_merged_at: DateTime", ta.created_at as "created_at!: DateTime", ta.updated_at as "updated_at!: DateTime" FROM task_attempts ta JOIN tasks t ON ta.task_id = t.id WHERE ta.id = $1 AND t.id = $2 AND t.project_id = $3"#, @@ -1704,11 +1712,14 @@ impl TaskAttempt { .await? .ok_or(TaskAttemptError::ProjectNotFound)?; + // Use the stored base branch if no new base branch is provided + let effective_base_branch = new_base_branch.or_else(|| Some(attempt.base_branch.clone())); + // Perform the git rebase operations (synchronous) let new_base_commit = Self::perform_rebase_operation( &attempt.worktree_path, &project.git_repo_path, - new_base_branch, + effective_base_branch, )?; // No need to update database as we now get base_commit live from git @@ -1726,7 +1737,7 @@ impl TaskAttempt { // Get the task attempt with validation let attempt = sqlx::query_as!( TaskAttempt, - r#"SELECT ta.id as "id!: Uuid", ta.task_id as "task_id!: Uuid", ta.worktree_path, ta.branch, ta.merge_commit, ta.executor, ta.pr_url, ta.pr_number, ta.pr_status, ta.pr_merged_at as "pr_merged_at: DateTime", ta.created_at as "created_at!: DateTime", ta.updated_at as "updated_at!: DateTime" + r#"SELECT ta.id as "id!: Uuid", ta.task_id as "task_id!: Uuid", ta.worktree_path, ta.branch, ta.base_branch, ta.merge_commit, ta.executor, ta.pr_url, ta.pr_number, ta.pr_status, ta.pr_merged_at as "pr_merged_at: DateTime", ta.created_at as "created_at!: DateTime", ta.updated_at as "updated_at!: DateTime" FROM task_attempts ta JOIN tasks t ON ta.task_id = t.id WHERE ta.id = $1 AND t.id = $2 AND t.project_id = $3"#, @@ -1796,7 +1807,7 @@ impl TaskAttempt { // Get the task attempt with validation let attempt = sqlx::query_as!( TaskAttempt, - r#"SELECT ta.id as "id!: Uuid", ta.task_id as "task_id!: Uuid", ta.worktree_path, ta.branch, ta.merge_commit, ta.executor, ta.pr_url, ta.pr_number, ta.pr_status, ta.pr_merged_at as "pr_merged_at: DateTime", ta.created_at as "created_at!: DateTime", ta.updated_at as "updated_at!: DateTime" + r#"SELECT ta.id as "id!: Uuid", ta.task_id as "task_id!: Uuid", ta.worktree_path, ta.branch, ta.base_branch, ta.merge_commit, ta.executor, ta.pr_url, ta.pr_number, ta.pr_status, ta.pr_merged_at as "pr_merged_at: DateTime", ta.created_at as "created_at!: DateTime", ta.updated_at as "updated_at!: DateTime" FROM task_attempts ta JOIN tasks t ON ta.task_id = t.id WHERE ta.id = $1 AND t.id = $2 AND t.project_id = $3"#, @@ -2041,7 +2052,7 @@ impl TaskAttempt { // Get the task attempt with validation let _attempt = sqlx::query_as!( TaskAttempt, - r#"SELECT ta.id as "id!: Uuid", ta.task_id as "task_id!: Uuid", ta.worktree_path, ta.branch, ta.merge_commit, ta.executor, ta.pr_url, ta.pr_number, ta.pr_status, ta.pr_merged_at as "pr_merged_at: DateTime", ta.created_at as "created_at!: DateTime", ta.updated_at as "updated_at!: DateTime" + r#"SELECT ta.id as "id!: Uuid", ta.task_id as "task_id!: Uuid", ta.worktree_path, ta.branch, ta.base_branch, ta.merge_commit, ta.executor, ta.pr_url, ta.pr_number, ta.pr_status, ta.pr_merged_at as "pr_merged_at: DateTime", ta.created_at as "created_at!: DateTime", ta.updated_at as "updated_at!: DateTime" FROM task_attempts ta JOIN tasks t ON ta.task_id = t.id WHERE ta.id = $1 AND t.id = $2 AND t.project_id = $3"#, diff --git a/backend/src/routes/task_attempts.rs b/backend/src/routes/task_attempts.rs index 1d9fb230..44404236 100644 --- a/backend/src/routes/task_attempts.rs +++ b/backend/src/routes/task_attempts.rs @@ -329,10 +329,28 @@ pub async fn create_github_pr( } }; - let base_branch = request - .base_branch - .or(config.github.default_pr_base) - .unwrap_or_else(|| "main".to_string()); + // Get the task attempt to access the stored base branch + let attempt = match TaskAttempt::find_by_id(&app_state.db_pool, attempt_id).await { + Ok(Some(attempt)) => attempt, + Ok(None) => return Err(StatusCode::NOT_FOUND), + Err(e) => { + tracing::error!("Failed to fetch task attempt {}: {}", attempt_id, e); + return Err(StatusCode::INTERNAL_SERVER_ERROR); + } + }; + + let base_branch = request.base_branch.unwrap_or_else(|| { + // Use the stored base branch from the task attempt as the default + // Fall back to config default or "main" only if stored base branch is somehow invalid + if !attempt.base_branch.trim().is_empty() { + attempt.base_branch.clone() + } else { + config + .github + .default_pr_base + .unwrap_or_else(|| "main".to_string()) + } + }); match TaskAttempt::create_github_pr( &app_state.db_pool, diff --git a/frontend/src/components/tasks/TaskDetailsToolbar.tsx b/frontend/src/components/tasks/TaskDetailsToolbar.tsx index 6219be93..bccecb2b 100644 --- a/frontend/src/components/tasks/TaskDetailsToolbar.tsx +++ b/frontend/src/components/tasks/TaskDetailsToolbar.tsx @@ -17,6 +17,13 @@ import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { Label } from '@/components/ui/label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; import { DropdownMenu, DropdownMenuContent, @@ -131,7 +138,9 @@ export function TaskDetailsToolbar({ const [showCreatePRDialog, setShowCreatePRDialog] = useState(false); const [prTitle, setPrTitle] = useState(''); const [prBody, setPrBody] = useState(''); - const [prBaseBranch, setPrBaseBranch] = useState('main'); + const [prBaseBranch, setPrBaseBranch] = useState( + selectedAttempt?.base_branch || 'main' + ); const [error, setError] = useState(null); // Set create attempt mode when there are no attempts @@ -139,6 +148,13 @@ export function TaskDetailsToolbar({ setIsInCreateAttemptMode(taskAttempts.length === 0); }, [taskAttempts.length]); + // Update PR base branch when selected attempt changes + useEffect(() => { + if (selectedAttempt?.base_branch) { + setPrBaseBranch(selectedAttempt.base_branch); + } + }, [selectedAttempt?.base_branch]); + // Branch status fetching const fetchBranchStatus = useCallback(async () => { if (!projectId || !selectedAttempt?.id || !selectedAttempt?.task_id) return; @@ -286,7 +302,7 @@ export function TaskDetailsToolbar({ // Reset form setPrTitle(''); setPrBody(''); - setPrBaseBranch('main'); + setPrBaseBranch(selectedAttempt?.base_branch || 'main'); } else { setError(result.message || 'Failed to create GitHub PR'); } @@ -908,12 +924,28 @@ export function TaskDetailsToolbar({
- setPrBaseBranch(e.target.value)} - placeholder="main" - /> +
diff --git a/shared/types.ts b/shared/types.ts index 18522e27..35c94ddb 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -56,7 +56,7 @@ export type UpdateTask = { title: string | null, description: string | null, sta export type TaskAttemptStatus = "setuprunning" | "setupcomplete" | "setupfailed" | "executorrunning" | "executorcomplete" | "executorfailed"; -export type TaskAttempt = { id: string, task_id: string, worktree_path: string, branch: string, merge_commit: string | null, executor: string | null, pr_url: string | null, pr_number: bigint | null, pr_status: string | null, pr_merged_at: string | null, created_at: string, updated_at: string, }; +export type TaskAttempt = { id: string, task_id: string, worktree_path: string, branch: string, base_branch: string, merge_commit: string | null, executor: string | null, pr_url: string | null, pr_number: bigint | null, pr_status: string | null, pr_merged_at: string | null, created_at: string, updated_at: string, }; export type CreateTaskAttempt = { executor: string | null, base_branch: string | null, };