如何从 git 历史记录中删除提交但在其他方面保持图形完全相同,包括合并?

How to remove commits from git history but otherwise keep the graph exactly the same, including merges?

我有:

---A----B-----C-----D--------*-----E-------> (master)
                     \      /
                      1----2 (foo)

我需要的:

---A---------------D--------*-----E-------> (master)
                    \      /
                     1----2 (foo)

前一段时间我做了两个提交,我想从我的 git 仓库中删除。我尝试了各种不同的变基 "tutorials" 并且所有这些都以奇怪的 git 历史结束,所以我创建了一个示例 repo,结果不是我所期望的。任何人都可以帮助我了解我所缺少的吗?

我有两个分支,masterfoo。我提交了 B 和一个我想删除的文件,并提交了 C 我修改了这个文件。随着其他提交,我再也没有碰过这个文件。

提交 ID:

A: f0e0796
B: 5ccb371
C: a46df1c
D: 8eb025b
E: b763a46
1: f5b0116
2: 175e01f

所以我使用 rebase -i f0e0796 并删除 B 5ccb371C a46df1c,正确的?如果我正确地解释了结果,这就是 gitk 为我的回购显示的内容,尽管 git branches 仍然列出了第二个分支。

---A-----1----2---E------> (master)

谁能告诉我这里发生了什么?

编辑: 这是从第一张图重新创建回购协议的方法:

git init foo
cd foo

touch A
git add A
git commit -m "add A"

touch B
git add B
git commit -m "add B"

echo "modify" > B
git add B
git commit -m "modify B"

touch C
git add C
git commit -m "add C"

git checkout -b foo

touch 1
git add 1
git commit -m "add 1"

touch 2
git add 2
git commit -m "add 2"

git switch master
git merge foo --no-ff

touch E
git add E
git commit -m "add E"

首先要了解的是提交是不可变的对象。当你按照你的建议重写历史时,你最终会得到一组完全不同的提交。父级是每个提交的不可变散列的一部分,以及您无法更改的其他内容。如果您按照您的建议进行操作,您的历史记录将如下所示:

     D'-----E'-----> (master)
    /
---A----B-----C-----D--------E-------> (abandoned)
                     \      /
                      1----2 (foo)

要实现这一点,您只需将 D..E 变基到 A 并将 master 重置为 E'。您可以(但实际上不必)然后将 1..foo 变基到 D'.

一种更简单且在我看来是正确的方法是在新提交中删除文件:

---A----B-----C-----D--------E-----F-----> (master)
                     \      /
                      1----2 (foo)

这里Fgit rm that_file的结果。 git的目的是维护历史。仅仅因为它看起来不漂亮而对其进行修剪是没有成效的(同样,我的意见)。我唯一会推荐前一个选项的情况是相关文件中包含密码等敏感信息。

另一方面,如果您想要擦除文件,则必须采取更极端的措施。例如:

要重新排列提交历史,有几种方法。

rebase 的问题是,当您想更改整个存储库的历史记录时,它一次只能移动一个分支。此外,它在处理合并方面存在问题,因此您不能简单地将 DE 变基到 A 上,同时保留现在存在的更新历史记录(因为 E 是一个合并).

可以解决所有这些问题,但该方法很复杂且容易出错。有些工具专为完整回购重写而设计。您可能想查看 filter-repo(一种替代 filter-branch 的工具)——但听起来您只是想从您的历史记录中清除特定文件,这 (1) 可能是一项不错的工作对于 BFG Repo Cleaner,或者 (2) 实际上是一个很简单的任务 filter-branch

(如果你想看BFG,https://rtyley.github.io/bfg-repo-cleaner/ ; if you want to look into filter-repo, https://github.com/newren/git-filter-repo)

为此目的使用filter-branch

git filter-branch --index-filter 'git rm --cached --ignore-unmatch path/to/file' --prune-empty -- --all

但是 - 您表示您需要该文件不在回购协议中(作为对某人建议从下一次提交中删除它的反驳)。因此,您需要了解 git 不会轻易放弃信息。使用这些技术中的任何一种后,您仍然可以从存储库中提取文件。

这是一个很大的话题,已经在 SO 上的各种 questions/answers 中讨论了很多次,所以我建议搜索您真正需要询问的内容:如何永久删除文件那永远不应该受到源代码控制。

一些注意事项:

1 - 如果有密码并且曾经被推送到共享远程,则这些密码已被泄露。你对此无能为力;更改密码。

2 - 每个存储库(远程和每个克隆)都必须有意地擦洗,或者扔掉并更换。 (如果某人不想合作,你不能强迫他们这样做,这是 (1) 的原因之一。)

3 - 在你进行修复的本地仓库中,你必须删除引用日志(以及如果你使用像 filter-branch 这样的工具可能已经创建的备份引用),然后运行gc。或者,重新克隆到一个只获取分支的新版本的新仓库可能更容易。

4 - 甚至可能无法清理遥控器,具体取决于它的托管方式。有时你能做的最好的事情就是核对遥控器然后从头开始重新创建它。

虽然我的提议会给你一个干净的、线性的历史;这就是 rebase 本质上应该做的事情。但是,我希望这能为您提供一种从提交历史记录中删除 B 和 B' 的方法。解释如下:

Repo recreation output:
---A----B-----B'-----C--------D-------> (master)
                      \      /
                       1----2 (foo)

git log --graph --all --oneline --decorate #initial view the git commit graph
* dfa0f63 (HEAD -> master) add E
*   843612e Merge branch 'foo'
|\  
| * 3fd261f (foo) add 2
| * ed338bb add 1
|/  
* bf79650 add C
* ff94039 modify B
* 583110a add B
* cd8f6cd add A

git rebase -i HEAD~5 #here you drop 583110a/add B and ff94039/modify B from
foo branch.

git log --graph --all --oneline --decorate
$ git rebase -i HEAD~5
* 701d9e7 (HEAD -> master) add E
* 5a4be4f add 2
* 75b43d5 add 1
* 151742d add C
| * 3fd261f (foo) add 2
| * ed338bb add 1
| * bf79650 add C
| * ff94039 modify B
| * 583110a add B
|/  
* cd8f6cd add A

$ git rebase -i master foo #drop 583110a/add B and ff94039/modify B again

$ git log --graph --all --oneline --decorate #view the git commit graph

* 701d9e7 (HEAD -> foo, master) add E
* 5a4be4f add 2
* 75b43d5 add 1
* 151742d add C
* cd8f6cd add A

最后,最后的结果可能不是您预期的顺序 A--C--1---2---E。但是,您可以再次在交互模式中重新安排顺序。试试 git rebase -i HEAD~n。

注意:最好避免更改commit/publishing历史记录。我是新手,正在探索 git,希望上面的解决方案能够坚持下去。也就是说,我确信网上还有大量其他更简单的解决方案。我发现这个 article 很有帮助,供大家以后参考。

git rebase 默认情况下只变基到单一的提交历史沿袭,因为这是人们更普遍想要的。如果您不另行告诉它,它将为您签出的分支执行此操作(在您的情况下是 master)。这就是为什么你最终得到一个重新设置基础的 master 分支,其中 foo 提交嫁接而不是合并,并且 foo 本身不变并且不再连接。

如果您有 git 2.18 或更高版本,您可以使用 --rebase-merges 选项*告诉 git 重新创建合并历史,而不是像默认情况下那样将其线性化。重新定位的历史记录将具有相同的分支和合并返回。下面我将引导您完成使用 --rebase-merges.

实现所需内容的步骤

这些步骤假定您在问题中显示的确切回购。

  1. git checkout master
  2. git rebase -i --rebase-merges f0e0796
  3. 在交互式变基 todo 文件中:
    • 删除您想删除的两个提交(或将它们注释掉,或将 pick 更改为 dropd
    • 在行 label foo 之后的新行上,添加以下内容:
    exec git branch -f foo head
    
    (解释见下文)
  4. 保存并关闭待办事项文件,瞧瞧,git 将根据您想要的图形重新设置提交的基数。


todo 文件解释

git rebase 只是自动执行您也可以手动执行的一系列步骤。此步骤序列在 todo 文件中表示。 git rebase --interactive 允许您在序列执行之前对其进行修改。

我会用解释来注释它,包括您将如何手动操作(很好的学习经验)。如果你在未来做了很多 rebase,那么了解这一点很重要,这样当合并冲突发生时,或者当你告诉 rebase 在某些点暂停以便你可以做一些手动修改时,你会有很好的方位。

label onto                  // labels "rebase onto" commit (f0e0796)
                            // this is what you would do in your head
                            // if doing this manually
# Branch foo
reset onto                  // git reset --hard <onto>
drop 5ccb371 add B          // skip this commit
drop a46df1c modify B       // skip this commit
pick 8eb025b add C          // git cherry-pick 8eb025b
label branch-point          // label this commit so we can reset back to it later
pick f5b0116 add 1          // git cherry-pick f5b0116
pick 175e01f add 2          // git cherry-pick 175e01f
label foo                   // label this commit so we can merge it later
                            //   This is just a rebase internal label. 
                            //   It does not affect the `foo` branch ref.
exec git branch -f foo head // point the `foo` branch ref to this commit 

reset branch-point # add C  // git reset --hard <branch-point>
merge -C b763a46 foo # Merge branch 'foo'  // git merge --no-ff foo
                                           // use comment from b763a46

exec git branch -f foo head解释

正如我上面提到的,git rebase 只在一个分支上运行。此 exec 命令的作用是将 ref foo 更改为指向当前 head。正如您在 todo 文件的序列中看到的,您告诉它在提交 foo 分支 ("add 2") 的最后一次提交后立即执行此操作,该分支被方便地标记为 label foo 在待办事项文件中。

如果您不再需要 foo 引用(例如,它是一个功能分支,这是它的最终合并),您可以跳过将此行添加到待办事项文件中。

您也可以跳过添加此行并在 rebase 完成后单独将 foo 重新指向您想要的提交:

git branch -f foo <hash of the rebased commit that should be the new head of `foo`>

如果您有任何问题,请告诉我。


*如果你有旧版本的 git,你可以使用现已弃用的 --preserve-merges 选项,尽管它与 rebase 的交互模式不兼容。

So I use rebase -i f0e0796 and remove B 5ccb371 and and C a46df1c, correct? If I interpret the result correctly, this is what gitk shows me for my repo, although git branches still lists the second branch.

...A---1---2---E    master

Can anyone tell me what happened here?

这就是它的目的:生成从单个尖端到单个碱基的无合并线性历史,保留所有可能仍需要合并回新碱基的部分。

rebase 文档对此可能更清楚:“commits which are clean cherry-picks (as determined by git log --cherry-mark …) are always dropped." is mentioned only as an aside in an option for how to treat empty commits, and "by default, a rebase will simply drop merge commits from the todo list, and put the rebased commits into a single, linear branch.”仅在另一个选项的描述中进一步提及。但这就是它的用途,它可以自动执行繁琐的识别和消除已经应用的修复程序,并从其他直接的樱桃选择中合并噪音。


Is git rebase the feature I am looking for my problem?

不是真的。 --rebase-merges 选项得到加强, works well for your specific case, but see the warnings in its docs:它有真正的限制和注意事项。正如 Inigo 的回答所指出的,“[t]这些步骤假定您在问题中显示的确切回购”,并且“git rebase 只是自动执行一系列您也可以手动执行的步骤”。这个答案的原因是,对于一次性工作,通常最好只做。

Rebase 的构建是为了自动化一个工作流程,在这个工作流程中,您有一个分支,您要从中合并或在开发过程中以其他方式保持同步,并且至少对于您想要的最终合并回(可能在此之前几次)清理你的历史。

它对于许多其他用途(特别是携带补丁)很方便,但同样:它不是万灵药。你需要lots of hammers。他们中的许多人都可以在紧要关头使用,我是“任何有用的东西”的忠实拥护者,但我认为这对于已经非常熟悉他们的工具的人来说是最好的。

您想要的不是生成单一的、干净的线性历史,您想要的是不同的东西。

使用熟悉的工具进行此操作的一般方法很简单,从您的演示脚本开始就可以了

git checkout :/A; git cherry-pick :/D :/1 :/2; git branch -f foo
git checkout foo^{/D}; git merge foo; git cherry-pick :/E; git branch -f master

大功告成。

是的,您 可以 得到 git rebase -ir 来为您设置,但是当我查看生成的选择列表时,在正确的说明中编辑并没有看起来比上面的序列更简单或更容易。弄清楚您想要什么确切的结果,并弄清楚如何让 git rebase -ir 为您做到这一点,然后就是这样做。

git rebase -r --onto :/A :/C master
git branch -f foo :/2

是我可能会使用的“任何有效”的答案,正如 Inigo 所说的“您在问题中显示的确切回购协议”。参见 the git help revisions docs for the message-search syntax