@shelken/pi-co-authored-by 0.2.11 → 0.2.12
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/package.json +8 -4
- package/src/git-commit.ts +170 -0
- package/{index.ts → src/index.ts} +2 -2
- /package/{lib → src}/commit.ts +0 -0
package/package.json
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shelken/pi-co-authored-by",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.12",
|
|
4
4
|
"description": "agent 执行 git commit 时自动追加 Co-Authored-By / Generated-By trailer",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"files": [
|
|
8
|
-
"
|
|
9
|
-
"lib/commit.ts"
|
|
8
|
+
"src"
|
|
10
9
|
],
|
|
11
10
|
"repository": {
|
|
12
11
|
"type": "git",
|
|
@@ -24,9 +23,14 @@
|
|
|
24
23
|
"git",
|
|
25
24
|
"co-authored-by"
|
|
26
25
|
],
|
|
26
|
+
"omp": {
|
|
27
|
+
"extensions": [
|
|
28
|
+
"./src/index.ts"
|
|
29
|
+
]
|
|
30
|
+
},
|
|
27
31
|
"pi": {
|
|
28
32
|
"extensions": [
|
|
29
|
-
"./index.ts"
|
|
33
|
+
"./src/index.ts"
|
|
30
34
|
]
|
|
31
35
|
},
|
|
32
36
|
"peerDependencies": {
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 判断一条 bash 命令是否包含"直接执行的 git commit"。
|
|
3
|
+
*
|
|
4
|
+
* 只识别当前命令语法树中的直接命令节点,不展开:
|
|
5
|
+
* - 脚本内容(bash -c / eval / ./release.sh / make / npm run)
|
|
6
|
+
* - alias / 变量(g commit / $cmd commit / git alias)
|
|
7
|
+
* - 外部程序参数(xargs / find -exec / parallel)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const TRANSPARENT_WRAPPERS = new Set([
|
|
11
|
+
"command",
|
|
12
|
+
"env",
|
|
13
|
+
"sudo",
|
|
14
|
+
"doas",
|
|
15
|
+
"nohup",
|
|
16
|
+
"exec",
|
|
17
|
+
"rtk",
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
// 带一个参数的前缀:timeout 30 git commit
|
|
21
|
+
const WRAPPERS_WITH_ARG = new Set(["timeout"]);
|
|
22
|
+
|
|
23
|
+
function isEnvAssignment(arg: string): boolean {
|
|
24
|
+
return /^[A-Za-z_][A-Za-z0-9_]*=/.test(arg);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isGitCommitArgv(argv: string[]): boolean {
|
|
28
|
+
let i = 0;
|
|
29
|
+
|
|
30
|
+
// 跳过环境变量赋值与透明包装器(可交替出现,如 env FOO=bar git commit)
|
|
31
|
+
while (i < argv.length) {
|
|
32
|
+
const name = argv[i];
|
|
33
|
+
if (isEnvAssignment(name) || TRANSPARENT_WRAPPERS.has(name)) {
|
|
34
|
+
i++;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (WRAPPERS_WITH_ARG.has(name)) {
|
|
38
|
+
i += 2;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (i >= argv.length) return false;
|
|
45
|
+
|
|
46
|
+
const executable = argv[i].split("/").pop() ?? argv[i];
|
|
47
|
+
if (executable !== "git") return false;
|
|
48
|
+
i++;
|
|
49
|
+
|
|
50
|
+
// 跳过 git 全局选项(-C repo / -c k=v 带参数,--key=value 与 --flag 单 token)
|
|
51
|
+
while (i < argv.length) {
|
|
52
|
+
const arg = argv[i];
|
|
53
|
+
if (arg === "-C" || arg === "-c" || arg === "--git-dir" || arg === "--work-tree") {
|
|
54
|
+
i += 2;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (/^--[a-z0-9-]+=/.test(arg) || (arg.startsWith("-") && arg !== "--")) {
|
|
58
|
+
i += 1;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return i < argv.length && argv[i] === "commit";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const CONTROL_KEYWORDS = new Set(["if", "then", "else", "elif", "do", "done", "fi", "(" , ")"]);
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 判断命令是否包含直接执行的 git commit。
|
|
71
|
+
* 控制操作符(&& || ; | & 换行)分割成多个命令段,任一命中即 true。
|
|
72
|
+
*/
|
|
73
|
+
export function isGitCommitCommand(command: string): boolean {
|
|
74
|
+
for (const segment of splitSegments(command)) {
|
|
75
|
+
if (isGitCommitSegment(segment)) return true;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function isGitCommitSegment(segment: string): boolean {
|
|
81
|
+
const argv = tokenize(segment);
|
|
82
|
+
let i = 0;
|
|
83
|
+
while (i < argv.length && CONTROL_KEYWORDS.has(argv[i])) i++;
|
|
84
|
+
return isGitCommitArgv(argv.slice(i));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** 按控制操作符切分,忽略引号内的内容。 */
|
|
88
|
+
export function splitSegments(command: string): string[] {
|
|
89
|
+
const segments: string[] = [];
|
|
90
|
+
let current = "";
|
|
91
|
+
let quote: "'" | '"' | null = null;
|
|
92
|
+
let escaped = false;
|
|
93
|
+
|
|
94
|
+
for (const char of command) {
|
|
95
|
+
if (escaped) {
|
|
96
|
+
current += char;
|
|
97
|
+
escaped = false;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (char === "\\") {
|
|
101
|
+
current += char;
|
|
102
|
+
escaped = true;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (quote) {
|
|
106
|
+
current += char;
|
|
107
|
+
if (char === quote) quote = null;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (char === "'" || char === '"') {
|
|
111
|
+
quote = char;
|
|
112
|
+
current += char;
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (char === "&" || char === "|" || char === ";" || char === "\n") {
|
|
116
|
+
if (current.trim()) segments.push(current.trim());
|
|
117
|
+
current = "";
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
current += char;
|
|
121
|
+
}
|
|
122
|
+
if (current.trim()) segments.push(current.trim());
|
|
123
|
+
return segments;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** 极简 shell 分词:保留引号组为整体,去掉空白。 */
|
|
127
|
+
export function tokenize(segment: string): string[] {
|
|
128
|
+
const argv: string[] = [];
|
|
129
|
+
let current = "";
|
|
130
|
+
let quote: "'" | '"' | null = null;
|
|
131
|
+
let escaped = false;
|
|
132
|
+
|
|
133
|
+
const flush = () => {
|
|
134
|
+
if (current) {
|
|
135
|
+
argv.push(current);
|
|
136
|
+
current = "";
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
for (const char of segment) {
|
|
141
|
+
if (escaped) {
|
|
142
|
+
current += char;
|
|
143
|
+
escaped = false;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (char === "\\") {
|
|
147
|
+
escaped = true;
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (quote) {
|
|
151
|
+
if (char === quote) {
|
|
152
|
+
quote = null;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
current += char;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (char === "'" || char === '"') {
|
|
159
|
+
quote = char;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (/\s/.test(char)) {
|
|
163
|
+
flush();
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
current += char;
|
|
167
|
+
}
|
|
168
|
+
flush();
|
|
169
|
+
return argv;
|
|
170
|
+
}
|
|
@@ -4,8 +4,8 @@ import {
|
|
|
4
4
|
createCommitHookDirectory,
|
|
5
5
|
removeCommitHookDirectory,
|
|
6
6
|
wrapBashWithCommitHook,
|
|
7
|
-
} from "./
|
|
8
|
-
import { isGitCommitCommand } from "./
|
|
7
|
+
} from "./commit.ts";
|
|
8
|
+
import { isGitCommitCommand } from "./git-commit.ts";
|
|
9
9
|
|
|
10
10
|
export default function (pi: ExtensionAPI) {
|
|
11
11
|
let hooksDir: string | undefined;
|
/package/{lib → src}/commit.ts
RENAMED
|
File without changes
|