Git 故障诊断与恢复手册#
🔥 前言#
这是一份“先恢复开发节奏,再追根因”的故障手册。目标不是背诵清理命令,而是先判断失败发生在工作区、索引、本地对象库、引用、远端传输还是托管平台,然后只修改真正阻塞的那一层。
一、六层状态模型 🔼 🔽#
| 层 | 典型对象 | 常用观察命令 | 常见故障 |
|---|---|---|---|
| 工作区 | 当前文件内容 | git status、git diff | 冲突、权限、文件与目录互换、未跟踪文件遮挡。 |
| 索引 | 下一次提交的快照 | git diff --cached、git ls-files -s | 暂存遗漏、锁、gitlink 与 .gitmodules 不一致。 |
| 本地对象库 | commit、tree、blob、tag | git cat-file、git fsck | 对象损坏、悬空对象、磁盘或文件系统异常。 |
| 本地引用 | HEAD、分支、标签、reflog | git show-ref、git reflog | 引用锁、分支指向错误、reflog 过期。 |
| 远端跟踪引用 | refs/remotes/<远端>/... | git for-each-ref、git remote show | stale ref、D/F 冲突、大小写碰撞。 |
| 远端服务 | GitHub、GitLab、自建服务 | git ls-remote、平台审计日志 | 认证、权限、保护分支、仓库不存在、服务故障。 |
二、通用止损清单#
2.1、先保存现场#
snapshot_dir="$(mktemp -d "${TMPDIR:-/tmp}/git-diagnosis.XXXXXX")"
git status --short --branch
git diff > "$snapshot_dir/git-working-tree.patch"
git diff --cached > "$snapshot_dir/git-index.patch"
git ls-files --others --exclude-standard > "$snapshot_dir/untracked-files.txt"
git reflog -30 --date=iso
git remote -v
git submodule status --recursive- Patch 只保存文本差异,不包含未跟踪文件、空目录、文件权限之外的元数据或子模块内部未提交内容;重要未跟踪文件要另行复制。
- 仓库正在 rebase、merge、cherry-pick 或 revert 时,先用
git status识别进行中的操作,不要混用另一套--continue/--abort。 - 不知道能否恢复时,不执行
git gc --prune=now、git prune、reset --hard、clean -fdx或删除整个.git。
2.2、确认真实仓库与 Git 目录#
git rev-parse --show-toplevel
git rev-parse --git-dir
git rev-parse --git-common-dir
git worktree list --porcelain- 普通仓库的 Git 目录通常是根目录下的
.git。 - 子模块和 linked worktree 的
.git可能是指向真实 gitdir 的文本文件;不能据此认定仓库损坏。 - 多 worktree 共用对象与部分引用。手动改
.git元数据前必须看--git-common-dir,否则可能改错位置。
三、无法 Commit#
3.1、先区分“无法暂存”与“无法创建提交”#
git status --short --branch
git diff
git diff --cached
git add --dry-run -A -- .git add把工作区内容写入索引;git commit读取索引并创建提交。- Sourcetree 的 Commit 界面会同时编排暂存、移除和提交,因此界面上的 Commit 失败可能实际是某条
git add或git rm失败。 git add -A -- .会在当前路径范围内统一记录新增、修改和删除;--结束选项解析,避免以-开头的路径被当作参数。
3.2、错误分流#
| 报错或现象 | 根因方向 | 先做什么 |
|---|---|---|
nothing to commit | 索引没有相对 HEAD 的变化 | 看 git diff --cached;需要提交的内容先暂存。 |
unmerged files | merge/rebase/cherry-pick 冲突未解决 | git status,解决每个冲突后 git add,再执行对应 --continue。 |
Author identity unknown | user.name / user.email 缺失 | 先查配置来源,再按仓库或全局设置。 |
Unable to create '.git/index.lock': File exists | 另一个 Git 索引写进程仍在运行,或进程异常退出留下旧锁 | 进入 3.4;仍被占用时停止,确认残留后移动留档,不直接删除。 |
| Hook 返回非零 | pre-commit、commit-msg 等校验失败 | 直接运行 Hook 或查看输出;--no-verify 只用于定位,不是长期修复。 |
| GPG / SSH signing failed | 签名程序、密钥、agent 或 pinentry 异常 | 查 commit.gpgsign、gpg.format 与 signing key。 |
.gitmodules / gitlink 报错 | 子模块配置、路径与索引模式 160000 不一致 | 进入 3.5 子模块预检。 |
not removing ... recursively without -r | GUI 对文件/目录互换或目录删除做了分步 git rm | 用完整索引刷新替代逐路径操作,并先核对范围。 |
No space left on device / 只读错误 | 磁盘、配额、挂载或权限问题 | 先处理系统资源,不要反复重建索引。 |
3.3、身份、Hook 与签名#
git config --show-origin --get-regexp '^user\.(name|email)$'
git config --show-origin --get commit.gpgsign
git config --show-origin --get gpg.format
git config --show-origin --get user.signingkey
git config --show-origin --get core.hooksPath
find "$(git rev-parse --git-path hooks)" -maxdepth 1 -type f -perm -u+x -printcore.hooksPath 没有输出时,Hook 默认在 git rev-parse --git-path hooks 指向的目录;如果它有输出,应检查配置指向的目录,而不是继续假设使用默认 .git/hooks。
仅当前仓库设置身份:
git config user.name 'Your Name' git config user.email 'you@example.com'git commit --no-verify会跳过pre-commit和commit-msg等 Hook,可能绕过团队质量门禁;只能在确认规则允许且已理解后果时使用。git commit --no-gpg-sign可用来判断是否由签名链路造成,但不应在强制签名的仓库中当成最终方案。
3.4、锁文件#
git_dir="$(git rev-parse --absolute-git-dir)"
index_lock="${git_dir}/index.lock"
find "$git_dir" -maxdepth 2 -name '*.lock' -print
if [[ -e "$index_lock" ]]; then
stat -f 'path=%N size=%z inode=%i modified=%Sm' "$index_lock"
lsof -nP -- "$index_lock"
fi
pgrep -alf git处理原则:
- 先通过
git rev-parse --absolute-git-dir找到当前 worktree 真正使用的 Git 元数据目录,不固定假设锁一定在工作区根目录的.git文件夹内。 - 同时检查
lsof的精确锁文件占用和同一仓库内的 Git 索引写进程;仅凭“界面没有弹窗”或ps没看见明显命令,不能认定锁已经失效。 - 锁仍被进程持有,或存在
add、commit、rm、reset、checkout、merge、rebase、stash、update-index等写索引进程时,立即停止;不要杀进程,也不要移动锁。 - 只有锁是普通文件、无人持有、没有索引写进程,并且检测前后设备号与 inode 没变化时,才把它视为残留锁。
- 残留锁移动到
${git_dir}/jobs-stale-lock-backups/index.lock.<时间>.stale留档,不直接rm;这样既解除阻塞,也保留异常现场。 - 移动后先运行
git ls-files --stage验证现有索引可读,再用git add --dry-run -A -- .验证暂存入口;索引不可读时应恢复锁并停止。
Jobs SourceTree Commit 修复动作 已自动实现以上边界:活锁拒绝处理,残留锁留档后验证索引,再进入后续暂存与子模块流程。
3.5、残留锁、子模块与 Jobs Commit 修复脚本#
父仓索引用模式 160000 记录子模块提交,也称 gitlink;父仓不会直接提交子模块工作区里的文件修改。
git ls-files -s | awk '$1 == 160000 { print $2, $4 }'
git submodule status --recursive
git config --file .gitmodules --get-regexp '^submodule\..*\.(path|url)$'Jobs SourceTree Commit 修复动作 当前实现的核心流程是:
C01工作树定位:核对当前路径、.git指针、gitdir 与core.worktree;只在旧路径失效或当前目录可安全独立化时修复绑定。C02索引锁:检查真实 gitdir 下的index.lock;活锁或索引写进程存在时停止,确认残留后移动到jobs-stale-lock-backups并验证索引可读。C03.gitmodules顺序:有配置变化时先执行git add -A -- .gitmodules,满足删除或迁移 gitlink 前的配置一致性要求。C04嵌套工作树:扫描未完成的子模块路径迁移、借用旧 gitdir 的同源副本及.git/core.worktree错位;路径、索引登记和 URL 不能相互证明时停止。C05子模块一致性:从索引读取全部模式160000的 gitlink,检查缺失、空目录、迁移、退役和内部真实修改;缺失工作树按.gitmodules尝试git submodule update --init --recursive。C06完整索引刷新:前置检查通过后只执行一次git add -A -- .,统一记录新增、修改、删除、重命名和文件/同名目录形态互换。C07解锁复验:确认没有遗留index.lock,再执行git ls-files --stage与git add --dry-run -A -- .;二者通过只代表索引/暂存入口已解锁。
如果父仓锁定提交已经无法从新克隆子模块取到,但脚本得到有效且 clean 的当前 HEAD,后续全量暂存可能让父仓 gitlink 改指该 HEAD,必须人工判断依赖升级是否正确。子模块内部真实修改会保持原样;脚本不会终止 Git 进程,不直接删除锁,也不会执行 commit、push、reset、clean 或 git add -f。Hook、作者身份、签名、未解决冲突和系统资源问题不属于 C07 的解锁承诺。
3.6、运行 Commit 修复动作#
在 Sourcetree 中选中目标仓库,运行自定义动作 📥修复Git无法Commit。动作接收 $REPO 后直接执行;结束后回到“文件状态”刷新,并逐项核对:
git status --short --branch
git diff --cached -- .gitmodules
git diff --cached --submodule
git ls-files -s | awk '$1 == 160000 { print $2, $4 }'
git_dir="$(git rev-parse --absolute-git-dir)"
if [[ -d "${git_dir}/jobs-stale-lock-backups" ]]; then
find "${git_dir}/jobs-stale-lock-backups" -maxdepth 1 -type f -name 'index.lock.*.stale' -print
fi- 报告
index.lock 正被进程持有或“仍有可能修改索引的 Git 进程”时,脚本会返回失败并保留原锁;先完成或退出对应 Git 操作,再重新运行。 - 报告“已归档无人占用的残留
index.lock”时,日志会给出精确备份路径;修复后仍需检查暂存范围,不能把“锁已解除”等同于“所有变更都应该提交”。
终端独立运行时,不要把 ~ 放进单引号;使用明确安装根目录:
source_tree_actions_root='/path/to/SourceTree.command'
"${source_tree_actions_root}/【MacOS@SourceTree】📥修复Git无法Commit.command/【MacOS@SourceTree】📥修复Git无法Commit.command" \
'/path/to/repository'日志写到系统临时目录,文件名为 【MacOS@SourceTree】📥修复Git无法Commit.log。日志可能包含本机路径、远端和子模块状态,分享前脱敏。
四、无法 Fetch#
4.1、Fetch 到底会改什么#
git fetch 下载远端对象,并按 refspec 更新本地引用;常见映射是:
[remote "origin"]
fetch = +refs/heads/*:refs/remotes/origin/*因此,即使工作区完全没改,Fetch 仍会写入 FETCH_HEAD、远端跟踪引用、reflog、对象与维护数据。Fetch 不会自动把远端提交合并进当前本地分支;那是 Pull 或后续 merge/rebase 的职责。
4.2、先做最小诊断#
git remote -v
git remote get-url --all origin
git ls-remote --heads origin
git fetch --prune origin
git for-each-ref --format='%(refname) %(objectname)' refs/remotes/origin/| 报错 | 方向 | 检查 |
|---|---|---|
Could not resolve host | DNS 或网络 | 域名解析、网络、代理。 |
Connection timed out / refused | 网络、端口、防火墙、服务不可用 | HTTPS/SSH 端口和远端状态。 |
Permission denied (publickey) | SSH key、agent、Host 配置或账号权限 | ssh -vT git@github.com,不要先重建所有密钥。 |
HTTP 401 / 403 | Token、SSO、权限或组织策略 | 凭据来源、Token 权限、有效期和 SSO 授权。 |
repository not found | URL 错误、仓库被移动/删除,或无权访问私有仓库 | git remote get-url 与平台页面。 |
cannot lock ref + .lock | 并发进程或旧锁 | 先确认 Git/Sourcetree 进程。 |
refs/remotes/... + Not a directory | 远端跟踪引用 D/F 冲突 | 进入 4.3。 |
exists; cannot create | 分支前缀互换或大小写路径碰撞 | 对比远端真实分支与本地 refs。 |
bad object / missing blob | 对象库或引用损坏 | 备份后执行 git fsck --full,必要时重新克隆对比。 |
No space left on device | 磁盘空间或配额 | 先释放空间并检查文件系统。 |
4.3、远端引用的文件/目录冲突#
Git 引用名映射为层级路径。远端从 release 迁移到 release/v2 时,本地旧的 refs/remotes/origin/release 可能是文件,而新分支需要它成为目录;反向迁移也会产生目录挡住文件的问题。macOS 默认大小写不敏感文件系统还可能把远端的 SaaS 与 saas/... 映射到同一路径。
安全顺序:
F01先正常执行git fetch --prune;成功时立即停止,不创建备份目录。- F01 失败后只在错误明确命中远端引用路径冲突时继续;用
git ls-remote --heads <远端>读取远端真实分支,并只提取错误点名的目标分支。 F02执行git remote prune <远端>清理失效远端跟踪引用,然后立即重新 Fetch;成功时停止。F03处理大小写前缀碰撞:执行 Git 原生git pack-refs --all,让有效引用继续保存在packed-refs,并按 Git 默认行为清理已打包的 loose refs;备份碰撞 reflog 后立即重新 Fetch。F04只备份foo文件阻止创建foo/bar目录的前缀 loose ref/reflog,然后立即重新 Fetch。F05只备份foo/目录阻止创建foo文件的同名 loose ref/reflog 目录,然后立即重新 Fetch。- 每次复试都以 Git 退出码为解锁判据;最近一次错误如果转为网络、认证、并发锁等其它故障,或 F01–F05 全部未通过,脚本停止扩大修改范围并返回失败。
Jobs SourceTree Fetch 修复动作 已按上述边界实现,备份写入当前仓库 Git 元数据下的:
.git/jobs-ref-conflict-backups/YYYYMMDD-HHMMSS-PID/
├── refs/remotes/...
└── logs/refs/remotes/...脚本不执行 Pull、merge、rebase、commit、push、reset 或 checkout;它只更新 Fetch 本来就会更新的远端跟踪状态、用 Git 原生 pack-refs 改变引用的存储形态,并移动本次明确阻塞的 loose ref/reflog。pack-refs 不改变引用 OID 或提交历史。网络、DNS、代理、认证、权限、真实并发锁和磁盘故障不属于该脚本的修复范围。
4.4、为什么不直接删除整个 refs/remotes/origin#
- 远端跟踪引用本身可重新 Fetch,但 reflog 可能承载本地排错证据。
- 多 worktree、packed refs、特殊 refspec 和多个远端可能让“看似只是缓存”的目录承担更多状态。
- 大小写碰撞时,被阻塞的路径可能仍对应远端有效分支;全部删除会扩大影响范围。
- 精确备份后重试既保留证据,也能验证真正阻塞点。
4.5、运行 Fetch 修复动作#
在 Sourcetree 中选中目标仓库,运行自定义动作 📥修复Git无法Fetch。默认远端为 origin;只有普通 Fetch 已失败且错误命中远端跟踪引用路径冲突时,脚本才进入备份修复。
终端运行:
source_tree_actions_root='/path/to/SourceTree.command'
"${source_tree_actions_root}/【MacOS@SourceTree】📥修复Git无法Fetch.command/【MacOS@SourceTree】📥修复Git无法Fetch.command" \
'/path/to/repository'指定其它远端:
source_tree_actions_root='/path/to/SourceTree.command'
"${source_tree_actions_root}/【MacOS@SourceTree】📥修复Git无法Fetch.command/【MacOS@SourceTree】📥修复Git无法Fetch.command" \
'/path/to/repository' \
'upstream'执行后核对:
git for-each-ref --format='%(refname) %(objectname)' refs/remotes/
git remote show <remote>
git status --short --branchFetch 动作的日志文件为 【MacOS@SourceTree】📥修复Git无法Fetch.log;它会记录 F01–F05 的执行顺序、每次实际 Fetch 复试、远端选择、错误分支、备份路径和最终退出结果。备份属于 Git 元数据排错证据,不应在未确认恢复完成前删除。
五、无法 Pull#
Pull 通常等价于 Fetch 后执行 merge 或 rebase;先把两阶段拆开:
git fetch --prune origin
git status --short --branch
git log --oneline --graph --decorate --all -30| 现象 | 处理 |
|---|---|
Need to specify how to reconcile divergent branches | 本次明确使用 --rebase、--no-rebase 或 --ff-only;不要不理解就全局写死。 |
refusing to merge unrelated histories | 两端没有共同祖先;确认确实要合并两个独立项目后,显式使用 --allow-unrelated-histories。 |
| 工作区改动会被覆盖 | 先提交、暂存到 stash,或取消本次整合;不要直接硬重置。 |
| merge/rebase 冲突 | 逐文件解决,确认索引后执行对应 --continue;需要放弃时用对应 --abort。 |
六、无法 Push#
6.1、常见分流#
| 报错 | 根因 | 处理 |
|---|---|---|
non-fast-forward | 远端分支含本地未整合的提交,或本地改写了历史 | 先 Fetch 并检查分叉;整合或在明确需要时使用安全强推。 |
protected branch hook declined | 保护分支或服务端 Hook 拒绝 | 走 Pull Request、审批或满足平台规则。 |
permission denied / 403 | 当前凭据无写权限 | 检查账号、远端 URL、Token/SSH 权限和组织策略。 |
pre-receive hook declined | 服务端校验失败 | 阅读远端输出;本地 --no-verify 无法跳过服务端 Hook。 |
6.2、安全强推#
裸 --force 会关闭非快进保护,可能覆盖他人提交。优先记录你确认过的远端提交,并把它作为 lease:
git fetch origin
expected_remote_commit="$(git rev-parse refs/remotes/origin/main)"
git push --force-with-lease=main:"$expected_remote_commit" origin HEAD:main- 如果远端已经不是
expected_remote_commit,推送会失败,提醒你重新检查别人的新提交。 - Sourcetree 或后台任务自动 Fetch 可能更新远端跟踪引用;显式写预期提交比无参数
--force-with-lease更清楚。 - 保护分支、服务端 Hook 和权限策略仍可以拒绝强推;客户端参数不能越过服务器规则。
七、认证、代理与传输排错#
7.1、SSH#
ssh -T git@github.com
ssh -vT git@github.com
git config --show-origin --get core.sshCommand-vT会输出密钥选择、Host 匹配和握手过程;日志可能包含用户名、路径、主机和网络信息,分享前脱敏。- 多账号应使用
~/.ssh/config的 Host 别名分别绑定IdentityFile,再让不同仓库使用不同别名 URL。 - Host Key 指纹变化不能直接忽略;先从托管平台官方页面核对指纹,排除中间人攻击或域名指向错误。
7.2、HTTPS、代理与凭据#
git config --show-origin --get-regexp '^(http\.|https\.|credential\.|remote\.)'
git config --global --get http.proxy
git config --global --get https.proxy- 不把
http.sslVerify=false当通用修复;这会失去 TLS 证书校验。 - 不把 Token 写入远端 URL、脚本、README 或 shell 历史。
- 组织可能要求 SSO、细粒度 Token、审批或限定有效期;“Token 没过期”不等于“对目标仓库有权限”。
八、对象、提交与 stash 恢复#
8.1、恢复顺序#
先查仍有语义名称的 reflog。
git reflog show --all --date=iso git log -g --all --oneline --decorate对删除的 stash 单独查 stash reflog;如果 ref/reflog 已被删除,再查悬空对象。
git fsck --full --no-reflogs --unreachable用
git show --stat <对象>、git show <对象>和父提交结构确认候选,不要把所有 dangling commit 自动合并到当前分支。先创建保护分支或新 stash,再恢复到工作区。
git branch recover/candidate <commit> git stash store -m 'recovered stash' <stash-commit>
8.2、边界#
- Push 到远端不会直接清理本地 dangling 对象。对象何时被删除取决于本地引用、reflog 过期、
git gc/git maintenance/git prune和相关配置。 git fsck --lost-found会把 dangling commit 和其它对象写入.git/lost-found,但它不会判断哪个对象是用户要找的 stash。- stash 通常不是单一普通提交,可能包含工作区、索引以及可选未跟踪内容的多父提交结构;恢复前要检查。
- 对象一旦已经被垃圾回收且没有远端、备份、文件系统快照或其它克隆副本,就不能靠 Git 命令凭空恢复。
九、诊断日志#
GIT_TRACE=1 git fetch origin
GIT_TRACE_PACKET=1 GIT_TRACE=1 git fetch origin
GIT_CURL_VERBOSE=1 git fetch origin- Trace 会增加大量内部信息。日志可能包含仓库地址、代理、用户名、路径和协议元数据;分享前必须脱敏。
- 只在复现最小问题时开启,完成后不要长期写进全局环境变量。
- Trace 不是修复动作;先保留原始错误,再用它缩小失败阶段。
十、官方资料#
- git-add:索引与
-A语义。 - git-fetch:远端跟踪引用、refspec 与 pruning。
- git-pack-refs:把 loose refs 收进
packed-refs,并在默认策略下清理已打包的 loose 路径。 - git-submodule:子模块初始化、更新、同步和路径。
- git-reflog:本地引用历史与过期。
- git-fsck:对象连通性、dangling 与 lost-found。
- git-push:快进规则与
--force-with-lease。 - GitHub SSH 文档:密钥、agent 与连接测试。
- Atlassian Sourcetree 自定义动作:自定义动作入口与执行方式。