这三个命令序列在git中是等效的吗?

时间:2017-12-14 23:41:21

标签: git

我很好奇以下内容是否相同

让我们说最新的master有头C,我的分支基于C并且有一个提交D

origin/master | A -> B -> C
                           \
          foo |             D

然后让我们说master更改。

origin/master | A -> B -> C -> E
                           \
          foo |             D

我好奇是否全部

  • git pull origin/master
  • git fetch && git merge origin/master
  • git reset --soft HEAD~ && git stash save && git fetch && git reset --hard origin/master && git stash pop
预计

是等效的,并且git为每个算法运行的算法在逻辑上是否相等。

1 个答案:

答案 0 :(得分:3)

所有箭头和分支标签都具有误导性,因为它们都非常明智。然而,Git向后工作。 :-)让我们用另一种方式画出它们,就像Git那样:

A <-B <-C <-E   <-- origin/master
         \
          D   <-- foo

即,名称origin/master包含提交E的哈希ID。提交E包含提交C的哈希ID,其中包含提交B的哈希ID,其中包含提交A的哈希ID。提交A没有其他哈希ID,因为它是第一个提交而无法拥有父级,因此它无处可指。

所有&#34;室内&#34;箭头指向后方。他们必须这样做,因为像所有Git对象一样,提交只有在创建后才是只读的。我们知道,当我们创建一个新的提交时,它的哈希ID是什么,但我们不知道,当我们创建它时,它的孩子或孩子会是什么,是否以及何时创建它们。结果,没有必要自己绘制父箭;我们可以连接提交,记住它们指向后方。

另一方面,分支名称一直在移动。所以保留分支名称箭头是个好主意。让我们添加名称master和箭头,并注意master也是当前分支(HEAD)

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

git pull origin/master

这不是一个非常有效的Git命令。实际命令是特殊拼写的git pull origin master

如果您是Git的新手,我建议完全避免 git pull一段时间。我认为这主要是让人困惑。所有它真正做的是为你运行另外两个Git命令:git fetch(传递你给它的其余参数,如果有的话),或者它从当前分支中提取的远程名称和分支名称,如果没有),然后是(通常)git merge

一旦您熟悉其他Git命令并知道对它们的期望,您可以开始使用git pull作为一种方便,只要您觉得它很方便(有时它是,有时它是's}不是)。

让我们看一下/ git fetchgit fetch做的是调用另一个Git并询问它的分支和标签。

这第二个Git有自己独立的master。你的Git通过他们的 master找出他们的Git识别的提交哈希值。然后,您的Git通过其哈希ID获取该提交。哈希ID是&#34;真名称&#34;提交 - 像master这样的名称只是一个可移动的指针,包含一个哈希ID,以及您的master或其master所具有的哈希ID随时间的变化。 / p>

如果他们的master名称提交E,并且您已经拥有提交E,则您的Git不必下载提交E。你的Git只是改变你自己的origin/master指向提交E(如果它已经指向那里就没有变化)。

如果你还没有提交E,你的Git会从他们的Git中获取它。 (与提交E一起,您的Git会获得您所拥有的所有内容,例如您已经拥有的内容,例如提交CB和/或{ {1}}和/或所有可能需要的树和blob对象。你们通常会拥有大部分这些,但无论你有什么,它们都会打包并发送给你,这样你的Git可以设置您的A。)

如果他们的origin/master提名其他提交(masterA中的任何提交,或者某些提交你还没有提交),那么您的Git会下载所需的任何内容它具有该提交及其所有辅助数据和其他可达提交,然后通过其哈希ID使您的D指向该提交。我现在假设他们的origin/master仍然指向master

这是E所有工作的结束:它获取各种对象,然后更新远程跟踪名称(您的git fetch名称)。嗯,它还有一件具有历史意义的事情:它将所有提取的名称写入origin/*。如果您运行.git/FETCH_HEAD,它将默认从git fetch获取所有分支和标记名称;如果您运行origin,则告诉它只从您调用git fetch origin master的其他Git中获取一个名称,即匹配master(因此为分支主控)的名称。

  

origin

运行git fetch && git merge origin/master后,git fetch origin master实际上会运行git pull origin master。它通过特殊的git merge origin/master文件执行,而不是按字面运行FETCH_HEAD - 但git merge origin/mastergit pull origin master在这种情况下会执行相同的操作。

请注意,git fetch && git merge origin/master是不受限制的表单:更新所有远程跟踪名称。如果您目前不在自己的git fetch,或者您的master上游设置不同,master将会git pull,但git fetch origin some-other-name将明确运行git pull origin master。然后它将使用从git fetch origin master(以及git merge参数)中提取的哈希ID运行.git/FETCH_HEAD。所以这里有很多不同之处,但大多数都是次要的,假设您在上游设置为-m的{​​{1}}上。

master步骤有点复杂。这样:

  1. 检查索引和当前(HEAD)提交是否匹配,如果不匹配,检查合并是否安全。理想情况下它们应该匹配(如果没有,你应该运行origin/master)。如果索引与HEAD提交不匹配(尽管git merge将尽力而为),退出失败的合并是很棘手的。

  2. 使用当前提交的哈希ID和合并目标提交的哈希ID来定位两个特定提交。由于git commit名称git merge --abortHEAD指向master,因此当前提交为master,目标为C。 Git没有目标提交的单一一致名称;我喜欢为左/本地/ C调用HEAD提交 L ,为右/远程/ E调用另一个 R 。不过,它在这里不重要,正如我们稍后会看到的那样。

  3. 计算 L R 提交的合并基础。合并基础简单地说(在某些情况下有点简单),当我们从 L R 开始并向后工作时,两个分支合在一起的第一个位置。

    在这种情况下,提交 L (又名--ours)本身!

  4. 如果没有共同的祖先合并库提交,则失败(在现代Git中)。如果合并基础不是两个 L R 提交之一,请执行true合并。如果公共基础是 R ,则不执行任何操作:无需合并。如果合并基础为 L / --theirs,则在允许的情况下执行快进操作。如果不允许,请采用真正的合并。

  5. 由于合并基础是 L ,并且您没有说C,因此Git将使用此特定合并的快进操作。结果将是检出提交HEAD并将名称--no-ff移至指向E

    master

    最后:

      

    E

    这个要复杂得多。

    使用 E <-- master (HEAD), origin/master / A--B--C \ D <-- foo 进行软重置告诉Git:

    1. 通过阅读git reset --soft HEAD~ && git stash save && git fetch && git reset --hard origin/master && git stash pop查找当前哈希ID。这通常包含类似HEAD~1的字符串,它告诉Git当前分支是.git/HEAD。如果你在&#34;分离的HEAD&#34;模式,ref: refs/heads/master将包含原始哈希ID,而不是分支名称;这会影响下面的第4步。否则,请阅读分支名称本身以查找哈希ID。
    2. 读取该提交的父哈希ID(master表示.git/HEAD,这意味着&#34;一个父母沿着祖先的第一父行返回&#34;)。
    3. 请勿触摸索引(HEAD~),也不要触摸工作树(HEAD~1--soft)。
    4. 将新哈希写回当前分支。或者,如果HEAD已分离,请将新哈希直接写入--soft
    5. 由于我们没有触及索引和工作树,所以它们保持不变,无论我们是否在步骤4中都有要重写的分支名称。假设--mixed名称.git/HEAD,并且索引和工作树匹配提交HEADmaster指向),此软重置将更改名称C以指向提交master,保留索引和工作-tree匹配commit master的内容。

      接下来,B写入两个提交,而不是任何分支。一个包含索引的内容,一个包含工作树的内容。 (这两者相互匹配或者它们与提交C匹配并不重要 - 这只是意味着两个新提交使用来自提交{{1}的现有顶级树对象},这节省了空间。)结果图现在看起来像这样:

      git stash save

      (我调用C提交丛,C分为stash bag,因为它会挂起当前运行 E <-- origin/master / C--D <-- foo / A--B <-- master (HEAD) |\ i-w <-- refs/stash 时的提交。)

      i-w步骤现在可以执行任何操作,可能会添加更多提交和/或移动refs/stash指向某处。我们在此假设它git stash save指向提交git fetch

      origin/master现在将origin/master变为哈希ID。这是我们之前E中的第1步,但这次我们没有阅读git reset --hard origin/master,我们只读了origin/master的值:

      git reset

      请注意,我们也可以这样做来计算.git/HEAD

      origin/master

      任何时候,git rev-parse origin/master 都可以将名称转换为原始哈希ID,只要我们需要它。对于HEAD~1,这就是我们需要的:我们将重置为的提交是什么?

      git rev-parse HEAD~1 现在将哈希ID写入git rev-parse,这次,因为我们使用git reset,将提交的树写入索引并更新工作 - 树匹配。虽然索引和工作树不在图中,但我们现在有了这个:

      git reset

      (我们可以在这里横向绘制master行,或者回到--hard行,除了 E <-- master (HEAD), origin/master / C--D <-- foo / A--B |\ i-w <-- refs/stash 之外的一行。

      最后,A-B-C-D接受D提交中的任何内容并尝试使用refs/stash将其合并为提交git stash pop作为合并基础,当前索引变为 L 树的树 - 因为我们w提交git merge-recursiveB L - 并将保存的git reset --hard提交为 R 。这个合并可能,取决于自提交E以来发生的事情,看到没有工作要做,什么都不做。

      如果它什么也没做,或做某事并且认为合并成功,它就会丢弃存储:

      E

      不会进行任何新的提交,因此索引和/或工作树现在可能与提交w中的快照不同,如果合并确实有效。

      这里有许多重要的事情需要注意:

      • B确实是 E <-- master (HEAD), origin/master / C--D <-- foo / A--B ,然后是第二个Git命令。 E的语法是奇数,并且它运行的两个子命令中的任何一个都可能失败,尽管git pull的失败不太可能(除了停止拉动之外通常是无害的)。 git fetch期间的失败很常见,需要手动干预才能完成或中止合并。知道你在这里做了什么是个好主意,包括你是否需要git pull需要帮助;并且要知道,自己第一次git fetch自己运行是很好的。

      • git merge本身非常复杂。它可以执行快进,这根本不是合并(并且永远不会遇到合并冲突)。它什么都不做。或者,它可以执行真正的合并,这可能会因合并​​冲突而失败。要了解它将做什么,您必须找到合并基础,这需要查看提交图(git merge)。一些clicky Web界面,例如GitHub上的那些界面,隐藏你的图表,并且很难或不可能告诉你将会发生什么。

      • git merge内部也很复杂。当一切顺利时,它似乎很简单,但是当它失败时,它会失败得相当惊人。

      • git merge有太多的操作模式,使其易于使用。使用git log --graphgit stashgit reset,它可以单向运行,三个选项只是告诉它何时停止工作:移动当前分支后,或重置索引后,或者重置索引和工作树之后。使用其他选项,它可以采用另一种(不同的)方式。

      • 使用--soft来处理复杂的事情很棘手。它所做的只是make commit,所以如果你做了一些复杂的事情,只需做一个你可以看到和使用的提交。您可以稍后使用--mixed--hard {/ 1}}将其删除。