git如何merge私有remote branch及如何计算树节点最小功能祖先
一、背景说明
对于某些git项目,可能只有少数几个人(假设为Maintainer——M)有何如权限,其它人的合入需要这些有权限的开发者(假设为Developer——D)合入,当然,这通常的流程都是某个程序员fork自己的分支,合入,然后有M合入主线分支。这个流程是假设M和P都经过公共的远端服务器进行合并。
git作为一种分布式系统,它的设计本身是去中心化的,所以M和P都需要经过中心服务器这种某事应该是可以避免的,可不可以让一个P创建自己分支之后,不把这个分支提交到主线上而让M进行merge呢?当然是可以的,比方说自己生成一个补丁,或者整个打个包来发送给M,但是这些方法都是相当于使用了git之外的第三封进程来实现的,那么git内部有没有内置的解决方案呢?
整个时候就可以使用git的remote功能了。
总结起来说,也就是系统通过git的内置remote功能来实现一个对中心服务器无感知的合入操作:
1、开发者D从主线fork自己的分支进行本地开发和commit
2、开发者D将自己的网络地址告诉维护者M
3、维护者M在本地添加D的远端地址,执行fetch + merge
4、维护者M向主线合入开发者D的修改内容
可以看到在整个过程中,开发者D对于主线只有一个读取操作,而且使用了git内置的remote功能来实现了流程,D的提交记录(日志内容)在M的合入也是没有丢失的。
二、模拟一下这个流程
为了简单起见,这里没有使用远端地址,所有操作都是使用本地文件系统。这里要注意的一点是,开发者tsecer始终没有向服务器执行过push或者merge,整个过程由维护者Maint把tsecer设置为远端库并更新
1、管理员创建一个git仓库
Admin@Repo: mkdir git.merge.remote
Admin@Repo: cd git.merge.remote
Admin@Repo: git init --bare
已初始化空的 Git 仓库于 /home/harry/git.merge.remote/
Admin@Repo:
2、Maint提交修改
Maint@harry:git clone /home/harry/git.merge.remote .
正克隆到 '.'...
warning: 您似乎克隆了一个空仓库。
完成。
Maint@harry:pwd
/home/harry/git.merge.local/Maint
Maint@harry:echo "Maint add" > readme
Maint@harry:git add readme
Maint@harry:git commit -m "form maint"
[master(根提交) 2dc1633] form maint
1 file changed, 1 insertion(+)
create mode 100644 readme
Maint@harry:git push
枚举对象: 3, 完成.
对象计数中: 100% (3/3), 完成.
写入对象中: 100% (3/3), 212 字节 | 212.00 KiB/s, 完成.
总共 3(差异 0),复用 0(差异 0),包复用 0
To /home/harry/git.merge.remote
* [new branch] master -> master
Maint@harry:git log
commit 2dc163344ab4d56e2b9d8a8a1d7872727f456e08 (HEAD -> master, origin/master)
Author: Maint <Maint@harry.com>
Date: Thu Jul 23 20:29:20 2020 +0800
form maint
Maint@harry:
3、Dev fork并本地修改
tsecer@harry: git clone /home/harry/git.merge.remote .
正克隆到 '.'...
完成。
tsecer@harry: echo "from tsecer" >> readme
tsecer@harry: git add readme
tsecer@harry: git commit -m "append from tsecer"
[master acccced] append from tsecer
1 file changed, 1 insertion(+)
tsecer@harry:
4、Maint设置tsecer为remote并合并
Maint@harry:git remote add tsecer /home/harry/git.merge.local/Dev
Maint@harry:git fetch tsecer
remote: 枚举对象: 5, 完成.
remote: 对象计数中: 100% (5/5), 完成.
remote: 总共 3(差异 0),复用 0(差异 0),包复用 0
展开对象中: 100% (3/3), 236 字节 | 236.00 KiB/s, 完成.
来自 /home/harry/git.merge.local/Dev
* [新分支] master -> tsecer/master
Maint@harry:git merge remotes/tsecer/master
更新 2dc1633..acccced
Fast-forward
readme | 1 +
1 file changed, 1 insertion(+)
Maint@harry:git log
commit acccced0299c2dfd116dbf66e3437a026bb46ee7 (HEAD -> master, tsecer/master)
Author: tsecer <tsecer@harry.com>
Date: Thu Jul 23 20:32:27 2020 +0800
append from tsecer
commit 2dc163344ab4d56e2b9d8a8a1d7872727f456e08 (origin/master)
Author: Maint <Maint@harry.com>
Date: Thu Jul 23 20:29:20 2020 +0800
form maint
Maint@harry:
三、为什么Maint merge的时候要指定为remotes/tsecer/master
git自带文档中对于版本号的说明 git-master\Documentation\revisions.txt
'<refname>', e.g. 'master', 'heads/master', 'refs/heads/master':: A symbolic ref name. E.g. 'master' typically means the commit
object referenced by 'refs/heads/master'. If you
happen to have both 'heads/master' and 'tags/master', you can explicitly say 'heads/master' to tell Git which one you mean.
When ambiguous, a '<refname>' is disambiguated by taking the
first match in the following rules:
. If '$GIT_DIR/<refname>' exists, that is what you mean (this is usually useful only for `HEAD`, `FETCH_HEAD`, `ORIG_HEAD`, `MERGE_HEAD`
and `CHERRY_PICK_HEAD`);
. otherwise, 'refs/<refname>' if it exists;
. otherwise, 'refs/tags/<refname>' if it exists;
. otherwise, 'refs/heads/<refname>' if it exists;
. otherwise, 'refs/remotes/<refname>' if it exists;
. otherwise, 'refs/remotes/<refname>/HEAD' if it exists.
git代码中对应的处理内容
git-master\refs.c
static const char *ref_rev_parse_rules[] = {
"%.*s",
"refs/%.*s",
"refs/tags/%.*s",
"refs/heads/%.*s",
"refs/remotes/%.*s",
"refs/remotes/%.*s/HEAD",
NULL
};
从这里可以看到,设置的远端并不符合整个模式,所以要指定完成路径,当然从这个地方看,remotes这个前缀是可以省略的,也就是
git merge tsecer/master
也是可以的啦。
四、git最小公共祖先节点
虽然tsecer的版本库在远端,但是毕竟是同祖同源的一个版本库,也就是说两个版本库都是有相同的祖先结点的。在执行merge的时候首先要识别出这个公共节点,从而保证两棵树是可以进行合并的。当然这个只是一个合理性检测,也就是说如果不满足这个条件不是说不能合并,而只是说合并的时候不合常理。如果是两个完全不同的版本库,那么可以在git merge的时候通过--allow-unrelated-histories选项来抑制这个检测。
那么如何确定最小祖先节点呢?这个问题如果是通用的问题就比较麻烦,但是好在git的每个提交日志都是由时间戳的,这样的话可以使用这个内置的时间戳提供的hint进行排序现场归并排序。大致来说就是维护一个优先级队列,按照时间戳进行排序,把两个分支的父节点放入优先级队列,每次从其中找到一个时间戳最新的节点,递归这个取父节点进入优先级队列过程(类似于广度优先搜索),直到取到某个节点身上同时设置了PARENT1和PARENT2两个标志位,这个节点就是公共祖先结点,整个过程解说。
git-master\commit-reach.c
/* all input commits in one and twos[] must have been parsed! */
static struct commit_list *paint_down_to_common(struct repository *r,
struct commit *one, int n,
struct commit **twos,
int min_generation)
{
……
while (queue_has_nonstale(&queue)) {
struct commit *commit = prio_queue_get(&queue);
struct commit_list *parents;
int flags;
if (min_generation && commit->generation > last_gen)
BUG("bad generation skip %8x > %8x at %s",
commit->generation, last_gen,
oid_to_hex(&commit->object.oid));
last_gen = commit->generation;
if (commit->generation < min_generation)
break;
flags = commit->object.flags & (PARENT1 | PARENT2 | STALE);
……
}
五、如果是通用二叉树呢
1、在力扣上这种问题的间接做法
它的实现思路是,它们的最小公共父节点,一定是唯一一个同时在左右子节点分别包含p和q的节点。
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if (!root || root == p || root == q) return root;
TreeNode *left = lowestCommonAncestor(root->left, p, q);
TreeNode *right = lowestCommonAncestor(root->right, p, q);
if (left && right) return root;
return left ? left : right;
}
};
2、可能更直观的方法
如果假设二叉树层数并不是很深,那么可以分别找到从当前节点到根节点的完成路径,然后从两个路径反向查找,第一个分叉的地方就是它们的公共父节点。当然需要假设每个节点是有父节点指针的。
这种方法对于git的提交树来说显然不太合适,因为他git的“树”其实很多情况下是单链的,而merge的时候可能存在不止一个父节点,从而使整个问题更加复杂。