文章目录
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
这样的写法。
版权归原作者 baiyu33 所有, 如有侵权,请联系我们删除。