1. 切个分支,凭什么卡我五分钟
我用 Git 十多年,遇到最让人无语的报错,不是合并冲突,而是切个分支的时候 Git 突然甩给你一行:
bash复制fatal: Unable to create '.../.git/index.lock': File exists.
没有冲突提示,没有上下文,就一个 index.lock 文件把你挡在门外。分支切不出去,代码提不进来,仓库像被什么东西锁死了一样。大部分人第一反应是上网搜,搜到的答案千篇一律:
bash复制rm -f .git/index.lock
删完确实好了,但下一次什么时候又出现,你完全没底。如果你只是偶尔遇到一次,删掉也就算了。但如果你的工作流里经常碰到这个问题,或者你负责维护团队里一堆开发机的环境,靠手动删文件显然不是办法。
这篇博文我想从 index.lock 的产生原理说起,解释清楚它为什么会残留,再把我沉淀下来的一套全局命令行工具完整分享出来。这套工具不是无脑删文件,而是会先判断锁文件是不是真的没人用、是不是真的残留,再决定要不要清理。它可以作为 git 的全局子命令安装,之后在任何仓库里遇到锁文件问题,一条命令就能处理。
内容适合两类人:一类是刚用 Git 不久、被这个报错搞懵的新手;另一类是经常帮别人处理 Git 疑难杂症的"仓库救火队员"。前者能看懂原理和用法,后者可以直接把工具拿走部署到自己的环境里。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. index.lock 到底是什么:从文件锁到索引写入的完整机制
2.1 这不是一个普通的"文件被占用"
很多人以为 index.lock 和 Windows 上"文件正在被另一个程序使用"是一回事,其实背后的机制完全不同。Git 为了保证索引文件(.git/index)在写入时的原子性,不会直接修改 index 文件本身,而是先在同级目录创建一个 index.lock 文件,把新的索引内容写进这个锁文件,等全部写完再通过一次原子性的 rename 操作替换掉正式文件。
这个过程有点像一个编辑改稿子时,先在草稿纸上写完一整版,确认没问题后才把草稿纸推到主编桌上,而不是直接在主编正在看的原稿上涂改。这样做有两个好处:
- 任何时刻读
index的进程看到的都是一个完整、一致的文件,不会读到写了一半的损坏数据。 - 多个 Git 命令同时运行时,先拿到锁的命令获得写入资格,后到的命令看到
index.lock存在,直接放弃并提示冲突。
所以 index.lock 存在的意义是让 Git 的写入操作具备"串行化"能力。它不是操作系统层面的文件锁,而是 Git 自己约定的标志文件。
2.2 这些锁文件散落在 .git 目录的各个角落
index.lock 只是整个锁文件体系的冰山一角。Git 里几乎所有需要写入的敏感文件都有对应的 .lock 后缀文件:
| 锁文件 | 对应保护的对象 | 通常出现在 |
|---|---|---|
index.lock |
索引文件 .git/index |
切换分支、git add、git commit 写索引时 |
HEAD.lock |
HEAD 引用 |
切换分支、重置 HEAD 时 |
packed-refs.lock |
打包后的引用文件 | git pack-refs、git fetch 服务端操作时 |
refs/heads/xxx.lock |
单个分支引用 | 更新某个分支指针时 |
config.lock |
仓库配置文件 | 修改 git config 时 |
FETCH_HEAD.lock |
FETCH_HEAD |
拉取远端更新时 |
shallow.lock |
shallow 克隆边界 | fetch、clone 相关操作时 |
也就是说,只要你在 .git 目录下看到任何以 .lock 结尾的文件,都意味着某次 Git 写操作中途被打断了。你的"切换分支报 index.lock"只是整个机制最常出现的代表,其他锁文件一旦残留,会以不同形式的报错出现,但本质都是一样的。
2.3 锁文件为什么容易残留
锁文件本身是正常机制,问题在于它太容易残留了。结合我实际踩过的坑,触发原因大致分几类:
- 操作被中断:切分支、
git add、git commit过程中你按了Ctrl+C,或者电脑断电、终端关闭,锁文件就留在原地。 - 多终端并发:一个终端在跑
git fetch,另一个终端同时执行git status或切换分支。正常情况下 Git 会提示等待或失败,但如果你在一个终端里强杀了 Git 进程,另一个终端可能就撞上锁。 - IDE 和编辑器自动操作:像 VS Code、IntelliJ 这类 IDE 会在后台自动执行
git fetch、刷新索引、自动 add 等操作。IDE 的 Git 插件和命令行 Git 同时操作时,容易产生锁竞争。 - 同步盘和杀毒软件:我把仓库放在同步盘里的时候遇到过很多次,同步盘正在上传
.git目录下的文件时,Git 的 rename 操作被外部程序介入,锁文件就留下了。 - 脚本和 CI 中断:你自己写的脚本里用了
git子进程,如果脚本异常退出,子进程被杀掉,锁也可能留下。
理解了这一点,你就能明白一个反直觉的事实:index.lock 存在的历史可能比你想象的要长,但触发它往往不是某一次"单一"操作,而是几次操作撞在一起的偶然结果。
3. 直接删文件为什么不是"彻底"解决:常见做法的三个隐患
3.1 你不确定那个进程是不是还活着
网上所有教程都让你删,但很少有文章提醒你:如果创建锁文件的 Git 进程还活着,你删掉锁文件的行为本身是有风险的。
举个例子。你在终端里执行了一个 git fetch,它正在写 FETCH_HEAD.lock。此时你在另一个终端看到报错提示锁文件存在,于是执行了 rm -f .git/FETCH_HEAD.lock。之后正在运行的 git fetch 执行到 rename 阶段,发现锁文件不见了,行为会变得不可预测——轻则报错退出,重则留下一个状态不一致的引用文件。
虽然大多数时候 index.lock 残留时创建它的进程已经死了,但"大多数时候"不是"永远"。真正可靠的工具应该先判断进程状态,而不是上来就删。
3.2 仓库不在当前目录,手动处理容易删错
很多人操作 Git 时并不是站在仓库根目录下的。你的仓库在 ~/project/backend,但你可能在 ~/project/backend/src/components 下切分支。此时报错信息里的路径是相对于 .git 目录的,你第一反应是去删 ./.git/index.lock,但当前目录根本没有 .git,因为在 src/components 这种子目录下,.git 要在上面好几层才能找到。
更麻烦的是,如果你用的是 Git 的 worktree 功能,.git 可能不是一个目录,而是一个文本文件,里面记录了真正的 gitdir 路径。这时候你连"找到 .git 目录"都要多做一步。
手动处理还会碰到权限问题。某些共享仓库或者用 sudo 初始化过的仓库,.git 目录下的文件可能是 root 所有,你当前用户去删会报权限不足。你得先 sudo,但 sudo rm 一个 Git 仓库内部文件,本身就是高风险动作。
3.3 删了之后立刻又出现,反反复复才最折磨人
最让人恼火的是另一种情况:删完 index.lock 后一切恢复正常,但五分钟后又报同样的错。遇到这种反复出现的锁文件,通常说明有某个后台程序在持续操作这个仓库,最常见的就是 IDE 的自动 fetch 和同步盘。此时"删文件"只是治标,你不光需要一个清理工具,还需要工具能告诉你这个锁文件是什么时候产生的、大概率和什么操作相关,这样才能顺藤摸瓜找到真正的元凶。
这也是我决定动手写一个全局命令行工具的原因:它不能只是一条 rm 的包装,而应该帮我把"每次遇到锁文件时都要重复做的那套判断"固定下来。
4. 全局命令行工具的整体设计与脚本实现
4.1 方案选型:为什么是 Python 而不是 Shell
这个工具最核心的需求有三个:跨平台、能判断文件是否被占用、能处理 worktree 等复杂情况。
有人可能会说 Shell 就够了,一条 rm -rf 不行吗?问题在于 Shell 脚本做文件句柄判断、跨平台兼容并不容易。在 macOS 上可以用 lsof,在 Linux 上不保证预装,在 Windows 上还得额外装工具。而 Python 的优势在于:
- macOS 和绝大多数 Linux 发行版都自带 Python 3,Windows 用户装了 Git 一般也有可用的 Python 3 环境。
- 标准库里的
os、subprocess、sys、argparse足够完成所有需求,不需要装任何第三方依赖。 - 代码可读性好,后续要扩展成检测 CI 环境、对接日志系统都方便。
所以我选择用 Python 写一个命令行脚本,命名为 git-unlock。这个命名是有讲究的:在 Unix 系统里,只要把可执行文件命名为 git-xxx 并放到 PATH 中,Git 就会把它识别为 git xxx 这个子命令。也就是说,装好后你可以直接输入 git unlock 来调用它,体验和 Git 原生命令完全一致。
4.2 完整脚本:git-unlock 的初版实现
下面这个版本我在 macOS 和 Linux 上都跑过,Windows 的 Git Bash 里也能用。核心逻辑是:
- 先定位当前仓库根目录,兼容普通仓库和 worktree。
- 扫描
.git目录下所有.lock文件。 - 对每个锁文件,读取文件修改时间。
- 用系统工具(
lsof)判断有没有进程正打开这个文件。 - 如果文件没被占用且修改时间超过阈值,判定为残留锁,自动删除。
- 对活跃锁或新产生的锁,给出提示,不自动处理;用户加
--force可以强制清理。
python复制#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
git-unlock: 安全清理 Git 仓库中的残留锁文件。
用法:
git unlock # 自动检测并清理残留锁
git unlock --force # 强制清理所有锁文件(有风险)
git unlock --max-age 60 # 只清理 60 秒前产生的锁
git unlock --path /path/to/repo # 指定仓库路径,默认当前目录
"""
import argparse
import os
import subprocess
import sys
import time
def get_repo_root(path):
"""获取指定路径对应的 Git 仓库根目录。"""
try:
result = subprocess.run(
["git", "rev-parse", "--show-toplevel"],
cwd=path,
capture_output=True,
text=True,
timeout=10,
)
except Exception as exc:
print(f"[git-unlock] 无法调用 git 命令:{exc}", file=sys.stderr)
sys.exit(2)
if result.returncode != 0:
print(
"[git-unlock] 当前目录不是 Git 仓库,"
"或者 git 命令不在 PATH 中。",
file=sys.stderr,
)
print(result.stderr.strip(), file=sys.stderr)
sys.exit(2)
return result.stdout.strip()
def resolve_git_dir(repo_root):
"""从仓库根目录定位真正的 .git 目录,兼容 worktree。"""
git_path = os.path.join(repo_root, ".git")
if os.path.isdir(git_path):
return git_path
# worktree 场景下 .git 是一个文件,内容形如 gitdir: /path/to/main/.git/worktrees/xxx
if os.path.isfile(git_path):
with open(git_path, "r", encoding="utf-8") as f:
content = f.read().strip()
if content.startswith("gitdir:"):
gitdir = content.split(":", 1)[1].strip()
return os.path.normpath(os.path.join(repo_root, gitdir))
print(f"[git-unlock] 无法解析 {git_path}", file=sys.stderr)
sys.exit(2)
def collect_lock_files(git_dir):
"""递归扫描 .git 目录下所有 .lock 文件,跳过 objects 目录。"""
lock_files = []
for dirpath, dirnames, filenames in os.walk(git_dir):
# objects 目录下的临时文件不属于我们要处理的锁,跳过可以避免误伤
if os.path.basename(dirpath) == "objects":
dirnames[:] = []
continue
for fn in filenames:
if fn.endswith(".lock"):
full_path = os.path.join(dirpath, fn)
rel_path = os.path.relpath(full_path, git_dir)
mtime = os.path.getmtime(full_path)
lock_files.append((rel_path, full_path, mtime))
return lock_files
def is_file_open_by_process(path):
"""判断文件是否被其他进程打开。
Unix/macOS 使用 lsof 检查;Windows 用独占打开方式粗略判断。
"""
if sys.platform.startswith("win"):
# Windows 下尝试用 r+b 模式打开文件,如果被独占则抛 PermissionError
try:
with open(path, "r+b"):
pass
return False
except PermissionError:
return True
except FileNotFoundError:
return False
except OSError:
return True
try:
result = subprocess.run(
["lsof", path],
capture_output=True,
text=True,
timeout=10,
)
except Exception:
# lsof 不存在或执行失败时,退回到"按时间判断"策略
return False
# lsof 返回 0 表示有进程打开该文件,返回 1 表示没有进程打开
return result.returncode == 0
def format_age(seconds):
"""把秒数转成可读的时长字符串。"""
if seconds < 60:
return f"{int(seconds)} 秒"
if seconds < 3600:
return f"{int(seconds // 60)} 分钟"
return f"{int(seconds // 3600)} 小时"
def main():
parser = argparse.ArgumentParser(
description="安全清理 Git 仓库中残留的锁文件"
)
parser.add_argument(
"--force",
action="store_true",
help="强制清理所有锁文件,即使它看起来仍被占用",
)
parser.add_argument(
"--path",
default=os.getcwd(),
help="仓库路径,默认使用当前目录",
)
parser.add_argument(
"--max-age",
type=float,
default=300,
help="锁文件超过多少秒且未被占用才自动清理,默认 300 秒",
)
args = parser.parse_args()
repo_root = get_repo_root(args.path)
git_dir = resolve_git_dir(repo_root)
lock_files = collect_lock_files(git_dir)
if not lock_files:
print(f"[git-unlock] 仓库 {repo_root} 中没有找到锁文件,一切正常。")
return
now = time.time()
cleaned = 0
skipped = 0
print(f"[git-unlock] 在 {git_dir} 下发现 {len(lock_files)} 个锁文件:")
for rel_path, full_path, mtime in sorted(lock_files):
age = now - mtime
opened = is_file_open_by_process(full_path)
print(f" - {rel_path} (已存在 {format_age(max(age, 0))}, {'被占用' if opened else '未被占用'})")
if args.force:
try:
os.remove(full_path)
print(f" [已删除] 强制清理")
cleaned += 1
except OSError as exc:
print(f" [失败] {exc}", file=sys.stderr)
skipped += 1
continue
if opened:
print(
f" [跳过] 文件正被进程占用,"
"不能盲目删除。如果确认没有操作,再加 --force。",
file=sys.stderr,
)
skipped += 1
continue
if age < args.max_age:
print(
f" [跳过] 文件刚产生 {format_age(age)},"
"可能是活跃操作留下的锁,暂不处理。"
"如需强制清理请加 --force。",
file=sys.stderr,
)
skipped += 1
continue
try:
os.remove(full_path)
print(f" [已删除] 残留锁清理完成")
cleaned += 1
except OSError as exc:
print(f" [失败] {exc}", file=sys.stderr)
skipped += 1
print()
print(f"[git-unlock] 完成:删除 {cleaned} 个,跳过 {skipped} 个。")
sys.exit(0 if cleaned > 0 else 1)
if __name__ == "__main__":
main()
4.3 关键逻辑逐段说明
有几个细节我单独拿出来讲,因为它们的取舍直接决定了这个工具是不是真的"安全"。
仓库定位部分,我先调用了 git rev-parse --show-toplevel,而不是自己一层层往上找 .git。这样做有个好处:Git 自己负责处理各种复杂场景,比如环境变量 GIT_DIR 被设置、当前目录是子模块、使用 worktree 等。你要是在脚本里自己写路径上升查找逻辑,很快会发现边界情况层出不穷。调用 Git 自己来解决 Git 的路径问题,是最省心也最不容易出错的做法。
worktree 兼容是容易被忽略的一个点。普通的 .git 是一个目录,但 worktree 场景下 .git 是一个文本文件,里面的内容类似 gitdir: /main/repo/.git/worktrees/dev。我通过读取文件内容来解析真正的 gitdir,这样即使你是在 worktree 里跑 git unlock,也能精准找到锁文件所在的位置。
扫描范围我用了递归遍历整个 .git 目录,但专门跳过了 objects 目录。原因很简单:objects 下面存放的是 Git 的对象文件,某些对象写入过程中也会产生临时文件,但那些临时文件不遵循 .lock 命名规则。我跳过这个目录的目的不是怕命中临时文件,而是为了避免不必要的 I/O——这个目录通常体积巨大,遍历它纯属浪费时间。
进程占用判断是整个工具安全性的核心。在 Unix 系系统上我用 lsof 检查是否有进程打开了这个文件,如果 lsof 报有进程持有,说明这个锁很可能还处于活跃状态,自动清理逻辑就会跳过它。lsof 在 macOS 上默认自带,Linux 上大多数发行版也预装了,实在没有的话我让脚本退回到"按时间判断"的策略,不至于直接报错。
时间阈值为什么默认 300 秒?我观察过正常情况下 Git 锁文件的寿命:一次 git fetch 或 git add 产生锁到释放锁,通常不会超过几秒钟。如果超过 5 分钟文件还没消失,基本可以断定是残留。当然也可能有特殊情况,所以这个值做成了可配置的参数,CI 环境里可以调小一些,比如 60 秒。
关于 --force,我的实现是:加了 --force 就无条件删除所有扫描到的 .lock 文件,不做任何判断。这其实是个有风险的操作,但保留它是为了应对一种场景:你明确知道某个 Git 进程已经死了,而 lsof 由于权限问题检测不到。脚本在输出里会明确提示风险,让使用者自己判断。
4.4 Windows 上的特殊处理
把 Windows 单独拿出来说,是因为我在这上面吃过亏。
Windows 下 lsof 一般不存在,我用的替代方案是"尝试用 r+b 模式打开文件"。如果文件被其他进程独占,Python 会抛出 PermissionError,据此判断文件正被占用。这个办法虽然不如 lsof 精准,但大多数场景下够用。
Windows 下还有一个更麻烦的问题:Git 命令本身可能不在 PATH 里。很多新手在 PowerShell 里运行 git 时遇到过"无法将 git 项识别为 cmdlet、函数、脚本文件或可运行程序的名称"的报错,这不是 Git 没装,而是 Git 的安装目录没加到 PATH。我的脚本里没有自动修复这个问题,因为这是一个环境配置问题,工具本身无能为力。但我在 get_repo_root 里做了错误处理,如果 git 命令找不到,会输出明确提示,而不是抛一个 Python 堆栈信息。
Windows 用户跑这个工具,我建议直接用 Git Bash,而不是 PowerShell 或 CMD。Git Bash 是随 Git for Windows 一起安装的,里面自带 Python 3 的话最好,没有的话可以在 Git Bash 里调用 Windows 的 Python 解释器。最稳的做法是先执行 python --version 确认环境,再继续。
5. 装成全局命令后的日常体验:实测效果与扩展用法
5.1 安装三步走,之后就能用 git unlock
整个安装过程不复杂,核心就是把脚本放到 PATH 里,并给它可执行权限。以 macOS 和 Linux 为例:
bash复制# 1. 把脚本保存为 git-unlock
# 可以放在 /usr/local/bin,也可以放在 ~/bin
curl -o /usr/local/bin/git-unlock https://your-server/git-unlock.py
# 2. 赋予可执行权限
chmod +x /usr/local/bin/git-unlock
# 3. 验证安装
git unlock --help
如果你把脚本放在 ~/bin,记得确保 ~/bin 在 PATH 中。可以在 ~/.bashrc 或 ~/.zshrc 里加一行:
bash复制export PATH="$HOME/bin:$PATH"
Windows 用户最简单的方式是把 git-unlock 脚本放进 Git 安装目录下的 usr/bin,例如 C:\Program Files\Git\usr\bin。因为 Git Bash 启动时会把那个目录加入 PATH。然后你同样可以在 Git Bash 里执行 git unlock。
如果你用的是 PowerShell 而不是 Git Bash,需要稍微折腾一下:要么把脚本打包成 exe,要么确保 python 在 PATH 中后用函数包装。鉴于 Git 操作最好还是在 Git Bash 里做,这个限制我认为可以接受。
5.2 一次真实的故障恢复过程
我拿最近一次实际遇到的场景来说明这个工具怎么用。
当时我在项目根目录执行 git switch release/1.0,Git 直接报 index.lock 错误。我不确定是不是刚才跑的一个 git commit --amend 脚本残留的,于是执行:
bash复制git unlock
输出如下:
text复制[git-unlock] 在 /home/user/project/.git 下发现 2 个锁文件:
- index.lock (已存在 15 分钟, 未被占用)
[已删除] 残留锁清理完成
- FETCH_HEAD.lock (已存在 3 秒, 未被占用)
[跳过] 文件刚产生 3 秒,可能是活跃操作留下的锁,暂不处理。如需强制清理请加 --force。
index.lock 是 15 分钟前产生的残留,被自动清理了。但 FETCH_HEAD.lock 是 3 秒前刚出现的,脚本判断这可能是一个正在进行的 fetch 操作,所以跳过了。我回去一看,果然有一个 IDE 的自动 fetch 正在跑。等它跑完,FETCH_HEAD.lock 自己就消失了。
这个例子很好地说明了"无脑删"和"智能清理"的区别。如果我只是 rm -rf .git/*.lock,那个正在写入的 FETCH_HEAD.lock 会被一并删掉,那个 fetch 操作可能就会出问题。
5.3 把 git unlock 挂进 CI 或作为预备命令
除了终端里手动调用,这个工具还能做两件更进阶的事。
第一,写进 CI 的错误恢复步骤。 如果你的构建机偶尔会因为上次任务被 kill 而留下锁文件,你可以在 CI 脚本的开头加一条:
bash复制git unlock --max-age 60 || true
--max-age 60 表示只清理 60 秒前产生的锁,|| true 的作用是即使没有找到锁文件、命令返回非零,也不会让 CI 失败。
第二,配合 shell 函数做"防呆"。 我曾经在 .bashrc 里写过这样一段:
bash复制function gco() {
git checkout "$@" 2>/dev/null || {
echo "切换分支失败,尝试清理残留锁文件..."
git unlock --force
git checkout "$@"
}
}
这样每次切分支遇到锁文件报错时,shell 会自动帮我跑一次 git unlock --force 再重试。注意这里我用了 --force,因为脚本已经自动重试了,说明此刻锁文件十有八九是残留,值得强制清理。
不过我并不推荐所有人都直接这样配。如果你的工作流里有多个终端同时在操作同一个仓库,--force 是有风险的。更稳妥的方式是先用不带 --force 的 git unlock 看输出,确认锁确实没被占用,再决定要不要重试。
5.4 和"目录泄露"相关的边界提醒
有一类特殊情况值得单独说:如果你怀疑一个目录是被爬虫或恶意工具泄露出来的 Git 仓库(网上经常讨论 git 目录泄露),千万不要在没确认安全的情况下用 git unlock 去恢复它。
正常的 git unlock 只处理 .git 目录下的 .lock 文件,不等于修复一个损坏的仓库。如果你从一个可疑来源拿到了别人导出的 .git 目录,里面的状态可能本来就不完整,我的工具不会也无法替你修复这些损坏。请把你的业务仓库和陌生来源的仓库严格分开处理,不要混用工具。
6. 一些额外的经验:锁文件反复出现时,真正的问题在别处
工具能帮你清理锁,但如果你发现自己几乎每周都要跑一次 git unlock,那真正的问题大概率不是 Git 本身,而是有东西在持续骚扰你的仓库。
我见过最多的几个"元凶":
- IDE 的自动 Git 操作。 VS Code 的
git.autofetch默认开启,它会每隔一段时间自动执行 fetch。IntelliJ 家族也有类似的自动更新机制。如果你的电脑性能一般,同时打开多个项目,IDE 启动时批量 fetch 很容易和手动操作产生锁竞争。 - 同步盘。 把仓库放进 Dropbox、百度网盘、OneDrive 这类目录里,等于给自己找麻烦。同步盘会实时监控文件变化并上传,Git 的原子替换操作在同步盘上会因为文件监视器的介入而变得不稳定,锁文件残留概率大增。
- 自动化格式化工具。 有些团队会在文件保存时自动执行 git 操作(比如自动 stage 或自动 commit),这种工具如果写得不够健壮,会在不该调 git 的时候调 git,锁竞争就是这么来的。
如果你反复遇到问题,建议按这个顺序排查:先关掉 IDE 的自动 fetch,再把仓库移出同步盘目录,最后检查是不是有自定义脚本在后台跑 git 命令。我见过有人为了解决锁文件问题,把 .git/index.lock 的路径加进杀软白名单,这确实治标,但如果你连哪个程序在动 Git 仓库都搞不清楚,很快会有下一个奇怪的问题冒出来。
从另一个角度看,频繁遇到锁文件也不完全是坏事。它至少说明你正在做的操作触发了 Git 的写入路径,那些从不看 index.lock 的人,可能只是在用 Git 做一次性下载而已。真正频繁提交、频繁切换分支的人,迟早都会遇到这个报错,区别只是有没有一个顺手、安全的处理工具。
最后再分享一个小技巧:如果你真的经常在多个终端里操作同一个仓库,Git 有一个环境变量 GIT_OPTIONAL_LOCKS=0 可以减少一部分锁的创建。它会让 Git 跳过一些"不必要"的锁操作,比如 status 命令可能就不再加锁了。代价是某些命令的并发安全性会降低。这个变量不适合全局设置,但在 CI 或只读脚本里临时加一下,能有效减少锁文件的产生。我的 git-unlock 工具解决的是锁文件残留之后的恢复,而这个变量解决的是减少锁文件产生的概率,两个搭配着用,日常 Git 操作的顺畅度会有比较明显的提升。
