Skip to content

Commit 144b659

Browse files
committed
feat: Implement filter strategies
1 parent 4384316 commit 144b659

12 files changed

Lines changed: 1104 additions & 20 deletions

File tree

README.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,18 @@ jobs:
3434
coverage/coverage1.xml
3535
coverage/coverage2.xml
3636
update-comment: true
37+
show-changed-lines-only: true
38+
show-glob-only: '**/**'
3739
```
3840
3941
### Inputs
4042
41-
| Name | Default | Description |
42-
|------------------|------------|-----------------------------------------------------------------|
43-
| `files` | (required) | One or multiple files. Can be multiple lines. Glob is supported |
44-
| `update-comment` | true | Visualize for changed lines only |
45-
| `path-glob` | true | Select subdirectories to show coverage data for |
43+
| Name | Default | Description |
44+
|---------------------------|------------|-----------------------------------------------------------------|
45+
| `files` | (required) | One or multiple files. Can be multiple lines. Glob is supported |
46+
| `update-comment` | true | TODO: Update |
47+
| `show-changed-lines-only` | true | If part of a PR, filter coverage data against changed lines |
48+
| `show-glob-only` | '**' | Select subdirectories to show coverage data for |
4649

4750
### Outputs
4851

action.yaml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,14 @@ inputs:
99
description: Whether to update an existing comment instead of creating a new one
1010
required: false
1111
default: 'true'
12-
path-glob:
12+
show-changed-lines-only:
13+
description: If part of a PR, filter coverage data against changed lines
14+
required: false
15+
default: 'true'
16+
show-glob-only:
1317
description: Select subdirectories to show coverage data for
1418
required: false
15-
default: ''
19+
default: '**'
1620
token:
1721
description: GitHub token for API access
1822
required: true

package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,21 @@
1212
"@octokit/action": "8.0.4",
1313
"@octokit/plugin-retry": "8.0.3",
1414
"fast-xml-parser": "^5.3.3",
15-
"glob": "^11.0.0"
15+
"glob": "^11.0.0",
16+
"minimatch": "^10.1.1"
1617
},
1718
"devDependencies": {
1819
"@biomejs/biome": "2.3.10",
1920
"@octokit/webhooks-types": "7.6.1",
2021
"@tsconfig/node20": "20.1.8",
2122
"@tsconfig/strictest": "2.0.8",
23+
"@types/minimatch": "^6.0.0",
2224
"@types/node": "20.19.27",
2325
"@vercel/ncc": "0.38.4",
26+
"@vitest/coverage-v8": "4.0.16",
2427
"pnpm": "10.26.1",
2528
"typescript": "5.9.3",
26-
"vitest": "4.0.16",
27-
"@vitest/coverage-v8": "4.0.16"
29+
"vitest": "4.0.16"
2830
},
2931
"engines": {
3032
"node": "20.x"

pnpm-lock.yaml

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/filter/changed-lines.ts

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { exec } from 'node:child_process'
2+
import { promisify } from 'node:util'
3+
import type { ChangedLinesMap } from './model.js'
4+
5+
const execAsync = promisify(exec)
6+
7+
/**
8+
* Get changed lines by comparing against a base ref using local git.
9+
* Requires the repository to be checked out.
10+
*
11+
* @param baseRef - The base ref to compare against (e.g., 'origin/main', commit SHA)
12+
* @returns Map of filename to set of changed line numbers
13+
*/
14+
export async function getChangedLinesFromGit(baseRef: string): Promise<ChangedLinesMap> {
15+
// Get the unified diff between base ref and HEAD
16+
const { stdout: diffOutput } = await execAsync(`git diff ${baseRef}...HEAD`, {
17+
maxBuffer: 10 * 1024 * 1024, // 10MB buffer for large diffs
18+
})
19+
20+
return parseDiffOutput(diffOutput)
21+
}
22+
23+
/**
24+
* Parse full git diff output to extract changed lines per file.
25+
*
26+
* @param diffOutput - Full git diff output
27+
* @returns Map of filename to set of changed line numbers
28+
*/
29+
export function parseDiffOutput(diffOutput: string): ChangedLinesMap {
30+
const changedLines: ChangedLinesMap = new Map()
31+
const lines = diffOutput.split('\n')
32+
33+
let currentFile: string | null = null
34+
35+
for (let i = 0; i < lines.length; i++) {
36+
const line = lines[i]!
37+
38+
// Parse diff header: diff --git a/path/to/file b/path/to/file
39+
const diffMatch = line.match(/^diff --git a\/.+ b\/(.+)$/)
40+
if (diffMatch) {
41+
currentFile = diffMatch[1]!
42+
continue
43+
}
44+
45+
// Skip deleted files (indicated by /dev/null in new file)
46+
if (line.startsWith('+++ /dev/null')) {
47+
currentFile = null
48+
continue
49+
}
50+
51+
// Skip if we don't have a current file
52+
if (!currentFile) {
53+
continue
54+
}
55+
56+
// Parse hunk and extract changed lines
57+
const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/)
58+
if (hunkMatch) {
59+
const startLine = Number.parseInt(hunkMatch[1]!, 10)
60+
const hunkChangedLines = parseHunkForChangedLines(lines, i, startLine)
61+
62+
// Merge with existing changed lines for this file
63+
const existing = changedLines.get(currentFile) ?? new Set<number>()
64+
for (const lineNum of hunkChangedLines) {
65+
existing.add(lineNum)
66+
}
67+
changedLines.set(currentFile, existing)
68+
}
69+
}
70+
71+
return changedLines
72+
}
73+
74+
/**
75+
* Parse a single hunk starting from a given index to extract changed line numbers.
76+
*
77+
* @param lines - All lines of the diff
78+
* @param hunkStartIndex - Index of the hunk header line
79+
* @param startLine - Starting line number from the hunk header
80+
* @returns Set of changed line numbers in this hunk
81+
*/
82+
function parseHunkForChangedLines(lines: string[], hunkStartIndex: number, startLine: number): Set<number> {
83+
const changedLines = new Set<number>()
84+
let currentLine = startLine
85+
86+
// Start after the hunk header
87+
for (let i = hunkStartIndex + 1; i < lines.length; i++) {
88+
const line = lines[i]!
89+
90+
// Stop at next hunk or diff header
91+
if (line.startsWith('@@') || line.startsWith('diff --git')) {
92+
break
93+
}
94+
95+
// Lines starting with '-' are removed lines (don't exist in new file)
96+
if (line.startsWith('-')) {
97+
// Don't increment currentLine for removed lines
98+
continue
99+
}
100+
101+
// Lines starting with '+' are added lines
102+
if (line.startsWith('+')) {
103+
changedLines.add(currentLine)
104+
currentLine++
105+
continue
106+
}
107+
108+
// Context lines (starting with space or no prefix) exist in both versions
109+
currentLine++
110+
}
111+
112+
return changedLines
113+
}
114+
115+
/**
116+
* Parse a unified diff patch to extract added/modified line numbers.
117+
*
118+
* The patch format uses @@ -old_start,old_count +new_start,new_count @@ headers
119+
* followed by context lines (starting with space), removed lines (starting with -)
120+
* and added lines (starting with +).
121+
*
122+
* We only care about added lines (lines starting with +) as those are the
123+
* "new" lines that exist in the PR's version of the file.
124+
*
125+
* @param patch - Unified diff patch string
126+
* @returns Set of line numbers that were added/modified
127+
*/
128+
export function parsePatchForChangedLines(patch: string): Set<number> {
129+
const changedLines = new Set<number>()
130+
const lines = patch.split('\n')
131+
132+
let currentLine = 0
133+
134+
for (const line of lines) {
135+
// Parse hunk header: @@ -old_start,old_count +new_start,new_count @@
136+
const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/)
137+
if (hunkMatch) {
138+
currentLine = Number.parseInt(hunkMatch[1]!, 10)
139+
continue
140+
}
141+
142+
// Skip if we haven't seen a hunk header yet
143+
if (currentLine === 0) {
144+
continue
145+
}
146+
147+
// Lines starting with '-' are removed lines (don't exist in new file)
148+
if (line.startsWith('-')) {
149+
// Don't increment currentLine for removed lines
150+
continue
151+
}
152+
153+
// Lines starting with '+' are added lines
154+
if (line.startsWith('+')) {
155+
changedLines.add(currentLine)
156+
currentLine++
157+
continue
158+
}
159+
160+
// Context lines (starting with space or no prefix) exist in both versions
161+
// We don't add them to changed lines, but we do increment the line counter
162+
currentLine++
163+
}
164+
165+
return changedLines
166+
}

0 commit comments

Comments
 (0)