主要内容:【Git 初始化】、【Git 暂存区】、【Git 对象】
Git 初始化
设置一下 Git 的环境变量,这个设置是一次性的工作。即这些设置会在全局文件(用户主目录下的 ~/.gitconfig
)或系统文件(/etc/gitconfig
)中做永久的记录。
配置的用户名和邮件地址将在版本库提交时作为提交者的用户名和邮件地址。
git config --global user.name "Jiang Xin" |
设置一些 Git 别名,以便可以使用更为简洁的子命令
只在本用户的全局配置中添加 Git 命令别名:
git config --global alias.br branch |
版本库的初始化
mkdir demo |
初始化空的 Git 版本库于 /path/to/my/workspace/demo/.git/
git init 命令的后面直接输入目录名称
cd /path/to/my/workspace |
ls -aF |
这个隐藏的 .git
目录就是 Git 版本库(又叫仓库,repository)。
.git
版本库目录所在的目录,即 /path/to/my/workspace/demo
目录称为 工作区。
将新建立的文件添加到版本库
git add welcome.txt |
再执行一次提交操作,使用 -m
参数直接给出了提交说明。
git ci -m "initialized" |
git ci
是上面配置的别名,我个人觉得 -s
这个参数比较冗余。
思考:为什么工作区下有一个 .git
目录?
Git 的这种设计,将版本库放在工作区根目录下,所有的版本控制操作(除了和其他远程版本库之间的互操作)都在本地即可完成,不像 Subversion 只有寥寥无几的几个命令才能脱离网络执行。而且 Git 也没有 CVS 和 Subversion 的安全泄漏问题(只要保护好 .git 目录),也没有 Subversion 在本地文件搜索时出现搜索结果混乱的问题,甚至 Git 还提供了一条 git grep
命令来更好地搜索工作区的文件内容。
git grep "工作区文件内容搜索" |
当工作区中包含了子目录,在子目录中执行 Git 命令时,如何定位版本库呢?
当在 Git 工作区目录下执行操作的时候,会对目录依次向上递归查找 .git
目录,找到的 .git
目录就是工作区对应的版本库,.git
所在的目录就是工作区的根目录,文件 .git/index
记录了工作区文件的状态(实际上是 暂存区 的状态)。
如果跟踪一下执行 git status
命令时的磁盘访问,会看到沿目录依次向上递归的过程。
strace -e 'trace=file' git status |
那么有什么办法知道 Git 版本库的位置,以及工作区的根目录在哪里呢?
显示版本库 .git
目录所在的位置。
git rev-parse --git-dir |
显示工作区根目录。
git rev-parse --show-toplevel |
把版本库 .git
目录放在工作区,是不是太不安全了?
Git 克隆可以降低因为版本库和工作区混杂在一起导致的版本库被破坏的风险。在本机另外的磁盘/目录中建立 Git 克隆,并在工作区有改动提交时,手动或自动地执行向克隆版本库的推送 git push
操作。如果使用网络协议,还可以实现在其他机器上建立克隆,这样就更安全了(双机备份)。
思考:git config
命令参数的区别?
将打开 /path/to/my/workspace/demo/.git/config 文件进行编辑:
cd /path/to/my/workspace/demo/ |
将打开 /home/jiangxin/.gitconfig(用户主目录下的 .gitconfig 文件)全局配置文件进行编辑:
git config -e --global |
将打开 /etc/gitconfig 系统级配置文件进行编辑:
git config -e --system |
Git 的三个配置文件分别是 版本库级别的配置文件
、全局配置文件
(用户主目录下)和 系统级配置文件
(/etc 目录下)。
其中 版本库级别配置文件
的优先级最高,全局配置文件
其次,系统级配置文件
优先级最低。
Git 配置文件采用的是 INI 文件格式。
cat /path/to/my/workspace/demo/.git/config |
例如读取 [core]
小节的 bare
的属性值
git config <section>.<key> |
更改或设置 INI 文件中某个属性
git config <section>.<key> <value> |
打开 .git/config
文件
[a] |
可以用 git config
命令操作任何其他的 INI 文件
GIT_CONFIG=test.ini git config a.b.c.d "hello, world" |
GIT_CONFIG=test.ini git config a.b.c.d |
思考:是谁完成的提交?
当最新的提交删除了 user.name
和 user.email
,提交时 Git 对提交者的用户名和邮件地址做了大胆的猜测,这个猜测可能是错的。
重新设置 user.name
和 user.email
,然后执行下面的命令,重新修改最新的提交,改正作者和提交者的错误信息。
git commit --amend --allow-empty --reset-author |
- 参数
--amend
是对刚刚的提交进行修补,这样就可以改正前面错误的提交(用户信息错误),而不会产生另外的新提交。 - 参数
--allow-empty
是因为要进行修补的提交实际上是一个空白提交,Git 默认不允许空白提交。 - 参数
--reset-author
的含义是将 Author(提交者)的 ID 重置,否则只会影响最新的 Commit(提交者)的 ID。这条命令也会重置 AuthorDate 信息。
思考:随意设置提交者姓名,是否太不安全?
Git 可以随意设置提交的用户名和邮件地址信息,这是分布式版本控制系统的特性使然,每个人都是自己版本库的主人,很难也没有必要进行身份认证从而使用经过认证的用户名作为提交的用户名。
但是可以使用 GitLab 等服务管理权限。
思考:命令别名是干什么的?
命令别名可以帮助用户解决从其他版本控制系统迁移到 Git 后的使用习惯问题。
备份本章的工作成果
cd /path/to/my/workspace |
Git 暂存区
git log --stat |
可以用 git log
查看提交日志,附加的 --stat
参数看到每次提交的文件变更统计。
修改不能直接提交?
现在就将修改的文件“添加”到提交暂存区:
git add welcome.txt |
这时如果和 HEAD(当前版本库的头指针)或者 master 分支(当前工作分支)进行比较,会发现有差异。这个差异才是正常的,因为尚未真正提交么。
git diff HEAD |
用简洁方式显示状态
git status -s |
通过参数 --cached
或者 --staged
参数调用 git diff 命令,看到的是提交暂存区 stage
和版本库中文件的差异。不然看到的是工作区的变动。
git diff --cached |
现在执行 git commit 命令进行提交。
git commit -m "which version checked in?" |
如何证明提交成功了呢?通过查看提交日志,看到了新的提交。
git log --pretty=oneline |
理解 Git 暂存区 stage
当执行 git status
命令(或者 git diff
命令)扫描工作区改动的时候,先依据 .git/index
文件中记录的(工作区跟踪文件的)时间戳、长度等信息判断工作区文件是否改变。
文件 .git/index
实际上就是一个包含文件索引的目录树,像是一个虚拟的工作区。在这个虚拟工作区的目录树中,记录了文件名、文件的状态信息(时间戳、文件长度等)。文件的内容并不存储其中,而是保存在 Git 对象库 .git/objects
目录中,文件索引建立了文件和对象库中对象实体之间的对应。
- 图中可以看出此时 HEAD 实际是指向 master 分支的一个“游标”。所以图示的命令中出现 HEAD 的地方可以用 master 来替换。
- 图中的 objects 标识的区域为 Git 的对象库,实际位于
.git/objects
目录下,会在后面的章节重点介绍。 - 当执行
git reset HEAD
命令时,暂存区的目录树会被重写,被 master 分支指向的目录树所替换,但是工作区不受影响。 - 当执行
git rm --cached <file>
命令时,会直接从暂存区删除文件,工作区则不做出改变。 - 当执行
git checkout .
或者git checkout -- <file>
命令时,会用暂存区全部或指定的文件替换工作区的文件。这个操作很危险,会清除工作区中未添加到暂存区的改动。 - 当执行
git checkout HEAD .
或者git checkout HEAD <file>
命令时,会用 HEAD 指向的 master 分支中的全部或者部分文件替换暂存区和以及工作区中的文件。这个命令也是极具危险性的,因为不但会清除工作区中未提交的改动,也会清除暂存区中未提交的改动。
Git diff 魔法
有什么办法能够像查看工作区一样的,直观的查看暂存区以及 HEAD 当中的目录树么?
git ls-tree -l HEAD |
要显示暂存区的目录树,可以使用 git ls-files
命令。
git ls-files -s |
不要使用 git commit -a
提交命令 git commit
可以带上 -a
参数,对本地所有变更的文件执行提交操作,包括本地修改的文件,删除的文件,但不包括未被版本库跟踪的文件。
这个“偷懒”的提交命令,就会丢掉 Git 暂存区带给用户最大的好处:对提交内容进行控制的能力。
Git 对象
什么是 HEAD
?什么是 master
?为什么它们二者可以相互替换使用?为什么 Git 中的很多对象像提交、树、文件内容等都用 40 位的 SHA1
哈希值来表示?
Git 对象库探秘
40 位十六进制数字组成的 SHA1
哈希值
git log -1 --pretty=raw |
研究 Git 对象 ID 的命令是 git cat-file
,用下面的命令可以查看一下这三个 ID 的类型。
git cat-file -t e695606 |
再用 git cat-file
命令查看一下这几个对象的内容。对于 blob
对象,这个对象保存着文件 welcome.txt 的内容。
git cat-file -p fd3c06 |
这个写对象都存在 Git 库中的 objects
目录下,ID 的前两位作为目录名,后 38 位作为文件名。
for id in e695606 f58da9a a0c641e fd3c069; do \ |
HEAD 和 master 的奥秘
git log -1 HEAD |
在当前版本库中,HEAD
、master
和 refs/heads/master
具有相同的指向。现在到版本库 .git
中一探它们的究竟:
find .git -name HEAD -o -name master |
显示一下 .git/HEAD
cat .git/HEAD |
指向一个引用:refs/heads/master
cat .git/refs/heads/master |
显示该提交的内容
git cat-file -p e695606fc5e31b2ff9038a48a3d363f4c21a3d86 |
原来分支 master
指向的是一个提交 ID(最新提交)。
这样的分支实现是多么的巧妙啊:既然可以从任何提交开始建立一条历史跟踪链,那么用一个文件指向这个链条的最新提交,那么这个文件就可以用于追踪整个提交历史了。
这个文件就是 .git/refs/heads/master
文件。
目录 .git/refs
是保存引用的命名空间,其中 .git/refs/heads
目录下的引用又称为分支。对于分支既可以使用正规的长格式的表示法,如 refs/heads/master
,也可以去掉前面的两级目录用 master
来表示。Git 有一个底层命令 git rev-parse
可以用于显示引用对应的提交 ID。
问题:SHA1 哈希值到底是什么,如何生成的?
哈希(hash)是一种数据摘要算法(或称散列算法),是信息安全领域当中重要的理论基石。该算法将任意长度的输入经过散列运算转换为固定长度的输出。固定长度的输出可以称为对应的输入的数字摘要或哈希值。
echo -n Git |sha1sum |
提交的 SHA1 哈希值生成方法:
git cat-file commit HEAD | wc -c |
文件内容的 SHA1 哈希值生成方法:
# 文件总共包含 25 字节的内容。 |
树的 SHA1 哈希值的形成方法:
# HEAD对应的树的内容共包含39个字节。 |
问题:为什么不用顺序的数字来表示提交?
集中式版本控制系统因为只有一个集中式的版本库,可以很容易的实现依次递增的全局唯一的提交号。Git 作为分布式版本控制系统,开发可以是非线性的。这就要求提交的编号不能仅仅是本地局部有效,而是要“全球唯一”。
采用部分的 SHA1 哈希值。不必写全 40 位的哈希值,只采用开头的部分,不和现有其他的冲突即可。
使用 master
代表分支 master
中最新的提交,使用全称 refs/heads/master
亦可。
使用 HEAD
代表版本库中最近的一次提交。
符号 ^
可以用于指代父提交。例如:
HEAD^
代表版本库中上一次提交,即最近一次提交的父提交。HEAD^^
则代表HEAD^
的父提交。
对于一个提交有多个父提交,可以在符号 ^
后面用数字表示是第几个父提交。例如:
a573106^2
含义是提交a573106
的多个父提交中的第二个父提交。HEAD^1
相当于HEAD^
含义是 HEAD 多个父提交中的第一个。HEAD^^2
含义是HEAD^
(HEAD 父提交)的多个父提交中的第二个。
符号 ~<n>
也可以用于指代祖先提交。效果等同:
a573106~5 |
提交所对应的树对象:a573106^{tree}
某一此提交对应的文件对象:a573106:path/to/file
暂存区中的文件对象::path/to/file
git rev-parse HEAD |