@jspg-ai/coding-bb 0.0.5-beta.3 → 0.0.5-beta.4
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/cbb/worktrees/_shared/scripts/deprecate-branches.js +67 -2
- package/cbb/worktrees/_shared/scripts/return-to-trunk.js +8 -1
- package/cbb/worktrees/skills/cbb-local-mode/SKILL.md +41 -8
- package/cbb/worktrees/skills/cbb-local-mode/scripts/deprecate-branches.js +67 -2
- package/cbb/worktrees/skills/cbb-local-mode/scripts/return-to-trunk.js +8 -1
- package/cbb/worktrees/skills/cbb-worktree-close/scripts/deprecate-branches.js +67 -2
- package/package.json +1 -1
|
@@ -9,9 +9,17 @@ require('./silence-popup');
|
|
|
9
9
|
*
|
|
10
10
|
* 用法:
|
|
11
11
|
* node deprecate-branches.js <ws_root> <branch> [--apps a,b]
|
|
12
|
+
* node deprecate-branches.js <ws_root> <branch> [--apps a,b] --check # 仅检查,不重命名
|
|
13
|
+
*
|
|
14
|
+
* 模式:
|
|
15
|
+
* --check 只读检查:逐仓库报远端分支存在性与本地未推送提交数
|
|
16
|
+
* (ls-remote 取远端权威 hash 与本地 <branch> 比对)。
|
|
17
|
+
* 用途:废弃重命名只覆盖远端已推送的部分——本地 ahead 提交
|
|
18
|
+
* 不会被带进 <branch>-deprecated,执行前须提示用户决策。
|
|
19
|
+
* 默认 执行远端重命名 <branch> → <branch>-deprecated
|
|
12
20
|
*
|
|
13
21
|
* 退出码:0 全部成功(含 skipped)/ 1 参数错误 / 2 存在失败
|
|
14
|
-
* 末行:DEPRECATE_RESULT [...]
|
|
22
|
+
* 末行:DEPRECATE_RESULT [...] 或 DEPRECATE_CHECK_RESULT [...]
|
|
15
23
|
*/
|
|
16
24
|
|
|
17
25
|
const { execSync } = require('child_process');
|
|
@@ -44,6 +52,51 @@ function loadApps(wsRoot) {
|
|
|
44
52
|
}
|
|
45
53
|
}
|
|
46
54
|
|
|
55
|
+
/**
|
|
56
|
+
* 检查单个仓库(--check,只读):
|
|
57
|
+
* unpushed: 0 = 已全部推送;>0 = 本地领先远端 N 个提交;'all' = 远端无该分支(提交全部未推送)
|
|
58
|
+
*/
|
|
59
|
+
function checkRepo(name, repo, branch) {
|
|
60
|
+
const rec = { name, status: 'skipped', remote_exists: false, unpushed: 0, detail: null };
|
|
61
|
+
if (!fs.existsSync(path.join(repo, '.git'))) {
|
|
62
|
+
rec.detail = 'no-repo';
|
|
63
|
+
return rec;
|
|
64
|
+
}
|
|
65
|
+
const remote = run('git remote', repo);
|
|
66
|
+
if (!remote.ok || !remote.out) {
|
|
67
|
+
rec.detail = 'no-remote';
|
|
68
|
+
return rec;
|
|
69
|
+
}
|
|
70
|
+
const r = remote.out.split(/\r?\n/)[0];
|
|
71
|
+
if (!run('git rev-parse --verify --quiet "refs/heads/' + branch + '"', repo).ok) {
|
|
72
|
+
rec.detail = 'no-local-branch';
|
|
73
|
+
return rec;
|
|
74
|
+
}
|
|
75
|
+
const ls = run('git ls-remote --heads "' + r + '" "refs/heads/' + branch + '"', repo);
|
|
76
|
+
if (!ls.ok) {
|
|
77
|
+
rec.status = 'failed';
|
|
78
|
+
rec.detail = 'ls-remote-failed';
|
|
79
|
+
return rec;
|
|
80
|
+
}
|
|
81
|
+
if (!ls.out) {
|
|
82
|
+
rec.status = 'ok';
|
|
83
|
+
rec.unpushed = 'all';
|
|
84
|
+
rec.detail = 'remote-has-no-branch';
|
|
85
|
+
return rec;
|
|
86
|
+
}
|
|
87
|
+
const remoteHash = ls.out.split(/\r?\n/)[0].split(/\s+/)[0];
|
|
88
|
+
const cnt = run('git rev-list --count "' + remoteHash + '..refs/heads/' + branch + '"', repo);
|
|
89
|
+
if (!cnt.ok) {
|
|
90
|
+
rec.status = 'failed';
|
|
91
|
+
rec.detail = 'rev-list-failed';
|
|
92
|
+
return rec;
|
|
93
|
+
}
|
|
94
|
+
rec.status = 'ok';
|
|
95
|
+
rec.remote_exists = true;
|
|
96
|
+
rec.unpushed = parseInt(cnt.out, 10) || 0;
|
|
97
|
+
return rec;
|
|
98
|
+
}
|
|
99
|
+
|
|
47
100
|
/** 对单个仓库执行废弃重命名 */
|
|
48
101
|
function deprecateRepo(name, repo, branch) {
|
|
49
102
|
const newName = branch + '-deprecated';
|
|
@@ -95,6 +148,7 @@ function main() {
|
|
|
95
148
|
const appFilter = appsIdx >= 0
|
|
96
149
|
? new Set((args[appsIdx + 1] || '').split(',').map(s => s.trim()).filter(Boolean))
|
|
97
150
|
: null;
|
|
151
|
+
const checkOnly = args.includes('--check');
|
|
98
152
|
|
|
99
153
|
const repos = [{ name: 'workspace', repo: wsRoot }];
|
|
100
154
|
for (const app of loadApps(wsRoot)) {
|
|
@@ -102,6 +156,17 @@ function main() {
|
|
|
102
156
|
repos.push({ name: app.name, repo: path.join(wsRoot, '.codespace', app.name) });
|
|
103
157
|
}
|
|
104
158
|
|
|
159
|
+
if (checkOnly) {
|
|
160
|
+
const results = repos.map(rp => checkRepo(rp.name, rp.repo, branch));
|
|
161
|
+
results.forEach(r => {
|
|
162
|
+
const up = r.status === 'ok' && r.unpushed !== 0 ? ' [unpushed: ' + r.unpushed + ']' : '';
|
|
163
|
+
console.log(r.status.toUpperCase() + ' ' + r.name + (r.detail ? ' — ' + r.detail : '') + up);
|
|
164
|
+
});
|
|
165
|
+
console.log('---');
|
|
166
|
+
console.log('DEPRECATE_CHECK_RESULT', JSON.stringify({ branch, results }));
|
|
167
|
+
process.exit(results.some(r => r.status === 'failed') ? 2 : 0);
|
|
168
|
+
}
|
|
169
|
+
|
|
105
170
|
const results = repos.map(rp => deprecateRepo(rp.name, rp.repo, branch));
|
|
106
171
|
|
|
107
172
|
results.forEach(r => {
|
|
@@ -116,4 +181,4 @@ function main() {
|
|
|
116
181
|
|
|
117
182
|
if (require.main === module) main();
|
|
118
183
|
|
|
119
|
-
module.exports = { run, loadApps, deprecateRepo };
|
|
184
|
+
module.exports = { run, loadApps, checkRepo, deprecateRepo };
|
|
@@ -21,7 +21,8 @@ require('./silence-popup');
|
|
|
21
21
|
* - 检出 dirty(含未跟踪)→ FAIL dirty-worktree(列未提交项数;不自动 stash,交用户决策)
|
|
22
22
|
* - 本地有主干分支 → git checkout <主干>;仅远端有 → git checkout -b <主干> --track origin/<主干>
|
|
23
23
|
* 5. 追平远端:origin/<主干> 存在 → git pull --ff-only origin <主干>
|
|
24
|
-
*
|
|
24
|
+
* (成功详情注记 `pulled origin/<主干>` / `up-to-date`;本地主干与 origin 分叉 → FAIL
|
|
25
|
+
* pull-failed,交用户决策;远端无主干 → 注记跳过)
|
|
25
26
|
* 6. 已在主干 → 跳过切回,仍追平(幂等)
|
|
26
27
|
*
|
|
27
28
|
* 退出码:0 全部成功(含 no-repo / 已在主干)/ 1 参数错误 / 2 任一仓库失败
|
|
@@ -160,12 +161,18 @@ function main() {
|
|
|
160
161
|
}
|
|
161
162
|
|
|
162
163
|
if (hasRef(rp.repo, 'refs/remotes/origin/' + main)) {
|
|
164
|
+
const before = run('git rev-parse --quiet HEAD', rp.repo);
|
|
163
165
|
const pull = run('git pull --ff-only --quiet origin "' + main + '"', rp.repo);
|
|
164
166
|
if (!pull.ok) {
|
|
165
167
|
rec.status = 'failed';
|
|
166
168
|
rec.reason = 'pull-failed';
|
|
167
169
|
rec.detail = (pull.err || '').split(/\r?\n/)[0];
|
|
168
170
|
failed = true;
|
|
171
|
+
} else {
|
|
172
|
+
const after = run('git rev-parse --quiet HEAD', rp.repo);
|
|
173
|
+
rec.detail = before.ok && after.ok && before.out !== after.out
|
|
174
|
+
? 'pulled origin/' + main
|
|
175
|
+
: 'up-to-date';
|
|
169
176
|
}
|
|
170
177
|
} else if (!rec.reason) {
|
|
171
178
|
rec.reason = 'no-remote-trunk';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cbb-local-mode
|
|
3
|
-
description: "业务空间本地模式开发:空间根与各涉及应用在 .codespace/<app>/ 就地建同名分支,当前会话直接开发(不建 worktree、不换目录)。触发:用户说'本地模式'/'就地开发'/'不走 worktree'/'在这个窗口开发',或双模式选择时用户选本地模式。覆盖开工(状态自检→布局确认→切分支)、开发、提交推送(转 cbb-worktree-push)、收尾(收尾合并门禁 ADR 0007:MR / 直合并 /
|
|
3
|
+
description: "业务空间本地模式开发:空间根与各涉及应用在 .codespace/<app>/ 就地建同名分支,当前会话直接开发(不建 worktree、不换目录)。触发:用户说'本地模式'/'就地开发'/'不走 worktree'/'在这个窗口开发',或双模式选择时用户选本地模式。覆盖开工(状态自检→布局确认→切分支)、开发、提交推送(转 cbb-worktree-push)、收尾(收尾合并门禁 ADR 0007:MR / 直合并 / 确认废弃;交付完成回主干 + 删分支,废弃保留本地分支)、升级 worktree(重跑 cbb-worktree-init)。一次只承载一个需求。"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Local Mode(本地模式)
|
|
@@ -103,9 +103,36 @@ node "<skill_dir>/scripts/merge-branches.js" "<ws_root>" "<需求名>" --apps "<
|
|
|
103
103
|
```
|
|
104
104
|
|
|
105
105
|
`need-mr`(远端主干受保护)→ 改用提 MR;`failed` → 输出明细、修复后重跑
|
|
106
|
-
- **确认废弃** → `
|
|
106
|
+
- **确认废弃** → 三步:检查未推送提交 → 二次确认 → 执行。**废弃后仍进 5.4 步骤 1 回主干,但跳过步骤 2(本地分支保留原名,不调用 `delete-branches.js`)**。
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
1. 检查未推送提交(废弃重命名只覆盖远端已推送的部分,本地 ahead 提交不会进 `<需求名>-deprecated`):
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
node "<skill_dir>/scripts/deprecate-branches.js" "<ws_root>" "<需求名>" --apps "<app1,app2>" --check
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
**解读末行 `DEPRECATE_CHECK_RESULT`**:每仓库 `unpushed`(本地领先远端的提交数;`all` = 远端无该分支、提交全部未推送;`0` = 已全部推送)。存在 `unpushed > 0` / `all` 时用 `AskUserQuestion`:
|
|
115
|
+
|
|
116
|
+
```
|
|
117
|
+
⚠️ 以下仓库存在未推送提交(将只保留在本地分支,不进 <需求名>-deprecated):
|
|
118
|
+
- workspace: 3 个 commit
|
|
119
|
+
|
|
120
|
+
请选择处理方式:
|
|
121
|
+
[先推送](推荐)调 cbb-worktree-push 推送全部提交后回到本步
|
|
122
|
+
[继续] 不推送直接废弃(这些提交只存在于被保留的本地分支)
|
|
123
|
+
[中止] 终止流程,我先手动处理
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
2. 二次确认(远程重命名不可逆,`AskUserQuestion` 固定选项):确认废弃 / 中止
|
|
127
|
+
3. 执行:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
node "<skill_dir>/scripts/deprecate-branches.js" "<ws_root>" "<需求名>" --apps "<app1,app2>"
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
`failed` → 报告并终止;完成后进 5.4(**仅步骤 1**)
|
|
134
|
+
|
|
135
|
+
**5.4 交付完成 → 各仓库回主干并删分支**(脚本化;主干名逐仓库经远端 HEAD 探测,兼容 main / master。**废弃出口仅执行步骤 1**——分支保留、不删):
|
|
109
136
|
|
|
110
137
|
1. 各仓库切回主干并追平远端(替代原裸 git 三连;不自动 stash,失败即报告):
|
|
111
138
|
|
|
@@ -114,20 +141,20 @@ node "<skill_dir>/scripts/return-to-trunk.js" "<ws_root>" --apps "<app1,app2>"
|
|
|
114
141
|
```
|
|
115
142
|
|
|
116
143
|
**解读末行 `TRUNK_RESULT`**:
|
|
117
|
-
- 退出码 0 →
|
|
144
|
+
- 退出码 0 → 全部已回主干并追平(detail 注记 `pulled origin/<主干>` = 本次有追平;`up-to-date` = 本就最新)
|
|
118
145
|
- 某仓库 `dirty-worktree` → 检出上有未提交改动(回归 5.1 前置,先处理再重跑);**不自动 stash**
|
|
119
146
|
- 某仓库 `pull-failed`(本地主干与 origin 分叉)→ 输出详情,交用户决策
|
|
120
147
|
- 某仓库 `cannot-detect-main` → 检查该仓库远端与主干分支(`fetch-failed` 同理)
|
|
121
148
|
- 幂等:已在主干的仓库仅追平远端,重复执行无害
|
|
122
149
|
|
|
123
|
-
2. 本地 +
|
|
150
|
+
2. 本地 + 远程需求分支删除(**仅交付完成出口执行**——MR 已合入 / 直合并成功;**废弃出口跳过本步**,本地分支保留原名。本地删除含 squash 合并的 `-d` → `-D` 兜底;删除前输出将删清单):
|
|
124
151
|
|
|
125
152
|
```bash
|
|
126
153
|
node "<skill_dir>/scripts/delete-branches.js" "<ws_root>" "<需求名>" local_remote
|
|
127
154
|
```
|
|
128
155
|
|
|
129
156
|
- openspec change 归档在合并前执行(产物在分支上随交付合入)
|
|
130
|
-
-
|
|
157
|
+
- 全部仓库回主干(交付完成出口含分支删除)后,向用户报告"本地模式已收尾,空间回到干净态"
|
|
131
158
|
|
|
132
159
|
## 升级 worktree(中途要并行 / 要隔离)
|
|
133
160
|
|
|
@@ -146,7 +173,7 @@ node "<skill_dir>/scripts/delete-branches.js" "<ws_root>" "<需求名>" local_re
|
|
|
146
173
|
| commit 被 hook 拦(main 载荷) | 大声报错是预期行为:回分支重做;禁止自动 --no-verify |
|
|
147
174
|
| 误在 main 检出上写了文件 | `git checkout -b <需求名>` 后改动随分支走;已 staged 则 unstage 后再切 |
|
|
148
175
|
| 提交推送 | 说"提交并推送" → cbb-worktree-push(本地布局自动发现) |
|
|
149
|
-
| 收尾 | 收尾合并门禁(ADR 0007):`merge-branches.js --check` 判已合并 → 全合并则 `return-to-trunk.js` 回主干 + `delete-branches.js` 删分支(主干名探测);否则三选一(MR / 直合并(临时 worktree) /
|
|
176
|
+
| 收尾 | 收尾合并门禁(ADR 0007):`merge-branches.js --check` 判已合并 → 全合并则 `return-to-trunk.js` 回主干 + `delete-branches.js` 删分支(主干名探测);否则三选一(MR / 直合并(临时 worktree) / 确认废弃(仅回主干不删分支)) |
|
|
150
177
|
| 要并行新需求 | 转 worktree 模式(init 分支从 origin/main 建,不占本地检出);本地需求继续占用根 |
|
|
151
178
|
| 升级隔离 | 重跑 cbb-worktree-init(提交随身带走);先回主干腾检出(return-to-trunk.js) |
|
|
152
179
|
|
|
@@ -174,6 +201,11 @@ node "<skill_dir>/scripts/delete-branches.js" "<ws_root>" "<需求名>" local_re
|
|
|
174
201
|
- **问题:** 回主干后未追平远端,本地主干陈旧,下次 `start-local` 从旧基线建分支
|
|
175
202
|
- **修复:** 收尾固定两步脚本:`return-to-trunk.js`(切回主干 + `pull --ff-only` 追平)→ `delete-branches.js` 删分支
|
|
176
203
|
|
|
204
|
+
### 废弃出口误跑删分支
|
|
205
|
+
|
|
206
|
+
- **问题:** 选"确认废弃"后照 5.4 全跑,`delete-branches.js` 对未合并的废弃分支 `-D` 强删——远端 `<需求名>-deprecated` 只含已推送部分,本地领先提交只剩 reflog 可捞
|
|
207
|
+
- **修复:** 废弃出口仅执行 5.4 步骤 1(回主干 + 追平);本地分支保留原名,**不调用** `delete-branches.js`
|
|
208
|
+
|
|
177
209
|
### 中途插单静默混入
|
|
178
210
|
|
|
179
211
|
- **问题:** 用户插的"顺手改一下"与本需求改动混在同一 commit,评审困难
|
|
@@ -200,5 +232,6 @@ node "<skill_dir>/scripts/delete-branches.js" "<ws_root>" "<需求名>" local_re
|
|
|
200
232
|
- 切分支统一走 `start-local.js`(基线 = `<remote>/<main>`,不取 HEAD)
|
|
201
233
|
- 收尾先过门禁判定(`merge-branches.js --check`);未合并必须三选一后收尾
|
|
202
234
|
- 直合并走 `merge-branches.js`(临时 worktree,零接触;本地检出由 5.4 `return-to-trunk.js` 切回主干)
|
|
203
|
-
-
|
|
235
|
+
- 废弃出口:先 `deprecate-branches.js --check` 查未推送提交(有 ahead 须用户决策)→ 二次确认后执行;废弃后**仅回主干**(5.4 步骤 1),不删本地分支
|
|
236
|
+
- 收尾脚本化(`return-to-trunk.js` 回主干 + 交付完成出口 `delete-branches.js` 删分支)完成后报告干净态
|
|
204
237
|
- 所有写操作前的分支确认(当前检出 = 需求分支)
|
|
@@ -9,9 +9,17 @@ require('./silence-popup');
|
|
|
9
9
|
*
|
|
10
10
|
* 用法:
|
|
11
11
|
* node deprecate-branches.js <ws_root> <branch> [--apps a,b]
|
|
12
|
+
* node deprecate-branches.js <ws_root> <branch> [--apps a,b] --check # 仅检查,不重命名
|
|
13
|
+
*
|
|
14
|
+
* 模式:
|
|
15
|
+
* --check 只读检查:逐仓库报远端分支存在性与本地未推送提交数
|
|
16
|
+
* (ls-remote 取远端权威 hash 与本地 <branch> 比对)。
|
|
17
|
+
* 用途:废弃重命名只覆盖远端已推送的部分——本地 ahead 提交
|
|
18
|
+
* 不会被带进 <branch>-deprecated,执行前须提示用户决策。
|
|
19
|
+
* 默认 执行远端重命名 <branch> → <branch>-deprecated
|
|
12
20
|
*
|
|
13
21
|
* 退出码:0 全部成功(含 skipped)/ 1 参数错误 / 2 存在失败
|
|
14
|
-
* 末行:DEPRECATE_RESULT [...]
|
|
22
|
+
* 末行:DEPRECATE_RESULT [...] 或 DEPRECATE_CHECK_RESULT [...]
|
|
15
23
|
*/
|
|
16
24
|
|
|
17
25
|
const { execSync } = require('child_process');
|
|
@@ -44,6 +52,51 @@ function loadApps(wsRoot) {
|
|
|
44
52
|
}
|
|
45
53
|
}
|
|
46
54
|
|
|
55
|
+
/**
|
|
56
|
+
* 检查单个仓库(--check,只读):
|
|
57
|
+
* unpushed: 0 = 已全部推送;>0 = 本地领先远端 N 个提交;'all' = 远端无该分支(提交全部未推送)
|
|
58
|
+
*/
|
|
59
|
+
function checkRepo(name, repo, branch) {
|
|
60
|
+
const rec = { name, status: 'skipped', remote_exists: false, unpushed: 0, detail: null };
|
|
61
|
+
if (!fs.existsSync(path.join(repo, '.git'))) {
|
|
62
|
+
rec.detail = 'no-repo';
|
|
63
|
+
return rec;
|
|
64
|
+
}
|
|
65
|
+
const remote = run('git remote', repo);
|
|
66
|
+
if (!remote.ok || !remote.out) {
|
|
67
|
+
rec.detail = 'no-remote';
|
|
68
|
+
return rec;
|
|
69
|
+
}
|
|
70
|
+
const r = remote.out.split(/\r?\n/)[0];
|
|
71
|
+
if (!run('git rev-parse --verify --quiet "refs/heads/' + branch + '"', repo).ok) {
|
|
72
|
+
rec.detail = 'no-local-branch';
|
|
73
|
+
return rec;
|
|
74
|
+
}
|
|
75
|
+
const ls = run('git ls-remote --heads "' + r + '" "refs/heads/' + branch + '"', repo);
|
|
76
|
+
if (!ls.ok) {
|
|
77
|
+
rec.status = 'failed';
|
|
78
|
+
rec.detail = 'ls-remote-failed';
|
|
79
|
+
return rec;
|
|
80
|
+
}
|
|
81
|
+
if (!ls.out) {
|
|
82
|
+
rec.status = 'ok';
|
|
83
|
+
rec.unpushed = 'all';
|
|
84
|
+
rec.detail = 'remote-has-no-branch';
|
|
85
|
+
return rec;
|
|
86
|
+
}
|
|
87
|
+
const remoteHash = ls.out.split(/\r?\n/)[0].split(/\s+/)[0];
|
|
88
|
+
const cnt = run('git rev-list --count "' + remoteHash + '..refs/heads/' + branch + '"', repo);
|
|
89
|
+
if (!cnt.ok) {
|
|
90
|
+
rec.status = 'failed';
|
|
91
|
+
rec.detail = 'rev-list-failed';
|
|
92
|
+
return rec;
|
|
93
|
+
}
|
|
94
|
+
rec.status = 'ok';
|
|
95
|
+
rec.remote_exists = true;
|
|
96
|
+
rec.unpushed = parseInt(cnt.out, 10) || 0;
|
|
97
|
+
return rec;
|
|
98
|
+
}
|
|
99
|
+
|
|
47
100
|
/** 对单个仓库执行废弃重命名 */
|
|
48
101
|
function deprecateRepo(name, repo, branch) {
|
|
49
102
|
const newName = branch + '-deprecated';
|
|
@@ -95,6 +148,7 @@ function main() {
|
|
|
95
148
|
const appFilter = appsIdx >= 0
|
|
96
149
|
? new Set((args[appsIdx + 1] || '').split(',').map(s => s.trim()).filter(Boolean))
|
|
97
150
|
: null;
|
|
151
|
+
const checkOnly = args.includes('--check');
|
|
98
152
|
|
|
99
153
|
const repos = [{ name: 'workspace', repo: wsRoot }];
|
|
100
154
|
for (const app of loadApps(wsRoot)) {
|
|
@@ -102,6 +156,17 @@ function main() {
|
|
|
102
156
|
repos.push({ name: app.name, repo: path.join(wsRoot, '.codespace', app.name) });
|
|
103
157
|
}
|
|
104
158
|
|
|
159
|
+
if (checkOnly) {
|
|
160
|
+
const results = repos.map(rp => checkRepo(rp.name, rp.repo, branch));
|
|
161
|
+
results.forEach(r => {
|
|
162
|
+
const up = r.status === 'ok' && r.unpushed !== 0 ? ' [unpushed: ' + r.unpushed + ']' : '';
|
|
163
|
+
console.log(r.status.toUpperCase() + ' ' + r.name + (r.detail ? ' — ' + r.detail : '') + up);
|
|
164
|
+
});
|
|
165
|
+
console.log('---');
|
|
166
|
+
console.log('DEPRECATE_CHECK_RESULT', JSON.stringify({ branch, results }));
|
|
167
|
+
process.exit(results.some(r => r.status === 'failed') ? 2 : 0);
|
|
168
|
+
}
|
|
169
|
+
|
|
105
170
|
const results = repos.map(rp => deprecateRepo(rp.name, rp.repo, branch));
|
|
106
171
|
|
|
107
172
|
results.forEach(r => {
|
|
@@ -116,4 +181,4 @@ function main() {
|
|
|
116
181
|
|
|
117
182
|
if (require.main === module) main();
|
|
118
183
|
|
|
119
|
-
module.exports = { run, loadApps, deprecateRepo };
|
|
184
|
+
module.exports = { run, loadApps, checkRepo, deprecateRepo };
|
|
@@ -21,7 +21,8 @@ require('./silence-popup');
|
|
|
21
21
|
* - 检出 dirty(含未跟踪)→ FAIL dirty-worktree(列未提交项数;不自动 stash,交用户决策)
|
|
22
22
|
* - 本地有主干分支 → git checkout <主干>;仅远端有 → git checkout -b <主干> --track origin/<主干>
|
|
23
23
|
* 5. 追平远端:origin/<主干> 存在 → git pull --ff-only origin <主干>
|
|
24
|
-
*
|
|
24
|
+
* (成功详情注记 `pulled origin/<主干>` / `up-to-date`;本地主干与 origin 分叉 → FAIL
|
|
25
|
+
* pull-failed,交用户决策;远端无主干 → 注记跳过)
|
|
25
26
|
* 6. 已在主干 → 跳过切回,仍追平(幂等)
|
|
26
27
|
*
|
|
27
28
|
* 退出码:0 全部成功(含 no-repo / 已在主干)/ 1 参数错误 / 2 任一仓库失败
|
|
@@ -160,12 +161,18 @@ function main() {
|
|
|
160
161
|
}
|
|
161
162
|
|
|
162
163
|
if (hasRef(rp.repo, 'refs/remotes/origin/' + main)) {
|
|
164
|
+
const before = run('git rev-parse --quiet HEAD', rp.repo);
|
|
163
165
|
const pull = run('git pull --ff-only --quiet origin "' + main + '"', rp.repo);
|
|
164
166
|
if (!pull.ok) {
|
|
165
167
|
rec.status = 'failed';
|
|
166
168
|
rec.reason = 'pull-failed';
|
|
167
169
|
rec.detail = (pull.err || '').split(/\r?\n/)[0];
|
|
168
170
|
failed = true;
|
|
171
|
+
} else {
|
|
172
|
+
const after = run('git rev-parse --quiet HEAD', rp.repo);
|
|
173
|
+
rec.detail = before.ok && after.ok && before.out !== after.out
|
|
174
|
+
? 'pulled origin/' + main
|
|
175
|
+
: 'up-to-date';
|
|
169
176
|
}
|
|
170
177
|
} else if (!rec.reason) {
|
|
171
178
|
rec.reason = 'no-remote-trunk';
|
|
@@ -9,9 +9,17 @@ require('./silence-popup');
|
|
|
9
9
|
*
|
|
10
10
|
* 用法:
|
|
11
11
|
* node deprecate-branches.js <ws_root> <branch> [--apps a,b]
|
|
12
|
+
* node deprecate-branches.js <ws_root> <branch> [--apps a,b] --check # 仅检查,不重命名
|
|
13
|
+
*
|
|
14
|
+
* 模式:
|
|
15
|
+
* --check 只读检查:逐仓库报远端分支存在性与本地未推送提交数
|
|
16
|
+
* (ls-remote 取远端权威 hash 与本地 <branch> 比对)。
|
|
17
|
+
* 用途:废弃重命名只覆盖远端已推送的部分——本地 ahead 提交
|
|
18
|
+
* 不会被带进 <branch>-deprecated,执行前须提示用户决策。
|
|
19
|
+
* 默认 执行远端重命名 <branch> → <branch>-deprecated
|
|
12
20
|
*
|
|
13
21
|
* 退出码:0 全部成功(含 skipped)/ 1 参数错误 / 2 存在失败
|
|
14
|
-
* 末行:DEPRECATE_RESULT [...]
|
|
22
|
+
* 末行:DEPRECATE_RESULT [...] 或 DEPRECATE_CHECK_RESULT [...]
|
|
15
23
|
*/
|
|
16
24
|
|
|
17
25
|
const { execSync } = require('child_process');
|
|
@@ -44,6 +52,51 @@ function loadApps(wsRoot) {
|
|
|
44
52
|
}
|
|
45
53
|
}
|
|
46
54
|
|
|
55
|
+
/**
|
|
56
|
+
* 检查单个仓库(--check,只读):
|
|
57
|
+
* unpushed: 0 = 已全部推送;>0 = 本地领先远端 N 个提交;'all' = 远端无该分支(提交全部未推送)
|
|
58
|
+
*/
|
|
59
|
+
function checkRepo(name, repo, branch) {
|
|
60
|
+
const rec = { name, status: 'skipped', remote_exists: false, unpushed: 0, detail: null };
|
|
61
|
+
if (!fs.existsSync(path.join(repo, '.git'))) {
|
|
62
|
+
rec.detail = 'no-repo';
|
|
63
|
+
return rec;
|
|
64
|
+
}
|
|
65
|
+
const remote = run('git remote', repo);
|
|
66
|
+
if (!remote.ok || !remote.out) {
|
|
67
|
+
rec.detail = 'no-remote';
|
|
68
|
+
return rec;
|
|
69
|
+
}
|
|
70
|
+
const r = remote.out.split(/\r?\n/)[0];
|
|
71
|
+
if (!run('git rev-parse --verify --quiet "refs/heads/' + branch + '"', repo).ok) {
|
|
72
|
+
rec.detail = 'no-local-branch';
|
|
73
|
+
return rec;
|
|
74
|
+
}
|
|
75
|
+
const ls = run('git ls-remote --heads "' + r + '" "refs/heads/' + branch + '"', repo);
|
|
76
|
+
if (!ls.ok) {
|
|
77
|
+
rec.status = 'failed';
|
|
78
|
+
rec.detail = 'ls-remote-failed';
|
|
79
|
+
return rec;
|
|
80
|
+
}
|
|
81
|
+
if (!ls.out) {
|
|
82
|
+
rec.status = 'ok';
|
|
83
|
+
rec.unpushed = 'all';
|
|
84
|
+
rec.detail = 'remote-has-no-branch';
|
|
85
|
+
return rec;
|
|
86
|
+
}
|
|
87
|
+
const remoteHash = ls.out.split(/\r?\n/)[0].split(/\s+/)[0];
|
|
88
|
+
const cnt = run('git rev-list --count "' + remoteHash + '..refs/heads/' + branch + '"', repo);
|
|
89
|
+
if (!cnt.ok) {
|
|
90
|
+
rec.status = 'failed';
|
|
91
|
+
rec.detail = 'rev-list-failed';
|
|
92
|
+
return rec;
|
|
93
|
+
}
|
|
94
|
+
rec.status = 'ok';
|
|
95
|
+
rec.remote_exists = true;
|
|
96
|
+
rec.unpushed = parseInt(cnt.out, 10) || 0;
|
|
97
|
+
return rec;
|
|
98
|
+
}
|
|
99
|
+
|
|
47
100
|
/** 对单个仓库执行废弃重命名 */
|
|
48
101
|
function deprecateRepo(name, repo, branch) {
|
|
49
102
|
const newName = branch + '-deprecated';
|
|
@@ -95,6 +148,7 @@ function main() {
|
|
|
95
148
|
const appFilter = appsIdx >= 0
|
|
96
149
|
? new Set((args[appsIdx + 1] || '').split(',').map(s => s.trim()).filter(Boolean))
|
|
97
150
|
: null;
|
|
151
|
+
const checkOnly = args.includes('--check');
|
|
98
152
|
|
|
99
153
|
const repos = [{ name: 'workspace', repo: wsRoot }];
|
|
100
154
|
for (const app of loadApps(wsRoot)) {
|
|
@@ -102,6 +156,17 @@ function main() {
|
|
|
102
156
|
repos.push({ name: app.name, repo: path.join(wsRoot, '.codespace', app.name) });
|
|
103
157
|
}
|
|
104
158
|
|
|
159
|
+
if (checkOnly) {
|
|
160
|
+
const results = repos.map(rp => checkRepo(rp.name, rp.repo, branch));
|
|
161
|
+
results.forEach(r => {
|
|
162
|
+
const up = r.status === 'ok' && r.unpushed !== 0 ? ' [unpushed: ' + r.unpushed + ']' : '';
|
|
163
|
+
console.log(r.status.toUpperCase() + ' ' + r.name + (r.detail ? ' — ' + r.detail : '') + up);
|
|
164
|
+
});
|
|
165
|
+
console.log('---');
|
|
166
|
+
console.log('DEPRECATE_CHECK_RESULT', JSON.stringify({ branch, results }));
|
|
167
|
+
process.exit(results.some(r => r.status === 'failed') ? 2 : 0);
|
|
168
|
+
}
|
|
169
|
+
|
|
105
170
|
const results = repos.map(rp => deprecateRepo(rp.name, rp.repo, branch));
|
|
106
171
|
|
|
107
172
|
results.forEach(r => {
|
|
@@ -116,4 +181,4 @@ function main() {
|
|
|
116
181
|
|
|
117
182
|
if (require.main === module) main();
|
|
118
183
|
|
|
119
|
-
module.exports = { run, loadApps, deprecateRepo };
|
|
184
|
+
module.exports = { run, loadApps, checkRepo, deprecateRepo };
|