WAL-журнал в Git-клиенте: как сделать rebase atomic
Git rebase может упасть на полпути. Merge может прерваться из-за отключения питания. Cherry-pick может зависнуть и оставить репозиторий в detached HEAD. Стандартный git CLI справляется с этим через .git/rebase-merge/ и .git/MERGE_HEAD, но для десктопного клиента этого недостаточно.
Проблема
Когда пользователь нажимает «Rebase» в GUI, происходит цепочка из 5-15 git-команд. Если процесс упадёт между 7-й и 8-й — репозиторий останется в неконсистентном состоянии. Git CLI это переживёт: git rebase --abort. Но UI-клиент должен:
- Знать, что операция не завершена
- Показать пользователю что случилось
- Предложить продолжить или откатить
- Гарантировать что данные не потеряны
Решение — Write-Ahead Log
WAL — паттерн из мира баз данных (SQLite, PostgreSQL). Идея: записать намерение В ЖУРНАЛ до выполнения. Если процесс упадёт — при следующем запуске читаем журнал и понимаем что было прервано.
type WalEntry = {
id: string;
operation: "rebase" | "merge" | "cherry-pick" | "reset";
steps: WalStep[];
currentStep: number;
status: "in_progress" | "completed" | "failed";
rollbackInfo: RollbackData;
};
type WalStep = {
command: string[];
preState: GitState;
postState?: GitState;
};
Жизненный цикл операции
- Запись в WAL — до первой git-команды. Сохраняем: тип операции, список шагов, текущий HEAD, рабочее дерево
- Выполнение шагов — каждый шаг записывает
postStateпосле успеха - Завершение — помечаем запись как
completed, очищаем - Сбой — запись остаётся
in_progress. При следующем запуске RecoveryManager подхватывает
RecoveryManager
При старте приложения — первым делом проверяем WAL:
class RecoveryManager {
async checkOnStartup(): Promise<RecoveryAction | null> {
const pending = await this.wal.getPendingEntries();
if (pending.length === 0) return null;
const entry = pending[0];
const lastCompleted = entry.steps.findIndex(s => !s.postState);
return {
entry,
lastCompletedStep: lastCompleted - 1,
canContinue: this.isResumable(entry),
canRollback: true,
};
}
}
Пользователь видит диалог: «Обнаружена незавершённая операция rebase (шаг 7 из 12). Продолжить или откатить?»
5 уровней защиты
WAL — один из пяти уровней. Полная архитектура:
- Auto-reflog — перед любой опасной операцией сохраняем ref в reflog с меткой
- Auto-stash — если есть несохранённые изменения, stash перед операцией, pop после
- WAL-журнал — atomic запись операций
- RecoveryManager — восстановление при сбоях
- Snapshot undo — UI-уровень: каждое действие можно отменить через Ctrl+Z
Каждый уровень работает независимо. Даже если WAL сам повреждён — auto-reflog позволяет восстановить состояние вручную через git reflog.
Формат хранения
WAL хранится как JSON-файл в ~/.gitbor/wal/. Один файл на операцию. Файлы операций старше 7 дней чистятся автоматически. Формат человекочитаемый — при крайней необходимости можно разобрать руками.
Результат
За 4 месяца тестирования — ноль потерь данных при имитированных крашах. Включая:
- Kill -9 процесса во время rebase
- Отключение питания (виртуалка) во время merge
- Segfault в native-модуле во время cherry-pick
Каждый раз RecoveryManager корректно подхватывал и предлагал восстановление.