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
156 changes: 156 additions & 0 deletions tool/lib/commands/presubmit.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// Copyright 2026 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.

import 'dart:async';
import 'dart:io';

import 'package:args/command_runner.dart';
import 'package:cli_util/cli_logging.dart';
import 'package:io/io.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;

import '../model.dart';
import '../utils.dart';

class PresubmitCommand extends Command {
PresubmitCommand({@visibleForTesting this.processManager}) {
argParser.addFlag(
'fix',
help: 'Apply dart fixes and formatting.',
defaultsTo: false,
negatable: false,
);
}

ProcessManager? processManager;

@override
String get name => 'presubmit';

@override
String get description =>
'Run repo checks, analysis, fix, and format on all packages.';

@override
Future run() async {
final log = Logger.standard();
final repo = DevToolsRepo.getInstance();
final pm = processManager ?? ProcessManager();
final fix = argResults!['fix'] as bool;

log.stdout('Running pub get...');
final pubGetResult = await runner?.run(['pub-get']);
if (pubGetResult is int && pubGetResult != 0) {
log.stderr('Pub get failed. Exiting early.');
return 1;
}

final packages = repo.getPackages(includeSubdirectories: false);
int failureCount = 0;

if (fix) {
log.stdout('Running Dart Fix and Format...');
for (final p in packages) {
if (!p.hasAnyDartCode) continue;

final progress = log.progress(' ${p.relativePath}');

final fixProcess = await pm.runProcess(
CliCommand.dart(['fix', '--apply'], throwOnException: false),
workingDirectory: p.packagePath,
);

final pathsToFormat = _getPathsToFormat(p);

final formatProcess = await pm.runProcess(
CliCommand.dart(['format', ...pathsToFormat], throwOnException: false),
workingDirectory: p.packagePath,
);

if (fixProcess.exitCode == 0 && formatProcess.exitCode == 0) {
progress.finish(showTiming: true);
} else {
failureCount++;
progress.finish(message: 'failed');
}
}

if (failureCount > 0) {
log.stderr('Presubmit failed.');
log.stderr(' Fix or Format failed on $failureCount packages.');
return 1;
}
}

log.stdout('Running Repo Check...');
final repoCheckResult = await runner?.run(['repo-check']);
if (repoCheckResult is int && repoCheckResult != 0) {
log.stderr('Repo checks failed. Exiting early.');
return 1;
}

log.stdout('Running Analyze...');
final analyzeResult = await runner?.run(['analyze']);
if (analyzeResult is int && analyzeResult != 0) {
log.stderr('Analysis failed. Exiting early.');
return 1;
}

if (!fix) {
log.stdout('Running Dart Format Check...');
for (final p in packages) {
if (!p.hasAnyDartCode) continue;

final progress = log.progress(' ${p.relativePath}');

final pathsToFormat = _getPathsToFormat(p);

final formatProcess = await pm.runProcess(
CliCommand.dart(
['format', '--output=none', '--set-exit-if-changed', ...pathsToFormat],
throwOnException: false,
),
workingDirectory: p.packagePath,
);

if (formatProcess.exitCode == 0) {
progress.finish(showTiming: true);
} else {
failureCount++;
progress.finish(message: 'failed');
}
}

if (failureCount > 0) {
log.stderr('Presubmit failed.');
log.stderr(' Formatting issues found in $failureCount packages.');
return 1;
}
}

log.stdout('Presubmit passed!');
return 0;
}

List<String> _getPathsToFormat(Package p) {
final pathsToFormat = <String>[];
if (p.relativePath == 'tool') {
final children = Directory(p.packagePath).listSync();
for (final entity in children) {
final name = path.basename(entity.path);
if (name.startsWith('.')) continue;
if (name == 'flutter-sdk') continue;
if (entity is Directory) {
pathsToFormat.add(name);
} else if (entity is File && name.endsWith('.dart')) {
pathsToFormat.add(name);
}
}
} else {
pathsToFormat.add('.');
}
return pathsToFormat;
}
}
2 changes: 2 additions & 0 deletions tool/lib/devtools_command_runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import 'package:devtools_tool/model.dart';

import 'commands/analyze.dart';
import 'commands/list.dart';
import 'commands/presubmit.dart';
import 'commands/pub_get.dart';
import 'commands/release_helper.dart';
import 'commands/repo_check.dart';
Expand All @@ -37,6 +38,7 @@ class DevToolsCommandRunner extends CommandRunner {
addCommand(FixGoldensCommand());
addCommand(GenerateCodeCommand());
addCommand(ListCommand());
addCommand(PresubmitCommand());
addCommand(PubGetCommand());
addCommand(ReleaseHelperCommand());
addCommand(ReleaseNotesCommand());
Expand Down
1 change: 1 addition & 0 deletions tool/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ dependencies:
cli_util: ^0.4.1
collection: ^1.19.0
io: ^1.0.4
meta: ^1.18.0
path: ^1.9.0
yaml: ^3.1.2

Expand Down
128 changes: 128 additions & 0 deletions tool/test/command_test_utils.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// Copyright 2026 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.

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

import 'package:args/command_runner.dart';
import 'package:io/io.dart';

class MockProcessManager implements ProcessManager {
MockProcessManager({this.onSpawn});

final Future<Process> Function(
String executable,
Iterable<String> arguments, {
String? workingDirectory,
Map<String, String>? environment,
bool includeParentEnvironment,
bool runInShell,
ProcessStartMode mode,
})?
onSpawn;

@override
Future<Process> spawn(
String executable,
Iterable<String> arguments, {
String? workingDirectory,
Map<String, String>? environment,
bool includeParentEnvironment = true,
bool runInShell = false,
ProcessStartMode mode = ProcessStartMode.normal,
}) async {
if (onSpawn != null) {
return onSpawn!(
executable,
arguments,
workingDirectory: workingDirectory,
environment: environment,
includeParentEnvironment: includeParentEnvironment,
runInShell: runInShell,
mode: mode,
);
}
return MockProcess();
}

@override
Future<Process> spawnBackground(
String executable,
Iterable<String> arguments, {
String? workingDirectory,
Map<String, String>? environment,
bool includeParentEnvironment = true,
bool runInShell = false,
ProcessStartMode mode = ProcessStartMode.normal,
}) async {
throw UnimplementedError();
}

@override
Future<Process> spawnDetached(
String executable,
Iterable<String> arguments, {
String? workingDirectory,
Map<String, String>? environment,
bool includeParentEnvironment = true,
bool runInShell = false,
ProcessStartMode mode = ProcessStartMode.normal,
}) async {
throw UnimplementedError();
}
}

class MockProcess implements Process {
MockProcess({
this.exitCodeValue = 0,
this.stdoutString = '',
this.stderrString = '',
});

final int exitCodeValue;
final String stdoutString;
final String stderrString;

@override
Future<int> get exitCode => Future.value(exitCodeValue);

@override
Stream<List<int>> get stdout => Stream.value(utf8.encode(stdoutString));

@override
Stream<List<int>> get stderr => Stream.value(utf8.encode(stderrString));

@override
bool kill([ProcessSignal signal = ProcessSignal.sigterm]) => true;

@override
int get pid => 0;

@override
IOSink get stdin => throw UnimplementedError();
}

class TestCommandRunner extends CommandRunner {
TestCommandRunner() : super('test', 'test description');

void addDummyCommand(String name, [int exitCode = 0]) {
addCommand(DummyCommand(name, exitCode));
}
}

class DummyCommand extends Command {
DummyCommand(this.name, this.exitCodeValue);

@override
final String name;

@override
String get description => 'Dummy command for testing';

final int exitCodeValue;

@override
Future<int> run() async => exitCodeValue;
}
Loading
Loading