远程协作:把你的代码推到全世界
# 第4章 · 远程协作指南
# 一、代码被覆盖
小李的优惠券系统终于写完了。前一天晚上,他把代码 push 到团队仓库,心满意足地回了家。
第二天早上 9 点,他的同事小王来上班,打开终端:
$ git push
! [rejected] master -> master (fetch first)
error: failed to push some refs to 'https://github.com/team/shop.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally.
2
3
4
5
6
小王愣住了。他昨天也改了 products.js,而且提交了。但现在 GitHub 说「远程有你看不到的代码」,拒绝了他的 push。
更糟的是,他按照提示执行了 git pull——然后终端弹出了冲突标记。他不知道怎么解决,手忙脚乱中敲了 git push --force。
五分钟后,全组人发现:小李昨晚写的优惠券系统,从远程仓库里消失了。
这不是段子。git push --force 是新人最危险的武器,而根源就在于不理解「远程协作」的机制。
# 二、远程本质
# 2.1 先纠正一个认知
很多新手以为:
「GitHub 上的仓库 = 主仓库,我本地的 = 副本」
错了。在 Git 的世界观里:
你本地的仓库 + 远程的仓库 = 两个地位平等的 Git 仓库。 只是它们之间通过网络互相传输数据。
这和第 1 章的「分布式」一脉相承:没有谁比谁更「权威」,哪个仓库该接收谁的代码,是人定的约定,不是 Git 的技术限制。
# 2.2 三个核心概念
mkdir remote-lab && cd remote-lab && git init
# 添加远程仓库链接
git remote add origin https://github.com/username/repo.git
# 看看远程配置
git remote -v
# origin https://github.com/username/repo.git (fetch)
# origin https://github.com/username/repo.git (push)
2
3
4
5
6
7
8
9
| 概念 | 含义 | 一句话 |
|---|---|---|
remote | 远程仓库的「地址簿名称」 | origin 只是一个别名,不是特殊关键字 |
origin | 惯例上的默认远程仓库名 | clone 时自动命名,你可以改 |
upstream | 上游仓库(fork 场景) | 别人的原始仓库,你 fork 了一份 |
# 可以添加多个 remote
git remote add upstream https://github.com/original/repo.git
git remote add backup https://gitlab.com/username/repo.git
# 重命名 remote
git remote rename origin github
# 删除 remote
git remote remove backup
2
3
4
5
6
7
8
9
# 三、三大命令
# 3.1 数据流动方向
你的本地仓库 远程仓库
┌─────────────┐ ┌─────────────┐
│ commit A │ │ commit A │
│ commit B │ ─── git push ────────→ │ commit B │ ← 你的 commit 过去了
│ commit C │ │ commit C │
└─────────────┘ └─────────────┘
↑ │
│ │
└──── git fetch / git pull ────────────┘
← 远程的数据拉下来了
2
3
4
5
6
7
8
9
10
关键区别:
| 命令 | 干了什么 | 改了工作区吗 |
|---|---|---|
git push | 把本地 commit 推上去 | 不关心(远程仓库没有「工作区」) |
git fetch | 把远程 commit 拉下来 | ❌ 只更新本地「远程分支引用」 |
git pull | fetch + merge 的合体 | ✅ 直接把代码合到工作区 |
# 3.2 实战:fetch 到底拉了什么?
# 首先,在 GitHub 上创建一个仓库(通过网页操作)
# 获取 URL,例如:https://github.com/你的用户名/remote-lab.git
# 关联远程
git remote add origin https://github.com/你的用户名/remote-lab.git
# 创建并推送初始内容
echo "# 远程协作实验" > README.md
git add . && git commit -m "init: README"
git push -u origin master
# -u = --set-upstream:记住 master 对应 origin/master,以后直接 git push
# 现在,去 GitHub 网页上直接编辑 README.md,加一行文字,提交
# 这样远程就比本地多了一个 commit
# 回到终端,只 fetch,不 merge
git fetch origin
# 看看发生了什么
git log --oneline --all --graph
# 输出类似:
# * cccccc (origin/master) 在 GitHub 上编辑的 ← 远程的新 commit
# * aaaaaa (HEAD -> master) init: README
# origin/master 是本地保存的「远程仓库的指针」
# fetch 只是更新了这个指针,没有动你的工作区
cat README.md
# 内容没变!fetch 不会改你的文件
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 3.3 为什么建议 fetch 再看,而不是直接 pull
# 不推荐(盲目合并):
git pull # 你可能不知道远程有什么,自动 merge 了
# 推荐(先侦察再决定):
git fetch origin # 拉最新信息,不改工作区
git log --oneline master..origin/master # 看看远程多了什么
git diff master origin/master # 看看具体改了什么
# 然后你再决定:
git merge origin/master # 确认无误再合并
# 或者
git rebase origin/master # 想保持线性历史就 rebase
2
3
4
5
6
7
8
9
10
11
💡 核心习惯:把
git pull当成git fetch+ 你手动决定 merge 还是 rebase。盲目 pull 是大多数冲突的根源。
# 四、克隆仓库
# 4.1 三种协议
# HTTPS(最简单,需要输入密码或 token)
git clone https://github.com/username/repo.git
# SSH(推荐,配置一次密钥,后续免密)
git clone git@github.com:username/repo.git
# Git 协议(只读,开放项目用)
git clone git://github.com/username/repo.git
2
3
4
5
6
7
8
# 4.2 clone 到底做了什么?
git clone https://github.com/username/repo.git my-project
# 等价于:
# mkdir my-project && cd my-project
# git init
# git remote add origin https://github.com/username/repo.git
# git fetch origin
# git checkout master (或 main)
2
3
4
5
6
7
8
克隆下来的仓库自带完整的提交历史、所有分支的引用,以及 origin 这个 remote。
# 五、PR 工作流
# 5.1 为什么需要 PR?
你可能会想:「我直接 push 到 master 不就行了?」
在一个人的项目里,可以。在团队里,这是灾难。PR 的本质是一个请求审查的流程:
你开发 feature → push 到远程 → 创建 PR → 别人审查 → 批准 → 合并到主分支
它解决了三个问题:
- 代码质量:不会有人直接把 bug push 到 master
- 知识传播:至少一个人看过你的代码,知道你在做什么
- 可追溯:每一个改动都有讨论记录存档
# 5.2 实战:一个人分饰两角,走完 PR 全流程
mkdir pr-lab && cd pr-lab && git init
echo "const app = { version: '1.0' };" > app.js
git add . && git commit -m "init: 应用骨架"
# 创建 feature 分支
git switch -c feature/添加日志
echo "function log(msg) { console.log(\`[LOG] \${msg}\`); }" >> app.js
git add . && git commit -m "feat: 添加日志函数"
echo "function error(msg) { console.error(\`[ERROR] \${msg}\`); }" >> app.js
git add . && git commit -m "feat: 添加错误日志函数"
2
3
4
5
6
7
8
9
10
11
12
13
现在 feature 分支上有 2 个 commit,推送到 GitHub:
# 在 GitHub 上创建仓库(通过网页),然后:
git remote add origin https://github.com/你的用户名/pr-lab.git
git push -u origin feature/添加日志
# 去 GitHub 网页 → Pull Requests → New Pull Request
# base: master ← compare: feature/添加日志
# 填写标题和描述,点击 Create Pull Request
2
3
4
5
6
7
一个规范的 PR 描述应该这样写:
## 做了什么
- 添加了 log() 函数,统一日志输出格式
- 添加了 error() 函数,专门处理错误日志
## 测试方法
1. 调用 log('测试消息'),确认控制台输出 [LOG] 测试消息
2. 调用 error('错误'),确认控制台输出 [ERROR] 错误
## 相关 Issue
Closes #42
2
3
4
5
6
7
8
9
10
# 5.3 Code Review + 合并
在 GitHub 上:
- 审查者查看
Files changed标签页,逐行审查 - 可以在具体行上留言、提建议
- 审查通过后,点击
Merge pull request - 选择合并策略:
Create a merge commit(推荐)或Squash and merge(压缩成一个 commit)或Rebase and merge(线性历史)
合并完成后,本地同步:
git switch master
git pull origin master # 把 PR 合并后的 master 拉下来
git branch -d feature/添加日志 # 清理本地 feature 分支
2
3
# 六、协作冲突
# 场景:小李和小王同时改 products.js
小李的操作:
mkdir team-lab && cd team-lab && git init
echo 'const products = [{ id: 1, name: "iPhone" }];' > products.js
git add . && git commit -m "init: 商品数据"
# 假设已 push 到远程
2
3
4
5
小王 clone 下来,开始改:
git clone https://github.com/team/team-lab.git wang-local
cd wang-local
# 小王给 iPhone 加了价格
sed -i '' 's/"iPhone"}/"iPhone", price: 6999}/' products.js
cat products.js
# const products = [{ id: 1, name: "iPhone", price: 6999 }];
git add . && git commit -m "feat: 添加 iPhone 价格"
git push origin master
# ✅ 成功!小王是第一个 push 的人
2
3
4
5
6
7
8
9
10
11
现在小李也想改同一行:
# 小李不知道小王已经改了
sed -i '' 's/"iPhone"}/"iPhone", stock: 100}/' products.js
git add . && git commit -m "feat: 添加 iPhone 库存"
git push origin master
# ❌ 被拒绝了!
# ! [rejected] master -> master (fetch first)
2
3
4
5
6
7
# 6.1 正确处理流程
# Step 1:先 fetch,看看远程多了什么
git fetch origin
git log --oneline master..origin/master
# 输出类似:
# abc1234 feat: 添加 iPhone 价格 ← 小王的 commit,你本地没有
# Step 2:看看远程改了什么
git diff master origin/master
# 输出:iPhone 被加了 price: 6999
# Step 3:把远程的改动合并到本地
git merge origin/master
# 如果有冲突(同一行),就需要手动解决:
# products.js 会显示:
# const products = [{ id: 1, name: "iPhone", price: 6999 }]; ← 小王的
# const products = [{ id: 1, name: "iPhone", stock: 100 }]; ← 小李的
# Step 4:手动融合——保留两边的改动:
# 编辑 products.js:
# const products = [{ id: 1, name: "iPhone", price: 6999, stock: 100 }];
git add products.js
git commit -m "merge: 合并价格和库存字段"
# Step 5:再 push
git push origin master
# ✅ 成功!
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 6.2 ⚠️ 错误的处理方式(千万别学)
# ❌ 错误1:盲目 force push
git push --force
# 后果:小王的 commit 被覆盖,price 字段永远丢失
# ❌ 错误2:git pull 看到冲突就慌,放弃解决直接 force
git pull # 冲突了
git push --force # 💀 同事的代码没了
# ❌ 错误3:reset 远程到自己的版本
git reset --hard HEAD~1
git push --force # 💀 同上
2
3
4
5
6
7
8
9
10
11
🛑 团队铁律:
git push --force需要至少两个人确认。更安全的是git push --force-with-lease——它会在覆盖之前检查远程是否有人推了新代码,如果有人推了,它会拒绝执行。
# 七、版本标签
# 7.1 标签 vs 分支
| 分支 | 标签 | |
|---|---|---|
| 本质 | 会移动的指针 | 固定的指针 |
| 作用 | 日常开发 | 标记里程碑 |
| 创建后 | 随新 commit 自动前进 | 永远钉在那个 commit 上 |
| 典型用途 | feature/xxx、hotfix/xxx | v1.0.0、v2.3.1 |
# 7.2 两种标签
mkdir tag-lab && cd tag-lab && git init
echo "v1.0 代码" > app.js
git add . && git commit -m "v1.0.0 发布"
# 轻量标签:只是一个指向 commit 的引用
git tag v1.0.0
# 附注标签:包含作者、日期、message(推荐)
git tag -a v1.0.1 -m "修复了登录崩溃的版本"
# 给过去的 commit 打标签
git log --oneline
# abc1234 v1.0.0 发布
git tag -a v0.9.0 abc1234 -m "第一个内部测试版"
# 查看标签
git tag # 列出所有
git tag -l "v1.*" # 按模式过滤
git show v1.0.1 # 查看标签详细信息
# 推送标签到远程(push 默认不推标签!)
git push origin v1.0.1 # 推送单个标签
git push origin --tags # 推送所有标签
# 删除标签
git tag -d v0.9.0 # 删除本地
git push origin :refs/tags/v0.9.0 # 删除远程(冒号语法)
# 基于标签开分支(发布后修 bug)
git checkout -b hotfix/v1.0.1 v1.0.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
💡 发布规范:每次正式发布用附注标签(
-a),message 写清楚"这个版本修了什么"。语义化版本:主版本号.次版本号.修订号(例如 v2.1.3 = 主版本 2 + 新增了 1 个功能 + 打了 3 次补丁)。
# 八、综合实战
# 场景设定
你是小李,团队要发布电商平台 v1.0.0。你负责完善商品搜索功能,小王负责修一个购物车 bug。你们通过 GitHub Flow 协作。
# 实战开始
# ==================== Phase 1:准备工作(小李视角) ====================
mkdir shop-collab && cd shop-collab && git init
echo 'const products = [
{ id: 1, name: "iPhone 15", price: 6999, category: "手机" },
{ id: 2, name: "MacBook Pro", price: 12999, category: "电脑" },
{ id: 3, name: "AirPods", price: 1299, category: "配件" }
];
function getAllProducts() {
return products;
}
' > products.js
echo 'const cart = [];
function addToCart(productId) {
const product = products.find(p => p.id === productId);
if (product) cart.push(product);
}
' > cart.js
echo '# 电商平台项目
## 安装
```bash
npm install
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 启动
npm start
' > README.md
git add . && git commit -m "init: 电商平台基础代码"
# 假设已 push 到 GitHub(https://github.com/team/shop.git)
git remote add origin https://github.com/team/shop.git git push -u origin master
# 打第一个标签
git tag -a v0.1.0 -m "项目初始化完成" git push origin v0.1.0
# ==================== Phase 2:小李开发商品搜索功能 ====================
git switch -c feature/商品搜索
echo ' function searchProducts(keyword) { return products.filter(p => p.name.includes(keyword) || p.category.includes(keyword) ); }
function searchByPrice(min, max) { return products.filter(p => p.price >= min && p.price <= max); } ' >> products.js
git add . && git commit -m "feat: 添加商品搜索功能"
echo ' function sortProducts(sortBy = "price") { return [...products].sort((a, b) => a[sortBy] - b[sortBy]); } ' >> products.js
git add . && git commit -m "feat: 添加商品排序功能"
# 整理 commit,准备提 PR
git rebase -i HEAD~2
# 把两个 squash 成一个:
# feat: 实现完整商品搜索功能(关键字搜索 + 价格区间 + 排序)
# 推送并创建 PR
git push -u origin feature/商品搜索
# 去 GitHub 创建 Pull Request
# base: master ← compare: feature/商品搜索
# 标题:feat: 实现完整商品搜索功能
# 等待审查...
# ==================== Phase 3:小王修购物车 bug ====================
# 切换到小王视角(你在另一台电脑/目录模拟)
cd .. git clone https://github.com/team/shop.git wang-shop cd wang-shop
git switch -c bugfix/购物车重复添加
# bug:addToCart 没有检查重复,同一个商品可以加多次
# 修复:
cat > cart.js << 'EOF' const cart = [];
function addToCart(productId) { const product = products.find(p => p.id === productId); if (!product) return;
const existing = cart.find(item => item.id === productId); if (existing) { existing.quantity += 1; } else { cart.push({ ...product, quantity: 1 }); } }
function removeFromCart(productId) { const idx = cart.findIndex(item => item.id === productId); if (idx !== -1) cart.splice(idx, 1); } EOF
git add . && git commit -m "fix: 修复购物车重复添加 bug,添加移除功能"
git push -u origin bugfix/购物车重复添加
# 去 GitHub 创建 PR
# ==================== Phase 4:审查 & 合并 ====================
# 审查者在 GitHub 上审查两个 PR
# 场景1:审查通过 → Merge
# 小李的 feature/商品搜索 审查通过,点击 "Merge pull request"
# 场景2:审查提了修改意见
# 审查者留言:"searchByPrice 的边界条件没处理,min 可能大于 max"
# 小李修复:
git switch feature/商品搜索
# 在 searchByPrice 开头加:
# if (min > max) [min, max] = [max, min];
git add . && git commit -m "fix: 处理搜索价格区间边界条件" git push origin feature/商品搜索
# PR 自动更新,审查者重新审查 → 通过 → Merge
# 场景3:小王的 bugfix PR 也通过了 → Merge
# ==================== Phase 5:发布 v1.0.0 ====================
# 两个 PR 都合并到 master 后,准备发布
# 小李拉最新 master
git switch master git pull origin master
# 确认所有功能都在
git log --oneline
# 输出类似:
# abcdef1 Merge pull request #2 (bugfix/购物车重复添加)
# 1234567 Merge pull request #1 (feature/商品搜索)
# 0000000 init: 电商平台基础代码
# 打发布标签
git tag -a v1.0.0 -m "v1.0.0 正式版发布:商品搜索功能上线 + 购物车 bug 修复" git push origin v1.0.0
# 🎉 发布完成!
# ==================== Phase 6:清理战场 ====================
# 删除远程已合并的分支
git push origin --delete feature/商品搜索 git push origin --delete bugfix/购物车重复添加
# 清理本地分支
git branch -d feature/商品搜索
# 如果本地没有 bugfix 分支,先 fetch 一下远程分支信息
git fetch --prune
# --prune 会清理本地已经不存在的远程分支引用
# 看看干净的状态
git branch -v
# * master abcdef1 Merge pull request #2
# 一切井然有序!
### 🎯 时间线全景
2
3
9:00 AM 10:00 AM 11:00 AM 12:00 PM
──────────────────┼────────────────────┼──────────────────┼──────────────────┼────→
│ │ │ │
小李:创建 feature │ 开发中... │ 提 PR │ 审查通过 │
小王:创建 bugfix │ 开发中... │ 提 PR │ 审查通过 │
│ │ │ │
远程 master: v0.1.0 ──────────────── 无变化 ────────── Merge #1 ──── Merge #2
│ │ │ │
│ │ │ 打 tag v1.0.0 🎉
---
## 九、本章回顾
| 我学会了什么 | 一句话总结 |
|---|---|
| 远程仓库概念 | `origin` 只是别名;远程和本地是平等的 Git 仓库 |
| fetch vs pull | `fetch` 只看不改;`pull` = fetch + merge(更激进) |
| 最佳实践 | 永远先 `fetch` → 查看 → 再决定 merge 还是 rebase |
| clone | 一键获取完整仓库 + 历史 + remote 配置 |
| PR 工作流 | 分支开发 → push → 提 PR → 审查 → 合并 → 清理分支 |
| 多人冲突 | fetch → diff → merge → 解决冲突 → commit → push |
| `push --force` 的代价 | 能覆盖别人的代码,团队协作禁用 |
| 标签 | 轻量标签只是指针;附注标签(-a)含完整信息,推荐用于发布 |
| 语义化版本 | `主.次.修订`——v2.1.3:主版本 2 + 1 个功能 + 3 次补丁 |
> 🎯 **核心习惯(三个)**:
> 1. **永远先 fetch 再决定**——不要盲目 `git pull`。
> 2. **用 PR 而不是直接 push master**——审查是质量的最后防线。
> 3. **`git push --force` 是核武器**——永远用 `--force-with-lease`,永远先确认。
---
> 🏃 **下一章预告**:基础都学完了。接下来是「特种作战」技巧——`stash` 临时保存工作现场、`cherry-pick` 精准移植一个 commit、`bisect` 二分法定位 bug。当你遇到「改了一半被叫去修 bug」「只想把某个 commit 挪到另一个分支」这些场景时,这些技巧就是你的瑞士军刀。
---
### 📎 本章涉及的命令速查
```bash
# === 远程仓库管理 ===
git remote -v # 查看所有远程仓库
git remote add <name> <url> # 添加远程
git remote rename <old> <new> # 重命名
git remote remove <name> # 删除远程
# === 推送与拉取 ===
git push <remote> <branch> # 推送
git push -u <remote> <branch> # 推送并建立追踪
git fetch <remote> # 拉取(不改工作区)
git fetch --prune # 拉取并清理无效远程引用
git pull <remote> <branch> # fetch + merge
git pull --rebase <remote> <branch> # fetch + rebase(推荐)
# === 克隆 ===
git clone <url> # HTTPS 克隆
git clone git@github.com:user/repo.git # SSH 克隆
git clone <url> <folder-name> # 指定目录名
# === 标签 ===
git tag # 列出所有标签
git tag -l "v1.*" # 按模式过滤
git tag <name> # 轻量标签
git tag -a <name> -m "message" # 附注标签(推荐)
git tag -a <name> <commit-hash> -m "message" # 给过去的 commit 打标签
git show <tag> # 查看标签详情
git push origin <tag> # 推送单个标签
git push origin --tags # 推送所有标签
git tag -d <tag> # 删除本地标签
git push origin :refs/tags/<tag> # 删除远程标签
# === 分支清理 ===
git push origin --delete <branch> # 删除远程分支
git fetch --prune # 同步删除后的远程分支状态
# === 安全推拉检查 ===
git log --oneline <branch>..<remote/branch> # 看远程比我多了什么
git log --oneline <remote/branch>..<branch> # 看我比远程多了什么
git diff <branch> <remote/branch> # 看具体差异
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70