| // 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 'dart:io' as io; |
| import 'dart:math'; |
| |
| import 'package:args/command_runner.dart'; |
| import 'package:file/file.dart'; |
| import 'package:git/git.dart'; |
| import 'package:path/path.dart' as p; |
| import 'package:platform/platform.dart'; |
| import 'package:yaml/yaml.dart'; |
| |
| import 'core.dart'; |
| import 'git_version_finder.dart'; |
| import 'output_utils.dart'; |
| import 'process_runner.dart'; |
| import 'repository_package.dart'; |
| |
| /// An entry in package enumeration for APIs that need to include extra |
| /// data about the entry. |
| class PackageEnumerationEntry { |
| /// Creates a new entry for the given package. |
| PackageEnumerationEntry(this.package, {required this.excluded}); |
| |
| /// The package this entry corresponds to. Be sure to check `excluded` before |
| /// using this, as having an entry does not necessarily mean that the package |
| /// should be included in the processing of the enumeration. |
| final RepositoryPackage package; |
| |
| /// Whether or not this package was excluded by the command invocation. |
| final bool excluded; |
| } |
| |
| /// Interface definition for all commands in this tool. |
| // TODO(stuartmorgan): Move most of this logic to PackageLoopingCommand. |
| abstract class PackageCommand extends Command<void> { |
| /// Creates a command to operate on [packagesDir] with the given environment. |
| PackageCommand( |
| this.packagesDir, { |
| this.processRunner = const ProcessRunner(), |
| this.platform = const LocalPlatform(), |
| GitDir? gitDir, |
| }) : _gitDir = gitDir { |
| thirdPartyPackagesDir = packagesDir.parent |
| .childDirectory('third_party') |
| .childDirectory('packages'); |
| |
| argParser.addMultiOption( |
| _packagesArg, |
| help: |
| 'Specifies which packages the command should run on (before sharding).\n', |
| valueHelp: 'package1,package2,...', |
| aliases: <String>[_pluginsLegacyAliasArg], |
| ); |
| argParser.addOption( |
| _shardIndexArg, |
| help: 'Specifies the zero-based index of the shard to ' |
| 'which the command applies.', |
| valueHelp: 'i', |
| defaultsTo: '0', |
| ); |
| argParser.addOption( |
| _shardCountArg, |
| help: 'Specifies the number of shards into which packages are divided.', |
| valueHelp: 'n', |
| defaultsTo: '1', |
| ); |
| argParser.addMultiOption( |
| _excludeArg, |
| abbr: 'e', |
| help: 'A list of packages to exclude from from this command.\n\n' |
| 'Alternately, a list of one or more YAML files that contain a list ' |
| 'of packages to exclude.', |
| defaultsTo: <String>[], |
| ); |
| argParser.addFlag(_runOnChangedPackagesArg, |
| negatable: false, |
| help: 'Run the command on changed packages.\n' |
| 'If no packages have changed, or if there have been changes that may\n' |
| 'affect all packages, the command runs on all packages.\n' |
| 'Packages excluded with $_excludeArg are excluded even if changed.\n' |
| 'See $_baseShaArg if a custom base is needed to determine the diff.\n\n' |
| 'Cannot be combined with $_packagesArg.\n'); |
| argParser.addFlag(_runOnDirtyPackagesArg, |
| negatable: false, |
| help: |
| 'Run the command on packages with changes that have not been committed.\n' |
| 'Packages excluded with $_excludeArg are excluded even if changed.\n' |
| 'Cannot be combined with $_packagesArg.\n', |
| hide: true); |
| argParser.addFlag(_packagesForBranchArg, |
| negatable: false, |
| help: 'This runs on all packages changed in the last commit on main ' |
| '(or master), and behaves like --run-on-changed-packages on ' |
| 'any other branch.\n\n' |
| 'Cannot be combined with $_packagesArg.\n\n' |
| 'This is intended for use in CI.\n', |
| hide: true); |
| argParser.addFlag(_currentPackageArg, |
| negatable: false, |
| help: |
| 'Set the target package(s) based on the current working directory.\n' |
| '- If the current working directory is (or is inside) a package, ' |
| 'that package will be targeted.\n' |
| '- If the current working directory is the root of a federated ' |
| 'plugin group, that group will be targeted.\n' |
| 'Cannot be combined with $_packagesArg.\n'); |
| argParser.addOption(_baseShaArg, |
| help: 'The base sha used to determine git diff. \n' |
| 'This is useful when $_runOnChangedPackagesArg is specified.\n' |
| 'If not specified, merge-base is used as base sha.'); |
| argParser.addOption(_baseBranchArg, |
| help: 'The base branch whose merge base is used as the base SHA if ' |
| '--$_baseShaArg is not provided. \n' |
| 'If not specified, FETCH_HEAD is used as the base branch.'); |
| argParser.addFlag(_logTimingArg, |
| help: 'Logs timing information.\n\n' |
| 'Currently only logs per-package timing for multi-package commands, ' |
| 'but more information may be added in the future.'); |
| } |
| |
| // Package selection. |
| static const String _packagesArg = 'packages'; |
| static const String _packagesForBranchArg = 'packages-for-branch'; |
| static const String _currentPackageArg = 'current-package'; |
| static const String _pluginsLegacyAliasArg = 'plugins'; |
| static const String _runOnChangedPackagesArg = 'run-on-changed-packages'; |
| static const String _runOnDirtyPackagesArg = 'run-on-dirty-packages'; |
| static const String _excludeArg = 'exclude'; |
| // Diff base selection. |
| static const String _baseBranchArg = 'base-branch'; |
| static const String _baseShaArg = 'base-sha'; |
| // Sharding. |
| static const String _shardCountArg = 'shardCount'; |
| static const String _shardIndexArg = 'shardIndex'; |
| // Utility. |
| static const String _logTimingArg = 'log-timing'; |
| |
| /// The directory containing the packages. |
| final Directory packagesDir; |
| |
| /// The directory containing packages wrapping third-party code. |
| late Directory thirdPartyPackagesDir; |
| |
| /// The process runner. |
| /// |
| /// This can be overridden for testing. |
| final ProcessRunner processRunner; |
| |
| /// The current platform. |
| /// |
| /// This can be overridden for testing. |
| final Platform platform; |
| |
| /// The git directory to use. If unset, [gitDir] populates it from the |
| /// packages directory's enclosing repository. |
| /// |
| /// This can be mocked for testing. |
| GitDir? _gitDir; |
| |
| int? _shardIndex; |
| int? _shardCount; |
| |
| // Cached set of explicitly excluded packages. |
| Set<String>? _excludedPackages; |
| |
| /// A context that matches the default for [platform]. |
| p.Context get path => platform.isWindows ? p.windows : p.posix; |
| |
| /// The command to use when running `flutter`. |
| String get flutterCommand => platform.isWindows ? 'flutter.bat' : 'flutter'; |
| |
| /// The shard of the overall command execution that this instance should run. |
| int get shardIndex { |
| if (_shardIndex == null) { |
| _checkSharding(); |
| } |
| return _shardIndex!; |
| } |
| |
| /// The number of shards this command is divided into. |
| int get shardCount { |
| if (_shardCount == null) { |
| _checkSharding(); |
| } |
| return _shardCount!; |
| } |
| |
| /// Returns the [GitDir] containing [packagesDir]. |
| Future<GitDir> get gitDir async { |
| GitDir? gitDir = _gitDir; |
| if (gitDir != null) { |
| return gitDir; |
| } |
| |
| // Ensure there are no symlinks in the path, as it can break |
| // GitDir's allowSubdirectory:true. |
| final String packagesPath = packagesDir.resolveSymbolicLinksSync(); |
| if (!await GitDir.isGitDir(packagesPath)) { |
| printError('$packagesPath is not a valid Git repository.'); |
| throw ToolExit(2); |
| } |
| gitDir = |
| await GitDir.fromExisting(packagesDir.path, allowSubdirectory: true); |
| _gitDir = gitDir; |
| return gitDir; |
| } |
| |
| /// Convenience accessor for boolean arguments. |
| bool getBoolArg(String key) { |
| return (argResults![key] as bool?) ?? false; |
| } |
| |
| /// Convenience accessor for String arguments. |
| String getStringArg(String key) { |
| return (argResults![key] as String?) ?? ''; |
| } |
| |
| /// Convenience accessor for String arguments. |
| String? getNullableStringArg(String key) { |
| return argResults![key] as String?; |
| } |
| |
| /// Convenience accessor for List<String> arguments. |
| List<String> getStringListArg(String key) { |
| // Clone the list so that if a caller modifies the result it won't change |
| // the actual arguments list for future queries. |
| return List<String>.from(argResults![key] as List<String>? ?? <String>[]); |
| } |
| |
| /// If true, commands should log timing information that might be useful in |
| /// analyzing their runtime (e.g., the per-package time for multi-package |
| /// commands). |
| bool get shouldLogTiming => getBoolArg(_logTimingArg); |
| |
| void _checkSharding() { |
| final int? shardIndex = int.tryParse(getStringArg(_shardIndexArg)); |
| final int? shardCount = int.tryParse(getStringArg(_shardCountArg)); |
| if (shardIndex == null) { |
| usageException('$_shardIndexArg must be an integer'); |
| } |
| if (shardCount == null) { |
| usageException('$_shardCountArg must be an integer'); |
| } |
| if (shardCount < 1) { |
| usageException('$_shardCountArg must be positive'); |
| } |
| if (shardIndex < 0 || shardCount <= shardIndex) { |
| usageException( |
| '$_shardIndexArg must be in the half-open range [0..$shardCount['); |
| } |
| _shardIndex = shardIndex; |
| _shardCount = shardCount; |
| } |
| |
| /// Returns the set of packages to exclude based on the `--exclude` argument. |
| Set<String> getExcludedPackageNames() { |
| final Set<String> excludedPackages = _excludedPackages ?? |
| getStringListArg(_excludeArg).expand<String>((String item) { |
| if (item.endsWith('.yaml')) { |
| final File file = packagesDir.fileSystem.file(item); |
| return (loadYaml(file.readAsStringSync()) as YamlList) |
| .toList() |
| .cast<String>(); |
| } |
| return <String>[item]; |
| }).toSet(); |
| // Cache for future calls. |
| _excludedPackages = excludedPackages; |
| return excludedPackages; |
| } |
| |
| /// Returns the root diretories of the packages involved in this command |
| /// execution. |
| /// |
| /// Depending on the command arguments, this may be a user-specified set of |
| /// packages, the set of packages that should be run for a given diff, or all |
| /// packages. |
| /// |
| /// By default, packages excluded via --exclude will not be in the stream, but |
| /// they can be included by passing false for [filterExcluded]. |
| Stream<PackageEnumerationEntry> getTargetPackages( |
| {bool filterExcluded = true}) async* { |
| // To avoid assuming consistency of `Directory.list` across command |
| // invocations, we collect and sort the package folders before sharding. |
| // This is considered an implementation detail which is why the API still |
| // uses streams. |
| final List<PackageEnumerationEntry> allPackages = |
| await _getAllPackages().toList(); |
| allPackages.sort((PackageEnumerationEntry p1, PackageEnumerationEntry p2) => |
| p1.package.path.compareTo(p2.package.path)); |
| final int shardSize = allPackages.length ~/ shardCount + |
| (allPackages.length % shardCount == 0 ? 0 : 1); |
| final int start = min(shardIndex * shardSize, allPackages.length); |
| final int end = min(start + shardSize, allPackages.length); |
| |
| for (final PackageEnumerationEntry package |
| in allPackages.sublist(start, end)) { |
| if (!(filterExcluded && package.excluded)) { |
| yield package; |
| } |
| } |
| } |
| |
| /// Returns the root Dart package folders of the packages involved in this |
| /// command execution, assuming there is only one shard. Depending on the |
| /// command arguments, this may be a user-specified set of packages, the |
| /// set of packages that should be run for a given diff, or all packages. |
| /// |
| /// This will return packages that have been excluded by the --exclude |
| /// parameter, annotated in the entry as excluded. |
| /// |
| /// Packages can exist in the following places relative to the packages |
| /// directory: |
| /// |
| /// 1. As a Dart package in a directory which is a direct child of the |
| /// packages directory. This is a non-plugin package, or a non-federated |
| /// plugin. |
| /// 2. Several plugin packages may live in a directory which is a direct |
| /// child of the packages directory. This directory groups several Dart |
| /// packages which implement a single plugin. This directory contains an |
| /// "app-facing" package which declares the API for the plugin, a |
| /// platform interface package which declares the API for implementations, |
| /// and one or more platform-specific implementation packages. |
| /// 3./4. Either of the above, but in a third_party/packages/ directory that |
| /// is a sibling of the packages directory. This is used for a small number |
| /// of packages in the flutter/packages repository. |
| Stream<PackageEnumerationEntry> _getAllPackages() async* { |
| final Set<String> packageSelectionFlags = <String>{ |
| _packagesArg, |
| _runOnChangedPackagesArg, |
| _runOnDirtyPackagesArg, |
| _packagesForBranchArg, |
| _currentPackageArg, |
| }; |
| if (packageSelectionFlags |
| .where((String flag) => argResults!.wasParsed(flag)) |
| .length > |
| 1) { |
| printError('Only one of the package selection arguments ' |
| '(${packageSelectionFlags.join(", ")}) ' |
| 'can be provided.'); |
| throw ToolExit(exitInvalidArguments); |
| } |
| |
| Set<String> packages = Set<String>.from(getStringListArg(_packagesArg)); |
| |
| final GitVersionFinder? changedFileFinder; |
| if (getBoolArg(_runOnChangedPackagesArg)) { |
| changedFileFinder = await retrieveVersionFinder(); |
| } else if (getBoolArg(_packagesForBranchArg)) { |
| final String? branch = await _getBranch(); |
| if (branch == null) { |
| printError('Unable to determine branch; --$_packagesForBranchArg can ' |
| 'only be used in a git repository.'); |
| throw ToolExit(exitInvalidArguments); |
| } else { |
| // Configure the change finder the correct mode for the branch. |
| // Log the mode to make it easier to audit logs to see that the |
| // intended diff was used (or why). |
| final bool lastCommitOnly; |
| if (branch == 'main' || branch == 'master') { |
| print('--$_packagesForBranchArg: running on default branch.'); |
| lastCommitOnly = true; |
| } else if (await _isCheckoutFromBranch('main')) { |
| print( |
| '--$_packagesForBranchArg: running on a commit from default branch.'); |
| lastCommitOnly = true; |
| } else { |
| print('--$_packagesForBranchArg: running on branch "$branch".'); |
| lastCommitOnly = false; |
| } |
| if (lastCommitOnly) { |
| print( |
| '--$_packagesForBranchArg: using parent commit as the diff base.'); |
| changedFileFinder = GitVersionFinder(await gitDir, baseSha: 'HEAD~'); |
| } else { |
| changedFileFinder = await retrieveVersionFinder(); |
| } |
| } |
| } else { |
| changedFileFinder = null; |
| } |
| |
| if (changedFileFinder != null) { |
| final String baseSha = await changedFileFinder.getBaseSha(); |
| final List<String> changedFiles = |
| await changedFileFinder.getChangedFiles(); |
| if (_changesRequireFullTest(changedFiles)) { |
| print('Running for all packages, since a file has changed that could ' |
| 'affect the entire repository.'); |
| } else { |
| print( |
| 'Running for all packages that have diffs relative to "$baseSha"\n'); |
| packages = _getChangedPackageNames(changedFiles); |
| } |
| } else if (getBoolArg(_runOnDirtyPackagesArg)) { |
| final GitVersionFinder gitVersionFinder = |
| GitVersionFinder(await gitDir, baseSha: 'HEAD'); |
| print('Running for all packages that have uncommitted changes\n'); |
| // _changesRequireFullTest is deliberately not used here, as this flag is |
| // intended for use in CI to re-test packages changed by |
| // 'make-deps-path-based'. |
| packages = _getChangedPackageNames( |
| await gitVersionFinder.getChangedFiles(includeUncommitted: true)); |
| // For the same reason, empty is not treated as "all packages" as it is |
| // for other flags. |
| if (packages.isEmpty) { |
| return; |
| } |
| } else if (getBoolArg(_currentPackageArg)) { |
| final String? currentPackageName = _getCurrentDirectoryPackageName(); |
| if (currentPackageName == null) { |
| printError('Unable to determine packages; --$_currentPackageArg can ' |
| 'only be used within a repository package or package group.'); |
| throw ToolExit(exitInvalidArguments); |
| } |
| packages = <String>{currentPackageName}; |
| } |
| |
| final Set<String> excludedPackageNames = getExcludedPackageNames(); |
| for (final Directory dir in <Directory>[ |
| packagesDir, |
| if (thirdPartyPackagesDir.existsSync()) thirdPartyPackagesDir, |
| ]) { |
| await for (final FileSystemEntity entity |
| in dir.list(followLinks: false)) { |
| // A top-level Dart package is a standard package. |
| if (isPackage(entity)) { |
| if (packages.isEmpty || packages.contains(p.basename(entity.path))) { |
| yield PackageEnumerationEntry( |
| RepositoryPackage(entity as Directory), |
| excluded: excludedPackageNames.contains(entity.basename)); |
| } |
| } else if (entity is Directory) { |
| // Look for Dart packages under this top-level directory; this is the |
| // standard structure for federated plugins. |
| await for (final FileSystemEntity subdir |
| in entity.list(followLinks: false)) { |
| if (isPackage(subdir)) { |
| // There are three ways for a federated plugin to match: |
| // - package name (path_provider_android) |
| // - fully specified name (path_provider/path_provider_android) |
| // - group name (path_provider), which matches all packages in |
| // the group |
| final Set<String> possibleMatches = <String>{ |
| path.basename(subdir.path), // package name |
| path.basename(entity.path), // group name |
| path.relative(subdir.path, from: dir.path), // fully specified |
| }; |
| if (packages.isEmpty || |
| packages.intersection(possibleMatches).isNotEmpty) { |
| yield PackageEnumerationEntry( |
| RepositoryPackage(subdir as Directory), |
| excluded: excludedPackageNames |
| .intersection(possibleMatches) |
| .isNotEmpty); |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| /// Returns all Dart package folders (typically, base package + example) of |
| /// the packages involved in this command execution. |
| /// |
| /// By default, packages excluded via --exclude will not be in the stream, but |
| /// they can be included by passing false for [filterExcluded]. |
| /// |
| /// Subpackages are guaranteed to be after the containing package in the |
| /// stream. |
| Stream<PackageEnumerationEntry> getTargetPackagesAndSubpackages( |
| {bool filterExcluded = true}) async* { |
| await for (final PackageEnumerationEntry package |
| in getTargetPackages(filterExcluded: filterExcluded)) { |
| yield package; |
| yield* getSubpackages(package.package).map( |
| (RepositoryPackage subPackage) => |
| PackageEnumerationEntry(subPackage, excluded: package.excluded)); |
| } |
| } |
| |
| /// Returns all Dart package folders (e.g., examples) under the given package. |
| Stream<RepositoryPackage> getSubpackages(RepositoryPackage package, |
| {bool filterExcluded = true}) async* { |
| yield* package.directory |
| .list(recursive: true, followLinks: false) |
| .where(isPackage) |
| .map((FileSystemEntity directory) => |
| // isPackage guarantees that this cast is valid. |
| RepositoryPackage(directory as Directory)); |
| } |
| |
| /// Returns the files contained, recursively, within the packages |
| /// involved in this command execution. |
| Stream<File> getFiles() { |
| return getTargetPackages().asyncExpand<File>( |
| (PackageEnumerationEntry entry) => getFilesForPackage(entry.package)); |
| } |
| |
| /// Returns the files contained, recursively, within [package]. |
| Stream<File> getFilesForPackage(RepositoryPackage package) { |
| return package.directory |
| .list(recursive: true, followLinks: false) |
| .where((FileSystemEntity entity) => entity is File) |
| .cast<File>(); |
| } |
| |
| /// Retrieve an instance of [GitVersionFinder] based on `_baseShaArg` and [gitDir]. |
| /// |
| /// Throws tool exit if [gitDir] nor root directory is a git directory. |
| Future<GitVersionFinder> retrieveVersionFinder() async { |
| final String? baseSha = getNullableStringArg(_baseShaArg); |
| final String? baseBranch = |
| baseSha == null ? getNullableStringArg(_baseBranchArg) : null; |
| |
| final GitVersionFinder gitVersionFinder = GitVersionFinder(await gitDir, |
| baseSha: baseSha, baseBranch: baseBranch); |
| return gitVersionFinder; |
| } |
| |
| // Returns the names of packages that have been changed given a list of |
| // changed files. |
| // |
| // The names will either be the actual package names, or potentially |
| // group/name specifiers (for example, path_provider/path_provider) for |
| // packages in federated plugins. |
| // |
| // The paths must use POSIX separators (e.g., as provided by git output). |
| Set<String> _getChangedPackageNames(List<String> changedFiles) { |
| final Set<String> packages = <String>{}; |
| |
| // A helper function that returns true if candidatePackageName looks like an |
| // implementation package of a plugin called pluginName. Used to determine |
| // if .../packages/parentName/candidatePackageName/... |
| // looks like a path in a federated plugin package (candidatePackageName) |
| // rather than a top-level package (parentName). |
| bool isFederatedPackage(String candidatePackageName, String parentName) { |
| return candidatePackageName == parentName || |
| candidatePackageName.startsWith('${parentName}_'); |
| } |
| |
| for (final String path in changedFiles) { |
| final List<String> pathComponents = p.posix.split(path); |
| final int packagesIndex = |
| pathComponents.indexWhere((String element) => element == 'packages'); |
| if (packagesIndex != -1) { |
| // Find the name of the directory directly under packages. This is |
| // either the name of the package, or a plugin group directory for |
| // a federated plugin. |
| final String topLevelName = pathComponents[packagesIndex + 1]; |
| String packageName = topLevelName; |
| if (packagesIndex + 2 < pathComponents.length && |
| isFederatedPackage( |
| pathComponents[packagesIndex + 2], topLevelName)) { |
| // This looks like a federated package; use the full specifier if |
| // the name would be ambiguous (i.e., for the app-facing package). |
| packageName = pathComponents[packagesIndex + 2]; |
| if (packageName == topLevelName) { |
| packageName = '$topLevelName/$packageName'; |
| } |
| } |
| packages.add(packageName); |
| } |
| } |
| if (packages.isEmpty) { |
| print('No changed packages.'); |
| } else { |
| final String changedPackages = packages.join(','); |
| print('Changed packages: $changedPackages'); |
| } |
| return packages; |
| } |
| |
| String? _getCurrentDirectoryPackageName() { |
| // Ensure that the current directory is within the packages directory. |
| final Directory absolutePackagesDir = packagesDir.absolute; |
| Directory currentDir = packagesDir.fileSystem.currentDirectory.absolute; |
| if (!currentDir.path.startsWith(absolutePackagesDir.path) || |
| currentDir.path == packagesDir.path) { |
| return null; |
| } |
| // If the current directory is a direct subdirectory of the packages |
| // directory, then that's the target. |
| if (currentDir.parent.path == absolutePackagesDir.path) { |
| return currentDir.basename; |
| } |
| // Otherwise, walk up until a package is found... |
| while (!isPackage(currentDir)) { |
| currentDir = currentDir.parent; |
| if (currentDir.path == absolutePackagesDir.path) { |
| return null; |
| } |
| } |
| // ... and then check whether it has an enclosing package. |
| final RepositoryPackage package = RepositoryPackage(currentDir); |
| final RepositoryPackage? enclosingPackage = package.getEnclosingPackage(); |
| final RepositoryPackage rootPackage = enclosingPackage ?? package; |
| final String name = rootPackage.directory.basename; |
| // For an app-facing package in a federated plugin, return the fully |
| // qualified name, since returning just the name will cause the entire |
| // group to run. |
| if (rootPackage.directory.parent.basename == name) { |
| return '$name/$name'; |
| } |
| return name; |
| } |
| |
| // Returns true if the current checkout is on an ancestor of [branch]. |
| // |
| // This is used because CI may check out a specific hash rather than a branch, |
| // in which case branch-name detection won't work. |
| Future<bool> _isCheckoutFromBranch(String branchName) async { |
| // The target branch may not exist locally; try some common remote names for |
| // the branch as well. |
| final List<String> candidateBranchNames = <String>[ |
| branchName, |
| 'origin/$branchName', |
| 'upstream/$branchName', |
| ]; |
| for (final String branch in candidateBranchNames) { |
| final io.ProcessResult result = await (await gitDir).runCommand( |
| <String>['merge-base', '--is-ancestor', 'HEAD', branch], |
| throwOnError: false); |
| if (result.exitCode == 0) { |
| return true; |
| } else if (result.exitCode == 1) { |
| // 1 indicates that the branch was successfully checked, but it's not |
| // an ancestor. |
| return false; |
| } |
| // Any other return code is an error, such as `branch` not being a valid |
| // name in the repository, so try other name variants. |
| } |
| return false; |
| } |
| |
| Future<String?> _getBranch() async { |
| final io.ProcessResult branchResult = await (await gitDir).runCommand( |
| <String>['rev-parse', '--abbrev-ref', 'HEAD'], |
| throwOnError: false); |
| if (branchResult.exitCode != 0) { |
| return null; |
| } |
| return (branchResult.stdout as String).trim(); |
| } |
| |
| // Returns true if one or more files changed that have the potential to affect |
| // any packages (e.g., CI script changes). |
| bool _changesRequireFullTest(List<String> changedFiles) { |
| const List<String> specialFiles = <String>[ |
| '.ci.yaml', // LUCI config. |
| '.cirrus.yml', // Cirrus config. |
| '.clang-format', // ObjC and C/C++ formatting options. |
| 'analysis_options.yaml', // Dart analysis settings. |
| ]; |
| const List<String> specialDirectories = <String>[ |
| '.ci/', // Support files for CI. |
| 'script/', // This tool, and its wrapper scripts. |
| ]; |
| // Directory entries must end with / to avoid over-matching, since the |
| // check below is done via string prefixing. |
| assert(specialDirectories.every((String dir) => dir.endsWith('/'))); |
| |
| return changedFiles.any((String path) => |
| specialFiles.contains(path) || |
| specialDirectories.any((String dir) => path.startsWith(dir))); |
| } |
| } |