├── .gitignore ├── img └── example.jpeg ├── src ├── utils │ ├── splitPaths.js │ ├── compareMarkdown.js │ ├── fetchDiffFromFile.js │ ├── getRulesPath.js │ ├── postComment.js │ ├── checkDiff.js │ ├── fetchComments.js │ ├── parsePaths.js │ ├── shouldMessageBePosted.js │ └── getAutoCommentData.js └── index.js ├── eslint.config.mjs ├── package.json ├── action.yml ├── .github └── workflows │ └── main.yml ├── README.md └── dist └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /img/example.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pshergie/prator/HEAD/img/example.jpeg -------------------------------------------------------------------------------- /src/utils/splitPaths.js: -------------------------------------------------------------------------------- 1 | const splitPaths = paths => paths ? paths.split(",").map((p) => p.trim()) : undefined; 2 | 3 | export default splitPaths; 4 | -------------------------------------------------------------------------------- /src/utils/compareMarkdown.js: -------------------------------------------------------------------------------- 1 | const compareMarkdown = (comment, message) => { 2 | return comment.replaceAll("- [x]", "- [ ]").includes(message); 3 | }; 4 | 5 | export default compareMarkdown; 6 | -------------------------------------------------------------------------------- /src/utils/fetchDiffFromFile.js: -------------------------------------------------------------------------------- 1 | const fetchDiffFromFile = (type) => fs.readFileSync(`${artifactPath}pr_files_diff_${type}.txt`, "utf8").split('\n').filter(Boolean); 2 | 3 | export default fetchDiffFromFile; 4 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import pluginJs from "@eslint/js"; 3 | 4 | 5 | export default [ 6 | {files: ["**/*.js"], languageOptions: {sourceType: "script"}}, 7 | {languageOptions: { globals: globals.browser }}, 8 | pluginJs.configs.recommended, 9 | ]; -------------------------------------------------------------------------------- /src/utils/getRulesPath.js: -------------------------------------------------------------------------------- 1 | const getRulesPath = (core) => { 2 | const rulesPath = core.getInput("rules-path"); 3 | 4 | if (!rulesPath) { 5 | throw new Error("The rulesPath variable is empty, please provide it."); 6 | } 7 | 8 | return rulesPath; 9 | }; 10 | 11 | export default getRulesPath; 12 | -------------------------------------------------------------------------------- /src/utils/postComment.js: -------------------------------------------------------------------------------- 1 | const postComment = async ( 2 | prependMsg, 3 | messagesToPost, 4 | pullNumber, 5 | context, 6 | octokit, 7 | ) => { 8 | const message = messagesToPost.join('\n\n'); 9 | const body = prependMsg ? `${prependMsg}\n\n` + message : message; 10 | 11 | await octokit.rest.issues.createComment({ 12 | ...context.repo, 13 | issue_number: pullNumber, 14 | body, 15 | }); 16 | }; 17 | 18 | export default postComment; 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gh-actions-test", 3 | "version": "2.0", 4 | "description": "", 5 | "author": "pshergie", 6 | "license": "ISC", 7 | "scripts": { 8 | "compile": "ncc build src/index.js" 9 | }, 10 | "dependencies": { 11 | "@actions/core": "^1.10.1", 12 | "@actions/github": "^6.0.0", 13 | "js-yaml": "^4.1.0", 14 | "minimatch": "^9.0.4" 15 | }, 16 | "devDependencies": { 17 | "@eslint/js": "^9.4.0", 18 | "eslint": "^9.4.0", 19 | "globals": "^15.3.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/checkDiff.js: -------------------------------------------------------------------------------- 1 | const { minimatch } = require("minimatch"); 2 | 3 | const checkDiff = (paths, diffFilesPaths) => { 4 | if (Array.isArray(paths)) { 5 | return paths.some((path) => 6 | diffFilesPaths.some( 7 | (diffPath) => diffPath.includes(path) || minimatch(diffPath, path), 8 | ), 9 | ); 10 | } else { 11 | throw new Error( 12 | `Wrong type for 'paths' variable (${typeof paths}). Make sure you followed the formatting rules.`, 13 | ); 14 | } 15 | }; 16 | 17 | export default checkDiff; 18 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: "Pull Request Auto Reviewer" 2 | description: "Add review comments to your pull requests based on changes" 3 | author: pshergie 4 | branding: 5 | icon: "message-square" 6 | color: "yellow" 7 | inputs: 8 | token: 9 | description: "token for github" 10 | required: true 11 | rules-path: 12 | description: "path to data yaml file with comments" 13 | required: true 14 | artifact-path: 15 | description: "path to PR number, comments and file diff" 16 | default: pr_diff/ 17 | runs: 18 | using: "node20" 19 | main: "dist/index.js" 20 | -------------------------------------------------------------------------------- /src/utils/fetchComments.js: -------------------------------------------------------------------------------- 1 | const fetchComments = async (context, pullNumber, octokit) => { 2 | let data = []; 3 | let pagesRemaining = true; 4 | let page = 1; 5 | 6 | while (pagesRemaining) { 7 | const response = await octokit.rest.issues.listComments({ 8 | ...context.repo, 9 | issue_number: pullNumber, 10 | per_page: 100, 11 | page, 12 | }); 13 | 14 | data = [...data, ...response.data]; 15 | const linkHeader = response.headers.link; 16 | pagesRemaining = linkHeader && linkHeader.includes(`rel=\"next\"`); 17 | page++; 18 | } 19 | 20 | return data; 21 | }; 22 | 23 | export default fetchComments; 24 | -------------------------------------------------------------------------------- /src/utils/parsePaths.js: -------------------------------------------------------------------------------- 1 | import splitPaths from "./splitPaths"; 2 | 3 | const parsePaths = configs => configs.map((config, i) => { 4 | if (!config.allCasesPaths && !config.modifiedOnlyPaths && !config.addedOnlyPaths && !config.deletedOnlyPaths) { 5 | throw new Error(`The config should have at least one path. Config #${i + 1}.${config.message ? ' Message:' + config.message : ''} `); 6 | }; 7 | 8 | return { 9 | allCasesPaths: splitPaths(config.allCasesPaths), 10 | modifiedOnlyPaths: splitPaths(config.modifiedOnlyPaths), 11 | addedOnlyPaths: splitPaths(config.addedOnlyPaths), 12 | deletedOnlyPaths: splitPaths(config.deletedOnlyPaths), 13 | message: config.message 14 | } 15 | }) 16 | 17 | export default parsePaths; 18 | -------------------------------------------------------------------------------- /src/utils/shouldMessageBePosted.js: -------------------------------------------------------------------------------- 1 | import checkDiff from "./checkDiff.js"; 2 | import compareMarkdown from "./compareMarkdown.js"; 3 | 4 | const shouldMessageBePosted = ( 5 | paths, 6 | message, 7 | diffFilesPaths, 8 | comments, 9 | messagesToPost, 10 | ) => { 11 | let areTargetPathsChanged = checkDiff(paths, diffFilesPaths); 12 | 13 | if (!pathCase || messagesToPost.includes(message)) { 14 | return false; 15 | } 16 | 17 | if (areTargetPathsChanged) { 18 | const isCommentExisting = comments.some( 19 | comment => 20 | comment.user.login === "github-actions[bot]" && 21 | compareMarkdown(comment.body, message), 22 | ); 23 | 24 | return isCommentExisting ? false : true; 25 | }; 26 | 27 | return false; 28 | }; 29 | 30 | export default shouldMessageBePosted; 31 | -------------------------------------------------------------------------------- /src/utils/getAutoCommentData.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const yaml = require("js-yaml"); 3 | const core = require("@actions/core"); 4 | 5 | import getRulesPath from "./getRulesPath.js"; 6 | 7 | const getAutoCommentData = () => { 8 | const refMsg = 'Use the Setup config section of the action description as a reference.'; 9 | const rulesPath = getRulesPath(core); 10 | const commentData = yaml.load( 11 | fs.readFileSync(rulesPath, "utf8"), 12 | ); 13 | 14 | if (!commentData) { 15 | console.log('Comment data: ', JSON.stringify(commentData, null, 4)); 16 | 17 | throw new Error('The auto comments data is empty or incorrect. ' + refMsg); 18 | } 19 | 20 | if (!commentData[1]) { 21 | console.log('Comment data: ', JSON.stringify(commentData, null, 4)); 22 | 23 | throw new Error('Checks data is not correct. ' + refMsg); 24 | } 25 | 26 | return commentData; 27 | }; 28 | 29 | export default getAutoCommentData; 30 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Auto Review Comment 2 | on: 3 | workflow_call: 4 | inputs: 5 | rules-path: 6 | description: "path to data yaml file with comments" 7 | required: true 8 | type: string 9 | artifact-path: 10 | description: "path to PR number, comments and file diff" 11 | required: false 12 | default: "pr_diff/" 13 | type: string 14 | jobs: 15 | auto-review: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | - name: Download Diff Artifact 21 | uses: actions/download-artifact@v4 22 | with: 23 | name: pr-diff 24 | path: pr_diff/ 25 | repository: ${{ github.repository_owner }}/${{ github.event.repository.name }} 26 | run-id: ${{ github.event.workflow_run.id }} 27 | github-token: ${{ secrets.GITHUB_TOKEN }} 28 | - name: Analyze changes 29 | uses: ./ 30 | id: auto-review-action 31 | with: 32 | token: ${{ secrets.GITHUB_TOKEN }} 33 | rules-path: ${{ inputs.rules-path }} 34 | artifact-path: ${{ inputs.artifact-path }} 35 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const core = require("@actions/core"); 3 | const github = require("@actions/github"); 4 | 5 | import postComment from "./utils/postComment.js"; 6 | import getAutoCommentData from "./utils/getAutoCommentData.js"; 7 | import fetchComments from "./utils/fetchComments.js"; 8 | import shouldMessageBePosted from "./utils/shouldMessageBePosted.js" 9 | import parsePaths from "./utils/parsePaths.js" 10 | import fetchDiffFromFile from "./utils/fetchDiffFromFile.js" 11 | 12 | async function run() { 13 | try { 14 | const artifactPath = core.getInput("artifact-path"); 15 | const [prependData, checksData] = getAutoCommentData(); 16 | const { prependMsg } = prependData; 17 | const checks = parsePaths(checksData.checks); 18 | const token = core.getInput("token"); 19 | const octokit = github.getOctokit(token); 20 | const context = github.context; 21 | const pullNumber = parseInt(fs.readFileSync(artifactPath + 'pr_number.txt', "utf8"), 10); 22 | const comments = await fetchComments(context, pullNumber, octokit); 23 | const diffPathList = ['all', 'mod', 'add', 'del'].map(type => fetchDiffFromFile(type)); 24 | const messagesToPost = []; 25 | 26 | checks.map(({ message, ...pathCases }) => pathCases.map((pathCase, i) => { 27 | if (shouldMessageBePosted(pathCase, message, diffPathList[i], comments, messagesToPost)) { 28 | messagesToPost.push(message); 29 | } 30 | })) 31 | 32 | if (messagesToPost.length > 0) { 33 | await postComment(prependMsg, messagesToPost, pullNumber, context, octokit); 34 | } 35 | } catch (error) { 36 | core.setFailed(error.message); 37 | } 38 | } 39 | 40 | run(); 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pull Request Auto Reviewer (GitHub Action) 2 | 3 | ![action example](./img/example.jpeg) 4 | 5 | _This script is for public repositories. For private ones use a [simplified version](https://github.com/marketplace/actions/pull-request-auto-reviewer-private) of it._ 6 | 7 | Add review comments to your pull requests based on changes in 3 steps: 8 | 9 | 1. Add rules for the action script [(more info)](#add-rules-for-the-action-script): 10 | 11 | ```yml 12 | - prependMsg: "🗯️ [pull-request-auto-reviewer](https://github.com/marketplace/actions/pull-request-auto-reviewer):" 13 | - checks: 14 | - paths: "**/*.js" 15 | message: | 16 | ### As you changed javascript file(s) tick the following checks: 17 | 18 | - [ ] unit/integration tests are added 19 | - [ ] no performance implications 20 | - [ ] relevant documentation is added/updated 21 | - paths: "package-lock.json" 22 | message: | 23 | ### Since you've added/updated dependencies, pay attention to this: 24 | 25 | - [ ] clear reasoning for adding or updating the dependency is provided 26 | - [ ] no security implications or concerns related to the dependency 27 | ``` 28 | 29 | 2. Add artifact uploading config [(more info)](#add-artifact-uploading-config): 30 | 31 | ```yml 32 | name: Auto Review Prepare 33 | on: 34 | pull_request: 35 | branches: 36 | - main 37 | - master 38 | jobs: 39 | prepare: 40 | name: Prepare 41 | runs-on: ubuntu-latest 42 | steps: 43 | - name: Checkout code 44 | uses: actions/checkout@v4 45 | with: 46 | fetch-depth: 2 47 | - name: Save PR number 48 | run: echo "${{ github.event.pull_request.number }}" > pr_number.txt 49 | - name: Generate Diff 50 | run: | 51 | git fetch origin ${{ github.event.pull_request.base.ref }} 52 | git diff --name-only origin/${{ github.event.pull_request.base.ref }}..${{ github.sha }} > pr_files_diff_all.txt 53 | git diff --name-status origin/${{ github.event.pull_request.base.ref }}..${{ github.sha }} | grep '^M' | cut -f2 > pr_files_diff_mod.txt 54 | git diff --name-status origin/${{ github.event.pull_request.base.ref }}..${{ github.sha }} | grep '^A' | cut -f2 > pr_files_diff_add.txt 55 | git diff --name-status origin/${{ github.event.pull_request.base.ref }}..${{ github.sha }} | grep '^D' | cut -f2 > pr_files_diff_del.txt 56 | - name: Create artifact folder 57 | run: mkdir -p pr_diff && mv pr_number.txt pr_files_diff.txt pr_diff/ 58 | - name: Upload PR details as artifact 59 | uses: actions/upload-artifact@v4 60 | with: 61 | name: pr-diff 62 | path: pr_diff/ 63 | ``` 64 | 65 | 3. Add artifact downloading and code analysis config [(more info)](#add-artifact-downloading-and-code-analysis-config) 66 | 67 | ```yml 68 | name: Auto Review Comment 69 | on: 70 | workflow_run: 71 | workflows: 72 | - Auto-review Diff Prepare 73 | types: 74 | - completed 75 | permissions: 76 | pull-requests: write 77 | jobs: 78 | auto-review: 79 | name: Review 80 | runs-on: ubuntu-latest 81 | steps: 82 | - name: Checkout the repository 83 | uses: actions/checkout@v4 84 | - name: Download PR diff from auto review prepare step 85 | uses: actions/download-artifact@v4 86 | with: 87 | name: pr-diff 88 | path: pr_diff/ 89 | repository: ${{ github.repository_owner }}/${{ github.event.repository.name }} 90 | run-id: ${{ github.event.workflow_run.id }} 91 | github-token: ${{ secrets.GITHUB_TOKEN }} 92 | - name: Analyze changes 93 | uses: pshergie/prator@v1 94 | id: auto-review-action 95 | with: 96 | token: ${{ secrets.GITHUB_TOKEN }} 97 | rules-path: path-to-your-rules-file 98 | ``` 99 | 100 | ## Add rules for the action script 101 | 102 | For this step you need to create a YAML file with the rules that are going to be utilized by the action script. There you need to specify 2 params: 103 | 104 | - `prependMsg` is a message that is prepended to every message of GitHub actions bot. Leave empty if not needed. 105 | - `checks` contains a list of `paths` and `message` keys. `paths` is dedicated to specify path(s) of changes that would trigger posting of a followed `message` as a pull request comment. In case of multiple `paths` they should be separated by a comma. `message` could be a simple string or markdown. All messages will be combined into a single comment. 106 | 107 | ## Add artifact uploading config 108 | 109 | Since posting comments on GitHub requires write permission you need two create 2 workflows. One to collect PR changes and upload as artifact (this step) and another to download them and apply the script (next step). There's an optional `artifact-path` parameter if you want a different path to the artifact (make sure that this change is reflected in the download artifact config). The default value is `pr_diff/` 110 | 111 | ## Add artifact downloading and code analysis config 112 | 113 | In this step create a workflow that downloads the artifact and applies the script. `pull-requests: write` permission is needed for the GitHub actions bot to be able to post a comment in a PR. It's also important to provide 2 params that are being consumed by the action script: 114 | 115 | - `token`: your GitHub token 116 | - `rules-path`: a path to the file with rules that you have specified earlier (for instance `.github/auto-review-rules.yml`) 117 | 118 | _Note:_ The workflow executes from the main/master branch and is not visible in the PR checks. 119 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | /******/ (() => { // webpackBootstrap 2 | /******/ var __webpack_modules__ = ({ 3 | 4 | /***/ 662: 5 | /***/ (function(module, __unused_webpack_exports, __nccwpck_require__) { 6 | 7 | /* module decorator */ module = __nccwpck_require__.nmd(module); 8 | ;(function (require, exports, module, platform) { 9 | 10 | if (module) module.exports = minimatch 11 | else exports.minimatch = minimatch 12 | 13 | if (!require) { 14 | require = function (id) { 15 | switch (id) { 16 | case "sigmund": return function sigmund (obj) { 17 | return JSON.stringify(obj) 18 | } 19 | case "path": return { basename: function (f) { 20 | f = f.split(/[\/\\]/) 21 | var e = f.pop() 22 | if (!e) e = f.pop() 23 | return e 24 | }} 25 | case "lru-cache": return function LRUCache () { 26 | // not quite an LRU, but still space-limited. 27 | var cache = {} 28 | var cnt = 0 29 | this.set = function (k, v) { 30 | cnt ++ 31 | if (cnt >= 100) cache = {} 32 | cache[k] = v 33 | } 34 | this.get = function (k) { return cache[k] } 35 | } 36 | } 37 | } 38 | } 39 | 40 | minimatch.Minimatch = Minimatch 41 | 42 | var LRU = require("lru-cache") 43 | , cache = minimatch.cache = new LRU({max: 100}) 44 | , GLOBSTAR = minimatch.GLOBSTAR = Minimatch.GLOBSTAR = {} 45 | , sigmund = require("sigmund") 46 | 47 | var path = require("path") 48 | // any single thing other than / 49 | // don't need to escape / when using new RegExp() 50 | , qmark = "[^/]" 51 | 52 | // * => any number of characters 53 | , star = qmark + "*?" 54 | 55 | // ** when dots are allowed. Anything goes, except .. and . 56 | // not (^ or / followed by one or two dots followed by $ or /), 57 | // followed by anything, any number of times. 58 | , twoStarDot = "(?:(?!(?:\\\/|^)(?:\\.{1,2})($|\\\/)).)*?" 59 | 60 | // not a ^ or / followed by a dot, 61 | // followed by anything, any number of times. 62 | , twoStarNoDot = "(?:(?!(?:\\\/|^)\\.).)*?" 63 | 64 | // characters that need to be escaped in RegExp. 65 | , reSpecials = charSet("().*{}+?[]^$\\!") 66 | 67 | // "abc" -> { a:true, b:true, c:true } 68 | function charSet (s) { 69 | return s.split("").reduce(function (set, c) { 70 | set[c] = true 71 | return set 72 | }, {}) 73 | } 74 | 75 | // normalizes slashes. 76 | var slashSplit = /\/+/ 77 | 78 | minimatch.filter = filter 79 | function filter (pattern, options) { 80 | options = options || {} 81 | return function (p, i, list) { 82 | return minimatch(p, pattern, options) 83 | } 84 | } 85 | 86 | function ext (a, b) { 87 | a = a || {} 88 | b = b || {} 89 | var t = {} 90 | Object.keys(b).forEach(function (k) { 91 | t[k] = b[k] 92 | }) 93 | Object.keys(a).forEach(function (k) { 94 | t[k] = a[k] 95 | }) 96 | return t 97 | } 98 | 99 | minimatch.defaults = function (def) { 100 | if (!def || !Object.keys(def).length) return minimatch 101 | 102 | var orig = minimatch 103 | 104 | var m = function minimatch (p, pattern, options) { 105 | return orig.minimatch(p, pattern, ext(def, options)) 106 | } 107 | 108 | m.Minimatch = function Minimatch (pattern, options) { 109 | return new orig.Minimatch(pattern, ext(def, options)) 110 | } 111 | 112 | return m 113 | } 114 | 115 | Minimatch.defaults = function (def) { 116 | if (!def || !Object.keys(def).length) return Minimatch 117 | return minimatch.defaults(def).Minimatch 118 | } 119 | 120 | 121 | function minimatch (p, pattern, options) { 122 | if (typeof pattern !== "string") { 123 | throw new TypeError("glob pattern string required") 124 | } 125 | 126 | if (!options) options = {} 127 | 128 | // shortcut: comments match nothing. 129 | if (!options.nocomment && pattern.charAt(0) === "#") { 130 | return false 131 | } 132 | 133 | // "" only matches "" 134 | if (pattern.trim() === "") return p === "" 135 | 136 | return new Minimatch(pattern, options).match(p) 137 | } 138 | 139 | function Minimatch (pattern, options) { 140 | if (!(this instanceof Minimatch)) { 141 | return new Minimatch(pattern, options, cache) 142 | } 143 | 144 | if (typeof pattern !== "string") { 145 | throw new TypeError("glob pattern string required") 146 | } 147 | 148 | if (!options) options = {} 149 | pattern = pattern.trim() 150 | 151 | // windows: need to use /, not \ 152 | // On other platforms, \ is a valid (albeit bad) filename char. 153 | if (platform === "win32") { 154 | pattern = pattern.split("\\").join("/") 155 | } 156 | 157 | // lru storage. 158 | // these things aren't particularly big, but walking down the string 159 | // and turning it into a regexp can get pretty costly. 160 | var cacheKey = pattern + "\n" + sigmund(options) 161 | var cached = minimatch.cache.get(cacheKey) 162 | if (cached) return cached 163 | minimatch.cache.set(cacheKey, this) 164 | 165 | this.options = options 166 | this.set = [] 167 | this.pattern = pattern 168 | this.regexp = null 169 | this.negate = false 170 | this.comment = false 171 | this.empty = false 172 | 173 | // make the set of regexps etc. 174 | this.make() 175 | } 176 | 177 | Minimatch.prototype.debug = function() {} 178 | 179 | Minimatch.prototype.make = make 180 | function make () { 181 | // don't do it more than once. 182 | if (this._made) return 183 | 184 | var pattern = this.pattern 185 | var options = this.options 186 | 187 | // empty patterns and comments match nothing. 188 | if (!options.nocomment && pattern.charAt(0) === "#") { 189 | this.comment = true 190 | return 191 | } 192 | if (!pattern) { 193 | this.empty = true 194 | return 195 | } 196 | 197 | // step 1: figure out negation, etc. 198 | this.parseNegate() 199 | 200 | // step 2: expand braces 201 | var set = this.globSet = this.braceExpand() 202 | 203 | if (options.debug) this.debug = console.error 204 | 205 | this.debug(this.pattern, set) 206 | 207 | // step 3: now we have a set, so turn each one into a series of path-portion 208 | // matching patterns. 209 | // These will be regexps, except in the case of "**", which is 210 | // set to the GLOBSTAR object for globstar behavior, 211 | // and will not contain any / characters 212 | set = this.globParts = set.map(function (s) { 213 | return s.split(slashSplit) 214 | }) 215 | 216 | this.debug(this.pattern, set) 217 | 218 | // glob --> regexps 219 | set = set.map(function (s, si, set) { 220 | return s.map(this.parse, this) 221 | }, this) 222 | 223 | this.debug(this.pattern, set) 224 | 225 | // filter out everything that didn't compile properly. 226 | set = set.filter(function (s) { 227 | return -1 === s.indexOf(false) 228 | }) 229 | 230 | this.debug(this.pattern, set) 231 | 232 | this.set = set 233 | } 234 | 235 | Minimatch.prototype.parseNegate = parseNegate 236 | function parseNegate () { 237 | var pattern = this.pattern 238 | , negate = false 239 | , options = this.options 240 | , negateOffset = 0 241 | 242 | if (options.nonegate) return 243 | 244 | for ( var i = 0, l = pattern.length 245 | ; i < l && pattern.charAt(i) === "!" 246 | ; i ++) { 247 | negate = !negate 248 | negateOffset ++ 249 | } 250 | 251 | if (negateOffset) this.pattern = pattern.substr(negateOffset) 252 | this.negate = negate 253 | } 254 | 255 | // Brace expansion: 256 | // a{b,c}d -> abd acd 257 | // a{b,}c -> abc ac 258 | // a{0..3}d -> a0d a1d a2d a3d 259 | // a{b,c{d,e}f}g -> abg acdfg acefg 260 | // a{b,c}d{e,f}g -> abdeg acdeg abdeg abdfg 261 | // 262 | // Invalid sets are not expanded. 263 | // a{2..}b -> a{2..}b 264 | // a{b}c -> a{b}c 265 | minimatch.braceExpand = function (pattern, options) { 266 | return new Minimatch(pattern, options).braceExpand() 267 | } 268 | 269 | Minimatch.prototype.braceExpand = braceExpand 270 | function braceExpand (pattern, options) { 271 | options = options || this.options 272 | pattern = typeof pattern === "undefined" 273 | ? this.pattern : pattern 274 | 275 | if (typeof pattern === "undefined") { 276 | throw new Error("undefined pattern") 277 | } 278 | 279 | if (options.nobrace || 280 | !pattern.match(/\{.*\}/)) { 281 | // shortcut. no need to expand. 282 | return [pattern] 283 | } 284 | 285 | var escaping = false 286 | 287 | // examples and comments refer to this crazy pattern: 288 | // a{b,c{d,e},{f,g}h}x{y,z} 289 | // expected: 290 | // abxy 291 | // abxz 292 | // acdxy 293 | // acdxz 294 | // acexy 295 | // acexz 296 | // afhxy 297 | // afhxz 298 | // aghxy 299 | // aghxz 300 | 301 | // everything before the first \{ is just a prefix. 302 | // So, we pluck that off, and work with the rest, 303 | // and then prepend it to everything we find. 304 | if (pattern.charAt(0) !== "{") { 305 | this.debug(pattern) 306 | var prefix = null 307 | for (var i = 0, l = pattern.length; i < l; i ++) { 308 | var c = pattern.charAt(i) 309 | this.debug(i, c) 310 | if (c === "\\") { 311 | escaping = !escaping 312 | } else if (c === "{" && !escaping) { 313 | prefix = pattern.substr(0, i) 314 | break 315 | } 316 | } 317 | 318 | // actually no sets, all { were escaped. 319 | if (prefix === null) { 320 | this.debug("no sets") 321 | return [pattern] 322 | } 323 | 324 | var tail = braceExpand.call(this, pattern.substr(i), options) 325 | return tail.map(function (t) { 326 | return prefix + t 327 | }) 328 | } 329 | 330 | // now we have something like: 331 | // {b,c{d,e},{f,g}h}x{y,z} 332 | // walk through the set, expanding each part, until 333 | // the set ends. then, we'll expand the suffix. 334 | // If the set only has a single member, then'll put the {} back 335 | 336 | // first, handle numeric sets, since they're easier 337 | var numset = pattern.match(/^\{(-?[0-9]+)\.\.(-?[0-9]+)\}/) 338 | if (numset) { 339 | this.debug("numset", numset[1], numset[2]) 340 | var suf = braceExpand.call(this, pattern.substr(numset[0].length), options) 341 | , start = +numset[1] 342 | , end = +numset[2] 343 | , inc = start > end ? -1 : 1 344 | , set = [] 345 | for (var i = start; i != (end + inc); i += inc) { 346 | // append all the suffixes 347 | for (var ii = 0, ll = suf.length; ii < ll; ii ++) { 348 | set.push(i + suf[ii]) 349 | } 350 | } 351 | return set 352 | } 353 | 354 | // ok, walk through the set 355 | // We hope, somewhat optimistically, that there 356 | // will be a } at the end. 357 | // If the closing brace isn't found, then the pattern is 358 | // interpreted as braceExpand("\\" + pattern) so that 359 | // the leading \{ will be interpreted literally. 360 | var i = 1 // skip the \{ 361 | , depth = 1 362 | , set = [] 363 | , member = "" 364 | , sawEnd = false 365 | , escaping = false 366 | 367 | function addMember () { 368 | set.push(member) 369 | member = "" 370 | } 371 | 372 | this.debug("Entering for") 373 | FOR: for (i = 1, l = pattern.length; i < l; i ++) { 374 | var c = pattern.charAt(i) 375 | this.debug("", i, c) 376 | 377 | if (escaping) { 378 | escaping = false 379 | member += "\\" + c 380 | } else { 381 | switch (c) { 382 | case "\\": 383 | escaping = true 384 | continue 385 | 386 | case "{": 387 | depth ++ 388 | member += "{" 389 | continue 390 | 391 | case "}": 392 | depth -- 393 | // if this closes the actual set, then we're done 394 | if (depth === 0) { 395 | addMember() 396 | // pluck off the close-brace 397 | i ++ 398 | break FOR 399 | } else { 400 | member += c 401 | continue 402 | } 403 | 404 | case ",": 405 | if (depth === 1) { 406 | addMember() 407 | } else { 408 | member += c 409 | } 410 | continue 411 | 412 | default: 413 | member += c 414 | continue 415 | } // switch 416 | } // else 417 | } // for 418 | 419 | // now we've either finished the set, and the suffix is 420 | // pattern.substr(i), or we have *not* closed the set, 421 | // and need to escape the leading brace 422 | if (depth !== 0) { 423 | this.debug("didn't close", pattern) 424 | return braceExpand.call(this, "\\" + pattern, options) 425 | } 426 | 427 | // x{y,z} -> ["xy", "xz"] 428 | this.debug("set", set) 429 | this.debug("suffix", pattern.substr(i)) 430 | var suf = braceExpand.call(this, pattern.substr(i), options) 431 | // ["b", "c{d,e}","{f,g}h"] -> 432 | // [["b"], ["cd", "ce"], ["fh", "gh"]] 433 | var addBraces = set.length === 1 434 | this.debug("set pre-expanded", set) 435 | set = set.map(function (p) { 436 | return braceExpand.call(this, p, options) 437 | }, this) 438 | this.debug("set expanded", set) 439 | 440 | 441 | // [["b"], ["cd", "ce"], ["fh", "gh"]] -> 442 | // ["b", "cd", "ce", "fh", "gh"] 443 | set = set.reduce(function (l, r) { 444 | return l.concat(r) 445 | }) 446 | 447 | if (addBraces) { 448 | set = set.map(function (s) { 449 | return "{" + s + "}" 450 | }) 451 | } 452 | 453 | // now attach the suffixes. 454 | var ret = [] 455 | for (var i = 0, l = set.length; i < l; i ++) { 456 | for (var ii = 0, ll = suf.length; ii < ll; ii ++) { 457 | ret.push(set[i] + suf[ii]) 458 | } 459 | } 460 | return ret 461 | } 462 | 463 | // parse a component of the expanded set. 464 | // At this point, no pattern may contain "/" in it 465 | // so we're going to return a 2d array, where each entry is the full 466 | // pattern, split on '/', and then turned into a regular expression. 467 | // A regexp is made at the end which joins each array with an 468 | // escaped /, and another full one which joins each regexp with |. 469 | // 470 | // Following the lead of Bash 4.1, note that "**" only has special meaning 471 | // when it is the *only* thing in a path portion. Otherwise, any series 472 | // of * is equivalent to a single *. Globstar behavior is enabled by 473 | // default, and can be disabled by setting options.noglobstar. 474 | Minimatch.prototype.parse = parse 475 | var SUBPARSE = {} 476 | function parse (pattern, isSub) { 477 | var options = this.options 478 | 479 | // shortcuts 480 | if (!options.noglobstar && pattern === "**") return GLOBSTAR 481 | if (pattern === "") return "" 482 | 483 | var re = "" 484 | , hasMagic = !!options.nocase 485 | , escaping = false 486 | // ? => one single character 487 | , patternListStack = [] 488 | , plType 489 | , stateChar 490 | , inClass = false 491 | , reClassStart = -1 492 | , classStart = -1 493 | // . and .. never match anything that doesn't start with ., 494 | // even when options.dot is set. 495 | , patternStart = pattern.charAt(0) === "." ? "" // anything 496 | // not (start or / followed by . or .. followed by / or end) 497 | : options.dot ? "(?!(?:^|\\\/)\\.{1,2}(?:$|\\\/))" 498 | : "(?!\\.)" 499 | , self = this 500 | 501 | function clearStateChar () { 502 | if (stateChar) { 503 | // we had some state-tracking character 504 | // that wasn't consumed by this pass. 505 | switch (stateChar) { 506 | case "*": 507 | re += star 508 | hasMagic = true 509 | break 510 | case "?": 511 | re += qmark 512 | hasMagic = true 513 | break 514 | default: 515 | re += "\\"+stateChar 516 | break 517 | } 518 | self.debug('clearStateChar %j %j', stateChar, re) 519 | stateChar = false 520 | } 521 | } 522 | 523 | for ( var i = 0, len = pattern.length, c 524 | ; (i < len) && (c = pattern.charAt(i)) 525 | ; i ++ ) { 526 | 527 | this.debug("%s\t%s %s %j", pattern, i, re, c) 528 | 529 | // skip over any that are escaped. 530 | if (escaping && reSpecials[c]) { 531 | re += "\\" + c 532 | escaping = false 533 | continue 534 | } 535 | 536 | SWITCH: switch (c) { 537 | case "/": 538 | // completely not allowed, even escaped. 539 | // Should already be path-split by now. 540 | return false 541 | 542 | case "\\": 543 | clearStateChar() 544 | escaping = true 545 | continue 546 | 547 | // the various stateChar values 548 | // for the "extglob" stuff. 549 | case "?": 550 | case "*": 551 | case "+": 552 | case "@": 553 | case "!": 554 | this.debug("%s\t%s %s %j <-- stateChar", pattern, i, re, c) 555 | 556 | // all of those are literals inside a class, except that 557 | // the glob [!a] means [^a] in regexp 558 | if (inClass) { 559 | this.debug(' in class') 560 | if (c === "!" && i === classStart + 1) c = "^" 561 | re += c 562 | continue 563 | } 564 | 565 | // if we already have a stateChar, then it means 566 | // that there was something like ** or +? in there. 567 | // Handle the stateChar, then proceed with this one. 568 | self.debug('call clearStateChar %j', stateChar) 569 | clearStateChar() 570 | stateChar = c 571 | // if extglob is disabled, then +(asdf|foo) isn't a thing. 572 | // just clear the statechar *now*, rather than even diving into 573 | // the patternList stuff. 574 | if (options.noext) clearStateChar() 575 | continue 576 | 577 | case "(": 578 | if (inClass) { 579 | re += "(" 580 | continue 581 | } 582 | 583 | if (!stateChar) { 584 | re += "\\(" 585 | continue 586 | } 587 | 588 | plType = stateChar 589 | patternListStack.push({ type: plType 590 | , start: i - 1 591 | , reStart: re.length }) 592 | // negation is (?:(?!js)[^/]*) 593 | re += stateChar === "!" ? "(?:(?!" : "(?:" 594 | this.debug('plType %j %j', stateChar, re) 595 | stateChar = false 596 | continue 597 | 598 | case ")": 599 | if (inClass || !patternListStack.length) { 600 | re += "\\)" 601 | continue 602 | } 603 | 604 | clearStateChar() 605 | hasMagic = true 606 | re += ")" 607 | plType = patternListStack.pop().type 608 | // negation is (?:(?!js)[^/]*) 609 | // The others are (?:) 610 | switch (plType) { 611 | case "!": 612 | re += "[^/]*?)" 613 | break 614 | case "?": 615 | case "+": 616 | case "*": re += plType 617 | case "@": break // the default anyway 618 | } 619 | continue 620 | 621 | case "|": 622 | if (inClass || !patternListStack.length || escaping) { 623 | re += "\\|" 624 | escaping = false 625 | continue 626 | } 627 | 628 | clearStateChar() 629 | re += "|" 630 | continue 631 | 632 | // these are mostly the same in regexp and glob 633 | case "[": 634 | // swallow any state-tracking char before the [ 635 | clearStateChar() 636 | 637 | if (inClass) { 638 | re += "\\" + c 639 | continue 640 | } 641 | 642 | inClass = true 643 | classStart = i 644 | reClassStart = re.length 645 | re += c 646 | continue 647 | 648 | case "]": 649 | // a right bracket shall lose its special 650 | // meaning and represent itself in 651 | // a bracket expression if it occurs 652 | // first in the list. -- POSIX.2 2.8.3.2 653 | if (i === classStart + 1 || !inClass) { 654 | re += "\\" + c 655 | escaping = false 656 | continue 657 | } 658 | 659 | // finish up the class. 660 | hasMagic = true 661 | inClass = false 662 | re += c 663 | continue 664 | 665 | default: 666 | // swallow any state char that wasn't consumed 667 | clearStateChar() 668 | 669 | if (escaping) { 670 | // no need 671 | escaping = false 672 | } else if (reSpecials[c] 673 | && !(c === "^" && inClass)) { 674 | re += "\\" 675 | } 676 | 677 | re += c 678 | 679 | } // switch 680 | } // for 681 | 682 | 683 | // handle the case where we left a class open. 684 | // "[abc" is valid, equivalent to "\[abc" 685 | if (inClass) { 686 | // split where the last [ was, and escape it 687 | // this is a huge pita. We now have to re-walk 688 | // the contents of the would-be class to re-translate 689 | // any characters that were passed through as-is 690 | var cs = pattern.substr(classStart + 1) 691 | , sp = this.parse(cs, SUBPARSE) 692 | re = re.substr(0, reClassStart) + "\\[" + sp[0] 693 | hasMagic = hasMagic || sp[1] 694 | } 695 | 696 | // handle the case where we had a +( thing at the *end* 697 | // of the pattern. 698 | // each pattern list stack adds 3 chars, and we need to go through 699 | // and escape any | chars that were passed through as-is for the regexp. 700 | // Go through and escape them, taking care not to double-escape any 701 | // | chars that were already escaped. 702 | var pl 703 | while (pl = patternListStack.pop()) { 704 | var tail = re.slice(pl.reStart + 3) 705 | // maybe some even number of \, then maybe 1 \, followed by a | 706 | tail = tail.replace(/((?:\\{2})*)(\\?)\|/g, function (_, $1, $2) { 707 | if (!$2) { 708 | // the | isn't already escaped, so escape it. 709 | $2 = "\\" 710 | } 711 | 712 | // need to escape all those slashes *again*, without escaping the 713 | // one that we need for escaping the | character. As it works out, 714 | // escaping an even number of slashes can be done by simply repeating 715 | // it exactly after itself. That's why this trick works. 716 | // 717 | // I am sorry that you have to see this. 718 | return $1 + $1 + $2 + "|" 719 | }) 720 | 721 | this.debug("tail=%j\n %s", tail, tail) 722 | var t = pl.type === "*" ? star 723 | : pl.type === "?" ? qmark 724 | : "\\" + pl.type 725 | 726 | hasMagic = true 727 | re = re.slice(0, pl.reStart) 728 | + t + "\\(" 729 | + tail 730 | } 731 | 732 | // handle trailing things that only matter at the very end. 733 | clearStateChar() 734 | if (escaping) { 735 | // trailing \\ 736 | re += "\\\\" 737 | } 738 | 739 | // only need to apply the nodot start if the re starts with 740 | // something that could conceivably capture a dot 741 | var addPatternStart = false 742 | switch (re.charAt(0)) { 743 | case ".": 744 | case "[": 745 | case "(": addPatternStart = true 746 | } 747 | 748 | // if the re is not "" at this point, then we need to make sure 749 | // it doesn't match against an empty path part. 750 | // Otherwise a/* will match a/, which it should not. 751 | if (re !== "" && hasMagic) re = "(?=.)" + re 752 | 753 | if (addPatternStart) re = patternStart + re 754 | 755 | // parsing just a piece of a larger pattern. 756 | if (isSub === SUBPARSE) { 757 | return [ re, hasMagic ] 758 | } 759 | 760 | // skip the regexp for non-magical patterns 761 | // unescape anything in it, though, so that it'll be 762 | // an exact match against a file etc. 763 | if (!hasMagic) { 764 | return globUnescape(pattern) 765 | } 766 | 767 | var flags = options.nocase ? "i" : "" 768 | , regExp = new RegExp("^" + re + "$", flags) 769 | 770 | regExp._glob = pattern 771 | regExp._src = re 772 | 773 | return regExp 774 | } 775 | 776 | minimatch.makeRe = function (pattern, options) { 777 | return new Minimatch(pattern, options || {}).makeRe() 778 | } 779 | 780 | Minimatch.prototype.makeRe = makeRe 781 | function makeRe () { 782 | if (this.regexp || this.regexp === false) return this.regexp 783 | 784 | // at this point, this.set is a 2d array of partial 785 | // pattern strings, or "**". 786 | // 787 | // It's better to use .match(). This function shouldn't 788 | // be used, really, but it's pretty convenient sometimes, 789 | // when you just want to work with a regex. 790 | var set = this.set 791 | 792 | if (!set.length) return this.regexp = false 793 | var options = this.options 794 | 795 | var twoStar = options.noglobstar ? star 796 | : options.dot ? twoStarDot 797 | : twoStarNoDot 798 | , flags = options.nocase ? "i" : "" 799 | 800 | var re = set.map(function (pattern) { 801 | return pattern.map(function (p) { 802 | return (p === GLOBSTAR) ? twoStar 803 | : (typeof p === "string") ? regExpEscape(p) 804 | : p._src 805 | }).join("\\\/") 806 | }).join("|") 807 | 808 | // must match entire pattern 809 | // ending in a * or ** will make it less strict. 810 | re = "^(?:" + re + ")$" 811 | 812 | // can match anything, as long as it's not this. 813 | if (this.negate) re = "^(?!" + re + ").*$" 814 | 815 | try { 816 | return this.regexp = new RegExp(re, flags) 817 | } catch (ex) { 818 | return this.regexp = false 819 | } 820 | } 821 | 822 | minimatch.match = function (list, pattern, options) { 823 | var mm = new Minimatch(pattern, options) 824 | list = list.filter(function (f) { 825 | return mm.match(f) 826 | }) 827 | if (options.nonull && !list.length) { 828 | list.push(pattern) 829 | } 830 | return list 831 | } 832 | 833 | Minimatch.prototype.match = match 834 | function match (f, partial) { 835 | this.debug("match", f, this.pattern) 836 | // short-circuit in the case of busted things. 837 | // comments, etc. 838 | if (this.comment) return false 839 | if (this.empty) return f === "" 840 | 841 | if (f === "/" && partial) return true 842 | 843 | var options = this.options 844 | 845 | // windows: need to use /, not \ 846 | // On other platforms, \ is a valid (albeit bad) filename char. 847 | if (platform === "win32") { 848 | f = f.split("\\").join("/") 849 | } 850 | 851 | // treat the test path as a set of pathparts. 852 | f = f.split(slashSplit) 853 | this.debug(this.pattern, "split", f) 854 | 855 | // just ONE of the pattern sets in this.set needs to match 856 | // in order for it to be valid. If negating, then just one 857 | // match means that we have failed. 858 | // Either way, return on the first hit. 859 | 860 | var set = this.set 861 | this.debug(this.pattern, "set", set) 862 | 863 | var splitFile = path.basename(f.join("/")).split("/") 864 | 865 | for (var i = 0, l = set.length; i < l; i ++) { 866 | var pattern = set[i], file = f 867 | if (options.matchBase && pattern.length === 1) { 868 | file = splitFile 869 | } 870 | var hit = this.matchOne(file, pattern, partial) 871 | if (hit) { 872 | if (options.flipNegate) return true 873 | return !this.negate 874 | } 875 | } 876 | 877 | // didn't get any hits. this is success if it's a negative 878 | // pattern, failure otherwise. 879 | if (options.flipNegate) return false 880 | return this.negate 881 | } 882 | 883 | // set partial to true to test if, for example, 884 | // "/a/b" matches the start of "/*/b/*/d" 885 | // Partial means, if you run out of file before you run 886 | // out of pattern, then that's fine, as long as all 887 | // the parts match. 888 | Minimatch.prototype.matchOne = function (file, pattern, partial) { 889 | var options = this.options 890 | 891 | this.debug("matchOne", 892 | { "this": this 893 | , file: file 894 | , pattern: pattern }) 895 | 896 | this.debug("matchOne", file.length, pattern.length) 897 | 898 | for ( var fi = 0 899 | , pi = 0 900 | , fl = file.length 901 | , pl = pattern.length 902 | ; (fi < fl) && (pi < pl) 903 | ; fi ++, pi ++ ) { 904 | 905 | this.debug("matchOne loop") 906 | var p = pattern[pi] 907 | , f = file[fi] 908 | 909 | this.debug(pattern, p, f) 910 | 911 | // should be impossible. 912 | // some invalid regexp stuff in the set. 913 | if (p === false) return false 914 | 915 | if (p === GLOBSTAR) { 916 | this.debug('GLOBSTAR', [pattern, p, f]) 917 | 918 | // "**" 919 | // a/**/b/**/c would match the following: 920 | // a/b/x/y/z/c 921 | // a/x/y/z/b/c 922 | // a/b/x/b/x/c 923 | // a/b/c 924 | // To do this, take the rest of the pattern after 925 | // the **, and see if it would match the file remainder. 926 | // If so, return success. 927 | // If not, the ** "swallows" a segment, and try again. 928 | // This is recursively awful. 929 | // 930 | // a/**/b/**/c matching a/b/x/y/z/c 931 | // - a matches a 932 | // - doublestar 933 | // - matchOne(b/x/y/z/c, b/**/c) 934 | // - b matches b 935 | // - doublestar 936 | // - matchOne(x/y/z/c, c) -> no 937 | // - matchOne(y/z/c, c) -> no 938 | // - matchOne(z/c, c) -> no 939 | // - matchOne(c, c) yes, hit 940 | var fr = fi 941 | , pr = pi + 1 942 | if (pr === pl) { 943 | this.debug('** at the end') 944 | // a ** at the end will just swallow the rest. 945 | // We have found a match. 946 | // however, it will not swallow /.x, unless 947 | // options.dot is set. 948 | // . and .. are *never* matched by **, for explosively 949 | // exponential reasons. 950 | for ( ; fi < fl; fi ++) { 951 | if (file[fi] === "." || file[fi] === ".." || 952 | (!options.dot && file[fi].charAt(0) === ".")) return false 953 | } 954 | return true 955 | } 956 | 957 | // ok, let's see if we can swallow whatever we can. 958 | WHILE: while (fr < fl) { 959 | var swallowee = file[fr] 960 | 961 | this.debug('\nglobstar while', 962 | file, fr, pattern, pr, swallowee) 963 | 964 | // XXX remove this slice. Just pass the start index. 965 | if (this.matchOne(file.slice(fr), pattern.slice(pr), partial)) { 966 | this.debug('globstar found match!', fr, fl, swallowee) 967 | // found a match. 968 | return true 969 | } else { 970 | // can't swallow "." or ".." ever. 971 | // can only swallow ".foo" when explicitly asked. 972 | if (swallowee === "." || swallowee === ".." || 973 | (!options.dot && swallowee.charAt(0) === ".")) { 974 | this.debug("dot detected!", file, fr, pattern, pr) 975 | break WHILE 976 | } 977 | 978 | // ** swallows a segment, and continue. 979 | this.debug('globstar swallow a segment, and continue') 980 | fr ++ 981 | } 982 | } 983 | // no match was found. 984 | // However, in partial mode, we can't say this is necessarily over. 985 | // If there's more *pattern* left, then 986 | if (partial) { 987 | // ran out of file 988 | this.debug("\n>>> no match, partial?", file, fr, pattern, pr) 989 | if (fr === fl) return true 990 | } 991 | return false 992 | } 993 | 994 | // something other than ** 995 | // non-magic patterns just have to match exactly 996 | // patterns with magic have been turned into regexps. 997 | var hit 998 | if (typeof p === "string") { 999 | if (options.nocase) { 1000 | hit = f.toLowerCase() === p.toLowerCase() 1001 | } else { 1002 | hit = f === p 1003 | } 1004 | this.debug("string match", p, f, hit) 1005 | } else { 1006 | hit = f.match(p) 1007 | this.debug("pattern match", p, f, hit) 1008 | } 1009 | 1010 | if (!hit) return false 1011 | } 1012 | 1013 | // Note: ending in / means that we'll get a final "" 1014 | // at the end of the pattern. This can only match a 1015 | // corresponding "" at the end of the file. 1016 | // If the file ends in /, then it can only match a 1017 | // a pattern that ends in /, unless the pattern just 1018 | // doesn't have any more for it. But, a/b/ should *not* 1019 | // match "a/b/*", even though "" matches against the 1020 | // [^/]*? pattern, except in partial mode, where it might 1021 | // simply not be reached yet. 1022 | // However, a/b/ should still satisfy a/* 1023 | 1024 | // now either we fell off the end of the pattern, or we're done. 1025 | if (fi === fl && pi === pl) { 1026 | // ran out of pattern and filename at the same time. 1027 | // an exact hit! 1028 | return true 1029 | } else if (fi === fl) { 1030 | // ran out of file, but still had pattern left. 1031 | // this is ok if we're doing the match as part of 1032 | // a glob fs traversal. 1033 | return partial 1034 | } else if (pi === pl) { 1035 | // ran out of pattern, still have file left. 1036 | // this is only acceptable if we're on the very last 1037 | // empty segment of a file with a trailing slash. 1038 | // a/* should match a/b/ 1039 | var emptyFileEnd = (fi === fl - 1) && (file[fi] === "") 1040 | return emptyFileEnd 1041 | } 1042 | 1043 | // should be unreachable. 1044 | throw new Error("wtf?") 1045 | } 1046 | 1047 | 1048 | // replace stuff like \* with * 1049 | function globUnescape (s) { 1050 | return s.replace(/\\(.)/g, "$1") 1051 | } 1052 | 1053 | 1054 | function regExpEscape (s) { 1055 | return s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&") 1056 | } 1057 | 1058 | })( true ? require : 0, 1059 | this, 1060 | true ? module : 0, 1061 | typeof process === "object" ? process.platform : "win32" 1062 | ) 1063 | 1064 | 1065 | /***/ }), 1066 | 1067 | /***/ 105: 1068 | /***/ ((module) => { 1069 | 1070 | module.exports = eval("require")("@actions/core"); 1071 | 1072 | 1073 | /***/ }), 1074 | 1075 | /***/ 82: 1076 | /***/ ((module) => { 1077 | 1078 | module.exports = eval("require")("@actions/github"); 1079 | 1080 | 1081 | /***/ }), 1082 | 1083 | /***/ 982: 1084 | /***/ ((module) => { 1085 | 1086 | module.exports = eval("require")("js-yaml"); 1087 | 1088 | 1089 | /***/ }), 1090 | 1091 | /***/ 147: 1092 | /***/ ((module) => { 1093 | 1094 | "use strict"; 1095 | module.exports = require("fs"); 1096 | 1097 | /***/ }) 1098 | 1099 | /******/ }); 1100 | /************************************************************************/ 1101 | /******/ // The module cache 1102 | /******/ var __webpack_module_cache__ = {}; 1103 | /******/ 1104 | /******/ // The require function 1105 | /******/ function __nccwpck_require__(moduleId) { 1106 | /******/ // Check if module is in cache 1107 | /******/ var cachedModule = __webpack_module_cache__[moduleId]; 1108 | /******/ if (cachedModule !== undefined) { 1109 | /******/ return cachedModule.exports; 1110 | /******/ } 1111 | /******/ // Create a new module (and put it into the cache) 1112 | /******/ var module = __webpack_module_cache__[moduleId] = { 1113 | /******/ id: moduleId, 1114 | /******/ loaded: false, 1115 | /******/ exports: {} 1116 | /******/ }; 1117 | /******/ 1118 | /******/ // Execute the module function 1119 | /******/ var threw = true; 1120 | /******/ try { 1121 | /******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __nccwpck_require__); 1122 | /******/ threw = false; 1123 | /******/ } finally { 1124 | /******/ if(threw) delete __webpack_module_cache__[moduleId]; 1125 | /******/ } 1126 | /******/ 1127 | /******/ // Flag the module as loaded 1128 | /******/ module.loaded = true; 1129 | /******/ 1130 | /******/ // Return the exports of the module 1131 | /******/ return module.exports; 1132 | /******/ } 1133 | /******/ 1134 | /************************************************************************/ 1135 | /******/ /* webpack/runtime/make namespace object */ 1136 | /******/ (() => { 1137 | /******/ // define __esModule on exports 1138 | /******/ __nccwpck_require__.r = (exports) => { 1139 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { 1140 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 1141 | /******/ } 1142 | /******/ Object.defineProperty(exports, '__esModule', { value: true }); 1143 | /******/ }; 1144 | /******/ })(); 1145 | /******/ 1146 | /******/ /* webpack/runtime/node module decorator */ 1147 | /******/ (() => { 1148 | /******/ __nccwpck_require__.nmd = (module) => { 1149 | /******/ module.paths = []; 1150 | /******/ if (!module.children) module.children = []; 1151 | /******/ return module; 1152 | /******/ }; 1153 | /******/ })(); 1154 | /******/ 1155 | /******/ /* webpack/runtime/compat */ 1156 | /******/ 1157 | /******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = __dirname + "/"; 1158 | /******/ 1159 | /************************************************************************/ 1160 | var __webpack_exports__ = {}; 1161 | // This entry need to be wrapped in an IIFE because it need to be in strict mode. 1162 | (() => { 1163 | "use strict"; 1164 | // ESM COMPAT FLAG 1165 | __nccwpck_require__.r(__webpack_exports__); 1166 | 1167 | ;// CONCATENATED MODULE: ./src/utils/postComment.js 1168 | const postComment = async ( 1169 | prependMsg, 1170 | messagesToPost, 1171 | pullNumber, 1172 | context, 1173 | octokit, 1174 | ) => { 1175 | const message = messagesToPost.join('\n\n'); 1176 | const body = prependMsg ? `${prependMsg}\n\n` + message : message; 1177 | 1178 | await octokit.rest.issues.createComment({ 1179 | ...context.repo, 1180 | issue_number: pullNumber, 1181 | body, 1182 | }); 1183 | }; 1184 | 1185 | /* harmony default export */ const utils_postComment = (postComment); 1186 | 1187 | ;// CONCATENATED MODULE: ./src/utils/getRulesPath.js 1188 | const getRulesPath = (core) => { 1189 | const rulesPath = core.getInput("rules-path"); 1190 | 1191 | if (!rulesPath) { 1192 | throw new Error("The rulesPath variable is empty, please provide it."); 1193 | } 1194 | 1195 | return rulesPath; 1196 | }; 1197 | 1198 | /* harmony default export */ const utils_getRulesPath = (getRulesPath); 1199 | 1200 | ;// CONCATENATED MODULE: ./src/utils/getAutoCommentData.js 1201 | const getAutoCommentData_fs = __nccwpck_require__(147); 1202 | const yaml = __nccwpck_require__(982); 1203 | const core = __nccwpck_require__(105); 1204 | 1205 | 1206 | 1207 | const getAutoCommentData = () => { 1208 | const refMsg = 'Use the Setup config section of the action description as a reference.'; 1209 | const rulesPath = utils_getRulesPath(core); 1210 | const commentData = yaml.load( 1211 | getAutoCommentData_fs.readFileSync(rulesPath, "utf8"), 1212 | ); 1213 | 1214 | if (!commentData) { 1215 | console.log('Comment data: ', JSON.stringify(commentData, null, 4)); 1216 | 1217 | throw new Error('The auto comments data is empty or incorrect. ' + refMsg); 1218 | } 1219 | 1220 | if (!commentData[1]) { 1221 | console.log('Comment data: ', JSON.stringify(commentData, null, 4)); 1222 | 1223 | throw new Error('Checks data is not correct. ' + refMsg); 1224 | } 1225 | 1226 | return commentData; 1227 | }; 1228 | 1229 | /* harmony default export */ const utils_getAutoCommentData = (getAutoCommentData); 1230 | 1231 | ;// CONCATENATED MODULE: ./src/utils/fetchComments.js 1232 | const fetchComments = async (context, pullNumber, octokit) => { 1233 | let data = []; 1234 | let pagesRemaining = true; 1235 | let page = 1; 1236 | 1237 | while (pagesRemaining) { 1238 | const response = await octokit.rest.issues.listComments({ 1239 | ...context.repo, 1240 | issue_number: pullNumber, 1241 | per_page: 100, 1242 | page, 1243 | }); 1244 | 1245 | data = [...data, ...response.data]; 1246 | const linkHeader = response.headers.link; 1247 | pagesRemaining = linkHeader && linkHeader.includes(`rel=\"next\"`); 1248 | page++; 1249 | } 1250 | 1251 | return data; 1252 | }; 1253 | 1254 | /* harmony default export */ const utils_fetchComments = (fetchComments); 1255 | 1256 | ;// CONCATENATED MODULE: ./src/utils/checkDiff.js 1257 | const { minimatch } = __nccwpck_require__(662); 1258 | 1259 | const checkDiff = (paths, diffFilesPaths) => { 1260 | if (Array.isArray(paths)) { 1261 | return paths.some((path) => 1262 | diffFilesPaths.some( 1263 | (diffPath) => diffPath.includes(path) || minimatch(diffPath, path), 1264 | ), 1265 | ); 1266 | } else { 1267 | throw new Error( 1268 | `Wrong type for 'paths' variable (${typeof paths}). Make sure you followed the formatting rules.`, 1269 | ); 1270 | } 1271 | }; 1272 | 1273 | /* harmony default export */ const utils_checkDiff = (checkDiff); 1274 | 1275 | ;// CONCATENATED MODULE: ./src/utils/compareMarkdown.js 1276 | const compareMarkdown = (comment, message) => { 1277 | return comment.replaceAll("- [x]", "- [ ]").includes(message); 1278 | }; 1279 | 1280 | /* harmony default export */ const utils_compareMarkdown = (compareMarkdown); 1281 | 1282 | ;// CONCATENATED MODULE: ./src/utils/shouldMessageBePosted.js 1283 | 1284 | 1285 | 1286 | const shouldMessageBePosted = ( 1287 | paths, 1288 | message, 1289 | diffFilesPaths, 1290 | comments, 1291 | messagesToPost, 1292 | ) => { 1293 | let areTargetPathsChanged = utils_checkDiff(paths, diffFilesPaths); 1294 | 1295 | if (!pathCase || messagesToPost.includes(message)) { 1296 | return false; 1297 | } 1298 | 1299 | if (areTargetPathsChanged) { 1300 | const isCommentExisting = comments.some( 1301 | comment => 1302 | comment.user.login === "github-actions[bot]" && 1303 | utils_compareMarkdown(comment.body, message), 1304 | ); 1305 | 1306 | return isCommentExisting ? false : true; 1307 | }; 1308 | 1309 | return false; 1310 | }; 1311 | 1312 | /* harmony default export */ const utils_shouldMessageBePosted = (shouldMessageBePosted); 1313 | 1314 | ;// CONCATENATED MODULE: ./src/utils/splitPaths.js 1315 | const splitPaths = paths => paths ? paths.split(",").map((p) => p.trim()) : undefined; 1316 | 1317 | /* harmony default export */ const utils_splitPaths = (splitPaths); 1318 | 1319 | ;// CONCATENATED MODULE: ./src/utils/parsePaths.js 1320 | 1321 | 1322 | const parsePaths = configs => configs.map((config, i) => { 1323 | if (!config.allCasesPaths && !config.modifiedOnlyPaths && !config.addedOnlyPaths && !config.deletedOnlyPaths) { 1324 | throw new Error(`The config should have at least one path. Config #${i + 1}.${config.message ? ' Message:' + config.message : ''} `); 1325 | }; 1326 | 1327 | return { 1328 | allCasesPaths: utils_splitPaths(config.allCasesPaths), 1329 | modifiedOnlyPaths: utils_splitPaths(config.modifiedOnlyPaths), 1330 | addedOnlyPaths: utils_splitPaths(config.addedOnlyPaths), 1331 | deletedOnlyPaths: utils_splitPaths(config.deletedOnlyPaths), 1332 | message: config.message 1333 | } 1334 | }) 1335 | 1336 | /* harmony default export */ const utils_parsePaths = (parsePaths); 1337 | 1338 | ;// CONCATENATED MODULE: ./src/utils/fetchDiffFromFile.js 1339 | const fetchDiffFromFile = (type) => fs.readFileSync(`${artifactPath}pr_files_diff_${type}.txt`, "utf8").split('\n').filter(Boolean); 1340 | 1341 | /* harmony default export */ const utils_fetchDiffFromFile = (fetchDiffFromFile); 1342 | 1343 | ;// CONCATENATED MODULE: ./src/index.js 1344 | const src_fs = __nccwpck_require__(147); 1345 | const src_core = __nccwpck_require__(105); 1346 | const github = __nccwpck_require__(82); 1347 | 1348 | 1349 | 1350 | 1351 | 1352 | 1353 | 1354 | 1355 | async function run() { 1356 | try { 1357 | const artifactPath = src_core.getInput("artifact-path"); 1358 | const [prependData, checksData] = utils_getAutoCommentData(); 1359 | const { prependMsg } = prependData; 1360 | const checks = utils_parsePaths(checksData.checks); 1361 | const token = src_core.getInput("token"); 1362 | const octokit = github.getOctokit(token); 1363 | const context = github.context; 1364 | const pullNumber = parseInt(src_fs.readFileSync(artifactPath + 'pr_number.txt', "utf8"), 10); 1365 | const comments = await utils_fetchComments(context, pullNumber, octokit); 1366 | const diffPathList = ['all', 'mod', 'add', 'del'].map(type => utils_fetchDiffFromFile(type)); 1367 | const messagesToPost = []; 1368 | 1369 | checks.map(({ message, ...pathCases }) => pathCases.map((pathCase, i) => { 1370 | if (utils_shouldMessageBePosted(pathCase, message, diffPathList[i], comments, messagesToPost)) { 1371 | messagesToPost.push(message); 1372 | } 1373 | })) 1374 | 1375 | if (messagesToPost.length > 0) { 1376 | await utils_postComment(prependMsg, messagesToPost, pullNumber, context, octokit); 1377 | } 1378 | } catch (error) { 1379 | src_core.setFailed(error.message); 1380 | } 1381 | } 1382 | 1383 | run(); 1384 | 1385 | })(); 1386 | 1387 | module.exports = __webpack_exports__; 1388 | /******/ })() 1389 | ; --------------------------------------------------------------------------------