简介

Difftastic 是一个根据文件语法进行比较的结构化比较工具。它 支持超过 30 种编程语言。当你使用时,便会知道它有多么

Difftastic 是一款开源软件(使用 MIT 许可证),你可以 在 GitHub 上查看其源代码

本说明书描述当前版本 0.62.0。更新日志 会记录每个版本的新特性与 bug 的修复。

如果你正寻找其他语言的说明书,我们同样提供了 英文版

语法差异分析

Difftastic 检测编程语言、解析代码,并比较语法树。以下是例子。

// old.rs
let ts_lang = guess(path, guess_src).map(tsp::from_language);
// new.rs
let ts_lang = language_override
    .or_else(|| guess(path, guess_src))
    .map(tsp::from_language);
$ difft old.rs new.rs

1 1 let ts_lang = language_override
. 2     .or_else(|| guess(path, guess_src))
. 3     .map(tsp::from_language);

注意 Difftastic 是如何识别 .map 没有变化的,尽管它在带有空格的新行上。

面对行的差异分析表现则会很不理想。

$ diff -u old.rs new.rs

@@ -1 +1,3 @@
-let ts_lang = guess(path, guess_src).map(tsp::from_language);
+let ts_lang = language_override
+    .or_else(|| guess(path, guess_src))
+    .map(tsp::from_language);

一些文本差异分析工具也会突出单词的变化(如 GitHub 或 git 的 --word-diff),但它们仍无法理解代码本身。Difftastic 永远会找到匹配的定界符:你可以看到 or_else 结尾处的 ) 已被突出显示。

备用的文本差异分析

如果 Difftastic 不能理解输入文件,它将用常规的面向行的文本差异分析与单词高亮显示。

同时,当输入的文件较大时,Difftastic 也会使用面向行的文本差异分析。

安装

从二进制安装

Difftastic 将预先编译好的二进制文件 提供到 GitHub realeases更新日志 描述了每个发行版的变更。

在以下平台上也可以使用软件包。

安装包状态

通过 homebrew 安装(macOS 或 Linux)

Difftastic 可以使用 Homebrew 安装在 macOS 或 Linux 上。

$ brew install difftastic

从源码构建

要求

Difftastic 使用 Rust 编写,所以你需安装 Rust。我推荐使用 rustup 安装 Rust。Difftastic 要求 Rust 版本不低于 1.65。

你也需要一个支持 C++14 的 C++ 编译器。如果你正在使用 GCC,则 GCC 版本至少为 8。

构建

你可以下载并使用 Cargo(Rust 的一部分)构建 Difftastic on crates.io

$ cargo install --locked difftastic

Difftastic 使用 cc 程序箱来构建 C/C++ 的依赖关系,这使你能通过环境变量 CCCXX 来控制使用的编译器(参照 cc 文档)。

参考 贡献 来查看有关构建的说明。

(可选)安装 MIME 数据库

如果有一个 MIME 数据库,Difftastic 将使用它来更准确地检测二进制文件。MIME 数据库在使用 file 命令时也需要被调用,因此你可能已经安装了它。

MIME 数据库的路径 在 XDG 的规范下

  • /usr/share/mime/magic
  • /usr/local/share/mime/magic
  • $HOME/.local/share/mime/magic

使用方法

文件参数

比较文件

$ difft FIRST-FILE SECOND-FILE

$ difft sample_files/simple_1.js sample_files/simple_2.js

比较文件夹

$ difft FIRST-DIRECTORY SECOND-DIRECTORY

# For example:
$ difft sample_files/dir_1/ sample_files/dir_2/

Difftastic 会递归地浏览这两个文件夹,对同名的文件进行差异分析。

对比的文件夹间有许多未改变的文件时,--skip-unchanged 选项会十分有用。

从 stdin 读取

您可以通过指定 - 作为文件路径从标准输入(stdin)读取文件。

$ difft - SECOND-FILE

$ cat sample_files/simple_1.js | difft - sample_files/simple_2.js

带冲突标记的文件

(在 0.50 版本新增)

如果你有一个带 <<<<<<< 冲突标记的文件,可以将它作为一个参数传入 Difftastic。Difftastic 会构建和比较文件的两个状态。

$ difft FILE-WITH-CONFLICTS

$ difft sample_files/conflicts.el

语言检测

Difftastic 根据文件的扩展名、文件名和第一行的内容猜测文件所用的语言。

你可以通过 --override 选项覆盖语言检测。如果输入的文件有所设定的后缀, Difftastic 将会处理它们,并且忽略其他语言。

$ difft --override=GLOB:NAME FIRST-FILE SECOND-FILE

$ difft --override=*.h:c sample_files/preprocesor_1.h sample_files/preprocesor_2.h

选项

Difftastic 包括一系列命令行选项,你可以使用 difft --help 获得完整列表。

Difftastic 也可以用环境变量进行配置,这同样可以在 --help 中看到。

例如,DFT_BACKGROUND=light 相当于 --background=light。这在使用 VCS 工具(例如 Git)时会很有用,因为此时不能直接调用 difft 二进制文件。

退出状态码

2:Difftastic 收到了无效参数。这包括用法无效(如参数数量不对)、无法读取路径(如路径不存在或权限不足)。

1:使用 --exit-code 选项时,Difftastic 在发现任何语法变化(文本文件)或字节变化(二进制文件)。

0:所有其他情况。

Git

Git 支持使用外部差异分析工具。你可以使用 GIT_EXTERNAL_DIFF 命令暂时地设置 diff 工具。

$ GIT_EXTERNAL_DIFF=difft git diff
$ GIT_EXTERNAL_DIFF=difft git log -p --ext-diff
$ GIT_EXTERNAL_DIFF=difft git show e96a7241760319 --ext-diff

如果你想默认使用 Difftastic,可以使用 git config

# 仅为当前存储库设置
$ git config diff.external difft

# 为全局设置
$ git config --global diff.external difft

在运行 git config 后,git diff 命令将会自动使用 difft。其他情况则需要使用 --ext-diff 来使用 diff.external

$ git diff
$ git log -p --ext-diff
$ git show e96a7241760319 --ext-diff

git-difftool

git difftool 是一个 Git 命令,用于使用不同差异分析工具来查看当前修改。如果你想要偶尔使用 Difftastic,这会非常有用。

添加下列内容到你的 .gitconfig 文件中,Difftastic 就会作为你的 diff 工具。

[diff]
        tool = difftastic

[difftool]
        prompt = false

[difftool "difftastic"]
        cmd = difft "$LOCAL" "$REMOTE"

然后,你可以使用 git difftool 来用 Difftastic 查看当前修改。

$ git difftool

我们还推荐使用下列设置来获得最好的差异分析工具体验。

# 对于较大的输出,和其它 Git 命令一样,使用分页器
[pager]
        difftool = true

# `git dft` 比 `git difftool` 更加短小
[alias]
        dft = difftool

Mercurial

Mercurial 在使用 Extdiff 拓展时,支持使用外部差异分析工具。你可以在 .hgrc 文件中添加 extensions 条目来启用它。

[extensions]
extdiff =

接下来,你可以运行 hg extdiff -p difft 命令,但不是 hg diff 命令(假定 difft 二进制文件已经存放在 $PATH 中)。

您还可以定义一个别名,用 hg 运行 Difftastic。将以下内容添加到您的 .hgrc 中,以使用 hg dft 命令运行 Difftastic。

[extdiff]
cmd.dft = difft
# 你可以添加更多选项,它们将被传递至命令行,例如:
# opts.dft = --background light

hg dft 也支持 hg diff 的所有选项。例如,hg dft --stat 会显示更改行的统计信息,hg dft -r 42 -r 45 会显示两个修订版之间的差异。

hg log -p

Mercurial 不能改变默认的差异工具,至少就作者所知。

如果你想查看最近一次提交的差异,可以使用下面的命令。

hg dft -r .^ -r .

这就等同于hg log -l 1 -p,尽管它不显示提交信息。

Fossil

Fossil 通过为当前存储库 设置diff-command 来支持外部 diff 命令:

fossil settings diff-command difft

如果你想对所有存储库使用 Difftastic,可以使用 --global

fossil settings diff-command --global difft

在 Fossil 上跳过 Difftastic

如果你已经将 Difftastic 设置为 Fossil 的 diff 命令,但想使用一次 Fossil 的内置差异分析工具,可以使用 -i 暂时跳过一次 Difftastic:

fossil diff -i

如果你想从某个存储库(或全局)移除 Difftastic,请使用 unset 命令:

fossil unset diff-command

unset 命令支持 --global 选项。

支持的语言

本页列出了 Difftastic 支持的所有语言。你也可以用 difft --list-languages 查看你当前安装的版本所支持的语言。

编程语言

结构化文本格式

解析代码

Difftastic会使用tree-sitter 来建立一个语法树。然后,该语法树被转换为一个可以用来对比差异的简化版语法树。

使用Tree-sitter解析代码

Difftastic依靠tree-sitter来理解语法。你可以使用--dump-ts来查看tree-sitter的语法树。

$ difft --dump-ts sample_files/javascript_simple_1.js | head
program (0, 0) - (7, 0)
  comment (0, 0) - (0, 8) "// hello"
  expression_statement (1, 0) - (1, 6)
    call_expression (1, 0) - (1, 5)
      identifier (1, 0) - (1, 3) "foo"
      arguments (1, 3) - (1, 5)
        ( (1, 3) - (1, 4) "("
        ) (1, 4) - (1, 5) ")"
    ; (1, 5) - (1, 6) ";"
  expression_statement (2, 0) - (2, 6)

简化的语法

Difftastic将tree-sitter语法树转换为简化版的语法树。语法树是一种统一的表示方式,其中所有东西都是原子(例如,整数、注释、变量名)或者是一个列表(由开放分界符、子句和关闭分界符组成)以及分隔符。

--dump-syntax将显示出当前文件所对应的语法树。

$ difft --dump-syntax sample_files/before.js
[
    Atom id:1 {
        content: "// hello",
        position: "0:0-8",
    },
    List id:2 {
        open_content: "",
        open_position: "1:0-0",
        children: [
          ...

转换过程

Difftastic语法树的简单表达方式使得差异分析变得更加容易。Difftastic是通过一种递归树的行走方式来将tree-sitter树进行简化,将tree-sitter的节点视作原子来处理。但有两个例外。

(1) Tree-sitter语法树有时会包括不需要的一些结构,有些语法会认为字符串是一种单一的字符,而有些则会将字符串视作为复杂的结构,此时的分隔符就会将字符串分割开。

tree-sitter_parser.rs使用atom_nodes来标记特定的tree-sitter节点为平原子,即使该节点存在子节点。

(2) Tree-sitter分析树包括开放和关闭定界符作为其代码。列表[1]将有一个包括[]的节点的语法树。

$ echo '[1]' > example.js
$ difft --dump-ts example.js
program (0, 0) - (1, 0)
  expression_statement (0, 0) - (0, 3)
    array (0, 0) - (0, 3)
      [ (0, 0) - (0, 1) "["
      number (0, 1) - (0, 2) "1"
      ] (0, 2) - (0, 3) "]"

tree_sitter_parser.rs使用open_delimiter_tokens来确保[]被用作包围列表内容的分隔符,而不会将其转换为原子。

Difftastic可以将出现简化语法树中不同部分的原子进行匹配。例如,如果一个[被当作一个原子,Difftastic可能会在其他地方将其与另一个]进行匹配。如果开放和关闭分界符的数量不同,最终的差异分析结果将会是不平衡的。

Lossy Syntax Trees简化的语法树

简化的语法树只存储节点内容与节点的位置,不会存储节点之间的空白,而且在差异分析的过程中,空格将会被忽略。

差异分析

Difftastic将diff计算视作为有向无环图上的寻路问题。

图表示

图中的一个顶点代表两个语法树中的一个位置。

开始顶点的两个位置都指向两个树的第一个语法节点。结束顶点的两个位置都正好在两棵语法树的最后一个节点之后。

AX A比较为例:

START
+---------------------+
| Left: A  Right: X A |
|       ^         ^   |
+---------------------+

END
+---------------------+
| Left: A  Right: X A |
|        ^           ^|
+---------------------+

从起始顶点开始,我们有两个选择:

  • 我们可以将左边的第一个语法节点标记为注意项,并推进到左边的下一个语法节点(即上面的顶点1)。
  • 我们可以将右边的第一个语法节点标记为注意项,并推进到右边的下一个语法节点上(即上面的顶点2)。
            START
            +---------------------+
            | Left: A  Right: X A |
            |       ^         ^   |
            +---------------------+
                   /       \
     Novel atom L /         \ Novel atom R
1                v       2   v
+---------------------+  +---------------------+
| Left: A  Right: X A |  | Left: A  Right: X A |
|        ^        ^   |  |       ^           ^ |
+---------------------+  +---------------------+

选择"新原子R"到顶点2将是最佳选择。从顶点2,我们可以看到有三条路线通往终点。

            2
            +---------------------+
            | Left: A  Right: X A |
            |       ^           ^ |
            +---------------------+
                   /    |   \
     Novel atom L /     |    \ Novel atom R
                 v      |     v
+---------------------+ | +---------------------+
| Left: A  Right: X A | | | Left: A  Right: X A |
|        ^          ^ | | |       ^            ^|
+---------------------+ | +---------------------+
  |                     |                    |
  | Novel atom R        | Nodes match        | Novel atom L
  |                     |                    |
  |         END         v                    |
  |         +---------------------+          |
  +-------->| Left: A  Right: X A |<---------+
            |        ^           ^|
            +---------------------+

比较路线

我们给每条边分配一个成本。将一个语法节点标记为新奇,比找到一个匹配的语法节点更糟糕,因此"新奇原子"边的成本比"语法节点匹配"边更高。

最佳路线是指从起始顶点到终端顶点成本最低的路线。

寻找最佳路线

Difftastic使用Dijkstra算法来寻找最佳(或称最低成本)的路线。

这种算法的一大优势是,我们不需要事先构建图。相对于语法节点的数量,构建整个图需要指数级的内存。相反顶点的邻居是在探索图的过程中构建的。

网上有很多解释Dijkstra的算法,但我特别推荐Red Blod Games的图搜索部分

棘手的例子

树状差异分析有时具有挑战性。本页展示了开发过程中观察到的困难情况和处理结果。

并非所有这些情况在 Difftastic 中都能很好地工作。

添加定界符

;; Before
x

;; After
(x)

可能输出:(x)

理想输出:(x)

这十分棘手,因为 x 改变了在树中的深度,但其本身却未发生改变。

并不是所有的树状差异分析算法可以处理这个例子。同时仔细地展示出范例是具有挑战性的:我们希望高亮已改变的定界符,而非他们的内容。这同样在更大的表达式中具有挑战性。

Difftastic:Difftastic 即使认为节点在不同深度,在这种情况下也能实现预期结果。

改变定界符

;; Before
(x)

;; After
[x]

理想输出:(x), [x]

正如这个例子,我们想要高亮定界符而非 x 这个内容。

Difftastic:通过树状差异分析,Difftastic 正确处理这个问题。

扩展定界符

;; Before
(x) y

;; After
(x y)

可能输出 1:(x) y, (x y)

可能输出 2:(x) y, (x y)

目前还不清楚在这种情况下,哪个结果更好。

Difftastic:Difftastic 目前显示结果 2,但这种情况下对成本模型很敏感。一些以前版本的 Difftastic 显示结果 1。

缩小定界符

;; Before
(x y)

;; After
(x) y

这与扩展定界符的情况类似。

使定界符不连贯

;; Before
(foo (bar))

;; After
(foo (novel) (bar))

理想输出:(foo (novel) (bar)

很容易会变成 (foo (novel) (bar)), 其中后一组的定界符会被选中。

重新组织大节点

;; Before
[[foo]]
(x y)

;; After
([[foo]] x y)

我们想高亮 [[foo]] 被移到了括号内。然而,一个简单的语法差异者更倾向于认为在前面删除 () 并在后面增加 (),因为这是最小的差异表现(见议题 #44)。

在列表内重新排列

;; Before
(x y)

;; After
(y x)

理想输出:(y x)

我们想高亮列表内容,而非定界符。

中间插入

// Before
foo(bar(123))

// After
foo(extra(bar(123)))

理想输出:foo(extra(bar(123)))

我们想把 foobar 看作是未改变的。这种情况对树进行自下而上然后自上而下匹配的衍合算法具有挑战性。

标点符号元素

// Before
foo(1, bar)

// After
foo(bar, 2)

可能输出:foo(bar, 2)

理想输出:foo(bar, 2)

() 内有两个元素,我们可以认为 bar, 中有一个未改变(但不能认为两者都不改变,因为它们已经重新排序)。

我们想把 bar 看作是未改变的,因为它相比于 , 元素更加重要。在语言不可知的方式下完成这一点存在困难,所以 Difftastic 有一个小的低优先级标点符号元素列表。

滑块(平移)

在基于文本的差异分析中,滑块是一个常见的问题,即行与行之间以混乱的方式进行匹配。

它们通常看起来像这样。差异分析必须任意选择一个包含分隔符的行,但它选择了错误的行。

+ }
+
+ function foo () {
  }

git-diff 有一些启发式方法(比如 Patience Diff)降低这种风险,但这个问题仍可能发生。

在树状差异分析时也有类似的问题。

;; Before
A B
C D

;; After
A B
A B
C D

可能输出:

A B
A B
C D

理想输出:

A B
A B
C D

理想情况下,我们更期望将连续节点标记为新的。从最长共子序列算法(LCS)看,这两个选择等价。

滑块(嵌套)

// Before
old1(old2)

// After
old1(new1(old2))

可能输出:old1(new1(old2))

理想输出:old1(new1(old2))

正确的答案是取决于语言。大多数语言希望优先使用内部定界符,而 Lisps 与 JSON 则期望使用外部定界符。

最小化深度改变

// Before
if true {
  foo(123);
}
foo(456);

// After
foo(789);

我们认为 foo(123)foo(456) 中,哪个与 foo(789) 匹配?Difftastic 优先考虑 foo(456),因为其优先考虑相同嵌套深度的节点。

有少量相似处的替代做法

// Before
function foo(x) { return x + 1; }

// After
function bar(y) { baz(y); }

可能结果:function bar(y) { baz(y); }

在这个例子中,我们删除了一个函数并写了一个完全不同的函数。树状结构差异分析可能会匹配 function 和外部定界符,从而导致高亮许多令人困惑的小变化。

与滑块一样,替代问题也可能发生在基于文本的行差中。如果有少量的共同行,行差就会陷入困境。但树状结构差异分析的精确、细化行为会使这个问题更加普遍。

匹配注释中的子字符串

// Before
/* The quick brown fox. */
foobar();

// After
/* The slow brown fox. */
foobaz();

foobarfoobaz 完全不同,它们的共同前缀 fooba 不应该被匹配。然而,为注释匹配共同的前缀或后缀是可取的。

多行注释

// Before
/* Hello
 * World. */

// After
if (x) {
  /* Hello
   * World. */
}

这两个注释的内部内容在技术上是不同的。然而,我们期望把它们当作是相同的。

文档注释的换行

块状评论的前缀并没有什么意义。

// Before
/* The quick brown fox jumps 
 * over the lazy dog. */

// After
/* The quick brown fox immediately
 * jumps over the lazy dog. */

里面的内容已经从 jumps * over 变成了 immediately * jumps over。然而,* 是装饰性的,我们并不关心它的移动。

长字符串的小变化

// Before
"""A very long string
with lots of words about
lots of stuff."""

// After
"""A very long string
with lots of NOVEL words about
lots of stuff."""

将整个字符串字头高亮为被删除并被一个新的字符串字头取代是正确的。然而,这让人很难看出实际改变了什么。

很明显,变量名应该被元素化处理,并且注释是安全的,可以显示子字符串的变化。但不清楚如何处理一个 20 行字符串字面值的小变化。

在空格上分割字符串并加以区别很具有挑战性,但用户仍期望知道字符串内部的空白何时改变。" "" " 是不同的。

自动格式化工具的拼写

// Before
foo("looooong", "also looooong");

// Before
foo(
  "looooong",
  "novel",
  "also looooong",
);

自动格式化(例如 Prettier)有时会在格式化时添加或删除标点符号,其中逗号和括号最常见。

语法差异可以忽略空白处的变化,但它必须假定标点符号有意义。这可能导致标点符号的变化被突出显示,而这可能与相关的内容变化相差甚远。

新空行

空行对于语法差异分析来说是一种挑战。我们要比较的是语法标记,所以我们不会看到空行。

// Before
A
B

// After
A

B

一般来说,我们期望语法差异能够忽略空行。在第一个例子中,这应该不会显示任何变化。

这有时是有问题的,因为它可以会意外地隐藏被重新格式化的代码。

// Before
A
B

// After
A
X

Y
B

在第二个例子中,我们插入了 X 和 Y 以及一个空行。我们想把空行作为一个补充来高亮。

// Before
A


B

// After
A
X
B

在第三个例子中,语法差异分析只看到了一个增加。从用户角度来看,也有两个空行被删除。

无效语法

我们不能保证我们得到的输入是有效的语法。即使代码是有效的,它也可能使用解析器不支持的语法。

Difftastic:如果发生任何解析错误,Difftastic 将退回到基于文本的差异,以避免差异不完整的语法树。发生这种情况时,文件头会报告错误计数。

$ difft sample_files/syntax_error_1.js sample_files/syntax_error_2.js
sample_files/syntax_error_after.js --- Text (2 errors, exceeded DFT_PARSE_ERROR_LIMIT)
...

用户可以选择通过将 DFT_PARSE_ERROR_LIMIT 设置为一个更大的值,加入语法差异分析。在这种模式下,Difftastic 会将树状差异分析的错误节点看作元素,并像通常一样进行树状差异分析。

贡献

构建

rustup安装Rust,然后克隆代码。

$ git clone git@github.com:Wilfred/difftastic.git
$ cd difftastic

Difftastic使用Cargo进行构建。

$ cargo build

调试构建的速度明显比发布构建的速度慢。对于超过50行的文件,通常建议使用一个优化的构建。

$ cargo build --release

Manual说明书

这个网站是用mdbook。mdbook可以用Cargo安装。

$ cargo install mdbook

然后你可以使用mdbook二进制文件来建立和在本地运行网站。

$ cd manual
$ mdbook serve

API文档

你可以浏览由rustdoc生成的内部API文档在这里

Difftastic的内部文档在docs.rs上没有提供,因为它不支持二进制工具箱

测试

$ cargo test

sample_files/中也有几个文件你可以使用。

测试difftastic的最好方法是在真实项目查看历史。设置GIT_EXTERNAL_DIFF指向你当前的构建。

例如,你可以在自己的源代码上运行Difftastic。

$ GIT_EXTERNAL_DIFF=./target/release/difft git log -p --ext-diff -- src

记录

Difftastic使用pretty_env_logger库来记录一些额外的调试信息。

$ DFT_LOG=debug cargo run sample_files/old.jsx sample_files/new.jsx

请参阅env_logger以获得完整的细节。

调试

如果你有一个特别慢的文件,你可以使用 cargo-flamegraph 来查看是哪些函数慢的。

$ CARGO_PROFILE_RELEASE_DEBUG=true cargo flamegraph --bin difft sample_files/slow_1.rs sample_files/slow_2.rs

内存的使用情况也是值得关注,因为图的遍历错误会导致巨大的内存消耗。

$ /usr/bin/time -v ./target/release/difft sample_files/slow_1.rs sample_files/slow_2.rs

如果定时测量有噪音,Linux的perf工具将报告 执行的指令,这也是更加稳定的。

$ perf stat ./target/release/difft sample_files/slow_1.rs sample_files/slow_2.rs
$ perf stat ./target/release/difft sample_files/typing_1.ml sample_files/typing_2.ml

还有很多剖析技术在The Rust性能手册中讨论了。

发布

使用Cargo创建一个新的版本,并在git中标记它。Difftastic有一个帮助脚本:

$ ./scripts/release.sh

现在你可以增加Cargo.toml中的版本,并在 CHANGELOG.md加一个新的条目。

包管理

Git Subtrees

Tree-sitter有时被打包在npm上,有时被打包在crates.io上,并且它们的发布频率不一样。Difftastic使用git subtrees(而不是git submodules)来追踪解析器。

升级解析器

如果要更新解析器,可以从上游的git仓库拉取提交。例如,下面的命令将更新Java解析器:

$ git subtree pull --prefix=vendored_parsers/tree-sitter-java git@github.com:tree-sitter/tree-sitter-java.git master

如果要查看每个解析器最后一次更新的时间,请使用以下的Shell命令:

$ for d in $(git log | grep git-subtree-dir | tr -d ' ' | cut -d ":" -f2 | sort); do echo "$d"; git log --pretty="  %cs" -n 1 $d; done

添加解析器

寻找解析器

Difftastic的新解析器必须完整且合理地维护。

有许多解析器可用,网站包括一些著名的解析器列表

添加源码

一旦你找到一个解析器,需要将其作为git的subtree添加到vendored_parsers/中。我们会使用tree-sitter-json作为例子。

$ git subtree add --prefix=vendored_parsers/tree-sitter-json git@github.com:tree-sitter/tree-sitter-json.git master

配置编译过程

Cargo不允许软件包包含Cargo.toml。需要在src/解析器子目录中添加一个符号链接。

$ cd vendor
$ ln -s tree-sitter-json/src tree-sitter-json-src

现在你可以通过在build.rs中加入目录,并将解析器添加到构建中。

TreeSitterParser {
    name: "tree-sitter-json",
    src_dir: "vendored_parsers/tree-sitter-json-src",
    extra_files: vec![],
},

如果你的解析器包括用于语法的自定义C或C++文件(例如,一个scanner.cc),请将它添加到extra_files中。

配置解析器

为你的语言在tree_sitter_parser.rs中增加一个条目。

Json => {
    let language = unsafe { tree_sitter_json() };
    TreeSitterConfig {
        name: "JSON",
        language,
        atom_nodes: vec!["string"].into_iter().collect(),
        delimiter_tokens: vec![("{", "}"), ("[", "]")],
        highlight_query: ts::Query::new(
            language,
            include_str!("../vendored_parsers/highlights/json.scm"),
        )
        .unwrap(),
    }
}

name是用户节目中显示的可读名称。

atom_nodes是一个树形节点名称的列表,这些节点应被视为原子。即使这些节点有子节点,也应被视为原子。这对于字符串表面之或插值字符串非常常见的,因为在这种情况下,节点可能有用来表示开头和结尾的引用号。

如果你没有设置atom_nodes,你可能会主要添加/删除的内容显示为白色。这通常表面了子节点的父节点应该被当作原子。

delimiter_tokens是Difftastic存储在闭包节点上的定界符。这使得Difftastic能够区分划线符号和语言中的其它标点符号。

如果你不设置delimiter_tokens,Difftastic会单独考虑这些标记,并会认为是添加了(,但是)没有发生变化。

你可以使用difft --dump-ts foo.json来查看树状解析器的结果,并使用difft --dump-syntax foo.json来确认你已经正确设置了原子和定界符。

配置滑块

请为你的语言在sliders.rs中添加入口。

配置语言检测

更新guess_language.rs中的from_extension以检测新的语言。

"json" => Some(Json),

也可能有与你的语言相关的文件名或shebangs。GitHub的语言定义是针对常见文件扩展名的一个有用来源。

语法高亮(可选)

要为你的语言添加语法高亮,如果有的话,你还需要在queries/highlights.scm文件一个符号链接。

$ cd vendored_parsers/highlights
$ ln -s ../tree-sitter-json/queries/highlights.scm json.scm

添加一个回归测试

最后,为你的语言添加一个回归测试,这样可以确保你的测试文件的输出不会意外改变。

回归测试文件存在于sample_files/中,其形式为 foo_1.abcfoo_2.abc

$ nano simple_1.json
$ nano simple_2.json

运行回归测试脚本并更新.expect文件。

$ ./sample_files/compare_all.sh
$ cp sample_files/compare.result sample_files/compare.expected

词典

原子: 原子是Difftastic语法树结构中的一个项目,没有子项。它代表着字面量、变量名以及注释。 另见'list'。

分隔符: 即一个成对的语法。一个列表有一个开放定界符和一个封闭定界符,例如[]。分隔符不可以是标点符号(例如,beginend)以及空字符串(例如:infix语法转换为Difftastic的语法树)。

LHS: 即Left-hand side。Difftastic会对比两个文件,而LHS是指第一个文件。另见'RHS'。

列表: 列表是Difftastic语法树结构中的一个项目,它有一个开放定界符、子项和一个封闭定界符。它代表表达式和函数定义等东西。另见'atom'。

注意项: 一个增加或一个减少。如果语法只出现在被比较的两个项目中的一个,那么它就是一个注意项。

RHS: 即Right-hand side。Difftastic会对比两个文件,而RHS是指第一个文件。另见'LHS'。

: 一个没有父节点的语法树。根代表被差异的文件中的顶级定义。

语法节点: Difftastic的语法树结构中的一个项目。可以是一个原子或一个列表。

字符: 一小段由Difftastic跟踪的语法(例如$x, function]),用于高亮显示和对齐显示。它是原子或者是一个非空的分隔符。

其它项目

有许多不同的工具可以比较文件。本说明书的这一个部分讨论了其他影响到Difftastic的工具。

树状差异分析

本页总结了一些其他可用的树形差异分析工具。

如果你很着急,可以先看看Autochrome。它的能力很强,并且对设计有着很好的描述。

如果你对学术文献的摘要感兴趣,这个帖子(和它附带的论文--在CC BY-NC的许可下可以被复制)将是一个很好的资源。

json-diff (2012)

语言:JSON 算法:Pairwise comparison
输出:CLI colours

json-diff展示了JSON文件的结构层面的差异分析。如果两者是不完全匹配的,那么它们的子树将是完全不同。例如,"foo"["foo"]是完全不同的。

可以注意的是,json-diff的结果显示十分方便查看。

GumTree (2014)

语言:约有10种编程语言 分析器:多种,包括 srcML
算法:Top-down,随后bottom-up 输出:HTML,Swing GUI或者text

GumTree可以分析多种编程语言,并且进行基于树结构的差异分析,输出一个HTML的结果界面。

GumTree算法在Falleri等人的相关论文《细粒度源码差异分析》中有所描述(DOI, PDF)。它对相同的子树进行贪婪的自下而上的搜索,随后进行自下而上的搜索来匹配其余的子树。

Tree Diff (2017)

语言:S-表达式数据格式 算法:A*搜索 输出:合并后的S-表达式文件

Tristan Hume在他2017年实习期间和在Jane Street期间写了一个树状差分算法。源代码是不可以用的,但是他写了一篇博客来对该设计进行了深入讨论。

该项目找到了Jane Street用作配置文件的s-表达式文件之间的最小差异。它使用了A*搜索来找到他们之间最小的差异,兵建立一个具有:date-switch进行标记差异的新的s-表达式文件。

(Jane Street一样有patdiff,但那似乎是一个面向行的差异分析,并不带着一些空格及整数差异显示。这个工具它并不理解在"foo "中的空格是具有意义的。)

Autochrome (2017)

语言:Clojure 分析器:Custom,并保留注释
算法:Dijkstra算法(A*搜索的先前版本)
输出:HTML

Autochrome使用了一个定制的、保留注释的解析器来分析Clojure。Autochrome使用Dijkstra算法来比较语法树之间的差异。

Auto chrome的网页包括该算法的工作实例以及对该设计权衡的讨论。这是一个用来了解树形差异分析的重要资源。

graphtage (2020)

语言:JSON, XML, HTML, YAML, plist, and CSS
解析器:json5, pyYAML, ignores comments
算法:Levenshtein距离 输出:CLI colours

graphtage通过将结构化数据解析为通用文件格式,随后进行差异分析。它甚至允许比较JSON文件和YAML文件之间的区别。

与json-diff一样,它不认为 ["foo"]"foo"之间有任何相似之处。

Diffsitter (2020)

解析器:Tree-sitter 算法:LCS(Longest-common-subsequence)
输出:CLI colours

Diffsitter是另一个使用了tree-sitter解析器的差异分析工具。它使用了LCS分析语法树中的子树

sdiff (2021)

语言:Scheme 解析器:Scheme内置的read,并忽略注释
算法:Chawathe论文中的MH-Diff
输出:CLI colours

Semantically meaningful S-expression diff: Tree-diff for lisp source code 在FOSDEM 2021中被发表。