diff --git a/README.md b/README.md index 32050d6..e30ea39 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,8 @@ This extension allows you to create a code review file you can hand over to a cu - [`code-review.privateCommentIcon`](#code-reviewprivatecommenticon) - [`code-review.defaultTemplatePath`](#code-reviewdefaulttemplatepath) - [`code-review.priorities`](#code-reviewpriorities) - - [`code-review.gitDirectory`](#code-reviewgitdirectory) + - [`code-review.vcs.git.provider`](#code-reviewvcsgitprovider) + - [`code-review.vcs.git.directory`](#code-reviewvcsgitdirectory) - [`code-review.filterCommentsByCommit`](#code-reviewfiltercommentsbycommit) - [`code-review.filterCommentsByFilename`](#code-reviewfiltercommentsbyfilename) - [`code-review.importBackup`](#code-reviewimportbackup) @@ -64,7 +65,7 @@ A file `code-review.csv` will be created containing your comments and the file a The result will look like this: ```csv -sha,filename,url,lines,title,comment,priority,additional +revision,filename,url,lines,title,comment,priority,additional "b45d2822d6c87770af520d7e2acc49155f0b4362","/test/a.txt","https://github.com/d-koppenhagen/vscode-code-review/tree/b45d2822d6c87770af520d7e2acc49155f0b4362/test/a.txt","1:2-4:3","foo","this should be refactored","Complexity",1,"see http://foo.bar" "b45d2822d6c87770af520d7e2acc49155f0b4362","/test/b.txt","https://github.com/d-koppenhagen/vscode-code-review/tree/b45d2822d6c87770af520d7e2acc49155f0b4362/test/b.txt","1:0-1:4|4:0-4:3","bar","wrong format","Best Practices",1,"" ``` @@ -166,7 +167,7 @@ By default `"code-review"` is used. ### `code-review.baseUrl` The base-URL is used to build a full link to the file. -It will be appended with the git SHA if available followed by the relative path of the file and the selected lines as an anker. +It will be appended with the revision if available followed by the relative path of the file and the selected lines as an anchor. This setting is skipped when the setting `code-review.customUrl` is defined which is more configurable. ```json @@ -182,14 +183,14 @@ This setting would lead into something like this: `https://github.com/foo/bar/bl The custom URL is used to build a full link to the file. The following placeholders are available: -- `{sha}`: insert the SHA ref for the file +- `{revision}`: insert the file's revision - `{file}`: insert the file name/path -- `{start}`: insert the start of the lines selection as an anker -- `{end}`: insert the end of the lines selection as an anker +- `{start}`: insert the start of the lines selection as an anchor +- `{end}`: insert the end of the lines selection as an anchor ```json { - "code-review.customUrl": "https://gitlab.com/foo/bar/baz/-/blob/{sha}/src/{file}#L{start}-{end}" + "code-review.customUrl": "https://gitlab.com/foo/bar/baz/-/blob/{revision}/src/{file}#L{start}-{end}" } ``` @@ -305,7 +306,17 @@ The defaults are listed below: } ``` -### `code-review.gitDirectory` +### `code-review.vcs.git.provider` + +Permits selecting the version control system (VCS) used by your repositories. +The default is git, but svn and git-svn are also supported. + +Note on git-svn: This provider is kind of a hybrid. It extracts the svn file revision through the git-svn-cloned repository. +Use case for this provider is an only temporarily available svn repository (e.g., only accessible from a certain +physical location, like an offline-in-house network) that has been locally cloned with git-svn. The interesting revision +number then is not the git sha as it this is only local information, but the underlying svn revision information. + +### `code-review.vcs.git.directory` Use this setting when the Git repository is located in an other directory than the workspace one. The path can be **relative** (prefixed with `.` or `..`) or **absolute** (prefixed with `/` on Linux/MacOS or `{drive}:\` on Windows). @@ -316,7 +327,7 @@ Examples: ```json { - "code-review.gitDirectory": "./app" + "code-review.vcs.git.directory": "./app" } ``` @@ -324,7 +335,7 @@ Examples: ```json { - "code-review.gitDirectory": "../app" + "code-review.vcs.git.directory": "../app" } ``` @@ -332,7 +343,7 @@ Examples: ```json { - "code-review.gitDirectory": "/path/to/my/app" + "code-review.vcs.git.directory": "/path/to/my/app" } ``` @@ -340,7 +351,7 @@ Examples: ```json { - "code-review.gitDirectory": "C:\\Path\\To\\My\\App" + "code-review.vcs.git.directory": "C:\\Path\\To\\My\\App" } ``` @@ -446,7 +457,7 @@ To create a code review with a report you should install this extension and go o - Download / clone the customer code and checkout the correct branch - Open the project in vscode - [Configure the `baseURL` option](#extension-settings) with the remote URL - - this will cause that the link in the report is generate with the correct target including SHA, file and line reference + - this will cause that the link in the report is generate with the correct target including revision, file and line reference - [Start creating your review notes](#create-review-notes). - [Export the report](#export-created-notes-as-html). - [Probably create an own template first](#custom-handlebars-template) diff --git a/package-lock.json b/package-lock.json index c717997..2acc403 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,26 +10,30 @@ "license": "SEE LICENSE IN LICENSE", "dependencies": { "@fast-csv/parse": "^4.3.6", - "common-tags": "^1.8.2", "git-commit-id": "^2.0.1", "handlebars": "^4.7.7", "js-base64": "^3.6.1", - "strip-indent": "^4.0.0", "uuid": "8.3.2" }, "devDependencies": { "@commitlint/cli": "^11.0.0", "@commitlint/config-conventional": "^11.0.0", + "@types/chai": "^4.2.22", + "@types/chai-as-promised": "^7.1.4", "@types/common-tags": "^1.8.1", "@types/glob": "^7.1.3", "@types/handlebars": "^4.1.0", "@types/js-base64": "^3.0.0", "@types/mocha": "^8.2.2", "@types/node": "14.x", + "@types/sinon": "^10.0.6", "@types/vscode": "^1.59.0", "@typescript-eslint/eslint-plugin": "^4.26.0", "@typescript-eslint/parser": "^4.26.0", + "chai": "^4.3.4", + "chai-as-promised": "^7.1.1", "commitizen": "^4.2.4", + "common-tags": "^1.8.2", "copy-webpack-plugin": "^6.2.1", "cz-conventional-changelog": "^3.3.0", "eslint": "^7.27.0", @@ -39,9 +43,11 @@ "mocha": "^8.4.0", "prettier": "^2.1.2", "pretty-quick": "^3.1.0", + "sinon": "^12.0.1", "standard-version": "^9.3.1", + "stub-spawn-once": "^2.3.0", "ts-loader": "^9.2.2", - "typescript": "^4.3.2", + "typescript": "^4.5.2", "vsce": "^1.96.1", "vscode-extension-tester": "^4.1.2", "vscode-test": "^1.5.2", @@ -737,6 +743,41 @@ "node": ">=10" } }, + "node_modules/@sinonjs/commons": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", + "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz", + "integrity": "sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.7.0" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.0.2.tgz", + "integrity": "sha512-jxPRPp9n93ci7b8hMfJOFDPRLFYadN6FSpeROFTR4UNF4i5b+EK6m4QXPO46BDhFgRy1JuS87zAnFOzCUwMJcQ==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.6.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -746,6 +787,21 @@ "node": ">= 6" } }, + "node_modules/@types/chai": { + "version": "4.2.22", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.22.tgz", + "integrity": "sha512-tFfcE+DSTzWAgifkjik9AySNqIyNoYwmR+uecPwwD/XRNfvOjmC/FjCxpiUGDkDVDphPfCUecSQVFw+lN3M3kQ==", + "dev": true + }, + "node_modules/@types/chai-as-promised": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.4.tgz", + "integrity": "sha512-1y3L1cHePcIm5vXkh1DSGf/zQq5n5xDKG1fpCvf18+uOkpce0Z1ozNFPkyWsVswK7ntN1sZBw3oU6gmN+pDUcA==", + "dev": true, + "dependencies": { + "@types/chai": "*" + } + }, "node_modules/@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", @@ -857,6 +913,24 @@ "integrity": "sha512-OFUilxQg+rWL2FMxtmIgCkUDlJB6pskkpvmew7yeXfzzsOBb5rc+y2+DjHm+r3r1ZPPcJefK3DveNSYWGiy68g==", "dev": true }, + "node_modules/@types/sinon": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.6.tgz", + "integrity": "sha512-6EF+wzMWvBNeGrfP3Nx60hhx+FfwSg1JJBLAAP/IdIUq0EYkqCYf70VT3PhuhPX9eLD+Dp+lNdpb/ZeHG8Yezg==", + "dev": true, + "dependencies": { + "@sinonjs/fake-timers": "^7.1.0" + } + }, + "node_modules/@types/sinon/node_modules/@sinonjs/fake-timers": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz", + "integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.7.0" + } + }, "node_modules/@types/vscode": { "version": "1.59.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.59.0.tgz", @@ -1493,6 +1567,15 @@ "node": ">=0.8" } }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -1841,6 +1924,35 @@ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", "dev": true }, + "node_modules/chai": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz", + "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==", + "dev": true, + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chai-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz", + "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==", + "dev": true, + "dependencies": { + "check-error": "^1.0.2" + }, + "peerDependencies": { + "chai": ">= 2.1.2 < 5" + } + }, "node_modules/chainsaw": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", @@ -1873,6 +1985,24 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, + "node_modules/check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/check-more-types": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", + "integrity": "sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA=", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/cheerio": { "version": "1.0.0-rc.10", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.10.tgz", @@ -2287,6 +2417,7 @@ "version": "1.8.2", "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true, "engines": { "node": ">=4.0.0" } @@ -3599,6 +3730,18 @@ "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", "dev": true }, + "node_modules/deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", @@ -4811,6 +4954,15 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/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, + "engines": { + "node": "*" + } + }, "node_modules/get-intrinsic": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", @@ -6040,6 +6192,12 @@ "set-immediate-shim": "~1.0.1" } }, + "node_modules/just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true + }, "node_modules/kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -6049,6 +6207,15 @@ "node": ">=0.10.0" } }, + "node_modules/lazy-ass": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", + "integrity": "sha1-eZllXoZGwX8In90YfRUNMyTVRRM=", + "dev": true, + "engines": { + "node": "> 0.8" + } + }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -6191,6 +6358,12 @@ "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", "integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=" }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true + }, "node_modules/lodash.groupby": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", @@ -6540,6 +6713,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, "engines": { "node": ">=4" } @@ -7115,6 +7289,28 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "node_modules/nise": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.0.tgz", + "integrity": "sha512-W5WlHu+wvo3PaKLsJJkgPup2LrsXCcm7AWwyNZkUnn5rwPkuPBi3Iwk5SQtN0mv+K65k7nKKjwNQ30wg3wLAQQ==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.7.0", + "@sinonjs/fake-timers": "^7.0.4", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + } + }, + "node_modules/nise/node_modules/@sinonjs/fake-timers": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz", + "integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.7.0" + } + }, "node_modules/node-releases": { "version": "1.1.74", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.74.tgz", @@ -7445,6 +7641,21 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/path-to-regexp/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -7454,6 +7665,15 @@ "node": ">=8" } }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "engines": { + "node": "*" + } + }, "node_modules/pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -8232,6 +8452,45 @@ "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", "dev": true }, + "node_modules/sinon": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-12.0.1.tgz", + "integrity": "sha512-iGu29Xhym33ydkAT+aNQFBINakjq69kKO6ByPvTsm3yyIACfyQttRTP03aBP/I8GfhFmLzrnKwNNkr0ORb1udg==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.8.3", + "@sinonjs/fake-timers": "^8.1.0", + "@sinonjs/samsam": "^6.0.2", + "diff": "^5.0.0", + "nise": "^5.1.0", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/sinon/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -8739,20 +8998,6 @@ "node": ">=6" } }, - "node_modules/strip-indent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.0.0.tgz", - "integrity": "sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==", - "dependencies": { - "min-indent": "^1.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/strip-json-comments": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", @@ -8762,6 +9007,35 @@ "node": ">=8" } }, + "node_modules/stub-spawn-once": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/stub-spawn-once/-/stub-spawn-once-2.3.0.tgz", + "integrity": "sha1-NOp1CPsAGZLwA+1T8rWIIniDT7s=", + "dev": true, + "dependencies": { + "check-more-types": "2.24.0", + "debug": "2.6.8", + "lazy-ass": "1.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/stub-spawn-once/node_modules/debug": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", + "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/stub-spawn-once/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -9309,6 +9583,15 @@ "node": ">= 0.8.0" } }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/type-fest": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", @@ -9340,9 +9623,9 @@ "dev": true }, "node_modules/typescript": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", - "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz", + "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -10818,12 +11101,62 @@ "mkdirp": "^1.0.4" } }, + "@sinonjs/commons": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", + "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-8.1.0.tgz", + "integrity": "sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "@sinonjs/samsam": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.0.2.tgz", + "integrity": "sha512-jxPRPp9n93ci7b8hMfJOFDPRLFYadN6FSpeROFTR4UNF4i5b+EK6m4QXPO46BDhFgRy1JuS87zAnFOzCUwMJcQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.6.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, "@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", "dev": true }, + "@types/chai": { + "version": "4.2.22", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.22.tgz", + "integrity": "sha512-tFfcE+DSTzWAgifkjik9AySNqIyNoYwmR+uecPwwD/XRNfvOjmC/FjCxpiUGDkDVDphPfCUecSQVFw+lN3M3kQ==", + "dev": true + }, + "@types/chai-as-promised": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.4.tgz", + "integrity": "sha512-1y3L1cHePcIm5vXkh1DSGf/zQq5n5xDKG1fpCvf18+uOkpce0Z1ozNFPkyWsVswK7ntN1sZBw3oU6gmN+pDUcA==", + "dev": true, + "requires": { + "@types/chai": "*" + } + }, "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", @@ -10934,6 +11267,26 @@ "integrity": "sha512-OFUilxQg+rWL2FMxtmIgCkUDlJB6pskkpvmew7yeXfzzsOBb5rc+y2+DjHm+r3r1ZPPcJefK3DveNSYWGiy68g==", "dev": true }, + "@types/sinon": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.6.tgz", + "integrity": "sha512-6EF+wzMWvBNeGrfP3Nx60hhx+FfwSg1JJBLAAP/IdIUq0EYkqCYf70VT3PhuhPX9eLD+Dp+lNdpb/ZeHG8Yezg==", + "dev": true, + "requires": { + "@sinonjs/fake-timers": "^7.1.0" + }, + "dependencies": { + "@sinonjs/fake-timers": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz", + "integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + } + } + }, "@types/vscode": { "version": "1.59.0", "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.59.0.tgz", @@ -11408,6 +11761,12 @@ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", "dev": true }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, "astral-regex": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", @@ -11684,6 +12043,29 @@ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", "dev": true }, + "chai": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz", + "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + } + }, + "chai-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz", + "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==", + "dev": true, + "requires": { + "check-error": "^1.0.2" + } + }, "chainsaw": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", @@ -11710,6 +12092,18 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "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 + }, + "check-more-types": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/check-more-types/-/check-more-types-2.24.0.tgz", + "integrity": "sha1-FCD/sQ/URNz8ebQ4kbv//TKoRgA=", + "dev": true + }, "cheerio": { "version": "1.0.0-rc.10", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.10.tgz", @@ -12044,7 +12438,8 @@ "common-tags": { "version": "1.8.2", "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", - "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==" + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "dev": true }, "commondir": { "version": "1.0.1", @@ -13054,6 +13449,15 @@ "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", "dev": true }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", @@ -13985,6 +14389,12 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "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 + }, "get-intrinsic": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", @@ -14912,12 +15322,24 @@ "set-immediate-shim": "~1.0.1" } }, + "just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true }, + "lazy-ass": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", + "integrity": "sha1-eZllXoZGwX8In90YfRUNMyTVRRM=", + "dev": true + }, "leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -15038,6 +15460,12 @@ "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", "integrity": "sha1-ZHYsSGGAglGKw99Mz11YhtriA0c=" }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true + }, "lodash.groupby": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", @@ -15316,7 +15744,8 @@ "min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==" + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true }, "minimatch": { "version": "3.0.4", @@ -15737,6 +16166,30 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "nise": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.0.tgz", + "integrity": "sha512-W5WlHu+wvo3PaKLsJJkgPup2LrsXCcm7AWwyNZkUnn5rwPkuPBi3Iwk5SQtN0mv+K65k7nKKjwNQ30wg3wLAQQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0", + "@sinonjs/fake-timers": "^7.0.4", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "@sinonjs/fake-timers": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.2.tgz", + "integrity": "sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + } + } + }, "node-releases": { "version": "1.1.74", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.74.tgz", @@ -15993,12 +16446,35 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "requires": { + "isarray": "0.0.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + } + } + }, "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true }, + "pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true + }, "pend": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", @@ -16598,6 +17074,37 @@ "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", "dev": true }, + "sinon": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-12.0.1.tgz", + "integrity": "sha512-iGu29Xhym33ydkAT+aNQFBINakjq69kKO6ByPvTsm3yyIACfyQttRTP03aBP/I8GfhFmLzrnKwNNkr0ORb1udg==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.8.3", + "@sinonjs/fake-timers": "^8.1.0", + "@sinonjs/samsam": "^6.0.2", + "diff": "^5.0.0", + "nise": "^5.1.0", + "supports-color": "^7.2.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -16995,20 +17502,40 @@ "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "dev": true }, - "strip-indent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.0.0.tgz", - "integrity": "sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==", - "requires": { - "min-indent": "^1.0.1" - } - }, "strip-json-comments": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", "dev": true }, + "stub-spawn-once": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/stub-spawn-once/-/stub-spawn-once-2.3.0.tgz", + "integrity": "sha1-NOp1CPsAGZLwA+1T8rWIIniDT7s=", + "dev": true, + "requires": { + "check-more-types": "2.24.0", + "debug": "2.6.8", + "lazy-ass": "1.6.0" + }, + "dependencies": { + "debug": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", + "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + } + } + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -17432,6 +17959,12 @@ "prelude-ls": "^1.2.1" } }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "type-fest": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", @@ -17456,9 +17989,9 @@ "dev": true }, "typescript": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz", - "integrity": "sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==", + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz", + "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==", "dev": true }, "uc.micro": { diff --git a/package.json b/package.json index 80f1343..eeefc0c 100644 --- a/package.json +++ b/package.json @@ -130,12 +130,12 @@ }, { "command": "codeReview.exportAsGitLabImportableCsv", - "when": "view == code-review.list", + "when": "view == code-review.list && config.code-review.vcs.provider == git", "group": "export_source_control@1" }, { "command": "codeReview.exportAsGitHubImportableCsv", - "when": "view == code-review.list", + "when": "view == code-review.list && config.code-review.vcs.provider == git", "group": "export_source_control@0" }, { @@ -160,12 +160,12 @@ }, { "command": "codeReview.filterByCommitEnable", - "when": "view == code-review.list && !isFilteredByCommit", + "when": "view == code-review.list && !isFilteredByCommit && config.code-review.vcs.provider == git", "group": "navigation@1" }, { "command": "codeReview.filterByCommitDisable", - "when": "view == code-review.list && isFilteredByCommit", + "when": "view == code-review.list && isFilteredByCommit && config.code-review.vcs.provider == git", "group": "navigation@1" }, { @@ -200,13 +200,13 @@ "code-review.baseUrl": { "type": "string", "title": "The base URL for a referenced file without the SHA", - "description": "The base URL is used to build a full link to the file (e.g. \"https://github.com/foo/bar/blob/b0b4...0175/src/file.txt#L12-L19\"). It will be appended with the GIT SHA if available followed by the relative path of the file and the selected lines as an anker. This setting is skipped when the setting `code-review.customUrl` is defined.", + "description": "The base URL is used to build a full link to the file (e.g. \"https://github.com/foo/bar/blob/b0b4...0175/src/file.txt#L12-L19\"). It will be appended with the GIT SHA if available followed by the relative path of the file and the selected lines as an anchor. This setting is skipped when the setting `code-review.customUrl` is defined.", "default": "" }, "code-review.customUrl": { "type": "string", "title": "The URL mask with placeholders for a referenced file without the SHA", - "description": "The custom URL is used to build a full link to the file.\nThe following placeholders are available:\n - {sha}: insert the SHA ref for the file\n - {file}: insert the file name/path\n - {start}: insert the start of the lines selection as an anker\n - {end}: insert the end of the lines selection as an anker\ne.g. \"https://gitlab.com/foo/bar/baz/-/blob/{sha}/src/{file}#L{start}-{end}\" becomes this in the end: \"https://gitlab.com/foo/bar/baz/-/blob/b0b4...0175/src/file.txt#L12-19\"", + "description": "The custom URL is used to build a full link to the file.\nThe following placeholders are available:\n - {sha}: insert the SHA ref for the file\n - {file}: insert the file name/path\n - {start}: insert the start of the lines selection as an anchor\n - {end}: insert the end of the lines selection as an anchor\ne.g. \"https://gitlab.com/foo/bar/baz/-/blob/{sha}/src/{file}#L{start}-{end}\" becomes this in the end: \"https://gitlab.com/foo/bar/baz/-/blob/b0b4...0175/src/file.txt#L12-19\"", "default": "" }, "code-review.categories": { @@ -274,7 +274,7 @@ "high" ] }, - "code-review.gitDirectory": { + "code-review.vcs.git.directory": { "type": "string", "title": "The folder containing the Git repository", "markdownDescription": "Use this setting when the Git repository is located in an other directory than the workspace one.\nThe path can be **relative** (prefixed with `.` or `..`) or **absolute** (prefixed with `/` on Linux/MacOS or `{drive}:\\` on Windows).\nExamples:\n- `./app`: for {workspace}/app (Linux/MacOS)\n- `../app`: for a folder at the same level as the workspace (Linux/MacOS)\n- `/path/to/my/app`: for an absolute path (Linux/MacOS)\n- `C:\\Path\\To\\My\\App`: for an absolute path (Windows)", @@ -326,6 +326,16 @@ "title": "Code selection background color.", "markdownDescription": "Background color used to highlight the code associated to a comment.\nMust be specified using a hexadecimal representation - with or without the alpha part (`#C8C832` or `#C8C83226`) - or a `rgba()` definition.", "default": "codereview.code.selection.background" + }, + "code-review.vcs.provider": { + "enum": [ + "git", + "svn", + "git-svn" + ], + "default": "git", + "title": "Version Control", + "description": "Version control used in this project. Necessary to determine the commit id to be attached to comments." } } }, @@ -418,16 +428,22 @@ "devDependencies": { "@commitlint/cli": "^11.0.0", "@commitlint/config-conventional": "^11.0.0", + "@types/chai": "^4.2.22", + "@types/chai-as-promised": "^7.1.4", "@types/common-tags": "^1.8.1", "@types/glob": "^7.1.3", "@types/handlebars": "^4.1.0", "@types/js-base64": "^3.0.0", "@types/mocha": "^8.2.2", "@types/node": "14.x", + "@types/sinon": "^10.0.6", "@types/vscode": "^1.59.0", "@typescript-eslint/eslint-plugin": "^4.26.0", "@typescript-eslint/parser": "^4.26.0", + "chai": "^4.3.4", + "chai-as-promised": "^7.1.1", "commitizen": "^4.2.4", + "common-tags": "^1.8.2", "copy-webpack-plugin": "^6.2.1", "cz-conventional-changelog": "^3.3.0", "eslint": "^7.27.0", @@ -437,9 +453,11 @@ "mocha": "^8.4.0", "prettier": "^2.1.2", "pretty-quick": "^3.1.0", + "sinon": "^12.0.1", "standard-version": "^9.3.1", + "stub-spawn-once": "^2.3.0", "ts-loader": "^9.2.2", - "typescript": "^4.3.2", + "typescript": "^4.5.2", "vsce": "^1.96.1", "vscode-extension-tester": "^4.1.2", "vscode-test": "^1.5.2", diff --git a/src/export-factory.ts b/src/export-factory.ts index 344c5c5..5e922c4 100644 --- a/src/export-factory.ts +++ b/src/export-factory.ts @@ -25,13 +25,14 @@ import { sortLineSelections, rangeFromStringDefinition, escapeEndOfLineForCsv, - standardizeFilename, + relativeToWorkspace, splitStringDefinition, } from './utils/workspace-util'; import { ReviewFileExportSection, GroupBy, ExportFormat, ExportMap, Group } from './interfaces'; import { CsvEntry, CsvStructure } from './model'; import { CommentListEntry } from './comment-list-entry'; import { FileGenerator } from './file-generator'; +import { gitRevision } from './vcs-provider'; import { Location, parseLocation, themeColorForPriority } from './utils/editor-utils'; const gitCommitId = require('git-commit-id'); @@ -105,7 +106,7 @@ export class ExportFactory { */ private isCommentEligible(entry: CsvEntry): boolean { return ( - (this.currentCommitId === null || entry.sha === this.currentCommitId) && + (this.currentCommitId === null || entry.revision === this.currentCommitId) && (this.currentFilename === null || entry.filename === this.currentFilename) ); } @@ -168,13 +169,13 @@ export class ExportFactory { const title = row.title ? row.title.substring(0, 255) : descShort; const fileRow = row.url ? `- file: [${row.filename}](${row.url})${EOL}` : `${row.filename}${EOL}`; const linesRow = `- lines: ${row.lines}${EOL}`; - const shaRow = row.sha ? `- SHA: ${row.sha}${EOL}${EOL}` : ''; + const revRow = row.revision ? `- SHA: ${row.revision}${EOL}${EOL}` : ''; const commentSection = `## Comment${EOL}${row.comment}${EOL}`; const additional = row.additional ? `## Additional information${EOL}${row.additional}${EOL}` : ''; const priority = row.priority ? `## Priority${EOL}${this.priorityName(row.priority)}${EOL}${EOL}` : ''; const category = row.category ? `## Category${EOL}${row.category}${EOL}${EOL}` : ''; const code = row.code ? `${EOL}## Source Code${EOL}${EOL}\`\`\`${EOL}${row.code}\`\`\`${EOL}` : ''; - const description = `${priority}${category}## Affected${EOL}${fileRow}${linesRow}${shaRow}${commentSection}${EOL}${additional}${code}`; + const description = `${priority}${category}## Affected${EOL}${fileRow}${linesRow}${revRow}${commentSection}${EOL}${additional}${code}`; fs.appendFileSync(outputFile, `"[code review] ${title}","${description}"${EOL}`); return row; }, @@ -202,14 +203,14 @@ export class ExportFactory { const fileRow = row.url ? `- file: [${row.filename}](${row.url})${EOL}` : `${row.filename}${EOL}`; const linesRow = `- lines: ${row.lines}${EOL}`; - const shaRow = row.sha ? `- SHA: ${row.sha}${EOL}${EOL}` : ''; + const revRow = row.revision ? `- SHA: ${row.revision}${EOL}${EOL}` : ''; const commentSection = `## Comment${EOL}${row.comment}${EOL}`; const additional = row.additional ? `## Additional information${EOL}${row.additional}${EOL}` : ''; const priority = row.priority ? `## Priority${EOL}${this.priorityName(row.priority)}${EOL}${EOL}` : ''; const category = row.category ? `## Category${EOL}${row.category}${EOL}${EOL}` : ''; const code = row.code ? `${EOL}## Source Code${EOL}${EOL}\`\`\`${EOL}${row.code}\`\`\`${EOL}` : ''; - const description = `${priority}${category}## Affected${EOL}${fileRow}${linesRow}${shaRow}${commentSection}${EOL}${additional}${code}`; + const description = `${priority}${category}## Affected${EOL}${fileRow}${linesRow}${revRow}${commentSection}${EOL}${additional}${code}`; fs.appendFileSync(outputFile, `"[code review] ${title}","${description}","code-review","open",""${EOL}`); return row; @@ -227,7 +228,7 @@ export class ExportFactory { writeFileHeader: (outputFile: string) => { fs.writeFileSync( outputFile, - `Summary,Description,Priority,sha,filename,url,lines,title,category,comment,additional${EOL}`, + `Summary,Description,Priority,revision,filename,url,lines,title,category,comment,additional${EOL}`, ); }, handleData: (outputFile: string, row: CsvEntry): CsvEntry => { @@ -241,17 +242,17 @@ export class ExportFactory { const fileRow = row.url ? `* file: [${row.filename}|${row.url}]${EOL}` : `${row.filename}${EOL}`; const linesRow = `* lines: ${row.lines}${EOL}`; - const shaRow = row.sha ? `* SHA: ${row.sha}${EOL}${EOL}` : ''; + const revRow = row.revision ? `* SHA: ${row.revision}${EOL}${EOL}` : ''; const categorySection = `h2. Category${EOL}${row.category}${EOL}${EOL}`; const commentSection = `h2. Comment${EOL}${row.comment}${EOL}`; const additional = row.additional ? `h2. Additional information${EOL}${row.additional}${EOL}` : ''; const code = row.code ? `${EOL}h2. Source Code${EOL}${EOL}{code}${EOL}${row.code}{code}${EOL}` : ''; - const description = `h2. Affected${EOL}${fileRow}${linesRow}${shaRow}${categorySection}${commentSection}${EOL}${additional}${code}`; + const description = `h2. Affected${EOL}${fileRow}${linesRow}${revRow}${categorySection}${commentSection}${EOL}${additional}${code}`; fs.appendFileSync( outputFile, - `"[code review] ${title}","${description}","${this.priorityName(row.priority)}","${row.sha}","${ + `"[code review] ${title}","${description}","${this.priorityName(row.priority)}","${row.revision}","${ row.filename }","${row.url}","${row.lines}","${row.title}","${row.category}","${row.comment}","${row.additional}"${EOL}`, ); @@ -539,17 +540,17 @@ export class ExportFactory { public setFilterByCommit(state: boolean): boolean { this.filterByCommit = state; if (this.filterByCommit) { - try { - const gitDirectory = workspace.getConfiguration().get('code-review.gitDirectory') as string; - const gitRepositoryPath = path.resolve(this.workspaceRoot, gitDirectory); - - this.currentCommitId = gitCommitId({ cwd: gitRepositoryPath }); - } catch (error) { - this.filterByCommit = false; - this.currentCommitId = null; + gitRevision('.', this.workspaceRoot).then( + (revision: string) => { + this.currentCommitId = revision; + }, + (error: string) => { + this.filterByCommit = false; + this.currentCommitId = null; - console.log('Not in a git repository. Disabling filter by commit', error); - } + console.log('Not in a git repository. Disabling filter by commit.', error); + }, + ); } else { this.currentCommitId = null; } @@ -581,7 +582,7 @@ export class ExportFactory { if (this.filterByFilename) { let filename = window.activeTextEditor?.document.fileName; if (filename) { - filename = standardizeFilename(this.workspaceRoot, filename); + filename = relativeToWorkspace(this.workspaceRoot, filename); if (this.currentFilename !== filename) { changedFile = true; this.currentFilename = filename; diff --git a/src/model.md b/src/model.md index 72107bb..7cf7c42 100644 --- a/src/model.md +++ b/src/model.md @@ -12,7 +12,7 @@ All the steps will be operated in the file `model.ts`. ```diff export interface CsvEntry { - sha: string; + revision: string; filename: string; url: string; lines: string; @@ -33,7 +33,7 @@ All the steps will be operated in the file `model.ts`. ```diff private static readonly headers: string[] = [ - 'sha', + 'revision', 'filename', 'url', 'lines', @@ -105,7 +105,7 @@ A sample way to deprecate a property is to add a comment above it in the **inter ```diff export interface CsvEntry { - sha: string; + revision: string; filename: string; + /** @deprecated */ url: string; diff --git a/src/model.ts b/src/model.ts index 0ec8b13..b9a60f0 100644 --- a/src/model.ts +++ b/src/model.ts @@ -16,7 +16,7 @@ const { v4: uuidv4, validate: uuidValidate } = require('uuid'); * 4. Update createCommentFromObject() (optional). */ export interface CsvEntry { - sha: string; + revision: string; filename: string; url: string; lines: string; @@ -66,7 +66,7 @@ export class CsvStructure { * - A deprecated stored property can't be removed from the array. */ private static readonly headers: string[] = [ - 'sha', + 'revision', 'filename', 'url', 'lines', diff --git a/src/review-comment.ts b/src/review-comment.ts index afff3f3..4983491 100644 --- a/src/review-comment.ts +++ b/src/review-comment.ts @@ -7,12 +7,11 @@ import { removeTrailingSlash, startLineNumberFromStringDefinition, endLineNumberFromStringDefinition, - standardizeFilename, + relativeToWorkspace, } from './utils/workspace-util'; -import { CommentListEntry } from './comment-list-entry'; import { getSelectionStringDefinition, hasSelection } from './utils/editor-utils'; import { getCsvFileLinesAsArray, setCsvFileLines } from './utils/storage-utils'; -import path from 'path'; +import { revision } from './vcs-provider'; export class ReviewCommentService { constructor(private reviewFile: string, private workspaceRoot: string) {} @@ -29,9 +28,11 @@ export class ReviewCommentService { return; } - comment.filename = standardizeFilename(this.workspaceRoot, editor!.document.fileName); + comment.filename = relativeToWorkspace(this.workspaceRoot, editor!.document.fileName); - setCsvFileLines(this.reviewFile, [CsvStructure.formatAsCsvLine(this.finalizeComment(comment))], false); + this.finalizeComment(comment, this.workspaceRoot).then((entry: CsvEntry) => { + setCsvFileLines(this.reviewFile, [CsvStructure.formatAsCsvLine(entry)], false); + }); } /** @@ -63,8 +64,10 @@ export class ReviewCommentService { return; } - rows[updateRowIndex] = CsvStructure.formatAsCsvLine(this.finalizeComment(comment)); - setCsvFileLines(this.reviewFile, rows); + this.finalizeComment(comment, this.workspaceRoot).then((entry: CsvEntry) => { + rows[updateRowIndex] = CsvStructure.formatAsCsvLine(entry); + setCsvFileLines(this.reviewFile, rows); + }); } async deleteComment(id: string, label: string) { @@ -117,34 +120,32 @@ export class ReviewCommentService { * @param comment The comment to finalize * @return The finalized comment */ - private finalizeComment(comment: CsvEntry): CsvEntry { + private async finalizeComment(comment: CsvEntry, workspaceRoot: string): Promise { const copy = { ...comment }; - const gitDirectory = workspace.getConfiguration().get('code-review.gitDirectory') as string; - const gitRepositoryPath = path.resolve(this.workspaceRoot, gitDirectory); - try { - copy.sha = gitCommitId({ cwd: gitRepositoryPath }); + copy.revision = await revision(comment.filename, workspaceRoot); } catch (error) { - copy.sha = ''; - console.log('Not in a git repository. Leaving SHA empty', error); + copy.revision = ''; + window.showErrorMessage(`Repository not under version control as configured in the plugin's settings. + Leaving revision empty.\n\Details: ${error}`); } - const startAnker = startLineNumberFromStringDefinition(copy.lines); - const endAnker = endLineNumberFromStringDefinition(copy.lines); - copy.url = this.remoteUrl(copy.sha, copy.filename, startAnker, endAnker); + const startAnchor = startLineNumberFromStringDefinition(copy.lines); + const endAnchor = endLineNumberFromStringDefinition(copy.lines); + copy.url = this.remoteUrl(copy.revision, copy.filename, startAnchor, endAnchor); return copy; } /** * Build the remote URL - * @param sha a git SHA that's included in the URL + * @param revision a git revision that's included in the URL * @param filePath the relative file path * @param start the first line from the first selection * @param end the last line from the first selection */ - private remoteUrl(sha: string, filePath: string, start?: number, end?: number) { + private remoteUrl(revision: string, filePath: string, start?: number, end?: number) { const customUrl = workspace.getConfiguration().get('code-review.customUrl') as string; const baseUrl = workspace.getConfiguration().get('code-review.baseUrl') as string; @@ -154,15 +155,15 @@ export class ReviewCommentService { return ''; } else if (customUrl) { return customUrl - .replace('{sha}', sha) + .replace('{revision}', revision) .replace('{file}', filePathWithoutLeadingAndTrailingSlash) .replace('{start}', start ? start.toString() : '0') .replace('{end}', end ? end.toString() : '0'); } else { const baseUrlWithoutTrailingSlash = removeTrailingSlash(baseUrl); - const shaPart = sha ? `${sha}/` : ''; - const ankerPart = start && end ? `#L${start}-L${end}` : ''; - return `${baseUrlWithoutTrailingSlash}/${shaPart}${filePathWithoutLeadingAndTrailingSlash}${ankerPart}`; + const revPart = revision ? `${revision}/` : ''; + const anchorPart = start && end ? `#L${start}-L${end}` : ''; + return `${baseUrlWithoutTrailingSlash}/${revPart}${filePathWithoutLeadingAndTrailingSlash}${anchorPart}`; } } diff --git a/src/template.default.hbs b/src/template.default.hbs index 6853c6c..747c5e2 100644 --- a/src/template.default.hbs +++ b/src/template.default.hbs @@ -137,10 +137,10 @@ {{line.additional}} {{/if}} - {{#if line.sha}} - - SHA - {{line.sha}} + {{#if line.revision}} + + Revision + {{line.revision}} {{/if}} {{#if line.code}} diff --git a/src/template.default.hbs.json b/src/template.default.hbs.json index e50f523..cab5e5f 100644 --- a/src/template.default.hbs.json +++ b/src/template.default.hbs.json @@ -3,7 +3,7 @@ "group": "Best Practices", "lines": [ { - "sha": "b45d2822d6c87770af520d7e2acc49155f0b4362", + "revision": "b45d2822d6c87770af520d7e2acc49155f0b4362", "filename": "/src/environments/environment.ts", "url": "https://gitlab.com/foo/bar/baz/-/blob/b45d2822d6c87770af520d7e2acc49155f0b4362/src/src/environments/environment.ts#L5-12", "lines": "5:0-12:2", @@ -15,7 +15,7 @@ "code": "ZXhwb3J0IGNvbnN0IGVudmlyb25tZW50ID0gewogIHByb2R1Y3Rpb246IGZhbHNlCn07CgovKgogKiBGb3IgZWFzaWVyIGRlYnVnZ2luZyBpbiBkZXZlbG9wbWVudCBtb2RlLCB5b3UgY2FuIGltcG9ydCB0aGUgZm9sbG93aW5nIGZpbGUKICogdG8gaWdub3JlIHpvbmUgcmVsYXRlZCBlcnJvciBzdGFjayBmcmFtZXMgc3VjaCBhcyBgem9uZS5ydW5gLCBgem9uZURlbGVnYXRlLmludm9rZVRhc2tgLgogKgogKiBUaGlzIGltcG9ydCBzaG91bGQgYmUgY29tbWVudGVkIG91dCBpbiBwcm9kdWN0aW9uIG1vZGUgYmVjYXVzZSBpdCB3aWxsIGhhdmUgYSBuZWdhdGl2ZSBpbXBhY3Q=" }, { - "sha": "b45d2822d6c87770af520d7e2acc49155f0b4362", + "revision": "b45d2822d6c87770af520d7e2acc49155f0b4362", "filename": "/src/app/app-routing.module.ts", "url": "https://gitlab.com/foo/bar/baz/-/blob/b45d2822d6c87770af520d7e2acc49155f0b4362/src/src/app/app-routing.module.ts#L8-10", "lines": "8:5-10:2", @@ -32,7 +32,7 @@ "group": "Code-Style", "lines": [ { - "sha": "b45d2822d6c87770af520d7e2acc49155f0b4362", + "revision": "b45d2822d6c87770af520d7e2acc49155f0b4362", "filename": "/src/app/app.component.html", "url": "https://gitlab.com/foo/bar/baz/-/blob/b45d2822d6c87770af520d7e2acc49155f0b4362/src/src/app/app.component.html#L12-14", "lines": "12:12-14:16", @@ -44,7 +44,7 @@ "code": "Zm9udC1mYW1pbHk6IC1hcHBsZS1zeXN0ZW0sIEJsaW5rTWFjU3lzdGVtRm9udCwgIlNlZ29lIFVJIiwgUm9ib3RvLCBIZWx2ZXRpY2EsIEFyaWFsLCBzYW5zLXNlcmlmLCAiQXBwbGUgQ29sb3IgRW1vamkiLCAiU2Vnb2UgVUkgRW1vamkiLCAiU2Vnb2UgVUkgU3ltYm9sIjsKZm9udC1zaXplOiAxNHB4Owpjb2xvcjogIzMzMzsKYm94LXNpemluZzogYm9yZGVyLWJveDs=" }, { - "sha": "b45d2822d6c87770af520d7e2acc49155f0b4362", + "revision": "b45d2822d6c87770af520d7e2acc49155f0b4362", "filename": "/src/app/app.component.spec.ts", "url": "https://gitlab.com/foo/bar/baz/-/blob/b45d2822d6c87770af520d7e2acc49155f0b4362/src/src/app/app.component.spec.ts#L10-14", "lines": "10:8-14:19", diff --git a/src/test/stub-spawn-once.d.ts b/src/test/stub-spawn-once.d.ts new file mode 100644 index 0000000..68db742 --- /dev/null +++ b/src/test/stub-spawn-once.d.ts @@ -0,0 +1,4 @@ +declare module 'stub-spawn-once'; + +declare function stubExecOnce(command: string, stdout: string): any; +declare function stubExecOnce(command: string, exit: number, stdout: string, stderr: string): any; diff --git a/src/test/suite/vcs-provider.test.ts b/src/test/suite/vcs-provider.test.ts new file mode 100644 index 0000000..917fdca --- /dev/null +++ b/src/test/suite/vcs-provider.test.ts @@ -0,0 +1,111 @@ +import * as chai from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { stripIndents } from 'common-tags'; +import { stubExecOnce } from 'stub-spawn-once'; + +import { gitsvnRevision, svnRevision } from '../../vcs-provider'; + +chai.use(chaiAsPromised); +chai.should(); + +suite('VCS Provider', () => { + suite('svnRevision', () => { + test('should extract the revision if command succeeds', () => { + const file = 'my/file.txt'; + stubExecOnce(`svn info --show-item last-changed-revision ${file}`, '42'); + + return svnRevision('my/file.txt', 'some/workspace').should.eventually.equal(42); + }); + + test('should fail on non-zero exit code', () => { + const file = 'my/file.txt'; + stubExecOnce(`svn info --show-item last-changed-revision ${file}`, 1, '', 'my-error'); + + return svnRevision('my/file.txt', 'some/workspace').should.eventually.be.rejectedWith('my-error'); + }); + + test('should fail on unexpected command output', () => { + const file = 'my/file.txt'; + stubExecOnce(`svn info --show-item last-changed-revision ${file}`, 0, 'not-a-number', ''); + + return svnRevision('my/file.txt', 'some/workspace').should.eventually.be.rejectedWith('not-a-number'); + }); + }); + + suite('gitsvnRevision', () => { + test('should extract the revision if command succeeds', () => { + const file = 'file.md'; + const gitsvnOutput = stripIndents` + Path: ${file} + Name: ${file} + Working Copy Root Path: /home/someone/gitsvn-test-repo-checkout + URL: file:///home/someone/gitsvn-test-repo/${file} + Relative URL: ^/${file} + Repository Root: file:///home/someone/gitsvn-test-repo + Repository UUID: 1e59ce09-e6f8-40d7-8293-efd8ad2d6261 + Revision: 123 + Node Kind: file + Schedule: normal + Last Changed Author: someone + Last Changed Rev: 42 + Last Changed Date: 2021-11-15 12:26:41 +0100 (Mo, 15 Nov 2021) + Text Last Updated: 2021-11-15 12:26:28 +0100 (Mo, 15 Nov 2021) + `; + + stubExecOnce(`git svn info ${file}`, gitsvnOutput); + return gitsvnRevision(file, 'some/workspace').should.eventually.equal(42); + }); + + test('should fail when revision in output has unexpected format', () => { + const file = 'file.md'; + const gitsvnOutput = stripIndents` + Path: ${file} + Name: ${file} + Working Copy Root Path: /home/someone/gitsvn-test-repo-checkout + URL: file:///home/someone/gitsvn-test-repo/${file} + Relative URL: ^/${file} + Repository Root: file:///home/someone/gitsvn-test-repo + Repository UUID: 1e59ce09-e6f8-40d7-8293-efd8ad2d6261 + Revision: 123 + Node Kind: file + Schedule: normal + Last Changed Author: someone + Last Changed Rev: not-a-number + Last Changed Date: 2021-11-15 12:26:41 +0100 (Mo, 15 Nov 2021) + Text Last Updated: 2021-11-15 12:26:28 +0100 (Mo, 15 Nov 2021) + `; + + stubExecOnce(`git svn info ${file}`, gitsvnOutput); + return gitsvnRevision(file, 'some/workspace').should.eventually.be.rejectedWith('Could not derive'); + }); + + test('should fail when output is of unexpected format', () => { + const file = 'file.md'; + const gitsvnOutput = stripIndents` + Path: ${file} + Name: ${file} + Working Copy Root Path: /home/someone/gitsvn-test-repo-checkout + URL: file:///home/someone/gitsvn-test-repo/${file} + Relative URL: ^/${file} + Repository Root: file:///home/someone/gitsvn-test-repo + Repository UUID: 1e59ce09-e6f8-40d7-8293-efd8ad2d6261 + Revision: 123 + Node Kind: file + Schedule: normal + Last Changed Author: someone + Last Changed Date: 2021-11-15 12:26:41 +0100 (Mo, 15 Nov 2021) + Text Last Updated: 2021-11-15 12:26:28 +0100 (Mo, 15 Nov 2021) + `; + + stubExecOnce(`git svn info ${file}`, gitsvnOutput); + return gitsvnRevision(file, 'some/workspace').should.eventually.be.rejectedWith('Could not derive'); + }); + + test('should fail when git-svn exits non-zero', () => { + const file = 'file.md'; + + stubExecOnce(`git svn info ${file}`, 1, '', ''); + return gitsvnRevision(file, 'some/workspace').should.eventually.be.rejectedWith('Could not retrieve'); + }); + }); +}); diff --git a/src/test/suite/workspace-util.test.ts b/src/test/suite/workspace-util.test.ts index 2cb37bb..599887e 100644 --- a/src/test/suite/workspace-util.test.ts +++ b/src/test/suite/workspace-util.test.ts @@ -22,7 +22,7 @@ import { escapeEndOfLineForCsv, unescapeEndOfLineFromCsv, rangeFromStringDefinition, - standardizeFilename, + relativeToWorkspace, rangesFromStringDefinition, splitStringDefinition, getBackupFilename, @@ -290,7 +290,7 @@ suite('Workspace Utils', () => { suite('sortCsvEntryForLines', () => { const dummyEntry = { - sha: 'string', + revision: 'string', filename: 'string', url: 'string', title: 'string', @@ -369,11 +369,18 @@ suite('Workspace Utils', () => { }); }); - suite('standardizeFilename', () => { - test('should return a refined filename', () => { + suite('relativeToWorkspace', () => { + test('should return filename', () => { const workspaceRoot = '/path/to/my/workspace'; - const filename = '/my/file'; - const refined = standardizeFilename(workspaceRoot, workspaceRoot + filename); + const filename = 'file'; + const refined = relativeToWorkspace(workspaceRoot, path.join(workspaceRoot, filename)); + assert.strictEqual(refined, filename); + }); + + test('should return filename with relative folder', () => { + const workspaceRoot = '/path/to/my/workspace'; + const filename = 'and/my/file'; + const refined = relativeToWorkspace(workspaceRoot, path.join(workspaceRoot, filename)); assert.strictEqual(refined, filename); }); }); @@ -548,10 +555,4 @@ suite('Workspace Utils', () => { assert.ok(getBackupFilename('test-csv').endsWith('Z.bak')); }); }); - - suite('standardizeFilename', () => { - test('should remove the workspace-part from the filename', () => { - assert.strictEqual(standardizeFilename('/foo/bar', '/foo/bar/baz.txt'), '/baz.txt'); - }); - }); }); diff --git a/src/utils/workspace-util.ts b/src/utils/workspace-util.ts index fb3685b..2b0cea0 100644 --- a/src/utils/workspace-util.ts +++ b/src/utils/workspace-util.ts @@ -218,6 +218,6 @@ export const getBackupFilename = (reviewFilePath: string): string => { * @param workspaceRoot The root path of the workspace * @param filename The name of the file */ -export const standardizeFilename = (workspaceRoot: string, filename: string): string => { - return filename.replace(workspaceRoot, ''); +export const relativeToWorkspace = (workspaceRoot: string, filename: string): string => { + return path.relative(workspaceRoot, filename); }; diff --git a/src/vcs-provider.ts b/src/vcs-provider.ts new file mode 100644 index 0000000..077faa2 --- /dev/null +++ b/src/vcs-provider.ts @@ -0,0 +1,136 @@ +import * as vscode from 'vscode'; +const gitCommitId = require('git-commit-id'); +import { exec, ExecException } from 'child_process'; +import * as path from 'path'; + +export enum VcsKind { + git = 'git', + svn = 'svn', + gitsvn = 'git-svn', +} + +/** + * Gets the svn revision for the given `file`. + * + * @remark Requires SVN command-line client to be installed and in the user path. + * + * @param file Path to file to get the SVN revision for. + * + * @returns SVN revision of `file`. + * @throws Error message if SVN command-line client is not installed or SVN revision for file could not be retrieved. + */ +export async function svnRevision(file: string, workspace: string): Promise { + return new Promise((resolve, reject) => { + exec( + `svn info --show-item last-changed-revision ${file}`, + { cwd: workspace }, + (error: Error | null, stdout: string, stderr: string) => { + if (error) { + reject(`Could not retrieve SVN revision for file: ${file}. Error(s): ${stderr}`); + return; + } + + const maybeRevision = Number(stdout.trim()); + if (isNaN(maybeRevision)) { + reject(`Unexpected command output: ${stdout}`); + return; + } + resolve(maybeRevision); + }, + ); + }); +} + +/** + * Gets the svn revision for the given `file` from the underlying git-svn repository. + * + * @remark Requires git command-line client to be installed and in the user path. + * + * @param file Path to file to get the SVN revision for. + * + * @returns SVN revision of `file`. + * @throws Error message if git command-line client is not installed or SVN revision for file could not be retrieved. + */ +export async function gitsvnRevision(file: string, workspace: string): Promise { + return new Promise((resolve, reject) => { + exec(`git svn info ${file}`, { cwd: workspace }, (error, stdout: string, stderr: string) => { + if (error) { + reject(`Could not retrieve SVN revision for file: ${file}. Error(s): ${stderr}`); + return; + } + + const revRegex = /^Last Changed Rev: (\d+)\s*$/gm; + const matches = [...stdout.trim().matchAll(revRegex)]; + + if (matches.length) { + const maybeRevision = Number(matches[0][1]); + if (isNaN(maybeRevision)) { + reject(`Unexpected command output: ${matches[0][1]}`); + } else { + resolve(maybeRevision); + } + } else { + reject('Could not derive SVN revision from git-svn history.'); + } + + return; + }); + }); +} + +export async function gitRevision(_file: string, workspace: string): Promise { + const gitDirectory = (vscode.workspace.getConfiguration().get('code-review.vcs.git.directory') as string) ?? '.'; + const gitRepositoryPath = path.resolve(workspace, gitDirectory); + + try { + return Promise.resolve(gitCommitId({ cwd: gitRepositoryPath })); + } catch (error) { + return Promise.reject(error); + } +} + +/** + * + * @param file + * @returns + */ +export async function revision(file: string, workspace: string): Promise { + switch (vcsKind()) { + case VcsKind.git: { + return gitRevision(file, workspace); + } + + case VcsKind.svn: { + let revision = await svnRevision(file, workspace); + return `${revision}`; + } + + case VcsKind.gitsvn: { + let revision = await gitsvnRevision(file, workspace); + return `${revision}`; + } + + default: + // Must never happen, i.e., should be dealt with by, e.g., `vcsKind()`. + return Promise.reject(); + } +} + +/** + * + * @returns + */ +export function vcsKind(): VcsKind { + const provider = vscode.workspace.getConfiguration().get('code-review.vcs.provider'); + + switch (provider) { + case 'git': + return VcsKind.git; + case 'svn': + return VcsKind.svn; + case 'git-svn': + return VcsKind.gitsvn; + default: + throw new Error(`Unsupported VCS provider: ${provider}`); + } +} diff --git a/src/webview.html b/src/webview.html index 6f038ab..aec24c7 100644 --- a/src/webview.html +++ b/src/webview.html @@ -322,7 +322,7 @@

FILENAME

const private = document.getElementById('private').checked ? 1 : 0; const formData = { - sha: '', + revision: '', filename: '', url: '', lines: '', diff --git a/src/workspace.ts b/src/workspace.ts index 7e48bdc..603716e 100644 --- a/src/workspace.ts +++ b/src/workspace.ts @@ -28,6 +28,7 @@ import { CommentListEntry } from './comment-list-entry'; import { ImportFactory, ConflictMode } from './import-factory'; import { gutterDecorations } from './utils/decoration-utils'; import { CommentLensProvider } from './comment-lens-provider'; +import { vcsKind, VcsKind } from './vcs-provider'; const checkForCodeReviewFile = (fileName: string) => { commands.executeCommand('setContext', 'codeReview:displayCodeReviewExplorer', fs.existsSync(fileName)); @@ -80,11 +81,14 @@ export class WorkspaceContext { this.updateReviewCommentService(); this.updateCommentsProvider(); this.setupFileWatcher(); - this.watchGitSwitch(); this.watchActiveEditor(); this.watchForFileChanges(); new CommentView(this.commentsProvider); this.updateDecorations(); + + if (vcsKind() === VcsKind.git) { + this.watchGitSwitch(); + } } watchActiveEditor() { @@ -119,7 +123,7 @@ export class WorkspaceContext { * Refresh comment view on git switch */ watchGitSwitch() { - const gitDirectory = (workspace.getConfiguration().get('code-review.gitDirectory') as string) ?? '.'; + const gitDirectory = (workspace.getConfiguration().get('code-review.vcs.git.directory') as string) ?? '.'; const gitHeadPath = path.resolve(gitDirectory, '.git/HEAD'); const gitWatcher = workspace.createFileSystemWatcher(`**${gitHeadPath}`); gitWatcher.onDidChange(() => { @@ -435,19 +439,24 @@ export class WorkspaceContext { this.openSelectionRegistration, this.addNoteRegistration, this.deleteNoteRegistration, - this.filterByCommitEnableRegistration, - this.filterByCommitDisableRegistration, this.filterByFilenameEnableRegistration, this.filterByFilenameDisableRegistration, this.exportAsHtmlWithDefaultTemplateRegistration, this.exportAsHtmlWithHandlebarsTemplateRegistration, - this.exportAsGitLabImportableCsvRegistration, - this.exportAsGitHubImportableCsvRegistration, this.exportAsJiraImportableCsvRegistration, this.exportAsJsonRegistration, this.importFromJsonRegistration, this.commentCodeLensProviderregistration, ); + + if (vcsKind() === VcsKind.git) { + this.context.subscriptions.push( + this.filterByCommitEnableRegistration, + this.filterByCommitDisableRegistration, + this.exportAsGitLabImportableCsvRegistration, + this.exportAsGitHubImportableCsvRegistration, + ); + } } /** @@ -457,18 +466,24 @@ export class WorkspaceContext { this.openSelectionRegistration.dispose(); this.addNoteRegistration.dispose(); this.deleteNoteRegistration.dispose(); - this.filterByCommitEnableRegistration.dispose(); - this.filterByCommitDisableRegistration.dispose(); + this.filterByFilenameEnableRegistration.dispose(); this.filterByFilenameDisableRegistration.dispose(); this.exportAsHtmlWithDefaultTemplateRegistration.dispose(); this.exportAsHtmlWithHandlebarsTemplateRegistration.dispose(); - this.exportAsGitLabImportableCsvRegistration.dispose(); - this.exportAsGitHubImportableCsvRegistration.dispose(); + this.exportAsJiraImportableCsvRegistration.dispose(); this.exportAsJsonRegistration.dispose(); this.importFromJsonRegistration.dispose(); this.commentCodeLensProviderregistration.dispose(); + + if (vcsKind() === VcsKind.git) { + this.filterByCommitEnableRegistration.dispose(); + this.filterByCommitDisableRegistration.dispose(); + this.exportAsGitLabImportableCsvRegistration.dispose(); + this.exportAsGitHubImportableCsvRegistration.dispose(); + } + this.updateSubscriptions(); } diff --git a/tsconfig.json b/tsconfig.json index a76a167..0d169da 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ "module": "commonjs", "target": "es6", "outDir": "out", - "lib": ["es6"], + "lib": ["es6", "es2020.string"], "sourceMap": true, "rootDir": "src", "strict": true /* enable all strict type-checking options */,