我的项目中想应用git diff的文件变更比对性能,但git diff返回的格局是纯文本且未解析的。网上找了相干的库,像是parse-git-patch,应用的是git format-patch命令生成的补丁文件,无奈间接接管命令行中返回的文本格式,找了几个都是这样,所以罗唆就本人入手实现一个。

一般解决常见文本个别都是用正则,但这里是大段的文本,用正则即便写进去也很难以保护。网上有篇文章就是讲述JavaScript实现的逻辑,但用JavaScript解决字符串又比拟繁琐。这里选用语法分析生成器来实现。

网上的文章常常能看到形象语法树(AST)这个词,将人类编写的文本转换成计算机可初步读懂的数据结构称之为AST,而在AST之前须要先对文本做词法剖析语法分析,像是素日天天用的Babel里的词法和语法分析器就是Babylon

语法分析器比拟繁琐且干燥,所以又呈现语法分析器的生成器。所有能用JavaScripts写的终将用JavaScript写,前端天然也有相应可用的库,比拟闻名的有PEGjsJison两个库。有了生成器咱们能做什么呢?小到代替正则,大到实现本人的畛域特定语言(DSL)。像是曾一度最有心愿代替JavaScriptCoffeeScript,他的V2版本就是用Jison库做本人的语法分析器。

PEGjs语法规定很容易上手,这里应用PEGjs来实现解析器。(PEGjs曾经没有维护者了,在应用的过程中意外发现有人另外建了一个分支版本在保护PEGgy,能够无缝过渡)

PEGGY

peggy能够在浏览器中应用,也能够引入我的项目中应用。又或是编写好规定,应用命令生成解析器提供应用。

简略的规定能够间接应用在线版的调试 https://peggyjs.org/online.html。

装置

npm install peggy

网页应用的话能够间接引入peggy.min.js。

应用

const PEG = require("peggy");// 导入规定 生成解析器const parser = PEG.generate(RULE, {  trace: true,});// 应用解析器const result = parser.parse(TEST_DATA, {    tracer: true,});

generate函数和parse函数都能够传入参数。

generate函数参数,网上有向南已翻译了我这里就摘录过去

  • allowedStartRules: 指定parser开始的rule. (默认是文法中第一个rule.)
  • cache: 如果设置为true, parser会将parse的后果缓存起来, 能够防止在极其状况下过长的解析工夫, 但同时它带来的副作用是会使得parser变慢(默认false).
  • dependencies: 设置parser的依赖, 其值是一个对象, 其key为拜访依赖的变量, 而value为须要加载的依赖module id.只有当format参数被设置为"amd", "commonjs", "umd" 该参数才失效. (默认为{})
  • exportVar: Name of a global variable into which the parser object is assigned to when no module loader is detected; valid only when format is set to "globals" or "umd" (default: null).
  • format: 生成的parser格局, 可选值为("amd", "bare", "commonjs", "globals", or "umd"). 只有output设置为source, 该参数才失效
  • optimize: 为生成的parser抉择一个优化计划, 可选值为"speed"或者"size". (默认"speed")
  • output: 设置generate()办法返回格局. 如果值为"parser", 则返回生成的parser对象. 如果设置为"source", 则返回parser source字符串
  • plugins: 要应用的插件
  • trace: 追踪parser的执行过程(默认是false).

parse函数参数

  • startRule:起始解析的rule名称
  • tracer:展现parser执行rule的过程
  • ...(任意其余参数):应用options变量接管参数,能够在parse函数中传入自定义参数,能够给解析规定提供一些配置性能

规定

规定很简略,会举一个例子说说,形容的会比拟繁琐,疾速了解能够间接看官网文档,一共没几条规定。

整体样子大略是这样:

在VSCode里能够装置Peggy Language插件,能提供语法高亮、跳转、谬误提醒等性能。
{{    function prefix(str){        return `Interger:${str.join("")}`    }}}/* 开始 */start = integer _ contentcontent    = "(" integer ")"    / "[" "TEXT:"i [a-zA-Z+-]* "]"    / "{" .* "}"    integer "整数"    = digits:[0-9]+ { return prefix(digits); }// 匹配空格_ = SPACE+ { return "" }SPACE = " " / "\t"

能够应用在线版,输出123456 (00)或是123456 [text:Yes]相似格局须要解析的字符串,能够看到匹配出的数据。

这个简短的规定把罕用的规定都列举进去了,来解释一下。

{{    function prefix(str){        return `Interger:${str.join("")}`    }}}

{{}}初始化器

最顶部的花括号的区域称之为初始化器,用一个{}两个{{}}括号定义都能够,前后括号数匹配的起来就行。在这里能够自定义一些JavaScript代码。

能够通过options拿到传参做预处理,也能够定义一些工具函数供后续的规定应用。

接下来遇到第一个规定:

/* 开始 */start = integer _ content

/**///正文

与其余编程语言一样反对/**///的正文。

start规定名

start是规定名称,能够任意拟定,只须要合乎JavaScript起名标准即可。

integer _ content解析表达式

=等号前面是解析的表达式,这里的表达式是integer _ content

是不是看不懂这些代表什么意思?这就阐明其中的表达式很可能是另一个规定名,所以能够持续往下看:

content    = "(" integer ")"    / "[" "TEXT:"i [a-zA-Z+-]* "]"    / "{" .* "}"

这里看到下面解析表达式中的其中一个规定content

空白符

会发现这里定义格局不太一样,等号能够换到第二行去。

pegpy在解析规定时,词法剖析先会把文本宰割成一个个token(像是="/、字符串、正文等),不同于JavaScript,空白符不作为token,所以在之间咱们能够任意换行或者插入空格。

/符号

/符号相似于的意思,代表了符号前后是两个规定,先匹配后面的规定,当不匹配时持续匹配前面的。能够有任意多个规定连贯,直到所有的规定匹配不胜利,抛出异样。

""引号

先来看第一个表达式"(" integer ")",文本数据咱们用引号引起,所以前后是匹配两个括号。

两头的仍然不意识,阐明是另一个规定名。所以能够揣测这段是解析两个括号之间的某些字符,依据名称是匹配两个括号之间的数字。

再来看下一条:

"[" "TEXT:"i [a-zA-Z+-]* "]",这里一样,但更简单一些,匹配方括号之间的值,但这里没有援用其余规定,而是写了明确的匹配信息。

“”i疏忽大小写

"TEXT:"i一样,引号两头是文本,所以匹配的是TEXT:,前面的i是做什么的?这跟正则一样,加i是疏忽字符串的大小写,所以也能够匹配text等模式。

[]从汇合中匹配

[a-zA-Z+-]*看的是不是很像正则?这里就跟正则一样,匹配az即所有的英文字母,A-Z是大写的英文字母,同时还有+-两个符号。

*+匹配次数管制

[]方括号示意只会匹配其中所列的一个,所以在最初的*示意匹配次数,零次或屡次。

同样,还有+号,代表一次或屡次。

?匹配失败返回null

有时在其余解析表达式里还能看到?符号,?不是像正则那样代表零次到一次,而是示意匹配胜利就返回后果,不胜利返回null

同时也没有{}表白额定的反复次数性能,只有*+两个符号,绝对于正则性能没那么丰盛。

"[" "TEXT:"i [a-zA-Z+-]* "]",所以这一段就是匹配方括号中以TEXT:结尾,前面的所有大小写字母及加减号字符。

第三条规定: "{" .* "}"

.任意一个字符

按之前学到的,这是匹配花括号中的内容,*代表匹配次数,那.呢?.在这里代表匹配任意一个字符,包含空格之类的字符。所以这句实际上是匹配花括号中的所有内容。

到这里其实曾经把根底规定学完了,说的比拟啰嗦,实际上很简略,没几个规定,下面的连起来是这样的:

匹配圆括号中的合乎integer规定的信息,如果有不合乎的,则换到下一个规定,匹配方括号以TEXT:不分大小写结尾的内容,其中内容只能是大小写字母及加减号字符这些字符,如果有其余字符则匹配不胜利,跳到最初一个规定,匹配花括号中的所有字符。再不胜利,则弹出错误信息。

再往下看,就能看到始终被提及的integer规定:

integer "整数"    = digits:[0-9]+ { return prefix(digits); }

规定别名

这里在规定前面等号后面又多了一个带引号字符串"整数",这是规定的别名,调试时应用,也能够同后面其余规定一样省略掉。

digits:[0-9]+解析表达式标签

在解析表达式中能够看到除了后面已知的局部[0-9]+,还多了 一个冒号的语法,这是给解析后果起一个名称,不便前面的action调用。

{ return prefix(digits); }解析表达式的action

绝对于其余规定,这个规定咱们在开端定义了相似函数的货色,这就是JavaScript函数。咱们能够在解析表达式之后减少花括号,其中写JavaScript代码。像是这句,就是调用了咱们在初始化器中定义的函数,将获取到的文本处理一下再返回。

这个就是peggy自在的中央,当自身语法解析能力不够的时候,或者解析进去的文本比拟系统(字符串经常会被宰割成一个个存到数组中),这时候咱们就须要用到action

这里联合起来integer规定是匹配一到多个数字字符,并且解决成间断的字符串(解决前匹配进去的数据是[1,2,3,4,5]这样的,为了不便浏览与应用,往往须要解决成12345),并且加上Interger:的前缀。

最初一段是解析空格的规定:

// 匹配空格_ = SPACE+ { return "" }SPACE = " " / "\t"

之前有提到,在token之间的空白符会疏忽,所以如果文本中有空格,也须要独自匹配。

用冒号即可" ",如果须要匹配制表符之类的也能够间接写"\t"

为了不影响解析表达式的浏览,命名为_不便辨认。同时将匹配到的后果用一个action转换为空,不便后续将数据处理掉。

运行起来匹配文本返回的数据大略是这样,有需要的话能够再加action将数据处理成指定格局:

git diff 的数据格式

说了这么多,当初才能够开始进入到咱们要做的需要中。要解析git diff返回数据,天然先要晓得格局标准。

返回数据大略长这样:

diff --git a/package.json b/package.jsonindex cb2f4bc..35455a2 100644--- a/package.json+++ b/package.json@@ -1,13 +1,14 @@ {   "name": "peg-git-diff-parser",-  "version": "0.0.0",+  "version": "1.0.0",   "description": "git diff 文本解析器",-  "main": "index.js",+  "main": "src/index.js",   "scripts": {     "build": "peggy -o dist/gitDiffParser.js src/gitDiffParser.peggy",     "test": "node src/index.js"   },   "author": "LnnCoCo",+  "new": "new",   "license": "ISC",   "dependencies": {     "peggy": "^1.2.0"

咱们一行一行来阐明。

diff --git a/package.json b/package.json

diff --git是固定字符,a和b示意变动前与变动后的文件。

index cb2f4bc..35455a2 100644

..是分隔符,示意index区域hash为cb2f4bc的对象,与工作区hash为35455a2的对象。100644为对象的模式,100代表一般文件,644代表权限信息。

--- a/package.json+++ b/package.json

比拟的文件名信息,---变动前,+++变动后。

@@ -1,13 +1,14 @@ {   "name": "peg-git-diff-parser",-  "version": "0.0.0",+  "version": "1.0.0",   "description": "git diff 文本解析器",-  "main": "index.js",+  "main": "src/index.js",   "scripts": {     "build": "peggy -o dist/gitDiffParser.js src/gitDiffParser.peggy",     "test": "node src/index.js"   },   "author": "LnnCoCo",+  "new": "new",   "license": "ISC",   "dependencies": {     "peggy": "^1.2.0"

从这里开始就是每个变动的信息块。以@@...@@起头,有多个块就有多个@@...@@,这里目前只有一个。

-1,13 +1,14-代表变动前、+代表变动后,1,13,代表从第一行开始展现之后的十三行。1,14同理,因为有一行是新增,所以变动后会多一行。

而后接下去的是文本,这里容易看见的是两个符号,实际上是有三个:+-空格。这个对解析很重要,所有行都是以这三个起始。-是变动前,+是变动后,空格是未变更内容。

未变更内容展现逻辑是,以变动行为核心,展现高低最多三行内容。

这样整个git diff输入格局就清晰了。

在查资料的时候发现理论还有新增已删除重命名,文件还有分二进制非二进制的状况,但在单纯的命令行git diff状况下,这些是无奈输入数据的,所以而且以后需要也没用到,就疏忽了这些其余状况。

实现解析规定

新建一个我的项目,而后装置peggy

新建一个gitDiffParser.peggy文件来编写规定,其余读取测试数据传入插件之类测试性代码能够本人补充。

残缺的我的项目地址:peg-git-diff-parser

在VSCode中能够装置Peggy Language插件,能提供高亮语法和谬误提醒之类的。

先定义一些公共的规定

/** * 公共定义 */// 门路文件名filePath = hit:[A-Za-z0-9\\\/\._\-@$()*&^+!]+ { return hit.join("") }// 换行LINE_END = "\r\n" / "\n"// 空白符__ = SPACE* { return "" }_ = SPACE+ { return "" }SPACE = " " / "\t"

而后开始吧。

diff --git a/package.json b/package.json

这里变动的内容就只有文件名,所以其余局部都能够写死,大略是这样。

header = "diff --git" _ filePath _ filePath LINE_END

很简略吧,只须要把变动的局部规定匹配起来就行了。

这里为了下层不便辨认,所以减少了标签,包成了对象返回了。

/**  * 首行 **/header  = "diff"i _ "--git"i _ 'a'beforePath:filePath _ 'b'afterPath:filePath LINE_END  {    return {      beforePath,      afterPath    }  }

接下来都差不多。间接来看看比拟麻烦的变动块的数据解析。

先解析头部

@@ -1,13 +1,14 @@

很容易,从@@开始定位,到@@完结。

changeHeader  = "@@" _ beforeChangeLine:changeLineInfo _ afterChangeLine:changeLineInfo _ "@@" LINE_END  {    return {      changeHeader: `@@ ${beforeChangeLine.text} ${afterChangeLine.text} @@`,      beforeChangeLine,      afterChangeLine    }  }  // 变动行信息  第N行开始,一共N行  1,6 第一行开始,一共6行(变动的-+两行算一行)changeLineInfo  = type:([-|+]) line:([0-9]+","[0-9]+)  {    const lineFormatText = formatLine(line);    return {      text: `${type}${lineFormatText}`,      type,      line: lineFormatText    }  }

因为行信息写在一起比拟麻烦,所以另外写了个formatLine函数解决。

而后就是麻烦的中央,之后的数据是不定长的。而且+-空格符号在其余部分也会呈现,所以只能限定结尾的局部匹配到这三个别离进入三个不同的规定中。

但这里没有像是正则一样的结尾标识符,所以换个角度想,每一行的开始,在上一行必然有一个换行符,所以能够这样定义LINE_END "-"LINE_END "+".

之后的内容须要全副匹配,间接.*必定是不行的,会将之后的所有信息一起匹配进去。好在文档中还写到有[]能够配合^符号用来反向匹配,比方[^ABC]就是匹配除了ABC的任意字符。

比拟遗憾的是无奈间接将规定配合^符号,不然能够写出较为简单的匹配逻辑。所以目前信息曾经能够解决了,变动块的数据必然是一行的,所以咱们只有辨认到换行符就进行匹配即可。

规定如下:

changeBeforeContent = LINE_END "-" hit:[^\r\n]+ {  return {    type: "-",    text: hit.join("")  }}changeAfterContent = LINE_END "+" hit:[^\r\n]+ {  return {    type: "+",    text: hit.join("")  }}

但咱们变动数据是在两头,其前后还有上下文相干的背景数据,这些数据是以空格结尾的,或者是当结尾不是+-符号的时候,就全作为上下文相干内容解决。所以规定很简略

changeContext = . {  return null}

而后咱们将规定组合一下,这样四种状况就能包含所有文本了。

changeChunk  = line:changeHeader    / beforeContent:changeBeforeContent    / afterContent:changeAfterContent    / changeContext

但这样只能匹配一行内容,所以咱们还须要加上次数信息和将上下文内容返回的空数据过滤了。

changeChunk  = hit:(      line:changeHeader      / beforeContent:changeBeforeContent      / afterContent:changeAfterContent      / changeContext    )*    {      return hit.filter(item => item)    }

再入口处将所须要的信息格式整顿一下,就能返回咱们预期的格式化后的diff数据了。

具体的细节能够到这里查看:peg-git-diff-parser

相干浏览

Peggy官网

Peggy Github

PEG.js 文档 [译]

不懂编译也能造JavaScript解释器 第0章、 前置常识

如何实现一个 Git Diff 解析器

读懂diff