Unity3D 解决默认碰撞检测的缺陷 —— 实现理想的匀速直线运动

首先介绍一下默认碰撞检测存在的问题。先不提【我只想要你丫告诉我碰撞盒撞上了,却非得要我挂个钢体组件,还得接受重力,阻尼,角速度的影响。不是在逗我?】,那么我们手动勾选上钢体的 Is Kinematic 属性,自己来控制。
我们看下边的图:
collider1
当这个圆走到实线圆圈位置的时候,就会触发碰撞检测回调。当然,如果是在虚线圆圈的位置,回调是不会被触发的。那么问题来了,如果一帧的移动距离刚好是从虚线位置移动到实线位置,情况就是这样的,移动前一帧不会触发碰撞回调,而后一帧两个碰撞体已经嵌到一起了。
当然,如果只是小小的插入一下,也并没有什么问题,接下来看下面的图:
collider2
当一帧的移动距离超过碰撞目标的时候,奇迹发生了,这时碰撞检测的回调根本不会触发。也就是说在速度足够快的情况下,你的墙是挡不住你的球的。
为了实现一个在理想条件下的匀速直线运动,我就用 Unity 本身的射线检测功能来解决这个问题。
首先取出自己的碰撞盒

var selfCollider = GetComponent<Collider2D>();
Collider 中有 Raycast 和 Cast 两个方法,参数差不多,具体的区别,还得画两张图:
ray1
图一
ray2
图二
ray3
图三
图一、图二中蓝色的线就是 Raycast 的结果,是从自己的中心射到目标碰撞盒上,如果按照这个射线的距离运动,结果就是图二的样子。
图三种的蓝色线是 Cast 方法的结果,射到目标碰撞盒上的是自己碰撞盒与目标碰撞盒的交点。这个距离就是从当前点移动到发生碰撞的点的距离。
简单一点就用 Cast 方法就够了,Raycast 比 Cast 提供了更多细节操作,比如专门针对某一层的检测等,如果需要做更细节的优化,就得自己用 Raycast 方法来处理。

RaycastHit2D[] casthits = new RaycastHit2D[maxRaycastCache]; 
int num = selfCollider.Cast(forward, casthits);
RaycastHit 中比较重要的属性:
collider —— 目标碰撞盒
distance —— 到碰撞点的距离(上面图3中蓝线的长度)
normal —— 碰撞点的法线
point —— 目标碰撞点
接下来的逻辑就是:
比较本帧移动距离和 distance ,如果本帧移动距离比较小,就直接移动。

float stepDistance = speed * Time.fixedDeltaTime; 
if (stepDistance < distance) 
{ 
   gameObject.transform.Translate(forward * stepDistance); distance -= stepDistance; 
}
当然之后要把总距离减去这步的移动距离。
如果这一帧的距离大于剩余的距离了,就需要处理反射了。
先移动到碰撞点→方向向量反射→继续移动完。

while (stepDistance > nextDistance) 
{ 
    gameObject.transform.Translate(forward * nextDistance); 
    stepDistance -= nextDistance;

    forward = Vector2.Reflect(forward, normal); 
    /* 找下一个碰撞目标 */ 
    …… 
}
打完收工。
当然这只是简单的描述,实际上还会遇到一些坑,比如当前自己和一个碰撞盒有重叠的时候,取到的第一个 RaycastHit 永远是有接触的这个碰撞盒,此时与方向向量无关,所以需要根据自己的逻辑处理。还有同时碰撞两个或多个碰撞盒等,都要小心处理。

libgit2使用教程(十三)git rebase

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

简单点说,这不是合并。

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

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

git_rebase* prebase = nullptr; 
git_rebase_options rebase_opt = GIT_REBASE_OPTIONS_INIT; 
error = git_rebase_open(&prebase, rep, &rebase_opt);

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

git_rebase_abort(prebase);

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

接下来创建一个 rebase

git_rebase* prebase = nullptr; 
git_rebase_options rebase_opt = GIT_REBASE_OPTIONS_INIT; git_reference* onto_branch = nullptr; 
git_annotated_commit* onto = nullptr; 
git_branch_lookup(&onto_branch, rep, "new", GIT_BRANCH_LOCAL); git_annotated_commit_from_ref(&onto, rep, onto_branch); 
git_rebase_init(&prebase, rep, 
   nullptr /* current branch */, 
   nullptr /* upstream */ , 
   onto    /* branch to rebase onto */, 
   &rebase_opt);

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

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

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

git_rebase_operation* operation = nullptr; 
git_rebase_next(&operation, prebase)

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

while (git_rebase_next(&operation, prebase) != GIT_ITEROVER)

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

// reslove conflicts 
git_repository_index(&index, rep); 
if (git_index_has_conflicts(index)) 
{ 
   // git checkout --theirs 
   git_checkout_options opt = GIT_CHECKOUT_OPTIONS_INIT; 
   opt.checkout_strategy |= GIT_CHECKOUT_USE_THEIRS; 
   git_checkout_index(rep, index, &opt); 
} 

// add 
git_index_update_all(index, nullptr, nullptr, nullptr); 
git_index_write(index);

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

git_signature* me = nullptr; 
git_oid new_commit; 
git_signature_now(&me, "XiaochenFTX", "xiaochenftx@gmail.com"); 
git_rebase_commit(&new_commit, prebase, me, me, "UTF-8", "new rebase");

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

git_rebase_finish(prebase, me);

示例代码:sample13

libgit2使用教程(十二)git tag

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

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

git_strarray tags = {0};
git_tag_list(&tags, rep);
for (int i = 0; i < tags.count; ++i)
{
   std::cout << "tag: " << tags.strings[i] << "\n";
}

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

int tag_foreach_cb(const char *name, git_oid *oid, void *payload) 
{
    git_repository *rep = (git_repository *) payload;
    git_tag *tag = nullptr;
    
    int error = 0;
    std::cout << "tag name: " << name << "\n";
    error = git_tag_lookup(&tag, rep, oid);
    if (error < 0) {
        const git_error *e = giterr_last();
        std::cout << "Error: " << error << " / " << e->klass << " : " << e->message << std::endl;
        return 0;
    }
    std::cout << "tag message: " << git_tag_message(tag) << "\n";
    
    git_tag_free(tag);
    
    return 0;
}
git_tag_foreach(rep, tag_foreach_cb, rep);

这个接口是通过回调的方式遍历所有的 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

git_reference *head = nullptr;
git_annotated_commit *commit = nullptr;
git_object *target = nullptr;
git_oid tag_oid;
git_repository_head( &head, rep);
git_annotated_commit_from_ref( &commit, rep, head);
git_object_lookup( &target, rep, git_annotated_commit_id(commit), GIT_OBJ_COMMIT);
error = git_tag_create_lightweight(&tag_oid, rep, "v1.2.3", target, false);
if (error == GIT_EEXISTS)
{
    std::cout<< "tag 123 already exists \n";
}

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

2. 带附注的 tag

git_reference *head = nullptr;
git_annotated_commit *commit = nullptr;
git_object *target = nullptr;
git_oid tag_oid;
git_signature *me = nullptr;
git_repository_head( &head, rep);
git_annotated_commit_from_ref( &commit, rep, head);
git_object_lookup( &target, rep, git_annotated_commit_id(commit), GIT_OBJ_COMMIT);
git_signature_now( &me, "XiaochenFTX", "xiaochenftx@gmail.com");
error = git_tag_create(&tag_oid, rep, "v3.2.1", target, me, "message", false);
if(error==GIT_EEXISTS)
{
    std::cout<< "tag 321 already exists \n";
}

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

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

git_strarray tags = {0};
git_tag_list_match(&tags, "v0.*", rep);
for (int i = 0; i < tags.count; ++i)
{
   std::cout << "tag: " << tags.strings[i] << "\n";
}

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

error = git_tag_delete(rep, "v1");

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

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

git_repository_set_head(rep, "refs/tags/v3.2.1");

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

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

git_push_options push_opts = GIT_PUSH_OPTIONS_INIT;
push_opts.callbacks.credentials = cred_acquire_cb;
const char *refs[] = {"refs/tags/*:refs/tags/*"};
git_strarray strarr = {(char **) refs, 1};
git_remote_lookup(&remote, rep, "origin");
git_remote_push(remote, &strarr, &push_opts);

示例代码:sample12

libgit2使用教程(十一)git push

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

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

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

int cred_acquire_cb(git_cred **cred, const char *url, const char *username_from_url, unsigned int allowed_types,
                    void *payload)
{
    git_cred_ssh_key_new(cred, username_from_url, nullptr, "/Users/xiaochen/.ssh/id_rsa", nullptr);
    return 0;
}
git_push_options opts = GIT_PUSH_OPTIONS_INIT;
opts.callbacks.credentials = cred_acquire_cb;

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

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

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

git_remote *remote = nullptr;
git_remote_lookup(&remote, rep, "origin");

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

const char *refs[] = {"refs/heads/master:refs/heads/master"};
git_strarray strarr = {(char **) refs, 1};
opts.callbacks.credentials = cred_acquire_cb;
git_remote_push(remote, &strarr, &opts);

示例代码:sample11

libgit2使用教程(十)git clone

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

直接上代码

git_repository *rep = nullptr;
git_clone_options opt = GIT_CLONE_OPTIONS_INIT;

opt.fetch_opts.callbacks.credentials = cred_acquire_cb;
git_clone(&rep, "git@github.com:XiaochenFTX/libgit2_samples.git", path, &opt);
int cred_acquire_cb(git_cred **cred, const char *url, const char *username_from_url, unsigned int allowed_types,
                    void *payload)
{
    git_cred_ssh_key_new(cred, username_from_url, nullptr, "/Users/xiaochen/.ssh/id_rsa", nullptr);
    return 0;
}

需要注意的就是不要忘记身份验证,然后就开始 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 *remote = nullptr;
git_fetch_options fetch_opts = GIT_FETCH_OPTIONS_INIT;

// get a remote
git_remote_lookup(&remote, rep, "origin");
// git fetch
fetch_opts.callbacks.credentials = cred_acquire_cb;
git_remote_fetch(remote, nullptr, &fetch_opts, nullptr);

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

int cred_acquire_cb(git_cred **cred, const char *url, const char *username_from_url, unsigned int allowed_types,
                    void *payload)
{
    git_cred_ssh_key_new(cred, username_from_url, nullptr, "/Users/xiaochen/.ssh/id_rsa", nullptr);
    return 0;
}

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

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

fetch_opts.prune = GIT_FETCH_PRUNE;

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

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

git_reference *local_master = nullptr;
git_branch_lookup(&local_master, rep, "master", GIT_BRANCH_LOCAL);
git_repository_set_head(rep, git_reference_name(local_master));

然后去拿 origin master 的 commit

git_reference *origin_master = nullptr;
git_branch_lookup(&origin_master, rep, "origin/master", GIT_BRANCH_REMOTE);

合并

git_merge_options merge_opt = GIT_MERGE_OPTIONS_INIT;
git_checkout_options checkout_opt = GIT_CHECKOUT_OPTIONS_INIT;
const git_annotated_commit *their_head[10];
git_annotated_commit_from_ref((git_annotated_commit **) &their_head[0], rep, origin_master);
git_merge(rep, their_head, 1, &merge_opt, &checkout_opt);

解决冲突

git_index *index = nullptr;
git_index_conflict_iterator *conflict_iterator = nullptr;
git_repository_index(&index, rep);
if (git_index_has_conflicts(index))
{
    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;
    }
    // git checkout --theirs <file>
    git_checkout_options opt = GIT_CHECKOUT_OPTIONS_INIT;
    opt.checkout_strategy |= GIT_CHECKOUT_USE_THEIRS;
    git_checkout_index(rep, index, &opt);
    git_index_conflict_iterator_free(conflict_iterator);
}

add 和 commit

git_commit_lookup(&their_commit, rep, git_reference_target(origin_master));
git_commit_lookup(&our_commit, rep, git_reference_target(local_master));
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_commit_create_v(&commit_id, rep, git_reference_name(local_master), me, me, "UTF-8", "pull commit", new_tree, 2,
                    our_commit, their_commit);

清空状态

git_repository_state_cleanup(rep);

打完收工。

示例代码: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

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

git_remote *remote = nullptr;
git_remote_create(&remote, rep, "origin2",
    "git@github.com:XiaochenFTX/libgit2_samples.git");

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

git_remote_lookup(&remote, rep, "origin2");
git_remote_add_fetch(rep, "origin2",
    "+refs/heads/*:refs/remotes/origin/*");

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

git_strarray remote_list;
git_remote_list(&remote_list, rep);
std::cout << "list count: " << remote_list.count << std::endl;
for (int i = 0; i < remote_list.count; ++i)
{
    std::cout << remote_list.strings[i] << std::endl;
}

删除一个 remote :

git_remote_delete(rep, "origin2”);

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

git_remote_callbacks remote_conn_opt = GIT_REMOTE_CALLBACKS_INIT;
remote_conn_opt.credentials = cred_acquire_cb;
git_remote_connect(remote, GIT_DIRECTION_FETCH, &remote_conn_opt, nullptr, nullptr);

这个回调是这样的:

int cred_acquire_cb(git_cred **cred, const char *url, const char *username_from_url, unsigned int allowed_types,
                    void *payload)
{
    git_cred_ssh_key_new(cred, username_from_url, nullptr, "/Users/xiaochen/.ssh/id_rsa", nullptr);
    return 0;
}

通过 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 :

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

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

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!