远程协作与离线双克隆实验

本节目标

区分 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 加后续整合,整合方式受显式选项与配置影响,不应把它理解为一个不可拆分动作。

一个可审查的协作流程

  1. git fetch origin 获取事实。
  2. 用 git log --oneline HEAD..origin/main 与 git diff HEAD...origin/main 检查差异。
  3. 在本地功能分支选择 merge 或 rebase,并解决冲突、运行测试。
  4. 推送功能分支,而不是直接改写受保护主分支。
  5. 在托管平台通过 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,并遵守团队策略;初学主项目不练强推,受保护主分支不使用它。

自测

  1. 为什么 origin/main 不等于服务器端实时状态?
  2. fetch 后为什么当前文件通常不变?
  3. push 被拒绝时,为什么先 fetch 和审查比强推更合理?
  4. 裸 git push 的推送集合为什么可能受 push.default 与 remote 配置影响?
  5. PR 审批属于 Git 核心还是托管平台治理?

下一步

命令速查见 远程仓库同步,配置与敏感输出边界见 Git 配置与关键文件,综合项目见 版本控制实战与自测。

参考资料

按 Git 2.55.0 官方文档核验,获取日期:2026-07-14。