来吧,撸一个快速 git 提交工具

2,403 阅读4分钟

「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战

4.png

一、背景

不知道大家是不是和我有一样的习惯,有一个自己平时练习学习或者有idea记录的仓库,并且这个目录一直跟随自己, 并且已经把他弄到了 github 上。每次练手后,都需要保存,然后提交到 git 上,但是提交内容又不是很重要,但就是每次自己都要自己add/commit/push。

是的,我就是这样,我终于烦透了,所以我决定写个工具吧,专门用于快速提交。这个东西最初是个脚本,但是随着我来回换电脑,脚本的局限性有逐渐的漏出来了,没办法写个命令行工具吧,直接 npm install -g 岂不是美哉,每次换电脑安装个全局包就好了,说(其实已经说了很多了)干就干!!!

二、明确需求

  1. 添加到命令行,可以 npm install -g xxx
  2. 在项目目录下设置 .env 文件,自定义 commit-msg 和默认的提交分支;
  3. 检查目录下是否有 .git 目录,没有提示;
  4. 检查 git 命令是否安装或者正确配置;
  5. 检查当前分支是否是 .env 配置的分支;
  6. 以上检查都通过以后自动执行 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.spawnchild_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,即读、写、执行,这个 xexcute 的缩写,标识这个文件可以被执行。我们创建的文件最终成为一个命令,所以它需要有一个可以被执行的权限,这里我们简单粗暴的:

$ 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 密码; 但是,说了不灌水,这个东西是可以用的,上图:

clipboard_image_1642510679843.png