@@ -10,6 +10,7 @@ import 'dart:async';
1010import 'dart:io' ;
1111
1212import 'package:file/file.dart' ;
13+ import 'package:process/process.dart' ;
1314
1415typedef 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
279345bool _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}
0 commit comments