如何将整个 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 --amendgit rebase。原始提交 — 其不变的哈希 ID 与提交本身一样永久 — 仍在您的存储库中。

您的每个 分支名称 都只是一个指针。也就是说,每个存储一个哈希ID。 master 中存储的一个哈希 ID 今天可能是 5d826e972970a784bd7a7bdf587512510097b8c7。明天,在你进行新的提交之后,它会是别的东西,但在 master.

中仍然只有 一个 hash ID

当您进行 new 提交时,Git:

  1. 打包完整的快照(来自您的索引,又名暂存区或缓存)。这使得 Git 树 object (不是您通常需要关心的东西)获得自己的哈希 ID。

  2. 收集您的日志消息、您的姓名和电子邮件地址以及当前 date/time 等。为此,Git 添加来自步骤 1 的树哈希,parent 行记录来自分支名称的 current 提交的哈希 ID,以及任何其他合适的元数据Git 想包括在内。从所有这些数据中,Git 进行了 提交 object,它获得了自己新的、唯一的哈希 ID。

    (时间戳的部分原因是为了确保哈希 ID 是唯一的。否则,如果您创建了一个与旧快照匹配的新快照,具有相同的 parent 和其他元数据,你会得到 old 提交的哈希 ID!这会使两个分支合并为一个分支。这实际上不是致命的 - 你可以通过欺骗和脚本来使这种情况发生例如,每秒进行多次提交,它确实有效——但它非常令人惊讶,并且有可能破坏一些工作流程。)

  3. 将步骤 2 中的哈希 ID 记录到当前分支名称中。瞧,分支现在指向 new 提交。新提交指向 先前的 提交。

因此,在进行此新提交之前,如果名称 master 指向某个提交 H(此处的 H 代表实际哈希 ID),其 parent 是 GG 的 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} 记住了指向 Imaster 的哈希 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 兼容的命令行解释器。)