【Git 权威指南】“和声”+“协同模型"两章的精炼合并版:远程协议、推送与冲突解决、里程碑共享、分支协同、工作流模型、共用代码(submodule / subtree)。
本文沿用书中
master作为默认分支名;Git 2.28+ 默认改为main,行为完全一致。
远程协议与版本库
协议选择
| 协议 | 用途 | 现状 |
|---|---|---|
| SSH | 推 + 拉,需公钥认证 | 协作首选 |
| HTTPS | 推 + 拉,凭证由 helper 缓存 | 网络受限环境首选;个人项目常用 |
| 本地 | file:// 或裸路径 | 备份 / 镜像 |
git:// | 只读匿名协议 | 已弃用,GitHub 2022 年关停 |
| FTP / RSYNC | — | 已弃用,忘了它们 |
ssh://[user@]example.com[:port]/path/to/repo.git
[user@]example.com:path/to/repo.git # SSH 简写
https://example.com/path/to/repo.git
远程版本库管理
git remote -v # 列出 fetch/push URL
git remote add <name> <url> # 注册
git remote set-url <name> <url> # 改 URL
git remote set-url --push <name> <url> # 单独改 push URL(读写分离)
git remote rename <old> <new>
git remote rm <name>
git remote update # 拉取所有 remote 的更新
git config remote.<name>.skipDefaultUpdate true # 排除某个 remote
fetch / pull 的关系
git pull = git fetch + git merge # 默认
git pull --rebase = git fetch + git rebase
习惯用 rebase 整理历史的人,全局开启:
git config --global pull.rebase true
git config --global branch.autosetuprebase always # 新追踪分支自动设 rebase
pull.rebase=true是 2.6+ 后协同的现代默认姿势,避免git pull平白塞进一堆Merge branch 'main'的噪声。
推送与冲突解决
推送基础
git push # 推当前分支到追踪的上游
git push -u origin <branch> # 首次推并建立 upstream
git push origin dev:master # 显式 refspec:本地 dev → 远端 master
非快进推送:force 的现代姿势
git push -f # 危险:会盖掉别人推的东西
git push --force-with-lease # 推荐:只在远端确实是你看到的版本时才覆盖
git push --force-with-lease --force-if-includes # 2.30+ 更严格:还要求你 fetch 过最新
--force-with-lease是-f的安全替代品。它检测远端是否被别人推了新东西,是就拒推 —— 多人协作改 rebase 后强推必备。
服务端禁止非快进推送(用于受保护分支):
git --git-dir=/path/to/shared.git config receive.denyNonFastForwards true
git --git-dir=/path/to/shared.git config receive.denyDeletes true
真正的权限控制还是交给 GitLab / GitHub 服务端的 protected branch / push rule,这里只是底层兜底。
冲突解决的实际流程
合并 / 变基遇冲突时,冲突文件长这样:
<<<<<<< HEAD
我这边的版本
=======
对方分支的版本
>>>>>>> feature
工作流程:
git status # 列出 "both modified" 的文件
# 编辑器里手动取舍,或调工具
git mergetool # 调用 vimdiff / opendiff / vscode 等
git diff --check # 检查是否还有遗留 <<<<<<<
git add <files>
git commit # merge 用 commit;rebase 用 git rebase --continue
git merge --abort # 想反悔
git rebase --abort
合并策略关键陷阱:-X ours vs -s ours
写法相近,效果完全不同:
| 命令 | 行为 |
|---|---|
git merge -X ours <branch> | recursive 策略 + ours 选项。只在冲突 hunk 上偏向本方,非冲突的对方改动正常合入 |
git merge -s ours <branch> | ours 策略。对方分支整个内容全部丢弃,只记录一笔合并提交 |
后者常用于"我宣告这个分支已废弃但要在历史上标一笔”。误用会神不知鬼不觉丢光对方所有改动。
合并方式:fast-forward 还是 no-ff
git merge feature # 默认:如果可以 ff 就 ff,分支痕迹消失
git merge --no-ff feature # 强制产生合并提交,保留分支拓扑
git config --global merge.ff false
团队偏好"看得见每个 feature 分支"的,统一关掉 ff;偏好线性历史的,反过来用 rebase + ff。
里程碑共享
tag 不会随分支自动推
git push origin v1.0 # 推单个 tag
git push origin --tags # 推所有本地 tag
git push origin refs/tags/v* # refspec 通配
git push origin :refs/tags/v1.0 # 删除远端 tag
git fetch --tags # 主动拉所有 tag
默认
git fetch只会带回与已 fetch 的 commit 可达的 tag,孤立 tag 不会自动同步。 远端有同名 tag 而本地没有时会建立;本地已存在则不会覆盖,需要git fetch --tags -f。
命名:语义化版本
Semantic Versioning 2.0 —— MAJOR.MINOR.PATCH:
- MAJOR 不兼容改动
- MINOR 向下兼容的新功能
- PATCH 向下兼容的修复
预发布 / 编译信息:1.0.0-beta.1+20251010。
分支协同
本地分支基本操作
git branch # 列本地
git branch -r # 列远程跟踪
git branch -a # 全部
git branch -vv # 含 upstream 关系
git switch <branch> # 切分支(2.23+,比 checkout 语义清晰)
git switch -c <new> [<start>] # 创建并切换
git branch -d <branch> # 安全删(未合并会拒绝)
git branch -D <branch> # 强删
git branch -m <old> <new> # 重命名
远程跟踪分支
origin/main 这种引用住在 refs/remotes/ 命名空间,是 fetch 后服务端最新状态的本地快照,不是真正的分支 —— 不能直接 commit 到它上面。
git switch hello-1.x # DWIM:本地无同名分支时,自动基于 origin/hello-1.x 建并跟踪
git switch -c local-feat origin/feat # 显式基于远程分支建跟踪分支
git branch --set-upstream-to=origin/main main # 给已存在分支补设 upstream
git fetch --prune # 清理远端已删的跟踪分支
DWIM (Do What I Mean):
git switch <name>时若本地无同名分支但远端恰有一个origin/<name>,Git 会自动创建本地分支并设 upstream。多个 remote 都有时不触发,避免歧义。
rebase 整理本地分支
dev 基于老的 master,希望以最新 master 为基底再推送:
git switch dev
git fetch origin
git rebase origin/master
# 冲突 → 解决 → git add → git rebase --continue
git push --force-with-lease # 历史被重写,必须 force(用 with-lease 才安全)
共享分支 rebase 后必须
--force-with-lease,且最好提前在团队里 broadcast 一下。 个人独享分支随便 rebase;团队共享主干永远 merge 不 rebase。
git worktree:并行多分支
不再需要"克隆好几份仓库"或者 git stash 来回切:
git worktree add ../proj-hotfix hotfix # 在隔壁目录开一个挂在 hotfix 分支的工作区
git worktree list
git worktree remove ../proj-hotfix
每个 worktree 共享同一 .git/objects,但有独立工作区和独立 HEAD。Code review、跑长测试、临时改 hotfix 都不打断主工作区。
分支安全配置
| 配置 | 作用 |
|---|---|
core.logallrefupdates=true | 给每条分支记 reflog(带工作区的库默认开;裸库默认关,建议手动打开) |
receive.denyNonFastForwards=true | 服务端拒绝非快进推送 |
receive.denyDeletes=true | 服务端拒绝删除分支 |
工作流模型
经典三种,选用按团队规模和发布节奏:
| 模型 | 适用 | 关键约束 |
|---|---|---|
| Trunk-based | 持续部署、CI 强的小到中团队 | 所有人直推 main(或短命 feature 分支)+ feature flag |
| GitHub Flow | SaaS 持续交付,最常见 | main 永远可部署,feature 分支走 PR → 合并 → 部署 |
| GitFlow | 有明确版本发布周期的产品(客户端、SDK) | main + develop + release/* + hotfix/*,重 |
Topgit、卖主分支 (Vendor Branch) 是 2010 年前的产物,现代项目用包管理器(npm / composer / cargo)+ submodule / subtree 覆盖了这些场景,可忽略。
共用代码:submodule vs subtree
两者解决同一问题(在一个仓库里嵌套引用另一个仓库),但取舍完全不同:
| 维度 | submodule | subtree |
|---|---|---|
| 子项目代码物理位置 | 独立 .git,靠 commit 指针引用 | 直接嵌入主仓库 |
| 克隆体验 | 需要 --recurse-submodules 或 init | 克隆主仓即拿全 |
| 主仓体积 | 小(只存指针) | 大(含子项目历史) |
| 协作者上手成本 | 高(需理解 submodule + detached HEAD) | 低(看起来就是普通目录) |
| 双向同步(向上游回推) | 直接在子仓库 push 即可 | 麻烦(git subtree push 提取相关 commit) |
| 适合 | 真正独立的依赖,少改 | 共享代码模板、内嵌工具脚本 |
submodule 标准姿势
git submodule add <url> <path> # 添加;生成 .gitmodules
git clone --recurse-submodules <url> # 一次克隆主仓 + 所有子模块(推荐)
git submodule update --init --recursive # 已克隆的仓库补拉子模块
git submodule update --remote # 把子模块拉到其追踪分支的最新
git submodule status
submodule 头号陷阱:每个 submodule 始终处于 detached HEAD。要在子模块里改东西,先
git -C <path> switch <branch>切到分支,提交后先推子仓库,再推主仓库 —— 顺序反了别人会拉不到对应 commit。
subtree 标准姿势
# 引入:把 S 项目作为主仓的 lib/s 子目录
git subtree add --prefix=lib/s <s-url> main --squash
# 拉上游更新
git subtree pull --prefix=lib/s <s-url> main --squash
# 把主仓内对 lib/s 的改动回推到 S 项目
git subtree push --prefix=lib/s <s-url> feature-branch
--squash 把上游历史压成一笔 commit,主仓 git log 干净,代价是主仓再也看不到子项目的提交图。统一加或统一不加,混用会撞合并冲突。
从已有项目剥离子模块
# 1. 从 P 项目里抽出 lib/s 的所有 commit,存到临时分支
git subtree split -P lib/s -b extracted
# 2. 创建独立的 S 仓库
mkdir ../s && cd ../s && git init
git pull ../P extracted
git remote add origin <s-url>
git push -u origin main
# 3. P 项目里删掉原目录,改用 subtree 引用
cd ../P
git rm -rf lib/s && git commit -m "Extract lib/s to standalone repo"
git subtree add --prefix=lib/s <s-url> main --squash
一个常见用途:把构建产物推到 GitHub Pages
git subtree push --prefix=dist origin gh-pages
补丁文件交互
少数场景(邮件协作 / 没有共同远端 / 跨网隔离环境)会用补丁文件传递提交:
git format-patch -s HEAD~3..HEAD # 把最近 3 个提交导成 3 个 *.patch 文件,-s 加 Signed-off-by
git format-patch -s origin/main..HEAD # 导出 fork 后所有新增提交
git am < user1.mbox # 收方导入,保留 author 与 message
git apply foo.patch # 仅打补丁,不产生 commit,author 信息丢失
git apply --check foo.patch # 试跑,不真改
Linux 内核、Git 本身都还在用邮件 + git format-patch / git am 流程;日常 GitHub / GitLab 协作用 PR / MR 即可。
References
– EOF –