opencode-commit-guard 1.0.0 → 1.0.1
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/dist/index.js +47 -11
- package/package.json +1 -1
- package/src/shell.ts +11 -2
- package/src/types.ts +1 -0
- package/src/validator.ts +51 -9
package/dist/index.js
CHANGED
|
@@ -525,6 +525,7 @@ function extractInvocation(words, commandStart) {
|
|
|
525
525
|
let hasSignoffFlag = false;
|
|
526
526
|
let isAmend = false;
|
|
527
527
|
let hasNoEdit = false;
|
|
528
|
+
let isFixup = false;
|
|
528
529
|
let isHelp = false;
|
|
529
530
|
const collectMessage = (word) => {
|
|
530
531
|
if (word === undefined)
|
|
@@ -540,6 +541,8 @@ function extractInvocation(words, commandStart) {
|
|
|
540
541
|
const argument = words[index];
|
|
541
542
|
if (argument === undefined)
|
|
542
543
|
break;
|
|
544
|
+
const equalsIndex = argument.indexOf("=");
|
|
545
|
+
const longOptionName = equalsIndex > 0 ? argument.slice(0, equalsIndex) : argument;
|
|
543
546
|
if (argument === "--")
|
|
544
547
|
break;
|
|
545
548
|
if (argument === "-h" || argument === "--help") {
|
|
@@ -564,8 +567,13 @@ function extractInvocation(words, commandStart) {
|
|
|
564
567
|
collectFile(words[index]);
|
|
565
568
|
} else if (argument.startsWith("--file=")) {
|
|
566
569
|
collectFile(argument.slice("--file=".length));
|
|
567
|
-
} else if (
|
|
568
|
-
|
|
570
|
+
} else if (argument === "--fixup" || argument.startsWith("--fixup=")) {
|
|
571
|
+
isFixup = true;
|
|
572
|
+
if (equalsIndex < 0)
|
|
573
|
+
index++;
|
|
574
|
+
} else if (commitLongOptionsWithArg.has(longOptionName)) {
|
|
575
|
+
if (equalsIndex < 0)
|
|
576
|
+
index++;
|
|
569
577
|
} else if (argument.startsWith("-") && !argument.startsWith("--") && argument.length > 1) {
|
|
570
578
|
for (let characterIndex = 1;characterIndex < argument.length; characterIndex++) {
|
|
571
579
|
const option = argument[characterIndex];
|
|
@@ -601,6 +609,7 @@ function extractInvocation(words, commandStart) {
|
|
|
601
609
|
hasSignoffFlag,
|
|
602
610
|
isAmend,
|
|
603
611
|
hasNoEdit,
|
|
612
|
+
isFixup,
|
|
604
613
|
isHelp,
|
|
605
614
|
directoryChanges
|
|
606
615
|
};
|
|
@@ -624,6 +633,7 @@ function extractGitCommits(command) {
|
|
|
624
633
|
}
|
|
625
634
|
|
|
626
635
|
// src/validator.ts
|
|
636
|
+
import { spawnSync } from "child_process";
|
|
627
637
|
import { closeSync, constants, existsSync, fstatSync, openSync, readSync, statSync } from "fs";
|
|
628
638
|
import { resolve } from "path";
|
|
629
639
|
var scopePattern = /^([a-zA-Z0-9_\-./]+(?:\([a-zA-Z0-9_\-./]+\))?):\s+(.+)$/;
|
|
@@ -670,14 +680,34 @@ function validateGitCommits(invocations, config, originalCommand, workingDirecto
|
|
|
670
680
|
if (invocation.isHelp) {
|
|
671
681
|
continue;
|
|
672
682
|
}
|
|
673
|
-
if (invocation.isAmend && invocation.hasNoEdit === true && invocation.messages.length === 0 && invocation.filePaths.length === 0) {
|
|
674
|
-
continue;
|
|
675
|
-
}
|
|
676
683
|
const collectedMessages = [...invocation.messages];
|
|
677
684
|
let messageDirectory = workingDirectory ?? process.cwd();
|
|
678
685
|
for (const directoryChange of invocation.directoryChanges ?? []) {
|
|
679
686
|
messageDirectory = resolve(messageDirectory, directoryChange);
|
|
680
687
|
}
|
|
688
|
+
if (invocation.isAmend && invocation.hasNoEdit === true && invocation.messages.length === 0 && invocation.filePaths.length === 0) {
|
|
689
|
+
const result = spawnSync("git", ["log", "-1", "--format=%B", "HEAD"], {
|
|
690
|
+
cwd: messageDirectory,
|
|
691
|
+
encoding: "utf-8",
|
|
692
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
693
|
+
});
|
|
694
|
+
if (result.error !== undefined) {
|
|
695
|
+
allViolations.push(`Failed to read the existing HEAD commit message: ${result.error.message}`);
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
if (result.status !== 0) {
|
|
699
|
+
const detail = result.stderr.trim();
|
|
700
|
+
const suffix = detail.length > 0 ? `: ${detail}` : ".";
|
|
701
|
+
allViolations.push(`Failed to read the existing HEAD commit message${suffix}`);
|
|
702
|
+
continue;
|
|
703
|
+
}
|
|
704
|
+
const existingMessage = result.stdout.replace(/[\r\n]+$/, "");
|
|
705
|
+
if (existingMessage.trim().length === 0) {
|
|
706
|
+
allViolations.push("The existing HEAD commit message is empty.");
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
collectedMessages.push(existingMessage);
|
|
710
|
+
}
|
|
681
711
|
const filePath = invocation.filePaths.at(-1);
|
|
682
712
|
if (filePath !== undefined) {
|
|
683
713
|
if (filePath === "-") {
|
|
@@ -722,6 +752,9 @@ function validateGitCommits(invocations, config, originalCommand, workingDirecto
|
|
|
722
752
|
}
|
|
723
753
|
}
|
|
724
754
|
if (collectedMessages.length === 0) {
|
|
755
|
+
if (invocation.isFixup === true) {
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
725
758
|
if (invocation.filePaths.length === 0) {
|
|
726
759
|
allViolations.push('No commit message provided. Commits in OpenCode must provide a commit message via -m "<scope>: <subject>" or -F <file>.');
|
|
727
760
|
}
|
|
@@ -733,21 +766,24 @@ function validateGitCommits(invocations, config, originalCommand, workingDirecto
|
|
|
733
766
|
const lines = fullMessage.split(/\r?\n/);
|
|
734
767
|
const firstLine = lines[0] ?? "";
|
|
735
768
|
const subjectLine = firstLine.trim();
|
|
736
|
-
const
|
|
769
|
+
const effectiveSubject = subjectLine.replace(/^(?:(?:fixup|squash)!\s+)+/, "");
|
|
770
|
+
const scopeMatch = effectiveSubject.match(scopePattern);
|
|
737
771
|
if (scopeMatch?.[1] !== undefined && config.allowedScopes !== undefined && config.allowedScopes.length > 0) {
|
|
738
772
|
const scopeViolation = validateAllowedScope(scopeMatch[1], config.allowedScopes);
|
|
739
|
-
if (scopeViolation !== undefined)
|
|
740
|
-
|
|
773
|
+
if (scopeViolation !== undefined) {
|
|
774
|
+
const fullSubjectDetail = effectiveSubject === subjectLine ? "" : ` Subject line: "${subjectLine}".`;
|
|
775
|
+
allViolations.push(`${scopeViolation}${fullSubjectDetail}`);
|
|
776
|
+
}
|
|
741
777
|
}
|
|
742
778
|
if (config.requireScope) {
|
|
743
779
|
if (subjectLine.length === 0) {
|
|
744
780
|
allViolations.push('Subject line is empty. The commit message must begin with "<scope>: <subject>".');
|
|
745
781
|
} else if (scopeMatch === null) {
|
|
746
|
-
if (/^:\s*/.test(
|
|
782
|
+
if (/^:\s*/.test(effectiveSubject)) {
|
|
747
783
|
allViolations.push(`Missing scope before colon in subject line "${subjectLine}". Expected format: "<scope>: <subject>".`);
|
|
748
|
-
} else if (/^[^:]+:\S/.test(
|
|
784
|
+
} else if (/^[^:]+:\S/.test(effectiveSubject)) {
|
|
749
785
|
allViolations.push(`Missing space after colon in subject line "${subjectLine}". Expected format: "<scope>: <subject>".`);
|
|
750
|
-
} else if (/^[^:]+:\s*$/.test(
|
|
786
|
+
} else if (/^[^:]+:\s*$/.test(effectiveSubject)) {
|
|
751
787
|
allViolations.push(`Subject text after colon is empty in "${subjectLine}". Expected format: "<scope>: <subject>".`);
|
|
752
788
|
} else {
|
|
753
789
|
allViolations.push(`Missing scope in subject line "${subjectLine}". First line must follow "<scope>: <subject>" format (e.g., "kernel: add support for foo" or "feat(parser): add subshell support").`);
|
package/package.json
CHANGED
package/src/shell.ts
CHANGED
|
@@ -563,6 +563,7 @@ function extractInvocation(
|
|
|
563
563
|
let hasSignoffFlag = false
|
|
564
564
|
let isAmend = false
|
|
565
565
|
let hasNoEdit = false
|
|
566
|
+
let isFixup = false
|
|
566
567
|
let isHelp = false
|
|
567
568
|
|
|
568
569
|
const collectMessage = (word: string | undefined): void => {
|
|
@@ -580,6 +581,9 @@ function extractInvocation(
|
|
|
580
581
|
|
|
581
582
|
if (argument === undefined) break
|
|
582
583
|
|
|
584
|
+
const equalsIndex = argument.indexOf("=")
|
|
585
|
+
const longOptionName = equalsIndex > 0 ? argument.slice(0, equalsIndex) : argument
|
|
586
|
+
|
|
583
587
|
if (argument === "--") break
|
|
584
588
|
|
|
585
589
|
if (argument === "-h" || argument === "--help") {
|
|
@@ -604,8 +608,12 @@ function extractInvocation(
|
|
|
604
608
|
collectFile(words[index])
|
|
605
609
|
} else if (argument.startsWith("--file=")) {
|
|
606
610
|
collectFile(argument.slice("--file=".length))
|
|
607
|
-
} else if (
|
|
608
|
-
|
|
611
|
+
} else if (argument === "--fixup" || argument.startsWith("--fixup=")) {
|
|
612
|
+
isFixup = true
|
|
613
|
+
|
|
614
|
+
if (equalsIndex < 0) index++
|
|
615
|
+
} else if (commitLongOptionsWithArg.has(longOptionName)) {
|
|
616
|
+
if (equalsIndex < 0) index++
|
|
609
617
|
} else if (argument.startsWith("-") && !argument.startsWith("--") && argument.length > 1) {
|
|
610
618
|
for (let characterIndex = 1; characterIndex < argument.length; characterIndex++) {
|
|
611
619
|
const option = argument[characterIndex]
|
|
@@ -644,6 +652,7 @@ function extractInvocation(
|
|
|
644
652
|
hasSignoffFlag,
|
|
645
653
|
isAmend,
|
|
646
654
|
hasNoEdit,
|
|
655
|
+
isFixup,
|
|
647
656
|
isHelp,
|
|
648
657
|
directoryChanges,
|
|
649
658
|
}
|
package/src/types.ts
CHANGED
package/src/validator.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process"
|
|
1
2
|
import { closeSync, constants, existsSync, fstatSync, openSync, readSync, statSync } from "node:fs"
|
|
2
3
|
import { resolve } from "node:path"
|
|
3
4
|
import type {
|
|
@@ -68,10 +69,6 @@ export function validateGitCommits(
|
|
|
68
69
|
continue
|
|
69
70
|
}
|
|
70
71
|
|
|
71
|
-
if (invocation.isAmend && invocation.hasNoEdit === true && invocation.messages.length === 0 && invocation.filePaths.length === 0) {
|
|
72
|
-
continue
|
|
73
|
-
}
|
|
74
|
-
|
|
75
72
|
const collectedMessages: string[] = [...invocation.messages]
|
|
76
73
|
let messageDirectory = workingDirectory ?? process.cwd()
|
|
77
74
|
|
|
@@ -79,6 +76,40 @@ export function validateGitCommits(
|
|
|
79
76
|
messageDirectory = resolve(messageDirectory, directoryChange)
|
|
80
77
|
}
|
|
81
78
|
|
|
79
|
+
if (
|
|
80
|
+
invocation.isAmend &&
|
|
81
|
+
invocation.hasNoEdit === true &&
|
|
82
|
+
invocation.messages.length === 0 &&
|
|
83
|
+
invocation.filePaths.length === 0
|
|
84
|
+
) {
|
|
85
|
+
const result = spawnSync("git", ["log", "-1", "--format=%B", "HEAD"], {
|
|
86
|
+
cwd: messageDirectory,
|
|
87
|
+
encoding: "utf-8",
|
|
88
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
if (result.error !== undefined) {
|
|
92
|
+
allViolations.push(`Failed to read the existing HEAD commit message: ${result.error.message}`)
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (result.status !== 0) {
|
|
97
|
+
const detail = result.stderr.trim()
|
|
98
|
+
const suffix = detail.length > 0 ? `: ${detail}` : "."
|
|
99
|
+
allViolations.push(`Failed to read the existing HEAD commit message${suffix}`)
|
|
100
|
+
continue
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const existingMessage = result.stdout.replace(/[\r\n]+$/, "")
|
|
104
|
+
|
|
105
|
+
if (existingMessage.trim().length === 0) {
|
|
106
|
+
allViolations.push("The existing HEAD commit message is empty.")
|
|
107
|
+
continue
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
collectedMessages.push(existingMessage)
|
|
111
|
+
}
|
|
112
|
+
|
|
82
113
|
const filePath = invocation.filePaths.at(-1)
|
|
83
114
|
|
|
84
115
|
if (filePath !== undefined) {
|
|
@@ -136,6 +167,10 @@ export function validateGitCommits(
|
|
|
136
167
|
}
|
|
137
168
|
|
|
138
169
|
if (collectedMessages.length === 0) {
|
|
170
|
+
if (invocation.isFixup === true) {
|
|
171
|
+
continue
|
|
172
|
+
}
|
|
173
|
+
|
|
139
174
|
if (invocation.filePaths.length === 0) {
|
|
140
175
|
allViolations.push(
|
|
141
176
|
'No commit message provided. Commits in OpenCode must provide a commit message via -m "<scope>: <subject>" or -F <file>.',
|
|
@@ -149,7 +184,8 @@ export function validateGitCommits(
|
|
|
149
184
|
const lines = fullMessage.split(/\r?\n/)
|
|
150
185
|
const firstLine = lines[0] ?? ""
|
|
151
186
|
const subjectLine = firstLine.trim()
|
|
152
|
-
const
|
|
187
|
+
const effectiveSubject = subjectLine.replace(/^(?:(?:fixup|squash)!\s+)+/, "")
|
|
188
|
+
const scopeMatch = effectiveSubject.match(scopePattern)
|
|
153
189
|
|
|
154
190
|
if (
|
|
155
191
|
scopeMatch?.[1] !== undefined &&
|
|
@@ -158,22 +194,28 @@ export function validateGitCommits(
|
|
|
158
194
|
) {
|
|
159
195
|
const scopeViolation = validateAllowedScope(scopeMatch[1], config.allowedScopes)
|
|
160
196
|
|
|
161
|
-
if (scopeViolation !== undefined)
|
|
197
|
+
if (scopeViolation !== undefined) {
|
|
198
|
+
const fullSubjectDetail = effectiveSubject === subjectLine
|
|
199
|
+
? ""
|
|
200
|
+
: ` Subject line: "${subjectLine}".`
|
|
201
|
+
|
|
202
|
+
allViolations.push(`${scopeViolation}${fullSubjectDetail}`)
|
|
203
|
+
}
|
|
162
204
|
}
|
|
163
205
|
|
|
164
206
|
if (config.requireScope) {
|
|
165
207
|
if (subjectLine.length === 0) {
|
|
166
208
|
allViolations.push('Subject line is empty. The commit message must begin with "<scope>: <subject>".')
|
|
167
209
|
} else if (scopeMatch === null) {
|
|
168
|
-
if (/^:\s*/.test(
|
|
210
|
+
if (/^:\s*/.test(effectiveSubject)) {
|
|
169
211
|
allViolations.push(
|
|
170
212
|
`Missing scope before colon in subject line "${subjectLine}". Expected format: "<scope>: <subject>".`,
|
|
171
213
|
)
|
|
172
|
-
} else if (/^[^:]+:\S/.test(
|
|
214
|
+
} else if (/^[^:]+:\S/.test(effectiveSubject)) {
|
|
173
215
|
allViolations.push(
|
|
174
216
|
`Missing space after colon in subject line "${subjectLine}". Expected format: "<scope>: <subject>".`,
|
|
175
217
|
)
|
|
176
|
-
} else if (/^[^:]+:\s*$/.test(
|
|
218
|
+
} else if (/^[^:]+:\s*$/.test(effectiveSubject)) {
|
|
177
219
|
allViolations.push(
|
|
178
220
|
`Subject text after colon is empty in "${subjectLine}". Expected format: "<scope>: <subject>".`,
|
|
179
221
|
)
|