@furongjun1999/dsh-memory 0.7.4 → 0.8.0
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/README.md +632 -630
- package/codebuddy/CODEBUDDY.md +5 -5
- package/docs/discipline/templates/zcode-user.md.tmpl +4 -1
- package/docs/eval/issue64_replay_check_/346/240/270/351/252/214/344/270/216/344/277/256/345/244/215_v1.0.md +284 -0
- package/docs/eval//345/217/221/345/270/20312_/344/270/226/347/225/214/346/250/241/345/236/213/345/212/237/350/203/275/347/253/257_v1.0.md +1 -1
- package/docs/eval//345/217/221/345/270/20313_/344/270/212/344/270/213/346/226/207/350/207/252/347/256/241/347/220/206/346/234/272/345/210/266_v1.0.md +114 -0
- package/docs/eval//345/217/221/345/270/20314_/350/272/253/344/275/223/303/227/350/204/221/347/273/204/345/220/210_v1.0.md +152 -0
- package/docs/eval//351/262/270/345/250/230/344/270/203/347/261/273/347/212/266/346/200/201/350/277/275/350/270/252_v1.0.md +176 -0
- package/docs/mdcg//344/270/226/347/225/214/346/250/241/345/236/213/345/212/237/350/203/275/347/253/257_/344/275/277/347/224/250/344/270/216/350/277/220/347/273/264_v1.0.md +92 -13
- package/docs/mdcg//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +43 -43
- package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +14 -3
- package/docs/mdcg//347/235/241/347/234/240/345/221/250/346/234/237_/350/277/220/347/273/264/345/211/215/346/217/220/344/270/216/347/273/264/346/212/244/346/214/207/345/215/227_v1.0.md +85 -1
- package/docs/plans//347/201/265/346/236/242/350/272/253/344/275/223/303/227/350/204/221_/344/270/226/347/225/214/346/250/241/345/236/213/345/257/271/346/216/245/350/256/276/350/256/241_v0.1.md +119 -0
- package/docs//345/267/245/344/275/234/347/272/252/345/276/213_/350/256/244/347/237/245/345/233/276/346/235/241/347/233/256_v1.1.json +461 -461
- package/docs//350/256/260/345/277/206/347/263/273/347/273/237/345/215/263/347/231/275/347/256/261/345/214/226/346/231/272/350/203/275_/346/236/266/346/236/204/345/257/271/347/205/247_v0.2.md +121 -0
- package/docs//350/256/260/345/277/206/347/263/273/347/273/237/345/215/263/347/231/275/347/256/261/345/214/226/346/231/272/350/203/275_/346/236/266/346/236/204/345/257/271/347/205/247_v0.3.md +117 -0
- package/lib/hooks.d.ts +34 -0
- package/lib/hooks.js +165 -0
- package/lib/index.js +22 -1
- package/lib/lib/mdcg_client.d.ts +37 -0
- package/lib/lib/mdcg_client.js +44 -0
- package/md_cg/audit.py +7 -1
- package/md_cg/bench_governance.py +4 -1
- package/md_cg/bench_locomo_zh.py +12 -2
- package/md_cg/bench_zh_mad.py +12 -2
- package/md_cg/chain.py +23 -7
- package/md_cg/consolidate.py +81 -10
- package/md_cg/interop.py +25 -9
- package/md_cg/lifecycle.py +7 -0
- package/md_cg/mcp_server.py +22 -0
- package/md_cg/mdcg.py +6 -1
- package/md_cg/scrub.py +4 -2
- package/md_cg/sleep.py +273 -103
- package/md_cg/sustain.py +150 -4
- package/md_cg/test_lifecycle_retire_leak.py +90 -0
- package/md_cg/test_p6_consolidate.py +389 -7
- package/md_cg/test_sleep_gitlock.py +134 -0
- package/md_cg/test_spatial_coords3d.py +292 -0
- package/md_cg/test_sustain_bounded.py +624 -0
- package/md_cg/units.py +4 -1
- package/package.json +2 -2
- package/skills/plugin.json +1 -1
- package/src/hooks.ts +177 -0
- package/src/index.ts +22 -1
- package/src/lib/mdcg_client.ts +48 -0
- package/zcode/AGENTS.md +5 -5
package/md_cg/sleep.py
CHANGED
|
@@ -75,6 +75,18 @@ GIT_USER_EMAIL = "sleep@lingshu.local"
|
|
|
75
75
|
FORBIDDEN_BASENAMES = ("_keys.json", "_access.log", "_index.json")
|
|
76
76
|
FORBIDDEN_SUFFIXES = (".tmp", ".lock")
|
|
77
77
|
DEFAULT_LOCK_TIMEOUT = 30.0
|
|
78
|
+
#: 单条 git 子进程的**硬超时**(秒)——issue #63 主修(单点可调)。
|
|
79
|
+
#: 取值依据:本机 git 操作为秒级(init / add / commit / rev-parse / worktree /
|
|
80
|
+
#: merge / revert 全是单库本地动作),120s 为安全上界;超时即 kill
|
|
81
|
+
#: (`subprocess.run(timeout=)` 语义);残留锁处置见 `_clear_git_lock`
|
|
82
|
+
#: (有界自清,2026-10-06 落地原「是否自动清理」待裁项)。
|
|
83
|
+
_GIT_TIMEOUT_S = 120
|
|
84
|
+
#: git 超时被 kill 后可能的残留锁名(有界自清的**唯一**对象名)。
|
|
85
|
+
_GIT_LOCK_NAME = "index.lock"
|
|
86
|
+
#: 残留锁的**超龄阈值**(秒)——有界自清的第二闸:本模块全部 git 调用都带
|
|
87
|
+
#: `_GIT_TIMEOUT_S` 超时,合法操作结构性不可能持锁超过它;300s(≈2.5×)之上
|
|
88
|
+
#: 仍存在的锁只可能是崩溃/被 kill 的残留。
|
|
89
|
+
_GIT_LOCK_STALE_S = 300
|
|
78
90
|
#: 睡眠轮次的**台账**(append-only JSONL)——落 `sleep_root()` 侧(状态面,
|
|
79
91
|
#: **不是数据根**):台账是运行态,不该成为真源面的一部分、也不该进版本库。
|
|
80
92
|
SLEEP_LEDGER = "_sleep.jsonl"
|
|
@@ -116,6 +128,16 @@ class SleepError(Exception):
|
|
|
116
128
|
"""git 操作失败(带 stderr 摘要)。"""
|
|
117
129
|
|
|
118
130
|
|
|
131
|
+
class SleepGitTimeout(SleepError):
|
|
132
|
+
"""git 子进程**超时已被 kill**(issue #63)——与「git 返回非零」区分。
|
|
133
|
+
|
|
134
|
+
为什么单列一支:超时与普通失败的处理面不同——普通失败(rc≠0)是 git
|
|
135
|
+
对本次操作的裁决(冲突 / 非法状态),超时是**本次操作没有完成**(可能
|
|
136
|
+
留下半成品与残留锁),台账文案必须能分开(物化超时 vs 物化失败)。
|
|
137
|
+
`SleepError` 的子类 ⇒ 所有既有 `except SleepError` 面照旧收得住。
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
|
|
119
141
|
# 生效条件:name 为 SLEEP_ENV_DEFAULTS 的键时,按 SLEEP_ENV_KEYS[name] 从 environ(缺省 os.environ)取名取值,缺键(None)时回落真源缺省;返回**原始字符串**(不做 bool/float 解释——解释归各具名包装)。name 非表内键时抛 KeyError(不做静默回落:拼错键名即编程错误)。
|
|
120
142
|
def sleep_env(name: str, environ=None) -> str:
|
|
121
143
|
"""§4.7 env 表的唯一读取出口(原始字面量)。"""
|
|
@@ -265,23 +287,93 @@ def face_delta(before: dict, after: dict) -> dict:
|
|
|
265
287
|
if before[k] != after[k])}
|
|
266
288
|
|
|
267
289
|
|
|
268
|
-
# 生效条件:git_dir_path 与 work_tree 给定时以 ["git", "--git-dir=<git_dir_path>", "--work-tree=<work_tree>", *args] 调 subprocess.run(capture_output + text + 显式 utf-8/errors=replace + env 带 PYTHONUTF8=1 + shell=False
|
|
290
|
+
# 生效条件:git_dir_path 与 work_tree 给定时以 ["git", "--git-dir=<git_dir_path>", "--work-tree=<work_tree>", *args] 调 subprocess.run(capture_output + text + 显式 utf-8/errors=replace + env 带 PYTHONUTF8=1 + shell=False + **timeout=_GIT_TIMEOUT_S** + **stdin=DEVNULL**)并返回 CompletedProcess;returncode 非零不抛(由调用方判);TimeoutExpired **不吞**——单点转 SleepGitTimeout(消息含「超时(_GIT_TIMEOUT_S)已被 kill」+ 命令名 + 残留锁人工核删指引)。
|
|
291
|
+
# 生效条件:git_dir_path 下存在 index.lock 且(force 为真 或 其 mtime 距今 ≥ _GIT_LOCK_STALE_S 秒)时删除该文件、记一行 `git_lock_cleared` 台账(写入失败不阻断)并返回 "cleared";锁不存在返回 "absent";存在但未达清理条件、或删除失败(OSError)返回 "left"(保持既有的人工核删指引面)。
|
|
292
|
+
def _clear_git_lock(git_dir_path: str, *, force: bool = False) -> str:
|
|
293
|
+
"""有界自清影子仓的 git 残留锁(三闸口径,2026-10-06 落地待裁项)。
|
|
294
|
+
|
|
295
|
+
背景(issue #63 台账「是否自动清理」):`_git` 超时 kill 自己的 git 后,
|
|
296
|
+
影子仓可能留下 `index.lock`,此前只给人工核删指引。现**有界**自清:
|
|
297
|
+
|
|
298
|
+
① **范围闸**:只碰本模块影子仓的 `<git_dir>/index.lock`——该仓由本模块
|
|
299
|
+
单属、轮级独占,无第三方 git 的合法持锁面。(**调用约定面**:影子仓
|
|
300
|
+
归属由调用方传入的 git_dir 保证;本函数机械面只保证对象名恒为
|
|
301
|
+
`<git_dir>/index.lock` 且须过②闸——独立复核 2026-10-06 观察 1 如实
|
|
302
|
+
标注。)
|
|
303
|
+
② **时机/年龄闸**:`force=True` 仅在本模块**刚 kill 掉自己超时的 git
|
|
304
|
+
子进程**后调用(此刻仓内任何锁只可能来自该进程或其更早残留);
|
|
305
|
+
非 force(每次 git 调用前的巡检)只在锁 **mtime 距今 ≥
|
|
306
|
+
`_GIT_LOCK_STALE_S`** 时清理(≫ 全部调用的 120s 超时上界,合法操作
|
|
307
|
+
结构性不可能持锁这么久);
|
|
308
|
+
③ **其余一律不删**(返回 "left"),异常消息保留人工核删指引。
|
|
309
|
+
|
|
310
|
+
返回 "cleared" / "left" / "absent"。
|
|
311
|
+
"""
|
|
312
|
+
lock = os.path.join(git_dir_path, _GIT_LOCK_NAME)
|
|
313
|
+
try:
|
|
314
|
+
st = os.stat(lock)
|
|
315
|
+
except OSError:
|
|
316
|
+
return "absent"
|
|
317
|
+
age = max(0.0, time.time() - st.st_mtime)
|
|
318
|
+
if not force and age < _GIT_LOCK_STALE_S:
|
|
319
|
+
return "left"
|
|
320
|
+
try:
|
|
321
|
+
os.remove(lock)
|
|
322
|
+
except OSError:
|
|
323
|
+
return "left"
|
|
324
|
+
try:
|
|
325
|
+
_append_ledger({"event": "git_lock_cleared", "lock": lock,
|
|
326
|
+
"age_s": round(age, 1), "force": bool(force)})
|
|
327
|
+
except OSError: # 台账失败不阻断 git 主链(与审计面同风格)
|
|
328
|
+
pass
|
|
329
|
+
return "cleared"
|
|
330
|
+
|
|
331
|
+
|
|
269
332
|
def _git(git_dir_path: str, work_tree: str, *args: str) -> subprocess.CompletedProcess:
|
|
270
333
|
"""git 调用的**唯一出口**:`--git-dir` 与 `--work-tree` 一律显式给。
|
|
271
334
|
|
|
272
335
|
`--work-tree` 不是装饰:物化与合并传数据根,对账+提交传影子——同一个版本库
|
|
273
336
|
两个工作树,靠这两个显式开关切换,**不依赖进程 cwd**(`hive/wm.py` 用 `-C`
|
|
274
337
|
是因为它的工作树就是版本库本身;这里两者分居,故用分裂形态)。
|
|
338
|
+
|
|
339
|
+
**有界化(issue #63)**——四件套缺一不可:
|
|
340
|
+
|
|
341
|
+
① `timeout=_GIT_TIMEOUT_S`:子进程**无界等待**是常驻循环停摆的直接成因
|
|
342
|
+
(实测停 9.5 小时:`SustainLoop` 单线程串行六档,任一档挂住即全停)。
|
|
343
|
+
② `stdin=subprocess.DEVNULL`:不指定 stdin 时子进程**继承父进程 stdin**
|
|
344
|
+
——DSH 常驻宿主的 stdin 是 **JSON-RPC 活管道**(不写入也不关闭),
|
|
345
|
+
任何读 stdin 的子进程会永久悬挂(实测:活管道下 6s 仍 poll=None;
|
|
346
|
+
DEVNULL 下 0.02s 立即 EOF)。先例形态:`md_cg/run_tests.py:105`。
|
|
347
|
+
③ `TimeoutExpired` **不吞**:转 `SleepGitTimeout`(`SleepError` 子类,
|
|
348
|
+
既有 `except SleepError` 面照旧收得住),消息带命令名与残留锁处置读数。
|
|
349
|
+
④ **残留锁有界自清**(2026-10-06 落地待裁项):调用前置巡检清超龄残留
|
|
350
|
+
(崩溃场景),超时 kill 后 `force` 清当前残留(见 `_clear_git_lock`
|
|
351
|
+
三闸口径)——消息里如实带「已清理/未清理」。
|
|
275
352
|
"""
|
|
276
353
|
argv = ["git", "--git-dir=" + git_dir_path, "--work-tree=" + work_tree]
|
|
277
354
|
argv.extend(args)
|
|
278
355
|
env = dict(os.environ, PYTHONUTF8="1")
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
356
|
+
# 前置巡检:清超龄残留锁(有界自清第一时机——崩溃残留,无 handler 可依)
|
|
357
|
+
_clear_git_lock(git_dir_path)
|
|
358
|
+
try:
|
|
359
|
+
return subprocess.run(argv, capture_output=True, text=True,
|
|
360
|
+
encoding="utf-8", errors="replace",
|
|
361
|
+
env=env, shell=False,
|
|
362
|
+
timeout=_GIT_TIMEOUT_S,
|
|
363
|
+
stdin=subprocess.DEVNULL)
|
|
364
|
+
except subprocess.TimeoutExpired as exc:
|
|
365
|
+
# 第二时机:本模块刚 kill 自己的 git——此刻仓内任何锁只可能是残留
|
|
366
|
+
_lock_state = _clear_git_lock(git_dir_path, force=True)
|
|
367
|
+
raise SleepGitTimeout(
|
|
368
|
+
"git %s 超时(%ss)已被 kill(命令:git %s)——"
|
|
369
|
+
"本次 git 操作未完成;残留锁处置:%s(%s)"
|
|
370
|
+
% (" ".join(args[:2]), _GIT_TIMEOUT_S, " ".join(args[:6]),
|
|
371
|
+
{"cleared": "已自动清理", "left": "未动,若后续报 lock 请人工核删",
|
|
372
|
+
"absent": "无残留锁"}.get(_lock_state, _lock_state),
|
|
373
|
+
os.path.join(git_dir_path, _GIT_LOCK_NAME))) from exc
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
# 生效条件:同 _git 的调用形态;returncode != 0 时抛 SleepError(消息取 stderr 为真值、为空则取 stdout,strip 后截 300 字符),为 0 时返回 stdout;_git 侧超时以 SleepGitTimeout(SleepError 子类,消息含「超时…已被 kill」)原样冒出——两条失败面**可区分**(rc≠0 = git 的裁决;超时 = 操作没完成)。
|
|
285
377
|
def _git_ok(git_dir_path: str, work_tree: str, *args: str) -> str:
|
|
286
378
|
r = _git(git_dir_path, work_tree, *args)
|
|
287
379
|
if r.returncode != 0:
|
|
@@ -380,26 +472,36 @@ def materialize(root: str = None, *, git_dir_path: str = None,
|
|
|
380
472
|
with FileLock(lock_path(), timeout=timeout, strict=False) as lk:
|
|
381
473
|
if not lk.acquired:
|
|
382
474
|
return _busy("materialize", timeout)
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
475
|
+
try:
|
|
476
|
+
rep = ensure_repo(root, G)
|
|
477
|
+
init = _stage_and_commit(G, root, "sleep: 基线(真源面入册)") \
|
|
478
|
+
if not _has_head(G, root) else {"committed": False,
|
|
479
|
+
"reason": "已有基线提交"}
|
|
480
|
+
if os.path.isdir(S):
|
|
481
|
+
r = _git(G, root, "worktree", "remove", "--force", S)
|
|
482
|
+
if r.returncode != 0:
|
|
483
|
+
# 非注册工作树(上一次运行被杀、目录残留)→ 直接摘目录
|
|
484
|
+
shutil.rmtree(S, ignore_errors=True)
|
|
485
|
+
r = _git(G, root, "worktree", "add", "-b", branch, S, BASE_BRANCH)
|
|
389
486
|
if r.returncode != 0:
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
487
|
+
r2 = _git(G, root, "worktree", "add", S, branch)
|
|
488
|
+
if r2.returncode != 0:
|
|
489
|
+
return {"ok": False, "busy": False, "lock_held": True,
|
|
490
|
+
"phase": "materialize", "lock": lock_path(),
|
|
491
|
+
"error": (r2.stderr or r2.stdout).strip()[:300],
|
|
492
|
+
"hint": "影子分支/目录未能挂上"}
|
|
493
|
+
reused = True
|
|
494
|
+
else:
|
|
495
|
+
reused = False
|
|
496
|
+
except SleepGitTimeout as e:
|
|
497
|
+
# 超时与「物化失败」分开记账(issue #63):超时 = git 操作**没完成**
|
|
498
|
+
# (已被 kill),与 rc≠0 的「git 的裁决失败」不是一回事——
|
|
499
|
+
# run_cycle 据此写「物化超时:…」(见 mat_fail 分流)。
|
|
500
|
+
return {"ok": False, "busy": False, "lock_held": True,
|
|
501
|
+
"phase": "materialize", "lock": lock_path(),
|
|
502
|
+
"timeout": True, "error": str(e),
|
|
503
|
+
"hint": ("git 子进程超时已被 kill:本轮物化放弃;"
|
|
504
|
+
"残留锁核删指引见 error")}
|
|
403
505
|
after = source_face_hashes(root)
|
|
404
506
|
delta = face_delta(before, after)
|
|
405
507
|
# ok = **物化动作成功**(影子已挂上);真源面漂移与否是另一个读数
|
|
@@ -446,9 +548,17 @@ def reconcile_and_commit(root: str = None, *, git_dir_path: str = None,
|
|
|
446
548
|
"error": "影子工作树不存在:先 materialize", "shadow": S}
|
|
447
549
|
# 链式工作树用自己的 gitdir(否则 HEAD/index 取主那一份,提交会落到 main)
|
|
448
550
|
GS = worktree_git_dir(S) or G
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
551
|
+
try:
|
|
552
|
+
br = branch or _git_ok(GS, S, "rev-parse", "--abbrev-ref",
|
|
553
|
+
"HEAD").strip()
|
|
554
|
+
msg = "sleep: %s 轮对账提交" % (batch or _stamp())
|
|
555
|
+
res = _stage_and_commit(GS, S, msg)
|
|
556
|
+
except SleepGitTimeout as e:
|
|
557
|
+
# 超时(issue #63):本阶段未完成——与「Δ 为空/未提交」等读数分开报。
|
|
558
|
+
return {"ok": False, "busy": False, "lock_held": True,
|
|
559
|
+
"phase": "reconcile_and_commit", "git_dir": G,
|
|
560
|
+
"worktree_git_dir": GS, "shadow": S, "timeout": True,
|
|
561
|
+
"error": str(e), "lock": lock_path()}
|
|
452
562
|
after = source_face_hashes(root)
|
|
453
563
|
delta = face_delta(before, after)
|
|
454
564
|
return {"ok": not (delta["added"] or delta["removed"] or delta["changed"]),
|
|
@@ -481,27 +591,40 @@ def merge(root: str = None, *, git_dir_path: str = None, branch: str,
|
|
|
481
591
|
with FileLock(lock_path(), timeout=timeout, strict=False) as lk:
|
|
482
592
|
if not lk.acquired:
|
|
483
593
|
return _busy("merge", timeout)
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
594
|
+
try:
|
|
595
|
+
cur = _git_ok(G, root, "rev-parse", "--abbrev-ref", "HEAD").strip()
|
|
596
|
+
if cur != BASE_BRANCH:
|
|
597
|
+
return {"ok": False, "busy": False, "lock_held": True,
|
|
598
|
+
"phase": "merge", "lock": lock_path(), "head": cur,
|
|
599
|
+
"error": "当前 HEAD 在 %s,merge 须在 %s 上执行"
|
|
600
|
+
% (cur, BASE_BRANCH)}
|
|
601
|
+
r = _git(G, root, "merge", "--no-ff", branch, "-m",
|
|
602
|
+
"sleep: merge %s into %s" % (branch, BASE_BRANCH))
|
|
603
|
+
if r.returncode != 0:
|
|
604
|
+
out = (r.stdout + r.stderr).strip()
|
|
605
|
+
return {"ok": False, "busy": False, "lock_held": True,
|
|
606
|
+
"phase": "merge", "lock": lock_path(), "shadow": S,
|
|
607
|
+
"conflict": "CONFLICT" in out, "error": out[:500],
|
|
608
|
+
"source_face_delta": face_delta(before,
|
|
609
|
+
source_face_hashes(root)),
|
|
610
|
+
"hint": ("冲突不自动解决(照 hive/wm.py 既有裁决):"
|
|
611
|
+
"人工/LLM 裁决后 git add + git commit 收口,"
|
|
612
|
+
"或 git merge --abort 放弃本次合并")}
|
|
613
|
+
head = _git_ok(G, root, "rev-parse", "HEAD").strip()
|
|
614
|
+
except SleepGitTimeout as e:
|
|
615
|
+
# 超时(issue #63):合并**未完成**(不是冲突、也不是被拒)——
|
|
616
|
+
# 单独一档读数,冲突裁决路径不受影响。
|
|
617
|
+
return {"ok": False, "busy": False, "lock_held": True,
|
|
618
|
+
"phase": "merge", "lock": lock_path(), "shadow": S,
|
|
619
|
+
"timeout": True, "error": str(e),
|
|
620
|
+
"hint": ("git 子进程超时已被 kill:合并未完成;"
|
|
621
|
+
"残留锁核删指引见 error")}
|
|
499
622
|
after = source_face_hashes(root)
|
|
500
623
|
delta = face_delta(before, after)
|
|
501
624
|
return {"ok": True, "busy": False, "lock_held": True, "phase": "merge",
|
|
502
|
-
"lock": lock_path(), "merged": branch,
|
|
503
|
-
"
|
|
504
|
-
"
|
|
625
|
+
"lock": lock_path(), "merged": branch, "commit": head,
|
|
626
|
+
"source_face_delta": delta,
|
|
627
|
+
"main_written": delta["added"] + delta["changed"]}
|
|
505
628
|
|
|
506
629
|
|
|
507
630
|
# 生效条件:commit 为版本库中存在的提交(或分支名);在根级锁内以其父提交数判是否 merge(git rev-list --parents -n1 字段数 > 2 即 merge),是 merge 则附 -m 1,执行 git revert --no-edit <参数> <commit>;returncode != 0 时返回 ok=False 与 conflict/error/hint,成功返回 ok=True、reverted、commit(新提交)与 source_face_delta。**只走 revert,不提供抹历史的强推档**。
|
|
@@ -521,22 +644,31 @@ def revert(commit: str, root: str = None, *, git_dir_path: str = None,
|
|
|
521
644
|
with FileLock(lock_path(), timeout=timeout, strict=False) as lk:
|
|
522
645
|
if not lk.acquired:
|
|
523
646
|
return _busy("revert", timeout)
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
args
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
647
|
+
try:
|
|
648
|
+
parents = _git_ok(G, root, "rev-list", "--parents", "-n1",
|
|
649
|
+
commit).split()
|
|
650
|
+
args = ["revert", "--no-edit"]
|
|
651
|
+
if len(parents) > 2:
|
|
652
|
+
args += ["-m", "1"]
|
|
653
|
+
args.append(commit)
|
|
654
|
+
r = _git(G, root, *args)
|
|
655
|
+
if r.returncode != 0:
|
|
656
|
+
out = (r.stdout + r.stderr).strip()
|
|
657
|
+
return {"ok": False, "busy": False, "lock_held": True,
|
|
658
|
+
"phase": "revert", "lock": lock_path(),
|
|
659
|
+
"conflict": "CONFLICT" in out, "error": out[:500],
|
|
660
|
+
"hint": ("冲突不自动解决;处理后可 git revert --continue "
|
|
661
|
+
"/ --abort")}
|
|
662
|
+
head = _git_ok(G, root, "rev-parse", "HEAD").strip()
|
|
663
|
+
except SleepGitTimeout as e:
|
|
664
|
+
return {"ok": False, "busy": False, "lock_held": True,
|
|
665
|
+
"phase": "revert", "lock": lock_path(), "timeout": True,
|
|
666
|
+
"error": str(e),
|
|
667
|
+
"hint": ("git 子进程超时已被 kill:回滚未完成;"
|
|
668
|
+
"残留锁核删指引见 error")}
|
|
536
669
|
after = source_face_hashes(root)
|
|
537
670
|
return {"ok": True, "busy": False, "lock_held": True, "phase": "revert",
|
|
538
|
-
"lock": lock_path(), "reverted": commit,
|
|
539
|
-
"commit": _git_ok(G, root, "rev-parse", "HEAD").strip(),
|
|
671
|
+
"lock": lock_path(), "reverted": commit, "commit": head,
|
|
540
672
|
"merge_commit": len(parents) > 2,
|
|
541
673
|
"source_face_delta": face_delta(before, after)}
|
|
542
674
|
|
|
@@ -981,10 +1113,20 @@ def reconcile_gated(root: str = None, *, git_dir_path: str = None,
|
|
|
981
1113
|
gated = gate_delta(cgm, delta, baseline_face=baseline_face,
|
|
982
1114
|
shadow_root=S)
|
|
983
1115
|
GS = worktree_git_dir(S) or G
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
1116
|
+
try:
|
|
1117
|
+
br = branch or _git_ok(GS, S, "rev-parse", "--abbrev-ref",
|
|
1118
|
+
"HEAD").strip()
|
|
1119
|
+
rels = _stage_set(delta, gated["accepted"])
|
|
1120
|
+
res = _commit_paths(GS, S, rels,
|
|
1121
|
+
"sleep: %s 轮对账提交" % (batch or _stamp()))
|
|
1122
|
+
except SleepGitTimeout as e:
|
|
1123
|
+
# 超时(issue #63):四闸裁决读数(delta / accepted / conflicts /
|
|
1124
|
+
# rejected)照带——只是入册没完成。
|
|
1125
|
+
return {"ok": False, "busy": False, "lock_held": True,
|
|
1126
|
+
"phase": "reconcile_gated", "git_dir": G,
|
|
1127
|
+
"worktree_git_dir": GS, "shadow": S, "timeout": True,
|
|
1128
|
+
"error": str(e), "lock": lock_path(),
|
|
1129
|
+
"delta": delta, **gated}
|
|
988
1130
|
after = source_face_hashes(root)
|
|
989
1131
|
return {"ok": True, "busy": False, "lock_held": True,
|
|
990
1132
|
"phase": "reconcile_gated", "git_dir": G, "worktree_git_dir": GS,
|
|
@@ -1061,7 +1203,14 @@ def merge_cycle(cg_main, root: str = None, *, git_dir_path: str = None,
|
|
|
1061
1203
|
with FileLock(lock_path(), timeout=timeout, strict=False) as lk:
|
|
1062
1204
|
if not lk.acquired:
|
|
1063
1205
|
return _busy("merge_cycle", timeout)
|
|
1064
|
-
|
|
1206
|
+
try:
|
|
1207
|
+
cur = _git_ok(G, root, "rev-parse", "--abbrev-ref", "HEAD").strip()
|
|
1208
|
+
except SleepGitTimeout as e:
|
|
1209
|
+
return {"ok": False, "busy": False, "lock_held": True,
|
|
1210
|
+
"phase": "merge_cycle", "lock": lock_path(),
|
|
1211
|
+
"branch": branch, "timeout": True, "error": str(e),
|
|
1212
|
+
"hint": ("git 子进程超时已被 kill:合并尚未开始;"
|
|
1213
|
+
"残留锁核删指引见 error")}
|
|
1065
1214
|
if cur != BASE_BRANCH:
|
|
1066
1215
|
return {"ok": False, "busy": False, "lock_held": True,
|
|
1067
1216
|
"phase": "merge_cycle", "lock": lock_path(), "head": cur,
|
|
@@ -1079,39 +1228,54 @@ def merge_cycle(cg_main, root: str = None, *, git_dir_path: str = None,
|
|
|
1079
1228
|
# 层迁移会把源层目录搬空,`pathspecs` 滤空目录后就把「旧位置删除」漏掉,
|
|
1080
1229
|
# 主库 HEAD 会留着旧文件 ⇒ 与影子分支的树不同(本机实测过这个漏)。
|
|
1081
1230
|
ps = _stage_set(delta or {}, accepted or [])
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1231
|
+
try:
|
|
1232
|
+
if ps:
|
|
1233
|
+
_git_ok(G, root, "add", "--", *ps)
|
|
1234
|
+
if _git(G, root, "diff", "--cached", "--quiet").returncode != 0:
|
|
1235
|
+
_git_ok(G, root, "commit", "-m",
|
|
1236
|
+
"sleep: %s 语义重放 Δ" % (batch or _stamp()))
|
|
1237
|
+
# **本轮内容所在的提交**(回退入口):按 §4.4 的次序,内容由
|
|
1238
|
+
# 「语义重放」先写进主库并提交,随后的 `--no-ff` 合并只是**拓扑记录**
|
|
1239
|
+
# (其树与第一父提交相同 ⇒ `git revert -m 1 <合并提交>` 对内容零影响,
|
|
1240
|
+
# 回退要指向本提交;ops 文档据此登记)。
|
|
1241
|
+
out["round_commit"] = _git_ok(G, root, "rev-parse",
|
|
1242
|
+
"HEAD").strip()
|
|
1243
|
+
out["replay_commit"] = _git_ok(G, root, "rev-parse", "HEAD").strip()
|
|
1244
|
+
# 仅当两侧树**完全相同**才合并(否则让 git 的文本自动合并去裁决就会绕过四闸)
|
|
1245
|
+
same = _git(G, root, "diff", "--quiet", branch,
|
|
1246
|
+
"HEAD").returncode == 0
|
|
1247
|
+
if not same:
|
|
1248
|
+
changed = [ln.strip() for ln in _git(
|
|
1249
|
+
G, root, "diff", "--name-only", branch,
|
|
1250
|
+
"HEAD").stdout.splitlines() if ln.strip()]
|
|
1251
|
+
out.update(deferred="tree_divergence",
|
|
1252
|
+
note=("影子分支与重放后的主库树不同 ⇒ 挂起不合并"
|
|
1253
|
+
"(不自动解决)"),
|
|
1254
|
+
conflict_ids=sorted({os.path.basename(p)[:-3]
|
|
1255
|
+
for p in changed}))
|
|
1256
|
+
out["source_face_delta"] = face_delta(before,
|
|
1257
|
+
source_face_hashes(root))
|
|
1258
|
+
return out
|
|
1259
|
+
r = _git(G, root, "merge", "--no-ff", branch, "-m",
|
|
1260
|
+
"sleep: merge %s into %s" % (branch, BASE_BRANCH))
|
|
1261
|
+
if r.returncode != 0:
|
|
1262
|
+
txt = (r.stdout + r.stderr).strip()
|
|
1263
|
+
out.update(ok=False, conflict="CONFLICT" in txt, error=txt[:500],
|
|
1264
|
+
hint=("冲突不自动解决(照 hive/wm.py 既有裁决):"
|
|
1265
|
+
"人工裁决后 git add + git commit 收口,"
|
|
1266
|
+
"或放弃本次合并"))
|
|
1267
|
+
out["source_face_delta"] = face_delta(before,
|
|
1268
|
+
source_face_hashes(root))
|
|
1269
|
+
return out
|
|
1270
|
+
out["merge_commit"] = _git_ok(G, root, "rev-parse", "HEAD").strip()
|
|
1271
|
+
except SleepGitTimeout as e:
|
|
1272
|
+
# 超时(issue #63):重放后的入册/合并**未完成**(既非冲突、也非
|
|
1273
|
+
# 树分歧)——单独一档读数,与上面两条裁决路径分开。
|
|
1274
|
+
out.update(ok=False, timeout=True, error=str(e),
|
|
1275
|
+
hint=("git 子进程超时已被 kill:入册/合并未完成;"
|
|
1276
|
+
"残留锁核删指引见 error"))
|
|
1112
1277
|
out["source_face_delta"] = face_delta(before, source_face_hashes(root))
|
|
1113
1278
|
return out
|
|
1114
|
-
out["merge_commit"] = _git_ok(G, root, "rev-parse", "HEAD").strip()
|
|
1115
1279
|
out["merged"] = branch
|
|
1116
1280
|
after = source_face_hashes(root)
|
|
1117
1281
|
dlt = face_delta(before, after)
|
|
@@ -1270,13 +1434,18 @@ def run_cycle(cg=None, *, root: str = None, batch: str = None,
|
|
|
1270
1434
|
phases["materialize"] = {k: m.get(k) for k in
|
|
1271
1435
|
("ok", "face_stable", "busy", "branch", "shadow",
|
|
1272
1436
|
"git_dir", "baseline", "source_face_delta",
|
|
1273
|
-
"reason", "error")}
|
|
1437
|
+
"reason", "error", "timeout")}
|
|
1274
1438
|
if not m.get("ok"):
|
|
1275
|
-
# 物化**未完成**(取不到根级锁 /
|
|
1276
|
-
#
|
|
1439
|
+
# 物化**未完成**(取不到根级锁 / 影子未能挂上 / git 超时)——与
|
|
1440
|
+
# 「门拦」区分记账:门拦是「按设计不动手」,这里是「想动手却没做成」,
|
|
1441
|
+
# 用户要能一眼分开;超时再单列一档文案(issue #63:物化超时 ≠
|
|
1442
|
+
# 物化失败——前者是操作没完成、后者是 git 的裁决)。
|
|
1277
1443
|
mat_fail = ("物化未完成:根级锁被占(另一进程在周期内)"
|
|
1278
1444
|
if m.get("busy")
|
|
1279
|
-
else "
|
|
1445
|
+
else ("物化超时:%s" % (m.get("error") or "未知原因")
|
|
1446
|
+
if m.get("timeout")
|
|
1447
|
+
else "物化未完成:%s" % (m.get("error")
|
|
1448
|
+
or "未知原因")))
|
|
1280
1449
|
if m.get("ok"):
|
|
1281
1450
|
# **物化成功即迭代**(issue #60):`ok` 现在只说「影子挂上了没」;
|
|
1282
1451
|
# 物化窗口内主库真源面的漂移另记 `face_stable`,**不阻断**迭代——
|
|
@@ -1292,7 +1461,7 @@ def run_cycle(cg=None, *, root: str = None, batch: str = None,
|
|
|
1292
1461
|
cg=cg, timeout=timeout)
|
|
1293
1462
|
phases["reconcile"] = {k: rc.get(k) for k in
|
|
1294
1463
|
("committed", "commit", "branch", "reason",
|
|
1295
|
-
"busy")}
|
|
1464
|
+
"busy", "timeout", "error")}
|
|
1296
1465
|
delta = rc.get("delta") or {}
|
|
1297
1466
|
digest = {"n": len(delta), "ids": sorted(delta)[:20],
|
|
1298
1467
|
"kinds": {k: sum(1 for v in delta.values() if v["kind"] == k)
|
|
@@ -1313,7 +1482,8 @@ def run_cycle(cg=None, *, root: str = None, batch: str = None,
|
|
|
1313
1482
|
("ok", "merged", "merge_commit",
|
|
1314
1483
|
"round_commit", "replay_commit",
|
|
1315
1484
|
"deferred", "conflict", "conflict_ids",
|
|
1316
|
-
"error", "main_written", "busy"
|
|
1485
|
+
"error", "main_written", "busy",
|
|
1486
|
+
"timeout")}
|
|
1317
1487
|
phases["merge"]["replayed"] = (mg.get("replayed") or {}).get("written")
|
|
1318
1488
|
rec["merged"] = bool(mg.get("ok")) and not mg.get("deferred")
|
|
1319
1489
|
if mg.get("deferred"):
|