libgit2使用教程(十二)git tag

tag 的作用是作为重要节点的标记,在需要的时候可以直接切过去。所以在 git 管理系统,比如 github ,直接以带信息描述的 tag 作为 release。并且通常也习惯以版本编号作为 tag 名。

首先来看看仓库里有哪些 tag ,libgit2 提供了两个接口,它们有不同的作用。
1. list

输出的是 tag 的名字,并且没有更多的详细信息。
2. foreach

这个接口是通过回调的方式遍历所有的 tag。回调中的参数 name 返回的是 tag 的完整路径(refs/tags/tagname 这种形式,这样看实际上它也是一种 reference),这一点要特别注意;oid 是这个 tag 的 oid,如果要获取这个 tag 对象的指针,则需要使用 lookup 这个接口去获取——这个接口需要仓库指针作为参数,所以仓库指针需要通过 payload 传入。lookup 的时候有可能会出现找不到的情况,那是因为只有带描述的 tag 才能被找到,也就是说只有名字的轻量级 tag 是找不到的。

然后有必要解释一下什么是轻量级 tag 么?
轻量级 tag 是这个 tag 只有名字,没有其他信息。通过 git tag [tagname] 这个命令来创建;
除了轻量级 tag 之外还有一种就是带附注的 tag,这个 tag 要求创建的时候输入一段描述信息,通过 git tag -a [tagname] -m [message] 这个命令创建。

因此使用 libgit2 创建 tag 也有不同的接口来实现。
1. 轻量级 tag

因为 tag 是一种 reference 所以,它一定是以 commit 为节点的,所以 target 参数要找一个目标 commit。

2. 带附注的 tag

相比 lightweight 版本多了两个参数,一个是提交者的信息,另一个是这个 tag 附带的信息。这个信息在 github 的 release 中会对外显示。

下面介绍一个模糊搜索的功能,对应的是 git tag -l “*.*” 这个命令:

接下来是删除 git tag -d [tagname]

如果 error 返回的是 -3 也就是 GIT_ENOTFOUND 这个宏,表示并没有找到输入的这个 tag。

接下来,tag 作为一个标记,就是为了作为一个目标点可以直接切过去。使用命令行工具的命令是 git checkout [tagname],它实现的是将当前 head 切成一个空分支,这个分支指向的就是目标 tag。
然而,在代码中这个操作并不是通过 checkout 系列的接口实现,我们的目标是把对象换到 head 所以,需要的是设置当前的 head。

这里的重点是第二个参数,需要这个 tag 的 reference 形式的全名,在 foreach 的回调中可以获取到,在文件目录里也可以找得到。

最后,要将 tag 上传到远端仓库,需要在 push 的时候显示的指出。就像命令行:git push origin –tags。在代码中的重点就是 push 接口的 refspecs 参数,由这个参数指定 push 什么东西。

示例代码:sample12

libgit2使用教程(十一)git push

将本地分支上传到远端,如果前面的各个功能都掌握了的话,这个接口可以算是非常简单的了。

需要注意的细节就是 1. 身份验证;2. refspecs

验证证书的回调,是通过 git_push_options 这个结构传递的,与之前一样的 callbacks 参数。详情可以参考前面的

refspecs 是用来指定这个 push 操作的本地分支和远端分支。它的格式是本地分支远端目标分支中间以冒号隔开。

比如我要把本地的 master 推到远端,碰巧远端对应的分支也叫 master,那好,它就是这个样子的:
refs/heads/master:refs/heads/master

当然,我们必须制定远端目标,因为它不一定仅仅是 origin:

那么最终,push 操作的代码就是:

示例代码:sample11

libgit2使用教程(十)git clone

这是一个非常重要的操作。
这是一个非常简单的接口。
这是一个没多少内容又不得不提的主题。

直接上代码

需要注意的就是不要忘记身份验证,然后就开始 clone 操作了。

最后就简单介绍一下 git_clone_options 的主要参数:

checkout_opts
checkout 的配置,如果需要的话

fetch_opts
fetch 的配置,如果需要的话

bare
0 表示标准仓库,1 表示 bare 仓库

local
决定是否拷贝目标数据库而不是 fetch 。(如果目标是在本地,直接拷贝会更快一些)

checkout_branch
切到哪个分支,如果是空使用远端默认的分支

repository_cb
在这个回调里可以自己创建一个 repository

remote_cb
在这个回调里可以自己创建一个 remote

关于 bare 这个参数,不仅仅在 clone 这个操作用到。在 init 或 open 也是同样的意思。
表示只有版本记录,没有实际的工程里的代码和文件那些东西。更具体一点就是除了 .git 这个文件夹以外啥都没有。

示例代码:sample10

libgit2使用教程(九)git fetch & pull

目前 libgit2 对 pull 操作的支持还不是太好,所以目前能找到的资料都指出 pull 操作就是先 fetch 然后再 merge 目标分支。比如要实现 git pull origin master ,就先 fetch 然后将 master 切换到 head 再将 origin master 合并到 head。

那么就先 fetch

git_remote_fetch 这个接口的后三个参数都是可以传 null 的,但是如果报了下面这个错:
authentication required but no callback set
说明远端地址需要验证身份,所以我们要设置 ssh 证书的回调。

这样就完成了 fetch 操作。so easy

prune 也是 fetch 操作的一个重要参数。表示:清理掉远端已经删除的分支对应的本地分支,有点绕,不过应该还好理解吧。具体信息可以查一下下面这个命令。
git fetch -p (或 git fetch –prune)
在代码里实现只需要在 option 参数里做一个设置:

这样就强制使用 prune 参数了。另外几个可选枚举值可以去读注释。

接下来为了实现类似 pull 的效果,就是蛋疼的合并操作了,顺便可以复习一下之前的内容。
首先将本地 master 设为 head

然后去拿 origin master 的 commit

合并

解决冲突

add 和 commit

清空状态

打完收工。

示例代码:sample9

Android 自动化批量打包精粹

在项目即将上线的时候,都会遇到这样的问题:
有好多个有自己 SDK 的商店,而且都要赶同一个档期上线。
个别商店可能会有一些独特的小需求,比如 icon 的角标、开机闪屏 logo 、或者独有的功能接口要调用。
当10个 SDK 接完8个了,第1个接入的有更新了,需要接入最新版本才给上线。最要命的是接口有变动。
双方联调,找问题,各种沟通不畅。
下一个项目这些问题再重演一遍。

所以这个事情如果要做的优雅,高逼格,高效简洁,我们应该做到:
一次接入可以让所有项目通用。
各个部分充分解耦(SDK、项目、服务器),单独接入单独调试单独测试。
渠道商店配置可以由非技术人员(市场、运营)来管理。
当然不可少的打包应该有工具自动化完成。

简简单单几句话来描述这个事情,看起来挺简单,但是仔细想想,都挺空的。所以,还是要细化一下具体的东西。

一、 一次接入所有项目通用
要实现通用,就需要有一个比较稳定的接口将功能性 SDK 封装起来。封装起来的 SDK 我们可以姑且命名为 SDK 插件,各个项目调用统一的接口,而不需要关心 SDK 插件之中的具体的 SDK 。

二、各个部分解耦
实际上稳定统一的接口就是完成了解耦的第一步,我们需要梳理清楚具体部分的划分。渠道商店的 SDK 往往都会有服务器端接口部分,用作用户校验和支付验证。所以大致可以分为三个部分:SDK 插件开发、渠道商店对接服务器、客户端使用的 API。
各部分详细的设计参考下面的示意图:
结构图

开发 SDK 插件的流程:
前提条件:模板工程、可用的 SDK 、测试服务器接口
流程:通过模板创建工程 → 接入 SDK → 测试通过 → 导出 SDK 插件
SDK开发模板
当完成这一部分的时候,SDK 的对接就可以完全不需要核心项目团队的资源来支持了。这样就实现了一定程度的解耦,而核心项目部分,不用关心这些 SDK 功能的具体实现,只要接入一个用于测试各种 SDK 功能的测试 SDK 就可以无忧了。
测试SDK
从上面两个图可以看到,独立出一个 Platform Server 平台服务器来做与 SDK 相关的对接。这样的好处是,服务器端也实现了解耦;一个平台服务器可以服务于多个项目;并且不需要项目团队拿出资源来维护这个部分。而区分不同项目不同商店渠道只要以自定义标准参数的形式,制定出一套自己的标准就可以通用于所有接入的项目了。

对于测试 SDK 的设计,有必要叨唠两句。这个东西的目的是把通过 SDK 获取的数据直接给客户端,所以有些东西可以更直接一些。比如,需要登录获取一个 user id ,这种需求直接做一个输入框或者下拉列表,直接让开发人员输入或者选择 user id,就可以了。没有必要还让测试人员走一个完整的登录流程是不是?并且这样可以随意调整需要登录的账号,方便测试。支付同理,客户端需要处理在成功时会怎样,失败时会怎样,那么直接用两个按钮,返回给客户端结果就可以了。真的没有必要还调个支付宝啥的真去走个第三方支付流程。

三、 由非技术人员管理
因为实际上对于渠道商店出包的需求都是由非技术人员(运营、市场等)发起的,所以最好由非技术人员负责管理渠道商店的参数、数据、出包等。这样不仅可以解放出一小部分技术人员的生产力,最关键的是减少了沟通造成的损耗,当运营取得渠道相关参数之后,直接在工具中填写并提交然后工具自动读取后将需求的包打出来,这样这个流程一个人就可以完成了。
这个工作流的设计的关键当然是非技术人员以什么形式来提交给工具这些参数?最好的方案,当然是有一个非常友好的界面,可以让用户根据提示正确的填写各种参数以及提交资源等。但是如果在开发资源不足的情况下,退而求其次可以考虑使用 excel 表格,一般运营人员经过简单培训也是完全可以 hold 住的,只要将标准制定的全面且完善。

四、 自动化打包
首先只打一包的话使用 ant 一个命令就可以实现,并且同时完成包括 编译 打包 混淆 签名 等完整的流程。然而,最重要的是我们要根据之前填好的数据自动打出所有我们需要的包。
要实现这个需求,单靠 ant 脚本就不太现实了,虽然理论上也是可以实现的。这里还是建议使用强逻辑型的语言实现这个程序。在最后打包的环节再调用 ant 脚本完成打包的最后一步。
这个程序大框架上看是一个很简单的流水线,可以看下面的图。然后有很多的细节需要处理,这其中的任何小细节被忽略都有可能无法良好的工作。
流水线

编译 C++ 我的习惯是只编译一次,所有包使用同一个 so 这样可以最大程度的保证编译速度。需要传给 C++ 部分的一些参数可以通过 JNI 传递,运行效率是会有一些小影响,几乎可以忽略不计,可以让打包部分的复杂度降低不少。不过确实有一些特殊情况需要在 so 文件中打入一些数据,那就根据自己的情况再具体解决了。

我习惯在打每个包之前都删掉生成文件夹 bin 和 gen ,因为曾经有发生过不少次因为之前的生成文件的存在导致后来的改动并没有被编译进生成文件中。并且不要忘记,如果项目有引入 Library 的话,最好递归的去清理一些。

在对工程做任何改动之前都不要忘记做现场的保护。我的做法是,如果是对一个已经存在的文件做修改,我会把那个文件做备份和操作记录。再这个包打完或者出问题被中断后可以把现场恢复到最初的状态;如果是加入一个新的文件也要记录下来,在打完这个包将其删除。这样做可以最大程度的保障不会让当前操作污染到以后的事情。

可能有人注意到了,流水线这张图并没有提到 SDK Plugin 。实际上我赞成把 SDK Plugin 看做一个实体,只是为了描述更方便,理解更直观一些,具体存在形式最好是根据自己系统的情况量身定制。它可以只是散乱的一堆代码和一堆资源,也可以是其他合适的形式甚至是代码片段或者 patches ,如何使用需要根据自己的情况具体问题具体分析,而这个流水线图展示的是最纯粹的重要工作节点。

引入 Library 只需要修改 project.properties 中的 android.library.reference.* 参数,当然不要忘了在后边添加新的之前要先解析一下旧的,取到一个当前数字再添加新的数字。然而为什么是这个文件呢?它的秘密都在 build.xml 这个默认的 ant 脚本文件里,这个文件的注释读一读可以获得好多重要的信息。

默认的 ant 构建系统是需要自己生成 local.properties 这个文件,并且设定 sdk.dir 来显示指出 Android SDK 的路径。我喜欢把 build.xml 做个小改动,让这个变量直接访问环境变量:

不管是选择使用传统的 local.properties 还是修改 build.xml ,都必须要保证所有引入的 library 都有一份。我选择修改 build.xml 这个做法就是为了在不同环境中不用再多修改一个文件。

每一个渠道商店的包都必然会有一些常量参数,而使用这些参数的大多是相关的 SDK 。所以我的方案是生成一个常量类写到代码文件中替换掉之前埋在工程中用于测试的那个类。而这里最好将生成后的类做个 log,以便在出包后可以再检查一下数据是否有误,毕竟打错参数这种事情有时候并不容易被测试发现。

有许多 SDK 都会有要求在 AndroidManifest.xml 加一些东西,比如,声明 Activity、 Receiver 、权限等等,也有包括包名、版本号这些需要修改的东西。所以读写这个 xml 也是这个工具必不可少的一部分。那么数据是从哪里来的呢?首先,参数类的数据,肯定是从配置信息里获取的(包名、版本号、其他参数等);然后,与 SDK 有关的,肯定是不能让非技术人员来配置的,这部分就要作为 SDK Plugin 的一部分,可以在 SDK Plugin 中放一个专属这个 SDK 的 xml 配置,在这一步的时候从里边读取与工程中的 AndroidManifest.xml 合并。(有人选择使用替换特殊标示的方式来操作这个文件,这样的话就需要提前在这个文件里写很多特殊标识符,其后果是这个工程可能就不能直接编译运行调试了。想运行一下还需要先替换标识符感觉就有点累了,所以我认为还是读写 xml 是最靠谱的方案)

写 AndroidManifest.xml 的时候经常会遇到命名空间不见了的情况,表现为原文件的 android:xxxx 变成了 ns0:xxxx 。原因就是原来的 xml 命名空间被丢掉了,修改这个文件一定不要忘记手动设置一下 manifest 的命名空间:
xmlns:android=”http://schemas.android.com/apk/res/android”

签名的配置修改 ant.properties 这个文件,key.store 这个变量就是签名文件的路径,不过签名文件是需要提前生成好的,这个常打包的同学应该并不陌生,这里就不展开了。

要强调的一点是 .properties 文件写中文的话是会出错的,这个文件的编码是 ascii 所以如果传入的路径或文件名有包含非 ascii 字符的话,是需要转换成 \u4e00 这种形式的编码的。

libgit2使用教程(八)git remote

从这篇开始进入到有网络的 libgit2 世界,如何添加远端服务器,如何 fetch ,pull ,push 你的版本库等等,将会陆续往下写。这一篇主要介绍如何实现:
git remote
git remote add <…>
git remote delete <…>

首先我们必须要掌握如何知道我们的 remote 操作是否成功。使用文本编辑器打开一个 clone 下来的仓库的 config 文件[.git/config],可以看到这样一个条目:

[remote “origin”]
        url = git@github.com:XiaochenFTX/libgit2_samples.git
        fetch = +refs/heads/*:refs/remotes/origin/*

origin 是 remote 的名字,url 是它的远端地址。那么 fetch 后边那一串是什么呢?它表示的是在 fetch 操作的时候,数据从哪来,到哪里去。以冒号分开两个 refspec 前边表示从哪来[远端的refspec],后边表示到哪去[本地的refspec]。
关于 refspec 有一篇挺不错的文章,直接看下面这个链接:
http://docs.pythontab.com/github/gitbook/Git-Internals/The-Refspec.html
官方原文:
https://git-scm.com/book/en/v2/Git-Internals-The-Refspec

当然,在本地直接初始化的仓库是没有一个条目的。所以我们需要自己去添加一个:

这个代码我们执行之后再去看 config 文件,remote 这条就出现了。我现在用的这个版本貌似有个 bug,如果是第一次调用 git_remote_create 会出现两个 [remote “origin2″] 一个放 url 一个放 fetch ,但是如果删除之后,再调用 create 的话 url 和 fetch 就都在第一个 [remote “origin”] 下面了。
这个接口会填写默认的 fetch ,如果需要自己设置,使用下面的代码:

 

通过 git_remote_list 接口来列出所有的 remote :

 

删除一个 remote :

 

接下来简单介绍一下如何使用 ssh 方式的 url 。用 git_remote_connect 来检验是否可以成功连接。
我们知道使用 ssh 方式连接,必须要有一个 ssh key 用于身份验证。这个 key 怎么传给接口呢?找了半天最后发现,这个奇葩的接口是通过回调的方式传个证书给回调参数。

这个回调是这样的:

通过 git_cred_ssh_key_new 来生成证书。如果是使用 github 这种在服务器填写公钥的方式,那么我们就将本地对应的私钥传入就可以了。
这里还有个 bug 要注意,如果不是使用这个接口正确的生成证书,可能会出现 connect 操作卡死的情况。
另外,如果使用 ssh 格式的 url [git@github.com:XiaochenFTX/libgit2_samples.git] 在 connect 的时候报了这个错:
Unsupported URL protocol
那么很有可能是没有找到 libssh2 这个库,请先确保这个库已经正常安装了。

示例代码:sample8

libgit2 版本升级到 0.24.1后发现 git_remote_connect 这个参数多了一个,已经在代码中做了更新。如果是用的老版本库的用户看到了这篇教程,请参考对应版本的函数声明。

libgit2使用教程(七)git merge

这篇的主要内容是合并操作,是 git 中非常重要的操作。合并就有可能出现冲突,所以同时会简单介绍一下解决冲突。
合并操作主要针对的对象实际上并不是分支,只不过 commit 组成了一条分支,所以可以用分支来作为 commit 的索引。我们首先需要找到需要合并过来的那个 commit (或者多个 commit)。示例只简单的将一个分支最新的 commit 合并到 HEAD ,更多拓展的操作您可以自己发挥。
首先获得分支最新的 commit :

DWIM 的意思貌似是:Do What I Mean

然后调接口实现 merge :

上面这几行代码就实现了 merge 操作,然而,不要以为就这样就结束了,调用完 merge 接口才是刚刚开始。这个时候合并操作会被更新到 index,但是并不会写到 tree 里 ,所以后边的 add 和 commit 操作还是需要写的。到目前为止这个操作可能叫 try merge 更合理一些。
需要补充一点,如果 merge 没有成功,有可能存在有没处理完的合并,因为有可能在之前的合并操并没有走完全部流程就被强制终止了。这个时候 git_merge 是会报错的,所以如果报错了需要做一下判断,是继续上次未完成的合并,或是做一下清理:

这个状态清理,是合并操作流程完成后一定要调用的,否则磁盘上的标志文件会一直存在,会影响下一次合并操作。

git_merge 操作完成后,就要检查一下是否有冲突:

需要通过 index 来检查。
如果是有冲突的情况,可以通过 conflict_iterator 对冲突进行遍历:

解决冲突有几种方式:
1. 直接 checkout –ours(theirs) 快速选择一个版本作为最终合并后的版本:

也可以选择对指定文件 checkout ,只要设置一下 options 的参数:

2. 手动改写工作目录下的文件,通过取出的 git_index_entry 的 oid 可以获得对应版本文件的 blob ,这样可以把对应文件的内容取出。根据需要,将工作目录下的文件改写,这个就完全看自己的业务逻辑需要了,所以就不提供参考代码了。
之后要将这个解决完的冲突 remove 掉:

 

现在解决完冲突的文件状态已经在硬盘上体现出来了,但是实际上并没有完全更新到 index ,接下来就依照文件更新后 add 和 commit 的操作:更新 index→生成 tree → commit 来完成整个合并操作。
需要注意的一点是,因为是两个 commit 合并成新的 commit 所以这个新的 commit 的 parent 应该有两个:

这些细节在之前写过了,所以就不细说了。最后将合并状态清理一下就可以结束了:

补充说明
虽然 git_merge 传入了 annotated_commit 的数组,但是从实现代码上来看,合并操作只取了数组的第0个元素,所以实际上并不能实现同时将好多个 commit 合并的 HEAD。

libgit2使用教程(六)分支操作

这篇一锅粥把主要的分支操作全都铺上来。
这里有一个基础概念需要复习一下,分支本质上上指的就是 reference ,在 .git/refs 中可以找到它们的信息。分支分为本地和远端,这里我们先不考虑远端分支,在之后介绍完联网操作之后再说。

所有内容:
git branch 列出所有分支
git branch <branch name> 新建分支
git branch -d <branch name> 删除分支
git branch -m <branch name> 分支改名
git checkout <branch name> 切换分支

1. git branch
主要使用 git_branch_iterator 对分支进行遍历。代码很简单,应该很好理解,就直接贴出来了:

可以看一下 git_branch_t ,它实际上就是一个超简单的枚举类型,标示出本地和远端分支:

git_branch_is_head,就是判断这个分支是否是当前工作分支。也就是判断这个 reference 是不是 head 。

2. git branch <branch name>
流程就是:找到源分支 → 取到最后一次 commit → 基于这个 commit 创建新的分支
那么如果这个分支并没有 commit 呢?那么这肯定是一个一次都没提交过的仓库,这种情况可以用命令行做一个测试:

实际上 git 并不支持这种行为。
所以新建分支的代码就是这样:

3. git branch -d <branch name>
很简单,先找到目标分支,然后调接口删之

删除分支要保证被删分支不是当前 head 分支,否则会报错

4. git branch -m <branch name>
跟删除分支差不多,先根据分支名找到要改名字的分支,然后调一个接口做改名操作

5. git checkout <branch name>
就是把当前工作分支(head)切换成指定的分支

git_repository_set_head 这个接口需要的名字不是简单的分支名,而是一个 reference 的全名(refs/heads/new),所以需要使用 git_reference_name 来取一下。

代码示例 sample6

libcurl 使用 http2

首先 http2 一定要是加密的,必须要 https 才行,所以第一步要先支持 https 。
如果是一个权威机构认证的证书,那么直接使用正常的请求方式也 OK 的,底层已经处理好了自动可以验证到证书的有效性。然而,如果是我们自己签名的证书底层没有办法自动验证证书,就需要我们自己来解决了。
如果服务器使用的是自签名,libcurl 会报证书无效的错误:
SSL certificate problem: Invalid certificate chain

有两个思路,一是干脆直接不做验证:

当然,对于强迫症人群来说,我们怕 DNS 被劫持,所以要自己验证一下证书。当然其实也很简单:

只要证书传对就可以了。如果使用 ip 访问,然而证书中指定了主机名,那么会报错。
WARNING: using IP address, SNI is being disabled by the OS.
这种情况如果一定要使用这个证书并且使用不同域名访问的话,可以把域名验证关掉。

 

https 可以成功了之后,再看如何发出一个 http2 的消息。
起始只要库的版本没问题并且服务器端支持 http2,那么默认的 https 就被底层默认处理为 http2 消息了。当然,显示的设定一下 http 版本算是一种礼貌吧,而且即使设成2.0如果不被支持的话也会默认转为1.1的。

如果需要使用 http2 的多路传输 pipelining 只需要做一个简单的设置:

关于更多链接复用和 pipelining 的细节,我还没深入了解,之后有空再研究。

关于环境的搭建:
安装支持 http2 的 apache

mac 系统安装支持 http2 的 curl:
使用 brew 安装一个全新的 curl

强行将默认的 curl 连接为这个最新版本

然后重启终端就可以生效了。
可以试一试

参考资料
如果不出意外,那么在返回数据中会有 HTTP/2.0 200 那么就说明环境没问题了。

我们在使用 libcurl 库的时候还要注意一下,一定要连接到新编译出的这个版本,如果是系统默认版本是不支持 http2 的。
我用的方法就是在 cmake 脚本简单指定了一下连接路径:

毕竟这不是重点,实现功能最重要。

新的功能已经更新到 github 可以直接 点击前往

libgit2使用教程(五)git diff

首先介绍一些重要的术语:
diff —— 两个仓库快照的差异列表,以 git_diff 对象来管理。
delta —— 文件的新版本和旧版本组成一个一对儿,如果没有旧版本,说明文件是新加入的;如果没有新版本,说明这个文件在这个版本被删除了。diff 就是一列 delta。
binary —— 非文本文件的差异。二进制差异并没有细节的每行的内容等详细数据,主要是给出了文件路径。
hunk —— 是一个 delta 中连续有改动的行以及相关上下文组成的块。会告诉我们这块从哪行到哪行,以及其中有哪些改动。
line —— 是在一个 hunk 中的一个行的信息,包含其在新版本和旧版本的行号,变化信息以及新行的具体内容等。

生成一个 diff 的代码实际上很简单,重点在于如何从生成的 diff 中取出自己想要的数据。

这是生成一个 index 相对于 workdir 的 diff ,相当于命令行:git diff
其他的就不具体贴细节代码了,就做一些简单的介绍,从函数名就可以看出具体行为了,对于 index 、HEAD 、tree 等这些名词的概念不太了解的话,可以转到我的另一篇文章先熟悉一下。《libgit2使用教程(特别篇)几个基本概念说明
实现 git diff --cached :
相当于 HEAD 相对于 index 的差异。可以取出 HEAD 的 tree 然后使用 git_diff_tree_to_index 来比对。
实现 git diff HEAD :
相当于 HEAD 相对于 workdir 的差异。可以取出 HEAD 的 tree 然后使用 git_diff_tree_to_workdir_with_index 来比对。
以及 commit 之间的差异,可以使用 git_diff_tree_to_tree 函数。
这些细节在官方的 101-samples 的 diff 部分有很多介绍,虽然代码给的并不够细,自己稍微鼓捣鼓捣也够用了。

我们有了一个 diff 之后,要做的就是从 diff 中获取数据了。就是通过 git_diff 指针取出 delta 、binary 、 hunk 、line 等信息。libgit2 只提供了一个 foreach 函数,这些数据通过传入的回调函数拿到。

这里边的回调并不一定都要传入,可以根据自己的需求选择传哪个,其他的可以为空指针。下面简单介绍一下回调回来的参数:
git_diff_delta :
主要带着新旧版本的文件信息(文件路径);其他主要参数:
status 这个文件的状态,新增还是修改还是删除什么的。
flags 主要标记是否是二进制。
nfiles 这个差异有几个文件,比如如果是新增或删除,没有 old 或 new 就是1,如果是修改 old 和 new 都有就是2。

git_diff_binary:
binary 就比较单纯了,并没有太多信息,主要就是新版本和旧版本的文件信息。二进制的比对细节应该确实也没啥必要。

git_diff_hunk:
hunk 就是 git diff 命令的输出数据的那个样子,

一个文件差异可能会有多个 hunk ,具体参数:
old_start 这个块在旧版本中的起始行
old_lines 旧版本中这个块的行数
new_start 这个块在新版本中的起始行
new_lines 新版本中这个块的行数
header_len header 的字节数
header 块描述文本,就是上边[ @@ -633,7 +633,9 @@ class WP { ]这行。
这个描述大概就是这样的结构:
@@ [1][2],[3] [4][5],[6] @@ [7]
[1] 据我观察只有加号和减号(+/-),我猜加号后边就是新版本,减号后边就是旧版本,两个版本之间以空格隔开。
[2] 这个块起始的行号。
[3] 这个块旧版本的行数
[4] 同[1]
[5] 同[2]
[6] 这个块新版本的行数
[7] 这个块所在的类的类名

git_diff_line:
origin 表示这个行的状态
old_lineno 旧的行的行号,如果是-1表示这个行的增加的
new_lineno 新的行的行号,如果是-1表示这个行是删掉的
num_lines 当前行的行数
content_len 当前行的字节数
content_offset 这个行在文件中的偏移量,只有在变化的行才有数据
content 这个行的内容
会把 hunk 的行数逐行都走一遍。

一个 diff 包含所有有差异文件,一个文件对应一个 delta,如果是二进制文件,就对应一个 binary ;如果是文本文件,一个 delta 可能对应一个或多个 hunk ,一个 hunk 可能对应一个或多个 line 。

最后,介绍一些稍微高级一些的设定。

将 untracked 的文件加入比对就不细说了,是针对 workdir 的,因为在新加入文件的情况下都是 untracked 的。

下边的两个回调是在比对的过程中触发的,因为整个比对的过程会遍历所有的文件,所以遍历的过程中没走过一个文件就会触发一次 progress_cb ;如果有一个文件新旧版本有差异,则会触发 notify_cb 。这两个回调是随着文件遍历进行的,所以这两个函数的一个参数 diff 明明为 so far ,指的就是当前为止的情况。如果只是想知道有哪些文件有差异而不需要具体的细节,那么就可以不使用 foreach 去遍历,使用 notify_cb 就够了。

如果一个文件和另一个文件完全一样,或者文件改了名,普通的 diff 是不会在 status 标出 GIT_DELTA_RENAMED 或 GIT_DELTA_COPIED 这些状态的。如果想要可以识别这些情况,就需要使用上边的代码和配置,通过 git_diff_find_similar 函数使这个配置生效。这个函数只要在 foreach 之前调用就可以了。

代码示例 sample5