【山大智云】SeafileServer源码分析之版本管理系统:提交差异与合并

2021SC@SDUSC

提交差异

为了方便用户检视,以及为合并做准备,需要设计一种算法,来统计两个提交间(分支)的差异。在思考算法的具体流程前,需要先总结差异的类型。

差异类型

若有两个提交Commit, Parent,假设我们考虑Commit相对于Parent的差异。即,Commit是我的提交,Parent是别人的提交,现在需要考虑我的提交相对于别人的提交的不同。

文件差异

差异说明符号
添加Commit相对于Parent添加了该文件。A
删除Commit相对于Parent删除了该文件。D
修改Commit相对于Parent添加了该文件。M
重命名Commit相对于Parent重命名了该文件,且内容尚未被修改。R

目录差异

差异说明符号
添加Commit相对于Parent添加了该目录。B
删除Commit相对于Parent删除了该目录。C
重命名Commit相对于Parent重命名了该目录,且链接尚未被修改。E

未合并差异

三路差异中的特殊情况。

差异说明
STATUS_UNMERGED_NONE未合并
STATUS_UNMERGED_BOTH_CHANGED两者都被修改:相同文件被修改为不同结果
STATUS_UNMERGED_BOTH_ADDED两者都被添加:两个合并都添加了相同的文件
STATUS_UNMERGED_I_REMOVED一个移除了文件,一个修改了文件
STATUS_UNMERGED_OTHERS_REMOVED一个修改了文件,一个移除了文件
STATUS_UNMERGED_DFC_I_ADDED_FILE一个将目录替换为了文件,另一个修改了目录中的文件
STATUS_UNMERGED_DFC_OTHERS_ADDED_FILE一个修改了目录中的文件,另一个将目录替换为了文件

(实际使用中的目的未知,因为尚未找到相关实现代码)

算法

流程

  • 同构递归

    目的是递归遍历各路文件树位置同构的部分。伪代码如下:

    文件树同构递归(Tree[s][n]:n路文件树上位置同构的目录):
    	Dirents[n]:n路文件树上位置同构的目录项
    	While (1):
    		获取同构目录项至Dirents
    		如果n路的Dirents完全相同:
    	    	Return
    		文件差异处理(Dirents)
    		目录差异处理(Dirents)
    
    文件差异处理(Dirents[n]:n路文件树上位置同构的文件):
    	获取对应的Files,然后调用差异回调函数
    
    目录差异处理(Dirents[n]:n路文件树上位置同构的文件):
    	获取对应的Dir,然后调用差异回调函数
    	文件树同构递归(Dirs)
    
  • 同构目录项

    同构递归之所以能做到多路位置同构,很重要的一点是需要获取同构目录项,而这一点需要某些基准。回到位置同构的本质,它实际上就是需要节点到根的路径完全相同。考虑搜索二/多叉树,因为存在权值,所以我们很容易找到多棵树中节点到根权值完全相同的路径。那么文件树中可以用什么作为权值呢?文件名/目录名。

    我们认为相同名称的目录项,在多路位置同构目录中也位置同构。基于这个假设,我们很快能设计出高效复杂度的遍历同构项的方法。考虑将目录项按名称排序(这也是获取Seafdir时的默认操作),然后我们就可以用类似归并排序中的方法,基于多指针来得到多路相同的目录项。

当然,仅仅是知道同构仍然是不够的,因为我们的目的是需要知道同构的差异,所以我们需要进一步使用差异算法来获得差异。

二路差异与三路差异

对于两种产生提交的方式,有两种差异算法:

  1. 二路差异:应用于普通提交。判断Commit相对于Parent的差异。能直接确定非差异内容,而对于修改内容,则需要判断内容。

  2. 三路差异:应用于合并后提交。判断Commit相对于ParentA和ParentB的差异。可以区分出Commit相对于两者的增加、删除。但是修改仍然要结合内容来判断。


无论是二路差异还是三路差异,都无法判断用户是否是重命名了文件或目录,因为在基于名称的位置同构中,重命名等价于删除后增加。后处理中将处理这种情况。

后处理

  • 重命名的近似判断

    已知,基于名称的同构中,如果一个文件被重命名了,那么它可能会被判定为既被删除又被增加。那么如何还原真实的状态?

    答案是只需要找到相同内容,如果相同内容下存在“删除-增加”对,则认为该目录或文件被重命名了。

    注意这只是一种近似判断,有可能用户真的是删除后又增加,但由于我们不监控用户的行为,所以统一认为是重命名。

  • 冗余空目录

    另一个会出现bug的点在空目录。空目录是个特殊存在,所有空目录都指向同一个空目录对象,因为它们的内容相同,所以SHA1即对象名都相同。因此存在两种情况会对一个空目录作出误判:

    1. 向空目录添加文件后,会判定该空目录被删除;
    2. 将某目录清空后,会判定空目录被增加。

    那么如何处理这两种情况?其实很简单,遍历所有状态为增加或删除的目录,然后特判一下,再将差异类型修正即可。

实现

  • 差异对象

    typedef struct DiffEntry { 	// 差异对象
        char type; 				// 差异类型
        char status; 			// 差异状态
        int unmerge_state; 		// 未合并状态
        unsigned char sha1[20]; // 用于解决重命名问题
        char *name; 			// 名称
        char *new_name;         // 新名称,仅被用于重命名情形
        gint64 size; 			// 大小
        gint64 origin_size;     // 原始大小,仅被用于修改情形
    } DiffEntry;
    
  • 差异对比选项

    typedef struct DiffOptions { // 比对选项
        char store_id[37]; 		// 存储id
        int version; 			// seafile版本
        // 两个回调
        DiffFileCB file_cb; 	// 文件差异处理回调
        DiffDirCB dir_cb;		// 目录差异处理回调
        void *data; 			// 用户参数
    } DiffOptions;
    

    在使用中向差异对比操作传入该结构体,需要设置目录和文件回调函数。回调即差异算法。

  • 差异对比算法

    • 同构位置递归
    static int // 文件树同构递归
    diff_trees_recursive (int n, SeafDir *trees[],
                          const char *basedir, DiffOptions *opt);
    int // 文件树差异处理,封装
    diff_trees (int n, const char *roots[], DiffOptions *opt)
    static int // 目录差异处理
    diff_directories (int n, SeafDirent *dents[], const char *basedir, DiffOptions *opt)
    static int // 文件差异处理
    diff_files (int n, SeafDirent *dents[], const char *basedir, DiffOptions *opt)
    
    • 差异算法
    static int // 二路文件差异处理
    twoway_diff_files (int n, const char *basedir, SeafDirent *files[], void *vdata)
    static int // 二路目录差异处理
    twoway_diff_dirs (int n, const char *basedir, SeafDirent *dirs[], void *vdata,
                      gboolean *recurse)
    static int // 三路文件差异处理
    threeway_diff_files (int n, const char *basedir, SeafDirent *files[], void *vdata)
    static int // 三路目录差异处理(默认无操作)
    threeway_diff_dirs (int n, const char *basedir, SeafDirent *dirs[], void *vdata,
                        gboolean *recurse)
    

    其中有一个小环节值得一提,就是如何判断内容是否相同。这时再次体现了SHA1摘要命名的好处:我们只需要对比两个文件系统对象的id,就能直接判断它们的内容是否相同。

    • 后处理
    void // 解决重命名问题
    diff_resolve_renames (GList **diff_entries)
    void // 解决冗余空目录问题
    diff_resolve_empty_dirs (GList **diff_entries)
    
  • 封装

    • 普通提交
    int // 比对两个提交的差异
    diff_commits (SeafCommit *commit1, SeafCommit *commit2, GList **results, // 结果记录在results列表
                  gboolean fold_dir_diff);
    int // 比对两个提交的差异;给定根目录
    diff_commit_roots (const char *store_id, int version,
                       const char *root1, const char *root2, GList **results,
                       gboolean fold_dir_diff);
    
    • 合并后提交
    int // 比对合并前后的差异(与两个父提交对比)
    diff_merge (SeafCommit *merge, GList **results, gboolean fold_dir_diff);
    int // 比对合并前后的差异;给定根目录
    diff_merge_roots (const char *store_id, int version,
                      const char *merged_root, const char *p1_root, const char *p2_root,
                      GList **results, gboolean fold_dir_diff);
    

分支合并

合并、冲突与解决

合并是一种重要的手段,能将多个提交(分支)合为一体。合并的对象时两个提交,但一般被认为是分支的合并,因为需要用分支作为提交的指针。分支合并在图中的性质就是入度等于二,即一个提交的生成由两个父提交产生,并且可以携带增量内容。

合并两个提交(分支),最重要的问题就是解决冲突。冲突的判断实际上与差异的判断类似,都是借助位置同构进行的,在此不做赘述。这里的关键问题在于如何判定冲突和解决冲突。冲突对于用户来讲敏感,用户需要通过比较选择哪一个的内容将其放到新版本中。同时冲突又有可能是用户定义的,所有非相同内容都有可能是冲突。

在此有两种解决思路:第一种是二路合并,第二种是三路合并。

二路合并很简单,仅相同内容被保留,删除增加修改全部留给用户选择。因为对二路的非相同内容完全无法判断是否应该保留还是舍弃。这样做实现很容易,但对用户来说很麻烦,不过非常保险,因为冲突选择权全部交给用户,完全可以由用户定义冲突。

三路合并则是具有一定自主性,引入了Base即两个分支的公共祖先,然后借助Base来判断保留内容还是交给用户选择。这样做提高了效率,但并不一定保险,因为算法定义的冲突有可能不符合用户定义。

三路合并

合并的参与者包括三个分支,Base、Head、Remote。我们的目的是将Head和Remote合并,并且Base是它们的共同祖先。

引入了Base后,所有差异内容的判断都有了判断准则。算法认为两分支相对于祖先不交叉的更改(包括增删改)都是被保留的,而交叉的更改则是冲突内容。这样的冲突定义实际上已经符合大多数情况,而且将冲突的可能范畴进一步缩小,可以说是在效率与用户需求之间找到了平衡。

我们用表格来表示合并中三个分支可能出现的情况。假设各个分支中位置同构部分的内容以大写字母表示,则可以得到如下合并结果:

BaseHeadRemote结果说明
AAAA相同内容
AABB如果只有一方修改了,那么选择修改的
ABAB同上
ABBB如果双方拥有相同的变更,则选择修改过的
ABCconflict如果双方都修改了且不一样,则报告冲突,需要用户解决

转自:https://blog.csdn.net/longintchar/article/details/83049840

由上述表格,我们能够直接给出各个同构位置合并是如何处理的。所以三路合并算法也自然显现:套用之前的同构递归,然后实现该表格中的判断即可。

实现

函数递归和调用流程如图:

内容就是实现了三路合并算法(二路合并并未实装,将只调用回调函数)。

最后需要阐明的是合并结果的存储。对于合并后的结果,有两种选项:如果do_merge=true,则直接写入硬盘,然后返回一个根目录id到merged_tree_root;反之,只调用回调函数。

在此不再粘贴详细源码,详见:merge-new.c

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值