如何修复错误的合并,并将好的提交重播到固定的合并中?


407

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

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



Answers:


297

如果您的情况不是问题中描述的情况,请不要使用此食谱。此食谱用于修复错误的合并,并将您的良好提交重播到固定的合并中。

尽管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命令,而不是使用临时分支名称。)


6
会不会git rebase -i更快且仍然如此简单?$ git rebase -i <sh1-of-merge>将正确的标记为“编辑” $ git rm somefile.orig $ git commit --amend $ git rebase --continue但是由于某些原因,我仍然在最后一个文件中保存该文件时间我做到了。可能丢失了一些东西。
2010年

12
git rebase -i这非常有用,尤其是当您需要执行多个rebase-y操作时,但是当您实际上并没有真正指向某人的肩膀并且可以看到他们在用编辑器做什么时,准确地描述它是正确的选择。我使用vim,但并非所有人都会满意:“ ggjcesquash <Esc> jddjp:wq”和类似的指示“将第一行移动到当前第二行之后,并将第四行的第一个单词更改为'edit',现在保存并退出”似乎比实际步骤要复杂得多。通常你结束了一些--amend--continue行动,以及。
CB Bailey 2010年

3
我这样做了,但是在修改后的新提交中再次应用了相同的消息。显然,git在包含有害文件的旧的未修改提交与另一个分支的固定提交之间进行了3种方式的合并,因此它在旧分支的顶部创建了一个新提交,以重新应用该文件。

6
@UncleCJ:您的文件是否添加到合并提交中?这个很重要。此食谱旨在解决错误的合并提交。如果在历史记录的常规提交中添加了不需要的文件,它将无法正常工作。
CB Bailey 2010年

1
我很惊讶如何使用smartgit来完成所有这些工作,而根本没有终端!谢谢你的食谱!
cregox

209

简介:您有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:非交互式Rebase

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

# 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相同的任务,即,修改或删除历史记录中的提交要比直接提交更早,因此您选择使用哪种解决方案取决于您自己。出于性能方面的考虑,交互式rebase不太适合对数百个提交进行基础调整,因此在这种情况下,我将使用非交互式rebase或filter分支解决方案(请参见下文)。

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

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)

您要修改或删除的提交将在此列表的顶部。要删除它,只需在列表中删除它的行即可。否则,将“选择”对1条“编辑” ST线,像这样:

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

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

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回购清洁器

我听说BFG Repo Cleaner工具的运行速度比快git filter-branch,因此您可能也希望将其检出。过滤器分支文档中甚至正式提到了可行的替代方法:

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

  • 文件的任何特定版本仅清除一次。BFG与git-filter-branch不同,它没有给您机会根据文件在历史记录中的提交位置或时间对文件进行不同的处理。该约束为BFG提供了核心性能优势,并且非常适合清理不良数据的任务-您不在乎不良数据在哪里,只希望它消失了

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

  • 命令选项都远远超过git的过滤分支更严格,并致力于只是为了消除不必要的数据-例如任务:--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回购清洁器(另请参阅创建者本人的回答)。

是否filter-branch会导致重新计算哈希?如果团队使用应该过滤大文件的仓库,他们该如何做,以便每个人最终都处于仓库的相同状态?
YakovL

@YakovL。一切都会重新计算哈希值。实际上,提交是不可变的。它会创建一个全新的历史记录,并将分支指针移至该历史记录。确保每个人都有相同历史记录的唯一方法是硬重置。
疯狂物理学家

118

如果您此后未提交任何内容,则只需git rm输入文件和即可git commit --amend

如果你有

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

将经历从merge-point到的每个更改HEAD,删除filename.orig并重写更改。使用--ignore-unmatch意味着如果由于某种原因filename.orig丢失而导致命令不会失败。这是git-filter-branch手册页中 “示例”部分的推荐方法。

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


3
谢谢!git filter-branch对我有用,但作为答案给出的重新配置示例不起作用:这些步骤似乎有效,但随后推送失败。进行拉动,然后成功推入,但文件仍然存在。试图重做rebase步骤,然后合并冲突变得一团糟。我使用了一个稍微不同的filter-branch命令,这里给出了“一种改进的方法”:github.com/guides/completely-remove-a-file-from-all-revisions git filter-branch -f --index-过滤器'git update-index --remove filename'<introduction-revision-sha1> .. HEAD
原子

1
我不确定哪一种是改进的方法。Git的官方文档git-filter-branch似乎给出了第一个。
2010年

5
查看zyxware.com/articles/4027/…我发现它是涉及的最完整,最直接的解决方案filter-branch
leontalbot 2014年

2
@atomicules,如果您尝试将本地存储库推送到远程存储库,则git将坚持先从远程存储库中拉出,因为它具有您本地没有的更改。您可以使用--force标志将其推送到远程-它将完全从那里删除文件。但是请小心,确保不要强制覆盖仅文件以外的内容。
sol0mka '16

1
记住要使用"而不是'Windows时使用,否则会收到无用的措辞“错误修订”错误。
cz

49

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

只要确保首先备份文件的副本即可。

编辑

不幸的是,Neon的编辑在审核期间被拒绝。
请参阅下面的霓虹灯发布,其中可能包含有用的信息!


例如,删除所有*.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

不知道为什么,因为我只有一个master分支。无论如何,我终于通过推送到一个新的空的和裸露的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_git../tmpdir目录(也许从现在起几个星期或一个月,以防万一...)


1
在“那对我还是不起作用?”之前,这对我有用。评论
shadi

很好的答案,但建议添加--prune-empty到filter-branch命令。
ideaman42

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(不在您的最新 commit中)的文件都将被删除。这比使用git-filter-branch相同的工具容易得多!

完全公开:我是BFG Repo-Cleaner的作者。


4
这是一个很好的工具:一个命令,它会产生非常清晰的输出,并提供一个日志文件,该文件将每个旧提交与新提交匹配。我不喜欢安装Java,但这是值得的。
mikemaccana 2014年

这是唯一对我有用的东西,但是那就像是因为我没有正确地使用git filter-branch。:-)
Kevin LaBranche

14
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

1
尽管所有答案似乎都在分支机构的轨道上,但这一答案着重说明了如何清除历史记录中的所有分支。
卡梅隆·洛厄尔·帕尔默

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

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."

所有的功劳归于Annopjohn,并leontalbot指出。

注意

请注意,该脚本不包含验证,因此请确保您不会出错,并且在发生问题时可以进行备份。它对我有用,但在您的情况下可能不起作用。请谨慎使用(如果您想了解发生了什么,请点击链接)。


3

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

可悲的是,这还不足以filename.orig从您的仓库中完全删除,因为它仍可以被标签,reflog条目,远程控件等引用。

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

git forget-blob filename.orig


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

git reflog expire --expire=now --all; git gc --prune=now这是一件非常不好的事情。除非磁盘空间不足,否则请让git垃圾在几周后收集这些提交
avmohan

感谢您指出了这一点。我的仓库已提交了许多大型二进制文件,并且仓库每晚都完全备份。所以我只想
从中脱颖而出


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.