首页 > 解决方案 > 如何正确设置上游并将主分支中的更改反转为先前的提交状态?

问题描述

我对git不是很有经验,所以请多多包涵

我在运行命令的地方犯了一个错误

git branch branchname --set-upstream-to=master

我认为这会将我的分支与 master 同步,但是我以某种方式将我在分支中的所有更改都放到了 master 分支。

所以我的问题是如何将 master 上的所有文件恢复到之前提交的状态?以及如何正确同步以便我可以将更新的更改从 master 获取到我的分支?

标签: git

解决方案


首先,放松一点:只需将名为的分支的上游设置为. 分支的上游实际上并没有改变分支的任何内容。它所做的是更改一些报告,并启用一些快捷方式。git branch --set-upstream-to=target branchbranchtarget

如果您根据报告采取行动,那很重要。如果没有,它不会。

如果您使用快捷方式,那很重要。如果没有,它不会。

删除分支 B 的上游:

git branch --unset-upstream B

要将分支 B 的上游更改为origin/B

git branch --set-upstream-to=origin/B B

我认为这会将我的分支与 master 同步,但是我以某种方式将我在分支中的所有更改都放到了 master 分支。

同样,这取决于您运行的任何其他命令。的--set-upstream-to选项git branch除了设置上游没有做任何事情。有关上游是什么的讨论——它有什么好处——请参阅我对为什么我必须“git push --set-upstream origin <branch>”的回答?

所以我的问题是如何将 master 上的所有文件恢复到之前提交的状态?以及如何正确同步以便我可以将更新的更改从 master 获取到我的分支?

回答这个问题很难,因为我认为你是从一些错误的假设开始的。这很容易做到,因为 Git 的分支系统非常奇怪。如果您以前从未使用过任何其他版本控制系统,您可能不会觉得这很奇怪,但如果您使用过,Git 可能会让人大吃一惊。

人们喜欢把树枝看作是真实的、坚固的东西:有用,也许像巨石一样不动,也许像建筑物一样遮蔽,无论如何,坚固可靠。在其他版本控制系统中,它们往往这样的。在 Git 中,它们不是:它们是轻量级的、短暂的、流动的,并且在很大程度上,在很多方面几乎是无用的。他们主要为我们做件事,而且做得很好,但他们并没有做我们期望分支机构做的大部分其他事情。但在这里我说的是分支名称,比如master等等developGit 中的分支这个词实际上是模棱两可的:它并不总是表示分支名称。另一个意思扎实。

(如果您从未使用过任何其他版本控制系统,那么以上都没有多大意义,因为您不会有事先的期望。)

Git 是关于提交的

在这里暂时忘记分支。Git 与分支无关。我们当然会使用它们——它们会做一些有用的事情——但 Git 确实是关于提交的。提交是 Git 中的基本单元,也是你最需要了解的东西。那么让我们看看提交是什么以及做什么。

Git 提交包含数据(所有文件的快照)和元数据:有关提交的信息。这就是提交中的内容:数据和元数据;文件的快照,以及有关快照的一些信息,例如制作者和制作时间。提交保存更改。他们只保存快照以及元数据。

一旦提交,就不能更改任何提交。 任何提交的任何一部分都不能改变。它的所有文件都被永久冻结。这有很多有用的属性。例如,有些人反对 Git为每次提交创建每个文件的新快照这一事实。这不会使存储库变得非常胖,非常快吗?好吧,如果 Git 以愚蠢的方式做到了,它会的;但Git没有,所以它没有。

假设您有一百个文件。你昨天做了一个提交,其中包含所有 100 个文件。您只更改了两个,并且刚刚进行了新的提交。提交中的 98 个文件与之前提交中的文件匹配。好吧,我们刚刚说过每个提交都完全冻结了——所以你的新提交可以共享所有 98 个未更改的文件。它只需要对两个不同的文件进行快照。

每次提交中的冻结文件会被进一步压缩,因为它们是一种特殊的、只读的、仅限 Git 的形式。它们在这种形式下实际上是不可改变的:不仅仅是不能改变它们。Git 本身也不能。这对于存档非常有用,这意味着对您的一个问题的回答是微不足道的:

我怎样才能将所有文件恢复到之前提交时的状态

他们已经处于那个状态,在那个提交中。您需要做的就是使用之前的提交。但在我们开始之前,让我们完成处理提交中包含的内容。

每个提交都有一个唯一的编号。 一个 Git 提交有一个hash ID,一旦你提交了,这个 hash ID 将永远保留,表示提交。从某种意义上说,它甚至在你提交之前就被保留了。但是,提交的哈希 ID 是通过对所有提交的数据和元数据进行加密哈希来构造的,并且元数据包括您创建提交的确切秒数,因此我们必须提前知道您何时将创建它,以及您要放入其中的所有其他内容,以预测其哈希 ID。

各地的每个 Git 都同意该提交获得哈希 ID。这意味着如果您将两个 Git 相互连接,它们只需查看彼此的哈希 ID:如果您的 Git 有他们的哈希 ID H1,那么您的 Git 有他们的提交H1。如果他们有你的哈希 ID H2,他们就有你的提交H2。无论你在哪里有一个他们没有的哈希 ID,反之亦然,你有一个他们没有的提交,反之亦然。1 这使得交换提交非常有效:您的 Git 知道他们有哪些提交,反之亦然,只需查看一些哈希 ID。

最后但非常重要的一点是,每个提交都存储其直接祖先的哈希 ID (或多个哈希 ID):其父级(或父级,在合并提交的情况下)。请注意,这种联系只有一种方式,从孩子到父母。那是因为当孩子“出生”时——当我们做出新的提交时——我们知道我们想要使用哪个父母。但是当我们提交时,我们还不知道它作为子节点会有什么哈希 ID。而且,每次提交的每个部分都是完全、完全、100% 只读的,所以我们以后不能添加子哈希 ID。


1 Git 对其所有四种内部对象类型都使用哈希 ID,而不仅仅是提交,所以从技术上讲,这有点偏离。比较只是哈希 ID,而不是提交哈希 ID;你有对象,或者没有,如果你有哈希ID,或者没有。


让我总结一下

因此,Git 存储commits,其中:

  • 是您所有文件的快照,永远冻结,
  • 加上一些元数据,例如提交的人、时间、原因等,
  • 并且每一个都有一个唯一的“数字”(hash ID);和
  • 每个都通过存储父级的哈希 ID 指向其直接父级。

这些指向回链接意味着我们可以绘制提交。让我们从一个只有三个提交的小型存储库开始:

A <-B <-C

CommitC最后一次提交,也就是我们最近一次提交。它保存了较早提交的哈希 ID B,所以BC的 parent:C指向B. 同时B持有较早提交的哈希 ID AB指向A. 提交A是我们的第一个提交。没有更早的提交指向,所以它只是没有。

在这里,我们使用单个大写字母来代表提交。但实际的哈希 ID 又大又丑,人类无法使用。它们必须很大,这样您就可以在每次进行新提交时获得一个独特的、不同于其他所有提交哈希 ID 的 ID。那么我们如何记住提交C最后一次提交呢?

这就是分支名称出现的地方。在 Git 中,像这样的分支名称master只是保存最后一次提交的实际哈希 ID:

A--B--C   <-- master

(我已经停止将提交之间的内部箭头绘制为箭头,因为它太难了。请记住,箭头都指向后面;Git 向后工作。)

如果我们有多个分支名称,则每个分支名称仅包含一个哈希 ID。多个名称可以拥有相同的哈希 ID:

A--B--C   <-- master, develop

现在我们需要一种方法来知道我们希望 Git 使用哪个分支名称HEAD,因此我们将特殊名称附加到一个分支名称(一次只有一个)上,像这样全部大写:

A--B--C   <-- master (HEAD), develop

在这里,我们使用commit C,但 master. 如果我们现在git checkout develop,我们切换到提交——即C,我们不切换任何东西——但我们也切换到使用名称develop,这样我们就可以 develop

A--B--C   <-- master, develop (HEAD)

如果我们现在进行一个的提交,以通常的方式——我不会在这里描述——我们会得到一个带有一个新的大而丑陋的哈希 ID 的新提交。我们就叫它吧D。提交D获取我们所有文件的新快照,即使它只是重新使用其中的大部分C文件(请注意,如果我们将文件改回来,我们将重新使用旧提交的副本,所以也许D重新使用大部分C和重复使用AB用于最后几个文件)。它有自己的元数据,包括我们作为创建者(提交者)和编写者(作者)的名字,2并将 commitC的哈希 ID 存储为其父级。这很容易做到,因为分支名称 developC的哈希 ID 现在在里面。

提交D之后,我们现在有:

A--B--C   <-- master, develop (HEAD)
       \
        D

我们现在处于最后一步git commit,即:它只是将它刚刚获得的任何哈希 ID 写入当前分支名称,即HEAD附加的分支名称。这会导致分支名称移动

A--B--C   <-- master
       \
        D   <-- develop (HEAD)

如果我们现在git checkout master做一个新的提交,我们将得到一个新的提交E,它将指向C,并且名称master将移动:

        E   <-- master (HEAD)
       /
A--B--C
       \
        D   <-- develop

请注意,如果我们愿意,我们可以绘制提交DE与前三个提交在同一行。重要的是我们连接DCECC连接BA

图表提交是真实的,并且相当稳固——只要我们在不破坏任何提交的情况下弯曲它,图表就很灵活——但分支名称只是标签。我们可以随时告诉 Git:去掉commit的标签。developD 我们可以让它指向我们拥有的任何提交。我们甚至可以完全删除它,但如果我们这样做,我们将很难找到提交D——毕竟它的哈希 ID 看起来完全是随机的。

如果我们找不到D,Git 最终会删除它。(Git 有一些安全措施可以找到暂时放弃的提交,通常至少持续 30 天,以防万一。)所以提交不一定永远。但是一旦做出来,就无法改变。只要您仍然拥有D并且可以找到它的哈希 ID,您就拥有了它,包括它的所有快照文件。

请注意,此时我们可以通过两种方式找到提交A-B-C我们可以从 startmaster和 find开始,E并使用它来 findC然后然后BA;或者我们可以从 开始develop查找D并使用它来查找C然后B然后A。这意味着提交A-B-C两个分支上。(这使得 Git 与大多数版本控制系统非常不同。)如果我们删除name develop,并且 can't find D,我们仍然可以找到A-B-C,现在它们只在一个分支上。或者,我们可以添加一个新名称:

        E   <-- master (HEAD)
       /
A--B--C   <-- three
       \
        D   <-- develop

现在提交A-B-C三个分支上,包括一个名为three. CommitC是 的最后一次提交,并且是和three的一部分,但不是最后一次。masterdevelop

这就是当我说分支并没有多大意义时的意思。他们所做的是让我们找到最后一次提交,并从那里找到所有较早的提交。如果我们有另一种方法来查找这些提交,那么分支名称所做的唯一事情就是记住某个提交——比如——是那个分支C的尖端。


2作者提交者之间的这种分离允许 Git 使用电子邮件,其中有人只是通过电子邮件向您发送您应用的补丁。然后你是提交者,另一个人是作者。Linux 人在 Git 的早期就这样使用 Git,有时仍然如此。


你如何使用这一切

签出分支名称意味着选择该分支的提示提交作为当前提交,并选择该分支作为当前分支。Git 通过以下方式做到这一点:

  • 将特殊名称附加HEAD到分支机构名称;和
  • 从选定的提交中提取冻结的只读文件到一个您可以查看和处理它们的区域。

所以,如果你有:

...--F--J--K   <-- master
      \
       G--H--I   <-- branchname (HEAD)

并且您不小心做了一些事情来添加您不想要的提交branchname,例如添加 commit I,您可以强制 Git 将名称branchname指向现有的 commit H

...--F--J--K   <-- master
      \
       G--H   <-- branchname (HEAD)
           \
            I   [abandoned]

CommitI会持续一段时间——可能至少 30 天——但不可见;你不会看到的。一旦它挂了太久,维护垃圾收集器git gc最终会真正删除它,它真的会消失。3

为此,您必须告诉 Git 强制将名称重新设置branchname为指向 commitH而不是 commit I。有多个 Git 命令可以执行此操作,但您通常使用的主要命令是git reset. 这也会清除所有未提交的工作——嗯,这取决于你如何使用它——并且未提交的工作在 30 多天或任何几天都无法恢复,所以要非常小心git reset --hard

git reset --hard <hash-of-H>

将执行我们上面绘制的操作,将 commit设为H当前提交,并将名称branchname指向 commit H。提交I仍然存在,但很难再次找到。4


3这假设您没有让其他 GitI通过其哈希 ID 将提交复制到他们的存储库中,因此抓住它并将其保存在他们选择的某个名称下。如果您这样做了,他们稍后可以将提交引入I存储库。

4如果您确实决定要返回它,查找其哈希 ID 的常用方法是通过 Git 的reflogs,它会跟踪每个ref中的哈希 ID 。但这是另一个 StackOverflow 问题。


将更改放入另一个分支

以及如何正确同步以便我可以将更新的更改从 master 获取到我的分支?

请记住,分支并不是那么重要。它的承诺很重要。而且,提交是快照。它们不是变化!不过,您可以将提交转化为更改。选择任意两个相邻的快照(父快照和子快照)并询问 Git:父快照与子快照有什么区别? 那是:

...--G--H   <-- master (HEAD)

如果我们让 Git 提取快照GH放入两个临时工作区域,然后比较两个临时区域中的每个文件,我们将看到发生了什么变化。所以这就是git log -p, git show, 和git diff做的。该git log -p案例获取它显示的每个提交,因为它一次显示一个提交,并将其父级与其进行比较 - 然后继续显示父级,依此类推。该git show命令需要一个提交来显示,通过将其父级与其进行比较来显示它,然后停止。使用git diff,您可以给它任意两个提交哈希 ID;它提取两个提交,并比较它们。5 它不会查看中间的任何提交:您只需选择左侧和右侧,然后进行比较。如果您愿意,可以将您的第一次提交与最新提交进行比较。

两个非常有用的 Git 命令是:

  • git merge,它找到三个提交,并执行两个git diffs 然后合并更改;和
  • git cherry-pick,它实际上通过将提交与其父级进行比较来复制提交,以对其进行一组更改。6

何时使用git merge、何时使用、何时使用git cherry-pick以及是否使用git rebase——它本质上为你运行了一系列git cherry-pick命令——是另一个话题,而且是一个相当大且经常固执己见的话题。我不会在这里详细介绍这些细节。让我们展示一个真正的合并。当你有两个分支时会发生这种情况——这就是 Git 对分支这个词的歧义是一个问题的地方——看起来像这样:

          I--J   <-- branch1, merge-me (HEAD)
         /
...--G--H
         \
          K--L   <-- branch2

请注意,我们当前的分支名称merge-me指向 commit J,并branch2指向 commit L。我们现在可以运行了git merge branch2。Git 将J很容易找到提交——这是我们当前的提交——并且L很容易找到,因为名称branch2指向L. 然后 Git 会找到最好的共享提交,Git 称之为合并基础。在某些情况下不清楚,但在这里,最好的共享提交显然是提交H(无论它的实际哈希 ID 是什么):提交H两个分支上,并且是最新的这样一个,晚于也在两个分支上的所有其他提交.

现在git mergegit diff提交Hvs J,以查看我们分支上所做的更改。它还将git diff提交H- 再次合并基础 - vs commit L,以查看他们在分支上所做的更改。然后,Git 会结合这些变化。如果我们将快照H中的文件 F 更改为新版本的 F in J,Git 会接受我们的更改。如果他们还将快照H中的文件 F 更改为 上的新版本L,Git 也会接受他们的更改。如果可以,合并代码会合并更改,并将合并后的更改从快照应用到文件HF。

这对每个更改的文件重复。对于没有人更改任何内容的文件——文件 F2 inH匹配一个 inJ 一个 in L——Git 可以采用三个版本中的任何一个,因为它们都匹配。对于只有一侧改变了任何东西的文件,Git 可以捷径:它可以只取我们的,也可以只取他们的。

这两个 diff 使用--find-renames, 运行以查找重命名的文件,并且 diff 会自动查找添加或删除的文件。合并代码也或至少尽可能地结合了这些。

如果我们的更改和它们的更改重叠,但不完全相同,则合并代码将声明合并冲突。在这种情况下,Git 将保留文件的所有三个输入提交副本7 Git 还将尽最大努力将遇到问题的冲突标记与您的工作区结合起来。您的工作就是通过生成正确的合并结果来解决这些冲突。 (然后您必须自己继续/完成合并。)

但是,如果 Git 没有检测到任何冲突,Git 将继续从结果中进行新的提交。请记住,这是合并所有更改的结果,并将合并后的更改应用于合并库中的任何内容 —快照H,在我们的示例中:

          I--J   <-- branch1
         /    \
...--G--H      M   <-- merge-me (HEAD)
         \    /
          K--L   <-- branch2

新提交M是一个合并提交:它指向J,就像任何提交一样,但也指向L,另一个被合并的提交。(事实上​​,用作H合并基础的合并没有记录在任何地方。如果你问 Git合并基础是什么? Git 只需要再次弄清楚它,就像它之前发现它的方式一样M。)新的提交M导致当前分支前进指向它,就像新提交一样。提交M有一个快照——而不是更改——就像所有其他提交一样。唯一特别的M是它有两个父母,而不仅仅是一个。当 Git 通过提交向后工作时,一次一个,它必须M两个 J L. 8


5由于内部存储格式,Git 实际上不需要提取任何内容,直到两个文件真正匹配为止。一般来说,它不需要临时工作区,它只是在内存中完成所有这些工作。

6在内部,Git 实际上git cherry-pick使用与执行相同的三向合并来实现git merge,但将 commit-to-copy 的父级作为合并基础。之后,<code>git cherry-pick不再进行合并提交(具有两个父项的提交),而是进行普通的单父提交。但这解释了为什么 rebase 比 merge 更难:如果你 rebase 一个 5 次提交的链,你实际上是在做5次合并,而不是只有一次。

7这些在 Git 的index aka staging area中,我没有在这里介绍。

8这会产生一些有趣的问题,因为 Git 一次只运行一个。但同样,这些是针对其他帖子的。


推荐阅读