简介

Difftastic是一个根据文件的语法的结构化比较工具。它支持超过20款编程语言,当使用它的时候,就会知道它有多么的

Difftastic是一款开源软件(使用MIT许可证)并且可以在Github上获得

该说明书会表明当前版本0.39.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 realease

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

Packaging status

通过homebrew安装(在macos或者Linux平台)

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

$ brew install difftastic

从源码安装

编译要求

Difftastic是使用Rust编写的,所以你需要安装Rust。我推荐使用rustup来安装Rust。

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

Build编译

你可以下载并通过Cargo(它是Rust的一部分)来编译difftastic on crates.io

$ cargo install difftastic

Difftastic使用cc程序箱来构建C/C++的依赖关系。这使得你可以通过环境变量CCCXX来控制使用的编译器(参照see the cc docs)。

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

(可选)安装MIME数据库

如果有一个MIME数据库,Difftastic将使用它来更准确地检测二进制文件。这个也是使用file命令时所调用的同一个数据库,你可能已经安装了它。

MIME数据库的路径是在XDG的规定下

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

使用方法

差异比较文件

$ difft sample_files/before.js sample_files/after.js

差异比较文件夹

$ difft sample_files/dir_before/ sample_files/dir_after/

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

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

语言检测

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

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

$ difft --language cpp before.c after.c

Options选项

Difftastic包括一系列的命令行选项,见difft --help获得完整列表。

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

例如,DFT_BACKGROUND=light就相当于--background=light。这在使用VCS工具例如git的时候会很有用,因为此时无法直接调用difft二进制文件。

Git

Git支持使用外部的diff工具。你可以使用GIT_EXTERNAL_DIFF来进行一键git命令。

$ 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

# Set git configuration for the current repository.
$ git config diff.external difft

# Set git configuration for all repositories.
$ 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 是一款使用不同diff工具来查看当前修改的git命令。如果你想要偶尔使用Difftastic的话,这将会非常有用。

添加下列内容到你的.gitconfig中就会让Difftastic作为你的difftool工具。

[diff]
        tool = difftastic

[difftool]
        prompt = false

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

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

$ git difftool

我们还推荐使用下列设置来获得最好的difftool体验。

# Use a pager for large output, just like other git commands.
[pager]
        difftool = true

# `git dft` is less to type than `git difftool`.
[alias]
        dft = difftool

Mercurial

Mercurial支持另外的diff工具与Extdiff扩展。通过在你的.hgrc中添加extensions条目来启用它。

[extensions]
extdiff =

然后你可以运行hg extdiff -p difft命令(假定difft二进制文件存放在你的$PATH中。

你也可以为带有hg的difftastic的语句定义一个别名。在你的.hgrc中添加以下内容,以便用hg dft运行Difftastic。

[extdiff]
cmd.dft = difft
opts.dft = --missing-as-empty

hg log -p

Mercurial没有办法改变默认的差异工具,至少就作者所知。

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

GIT_PAGER_IN_USE=1 hg dft -r .^ -r . | less

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

支持语言

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

编程语言

语言使用的解析器
Bashtree-sitter/tree-sitter-bash
Ctree-sitter/tree-sitter-c
C++tree-sitter/tree-sitter-cpp
C#tree-sitter/tree-sitter-c-sharp
Clojuresogaiu/tree-sitter-clojure (branched)
CMakeuyha/tree-sitter-cmake
Common LisptheHamsta/tree-sitter-commonlisp
DartUserNobody14/tree-sitter-dart
Elixirelixir-lang/tree-sitter-elixir
Elmelm-tooling/tree-sitter-elm
Elvishckafi/tree-sitter-elvish
Emacs Lispwilfred/tree-sitter-elisp
Gleamgleam-lang/tree-sitter-gleam
Gotree-sitter/tree-sitter-go
Hackslackhq/tree-sitter-hack
Haskelltree-sitter/tree-sitter-haskell
Janetsogaiu/tree-sitter-janet-simple
Javatree-sitter/tree-sitter-java
JavaScript, JSXtree-sitter/tree-sitter-javascript
Juliatree-sitter/tree-sitter-julia
Kotlinfwcd/tree-sitter-kotlin
Luanvim-treesitter/tree-sitter-lua
Makealemuller/tree-sitter-make
Nixcstrahan/tree-sitter-nix
OCamltree-sitter/tree-sitter-ocaml
Perlganezdragon/tree-sitter-perl
PHPtree-sitter/tree-sitter-php
Pythontree-sitter/tree-sitter-python
Rubytree-sitter/tree-sitter-ruby
Rusttree-sitter/tree-sitter-rust (forked)
Scalatree-sitter/tree-sitter-scala
SQLm-novikov/tree-sitter-sql
Swiftalex-pinkus/tree-sitter-swift
TypeScript, TSXtree-sitter/tree-sitter-typescript
Zigmaxxnino/tree-sitter-zig

结构化文本格式

语言使用的解析器
CSStree-sitter/tree-sitter-css
HCLMichaHoffmann/tree-sitter-hcl
HTMLtree-sitter/tree-sitter-html
JSONtree-sitter/tree-sitter-json
TOMLikatyang/tree-sitter-toml
YAMLikatyang/tree-sitter-yaml

解析代码

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

使用Tree-sitter解析代码

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

$ difft --dump-ts sample_files/javascript_simple_before.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本身却未发生改变。

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

改变定界符

;; Before
(x)

;; After
[x]

正如这个包裹的例子,我们想要去高亮出定界符而不是x这个内容。

拓展定界符

;; Before
(x) y

;; After
(x y)

理想输出:(x y)

在这个例子下,我们想要去高亮y。高亮显示定界符的话可能会让x看起来有所变化。

缩小定界符

;; 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]]被移到括号内了。然而,一个简单的语法差异者更倾向于认为在前面删除(),在后面增加(),是最小的差异表现。 (见issue 44。)

在列表内重新排列

;; Before
(x y)

;; After
(y x)

理想输出:(y x)

我们想突出显示列表的内容,而不是定界符。

中间插入

// Before
foo(bar(123))

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

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

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

滑块(平移)

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

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

+ }
+
+ function foo () {
  }

git-diff有一些启发式方法来减少这种风险(比如说"patience diff"),但这个问题仍然可能发生。

接下来是一个在树状差异分析时常见的问题。

;; Before
A B
C D

;; After
A B
A B
C D

理想情况下,我们更愿意将连续的节点标记为新的,所以我们强调A B而不是B/nA。从最长公序算法的角度来看,这两种选择是等价的。

滑块(嵌套)

// 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); }

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

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

匹配注释中的子字符串

// 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

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

无效语法

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

Tree-sitter可以显示出显式的错误节点,而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库来记录一些额外的调试信息。

$ RUST_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_before.rs sample_files/slow_after.rs

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

$ /usr/bin/time -v ./target/release/difft sample_files/slow_before.rs sample_files/slow_after.rs

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

$ perf stat ./target/release/difft sample_files/slow_before.rs sample_files/slow_after.rs
$ perf stat ./target/release/difft sample_files/typing_old.ml sample_files/typing_new.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=vendor/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添加到vendor/中。我们会使用tree-sitter-json作为例子。

$ git subtree add --prefix=vendor/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: "vendor/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!("../vendor/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 vendor/highlights
$ ln -s ../tree-sitter-json/queries/highlights.scm json.scm

添加一个回归测试

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

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

$ nano simple_before.json
$ nano simple_after.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中被发表。