├── lib ├── options.dart ├── documentation_updater.dart ├── src │ ├── remove_doc_tags.dart │ ├── sync_data.dart │ ├── runner.dart │ ├── util.dart │ ├── generate_readme.dart │ ├── generate_doc.dart │ ├── generate_gh_pages.dart │ ├── git_repository.dart │ ├── options.dart │ └── git_documentation_updater.dart └── example2uri.dart ├── analysis_options.yaml ├── pubspec.yaml ├── .gitignore ├── LICENSE ├── CHANGELOG.md ├── bin └── dart_doc_syncer.dart ├── test └── remove_doc_tags_test.dart └── README.md /lib/options.dart: -------------------------------------------------------------------------------- 1 | export 'src/options.dart'; 2 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | analyzer: 2 | # exclude: 3 | # - path/to/excluded/files/** 4 | 5 | # Lint rules and documentation, see http://dart-lang.github.io/linter/lints 6 | linter: 7 | rules: 8 | - cancel_subscriptions 9 | - hash_and_equals 10 | - iterable_contains_unrelated_type 11 | - list_remove_unrelated_type 12 | - test_types_in_equals 13 | - unrelated_type_equality_checks 14 | - valid_regexps 15 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: dart_doc_syncer 2 | version: 1.0.2 3 | publish_to: none 4 | 5 | description: > 6 | Updates a documentation repository for an Angular Dart example. 7 | 8 | authors: 9 | - Thibault Sottiaux 10 | - Patrice Chalin 11 | 12 | environment: 13 | sdk: '>=2.0.0-dev.68.0 <3.0.0' 14 | 15 | # Add bin/dart_doc_syncer.dart to the scripts that pub installs. 16 | executables: 17 | dart_doc_syncer: 18 | 19 | dependencies: 20 | args: ^1.4.0 21 | logging: ^0.11.2 22 | path: ^1.2.0 23 | yaml: ^2.1.13 24 | 25 | dev_dependencies: 26 | test: ^1.0.0 27 | -------------------------------------------------------------------------------- /lib/documentation_updater.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'src/git_documentation_updater.dart'; 4 | import 'src/git_repository.dart'; 5 | 6 | abstract class DocumentationUpdater { 7 | factory DocumentationUpdater() => 8 | new GitDocumentationUpdater(new GitRepositoryFactory()); 9 | 10 | /// Updates [outRepositoryUri] based on the content of the example under 11 | /// [examplePath] in the Angular docs repository. 12 | Future updateRepository(String examplePath, String outRepositoryUri, 13 | {String exampleName, bool push, bool clean}); 14 | 15 | /// Updates all example repositories containing a doc syncer data file 16 | /// and whose path matches [match] but not [skip]. 17 | Future updateMatchingRepo(RegExp match, RegExp skip, {bool push, bool clean}); 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | 3 | # Don’t commit the following directories created by pub. 4 | .dart_tool 5 | packages 6 | pubspec.lock 7 | .pub 8 | .packages 9 | 10 | /dist/ 11 | .buildlog 12 | node_modules 13 | bower_components 14 | 15 | # Or broccoli working directory 16 | tmp 17 | 18 | # Or the files created by dart2js. 19 | *.dart.js 20 | *.dart.precompiled.js 21 | *.js_ 22 | *.js.deps 23 | *.js.map 24 | 25 | # Or type definitions we mirror from github 26 | # (NB: these lines are removed in publish-build-artifacts.sh) 27 | **/typings/**/*.d.ts 28 | **/typings/tsd.cached.json 29 | 30 | # Include when developing application packages. 31 | pubspec.lock 32 | .c9 33 | .idea/ 34 | .settings/ 35 | *.swo 36 | modules/.settings 37 | .vscode 38 | modules/.vscode 39 | 40 | # Don't check in secret files 41 | *secret.js 42 | 43 | # Ignore npm debug log 44 | npm-debug.log 45 | 46 | /docs/bower_components/ 47 | 48 | # build-analytics 49 | .build-analytics 50 | 51 | # built dart payload tests 52 | /modules_dart/payload/**/build 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2014-2016 Google, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/src/remove_doc_tags.dart: -------------------------------------------------------------------------------- 1 | /// Returns [code] with filtered out doc tag comment lines. 2 | String removeDocTags(String code) => 3 | code.split('\n').where(_isNotDocTagComment).join('\n'); 4 | 5 | final _docTags = ['#docregion', '#enddocregion', '#docplaster']; 6 | final _commentPrefixes = ['//', ' 42 | 43 |
44 | Hello {{name}}! 45 |
46 | 47 | '''; 48 | 49 | final cleanedCode = ''' 50 |
51 | Hello {{name}}! 52 |
53 | '''; 54 | 55 | expect(removeDocTags(code), equals(cleanedCode)); 56 | }); 57 | 58 | test("leaves non-comments alone (html)", () { 59 | var code = ''' 60 | Example of a doc tag: #docplaster; 61 | '''; 62 | 63 | expect(removeDocTags(code), equals(code)); 64 | }); 65 | 66 | test("leaves comments without doc-tags alone (html)", () { 67 | var code = ''' 68 | 69 | Sum: {{2 + 2}} 70 | '''; 71 | 72 | expect(removeDocTags(code), equals(code)); 73 | }); 74 | }); 75 | } 76 | -------------------------------------------------------------------------------- /lib/src/sync_data.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:path/path.dart' as p; 3 | 4 | import '../example2uri.dart'; 5 | import 'options.dart'; 6 | 7 | /// Holds metadata about the example application that is used to generate a 8 | /// README file. 9 | class SyncData { 10 | final String name; // e.g., 'architecture' 11 | final String path; // e.g., 'ng/doc/architecture' 12 | final String title; // e.g. 'Architecture Overview' 13 | final String docPart; // e.g. 'guide' (from 'guide/architecture') 14 | final String docHref; 15 | final String repoHref; 16 | final String liveExampleHref; 17 | final List links; 18 | 19 | String get id => path; 20 | 21 | SyncData( 22 | {String name: '', 23 | this.title: '', 24 | String docPart: '', 25 | String docHref: '', 26 | String liveExampleHref: '', 27 | this.links: const [], 28 | String repoHref: '', 29 | String path}) 30 | : this.name = _name(name, path), 31 | this.path = path, 32 | this.docPart = docPart, 33 | this.docHref = docHref.isEmpty 34 | ? p.join(options.webdevURL, docPart, _name(name, path)) 35 | : docHref.startsWith('http') 36 | ? docHref 37 | : p.join(options.webdevURL, docPart, docHref), 38 | this.liveExampleHref = liveExampleHref == null 39 | ? p.join(options.webdevURL, docExampleDirRoot, _name(name, path)) 40 | : liveExampleHref.startsWith('http') || liveExampleHref.isEmpty 41 | ? liveExampleHref 42 | : p.join(options.webdevURL, liveExampleHref), 43 | this.repoHref = repoHref.isEmpty 44 | ? '//github.com/dart-lang/site-webdev/tree/${options.branch}/' + path 45 | : repoHref; 46 | 47 | static String _name(String name, String path) => 48 | name.isEmpty ? getExampleName(path) : name; 49 | 50 | factory SyncData.fromJson(String json, {String path}) { 51 | final data = jsonDecode(json); 52 | return new SyncData( 53 | name: data['name'] ?? '', 54 | title: data['title'] ?? '', 55 | docPart: data['docPart'] ?? '', 56 | docHref: data['docHref'] ?? '', 57 | repoHref: data['repoHref'] ?? '', 58 | liveExampleHref: data['liveExampleHref'], 59 | links: data['links'] ?? [], 60 | path: path); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/src/runner.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:logging/logging.dart'; 5 | 6 | import 'options.dart'; 7 | 8 | export 'options.dart' show options; 9 | 10 | final Logger _logger = new Logger('runner'); 11 | final spacesRE = new RegExp(r'\s+'); 12 | 13 | Exception newException(String msg) => new Exception(msg); 14 | 15 | Future runCmd( 16 | String cmdAndArgs, { 17 | Map environment, 18 | String workingDirectory, 19 | bool isException(ProcessResult r), 20 | Exception mkException(String msg): newException, 21 | }) async { 22 | final parts = cmdAndArgs.split(spacesRE); 23 | return run( 24 | parts[0], 25 | parts.sublist(1), 26 | environment: environment, 27 | workingDirectory: workingDirectory, 28 | isException: isException, 29 | mkException: mkException, 30 | ); 31 | } 32 | 33 | Future run( 34 | String executable, 35 | List arguments, { 36 | Map environment, 37 | String workingDirectory, 38 | bool isException(ProcessResult r), 39 | Exception mkException(String msg): newException, 40 | }) async { 41 | var cmd = "$executable ${arguments.join(' ')}"; 42 | if (workingDirectory != null) cmd += ' ($workingDirectory)'; 43 | _logger.finest(' > $cmd'); 44 | 45 | if (!options.dryRun) { 46 | final r = await Process.run(executable, arguments, 47 | workingDirectory: workingDirectory, environment: environment); 48 | if (r.exitCode == 0 && (isException == null || !isException(r))) { 49 | _logStdout(r.stdout); 50 | return r; 51 | } 52 | // _logger.info('ERROR running: $cmd. Here are stderr and stdout:'); 53 | // _logger.info(r.stderr); 54 | // _logger.info('\n' + '=' * 50 + '\nSTDOUT:\n'); 55 | // _logger.info(r.stdout); 56 | throw mkException(r.stderr.isEmpty ? r.stdout : r.stderr); 57 | } 58 | 59 | if (executable == 'git' && arguments[0] == 'clone') { 60 | var path = arguments[2]; 61 | new Directory(path).createSync(recursive: true); 62 | } 63 | const int _bogusPid = 0; 64 | const int _exitOk = 0; 65 | return new Future.value(new ProcessResult(_bogusPid, _exitOk, null, null)); 66 | } 67 | 68 | void _logStdout(dynamic rawOut) { 69 | if (!options.verbose) return; 70 | final out = _trimOut(rawOut); 71 | if (out.isNotEmpty) _logger.finer(out); 72 | } 73 | 74 | String _trimOut(dynamic rawOut) { 75 | var out = rawOut.toString().trim(); 76 | if (out.length > 6000) 77 | out = out.substring(0, 1000) + '\n[Output trimmed] ...\n'; 78 | return out; 79 | } 80 | -------------------------------------------------------------------------------- /lib/src/util.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:logging/logging.dart'; 5 | import 'package:path/path.dart' as p; 6 | 7 | import 'options.dart'; 8 | 9 | final Logger _logger = new Logger('update_doc_repo'); 10 | 11 | // TODO: consider using this helper method so that we can log dir creation. 12 | //Future mkdir(String path) async { 13 | // _logger.fine('mkdir $path.'); 14 | // if (dryRun) return new Future.value(); 15 | // return await new Directory(path).create(); 16 | //} 17 | 18 | /// Create directory only during dry runs. 19 | Future dryRunMkDir(String path) async { 20 | var dir = new Directory(path); 21 | if (!options.dryRun || dir.existsSync()) return new Future.value(); 22 | _logger.fine('[dryrun] mkdir $path.'); 23 | return dir.create(); 24 | } 25 | 26 | /// Read [path] as a string, apply [transformer] and write back the result. 27 | Future transformFile( 28 | String path, String transformer(String content)) async { 29 | _logger.fine(' Transform file $path'); 30 | if (options.dryRun) return new Future.value(); 31 | 32 | File file = new File(path); 33 | await file.writeAsString(transformer(await file.readAsString())); 34 | } 35 | 36 | /// Like path.join(...), but first filters out null and empty path parts. 37 | String pathJoin(List pathParts) => 38 | p.joinAll(pathParts.where((p) => p != null && p.isNotEmpty)); 39 | 40 | String stripPathPrefix(String prefix, String path) { 41 | if (prefix == null || prefix.isEmpty) return path; 42 | if (path == prefix) return '.'; 43 | final normalizedPrefix = prefix.endsWith('/') ? prefix : '$prefix/'; 44 | assert(path.startsWith(normalizedPrefix), 45 | '"$path" should start with "$normalizedPrefix"'); 46 | return path.substring(normalizedPrefix.length); 47 | } 48 | 49 | // Dart 1 polyfill 50 | // ignore_for_file: deprecated_member_use 51 | int tryParse(String s) => int.parse(s, onError: (_) => null); 52 | 53 | /// Return list of directories containing pubspec files. If [dir] contains 54 | /// a pubspec, return `[dir]`, otherwise look one level down in the 55 | /// subdirectories of [dir], for pubspecs. 56 | List getAppRoots(Directory dir) { 57 | final List appRoots = []; 58 | if (_containsPubspec(dir)) { 59 | appRoots.add(dir); 60 | } else { 61 | for (var fsEntity in dir.listSync(followLinks: false)) { 62 | if (p.basename(fsEntity.path).startsWith('.') || 63 | options.containsBuildDir(fsEntity.path)) continue; 64 | if (fsEntity is Directory) { 65 | if (!_containsPubspec(fsEntity)) continue; 66 | _logger.finer(' >> pubspec found under ${fsEntity.path}'); 67 | appRoots.add(fsEntity); 68 | } 69 | } 70 | } 71 | if (appRoots.length == 0) 72 | throw new Exception('No pubspec.yaml found under ${dir.path}'); 73 | return appRoots; 74 | } 75 | 76 | bool _containsPubspec(Directory dir) => 77 | new File(p.join(dir.path, 'pubspec.yaml')).existsSync(); 78 | 79 | String pathToBuiltApp(String projectRootPath) => 80 | // We build for deployment only now, so options.buildDir contains the built 81 | // app (as opposed to it being under the `web` subfolder of the buildDir). 82 | p.join(projectRootPath, options.buildDir); 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dart-doc-syncer 2 | 3 | A utility for syncing Dart examples for the AngularDart docs (https://webdev.dartlang.org/angular). 4 | 5 | Example sources are read from the [dart-lang/site-webdev repo](https://github.com/dart-lang/site-webdev) and written 6 | to individual repos under [angular-examples](https://github.com/angular-examples). 7 | 8 | For specific commands to use when updating the AngularDart docs and examples, see 9 | https://github.com/dart-lang/site-webdev/wiki/Updating-Angular-docs. 10 | 11 | ## Syncing a single example 12 | 13 | Use the example name as an argument. For example: 14 | 15 | ``` 16 | dart dart_doc_syncer architecture 17 | ``` 18 | 19 | ## Syncing multiple examples 20 | 21 | The `--match` option takes a regular expression as an argument. 22 | The `dart_doc_syncer` will sync all examples that match the regex. 23 | To sync all examples, you can use `.` (dot) as a "match-all" pattern: 24 | 25 | ``` 26 | dart dart_doc_syncer --match . 27 | ``` 28 | 29 | ## Options 30 | 31 | ``` 32 | dart ~/GITHUB/dart-doc-syncer/bin/dart_doc_syncer.dart --help 33 | 34 | Syncs Angular docs example apps. 35 | 36 | Usage: dart_doc_syncer [options] [ | ] 37 | 38 | -h, --help Show this usage information 39 | -b, --branch 40 | Git branch to fetch webdev and examples from 41 | (defaults to "master") 42 | 43 | -n, --dry-run Show which commands would be executed but make (almost) no changes; 44 | only the temporary directory will be created 45 | 46 | -f, --force-build Forces build of example app when sources have not changed 47 | -g, --gh-pages-app-dir 48 | Directory in which the generated example apps will be placed (gh-pages branch) 49 | 50 | -k, --keep-tmp Do not delete temporary working directory once done 51 | --pub-get Use `pub get` instead of `pub upgrade` before building apps 52 | -p, --[no-]push Prepare updates and push to example repo 53 | (defaults to on) 54 | 55 | -m, --match 56 | Sync all examples having a data file (.docsync.json) 57 | and whose repo path matches the given regular expression; 58 | use "." to match all 59 | 60 | --skip 61 | Negative filter applied to the project list created by use of the --match option 62 | 63 | --url [dev|main] 64 | Webdev site URL to use in generated README. 65 | (defaults to "main") 66 | 67 | -u, --user 68 | GitHub id of repo to fetch examples from 69 | (defaults to "dart-lang") 70 | 71 | -v, --verbose 72 | --web-compiler , either dart2js or dartdevc 73 | (defaults to "dart2js") 74 | 75 | -w, --work-dir 76 | Path to a working directory; when unspecified a system-generated path to a temporary directory is used 77 | ``` 78 | -------------------------------------------------------------------------------- /lib/src/generate_readme.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:logging/logging.dart'; 5 | import 'package:path/path.dart' as p; 6 | 7 | import 'options.dart'; 8 | import 'sync_data.dart'; 9 | 10 | final Logger _logger = new Logger('generateReadme'); 11 | 12 | /// Generates a README file for the example at [path]. 13 | Future generateReadme(String path, {String webdevNgPath}) async { 14 | final syncDataFile = new File(p.join(path, exampleConfigFileName)); 15 | final dataExists = await syncDataFile.exists(); 16 | 17 | final syncData = dataExists 18 | ? new SyncData.fromJson(await syncDataFile.readAsStringSync(), 19 | path: webdevNgPath) 20 | : new SyncData(title: p.basename(path), path: webdevNgPath); 21 | 22 | await _generateReadme(path, syncData); 23 | if (dataExists) await syncDataFile.delete(); 24 | } 25 | 26 | /// Generates a README file for the example at [path] based on [syncData]. 27 | Future _generateReadme(String path, SyncData syncData) async { 28 | final warningMessage = (syncData.docHref.isEmpty) 29 | ? ''' 30 | **WARNING:** This example is preliminary and subject to change. 31 | 32 | ------------------------------------------------------------------ 33 | 34 | 35 | ''' 36 | : ''; 37 | 38 | final linkSection = syncData.links.isEmpty 39 | ? '' 40 | : '\nSee also:\n' + 41 | syncData.links.map((String link) { 42 | return '- $link'; 43 | }).join('\n') + 44 | '\n'; 45 | 46 | final newIssueUri = 47 | '//github.com/dart-lang/site-webdev/issues/new?title=[${options.branch}]%20${syncData.id}'; 48 | 49 | String readmeContent = ''' 50 | $warningMessage## ${syncData.title} 51 | 52 | Welcome to the example app used in the 53 | [${syncData.title}](${syncData.docHref}) page 54 | of [Dart for the web](${options.webdevURL}). 55 | '''; 56 | 57 | if (syncData.liveExampleHref.isNotEmpty) 58 | readmeContent += ''' 59 | 60 | You can run a [hosted copy](${syncData.liveExampleHref}) of this 61 | sample. Or run your own copy: 62 | 63 | 1. Create a local copy of this repo (use the "Clone or download" button above). 64 | 2. Get the dependencies: `pub get` 65 | '''; 66 | 67 | var step = 3; 68 | if (options.useNewBuild) 69 | readmeContent += ''' 70 | ${step++}. Get the webdev tool: `pub global activate webdev` 71 | '''; 72 | 73 | readmeContent += ''' 74 | ${step++}. Launch a development server: `${options.useNewBuild ? 'webdev' : 'pub'} serve` 75 | ${step++}. In a browser, open [http://localhost:8080](http://localhost:8080) 76 | '''; 77 | 78 | if (!options.useNewBuild) 79 | readmeContent += ''' 80 | 81 | In Dartium, you'll see the app right away. In other modern browsers, 82 | you'll have to wait a bit while pub converts the app. 83 | '''; 84 | 85 | readmeContent += ''' 86 | $linkSection 87 | --- 88 | 89 | *Note:* The content of this repository is generated from the 90 | [Angular docs repository][docs repo] by running the 91 | [dart-doc-syncer](//github.com/dart-lang/dart-doc-syncer) tool. 92 | If you find a problem with this sample's code, please open an [issue][]. 93 | 94 | [docs repo]: ${syncData.repoHref} 95 | [issue]: $newIssueUri 96 | '''; 97 | 98 | final readmeFile = new File(p.join(path, 'README.md')); 99 | _logger.fine('Generating $readmeFile.'); 100 | await readmeFile.writeAsString(readmeContent); 101 | } 102 | -------------------------------------------------------------------------------- /lib/src/generate_doc.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:logging/logging.dart'; 5 | import 'package:path/path.dart' as p; 6 | 7 | import 'generate_readme.dart'; 8 | import 'options.dart'; 9 | import 'remove_doc_tags.dart'; 10 | import 'runner.dart' as Process; // TODO(chalin) tmp name to avoid code changes 11 | import 'util.dart'; 12 | 13 | final Logger _logger = new Logger('update_doc_repo'); 14 | 15 | const whitelist = const ['.css', '.dart', '.html', '.yaml']; 16 | 17 | /// Clears the [out] example repo root and creates fresh content from [webdev]. 18 | Future refreshExampleRepo(Directory webdev, Directory out, 19 | {final /*out*/ List appRoots, String webdevNgPath}) async { 20 | out.createSync(recursive: false); 21 | 22 | // Add all files from webdev folder. 23 | await Process.run('cp', ['-a', p.join(webdev.path, '.'), out.path]); 24 | 25 | assert(appRoots.isEmpty); 26 | appRoots.addAll(getAppRoots(out)); 27 | 28 | for (var appRoot in appRoots) { 29 | await _refreshExample(webdev, appRoot, webdevNgPath: webdevNgPath); 30 | } 31 | await generateReadme(out.path, webdevNgPath: webdevNgPath); 32 | } 33 | 34 | Future _refreshExample(Directory snapshot, Directory out, 35 | {String webdevNgPath}) async { 36 | // Remove unimportant files that would distract the user. 37 | await Process.run('rm', [ 38 | '-f', 39 | p.join(out.path, 'example-config.json'), 40 | p.join(out.path, 'e2e-spec.ts') 41 | ]); 42 | 43 | // Remove source files used solely in support of the prose. 44 | final targetFiles = whitelist.map((ext) => '-name *_[0-9]$ext').join(' -o '); 45 | await Process.run('find', 46 | [out.path]..addAll('( $targetFiles ) -exec rm -f {} +'.split(' '))); 47 | 48 | await _addBoilerplateFiles(snapshot.parent, out); 49 | 50 | // Clean the application code 51 | _logger.fine('Removing doc tags in ${out.path}.'); 52 | await _removeDocTagsFromApplication(out.path); 53 | 54 | // Format the Dart code 55 | _logger.fine('Running dartfmt in ${out.path}.'); 56 | await Process.run('dartfmt', ['-w', p.absolute(out.path)]); 57 | } 58 | 59 | Future _addBoilerplateFiles(Directory exDir, Directory target) async { 60 | for (final boilerPlateDir in _findBoilerPlateDir(exDir)) { 61 | await Process.run('cp', ['-a', '${boilerPlateDir.path}/', target.path]); 62 | } 63 | } 64 | 65 | Iterable _findBoilerPlateDir(Directory dir, 66 | {bool searchParentDir = true}) sync* { 67 | final entities = dir.listSync(followLinks: false); 68 | for (var fsEntity in entities) { 69 | if (fsEntity is! Directory) continue; 70 | if (p.basename(fsEntity.path) == '_boilerplate') yield fsEntity; 71 | } 72 | if (!searchParentDir) return; 73 | final parent = dir.parent; 74 | if (parent == null) return; 75 | yield* _findBoilerPlateDir(parent, 76 | searchParentDir: p.basename(parent.path) != docExampleDirRoot); 77 | } 78 | 79 | /// Rewrites all files under the [path] directory by filtering out the 80 | /// documentation tags. 81 | Future _removeDocTagsFromApplication(String path) async { 82 | if (Process.options.dryRun) return new Future.value(null); 83 | 84 | final files = await new Directory(path) 85 | .list(recursive: true, followLinks: false) 86 | .where((e) => e is File && !e.path.contains('/.')) 87 | .toList(); 88 | _logger.finer('>> Files to be stripped of doctags: ${files.join('\n ')}'); 89 | return Future.wait(files.map(_removeDocTagsFromFile)); 90 | } 91 | 92 | /// Rewrites the [file] by filtering out the documentation tags. 93 | Future _removeDocTagsFromFile(FileSystemEntity file) async { 94 | if (file is File) { 95 | if (whitelist.every((String e) => !file.path.endsWith(e))) return null; 96 | 97 | final content = await file.readAsString(); 98 | final cleanedContent = removeDocTags(content); 99 | 100 | if (content == cleanedContent) return null; 101 | 102 | return file.writeAsString(cleanedContent); 103 | } 104 | return null; 105 | } 106 | -------------------------------------------------------------------------------- /lib/src/generate_gh_pages.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:convert'; 3 | 4 | import 'dart:io'; 5 | 6 | import 'package:logging/logging.dart'; 7 | import 'package:path/path.dart' as p; 8 | import 'package:yaml/yaml.dart'; 9 | 10 | import 'options.dart'; 11 | import 'runner.dart' as Process; // TODO(chalin) tmp name to avoid code changes 12 | import 'util.dart'; 13 | 14 | final Logger _logger = new Logger('generate_gh_pages'); 15 | 16 | Future adjustBaseHref(String pathToWebFolder, String href) async { 17 | _logger.fine( 18 | 'Adjust index.html so that app runs under gh-pages'); 19 | 20 | // If the `index.html` either statically or dynamically sets 21 | // replace that element by a appropriate for serving via GH pages. 22 | final baseHrefEltOrScript = new RegExp(r'|' 23 | r''); 24 | 25 | final appBaseHref = ''; 26 | await transformFile( 27 | p.join(pathToWebFolder, 'index.html'), 28 | (String content) => 29 | content.replaceFirst(baseHrefEltOrScript, appBaseHref)); 30 | } 31 | 32 | final errorOrFailure = new RegExp('^(error|fail)', caseSensitive: false); 33 | 34 | bool isException(ProcessResult r) { 35 | final stderr = r.stderr; 36 | return stderr is! String || stderr.contains(errorOrFailure); 37 | } 38 | 39 | Future buildApp(Directory example) async { 40 | _logger.fine("Building ${example.path}"); 41 | 42 | await Process.runCmd('pub ${options.pubGetOrUpgrade} --no-precompile', 43 | workingDirectory: example.path, isException: isException); 44 | 45 | // Use default build config for now. 46 | // await _generateBuildYaml(example.path); 47 | final pubBuild = options.useNewBuild 48 | ? 'pub run build_runner build --release --delete-conflicting-outputs --output=web:${options.buildDir}' 49 | : 'pub ${options.buildDir}'; 50 | await Process.runCmd(pubBuild, 51 | workingDirectory: example.path, isException: isException); 52 | } 53 | 54 | // Currently unused, but keeping the code in case we need to generate build 55 | // config files later on. 56 | // ignore: unused_element 57 | Future _generateBuildYaml(String projectPath) async { 58 | final pubspecYamlFile = new File(p.join(projectPath, 'pubspec.yaml')); 59 | final pubspecYaml = 60 | loadYaml(await pubspecYamlFile.readAsString()) as YamlMap; 61 | final buildYaml = _buildYaml(pubspecYaml['name'], options.webCompiler, 62 | _extractNgVers(pubspecYaml) ?? 0); 63 | final buildYamlFile = new File(p.join(projectPath, 'build.yaml')); 64 | _logger.info('Generating ${buildYamlFile.path}:\n$buildYaml'); 65 | await buildYamlFile.writeAsString(buildYaml); 66 | } 67 | 68 | // This is currently unused. 69 | // Note: we could use ${pkgName} as the target. 70 | String _buildYaml(String pkgName, String webCompiler, int majorNgVers) => 71 | ''' 72 | targets: 73 | \$default: 74 | builders: 75 | build_web_compilers|entrypoint: 76 | generate_for: 77 | - web/main.dart 78 | options: 79 | compiler: $webCompiler 80 | dart2js_args: 81 | - --fast-startup 82 | - --minify 83 | - --trust-type-annotations 84 | ''' + 85 | (majorNgVers >= 5 86 | ? ''' 87 | - --enable-asserts 88 | # - --preview-dart-2 # This option isn't supported yet 89 | ''' 90 | : ''); 91 | 92 | int _extractNgVers(YamlMap pubspecYaml) { 93 | final ngVersConstraint = pubspecYaml['dependencies']['angular']; 94 | final match = new RegExp(r'\^?(\d+)\.').firstMatch(ngVersConstraint); 95 | if (match == null) return null; 96 | return int.tryParse(match[1]); 97 | } 98 | 99 | // Until we can specify the needed web-compiler on the command line 100 | // (https://github.com/dart-lang/build/issues/801), we'll auto- 101 | // generate build.yml. Ignore the generated build.yaml. 102 | String _filesToExclude() => ''' 103 | .dart_tool/ 104 | .packages 105 | .pub/ 106 | ${options.buildDir}/ 107 | build.yaml 108 | '''; 109 | 110 | /// Files created when the app was built should be ignored. 111 | void excludeTmpBuildFiles(Directory exampleRepo, Iterable appDirPaths) { 112 | final excludeFilePath = p.join(exampleRepo.path, '.git', 'info', 'exclude'); 113 | final excludeFile = new File(excludeFilePath); 114 | final excludeFileAsString = excludeFile.readAsStringSync(); 115 | var filesToExclude = _filesToExclude(); 116 | if (options.ghPagesAppDir.isNotEmpty) filesToExclude += '\n/pubspec.lock'; 117 | final excludes = appDirPaths.length < 2 118 | ? filesToExclude 119 | : appDirPaths.map((p) => '/$p').join('\n'); 120 | if (!excludeFileAsString.contains(filesToExclude)) { 121 | _logger.fine(' > Adding tmp build files to $excludeFilePath: ' + 122 | filesToExclude.replaceAll('\n', ' ')); 123 | excludeFile.writeAsStringSync('$excludeFileAsString\n$excludes\n'); 124 | } 125 | } 126 | 127 | Future createBuildInfoFile( 128 | String pathToWebFolder, String exampleName, String commitHash) async { 129 | final buildInfoFile = new File(p.join(pathToWebFolder, buildInfoFileName)); 130 | 131 | // We normalize the build timestamp to TZ=US/Pacific, which is easier 132 | // to do using the OS date command. Failing that use DateTime.now(). 133 | final r = await Process.run('date', [], environment: {'TZ': 'US/Pacific'}); 134 | final date = r.stdout as String; 135 | 136 | final json = { 137 | 'build-time': date.isNotEmpty ? date.trim() : new DateTime.now(), 138 | 'commit-sha': 139 | 'https://github.com/angular-examples/$exampleName/commit/$commitHash' 140 | }; 141 | buildInfoFile.writeAsStringSync(jsonEncode(json)); 142 | } 143 | -------------------------------------------------------------------------------- /lib/src/git_repository.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:logging/logging.dart'; 5 | import 'package:path/path.dart' as p; 6 | 7 | import 'runner.dart' as Process; // TODO(chalin) tmp name to avoid code changes 8 | import 'options.dart'; 9 | import 'util.dart'; 10 | 11 | class GitRepositoryFactory { 12 | GitRepository create(String directory, [String branch = defaultGitBranch]) => 13 | new GitRepository(directory, branch); 14 | } 15 | 16 | class GitRepository { 17 | final _logger = new Logger('GitRepository'); 18 | final String branch; 19 | 20 | /// Local path to directory where this repo will reside. 21 | final String dirPath; 22 | final Directory dir; 23 | 24 | GitRepository(this.dirPath, this.branch) : dir = new Directory(dirPath); 25 | 26 | /// Clones the git [repository]'s [branch] into this [dirPath]. 27 | Future cloneFrom(String repository) async { 28 | _logger.fine('Cloning $repository ($branch) into $dirPath.'); 29 | dryRunMkDir(dirPath); 30 | if (dir.existsSync()) { 31 | _logger.fine(' > clone already exists for $dirPath'); 32 | await checkout(); 33 | return; 34 | } 35 | try { 36 | await _git(['clone', '-b', branch, repository, dirPath]); 37 | return; 38 | } catch (e) { 39 | // if (!e.toString().contains('Remote branch $branch not found')) 40 | throw e; 41 | } 42 | // Disable example repo branch creation for now. Handle it outside the dds. 43 | // _logger.info('Branch $branch does not exist, creating it from master'); 44 | // await _git(['clone', '-b', defaultGitBranch, repository, directory]); 45 | // await _git(['checkout', '-b', branch], workingDirectory: directory); 46 | } 47 | 48 | Future checkout() async { 49 | _logger.fine('Checkout $branch.'); 50 | await _git(['checkout', branch], workingDirectory: dirPath); 51 | } 52 | 53 | /// Delete all files under [dirPath]/subdir. 54 | Future delete([String subdir = '']) async { 55 | final dir = p.join(dirPath, subdir); 56 | _logger.fine('Git rm * under $dir.'); 57 | if (new Directory(dir).existsSync()) { 58 | try { 59 | await _git(['rm', '-rf', '*'], workingDirectory: dir); 60 | return; 61 | } catch (e) { 62 | if (!e.toString().contains('did not match any files')) throw e; 63 | } 64 | } 65 | _logger.fine(' > No matching files; probably already removed ($dir)'); 66 | } 67 | 68 | /// Push given branch, or [branch] to origin from [dirPath]. 69 | Future push([String _branch]) async { 70 | final branch = _branch ?? this.branch; 71 | _logger.fine('Pushing $branch from $dirPath.'); 72 | await _git(['push', '--set-upstream', 'origin', branch], 73 | workingDirectory: dirPath); 74 | } 75 | 76 | Future update({String commitMessage}) async { 77 | await checkout(); 78 | 79 | _logger.fine('Staging local changes for $dirPath.'); 80 | await _git(['add', '.'], workingDirectory: dirPath); 81 | 82 | _logger.fine('Committing changes for $dirPath.'); 83 | await _git(['commit', '-m', commitMessage], workingDirectory: dirPath); 84 | } 85 | 86 | /// Clones the git [repository] into this [dirPath]. 87 | Future updateGhPages(Iterable appRoots, String message) async { 88 | await checkoutGhPages(); 89 | 90 | // Remove all previous content of branch 91 | await delete(options.ghPagesAppDir); 92 | 93 | // Copy newly built app files 94 | final baseDest = p.join(dirPath, options.ghPagesAppDir); 95 | for (var appRoot in appRoots) { 96 | final web = pathToBuiltApp(appRoot); 97 | _logger.fine('Copy from $web to $dirPath.'); 98 | final dest = appRoot.isEmpty ? baseDest : p.join(baseDest, appRoot); 99 | await Process.run('mkdir', ['-p', dest]); 100 | await Process.run('cp', ['-a', p.join(web, '.'), dest], 101 | workingDirectory: dirPath); 102 | // Deploy pubspec.lock so we know what the app was built with. 103 | await Process.run('cp', [p.join(appRoot, 'pubspec.lock'), dest], 104 | workingDirectory: dirPath); 105 | } 106 | 107 | // Clean out temporary files 108 | await Process.run( 109 | 'find', 110 | [baseDest]..addAll( 111 | '( -name *.ng_*.json -o -name *.ng_placeholder ) -exec rm -f {} +' 112 | .split(' '))); 113 | 114 | _logger.fine('Committing gh-pages changes for $dirPath.'); 115 | await _git(['add', '.'], workingDirectory: dirPath); 116 | final statusLines = (await this.statusLines()) 117 | ..removeWhere( 118 | (line) => line.startsWith('M') && line.contains(buildInfoFileName)); 119 | if (statusLines.length == 0) { 120 | final msg = 121 | 'At most the $buildInfoFileName file has changed: nothing to commit'; 122 | _logger.fine(' $msg'); 123 | throw msg; 124 | } 125 | await _git(['commit', '-m', message], workingDirectory: dirPath); 126 | } 127 | 128 | /// Output from `git status --short` 129 | Future> statusLines({Pattern removePattern}) async { 130 | final status = await _git(['status', '--short'], workingDirectory: dirPath); 131 | final statusLines = status.split('\n') 132 | // I don't think the output can contain empty lines, but just in case: 133 | ..removeWhere((statusLine) => statusLine.isEmpty); 134 | if (removePattern != null) 135 | statusLines.removeWhere((line) => line.contains(removePattern)); 136 | return statusLines; 137 | } 138 | 139 | /// Fetch and checkout gh-pages. If it does not exist then create it as a 140 | /// new orphaned branch. 141 | Future checkoutGhPages() async { 142 | _logger.fine('Checkout gh-pages.'); 143 | 144 | try { 145 | await _git(['fetch', 'origin', 'gh-pages'], workingDirectory: dirPath); 146 | await _git(['checkout', 'gh-pages'], workingDirectory: dirPath); 147 | } catch (e) { 148 | if (e is! GitException || 149 | !e.message.contains("Couldn't find remote ref gh-pages")) throw e; 150 | _logger 151 | .fine(' Unable to fetch gh-pages: ${(e as GitException).message}'); 152 | _logger.fine(' Creating new --orphan gh-pages branch.'); 153 | await _git(['checkout', '--orphan', 'gh-pages'], 154 | workingDirectory: dirPath); 155 | await delete(); 156 | } 157 | } 158 | 159 | /// Returns the commit hash at HEAD. 160 | Future getCommitHash({bool short: false}) async { 161 | if (Process.options.dryRun) return new Future.value('COMMIT_HASH_CODE'); 162 | 163 | final args = "rev-parse${short ? ' --short' : ''} HEAD".split(' '); 164 | final hash = await _git(args, workingDirectory: dirPath); 165 | 166 | return hash.split('\n')[0].trim(); 167 | } 168 | 169 | Future git(/*String|List*/ dynamic args) => 170 | _git(args is String ? args.split(' ') : args, workingDirectory: dirPath); 171 | } 172 | 173 | class GitException implements Exception { 174 | final String message; 175 | 176 | GitException(this.message); 177 | 178 | String toString() { 179 | if (message == null) return "GitException"; 180 | return "GitException: $message"; 181 | } 182 | } 183 | 184 | Future _git(List arguments, 185 | {String workingDirectory, Exception mkException(String msg)}) async { 186 | final r = await Process.run('git', arguments, 187 | workingDirectory: workingDirectory, 188 | mkException: (msg) => new GitException(msg)); 189 | return r.stdout; 190 | } 191 | -------------------------------------------------------------------------------- /lib/src/options.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:args/args.dart'; 4 | import 'package:path/path.dart' as p; 5 | 6 | /// Global options 7 | class Options { 8 | String branch = defaultGitBranch; 9 | // buildDir isn't user configurable (yet). 10 | String buildDir = 'build'; // Path to build directory relative to package root 11 | bool dryRun = true; 12 | bool forceBuild = false; 13 | String ghPagesAppDir; 14 | bool keepTmp = false; 15 | bool pubGet = false; 16 | String get pubGetOrUpgrade => pubGet ? 'get' : 'upgrade'; 17 | bool push = true; 18 | /*@nullable*/ RegExp match, skip; 19 | String url = 'main'; 20 | String user = 'dart-lang'; 21 | bool verbose = false; 22 | // Usage help text generated by arg parser 23 | String usage = ''; 24 | /*@nullable*/ String workDir; 25 | String webCompiler = 'dart2js'; 26 | 27 | bool get isValidWorkDir { 28 | if (workDir == null) return true; 29 | final path = new Directory(workDir); 30 | return path.existsSync(); 31 | } 32 | 33 | int get _ngVers => int.parse(ghPagesAppDir); 34 | bool get useNewBuild => _ngVers >= 5; 35 | 36 | RegExp get _buildDirRE => new RegExp(r'\b' + buildDir + r'\b'); 37 | bool containsBuildDir(String path) => _buildDirRE.hasMatch(path); 38 | 39 | String get webdevURL => url == 'main' ? _webdevURL : _webdevDevURL; 40 | } 41 | 42 | Options options = new Options(); 43 | 44 | // TODO: make these configurable? (with defaults as given) 45 | const buildInfoFileName = 'build-info.json'; 46 | const defaultGitBranch = 'master'; 47 | const docExampleDirRoot = 'examples'; 48 | const exampleConfigFileName = '.docsync.json'; 49 | const tempFolderNamePrefix = 'dds-'; 50 | const _webdevURL = 'https://webdev.dartlang.org'; 51 | const _webdevDevURL = 'https://webdev-dartlang-org-dev.firebaseapp.com'; 52 | const readmeMd = 'README.md'; 53 | 54 | Directory initWorkingDir() { 55 | final tmpEnvVar = Platform.environment['TMP']; 56 | if (tmpEnvVar != null) { 57 | final dir = new Directory(tmpEnvVar); 58 | if (dir.existsSync()) return dir; 59 | } 60 | return Directory.systemTemp; 61 | } 62 | 63 | Directory workDir; 64 | 65 | const Map _help = const { 66 | 'branch': '\nGit branch to fetch webdev and examples from', 67 | 'gh-pages-app-dir': '\nDirectory in which the generated example apps ' 68 | 'will be placed (gh-pages branch)\n', 69 | 'dry-run': 'Show which commands would be executed but make (almost) ' 70 | 'no changes;\nonly the temporary directory will be created', 71 | 'force-build': 'Forces build of example app when sources have not changed', 72 | 'help': 'Show this usage information', 73 | 'keep-tmp': 'Do not delete temporary working directory once done', 74 | 'pub-get': 'Use `pub get` instead of `pub upgrade` before building apps', 75 | 'push': 'Prepare updates and push to example repo', 76 | 'match': '\n' 77 | 'Sync all examples having a data file ($exampleConfigFileName)\n' 78 | 'and whose repo path matches the given regular expression;\n' 79 | 'use "." to match all', 80 | 'skip': '\nNegative filter applied to the project list created ' 81 | 'by use of the --match option', 82 | 'url': '[dev|main]\nWebdev site URL to use in generated README.', 83 | 'user': '\nGitHub id of repo to fetch examples from', 84 | 'web-compiler': ', either dart2js or dartdevc', 85 | 'work-dir': '\nPath to a working directory; when unspecified ' 86 | 'a system-generated path to a temporary directory is used' 87 | }; 88 | 89 | /// Processes command line options and returns remaining arguments. 90 | List processArgs(List args) { 91 | ArgParser argParser = new ArgParser(allowTrailingOptions: true) 92 | ..addFlag( 93 | 'help', 94 | abbr: 'h', 95 | negatable: false, 96 | help: _help['help'], 97 | ) 98 | ..addOption( 99 | 'branch', 100 | abbr: 'b', 101 | help: _help['branch'], 102 | defaultsTo: options.branch, 103 | ) 104 | ..addFlag( 105 | 'dry-run', 106 | abbr: 'n', 107 | negatable: false, 108 | help: _help['dry-run'], 109 | ) 110 | ..addFlag( 111 | 'force-build', 112 | abbr: 'f', 113 | negatable: false, 114 | help: _help['force-build'], 115 | ) 116 | ..addOption( 117 | 'gh-pages-app-dir', 118 | abbr: 'g', 119 | help: _help['gh-pages-app-dir'], 120 | defaultsTo: options.ghPagesAppDir, 121 | ) 122 | ..addFlag( 123 | 'keep-tmp', 124 | abbr: 'k', 125 | negatable: false, 126 | help: _help['keep-tmp'], 127 | ) 128 | ..addFlag( 129 | 'pub-get', 130 | help: _help['pub-get'], 131 | negatable: false, 132 | defaultsTo: options.pubGet, 133 | ) 134 | ..addFlag( 135 | 'push', 136 | abbr: 'p', 137 | help: _help['push'], 138 | defaultsTo: options.push, 139 | ) 140 | ..addOption( 141 | 'match', 142 | abbr: 'm', 143 | help: _help['match'], 144 | ) 145 | ..addOption( 146 | 'skip', 147 | help: _help['skip'], 148 | ) 149 | ..addOption( 150 | 'url', 151 | help: _help['url'], 152 | defaultsTo: options.url, 153 | ) 154 | ..addOption( 155 | 'user', 156 | abbr: 'u', 157 | help: _help['user'], 158 | defaultsTo: options.user, 159 | ) 160 | ..addFlag( 161 | 'verbose', 162 | abbr: 'v', 163 | negatable: false, 164 | defaultsTo: options.verbose, 165 | ) 166 | ..addOption( 167 | 'web-compiler', 168 | help: _help['web-compiler'], 169 | defaultsTo: options.webCompiler, 170 | ) 171 | ..addOption( 172 | 'work-dir', 173 | abbr: 'w', 174 | help: _help['work-dir'], 175 | ); 176 | 177 | var argResults; 178 | try { 179 | argResults = argParser.parse(args); 180 | } on FormatException catch (e) { 181 | printUsageAndExit(e.message, 0); 182 | } 183 | 184 | options.usage = argParser.usage; 185 | if (argResults['help']) printUsageAndExit(); 186 | 187 | options 188 | ..branch = argResults['branch'] 189 | ..dryRun = argResults['dry-run'] 190 | ..forceBuild = argResults['force-build'] 191 | ..ghPagesAppDir = argResults['gh-pages-app-dir'] 192 | ..keepTmp = argResults['keep-tmp'] 193 | ..pubGet = argResults['pub-get'] 194 | ..push = argResults['push'] 195 | ..match = 196 | argResults['match'] != null ? new RegExp(argResults['match']) : null 197 | ..skip = argResults['skip'] != null ? new RegExp(argResults['skip']) : null 198 | ..url = argResults['url'] 199 | ..user = argResults['user'] 200 | ..verbose = argResults['verbose'] 201 | ..webCompiler = argResults['web-compiler'] 202 | ..workDir = argResults['work-dir']; 203 | 204 | validateAndNormalizeGhPagesAppDir(); 205 | 206 | if (options.webCompiler != 'dart2js' && options.webCompiler != 'dartdevc') 207 | printUsageAndExit("Invalid --web-compiler '${options.webCompiler}'"); 208 | 209 | if (!options.isValidWorkDir) 210 | printUsageAndExit("Invalid --workDir '${options.workDir}'"); 211 | 212 | workDir = options.workDir == null 213 | ? initWorkingDir().createTempSync(tempFolderNamePrefix) 214 | : new Directory(options.workDir); 215 | 216 | return argResults.rest; 217 | } 218 | 219 | void validateAndNormalizeGhPagesAppDir() { 220 | // FIXME: revert temporary change once all scripts have been updated. Also make '' the new default. 221 | // Temporarily making --gh-pages-app-dir mandatory. 222 | if (options.ghPagesAppDir == null) 223 | printUsageAndExit("Option --gh-pages-app-dir is currently mandatory, " 224 | "and it usually matches the major Angular version number being used " 225 | "by the example apps; e.g., --gh-pages-app-dir=4. " 226 | "Use '' if you reall mean to use an empty path"); 227 | 228 | if (options.ghPagesAppDir.startsWith('/')) 229 | printUsageAndExit("Invalid --gh-pages-app-dir '${options.ghPagesAppDir}'; " 230 | 'path must be relative'); 231 | 232 | if (options.ghPagesAppDir.isNotEmpty && options.ghPagesAppDir.endsWith('/')) 233 | options.ghPagesAppDir = 234 | options.ghPagesAppDir.substring(0, options.ghPagesAppDir.length - 1); 235 | } 236 | 237 | void printUsageAndExit([String _msg, int exitCode = 1]) { 238 | var msg = 'Syncs Angular docs example apps'; 239 | if (_msg != null) msg = _msg; 240 | print(''' 241 | 242 | $msg. 243 | 244 | Usage: ${p.basenameWithoutExtension(Platform.script.path)} [options] [ | ] 245 | 246 | ${options.usage} 247 | '''); 248 | exit(exitCode); 249 | } 250 | -------------------------------------------------------------------------------- /lib/src/git_documentation_updater.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:path/path.dart' as p; 5 | import 'package:logging/logging.dart'; 6 | 7 | import '../documentation_updater.dart'; 8 | import '../example2uri.dart'; 9 | import 'git_repository.dart'; 10 | import 'generate_doc.dart'; 11 | import 'generate_gh_pages.dart'; 12 | import 'options.dart'; 13 | import 'util.dart'; 14 | 15 | final Logger _logger = new Logger('update_doc_repo'); 16 | 17 | class GitDocumentationUpdater implements DocumentationUpdater { 18 | final GitRepositoryFactory _gitFactory; 19 | final String _webdevRepoUri = 20 | 'https://github.com/${options.user}/site-webdev'; 21 | GitRepository webdevRepo; 22 | 23 | GitDocumentationUpdater(this._gitFactory); 24 | 25 | @override 26 | Future updateMatchingRepo(RegExp match, RegExp skip, 27 | {bool push, bool clean}) async { 28 | int updateCount = 0; 29 | try { 30 | await _cloneWebdevRepoIntoWorkDir(); 31 | final files = await new Directory(webdevRepo.dirPath) 32 | .list(recursive: true) 33 | .where( 34 | (e) => e is File && p.basename(e.path) == exampleConfigFileName) 35 | .toList(); 36 | files.sort((e1, e2) => e1.path.compareTo(e2.path)); 37 | for (var e in files) { 38 | var dartDir = p.dirname(e.path).substring(webdevRepo.dirPath.length); 39 | if (dartDir.startsWith('/')) dartDir = dartDir.substring(1); 40 | final e2u = new Example2Uri(dartDir); 41 | if (match.hasMatch(dartDir) && 42 | (skip == null || !skip.hasMatch(dartDir))) { 43 | var updated = await updateRepository(e2u.path, e2u.repositoryUri, 44 | clean: clean, push: push); 45 | if (updated) updateCount++; 46 | } else if (options.verbose) { 47 | print('Skipping ${e2u.path}'); 48 | } 49 | } 50 | } on GitException catch (e) { 51 | _logger.severe(e.message); 52 | } finally { 53 | await _deleteWorkDir(clean); 54 | } 55 | 56 | print("Example repo(s) updated: $updateCount"); 57 | return updateCount; 58 | } 59 | 60 | /// [rrrExamplePath] is a repo-root-relative example path, e.g., 61 | /// 'examples/ng/doc/quickstart' 62 | @override 63 | Future updateRepository(String rrrExamplePath, String outRepositoryUri, 64 | {String exampleName: '', bool push: true, bool clean: true}) async { 65 | // final examplePath = getExamplePath(rrrExamplePath); 66 | if (exampleName.isEmpty) exampleName = getExampleName(rrrExamplePath); 67 | print('Processing $rrrExamplePath'); 68 | 69 | var updated = false, onlyReadMeChanged = false; 70 | String commitMessage; 71 | final List appRoots = []; 72 | 73 | try { 74 | await _cloneWebdevRepoIntoWorkDir(); 75 | 76 | // Clone [outRepository] into working directory. 77 | final outPath = p.join(workDir.path, exampleName); 78 | final outRepo = _gitFactory.create(outPath, options.branch); 79 | if (outRepo.dir.existsSync()) { 80 | _logger.fine( 81 | ' > repo exists; assuming source files have already been updated ($outPath)'); 82 | outRepo.checkout(); 83 | appRoots.addAll(getAppRoots(outRepo.dir)); 84 | } else { 85 | await outRepo.cloneFrom(outRepositoryUri); 86 | 87 | // Remove existing content as we will generate an updated version. 88 | await outRepo.delete(); 89 | 90 | _logger.fine( 91 | 'Generating ${appRoots.length} updated example app(s) into $outPath.'); 92 | final exampleFolder = p.join(webdevRepo.dirPath, rrrExamplePath); 93 | await refreshExampleRepo( 94 | new Directory(exampleFolder), new Directory(outRepo.dirPath), 95 | appRoots: appRoots, webdevNgPath: rrrExamplePath); 96 | 97 | commitMessage = await _createCommitMessage(webdevRepo, rrrExamplePath); 98 | 99 | updated = await __handleUpdate( 100 | () => _update(outRepo, commitMessage, push), 101 | 'Example source changed', 102 | exampleName, 103 | outRepo.branch); 104 | 105 | onlyReadMeChanged = updated && 106 | (await outRepo.git('diff-tree --no-commit-id --name-only -r HEAD')) 107 | .trim() == 108 | readmeMd; 109 | } 110 | 111 | var msg = options.forceBuild 112 | ? 'Force build requested' 113 | : updated 114 | ? "Changes to sources detected${onlyReadMeChanged ? ', but only in $readmeMd file' : ''}" 115 | : null; 116 | if (msg != null) print(' $msg'); 117 | 118 | if (options.forceBuild || updated && !onlyReadMeChanged) { 119 | if (commitMessage == null) 120 | commitMessage = 121 | await _createCommitMessage(webdevRepo, rrrExamplePath); 122 | 123 | updated = await __handleUpdate( 124 | () => _updateGhPages( 125 | outRepo, exampleName, appRoots, commitMessage, push), 126 | 'App files have changed', 127 | exampleName, 128 | 'gh-pages') || 129 | updated; 130 | } else { 131 | final msg = 'not built (to force use `--force-build`)'; 132 | print(" $exampleName (gh-pages): $msg"); 133 | } 134 | } on GitException catch (e) { 135 | _logger.severe(e.message); 136 | } finally { 137 | await _deleteWorkDir(clean); 138 | } 139 | return updated; 140 | } 141 | 142 | final _errorOrFatal = new RegExp(r'error|fatal', caseSensitive: false); 143 | Future __handleUpdate(Future update(), String infoMsg, String exampleName, 144 | String branch) async { 145 | var updated = false; 146 | try { 147 | await update(); 148 | print(" $infoMsg: updated $exampleName ($branch)"); 149 | updated = true; 150 | } catch (e, st) { 151 | var es = e.toString(); 152 | if (es.contains(_errorOrFatal)) { 153 | rethrow; 154 | } else if (!es.contains('nothing to commit')) { 155 | print(es); 156 | _logger.finest(st); 157 | } else { 158 | print(" $exampleName ($branch): nothing to commit"); 159 | } 160 | } 161 | return updated; 162 | } 163 | 164 | Future _deleteWorkDir(bool clean) async { 165 | if (!clean) { 166 | _logger.fine('Keeping ${workDir.path}.'); 167 | } else if (await workDir.exists()) { 168 | _logger.fine('Deleting ${workDir.path}.'); 169 | await workDir.delete(recursive: true); 170 | } 171 | } 172 | 173 | /// Clone webdev repo into working directory, if it is not already present. 174 | Future _cloneWebdevRepoIntoWorkDir() async { 175 | if (webdevRepo != null) return; 176 | final webdevRepoPath = p.join(workDir.path, 'site_webdev_ng'); 177 | webdevRepo = _gitFactory.create(webdevRepoPath, options.branch); 178 | await webdevRepo.cloneFrom(_webdevRepoUri); 179 | } 180 | 181 | /// Generates a commit message containing the commit hash of the Angular docs 182 | /// snapshot used to generate the content of the example repository. 183 | Future _createCommitMessage( 184 | GitRepository repo, String webdevNgPath) async { 185 | final short = await repo.getCommitHash(short: true); 186 | final long = await repo.getCommitHash(); 187 | 188 | return 'Sync with $short\n\n' 189 | 'Synced with dart-lang/site-webdev ${repo.branch} branch, commit $short:\n' 190 | '$_webdevRepoUri/tree/$long/$webdevNgPath'; 191 | } 192 | 193 | /// Updates the branch with the latest cleaned example application code. 194 | Future _update(GitRepository repo, String message, bool push) async { 195 | await repo.update(commitMessage: message); 196 | if (push) { 197 | await repo.push(); 198 | } else { 199 | _logger.fine('NOT Pushing changes for ${repo.dirPath}.'); 200 | } 201 | } 202 | 203 | /// Updates the gh-pages branch with the latest built app(s). 204 | Future _updateGhPages(GitRepository exampleRepo, String exampleName, 205 | List appRoots, String commitMessage, bool push) async { 206 | final relativeAppRoots = 207 | appRoots.map((d) => stripPathPrefix(exampleRepo.dirPath, d.path)); 208 | excludeTmpBuildFiles(exampleRepo.dir, relativeAppRoots); 209 | 210 | final commitHash = await exampleRepo.getCommitHash(); 211 | 212 | for (var appRoot in appRoots) { 213 | if (appRoots.length > 1) 214 | print(' Building app ${stripPathPrefix(workDir.path, appRoot.path)}'); 215 | await _buildApp(appRoot, exampleName, commitHash); 216 | } 217 | 218 | await exampleRepo.updateGhPages(relativeAppRoots, commitMessage); 219 | if (push) { 220 | await exampleRepo.push('gh-pages'); 221 | } else { 222 | _logger 223 | .info('NOT Pushing changes to gh-pages for ${exampleRepo.dirPath}.'); 224 | } 225 | } 226 | 227 | Future _buildApp(Directory dir, String exampleName, String commitHash) async { 228 | await buildApp(dir); 229 | var href = '/$exampleName/' + 230 | (options.ghPagesAppDir.isEmpty ? '' : '${options.ghPagesAppDir}/'); 231 | if (!dir.path.endsWith(exampleName)) { 232 | href += p.basename(dir.path) + '/'; 233 | } 234 | final pathToBuildWeb = pathToBuiltApp(dir.path); 235 | await adjustBaseHref(pathToBuildWeb, href); 236 | await createBuildInfoFile(pathToBuildWeb, exampleName, commitHash); 237 | } 238 | } 239 | --------------------------------------------------------------------------------