从Git历史记录中删除敏感文件及其提交记录

509

我想将一个Git项目放到GitHub上,但它包含某些包含敏感数据的文件(用户名和密码,例如capistrano的/config/deploy.rb)。

我知道可以将这些文件名添加到.gitignore中,但这并不会从Git中删除它们的历史记录。

我也不想通过删除/.git目录来重新开始。

有没有一种方法可以删除您Git历史记录中特定文件的所有痕迹?


1
相关内容 https://help.github.com/articles/removing-sensitive-data-from-a-repository/ - Trevor Boyd Smith
12个回答

605
就实际目的而言,首要的是更改您的密码!从您的问题中并不清楚您的 git 存储库是否完全是本地的,或者是否已经存在远程存储库;如果它是远程的且没有受到他人保护,则存在问题。如果在您修复此问题之前有任何人克隆了那个存储库,他们将在他们的本地机器上拥有您密码的副本,并且没有办法强制他们更新到您“修复”的版本,因为它已经从历史记录中删除了。您唯一可以做的安全操作是在您使用过该密码的所有地方更改为其他密码。


解决方案如下。GitHub正是通过FAQ回答了这个问题

Windows 用户请注意:在此命令中,请使用双引号(")而不是单引号

git filter-branch --index-filter \
'git update-index --remove PATH-TO-YOUR-FILE-WITH-SENSITIVE-DATA' <introduction-revision-sha1>..HEAD
git push --force --verbose --dry-run
git push --force

更新于2019年:

以下是常见问题解答中的当前代码:

  git filter-branch --force --index-filter \
  "git rm --cached --ignore-unmatch PATH-TO-YOUR-FILE-WITH-SENSITIVE-DATA" \
  --prune-empty --tag-name-filter cat -- --all
  git push --force --verbose --dry-run
  git push --force
请记住,一旦您将此代码推送到像GitHub这样的远程存储库,并且其他人已经克隆了该远程存储库,那么您现在处于重写历史记录的情况下。当其他人尝试在此之后拉取您的最新更改时,他们将收到一条消息,指示更改无法应用,因为它不是快速转发。
为了解决这个问题,他们必须删除其现有存储库并重新克隆它,或按照“从上游重置恢复”下的说明操作git-rebase手册
提示:执行git rebase --interactive
在将来,如果您意识到在将更改推送到远程存储库之前不小心提交了某些包含敏感信息的更改,则有一些更简单的修复方法。如果您的最后一次提交是添加敏感信息的提交,则可以简单地删除敏感信息,然后运行:
git commit -a --amend

这将使用新的更改修改先前的提交,包括使用 git rm 删除整个文件。如果更改在历史记录中更早但仍未推送到远程存储库,则可以进行交互式变基:

git rebase -i origin/master

这将打开一个编辑器,其中包含您自上次与远程存储库共同祖先以来所做的提交。将任何表示具有敏感信息的提交行中的 "pick" 更改为 "edit",然后保存并退出。Git 将遍历更改,并在可进行以下操作的位置停留:

$EDITOR file-to-fix
git commit -a --amend
git rebase --continue

对于每个包含敏感信息的更改,请最终返回到您的分支,然后可以安全地推送新更改。


4
成功执行了。我之前有些翻译上的困惑。我使用链接代替了命令。同时,基于 ripper234 的建议,Windows 命令需要双引号,基于 MigDus 的建议,需要完整路径,并且不要包含链接中作为换行符的“\”字符。最终命令看起来像这样:git filter-branch --force --index-filter "git rm --cached --ignore-unmatch src[Project][File].[ext]" --prune-empty --tag-name-filter cat -- --all - Eric Swanson
3
你的filter-branch代码和你链接到的Github页面上的代码似乎有一些实质性的差异,例如,他们第三行的代码是--prune-empty --tag-name-filter cat -- --all。这个解决方案是否已更改或者我漏掉了什么? - geotheory
2
这个解决方案看起来很不错,但是如果我在初始提交中引入了要删除的文件<introduction-revision-sha1>..HEAD就无法工作。它只会从第二次提交开始删除该文件。(如何将初始提交包含在提交范围内?)这里指出了安全的方法:https://help.github.com/articles/removing-sensitive-data-from-a-repository/ git filter-branch --force --index-filter \ 'git rm --cached --ignore-unmatch PATH-TO-YOUR-FILE-WITH-SENSITIVE-DATA' \ --prune-empty --tag-name-filter cat -- --all - white_gecko
2
这个命令不会删除所有分支和标签中的文件。官方Github帮助可以完美地解决此问题。 - transang
1
这将从操作系统文件系统中删除文件!!!git filter-branch --force --index-filter "git rm --cached --ignore-unmatch PATH-TO-YOUR-FILE-WITH-SENSITIVE-DATA" --prune-empty --tag-name-filter cat -- --all,路径是相对于git目录的相对路径,没有前导斜杠。 - toddmo
显示剩余15条评论

139
更改您的密码是个好主意,但如果想要从存储库历史记录中删除密码,我建议使用BFG Repo-Cleaner,这是一个更快、更简单的替代方法,专门设计用于从Git存储库中删除私有数据,与git-filter-branch相比。

创建一个名为private.txt的文件,并列出您想要删除的密码等内容(每行一个条目),然后运行以下命令:

$ java -jar bfg.jar  --replace-text private.txt  my-repo.git

您的存储库历史记录中所有大小低于阈值大小(默认为1MB)的文件都将被扫描,任何匹配的字符串(不在您的最新提交中的字符串)都将被替换为字符串"***REMOVED***"。然后,您可以使用git gc清除死数据:

$ git gc --prune=now --aggressive

BFG Repo-Cleaner通常比运行git-filter-branch快10-50倍,并且选项已简化并针对这两种常见用例进行了定制:

  • 删除超大文件
  • 删除密码,凭据和其他私人数据

完整声明:我是BFG Repo-Cleaner的作者。


10
这是一个巨大的胜利。经过几次尝试,我能够使用它彻底删除私有存储库中包含敏感信息的提交并强制更新远程存储库的修订历史。需要注意的是,您必须确保存储库的末端(HEAD)本身没有敏感数据,因为此提交被视为“受保护”,不会被此工具修订。如果有,请手动清除/替换并 git commit。否则,这是开发人员工具箱中的新工具 +1 :) - Matt Borja
2
根据我最近的评论,假设您的应用程序当前位于分支的顶部或头部(即最新提交),它不应该像您预期的那样破坏您的应用程序。在遍历和修订其余提交历史记录时,此工具将明确报告您最后一次提交的“这些是受保护的提交,因此它们的内容不会被更改”。但是,如果您需要回滚,则确实需要在刚刚回滚到的提交中搜索“*** REMOVED ***”。 - Matt Borja
2
如果您已经安装了Java或者不介意安装它,那么使用BFG工具可以让您的工作事半功倍。但是需要注意的是,如果要删除的文件包含在HEAD中,BFG将拒绝执行删除操作。因此最好先提交一个commit,将需要删除的文件从HEAD中移除,然后再运行BFG。完成后,您可以撤销上一次提交,这样就不会对代码产生任何影响了。 - Fr0sT
1
这实际上应该被接受为正确答案。完全符合说明书! - gjoris
2
git push --force 丢失了。 - Gino Pane
显示剩余9条评论

51

git filter-repo 现已正式推荐使用,取代 git filter-branch

这在 Git 2.5 的 git filter-branch 手册中提到了。

使用 git filter repo,您可以通过以下方式删除某些文件:从 git/GitHub 历史记录中删除文件夹及其内容

pip install git-filter-repo
git filter-repo --path path/to/remove1 --path path/to/remove2 --invert-paths

这将自动删除空提交。

或者,您可以使用以下操作将某些字符串替换为:如何在整个Git历史中替换字符串?

git filter-repo --replace-text <(echo 'my_password==>xxxxxxxx')

如果你已经将代码推送到GitHub,那么强制推送是不够的,必须要删除仓库或联系支持团队。即使你在一秒钟后强制推送,也是不够的,因为如下所述。唯一有效的行动方案是:如果泄露了类似密码这样可变的凭据,请立即修改你的密码,并考虑使用更多的OAuth和API密钥!如果没有泄露(裸照),你需要考虑以下问题:是否在意仓库中的所有问题都被清空?如果不在意,请删除仓库;如果在意,则需要联系支持团队。如果泄漏对你非常重要,以至于你愿意让仓库停机一段时间以减少泄漏的可能性,可以在等待GitHub支持团队回复时将其设置为私有。强制推送一秒钟后是不够的,因为: 如果您删除存储库而不仅是强制推送,则提交甚至会立即从API中消失并显示404,例如https://api.github.com/repos/cirosantilli/test-dangling-delete/commits/8c08448b5fbf0f891696819f3b2b2d653f7a3824。即使您使用相同的名称重新创建另一个存储库,此方法也有效。
为了测试这一点,我创建了一个repo:https://github.com/cirosantilli/test-dangling,然后执行了以下操作:
git init
git remote add origin git@github.com:cirosantilli/test-dangling.git

touch a
git add .
git commit -m 0
git push

touch b
git add .
git commit -m 1
git push

touch c
git rm b
git add .
git commit --amend --no-edit
git push -f

参考链接: 如何从GitHub中删除悬空提交?


1
如果存储库是分叉网络的一部分,则将存储库设为私有或删除它可能无济于事,甚至可能会使问题更加严重。在GitHub上的分叉网络似乎共享一个内部裸仓库,因此一个分支中的提交也可以通过其他分支进行检索。将存储库设为私有或删除它会导致与分叉网络的分离,敏感提交现在在每个剩余的裸仓库中都被复制。在两个裸仓库上运行GC之前,这些提交将继续通过分支可访问。 - knuton
我尝试了 git filter-repo --replace-text <(echo 'my_password==>xxxxxxxx'),虽然它确实起作用了,但是它也使得所有的提交看起来像是今天发生的。有没有一种方法可以在更改历史记录中的文件时保留提交日期? - devordem
1
@devordem 我没有复现这个问题,在 git 2.37.2、filter-repo ac039ecc095d 和 Ubuntu 22.10 上,作者和提交者的日期都已经为我保留。 - Ciro Santilli OurBigBook.com
1
没关系,我后来发现是我做了其他事情重写了提交日期,而不是 git-filter-repo - devordem
@devordem 太棒了,谢谢你的确认。 - Ciro Santilli OurBigBook.com

20

2
此答案中的链接似乎已经失效。 - Some Guy
这个链接指向一个病毒/垃圾邮件。 - paltaa

19

我推荐使用 David Underhill 的这个脚本,对我来说非常好用。

它与 natacado 的 filter-branch 命令一起使用,可以清理 filter-branch 留下的混乱:

rm -rf .git/refs/original/
git reflog expire --all
git gc --aggressive --prune

完整脚本(全部归功于David Underhill)

#!/bin/bash
set -o errexit

# Author: David Underhill
# Script to permanently delete files/folders from your git repository.  To use 
# it, cd to your repository's root and then run the script with a list of paths
# you want to delete, e.g., git-delete-history path1 path2

if [ $# -eq 0 ]; then
    exit 0
fi

# make sure we're at the root of git repo
if [ ! -d .git ]; then
    echo "Error: must run this script from the root of a git repository"
    exit 1
fi

# remove all paths passed as arguments from the history of the repo
files=$@
git filter-branch --index-filter \
"git rm -rf --cached --ignore-unmatch $files" HEAD

# remove the temporary history git-filter-branch
# otherwise leaves behind for a long time
rm -rf .git/refs/original/ && \
git reflog expire --all && \
git gc --aggressive --prune

如果将最后两个命令更改为以下内容,它们可能会更好地运行:

git reflog expire --expire=now --all && \
git gc --aggressive --prune=now

2
请注意,你对 expire 和 prune 的使用是错误的。如果你没有指定日期,它会默认将所有早于两周的提交都清除掉。而你想要的是清除所有的提交,请执行以下命令:git gc --aggressive --prune=now - Adam Parkin
@Adam Parkin 我会将回答中的代码保持不变,因为它来自David Underhill网站上的脚本,你可以在那里留言,如果他改变了,我会更改这个答案,因为我对Git并不是很熟悉。在修剪之前的过期命令不会影响它,对吗? - Jason Goemaat
1
@MarkusUnterwaditzer:对于已推送的提交,那个方法不起作用。 - Max Beikirch
1
也许你应该把所有的命令都放在你的回答中,这样会更加一致,而且不需要将不同的帖子进行心理组合 :) - Andrew Mao

16

以下是关于Windows下的解决方案:

git filter-branch --tree-filter "rm -f 'filedir/filename'" HEAD

git push --force

请确保路径正确,否则此方法将无法生效。

希望对您有所帮助。


13

使用 filter-branch 命令:

git filter-branch --force --index-filter 'git rm --cached --ignore-unmatch *file_path_relative_to_git_repo*' --prune-empty --tag-name-filter cat -- --all

git push origin *branch_name* -f

对于Windows用户,请确保在第一行中将单引号替换为双引号。 - undefined

8
请明确:被接受的答案是正确的。首先尝试它。然而,对于某些用例来说,这可能过于复杂,特别是当你遇到令人讨厌的错误,比如“fatal: bad revision --prune-empty”,或者真的不关心你的repo的历史记录时。
另一个选择是:
  1. cd到项目的基本分支
  2. 删除敏感代码/文件
  3. rm -rf .git/ #从代码中删除所有git信息
  4. 转到github并删除您的存储库
  5. 按照此指南将您的代码推送到新存储库,就像通常一样 - https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/
这当然会删除您的github repo和本地git repo中的所有提交历史分支和问题。如果这是不可接受的,您将不得不使用其他方法。
称之为核选项。

5
在我的安卓项目中,我使用了一个名为 admob_keys.xml 的独立 xml 文件,存放在 app/src/main/res/values/ 文件夹中。为了删除这个敏感文件,我使用了下面的脚本,并且它完美地运行了。
git filter-branch --force --index-filter \
'git rm --cached --ignore-unmatch  app/src/main/res/values/admob_keys.xml' \
--prune-empty --tag-name-filter cat -- --all

3

我曾经不止一次地需要这样做。请注意,这仅适用于一次只操作一个文件的情况。

  1. 获取修改了文件的所有提交记录列表。最底部的一个将是第一个提交:

    git log --pretty=oneline --branches -- pathToFile

  2. 使用上一个命令中得到的第一个提交sha1和文件路径,填写下面的命令以从历史记录中移除文件:

    git filter-branch --index-filter 'git rm --cached --ignore-unmatch <path-to-file>' -- <sha1-where-the-file-was-first-added>..


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接