422

几次提交前,我不小心将一个不需要的文件(filename.orig在解决合并时)提交到我的存储库,直到现在我才注意到它。我想从存储库历史记录中完全删除该文件。

是否可以重写filename.orig从未添加到存储库中的更改历史记录?

4

12 回答 12

306

如果您的情况不是问题中描述的情况,请不要使用此食谱。这个秘籍是为了修复一个错误的合并,并将你好的提交重播到一个固定的合并上。

虽然filter-branch会做你想做的事,但这是一个相当复杂的命令,我可能会选择使用git rebase. 这可能是个人喜好。filter-branch可以在一个稍微复杂一点的命令中完成,而rebase解决方案是一次执行等效的逻辑操作。

试试下面的食谱:

# create and check out a temporary branch at the location of the bad merge
git checkout -b tmpfix <sha1-of-merge>

# remove the incorrectly added file
git rm somefile.orig

# commit the amended merge
git commit --amend

# go back to the master branch
git checkout master

# replant the master branch onto the corrected merge
git rebase tmpfix

# delete the temporary branch
git branch -d tmpfix

(请注意,您实际上并不需要临时分支,您可以使用“分离的 HEAD”执行此操作,但您需要记下由git commit --amend步骤生成的提交 ID 以提供给git rebase命令,而不是使用临时分支姓名。)

于 2008-11-21T13:02:21.957 回答
222

简介:您有 5 个可用的解决方案

原始海报说:

我不小心将一个不需要的文件提交到我的存储库几次提交前...我想从存储库历史记录中完全删除该文件。

是否可以重写filename.orig从未添加到存储库中的更改历史记录?

有许多不同的方法可以从 git 中完全删除文件的历史记录:

  1. 修改提交。
  2. 硬重置(可能加上变基)。
  3. 非交互式变基。
  4. 交互式变基。
  5. 过滤分支。

在原始海报的情况下,修改提交本身并不是一个真正的选择,因为他之后做了几个额外的提交,但为了完整起见,我也会解释如何去做,对于任何想要的人修改他们之前的提交。

请注意,所有这些解决方案都涉及以另一种方式更改/重写历史记录/提交,因此任何拥有旧提交副本的人都必须做额外的工作才能将他们的历史记录与新的历史记录重新同步。


解决方案 1:修改提交

如果您不小心在之前的提交中进行了更改(例如添加文件),并且您不希望该更改的历史记录不再存在,那么您可以简单地修改之前的提交以从中删除文件:

git rm <file>
git commit --amend --no-edit

解决方案 2:硬重置(可能加上变基)

与解决方案 #1 一样,如果您只是想摆脱以前的提交,那么您还可以选择简单地对其父项进行硬重置:

git reset --hard HEAD^

该命令会将您的分支硬重置为之前的一个父提交。

但是,如果像原始海报一样,您在要撤消更改的提交之后进行了多次提交,您仍然可以使用硬重置来修改它,但这样做也涉及使用变基。以下是您可以用来修改历史上更早的提交的步骤:

# Create a new branch at the commit you want to amend
git checkout -b temp <commit>

# Amend the commit
git rm <file>
git commit --amend --no-edit

# Rebase your previous branch onto this new commit, starting from the old-commit
git rebase --preserve-merges --onto temp <old-commit> master

# Verify your changes
git diff master@{1}

解决方案 3:非交互式变基

如果您只想从历史记录中完全删除提交,这将起作用:

# Create a new branch at the parent-commit of the commit that you want to remove
git branch temp <parent-commit>

# Rebase onto the parent-commit, starting from the commit-to-remove
git rebase --preserve-merges --onto temp <commit-to-remove> master

# Or use `-p` insteda of the longer `--preserve-merges`
git rebase -p --onto temp <commit-to-remove> master

# Verify your changes
git diff master@{1}

解决方案 4:交互式变基

此解决方案将允许您完成与解决方案 #2 和 #3 相同的事情,即修改或删除比您之前的提交更早的历史提交,因此您选择使用哪种解决方案取决于您。出于性能原因,交互式变基不太适合对数百个提交进行变基,因此在这种情况下,我会使用非交互式变基或过滤器分支解决方案(见下文)。

要开始交互式变基,请使用以下命令:

git rebase --interactive <commit-to-amend-or-remove>~

# Or `-i` instead of the longer `--interactive`
git rebase -i <commit-to-amend-or-remove>~

这将导致 git 将提交历史回退到您要修改或删除的提交的父级。然后它将以相反的顺序为您提供一个重新提交的列表,其中包含 git 设置使用的任何编辑器(默认情况下是 Vim):

pick 00ddaac Add symlinks for executables
pick 03fa071 Set `push.default` to `simple`
pick 7668f34 Modify Bash config to use Homebrew recommended PATH
pick 475593a Add global .gitignore file for OS X
pick 1b7f496 Add alias for Dr Java to Bash config (OS X)

您要修改或删除的提交将位于此列表的顶部。要删除它,只需在列表中删除它的行。否则,将第一行的“pick”替换为“edit” ,如下所示:

edit 00ddaac Add symlinks for executables
pick 03fa071 Set `push.default` to `simple`

接下来,输入git rebase --continue。如果您选择完全删除提交,那么您需要做的就是这一切(除了验证,请参阅此解决方案的最后一步)。另一方面,如果您想修改提交,那么 git 将重新应用提交,然后暂停变基。

Stopped at 00ddaacab0a85d9989217dd9fe9e1b317ed069ac... Add symlinks
You can amend the commit now, with

        git commit --amend

Once you are satisfied with your changes, run

        git rebase --continue

此时,您可以删除文件并修改提交,然后继续 rebase:

git rm <file>
git commit --amend --no-edit
git rebase --continue

就是这样。作为最后一步,无论您是修改了提交还是完全删除了它,最好通过在变基之前将其与分支的状态进行比较来验证您的分支是否没有进行其他意外更改:

git diff master@{1}

解决方案 5:过滤分支

最后,如果您想从历史中完全清除文件存在的所有痕迹,则此解决方案是最好的,并且其他解决方案都不能胜任这项任务。

git filter-branch --index-filter \
'git rm --cached --ignore-unmatch <file>'

<file>从根提交开始,这将从所有提交中删除。相反,如果您只想重写提交范围HEAD~5..HEAD,那么您可以将其作为附加参数传递给,如此答案filter-branch中所指出的 :

git filter-branch --index-filter \
'git rm --cached --ignore-unmatch <file>' HEAD~5..HEAD

同样,在filter-branch完成之后,通常最好通过在过滤操作之前将您的分支与之前的状态进行比较来验证是否没有其他意外更改:

git diff master@{1}

过滤器分支替代品:BFG Repo Cleaner

我听说BFG Repo Cleaner工具的运行速度比 快git filter-branch,因此您可能也想将其作为一个选项进行检查。它甚至在filter-branch 文档中正式提到作为一种可行的替代方案:

git-filter-branch 允许您对 Git 历史记录进行复杂的 shell 脚本重写,但如果您只是删除不需要的数据(如大文件或密码),则可能不需要这种灵活性。对于这些操作,您可能需要考虑BFG Repo-Cleaner,它是 git-filter-branch 的基于 JVM 的替代方案,对于这些用例通常至少快 10-50 倍,并且具有完全不同的特性:

  • 任何特定版本的文件都会被清理一次。与 git-filter-branch 不同,BFG 不会让您有机会根据文件在历史记录中提交的位置或时间来不同地处理文件。这个约束提供了 BFG 的核心性能优势,并且非常适合清理坏数据的任务——你不关心坏数据在哪里,你只想让它消失

  • 默认情况下,BFG 充分利用多核机器,并行清理提交文件树。git-filter-branch 按顺序清理提交(即以单线程方式),尽管可以 在针对每个提交执行的脚本中编写包含它们自己的并行性的过滤器。

  • 命令选项比 git-filter 分支更具限制性,并且仅用于删除不需要的数据的任务,例如:--strip-blobs-bigger-than 1M.

其他资源

  1. Pro Git § 6.4 Git 工具 - 重写历史
  2. git-filter-branch(1) 手册页
  3. git-commit(1) 手册页
  4. git-reset(1) 手册页
  5. git-rebase(1) 手册页
  6. BFG Repo Cleaner(另请参阅创建者本人的这个答案)。
于 2014-04-20T23:10:53.043 回答
121

如果您此后没有提交任何内容,只需git rm文件和git commit --amend.

如果你有

git filter-branch \
--index-filter 'git rm --cached --ignore-unmatch path/to/file/filename.orig' merge-point..HEAD

将经历从merge-pointto 的每次更改HEAD,删除 filename.orig 并重写更改。Using--ignore-unmatch意味着如果由于某种原因 filename.orig 在更改中丢失,该命令不会失败。这是git-filter-branch 手册页中示例部分的推荐方法。

Windows 用户注意事项:文件路径必须使用正斜杠

于 2009-03-14T20:44:57.617 回答
50

这是最好的方法:http:
//github.com/guides/completely-remove-a-file-from-all-revisions

请务必先备份文件的副本。

编辑

不幸的是, Neon的编辑在审核期间被拒绝了。
请参阅下面的 Neons 帖子,它可能包含有用的信息!


例如删除所有*.gz意外提交到 git 存储库的文件:

$ du -sh .git ==> e.g. 100M
$ git filter-branch --index-filter 'git rm --cached --ignore-unmatch *.gz' HEAD
$ git push origin master --force
$ rm -rf .git/refs/original/
$ git reflog expire --expire=now --all
$ git gc --prune=now
$ git gc --aggressive --prune=now

那仍然对我不起作用?(我目前在 git 版本 1.7.6.1)

$ du -sh .git ==> e.g. 100M

不知道为什么,因为我只有一个主分支。无论如何,我终于通过推入一个新的空且裸露的 git 存储库来真正清理我的 git 存储库,例如

$ git init --bare /path/to/newcleanrepo.git
$ git push /path/to/newcleanrepo.git master
$ du -sh /path/to/newcleanrepo.git ==> e.g. 5M 

(是的!)

然后我将它克隆到一个新目录并将它的 .git 文件夹移到这个目录中。例如

$ mv .git ../large_dot_git
$ git clone /path/to/newcleanrepo.git ../tmpdir
$ mv ../tmpdir/.git .
$ du -sh .git ==> e.g. 5M 

(是的!终于清理干净了!)

确认一切正常后,您可以删除../large_dot_gitand../tmpdir目录(可能在几周或几个月后,以防万一......)

于 2010-02-04T05:52:43.073 回答
27

重写 Git 历史记录需要更改所有受影响的提交 ID,因此从事该项目的每个人都需要删除他们的旧副本,并在清理历史记录后进行新的克隆。给它带来不便的人越多,你就越需要有充分的理由去做——你多余的文件并没有真正造成问题,但如果只是在处理项目,你不妨清理一下 Git 历史记录到!

为了使其尽可能简单,我建议使用BFG Repo-Cleaner,这是一种更简单、更快的替代方案,git-filter-branch专门用于从 Git 历史记录中删除文件。它使您的生活更轻松的一种方法是它实际上默认处理所有引用(所有标签、分支等),但它的速度也快了10 到 50 倍

您应该仔细按照此处的步骤操作:http ://rtyley.github.com/bfg-repo-cleaner/#usage - 但核心位是这样的:下载BFG jar(需要 Java 6 或更高版本)并运行此命令:

$ java -jar bfg.jar --delete-files filename.orig my-repo.git

将扫描您的整个存储库历史记录,并删除任何名为filename.orig(不在您的最新提交中)的文件。这比使用git-filter-branch做同样的事情要容易得多!

全面披露:我是 BFG Repo-Cleaner 的作者。

于 2013-03-31T12:35:49.690 回答
16
You should probably clone your repository first.

Remove your file from all branches history:
git filter-branch --tree-filter 'rm -f filename.orig' -- --all

Remove your file just from the current branch:
git filter-branch --tree-filter 'rm -f filename.orig' -- --HEAD    

Lastly you should run to remove empty commits:
git filter-branch -f --prune-empty -- --all
于 2016-06-10T06:35:50.640 回答
4

只是为了将它添加到 Charles Bailey 的解决方案中,我只是使用 git rebase -i 从早期提交中删除不需要的文件,它就像一个魅力。步骤:

# Pick your commit with 'e'
$ git rebase -i

# Perform as many removes as necessary
$ git rm project/code/file.txt

# amend the commit
$ git commit --amend

# continue with rebase
$ git rebase --continue
于 2013-10-16T13:10:21.617 回答
4

我发现的最简单的方法是leontalbot(作为评论)建议的,这是Anoopjohn 发布的帖子。我认为它值得拥有自己的空间作为答案:

(我将其转换为 bash 脚本)

#!/bin/bash
if [[ $1 == "" ]]; then
    echo "Usage: $0 FILE_OR_DIR [remote]";
    echo "FILE_OR_DIR: the file or directory you want to remove from history"
    echo "if 'remote' argument is set, it will also push to remote repository."
    exit;
fi
FOLDERNAME_OR_FILENAME=$1;

#The important part starts here: ------------------------

git filter-branch -f --index-filter "git rm -rf --cached --ignore-unmatch $FOLDERNAME_OR_FILENAME" -- --all
rm -rf .git/refs/original/
git reflog expire --expire=now --all
git gc --prune=now
git gc --aggressive --prune=now

if [[ $2 == "remote" ]]; then
    git push --all --force
fi
echo "Done."

所有学分都归Annopjohnleontalbot用于指出。

笔记

请注意,该脚本不包括验证,因此请确保您不会犯错误,并且您有备份以防万一出现问题。它对我有用,但它可能不适用于您的情况。谨慎使用(如果您想知道发生了什么,请点击链接)。

于 2016-05-17T02:26:14.967 回答
4

绝对git filter-branch是要走的路。

遗憾的是,这不足以filename.orig从您的 repo 中完全删除,因为它仍然可以被标签、reflog 条目、遥控器等引用。

我建议也删除所有这些引用,然后调用垃圾收集器。您可以使用git forget-blob网站上的脚本一步完成所有这些操作。

git forget-blob filename.orig

于 2017-01-30T12:54:26.640 回答
1

如果这是您要清理的最新提交,我尝试使用 git 版本 2.14.3 (Apple Git-98):

touch empty
git init
git add empty
git commit -m init

# 92K   .git
du -hs .git

dd if=/dev/random of=./random bs=1m count=5
git add random
git commit -m mistake

# 5.1M  .git
du -hs .git

git reset --hard HEAD^
git reflog expire --expire=now --all
git gc --prune=now

# 92K   .git
du -hs .git
于 2018-03-29T15:40:36.120 回答
0

这就是git filter-branch设计的目的。

于 2008-11-21T10:26:19.447 回答
-1

您还可以使用:

git reset HEAD file/path

于 2009-09-03T04:00:45.910 回答