Git与CI/CD:让代码自动飞 —— 从Git Hooks到自动化流水线
手动检查代码风格、手动跑测试、手动打版本号、手动部署——每一个”手动”背后都是一次可能的人为失误。Git不仅是版本控制工具,它还内置了一套完善的钩子系统,让你能在关键节点自动执行任务。当Git Hooks与CI/CD平台联手,代码从提交到上线就变成了一条自动运转的传送带。
开篇自测:你已经知道多少?
- Git Hooks分为客户端钩子和服务端钩子,它们分别在什么时机触发?
- Conventional Commits规范中,
feat、fix、chore分别表示什么类型的变更?- GitHub Actions的工作流配置文件应该放在项目的哪个目录下?
- 语义化版本号(SemVer)中,1.2.3三个数字分别代表什么含义?
一、Git Hooks:代码仓库里的”门禁系统”
想象你管理一栋写字楼。每层楼都有门禁卡,有的在你进门时检查身份,有的在你出门时记录考勤。Git Hooks就是代码仓库里的门禁系统——它们在特定的Git操作前后自动触发,执行你预设的脚本。
钩子的两大阵营
Git Hooks分为客户端钩子和服务端钩子:
┌─────────────────────────────────────────────────────────────┐
│ Git Hooks 全景图 │
├──────────────────────────┬──────────────────────────────────┤
│ 客户端钩子(本地) │ 服务端钩子(远程仓库) │
├──────────────────────────┼──────────────────────────────────┤
│ pre-commit │ pre-receive │
│ prepare-commit-msg │ update │
│ commit-msg │ post-receive │
│ post-commit │ │
│ pre-push │ │
│ pre-rebase │ │
│ post-merge │ │
│ post-checkout │ │
└──────────────────────────┴──────────────────────────────────┘
- 客户端钩子运行在开发者本地机器上,通常用于代码质量检查。它们可以被开发者绕过(加
--no-verify参数),所以更像是一道”建议性”的防线。 - 服务端钩子运行在远程仓库所在的服务器上,通常用于强制执行团队规范。它们无法被绕过,是真正的”铁门禁”。
钩子脚本存放在每个仓库的 .git/hooks/ 目录下。你可以查看一下这个目录:
ls .git/hooks/
# 你会看到一堆 .sample 结尾的文件
# applypatch-msg.sample commit-msg.sample pre-commit.sample ...
# 去掉 .sample 后缀,脚本就会生效
三个最实用的客户端钩子
1. pre-commit:提交前的质量门卫
pre-commit 在你执行 git commit 之后、真正创建提交对象之前触发。如果脚本返回非零退出码,提交会被中止。
来写一个检查代码中是否残留调试语句的钩子:
#!/bin/sh
# .git/hooks/pre-commit
# 检查暂存区的文件中是否包含 console.log 或 debugger
FORBIDDEN_PATTERNS="console\.log\|debugger\|TODO:.*HACK"
# 只检查暂存区中的文件(即将被提交的文件)
FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(js|ts|jsx|tsx)$')
if [ -z "$FILES" ]; then
exit 0
fi
FOUND=$(echo "$FILES" | xargs grep -n -E "$FORBIDDEN_PATTERNS" 2>/dev/null)
if [ -n "$FOUND" ]; then
echo "================================================"
echo " 提交被拦截!发现以下调试代码:"
echo "================================================"
echo "$FOUND"
echo ""
echo "请移除后重新提交,或使用 git commit --no-verify 跳过检查"
exit 1
fi
exit 0
2. commit-msg:提交信息的格式审查员
commit-msg 钩子在编辑器关闭、提交信息写入临时文件后触发。它接收一个参数——存放提交信息的临时文件路径。
#!/bin/sh
# .git/hooks/commit-msg
COMMIT_MSG_FILE=$1
COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")
# 检查是否符合 Conventional Commits 格式
# 格式:type(scope): description
PATTERN="^(feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert)(\(.+\))?: .{1,100}"
if ! echo "$COMMIT_MSG" | grep -qE "$PATTERN"; then
echo "================================================"
echo " 提交信息格式不符合规范!"
echo "================================================"
echo ""
echo "正确格式:type(scope): description"
echo ""
echo "type 可选值:"
echo " feat - 新功能"
echo " fix - 修复Bug"
echo " docs - 文档变更"
echo " style - 代码格式(不影响逻辑)"
echo " refactor - 重构(非新功能、非修复)"
echo " perf - 性能优化"
echo " test - 测试相关"
echo " chore - 构建过程或辅助工具变更"
echo " ci - CI配置变更"
echo " build - 构建系统变更"
echo " revert - 回退提交"
echo ""
echo "示例:feat(auth): 添加微信扫码登录"
echo " fix: 修复用户列表分页错误"
exit 1
fi
exit 0
3. pre-push:推送前的最后防线
pre-push 在 git push 执行之后、数据实际传输之前触发。这是阻止有问题的代码到达远程仓库的最后机会。
#!/bin/sh
# .git/hooks/pre-push
REMOTE="$1"
# 阻止直接推送到 main 或 master 分支
CURRENT_BRANCH=$(git symbolic-ref HEAD 2>/dev/null | sed 's|refs/heads/||')
if [ "$CURRENT_BRANCH" = "main" ] || [ "$CURRENT_BRANCH" = "master" ]; then
echo "================================================"
echo " 禁止直接推送到 $CURRENT_BRANCH 分支!"
echo "================================================"
echo "请创建功能分支,通过 Pull Request 合并。"
exit 1
fi
# 推送前运行测试
echo "推送前运行测试..."
npm test 2>/dev/null
if [ $? -ne 0 ]; then
echo "测试未通过,推送已取消。"
exit 1
fi
exit 0
想一想
.git/hooks/目录不会被git add追踪(因为它在.git内部),那么团队成员之间如何共享同一套钩子脚本?
二、Husky + lint-staged:工程化的钩子管理方案
上面提到的问题正好引出了工程化方案。手动管理 .git/hooks/ 目录有两个痛点:一是钩子脚本无法随代码一起版本控制;二是团队成员需要手动配置。Husky 和 lint-staged 正是为解决这些问题而生。
安装与配置
# 初始化(假设项目已有 package.json,需要 Husky v9+)
npx husky init
# 这会做两件事:
# 1. 在 package.json 的 scripts 中添加 "prepare": "husky"
# 2. 创建 .husky/ 目录并写入一个示例 pre-commit 钩子
💡 版本提示:
npx husky init是 Husky v9+ 的用法。如果你使用的是 v8 或更早版本,初始化命令为npx husky-init,配置方式也有所不同。建议直接使用最新版本。
安装 lint-staged,它的作用是只对暂存区的文件运行检查,而不是整个项目:
npm install --save-dev lint-staged
配置 lint-staged
在 package.json 中添加配置:
{
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{css,scss,less}": [
"prettier --write"
],
"*.{json,md}": [
"prettier --write"
]
}
}
配置 Husky 钩子
# 编辑 pre-commit 钩子
echo "npx lint-staged" > .husky/pre-commit
# 添加 commit-msg 钩子(配合 commitlint 使用)
npm install --save-dev @commitlint/cli @commitlint/config-conventional
echo "npx --no -- commitlint --edit \$1" > .husky/commit-msg
创建 commitlint 配置文件 commitlint.config.js(如果项目在 package.json 中设置了 "type": "module",需改用 commitlint.config.mjs 并将 module.exports 替换为 export default):
// commitlint.config.js(CommonJS 格式)
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [2, 'always', [
'feat', 'fix', 'docs', 'style', 'refactor',
'perf', 'test', 'chore', 'ci', 'build', 'revert'
]],
'subject-max-length': [2, 'always', 100],
'subject-empty': [2, 'never'],
}
};
现在,当团队成员克隆项目并执行 npm install 时,prepare 脚本会自动运行 husky,配置好Git钩子。整个过程对开发者来说是透明的。
团队成员克隆项目 → npm install → prepare脚本自动执行husky
↓
.git/hooks/ 被自动配置
↓
此后每次 commit/push 都会触发检查
常见误区
- 误区一:“lint-staged会检查项目中所有的文件”。lint-staged的核心价值恰恰在于它只检查暂存区中被修改的文件。如果你的项目有上万个文件,但你只改了3个,那lint-staged只会对这3个文件运行ESLint和Prettier,速度极快。
- 误区二:“钩子检查太严格会影响开发效率”。恰恰相反,在提交时就拦住格式问题和低级错误,比在代码评审时才发现要高效得多。把机械性的检查交给机器,人类只需要审查逻辑。
三、GitHub Actions:云端的自动化引擎
Git Hooks是在本地运行的”门禁”,而CI/CD平台则是云端的”自动化工厂”。代码推送到远程仓库后,CI/CD平台接手后续的测试、构建、部署工作。
GitHub Actions是目前最流行的CI/CD平台之一,它与GitHub深度集成,对公开仓库完全免费。
Workflow的基本结构
工作流配置文件存放在 .github/workflows/ 目录下,使用YAML格式:
.github/
workflows/
ci.yml ← 持续集成工作流
release.yml ← 自动发版工作流
deploy.yml ← 部署工作流
一个工作流的骨架长这样:
name: CI # 工作流名称
on: # 触发条件
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs: # 任务列表
lint-and-test: # 任务名称
runs-on: ubuntu-latest # 运行环境
steps: # 步骤列表
- uses: actions/checkout@v6 # 使用现成的Action
- run: npm install # 运行命令
- run: npm test
触发条件详解
GitHub Actions支持丰富的触发条件:
on:
# 代码推送时触发(支持同时匹配分支和标签)
push:
branches: [main]
paths:
- 'src/**' # 只有src目录下的文件变更才触发
- '!src/**/*.md' # 排除markdown文件
tags:
- 'v*' # 匹配 v1.0.0、v2.1.3 等
# PR事件触发
pull_request:
types: [opened, synchronize, reopened]
# 定时触发(cron表达式)
schedule:
- cron: '0 2 * * 1' # 每周一凌晨2点
# 手动触发
workflow_dispatch:
inputs:
environment:
description: '部署环境'
required: true
default: 'staging'
type: choice
options:
- staging
- production
💡 注意:YAML 规范要求同一层级的键不能重复。如果你需要分别为分支推送和标签推送配置不同的工作流,应该创建两个独立的 workflow 文件,而不是在同一个
on:块中写两个push:键。
实战:前端项目CI工作流
下面是一个React/Vue项目的完整CI配置:
name: Frontend CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
quality-check:
name: 代码质量检查
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 22] # 同时测试两个LTS版本
steps:
- name: 检出代码
uses: actions/checkout@v6
- name: 配置 Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: 安装依赖
run: npm ci
- name: 代码风格检查
run: npm run lint
- name: 类型检查
run: npm run type-check
- name: 运行单元测试
run: npm run test -- --coverage
- name: 上传覆盖率报告
if: matrix.node-version == 22
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
build:
name: 构建验证
runs-on: ubuntu-latest
needs: quality-check # 依赖quality-check成功
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run build
- name: 上传构建产物
uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
retention-days: 7
注意几个关键设计:
- matrix策略:同时在多个Node.js版本上运行测试,确保兼容性。
- needs依赖:
build任务等待quality-check通过后才执行,避免浪费计算资源。 - artifact保存:将覆盖率报告和构建产物保存下来,方便后续查看或部署。
四、CI流水线的完整链路
一条设计良好的CI/CD流水线,就像工厂的装配线——每个工位负责一道工序,产品依次通过每个工位,任何一道工序不合格就停线报警。
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
│ 推送 │───→│ Lint │───→│ 测试 │───→│ 构建 │───→│ 预览 │───→│ 部署 │
│ 代码 │ │ 检查 │ │ │ │ │ │ 环境 │ │ 生产 │
└──────┘ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘
│ │ │ │
↓ ↓ ↓ ↓
格式不过? 测试失败? 构建报错? 需要人工审批
← 阻止 → ← 阻止 → ← 阻止 → ← 等待确认 →
GitLab CI/CD简介
如果你的团队使用GitLab,CI/CD配置文件是项目根目录的 .gitlab-ci.yml。核心概念与GitHub Actions类似,但术语有所不同:
| 概念 | GitHub Actions | GitLab CI/CD |
|---|---|---|
| 配置文件 | .github/workflows/*.yml | .gitlab-ci.yml |
| 执行单元 | Job | Job |
| 执行阶段 | 通过needs控制 | stages定义顺序 |
| 运行环境 | Runner(GitHub托管) | Runner(需自行注册或用共享的) |
| 密钥管理 | Secrets | Variables |
一个GitLab CI配置示例:
stages:
- check
- test
- build
- deploy
variables:
NODE_VERSION: "20"
lint:
stage: check
image: node:${NODE_VERSION}-alpine
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
script:
- npm ci
- npm run lint
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "main"'
unit-test:
stage: test
image: node:${NODE_VERSION}-alpine
script:
- npm ci
- npm run test -- --coverage
coverage: '/All files[^|]*\|[^|]*\s+([\d\.]+)/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
build:
stage: build
image: node:${NODE_VERSION}-alpine
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 week
only:
- main
deploy-staging:
stage: deploy
script:
- echo "Deploying to staging..."
environment:
name: staging
only:
- main
deploy-production:
stage: deploy
script:
- echo "Deploying to production..."
environment:
name: production
when: manual # 生产部署需手动触发
only:
- main
想一想 GitHub Actions和GitLab CI/CD都支持”缓存”机制(缓存node_modules等依赖目录),这对CI流水线的执行速度有多大影响?如果不缓存,每次运行都要从零安装依赖,一个大型项目的
npm install可能需要2-3分钟。
五、Conventional Commits与语义化版本
代码的提交信息不只是给人看的——当你的提交信息遵循统一的规范时,工具就能自动从中提取信息,实现自动化的版本号管理和变更日志生成。
Conventional Commits规范
这是目前社区最广泛采用的提交信息规范,格式如下:
<type>(<scope>): <description>
[可选的正文]
[可选的脚注]
各字段的含义:
feat(shopping-cart): 添加批量删除商品功能
│ │ │
│ │ └─ description:简短描述,用祈使句
│ └─ scope:影响范围(可选),通常是模块名
└─ type:变更类型
BREAKING CHANGE: 购物车API的返回格式从数组改为对象
│
└─ 脚注:破坏性变更声明(会触发主版本号递增)
type的完整列表及其含义:
| type | 含义 | 示例 |
|---|---|---|
feat | 新功能 | feat: 添加用户头像上传 |
fix | 修复Bug | fix: 修复登录页面白屏问题 |
docs | 文档变更 | docs: 更新API接口文档 |
style | 代码格式调整 | style: 统一缩进为2空格 |
refactor | 重构 | refactor: 拆分订单处理模块 |
perf | 性能优化 | perf: 优化首页图片懒加载 |
test | 测试相关 | test: 补充支付模块单元测试 |
chore | 杂务(依赖更新等) | chore: 升级webpack到v5 |
ci | CI配置变更 | ci: 添加代码覆盖率检查 |
build | 构建系统变更 | build: 切换到Vite构建 |
revert | 回退提交 | revert: 回退feat(cart)提交 |
语义化版本(Semantic Versioning)
语义化版本号的格式是 MAJOR.MINOR.PATCH:
v2.4.1
│ │ │
│ │ └─ PATCH:修复Bug(向后兼容)
│ └─ MINOR:新增功能(向后兼容)
└─ MAJOR:破坏性变更(不向后兼容)
Conventional Commits与SemVer之间有明确的映射关系:
fix 类型的提交 → PATCH 版本号递增 → 1.0.0 → 1.0.1
feat 类型的提交 → MINOR 版本号递增 → 1.0.0 → 1.1.0
BREAKING CHANGE → MAJOR 版本号递增 → 1.0.0 → 2.0.0
自动发版实战
利用 standard-version 或 release-please 等工具,可以实现完全自动化的版本管理:
# .github/workflows/release.yml
name: Release
on:
push:
branches: [main]
permissions:
contents: write
pull-requests: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0 # 获取完整历史,用于分析提交
- uses: actions/setup-node@v6
with:
node-version: 20
registry-url: 'https://registry.npmjs.org'
- name: 安装依赖
run: npm ci
- name: 构建
run: npm run build
# 使用 release-please 自动管理版本和CHANGELOG
- uses: googleapis/release-please-action@v4
id: release
with:
release-type: node
# 如果有新版本发布,自动发布到npm
- name: 发布到npm
if: ${{ steps.release.outputs.release_created }}
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
这个工作流的运作方式是:
- 每次推送到main分支时,
release-please分析自上个版本以来的所有提交信息 - 根据Conventional Commits规范自动计算下一个版本号
- 自动生成CHANGELOG.md
- 创建一个Release PR
- 当Release PR被合并时,自动打Tag、创建GitHub Release、发布到npm
feat提交被合并到main → release-please分析提交 → 创建Release PR
↓
PR内容包含:
- 版本号变更
- CHANGELOG更新
↓
维护者审核并合并PR
↓
自动打Tag + 发布npm包
常见误区
- 误区一:“Conventional Commits太死板,增加了提交的心智负担”。一开始确实需要适应,但配合commitlint自动校验后,格式错误会被即时拦截并提示正确写法,很快就能形成肌肉记忆。而收益是巨大的——自动化的CHANGELOG和版本号让你再也不需要在发版时手动整理”这次版本改了什么”。
- 误区二:“每个commit都必须严格遵循规范”。在使用Squash Merge策略时,中间的commit信息不太重要,只需要保证最终合并到主分支的那条信息符合规范即可。
- 误区三:“语义化版本号只是好看”。SemVer是一种承诺——当用户看到你的库从1.2.3升级到1.3.0,他们可以放心升级,因为MINOR版本变更承诺了向后兼容。这是开源生态信任链的基石。
六、实战:配置一套完整的前端自动化流水线
现在让我们把前面所有的知识串联起来,为一个真实的前端项目配置从提交到发版的全套自动化。
第一步:初始化项目钩子
# 安装所有工具
npm install --save-dev husky lint-staged eslint prettier \
@commitlint/cli @commitlint/config-conventional
# 初始化husky
npx husky init
第二步:配置 package.json
{
"scripts": {
"prepare": "husky",
"lint": "eslint src/ --ext .js,.jsx,.ts,.tsx",
"lint:fix": "eslint src/ --ext .js,.jsx,.ts,.tsx --fix",
"format": "prettier --write 'src/**/*.{js,jsx,ts,tsx,css,json}'",
"type-check": "tsc --noEmit",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"build": "vite build"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{css,json,md}": [
"prettier --write"
]
}
}
第三步:配置钩子脚本
# pre-commit:只检查暂存区文件
echo "npx lint-staged" > .husky/pre-commit
# commit-msg:校验提交信息格式
echo "npx --no -- commitlint --edit \$1" > .husky/commit-msg
# pre-push:推送前运行测试
cat > .husky/pre-push << 'HOOK'
npm run type-check && npm run test
HOOK
第四步:配置GitHub Actions
# .github/workflows/ci.yml
name: CI Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true # 同一分支的新推送会取消旧的运行
jobs:
lint:
name: Lint & Format
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm run type-check
test:
name: Unit Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run test:coverage
- uses: actions/upload-artifact@v4
with:
name: coverage
path: coverage/
build:
name: Build
runs-on: ubuntu-latest
needs: [lint, test]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
deploy-preview:
name: Deploy Preview
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'pull_request'
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: 部署到预览环境
run: echo "Deploy preview for PR #${{ github.event.number }}"
# 实际项目中可使用 Vercel、Netlify 等平台的预览部署
deploy-production:
name: Deploy Production
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
environment: production # 可配置需要人工审批
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: 部署到生产环境
run: echo "Deploying to production..."
整体链路图
开发者本地 │ GitHub 云端
│
编写代码 │
↓ │
git add │
↓ │
git commit │
↓ │
[pre-commit 触发] │
└→ lint-staged │
└→ ESLint + Prettier │
↓ │
[commit-msg 触发] │
└→ commitlint 校验格式 │
↓ │
git push │
↓ │
[pre-push 触发] │
└→ 类型检查 + 单元测试 │
↓ │
代码到达远程仓库 ──────────→ GitHub Actions 触发
│ ├→ Lint检查
│ ├→ 测试 + 覆盖率
│ ├→ 构建
│ ├→ 预览部署(PR时)
│ └→ 生产部署(合并到main时)
│
│ release-please 分析提交
│ └→ 自动版本号 + CHANGELOG + 发版
这条流水线实现了一个目标:开发者只需要关心写代码和写好提交信息,其余的格式检查、测试、构建、发版全部由自动化工具完成。
掌握度自测
-
请解释
pre-commit、commit-msg、pre-push三个钩子各自的触发时机和典型用途。如果开发者使用--no-verify跳过了本地钩子,还有什么机制能保证代码质量? -
在一个已有项目中引入husky + lint-staged,需要哪些步骤?新成员克隆项目后如何自动获得钩子配置?
-
请写出以下变更对应的Conventional Commits提交信息:(a) 在用户模块中新增了手机号登录功能;(b) 修复了订单金额计算精度丢失的Bug;(c) 将打包工具从webpack迁移到vite,所有构建命令都变了。
-
一个npm包当前版本是
2.3.1,经过以下三次提交后版本号应该是多少?fix: 修复类型定义→feat: 添加深色模式→fix: 修复深色模式下按钮颜色 -
在GitHub Actions中,如何实现”lint和test并行执行,两者都通过后再执行build”的流水线结构?请说明关键配置。
自我评估
- 全部答对:你已经掌握了Git自动化的完整技能栈。代码质量和交付效率可以同时提升,这就是工程化的力量。
- 答对3-4题:基础扎实,建议在自己的项目中实际配置一遍完整流水线,实践出真知。
- 答对1-2题:先从最简单的开始——给项目加一个pre-commit钩子,只做ESLint检查。用起来之后,再逐步增加commit-msg校验和CI工作流。
购买课程解锁全部内容
版本控制不翻车:Git 从基础到团队协作
¥29.90