moonreview

    Unified diff parser and AI code review CLI built from scratch in MoonBit.

    diff
    unified-diff
    git
    code-review
    cli
    parser
    Download zip
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    17 days ago
    Downloads
    27

    No dependencies

    #cookies060809/moonreview

    unified diff 文本解析器。parse_diff 把 git diff 的输出读成 Diff / FileDiff / Hunk / Line,每一行都带改动前和改动后的行号;format_stat 和 format_detail 把它渲染成人能看的摘要和逐行视图。

    解析按 @@ 头声明的行数驱动,不靠逐行看前缀,所以 diff 里被删掉的 -- foo 不会被误认成文件头。review_prompt 和 chat_body 把同一份数据渲染成给大模型的 评审提示词和请求体,chat_reply 解接口响应——这几步也全是纯函数,只用到标准库。

    命令行用法和完整说明见仓库:https://github.com/cookies060809/moonreview。

    DiffError

    pub suberror DiffError {
    BadDiff(Int, String)
    } derive(Eq,
    Debug
    )

    解析失败。BadDiff(行号, 说明),行号从 1 开始,指向出错的源文本行。

    DiffError::equal

    fn DiffError::equal(DiffError, DiffError) -> Bool

    DiffError::not_equal

    fn DiffError::not_equal(x : DiffError, y : DiffError) -> Bool

    Diff

    一份完整的 diff,也就是 git diff 一次的输出。

    Diff::added_count

    fn Diff::added_count(self : Diff) -> Int

    Diff::chat_body

    fn Diff::chat_body(self : Diff, model : String) -> String

    请求体:system 消息放规则,user 消息放逐文件的 diff 文本。

    Diff::empty

    fn Diff::empty() -> Diff

    Diff::equal

    fn Diff::equal(Diff, Diff) -> Bool

    Diff::file_count

    fn Diff::file_count(self : Diff) -> Int

    Diff::format_detail

    fn Diff::format_detail(self : Diff) -> String

    按文件、按 hunk 展开的视图,保留 diff 自己的行首符号。

    Diff::format_json

    fn Diff::format_json(self : Diff) -> String

    缩进两格的可读 JSON,给命令行 --json 用。

    Diff::format_stat

    fn Diff::format_stat(self : Diff) -> String

    git diffstat 风格:每个文件一行,末尾跟一条总计。

    二进制文件和只改权限的文件没有 hunk,计数自然是 0,照样占一行。

    Diff::hunk_count

    fn Diff::hunk_count(self : Diff) -> Int

    Diff::is_empty

    fn Diff::is_empty(self : Diff) -> Bool

    Diff::not_equal

    fn Diff::not_equal(x : Diff, y : Diff) -> Bool

    Diff::removed_count

    fn Diff::removed_count(self : Diff) -> Int

    Diff::review_prompt

    fn Diff::review_prompt(self : Diff) -> String

    Diff::to_json

    fn Diff::to_json(self : Diff) -> Json

    Diff::to_repr

    Diff::touched_paths

    fn Diff::touched_paths(self : Diff) -> Array[String]

    这次改动碰到了哪些文件

    FileDiff

    type FileDiff derive(Eq,
    Debug
    )

    一个文件的完整改动记录。

    FileDiff::added_count

    fn FileDiff::added_count(self : FileDiff) -> Int

    FileDiff::changed_line_count

    fn FileDiff::changed_line_count(self : FileDiff) -> Int

    FileDiff::display_path

    fn FileDiff::display_path(self : FileDiff) -> String

    展示用的路径:优先给改动后的,被删除的文件退回改动前的。

    FileDiff::equal

    fn FileDiff::equal(FileDiff, FileDiff) -> Bool

    FileDiff::not_equal

    fn FileDiff::not_equal(x : FileDiff, y : FileDiff) -> Bool

    FileDiff::removed_count

    fn FileDiff::removed_count(self : FileDiff) -> Int

    FileDiff::to_repr

    FileStatus

    type FileStatus derive(Eq,
    Debug
    )

    这个文件整体发生了什么。

    FileStatus::equal

    fn FileStatus::equal(FileStatus, FileStatus) -> Bool

    FileStatus::label

    fn FileStatus::label(self : FileStatus) -> String

    人话版的状态名,用在详细视图的标题行上。

    FileStatus::not_equal

    fn FileStatus::not_equal(x : FileStatus, y : FileStatus) -> Bool

    Hunk

    一个 @@ 块。

    Hunk::added

    fn Hunk::added(self : Hunk) -> Array[Line]

    Hunk::added_count

    fn Hunk::added_count(self : Hunk) -> Int

    Hunk::changed

    fn Hunk::changed(self : Hunk) -> Array[Line]

    只看真正改动的行,跳过上下文

    Hunk::equal

    fn Hunk::equal(Hunk, Hunk) -> Bool

    Hunk::new_side_text

    fn Hunk::new_side_text(self : Hunk) -> String

    重建「改动后」的代码片段:上下文 + 新增行。 喂给大模型看的应该就是这段。

    Hunk::not_equal

    fn Hunk::not_equal(x : Hunk, y : Hunk) -> Bool

    Hunk::old_side_text

    fn Hunk::old_side_text(self : Hunk) -> String

    重建「改动前」的代码片段:上下文 + 删除行。

    Hunk::removed

    fn Hunk::removed(self : Hunk) -> Array[Line]

    Hunk::removed_count

    fn Hunk::removed_count(self : Hunk) -> Int

    Hunk::to_repr

    HunkHeader

    type HunkHeader derive(Eq,
    Debug
    )

    从 @@ -10,3 +10,4 @@ 函数名 读出来的定位信息。

    HunkHeader::equal

    fn HunkHeader::equal(HunkHeader, HunkHeader) -> Bool

    HunkHeader::not_equal

    fn HunkHeader::not_equal(x : HunkHeader, y : HunkHeader) -> Bool

    Line

    diff 中的单行。

    行号在对应一侧不存在时为 None:新增行没有「改动前」的行号, 删除行没有「改动后」的行号,上下文行两侧都有。

    Line::equal

    fn Line::equal(Line, Line) -> Bool

    Line::is_change

    fn Line::is_change(self : Line) -> Bool

    是不是改动行(上下文行不算)。

    Line::not_equal

    fn Line::not_equal(x : Line, y : Line) -> Bool

    Line::to_repr

    LineKind

    type LineKind derive(Eq,
    Debug
    )

    一行源码在 diff 里扮演的角色。

    LineKind::equal

    fn LineKind::equal(LineKind, LineKind) -> Bool

    LineKind::not_equal

    fn LineKind::not_equal(x : LineKind, y : LineKind) -> Bool

    LineKind::prefix

    fn LineKind::prefix(self : LineKind) -> String

    一行在 diff 里的行首符号。

    LineKind::to_repr

    body_file

    let body_file : String

    请求体、curl 配置、响应都落在当前目录的隐藏文件里: 用相对路径可以绕开 Windows 反斜杠在 curl 配置里被当转义符的坑, 也让命令本身变成一个不含任何外部输入的固定字符串。

    chat_endpoint

    fn chat_endpoint(base_url : String) -> String

    chat_reply

    fn chat_reply(raw : String) -> (Bool, String)

    成功时返回模型写的正文,失败时返回一句能直接给人看的说明。 用元组而不是新类型,是为了不把错误类型的构造器再导出一次。

    clean_path

    fn clean_path(raw : String) -> String

    清理 --- / +++ 行里的路径:
    1. 丢掉制表符之后的时间戳(--- a/f.c\t2026-01-01 10:00)
    2. 去掉包裹的引号
    3. 去掉 git 的 a/ b/ 前缀

    /dev/null 会原样保留,靠它判断新增还是删除。

    config_file

    let config_file : String

    curl_command

    let curl_command : String

    固定命令串:不含任何来自 diff、密钥或环境的内容,所以交给 shell 执行是安全的。

    curl_config

    fn curl_config(base_url : String, api_key : String) -> String

    curl 的配置文件格式,对应 curl -K <config_file>。 请求体走 data-binary = "@file",只有文件名进配置,正文不进。

    parse_diff

    fn parse_diff(text : String) -> Diff raise DiffError

    把一份 unified diff 解析成结构化结果。

    空文本是合法的,得到 0 个文件——代码评审里「没有改动」不是错误。 无法识别的杂散行(commit 说明、version 1.0 之类)会被跳过, 但 hunk 行数对不上会报错,因为那意味着 diff 本身被截断或损坏了。

    parse_hunk_header

    fn parse_hunk_header(line : String, line_no : Int) -> HunkHeader raise DiffError

    解析 @@ -old_start,old_count +new_start,new_count @@ heading。

    省略的计数按 GNU 规则当作 1。计数为 0 表示纯插入/纯删除, 此时起始行号含义是「在第几行之后插入」,原样保留不做换算。

    read_two_paths

    fn read_two_paths(rest : String) -> (String, String)

    读 diff --git a/x b/y 去掉前缀之后的两个路径。

    resolve_base_url

    fn resolve_base_url(value : String?) -> String

    base_url 和 model 都允许不传,这时用默认值。

    resolve_model

    fn resolve_model(value : String?) -> String

    response_file

    let response_file : String

    split_lines

    fn split_lines(text : String) -> Array[String]

    按行切分,兼容 \n、\r\n、\r。 文本以换行结尾时不会多出一个空行。