RS

WAL-журнал в Git-клиенте: как сделать rebase atomic

Invalid Date мин чтения

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-клиент должен:

  1. Знать, что операция не завершена
  2. Показать пользователю что случилось
  3. Предложить продолжить или откатить
  4. Гарантировать что данные не потеряны

Решение — 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;
};

Жизненный цикл операции

  1. Запись в WAL — до первой git-команды. Сохраняем: тип операции, список шагов, текущий HEAD, рабочее дерево
  2. Выполнение шагов — каждый шаг записывает postState после успеха
  3. Завершение — помечаем запись как completed, очищаем
  4. Сбой — запись остаётся 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 — один из пяти уровней. Полная архитектура:

  1. Auto-reflog — перед любой опасной операцией сохраняем ref в reflog с меткой
  2. Auto-stash — если есть несохранённые изменения, stash перед операцией, pop после
  3. WAL-журнал — atomic запись операций
  4. RecoveryManager — восстановление при сбоях
  5. 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 корректно подхватывал и предлагал восстановление.