diff --git a/src-tauri/src/commands/worktree.rs b/src-tauri/src/commands/worktree.rs index 5b14796..9d81297 100644 --- a/src-tauri/src/commands/worktree.rs +++ b/src-tauri/src/commands/worktree.rs @@ -43,6 +43,11 @@ pub fn apply_fix_to_branch( worktree_id: String, target_branch: String, ) -> Result<(), AppError> { + let target_branch = target_branch.trim().to_string(); + if target_branch.is_empty() { + return Err(AppError::from("Target branch is required".to_string())); + } + let conn = state.db.lock().map_err(|e| AppError::from(e.to_string()))?; let wt = Worktree::get_by_id(&conn, &worktree_id)?; @@ -52,6 +57,15 @@ pub fn apply_fix_to_branch( drop(conn); + let local_branches = + worktree_manager::list_local_branches(&project.path).map_err(AppError::from)?; + if !local_branches.iter().any(|branch| branch == &target_branch) { + return Err(AppError::from(format!( + "Target branch '{}' does not exist locally", + target_branch + ))); + } + worktree_manager::apply_fix( &project.path, &project.base_branch, @@ -102,3 +116,23 @@ pub fn list_local_branches( let branches = worktree_manager::list_local_branches(&project.path).map_err(AppError::from)?; Ok(branches) } + +#[tauri::command] +pub fn list_local_branches_for_worktree( + state: State<'_, AppState>, + worktree_id: String, +) -> Result, AppError> { + let conn = state.db.lock().map_err(|e| AppError::from(e.to_string()))?; + + let wt = Worktree::get_by_id(&conn, &worktree_id)?; + let ticket = ProcessedTicket::get_by_id(&conn, &wt.ticket_id)?; + let tracker = WatchedTracker::get_by_id(&conn, &ticket.tracker_id)?; + let project = Project::get_by_id(&conn, &tracker.project_id)?; + + drop(conn); + + let mut branches = + worktree_manager::list_local_branches(&project.path).map_err(AppError::from)?; + branches.sort(); + Ok(branches) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 03fc005..c631510 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -106,6 +106,7 @@ pub fn run() { commands::worktree::apply_fix_to_branch, commands::worktree::delete_worktree_cmd, commands::worktree::list_local_branches, + commands::worktree::list_local_branches_for_worktree, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/components/tickets/TicketDetail.tsx b/src/components/tickets/TicketDetail.tsx index 6d086fb..f33402c 100644 --- a/src/components/tickets/TicketDetail.tsx +++ b/src/components/tickets/TicketDetail.tsx @@ -8,6 +8,7 @@ import { deleteWorktreeCmd, getTicketResult, getWorktreeDiff, + listLocalBranchesForWorktree, retryTicket, } from "../../lib/api"; import { getErrorMessage } from "../../lib/errors"; @@ -65,11 +66,41 @@ export default function TicketDetail() { const [worktree, setWorktree] = useState(null); const [diff, setDiff] = useState(null); const [targetBranch, setTargetBranch] = useState(""); + const [availableBranches, setAvailableBranches] = useState([]); + const [branchInputMode, setBranchInputMode] = useState<"select" | "manual">("select"); + const [branchesLoading, setBranchesLoading] = useState(false); + const [branchesError, setBranchesError] = useState(""); const [tab, setTab] = useState<"info" | "analyst" | "developer" | "diff">("info"); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); const [isDeleteWorktreeModalOpen, setIsDeleteWorktreeModalOpen] = useState(false); + async function loadBranchOptions(worktreeId: string) { + setBranchesLoading(true); + setBranchesError(""); + try { + const branches = await listLocalBranchesForWorktree(worktreeId); + setAvailableBranches(branches); + + if (branches.length > 0) { + setBranchInputMode("select"); + setTargetBranch((prev) => { + const trimmedPrev = prev.trim(); + if (trimmedPrev && branches.includes(trimmedPrev)) return trimmedPrev; + return branches[0]; + }); + } else { + setBranchInputMode("manual"); + } + } catch (err) { + setAvailableBranches([]); + setBranchInputMode("manual"); + setBranchesError(getErrorMessage(err)); + } finally { + setBranchesLoading(false); + } + } + async function loadData() { if (!ticketId) return; try { @@ -81,6 +112,7 @@ export default function TicketDetail() { else if (result.ticket.analyst_report) setTab("analyst"); if (result.worktree && result.worktree.status === "Active") { + await loadBranchOptions(result.worktree.id); try { const d = await getWorktreeDiff(result.worktree.id); setDiff(d); @@ -89,6 +121,11 @@ export default function TicketDetail() { } } else { setDiff(null); + setAvailableBranches([]); + setBranchInputMode("select"); + setTargetBranch(""); + setBranchesError(""); + setBranchesLoading(false); } } catch (err) { setError(getErrorMessage(err)); @@ -124,11 +161,27 @@ export default function TicketDetail() { } async function handleApplyFix() { - if (!worktree || !targetBranch) return; + if (!worktree) return; + const normalizedTargetBranch = targetBranch.trim(); + + if (!normalizedTargetBranch) { + setError("Veuillez sélectionner ou saisir une branche cible."); + return; + } + if ( + availableBranches.length > 0 && + !availableBranches.includes(normalizedTargetBranch) + ) { + setError( + `La branche cible "${normalizedTargetBranch}" n'existe pas localement.` + ); + return; + } + setLoading(true); setError(""); try { - await applyFixToBranch(worktree.id, targetBranch); + await applyFixToBranch(worktree.id, normalizedTargetBranch); await loadData(); } catch (err) { setError(getErrorMessage(err)); @@ -144,6 +197,11 @@ export default function TicketDetail() { await deleteWorktreeCmd(worktree.id); setWorktree(null); setDiff(null); + setAvailableBranches([]); + setTargetBranch(""); + setBranchInputMode("select"); + setBranchesError(""); + setBranchesLoading(false); } catch (err) { setError(getErrorMessage(err)); } @@ -274,22 +332,67 @@ export default function TicketDetail() { {worktree && worktree.status === "Active" && (

Worktree Actions

+
+

Target branch

+ {availableBranches.length > 0 && ( + + )} +
+
- setTargetBranch(e.target.value)} - className="flex-1 rounded border border-gray-300 px-3 py-1.5 text-sm focus:border-transparent focus:ring-2 focus:ring-blue-500" - /> + {branchInputMode === "select" && availableBranches.length > 0 ? ( + + ) : ( + setTargetBranch(e.target.value)} + className="flex-1 rounded border border-gray-300 px-3 py-1.5 text-sm focus:border-transparent focus:ring-2 focus:ring-blue-500" + /> + )}
+ {branchesLoading &&

Loading local branches...

} + {!branchesLoading && branchesError && ( +

+ Could not preload branches. Manual input is still available. +

+ )} + {!branchesLoading && + !branchesError && + availableBranches.length === 0 && + branchInputMode === "manual" && ( +

+ No local branches detected. Enter the target branch manually. +

+ )}