首先介绍一些重要的术语:
diff —— 两个仓库快照的差异列表,以 git_diff 对象来管理。
delta —— 文件的新版本和旧版本组成一个一对儿,如果没有旧版本,说明文件是新加入的;如果没有新版本,说明这个文件在这个版本被删除了。diff 就是一列 delta。
binary —— 非文本文件的差异。二进制差异并没有细节的每行的内容等详细数据,主要是给出了文件路径。
hunk —— 是一个 delta 中连续有改动的行以及相关上下文组成的块。会告诉我们这块从哪行到哪行,以及其中有哪些改动。
line —— 是在一个 hunk 中的一个行的信息,包含其在新版本和旧版本的行号,变化信息以及新行的具体内容等。
生成一个 diff 的代码实际上很简单,重点在于如何从生成的 diff 中取出自己想要的数据。
git_diff *diff_index_to_workdir = nullptr;
git_diff_index_to_workdir(&diff_index_to_workdir, rep, nullptr, nullptr);
这是生成一个 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 函数,这些数据通过传入的回调函数拿到。
int file_cb(const git_diff_delta *delta, float progress, void *payload)
{
std::cout << "old file path: " << delta->old_file.path << "\n";
std::cout << "new file path: " << delta->new_file.path << "\n";
std::cout << "file number: " << delta->nfiles << "\n";
std::cout << "status: " << delta->status << "\n";
std::cout << "flags: " << delta->flags << "\n";
std::cout << "progress: " << progress << "\n";
std::cout << "==================" << std::endl;
return 0;
}
int binary_cb(const git_diff_delta *delta, const git_diff_binary *binary, void *payload)
{
std::cout << "old file path: " << delta->old_file.path << "\n";
std::cout << "new file path: " << delta->old_file.path << "\n";
std::cout << "=================" << std::endl;
return 0;
}
int hunk_cb(const git_diff_delta *delta, const git_diff_hunk *hunk, void *payload)
{
std::cout << "old start: " << hunk->old_start << "\n";
std::cout << "old lines: " << hunk->old_lines << "\n";
std::cout << "new start: " << hunk->new_start << "\n";
std::cout << "new lines: " << hunk->new_lines << "\n";
std::cout << "header: " << hunk->header << "\n";
std::cout << "=================" << std::endl;
return 0;
}
int line_cb(const git_diff_delta *delta, const git_diff_hunk *hunk, const git_diff_line *line, void *payload)
{
std::cout << "origin: " << line->origin << "\n";
std::cout << "old line number: " << line->old_lineno << "\n";
std::cout << "new line number: " << line->new_lineno << "\n";
std::cout << "num lines: " << line->num_lines << "\n";
std::cout << "content offset: " << line->content_offset << "\n";
std::cout << "=================" << std::endl;
return 0;
}
git_diff_foreach(diff_index_to_workdir, file_cb, binary_cb, hunk_cb, line_cb, nullptr);
这里边的回调并不一定都要传入,可以根据自己的需求选择传哪个,其他的可以为空指针。下面简单介绍一下回调回来的参数:
git_diff_delta :
主要带着新旧版本的文件信息(文件路径);其他主要参数:
status 这个文件的状态,新增还是修改还是删除什么的。
flags 主要标记是否是二进制。
nfiles 这个差异有几个文件,比如如果是新增或删除,没有 old 或 new 就是1,如果是修改 old 和 new 都有就是2。
git_diff_binary:
binary 就比较单纯了,并没有太多信息,主要就是新版本和旧版本的文件信息。二进制的比对细节应该确实也没啥必要。
git_diff_hunk:
hunk 就是 git diff 命令的输出数据的那个样子,
@@ -633,7 +633,9 @@ class WP { return; } } - +//a +//b +//c // We will 404 for paged queries, as no posts were found. if ( ! is_paged() ) {
一个文件差异可能会有多个 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 。
最后,介绍一些稍微高级一些的设定。
int notify_cb(const git_diff *diff_so_far, const git_diff_delta *delta_to_add, const char *matched_pathspec,
void *payload)
{
std::cout << "***notify cb***" << "\n";
std::cout << "old file: " << delta_to_add->old_file.path << "\n";
std::cout << "new file: " << delta_to_add->new_file.path << "\n";
std::cout << "matched path spec: " << (matched_pathspec != nullptr ? matched_pathspec : "null") << "\n";
std::cout << "*****************" << std::endl;
return 0;
}
int progress_cb(const git_diff *diff_so_far, const char *old_path, const char *new_path, void *payload)
{
std::cout << "****pro cb****" << "\n";
std::cout << "old path: " << old_path << "\n";
std::cout << "new path: " << new_path << "\n";
std::cout << "*****************" << std::endl;
return 0;
}
git_diff_options diff_opt = GIT_DIFF_OPTIONS_INIT;
diff_opt.flags |= GIT_DIFF_INCLUDE_UNTRACKED;
diff_opt.notify_cb = notify_cb;
diff_opt.progress_cb = progress_cb;
git_diff_index_to_workdir(&diff_index_to_workdir, rep, nullptr, &diff_opt);
将 untracked 的文件加入比对就不细说了,是针对 workdir 的,因为在新加入文件的情况下都是 untracked 的。
下边的两个回调是在比对的过程中触发的,因为整个比对的过程会遍历所有的文件,所以遍历的过程中没走过一个文件就会触发一次 progress_cb ;如果有一个文件新旧版本有差异,则会触发 notify_cb 。这两个回调是随着文件遍历进行的,所以这两个函数的一个参数 diff 明明为 so far ,指的就是当前为止的情况。如果只是想知道有哪些文件有差异而不需要具体的细节,那么就可以不使用 foreach 去遍历,使用 notify_cb 就够了。
git_diff_find_options diff_find_opt = GIT_DIFF_FIND_OPTIONS_INIT;
diff_find_opt.flags |= GIT_DIFF_FIND_RENAMES;
diff_find_opt.flags |= GIT_DIFF_FIND_COPIES;
diff_find_opt.flags |= GIT_DIFF_FIND_FOR_UNTRACKED;
git_diff_find_similar(diff_index_to_workdir, &diff_find_opt);
如果一个文件和另一个文件完全一样,或者文件改了名,普通的 diff 是不会在 status 标出 GIT_DELTA_RENAMED 或 GIT_DELTA_COPIED 这些状态的。如果想要可以识别这些情况,就需要使用上边的代码和配置,通过 git_diff_find_similar 函数使这个配置生效。这个函数只要在 foreach 之前调用就可以了。
代码示例 sample5
不错,不错,看看了!
博客多久更新一次?
不定期,有时间我就会尽量写
我又来了,您高兴吗?!