如何将整个 git 存储库(包括所有分支)恢复到以前的日期?
How to revert an entire git repository -- including all branches -- to a previous date?
我搞砸了我的 github 存储库。我已经将我希望没有的东西推送到 GitHub(不仅仅是本地)。仓库有20个"steps",Step1
合并到Step2
,再合并到Step3
,一直到Step20
,乱得一塌糊涂。
我希望我的 整个 存储库恢复到 12 月 10 日的状态,因为 all 整个仓库中的分支。
对于给定的分支,我已经看到了多种方法,我想我可以这样做二十次,对吗?
但是,我希望有一种方法可以做到这一点,而无需检查所有 20 个分支并将每个分支都设置回去。
除非你曾经删除提交或删除一些分支名称,或删除并re-cloned你的存储库,这对于典型的设置来说实际上很容易。这部分是由于:
... December 10 ...
此时,也就是十天前。由于 reflog 条目到期,30 或 90 天后事情会变得更难。 (这假定您没有告诉 Git 使用不同的保留时间段的默认配置。)
背景
这里要记住的是 Git 存储的不是 changes 而不是 files,而是 提交。 (每次提交存储文件,所以 Git 间接存储文件,但事物可见的级别是提交。)每个提交都由一些哈希 ID 唯一标识,这是一个丑陋的大字符串,如 5d826e972970a784bd7a7bdf587512510097b8c7
.通常,您也只会 添加新提交 到 Git 存储库。即使 似乎 删除提交的操作也是如此,例如 git commit --amend
或 git rebase
。原始提交 — 其不变的哈希 ID 与提交本身一样永久 — 仍在您的存储库中。
您的每个 分支名称 都只是一个指针。也就是说,每个存储一个哈希ID。 master
中存储的一个哈希 ID 今天可能是 5d826e972970a784bd7a7bdf587512510097b8c7
。明天,在你进行新的提交之后,它会是别的东西,但在 master
.
中仍然只有 一个 hash ID
当您进行 new 提交时,Git:
打包完整的快照(来自您的索引,又名暂存区或缓存)。这使得 Git 树 object (不是您通常需要关心的东西)获得自己的哈希 ID。
收集您的日志消息、您的姓名和电子邮件地址以及当前 date/time 等。为此,Git 添加来自步骤 1 的树哈希,parent
行记录来自分支名称的 current 提交的哈希 ID,以及任何其他合适的元数据Git 想包括在内。从所有这些数据中,Git 进行了 提交 object,它获得了自己新的、唯一的哈希 ID。
(时间戳的部分原因是为了确保哈希 ID 是唯一的。否则,如果您创建了一个与旧快照匹配的新快照,具有相同的 parent 和其他元数据,你会得到 old 提交的哈希 ID!这会使两个分支合并为一个分支。这实际上不是致命的 - 你可以通过欺骗和脚本来使这种情况发生例如,每秒进行多次提交,它确实有效——但它非常令人惊讶,并且有可能破坏一些工作流程。)
将步骤 2 中的哈希 ID 记录到当前分支名称中。瞧,分支现在指向 new 提交。新提交指向 先前的 提交。
因此,在进行此新提交之前,如果名称 master
指向某个提交 H
(此处的 H 代表实际哈希 ID),其 parent 是 G
,G
的 parent 为 F
,依此类推,您有:
... <-F <-G <-H <--master
之后,如果我们有I
代替新的提交,图片是:
... <-F <-G <-H <-I <--master
引用日志
每当 Git 更新一个引用,例如像 master
这样的分支名称时,Git 记录该分支名称的 previous 值到日志中。此日志称为 reflog 以供参考。每个日志条目上也有一个 date/time 标记,因此您可以 运行 git reflog master
将其溢出:
$ git reflog master
5d826e9729 master@{0}: merge refs/remotes/origin/master: Fast-forward
965798d1f2 master@{1}: merge refs/remotes/origin/master: Fast-forward
8a0ba68f6d master@{2}: merge refs/remotes/origin/master: Fast-forward
...
(这是我 Git 的 Git 存储库,其中 not-very-interesting 事情发生在 master
上:基本上,我一直 fast-forward 它.)
这些条目根据它们在日志中的新近度编号:@{0}
是当前值,@{1}
是前一个,@{2}
是前一个 [=40] =] 是 @{0}
等等。 git reflog
的默认设置是按编号打印它们,像这样,但是使用 --date=relative
,它会打印它们的 时间戳 :
5d826e9729 master@{11 days ago}: merge refs/remotes/origin/master: Fast-forward
965798d1f2 master@{12 days ago}: merge refs/remotes/origin/master: Fast-forward
等等。
您还可以使用 --date=local
和许多其他格式。要查看全部,请阅读 git log
文档(git reflog
实际上是 git log -g
)。现在用 --date=local
试试吧。
你的工作
想象一下,我们有这样简单的事情:
...--F--G--H--I <--master
(我已经停止绘制内部箭头,因为它太难/太烦人了,你马上就会看到。请记住,它们必须指向 向后: 你不能做一个旧的提交,转发到一个还不存在的新提交,因为你不知道新提交的哈希 ID,直到 after 你才知道。然后就太晚了,因为 nothing in any 提交——或者事实上,在任何 Git 内部object——可以永远改变。)
为了让master
回到昨天的样子,当它指向H
而不是I
时,你只需要告诉Git: 现在设置名称 master
指向提交 H
。 结果是:
I
/
...--F--G--H <-- master
提交 I
并未 消失 。事实上,这只是在 reflog 中添加了一个新条目,因此 master@{1}
记住了指向 I
时 master
的哈希 ID。 (那个新的 reflog 条目具有“now”的 date-and-time 标记。)
最终——默认情况下至少 30 天,大多数典型情况下为 90 天——Git 将扫描 reflog 以查找 master
并丢弃任何太旧的条目。但至少在 30 天后,master@{<something>}
会记住提交的哈希 ID I
.
那么,我们所要做的就是找出 所有 reflog 所说的 关于 所有分支 的内容。 对于十天前存在的每个分支,该分支执行了哪个提交 point-to?
您可以通过枚举所有分支名称来做到这一点:
$ git branch
<list of names spills out, one of them probably marked `*` as current branch>
然后,对于每个名字,你可以运行 git reflog --date=local <em>name</em>
并查找对应于 12 月 10 日的条目。这可能是 10 号之前的日期,如本例所示:
$ git reflog --date=local master
5d826e9729 master@{Sun Dec 9 11:03:46 2018}: merge refs/remotes/origin/master: Fast-forward
965798d1f2 master@{Sat Dec 8 07:53:19 2018}: merge refs/remotes/origin/master: Fast-forward
这意味着5d826e9729
是9日持有的值master
,此后没有更新,所以它也必须是10日持有的值master
。
您可以对每个分支名称重复此操作,并确定您希望每个分支名称标识哪个提交。如果该分支在 10 日之前不存在,您可能希望 完全删除 该分支;请注意,如果您这样做,Git 也会删除其 reflog,因此无法返回。
不过,还有一种更简单的方法。 reflog 语法允许您编写:
master@{10.days.ago}
或:
master@{10.dec.2018}
让Git自己解决这个问题!通常,任何采用散列 ID 的 Git 命令也采用此散列 ID。例如,git rev-parse
将名称转换为相应的哈希 ID:
$ git rev-parse master
5d826e972970a784bd7a7bdf587512510097b8c7
但是:
$ git rev-parse master@{8.dec.2018}
965798d1f2992a4bdadb81eba195a7d465b6454a
我在这里使用了 8 dec
因为我的 reflog 之后没有任何有趣的东西。注意出来的是96579...
,也就是master@{1}
,而是从8号开始的
注意:如果您在所选日期有 多个 值,Git 将选择 一个他们。事实上,8.dec.2018
表示那天的 midnight,如果您想要 0900 或 1432 或其他时间,您可以添加更多时间说明符。还要注意时区问题——确保你选择了正确的 reflog 条目!
自动化这个
与其手动调用 git branch
、复制名称,然后手动执行每个,您可以从以下开始:
git for-each-ref --format='%(refname:short)' refs/heads |
while read branch; do echo git branch -f $branch $branch@{10.dec}; done
它打印出它 会 运行 的命令,如果它 echo
被取出的话。
如果它们看起来不错,并且您真的确定这一切都是正确的,那么您现在可以取出 echo
让命令真正发生。
(假设有一个 sh/bash 兼容的命令行解释器。)
我搞砸了我的 github 存储库。我已经将我希望没有的东西推送到 GitHub(不仅仅是本地)。仓库有20个"steps",Step1
合并到Step2
,再合并到Step3
,一直到Step20
,乱得一塌糊涂。
我希望我的 整个 存储库恢复到 12 月 10 日的状态,因为 all 整个仓库中的分支。
对于给定的分支,我已经看到了多种方法,我想我可以这样做二十次,对吗?
但是,我希望有一种方法可以做到这一点,而无需检查所有 20 个分支并将每个分支都设置回去。
除非你曾经删除提交或删除一些分支名称,或删除并re-cloned你的存储库,这对于典型的设置来说实际上很容易。这部分是由于:
... December 10 ...
此时,也就是十天前。由于 reflog 条目到期,30 或 90 天后事情会变得更难。 (这假定您没有告诉 Git 使用不同的保留时间段的默认配置。)
背景
这里要记住的是 Git 存储的不是 changes 而不是 files,而是 提交。 (每次提交存储文件,所以 Git 间接存储文件,但事物可见的级别是提交。)每个提交都由一些哈希 ID 唯一标识,这是一个丑陋的大字符串,如 5d826e972970a784bd7a7bdf587512510097b8c7
.通常,您也只会 添加新提交 到 Git 存储库。即使 似乎 删除提交的操作也是如此,例如 git commit --amend
或 git rebase
。原始提交 — 其不变的哈希 ID 与提交本身一样永久 — 仍在您的存储库中。
您的每个 分支名称 都只是一个指针。也就是说,每个存储一个哈希ID。 master
中存储的一个哈希 ID 今天可能是 5d826e972970a784bd7a7bdf587512510097b8c7
。明天,在你进行新的提交之后,它会是别的东西,但在 master
.
当您进行 new 提交时,Git:
打包完整的快照(来自您的索引,又名暂存区或缓存)。这使得 Git 树 object (不是您通常需要关心的东西)获得自己的哈希 ID。
收集您的日志消息、您的姓名和电子邮件地址以及当前 date/time 等。为此,Git 添加来自步骤 1 的树哈希,
parent
行记录来自分支名称的 current 提交的哈希 ID,以及任何其他合适的元数据Git 想包括在内。从所有这些数据中,Git 进行了 提交 object,它获得了自己新的、唯一的哈希 ID。(时间戳的部分原因是为了确保哈希 ID 是唯一的。否则,如果您创建了一个与旧快照匹配的新快照,具有相同的 parent 和其他元数据,你会得到 old 提交的哈希 ID!这会使两个分支合并为一个分支。这实际上不是致命的 - 你可以通过欺骗和脚本来使这种情况发生例如,每秒进行多次提交,它确实有效——但它非常令人惊讶,并且有可能破坏一些工作流程。)
将步骤 2 中的哈希 ID 记录到当前分支名称中。瞧,分支现在指向 new 提交。新提交指向 先前的 提交。
因此,在进行此新提交之前,如果名称 master
指向某个提交 H
(此处的 H 代表实际哈希 ID),其 parent 是 G
,G
的 parent 为 F
,依此类推,您有:
... <-F <-G <-H <--master
之后,如果我们有I
代替新的提交,图片是:
... <-F <-G <-H <-I <--master
引用日志
每当 Git 更新一个引用,例如像 master
这样的分支名称时,Git 记录该分支名称的 previous 值到日志中。此日志称为 reflog 以供参考。每个日志条目上也有一个 date/time 标记,因此您可以 运行 git reflog master
将其溢出:
$ git reflog master
5d826e9729 master@{0}: merge refs/remotes/origin/master: Fast-forward
965798d1f2 master@{1}: merge refs/remotes/origin/master: Fast-forward
8a0ba68f6d master@{2}: merge refs/remotes/origin/master: Fast-forward
...
(这是我 Git 的 Git 存储库,其中 not-very-interesting 事情发生在 master
上:基本上,我一直 fast-forward 它.)
这些条目根据它们在日志中的新近度编号:@{0}
是当前值,@{1}
是前一个,@{2}
是前一个 [=40] =] 是 @{0}
等等。 git reflog
的默认设置是按编号打印它们,像这样,但是使用 --date=relative
,它会打印它们的 时间戳 :
5d826e9729 master@{11 days ago}: merge refs/remotes/origin/master: Fast-forward
965798d1f2 master@{12 days ago}: merge refs/remotes/origin/master: Fast-forward
等等。
您还可以使用 --date=local
和许多其他格式。要查看全部,请阅读 git log
文档(git reflog
实际上是 git log -g
)。现在用 --date=local
试试吧。
你的工作
想象一下,我们有这样简单的事情:
...--F--G--H--I <--master
(我已经停止绘制内部箭头,因为它太难/太烦人了,你马上就会看到。请记住,它们必须指向 向后: 你不能做一个旧的提交,转发到一个还不存在的新提交,因为你不知道新提交的哈希 ID,直到 after 你才知道。然后就太晚了,因为 nothing in any 提交——或者事实上,在任何 Git 内部object——可以永远改变。)
为了让master
回到昨天的样子,当它指向H
而不是I
时,你只需要告诉Git: 现在设置名称 master
指向提交 H
。 结果是:
I
/
...--F--G--H <-- master
提交 I
并未 消失 。事实上,这只是在 reflog 中添加了一个新条目,因此 master@{1}
记住了指向 I
时 master
的哈希 ID。 (那个新的 reflog 条目具有“now”的 date-and-time 标记。)
最终——默认情况下至少 30 天,大多数典型情况下为 90 天——Git 将扫描 reflog 以查找 master
并丢弃任何太旧的条目。但至少在 30 天后,master@{<something>}
会记住提交的哈希 ID I
.
那么,我们所要做的就是找出 所有 reflog 所说的 关于 所有分支 的内容。 对于十天前存在的每个分支,该分支执行了哪个提交 point-to?
您可以通过枚举所有分支名称来做到这一点:
$ git branch
<list of names spills out, one of them probably marked `*` as current branch>
然后,对于每个名字,你可以运行 git reflog --date=local <em>name</em>
并查找对应于 12 月 10 日的条目。这可能是 10 号之前的日期,如本例所示:
$ git reflog --date=local master
5d826e9729 master@{Sun Dec 9 11:03:46 2018}: merge refs/remotes/origin/master: Fast-forward
965798d1f2 master@{Sat Dec 8 07:53:19 2018}: merge refs/remotes/origin/master: Fast-forward
这意味着5d826e9729
是9日持有的值master
,此后没有更新,所以它也必须是10日持有的值master
。
您可以对每个分支名称重复此操作,并确定您希望每个分支名称标识哪个提交。如果该分支在 10 日之前不存在,您可能希望 完全删除 该分支;请注意,如果您这样做,Git 也会删除其 reflog,因此无法返回。
不过,还有一种更简单的方法。 reflog 语法允许您编写:
master@{10.days.ago}
或:
master@{10.dec.2018}
让Git自己解决这个问题!通常,任何采用散列 ID 的 Git 命令也采用此散列 ID。例如,git rev-parse
将名称转换为相应的哈希 ID:
$ git rev-parse master
5d826e972970a784bd7a7bdf587512510097b8c7
但是:
$ git rev-parse master@{8.dec.2018}
965798d1f2992a4bdadb81eba195a7d465b6454a
我在这里使用了 8 dec
因为我的 reflog 之后没有任何有趣的东西。注意出来的是96579...
,也就是master@{1}
,而是从8号开始的
注意:如果您在所选日期有 多个 值,Git 将选择 一个他们。事实上,8.dec.2018
表示那天的 midnight,如果您想要 0900 或 1432 或其他时间,您可以添加更多时间说明符。还要注意时区问题——确保你选择了正确的 reflog 条目!
自动化这个
与其手动调用 git branch
、复制名称,然后手动执行每个,您可以从以下开始:
git for-each-ref --format='%(refname:short)' refs/heads |
while read branch; do echo git branch -f $branch $branch@{10.dec}; done
它打印出它 会 运行 的命令,如果它 echo
被取出的话。
如果它们看起来不错,并且您真的确定这一切都是正确的,那么您现在可以取出 echo
让命令真正发生。
(假设有一个 sh/bash 兼容的命令行解释器。)