run method

  1. @override
Future<int> run()
override

Runs this command.

The return value is wrapped in a Future if necessary and returned by CommandRunner.runCommand.

Implementation

@override
Future<int> run() async {
  // The configuration is optional here: reading the git history does not
  // need one, and `distribute changelog` is useful before `init` has run.
  ChangelogSettings settings = const ChangelogSettings();
  Map<String, dynamic> aiSection = const {};

  final file = File(configPath);
  if (!file.existsSync() && _configWasTyped) {
    // A default that is not there is fine — the history does not need one.
    // A path the user typed is different: silently ignoring it would read as
    // if the settings had been applied.
    logger.logError("Configuration file '$configPath' not found.");
    return 1;
  }
  if (file.existsSync()) {
    try {
      final config = await ConfigParser.distributeYaml(
        configPath,
        globalResults,
      );
      settings = ChangelogSettings.fromYaml(config.changelog);
      // The key is commonly stored as `${{OPENAI_API_KEY}}`, so it has to be
      // resolved before it reaches the provider — otherwise the placeholder
      // itself is sent as the credential.
      aiSection = {
        for (final entry in config.ai.entries)
          entry.key: entry.value is String
              ? await config.variables.process(entry.value as String)
              : entry.value,
      };
    } on ConfigException catch (e) {
      logger.logError(e.message);
      return 1;
    } on ArgumentError catch (e) {
      logger.logError('${e.message}');
      logger.logDetail('check the `changelog:` section of $configPath');
      return 1;
    }
  }

  final limit = _parsedLimit();
  if (!limit.ok) return 64;

  final Changelog changelog;
  try {
    changelog = await Changelog.fromGit(
      from: (argResults!['from'] as String?) ?? settings.from,
      to: argResults!['to'] as String,
      limit: limit.value ?? settings.limit,
      includeMerges:
          (argResults!['merges'] as bool) || settings.includeMerges,
    );
  } on ChangelogException catch (e) {
    logger.logError(e.message);
    return 1;
  }

  if (changelog.shallow) {
    ColorizeLogger.reserveStdout = true;
    logger.logWarning(
      'this is a shallow clone, so the notes may be missing older commits',
    );
    logger.logDetail(
      'fetch the full history first — `git fetch --unshallow`, or '
      '`fetch-depth: 0` on a CI checkout',
    );
  }

  if (changelog.isEmpty) {
    ColorizeLogger.reserveStdout = true;
    logger.logWarning('no commits in ${changelog.range}');
    logger.logDetail(
      'pass --from to widen the range, or tag the previous release',
    );
    // An empty range must not leave whatever `-o` pointed at in place: a
    // stale file next to a successful exit reads as "these are the notes".
    final target = argResults!['output'] as String?;
    if (target != null && target.isNotEmpty && File(target).existsSync()) {
      logger.logWarning('$target still holds the notes from an earlier run');
    }
    return 0;
  }

  final format = argResults!.wasParsed('format')
      ? ChangelogFormat.parse(argResults!['format'] as String)
      : settings.format;

  var rendered = changelog.render(
    format: format,
    group: argResults!.wasParsed('group')
        ? argResults!['group'] as bool
        : settings.group,
    includeShas: (argResults!['shas'] as bool) || settings.includeShas,
  );

  final output = argResults!['output'] as String?;
  // Without `-o` the notes are the command's stdout, so every log line has
  // to move aside — otherwise `distribute changelog --ai > NOTES.md` writes
  // the progress line into the notes.
  if (output == null || output.isEmpty) {
    ColorizeLogger.reserveStdout = true;
    ColorizeLogger.retargetColors();
  }

  if ((argResults!['ai'] as bool) || settings.ai) {
    final polished = await _polish(rendered, aiSection, settings);
    if (polished == null) return 1;
    rendered = polished;
  }

  if (output == null || output.isEmpty) {
    // The notes are the product of this command, so they go to stdout on
    // their own; everything else the command said went to stderr.
    stdout.writeln(rendered);
    return 0;
  }

  try {
    final target = File(output);
    await target.parent.create(recursive: true);
    await target.writeAsString('$rendered\n');
    logger.logSuccess(
      'wrote ${changelog.entries.length} entr'
      '${changelog.entries.length == 1 ? 'y' : 'ies'} to $output',
    );
    logger.logDetail('range: ${changelog.range}');
    return 0;
  } on FileSystemException catch (e) {
    logger.logError('could not write $output: ${e.message}');
    return 1;
  }
}