diff --git a/index.d.ts b/index.d.ts index 2dfef99d..1560239b 100644 --- a/index.d.ts +++ b/index.d.ts @@ -51,6 +51,7 @@ declare class RuleInfo { fixConfig?: any policyInfo?: string policyUrl?: string + sequentialOnly?: boolean } declare class FormatResult { diff --git a/index.js b/index.js index e0767fb7..4d247dbb 100644 --- a/index.js +++ b/index.js @@ -289,6 +289,9 @@ async function runRuleset(ruleset, targets, fileSystem, dryRun) { // generate a flat array of axiom string identifiers /** @ignore @type {string[]} */ let targetArray = [] + const sequentialRuleProcessingArrayList = ruleset.filter( + r => r.sequentialOnly + ) if (typeof targets !== 'boolean') { targetArray = Object.entries(targets) // restricted to only passed axioms @@ -302,26 +305,33 @@ async function runRuleset(ruleset, targets, fileSystem, dryRun) { .reduce((a, c) => a.concat(c), []) } // run the ruleset + ruleset = ruleset.filter(r => !r.sequentialOnly) const results = ruleset.map(async r => { // check axioms and enable appropriately if (r.level === 'off') { - return FormatResult.CreateIgnored(r, 'ignored because level is "off"') + return Promise.resolve( + FormatResult.CreateIgnored(r, 'ignored because level is "off"') + ) } // filter to only targets with no matches if (typeof targets !== 'boolean' && r.where && r.where.length) { const ignoreReasons = shouldRuleRun(targetArray, r.where) if (ignoreReasons.length > 0) { - return FormatResult.CreateIgnored( - r, - `ignored due to unsatisfied condition(s): "${ignoreReasons.join( - '", "' - )}"` + return Promise.resolve( + FormatResult.CreateIgnored( + r, + `ignored due to unsatisfied condition(s): "${ignoreReasons.join( + '", "' + )}"` + ) ) } } // check if the rule file exists if (!Object.prototype.hasOwnProperty.call(Rules, r.ruleType)) { - return FormatResult.CreateError(r, `${r.ruleType} is not a valid rule`) + return Promise.resolve( + FormatResult.CreateError(r, `${r.ruleType} is not a valid rule`) + ) } let result try { @@ -330,9 +340,11 @@ async function runRuleset(ruleset, targets, fileSystem, dryRun) { // run the rule! result = await ruleFunc(fileSystem, r.ruleConfig) } catch (e) { - return FormatResult.CreateError( - r, - `${r.ruleType} threw an error: ${e.message}` + return Promise.resolve( + FormatResult.CreateError( + r, + `${r.ruleType} threw an error: ${e.message}` + ) ) } // generate fix targets @@ -341,27 +353,123 @@ async function runRuleset(ruleset, targets, fileSystem, dryRun) { : [] // if there's no fix or the rule passed, we're done if (!r.fixType || result.passed) { - return FormatResult.CreateLintOnly(r, result) + return Promise.resolve(FormatResult.CreateLintOnly(r, result)) } // else run the fix // check if the rule file exists if (!Object.prototype.hasOwnProperty.call(Fixes, r.fixType)) { - return FormatResult.CreateError(r, `${r.fixType} is not a valid fix`) + return Promise.resolve( + FormatResult.CreateError(r, `${r.fixType} is not a valid fix`) + ) } let fixresult try { const fixFunc = Fixes[r.fixType] fixresult = await fixFunc(fileSystem, r.fixConfig, fixTargets, dryRun) } catch (e) { - return FormatResult.CreateError( - r, - `${r.fixType} threw an error: ${e.message}` + return Promise.resolve( + FormatResult.CreateError(r, `${r.fixType} threw an error: ${e.message}`) ) } // all done! return the final format object - return FormatResult.CreateLintAndFix(r, result, fixresult) + return Promise.resolve(FormatResult.CreateLintAndFix(r, result, fixresult)) }) + await Promise.all(results) + for (let i = 0; i < sequentialRuleProcessingArrayList.length; i++) { + const r = sequentialRuleProcessingArrayList[i] + // check axioms and enable appropriately + if (r.level === 'off') { + results.push( + Promise.resolve( + FormatResult.CreateIgnored(r, 'ignored because level is "off"') + ) + ) + continue + } + // filter to only targets with no matches + if (typeof targets !== 'boolean' && r.where && r.where.length) { + const ignoreReasons = shouldRuleRun(targetArray, r.where) + if (ignoreReasons.length > 0) { + results.push( + Promise.resolve( + FormatResult.CreateIgnored( + r, + `ignored due to unsatisfied condition(s): "${ignoreReasons.join( + '", "' + )}"` + ) + ) + ) + continue + } + } + // check if the rule file exists + if (!Object.prototype.hasOwnProperty.call(Rules, r.ruleType)) { + results.push( + Promise.resolve( + FormatResult.CreateError(r, `${r.ruleType} is not a valid rule`) + ) + ) + continue + } + let result + try { + // load the rule + const ruleFunc = Rules[r.ruleType] + // run the rule! + result = await ruleFunc(fileSystem, r.ruleConfig) + } catch (e) { + results.push( + Promise.resolve( + FormatResult.CreateError( + r, + `${r.ruleType} threw an error: ${e.message}` + ) + ) + ) + continue + } + // generate fix targets + const fixTargets = !result.passed + ? result.targets.filter(t => !t.passed && t.path).map(t => t.path) + : [] + // if there's no fix or the rule passed, we're done + if (!r.fixType || result.passed) { + results.push(Promise.resolve(FormatResult.CreateLintOnly(r, result))) + continue + } + // else run the fix + // check if the rule file exists + if (!Object.prototype.hasOwnProperty.call(Fixes, r.fixType)) { + results.push( + Promise.resolve( + FormatResult.CreateError(r, `${r.fixType} is not a valid fix`) + ) + ) + continue + } + let fixresult + try { + const fixFunc = Fixes[r.fixType]() + fixresult = await fixFunc(fileSystem, r.fixConfig, fixTargets, dryRun) + } catch (e) { + results.push( + Promise.resolve( + FormatResult.CreateError( + r, + `${r.fixType} threw an error: ${e.message}` + ) + ) + ) + continue + } + // all done! return the final format object + results.push( + Promise.resolve(FormatResult.CreateLintAndFix(r, result, fixresult)) + ) + } + return Promise.all(results) } diff --git a/lib/config.js b/lib/config.js index b7056a6e..a1a5571c 100644 --- a/lib/config.js +++ b/lib/config.js @@ -190,7 +190,8 @@ function parseConfig(config) { cfg.fix && cfg.fix.type, cfg.fix && cfg.fix.options, cfg.policyInfo, - cfg.policyUrl + cfg.policyUrl, + cfg.sequentialOnly ) ) } diff --git a/lib/ruleinfo.js b/lib/ruleinfo.js index b98996f5..3af5c045 100644 --- a/lib/ruleinfo.js +++ b/lib/ruleinfo.js @@ -26,7 +26,8 @@ class RuleInfo { fixType, fixConfig, policyInfo, - policyUrl + policyUrl, + sequentialOnly ) { this.name = name this.level = level @@ -37,6 +38,7 @@ class RuleInfo { if (fixConfig) this.fixConfig = fixConfig if (policyInfo) this.policyInfo = policyInfo if (policyUrl) this.policyUrl = policyUrl + if (sequentialOnly) this.sequentialOnly = sequentialOnly } } diff --git a/rules/file-contents-config.json b/rules/file-contents-config.json index 784698d6..d6aa36f2 100644 --- a/rules/file-contents-config.json +++ b/rules/file-contents-config.json @@ -11,6 +11,12 @@ "type": "array", "items": { "type": "string" } }, + "branches": { + "type": "array", + "items": { "type": "string" }, + "default": [] + }, + "skipDefaultBranch": { "type": "boolean", "default": false }, "content": { "type": "string" }, "flags": { "type": "string" }, "human-readable-content": { "type": "string" }, diff --git a/rules/file-contents.js b/rules/file-contents.js index 89ed1a0c..2471f0fe 100644 --- a/rules/file-contents.js +++ b/rules/file-contents.js @@ -5,6 +5,7 @@ const Result = require('../lib/result') // eslint-disable-next-line no-unused-vars const FileSystem = require('../lib/file_system') +const simpleGit = require('simple-git') function getContent(options) { return options['human-readable-content'] !== undefined @@ -29,14 +30,20 @@ function getContext(matchedLine, regexMatch, contextLength) { * @param {FileSystem} fs A filesystem object configured with filter paths and target directories * @param {object} options The rule configuration * @param {boolean} not Whether or not to invert the result (not contents instead of contents) + * @param {boolean} any Whether to check if the regular expression is contained by at least one of the files in the list * @returns {Promise} The lint rule result * @ignore */ -async function fileContents(fs, options, not = false) { +async function fileContents(fs, options, not = false, any = false) { // support legacy configuration keys - const fileList = options.globsAll || options.files + const fileList = (any ? options.globsAny : options.globsAll) || options.files const files = await fs.findAllFiles(fileList, !!options.nocase) const regexFlags = options.flags || '' + const branchOptionEnabled = isBranchOptionEnabled(options) + + if (branchOptionEnabled) { + return await fileContentsWithBranchOption(fs, options, not, any, undefined) + } if (files.length === 0) { return new Result( @@ -272,8 +279,367 @@ async function fileContents(fs, options, not = false) { } const filteredResults = results.filter(r => r !== null) - const passed = !filteredResults.find(r => !r.passed) + const passed = any + ? filteredResults.some(r => r.passed) + : !filteredResults.find(r => !r.passed) + return new Result('', filteredResults, passed) +} + +/** + * Check if a list of files in one or more branches contains a regular expression. + * + * @param {FileSystem} fs A filesystem object configured with filter paths and target directories + * @param {object} options The rule configuration + * @param {boolean} not Whether or not to invert the result (not contents instead of contents) + * @param {boolean} any Whether to check if the regular expression is contained by at least one of the files in the list + * @param {SimpleGit} git A simple-git object configured correct path + * @returns {Promise} The lint rule result + * @ignore + */ +async function fileContentsWithBranchOption( + fs, + options, + not = false, + any = false, + git +) { + // support legacy configuration keys + const fileList = (any ? options.globsAny : options.globsAll) || options.files + const regexFlags = options.flags || '' + const regex = new RegExp(options.content, regexFlags) + + if (git === undefined) { + git = simpleGit({ + progress({ method, stage, progress }) { + console.log(`git.${method} ${stage} stage ${progress}% complete`) + }, + baseDir: fs.targetDir + }) + } + + const defaultBranch = (await git.branchLocal()).current + const branches = options.branches + if (!options.skipDefaultBranch) { + branches.unshift(defaultBranch) + } + const defaultRemote = (await git.getRemotes())[0] + await fetchAllBranchesRemote(git, defaultRemote.name) + + let results = [] + let noMatchingFileFoundCount = 0 + let switchedBranch = false + for (let index = 0; index < branches.length; index++) { + const branch = branches[index] + if ( + !(await doesBranchExist(git, branch)) && + !(await doesBranchExist(git, `${defaultRemote.name}/${branch}`)) + ) { + noMatchingFileFoundCount++ + continue + } + // if branch name is the default branch from clone, ignore and do not checkout. + if (branch !== defaultBranch) { + // perform git checkout of the target branch + await gitCheckout(git, branch, defaultRemote.name) + switchedBranch = true + } + const files = await fs.findAllFiles(fileList, !!options.nocase) + if (files.length === 0) { + noMatchingFileFoundCount++ + continue + } + if (!options['display-result-context']) { + /** + * Default "Contains" / "Doesn't contain" + * @ignore + */ + results = results.concat( + await Promise.all( + files.map(async file => { + const fileContents = await fs.getFileContents(file) + if (!fileContents) return null + + const passed = fileContents.search(regex) >= 0 + const message = `${ + passed ? 'Contains' : "Doesn't contain" + } ${getContent(options)}` + + return { + passed: not ? !passed : passed, + path: file, + message + } + }) + ) + ) + } else { + /** + * Add regular expression matched content context into result. + * Added contexts includes: + * - line # of the regular expression. + * - 'options.context-char-length' number of characters before and after the regex match. + * The added context will be in result.message. + * + * Note: if 'g' is not presented in 'options.flags', + * the regular expression will only display the first match context. + * @ignore + */ + results = results + .concat( + await Promise.all( + files.map(async file => { + const fileContents = await fs.getFileContents(file) + if (!fileContents) return null + + const optionContextCharLength = + options['context-char-length'] || 50 + const split = fileContents.split(regex) + const regexHasMatch = split.length > 1 + if (!regexHasMatch) { + return { + passed: not ? !regexHasMatch : regexHasMatch, + path: file, + contextLines: [], + message: `Doesn't contain '${getContent(options)}'` + } + } + + const fileLines = fileContents.split('\n') + const contextLines = split + /** + * @return sum of line numbers in each regexp split chunks. + * @ignore + */ + .map(fileChunk => { + /** + * Note: Handle *undefined* in regex split result issue + * by treating *undefined* as '' + * @ignore + */ + if (fileChunk !== undefined) + return fileChunk.split('\n').length + return 1 + }) + /** + * Get lines of regexp match + * @return list of lines contains regexp matchs + * @ignore + */ + .reduce((previous, current, index, array) => { + /** + * Push number of lines before the first regex match to the result array. + * @ignore + */ + if (previous.length === 0) { + previous.push(current) + } else if (current === 1 || index === array.length - 1) { + /** + * We don't need to count multiple times if one line contains multiple regex match. + * We don't need to count rest of lines after last regex match. + * @ignore + */ + } else { + /** + * Add *relative number of lines* between this regex match and last regex match (current-1) + * to the last *absolute number of lines* of last regex match to the top of file (previous[lastElement]) + * to get the *absolute number of lines* of current regex match. + * @ignore + */ + previous.push(current - 1 + previous[previous.length - 1]) + } + return previous + }, []) + /** + * @return lines and contexts of every regex matches. + * @ignore + */ + .reduce((previous, current) => { + const matchedLine = fileLines[current - 1] + /** + * We can't do multi-line match on a single line context, + * so we try to detect a match on the line + * and print helpful info if there is none. + * + * Note: multi-line output context can be challenging to read. + * So instead of print unpredictable context in the output, + * we just print line number. + * @ignore + */ + if (regexFlags.includes('m')) { + let currentMatch = regex.exec(matchedLine) + + /** + * Found no match, the regex match was multi-line. + * Print info in context instead of actual context. + * @ignore + */ + if (currentMatch === null) { + previous.push({ + line: current, + context: + '-- This is a multi-line regex match so we only displaying line number --' + }) + return previous + } + /** + * Find a match, so we try to find all matches. + * Reset regex.lastIndex to start from beginning. + * @ignore + */ + regex.lastIndex = 0 + while ((currentMatch = regex.exec(matchedLine)) !== null) { + previous.push({ + line: current, + context: getContext( + matchedLine, + currentMatch, + optionContextCharLength + ) + }) + if (regex.lastIndex === 0) break + } + return previous + } + + /** + * No *global* flag means regex.lastIndex will not advance. + * We just need to run regex.exec once + * @ignore + */ + if (!regexFlags.includes('g')) { + const currentMatch = regex.exec(matchedLine) + /** + * Found a match! Put it in the result + * @ignore + */ + if (currentMatch != null) { + previous.push({ + line: current, + context: getContext( + matchedLine, + currentMatch, + optionContextCharLength + ) + }) + return previous + } + /** + * User should never reach here, throw an error when that happens. + * @ignore + */ + console.trace('Error trace:') + throw new Error( + 'Please open an issue on https://github.com/todogroup/repolinter' + ) + } + + /** + * Find all matches on the string with non-multi-line regex + * @ignore + */ + let currentMatch + while ((currentMatch = regex.exec(matchedLine)) !== null) { + previous.push({ + line: current, + context: getContext( + matchedLine, + currentMatch, + optionContextCharLength + ) + }) + } + return previous + }, []) + + return { + passed: not ? !regexHasMatch : regexHasMatch, + path: file, + contextLines, + message: `Contains '${getContent(options)}'` + } + }) + ) + ) + .filter(result => result && (not ? !result.passed : result.passed)) + .reduce((previous, current) => { + current.contextLines.forEach(lineContext => { + previous.push({ + passed: current.passed, + path: current.path, + message: `${current.message} on line ${lineContext.line}, context: \n\t|${lineContext.context}` + }) + }) + return previous + }, []) + } + } + if (switchedBranch) { + // Make sure we are back using the default branch + await gitCheckout(git, defaultBranch, defaultRemote.name) + } + + if (noMatchingFileFoundCount === branches.length) { + return new Result( + 'Did not find file matching the specified patterns', + fileList.map(f => { + return { passed: false, pattern: f } + }), + !options['fail-on-non-existent'] + ) + } + + const filteredResults = results.filter(r => r !== null) + const passed = any + ? filteredResults.some(r => r.passed) + : !filteredResults.find(r => !r.passed) return new Result('', filteredResults, passed) } module.exports = fileContents + +// isBranchesOptionEnabled returns true if the branches option is enabled. +function isBranchOptionEnabled(options) { + if ( + options.branches !== undefined && + options.branches !== null && + options.branches !== [] && + options.branches.length > 0 + ) { + return true + } + return false +} + +// Fetch all remote branches, fetches just the names on remote. +// Needs to be done since we did a shallow checkout +async function fetchAllBranchesRemote(git, defaultRemote) { + // Since we do a shallow clone, we need to first retrieve the branches + await git.addConfig( + `remote.${defaultRemote}.fetch`, + `+refs/heads/*:refs/remotes/${defaultRemote}/*` + ) + await git.remote(['update']) +} + +// Check if branch exists +async function doesBranchExist(git, branch) { + const branches = (await git.branch(['-r'])).all + if (branches.find(v => v === branch)) { + return true + } + return false +} +// Helper method to quickly checkout to a different branch +async function gitCheckout(git, branch, defaultRemote) { + const checkoutResult = await git.checkout(branch) + if (checkoutResult) { + const checkoutResultWithDefaultOrigin = await git.checkout( + `${defaultRemote}/${branch}` + ) + if (checkoutResultWithDefaultOrigin) { + console.error(checkoutResult) + process.exitCode = 1 + throw new Error(`Failed checking out branch: ${defaultRemote}/${branch}`) + } + } +} diff --git a/rules/file-not-contents-config.json b/rules/file-not-contents-config.json index 24f5fd1c..b0e8115a 100644 --- a/rules/file-not-contents-config.json +++ b/rules/file-not-contents-config.json @@ -18,6 +18,12 @@ "type": "string" } }, + "branches": { + "type": "array", + "items": { "type": "string" }, + "default": [] + }, + "skipDefaultBranch": { "type": "boolean", "default": false }, "flags": { "type": "string" }, "human-readable-content": { "type": "string" }, "fail-on-non-existent": { diff --git a/rulesets/schema.json b/rulesets/schema.json index 8c469454..af5248a5 100644 --- a/rulesets/schema.json +++ b/rulesets/schema.json @@ -42,6 +42,10 @@ "additionalProperties": false, "properties": { "level": { "enum": ["off", "warning", "error"] }, + "sequentialOnly": { + "type": "boolean", + "default": false + }, "where": { "type": "array", "items": { "type": "string" } diff --git a/tests/rules/file_contents_tests.js b/tests/rules/file_contents_tests.js index b1108081..83ca8ad3 100644 --- a/tests/rules/file_contents_tests.js +++ b/tests/rules/file_contents_tests.js @@ -4,11 +4,53 @@ const chai = require('chai') const expect = chai.expect const FileSystem = require('../../lib/file_system') +const cp = require('child_process') +const path = require('path') +const fileContents = require('../../rules/file-contents') + +/** + * Execute a command in a childprocess asynchronously. Not secure, but good for testing. + * + * @param {string} command The command to execute + * @param {import('child_process').ExecOptions} [opts] Options to execute against. + * @returns {Promise<{out: string, err: string, code: number}>} The command output + */ +async function execAsync(command, opts = {}) { + return new Promise((resolve, reject) => { + cp.exec(command, opts, (err, outstd, errstd) => + err !== null && err.code === undefined + ? reject(err) + : resolve({ + out: outstd, + err: errstd, + code: err !== null ? err.code : 0 + }) + ) + }) +} describe('rule', () => { describe('files_contents', () => { - const fileContents = require('../../rules/file-contents') - + const mockGit = { + branchLocal() { + return { current: 'master' } + }, + getRemotes() { + return [{ name: 'origin' }] + }, + addConfig() { + return Promise.resolve + }, + remote() { + return Promise.resolve + }, + branch() { + return { all: ['master'] } + }, + checkout() { + return Promise.resolve + } + } it('returns passes if requested file contents exists', async () => { /** @type {any} */ const mockfs = { @@ -26,7 +68,13 @@ describe('rule', () => { content: 'foo' } - const actual = await fileContents(mockfs, ruleopts) + const actual = await fileContents( + mockfs, + ruleopts, + undefined, + undefined, + mockGit + ) expect(actual.passed).to.equal(true) expect(actual.targets).to.have.length(1) expect(actual.targets[0]).to.deep.include({ @@ -112,7 +160,13 @@ describe('rule', () => { 'human-readable-content': 'actually foo' } - const actual = await fileContents(mockfs, ruleopts) + const actual = await fileContents( + mockfs, + ruleopts, + undefined, + undefined, + mockGit + ) expect(actual.passed).to.equal(true) expect(actual.targets).to.have.length(1) expect(actual.targets[0]).to.deep.include({ @@ -141,7 +195,13 @@ describe('rule', () => { content: 'bar' } - const actual = await fileContents(mockfs, ruleopts) + const actual = await fileContents( + mockfs, + ruleopts, + undefined, + undefined, + mockGit + ) expect(actual.passed).to.equal(false) expect(actual.targets).to.have.length(1) @@ -166,7 +226,13 @@ describe('rule', () => { content: 'foo' } - const actual = await fileContents(mockfs, ruleopts) + const actual = await fileContents( + mockfs, + ruleopts, + undefined, + undefined, + mockGit + ) expect(actual.passed).to.equal(true) expect(actual.targets).to.have.length(1) expect(actual.targets[0].passed).to.equal(true) @@ -188,7 +254,13 @@ describe('rule', () => { 'fail-on-non-existent': true } - const actual = await fileContents(mockfs, ruleopts) + const actual = await fileContents( + mockfs, + ruleopts, + undefined, + undefined, + mockGit + ) expect(actual.passed).to.equal(false) }) @@ -204,8 +276,121 @@ describe('rule', () => { lineCount: 1, patterns: ['something'] } - const actual = await fileContents(fs, rule) + const actual = await fileContents(fs, rule, undefined, undefined, mockGit) + expect(actual.passed).to.equal(true) }) }) + describe('rule file_contents in branch', function () { + const repolinterPath = + process.platform === 'win32' + ? path.resolve('bin/repolinter.bat') + : path.resolve('bin/repolinter.js') + this.timeout(30000) + describe('with incorrectly configured branches option', () => { + it('it should ignore the value and continue', async () => { + /** @type {any} */ + const mockfs = { + findAllFiles() { + return ['README.md'] + }, + getFileContents() { + return 'foo' + }, + targetDir: '.' + } + // Check undefined option + let ruleopts = { + globsAll: ['README*'], + content: 'foo', + branches: undefined + } + + let actual = await fileContents(mockfs, ruleopts, undefined, undefined) + expect(actual.passed).to.equal(true) + expect(actual.targets).to.have.length(1) + expect(actual.targets[0]).to.deep.include({ + passed: true, + path: 'README.md' + }) + + // Check empty array + ruleopts = { + globsAll: ['README*'], + content: 'foo', + branches: [] + } + + actual = await fileContents(mockfs, ruleopts, undefined, undefined) + expect(actual.passed).to.equal(true) + expect(actual.targets).to.have.length(1) + expect(actual.targets[0]).to.deep.include({ + passed: true, + path: 'README.md' + }) + // Check null option + ruleopts = { + globsAll: ['README*'], + content: 'foo', + branches: null + } + + actual = await fileContents(mockfs, ruleopts, undefined, undefined) + expect(actual.passed).to.equal(true) + expect(actual.targets).to.have.length(1) + expect(actual.targets[0]).to.deep.include({ + passed: true, + path: 'README.md' + }) + }) + }) + describe('when checking only default branch', () => { + it('returned content should not find content from different branches', async () => { + const actual = await execAsync( + `${repolinterPath} lint -g https://github.com/Brend-Smits/repolinter-tests.git --rulesetFile rulesets/file-content-default-branch.json` + ) + + expect(actual.code).to.equal(0) + expect(actual.out.trim()).to.contain('Lint:') + expect(actual.out.trim()).to.contain('Contains MAINCONTENT') + }) + it('returns error if content is not found', async () => { + const actual = await execAsync( + `${repolinterPath} lint -g https://github.com/Brend-Smits/repolinter-tests.git --rulesetFile rulesets/file-content-default-branch-should-not-find.json` + ) + + expect(actual.code).to.equal(1) + expect(actual.out.trim()).to.contain('Lint:') + expect(actual.out.trim()).to.contain("Doesn't contain SECONDARYCONTENT") + }) + }) + describe('when checking various branches', () => { + it('returns content from both default and target branch', async () => { + const actual = await execAsync( + `${repolinterPath} lint -g https://github.com/Brend-Smits/repolinter-tests.git --rulesetFile rulesets/any-content-check-branch.json` + ) + + expect(actual.code).to.equal(0) + expect(actual.out.trim()).to.contain('Lint:') + expect(actual.out.trim()).to.contain('Contains MAINCONTENT') + }) + it('returns matched content from different branch that is not default', async () => { + const actual = await execAsync( + `${repolinterPath} lint -g https://github.com/Brend-Smits/repolinter-tests.git --rulesetFile rulesets/any-content-check-only-target-branch.json` + ) + + expect(actual.code).to.equal(0) + expect(actual.out.trim()).to.contain('Lint:') + expect(actual.out.trim()).to.contain('Contains SECONDARYCONTENT') + }) + it('returns matched content from all other branches', async () => { + const actual = await execAsync( + `${repolinterPath} lint -g https://github.com/Brend-Smits/repolinter-tests.git --rulesetFile rulesets/any-content-check-all-other-branches.json` + ) + expect(actual.code).to.equal(0) + expect(actual.out.trim()).to.contain('Lint:') + expect(actual.out.trim()).to.contain('Contains SECONDARYCONTENT') + }) + }) + }) })