最近在开发一个构建工具,需要操作版本管理系统,实现代码提交(如:svn add, svn commit…),GitHub上有类似的库,但存在两个问题:
基于我需要兼容git和svn,于是自己简单实现一个版本管理模块:version-control,下面将简单记录一下设计中的一些考虑和遇到的一些问题。
由于开发时间有限和本人使用场景较为简单,若你想使用version-control,请先自行评估是否能满足你的需求,当然作为开源项目,欢迎提issue,发PR。
回想我们在没有GUI版本管理工具的时候是怎么进行版本操作的:敲命令行,于是自然想到实现思路:基于node执行指定命令行来实现版本操作。
这里要注意的是,Windows下在命令行中不一定有git或者svn的执行环境,需要安装额外的安装包,由于Mac OS暂时没有这个问题,所以我还没有做处理。
// node执行命令
const child_process = require('child_process');
module.exports = (command, cwd) => new Promise((resolve, reject) => {
child_process.exec(command, { cwd }, (err, stdout) => {
if (err) reject(err);
else resolve(stdout);
});
});
凡是说到兼容,我就会想到抽象,站在用户的角度,要完成的任务是版本管理,并不太在意具体用哪些命令。于是我从svn和git操作中跳脱出来,只关心用户需要,针对用户需要,梳理出以下几个接口:
确定了抽象接口之后,我采用动态引入的方式兼容svn和git,并在架构上进行作用域隔离,单独维护svn操作逻辑和git操作逻辑:
VersionControl在实例化的时候判断用户需要svn还是git,根据用户需要引入底层实现,最后在对外接口中调用具体的底层实现来实现svn和git兼容。
/**
* VersionControl
* @TODO 实现Git状态管理
*/
class VersionControl {
constructor(versionControlSystemType) {
if (versionControlSystemType && !['svn', 'git'].contains(versionControlSystemType)) throw new Error('Only git or svn can be pass in.');
this.type = versionControlSystemType || 'svn';
// vsc = versionControlSystem
/* eslint global-require: "off" */
this.vcs = this.type === 'svn' ? require('./svn') : require('./git');
}
status(codePath) {
return this.vcs.status(codePath);
}
update(codePath) {
return this.vcs.update(codePath);
}
commit(codePath, commitMsg) {
return this.vcs.commit(codePath, commitMsg);
}
log(codePath) {
return this.vcs.log(codePath);
}
revert(codePath, commitMsg, version) {
return this.vcs.revert(codePath, commitMsg, version);
}
}
module.exports = VersionControl;
在执行svn commit -m '中文'
时,在某些node环境下(例如electron编译后的安装包中)会报错:
svn: Can't convert string from 'UTF-8' to native encoding
经过构建前后差异分析定位,我发现,electron构建前后执行时,node的环境不一样(具体环境参数可以通过process.env
获取,更多详情请参考:node.js文档 - process.env)。具体原因在于:编码格式不一样。
解决方案:将node执行环境中的语言改成UTF8:process.env.LANG = 'zh_CN.UTF-8';
在执行svn操作是,报错:svn: E155036: Please see the 'svn upgrade' command; svn: E155036: The working copy at 'xxx'
,意思是本地svn和服务器svn版本不一致,需要更新,此时执行svn upgrade
即可,但需要注意的是,svn upgrade
只能在svn根目录中执行,不然会报另外一个错误,于是我们需要在上面的报错信息中解析出svn root,也就是上面说的working copy。
部分代码如下,有很大的优化空间,例如这个错误处理逻辑,应该抽离出来当成通用的错误处理逻辑(因为所有的svn操作都有可能会报这个错误)。
static async status(codePath) {
const command = 'svn status';
let statusData = [];
await exec(command, codePath)
.then((data) => {
statusData = parseStatus(data);
})
.catch(async (error) => {
const { message } = error;
// 处理svn upgrade提示:在svn根目录(Working Copy Root Path)执行svn upgrade
// @NOTE 错误提示
// svn: E155036: Please see the 'svn upgrade' command
// svn: E155036: The working copy at 'xxx'
if (message.indexOf('svn upgrade') !== -1) {
const SVN_ROOT_REG = /The working copy at '(.*)'/;
const svnRoot = SVN_ROOT_REG.exec(message)['1'];
await this.upgrade(svnRoot);
return this.status(codePath);
}
return statusData;
});
return statusData;
}
我在svn文档中没有找到类似git add --all
的命令,于是在stackoverflow中找到了解决方法:
svn add --force * --auto-props --parents --depth infinity -q
刚开始在本地测试的时候测试通过,后台构建工具发布后,同事反馈commit的时候耗时很长(一分钟),于是我开始了问题定位之旅:
从上面命令的参数可以看到,执行这个命令会去深度遍历指定路径下的所有目录,导致耗时过长。
这其实是一个全量操作和增量操作的问题,全量操作方便,但耗费性能,增量操作繁琐,但性能优越。于是在全量操作遇到问题后,我只好自己手动实现svn add all:
static async addAll(codePath) {
const statusData = await this.status(codePath);
for (let i = 0; i < statusData.length; i += 1) {
const { rawType, path } = statusData[i];
/* eslint default-case: "off" */
/* eslint no-await-in-loop: "off" */
switch (rawType) {
case '?':
// svn add xxx
await this.add(codePath, path);
break;
case '!':
// svn delete xxx
await this.delete(codePath, path);
break;
}
}
在使用svn commit
之后,执行svn log
并没有看到刚刚提交的记录,查阅资料发现,需要在commit后执行svn update
才会看到记录,这属于svn的特性吧,简单记录一下。
上面只是提到一些实现思路和解决的一些问题,细节欢迎大家阅读version-control源码。此外大家会发现里面有许多不完善的地方,但目前已经能满足我的需求,后续随着需求的变更,我也继续更新,也欢迎大家issue,pr走起。下面是一些要做的事情:
博客写得比较简单,更像是工作笔记。有一段时间没有写博客了,因为**陷入了一个怪圈,觉得小的东西没必要写,大的东西又还没有成果不好写。**好在同事建议说:“就当做工作笔记好了,不用太认真”。想想也是,起码一直在记录,也能促进自己沉淀。