@@ -10,6 +10,7 @@ import 'dart:async';
10
10
import 'dart:io' ;
11
11
12
12
import 'package:file/file.dart' ;
13
+ import 'package:process/process.dart' ;
13
14
14
15
typedef LogFunction = void Function (String );
15
16
@@ -18,6 +19,7 @@ Future<int> fixCopyrights(
18
19
required bool force,
19
20
required String year,
20
21
required List <String > paths,
22
+ ProcessManager processManager = const LocalProcessManager (),
21
23
LogFunction ? log,
22
24
LogFunction ? error,
23
25
}) async {
@@ -28,17 +30,75 @@ Future<int> fixCopyrights(
28
30
void stdErr (String message) =>
29
31
(error ?? stderr.writeln as LogFunction ).call (message);
30
32
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
+
31
73
String getExtension (File file) {
32
74
final pathExtension = path.extension (file.path);
33
75
return pathExtension.isNotEmpty ? pathExtension.substring (1 ) : '' ;
34
76
}
35
77
36
78
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;
42
102
}
43
103
44
104
final rest = paths.isEmpty ? < String > ['.' ] : paths;
@@ -74,42 +134,49 @@ Future<int> fixCopyrights(
74
134
}
75
135
final info = extensionMap[extension ]! ;
76
136
final inputFile = file.absolute;
77
- var originalContents = inputFile.readAsStringSync ();
137
+ final originalContents = inputFile.readAsStringSync ();
78
138
if (_hasCorrectLicense (originalContents, info)) {
79
139
continue ;
80
140
}
81
141
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 ' ;
102
173
} else {
103
- newContents = '${info .combined }$newContents ' ;
174
+ newContents = '${info .combined }$contents ' ;
104
175
}
105
- } else {
106
- newContents = '${info .combined }$newContents ' ;
107
- }
108
- if (newContents != originalContents) {
109
- if (force) {
176
+
177
+ if (newContents != originalContents.replaceAll ('\r\n ' , '\n ' )) {
110
178
inputFile.writeAsStringSync (newContents);
111
179
}
112
- nonCompliantFiles.add (file);
113
180
}
114
181
} on FileSystemException catch (e) {
115
182
stdErr ('Could not process file ${file .path }: $e ' );
@@ -153,8 +220,9 @@ class CopyrightInfo {
153
220
154
221
RegExp get pattern {
155
222
return RegExp (
156
- '${headerPattern ?? (header != null ? RegExp .escape (header !) : '' )}'
223
+ '^(?: ${headerPattern ?? (header != null ? RegExp .escape (header !) : '' )})? '
157
224
'${RegExp .escape (copyright )}\n ${trailingBlank ? r'\n' : '' }' ,
225
+ multiLine: true ,
158
226
);
159
227
}
160
228
@@ -240,7 +308,7 @@ ${isParagraph ? '' : prefix}found in the LICENSE file.$suffix''';
240
308
'cc' : generateInfo (prefix: '// ' ),
241
309
'cmake' : generateInfo (prefix: '# ' ),
242
310
'cpp' : generateInfo (prefix: '// ' ),
243
- 'dart' : generateInfo (prefix: '// ' ),
311
+ 'dart' : generateInfo (prefix: '// ' , headerPattern : r'(?<header>#!.*\n?)' ),
244
312
'gn' : generateInfo (prefix: '# ' ),
245
313
'gradle' : generateInfo (prefix: '// ' ),
246
314
'h' : generateInfo (prefix: '// ' ),
@@ -250,35 +318,33 @@ ${isParagraph ? '' : prefix}found in the LICENSE file.$suffix''';
250
318
isParagraph: true ,
251
319
trailingBlank: false ,
252
320
header: '<!DOCTYPE HTML>\n ' ,
253
- headerPattern: r'(?<header><!DOCTYPE\s+HTML[^>]*>\n)?' ,
321
+ headerPattern: r'(?<header><!DOCTYPE\s+HTML[^>]*>\n? )?' ,
254
322
),
255
323
'js' : generateInfo (prefix: '// ' ),
256
324
'java' : generateInfo (prefix: '// ' ),
257
325
'kt' : generateInfo (prefix: '// ' ),
258
326
'm' : generateInfo (prefix: '// ' ),
259
327
'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?)' ),
266
329
'swift' : generateInfo (prefix: '// ' ),
267
330
'ts' : generateInfo (prefix: '// ' ),
268
331
'xml' : generateInfo (
269
332
prefix: '<!-- ' ,
270
333
suffix: ' -->' ,
271
334
isParagraph: true ,
272
335
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? |)''' ,
274
337
),
275
338
'yaml' : generateInfo (prefix: '# ' ),
276
339
};
277
340
}
278
341
279
342
bool _hasCorrectLicense (String rawContents, CopyrightInfo info) {
280
343
// Normalize line endings.
281
- final contents = rawContents.replaceAll ('\r\n ' , '\n ' );
344
+ var contents = rawContents.replaceAll ('\r\n ' , '\n ' );
282
345
// 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);
284
350
}
0 commit comments