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:
@@ -40,6 +40,9 @@ export function registerWsGateway(
|
||||
let subscribedWorktrees = false;
|
||||
let subscribedGroups = false;
|
||||
let alive = true;
|
||||
// Worktrees « regardés » par CETTE connexion (clé repoId\0path) → push ciblé de worktree_changes.
|
||||
const watched = new Set<string>();
|
||||
const watchKey = (repoId: string, path: string): string => `${repoId}\0${path}`;
|
||||
|
||||
const send = (msg: ServerMessage): void => {
|
||||
if (socket.readyState === socket.OPEN) socket.send(JSON.stringify(msg));
|
||||
@@ -76,6 +79,10 @@ export function registerWsGateway(
|
||||
const onGroupRemoved = (groupId: string): void => {
|
||||
if (subscribedGroups) send({ type: 'group_removed', groupId });
|
||||
};
|
||||
// P7 — détail d'un worktree modifié : poussé UNIQUEMENT si cette connexion le regarde.
|
||||
const onWorktreeChanges = (e: { repoId: string; path: string }): void => {
|
||||
if (watched.has(watchKey(e.repoId, e.path))) send({ type: 'worktree_changes', ...e });
|
||||
};
|
||||
manager.on('session_update', onSessionUpdate);
|
||||
manager.on('session_exit', onSessionExit);
|
||||
discovery.on('discovery_update', onDiscoveryUpdate);
|
||||
@@ -83,6 +90,7 @@ export function registerWsGateway(
|
||||
worktrees.on('repo_removed', onRepoRemoved);
|
||||
worktrees.on('worktree_update', onWorktreeUpdate);
|
||||
worktrees.on('worktree_removed', onWorktreeRemoved);
|
||||
worktrees.on('worktree_changes', onWorktreeChanges);
|
||||
groups.on('group_update', onGroupUpdate);
|
||||
groups.on('group_removed', onGroupRemoved);
|
||||
|
||||
@@ -126,6 +134,22 @@ export function registerWsGateway(
|
||||
subscribedGroups = msg.topics.includes('groups');
|
||||
return;
|
||||
}
|
||||
case 'watch': {
|
||||
const key = watchKey(msg.repoId, msg.path);
|
||||
if (watched.has(key)) return;
|
||||
watched.add(key);
|
||||
// Validation repo/worktree + armement du watcher FS (asynchrone, best-effort).
|
||||
void worktrees.watch(msg.repoId, msg.path).catch((err) => {
|
||||
watched.delete(key);
|
||||
send({ type: 'error', code: 'NOT_FOUND', message: (err as Error).message });
|
||||
});
|
||||
return;
|
||||
}
|
||||
case 'unwatch': {
|
||||
watched.delete(watchKey(msg.repoId, msg.path));
|
||||
worktrees.unwatch(msg.repoId, msg.path);
|
||||
return;
|
||||
}
|
||||
case 'attach': {
|
||||
const channel = nextChannel++;
|
||||
const binding: ClientBinding = {
|
||||
@@ -215,10 +239,17 @@ export function registerWsGateway(
|
||||
worktrees.off('repo_removed', onRepoRemoved);
|
||||
worktrees.off('worktree_update', onWorktreeUpdate);
|
||||
worktrees.off('worktree_removed', onWorktreeRemoved);
|
||||
worktrees.off('worktree_changes', onWorktreeChanges);
|
||||
groups.off('group_update', onGroupUpdate);
|
||||
groups.off('group_removed', onGroupRemoved);
|
||||
for (const [, st] of channels) manager.detach(st.sessionId, st.binding);
|
||||
channels.clear();
|
||||
// Libère le refcount du watcher pour chaque worktree regardé par cette connexion.
|
||||
for (const key of watched) {
|
||||
const sep = key.indexOf('\0');
|
||||
worktrees.unwatch(key.slice(0, sep), key.slice(sep + 1));
|
||||
}
|
||||
watched.clear();
|
||||
});
|
||||
|
||||
void req;
|
||||
|
||||
Reference in New Issue
Block a user