From 27279d70b20e7ed71801b3a56bc52e8bdf32b831 Mon Sep 17 00:00:00 2001 From: reduckted Date: Sun, 30 Jul 2017 19:25:19 +1000 Subject: [PATCH] Initial commit. --- .editorconfig | 8 + .gitignore | 90 + .vscode/launch.json | 28 + .vscode/settings.json | 11 + .vscode/tasks.json | 30 + .vscodeignore | 9 + CHANGELOG.md | 8 + LICENSE | 21 + README.md | 72 + media/copy-file-explorer.png | Bin 0 -> 24044 bytes media/copy-file-tab.png | Bin 0 -> 14000 bytes media/copy-selection.png | Bin 0 -> 24702 bytes package-lock.json | 2229 +++++++++++++++++ package.json | 133 + src/commands/CopyLinkCommand.ts | 37 + src/commands/CopyLinkToFileCommand.ts | 19 + src/commands/CopyLinkToSelectionCommand.ts | 34 + src/configuration/CustomServerProvider.ts | 22 + src/constants.ts | 3 + src/extension.ts | 76 + src/git/Git.ts | 22 + src/git/GitInfo.ts | 8 + src/git/GitInfoFinder.ts | 112 + src/links/BitbucketCloudHandler.ts | 50 + src/links/BitbucketServerHandler.ts | 72 + src/links/GitHubHandler.ts | 65 + src/links/LinkHandler.ts | 133 + src/links/LinkHandlerFinder.ts | 32 + src/utilities/Clipboard.ts | 10 + src/utilities/Selection.ts | 8 + src/utilities/ServerUrl.ts | 8 + test/commands/CopyLinkToFileCommand.test.ts | 72 + .../CopyLinkToSelectionCommand.test.ts | 80 + .../CustomServiceProvider.test.ts | 79 + test/extension.test.ts | 202 ++ test/git/Git.test.ts | 21 + test/git/GitInfoFinder.test.ts | 76 + test/index.ts | 9 + test/links/BitbucketCloudHandler.test.ts | 146 ++ test/links/BitbucketServerHandler.test.ts | 243 ++ test/links/GitHubHandler.test.ts | 244 ++ test/links/LinkHandlerFinder.test.ts | 74 + test/test-helpers/MockLinkHandler.ts | 40 + test/test-helpers/data/10lines.txt | 9 + test/tslint.json | 10 + tsconfig.json | 17 + tslint.json | 31 + 47 files changed, 4703 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 .vscodeignore create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 media/copy-file-explorer.png create mode 100644 media/copy-file-tab.png create mode 100644 media/copy-selection.png create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/commands/CopyLinkCommand.ts create mode 100644 src/commands/CopyLinkToFileCommand.ts create mode 100644 src/commands/CopyLinkToSelectionCommand.ts create mode 100644 src/configuration/CustomServerProvider.ts create mode 100644 src/constants.ts create mode 100644 src/extension.ts create mode 100644 src/git/Git.ts create mode 100644 src/git/GitInfo.ts create mode 100644 src/git/GitInfoFinder.ts create mode 100644 src/links/BitbucketCloudHandler.ts create mode 100644 src/links/BitbucketServerHandler.ts create mode 100644 src/links/GitHubHandler.ts create mode 100644 src/links/LinkHandler.ts create mode 100644 src/links/LinkHandlerFinder.ts create mode 100644 src/utilities/Clipboard.ts create mode 100644 src/utilities/Selection.ts create mode 100644 src/utilities/ServerUrl.ts create mode 100644 test/commands/CopyLinkToFileCommand.test.ts create mode 100644 test/commands/CopyLinkToSelectionCommand.test.ts create mode 100644 test/configuration/CustomServiceProvider.test.ts create mode 100644 test/extension.test.ts create mode 100644 test/git/Git.test.ts create mode 100644 test/git/GitInfoFinder.test.ts create mode 100644 test/index.ts create mode 100644 test/links/BitbucketCloudHandler.test.ts create mode 100644 test/links/BitbucketServerHandler.test.ts create mode 100644 test/links/GitHubHandler.test.ts create mode 100644 test/links/LinkHandlerFinder.test.ts create mode 100644 test/test-helpers/MockLinkHandler.ts create mode 100644 test/test-helpers/data/10lines.txt create mode 100644 test/tslint.json create mode 100644 tsconfig.json create mode 100644 tslint.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..2fc34bb --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +indent_size = 4 +charset = utf-8 +end_of_line = crlf +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..27d4b29 --- /dev/null +++ b/.gitignore @@ -0,0 +1,90 @@ +.vscode/* +.vscode-test/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +out/ diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..cd6b87b --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,28 @@ +// A launch configuration that compiles the extension and then opens it inside a new window +{ + "version": "0.1.0", + "configurations": [ + { + "name": "Launch Extension", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": ["--extensionDevelopmentPath=${workspaceRoot}" ], + "stopOnEntry": false, + "sourceMaps": true, + "outFiles": [ "${workspaceRoot}/out/src/**/*.js" ], + "preLaunchTask": "npm" + }, + { + "name": "Launch Tests", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test" ], + "stopOnEntry": false, + "sourceMaps": true, + "outFiles": [ "${workspaceRoot}/out/test/**/*.js" ], + "preLaunchTask": "npm" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..105f6c9 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "files.exclude": { + "out": true, + "node_modules": true, + ".vscode-test": true + }, + "search.exclude": { + "out": true, + ".vscode-test": true + } +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..1e37eb7 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,30 @@ +// Available variables which can be used inside of strings. +// ${workspaceRoot}: the root folder of the team +// ${file}: the current opened file +// ${fileBasename}: the current opened file's basename +// ${fileDirname}: the current opened file's dirname +// ${fileExtname}: the current opened file's extension +// ${cwd}: the current working directory of the spawned process + +// A task runner that calls a custom npm script that compiles the extension. +{ + "version": "0.1.0", + + // we want to run npm + "command": "npm", + + // the command is a shell script + "isShellCommand": true, + + // show the output window only if unrecognized errors occur. + "showOutput": "silent", + + // we run the custom script "compile" as defined in package.json + "args": ["run", "compile", "--loglevel", "silent"], + + // The tsc compiler is started in watching mode + "isBackground": true, + + // use the standard tsc in watch mode problem matcher to find compile problems in the output. + "problemMatcher": "$tsc-watch" +} \ No newline at end of file diff --git a/.vscodeignore b/.vscodeignore new file mode 100644 index 0000000..5ff3c19 --- /dev/null +++ b/.vscodeignore @@ -0,0 +1,9 @@ +.vscode/** +.vscode-test/** +out/test/** +test/** +src/** +**/*.map +.gitignore +tsconfig.json +vsc-extension-quickstart.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d25e9f2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,8 @@ +# Change Log +All notable changes to this extension will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2017-06-20 +- Initial release diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8864d4a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..63b54b1 --- /dev/null +++ b/README.md @@ -0,0 +1,72 @@ +# Git Web Links for VS Code + +Copy links to files in their online Git repositories from inside Visual Studio Code. + +Works with GitHub, GitHub Enterprise, Bitbucket Server and Bitbucket Cloud. + +:information_source: For GitHub Enterprise and Bitbucket Server, there is some configuration required. [See below for more details](#github-enterprise-and-bitbucket-server). + +## Copy Link to File + +To copy a link to the file on GitHub (or Bitbucket), right-click on the file's tab and select _Copy Web Link to File_. + +![Copy Link to File](media/copy-file-tab.png) + +You can also right-click on a file in Explorer panel and select _Copy Web Link to File_. + +![Copy Link to File](media/copy-file-explorer.png) + +## Copy Link to Selection + +To copy a link to a particular line in the file, right-click on the line in the editor and select _Copy Web Link to Selection_. + +If you want to copy a link to a range of lines, just select the lines first. + +![Copy Link to Selection](media/copy-selection.png) + +## GitHub Enterprise and Bitbucket Server + +If you use GitHub Enterprise or Bitbucket Server, you will need to tell the extension the URLs of those servers. Do this in your user settings file (_File -> Preferences -> Settings_). You need to specify the base HTTP/HTTPS URL of the server, and if you use SSH, the base SSH URL. + +Make sure you include any port numbers (if it's not port 80) and context paths. + +### GitHub Enterprise + +```json +"gitweblinks.gitHubEnterprise": [ + { + "baseUrl": "https://local-github", + "sshUrl": "git@local-github" + } +] +``` + +### Bitbucket Server + +```json +"gitweblinks.bitbucketServer": [ + { + "baseUrl": "https://local-bitbucket:7990/context", + "sshUrl": "git@local-bitbucket:7999" + } +] +``` + +## Commands + +There are two commands provided by this extension: + +* `gitweblinks.copyFile` +* `gitweblinks.copySelection` + +## Requirements + +This extension requires Git to already be installed and on your PATH. If this isn't suitable for you and you'd prefer to specify the location of Git, please open a new issue in this repository and I'll see what I can do :) + +## Release Notes + +Users appreciate release notes as you update your extension. + +### 1.0.0 + +Initial release diff --git a/media/copy-file-explorer.png b/media/copy-file-explorer.png new file mode 100644 index 0000000000000000000000000000000000000000..85b23862c1735a3b95198ec4dc19a8d0f494d309 GIT binary patch literal 24044 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 literal 0 HcmV?d00001 diff --git a/media/copy-file-tab.png b/media/copy-file-tab.png new file mode 100644 index 0000000000000000000000000000000000000000..8be3dfb28e45c2f9408f40071f3496af7bf3eacb GIT binary patch literal 14000 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 NhPtLYRod5L{{gxIY3Tp} literal 0 HcmV?d00001 diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..2f853ee --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2229 @@ +{ + "name": "gitweblinks", + "version": "1.0.0", + "lockfileVersion": 1, + "dependencies": { + "@types/chai": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.0.1.tgz", + "integrity": "sha512-DWrdkraJO+KvBB7+Jc6AuDd2+fwV6Z9iK8cqEEoYpcurYrH7GiUZmwjFuQIIWj5HhFz6NsSxdN72YMIHT7Fy2Q==", + "dev": true + }, + "@types/copy-paste": { + "version": "1.1.30", + "resolved": "https://registry.npmjs.org/@types/copy-paste/-/copy-paste-1.1.30.tgz", + "integrity": "sha1-p9RUyeHkVCMo9/Huz1Mzvoz7UO0=", + "dev": true + }, + "@types/mkdirp": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@types/mkdirp/-/mkdirp-0.5.0.tgz", + "integrity": "sha512-9UvtpVx/f9ly3T0bTri3DNQYyRWoJ2CPwvBKCeD0BOG41XQBVCx4wr1aKcdOv3Uv+oeqJoFRrgAOxxO3hrFg5g==", + "dev": true + }, + "@types/mocha": { + "version": "2.2.41", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-2.2.41.tgz", + "integrity": "sha1-4nzwgXFT658nE7LT9saPHhw8pgg=", + "dev": true + }, + "@types/node": { + "version": "6.0.84", + "resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.84.tgz", + "integrity": "sha512-1SvEazClhUBRNroJM3oB3xf3u2r6xGmHDGbdigqNPHvNKLl8/BtATgO9eC04ZLuovpSh0B20BF1QJxdi+qmTlg==", + "dev": true + }, + "@types/rimraf": { + "version": "0.0.28", + "resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-0.0.28.tgz", + "integrity": "sha1-VWJRm8eWPKyoq/fxKMrjtZTUHQY=", + "dev": true + }, + "@types/sinon": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-2.3.3.tgz", + "integrity": "sha512-bnoHhhCsx0p0yhLOywFg6T7Le37JjtnzLcWal6cuSPvIZUBzKRIsqM6E5OsKUIRVErCaBCghHIZmqtyGk5uXyA==", + "dev": true + }, + "@types/sinon-chai": { + "version": "2.7.28", + "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-2.7.28.tgz", + "integrity": "sha512-qh9K/XtXzdHWiUqvFFjw3jQ5ZNrw0wzHaCWTcgBfSn7KwbjZHywinAdinSpUXeHBv+4cojk/9WSrPwVPYiITTA==", + "dev": true + }, + "@types/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-Vd+WmnrQKrrfVJ+9LWyOWqlBQJFsfi8rhKRm3ag3ZrOjY5SmzZkGmxbkgRIk9jpZt4dpvE21cmbBSp1dCV7/fw==", + "dev": true + }, + "ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "arr-diff": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-2.0.0.tgz", + "integrity": "sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8=", + "dev": true + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true + }, + "array-differ": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", + "integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=", + "dev": true + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "dev": true + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", + "dev": true + }, + "array-unique": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.2.1.tgz", + "integrity": "sha1-odl8yvy8JiXMcPrc6zalDFiwGlM=", + "dev": true + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", + "dev": true + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", + "dev": true + }, + "assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", + "dev": true + }, + "assertion-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.2.tgz", + "integrity": "sha1-E8pRXYYgbaC6xm6DTdOX2HWBCUw=", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", + "dev": true + }, + "aws4": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=", + "dev": true + }, + "babel-code-frame": { + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.22.0.tgz", + "integrity": "sha1-AnYgvuVnqIwyVhV05/0IAdMxGOQ=", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "dev": true, + "optional": true + }, + "beeper": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/beeper/-/beeper-1.1.1.tgz", + "integrity": "sha1-5tXqjF2tABMEpwsiY4RH9pyy+Ak=", + "dev": true + }, + "block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "dev": true + }, + "boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", + "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", + "dev": true + }, + "braces": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/braces/-/braces-1.8.5.tgz", + "integrity": "sha1-uneWLhLf+WnWt2cR6RS3N4V79qc=", + "dev": true + }, + "browser-stdout": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", + "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", + "dev": true + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "dev": true + }, + "caseless": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", + "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=", + "dev": true + }, + "chai": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.0.tgz", + "integrity": "sha1-MxoDkbVcOvh0CunDt0WLwcOAXm0=", + "dev": true + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "dependencies": { + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + } + } + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, + "clone": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.2.tgz", + "integrity": "sha1-Jgt6meux7f4kdTgXX3gyQ8sZ0Uk=", + "dev": true + }, + "clone-buffer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", + "integrity": "sha1-4+JbIHrE5wGvch4staFnksrD3Fg=", + "dev": true + }, + "clone-stats": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-0.0.1.tgz", + "integrity": "sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=", + "dev": true + }, + "cloneable-readable": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.0.0.tgz", + "integrity": "sha1-pikNQT8hemEjL5XkWP84QYz7ARc=", + "dev": true + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, + "colors": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", + "dev": true + }, + "combined-stream": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", + "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", + "dev": true + }, + "commander": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.3.0.tgz", + "integrity": "sha1-/UMOiJgy7DU7ms0d4hfBHLPu+HM=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "convert-source-map": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.0.tgz", + "integrity": "sha1-ms1whRxtXf3ZPZKC5e35SgP/RrU=", + "dev": true + }, + "copy-paste": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/copy-paste/-/copy-paste-1.3.0.tgz", + "integrity": "sha1-p+bEocKP3t8rCB5yuX3y75X0ce0=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "dev": true + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "dateformat": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.0.0.tgz", + "integrity": "sha1-J0Pjq7XD/CRi5SfcpEXgTp9N7hc=", + "dev": true + }, + "debug": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", + "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", + "dev": true + }, + "deep-assign": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/deep-assign/-/deep-assign-1.0.0.tgz", + "integrity": "sha1-sJJ0O+hCfcYh6gBnzex+cN0Z83s=", + "dev": true + }, + "deep-eql": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-2.0.2.tgz", + "integrity": "sha1-sbrAblbwp2d3aG1Qyf63XC7XZ5o=", + "dev": true, + "dependencies": { + "type-detect": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-3.0.0.tgz", + "integrity": "sha1-RtDMhVOrt7E6NSsNbeov1Y8tm1U=", + "dev": true + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "diff": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-1.4.0.tgz", + "integrity": "sha1-fyjS657nsVqX79ic5j3P2qPMur8=", + "dev": true + }, + "duplexer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", + "dev": true + }, + "duplexer2": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz", + "integrity": "sha1-xhTc9n4vsUmVqRcR5aYX6KYKMds=", + "dev": true, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + } + } + }, + "duplexify": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.5.0.tgz", + "integrity": "sha1-GqdzAC4VeEV+nZ1KULDMquvL1gQ=", + "dev": true + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "dev": true, + "optional": true + }, + "end-of-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.0.0.tgz", + "integrity": "sha1-1FlucCc0qT5A6a+GQxnqvZn/Lw4=", + "dev": true, + "dependencies": { + "once": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/once/-/once-1.3.3.tgz", + "integrity": "sha1-suJhVXzkwxTsgwTz+oJmPkKXyiA=", + "dev": true + } + } + }, + "escape-string-regexp": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.2.tgz", + "integrity": "sha1-Tbwv5nTnGUnK8/smlc5/LcHZqNE=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "event-stream": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", + "integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=", + "dev": true + }, + "expand-brackets": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-0.1.5.tgz", + "integrity": "sha1-3wcoTjQqgHzXM6xa9yQR5YHRF3s=", + "dev": true + }, + "expand-range": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", + "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", + "dev": true + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", + "dev": true + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "dev": true + }, + "extglob": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-0.3.2.tgz", + "integrity": "sha1-Lhj/PS9JqydlzskCPwEdqo2DSaE=", + "dev": true, + "dependencies": { + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + } + } + }, + "extsprintf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz", + "integrity": "sha1-4QgOBljjALBilJkMxw4VAiNf1VA=", + "dev": true + }, + "fancy-log": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.0.tgz", + "integrity": "sha1-Rb4X0Cu5kX1gzP/UmVyZnmyMmUg=", + "dev": true + }, + "fd-slicer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", + "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", + "dev": true + }, + "filename-regex": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", + "integrity": "sha1-wcS5vuPglyXdsQa3XB4wH+LxiyY=", + "dev": true + }, + "fill-range": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.3.tgz", + "integrity": "sha1-ULd9/X5Gm8dJJHCWNpn+eoSFpyM=", + "dev": true + }, + "first-chunk-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/first-chunk-stream/-/first-chunk-stream-1.0.0.tgz", + "integrity": "sha1-Wb+1DNkF9g18OUzT2ayqtOatk04=", + "dev": true + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "dev": true + }, + "for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "dev": true + }, + "formatio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.2.0.tgz", + "integrity": "sha1-87IWfZBoxGmKjVH092CjmlTYGOs=", + "dev": true + }, + "from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "fstream": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", + "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", + "dev": true + }, + "generate-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", + "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", + "dev": true + }, + "generate-object-property": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", + "dev": true + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", + "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", + "dev": true + }, + "glob-base": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", + "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", + "dev": true, + "dependencies": { + "glob-parent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", + "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", + "dev": true + }, + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true + } + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dev": true + }, + "glob-stream": { + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-5.3.5.tgz", + "integrity": "sha1-pVZlqajM3EGRWofHAeMtTgFvrSI=", + "dev": true, + "dependencies": { + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "through2": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", + "dev": true + } + } + }, + "glogg": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.0.tgz", + "integrity": "sha1-f+DxmfV6yQbPUS/urY+Q7kooT8U=", + "dev": true + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + }, + "graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", + "dev": true + }, + "growl": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", + "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", + "dev": true + }, + "gulp-chmod": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/gulp-chmod/-/gulp-chmod-2.0.0.tgz", + "integrity": "sha1-AMOQuSigeZslGsz2MaoJ4BzGKZw=", + "dev": true + }, + "gulp-filter": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/gulp-filter/-/gulp-filter-5.0.0.tgz", + "integrity": "sha1-z6gZZvtniE8rp1SwZxUpKUKNWbw=", + "dev": true + }, + "gulp-gunzip": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/gulp-gunzip/-/gulp-gunzip-0.0.3.tgz", + "integrity": "sha1-e24HsPWP09QlFcSOrVpj3wVy9i8=", + "dev": true, + "dependencies": { + "clone": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/clone/-/clone-0.2.0.tgz", + "integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "through2": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", + "dev": true + }, + "vinyl": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.4.6.tgz", + "integrity": "sha1-LzVsh6VQolVGHza76ypbqL94SEc=", + "dev": true + } + } + }, + "gulp-remote-src": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/gulp-remote-src/-/gulp-remote-src-0.4.2.tgz", + "integrity": "sha1-zrN3DjREMo1hOG+6qrIAvBHNmKg=", + "dev": true, + "dependencies": { + "clone-stats": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", + "integrity": "sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=", + "dev": true + }, + "replace-ext": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", + "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", + "dev": true + }, + "request": { + "version": "2.79.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.79.0.tgz", + "integrity": "sha1-Tf5b9r6LjNw3/Pk+BLZVd3InEN4=", + "dev": true + }, + "vinyl": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.0.2.tgz", + "integrity": "sha1-CjcT2NTpIhxY8QyhbAEWyeJe2nw=", + "dev": true + } + } + }, + "gulp-sourcemaps": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-1.6.0.tgz", + "integrity": "sha1-uG/zSdgBzrVuHZ59x7vLS33uYAw=", + "dev": true, + "dependencies": { + "vinyl": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", + "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", + "dev": true + } + } + }, + "gulp-symdest": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/gulp-symdest/-/gulp-symdest-1.1.0.tgz", + "integrity": "sha1-wWUyBzLRks5W/ZQnH/oSMjS/KuA=", + "dev": true + }, + "gulp-untar": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/gulp-untar/-/gulp-untar-0.0.6.tgz", + "integrity": "sha1-1r3v3n6ajgVMnxYjhaB4LEvnQAA=", + "dev": true + }, + "gulp-util": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/gulp-util/-/gulp-util-3.0.8.tgz", + "integrity": "sha1-AFTh50RQLifATBh8PsxQXdVLu08=", + "dev": true, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, + "gulp-vinyl-zip": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/gulp-vinyl-zip/-/gulp-vinyl-zip-1.4.0.tgz", + "integrity": "sha1-VjgvLMtXIxuwR4x4c3zNVylzvuE=", + "dev": true, + "dependencies": { + "clone": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/clone/-/clone-0.2.0.tgz", + "integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "through2": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", + "dev": true + }, + "vinyl": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.4.6.tgz", + "integrity": "sha1-LzVsh6VQolVGHza76ypbqL94SEc=", + "dev": true + } + } + }, + "gulplog": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz", + "integrity": "sha1-4oxNRdBey77YGDY86PnFkmIp/+U=", + "dev": true + }, + "har-schema": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", + "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=", + "dev": true + }, + "har-validator": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", + "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=", + "dev": true, + "dependencies": { + "commander": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", + "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", + "dev": true + } + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "has-gulplog": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/has-gulplog/-/has-gulplog-0.1.0.tgz", + "integrity": "sha1-ZBTIKRNpfaUVkDl9r7EvIpZ4Ec4=", + "dev": true + }, + "hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "dev": true + }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", + "dev": true + }, + "http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "dev": true + }, + "iconv-lite": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.18.tgz", + "integrity": "sha512-sr1ZQph3UwHTR0XftSbK85OvBbxe/abLGzEnPENCQwmHf7sck8Oyu4ob3LgBxWWxRoM+QszeUyl7jbqapu2TqA==" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "is": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is/-/is-3.2.1.tgz", + "integrity": "sha1-0Kwq1V63sL7JJqUmb2xmKqqD3KU=", + "dev": true + }, + "is-buffer": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.5.tgz", + "integrity": "sha1-Hzsm72E7IUuIy8ojzGwB2Hlh7sw=", + "dev": true + }, + "is-dotfile": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-dotfile/-/is-dotfile-1.0.3.tgz", + "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", + "dev": true + }, + "is-equal-shallow": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", + "integrity": "sha1-IjgJj8Ih3gvPpdnqxMRdY4qhxTQ=", + "dev": true + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", + "dev": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dev": true + }, + "is-my-json-valid": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.16.0.tgz", + "integrity": "sha1-8Hndm/2uZe4gOKrorLyGqxCeNpM=", + "dev": true + }, + "is-number": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-2.1.0.tgz", + "integrity": "sha1-Afy7s5NGOlSPL0ZszhbezknbkI8=", + "dev": true + }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", + "dev": true + }, + "is-posix-bracket": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz", + "integrity": "sha1-MzTceXdDaOkvAW5vvAqI9c1ua8Q=", + "dev": true + }, + "is-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", + "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", + "dev": true + }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", + "dev": true + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "is-valid-glob": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-0.3.0.tgz", + "integrity": "sha1-1LVcafUYhvm2XHDWwmItN+KfSP4=", + "dev": true + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "jade": { + "version": "0.26.3", + "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", + "integrity": "sha1-jxDXl32NefL2/4YqgbBRPMslaGw=", + "dev": true, + "dependencies": { + "commander": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", + "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=", + "dev": true + }, + "mkdirp": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", + "integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=", + "dev": true + } + } + }, + "js-tokens": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", + "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", + "dev": true + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true, + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "json3": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", + "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", + "dev": true + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true + }, + "jsonpointer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", + "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", + "dev": true + }, + "jsprim": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.0.tgz", + "integrity": "sha1-o7h+QCmNjDgFUtjMdiigu5WiKRg=", + "dev": true, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true + }, + "lazystream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", + "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", + "dev": true + }, + "lodash._baseassign": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", + "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", + "dev": true + }, + "lodash._basecopy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", + "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", + "dev": true + }, + "lodash._basecreate": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz", + "integrity": "sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE=", + "dev": true + }, + "lodash._basetostring": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basetostring/-/lodash._basetostring-3.0.1.tgz", + "integrity": "sha1-0YYdh3+CSlL2aYMtyvPuFVZqB9U=", + "dev": true + }, + "lodash._basevalues": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._basevalues/-/lodash._basevalues-3.0.0.tgz", + "integrity": "sha1-W3dXYoAr3j0yl1A+JjAIIP32Ybc=", + "dev": true + }, + "lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", + "dev": true + }, + "lodash._isiterateecall": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", + "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", + "dev": true + }, + "lodash._reescape": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reescape/-/lodash._reescape-3.0.0.tgz", + "integrity": "sha1-Kx1vXf4HyKNVdT5fJ/rH8c3hYWo=", + "dev": true + }, + "lodash._reevaluate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reevaluate/-/lodash._reevaluate-3.0.0.tgz", + "integrity": "sha1-WLx0xAZklTrgsSTYBpltrKQx4u0=", + "dev": true + }, + "lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", + "dev": true + }, + "lodash._root": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._root/-/lodash._root-3.0.1.tgz", + "integrity": "sha1-+6HEUkwZ7ppfgTa0YJ8BfPTe1pI=", + "dev": true + }, + "lodash.assign": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", + "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=" + }, + "lodash.create": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz", + "integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=", + "dev": true + }, + "lodash.escape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.2.0.tgz", + "integrity": "sha1-mV7g3BjBtIzJLv+ucaEKq1tIdpg=", + "dev": true + }, + "lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", + "dev": true + }, + "lodash.isarray": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", + "dev": true + }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", + "dev": true + }, + "lodash.keys": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", + "dev": true + }, + "lodash.restparam": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/lodash.restparam/-/lodash.restparam-3.6.1.tgz", + "integrity": "sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=", + "dev": true + }, + "lodash.template": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-3.6.2.tgz", + "integrity": "sha1-+M3sxhaaJVvpCYrosMU9N4kx0U8=", + "dev": true + }, + "lodash.templatesettings": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz", + "integrity": "sha1-+zB4RHU7Zrnxr6VOJix0UwfbqOU=", + "dev": true + }, + "lolex": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-1.6.0.tgz", + "integrity": "sha1-OpoCg0UqR9dDnnJzG54H1zhuSfY=", + "dev": true + }, + "lru-cache": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", + "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", + "dev": true + }, + "map-stream": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", + "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=", + "dev": true + }, + "merge-stream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz", + "integrity": "sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=", + "dev": true + }, + "micromatch": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-2.3.11.tgz", + "integrity": "sha1-hmd8l9FyCzY0MdBNDRUpO9OMFWU=", + "dev": true, + "dependencies": { + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true + } + } + }, + "mime-db": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.27.0.tgz", + "integrity": "sha1-gg9XIpa70g7CXtVeW13oaeVDbrE=", + "dev": true + }, + "mime-types": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.15.tgz", + "integrity": "sha1-pOv1BkCUVpI3uM9wBGd20J/JKu0=", + "dev": true + }, + "minimatch": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", + "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", + "dev": true + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true + }, + "mocha": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-2.5.3.tgz", + "integrity": "sha1-FhvlvetJZ3HrmzV0UFC2IrWu/Fg=", + "dev": true + }, + "ms": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", + "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", + "dev": true + }, + "multimatch": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-2.1.0.tgz", + "integrity": "sha1-nHkGoi+0wCkZ4vX3UWG0zb1LKis=", + "dev": true, + "dependencies": { + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true + } + } + }, + "multipipe": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/multipipe/-/multipipe-0.1.2.tgz", + "integrity": "sha1-Ko8t33Du1WTf8tV/HhoTfZ8FB4s=", + "dev": true + }, + "native-promise-only": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/native-promise-only/-/native-promise-only-0.8.1.tgz", + "integrity": "sha1-IKMYwwy0X3H+et+/eyHJnBRy7xE=", + "dev": true + }, + "node.extend": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/node.extend/-/node.extend-1.1.6.tgz", + "integrity": "sha1-p7iCyC1sk6SGOlUEvV3o7IYli5Y=", + "dev": true + }, + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", + "dev": true + }, + "object-assign": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-3.0.0.tgz", + "integrity": "sha1-m+3VygiXlJvKR+f/QIBi1Un1h/I=", + "dev": true + }, + "object.omit": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", + "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true + }, + "ordered-read-streams": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-0.3.0.tgz", + "integrity": "sha1-cTfmmzKYuzQiR6G77jiByA4v14s=", + "dev": true + }, + "parse-glob": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", + "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", + "dev": true, + "dependencies": { + "is-extglob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", + "dev": true + }, + "is-glob": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", + "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", + "dev": true + } + } + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-parse": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", + "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", + "dev": true + }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "dev": true, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + } + } + }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, + "pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=", + "dev": true + }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "dev": true + }, + "performance-now": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", + "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true + }, + "preserve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", + "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", + "dev": true + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", + "dev": true + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "qs": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.3.2.tgz", + "integrity": "sha1-51vV9uJoEioqDgvaYwslUMFmUCw=", + "dev": true + }, + "querystringify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-1.0.0.tgz", + "integrity": "sha1-YoYkIRLFtxL6ZU5SZlK/ahP/Bcs=", + "dev": true + }, + "queue": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/queue/-/queue-3.1.0.tgz", + "integrity": "sha1-bEnQHwCeIlZ4h4nyv/rGuLmZBYU=", + "dev": true + }, + "randomatic": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-1.1.7.tgz", + "integrity": "sha512-D5JUjPyJbaJDkuAazpVnSfVkLlpeO3wDlPROTMLGKG1zMFNFRgrciKo1ltz/AzNTkqE0HzDx655QOL51N06how==", + "dev": true, + "dependencies": { + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "dev": true, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true + } + } + }, + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "dev": true + } + } + }, + "readable-stream": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", + "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", + "dev": true + }, + "regex-cache": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.3.tgz", + "integrity": "sha1-mxpsNdTQ3871cRrmUejp09cRQUU=", + "dev": true + }, + "remove-trailing-separator": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.0.2.tgz", + "integrity": "sha1-abBi2XhyetFNxrVrpKt3L9jXBRE=", + "dev": true + }, + "repeat-element": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", + "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", + "dev": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "replace-ext": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz", + "integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=", + "dev": true + }, + "request": { + "version": "2.81.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", + "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", + "dev": true, + "dependencies": { + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true + }, + "har-validator": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", + "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", + "dev": true + }, + "qs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", + "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=", + "dev": true + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true + } + } + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", + "dev": true + }, + "resolve": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.3.3.tgz", + "integrity": "sha1-ZVkHw0aahoDcLeOidaj91paR8OU=", + "dev": true + }, + "rimraf": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz", + "integrity": "sha1-wjOOxkPfeht/5cVPqG9XQopV8z0=", + "dev": true, + "dependencies": { + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true + } + } + }, + "rxjs": { + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.4.2.tgz", + "integrity": "sha1-KjI2/L8D31e64G/Wly/ZnlwI/Pc=" + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", + "dev": true + }, + "samsam": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.2.1.tgz", + "integrity": "sha1-7dOQk6MYQ3DLhZJDsr3yVefY6mc=", + "dev": true + }, + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", + "dev": true + }, + "sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", + "dev": true + }, + "sinon": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-2.3.8.tgz", + "integrity": "sha1-Md4G/tj7o6Zx5XbdltClhjeW8lw=", + "dev": true, + "dependencies": { + "diff": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.0.tgz", + "integrity": "sha512-w0XZubFWn0Adlsapj9EAWX0FqWdO4tz8kc3RiYdWLh4k/V8PTb6i0SMgXt0vRM3zyKnT8tKO7mUlieRQHIjMNg==", + "dev": true + } + } + }, + "sinon-chai": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-2.12.0.tgz", + "integrity": "sha512-/J38xAWY5ppvRKuSrdnpVv7rWmxjfma9lL/iYaqn+ge/JynkhM9w8PaFAoGvGv+Tj2nEQWkkS8S4Syt4Lw1K6Q==", + "dev": true + }, + "sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "dev": true + }, + "source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha1-dc449SvwczxafwwRjYEzSiu19BI=", + "dev": true + }, + "source-map-support": { + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.15.tgz", + "integrity": "sha1-AyAt9lwG0r2MfsI2KhkwVv7407E=", + "dev": true + }, + "sparkles": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.0.tgz", + "integrity": "sha1-Gsu/tZJDbRC76PeFt8xvgoFQEsM=", + "dev": true + }, + "spawn-rx": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/spawn-rx/-/spawn-rx-2.0.11.tgz", + "integrity": "sha1-ZUUa1lZigB2up1VJgyp4LeAEjb8=", + "dependencies": { + "debug": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", + "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "split": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", + "integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=", + "dev": true + }, + "sshpk": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", + "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", + "dev": true, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "stat-mode": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-0.2.2.tgz", + "integrity": "sha1-5sgLYjEj19gM8TLOU480YokHJQI=", + "dev": true + }, + "stream-combiner": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=", + "dev": true + }, + "stream-shift": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", + "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", + "dev": true + }, + "streamfilter": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/streamfilter/-/streamfilter-1.0.5.tgz", + "integrity": "sha1-h1BxEb644phFFxe1Ec/tjwAqv1M=", + "dev": true + }, + "streamifier": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/streamifier/-/streamifier-0.1.1.tgz", + "integrity": "sha1-l+mNj6TRBdYqJpHR3AfoINuN/E8=", + "dev": true + }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "dev": true + }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "dev": true + }, + "strip-bom-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-stream/-/strip-bom-stream-1.0.0.tgz", + "integrity": "sha1-5xRDmFd9Uaa+0PoZlPoF9D/ZiO4=", + "dev": true + }, + "supports-color": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-1.2.0.tgz", + "integrity": "sha1-/x7R5hFp0Gs88tWI4YixjYhH4X4=", + "dev": true + }, + "symbol-observable": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.4.tgz", + "integrity": "sha1-Kb9hXUqnEhvdiYsi1LP5vE4qoD0=" + }, + "sync-exec": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/sync-exec/-/sync-exec-0.6.2.tgz", + "integrity": "sha1-cX0izFPwzh3vVZQ2LzqJouu5EQU=", + "optional": true + }, + "tar": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", + "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", + "dev": true + }, + "text-encoding": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", + "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "through2": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", + "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", + "dev": true + }, + "through2-filter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-2.0.0.tgz", + "integrity": "sha1-YLxVoNrLdghdsfna6Zq0P4PWIuw=", + "dev": true + }, + "time-stamp": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", + "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=", + "dev": true + }, + "to-absolute-glob": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-0.1.1.tgz", + "integrity": "sha1-HN+kcqnvUMI57maZm2YsoOs5k38=", + "dev": true + }, + "to-iso-string": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/to-iso-string/-/to-iso-string-0.0.2.tgz", + "integrity": "sha1-TcGeZk38y+Jb2NtQiwDG2hWCVdE=", + "dev": true + }, + "tough-cookie": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz", + "integrity": "sha1-8IH3bkyFcg5sN6X6ztc3FQ2EByo=", + "dev": true + }, + "tslib": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.7.1.tgz", + "integrity": "sha1-vIAEFkaRkjp5/oN4u+s9ogF1OOw=", + "dev": true + }, + "tslint": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.5.0.tgz", + "integrity": "sha1-EOjas+MGH6YelELozuOYKs8gpqo=", + "dev": true, + "dependencies": { + "commander": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", + "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", + "dev": true + }, + "diff": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.0.tgz", + "integrity": "sha512-w0XZubFWn0Adlsapj9EAWX0FqWdO4tz8kc3RiYdWLh4k/V8PTb6i0SMgXt0vRM3zyKnT8tKO7mUlieRQHIjMNg==", + "dev": true + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true + } + } + }, + "tsutils": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.7.1.tgz", + "integrity": "sha1-QRoOlGZSWisoaSYKVWINcpIVXiQ=", + "dev": true + }, + "tunnel-agent": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", + "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=", + "dev": true + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true, + "optional": true + }, + "type-detect": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.3.tgz", + "integrity": "sha1-Dj8mcLRAmbC0bChNE2p+9Jx0wuo=", + "dev": true + }, + "typescript": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.4.2.tgz", + "integrity": "sha1-+DlfhdRZJ2BnyYiqQYN6j4KHCEQ=", + "dev": true + }, + "unique-stream": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.2.1.tgz", + "integrity": "sha1-WqADz76Uxf+GbE59ZouxxNuts2k=", + "dev": true + }, + "url-parse": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.1.9.tgz", + "integrity": "sha1-xn8dd11R8KGJEd17P/rSe7nlvRk=", + "dev": true + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "uuid": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", + "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==", + "dev": true + }, + "vali-date": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/vali-date/-/vali-date-1.0.0.tgz", + "integrity": "sha1-G5BKWWCfsyjvB4E4Qgk09rhnCaY=", + "dev": true + }, + "verror": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", + "integrity": "sha1-z/XfEpRtKX0rqu+qJoniW+AcAFw=", + "dev": true + }, + "vinyl": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz", + "integrity": "sha1-sEVbOPxeDPMNQyUTLkYZcMIJHN4=", + "dev": true + }, + "vinyl-fs": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-2.4.4.tgz", + "integrity": "sha1-vm/zJwy1Xf19MGNkDegfJddTIjk=", + "dev": true, + "dependencies": { + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true + }, + "vinyl": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", + "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", + "dev": true + } + } + }, + "vinyl-source-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vinyl-source-stream/-/vinyl-source-stream-1.1.0.tgz", + "integrity": "sha1-RMvlEIIFJ53rDFZTwJSiiHk4sas=", + "dev": true, + "dependencies": { + "clone": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/clone/-/clone-0.2.0.tgz", + "integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "readable-stream": { + "version": "1.0.34", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", + "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", + "dev": true + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "through2": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-0.6.5.tgz", + "integrity": "sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=", + "dev": true + }, + "vinyl": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.4.6.tgz", + "integrity": "sha1-LzVsh6VQolVGHza76ypbqL94SEc=", + "dev": true + } + } + }, + "vscode": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/vscode/-/vscode-1.1.4.tgz", + "integrity": "sha1-Hx1NZi1VyaKLxGeqy2MikfcKaG0=", + "dev": true, + "dependencies": { + "commander": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", + "dev": true + }, + "debug": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.0.tgz", + "integrity": "sha1-vFlryr52F/Edn6FTYe3tVgi4SZs=", + "dev": true + }, + "diff": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz", + "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true + }, + "mocha": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-3.4.2.tgz", + "integrity": "sha1-0O9NMyEm2/GNDWQMmzgt1IvpdZQ=", + "dev": true, + "dependencies": { + "glob": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", + "integrity": "sha1-gFIR3wT6rxxjo2ADBs31reULLsg=", + "dev": true + } + } + }, + "ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha1-riXPJRKziFodldfwN4aNhDESR2U=", + "dev": true + }, + "supports-color": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz", + "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=", + "dev": true + } + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "dev": true + }, + "yauzl": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.8.0.tgz", + "integrity": "sha1-eUUK/yKyqcWkHvVOAtuQfM+/nuI=", + "dev": true + }, + "yazl": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.4.2.tgz", + "integrity": "sha1-FMsZCD4eJacAksFYiqvg9OTdTYg=", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..7ed024c --- /dev/null +++ b/package.json @@ -0,0 +1,133 @@ +{ + "name": "gitweblinks", + "displayName": "Git Web Links", + "description": "Copy links to files in their online Git repositories", + "version": "1.0.0", + "publisher": "reduckted", + "repository": { + "type": "git", + "url": "https://github.com/reduckted/vscode-gitweblinks" + }, + "private": true, + "main": "./out/src/extension", + "license": "MIT", + "scripts": { + "vscode:prepublish": "tsc -p ./", + "compile": "tsc -watch -p ./", + "postinstall": "node ./node_modules/vscode/bin/install", + "test": "node ./node_modules/vscode/bin/test", + "lint": "tslint -p tsconfig.json" + }, + "dependencies": { + "copy-paste": "^1.3.0", + "spawn-rx": "^2.0.11" + }, + "devDependencies": { + "@types/chai": "^4.0.1", + "@types/copy-paste": "^1.1.30", + "@types/mkdirp": "^0.5.0", + "@types/mocha": "^2.2.32", + "@types/node": "^6.0.40", + "@types/rimraf": "0.0.28", + "@types/sinon": "^2.3.3", + "@types/sinon-chai": "^2.7.28", + "@types/uuid": "^3.4.0", + "chai": "^4.1.0", + "mkdirp": "^0.5.1", + "mocha": "^2.3.3", + "rimraf": "^2.6.1", + "sinon": "^2.3.8", + "sinon-chai": "^2.12.0", + "tslint": "^5.5.0", + "typescript": "^2.0.3", + "uuid": "^3.1.0", + "vscode": "^1.0.0" + }, + "engines": { + "vscode": "^1.14.0" + }, + "categories": [ + "Other" + ], + "activationEvents": [ + "*" + ], + "contributes": { + "commands": [ + { + "command": "gitweblinks.copyFile", + "title": "Copy Web Link to File" + }, + { + "command": "gitweblinks.copySelection", + "title": "Copy Web Link to Selection" + } + ], + "menus": { + "editor/context": [ + { + "command": "gitweblinks.copySelection", + "group": "gitweblinks@1", + "when": "gitweblinks:canCopy" + } + ], + "editor/title/context": [ + { + "command": "gitweblinks.copyFile", + "group": "gitweblinks@1", + "when": "gitweblinks:canCopy" + } + ], + "explorer/context": [ + { + "command": "gitweblinks.copyFile", + "group": "gitweblinks@1", + "when": "gitweblinks:canCopy" + } + ] + }, + "configuration": { + "title": "Git Web Links configuration", + "properties": { + "gitweblinks.bitbucketServer": { + "type": "array", + "items": { + "type": "object", + "properties": { + "baseUrl": { + "type": "string", + "description": "The base URL for the website." + }, + "sshUrl": { + "type": "string", + "description": "The SSH URL for remotes." + } + }, + "required": [ + "baseUrl" + ] + } + }, + "gitweblinks.gitHubEnterprise": { + "type": "array", + "items": { + "type": "object", + "properties": { + "baseUrl": { + "type": "string", + "description": "The base URL for the website." + }, + "sshUrl": { + "type": "string", + "description": "The SSH URL for remotes." + } + }, + "required": [ + "baseUrl" + ] + } + } + } + } + } +} diff --git a/src/commands/CopyLinkCommand.ts b/src/commands/CopyLinkCommand.ts new file mode 100644 index 0000000..6b6baff --- /dev/null +++ b/src/commands/CopyLinkCommand.ts @@ -0,0 +1,37 @@ +import { commands, Disposable, Uri, window } from 'vscode'; + +import { GitInfo } from '../git/GitInfo'; +import { LinkHandler } from '../links/LinkHandler'; +import { Clipboard } from '../utilities/Clipboard'; +import { Selection } from '../utilities/Selection'; + + +export abstract class CopyLinkCommand extends Disposable { + + private disposable: Disposable; + + + constructor(identifier: string, private gitInfo: GitInfo, private linkHandler: LinkHandler) { + super(() => this.disposable && this.disposable.dispose()); + this.disposable = commands.registerCommand(identifier, this.execute, this); + } + + + protected async execute(resource: Uri | undefined): Promise { + if (resource && (resource.scheme === 'file')) { + let selection: Selection | undefined; + let url: string; + + + selection = this.getLineSelection(); + + url = await this.linkHandler.makeUrl(this.gitInfo, resource.fsPath, selection); + + Clipboard.setText(url); + } + } + + + protected abstract getLineSelection(): Selection | undefined; + +} diff --git a/src/commands/CopyLinkToFileCommand.ts b/src/commands/CopyLinkToFileCommand.ts new file mode 100644 index 0000000..1f51bd2 --- /dev/null +++ b/src/commands/CopyLinkToFileCommand.ts @@ -0,0 +1,19 @@ +import { GitInfo } from '../git/GitInfo'; +import { LinkHandler } from '../links/LinkHandler'; +import { Selection } from '../utilities/Selection'; +import { CopyLinkCommand } from './CopyLinkCommand'; + + +export class CopyLinkToFileCommand extends CopyLinkCommand { + + constructor(gitInfo: GitInfo, linkHandler: LinkHandler) { + super('gitweblinks.copyFile', gitInfo, linkHandler); + } + + + protected getLineSelection(): Selection | undefined { + return undefined; + } + +} + diff --git a/src/commands/CopyLinkToSelectionCommand.ts b/src/commands/CopyLinkToSelectionCommand.ts new file mode 100644 index 0000000..bfa3547 --- /dev/null +++ b/src/commands/CopyLinkToSelectionCommand.ts @@ -0,0 +1,34 @@ +import { TextEditor, window } from 'vscode'; + +import { GitInfo } from '../git/GitInfo'; +import { LinkHandler } from '../links/LinkHandler'; +import { Selection } from '../utilities/Selection'; +import { CopyLinkCommand } from './CopyLinkCommand'; + + +export class CopyLinkToSelectionCommand extends CopyLinkCommand { + + constructor(gitInfo: GitInfo, linkHandler: LinkHandler) { + super('gitweblinks.copySelection', gitInfo, linkHandler); + } + + + protected getLineSelection(): Selection | undefined { + let editor: TextEditor | undefined; + + + editor = window.activeTextEditor; + + if (editor) { + // The line numbers are zero-based in the + // editor, but we need them to be one-based. + return { + startLine: editor.selection.start.line + 1, + endLine: editor.selection.end.line + 1 + }; + } + + return undefined; + } + +} diff --git a/src/configuration/CustomServerProvider.ts b/src/configuration/CustomServerProvider.ts new file mode 100644 index 0000000..8c59041 --- /dev/null +++ b/src/configuration/CustomServerProvider.ts @@ -0,0 +1,22 @@ +import { workspace } from 'vscode'; + +import { CONFIGURATION_KEY } from '../constants'; +import { ServerUrl } from '../utilities/ServerUrl'; + + +export class CustomServerProvider { + + public getServers(type: string): ServerUrl[] { + let servers: ServerUrl[] | undefined; + + + servers = workspace.getConfiguration().get(`${CONFIGURATION_KEY}.${type}`); + + if (servers && Array.isArray(servers)) { + return servers.filter((x) => !!x.baseUrl); + } + + return []; + } + +} diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..86f06de --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,3 @@ +export const EXTENSION_ID: string = 'gitweblinks'; +export const EXTENSION_NAME: string = 'Git Web Links'; +export const CONFIGURATION_KEY: string = EXTENSION_ID; diff --git a/src/extension.ts b/src/extension.ts new file mode 100644 index 0000000..2f1f190 --- /dev/null +++ b/src/extension.ts @@ -0,0 +1,76 @@ +import { commands, ExtensionContext, window, workspace } from 'vscode'; + +import { CopyLinkToFileCommand } from './commands/CopyLinkToFileCommand'; +import { CopyLinkToSelectionCommand } from './commands/CopyLinkToSelectionCommand'; +import { EXTENSION_NAME } from './constants'; +import { Git } from './git/Git'; +import { GitInfo } from './git/GitInfo'; +import { GitInfoFinder } from './git/GitInfoFinder'; +import { LinkHandler } from './links/LinkHandler'; +import { LinkHandlerFinder } from './links/LinkHandlerFinder'; + + +export async function activate(context: ExtensionContext): Promise { + let enabled: boolean; + + + enabled = false; + + if (await initializeGit()) { + if (workspace.rootPath) { + let gitInfo: GitInfo | undefined; + + + gitInfo = await findGitInfo(workspace.rootPath); + + if (gitInfo) { + let handler: LinkHandler | undefined; + + + handler = (new LinkHandlerFinder()).find(gitInfo); + + if (handler) { + context.subscriptions.push(new CopyLinkToFileCommand(gitInfo, handler)); + context.subscriptions.push(new CopyLinkToSelectionCommand(gitInfo, handler)); + enabled = true; + } + } + } + } + + // Set the context for our commands. If we found the + // Git info and a handler, then those commands can run. + await commands.executeCommand('setContext', 'gitweblinks:canCopy', enabled); +} + + +export function deactivate(): void { + // Nothing to do here. +} + + +async function initializeGit(): Promise { + try { + await Git.test(); + return true; + + } catch (ex) { + window.showErrorMessage( + `${EXTENSION_NAME} could not find Git. Make sure Git is installed and in the PATH.` + ); + + return false; + } +} + + +async function findGitInfo(workspaceRoot: string): Promise { + try { + return await (new GitInfoFinder()).find(workspaceRoot); + + } catch (ex) { + // tslint:disable-next-line:no-console + console.error('Failed to initialize: ', ex); + window.showErrorMessage('Git Web Links failed to initialize.'); + } +} diff --git a/src/git/Git.ts b/src/git/Git.ts new file mode 100644 index 0000000..ed06fb1 --- /dev/null +++ b/src/git/Git.ts @@ -0,0 +1,22 @@ +import { spawnPromise } from 'spawn-rx'; + + +let gitPath: string = 'git'; + + +export class Git { + + public static async test(): Promise { + await spawnPromise(gitPath, ['--version'], process.cwd()); + } + + + public static async execute(root: string, ...args: string[]): Promise { + // Handle non-ASCII characters in filenames. + // See https://stackoverflow.com/questions/4144417/ + args.splice(0, 0, '-c', 'core.quotepath=false'); + + return spawnPromise(gitPath, args, { cwd: root }); + } + +} diff --git a/src/git/GitInfo.ts b/src/git/GitInfo.ts new file mode 100644 index 0000000..235b70a --- /dev/null +++ b/src/git/GitInfo.ts @@ -0,0 +1,8 @@ +export interface GitInfo { + + rootDirectory: string; + + + remoteUrl: string; + +} diff --git a/src/git/GitInfoFinder.ts b/src/git/GitInfoFinder.ts new file mode 100644 index 0000000..364ddf6 --- /dev/null +++ b/src/git/GitInfoFinder.ts @@ -0,0 +1,112 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +import { Git } from './Git'; +import { GitInfo } from './GitInfo'; + + +interface Remote { + name: string; + url: string; +} + + +export class GitInfoFinder { + + public async find(workspaceRoot: string): Promise { + let root: string | undefined; + + + root = await this.findGitRoot(workspaceRoot); + + if (root) { + let remote: string | undefined; + + + remote = await this.findRemote(root); + + if (remote) { + return { rootDirectory: root, remoteUrl: remote }; + } + } + + return undefined; + } + + + private async findGitRoot(startingDirectory: string): Promise { + let dir: string; + + + dir = startingDirectory; + + while (dir) { + let parent: string; + + + if (await this.directoryExists(path.join(dir, '.git'))) { + return dir; + } + + parent = path.dirname(dir); + + if (parent === dir) { + break; + } + + dir = parent; + } + + return undefined; + } + + + private async directoryExists(dir: string): Promise { + return new Promise((resolve) => { + fs.stat(dir, (err, stats) => { + resolve(!err && stats.isDirectory()); + }); + }); + } + + + private async findRemote(root: string): Promise { + let data: string; + let remotes: Remote[]; + let remote: Remote; + + + data = await Git.execute(root, 'remote', '-v'); + + remotes = data.split('\n').filter((x) => !!x).map((x) => this.parseRemote(x)); + + // Use the "origin" remote if it exists; + // otherwise, just use the first remote. + remote = remotes.filter((x) => x.name === 'origin')[0]; + + if (!remote) { + remotes.sort((x, y) => x.name.localeCompare(y.name)); + remote = remotes[0]; + } + + if (remote) { + return remote.url; + } + + return undefined; + } + + + private parseRemote(line: string): Remote { + let name: string; + let urlAndType: string; + let url: string; + + + [name, urlAndType] = line.split('\t'); + [url] = urlAndType.split(' '); + + return { name, url }; + } + +} diff --git a/src/links/BitbucketCloudHandler.ts b/src/links/BitbucketCloudHandler.ts new file mode 100644 index 0000000..348d92b --- /dev/null +++ b/src/links/BitbucketCloudHandler.ts @@ -0,0 +1,50 @@ +import * as path from 'path'; + +import { Git } from '../git/Git'; +import { Selection } from '../utilities/Selection'; +import { ServerUrl } from '../utilities/ServerUrl'; +import { LinkHandler } from './LinkHandler'; + + +export class BitbucketCloudHandler extends LinkHandler { + + private static BITBUCKET_SERVER: ServerUrl = { + baseUrl: 'https://bitbucket.org', + sshUrl: 'git@bitbucket.org' + }; + + + protected getServerUrls(): ServerUrl[] { + return [BitbucketCloudHandler.BITBUCKET_SERVER]; + } + + + protected async getCurrentBranch(rootDirectory: string): Promise { + return (await Git.execute(rootDirectory, 'rev-parse', '--abbrev-ref', 'HEAD')).trim(); + } + + + protected createUrl( + baseUrl: string, + repositoryPath: string, + branch: string, + relativePathToFile: string + ): string { + return [baseUrl, repositoryPath, 'src', branch, relativePathToFile].join('/'); + } + + + protected getSelectionHash(filePath: string, selection: Selection): string { + let hash: string; + + + hash = `#${encodeURIComponent(path.basename(filePath))}-${selection.startLine}`; + + if (selection.startLine !== selection.endLine) { + hash += `:${selection.endLine}`; + } + + return hash; + } + +} diff --git a/src/links/BitbucketServerHandler.ts b/src/links/BitbucketServerHandler.ts new file mode 100644 index 0000000..fda5729 --- /dev/null +++ b/src/links/BitbucketServerHandler.ts @@ -0,0 +1,72 @@ +import * as path from 'path'; + +import { CustomServerProvider } from '../configuration/CustomServerProvider'; +import { Git } from '../git/Git'; +import { Selection } from '../utilities/Selection'; +import { ServerUrl } from '../utilities/ServerUrl'; +import { LinkHandler } from './LinkHandler'; + + +export class BitbucketServerHandler extends LinkHandler { + + private customServerProvider: CustomServerProvider; + + + constructor() { + super(); + this.customServerProvider = new CustomServerProvider(); + } + + protected getServerUrls(): ServerUrl[] { + return this.customServerProvider.getServers('bitbucketServer'); + } + + + protected async getCurrentBranch(rootDirectory: string): Promise { + return (await Git.execute(rootDirectory, 'symbolic-ref', 'HEAD')).trim(); + } + + + protected createUrl( + baseUrl: string, + repositoryPath: string, + branch: string, + relativePathToFile: string + ): string { + + let match: RegExpExecArray | null; + let project: string; + let repo: string; + let url: string; + + + match = /([^\/]+)\/([^\/]+)$/.exec(repositoryPath); + + if (!match) { + throw new Error('Could not find the project and repository names in the remote URL.'); + } + + project = match[1]; + repo = match[2]; + + url = [baseUrl, 'projects', project, 'repos', repo, 'browse', relativePathToFile].join('/'); + + // The branch name is specified via a query parameter. + return url + `?at=${encodeURIComponent(branch)}`; + } + + + protected getSelectionHash(filePath: string, selection: Selection): string { + let hash: string; + + + hash = `#${selection.startLine}`; + + if (selection.startLine !== selection.endLine) { + hash += `-${selection.endLine}`; + } + + return hash; + } + +} diff --git a/src/links/GitHubHandler.ts b/src/links/GitHubHandler.ts new file mode 100644 index 0000000..8137213 --- /dev/null +++ b/src/links/GitHubHandler.ts @@ -0,0 +1,65 @@ +import { CustomServerProvider } from '../configuration/CustomServerProvider'; +import { Git } from '../git/Git'; +import { Selection } from '../utilities/Selection'; +import { ServerUrl } from '../utilities/ServerUrl'; +import { LinkHandler } from './LinkHandler'; + + +export class GitHubHandler extends LinkHandler { + + private static GITHUB_SERVER: ServerUrl = { + baseUrl: 'https://github.com', + sshUrl: 'git@github.com' + }; + + + private customServerProvider: CustomServerProvider; + + + constructor() { + super(); + this.customServerProvider = new CustomServerProvider(); + } + + + protected getServerUrls(): ServerUrl[] { + let urls: ServerUrl[]; + + + urls = [GitHubHandler.GITHUB_SERVER]; + + Array.prototype.push.apply(urls, this.customServerProvider.getServers('gitHubEnterprise')); + + return urls; + } + + + protected async getCurrentBranch(rootDirectory: string): Promise { + return (await Git.execute(rootDirectory, 'rev-parse', '--abbrev-ref', 'HEAD')).trim(); + } + + + protected createUrl( + baseUrl: string, + repositoryPath: string, + branch: string, + relativePathToFile: string + ): string { + return [baseUrl, repositoryPath, 'blob', branch, relativePathToFile].join('/'); + } + + + protected getSelectionHash(filePath: string, selection: Selection): string { + let hash: string; + + + hash = `#L${selection.startLine}`; + + if (selection.startLine !== selection.endLine) { + hash += `-L${selection.endLine}`; + } + + return hash; + } + +} diff --git a/src/links/LinkHandler.ts b/src/links/LinkHandler.ts new file mode 100644 index 0000000..449b9da --- /dev/null +++ b/src/links/LinkHandler.ts @@ -0,0 +1,133 @@ +import { GitInfo } from '../git/GitInfo'; +import { Selection } from '../utilities/Selection'; +import { ServerUrl } from '../utilities/ServerUrl'; + + +export abstract class LinkHandler { + + private static SSH_PREFIX: string = 'ssh://'; + + + public isMatch(remoteUrl: string): boolean { + return this.getMatchingServerUrl(this.fixRemoteUrl(remoteUrl)) !== undefined; + } + + + public async makeUrl(gitInfo: GitInfo, filePath: string, selection: Selection | undefined): Promise { + let url: string; + let fixedRemoteUrl: string; + let server: ServerUrl; + let repositoryPath: string; + let relativePathToFile: string; + let branch: string; + let baseUrl: string; + + + fixedRemoteUrl = this.fixRemoteUrl(gitInfo.remoteUrl); + server = this.getMatchingServerUrl(fixedRemoteUrl); + + // Get the repository's path out of the remote URL. + repositoryPath = this.getRepositoryPath(fixedRemoteUrl, server); + + relativePathToFile = filePath.substring(gitInfo.rootDirectory.length).split('\\').join('/'); + + // Trim slashes from the start of the string. + relativePathToFile = relativePathToFile.replace(/^\/+/, ''); + + // Get the current branch name. The remote branch might not be the same, + // but it's better than using a commit hash which won't match anything on + // the remote if there are commits to this branch on the local repository. + branch = await this.getCurrentBranch(gitInfo.rootDirectory); + + baseUrl = server.baseUrl; + + if (baseUrl.endsWith('/')) { + baseUrl = baseUrl.substring(0, baseUrl.length - 1); + } + + url = this.createUrl( + baseUrl, + repositoryPath, + encodeURI(branch), + relativePathToFile.split('/').map((x) => encodeURIComponent(x)).join('/') + ); + + if (selection) { + url += this.getSelectionHash(filePath, selection); + } + + return url; + } + + + private getMatchingServerUrl(remoteUrl: string): ServerUrl { + return this.getServerUrls().filter((x) => remoteUrl.startsWith(x.baseUrl) || remoteUrl.startsWith(x.sshUrl))[0]; + } + + + private fixRemoteUrl(remoteUrl: string): string { + if (remoteUrl.startsWith(LinkHandler.SSH_PREFIX)) { + // Remove the SSH prefix. + remoteUrl = remoteUrl.substring(LinkHandler.SSH_PREFIX.length); + + } else { + let match: RegExpExecArray | null; + + + // This will be an HTTP address. Check if there's + // a username in the URL And if there Is, remove it. + match = /(https?:\/\/)[^@]+@(.+)/.exec(remoteUrl); + + if (match) { + remoteUrl = match[1] + match[2]; + } + } + + return remoteUrl; + } + + + protected abstract getServerUrls(): ServerUrl[]; + + + private getRepositoryPath(remoteUrl: string, matchingServer: ServerUrl): string { + let path: string; + + + if (remoteUrl.startsWith(matchingServer.baseUrl)) { + path = remoteUrl.substring(matchingServer.baseUrl.length); + } else { + path = remoteUrl.substring(matchingServer.sshUrl.length); + } + + // The server URL we matched against may not have ended + // with a slash (for HTTPS paths) or a colon (for Git paths), + // which means the path might start with that. Trim that off now. + if (path.length > 0) { + if ((path[0] === '/') || (path[0] === ':')) { + path = path.substring(1); + } + } + + if (path.endsWith('.git')) { + path = path.substring(0, path.length - 4); + } + + return path; + } + + + protected abstract getCurrentBranch(rootDirectory: string): Promise; + + + protected abstract createUrl( + baseUrl: string, + repositoryPath: string, + branch: string, + relativePathToFile: string + ): string; + + + protected abstract getSelectionHash(filePath: string, selection: Selection): string; + +} diff --git a/src/links/LinkHandlerFinder.ts b/src/links/LinkHandlerFinder.ts new file mode 100644 index 0000000..0792780 --- /dev/null +++ b/src/links/LinkHandlerFinder.ts @@ -0,0 +1,32 @@ +import { GitInfo } from '../git/GitInfo'; +import { BitbucketCloudHandler } from './BitbucketCloudHandler'; +import { BitbucketServerHandler } from './BitbucketServerHandler'; +import { GitHubHandler } from './GitHubHandler'; +import { LinkHandler } from './LinkHandler'; + + +export class LinkHandlerFinder { + + private handlers: LinkHandler[]; + + + constructor() { + this.handlers = [ + new BitbucketCloudHandler(), + new BitbucketServerHandler(), + new GitHubHandler() + ]; + } + + + public find(gitInfo: GitInfo): LinkHandler | undefined { + for (let handler of this.handlers) { + if (handler.isMatch(gitInfo.remoteUrl)) { + return handler; + } + } + + return undefined; + } + +} diff --git a/src/utilities/Clipboard.ts b/src/utilities/Clipboard.ts new file mode 100644 index 0000000..01d2be8 --- /dev/null +++ b/src/utilities/Clipboard.ts @@ -0,0 +1,10 @@ +import * as clipboard from 'copy-paste'; + + +export class Clipboard { + + public static setText(text: string) { + clipboard.copy(text); + } + +} diff --git a/src/utilities/Selection.ts b/src/utilities/Selection.ts new file mode 100644 index 0000000..c730073 --- /dev/null +++ b/src/utilities/Selection.ts @@ -0,0 +1,8 @@ +export interface Selection { + + startLine: number; + + + endLine: number; + +} diff --git a/src/utilities/ServerUrl.ts b/src/utilities/ServerUrl.ts new file mode 100644 index 0000000..7643f67 --- /dev/null +++ b/src/utilities/ServerUrl.ts @@ -0,0 +1,8 @@ +export interface ServerUrl { + + baseUrl: string; + + + sshUrl: string; + +} diff --git a/test/commands/CopyLinkToFileCommand.test.ts b/test/commands/CopyLinkToFileCommand.test.ts new file mode 100644 index 0000000..3eb7d9b --- /dev/null +++ b/test/commands/CopyLinkToFileCommand.test.ts @@ -0,0 +1,72 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { commands, Uri } from 'vscode'; + +import { CopyLinkToFileCommand } from '../../src/commands/CopyLinkToFileCommand'; +import { Clipboard } from '../../src/utilities/Clipboard'; + +import { FINAL_URL, GIT_INFO, MockLinkHandler } from '../test-helpers/MockLinkHandler'; + + +describe('CopyLinkToFileCommand', () => { + + let sandbox: sinon.SinonSandbox; + let clipboardStub: sinon.SinonStub; + let command: CopyLinkToFileCommand | undefined; + + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + clipboardStub = sandbox.stub(Clipboard, 'setText'); + }); + + + afterEach(() => { + if (command) { + command.dispose(); + command = undefined; + } + + sandbox.restore(); + }); + + + it('should unregister the command when disposed.', async () => { + let all: string[]; + + + command = new CopyLinkToFileCommand(GIT_INFO, new MockLinkHandler()); + + all = await commands.getCommands(); + expect(all).to.contain('gitweblinks.copyFile'); + + command.dispose(); + command = undefined; + + all = await commands.getCommands(); + expect(all).to.not.contain('gitweblinks.copyFile'); + }); + + + it('should not use a line selection.', async () => { + let handler: MockLinkHandler; + + + handler = new MockLinkHandler(); + command = new CopyLinkToFileCommand(GIT_INFO, handler); + + await commands.executeCommand('gitweblinks.copyFile', Uri.file(`${GIT_INFO.rootDirectory}foo.txt`)); + + expect(handler.selection).to.be.undefined; + }); + + + it('should copy the URL to the clipboard.', async () => { + command = new CopyLinkToFileCommand(GIT_INFO, new MockLinkHandler()); + + await commands.executeCommand('gitweblinks.copyFile', Uri.file(`${GIT_INFO.rootDirectory}foo.txt`)); + + expect(clipboardStub.calledWith(FINAL_URL)).to.be.true; + }); + +}); diff --git a/test/commands/CopyLinkToSelectionCommand.test.ts b/test/commands/CopyLinkToSelectionCommand.test.ts new file mode 100644 index 0000000..3e0ff78 --- /dev/null +++ b/test/commands/CopyLinkToSelectionCommand.test.ts @@ -0,0 +1,80 @@ +import { expect } from 'chai'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { commands, Position, Selection, TextDocument, TextEditor, Uri, window, workspace } from 'vscode'; + +import { CopyLinkToSelectionCommand } from '../../src/commands/CopyLinkToSelectionCommand'; +import { Clipboard } from '../../src/utilities/Clipboard'; + +import { FINAL_URL, GIT_INFO, MockLinkHandler } from '../test-helpers/MockLinkHandler'; + + +describe('CopyLinkToSelectionCommand', () => { + + let sandbox: sinon.SinonSandbox; + let clipboardStub: sinon.SinonStub; + let command: CopyLinkToSelectionCommand | undefined; + + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + clipboardStub = sandbox.stub(Clipboard, 'setText'); + }); + + + afterEach(() => { + if (command) { + command.dispose(); + command = undefined; + } + + sandbox.restore(); + }); + + + it('should unregister the command when disposed.', async () => { + let all: string[]; + + + command = new CopyLinkToSelectionCommand(GIT_INFO, new MockLinkHandler()); + + all = await commands.getCommands(); + expect(all).to.contain('gitweblinks.copySelection'); + + command.dispose(); + command = undefined; + + all = await commands.getCommands(); + expect(all).to.not.contain('gitweblinks.copySelection'); + }); + + + it(`should use the active document's selection and make it one-based.`, async () => { + let handler: MockLinkHandler; + let doc: TextDocument; + let editor: TextEditor; + + + handler = new MockLinkHandler(); + command = new CopyLinkToSelectionCommand(GIT_INFO, handler); + + doc = await workspace.openTextDocument(path.resolve(__dirname, '../../../test/test-helpers/data/10lines.txt')); + editor = await window.showTextDocument(doc); + + editor.selection = new Selection(new Position(1, 3), new Position(5, 2)); + + await commands.executeCommand('gitweblinks.copySelection', Uri.file(`${GIT_INFO.rootDirectory}foo.txt`)); + + expect(handler.selection).to.deep.equal({ startLine: 2, endLine: 6 }); + }); + + + it('should copy the URL to the clipboard.', async () => { + command = new CopyLinkToSelectionCommand(GIT_INFO, new MockLinkHandler()); + + await commands.executeCommand('gitweblinks.copySelection', Uri.file(`${GIT_INFO.rootDirectory}foo.txt`)); + + expect(clipboardStub.calledWith(FINAL_URL)).to.be.true; + }); + +}); diff --git a/test/configuration/CustomServiceProvider.test.ts b/test/configuration/CustomServiceProvider.test.ts new file mode 100644 index 0000000..b2bee91 --- /dev/null +++ b/test/configuration/CustomServiceProvider.test.ts @@ -0,0 +1,79 @@ +import * as chai from 'chai'; +import * as sinon from 'sinon'; +import * as sinonChai from 'sinon-chai'; + +import { workspace } from 'vscode'; + +import { CustomServerProvider } from '../../src/configuration/CustomServerProvider'; +import { ServerUrl } from '../../src/utilities/ServerUrl'; + + +const expect = chai.use(sinonChai).expect; + + +describe('CustomServerProvider', () => { + + let sandbox: sinon.SinonSandbox; + + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + + + afterEach(() => { + sandbox.restore(); + }); + + + describe('getServers', () => { + + it('should return the servers with the specified type.', () => { + let servers: ServerUrl[]; + let provider: CustomServerProvider; + let getConfiguration: sinon.SinonSpy; + let get: sinon.SinonSpy; + + + get = sinon.stub().returns([]); + getConfiguration = sandbox.stub(workspace, 'getConfiguration').returns({ get }); + + provider = new CustomServerProvider(); + servers = provider.getServers('foo'); + + expect(get).to.have.been.calledWith('gitweblinks.foo'); + }); + + + it('should not return servers without a base url.', () => { + let servers: ServerUrl[]; + let provider: CustomServerProvider; + let getConfiguration: sinon.SinonSpy; + let get: sinon.SinonSpy; + + + provider = new CustomServerProvider(); + + get = sinon.stub().returns([ + { baseUrl: 'a', sshUrl: 'b' }, + { baseUrl: '', sshUrl: 'd' }, + { baseUrl: undefined, sshUrl: 'f' }, + { baseUrl: 'g', sshUrl: '' }, + { baseUrl: 'i', sshUrl: undefined } + ]); + + getConfiguration = sandbox.stub(workspace, 'getConfiguration').returns({ get }); + + provider = new CustomServerProvider(); + servers = provider.getServers('foo'); + + expect(servers).to.deep.equal([ + { baseUrl: 'a', sshUrl: 'b' }, + { baseUrl: 'g', sshUrl: '' }, + { baseUrl: 'i', sshUrl: undefined } + ] as ServerUrl[]); + }); + + }); + +}); diff --git a/test/extension.test.ts b/test/extension.test.ts new file mode 100644 index 0000000..8dab821 --- /dev/null +++ b/test/extension.test.ts @@ -0,0 +1,202 @@ +import * as chai from 'chai'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import * as sinonChai from 'sinon-chai'; +import * as vscode from 'vscode'; + +import { CopyLinkToFileCommand } from '../src/commands/CopyLinkToFileCommand'; +import { CopyLinkToSelectionCommand } from '../src/commands/CopyLinkToSelectionCommand'; +import * as extension from '../src/extension'; +import { Git } from '../src/git/Git'; +import { GitInfo } from '../src/git/GitInfo'; +import { GitInfoFinder } from '../src/git/GitInfoFinder'; +import { LinkHandler } from '../src/links/LinkHandler'; +import { LinkHandlerFinder } from '../src/links/LinkHandlerFinder'; + + +const expect = chai.use(sinonChai).expect; + + +describe('extension', () => { + + describe('activate', () => { + + let sandbox: sinon.SinonSandbox; + let context: vscode.ExtensionContext; + + + function mockContext(): vscode.ExtensionContext { + return { + subscriptions: [], + } as any as vscode.ExtensionContext; + } + + + async function expectCommandsToHaveNotBeenRegistered(): Promise { + let commands: string[]; + + + commands = await vscode.commands.getCommands(); + + expect(commands).to.not.contain('gitweblinks.copyFile'); + expect(commands).to.not.contain('gitweblinks.copySelection'); + expect(context.subscriptions).to.be.empty; + } + + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + context = mockContext(); + sinon.stub(vscode, 'workspace').value({ rootPath: undefined }); + }); + + + afterEach(async () => { + context.subscriptions.forEach((d) => d.dispose()); + sandbox.restore(); + }); + + + it('should disable the commands if Git is not initialized.', async () => { + let test: sinon.SinonSpy; + let findGitInfo: sinon.SinonSpy; + let findHandler: sinon.SinonSpy; + let executeCommand: sinon.SinonSpy; + + + test = sandbox.stub(Git, 'test').returns(Promise.reject(new Error('nope'))); + findGitInfo = sandbox.stub(GitInfoFinder.prototype, 'find').returns(Promise.resolve(undefined)); + findHandler = sandbox.stub(LinkHandlerFinder.prototype, 'find').returns(Promise.resolve(undefined)); + executeCommand = sandbox.spy(vscode.commands, 'executeCommand'); + + await extension.activate(context); + + expect(test).to.have.been.called; + expect(executeCommand).to.have.been.calledWith('setContext', 'gitweblinks:canCopy', false); + + expect(findGitInfo).to.have.not.been.called; + expect(findHandler).to.have.not.been.called; + + await expectCommandsToHaveNotBeenRegistered(); + }); + + + it('should disable the commands if there is no workspace root.', async () => { + let test: sinon.SinonSpy; + let findGitInfo: sinon.SinonSpy; + let findHandler: sinon.SinonSpy; + let executeCommand: sinon.SinonSpy; + + + test = sandbox.stub(Git, 'test').returns(Promise.resolve()); + findGitInfo = sandbox.stub(GitInfoFinder.prototype, 'find').returns(Promise.resolve(undefined)); + findHandler = sandbox.stub(LinkHandlerFinder.prototype, 'find').returns(Promise.resolve(undefined)); + executeCommand = sandbox.spy(vscode.commands, 'executeCommand'); + + vscode.workspace.rootPath = undefined; + + await extension.activate(context); + + expect(test).to.have.been.called; + expect(executeCommand).to.have.been.calledWith('setContext', 'gitweblinks:canCopy', false); + + expect(findGitInfo).to.have.not.been.called; + expect(findHandler).to.have.not.been.called; + + await expectCommandsToHaveNotBeenRegistered(); + }); + + + it('should disable the commands if Git info is not found.', async () => { + let test: sinon.SinonSpy; + let findGitInfo: sinon.SinonSpy; + let findHandler: sinon.SinonSpy; + let executeCommand: sinon.SinonSpy; + + + test = sandbox.stub(Git, 'test').returns(Promise.resolve()); + findGitInfo = sandbox.stub(GitInfoFinder.prototype, 'find').returns(Promise.resolve(undefined)); + findHandler = sandbox.stub(LinkHandlerFinder.prototype, 'find').returns(Promise.resolve(undefined)); + executeCommand = sandbox.spy(vscode.commands, 'executeCommand'); + + vscode.workspace.rootPath = 'abc'; + + await extension.activate(context); + + expect(test).to.have.been.called; + expect(findGitInfo).to.have.been.called; + expect(executeCommand).to.have.been.calledWith('setContext', 'gitweblinks:canCopy', false); + + expect(findHandler).to.have.not.been.called; + + await expectCommandsToHaveNotBeenRegistered(); + }); + + + it('should disable the commands if no link handler was found.', async () => { + let test: sinon.SinonSpy; + let findGitInfo: sinon.SinonSpy; + let findHandler: sinon.SinonSpy; + let executeCommand: sinon.SinonSpy; + let info: GitInfo; + + + info = { rootDirectory: 'a', remoteUrl: 'b' }; + + test = sandbox.stub(Git, 'test').returns(Promise.resolve()); + findGitInfo = sandbox.stub(GitInfoFinder.prototype, 'find').returns(Promise.resolve(info)); + findHandler = sandbox.stub(LinkHandlerFinder.prototype, 'find').returns(undefined); + executeCommand = sandbox.spy(vscode.commands, 'executeCommand'); + + vscode.workspace.rootPath = 'abc'; + + await extension.activate(context); + + expect(test).to.have.been.called; + expect(findGitInfo).to.have.been.called; + expect(findHandler).to.have.been.called; + expect(executeCommand).to.have.been.calledWith('setContext', 'gitweblinks:canCopy', false); + + await expectCommandsToHaveNotBeenRegistered(); + }); + + + it('should enable the commands if a link handler was found.', async () => { + let test: sinon.SinonSpy; + let findGitInfo: sinon.SinonSpy; + let findHandler: sinon.SinonSpy; + let executeCommand: sinon.SinonSpy; + let info: GitInfo; + let handler: LinkHandler; + let commands: string[]; + + + info = { rootDirectory: 'a', remoteUrl: 'b' }; + handler = {} as any; + + test = sandbox.stub(Git, 'test').returns(Promise.resolve()); + findGitInfo = sandbox.stub(GitInfoFinder.prototype, 'find').returns(Promise.resolve(info)); + findHandler = sandbox.stub(LinkHandlerFinder.prototype, 'find').returns(handler); + executeCommand = sandbox.spy(vscode.commands, 'executeCommand'); + + vscode.workspace.rootPath = 'abc'; + + await extension.activate(context); + + expect(test).to.have.been.called; + expect(findGitInfo).to.have.been.called; + expect(findHandler).to.have.been.called; + expect(executeCommand).to.have.been.calledWith('setContext', 'gitweblinks:canCopy', true); + + commands = await vscode.commands.getCommands(); + expect(commands).to.contain('gitweblinks.copyFile'); + expect(commands).to.contain('gitweblinks.copySelection'); + + expect(context.subscriptions).to.have.lengthOf(2); + expect(context.subscriptions.filter((x) => x instanceof CopyLinkToFileCommand)).to.have.lengthOf(1); + expect(context.subscriptions.filter((x) => x instanceof CopyLinkToSelectionCommand)).to.have.lengthOf(1); + }); + + }); + +}); diff --git a/test/git/Git.test.ts b/test/git/Git.test.ts new file mode 100644 index 0000000..08b1fee --- /dev/null +++ b/test/git/Git.test.ts @@ -0,0 +1,21 @@ +import { expect } from 'chai'; + +import { Git } from '../../src/git/Git'; + + +describe('Git', () => { + + describe('execute', () => { + + it('should execute the command.', async () => { + let output: string; + + + output = await Git.execute(process.cwd(), '--version'); + + expect(output).to.match(/^git version /); + }); + + }); + +}); diff --git a/test/git/GitInfoFinder.test.ts b/test/git/GitInfoFinder.test.ts new file mode 100644 index 0000000..3e9bbaf --- /dev/null +++ b/test/git/GitInfoFinder.test.ts @@ -0,0 +1,76 @@ +import { expect } from 'chai'; +import * as mkdirp from 'mkdirp'; +import * as os from 'os'; +import * as path from 'path'; +import * as rimraf from 'rimraf'; +import { v4 as guid } from 'uuid'; + +import { Git } from '../../src/git/Git'; +import { GitInfo } from '../../src/git/GitInfo'; +import { GitInfoFinder } from '../../src/git/GitInfoFinder'; + + +describe('Git', () => { + + describe('find', () => { + + let root: string; + + + beforeEach(() => { + root = path.join(os.tmpdir(), guid()); + mkdirp.sync(root); + }); + + + afterEach(() => { + rimraf.sync(root); + }); + + + it('should not find the info when the workspace is not in a Git repository.', async () => { + let finder: GitInfoFinder; + let result: GitInfo | undefined; + + + finder = new GitInfoFinder(); + result = await finder.find(root); + + expect(result).to.be.undefined; + }); + + + it('should find the info when the workspace is at the root of the repository.', async () => { + let finder: GitInfoFinder; + let result: GitInfo | undefined; + + + await Git.execute(root, 'init'); + + finder = new GitInfoFinder(); + result = await finder.find(root); + + expect(result).to.be.undefined; + }); + + + it('should find the info when the workspace is below the root of the repository.', async () => { + let finder: GitInfoFinder; + let child: string; + let result: GitInfo | undefined; + + + await Git.execute(root, 'init'); + + child = path.join(root, 'child'); + mkdirp.sync(child); + + finder = new GitInfoFinder(); + result = await finder.find(child); + + expect(result).to.be.undefined; + }); + + }); + +}); diff --git a/test/index.ts b/test/index.ts new file mode 100644 index 0000000..8599fdc --- /dev/null +++ b/test/index.ts @@ -0,0 +1,9 @@ +// tslint:disable-next-line:no-var-requires +let testRunner = require('vscode/lib/testrunner'); + +testRunner.configure({ + ui: 'bdd', + useColors: true +}); + +module.exports = testRunner; diff --git a/test/links/BitbucketCloudHandler.test.ts b/test/links/BitbucketCloudHandler.test.ts new file mode 100644 index 0000000..2f27690 --- /dev/null +++ b/test/links/BitbucketCloudHandler.test.ts @@ -0,0 +1,146 @@ +// tslint:disable:max-line-length + +import { expect } from 'chai'; +import * as fs from 'fs'; +import * as mkdirp from 'mkdirp'; +import * as os from 'os'; +import * as path from 'path'; +import * as rimraf from 'rimraf'; +import { v4 as guid } from 'uuid'; + +import { Git } from '../../src/git/Git'; +import { GitInfo } from '../../src/git/GitInfo'; +import { BitbucketCloudHandler } from '../../src/links/BitbucketCloudHandler'; + + +describe('BitbucketCloudHandler', () => { + + function getRemotes(): string[] { + return [ + 'https://bitbucket.org/atlassian/atlassian-bamboo_rest.git', + 'https://username@bitbucket.org/atlassian/atlassian-bamboo_rest.git', + 'git@bitbucket.org:atlassian/atlassian-bamboo_rest.git', + 'ssh://git@bitbucket.org:atlassian/atlassian-bamboo_rest.git' + ]; + } + + + describe('isMatch', () => { + + getRemotes().forEach((remote) => { + it(`should match server '${remote}'.`, () => { + let handler: BitbucketCloudHandler; + + + handler = new BitbucketCloudHandler(); + + expect(handler.isMatch(remote)).to.be.true; + }); + }); + + + it('should not match other servers.', () => { + let handler: BitbucketCloudHandler; + + + handler = new BitbucketCloudHandler(); + + expect(handler.isMatch('https://codeplex.com/foo/bar.git')).to.be.false; + }); + + }); + + + describe('makeUrl', () => { + + let root: string; + + + beforeEach(async () => { + root = path.join(os.tmpdir(), guid()); + mkdirp.sync(root); + + await Git.execute(root, 'init'); + + fs.writeFileSync(path.join(root, 'file'), '', 'utf8'); + + await Git.execute(root, 'add', '.'); + await Git.execute(root, 'commit', '-m', '"initial"'); + }); + + + afterEach(() => { + rimraf.sync(root); + }); + + + getRemotes().forEach((remote) => { + it(`should create the correct link from the remote URL '${remote}'.`, async () => { + let handler: BitbucketCloudHandler; + let info: GitInfo; + let fileName: string; + + + info = { rootDirectory: root, remoteUrl: remote }; + fileName = path.join(root, 'lib/puppet/feature/restclient.rb'); + handler = new BitbucketCloudHandler(); + + expect(await handler.makeUrl(info, fileName, undefined)).to.equal( + 'https://bitbucket.org/atlassian/atlassian-bamboo_rest/src/master/lib/puppet/feature/restclient.rb', + ); + }); + }); + + + it('creates correct link with single line selection.', async () => { + let handler: BitbucketCloudHandler; + let info: GitInfo; + let fileName: string; + + + info = { rootDirectory: root, remoteUrl: 'git@bitbucket.org:atlassian/atlassian-bamboo_rest.git' }; + fileName = path.join(root, 'lib/puppet/feature/restclient.rb'); + handler = new BitbucketCloudHandler(); + + expect(await handler.makeUrl(info, fileName, { startLine: 2, endLine: 2 })).to.equal( + 'https://bitbucket.org/atlassian/atlassian-bamboo_rest/src/master/lib/puppet/feature/restclient.rb#restclient.rb-2', + ); + }); + + + it('creates correct link with multiple line selection.', async () => { + let handler: BitbucketCloudHandler; + let info: GitInfo; + let fileName: string; + + + info = { rootDirectory: root, remoteUrl: 'git@bitbucket.org:atlassian/atlassian-bamboo_rest.git' }; + fileName = path.join(root, 'lib/puppet/feature/restclient.rb'); + handler = new BitbucketCloudHandler(); + + expect(await handler.makeUrl(info, fileName, { startLine: 1, endLine: 3 })).to.equal( + 'https://bitbucket.org/atlassian/atlassian-bamboo_rest/src/master/lib/puppet/feature/restclient.rb#restclient.rb-1:3', + ); + }); + + + it('uses the current branch.', async () => { + let handler: BitbucketCloudHandler; + let info: GitInfo; + let fileName: string; + + + info = { rootDirectory: root, remoteUrl: 'git@bitbucket.org:atlassian/atlassian-bamboo_rest.git' }; + fileName = path.join(root, 'lib/puppet/feature/restclient.rb'); + handler = new BitbucketCloudHandler(); + + await Git.execute(root, 'checkout', '-b', 'feature/thing'); + + expect(await handler.makeUrl(info, fileName, undefined)).to.equal( + 'https://bitbucket.org/atlassian/atlassian-bamboo_rest/src/feature/thing/lib/puppet/feature/restclient.rb', + ); + }); + + }); + +}); diff --git a/test/links/BitbucketServerHandler.test.ts b/test/links/BitbucketServerHandler.test.ts new file mode 100644 index 0000000..99aade8 --- /dev/null +++ b/test/links/BitbucketServerHandler.test.ts @@ -0,0 +1,243 @@ +// tslint:disable:max-line-length + +import { expect } from 'chai'; +import * as fs from 'fs'; +import * as mkdirp from 'mkdirp'; +import * as os from 'os'; +import * as path from 'path'; +import * as rimraf from 'rimraf'; +import * as sinon from 'sinon'; +import { v4 as guid } from 'uuid'; + +import { CustomServerProvider } from '../../src/configuration/CustomServerProvider'; +import { Git } from '../../src/git/Git'; +import { GitInfo } from '../../src/git/GitInfo'; +import { BitbucketServerHandler } from '../../src/links/BitbucketServerHandler'; +import { ServerUrl } from '../../src/utilities/ServerUrl'; + + +describe('BitbucketServerHandler', () => { + + let sandbox: sinon.SinonSandbox; + + + function getRemotes(): string[] { + return [ + getHttpsRemoteUrl(), + getHttpsRemoteUrl().replace('https://', 'https://username@'), + getGitRemoteUrl(), + `ssh://${getGitRemoteUrl()}` + ]; + } + + + function getHttpsRemoteUrl(): string { + return 'https://local-bitbucket:7990/context/scm/bb/my-code.git'; + } + + + function getGitRemoteUrl(): string { + return 'git@local-bitbucket:7999/bb/my-code.git'; + } + + + function stubGetServers(servers?: ServerUrl[]): void { + if (!servers) { + servers = [{ + baseUrl: 'https://local-bitbucket:7990/context', + sshUrl: 'git@local-bitbucket:7999' + }]; + } + + sandbox.stub(CustomServerProvider.prototype, 'getServers').withArgs('bitbucketServer').returns(servers); + } + + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + + + afterEach(() => { + sandbox.restore(); + }); + + + describe('isMatch', () => { + + getRemotes().forEach((remote) => { + it(`should match server '${remote}'.`, () => { + let handler: BitbucketServerHandler; + + + stubGetServers(); + handler = new BitbucketServerHandler(); + + expect(handler.isMatch(remote)).to.be.true; + }); + }); + + + it('should not match other servers.', () => { + let handler: BitbucketServerHandler; + + + stubGetServers(); + handler = new BitbucketServerHandler(); + + expect(handler.isMatch('https://codeplex.com/foo/bar.git')).to.be.false; + }); + + }); + + + describe('makeUrl', () => { + + let root: string; + + + beforeEach(async () => { + root = path.join(os.tmpdir(), guid()); + mkdirp.sync(root); + + await Git.execute(root, 'init'); + + fs.writeFileSync(path.join(root, 'file'), '', 'utf8'); + + await Git.execute(root, 'add', '.'); + await Git.execute(root, 'commit', '-m', '"initial"'); + }); + + + afterEach(() => { + rimraf.sync(root); + }); + + + getRemotes().forEach((remote) => { + it(`should create the correct link from the remote URL '${remote}'.`, async () => { + let handler: BitbucketServerHandler; + let info: GitInfo; + let fileName: string; + + + stubGetServers(); + + info = { rootDirectory: root, remoteUrl: remote }; + fileName = path.join(root, 'lib/server/main.cs'); + handler = new BitbucketServerHandler(); + + expect(await handler.makeUrl(info, fileName, undefined)).to.equal( + 'https://local-bitbucket:7990/context/projects/bb/repos/my-code/browse/lib/server/main.cs?at=refs%2Fheads%2Fmaster', + ); + }); + }); + + + getRemotes().forEach((remote) => { + it(`should create the correct link from the HTTP remote '${remote}'`, async () => { + let handler: BitbucketServerHandler; + let info: GitInfo; + let fileName: string; + + + // Swap the `https` URL for an `http` URL if the remote us HTTPS. + if (remote.startsWith('https://')) { + remote = 'http://' + remote.substring('https://'.length); + } + + stubGetServers([{ + baseUrl: 'http://local-bitbucket:7990/context', + sshUrl: 'git@local-bitbucket:7999' + }]); + + info = { rootDirectory: root, remoteUrl: remote }; + fileName = path.join(root, 'lib/server/main.cs'); + handler = new BitbucketServerHandler(); + + expect(await handler.makeUrl(info, fileName, undefined)).to.equal( + 'http://local-bitbucket:7990/context/projects/bb/repos/my-code/browse/lib/server/main.cs?at=refs%2Fheads%2Fmaster', + ); + }); + }); + + + it('should creates the correct link when the server URL ends with a slash.', async () => { + let handler: BitbucketServerHandler; + let info: GitInfo; + let fileName: string; + + + stubGetServers([{ + baseUrl: 'https://local-bitbucket:7990/context/', + sshUrl: 'git@local-bitbucket:7999' + }]); + + info = { rootDirectory: root, remoteUrl: getHttpsRemoteUrl() }; + fileName = path.join(root, 'lib/server/main.cs'); + handler = new BitbucketServerHandler(); + + expect(await handler.makeUrl(info, fileName, undefined)).to.equal( + 'https://local-bitbucket:7990/context/projects/bb/repos/my-code/browse/lib/server/main.cs?at=refs%2Fheads%2Fmaster', + ); + }); + + + it('creates correct link with single line selection.', async () => { + let handler: BitbucketServerHandler; + let info: GitInfo; + let fileName: string; + + + stubGetServers(); + + info = { rootDirectory: root, remoteUrl: getGitRemoteUrl() }; + fileName = path.join(root, 'lib/server/main.cs'); + handler = new BitbucketServerHandler(); + + expect(await handler.makeUrl(info, fileName, { startLine: 2, endLine: 2 })).to.equal( + 'https://local-bitbucket:7990/context/projects/bb/repos/my-code/browse/lib/server/main.cs?at=refs%2Fheads%2Fmaster#2', + ); + }); + + + it('creates correct link with multiple line selection.', async () => { + let handler: BitbucketServerHandler; + let info: GitInfo; + let fileName: string; + + + stubGetServers(); + + info = { rootDirectory: root, remoteUrl: getGitRemoteUrl() }; + fileName = path.join(root, 'lib/server/main.cs'); + handler = new BitbucketServerHandler(); + + expect(await handler.makeUrl(info, fileName, { startLine: 10, endLine: 23 })).to.equal( + 'https://local-bitbucket:7990/context/projects/bb/repos/my-code/browse/lib/server/main.cs?at=refs%2Fheads%2Fmaster#10-23', + ); + }); + + + it('uses the current branch.', async () => { + let handler: BitbucketServerHandler; + let info: GitInfo; + let fileName: string; + + + stubGetServers(); + + info = { rootDirectory: root, remoteUrl: getGitRemoteUrl() }; + fileName = path.join(root, 'lib/server/main.cs'); + handler = new BitbucketServerHandler(); + + await Git.execute(root, 'checkout', '-b', 'feature/thing'); + + expect(await handler.makeUrl(info, fileName, undefined)).to.equal( + 'https://local-bitbucket:7990/context/projects/bb/repos/my-code/browse/lib/server/main.cs?at=refs%2Fheads%2Ffeature%2Fthing', + ); + }); + + }); + +}); diff --git a/test/links/GitHubHandler.test.ts b/test/links/GitHubHandler.test.ts new file mode 100644 index 0000000..508ca04 --- /dev/null +++ b/test/links/GitHubHandler.test.ts @@ -0,0 +1,244 @@ +// tslint:disable:max-line-length + +import { expect } from 'chai'; +import * as fs from 'fs'; +import * as mkdirp from 'mkdirp'; +import * as os from 'os'; +import * as path from 'path'; +import * as rimraf from 'rimraf'; +import * as sinon from 'sinon'; +import { v4 as guid } from 'uuid'; + +import { CustomServerProvider } from '../../src/configuration/CustomServerProvider'; +import { Git } from '../../src/git/Git'; +import { GitInfo } from '../../src/git/GitInfo'; +import { GitHubHandler } from '../../src/links/GitHubHandler'; +import { ServerUrl } from '../../src/utilities/ServerUrl'; + + +describe('GitHubHandler', () => { + + let sandbox: sinon.SinonSandbox; + + + function getCloudRemotes(): string[] { + return [ + 'https://github.com/dotnet/corefx.git', + 'https://username@github.com/dotnet/corefx.git', + 'git@github.com:dotnet/corefx.git', + 'ssh://git@github.com:dotnet/corefx.git' + ]; + } + + + function stubGetServers(servers?: ServerUrl[]): void { + if (!servers) { + servers = [{ + baseUrl: 'https://local-github', + sshUrl: 'git@local-github' + }]; + } + + sandbox.stub(CustomServerProvider.prototype, 'getServers').withArgs('gitHubEnterprise').returns(servers); + } + + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + + + afterEach(() => { + sandbox.restore(); + }); + + + describe('isMatch', () => { + + [ + 'https://github.com/dotnet/corefx.git', + 'git@github.com:dotnet/corefx.git', + 'ssh://git@github.com:dotnet/corefx.git', + ].forEach((remote) => { + it(`should match GitHub server URL '${remote}'.`, () => { + let handler: GitHubHandler; + + + stubGetServers(); + + handler = new GitHubHandler(); + + expect(handler.isMatch(remote)).to.be.true; + }); + }); + + + [ + 'https://local-github/dotnet/corefx.git', + 'git@local-github:dotnet/corefx.git', + 'ssh://git@local-github:dotnet/corefx.git' + ].forEach((remote) => { + it(`should match server URL from settings for remote '${remote}'`, () => { + let handler: GitHubHandler; + + + stubGetServers([{ baseUrl: 'https://local-github', sshUrl: 'git@local-github' }]); + + handler = new GitHubHandler(); + + expect(handler.isMatch(remote)).to.be.true; + }); + }); + + + it('should not match server URLs not in the settings.', () => { + let handler: GitHubHandler; + + + stubGetServers([{ baseUrl: 'https://local-github', sshUrl: 'git@local-github' }]); + + handler = new GitHubHandler(); + + expect(handler.isMatch('https://codeplex.com/foo/bar.git')).to.be.false; + }); + + }); + + + describe('makeUrl', () => { + + let root: string; + + + beforeEach(async () => { + root = path.join(os.tmpdir(), guid()); + mkdirp.sync(root); + + await Git.execute(root, 'init'); + + fs.writeFileSync(path.join(root, 'file'), '', 'utf8'); + + await Git.execute(root, 'add', '.'); + await Git.execute(root, 'commit', '-m', '"initial"'); + }); + + + afterEach(() => { + rimraf.sync(root); + }); + + + getCloudRemotes().forEach((remote) => { + it(`should create the correct link from the remote URL '${remote}'`, async () => { + let handler: GitHubHandler; + let info: GitInfo; + let fileName: string; + + + stubGetServers(); + + info = { rootDirectory: root, remoteUrl: remote }; + fileName = path.join(root, 'src/System.IO.FileSystem/src/System/IO/Directory.cs'); + handler = new GitHubHandler(); + + expect(await handler.makeUrl(info, fileName, undefined)).to.equal( + 'https://github.com/dotnet/corefx/blob/master/src/System.IO.FileSystem/src/System/IO/Directory.cs', + ); + }); + }); + + + it('should create the correct link when the server URL ends with a slash.', async () => { + let handler: GitHubHandler; + let info: GitInfo; + let fileName: string; + + + stubGetServers([{ baseUrl: 'https://local-github/', sshUrl: 'git@local-github' }]); + + info = { rootDirectory: root, remoteUrl: 'https://local-github/dotnet/corefx.git' }; + fileName = path.join(root, 'src/System.IO.FileSystem/src/System/IO/Directory.cs'); + handler = new GitHubHandler(); + + expect(await handler.makeUrl(info, fileName, undefined)).to.equal( + 'https://local-github/dotnet/corefx/blob/master/src/System.IO.FileSystem/src/System/IO/Directory.cs', + ); + }); + + + it('should create the correct link when the server URL whends with a colon.', async () => { + let handler: GitHubHandler; + let info: GitInfo; + let fileName: string; + + + stubGetServers([{ baseUrl: 'https://local-github', sshUrl: 'git@local-github:' }]); + + info = { rootDirectory: root, remoteUrl: 'git@local-github:dotnet/corefx.git' }; + fileName = path.join(root, 'src/System.IO.FileSystem/src/System/IO/Directory.cs'); + handler = new GitHubHandler(); + + expect(await handler.makeUrl(info, fileName, undefined)).to.equal( + 'https://local-github/dotnet/corefx/blob/master/src/System.IO.FileSystem/src/System/IO/Directory.cs', + ); + }); + + + it('should create the correct link with a single line selection.', async () => { + let handler: GitHubHandler; + let info: GitInfo; + let fileName: string; + + + stubGetServers(); + + info = { rootDirectory: root, remoteUrl: 'git@github.com:dotnet/corefx.git' }; + fileName = path.join(root, 'src/System.IO.FileSystem/src/System/IO/Directory.cs'); + handler = new GitHubHandler(); + + expect(await handler.makeUrl(info, fileName, { startLine: 38, endLine: 38 })).to.equal( + 'https://github.com/dotnet/corefx/blob/master/src/System.IO.FileSystem/src/System/IO/Directory.cs#L38', + ); + }); + + + it('should create the correct link with a multi-line selection.', async () => { + let handler: GitHubHandler; + let info: GitInfo; + let fileName: string; + + + stubGetServers(); + + info = { rootDirectory: root, remoteUrl: 'git@github.com:dotnet/corefx.git' }; + fileName = path.join(root, 'src/System.IO.FileSystem/src/System/IO/Directory.cs'); + handler = new GitHubHandler(); + + expect(await handler.makeUrl(info, fileName, { startLine: 38, endLine: 49 })).to.equal( + 'https://github.com/dotnet/corefx/blob/master/src/System.IO.FileSystem/src/System/IO/Directory.cs#L38-L49', + ); + }); + + + it('should use the current branch.', async () => { + let handler: GitHubHandler; + let info: GitInfo; + let fileName: string; + + + stubGetServers(); + + info = { rootDirectory: root, remoteUrl: 'git@github.com:dotnet/corefx.git' }; + fileName = path.join(root, 'src/System.IO.FileSystem/src/System/IO/Directory.cs'); + handler = new GitHubHandler(); + + await Git.execute(root, 'checkout', '-b', 'feature/thing'); + + expect(await handler.makeUrl(info, fileName, undefined)).to.equal( + 'https://github.com/dotnet/corefx/blob/feature/thing/src/System.IO.FileSystem/src/System/IO/Directory.cs', + ); + }); + + }); + +}); diff --git a/test/links/LinkHandlerFinder.test.ts b/test/links/LinkHandlerFinder.test.ts new file mode 100644 index 0000000..f3ae460 --- /dev/null +++ b/test/links/LinkHandlerFinder.test.ts @@ -0,0 +1,74 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; + +import { BitbucketCloudHandler } from '../../src/links/BitbucketCloudHandler'; +import { BitbucketServerHandler } from '../../src/links/BitbucketServerHandler'; +import { GitHubHandler } from '../../src/links/GitHubHandler'; +import { LinkHandler } from '../../src/links/LinkHandler'; +import { LinkHandlerFinder } from '../../src/links/LinkHandlerFinder'; + + +describe('LinkHandlerFinder', () => { + + let sandbox: sinon.SinonSandbox; + + + beforeEach(() => { + sandbox = sinon.sandbox.create(); + }); + + + afterEach(() => { + sandbox.restore(); + }); + + + function getHandlerTypes(): any[] { + return [ + BitbucketCloudHandler, + BitbucketServerHandler, + GitHubHandler, + ]; + } + + + describe('find', () => { + + it('should return undefined when no handler matches.', () => { + let finder: LinkHandlerFinder; + let result: LinkHandler | undefined; + + + getHandlerTypes().forEach((type) => { + sandbox.stub(type.prototype, 'isMatch').returns(false); + }); + + finder = new LinkHandlerFinder(); + + result = finder.find({ remoteUrl: 'a', rootDirectory: 'b' }); + + expect(result).to.be.undefined; + }); + + + getHandlerTypes().forEach((handler) => { + it(`should return the ${handler.name} when it matches.`, () => { + let finder: LinkHandlerFinder; + let result: LinkHandler | undefined; + + + getHandlerTypes().forEach((type) => { + sandbox.stub(type.prototype, 'isMatch').returns(type === handler); + }); + + finder = new LinkHandlerFinder(); + + result = finder.find({ remoteUrl: 'a', rootDirectory: 'b' }); + + expect(result).to.be.an.instanceOf(handler); + }); + }); + + }); + +}); diff --git a/test/test-helpers/MockLinkHandler.ts b/test/test-helpers/MockLinkHandler.ts new file mode 100644 index 0000000..6ab939f --- /dev/null +++ b/test/test-helpers/MockLinkHandler.ts @@ -0,0 +1,40 @@ +import { GitInfo } from '../../src/git/GitInfo'; +import { LinkHandler } from '../../src/links/LinkHandler'; +import { Selection } from '../../src/utilities/Selection'; +import { ServerUrl } from '../../src/utilities/ServerUrl'; + + +const BASE_URL: string = 'http://foo'; +const SSH_URL: string = 'git@foo'; + + +export const GIT_INFO: GitInfo = { rootDirectory: 'Z:\\', remoteUrl: `${BASE_URL}/meep` }; +export const FINAL_URL: string = 'the url'; + + +export class MockLinkHandler extends LinkHandler { + + public selection: Selection | undefined; + + + protected getServerUrls(): ServerUrl[] { + return [{ baseUrl: BASE_URL, sshUrl: SSH_URL }]; + } + + + protected getCurrentBranch(rootDirectory: string): Promise { + return Promise.resolve('foo'); + } + + + protected createUrl(baseUrl: string, repositoryPath: string, branch: string, relativePathToFile: string): string { + return FINAL_URL; + } + + + protected getSelectionHash(filePath: string, selection: Selection): string { + this.selection = selection; + return ''; + } + +} diff --git a/test/test-helpers/data/10lines.txt b/test/test-helpers/data/10lines.txt new file mode 100644 index 0000000..93e0521 --- /dev/null +++ b/test/test-helpers/data/10lines.txt @@ -0,0 +1,9 @@ +foo +bar +meep +bork +beep +boop +ping +pong +ding diff --git a/test/tslint.json b/test/tslint.json new file mode 100644 index 0000000..11d6371 --- /dev/null +++ b/test/tslint.json @@ -0,0 +1,10 @@ +{ + "extends": [ + "../tslint.json" + ], + "rules": { + "no-unused-expression": [ + false + ] + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..108f3a0 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "outDir": "out", + "strict": true, + "lib": [ + "es6" + ], + "sourceMap": true, + "rootDir": "." + }, + "exclude": [ + "node_modules", + ".vscode-test" + ] +} \ No newline at end of file diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..64f5951 --- /dev/null +++ b/tslint.json @@ -0,0 +1,31 @@ +{ + "defaultSeverity": "warning", + "extends": [ + "tslint:recommended" + ], + "rules": { + "quotemark": [ + true, + "single" + ], + "no-consecutive-blank-lines": [ + true, + 2 + ], + "prefer-const": [ + false + ], + "interface-name": [ + false + ], + "trailing-comma": [ + false + ], + "member-ordering": [ + false + ], + "object-literal-sort-keys": [ + false + ] + } +}