diff --git a/devel/0077.md b/devel/0077.md new file mode 100644 index 00000000..1a5fc23e --- /dev/null +++ b/devel/0077.md @@ -0,0 +1,70 @@ +# [0077] gf fmt --changed-since 支持 C++ 文件 + +## 1. 相关文档 +- [dddd.md](dddd.md) - 任务文档模板 + +## 2. 任务相关的代码文件 +- `tools/fmt/liii/goldfmt.scm` - gf fmt 主入口,新增通用 `format-changed-since` 多语言派发 +- `tools/fmt/liii/goldfmt-lang.scm` - 语言注册表,新增 `lang-for-name` +- `tools/fmt/liii/scheme-fmt.scm` - 取消导出 `format-changed-since`(由主入口接管) +- `tools/fmt/tests/liii/goldfmt-changed-since-test.scm` - 新增单元测试 + +## 3. 如何测试 + +### 3.1 确定性测试(单元测试) +```bash +xmake b goldfish +bin/gf test tools/fmt/tests/liii/goldfmt-changed-since-test.scm +``` + +### 3.2 运行全部 fmt 测试 +```bash +bin/gf test tools/fmt/tests/ +``` + +### 3.3 手动验证 +在包含未提交 C++ 修改的分支上: +```bash +bin/gf fmt --changed-since=main # 应同时处理 Scheme 与 C++ 变更文件 +bin/gf fmt --changed-since=main -e cpp # 只处理 C++ 变更文件 +bin/gf fmt --changed-since=main -e scm # 只处理 Scheme 变更文件 +``` + +## 4. 如何提交 + +提交前执行以下最少步骤: +```bash +bin/gf test --changed-since=main +``` + +## 6. 2026-06-25 让 changed-since 测试在无 clang-format 时跳过 C++ 端到端段 + +### 6.1 What +1. `goldfmt-changed-since-test.scm` 末尾依赖 clang-format 的 C++ 端到端格式化段,改为用 `(when (clang-format-available?) ...)` 守卫:检测到 clang-format 才跑,否则跳过。 +2. 新增测试内辅助 `clang-format-available?`,通过 `clang-format --version` 的退出码判断可用性(二进制名取自 `(liii cpp-fmt)` 导出的 `clang-format-binary`)。 + +### 6.2 Why +macos / fedora / debian 三个 CI 均未安装 clang-format。`format-changed-since` 在缺少 clang-format 时仅打印提示并跳过 C++ 文件、返回 `#t`,导致 `b.cpp` 内容未被格式化、断言 `int b = 3;` 失败。该端到端断言本身有价值(覆盖默认全语言的端到端路径),故选择环境检测后跳过,而非删除。 + +### 6.3 How +保留 C++ 端到端段,仅在入口加 `clang-format-available?` 守卫。无 clang-format 的环境跳过整段,有 clang-format 的环境(含 format-check CI、本地)仍完整验证。无需改动 CI 安装步骤。 + +## 5. 2026-06-25 gf fmt --changed-since 支持多语言 + +### 5.1 What +1. `gf fmt --changed-since=REV` 不再只支持 Scheme,改为支持所有已注册语言(Scheme + C++)。 +2. 未显式指定 `-e/--extension` 时,`--changed-since` 默认使用所有已注册语言的后缀;显式指定 `-e` 时按过滤条件执行。 +3. 新增 `tools/fmt/liii/goldfmt-lang.scm` 的 `lang-for-name` 辅助函数,用于按语言名查找 handler。 +4. 新增 `tools/fmt/tests/liii/goldfmt-changed-since-test.scm` 单元测试,覆盖分组逻辑与 Scheme/C++ 端到端格式化。 + +### 5.2 Why +之前 `gf fmt --changed-since=main` 直接调用 Scheme 专属的 `format-changed-since`,C++ 变更文件被排除,导致 C++ 开发者需要手动格式化或运行全仓库 `gf fmt`。 + +### 5.3 How +1. 在 `goldfmt.scm` 中实现通用 `format-changed-since`: + - 用 `changed-existing-files-since` 取变更文件; + - 按 `-e` 或全部注册语言后缀过滤; + - 用 `group-files-by-lang` 按后缀把文件分组到对应 language handler; + - 调用各 handler 的 `format-files` 批量格式化。 +2. 通过扫描原始 `(argv)` 判断 `-e/--extension` 是否显式传入,从而区分"默认全部语言"与"仅 Scheme"。 +3. `scheme-fmt.scm` 中的 `format-changed-since` 取消导出,避免与主入口版本冲突。 diff --git a/gf_fmt.json b/gf_fmt.json index b78f1d57..eda6a5b5 100644 --- a/gf_fmt.json +++ b/gf_fmt.json @@ -2,7 +2,7 @@ "cpp": { "suffix": ["hpp", "cpp", "h", "c", "cc", "cxx"], "path": ["src"], - "binary-linux": ["/usr/bin/clang-format", "/usr/bin/clang-format-19"], + "binary-linux": ["/usr/bin/clang-format-19", "/usr/bin/clang-format"], "binary-macos": ["/opt/homebrew/opt/llvm@19/bin/clang-format", "/usr/local/opt/llvm@19/bin/clang-format"], "exclude": [ {"path": "src/s7*", "reason": "s7 体系文件(src/s7.c、src/s7.h 及所有 s7_* 拆分/派生文件),沿用 s7 自身代码风格,clang-format 会破坏其脆弱的宏/排版,不纳入格式化"}, diff --git a/tools/fmt/liii/goldfmt-lang.scm b/tools/fmt/liii/goldfmt-lang.scm index 48dd015c..51fbd405 100644 --- a/tools/fmt/liii/goldfmt-lang.scm +++ b/tools/fmt/liii/goldfmt-lang.scm @@ -42,6 +42,7 @@ extensions-for-lang-name lang-for-extension lang-for-extensions + lang-for-name path-matches-exclude? file-excluded? collect-files @@ -159,6 +160,17 @@ ) ;let ) ;define + ;; 按语言名(符号)查 handler:返回第一个 name 匹配的 handler,无则 #f。 + (define (lang-for-name name) + (let loop + ((handlers (lang-list))) + (if (null? handlers) + #f + (if (eq? (lang-name (car handlers)) name) (car handlers) (loop (cdr handlers))) + ) ;if + ) ;let + ) ;define + ;; ---- exclude 匹配(迁移自 goldformat-path.scm)--------------------- ;; 把路径分隔符统一成正斜杠。 (define (normalize-sep s) diff --git a/tools/fmt/liii/goldfmt.scm b/tools/fmt/liii/goldfmt.scm index c150f4aa..377d4657 100644 --- a/tools/fmt/liii/goldfmt.scm +++ b/tools/fmt/liii/goldfmt.scm @@ -48,14 +48,25 @@ (liii path) (liii string) (liii argparse) + (liii json) + (liii list) (liii goldfmt-scan) (liii goldfmt-format) (liii goldfmt-lang) (liii goldfmt-config) + (liii goldtool-changed) (liii scheme-fmt) (liii cpp-fmt) ) ;import - (export main format-datum format-datum+node format-node format-string) + (export main + format-datum + format-datum+node + format-node + format-string + all-registered-extensions + group-files-by-lang + format-changed-since + ) ;export (begin ;; ---- 参数解析 ------------------------------------------------------- @@ -66,7 +77,7 @@ ) ;if ) ;define - ;; 把单个 -e token 转成后缀列表:若是语言名(cpp/scheme/...)展开为该语言后缀表, + ;; 按 -e token 转成后缀列表:若是语言名(cpp/scheme/...)展开为该语言后缀表, ;; 否则按后缀处理(补点)。使 -e cpp 涵盖 .cpp/.hpp/.h/.c/.cc/.cxx 全部。 (define (token->extensions token) (let ((lang-exts (extensions-for-lang-name token))) @@ -74,6 +85,16 @@ ) ;let ) ;define + (define (file-extension-match? filename extensions) + (let loop + ((exts extensions)) + (if (null? exts) + #f + (if (string-ends? filename (car exts)) #t (loop (cdr exts))) + ) ;if + ) ;let + ) ;define + (define (parse-extensions raw) (let loop ((tokens (string-split raw ",")) (acc '())) @@ -158,7 +179,7 @@ (display " -e, --extension EXT 按语言名或后缀指定:-e cpp 涵盖 .cpp/.hpp/.h/.c/.cc/.cxx;-e scheme 涵盖 .scm;也可直接写后缀 -e scm,sld" ) ;display (newline) - (display " --changed-since REV 仅格式化自 REV 以来变更的 Scheme 文件" + (display " --changed-since REV 仅格式化自 REV 以来变更的文件(按 -e 过滤;未指定 -e 时包含所有语言)" ) ;display (newline) (display " --exclude PATTERN 跳过匹配的文件(路径后缀匹配,逗号分隔多个)" @@ -191,7 +212,7 @@ (display " gf fmt -e scm,sld dir/ 递归格式化目录下所有 .scm 和 .sld 文件" ) ;display (newline) - (display " gf fmt --changed-since=HEAD 格式化自 HEAD 以来变更的 Scheme 文件" + (display " gf fmt --changed-since=HEAD 格式化自 HEAD 以来变更的所有文件" ) ;display (newline) ) ;define @@ -397,6 +418,145 @@ ) ;let ) ;define + ;; ---- 增量格式化(--changed-since)------------------------------------ + ;; 检测用户是否在命令行显式传入了 -e/--extension。 + (define (extension-option-explicit? args) + (let loop + ((as args)) + (if (null? as) + #f + (let ((a (car as))) + (if (or (string=? a "-e") + (string=? a "--extension") + (string-starts? a "-e=") + (string-starts? a "--extension=") + ) ;or + #t + (loop (cdr as)) + ) ;if + ) ;let + ) ;if + ) ;let + ) ;define + + ;; 收集所有已注册语言的后缀。 + (define (all-registered-extensions) + (let loop + ((handlers (lang-list)) (acc '())) + (if (null? handlers) + acc + (loop (cdr handlers) (append (lang-extensions (car handlers)) acc)) + ) ;if + ) ;let + ) ;define + + ;; 按语言名(符号)把文件分组,返回 ((name . files) ...)。 + (define (add-file-to-group groups file handler) + (let ((name (lang-name handler))) + (let loop + ((gs groups) (acc '()) (found #f)) + (cond ((null? gs) + (if found (reverse acc) (reverse (cons (cons name (list file)) acc))) + ) ; + ((and (not found) (eq? (caar gs) name)) + (loop (cdr gs) (cons (cons name (cons file (cdar gs))) acc) #t) + ) ; + (else (loop (cdr gs) (cons (car gs) acc) found)) + ) ;cond + ) ;let + ) ;let + ) ;define + + (define (group-files-by-lang files) + (let loop + ((fs files) (groups '())) + (if (null? fs) + groups + (let* ((file (car fs)) + (ext (path-suffix (path file))) + (handler (or (lang-for-extension ext) (scheme-handler-of))) + ) ; + (loop (cdr fs) (add-file-to-group groups file handler)) + ) ;let* + ) ;if + ) ;let + ) ;define + + ;; 多语言增量格式化:按文件后缀分派到对应 handler 的 format-files。 + ;; 未显式指定 -e 时,默认使用所有已注册语言的后缀(与无路径参数的仓库批量行为一致)。 + ;; --dry-run 与多文件 changed-since 不兼容,直接报错。 + (define (format-changed-since since path-str extensions excludes dry-run) + (if dry-run + (begin + (display "错误: --dry-run 选项与 --changed-since 不能同时使用") + (newline) + (exit 1) + ) ;begin + (let ((scope (if (string=? path-str "") #f path-str)) + (cfg (or (catch #t (lambda () (load-fmt-config)) (lambda (type info) #f)) + (string->json "{}") + ) ;or + ) ;cfg + ) ; + (let ((files (if scope + (changed-existing-files-since since scope) + (changed-existing-files-since since) + ) ;if + ) ;files + ) ; + (let ((filtered (filter (lambda (f) + (and (file-extension-match? f extensions) (not (file-excluded? f excludes))) + ) ;lambda + files + ) ;filter + ) ;filtered + ) ; + (if (null? filtered) + (begin + (display (string-append "No changed files since " since)) + (newline) + #t + ) ;begin + (let ((groups (group-files-by-lang filtered))) + (let loop + ((gs groups) (total 0) (updated 0) (cached 0)) + (if (null? gs) + (begin + (display (string-append "Total files formatted: " + (number->string total) + ", Files updated: " + (number->string updated) + ", Files cached: " + (number->string cached) + ) ;string-append + ) ;display + (newline) + #t + ) ;begin + (let* ((g (car gs)) + (handler (lang-for-name (car g))) + (format-files-fn (lang-ref handler 'format-files)) + (stats (format-files-fn (cdr g) cfg)) + ) ; + (display (string-append "=== Formatting " (lang-label handler) " files ===")) + (newline) + (flush-output-port (current-output-port)) + (loop (cdr gs) + (+ total (car stats)) + (+ updated (cadr stats)) + (+ cached (caddr stats)) + ) ;loop + ) ;let* + ) ;if + ) ;let + ) ;let + ) ;if + ) ;let + ) ;let + ) ;let + ) ;if + ) ;define + ;; ---- 主入口 --------------------------------------------------------- (define (main) (let ((parser (make-fmt-arg-parser))) @@ -410,9 +570,18 @@ (path-str (first-positional parser)) ) ; (cond (help-flag (display-help) #t) - ;; changed-since 优先:即使无路径参数也走 Scheme 增量,而非仓库批量。 - (changed-since (let ((excludes (append cli-excludes (scheme-config-excludes)))) - (format-changed-since changed-since path-str extensions excludes dry-run) + ;; changed-since 优先:即使无路径参数也走增量格式化,而非仓库批量。 + ;; 未显式指定 -e 时默认包含所有已注册语言。 + (changed-since (let ((excludes (append cli-excludes (scheme-config-excludes))) + (effective-extensions (if (extension-option-explicit? (argv)) extensions (all-registered-extensions)) + ) ;effective-extensions + ) ; + (format-changed-since changed-since + path-str + effective-extensions + excludes + dry-run + ) ;format-changed-since ) ;let ) ;changed-since ;; 无路径参数:仓库批量 / check(需 gf_fmt.json)。 diff --git a/tools/fmt/liii/scheme-fmt.scm b/tools/fmt/liii/scheme-fmt.scm index cbd70fed..adf91a02 100644 --- a/tools/fmt/liii/scheme-fmt.scm +++ b/tools/fmt/liii/scheme-fmt.scm @@ -28,14 +28,8 @@ (liii goldfmt-format) (liii goldfmt-lang) (liii goldfmt-config) - (liii goldtool-changed) ) ;import - (export scheme-extensions - format-single-file - format-directory - format-changed-since - format-file-list - ) ;export + (export scheme-extensions format-single-file format-directory format-file-list) (begin ;; Scheme 语言接管的后缀表(带点)。gf_fmt.json 未写 scheme.suffix 时也用此表。 @@ -209,40 +203,6 @@ ) ;if ) ;define - ;; ---- 增量格式化 ----------------------------------------------------- - (define (format-changed-since since path-str extensions excludes dry-run) - (let ((scope (if (string=? path-str "") #f path-str))) - (let ((files (if scope - (changed-scheme-files-since since scope extensions) - (changed-scheme-files-since since #f extensions) - ) ;if - ) ;files - ) ; - (if (null? files) - (begin - (display (string-append "No changed Scheme files since " since)) - (newline) - #t - ) ;begin - (call-with-values (lambda () (format-file-list files dry-run excludes)) - (lambda (total updated cached) - (display (string-append "Total files formatted: " - (number->string total) - ", Files updated: " - (number->string updated) - ", Files cached: " - (number->string cached) - ) ;string-append - ) ;display - (newline) - #t - ) ;lambda - ) ;call-with-values - ) ;if - ) ;let - ) ;let - ) ;define - ;; ---- handler 协议实现(供仓库批量 / check 使用)--------------------- ;; 各方法统一接收 cfg,内部用 goldfmt-config 访问器自取本语言的 path/exclude。 diff --git a/tools/fmt/tests/liii/goldfmt-changed-since-test.scm b/tools/fmt/tests/liii/goldfmt-changed-since-test.scm new file mode 100644 index 00000000..df4fe936 --- /dev/null +++ b/tools/fmt/tests/liii/goldfmt-changed-since-test.scm @@ -0,0 +1,142 @@ +(set! *load-path* (cons "tools/fmt" *load-path*)) +(set! *load-path* (cons "tools/common" *load-path*)) + +(import (liii check) + (liii goldfmt) + (liii cpp-fmt) + (liii goldtool-changed) + (liii list) + (liii os) + (liii path) + (liii string) +) ;import + +(check-set-mode! 'report-failed) + +(define (contains? item xs) + (if (member item xs) #t #f) +) ;define + +(define (must command) + (let ((status (os-call command))) + (unless (zero? status) + (error "Command failed" command) + ) ;unless + ) ;let +) ;define + +;; 检测 clang-format 是否可用;不可用时(如未安装 clang-format 的 CI)相关 C++ 端到端断言跳过。 + +(define (clang-format-available?) + (zero? (os-call (string-append (clang-format-binary) " --version"))) +) ;define + +(define (remove-tree target) + (cond ((path-file? target) (path-unlink target #t)) + ((path-dir? target) + (let ((entries (path-list-path target))) + (let loop + ((i 0)) + (if (< i (vector-length entries)) + (begin + (remove-tree (vector-ref entries i)) + (loop (+ i 1)) + ) ;begin + #t + ) ;if + ) ;let + ) ;let + (path-rmdir target) + ) ; + ) ;cond +) ;define + +;; ---- 静态分组测试 ------------------------------------------------------- + +(let ((exts (all-registered-extensions))) + (check (contains? ".scm" exts) => #t) + (check (contains? ".cpp" exts) => #t) + (check (contains? ".hpp" exts) => #t) +) ;let + +(let ((groups (group-files-by-lang '("a.scm" "b.cpp" "c.hpp" "d.scm")))) + (check (length groups) => 2) + (check (contains? 'scheme (map car groups)) => #t) + (check (contains? 'cpp (map car groups)) => #t) + (let ((scheme-files (cdr (assq 'scheme groups))) + (cpp-files (cdr (assq 'cpp groups))) + ) ; + (check (length scheme-files) => 2) + (check (length cpp-files) => 2) + (check (contains? "a.scm" scheme-files) => #t) + (check (contains? "d.scm" scheme-files) => #t) + (check (contains? "b.cpp" cpp-files) => #t) + (check (contains? "c.hpp" cpp-files) => #t) + ) ;let +) ;let + +;; ---- 真实 git 仓库测试:Scheme 增量格式化 ------------------------------- +;; 为避免依赖 clang-format,端到端测试只验证 Scheme 分支。 + +(define original-cwd (getcwd)) + +(define repo-dir + (path->string (path-join (path-temp-dir) + (string-append "goldfmt-changed-since-test-" (number->string (getpid))) + ) ;path-join + ) ;path->string +) ;define + +(dynamic-wind (lambda () (remove-tree repo-dir) (mkdir repo-dir) (chdir repo-dir)) + (lambda () + (must "git init -q") + (must "git config user.email goldfish-test@example.com") + (must "git config user.name Goldfish Test") + + (path-write-text (path "a.scm") "( define a 1 )\n") + (path-write-text (path "b.cpp") "int b = 1;\n") + (must "git add .") + (must "git commit -q -m initial") + + (path-write-text (path "a.scm") "( define a 2 )\n") + (path-write-text (path "b.cpp") "int b = 2;\n") + + ;; -e scm 只处理 Scheme 文件 + (let ((result (format-changed-since "HEAD" "" '(".scm") '() #f))) + (check result => #t) + ) ;let + (check (string=? (path-read-text (path "a.scm")) "(define a 2)\n") => #t) + ;; C++ 文件不应被 Scheme 格式化器改动 + (check (string=? (path-read-text (path "b.cpp")) "int b = 2;\n") => #t) + + ;; 默认使用所有语言时,应识别到 .scm 和 .cpp 两个变更文件 + ;; 这里不实际调用 clang-format,只检查分组结果。 + (let* ((all-files (changed-existing-files-since "HEAD")) + (filtered (filter (lambda (f) (or (string-ends? f ".scm") (string-ends? f ".cpp"))) + all-files + ) ;filter + ) ;filtered + (groups (group-files-by-lang filtered)) + ) ; + (check (= (length filtered) 2) => #t) + (check (contains? "a.scm" filtered) => #t) + (check (contains? "b.cpp" filtered) => #t) + (check (length groups) => 2) + ) ;let* + + ;; 再次修改,测试默认所有语言的端到端格式化(依赖 clang-format) + ;; 无 clang-format 的环境(如 macos/fedora/debian CI)跳过本段。 + (when (clang-format-available?) + (path-write-text (path "a.scm") "( define a 3 )\n") + (path-write-text (path "b.cpp") "int b = 3;\n") + (let ((result (format-changed-since "HEAD" "" (all-registered-extensions) '() #f))) + (check result => #t) + ) ;let + (check (string=? (path-read-text (path "a.scm")) "(define a 3)\n") => #t) + (check (string=? (path-read-text (path "b.cpp")) "int b = 3;\n") => #t) + ) ;when + ) ;lambda + (lambda () (chdir original-cwd) (remove-tree repo-dir)) +) ;dynamic-wind + +(check-report)