

避免提交非文本文件到 git 仓库
source link: https://blog.wolfogre.com/posts/git-text/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

避免提交非文本文件到 git 仓库
为了避免提交不必要的文件到 git 仓库,我们通常需要在 .gitignore 文件里写上大量的规则来指示 git 忽略不必要的文件。然而,.gitignore 是靠匹配文件名、文件后缀名、文件路径来忽略文件的,这就导致难免会有一些漏网之鱼。
我们很难一开始就能写出一个完美的 .gitignore 文件,更多的时候是随着项目的推进,根据实际情况修补 .gitignore。这里顺便推荐一个帮助生成 .gitignore 文件的工具 gitignore.io。
但是,如果很不幸,某次 commit 真的不小心把一个二进制文件提交进了仓库,等到发现时,可能已经 push 到了远端仓库且又发生了很多 commit 了,这时候,删除工作目录的二进制文件再提交是无济于事的了,大块头的二进制文件已经成为了仓库的历史内容,顿时将仓库的尺寸撑大好几兆甚至几十兆,你正常提交几百次可能都产生不了这样的效果。且这份阴影将一直伴随着这个仓库的存在而存在,你每一次 clone 都要忍受仓库过大带来的带宽负载和无谓耗时。这时候想要弥补,就只能用 reset hard 和 push force 进行一顿骚操作,但这样做的风险成本很高,在多人合作的项目中几乎是不可能完成的。
我自认为发生这样的错误是低级的,是不能被原谅的,但是打脸的是,我自己还是会偶尔犯这样的错误。更恼人的是,被我不小心提交的往往是 golang 编译的二进制文件,编译机制决定了这样的文件往往只有一个且动不动就有几十兆大小,这对 git 仓库的拖累往往是致命的。
于是我决定想个一劳永逸的办法。
git 钩子与 file 命令
和其它版本控制系统一样,git 能在特定的重要动作发生时触发自定义脚本,称为 git 钩子。
其中有个钩子叫pre-commit
,在 commit 之前触发运行,如果该钩子以非零值退出,git 将放弃这次 commit。
很自然的,我开始思考可不可以写个 pre-commit 钩子来检查每次 commit 的内容,如果发现其中有二进制文件则阻止 commit。
那么下一个问题就是如何判断一个文件是不是二进制文件。
当然这里的判断肯定不是基于文件后缀名,因为它既然已经被 .gitignore 遗漏,说明它的后缀名很可能是有问题的甚至是没有后缀名,这里要给 commit 做最后一道防线,一定是基于文件实际内容做审查的,否则就形同虚设了。
一开始的想法是基于文件尺寸进行判断,超过一定大小的则认为是二进制文件,但很显然这样做很蹩脚。事实上,网上已经有一些资料教你设置钩子限制提交文件尺寸的方法,如 How to limit file size on commit,但是要注意,现在的需求是避免二进制文件被提交,而不是避免大文件被提交,即使这两个需求虽然很接近但仍不是同一个需求,二进制文件也可能不那么大,文本文件也可能尺寸爆表。。
帮助我解决这个问题的是 file 命令。是的,file 也是个命令,还是一个历史悠久的常见命令,在绝大多数 linux 发行版里你都可以直接找到它而无需安装,在 macOS 里或者 windows git bash 里也是如此。它的功能是检测一个文件的文件类型,形如:
$ file test.txt
test.txt: ASCII text
$ file test.gz
test.gz: gzip compressed data, was "test", last modified: Fri Feb 1 07:44:48 2019, from Unix, original size 3
file 命令是通过读取文件内容,在结合预置的判定规则来判断文件类型的,所以即使我起一些容易误导人的文件名,也不会影响判断结果,比如:
$ cp test.txt test-txt.gz
$ cp test.gz test-gz.txt
$ file test-txt.gz
test-txt.gz: ASCII text
$ file test-gz.txt
test-gz.txt: gzip compressed data, was "test", last modified: Fri Feb 1 07:44:48 2019, from Unix, original size 3
不过你可以看到,默认的输出结果啰里吧嗦没有任何规则可言,所以我们可以让 file 以 MIME(多用途互联网邮件扩展类型)标准输出,这样可读性要好很多:
$ file --mime-type test.txt
test.txt: text/plain
$ file --mime-type test.gz
test.gz: application/x-gzip
这里有份官方的 MIME 类型列表,我花了点时间过了一遍,基本可以确认使用 text/*
来判断文件是否是文本文件是没有问题的。但在实际实验中发现还需要额外考虑空文件的问题,空文件本质上说是没有”文件类型“这么一说的,既可以认为是文本文件也可以认为是二进制文件。但 git 有个特性,git add
时会忽略空文件夹,但不会忽略空文件,所以我们在开发过程中往往会在空文件夹里放一个空文件来强制让这个文件夹被提交,由此可见,这里应当视空文件为文本文件才是。
使用 file 检测空文件的 MIME 类型,结果是 inode/x-empty
,所以最后的判断规则是:文件的 MIME 类型为 text/*
或 inode/x-empty
则认为是文本文件。
综合上述思路,我写了一个小工具:git-text。
新工具 git-text
git-text 最核心的东西其实就是一个用于做 pre-commit 钩子的脚步文件,目前它的长度没超过 20 行,即使我把它全文粘贴到这里也不会有撑文章字数之嫌:
#!/bin/bash
# Introduce: https://github.com/wolfogre/git-text
# Version: v1.0.0
set -e
FILES=$(git status --short | grep -E "^(A|M)" | awk '{print $2}' | xargs)
if [[ -z "$FILES" ]]; then
exit 0
fi
WRONG_FILES=$(file --mime-type ${FILES} | grep -v -E "(text/[A-Za-z0-9.-]*|inode/x-empty)$" | cat)
if [[ -n "${WRONG_FILES}" ]]; then
echo "DELETE NON-TEXT FILES OR USE 'git commit -n':"
echo -e "${WRONG_FILES}"
exit 1
fi
echo "ALL FILES ARE TEXT:"
file --mime-type ${FILES}
脚本内容的含义我就不再解释了,上文我已经将它的原理阐尽,无非是一些脑洞和小技巧。
但让人蛋疼的是,一个 git 仓库的钩子并不是这个仓库的一部分,换句话说,我只能给一个本地仓库设置钩子,钩子本身是不会被推送到远端仓库的,自然也不会被其他人拉取到,这就意味着,每次我 clone 或 init 一个仓库,就需要在特定位置创建一个钩子文件,写入上述的脚步内容,还得设置下文件的可执行权限,这未免也太麻烦了。
目前我能想到的解决这个问题的办法是使用 git 别名,设置一个全局的新命令来简化给仓库安装钩子的步骤:
git config --global alias.text '!f() { set -ex ; hookfile=$(git rev-parse --show-toplevel)/.git/hooks/pre-commit ; curl -sSL https://raw.githubusercontent.com/wolfogre/git-text/master/pre-commit -o $hookfile ; chmod +x $hookfile ; }; f'
这里设置了一个新命令叫 git text
,当执行它时会下载 GitHub 上的钩子文件到当前仓库的指定位置,再设置文件具有可执行权限,完成给一个仓库安装钩子的工作。
首先创建一个本地仓库方便测试:
$ mkdir test-repo
$ cd test-repo/
$ git init
Initialized empty Git repository in /root/test-repo/.git/
执行 git text
为这个仓库安装钩子:
$ git text
++ git rev-parse --show-toplevel
+ hookfile=/root/test-repo/.git/hooks/pre-commit
+ curl -sSL https://raw.githubusercontent.com/wolfogre/git-text/master/pre-commit -o /root/test-repo/.git/hooks/pre-commit
+ chmod +x /root/test-repo/.git/hooks/pre-commit
测试提交一些文本文件,可以看到提交是成功了的:
$ touch test-empty-file
$ echo ok > test-text-file
$ git add --all
$ git commit -m "test commit"
ALL FILES ARE TEXT:
test-empty-file: inode/x-empty
test-text-file: text/plain
[master (root-commit) f17008d] test commit
2 files changed, 1 insertion(+)
create mode 100644 test-empty-file
create mode 100644 test-text-file
测试提交非文本文件,可以看到提交提交被终止,并提示类型异常的文件:
$ gzip test-text-file
$ git add --all
$ git commit -m "test commit"
DELETE NON-TEXT FILES OR USE 'git commit -n':
test-text-file.gz: application/x-gzip
如果我非要提交这个文件,比如需要提交一些图片等资源文件,可以在提交时加上 -n
参数来绕过钩子:
$ git commit -n -m "force commit non-text"
[master 7c01515] force commit non-text
2 files changed, 64 deletions(-)
delete mode 100644 test-text-file
create mode 100644 test-text-file.gz
关于这个工具更多的内容,可查看项目的 GitHub 页。
目前我对这个工具的版本定位于“实验阶段”,因为它虽然满足了我日常的需求,但还没有经过更长时间的考验和更广泛的测试,所以如何你在使用过程中发现有什么问题,莫要恼火,提个 Issue 且让我瞧瞧便是。
在使用过程中,发现仓库如果使用了 git-submodule,是会跟踪一个空目录的,这导致 git-text 误以为提交了非文本文件,而拒绝 commit。
为了兼容这种情况,我已经修改了钩子代码,放行 MIME 为 inode/directory
的文件(夹)。
Recommend
-
12
清除Git仓库未托管的文件 需使用命令行clean git仓库的地方不多,就没有怎么记,在blog里面记录一下,下次好找。
-
4
step 1git filter-branch --force --index-filter "git rm -rf --cached --ignore-unmatch filename" --prune-empty --tag-name-filter cat -- --all step2rm -rf .git/refs/original &...
-
9
git tips之只提交文件中的某些变更 git tips之只提交文件中的某些变更 有时候可能会不小心在同一个文件中对针对多个需求了修改,但是在提交代码时只想提交针对某个变更做的修改。 这时候可以试试 git add --patch ${file} 命...
-
7
git merge 的提交没有出现在文件提交记录上? V2EX › 程序员 git merge 的提交没有出现在文件提交记录上?
-
3
V2EX › git Git 提交时莫名其妙删除文件 ddllzz · 7 小时 43 分钟前 · 1294 次点击 ...
-
8
jgit获取git仓库中的部分文件 March 20, 2019 in java ...
-
9
Git 项目仓库中的 OWNERS 文件 2020年5月21日 | 字数 1286 |
-
9
git 不提交权限改变的 Git识别文件权限修改 刚打开IDE,工作区的代码状态全部变成修改未提交的状态了?这是这么回事? 这是因为Git忽略文件权限或者拥有者改变导致的git状态变化。 默认Git会记录文件的权限信息,如果文件的权...
-
4
完全删除 Git 仓库的文件 2021-01-25 约 407 字 预计阅读 1 分钟 ...
-
8
Git 历史提交日志导出到文件中 2023-11-07 Git ...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK