团队协作里出的问题,八成不是 Git 命令不会用,而是配置没立规矩、提交历史乱成一锅粥、出事了找不到是哪次提交引入的。所以这里不重复 git add git commit 这类人尽皆知的内容,只讲实际工程里真正用得上的进阶命令和一些协作规范。
一、全局配置
新机器装完 Git,第一件事不是 clone,是把全局配置定下来。我们可以通过 git config --global 一项一项的配置,也可以直接维护一份 ~/.gitconfig 文件。但一般直接编辑这个文件更加方便。
1.1 一份开箱即用的配置文件
把下面内容存到 ~/.gitconfig,按需改掉 user.name 和 user.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 = true 让 git pull 默认走 rebase 而不是 merge,历史更线性。这条争议较大,团队若强调合并轨迹可追溯就改成 false。关键是在全局定一个统一默认,别让每个人按自己习惯来,否则同一条 pull 历史里一会儿是 merge 提交一会儿是 rebase。
push.default = current 让 git push 只推当前分支,避免误推全部分支。默认的 simple 在某些场景下仍会带上有 upstream 的分支,current 更保守更安全。再配 push.autoSetupRemote = true,首次推送自动建好 upstream 关系,不用每次先 git push -u。这个选项在较新的 Git 版本里才有(2.40 以上确定支持,更老的版本可以先 git push -u 顶着)。
fetch.prune = true 让 git 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)。
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 误操作。
cleanup 和 recent 这两个尤其值得配。前者清理已合并到 main 的本地分支,解决”本地堆了几十个已合并分支”的洁癖问题;后者显示最近改过的分支,解决”我刚才在哪个分支来着”的频繁切换问题。
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-jobcommit.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 在中间多加一个 ||||||| 块显示共同祖先的原文;zdiff3 在 diff3 的基础上,把冲突区两侧那些本来相同的行移出冲突标记,让冲突区域更小、焦点更集中在真正分歧的地方。看到共同祖先能让你明白双方各自从什么起点改的,手动解冲突时不容易误删。
branch.sort = -committerdate 让 git branch 按最近提交时间倒序排列,最近在用的分支排前面,找分支不用从头翻到尾。tag.sort = version:refname 按语义版本排序,v1.10.0 会正确排在 v1.9.0 之后,而不是按字符串排成 v1.10.0 < v1.9.0。log.date = iso 把日志日期固定成 ISO 格式,比默认的相对时间更适合做问题排查时的精确对账。
includeIf 是多身份场景的关键。公司仓库和个人仓库混在一台机器上时,全局 user.email 只能有一个,提交身份会串。includeIf "gitdir:~/work/job/" 规定只要在指定目录下的仓库,就额外加载 ~/.gitconfig-job,里面覆盖成公司的 name 和 email。这样不同目录自动用不同身份提交,不用每次切仓库都改配置。
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 featuregit 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 formpick def456 WIP: work on validationpick ghi789 fix: typo
# 将 pick 改成下面这些操作:# pick 保留# reword 保留提交但改 message# squash 合并到上一个提交,message 也合并# fixup 合并到上一个提交,丢弃这条 message# drop 删除fixup 比 squash 更常用:开发时频繁提交 WIP,最后整理时把那些 WIP 全标成 fixup,message 都不要,只留一个干净的 feature 提交。
变基的黄金法则:不要对已经推到共享分支的提交做变基。变基会改写提交哈希,同事拉下来会冲突。如果必须变基已推送的提交,先通知团队,推送时用 --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 startgit 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.0git 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 featuregit stash pop # 把暂存的改动放回来stash 不是长期存储,别把它当分支用。放进去的东西如果当天没拿出来,多半已经忘了里面是什么,不如直接建个临时分支提交上去。
四、标签与版本管理规范
发布不是 git push 完事,得打标签。标签是版本的可寻址锚点,线上出问题回滚就是 checkout 到某个 tag。
4.1 语义化版本
版本号 MAJOR.MINOR.PATCH 三段:
- MAJOR:不兼容的接口变更
- MINOR:向后兼容的新功能
- PATCH:向后兼容的 bug 修复
4.2 标签命名规范
# 附注标签(推荐),带提交者信息、日期、messagegit 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.1、v1.2.0-beta.2 - 补丁:
v1.2.1
标签不会随 git push 自动推送。发布后要显式推标签:git push origin v1.2.0 或 git 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/repogit 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 盯着强得多。
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






