如何根据 gitignore 过滤历史记录?

Car*_*ong 6 git rebase gitignore

为了明确这个问题,我不是在问如何从历史记录中删除单个文件,就像这个问题:Completely remove file from all Git repository commit history。我也不是在询问从 gitignore取消跟踪文件,就像在这个问题中一样:Ignore files that have been commited to a Git repository

我说的是“更新 .gitignore 文件,然后从历史记录中删除与列表匹配的所有内容”,或多或少类似于这个问题:忽略已提交到 Git 存储库的文件。但是,不幸的是,该问题的答案不适用于此目的,因此我在这里尝试详细说明该问题,并希望找到一个不涉及人工查看整个源树以手动执行过滤器分支的好答案在每个匹配的文件上。

在这里,我提供了一个测试脚本,目前正在执行Ignore files that have been commited to a Git repository的答案中的过程。它将root在 PWD 下删除并创建一个文件夹,因此在运行它之前要小心。我将在代码之后描述我的目标。

#!/bin/bash -e

TESTROOT=${PWD}
GREEN="\e[32m"
RESET="\e[39m"

rm -rf root
mkdir -v root
pushd root

mkdir -v repo
pushd repo
git init

touch a b c x 
mkdir -v main
touch main/{a,x,y,z}

# Initial commit
git add .
git commit -m "Initial Commit"
echo -e "${GREEN}Contents of first commit${RESET}"
git ls-files | tee ../00-Initial.txt

# Add another commit just for demo
touch d e f y z main/{b,c}
## Make some other changes
echo "Test" | tee a | tee b | tee c | tee x | tee main/a > main/x
git add .
git commit -m "Some edits"

echo -e "${GREEN}Contents of second commit${RESET}"
git ls-files | tee ../01-Changed.txt

# Now I want to ignore all 'a' and 'b', and all 'main/x', but not 'main/b'
## Checkout the root commit
git checkout -b temp $(git rev-list HEAD | tail -1)
## Add .gitignores
echo "a" >> .gitignore
echo "b" >> .gitignore
echo "x" >> main/.gitignore
echo "!b" >> main/.gitignore
git add .
git commit --amend -m "Initial Commit (2)"
## --v Not sure if it is correct
git rebase --onto temp master
git checkout master
## --v Now, why should I delete this branch?
git branch -D temp
echo -e "${GREEN}Contents after rebase${RESET}"
git ls-files | tee ../02-Rebased.txt

# Supposingly, rewrite history
git filter-branch --tree-filter 'git clean -f -X' -- --all
echo -e "${GREEN}Contents after filter-branch${RESET}"
git ls-files | tee ../03-Rewritten.txt

echo "History of 'a'"
git log -p a

popd # repo

popd # root
Run Code Online (Sandbox Code Playgroud)

此代码创建一个存储库,添加一些文件,进行一些编辑,并执行清理过程。此外,还会生成一些日志文件。理想情况下,我想abmain/x从历史上消失,而main/b停留。然而,现在没有什么可以从历史中删除。应该修改什么来实现这个目标?

如果这可以在多个分支上完成,则奖励积分。但是现在,将它保留在一个主分支上。

tor*_*rek 5

达到你想要的结果有点棘手。git filter-branch与 a 一起使用的最简单方法--tree-filter将非常慢。 编辑:我已经修改了您的示例脚本来执行此操作;看到这个答案的结尾。

首先,让我们注意一个约束:你永远不能改变任何现有的提交。你所能做的就是进行新的提交,看起来很像旧的提交,但是“新的和改进的”。然后,您指示 Git 停止查看旧提交,而只查看新提交。这就是我们在这里要做的。(然后,如果需要,您可以强制 Git真正忘记旧的提交。最简单的方法是重新克隆克隆。)

现在,要重新提交可从一个或多个分支和/或标签名称访问的每个提交,保留除我们明确告诉它更改的所有内容之外的所有内容,1我们可以使用git filter-branch. filter-branch 命令有一系列令人眼花缭乱的过滤选项,其中大部分是为了让它更快,因为复制每个提交都非常慢。如果存储库中只有几百个提交,每个提交有几十个或数百个文件,那还不错;但是如果有大约 100k 个提交,每个提交包含大约 100k 个文件,那就是一亿个文件(10,000,000,000 个文件)需要检查和重新提交。这将需要一段时间。

不幸的是,没有简单方便的方法来加快速度。加快速度的最佳方法是使用--index-filter,但没有内置的索引过滤器命令可以执行您想要的操作。最容易使用的过滤器是--tree-filter,这也是最慢的过滤器。您可能想尝试编写自己的索引过滤器,可能是在 shell 脚本中,也可能是在您喜欢的另一种语言中(您仍然需要以git update-index任何一种方式调用)。


1 已签名的注释标签无法完整保存,因此其签名将被剥离。签名提交可能会使其签名失效(如果提交哈希更改,这取决于它是否必须:记住提交的哈希 ID 是提交内容的校验和,因此如果文件集更改,校验和更改;但如果父提交的校验和更改,则此提交的校验和也会更改)。


使用 --tree-filter

当您使用git filter-branchwith 时--tree-filter,filter-branch 代码的作用是将每个提交提取到一个临时目录中,一次一个。这个临时目录没有.git目录,也不是你运行的地方git filter-branch(它实际上在目录的一个子目录中,.git除非你使用-d选项将 Git 重定向到内存文件系统,这是一个加速它的好主意)。

将整个提交提取到这个临时目录后,Git 运行您的树过滤器。一旦您的树过滤器完成,Git 会将该临时目录中的所有内容打包到新的提交中。无论你在那里留下什么,都在里面。你在那里添加的任何东西,都会被添加。无论你在那里修改什么,都会被修改。无论你从那里删除,都不再在新的提交中。

请注意,.gitignore此临时目录中的文件对将提交的内容没有影响(但.gitignore文件本身被提交,因为临时目录中的任何内容都会成为新的复制提交)。因此,如果您想确保某个已知路径的文件提交,只需rm -f known/path/to/file.ext. 如果文件在临时目录中,它现在已经消失了。如果没有,什么都不会发生,一切都很好。

因此,一个可行的树过滤器将是:

rm -f $(cat /tmp/files-to-remove)
Run Code Online (Sandbox Code Playgroud)

(假设文件名中没有空格问题;用于xargs ... | rm -f避免空格问题,使用您喜欢的 xargs 输入的任何编码;-z样式编码是理想的,因为\0在路径名中被禁止)。

将其转换为索引过滤器

使用索引过滤器可以让 Git 跳过提取和检查阶段。如果你有一个正确形式的固定“删除”列表,它会很容易使用。

假设您的文件名/tmp/files-to-remove采用适合xargs -0. 然后您的索引过滤器可能会完整读取:

xargs -0 /tmp/files-to-remove | git rm --cached -f --ignore-unmatch
Run Code Online (Sandbox Code Playgroud)

这与rm -f上面的基本相同,但在 Git 用于每个要复制的提交的临时索引内工作。(添加-qgit rm --cached使其安静。)

.gitignore在树过滤器中应用文件

您的示例脚本尝试使用 a --tree-filterafter rebase 到具有所需项目的初始提交:

git filter-branch --tree-filter 'git clean -f -X' -- --all
Run Code Online (Sandbox Code Playgroud)

虽然有一个初始错误(git rebase错误):

-git rebase --onto temp master
+git rebase --onto temp temp master
Run Code Online (Sandbox Code Playgroud)

解决这个问题,事情仍然不起作用,原因是git clean -f -X只删除了实际上被忽略的文件。任何已经在索引中的文件实际上都不会被忽略。

诀窍是清空索引。然而,这样做太多了: git clean然后永远不会下降到子目录中——所以技巧有两个部分:清空索引,然后用未忽略的文件重新填充它。现在git clean -f -X将删除剩余的文件:

-git filter-branch --tree-filter 'git clean -f -X' -- --all
+git filter-branch --tree-filter 'git rm --cached -qrf . && git add . && git clean -fqX' -- --all
Run Code Online (Sandbox Code Playgroud)

(我在这里添加了几个“安静”标志)。

为了避免在安装初始.gitignore文件时首先需要变基,假设您.gitignore在每次提交中都有一组所需的主文件(然后我们也将在树过滤器中使用)。只需将这些放在临时树中,别无他物:

mkdir /tmp/ignores-to-add
cp .gitignore /tmp/ignores-to-add
mkdir /tmp/ignores-to-add/main
cp main/.gitignore /tmp/ignores-to-add
Run Code Online (Sandbox Code Playgroud)

(我将继续编写一个脚本来查找.gitignore文件并将其复制给您,没有它似乎有点烦人)。然后,对于--tree-filter,使用:

cp -R /tmp/ignores-to-add . &&
    git rm --cached -qrf . &&
    git add . &&
    git clean -fqX
Run Code Online (Sandbox Code Playgroud)

第一步cp -R(实际上可以在 之前的任何地方完成git add .)安装正确的.gitignore文件。由于我们对每次提交都执行此操作,因此在运行filter-branch.

第二个从索引中删除所有内容。(稍微快一点的方法只是,rm $GIT_INDEX_FILE但不能保证这将永远有效。)

第三次重新添加.,即临时树中的所有内容。由于.gitignore文件就位,我们只添加未忽略的文件。

最后一步,git clean -qfX删除被忽略的工作树文件,因此filter-branch 不会将它们放回原处。


归档时间:

查看次数:

2028 次

最近记录:

6 年,2 月 前