Skip to content

Commit

Permalink
Merge branch 'numstat'
Browse files Browse the repository at this point in the history
  • Loading branch information
lykahb committed Mar 11, 2022
2 parents 31fc4ae + 1ee4529 commit 4b7660a
Show file tree
Hide file tree
Showing 7 changed files with 311 additions and 148 deletions.
6 changes: 3 additions & 3 deletions __tests__/filter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ describe('matching specific change status', () => {
- added: "**/*"
`
let filter = new Filter(yaml)
const files = [{status: ChangeStatus.Added, filename: 'file.js'}]
const files = [{status: ChangeStatus.Added, filename: 'file.js', additions: 1, deletions: 0}]
const match = filter.match(files)
expect(match.add).toEqual(files)
})
Expand All @@ -161,7 +161,7 @@ describe('matching specific change status', () => {
- added|modified: "**/*"
`
let filter = new Filter(yaml)
const files = [{status: ChangeStatus.Modified, filename: 'file.js'}]
const files = [{status: ChangeStatus.Modified, filename: 'file.js', additions: 1, deletions: 1}]
const match = filter.match(files)
expect(match.addOrModify).toEqual(files)
})
Expand All @@ -183,6 +183,6 @@ describe('matching specific change status', () => {

function modified(paths: string[]): File[] {
return paths.map(filename => {
return {filename, status: ChangeStatus.Modified}
return {filename, status: ChangeStatus.Modified, additions: 1, deletions: 1}
})
}
17 changes: 15 additions & 2 deletions __tests__/git.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import * as git from '../src/git'
import {ChangeStatus} from '../src/file'

describe('parsing output of the git diff command', () => {
test('parseGitDiffOutput returns files with correct change status', async () => {
const files = git.parseGitDiffOutput(
test('parseGitDiffNameStatusOutput returns files with correct change status', async () => {
const files = git.parseGitDiffNameStatusOutput(
'A\u0000LICENSE\u0000' + 'M\u0000src/index.ts\u0000' + 'D\u0000src/main.ts\u0000'
)
expect(files.length).toBe(3)
Expand All @@ -14,6 +14,19 @@ describe('parsing output of the git diff command', () => {
expect(files[2].filename).toBe('src/main.ts')
expect(files[2].status).toBe(ChangeStatus.Deleted)
})

test('parseGitDiffNumstatOutput returns files with correct change status', async () => {
const files = git.parseGitDiffNumstatOutput(
'4\t2\tLICENSE\u0000' + '5\t0\tsrc/index.ts\u0000'
)
expect(files.length).toBe(2)
expect(files[0].filename).toBe('LICENSE')
expect(files[0].additions).toBe(4)
expect(files[0].deletions).toBe(2)
expect(files[1].filename).toBe('src/index.ts')
expect(files[1].additions).toBe(5)
expect(files[1].deletions).toBe(0)
})
})

describe('git utility function tests (those not invoking git)', () => {
Expand Down
199 changes: 129 additions & 70 deletions dist/index.js

Large diffs are not rendered by default.

10 changes: 9 additions & 1 deletion src/file.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
export interface File {
export interface FileStatus {
filename: string
status: ChangeStatus
}

export interface FileNumstat {
filename: string
additions: number
deletions: number
}

export type File = FileStatus & FileNumstat

export enum ChangeStatus {
Added = 'added',
Copied = 'copied',
Expand Down
11 changes: 9 additions & 2 deletions src/filter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as jsyaml from 'js-yaml'
import picomatch from 'picomatch'
import {File, ChangeStatus} from './file'
import {File, ChangeStatus, FileStatus} from './file'

// Type definition of object we expect to load from YAML
interface FilterYaml {
Expand Down Expand Up @@ -58,10 +58,17 @@ export class Filter {
for (const [key, patterns] of Object.entries(this.rules)) {
result[key] = files.filter(file => this.isMatch(file, patterns))
}

if (!this.rules.hasOwnProperty('other')) {
const matchingFilenamesList = Object.values(result).flatMap(filteredFiles => filteredFiles.map(file => file.filename))
const matchingFilenamesSet = new Set(matchingFilenamesList)
result.other = files.filter(file => !matchingFilenamesSet.has(file.filename))
}

return result
}

private isMatch(file: File, patterns: FilterRuleItem[]): boolean {
private isMatch(file: FileStatus, patterns: FilterRuleItem[]): boolean {
return patterns.some(
rule => (rule.status === undefined || rule.status.includes(file.status)) && rule.isMatch(file.filename)
)
Expand Down
128 changes: 75 additions & 53 deletions src/git.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,41 @@
import exec from './exec'
import * as core from '@actions/core'
import {File, ChangeStatus} from './file'
import {File, ChangeStatus, FileNumstat, FileStatus} from './file'

export const NULL_SHA = '0000000000000000000000000000000000000000'
export const HEAD = 'HEAD'

export async function getChangesInLastCommit(): Promise<File[]> {
core.startGroup(`Change detection in last commit`)
let output = ''
try {
output = (await exec('git', ['log', '--format=', '--no-renames', '--name-status', '-z', '-n', '1'])).stdout
} finally {
fixStdOutNullTermination()
core.endGroup()
}

return parseGitDiffOutput(output)
return core.group(`Change detection in last commit`, async () => {
try {
// Calling git log on the last commit works when only the last commit may be checked out. Calling git diff HEAD^..HEAD needs two commits.
const statusOutput = (await exec('git', ['log', '--format=', '--no-renames', '--name-status', '-z', '-n', '1'])).stdout
const numstatOutput = (await exec('git', ['log', '--format=', '--no-renames', '--numstat', '-z', '-n', '1'])).stdout
const statusFiles = parseGitDiffNameStatusOutput(statusOutput)
const numstatFiles = parseGitDiffNumstatOutput(numstatOutput)
return mergeStatusNumstat(statusFiles, numstatFiles)
} finally {
fixStdOutNullTermination()
}
})
}

export async function getChanges(base: string, head: string): Promise<File[]> {
const baseRef = await ensureRefAvailable(base)
const headRef = await ensureRefAvailable(head)

// Get differences between ref and HEAD
core.startGroup(`Change detection ${base}..${head}`)
let output = ''
try {
// Two dots '..' change detection - directly compares two versions
output = (await exec('git', ['diff', '--no-renames', '--name-status', '-z', `${baseRef}..${headRef}`])).stdout
} finally {
fixStdOutNullTermination()
core.endGroup()
}

return parseGitDiffOutput(output)
// Two dots '..' change detection - directly compares two versions
return core.group(`Change detection ${base}..${head}`, () =>
getGitDiffStatusNumstat(`${baseRef}..${headRef}`)
)
}

export async function getChangesOnHead(): Promise<File[]> {
// Get current changes - both staged and unstaged
core.startGroup(`Change detection on HEAD`)
let output = ''
try {
output = (await exec('git', ['diff', '--no-renames', '--name-status', '-z', 'HEAD'])).stdout
} finally {
fixStdOutNullTermination()
core.endGroup()
}

return parseGitDiffOutput(output)
return core.group(`Change detection on HEAD`, () =>
getGitDiffStatusNumstat(`HEAD`)
)
}

export async function getChangesSinceMergeBase(base: string, head: string, initialFetchDepth: number): Promise<File[]> {
Expand Down Expand Up @@ -119,21 +107,32 @@ export async function getChangesSinceMergeBase(base: string, head: string, initi
}

// Get changes introduced on ref compared to base
core.startGroup(`Change detection ${diffArg}`)
return getGitDiffStatusNumstat(diffArg)
}

async function gitDiffNameStatus(diffArg: string): Promise<string> {
let output = ''
try {
output = (await exec('git', ['diff', '--no-renames', '--name-status', '-z', diffArg])).stdout
} finally {
fixStdOutNullTermination()
core.endGroup()
}
return output
}

return parseGitDiffOutput(output)
async function gitDiffNumstat(diffArg: string): Promise<string> {
let output = ''
try {
output = (await exec('git', ['diff', '--no-renames', '--numstat', '-z', diffArg])).stdout
} finally {
fixStdOutNullTermination()
}
return output
}

export function parseGitDiffOutput(output: string): File[] {
export function parseGitDiffNameStatusOutput(output: string): FileStatus[] {
const tokens = output.split('\u0000').filter(s => s.length > 0)
const files: File[] = []
const files: FileStatus[] = []
for (let i = 0; i + 1 < tokens.length; i += 2) {
files.push({
status: statusMap[tokens[i]],
Expand All @@ -143,23 +142,46 @@ export function parseGitDiffOutput(output: string): File[] {
return files
}

export async function listAllFilesAsAdded(): Promise<File[]> {
core.startGroup('Listing all files tracked by git')
let output = ''
try {
output = (await exec('git', ['ls-files', '-z'])).stdout
} finally {
fixStdOutNullTermination()
core.endGroup()
}
function mergeStatusNumstat(statusEntries: FileStatus[], numstatEntries: FileNumstat[]): File[] {
const statusMap: {[key: string]: FileStatus} = {}
statusEntries.forEach(f => statusMap[f.filename] = f)

return output
.split('\u0000')
.filter(s => s.length > 0)
.map(path => ({
status: ChangeStatus.Added,
filename: path
}))
return numstatEntries.map(f => {
const status = statusMap[f.filename]
if (!status) {
throw new Error(`Cannot find the status entry for file: ${f.filename}`);
}
return {...f, status: status.status}
})
}

export async function getGitDiffStatusNumstat(diffArg: string) {
const statusFiles = await gitDiffNameStatus(diffArg).then(parseGitDiffNameStatusOutput)
const numstatFiles = await gitDiffNumstat(diffArg).then(parseGitDiffNumstatOutput)
return mergeStatusNumstat(statusFiles, numstatFiles)
}

export function parseGitDiffNumstatOutput(output: string): FileNumstat[] {
const rows = output.split('\u0000').filter(s => s.length > 0)
return rows.map(row => {
const tokens = row.split('\t')
// For the binary files set the numbers to zero. This matches the response of Github API.
const additions = tokens[0] == '-' ? 0 : Number.parseInt(tokens[0])
const deletions = tokens[1] == '-' ? 0 : Number.parseInt(tokens[1])
return {
filename: tokens[2],
additions,
deletions,
}
})
}


export async function listAllFilesAsAdded(): Promise<File[]> {
return core.group(`Listing all files tracked by git`, async () => {
const emptyTreeHash = (await exec('git', ['hash-object', '-t', 'tree', '/dev/null'])).stdout
return getGitDiffStatusNumstat(emptyTreeHash)
})
}

export async function getCurrentRef(): Promise<string> {
Expand Down
Loading

0 comments on commit 4b7660a

Please sign in to comment.