opencode-commit-guard 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +110 -0
- package/dist/index.js +814 -0
- package/index.ts +1 -0
- package/package.json +45 -0
- package/src/config.ts +80 -0
- package/src/plugin.ts +50 -0
- package/src/shell.ts +671 -0
- package/src/types.ts +60 -0
- package/src/validator.ts +224 -0
package/src/shell.ts
ADDED
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
import type { GitCommitInvocation, ShellToken } from "./types.js"
|
|
2
|
+
|
|
3
|
+
const gitGlobalOptionsWithArg = new Set([
|
|
4
|
+
"-C",
|
|
5
|
+
"-c",
|
|
6
|
+
"--git-dir",
|
|
7
|
+
"--work-tree",
|
|
8
|
+
"--namespace",
|
|
9
|
+
"--exec-path",
|
|
10
|
+
"--super-prefix",
|
|
11
|
+
])
|
|
12
|
+
|
|
13
|
+
const commandPrefixes = new Set([
|
|
14
|
+
"!",
|
|
15
|
+
"do",
|
|
16
|
+
"elif",
|
|
17
|
+
"else",
|
|
18
|
+
"if",
|
|
19
|
+
"then",
|
|
20
|
+
"time",
|
|
21
|
+
"until",
|
|
22
|
+
"while",
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
const commitLongOptionsWithArg = new Set([
|
|
26
|
+
"--author",
|
|
27
|
+
"--cleanup",
|
|
28
|
+
"--date",
|
|
29
|
+
"--fixup",
|
|
30
|
+
"--pathspec-from-file",
|
|
31
|
+
"--reedit-message",
|
|
32
|
+
"--reuse-message",
|
|
33
|
+
"--squash",
|
|
34
|
+
"--template",
|
|
35
|
+
"--trailer",
|
|
36
|
+
])
|
|
37
|
+
|
|
38
|
+
interface CommandSubstitution {
|
|
39
|
+
readonly content: string
|
|
40
|
+
readonly end: number
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface CommandStart {
|
|
44
|
+
readonly index: number
|
|
45
|
+
readonly directoryChanges: readonly string[]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isWordSeparator(character: string): boolean {
|
|
49
|
+
return (
|
|
50
|
+
character === " " ||
|
|
51
|
+
character === "\t" ||
|
|
52
|
+
character === "\r" ||
|
|
53
|
+
character === "\n" ||
|
|
54
|
+
character === ";" ||
|
|
55
|
+
character === "&" ||
|
|
56
|
+
character === "|" ||
|
|
57
|
+
character === "(" ||
|
|
58
|
+
character === ")" ||
|
|
59
|
+
character === "{" ||
|
|
60
|
+
character === "}"
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function consumeCommandSubstitution(command: string, start: number): CommandSubstitution {
|
|
65
|
+
let depth = 1
|
|
66
|
+
let quote: "'" | '"' | undefined
|
|
67
|
+
let i = start + 2
|
|
68
|
+
let atWordStart = true
|
|
69
|
+
|
|
70
|
+
while (i < command.length) {
|
|
71
|
+
const character = command[i]
|
|
72
|
+
|
|
73
|
+
if (character === undefined) break
|
|
74
|
+
|
|
75
|
+
if (quote === "'") {
|
|
76
|
+
if (character === "'") quote = undefined
|
|
77
|
+
i++
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (quote === '"') {
|
|
82
|
+
if (character === "\\") {
|
|
83
|
+
i += i + 1 < command.length ? 2 : 1
|
|
84
|
+
} else if (character === '"') {
|
|
85
|
+
quote = undefined
|
|
86
|
+
i++
|
|
87
|
+
} else if (character === "$" && command[i + 1] === "(") {
|
|
88
|
+
i = consumeCommandSubstitution(command, i).end
|
|
89
|
+
} else {
|
|
90
|
+
i++
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (character === "\\") {
|
|
97
|
+
if (command[i + 1] !== "\n") atWordStart = false
|
|
98
|
+
i += i + 1 < command.length ? 2 : 1
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (character === "'") {
|
|
103
|
+
quote = "'"
|
|
104
|
+
atWordStart = false
|
|
105
|
+
i++
|
|
106
|
+
continue
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (character === '"') {
|
|
110
|
+
quote = '"'
|
|
111
|
+
atWordStart = false
|
|
112
|
+
i++
|
|
113
|
+
continue
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (character === "#" && atWordStart) {
|
|
117
|
+
while (i < command.length && command[i] !== "\n") i++
|
|
118
|
+
continue
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (character === "$" && command[i + 1] === "(") {
|
|
122
|
+
i = consumeCommandSubstitution(command, i).end
|
|
123
|
+
atWordStart = false
|
|
124
|
+
continue
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (character === "(") {
|
|
128
|
+
depth++
|
|
129
|
+
i++
|
|
130
|
+
atWordStart = true
|
|
131
|
+
continue
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (character === ")") {
|
|
135
|
+
depth--
|
|
136
|
+
|
|
137
|
+
if (depth === 0) {
|
|
138
|
+
return { content: command.slice(start + 2, i), end: i + 1 }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
atWordStart = true
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (isWordSeparator(character)) atWordStart = true
|
|
145
|
+
else atWordStart = false
|
|
146
|
+
i++
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return { content: command.slice(start + 2), end: command.length }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function pushWord(
|
|
153
|
+
tokens: ShellToken[],
|
|
154
|
+
value: string,
|
|
155
|
+
substitutions: readonly string[],
|
|
156
|
+
): void {
|
|
157
|
+
if (substitutions.length > 0) {
|
|
158
|
+
tokens.push({ type: "word", value, substitutions })
|
|
159
|
+
} else {
|
|
160
|
+
tokens.push({ type: "word", value })
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function tokenizeShell(command: string): ShellToken[] {
|
|
165
|
+
const tokens: ShellToken[] = []
|
|
166
|
+
let i = 0
|
|
167
|
+
|
|
168
|
+
while (i < command.length) {
|
|
169
|
+
const character = command[i]
|
|
170
|
+
|
|
171
|
+
if (character === undefined) break
|
|
172
|
+
|
|
173
|
+
if (character === " " || character === "\t" || character === "\r") {
|
|
174
|
+
i++
|
|
175
|
+
continue
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (character === "\\" && command[i + 1] === "\n") {
|
|
179
|
+
i += 2
|
|
180
|
+
continue
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (character === "#") {
|
|
184
|
+
while (i < command.length && command[i] !== "\n") i++
|
|
185
|
+
continue
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const twoCharacters = command.slice(i, i + 2)
|
|
189
|
+
|
|
190
|
+
if (
|
|
191
|
+
twoCharacters === "&&" ||
|
|
192
|
+
twoCharacters === "||" ||
|
|
193
|
+
twoCharacters === "|&" ||
|
|
194
|
+
twoCharacters === ";;"
|
|
195
|
+
) {
|
|
196
|
+
tokens.push({ type: "operator", value: twoCharacters })
|
|
197
|
+
i += 2
|
|
198
|
+
continue
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (
|
|
202
|
+
character === "|" ||
|
|
203
|
+
character === ";" ||
|
|
204
|
+
character === "&" ||
|
|
205
|
+
character === "\n" ||
|
|
206
|
+
character === "(" ||
|
|
207
|
+
character === ")" ||
|
|
208
|
+
character === "{" ||
|
|
209
|
+
character === "}"
|
|
210
|
+
) {
|
|
211
|
+
tokens.push({ type: "operator", value: character })
|
|
212
|
+
i++
|
|
213
|
+
continue
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const redirectMatch = command.slice(i).match(/^(\d+)?(<<<|>>|<<|>&|<&|>\||>|<)(?:-|\d+)?/)
|
|
217
|
+
|
|
218
|
+
if (redirectMatch !== null && redirectMatch[0] !== undefined) {
|
|
219
|
+
tokens.push({ type: "redirect", value: redirectMatch[0] })
|
|
220
|
+
i += redirectMatch[0].length
|
|
221
|
+
continue
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let value = ""
|
|
225
|
+
const substitutions: string[] = []
|
|
226
|
+
|
|
227
|
+
while (i < command.length) {
|
|
228
|
+
const current = command[i]
|
|
229
|
+
|
|
230
|
+
if (current === undefined || isWordSeparator(current)) break
|
|
231
|
+
|
|
232
|
+
if (current === "\\" && command[i + 1] === "\n") {
|
|
233
|
+
i += 2
|
|
234
|
+
continue
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (current === "\\") {
|
|
238
|
+
if (i + 1 < command.length) {
|
|
239
|
+
value += command[i + 1]
|
|
240
|
+
i += 2
|
|
241
|
+
} else {
|
|
242
|
+
value += "\\"
|
|
243
|
+
i++
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
continue
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (current === "$" && command[i + 1] === "'") {
|
|
250
|
+
i += 2
|
|
251
|
+
|
|
252
|
+
while (i < command.length) {
|
|
253
|
+
const ansiCharacter = command[i]
|
|
254
|
+
|
|
255
|
+
if (ansiCharacter === undefined) break
|
|
256
|
+
|
|
257
|
+
if (ansiCharacter === "'") {
|
|
258
|
+
i++
|
|
259
|
+
break
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (ansiCharacter === "\\" && i + 1 < command.length) {
|
|
263
|
+
const escaped = command[i + 1]
|
|
264
|
+
|
|
265
|
+
if (escaped === "n") value += "\n"
|
|
266
|
+
else if (escaped === "t") value += "\t"
|
|
267
|
+
else if (escaped === "r") value += "\r"
|
|
268
|
+
else if (escaped === "\\") value += "\\"
|
|
269
|
+
else if (escaped === "'") value += "'"
|
|
270
|
+
else if (escaped === '"') value += '"'
|
|
271
|
+
else value += escaped
|
|
272
|
+
i += 2
|
|
273
|
+
} else {
|
|
274
|
+
value += ansiCharacter
|
|
275
|
+
i++
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
continue
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (current === "'") {
|
|
283
|
+
i++
|
|
284
|
+
|
|
285
|
+
while (i < command.length && command[i] !== "'") {
|
|
286
|
+
value += command[i]
|
|
287
|
+
i++
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (command[i] === "'") i++
|
|
291
|
+
continue
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (current === '"') {
|
|
295
|
+
i++
|
|
296
|
+
|
|
297
|
+
while (i < command.length && command[i] !== '"') {
|
|
298
|
+
const quotedCharacter = command[i]
|
|
299
|
+
|
|
300
|
+
if (quotedCharacter === undefined) break
|
|
301
|
+
|
|
302
|
+
if (quotedCharacter === "\\" && i + 1 < command.length) {
|
|
303
|
+
const escaped = command[i + 1]
|
|
304
|
+
|
|
305
|
+
if (escaped === "\n") {
|
|
306
|
+
i += 2
|
|
307
|
+
} else if (escaped === '"' || escaped === "\\" || escaped === "$" || escaped === "`") {
|
|
308
|
+
value += escaped
|
|
309
|
+
i += 2
|
|
310
|
+
} else {
|
|
311
|
+
value += `\\${escaped}`
|
|
312
|
+
i += 2
|
|
313
|
+
}
|
|
314
|
+
} else if (quotedCharacter === "$" && command[i + 1] === "(") {
|
|
315
|
+
const substitution = consumeCommandSubstitution(command, i)
|
|
316
|
+
value += command.slice(i, substitution.end)
|
|
317
|
+
substitutions.push(substitution.content)
|
|
318
|
+
i = substitution.end
|
|
319
|
+
} else {
|
|
320
|
+
value += quotedCharacter
|
|
321
|
+
i++
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (command[i] === '"') i++
|
|
326
|
+
continue
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (current === "$" && command[i + 1] === "(") {
|
|
330
|
+
const substitution = consumeCommandSubstitution(command, i)
|
|
331
|
+
value += command.slice(i, substitution.end)
|
|
332
|
+
substitutions.push(substitution.content)
|
|
333
|
+
i = substitution.end
|
|
334
|
+
continue
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const nextRedirect = command.slice(i).match(/^(\d+)?(<<<|>>|<<|>&|<&|>\||>|<)(?:-|\d+)?/)
|
|
338
|
+
|
|
339
|
+
if (nextRedirect !== null) break
|
|
340
|
+
|
|
341
|
+
value += current
|
|
342
|
+
i++
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
pushWord(tokens, value, substitutions)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return tokens
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function splitSimpleCommands(tokens: readonly ShellToken[]): ShellToken[][] {
|
|
352
|
+
const commands: ShellToken[][] = []
|
|
353
|
+
let current: ShellToken[] = []
|
|
354
|
+
|
|
355
|
+
for (const token of tokens) {
|
|
356
|
+
if (token.type === "operator") {
|
|
357
|
+
if (current.length > 0) commands.push(current)
|
|
358
|
+
current = []
|
|
359
|
+
} else {
|
|
360
|
+
current.push(token)
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (current.length > 0) commands.push(current)
|
|
365
|
+
|
|
366
|
+
return commands
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function extractCommandWords(tokens: readonly ShellToken[]): string[] {
|
|
370
|
+
const words: string[] = []
|
|
371
|
+
let i = 0
|
|
372
|
+
|
|
373
|
+
while (i < tokens.length) {
|
|
374
|
+
const token = tokens[i]
|
|
375
|
+
|
|
376
|
+
if (token === undefined) break
|
|
377
|
+
|
|
378
|
+
if (token.type === "redirect") {
|
|
379
|
+
if (!token.value.includes("&") && tokens[i + 1]?.type === "word") i += 2
|
|
380
|
+
else i++
|
|
381
|
+
continue
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (token.type === "word") {
|
|
385
|
+
words.push(token.value)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
i++
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return words
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function isAssignment(word: string): boolean {
|
|
395
|
+
return /^[a-zA-Z_][a-zA-Z0-9_]*=/.test(word)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function skipAssignments(words: readonly string[], start: number): number {
|
|
399
|
+
let index = start
|
|
400
|
+
|
|
401
|
+
while (words[index] !== undefined && isAssignment(words[index])) index++
|
|
402
|
+
|
|
403
|
+
return index
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function findCommandStart(words: readonly string[]): CommandStart {
|
|
407
|
+
let index = skipAssignments(words, 0)
|
|
408
|
+
const directoryChanges: string[] = []
|
|
409
|
+
|
|
410
|
+
while (index < words.length) {
|
|
411
|
+
const word = words[index]
|
|
412
|
+
|
|
413
|
+
if (word !== undefined && commandPrefixes.has(word)) {
|
|
414
|
+
index = skipAssignments(words, index + 1)
|
|
415
|
+
continue
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (word === "env") {
|
|
419
|
+
index++
|
|
420
|
+
|
|
421
|
+
while (index < words.length) {
|
|
422
|
+
const argument = words[index]
|
|
423
|
+
|
|
424
|
+
if (argument === undefined) break
|
|
425
|
+
|
|
426
|
+
if (argument === "--") {
|
|
427
|
+
index++
|
|
428
|
+
break
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (argument === "-u" || argument === "--unset") {
|
|
432
|
+
index += 2
|
|
433
|
+
} else if (argument === "-C" || argument === "--chdir") {
|
|
434
|
+
const directory = words[index + 1]
|
|
435
|
+
|
|
436
|
+
if (directory !== undefined) {
|
|
437
|
+
directoryChanges.push(directory)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
index += 2
|
|
441
|
+
} else if (argument.startsWith("--chdir=")) {
|
|
442
|
+
directoryChanges.push(argument.slice("--chdir=".length))
|
|
443
|
+
index++
|
|
444
|
+
} else if (/^-C.+/.test(argument)) {
|
|
445
|
+
directoryChanges.push(argument.slice(2))
|
|
446
|
+
index++
|
|
447
|
+
} else if (argument.startsWith("--unset=") || /^-u.+/.test(argument)) {
|
|
448
|
+
index++
|
|
449
|
+
} else if (argument.startsWith("-")) {
|
|
450
|
+
index++
|
|
451
|
+
} else if (isAssignment(argument)) {
|
|
452
|
+
index++
|
|
453
|
+
} else {
|
|
454
|
+
break
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
index = skipAssignments(words, index)
|
|
459
|
+
continue
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (word === "exec") {
|
|
463
|
+
index++
|
|
464
|
+
|
|
465
|
+
while (index < words.length) {
|
|
466
|
+
const argument = words[index]
|
|
467
|
+
|
|
468
|
+
if (argument === "--") {
|
|
469
|
+
index++
|
|
470
|
+
break
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (argument === "-a") index += 2
|
|
474
|
+
else if (argument !== undefined && argument.startsWith("-")) index++
|
|
475
|
+
else break
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
index = skipAssignments(words, index)
|
|
479
|
+
continue
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (word === "command" || word === "nohup" || word === "builtin") {
|
|
483
|
+
index++
|
|
484
|
+
|
|
485
|
+
while (words[index]?.startsWith("-")) index++
|
|
486
|
+
index = skipAssignments(words, index)
|
|
487
|
+
continue
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
break
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return { index, directoryChanges }
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function extractInvocation(
|
|
497
|
+
words: readonly string[],
|
|
498
|
+
commandStart: CommandStart,
|
|
499
|
+
): GitCommitInvocation | undefined {
|
|
500
|
+
let wordIndex = commandStart.index
|
|
501
|
+
const executable = words[wordIndex]
|
|
502
|
+
|
|
503
|
+
if (executable === undefined || (executable !== "git" && !executable.endsWith("/git"))) return undefined
|
|
504
|
+
wordIndex++
|
|
505
|
+
const directoryChanges = [...commandStart.directoryChanges]
|
|
506
|
+
|
|
507
|
+
let subcommand: string | undefined
|
|
508
|
+
|
|
509
|
+
while (wordIndex < words.length) {
|
|
510
|
+
const argument = words[wordIndex]
|
|
511
|
+
|
|
512
|
+
if (argument === undefined) break
|
|
513
|
+
|
|
514
|
+
if (argument === "--") {
|
|
515
|
+
subcommand = words[wordIndex + 1]
|
|
516
|
+
wordIndex += 2
|
|
517
|
+
break
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (gitGlobalOptionsWithArg.has(argument)) {
|
|
521
|
+
const optionValue = words[wordIndex + 1]
|
|
522
|
+
|
|
523
|
+
if (argument === "-C" && optionValue !== undefined) {
|
|
524
|
+
directoryChanges.push(optionValue)
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
wordIndex += 2
|
|
528
|
+
continue
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (
|
|
532
|
+
argument.startsWith("--git-dir=") ||
|
|
533
|
+
argument.startsWith("--work-tree=") ||
|
|
534
|
+
argument.startsWith("--namespace=") ||
|
|
535
|
+
argument.startsWith("--exec-path=") ||
|
|
536
|
+
argument.startsWith("--super-prefix=") ||
|
|
537
|
+
argument.startsWith("-c")
|
|
538
|
+
) {
|
|
539
|
+
wordIndex++
|
|
540
|
+
continue
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (argument.startsWith("-C")) {
|
|
544
|
+
directoryChanges.push(argument.slice(2))
|
|
545
|
+
wordIndex++
|
|
546
|
+
continue
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (argument.startsWith("-")) {
|
|
550
|
+
wordIndex++
|
|
551
|
+
continue
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
subcommand = argument
|
|
555
|
+
wordIndex++
|
|
556
|
+
break
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (subcommand !== "commit") return undefined
|
|
560
|
+
|
|
561
|
+
const messages: string[] = []
|
|
562
|
+
const filePaths: string[] = []
|
|
563
|
+
let hasSignoffFlag = false
|
|
564
|
+
let isAmend = false
|
|
565
|
+
let hasNoEdit = false
|
|
566
|
+
let isHelp = false
|
|
567
|
+
|
|
568
|
+
const collectMessage = (word: string | undefined): void => {
|
|
569
|
+
if (word === undefined) return
|
|
570
|
+
messages.push(word)
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const collectFile = (word: string | undefined): void => {
|
|
574
|
+
if (word === undefined) return
|
|
575
|
+
filePaths.push(word)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
for (let index = wordIndex; index < words.length; index++) {
|
|
579
|
+
const argument = words[index]
|
|
580
|
+
|
|
581
|
+
if (argument === undefined) break
|
|
582
|
+
|
|
583
|
+
if (argument === "--") break
|
|
584
|
+
|
|
585
|
+
if (argument === "-h" || argument === "--help") {
|
|
586
|
+
isHelp = true
|
|
587
|
+
} else if (argument === "--amend") {
|
|
588
|
+
isAmend = true
|
|
589
|
+
} else if (argument === "--no-edit") {
|
|
590
|
+
hasNoEdit = true
|
|
591
|
+
} else if (argument === "--edit") {
|
|
592
|
+
hasNoEdit = false
|
|
593
|
+
} else if (argument === "-s" || argument === "--signoff") {
|
|
594
|
+
hasSignoffFlag = true
|
|
595
|
+
} else if (argument === "--no-signoff") {
|
|
596
|
+
hasSignoffFlag = false
|
|
597
|
+
} else if (argument === "-m" || argument === "--message") {
|
|
598
|
+
index++
|
|
599
|
+
collectMessage(words[index])
|
|
600
|
+
} else if (argument.startsWith("--message=")) {
|
|
601
|
+
collectMessage(argument.slice("--message=".length))
|
|
602
|
+
} else if (argument === "-F" || argument === "--file") {
|
|
603
|
+
index++
|
|
604
|
+
collectFile(words[index])
|
|
605
|
+
} else if (argument.startsWith("--file=")) {
|
|
606
|
+
collectFile(argument.slice("--file=".length))
|
|
607
|
+
} else if (commitLongOptionsWithArg.has(argument)) {
|
|
608
|
+
index++
|
|
609
|
+
} else if (argument.startsWith("-") && !argument.startsWith("--") && argument.length > 1) {
|
|
610
|
+
for (let characterIndex = 1; characterIndex < argument.length; characterIndex++) {
|
|
611
|
+
const option = argument[characterIndex]
|
|
612
|
+
|
|
613
|
+
if (option === "s") {
|
|
614
|
+
hasSignoffFlag = true
|
|
615
|
+
} else if (option === "h") {
|
|
616
|
+
isHelp = true
|
|
617
|
+
} else if (option === "e") {
|
|
618
|
+
hasNoEdit = false
|
|
619
|
+
} else if (option === "m" || option === "F") {
|
|
620
|
+
const attached = argument.slice(characterIndex + 1)
|
|
621
|
+
|
|
622
|
+
const input = attached.length > 0
|
|
623
|
+
? attached
|
|
624
|
+
: words[++index]
|
|
625
|
+
|
|
626
|
+
if (option === "m") collectMessage(input)
|
|
627
|
+
else collectFile(input)
|
|
628
|
+
break
|
|
629
|
+
} else if (option === "S") {
|
|
630
|
+
break
|
|
631
|
+
} else if (option === "u") {
|
|
632
|
+
break
|
|
633
|
+
} else if (option === "c" || option === "C" || option === "t") {
|
|
634
|
+
if (argument.slice(characterIndex + 1).length === 0) index++
|
|
635
|
+
break
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
return {
|
|
642
|
+
messages,
|
|
643
|
+
filePaths,
|
|
644
|
+
hasSignoffFlag,
|
|
645
|
+
isAmend,
|
|
646
|
+
hasNoEdit,
|
|
647
|
+
isHelp,
|
|
648
|
+
directoryChanges,
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
export function extractGitCommits(command: string): GitCommitInvocation[] {
|
|
653
|
+
const tokens = tokenizeShell(command)
|
|
654
|
+
const invocations: GitCommitInvocation[] = []
|
|
655
|
+
|
|
656
|
+
for (const commandTokens of splitSimpleCommands(tokens)) {
|
|
657
|
+
const words = extractCommandWords(commandTokens)
|
|
658
|
+
const commandStart = findCommandStart(words)
|
|
659
|
+
const invocation = extractInvocation(words, commandStart)
|
|
660
|
+
|
|
661
|
+
if (invocation !== undefined) invocations.push(invocation)
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
for (const token of tokens) {
|
|
665
|
+
for (const substitution of token.substitutions ?? []) {
|
|
666
|
+
invocations.push(...extractGitCommits(substitution))
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
return invocations
|
|
671
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export type JsonPrimitive = string | number | boolean | null
|
|
2
|
+
|
|
3
|
+
export type JsonValue =
|
|
4
|
+
| JsonPrimitive
|
|
5
|
+
| JsonValue[]
|
|
6
|
+
| { readonly [key: string]: JsonValue | undefined }
|
|
7
|
+
|
|
8
|
+
export function isJSONString(value: JsonValue | undefined): value is string {
|
|
9
|
+
return Object.prototype.toString.call(value) === "[object String]"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isJSONNumber(value: JsonValue | undefined): value is number {
|
|
13
|
+
return Object.prototype.toString.call(value) === "[object Number]"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function isJSONBoolean(value: JsonValue | undefined): value is boolean {
|
|
17
|
+
return Object.prototype.toString.call(value) === "[object Boolean]"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isRecord(value: JsonValue | undefined): value is Record<string, JsonValue> {
|
|
21
|
+
return value instanceof Object && !Array.isArray(value)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CommitGuardConfig {
|
|
25
|
+
readonly requireScope: boolean
|
|
26
|
+
readonly allowedScopes?: readonly string[]
|
|
27
|
+
readonly maxLineLength: number
|
|
28
|
+
readonly requireSignoff: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const defaultConfig: CommitGuardConfig = {
|
|
32
|
+
requireScope: true,
|
|
33
|
+
allowedScopes: undefined,
|
|
34
|
+
maxLineLength: 72,
|
|
35
|
+
requireSignoff: true,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type ShellTokenType = "word" | "operator" | "redirect"
|
|
39
|
+
|
|
40
|
+
export interface ShellToken {
|
|
41
|
+
readonly type: ShellTokenType
|
|
42
|
+
readonly value: string
|
|
43
|
+
readonly substitutions?: readonly string[]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface GitCommitInvocation {
|
|
47
|
+
readonly messages: readonly string[]
|
|
48
|
+
readonly filePaths: readonly string[]
|
|
49
|
+
readonly hasSignoffFlag: boolean
|
|
50
|
+
readonly isAmend: boolean
|
|
51
|
+
readonly hasNoEdit?: boolean
|
|
52
|
+
readonly isHelp: boolean
|
|
53
|
+
readonly directoryChanges?: readonly string[]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface OverlongLine {
|
|
57
|
+
readonly lineNumber: number
|
|
58
|
+
readonly length: number
|
|
59
|
+
readonly text: string
|
|
60
|
+
}
|