在 Git 中查找第一个非本地提交。

3

相关链接:列出尚未推送到远程的Git提交记录

git rev-parse HEAD 可以给出工作区中最新的提交记录,但它可能是一个本地提交的 Git哈希值。也就是说,这个提交记录尚未被推送到远程仓库。

如何查找最新的提交记录,并且在工作区和远程仓库中都存在呢?

2个回答

6

获取当前所在分支配置的远程分支的最新提交,执行以下操作:

# 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} 不是占位符,需要按照原样输入)

文档中得知:

[<分支名>]@{upstream},例如 master@{upstream},@{u}
分支名后缀 @{upstream}(简写为 @{u})指的是该分支构建在哪个分支之上(通过 branch..remote 和 branch..merge 进行配置)。如果省略分支名,则默认使用当前分支。


3

技术上,git rev-parse HEAD 给出了当前提交的哈希 ID。这不一定是最新的,并且即使在正常使用中,它也可能与工作树中的内容不匹配(因为工作树可以被修改但尚未提交)。这些要点会干扰回答您的问题:也许您并不想要最新的提交。此外,您可以通过 git push 推送到某个远程存储库的提交通常不在任何工作树中,因为这些远程存储库通常是裸存储库:裸存储库通常接受 git push 请求,而非裸存储库则不接受

所有这些都不重要,你可能只需要一个简单的:

git rev-parse origin/master

或者:

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

或者:

git rev-parse @{upstream}

这些内容中的最后一个需要进一步解释。前两个只是使用您现有的名称,在您现有的Git存储库中,以与git rev-parse HEAD相同的方式找到哈希ID,尽管通常较少复杂。

有可能您的本地Git存储库与其他(远程)Git存储库不同步。在这种情况下,您可能需要运行:

git fetch origin

首先要获取开发者的任何新提交并更新各种远程跟踪名称,例如 origin/masterorigin/develop 等名称。

这里发生了什么

Git 将一个分支名定义为一种名称,例如 mastermaindevelopfeature/tall 或其他持有此存储库中某个现有有效提交的哈希 ID 的名称。1 此哈希 ID 定义上是该分支上的最后提交。

Git 处理这个问题本身有点复杂,但如果我们注意到大多数提交——所有的普通的提交2——都为其直接父提交存储了恰好一个哈希 ID,我们就可以像在一条字符串上放置珠子或玻璃珠一样依次放置提交:

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

这里,H 代表某个现有提交的哈希 ID。该提交存储其父(较早)提交 G 的哈希 ID。提交 G 再次存储更早提交 F 的哈希 ID,以此类推。
由于提交无法更改,且哈希 ID 不可预测,这些箭头总是指向后面。然后,分支名称 只指向链中最后一个提交。
...--G--H   <-- main

此外,Git会设置一些东西,以便在使用git checkoutgit switch选择某个分支名称作为当前分支时,特殊名称HEAD附加到分支名称上:
...--G--H   <-- main (HEAD)

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

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

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

作为git commit最后一步骤,Git会将新提交的哈希ID写入HEAD所附加的任何名称中,从而得到:
...--G--H
         \
          I   <-- main (HEAD)

名称HEAD仍然附加在名称main上,但是名称main现在表示提交I是该分支上的最后一次提交。
然而,Git确实有一个称为detached HEAD模式的模式。 在这里,我们告诉Git选择某个提交,而不是分支名称。 例如,我们可能希望查看提交G的快照,因此运行git checkout hash-of-G或类似命令。结果是:
...--G   <-- HEAD
      \
       H--I   <-- main
git rev-parse HEAD命令现在显示提交G的哈希ID:不是提交H的哈希ID,也不是提交I的哈希ID,而是提交H的哈希ID。这是因为HEAD不再附加(到分支名称),而是分离(意味着HEAD直接包含提交的哈希ID)。
(要返回提交I并进入main分支,我们可以使用git checkout maingit switch main。这将重新附加HEAD。)

1除非您的存储库已损坏,否则不存在现有但无效的提交。这里的重点是强调,尽管哈希 ID 看起来像随机垃圾,但您不能随意制作一个。实际上,它们是通过运行加密校验和生成的大型数字的十六进制表示形式,因此它们根本不是随机的。

2这里的“普通提交”定义为存储一个父哈希 ID。 “合并提交”的定义是存储两个或多个父哈希 ID 的提交,而 Git 有第三种提交类型,“根提交”,它不存储任何父哈希 ID。根提交 - 在非空、非浅层存储库中始终至少有一个 - 通常是某人为该存储库制作的第一个提交。可能会出现更多的根提交 - 出于错误或故意 - 但很少有好的理由这样做;它只是 Git 使用的图形算法的副产品。

为了使Git像这样工作,它会在每个提交中插入一些独特的内容。具体而言,每个提交都有一个时间戳,在正常使用中很难预测某个未来提交的未来时间戳。虽然在理论上可能会出现问题,但至少今天来说,这些问题是不切实际的,即使用作恶作剧也是如此。

更新名称,包括使用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发送到位于 origin 的其他 Git 存储库中:

git push origin develop

从你们的角度来看,他们需要在他们的代码库中设置一些分支名称。按照惯例(因为人类很容易被愚弄),我们倾向于在他们的代码库和我们的代码库中使用相同的分支名称。因此,上述git push命令要求他们设置他们的develop分支。
如果develop是一个新的名称那就没问题,如果我们不会丢失他们的提交记录,我们也可以要求他们设置他们的main分支。也就是说,假设他们有:
...--G--H   <-- main

我们可以要求他们将其main设置为指向一些新的提交J,只要J最终指向H(可能通过I)即可:
...--G--H   <-- main
         \
          I--J   <-- request: please make "main" go here

Git将这种请求称为“快进操作”,通常允许它。 (许多附加站点(如GitHub)添加了更高级的分支保护系统,可以让您更加挑剔;但是这个快进检查是基本Git中内置的全部内容。)但是基本Git不允许您执行以下操作:
...--G--H--I   <-- main
         \
          J   <-- request: please make "main" go here

因为如果他们这样做,他们将失去对其提交代码“ I”的访问权限。
远程跟踪名称和git fetch
为了解决这种问题,我们应该在运行git push之前先使用git fetch。当我们运行git fetch时,我们的Git会调用他们的Git,就像对于git push一样,我们会向他们发送我们的新提交,但是我们的Git不会将提交发送给他们,而是让我们的Git要求他们的Git提供任何我们没有的新提交。他们发送这些内容以及有关他们的分支名称指向哪些提交的信息,我们的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时,我们会获取到他们的新提交:
          I   <-- (main in their Git)
         /
...--G--H
         \
          J   <-- main

我们的 Git 无法更新我们的 main,因为如果它这样做了,我们就会失去我们自己的提交 J。所以,我们的 Git 会将他们的分支名称 main 改变成一个远程跟踪名称,无论他们是否添加了任何新的提交。我们的 Git 在他们的分支名称前加上 origin/,这样我们最终得到的是这样的结果:
          I   <-- origin/main
         /
...--G--H
         \
          J   <-- main

注意:如果我们当前已经检出 main 分支,那么将 HEAD 添加到其中。

这个 git fetch 步骤:

  • 获取他们拥有的任何新提交;
  • 更新所有我们的远程跟踪分支名称;因此
  • 准备好做任何必要的操作以连接新的提交行(rebase或merge)。

这意味着通常跟随 git fetch 的是使用 git rebasegit merge 命令。Git 提供了一个方便的命令 git pull,它可以组合两个操作。由于许多原因,我不喜欢这个命令,并鼓励那些刚开始使用 Git 的人至少在熟悉整个过程之前使用单独的 fetch 和第二个 Git 命令序列。5

无论如何,总结一下,远程跟踪名是 Git 记住另一个 Git 仓库在其分支名称中的内容的方式,上次我们的 Git 和他们的 Git 交流时。 git fetch 操作通常会更新它们所有,而 git push 操作在成功推送到一个分支时会更新一个。我们的 Git 从他们的 Git 得到确认,他们接受了我们的请求,因此我们的 Git 现在知道他们的 Git 已将该名称设置为该哈希 ID。6

4从技术上讲,远程跟踪名称位于单独的名称空间中,因此即使我们意外地调用了一个(本地)分支origin / xyz ,Git 也能够区分我们基于其xyz分支的origin/xyz 分支与我们的origin/xyz远程跟踪名称。但这涉及到愚蠢的人类技巧,会让机器人Bender发笑;不要这样做。

5并非所有人都对git pull如此谨慎。我对它的厌恶部分原因是早期它存在一些非常严重的错误,我曾多次因此失去了很多工作。但主要问题在于,在我看来它做得太多了。目前有一股运动正在兴起,旨在通过默认设置使git pull更好地运行,尽管我不确定这是否会很快发生。如果真的发生了,我仍然会建议分步操作,但不会那么快地建议新手避免使用git pull:如果它能正常工作,它就是正确的,否则就没有单一正确的方法。

6一些自动获取时间更新是在Git 1.8.4中新增的,因此,如果您使用的Git版本比这还古老,请务必使用git fetch origin而没有任何约束来更新所有内容。在这些古老的Git版本中,git fetch(另一个对git fetch持怀疑态度的原因)经常无法更新任何内容。


分支、上游和@{upstream}

每个分支名称都允许但不需要有一个(1)上游设置。通常,像maindevelop这样的分支的上游设置被设置为origin/mainorigin/develop:在你自己的Git仓库中的远程跟踪名称

有了这个设置,可以使用一些方便的项目。它实际上从来没有被要求过。当你在自己的仓库中创建一个全新的分支名称时,不使用远程跟踪名称(因为origin Git还没有这个分支),它还没有上游,你将需要使用git push -u origin HEAD或类似命令在那里创建该分支。这将在本地创建适当的远程跟踪名称,并且-u会使你的Git将远程跟踪名称设置为该分支的上游。

一旦您设置了上游,技术上您可以将@{upstream}后缀附加到任何分支名称上,告诉Git查找该分支的上游。也就是说,假设您已将master的上游设置为默认的origin/master,那么master@{upstream}就是origin/master。每个分支名称都是如此。裸的@{upstream}文本就像这样写,意味着HEAD@{upstream}。因此,它使用HEAD来确定您所在的分支,然后使用该分支的上游设置来确定要在您自己的本地Git存储库中使用哪个远程跟踪名称。以上所有内容解释了RomainValeri's answer为什么是这个问题的简短版本以及如何实现。 :-)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接