开源一夏 | Java"实现"svn 文件对比
作者:六月的雨在infoQ
- 2022 年 8 月 11 日 北京
本文字数:18150 字
阅读完需:约 60 分钟
需求
当前 web 项目需要实现文件内容对比功能,开发语言是 java,也就是通过 java 实现类似于 svn 的文件对比功能。
实现效果
实现要达到的预期效果图即使不能和 svn 完全一样,也要基本达到相应效果,效果图如下
后端代码引入
首先在 springboot 项目中的 pom.xml 文件引入文件对比的核心 jar 包
<!--对比工具依赖-->
<dependency>
<groupId>io.github.java-diff-utils</groupId>
<artifactId>java-diff-utils</artifactId>
<version>4.9</version>
</dependency>
复制代码
页面流程实现,点击列表页按钮 历史版本 查看历史版本记录
历史版本跳转方法
<li>
<a class="btn btn-info" onclick="getHistoryVersion();" shiro:hasPermission="project:publishCosFiles:history">
<i class="fa fa-history"></i> 历史版本
</a>
</li>
//查看当前目录的历史版本
function getHistoryVersion(){
$.modal.openTab('历史版本',prefix + "/getHistoryVersion?platform="+$('#platform').val()+"&title="+$("#title").val());
}
复制代码
controller 方法,接收页面传参跳转到历史版本列表页面
/**
* 获取目录历史版本页面
*/
@RequestMapping("/getHistoryVersion")
@RequiresPermissions("project:publishCosFiles:history")
public String getHistoryVersion(HttpServletRequest request) throws Exception {
String title = request.getParameter("title");
String platform = request.getParameter("platform");
request.setAttribute("title", title);
request.setAttribute("platform", platform);
return prefix + "/publishCosFileVersion";
}
复制代码
历史版本列表页面
历史版本列表页面代码 publishCosFileVersion.html
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro">
<head>
<th:block th:include="include :: header('文件版本列表')" />
<style>
.my-search-collapse {
width: 100%;
background: #fff;
border-radius: 6px;
margin-top: 10px;
padding-top: 10px;
padding-bottom: 6px;
box-shadow: 1px 1px 3px rgba(0,0,0,.2);
}
</style>
</head>
<body class="gray-bg">
<div class="container-div">
<div class="row">
<div class="col-sm-12 my-search-collapse">
<div>
<div style="color:red;">当前目录:[[${title}]]</div>
</div>
</div>
<div class="col-sm-12 search-collapse">
<form id="formId">
<div class="select-list">
<ul>
<input type="hidden" id="path" name="path" th:value="${title}">
<input type="hidden" id="platform" name="platform" th:value="${platform}">
<li>
文件名:<input type="text" name="name"/>
</li>
<li>
<input name="createUser" type="hidden" id="createUser"/>
操作人:<input id="createUserName" name="createUserName" readonly type="text" onclick="selectUserDeptTree('createUser','createUserName')" >
</li>
<li>
备注信息:<input type="text" name="remark"/>
</li>
<li class="input-daterange input-group">
修改时间:
<input type="text" name="createStartDate" readonly id="createStartDate"/>
至
<input type="text" name="createEndDate" readonly id="createEndDate"/>
</li>
<li>
<a class="btn btn-primary btn-rounded btn-sm" onclick="search1();"><i
class="fa fa-search"></i> 搜索</a>
<a class="btn btn-warning btn-rounded btn-sm" onclick="reset1();"><i
class="fa fa-refresh"></i> 重置</a>
</li>
<li style="float: right">
<button style="width: 40px;" class="btn btn-default btn-outline" type="button" onclick="search1();" name="refresh" aria-label="刷新" title="刷新"><i class="glyphicon glyphicon-refresh icon-refresh"></i> </button>
</li>
</ul>
</div>
</form>
</div>
<div class="col-sm-12 select-table table-striped">
<table id="bootstrap-table" data-mobile-responsive="true"></table>
</div>
</div>
</div>
<div th:include="include :: footer"></div>
<script th:inline="javascript">
var viewImgFlag = [[${@permission.hasPermi('project:publishCosFiles:showImage')}]];
var prefix = ctx + "project/publishFileVersion";
$(function() {
var options = {
url: prefix + "/cmsFileVersionList",
modalName: "文件版本",
showExport: false,
pagination: false,
showSearch: false,
showToggle: false,
showRefresh:false,
showColumns:false,
sortName:'name',
sortOrder:'asc',
columns: [
{
title: '',
align: 'center',
formatter: function(value, row, index) {
var actions = [];
//所有的加版本号
actions.push('<a class="" href="javascript:" onclick="addChildHtml(\'' + row.name + '\', this,'+row.id+' )"><i class="glyphicon glyphicon-plus icon-plus"></i></a>');
return actions.join('');
}
},
{
checkbox: true
},
{
field : 'id',
title : '主键id',
visible: false
},
{
field : 'name',
title : '文件名',
formatter: function(value, row, index) {
var showname = value;
if (value.length > 15) {
showname = value.substr(0,14)+"...";
}
var html = "<div title='"+value+"'>"+showname+"</div>";
return html;
},
sortable:true
},
{
field : 'versionNum',
title : '版本号',
},
{
field : 'createDate',
title : '修改时间',
sortable:true
},
{
field : 'size',
title : '文件大小',
formatter: function(value, row, index) {
if(row.delFlag === 1){
// 删除的时候大小展示已删除
return "已删除";
}
var fileSize = row.size;
if (fileSize != null) {
var number1024 = 1<<10;
var number1048576 = 1<<20;
var number1073741824 = 1<<30;
if (fileSize > number1024) {
if (fileSize > number1048576) {
if (fileSize > number1073741824) {
var fixed = (fileSize/number1073741824).toFixed(2);
return fixed+"GB";
}else {
var fixed = (fileSize/number1048576).toFixed(2);
return fixed+"MB";
}
}else {
var fixed = (fileSize/number1024).toFixed(2);
return fixed+"KB";
}
}else {
return fileSize+"B";
}
}else {
return "-";
}
}
},
{
field : 'createUserName',
title : '修改人',
},
{
field : 'remark',
title : '备注信息',
formatter: function(value, row, index) {
return $.table.tooltip(value);
}
},
{
title: '操作',
align: 'left',
cellStyle: function (value, row, index) {
return {css: {"overflow": "hidden", "text-overflow": "ellipsis", "white-space": "nowrap"}}
},
formatter: function(value, row, index) {
var actions = [];
var fileType = row.type;
if (fileType != null
&& ("jpg" == fileType.toLowerCase() || "png" == fileType.toLowerCase() || "gif" == fileType.toLowerCase() || "jpeg" == fileType.toLowerCase())) {
actions.push('<a class="btn btn-success btn-xs ' + viewImgFlag + '" href="javascript:void(0)" onclick="viewImage(\'' + row.bakPathPrefix +"/"+ row.bakName +'\')"><i class="fa fa-leaf"></i>预览</a> ');
}
return actions.join('');
}
}]
};
$.table.init(options);
initLaydate();
});
/*账户管理-新增-选择人部门树*/
function selectUserDeptTree(treeId,treeName) {
// url 可选参数, 其他参数 请自定扩展
// permission 权限 0.无权限 1.按照登录人数据权限
// showType 展示方式 0.人员 1.人员+工号
// checkType 选中类型 0.多选 1.单选
// showLevel 展示层级
var url = ctx + "system/userDeptTree?showType=1&showLevel=2&checkType=1";
var options = {
title: '选择部门',
width: "500",
url: url,
callBack: function (index, layero) {
// 是否允许选父级
var body = layer.getChildFrame('body', index);
$("#"+treeId).val(body.find('#ids').val());
$("#"+treeName).val(body.find('#names').val());
layer.close(index);
}
};
$.modal.openOptions(options);
}
function initLaydate() {
layui.use('laydate', function() {
var laydate = layui.laydate;
var startDate = laydate.render({
elem: '#createStartDate',
//max: $('#createEndDate').val(),
type: 'datetime',
theme: 'molv',
trigger: 'click'
/*done: function (value, date) {
// 结束时间大于开始时间
if (value !== '') {
endDate.config.min.year = date.year;
endDate.config.min.month = date.month - 1;
endDate.config.min.date = date.date;
} else {
endDate.config.min.year = '';
endDate.config.min.month = '';
endDate.config.min.date = '';
}
}*/
});
var endDate = laydate.render({
elem: '#createEndDate',
//max:$.common.getNowFormatDate(),
type: 'datetime',
theme: 'molv',
trigger: 'click'
/*done: function (value, date) {
// 开始时间小于结束时间
if (value !== '') {
startDate.config.max.year = date.year;
startDate.config.max.month = date.month - 1;
startDate.config.max.date = date.date;
} else {
startDate.config.max.year = '';
startDate.config.max.month = '';
startDate.config.max.date = '';
}
}*/
});
})
}
//预览图片
function viewImage(filePath) {
var url = ctx + 'project/publishCosFiles/showImage?filePath=' + filePath;
$.modal.open("图片预览", url , '600', '500',function (index) {
layer.close(index);
});
}
//加载当前文件的历史版本
function addChildHtml(name,tdThis,id) {
// 判断是否是加号,加号的话展开并加载子类,如果是减号的话则删除掉子类
if ($(tdThis).children('i').hasClass('glyphicon-plus')) {
// 加载子类数据
var html = "";
var formData = {"path":$("#path").val(),"platform":$("#platform").val(),"name":name};
$.ajax({
url: prefix + "/getOneFileHistory",
type: 'post',
dataType: "json",
data: formData,
success: function(result) {
if(result.code == web_status.SUCCESS){
$.each(result.data,function (i,k) {
html += '<tr child-index="'+i+'" class="child-tr pid-'+id+'">';
html +='<td style="text-align: center; "/>';
html +='<td/>';
if(k.name==null){
html +='<td style="">-</td>';
}else {
var showname = k.name;
if (showname.length > 15) {
showname = showname.substr(0,14)+"...";
}
html += '<td title="'+k.name+'">'+showname+'</td>';
}
html +='<td style="">'+k.versionNum+'</td>';
html +='<td style="">'+k.createDate+'</td>';
var fileSize = k.size;
if (fileSize != null) {
var number1024 = 1<<10;
var number1048576 = 1<<20;
var number1073741824 = 1<<30;
if (fileSize > number1024) {
if (fileSize > number1048576) {
if (fileSize > number1073741824) {
var fixed = (fileSize/number1073741824).toFixed(2);
html +='<td style="">'+fixed+'GB</td>';
}else {
var fixed = (fileSize/number1048576).toFixed(2);
html +='<td style="">'+fixed+'MB</td>';
}
}else {
var fixed = (fileSize/number1024).toFixed(2);
html +='<td style="">'+fixed+'KB</td>';
}
}else {
html +='<td style="">'+fileSize+'B</td>';
}
}else {
html +='<td style="">-</td>';
}
html +='<td style="">'+k.createUserName+'</td>';
html +='<td style="">'+$.table.tooltip(k.remark);+'</td>';
html +='<td style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: left; ">';
var fileType = k.type;
if (fileType != null
&& ("jpg" == fileType.toLowerCase() || "png" == fileType.toLowerCase() || "gif" == fileType.toLowerCase() || "jpeg" == fileType.toLowerCase())) {
html +='<a class="btn btn-success btn-xs ' + viewImgFlag + '" href="javascript:void(0)" onclick="viewImage(\'' + k.bakPathPrefix +"/"+ k.bakName +'\')"><i class="fa fa-leaf"></i>预览</a> ';
}
if(i!=0){
//不是第一个的时候加还原
html +='<a class="btn btn-warning btn-xs " href="javascript:void(0)" onclick="rollbackFile('+k.id+')">';
html +='<i class="fa fa-mail-reply"/>还原</a> ';
}else {
// 是第一个的时候如果是删除的加还原
if(k.delFlag === 1){
html +='<a class="btn btn-warning btn-xs " href="javascript:void(0)" onclick="rollbackFile('+k.id+')">';
html +='<i class="fa fa-mail-reply"/>还原</a> ';
}
}
if (fileType != null && ("html" == fileType.toLowerCase() || "shtml" == fileType.toLowerCase() || "css" == fileType.toLowerCase() ||
"js" == fileType.toLowerCase()|| "txt" == fileType.toLowerCase())) {
html +='<a class="btn btn-info btn-xs " href="javascript:void(0)" onclick="comparedFile('+k.id+')">';
html +='<i class="fa fa-exchange"/>对比</a>';
}
html +='</td>';
html +='</tr>';
});
$(tdThis).parent().parent().after(html);
}else{
if("未登录或登录超时。请重新登录"==res.msg){
window.location=ctx+"login";
}else {
$.modal.alertError(result.msg);
}
}
}
});
// 写入之类成功之后在去掉加号等样式,防止出错
$(tdThis).children('i').removeClass('glyphicon-plus');
$(tdThis).children('i').removeClass('icon-plus');
$(tdThis).children('i').addClass('glyphicon-minus');
$(tdThis).children('i').addClass('icon-minus');
}else {
// 减号的时候点击将子类数据移除,然后将减号变加号
$('.pid-'+id+'').remove();
$(tdThis).children('i').removeClass('glyphicon-minus');
$(tdThis).children('i').removeClass('icon-minus');
$(tdThis).children('i').addClass('glyphicon-plus');
$(tdThis).children('i').addClass('icon-plus');
}
}
//还原文件的方法
function rollbackFile(id){
$.modal.confirm("确认要还原该文件吗?", function() {
var url = prefix + "/rollbackFile";
var data = { "id": id };
$.operate.submit(url, "post", "json", data,function () {
$.table.search();
});
});
}
//对比选择
function comparedFile(id){
$.modal.openTab('对比选择',prefix + "/selectCompared/?id="+id);
}
function search1() {
var start = $("#createStartDate").val();
var end = $("#createEndDate").val();
var startdate = new Date(start);
var enddate = new Date(end);
if (startdate.getTime() > enddate.getTime()) {
$.modal.alertError("开始时间不能大于结束时间");
return false;
}
$.table.search();
}
function reset1() {
$("#createUser").val("");
$.form.reset();
}
</script>
</body>
</html>
复制代码
点击列表页之后跳转文件对比 controller 方法
@RequestMapping("/comparedThisFile")
public String comparedThisFile(HttpServletRequest request, ModelMap mmap)
{
String text1path = null;
String text2path = null;
//String htmlPath = null;
String platform = request.getParameter("platform");
try {
String leftId = request.getParameter("leftId");
String rightId = request.getParameter("rightId");
String profile = ConfigConstant.cosTempPath;
PublishFileVersion left = publishFileVersionService.selectPublishFileVersionById(Long.parseLong(leftId));
// 获取左边文件路径
String bakpath = left.getBakPathPrefix();
String bakname = left.getBakName();
//file.text_0
String filename = left.getName();
String downname = filename +"_"+left.getVersionNum();
if (filename.contains(".")) {
downname = filename.replace(".","_"+left.getVersionNum()+".");
}
if(left.getObfuscateFlag()==1){
// 混淆的时候文件名用混淆的那个名字
bakname = left.getObfuscateSourceName();
}
String leftkey = bakpath + bakname;
PublishFileVersion right = publishFileVersionService.selectPublishFileVersionById(Long.parseLong(rightId));
// 获取右边文件路径
String bakpath2 = right.getBakPathPrefix();
String bakname2 = right.getBakName();
//file.text_0
String filename2 = right.getName();
String downname2 = filename2 +"_"+right.getVersionNum();
if (filename2.contains(".")) {
downname2 = filename2.replace(".","_"+right.getVersionNum()+".");
}
if(right.getObfuscateFlag()==1){
// 混淆的时候文件名用混淆的那个名字
bakname2 = right.getObfuscateSourceName();
}
String rightkey = bakpath2 + bakname2;
if (Constants.Platform.COS.getValue().equals(Integer.valueOf(platform))) {
CosClientUtil cosClientUtil = new CosClientUtil();
try {
//下载到服务器
text1path = profile + downname;
cosClientUtil.download(leftkey, text1path);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
//下载到服务器
text2path = profile + downname2;
cosClientUtil.download(rightkey, text2path);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
text1path = bakpath+File.separator+bakname;
text2path = bakpath2+File.separator+bakname2;
}
List<String> diffString = DiffHandleUtils.diffString(text1path,text2path,downname,downname2);
//System.out.println("======diff::::::"+diffString);
//在服务器生成一个diff.html文件,打开便可看到两个文件的对比
//htmlPath = profile + "compareresult.html";
String string = DiffHandleUtils.generateDiffString(diffString);
//System.out.println("======res::::::"+string);
mmap.put("right", string);
}catch (Exception e) {
e.printStackTrace();
}finally {
if (Constants.Platform.COS.getValue().equals(Integer.valueOf(platform))) {
//删除下载的临时文件
if (StringUtils.isNotEmpty(text1path)) {
boolean delete = new File(text1path).delete();
}
if (StringUtils.isNotEmpty(text2path)) {
boolean delete = new File(text2path).delete();
}
}
}
return prefix+"/newComparedFile";
}
复制代码
这里参与对比的文件都是前面业务操作已经上传到腾讯云服务上面的内容,所以在对比时需要先临时拉下来一版放在本地临时文件夹中,然后读取文件内容进行对比。
项目核心文件对比工具类方法 DiffHandleUtils.java
package com.dongao.project.utils;
import com.github.difflib.UnifiedDiffUtils;
import com.github.difflib.patch.Patch;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @Author zhangw
* @Date 2022/4/21 18:04
* @Version 1.0
*/
public class DiffHandleUtils {
/**
* 对比两文件的差异,返回原始文件+diff格式
*
* @param original 原文件内容
* @param revised 对比文件内容
*/
public static List<String> diffString(List<String> original, List<String> revised) {
return diffString(original, revised, null, null);
}
/**
* 对比两文件的差异,返回原始文件+diff格式
*
* @param original 原文件内容
* @param revised 对比文件内容
* @param originalFileName 原始文件名
* @param revisedFileName 对比文件名
*/
public static List<String> diffString(List<String> original, List<String> revised, String originalFileName, String revisedFileName) {
originalFileName = originalFileName == null ? "原始文件" : originalFileName;
revisedFileName = revisedFileName == null ? "对比文件" : revisedFileName;
//两文件的不同点
Patch<String> patch = com.github.difflib.DiffUtils.diff(original, revised);
//生成统一的差异格式
List<String> unifiedDiff = UnifiedDiffUtils.generateUnifiedDiff(originalFileName, revisedFileName, original, patch, 0);
if (unifiedDiff.size() == 0) {
//如果两文件没差异则插入如下
unifiedDiff.add("--- " + originalFileName);
unifiedDiff.add("+++ " + revisedFileName);
unifiedDiff.add("@@ -0,0 +0,0 @@");
} else if (unifiedDiff.size() >= 3 && !unifiedDiff.get(2).contains("@@ -1,")) {
//如果第一行没变化则插入@@ -0,0 +0,0 @@
unifiedDiff.add(2, "@@ -0,0 +0,0 @@");
}
//原始文件中每行前加空格
List<String> original1 = original.stream().map(v -> " " + v).collect(Collectors.toList());
//差异格式插入到原始文件中
return insertOrig(original1, unifiedDiff);
}
/**
* 对比两文件的差异,返回原始文件+diff格式
*
* @param filePathOriginal 原文件路径
* @param filePathRevised 对比文件路径
*/
public static List<String> diffString(String filePathOriginal, String filePathRevised,String originalName,
String revisedName) {
//原始文件
List<String> original = null;
//对比文件
List<String> revised = null;
File originalFile = new File(filePathOriginal);
File revisedFile = new File(filePathRevised);
try {
original = Files.readAllLines(originalFile.toPath());
revised = Files.readAllLines(revisedFile.toPath());
} catch (IOException e) {
e.printStackTrace();
}
return diffString(original, revised, originalName, revisedName);
}
/**
* 通过两文件的差异diff生成 html文件,打开此 html文件便可看到文件对比的明细内容
*
* @param diffString 调用上面 diffString方法获取到的对比结果
* @param htmlPath 生成的html路径,如:/user/var/mbos/ent/21231/diff.html
* HTML输出接受一个Javascript对象,该对象可能有以下配置项:
*
* inputFormat: 输入数据的格式: 'diff' 或者 'json', 默认是'diff'
* outputFormat: 输出数据的格式: 'line-by-line' 或者 'side-by-side', 默认是'line-by-line'
* showFiles: 在对比之前查看文件列表,true 或者false,默认是false
* matching: 匹配level: 'lines'用于匹配行, 'words' 用于匹配行和单词,或者设置为'none',默认为none
* matchWordsThreshold: 单词相似度下限, 默认是0.25
* matchingMaxComparisons: 为了匹配一组变化最多执行的比较次数,默认是2500
* maxLineLengthHighlight: 如果行数小于此值,则仅仅执行差异突出显示,默认是10000
* templates: 使用预备好的编译的模板替换部分html的对象。
* rawTemplates: 具有原始未编译模板的对象替换部分html。
* 更多参考 https://github.com/rtfpessoa/diff2html/tree/master/src/templates
*/
public static void generateDiffHtml(List<String> diffString, String htmlPath) {
StringBuilder builder = new StringBuilder();
for (String line : diffString) {
builder.append(line);
builder.append("\n");
}
String template = "<!DOCTYPE html>\n" +
"<html lang=\"en-us\">\n" +
" <head>\n" +
" <meta charset=\"utf-8\" />\n" +
" <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.7.1/styles/github.min.css\" />\n" +
" <link rel=\"stylesheet\" type=\"text/css\" href=\"https://cdn.jsdelivr.net/npm/diff2html/bundles/css/diff2html.min.css\" />\n" +
" <script type=\"text/javascript\" src=\"https://cdn.jsdelivr.net/npm/diff2html/bundles/js/diff2html-ui.min.js\"></script>\n" +
" </head>\n" +
" <script>\n" +
" const diffString = `\n" +
"temp\n" +
"`;\n" +
"\n" +
"\n" +
" document.addEventListener('DOMContentLoaded', function () {\n" +
" var targetElement = document.getElementById('diffElement');\n" +
" var configuration = {\n" +
" drawFileList: true,\n" +
" fileListToggle: false,\n" +
" fileListStartVisible: false,\n" +
" fileContentToggle: false,\n" +
" matching: 'words',\n" +
" outputFormat: 'side-by-side',\n" +
" synchronisedScroll: true,\n" +
" highlight: true,\n" +
" renderNothingWhenEmpty: true,\n" +
" };\n" +
" var diff2htmlUi = new Diff2HtmlUI(targetElement, diffString, configuration);\n" +
" diff2htmlUi.draw();\n" +
" diff2htmlUi.highlightCode();\n" +
" });\n" +
" </script>\n" +
" <body>\n" +
" <div id=\"diffElement\"></div>\n" +
" </body>\n" +
"</html>";
String string = builder.toString();
string = string.replace("/", "\\/");
template = template.replace("temp", string);
FileWriter f = null; //文件读取为字符流
try {
f = new FileWriter(htmlPath);
BufferedWriter buf = new BufferedWriter(f); //文件加入缓冲区
buf.write(template); //向缓冲区写入
buf.close(); //关闭缓冲区并将信息写入文件
f.close();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 通过两文件的差异diff生成 string,打开此 html文件便可看到文件对比的明细内容
* @param diffString
* @return
*/
public static String generateDiffString(List<String> diffString) {
StringBuilder builder = new StringBuilder();
builder.append("`");
for (String line : diffString) {
//对比页面开始符号冲突
if (line.contains("`")) {
line = line.replace("`","\\`");
}
//</script>结束标志</冲突
if (line.contains("</")) {
line = line.replace("</", "<\\/");
}
//正则\冲突
if (line.contains("\\") && line.contains("^")) {
line = line.replace("\\", "\\\\");
}
//页面取值冲突 ${}
if (line.contains("${")) {
line = line.replace("${", "$\\{");
}
builder.append(line);
builder.append("\n");
}
builder.append("`");
String string = builder.toString();
return string;
}
//统一差异格式插入到原始文件
public static List<String> insertOrig(List<String> original, List<String> unifiedDiff) {
List<String> result = new ArrayList<>();
//unifiedDiff中根据@@分割成不同行,然后加入到diffList中
List<List<String>> diffList = new ArrayList<>();
List<String> d = new ArrayList<>();
for (int i = 0; i < unifiedDiff.size(); i++) {
String u = unifiedDiff.get(i);
if (u.startsWith("@@") && !"@@ -0,0 +0,0 @@".equals(u) && !u.contains("@@ -1,")) {
List<String> twoList = new ArrayList<>();
twoList.addAll(d);
diffList.add(twoList);
d.clear();
d.add(u);
continue;
}
if (i == unifiedDiff.size() - 1) {
d.add(u);
List<String> twoList = new ArrayList<>();
twoList.addAll(d);
diffList.add(twoList);
d.clear();
break;
}
d.add(u);
}
//将diffList和原始文件original插入到result,返回result
for (int i = 0; i < diffList.size(); i++) {
List<String> diff = diffList.get(i);
List<String> nexDiff = i == diffList.size() - 1 ? null : diffList.get(i + 1);
//含有@@的一行
String simb = i == 0 ? diff.get(2) : diff.get(0);
String nexSimb = nexDiff == null ? null : nexDiff.get(0);
//插入到result
insert(result, diff);
//解析含有@@的行,得到原文件从第几行开始改变,改变了多少(即增加和减少的行)
Map<String, Integer> map = getRowMap(simb);
if (null != nexSimb) {
Map<String, Integer> nexMap = getRowMap(nexSimb);
int start = 0;
if (map.get("orgRow") != 0) {
start = map.get("orgRow") + map.get("orgDel") - 1;
}
int end = nexMap.get("revRow") - 2;
//插入不变的
insert(result, getOrigList(original, start, end));
}
if (simb.contains("@@ -1,") && null == nexSimb) {
insert(result, getOrigList(original, 0, original.size() - 1));
} else if (null == nexSimb && map.get("orgRow") < original.size()) {
insert(result, getOrigList(original, map.get("orgRow"), original.size() - 1));
}
}
return result;
}
//将源文件中没变的内容插入result
public static void insert(List<String> result, List<String> noChangeContent) {
for (String ins : noChangeContent) {
result.add(ins);
}
}
//解析含有@@的行得到修改的行号删除或新增了几行
public static Map<String, Integer> getRowMap(String str) {
Map<String, Integer> map = new HashMap<>();
if (str.startsWith("@@")) {
String[] sp = str.split(" ");
String org = sp[1];
String[] orgSp = org.split(",");
//源文件要删除行的行号
map.put("orgRow", Integer.valueOf(orgSp[0].substring(1)));
//源文件删除的行数
map.put("orgDel", Integer.valueOf(orgSp[1]));
String[] revSp = org.split(",");
//对比文件要增加行的行号
map.put("revRow", Integer.valueOf(revSp[0].substring(1)));
map.put("revAdd", Integer.valueOf(revSp[1]));
}
return map;
}
//从原文件中获取指定的部分行
public static List<String> getOrigList(List<String> original1, int start, int end) {
List<String> list = new ArrayList<>();
if (start <= end && end < original1.size()) {
for (; start <= end; start++) {
list.add(original1.get(start));
}
}
return list;
}
}
复制代码
返回页面的展示结果如开始时截图的效果,那么截图 newComparedFile.html 页面代码如下
<!DOCTYPE html>
<html lang="en-us">
<head>
<meta charset="utf-8" />
<link rel="stylesheet" type="text/css" th:href="@{/css/github.min.css}"/>
<link rel="stylesheet" type="text/css" th:href="@{/css/diff2html.min.css}"/>
<script type="text/javascript" th:src="@{/js/diff2html-ui.min.js}"></script>
<script type="text/javascript" th:src="@{/js/jquery.min.js}"></script>
</head>
<script>
const diffString = [(${right})];
document.addEventListener('DOMContentLoaded', function () {
var targetElement = document.getElementById('diffElement');
var configuration = {
//在diff之前显示文件列表:true或false,默认值为true
drawFileList: true,
//允许切换文件摘要列表:true或false,默认值为true
fileListToggle: false,
//选择文件摘要列表是否开始可见:true或false,默认值为false
fileListStartVisible: false,
//允许切换每个文件内容:true或false,默认值为true
fileContentToggle: false,
//匹配level: 'lines'用于匹配行, 'words' 用于匹配行和单词,或者设置为'none',默认为none
matching: 'words',
//输出数据的格式: 'line-by-line' 或者 'side-by-side', 默认是'line-by-line'
outputFormat: 'side-by-side',
synchronisedScroll: true,
highlight: true,
renderNothingWhenEmpty: true,
};
var diff2htmlUi = new Diff2HtmlUI(targetElement, diffString, configuration);
diff2htmlUi.draw();
diff2htmlUi.highlightCode();
var texts = $(".d2h-code-wrapper").find("td[class='d2h-info']").find("div");
if (texts.length) {
$.each(texts,function (index,value) {
var _this = $(this);
//console.log(_this.text())
_this.text("");
});
}
});
</script>
<body>
<div id="diffElement"></div>
</body>
</html>
复制代码
参考文献:https://github.com/rtfpessoa/diff2html#diff2htmlui-api
日常开发记录,欢迎指正
划线
评论
复制
发布于: 刚刚阅读数: 5
版权声明: 本文为 InfoQ 作者【六月的雨在infoQ】的原创文章。
原文链接:【http://xie.infoq.cn/article/88c597961faf5a209235dc5fd】。
本文遵守【CC-BY 4.0】协议,转载请保留原文出处及本版权声明。
六月的雨在infoQ
关注
让技术不再枯燥,让每一位技术人爱上技术 2022.07.22 加入
还未添加个人简介
评论