commit method

Future<TransactionResult> commit({
  1. bool dryRun = false,
  2. bool force = false,
})

Executes the staged operations.

@param dryRun When true, prints the staged ops and returns DryRun without touching disk. @param force When true, bypasses the conflict pre-flight so the transaction commits over user-modified files. @return A TransactionResult describing the outcome. @throws StateError When invoked more than once on the same instance.

Implementation

Future<TransactionResult> commit({
  bool dryRun = false,
  bool force = false,
}) async {
  if (_committed) {
    throw StateError(
      'InstallTransaction.commit() called twice on the same instance; '
      'transactions are one-shot.',
    );
  }
  _committed = true;

  // 1. Dry-run short-circuit: print the preview, return without touching disk.
  if (dryRun) {
    _renderDryRun();
    return DryRun(opCount: _ops.length);
  }

  // 2. Conflict pre-flight. Until Step 7 wires the real ConflictDetector,
  //    `_detectConflicts` returns the test-seeded list (or an empty list in
  //    production). The `force` flag bypasses this gate.
  final conflicts = _detectConflicts();
  if (conflicts.isNotEmpty && !force) {
    return Conflict(conflicts: conflicts);
  }

  // 3. Stage every op into an in-memory write plan. A `null` value marks a
  //    delete; a non-null value carries the final UTF-8 content to write.
  //    Unknown / not-yet-implemented op types short-circuit the whole commit
  //    with an [Error] (no disk side effects yet, so rolledBack is false).
  final stagedWrites = <String, String?>{};
  for (final op in _ops) {
    final stageError = _stageOp(op, stagedWrites);
    if (stageError != null) {
      return stageError;
    }
  }

  // 4. Atomic disk flush. Write every non-null entry to `<absPath>.tmp`
  //    first. If any single write throws, loop the successful temps and
  //    delete them all before returning an [Error] with rolledBack=true.
  final committedTemps = <String>[];
  try {
    for (final entry in stagedWrites.entries) {
      final content = entry.value;
      if (content == null) continue;
      final tmpPath = '${entry.key}.tmp';
      _ctx.fs.writeAsString(tmpPath, content);
      committedTemps.add(tmpPath);
    }
  } catch (e) {
    for (final tmp in committedTemps) {
      _ctx.fs.delete(tmp);
    }
    return Error(error: e.toString(), rolledBack: true);
  }

  // 5. All `.tmp` writes succeeded. Rename each one over its target, and
  //    apply pending deletes. Rename + delete failures here are not rolled
  //    back: the .tmp swap is the atomic boundary on POSIX. Any partial
  //    failure past this point is surfaced as an [Error] with rolledBack
  //    false so the operator can inspect manually.
  try {
    for (final entry in stagedWrites.entries) {
      final absPath = entry.key;
      if (entry.value == null) {
        _ctx.fs.delete(absPath);
      } else {
        _ctx.fs.rename('$absPath.tmp', absPath);
      }
    }
  } catch (e) {
    return Error(error: e.toString(), rolledBack: false);
  }

  // 6. Persist the install record BEFORE running shell ops. The atomic
  //    rename boundary in phase 5 is the point of no return for filesystem
  //    state; once renames land, the record must reflect what is on disk
  //    so a downstream shell failure leaves an uninstallable state instead
  //    of orphaning the changes. Subsequent shell failures are independent
  //    of install-state durability.
  final recordPath = p.join(
    _ctx.projectRoot,
    '.artisan',
    'installed',
    '$_pluginName.json',
  );
  final record = _buildRecord(stagedWrites);
  _ctx.fs.writeAsString(recordPath, _encodeJson(record));

  // 7. Run deferred shell ops (RunShell). These execute AFTER every file
  //    mutation has landed so a hook like `flutter pub get` observes the
  //    final on-disk state. A non-zero exit surfaces as Error; the
  //    install record from phase 6 stays on disk so the operator can
  //    `plugin:uninstall` to roll back if the shell precondition cannot
  //    be satisfied.
  final shellError = _runShellOps();
  if (shellError != null) {
    return shellError;
  }

  return Success(opCount: _ops.length, recordPath: recordPath);
}