0


成为git砖家(5): 理解 HEAD

文章目录

1. git rev-parse 命令

git rev-parse

命令是一个非常有用的 git 命令, 主要用于解析和转换 git 对象的引用(例如分支名、标签、提交哈希等)为更具体、更底层的哈希值。

假设当前处于

main

分支,那么

HEAD

显然和

main

表达同样的含义,转换为对应的哈希值是一样的:

git rev-parse main
git rev-parse HEAD

在这里插入图片描述
当然,完整的 git hash值有40位,没法让人一下子记住,我们可以只查看段的hash值,默认是7位:

git rev-parse --short main
git rev-parse --short HEAD

在这里插入图片描述

2. 什么是 HEAD

在 Pro Git 这本书中很好的解释了 HEAD 的概念: 指向当前所在的分支。作为验证, 可以通过查看

.git/HEAD

文件内容,或

git rev-parse HEAD

命令来确认。

2.1 创建分支当并未切换, HEAD 不变

git branch testing

此时创建了新分支

testing

, 但并且切换到新分支, 仍处于老的分支

master

, 此时 HEAD 指向 master:
在这里插入图片描述

2.2 切换分支,HEAD 改变

当执行了分支切换的命令后,HEAD随之改变:

git checkout testing

在这里插入图片描述

2.3 再次切换分支, HEAD 再次改变

当从 testing 分支切换回 master 分支, HEAD 也随之改变:

git checkout master

在这里插入图片描述

3. detached HEAD

有时候切换到某个 commit 时,并未指定分支名字, 这叫做游离状态的 HEAD。

git checkout <hash>

在这里插入图片描述
可以借助 git图形化界面工具如 gitk,查看当前 commit 情况,其中黄色节点

conv1x1

(42e6766) 是 detached HEAD:

gitk --all

在这里插入图片描述
作为验证,使用

git rev-parse HEAD

可以得到对应的哈希值:
在这里插入图片描述

4. HEAD 表示分支、表示 detached HEAD 有什么区别?

区别

区别在于 detached HEAD 情况下,

git branch

返回的不是分支名字:

在这里插入图片描述
在这里插入图片描述
此时的

.git/HEAD

文件内容也变为了具体的hash值:
在这里插入图片描述

而如果是常规的 HEAD (处于分支),git branch 命令得到分支名字:
在这里插入图片描述

相同点

不管是出于 detached HEAD 还是常规的分支,

git rev-parse HEAD

都是可以使用的,

HEAD~1

这样的表达式都是可以使用的。

5.

HEAD~

,

HEAD^

,

HEAD~1

,

HEAD^1

,

HEAD~n

,

HEAD^2

用法说明

5.1 概念浅析

目前应该找不到比 git在回退版本时HEAD~和HEAD^的作用和区别 这篇还清晰的讲解了,这里简单贴一下个人读后感:

  • HEAD~ 等价于 HEAD~1
  • HEAD^ 等价于 HEAD^1
  • HEAD~1 表示回退一步,退到第一个父节点上
  • HEAD^1 表示回退到前一步的第一个父节点上
  • HEAD^2 表示回退到前一步的第二个父节点上
  • HEAD~n 表示回退到前n步的第一个父节点上

5.2 加深理解 - 准备可复现的测试工程

下面给出可以复现的步骤来进行说明:

  • generate_commits.sh 生成测试仓库, 虽然你执行的时候commit 哈希会变,但是commit结构不变、tag名字不变
  • git_commit_to_binary_tree.py: 扫描给定的git仓库的commit记录,生成 .dot 文件
generate_commits.sh

:

mkdir my-git-repo
cd my-git-repo
git init

# Initial commitecho"Initial commit"> file.txt
echo"*.txt merge=union"> .gitattributes # https://stackoverflow.com/questions/71369712/how-to-use-git-merge-driver-uniongitadd file.txt
git commit -m"Initial commit"git tag root

git branch dev1
git branch dev2
git branch dev3
git branch dev4

# branch dev1git checkout dev1
echo"dev1 - 1"> file.txt
git commit -am"update readme at dev1 - 1"git tag A1

echo"dev1 - 2"> file.txt
git commit -am"update readme at dev1 - 2"git tag B1

# branch dev2git checkout dev2
echo"dev2 - 1"> file.txt
git commit -am"update at dev2 - 1"git tag A2

echo"dev2 - 2"> file.txt
git commit -am"update at dev2 - 2"git tag B2

# merge dev1 and dev2git switch dev1
git merge dev2 --no-edit
git tag C1

echo"dev1 - 3"> file.txt
git commit -am"update at dev1 - 3"git tag D1

# branch dev3git checkout dev3
echo"dev3 - 1"> file.txt
git commit -am"update readme at dev3 - 1"git tag A3

echo"dev3 - 2"> file.txt
git commit -am"update readme at dev3 - 2"git tag B3

# branch dev4git checkout dev4
echo"dev4 - 1"> file.txt
git commit -am"update at dev4 - 1"git tag A4

echo"dev4 - 2"> file.txt
git commit -am"update at dev4 - 2"git tag B4

# merge dev3 and dev4git switch dev3
git merge dev4 --no-edit
git tag C3

echo"dev3 - 3"> file.txt
git commit -am"update at dev3 - 3"git tag D3

# merge dev1 and dev3git switch dev1
git merge dev3 --no-edit
git_commit_to_binary_tree.py
import subprocess
import os
from graphviz import Digraph

# Step 1: 获取 Git 提交记录defget_git_commits(repo_path):
    os.chdir(repo_path)# 获取提交记录,包括简短的哈希值
    result = subprocess.run(['git','log','--pretty=format:%h %H %P'], stdout=subprocess.PIPE)
    commit_lines = result.stdout.decode('utf-8').split('\n')
    commits =[]for line in commit_lines:
        parts = line.split()
        commit ={"short_hash": parts[0],"hash": parts[1],"parents": parts[2:]}
        commits.append(commit)return commits

# 获取标签信息defget_git_tags(repo_path):
    os.chdir(repo_path)
    result = subprocess.run(['git','tag','-l','--format=%(objectname) %(refname:short)'], stdout=subprocess.PIPE)
    tag_lines = result.stdout.decode('utf-8').split('\n')
    tags ={}for line in tag_lines:
        parts = line.split()iflen(parts)==2:
            tags[parts[0]]= parts[1]return tags

# 获取当前HEAD的简短哈希defget_git_head(repo_path):
    os.chdir(repo_path)
    result = subprocess.run(['git','rev-parse','--short','HEAD'], stdout=subprocess.PIPE)return result.stdout.decode('utf-8').strip()# Step 2: 生成提交记录的二叉树结构classNode:def__init__(self, commit_hash):
        self.commit_hash = commit_hash
        self.label =""
        self.left =None
        self.right =Nonedefbuild_binary_tree(commits, tags, head_short_hash):
    nodes ={}for commit in commits:
        short_hash = commit['short_hash']
        node = Node(short_hash)if commit['hash']in tags:
            node.label = tags[commit['hash']]elif short_hash == head_short_hash:
            node.label ="HEAD"else:
            node.label = short_hash
        nodes[commit['hash']]= node

    for commit in commits:
        node = nodes[commit['hash']]iflen(commit['parents'])>0:
            node.left = nodes.get(commit['parents'][0],None)iflen(commit['parents'])>1:
            node.right = nodes.get(commit['parents'][1],None)return nodes

# Step 3: 生成 .dot 文件defgenerate_dot_file(root_hash, nodes, dot_filename):
    dot = Digraph()
    root = nodes[root_hash]defadd_edges(node):if node isnotNone:
            dot.node(node.commit_hash, label=node.label)if node.left:
                dot.edge(node.commit_hash, node.left.commit_hash)
                add_edges(node.left)if node.right:
                dot.edge(node.commit_hash, node.right.commit_hash)
                add_edges(node.right)

    add_edges(root)

    dot.save(dot_filename)# 使用示例
repo_path ='my-git-repo'# 替换为你的Git仓库路径
dot_filename ='commit_tree.dot'

commits = get_git_commits(repo_path)
tags = get_git_tags(repo_path)
head_short_hash = get_git_head(repo_path)
nodes = build_binary_tree(commits, tags, head_short_hash)
root_hash = commits[0]['hash']# 假设最近的提交为根节点

generate_dot_file(root_hash, nodes, dot_filename)

执行:

python git_commit_to_binary_tree.py

会生成

commit_tree.dot

文件。

生成 .png 图像
dot -Tpng commit_tree.dot -o commit_tree.png

打开 commit_tree.png

在这里插入图片描述

5.3

HEAD~

,

HEAD^

,

HEAD~1

,

HEAD^1

,

HEAD^2

的理解

在这里插入图片描述
在这里插入图片描述

(base) ➜  my-git-repo git:(dev1)git rev-parse HEAD
3d63abe282aebfa3aff013972d2acf2181bf1bf7
(base) ➜  my-git-repo git:(dev1)git rev-parse --short HEAD
3d63abe
(base) ➜  my-git-repo git:(dev1)git rev-parse --short D1
4bd7d08
(base) ➜  my-git-repo git:(dev1)git rev-parse --short D4
7e27b48
(base) ➜  my-git-repo git:(dev1)git rev-parse --short HEAD~
4bd7d08
(base) ➜  my-git-repo git:(dev1)git rev-parse --short HEAD^
4bd7d08
(base) ➜  my-git-repo git:(dev1)git rev-parse --short HEAD~1
4bd7d08
(base) ➜  my-git-repo git:(dev1)git rev-parse --short HEAD^1
4bd7d08
(base) ➜  my-git-repo git:(dev1)git rev-parse --short HEAD^2
7e27b48

5.4

HEAD~1

,

HEAD~2

,

HEAD~3

,

HEAD~4

,

HEAD~5

的直观理解

HEAD~n

表示第n级祖先节点中的第一个节点。例如红色的 HEAD~1 表示父节点,黄色的

HEAD~2

表示爷爷节点, 绿色的

HEAD~3

表示第3级父节点,蓝色的

HEAD~4

表示第4级父节点。
在这里插入图片描述

对于

B2

节点,应当用

HEAD~2^2

表示:

HEAD~2

表达了从 HEAD 到 D1 再到 C1 的路径,

^2

则表达了从 B1, B2 里选择 B2:

在这里插入图片描述

6.

~

^

不仅限于 HEAD 使用

commit 哈希码也可以使用。
tag 也可以使用。

举例:

  • 3d63abe~1
  • 3d63abe^2
  • D1~2
  • C4~在这里插入图片描述

7. git 官方文档中关于 HEAD~ 等表示的说明

https://git-scm.com/docs/gitrevisions

在这里插入图片描述

8. git push -u origin HEAD 怎么理解?

在新建分支、本地完成开发后,提交到remote的时候,最简短的写法是:

git push -u origin HEAD

其中

-u

表示设置 upstream branch, origin 是 remote 的名字, HEAD 则表示当前分支的名字。假设当前是 dev 分支,那么这就话就等价于

git push -u origin dev

可以说,

HEAD

的写法非常简单、可以避免手贱写错当前分支名字,很好用。

9. 总结

HEAD

表示当前分支的别名。当切换分支,

.git/HEAD

就变化了。

查看

.git/HEAD

并不是很直观, 直观的方式是用

git rev-parse HEAD

命令, 以及

git rev-parse main

这样的写法。进一步的, 使用

git rev-parse --short HEAD

查看短哈希更佳直观。

HEAD

之外,还可以使用

HEAD~

,

HEAD^

的形式, 以及

HEAD~n

的形式。

HEAD^2

表示上一层节点中的第二个节点, 而

HEAD~2

则表示“爷爷节点”。

通过使用 graphviz 和 python,解析了 git 仓库的历史提交记录, 并结合 tag, 直观的理解了

HEAD~2^2

这样的写法。


本文转载自: https://blog.csdn.net/baiyu33/article/details/140752115
版权归原作者 baiyu33 所有, 如有侵权,请联系我们删除。

“成为git砖家(5): 理解 HEAD”的评论:

还没有评论