zen-gitsync 2.15.10 → 2.15.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 +4 -3
- package/src/gitCommit.js +475 -475
- package/src/ui/public/assets/AppVersionBadge-BaXEI9Aq.js +2 -0
- package/src/ui/public/assets/BranchSelector-DJhEF0y9.js +1 -0
- package/src/ui/public/assets/CommitForm-BfpcK-bz.js +6 -0
- package/src/ui/public/assets/ConsoleView-D838O0O6.js +17 -0
- package/src/ui/public/assets/CustomCommandManager-BcCK46df.js +1 -0
- package/src/ui/public/assets/EditorView-DnnqYNGF.js +0 -0
- package/src/ui/public/assets/{FlowExecutionViewer-Dvv2-pUp.js → FlowExecutionViewer-CSlMmcDS.js} +1 -1
- package/src/ui/public/assets/FlowOrchestrationWorkspace-UgHpZ3FR.js +21 -0
- package/src/ui/public/assets/LogList-uUgskwwp.js +7 -0
- package/src/ui/public/assets/MindmapView-CCmFMiHR.js +1 -0
- package/src/ui/public/assets/{MindmapView-Bt2VdyoX.css → MindmapView-KFcJx-i-.css} +1 -1
- package/src/ui/public/assets/MonitorView-BxoqeX70.js +1 -0
- package/src/ui/public/assets/ProjectStartupButton-C3AwivIB.js +1 -0
- package/src/ui/public/assets/{RemoteRepoCard-BbKpAzPj.js → RemoteRepoCard-BbxeVpje.js} +1 -1
- package/src/ui/public/assets/SourceMapView-DKv-Y6t_.js +3 -0
- package/src/ui/public/assets/{SvgIcon-BoP8CWdO.js → SvgIcon-sMM8XFgm.js} +1 -1
- package/src/ui/public/assets/TemplateManager-Dzj8rXVw.js +1 -0
- package/src/ui/public/assets/UserInputNode-BU7Trzq-.js +1 -0
- package/src/ui/public/assets/WorkbenchView-DMoulJCG.css +1 -0
- package/src/ui/public/assets/WorkbenchView-DTgRoOkv.js +12 -0
- package/src/ui/public/assets/{_plugin-vue_export-helper-D9aIxbUC.js → _plugin-vue_export-helper-8ryIYVvs.js} +1 -1
- package/src/ui/public/assets/{configStore-CiAM1jWr.js → configStore-D77uNk8X.js} +1 -1
- package/src/ui/public/assets/element-plus-D8SQiesr.js +17 -0
- package/src/ui/public/assets/element-plus-DB_sOyH_.css +1 -0
- package/src/ui/public/assets/flow-mindmap-BLa-kSA7.js +90 -0
- package/src/ui/public/assets/flow-mindmap-C_aN-n3H.css +1 -0
- package/src/ui/public/assets/index-CpFreGqX.js +23 -0
- package/src/ui/public/assets/index-CytKCnOp.css +1 -0
- package/src/ui/public/assets/{socket-io-z4n873W1.js → socket-io-B1foIbJS.js} +1 -1
- package/src/ui/public/assets/{vendor-SOsdHF7M.js → vendor-B9orL8iU.js} +37 -37
- package/src/ui/public/assets/vendor-Dm_yd-By.css +1 -0
- package/src/ui/public/assets/{vue-flow-D5lw7RxP.js → vue-flow-BY7LODit.js} +1 -1
- package/src/ui/public/favicon.svg +75 -75
- package/src/ui/public/index.html +28 -28
- package/src/ui/public/logo.svg +74 -74
- package/src/ui/server/middleware/requestLogger.js +53 -53
- package/src/ui/server/routes/branchStatus.js +105 -105
- package/src/ui/server/routes/fileOpen.js +309 -309
- package/src/ui/server/routes/fs.js +791 -791
- package/src/ui/server/routes/git/diff.js +426 -426
- package/src/ui/server/routes/git/diffUtils.js +149 -149
- package/src/ui/server/routes/git/stash.js +550 -550
- package/src/ui/server/routes/git/tags.js +176 -176
- package/src/ui/server/routes/git.js +181 -181
- package/src/ui/server/routes/gitOps.js +1211 -1211
- package/src/ui/server/routes/process.js +86 -86
- package/src/ui/server/routes/status.js +67 -67
- package/src/ui/server/utils/createSavePortToFile.js +48 -48
- package/src/ui/server/utils/instanceRegistry.js +282 -282
- package/src/ui/server/utils/startServerOnAvailablePort.js +116 -116
- package/src/utils/index.js +1168 -1168
- package/src/ui/public/assets/AppVersionBadge-ZginY_T5.js +0 -2
- package/src/ui/public/assets/BranchSelector-BD8y_UMH.js +0 -1
- package/src/ui/public/assets/CommitForm-B1DJxyib.js +0 -6
- package/src/ui/public/assets/ConsoleView-EJa9vjMa.js +0 -17
- package/src/ui/public/assets/CustomCommandManager-BRxKf9jl.js +0 -1
- package/src/ui/public/assets/EditorView-DR2O8qKP.js +0 -0
- package/src/ui/public/assets/FlowOrchestrationWorkspace-DzSgBTmq.js +0 -21
- package/src/ui/public/assets/LogList-BEnd3kcT.js +0 -7
- package/src/ui/public/assets/MindmapView-Ds2Qd2ZL.js +0 -1
- package/src/ui/public/assets/MonitorView-B9t-8uJB.js +0 -1
- package/src/ui/public/assets/ProjectStartupButton-CHh6z5Pn.js +0 -1
- package/src/ui/public/assets/SourceMapView-Deai1zNG.js +0 -3
- package/src/ui/public/assets/TemplateManager-Eu4YhYCk.js +0 -1
- package/src/ui/public/assets/UserInputNode-C0GJHtef.js +0 -1
- package/src/ui/public/assets/WorkbenchView-C2xSxJfg.css +0 -1
- package/src/ui/public/assets/WorkbenchView-DUS0hiHf.js +0 -12
- package/src/ui/public/assets/element-plus-DB-NeCl9.js +0 -24
- package/src/ui/public/assets/element-plus-R1nMzM9j.css +0 -1
- package/src/ui/public/assets/flow-mindmap-CI2OSS19.css +0 -1
- package/src/ui/public/assets/flow-mindmap-DrcjupFb.js +0 -90
- package/src/ui/public/assets/index-Dleiy2zi.js +0 -23
- package/src/ui/public/assets/index-DltbTqIJ.css +0 -1
- package/src/ui/public/assets/vendor-CT8e9rs3.css +0 -1
- package/src/ui/server/.claude/codediff.txt +0 -8
package/src/utils/index.js
CHANGED
|
@@ -1,1168 +1,1168 @@
|
|
|
1
|
-
// Copyright 2026 xz333221
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
//
|
|
15
|
-
// git 子进程包装 + 命令历史 + 锁定文件过滤 git add
|
|
16
|
-
// 纯函数(parseCwdArg / truncateForHistory / formatDuration / delay)已抽到
|
|
17
|
-
// ./format.js,这里 re-export 以保持向后兼容(老调用方无需改 import)。
|
|
18
|
-
import chalk from 'chalk';
|
|
19
|
-
import boxen from "boxen";
|
|
20
|
-
import stringWidth from 'string-width';
|
|
21
|
-
import {execFile, execSync} from 'child_process'
|
|
22
|
-
import os from 'os'
|
|
23
|
-
import ora from "ora";
|
|
24
|
-
import readline from 'readline'
|
|
25
|
-
import path from 'path'
|
|
26
|
-
import fs from 'fs/promises'
|
|
27
|
-
import config from '../config.js'
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
import {
|
|
31
|
-
parseCwdArg as _parseCwdArg,
|
|
32
|
-
truncateForHistory as _truncateForHistory,
|
|
33
|
-
formatDuration as _formatDuration,
|
|
34
|
-
delay as _delay,
|
|
35
|
-
} from './format.js'
|
|
36
|
-
|
|
37
|
-
// 向后兼容 re-export — 老调用方 import { parseCwdArg } from '../utils' 仍然可用
|
|
38
|
-
export const parseCwdArg = _parseCwdArg
|
|
39
|
-
export const truncateForHistory = _truncateForHistory
|
|
40
|
-
export const formatDuration = _formatDuration
|
|
41
|
-
export const delay = _delay
|
|
42
|
-
|
|
43
|
-
const calcColor = (commandLine, str) => {
|
|
44
|
-
let color = 'reset'
|
|
45
|
-
if (commandLine === 'git status' && str.startsWith('\t')) {
|
|
46
|
-
color = 'red'
|
|
47
|
-
if (str.startsWith('modified:')) color = 'green'
|
|
48
|
-
if (str.startsWith('deleted:')) color = 'red'
|
|
49
|
-
}
|
|
50
|
-
return color
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const tableLog = (commandLine, content, type) => {
|
|
54
|
-
const headLabel = `> ${commandLine}`
|
|
55
|
-
let head = chalk.bold.blue(headLabel)
|
|
56
|
-
if (type === 'error') {
|
|
57
|
-
content = content.toString().split('\n')
|
|
58
|
-
head = chalk.bold.red(headLabel)
|
|
59
|
-
} else if (type === 'common') {
|
|
60
|
-
content = content.split('\n')
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// 单次输出限幅,避免 git log --all 之类命令刷屏
|
|
64
|
-
const MAX_LINES = 10
|
|
65
|
-
const MAX_LINE_LENGTH = 200
|
|
66
|
-
let isTruncated = false
|
|
67
|
-
if (content.length > MAX_LINES) {
|
|
68
|
-
content = content.slice(0, MAX_LINES)
|
|
69
|
-
isTruncated = true
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
content = content.map((item) => {
|
|
73
|
-
const fontColor = calcColor(commandLine, item)
|
|
74
|
-
let row = item.replaceAll('\t', ' ')
|
|
75
|
-
if (row.length > MAX_LINE_LENGTH) {
|
|
76
|
-
row = row.substring(0, MAX_LINE_LENGTH) + '...'
|
|
77
|
-
}
|
|
78
|
-
return chalk[fontColor](row)
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
if (isTruncated) {
|
|
82
|
-
content.push(chalk.dim('... (输出内容过多,已省略)'))
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// 用 box-drawing 字符画完整边框(上下左右 + header 分隔线),视觉上把每条 git 命令的输出框起来
|
|
86
|
-
// 复刻旧版 coloredLog 的样式:┌─┐│├─┤└─┘,每行右侧补空格对齐到框宽 + 右边框 │
|
|
87
|
-
const terminalWidth = Math.min(process.stdout.columns || 80, 120)
|
|
88
|
-
const startLine = '┌' + '─'.repeat(terminalWidth - 2) + '┐'
|
|
89
|
-
const midLine = '├' + '─'.repeat(terminalWidth - 2) + '┤'
|
|
90
|
-
const endLine = '└' + '─'.repeat(terminalWidth - 2) + '┘'
|
|
91
|
-
|
|
92
|
-
// 计算每行右侧需要补多少空格(用 stringWidth 处理中文/emoji 宽度,避免对不齐)
|
|
93
|
-
const padRight = (text, extraFix = 0) => {
|
|
94
|
-
const len = stringWidth(text)
|
|
95
|
-
const repeatLen = terminalWidth - len - 3 - extraFix
|
|
96
|
-
return ' '.repeat(Math.max(repeatLen, 0)) + '│'
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
console.log(startLine)
|
|
100
|
-
// header 行:│ > command │,下面跟一条分隔线
|
|
101
|
-
console.log(`│ ${head}${padRight(headLabel, 2)}`)
|
|
102
|
-
console.log(midLine)
|
|
103
|
-
// 内容行:│ content │
|
|
104
|
-
for (const line of content) {
|
|
105
|
-
if (line.trim().length === 0) continue
|
|
106
|
-
console.log(`│ ${line}${padRight(line)}`)
|
|
107
|
-
}
|
|
108
|
-
console.log(endLine)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const coloredLog = (...args) => {
|
|
112
|
-
const [commandLine, content, type = 'common'] = args
|
|
113
|
-
tableLog(commandLine, content, type)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const errorLog = (commandLine, content) => {
|
|
117
|
-
const msg = ` FAIL ${commandLine}\n content: ${content} `
|
|
118
|
-
const box = boxen(chalk.red.bold(msg))
|
|
119
|
-
console.log(box)
|
|
120
|
-
}
|
|
121
|
-
function execSyncGitCommand(command, options = {}) {
|
|
122
|
-
let {encoding = 'utf-8', maxBuffer = 30 * 1024 * 1024, head = command, log = true} = options
|
|
123
|
-
try {
|
|
124
|
-
let cwd = getCwd()
|
|
125
|
-
const output = execSync(command, {
|
|
126
|
-
env: {
|
|
127
|
-
...process.env,
|
|
128
|
-
// LANG: 'en_US.UTF-8', // Linux/macOS
|
|
129
|
-
// LC_ALL: 'en_US.UTF-8', // Linux/macOS
|
|
130
|
-
GIT_CONFIG_PARAMETERS: "'core.quotepath=false'" // 关闭路径转义
|
|
131
|
-
}, encoding, maxBuffer, cwd
|
|
132
|
-
})
|
|
133
|
-
if (options.spinner) {
|
|
134
|
-
options.spinner.stop();
|
|
135
|
-
}
|
|
136
|
-
let result = output.trim()
|
|
137
|
-
log && coloredLog(head, result)
|
|
138
|
-
// 打印当前目录和时间信息
|
|
139
|
-
if (log) {
|
|
140
|
-
const currentTime = new Date().toLocaleString('zh-CN', { hour12: false });
|
|
141
|
-
console.log(chalk.dim(`📁 目录: ${cwd} | ⏰ 时间: ${currentTime}`));
|
|
142
|
-
}
|
|
143
|
-
return result
|
|
144
|
-
} catch (e) {
|
|
145
|
-
log && coloredLog(command, e, 'error')
|
|
146
|
-
// 透传原始 error,保留 stack + 已挂载的 stdout/stderr
|
|
147
|
-
throw e
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Add a command history array to store commands and their results
|
|
152
|
-
const commandHistory = [];
|
|
153
|
-
const MAX_HISTORY_SIZE = 100;
|
|
154
|
-
// 输出截断上限 — 单点常量,避免在 execGitCommand / addCommandToHistory /
|
|
155
|
-
// 其他未来 history 写入点之间漂移导致截断行为不一致。
|
|
156
|
-
// 截断提示也集中在这里,所有调用方用同一个文案,便于后续 i18n 替换。
|
|
157
|
-
const MAX_OUTPUT_LENGTH = 5000;
|
|
158
|
-
const TRUNCATED_SUFFIX_STDOUT = '\n... (output truncated)';
|
|
159
|
-
const TRUNCATED_SUFFIX_STDERR = '\n... (error output truncated)';
|
|
160
|
-
const TRUNCATED_SUFFIX_MANUAL = '...[truncated]';
|
|
161
|
-
|
|
162
|
-
// 添加一个变量来保存Socket.io实例
|
|
163
|
-
let ioInstance = null;
|
|
164
|
-
|
|
165
|
-
// 提供注册Socket.io实例的函数
|
|
166
|
-
function registerSocketIO(io) {
|
|
167
|
-
ioInstance = io;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// 清空命令历史记录
|
|
171
|
-
function clearCommandHistory() {
|
|
172
|
-
// 清空数组
|
|
173
|
-
commandHistory.length = 0;
|
|
174
|
-
|
|
175
|
-
// 通过WebSocket广播历史已清空
|
|
176
|
-
if (ioInstance) {
|
|
177
|
-
ioInstance.emit('command_history_cleared');
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return true;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// 按 git 子命令类型设定默认超时(毫秒),避免 git push/fetch 撞死网导致
|
|
184
|
-
// 整个 Node 事件循环永久挂起。读类操作给短超时(快);网络类给长超时;
|
|
185
|
-
// 默认 60s 兜底。调用方可通过 options.timeout 显式覆盖,timeout=0 表示不超时。
|
|
186
|
-
const DEFAULT_GIT_TIMEOUT_MS = {
|
|
187
|
-
// 只读 metadata,通常几 ms 完成
|
|
188
|
-
status: 30_000,
|
|
189
|
-
'rev-parse': 10_000,
|
|
190
|
-
branch: 10_000,
|
|
191
|
-
'rev-list': 30_000,
|
|
192
|
-
// 输出可能很大
|
|
193
|
-
log: 30_000,
|
|
194
|
-
diff: 60_000,
|
|
195
|
-
show: 30_000,
|
|
196
|
-
// 写操作,锁文件 + 索引更新
|
|
197
|
-
add: 60_000,
|
|
198
|
-
commit: 60_000,
|
|
199
|
-
// 网络操作,留足时间应对慢网 + 钩子 + 大仓库
|
|
200
|
-
fetch: 300_000,
|
|
201
|
-
pull: 300_000,
|
|
202
|
-
push: 300_000,
|
|
203
|
-
clone: 600_000,
|
|
204
|
-
remote: 60_000,
|
|
205
|
-
// 兜底
|
|
206
|
-
default: 60_000,
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
function getDefaultGitTimeout(command) {
|
|
210
|
-
if (!Array.isArray(command) || command.length === 0) return DEFAULT_GIT_TIMEOUT_MS.default;
|
|
211
|
-
const sub = String(command[0] || '').trim();
|
|
212
|
-
return DEFAULT_GIT_TIMEOUT_MS[sub] ?? DEFAULT_GIT_TIMEOUT_MS.default;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function execGitCommand(command, options = {}) {
|
|
216
|
-
return new Promise((resolve, reject) => {
|
|
217
|
-
let {encoding = 'utf-8', maxBuffer = 30 * 1024 * 1024, head = Array.isArray(command) ? command.join(' ') : command, log = true, timeout} = options
|
|
218
|
-
// options.timeout === undefined → 按子命令类型取默认;0 = 不超时(显式禁用)
|
|
219
|
-
if (timeout === undefined) {
|
|
220
|
-
timeout = getDefaultGitTimeout(command);
|
|
221
|
-
}
|
|
222
|
-
let cwd = getCwd()
|
|
223
|
-
|
|
224
|
-
// Record start time for command execution
|
|
225
|
-
const startTime = Date.now();
|
|
226
|
-
|
|
227
|
-
// setTimeout(() => {
|
|
228
|
-
// 用 execFile('git', argv) 跨平台执行:
|
|
229
|
-
// - 不走 shell,Windows cmd.exe / POSIX sh 都一致
|
|
230
|
-
// - argv 数组天然免疫 shell 注入,文件名/参数无需 shellQuote
|
|
231
|
-
// - 所有调用点约定只跑 git 子命令,见 src/utils/index.js 内的 execGitCommand 调用清单
|
|
232
|
-
// - timeout 显式传 0 表示禁用;否则按 getDefaultGitTimeout() 自动选值
|
|
233
|
-
execFile('git', command, {
|
|
234
|
-
env: {
|
|
235
|
-
...process.env,
|
|
236
|
-
// LANG: 'en_US.UTF-8', // Linux/macOS
|
|
237
|
-
// LC_ALL: 'en_US.UTF-8', // Linux/macOS
|
|
238
|
-
GIT_CONFIG_PARAMETERS: "'core.quotepath=false'" // 关闭路径转义
|
|
239
|
-
},
|
|
240
|
-
encoding,
|
|
241
|
-
maxBuffer,
|
|
242
|
-
cwd,
|
|
243
|
-
// Windows 下 git 是 .exe;POSIX 下直接 PATH 找。
|
|
244
|
-
// 不传 shell,杜绝 cmd.exe 单/双引号兼容问题与注入风险。
|
|
245
|
-
windowsHide: true,
|
|
246
|
-
// 超时后 execFile 自动给子进程发 SIGTERM,callback 收到 error.killed=true
|
|
247
|
-
timeout
|
|
248
|
-
}, (error, stdout, stderr) => {
|
|
249
|
-
if (options.spinner) {
|
|
250
|
-
options.spinner.stop();
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Calculate execution time
|
|
254
|
-
const executionTime = Date.now() - startTime;
|
|
255
|
-
|
|
256
|
-
// Truncate long outputs
|
|
257
|
-
let truncatedStdout = stdout;
|
|
258
|
-
let truncatedStderr = stderr;
|
|
259
|
-
let isStdoutTruncated = false;
|
|
260
|
-
let isStderrTruncated = false;
|
|
261
|
-
|
|
262
|
-
if (stdout && stdout.length > MAX_OUTPUT_LENGTH) {
|
|
263
|
-
truncatedStdout = truncateForHistory(stdout, MAX_OUTPUT_LENGTH, TRUNCATED_SUFFIX_STDOUT);
|
|
264
|
-
isStdoutTruncated = true;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if (stderr && stderr.length > MAX_OUTPUT_LENGTH) {
|
|
268
|
-
truncatedStderr = truncateForHistory(stderr, MAX_OUTPUT_LENGTH, TRUNCATED_SUFFIX_STDERR);
|
|
269
|
-
isStderrTruncated = true;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// Add command to history
|
|
273
|
-
// 用 head(已 join)而不是原始 command,确保历史里的 command 永远是字符串
|
|
274
|
-
// 防止 execGitCommand(['status']) 这类数组调用把数组塞进历史 → 前端 item.command.trim() 崩
|
|
275
|
-
const historyItem = {
|
|
276
|
-
command: head,
|
|
277
|
-
stdout: truncatedStdout || '',
|
|
278
|
-
stderr: truncatedStderr || '',
|
|
279
|
-
error: error ? error.message : null,
|
|
280
|
-
executionTime,
|
|
281
|
-
timestamp: new Date().toISOString(),
|
|
282
|
-
success: !error,
|
|
283
|
-
isStdoutTruncated,
|
|
284
|
-
isStderrTruncated
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
// Add to history (limited size)
|
|
288
|
-
commandHistory.unshift(historyItem);
|
|
289
|
-
if (commandHistory.length > MAX_HISTORY_SIZE) {
|
|
290
|
-
commandHistory.pop();
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// 通过WebSocket广播命令历史更新
|
|
294
|
-
if (ioInstance) {
|
|
295
|
-
ioInstance.emit('command_history_update', {
|
|
296
|
-
newCommand: historyItem,
|
|
297
|
-
fullHistory: commandHistory.slice(0, 10) // 只发送最近10条以减小数据量
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
if (stdout) {
|
|
302
|
-
log && coloredLog(head, stdout)
|
|
303
|
-
}
|
|
304
|
-
if (stderr) {
|
|
305
|
-
log && coloredLog(head, stderr)
|
|
306
|
-
}
|
|
307
|
-
// 打印当前目录和时间信息
|
|
308
|
-
if (log && (stdout || stderr)) {
|
|
309
|
-
const currentTime = new Date().toLocaleString('zh-CN', { hour12: false });
|
|
310
|
-
console.log(chalk.dim(`📁 目录: ${cwd} | ⏰ 时间: ${currentTime}`));
|
|
311
|
-
}
|
|
312
|
-
if (error) {
|
|
313
|
-
// 检测是否超时:execFile 超时后 error.killed=true,signal='SIGTERM',
|
|
314
|
-
// error.code=null(被信号杀,无 exit code)。在 error 上加 isTimeout 标记
|
|
315
|
-
// 方便上层(路由 / GUI 前端)区分"超时"和"git 自己报错"做不同提示。
|
|
316
|
-
if (error.killed && (error.signal === 'SIGTERM' || /TIMEOUT|timeout/i.test(error.message || ''))) {
|
|
317
|
-
error.isTimeout = true
|
|
318
|
-
if (log) {
|
|
319
|
-
console.log(chalk.yellow(`⏰ git 命令超时 (${timeout}ms): ${head}`))
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
// 将 stdout 和 stderr 附加到 error 对象,以便上层可以获取完整输出
|
|
323
|
-
error.stdout = stdout
|
|
324
|
-
error.stderr = stderr
|
|
325
|
-
// ignoreError=true:命令预期可能失败(如 git rev-parse @{u} 查无 upstream 时 git 报
|
|
326
|
-
// "fatal: no upstream configured")。此时不当错误抛,resolve {stdout, stderr, error}
|
|
327
|
-
// 让上层用 stdout 为空自行降级,而不是把整个接口 500 掉。
|
|
328
|
-
if (options.ignoreError) {
|
|
329
|
-
resolve({ stdout: stdout || '', stderr: stderr || '', error })
|
|
330
|
-
return
|
|
331
|
-
}
|
|
332
|
-
log && coloredLog(head, error, 'error')
|
|
333
|
-
// 错误情况也打印目录和时间
|
|
334
|
-
if (log) {
|
|
335
|
-
const currentTime = new Date().toLocaleString('zh-CN', { hour12: false });
|
|
336
|
-
console.log(chalk.dim(`📁 目录: ${cwd} | ⏰ 时间: ${currentTime}`));
|
|
337
|
-
}
|
|
338
|
-
reject(error)
|
|
339
|
-
return
|
|
340
|
-
}
|
|
341
|
-
resolve({
|
|
342
|
-
stdout,
|
|
343
|
-
stderr
|
|
344
|
-
})
|
|
345
|
-
})
|
|
346
|
-
// }, 1000)
|
|
347
|
-
})
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
* 检查并尝试清理 Git 锁文件
|
|
352
|
-
*
|
|
353
|
-
* 安全策略:必须先验证锁是否真的过期(孤儿锁),才能 unlink。盲目删锁
|
|
354
|
-
* 会删掉别人正在写的锁 → `.git/index` 半写状态 → 仓库损坏。
|
|
355
|
-
*
|
|
356
|
-
* 判定顺序:
|
|
357
|
-
* 1. 读 .git/index.lock 内容,若能解析为 PID,调 process.kill(pid, 0) 探测
|
|
358
|
-
* - 探测成功(无错)→ 进程存活,锁仍被持有 → 跳过清理
|
|
359
|
-
* - ESRCH(无此进程)→ 锁是孤儿 → 进入清理
|
|
360
|
-
* - EPERM(进程存在但无权限)→ 锁被别人持有且我们是其他用户 → 跳过
|
|
361
|
-
* 2. 锁文件内容不是 PID(原生 git 通过 O_CREAT|O_EXCL 创建空文件作锁,
|
|
362
|
-
* 内容几乎总为空;此分支为兼容场景与未知第三方工具的 mtime 兜底):
|
|
363
|
-
* - force=false(默认):超过 5 分钟未更新视为陈旧,否则保守跳过
|
|
364
|
-
* - force=true:绕过 mtime 判断直接视为陈旧(用于破坏性入口,
|
|
365
|
-
* 如 reset --hard / discard-all-changes — 用户主动触发,
|
|
366
|
-
* 不该被一个刚崩出来的新孤儿锁挡住)
|
|
367
|
-
*
|
|
368
|
-
* 注:force=true 仅绕过 mtime,**仍走 PID liveness 校验**——
|
|
369
|
-
* 若锁由活跃进程持有(无论新旧)一律跳过,确保不会误删活跃锁。
|
|
370
|
-
*
|
|
371
|
-
* 注:阈值取 5min 而非 60s,是为大仓库的 git add -A / stash / gc 等长操作
|
|
372
|
-
* 留出空间 — 60s 阈值下用户在手抖触发清理时可能误删活跃锁,
|
|
373
|
-
* 导致 .git/index 半写损坏。5min 是 GitHub Desktop / Sourcetree 等
|
|
374
|
-
* GUI 客户端常用的陈旧判定阈值。
|
|
375
|
-
*
|
|
376
|
-
* @param {Object} [options]
|
|
377
|
-
* @param {boolean} [options.force=false] 是否绕过 mtime 兜底判断(破坏性入口用)。
|
|
378
|
-
* 注意:force 不影响 PID liveness 校验——活跃进程持有的锁永远不会被删。
|
|
379
|
-
* @returns {Promise<boolean>} 是否清理成功
|
|
380
|
-
*/
|
|
381
|
-
async function checkAndClearGitLock({ force = false } = {}) {
|
|
382
|
-
// 锁文件"陈旧"阈值(毫秒):5min 是经验值。
|
|
383
|
-
// - 大仓库 git add -A / stash / gc 单次可能 > 60s,60s 阈值会误删活跃锁;
|
|
384
|
-
// - 5min 与 GitHub Desktop / Sourcetree 等 GUI 客户端的陈旧判定一致;
|
|
385
|
-
// - 只有真正崩溃/被 kill -9 留下的孤儿锁才会存在这么久。
|
|
386
|
-
// - force=true 时此阈值被绕过,但仍受 PID liveness 校验保护。
|
|
387
|
-
const STALE_LOCK_THRESHOLD_MS = 5 * 60_000;
|
|
388
|
-
try {
|
|
389
|
-
const cwd = getCwd();
|
|
390
|
-
let gitRoot;
|
|
391
|
-
try {
|
|
392
|
-
// 使用 execSync 快速获取 Git 根目录,加 5s 超时避免 rev-parse 卡死
|
|
393
|
-
const rootOutput = execSync('git rev-parse --show-toplevel', {
|
|
394
|
-
cwd,
|
|
395
|
-
encoding: 'utf-8',
|
|
396
|
-
timeout: 5000
|
|
397
|
-
});
|
|
398
|
-
gitRoot = rootOutput.trim();
|
|
399
|
-
} catch (e) {
|
|
400
|
-
gitRoot = cwd;
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
const lockFilePath = path.join(gitRoot, '.git', 'index.lock');
|
|
404
|
-
|
|
405
|
-
// 一次 IO 替代原来的 fs.access + fs.unlink 两步
|
|
406
|
-
let content;
|
|
407
|
-
try {
|
|
408
|
-
content = await fs.readFile(lockFilePath, 'utf-8');
|
|
409
|
-
} catch (e) {
|
|
410
|
-
if (e.code === 'ENOENT') return false; // 锁文件不存在,无需清理
|
|
411
|
-
throw e;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// 判定锁是否过期
|
|
415
|
-
const trimmed = (content || '').trim();
|
|
416
|
-
// 原生 git 通过 O_CREAT|O_EXCL 创建空文件作锁,内容几乎总为空,
|
|
417
|
-
// 不会往 .git/index.lock 写 PID。此 PID 分支为兼容某些会写 PID 的
|
|
418
|
-
// 第三方工具(如部分 GUI 客户端 / 自定义钩子),并非常规路径。
|
|
419
|
-
const isPidFormat = /^\d+$/.test(trimmed);
|
|
420
|
-
let isStale = false;
|
|
421
|
-
|
|
422
|
-
if (isPidFormat) {
|
|
423
|
-
const pid = parseInt(trimmed, 10);
|
|
424
|
-
if (Number.isInteger(pid) && pid > 0) {
|
|
425
|
-
try {
|
|
426
|
-
// signal 0 不真发信号,只探测进程是否存在
|
|
427
|
-
process.kill(pid, 0);
|
|
428
|
-
// 无错 → 进程存活,锁还活着
|
|
429
|
-
console.log(chalk.yellow(`⚠️ 检测到 Git 锁由活跃进程持有 (PID=${pid}),跳过清理`));
|
|
430
|
-
return false;
|
|
431
|
-
} catch (e) {
|
|
432
|
-
if (e.code === 'ESRCH') {
|
|
433
|
-
// 进程已死,锁是孤儿
|
|
434
|
-
isStale = true;
|
|
435
|
-
} else if (e.code === 'EPERM') {
|
|
436
|
-
// 进程存在但无权限(其他用户的进程),不能删
|
|
437
|
-
console.log(chalk.yellow(`⚠️ 检测到 Git 锁由无权限访问的进程持有,跳过清理`));
|
|
438
|
-
return false;
|
|
439
|
-
} else {
|
|
440
|
-
// 其他探测错误,保守不删
|
|
441
|
-
console.warn(chalk.yellow(`⚠️ 探测 PID liveness 失败: ${e.message},跳过清理`));
|
|
442
|
-
return false;
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
} else {
|
|
447
|
-
// 锁文件内容不是 PID — 原生 git 用 O_CREAT|O_EXCL 创建空文件,
|
|
448
|
-
// 内容为空,用 mtime 兜底
|
|
449
|
-
if (force) {
|
|
450
|
-
// 破坏性入口(reset --hard / discard-all-changes 等):用户主动
|
|
451
|
-
// 触发,不应被刚崩出来的新孤儿锁挡住。PID liveness 已在上面通过
|
|
452
|
-
// (走到这里说明锁不是 PID 格式或 PID 已死),直接视为陈旧清理。
|
|
453
|
-
// 仍打印 mtime 便于排查。
|
|
454
|
-
try {
|
|
455
|
-
const stat = await fs.stat(lockFilePath);
|
|
456
|
-
const ageMs = Date.now() - stat.mtimeMs;
|
|
457
|
-
console.log(chalk.gray(`ℹ️ force=true,绕过 mtime 判断清理 Git 锁(年龄 ${Math.round(ageMs / 1000)}s)`));
|
|
458
|
-
} catch (_) {}
|
|
459
|
-
isStale = true;
|
|
460
|
-
} else {
|
|
461
|
-
try {
|
|
462
|
-
const stat = await fs.stat(lockFilePath);
|
|
463
|
-
const ageMs = Date.now() - stat.mtimeMs;
|
|
464
|
-
if (ageMs > STALE_LOCK_THRESHOLD_MS) {
|
|
465
|
-
isStale = true;
|
|
466
|
-
} else {
|
|
467
|
-
console.log(chalk.yellow(`⚠️ Git 锁文件较新(${Math.round(ageMs / 1000)}s),保守跳过清理`));
|
|
468
|
-
return false;
|
|
469
|
-
}
|
|
470
|
-
} catch (_) {
|
|
471
|
-
return false;
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
if (!isStale) return false;
|
|
477
|
-
|
|
478
|
-
// 锁是孤儿,清理
|
|
479
|
-
await fs.unlink(lockFilePath);
|
|
480
|
-
console.log(chalk.green(`✅ 已清理过期 Git 锁文件: ${lockFilePath}`));
|
|
481
|
-
return true;
|
|
482
|
-
} catch (error) {
|
|
483
|
-
console.error(chalk.red('清理 Git 锁文件失败:'), error.message);
|
|
484
|
-
return false;
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// Function to get command history
|
|
489
|
-
// Cached cwd — 进程内 getCwd() 只算一次,见下方 getCwd() 实现
|
|
490
|
-
let _cachedCwd = null
|
|
491
|
-
|
|
492
|
-
function getCommandHistory() {
|
|
493
|
-
return [...commandHistory];
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
// Function to manually add command to history (for commands not using execGitCommand)
|
|
497
|
-
function addCommandToHistory(command, stdout = '', stderr = '', error = null, executionTime = 0) {
|
|
498
|
-
// 防御:历史里的 command 必须始终是字符串,防止前端 item.command.trim() 抛错
|
|
499
|
-
if (Array.isArray(command)) {
|
|
500
|
-
command = command.join(' ')
|
|
501
|
-
} else if (typeof command !== 'string') {
|
|
502
|
-
command = String(command ?? '')
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
// Truncate outputs if too long
|
|
506
|
-
const isStdoutTruncated = stdout.length > MAX_OUTPUT_LENGTH;
|
|
507
|
-
const isStderrTruncated = stderr.length > MAX_OUTPUT_LENGTH;
|
|
508
|
-
const truncatedStdout = isStdoutTruncated ? truncateForHistory(stdout, MAX_OUTPUT_LENGTH, TRUNCATED_SUFFIX_MANUAL) : stdout;
|
|
509
|
-
const truncatedStderr = isStderrTruncated ? truncateForHistory(stderr, MAX_OUTPUT_LENGTH, TRUNCATED_SUFFIX_MANUAL) : stderr;
|
|
510
|
-
|
|
511
|
-
const historyItem = {
|
|
512
|
-
command,
|
|
513
|
-
stdout: truncatedStdout || '',
|
|
514
|
-
stderr: truncatedStderr || '',
|
|
515
|
-
error: error ? (typeof error === 'string' ? error : error.message) : null,
|
|
516
|
-
executionTime,
|
|
517
|
-
timestamp: new Date().toISOString(),
|
|
518
|
-
success: !error,
|
|
519
|
-
isStdoutTruncated,
|
|
520
|
-
isStderrTruncated
|
|
521
|
-
};
|
|
522
|
-
|
|
523
|
-
// Add to history (limited size)
|
|
524
|
-
commandHistory.unshift(historyItem);
|
|
525
|
-
if (commandHistory.length > MAX_HISTORY_SIZE) {
|
|
526
|
-
commandHistory.pop();
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
// Broadcast via WebSocket if available
|
|
530
|
-
if (ioInstance) {
|
|
531
|
-
ioInstance.emit('command_history_update', {
|
|
532
|
-
newCommand: historyItem,
|
|
533
|
-
fullHistory: commandHistory.slice(0, 10)
|
|
534
|
-
});
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
return historyItem;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
const getCwd = () => {
|
|
541
|
-
// process.argv 在进程内不变(CLI 启动后用户不会去改 process.argv),缓存一次
|
|
542
|
-
// 即可。execGitCommand 在一次 g 流程里会被调 5-15 次(每个 git 子命令一次),
|
|
543
|
-
// 缓存省掉重复的 Array.find + startsWith 扫描,在 GUI 后端常驻进程里累计收益更大。
|
|
544
|
-
if (_cachedCwd === null) {
|
|
545
|
-
const parsed = parseCwdArg(process.argv)
|
|
546
|
-
_cachedCwd = parsed || process.cwd()
|
|
547
|
-
}
|
|
548
|
-
return _cachedCwd
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
/**
|
|
552
|
-
* 主动让 getCwd 缓存失效。CLI 内部不会调;供 fs.js /api/change_directory
|
|
553
|
-
* 之类的"cwd 改变"路径使用,避免后续 execGitCommand 仍然用旧 cwd 跑子命令。
|
|
554
|
-
* 注意:CLI 子进程通常不需要这个,只有 GUI 后端在 chdir 后需要手动 clear。
|
|
555
|
-
*/
|
|
556
|
-
export function invalidateCwdCache() {
|
|
557
|
-
_cachedCwd = null
|
|
558
|
-
}
|
|
559
|
-
const judgePlatform = () => {
|
|
560
|
-
// 判断是否是 Windows 系统
|
|
561
|
-
if (os.platform() === 'win32') {
|
|
562
|
-
try {
|
|
563
|
-
// 设置终端字符编码为 UTF-8 — 这条必须 sync,后续 console 输出依赖
|
|
564
|
-
// 新的 code page,异步的话早期 console.log 会乱码。
|
|
565
|
-
execSync('chcp 65001');
|
|
566
|
-
} catch (e) {
|
|
567
|
-
console.error('设置字符编码失败:', e.message);
|
|
568
|
-
}
|
|
569
|
-
// git config --global 不影响启动期 console 输出,丢到后台跑,避免
|
|
570
|
-
// 两次额外的同步子进程阻塞 Node 启动(Windows 上各 ~30-80ms)
|
|
571
|
-
setImmediate(() => {
|
|
572
|
-
try {
|
|
573
|
-
execSync('git config --global core.autocrlf true');
|
|
574
|
-
execSync('git config --global core.quotepath false');
|
|
575
|
-
} catch (e) {
|
|
576
|
-
// 后台配置失败不致命,首次 commit 时用户会发现并自己跑命令
|
|
577
|
-
}
|
|
578
|
-
});
|
|
579
|
-
} else {
|
|
580
|
-
// 非 Windows:同样丢到后台
|
|
581
|
-
setImmediate(() => {
|
|
582
|
-
try {
|
|
583
|
-
execSync('git config --global core.autocrlf input');
|
|
584
|
-
} catch (_) {}
|
|
585
|
-
});
|
|
586
|
-
}
|
|
587
|
-
};
|
|
588
|
-
const showHelp = () => {
|
|
589
|
-
const helpMessage = `
|
|
590
|
-
Usage: g [options]
|
|
591
|
-
|
|
592
|
-
Options:
|
|
593
|
-
-h, --help Show this help message
|
|
594
|
-
--set-default-message=<msg> Set default commit message
|
|
595
|
-
get-config Show current configuration
|
|
596
|
-
-y Auto commit with default message
|
|
597
|
-
--ai AI generate commit message, commit and push
|
|
598
|
-
-m <message> Commit message (use quotes if message contains spaces)
|
|
599
|
-
-m=<message> Commit message (use this form without spaces around '=')
|
|
600
|
-
--path=<path> Set custom working directory
|
|
601
|
-
--cwd=<path> Set custom working directory
|
|
602
|
-
--interval=<seconds> Set interval time for automatic commits (in seconds)
|
|
603
|
-
log Show git commit logs
|
|
604
|
-
--n=<number> Number of commits to show with --log
|
|
605
|
-
--no-diff Skip displaying git diff
|
|
606
|
-
addScript Add "g:y": "g -y" to package.json scripts
|
|
607
|
-
addResetScript Add "g:reset": "git reset --hard origin/<current-branch>" to package.json scripts
|
|
608
|
-
ui Launch graphical user interface (v2.0.0)
|
|
609
|
-
|
|
610
|
-
File Locking:
|
|
611
|
-
--lock-file=<path> Lock a file to exclude it from commits
|
|
612
|
-
--unlock-file=<path> Unlock a previously locked file
|
|
613
|
-
--list-locked List all currently locked files
|
|
614
|
-
--check-lock=<path> Check if a file is locked
|
|
615
|
-
|
|
616
|
-
--cmd="your-cmd" Execute custom cmd command (immediately, at a time, or periodically)
|
|
617
|
-
--cmd-interval=<seconds> Execute custom cmd every N seconds
|
|
618
|
-
--at="HH:MM" Execute custom cmd at a specific time (today) or --at="YYYY-MM-DD HH:MM:SS"
|
|
619
|
-
--daily Repeat with --at every day at the same time
|
|
620
|
-
|
|
621
|
-
Example:
|
|
622
|
-
g --cmd="echo hello" --cmd-interval=5 # 每5秒执行一次echo hello
|
|
623
|
-
g --cmd="echo at-time" --at=23:59 # 在23:59执行一次echo at-time
|
|
624
|
-
g --cmd="echo daily" --at=23:59 --daily # 每天23:59执行一次echo daily
|
|
625
|
-
g --cmd="echo now" # 立即执行一次echo now
|
|
626
|
-
g --cmd="echo hi" --cmd-interval=10 --interval=60 # cmd和git自动提交并行
|
|
627
|
-
g -m "Initial commit" Commit with a custom message
|
|
628
|
-
g -m=Fix-bug Commit with a custom message (no spaces around '=')
|
|
629
|
-
g -y Auto commit with the default message
|
|
630
|
-
g --ai AI generate commit message, commit and push
|
|
631
|
-
g --ai --no-diff AI commit without displaying diff
|
|
632
|
-
g --ai --interval=600 AI commit every 10 minutes (600 seconds)
|
|
633
|
-
g -y --interval=600 Commit every 10 minutes (600 seconds)
|
|
634
|
-
g --path=/path/to/repo Specify a custom working directory
|
|
635
|
-
g log Show recent commit logs
|
|
636
|
-
g log --n=5 Show the last 5 commits with --log
|
|
637
|
-
g addScript Add auto commit script to package.json
|
|
638
|
-
g addResetScript Add reset script to package.json
|
|
639
|
-
g --lock-file=config.json Lock config.json file
|
|
640
|
-
g --unlock-file=config.json Unlock config.json file
|
|
641
|
-
g --list-locked List all locked files
|
|
642
|
-
|
|
643
|
-
Add auto submit in package.json:
|
|
644
|
-
"scripts": {
|
|
645
|
-
"g:y": "g -y",
|
|
646
|
-
"g:reset": "git reset --hard origin/<current-branch>"
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
Run in the background across platforms:
|
|
650
|
-
Windows:
|
|
651
|
-
start /min cmd /k "g -y --path=your-folder --interval=600"
|
|
652
|
-
|
|
653
|
-
Linux/macOS:
|
|
654
|
-
nohup g -y --path=your-folder --interval=600 > git-autocommit.log 2>&1 &
|
|
655
|
-
|
|
656
|
-
Start GUI interface:
|
|
657
|
-
g ui
|
|
658
|
-
|
|
659
|
-
Stop all monitoring processes:
|
|
660
|
-
Windows: Terminate the Node.js process in the Task Manager.
|
|
661
|
-
Linux/macOS:
|
|
662
|
-
pkill -f "g -y" # Terminate all auto-commit processes
|
|
663
|
-
ps aux | grep "g -y" # Find the specific process ID
|
|
664
|
-
kill [PID] # Terminate the specified process
|
|
665
|
-
`;
|
|
666
|
-
|
|
667
|
-
console.log(helpMessage);
|
|
668
|
-
process.exit();
|
|
669
|
-
};
|
|
670
|
-
|
|
671
|
-
function judgeLog() {
|
|
672
|
-
const logArg = process.argv.find(arg => arg === 'log');
|
|
673
|
-
if (logArg) {
|
|
674
|
-
printGitLog(); // 如果有 log 参数,打印 Git 提交记录
|
|
675
|
-
// 打印完成后退出
|
|
676
|
-
process.exit();
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
function judgeHelp() {
|
|
681
|
-
if (process.argv.includes('-h') || process.argv.includes('--help')) {
|
|
682
|
-
showHelp();
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
async function printGitLog() {
|
|
687
|
-
let n = 20;
|
|
688
|
-
let logArg = process.argv.find(arg => arg.startsWith('--n='));
|
|
689
|
-
if (logArg) {
|
|
690
|
-
n = parseInt(logArg.split('=')[1], 10);
|
|
691
|
-
}
|
|
692
|
-
// 使用 ASCII 记录分隔符 %x1E 作为字段分隔符
|
|
693
|
-
const logCommand = ['log', '-n', n, '--pretty=format:%C(green)%h%C(reset) %x1E %C(cyan)%an%C(reset) %x1E %C(yellow)%ad%C(reset) %x1E %C(blue)%D%C(reset) %x1E %C(magenta)%s%C(reset)', '--date=format:%Y-%m-%d %H:%M', '--graph', '--decorate', '--color']
|
|
694
|
-
try {
|
|
695
|
-
const logOutput = await execGitCommand(logCommand, {
|
|
696
|
-
head: `git log`
|
|
697
|
-
});
|
|
698
|
-
} catch (error) {
|
|
699
|
-
console.error('无法获取 Git 提交记录:', error.message);
|
|
700
|
-
}
|
|
701
|
-
// 打印完成后退出
|
|
702
|
-
process.exit();
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
function exec_exit(exit) {
|
|
706
|
-
// 仅当显式传 true 时退出;空值/0/'false'/'no'/其他非真值都按"不退出"处理。
|
|
707
|
-
// 之前的 truthy 判断会把字符串 'false' 误判为 true(非空字符串都是 truthy),
|
|
708
|
-
// 调用方传 '--exit=false' 等解析出来的字符串时会错误退出。
|
|
709
|
-
if (exit === true) {
|
|
710
|
-
process.exit()
|
|
711
|
-
}
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
function judgeUnmerged(statusOutput) {
|
|
715
|
-
const hasUnmerged = statusOutput.includes('You have unmerged paths');
|
|
716
|
-
if (hasUnmerged) {
|
|
717
|
-
errorLog('错误', '存在未合并的文件,请先解决冲突')
|
|
718
|
-
process.exit(1);
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
async function exec_push({exit, commitMessage}) {
|
|
723
|
-
// 执行 git push
|
|
724
|
-
const spinner = ora('正在推送代码...').start();
|
|
725
|
-
try {
|
|
726
|
-
const {stdout, stderr} = await execGitCommand(['push'], {
|
|
727
|
-
spinner
|
|
728
|
-
});
|
|
729
|
-
await printCommitLog({commitMessage});
|
|
730
|
-
return {stdout, stderr};
|
|
731
|
-
} catch (error) {
|
|
732
|
-
throw error;
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
async function printCommitLog({commitMessage}) {
|
|
737
|
-
try {
|
|
738
|
-
// 获取项目名称(取git仓库根目录名)
|
|
739
|
-
const projectRootResult = await execGitCommand(['rev-parse', '--show-toplevel'], {log: false});
|
|
740
|
-
const projectName = chalk.blueBright(path.basename(projectRootResult.stdout.trim()));
|
|
741
|
-
|
|
742
|
-
// 获取当前提交hash(取前7位)
|
|
743
|
-
const commitHashResult = await execGitCommand(['rev-parse', '--short', 'HEAD'], {log: false});
|
|
744
|
-
const hashDisplay = chalk.yellow(commitHashResult.stdout.trim());
|
|
745
|
-
|
|
746
|
-
// 获取分支信息
|
|
747
|
-
const branchResult = await execGitCommand(['branch', '--show-current'], {log: false});
|
|
748
|
-
const branchDisplay = chalk.magenta(branchResult.stdout.trim());
|
|
749
|
-
|
|
750
|
-
// 构建信息内容
|
|
751
|
-
const message = [
|
|
752
|
-
`${chalk.cyan.bold('Project:')} ${projectName}`,
|
|
753
|
-
`${chalk.cyan.bold('Commit:')} ${hashDisplay} ${chalk.dim('on')} ${branchDisplay}`,
|
|
754
|
-
`${chalk.cyan.bold('Message:')} ${chalk.reset(commitMessage)}`,
|
|
755
|
-
`${chalk.cyan.bold('Time:')} ${new Date().toLocaleString()}`
|
|
756
|
-
].join('\n');
|
|
757
|
-
|
|
758
|
-
// 使用boxen创建装饰框
|
|
759
|
-
const box = boxen(message, {
|
|
760
|
-
padding: 1,
|
|
761
|
-
margin: 1,
|
|
762
|
-
borderStyle: 'round',
|
|
763
|
-
borderColor: 'green',
|
|
764
|
-
title: chalk.bold.green('✅ COMMIT SUCCESS'),
|
|
765
|
-
titleAlignment: 'center',
|
|
766
|
-
float: 'left',
|
|
767
|
-
textAlignment: 'left'
|
|
768
|
-
});
|
|
769
|
-
|
|
770
|
-
console.log(box);
|
|
771
|
-
} catch (error) {
|
|
772
|
-
// 异常处理
|
|
773
|
-
const errorBox = boxen(chalk.red(`Failed to get commit details: ${error.message}`), {
|
|
774
|
-
borderColor: 'red',
|
|
775
|
-
padding: 1
|
|
776
|
-
});
|
|
777
|
-
console.log(errorBox);
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
async function execPull() {
|
|
782
|
-
try {
|
|
783
|
-
// 检查是否需要拉取更新
|
|
784
|
-
const spinner = ora('正在拉取代码...').start();
|
|
785
|
-
await execGitCommand(['pull'], {
|
|
786
|
-
spinner
|
|
787
|
-
})
|
|
788
|
-
} catch (e) {
|
|
789
|
-
console.log(chalk.yellow('⚠️ 拉取远程更新合并失败,可能存在冲突,请手动处理'));
|
|
790
|
-
throw Error(e)
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
async function judgeRemote() {
|
|
795
|
-
const spinner = ora('正在检查远程更新...').start();
|
|
796
|
-
try {
|
|
797
|
-
// 检查是否有远程更新
|
|
798
|
-
// 先获取远程最新状态
|
|
799
|
-
await execGitCommand(['remote', 'update'], {
|
|
800
|
-
head: 'Fetching remote updates',
|
|
801
|
-
log: false
|
|
802
|
-
});
|
|
803
|
-
// 检查是否需要 pull
|
|
804
|
-
const res = await execGitCommand(['rev-list', 'HEAD..@{u}', '--count'], {
|
|
805
|
-
head: 'Checking if behind remote',
|
|
806
|
-
log: false
|
|
807
|
-
});
|
|
808
|
-
const behindCount = res.stdout.trim()
|
|
809
|
-
const { green, black, bgGreen, white } = chalk;
|
|
810
|
-
// 如果本地落后于远程
|
|
811
|
-
if (parseInt(behindCount) > 0) {
|
|
812
|
-
try {
|
|
813
|
-
spinner.stop();
|
|
814
|
-
// const spinner_pull = ora('发现远程更新,正在拉取...').start();
|
|
815
|
-
await execPull()
|
|
816
|
-
|
|
817
|
-
// // 尝试使用 --ff-only 拉取更新
|
|
818
|
-
// const res = await execGitCommand('git pull --ff-only', {
|
|
819
|
-
// spinner: spinner_pull,
|
|
820
|
-
// head: 'Pulling updates'
|
|
821
|
-
// });
|
|
822
|
-
// bgGreen.white.bold(' SYNC ') +
|
|
823
|
-
// green` ➔ ` +
|
|
824
|
-
// chalk.blue.bold('远程仓库已同步') +
|
|
825
|
-
// green(' ✔')
|
|
826
|
-
// );
|
|
827
|
-
const message = '已成功同步远程更新'.split('').map((char, i) =>
|
|
828
|
-
chalk.rgb(0, 255 - i*10, 0)(char)
|
|
829
|
-
).join('');
|
|
830
|
-
|
|
831
|
-
console.log(chalk.bold(`✅ ${message}`));
|
|
832
|
-
} catch (pullError) {
|
|
833
|
-
// // 如果 --ff-only 拉取失败,尝试普通的 git pull
|
|
834
|
-
// await this.execPull()
|
|
835
|
-
throw new Error(pullError)
|
|
836
|
-
}
|
|
837
|
-
} else {
|
|
838
|
-
spinner.stop();
|
|
839
|
-
const message = '本地已是最新'.split('').map((char, i) =>
|
|
840
|
-
chalk.rgb(0, 255 - i*10, 0)(char)
|
|
841
|
-
).join('');
|
|
842
|
-
console.log(chalk.bold(`✅ ${message}`));
|
|
843
|
-
}
|
|
844
|
-
} catch (e) {
|
|
845
|
-
spinner.stop();
|
|
846
|
-
throw new Error(e)
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
async function execDiff() {
|
|
851
|
-
const no_diff = process.argv.find(arg => arg.startsWith('--no-diff'))
|
|
852
|
-
if (!no_diff) {
|
|
853
|
-
await execGitCommand(['diff', '--color=always'], {
|
|
854
|
-
head: `git diff`
|
|
855
|
-
})
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
|
|
859
|
-
// 执行 git add 但排除锁定的文件
|
|
860
|
-
async function execGitAddWithLockFilter() {
|
|
861
|
-
try {
|
|
862
|
-
// 获取锁定的文件列表
|
|
863
|
-
const lockedFiles = await config.getLockedFiles();
|
|
864
|
-
|
|
865
|
-
if (lockedFiles.length === 0) {
|
|
866
|
-
// 如果没有锁定文件,直接执行 git add .
|
|
867
|
-
await execGitCommand(['add', '.']);
|
|
868
|
-
return;
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
// 获取Git工作目录根路径,确保路径匹配的准确性
|
|
872
|
-
let gitRoot;
|
|
873
|
-
try {
|
|
874
|
-
const gitRootResult = await execGitCommand(['rev-parse', '--show-toplevel'], {log: false});
|
|
875
|
-
gitRoot = path.normalize(gitRootResult.stdout.trim());
|
|
876
|
-
} catch (error) {
|
|
877
|
-
console.warn(chalk.yellow('⚠️ 无法获取Git根目录,使用当前工作目录'));
|
|
878
|
-
gitRoot = path.normalize(process.cwd());
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
// 获取所有修改的文件(包括未跟踪文件)
|
|
882
|
-
const statusResult = await execGitCommand(['status', '--porcelain', '--untracked-files=all'], {log: false});
|
|
883
|
-
const modifiedFiles = statusResult.stdout
|
|
884
|
-
.split('\n')
|
|
885
|
-
.filter(line => line.trim())
|
|
886
|
-
.map(line => {
|
|
887
|
-
// 解析 git status --porcelain 的输出格式
|
|
888
|
-
// 格式: XY filename 或 XY "filename with spaces"
|
|
889
|
-
const match = line.match(/^..\s+(.+)$/);
|
|
890
|
-
if (match) {
|
|
891
|
-
let filename = match[1];
|
|
892
|
-
// 如果文件名被引号包围,去掉引号
|
|
893
|
-
if (filename.startsWith('"') && filename.endsWith('"')) {
|
|
894
|
-
filename = filename.slice(1, -1);
|
|
895
|
-
// 处理转义字符
|
|
896
|
-
filename = filename.replace(/\\(.)/g, '$1');
|
|
897
|
-
}
|
|
898
|
-
return filename;
|
|
899
|
-
}
|
|
900
|
-
return null;
|
|
901
|
-
})
|
|
902
|
-
.filter(Boolean);
|
|
903
|
-
|
|
904
|
-
// 过滤掉锁定的文件,使用更严格的路径匹配逻辑
|
|
905
|
-
const filesToAdd = modifiedFiles.filter(file => {
|
|
906
|
-
// Git status 返回的是相对于Git根目录的路径
|
|
907
|
-
const gitRelativeFile = path.normalize(file);
|
|
908
|
-
|
|
909
|
-
const isLocked = lockedFiles.some(lockedFile => {
|
|
910
|
-
// 处理锁定文件路径:可能是相对路径或绝对路径
|
|
911
|
-
let normalizedLocked;
|
|
912
|
-
if (path.isAbsolute(lockedFile)) {
|
|
913
|
-
// 绝对路径:转换为相对于Git根目录的路径
|
|
914
|
-
const absoluteLocked = path.normalize(lockedFile);
|
|
915
|
-
if (absoluteLocked.startsWith(gitRoot)) {
|
|
916
|
-
normalizedLocked = path.relative(gitRoot, absoluteLocked);
|
|
917
|
-
} else {
|
|
918
|
-
// 锁定文件不在当前Git仓库中,跳过
|
|
919
|
-
return false;
|
|
920
|
-
}
|
|
921
|
-
} else {
|
|
922
|
-
// 相对路径:直接使用
|
|
923
|
-
normalizedLocked = path.normalize(lockedFile);
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
// 统一路径分隔符(Windows兼容性)
|
|
927
|
-
const normalizedGitFile = gitRelativeFile.replace(/\\/g, '/');
|
|
928
|
-
const normalizedLockedFile = normalizedLocked.replace(/\\/g, '/');
|
|
929
|
-
|
|
930
|
-
// 精确匹配或目录匹配(双向检查)
|
|
931
|
-
const isExactMatch = normalizedGitFile === normalizedLockedFile;
|
|
932
|
-
const isFileInLockedDir = normalizedGitFile.startsWith(normalizedLockedFile + '/');
|
|
933
|
-
const isLockedFileInDir = normalizedLockedFile.startsWith(normalizedGitFile + '/');
|
|
934
|
-
|
|
935
|
-
return isExactMatch || isFileInLockedDir || isLockedFileInDir;
|
|
936
|
-
});
|
|
937
|
-
|
|
938
|
-
// 额外检查:如果是目录路径,检查该目录下是否有任何未锁定的文件
|
|
939
|
-
if (!isLocked && file.endsWith('/')) {
|
|
940
|
-
// 这是一个目录路径,检查是否该目录下所有文件都被锁定
|
|
941
|
-
const dirPath = file.slice(0, -1); // 移除末尾的 '/'
|
|
942
|
-
const hasUnlockedFilesInDir = modifiedFiles.some(otherFile => {
|
|
943
|
-
if (otherFile === file) return false; // 跳过目录本身
|
|
944
|
-
|
|
945
|
-
const normalizedOtherFile = path.normalize(otherFile).replace(/\\/g, '/');
|
|
946
|
-
const normalizedDirPath = dirPath.replace(/\\/g, '/');
|
|
947
|
-
|
|
948
|
-
// 检查文件是否在这个目录下
|
|
949
|
-
if (normalizedOtherFile.startsWith(normalizedDirPath + '/')) {
|
|
950
|
-
// 检查这个文件是否被锁定
|
|
951
|
-
const isOtherFileLocked = lockedFiles.some(lockedFile => {
|
|
952
|
-
let normalizedLocked;
|
|
953
|
-
if (path.isAbsolute(lockedFile)) {
|
|
954
|
-
const absoluteLocked = path.normalize(lockedFile);
|
|
955
|
-
if (absoluteLocked.startsWith(gitRoot)) {
|
|
956
|
-
normalizedLocked = path.relative(gitRoot, absoluteLocked);
|
|
957
|
-
} else {
|
|
958
|
-
return false;
|
|
959
|
-
}
|
|
960
|
-
} else {
|
|
961
|
-
normalizedLocked = path.normalize(lockedFile);
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
const normalizedLockedFile = normalizedLocked.replace(/\\/g, '/');
|
|
965
|
-
return normalizedOtherFile === normalizedLockedFile ||
|
|
966
|
-
normalizedOtherFile.startsWith(normalizedLockedFile + '/') ||
|
|
967
|
-
normalizedLockedFile.startsWith(normalizedOtherFile + '/');
|
|
968
|
-
});
|
|
969
|
-
|
|
970
|
-
return !isOtherFileLocked; // 如果文件未锁定,返回 true
|
|
971
|
-
}
|
|
972
|
-
return false;
|
|
973
|
-
});
|
|
974
|
-
|
|
975
|
-
// 如果目录下没有未锁定的文件,则跳过这个目录
|
|
976
|
-
if (!hasUnlockedFilesInDir) {
|
|
977
|
-
console.log(chalk.yellow(`🔒 跳过目录(所有文件都被锁定): ${file}`));
|
|
978
|
-
return false;
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
if (isLocked) {
|
|
983
|
-
console.log(chalk.yellow(`🔒 跳过锁定文件: ${file}`));
|
|
984
|
-
return false;
|
|
985
|
-
}
|
|
986
|
-
return true;
|
|
987
|
-
});
|
|
988
|
-
|
|
989
|
-
if (filesToAdd.length === 0) {
|
|
990
|
-
console.log(chalk.blue('📝 所有修改的文件都被锁定,没有文件需要添加'));
|
|
991
|
-
return;
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
// 逐个添加未锁定的文件
|
|
995
|
-
// 用 execFile('git', argv) 直接传 argv 数组,不经过 shell,
|
|
996
|
-
// 文件名含空格/特殊字符时不需要 shellQuote;同时跨 Windows cmd.exe /
|
|
997
|
-
// POSIX sh 都一致(此前单引号写法在 Windows cmd.exe 不被识别,导致
|
|
998
|
-
// "pathspec '...did not match any files" 报错,见 da01bcb 回归)。
|
|
999
|
-
let successCount = 0
|
|
1000
|
-
let failedFiles = []
|
|
1001
|
-
for (const file of filesToAdd) {
|
|
1002
|
-
try {
|
|
1003
|
-
await execGitCommand(['add', '--', file], {
|
|
1004
|
-
head: `git add ${file}`,
|
|
1005
|
-
log: false
|
|
1006
|
-
})
|
|
1007
|
-
successCount++
|
|
1008
|
-
} catch (err) {
|
|
1009
|
-
// 单文件失败不阻断整批,记录失败清单让用户感知
|
|
1010
|
-
failedFiles.push({ file, error: err?.message || String(err) })
|
|
1011
|
-
console.warn(chalk.yellow(`⚠️ 添加失败: ${file} — ${err?.message || err}`))
|
|
1012
|
-
}
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
const skippedCount = modifiedFiles.length - filesToAdd.length;
|
|
1016
|
-
const failSuffix = failedFiles.length > 0 ? `, ${failedFiles.length} 个失败` : ''
|
|
1017
|
-
console.log(chalk.green(`✅ 已添加 ${successCount} 个文件到暂存区${skippedCount > 0 ? ` (跳过 ${skippedCount} 个锁定文件)` : ''}${failSuffix}`));
|
|
1018
|
-
if (failedFiles.length > 0) {
|
|
1019
|
-
// 把失败列表挂到 thrown error 上,上层可选择性展示
|
|
1020
|
-
const e = new Error(`git add 部分失败 (${failedFiles.length}/${filesToAdd.length})`)
|
|
1021
|
-
e.failedFiles = failedFiles
|
|
1022
|
-
throw e
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
} catch (error) {
|
|
1026
|
-
console.error(chalk.red('执行 git add 时出错:'), error.message);
|
|
1027
|
-
throw error;
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
async function execAddAndCommit({statusOutput, commitMessage, exit}) {
|
|
1032
|
-
// 检查 -m 参数(提交信息)
|
|
1033
|
-
const commitMessageArg = process.argv.find(arg => arg.startsWith('-m'));
|
|
1034
|
-
if (commitMessageArg) {
|
|
1035
|
-
if (commitMessageArg.includes('=')) {
|
|
1036
|
-
// 处理 -m=<message> 的情况
|
|
1037
|
-
commitMessage = commitMessageArg.split('=')[1]?.replace(/^['"]|['"]$/g, '');
|
|
1038
|
-
} else {
|
|
1039
|
-
// 处理 -m <message> 的情况
|
|
1040
|
-
const index = process.argv.indexOf(commitMessageArg);
|
|
1041
|
-
if (index !== -1 && process.argv[index + 1]) {
|
|
1042
|
-
commitMessage = process.argv[index + 1]?.replace(/^['"]|['"]$/g, '');
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
// 检查命令行参数,判断是否有 -y 或 --ai 参数(都跳过用户输入提示)
|
|
1048
|
-
const autoCommit = process.argv.includes('-y') || process.argv.includes('--ai');
|
|
1049
|
-
|
|
1050
|
-
if (!autoCommit && !commitMessageArg) {
|
|
1051
|
-
// 如果没有 -y 参数,则等待用户输入提交信息
|
|
1052
|
-
const rl = readline.createInterface({
|
|
1053
|
-
input: process.stdin,
|
|
1054
|
-
output: process.stdout
|
|
1055
|
-
})
|
|
1056
|
-
|
|
1057
|
-
function rlPromisify(fn) {
|
|
1058
|
-
return async (...args) => {
|
|
1059
|
-
return new Promise((resolve, reject) => fn(...args, resolve, reject))
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
const question = rlPromisify(rl.question.bind(rl))
|
|
1064
|
-
commitMessage = await question('请输入提交信息:') || commitMessage;
|
|
1065
|
-
rl.close(); // 关闭 readline 接口
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
|
-
// 使用带锁定文件过滤的 git add
|
|
1069
|
-
if (statusOutput.includes('(use "git add')) {
|
|
1070
|
-
await execGitAddWithLockFilter();
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
// 提交前二次校验(包括未跟踪文件)
|
|
1074
|
-
const checkStatus = await execGitCommand(['status', '--porcelain', '--untracked-files=all'], {log: false});
|
|
1075
|
-
if (!checkStatus.stdout.trim()) {
|
|
1076
|
-
console.log(chalk.yellow('⚠️ 没有检测到可提交的变更'));
|
|
1077
|
-
// exec_exit(exit)
|
|
1078
|
-
return commitMessage; // 返回提交信息(即使没有提交)
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
// 执行 git commit
|
|
1082
|
-
if (statusOutput.includes('Untracked files:') || statusOutput.includes('Changes not staged for commit') || statusOutput.includes('Changes to be committed')) {
|
|
1083
|
-
await execGitCommand(['commit', '-m', commitMessage])
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
// 返回实际使用的提交信息
|
|
1087
|
-
return commitMessage;
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
// 添加时间格式化函数
|
|
1091
|
-
async function addScriptToPackageJson() {
|
|
1092
|
-
try {
|
|
1093
|
-
// 读取当前目录的 package.json
|
|
1094
|
-
const packagePath = path.join(process.cwd(), 'package.json');
|
|
1095
|
-
const packageJson = JSON.parse(await fs.readFile(packagePath, 'utf8'));
|
|
1096
|
-
|
|
1097
|
-
// 确保有 scripts 部分
|
|
1098
|
-
if (!packageJson.scripts) {
|
|
1099
|
-
packageJson.scripts = {};
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
// 添加 g:y 命令
|
|
1103
|
-
if (!packageJson.scripts['g:y']) {
|
|
1104
|
-
packageJson.scripts['g:y'] = 'g -y';
|
|
1105
|
-
// 写回文件
|
|
1106
|
-
await fs.writeFile(packagePath, JSON.stringify(packageJson, null, 2));
|
|
1107
|
-
console.log(chalk.green('✓ 成功添加 g:y 脚本到 package.json'));
|
|
1108
|
-
} else {
|
|
1109
|
-
console.log(chalk.yellow('⚠️ g:y 脚本已存在'));
|
|
1110
|
-
}
|
|
1111
|
-
} catch (error) {
|
|
1112
|
-
if (error.code === 'ENOENT') {
|
|
1113
|
-
console.error(chalk.red('❌ 当前目录下未找到 package.json 文件'));
|
|
1114
|
-
} else {
|
|
1115
|
-
console.error(chalk.red('❌ 添加脚本失败:'), error.message);
|
|
1116
|
-
}
|
|
1117
|
-
process.exit(1);
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
async function addResetScriptToPackageJson() {
|
|
1122
|
-
try {
|
|
1123
|
-
// 读取当前目录的 package.json
|
|
1124
|
-
const packagePath = path.join(process.cwd(), 'package.json');
|
|
1125
|
-
const packageJson = JSON.parse(await fs.readFile(packagePath, 'utf8'));
|
|
1126
|
-
|
|
1127
|
-
// 确保有 scripts 部分
|
|
1128
|
-
if (!packageJson.scripts) {
|
|
1129
|
-
packageJson.scripts = {};
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
// 获取当前分支名
|
|
1133
|
-
const branchResult = await execGitCommand(['branch', '--show-current'], {log: false});
|
|
1134
|
-
const branch = branchResult.stdout.trim();
|
|
1135
|
-
|
|
1136
|
-
// 添加 g:reset 命令
|
|
1137
|
-
if (!packageJson.scripts['g:reset']) {
|
|
1138
|
-
packageJson.scripts['g:reset'] = `git reset --hard origin/${branch}`;
|
|
1139
|
-
// 写回文件
|
|
1140
|
-
await fs.writeFile(packagePath, JSON.stringify(packageJson, null, 2));
|
|
1141
|
-
console.log(chalk.green(`✓ 成功添加 g:reset 脚本到 package.json (重置到 origin/${branch})`));
|
|
1142
|
-
} else {
|
|
1143
|
-
console.log(chalk.yellow('⚠️ g:reset 脚本已存在'));
|
|
1144
|
-
}
|
|
1145
|
-
} catch (error) {
|
|
1146
|
-
if (error.code === 'ENOENT') {
|
|
1147
|
-
console.error(chalk.red('❌ 当前目录下未找到 package.json 文件'));
|
|
1148
|
-
} else {
|
|
1149
|
-
console.error(chalk.red('❌ 添加脚本失败:'), error.message);
|
|
1150
|
-
}
|
|
1151
|
-
process.exit(1);
|
|
1152
|
-
}
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
export {
|
|
1156
|
-
coloredLog, errorLog, execSyncGitCommand,
|
|
1157
|
-
execGitCommand, getCommandHistory, addCommandToHistory, // Add command history exports
|
|
1158
|
-
clearCommandHistory,
|
|
1159
|
-
// parseCwdArg / truncateForHistory / formatDuration / delay 已在文件顶部从
|
|
1160
|
-
// ./format.js re-export,这里不再重复导出,避免 Duplicate export 错误。
|
|
1161
|
-
checkAndClearGitLock,
|
|
1162
|
-
registerSocketIO, // 导出注册Socket.io的函数
|
|
1163
|
-
getCwd, judgePlatform, showHelp, judgeLog, printGitLog,
|
|
1164
|
-
judgeHelp, exec_exit, judgeUnmerged,
|
|
1165
|
-
exec_push, execPull, judgeRemote, execDiff, execAddAndCommit,
|
|
1166
|
-
execGitAddWithLockFilter, // 导出新的 git add 函数
|
|
1167
|
-
addScriptToPackageJson, addResetScriptToPackageJson
|
|
1168
|
-
};
|
|
1
|
+
// Copyright 2026 xz333221
|
|
2
|
+
//
|
|
3
|
+
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
// you may not use this file except in compliance with the License.
|
|
5
|
+
// You may obtain a copy of the License at
|
|
6
|
+
//
|
|
7
|
+
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
//
|
|
9
|
+
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
// See the License for the specific language governing permissions and
|
|
13
|
+
// limitations under the License.
|
|
14
|
+
//
|
|
15
|
+
// git 子进程包装 + 命令历史 + 锁定文件过滤 git add
|
|
16
|
+
// 纯函数(parseCwdArg / truncateForHistory / formatDuration / delay)已抽到
|
|
17
|
+
// ./format.js,这里 re-export 以保持向后兼容(老调用方无需改 import)。
|
|
18
|
+
import chalk from 'chalk';
|
|
19
|
+
import boxen from "boxen";
|
|
20
|
+
import stringWidth from 'string-width';
|
|
21
|
+
import {execFile, execSync} from 'child_process'
|
|
22
|
+
import os from 'os'
|
|
23
|
+
import ora from "ora";
|
|
24
|
+
import readline from 'readline'
|
|
25
|
+
import path from 'path'
|
|
26
|
+
import fs from 'fs/promises'
|
|
27
|
+
import config from '../config.js'
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
import {
|
|
31
|
+
parseCwdArg as _parseCwdArg,
|
|
32
|
+
truncateForHistory as _truncateForHistory,
|
|
33
|
+
formatDuration as _formatDuration,
|
|
34
|
+
delay as _delay,
|
|
35
|
+
} from './format.js'
|
|
36
|
+
|
|
37
|
+
// 向后兼容 re-export — 老调用方 import { parseCwdArg } from '../utils' 仍然可用
|
|
38
|
+
export const parseCwdArg = _parseCwdArg
|
|
39
|
+
export const truncateForHistory = _truncateForHistory
|
|
40
|
+
export const formatDuration = _formatDuration
|
|
41
|
+
export const delay = _delay
|
|
42
|
+
|
|
43
|
+
const calcColor = (commandLine, str) => {
|
|
44
|
+
let color = 'reset'
|
|
45
|
+
if (commandLine === 'git status' && str.startsWith('\t')) {
|
|
46
|
+
color = 'red'
|
|
47
|
+
if (str.startsWith('modified:')) color = 'green'
|
|
48
|
+
if (str.startsWith('deleted:')) color = 'red'
|
|
49
|
+
}
|
|
50
|
+
return color
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const tableLog = (commandLine, content, type) => {
|
|
54
|
+
const headLabel = `> ${commandLine}`
|
|
55
|
+
let head = chalk.bold.blue(headLabel)
|
|
56
|
+
if (type === 'error') {
|
|
57
|
+
content = content.toString().split('\n')
|
|
58
|
+
head = chalk.bold.red(headLabel)
|
|
59
|
+
} else if (type === 'common') {
|
|
60
|
+
content = content.split('\n')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 单次输出限幅,避免 git log --all 之类命令刷屏
|
|
64
|
+
const MAX_LINES = 10
|
|
65
|
+
const MAX_LINE_LENGTH = 200
|
|
66
|
+
let isTruncated = false
|
|
67
|
+
if (content.length > MAX_LINES) {
|
|
68
|
+
content = content.slice(0, MAX_LINES)
|
|
69
|
+
isTruncated = true
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
content = content.map((item) => {
|
|
73
|
+
const fontColor = calcColor(commandLine, item)
|
|
74
|
+
let row = item.replaceAll('\t', ' ')
|
|
75
|
+
if (row.length > MAX_LINE_LENGTH) {
|
|
76
|
+
row = row.substring(0, MAX_LINE_LENGTH) + '...'
|
|
77
|
+
}
|
|
78
|
+
return chalk[fontColor](row)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
if (isTruncated) {
|
|
82
|
+
content.push(chalk.dim('... (输出内容过多,已省略)'))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 用 box-drawing 字符画完整边框(上下左右 + header 分隔线),视觉上把每条 git 命令的输出框起来
|
|
86
|
+
// 复刻旧版 coloredLog 的样式:┌─┐│├─┤└─┘,每行右侧补空格对齐到框宽 + 右边框 │
|
|
87
|
+
const terminalWidth = Math.min(process.stdout.columns || 80, 120)
|
|
88
|
+
const startLine = '┌' + '─'.repeat(terminalWidth - 2) + '┐'
|
|
89
|
+
const midLine = '├' + '─'.repeat(terminalWidth - 2) + '┤'
|
|
90
|
+
const endLine = '└' + '─'.repeat(terminalWidth - 2) + '┘'
|
|
91
|
+
|
|
92
|
+
// 计算每行右侧需要补多少空格(用 stringWidth 处理中文/emoji 宽度,避免对不齐)
|
|
93
|
+
const padRight = (text, extraFix = 0) => {
|
|
94
|
+
const len = stringWidth(text)
|
|
95
|
+
const repeatLen = terminalWidth - len - 3 - extraFix
|
|
96
|
+
return ' '.repeat(Math.max(repeatLen, 0)) + '│'
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log(startLine)
|
|
100
|
+
// header 行:│ > command │,下面跟一条分隔线
|
|
101
|
+
console.log(`│ ${head}${padRight(headLabel, 2)}`)
|
|
102
|
+
console.log(midLine)
|
|
103
|
+
// 内容行:│ content │
|
|
104
|
+
for (const line of content) {
|
|
105
|
+
if (line.trim().length === 0) continue
|
|
106
|
+
console.log(`│ ${line}${padRight(line)}`)
|
|
107
|
+
}
|
|
108
|
+
console.log(endLine)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const coloredLog = (...args) => {
|
|
112
|
+
const [commandLine, content, type = 'common'] = args
|
|
113
|
+
tableLog(commandLine, content, type)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const errorLog = (commandLine, content) => {
|
|
117
|
+
const msg = ` FAIL ${commandLine}\n content: ${content} `
|
|
118
|
+
const box = boxen(chalk.red.bold(msg))
|
|
119
|
+
console.log(box)
|
|
120
|
+
}
|
|
121
|
+
function execSyncGitCommand(command, options = {}) {
|
|
122
|
+
let {encoding = 'utf-8', maxBuffer = 30 * 1024 * 1024, head = command, log = true} = options
|
|
123
|
+
try {
|
|
124
|
+
let cwd = getCwd()
|
|
125
|
+
const output = execSync(command, {
|
|
126
|
+
env: {
|
|
127
|
+
...process.env,
|
|
128
|
+
// LANG: 'en_US.UTF-8', // Linux/macOS
|
|
129
|
+
// LC_ALL: 'en_US.UTF-8', // Linux/macOS
|
|
130
|
+
GIT_CONFIG_PARAMETERS: "'core.quotepath=false'" // 关闭路径转义
|
|
131
|
+
}, encoding, maxBuffer, cwd
|
|
132
|
+
})
|
|
133
|
+
if (options.spinner) {
|
|
134
|
+
options.spinner.stop();
|
|
135
|
+
}
|
|
136
|
+
let result = output.trim()
|
|
137
|
+
log && coloredLog(head, result)
|
|
138
|
+
// 打印当前目录和时间信息
|
|
139
|
+
if (log) {
|
|
140
|
+
const currentTime = new Date().toLocaleString('zh-CN', { hour12: false });
|
|
141
|
+
console.log(chalk.dim(`📁 目录: ${cwd} | ⏰ 时间: ${currentTime}`));
|
|
142
|
+
}
|
|
143
|
+
return result
|
|
144
|
+
} catch (e) {
|
|
145
|
+
log && coloredLog(command, e, 'error')
|
|
146
|
+
// 透传原始 error,保留 stack + 已挂载的 stdout/stderr
|
|
147
|
+
throw e
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Add a command history array to store commands and their results
|
|
152
|
+
const commandHistory = [];
|
|
153
|
+
const MAX_HISTORY_SIZE = 100;
|
|
154
|
+
// 输出截断上限 — 单点常量,避免在 execGitCommand / addCommandToHistory /
|
|
155
|
+
// 其他未来 history 写入点之间漂移导致截断行为不一致。
|
|
156
|
+
// 截断提示也集中在这里,所有调用方用同一个文案,便于后续 i18n 替换。
|
|
157
|
+
const MAX_OUTPUT_LENGTH = 5000;
|
|
158
|
+
const TRUNCATED_SUFFIX_STDOUT = '\n... (output truncated)';
|
|
159
|
+
const TRUNCATED_SUFFIX_STDERR = '\n... (error output truncated)';
|
|
160
|
+
const TRUNCATED_SUFFIX_MANUAL = '...[truncated]';
|
|
161
|
+
|
|
162
|
+
// 添加一个变量来保存Socket.io实例
|
|
163
|
+
let ioInstance = null;
|
|
164
|
+
|
|
165
|
+
// 提供注册Socket.io实例的函数
|
|
166
|
+
function registerSocketIO(io) {
|
|
167
|
+
ioInstance = io;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 清空命令历史记录
|
|
171
|
+
function clearCommandHistory() {
|
|
172
|
+
// 清空数组
|
|
173
|
+
commandHistory.length = 0;
|
|
174
|
+
|
|
175
|
+
// 通过WebSocket广播历史已清空
|
|
176
|
+
if (ioInstance) {
|
|
177
|
+
ioInstance.emit('command_history_cleared');
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 按 git 子命令类型设定默认超时(毫秒),避免 git push/fetch 撞死网导致
|
|
184
|
+
// 整个 Node 事件循环永久挂起。读类操作给短超时(快);网络类给长超时;
|
|
185
|
+
// 默认 60s 兜底。调用方可通过 options.timeout 显式覆盖,timeout=0 表示不超时。
|
|
186
|
+
const DEFAULT_GIT_TIMEOUT_MS = {
|
|
187
|
+
// 只读 metadata,通常几 ms 完成
|
|
188
|
+
status: 30_000,
|
|
189
|
+
'rev-parse': 10_000,
|
|
190
|
+
branch: 10_000,
|
|
191
|
+
'rev-list': 30_000,
|
|
192
|
+
// 输出可能很大
|
|
193
|
+
log: 30_000,
|
|
194
|
+
diff: 60_000,
|
|
195
|
+
show: 30_000,
|
|
196
|
+
// 写操作,锁文件 + 索引更新
|
|
197
|
+
add: 60_000,
|
|
198
|
+
commit: 60_000,
|
|
199
|
+
// 网络操作,留足时间应对慢网 + 钩子 + 大仓库
|
|
200
|
+
fetch: 300_000,
|
|
201
|
+
pull: 300_000,
|
|
202
|
+
push: 300_000,
|
|
203
|
+
clone: 600_000,
|
|
204
|
+
remote: 60_000,
|
|
205
|
+
// 兜底
|
|
206
|
+
default: 60_000,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
function getDefaultGitTimeout(command) {
|
|
210
|
+
if (!Array.isArray(command) || command.length === 0) return DEFAULT_GIT_TIMEOUT_MS.default;
|
|
211
|
+
const sub = String(command[0] || '').trim();
|
|
212
|
+
return DEFAULT_GIT_TIMEOUT_MS[sub] ?? DEFAULT_GIT_TIMEOUT_MS.default;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function execGitCommand(command, options = {}) {
|
|
216
|
+
return new Promise((resolve, reject) => {
|
|
217
|
+
let {encoding = 'utf-8', maxBuffer = 30 * 1024 * 1024, head = Array.isArray(command) ? command.join(' ') : command, log = true, timeout} = options
|
|
218
|
+
// options.timeout === undefined → 按子命令类型取默认;0 = 不超时(显式禁用)
|
|
219
|
+
if (timeout === undefined) {
|
|
220
|
+
timeout = getDefaultGitTimeout(command);
|
|
221
|
+
}
|
|
222
|
+
let cwd = getCwd()
|
|
223
|
+
|
|
224
|
+
// Record start time for command execution
|
|
225
|
+
const startTime = Date.now();
|
|
226
|
+
|
|
227
|
+
// setTimeout(() => {
|
|
228
|
+
// 用 execFile('git', argv) 跨平台执行:
|
|
229
|
+
// - 不走 shell,Windows cmd.exe / POSIX sh 都一致
|
|
230
|
+
// - argv 数组天然免疫 shell 注入,文件名/参数无需 shellQuote
|
|
231
|
+
// - 所有调用点约定只跑 git 子命令,见 src/utils/index.js 内的 execGitCommand 调用清单
|
|
232
|
+
// - timeout 显式传 0 表示禁用;否则按 getDefaultGitTimeout() 自动选值
|
|
233
|
+
execFile('git', command, {
|
|
234
|
+
env: {
|
|
235
|
+
...process.env,
|
|
236
|
+
// LANG: 'en_US.UTF-8', // Linux/macOS
|
|
237
|
+
// LC_ALL: 'en_US.UTF-8', // Linux/macOS
|
|
238
|
+
GIT_CONFIG_PARAMETERS: "'core.quotepath=false'" // 关闭路径转义
|
|
239
|
+
},
|
|
240
|
+
encoding,
|
|
241
|
+
maxBuffer,
|
|
242
|
+
cwd,
|
|
243
|
+
// Windows 下 git 是 .exe;POSIX 下直接 PATH 找。
|
|
244
|
+
// 不传 shell,杜绝 cmd.exe 单/双引号兼容问题与注入风险。
|
|
245
|
+
windowsHide: true,
|
|
246
|
+
// 超时后 execFile 自动给子进程发 SIGTERM,callback 收到 error.killed=true
|
|
247
|
+
timeout
|
|
248
|
+
}, (error, stdout, stderr) => {
|
|
249
|
+
if (options.spinner) {
|
|
250
|
+
options.spinner.stop();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Calculate execution time
|
|
254
|
+
const executionTime = Date.now() - startTime;
|
|
255
|
+
|
|
256
|
+
// Truncate long outputs
|
|
257
|
+
let truncatedStdout = stdout;
|
|
258
|
+
let truncatedStderr = stderr;
|
|
259
|
+
let isStdoutTruncated = false;
|
|
260
|
+
let isStderrTruncated = false;
|
|
261
|
+
|
|
262
|
+
if (stdout && stdout.length > MAX_OUTPUT_LENGTH) {
|
|
263
|
+
truncatedStdout = truncateForHistory(stdout, MAX_OUTPUT_LENGTH, TRUNCATED_SUFFIX_STDOUT);
|
|
264
|
+
isStdoutTruncated = true;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (stderr && stderr.length > MAX_OUTPUT_LENGTH) {
|
|
268
|
+
truncatedStderr = truncateForHistory(stderr, MAX_OUTPUT_LENGTH, TRUNCATED_SUFFIX_STDERR);
|
|
269
|
+
isStderrTruncated = true;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Add command to history
|
|
273
|
+
// 用 head(已 join)而不是原始 command,确保历史里的 command 永远是字符串
|
|
274
|
+
// 防止 execGitCommand(['status']) 这类数组调用把数组塞进历史 → 前端 item.command.trim() 崩
|
|
275
|
+
const historyItem = {
|
|
276
|
+
command: head,
|
|
277
|
+
stdout: truncatedStdout || '',
|
|
278
|
+
stderr: truncatedStderr || '',
|
|
279
|
+
error: error ? error.message : null,
|
|
280
|
+
executionTime,
|
|
281
|
+
timestamp: new Date().toISOString(),
|
|
282
|
+
success: !error,
|
|
283
|
+
isStdoutTruncated,
|
|
284
|
+
isStderrTruncated
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// Add to history (limited size)
|
|
288
|
+
commandHistory.unshift(historyItem);
|
|
289
|
+
if (commandHistory.length > MAX_HISTORY_SIZE) {
|
|
290
|
+
commandHistory.pop();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 通过WebSocket广播命令历史更新
|
|
294
|
+
if (ioInstance) {
|
|
295
|
+
ioInstance.emit('command_history_update', {
|
|
296
|
+
newCommand: historyItem,
|
|
297
|
+
fullHistory: commandHistory.slice(0, 10) // 只发送最近10条以减小数据量
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (stdout) {
|
|
302
|
+
log && coloredLog(head, stdout)
|
|
303
|
+
}
|
|
304
|
+
if (stderr) {
|
|
305
|
+
log && coloredLog(head, stderr)
|
|
306
|
+
}
|
|
307
|
+
// 打印当前目录和时间信息
|
|
308
|
+
if (log && (stdout || stderr)) {
|
|
309
|
+
const currentTime = new Date().toLocaleString('zh-CN', { hour12: false });
|
|
310
|
+
console.log(chalk.dim(`📁 目录: ${cwd} | ⏰ 时间: ${currentTime}`));
|
|
311
|
+
}
|
|
312
|
+
if (error) {
|
|
313
|
+
// 检测是否超时:execFile 超时后 error.killed=true,signal='SIGTERM',
|
|
314
|
+
// error.code=null(被信号杀,无 exit code)。在 error 上加 isTimeout 标记
|
|
315
|
+
// 方便上层(路由 / GUI 前端)区分"超时"和"git 自己报错"做不同提示。
|
|
316
|
+
if (error.killed && (error.signal === 'SIGTERM' || /TIMEOUT|timeout/i.test(error.message || ''))) {
|
|
317
|
+
error.isTimeout = true
|
|
318
|
+
if (log) {
|
|
319
|
+
console.log(chalk.yellow(`⏰ git 命令超时 (${timeout}ms): ${head}`))
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// 将 stdout 和 stderr 附加到 error 对象,以便上层可以获取完整输出
|
|
323
|
+
error.stdout = stdout
|
|
324
|
+
error.stderr = stderr
|
|
325
|
+
// ignoreError=true:命令预期可能失败(如 git rev-parse @{u} 查无 upstream 时 git 报
|
|
326
|
+
// "fatal: no upstream configured")。此时不当错误抛,resolve {stdout, stderr, error}
|
|
327
|
+
// 让上层用 stdout 为空自行降级,而不是把整个接口 500 掉。
|
|
328
|
+
if (options.ignoreError) {
|
|
329
|
+
resolve({ stdout: stdout || '', stderr: stderr || '', error })
|
|
330
|
+
return
|
|
331
|
+
}
|
|
332
|
+
log && coloredLog(head, error, 'error')
|
|
333
|
+
// 错误情况也打印目录和时间
|
|
334
|
+
if (log) {
|
|
335
|
+
const currentTime = new Date().toLocaleString('zh-CN', { hour12: false });
|
|
336
|
+
console.log(chalk.dim(`📁 目录: ${cwd} | ⏰ 时间: ${currentTime}`));
|
|
337
|
+
}
|
|
338
|
+
reject(error)
|
|
339
|
+
return
|
|
340
|
+
}
|
|
341
|
+
resolve({
|
|
342
|
+
stdout,
|
|
343
|
+
stderr
|
|
344
|
+
})
|
|
345
|
+
})
|
|
346
|
+
// }, 1000)
|
|
347
|
+
})
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* 检查并尝试清理 Git 锁文件
|
|
352
|
+
*
|
|
353
|
+
* 安全策略:必须先验证锁是否真的过期(孤儿锁),才能 unlink。盲目删锁
|
|
354
|
+
* 会删掉别人正在写的锁 → `.git/index` 半写状态 → 仓库损坏。
|
|
355
|
+
*
|
|
356
|
+
* 判定顺序:
|
|
357
|
+
* 1. 读 .git/index.lock 内容,若能解析为 PID,调 process.kill(pid, 0) 探测
|
|
358
|
+
* - 探测成功(无错)→ 进程存活,锁仍被持有 → 跳过清理
|
|
359
|
+
* - ESRCH(无此进程)→ 锁是孤儿 → 进入清理
|
|
360
|
+
* - EPERM(进程存在但无权限)→ 锁被别人持有且我们是其他用户 → 跳过
|
|
361
|
+
* 2. 锁文件内容不是 PID(原生 git 通过 O_CREAT|O_EXCL 创建空文件作锁,
|
|
362
|
+
* 内容几乎总为空;此分支为兼容场景与未知第三方工具的 mtime 兜底):
|
|
363
|
+
* - force=false(默认):超过 5 分钟未更新视为陈旧,否则保守跳过
|
|
364
|
+
* - force=true:绕过 mtime 判断直接视为陈旧(用于破坏性入口,
|
|
365
|
+
* 如 reset --hard / discard-all-changes — 用户主动触发,
|
|
366
|
+
* 不该被一个刚崩出来的新孤儿锁挡住)
|
|
367
|
+
*
|
|
368
|
+
* 注:force=true 仅绕过 mtime,**仍走 PID liveness 校验**——
|
|
369
|
+
* 若锁由活跃进程持有(无论新旧)一律跳过,确保不会误删活跃锁。
|
|
370
|
+
*
|
|
371
|
+
* 注:阈值取 5min 而非 60s,是为大仓库的 git add -A / stash / gc 等长操作
|
|
372
|
+
* 留出空间 — 60s 阈值下用户在手抖触发清理时可能误删活跃锁,
|
|
373
|
+
* 导致 .git/index 半写损坏。5min 是 GitHub Desktop / Sourcetree 等
|
|
374
|
+
* GUI 客户端常用的陈旧判定阈值。
|
|
375
|
+
*
|
|
376
|
+
* @param {Object} [options]
|
|
377
|
+
* @param {boolean} [options.force=false] 是否绕过 mtime 兜底判断(破坏性入口用)。
|
|
378
|
+
* 注意:force 不影响 PID liveness 校验——活跃进程持有的锁永远不会被删。
|
|
379
|
+
* @returns {Promise<boolean>} 是否清理成功
|
|
380
|
+
*/
|
|
381
|
+
async function checkAndClearGitLock({ force = false } = {}) {
|
|
382
|
+
// 锁文件"陈旧"阈值(毫秒):5min 是经验值。
|
|
383
|
+
// - 大仓库 git add -A / stash / gc 单次可能 > 60s,60s 阈值会误删活跃锁;
|
|
384
|
+
// - 5min 与 GitHub Desktop / Sourcetree 等 GUI 客户端的陈旧判定一致;
|
|
385
|
+
// - 只有真正崩溃/被 kill -9 留下的孤儿锁才会存在这么久。
|
|
386
|
+
// - force=true 时此阈值被绕过,但仍受 PID liveness 校验保护。
|
|
387
|
+
const STALE_LOCK_THRESHOLD_MS = 5 * 60_000;
|
|
388
|
+
try {
|
|
389
|
+
const cwd = getCwd();
|
|
390
|
+
let gitRoot;
|
|
391
|
+
try {
|
|
392
|
+
// 使用 execSync 快速获取 Git 根目录,加 5s 超时避免 rev-parse 卡死
|
|
393
|
+
const rootOutput = execSync('git rev-parse --show-toplevel', {
|
|
394
|
+
cwd,
|
|
395
|
+
encoding: 'utf-8',
|
|
396
|
+
timeout: 5000
|
|
397
|
+
});
|
|
398
|
+
gitRoot = rootOutput.trim();
|
|
399
|
+
} catch (e) {
|
|
400
|
+
gitRoot = cwd;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const lockFilePath = path.join(gitRoot, '.git', 'index.lock');
|
|
404
|
+
|
|
405
|
+
// 一次 IO 替代原来的 fs.access + fs.unlink 两步
|
|
406
|
+
let content;
|
|
407
|
+
try {
|
|
408
|
+
content = await fs.readFile(lockFilePath, 'utf-8');
|
|
409
|
+
} catch (e) {
|
|
410
|
+
if (e.code === 'ENOENT') return false; // 锁文件不存在,无需清理
|
|
411
|
+
throw e;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// 判定锁是否过期
|
|
415
|
+
const trimmed = (content || '').trim();
|
|
416
|
+
// 原生 git 通过 O_CREAT|O_EXCL 创建空文件作锁,内容几乎总为空,
|
|
417
|
+
// 不会往 .git/index.lock 写 PID。此 PID 分支为兼容某些会写 PID 的
|
|
418
|
+
// 第三方工具(如部分 GUI 客户端 / 自定义钩子),并非常规路径。
|
|
419
|
+
const isPidFormat = /^\d+$/.test(trimmed);
|
|
420
|
+
let isStale = false;
|
|
421
|
+
|
|
422
|
+
if (isPidFormat) {
|
|
423
|
+
const pid = parseInt(trimmed, 10);
|
|
424
|
+
if (Number.isInteger(pid) && pid > 0) {
|
|
425
|
+
try {
|
|
426
|
+
// signal 0 不真发信号,只探测进程是否存在
|
|
427
|
+
process.kill(pid, 0);
|
|
428
|
+
// 无错 → 进程存活,锁还活着
|
|
429
|
+
console.log(chalk.yellow(`⚠️ 检测到 Git 锁由活跃进程持有 (PID=${pid}),跳过清理`));
|
|
430
|
+
return false;
|
|
431
|
+
} catch (e) {
|
|
432
|
+
if (e.code === 'ESRCH') {
|
|
433
|
+
// 进程已死,锁是孤儿
|
|
434
|
+
isStale = true;
|
|
435
|
+
} else if (e.code === 'EPERM') {
|
|
436
|
+
// 进程存在但无权限(其他用户的进程),不能删
|
|
437
|
+
console.log(chalk.yellow(`⚠️ 检测到 Git 锁由无权限访问的进程持有,跳过清理`));
|
|
438
|
+
return false;
|
|
439
|
+
} else {
|
|
440
|
+
// 其他探测错误,保守不删
|
|
441
|
+
console.warn(chalk.yellow(`⚠️ 探测 PID liveness 失败: ${e.message},跳过清理`));
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
} else {
|
|
447
|
+
// 锁文件内容不是 PID — 原生 git 用 O_CREAT|O_EXCL 创建空文件,
|
|
448
|
+
// 内容为空,用 mtime 兜底
|
|
449
|
+
if (force) {
|
|
450
|
+
// 破坏性入口(reset --hard / discard-all-changes 等):用户主动
|
|
451
|
+
// 触发,不应被刚崩出来的新孤儿锁挡住。PID liveness 已在上面通过
|
|
452
|
+
// (走到这里说明锁不是 PID 格式或 PID 已死),直接视为陈旧清理。
|
|
453
|
+
// 仍打印 mtime 便于排查。
|
|
454
|
+
try {
|
|
455
|
+
const stat = await fs.stat(lockFilePath);
|
|
456
|
+
const ageMs = Date.now() - stat.mtimeMs;
|
|
457
|
+
console.log(chalk.gray(`ℹ️ force=true,绕过 mtime 判断清理 Git 锁(年龄 ${Math.round(ageMs / 1000)}s)`));
|
|
458
|
+
} catch (_) {}
|
|
459
|
+
isStale = true;
|
|
460
|
+
} else {
|
|
461
|
+
try {
|
|
462
|
+
const stat = await fs.stat(lockFilePath);
|
|
463
|
+
const ageMs = Date.now() - stat.mtimeMs;
|
|
464
|
+
if (ageMs > STALE_LOCK_THRESHOLD_MS) {
|
|
465
|
+
isStale = true;
|
|
466
|
+
} else {
|
|
467
|
+
console.log(chalk.yellow(`⚠️ Git 锁文件较新(${Math.round(ageMs / 1000)}s),保守跳过清理`));
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
} catch (_) {
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (!isStale) return false;
|
|
477
|
+
|
|
478
|
+
// 锁是孤儿,清理
|
|
479
|
+
await fs.unlink(lockFilePath);
|
|
480
|
+
console.log(chalk.green(`✅ 已清理过期 Git 锁文件: ${lockFilePath}`));
|
|
481
|
+
return true;
|
|
482
|
+
} catch (error) {
|
|
483
|
+
console.error(chalk.red('清理 Git 锁文件失败:'), error.message);
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Function to get command history
|
|
489
|
+
// Cached cwd — 进程内 getCwd() 只算一次,见下方 getCwd() 实现
|
|
490
|
+
let _cachedCwd = null
|
|
491
|
+
|
|
492
|
+
function getCommandHistory() {
|
|
493
|
+
return [...commandHistory];
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Function to manually add command to history (for commands not using execGitCommand)
|
|
497
|
+
function addCommandToHistory(command, stdout = '', stderr = '', error = null, executionTime = 0) {
|
|
498
|
+
// 防御:历史里的 command 必须始终是字符串,防止前端 item.command.trim() 抛错
|
|
499
|
+
if (Array.isArray(command)) {
|
|
500
|
+
command = command.join(' ')
|
|
501
|
+
} else if (typeof command !== 'string') {
|
|
502
|
+
command = String(command ?? '')
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Truncate outputs if too long
|
|
506
|
+
const isStdoutTruncated = stdout.length > MAX_OUTPUT_LENGTH;
|
|
507
|
+
const isStderrTruncated = stderr.length > MAX_OUTPUT_LENGTH;
|
|
508
|
+
const truncatedStdout = isStdoutTruncated ? truncateForHistory(stdout, MAX_OUTPUT_LENGTH, TRUNCATED_SUFFIX_MANUAL) : stdout;
|
|
509
|
+
const truncatedStderr = isStderrTruncated ? truncateForHistory(stderr, MAX_OUTPUT_LENGTH, TRUNCATED_SUFFIX_MANUAL) : stderr;
|
|
510
|
+
|
|
511
|
+
const historyItem = {
|
|
512
|
+
command,
|
|
513
|
+
stdout: truncatedStdout || '',
|
|
514
|
+
stderr: truncatedStderr || '',
|
|
515
|
+
error: error ? (typeof error === 'string' ? error : error.message) : null,
|
|
516
|
+
executionTime,
|
|
517
|
+
timestamp: new Date().toISOString(),
|
|
518
|
+
success: !error,
|
|
519
|
+
isStdoutTruncated,
|
|
520
|
+
isStderrTruncated
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
// Add to history (limited size)
|
|
524
|
+
commandHistory.unshift(historyItem);
|
|
525
|
+
if (commandHistory.length > MAX_HISTORY_SIZE) {
|
|
526
|
+
commandHistory.pop();
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Broadcast via WebSocket if available
|
|
530
|
+
if (ioInstance) {
|
|
531
|
+
ioInstance.emit('command_history_update', {
|
|
532
|
+
newCommand: historyItem,
|
|
533
|
+
fullHistory: commandHistory.slice(0, 10)
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
return historyItem;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const getCwd = () => {
|
|
541
|
+
// process.argv 在进程内不变(CLI 启动后用户不会去改 process.argv),缓存一次
|
|
542
|
+
// 即可。execGitCommand 在一次 g 流程里会被调 5-15 次(每个 git 子命令一次),
|
|
543
|
+
// 缓存省掉重复的 Array.find + startsWith 扫描,在 GUI 后端常驻进程里累计收益更大。
|
|
544
|
+
if (_cachedCwd === null) {
|
|
545
|
+
const parsed = parseCwdArg(process.argv)
|
|
546
|
+
_cachedCwd = parsed || process.cwd()
|
|
547
|
+
}
|
|
548
|
+
return _cachedCwd
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* 主动让 getCwd 缓存失效。CLI 内部不会调;供 fs.js /api/change_directory
|
|
553
|
+
* 之类的"cwd 改变"路径使用,避免后续 execGitCommand 仍然用旧 cwd 跑子命令。
|
|
554
|
+
* 注意:CLI 子进程通常不需要这个,只有 GUI 后端在 chdir 后需要手动 clear。
|
|
555
|
+
*/
|
|
556
|
+
export function invalidateCwdCache() {
|
|
557
|
+
_cachedCwd = null
|
|
558
|
+
}
|
|
559
|
+
const judgePlatform = () => {
|
|
560
|
+
// 判断是否是 Windows 系统
|
|
561
|
+
if (os.platform() === 'win32') {
|
|
562
|
+
try {
|
|
563
|
+
// 设置终端字符编码为 UTF-8 — 这条必须 sync,后续 console 输出依赖
|
|
564
|
+
// 新的 code page,异步的话早期 console.log 会乱码。
|
|
565
|
+
execSync('chcp 65001');
|
|
566
|
+
} catch (e) {
|
|
567
|
+
console.error('设置字符编码失败:', e.message);
|
|
568
|
+
}
|
|
569
|
+
// git config --global 不影响启动期 console 输出,丢到后台跑,避免
|
|
570
|
+
// 两次额外的同步子进程阻塞 Node 启动(Windows 上各 ~30-80ms)
|
|
571
|
+
setImmediate(() => {
|
|
572
|
+
try {
|
|
573
|
+
execSync('git config --global core.autocrlf true');
|
|
574
|
+
execSync('git config --global core.quotepath false');
|
|
575
|
+
} catch (e) {
|
|
576
|
+
// 后台配置失败不致命,首次 commit 时用户会发现并自己跑命令
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
} else {
|
|
580
|
+
// 非 Windows:同样丢到后台
|
|
581
|
+
setImmediate(() => {
|
|
582
|
+
try {
|
|
583
|
+
execSync('git config --global core.autocrlf input');
|
|
584
|
+
} catch (_) {}
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
const showHelp = () => {
|
|
589
|
+
const helpMessage = `
|
|
590
|
+
Usage: g [options]
|
|
591
|
+
|
|
592
|
+
Options:
|
|
593
|
+
-h, --help Show this help message
|
|
594
|
+
--set-default-message=<msg> Set default commit message
|
|
595
|
+
get-config Show current configuration
|
|
596
|
+
-y Auto commit with default message
|
|
597
|
+
--ai AI generate commit message, commit and push
|
|
598
|
+
-m <message> Commit message (use quotes if message contains spaces)
|
|
599
|
+
-m=<message> Commit message (use this form without spaces around '=')
|
|
600
|
+
--path=<path> Set custom working directory
|
|
601
|
+
--cwd=<path> Set custom working directory
|
|
602
|
+
--interval=<seconds> Set interval time for automatic commits (in seconds)
|
|
603
|
+
log Show git commit logs
|
|
604
|
+
--n=<number> Number of commits to show with --log
|
|
605
|
+
--no-diff Skip displaying git diff
|
|
606
|
+
addScript Add "g:y": "g -y" to package.json scripts
|
|
607
|
+
addResetScript Add "g:reset": "git reset --hard origin/<current-branch>" to package.json scripts
|
|
608
|
+
ui Launch graphical user interface (v2.0.0)
|
|
609
|
+
|
|
610
|
+
File Locking:
|
|
611
|
+
--lock-file=<path> Lock a file to exclude it from commits
|
|
612
|
+
--unlock-file=<path> Unlock a previously locked file
|
|
613
|
+
--list-locked List all currently locked files
|
|
614
|
+
--check-lock=<path> Check if a file is locked
|
|
615
|
+
|
|
616
|
+
--cmd="your-cmd" Execute custom cmd command (immediately, at a time, or periodically)
|
|
617
|
+
--cmd-interval=<seconds> Execute custom cmd every N seconds
|
|
618
|
+
--at="HH:MM" Execute custom cmd at a specific time (today) or --at="YYYY-MM-DD HH:MM:SS"
|
|
619
|
+
--daily Repeat with --at every day at the same time
|
|
620
|
+
|
|
621
|
+
Example:
|
|
622
|
+
g --cmd="echo hello" --cmd-interval=5 # 每5秒执行一次echo hello
|
|
623
|
+
g --cmd="echo at-time" --at=23:59 # 在23:59执行一次echo at-time
|
|
624
|
+
g --cmd="echo daily" --at=23:59 --daily # 每天23:59执行一次echo daily
|
|
625
|
+
g --cmd="echo now" # 立即执行一次echo now
|
|
626
|
+
g --cmd="echo hi" --cmd-interval=10 --interval=60 # cmd和git自动提交并行
|
|
627
|
+
g -m "Initial commit" Commit with a custom message
|
|
628
|
+
g -m=Fix-bug Commit with a custom message (no spaces around '=')
|
|
629
|
+
g -y Auto commit with the default message
|
|
630
|
+
g --ai AI generate commit message, commit and push
|
|
631
|
+
g --ai --no-diff AI commit without displaying diff
|
|
632
|
+
g --ai --interval=600 AI commit every 10 minutes (600 seconds)
|
|
633
|
+
g -y --interval=600 Commit every 10 minutes (600 seconds)
|
|
634
|
+
g --path=/path/to/repo Specify a custom working directory
|
|
635
|
+
g log Show recent commit logs
|
|
636
|
+
g log --n=5 Show the last 5 commits with --log
|
|
637
|
+
g addScript Add auto commit script to package.json
|
|
638
|
+
g addResetScript Add reset script to package.json
|
|
639
|
+
g --lock-file=config.json Lock config.json file
|
|
640
|
+
g --unlock-file=config.json Unlock config.json file
|
|
641
|
+
g --list-locked List all locked files
|
|
642
|
+
|
|
643
|
+
Add auto submit in package.json:
|
|
644
|
+
"scripts": {
|
|
645
|
+
"g:y": "g -y",
|
|
646
|
+
"g:reset": "git reset --hard origin/<current-branch>"
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
Run in the background across platforms:
|
|
650
|
+
Windows:
|
|
651
|
+
start /min cmd /k "g -y --path=your-folder --interval=600"
|
|
652
|
+
|
|
653
|
+
Linux/macOS:
|
|
654
|
+
nohup g -y --path=your-folder --interval=600 > git-autocommit.log 2>&1 &
|
|
655
|
+
|
|
656
|
+
Start GUI interface:
|
|
657
|
+
g ui
|
|
658
|
+
|
|
659
|
+
Stop all monitoring processes:
|
|
660
|
+
Windows: Terminate the Node.js process in the Task Manager.
|
|
661
|
+
Linux/macOS:
|
|
662
|
+
pkill -f "g -y" # Terminate all auto-commit processes
|
|
663
|
+
ps aux | grep "g -y" # Find the specific process ID
|
|
664
|
+
kill [PID] # Terminate the specified process
|
|
665
|
+
`;
|
|
666
|
+
|
|
667
|
+
console.log(helpMessage);
|
|
668
|
+
process.exit();
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
function judgeLog() {
|
|
672
|
+
const logArg = process.argv.find(arg => arg === 'log');
|
|
673
|
+
if (logArg) {
|
|
674
|
+
printGitLog(); // 如果有 log 参数,打印 Git 提交记录
|
|
675
|
+
// 打印完成后退出
|
|
676
|
+
process.exit();
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function judgeHelp() {
|
|
681
|
+
if (process.argv.includes('-h') || process.argv.includes('--help')) {
|
|
682
|
+
showHelp();
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
async function printGitLog() {
|
|
687
|
+
let n = 20;
|
|
688
|
+
let logArg = process.argv.find(arg => arg.startsWith('--n='));
|
|
689
|
+
if (logArg) {
|
|
690
|
+
n = parseInt(logArg.split('=')[1], 10);
|
|
691
|
+
}
|
|
692
|
+
// 使用 ASCII 记录分隔符 %x1E 作为字段分隔符
|
|
693
|
+
const logCommand = ['log', '-n', n, '--pretty=format:%C(green)%h%C(reset) %x1E %C(cyan)%an%C(reset) %x1E %C(yellow)%ad%C(reset) %x1E %C(blue)%D%C(reset) %x1E %C(magenta)%s%C(reset)', '--date=format:%Y-%m-%d %H:%M', '--graph', '--decorate', '--color']
|
|
694
|
+
try {
|
|
695
|
+
const logOutput = await execGitCommand(logCommand, {
|
|
696
|
+
head: `git log`
|
|
697
|
+
});
|
|
698
|
+
} catch (error) {
|
|
699
|
+
console.error('无法获取 Git 提交记录:', error.message);
|
|
700
|
+
}
|
|
701
|
+
// 打印完成后退出
|
|
702
|
+
process.exit();
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
function exec_exit(exit) {
|
|
706
|
+
// 仅当显式传 true 时退出;空值/0/'false'/'no'/其他非真值都按"不退出"处理。
|
|
707
|
+
// 之前的 truthy 判断会把字符串 'false' 误判为 true(非空字符串都是 truthy),
|
|
708
|
+
// 调用方传 '--exit=false' 等解析出来的字符串时会错误退出。
|
|
709
|
+
if (exit === true) {
|
|
710
|
+
process.exit()
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function judgeUnmerged(statusOutput) {
|
|
715
|
+
const hasUnmerged = statusOutput.includes('You have unmerged paths');
|
|
716
|
+
if (hasUnmerged) {
|
|
717
|
+
errorLog('错误', '存在未合并的文件,请先解决冲突')
|
|
718
|
+
process.exit(1);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
async function exec_push({exit, commitMessage}) {
|
|
723
|
+
// 执行 git push
|
|
724
|
+
const spinner = ora('正在推送代码...').start();
|
|
725
|
+
try {
|
|
726
|
+
const {stdout, stderr} = await execGitCommand(['push'], {
|
|
727
|
+
spinner
|
|
728
|
+
});
|
|
729
|
+
await printCommitLog({commitMessage});
|
|
730
|
+
return {stdout, stderr};
|
|
731
|
+
} catch (error) {
|
|
732
|
+
throw error;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
async function printCommitLog({commitMessage}) {
|
|
737
|
+
try {
|
|
738
|
+
// 获取项目名称(取git仓库根目录名)
|
|
739
|
+
const projectRootResult = await execGitCommand(['rev-parse', '--show-toplevel'], {log: false});
|
|
740
|
+
const projectName = chalk.blueBright(path.basename(projectRootResult.stdout.trim()));
|
|
741
|
+
|
|
742
|
+
// 获取当前提交hash(取前7位)
|
|
743
|
+
const commitHashResult = await execGitCommand(['rev-parse', '--short', 'HEAD'], {log: false});
|
|
744
|
+
const hashDisplay = chalk.yellow(commitHashResult.stdout.trim());
|
|
745
|
+
|
|
746
|
+
// 获取分支信息
|
|
747
|
+
const branchResult = await execGitCommand(['branch', '--show-current'], {log: false});
|
|
748
|
+
const branchDisplay = chalk.magenta(branchResult.stdout.trim());
|
|
749
|
+
|
|
750
|
+
// 构建信息内容
|
|
751
|
+
const message = [
|
|
752
|
+
`${chalk.cyan.bold('Project:')} ${projectName}`,
|
|
753
|
+
`${chalk.cyan.bold('Commit:')} ${hashDisplay} ${chalk.dim('on')} ${branchDisplay}`,
|
|
754
|
+
`${chalk.cyan.bold('Message:')} ${chalk.reset(commitMessage)}`,
|
|
755
|
+
`${chalk.cyan.bold('Time:')} ${new Date().toLocaleString()}`
|
|
756
|
+
].join('\n');
|
|
757
|
+
|
|
758
|
+
// 使用boxen创建装饰框
|
|
759
|
+
const box = boxen(message, {
|
|
760
|
+
padding: 1,
|
|
761
|
+
margin: 1,
|
|
762
|
+
borderStyle: 'round',
|
|
763
|
+
borderColor: 'green',
|
|
764
|
+
title: chalk.bold.green('✅ COMMIT SUCCESS'),
|
|
765
|
+
titleAlignment: 'center',
|
|
766
|
+
float: 'left',
|
|
767
|
+
textAlignment: 'left'
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
console.log(box);
|
|
771
|
+
} catch (error) {
|
|
772
|
+
// 异常处理
|
|
773
|
+
const errorBox = boxen(chalk.red(`Failed to get commit details: ${error.message}`), {
|
|
774
|
+
borderColor: 'red',
|
|
775
|
+
padding: 1
|
|
776
|
+
});
|
|
777
|
+
console.log(errorBox);
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
async function execPull() {
|
|
782
|
+
try {
|
|
783
|
+
// 检查是否需要拉取更新
|
|
784
|
+
const spinner = ora('正在拉取代码...').start();
|
|
785
|
+
await execGitCommand(['pull'], {
|
|
786
|
+
spinner
|
|
787
|
+
})
|
|
788
|
+
} catch (e) {
|
|
789
|
+
console.log(chalk.yellow('⚠️ 拉取远程更新合并失败,可能存在冲突,请手动处理'));
|
|
790
|
+
throw Error(e)
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
async function judgeRemote() {
|
|
795
|
+
const spinner = ora('正在检查远程更新...').start();
|
|
796
|
+
try {
|
|
797
|
+
// 检查是否有远程更新
|
|
798
|
+
// 先获取远程最新状态
|
|
799
|
+
await execGitCommand(['remote', 'update'], {
|
|
800
|
+
head: 'Fetching remote updates',
|
|
801
|
+
log: false
|
|
802
|
+
});
|
|
803
|
+
// 检查是否需要 pull
|
|
804
|
+
const res = await execGitCommand(['rev-list', 'HEAD..@{u}', '--count'], {
|
|
805
|
+
head: 'Checking if behind remote',
|
|
806
|
+
log: false
|
|
807
|
+
});
|
|
808
|
+
const behindCount = res.stdout.trim()
|
|
809
|
+
const { green, black, bgGreen, white } = chalk;
|
|
810
|
+
// 如果本地落后于远程
|
|
811
|
+
if (parseInt(behindCount) > 0) {
|
|
812
|
+
try {
|
|
813
|
+
spinner.stop();
|
|
814
|
+
// const spinner_pull = ora('发现远程更新,正在拉取...').start();
|
|
815
|
+
await execPull()
|
|
816
|
+
|
|
817
|
+
// // 尝试使用 --ff-only 拉取更新
|
|
818
|
+
// const res = await execGitCommand('git pull --ff-only', {
|
|
819
|
+
// spinner: spinner_pull,
|
|
820
|
+
// head: 'Pulling updates'
|
|
821
|
+
// });
|
|
822
|
+
// bgGreen.white.bold(' SYNC ') +
|
|
823
|
+
// green` ➔ ` +
|
|
824
|
+
// chalk.blue.bold('远程仓库已同步') +
|
|
825
|
+
// green(' ✔')
|
|
826
|
+
// );
|
|
827
|
+
const message = '已成功同步远程更新'.split('').map((char, i) =>
|
|
828
|
+
chalk.rgb(0, 255 - i*10, 0)(char)
|
|
829
|
+
).join('');
|
|
830
|
+
|
|
831
|
+
console.log(chalk.bold(`✅ ${message}`));
|
|
832
|
+
} catch (pullError) {
|
|
833
|
+
// // 如果 --ff-only 拉取失败,尝试普通的 git pull
|
|
834
|
+
// await this.execPull()
|
|
835
|
+
throw new Error(pullError)
|
|
836
|
+
}
|
|
837
|
+
} else {
|
|
838
|
+
spinner.stop();
|
|
839
|
+
const message = '本地已是最新'.split('').map((char, i) =>
|
|
840
|
+
chalk.rgb(0, 255 - i*10, 0)(char)
|
|
841
|
+
).join('');
|
|
842
|
+
console.log(chalk.bold(`✅ ${message}`));
|
|
843
|
+
}
|
|
844
|
+
} catch (e) {
|
|
845
|
+
spinner.stop();
|
|
846
|
+
throw new Error(e)
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
async function execDiff() {
|
|
851
|
+
const no_diff = process.argv.find(arg => arg.startsWith('--no-diff'))
|
|
852
|
+
if (!no_diff) {
|
|
853
|
+
await execGitCommand(['diff', '--color=always'], {
|
|
854
|
+
head: `git diff`
|
|
855
|
+
})
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// 执行 git add 但排除锁定的文件
|
|
860
|
+
async function execGitAddWithLockFilter() {
|
|
861
|
+
try {
|
|
862
|
+
// 获取锁定的文件列表
|
|
863
|
+
const lockedFiles = await config.getLockedFiles();
|
|
864
|
+
|
|
865
|
+
if (lockedFiles.length === 0) {
|
|
866
|
+
// 如果没有锁定文件,直接执行 git add .
|
|
867
|
+
await execGitCommand(['add', '.']);
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// 获取Git工作目录根路径,确保路径匹配的准确性
|
|
872
|
+
let gitRoot;
|
|
873
|
+
try {
|
|
874
|
+
const gitRootResult = await execGitCommand(['rev-parse', '--show-toplevel'], {log: false});
|
|
875
|
+
gitRoot = path.normalize(gitRootResult.stdout.trim());
|
|
876
|
+
} catch (error) {
|
|
877
|
+
console.warn(chalk.yellow('⚠️ 无法获取Git根目录,使用当前工作目录'));
|
|
878
|
+
gitRoot = path.normalize(process.cwd());
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// 获取所有修改的文件(包括未跟踪文件)
|
|
882
|
+
const statusResult = await execGitCommand(['status', '--porcelain', '--untracked-files=all'], {log: false});
|
|
883
|
+
const modifiedFiles = statusResult.stdout
|
|
884
|
+
.split('\n')
|
|
885
|
+
.filter(line => line.trim())
|
|
886
|
+
.map(line => {
|
|
887
|
+
// 解析 git status --porcelain 的输出格式
|
|
888
|
+
// 格式: XY filename 或 XY "filename with spaces"
|
|
889
|
+
const match = line.match(/^..\s+(.+)$/);
|
|
890
|
+
if (match) {
|
|
891
|
+
let filename = match[1];
|
|
892
|
+
// 如果文件名被引号包围,去掉引号
|
|
893
|
+
if (filename.startsWith('"') && filename.endsWith('"')) {
|
|
894
|
+
filename = filename.slice(1, -1);
|
|
895
|
+
// 处理转义字符
|
|
896
|
+
filename = filename.replace(/\\(.)/g, '$1');
|
|
897
|
+
}
|
|
898
|
+
return filename;
|
|
899
|
+
}
|
|
900
|
+
return null;
|
|
901
|
+
})
|
|
902
|
+
.filter(Boolean);
|
|
903
|
+
|
|
904
|
+
// 过滤掉锁定的文件,使用更严格的路径匹配逻辑
|
|
905
|
+
const filesToAdd = modifiedFiles.filter(file => {
|
|
906
|
+
// Git status 返回的是相对于Git根目录的路径
|
|
907
|
+
const gitRelativeFile = path.normalize(file);
|
|
908
|
+
|
|
909
|
+
const isLocked = lockedFiles.some(lockedFile => {
|
|
910
|
+
// 处理锁定文件路径:可能是相对路径或绝对路径
|
|
911
|
+
let normalizedLocked;
|
|
912
|
+
if (path.isAbsolute(lockedFile)) {
|
|
913
|
+
// 绝对路径:转换为相对于Git根目录的路径
|
|
914
|
+
const absoluteLocked = path.normalize(lockedFile);
|
|
915
|
+
if (absoluteLocked.startsWith(gitRoot)) {
|
|
916
|
+
normalizedLocked = path.relative(gitRoot, absoluteLocked);
|
|
917
|
+
} else {
|
|
918
|
+
// 锁定文件不在当前Git仓库中,跳过
|
|
919
|
+
return false;
|
|
920
|
+
}
|
|
921
|
+
} else {
|
|
922
|
+
// 相对路径:直接使用
|
|
923
|
+
normalizedLocked = path.normalize(lockedFile);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// 统一路径分隔符(Windows兼容性)
|
|
927
|
+
const normalizedGitFile = gitRelativeFile.replace(/\\/g, '/');
|
|
928
|
+
const normalizedLockedFile = normalizedLocked.replace(/\\/g, '/');
|
|
929
|
+
|
|
930
|
+
// 精确匹配或目录匹配(双向检查)
|
|
931
|
+
const isExactMatch = normalizedGitFile === normalizedLockedFile;
|
|
932
|
+
const isFileInLockedDir = normalizedGitFile.startsWith(normalizedLockedFile + '/');
|
|
933
|
+
const isLockedFileInDir = normalizedLockedFile.startsWith(normalizedGitFile + '/');
|
|
934
|
+
|
|
935
|
+
return isExactMatch || isFileInLockedDir || isLockedFileInDir;
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
// 额外检查:如果是目录路径,检查该目录下是否有任何未锁定的文件
|
|
939
|
+
if (!isLocked && file.endsWith('/')) {
|
|
940
|
+
// 这是一个目录路径,检查是否该目录下所有文件都被锁定
|
|
941
|
+
const dirPath = file.slice(0, -1); // 移除末尾的 '/'
|
|
942
|
+
const hasUnlockedFilesInDir = modifiedFiles.some(otherFile => {
|
|
943
|
+
if (otherFile === file) return false; // 跳过目录本身
|
|
944
|
+
|
|
945
|
+
const normalizedOtherFile = path.normalize(otherFile).replace(/\\/g, '/');
|
|
946
|
+
const normalizedDirPath = dirPath.replace(/\\/g, '/');
|
|
947
|
+
|
|
948
|
+
// 检查文件是否在这个目录下
|
|
949
|
+
if (normalizedOtherFile.startsWith(normalizedDirPath + '/')) {
|
|
950
|
+
// 检查这个文件是否被锁定
|
|
951
|
+
const isOtherFileLocked = lockedFiles.some(lockedFile => {
|
|
952
|
+
let normalizedLocked;
|
|
953
|
+
if (path.isAbsolute(lockedFile)) {
|
|
954
|
+
const absoluteLocked = path.normalize(lockedFile);
|
|
955
|
+
if (absoluteLocked.startsWith(gitRoot)) {
|
|
956
|
+
normalizedLocked = path.relative(gitRoot, absoluteLocked);
|
|
957
|
+
} else {
|
|
958
|
+
return false;
|
|
959
|
+
}
|
|
960
|
+
} else {
|
|
961
|
+
normalizedLocked = path.normalize(lockedFile);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
const normalizedLockedFile = normalizedLocked.replace(/\\/g, '/');
|
|
965
|
+
return normalizedOtherFile === normalizedLockedFile ||
|
|
966
|
+
normalizedOtherFile.startsWith(normalizedLockedFile + '/') ||
|
|
967
|
+
normalizedLockedFile.startsWith(normalizedOtherFile + '/');
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
return !isOtherFileLocked; // 如果文件未锁定,返回 true
|
|
971
|
+
}
|
|
972
|
+
return false;
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
// 如果目录下没有未锁定的文件,则跳过这个目录
|
|
976
|
+
if (!hasUnlockedFilesInDir) {
|
|
977
|
+
console.log(chalk.yellow(`🔒 跳过目录(所有文件都被锁定): ${file}`));
|
|
978
|
+
return false;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
if (isLocked) {
|
|
983
|
+
console.log(chalk.yellow(`🔒 跳过锁定文件: ${file}`));
|
|
984
|
+
return false;
|
|
985
|
+
}
|
|
986
|
+
return true;
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
if (filesToAdd.length === 0) {
|
|
990
|
+
console.log(chalk.blue('📝 所有修改的文件都被锁定,没有文件需要添加'));
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// 逐个添加未锁定的文件
|
|
995
|
+
// 用 execFile('git', argv) 直接传 argv 数组,不经过 shell,
|
|
996
|
+
// 文件名含空格/特殊字符时不需要 shellQuote;同时跨 Windows cmd.exe /
|
|
997
|
+
// POSIX sh 都一致(此前单引号写法在 Windows cmd.exe 不被识别,导致
|
|
998
|
+
// "pathspec '...did not match any files" 报错,见 da01bcb 回归)。
|
|
999
|
+
let successCount = 0
|
|
1000
|
+
let failedFiles = []
|
|
1001
|
+
for (const file of filesToAdd) {
|
|
1002
|
+
try {
|
|
1003
|
+
await execGitCommand(['add', '--', file], {
|
|
1004
|
+
head: `git add ${file}`,
|
|
1005
|
+
log: false
|
|
1006
|
+
})
|
|
1007
|
+
successCount++
|
|
1008
|
+
} catch (err) {
|
|
1009
|
+
// 单文件失败不阻断整批,记录失败清单让用户感知
|
|
1010
|
+
failedFiles.push({ file, error: err?.message || String(err) })
|
|
1011
|
+
console.warn(chalk.yellow(`⚠️ 添加失败: ${file} — ${err?.message || err}`))
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
const skippedCount = modifiedFiles.length - filesToAdd.length;
|
|
1016
|
+
const failSuffix = failedFiles.length > 0 ? `, ${failedFiles.length} 个失败` : ''
|
|
1017
|
+
console.log(chalk.green(`✅ 已添加 ${successCount} 个文件到暂存区${skippedCount > 0 ? ` (跳过 ${skippedCount} 个锁定文件)` : ''}${failSuffix}`));
|
|
1018
|
+
if (failedFiles.length > 0) {
|
|
1019
|
+
// 把失败列表挂到 thrown error 上,上层可选择性展示
|
|
1020
|
+
const e = new Error(`git add 部分失败 (${failedFiles.length}/${filesToAdd.length})`)
|
|
1021
|
+
e.failedFiles = failedFiles
|
|
1022
|
+
throw e
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
} catch (error) {
|
|
1026
|
+
console.error(chalk.red('执行 git add 时出错:'), error.message);
|
|
1027
|
+
throw error;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
async function execAddAndCommit({statusOutput, commitMessage, exit}) {
|
|
1032
|
+
// 检查 -m 参数(提交信息)
|
|
1033
|
+
const commitMessageArg = process.argv.find(arg => arg.startsWith('-m'));
|
|
1034
|
+
if (commitMessageArg) {
|
|
1035
|
+
if (commitMessageArg.includes('=')) {
|
|
1036
|
+
// 处理 -m=<message> 的情况
|
|
1037
|
+
commitMessage = commitMessageArg.split('=')[1]?.replace(/^['"]|['"]$/g, '');
|
|
1038
|
+
} else {
|
|
1039
|
+
// 处理 -m <message> 的情况
|
|
1040
|
+
const index = process.argv.indexOf(commitMessageArg);
|
|
1041
|
+
if (index !== -1 && process.argv[index + 1]) {
|
|
1042
|
+
commitMessage = process.argv[index + 1]?.replace(/^['"]|['"]$/g, '');
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// 检查命令行参数,判断是否有 -y 或 --ai 参数(都跳过用户输入提示)
|
|
1048
|
+
const autoCommit = process.argv.includes('-y') || process.argv.includes('--ai');
|
|
1049
|
+
|
|
1050
|
+
if (!autoCommit && !commitMessageArg) {
|
|
1051
|
+
// 如果没有 -y 参数,则等待用户输入提交信息
|
|
1052
|
+
const rl = readline.createInterface({
|
|
1053
|
+
input: process.stdin,
|
|
1054
|
+
output: process.stdout
|
|
1055
|
+
})
|
|
1056
|
+
|
|
1057
|
+
function rlPromisify(fn) {
|
|
1058
|
+
return async (...args) => {
|
|
1059
|
+
return new Promise((resolve, reject) => fn(...args, resolve, reject))
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
const question = rlPromisify(rl.question.bind(rl))
|
|
1064
|
+
commitMessage = await question('请输入提交信息:') || commitMessage;
|
|
1065
|
+
rl.close(); // 关闭 readline 接口
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// 使用带锁定文件过滤的 git add
|
|
1069
|
+
if (statusOutput.includes('(use "git add')) {
|
|
1070
|
+
await execGitAddWithLockFilter();
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// 提交前二次校验(包括未跟踪文件)
|
|
1074
|
+
const checkStatus = await execGitCommand(['status', '--porcelain', '--untracked-files=all'], {log: false});
|
|
1075
|
+
if (!checkStatus.stdout.trim()) {
|
|
1076
|
+
console.log(chalk.yellow('⚠️ 没有检测到可提交的变更'));
|
|
1077
|
+
// exec_exit(exit)
|
|
1078
|
+
return commitMessage; // 返回提交信息(即使没有提交)
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// 执行 git commit
|
|
1082
|
+
if (statusOutput.includes('Untracked files:') || statusOutput.includes('Changes not staged for commit') || statusOutput.includes('Changes to be committed')) {
|
|
1083
|
+
await execGitCommand(['commit', '-m', commitMessage])
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// 返回实际使用的提交信息
|
|
1087
|
+
return commitMessage;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// 添加时间格式化函数
|
|
1091
|
+
async function addScriptToPackageJson() {
|
|
1092
|
+
try {
|
|
1093
|
+
// 读取当前目录的 package.json
|
|
1094
|
+
const packagePath = path.join(process.cwd(), 'package.json');
|
|
1095
|
+
const packageJson = JSON.parse(await fs.readFile(packagePath, 'utf8'));
|
|
1096
|
+
|
|
1097
|
+
// 确保有 scripts 部分
|
|
1098
|
+
if (!packageJson.scripts) {
|
|
1099
|
+
packageJson.scripts = {};
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// 添加 g:y 命令
|
|
1103
|
+
if (!packageJson.scripts['g:y']) {
|
|
1104
|
+
packageJson.scripts['g:y'] = 'g -y';
|
|
1105
|
+
// 写回文件
|
|
1106
|
+
await fs.writeFile(packagePath, JSON.stringify(packageJson, null, 2));
|
|
1107
|
+
console.log(chalk.green('✓ 成功添加 g:y 脚本到 package.json'));
|
|
1108
|
+
} else {
|
|
1109
|
+
console.log(chalk.yellow('⚠️ g:y 脚本已存在'));
|
|
1110
|
+
}
|
|
1111
|
+
} catch (error) {
|
|
1112
|
+
if (error.code === 'ENOENT') {
|
|
1113
|
+
console.error(chalk.red('❌ 当前目录下未找到 package.json 文件'));
|
|
1114
|
+
} else {
|
|
1115
|
+
console.error(chalk.red('❌ 添加脚本失败:'), error.message);
|
|
1116
|
+
}
|
|
1117
|
+
process.exit(1);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
async function addResetScriptToPackageJson() {
|
|
1122
|
+
try {
|
|
1123
|
+
// 读取当前目录的 package.json
|
|
1124
|
+
const packagePath = path.join(process.cwd(), 'package.json');
|
|
1125
|
+
const packageJson = JSON.parse(await fs.readFile(packagePath, 'utf8'));
|
|
1126
|
+
|
|
1127
|
+
// 确保有 scripts 部分
|
|
1128
|
+
if (!packageJson.scripts) {
|
|
1129
|
+
packageJson.scripts = {};
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// 获取当前分支名
|
|
1133
|
+
const branchResult = await execGitCommand(['branch', '--show-current'], {log: false});
|
|
1134
|
+
const branch = branchResult.stdout.trim();
|
|
1135
|
+
|
|
1136
|
+
// 添加 g:reset 命令
|
|
1137
|
+
if (!packageJson.scripts['g:reset']) {
|
|
1138
|
+
packageJson.scripts['g:reset'] = `git reset --hard origin/${branch}`;
|
|
1139
|
+
// 写回文件
|
|
1140
|
+
await fs.writeFile(packagePath, JSON.stringify(packageJson, null, 2));
|
|
1141
|
+
console.log(chalk.green(`✓ 成功添加 g:reset 脚本到 package.json (重置到 origin/${branch})`));
|
|
1142
|
+
} else {
|
|
1143
|
+
console.log(chalk.yellow('⚠️ g:reset 脚本已存在'));
|
|
1144
|
+
}
|
|
1145
|
+
} catch (error) {
|
|
1146
|
+
if (error.code === 'ENOENT') {
|
|
1147
|
+
console.error(chalk.red('❌ 当前目录下未找到 package.json 文件'));
|
|
1148
|
+
} else {
|
|
1149
|
+
console.error(chalk.red('❌ 添加脚本失败:'), error.message);
|
|
1150
|
+
}
|
|
1151
|
+
process.exit(1);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
export {
|
|
1156
|
+
coloredLog, errorLog, execSyncGitCommand,
|
|
1157
|
+
execGitCommand, getCommandHistory, addCommandToHistory, // Add command history exports
|
|
1158
|
+
clearCommandHistory,
|
|
1159
|
+
// parseCwdArg / truncateForHistory / formatDuration / delay 已在文件顶部从
|
|
1160
|
+
// ./format.js re-export,这里不再重复导出,避免 Duplicate export 错误。
|
|
1161
|
+
checkAndClearGitLock,
|
|
1162
|
+
registerSocketIO, // 导出注册Socket.io的函数
|
|
1163
|
+
getCwd, judgePlatform, showHelp, judgeLog, printGitLog,
|
|
1164
|
+
judgeHelp, exec_exit, judgeUnmerged,
|
|
1165
|
+
exec_push, execPull, judgeRemote, execDiff, execAddAndCommit,
|
|
1166
|
+
execGitAddWithLockFilter, // 导出新的 git add 函数
|
|
1167
|
+
addScriptToPackageJson, addResetScriptToPackageJson
|
|
1168
|
+
};
|