拉取 · 提交 · 推送(冲突交给人裁决)
把「拉取 → 提交 → 推送」做成一条可重复的机械流程,并把唯一需要判断力的环节——冲突取舍——留给用户。
核心原则:机器只做没有歧义的事。任何需要"选一个"的动作,都不许替用户选。
确定性内核:scripts/git-sync.sh(判断与升级策略就是下面这些规则;脚本只执行规则)。
铁律
- 冲突不裁决:只要脚本报
status=needs_decision(退出码 3),停下来问用户。不允许用--ours/--theirs一把梭、不允许把两边内容手工拼成"都能过"的版本、不允许--skip。唯一例外:git 自己就解掉了(rerere 重放、非重叠自动合并)——那不算冲突。 - 未跟踪文件默认不入库:只
git add -u(已跟踪文件的修改)。未跟踪文件在报告里列出即可;要收进版本库必须显式--include-untracked,且先跟用户确认。 - 永不 force push、永不
reset --hard、永不clean:远端已有别人的提交时,覆盖或丢弃都不是"同步"。 - 先 fetch 再判断:本地
origin/*引用可能是过期的,git status会谎报"已是最新"(实测发生过,见下)。 - 冲突后不留半成品:脚本发现冲突会
git rebase --abort回到干净态再退出。要就地手工解决冲突才用--keep-conflict。
用法
# 正常同步(提交信息必填;有改动却没给 -m 会以退出码 6 拒绝)
bash scripts/git-sync.sh -C /path/to/repo -m "feat: 说明这次改了什么"
# 看清会发生什么、不真推
bash scripts/git-sync.sh -C /path/to/repo -m "..." --no-push
# 显式把未跟踪文件也纳入提交(先问过用户)
bash scripts/git-sync.sh -C /path/to/repo -m "..." --include-untracked
# 冲突时就地保留现场(默认是 abort 回干净态)
bash scripts/git-sync.sh -C /path/to/repo -m "..." --keep-conflict
最后一行固定输出 GIT_SYNC_RESULT <json>,直接解析它,别去 grep 人类可读输出。
| 退出码 | 含义 | 该做什么 | |---|---|---| | 0 | 已同步或本来无事可做 | 报告 sha 即可 | | 3 | 需要人工裁决(整合冲突) | 按下面「升级协议」问用户,不要自己动手 | | 4 | 前置条件不满足(detached HEAD / 有进行中的 merge·rebase / 无 upstream / 远端不可达 / push 被拒) | 先修前置条件,别硬来 | | 5 | 安全检查拦截(敏感文件待提交 / 子模块有未推送提交) | 把清单给用户,按他的决定处理 | | 6 | 有改动但没给提交信息 | 先写清楚这次改了什么,再跑一次 |
升级协议(退出码 3 时,问用户什么)
一次问清,别挤牙膏;不要贴整篇 diff:
- 一句话结论:哪个仓库、哪个分支、几个文件冲突、本地/远端各领先几个提交。
- 每个冲突文件一行:路径 + 冲突类型(both modified / both added / delete-modify / rename)+ 双方各自的意图(取各自 commit message 与几行关键 hunk,不要全文)。
- 给 2–3 个可执行选项,例如:① 保留本地改动、把远端提交 rebase 到本地之上;② 放弃本地改动取远端(需用户明确同意);③ 用户手工合并后重跑脚本;④ 改用 merge 保留分叉(当分支是多人共享时比 rebase 安全)。
- 等用户选。选完再动手、动完再推。
容易处理 vs 难以处理
- 可以直接做完:本地领先(直接推);远端领先且本地无改动(快进);双方改的是不同文件/不同区域(git 自动合并);未跟踪文件(默认不动,只报告)。
- 必须问用户:同一处内容冲突、删除/修改冲突、二进制文件冲突(
.uasset/图片/模型)、子模块指针冲突、冲突文件超过 5 个、pull 时远端又变了导致二次冲突。 - 判断不了就算难以处理——宁可多问一次,也不要替用户在两个语义之间选边。
常见错误(都实测过)
| 错误 | 现实后果 |
|---|---|
| git add -A 顺手把未跟踪文件提交上去 | 草稿、本地配置、备份文件被推上共享仓库(实测发生:一个 notes-untracked.txt 被当成"本地改动"提交了) |
| 冲突时"两边都留"拼成新内容再推 | 这不是合并是发明语义,远端已收下错误内容,别人基于它继续开发 |
| 信 git status 的"与上游一致" | 本地引用过期时的谎报;不 fetch 就下结论会漏掉远端提交 |
| 冲突后把工作区丢那儿 | 半完成 rebase 会让后续所有 git 操作踩雷(脚本默认 abort 就是为了避免这个) |
| 拿 git ls-files --others 的输出直接 git add | git 默认把含非 ASCII 的路径输出成 C 风格转义("Docs/\345\212\237..."),add 匹配不到 → 文件被静默漏掉、脚本照报成功(脚本已修:加 -c core.quotePath=false,失败项记进 untracked_add_failed;自测 S10 守着这条) |
| push --force 解决被拒 | 覆盖他人已推送的历史,不可恢复 |
| 子模块改了只推父仓库 | 父仓库指针指向子模块里不存在的提交,别人拉下来是坏的 |
自测
bash scripts/selftest.sh # 建临时测试场,跑 7 个场景(含"冲突必须升级、未跟踪文件不得入库")
改了脚本或本技能后必须重跑;新增场景就加进 selftest.sh,不要靠嘴保证。
微信扫一扫