parent
0a2e7daec9
commit
45c51730ec
4 changed files with 151 additions and 10 deletions
|
|
@ -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<Vec<String>, 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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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<Worktree | null>(null);
|
||||
const [diff, setDiff] = useState<string | null>(null);
|
||||
const [targetBranch, setTargetBranch] = useState("");
|
||||
const [availableBranches, setAvailableBranches] = useState<string[]>([]);
|
||||
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" && (
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-4">
|
||||
<h3 className="mb-3 text-sm font-semibold">Worktree Actions</h3>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<p className="text-xs text-gray-500">Target branch</p>
|
||||
{availableBranches.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setBranchInputMode((mode) => (mode === "select" ? "manual" : "select"))
|
||||
}
|
||||
className="text-xs text-blue-600 hover:underline"
|
||||
>
|
||||
{branchInputMode === "select"
|
||||
? "Use manual input"
|
||||
: "Choose existing branch"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mb-3 flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Target branch (e.g. feature/login)"
|
||||
value={targetBranch}
|
||||
onChange={(e) => 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 ? (
|
||||
<select
|
||||
value={targetBranch}
|
||||
onChange={(e) => setTargetBranch(e.target.value)}
|
||||
className="flex-1 rounded border border-gray-300 bg-white px-3 py-1.5 text-sm focus:border-transparent focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
{availableBranches.map((branch) => (
|
||||
<option key={branch} value={branch}>
|
||||
{branch}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Target branch (e.g. feature/login)"
|
||||
value={targetBranch}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
onClick={handleApplyFix}
|
||||
disabled={loading || !targetBranch}
|
||||
disabled={loading || branchesLoading || !targetBranch.trim()}
|
||||
className="rounded bg-green-600 px-3 py-1.5 text-sm text-white hover:bg-green-700 disabled:opacity-50"
|
||||
>
|
||||
Apply fix
|
||||
</button>
|
||||
</div>
|
||||
{branchesLoading && <p className="mb-3 text-xs text-gray-500">Loading local branches...</p>}
|
||||
{!branchesLoading && branchesError && (
|
||||
<p className="mb-3 text-xs text-amber-700">
|
||||
Could not preload branches. Manual input is still available.
|
||||
</p>
|
||||
)}
|
||||
{!branchesLoading &&
|
||||
!branchesError &&
|
||||
availableBranches.length === 0 &&
|
||||
branchInputMode === "manual" && (
|
||||
<p className="mb-3 text-xs text-gray-500">
|
||||
No local branches detected. Enter the target branch manually.
|
||||
</p>
|
||||
)}
|
||||
<button
|
||||
onClick={() => setIsDeleteWorktreeModalOpen(true)}
|
||||
disabled={loading}
|
||||
|
|
|
|||
|
|
@ -194,6 +194,9 @@ export async function deleteWorktreeCmd(worktreeId: string): Promise<void> {
|
|||
export async function listLocalBranches(projectId: string): Promise<string[]> {
|
||||
return invoke("list_local_branches", { projectId });
|
||||
}
|
||||
export async function listLocalBranchesForWorktree(worktreeId: string): Promise<string[]> {
|
||||
return invoke("list_local_branches_for_worktree", { worktreeId });
|
||||
}
|
||||
|
||||
// Notifications
|
||||
export async function listNotifications(
|
||||
|
|
|
|||
Loading…
Reference in a new issue