前面五篇讲的都是单人在自己电脑上操作,这一篇讲的是一群人怎么在同一个项目上协作,以及代码怎么从大家的电脑汇总成一个对外发布的版本。
Fork、Clone、Pull Request
这三个词经常一起出现,先把关系理清楚:
| 动作 | 做什么 | 在哪操作 |
|---|---|---|
| Fork | 把别人的仓库复制一份到我们自己的账号下 | GitHub 网页上点一下 |
| Clone | 把仓库下载到本地 | git clone <url> |
| Pull Request | 请求把我们的改动合进原仓库 | GitHub 网页上发起 |
Fork 和 Clone 的区别在于目标位置:Fork 是复制到我们的 GitHub 账号(还是远程),Clone 是下载到我们自己的电脑(本地)。
什么时候用 Fork
开源项目:我们没有原仓库的写权限,只能 Fork 一份到自己账号下,改完发 PR 请求维护者合并。
公司内部项目:通常大家都有权限,直接在同一个仓库上建分支就行,不需要 Fork。两种流程的差别大概是:
text
Fork 模式(开源) 分支模式(团队内部)
Fork 原仓库 → 你的账号 在有权限的仓库直接建分支
↓ clone ↓ clone
本地改 → push 到你的 Fork 本地改 → push 到仓库的分支
↓ ↓
发 PR:你的分支 → 原仓库的 main 发 PR:你的分支 → main
一次完整的协作流程
以团队内部的分支模式为例,从接到任务到代码合入主干:
shell
# 1. 同步最新的主干
git switch main
git pull
# 2. 建一个功能分支(从最新的 main 上分)
git switch -c feature/user-validation
# 3. 开发,随时提交
git add .
git commit -m "添加用户名格式校验"
# 4. 期间主干有更新,同步一下(保持自己的分支不落后)
git fetch origin
git rebase origin/main
# 5. 推送到远程(第一次要 -u)
git push -u origin feature/user-validation
# 6. 在平台上发起 PR / MR
# 选好源分支和目标分支,写清说明,指定 reviewer
# 7. 根据 review 意见改代码(如果改的是同一个提交,用 amend)
git add .
git commit --amend --no-edit
git push --force-with-lease
# 如果是新增的改动,就正常提交再 push,不用强推
# 8. 合并之后再清理分支
git switch main
git pull
git branch -d feature/user-validation
git push origin --delete feature/user-validation
几个实践上的注意点:
- 功能分支要短命。活了一周以上的分支,rebase 和合并都会很痛苦,尽量拆小、尽快合
- PR 别太大。几百行的 PR 大家愿意认真看,几千行的 PR 一般只会点个 approve
- 合并前先同步主干 ,让自己的分支包含最新的
main,CI 才会跑在真实状态上 - 第 7 步用的是
--force-with-lease而不是--force,原因在第 4 篇的强推边界里讲过
四种工作流的对比
Git Flow
分支最多,规矩也最严:
text
main 只放已发布的版本
hotfix/* 紧急修复,从 main 分,修完合回 main 和 develop
release/* 发布准备,从 develop 分
develop 集成分支,功能都往这里合
feature/* 功能分支,从 develop 分
好处是发布流程很清晰,适合有明确版本号和发布周期的产品;坏处是复杂,日常开发要在 5 种分支之间来回切,在持续交付的场景下这套流程太重了。所以它现在基本只在传统软件、或者需要同时维护多个历史版本的项目里用。
GitHub Flow
极简,只有一条规则:main 永远可发布,其它都是短命的功能分支。
text
main 随时可部署
feature/* 从 main 分,改完发 PR,合并回 main
合并之后立刻部署,适合持续部署的 Web 服务。
- 优点:简单,分支少,和自动化部署天然契合
- 缺点:没有处理"多版本并行维护"的机制,也不区分发布和开发
GitLab Flow
可以看成 GitHub Flow 的加强版,引入了环境分支来解决部署问题:
text
main → pre-production → production
代码先合进 main,再往上游的环境分支合并,每个环境分支对应一个部署环境。也可以配合 release 分支做版本管理。
它比 GitHub Flow 多了一层环境管控,又比 Git Flow 简单,适合需要区分多套环境的团队。
主干开发(Trunk Based)
所有人直接在主干上开发,或者只开存活不超过一天的超短分支。
它的前提是有完善的自动化测试和 CI,否则主干随时可能是坏的。通常还会配合特性开关(feature flag):代码合了但功能没开,等真正要上线才打开开关。Google、Facebook 这类公司用的就是这套。
怎么选
| 工作流 | 分支数量 | 复杂度 | 适合 |
|---|---|---|---|
| Git Flow | 5 种 | 高 | 有明确版本发布周期的产品 |
| GitHub Flow | 2 种 | 低 | 持续部署的 Web 服务 |
| GitLab Flow | 2~3 种 | 中 | 需要多环境管控 |
| 主干开发 | 1 种 | 低(但对 CI 要求高) | 测试覆盖率高、追求快速迭代 |
一个粗略的判断标准:部署越频繁,分支就该越少。每天发好几次的团队用 Git Flow 会把自己累死,一年发两次版的软件用主干开发也不现实。
tag 和版本发布
tag 是给某个提交打的固定标记,常用来标记版本。
轻量标签和附注标签
shell
git tag v1.0.0 # 轻量标签
git tag -a v1.0.0 -m "第一个正式版本" # 附注标签
| 轻量标签 | 附注标签 | |
|---|---|---|
| 是什么 | 就是一个指向提交的指针 | 一个独立的对象,含打标签的人、时间、说明 |
| 能签名吗 | 不能 | 能,-s 参数可以做 GPG 签名 |
| 命令 | git tag <名字> |
git tag -a <名字> -m "说明" |
| 建议 | 临时标记可以用 | 正式发版用这个 |
shell
git tag # 列出所有标签
git tag -l "v1.*" # 按模式过滤
git show v1.0.0 # 看某个标签的信息
git tag -d v1.0.0 # 删除本地标签
推送标签
git push 默认不会推送标签,这一条很容易踩:
shell
git push origin v1.0.0 # 推一个
git push origin --tags # 推所有
git push origin --follow-tags # 只推附注标签(更安全)
删除远程标签:
shell
git push origin --delete v1.0.0
检出某个标签
tag 指向的是固定提交,不能像分支那样移动,所以检出标签会进入游离 HEAD:
shell
git switch --detach v1.0.0
想基于某个版本修 bug,应该从标签建一个分支出来:
shell
git switch -c hotfix/1.0.1 v1.0.0
语义化版本
约定是 MAJOR.MINOR.PATCH:
| 位 | 什么时候加 |
|---|---|
| MAJOR | 有不兼容的改动 |
| MINOR | 新增功能,向后兼容 |
| PATCH | 修 bug,向后兼容 |
比如 1.4.2 修个 bug 就是 1.4.3,加功能是 1.5.0,改了不兼容的接口才是 2.0.0。加上预发布标识就是 2.0.0-beta.1 这种形式。
.gitignore 的常见坑
已经被跟踪的文件,加进 .gitignore 没用
这是最常见的困惑:明明把 config.local 加进 .gitignore 了,git status 里还是能看到它。
原因是 .gitignore 只对"未被跟踪的文件"生效 。一个文件一旦被 git add 过,Git 就一直在跟踪它,后面写进 .gitignore 也管不着。
正确的做法是先把它从跟踪里移除(保留本地文件):
shell
git rm --cached config.local # 只从 Git 里移除,磁盘上的文件还在
git commit -m "停止跟踪本地配置文件"
注意别漏了 --cached,直接 git rm config.local 会把本地文件也一起删掉。
几个写法上的细节
text
node_modules/ # 目录要带斜杠
*.log # 通配符
!important.log # ! 表示不忽略(取反),必须写在忽略规则后面才生效
/build # 开头的斜杠表示只匹配仓库根目录下的 build
**/temp # 任意层级的 temp 目录
如果发现规则没生效,别自己猜,用这条命令直接问 Git:
shell
git check-ignore -v config.local # 看是哪条规则把它忽略了
它会明确告诉我们匹配到的是哪个文件的哪一行,比自己排查快得多。
另外两个容易忽略的点:.gitignore 本身应该被提交 ,这样团队所有人共享同一套规则;而个人的偏好(比如 .idea/、.vscode/)不该写进项目的 .gitignore,应该放到全局配置里:
shell
git config --global core.excludesFile ~/.gitignore_global
敏感信息误提交了怎么办
分三步,而且顺序不能反。
第一步:立刻作废这个凭据
这一步比清理历史重要得多。
只要提交推送到了远程,就默认它已经泄露了。哪怕是私有仓库、哪怕我们一分钟内就删掉了,都要当作泄露处理,因为平台可能已经缓存、索引、备份过,CI/CD 系统可能已经拉取过,有权限的同事可能已经拉到了本地,甚至可能已经有人看到了通知邮件。
所以正确的第一反应是:去对应的平台把密钥、密码、token 作废并重新生成。数据库密码就改密码,云服务的 AK 就去控制台禁用旧的。清理历史是第二步的事。
第二步:从历史里移除
作废之后,如果还想把内容从 Git 历史里清掉(比如开源仓库不希望留个把柄),可以用 git filter-repo:
shell
# 安装:pip install git-filter-repo
git filter-repo --path config/secret.yml --invert-paths
或者替换文件里的某段文本:
shell
git filter-repo --replace-text expressions.txt
expressions.txt 里写替换规则:
text
password123==>REMOVED
sk-xxxxx==>REMOVED
早期用的是
git filter-branch,但它慢、容易出错,Git 官方已经明确不推荐使用 ,改用git filter-repo。BFG Repo-Cleaner 是另一个选择,Java 写的,速度很快。
清理完历史之后,本地所有提交的哈希都变了,需要强推:
shell
git push --force --all
git push --force --tags
第三步:通知协作者
强推之后,别人本地的仓库和历史就对不上了。标准做法是让所有人重新克隆:
shell
git clone <url> # 重新拉一份,最省事
不重新克隆的话,他们那些旧提交还在本地,一不小心又推回远程,等于白清理。
这也是为什么"清理历史"是一件很麻烦的事:它是一次影响全团队的操作。所以更省事的做法是从一开始就防住:
.gitignore里提前写好*.env、*.pem、config.local这类模式- 用
git add -p而不是git add .,提交前过一眼到底加了什么- 装个
git-secrets,或者用平台的 secret scanning,让推送时直接被拦下来