@zjex/git-workflow 0.5.1 → 0.5.3
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/CHANGELOG.md +10 -0
- package/README.md +1 -1
- package/dist/index.js +84 -62
- package/docs/commands/review.md +3 -0
- package/docs/guide/ai-review.md +6 -0
- package/package.json +1 -1
- package/src/commands/review.ts +59 -16
- package/src/update-notifier.ts +43 -55
- package/tests/update-notifier.test.ts +29 -77
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [v0.5.2](https://github.com/iamzjt-front-end/git-workflow/compare/v0.5.1...v0.5.2) (2026-02-06)
|
|
4
|
+
|
|
5
|
+
- 🔖 chore(release): 发布 v0.5.2 ([d2b659c](https://github.com/iamzjt-front-end/git-workflow/commit/d2b659c))
|
|
6
|
+
- refactor(update-notifier): Simplify version checking logic ([165e21f](https://github.com/iamzjt-front-end/git-workflow/commit/165e21f))
|
|
7
|
+
|
|
8
|
+
## [v0.5.1](https://github.com/iamzjt-front-end/git-workflow/compare/v0.5.0...v0.5.1) (2026-02-06)
|
|
9
|
+
|
|
10
|
+
- 🔖 chore(release): 发布 v0.5.1 ([88c2089](https://github.com/iamzjt-front-end/git-workflow/commit/88c2089))
|
|
11
|
+
- feat(release): 优化发布脚本,自动同步更新 CHANGELOG 和 README 版本号 ([89af235](https://github.com/iamzjt-front-end/git-workflow/commit/89af235))
|
|
12
|
+
|
|
3
13
|
## [v0.5.0](https://github.com/iamzjt-front-end/git-workflow/compare/v0.4.7...v0.5.0) (2026-02-06)
|
|
4
14
|
|
|
5
15
|
- 🔖 chore(release): 发布 v0.5.0 ([51c5740](https://github.com/iamzjt-front-end/git-workflow/commit/51c5740))
|
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<a href="https://github.com/iamzjt-front-end/git-workflow"><img src="https://img.shields.io/github/stars/iamzjt-front-end/git-workflow?style=flat&colorA=18181B&colorB=F59E0B" alt="github stars"></a>
|
|
13
13
|
<a href="https://github.com/iamzjt-front-end/git-workflow/blob/main/LICENSE"><img src="https://img.shields.io/npm/l/@zjex/git-workflow?style=flat&colorA=18181B&colorB=10B981" alt="license"></a>
|
|
14
14
|
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%3E%3D18-339933?style=flat&logo=node.js&logoColor=white&colorA=18181B" alt="node version"></a>
|
|
15
|
-
<a href="https://github.com/iamzjt-front-end/git-workflow/actions"><img src="https://img.shields.io/badge/tests-
|
|
15
|
+
<a href="https://github.com/iamzjt-front-end/git-workflow/actions"><img src="https://img.shields.io/badge/tests-570%20passed-success?style=flat&colorA=18181B" alt="tests"></a>
|
|
16
16
|
<a href="https://github.com/iamzjt-front-end/git-workflow/issues"><img src="https://img.shields.io/github/issues/iamzjt-front-end/git-workflow?style=flat&colorA=18181B&colorB=EC4899" alt="issues"></a>
|
|
17
17
|
</p>
|
|
18
18
|
|
package/dist/index.js
CHANGED
|
@@ -175,7 +175,7 @@ __export(update_notifier_exports, {
|
|
|
175
175
|
checkForUpdates: () => checkForUpdates,
|
|
176
176
|
clearUpdateCache: () => clearUpdateCache
|
|
177
177
|
});
|
|
178
|
-
import { execSync as execSync3 } from "child_process";
|
|
178
|
+
import { execSync as execSync3, spawn as spawn3 } from "child_process";
|
|
179
179
|
import { readFileSync as readFileSync3, writeFileSync as writeFileSync4, existsSync as existsSync3, unlinkSync as unlinkSync2 } from "fs";
|
|
180
180
|
import { homedir as homedir3 } from "os";
|
|
181
181
|
import { join as join4 } from "path";
|
|
@@ -206,36 +206,45 @@ async function checkForUpdates(currentVersion, packageName = "@zjex/git-workflow
|
|
|
206
206
|
}
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
|
-
|
|
209
|
+
spawnBackgroundCheck(packageName);
|
|
210
210
|
} catch (error) {
|
|
211
211
|
if (error?.constructor?.name === "ExitPromptError") {
|
|
212
212
|
throw error;
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
215
|
}
|
|
216
|
-
function
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
216
|
+
function spawnBackgroundCheck(packageName) {
|
|
217
|
+
try {
|
|
218
|
+
const cacheFile = join4(homedir3(), CACHE_FILE);
|
|
219
|
+
const script = `
|
|
220
|
+
const { execSync } = require('child_process');
|
|
221
|
+
const { writeFileSync, readFileSync, existsSync } = require('fs');
|
|
222
|
+
try {
|
|
223
|
+
const version = execSync('npm view ${packageName} version', {
|
|
224
|
+
encoding: 'utf-8',
|
|
225
|
+
timeout: 10000,
|
|
226
|
+
stdio: ['pipe', 'pipe', 'ignore']
|
|
227
|
+
}).trim();
|
|
228
|
+
if (version) {
|
|
229
|
+
let cache = {};
|
|
230
|
+
try {
|
|
231
|
+
if (existsSync('${cacheFile}')) {
|
|
232
|
+
cache = JSON.parse(readFileSync('${cacheFile}', 'utf-8'));
|
|
233
|
+
}
|
|
234
|
+
} catch {}
|
|
235
|
+
cache.lastCheck = Date.now();
|
|
236
|
+
cache.latestVersion = version;
|
|
237
|
+
writeFileSync('${cacheFile}', JSON.stringify(cache), 'utf-8');
|
|
238
|
+
}
|
|
239
|
+
} catch {}
|
|
240
|
+
`;
|
|
241
|
+
const child = spawn3("node", ["-e", script], {
|
|
242
|
+
detached: true,
|
|
243
|
+
stdio: "ignore"
|
|
244
|
+
});
|
|
245
|
+
child.unref();
|
|
246
|
+
} catch {
|
|
223
247
|
}
|
|
224
|
-
setImmediate(async () => {
|
|
225
|
-
try {
|
|
226
|
-
const latestVersion = await getLatestVersion(packageName);
|
|
227
|
-
if (latestVersion) {
|
|
228
|
-
const cache2 = readCache() || {};
|
|
229
|
-
writeCache({
|
|
230
|
-
...cache2,
|
|
231
|
-
lastCheck: Date.now(),
|
|
232
|
-
latestVersion,
|
|
233
|
-
checkedVersion: currentVersion
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
} catch {
|
|
237
|
-
}
|
|
238
|
-
});
|
|
239
248
|
}
|
|
240
249
|
function isUsingVolta() {
|
|
241
250
|
try {
|
|
@@ -245,19 +254,6 @@ function isUsingVolta() {
|
|
|
245
254
|
return false;
|
|
246
255
|
}
|
|
247
256
|
}
|
|
248
|
-
async function getLatestVersion(packageName) {
|
|
249
|
-
try {
|
|
250
|
-
const result = execSync3(`npm view ${packageName} version`, {
|
|
251
|
-
encoding: "utf-8",
|
|
252
|
-
timeout: 3e3,
|
|
253
|
-
stdio: ["pipe", "pipe", "ignore"]
|
|
254
|
-
// 忽略 stderr
|
|
255
|
-
});
|
|
256
|
-
return result.trim();
|
|
257
|
-
} catch {
|
|
258
|
-
return null;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
257
|
function showSimpleNotification(current, latest) {
|
|
262
258
|
const message = `${colors.yellow("\u{1F389} \u53D1\u73B0\u65B0\u7248\u672C")} ${colors.dim(
|
|
263
259
|
current
|
|
@@ -393,13 +389,12 @@ function writeCache(cache) {
|
|
|
393
389
|
} catch {
|
|
394
390
|
}
|
|
395
391
|
}
|
|
396
|
-
var DISMISS_INTERVAL,
|
|
392
|
+
var DISMISS_INTERVAL, CACHE_FILE;
|
|
397
393
|
var init_update_notifier = __esm({
|
|
398
394
|
"src/update-notifier.ts"() {
|
|
399
395
|
"use strict";
|
|
400
396
|
init_utils();
|
|
401
397
|
DISMISS_INTERVAL = 1e3 * 60 * 60 * 24;
|
|
402
|
-
CHECK_INTERVAL = 1e3 * 60 * 60 * 1;
|
|
403
398
|
CACHE_FILE = ".gw-update-check";
|
|
404
399
|
}
|
|
405
400
|
});
|
|
@@ -2817,7 +2812,7 @@ init_update_notifier();
|
|
|
2817
2812
|
|
|
2818
2813
|
// src/commands/update.ts
|
|
2819
2814
|
init_utils();
|
|
2820
|
-
import { execSync as execSync4, spawn as
|
|
2815
|
+
import { execSync as execSync4, spawn as spawn4 } from "child_process";
|
|
2821
2816
|
import ora6 from "ora";
|
|
2822
2817
|
import boxen3 from "boxen";
|
|
2823
2818
|
import semver2 from "semver";
|
|
@@ -2834,9 +2829,9 @@ function clearUpdateCache2() {
|
|
|
2834
2829
|
} catch {
|
|
2835
2830
|
}
|
|
2836
2831
|
}
|
|
2837
|
-
async function
|
|
2832
|
+
async function getLatestVersion(packageName) {
|
|
2838
2833
|
return new Promise((resolve) => {
|
|
2839
|
-
const npmView =
|
|
2834
|
+
const npmView = spawn4("npm", ["view", packageName, "version"], {
|
|
2840
2835
|
stdio: ["ignore", "pipe", "ignore"],
|
|
2841
2836
|
timeout: 5e3
|
|
2842
2837
|
});
|
|
@@ -2874,7 +2869,7 @@ async function update(currentVersion) {
|
|
|
2874
2869
|
console.log("");
|
|
2875
2870
|
const spinner = ora6("\u6B63\u5728\u83B7\u53D6\u6700\u65B0\u7248\u672C\u4FE1\u606F...").start();
|
|
2876
2871
|
try {
|
|
2877
|
-
const latestVersion = await
|
|
2872
|
+
const latestVersion = await getLatestVersion(packageName);
|
|
2878
2873
|
if (!latestVersion) {
|
|
2879
2874
|
spinner.fail("\u65E0\u6CD5\u83B7\u53D6\u6700\u65B0\u7248\u672C\u4FE1\u606F");
|
|
2880
2875
|
console.log(colors.dim(" \u8BF7\u68C0\u67E5\u7F51\u7EDC\u8FDE\u63A5\u540E\u91CD\u8BD5"));
|
|
@@ -2925,7 +2920,7 @@ async function update(currentVersion) {
|
|
|
2925
2920
|
console.log("");
|
|
2926
2921
|
const updateCommand = usingVolta ? `volta install ${packageName}@latest` : `npm install -g ${packageName}@latest`;
|
|
2927
2922
|
const [command, ...args] = updateCommand.split(" ");
|
|
2928
|
-
const updateProcess =
|
|
2923
|
+
const updateProcess = spawn4(command, args, {
|
|
2929
2924
|
stdio: "inherit"
|
|
2930
2925
|
// 继承父进程的 stdio,显示实时输出
|
|
2931
2926
|
});
|
|
@@ -2996,7 +2991,7 @@ async function update(currentVersion) {
|
|
|
2996
2991
|
init_utils();
|
|
2997
2992
|
import { execSync as execSync5 } from "child_process";
|
|
2998
2993
|
import boxen4 from "boxen";
|
|
2999
|
-
import { spawn as
|
|
2994
|
+
import { spawn as spawn5 } from "child_process";
|
|
3000
2995
|
function parseGitLog(output) {
|
|
3001
2996
|
const commits = [];
|
|
3002
2997
|
const lines = output.trim().split("\n");
|
|
@@ -3214,7 +3209,7 @@ function formatTimelineStyle(commits) {
|
|
|
3214
3209
|
function startInteractivePager(content) {
|
|
3215
3210
|
const pager = process.env.PAGER || "less";
|
|
3216
3211
|
try {
|
|
3217
|
-
const pagerProcess =
|
|
3212
|
+
const pagerProcess = spawn5(pager, ["-R", "-S", "-F", "-X", "-i"], {
|
|
3218
3213
|
stdio: ["pipe", "inherit", "inherit"],
|
|
3219
3214
|
env: { ...process.env, LESS: "-R -S -F -X -i" }
|
|
3220
3215
|
});
|
|
@@ -3630,16 +3625,19 @@ var AI_PROVIDERS2 = {
|
|
|
3630
3625
|
defaultModel: "qwen2.5-coder:14b"
|
|
3631
3626
|
}
|
|
3632
3627
|
};
|
|
3628
|
+
function parseCommitLine(line) {
|
|
3629
|
+
const parts = line.split("|");
|
|
3630
|
+
if (parts.length < 5) return null;
|
|
3631
|
+
const [hash, shortHash, subject, author, date] = parts;
|
|
3632
|
+
return { hash, shortHash, subject, author, date };
|
|
3633
|
+
}
|
|
3633
3634
|
function getRecentCommits3(limit = 20) {
|
|
3634
3635
|
try {
|
|
3635
3636
|
const output = execOutput(
|
|
3636
3637
|
`git log -${limit} --pretty=format:"%H|%h|%s|%an|%ad" --date=short`
|
|
3637
3638
|
);
|
|
3638
3639
|
if (!output) return [];
|
|
3639
|
-
return output.split("\n").filter(Boolean).map((line) =>
|
|
3640
|
-
const [hash, shortHash, subject, author, date] = line.split("|");
|
|
3641
|
-
return { hash, shortHash, subject, author, date };
|
|
3642
|
-
});
|
|
3640
|
+
return output.split("\n").filter(Boolean).map((line) => parseCommitLine(line)).filter((c) => c !== null);
|
|
3643
3641
|
} catch {
|
|
3644
3642
|
return [];
|
|
3645
3643
|
}
|
|
@@ -4079,25 +4077,49 @@ async function review(hashes, options = {}) {
|
|
|
4079
4077
|
let diff = "";
|
|
4080
4078
|
let commits = [];
|
|
4081
4079
|
if (hashes && hashes.length > 0) {
|
|
4082
|
-
|
|
4083
|
-
const
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4080
|
+
if (hashes.length === 1 && hashes[0].includes("..") && !hashes[0].includes("...")) {
|
|
4081
|
+
const range = hashes[0];
|
|
4082
|
+
const [startHash, endHash] = range.split("..");
|
|
4083
|
+
const inclusiveRange = `${startHash}^..${endHash}`;
|
|
4084
|
+
try {
|
|
4085
|
+
const output = execOutput(
|
|
4086
|
+
`git log ${inclusiveRange} --pretty=format:"%H|%h|%s|%an|%ad" --date=short --reverse`
|
|
4087
|
+
);
|
|
4088
|
+
if (!output) {
|
|
4089
|
+
console.log(colors.red(`\u274C \u65E0\u6548\u7684 commit \u8303\u56F4: ${range}`));
|
|
4090
|
+
process.exit(1);
|
|
4091
|
+
}
|
|
4092
|
+
commits = output.split("\n").filter(Boolean).map((line) => parseCommitLine(line)).filter((c) => c !== null);
|
|
4093
|
+
diff = execOutput(`git diff ${inclusiveRange}`) || "";
|
|
4094
|
+
} catch {
|
|
4095
|
+
console.log(colors.red(`\u274C \u65E0\u6548\u7684 commit \u8303\u56F4: ${range}`));
|
|
4088
4096
|
process.exit(1);
|
|
4089
4097
|
}
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4098
|
+
} else {
|
|
4099
|
+
commits = hashes.map((hash) => {
|
|
4100
|
+
const info = execOutput(
|
|
4101
|
+
`git log -1 --pretty=format:"%H|%h|%s|%an|%ad" --date=short ${hash}`
|
|
4102
|
+
);
|
|
4103
|
+
if (!info) {
|
|
4104
|
+
console.log(colors.red(`\u274C \u627E\u4E0D\u5230 commit: ${hash}`));
|
|
4105
|
+
process.exit(1);
|
|
4106
|
+
}
|
|
4107
|
+
const commit2 = parseCommitLine(info);
|
|
4108
|
+
if (!commit2) {
|
|
4109
|
+
console.log(colors.red(`\u274C \u65E0\u6CD5\u89E3\u6790 commit \u4FE1\u606F: ${hash}`));
|
|
4110
|
+
process.exit(1);
|
|
4111
|
+
}
|
|
4112
|
+
return commit2;
|
|
4113
|
+
});
|
|
4114
|
+
diff = getMultipleCommitsDiff(hashes);
|
|
4115
|
+
}
|
|
4094
4116
|
} else if (options.last) {
|
|
4095
4117
|
commits = getRecentCommits3(options.last);
|
|
4096
4118
|
diff = getMultipleCommitsDiff(commits.map((c) => c.hash));
|
|
4097
4119
|
} else if (options.staged) {
|
|
4098
4120
|
diff = getStagedDiff();
|
|
4099
4121
|
} else {
|
|
4100
|
-
const recentCommits = getRecentCommits3(
|
|
4122
|
+
const recentCommits = getRecentCommits3(10);
|
|
4101
4123
|
const stagedDiff = getStagedDiff();
|
|
4102
4124
|
const choices = [];
|
|
4103
4125
|
if (stagedDiff) {
|
|
@@ -4210,7 +4232,7 @@ process.on("SIGTERM", () => {
|
|
|
4210
4232
|
console.log("");
|
|
4211
4233
|
process.exit(0);
|
|
4212
4234
|
});
|
|
4213
|
-
var version = true ? "0.5.
|
|
4235
|
+
var version = true ? "0.5.3" : "0.0.0-dev";
|
|
4214
4236
|
async function mainMenu() {
|
|
4215
4237
|
console.log(
|
|
4216
4238
|
colors.green(`
|
package/docs/commands/review.md
CHANGED
package/docs/guide/ai-review.md
CHANGED
|
@@ -21,6 +21,9 @@ gw review abc1234
|
|
|
21
21
|
# 审查多个 commits
|
|
22
22
|
gw review abc1234 def5678
|
|
23
23
|
|
|
24
|
+
# 审查 commit 范围(包含 abc1234 和 def5678 的所有 commits)
|
|
25
|
+
gw review abc1234..def5678
|
|
26
|
+
|
|
24
27
|
# 审查最近 N 个 commits
|
|
25
28
|
gw review -n 3
|
|
26
29
|
|
|
@@ -137,6 +140,9 @@ gw c
|
|
|
137
140
|
```bash
|
|
138
141
|
# 审查某个 PR 的所有 commits
|
|
139
142
|
gw review abc1234 def5678 ghi9012
|
|
143
|
+
|
|
144
|
+
# 或使用范围语法审查从 abc1234 到 def5678 的所有 commits(包含两端)
|
|
145
|
+
gw review abc1234..def5678
|
|
140
146
|
```
|
|
141
147
|
|
|
142
148
|
### 3. 定期代码审计
|
package/package.json
CHANGED
package/src/commands/review.ts
CHANGED
|
@@ -71,6 +71,17 @@ const AI_PROVIDERS: Record<string, AIProvider> = {
|
|
|
71
71
|
|
|
72
72
|
// ========== 辅助函数 ==========
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* 解析 git log 输出的 commit 信息
|
|
76
|
+
*/
|
|
77
|
+
function parseCommitLine(line: string): CommitInfo | null {
|
|
78
|
+
const parts = line.split("|");
|
|
79
|
+
if (parts.length < 5) return null;
|
|
80
|
+
|
|
81
|
+
const [hash, shortHash, subject, author, date] = parts;
|
|
82
|
+
return { hash, shortHash, subject, author, date };
|
|
83
|
+
}
|
|
84
|
+
|
|
74
85
|
/**
|
|
75
86
|
* 获取最近的 commits 列表
|
|
76
87
|
*/
|
|
@@ -84,10 +95,8 @@ function getRecentCommits(limit: number = 20): CommitInfo[] {
|
|
|
84
95
|
return output
|
|
85
96
|
.split("\n")
|
|
86
97
|
.filter(Boolean)
|
|
87
|
-
.map((line) =>
|
|
88
|
-
|
|
89
|
-
return { hash, shortHash, subject, author, date };
|
|
90
|
-
});
|
|
98
|
+
.map((line) => parseCommitLine(line))
|
|
99
|
+
.filter((c): c is CommitInfo => c !== null);
|
|
91
100
|
} catch {
|
|
92
101
|
return [];
|
|
93
102
|
}
|
|
@@ -629,19 +638,53 @@ export async function review(
|
|
|
629
638
|
|
|
630
639
|
// 确定要审查的内容
|
|
631
640
|
if (hashes && hashes.length > 0) {
|
|
632
|
-
//
|
|
633
|
-
|
|
634
|
-
const
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
641
|
+
// 检查是否是范围语法 (abc123..def456)
|
|
642
|
+
if (hashes.length === 1 && hashes[0].includes("..") && !hashes[0].includes("...")) {
|
|
643
|
+
const range = hashes[0];
|
|
644
|
+
const [startHash, endHash] = range.split("..");
|
|
645
|
+
|
|
646
|
+
// 使用 startHash^..endHash 来包含起始 commit(闭区间 [A, B])
|
|
647
|
+
const inclusiveRange = `${startHash}^..${endHash}`;
|
|
648
|
+
|
|
649
|
+
// 获取范围内的所有 commits
|
|
650
|
+
try {
|
|
651
|
+
const output = execOutput(
|
|
652
|
+
`git log ${inclusiveRange} --pretty=format:"%H|%h|%s|%an|%ad" --date=short --reverse`
|
|
653
|
+
);
|
|
654
|
+
if (!output) {
|
|
655
|
+
console.log(colors.red(`❌ 无效的 commit 范围: ${range}`));
|
|
656
|
+
process.exit(1);
|
|
657
|
+
}
|
|
658
|
+
commits = output
|
|
659
|
+
.split("\n")
|
|
660
|
+
.filter(Boolean)
|
|
661
|
+
.map((line) => parseCommitLine(line))
|
|
662
|
+
.filter((c): c is CommitInfo => c !== null);
|
|
663
|
+
// 获取范围 diff
|
|
664
|
+
diff = execOutput(`git diff ${inclusiveRange}`) || "";
|
|
665
|
+
} catch {
|
|
666
|
+
console.log(colors.red(`❌ 无效的 commit 范围: ${range}`));
|
|
639
667
|
process.exit(1);
|
|
640
668
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
669
|
+
} else {
|
|
670
|
+
// 指定了单个或多个 commit hash
|
|
671
|
+
commits = hashes.map((hash) => {
|
|
672
|
+
const info = execOutput(
|
|
673
|
+
`git log -1 --pretty=format:"%H|%h|%s|%an|%ad" --date=short ${hash}`
|
|
674
|
+
);
|
|
675
|
+
if (!info) {
|
|
676
|
+
console.log(colors.red(`❌ 找不到 commit: ${hash}`));
|
|
677
|
+
process.exit(1);
|
|
678
|
+
}
|
|
679
|
+
const commit = parseCommitLine(info);
|
|
680
|
+
if (!commit) {
|
|
681
|
+
console.log(colors.red(`❌ 无法解析 commit 信息: ${hash}`));
|
|
682
|
+
process.exit(1);
|
|
683
|
+
}
|
|
684
|
+
return commit;
|
|
685
|
+
});
|
|
686
|
+
diff = getMultipleCommitsDiff(hashes);
|
|
687
|
+
}
|
|
645
688
|
} else if (options.last) {
|
|
646
689
|
// 审查最近 N 个 commits
|
|
647
690
|
commits = getRecentCommits(options.last);
|
|
@@ -651,7 +694,7 @@ export async function review(
|
|
|
651
694
|
diff = getStagedDiff();
|
|
652
695
|
} else {
|
|
653
696
|
// 交互式选择
|
|
654
|
-
const recentCommits = getRecentCommits(
|
|
697
|
+
const recentCommits = getRecentCommits(10);
|
|
655
698
|
const stagedDiff = getStagedDiff();
|
|
656
699
|
|
|
657
700
|
const choices: any[] = [];
|
package/src/update-notifier.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execSync } from "child_process";
|
|
1
|
+
import { execSync, spawn } from "child_process";
|
|
2
2
|
import { readFileSync, writeFileSync, existsSync, unlinkSync } from "fs";
|
|
3
3
|
import { homedir } from "os";
|
|
4
4
|
import { join } from "path";
|
|
@@ -9,7 +9,6 @@ import semver from "semver";
|
|
|
9
9
|
import { colors } from "./utils.js";
|
|
10
10
|
|
|
11
11
|
const DISMISS_INTERVAL = 1000 * 60 * 60 * 24; // 24 小时后再次提示
|
|
12
|
-
const CHECK_INTERVAL = 1000 * 60 * 60 * 1; // 已是最新版本时,1 小时检查一次
|
|
13
12
|
const CACHE_FILE = ".gw-update-check";
|
|
14
13
|
|
|
15
14
|
interface UpdateCache {
|
|
@@ -35,7 +34,7 @@ export async function checkForUpdates(
|
|
|
35
34
|
const cache = readCache();
|
|
36
35
|
const now = Date.now();
|
|
37
36
|
|
|
38
|
-
// 1.
|
|
37
|
+
// 1. 先用缓存的结果提示用户(如果有更新版本)
|
|
39
38
|
if (
|
|
40
39
|
cache?.latestVersion &&
|
|
41
40
|
semver.gt(cache.latestVersion, currentVersion)
|
|
@@ -63,8 +62,8 @@ export async function checkForUpdates(
|
|
|
63
62
|
}
|
|
64
63
|
}
|
|
65
64
|
|
|
66
|
-
// 2.
|
|
67
|
-
|
|
65
|
+
// 2. 后台子进程检查更新(每次都检查,不阻塞)
|
|
66
|
+
spawnBackgroundCheck(packageName);
|
|
68
67
|
} catch (error) {
|
|
69
68
|
// 如果是用户按 Ctrl+C,重新抛出让全局处理
|
|
70
69
|
if (error?.constructor?.name === "ExitPromptError") {
|
|
@@ -75,42 +74,47 @@ export async function checkForUpdates(
|
|
|
75
74
|
}
|
|
76
75
|
|
|
77
76
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* - 已是最新版本时:1 小时检查一次
|
|
77
|
+
* 在子进程中检查更新(不阻塞主进程)
|
|
78
|
+
* 使用 unref() 确保主进程退出后子进程仍能完成
|
|
81
79
|
*/
|
|
82
|
-
function
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
80
|
+
function spawnBackgroundCheck(packageName: string): void {
|
|
81
|
+
try {
|
|
82
|
+
const cacheFile = join(homedir(), CACHE_FILE);
|
|
83
|
+
|
|
84
|
+
// 使用 node -e 执行检查脚本
|
|
85
|
+
const script = `
|
|
86
|
+
const { execSync } = require('child_process');
|
|
87
|
+
const { writeFileSync, readFileSync, existsSync } = require('fs');
|
|
88
|
+
try {
|
|
89
|
+
const version = execSync('npm view ${packageName} version', {
|
|
90
|
+
encoding: 'utf-8',
|
|
91
|
+
timeout: 10000,
|
|
92
|
+
stdio: ['pipe', 'pipe', 'ignore']
|
|
93
|
+
}).trim();
|
|
94
|
+
if (version) {
|
|
95
|
+
let cache = {};
|
|
96
|
+
try {
|
|
97
|
+
if (existsSync('${cacheFile}')) {
|
|
98
|
+
cache = JSON.parse(readFileSync('${cacheFile}', 'utf-8'));
|
|
99
|
+
}
|
|
100
|
+
} catch {}
|
|
101
|
+
cache.lastCheck = Date.now();
|
|
102
|
+
cache.latestVersion = version;
|
|
103
|
+
writeFileSync('${cacheFile}', JSON.stringify(cache), 'utf-8');
|
|
104
|
+
}
|
|
105
|
+
} catch {}
|
|
106
|
+
`;
|
|
95
107
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
latestVersion,
|
|
107
|
-
checkedVersion: currentVersion,
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
} catch {
|
|
111
|
-
// 静默失败
|
|
112
|
-
}
|
|
113
|
-
});
|
|
108
|
+
const child = spawn("node", ["-e", script], {
|
|
109
|
+
detached: true,
|
|
110
|
+
stdio: "ignore",
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// unref 让主进程可以独立退出
|
|
114
|
+
child.unref();
|
|
115
|
+
} catch {
|
|
116
|
+
// 静默失败
|
|
117
|
+
}
|
|
114
118
|
}
|
|
115
119
|
|
|
116
120
|
/**
|
|
@@ -125,22 +129,6 @@ function isUsingVolta(): boolean {
|
|
|
125
129
|
}
|
|
126
130
|
}
|
|
127
131
|
|
|
128
|
-
/**
|
|
129
|
-
* 获取 npm 上的最新版本
|
|
130
|
-
*/
|
|
131
|
-
async function getLatestVersion(packageName: string): Promise<string | null> {
|
|
132
|
-
try {
|
|
133
|
-
const result = execSync(`npm view ${packageName} version`, {
|
|
134
|
-
encoding: "utf-8",
|
|
135
|
-
timeout: 3000,
|
|
136
|
-
stdio: ["pipe", "pipe", "ignore"], // 忽略 stderr
|
|
137
|
-
});
|
|
138
|
-
return result.trim();
|
|
139
|
-
} catch {
|
|
140
|
-
return null;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
132
|
/**
|
|
145
133
|
* 显示简单的更新通知(非交互式,不阻塞)
|
|
146
134
|
*/
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
-
import { execSync } from "child_process";
|
|
2
|
+
import { execSync, spawn } from "child_process";
|
|
3
3
|
import { readFileSync, writeFileSync, existsSync, unlinkSync } from "fs";
|
|
4
4
|
import { homedir } from "os";
|
|
5
5
|
import { checkForUpdates, clearUpdateCache } from "../src/update-notifier";
|
|
@@ -28,6 +28,11 @@ describe("Update Notifier 模块测试", () => {
|
|
|
28
28
|
vi.clearAllMocks();
|
|
29
29
|
vi.mocked(homedir).mockReturnValue("/home/user");
|
|
30
30
|
vi.useFakeTimers();
|
|
31
|
+
|
|
32
|
+
// Mock spawn 返回一个带 unref 的对象
|
|
33
|
+
vi.mocked(spawn).mockReturnValue({
|
|
34
|
+
unref: vi.fn(),
|
|
35
|
+
} as any);
|
|
31
36
|
});
|
|
32
37
|
|
|
33
38
|
afterEach(() => {
|
|
@@ -62,16 +67,20 @@ describe("Update Notifier 模块测试", () => {
|
|
|
62
67
|
});
|
|
63
68
|
|
|
64
69
|
describe("checkForUpdates 函数", () => {
|
|
65
|
-
it("
|
|
70
|
+
it("没有缓存时应该启动后台检查", async () => {
|
|
66
71
|
vi.mocked(existsSync).mockReturnValue(false);
|
|
67
|
-
vi.mocked(execSync).mockReturnValue("1.0.1" as any);
|
|
68
72
|
|
|
69
73
|
await checkForUpdates("1.0.0");
|
|
70
74
|
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
// 没有缓存时应该启动子进程检查
|
|
76
|
+
expect(spawn).toHaveBeenCalledWith(
|
|
77
|
+
"node",
|
|
78
|
+
expect.arrayContaining(["-e", expect.any(String)]),
|
|
79
|
+
expect.objectContaining({
|
|
80
|
+
detached: true,
|
|
81
|
+
stdio: "ignore",
|
|
82
|
+
})
|
|
83
|
+
);
|
|
75
84
|
});
|
|
76
85
|
|
|
77
86
|
it("版本相同时不应该显示提示", async () => {
|
|
@@ -153,62 +162,20 @@ describe("Update Notifier 模块测试", () => {
|
|
|
153
162
|
consoleSpy.mockRestore();
|
|
154
163
|
});
|
|
155
164
|
|
|
156
|
-
it("
|
|
157
|
-
const mockCache = {
|
|
158
|
-
lastCheck: Date.now(),
|
|
159
|
-
latestVersion: "1.0.0",
|
|
160
|
-
checkedVersion: "1.0.0",
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
vi.mocked(existsSync).mockReturnValue(true);
|
|
164
|
-
vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockCache));
|
|
165
|
-
vi.mocked(execSync).mockReturnValue("1.0.1" as any);
|
|
166
|
-
|
|
167
|
-
await checkForUpdates("1.0.0");
|
|
168
|
-
await vi.runAllTimersAsync();
|
|
169
|
-
|
|
170
|
-
// writeFileSync 不应该被调用(因为已是最新版本且在1小时内)
|
|
171
|
-
expect(writeFileSync).not.toHaveBeenCalled();
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it("已是最新版本但超过1小时应该重新检查", async () => {
|
|
175
|
-
const oneHourAgo = Date.now() - 2 * 60 * 60 * 1000; // 2小时前
|
|
176
|
-
const mockCache = {
|
|
177
|
-
lastCheck: oneHourAgo,
|
|
178
|
-
latestVersion: "1.0.0",
|
|
179
|
-
checkedVersion: "1.0.0",
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
vi.mocked(existsSync).mockReturnValue(true);
|
|
183
|
-
vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockCache));
|
|
184
|
-
vi.mocked(execSync).mockReturnValue("1.0.1" as any);
|
|
185
|
-
|
|
186
|
-
await checkForUpdates("1.0.0");
|
|
187
|
-
await vi.runAllTimersAsync();
|
|
188
|
-
|
|
189
|
-
expect(writeFileSync).toHaveBeenCalled();
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it("有新版本时每次都应该后台检查", async () => {
|
|
165
|
+
it("每次运行都应该启动后台检查", async () => {
|
|
193
166
|
const mockCache = {
|
|
194
167
|
lastCheck: Date.now(), // 刚刚检查过
|
|
195
|
-
latestVersion: "1.0.
|
|
168
|
+
latestVersion: "1.0.0",
|
|
196
169
|
checkedVersion: "1.0.0",
|
|
197
170
|
};
|
|
198
171
|
|
|
199
172
|
vi.mocked(existsSync).mockReturnValue(true);
|
|
200
173
|
vi.mocked(readFileSync).mockReturnValue(JSON.stringify(mockCache));
|
|
201
|
-
vi.mocked(execSync).mockReturnValue("1.0.2" as any);
|
|
202
|
-
|
|
203
|
-
const consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
204
174
|
|
|
205
175
|
await checkForUpdates("1.0.0");
|
|
206
|
-
await vi.runAllTimersAsync();
|
|
207
|
-
|
|
208
|
-
// 即使刚检查过,有新版本时也应该继续检查
|
|
209
|
-
expect(writeFileSync).toHaveBeenCalled();
|
|
210
176
|
|
|
211
|
-
|
|
177
|
+
// 每次都应该启动子进程检查
|
|
178
|
+
expect(spawn).toHaveBeenCalled();
|
|
212
179
|
});
|
|
213
180
|
|
|
214
181
|
it("缓存文件损坏时应该静默处理", async () => {
|
|
@@ -311,7 +278,7 @@ describe("Update Notifier 模块测试", () => {
|
|
|
311
278
|
{ current: "1.0.0", latest: "1.0.1", shouldShow: true },
|
|
312
279
|
{ current: "1.0.0", latest: "1.1.0", shouldShow: true },
|
|
313
280
|
{ current: "1.0.0", latest: "2.0.0", shouldShow: true },
|
|
314
|
-
{ current: "1.0.1", latest: "1.0.0", shouldShow: false },
|
|
281
|
+
{ current: "1.0.1", latest: "1.0.0", shouldShow: false }, // 本地版本更高,不提示
|
|
315
282
|
{ current: "1.0.0", latest: "1.0.0", shouldShow: false },
|
|
316
283
|
];
|
|
317
284
|
|
|
@@ -344,23 +311,8 @@ describe("Update Notifier 模块测试", () => {
|
|
|
344
311
|
});
|
|
345
312
|
|
|
346
313
|
describe("缓存读写", () => {
|
|
347
|
-
it("应该正确写入缓存", async () => {
|
|
348
|
-
vi.mocked(existsSync).mockReturnValue(false);
|
|
349
|
-
vi.mocked(execSync).mockReturnValue("1.0.1" as any);
|
|
350
|
-
|
|
351
|
-
await checkForUpdates("1.0.0");
|
|
352
|
-
await vi.runAllTimersAsync();
|
|
353
|
-
|
|
354
|
-
expect(writeFileSync).toHaveBeenCalledWith(
|
|
355
|
-
"/home/user/.gw-update-check",
|
|
356
|
-
expect.stringContaining("1.0.1"),
|
|
357
|
-
"utf-8"
|
|
358
|
-
);
|
|
359
|
-
});
|
|
360
|
-
|
|
361
314
|
it("写入缓存失败时应该静默处理", async () => {
|
|
362
315
|
vi.mocked(existsSync).mockReturnValue(false);
|
|
363
|
-
vi.mocked(execSync).mockReturnValue("1.0.1" as any);
|
|
364
316
|
vi.mocked(writeFileSync).mockImplementation(() => {
|
|
365
317
|
throw new Error("Write failed");
|
|
366
318
|
});
|
|
@@ -381,24 +333,24 @@ describe("Update Notifier 模块测试", () => {
|
|
|
381
333
|
describe("网络请求", () => {
|
|
382
334
|
it("获取最新版本失败时应该静默处理", async () => {
|
|
383
335
|
vi.mocked(existsSync).mockReturnValue(false);
|
|
384
|
-
vi.mocked(
|
|
385
|
-
throw new Error("
|
|
336
|
+
vi.mocked(spawn).mockImplementation(() => {
|
|
337
|
+
throw new Error("Spawn error");
|
|
386
338
|
});
|
|
387
339
|
|
|
388
340
|
await expect(checkForUpdates("1.0.0")).resolves.not.toThrow();
|
|
389
341
|
});
|
|
390
342
|
|
|
391
|
-
it("
|
|
343
|
+
it("后台检查应该使用正确的参数", async () => {
|
|
392
344
|
vi.mocked(existsSync).mockReturnValue(false);
|
|
393
|
-
vi.mocked(execSync).mockReturnValue("1.0.1" as any);
|
|
394
345
|
|
|
395
346
|
await checkForUpdates("1.0.0", "@zjex/git-workflow");
|
|
396
|
-
await vi.runAllTimersAsync();
|
|
397
347
|
|
|
398
|
-
expect(
|
|
399
|
-
"
|
|
348
|
+
expect(spawn).toHaveBeenCalledWith(
|
|
349
|
+
"node",
|
|
350
|
+
expect.arrayContaining(["-e", expect.stringContaining("npm view @zjex/git-workflow version")]),
|
|
400
351
|
expect.objectContaining({
|
|
401
|
-
|
|
352
|
+
detached: true,
|
|
353
|
+
stdio: "ignore",
|
|
402
354
|
})
|
|
403
355
|
);
|
|
404
356
|
});
|