前言
这篇文章主要是对于公司近期对于git提交规范进行了限制之后产出,其主要目的是为了实现本公司的提交规范的实现,以及简单方便化个人操作,本文的代码是由 git-cz基础上进行的修改。注:本文会写上主要代码各位可以自行下载git-cz源码修改后创建自己的独特的git提交管理工具,也会附上一些api接口(如果有想要我来帮忙修改的也可以私聊我哦)
实现效果
这就是进行修改后的提交流程逻辑,如果在正常对于jira号和提交信息对应的情况下我们可以不用输入任何信息就可以完成提交,并且生成信息也是完全可以按照我们制定的规范来进行提交,所以这样做可以对于我们的提交信息进行管理并且还可以把我们的提交信息和jira号关联对应上,如下图:
这样其实就简单的实现我们这样做的最基本需求,就是规范提交信息让我们的jira和git提交对应起来,并且后期可以通过git log来生成我们需求的日志文件。但是,我既然都写到这了怎么不再把gitlab上的操作再省略一些呢:
是的,我们一次性完成了全部操作直接把gitlab上的mr请求都干掉了直接提交这样岂不美滋滋, 再也不用打开jira网页查看jira号和gitlab发送mr请求了!
具体实现
其实整体下来说有两种方式可以实现这种效果:
- 利用puppeteer自动化无头浏览器来完成
- 利用jira和gitlab的接口来完成
puppeteer
什么是puppeteerPuppeteer是一个Node库,它提供了高级API来通过DevTools协议控制Chrome或Chromium 。
Puppeteer 默认情况下无头运行,但可以配置为运行完整(无头)的Chrome或Chromium。
好吧其实这是官方的介绍,简单来说就是给我们提供了一个可以操控的浏览器,而我们的目的就是通过这个浏览器的dom来获取我们的信息
var PCR = require('puppeteer');
const browser = await PCR.launch({
// executablePath: this.pcr.executablePath,
headless: true,
// 设置超时时间
timeout: 120000,
// 如果是访问https页面 此属性会忽略https错误
ignoreHTTPSErrors: true,
// 打开开发者工具, 当此值为true时, headless总为false
devtools: true,
defaultViewport: {
width: 1900,
height: 900,
hasTouch: true,
isMobile: true,
deviceScaleFactor: 3
},
// 关闭headless模式, 不会打开浏览器
// headless: enableChromeDebug !== 'Y',
args: ['--no-sandbox']
});
let json = {};
try {
json = fs.readFileSync(_path.default.resolve(__dirname + '/../../cz-cli-git.json'), 'utf8');
json = JSON.parse(json);
} catch (e) {
console.log(e);
}
if (!json.username || !json.password) {
const getMessage = await _inquirer.default.prompt([{
type: 'input',
name: 'username',
message: '请输入您的账号'
}, {
type: 'input',
name: 'password',
message: '请输入您的密码'
}]);
json = {
username: getMessage.username,
password: getMessage.password
};
fs.writeFileSync(_path.default.resolve(__dirname + '/../../cz-cli-git.json'), JSON.stringify(json));
}
let loading = ora();
loading.start(`正在获取分支信息`);
const page = await browser.newPage();
await page.goto('公司的jira地址/users/sign_in');
await page.waitFor(1000);
const elUsername = await page.$('#user_login');
const elPassword = await page.$('#user_password');
const elSubmit = await page.$('.move-submit-down');
await elUsername.type(json.username);
await elPassword.type(json.password);
await elSubmit.click();
await page.waitFor(1000);
await page.goto(gitUrl + '/merge_requests/new');
await page.waitFor(1000);
const sourceBranch = await page.$('.js-source-branch');
const targetBranch = await page.$('.js-target-branch');
await sourceBranch.click();
await page.waitFor(1000);
const AllBranch = await page.$$eval('.js-source-branch-dropdown a', el => el.map(x => {
return {
name: x.innerText,
value: x.getAttribute('data-ref')
};
}));
loading.succeed('分支信息获取成功');
console.log(AllBranch);
看着这么长一串的await其实就是获取dom节点和实现dom操作而已,这样我们就能获取到AllBranch了(当然我们公司的jira版本号是7.3.8)
虽然这样就可以获取到我们的jira信息了,但是有以下几个问题
- 整个npm库较重,应为利用了无头浏览器,所以npm会给你下载一个Chromium自动化测试浏览器(大约134MB)
- 安装npm的时候很慢很慢,由于有这么大一个浏览器下载经常出现下载失败和下载很慢的问题
- 使用的时候获取效率较慢,由于是采用模拟浏览器操作所以有页面加载时间需要进行等待
这个方案代码写起来简单轻松但是问题还是挺多的,但是如果需求不高这样操作也不失是一个办法,因为逻辑上来说这样可以代替你的所有操作,并且不用担心接口权限等问题
接口请求
这个字眼看着就是简单粗暴的做法了,看起来很简单其实并没有,最关键的问题在于:找接口!
是的,对于一个不熟悉jira和gitlab接口的人来说(本菜鸡),官方提供的一大片接口简直辣眼睛,jira上还没有官方提供的都是去页面上自己试出来的。那么这样的作法的优点当然也很明显了:
- npm包较轻,无浏览器安装
- 相应速度块,可以快速获取接口信息展示
- 异常捕获容易,可以较为轻松的实现异常情况的处理
如果能够使用接口请求的情况下我觉得还是尽量使用接口来提高使用质量和异常处理,当然如果有接口无法完成的任务当我没说。关键说一个对于gitlab的访问令牌的说明的,由于gitlab的安全验证比较复杂所以使用了简单的访问令牌来实现,具体如果生成令牌可以点击这个生成令牌查看,并且所有接口都需要在头部添加
jira就不展示代码了毕竟只有一个接口,这里就把gitlab代码展示出来吧(jira版本: 7.3.8,gitlab好像没太大版本之分)
获取未完成jira列表:
url: /rest/issueNav/1/issueTable
type: post
params: {
layoutKey: 'split-view', // 固定填写
jql: 'assignee = currentUser() AND resolution = Unresolved order by updated DESC', // 固定填写
os_username: json.username, // 用户账号
os_password: json.password // 用户密码
}
const reg = /http(.*?)\s/;
const git = await execSync( 'git remote -v' ).toString().trim(); //姓名
const gitUrl = git.match( reg )[ 0 ].replace( /\s/g , '' ).replace('https://域名地址', 'http://ip地址').replace('.git', '');
let response
try {
response = await axios.get( 'https://域名地址/api/v4/projects' , {
params : {
search : gitUrl.split('/')[gitUrl.split('/').length - 1]
} ,
headers : {
'Authorization' : `Bearer ${json.Authorization}` ,
'Content-Type' : 'application/x-www-form-urlencoded'
}
} );
} catch ( e ) {
throw Error('获取项目信息失败')
}
response = response.data.filter( ( item ) => item.web_url === gitUrl );
if ( response.length > 1 ) {
throw Error('项目管理有问题,请联系jira项目管理员');
}
const projectId = response[0].id;
let forksRes;
try {
forksRes = await axios.get( `https://域名地址/api/v4/projects/${projectId}/repository/branches` , {
headers : {
'Authorization' : `Bearer ${json.Authorization}` ,
'Content-Type' : 'application/x-www-form-urlencoded'
}
} );
} catch ( e ) {
throw Error('请求分支失败请重试')
}
const branchList = forksRes.data.map(item => {
return {
name: item.name,
value: item.name
}
})
let menberRes;
try {
menberRes = await axios.get( `https://域名地址/api/v4/projects/${projectId}/members/all` , {
headers : {
'Authorization' : `Bearer ${json.Authorization}` ,
'Content-Type' : 'application/x-www-form-urlencoded'
}
} );
} catch ( e ) {
throw Error('获取检查用户失败请重试')
}
const menberList = menberRes.data.map(item => {
return {
name: item.name,
value: item.id
}
})
loading.succeed(`填写信息获取成功`);
console.warn('当前分支为:' + await execSync( 'git name-rev --name-only HEAD' ).toString().trim() + ' (为确保操作准确性,只允许在当前分支发起mr请求)');
const getInputB = await inquirer
.prompt([
{
type: 'input',
name: 'branchIn',
message: '输入合并入的主分支如果想手动选择可以直接回车进入下一步',
}
])
const proptList = [{
type: 'list',
name: 'branch',
message: '请选择合入的主分支',
choices: branchList
},
{
type: 'input',
name: 'title',
message: '请输入合并标题',
default: '默认合并请求信息'
},
{
type: 'input',
name: 'desc',
message: '请输入合并描述'
},
{
type: 'list',
name: 'users',
message: '选择合并用户',
choices: menberList
}]
getInputB.branchIn && proptList.splice(0, 1)
const getBranch = await inquirer
.prompt(proptList)
try {
let mergeRes = await axios.post( `https://域名地址/api/v4/projects/${projectId}/merge_requests` , qs.stringify({
source_branch: await execSync( 'git name-rev --name-only HEAD' ).toString().trim(),
target_branch: getInputB.branchIn ? getInputB.branchIn : getBranch.branch,
title: getBranch.title,
assignee_id: getBranch.users,
description: getBranch.desc,
remove_source_branch: true
}), {
headers : {
'Authorization' : `Bearer ${json.Authorization}` ,
'Content-Type' : 'application/x-www-form-urlencoded'
}
} );
if (mergeRes.status === 201) {
console.log('创建成功');
console.log('访问地址:' + mergeRes.data.web_url);
} else {
console.log('请检查分支选择和标题不能为空');
}
} catch ( e ) {
throw Error('提交失败,请检查分支选择和标题不能为空,或者线上已经存在一样合并请求了无需再提交');
}
其他问题
账号管理
如果看了上面的代码其实有个东西就是账号管理没有看到,这个地方我使用的是第一次填写后将账号密码保存至文件种,后面再访问就通过文件获取
let json = {};
try {
json = fs.readFileSync(path.resolve(__dirname + '/cz-cli.json'), 'utf8')
json = JSON.parse(json);
} catch ( e ) {
// console.log(e);
}
if (!json.username || !json.password) {
const getMessage = await inquirer
.prompt([
{
type: 'input',
name: 'username',
message: '请输入您的账号'
},
{
type: 'input',
name: 'password',
message: '请输入您的密码'
}
])
json = {
username: getMessage.username,
password: getMessage.password
}
fs.writeFileSync(path.resolve(__dirname + '/cz-cli.json'), JSON.stringify(json))
}
这个代码其实很简单就不再做描述了。
git cz
其实说实话这个才是最关键的东西,但是我觉得既然都看到这了大佬可能都不缺这点实力来读读git-cz的源码了,我就简单说一下我们应该怎么修改让我们的功能添加上
首先这个基本上就是git cz执行的基础逻辑了,真正在代码中的反复方法调用,方法传递较深所以实际看起来代码没有那么简单明了,但是也可以说的是在代码中的获取提问信息是通过require第三方包然后加载实现的(同步获取!),是的他是属于同步获取,所以我们的接口请求无法等待,那么就要我们进行一点点的魔改了
第一处
#!src/commitizen/adapter.js
// 142行原本代码
function getPrompter (adapterPath) {
// Resolve the adapter path
let resolvedAdapterPath = resolveAdapterPath(adapterPath);
// Load the adapter
let adapter = require(resolvedAdapterPath);
/* istanbul ignore next */
if (adapter && adapter.prompter && isFunction(adapter.prompter)) {
return adapter.prompter;
} else if (adapter && adapter.default && adapter.default.prompter && isFunction(adapter.default.prompter)) {
return adapter.default.prompter;
} else {
throw new Error(`Could not find prompter method in the provided adapter module: ${adapterPath}`);
}
}
//修改为
function getPrompter (adapterPath) {
// Resolve the adapter path
return new Promise(async (resolve) => {
let resolvedAdapterPath = resolveAdapterPath(adapterPath);
// Load the adapter
let adapter = await require(resolvedAdapterPath);
/* istanbul ignore next */
if (adapter && adapter.prompter && isFunction(adapter.prompter)) {
resolve(adapter.prompter);
} else if (adapter && adapter.default && adapter.default.prompter && isFunction(adapter.default.prompter)) {
resolve(adapter.default.prompter);
} else {
throw new Error(`Could not find prompter method in the provided adapter module: ${adapterPath}`);
}
})
}
我们将原本的直接执行代码给改成了异步执行返回Primose回调了,然后只需要进行下面一步就可以把获取改成异步了
#!src/commitizen/adapter.js
// 将第43行的代码
let prompter = getPrompter(adapterConfig.path);
// 修改为(并且将方法改为async就可以了)
let prompter = await getPrompter(adapterConfig.path);
这样我们就可以进行jira的异步获取了,那么对于处理完成后我们的操作呢?
在/src/strategies/git-cz.js文件中的第57行的commit方法执行中有一个done的回调方法,我们可以把后续操作添加到这个方法内执行就可以了
commit(inquirer, process.cwd(), prompter, {
args: parsedGitCzArgs,
disableAppendPaths: true,
emitData: true,
quiet: false,
retryLastCommit,
hookMode
}, function (error) { // 就是这个方法
if (error) {
throw error;
}
});
最后一步
其实到这了基本上看了整篇文章就可以实现你想自主修改的git cz了,然后我们添加一个git mr指令来让我们在没有执行提交的时候也可以进行gitlab的merge request请求,在更目录下的bin文件夹中添加文件然后再package.jso中配置以下即可(其实这就是增加指令就是创建脚手架的,如果不熟悉脚手架可以查看我的历史文章中有一篇关于脚手架的实战)
// package.json
"bin": {
"git-cz": "./bin/git-cz",
"git-mr": "./bin/git-mr",
"commitizen": "./bin/commitizen"
},
//git-mr
#!/usr/bin/env node
require('./git-mr.js');
//git-mr.js
process.on('uncaughtException', function (err) {
console.error(err.message || err);
process.exit(1);
})
require('../dist/cli/git-cz.js').MrApi();
//git-mr.cmd
@node "%~dpn0" %*