diff --git a/apps/desktop/dev-server.mjs b/apps/desktop/dev-server.mjs index 9be99ec8..59ebef09 100644 --- a/apps/desktop/dev-server.mjs +++ b/apps/desktop/dev-server.mjs @@ -840,7 +840,7 @@ async function handleRequest(req, res) { return jsonResponse(req, res, { branch, remote, ahead, behind, mainCommitCount, pushRemote, aheadPush, staged, unstaged, untracked, conflicted }); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -943,7 +943,7 @@ async function handleRequest(req, res) { return jsonResponse(req, res, { path, hunks }); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -1019,7 +1019,7 @@ async function handleRequest(req, res) { return jsonResponse(req, res, entries.filter((e) => !e.message.startsWith("index on ") && !e.message.startsWith("untracked files on "))); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -1075,7 +1075,7 @@ async function handleRequest(req, res) { }); return jsonResponse(req, res, { ok: true }); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -1092,7 +1092,7 @@ async function handleRequest(req, res) { }); return jsonResponse(req, res, { ok: true }); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -1112,7 +1112,7 @@ async function handleRequest(req, res) { }).trim(); return jsonResponse(req, res, { hash }); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -1132,7 +1132,7 @@ async function handleRequest(req, res) { }).trim(); return jsonResponse(req, res, { hash }); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -1523,7 +1523,7 @@ async function handleRequest(req, res) { } return jsonResponse(req, res, { state: "clean", hasConflict: false, operationHead: null, targetBranch: null, step: 0, total: 0 }); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -1660,7 +1660,7 @@ async function handleRequest(req, res) { } return jsonResponse(req, res, { ok: true }); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -1767,7 +1767,7 @@ async function handleRequest(req, res) { return jsonResponse(req, res, diffs); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -1851,7 +1851,7 @@ async function handleRequest(req, res) { return jsonResponse(req, res, branches); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -1882,7 +1882,7 @@ async function handleRequest(req, res) { execSync(`git checkout "${name}"`, { cwd: resolvedCwd, encoding: "utf-8", shell: true }); return jsonResponse(req, res, { ok: true }); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -1896,7 +1896,7 @@ async function handleRequest(req, res) { execSync(`git branch ${flag} "${name}"`, { cwd: resolvedCwd, encoding: "utf-8", shell: true }); return jsonResponse(req, res, { ok: true }); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -1909,7 +1909,7 @@ async function handleRequest(req, res) { execFileSync("git", ["push", remote, "--delete", name], { cwd: resolvedCwd, encoding: "utf-8" }); return jsonResponse(req, res, { ok: true }); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -1922,7 +1922,7 @@ async function handleRequest(req, res) { execSync(`git branch -m "${oldName}" "${newName}"`, { cwd: resolvedCwd, encoding: "utf-8", shell: true }); return jsonResponse(req, res, { ok: true }); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -1940,7 +1940,7 @@ async function handleRequest(req, res) { execFileSync("git", args, { cwd: resolvedCwd, encoding: "utf-8" }); return jsonResponse(req, res, { ok: true }); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -1953,7 +1953,7 @@ async function handleRequest(req, res) { execSync("git stash pop", { cwd: resolvedCwd, encoding: "utf-8", shell: true }); return jsonResponse(req, res, { ok: true }); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -2005,7 +2005,7 @@ async function handleRequest(req, res) { }); return jsonResponse(req, res, entries); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -2023,7 +2023,7 @@ async function handleRequest(req, res) { }); return jsonResponse(req, res, { ok: true }); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -2041,7 +2041,7 @@ async function handleRequest(req, res) { }); return jsonResponse(req, res, { ok: true }); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -2059,7 +2059,7 @@ async function handleRequest(req, res) { }); return jsonResponse(req, res, { ok: true }); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -2079,7 +2079,7 @@ async function handleRequest(req, res) { ); return jsonResponse(req, res, { diff: out }); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -2119,7 +2119,7 @@ async function handleRequest(req, res) { } return jsonResponse(req, res, blameLines); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -2136,7 +2136,7 @@ async function handleRequest(req, res) { { cwd: resolvedCwd, encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 }); return jsonResponse(req, res, parseFileLog(out.stdout || "")); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -2153,7 +2153,7 @@ async function handleRequest(req, res) { { cwd: resolve(cwd), encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 }); return jsonResponse(req, res, parseFileLog(out.stdout || "")); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -2171,7 +2171,7 @@ async function handleRequest(req, res) { { cwd: resolve(cwd), encoding: "utf-8", maxBuffer: 10 * 1024 * 1024 }); return jsonResponse(req, res, parseFileLog(out.stdout || "")); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -2191,7 +2191,7 @@ async function handleRequest(req, res) { return jsonResponse(req, res, data.login ?? ""); } catch (err) { console.error("[gh-current-user]", err.message); - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -2243,7 +2243,7 @@ async function handleRequest(req, res) { return jsonResponse(req, res, prs); } catch (err) { console.error("[gh-list-prs]", err.message); - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -2401,7 +2401,7 @@ async function handleRequest(req, res) { }); } catch (err) { console.error("[gh-create-pr]", err.message); - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -2438,7 +2438,7 @@ async function handleRequest(req, res) { return jsonResponse(req, res, all); } catch (err) { console.error("[gh-reviewer-candidates]", err.message); - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -2479,7 +2479,7 @@ async function handleRequest(req, res) { checks_status: "", }); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -2498,7 +2498,7 @@ async function handleRequest(req, res) { const diff = await resp.text(); return jsonResponse(req, res, { diff }); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -2529,7 +2529,7 @@ async function handleRequest(req, res) { details_url: c.html_url ?? "", }))); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -2566,7 +2566,7 @@ async function handleRequest(req, res) { url: c.html_url ?? "", }))); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -2613,7 +2613,7 @@ async function handleRequest(req, res) { in_reply_to_id: c.in_reply_to_id ?? null, diff_hunk: c.diff_hunk ?? "", url: c.html_url ?? "", }); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -2643,7 +2643,7 @@ async function handleRequest(req, res) { const c = await resp.json(); return jsonResponse(req, res, { id: c.id, body: c.body, updated_at: c.updated_at }); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -2668,7 +2668,7 @@ async function handleRequest(req, res) { if (!resp.ok && resp.status !== 204) return jsonResponse(req, res, { error: `GitHub API ${resp.status}` }, 500); return jsonResponse(req, res, { ok: true }); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -2689,7 +2689,7 @@ async function handleRequest(req, res) { const reviews = await resp.json(); return jsonResponse(req, res, reviews); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -2746,7 +2746,7 @@ async function handleRequest(req, res) { const review = await resp.json(); return jsonResponse(req, res, review); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -2856,7 +2856,7 @@ async function handleRequest(req, res) { : `✅ Pas de conflit détecté`, }); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -2901,7 +2901,7 @@ async function handleRequest(req, res) { }); return jsonResponse(req, res, hotspots); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -2915,7 +2915,7 @@ async function handleRequest(req, res) { const count = (r.stdout || "").trim().split("\n").filter(Boolean).length; return jsonResponse(req, res, { count }); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -2955,7 +2955,7 @@ async function handleRequest(req, res) { } return jsonResponse(req, res, result); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -3041,7 +3041,7 @@ async function handleRequest(req, res) { detail, }); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -3066,7 +3066,7 @@ async function handleRequest(req, res) { } return res.writeHead(200, { ...corsHeaders(req), "Content-Type": "text/plain" }).end(r.stdout); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -3133,7 +3133,7 @@ async function handleRequest(req, res) { detail, }); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -3156,7 +3156,7 @@ async function handleRequest(req, res) { } return res.writeHead(200, { ...corsHeaders(req), "Content-Type": "text/plain" }).end(r.stdout); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -3193,7 +3193,7 @@ async function handleRequest(req, res) { } return jsonResponse(req, res, { ok: true }); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -3215,7 +3215,7 @@ async function handleRequest(req, res) { } return jsonResponse(req, res, { ok: true }); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -3289,7 +3289,7 @@ async function handleRequest(req, res) { return jsonResponse(req, res, { ok: true }); } catch (err) { console.error("[rebase] Error:", err); - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -3313,7 +3313,7 @@ async function handleRequest(req, res) { exitCode: r.status ?? -1, }); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -3350,7 +3350,7 @@ async function handleRequest(req, res) { // Not found — return empty string (same as Rust backend) return res.writeHead(200, { "Content-Type": "text/plain" }).end(""); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -3380,7 +3380,7 @@ async function handleRequest(req, res) { writeFileSync(target, content, "utf-8"); return jsonResponse(req, res, { ok: true }); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -3424,7 +3424,7 @@ async function handleRequest(req, res) { }); return jsonResponse(req, res, entries); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -3442,7 +3442,7 @@ async function handleRequest(req, res) { else if (!enabled && existsSync(enabledPath)) renameSync(enabledPath, disabledPath); return jsonResponse(req, res, {}); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -3459,7 +3459,7 @@ async function handleRequest(req, res) { writeFileSync(join(hooksDir, name), script, { mode: 0o755 }); return jsonResponse(req, res, {}); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -3477,7 +3477,7 @@ async function handleRequest(req, res) { if (existsSync(disabledPath)) unlinkSync(disabledPath); return jsonResponse(req, res, {}); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -3491,7 +3491,7 @@ async function handleRequest(req, res) { if (!existsSync(file)) return jsonResponse(req, res, { error: "No workspace file found" }, 404); return jsonResponse(req, res, JSON.parse(readFileSync(file, "utf-8"))); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -3503,7 +3503,7 @@ async function handleRequest(req, res) { writeFileSync(file, JSON.stringify(workspace, null, 2)); return jsonResponse(req, res, {}); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -3527,7 +3527,7 @@ async function handleRequest(req, res) { }); return jsonResponse(req, res, statuses); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -3555,7 +3555,7 @@ async function handleRequest(req, res) { }); return jsonResponse(req, res, statuses); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -3582,7 +3582,7 @@ async function handleRequest(req, res) { }); return jsonResponse(req, res, statuses); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -3659,7 +3659,7 @@ async function handleRequest(req, res) { }); return jsonResponse(req, res, items); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -3715,7 +3715,7 @@ async function handleRequest(req, res) { }); return jsonResponse(req, res, results); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -3753,7 +3753,7 @@ async function handleRequest(req, res) { }); return jsonResponse(req, res, results); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -3776,23 +3776,27 @@ async function handleRequest(req, res) { } if (cur) worktrees.push(cur); + const CONFLICT_CODES = new Set(["UU", "AA", "DD", "AU", "UA", "DU", "UD"]); const statuses = worktrees.map(wt => { try { const branch = execSync("git rev-parse --abbrev-ref HEAD", { cwd: wt.path, encoding: "utf-8" }).trim(); - let ahead = 0, behind = 0; + let ahead = 0, behind = 0, has_upstream = false; try { const ab = execSync("git rev-list --left-right --count HEAD...@{upstream}", { cwd: wt.path, encoding: "utf-8" }).trim().split(/\s+/); ahead = parseInt(ab[0]) || 0; behind = parseInt(ab[1]) || 0; + has_upstream = true; } catch {} - const modified = execSync("git status --porcelain --untracked-files=no", { cwd: wt.path, encoding: "utf-8" }).trim().split("\n").filter(Boolean).length; - return { path: wt.path, name: wt.branch || branch, branch, ahead, behind, modified, error: null }; + const statusLines = execSync("git status --porcelain --untracked-files=no", { cwd: wt.path, encoding: "utf-8" }).trim().split("\n").filter(Boolean); + const conflicted = statusLines.filter(l => l.length >= 2 && CONFLICT_CODES.has(l.slice(0, 2))).length; + const modified = statusLines.filter(l => l.length >= 2 && !CONFLICT_CODES.has(l.slice(0, 2))).length; + return { path: wt.path, name: wt.branch || branch, branch, ahead, behind, has_upstream, modified, conflicted, error: null }; } catch (e) { - return { path: wt.path, name: wt.branch || "", branch: "", ahead: 0, behind: 0, modified: 0, error: e.message }; + return { path: wt.path, name: wt.branch || "", branch: "", ahead: 0, behind: 0, has_upstream: false, modified: 0, conflicted: 0, error: e.message }; } }); return jsonResponse(req, res, statuses); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -3805,26 +3809,34 @@ async function handleRequest(req, res) { const raw = execSync("git worktree list --porcelain", { cwd, encoding: "utf-8" }); const entries = []; let current = null; - let isFirst = true; for (const line of raw.split("\n")) { if (line.startsWith("worktree ")) { if (current) entries.push(current); - current = { path: line.slice("worktree ".length), branch: "", head: "", is_main: isFirst, is_locked: false, is_bare: false }; - isFirst = false; + current = { path: line.slice("worktree ".length), branch: "", head: "", is_main: false, is_locked: false, lock_reason: null, is_bare: false, is_prunable: false, prunable_reason: null }; } else if (current) { - if (line.startsWith("HEAD ")) current.head = line.slice("HEAD ".length); + if (line === "main") current.is_main = true; + else if (line.startsWith("HEAD ")) current.head = line.slice("HEAD ".length); else if (line.startsWith("branch ")) { const full = line.slice("branch ".length); current.branch = full.startsWith("refs/heads/") ? full.slice("refs/heads/".length) : full; } else if (line === "bare") current.is_bare = true; - else if (line.startsWith("locked")) current.is_locked = true; - else if (line === "detached") current.branch = "(detached HEAD)"; + else if (line.startsWith("locked")) { + current.is_locked = true; + const reason = line.slice("locked".length).trim(); + if (reason) current.lock_reason = reason; + } else if (line.startsWith("prunable")) { + current.is_prunable = true; + const reason = line.slice("prunable".length).trim(); + if (reason) current.prunable_reason = reason; + } else if (line === "detached") current.branch = "(detached HEAD)"; } } if (current) entries.push(current); + // Fallback git < 2.36 : marquer le premier comme main si aucun ne l'est + if (entries.length && entries.every(e => !e.is_main)) entries[0].is_main = true; return jsonResponse(req, res, entries); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -3833,14 +3845,23 @@ async function handleRequest(req, res) { try { const { cwd, path: wtPath, branch, new_branch } = await readBody(req); const resolvedCwd = resolve(cwd); + + // Ensure parent directories exist + const parentDir = dirname(wtPath); + if (!existsSync(parentDir)) { + mkdirSync(parentDir, { recursive: true }); + } + let cmd = `git worktree add "${wtPath}"`; if (new_branch) cmd += ` -b "${new_branch}" "${branch}"`; else cmd += ` "${branch}"`; execSync(cmd, { cwd: resolvedCwd, encoding: "utf-8", shell: true }); const resolvedBranch = new_branch || branch; - return jsonResponse(req, res, { path: wtPath, branch: resolvedBranch, head: "", is_main: false, is_locked: false, is_bare: false }); + let head = ""; + try { head = execSync("git rev-parse HEAD", { cwd: wtPath, encoding: "utf-8" }).trim(); } catch {} + return jsonResponse(req, res, { path: wtPath, branch: resolvedBranch, head, is_main: false, is_locked: false, lock_reason: null, is_bare: false, is_prunable: false, prunable_reason: null }); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -3853,7 +3874,7 @@ async function handleRequest(req, res) { execSync(`git worktree remove ${forceFlag}"${wtPath}"`, { cwd: resolvedCwd, encoding: "utf-8", shell: true }); return jsonResponse(req, res, {}); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -3864,7 +3885,19 @@ async function handleRequest(req, res) { execSync("git worktree prune", { cwd: resolve(cwd), encoding: "utf-8" }); return jsonResponse(req, res, {}); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); + } + } + + // POST /api/git-worktree-repair { cwd, paths? } + if (url.pathname === "/api/git-worktree-repair" && req.method === "POST") { + try { + const { cwd, paths = [] } = await readBody(req); + const extraPaths = paths.map(p => `"${p}"`).join(" "); + execSync(`git worktree repair ${extraPaths}`.trim(), { cwd: resolve(cwd), encoding: "utf-8", shell: true }); + return jsonResponse(req, res, {}); + } catch (err) { + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -3916,7 +3949,7 @@ async function handleRequest(req, res) { } return jsonResponse(req, res, sessions); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -3928,7 +3961,7 @@ async function handleRequest(req, res) { spawn(binary, [], { cwd: resolve(cwd), detached: true, stdio: "ignore" }).unref(); return jsonResponse(req, res, {}); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -3977,7 +4010,7 @@ async function handleRequest(req, res) { } return jsonResponse(req, res, entries); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -3988,7 +4021,7 @@ async function handleRequest(req, res) { execSync("git submodule init", { cwd: resolve(cwd), encoding: "utf-8" }); return jsonResponse(req, res, {}); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -4002,7 +4035,7 @@ async function handleRequest(req, res) { execSync(cmd, { cwd: resolve(cwd), encoding: "utf-8", shell: true }); return jsonResponse(req, res, {}); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -4013,7 +4046,7 @@ async function handleRequest(req, res) { execSync(`git submodule add "${smUrl}" "${smPath}"`, { cwd: resolve(cwd), encoding: "utf-8", shell: true }); return jsonResponse(req, res, {}); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -4145,7 +4178,7 @@ async function handleRequest(req, res) { } return jsonResponse(req, res, { name, url: remoteUrl, provider, owner, repo }); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -4208,7 +4241,7 @@ async function handleRequest(req, res) { conflicts: hasConflicts, }); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -4263,7 +4296,7 @@ async function handleRequest(req, res) { }).filter(Boolean); return jsonResponse(req, res, tags); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -4291,7 +4324,7 @@ async function handleRequest(req, res) { const unpushed = [...localTags].filter(t => !remoteTags.has(t)).sort(); return jsonResponse(req, res, unpushed); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -4431,7 +4464,7 @@ async function handleRequest(req, res) { return jsonResponse(req, res, JSON.parse(raw.trim() || "[]")); } catch (err) { console.error("[pr-files]", err.message); - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -4448,7 +4481,7 @@ async function handleRequest(req, res) { if (result.status !== 0) return jsonResponse(req, res, { sha: "" }); return jsonResponse(req, res, { sha: result.stdout.trim() }); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } @@ -4476,7 +4509,7 @@ async function handleRequest(req, res) { .filter((b) => b && b !== defaultBranch && b !== current); return jsonResponse(req, res, merged); } catch (err) { - return jsonResponse(req, res, { error: err.message }, 500); + return jsonResponse(req, res, { error: err.stderr?.toString() || err.message }, 500); } } diff --git a/apps/desktop/src-tauri/src/commands/ops.rs b/apps/desktop/src-tauri/src/commands/ops.rs index a508ab9e..44e0c3ce 100644 --- a/apps/desktop/src-tauri/src/commands/ops.rs +++ b/apps/desktop/src-tauri/src/commands/ops.rs @@ -1610,7 +1610,6 @@ pub(crate) fn git_worktree_list(cwd: String) -> Result, Strin let stdout = String::from_utf8_lossy(&output.stdout); let mut entries: Vec = Vec::new(); let mut current: Option = None; - let mut is_first = true; for line in stdout.lines() { if line.starts_with("worktree ") { @@ -1622,13 +1621,18 @@ pub(crate) fn git_worktree_list(cwd: String) -> Result, Strin path, branch: String::new(), head: String::new(), - is_main: is_first, + is_main: false, is_locked: false, + lock_reason: None, is_bare: false, + is_prunable: false, + prunable_reason: None, }); - is_first = false; } else if let Some(ref mut e) = current { - if line.starts_with("HEAD ") { + if line == "main" { + // Attribut explicite depuis git 2.36 + e.is_main = true; + } else if line.starts_with("HEAD ") { e.head = line["HEAD ".len()..].to_string(); } else if line.starts_with("branch ") { let full = &line["branch ".len()..]; @@ -1637,15 +1641,32 @@ pub(crate) fn git_worktree_list(cwd: String) -> Result, Strin e.is_bare = true; } else if line.starts_with("locked") { e.is_locked = true; + // Format : "locked" seul ou "locked " avec raison inline + let reason = line["locked".len()..].trim(); + if !reason.is_empty() { + e.lock_reason = Some(reason.to_string()); + } + } else if line.starts_with("prunable") { + e.is_prunable = true; + let reason = line["prunable".len()..].trim(); + if !reason.is_empty() { + e.prunable_reason = Some(reason.to_string()); + } } else if line == "detached" { e.branch = "(detached HEAD)".to_string(); } } } - if let Some(e) = current { + if let Some(e) = current.take() { entries.push(e); } + // Fallback pour git < 2.36 : l'attribut "main" n'existait pas. + // Si aucune entrée n'est marquée is_main, on marque la première. + if !entries.is_empty() && entries.iter().all(|e| !e.is_main) { + entries[0].is_main = true; + } + Ok(entries) } @@ -1656,6 +1677,15 @@ pub(crate) fn git_worktree_add( branch: String, new_branch: Option, ) -> Result { + // Note, create folders if they dont exist. + let target_path = std::path::Path::new(&path); + if let Some(parent) = target_path.parent() { + if !parent.exists() { + std::fs::create_dir_all(parent) + .map_err(|e| format!("Failed to create worktree base directory: {}", e))?; + } + } + let mut cmd = git_cmd(); cmd.arg("worktree").arg("add").arg(&path); @@ -1678,13 +1708,28 @@ pub(crate) fn git_worktree_add( } let resolved_branch = new_branch.as_deref().unwrap_or(&branch).to_string(); + + // Récupérer le SHA HEAD réel depuis le nouveau worktree + let head = git_cmd() + .args(["rev-parse", "HEAD"]) + .current_dir(&path) + .output() + .ok() + .filter(|o| o.status.success()) + .and_then(|o| String::from_utf8(o.stdout).ok()) + .map(|s| s.trim().to_string()) + .unwrap_or_default(); + Ok(WorktreeEntry { path, branch: resolved_branch, - head: String::new(), + head, is_main: false, is_locked: false, + lock_reason: None, is_bare: false, + is_prunable: false, + prunable_reason: None, }) } @@ -1746,12 +1791,16 @@ pub(crate) fn git_worktree_status_all(cwd: String) -> Result = s.trim().split_whitespace().collect(); @@ -1761,22 +1810,53 @@ pub(crate) fn git_worktree_status_all(cwd: String) -> Result= 2 && CONFLICT_CODES.contains(&&l[..2])) + .count() as u32; + let modified = status_out + .lines() + .filter(|l| l.len() >= 2 && !CONFLICT_CODES.contains(&&l[..2])) + .count() as u32; - WorkspaceRepoStatus { path, name, branch, ahead, behind, modified, error: None } + WorkspaceRepoStatus { path, name, branch, ahead, behind, has_upstream, modified, conflicted, error: None } }).collect(); Ok(statuses) } +#[tauri::command] +pub(crate) fn git_worktree_repair(cwd: String, paths: Vec) -> Result<(), String> { + let mut cmd = git_cmd(); + cmd.args(["worktree", "repair"]); + for p in &paths { + cmd.arg(p); + } + let output = cmd + .current_dir(&cwd) + .output() + .map_err(|e| format!("Failed to repair worktrees: {}", e))?; + + if !output.status.success() { + return Err(format!( + "git worktree repair failed: {}", + String::from_utf8_lossy(&output.stderr) + )); + } + Ok(()) +} + // ─── Git clone / fork ───────────────────────────────────────── // ─── Clone progress helpers ────────────────────────────────── diff --git a/apps/desktop/src-tauri/src/commands/workspace.rs b/apps/desktop/src-tauri/src/commands/workspace.rs index d930dd54..6c5a81e6 100644 --- a/apps/desktop/src-tauri/src/commands/workspace.rs +++ b/apps/desktop/src-tauri/src/commands/workspace.rs @@ -53,10 +53,20 @@ pub(crate) fn workspace_status_all(repos: Vec) -> Vec, } @@ -673,7 +675,10 @@ pub struct WorktreeEntry { pub head: String, pub is_main: bool, pub is_locked: bool, + pub lock_reason: Option, pub is_bare: bool, + pub is_prunable: bool, + pub prunable_reason: Option, } // ─── Agent session types ────────────────────────────────────────── diff --git a/apps/desktop/src/App.vue b/apps/desktop/src/App.vue index 7fed8e4c..055cd429 100644 --- a/apps/desktop/src/App.vue +++ b/apps/desktop/src/App.vue @@ -219,6 +219,7 @@ const { applyStash: applyStashRepo, popStash: popStashRepo, dropStash, + worktreeBranches, } = useGitRepo(); function switchToChangesWithFirstFile() { @@ -2189,7 +2190,7 @@ onUnmounted(() => { :main-commit-count="mainCommitCount" :push-remote="pushRemote" :ahead-push-count="aheadPushCount" :is-pushing="isPushing" :is-pulling="isPulling" :force-push-preferred="forcePushPreferred" :is-fetching="isFetching" - :cwd="repoFolderPath ?? ''" :branches="branches" :branches-loading="branchesLoading" + :cwd="repoFolderPath ?? ''" :branches="branches" :worktree-branches="worktreeBranches" :branches-loading="branchesLoading" :is-switching-branch="isSwitchingBranch" :is-merging="isMerging" :tabs="repoTabs" :active-tab-id="activeTabId" @open-folder="handleOpenFolder" @open-repo="handleOpenPath" @switch-tab="switchTab" @close-tab="closeTab" @reorder-tabs="reorderTabs" @@ -2355,7 +2356,7 @@ onUnmounted(() => {