Change detection using git "three dot" diff (#35)

* Rework change detection via `git diff`

Previous implementation performed simple diff between two versions. New implementation fetches on demand more commits to have the merge base between two branches. Now it will detect only changes introduced by branch that was pushed, instead of mixing with changes introduced meanwhile on the base branch.
This commit is contained in:
Michal Dorner 2020-09-01 22:47:38 +02:00 committed by GitHub
parent 3f845744aa
commit 81c90ccae8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 383 additions and 256 deletions

View file

@ -18,9 +18,11 @@ async function run(): Promise<void> {
}
const token = core.getInput('token', {required: false})
const base = core.getInput('base', {required: false})
const filtersInput = core.getInput('filters', {required: true})
const filtersYaml = isPathInput(filtersInput) ? getConfigFileContent(filtersInput) : filtersInput
const listFiles = core.getInput('list-files', {required: false}).toLowerCase() || 'none'
const initialFetchDepth = parseInt(core.getInput('initial-fetch-depth', {required: false})) || 10
if (!isExportFormat(listFiles)) {
core.setFailed(`Input parameter 'list-files' is set to invalid value '${listFiles}'`)
@ -28,15 +30,9 @@ async function run(): Promise<void> {
}
const filter = new Filter(filtersYaml)
const files = await getChangedFiles(token)
if (files === null) {
// Change detection was not possible
exportNoMatchingResults(filter)
} else {
const results = filter.match(files)
exportResults(results, listFiles)
}
const files = await getChangedFiles(token, base, initialFetchDepth)
const results = filter.match(files)
exportResults(results, listFiles)
} catch (error) {
core.setFailed(error.message)
}
@ -58,52 +54,45 @@ function getConfigFileContent(configPath: string): string {
return fs.readFileSync(configPath, {encoding: 'utf8'})
}
async function getChangedFiles(token: string): Promise<File[] | null> {
async function getChangedFiles(token: string, base: string, initialFetchDepth: number): Promise<File[]> {
if (github.context.eventName === 'pull_request' || github.context.eventName === 'pull_request_target') {
const pr = github.context.payload.pull_request as Webhooks.WebhookPayloadPullRequestPullRequest
return token ? await getChangedFilesFromApi(token, pr) : await getChangedFilesFromGit(pr.base.sha)
return token
? await getChangedFilesFromApi(token, pr)
: await git.getChangesSinceRef(pr.base.ref, initialFetchDepth)
} else if (github.context.eventName === 'push') {
return getChangedFilesFromPush()
return getChangedFilesFromPush(base, initialFetchDepth)
} else {
throw new Error('This action can be triggered only by pull_request or push event')
throw new Error('This action can be triggered only by pull_request, pull_request_target or push event')
}
}
async function getChangedFilesFromPush(): Promise<File[] | null> {
async function getChangedFilesFromPush(base: string, initialFetchDepth: number): Promise<File[]> {
const push = github.context.payload as Webhooks.WebhookPayloadPush
// No change detection for pushed tags
if (git.isTagRef(push.ref)) {
core.info('Workflow is triggered by pushing of tag. Change detection will not run.')
return null
core.info('Workflow is triggered by pushing of tag - all files will be listed as added')
return await git.listAllFilesAsAdded()
}
// Get base from input or use repo default branch.
// It it starts with 'refs/', it will be trimmed (git fetch refs/heads/<NAME> doesn't work)
const baseInput = git.trimRefs(core.getInput('base', {required: false}) || push.repository.default_branch)
const baseRef = git.trimRefsHeads(base || push.repository.default_branch)
const pushRef = git.trimRefsHeads(push.ref)
// If base references same branch it was pushed to, we will do comparison against the previously pushed commit.
// Otherwise changes are detected against the base reference
const base = git.trimRefsHeads(baseInput) === git.trimRefsHeads(push.ref) ? push.before : baseInput
if (baseRef === pushRef) {
if (push.before === git.NULL_SHA) {
core.info('First push of a branch detected - all files will be listed as added')
return await git.listAllFilesAsAdded()
}
// There is no previous commit for comparison
// e.g. change detection against previous commit of just pushed new branch
if (base === git.NULL_SHA) {
core.info('There is no previous commit for comparison. Change detection will not run.')
return null
core.info(`Changes will be detected against the last previously pushed commit on same branch (${pushRef})`)
return await git.getChangesAgainstSha(push.before)
}
return await getChangedFilesFromGit(base)
}
// Fetch base branch and use `git diff` to determine changed files
async function getChangedFilesFromGit(ref: string): Promise<File[]> {
return core.group(`Fetching base and using \`git diff-index\` to determine changed files`, async () => {
await git.fetchCommit(ref)
// FETCH_HEAD will always point to the just fetched commit
// No matter if ref is SHA, branch or tag name or full git ref
return await git.getChangedFiles(git.FETCH_HEAD)
})
// Changes introduced by current branch against the base branch
core.info(`Changes will be detected against the branch ${baseRef}`)
return await git.getChangesSinceRef(baseRef, initialFetchDepth)
}
// Uses github REST api to get list of files changed in PR
@ -149,20 +138,18 @@ async function getChangedFilesFromApi(
return files
}
function exportNoMatchingResults(filter: Filter): void {
core.info('All filters will be set to true but no matched files will be exported.')
for (const key of Object.keys(filter.rules)) {
core.setOutput(key, true)
}
}
function exportResults(results: FilterResults, format: ExportFormat): void {
core.info('Results:')
for (const [key, files] of Object.entries(results)) {
const value = files.length > 0
core.startGroup(`Filter ${key} = ${value}`)
core.info('Matching files:')
for (const file of files) {
core.info(`${file.filename} [${file.status}]`)
if (files.length > 0) {
core.info('Matching files:')
for (const file of files) {
core.info(`${file.filename} [${file.status}]`)
}
} else {
core.info('Matching files: none')
}
core.setOutput(key, value)