feat(worktrees): création intelligente (auto) + commit/push/promotion + grille agrandissable

- addWorktree résout la branche par dépôt (branchExists) : checkout si présente, suivi de origin/<b> si seulement remote, sinon création (-b) depuis la branche par défaut. Corrige le 'fatal: invalid reference' sur les groupes hétérogènes ; newBranch -> mode (déprécié, mappé en route).
- Nouvelles opérations git : commit (add -A), push (upstream auto), promotion « en principal » (la branche du worktree devient le checkout principal, sans merge ni conflit, ancienne branche conservée). Routes /worktrees/{commit,push,promote} auditées + GET /repos/:id/branches.
- Web : actions Commit/Push/Promouvoir sur WorktreeCard, sélecteur de branche de base (datalist) dans RepoSection et GroupSessionModal, agrandissement d'un terminal au choix dans TerminalGrid.
- Court-circuite la session de groupe quand aucun worktree n'a pu être créé (fini le masquage par « no resolvable worktree »).
- Tests : git.ts (branchExists/listBranches/commitAll/auto), worktree-manager (commit/promote/branches) ; acceptance P5 étend le scénario worktree de groupe sur branche neuve + commit + promotion.
This commit is contained in:
2026-06-22 19:55:45 +02:00
parent e84c6a7f6d
commit f4fb6c3b52
18 changed files with 732 additions and 62 deletions

View File

@@ -4,7 +4,7 @@
// group_removed sur le topic 'groups', ajout/retrait de repo, et purge CASCADE de la membership
// quand le repo est supprimé (PRAGMA foreign_keys = ON).
import { spawn, execFileSync } from 'node:child_process';
import { mkdtempSync, rmSync } from 'node:fs';
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -142,6 +142,34 @@ try {
const pushedSession = await c.waitMsg((m) => m.type === 'session_update' && m.session?.id === gsession.id);
check('broadcast WS session_update (session de groupe)', !!pushedSession);
// ---- Worktree de groupe sur une NOUVELLE branche (scénario corrigé : mode auto, plus de -b raté) ----
const wtA = await j(`/api/v1/repos/${repoSummary.id}/worktrees`, 'POST', cookie, { branch: 'feature/cross', mode: 'auto', startSession: null });
const wtABody = await wtA.json();
check('POST /repos/:id/worktrees (branche neuve) → 201 + action=created', wtA.status === 201 && wtABody.action === 'created');
const wtB = await j(`/api/v1/repos/${repo2Summary.id}/worktrees`, 'POST', cookie, { branch: 'feature/cross', mode: 'auto', startSession: null });
check('POST /repos (2e repo) worktree branche neuve → 201', wtB.status === 201);
// GET /branches : la nouvelle branche apparaît (alimente le sélecteur de base côté UI).
const branchesA = await (await j(`/api/v1/repos/${repoSummary.id}/branches`, 'GET', cookie)).json();
check('GET /repos/:id/branches : feature/cross présente', Array.isArray(branchesA.local) && branchesA.local.includes('feature/cross'));
// session de groupe SUR cette branche → résout les worktrees créés (le cas qui échouait avant).
const gsBranch = await j(`/api/v1/groups/${group.id}/session`, 'POST', cookie, { command: 'bash', branch: 'feature/cross' });
const gsBranchBody = await gsBranch.json();
check('POST /groups/:id/session (branche) → 201 + 2 dirs', gsBranch.status === 201 && gsBranchBody.dirs?.length === 2);
await j(`/api/v1/sessions/${gsBranchBody.session.id}`, 'DELETE', cookie);
// commit : fichier neuf dans le worktree de repo1 puis commit via l'endpoint.
writeFileSync(join(wtABody.worktree.path, 'cross.txt'), 'wip\n');
const commitRes = await j(`/api/v1/repos/${repoSummary.id}/worktrees/commit`, 'POST', cookie, { path: wtABody.worktree.path, message: 'wip cross' });
check('POST /worktrees/commit → 200 + dirty=0', commitRes.status === 200 && (await commitRes.json()).worktree?.git?.dirtyCount === 0);
// promotion « en principal » : feature/cross devient le checkout principal de repo1, worktree supprimé.
const promRes = await j(`/api/v1/repos/${repoSummary.id}/worktrees/promote`, 'POST', cookie, { path: wtABody.worktree.path });
check('POST /worktrees/promote → 200', promRes.status === 200);
const wtsAfter = await (await j(`/api/v1/repos/${repoSummary.id}/worktrees`, 'GET', cookie)).json();
check('promotion : checkout principal désormais sur feature/cross', wtsAfter.worktrees?.find((w) => w.isMain)?.branch === 'feature/cross');
// groupe sans worktree résolu (branche inexistante) → 400.
const gsBad = await j(`/api/v1/groups/${group.id}/session`, 'POST', cookie, { command: 'bash', branch: 'no/such/branch' });
check('POST /groups/:id/session branche absente → 400', gsBad.status === 400);