P4 的 workspace 映射、服务器 have list 和本地文件内容可以不同步。先确认 P4PORT、P4USER、P4CLIENT,再决定是同步文件、登记修改、shelve 还是提交。示例路径和 changelist 需要替换。
先查映射,不用强制同步掩盖问题
确认服务、身份与 workspace
p4 info
核对 Server address、User name、Client name、Client root;错误先从连接参数与认证排查,不执行 sync。
p4 login -s
过期时通过 p4 login 交互登录,再重查;不要把密码写进命令行。配置来源有疑问时单独 p4 set 检查 P4PORT/P4USER/P4CLIENT 等。
p4 client -o
这是读取,不修改 workspace;需要改映射时先保存规格并确认本地工作归属,再通过 p4 client 编辑。
p4 where //depot/project/file.txt
替换为实际 depot 文件;not in client view 指向映射范围问题,强制 sync 不能修复错误的 View。
p4 have //depot/project/...
... 包含该范围递归文件;have 不读取磁盘验证一致性。
先预览同一范围,再同步
p4 sync -n //depot/project/...
核对数量、路径与目标版本,保存需要保留的本地修改;确认后才执行下一条。
p4 sync //depot/project/...
固定版本可在同一 file spec 后附 @CHANGE 或 #REV,先按同一 spec 预览。完成后核对输出与 have;有已打开文件或覆盖保护报错时先处理该文件,不能假定所有路径都成功。
p4 where 对照 depot、client 和本地路径;Root、View 或 Stream 错了,就会同步到错误位置。client name 是逻辑工作区名,不要求等于主机名。have 记录上次同步版本,不读取文件内容证明它仍一致。
正在呈现知识画面
have list 和磁盘字节不是同一份状态sync -k 只更新服务器记录,磁盘保持原内容;sync -f 才重新传输,#none 则移除本地受控文件与 have 记录。
{"id":"ops-p4-sync","title":"have list 和磁盘字节不是同一份状态","summary":"sync -k 只更新服务器记录,磁盘保持原内容;sync -f 才重新传输,#none 则移除本地受控文件与 have 记录。","height":600,"html":"<main><p class=\"quiet\" data-i18n=\"scope\"></p><div class=\"cards\"><section class=\"card \"><h3 data-i18n=\"disk\"></h3><strong id=\"disk\"></strong></section><section class=\"card \"><h3 data-i18n=\"have\"></h3><strong id=\"have\"></strong></section><section class=\"card \"><h3 data-i18n=\"head\"></h3><strong id=\"head\">#3</strong></section></div><div class=\"controls\"><button type=\"button\" data-action=\"sync\" data-i18n=\"sync\"></button><button type=\"button\" data-action=\"metadata\" data-i18n=\"metadata\"></button><button type=\"button\" data-action=\"force\" data-i18n=\"force\"></button><button type=\"button\" data-action=\"none\" data-i18n=\"none\"></button><button type=\"button\" data-action=\"reset\" data-i18n=\"reset\"></button></div><div class=\"explain\"><div class=\"stable \"><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"same\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"different\"></p><p id=\"message\" aria-live=\"polite\"></p></div></div></main>\n","css":"main {\n display: grid;\n gap: 18px;\n min-width: 0;\n}\np {\n overflow-wrap: anywhere;\n}\n.controls {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n align-items: center;\n}\n.presets {\n display: flex;\n flex-wrap: wrap;\n}\n.presets button {\n flex: 1 1 85px;\n font-size: 15px;\n}\n.cards {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 10px;\n}\n.card {\n border: 1px solid var(--rule);\n border-radius: 10px;\n padding: 14px;\n min-width: 0;\n background: var(--surface);\n}\n.card h3 {\n font-size: 15px;\n color: var(--muted);\n margin-bottom: 8px;\n}\n.card strong {\n font-size: 23px;\n font-weight: 500;\n overflow-wrap: anywhere;\n}\n.code {\n font-family: var(--viz-font-mono);\n font-size: 15px;\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n}\n.stable {\n display: grid;\n min-width: 0;\n}\n.stable > * {\n grid-area: 1/1;\n margin: 0;\n}\n.stable > .reserve {\n visibility: hidden;\n pointer-events: none;\n}\n.explain {\n border-left: 3px solid var(--accent);\n padding: 12px 14px;\n background: var(--accent-soft);\n}\n.arrow {\n text-align: center;\n color: var(--accent);\n font-size: 25px;\n}\n.row {\n padding: 12px;\n border: 1px solid var(--rule);\n border-radius: 8px;\n min-width: 0;\n}\n.row.on {\n border-color: var(--accent);\n background: var(--accent-soft);\n}\n.row.off {\n color: var(--muted);\n}\n.bar {\n height: 18px;\n background: var(--surface);\n border: 1px solid var(--rule);\n border-radius: 5px;\n overflow: hidden;\n margin-top: 8px;\n}\n.bar > i {\n display: block;\n height: 100%;\n background: var(--accent);\n width: 0;\n transition: width 0.22s;\n}\n.checks {\n display: grid;\n gap: 8px;\n}\n.checks label {\n display: flex;\n gap: 10px;\n align-items: center;\n padding: 9px;\n border: 1px solid var(--rule);\n border-radius: 7px;\n font-size: 16px;\n}\n.checks input {\n width: 18px;\n height: 18px;\n accent-color: var(--accent);\n flex-shrink: 0;\n}\nlabel.range {\n display: block;\n font-size: 16px;\n}\nlabel.range output {\n float: right;\n font-variant-numeric: tabular-nums;\n}\n.pair {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 10px;\n}\n.flow {\n display: grid;\n gap: 6px;\n}\n.quiet {\n font-size: 14px;\n color: var(--muted);\n}\n.badge {\n display: inline-block;\n padding: 2px 8px;\n border-radius: 5px;\n background: var(--accent-soft);\n color: var(--accent);\n}\nbutton {\n overflow-wrap: anywhere;\n}\n.large {\n font-size: 32px;\n color: var(--accent);\n font-weight: 500;\n}\n.layers {\n display: grid;\n gap: 12px;\n}\n.layer {\n border-left: 3px solid var(--rule);\n padding-left: 12px;\n}\n.layer.on {\n border-color: var(--accent);\n}\n@media (max-width: 520px) {\n .cards,\n .pair {\n grid-template-columns: 1fr;\n }\n .card {\n padding: 12px;\n }\n .card strong {\n font-size: 22px;\n }\n}\n","js":"const $ = (s) => document.querySelector(s);\nconst $$ = (s) => [...document.querySelectorAll(s)];\nconst t = (k) => viz.t(k);\nconst put = (id, v) => ($(\"#\" + id).textContent = v);\nconst say = (k) => put(\"message\", t(k));\nconst state = (v) => ($(\"main\").dataset.state = JSON.stringify(v));\nconst press = (key, v) =>\n $$(\"[data-\" + key + \"]\").forEach((b) =>\n b.setAttribute(\"aria-pressed\", String(b.dataset[key] === v)),\n );\nlet disk = 2,\n have = 2;\nfunction draw() {\n put(\"disk\", disk === null ? \"∅\" : \"#\" + disk);\n put(\"have\", have === null ? \"∅\" : \"#\" + have);\n say(disk === have ? \"same\" : \"different\");\n state({ disk, have });\n}\n$$(\"[data-action]\").forEach(\n (b) =>\n (b.onclick = () => {\n let a = b.dataset.action;\n if (a === \"sync\" && have !== 3) {\n disk = 3;\n have = 3;\n }\n if (a === \"metadata\") have = 3;\n if (a === \"force\") {\n disk = 3;\n have = 3;\n }\n if (a === \"none\") {\n disk = null;\n have = null;\n }\n if (a === \"reset\") {\n disk = 2;\n have = 2;\n }\n draw();\n }),\n);\ndraw();\n","audio":false,"strings":{"different":"记录已是 #3,磁盘仍旧。普通 sync 可能认为已最新;不要用 -k 代替下载。","disk":"本地文件内容","force":"sync -f #3","have":"服务器 have list","head":"depot 最新版","metadata":"sync -k #3","none":"sync #none","reset":"重置 #2","same":"磁盘与 have 一致。have 是记录,不能靠它独立证明真实内容。","scope":"单个未打开文件:初始磁盘与 have 都为 #2,depot 最新 #3。允许覆盖,无本地编辑;自由操作观察不一致。","sync":"sync #3"}}| 操作 | 本地文件 | have list |
| p4 sync PATH | 更新到指定版本,受打开文件与覆盖保护约束 | 同步更新 |
| p4 sync -f PATH | 强制重新传输;先确认没有要保留的本地修改 | 同步更新 |
| p4 sync -k PATH | 不传输、不修改文件 | 只更新记录;等价于 flush,可能人为制造不一致 |
| p4 sync "//depot/project/...#none" | 移除该范围内受版本控制的本地文件,遵循打开文件规则 | 清除对应 have 记录 |
#none 不是“只清元数据”,引号还可避免 shell 把符号当特殊语法。操作前用同范围的 sync -n 预览;不要默认 -f 就能解决映射错误。
先打开文件,再审阅 changelist
按变更类型选择一种打开方式
以下 edit / add / delete / move 面向不同文件,不是一份全部执行的脚本。路径相对当前 workspace;执行前已确认映射。
p4 edit file.txt
随后编辑文件并用 p4 diff file.txt 审阅。
p4 add new-file.txt
命令不替你写文件内容;用 p4 opened 检查 add 状态。
p4 delete obsolete-file.txt
会影响本地文件并在提交后删除 depot 当前版本;提交前用 opened 核对目标,误选时先审阅 revert 的恢复影响。
p4 move old-name.txt new-name.txt
源文件须按服务器/workspace 要求先打开为 edit;目标名不能占用。opened 应出现 move/delete 与 move/add 配对,之后核对新路径内容。
离线编辑需要先发现再登记
p4 reconcile -n ...
核对新增/修改/删除,排除生成物后再用限范围 p4 reconcile path/to/file 登记。未打开文件的补丁可用 p4 diff -f file.txt,-f 在这里是检查未打开文件,不是强制同步。
p4 change
编辑 Description 和 Files,保存后记录输出的编号;下文 12345 是该编号示例,不应照抄。
P4 默认不会把任意磁盘写入自动收进 changelist。离线改动先 reconcile -n 看 add/edit/delete,再限范围 reconcile;未打开的文件可用 p4 diff -f 查看。创建 numbered changelist 后,用 reopen 移动已打开文件。
整理列表、解决冲突、再提交
p4 reopen -c 12345 file.txt
-c 选择 changelist,不会把尚未打开的任意文件自动加入。
p4 opened -c 12345
再用 p4 diff -du file.txt 逐文件看内容;新增文件直接审阅工作文件。p4 describe -s 12345 的摘要不替代补丁检查。
p4 resolve -n -c 12345
需要解决时选择 p4 resolve -c 12345,按提示逐项审阅,随后 diff、测试并再次预览;不要无条件选接受某一侧。
p4 submit -c 12345
成功后记录 submitted change 编号,并用 p4 describe -s NUMBER 核对。失败先读错误和 opened 状态,不要认为列表已经提交。
放弃本地修改需单独确认
p4 revert -n file.txt
保存需要的内容后才用 p4 revert file.txt;edit 的修改会丢弃,add 的本地文件处理与 delete 不同,按动作读结果。仅清理未修改文件时可选 p4 revert -a,仍先限制范围和预览。
12345 为待提交列表的示例。resolve 预览后按每个冲突决定合并;提交前确认内容、测试和目标 stream。p4 revert 会放弃已打开文件的工作改动;p4 revert -a 只处理符合条件的未修改文件,也应先检查。
Shelve 是服务器上的未提交内容
保存 shelf 并核对服务器副本
p4 shelve -c 12345
用已创建的 changelist 编号;此操作既不 submit,也不清除本地工作。
p4 describe -S 12345
接收方可用 p4 changes -s shelved -u YOUR_USER 找到列表;用户名称从 info 或交接记录取得。
在接收 workspace 恢复到新的列表
先在接收方确认映射与本地工作,并通过 p4 change 创建目标 12346;它与来源 12345 是两个编号。
p4 unshelve -s 12345 -c 12346
随后 p4 opened -c 12346、diff 与 p4 resolve -n -c 12346 检查,必要时解决冲突并测试。源 shelf 仍保留,跨 stream 恢复还需确认映射规则。
p4 shelve -d -c 12345
这不等于 revert 本地工作,也不删除已提交历史;删除前再次 describe -S 核对编号与内容。
历史、逐行来源与 stream 规格按需查询
p4 filelog file.txt
需要逐行追溯时另用 p4 annotate file.txt,再回到对应 changelist 查原因;不是按顺序修改文件的步骤。
p4 stream -o //streams/main
替换为实际 stream 名,与 client -o 的 Stream 字段对照。
shelve 不会提交 depot,也不会自动清空本地修改;unshelve 放入目标 changelist,可能需要 resolve。替换既有 shelf 前确认接收者使用的版本;删除 shelf 与 revert 本地文件是不同动作。跨 stream 的映射和集成来源需单独检查。
p4 sync · p4 reconcile · p4 shelve