Vue 集成 v-code-diff 展示工蜂仓库 MR 文件 diff 信息
由于项目需要融合工蜂,因此需要实现一个用于展示某个工蜂仓库的某次 MR 提交的详情页,包括 MR 基础信息的展示、MR 包含的文件列表的展示以及每一个文件的新旧版本之间的差异信息。
一、业务描述
由于项目需要融合工蜂,因此需要实现一个用于展示某个工蜂仓库的某次 MR 提交的详情页,包括 MR 基础信息的展示、MR 包含的文件列表的展示以及每一个文件的新旧版本之间的差异信息。
因此,主要的业务流程是:
- 通过工蜂 API 获取 MR 详情数据;
- 进行一些数据处理,返回给前端;
- 前端通过 v-code-diff 组件展示。
二、从后端拿到数据
之前的一篇文章中,封装了 Java 调用 HttpURLConnection 来访问第三方 API 的接口,感兴趣可以移步链接:Java 封装 HTTP 请求对接企业 API。
因此,后端的接口具体实现如下:
public MrDetail getMrDetail(int gitProjectId, int mrId) {
String url = "/api/v3/projects/"+ gitProjectId +"/merge_request/" + mrId + "/changes";
String params = "";
MrDetail mrDetail = new MrDetail();
List<FileDiff> fileDiffList = new ArrayList<>();
try {
JSONObject json = requestService.getGitCodeRequest(url, params);
JSONArray jsonFileList = json.getJSONArray("files");
for(Object file : jsonFileList) {
JSONObject jsonFile = (JSONObject) file;
FileDiff fileDiff = new FileDiff();
fileDiff
.setOldPath(jsonFile.getString("old_path"))
.setNewPath(jsonFile.getString("new_path"))
.setAMode(jsonFile.getInteger("a_mode"))
.setBMode(jsonFile.getInteger("b_mode"))
.setDiff(jsonFile.getString("diff"))
.setNewFile(jsonFile.getBoolean("new_file"))
.setRenameFile(jsonFile.getBoolean("renamed_file"))
.setDeleteFile(jsonFile.getBoolean("deleted_file"))
.setTooLarge(jsonFile.getBoolean("is_too_large"))
.setCollapse(jsonFile.getBoolean("is_collapse"))
.setAdditions(jsonFile.getInteger("additions"))
.setDeletions(jsonFile.getInteger("deletions"));
/* ** 注释掉的这部分是数据清洗的操作,后面会讲到。
**
splitFileDiff(fileDiff); // 处理file中的Diff信息;
if(isVaildFile(fileDiff)) { // 判断file是否合法 ;
fileDiff.setForShow(true);
} else {
fileDiff.setForShow(false);
}
**
** */
fileDiffList.add(fileDiff);
}
mrDetail.setId(json.getInteger("id"))
.setTitle(json.getString("title"))
.setSourceBranch(json.getString("source_branch"))
.setTargetBranch(json.getString("target_branch"))
.setState(json.getString("state"))
.setMergeStatus(json.getString("merge_status"))
.setDescription(json.getString("description"))
.setAuthor(((JSONObject)json.get("author")).getString("username"))
.setCreatedTime(json.getString("created_at"))
.setGitProjectId(Integer.toString(gitProjectId))
.setFileDiffs(fileDiffList);
} catch (Exception e) {
throw new BusinessException(e.toString());
}
return mrDetail;
}
通过这种方式,可以获得 MR 详情的 JSON 对象。如下图所示。
{
// 仅截取部分关键信息
"id": 123456,
"title": "WIP ajax filter",
"target_branch": "master",
"source_branch": "user",
"state": "opened",
"created_at": "2022-07-08T08:58:08+0000",
"files": [
{
"old_path": "web/pages/user/regist_success.html",
"new_path": "web/pages/user/regist_success.html",
"a_mode": 33188,
"b_mode": 33188,
"diff": "@@ -3,7 +3,8 @@\n <head>\n <meta charset=\"UTF-8\">\n <title>尚硅谷会员注册页面</title>\n-<link type=\"text/css\" rel=\"stylesheet\" href=\"../../static/css/style.css\" >\n+\t<base href=\"http://localhost:8080/book/\">\n+<link type=\"text/css\" rel=\"stylesheet\" href=\"static/css/style.css\" >\n <style type=\"text/css\">\n \th1 {\n \t\ttext-align: center;\n@@ -11,13 +12,13 @@\n \t}\n \t\n \th1 a {\n-\t\tcolor:red;\n+\t\tcolor: #ff0000;\n \t}\n </style>\n </head>\n <body>\n \t\t<div id=\"header\">\n-\t\t\t\t<img class=\"logo_img\" alt=\"\" src=\"../../static/img/logo.gif\" >\n+\t\t\t\t<img class=\"logo_img\" alt=\"\" src=\"static/img/logo.gif\" >\n \t\t\t\t<span class=\"wel_word\"></span>\n \t\t\t\t<div>\n \t\t\t\t\t<span>欢迎<span class=\"um_span\">韩总</span>光临尚硅谷书城</span>\n@@ -29,7 +30,7 @@\n \t\t\n \t\t<div id=\"main\">\n \t\t\n-\t\t\t<h1>注册成功! <a href=\"../../index.html\">转到主页</a></h1>\n+\t\t\t<h1>注册成功! <a href=\"index.html\">转到主页</a></h1>\n \t\n \t\t</div>\n \t\t\n",
"new_file": false,
"renamed_file": false,
"deleted_file": false,
"is_too_large": false,
"is_collapse": false,
"additions": 5,
"deletions": 4
},
{
"old_path": "web/pages/user/login_success.html",
"new_path": "web/pages/user/login_success.html",
"a_mode": 33188,
"b_mode": 33188,
"diff": "@@ -3,7 +3,8 @@\n <head>\n <meta charset=\"UTF-8\">\n <title>尚硅谷会员注册页面</title>\n-<link type=\"text/css\" rel=\"stylesheet\" href=\"../../static/css/style.css\" >\n+\t<base href=\"http://localhost:8080/book/\">\n+<link type=\"text/css\" rel=\"stylesheet\" href=\"static/css/style.css\" >\n <style type=\"text/css\">\n \th1 {\n \t\ttext-align: center;\n@@ -17,12 +18,12 @@\n </head>\n <body>\n \t\t<div id=\"header\">\n-\t\t\t\t<img class=\"logo_img\" alt=\"\" src=\"../../static/img/logo.gif\" >\n+\t\t\t\t<img class=\"logo_img\" alt=\"\" src=\"static/img/logo.gif\" >\n \t\t\t\t<div>\n \t\t\t\t\t<span>欢迎<span class=\"um_span\">韩总</span>光临尚硅谷书城</span>\n-\t\t\t\t\t<a href=\"../order/order.html\">我的订单</a>\n-\t\t\t\t\t<a href=\"../../index.html\">注销</a> \n-\t\t\t\t\t<a href=\"../../index.html\">返回</a>\n+\t\t\t\t\t<a href=\"pages/order/order.html\">我的订单</a>\n+\t\t\t\t\t<a href=\"index.html\">注销</a> \n+\t\t\t\t\t<a href=\"index.html\">返回</a>\n \t\t\t\t</div>\n \t\t</div>\n \t\t\n",
"new_file": false,
"renamed_file": false,
"deleted_file": false,
"is_too_large": false,
"is_collapse": false,
"additions": 6,
"deletions": 5
},
]
}
其中,files 是一个对象列表,里面包含了每个文件的新旧版本差异信息。
但是可以看到,diff 中的字符串是将新旧版本的代码行添加进了一个文件中,并没有给我们拆分好两个文件,因此,这也是后续需要数据清洗的原因。
三、前端集成 v-code-diff 显示 code diff 信息
前端需要用什么方式展示 code diff 信息呢,这里本项目采用了 GitHub 上的一个开源组件:v-code-diff(官方文档:https://github.com/Shimada666/v-code-diff)
该组件展示页面如下图所示:
想亲自体验一把的,请移步官方项目演示链接(https://shimada666.github.io/v-code-diff/)。
在前端代码中集成:
<!-- File borad -->
<div class="file_board">
<div class="file_list">
<el-card style="height: 100%; overflow: scroll;">
文件列表:
<div v-for="(file, index) in fileList" :key="index">
<el-tooltip v-if="file.forShow" class="item" effect="dark" placement="top">
<template v-slot:content>
{{file.newPath}}
</template>
<el-button v-if="file.changeLineNo > 50" type="text" @click="showFile = file">
*
<i class="el-icon-document"></i>
{{file.newPath}}
</el-button>
<div v-else>
<el-button v-if="file.newFile" type="text" icon="el-icon-document-add" @click="showFile = file">{{file.newPath}}</el-button>
<el-button v-else type="text" icon="el-icon-document" @click="showFile = file">{{file.newPath}}</el-button>
</div>
</el-tooltip>
</div>
</el-card>
</div>
<div class="diff_list">
<code-diff
:old-string="showFile.oldStr"
:new-string="showFile.newStr"
:file-name="showFile.newPath"
:context="15"
:drawFileList="true"
output-format="side-by-side"/>
</div>
</div>
<style lang="less" scoped>
.file_board {
width: 100%;
height: 500px;
display: flex;
margin-top: 10px;
}
.file_list {
height: 500px;
width: 30%;
}
.diff_list {
height: 500px;
width: 70%;
overflow-y: scroll;
}
</style>
但此时,后端传过来的数据是没有办法直接在前端显示的。因为 code-diff 组件需要传入两个字符串(旧版本 + 新版本),但是目前的数据中,新旧版本是整合到一个字符串中的,因此需要在后端进行数据处理。
四、回到后端,数据清洗
由于前端对于每一个文件,都需要两个不同的字符串属性,分别存储 oldString 和 newString。因此主要的清洗目的,就是将 files 中每一个 file 的 diff 字符串,拆分成两个新的属性。
因此可以采用 正则表达式 匹配的方式,首先将字符串分割成若干个包含 diff 的代码段,然后对于每一个代码段,再次分割成代码行。此时,粒度划分到最低层次。遍历所有的代码行,根据首字符是 “+” 还是 “-”,来决定将这一行代码加入 oldString 还是 newString。
具体实现代码如下所示。
private void splitFileDiff(FileDiff fileDiff) {
String pattern1 = "@{2}\\s+[+-]\\d+,\\d+\\s+[+-]\\d+,\\d+\\s+@{2}\\n";
String pattern2 = "\\n";
String oldStr = "";
String newStr = "";
int lines = 0;
// 按照 pattern1 分割成若干个代码段
for(int i = 0; i < fileDiff.getDiff().split(pattern1).length; i++) {
// 按照 pattern2 分割成代码行
for(int j = 0; j < fileDiff.getDiff().split(pattern1)[i].split(pattern2).length; j++) {
// 去除首尾空字符
String line = fileDiff.getDiff().split(pattern1)[i].split(pattern2)[j];
// 如果开头是“-”,放入oldStr;如果开头是“+”,放入newStr
if(line.startsWith("-")) {
oldStr = oldStr + line.substring(1) + "\n";
} else if(line.startsWith("+")) {
newStr = newStr + line.substring(1) + "\n";
} else {
oldStr = oldStr + line + "\n";
newStr = newStr + line + "\n";
}
lines++;
}
oldStr = oldStr + "\n" + "/* ** ** 已省略部分代码 ** ** */ \n";
newStr = newStr + "\n" + "/* ** ** 已省略部分代码 ** ** */ \n";
}
fileDiff.setOldStr(oldStr).setNewStr(newStr).setLines(lines);
}
要注意的是,正则表达式
“@{2}\s+[±]\d+,\d+\s+[±]\d+,\d+\s+@{2}\n”
匹配的是:
“@@ -3,7 +3,8 @@”
迭代中,需要在代码块与代码块之间插入分割行。
并且,前端作为简单功能,不可能将一次 MR 的文件尽数展示,因此,需要对文件列表中的不合法的文件进行过滤。这里过滤的条件是:
- 后缀为 “.jar” 的文件(包文件,不展示);
- 后缀为 “/null” 的文件(代表已删除的文件,不必展示);
- 前缀为 “out/” 的文件(代表编译后的制品文件,不展示);
- 文件行数过长,不展示。
代码实现如下:
private boolean isVaildFile(FileDiff file) {
if(file.isRenameFile() || file.isDeleteFile() || file.isTooLarge()) {
return false;
} else if(file.getNewPath().endsWith(".jar") || file.getNewPath().endsWith("/dev/null")) {
return false;
} else if(file.getNewPath().startsWith("out/")) {
return false;
} else if(file.getLines() > 50) {
return false;
} else {
return true;
}
}
这时,再去前端发送请求,可以看到,现在的字符串已经拆分成功了。
五、对接前端,处理样式冲突
将前端的 oldString 和 newString 进行绑定到v-code-diff 中后,出现了一个讨厌的问题——样式问题。
组件左右两栏的第一列长度超出了既定的高度,溢出了。如下图所示。
为了解决这个问题,我们进入浏览器的开发者模式,查看组件的样式表。
我们发现,v-code-diff 组件的表格是基于 tr/td 的,而表头一栏的数字是由 d2h-code-side-linenumber 样式决定的。仔细查看这个样式表,发现其 position 属性被确定了 “absolute”,原来问题出在这里。
回到前端代码样式表,使用 lang=“less” 更改原生样式。
<style lang="less" scoped>
/deep/ .d2h-code-side-linenumber {
position: relative;
}
</style>
刷新查看,问题解决!
更多推荐
所有评论(0)