Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions pkgs/repo_manage/lib/changelog_updater.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:convert';
import 'dart:io';

import 'src/common.dart';

String updateChangelogContent(String changelog, String message) {
final lines = LineSplitter.split(changelog).toList();
var currentVersion = '0.0.1';
var currentVersionLine = 0;

for (var i = 0; i < lines.length; i++) {
final line = lines[i].trim();
if (line.startsWith('## ')) {
final content = line.substring(3).trim();
final version = content.split(' ').first;
if (version.startsWith(RegExp(r'\d'))) {
currentVersion = version;
currentVersionLine = i;
break;
}
}
}

final isWip = currentVersion.endsWith('-wip');

final output = <String>[];
if (isWip) {
var sectionEnd = lines.indexWhere(
(line) => line.startsWith('## '), currentVersionLine + 1) -
1;
if (sectionEnd < 0) {
sectionEnd = lines.length;
}

output.addAll(lines.take(sectionEnd));
output.add('- $message');
output.addAll(lines.skip(sectionEnd));
Comment thread
mosuem marked this conversation as resolved.
} else {
output.addAll([
'## $currentVersion-wip',
'',
'- $message',
'',
...lines,
]);
}

return '${output.join('\n')}\n';
}

class ChangelogUpdaterCommand extends ReportCommand {
ChangelogUpdaterCommand()
: super('changelog', 'Update a changelog with a new entry.') {
argParser.addOption(
'changelog',
abbr: 'c',
help: 'Path to the changelog file to update. Defaults to CHANGELOG.md.',
);
}

@override
Future<int> run() async {
final args = argResults?.rest ?? const <String>[];
if (args.isEmpty) {
stderr.writeln('''
Usage: dart run report.dart changelog [--changelog <path>] "Your changelog message"''');
stderr
.writeln('If no --changelog path is provided, CHANGELOG.md is used.');
return 1;
}

final message = args.join(' ');
final changelogFile =
File(argResults?['changelog'] as String? ?? 'CHANGELOG.md');

if (!changelogFile.existsSync()) {
stderr.writeln('Error: ${changelogFile.path} not found.');
return 1;
}

final newChangelog = updateChangelogContent(
changelogFile.readAsStringSync(),
message,
);
changelogFile.writeAsStringSync(newChangelog);
stdout.writeln('Changelog updated successfully.');
return 0;
}
}
2 changes: 2 additions & 0 deletions pkgs/repo_manage/lib/src/common.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:github/github.dart';
import 'package:graphql/client.dart';

import '../branches.dart';
import '../changelog_updater.dart';
import '../contributors.dart';
import '../issue_transfer.dart';
import '../labels.dart';
Expand Down Expand Up @@ -80,6 +81,7 @@ class ReportCommandRunner extends CommandRunner<int> {
: super('report',
'Run various reports on Dart and Flutter related repositories.') {
addCommand(BranchesCommand());
addCommand(ChangelogUpdaterCommand());
addCommand(ContributorsCommand());
addCommand(LabelsCommand());
addCommand(LabelsUpdateCommand());
Expand Down
5 changes: 5 additions & 0 deletions pkgs/repo_manage/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ description: Miscellaneous issue, repo, and PR query tools.

publish_to: none

executables:
report:


environment:
sdk: ^3.3.0

Expand All @@ -15,3 +19,4 @@ dependencies:

dev_dependencies:
dart_flutter_team_lints: ^3.0.0
test: ^1.25.0
66 changes: 66 additions & 0 deletions pkgs/repo_manage/test/changelog_updater_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import 'dart:io';

import 'package:repo_manage/changelog_updater.dart';
import 'package:test/test.dart';

void main() {
group('updateChangelogContent', () {
test('adds an entry to a wip changelog', () {
expectGolden(
inputPath: 'test/data/changelog_wip.md',
goldenPath: 'test/data/golden/changelog_wip.golden',
message: 'Add new feature',
);
});

test('creates a new wip section from a released changelog', () {
expectGolden(
inputPath: 'test/data/changelog_release.md',
goldenPath: 'test/data/golden/changelog_release.golden',
message: 'Fix regression',
);
});

test('creates a starter changelog when none is present', () {
expectGolden(
inputPath: 'test/data/changelog_empty.md',
goldenPath: 'test/data/golden/changelog_empty.golden',
message: 'Initial entry',
);
});

test('handles complex timezone formatting', () {
expectGolden(
inputPath: 'test/data/changelog_complex_timezone.md',
goldenPath: 'test/data/golden/changelog_complex_timezone.golden',
message: 'Add new feature',
);
});

test('handles complex timezone formatting with WIP suffix', () {
expectGolden(
inputPath: 'test/data/changelog_complex_timezone_wip.md',
goldenPath: 'test/data/golden/changelog_complex_timezone_wip.golden',
message: 'Add new feature',
);
});
});
}

void expectGolden({
required String inputPath,
required String goldenPath,
required String message,
}) {
final input = File(inputPath).readAsStringSync();
final output = updateChangelogContent(input, message);
final goldenFile = File(goldenPath);

if (!goldenFile.existsSync()) {
goldenFile.parent.createSync(recursive: true);
goldenFile.writeAsStringSync(output);
return;
}

expect(output, goldenFile.readAsStringSync());
}
Loading
Loading