31 Commits

Author SHA1 Message Date
Michal Dorner
87375a4a68 Merge pull request #84 from dorny/v2.10
Version v2.10.0
2021-04-11 21:54:12 +02:00
Michal Dorner
24a74833cc Update CHANGELOG for v2.10.0 2021-04-11 21:48:05 +02:00
Michal Dorner
d0507d9a8a Improve log output 2021-04-11 21:39:09 +02:00
Michal Dorner
f093f3520b Merge pull request #83 from dorny/issue-81-gh-api-reports-no-changes
Fix change detection in PR when pullRequest.changed_files is incorrect
2021-04-11 21:23:46 +02:00
Michal Dorner
8d029eb508 Fix change detection in PR when pullRequest.changed_files was incorrect 2021-04-11 21:15:30 +02:00
Michal Dorner
07d6abdb9c Merge pull request #82 from dorny/issue-79-add-head-ref-input
Add ref input parameter
2021-04-11 21:00:00 +02:00
Michal Dorner
e8f370c197 Run PR workflow on PR to any branch 2021-04-11 20:58:42 +02:00
Michal Dorner
58ed00ec48 Update README 2021-04-11 20:49:05 +02:00
Michal Dorner
02eeef4973 Fix undefined instead of ref in log output 2021-04-07 22:41:22 +02:00
Michal Dorner
37a6d38b2d Add ref input parameter 2021-04-07 22:32:59 +02:00
Michal Dorner
208adf42c8 Ignore error when some tag on remote was updated 2021-03-29 17:09:18 +02:00
Michal Dorner
ad1ae68cd0 Merge pull request #78 from dorny/issue-77-tag-as-base-broken
Fix change detection when base is a tag
2021-03-26 00:18:14 +01:00
Michal Dorner
5d414b88ab Update CHANGELOG for v2.9.3 2021-03-26 00:14:59 +01:00
Michal Dorner
a6989ad592 Get full ref name without multiple invocations to git show-ref 2021-03-26 00:05:48 +01:00
Michal Dorner
6d8169070c Improve logging 2021-03-25 23:45:44 +01:00
Michal Dorner
3d4a25053b Update dist 2021-03-25 23:39:10 +01:00
Michal Dorner
e59197f91b Fix change detection when base is tag 2021-03-25 23:34:50 +01:00
Michal Dorner
ca8fa4002c Merge pull request #75 from dorny/fix-searching-for-merge-base
Fetch base and search merge-base without creating local branch
2021-03-09 22:05:03 +01:00
Michal Dorner
c64be944bf Update CHANGELOG 2021-03-09 22:01:10 +01:00
Michal Dorner
138368ff4f Use ref instead of HEAD 2021-03-09 21:56:18 +01:00
Michal Dorner
a301a0ad83 Refactor getChangesSinceMergeBase() code 2021-03-09 21:44:15 +01:00
Michal Dorner
0c0d1a854a Fetch base and search merge-base without creating local branch 2021-03-09 21:13:57 +01:00
Michal Dorner
0aa1597c2b Merge pull request #74 from dorny/fix-searching-for-merge-base
Fix fetching git history + fallback to unshallow repo
2021-03-08 17:11:16 +01:00
Michal Dorner
46d2898cef Update CHANGELOG 2021-03-08 17:10:14 +01:00
Michal Dorner
c90ecaa5a1 Increase default value of initial-fetch-depth to 100
For most situation it should be enough to find merge base. Previous value was too slow and overhead of doing fetch was significantly higher than saving of transfer.
2021-03-08 17:06:12 +01:00
Michal Dorner
49abb091ed Use combination of --depth and --deepen 2021-03-08 17:00:52 +01:00
Michal Dorner
8801c887e9 Do not try to update head of current branch 2021-03-08 15:29:27 +01:00
Michal Dorner
68792bf56a Allow single line filters 2021-03-08 15:24:06 +01:00
Michal Dorner
31c576896e Fix lastCommitCount has not been updated 2021-03-08 15:12:58 +01:00
Michal Dorner
3be8c93277 Fix fetching git history + fallback to unshallow repo 2021-03-08 15:09:07 +01:00
Michal Dorner
1cdd3bbdf6 Fix typo in README 2021-02-24 11:24:20 +01:00
7 changed files with 339 additions and 195 deletions

View File

@@ -4,7 +4,7 @@ on:
paths-ignore: [ '*.md' ]
branches:
- master
- develop
- '**'
jobs:
build:

View File

@@ -1,5 +1,18 @@
# Changelog
## v2.10.0
- [Add ref input parameter](https://github.com/dorny/paths-filter/pull/82)
- [Fix change detection in PR when pullRequest.changed_files is incorrect](https://github.com/dorny/paths-filter/pull/83)
## v2.9.3
- [Fix change detection when base is a tag](https://github.com/dorny/paths-filter/pull/78)
## v2.9.2
- [Fix fetching git history](https://github.com/dorny/paths-filter/pull/75)
## v2.9.1
- [Fix fetching git history + fallback to unshallow repo](https://github.com/dorny/paths-filter/pull/74)
## v2.9.0
- [Add list-files: csv format](https://github.com/dorny/paths-filter/pull/68)

View File

@@ -66,13 +66,11 @@ For more scenarios see [examples](#examples) section.
# What's New
- Add `ref` input parameter
- Add `list-files: csv` format
- Configure matrix job to run for each folder with changes using `changes` output
- Improved listing of matching files with `list-files: shell` and `list-files: escape` options
- Support local changes
- Fixed retrieval of all changes via Github API when there are 100+ changes
- Paths expressions are now evaluated using [picomatch](https://github.com/micromatch/picomatch) library
- Support workflows triggered by any event
For more information see [CHANGELOG](https://github.com/dorny/paths-filter/blob/master/CHANGELOG.md)
@@ -111,13 +109,20 @@ For more information see [CHANGELOG](https://github.com/dorny/paths-filter/blob/
# Default: repository default branch (e.g. master)
base: ''
# Git reference (e.g. branch name) from which the changes will be detected.
# Useful when workflow can be triggered only on default branch (e.g. repository_dispatch event)
# but you want to get changes on different branch.
# This option is ignored if action is triggered by pull_request event.
# default: ${{ github.ref }}
ref:
# How many commits are initially fetched from base branch.
# If needed, each subsequent fetch doubles the
# previously requested number of commits until the merge-base
# is found or there are no more commits in the history.
# This option takes effect only when changes are detected
# using git against base branch (feature branch workflow).
# Default: 20
# Default: 100
initial-fetch-depth: ''
# Enables listing of files matching the filter:
@@ -262,7 +267,7 @@ jobs:
matrix:
# Parse JSON array containing names of all filters matching any of changed files
# e.g. ['package1', 'package2'] if both package folders contains changes
package: ${{ fromJson(needs.changes.outputs.packages) }}
package: ${{ fromJSON(needs.changes.outputs.packages) }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2

View File

@@ -9,6 +9,12 @@ inputs:
working-directory:
description: 'Relative path under $GITHUB_WORKSPACE where the repository was checked out.'
required: false
ref:
description: |
Git reference (e.g. branch name) from which the changes will be detected.
This option is ignored if action is triggered by pull_request event.
default: ${{ github.ref }}
required: false
base:
description: |
Git reference (e.g. branch name) against which the changes will be detected. Defaults to repository default branch (e.g. master).
@@ -38,7 +44,7 @@ inputs:
until the merge-base is found or there are no more commits in the history.
This option takes effect only when changes are detected using git against different base branch.
required: false
default: '10'
default: '100'
outputs:
changes:
description: JSON array with names of all filters matching any of changed files

240
dist/index.js vendored
View File

@@ -3830,19 +3830,19 @@ async function getChangesInLastCommit() {
return parseGitDiffOutput(output);
}
exports.getChangesInLastCommit = getChangesInLastCommit;
async function getChanges(ref) {
if (!(await hasCommit(ref))) {
async function getChanges(baseRef) {
if (!(await hasCommit(baseRef))) {
// Fetch single commit
core.startGroup(`Fetching ${ref} from origin`);
await exec_1.default('git', ['fetch', '--depth=1', '--no-tags', 'origin', ref]);
core.startGroup(`Fetching ${baseRef} from origin`);
await exec_1.default('git', ['fetch', '--depth=1', '--no-tags', 'origin', baseRef]);
core.endGroup();
}
// Get differences between ref and HEAD
core.startGroup(`Change detection ${ref}..HEAD`);
core.startGroup(`Change detection ${baseRef}..HEAD`);
let output = '';
try {
// Two dots '..' change detection - directly compares two versions
output = (await exec_1.default('git', ['diff', '--no-renames', '--name-status', '-z', `${ref}..HEAD`])).stdout;
output = (await exec_1.default('git', ['diff', '--no-renames', '--name-status', '-z', `${baseRef}..HEAD`])).stdout;
}
finally {
fixStdOutNullTermination();
@@ -3865,47 +3865,72 @@ async function getChangesOnHead() {
return parseGitDiffOutput(output);
}
exports.getChangesOnHead = getChangesOnHead;
async function getChangesSinceMergeBase(ref, initialFetchDepth) {
if (!(await hasCommit(ref))) {
// Fetch and add base branch
core.startGroup(`Fetching ${ref}`);
try {
await exec_1.default('git', ['fetch', `--depth=${initialFetchDepth}`, '--no-tags', 'origin', `${ref}:${ref}`]);
}
finally {
core.endGroup();
}
}
async function getChangesSinceMergeBase(base, head, initialFetchDepth) {
let baseRef;
let headRef;
async function hasMergeBase() {
return (await exec_1.default('git', ['merge-base', ref, 'HEAD'], { ignoreReturnCode: true })).code === 0;
if (baseRef === undefined || headRef === undefined) {
return false;
}
return (await exec_1.default('git', ['merge-base', baseRef, headRef], { ignoreReturnCode: true })).code === 0;
}
async function countCommits() {
return (await getNumberOfCommits('HEAD')) + (await getNumberOfCommits(ref));
}
core.startGroup(`Searching for merge-base with ${ref}`);
// Fetch more commits until merge-base is found
if (!(await hasMergeBase())) {
let deepen = initialFetchDepth;
let lastCommitsCount = await countCommits();
do {
await exec_1.default('git', ['fetch', `--deepen=${deepen}`, '--no-tags']);
const count = await countCommits();
if (count <= lastCommitsCount) {
core.info('No merge base found - all files will be listed as added');
core.endGroup();
return await listAllFilesAsAdded();
let noMergeBase = false;
core.startGroup(`Searching for merge-base ${base}...${head}`);
try {
baseRef = await getFullRef(base);
headRef = await getFullRef(head);
if (!(await hasMergeBase())) {
await exec_1.default('git', ['fetch', '--no-tags', `--depth=${initialFetchDepth}`, 'origin', base, head]);
if (baseRef === undefined || headRef === undefined) {
baseRef = baseRef !== null && baseRef !== void 0 ? baseRef : (await getFullRef(base));
headRef = headRef !== null && headRef !== void 0 ? headRef : (await getFullRef(head));
if (baseRef === undefined || headRef === undefined) {
await exec_1.default('git', ['fetch', '--tags', '--depth=1', 'origin', base, head], {
ignoreReturnCode: true // returns exit code 1 if tags on remote were updated - we can safely ignore it
});
baseRef = baseRef !== null && baseRef !== void 0 ? baseRef : (await getFullRef(base));
headRef = headRef !== null && headRef !== void 0 ? headRef : (await getFullRef(head));
if (baseRef === undefined) {
throw new Error(`Could not determine what is ${base} - fetch works but it's not a branch or tag`);
}
if (headRef === undefined) {
throw new Error(`Could not determine what is ${head} - fetch works but it's not a branch or tag`);
}
}
}
lastCommitsCount = count;
deepen = Math.min(deepen * 2, Number.MAX_SAFE_INTEGER);
} while (!(await hasMergeBase()));
let depth = initialFetchDepth;
let lastCommitCount = await getCommitCount();
while (!(await hasMergeBase())) {
depth = Math.min(depth * 2, Number.MAX_SAFE_INTEGER);
await exec_1.default('git', ['fetch', `--deepen=${depth}`, 'origin', base, head]);
const commitCount = await getCommitCount();
if (commitCount === lastCommitCount) {
core.info('No more commits were fetched');
core.info('Last attempt will be to fetch full history');
await exec_1.default('git', ['fetch']);
if (!(await hasMergeBase())) {
noMergeBase = true;
}
break;
}
lastCommitCount = commitCount;
}
}
}
core.endGroup();
// Get changes introduced on HEAD compared to ref
core.startGroup(`Change detection ${ref}...HEAD`);
finally {
core.endGroup();
}
// Three dots '...' change detection - finds merge-base and compares against it
let diffArg = `${baseRef}...${headRef}`;
if (noMergeBase) {
core.warning('No merge base found - change detection will use direct <commit>..<commit> comparison');
diffArg = `${baseRef}..${headRef}`;
}
// Get changes introduced on ref compared to base
core.startGroup(`Change detection ${diffArg}`);
let output = '';
try {
// Three dots '...' change detection - finds merge-base and compares against it
output = (await exec_1.default('git', ['diff', '--no-renames', '--name-status', '-z', `${ref}...HEAD`])).stdout;
output = (await exec_1.default('git', ['diff', '--no-renames', '--name-status', '-z', diffArg])).stdout;
}
finally {
fixStdOutNullTermination();
@@ -3956,7 +3981,7 @@ async function getCurrentRef() {
if (describe.code === 0) {
return describe.stdout.trim();
}
return (await exec_1.default('git', ['rev-parse', 'HEAD'])).stdout.trim();
return (await exec_1.default('git', ['rev-parse', exports.HEAD])).stdout.trim();
}
finally {
core.endGroup();
@@ -3988,11 +4013,29 @@ async function hasCommit(ref) {
core.endGroup();
}
}
async function getNumberOfCommits(ref) {
const output = (await exec_1.default('git', ['rev-list', `--count`, ref])).stdout;
async function getCommitCount() {
const output = (await exec_1.default('git', ['rev-list', '--count', '--all'])).stdout;
const count = parseInt(output);
return isNaN(count) ? 0 : count;
}
async function getFullRef(shortName) {
if (isGitSha(shortName)) {
return shortName;
}
const output = (await exec_1.default('git', ['show-ref', shortName], { ignoreReturnCode: true })).stdout;
const refs = output
.split(/\r?\n/g)
.map(l => { var _a, _b; return (_b = (_a = l.match(/refs\/.*$/)) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : ''; })
.filter(l => l !== '');
if (refs.length === 0) {
return undefined;
}
const remoteRef = refs.find(ref => ref.startsWith('refs/remotes/origin/'));
if (remoteRef) {
return remoteRef;
}
return refs[0];
}
function fixStdOutNullTermination() {
// Previous command uses NULL as delimiters and output is printed to stdout.
// We have to make sure next thing written to stdout will start on new line.
@@ -4657,6 +4700,7 @@ async function run() {
process.chdir(workingDirectory);
}
const token = core.getInput('token', { required: false });
const ref = core.getInput('ref', { required: false });
const base = core.getInput('base', { required: false });
const filtersInput = core.getInput('filters', { required: true });
const filtersYaml = isPathInput(filtersInput) ? getConfigFileContent(filtersInput) : filtersInput;
@@ -4667,7 +4711,8 @@ async function run() {
return;
}
const filter = new filter_1.Filter(filtersYaml);
const files = await getChangedFiles(token, base, initialFetchDepth);
const files = await getChangedFiles(token, base, ref, initialFetchDepth);
core.info(`Detected ${files.length} changed files`);
const results = filter.match(files);
exportResults(results, listFiles);
}
@@ -4676,7 +4721,7 @@ async function run() {
}
}
function isPathInput(text) {
return !text.includes('\n');
return !(text.includes('\n') || text.includes(':'));
}
function getConfigFileContent(configPath) {
if (!fs.existsSync(configPath)) {
@@ -4687,7 +4732,7 @@ function getConfigFileContent(configPath) {
}
return fs.readFileSync(configPath, { encoding: 'utf8' });
}
async function getChangedFiles(token, base, initialFetchDepth) {
async function getChangedFiles(token, base, ref, initialFetchDepth) {
// if base is 'HEAD' only local uncommitted changes will be detected
// This is the simplest case as we don't need to fetch more commits or evaluate current/before refs
if (base === git.HEAD) {
@@ -4702,14 +4747,14 @@ async function getChangedFiles(token, base, initialFetchDepth) {
return await git.getChangesInLastCommit();
}
else {
return getChangedFilesFromGit(base, initialFetchDepth);
return getChangedFilesFromGit(base, ref, initialFetchDepth);
}
}
async function getChangedFilesFromGit(base, initialFetchDepth) {
async function getChangedFilesFromGit(base, head, initialFetchDepth) {
var _a;
const defaultRef = (_a = github.context.payload.repository) === null || _a === void 0 ? void 0 : _a.default_branch;
const beforeSha = github.context.eventName === 'push' ? github.context.payload.before : null;
const pushRef = git.getShortName(github.context.ref) ||
const ref = git.getShortName(head || github.context.ref) ||
(core.warning(`'ref' field is missing in event payload - using current branch, tag or commit SHA`),
await git.getCurrentRef());
const baseRef = git.getShortName(base) || defaultRef;
@@ -4717,10 +4762,10 @@ async function getChangedFilesFromGit(base, initialFetchDepth) {
throw new Error("This action requires 'base' input to be configured or 'repository.default_branch' to be set in the event payload");
}
const isBaseRefSha = git.isGitSha(baseRef);
const isBaseSameAsPush = baseRef === pushRef;
const isBaseRefSameAsRef = baseRef === ref;
// If base is commit SHA we will do comparison against the referenced commit
// Or if base references same branch it was pushed to, we will do comparison against the previously pushed commit
if (isBaseRefSha || isBaseSameAsPush) {
if (isBaseRefSha || isBaseRefSameAsRef) {
if (!isBaseRefSha && !beforeSha) {
core.warning(`'before' field is missing in event payload - changes will be detected from last commit`);
return await git.getChangesInLastCommit();
@@ -4731,7 +4776,7 @@ async function getChangedFilesFromGit(base, initialFetchDepth) {
if (baseSha === git.NULL_SHA) {
if (defaultRef && baseRef !== defaultRef) {
core.info(`First push of a branch detected - changes will be detected against the default branch ${defaultRef}`);
return await git.getChangesSinceMergeBase(defaultRef, initialFetchDepth);
return await git.getChangesSinceMergeBase(defaultRef, ref, initialFetchDepth);
}
else {
core.info('Initial push detected - all files will be listed as added');
@@ -4742,53 +4787,64 @@ async function getChangedFilesFromGit(base, initialFetchDepth) {
return await git.getChanges(baseSha);
}
// Changes introduced by current branch against the base branch
core.info(`Changes will be detected against the branch ${baseRef}`);
return await git.getChangesSinceMergeBase(baseRef, initialFetchDepth);
core.info(`Changes will be detected against ${baseRef}`);
return await git.getChangesSinceMergeBase(baseRef, ref, initialFetchDepth);
}
// Uses github REST api to get list of files changed in PR
async function getChangedFilesFromApi(token, pullRequest) {
core.startGroup(`Fetching list of changed files for PR#${pullRequest.number} from Github API`);
core.info(`Number of changed_files is ${pullRequest.changed_files}`);
const client = new github.GitHub(token);
const pageSize = 100;
const files = [];
for (let page = 1; (page - 1) * pageSize < pullRequest.changed_files; page++) {
core.info(`Invoking listFiles(pull_number: ${pullRequest.number}, page: ${page}, per_page: ${pageSize})`);
const response = await client.pulls.listFiles({
owner: github.context.repo.owner,
repo: github.context.repo.repo,
pull_number: pullRequest.number,
page,
per_page: pageSize
});
for (const row of response.data) {
core.info(`[${row.status}] ${row.filename}`);
// There's no obvious use-case for detection of renames
// Therefore we treat it as if rename detection in git diff was turned off.
// Rename is replaced by delete of original filename and add of new filename
if (row.status === file_1.ChangeStatus.Renamed) {
files.push({
filename: row.filename,
status: file_1.ChangeStatus.Added
});
files.push({
// 'previous_filename' for some unknown reason isn't in the type definition or documentation
filename: row.previous_filename,
status: file_1.ChangeStatus.Deleted
});
try {
const client = new github.GitHub(token);
const per_page = 100;
const files = [];
for (let page = 1;; page++) {
core.info(`Invoking listFiles(pull_number: ${pullRequest.number}, page: ${page}, per_page: ${per_page})`);
const response = await client.pulls.listFiles({
owner: github.context.repo.owner,
repo: github.context.repo.repo,
pull_number: pullRequest.number,
per_page,
page
});
if (response.status !== 200) {
throw new Error(`Fetching list of changed files from GitHub API failed with error code ${response.status}`);
}
else {
// Github status and git status variants are same except for deleted files
const status = row.status === 'removed' ? file_1.ChangeStatus.Deleted : row.status;
files.push({
filename: row.filename,
status
});
core.info(`Received ${response.data.length} items`);
if (response.data.length === 0) {
core.info('All changed files has been fetched from GitHub API');
break;
}
for (const row of response.data) {
core.info(`[${row.status}] ${row.filename}`);
// There's no obvious use-case for detection of renames
// Therefore we treat it as if rename detection in git diff was turned off.
// Rename is replaced by delete of original filename and add of new filename
if (row.status === file_1.ChangeStatus.Renamed) {
files.push({
filename: row.filename,
status: file_1.ChangeStatus.Added
});
files.push({
// 'previous_filename' for some unknown reason isn't in the type definition or documentation
filename: row.previous_filename,
status: file_1.ChangeStatus.Deleted
});
}
else {
// Github status and git status variants are same except for deleted files
const status = row.status === 'removed' ? file_1.ChangeStatus.Deleted : row.status;
files.push({
filename: row.filename,
status
});
}
}
}
return files;
}
finally {
core.endGroup();
}
core.endGroup();
return files;
}
function exportResults(results, format) {
core.info('Results:');

View File

@@ -18,20 +18,20 @@ export async function getChangesInLastCommit(): Promise<File[]> {
return parseGitDiffOutput(output)
}
export async function getChanges(ref: string): Promise<File[]> {
if (!(await hasCommit(ref))) {
export async function getChanges(baseRef: string): Promise<File[]> {
if (!(await hasCommit(baseRef))) {
// Fetch single commit
core.startGroup(`Fetching ${ref} from origin`)
await exec('git', ['fetch', '--depth=1', '--no-tags', 'origin', ref])
core.startGroup(`Fetching ${baseRef} from origin`)
await exec('git', ['fetch', '--depth=1', '--no-tags', 'origin', baseRef])
core.endGroup()
}
// Get differences between ref and HEAD
core.startGroup(`Change detection ${ref}..HEAD`)
core.startGroup(`Change detection ${baseRef}..HEAD`)
let output = ''
try {
// Two dots '..' change detection - directly compares two versions
output = (await exec('git', ['diff', '--no-renames', '--name-status', '-z', `${ref}..HEAD`])).stdout
output = (await exec('git', ['diff', '--no-renames', '--name-status', '-z', `${baseRef}..HEAD`])).stdout
} finally {
fixStdOutNullTermination()
core.endGroup()
@@ -54,50 +54,75 @@ export async function getChangesOnHead(): Promise<File[]> {
return parseGitDiffOutput(output)
}
export async function getChangesSinceMergeBase(ref: string, initialFetchDepth: number): Promise<File[]> {
if (!(await hasCommit(ref))) {
// Fetch and add base branch
core.startGroup(`Fetching ${ref}`)
try {
await exec('git', ['fetch', `--depth=${initialFetchDepth}`, '--no-tags', 'origin', `${ref}:${ref}`])
} finally {
core.endGroup()
}
}
export async function getChangesSinceMergeBase(base: string, head: string, initialFetchDepth: number): Promise<File[]> {
let baseRef: string | undefined
let headRef: string | undefined
async function hasMergeBase(): Promise<boolean> {
return (await exec('git', ['merge-base', ref, 'HEAD'], {ignoreReturnCode: true})).code === 0
if (baseRef === undefined || headRef === undefined) {
return false
}
return (await exec('git', ['merge-base', baseRef, headRef], {ignoreReturnCode: true})).code === 0
}
async function countCommits(): Promise<number> {
return (await getNumberOfCommits('HEAD')) + (await getNumberOfCommits(ref))
}
core.startGroup(`Searching for merge-base with ${ref}`)
// Fetch more commits until merge-base is found
if (!(await hasMergeBase())) {
let deepen = initialFetchDepth
let lastCommitsCount = await countCommits()
do {
await exec('git', ['fetch', `--deepen=${deepen}`, '--no-tags'])
const count = await countCommits()
if (count <= lastCommitsCount) {
core.info('No merge base found - all files will be listed as added')
core.endGroup()
return await listAllFilesAsAdded()
let noMergeBase = false
core.startGroup(`Searching for merge-base ${base}...${head}`)
try {
baseRef = await getFullRef(base)
headRef = await getFullRef(head)
if (!(await hasMergeBase())) {
await exec('git', ['fetch', '--no-tags', `--depth=${initialFetchDepth}`, 'origin', base, head])
if (baseRef === undefined || headRef === undefined) {
baseRef = baseRef ?? (await getFullRef(base))
headRef = headRef ?? (await getFullRef(head))
if (baseRef === undefined || headRef === undefined) {
await exec('git', ['fetch', '--tags', '--depth=1', 'origin', base, head], {
ignoreReturnCode: true // returns exit code 1 if tags on remote were updated - we can safely ignore it
})
baseRef = baseRef ?? (await getFullRef(base))
headRef = headRef ?? (await getFullRef(head))
if (baseRef === undefined) {
throw new Error(`Could not determine what is ${base} - fetch works but it's not a branch or tag`)
}
if (headRef === undefined) {
throw new Error(`Could not determine what is ${head} - fetch works but it's not a branch or tag`)
}
}
}
lastCommitsCount = count
deepen = Math.min(deepen * 2, Number.MAX_SAFE_INTEGER)
} while (!(await hasMergeBase()))
}
core.endGroup()
// Get changes introduced on HEAD compared to ref
core.startGroup(`Change detection ${ref}...HEAD`)
let depth = initialFetchDepth
let lastCommitCount = await getCommitCount()
while (!(await hasMergeBase())) {
depth = Math.min(depth * 2, Number.MAX_SAFE_INTEGER)
await exec('git', ['fetch', `--deepen=${depth}`, 'origin', base, head])
const commitCount = await getCommitCount()
if (commitCount === lastCommitCount) {
core.info('No more commits were fetched')
core.info('Last attempt will be to fetch full history')
await exec('git', ['fetch'])
if (!(await hasMergeBase())) {
noMergeBase = true
}
break
}
lastCommitCount = commitCount
}
}
} finally {
core.endGroup()
}
// Three dots '...' change detection - finds merge-base and compares against it
let diffArg = `${baseRef}...${headRef}`
if (noMergeBase) {
core.warning('No merge base found - change detection will use direct <commit>..<commit> comparison')
diffArg = `${baseRef}..${headRef}`
}
// Get changes introduced on ref compared to base
core.startGroup(`Change detection ${diffArg}`)
let output = ''
try {
// Three dots '...' change detection - finds merge-base and compares against it
output = (await exec('git', ['diff', '--no-renames', '--name-status', '-z', `${ref}...HEAD`])).stdout
output = (await exec('git', ['diff', '--no-renames', '--name-status', '-z', diffArg])).stdout
} finally {
fixStdOutNullTermination()
core.endGroup()
@@ -150,7 +175,7 @@ export async function getCurrentRef(): Promise<string> {
return describe.stdout.trim()
}
return (await exec('git', ['rev-parse', 'HEAD'])).stdout.trim()
return (await exec('git', ['rev-parse', HEAD])).stdout.trim()
} finally {
core.endGroup()
}
@@ -181,12 +206,35 @@ async function hasCommit(ref: string): Promise<boolean> {
}
}
async function getNumberOfCommits(ref: string): Promise<number> {
const output = (await exec('git', ['rev-list', `--count`, ref])).stdout
async function getCommitCount(): Promise<number> {
const output = (await exec('git', ['rev-list', '--count', '--all'])).stdout
const count = parseInt(output)
return isNaN(count) ? 0 : count
}
async function getFullRef(shortName: string): Promise<string | undefined> {
if (isGitSha(shortName)) {
return shortName
}
const output = (await exec('git', ['show-ref', shortName], {ignoreReturnCode: true})).stdout
const refs = output
.split(/\r?\n/g)
.map(l => l.match(/refs\/.*$/)?.[0] ?? '')
.filter(l => l !== '')
if (refs.length === 0) {
return undefined
}
const remoteRef = refs.find(ref => ref.startsWith('refs/remotes/origin/'))
if (remoteRef) {
return remoteRef
}
return refs[0]
}
function fixStdOutNullTermination(): void {
// Previous command uses NULL as delimiters and output is printed to stdout.
// We have to make sure next thing written to stdout will start on new line.

View File

@@ -19,6 +19,7 @@ async function run(): Promise<void> {
}
const token = core.getInput('token', {required: false})
const ref = core.getInput('ref', {required: false})
const base = core.getInput('base', {required: false})
const filtersInput = core.getInput('filters', {required: true})
const filtersYaml = isPathInput(filtersInput) ? getConfigFileContent(filtersInput) : filtersInput
@@ -31,7 +32,8 @@ async function run(): Promise<void> {
}
const filter = new Filter(filtersYaml)
const files = await getChangedFiles(token, base, initialFetchDepth)
const files = await getChangedFiles(token, base, ref, initialFetchDepth)
core.info(`Detected ${files.length} changed files`)
const results = filter.match(files)
exportResults(results, listFiles)
} catch (error) {
@@ -40,7 +42,7 @@ async function run(): Promise<void> {
}
function isPathInput(text: string): boolean {
return !text.includes('\n')
return !(text.includes('\n') || text.includes(':'))
}
function getConfigFileContent(configPath: string): string {
@@ -55,7 +57,7 @@ function getConfigFileContent(configPath: string): string {
return fs.readFileSync(configPath, {encoding: 'utf8'})
}
async function getChangedFiles(token: string, base: string, initialFetchDepth: number): Promise<File[]> {
async function getChangedFiles(token: string, base: string, ref: string, initialFetchDepth: number): Promise<File[]> {
// if base is 'HEAD' only local uncommitted changes will be detected
// This is the simplest case as we don't need to fetch more commits or evaluate current/before refs
if (base === git.HEAD) {
@@ -70,18 +72,18 @@ async function getChangedFiles(token: string, base: string, initialFetchDepth: n
core.info('Github token is not available - changes will be detected from PRs merge commit')
return await git.getChangesInLastCommit()
} else {
return getChangedFilesFromGit(base, initialFetchDepth)
return getChangedFilesFromGit(base, ref, initialFetchDepth)
}
}
async function getChangedFilesFromGit(base: string, initialFetchDepth: number): Promise<File[]> {
async function getChangedFilesFromGit(base: string, head: string, initialFetchDepth: number): Promise<File[]> {
const defaultRef = github.context.payload.repository?.default_branch
const beforeSha =
github.context.eventName === 'push' ? (github.context.payload as Webhooks.WebhookPayloadPush).before : null
const pushRef =
git.getShortName(github.context.ref) ||
const ref =
git.getShortName(head || github.context.ref) ||
(core.warning(`'ref' field is missing in event payload - using current branch, tag or commit SHA`),
await git.getCurrentRef())
@@ -93,11 +95,11 @@ async function getChangedFilesFromGit(base: string, initialFetchDepth: number):
}
const isBaseRefSha = git.isGitSha(baseRef)
const isBaseSameAsPush = baseRef === pushRef
const isBaseRefSameAsRef = baseRef === ref
// If base is commit SHA we will do comparison against the referenced commit
// Or if base references same branch it was pushed to, we will do comparison against the previously pushed commit
if (isBaseRefSha || isBaseSameAsPush) {
if (isBaseRefSha || isBaseRefSameAsRef) {
if (!isBaseRefSha && !beforeSha) {
core.warning(`'before' field is missing in event payload - changes will be detected from last commit`)
return await git.getChangesInLastCommit()
@@ -109,7 +111,7 @@ async function getChangedFilesFromGit(base: string, initialFetchDepth: number):
if (baseSha === git.NULL_SHA) {
if (defaultRef && baseRef !== defaultRef) {
core.info(`First push of a branch detected - changes will be detected against the default branch ${defaultRef}`)
return await git.getChangesSinceMergeBase(defaultRef, initialFetchDepth)
return await git.getChangesSinceMergeBase(defaultRef, ref, initialFetchDepth)
} else {
core.info('Initial push detected - all files will be listed as added')
return await git.listAllFilesAsAdded()
@@ -121,8 +123,8 @@ async function getChangedFilesFromGit(base: string, initialFetchDepth: number):
}
// Changes introduced by current branch against the base branch
core.info(`Changes will be detected against the branch ${baseRef}`)
return await git.getChangesSinceMergeBase(baseRef, initialFetchDepth)
core.info(`Changes will be detected against ${baseRef}`)
return await git.getChangesSinceMergeBase(baseRef, ref, initialFetchDepth)
}
// Uses github REST api to get list of files changed in PR
@@ -131,47 +133,61 @@ async function getChangedFilesFromApi(
pullRequest: Webhooks.WebhookPayloadPullRequestPullRequest
): Promise<File[]> {
core.startGroup(`Fetching list of changed files for PR#${pullRequest.number} from Github API`)
core.info(`Number of changed_files is ${pullRequest.changed_files}`)
const client = new github.GitHub(token)
const pageSize = 100
const files: File[] = []
for (let page = 1; (page - 1) * pageSize < pullRequest.changed_files; page++) {
core.info(`Invoking listFiles(pull_number: ${pullRequest.number}, page: ${page}, per_page: ${pageSize})`)
const response = await client.pulls.listFiles({
owner: github.context.repo.owner,
repo: github.context.repo.repo,
pull_number: pullRequest.number,
page,
per_page: pageSize
})
for (const row of response.data) {
core.info(`[${row.status}] ${row.filename}`)
// There's no obvious use-case for detection of renames
// Therefore we treat it as if rename detection in git diff was turned off.
// Rename is replaced by delete of original filename and add of new filename
if (row.status === ChangeStatus.Renamed) {
files.push({
filename: row.filename,
status: ChangeStatus.Added
})
files.push({
// 'previous_filename' for some unknown reason isn't in the type definition or documentation
filename: (<any>row).previous_filename as string,
status: ChangeStatus.Deleted
})
} else {
// Github status and git status variants are same except for deleted files
const status = row.status === 'removed' ? ChangeStatus.Deleted : (row.status as ChangeStatus)
files.push({
filename: row.filename,
status
})
try {
const client = new github.GitHub(token)
const per_page = 100
const files: File[] = []
for (let page = 1; ; page++) {
core.info(`Invoking listFiles(pull_number: ${pullRequest.number}, page: ${page}, per_page: ${per_page})`)
const response = await client.pulls.listFiles({
owner: github.context.repo.owner,
repo: github.context.repo.repo,
pull_number: pullRequest.number,
per_page,
page
})
if (response.status !== 200) {
throw new Error(`Fetching list of changed files from GitHub API failed with error code ${response.status}`)
}
core.info(`Received ${response.data.length} items`)
if (response.data.length === 0) {
core.info('All changed files has been fetched from GitHub API')
break
}
for (const row of response.data) {
core.info(`[${row.status}] ${row.filename}`)
// There's no obvious use-case for detection of renames
// Therefore we treat it as if rename detection in git diff was turned off.
// Rename is replaced by delete of original filename and add of new filename
if (row.status === ChangeStatus.Renamed) {
files.push({
filename: row.filename,
status: ChangeStatus.Added
})
files.push({
// 'previous_filename' for some unknown reason isn't in the type definition or documentation
filename: (<any>row).previous_filename as string,
status: ChangeStatus.Deleted
})
} else {
// Github status and git status variants are same except for deleted files
const status = row.status === 'removed' ? ChangeStatus.Deleted : (row.status as ChangeStatus)
files.push({
filename: row.filename,
status
})
}
}
}
}
core.endGroup()
return files
return files
} finally {
core.endGroup()
}
}
function exportResults(results: FilterResults, format: ExportFormat): void {