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

时间:2017-04-18 04:41:30

标签: git gitignore rebase

要明确这个问题,我不会询问如何从历史记录中删除单个文件,例如:Completely remove file from all Git repository commit history。我也没有问过来自gitignore的取消跟踪文件,就像这个问题:Ignore files that have already been committed to a Git repository

我正在谈论"更新.gitignore文件,然后从历史记录中删除与列表匹配的所有内容",或多或少像这个问题:Ignore files that have already been committed to a Git repository。然而,遗憾的是,该问题的答案并不适用于此目的,所以我在这里尝试详细阐述问题并希望找到一个好的答案,不涉及人类通过整个源树查看手动执行过滤器分支在每个匹配的文件上。

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

#!/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

此代码创建存储库,添加一些文件,执行一些编辑以及执行清理过程。此外,还会生成一些日志文件。 理想情况下,我希望abmain/x从历史记录中消失,而main/b保留。但是,现在没有任何东西从历史中删除。应该修改什么来实现这个目标?

如果可以在多个分支上执行此操作,则可获得奖励积分。但是现在,请将它保留在一个主分支中。

3 个答案:

答案 0 :(得分:3)

实现您想要的结果有点棘手。将git filter-branch--tree-filter一起使用的最简单方法将非常缓慢。 修改:我已修改您的示例脚本以执行此操作;看到这个答案的结尾。

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

现在,要重新提交可从一个或多个分支和/或标记名称访问的每个提交,保留除我们明确指示要更改的所有内容之外的所有内容, 1 我们可以使用{{ 1}}。 filter-branch命令有一个相当令人眼花缭乱的过滤选项,其中大部分都是为了让它更快,因为复制每次提交都非常慢。如果存储库中只有几百个提交,每个提交几十个或几百个文件,那就不那么糟糕;但是如果大约有100k提交,每个提交大约100k文件,那么要检查和重新提交的文件将达到10万个(10,000,000,000个文件)。这需要一段时间。

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

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

使用git update-index

--tree-filtergit filter-branch一起使用时,过滤器分支代码的作用是将每个提交(一次一个)提取到临时目录中。此临时目录没有--tree-filter目录,并且不在您运行.git的位置(除非您使用git filter-branch,否则它实际位于.git目录的子目录中选择将Git重定向到一个内存文件系统,这对于加速它来说是一个好主意。)

将整个提交解压缩到此临时目录后,Git会运行您的树过滤器。树形筛选器完成后,Git会将该临时目录中的所有打包到新提交中。无论你离开那里,都在。无论你添加什么,都会增加。无论你在那里修改什么,都会被修改。无论你从那里删除什么,都不再是新的提交。

请注意,此临时目录中的-d文件对将要提交的内容没有影响(但<{1}}文件本身提交,因为临时目录成为新的copy-commit)。因此,如果您想确保某个已知路径的文件已提交,只需.gitignore。如果文件在临时目录中,它现在就消失了。如果没有,没有任何反应,一切都很好。

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

.gitignore

(假设文件名中没有空格问题;使用rm -f known/path/to/file.ext来避免空白问题,使用xargs输入所需的任何编码; rm -f $(cat /tmp/files-to-remove) 样式编码是理想的xargs ... | rm -f路径名中禁止使用。)

将其转换为索引过滤器

使用索引过滤器可让Git跳过提取和检查阶段。如果您有固定的&#34;删除&#34;以正确的形式列出,它很容易使用。

我们假设您在-z中有一个适合\0的表单中的文件名。然后,您的索引过滤器可能会完整地读取:

/tmp/files-to-remove

与上面的xargs -0基本相同,但是在Git用于每个要复制的提交的临时索引中工作。 (将xargs -0 /tmp/files-to-remove | git rm --cached -f --ignore-unmatch 添加到rm -f以使其保持安静。)

在树过滤器中应用-q个文件

您的示例脚本尝试在重新定位到具有所需项目的初始提交后使用git rm --cached

.gitignore

虽然有一个初始错误(--tree-filter错误):

git filter-branch --tree-filter 'git clean -f -X' -- --all

修复此问题仍然无效,原因是git rebase仅删除实际忽略的文件。实际上不会忽略索引中已有的任何文件。

诀窍是清空索引。但是,这确实太多了: -git rebase --onto temp master +git rebase --onto temp temp master 然后永远不会进入子目录 - 所以技巧分为两部分:清空索引,然后用非忽略文件重新填充它。现在git clean -f -X将删除剩余的文件:

git clean

(我在这里添加了几个&#34; quiet&#34;标志)。

为了避免首先需要重新安装以安装初始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 个主文件(我们可以;然后在树过滤器中使用)。只需将这些内容放在临时树中即可:

.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 ,请使用:

.gitignore

第一步--tree-filter(可以在cp -R /tmp/ignores-to-add . && git rm --cached -qrf . && git add . && git clean -fqX 之前的任何地方完成),安装正确的cp -R文件。由于我们对每次提交执行此操作,因此在运行git add .之前我们永远不需要重新绑定。

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

第三个重新添加filter-branch,即临时树中的所有内容。由于rm $GIT_INDEX_FILE文件已到位,我们只添加了未被忽略的文件。

最后一步.删除被忽略的工作树文件,以便.gitignore 赢得&#t> 将它们放回原位。

答案 1 :(得分:2)

在Windows上,此序列对我不起作用

cp -R /tmp/ignores-to-add . &&
git rm --cached -qrf . &&
git add . &&
git clean -fqX

但是下面的作品。

使用现有的.gitignore更新每次提交:

git filter-branch --index-filter '
  git ls-files -i --exclude-from=.gitignore | xargs git rm --cached -q 
' -- --all

在每个提交和过滤器文件中更新.gitignore:

cp ../.gitignore /d/tmp-gitignore
git filter-branch --index-filter '
  cp /d/tmp-gitignore ./.gitignore
  git add .gitignore
  git ls-files -i --exclude-from=.gitignore | xargs git rm --cached -q 
' -- --all
rm /d/tmp-gitignore

如果您遇到特殊情况,请使用grep -v ,例如文件empty以保留空目录:

git ls-files -i --exclude-from=.gitignore | grep -vE "empty$" | xargs git rm --cached -q

答案 2 :(得分:1)

  

此方法使git 完全忘记被忽略的文件(过去 /现在/将来),但是从工作目录中删除任何内容(甚至从远程重新拉动时。

     

此方法要求使用全部中的/.git/info/exclude(首选) 预先存在的 .gitignore提交的文件被忽略/遗忘的提交。 1

     

所有强制执行git的方法事后都会忽略行为,从而有效地重写了历史记录,因此对于在此过程之后可能被拉出的任何公共/共享/协作存储库都具有significant ramifications 2

     

一般建议:从干净的仓库开始-提交的所有内容,工作目录或索引中没有待处理的内容,并进行备份

     

此外,revision history的评论/ this answerand revision historythis question)可能是有用/启发性的。

#commit up-to-date .gitignore (if not already existing)
#this command must be run on each branch

git add .gitignore
git commit -m "Create .gitignore"

#apply standard git ignore behavior only to current index, not working directory (--cached)
#if this command returns nothing, ensure /.git/info/exclude AND/OR .gitignore exist
#this command must be run on each branch

git ls-files -z --ignored --exclude-standard | xargs -0 git rm --cached

#Commit to prevent working directory data loss!
#this commit will be automatically deleted by the --prune-empty flag in the following command
#this command must be run on each branch

git commit -m "ignored index"

#Apply standard git ignore behavior RETROACTIVELY to all commits from all branches (--all)
#This step WILL delete ignored files from working directory UNLESS they have been dereferenced from the index by the commit above
#This step will also delete any "empty" commits.  If deliberate "empty" commits should be kept, remove --prune-empty and instead run git reset HEAD^ immediately after this command

git filter-branch --tree-filter 'git ls-files -z --ignored --exclude-standard | xargs -0 git rm -f --ignore-unmatch' --prune-empty --tag-name-filter cat -- --all

#List all still-existing files that are now ignored properly
#if this command returns nothing, it's time to restore from backup and start over
#this command must be run on each branch

git ls-files --other --ignored --exclude-standard

最后,请遵循this GitHub guide的其余部分(从第6步开始),其中包括有关以下命令的重要警告/信息

git push origin --force --all
git push origin --force --tags
git for-each-ref --format="delete %(refname)" refs/original | git update-ref --stdin
git reflog expire --expire=now --all
git gc --prune=now

从现在修改的远程仓库中提取的其他开发人员应进行备份,然后:

#fetch modified remote

git fetch --all

#"Pull" changes WITHOUT deleting newly-ignored files from working directory
#This will overwrite local tracked files with remote - ensure any local modifications are backed-up/stashed
#Switching branches after this procedure WILL LOOSE all newly-gitignored files in working directory because they are no longer tracked when switching branches

git reset FETCH_HEAD

脚语

1 由于可以按照上述说明将/.git/info/exclude应用于所有历史提交,因此有关将.gitignore文件放入历史记录中的详细信息需要它的提交不在此答案的范围内。我希望在根提交中使用适当的.gitignore,好像这是我所做的第一件事。其他人可能不在乎,因为/.git/info/exclude可以完成相同的操作,而不管.gitignore在提交历史记录中的什么位置,并且显然,重写历史记录是非常的敏感主题,即使了解ramifications

FWIW,可能的方法可能包括git rebasegit filter-branch,它们将外部 .gitignore复制到每个提交中,例如对this question的回答

2 通过提交独立git rm --cached命令的结果来强制git事后忽略行为,将来可能会导致新忽略的文件删除从受力推动的遥控器上拉出。以下--prune-empty命令中的git filter-branch标志通过自动删除以前的“删除所有被忽略的文件”仅索引提交来避免此问题。重写git历史记录也会更改提交哈希,这将wreak havoc用于将来从公共/共享/协作存储库中提取的信息。在进行此类回购之前,请先全面了解ramificationsThis GitHub guide指定以下内容:

  

告诉您的合作者rebase合并,合并他们从旧的(受污染的)存储库历史中创建的任何分支。一次合并提交可能会重新引入您刚刚遇到清除麻烦的部分或全部历史记录。

不影响的其他解决方案是git update-index --assume-unchanged </path/file>git update-index --skip-worktree <file>,可以在here中找到示例。