Git 找到第一个非本地提交

Git find first non-local commit

相关:List Git commits not pushed to the origin yet

git rev-parse HEAD 给我工作区中的最新提交,但这可以是本地提交的 githash。换句话说,尚未推送到远程的提交

如何找到工作区中远程也存在的最新提交

要获取当前签出分支的已配置远程分支的最新提交,请执行

# first get your remote-tracking branches up-to-date with remote
git fetch

# then do
git rev-parse @{upstream}
# or even just
git rev-parse @{u}

(注意:@{upstream} / @{u} 不是 占位符,它们应该按原样输入)

来自doc :

[<branchname>]@{upstream}, e.g. master@{upstream}, @{u}
The suffix @{upstream} to a branchname (short form @{u}) refers to the branch that the branch specified by branchname is set to build on top of (configured with branch..remote and branch..merge). A missing branchname defaults to the current one.

从技术上讲,git rev-parse HEAD 为您提供 当前 提交的哈希 ID。这不一定是 latest,即使在正常使用中它也不需要与工作树中的内容匹配(因为工作树可以修改但尚未提交)。这些要点也会干扰您按要求回答问题:也许您不想要 latest 提交。此外,您可以 git push 访问的某些远程存储库中的提交通常不在 任何 工作树中,因为此类远程存储库通常是 bare 存储库:.

除此之外,您可能想要的很简单:

git rev-parse origin/master

或:

git rev-parse origin/<some-other-name-here>

或:

git rev-parse @{upstream}

最后一项需要进一步解释。前两个简单地使用您现有的名称,在您现有的 Git 存储库中,以与 git rev-parse HEAD 相同的方式查找哈希 ID,尽管通常 less 复杂.

您的本地 Git 存储库可能 已过时 相对于另一个(远程) Git 存储库。在这种情况下,您可能需要 运行:

git fetch origin

首先是为了获得他们拥有的任何新提交,并更新您的各种 remote-tracking 名称: 名称如 origin/masterorigin/develop 等等。

这是怎么回事

Git 将 分支名称 定义为一个名称——如 mastermain,或 develop,或 feature/tall 或其他什么——保存此存储库中某些现有有效提交的哈希 ID。1 根据定义,该哈希 ID 是 last 在该分支“上”提交。

Git 对此的处理本身有点复杂,但如果我们注意到大多数提交——所有 普通 2—为它们的直接 parent 提交准确存储 one 哈希 ID,我们发现我们可以将提交彼此相邻放置,如珍珠或串珠:

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

在这里,H 代表一些现有提交的哈希 ID。该提交存储其父(较早)提交的哈希 ID G。提交 G 依次存储 still-earlier 提交 F 的哈希 ID,依此类推。

因为无法更改提交,并且哈希 ID 不可预测,3 这些箭头始终指向后方。 分支名称然后指向链中的最后一次提交:

...--G--H   <-- main

此外,Git 进行设置,以便当您使用 git checkoutgit switch 时 select 一些分支名称作为 当前分支,特殊名称 HEAD 附加到 分支名称:

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

此时,git rev-parse maingit rev-parse HEAD都会产生相同的哈希ID,即提交H.

如果您添加一个 new 提交,Git 通过写出该提交的快照和元数据并使元数据包含 H 来构建新提交] 的哈希 ID,以便新提交 I 向后指向现有提交 H:

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

然后,作为 git commitlast 步骤,Git 将 新提交的哈希 ID 写入任何名称 HEAD 附加到 ,给出:

...--G--H
         \
          I   <-- main (HEAD)

名称 HEAD 仍然附加到名称 main,但名称 main 现在表示提交 I 最后一个 在分支上提交。

然而,

Git 确实有一种称为 detached HEAD 模式的模式。在这里,我们告诉 Git 到 select 通过分支名称以外的其他内容进行一些提交。例如,我们可能希望查看提交 G 的快照,因此 运行 git checkout <em>hash-of-G</em> 或类似的。结果是:

...--G   <-- HEAD
      \
       H--I   <-- main

git rev-parse HEAD 命令现在显示提交 G 的哈希 ID:不是提交 H 的哈希 ID,不是提交 I 的哈希 ID,而是提交 H。那是因为 HEAD 不再 attached (分支名称),而是 detached (意思是 HEAD 包含直接提交的哈希 ID)。

(要返回提交 I 并在分支 main 上,我们将使用 git checkout maingit switch main。这些将 re-attach HEAD.)


1除非您的存储库已损坏,否则不会存在现有但无效的提交。这里的想法是强调,尽管哈希 ID 看起来 像随机垃圾,但您不能随便编一个。它们实际上是 hexadecimal 由 运行 加密校验和生成的大量数字的表示,因此它们根本不是随机的。

2这里ordinary commit的定义是存储一个parent hash ID。 merge commit的定义是存储两个或多个parent hash ID的commit,而Git还有第三种commit,一个root commit,存储 no paret 哈希 ID。根提交——在 non-empty、non-shallow 存储库中总是至少有一个提交——通常是某人为该存储库所做的第一个提交。可能有意或无意地进行更多的 root 提交,但很少有 good 这样做的理由;它只是脱离了 Git 使用的图形算法。

3为了让它像这样工作,Git 在每个提交中添加了一些独特的东西。特别是,每个提交都有一个 time-stamp,并且在正常使用中很难预测某些 future[=]time-stamp 的 future[=]future 提交将是。有一些理论上的方法可以在这里引起问题,但即使用作恶作剧也不切实际,至少在今天是这样。


更新名称,包括 git push

分支 名称特定于每个Git 存储库。您的 Git 存储库包含 您的 分支名称。其他一些 Git 存储库有 自己的 分支名称。

当你创建一个分支名称时,你只需为它选择一些现有的提交:

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

可能会变成:

...--G--H   <-- develop (HEAD), main

您已经创建了新名称 develop 并为该名称选择了现有提交 H。如果您现在进行新提交 I,结果包括更改存储在 develop 中的哈希 ID,以生成:

...--G--H   <-- main
         \
          I   <-- develop (HEAD)

请注意,这次移动的是名称 develop,因为 HEAD 附加到 develop,而不是 main

任何可以直接访问存储库的人都可以随时创建或销毁分支名称,使用 git branch(可能 -D 删除),或 git checkout -bgit switch -c。他们还可以随时创建新的提交

不过,每次提交都会获得一个唯一哈希ID。一旦你创建了一些提交集,你就可以使用 git push 将这些提交 发送 到其他一些 Git。他们完全按原样获取整个提交(每个提交的完整快照和元数据),并且他们计算相同的加密校验和,因此他们为这些相同的提交分配与您的 Git 分配给它们相同的哈希 ID。

通过使用这个原则,两个 Git 实际上设法通过查看 哈希 ID 来找出谁提交了哪个。这就是 Git 存储库的分布式特性。魔术真的全在散列中。

但是有一个问题。正如您自己的 Git 使用某个分支名称找到您的 最新 提交一样,他们的 Git 使用 他们的 找到最新提交 他们的 分支名称。因此,如果您要将提交 I 发送到其他 Git 存储库,请在 origin 处发送:

git push origin develop

在你这边,他们 将不得不在 他们的 存储库中设置一些分支名称。按照惯例——because humans are so easily fooled——我们倾向于在他们的仓库和我们的仓库中使用相同的分支名称。所以上面的git push要求他们设置他们的develop.

如果 develop 是一个 new 名字就没问题。如果我们不打算 丢失他们的任何提交 ,我们也可以要求他们设置他们的 main。也就是说,假设他们有:

...--G--H   <-- main

我们可以要求他们将 main 设置为指向某个新提交 J,只要 J 最终指向 H(可能通过 I):

...--G--H   <-- main
         \
          I--J   <-- request: please make "main" go here

Git 将这种请求称为 fast-forward 操作 并且通常允许它。 (许多 add-on 网站,如 GitHub 添加了更高级的分支保护系统,让您更加挑剔;不过,此 fast-forward 检查是基础 Git 中内置的全部内容。)什么基础 Git 不会让你做的是这样的:

...--G--H--I   <-- main
         \
          J   <-- request: please make "main" go here

因为如果 他们 这样做,他们 将无法访问他们的提交 I

Remote-tracking 姓名和 git fetch

要解决此类问题,我们应该先使用 git fetch,然后再使用 运行 git push。当我们 运行 git fetch 时,我们的 Git 调用他们的 Git——就好像 git push,我们会向他们发送我们的新提交——但不是发送提交他们,我们Git向他们Git询问任何new-to-us 提交。他们发送这些——连同关于他们的哪个分支名称指向哪个提交的信息——我们的Git现在有任何新的提交他们有,那个我们不会。

让我们假设我们的 main 上都有 ...-G-H,并且 他们 从某个地方获得了一些新的提交 I。不过,与此同时,我们在 main 上添加了一个新提交 J *。所以我们的起点是一样的:

...--G--H   <-- main

但从那时起,他们 添加了 I:

          I   <-- (main in their Git)
         /
...--G--H

我们添加了J:

...--G--H
         \
          J   <-- main (in our Git)

当我们运行git fetch时,我们拾取thir 新提交:

          I   <-- (main in their Git)
         /
...--G--H
         \
          J   <-- main

我们的 Git 无法更新我们的 main 因为如果更新了,我们将丢失我们自己的提交 J. 所以我们的 Git 所做的——不管他们是否添加了任何 new 提交——是取他们的 branch 名称,main,并改变它。我们的 Git 将他们的 branch 名称变成 remote-tracking 名称,方法是在其前面加上 origin/4 所以我们最终得到:

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

(注意:如果我们将 HEAD 作为当前的 checked-out 分支,请将 HEAD 添加到我们的 main 中。

git fetch 步骤:

  • 获取他们拥有的所有新提交;
  • 更新我们所有的remote-tracking名字;因此
  • 让我们准备好做任何必要的事情来加入新的提交行(变基或合并)。

这意味着在 git fetch 之后跟进 git rebasegit merge 通常是明智的。 Git 提供了一个方便的命令,git pull,它结合了这两个操作。由于多种原因,我不喜欢它,并鼓励 Git 的新手使用单独的 fetch 和 second-Git-command 序列,至少在他们非常熟悉整个过程之前。5

无论如何,所有这一切的总结是一个remote-tracking名称是Git记住其他Git存储库的方式在它的分支名称中,我们的 Git 最后一次与他们的 Git. 交谈 git fetch 操作倾向于更新所有这些,而 git push 操作在成功推送到一个分支时更新 one 。我们的 Git 从他们的 Git 那里得到确认他们接受了我们的请求,所以我们的 Git 现在知道他们的 Git 已经将那个名字设置为那个哈希 ID。6


4从技术上讲,remote-tracking 名称位于单独的 namespace 中,因此即使我们不小心调用了(本地)分支 origin/xyz, Git 将能够根据他们的 xyz 分支。但这可以追溯到愚蠢的人类把戏,这让机器人班德发笑;不要那样做。

5并不是每个人都对git pull如此谨慎。我不喜欢它的部分原因是它在早期有一些非常严重的错误,我不止一次因为 git pull 而失去了很多工作。但在我看来,主要问题是它做的太多了。正在采取行动使 git pull 在默认情况下表现得更好,尽管我不确定这在短期内发生的可能性有多大。如果它真的发生了,我仍然会推荐单独的步骤,但不会很快建议新手 避免 git pull:这将是一个命令,如果它有效,它就做了正确的事情,如果没有,那就没有一件正确的事情。

6一些自动 fetch-time 更新是 Git 1.8.4 中的新功能,所以如果你有一个非常古老的 Git ,比这更早,请务必使用 git fetch origin 无限制地更新所有内容。在这些古老的 Git 版本中,git pull 运行 的 git fetch 通常根本无法更新任何内容(另一个要警惕 git fetch 的原因。


分支机构、上游和@{upstream}

允许但不要求每个分支名称具有一 (1) 个 upstream 设置。通常,maindevelop 等分支的上游设置设置为 origin/mainorigin/developremote-tracking name 在您自己的 Git 存储库中。

有了这个套装可以使用一些方便的物品。实际上从来 不需要 。而且,当您在自己的存储库中创建一个全新的分支名称时,不使用 remote-tracking 名称(该名称尚不存在,因为 origin Git 还没有此分支), 还没有它的上游,你会想使用git push -u origin HEAD或类似于create的分支。这将在本地创建适当的 remote-tracking 名称,并且 -u 将使您的 Git 将 remote-tracking 名称设置为分支的上游。

一旦你设置了一个上游,@{upstream} 后缀——从技术上讲,它是一个你可以附加到 any 分支名称的后缀——告诉 Git找到那个分支的 upstream。也就是说,master@{upstream}origin/master,假设您已将 master 的上游设置为默认值 origin/master。这对每个分支名称重复。

光秃秃的 @{upstream} 文字,就是这样写的,“意味着”HEAD@{upstream}。所以这使用 HEAD 来确定你 上的哪个分支,然后使用分支的上游设置来确定你自己使用哪个 remote-tracking 名称本地 Git 存储库。

以上就是为什么以及如何 是这个的简短版本。 :-)