run method
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;
}
}