Skip to content

Commit

Permalink
Merge pull request #74 from dorny/fix-searching-for-merge-base
Browse files Browse the repository at this point in the history
Fix fetching git history + fallback to unshallow repo
  • Loading branch information
dorny committed Mar 8, 2021
2 parents 1cdd3bb + 46d2898 commit 0aa1597
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 99 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,8 @@
# Changelog

## 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)

Expand Down
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -117,7 +117,7 @@ For more information see [CHANGELOG](https://github.com/dorny/paths-filter/blob/
# 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:
Expand Down
2 changes: 1 addition & 1 deletion action.yml
Expand Up @@ -38,7 +38,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
Expand Down
102 changes: 53 additions & 49 deletions dist/index.js
Expand Up @@ -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();
Expand All @@ -3865,47 +3865,51 @@ 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(baseRef, ref, initialFetchDepth) {
async function hasMergeBase() {
return (await exec_1.default('git', ['merge-base', ref, 'HEAD'], { 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();
return (await exec_1.default('git', ['merge-base', baseRef, ref], { ignoreReturnCode: true })).code === 0;
}
let noMergeBase = false;
core.startGroup(`Searching for merge-base ${baseRef}...${ref}`);
try {
let init = true;
let lastCommitCount = await getCommitCount();
let depth = Math.max(lastCommitCount * 2, initialFetchDepth);
while (!(await hasMergeBase())) {
if (init) {
await exec_1.default('git', ['fetch', `--depth=${depth}`, 'origin', `${baseRef}:${baseRef}`, `${ref}`]);
init = false;
}
else {
await exec_1.default('git', ['fetch', `--deepen=${depth}`, 'origin', baseRef, ref]);
}
lastCommitsCount = count;
deepen = Math.min(deepen * 2, Number.MAX_SAFE_INTEGER);
} while (!(await hasMergeBase()));
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', '--unshallow']);
if (!(await hasMergeBase())) {
noMergeBase = true;
}
break;
}
depth = Math.min(depth * 2, Number.MAX_SAFE_INTEGER);
lastCommitCount = commitCount;
}
}
finally {
core.endGroup();
}
if (noMergeBase) {
core.warning('No merge base found - all files will be listed as added');
return await listAllFilesAsAdded();
}
core.endGroup();
// Get changes introduced on HEAD compared to ref
core.startGroup(`Change detection ${ref}...HEAD`);
core.startGroup(`Change detection ${baseRef}...${ref}`);
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', `${baseRef}...${ref}`])).stdout;
}
finally {
fixStdOutNullTermination();
Expand Down Expand Up @@ -3956,7 +3960,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();
Expand Down Expand Up @@ -3988,8 +3992,8 @@ 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;
}
Expand Down Expand Up @@ -4676,7 +4680,7 @@ async function run() {
}
}
function isPathInput(text) {
return !text.includes('\n');
return !(text.includes('\n') || text.includes(':'));
}
function getConfigFileContent(configPath) {
if (!fs.existsSync(configPath)) {
Expand Down Expand Up @@ -4709,18 +4713,18 @@ async function getChangedFilesFromGit(base, 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(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;
if (!baseRef) {
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();
Expand All @@ -4731,7 +4735,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');
Expand All @@ -4743,7 +4747,7 @@ async function getChangedFilesFromGit(base, initialFetchDepth) {
}
// 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);
return await git.getChangesSinceMergeBase(baseRef, ref, initialFetchDepth);
}
// Uses github REST api to get list of files changed in PR
async function getChangedFilesFromApi(token, pullRequest) {
Expand Down
90 changes: 48 additions & 42 deletions src/git.ts
Expand Up @@ -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()
Expand All @@ -54,50 +54,56 @@ 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(
baseRef: string,
ref: string,
initialFetchDepth: number
): Promise<File[]> {
async function hasMergeBase(): Promise<boolean> {
return (await exec('git', ['merge-base', ref, 'HEAD'], {ignoreReturnCode: true})).code === 0
return (await exec('git', ['merge-base', baseRef, ref], {ignoreReturnCode: true})).code === 0
}

async function countCommits(): Promise<number> {
return (await getNumberOfCommits('HEAD')) + (await getNumberOfCommits(ref))
let noMergeBase = false
core.startGroup(`Searching for merge-base ${baseRef}...${ref}`)
try {
let init = true
let lastCommitCount = await getCommitCount()
let depth = Math.max(lastCommitCount * 2, initialFetchDepth)
while (!(await hasMergeBase())) {
if (init) {
await exec('git', ['fetch', `--depth=${depth}`, 'origin', `${baseRef}:${baseRef}`, `${ref}`])
init = false
} else {
await exec('git', ['fetch', `--deepen=${depth}`, 'origin', baseRef, ref])
}
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', '--unshallow'])
if (!(await hasMergeBase())) {
noMergeBase = true
}
break
}
depth = Math.min(depth * 2, Number.MAX_SAFE_INTEGER)
lastCommitCount = commitCount
}
} finally {
core.endGroup()
}

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()
}
lastCommitsCount = count
deepen = Math.min(deepen * 2, Number.MAX_SAFE_INTEGER)
} while (!(await hasMergeBase()))
if (noMergeBase) {
core.warning('No merge base found - all files will be listed as added')
return await listAllFilesAsAdded()
}
core.endGroup()

// Get changes introduced on HEAD compared to ref
core.startGroup(`Change detection ${ref}...HEAD`)
core.startGroup(`Change detection ${baseRef}...${ref}`)
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', `${baseRef}...${ref}`])).stdout
} finally {
fixStdOutNullTermination()
core.endGroup()
Expand Down Expand Up @@ -150,7 +156,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()
}
Expand Down Expand Up @@ -181,8 +187,8 @@ 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
}
Expand Down
12 changes: 6 additions & 6 deletions src/main.ts
Expand Up @@ -40,7 +40,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 {
Expand Down Expand Up @@ -80,7 +80,7 @@ async function getChangedFilesFromGit(base: string, initialFetchDepth: number):
const beforeSha =
github.context.eventName === 'push' ? (github.context.payload as Webhooks.WebhookPayloadPush).before : null

const pushRef =
const ref =
git.getShortName(github.context.ref) ||
(core.warning(`'ref' field is missing in event payload - using current branch, tag or commit SHA`),
await git.getCurrentRef())
Expand All @@ -93,11 +93,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()
Expand All @@ -109,7 +109,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()
Expand All @@ -122,7 +122,7 @@ 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)
return await git.getChangesSinceMergeBase(baseRef, ref, initialFetchDepth)
}

// Uses github REST api to get list of files changed in PR
Expand Down

0 comments on commit 0aa1597

Please sign in to comment.