feat(p7): moteur git détaillé, API fichiers et watcher FS temps réel
Fondations de la refonte « vrai IDE IA worktree » (chantier P7→P12).
- git.ts : listChanges (status --porcelain=v2 -z + --numstat fusionnés),
fileDiff (diff unifié borné, refus binaire, untracked via --no-index),
stage/unstage/restore/clean, commitStaged, amendCommit (refus si poussé),
fetchRemote, pull (ff-only|rebase), lastCommit, resolveGitDir,
isSafeRelativePath ; worktreeStatus enrichi (compteurs staged/unstaged/
conflict + dernier commit), champs additifs WorktreeGitStatus.
- core/fs-watcher.ts : FsWatcherService (chokidar, refcount + pool LRU borné,
ignore .git sauf HEAD/index, débounce) → invalide factsCache, rediffuse
worktree_update et émet worktree_changes.
- WorktreeManager : getWorktreeChanges/getFileDiff/stage/unstage/discard/
fetch/pull/watch/unwatch/assertPathInWorktree ; commitWorktree { mode, amend }.
- routes/git.ts (changes/diff/stage/unstage/discard/fetch/pull) et
routes/files.ts (GET/PUT contenu fichier, bornage strict au worktree) ;
fs/list?includeFiles=1 (flag isFile).
- protocole ADDITIF (PROTOCOL_VERSION inchangé) : messages WS watch/unwatch
(validés dans parseClientMessage) + worktree_changes poussé ciblé par la
gateway ; front wsClient.watchWorktree + lib/git-api.ts + api.put.
- tests git/fs-watcher/protocole + acceptance-p7.mjs (ALL GREEN).
- dépendance : chokidar (serveur).
This commit is contained in:
@@ -157,6 +157,20 @@ export interface WorktreeGitStatus {
|
||||
behind: number;
|
||||
dirtyCount: number;
|
||||
upstream: string | null;
|
||||
// ---- Champs additifs P7 (optionnels, rétrocompat) — compteurs bon marché calculés dans le
|
||||
// même passage `status --porcelain=v2` que dirtyCount. La liste détaillée des changements
|
||||
// (FileChange[]) n'est JAMAIS diffusée ici : elle est servie à la demande (REST) et poussée
|
||||
// uniquement aux clients qui « regardent » le worktree (voir `worktree_changes`).
|
||||
/** fichiers avec des modifications indexées (staged). */
|
||||
stagedCount?: number;
|
||||
/** fichiers avec des modifications non indexées (unstaged, untracked inclus). */
|
||||
unstagedCount?: number;
|
||||
/** fichiers en conflit de merge. */
|
||||
conflictCount?: number;
|
||||
/** hash court du dernier commit (HEAD), null si le dépôt n'a aucun commit. */
|
||||
lastCommitHash?: string | null;
|
||||
/** sujet (première ligne) du dernier commit. */
|
||||
lastCommitSubject?: string | null;
|
||||
}
|
||||
|
||||
export interface WorktreeSummary {
|
||||
@@ -203,6 +217,11 @@ export type ClientMessage =
|
||||
| { type: 'resize'; channel: number; cols: number; rows: number }
|
||||
| { type: 'ack'; channel: number; bytes: number }
|
||||
| { type: 'sub'; topics: Array<'sessions' | 'worktrees' | 'groups'> }
|
||||
// P7 — abonnement ciblé au détail d'un worktree (diff/statut fin) : on ne pousse le détail
|
||||
// qu'aux connexions qui « regardent » ce worktree (clé = repoId + path), indépendamment du
|
||||
// topic global 'worktrees' (qui ne transporte que les compteurs légers).
|
||||
| { type: 'watch'; repoId: string; path: string }
|
||||
| { type: 'unwatch'; repoId: string; path: string }
|
||||
| { type: 'ping' };
|
||||
|
||||
// ---- Messages serveur → client ----
|
||||
@@ -217,6 +236,10 @@ export type ServerMessage =
|
||||
| { type: 'repo_removed'; repoId: string }
|
||||
| { type: 'worktree_update'; repoId: string; worktree: WorktreeSummary }
|
||||
| { type: 'worktree_removed'; repoId: string; path: string }
|
||||
// P7 — signal léger « le détail (diff/fichiers modifiés) de ce worktree a changé, refais un
|
||||
// GET /changes ou /diff ». Envoyé UNIQUEMENT aux connexions ayant `watch`é cette clé. On ne
|
||||
// pousse pas le diff complet dans la frame (taille/flow control) : le client re-fetch en REST.
|
||||
| { type: 'worktree_changes'; repoId: string; path: string }
|
||||
| { type: 'group_update'; group: GroupSummary }
|
||||
| { type: 'group_removed'; groupId: string }
|
||||
| { type: 'error'; code: ErrorCode; message: string; channel?: number }
|
||||
@@ -279,6 +302,14 @@ export function parseClientMessage(raw: string): ClientMessage | null {
|
||||
return Array.isArray(m.topics) && m.topics.every((t) => t === 'sessions' || t === 'worktrees' || t === 'groups')
|
||||
? { type: 'sub', topics: m.topics as Array<'sessions' | 'worktrees' | 'groups'> }
|
||||
: null;
|
||||
case 'watch':
|
||||
case 'unwatch':
|
||||
// repoId/path bornés ici (typage + longueur) ; l'existence repo/worktree et la sûreté du
|
||||
// chemin sont re-validées côté serveur (isSafeAbsolutePath + findWorktree) avant d'armer.
|
||||
return typeof m.repoId === 'string' && m.repoId.length > 0 && m.repoId.length <= 128 &&
|
||||
typeof m.path === 'string' && m.path.length > 0 && m.path.length <= 4096
|
||||
? { type: m.type, repoId: m.repoId, path: m.path }
|
||||
: null;
|
||||
case 'ping':
|
||||
return { type: 'ping' };
|
||||
default:
|
||||
|
||||
Reference in New Issue
Block a user