GitHub 协作最佳实践
PR 大小与描述
一个好的 Pull Request 的准则:
- 一个 PR 只做一件事,保持单一职责
- 变更行数控制在 400 行以内(研究表明超过 400 行,Review 质量明显下降)
- 大功能拆分成多个小 PR,逐步合并
PR 描述模板
在仓库 .github/PULL_REQUEST_TEMPLATE.md 中设置 PR 模板:
## Why(为什么做这个改动)
关联 Issue: #123
解决了用户无法在移动端完成注册的问题。
## What(做了什么改动)
- 修复了手机号验证正则表达式
- 增加了对国际区号的支持
- 新增了 3 个单元测试用例
## How to test(如何测试)
- [ ] 访问 /register,用手机号注册
- [ ] 测试国际号码格式(+1-555-1234567)
- [ ] 确认错误信息显示正确
## Screenshots(截图,如有 UI 变更)
CODEOWNERS 文件
在 .github/CODEOWNERS 文件中定义代码所有者,当相关路径的文件被修改时,自动添加指定成员为 Reviewer:
# .github/CODEOWNERS
# 格式:路径模式 @用户名或@组
# 整个仓库的默认 owner
* @org/core-team
# 前端代码由前端团队负责
/src/frontend/ @org/frontend-team
# 支付相关代码需要特定成员审核
/src/payment/ @alice @bob
# 基础设施配置
/infra/ @org/devops-team
*.yml @org/devops-team
GitHub 高级功能
Protected Branch(保护分支)
在 GitHub 仓库的 Settings → Branches 中可以为 main 等重要分支设置保护规则:
- Require pull request reviews:合并前需要 N 个 Approve
- Dismiss stale pull request approvals:新提交后需重新 Review
- Require status checks to pass:CI 必须全部通过才能合并
- Require branches to be up to date:合并前分支必须是最新的
- Restrict who can push:限制哪些人可以直接推送
- Allow force pushes:默认禁止 force push
GitHub Actions 简介
GitHub Actions 是 GitHub 内置的 CI/CD 平台,与 Git Hooks 的核心区别:
| 维度 | Git Hooks | GitHub Actions |
|---|---|---|
| 运行位置 | 开发者本地机器 | GitHub 云端服务器 |
| 可被绕过 | 可以(--no-verify) | 不可以(服务端运行) |
| 触发条件 | Git 命令事件 | push、PR、定时等任意事件 |
| 典型用途 | 本地快速检查 | 完整 CI、自动部署 |
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm test
- run: npm run build
Git 内部原理——对象模型
理解 Git 的内部原理,是真正掌握 Git 的关键。Git 的底层其实是一个简单而优雅的内容寻址文件系统(Content-Addressable Filesystem)。
四种 Git 对象
Git 中的一切都存储为对象,存放在 .git/objects/ 目录中。每个对象由其内容的 SHA-1 哈希唯一标识。
- blob 存储文件内容(只有内容,没有文件名)。两个内容相同的文件共享同一个 blob 对象,无论它们在哪里、叫什么名字。
- tree 存储目录结构,包含一组指向 blob(文件)和 tree(子目录)的引用,以及每个条目的文件名和权限。相当于文件系统的目录。
- commit 存储一次提交的快照:指向一个 tree(项目根目录的快照)、指向父 commit(历史链)、作者/提交者信息、提交时间、提交信息。
- tag 附注标签对象,包含:被标记对象的引用(通常是 commit)、标签名、标签者信息、标签消息,以及可选的 GPG 签名。
对象之间的关系
│
commit ──▶ parent commit ──▶ ...
│
tree (root/)
├──▶ blob (README.md)
├──▶ tree (src/)
│ ├──▶ blob (app.js)
│ └──▶ blob (utils.js)
└──▶ tree (tests/)
└──▶ blob (app.test.js)
亲手查看 Git 对象
# 查看某个文件对应的 blob 哈希
git hash-object README.md
# 查看 HEAD 指向的 commit 对象内容
git cat-file -p HEAD
# 输出示例:
# tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
# parent a3f2c1d8e5b9f2130459a781cd13f58cc6f88c9
# author 张三 <zhang@example.com> 1710000000 +0800
# committer 张三 <zhang@example.com> 1710000000 +0800
# feat: add profile page
# 查看 tree 对象的内容
git cat-file -p HEAD^{tree}
# 输出:100644 blob abc1234... README.md
# 040000 tree def5678... src
# 查看对象类型
git cat-file -t HEAD # 输出:commit
# 在 .git/objects/ 中直接查看(二进制压缩,需要解压)
ls .git/objects/
引用(Refs)
分支和标签都是引用——存储在 .git/refs/ 目录下的文本文件,内容就是一个 40 字符的 commit 哈希。
# 查看 main 分支指向哪个 commit
cat .git/refs/heads/main
# 查看某个 tag 指向哪个对象
cat .git/refs/tags/v1.0.0
# 查看 HEAD 文件(指向当前分支)
cat .git/HEAD
# 输出:ref: refs/heads/main
# 创建分支本质上就是创建一个包含 commit 哈希的文件
echo a3f2c1d... > .git/refs/heads/new-branch
"Git 中一切都是 SHA 指针"的意义
理解这个设计哲学,可以解释很多 Git 的行为:
- 为什么分支创建如此廉价?因为创建分支只是写一个 41 字节的文件(SHA + 换行)。
- 为什么 Git 如此快?因为大多数操作只是读写几个哈希文件,本地完成,不需要网络。
- 为什么 Git 数据如此安全?因为每个对象的哈希由其内容决定,任何数据损坏都会导致哈希不匹配,立即被发现。
- 为什么相同内容不会被存储两次?内容寻址存储保证了相同内容永远有相同哈希,自动去重。
- 为什么 rebase 会"改变历史"?因为 rebase 创建了新的 commit 对象(不同内容 → 不同 SHA),旧的 commit 对象不会被立即删除,但不再被任何引用指向。
常见 Git 问题排查
问题 1:合并后发现 bug,如何回滚到合并前
# 找到 merge commit 的哈希
git log --oneline --merges
# 方案 A:revert merge commit(安全,适合公共分支)
# -m 1 表示保留第一个父(被合并进来的 main)
git revert -m 1 merge-commit-hash
# 方案 B:reset --hard 到合并前(适合个人分支)
git reset --hard commit-before-merge
问题 2:不小心提交了密钥,如何从历史中彻底删除
密钥一旦提交并推送,应立即撤销密钥!即使从历史中删除,也无法确保没有人缓存了那段历史。删除历史只是减少曝光风险,不能代替撤销密钥。
# 使用 git-filter-repo(推荐,比 filter-branch 快得多)
pip install git-filter-repo
# 从所有历史中删除含有密钥的文件
git filter-repo --path .env --invert-paths
# 或者替换文件中的敏感内容
git filter-repo --replace-text replacements.txt
# replacements.txt 格式:
# literal:my-secret-key==>REDACTED
# 处理完后强制推送所有分支(会改写历史!)
git push --force --all
git push --force --tags
# 通知所有协作者重新 clone 仓库
BFG Repo Cleaner 是另一个流行工具(bfg.jar),比 git filter-repo 更简单但功能略少。命令:java -jar bfg.jar --delete-files .env。
问题 3:大文件误入仓库
将大型二进制文件(视频、数据集、编译产物)提交到 Git 仓库会导致仓库体积膨胀,克隆速度极慢。
# 使用 git-filter-repo 从历史中移除大文件
git filter-repo --strip-blobs-bigger-than 10M
# 或者使用 Git LFS(Large File Storage)管理大文件
# Git LFS 用指针文件替代实际的大文件,实际内容存储在 LFS 服务器
# 安装 Git LFS
git lfs install
# 追踪特定类型的大文件
git lfs track "*.psd"
git lfs track "*.mp4"
git lfs track "datasets/**"
# 必须将 .gitattributes 文件提交到仓库
git add .gitattributes
git commit -m "chore: configure Git LFS for large files"
# 之后 add/commit/push 大文件,它们会自动走 LFS 通道
问题 4:仓库太大,克隆太慢
# 浅克隆(只拉取最近 N 次提交)
git clone --depth 1 https://github.com/user/large-repo.git
# 只克隆指定分支
git clone --single-branch --branch main URL
# 部分克隆(Git 2.19+,只下载需要的对象)
git clone --filter=blob:none URL # 不下载 blob,按需加载
学习之旅完成!你已经学习了从 Git 基础到内部原理的全部核心内容。Git 是一门需要实践的技艺——从今天起,在每个项目中有意识地运用这些知识:规范 commit 信息、合理使用分支、通过 PR 协作。熟练之后,你会发现 Git 不只是工具,而是一种思考代码演进的方式。