如何替换整个Git历史记录中的字符串?

27

我有一个密码已经提交到我的Git仓库中的几个文件中。是否有一种方法可以自动替换整个历史记录中的此密码为其他字符串,以便没有痕迹?理想情况下,如果我能编写一个简单的bash脚本来接收要查找和替换的字符串,并自行完成所有工作,类似于:

./replaceStringInWholeGitHistory.sh "my_password" "xxxxxxxx"

编辑:这个问题不是那个问题的重复,因为我询问的是如何替换字符串而不是删除整个文件。


可以做到。你把你的代码库发布到远程服务器上了吗(如github、gitlab或其他)?还有其他人在与它一起工作吗? - Techniv
严格来说,这是我们公司的账户,只有少数人可以访问它,并且我们在自己的服务器上使用内部GitHub仓库。但总的来说,现在每个有权限访问仓库的人都是值得信任的。 - Karol Selak
3个回答

27

git filter-repo --replace-text

Git 2.25的man git-filter-branch已经明确建议使用git filter-repo而不是git filter-tree,所以我们就这么做吧。

安装https://superuser.com/questions/1563034/how-do-you-install-git-filter-repo/1589985#1589985

python3 -m pip install --user git-filter-repo

然后使用:
echo 'my_password==>xxxxxxxx' > replace.txt
git filter-repo --replace-text replace.txt

或者使用Bash魔法实现等效功能:
git filter-repo --replace-text <(echo 'my_password==>xxxxxxxx')

使用这个简单的测试存储库进行了测试:https://github.com/cirosantilli/test-git-filter-repository和替换字符串。
d1==>asdf
d2==>qwer

默认情况下,上述操作会对所有分支生效(真是太烦人了!),如果只想对特定分支进行操作,请使用以下方法:git filter-repo: can it be used on a specific branch? 例如:
--refs HEAD
--refs refs/heads/master

仅对指定的提交范围进行操作,您可以使用 git filter-repo 来修改一系列提交,而不是整个分支历史记录。如何使用 git filter-repo 仅修改一系列提交,而不是整个分支历史记录?

--refs HEAD~2..master
--refs HEAD~2..HEAD

选项--replace-text的文档位于:https://github.com/newren/git-filter-repo/blob/7b3e714b94a6e5b9f478cb981c7f560ef3f36506/Documentation/git-filter-repo.txt#L155

--replace-text <expressions_file>::

一个包含表达式的文件,如果找到,则将其替换。默认情况下,每个表达式都被视为文字文本,但支持regex:glob:前缀。您可以以==>结尾并带有一些替换文本来选择除默认值***REMOVED***之外的替换选项。

如何在单个文件中进行替换:git-filter-repo replace text by expression in a single file

当然,一旦你公开了一个密码,就已经太迟了,你将不得不更改密码,所以在这种情况下我甚至不会费心去替换它:从Git历史中删除敏感文件及其提交

相关链接:如何替换Git历史中的文本?

在git-filter-repo ac039ecc095d上进行了测试。

17

首先,找到所有可能包含密码的文件。假设密码是abc123,分支是master。您可能需要排除那些仅将abc123作为普通字符串的文件。

git log -S "abc123" master --name-only --pretty=format: | sort -u

然后将 "abc123" 替换为 "******"。假设其中一个文件是 foo/bar.txt

git filter-branch --tree-filter "if [ -f foo/bar.txt ];then sed -i s/abc123/******/g foo/bar.txt;fi"

如果远程仓库存在,最后强制推送master分支到远程仓库。

git push origin -f master:master

我进行了一项简单的测试,它可以正常工作,但我不确定在你的情况下是否适用。您需要处理所有分支中的所有文件。至于标签,您可能需要删除所有旧标签并创建新标签。


嗯,好的,它可以用于实际分支,但如果有更多的话,我可能需要为每个分支都这样做。 - Karol Selak
我在除了主分支之外的分支上遇到了问题。当我尝试运行 git log -S "abc123" test --name-only --pretty=format: | sort -u 时,出现错误:fatal: ambiguous argument 'test': both revision and filename。有没有什么办法可以避免这个错误? - Karol Selak
2
@KarolSelak 错误提示说你有一个名为 test 的引用和一个名为 test 的文件。这是一个命名冲突。如果你希望 Git 将 test 解释为引用,请使用 git log -S "abc123" test --name-only --pretty=format: -- | sort -u。如果将其解释为文件,则使用 git log -S "abc123" --name-only --pretty=format: -- test | sort -u。如果你需要两者都,请使用 git log -S "abc123" test --name-only --pretty=format: -- test | sort -u-- 周围有空格。更多信息请参见 https://www.git-scm.com/docs/gitcli#_description。 - ElpieKay
非常感谢,我终于写出了我需要的东西,但这主要归功于你。我希望最终的解决方案能够长期为他人服务 :) - Karol Selak
@KarolSelak 很高兴能帮到你 =)。别忘了删除并重新创建你已经推送的标签。它们仍然指向可能包含你密码的旧提交。 - ElpieKay
好的,谢谢你的重要建议。幸运的是,我的存储库中没有任何标签,但我会编辑我的答案以包含它。 - Karol Selak

4

首先,我要感谢ElpieKay发布了我的解决方案的核心功能,我只是将其自动化了。

所以,最终我拥有了想要的脚本。我将它分成了相互依赖且可以作为独立脚本的片段。它看起来像这样:

censorStringsInWholeGitHistory.sh:

#!/bin/bash
#arguments are strings to censore

for string in "$@"
do
  echo ""
  echo "================ Censoring string "$string": ================"
  ~/replaceStringInWholeGitHistory.sh "$string" "********"
done

使用方法:

~/censorStringsInWholeGitHistory.sh "my_password1" "my_password2" "some_f_word"

replaceStringInWholeGitHistory.sh:

#!/bin/bash
# $1 - string to find
# $2 - string to replace with

for branch in $(git branch | cut -c 3-); do
  echo ""
  echo ">>> Replacing strings in branch $branch:"
  echo ""
  ~/replaceStringInBranch.sh "$branch" "$1" "$2"
done

使用方法:

~/replaceStringInWholeGitHistory.sh "my_password" "********"

replaceStringInBranch.sh:

#!/bin/bash
# $1 - branch
# $2 - string to find
# $3 - string to replace with

git checkout $1
for file in $(~/findFilesContainingStringInBranch.sh "$2"); do
  echo "          Filtering file $file:"
  ~/changeStringsInFileInCurrentBranch.sh "$file" "$2" "$3"
done

使用方法:

~/replaceStringInBranch.sh master "my_password" "********"

findFilesContainingStringInBranch.sh:

#!/bin/bash

# $1 - string to find
# $2 - branch name or nothing (current branch in that case)

git log -S "$1" $2 --name-only --pretty=format: -- | sort -u

用法:

~/findFilesContainingStringInBranch.sh "my_password" master

changeStringsInFileInCurrentBranch.sh:

#!/bin/bash

# $1 - file name
# $2 - string to find
# $3 - string to replace

git filter-branch -f --tree-filter "if [ -f $1 ];then sed -i s/$2/$3/g $1;fi"

使用方法:

~/changeStringsInFileInCurrentBranch.sh "abc.txt" "my_password" "********"

我把所有这些脚本都放在了我的主文件夹里,这样才能使它们在这个版本中正常工作。虽然我不确定这是否是最佳选择,但目前我还没有找到更好的方法。当然,每个脚本都必须可执行,这可以通过使用 chmod +x ~/myscript.sh 来实现。
可能我的脚本并不是最优的,在处理大型仓库时会很慢,但它能够正常工作 :)
最后,我们可以将被审查过的仓库推送到任何远程仓库中:
git push <remote> -f --all

编辑:来自ElpieKay的重要提示:

不要忘记删除并重新创建你已经推送过的标签。它们仍然指向可能包含密码的旧提交。

也许我将来会改进我的脚本,以自动执行此操作。


这些脚本真的能用吗?我无法让它们工作:sed:-e表达式#1,char 7:未终止`s'命令 树过滤器失败: - E. T.
是的,我刚刚检查了一下,现在它对我有效。虽然我使用的是Git v2.17.1,但我不确定更新版本是否也适用。而且我使用Ubuntu。 - Karol Selak
问题可能是sed字符串需要转义吗?如果它包含空格、正斜杠或类似字符,我不知道它怎么能工作。 - E. T.
我不知道,我的答案基于ElpieKay的回答(https://dev59.com/Y1YN5IYBdhLWcg3wwKdZ#46951323),所以也许他能帮助你。 - Karol Selak

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