git 你如何修复一个错误的合并,并将你的好提交重放到一个固定的合并上?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/307828/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-10 05:58:50  来源:igfitidea点击:

How do you fix a bad merge, and replay your good commits onto a fixed merge?

gitgit-filter-branchgit-rewrite-historygit-rm

提问by Grant Limberg

I accidentally committed an unwanted file (filename.origwhile resolving a merge) to my repository several commits ago, without me noticing it until now. I want to completely delete the file from the repository history.

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

Is it possible to rewrite the change history such that filename.origwas never added to the repository in the first place?

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

采纳答案by CB Bailey

Please don't use this recipe if your situation is not the one described in the question. This recipe is for fixing a bad merge, and replaying your good commits onto a fixed merge.

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

Although filter-branchwill do what you want, it is quite a complex command and I would probably choose to do this with git rebase. It's probably a personal preference. filter-branchcan do it in a single, slightly more complex command, whereas the rebasesolution is performing the equivalent logical operations one step at a time.

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

Try the following recipe:

试试下面的食谱:

# 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

(Note that you don't actually need a temporary branch, you can do this with a 'detached HEAD', but you need to take a note of the commit id generated by the git commit --amendstep to supply to the git rebasecommand rather than using the temporary branch name.)

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

回答by CB Bailey

Intro: You Have 5 Solutions Available

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

The original poster states:

原始海报指出:

I accidentally committed an unwanted file...to my repository several commits ago...I want to completely delete the file from the repository history.

Is it possible to rewrite the change history such that filename.origwas never added to the repository in the first place?

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

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

There are many different ways to remove the history of a file completely from git:

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

  1. Amending commits.
  2. Hard resets (possibly plus a rebase).
  3. Non-interactive rebase.
  4. Interactive rebases.
  5. Filtering branches.
  1. 修改提交。
  2. 硬重置(可能加上变基)。
  3. 非交互式变基。
  4. 交互式变基。
  5. 过滤分支。

In the case of the original poster, amending the commit isn't really an option by itself, since he made several additional commits afterwards, but for the sake of completeness, I will also explain how to do it, for anyone else who justs wants to amend their previous commit.

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

Note that all of these solutions involve altering/re-writinghistory/commits in one way another, so anyone with old copies of the commits will have to do extra work to re-sync their history with the new history.

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



Solution 1: Amending Commits

解决方案 1:修改提交

If you accidentally made a change (such as adding a file) in your previous commit, and you don't want the history of that change to exist anymore, then you can simply amend the previous commit to remove the file from it:

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

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


Solution 2: Hard Reset (Possibly Plus a Rebase)

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

Like solution #1, if you just want to get rid of your previous commit, then you also have the option of simply doing a hard reset to its parent:

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

git reset --hard HEAD^

That command will hard-reset your branch to the previous 1stparent commit.

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

However, if, like the original poster, you've made several commits after the commit you want to undo the change to, you can still use hard resets to modify it, but doing so also involves using a rebase. Here are the steps that you can use to amend a commit further back in history:

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

# 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}


Solution 3: Non-interactive Rebase

解决方案 3:非交互式 Rebase

This will work if you just want to remove a commit from history entirely:

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

# 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}


Solution 4: Interactive Rebases

解决方案 4:交互式变基

This solution will allow you to accomplish the same things as solutions #2 and #3, i.e. modify or remove commits further back in history than your immediately previous commit, so which solution you choose to use is sort of up to you. Interactive rebases are not well-suited to rebasing hundreds of commits, for performance reasons, so I would use non-interactive rebases or the filter branch solution (see below) in those sort of situations.

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

To begin the interactive rebase, use the following:

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

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

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

This will cause git to rewind the commit history back to the parent of the commit that you want to modify or remove. It will then present you a list of the rewound commits in reverse order in whatever editor git is set to use (this is Vim by default):

这将导致 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)

The commit that you want to modify or remove will be at the top of this list. To remove it, simply delete its line in the list. Otherwise, replace "pick" with "edit" on the 1stline, like so:

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

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

Next, enter git rebase --continue. If you chose to remove the commit entirely, then that it all you need to do (other than verification, see final step for this solution). If, on the other hand, you wanted to modify the commit, then git will reapply the commit and then pause the rebase.

接下来,输入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

At this point, you can remove the file and amend the commit, then continue the rebase:

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

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

That's it. As a final step, whether you modified the commit or removed it completely, it's always a good idea to verify that no other unexpected changes were made to your branch by diffing it with its state before the rebase:

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

git diff master@{1}


Solution 5: Filtering Branches

解决方案 5:过滤分支

Finally, this solution is best if you want to completely wipe out all traces of a file's existence from history, and none of the other solutions are quite up to the task.

最后,如果您想从历史记录中完全清除文件存在的所有痕迹,并且其他任何解决方案都无法胜任这项任务,那么此解决方案是最佳选择。

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

That will remove <file>from all commits, starting from the root commit. If instead you just want to rewrite the commit range HEAD~5..HEAD, then you can pass that as an additional argument to filter-branch, as pointed out in this answer:

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

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

Again, after the filter-branchis complete, it's usually a good idea to verify that there are no other unexpected changes by diffing your branch with its previous state before the filtering operation:

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

git diff master@{1}


Filter-Branch Alternative: BFG Repo Cleaner

过滤器分支替代方案:BFG Repo Cleaner

I've heard that the BFG Repo Cleanertool runs faster than git filter-branch, so you might want to check that out as an option too. It's even mentioned officially in the filter-branch documentationas a viable alternative:

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

git-filter-branch allows you to make complex shell-scripted rewrites of your Git history, but you probably don't need this flexibility if you're simply removing unwanted datalike large files or passwords. For those operations you may want to consider The BFG Repo-Cleaner, a JVM-based alternative to git-filter-branch, typically at least 10-50x faster for those use-cases, and with quite different characteristics:

  • Any particular version of a file is cleaned exactly once. The BFG, unlike git-filter-branch, does not give you the opportunity to handle a file differently based on where or when it was committed within your history. This constraint gives the core performance benefit of The BFG, and is well-suited to the task of cleansing bad data - you don't care wherethe bad data is, you just want it gone.

  • By default The BFG takes full advantage of multi-core machines, cleansing commit file-trees in parallel. git-filter-branch cleans commits sequentially (ie in a single-threaded manner), though it ispossible to write filters that include their own parallellism, in the scripts executed against each commit.

  • The command optionsare much more restrictive than git-filter branch, and dedicated just to the tasks of removing unwanted data- e.g: --strip-blobs-bigger-than 1M.

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

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

  • 默认情况下,BFG 充分利用多核机器,并行清理提交文件树。GIT-滤波器分支清洁提交顺序(即,在单线程方式),虽然它可以写入过滤器,包括它们自己的parallellism,在针对每一个执行的脚本提交。

  • 命令选项都远远超过git的过滤分支更严格,并致力于只是为了消除不必要的数据-例如任务:--strip-blobs-bigger-than 1M

Additional Resources

其他资源

  1. Pro Git § 6.4 Git Tools - Rewriting History.
  2. git-filter-branch(1) Manual Page.
  3. git-commit(1) Manual Page.
  4. git-reset(1) Manual Page.
  5. git-rebase(1) Manual Page.
  6. The BFG Repo Cleaner(see also this answer from the creator himself).
  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(另请参阅创建者本人的这个答案)。

回答by Schwern

If you haven't committed anything since, just git rmthe file and git commit --amend.

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

If you have

如果你有

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

will go through each change from merge-pointto HEAD, delete filename.orig and rewrite the change. Using --ignore-unmatchmeans the command won't fail if for some reason filename.orig is missing from a change. That's the recommended way from the Examples section in the git-filter-branch man page.

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

Note for Windows users: The file path mustuse forward slashes

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

回答by Darren

This is the best way:
http://github.com/guides/completely-remove-a-file-from-all-revisions

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

Just be sure to backup the copies of the files first.

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

EDIT

编辑

The edit by Neongot unfortunately rejected during review.
See Neons post below, it might contain useful information!

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



E.g. to remove all *.gzfiles accidentally committed into git repository:

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

That still didn't work for me? (I am currently at git version 1.7.6.1)

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

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

Not sure why, since I only had ONE master branch. Anyways, I finally got my git repo truely cleaned up by pushing into a new empty and bare git repository, e.g.

不知道为什么,因为我只有一个主分支。无论如何,我终于通过推入一个新的空的裸 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 

(yes!)

(是的!)

Then I clone that to a new directory and moved over it's .git folder into this one. e.g.

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

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

(yeah! finally cleaned up!)

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

After verifying that all is well, then you can delete the ../large_dot_gitand ../tmpdirdirectories (maybe in a couple weeks or month from now, just in case...)

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

回答by Roberto Tyley

Rewriting Git history demands changing all the affected commit ids, and so everyone who's working on the project will need to delete their old copies of the repo, and do a fresh clone after you've cleaned the history. The more people it inconveniences, the more you need a good reason to do it - your superfluous file isn't really causing a problem, but if only youare working on the project, you might as well clean up the Git history if you want to!

重写 Git 历史需要更改所有受影响的提交 ID,因此参与该项目的每个人都需要删除其旧的 repo 副本,并在清除历史记录后进行新的克隆。不便的人越多,你就越需要一个很好的理由去做——你多余的文件并没有真正造成问题,但如果只有在做这个项目,如果你愿意,你不妨清理一下 Git 历史记录到!

To make it as easy as possible, I'd recommend using the BFG Repo-Cleaner, a simpler, faster alternative to git-filter-branchspecifically designed for removing files from Git history. One way in which it makes your life easier here is that it actually handles allrefs by default (all tags, branches, etc) but it's also 10 - 50xfaster.

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

You should carefully follow the steps here: http://rtyley.github.com/bfg-repo-cleaner/#usage- but the core bit is just this: download the BFG jar(requires Java 6 or above) and run this command:

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

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

Your entire repository history will be scanned, and any file named filename.orig(that's not in your latestcommit) will be removed. This is considerably easier than using git-filter-branchto do the same thing!

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

Full disclosure: I'm the author of the BFG Repo-Cleaner.

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

回答by paulalexandru

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

回答by Sverrir Sigmundarson

Just to add that to Charles Bailey's solution, I just used a git rebase -i to remove unwanted files from an earlier commit and it worked like a charm. The steps:

只是为了将其添加到 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

回答by lepe

The simplest way I found was suggested by leontalbot(as a comment), which is a post published by Anoopjohn. I think its worth its own space as an answer:

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

(I converted it to a bash script)

(我将其转换为 bash 脚本)

#!/bin/bash
if [[  == "" ]]; then
    echo "Usage: 
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
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=; #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 [[ == "remote" ]]; then git push --all --force fi echo "Done."

All credits goes to Annopjohn, and to leontalbotfor pointing it out.

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

NOTE

笔记

Be aware that the script doesn't include validations, so be sure you don't make mistakes and that you have a backup in case something goes wrong. It worked for me, but it may not work in your situation. USE IT WITH CAUTION (follow the link if you want to know what is going on).

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

回答by nachoparker

Definitely, git filter-branchis the way to go.

当然,git filter-branch是要走的路。

Sadly, this will not suffice to completely remove filename.origfrom your repo, as it can be still be referenced by tags, reflog entries, remotes and so on.

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

I recommend removing all these references as well, and then calling the garbage collector. You can use the git forget-blobscript from thiswebsite to do all this in one step.

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

git forget-blob filename.orig

git forget-blob filename.orig

回答by clarkttfu

If it's the latest commit you want to clean up, I tried with git version 2.14.3 (Apple Git-98):

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

##代码##