husky 源碼分析
前言
Github:https://github.com/typicode/husky 使用 Git 鉤子變得簡單
在做前端工程化時 husky 可以說是一個必不可少的工具。husky 可以讓我們在項目中方便添加 git hooks。
這個庫的名字指的是 “哈士奇”,結合庫主要用在提交前發現問題、規范代碼的作用,應該是這個意思: ‘不好好規范你的代碼,你就像一個哈士奇一樣,會用代碼拆家的’ 。這個寓意跟另一個經常和 husky 搭配使用的庫 lint-staged 很像
lint-staged官網
Run linters against staged git files and don't let ?? slip into your code base!(大概意思:不要讓粑粑溜進你的代碼庫!)
這大概就是 ‘屎山’ 一詞的由來吧(我猜的)有個性!同時也說明了代碼規范的重要性!
今天我從源碼來探究下,這個 npm 庫到底做了什么!
該項目為 ts 項目,通過 tsconfig.json、package.json 可知,編譯入口為 src 出口為 lib 。項目執行主文件為 lib/index.js
主要文件為:src/index.ts、src/bin.ts、husky.sh
主文件 src/index.ts
import cp = require('child_process') // 子進程
import fs = require('fs') // 文件處理
import p = require('path') // 路徑方法
// 日志輸出
const l = (msg: string): void => console.log(`husky - ${msg}`)
// 通過 node 子進程拼接 git 參數并返回執行結果
const git = (args: string[]): cp.SpawnSyncReturns<Buffer> =>
cp.spawnSync('git', args, { stdio: 'inherit' })
/**
* install 方法可以看作初始化設置,在此方法中實現了以下功能
* 1、輔助 Plumbing 底層命令,主要用于操作
* 2、判斷安裝目錄是否為項目根目錄并創建 .husky 文件夾
* 3、判斷項目中是否存在 .git 文件。如果不存在 則不存在 githook 文件,也就 不存在替換 githook 的文件路徑的可能了
* 4、
* [1] 在 .husky 文件下創建文件夾 '_'
* [2] 在 '_' 文件下寫入文件 .gitignore, 文件內容為 ‘*’, 忽略該目錄下所有文件的 git 提交
* [3] 復制 husky 項目根目錄下的 husky.sh 文件 到 目標項目的 '_' 目錄下,名稱不變
* [4] 執行 git 操作,修改 githook 的執行路徑為目標項目的 .husky 文件下
* [5] 執行成功 or 失敗后的 log 提示
*/
export function install(dir = '.husky'): void {
// Ensure that we're inside a git repository
// If git command is not found, status is null and we should return.
// That's why status value needs to be checked explicitly.
// ----1----
if (git(['rev-parse']).status !== 0) {
return
}
// Custom dir help
const url = 'https://git.io/Jc3F9'
// ----2----
// Ensure that we're not trying to install outside of cwd
if (!p.resolve(process.cwd(), dir).startsWith(process.cwd())) {
throw new Error(`.. not allowed (see ${url})`)
}
// ----3----
// Ensure that cwd is git top level
if (!fs.existsSync('.git')) {
throw new Error(`.git can't be found (see ${url})`)
}
// ----4----
try {
// Create .husky/_ ----4.1----
fs.mkdirSync(p.join(dir, '_'), { recursive: true })
// Create .husky/_/.gitignore ----4.2----
fs.writeFileSync(p.join(dir, '_/.gitignore'), '*')
// Copy husky.sh to .husky/_/husky.sh ----4.3----
fs.copyFileSync(p.join(__dirname, '../husky.sh'), p.join(dir, '_/husky.sh'))
// Configure repo ----4.4----
const { error } = git(['config', 'core.hooksPath', dir])
if (error) {
throw error
}
// ----4.5----
} catch (e) {
l('Git hooks failed to install')
throw e
}
l('Git hooks installed')
}
/**
* set: 創建指定的 githook 文件,并寫入文件內容
* 1、如果文件目錄不存在, 中斷并提示執行 husky install 初始化配置
* 2、寫入文件, 指定解釋器為 sh 執行 shell 腳本, cmd 動態參數,為開發者想要在這個 githook 階段執行的操作,一般為腳本
* 例:npm run lint
* 3、創建成功的 log
*/
export function set(file: string, cmd: string): void {
const dir = p.dirname(file)
// ----1----
if (!fs.existsSync(dir)) {
throw new Error(
`can't create hook, ${dir} directory doesn't exist (try running husky install)`,
)
}
// ----2----
fs.writeFileSync(
file,
`#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
${cmd}
`,
{ mode: 0o0755 },
)
// ----3----
l(`created ${file}`)
}
/**
* add: 在已有的 githook 文件中追加命令
* 1、githook 文件是否存在
* 2、存在:在原有文件基礎上追加新的內容
* 3、不存在:執行 set 添加該 githook 文件
*/
export function add(file: string, cmd: string): void {
// ----1----
if (fs.existsSync(file)) {
// ----2----
fs.appendFileSync(file, `${cmd}\n`)
l(`updated ${file}`)
} else {
// ----3----
set(file, cmd)
}
}
/**
* uninstall: 卸載 install 中指定的 hooksPath 的路徑,恢復為 git 默認的 githook 路徑
*/
export function uninstall(): void {
git(['config', '--unset', 'core.hooksPath'])
}
入口文件 src/bin.ts
用于接受命令行參數,觸發 src/index.ts 中的執行文件
#!/usr/bin/env node // 指定使用 node 解析運行文件
import p = require('path') // 路徑方法
import h = require('./') // src/index.ts 主文件方法
// 打印幫助命令,這里沒有使用 command 包,而是使用進程方法獲取參數,所以自己打印了幫助 log
// Show usage and exit with code
function help(code: number) {
console.log(`Usage:
husky install [dir] (default: .husky)
husky uninstall
husky set|add <file> [cmd]`)
process.exit(code)
}
// Get CLI arguments 作者說了:獲取命令行參數
const [, , cmd, ...args] = process.argv
const ln = args.length
const [x, y] = args
// Set or add command in hook
// 處理需要參數的主文件的函數并執行, 對錯誤的參數進行了長度判斷
const hook = (fn: (a1: string, a2: string) => void) => (): void =>
// Show usage if no arguments are provided or more than 2
!ln || ln > 2 ? help(2) : fn(x, y)
// CLI commands
// 執行命令相對應的 src/index.ts 文件中的函數, 沒有參數的直接調用,需要參數的,套了一層 hook 函數,用于參數處理
const cmds: { [key: string]: () => void } = {
install: (): void => (ln > 1 ? help(2) : h.install(x)),
uninstall: h.uninstall, // 沒有參數直接調用
set: hook(h.set),
add: hook(h.add),
['-v']: () =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-var-requires
console.log(require(p.join(__dirname, '../package.json')).version),
}
// Run CLI
try {
// Run command or show usage for unknown command
// 作者說了:命令存在則運行指定函數,不存在則打印幫助 log
cmds[cmd] ? cmds[cmd]() : help(0)
} catch (e) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
console.error(e.message) // 打印 error 信息
process.exit(1) // 報錯退出
}
shell 腳本文件 husky.sh
還記得在主文件的 set 方法中,寫入自定義 githook 方法時會默認寫入的兩行執行命令嘛?
#!/bin/sh // 指定該文件使用 shell 解析執行
// 執行當前當前目錄下指定的文件,并執行該路徑下的 _/husky.sh 文件,也就是 主文件中 install 方法中復制到項目里的腳本文件
// $0 為 當前腳本名稱 調用 husky.sh 腳本
. "$(dirname "$0")/_/husky.sh"
#!/bin/sh
// 判斷變量 husky_skip_init 的長度是否為 0
if [ -z "$husky_skip_init" ]; then
// 為 0 時, 創建 debug 函數, 用來打印報錯日志
debug () {
// HUSKY_DEBUG 為 “1” 時打印
if [ "$HUSKY_DEBUG" = "1" ]; then
// $1 表示參數
echo "husky (debug) - $1"
fi
}
// 聲明一個只讀參數, 內容為 basename + 文件名稱
readonly hook_name="$(basename "$0")"
debug "starting $hook_name..."
// 判斷變量 HUSKY 是否 = “0”
if [ "$HUSKY" = "0" ]; then
debug "HUSKY env variable is set to 0, skipping hook"
exit 0
fi
// 判斷 ~/.huskyrc 是否為普通文件
if [ -f ~/.huskyrc ]; then
debug "sourcing ~/.huskyrc"
. ~/.huskyrc
fi
// 聲明只讀變量
export readonly husky_skip_init=1
// 當前文件名 是否在 傳進來的參數中 存在則執行
sh -e "$0" "$@"
exitCode="$?"
// 當 exitCode 不等于 0 時,打印當前執行的 hook 名稱 以及 退出碼
if [ $exitCode != 0 ]; then
echo "husky - $hook_name hook exited with code $exitCode (error)"
fi
exit $exitCode
fi
總結
該工具的最核心的代碼為
git config core.hooksPath 路徑 // 指定 githook觸發的路徑
git config --unset core.hooksPath // 卸載指定的路徑,恢復默認路徑
更改了 hook 執行的路徑,在該路徑下新建對應的 hook 的鉤子文件,該文件就會在指定的階段被觸發執行。
例:src/pre-commit 會在 git commit 時執行該文件的代碼
#!/bin/sh
npm run lint
該庫主要是將 git 命令進行了封裝、完善異常報錯機制,方便開發者使用。
作者自己打印了 cli 的幫助函數,可能是因為命令不多的原因吧,不過在寫 cli 的時候還是建議使用 command 庫,可以幫你處理很多事情,當然也會增加包的體積。