mirror of
https://github.com/flutter/packages.git
synced 2025-06-17 20:19:14 +08:00
311 lines
11 KiB
Dart
311 lines
11 KiB
Dart
// Copyright 2013 The Flutter Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
import 'package:args/command_runner.dart';
|
|
import 'package:file/file.dart';
|
|
import 'package:git/git.dart';
|
|
import 'package:pub_semver/pub_semver.dart';
|
|
import 'package:yaml_edit/yaml_edit.dart';
|
|
|
|
import 'common/core.dart';
|
|
import 'common/git_version_finder.dart';
|
|
import 'common/package_looping_command.dart';
|
|
import 'common/package_state_utils.dart';
|
|
import 'common/repository_package.dart';
|
|
|
|
/// Supported version change types, from smallest to largest component.
|
|
enum _VersionIncrementType { build, bugfix, minor }
|
|
|
|
/// Possible results of attempting to update a CHANGELOG.md file.
|
|
enum _ChangelogUpdateOutcome { addedSection, updatedSection, failed }
|
|
|
|
/// A state machine for the process of updating a CHANGELOG.md.
|
|
enum _ChangelogUpdateState {
|
|
/// Looking for the first version section.
|
|
findingFirstSection,
|
|
|
|
/// Looking for the first list entry in an existing section.
|
|
findingFirstListItem,
|
|
|
|
/// Finished with updates.
|
|
finishedUpdating,
|
|
}
|
|
|
|
/// A command to update the changelog, and optionally version, of packages.
|
|
class UpdateReleaseInfoCommand extends PackageLoopingCommand {
|
|
/// Creates a publish metadata updater command instance.
|
|
UpdateReleaseInfoCommand(
|
|
Directory packagesDir, {
|
|
GitDir? gitDir,
|
|
}) : super(packagesDir, gitDir: gitDir) {
|
|
argParser.addOption(_changelogFlag,
|
|
mandatory: true,
|
|
help: 'The changelog entry to add. '
|
|
'Each line will be a separate list entry.');
|
|
argParser.addOption(_versionTypeFlag,
|
|
mandatory: true,
|
|
help: 'The version change level',
|
|
allowed: <String>[
|
|
_versionNext,
|
|
_versionMinimal,
|
|
_versionBugfix,
|
|
_versionMinor,
|
|
],
|
|
allowedHelp: <String, String>{
|
|
_versionNext:
|
|
'No version change; just adds a NEXT entry to the changelog.',
|
|
_versionBugfix: 'Increments the bugfix version.',
|
|
_versionMinor: 'Increments the minor version.',
|
|
_versionMinimal: 'Depending on the changes to each package: '
|
|
'increments the bugfix version (for publishable changes), '
|
|
"uses NEXT (for changes that don't need to be published), "
|
|
'or skips (if no changes).',
|
|
});
|
|
}
|
|
|
|
static const String _changelogFlag = 'changelog';
|
|
static const String _versionTypeFlag = 'version';
|
|
|
|
static const String _versionNext = 'next';
|
|
static const String _versionBugfix = 'bugfix';
|
|
static const String _versionMinor = 'minor';
|
|
static const String _versionMinimal = 'minimal';
|
|
|
|
// The version change type, if there is a set type for all platforms.
|
|
//
|
|
// If null, either there is no version change, or it is dynamic (`minimal`).
|
|
_VersionIncrementType? _versionChange;
|
|
|
|
// The cache of changed files, for dynamic version change determination.
|
|
//
|
|
// Only set for `minimal` version change.
|
|
late final List<String> _changedFiles;
|
|
|
|
@override
|
|
final String name = 'update-release-info';
|
|
|
|
@override
|
|
final String description = 'Updates CHANGELOG.md files, and optionally the '
|
|
'version in pubspec.yaml, in a way that is consistent with version-check '
|
|
'enforcement.';
|
|
|
|
@override
|
|
bool get hasLongOutput => false;
|
|
|
|
@override
|
|
Future<void> initializeRun() async {
|
|
if (getStringArg(_changelogFlag).trim().isEmpty) {
|
|
throw UsageException('Changelog message must not be empty.', usage);
|
|
}
|
|
switch (getStringArg(_versionTypeFlag)) {
|
|
case _versionMinor:
|
|
_versionChange = _VersionIncrementType.minor;
|
|
break;
|
|
case _versionBugfix:
|
|
_versionChange = _VersionIncrementType.bugfix;
|
|
break;
|
|
case _versionMinimal:
|
|
final GitVersionFinder gitVersionFinder = await retrieveVersionFinder();
|
|
_changedFiles = await gitVersionFinder.getChangedFiles();
|
|
// Anothing other than a fixed change is null.
|
|
_versionChange = null;
|
|
break;
|
|
case _versionNext:
|
|
_versionChange = null;
|
|
break;
|
|
default:
|
|
throw UnimplementedError('Unimplemented version change type');
|
|
}
|
|
}
|
|
|
|
@override
|
|
Future<PackageResult> runForPackage(RepositoryPackage package) async {
|
|
String nextVersionString;
|
|
|
|
_VersionIncrementType? versionChange = _versionChange;
|
|
|
|
// If the change type is `minimal` determine what changes, if any, are
|
|
// needed.
|
|
if (versionChange == null &&
|
|
getStringArg(_versionTypeFlag) == _versionMinimal) {
|
|
final Directory gitRoot =
|
|
packagesDir.fileSystem.directory((await gitDir).path);
|
|
final String relativePackagePath =
|
|
getRelativePosixPath(package.directory, from: gitRoot);
|
|
final PackageChangeState state = await checkPackageChangeState(package,
|
|
changedPaths: _changedFiles,
|
|
relativePackagePath: relativePackagePath);
|
|
|
|
if (!state.hasChanges) {
|
|
return PackageResult.skip('No changes to package');
|
|
}
|
|
if (state.needsVersionChange) {
|
|
versionChange = _VersionIncrementType.bugfix;
|
|
}
|
|
}
|
|
|
|
if (versionChange != null) {
|
|
final Version? updatedVersion =
|
|
_updatePubspecVersion(package, versionChange);
|
|
if (updatedVersion == null) {
|
|
return PackageResult.fail(
|
|
<String>['Could not determine current version.']);
|
|
}
|
|
nextVersionString = updatedVersion.toString();
|
|
print('${indentation}Incremented version to $nextVersionString.');
|
|
} else {
|
|
nextVersionString = 'NEXT';
|
|
}
|
|
|
|
final _ChangelogUpdateOutcome updateOutcome =
|
|
_updateChangelog(package, nextVersionString);
|
|
switch (updateOutcome) {
|
|
case _ChangelogUpdateOutcome.addedSection:
|
|
print('${indentation}Added a $nextVersionString section.');
|
|
break;
|
|
case _ChangelogUpdateOutcome.updatedSection:
|
|
print('${indentation}Updated NEXT section.');
|
|
break;
|
|
case _ChangelogUpdateOutcome.failed:
|
|
return PackageResult.fail(<String>['Could not update CHANGELOG.md.']);
|
|
}
|
|
|
|
return PackageResult.success();
|
|
}
|
|
|
|
_ChangelogUpdateOutcome _updateChangelog(
|
|
RepositoryPackage package, String version) {
|
|
if (!package.changelogFile.existsSync()) {
|
|
printError('${indentation}Missing CHANGELOG.md.');
|
|
return _ChangelogUpdateOutcome.failed;
|
|
}
|
|
|
|
final String newHeader = '## $version';
|
|
final RegExp listItemPattern = RegExp(r'^(\s*[-*])');
|
|
|
|
final StringBuffer newChangelog = StringBuffer();
|
|
_ChangelogUpdateState state = _ChangelogUpdateState.findingFirstSection;
|
|
bool updatedExistingSection = false;
|
|
|
|
for (final String line in package.changelogFile.readAsLinesSync()) {
|
|
switch (state) {
|
|
case _ChangelogUpdateState.findingFirstSection:
|
|
final String trimmedLine = line.trim();
|
|
if (trimmedLine.isEmpty) {
|
|
// Discard any whitespace at the top of the file.
|
|
} else if (trimmedLine == '## NEXT') {
|
|
// Replace the header with the new version (which may also be NEXT).
|
|
newChangelog.writeln(newHeader);
|
|
// Find the existing list to add to.
|
|
state = _ChangelogUpdateState.findingFirstListItem;
|
|
} else {
|
|
// The first content in the file isn't a NEXT section, so just add
|
|
// the new section.
|
|
<String>[
|
|
newHeader,
|
|
'',
|
|
..._changelogAdditionsAsList(),
|
|
'',
|
|
line, // Don't drop the current line.
|
|
].forEach(newChangelog.writeln);
|
|
state = _ChangelogUpdateState.finishedUpdating;
|
|
}
|
|
break;
|
|
case _ChangelogUpdateState.findingFirstListItem:
|
|
final RegExpMatch? match = listItemPattern.firstMatch(line);
|
|
if (match != null) {
|
|
final String listMarker = match[1]!;
|
|
// Add the new items on top. If the new change is changing the
|
|
// version, then the new item should be more relevant to package
|
|
// clients than anything that was already there. If it's still
|
|
// NEXT, the order doesn't matter.
|
|
<String>[
|
|
..._changelogAdditionsAsList(listMarker: listMarker),
|
|
line, // Don't drop the current line.
|
|
].forEach(newChangelog.writeln);
|
|
state = _ChangelogUpdateState.finishedUpdating;
|
|
updatedExistingSection = true;
|
|
} else if (line.trim().isEmpty) {
|
|
// Scan past empty lines, but keep them.
|
|
newChangelog.writeln(line);
|
|
} else {
|
|
printError(' Existing NEXT section has unrecognized format.');
|
|
return _ChangelogUpdateOutcome.failed;
|
|
}
|
|
break;
|
|
case _ChangelogUpdateState.finishedUpdating:
|
|
// Once changes are done, add the rest of the lines as-is.
|
|
newChangelog.writeln(line);
|
|
break;
|
|
}
|
|
}
|
|
|
|
package.changelogFile.writeAsStringSync(newChangelog.toString());
|
|
|
|
return updatedExistingSection
|
|
? _ChangelogUpdateOutcome.updatedSection
|
|
: _ChangelogUpdateOutcome.addedSection;
|
|
}
|
|
|
|
/// Returns the changelog to add as a Markdown list, using the given list
|
|
/// bullet style (default to the repository standard of '*'), and adding
|
|
/// any missing periods.
|
|
///
|
|
/// E.g., 'A line\nAnother line.' will become:
|
|
/// ```
|
|
/// [ '* A line.', '* Another line.' ]
|
|
/// ```
|
|
Iterable<String> _changelogAdditionsAsList({String listMarker = '*'}) {
|
|
return getStringArg(_changelogFlag).split('\n').map((String entry) {
|
|
String standardizedEntry = entry.trim();
|
|
if (!standardizedEntry.endsWith('.')) {
|
|
standardizedEntry = '$standardizedEntry.';
|
|
}
|
|
return '$listMarker $standardizedEntry';
|
|
});
|
|
}
|
|
|
|
/// Updates the version in [package]'s pubspec according to [type], returning
|
|
/// the new version, or null if there was an error updating the version.
|
|
Version? _updatePubspecVersion(
|
|
RepositoryPackage package, _VersionIncrementType type) {
|
|
final Pubspec pubspec = package.parsePubspec();
|
|
final Version? currentVersion = pubspec.version;
|
|
if (currentVersion == null) {
|
|
printError('${indentation}No version in pubspec.yaml');
|
|
return null;
|
|
}
|
|
|
|
// For versions less than 1.0, shift the change down one component per
|
|
// Dart versioning conventions.
|
|
final _VersionIncrementType adjustedType = currentVersion.major > 0
|
|
? type
|
|
: _VersionIncrementType.values[type.index - 1];
|
|
|
|
final Version newVersion = _nextVersion(currentVersion, adjustedType);
|
|
|
|
// Write the new version to the pubspec.
|
|
final YamlEditor editablePubspec =
|
|
YamlEditor(package.pubspecFile.readAsStringSync());
|
|
editablePubspec.update(<String>['version'], newVersion.toString());
|
|
package.pubspecFile.writeAsStringSync(editablePubspec.toString());
|
|
|
|
return newVersion;
|
|
}
|
|
|
|
Version _nextVersion(Version version, _VersionIncrementType type) {
|
|
switch (type) {
|
|
case _VersionIncrementType.minor:
|
|
return version.nextMinor;
|
|
case _VersionIncrementType.bugfix:
|
|
return version.nextPatch;
|
|
case _VersionIncrementType.build:
|
|
final int buildNumber =
|
|
version.build.isEmpty ? 0 : version.build.first as int;
|
|
return Version(version.major, version.minor, version.patch,
|
|
build: '${buildNumber + 1}');
|
|
}
|
|
}
|
|
}
|