libgit2使用教程(十三)git rebase

rebase 就是将制定目标分支,或者制定 commit 所在的一条路径,直接插到当前分支或目标分支。(好像有点乱,具体的东西自己去查吧)

简单点说,这不是合并。

当然,虽然不是 merge 但是也会有冲突的可能,所以中途有解决冲突的需要,所以 rebase 操作是分阶段进行的,因此逻辑会复杂一些。

首先尝试打开现有未完成的 rebase

如果存在未完成的 rebase,可以选择继续将其完成,或者把它终止掉

在 git_rebase_open 返回 -3 (也就是 GIT_ENOTFOUND ) 表示当前仓库并没有其他未完成的 rebase 可以放心大胆的从头开始搞。

接下来创建一个 rebase

init 的第三个参数是需要被操作的分支,传空表示当前分支;第四个参数和第五个参数这俩二选一,前者表示以一个 commit 为节点把到这个节点为止的一条链合并到目标分支;后者是直接选一个分支的最新一个 commit ,将这个分支的整条链合并到目标分支。

当然,这个时候工作区不会有任何变化。到 .git 文件夹里面会看到多了一个叫做 rebase-merge 的文件夹。如果这个时候程序被终止,这个文件夹会保留,在下一次启动的时候,就可以通过 git_rebase_open 打开这个 rebase 。

接下来就是实际执行 rebase 这个操作

这里可能存在遍历,但是为什么会有多个 operation 我也还没搞太明白,不过为了避免出事,还是循环调用保险一些:

接下来,不要忘记查看是否有冲突,需要将冲突解决才可以做后边的 commit 的操作:

在解决完冲突,并且 add 之后,就可以 commit 了。这里并不需要使用 commit 的 create 接口,rebase 部分提供了 rebase 专用的 commit 接口。

最后,做一下收尾 finish 掉这个 rebase 操作

示例代码:sample13

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

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

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

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 有很多要详细解释的东西,就放到下回了,我得花点时间读注释去了……