Skip to content

Commit f9d114d

Browse files
committed
Fix copyright script to ignore submodules, add tests.
1 parent e38ba79 commit f9d114d

File tree

4 files changed

+569
-214
lines changed

4 files changed

+569
-214
lines changed

tool/fix_copyright/bin/fix_copyright.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ Future<int> main(List<String> arguments) async {
3838
defaultsTo: '2025',
3939
help: 'Set the year to use for the copyright year.',
4040
);
41+
argParser.addFlag(
42+
'skip-submodules',
43+
defaultsTo: true,
44+
help: 'Skip git submodules when fixing copyrights.',
45+
);
4146
argParser.addFlag(
4247
'help',
4348
negatable: false,
@@ -71,6 +76,7 @@ Future<int> main(List<String> arguments) async {
7176
force: parsedArguments['force'] as bool,
7277
year: parsedArguments['year']! as String,
7378
paths: parsedArguments.rest,
79+
skipSubmodules: parsedArguments['skip-submodules'] as bool,
7480
);
7581
exit(exitCode);
7682
}

tool/fix_copyright/lib/src/fix_copyright.dart

Lines changed: 111 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import 'dart:async';
1010
import 'dart:io';
1111

1212
import 'package:file/file.dart';
13+
import 'package:process/process.dart';
1314

1415
typedef LogFunction = void Function(String);
1516

@@ -18,6 +19,8 @@ Future<int> fixCopyrights(
1819
required bool force,
1920
required String year,
2021
required List<String> paths,
22+
bool skipSubmodules = true,
23+
ProcessManager processManager = const LocalProcessManager(),
2124
LogFunction? log,
2225
LogFunction? error,
2326
}) async {
@@ -28,17 +31,80 @@ Future<int> fixCopyrights(
2831
void stdErr(String message) =>
2932
(error ?? stderr.writeln as LogFunction).call(message);
3033

34+
final Set<String> submodulePaths;
35+
if (skipSubmodules) {
36+
final gitRootResult = await processManager.run([
37+
'git',
38+
'rev-parse',
39+
'--show-toplevel',
40+
]);
41+
if (gitRootResult.exitCode != 0) {
42+
stdErr('Warning: not a git repository. Cannot check for submodules.');
43+
submodulePaths = <String>{};
44+
} else {
45+
final repoRoot = gitRootResult.stdout.toString().trim();
46+
final result = await processManager.run([
47+
'git',
48+
'submodule',
49+
'status',
50+
'--recursive',
51+
], workingDirectory: repoRoot);
52+
if (result.exitCode == 0) {
53+
submodulePaths = result.stdout
54+
.toString()
55+
.split('\n')
56+
.where((line) => line.trim().isNotEmpty)
57+
.map((line) {
58+
final parts = line.trim().split(RegExp(r'\s+'));
59+
if (parts.length > 1) {
60+
return path.canonicalize(path.join(repoRoot, parts[1]));
61+
}
62+
return null;
63+
})
64+
.whereType<String>()
65+
.toSet();
66+
} else {
67+
submodulePaths = <String>{};
68+
stdErr(
69+
'Warning: could not get submodule status. '
70+
'Not skipping any submodules.',
71+
);
72+
}
73+
}
74+
} else {
75+
submodulePaths = <String>{};
76+
}
77+
3178
String getExtension(File file) {
3279
final pathExtension = path.extension(file.path);
3380
return pathExtension.isNotEmpty ? pathExtension.substring(1) : '';
3481
}
3582

3683
Iterable<File> matchingFiles(Directory dir) {
37-
return dir
38-
.listSync(recursive: true)
39-
.whereType<File>()
40-
.where((File file) => extensionMap.containsKey(getExtension(file)))
41-
.map((File file) => file.absolute);
84+
final files = <File>[];
85+
final directories = <Directory>[dir];
86+
while (directories.isNotEmpty) {
87+
final currentDir = directories.removeAt(0);
88+
if (skipSubmodules &&
89+
submodulePaths.contains(path.canonicalize(currentDir.path))) {
90+
stdLog('Skipping submodule: ${currentDir.path}');
91+
continue;
92+
}
93+
try {
94+
for (final entity in currentDir.listSync()) {
95+
if (entity is File) {
96+
if (extensionMap.containsKey(getExtension(entity))) {
97+
files.add(entity.absolute);
98+
}
99+
} else if (entity is Directory) {
100+
directories.add(entity);
101+
}
102+
}
103+
} on FileSystemException catch (e) {
104+
stdErr('Could not list directory ${currentDir.path}: $e');
105+
}
106+
}
107+
return files;
42108
}
43109

44110
final rest = paths.isEmpty ? <String>['.'] : paths;
@@ -74,42 +140,47 @@ Future<int> fixCopyrights(
74140
}
75141
final info = extensionMap[extension]!;
76142
final inputFile = file.absolute;
77-
var originalContents = inputFile.readAsStringSync();
143+
final originalContents = inputFile.readAsStringSync();
78144
if (_hasCorrectLicense(originalContents, info)) {
79145
continue;
80146
}
81147

82-
// If a sort-of correct copyright is there, but just doesn't have the
83-
// right case, date, spacing, license type or trailing newline, then
84-
// remove it.
85-
var newContents = originalContents.replaceFirst(
86-
RegExp(info.copyrightPattern, caseSensitive: false, multiLine: true),
87-
'',
88-
);
89-
// Strip any matching header from the existing file, and replace it with
90-
// the correct combined copyright and the header that was matched.
91-
if ((info.headerPattern ?? info.header) != null) {
92-
final match = RegExp(
93-
info.headerPattern ?? '(?<header>${RegExp.escape(info.header!)})',
94-
caseSensitive: false,
95-
).firstMatch(newContents);
96-
if (match != null) {
97-
final header = match.namedGroup('header') ?? '';
98-
newContents = newContents.substring(match.end);
99-
newContents =
100-
'$header${info.copyright}\n${info.trailingBlank ? '\n' : ''}'
101-
'$newContents';
148+
nonCompliantFiles.add(file);
149+
150+
if (force) {
151+
var contents = originalContents.replaceAll('\r\n', '\n');
152+
String? fileHeader;
153+
if (info.headerPattern != null) {
154+
final match = RegExp(
155+
info.headerPattern!,
156+
caseSensitive: false,
157+
).firstMatch(contents);
158+
if (match != null && match.start == 0) {
159+
fileHeader = match.group(0);
160+
contents = contents.substring(match.end);
161+
}
162+
}
163+
164+
// If a sort-of correct copyright is there, but just doesn't have the
165+
// right case, date, spacing, license type or trailing newline, then
166+
// remove it.
167+
contents = contents.replaceFirst(
168+
RegExp(info.copyrightPattern, caseSensitive: false, multiLine: true),
169+
'',
170+
);
171+
contents = contents.trimLeft();
172+
var newContents = '';
173+
if (fileHeader != null) {
174+
final String copyrightBlock =
175+
'${info.copyright}${info.trailingBlank ? '\n\n' : '\n'}';
176+
newContents = '$fileHeader$copyrightBlock$contents';
102177
} else {
103-
newContents = '${info.combined}$newContents';
178+
newContents = '${info.combined}$contents';
104179
}
105-
} else {
106-
newContents = '${info.combined}$newContents';
107-
}
108-
if (newContents != originalContents) {
109-
if (force) {
180+
181+
if (newContents != originalContents.replaceAll('\r\n', '\n')) {
110182
inputFile.writeAsStringSync(newContents);
111183
}
112-
nonCompliantFiles.add(file);
113184
}
114185
} on FileSystemException catch (e) {
115186
stdErr('Could not process file ${file.path}: $e');
@@ -153,7 +224,7 @@ class CopyrightInfo {
153224

154225
RegExp get pattern {
155226
return RegExp(
156-
'${headerPattern ?? (header != null ? RegExp.escape(header!) : '')}'
227+
'^(?:${headerPattern ?? (header != null ? RegExp.escape(header!) : '')})?'
157228
'${RegExp.escape(copyright)}\n${trailingBlank ? r'\n' : ''}',
158229
);
159230
}
@@ -257,12 +328,7 @@ ${isParagraph ? '' : prefix}found in the LICENSE file.$suffix''';
257328
'kt': generateInfo(prefix: '// '),
258329
'm': generateInfo(prefix: '// '),
259330
'ps1': generateInfo(prefix: '# '),
260-
'sh': generateInfo(
261-
prefix: '# ',
262-
header: '#!/usr/bin/env bash\n',
263-
headerPattern:
264-
r'(?<header>#!/usr/bin/env bash\n|#!/bin/sh\n|#!/bin/bash\n)',
265-
),
331+
'sh': generateInfo(prefix: '# ', headerPattern: r'(?<header>#!.*\n?)'),
266332
'swift': generateInfo(prefix: '// '),
267333
'ts': generateInfo(prefix: '// '),
268334
'xml': generateInfo(
@@ -278,7 +344,10 @@ ${isParagraph ? '' : prefix}found in the LICENSE file.$suffix''';
278344

279345
bool _hasCorrectLicense(String rawContents, CopyrightInfo info) {
280346
// Normalize line endings.
281-
final contents = rawContents.replaceAll('\r\n', '\n');
347+
var contents = rawContents.replaceAll('\r\n', '\n');
282348
// Ignore empty files.
283-
return contents.isEmpty || contents.startsWith(info.pattern);
349+
if (contents.isEmpty) {
350+
return true;
351+
}
352+
return info.pattern.hasMatch(contents);
284353
}

tool/fix_copyright/pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ dependencies:
1414
dart_flutter_team_lints: ^3.5.2
1515
file: ^7.0.1
1616
path: ^1.9.0
17+
process: ^5.0.5
1718

1819
dev_dependencies:
1920
lints: ^6.0.0

0 commit comments

Comments
 (0)