首页 > 解决方案 > 不同步源和主

问题描述

我有一个 git repo,上面有两个文件master(例如,当我访问 GitHub.com 时)和一个本地计算机上的 git repo(origin),里面有很多很多文件。有几个文件origin不在 master 上,我已经对 README 进行了更改,但master我还没有在origin.

我想git push选择从源到主的脚本。

但是,我得到了错误

hint: Updates were rejected because the remote contains work that you do not have locally."

所以,我想要git pull我所拥有的,master但我很担心这样做,因为我不想失去我在当地拥有的所有工作。

我正在考虑将我所有的工作保存在origin其他地方,做到git pull原点,然后将工作移回原点并做git push <files>

还有其他不那么迂回的方法吗?

如果有的话,我宁愿从中删除所有内容,master因为那里实际上有两个文件,然后添加我需要的所有内容origin,但我不知道是否有方向地基于 git 的工作方式会没问题。

谢谢你!

标签: gitgithubversion-control

解决方案


这是我的建议:不要以git pull. 开始git fetch。使用git fetch和其他 Git 命令一段时间,直到你真正“了解”git fetch和其他 Git 命令做什么。 然后你就可以使用了git pull,如果你愿意的话,因为这git pull意味着:

  1. 运行git fetch;然后
  2. 通常运行第二个 Git 命令,git merge但您也可以选择git rebase

这是git pull一个方便的快捷方式,因为事实证明,在 之后git fetch,您经常需要一个git mergeor git rebase。但我经常喜欢在 之间git log添加一个或其他命令,而使用 时,你不能这样做。事实证明,便利命令是......不方便。git pull

新手遇到的更大问题git pull是它似乎是魔术。这使得人们无法思考到底发生了什么。大约 15 年前,当我第一次使用 Git 时,我曾经遇到过同样的问题。了解我可以分离git pullgit fetch+ 第二个命令是理解 Git 的关键之一。

要知道的事情

您需要了解的内容包括:

  • Git 不是关于文件,而是关于提交。因此,您需要查看您有哪些提交,以及其他 Git 存储库(例如 over on )有哪些提交。origin

  • 提交包含文件。事实上,每个提交都有每个文件的完整快照。它们以压缩的、只读的、仅限 Git 的格式存储,只有 Git 可以读取,并且几乎没有任何东西(甚至 Git 本身)可以覆盖。一旦文件保存在提交中,那么在该提交中它是完全安全的。 没有什么可以改变它。只要你还有那个commit,你也仍然有那个文件

  • 但这反过来意味着您可以查看和处理/使用的文件不在 Git 中。您看到和处理的文件是普通的读/写文件。由于 Git 提交中的文件不是普通文件,因此它们必须是不同的文件。他们就是这样。当您使用git checkout或 newfangledgit switch时,您会从某个分支中挑选出一些 Git 应该复制的提交。Git 找到该提交中的所有文件并将它们复制到您的工作区,Git 将其称为您的工作树工作树

一旦你牢牢掌握了这一点,你就需要了解更多的事情:

  • Git 做出新的提交,不是来自你工作树中的内容,而是来自GitGit 的 index中的内容。这个东西——“索引”——还有两个名字:它也被称为暂存区,有时——现在很少见,主要是在像缓存这样的标志--cached(为什么它有三个名字有点神秘,但是暂存区是三个名字中最新的,也是最具描述性的,所以很明显,至少部分原因是原来的名字不好。)

    您可以将 Git 的索引视为您提议的下一次提交git checkout当您第一次使用or签出某个特定提交时git switch,Git 会将整个提交复制索引/暂存区域。所以现在提议的下一次提交、当前提交和您的工作树都具有相同的文件。因此,每个文件都有三个副本:

    1. 当前提交中不可更改的、永久保存的副本,因为它在提交中,所以完全不受更改的影响;
    2. Git 索引中提出的下一个提交副本,您可以更改该副本;和
    3. 您可以在工作树中查看和编辑的副本。

    这意味着您在工作树中进行工作,然后git add在您更新的任何文件上运行。你可以在每个文件上运行git add .(或者您可以直接在刚刚更改的文件上运行它。这里所做的是将文件的更新的工作树副本复制回 Git 的 index。这会更改提议的下一次提交git add -ugit add

  • 当你运行git status

    • Git 将当前提交 ( HEAD) 与索引/暂存区域进行比较。这两者之间的任何区别是staged for commit,即,这就是和提议的下一次提交之间的区别。HEAD新的提交仍然是每个文件的完整快照,但只有一些文件被更改
    • 另外,Git 将索引/暂存区域与您的工作树进行比较。这两者之间的任何差异都是not staged for commit. 也就是说,您可以运行git add这些更改,以更新文件的建议下一个提交副本,这可能会更改早期HEAD-vs-index 差异的输出。
  • 当你运行时git commit,Git 只是打包当时Git 索引中的任何内容,并使用它来进行新的提交。

  • 像just 这样的分支名称master可以帮助 Git 找到一些特定的提交。当您启动 master并运行git commit时,Git:

    1. 将文件打包到其索引中;
    2. 添加您的姓名、电子邮件地址等,以对所有这些进行提交;
    3. 写出新的提交;和
    4. 使名称 master标识提交。

    新的提交链接回之前的最后一次提交。

提交的心理形象

这意味着我们可以画出正在发生的事情,我觉得这很有帮助。假设我们从一个只有几个提交的小型存储库开始,只有一个分支名称,master或者main,它标识此存储库中的最后一次提交:

... <-F <-G <-H   <--master

每个提交都有一个number,一个大而难看的十六进制数字,对于那个特定的提交来说是唯一的。我们称这个数字为哈希 ID。进行提交会为新提交分配一个哈希 ID。 在任何 Git 存储库(您的或其他任何人的)中,没有其他提交可以拥有哈希ID。 这就是为什么这个数字如此之大,如此丑陋,以至于没有人可以处理它。

因此,我没有处理数字,而是使用一个大写字母来代替数字。这是 branch 中最后一次提交H的哈希 ID 。提交具有每个文件的永久、安全保存的快照。它也有一些元数据——一些东西说做了它(如果你做了它),你什么时候做的,你的日志信息,等等。在该元数据中,Git 添加了早期commit的原始哈希 ID 。masterHG

我们说 commitH 指向较早的 commit G。CommitG当然有快照和元数据。元数据告诉我们谁做了G,什么时候,为什么——他们的日志消息——并在其中包含更早提交的哈希 ID F。所以G 指向 F.

当然, CommitF具有所有这些相同的东西,所以它指向一些更早的提交,等等。这在整个历史中一直重复——只不过是这些提交——直到我们得到第一个提交。它不能向后指向某个较早的提交,所以它只是没有,这就是 Git 知道历史在这里结束(呃,开始?)的方式。

请注意,在所有这些中,Git 是向后工作的。它必须这样做,因为分支名称会找到最后一次提交。最后一个提交找到一个更早的提交,它找到一个更早的提交,依此类推。

请注意,分支名称指向最后一次提交。然后,要添加一个的提交,我们只需要这个提交字符串:

...--F--G--H   <-- master

并创建一个I指向现有提交的新提交H

...--F--G--H
            \
             I

如果我们在执行此操作时分支上master,这会告诉 Git:现在我们有了新的 commit I,让名称master指向I,这意味着我们得到了:

...--F--G--H
            \
             I   <-- master

现在没有理由画I一条单独的线:

...--F--G--H--I   <-- master

意思是一样的。

git fetch

什么git fetch是调用其他 Git 并从其他 Git 获取提交。要调用其他 Git,我们需要一个 URL:,ssh://git@github.com/user/repo.githttps://github.com/user/repo.git,或其他任何东西。我们让 Git 为我们保存该 URL,名称为 ,这样origin我们就不必一直输入它。

然后我们运行:

git fetch origin

甚至:

git fetch origin master

这让我们的 Git通过保存在 name 下的 URL调用他们的originGit 。他们的 Git 为我们的 Git 列出了他们所有的分支(以及标签和其他东西)。1 我们的 Git 然后挑选出任何有趣的——它们都很有趣,除非我们使用第二种形式,我们说只关注它们master——并检查他们的 Git 以查看他们是否有任何我们想要的提交并且没有。

(这涉及使用提交编号,这就是为什么提交编号必须是唯一的。但这是另一个讨论的主题。)

一般来说,Git 是贪婪的:他们有任何我们没有的提交,我们想要!我们的 Git 使用 fetch 协议从他们那里获取他们所有的新提交。当然,我们所有提交,我们已经拥有,以及我们之前从他们那里得到的所有提交,我们已经拥有。我们的 Git 和他们的 Git 谈话以及我们的 Git 找到了一种只获取内容的好方法,即使我们已经拥有这些文件,即使在新提交中也避免重新下载任何文件。(也就是说,获取速度很快并且不会获取我们已经拥有的东西,即使在新的提交中也是如此。不过,这也是另一个讨论的话题。)

最后,一旦我们有了这些新的提交,我们的 Git 会更新一些名称,以帮助我们的 Git跟踪这些新的提交。这些是我们的远程跟踪名称,例如2的形式origin/master

假设,当我们进行 commitI时,他们获得了一些新的 commit J。因此,在我们做任何事情之前——进行提交运行git fetch——<em>我们在我们的存储库中拥有:

...--G--H   <-- master, origin/master

我们做出新的承诺并拥有:

...--G--H   <-- origin/master
         \
          I   <-- master

(现在有理由将Iandmaster放在单独的一行上,这样我们origin/master仍然可以指向H。)

如果我们现在运行git fetch,我们会发现他们有一个新的提交,我们将获得并绘制为J

          J   <-- origin/master
         /
...--G--H
         \
          I   <-- master

请注意,提交H都在这两个链上;它只是IJ那个分歧。我们也可以在这里画一条线,也可以画IJ同一条线上H:这次我只是选择origin/master一条单独的线,以强调提交的共享性质,包括H.

请注意,所有这些提交都是只读的,它们都不是我们的工作树文件。 如果我们I检查了提交,那很好:这意味着 commitI是我们当前的提交,并且 Git 的索引和我们的工作树匹配 commit I,或者正在为新的提交做准备,或者其他什么。 通过添加提交J到存储库git fetch对我们现在正在进行的任何工作都没有影响。git fetch是相当安全 的。我们可以随时做这部分。


1在具有大量分支和标签的存储库中,这可能会花费大量时间并涉及数兆字节的数据,仅用于列表,因此在现代 Git 中,git fetch origin master版本可以限制列表并加快速度。这对于大多数存储库来说无关紧要,它们大多只有十几个分支和几百或几千个标签;它是类似于 Chrome 的存储库,具有 50+ MiB 的分支和标签名称,这是一个问题。

2 Git 调用这些远程跟踪分支名称。它们是我们的 Git 对其 Git 分支名称的记忆。我发现分支这个词在这里是多余的,所以我只使用短语remote-tracking names。Git 有很多种名称:分支名称、标签名称、注释名称、refs/stash它在 期间临时使用的名称git bisect,等等。远程跟踪名称只是另一种名称。每个名称仅包含一个哈希 ID — 这就是它们的工作方式,无论它们是什么类型的名称。


这是与我们的工作树文件混淆的第二个命令

现在我们有了,说:

          J   <-- origin/master
         /
...--G--H
         \
          I   <-- master (HEAD)

我们可以运行第二个Git 命令,就像git pull会做的那样。第二命令会弄乱我们的文件——我们工作树中的文件。

无论我们选择git merge还是git rebase这两个命令都需要能够覆盖我们的工作树。所以现在我们当然需要运行git status并确保它说一切都是“干净的”。

我们选择的命令——merge vs rebase——以及它的作用,都是有趣的问题。不过,StackOverflow 上已经有很多关于它们的答案。知道git pull运行git fetch- 这是安全的 - 然后第二个命令在同样意义上“安全”,会让你走得很远。


推荐阅读