我怎样才能发现我隐藏的 git commit 哈希?

Rya*_*yan 4 git

我意识到我的问题与How to list the parent commit of a stash in `git stash list`Get git stash parent commit非常相似,但那些有太多令人困惑、不同的回答,我在问我自己的问题。

让我们假设如果我跑git stash list,我看到stash@{0}: On featureX: someMessageHere

当我进行存储时,我如何才能显示我正在工作的提交的哈希值(我想这可以被视为父提交)?

我看到了很多不同的答案,我很困惑这些答案是什么,它们有什么不同,以及哪个是我问题的答案:

  • git show stash@{1}^
  • git log -1 commitish^
  • git log -1 stash@{0}^
  • git log -g --no-walk --parents refs/stash
  • git for-each-ref --format='%(refname:short)' --points-at $(git rev-parse refs/stash~1) refs/heads
git log -g --format="%gd %H" refs/stash |
while read name hash; do
    printf "%s %s " $name $(git rev-parse --short $name^)
    git log -1 --format=%s $hash
done
Run Code Online (Sandbox Code Playgroud)

对于额外的上下文,这就是我要问的原因

tor*_*rek 6

LeGEC 的回答是正确的。不过,(我希望)可以帮助您理解这一部分:

我对它们各自做什么、它们有何不同以及哪一个是我的问题的答案感到困惑:[各种命令的列表]

...在这方面,让我们快速(好吧...也许不是那么快)了解 Git 的内部工作方式。

首先,Git 中最重要的事情是commit。Git 中有一个反复出现的主题:您进行提交 ( git commit),您找到一个提交(多种方式),您显示一个提交(git show或有时git log),您签出一个提交(git checkoutgit switch),然后您查看或查看提交(git log再次)。甚至git stash通过提交来工作。

提交有三大特点:

  • 每一个都有一个唯一的ID。这是它的哈希 ID,它看起来像,例如,4a0fcf9f760c9774be77f51e1e88a7499b53d2e2。很多 Git 命令都缩写了这些——4a0f例如,有时你可以缩短到前四个字符,只要这不是模棱两可的,但是在一个大的存储库中,你通常需要 7 个或更多的字符(以及 Linux 存储库)现在最多 12 个)。1

  • 每个存储一个完整的文件快照。我们不会在这里详细介绍。

  • 而且,每个都存储一些元数据:诸如谁提交、何时提交以及为什么提交(日志消息)等信息。这个元数据的一部分是针对 Git 本身的,它给出了提交的提交的哈希 ID——在提交本身之前的提交。

大多数提交只有一个父级。有些有两个或更多,在这种情况下,第一个父级是这里有趣的父级。至少一个提交——任何人在存储库中所做的第一个提交——必然没有父提交,因为在第一次提交之前没有提交。通常只有这些提交之一;所有其他人都有历史。


1这些东西看起来随机,但实际上一点也不随机。随着您将越来越多的对象添加到 Git 存储库,每个对象都获得这些对象唯一的 ID 之一,因此您越来越有可能需要使用更完整的名称来区分它们。这就像一个聚会:如果那里只有 10 个人,Bruce 的名字可能是独一无二的,但是一旦您达到 10,000,您可能至少还需要一个名字首字母。

存储库中有四种 Git 对象,但大多数情况下,我们处理提交对象,而忽略其他对象。


父路径是 Git 的工作原理

这个父级——或者第一个父级,对于合并提交——是 Git 的工作方式:向后。我们通常在最后一次提交时启动 Git ,或者更准确地说,是某个分支中的最后一次提交。然后 Git 对最后一次提交做一些事情,例如:向我们显示作者的日期、姓名和电子邮件以及日志消息。然后,Git 使用该提交的父项将一个提交移回。它向我们展示了之前的提交。然后 Git 继续访问父级的父级——原始提交的祖父级——并向我们展示该提交,然后它再次返回。

当没有任何合并时,这形成了一个很好的简单的向后提交链。如果我们让一个大写字母代表每个提交的哈希 ID,我们可以这样绘制:

... <-F <-G <-H
Run Code Online (Sandbox Code Playgroud)

H是链中的最后一次提交。我们(不知何故)让 Git 找到这个提交,并展示它。然后 Git 找到G的哈希 ID,它存储在 的元数据中H。Git 使用它来查找 commit G,它向我们展示了它。然后 GitF在里面找到的哈希ID G,依此类推。

(请注意,我们说的提交指回至其较早父提交。这就是为什么我们制定这些向后指向箭头,实现了Git可以轻松地去它有时重要的倒退,但艰难时期向前。提交G点背to early F, but not forwards to later H。不过,大多数时候,我们真的不必关心,而且很难画好这些箭头,所以大多数时候,我不打扰。)

git log例如,这就是这样做的。但是它如何找到 commitH呢?好吧,简单的方法是我们告诉它git log master。在上图中,我们可以再添加一个指针:我们有name master,指向 commit H,如下所示:

...--F--G--H   <-- master
Run Code Online (Sandbox Code Playgroud)

如果我们git checkout master进行提交,Git 将添加新提交,使其父项为H

...--F--G--H   <-- master
            \
             I
Run Code Online (Sandbox Code Playgroud)

但随后立即更新名称master,使其指向I现在提交:

...--F--G--H--I   <-- master
Run Code Online (Sandbox Code Playgroud)

最后一部分的意思是git log使用名称来查找最后一次提交。如果我们给它一个分支名称,那就是它使用的名称。如果我们不给它任何名称,则git log使用特殊名称HEAD。但是我们也可以给它一些不是分支名称的东西,stash就是这样。

关于 stash 提交的知识

git stash save(进行存储的旧方法)或git stash push(进行存储的新方法)进行提交时,它会设置它们以便特殊名称stash指代这些提交中的一个,并且该提交作为其第一个父级 -稍后我们将更多地讨论第一个父母——在你运行时(现在仍然是)当前的提交git stash

也就是说,如果我们绘制它们,我们会得到:

...--G--H   <-- master
        |\
        i-w   <-- stash
Run Code Online (Sandbox Code Playgroud)

我不会去到为什么我打电话给他们i,并w在这里,但git stash文件还要求他们IW(大写小写字母代替,我喜欢让我的大写字母更正常提交,不为这些藏匿的)。

这里重要的是 commit的第一个父w是 commit H,它是您运行时的提交git stash push或用于创建的任何内容w

有一整套复杂的方法来命名提交

当 Git 需要提交——或者实际上是任何内部对象,但我们再次只对这里的提交感兴趣时——实际上有很多方法可以命名它。完整的列表在gitrevisions 文档中进行了介绍。但是,出于我们的目的,我们希望专门查看^~后缀。稍后我们将讨论大括号和数字后缀。

如果我们采用一些有效名称,例如HEADormasterstash,并在末尾添加一个插入符号/帽子^或波浪号~,这是对 Git 内部修订查找器的指令:从我们已经命名的提交开始,找到提交的父级)。然后^后缀选择提交的第一个父项,因此这stash^意味着名称找到的提交的第一个父项stash

波浪号后缀也选择第一亲。起初这似乎是多余的:develop^并且develop~都选择了 name 选择的提交的第一个父级branch。不过,我们可以在它们之后添加数字,然后它们就会变得不同。理解这一点的关键在于我们上面绘制的图表。假设我们有:

          I--J
         /    \
...--G--H      M   <-- develop
         \    /
          K--L   <-- feature
Run Code Online (Sandbox Code Playgroud)

在这里, commitM是一个合并提交,因此它有两个父项。假设第一个父节点MJ第二个父节点ML——如果我们在命名为 commit时执行提交,然后运行到 make ,我们会得到M这样的结果。git checkout developdevelopJgit merge featureM

语法develop^2意味着找到 commit 的第二个父级M,即 find commit L。这命名了我们使用 name 获得的相同提交 - 所以我们feature可以这样做,只要我们还没有删除name feature。但重点是,M^2或者develop^2找到 commit L,因为这意味着找到第二个 parent

同时,语法develop~2意味着找到 commit 的第一个父级的第一个父级M,即 find commit I。那是因为2在这种情况下是后退的次数。所以我们退一步,沿着第一个父行 from Mto J,然后再次沿着第一个(也是唯一的)父行 from Jto I

^或之后的数字~1或根本不存在时,两者都做完全相同的事情:^1表示找到第一个父节点(后退一个第一父链接),并~1表示后退一个第一父链接

顺便说一下,让我们开始查看您的项目符号列表

  • git show stash@{1}^

我们稍后会介绍@{1}。现在,想象一下刚才说的stash^。namestash会找到一些提交,而 the^会找到它的第一个父级。然后git show显示该提交。该git show命令通过以下方式执行此操作:

  • 打印提交的哈希 ID
  • 使用作者等打印日志消息的格式化版本(您可以使用该--pretty=format:...选项进行更改)
  • 显示差异:
    • Git 获取此提交的父级快照,放入临时区域(在内存中)
    • 然后 Git 也获取此提交的快照
    • 然后 Git 比较这两个快照并告诉您不同的文件,而没有提及相同的文件

最后一部分使它看起来像是提交本身持有一个差异——但事实并非如此。差异是为你计算的,当git show你开始这样做时。

  • git log -1 commitish^

同样,^后缀使 Git 返回到提交的父级。然后git log -1显示一次提交的日志消息,但不显示差异 - 显示内容的第一部分git show- 但-1在显示一次提交后停止。

  • git log -1 stash@{0}^

这很相似,除了现在我们有stash@{0}^代替commitish^。该^后缀适用于stash@{0}符,我们会得到一个位,再一次。

  • git log -g --no-walk --parents refs/stash

这个有点不同。该--no-walk选项是多余的-g并且没有意义,因为-g接管。不过,这个--parents选项确实有意义。为了-g正确谈论,我们需要进入我们涵盖该部分的部分。让我们把最后两个命令留到以后,现在开始 reflogs。@{number}

引用

在 Git 中,每个引用——每个名字都像masterordevelop或者,事实上,stash——也可以保留自己单独的“以前”值的日志。对于普通的分支名称,这些日志只记住分支名称曾经指向的位置。因此,每个日志条目都会记住一个哈希 ID:分支名称的值。

例如,当您进行新的提交时,Git 会自动将分支名称提前以指向新的提交。但是名称曾经指向提交的父级,因此日志现在包含父级哈希 ID。如果您使用git reset重置分支,这使得预重置哈希ID写入日志了。因此,日志只会在您工作时累积每个哈希 ID。

有知道这里有另外一个重要的事情:后缀选择“个日志条目。数字零表示name 的当前值。所以拼写起来还有很长的路要走,但是 的值,是旧值,但现在更旧了,在你做了一些更新之后。@{number}master@{0}mastermaster@{1}mastermaster@{2}master

Git 通常会在一段时间后清除旧的日志条目——大多数日志条目默认为 90 天后,某些日志条目默认为 30 天。但是stash很特别,它的日志条目通常不会根据年龄清除。由于stash不是分支名称,因此它不受分支命令的操作。它改为操作由git stash命令,与它的pushpopdrop操作。

在这里,git stash使用stashreflog 来跟踪先前保存的 stash。当您使用git stash push,Git的重编使什么先前的日志条目 stash@{0}stash@{1},什么是stash@{1}stash@{2},等等。这实际上与任何普通的分支引用日志条目相同(除了永不过期的部分)。但是不同的是,当您使用git stash popgit stash drop,Git会扔掉旧的stash@{0}条目,那么什么是stash@{1}现在stash@{0},什么是stash@{2}现在stash@{1},等等。

所以现在我们可以正确地stash@{1}^从第一个开始git show

git show stash@{1}^

stash@{1}操作意味着在存储堆栈中找到一层深的存储提交。然后^后缀选择它的第一个父级。

由于stash@{1}w存储堆栈中一层深的存储stash@{1}^提交,因此是其父提交。这就是这个 stash 挂起的提交。

最后,我们还可以解决这个问题:

  • git log -g --parents refs/stash

(我已经去掉了无意义的--no-walk。)

  • -g选项指示git log查看引用日志,而不是像往常一样查找提交然后在历史记录中倒退。它将调查的一个 reflog 是 for refs/stash— 的完整拼写stash

  • --parents选项告诉git log不仅要显示每个提交哈希 ID,还要显示其所有父提交哈希 ID。

  • 因此,我们将w在 stash 堆栈中看到每个提交,以及它的两个父提交。第一个父项将是该i-w对挂起的提交,第二个父项将是i提交。

最后两个命令

  • git for-each-ref --format='%(refname:short)' --points-at $(git rev-parse refs/stash~1) refs/heads

git for-each-ref命令是一个内部主力命令——它并不是真正面向最终用户的——它同时实现git branch --listgit tag --list,以及其他几个。因为这个命令是为了编写面向用户的命令,而不是直接被用户使用,它有很多选项:

  • --format选项告诉它如何产生输出。在这里,我们选择打印名称的缩写形式(由于稍后的选项,它将是一个分支名称)。

  • --points-at选项告诉它不要打印名称,除非名称专门命名了特定的提交。我们在这里告诉它我们想要的提交是另一个 Git 命令的输出,git rev-parse.

  • refs/heads选项告诉git for-each-ref要使用哪些引用。该refs/heads空间包含您所有的分支名称。所以这告诉它:只查看我的分支名称,找到那些命名一个特定提交的分支名称;然后,对于您找到的任何名称,打印该分支名称的简短版本。

我们选择让它搜索的提交是 commit 的哈希 ID refs/stash~1。这使用~后缀来获取由 name 标识的提交的第一个父级refs/stash。这是 的完整拼写形式stash,因此我们要求 Gitrefs/stash用于查找w提交,然后使用它~来查找其父项,例如 commit H。然后我们让 Git 搜索所有分支名称以查看这些名称中是否有任何一个 commit H。如果名称refs/heads/master—branch —master标识 commit H,则该命令将打印名称master

最后:

git log -g --format="%gd %H" refs/stash |
while read name hash; do
    printf "%s %s " $name $(git rev-parse --short $name^)
    git log -1 --format=%s $hash
done
Run Code Online (Sandbox Code Playgroud)

这再次使用git log -grefs/stash查看refs/stash. 告诉 Git如何打印每个这样的条目的%gd %H指令:打印或或或任何合适的,并打印与该引用日志条目关联的哈希 ID。--format%gdstash@{0}stash@{1}stash@{2}%H

此命令的输出进入 shell 循环。此循环读取名称(部分)和哈希 ID。然后命令打印:stash@{number}printf

  • 名字;
  • 空间;
  • git rev-parse给定我们刚刚读取的哈希 ID 时找到的哈希的简短版本加上^后缀,即存储的父提交的哈希的简短版本;
  • 还有一个空格,但还没有换行符。

最后,运行git log -1 --format=%s $hash,打印存储本身的主题行。

因此,这也将打印您想要的信息——由于--shortgit rev-parse命令中——使用每个存储的适当父提交哈希 ID 的缩写形式。

  • 这个答案太棒了!!谢谢你!我还没有听说过“git stash push”,所以一定要查一下。我确信我会继续回到这个答案来参考您所教授的所有内容。谢谢。:-) (2认同)

LeG*_*GEC 5

您正在寻找的提交是stash@{0}^

git show stash@{0}^
git log -1 --oneline stash@{0}^
git rev-parse stash@{0}^
Run Code Online (Sandbox Code Playgroud)