这篇的主要内容是合并操作,是 git 中非常重要的操作。合并就有可能出现冲突,所以同时会简单介绍一下解决冲突。
合并操作主要针对的对象实际上并不是分支,只不过 commit 组成了一条分支,所以可以用分支来作为 commit 的索引。我们首先需要找到需要合并过来的那个 commit (或者多个 commit)。示例只简单的将一个分支最新的 commit 合并到 HEAD ,更多拓展的操作您可以自己发挥。
首先获得分支最新的 commit :
1 2 3 4 5 |
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 :
1 2 3 |
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 是会报错的,所以如果报错了需要做一下判断,是继续上次未完成的合并,或是做一下清理:
1 |
git_repository_state_cleanup(rep); |
这个状态清理,是合并操作流程完成后一定要调用的,否则磁盘上的标志文件会一直存在,会影响下一次合并操作。
git_merge 操作完成后,就要检查一下是否有冲突:
1 2 3 4 |
git_index* index = nullptr; git_repository_index(&index, rep); if (git_index_has_conflicts(index)) { /* reslove code ... */ } |
需要通过 index 来检查。
如果是有冲突的情况,可以通过 conflict_iterator 对冲突进行遍历:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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) 快速选择一个版本作为最终合并后的版本:
1 2 3 |
git_checkout_options opt = GIT_CHECKOUT_OPTIONS_INIT; opt.checkout_strategy |= GIT_CHECKOUT_USE_THEIRS; git_checkout_index(rep, index, &opt); |
也可以选择对指定文件 checkout ,只要设置一下 options 的参数:
1 2 3 |
const char* p = "file"; opt.paths.strings = (char**)&p; opt.paths.count = 1; |
2. 手动改写工作目录下的文件,通过取出的 git_index_entry 的 oid 可以获得对应版本文件的 blob ,这样可以把对应文件的内容取出。根据需要,将工作目录下的文件改写,这个就完全看自己的业务逻辑需要了,所以就不提供参考代码了。
之后要将这个解决完的冲突 remove 掉:
1 |
git_index_conflict_remove(index, "file"); |
现在解决完冲突的文件状态已经在硬盘上体现出来了,但是实际上并没有完全更新到 index ,接下来就依照文件更新后 add 和 commit 的操作:更新 index→生成 tree → commit 来完成整个合并操作。
需要注意的一点是,因为是两个 commit 合并成新的 commit 所以这个新的 commit 的 parent 应该有两个:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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); |
这些细节在之前写过了,所以就不细说了。最后将合并状态清理一下就可以结束了:
1 |
git_repository_state_cleanup(rep); |
补充说明
虽然 git_merge 传入了 annotated_commit 的数组,但是从实现代码上来看,合并操作只取了数组的第0个元素,所以实际上并不能实现同时将好多个 commit 合并的 HEAD。
1 2 3 4 5 |
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; |