Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .githooks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,16 @@ make hooks # 等同 git config core.hooksPath .githooks
| hook | 时机 | 做什么 | 量级 |
|---|---|---|---|
| `commit-msg` | 提交时 | 强制 `type(scope): subject` 格式 | 毫秒级 |
| `pre-commit` | 提交前 | 检查 staged `.lvt` 测试文件新增行是否含 `\TEST{...~...}` / `\BEGINTEST{...~...}` / `\TYPE{...~...}` 误用(见 [#893]) | 毫秒级 |
| `pre-push` | 推送前 + 推送后 | ① 可选 l3build sanity(`LATEX_PREPUSH_BUILD=1` 启用);② self-wrapping inner push;③ 阻塞等 CI + 抓 PR 新评论活动 | 推送时秒级 + CI 等待 |

- 所有 hook 在 CI 环境(`$CI`/`$GITHUB_ACTIONS`)自动短路。
- `pre-commit` 的同款检查 CI 上由 `.github/workflows/lint-test-files.yml` 跑,确保未装 hook 的 PR 也被拦截。
- 完整测试(`l3build check`)**不在 hook 里跑** —— LaTeX 单包全量 check 动辄 20min,那是 CI 的事。
- 紧急跳过:`git commit --no-verify` / `git push --no-verify`(仅紧急情况)。

[#893]: https://github.com/CTeX-org/ctex-kit/issues/893

## `commit-msg` 允许的 type

`feat fix doc docs test chore perf refactor ci bench build revert`
Expand Down
41 changes: 36 additions & 5 deletions .githooks/check-pr-ci.sh
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,34 @@ if [ -n "$head_committed_at" ]; then
' 2>/dev/null || true)"
fi

# 2) 未解决的 review thread
# 一次 gh repo view 拿 owner+name, 喂给 GraphQL 而非两次子 shell.
unresolved_threads=""
# 提前拿 owner+name 给后面 1b 和 2 两段共用 (一次 API 调用).
repo_owner_name="$(gh repo view --json owner,name --jq '"\(.owner.login)\t\(.name)"' 2>/dev/null)"
repo_owner=""; repo_name=""
if [ -n "$repo_owner_name" ]; then
repo_owner="${repo_owner_name%$'\t'*}"
repo_name="${repo_owner_name#*$'\t'}"
fi

# 1b) push 之后是否有新 issue comment. agentic-pr-review bot 用
# `gh pr comment` (issue comment API) 发评论, 不走 formal review
# (.reviews[]) — 上面 (1) 永远看不到 bot 的增量审核. 这里补一刀:
# 用 REST API 拿 user.type, 过滤所有 Bot 作者 (不硬编码具体 login —
# 兼容 Dependabot / 自定义 GitHub App / 未来其它 bot).
new_bot_comment_after_push=""
if [ -n "$head_committed_at" ] && [ -n "$repo_owner" ]; then
new_bot_comment_after_push="$(gh api \
"repos/${repo_owner}/${repo_name}/issues/${pr_number}/comments?per_page=100" \
--jq --arg t "$head_committed_at" '
.[]?
| select(.user.type == "Bot")
| select((.created_at // "") > $t)
| "\(.user.login)\tCOMMENT\t\(.created_at)\t\(.html_url)"
' 2>/dev/null || true)"
fi

# 2) 未解决的 review thread (复用上面拿到的 owner+name)
unresolved_threads=""
if [ -n "$repo_owner" ]; then
unresolved_threads="$(gh api graphql -f query='
query($owner:String!, $repo:String!, $pr:Int!) {
repository(owner:$owner, name:$repo) {
Expand All @@ -149,18 +170,28 @@ if [ -n "$repo_owner_name" ]; then
fi

# 状态报告: 区分三档
if [ -n "$new_review_after_push" ] || [ -n "$unresolved_threads" ]; then
if [ -n "$new_review_after_push" ] || [ -n "$new_bot_comment_after_push" ] \
|| [ -n "$unresolved_threads" ]; then
log ""
log "════════════════════════════════════════════════════════════"
log " ⚠ post-push: CI passed for PR #${pr_number}, but review activity pending"
if [ -n "$new_review_after_push" ]; then
log ""
log " New review(s) submitted after this push:"
log " New formal review(s) submitted after this push:"
while IFS=$'\t' read -r who state when; do
[ -z "$who" ] && continue
log " • ${who} [${state}] @${when}"
done <<< "$new_review_after_push"
fi
if [ -n "$new_bot_comment_after_push" ]; then
log ""
log " New bot comment(s) (agentic-pr-review etc.) after this push:"
while IFS=$'\t' read -r who state when url; do
[ -z "$who" ] && continue
log " • ${who} [${state}] @${when}"
[ -n "$url" ] && log " ${url}"
done <<< "$new_bot_comment_after_push"
fi
if [ -n "$unresolved_threads" ]; then
log ""
log " Unresolved review thread(s):"
Expand Down
165 changes: 165 additions & 0 deletions .githooks/check-test-tilde.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
#!/usr/bin/env bash
# check-test-tilde.sh — 检查 .lvt 测试文件里 \TEST{...} / \BEGINTEST{...} /
# \TYPE{...} 命令大括号内是否含 `~` 误用 (issue #893).
#
# 重要约束: 仅在 **\ExplSyntaxOff 段** 报错. 在 \ExplSyntaxOn 段内 `~` 是
# 合法的 expl3 空格 (catcode 10), 而普通空格则被 ignore (catcode 9),
# 不能盲目替换.
#
# 用法 (两种形式之一):
# git diff --cached -U0 -- '*.lvt' | .githooks/check-test-tilde.sh
# git diff <base> HEAD -U0 -- '*.lvt' | .githooks/check-test-tilde.sh
#
# 仅检 diff 中 `+` 开头 (新增行) 的命中, 不动存量.
#
# 实现:
# 1) 用 awk 从 diff 中提取 (file, lineno, line) 三元组的命中候选
# 2) 对每个候选, 读取工作目录中该文件, 算该行是否在 ExplSyntaxOff 段
# 3) 只报 ExplSyntaxOff 段命中
set -uo pipefail

# Step 1: 从 stdin 的 diff 找命中候选, 输出 "<file>\t<lineno>\t<line>"
# 注: 命令内的 `[^{}]*` 与 fix-test-tilde.py 的 CMD_PATTERN 保持一致 — 不
# 跨嵌套大括号. 嵌套 (如 \TYPE{\foo{bar~baz}}) 不会命中, 但这是 false
# negative 安全方向 (宁可漏报不误报). 当前测试文件中未见这种模式.
candidates=$(awk '
/^\+\+\+ b\// {
file = substr($0, 7)
if (file ~ /\.lvt$/) { interesting = 1 } else { interesting = 0 }
next
}
/^@@/ {
if (match($0, /\+[0-9]+/)) {
newline = substr($0, RSTART + 1, RLENGTH - 1) + 0
}
next
}
/^\+/ {
if (/^\+\+\+/) { next }
if (interesting) {
if (match($0, /\\(TEST|BEGINTEST|TYPE)[[:space:]]*\{[^{}]*~[^{}]*\}/)) {
# tab-separated triple: file \t lineno \t line
print file "\t" newline "\t" substr($0, 2) # 去掉首字符 `+`
}
}
newline++
next
}
/^ / { newline++ }
')

if [ -z "$candidates" ]; then
exit 0
fi

# Step 2 + 3: 对每个候选, 判断是否在 ExplSyntaxOff 段, 仅报 Off 段.
#
# 缓存策略: 用 tmp 目录的文件做 per-source-file 缓存, 避免 bash 4+ 才有的
# associative array (declare -A) — macOS 自带 bash 是 3.2, 不支持. 这种
# 缓存方式跨任意 bash 版本.
cache_dir="$(mktemp -d)"
trap 'rm -rf "$cache_dir"' EXIT
# 文件路径 -> 缓存路径: 用 sha1 防路径冲突. tr '/' '_' 会让
# `a/b_c.lvt` 与 `a_b/c.lvt` 撞 key, sha1 没这问题. 不依赖 GNU coreutils
# 之外的工具 — sha1sum / shasum (mac) 都支持, 选可用的一个.
if command -v sha1sum >/dev/null 2>&1; then
cache_key() { printf '%s' "$1" | sha1sum | cut -c1-40; }
elif command -v shasum >/dev/null 2>&1; then
cache_key() { printf '%s' "$1" | shasum | cut -c1-40; }
else
# fallback: 路径转义到 hex, 没 sha 也至少没冲突
cache_key() { printf '%s' "$1" | od -An -tx1 | tr -d ' \n'; }
fi

offending=""
n=0
while IFS=$'\t' read -r file lineno line; do
[ -z "$file" ] && continue

# 缓存每个文件的 line→state 表
ckey="$(cache_key "$file")"
cfile="${cache_dir}/${ckey}"
if [ ! -s "$cfile" ]; then
if [ ! -f "$file" ]; then
# 文件可能被删除, 跳过
continue
fi
awk '
BEGIN { state = "off"; depth = 0 }
# awk (POSIX) 不支持 \b 字符级锚, 用 [^a-zA-Z] 替代防止前缀
# 误匹配 (e.g. \ExplSyntaxOnDemand).
#
# 状态切换只在 file-level top scope (depth == 0) 生效. depth>0
# 时即便看到 \ExplSyntaxOff 也是某个 group 内局部切换, 出 group
# 后自动恢复, 字面状态机不该跟. 这能正确处理
# \sys_if_engine_luatex:F { \ExplSyntaxOff ... } 这种 expl3
# group 内的局部 catcode 切换.
#
# 简化假设: 状态机按整行判定 — 同一行同时出现 \ExplSyntaxOff 和
# \TEST{...} 时, 该行 state 用上一行末尾的 state (因为先 print
# state 再算 depth, 而 depth 更新在最后). 实际 .lvt 不会这么写,
# 这条记录是为未来注意.
{
# TeX 注释 (% 后): 第一个非转义 % 起到行末都是注释.
# 转义判定: % 前面**连续**的反斜杠数为奇数才是 \% 转义.
# \\% 是 "字面反斜杠 + 注释开始", 前 2 个 \ 互为转义对, % 本身
# 不被转义, 应当作注释起点.
stripped = $0
i = 1
while (i <= length(stripped)) {
c = substr(stripped, i, 1)
if (c == "%") {
# 数 i 前面连续的 `\` 个数
bs = 0; j = i - 1
while (j >= 1 && substr(stripped, j, 1) == "\\") {
bs++; j--
}
if (bs % 2 == 0) {
stripped = substr(stripped, 1, i-1)
break
}
}
i++
}
if (stripped ~ /\\ExplSyntaxOn([^a-zA-Z]|$)/ && depth == 0) state = "on"
if (stripped ~ /\\ExplSyntaxOff([^a-zA-Z]|$)/ && depth == 0) state = "off"
print NR "\t" state
# 数本行净的 {/} 差, 更新 depth (留到下一行使用). 用 stripped
# (去注释) 而非 $0, 避免 % { 这种字面字符被当 group 边界.
sline = stripped
o = 0; while (match(sline, /\{/)) { o++; sline = substr(sline, RSTART+1) }
sline = stripped
c = 0; while (match(sline, /\}/)) { c++; sline = substr(sline, RSTART+1) }
depth = depth + o - c
}
' "$file" > "$cfile"
fi

# 查 lineno 对应的 state
state=$(awk -F'\t' -v ln="$lineno" '$1 == ln { print $2; exit }' "$cfile")

if [ "$state" = "off" ]; then
n=$((n + 1))
offending="${offending} ${file}:${lineno}: ${line}"$'\n'
fi
done <<< "$candidates"

if [ "$n" -gt 0 ]; then
{
printf "✗ check-test-tilde: 发现 %d 处 .lvt 测试命令大括号内含 \`~\` (issue #893)\n" "$n"
printf "\n"
printf "%s" "$offending"
printf "\n"
printf " 说明: \\\\TEST/\\\\BEGINTEST 标题与 \\\\TYPE log 输出里, 在 \\\\ExplSyntaxOff\n"
printf " 段 (默认 LaTeX catcode), \`~\` 是 active char (不可断空格), 会让\n"
printf " .tlg baseline 出现字面 \`~\`. 应改为普通空格.\n"
printf "\n"
printf " 注: \\\\ExplSyntaxOn 段内 \`~\` 是 expl3 合法空格 (catcode 10), 本检查\n"
printf " 自动跳过该段, 不会误报.\n"
printf "\n"
printf " 紧急跳过: git commit --no-verify\n"
} >&2
exit 1
fi

exit 0
17 changes: 17 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
# pre-commit: 检查 staged 变更里的 .lvt 测试文件是否引入 `~` 误用 (#893).
#
# 仅检查本次 commit 的**新增/修改行**, 不动存量, 避免阻塞修改其它部分.
#
# CI 环境短路 (CI 由 .github/workflows/lint-test-files.yml 单独检 PR diff).
# 跳过: 仅紧急情况下 git commit --no-verify.
set -uo pipefail

if [ -n "${CI:-}" ] || [ -n "${GITHUB_ACTIONS:-}" ]; then
exit 0
fi

hook_dir="$(cd "$(dirname "$0")" && pwd)"

# -U0: 不输出 context 行, 加快 awk 状态机处理.
git diff --cached -U0 -- '*.lvt' | "${hook_dir}/check-test-tilde.sh"
33 changes: 33 additions & 0 deletions .github/workflows/lint-test-files.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Lint test files

# 检查 PR 引入的 .lvt 测试文件改动是否含 `~` 误用 (#893).
# 与 .githooks/pre-commit 共用 .githooks/check-test-tilde.sh 脚本.
# 只检 PR 新增/修改的行, 不动存量.

on:
pull_request:
paths:
- '**/*.lvt'
- '.githooks/check-test-tilde.sh'
- '.github/workflows/lint-test-files.yml'

permissions:
contents: read

jobs:
check-test-tilde:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
# 需要 PR base 才能算 merge-base diff
fetch-depth: 0

- name: Check .lvt files for ~ misuse (#893)
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
# 从 PR base 到 head 的 diff, 不输出 context 行加速 awk.
git diff "${BASE_SHA}" "${HEAD_SHA}" -U0 -- '*.lvt' \
| .githooks/check-test-tilde.sh
4 changes: 2 additions & 2 deletions ctex/test/testfiles/verb01.luatex.tlg

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions ctex/test/testfiles/verb01.lvt
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@
\edef\widthTexttt{\the\wd1}

\TEST{xkanjiskip survives verb}{
\TYPE{verb~=~\widthVerb}
\TYPE{texttt~=~\widthTexttt}
\TYPE{verb = \widthVerb}
\TYPE{texttt = \widthTexttt}
\ifdim\wd0=\wd1
\TYPE{PASS}
\else
\TYPE{FAIL:~verb~and~texttt~widths~differ}
\TYPE{FAIL: verb and texttt widths differ}
\fi
}

Expand Down
2 changes: 1 addition & 1 deletion ctex/test/testfiles/verbatim01.luatex.tlg

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion ctex/test/testfiles/verbatim01.lvt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ hello world
\clearpage
\ENDTEST

\BEGINTEST{verb~inline}
\BEGINTEST{verb inline}
正文中\verb|内联代码|和\verb+中文verb+。
\clearpage
\ENDTEST
Expand Down
2 changes: 1 addition & 1 deletion ctex/test/testfiles/verbatim01.pdftex.tlg

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion ctex/test/testfiles/verbatim01.tlg

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion ctex/test/testfiles/verbatim01.uptex.tlg

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 6 additions & 2 deletions llmdoc/reference/build-and-test.md
Original file line number Diff line number Diff line change
Expand Up @@ -353,11 +353,15 @@ CTAN 打包现已完全由 `.github/workflows/release.yml` 自动化驱动。原
# 1. 同步包到 usertree(前提:已 init 过 ~/texmf + ~/.texlive2026/)
tlmgr --usermode update --all

# 2. 重生成 xelatex fmt(必须,否则启动时仍加载老内核)
# 2. 重生成 fmt(必须,否则启动时仍加载老内核)。要按你跑的引擎一个个来:
# ctex 默认跨 4 个 engine 测试,全部都要 rebuild
fmtutil-user --byfmt latex # pdftex
fmtutil-user --byfmt xelatex
fmtutil-user --byfmt lualatex
fmtutil-user --byfmt uplatex # ctex 要这个,别漏了;漏了会全 49 个 uptex 测试 fail
```

仅做第 1 步是常见坑:xelatex 启动加载的是预编译 `xelatex.fmt`,里面 dump 的 `latex.ltx` 是包升级**前**的版本,新 `.ltx` / `.sty` 文件即使已落盘也不会生效。
仅做第 1 步是常见坑:xelatex 启动加载的是预编译 `xelatex.fmt`,里面 dump 的 `latex.ltx` 是包升级**前**的版本,新 `.ltx` / `.sty` 文件即使已落盘也不会生效。**只 rebuild 部分 engine fmt** 也是常见坑——漏掉的 engine 全部 fail 同一种 `expl3.sty Mismatched LaTeX support files` 错。

`tlmgr --usermode` 的边界:

Expand Down
Loading
Loading