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

Posted

技术标签:

【中文标题】你如何修复一个错误的合并,并将你好的提交重播到一个固定的合并上?【英文标题】:How do you fix a bad merge, and replay your good commits onto a fixed merge? 【发布时间】:2010-09-23 09:32:11 【问题描述】:

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

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

【问题讨论】:

相关:How to remove/delete a large file from commit history in Git repository?. 相关help.github.com/articles/… 【参考方案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 垃圾收集这些提交 感谢您指出这一点。我的 repo 提交了许多大型二进制文件,并且每晚都会完全备份 repo。所以我只是想要它的每一点;)【参考方案2】:

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

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

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

git forget-blob filename.orig

【讨论】:

是要走的路” - 它不再是 - even the docu says you should use git filter-repo instead【参考方案3】:
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

【讨论】:

虽然所有答案似乎都在 filter-branch 轨道上,但这个答案突出了如何清理历史记录中的所有分支。【参考方案4】:

leontalbot(作为评论)建议了我找到的最简单的方法,即post published by 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 指出。

注意

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

【讨论】:

【参考方案5】:

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

如果你有

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

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

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

【讨论】:

谢谢! git filter-branch 为我工作,其中作为答案给出的变基示例没有:这些步骤似乎有效,但随后推送失败。拉动,然后成功推送,但文件仍然存在。试图重做变基步骤,然后它因合并冲突而变得一团糟。我使用了一个稍微不同的 filter-branch 命令,这里给出了“一种改进的方法”:github.com/guides/completely-remove-a-file-from-all-revisions git filter-branch -f --index-filter 'git update-index --remove filename' ..HEAD 我不确定哪一种是改进的方法。 git-filter-branch的Git官方文档好像给了第一个。 查看zyxware.com/articles/4027/… 我发现它是涉及filter-branch 的最完整和最直接的解决方案 @atomicules,如果您尝试将本地存储库推送到远程存储库,git 将坚持首先从远程提取,因为它具有您本地没有的更改。您可以使用 --force 标志推送到远程 - 它会从那里完全删除文件。但请注意,请确保您不会强制覆盖除文件以外的其他内容。 记住在使用 Windows 时使用" 而不是',否则您将收到一个无用的措辞“错误修订”错误。【参考方案6】:

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

原海报说:

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

是吗 可以重写更改历史记录,以使 filename.orig 永远不会 首先添加到存储库中?

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

    修改提交。 硬重置(可能加上变基)。 非交互式变基。 交互式变基。 过滤分支。

在原始海报的情况下,修改提交并不是一个真正的选择 就其本身而言,因为他之后做了几次额外的提交,但为了 为了完整性,我还将为其他任何人解释如何做到这一点 想要修改他们之前的提交。

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


解决方案 1:修改提交

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

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

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

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

git reset --hard HEAD^

该命令会将您的分支硬重置为之前的 1st 父级 提交。

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

# 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”替换为 在第 1st 行“编辑”,如下所示:

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>'

这将从根提交开始,从所有提交中删除 &lt;file&gt;。如果 相反,您只想重写提交范围HEAD~5..HEAD,那么您可以 将其作为附加参数传递给filter-branch,正如在 this answer:

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

同样,在filter-branch 完成后,验证通常是个好主意 通过将您的分支与其 过滤操作前的前一个状态:

git diff master@1

Filter-Branch 替代方案:BFG Repo Cleaner

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

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

文件的任何特定版本都被精确地清理一次。 BFG 与 git-filter-branch 不同,它不给你处理的机会 一个文件根据它在您的内部提交的位置或时间而不同 历史。此约束提供了核心性能优势 BFG,非常适合清理不良数据的任务——你不需要 关心哪里坏数据在哪里,你只是希望它消失

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

command options多 比 git-filter 分支更具限制性,并且专门用于 删除不需要的数据的任务 - 例如:--strip-blobs-bigger-than 1M

其他资源

    Pro Git § 6.4 Git Tools - Rewriting History。 git-filter-branch(1) Manual Page。 git-commit(1) Manual Page。 git-reset(1) Manual Page。 git-rebase(1) Manual Page。 The BFG Repo Cleaner(另见this answer from the creator himself)。

【讨论】:

filter-branch 是否会导致重新计算哈希?如果一个团队使用一个应该过滤大文件的存储库,他们如何做到这一点,以便每个人最终都得到相同的存储库状态? @YakovL。一切都重新计算哈希。实际上,提交是不可变的。它创建了一个全新的历史,并将您的分支指针移动到它。确保每个人都有相同历史记录的唯一方法是硬重置。 你是救生员。解决方案 5 为我做到了!【参考方案7】:

你也可以使用:

git reset HEAD file/path

【讨论】:

如果文件已被添加到提交中,那么这甚至不会从索引中删除文件,它只是将索引重置为文件的 HEAD 版本。【参考方案8】:

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

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

您应该仔细按照此处的步骤操作: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 的文件(不在您的 latest commit 中)。这比使用git-filter-branch 做同样的事情要容易得多!

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

【讨论】:

这是一个出色的工具:一个命令,它会产生非常清晰的输出并提供一个将每个旧提交匹配到新提交的日志文件。我不喜欢安装 Java,但这是值得的。 这是唯一对我有用的东西,但这就像因为我没有正确工作 git filter-branch 。 :-)【参考方案9】:

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

虽然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 以提供给 @ 987654327@ 命令而不是使用临时分支名称。)

【讨论】:

git rebase -i 会不会更快又更简单? $ git rebase -i 将正确的标记为“编辑” $ git rm somefile.orig $ git commit --amend $ git rebase --continue 但是由于某种原因,我仍然有那个文件最后我这样做的时间。可能遗漏了什么。 git rebase -i 非常有用,尤其是当您要执行多个 rebase-y 操作时,但是当您实际上并没有指向某人的肩膀并且可以看到他们的内容时,准确地描述是一种正确的痛苦'正在和他们的编辑一起做。我使用 vim,但不是每个人都会对:“ggjcesquashjddjp:wq”和“将第一行移到当前第二行之后并将第四行的第一个单词更改为 'edit' 现在保存和退出”很快看起来比实际步骤更复杂。您通常也会执行一些 --amend--continue 操作。 我这样做了,但是在修改后的提交之上重新应用了一个新的提交,并带有相同的消息。显然 git 在包含不需要的文件的旧的、未修改的提交和来自另一个分支的固定提交之间进行了 3 路合并,因此它在旧的提交之上创建了一个新的提交,以重新应用文件。跨度> @UncleCJ:您的文件是在合并提交中添加的吗?这个很重要。这个秘籍旨在应对错误的合并提交。如果您不需要的文件被添加到历史记录中的正常提交中,它将无法正常工作。 我很惊讶我是如何使用 smartgit 而完全没有终端来完成这一切的!谢谢你的食谱!【参考方案10】:

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

【讨论】:

【参考方案11】:

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

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

编辑

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


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

$ 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_git../tmpdir 目录(可能在几周或几个月后,以防万一......)

【讨论】:

这在“那仍然对我不起作用?”之前对我有用。评论 很好的答案,但建议在 filter-branch 命令中添加--prune-empty【参考方案12】:

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

【讨论】:

以上是关于你如何修复一个错误的合并,并将你好的提交重播到一个固定的合并上?的主要内容,如果未能解决你的问题,请参考以下文章

Git:如何防止特定的提交被合并到另一个分支中?

你如何恢复错误的 git 合并提交

如何将错误修复分支合并到主分支?

将父分支合并到子分支

还原合并提交后解决合并冲突

恢复分支独有的提交