TLDR;
介绍 Git 钩子的基本开发流程
介绍如何用 Node.js 写 Git 钩子
Hooks-钩子
简介
Git 钩子是指在特定的 Git 动作(如:git commit
、 git push
)下被触发的脚本。而钩子主要被分为两种:
客户端钩子
服务端钩子
而客户端钩子又被分为以下几种:
类型 | 钩子名称 | 接收参数 | 可否终止操作 |
---|---|---|---|
提交工作流钩子 | pre-commit | \ | 是 |
提交工作流钩子 | prepare-commit-msg | filepath、committype、sha-1 | \ |
提交工作流钩子 | commit-msg | filepath | 是 |
提交工作流钩子 | post-commit | \ | \ |
电子邮件工作流钩子 | applypatch-msg | merge-filename | 是 |
电子邮件工作流钩子 | pre-applypatch | \ | 是 |
电子邮件工作流钩子 | post-commit | \ | 否 |
其它客户端钩子 | pre-rebase | \ | 是 |
其它客户端钩子 | post-rewrite、post-checkout 和 post-merge | commandname | \ |
其它客户端钩子 | pre-push | originbranhname & head | 是 |
服务器端钩子主要有三种:
钩子名称 | 接收参数 | 可否终止操作 |
---|---|---|
pre-receive | 推送的引用 | 是 |
update | 引用的名字(分支),推送前的引用指向的内容的 SHA-1 值,以及用户准备推送的内容的 SHA-1 值 | 是 |
post-receive | 同pre-receive | 否 |
客户端钩子和服务端钩子的异同
Git 的钩子不管客户端钩子还是服务端钩子,都是放在当前项目的 .git/hooks
目录下。不同的是,客户端钩子是放置在你的本地项目的目录下,而服务器端钩子是放在对应的服务器上的目录。
我们知道 Git 相当于本地的文件数据库,而 .git
目录存放了项目文件的快照以及其他一系列 git 信息,且 .git
目录是不会被提交到服务器上的,所以放置在 .git/hooks
目录中的客户端脚本也不会被提交。所以如果想让项目中的其他人使用你的钩子,就需要一种策略来偷偷的安装这个钩子或者在服务端放置实现这个钩子的功能。
如何用 Nodejs 写一个钩子
钩子都被存储在 Git 目录下的 hooks 子目录中。 也即绝大部分项目中的 .git/hooks 。 当你用 git init 初始化一个新版本库时,Git 默认会在这个目录中放置一些示例脚本。这些脚本除了本身可以被调用外,它们还透露了被触发时所传入的参数。 所有的示例都是 shell 脚本,其中一些还混杂了 Perl 代码,不过,任何正确命名的可执行脚本都可以正常使用 —— 你可以用 Ruby 或 Python,或其它语言编写它们。 这些示例的名字都是以 .sample 结尾,如果你想启用它们,得先移除这个后缀。
把一个正确命名且可执行的文件放入 Git 目录下的 hooks 子目录中,即可激活该钩子脚本。
之后我会用 Node.js 来写一个拒绝提交没有被解决的冲突的文件
的钩子。
需要的知识储备:
会写 Javascript
了解一点环境变量的知识
了解 Nodejs require 路径规则
写这个钩子的初衷是因为在多人合作项目中,总是难免会遇到文件冲突的情况,而有些同事没有找到全部的冲突文件并一一解决,这个钩子就会在 commit 的时候检查是否有冲突,如果有冲突,就会把所有冲突找到,并提示出错文件后,拒绝 commit。
直接上源码:
#!/usr/bin/env node
// 在 commit 之前检查是否有冲突,如果有冲突就 process.exit(1)
const execSync = require('child_process').execSync
// git 对所有冲突的地方都会生成下面这种格式的信息,所以写个检测冲突文件的正则
const isConflictRegular = "^<<<<<<<\\s|^=======$|^>>>>>>>\\s"
let results
try {
// git grep 命令会执行 perl 的正则匹配所有满足冲突条件的文件
results = execSync(`git grep -n -P "${isConflictRegular}"`, {encoding: 'utf-8'})
} catch (e) {
console.log('没有发现冲突,等待 commit')
process.exit(0)
}
if(results) {
console.error('发现冲突,请解决后再提交,冲突文件:')
console.error(results.trim())
process.exit(1)
}
process.exit(0)
把这个文件拷贝到 .git/hooks/pre-commit
下,并执行 chmod 777 pre-commit
就可以在每次 commit 的情况下检查之前文件是否有冲突。
有没有更好的做法?
试想一下,我们在写钩子的时候,并不会一次就把代码写对,所以需要经常把这个文件拷贝到 .git/hooks
目录下;有没有更好的做法? 有的。只需要在.git/hooks
下面创建一个 shell 脚本,来调用这个 js 文件即可。
#!/usr/bin/env node
const execSync = require('child_process').execSync
execSync("./pre-commit.js" )
这中 shebang
写法在使用 git 的命令来运行的时候是没有问题的,但是在使用 Source Tree
的 Git-GUI,会报 node 命令不存在, 这是新版本的 osx 的安全策略造成的(可以运行 which node 命令看看和上面的 shebang 有什么区别),对于这种情况使用下面的脚本可以完美解决。
#!/usr/bin/env bash
# 支持 sourcetree
export PATH=/usr/local/bin:$PATH
node "./pre-commit.js"
NOTE:
注意 node './pre-commit.js'
这个路径,是指如果在当前项目的根目录下运行 git commit
,所以 pre-commit.js
是相对于当前根目录的路径。想优化的话可以通过 Git 的一些默认环境变量来配置。
到这里就基本结束了,但是我们再回忆下之前说过的内容『客户端钩子是不会被其他项目成员 clone 下来的』,所以需要一种策略来保证项目中每个成员都安装了这个钩子。由于我们的前端项目是需要每个成员都通过 npm start 命令开启服务的,所以可以在 npm start 中做些手脚。
const fs = require('fs');
// 判断是否已经存在 pre-commit,不存在就读取 pre-commit.sh 并写入
if (!fs.existsSync('.git/hooks/pre-commit')) {
if(!fs.existsSync('.git/hooks/')) {
fs.mkdirSync('.git/hooks/');
}
let preCommitFile = fs.readFileSync('./pre-commit.sh');
fs.writeFileSync('.git/hooks/pre-commit', preCommitFile, {
encoding: 'utf8',
mode: 0o777
});
}
总结:
凡是能被 JS 重写的项目,最终一定会被 JS 重写。