libgit2使用教程(七)git merge

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

const git_annotated_commit *their_head[10];
git_reference *branch = nullptr;

git_reference_dwim(&branch, rep, "new");
git_annotated_commit_from_ref((git_annotated_commit **) &their_head[0], rep, branch);

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

然后调接口实现 merge :

git_merge_options merge_opt = GIT_MERGE_OPTIONS_INIT;
git_checkout_options checkout_opt = GIT_CHECKOUT_OPTIONS_INIT;
git_merge(rep, their_head, 1, &merge_opt, &checkout_opt);

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

git_repository_state_cleanup(rep);

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

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

git_index *index = nullptr;
git_repository_index(&index, rep);
if (git_index_has_conflicts(index)) { /* reslove code ... */ }

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

git_index_conflict_iterator *conflict_iterator = nullptr;
const git_index_entry *ancestor_out = nullptr;
const git_index_entry *our_out = nullptr;
const git_index_entry *their_out = nullptr;

git_index_conflict_iterator_new(&conflict_iterator, index);

while (git_index_conflict_next(&ancestor_out, &our_out, &their_out, conflict_iterator) != GIT_ITEROVER)
{
    if (ancestor_out) std::cout << "ancestor: " << ancestor_out->path << std::endl;
    if (our_out) std::cout << "our: " << our_out->path << std::endl;
    if (their_out) std::cout << "their: " << their_out->path << std::endl;
    /* your code ... */
}

git_index_conflict_iterator_free(conflict_iterator);

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

git_checkout_options opt = GIT_CHECKOUT_OPTIONS_INIT;
opt.checkout_strategy |= GIT_CHECKOUT_USE_THEIRS;
git_checkout_index(rep, index, &opt);

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

const char *p = "file";
opt.paths.strings = (char **) &p;
opt.paths.count = 1;

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

git_index_conflict_remove(index, "file");

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

git_oid new_tree_id;
git_tree *new_tree = nullptr;
git_signature *me = nullptr;
git_reference *head = nullptr;
git_commit *parent_our = nullptr;
git_commit *parent_their = nullptr;
git_oid commit_id;

git_index_update_all(index, nullptr, nullptr, nullptr);
git_index_write(index);
git_index_write_tree(&new_tree_id, index);
git_tree_lookup(&new_tree, rep, &new_tree_id);

git_signature_now(&me, "XiaochenFTX", "xiaochenftx@gmail.com");

git_repository_head(&head, rep);
git_commit_lookup(&parent_our, rep, git_reference_target(head));
git_commit_lookup(&parent_their, rep, git_reference_target(branch));

git_commit_create_v(&commit_id, rep, "HEAD", me, me, "UTF-8",
    "merge commit", new_tree, 2, parent_our, parent_their);

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

git_repository_state_cleanup(rep);

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

if ((error =
    merge_annotated_commits(&index, &base, repo, our_head, (git_annotated_commit *) their_heads[0], 0, merge_opts)) < 0
     || (error = git_merge__check_result(repo, index)) < 0
     || (error = git_merge__append_conflicts_to_merge_msg(repo, index)) < 0)
    goto done;

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_iterator *branch_iterator = nullptr;
git_reference *tmp_branch = nullptr;
git_branch_t branch_type;
// list branch
git_branch_iterator_new(&branch_iterator, rep, GIT_BRANCH_LOCAL);
while (GIT_ITEROVER != git_branch_next(&tmp_branch, &branch_type, branch_iterator))
{
    const char *branch_name;
    git_branch_name(&branch_name, tmp_branch);
    bool is_head = git_branch_is_head(tmp_branch);
    if (is_head)
    {
        std::cout << "*";
    }
    std::cout << "name: " << branch_name << std::endl;
}

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

/** Basic type of any Git branch. */
typedef enum {
    GIT_BRANCH_LOCAL = 1,
    GIT_BRANCH_REMOTE = 2,
    GIT_BRANCH_ALL = GIT_BRANCH_LOCAL | GIT_BRANCH_REMOTE,
} git_branch_t;

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

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

$ git branch dev 
fatal: Not a valid object name: 'master'.

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

git_reference *head = nullptr;
git_reference *new_branch = nullptr;
const git_oid *commit_id = nullptr;
git_commit *last_commit = nullptr;

git_repository_head(&head, rep);
commit_id = git_reference_target(head);
git_commit_lookup(&last_commit, rep, commit_id);
git_branch_create(&new_branch, rep, "new"/* branch name */, last_commit, 0);

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

git_reference *lookup_branch = nullptr;
git_branch_lookup(&lookup_branch, rep, "new"/* branch name */, GIT_BRANCH_LOCAL);
if (lookup_branch != nullptr && !git_branch_is_head(lookup_branch))
{
    git_branch_delete(lookup_branch);
}

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

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

git_reference *lookup_branch = nullptr;
git_reference *renamed_branch = nullptr;

git_branch_lookup(&lookup_branch, rep, "new"/* old name */, GIT_BRANCH_LOCAL);
git_branch_move(&renamed_branch, lookup_branch, "new3"/* new name */, 0);

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

git_reference *lookup_branch = nullptr;
git_branch_lookup(&lookup_branch, rep, "new"/* branch name */, GIT_BRANCH_LOCAL);
git_repository_set_head(rep, git_reference_name(lookup_branch));

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

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

curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);

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

curl_easy_setopt(curl, CURLOPT_CAINFO, "<your certificate path>");

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

curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);

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

curl_easy_setopt(curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2);

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

curl_multi_setopt(curlm, CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX);
// 尽可能多路复用 
curl_easy_setopt(curl, CURLOPT_PIPEWAIT, 1L);

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

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

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

brew install curl --with-nghttp2

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

brew link curl —force

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

curl -v --http2 https://10.10.10.109/test_http2.php

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

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

link_directories(/usr/local/Cellar/curl/7.48.0/lib)

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

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

libgit2使用教程(五)git diff

首先介绍一些重要的术语:
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

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

WordPress 版本: 4.4.1 中文版

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

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

$req_uri = str_replace($pathinfo, '', $req_uri);

修改为:

$req_uri = str_replace($pathinfo, '', urldecode( $req_uri ));

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

调试过程:
经过艰苦卓绝的调试,各种打 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年的愚人节惊喜么?

RSS
Follow by Email
YouTube
YouTube
Pinterest
fb-share-icon
LinkedIn
Share
VK
Weibo
WeChat
WhatsApp
Reddit
FbMessenger
Copy link
URL has been copied successfully!