
“拉一下代码”常常把几件事混成一个动作下载对象、更新origin/master、整合到当前分支、替换工作区。于是fetch成功文件没变时人会怀疑缓存push被拒绝时又想直接覆盖远端。这次不开账号、不碰公网。只用标准Git在临时目录建一个bare仓库和两个工作仓库让同一份变化先经过fetch再经过明确指定策略的pull最后观察分叉后的拒绝与整合。使用Git for Windows 2.45.2.windows.1和PowerShell所有块在同一窗口依次执行。不设置代理不使用登录凭据不强制推送也不把本地路径传输当HTTP协议实验。1. 服务端不是第三份正在编辑的工作目录server是bare仓库保存对象和引用但没有我们要部署或编辑的工作区。这样避免把“push成功”和“服务端磁盘文件更新”混成同一件事。$ErrorActionPreference Stop $PSNativeCommandUseErrorActionPreference $false function G { git args; if ($LASTEXITCODE -ne 0) { throw git failed: $args } } $root Join-Path $env:TEMP (git-remote-lab- [guid]::NewGuid().ToString(N)) New-Item -ItemType Directory -Path $root | Out-Null $server Join-Path $root server.git $repoA Join-Path $root A $repoB Join-Path $root B G init --object-formatsha1 --bare -b master $server G init --object-formatsha1 -b master $repoA Set-Location $repoA G config user.name Blog experiment G config user.email blogexample.invalid G config core.autocrlf false G config gc.auto 0 Set-Content demo.txt v1 G add demo.txt G commit -m c1 $c1 G rev-parse HEAD G remote add origin $server G push -u origin master G clone --no-local $server $repoB G -C $repoB config user.name Blog experiment G -C $repoB config user.email blogexample.invalid G -C $repoB config core.autocrlf false G -C $repoB config gc.auto 0no-local让这次clone不走本地硬链接优化两个工作仓库不共享对象文件。它仍然使用本地路径不说明公网传输、认证或服务端钩子都测试过。2. 在B推送再回A只fetchSet-Location $repoB Set-Content demo.txt v2 G add demo.txt G commit -m c2 $c2 G rev-parse HEAD G push origin master Set-Location $repoA git cat-file -e ${c2}^{commit} if ($LASTEXITCODE -eq 0) { throw A unexpectedly already has c2 } G fetch origin fetched_object_present ((G cat-file -t $c2) -eq commit) tracking_ref_is_c2 ((G rev-parse refs/remotes/origin/master) -eq $c2) fetch_keeps_HEAD ((G rev-parse HEAD) -eq $c1) fetch_keeps_file_v1 ((Get-Content demo.txt -Raw).Trim() -eq v1)本轮四项均为True。A现在能读c2origin/master也到c2了但当前master仍是c1文件仍是v1。fetch没有“失败一半”而是完成了自己的阶段。origin是远程配置的名字origin/master是本地保存的远端跟踪引用不是通过这个名字实时读取服务器。服务器可能继续前进直到下一次fetch我们才获得新的观察结果。Git fetch手册3. 明确策略才知道pull要做什么G pull --ff-only origin master pull_moves_HEAD_to_c2 ((G rev-parse HEAD) -eq $c2) pull_updates_file_v2 ((Get-Content demo.txt -Raw).Trim() -eq v2)本例A没有独立提交因此可以快进。ff-only表示只接受无需新建整合提交的推进若分叉不能为了“成功拉代码”擅自替读者选择merge或rebase。A里的状态fetch前fetch后ff-only成功后当前HEAD/masterc1c1c2origin/masterc1c2c2c2对象不在A里已在已在demo.txtv1v1v2pull不只有一种整合方式。merge、rebase和只允许快进的行为应由明确选项及配置决定。本篇不依赖“我机器上的默认pull策略”。Git pull手册4. 拒绝推送为什么也是正确结果让B在c2后增加other.txt并推送c3A在c2后增加local.txt得到L。两次修改没有文本冲突但历史已经分叉。没有文本冲突不等于可以快进。Set-Location $repoB Set-Content other.txt remote G add other.txt G commit -m c3 $c3 G rev-parse HEAD G push origin master Set-Location $repoA Set-Content local.txt local G add local.txt G commit -m L $localTip G rev-parse HEAD git push origin master $pushExit $LASTEXITCODE if ($pushExit -eq 0) { throw expected divergent push rejection } push_rejectedTrue server_keeps_c3 ((G -C $server rev-parse refs/heads/master) -eq $c3) local_tip_unchanged ((G rev-parse HEAD) -eq $localTip)只看rejected文本还不够。验证同时要求非零退出、server引用仍是c3、A仍是L。如果提示失败却已经移动远端入口同样不算正确保护。5. fetch能成功ff-only仍然应当失败G fetch origin git pull --ff-only origin master $pullExit $LASTEXITCODE if ($pullExit -eq 0) { throw expected ff-only refusal on divergence } ff_only_refusedTrue refusal_keeps_local_tip ((G rev-parse HEAD) -eq $localTip) refusal_keeps_local_file ((Get-Content local.txt -Raw).Trim() -eq local) tracking_ref_updated_to_c3 ((G rev-parse refs/remotes/origin/master) -eq $c3)这时拉取阶段已经拿到c3整合阶段却拒绝改变当前分支。不能用“命令总体失败”反推其间所有状态都没变化远端跟踪引用可以已更新HEAD却保持不变。检查对象本轮分叉状态下一步需要判断什么A的masterL含本地工作是否保留及如何整合origin/masterc3含远端工作与L的公共祖先在哪里server的masterc3推送不能直接让它失去c3入口工作区local.txt仍在拒绝没有替我们解决历史关系6. 用普通整合保住两条历史本例改不同文件显式选择merge。它不是所有冲突的万能解也不要求真实团队统一用这个策略。G merge --no-edit origin/master $merged G rev-parse HEAD $parents (G rev-list --parents -n 1 HEAD) -split \s if ($parents.Count -ne 3 -or $parents[1] -ne $localTip -or $parents[2] -ne $c3) { throw merge parent mismatch } if ((Get-Content local.txt -Raw).Trim() -ne local) { throw local work lost } if ((Get-Content other.txt -Raw).Trim() -ne remote) { throw remote work lost } G push origin master server_accepts_merge ((G -C $server rev-parse refs/heads/master) -eq $merged) both_files_preserved ((Test-Path local.txt) -and (Test-Path other.txt)) G -C $repoA fsck --full G -C $repoB fsck --full G -C $server fsck --full LOCAL_REMOTE_CHECKS_PASSED新merge提交含两个parentserver可以从c3快进到它因此这次正常push成功不需要强制覆盖。更重要的是保住两份文件与保住两条历史都被分别检查了。Git push手册图是本轮stdout节选排版后的截图不冒充桌面终端窗口。验证有三组仓库不只检查客户端提示同时核验对象存在、双方引用、工作区内容、merge的parent和三个仓库的fsck。不涉及鉴权、远端并发、网络中断、force-with-lease或部署。有限实验不能证明生产同步协议全面正确但足以把一个实用排查顺序立起来对象拿到了吗哪个引用更新了当前分支整合了吗工作区应该更新吗这比把三个命令统称“同步”更准确。