首页 > 解决方案 > 如何将本地文件恢复到 git pull 之前的状态

问题描述

我试图将远程 repo 拉到我的本地文件中,并忘记在操作之前存储我的本地文件。由于我的本地更改已经消失并且我进行了很多更改,我该如何恢复此过程?

标签: git

解决方案


这不是一个真正的答案。不幸的是,没有答案。一般来说,Git 不应该丢失你的文件,除非你明确告诉它这样做。不过,有很多方法可以告诉 Git为我丢失了我的文件。如果没有看到你运行的确切命令集,除了一般建议之外,没有人能给你任何东西。


一般来说,丢失未保存工作的拉取或合并应该会失败:

$ git merge
Updating 51ebf55b93..98cedd0233
error: Your local changes to the following files would be overwritten by merge:
    Makefile
Please commit your changes or stash them before you merge.
Aborting

git pull在我运行 Git 2.24.0 的机器上产生相同的消息。

一些早期的 Git 版本会给出不同的错误,如果你有未提交的工作,一些真正古老的 Git 版本确实有时会破坏你的工作树。你没有提到你拥有哪个版本的 Git,但如果它至少是 Git 2.0,这应该不是问题。

尽管如此,我建议避免 git pull. 什么git pull是运行两个 Git 命令。第一个很简单git fetch。例如,跑步git pull origin跑步git fetch origin;跑步git pull origin master跑步git fetch origin master;并且在没有选项的情况下git pull运行git fetch,在没有选项的情况下运行。

危险的是第二个命令——或者,好吧,一个词太危险了:第二个命令可能会干扰你自己的工作。跑步总是安全的git fetch1 学习如何阅读git fetch印刷品——这有点令人困惑,但这一切都是有意义的。详情见下文。

运行的第二个命令git fetch默认为git merge. 了解做什么git merge,以及它如何影响您当前的工作树。了解其选项:--ff-only--no-ff等。

您可以选择将其作为第二个命令git pull运行。git rebase了解做什么git rebase,以及它如何影响您当前的工作树,以及您在此过程中变基的提交。了解它的选项,例如-i交互式变基。了解这git rebase实际上就像一系列重复的git cherry-pick命令,2使用 Git 的分离 HEAD模式。3

然后,在运行git fetch并查看做了什么git fetch之后,决定是否要查看进来的提交,以及是否要合并——有或没有--ff-only--no-ff——或者是否需要变基。如果你愿意,你现在有空间和时间来运行git log,还有空间和时间来决定变基还是合并。

一旦您非常熟悉所有这些元素,并且非常确定您将git fetch来自的任何存储库中发生了什么,那么提前选择合并与变基并使用便捷git pull的快捷方式运行git fetch然后立即运行第二个就变得安全了命令也。


1如果您摆弄内部git fetch设置,您可能会使其变得不安全。把它想象成重新连接你的电灯开关,这样开关附近的墙上就有一个带电板。如果您不破坏您的设置,git fetch将是安全的。如果你这样做,那是你的。

2一种变基确实使用git cherry-pick. 另一种用途git format-patchgit apply而不是。这第二种 rebase 在大多数方面都没有那么好,但确实运行得更快,所以它曾经是默认的——但 Git 2.26 即将切换默认以使用cherry-pick 方法。

3一个变基可能会在中间停止,需要你的帮助来修复它。发生这种情况时,Git 将继续处于这种分离的 HEAD 模式。因此,您应该了解什么是分离 HEAD 模式。


git fetch做什么

请记住,Git 确实是关于提交的。每个提交都包含所有文件的完整快照。而且,每个提交都有一些元数据:关于提交的一些信息,这不是提交数据的一部分,但 Git 也希望或需要为您保留。每个提交都有一个唯一的哈希 ID:一个大而丑陋的字母和数字字符串,4而不是一个简单的数字(1、2、3,...)。

提交哈希 ID 的真正魔力——以及它必须如此庞大和丑陋的原因——在于它在任何地方的每个Git 存储库中都是独一无二的。请记住,您不仅拥有自己的Git 存储库,或者可能拥有多个 Git 存储库,其他所有人也都有自己的存储库。另一个 Git 在origin. 那是您克隆的存储库,当您运行时:

git clone <url>

您的 Git 创建了一个新的空存储库,将您提供的 URL 粘贴在 name 下origin,然后在该 URL 处调用另一个 Git。然后,另一个 Git 向您的 Git 提供它的提交,告诉您的 Git它的分支名称(以及标签名称和其他名称,但我们将在这里专注于分支名称)。你的 Git 让他们的 Git 给你他们所有的提交,你的 Git 将所有这些提交放入的所有提交的数据库中。然后你的 Git 获取他们的分支名称并复制它们来制作你的远程跟踪名称。

他们master成了你的origin/master。他们的develop,如果他们有的话,就变成了你的origin/develop。这里的模式很简单:对于他们拥有的每个分支,你的 Git 都会创建一个origin/-prefixed 变体。以这个名字,你的 Git 将他们的Git 说的与他们的分支名称一起使用的哈希 ID 隐藏起来。

简而言之,您的远程跟踪名称会记住它们的分支名称。

但是您在几秒钟前克隆了那个 Git 存储库。甚至可能是几分钟、几小时,或者(恐怖的)几天或几周。从那以后发生了很多事情!(在某些情况下,一个活动的存储库每隔几秒就可以获得数十次提交。)

所以,要从他们那里获取的东西,你运行:

git fetch origin

(或者只是git fetch,通常默认使用origin反正)。你的 Git 会调用他们的 Git,就像之前一样。它让他们像以前一样列出他们的分支名称(以及标签和其他名称)。他们master现在可能不一样了!如果是这样,他们可能有你的 Git 没有的提交。你的 Git 现在从他们的 Git 中获得了他们拥有的提交,而你没有。5 您的 Git 将这些添加到您的收藏中,现在您拥有了它们 - 现在您的 Git 更新了您的远程跟踪名称

我有一个官方 Linux 内核的克隆(嗯,几个之一),我倾向于非常零星地更新它,在这里的一个方便的测试机器上。现在让我们运行git fetch它并观察:

$ git fetch
remote: Enumerating objects: 243814, done.
remote: Counting objects: 100% (243814/243814), done.
remote: Compressing objects: 100% (45189/45189), done.
remote: Total 243814 (delta 198891), reused 242574 (delta 198039)
Receiving objects: 100% (243814/243814), 76.86 MiB | 4.94 MiB/s, done.
Resolving deltas: 100% (198891/198891), completed with 12626 local objects.
From git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux
   0f1a7b3fac05..5ad0ec0b8652  master     -> origin/master
 * [new tag]                   [snip lots of new tags]

我们在这里看到一堆消息。前几个以remote:. 这些实际上是他们的Git 生成的消息,我们的 Git 只是复制这些消息,以便我们可以看到它们。他们谈论枚举、计数和压缩对象。这些对象是 Git 用于其每个数据库对象的内部表示:提交、树、blob 和带注释的标签。每次提交都使用一定数量的树和 blob 来保存它的文件,在一个永久冻结的快照中。提交可以共享树和 blob 以节省空间:如果提交重用了之前或之后的提交中的文件或完整的文件树,它们只是共享它们的树和/或 blob。

计数步骤计算他们有多少这些对象,我没有,他们将要发送。一般来说,他们不会发送我有的6 所以这个计数发生在许多提交及其文件,以及一些带注释的标签(new tag上面的东西)上,它计算了近 25 万个对象。

然后,他们压缩了其中一些对象——在这种情况下,大约 20% 的对象(通常占网络传输总量的更多百分比),并将它们发送到我的 Git。我们现在摆脱了remote:-prefixed 行,我的 Git 重建了原始对象并将它们添加到我的存储库中。

最后,我的 Git 更新了我的远程跟踪名称,origin/master基于他们的master. 这是非常重要的一行:

   0f1a7b3fac05..5ad0ec0b8652  master     -> origin/master

这里的两个哈希 ID 是:

  • 0f1a7b3fac05:我在更新之前origin/master持有这个哈希 ID ;git fetch
  • 5ad0ec0b8652:我更新origin/master持有这个哈希 ID 。git fetch

在此之后,我们看到他们的分支名称,master然后是这个 ASCII 箭头->,然后是我的远程跟踪名称,origin/master

这告诉我我origin/master已经调整为添加新的提交,而不丢弃任何现有的提交。如果我想知道我刚刚获得了多少次提交,我现在可以计算它们:

$ git rev-list --count 0f1a7b3fac05..5ad0ec0b8652
31244

因此,自从我上次运行以来git fetch,我刚刚又收到了 31,244 次提交,全部在他们的master/my上origin/master

(我还为内核版本 5.4 和 5.5 以及即将推出的 5.6 挑选了一堆新标签。)

所有这一切的简短版本是,您可以在git fetch此处获取输出并将两个哈希 ID 剪切并粘贴到 agit loggit rev-list --count查看提交,或查看有多少提交。

这个特定的存储库没有git push --force应用到它们的分支。但是,Git 本身的 Git 存储库可以。我在这里的那个副本是最新的,所以我无法显示实际git fetch输出,但它看起来像这样:

 + old...new  pu     -> origin/pu (forced update)

除了两个哈希 ID 不同(在这种情况下是假的)之外,这个输出有三个不同的地方:

  1. 前面有a +,表示强制更新;
  2. 两个哈希ID之间有三个点,表示强制更新;和
  3. 它以 结尾(forced update),表示强制更新。

这三个指标没有区别。我的Git 让我知道这是一个强制更新,这非常重要,也就是说,我现在在我的存储库中有一些提交,剩下的,他们决定应该被丢弃。如果我以某种方式使用他们的分支——我的分支——,我应该意识到这一点。puorigin/pu

Git 中的pu(Proposed Update 或 PickUp)分支是所有 Git 开发人员和其他支持者都同意定期重新定位或重新构建的分支。分支中的提交可能会被新的和改进的提交替换,或者完全丢弃;我们都同意我们会处理这个问题,而不是抱怨它。


4从技术上讲,哈希 ID 只是一个位或字节字符串。Git 目前使用 SHA-1 哈希值,长度为 160 位,然后将这些哈希值表示为 40 个字符的十六进制数字。Git 正逐渐用 256 位长的 SHA-2 散列代替这些。

5请注意,在此之前您可能已将自己的提交添加到您的存储库中。这就是 Git 不能使用简单序列号的原因。假设您从他们那里获得了 1000 个提交,编号为 1 到 1000。然后您自己进行了两次提交,编号为 1001 和 1002。现在您调用他们的 Git,他们有三个新的提交,编号为 1001 到 1003。我们该怎么做这些重叠的数字?

实际上可以保留一些本地号码。与 Git 一样强大的 Mercurial 就是这样做的。本地号码有时很方便——但最终它们并不像人们想的那样有用。Git 只是不理会它们。

6这种普遍性提出了很多假设。Git 在完全精确与用最少的信息快速完成这件事之间进行权衡。如果你的 Git 和他们的 Git 有更长的对话,这将占用更多的网络带宽,他们的 Git 有时可能会省略一些对象,这将占用更少的网络带宽。


为什么需要第二个 Git 命令

git fetch获得新的提交时,它们就在你的存储库数据库中。你不能直接看到这些。您可以git log在它们上运行,或者git show. 您可以将找到的提交哈希 ID 与远程跟踪名称一起使用。但总的来说,您必须对这些提交做一些事情才能获取文件。

如果您做出自己的承诺,那么您现在需要做的事情通常是将他们的工作以某种形式融入我的工作中。执行此操作的两个主要 Git 命令是git merge,它将他们的工作合并git rebase到您的工作中,以及,它将您的原始提交复制到新的和改进的提交中,其中改进(至少,您希望这是一种改进)是将这些提交 他们的新提交之后,而不是在他们的旁边。

这两个命令都会影响您的分支。

Fetch 不会:git fetch让您获得新的提交,但您现有的提交完全不受添加提交的影响。新的有不同的大而丑陋的哈希 ID。每个新提交都具有与其他所有提交不同的哈希 ID。提交的数量不小;没有人会提交 #3,因为提交 #3 永远不存在。git fetch所做的只是添加新的提交并更新您的远程跟踪名称。

但是两者都git merge需要git rebase使用你的work-tree。你的工作树或工作树(Git 现在主要使用长期)是 Git提取提交快照的地方,这样你就可以看到它们并处理/使用它们——因此得名工作树:它是你工作的地方. 合并操作通常需要更改您的工作树。rebase 操作重复运行git cherry-picks,每一个经常需要改变你的工作树。两者git mergegit cherry-pick都可能有合并冲突,你必须解决:这意味着 Git 无法自行解决。在这种情况下,Git 会让你的工作树变得一团糟,你必须修复它。

在这两种情况下,合并或变基的最终结果都可以更改哪个提交是分支中最后一个提交。他们总是与您当前的分支合作。所以这两个命令需要非常小心地使用,并且首先运行总是明智的,以确保您当前的分支是(a)正确的分支并且(b)准备好进行合并或变基操作。git status

随意使用git pull,只要知道它将(1)运行git fetch,然后(2)运行这些其他命令之一。您将没有机会查看输出并根据git fetch输出做出决定:您已提前承诺立即执行git mergegit rebase. 7 这从根本上没有错,但我更愿意将这两个操作分开。

(我有一个别名,git mff运行git merge --ff-only,这是我经常在之后使用git fetch的。结合对 fetch 输出和当前状态的简要浏览,通常是想要的,如果它不起作用,那么我想更仔细地观察一切。)


7有一些不寻常的情况,第二部分git pull既不是合并也不是变基。例如,git pull进入一个完全空的存储库需要做 agit checkout而不是,所以它就这样做了。在古老的 Git(1.5.something 或 1.6.something)中,这让我失去了一两天的工作,一次,因为它没有仔细编码并且我还没有提交。这是我避免的原因之一git pull:它不止一次地烧伤了我,而那个特殊情况是最糟糕的。


推荐阅读