这篇的主要内容是合并操作,是 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;