文章目录
- 1. git rev-parse 命令
- 2. 什么是 HEAD
- 2.1 创建分支当并未切换, HEAD 不变
- 2.2 切换分支,HEAD 改变
- 2.3 再次切换分支, HEAD 再次改变
- 3. detached HEAD
- 4. HEAD 表示分支、表示 detached HEAD 有什么区别?
- 区别
- 相同点
- 5. `HEAD~`, `HEAD^`, `HEAD~1`, `HEAD^1`, `HEAD~n`, `HEAD^2` 用法说明
- 5.1 概念浅析
- 5.2 加深理解 - 准备可复现的测试工程
- `generate_commits.sh`:
- `git_commit_to_binary_tree.py`
- 生成 .png 图像
- 5.3 `HEAD~`, `HEAD^`, `HEAD~1`, `HEAD^1`, `HEAD^2` 的理解
- 5.4 `HEAD~1`, `HEAD~2`, `HEAD~3`, `HEAD~4`, `HEAD~5` 的直观理解
- 6. `~` 和 `^` 不仅限于 HEAD 使用
- 7. git push -u origin HEAD 怎么理解?
- 8. 总结
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 commit
echo "Initial commit" > file.txt
echo "*.txt merge=union" > .gitattributes # https://stackoverflow.com/questions/71369712/how-to-use-git-merge-driver-union
git add file.txt
git commit -m "Initial commit"
git tag rootgit branch dev1
git branch dev2
git branch dev3
git branch dev4# branch dev1
git checkout dev1
echo "dev1 - 1" > file.txt
git commit -am "update readme at dev1 - 1"
git tag A1echo "dev1 - 2" > file.txt
git commit -am "update readme at dev1 - 2"
git tag B1# branch dev2
git checkout dev2
echo "dev2 - 1" > file.txt
git commit -am "update at dev2 - 1"
git tag A2echo "dev2 - 2" > file.txt
git commit -am "update at dev2 - 2"
git tag B2# merge dev1 and dev2
git switch dev1
git merge dev2 --no-edit
git tag C1echo "dev1 - 3" > file.txt
git commit -am "update at dev1 - 3"
git tag D1# branch dev3
git checkout dev3
echo "dev3 - 1" > file.txt
git commit -am "update readme at dev3 - 1"
git tag A3echo "dev3 - 2" > file.txt
git commit -am "update readme at dev3 - 2"
git tag B3# branch dev4
git checkout dev4
echo "dev4 - 1" > file.txt
git commit -am "update at dev4 - 1"
git tag A4echo "dev4 - 2" > file.txt
git commit -am "update at dev4 - 2"
git tag B4# merge dev3 and dev4
git switch dev3
git merge dev4 --no-edit
git tag C3echo "dev3 - 3" > file.txt
git commit -am "update at dev3 - 3"
git tag D3# merge dev1 and dev3
git switch dev1
git merge dev3 --no-edit
git_commit_to_binary_tree.py
import subprocess
import os
from graphviz import Digraph# Step 1: 获取 Git 提交记录
def get_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# 获取标签信息
def get_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()if len(parts) == 2:tags[parts[0]] = parts[1]return tags# 获取当前HEAD的简短哈希
def get_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: 生成提交记录的二叉树结构
class Node:def __init__(self, commit_hash):self.commit_hash = commit_hashself.label = ""self.left = Noneself.right = Nonedef build_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_hashnodes[commit['hash']] = nodefor commit in commits:node = nodes[commit['hash']]if len(commit['parents']) > 0:node.left = nodes.get(commit['parents'][0], None)if len(commit['parents']) > 1:node.right = nodes.get(commit['parents'][1], None)return nodes# Step 3: 生成 .dot 文件
def generate_dot_file(root_hash, nodes, dot_filename):dot = Digraph()root = nodes[root_hash]def add_edges(node):if node is not None: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 push -u origin HEAD 怎么理解?
在新建分支、本地完成开发后,提交到remote的时候,最简短的写法是:
git push -u origin HEAD
其中 -u
表示设置 upstream branch, origin 是 remote 的名字, HEAD 则表示当前分支的名字。假设当前是 dev 分支,那么这就话就等价于
git push -u origin dev
可以说, HEAD
的写法非常简单、可以避免手贱写错当前分支名字,很好用。
8. 总结
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
这样的写法。