mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
3023 字
8 分钟
Git 进阶与团队协作
2020-03-18

团队协作里出的问题,八成不是 Git 命令不会用,而是配置没立规矩、提交历史乱成一锅粥、出事了找不到是哪次提交引入的。所以这里不重复 git add git commit 这类人尽皆知的内容,只讲实际工程里真正用得上的进阶命令和一些协作规范。

一、全局配置#

新机器装完 Git,第一件事不是 clone,是把全局配置定下来。我们可以通过 git config --global 一项一项的配置,也可以直接维护一份 ~/.gitconfig 文件。但一般直接编辑这个文件更加方便。

1.1 一份开箱即用的配置文件#

把下面内容存到 ~/.gitconfig,按需改掉 user.nameuser.email 即可:

[user]
name = your-name
email = your-name@example.com
[init]
defaultBranch = main
[pull]
rebase = true
[push]
default = current
autoSetupRemote = true ; 首次 push 自动建 upstream,省掉 -u
[fetch]
prune = true ; fetch 时自动清理远程已删除的分支引用
[rebase]
autoStash = true ; 变基前自动 stash,结束自动 pop
[rerere]
enabled = true ; 记住手动解冲突的方式,下次同类冲突自动套用
[core]
quotepath = false
autocrlf = input ; Linux/macOS 用 input,Windows 改成 true
[commit]
template = ~/.git-commit-template
[alias]
lg = log --oneline --graph --decorate --all
amend = commit --amend --no-edit
pushf = push --force-with-lease
undo = reset --hard HEAD
cleanup = !git branch --merged main | grep -v 'main$' | xargs git branch -d
recent = for-each-ref --sort=-committerdate --count=10 --format='%(committerdate:relative) %(refname:short)' refs/heads/

下面逐块说明每项解决什么问题,为什么要这么设。

1.2 身份与默认行为#

[user] 的 name 和 email 会写进每次提交,团队协作里这是追溯责任的关键,别用默认的 root 或机器名。init.defaultBranch = main 把新仓库的初始分支从老版本的 master 改成 main,和 GitHub 现在的默认对齐。

pull.rebase = truegit pull 默认走 rebase 而不是 merge,历史更线性。这条争议较大,团队若强调合并轨迹可追溯就改成 false。关键是在全局定一个统一默认,别让每个人按自己习惯来,否则同一条 pull 历史里一会儿是 merge 提交一会儿是 rebase。

push.default = currentgit push 只推当前分支,避免误推全部分支。默认的 simple 在某些场景下仍会带上有 upstream 的分支,current 更保守更安全。再配 push.autoSetupRemote = true,首次推送自动建好 upstream 关系,不用每次先 git push -u。这个选项在较新的 Git 版本里才有(2.40 以上确定支持,更老的版本可以先 git push -u 顶着)。

fetch.prune = truegit fetch 顺手清掉远程已删除分支的本地引用,否则本地会堆一堆指向已删分支的幽灵引用。rebase.autoStash = true 在变基前自动 stash 未提交改动、结束后自动 pop,配合上面的 pull.rebase,工作区不干净也能直接 pull。rerere.enabled = true(reuse recorded resolution)会记住你手动解过的冲突,下次遇到同一处冲突自动套用上次的方案,长跑的 rebase 能省掉大量重复劳动。

1.3 换行符与提交模板#

core.quotepath = false 让中文文件名在 git status 里正常显示,不被转义成 \xxx 序列。

core.autocrlf 是跨平台协作的隐形炸弹。一个 Windows 同事用 CRLF 提交,Linux 同事拉下来改一行,diff 里整文件标红,code review 根本没法看。Linux/macOS 设 input(提交时转 LF,检出不转),Windows 设 true(提交转 LF,检出转 CRLF)。

Important

core.autocrlf 全局配置只是兜底,更可靠的做法是在仓库根目录写 .gitattributes,强制约定换行符,因为 .gitattributes 跟着仓库走,每个协作者都生效。

commit.template 指向一个提交模板文件,git commit 不带 -m 时会加载它。模板内容通常是 Conventional Commits 的格式提示:

# <type>(<scope>): <subject>
#
# 类型:feat / fix / docs / refactor / perf / test / chore
# subject 用祈使句、现在时,首字母小写,结尾不加句号
# 行首 # 开头的是注释,不会被写入提交信息

1.4 有价值的别名#

[alias] 段把团队约定固化成命令。注意 cleanup 前面有个 !,表示这是 shell 命令而不是 Git 子命令的简写,Git 会把它当 shell 脚本执行。

lg 是一行带分支图的日志,看历史最顺手,比敲一长串 log --oneline --graph... 强得多。amend 改上一次提交,漏文件时不用 reset 重来。pushf 是强制推送的安全版本,用 --force-with-lease 杜绝 git push -f 误操作。

cleanuprecent 这两个尤其值得配。前者清理已合并到 main 的本地分支,解决”本地堆了几十个已合并分支”的洁癖问题;后者显示最近改过的分支,解决”我刚才在哪个分支来着”的频繁切换问题。

Warning

pushf--force-with-lease 而不是 -f。前者在远程被别人推过新提交时会拒绝推送,后者直接覆盖,会吃掉同事的提交。

1.5 进阶配置:签名、多身份与冲突样式#

上面那份是开箱即用的基础配置。真正长期在团队里工作,通常还会再配几项,集中在签名、多身份和冲突可读性上。

[user]
signingkey = ~/.ssh/signing_key.pub
[gpg]
format = ssh
[commit]
gpgsign = true
[merge]
conflictStyle = zdiff3
[branch]
sort = -committerdate
[tag]
sort = version:refname
[log]
date = iso
[includeIf "gitdir:~/work/job/"]
path = ~/.gitconfig-job

commit.gpgsign = true 让每次提交自动签名,gpg.format = ssh 把签名方式从传统 GPG 切到 SSH key(GitHub 2022 年起官方支持)。用 SSH key 签名的好处是大多数开发机本来就有 SSH key,不用再单独维护 GPG 钥匙环,user.signingkey 指向对应的公钥即可。签名后 GitHub 上提交会显示 Verified 标记,能证明提交确实来自你本人而非冒名。

merge.conflictStyle = zdiff3 比默认的 merge 和更老的 diff3 都好用。默认的 merge 只显示 <<<<<<< 你这侧、=======>>>>>>> 对侧两块;diff3 在中间多加一个 ||||||| 块显示共同祖先的原文;zdiff3diff3 的基础上,把冲突区两侧那些本来相同的行移出冲突标记,让冲突区域更小、焦点更集中在真正分歧的地方。看到共同祖先能让你明白双方各自从什么起点改的,手动解冲突时不容易误删。

branch.sort = -committerdategit branch 按最近提交时间倒序排列,最近在用的分支排前面,找分支不用从头翻到尾。tag.sort = version:refname 按语义版本排序,v1.10.0 会正确排在 v1.9.0 之后,而不是按字符串排成 v1.10.0 < v1.9.0log.date = iso 把日志日期固定成 ISO 格式,比默认的相对时间更适合做问题排查时的精确对账。

includeIf 是多身份场景的关键。公司仓库和个人仓库混在一台机器上时,全局 user.email 只能有一个,提交身份会串。includeIf "gitdir:~/work/job/" 规定只要在指定目录下的仓库,就额外加载 ~/.gitconfig-job,里面覆盖成公司的 name 和 email。这样不同目录自动用不同身份提交,不用每次切仓库都改配置。

Tip

GitHub 的凭据如果用 gh CLI 管理,跑一次 gh auth setup-git 就会自动把 credential 段写进 ~/.gitconfig,指向 gh auth git-credential。不用手动维护 token,也不用担心 token 过期。

二、合并策略与变基#

2.1 三种合并方式的取舍#

Git 合并不是只有 git merge,选哪种直接影响历史可读性。

# Fast-Forward:main 没新提交时,main 指针直接前移,历史无分叉
git merge feature
# No-Fast-Forward:即使能 FF 也强制建合并提交,保留分支存在过的痕迹
git merge --no-ff feature
# Squash:把 feature 上的多个提交压成一个再合入
git merge --squash feature
git commit -m "feat: add user auth"

实战经验:

  • 功能分支合入 main 用 --no-ff,历史里能看到”这个功能是在哪个分支上做的”,回溯方便。
  • 一个分支上挤了十几个 WIP 提交、合进来会污染 main 的,用 --squash 压成一个干净提交。
  • 想要完全线性历史,先 rebase 再 FF 合并,配合 pushf 推上去。

2.2 交互式变基#

交互式变基是整理提交历史的核武器, squash 多个提交、改写 message、调整提交顺序、删掉误提交,全靠它。

# 整理最近 3 个提交
git rebase -i HEAD~3

打开编辑器后会看到:

pick abc123 feat: add login form
pick def456 WIP: work on validation
pick ghi789 fix: typo
# 将 pick 改成下面这些操作:
# pick 保留
# reword 保留提交但改 message
# squash 合并到上一个提交,message 也合并
# fixup 合并到上一个提交,丢弃这条 message
# drop 删除

fixupsquash 更常用:开发时频繁提交 WIP,最后整理时把那些 WIP 全标成 fixup,message 都不要,只留一个干净的 feature 提交。

Caution

变基的黄金法则:不要对已经推到共享分支的提交做变基。变基会改写提交哈希,同事拉下来会冲突。如果必须变基已推送的提交,先通知团队,推送时用 --force-with-lease

2.3 解决冲突的两个开关#

冲突时一锅端选某一侧,用 -X 选项:

git merge -X theirs feature # 冲突处优先取 feature 的版本
git merge -X ours feature # 冲突处优先取当前分支的版本

这不是”自动解决冲突”,只对真正冲突的部分生效,不冲突的正常合并。适合明确知道某侧是正确版本的场景,比如合并自动生成的文件。不清楚就用别用,会悄悄丢代码。

三、救急命令#

3.1 用 bisect 定位 bug 引入点#

线上突然出 bug,但不知道是哪次提交引入的。手动二分太慢,git bisect 自动帮你二分。

git bisect start
git bisect bad # 当前提交是坏的
git bisect good v1.2.0 # 这个版本是好的
# Git 自动 checkout 到中间提交,你测试后告诉它好坏
git bisect good # 或 git bisect bad
# 几轮后 Git 会输出:xxx commit 是第一个坏提交
git bisect reset # 退出,回到原来的分支

如果验证好坏靠跑测试脚本,可以全自动:

git bisect start HEAD v1.2.0
git bisect run ./test-bug.sh # 脚本返回 0 表示 good,非 0 表示 bad

几百个提交里找 bug,手动要查半天,bisect 几分钟锁定。

3.2 reflog 找回误删#

git reset --hard 之后发现删错了,还没推到远程,能救。Git 的 reflog 记录了 HEAD 的所有移动轨迹,包括本地操作。

git reflog
# 输出类似:
# a1b2c3d HEAD@{0}: reset: moving to HEAD~3
# e4f5g6h HEAD@{1}: commit: feat: add feature
# ...
# 回到 reset 之前的状态
git reset --hard HEAD@{1}

reflog 默认保留 90 天,过期才会被 git gc 清理。只要没过期、没推到远程被覆盖,本地操作基本都能找回。

3.3 stash 暂存半截工作#

正在改 feature,突然要切分支修紧急 bug,又不想提交半成品。

git stash push -m "wip: auth feature" # 暂存当前改动
git checkout main
# 修 bug、提交、合并...
git checkout feature
git stash pop # 把暂存的改动放回来

stash 不是长期存储,别把它当分支用。放进去的东西如果当天没拿出来,多半已经忘了里面是什么,不如直接建个临时分支提交上去。

四、标签与版本管理规范#

发布不是 git push 完事,得打标签。标签是版本的可寻址锚点,线上出问题回滚就是 checkout 到某个 tag。

4.1 语义化版本#

版本号 MAJOR.MINOR.PATCH 三段:

  • MAJOR:不兼容的接口变更
  • MINOR:向后兼容的新功能
  • PATCH:向后兼容的 bug 修复

4.2 标签命名规范#

# 附注标签(推荐),带提交者信息、日期、message
git tag -a v1.2.0 -m "release: add user auth, fix login crash"
# 轻量标签,只是个指针,没有额外信息
git tag v1.2.0

发布用附注标签,临时标记用轻量标签。附注标签能被 GPG 签名验证来源,发布场景更可靠。

命名约定:

  • 正式版:v1.2.0
  • 预发布:v1.2.0-rc.1v1.2.0-beta.2
  • 补丁:v1.2.1
Important

标签不会随 git push 自动推送。发布后要显式推标签:git push origin v1.2.0git push origin --tags。CI/CD 的发布流水线通常以 tag 触发,忘了推 tag 等于没发布。

4.3 修改已发布的标签#

标签打错了想改,直接覆盖:

git tag -f -a v1.2.0 -m "release: corrected message"
git push origin v1.2.0 -f

但已发布给别人用的 tag 不要这么干,会破坏别人拉取的版本。打 tag 前确认好,发布后尽量不动。

五、子模块与工作区#

5.1 submodule vs subtree#

引用外部仓库有两条路。

submodule 把外部仓库作为独立引用保留,各子模块版本独立管理,适合引用第三方库且需要跟踪其版本。代价是克隆时要 --recursive,团队新人最容易踩这个坑。

subtree 把外部代码直接合并进当前仓库,克隆时无额外步骤,但更新和拆分操作复杂,适合需要深度定制外部代码、又不想让协作者感知子模块存在的场景。

# 添加子模块
git submodule add https://github.com/org/repo path/to/repo
git submodule update --init --recursive # 首次拉取子模块
# 子模块跟随远程更新
git submodule update --remote

经验:如果只是引用一个稳定的第三方库,subtree 更省心;如果要跟踪上游频繁更新且需锁定到特定 commit,submodule 更合适。

5.2 git worktree#

git worktree 让一个仓库同时检出多个分支到不同目录,免去反复切分支打断工作。

# 在另一个目录检出 feature 分支
git worktree add ../project-feature feature
# 在 main 分支做热修复的同时,另一个目录里继续 feature 开发
cd ../project-feature
# 这里工作区是 feature 分支,和主目录互不干扰
# 用完清理
git worktree remove ../project-feature

适合长跑的多个分支并行工作,比如一边在 main 修线上 bug,一边在 feature 目录继续开发新功能。比 stash 和反复 checkout 都干净。

小结#

回头看这几节,其实是一条链:全局配置立下规矩,合并策略和变基让历史保持整洁,救急命令兜住出事时的底线,标签让每个发布版本可寻址,子模块和工作区处理仓库边界的问题。

真正决定团队协作质量的,从来不是谁记得更多 Git 命令,而是这套规范有没有立起来、大家有没有一致遵守。命令随时能查,规范不统一却会让每个人按自己的习惯制造混乱。把配置和约定固化进 ~/.gitconfig.gitattributes、提交模板和分支命名规范里,让工具替团队执行纪律,比靠口头约定和 code review 盯着强得多。

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

Git 进阶与团队协作
https://blog.souloss.cn/posts/tools/git-advanced-and-team-collaboration/
作者
Souloss
发布于
2020-03-18
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时