cursor Init 完善多人协同changelog,以及godot相关基础skill和代码规范
This commit is contained in:
252
.cursor/changelog/tools/migrate_changelog.py
Normal file
252
.cursor/changelog/tools/migrate_changelog.py
Normal file
@@ -0,0 +1,252 @@
|
||||
#!/usr/bin/env python3
|
||||
"""migrate_changelog.py — 一次性迁移:把旧的三层 changelog 文件拆成 fragment。
|
||||
|
||||
把现有 ``changelog-full.md`` 按 ``### [CL-...]`` 头切成一条一文件的 fragment,
|
||||
写到 ``entries/legacy/<id>.md``;L3/L2 内容尽量从旧的 headlines/recent 中回收,
|
||||
回收不到时用标题兜底。迁移后由 ``changelog_build.py`` 重新生成视图。
|
||||
|
||||
幂等:重复运行会覆盖 entries/legacy/ 下的同名文件。只用标准库。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ENTRY_HEADER_RE = re.compile(r"^### \[(CL-[^\]]+)\]\s*(.*)$")
|
||||
DATE_TITLE_RE = re.compile(r"^(\d{4}-\d{2}-\d{2}[^—]*?)\s*—\s*(.*)$")
|
||||
ID_DIGITS_RE = re.compile(r"^CL-(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})")
|
||||
HEADLINE_LINE_RE = re.compile(r"^- \[(CL-[^\]]+)\]\s*(.*)$")
|
||||
MERGE_TAG_RE = re.compile(r"\s*\[merge:[^\]]+\]\s*$")
|
||||
|
||||
TYPE_BY_TAG = {
|
||||
"bugfix": "fix",
|
||||
"fix": "fix",
|
||||
"feature": "feat",
|
||||
"feat": "feat",
|
||||
"docs": "docs",
|
||||
"refactor": "refactor",
|
||||
"chore": "chore",
|
||||
}
|
||||
|
||||
|
||||
def split_entries(full_text: str) -> list[tuple[str, list[str]]]:
|
||||
"""返回 [(id, [lines...]), ...],lines 含 header 行。"""
|
||||
lines = full_text.splitlines()
|
||||
entries: list[tuple[str, list[str]]] = []
|
||||
cur_id: str | None = None
|
||||
cur: list[str] = []
|
||||
for line in lines:
|
||||
m = ENTRY_HEADER_RE.match(line)
|
||||
if m:
|
||||
if cur_id is not None:
|
||||
entries.append((cur_id, cur))
|
||||
cur_id = m.group(1)
|
||||
cur = [line]
|
||||
elif cur_id is not None:
|
||||
cur.append(line)
|
||||
if cur_id is not None:
|
||||
entries.append((cur_id, cur))
|
||||
return entries
|
||||
|
||||
|
||||
def parse_headlines(text: str) -> dict[str, str]:
|
||||
out: dict[str, str] = {}
|
||||
for line in text.splitlines():
|
||||
m = HEADLINE_LINE_RE.match(line)
|
||||
if m:
|
||||
body = MERGE_TAG_RE.sub("", m.group(2)).strip()
|
||||
out[m.group(1)] = body
|
||||
return out
|
||||
|
||||
|
||||
def parse_recent_summaries(text: str) -> dict[str, str]:
|
||||
out: dict[str, str] = {}
|
||||
cur_id: str | None = None
|
||||
for line in text.splitlines():
|
||||
m = ENTRY_HEADER_RE.match(line)
|
||||
if m:
|
||||
cur_id = m.group(1)
|
||||
continue
|
||||
if cur_id and line.startswith("- **summary**:"):
|
||||
out[cur_id] = line[len("- **summary**:"):].strip()
|
||||
return out
|
||||
|
||||
|
||||
def strip_wrap_backticks(s: str) -> str:
|
||||
return re.sub(r"^`([^`]+)`$", r"\1", s.strip())
|
||||
|
||||
|
||||
def derive_date(entry_id: str, header_rest: str) -> tuple[str, str]:
|
||||
"""返回 (date, title)。"""
|
||||
m = DATE_TITLE_RE.match(header_rest)
|
||||
if m:
|
||||
return m.group(1).strip(), m.group(2).strip()
|
||||
# 无 date —— 标题就是整段,date 从 id 推
|
||||
dm = ID_DIGITS_RE.match(entry_id)
|
||||
if dm:
|
||||
y, mo, d, hh, mm = dm.groups()
|
||||
return f"{y}-{mo}-{d} {hh}:{mm}", header_rest.strip()
|
||||
return "", header_rest.strip()
|
||||
|
||||
|
||||
def parse_body(lines: list[str]) -> dict[str, object]:
|
||||
"""解析一个 L1 条目的 body(不含 header 行)。"""
|
||||
tags: list[str] = []
|
||||
affected: list[str] = []
|
||||
merge_source = ""
|
||||
source_chat = ""
|
||||
l1_lines: list[str] = []
|
||||
|
||||
i = 0
|
||||
n = len(lines)
|
||||
while i < n:
|
||||
line = lines[i]
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("- **merge_source**:"):
|
||||
merge_source = stripped[len("- **merge_source**:"):].strip()
|
||||
i += 1
|
||||
continue
|
||||
if stripped.startswith("- **tags**:"):
|
||||
raw = stripped[len("- **tags**:"):].strip()
|
||||
tags = [strip_wrap_backticks(t) for t in raw.split(",") if t.strip()]
|
||||
i += 1
|
||||
continue
|
||||
if stripped.startswith("- **affected_files**:"):
|
||||
inline = stripped[len("- **affected_files**:"):].strip()
|
||||
if inline:
|
||||
affected = [strip_wrap_backticks(x) for x in re.split(r"[、,]", inline) if x.strip()]
|
||||
i += 1
|
||||
else:
|
||||
i += 1
|
||||
while i < n and re.match(r"^\s+-\s+", lines[i]):
|
||||
item = lines[i].strip()[1:].strip()
|
||||
affected.append(strip_wrap_backticks(item))
|
||||
i += 1
|
||||
continue
|
||||
if stripped.startswith("- **source_chat**:"):
|
||||
source_chat = stripped[len("- **source_chat**:"):].strip()
|
||||
i += 1
|
||||
continue
|
||||
l1_lines.append(line)
|
||||
i += 1
|
||||
|
||||
# 去掉首尾空行
|
||||
while l1_lines and not l1_lines[0].strip():
|
||||
l1_lines.pop(0)
|
||||
while l1_lines and not l1_lines[-1].strip():
|
||||
l1_lines.pop()
|
||||
|
||||
return {
|
||||
"tags": tags,
|
||||
"affected_files": affected,
|
||||
"merge_source": merge_source,
|
||||
"source_chat": source_chat,
|
||||
"l1": "\n".join(l1_lines),
|
||||
}
|
||||
|
||||
|
||||
def infer_type(tags: list[str]) -> str:
|
||||
for t in tags:
|
||||
if t.lower() in TYPE_BY_TAG:
|
||||
return TYPE_BY_TAG[t.lower()]
|
||||
return ""
|
||||
|
||||
|
||||
def fm_scalar(key: str, value: str) -> str:
|
||||
return f"{key}: {value}".rstrip()
|
||||
|
||||
|
||||
def fm_list(key: str, items: list[str]) -> str:
|
||||
if not items:
|
||||
return f"{key}:"
|
||||
body = "\n".join(f" - {x}" for x in items)
|
||||
return f"{key}:\n{body}"
|
||||
|
||||
|
||||
def build_fragment_text(
|
||||
*, fid: str, date: str, author: str, type_: str, merge_source: str,
|
||||
title: str, source_chat: str, tags: list[str], affected: list[str],
|
||||
l3: str, l2: str, l1: str,
|
||||
) -> str:
|
||||
fm = [
|
||||
"---",
|
||||
fm_scalar("id", fid),
|
||||
fm_scalar("date", date),
|
||||
fm_scalar("author", author),
|
||||
fm_scalar("author_name", ""),
|
||||
fm_scalar("type", type_),
|
||||
fm_scalar("merge_source", merge_source),
|
||||
fm_scalar("status", "active"),
|
||||
fm_scalar("superseded_by", ""),
|
||||
fm_scalar("title", title),
|
||||
fm_scalar("source_chat", source_chat),
|
||||
fm_list("tags", tags),
|
||||
fm_list("affected_files", affected),
|
||||
"---",
|
||||
]
|
||||
parts = [
|
||||
"\n".join(fm),
|
||||
"",
|
||||
"<!-- L3 -->",
|
||||
l3,
|
||||
"",
|
||||
"<!-- L2 -->",
|
||||
l2,
|
||||
"",
|
||||
"<!-- L1 -->",
|
||||
l1,
|
||||
"",
|
||||
]
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
argv = argv if argv is not None else sys.argv[1:]
|
||||
changelog_dir = Path(argv[0]) if argv else Path(__file__).resolve().parent.parent
|
||||
author = argv[1] if len(argv) > 1 else "legacy"
|
||||
|
||||
full_path = changelog_dir / "changelog-full.md"
|
||||
recent_path = changelog_dir / "changelog-recent.md"
|
||||
headlines_path = changelog_dir / "changelog-headlines.md"
|
||||
if not full_path.exists():
|
||||
raise SystemExit(f"找不到 {full_path}")
|
||||
|
||||
full_text = full_path.read_text(encoding="utf-8")
|
||||
headlines = parse_headlines(headlines_path.read_text(encoding="utf-8")) if headlines_path.exists() else {}
|
||||
recent = parse_recent_summaries(recent_path.read_text(encoding="utf-8")) if recent_path.exists() else {}
|
||||
|
||||
entries = split_entries(full_text)
|
||||
out_dir = changelog_dir / "entries" / author
|
||||
out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
count = 0
|
||||
for fid, lines in entries:
|
||||
header_rest = ENTRY_HEADER_RE.match(lines[0]).group(2)
|
||||
date, title = derive_date(fid, header_rest)
|
||||
parsed = parse_body(lines[1:])
|
||||
l3 = headlines.get(fid, "").strip() or title
|
||||
l2 = recent.get(fid, "").strip() or l3
|
||||
text = build_fragment_text(
|
||||
fid=fid,
|
||||
date=date,
|
||||
author=author,
|
||||
type_=infer_type(parsed["tags"]),
|
||||
merge_source=str(parsed["merge_source"]),
|
||||
title=title,
|
||||
source_chat=str(parsed["source_chat"]),
|
||||
tags=list(parsed["tags"]),
|
||||
affected=list(parsed["affected_files"]),
|
||||
l3=l3,
|
||||
l2=l2,
|
||||
l1=str(parsed["l1"]),
|
||||
)
|
||||
(out_dir / f"{fid}.md").write_text(text, encoding="utf-8", newline="\n")
|
||||
count += 1
|
||||
|
||||
print(f"迁移完成:{count} 条 → {out_dir}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user