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
+
2
26
import 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.
4
30
import gitDiffTree from 'git-diff-tree'
5
31
import { findUp } from 'vfile-find-up'
6
32
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.
9
35
/** @type {string } */
10
36
let previousRange
11
37
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 ) {
18
55
const base = file . dirname
19
- /** @type {string| undefined } */
56
+ /** @type {string | undefined } */
20
57
let commitRange
21
- /** @type {Array<string>| undefined } */
58
+ /** @type {Range | undefined } */
22
59
let range
23
60
24
61
// Looks like Travis.
25
62
if ( process . env . TRAVIS_COMMIT_RANGE ) {
26
63
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 } / ) )
28
66
}
29
67
// Looks like GH Actions.
30
68
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
+
42
83
commitRange = range . join ( '...' )
43
84
}
44
85
@@ -49,119 +90,124 @@ export default function diff() {
49
90
! file . dirname ||
50
91
range . length !== 2
51
92
) {
52
- return next ( )
93
+ return
53
94
}
54
95
55
- if ( commitRange !== previousRange ) {
56
- cache = { }
96
+ // Reset cache.
97
+ if ( previousRange !== commitRange ) {
98
+ cache = new Map ( )
57
99
previousRange = commitRange
58
100
}
59
101
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 )
74
103
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
78
123
}
79
124
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
+ )
92
130
} )
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 ] )
130
189
} else {
131
- position = undefined
190
+ ranges [ position ] [ 1 ] = no
132
191
}
192
+ } else {
193
+ position = undefined
133
194
}
195
+ }
134
196
135
- const fp = path . resolve ( root , data . bPath )
197
+ const fp = path . resolve ( root , data . bPath )
136
198
137
- // Long diffs.
138
- /* c8 ignore next */
139
- if ( ! ( fp in diffs ) ) diffs [ fp ] = [ ]
199
+ let list = diffs . get ( fp )
140
200
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 )
152
204
}
153
205
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
+ } )
167
213
}
0 commit comments