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,
|
worktree_id: String,
|
||||||
target_branch: String,
|
target_branch: String,
|
||||||
) -> Result<(), AppError> {
|
) -> 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 conn = state.db.lock().map_err(|e| AppError::from(e.to_string()))?;
|
||||||
|
|
||||||
let wt = Worktree::get_by_id(&conn, &worktree_id)?;
|
let wt = Worktree::get_by_id(&conn, &worktree_id)?;
|
||||||
|
|
@ -52,6 +57,15 @@ pub fn apply_fix_to_branch(
|
||||||
|
|
||||||
drop(conn);
|
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(
|
worktree_manager::apply_fix(
|
||||||
&project.path,
|
&project.path,
|
||||||
&project.base_branch,
|
&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)?;
|
let branches = worktree_manager::list_local_branches(&project.path).map_err(AppError::from)?;
|
||||||
Ok(branches)
|
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::apply_fix_to_branch,
|
||||||
commands::worktree::delete_worktree_cmd,
|
commands::worktree::delete_worktree_cmd,
|
||||||
commands::worktree::list_local_branches,
|
commands::worktree::list_local_branches,
|
||||||
|
commands::worktree::list_local_branches_for_worktree,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
deleteWorktreeCmd,
|
deleteWorktreeCmd,
|
||||||
getTicketResult,
|
getTicketResult,
|
||||||
getWorktreeDiff,
|
getWorktreeDiff,
|
||||||
|
listLocalBranchesForWorktree,
|
||||||
retryTicket,
|
retryTicket,
|
||||||
} from "../../lib/api";
|
} from "../../lib/api";
|
||||||
import { getErrorMessage } from "../../lib/errors";
|
import { getErrorMessage } from "../../lib/errors";
|
||||||
|
|
@ -65,11 +66,41 @@ export default function TicketDetail() {
|
||||||
const [worktree, setWorktree] = useState<Worktree | null>(null);
|
const [worktree, setWorktree] = useState<Worktree | null>(null);
|
||||||
const [diff, setDiff] = useState<string | null>(null);
|
const [diff, setDiff] = useState<string | null>(null);
|
||||||
const [targetBranch, setTargetBranch] = useState("");
|
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 [tab, setTab] = useState<"info" | "analyst" | "developer" | "diff">("info");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [isDeleteWorktreeModalOpen, setIsDeleteWorktreeModalOpen] = useState(false);
|
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() {
|
async function loadData() {
|
||||||
if (!ticketId) return;
|
if (!ticketId) return;
|
||||||
try {
|
try {
|
||||||
|
|
@ -81,6 +112,7 @@ export default function TicketDetail() {
|
||||||
else if (result.ticket.analyst_report) setTab("analyst");
|
else if (result.ticket.analyst_report) setTab("analyst");
|
||||||
|
|
||||||
if (result.worktree && result.worktree.status === "Active") {
|
if (result.worktree && result.worktree.status === "Active") {
|
||||||
|
await loadBranchOptions(result.worktree.id);
|
||||||
try {
|
try {
|
||||||
const d = await getWorktreeDiff(result.worktree.id);
|
const d = await getWorktreeDiff(result.worktree.id);
|
||||||
setDiff(d);
|
setDiff(d);
|
||||||
|
|
@ -89,6 +121,11 @@ export default function TicketDetail() {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setDiff(null);
|
setDiff(null);
|
||||||
|
setAvailableBranches([]);
|
||||||
|
setBranchInputMode("select");
|
||||||
|
setTargetBranch("");
|
||||||
|
setBranchesError("");
|
||||||
|
setBranchesLoading(false);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(getErrorMessage(err));
|
setError(getErrorMessage(err));
|
||||||
|
|
@ -124,11 +161,27 @@ export default function TicketDetail() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleApplyFix() {
|
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);
|
setLoading(true);
|
||||||
setError("");
|
setError("");
|
||||||
try {
|
try {
|
||||||
await applyFixToBranch(worktree.id, targetBranch);
|
await applyFixToBranch(worktree.id, normalizedTargetBranch);
|
||||||
await loadData();
|
await loadData();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(getErrorMessage(err));
|
setError(getErrorMessage(err));
|
||||||
|
|
@ -144,6 +197,11 @@ export default function TicketDetail() {
|
||||||
await deleteWorktreeCmd(worktree.id);
|
await deleteWorktreeCmd(worktree.id);
|
||||||
setWorktree(null);
|
setWorktree(null);
|
||||||
setDiff(null);
|
setDiff(null);
|
||||||
|
setAvailableBranches([]);
|
||||||
|
setTargetBranch("");
|
||||||
|
setBranchInputMode("select");
|
||||||
|
setBranchesError("");
|
||||||
|
setBranchesLoading(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(getErrorMessage(err));
|
setError(getErrorMessage(err));
|
||||||
}
|
}
|
||||||
|
|
@ -274,22 +332,67 @@ export default function TicketDetail() {
|
||||||
{worktree && worktree.status === "Active" && (
|
{worktree && worktree.status === "Active" && (
|
||||||
<div className="rounded-lg border border-gray-200 bg-white p-4">
|
<div className="rounded-lg border border-gray-200 bg-white p-4">
|
||||||
<h3 className="mb-3 text-sm font-semibold">Worktree Actions</h3>
|
<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">
|
<div className="mb-3 flex items-center gap-2">
|
||||||
<input
|
{branchInputMode === "select" && availableBranches.length > 0 ? (
|
||||||
type="text"
|
<select
|
||||||
placeholder="Target branch (e.g. feature/login)"
|
value={targetBranch}
|
||||||
value={targetBranch}
|
onChange={(e) => setTargetBranch(e.target.value)}
|
||||||
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"
|
||||||
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"
|
>
|
||||||
/>
|
{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
|
<button
|
||||||
onClick={handleApplyFix}
|
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"
|
className="rounded bg-green-600 px-3 py-1.5 text-sm text-white hover:bg-green-700 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
Apply fix
|
Apply fix
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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
|
<button
|
||||||
onClick={() => setIsDeleteWorktreeModalOpen(true)}
|
onClick={() => setIsDeleteWorktreeModalOpen(true)}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
|
|
|
||||||
|
|
@ -194,6 +194,9 @@ export async function deleteWorktreeCmd(worktreeId: string): Promise<void> {
|
||||||
export async function listLocalBranches(projectId: string): Promise<string[]> {
|
export async function listLocalBranches(projectId: string): Promise<string[]> {
|
||||||
return invoke("list_local_branches", { projectId });
|
return invoke("list_local_branches", { projectId });
|
||||||
}
|
}
|
||||||
|
export async function listLocalBranchesForWorktree(worktreeId: string): Promise<string[]> {
|
||||||
|
return invoke("list_local_branches_for_worktree", { worktreeId });
|
||||||
|
}
|
||||||
|
|
||||||
// Notifications
|
// Notifications
|
||||||
export async function listNotifications(
|
export async function listNotifications(
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue