我想删除个人电子邮件地址,从提交到GitHub仓库,所以我跟着Git Bash steps they provide,它在临时克隆仓库中使用git filter-branch
更新受影响的提交,然后运行:
git push --force --tags origin 'refs/heads/*'
现在,回购在GitHub上看起来是正确的,带有清理过的电子邮件。但是,我是Git的新手,并且不确定我需要做什么后续才能同步我的本地副本。
当我试图拉动时,我得到错误“拒绝合并不相关的历史”。
在此之前我没有在本地进行任何更改,所以最简单的方法就是删除我的本地仓库并再次检查项目,但这不是我理解的最佳实践或最灵活的方式。
看起来我需要对重写的历史进行反思,也许是这样的:
git pull --rebase
这是最好的方法吗?如果不是,那是什么?
侧面注意:我正在使用IntelliJ IDEA,理想情况下只使用cmd行来处理像作者更改脚本这样的不寻常的事情,而它的Pull对话框没有Rebase选项,但Update Project确实如此,所以这就是我实际做的。那是对的吗?
当您重写这样的历史记录时,您可以 - 并且您的案例确实 - 获得了相当于新的不同存储库的内容。在这种情况下,旧存储库的所有现有克隆仅与旧存储库一起使用。您现在只需创建新存储库的新克隆,这是一个新项目,您永远不应该连接到旧项目:这两个不再兼容,并且不能再将提交从一个转移到另一个。
这是复杂现实的简化视图,但它应该足以满足您的需求。如果你想了解现实,请继续阅读。
Git存储库的本质是一对数据库。大数据库是保存所有提交的数据库,或者更确切地说,是所有Git对象的数据库。 (有四种类型的Git对象:提交,树,blob和带注释的标记。树和blob是提交如何在其自身内存储文件,而带注释的标记对象只是用于保存带注释的标记数据。)每个唯一的Git对象都有唯一的哈希ID,因此每个提交都有自己唯一的哈希ID,与其他提交不同。
不仅所有这些哈希ID都是唯一的,它们也是通用的。 (它们是Globally Universal IDs or GUIDs, also called UUIDs。)这意味着Universe中的每个Git都为该提交使用相同的GUID。
Git实际实现这一点的方式是ID是提交内容的加密校验和。这意味着在提交中几乎不可能改变任何东西:如果你真的设法改变某些东西,你得到的是一个新的和不同的提交,具有新的和不同的哈希ID。给定哈希ID,Git可以检查它是否有对象。如果是这样,它可以检索该对象。如果没有,你的Git可以向完整对象询问一些其他Git(确实有对象),并将生成的对象填充到其大数据库中。
每当我们有哈希ID并且实际对象在数据库中时,我们就说我们有一个指向该对象的指针。这些指针让我们找到提交(或其他Git对象,但主要是我们使用提交)。
在任何情况下,提交的实际内容通常都很短:每次提交都包含该提交的文件快照的哈希ID - 这是您要永久保留的数据 - 以及一组元数据,例如您的名称和电子邮件地址。但是,每次提交的元数据之一是提交的父哈希ID(如果提交是合并提交,则为多个ID)。因此,每个提交都通过哈希ID指向其父级。
我们可以画这个,如果我们使用单个大写字母代表提交,它甚至看起来有点合理。 (当然我们会快速用完字母,这就是为什么Git使用那些丑陋的哈希ID。)这是一个只有master
和8个提交的存储库的例子,其哈希ID是A
到H
:
A <-B <-C ... <-F <-G <-H <--master
名为master
的分支上的最后一次提交具有哈希ID H
。 Commit H
本身存储commit G
的哈希ID,它存储了F
的ID,依此类推。最终我们一直回到第一次提交,提交A
。它没有父级,因为它不能有一个:它是第一次提交。这让我们(和Git)停下来。
请注意,Git必须始终向后工作。我们总是从最后开始 - 一些分支的提示提交 - 由一些分支名称找到。因此,Git的第二个较小的数据库是一个名称 - 分支名称,标记名称和其他引用的表 - 每个引用都只包含一个哈希ID。当引用是分支名称时,哈希ID是提交的哈希值,并且通过跟随所有向后指向的箭头,我们找到可从分支到达的所有提交。
当我们创建一个新分支时,我们只需创建一个指向某个现有提交的新名称:
...--F--G--H <-- master, develop
现在两个分支都指向提交H
。我们选择一个分支“on”并使用git checkout
将我们的HEAD
附加到分支:
...--F--G--H <-- master, develop (HEAD)
现在我们可以按照通常的方式进行新的提交。当我们这样做时,Git打包我们所有的文件,附上我们的元数据 - 我们的日志消息,名称,电子邮件地址,时间戳等等 - 并写出新的提交。新提交的父级是当前提交H
。新提交的数据哈希到一些丑陋的随机查找字符串,这与其他任何提交都不同,但我们只需要调用I
:
...--F--G--H <-- master, develop (HEAD)
\
I
而现在非常聪明的一点发生了。现在Git将I
的哈希ID写入HEAD
所附加的分支名称:
...--F--G--H <-- master
\
I <-- develop (HEAD)
如果我们切换回master
并在那里做出新的提交,那么这两个分支就会分歧。
git filter-branch
所做的是列出每个提交或每个提交来自某个子集,具体取决于您的选项 - 并开始提取每个提交,运行您指定的过滤器 - 再加上一个,尽管您也可以指定一个 - 并进行新的提交从结果。每当过滤器改变任何东西时,根据定义,新的提交将不会与旧的提交一点一点地相同,因此它将获得不同的哈希ID。额外的过滤器是进行新提交的过程,并且它会自动交换结果的父哈希ID以进行任何早期更改。所以假设你有:
D--E <-- master
/
A--B--C
\
F--G <-- feature
并且您的过滤器会更改作者信息。提交A
成为新的提交A'
:
D--E <-- master
/
A--B--C
\
F--G <-- feature
A' [in progress]
现在filter-branch必须复制B
。即使你的过滤器没有变化,新的提交必须有A'
作为它的父,而不是A
,所以最后的commit-maker改变了父哈希(也许早期的过滤器也改变了作者信息),我们得到:
D--E <-- master
/
A--B--C
\
F--G <-- feature
A'-B' [in progress]
这一直重复到E
和G
:
D--E <-- master
/
A--B--C
\
F--G <-- feature
D'-E' <-- (replacement for master)
/
A'-B'-C'
\
F'-G' <-- (replacement for feature)
一旦git filter-branch
完成了每次提交的通过,它就会替换名称:它将E'
的ID写入master
,将G'
的ID写入feature
,现在你的名字数据库根本不再记住原始的E
和G
,你看的一切都将从E'
或G'
开始。这些新的和(真的或至少据称)改进的提交是你想要的;你想忘记旧的。
旧的提交仍在那里 - 事实上,filter-branch将原始的master
引用复制到例如refs/original/refs/heads/master
,但新的提交集是新的存储库。克隆此存储库不会复制原始文件,只会复制可达到的新的和改进的提交。删除refs/original/
名称将使Git垃圾收集旧的提交,最终(通常在30天后的一段时间,但确切地说多久后,取决于许多其他因素)。
1如果过滤器实际上没有变化,则新提交与原始提交一点一点,因此具有原始提交的哈希ID,并且字面上是原始提交。但是最后一个过滤器,即提交本身的过滤器,通常会改变一些东西。
无论您选择使本地存储库同步的过程如何,最终结果都是相同的:两个存储库具有相同的历史记录。在这种情况下,本地被远程版本覆盖。
因此,由于您没有保留本地更改,因此很可能没有什么比您考虑的再次克隆项目更清洁,更快捷。我会说去吧。
(作为旁注,我认为没有任何理由说这可能是一种不好的做法。您预见到哪些具体问题?)