Skip to content

Commit 23dbd72

Browse files
authored
Skip submodules, add more tests (#324)
1 parent 15ca53b commit 23dbd72

File tree

4 files changed

+572
-217
lines changed

4 files changed

+572
-217
lines changed

.vscode/launch.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,15 @@
2121
"cwd": "packages/catalog_gallery",
2222
"request": "launch",
2323
"type": "dart"
24+
},
25+
{
26+
"name": "fix_copyright",
27+
"request": "launch",
28+
"program": "bin/fix_copyright.dart",
29+
"cwd": "tool/fix_copyright",
30+
"args": ["--force", "${workspaceFolder}"],
31+
"type": "dart",
32+
"console": "debugConsole"
2433
}
2534
]
2635
}

tool/fix_copyright/lib/src/fix_copyright.dart

Lines changed: 111 additions & 45 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,7 @@ Future<int> fixCopyrights(
1819
required bool force,
1920
required String year,
2021
required List<String> paths,
22+
ProcessManager processManager = const LocalProcessManager(),
2123
LogFunction? log,
2224
LogFunction? error,
2325
}) async {
@@ -28,17 +30,75 @@ Future<int> fixCopyrights(
2830
void stdErr(String message) =>
2931
(error ?? stderr.writeln as LogFunction).call(message);
3032

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

3678
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);
79+
final files = <File>[];
80+
final directories = <Directory>[dir];
81+
while (directories.isNotEmpty) {
82+
final currentDir = directories.removeAt(0);
83+
if (submodulePaths.contains(path.canonicalize(currentDir.path))) {
84+
stdLog('Skipping submodule: ${currentDir.path}');
85+
continue;
86+
}
87+
try {
88+
for (final entity in currentDir.listSync()) {
89+
if (entity is File) {
90+
if (extensionMap.containsKey(getExtension(entity))) {
91+
files.add(entity.absolute);
92+
}
93+
} else if (entity is Directory) {
94+
directories.add(entity);
95+
}
96+
}
97+
} on FileSystemException catch (e) {
98+
stdErr('Could not list directory ${currentDir.path}: $e');
99+
}
100+
}
101+
return files;
42102
}
43103

44104
final rest = paths.isEmpty ? <String>['.'] : paths;
@@ -74,42 +134,49 @@ Future<int> fixCopyrights(
74134
}
75135
final info = extensionMap[extension]!;
76136
final inputFile = file.absolute;
77-
var originalContents = inputFile.readAsStringSync();
137+
final originalContents = inputFile.readAsStringSync();
78138
if (_hasCorrectLicense(originalContents, info)) {
79139
continue;
80140
}
81141

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';
142+
nonCompliantFiles.add(file);
143+
144+
if (force) {
145+
var contents = originalContents.replaceAll('\r\n', '\n');
146+
String? fileHeader;
147+
if (info.headerPattern != null) {
148+
final match = RegExp(
149+
info.headerPattern!,
150+
caseSensitive: false,
151+
).firstMatch(contents);
152+
if (match != null && match.start == 0) {
153+
fileHeader = match.group(0);
154+
contents = contents.substring(match.end);
155+
}
156+
}
157+
158+
contents = contents.trimLeft();
159+
160+
// If a sort-of correct copyright is there, but just doesn't have the
161+
// right case, date, spacing, license type or trailing newline, then
162+
// remove it.
163+
contents = contents.replaceFirst(
164+
RegExp(info.copyrightPattern, caseSensitive: false, multiLine: true),
165+
'',
166+
);
167+
contents = contents.trimLeft();
168+
var newContents = '';
169+
if (fileHeader != null) {
170+
final copyrightBlock =
171+
'${info.copyright}${info.trailingBlank ? '\n\n' : '\n'}';
172+
newContents = '$fileHeader$copyrightBlock$contents';
102173
} else {
103-
newContents = '${info.combined}$newContents';
174+
newContents = '${info.combined}$contents';
104175
}
105-
} else {
106-
newContents = '${info.combined}$newContents';
107-
}
108-
if (newContents != originalContents) {
109-
if (force) {
176+
177+
if (newContents != originalContents.replaceAll('\r\n', '\n')) {
110178
inputFile.writeAsStringSync(newContents);
111179
}
112-
nonCompliantFiles.add(file);
113180
}
114181
} on FileSystemException catch (e) {
115182
stdErr('Could not process file ${file.path}: $e');
@@ -153,8 +220,9 @@ class CopyrightInfo {
153220

154221
RegExp get pattern {
155222
return RegExp(
156-
'${headerPattern ?? (header != null ? RegExp.escape(header!) : '')}'
223+
'^(?:${headerPattern ?? (header != null ? RegExp.escape(header!) : '')})?'
157224
'${RegExp.escape(copyright)}\n${trailingBlank ? r'\n' : ''}',
225+
multiLine: true,
158226
);
159227
}
160228

@@ -240,7 +308,7 @@ ${isParagraph ? '' : prefix}found in the LICENSE file.$suffix''';
240308
'cc': generateInfo(prefix: '// '),
241309
'cmake': generateInfo(prefix: '# '),
242310
'cpp': generateInfo(prefix: '// '),
243-
'dart': generateInfo(prefix: '// '),
311+
'dart': generateInfo(prefix: '// ', headerPattern: r'(?<header>#!.*\n?)'),
244312
'gn': generateInfo(prefix: '# '),
245313
'gradle': generateInfo(prefix: '// '),
246314
'h': generateInfo(prefix: '// '),
@@ -250,35 +318,33 @@ ${isParagraph ? '' : prefix}found in the LICENSE file.$suffix''';
250318
isParagraph: true,
251319
trailingBlank: false,
252320
header: '<!DOCTYPE HTML>\n',
253-
headerPattern: r'(?<header><!DOCTYPE\s+HTML[^>]*>\n)?',
321+
headerPattern: r'(?<header><!DOCTYPE\s+HTML[^>]*>\n?)?',
254322
),
255323
'js': generateInfo(prefix: '// '),
256324
'java': generateInfo(prefix: '// '),
257325
'kt': generateInfo(prefix: '// '),
258326
'm': generateInfo(prefix: '// '),
259327
'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-
),
328+
'sh': generateInfo(prefix: '# ', headerPattern: r'(?<header>#!.*\n?)'),
266329
'swift': generateInfo(prefix: '// '),
267330
'ts': generateInfo(prefix: '// '),
268331
'xml': generateInfo(
269332
prefix: '<!-- ',
270333
suffix: ' -->',
271334
isParagraph: true,
272335
headerPattern:
273-
r'''(?<header><\?xml\s+(?:version="1.0"\s+encoding="utf-8"|encoding="utf-8"\s+version="1.0")[^>]*\?>\n|)''',
336+
r'''(?<header><\?xml\s+(?:version="1.0"\s+encoding="utf-8"|encoding="utf-8"\s+version="1.0")[^>]*\?>\n?|)''',
274337
),
275338
'yaml': generateInfo(prefix: '# '),
276339
};
277340
}
278341

279342
bool _hasCorrectLicense(String rawContents, CopyrightInfo info) {
280343
// Normalize line endings.
281-
final contents = rawContents.replaceAll('\r\n', '\n');
344+
var contents = rawContents.replaceAll('\r\n', '\n');
282345
// Ignore empty files.
283-
return contents.isEmpty || contents.startsWith(info.pattern);
346+
if (contents.isEmpty) {
347+
return true;
348+
}
349+
return info.pattern.hasMatch(contents);
284350
}

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)