1- import process from 'node:process'
1+ /**
2+ * @typedef {import('vfile').VFile } VFile
3+ */
4+
5+ /**
6+ * @typedef {'patch' | 'stats' | 'raw' } DiffType
7+ *
8+ * @typedef PatchData
9+ * Data of a patch.
10+ * @property {string } aPath
11+ * From.
12+ * @property {string } bPath
13+ * To.
14+ * @property {Array<string> } lines
15+ * Changes.
16+ * @property {boolean } isBlacklisted
17+ * No idea.
18+ *
19+ * @typedef {[originalRev: string, rev: string] } Range
20+ * Range of two refs (such as commits).
21+ *
22+ * @typedef {[from: number, to: number] } Diff
23+ * Diff range, two line numbers between which there’s been a change.
24+ */
25+
226import path from 'node:path'
3- // @ts -expect-error: hush
27+ import process from 'node:process'
28+ import { ok as assert } from 'devlop'
29+ // @ts -expect-error: not typed.
430import gitDiffTree from 'git-diff-tree'
531import { findUp } from 'vfile-find-up'
632
7- const own = { } . hasOwnProperty
8-
33+ // This is mostly to enable the tests to mimick different CIs.
34+ // Normally, a Node process exits between CI runs.
935/** @type {string } */
1036let previousRange
1137
12- /** @type {import('unified').Plugin<[]> } */
13- export default function diff ( ) {
14- /** @type {Record<string, string> } */
15- let cache = { }
16-
17- return function ( _ , file , next ) {
38+ /**
39+ * @returns
40+ * Transform.
41+ */
42+ export default function unifiedDiff ( ) {
43+ /** @type {Map<string, string> } */
44+ let cache = new Map ( )
45+
46+ /**
47+ * @param {unknown } _
48+ * Tree.
49+ * @param {VFile } file
50+ * File.
51+ * @returns {Promise<undefined> }
52+ * Promise to nothing.
53+ */
54+ return async function ( _ , file ) {
1855 const base = file . dirname
19- /** @type {string| undefined } */
56+ /** @type {string | undefined } */
2057 let commitRange
21- /** @type {Array<string>| undefined } */
58+ /** @type {Range | undefined } */
2259 let range
2360
2461 // Looks like Travis.
2562 if ( process . env . TRAVIS_COMMIT_RANGE ) {
2663 commitRange = process . env . TRAVIS_COMMIT_RANGE
27- range = commitRange . split ( / \. { 3 } / )
64+ // Cast because we check `length` later.
65+ range = /** @type {Range } */ ( commitRange . split ( / \. { 3 } / ) )
2866 }
2967 // Looks like GH Actions.
3068 else if ( process . env . GITHUB_SHA ) {
31- // @ts -expect-error: fine.
32- range =
33- // This is a PR: check the whole PR.
34- // Refs take the form `refs/heads/main`.
35- process . env . GITHUB_BASE_REF && process . env . GITHUB_HEAD_REF
36- ? [
37- process . env . GITHUB_BASE_REF . split ( '/' ) . pop ( ) ,
38- process . env . GITHUB_HEAD_REF . split ( '/' ) . pop ( )
39- ]
40- : [ process . env . GITHUB_SHA + '^1' , process . env . GITHUB_SHA ]
41- // @ts -expect-error: We definitely just defined this
69+ const sha = process . env . GITHUB_SHA
70+ const base = process . env . GITHUB_BASE_REF
71+ const head = process . env . GITHUB_HEAD_REF
72+
73+ if ( base && head ) {
74+ const baseTail = base . split ( '/' ) . pop ( )
75+ const headTail = head . split ( '/' ) . pop ( )
76+ assert ( baseTail )
77+ assert ( headTail )
78+ range = [ baseTail , headTail ]
79+ } else {
80+ range = [ sha + '^1' , sha ]
81+ }
82+
4283 commitRange = range . join ( '...' )
4384 }
4485
@@ -49,119 +90,124 @@ export default function diff() {
4990 ! file . dirname ||
5091 range . length !== 2
5192 ) {
52- return next ( )
93+ return
5394 }
5495
55- if ( commitRange !== previousRange ) {
56- cache = { }
96+ // Reset cache.
97+ if ( previousRange !== commitRange ) {
98+ cache = new Map ( )
5799 previousRange = commitRange
58100 }
59101
60- /* c8 ignore next 3 */
61- if ( own . call ( cache , base ) ) {
62- tick ( cache [ base ] )
63- } else {
64- findUp ( '.git' , file . dirname , ( error , git ) => {
65- // Never happens.
66- /* c8 ignore next */
67- if ( error ) return next ( error )
68-
69- // Not testable in a Git repo…
70- /* c8 ignore next 3 */
71- if ( ! git || ! git . dirname ) {
72- return next ( new Error ( 'Not in a git repository' ) )
73- }
102+ let gitFolder = cache . get ( base )
74103
75- cache [ base ] = git . dirname
76- tick ( git . dirname )
77- } )
104+ if ( ! gitFolder ) {
105+ const gitFolderFile = await findUp ( '.git' , file . dirname )
106+
107+ /* c8 ignore next 3 -- not testable in a Git repo… */
108+ if ( ! gitFolderFile || ! gitFolderFile . dirname ) {
109+ throw new Error ( 'Not in a git repository' )
110+ }
111+
112+ cache . set ( base , gitFolderFile . dirname )
113+ gitFolder = gitFolderFile . dirname
114+ }
115+
116+ const diffs = await checkGit ( gitFolder , range )
117+ const ranges = diffs . get ( path . resolve ( file . cwd , file . path ) )
118+
119+ // Unchanged file: drop all messages.
120+ if ( ! ranges || ranges . length === 0 ) {
121+ file . messages . length = 0
122+ return
78123 }
79124
80- /**
81- * @param {string } root
82- */
83- function tick ( root ) {
84- /** @type {Record<string, Array<[number, number]>> } */
85- const diffs = { }
86-
87- gitDiffTree ( path . join ( root , '.git' ) , {
88- // @ts -expect-error: fine.
89- originalRev : range [ 0 ] ,
90- // @ts -expect-error: fine.
91- rev : range [ 1 ]
125+ file . messages = file . messages . filter ( function ( message ) {
126+ return ranges . some ( function ( range ) {
127+ return (
128+ message . line && message . line >= range [ 0 ] && message . line <= range [ 1 ]
129+ )
92130 } )
93- . on ( 'error' , next )
94- . on (
95- 'data' ,
96- /**
97- * @param {string } type
98- * @param {{lines: string, aPath: string, bPath: string} } data
99- */
100- ( type , data ) => {
101- if ( type !== 'patch' ) return
102-
103- const lines = data . lines
104- const re = / ^ @ @ - ( \d + ) , ? ( \d + ) ? \+ ( \d + ) , ? ( \d + ) ? @ @ /
105- const match = lines [ 0 ] . match ( re )
106-
107- // Should not happen, maybe if Git returns weird diffs?
108- /* c8 ignore next */
109- if ( ! match ) return
110-
111- /** @type {Array<[number, number]> } */
112- const ranges = [ ]
113- const start = Number . parseInt ( match [ 3 ] , 10 ) - 1
114- let index = 0
115- /** @type {number|undefined } */
116- let position
117-
118- while ( ++ index < lines . length ) {
119- const line = lines [ index ]
120-
121- if ( line . charAt ( 0 ) === '+' ) {
122- const no = start + index
123-
124- if ( position === undefined ) {
125- position = ranges . length
126- ranges . push ( [ no , no ] )
127- } else {
128- ranges [ position ] [ 1 ] = no
129- }
131+ } )
132+ }
133+ }
134+
135+ /**
136+ * Check a folder.
137+ *
138+ * @param {string } root
139+ * Folder.
140+ * @param {Range } range
141+ * Range.
142+ * @returns {Promise<Map<string, Array<Diff>>> }
143+ * Nothing.
144+ */
145+ function checkGit ( root , range ) {
146+ return new Promise ( function ( resolve , reject ) {
147+ /** @type {Map<string, Array<Diff>> } */
148+ const diffs = new Map ( )
149+ const [ originalRev , rev ] = range
150+
151+ gitDiffTree ( path . join ( root , '.git' ) , { originalRev, rev} )
152+ . on ( 'error' , reject )
153+ . on (
154+ 'data' ,
155+ /**
156+ * @param {DiffType } type
157+ * Data type.
158+ * @param {PatchData } data
159+ * Data.
160+ * @returns {undefined }
161+ * Nothing.
162+ */
163+ function ( type , data ) {
164+ if ( type !== 'patch' ) return
165+
166+ const lines = data . lines
167+ const re = / ^ @ @ - ( \d + ) , ? ( \d + ) ? \+ ( \d + ) , ? ( \d + ) ? @ @ /
168+ const match = lines [ 0 ] . match ( re )
169+
170+ /* c8 ignore next -- should not happen, maybe if Git returns weird diffs? */
171+ if ( ! match ) return
172+
173+ /** @type {Array<Diff> } */
174+ const ranges = [ ]
175+ const start = Number . parseInt ( match [ 3 ] , 10 ) - 1
176+ let index = 0
177+ /** @type {number | undefined } */
178+ let position
179+
180+ while ( ++ index < lines . length ) {
181+ const line = lines [ index ]
182+
183+ if ( line . charAt ( 0 ) === '+' ) {
184+ const no = start + index
185+
186+ if ( position === undefined ) {
187+ position = ranges . length
188+ ranges . push ( [ no , no ] )
130189 } else {
131- position = undefined
190+ ranges [ position ] [ 1 ] = no
132191 }
192+ } else {
193+ position = undefined
133194 }
195+ }
134196
135- const fp = path . resolve ( root , data . bPath )
197+ const fp = path . resolve ( root , data . bPath )
136198
137- // Long diffs.
138- /* c8 ignore next */
139- if ( ! ( fp in diffs ) ) diffs [ fp ] = [ ]
199+ let list = diffs . get ( fp )
140200
141- diffs [ fp ] . push ( ...ranges )
142- }
143- )
144- . on ( 'end' , ( ) => {
145- const fp = path . resolve ( file . cwd , file . path )
146- const ranges = diffs [ fp ]
147-
148- // Unchanged file.
149- if ( ! ranges || ranges . length === 0 ) {
150- file . messages = [ ]
151- return next ( )
201+ if ( ! list ) {
202+ list = [ ]
203+ diffs . set ( fp , list )
152204 }
153205
154- file . messages = file . messages . filter ( ( message ) =>
155- ranges . some (
156- ( range ) =>
157- message . line &&
158- message . line >= range [ 0 ] &&
159- message . line <= range [ 1 ]
160- )
161- )
162-
163- next ( )
164- } )
165- }
166- }
206+ list . push ( ...ranges )
207+ }
208+ )
209+ . on ( 'end' , function ( ) {
210+ resolve ( diffs )
211+ } )
212+ } )
167213}
0 commit comments