run method

Future<int> run()

Implementation

Future<int> run() async {
  final allFilesResult = await this.allFiles;
  if (allFilesResult case (_, final int code)) {
    return code;
  }
  final (allFiles, _) = allFilesResult;
  logger.detail('Found ${allFiles.length} files');
  for (final file in allFiles) {
    logger.detail('  - $file');
  }

  await _wait(durations.short);

  logger.detail('Resolving files');

  final pendingHook = PendingHook(hook.resolve(allFiles), logger: logger);

  if (!hook.shouldRunOnEmpty &&
      !pendingHook.topLevelTasks.any((e) => e.shouldAlwaysRun)) {
    if (pendingHook.topLevelTasks.every((e) => e.files.isEmpty)) {
      logger.info(
        darkGray.wrap('Skipping $hookName hook, no files match any tasks'),
      );
      return 0;
    }
  }

  final context = await git.prepareFiles();

  logger.detail('Starting tasks');
  await _wait(durations.short);

  final steps = Steps();

  final runner = CancelableOperation.fromFuture(
    runApp(
      HooksmanApp(
        pendingHook: pendingHook,
        nameOfHook: hookName,
        debug: debug,
        steps: steps,
      ),
      enableHotReload: false,
    ),
    onCancel: () {
      // Stop the nocterm event loop so runApp completes. Do not use
      // shutdownApp()/requestExit — that would exit(0) and race the hook's
      // intentional non-zero exit after Ctrl+C.
      try {
        TerminalBinding.instance.shutdown();
      } catch (_) {}
    },
  );

  steps.current = Step.running;
  await pendingHook.start();
  await pendingHook.wait();

  final failed = pendingHook.topLevelTasks.any((task) {
    final code = task.code;
    return code != null && code != 0;
  });

  if (pendingHook.wasKilled) {
    steps.current = Step.quit;
    logger.detail('Hook was killed');
  } else if (failed) {
    steps.current = Step.error;
    for (final task in pendingHook.topLevelTasks) {
      final code = task.code;
      if (code != null && code != 0) {
        logger.detail(
          'Task failed: ${task.resolvedTask.original.resolvedName}',
        );
      }
    }
  } else {
    steps.current = Step.complete;
    logger.detail('Tasks finished');
  }

  // Let the final step paint before tearing down the TUI.
  await Future<void>.delayed(const Duration(milliseconds: 150));
  await runner.cancel();

  if (logger.level.index == Level.verbose.index || !pendingHook.wasKilled) {
    logger.flush();
  }
  if (!pendingHook.wasKilled) {
    logger.write('\n');
  }

  if (pendingHook.wasKilled || failed) {
    logger.detail('stopping hook tasks');
    return 1;
  }

  if (hook is PreCommitHook) {
    logger.detail('Applying modifications');
    for (final file in context.nonStagedFiles) {
      logger.detail('  - $file');
    }
    await _wait(durations.short);
    await git.applyModifications([
      ...context.nonStagedFiles,
      ...context.deletedFiles,
    ]);
    await _wait(durations.long);
  } else {
    logger.detail('Skipped applying modifications for $hookName');
  }

  // Only PreCommitHook (without allowEmpty) should fail when tasks left
  // nothing to commit. Other hooks succeed after successful task runs.
  if (hook case PreCommitHook(allowEmpty: false)) {
    final files = await git.diffFiles(
      diffArgs: hook.diffArgs,
      diffFilters: hook.diffFilters,
    );

    final code = postSuccessExitCode(hook, files);
    if (code != 0) {
      logger
        ..info('No changes to commit')
        ..detail('--FINISHED--');
      return code;
    }
  }

  logger.detail('--FINISHED--');
  return 0;
}