使用Rebase操作抹去Git仓库中的敏感信息
使用Git的时候,有时候会碰到需要从Git仓库中永久“抹除”某些敏感信息的情况。例如不小心提交了密码之类的信息到仓库,此时只抹掉这些信息重新提交是没有用的,因为其他人仍然可以通过Git历史看到这些敏感信息。因此需要一种方法将这些信息彻底从仓库中抹去。
如果去网上搜索的话,能很容易找到使用branch-filter
来处理的方法,例如
1 | git filter-branch --tree-filter "find . -name '*.*' -exec sed -i '' -e 's/OLDSTRING/NEWSTRING/g' {} \;" -f |
写法有很多种,但是思路都差不多,就是遍历一遍所有的提交,对这些提交执行指定的命令(例如用sed
替换指定的内容,或者移除相关文件),然后重新生成新的提交和分支。
不过这种思路对于我来说却不太受用,原因有几个:
- 命令行掌握不太好,看到这种命令都不太认识,完全不敢直接放在项目中去跑
- 直接进行字符串级别的替换,在某些情况下不够用,例如想通过更复杂的编辑手段(新增文本、修改文本、删除文本同时操作)抹除敏感信息
- 直接对整个仓库/整个文件进行字符串级别的替换还是有些不放心,毕竟要修改的部分是明确的,却无法明确地指定这个命令只修改这一部分信息
那怎么办呢?其实在这种场景下,也可以尝试使用git rebase
来解决问题。
rebase是干什么的
rebase
顾名思义,就是重新确定一个提交(一个分支)的“基”,这个“基”就是指它的祖先元素。具体的做法是,首先将提交退回到“基”所在的点,然后将之前做过的提交在这个“基”的基础上重复做一遍。相当于修改了当前分支衍生出来的基础,因此中文也被译为“变基”。
还是举个例子:
新建一个仓库,然后做两次提交A1
、A2
:
1 | # 初始化 |
接下来分成两个分支,分别进行提交B1
、B2
和C1
、C2
:
1 | # 创建新分支 |
于是得到一张这样的分支图:
此时new
和master
两个分支分别指向B2
和C2
两次提交,而他们的共同祖先(即前文中说的“基”,这个说法不严谨,仅为理解),则是A2
这次提交。此时我们就可以用rebase
来改变其中某一个分支的走向。例如,让C1
在B2
的基础上修改,即让master
分支的提交顺序变成A1-A2-B1-B2-C1-C2
:
1 | # 对哪个分支rebase就切换到哪个分支 |
而此时就出现了冲突,因为C1
和B1
两次提交都对1.txt
做了修改。
1 | A1 |
我们选择手工解决冲突,将B1
放前面,C1
放后面,解决完之后继续rebase
1 | # 用add标记解决完冲突 |
此时分支图会变成这样,表明C1这次提交已经变基成功。但同时也能看到C2在变基的时候也产生了冲突。
这里和上面一样处理即可。完成后就能看到变基后的分支图。
需要说明的是,尽管提交的描述信息(commit message)没变,但是C1
、C2
这两次提交实际上是新产生的,因此它们的commit id和之前的提交是完全不同的。
通过这个例子,能清楚地看到rebase
命令的作用,即改变提交(分支)的基础。一般来说,在多人协作过程中,适合将同一分支互相拉取变更的操作使用rebase
来完成,这样可以保持同一分支的提交历史是线性的,方便回溯。
使用rebase改变Git历史
在上面rebase
的例子中,还有一个点值得注意,以C1
这次提交为例,rebase
前后两种情况下,虽然都是在1.txt
结尾添加C1
这行文字,但是基础和结果都是不同的。在rebase
之前,1.txt
的内容是由A1
变成A1\nC1
,而在rebase
之后则是由A1\nB1
变为A1\nB1\nC1
。
可见在rebase
的时候,不止是提交的父节点(“基”)会变,文件内容也有所变化。而我们之前在rebase
时面临的冲突,也正是因为这个变化所带来的。但同时,正因为有这样一个变化,使得我们有机会通过rebase
的方式来永久改写Git仓库中某一个文件的历史。
仍然看上面的例子,现在只看rebase
之后的情况。在C1
这次提交中,我们为文件1.txt
在结尾处添加了内容C1
。假设这个C1
是一个很敏感的信息(例如密码),我们要如何将它从仓库历史中抹去呢?
首先我们在C1提交之前找到一个点,例如B2,然后基于它新建一个分支。(例如new
这个分支。)接下来在这个分支上,对文件1.txt
进行修改,例如我们增加一行C2
。即1.txt
内容变为
1 | A1 |
并进行一次提交。
接下来,我们对C2这次提交(即master
分支)进行rebase
操作:
1 | > git checkout master |
此时git会告诉我们,产生了冲突。
1 | First, rewinding head to replay your work on top of it... |
冲突内容正是原来的C1
和我们刚在新分支上添加的C2
:
1 | A1 |
前文假设,C1
是我们要删除的敏感信息,因此此时手工解决冲突,将C1
删除,留下C2
。然后git rebase --continue
即可。
可以看到我们的提交记录变成了这样:
敏感信息被彻底删除了。
小结
上面只是展示了一种简单的情况,但已经足够说明使用rebase
来删除代码库中敏感信息的核心思路和关键步骤了。
有一些值得注意的细节:
- 由于C1提交和重写C1的提交都只修改了一行代码,因此在
rebase
过程中,把这一行的冲突解决完,并且git rebase --continue
时,会提示没有变更(因为唯一的变更在冲突解决过程中被编辑好了),此时需要使用git rebase --skip
跳过这次提交。 - 上面我们是使用
rebase
操作时,编辑冲突的时机来编辑代码文件,从而将C1
这个敏感信息删除的。如果无法保证一定产生冲突,则可以使用git rebase -i
(交互式变基)来手工指定需要对哪些提交进行编辑,从而在不一定有冲突时,也有机会编辑代码文件,来将敏感信息删除。关于交互式变基,可参考网络上相关文档。 - 如果敏感信息在第一次提交就被带入版本库了,则上面说的“在C1提交之前找到一个点”无法完成。此时可以用
git checkout --orphan branch-name
来创建一个完全空白且没有父节点的分支,并且将当前分支的提交基于这个新的空分支来进行rebase
,从而获得编辑代码删除敏感信息的机会。
最后,一个提醒:不论用什么方法来修改版本库历史,都是在重写历史,虽然看起来提交的commit message是一样的,但是却是完全全新的提交和分支发展路径。当推送到代码库时,需要使用git push --force
来强制推送,其他人则需要使用git pull --rebase
来重写本地分支。