🌟面试官问:天天在用Git,那你知道Git Hooks是什么吗?🌟

2,725 阅读6分钟

前言

git 我们应该不陌生了,几乎每天工作都会用到。

今天要为大家介绍的是 git 里面的 hooks ,也就是 git 钩子。

git 钩子是什么呢?它可以用来干什么呢?为什么说它是强大的武器呢?带着这些问题,我们一起来看看, git 钩子它到底是个啥。

Git Hooks 简介

Git hooks 是 git 版本控制系统的一个功能,它允许我们在特定的 git 事件发生时执行自定义脚本。

这些事件可以是提交 (commit) 、推送 (push) 、合并 (merge) 等等。通过使用 Git hooks ,我们可以在这些事件发生前或发生后执行特定的操作,比如自动化测试、代码风格检查、部署到服务器等等。

Git hooks 是存储在 .git/hooks 目录下的可执行文件,这些文件的名称对应着不同的 git 事件。当 git 执行相关事件时,相应名称的钩子脚本就会被执行。

.git 是一个隐藏文件夹,在 mac 或者 linux 系统下,可以使用 ls -a 来查看,进入到.git/hooks目录下,可以看到有如下文件:

image.png

下面详细介绍一下 git 系统中常用钩子的执行时机以及用途:

  1. pre-commit :在执行提交 (commit) 之前运行。通常用于执行代码风格检查、静态代码分析、单元测试等操作,以确保提交的代码质量。
  2. prepare-commit-msg:在提交 (commit) 消息被编辑之前运行。通常用于修改或扩展提交消息,例如添加自动化生成的信息、验证提交消息格式等。
  3. commit-msg :在提交 (commit) 消息被创建后,但提交动作尚未完成时运行。通常用于验证提交消息的格式、内容等是否符合规范。
  4. post-commit :在提交 (commit) 完成后运行。通常用于发送通知、更新文档、执行某些特定的后续处理等操作。
  5. pre-rebase :在执行变基 (rebase) 操作之前运行。通常用于执行一些预检查,例如确保变基操作不会产生冲突或导致代码质量下降。
  6. post-checkout :在检出 (checkout) 完成后运行。通常用于执行一些与工作目录切换相关的操作,例如更新依赖、清理临时文件等。
  7. post-merge :在合并 (merge) 完成后运行。通常用于执行一些与合并操作相关的操作,例如重新构建项目、更新子模块等。
  8. pre-push :在执行推送 (push) 之前运行。通常用于执行一些预检查,例如运行测试、检查代码质量等。
  9. pre-receive(服务端钩子) :在远程仓库接收到推送 (push) 之前运行。通常用于执行一些服务端的预检查,例如验证提交的代码是否符合特定规范。
  10. update(服务端钩子) :在推送 (push) 到远程仓库但尚未完成更新时运行。通常用于执行一些与分支更新相关的操作,例如验证提交的代码是否符合特定规范、是否有权限更新等。
  11. post-receive(服务端钩子) :在远程仓库接收到推送 (push) 并完成更新后运行。通常用于执行一些服务端的后续处理,例如自动化部署、更新文档等。

Husky 工作流原理揭秘

在大致了解了这些钩子的执行时机以及作用之后,我们来学习一个在前端工程化中常用到代码规范技术选型: Husky+lint-staged+eslint 。

首先介绍一下这套技术是用来干嘛的——主要用来在提交代码的时候自动触发 eslint 代码扫描,用来校验并统一整个项目的代码规范。

下面分别介绍这三个工具在这套流程中起到的作用:

  • eslint :这个大家都比较熟悉,主要是用来做代码扫描的
  • lint-staged :是一个用于在提交前运行 Linter 的工具,它只会检查即将提交的文件,而不是整个项目。
  • Husky :允许我们自定义一些 git 钩子

所以整个流程就是,使用 Husky 来自定义一些 git 钩子,然后配置 lint-staged 调用 eslint 去扫描代码,此时仅仅扫描待提交的文件而不是整个项目。

husky是怎么注入钩子的

首先我们先来安装一下 husky , npm i husky --save-dev ,这里安装的是 ^9.0.11 这个版本,然后根据官方文档的提示,我们执行一下 npx husky init。

此时会发现项目下多了一个 .husky 文件夹,以及 package.json 中多了一条命令。

image.png

当我们执行npx husky init的时候,实际上执行了下面的这段代码。

image.png

它调用了 index.mjs ,以及在 .husky 目录下新建了一个 pre-commit 文件。我们先来看 index.mjs 。

image.png

下面重点来看这段代码

let { status: s, stderr: e } = c.spawnSync('git', ['config', 'core.hooksPath', `${d}/_`])

它主要执行了git config core.hooksPath ${d}/_这个系统调用,这个系统调用的意思是,在执行 git 钩子时,不去调用.git/hooks目录下,而去调用 ${d}/_ 这个目录,而这个目录就是 .husky 下的 _ 目录

数组 l 主要是一些 git 钩子的名称, pre-commit 、 commit-msg 等,然后在 .husky 目录下新建了一个 _ 文件夹,以及在这个文件夹下新建了几个钩子文件,这几个钩子文件的内容都填入了下面的内容,下面的内容意思是当调用这个脚本的时候,调用同目录下的 h 脚本。

#!/usr/bin/env sh
. "${0%/*}/h"

也就是说,当我们 commit 的时候, git 会帮我们调用 pre-commit 钩子,然后 husky 设置了这些钩子的执行目录—— .husky/_。

所以当我们 commit 的时候,实际上调用了.husky/_目录下的 pre-commit 脚本,然后 pre-commit 脚本又调用了 h 脚本。

我们再来看 h 脚本:

image.png

可以看到 h 脚本执行了这个命令

sh -e "$s" "$@"

然后我们主要关注这两句:

h="${0##*/}"
s="${0%/*/*}/$h"

这两句的意思是调用上一级目录的脚本名称,也就是当我们 commit 的时候,会调用 .husky 目录下的 pre-commit 脚本。

image.png

可以看到最终调用结果与我们的分析无误,因为我们没有配置 test 这个命令 所以执行报错。

image.png

至此,已经把 husky 注入 git 钩子的主要流程阐述完毕。

自定义pre-commit进行eslint扫描

下面我们来试试自定义一个 pre-commit 钩子,主要是在 pre-commit 钩子文件中调用需要执行的命令。

我们希望在 commit 的时候调用 eslint 来做代码检查,所以可以先安装一下:npm i eslint --save-dev。

然后 eslint 的配置文件 .eslintrc.cjs 内容如下:

image.png

这个时候只需要在 .husky/pre-commit 中填入以下内容:

npx eslint .

就可以在提交代码的时候自动调用 eslint 进行代码检查:

image.png

lint-staged是怎么工作的

lint-staged 是一个用于在 git 暂存区中运行代码检查工具的工具,通常与 ESLint、Prettier、Stylelint 等代码检查工具一起使用,以确保只对暂存区中的文件进行检查,避免不必要的全局检查。

首先来安装一下 lint-staged :

npm install lint-staged --save-dev

然后在 package.json 中配置一下 lint-staged 需要调用的脚本

  "lint-staged": {
    "*.(js|jsx|tsx)": "eslint"
  },

最后在 pre-commit 钩子中配置一下调用 lint-staged :

npx lint-staged

image.png

可以看到此时提交的时候已经使用 lint-staged 去配合 eslint 做代码检查。

lint-staged 也是通过 git 命令去获取暂存区的文件,比如可以通过 git diff --cached --name-only 这样的命令去获取。

image.png

获取到暂存区的文件之后,再调用相应的 linter 去扫描对应的文件。比如 eslint 就扫描 js/ts 文件, stylelint 扫描 css/less 等文件。

Git Hook 实现自动化部署

下面再来介绍一个使用 Git Hook 做自动化部署的示例,在我们推送代码的时候,自动帮我们构建并且把产物推送到服务器上。

这种方式用来部署测试环境没有问题的,但是一定不能用在部署生产环境上。

相比于 Jenkins+Git服务端钩子 而言,这种方式更加的简单方便,无需过多的环境配置。

但是缺点也比较明显:打包是在本地,会占用本地的资源;团队协作时打包环境很容易不一致,可能会导致一些意想不到的问题。

我们主要用到的是 pre-push 这个钩子,它在你执行git push命令之前被触发。这个钩子允许你在数据推送到远程仓库之前执行一些自定义的操作。

我们可以在这个钩子中编写一些脚本来执行想要的操作,如果这些操作成功完成,Git就会继续推送数据到远程仓库;如果这些操作失败, Git 会中止推送过程,并显示相应的错误信息。

以下的自动化部署适用于你的部署流程是走 ftp 部署,即把打包后的 dist 目录传输到 nginx 或者其他 web 服务器目录下。

我们在 .huksy 目录下新建一个 pre-push 文件,然后填入以下内容

#!/bin/bash
path='你的项目路径'
cd "$path"
rm package-lock.json
rm -rf node_modules
npm i
npx vite build && node "$path/scripts/publish.cjs"
  1. 进入到项目目录中
  2. 删除并重装依赖
  3. 构建并执行发布脚本

然后来看发布脚本:

const fs = require("fs");
const path = require("path");
const archiver = require("archiver");
const { sshConfig } = require("./config.cjs");
/* ------------------ 请配置服务器信息 --- start --------------------- */
const zipName = "test.zip";
const remotePath = `/www/wwwroot/test-${+new Date()}`; // 要上传到服务器的目标路径
const originRemotePath = "/www/wwwroot/test";
const distPath = "../dist"; // 要压缩的文件夹路径
/* ------------------ 配置服务器信息 --- end --------------------- */

const output = fs.createWriteStream(zipName); // 压缩后的文件
const archive = archiver("zip");
output.on("close", function () {
  console.log(`${archive.pointer()} total bytes`);
  console.log(
    "archiver has been finalized and the output file descriptor has closed."
  );
});
archive.on("error", function (err) {
  throw err;
});
archive.pipe(output);
const directoryPath = path.join(__dirname, distPath);
archive.directory(directoryPath, false);
archive.finalize();
const Client = require("ssh2").Client;
const conn = new Client();
conn
  .on("ready", function () {
    console.log("服务器连接成功");
    conn.exec(`mkdir ${remotePath}`, (err) => {
      if (err) throw err;
      conn.sftp(function (err, sftp) {
        if (err) throw err;
        const readStream = fs.createReadStream(zipName);
        const writeStream = sftp.createWriteStream(remotePath + "/" + zipName);
        readStream.pipe(writeStream);
        writeStream.on("close", function () {
          console.log(`File ${remotePath} 上传 完成`);
          // 解压
          conn.exec(
            `rm -rf ${originRemotePath} && cd ${remotePath} && unzip -o ${zipName} && mv ${remotePath} ${originRemotePath} `,
            function (err, stream) {
              if (err) throw err;
              stream
                .on("close", function (code, signal) {
                  console.log("部署 完成");
                  // 删除本地压缩包
                  fs.unlinkSync(zipName);
                  conn.end();
                })
                .on("data", function (data) {
                  console.log("解压中: " + data);
                });
            }
          );
        });
      });
    });
  })
  .connect(sshConfig);


  1. 根据你的服务器 ssh 配置去连接服务器
  2. 把打包产物 dist 压缩成一个压缩包
  3. 把这个压缩包传输到你服务器的资源目录下,比如 nginx 的静态资源目录
  4. 解压这个压缩包并删除旧的打包产物,这就完成了整个发布

其中 ssh 配置大致长成这个样子:

module.exports.sshConfig = {
  host: "hostname",
  port: 22,
  username: "username",
  password: "password",
};

image.png

至此,我们已经完成在提交代码的时候自动构建以及推送产物到测试环境中,完成了部署。

最后

以上就是本文的全部内容,介绍了 git 钩子以及它的一些实际用途。如果你觉得有意思的话,点点关注点点赞吧~