做一个现实的理想主义者

Git的核心原理以及常用命令 - 【耍蛇】

2017.04.13

前言

作为一个程序猿,Git必须得掌握,毕竟是目前世界上最优秀的分布式版本控制系统。

以前用过一段时间的SVN,也不过是会几个简单的操作命令,一旦遇到问题就蒙蔽了,这也是导致我每能长期坚持用下去的,只知其然,不知其所以然。

有了过往的教训后,这次学习Git就有了方向:

  • Git到底是什么,掌握核心原理
  • 常用命令是干嘛的,其工作原理是什么
  • 用进废退,得多用。

Git核心原理

说起Git的核心原理,我们得提前了解下版本控制系统,毕竟Git也是一种版本控制系统。

版本控制系统

版本控制系统分为三大类:本地版本控制系统集中式版本控制系统分布式版本控制系统

本地版本控制系统
本地版本控制系统

本地版本控制系统是将文件的各个版本以特定的数据格式存储在本地的磁盘,这种方式在一定程度上能解决了手动复制粘贴的问题,但无法解决多人协作的问题。

集中式版本控制系统

集中式版本控制系统

集中式版本控制系统相比本地版本控制系统的唯一区别在于多了一个中央服务器,各个版本的数据存储在中央服务器,解决了多人协作问题,管理员可以控制开发人员的权限,而开发人员也可以从中央服务器拉取数据。但是,由于所有数据都存储在中央服务器,服务器一旦宕机或者磁盘损坏,会造成不可估量的损失。

分布式版本控制系统

分布式版本控制系统

分布式版本控制系统相较前两者,有两个显著的特点:其一,在分布式版本控制系统中,系统保存的不是文件变化的差量,而是文件的快照,即把文件的整体复制下来保存,而不关心具体的变化内容;其二,系统是分布式的,当你从中央服务器拷贝下来代码时,你拷贝的是一个完整的版本库,包括历史记录、提交记录等,这样即使中央服务器或者任意一台机器宕机了,也能从其它的机器找到文件的完整备份。

Git的基本原理

Git基础

Git是一个分布式版本控制系统,保存的是文件的完整快照,而不是差异变化或者文件补丁。

保存每一次变化文件的内容。

保存每一次变化文件的完整内容

Git每一次提交都是对项目文件的一个完整拷贝,因此你可以完全恢复到以前的任一个提交而不会发生任何区别。这里有一个问题:如果我的项目大小是10M,那Git占用的空间是不是随着提交次数的增加线性增加呢?我提交(commit)了10次,占用空间是不是100M呢?很显然不是,Git是很智能的,如果文件没有变化,它只会保存一个指向上一个版本的文件的指针,即,对于一个特定版本的文件,Git只会保存一个副本,但可以有多个指向该文件的指针

另外注意,Git最适合保存文本文件,事实上Git被设计出来就是为了保存文本文件的,像各种语言的源代码,因为Git可以对文本文件进行很好的压缩和差异分析。而二进制文件,像视频、图片等,Git也能管理,但不能取得较好的效果(压缩比率低,不能差异分析)。实验证明,一个500k的文本文件经Git压缩后仅50k左右,稍微改变内容后两次提交,会有两个50k左右的文件,没错的,保存的是完整快照。而对于二进制文件,像视频、图片等,压缩率非常小,Git占用空间机会随着提交次数线性增长。

未变化的文件只保存上一个版本的指针

未变化的文件只保存上一个版本的指针

Git工程有三个工作区域:工作目录,暂存区域,以及本地仓库。工作目录是你当前进行工作的区域;暂存区域是你运行git add命令后文件保存的区域,也是下次提交将要保存的文件(注意:Git提交实际读取的是暂存区域的内容,而与工作区域的文件无关,这也是你修改了文件之后,如果没有添加git add到暂存区域,并不会保存到版本库的原因);本地仓库就是版本库,记录了你工程某次提交的完整状态和内容,这意味着你的数据永远不会丢失。

相应的,文件也有三种状态:已提交(committed)、已修改(modified)和已暂存(staged)。已提交表示该文件已被安全地保存在本地版本库中了;已修改表示修改了某个文件,但还没有提交保存;已暂存表示把已修改的文件放在下次提交时要保存的清单中,即暂存区域。

所以,使用Git的基本工作流程如下:

  1. 在工作区域增加、删除或者修改文件;
  2. 运行git add,将文件快照保存到暂存区域;
  3. 提交更新,将文件永久保存到版本库中。

Git的基本工作流程

Git的对象

现在已经明白Git的基本流程,但Git是怎么完成的呢?Git怎么区分文件是否发生了变化?下面简单介绍下Git的基本原理。

SHA-1校验和

Git是一套内容寻址文件系统,意思就是说Git从核心上来看不过是简单地存储键值对(key-value),value是文件的内容,而key是文件内容与文件头信息的40个子符长度的SHA-1校验和,例如:5453545dccd33565a585ffe5f53fda3e067b84d8。Git使用该校验和不是为了加密,而是为了数据的完整性,它可以保证,在很多年后,你重新checkout某个commit时,一定是它多年前的当时的状态,完全一模一样。当你对文件进行了哪怕一丁点的修改,也会计算出完全不同的SHA-1校验和,这种现象叫做“雪崩效应”(Avalanche effect)。

SHA-1校验和也就是上文提到的文件的指针,这和C语言中的指针很有些不同:C语言将数据在内存中的地址作为指针,Git将文件的SHA-1校验和作为指针,目的都是为了唯一区分不同的对象。但是当C语言指针指向的内存中的内容发生变化时,指针并不发生变化,但Git的指针指向的文件内容发生变化时,指针也会发生变化。所以,Git中每一个版本的文件,都有一个唯一的指针指向它。

文件(blob)对象,树(tree)对象,提交(commit)对象

blob对象保存的仅仅是文件的内容,tree对象更像是操作系统中的目录,它可以保存blob对象和tree对象。一个单独的tree对象包含一条或多条tree记录,每一条记录含有一个指向blob对象或子tree对象的SHA-1指针,并附有该对象的权限模式(mode)、类型和文件名信息等。

文文件对象、树对象、提交对象

当你对文件进行修改并提交时,变化的文件会生成一个新的blob对象,记录文件的完整内容(是全部内容,不是变化内容),然后针对该文件有一个唯一的SHA-1校验和,修改此次提交该文件的指针为该SHA-1校验和,而对于没有变化的文件,简单拷贝上一次版本的指针即SHA-1校验和,而不是生成一个全新的blob对象,这也解释了10M大小的项目进行10次提交远远小于100M的原因。

另外,每次提交可能不仅仅只有一个tree对象,它们指明了项目的不同快照,但你IXUS记住所有对象的SHA-1校验和才能获得完整的快照,而且没有作者、何时、为什么保存这些快照的原因。commit对象就是为了解决这些问题诞生的,commit对象的格式很简单:指明了该时间点项目快照的顶层tree对象、作者/提交者信息(从Git设置的user.name和user.email中获得)以及当前时间戳、一个空行,上一次的提交对象的ID以及提交注释信息。你可以简单的运行git log来获取这些信息:

git log信息

示例

上图的Test.txt是第一次提交之前生成的,第一次它的初始SHA-1和以3c4e9c开头。随后对它进行了修改,所以第二次提交时生成了一个全新blob对象,校验和以1f7a7a开头。而第三次提交时Test.txt并没有变化,所以只是保存最近版本的SHA-1校验和而不生成全新的blob队形。在项目开发过程中新增加的文件在提交后都会生成一个全新的blob对象来保存它。注意除了第一次每个提交对象都有一个指向上一次提交对象的指针。

因此简单来说,blob对象保存文件的内容;tree对象类似文件夹,保存blob对象和其它tree对象;commit对象保存tree对象,提交信息、作者、邮箱以及上一次的提交对象的ID(第一次提交没有)。而Git就是通过组织和管理这些对象的状态以及复杂的关系实现的版本控制以及其它功能。

Git引用

现在再来看引用,就会很简单了。如果我们像要看某个提交记录之前的完整历史,就必须记住这个提交ID,但提交ID是一个40位的SHA-1校验和,难记。所以引用SHA-1校验和的别名,存储在.git/refs文件夹中。

最常见的引用也许就是masterl ,因为这是Git默认创建的(可以修改,但一般不修改),它始终指向你项目主分支的最后一次提交记录。如果在项目根目录运行cat .git/refs/heads,会输出一个SHA-1校验和,例如:

master引用

因此master只是一个40位SHA-1校验和的别名罢了。

还有一个问题,Git如何知道你当前分支的最后一次的提交ID?在.git文件夹下有一个HEAD文件,像这样:

HEAD文件

HEAD文件其实并不包含SHA-1值,而是一个指向当前分支的引用,内容会随着切换分支而变化,内容格式像这样:ref:refs/heads/<branch-name>。当你执行git commit命令时,它就创建了一个commit对象,把这个commit对象的父级设置为HEAD指向的引用的SHA-1值。

再来说说Git的tag——标签,标签从某种意义上像是一个引用,它指向一个commit对象而不是一个tree,包含一个标签、一组数据、一个消息和一个commit对象的指针。但是区别就是引用随着项目进行它的值在不断向前推进变化,但是标签不会变化——永远指向同一个commit,仅仅是提供一个更加友好的名字。

Git分支

分支

分支是Git的杀手级特征,而且Git鼓励在工作流程中频繁使用分支与合并,哪怕一天之内进行许多次都没有关系。因为Git分支非常轻量级,不像其它的版本控制,创建分支意味着要把项目完整的拷贝一份,而Git创建分支是在瞬间完成的,而与你工程的复杂程度无关。

因为在上文中已经说到,Git保存文件的最基本的对象是blob对象,Git本质上只是一颗巨大的文件数,树的每一个节点就是blob对象,而分支只是树的一个分叉。说白了,分支就是一个有名字的引用,它包含一个提交对象的40位校验和,所以创建分支就是向一个文件写入41个字节(外加一个换行符)那么简单,所以自然就快了而且与项目的复杂程度无关。

Git的默认分支是master,存储在.git\refs\heads\master文件中,假设你在master分支运行git branch dev创建了一个名为dev的分支,那么Git所做的实际操作是:

  1. .git\refs\heads文件夹下新建一个文件名为dev(没有扩展名)的文本文件;
  2. 将HEAD指向的当前分支(当前为master)的40位SHA-1校验和加一个换行符写入 dev文件;
  3. 结束。

创建分支

创建分支就是这么简单,那么切换分支呢?更简单:

  1. 修改.git文件下的HEAD文件为ref:refs\heads\<分支名称>
  2. 按照分支指向的提交记录将工作区的文件恢复至一模一样;
  3. 结束。

记住,HEAD文件指向当前分支的最后一次提交,同时,它也是以当前分支再次创建一个分支时,将要写入的内容。

分支合并

再来说一说合并,首先是Fast-forward,换句话说,如果顺着一个分支走下去可以达到另一个分支的话,那么Git在合并两者时,只会简单地把指针右移,因为这种单线的历史分支不存在任何需要解决的分歧,所以这种合并过程可以成为快进(Fast forward)。比如:

Fast forward

注意箭头方向,因为每一次提交都有一个指向上一次提交的指针,所以箭头方向向左,更为合理

当在master分支合并dev分支时,因为他们在一条线上,这种单线的历史分支不存在任何需要解决的分歧,所以只需要master分支指向dev分支即可,所以非常快。

当分支出现分叉时,就有可能出现冲突,而这时Git就会要求你却解决冲突,你如像下面的历史:

分叉分支

因为master分支和dev分支不在一条线上,即v7不是v5的直接祖先,Git不得不进行一些额外处理。就此例而言,Git会用两个分支的末端(v7v5)以及他们的共同祖先(v3)进行一次简单的三方合并计算。合并之后会生成一个合并提交v8

三方合并计算

注意:合并提交有两个祖先(v7v5

分支的变基rebase

把一个分支中的修改整合到另一个分支的办法有两种:mergerebase。首先,mergerebase最终的结果是一样的,但rebase能产生一个更为整洁的提交历史。仍然以上图为例,如果简单的merge,会生成一个提交对象v8,现在我们尝试使用变基合并分支,切换到dev:

分支的变基命令

分支的变基示例

这段代码的意思是:回到两个分子最近的共同祖先v3,根据当前分支(也就是要进行变基的分支dev)后续的历次提交对象(包括v4,v5),生成一系列文件补丁,然后以基底分支(也就是主干分支master)最后一个提交对象(v7)为新的出发点,逐个应用之前准备好的补丁文件,最后会生成两个新的合并提交对象(v4,v5),从而改写dev的提交历史,使它成为master分支的直接下游,如下图:

现在,就可以回到master分支进行快速合并Fast-forward了,因为master分支和dev分支在一条线上:

现在的v5'对应的快照,其实和普通的三方合并,即上个例子中的v8对应的快照内容一模一样。虽然最后整合得到的结果没有任何区别,但变基能产生一个更为整洁的提交历史。如果视察一个变基过的分支的历史记录,看来会更清楚:彷佛所有修改都是在一根线上先后进行的,尽管实际上它们原本是同时并行发生的。

Git命令图解

日常命令

日常使用的命令图解

Git的命令非常多,要想熟练的掌握Git,可能需要记住近百个命令,不过别吓着了,常用的命令不多,也就是上图所示的6个。

命令大全

新建代码库

# 在当前目录新建一个Git代码库
$ git init

# 新建一个目录,将其初始化为Git代码库
$ git init [project-name]

# 下载一个项目和它的整个代码历史
$ git clone [url]

配置

Git的设置文件为.gitconfig,它可以在用户主目录下(全局配置),也可以在项目目录下(项目配置)。

# 显示当前的Git配置
$ git config --list

# 编辑Git配置文件
$ git config -e [--global]

# 设置提交代码时的用户信息
$ git config [--global] user.name "[name]"
$ git config [--global] user.email "[email address]"

增加/删除文件

# 添加指定文件到暂存区
$ git add [file1] [file2] ...

# 添加指定目录到暂存区,包括子目录
$ git add [dir]

# 添加当前目录的所有文件到暂存区
$ git add .

# 添加每个变化前,都会要求确认
# 对于同一个文件的多出变化,可以实现分次提交
$ git add -p

# 删除工作区文件,并且将这次删除放入暂存区
$ git rm [file1] [file2] ...

# 停止追踪指定文件,但该文件会保留再工作区
$ git rm --cached [file]

# 改名文件,并且将这个改名放入暂存区
$ git mv [file-original] [file-renamed]

代码提交

# 提交暂存区到仓库区
$ git commit  -m [message]

# 提交暂存区的指定文件到仓库区
$ git commit [file1] [file2] ... -m [message]

# 提交工作区自上次commit之后的变化,直接到仓库区
$ git commit -a

# 提交时显示所有diff信息
$ git commit -v

# 使用一次新的commit,替代上一次提交
# 如果代码没有任何新变化,则用来改写上一次commit的提交信息
$ git commit --amend -m [message]

# 重做上一次commit,并包括指定文件的新变化
$ git commit --amend [file1] [file2] ...

分支

# 列出所有本地分支
$ git branch

# 列出所有远程分支
$ git branch -r

# 列出所有本地分支和远程分支
$ git branch -a

# 新建一个分支,但依然停留当前分支
$ git branch [branch-name]

# 新建一个分支,并切换到该分支
$ git checkout -b [branch-name]

# 新建一个分支,指向指定commit
$ git branch [branch] [commit]

# 新建一个分支,与指定的远程分支建立追踪关系
$ git branch --track [branch] [remote-branch]

# 切换到指定分支,并更新工作区
$ git checkout [branch-name]

# 切换到上一个分支
$ git checkout -

# 简历追踪关系,在现有分支与指定的远程分支之间
$ git branch --set-upstream [branch] [remote-branch]

# 合并指定分支到当前分支
$ git merge [branch]

# 选择一个commit,合并进当前分支
$ git cherry-pick [commit]

# 删除分支
$ git branch -d [branch-name]

# 删除远程分支
$ git push origin --delete [branch-name]
$ git branch -dr [remote/branch]

标签

# 列出所有tag
$ git tag

# 新建一个tag在当前commit
$ git tag [tag]

# 新建一个tag在指定commit
$ git tag [tag] [commit]

# 删除本地tag
$ git tag -d [tag]

# 删除远程tag
$ git push origin :refs/tags/[tagname]

# 查看tag信息
$ tag show [tag]

# 提交指定tag
$ git push [remote] [tag]

# 提交所有tag
$ git push [remote] --tags

# 新建一个分支,指向某个tag
$ git checkout -b [brach] [tag]

查看信息

# 显示有变更的文件
$ git status

# 显示当前分支的版本历史
$ git log

# 显示commit历史,以及每次commit发生变更的文件
$ git log --stat

# 搜索提交历史,根据关键词
$ git log -S [keyword]

# 显示某个commit之后的所有变动,每个commit占据一行
$ git log [tags] HEAD --pretty=format:%

# 显示某个commit之后的所有变动,其“提交说明”必须符合搜索条件
$ git log [tag] HEAD --grep feature

# 显示某个文件的版本历史,包括文件改名
$ git log --follow [file]
$ git whatchanged [file]

# 显示指定文件相关的每一次diff
$ git log -p [file]

# 显示过去5次提交
$ git log -5 --pretty --oneline

# 显示所有提交过的用户,按提交次数排序
$ git shortlog -sn

# 显示指定文件是什么人在什么时间修改过
$ git blame [file]

# 显示暂存区和工作区的差异
$ git diff

# 显示暂存区和上一个commit的差异
$ git diff --cached [file]

# 显示工作区与当前分支最新commit之间的差异
$ git diff HEAD

# 显示两次提交之前的差异
$ git diff [first-branch]...[second-branch]

# 显示今天你写了多少行代码
$ git diff --short "@{0 day ago}"

# 显示某次提交的元数据和内容变化
$ git show [commit]

# 显示某次提交时,某个文件的内容
$ git show[commit]:[filename]

# 显示当前分支的最近几次提交
$ git reflog

远程同步

# 下载远程仓库的所有变动
$ git fetch [remote]

# 显示所有远程仓库
$ git remote -v

# 显示某个远程仓库的信息
$ git remote show [remote]

# 增加一个新的远程仓库,并命名
$ git remote add [shortname] [url]

# 取回远程仓库的变化,并与本地分支合并
$ git pull [remote] [branch]

# 上传本地指定分支到远程仓库
$ git push [remote] [branch]

# 强行推送当前分支到远程仓库,即使有冲突
$ git push [remote] --force

# 推送所有分支到远程仓库
$ git push [remote] --all

撤销

# 恢复暂存区的指定文件到工作区
$ git checkout [file]

# 恢复某个commit的指定文件到暂存区和工作区
$ git  checkout [commit] [file]

# 恢复暂存区的所有文件到工作区
$ git checkout .

# 重置暂存区的指定文件,与上一次commit保持一致,但工作区不变
$ git reset [file]

# 重置暂存区与工作区,与上一次commit保持一致
$ git reset --hard

# 重置当前分支的指针为指定commit,同时重置暂存区,但工作区不变
$ git reset [commit]

# 重置当前分支的HEAD为指定commit,同时重置暂存区和工作区,与指定commit一致
$ git reset --hard [commit]

# 重置当前HEAD为指定commit,但保持暂存区和工作区不变
$ git reset --keep [commit]

# 新建一个commit,用来撤销指定commit
# 后者的所有变化都将被前者抵消,并且应用到当前分支
$ git revert [commit]

# 暂时将未提交的变化移除,稍后再移入
$ git stash
$ git stash pop

其它

# 生成一个可供发布的压缩包
$ git archive

命令图解

diff

有许多种方法查看两次提交之间的变动。下面是一些示例:

diff命令图解

commit

提交时,git用暂存区域的文件创建一个新的提交,并把此时的节点设为父节点。然后把当前分支指向新的提交节点。下图中,当前分支是master。在运行命令之前,master指向ed489,提交后,master指向新的节点f0cec并以ed489作为父节点。

commit命令图解1

即使当前分支是某次提交的祖父节点,git会同样操作。下图中,在master分支的祖父节点maint分支进行一次提交,生成了1800b。这样,maint分支就不再是master分支的祖父节点。此时,合并是必须的。

commit命令图解2

如果想更改一次提交,使用git commit --amend,Git会使用与当前提交相同的父节点进行一次新提交,旧得提交会被取消。

commit命令图解3

checkout

checkout命令用于从历史提交(或者暂存区域)中拷贝文件到工作目录,也可用于切换分支。

当指定某个文件名(或者打开-p选项,或者文件名和-p选项同时打开)时,Git会从指定的提交中拷贝文件到暂存区域和工作目录。比如,git checkout HEAD~ foo.c会将提交节点HEAD~(即当前提交节点的父节点)中的foo.c复制到工作目录并且加到暂存区域中。(如果命令中没有指定提交节点,则会从暂存区域中拷贝内容。)注意当前分支不会发生变化。

checkout命令图解1

当不指定文件名,而是给出一个(本地)分支时,那么HEAD标识会移动到那个分支(也就是说,我们“切换”到那个分支了),然后暂存区域和工作目录中的内容和HEAD对应的提交节点一致。新提交节点(下图中的a47c3)中的所有文件都会呗复制(到暂存区域和工作目录中);只存在于老的提交节点(ed489)中的文件会被删除;不属于上述两者的文件会被忽略,不受影响。

checkout命令图解2

如果既没有指定文件名,也没有指定分支名,而是一个标签、远程分支、SHA-1值或者是像master~3类似的东西,就得到一个匿名分支,称作 detached HEAD(被分离的HEAD标识)。这样可以很方便地在历史版本之间互相切换。比如说你想要变异1.6.6.1版本的git,你可以运行git checkout v1.6.6.1(这是一个标签,而非分支名),编译,安装,然后切换回另一个分支,比如说git checkout master。然而,当提交操作涉及到“分离的HEAD”时,其行为会略有不同,详情见下面。

checkout命令图解3

HEAD标识处于分离状态时的提交操作

当HEAD处于分离状态(不依附于任一分支)时,提交操作可以正常进行,但是不会更新任何已命令的分支。(你可以认为这是在更新一个匿名分支)

HEAD标识处于分离状态时的提交操作1

一旦此后你切换到别的分支,比如说master,那么这个提交节点(可能)再也不会被引用到,然后就被会丢弃掉了。注意这个命令之后就不再会有东西引用2eecb

HEAD标识处于分离状态时的提交操作2

但是,如果你想保存这个状态,可以用命令git checkout -b name来创建一个新的分支。

HEAD标识处于分离状态时的提交操作3

reset

reset命令把当前分支指向另一个位置,并且有选择的变动工作目录和索引。也用来在从历史仓库中复制文件到索引,而不动工作目录。

如果不给选项,那么当前分支指向到那个提交。如果用--hard选项,那么工作目录也更新,如果用--soft选项,那么都不变。

reset命令图解1

如果没有给出提交点的版本号,那么默认用HEAD。这样,分支指向不变,但是索引会回滚到最后一次提交,如果用--hard选项,工作目录也同样。

reset命令图解2

如果给了文件名(或者-p选项),那么效果和带文件名的checkout差不多,除了索引被更新。

reset命令图解3

merge

merge命令把不同分支合并起来。合并前,索引必须和当前提交相同。如果另一个分支是当前提交的祖父节点,那么合并命令将什么也不做。另一种情况是如果当前提交是另一个分支的祖父节点,就导致Fast-forward合并。指向只是简单的移动,并生成一个新的提交。

merge命令图解1

否则就是一次真正的合并。默认把当前提交(ed489如下所示)和另一个提交(33104)以及他们共同祖父节点(b325c)进行一次三方合并。结果是先保存当前目录和索引,然后和父节点33104一起做一次新提交。

merge命令图解2

cherry pick

cherry-pick 命令“复制”一个提交节点并在当前分支做一次完全一样的新提交。

cherry-pick命令图解

rebase

衍合是合并命令的另一种选择。合并把两个父分支合并进行一次提交,提交历史不是线性的。衍合在当前分支上重演另一个分支的历史,提交历史是线性的。本质上,这是线性化的自动的cherry-pick。

rebase命令图解1

上面的命令都在topic分支中进行,而不是master分支,在master分支上重演,并且把分支指向新的节点。注意,旧提交没有被引用,将被回收。

要限制回滚范围,使用--noto选项。下面的命令在master分支上重演当前分支从169a6依赖的最近几个提及哦啊,即2c33a

rebase命令图解2

同样有git rebase --interactive让你更方便的完成一些复杂操作,比如丢弃、重排、修改、合并提交。

技术说明

文件内容并没有真正存储在索引(.git/index)或者提交对象中,而是以blob的形式分别存储在数据库中(.git/objects),并用SHA-1值来校验。索引文件用识别码列出相关的blob文件以及别的数据。对于提交来说,以树(tree)的形式存储,同样用对应的哈希值识别。树对应着工作目录中的文件夹,树中包含的树或者blob对象对应着相应的子目录和文件。每次提交都存储下它的上以级树的识别码。

如果用detached HEAD提交,那么最后一次提交会被the reflog for HEAD引用。但是过一段时间就失效,最终被回收,与git commit --amend或者git rebase很像。


参考:

  1. Git的核心概念
  2. 图解Git
  3. Git教程
  4. 常用 Git 命令清单
  5. Git-Book
Comments
Write a Comment