feat(worktrees): démarrer une session sur la branche principale ou tout worktree
Ajoute le choix « créer un worktree OU bosser sur la branche principale » au moment de démarrer le travail, sur deux surfaces : - bouton « Démarrer » (claude/bash) sur chaque WorktreeCard, y compris le checkout principal (badge « principal ») — comblait un manque : impossible jusqu'ici de lancer une session sur un worktree existant depuis le dashboard ; - toggle de mode (Worktree / Branche principale) dans le formulaire d'entête de RepoSection. Option git « créer & basculer » : nouvel endpoint POST /api/v1/repos/:id/session qui lance UNE session dans le checkout principal (repo.path), avec création ou bascule de branche optionnelle (git switch[-c]) — refusée en 409 si l'arbre est sale. Pas de hooks ni de pré-trust (c'est le dépôt réel de l'utilisateur). - shared : type additif StartRepoSessionRequest. - server : git.switchBranch + WorktreeManager.startMainSession + route. - web : store worktrees.startMainSession, UI WorktreeCard/RepoSection, i18n fr/en. - tests : +5 unitaires (switchBranch, startMainSession) ; acceptance-p3 étendue.
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
addWorktree,
|
||||
removeWorktree,
|
||||
pruneWorktrees,
|
||||
switchBranch,
|
||||
isDirtyWorktreeError,
|
||||
} from '../src/core/git.js';
|
||||
|
||||
@@ -118,6 +119,20 @@ describe('opérations git (repo tmp réel)', () => {
|
||||
expect((await listWorktrees(repo)).some((w) => resolve(w.path) === resolve(wtPath))).toBe(false);
|
||||
});
|
||||
|
||||
it('switchBranch : create=true crée une branche, create=false bascule sur une existante', async () => {
|
||||
const repo = makeTmpRepo();
|
||||
const cur = (): string => execFileSync('git', ['branch', '--show-current'], { cwd: repo }).toString().trim();
|
||||
|
||||
await switchBranch(repo, { branch: 'feature/new', create: true });
|
||||
expect(cur()).toBe('feature/new');
|
||||
|
||||
await switchBranch(repo, { branch: 'main', create: false });
|
||||
expect(cur()).toBe('main');
|
||||
|
||||
// créer une branche déjà existante échoue (git refuse).
|
||||
await expect(switchBranch(repo, { branch: 'feature/new', create: true })).rejects.toBeDefined();
|
||||
});
|
||||
|
||||
it('prune retire un worktree dont le dossier a disparu', async () => {
|
||||
const repo = makeTmpRepo();
|
||||
const wtPath = join(dirname(repo), `${basename(repo)}-wt-gone`);
|
||||
|
||||
@@ -116,6 +116,46 @@ describe('WorktreeManager', () => {
|
||||
await expect(wt.createWorktree(r.id, { branch: '../evil', newBranch: true })).rejects.toMatchObject({ statusCode: 400 });
|
||||
});
|
||||
|
||||
it('startMainSession : session dans le checkout principal (branche actuelle, sans mutation git)', async () => {
|
||||
const repo = makeTmpRepo();
|
||||
const r = await wt.addRepo({ path: repo });
|
||||
const out = await wt.startMainSession(r.id, { command: 'bash' });
|
||||
expect(resolve(out.session.cwd)).toBe(resolve(repo));
|
||||
expect(out.session).toMatchObject({ command: 'bash', live: true });
|
||||
// pas de bascule de branche → toujours sur main.
|
||||
expect(execFileSync('git', ['branch', '--show-current'], { cwd: repo }).toString().trim()).toBe('main');
|
||||
const main = (await wt.listRepoWorktrees(r.id, true)).find((w) => w.isMain);
|
||||
expect(main?.sessions.some((s) => s.id === out.session.id)).toBe(true);
|
||||
});
|
||||
|
||||
it('startMainSession : newBranch crée et bascule la branche dans le checkout principal + event', async () => {
|
||||
const repo = makeTmpRepo();
|
||||
const r = await wt.addRepo({ path: repo });
|
||||
const events: Array<{ worktree: WorktreeSummary }> = [];
|
||||
wt.on('worktree_update', (e) => events.push(e));
|
||||
const out = await wt.startMainSession(r.id, { command: 'bash', branch: 'feature/x', newBranch: true });
|
||||
expect(execFileSync('git', ['branch', '--show-current'], { cwd: repo }).toString().trim()).toBe('feature/x');
|
||||
expect(out.worktree?.branch).toBe('feature/x');
|
||||
expect(events.some((e) => e.worktree.isMain && e.worktree.branch === 'feature/x')).toBe(true);
|
||||
});
|
||||
|
||||
it('startMainSession : checkout principal sale → 409 DIRTY_TREE (pas de bascule)', async () => {
|
||||
const repo = makeTmpRepo();
|
||||
const r = await wt.addRepo({ path: repo });
|
||||
writeFileSync(join(repo, 'scratch.txt'), 'wip\n'); // arbre sale
|
||||
await expect(wt.startMainSession(r.id, { command: 'bash', branch: 'feature/y', newBranch: true })).rejects.toMatchObject({
|
||||
statusCode: 409,
|
||||
code: 'DIRTY_TREE',
|
||||
});
|
||||
expect(execFileSync('git', ['branch', '--show-current'], { cwd: repo }).toString().trim()).toBe('main');
|
||||
});
|
||||
|
||||
it('startMainSession : branche invalide → 400', async () => {
|
||||
const repo = makeTmpRepo();
|
||||
const r = await wt.addRepo({ path: repo });
|
||||
await expect(wt.startMainSession(r.id, { command: 'bash', branch: '../evil', newBranch: true })).rejects.toMatchObject({ statusCode: 400 });
|
||||
});
|
||||
|
||||
it('deleteWorktree : main refusé (400), dirty refusé (409) puis force OK', async () => {
|
||||
const repo = makeTmpRepo();
|
||||
const r = await wt.addRepo({ path: repo });
|
||||
|
||||
Reference in New Issue
Block a user