如何从 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,结果不是我所期望的。任何人都可以帮助我了解我所缺少的吗?
我有两个分支,master
和 foo
。我提交了 B 和一个我想删除的文件,并提交了 C 我修改了这个文件。随着其他提交,我再也没有碰过这个文件。
提交 ID:
A: f0e0796
B: 5ccb371
C: a46df1c
D: 8eb025b
E: b763a46
1: f5b0116
2: 175e01f
所以我使用 rebase -i f0e0796
并删除 B 5ccb371
和 C 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)
这里F
是git rm that_file
的结果。 git的目的是维护历史。仅仅因为它看起来不漂亮而对其进行修剪是没有成效的(同样,我的意见)。我唯一会推荐前一个选项的情况是相关文件中包含密码等敏感信息。
另一方面,如果您想要擦除文件,则必须采取更极端的措施。例如:
要重新排列提交历史,有几种方法。
rebase
的问题是,当您想更改整个存储库的历史记录时,它一次只能移动一个分支。此外,它在处理合并方面存在问题,因此您不能简单地将 D
和 E
变基到 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
.
实现所需内容的步骤
这些步骤假定您在问题中显示的确切回购。
git checkout master
git rebase -i --rebase-merges f0e0796
- 在交互式变基
todo
文件中:
- 删除您想删除的两个提交(或将它们注释掉,或将
pick
更改为 drop
或 d
)
- 在行
label foo
之后的新行上,添加以下内容:
exec git branch -f foo head
(解释见下文)
- 保存并关闭待办事项文件,瞧瞧,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。
我有:
---A----B-----C-----D--------*-----E-------> (master)
\ /
1----2 (foo)
我需要的:
---A---------------D--------*-----E-------> (master)
\ /
1----2 (foo)
前一段时间我做了两个提交,我想从我的 git 仓库中删除。我尝试了各种不同的变基 "tutorials" 并且所有这些都以奇怪的 git 历史结束,所以我创建了一个示例 repo,结果不是我所期望的。任何人都可以帮助我了解我所缺少的吗?
我有两个分支,master
和 foo
。我提交了 B 和一个我想删除的文件,并提交了 C 我修改了这个文件。随着其他提交,我再也没有碰过这个文件。
提交 ID:
A: f0e0796
B: 5ccb371
C: a46df1c
D: 8eb025b
E: b763a46
1: f5b0116
2: 175e01f
所以我使用 rebase -i f0e0796
并删除 B 5ccb371
和 C 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)
这里F
是git rm that_file
的结果。 git的目的是维护历史。仅仅因为它看起来不漂亮而对其进行修剪是没有成效的(同样,我的意见)。我唯一会推荐前一个选项的情况是相关文件中包含密码等敏感信息。
另一方面,如果您想要擦除文件,则必须采取更极端的措施。例如:
要重新排列提交历史,有几种方法。
rebase
的问题是,当您想更改整个存储库的历史记录时,它一次只能移动一个分支。此外,它在处理合并方面存在问题,因此您不能简单地将 D
和 E
变基到 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
.
这些步骤假定您在问题中显示的确切回购。
git checkout master
git rebase -i --rebase-merges f0e0796
- 在交互式变基
todo
文件中:- 删除您想删除的两个提交(或将它们注释掉,或将
pick
更改为drop
或d
) - 在行
label foo
之后的新行上,添加以下内容:
(解释见下文)exec git branch -f foo head
- 删除您想删除的两个提交(或将它们注释掉,或将
- 保存并关闭待办事项文件,瞧瞧,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 B5ccb371
and and Ca46df1c
, correct? If I interpret the result correctly, this is whatgitk
shows me for my repo, althoughgit 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
选项得到加强,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。