远程协作与离线双克隆实验
本节目标
区分 remote、远程跟踪引用、upstream 与服务器端分支;理解 fetch、整合、push 的边界;不用账户、网络或真实凭据,在本机临时目录模拟两位协作者。
四个名字不要混用
| 名称 | 在哪里 | 例子 | 含义 |
|---|---|---|---|
| remote | 本地配置 | origin | 一组 URL 与 fetch/push 规则的简称 |
| 服务器分支 | 远端仓库 | 服务器上的 main | 远端当前实际引用 |
| remote-tracking ref | 本地仓库 | origin/main | 上次 fetch 后,本地对远端状态的记录 |
| upstream | 本地分支配置 | main 跟踪 origin/main | 裸 pull、status 等使用的默认对应关系 |
origin/main 不是实时网络窗口。只有 fetch、pull 或某些后台工具更新后,它才反映新的已获取状态。
Fetch、整合与 Push
服务器引用 ── fetch ──▶ 本地 remote-tracking ref
│
merge / rebase
▼
本地分支 ── push ──▶ 服务器引用git fetch获取对象并更新 remote-tracking refs,通常不改当前工作区。git merge origin/main或git rebase origin/main才把已获取历史整合进当前分支。git push请求远端移动引用;远端可因非快进、权限、保护规则或 hook 拒绝。git pull是 fetch 加后续整合,整合方式受显式选项与配置影响,不应把它理解为一个不可拆分动作。
一个可审查的协作流程
git fetch origin获取事实。- 用
git log --oneline HEAD..origin/main与git diff HEAD...origin/main检查差异。 - 在本地功能分支选择 merge 或 rebase,并解决冲突、运行测试。
- 推送功能分支,而不是直接改写受保护主分支。
- 在托管平台通过 pull request / merge request 进行评审、检查和批准。
PR、保护分支与审批是托管平台和组织治理能力,不是 Git 对象模型本身。具体按钮与权限以平台官方文档和团队规则为准。
凭据边界
- HTTPS、SSH 是不同传输与认证路径;托管平台的登录规则不由 Git 单方面统一。
- Git for Windows 通常可配合 Git Credential Manager;平台也可能要求 OAuth、PAT 或 SSH key。
- 不要把 token 写入 URL 的 userinfo 部分,也不要把它写入脚本、remote 配置、笔记或命令输出。
- 公开
git remote -v、.git/config或完整 config 输出前先检查 userinfo、代理和自定义 header。 - 本课核心实验使用本地路径 remote,不需要网络和凭据。
离线双克隆实验
安全边界
以下实验会创建提交和本地 bare remote,只能在 %TEMP% 下新建的唯一目录运行;不得在当前 vault 或真实项目中执行。本知识库建设过程只做了代码块语法检查,没有执行这些写操作。
创建并验证隔离根:
$tempRoot = [IO.Path]::GetFullPath([IO.Path]::GetTempPath())
$labPath = Join-Path $tempRoot ("git-remote-lab-" + [Guid]::NewGuid().ToString("N"))
if (Test-Path -LiteralPath $labPath) {
throw "实验目录已存在,停止。"
}
New-Item -ItemType Directory -Path $labPath | Out-Null
$resolvedLab = (Resolve-Path -LiteralPath $labPath).Path
if (-not $resolvedLab.StartsWith($tempRoot, [StringComparison]::OrdinalIgnoreCase)) {
throw "实验目录不在系统临时目录中,停止。"
}
if (@(Get-ChildItem -LiteralPath $resolvedLab -Force).Count -ne 0) {
throw "实验目录不是空目录,停止。"
}
Set-Location -LiteralPath $resolvedLab
git rev-parse --show-toplevel 2>$null
if ($LASTEXITCODE -eq 0) {
throw "实验目录已位于其他 Git worktree,停止。"
}
$resolvedLab建立 bare remote 与第一位协作者:
git init --bare --initial-branch=main remote.git
if ($LASTEXITCODE -ne 0) { throw "创建 bare remote 失败,停止。" }
git clone "$resolvedLab/remote.git" alice
if ($LASTEXITCODE -ne 0) { throw "克隆 Alice 失败,停止。" }
Set-Location -LiteralPath "$resolvedLab/alice"
git config set user.name "Alice Learner"
git config set user.email "alice@example.test"
Set-Content -LiteralPath README.md -Encoding utf8 -Value "# Offline remote lab"
git add -- README.md
git commit -m "docs: initialize offline lab"
if ($LASTEXITCODE -ne 0) { throw "Alice 初始提交失败,停止。" }
git push -u origin main
if ($LASTEXITCODE -ne 0) { throw "Alice 初始推送失败,停止。" }再克隆第二位协作者:
Set-Location -LiteralPath $resolvedLab
git clone "$resolvedLab/remote.git" bob
if ($LASTEXITCODE -ne 0) { throw "克隆 Bob 失败,停止。" }
Set-Location -LiteralPath "$resolvedLab/bob"
git config set user.name "Bob Learner"
git config set user.email "bob@example.test"
git status --short --branch
git remote先让 Alice 创建一个尚未 push 的本地提交:
Set-Location -LiteralPath "$resolvedLab/alice"
Set-Content -LiteralPath alice.md -Encoding utf8 -Value "Alice local change"
git add -- alice.md
git commit -m "docs: add Alice note"
if ($LASTEXITCODE -ne 0) { throw "Alice 本地提交失败,停止。" }再让 Bob 创建另一个提交并先 push:
Set-Location -LiteralPath "$resolvedLab/bob"
Set-Content -LiteralPath bob.md -Encoding utf8 -Value "Bob remote change"
git add -- bob.md
git commit -m "docs: add Bob note"
if ($LASTEXITCODE -ne 0) { throw "Bob 提交失败,停止。" }
git push
if ($LASTEXITCODE -ne 0) { throw "Bob 推送失败,停止。" }回到 Alice 尝试 push,应遇到 non-fast-forward 拒绝:
Set-Location -LiteralPath "$resolvedLab/alice"
git push
if ($LASTEXITCODE -eq 0) {
throw "预期的 non-fast-forward 拒绝没有发生,停止并检查实验顺序。"
}不要把拒绝当成要消灭的报错。Alice 随后只获取并检查:
git fetch origin
if ($LASTEXITCODE -ne 0) { throw "Alice fetch 失败,停止。" }
git log --graph --oneline --decorate --all
git log --oneline HEAD..origin/main
git diff HEAD...origin/main根据实验意图选择 merge 或 rebase,解决冲突并运行检查后再 push。不要用 --force 绕过拒绝;拒绝是并发保护证据。
这一组文件不应冲突,可显式用 rebase 完成练习:
git rebase origin/main
if ($LASTEXITCODE -ne 0) {
git status
git rebase --abort
throw "Alice rebase 未按预期完成,已尝试中止。"
}
git log --graph --oneline --decorate --all
git push
if ($LASTEXITCODE -ne 0) { throw "Alice 最终推送失败,停止。" }最后让 Bob 获取并 fast-forward,断言两边 main 指向同一提交:
Set-Location -LiteralPath "$resolvedLab/bob"
git fetch origin
if ($LASTEXITCODE -ne 0) { throw "Bob 最终 fetch 失败,停止。" }
git merge --ff-only origin/main
if ($LASTEXITCODE -ne 0) { throw "Bob 无法 fast-forward,停止。" }
$aliceTip = (git -C "$resolvedLab/alice" rev-parse main).Trim()
$bobTip = (git rev-parse main).Trim()
if ($aliceTip -ne $bobTip) {
throw "Alice 与 Bob 的 main 尚未同步。"
}
git log --graph --oneline --decorate --all验收证据
git remote只有预期的origin;remote URL 是本地实验路径。- 两个 clone 都能解释本地分支、
origin/main和 upstream。 - 保存一次 push rejection 的错误类别,不记录凭据或私人路径。
- fetch 前后能说明
origin/main如何变化,工作区是否变化。 - 整合后两位协作者能看到相同的可达提交图。
--force-with-lease 为什么仍然危险
它仍是强推。省略显式期望值时,lease 通常依赖本地 remote-tracking ref;IDE 或后台 fetch 可能更新该 ref,使保护弱于使用者直觉。高风险场景必须限定 remote、分支和期望对象 ID,并遵守团队策略;初学主项目不练强推,受保护主分支不使用它。
自测
- 为什么
origin/main不等于服务器端实时状态? - fetch 后为什么当前文件通常不变?
- push 被拒绝时,为什么先 fetch 和审查比强推更合理?
- 裸
git push的推送集合为什么可能受push.default与 remote 配置影响? - PR 审批属于 Git 核心还是托管平台治理?
下一步
命令速查见 远程仓库同步,配置与敏感输出边界见 Git 配置与关键文件,综合项目见 版本控制实战与自测。
参考资料
按 Git 2.55.0 官方文档核验,获取日期:2026-07-14。