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

解决了 WordPress 中文标签或分类目录返回404的问题

WordPress 版本: 4.4.1 中文版

问题描述:
在访问中文的标签[tag] 或分类目录[category] 的时候,系统返回404。
举个例子,可以试一下访问 http://ftxtool.org/index.php/tag/测试/ ,显示的页面是“未找到” ,然而是一个正常返回的页面,并不是404;而如果访问一个根本没有的标签或分来目录,比如 http://ftxtool.org/index.php/tag/根本没有/ ,显示的页面是 “有点尴尬诶!该页无法显示。”,而且 http 响应不是200而是404。
所以正确的姿势应该是,访问一个存在的标签,就应该返回200而不是404。

解决方法:
代码文件:wp-includes/class-wp.php
代码:

修改为:

吐槽:
我不知道网上分享解决方案的那些人是在什么样的环境下解决了问题的,我很愿意相信他们是真的亲自操作遇到问题并解决了。然而我的环境下的这个问题,用那些搜出来的长得都差不多的信息对于从根本上解决问题并没有什么帮助。

调试过程:
经过艰苦卓绝的调试,各种打 log ,最后发现 $_SERVER[‘PATH_INFO’] 和 $_SERVER[‘REQUEST_URI’] 这两个变量的值在编码上是有区别的。PATH_INFO 取出的值是其中的中文是经过 urldecode 之后的,就是正常的中文;而 REQUEST_URI 的中文并没有经过 urldecode ,是那种百分号和16进制数的那种形式。因为这样的区别,所以导致 str_replace 并没有达到其所期望的效果。
因为 replace 后的字符串是错的,导致后边一系列错误导致没有正确匹配到数据库中的数据。

最后吐槽一下 WordPress 官方。在解决完这个问题后,我想把修改的代码提交给官方,然后发现他们的 github 是不接受 pull request 的,只能去官方的网站提交 trac [https://core.trac.wordpress.org/] 。写好之后他们的回复到是挺快的,大叔告诉我这个问题已经有人提过 issus 了,在 #10249#17450,有兴趣的同学可以点进去看。尼玛都有5年的历史了,有木有……新的代码里还是没有加进来,有木有……这是准备了5年的愚人节惊喜么?

libgit2使用教程(四) git status

status 这个操作是干啥的就不细解释了,这个不知道的话是不太适合看这套教程的,先去补补 git 相关的知识。这集的代码写起来还比较简单,在开动之前先复习一下基础概念。
HEAD、index、workdir:
HEAD 是当前所在的分支的最后一次 commit ,重点是已经 commit 了的状态。
index 是 git_index_add 之后的状态。但是要注意一下,这指的并不一定是使用命令行的 git add 命令完成。仅仅是加到 index 中就算了,并不需要完成 git_index_write 把内存中的状态写入磁盘。
workdir 还没加到 index 之前的都算在 workdir 的状态。

libgit 实现 status 大体上有两个方案,一个是简单一些,可以得到的信息也相对比较少的;另一个可以获得更详细的信息。我们从简单的开始

1. 简单方案

通过 callback 获取状态信息,
path 文件的相对路径
status_flags 是状态的枚举值
payload 是一个透传参数,从 git_status_foreach 第三个参数原样传入

关于状态的枚举值,这个枚举的声明是这样的:

IGNORED 是被忽略的文件,表示这个文件满足 .gitignore 中的规则。
CONFLICTED 是合并之后有冲突的文件。
GIT_STATUS_INDEX 开头的是 index 相对于 HEAD 的状态。
GIT_STATUS_WT 开头的是 workdir 相对于 index 的状态。

2. 详细方案

1) 初始化选项
2) 获取 git_status_list
3) 遍历 list 取出 git_status_entry

git_status_entry 除了有文件状态的标示,还带有 diff 的详细信息,而且严格区分了 HEAD 相对 index ,和 index 相对 workdir。
关于 diff 有很多要详细解释的东西,就放到下回了,我得花点时间读注释去了……

libcurl 撸记

废话不讲,直奔主题

1. 基本流程
首先在使用这个库的任何操作前,最先进行的一个操作就是全局初始化。当然,相对的,在结束后也需要全局的销毁。这两个函数在开始的时候最好就直接先拍上,肯定不会浪费的。

然而,这似乎并不是必须的,从 libcurl 提供的 examples 来看,并不是所有情况都有用这两个函数。具体情况就不太深究了,写上也没什么损失。根据官方资料,每个程序(application)都要执行一次。

2. 同步请求
同步请求就是使用 easy 系列函数,基本的操作流程就是:
初始化 → 设置属性 → 执行 → 获得返回数据 → 销毁
这里每一步都是阻塞式的,perform 这步实际发出 http 请求,收到回复前都会阻塞住。

3. 异步请求

异步请求就要复杂了,需要同时使用 easy 系列函数和 multi 系列函数。
multi 负责管理所有的添加进来的 easy 指针,然而它主要做的工作是异步的把所有加进来的 easy 都 perform 掉。最终取数据的时候,它会返回 easy 的指针,所以取数据还是要使用同步方式的操作。并且,需要自己手动调用 select 来执行异步操作的。
最终可以实现所有操作不会把线程阻塞住。
因为异步,所以流程并不固定,大概可以这样描述:
固定流程:初始化 → 循环[ 开始请求 → 取 fd_set → 执行 select → 取消息 ] → 销毁
添加请求:生成一个 easy 请求句柄 → 将指针添加给 multi
获取返回:通过 multi 取出的消息拿到 easy 指针 → 通过这个 easy 指针读取返回数据
这样生成请求和读取返回信息可以是完全异步完成的。

4. 返回数据接收
通过 curl_easy_getinfo 只能获取到返回的 response code 不能获得返回的数据。如果需要通过 http 请求获得数据返回或者下载文件的话,需要在 CURL 指针的设置中,挂一个接收数据的函数。

这个函数的声明是这样的:

buffer 数据指针
size 指针单个偏移的大小
nitems 指针能偏移多少个
outstream 由用户传入的指针 CURLOPT_WRITEDATA
返回值:本次接收的数据大小

然后我们在这个函数中来处理返回来的数据。
这里需要注意的是这个函数不一定是一次性返回全部的数据,所以要妥善处理才行,直到 curl_easy_perform 返回或者 curl_multi_info_read 拿到这个句柄才能认为返回数据已经写入完毕,否则接收到的数据都可能是不完整的。
下面是我下载部分的处理:

不过如果不需要特殊处理,可以不需要 CURLOPT_WRITEFUNCTION 传入一个函数,只需要 CURLOPT_WRITEDATA 传入一个 FILE* ,下载的数据会自动写入文件。(至少我使用的版本是这样的)

5. POST 和 GET
libcurl 默认的模式是 GET ,传参数只需要写在 url 后边就可以了,只要不做特殊的设置,就是以 GET 形式向服务器发请求。
那么如果我们要发 POST 呢?
首先,我们要显示的设置我们这个请求是一个 POST 请求:

POST 的参数不是接在 url 后边的,而是要单独传递:

这样设置好,perform 后就向服务器发送 POST 请求了。

6. 分块下载
使用 http 协议下载文件,数据是绝对顺序传输的,如果客户端或者服务器端在传输过程中发生了一些问题,就会导致 TCP 层重试,导致下载效率降低。所以如果只是直接单纯的顺序传输下载,效果是比较差的。因此同时开多个连接,同时下载文件的不同部分,会对网络的利用率更高一些。
下载文件的一个块,在 http1.1 中引入了 Range 参数。这个参数就指定了这个连接负责下载文件的哪个部分。

写文件的部分只需要每个连接各自拿一个文件句柄 seek 到起始位置就可以了。

然而如何让所有连接同时工作起来呢?
有两个选择:1) 每个连接各自使用 curl_easy_perform 发起请求;2) 加入到 multi 中异步请求。
1) 因为是会阻塞,所以单线程是无法完成一起工作的,所以如果选择这个种简单的方式,就需要手动开多个线程来完成。这里需要注意的是每个线程使用自己的 CURL* 基本上也不会出什么乱子。
2) 这种方式会比较省心一些,不用处理复杂的线程同步问题。只是 multi 异步架构起来会稍微复杂。
我个人倾向于选择方案2,因为没有太大必要折腾多线程来处理下载的问题。线程开的再多也并不会使网线带宽变大,所以也没必要花心思处理多线程数据同步问题。
方案2,只要处理好最大连接数,以及下载结束后的收尾,在性能和稳定性方面还是更靠谱一些的。

7. 断点续传
要实现断点续传首先我们应该确定断点,必须知道下载的文件断在了什么地方才能知道从哪里开始继续下载。
这里可以分两种情况来考虑
1) 不分块顺序下载,那么文件已经下载好的部分就是断点。那么下一次只需要取出已下载文件的大小,从这里开始继续下就可以了。这可以不必使用 Range 参数,可以使用 RESUME_FROM 设置起始点。不过对于大文件,不适用分块的话效率会比较低。
2) 分块下载,对于分块下载到同一个文件的情况,读取文件已完成部分大小的方式就显然不可行了。这里就需要借助另外一个文件来记录文件每块的下载进度了。在写被下载文件的同时再写一个记录进度的 log 文件,如果正常完成了下载,就可以将 log 文件删除了,如果没有完成,下一次可以先读取 log 文件,通过 log 文件中的记录继续剩余分块的下载。
那么再写一个 log 文件是不是会有性能问题呢?答案是肯定会多消耗一些磁盘的写带宽,但是对于下载这件事的影响有多大呢?理论上不会有什么影响,毕竟磁盘的写入带宽要远远大于网络传输带宽,所以对实际效果的影响应该是不大的。
据我观察迅雷等下载软件也是会给每个下载的文件多写一个 log 文件的。当然我并不确定它是不是用这个 log 文件作为断点记录。

写在最后
这篇贴出来的代码都是小片段,因为自己撸出来的代码确实挺丑,就不拿出来丢人了。贴出来除了多占篇幅,也起不到太大作用。
我封装了一个 http client 放到了 github 有需要的同学自取。实现了 http post 、http get 以及分块下载和断点续传。
点击前往:ftxHttpClient

libgit2使用教程(三) git commit

首先去看 API 中关于 commit 的部分都有哪个函数比较像,但是意外的发现了 API 中并没有官方 101-samples 中的 git_commit_create 函数,搞得我好意外。不过据我所知,实现 commit 操作肯定是要使用这个 git_commit_create 函数了。那咱们先去头文件看看它需要什么参数:

·id 返回一个 OID 的指针,这个 OID 表示的就是新创建的这个 commit
·repo 仓库指针
·update_ref 需要提交的目标引用,使用当前分支的引用就用 “HEAD” 就可以了
·author 作者 git_signature 指针。git_signature 记录作者的名字、email、和一个时间
·committer 提交人也是 git_signature 指针,跟上边可以是同一个
·message_encoding 这次提交说明信息的字符集
·message 这次提交的完整说明
·tree 我们需要传入一个 git_tree 实例的指针,这个 tree 对象必须属于这个仓库。这个 ·tree 将是我们下边要解决的第一个问题
·parent_count 这个 commit 的父级的数量,如果是 merge 后的 commit 会有两个甚至以上,咱们暂时先不考虑分支的事情,所以只玩儿1个的
·parents 是一个 git_commit 指针的数组,表示上一级的提交,如果是第一次 commit 数组应该是空的,同时 parent_count 应该是0。如果不是,咱们就得先把爸爸找出来,这将是下边要解决的第二个问题

1. tree:
这个 tree 实际上就是把 index 的 entry 写到一个 tree 中,最终把这个 tree 提交。从 API 文档可以找到 git_index_write_tree 这个函数。它的作用就是写一个 tree,所以就可以写出下面的代码:

2. parent:
因为我们还先不考虑 merge 的情况,所以父级 commit 只有一个,这个父级实际上就是 head 引用,所以我们第一步先拿出 head。

这里我们要考虑一个情况:在一个全新的仓库的情况下,是没有之前的 commit 的,因此,取出来的 ref_head 会是空的。所以要做一个判断:

这样我们就拿到了我们需要的 tree 和 parent commit ,下面就是签名,然后提交了:

这样 commit 操作就完成了。

示例代码是 sample3
同样放在 github 的 libgit2_samples 中

libgit2使用教程(特别篇)几个基本概念说明

本来打算这篇写 commit 但是感觉涉及的一些概念不细说一下可能不太好,因此花了一下午时间整理了这些基础概念。对这些概念理解之后会比较有利于理解 git 仓库的结构,对于理解 git 系统的工作原理也会有很大的帮助。所以,这篇没有任何功能实现,纯描述概念,可能有些无聊,对于对下面这些完全没有概念同学,还得代码敲起来,自己动手探索才能理解到其中的精髓。

  • blob
  • oid
  • tree
  • commit
  • reference
  • branch
  • index

1. blob:

blob 对象直接包含 git_object ,但是 git_object 这个概念是封装起来的,我们一般情况下是接触不到的。它将直接对应存放在仓库中的数据文件,对于 blob 我们就把它直接理解成我们文件夹中的文件就可以了。它是整个 git 仓库管理的基础单位,作为实际文件的代表,可以说 git 的版本管理就是花式玩儿 blob 。

2. oid:
指的是 git_object 的 id。每个独立的 git_object 都有一个 id,id 相等则可以判定是同一个对象。
它存储的是一个 SHA-1 值,20个字节大小,每个字节存放一个16进制数。如果转成字符串,则是一个40个字符长的字符串,两个字符表示一个16进制数。
相互转换的函数:
git_oid_fromstr 把 SHA-1 转成 oid
git_oid_tostr 把 oid 转成 SHA-1

3. tree 和 tree entry:
tree 就如它字面上的意思,是一个树形数据结构,tree entry 就是这个树的节点。

可以用上边的方法简单遍历 tree 的一层,这些 entry 可能是没有子节点的“文件”,也有可能是还有子节点的“文件夹”,也就是说,tree entry 还可以作为 tree 持有自己的 tree entry。

通过 git_filemode_t 我们可以看出,entry 就可以表示一个 blob。

这里有一个要注意的地方,这个枚举的值是以8进制表示的,直接以10进制数方式打印,跟这个字面值是不一样的。

4. commit:
commit 是版本的基本单位,版本库的记录都是以 commit 为基础的,谁提交的、什么时候提交的、提交的说明信息、提交时的 tree 的状态都是由 commit 管理的。它还要知道它的父级 commit 是哪个或者哪几个,最终 commit 们会组成一个有向图。
我们要找某一个版本,就是去找这个版本的 commit ,有了这个 commit 我们就可以通过它调出那个版本当时的 tree,然后由 tree 管理的所有文件就都可以找到了。可以说 commit 是 git 版本管理的核心,其他所有的概念都是围绕着 commit 展开的。

5. reference:
这个引用怎么用语言来描述我一直很纠结。reference 引用的是一条 commit 链——更准确的说不应该是链,因为 commit 网上追溯有可能有多个父 commit 的情况。它实际上是一个倒过来的树,reference 是它的根节点,commit 的 parent 就是子节点。这是为了方便向上追溯版本。
实际上每多一个分支,就相当于多了一个 reference ,reference 就包含着这个分支的最新的一个 commit。
git 系统也预置了几个重要的 reference 名称,用于方便索引。其中最重要的就是 HEAD 了,在工程的 .git 文件夹就可以看到这个文件,打开看看,它只有一个指向一个 refs 文件夹的路径,实际上它指向的就是当前所在的分支。所以,通过 HEAD 就可以非常方便的找到当前的 reference。

6. branch:
在上边 reference 的部分我多次提到了“分支”这个词,然而了解一些 git 命令行工具的同学肯定知道,git 工具分支的命令是 branch ,而且日常沟通也都是以 branch 的概念来代表分支的。
这没有错,git 概念中的分支就是 branch ,同时 libgit2 这个库中也有 branch 这个概念,它所指的实际上就是 reference 。就是给 reference 起了个名字,这个名字是方便记忆的,而不是 reference 用的一个路径。

从这个函数声明就可以看出,通过一个分支名返回是一个 reference ,同时,在函数实现的内部,也是通过 git_reference_lookup 实现的。因此可以看出,branch 实际上就是一个有名字的 reference 。

如果此时 HEAD 就是 master 分支的话,roid 和 boid 的值是相等的。

7. index 和 index entry:
index 索引的是当前工作区中未提交的内容。完整的 commit 操作就是将 index 中的内容写到一个 tree ,然后用这个新的 tree 创建一个新的 commit ,然后更新 reference 。而在这之前的 add 操作,就是将改动更新到当前 index。
index entry 就像 tree entry 也是作为文件的代表,而不一样的地方就是,单独的文件夹不再是 tree 了。可以通过它的 mode 属性,看出每一个文件是一个 blob ,submodule 是另外一个 blob。
实际上在没有改动的情况下 index entry 和 tree entry 是相同的 blob 。只有有新的 add 才会使同一个文件对应的 blob 不同。