├── .prettierignore ├── .npmrc ├── index.js ├── .gitignore ├── .editorconfig ├── .github └── workflows │ ├── bb.yml │ └── main.yml ├── tsconfig.json ├── license ├── test ├── processor.js └── index.js ├── package.json ├── lib └── index.js └── readme.md /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | *.md 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-scripts=true 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export {default} from './lib/index.js' 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | node_modules/ 3 | .DS_Store 4 | *.d.ts 5 | *.log 6 | yarn.lock 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.github/workflows/bb.yml: -------------------------------------------------------------------------------- 1 | name: bb 2 | on: 3 | issues: 4 | types: [opened, reopened, edited, closed, labeled, unlabeled] 5 | pull_request_target: 6 | types: [opened, reopened, edited, closed, labeled, unlabeled] 7 | jobs: 8 | main: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: unifiedjs/beep-boop-beta@main 12 | with: 13 | repo-token: ${{secrets.GITHUB_TOKEN}} 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true, 4 | "customConditions": ["development"], 5 | "declaration": true, 6 | "emitDeclarationOnly": true, 7 | "exactOptionalPropertyTypes": true, 8 | "lib": ["es2022"], 9 | "module": "node16", 10 | "strict": true, 11 | "target": "es2022" 12 | }, 13 | "exclude": ["coverage/", "node_modules/"], 14 | "include": ["**/*.js"] 15 | } 16 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | on: 3 | - pull_request 4 | - push 5 | jobs: 6 | main: 7 | name: ${{matrix.node}} 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: actions/setup-node@v3 12 | with: 13 | node-version: ${{matrix.node}} 14 | - run: npm install 15 | - run: npm test 16 | - uses: codecov/codecov-action@v3 17 | strategy: 18 | matrix: 19 | node: 20 | - lts/gallium 21 | - node 22 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2016 Titus Wormer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /test/processor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('nlcst').Root} Root 3 | */ 4 | 5 | import {toString} from 'nlcst-to-string' 6 | import {ParseEnglish} from 'parse-english' 7 | import {unified} from 'unified' 8 | import unifiedDiff from 'unified-diff' 9 | import {visit} from 'unist-util-visit' 10 | 11 | // To do: use `retext-english`, `retext-stringify` when they are released. 12 | // import retextEnglish from 'retext-english' 13 | // import retextStringify from 'retext-stringify' 14 | 15 | export const processor = unified() 16 | .use( 17 | /** @type {import('unified').Plugin<[], string, Root>} */ 18 | // @ts-expect-error: TS doesn’t understand `this`. 19 | function () { 20 | this.parser = parser 21 | /** @type {import('unified').Parser} */ 22 | function parser(value) { 23 | const parser = new ParseEnglish() 24 | const node = parser.parse(value) 25 | return node 26 | } 27 | } 28 | ) 29 | .use( 30 | /** @type {import('unified').Plugin<[], Root, string>} */ 31 | // @ts-expect-error: TS doesn’t understand `this`. 32 | function () { 33 | // @ts-expect-error: TS doesn’t understand `this`. 34 | this.compiler = compiler 35 | /** @type {import('unified').Compiler} */ 36 | function compiler(node) { 37 | return toString(node) 38 | } 39 | } 40 | ) 41 | .use(function () { 42 | /** 43 | * @param {Root} tree 44 | * Tree. 45 | * @returns {undefined} 46 | * Nothing. 47 | */ 48 | return function (tree, file) { 49 | visit(tree, 'WordNode', function (node) { 50 | if (/lorem/i.test(toString(node))) { 51 | file.message('No lorem!', node) 52 | } 53 | }) 54 | } 55 | }) 56 | .use(unifiedDiff) 57 | .freeze() 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "unified-diff", 3 | "version": "5.0.0", 4 | "description": "unified plugin to ignore unrelated messages", 5 | "license": "MIT", 6 | "keywords": [ 7 | "diff", 8 | "difference", 9 | "plugin", 10 | "pr", 11 | "rehype", 12 | "remark", 13 | "retext", 14 | "travis", 15 | "unified", 16 | "unified-plugin" 17 | ], 18 | "repository": "unifiedjs/unified-diff", 19 | "bugs": "https://github.com/unifiedjs/unified-diff/issues", 20 | "funding": { 21 | "type": "opencollective", 22 | "url": "https://opencollective.com/unified" 23 | }, 24 | "author": "Titus Wormer (https://wooorm.com)", 25 | "contributors": [ 26 | "Titus Wormer (https://wooorm.com)" 27 | ], 28 | "sideEffects": false, 29 | "type": "module", 30 | "exports": "./index.js", 31 | "files": [ 32 | "lib/", 33 | "index.d.ts", 34 | "index.js" 35 | ], 36 | "dependencies": { 37 | "devlop": "^1.0.0", 38 | "git-diff-tree": "^1.0.0", 39 | "vfile": "^6.0.0", 40 | "vfile-find-up": "^7.0.0" 41 | }, 42 | "devDependencies": { 43 | "@types/nlcst": "^2.0.0", 44 | "@types/node": "^20.0.0", 45 | "c8": "^8.0.0", 46 | "nlcst-to-string": "^4.0.0", 47 | "parse-english": "^7.0.0", 48 | "prettier": "^3.0.0", 49 | "remark-cli": "^11.0.0", 50 | "remark-preset-wooorm": "^9.0.0", 51 | "to-vfile": "^8.0.0", 52 | "type-coverage": "^2.0.0", 53 | "typescript": "^5.0.0", 54 | "unified": "^11.0.0", 55 | "unist-util-visit": "^5.0.0", 56 | "xo": "^0.56.0" 57 | }, 58 | "scripts": { 59 | "build": "tsc --build --clean && tsc --build && type-coverage", 60 | "format": "remark . --frail --output --quiet && prettier . --log-level warn --write && xo --fix", 61 | "prepack": "npm run build && npm run format", 62 | "test": "npm run build && npm run format && npm run test-coverage", 63 | "test-api": "node --conditions development test/index.js", 64 | "test-coverage": "c8 --100 --check-coverage --reporter lcov npm run test-api" 65 | }, 66 | "prettier": { 67 | "bracketSpacing": false, 68 | "singleQuote": true, 69 | "semi": false, 70 | "tabWidth": 2, 71 | "trailingComma": "none", 72 | "useTabs": false 73 | }, 74 | "remarkConfig": { 75 | "plugins": [ 76 | "remark-preset-wooorm" 77 | ] 78 | }, 79 | "typeCoverage": { 80 | "atLeast": 100, 81 | "detail": true, 82 | "ignoreCatch": true, 83 | "strict": true 84 | }, 85 | "xo": { 86 | "prettier": true 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('vfile').VFile} VFile 3 | */ 4 | 5 | /** 6 | * @typedef {'patch' | 'stats' | 'raw'} DiffType 7 | * 8 | * @typedef PatchData 9 | * Data of a patch. 10 | * @property {string} aPath 11 | * From. 12 | * @property {string} bPath 13 | * To. 14 | * @property {Array} lines 15 | * Changes. 16 | * @property {boolean} isBlacklisted 17 | * No idea. 18 | * 19 | * @typedef {[originalRev: string, rev: string]} Range 20 | * Range of two refs (such as commits). 21 | * 22 | * @typedef {[from: number, to: number]} Diff 23 | * Diff range, two line numbers between which there’s been a change. 24 | */ 25 | 26 | import path from 'node:path' 27 | import process from 'node:process' 28 | import {ok as assert} from 'devlop' 29 | // @ts-expect-error: not typed. 30 | import gitDiffTree from 'git-diff-tree' 31 | import {findUp} from 'vfile-find-up' 32 | 33 | // This is mostly to enable the tests to mimick different CIs. 34 | // Normally, a Node process exits between CI runs. 35 | /** @type {string} */ 36 | let previousRange 37 | 38 | /** 39 | * Ignore unrelated messages in GitHub Actions and Travis. 40 | * 41 | * There are no options. 42 | * If there’s a `TRAVIS_COMMIT_RANGE`, `GITHUB_BASE_REF` and `GITHUB_HEAD_REF`, 43 | * or `GITHUB_SHA` environment variable, then this plugin runs, otherwise it 44 | * does nothing. 45 | * 46 | * ###### To do 47 | * 48 | * * [ ] Add support for other CIs (ping if you want to work on this) 49 | * * [ ] Add non-CI support (I’m not sure how though) 50 | * 51 | * PRs welcome! 52 | * 53 | * @returns 54 | * Transform. 55 | */ 56 | export default function unifiedDiff() { 57 | /** @type {Map} */ 58 | let cache = new Map() 59 | 60 | /** 61 | * @param {unknown} _ 62 | * Tree. 63 | * @param {VFile} file 64 | * File. 65 | * @returns {Promise} 66 | * Promise to nothing. 67 | */ 68 | return async function (_, file) { 69 | const base = file.dirname 70 | /** @type {string | undefined} */ 71 | let commitRange 72 | /** @type {Range | undefined} */ 73 | let range 74 | 75 | // Looks like Travis. 76 | if (process.env.TRAVIS_COMMIT_RANGE) { 77 | commitRange = process.env.TRAVIS_COMMIT_RANGE 78 | // Cast because we check `length` later. 79 | range = /** @type {Range} */ (commitRange.split(/\.{3}/)) 80 | } 81 | // Looks like GH Actions. 82 | else if (process.env.GITHUB_SHA) { 83 | const sha = process.env.GITHUB_SHA 84 | const base = process.env.GITHUB_BASE_REF 85 | const head = process.env.GITHUB_HEAD_REF 86 | 87 | if (base && head) { 88 | const baseTail = base.split('/').pop() 89 | const headTail = head.split('/').pop() 90 | assert(baseTail) 91 | assert(headTail) 92 | range = [baseTail, headTail] 93 | } else { 94 | range = [sha + '^1', sha] 95 | } 96 | 97 | commitRange = range.join('...') 98 | } 99 | 100 | if ( 101 | !base || 102 | !commitRange || 103 | !range || 104 | !file.dirname || 105 | range.length !== 2 106 | ) { 107 | return 108 | } 109 | 110 | // Reset cache. 111 | if (previousRange !== commitRange) { 112 | cache = new Map() 113 | previousRange = commitRange 114 | } 115 | 116 | let gitFolder = cache.get(base) 117 | 118 | if (!gitFolder) { 119 | const gitFolderFile = await findUp('.git', file.dirname) 120 | 121 | /* c8 ignore next 3 -- not testable in a Git repo… */ 122 | if (!gitFolderFile || !gitFolderFile.dirname) { 123 | throw new Error('Not in a git repository') 124 | } 125 | 126 | cache.set(base, gitFolderFile.dirname) 127 | gitFolder = gitFolderFile.dirname 128 | } 129 | 130 | const diffs = await checkGit(gitFolder, range) 131 | const ranges = diffs.get(path.resolve(file.cwd, file.path)) 132 | 133 | // Unchanged file: drop all messages. 134 | if (!ranges || ranges.length === 0) { 135 | file.messages.length = 0 136 | return 137 | } 138 | 139 | file.messages = file.messages.filter(function (message) { 140 | return ranges.some(function (range) { 141 | return ( 142 | message.line && message.line >= range[0] && message.line <= range[1] 143 | ) 144 | }) 145 | }) 146 | } 147 | } 148 | 149 | /** 150 | * Check a folder. 151 | * 152 | * @param {string} root 153 | * Folder. 154 | * @param {Range} range 155 | * Range. 156 | * @returns {Promise>>} 157 | * Nothing. 158 | */ 159 | function checkGit(root, range) { 160 | return new Promise(function (resolve, reject) { 161 | /** @type {Map>} */ 162 | const diffs = new Map() 163 | const [originalRev, rev] = range 164 | 165 | gitDiffTree(path.join(root, '.git'), {originalRev, rev}) 166 | .on('error', reject) 167 | .on( 168 | 'data', 169 | /** 170 | * @param {DiffType} type 171 | * Data type. 172 | * @param {PatchData} data 173 | * Data. 174 | * @returns {undefined} 175 | * Nothing. 176 | */ 177 | function (type, data) { 178 | if (type !== 'patch') return 179 | 180 | const lines = data.lines 181 | const re = /^@@ -(\d+),?(\d+)? \+(\d+),?(\d+)? @@/ 182 | const match = lines[0].match(re) 183 | 184 | /* c8 ignore next -- should not happen, maybe if Git returns weird diffs? */ 185 | if (!match) return 186 | 187 | /** @type {Array} */ 188 | const ranges = [] 189 | const start = Number.parseInt(match[3], 10) - 1 190 | let index = 0 191 | /** @type {number | undefined} */ 192 | let position 193 | 194 | while (++index < lines.length) { 195 | const line = lines[index] 196 | 197 | if (line.charAt(0) === '+') { 198 | const no = start + index 199 | 200 | if (position === undefined) { 201 | position = ranges.length 202 | ranges.push([no, no]) 203 | } else { 204 | ranges[position][1] = no 205 | } 206 | } else { 207 | position = undefined 208 | } 209 | } 210 | 211 | const fp = path.resolve(root, data.bPath) 212 | 213 | let list = diffs.get(fp) 214 | 215 | if (!list) { 216 | list = [] 217 | diffs.set(fp, list) 218 | } 219 | 220 | list.push(...ranges) 221 | } 222 | ) 223 | .on('end', function () { 224 | resolve(diffs) 225 | }) 226 | }) 227 | } 228 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # unified-diff 2 | 3 | [![Build][build-badge]][build] 4 | [![Coverage][coverage-badge]][coverage] 5 | [![Downloads][downloads-badge]][downloads] 6 | [![Sponsors][sponsors-badge]][collective] 7 | [![Backers][backers-badge]][collective] 8 | [![Chat][chat-badge]][chat] 9 | 10 | **[unified][]** plugin to ignore unrelated messages in GitHub Actions and 11 | Travis. 12 | 13 | ## Contents 14 | 15 | * [What is this?](#what-is-this) 16 | * [When should I use this?](#when-should-i-use-this) 17 | * [Install](#install) 18 | * [Use](#use) 19 | * [API](#api) 20 | * [`unified().use(unifiedDiff)`](#unifieduseunifieddiff) 21 | * [Types](#types) 22 | * [Compatibility](#compatibility) 23 | * [Contribute](#contribute) 24 | * [License](#license) 25 | 26 | ## What is this? 27 | 28 | This package is a [unified][] plugin to ignore unrelated lint messages in a CI. 29 | 30 | **unified** is a project that transforms content with abstract syntax trees 31 | (ASTs). 32 | **vfile** is the virtual file interface used in unified which manages messages. 33 | This is a unified plugin that filters messages on the vfile. 34 | 35 | ## When should I use this? 36 | 37 | You can use this plugin when you are dealing with a large, existing project. 38 | 39 | Using tools that check whether things follow a style guide is typically very 40 | useful. 41 | However, it can be hard to start using something in a large existing project. 42 | This plugin helps, because it ignores messages that occur in lines that are not 43 | touched by a PR in a CI. 44 | When this plugin is used outside of a supported CIs, it doesn’t do anything. 45 | 46 | ## Install 47 | 48 | This package is [ESM only][esm]. 49 | In Node.js (version 16+), install with [npm][]: 50 | 51 | ```sh 52 | npm install unified-diff 53 | ``` 54 | 55 | In Deno with [`esm.sh`][esmsh]: 56 | 57 | ```js 58 | import unifiedDiff from 'https://esm.sh/unified-diff@5' 59 | ``` 60 | 61 | In browsers with [`esm.sh`][esmsh]: 62 | 63 | ```html 64 | 67 | ``` 68 | 69 | ## Use 70 | 71 | Say our document `example.md` contains: 72 | 73 | ```markdown 74 | This is an an example. 75 | ``` 76 | 77 | > 👉 **Note**: `an an` is a typo. 78 | 79 | …and our module `example.js` looks as follows: 80 | 81 | ```js 82 | import process from 'node:process' 83 | import remarkParse from 'remark-parse' 84 | import remarkRetext from 'remark-retext' 85 | import remarkStringify from 'remark-stringify' 86 | import retextEnglish from 'retext-english' 87 | import retextIndefiniteArticle from 'retext-indefinite-article' 88 | import retextRepeatedWords from 'retext-repeated-words' 89 | import {read} from 'to-vfile' 90 | import {unified} from 'unified' 91 | import unifiedDiff from 'unified-diff' 92 | import {reporter} from 'vfile-reporter' 93 | 94 | const file = await unified() 95 | .use(remarkParse) 96 | .use( 97 | remarkRetext, 98 | unified() 99 | .use(retextEnglish) 100 | .use(retextRepeatedWords) 101 | .use(retextIndefiniteArticle) 102 | ) 103 | .use(remarkStringify) 104 | .use(unifiedDiff) 105 | .process(await read('example.md')) 106 | 107 | console.error(reporter(file)) 108 | process.exitCode = file.messages.length > 0 ? 1 : 0 109 | ``` 110 | 111 | …and our Travis configuration `.travis.yml` contains: 112 | 113 | ```yml 114 | # … 115 | script: 116 | - npm test 117 | - node example.js 118 | # … 119 | ``` 120 | 121 | > 👉 **Note**: an equivalent GH Actions workflow file is also supported. 122 | 123 | Then, say someone creates a PR which adds the following diff: 124 | 125 | ```diff 126 | diff --git a/example.md b/example.md 127 | index 360b225..5a96b86 100644 128 | --- a/example.md 129 | +++ b/example.md 130 | @@ -1 +1,3 @@ 131 | This is an an example. 132 | + 133 | +Some more more text. A error. 134 | ``` 135 | 136 | > 👉 **Note**: `more more` and `A` before `error` are typos. 137 | 138 | When run in CI, we’ll see the following printed on **stderr**(4). 139 | 140 | ```txt 141 | example.md 142 | 3:6-3:15 warning Expected `more` once, not twice retext-repeated-words retext-repeated-words 143 | 3:22-3:23 warning Use `An` before `error`, not `A` retext-indefinite-article retext-indefinite-article 144 | 145 | ⚠ 2 warnings 146 | ``` 147 | 148 | > 👉 **Note**: `an an` on L1 is not included because it’s unrelated to this PR. 149 | 150 | The build exits with `1` as there are messages, thus failing CI. 151 | The user sees this and amends the PR to the following: 152 | 153 | ```diff 154 | diff --git a/example.md b/example.md 155 | index 360b225..5a96b86 100644 156 | --- a/example.md 157 | +++ b/example.md 158 | @@ -1 +1,3 @@ 159 | This is an an example. 160 | + 161 | +Some more text. An error. 162 | ``` 163 | 164 | This time our lint task exits successfully, even though L1 would normally emit 165 | an error, but it’s unrelated to the PR. 166 | 167 | ## API 168 | 169 | This package exports no identifiers. 170 | The default export is [`unifiedDiff`][api-unified-diff]. 171 | 172 | ### `unified().use(unifiedDiff)` 173 | 174 | Ignore unrelated messages in GitHub Actions and Travis. 175 | 176 | There are no options. 177 | If there’s a `TRAVIS_COMMIT_RANGE`, `GITHUB_BASE_REF` and `GITHUB_HEAD_REF`, or 178 | `GITHUB_SHA` environment variable, then this plugin runs, otherwise it does 179 | nothing. 180 | 181 | ###### To do 182 | 183 | * [ ] Add support for other CIs (ping if you want to work on this) 184 | * [ ] Add non-CI support (I’m not sure how though) 185 | 186 | PRs welcome! 187 | 188 | ###### Returns 189 | 190 | Transform ([`Transformer`][transformer]). 191 | 192 | ## Types 193 | 194 | This package is fully typed with [TypeScript][]. 195 | It exports no additional types. 196 | 197 | ## Compatibility 198 | 199 | Projects maintained by the unified collective are compatible with maintained 200 | versions of Node.js. 201 | 202 | When we cut a new major release, we drop support for unmaintained versions of 203 | Node. 204 | This means we try to keep the current release line, `unified-diff@^5`, 205 | compatible with Node.js 16. 206 | 207 | ## Contribute 208 | 209 | See [`contributing.md`][contributing] in [`unifiedjs/.github`][health] for ways 210 | to get started. 211 | See [`support.md`][support] for ways to get help. 212 | 213 | This project has a [code of conduct][coc]. 214 | By interacting with this repository, organization, or community you agree to 215 | abide by its terms. 216 | 217 | ## License 218 | 219 | [MIT][license] © [Titus Wormer][author] 220 | 221 | 222 | 223 | [build-badge]: https://github.com/unifiedjs/unified-diff/workflows/main/badge.svg 224 | 225 | [build]: https://github.com/unifiedjs/unified-diff/actions 226 | 227 | [coverage-badge]: https://img.shields.io/codecov/c/github/unifiedjs/unified-diff.svg 228 | 229 | [coverage]: https://codecov.io/github/unifiedjs/unified-diff 230 | 231 | [downloads-badge]: https://img.shields.io/npm/dm/unified-diff.svg 232 | 233 | [downloads]: https://www.npmjs.com/package/unified-diff 234 | 235 | [sponsors-badge]: https://opencollective.com/unified/sponsors/badge.svg 236 | 237 | [backers-badge]: https://opencollective.com/unified/backers/badge.svg 238 | 239 | [collective]: https://opencollective.com/unified 240 | 241 | [chat-badge]: https://img.shields.io/badge/chat-discussions-success.svg 242 | 243 | [chat]: https://github.com/unifiedjs/unified/discussions 244 | 245 | [npm]: https://docs.npmjs.com/cli/install 246 | 247 | [esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c 248 | 249 | [esmsh]: https://esm.sh 250 | 251 | [typescript]: https://www.typescriptlang.org 252 | 253 | [health]: https://github.com/unifiedjs/.github 254 | 255 | [contributing]: https://github.com/unifiedjs/.github/blob/main/contributing.md 256 | 257 | [support]: https://github.com/unifiedjs/.github/blob/main/support.md 258 | 259 | [coc]: https://github.com/unifiedjs/.github/blob/main/code-of-conduct.md 260 | 261 | [license]: license 262 | 263 | [author]: https://wooorm.com 264 | 265 | [unified]: https://github.com/unifiedjs/unified 266 | 267 | [transformer]: https://github.com/unifiedjs/unified#transformer 268 | 269 | [api-unified-diff]: #unifieduseunifieddiff 270 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert/strict' 2 | import childProcess from 'node:child_process' 3 | import fsDefault, {promises as fs} from 'node:fs' 4 | import path from 'node:path' 5 | import process from 'node:process' 6 | import test from 'node:test' 7 | import {promisify} from 'node:util' 8 | import {write} from 'to-vfile' 9 | import {VFile} from 'vfile' 10 | import {processor} from './processor.js' 11 | 12 | const exec = promisify(childProcess.exec) 13 | 14 | const range = process.env.TRAVIS_COMMIT_RANGE 15 | const sha = process.env.GITHUB_SHA 16 | const base = process.env.GITHUB_BASE_REF 17 | const head = process.env.GITHUB_HEAD_REF 18 | 19 | // Remove potential variables that we’re testing on CIs. 20 | delete process.env.TRAVIS_COMMIT_RANGE 21 | delete process.env.GITHUB_SHA 22 | delete process.env.GITHUB_BASE_REF 23 | delete process.env.GITHUB_HEAD_REF 24 | 25 | const current = process.cwd() 26 | 27 | process.chdir(path.join(current, 'test')) 28 | 29 | process.on('exit', function () { 30 | // Has to be sync. 31 | fsDefault.rmSync(new URL('.git', import.meta.url), { 32 | force: true, 33 | recursive: true 34 | }) 35 | fsDefault.rmSync(new URL('example.txt', import.meta.url), {force: true}) 36 | process.env.TRAVIS_COMMIT_RANGE = range 37 | process.env.GITHUB_SHA = sha 38 | process.env.GITHUB_BASE_REF = base 39 | process.env.GITHUB_HEAD_REF = head 40 | process.chdir(path.join(current)) 41 | }) 42 | 43 | test('unifiedDiff', async function (t) { 44 | await t.test('should expose the public api', async function () { 45 | assert.deepEqual(Object.keys(await import('unified-diff')).sort(), [ 46 | 'default' 47 | ]) 48 | }) 49 | }) 50 | 51 | test('unifiedDiff (travis)', async function (t) { 52 | const stepOne = [ 53 | 'Lorem ipsum dolor sit amet.', 54 | '', 55 | 'Lorem ipsum. Dolor sit amet.', 56 | '' 57 | ].join('\n') 58 | const stepTwo = stepOne + '\nLorem ipsum.' 59 | const stepThree = 'Lorem.\n' + stepOne + '\nLorem.' 60 | const other = 'Lorem ipsum.' 61 | 62 | await exec('git init') 63 | 64 | // Set up. 65 | try { 66 | await exec('git config --global user.email') 67 | } catch { 68 | await exec('git config --global user.email info@example.com') 69 | await exec('git config --global user.name Ex Ample') 70 | } 71 | 72 | // Add initial file. 73 | const fileOne = new VFile({path: 'example.txt', value: stepOne}) 74 | 75 | await processor().process(fileOne) 76 | 77 | await t.test('should show messages if not on disk', async function () { 78 | assert.deepEqual(fileOne.messages.map(String), [ 79 | 'example.txt:1:1-1:6: No lorem!', 80 | 'example.txt:3:1-3:6: No lorem!' 81 | ]) 82 | }) 83 | 84 | await write(fileOne) 85 | 86 | await exec('git add example.txt') 87 | await exec('git commit -m one') 88 | const resultInitial = await exec('git rev-parse HEAD') 89 | const initial = resultInitial.stdout.trim() 90 | 91 | // Change files. 92 | await write({path: 'example.txt', value: stepTwo}) 93 | await exec('git add example.txt') 94 | await exec('git commit -m two') 95 | const resultFinal = await exec('git rev-parse HEAD') 96 | const final = resultFinal.stdout.trim() 97 | process.env.TRAVIS_COMMIT_RANGE = [initial, final].join('...') 98 | 99 | const fileTwo = await processor().process( 100 | new VFile({path: 'example.txt', value: stepTwo}) 101 | ) 102 | 103 | await t.test( 104 | 'should show only messages for changed lines', 105 | async function () { 106 | assert.deepEqual(fileTwo.messages.map(String), [ 107 | 'example.txt:5:1-5:6: No lorem!' 108 | ]) 109 | } 110 | ) 111 | 112 | // Again! 113 | const fileAgain = await processor().process( 114 | new VFile({path: 'example.txt', value: stepTwo}) 115 | ) 116 | 117 | await t.test( 118 | 'should not recheck (coverage for optimisations)', 119 | async function () { 120 | assert.deepEqual(fileAgain.messages.map(String), [ 121 | 'example.txt:5:1-5:6: No lorem!' 122 | ]) 123 | } 124 | ) 125 | 126 | // Unstaged files. 127 | const fileMissing = await processor().process( 128 | new VFile({path: 'missing.txt', value: other}) 129 | ) 130 | 131 | await t.test('should ignore unstaged files', async function () { 132 | assert.deepEqual(fileMissing.messages.map(String), []) 133 | }) 134 | 135 | // New file. 136 | await write({path: 'example.txt', value: stepThree}) 137 | await write({path: 'new.txt', value: other}) 138 | await exec('git add example.txt new.txt') 139 | await exec('git commit -m three') 140 | const resultNew = await exec('git rev-parse HEAD') 141 | 142 | process.env.TRAVIS_COMMIT_RANGE = initial + '...' + resultNew.stdout.trim() 143 | 144 | const fileNew = await processor().process( 145 | new VFile({path: 'example.txt', value: stepThree}) 146 | ) 147 | 148 | await t.test('should deal with multiple patches', async function () { 149 | assert.deepEqual(fileNew.messages.map(String), [ 150 | 'example.txt:1:1-1:6: No lorem!', 151 | 'example.txt:6:1-6:6: No lorem!' 152 | ]) 153 | }) 154 | 155 | const fileNewTwo = await processor().process( 156 | new VFile({path: 'new.txt', value: other}) 157 | ) 158 | 159 | await t.test('should deal with new files', async function () { 160 | assert.deepEqual(fileNewTwo.messages.map(String), [ 161 | 'new.txt:1:1-1:6: No lorem!' 162 | ]) 163 | }) 164 | 165 | delete process.env.TRAVIS_COMMIT_RANGE 166 | 167 | await fs.rm(new URL('.git', import.meta.url), { 168 | recursive: true 169 | }) 170 | await fs.rm(new URL('example.txt', import.meta.url)) 171 | await fs.rm(new URL('new.txt', import.meta.url)) 172 | }) 173 | 174 | test('unifiedDiff (GitHub Actions)', async function (t) { 175 | const stepOne = [ 176 | 'Lorem ipsum dolor sit amet.', 177 | '', 178 | 'Lorem ipsum. Dolor sit amet.', 179 | '' 180 | ].join('\n') 181 | const stepTwo = stepOne + '\nLorem ipsum.\n' 182 | const stepThree = 'Lorem.\n\n' + stepOne + '\nAlpha bravo.\n' 183 | const stepFour = stepThree + '\nIpsum lorem.\n' 184 | 185 | await exec('git init') 186 | // Add initial file. 187 | await write({path: 'example.txt', value: stepOne}) 188 | await exec('git add example.txt') 189 | await exec('git commit -m one') 190 | 191 | // Change file. 192 | await write({path: 'example.txt', value: stepTwo}) 193 | await exec('git add example.txt') 194 | await exec('git commit -m two') 195 | const resultInitial = await exec('git rev-parse HEAD') 196 | 197 | process.env.GITHUB_SHA = resultInitial.stdout.trim() 198 | 199 | const fileInitial = await processor().process( 200 | new VFile({path: 'example.txt', value: stepTwo}) 201 | ) 202 | 203 | await t.test('should show only messages for this commit', async function () { 204 | assert.deepEqual(fileInitial.messages.map(String), [ 205 | 'example.txt:5:1-5:6: No lorem!' 206 | ]) 207 | }) 208 | 209 | const resultCurrent = await exec('git branch --show-current') 210 | const main = resultCurrent.stdout.trim() 211 | 212 | await exec('git checkout -b other-branch') 213 | 214 | // Change file. 215 | await write({path: 'example.txt', value: stepThree}) 216 | await exec('git add example.txt') 217 | await exec('git commit -m three') 218 | await write({path: 'example.txt', value: stepFour}) 219 | await exec('git add example.txt') 220 | await exec('git commit -m four') 221 | const final = await exec('git rev-parse HEAD') 222 | 223 | process.env.GITHUB_SHA = final.stdout.trim() 224 | process.env.GITHUB_BASE_REF = 'refs/heads/' + main 225 | process.env.GITHUB_HEAD_REF = 'refs/heads/other-branch' 226 | 227 | const fileFour = await processor().process( 228 | new VFile({path: 'example.txt', value: stepFour}) 229 | ) 230 | 231 | await t.test('should deal with PRs', async function () { 232 | assert.deepEqual(fileFour.messages.map(String), [ 233 | 'example.txt:1:1-1:6: No lorem!', 234 | 'example.txt:9:7-9:12: No lorem!' 235 | ]) 236 | }) 237 | 238 | delete process.env.GITHUB_SHA 239 | delete process.env.GITHUB_BASE_REF 240 | delete process.env.GITHUB_HEAD_REF 241 | 242 | await fs.rm(new URL('.git', import.meta.url), { 243 | recursive: true 244 | }) 245 | await fs.rm(new URL('example.txt', import.meta.url)) 246 | }) 247 | --------------------------------------------------------------------------------