「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战」
一、背景
不知道大家是不是和我有一样的习惯,有一个自己平时练习学习或者有idea记录的仓库,并且这个目录一直跟随自己,
并且已经把他弄到了 github
上。每次练手后,都需要保存,然后提交到 git 上,但是提交内容又不是很重要,但就是每次自己都要自己add/commit/push。
是的,我就是这样,我终于烦透了,所以我决定写个工具吧,专门用于快速提交。这个东西最初是个脚本,但是随着我来回换电脑,脚本的局限性有逐渐的漏出来了,没办法写个命令行工具吧,直接 npm install -g
岂不是美哉,每次换电脑安装个全局包就好了,说(其实已经说了很多了)干就干!!!
二、明确需求
- 添加到命令行,可以
npm install -g xxx
; - 在项目目录下设置
.env
文件,自定义 commit-msg 和默认的提交分支; - 检查目录下是否有
.git
目录,没有提示; - 检查
git
命令是否安装或者正确配置; - 检查当前分支是否是
.env
配置的分支; - 以上检查都通过以后自动执行
add/commit/push
命令;
三、储备知识
3.1 npm 账号
现在已经是 2022 年了,如果你还没有的话,请自行搜索 npm 账号注册
3.2 添加到命令行
这个倒是不难,只需要在这个项目的 package.json 文件的 bin 字段中注册好就可以了,package.json 的 bin 是二进制文件的意思,即可执行文件,可以简单粗暴的理解成这就注册成一个命令了。
bin 字段一般以一个对象的形式出现,其中 key 将来注册成命令名字,value 指向这个名字对应的可执行文件,示例:
{
"name": "fast-commit",
"bin": {
"rs7": "lib/index.js"
},
"productVersion": "1.2.10"
}
当 fast-commit
这个包被全局安装:npm install fast-commit -g
以后, rs7
将会注册到全局,这里大家就会了啊,就可以再你的 Terminal 或者 cmd 里运行 rs7
了,其实执行这个 rs7
命令,就是在执行 lib/index.js
这个可执行文件,等效于 node lib/index.js
。
3.3 .env 文件
.env
配置文件,即在你的项目中增加一个 .env 的文件,这里面写好了一些静态配置,这里我们的静态配置如下:
# .env file
# default fast commit branch
FC_BRANCH=master
#default commit msg, date-time is default
FC_COMMIT_MSG=date
将来可以在 process.env 上访问 FC_BRANCH 和 FC_COMMIT_MSG 两个属性;这又是咋实现的?这个东西是通过一个 dotenv 的 npm 包实现的,这里不多说啦;
3.4 执行命令
有的人可能已经想到 shelljs
了对吧,但是
这个能力 node.js
已经为我们提供好了,这需要使用到 child_process
这个模块来调用系统命令,主要用到 child_process.spawn
和 child_process.exec
方法,用 promise
封装一番就是一个字,爽!
const runCommand = (cmd, args, needReturn) => {
return new Promise((resolve, reject) => {
if (!needReturn) {
let executedCommand = cp.spawn(cmd, args, {
shell: true,
stdio: 'inherit'
});
executedCommand.on('error', reject);
executedCommand.on('exit', code => +code === 0 ? resolve(code) : reject(code));
} else {
cp.exec(cmd, args, (err, stdo, stde) => {
if (err && err.code !== 0) reject(err.code);
else resolve({ code: 0, data: stdo })
})
}
})
};
该方法主要用来调用系统命令,其中 needReturn
标识是否需要接收当前命令的一个返回结果,比如有些场景需要我们分析命令执行后的输出结果,就需要命令返回结果;
3.5 命令行好看的提示信息
这个能力是 chalk
这个 npm 包提供的能力,安装它吧,我想写这个包的程序员一定很浪漫,让你的命令行美颜过一样;
3.6 可执行文件
文件权限:r、w、x
,即读、写、执行,这个 x
是 excute
的缩写,标识这个文件可以被执行。我们创建的文件最终成为一个命令,所以它需要有一个可以被执行的权限,这里我们简单粗暴的:
$ chmod +x index.js
这表示给文件加上了可以执行的权限。如果不加这个 x
的权限,你需要 node ./index.js
这样调用,如果加了 x
并且配合文件 #!/usr/bin/env node
这个头,就可以 ./index.js
或注册 bin
命令的方式执行它;
3.7 本地调试
你需要一个好东西叫做 npm link
命令
四、代码正文
#!/usr/bin/env node
import chalk from 'chalk';
import * as readLine from 'readline';
import * as fs from 'fs';
import * as path from 'path';
import * as cp from 'child_process';
import 'dotenv/config';
// 获取 dot env 的配置详情
const { FC_BRANCH, FC_COMMIT_MSG = 'date-time' } = process.env
// 封装调用系统命令的方法
const runCommand = (cmd, args, needReturn) => {
return new Promise((resolve, reject) => {
if (!needReturn) {
let executedCommand = cp.spawn(cmd, args, {
shell: true,
stdio: 'inherit'
});
executedCommand.on('error', reject);
executedCommand.on('exit', code => +code === 0 ? resolve(code) : reject(code));
} else {
cp.exec(cmd, args, (err, stdo, stde) => {
if (err && err.code !== 0) reject(err.code);
else resolve({ code: 0, data: stdo })
})
}
})
};
// 这两个方法可以写一个交互式的命令行工具,即在命令行输出提示,然后读取命令行输入做出动作
// 但是我太懒了,如果写个这个还要和命令行对话,这和我一键提交的目的不一致,但是方法放这里了哈
const readLineInterface = readLine.createInterface({
input: process.stdin,
output: process.stderr
});
const promiseQuestion = q => {
return new Promise((resolve, reject) => {
readLineInterface.question(q, answer => {
if (answer.trim()) resolve(answer);
else promiseQuestion(q).then(resolve, reject)
})
})
};
// 因为全局安装,你需要知道你的脚步运行在哪个目录,我们只提交这个目录下的内容
const cwd = process.cwd();
// async 自执行函数为了方便使用 await,如果 node 有一天支持了全局的 await 就舒服了
// async iife for using await
(async () => {
// 检查 git 是否安装或者 git 命令被正确配置了
// check git is installed or .git dir existence
let gitStatus
let dotGitDir
try {
// 检查是否是一个 git 仓库,通过检查 .git 目录是否存在
dotGitDir = path.resolve(cwd, './.git')
await fs.promises.stat(dotGitDir)
console.log(chalk.green(`.git directory has been detected on path: ${dotGitDir}!!!`));
} catch (e) {
console.log(e);
console.log(chalk.bgRed('.git directory does not exist on:', cwd));
process.exit(2);
}
// console tips
console.log(chalk.yellow('we will commit all changes /r/n/r/n/r...'));
try {
// 检查 git 命令是否可以正常使用
await runCommand('git status');
} catch (e) {
console.error(chalk.bgRed('git installation check failed!'));
console.error(chalk.bgRed('please install git, or config "git" command correctly!!'));
process.exit(1);
}
// 读取配置,检测分支是否和配置分支一样
const reg = /\*\s([^\n]+)/g;
// 通过 git branch 然后结合 grep * 把当前分支抠出来
let curBranchData = await runCommand(`git branch | grep '*'`, null, 1);
let branch = reg.exec(curBranchData.data)[1]; // 用正则抠出来
if (branch !== FC_BRANCH) {
console.log(chalk.bgRed(`current branch is on ${branch}, please checkout to ${FC_BRANCH}`));
process.exit(6);
}
// 计算 commit-msg
let msg
switch (FC_COMMIT_MSG) {
case 'date':
msg = new Date().toLocaleDateString();
break;
case 'date-time':
msg = new Date().toLocaleTimeString();
break;
default:
msg = FC_COMMIT_MSG
}
// 执行 add commit push
try {
await runCommand('git add .');
await runCommand(`git commit -m"${msg}"`);
await runCommand(`git push origin ${branch || 'master'}`);
console.log(chalk.green('FAST-COMMIT HAS PUSH YOU COMMIT TO MASTER'));
process.exit(0);
} catch (e) {
if (/nothing to comit/igm.test(e)) {
process.exit(0)
} else {
console.log(chalk.bgRed('FAST-COMMIT ENCOUNTER SOME ACCIDENT FOR '));
process.exit(3)
}
console.error(e);
}
})();
五、最后
别看我说这么多,npm 包并没发出来,原因很简单,我忘记了我的 npm 密码; 但是,说了不灌水,这个东西是可以用的,上图: