Итерация 5: Git status, file watching, расширенные возможности
Зависит от: Итерация 4
Цель
Git status в дереве файлов. Live refresh при изменениях на диске. Conflict detection при сохранении. Line wrap toggle.
Новые npm-зависимости
simple-git v3.32+ (pnpm add simple-git) — обёртка над git CLI с TypeScript типами, parsed StatusResult, встроенным timeout/abort. 7.9M downloads/нед, dual ESM/CJS, не native module
chokidar v4.0.3 (pnpm add chokidar@4) — file watcher (117M downloads/нед). Решает все проблемы raw fs.watch: нормализация событий, recursive на Linux, ENOSPC handling, symlinks. Dual CJS/ESM, Node 14+
IPC каналы
| Канал |
Описание |
editor:gitStatus |
Git status через simple-git (v3.32+), кеш 5 сек |
editor:watchDir |
Запуск file watcher (opt-in, НЕ по умолчанию) |
editor:change |
Event: файл изменился на диске (main -> renderer) |
Новые файлы
| # |
Файл |
Описание |
| 1 |
src/main/services/editor/EditorFileWatcher.ts |
FileWatcher через chokidar v4 (~50-70 LOC). Burst coalescing, ENOSPC, recursive Linux — всё встроено. Фильтрация node_modules/.git/dist через ignored option |
| 2 |
src/main/services/editor/GitStatusService.ts |
Git status через simple-git с StatusResult маппингом, кеш 5 сек. Переиспользовать isGitRepo() из GitDiffFallback.ts (~80-100 LOC) |
| 3 |
src/main/services/editor/conflictDetection.ts |
Утилита mtime check: сравнение mtime до/после save, conflict resolution (~40 LOC) |
| 4 |
src/renderer/components/team/editor/GitStatusBadge.tsx |
M/U/A бейджи в дереве |
Изменения в существующих файлах
| # |
Файл |
Изменение |
| 1 |
src/shared/types/editor.ts |
GitFileStatus, EditorFileChangeEvent |
| 2 |
src/shared/types/api.ts |
gitStatus, onEditorChange в EditorAPI |
| 3 |
src/main/ipc/editor.ts |
Handlers для git status и file watcher |
| 4 |
src/preload/index.ts |
editor.gitStatus, editor.onEditorChange (НЕ onFileChange — конфликт с существующим ElectronAPI.onFileChange) |
| 5 |
src/preload/constants/ipcChannels.ts |
EDITOR_GIT_STATUS, EDITOR_WATCH_DIR, EDITOR_CHANGE |
| 6 |
src/renderer/components/team/editor/EditorFileTree.tsx |
Git status badges |
| 7 |
src/renderer/components/team/editor/CodeMirrorEditor.tsx |
Conflict detection (mtime check) при сохранении |
| 8 |
src/renderer/components/team/editor/ProjectEditorOverlay.tsx |
File watcher подписка, auto-refresh, conflict modal |
| 9 |
src/renderer/store/slices/editorSlice.ts |
Git status data, file watcher state |
| 10 |
src/renderer/store/index.ts |
В initializeNotificationListeners() добавить подписку if (api.editor?.onEditorChange) → обновление дерева/табов при внешних изменениях (guard обязателен — паттерн из всех существующих subscriptions) |
| 11 |
src/main/index.ts |
mainWindow.on('closed') → cleanupEditorState(). shutdownServices() → cleanupEditorState() |
Security-требования
editor:gitStatus: cwd = activeProjectRoot (валидный). Не передавать full paths от git без валидации
editor:change: пути в events могут утечь через symlink -- валидировать перед передачей в renderer (SEC-2)
Watcher lifecycle cleanup (macOS: window closed but app alive)
editor:open — если activeProjectRoot !== null, сначала остановить предыдущий watcher и сбросить state (идемпотентный reset). Guard: if (activeProjectRoot !== null) throw new Error('Another editor is already open')
mainWindow.on('closed') в src/main/index.ts — вызвать cleanupEditorState() (экспорт из editor.ts): сброс activeProjectRoot, остановка watcher. Аналог существующего cleanup для notificationManager, ptyTerminalService
shutdownServices() — добавить cleanupEditorState() рядом с removeIpcHandlers()
Performance-требования (R4/R5)
- File watcher opt-in: по умолчанию ВЫКЛЮЧЕН. Toggle "Watch for external changes". По умолчанию ручной refresh (F5)
chokidar конфигурация
// src/main/services/editor/EditorFileWatcher.ts
import { watch, type FSWatcher } from 'chokidar';
let watcher: FSWatcher | null = null;
function startWatching(projectRoot: string, onChange: (event: EditorFileChangeEvent) => void) {
watcher = watch(projectRoot, {
ignored: /(node_modules|\.git|dist|__pycache__|\.cache|\.next|\.venv|\.tox|vendor)/,
ignoreInitial: true,
followSymlinks: false,
depth: 20,
});
watcher.on('change', path => onChange({ type: 'change', path }));
watcher.on('add', path => onChange({ type: 'create', path }));
watcher.on('unlink', path => onChange({ type: 'delete', path }));
}
function stopWatching() {
watcher?.close();
watcher = null;
}
- chokidar v4 решает все проблемы raw
fs.watch: нормализация событий macOS, recursive на Linux, ENOSPC handling, debounce
- macOS: FSEvents через chokidar (надёжность 9/10)
- Linux: inotify через chokidar с автоматическим fallback (надёжность 8/10, было 6/10 с raw fs.watch)
- Git status кешировать на 5 секунд. Invalidate по file watcher event
simple-git конфигурация
// src/main/services/editor/GitStatusService.ts
import { simpleGit, StatusResult, SimpleGit } from 'simple-git';
// Создать инстанс с --no-optional-locks + timeout
const createGit = (projectRoot: string): SimpleGit =>
simpleGit({
baseDir: projectRoot,
timeout: { block: 10_000 }, // 10s (паттерн из GitDiffFallback.ts)
}).env('GIT_OPTIONAL_LOCKS', '0'); // эквивалент --no-optional-locks
// Маппинг StatusResult → GitFileStatus[]
function mapStatus(result: StatusResult): GitFileStatus[] {
const files: GitFileStatus[] = [];
for (const p of result.modified) files.push({ path: p, status: 'modified' });
for (const p of result.not_added) files.push({ path: p, status: 'untracked' });
for (const p of result.staged) files.push({ path: p, status: 'staged' });
for (const p of result.deleted) files.push({ path: p, status: 'deleted' });
for (const p of result.conflicted) files.push({ path: p, status: 'conflict' });
for (const r of result.renamed) files.push({ path: r.to, status: 'renamed', renamedFrom: r.from });
return files;
}
GIT_OPTIONAL_LOCKS=0 — предотвращает .git/index.lock конфликты (критично для фоновых запросов!)
timeout.block: 10_000 — SIGINT после 10 сек без вывода
- Парсинг не нужен —
simple-git делает полный парсинг porcelain вывода включая renamed, conflicts, ahead/behind
- Переиспользовать
isGitRepo() из GitDiffFallback.ts для проверки наличия .git
- Graceful degradation:
- Нет git → скрыть git бейджи, "Git not available" в status bar
- Не git-repo → скрыть git бейджи
- Timeout → "Git status unavailable" + кнопка retry
UX-требования
- File changed on disk while open: banner в табе "File changed on disk. [Reload] [Keep mine] [Show diff]" (НЕ перезаписывать молча)
- File deleted on disk while open: banner "File no longer exists on disk. [Close tab]"
- Conflict detection при save: mtime check. Если изменился -- dialog "Overwrite / Cancel / Show diff"
- Line wrap toggle в toolbar
Тестирование
| # |
Что тестировать |
Файл |
| 1 |
GitStatusService -- маппинг simple-git StatusResult → GitFileStatus[], кеш, graceful degradation |
test/main/services/editor/GitStatusService.test.ts |
| 2 |
EditorFileWatcher -- chokidar watcher lifecycle (start/stop), event mapping, cleanup |
test/main/services/editor/EditorFileWatcher.test.ts |
| 3 |
conflictDetection -- mtime check логика |
test/main/services/editor/conflictDetection.test.ts |
| 4 |
Manual: изменить файл в внешнем редакторе -> conflict banner |
— |
Критерии готовности
Оценка
- Надёжность решения: 9/10 (было 8/10) --
simple-git + chokidar убирают ~300 LOC ручного кода: парсинг porcelain, fs.watch нормализация, ENOSPC fallback. Всё покрыто проверенными пакетами.
- Уверенность: 9/10 -- simple-git 7.9M + chokidar 117M downloads/нед. Оба с TypeScript, проверены в production.