Git 是最流行的版本管理工具,也是程序员的必备技能之一。html
即便每天使用它,不少人也未必了解它的原理。Git 为何能够管理版本?git add
、git commit
这些基本命令,到底在作什么,你说得清楚吗?git
首先,让咱们建立一个项目目录,并进入该目录。程序员
$ mkdir git-demo-project $ cd git-demo-project
咱们打算对该项目进行版本管理,第一件事就是使用git init
命令,进行初始化。
git init
命令只作一件事,就是在项目根目录下建立一个子目录,用来保存版本信息。$ git initgit init.git
$ ls .git branches/ config description HEAD hooks/ info/ objects/ refs/
接下来,新建一个空文件test.txt
。test.txt
而后,把这个文件加入 Git 仓库,也就是为的当前内容建立一个副本。$ touch test.txt test.txt
$ git hash-object -w test.txt e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
上面代码中,git hash-object
命令把test.txt
的当前内容压缩成二进制文件,存入 Git。压缩后的二进制文件,称为一个 Git 对象,保存在.git/objects
目录。
这个命令还会计算当前内容的 SHA1 哈希值(长度40的字符串),做为该对象的文件名。下面看一下这个新生成的 Git 对象文件。
git hash-objecttest.txt.git/objects
$ ls -R .git/objects .git/objects/e6: 9de29bb2d1d6434b8b29ae775ad8c2e48c5391
上面代码能够看到,.git/objects
下面多了一个子目录,目录名是哈希值的前2个字符,该子目录下面有一个文件,文件名是哈希值的后38个字符。bash
再看一下这个文件的内容。工具
$ cat .git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391
上面代码输出的文件内容,都是一些二进制字符。你可能会问,test.txt
是一个空文件,为何会有内容?这是由于二进制对象里面还保存一些元数据。spa
若是想看该文件原始的文本内容,要用git cat-file
命令。指针
$ git cat-file -p e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
由于原始文件是空文件,因此上面的命令什么也看不到。如今向test.txt
写入一些内容。code
$ echo 'hello world' > test.txt
由于文件内容已经改变,须要将它再次保存成 Git 对象。htm
$ git hash-object -w test.txt 3b18e512dba79e4c8300dd08aeb37f8e728b8dad
上面代码能够看到,随着内容改变,test.txt
的哈希值已经变了。同时,新文件.git/objects/3b/18e512dba79e4c8300dd08aeb37f8e728b8dad
也已经生成了。如今能够看到文件内容了。对象
$ git cat-file -p 3b18e512dba79e4c8300dd08aeb37f8e728b8dad hello world
文件保存成二进制对象之后,还须要通知 Git 哪些文件发生了变更。全部变更的文件,Git 都记录在一个区域,叫作"暂存区"(英文叫作 index 或者 stage)。等到变更告一段落,再统一把暂存区里面的文件写入正式的版本历史。
git update-index
命令用于在暂存区记录一个发生变更的文件。
$ git update-index --add --cacheinfo 100644 \ 3b18e512dba79e4c8300dd08aeb37f8e728b8dad test.txt
上面命令向暂存区写入文件名test.txt
、二进制对象名(哈希值)和文件权限。
git ls-files
命令能够显示暂存区当前的内容。
$ git ls-files --stage 100644 3b18e512dba79e4c8300dd08aeb37f8e728b8dad 0 test.txt
上面代码表示,暂存区如今只有一个文件test.txt
,以及它的二进制对象名和权限。知道了二进制对象名,就能够在.git/objects
子目录里面读出这个文件的内容。
git status
命令会产生更可读的结果。
$ git status
要提交的变动:
新文件: test.txt
上面代码表示,暂存区里面只有一个新文件test.txt
,等待写入历史。
上面两步(保存对象和更新暂存区),若是每一个文件都作一遍,那是很麻烦的。Git 提供了git add
命令简化操做。
$ git add --all
上面命令至关于,对当前项目全部变更的文件,执行前面的两步操做。
暂存区保留本次变更的文件信息,等到修改了差很少了,就要把这些信息写入历史,这就至关于生成了当前项目的一个快照(snapshot)。
项目的历史就是由不一样时点的快照构成。Git 能够将项目恢复到任意一个快照。快照在 Git 里面有一个专门名词,叫作 commit,生成快照又称为完成一次提交。
下文全部提到"快照"的地方,指的就是 commit。
首先,设置一下用户名和 Email,保存快照的时候,会记录是谁提交的。
$ git config user.name "用户名" $ git config user.email "Email 地址"
接下来,要保存当前的目录结构。前面保存对象的时候,只是保存单个文件,并无记录文件之间的目录关系(哪一个文件在哪里)。
git write-tree
命令用来将当前的目录结构,生成一个 Git 对象。
$ git write-tree c3b8bb102afeca86037d5b5dd89ceeb0090eae9d
上面代码中,目录结构也是做为二进制对象保存的,也保存在.git/objects
目录里面,对象名就是哈希值。
让咱们看一下这个文件的内容。
$ git cat-file -p c3b8bb102afeca86037d5b5dd89ceeb0090eae9d 100644 blob 3b18e512dba79e4c8300dd08aeb37f8e728b8dad test.txt
能够看到,当前的目录里面只有一个test.txt
文件。
所谓快照,就是保存当前的目录结构,以及每一个文件对应的二进制对象。上一个操做,目录结构已经保存好了,如今须要将这个目录结构与一些元数据一块儿写入版本历史。
git commit-tree
命令用于将目录树对象写入版本历史。
$ echo "first commit" | git commit-tree c3b8bb102afeca86037d5b5dd89ceeb0090eae9d c9053865e9dff393fd2f7a92a18f9bd7f2caa7fa
上面代码中,提交的时候须要有提交说明,echo "first commit"
就是给出提交说明。而后,git commit-tree
命令将元数据和目录树,一块儿生成一个 Git 对象。如今,看一下这个对象的内容。
$ git cat-file -p c9053865e9dff393fd2f7a92a18f9bd7f2caa7fa tree c3b8bb102afeca86037d5b5dd89ceeb0090eae9d author ruanyf 1538889134 +0800 committer ruanyf 1538889134 +0800 first commit
上面代码中,输出结果的第一行是本次快照对应的目录树对象(tree),第二行和第三行是做者和提交人信息,最后是提交说明。
git log
命令也能够用来查看某个快照信息。
$ git log --stat c9053865e9dff393fd2f7a92a18f9bd7f2caa7fa commit c9053865e9dff393fd2f7a92a18f9bd7f2caa7fa Author: ruanyf Date: Sun Oct 7 13:12:14 2018 +0800 first commit test.txt | 1 + 1 file changed, 1 insertion(+)
Git 提供了git commit
命令,简化提交操做。保存进暂存区之后,只要git commit
一个命令,就同时提交目录结构和说明,生成快照。
$ git commit -m "first commit"
此外,还有两个命令也颇有用。
git checkout
命令用于切换到某个快照。
$ git checkout c9053865e9dff393fd2f7a92a18f9bd7f2caa7fa
git show
命令用于展现某个快照的全部代码变更。
$ git show c9053865e9dff393fd2f7a92a18f9bd7f2caa7fa
到了这一步,还没完。若是这时用git log
命令查看整个版本历史,你看不到新生成的快照。
$ git log
上面命令没有任何输出,这是为何呢?快照明明已经写入历史了。
原来git log
命令只显示当前分支的变更,虽然咱们前面已经提交了快照,可是尚未记录这个快照属于哪一个分支。
所谓分支(branch)就是指向某个快照的指针,分支名就是指针名。哈希值是没法记忆的,分支使得用户能够为快照起别名。并且,分支会自动更新,若是当前分支有新的快照,指针就会自动指向它。好比,master 分支就是有一个叫作 master 指针,它指向的快照就是 master 分支的当前快照。
用户能够对任意快照新建指针。好比,新建一个 fix-typo 分支,就是建立一个叫作 fix-typo 的指针,指向某个快照。因此,Git 新建分支特别容易,成本极低。
Git 有一个特殊指针HEAD
, 老是指向当前分支的最近一次快照。另外,Git 还提供简写方式,HEAD^
指向 HEAD
的前一个快照(父节点),HEAD~6
则是HEAD
以前的第6个快照。
每个分支指针都是一个文本文件,保存在.git/refs/heads/
目录,该文件的内容就是它所指向的快照的二进制对象名(哈希值)。
下面演示更新分支是怎么回事。首先,修改一下test.txt
。
$ echo "hello world again" > test.txt
而后,保存二进制对象。
$ git hash-object -w test.txt c90c5155ccd6661aed956510f5bd57828eec9ddb
接着,将这个对象写入暂存区,并保存目录结构。
$ git update-index test.txt $ git write-tree 1552fd52bc14497c11313aa91547255c95728f37
最后,提交目录结构,生成一个快照。
$ echo "second commit" | git commit-tree 1552fd52bc14497c11313aa91547255c95728f37 -p c9053865e9dff393fd2f7a92a18f9bd7f2caa7fa 785f188674ef3c6ddc5b516307884e1d551f53ca
上面代码中,git commit-tree
的-p
参数用来指定父节点,也就是本次快照所基于的快照。
如今,咱们把本次快照的哈希值,写入.git/refs/heads/master
文件,这样就使得master
指针指向这个快照。
$ echo 785f188674ef3c6ddc5b516307884e1d551f53ca > .git/refs/heads/master
如今,git log
就能够看到两个快照了。
$ git log
commit 785f188674ef3c6ddc5b516307884e1d551f53ca (HEAD -> master) Author: ruanyf Date: Sun Oct 7 13:38:00 2018 +0800 second commit commit c9053865e9dff393fd2f7a92a18f9bd7f2caa7fa Author: ruanyf Date: Sun Oct 7 13:12:14 2018 +0800 first commit
git log
的运行过程是这样的:
- 查找
HEAD
指针对应的分支,本例是master
- 找到
master
指针指向的快照,本例是785f188674ef3c6ddc5b516307884e1d551f53ca
- 找到父节点(前一个快照)
c9053865e9dff393fd2f7a92a18f9bd7f2caa7fa
- 以此类推,显示当前分支的全部快照
最后,补充一点。前面说过,分支指针是动态的。缘由在于,下面三个命令会自动改写分支指针。
git commit
:当前分支指针移向新建立的快照。git pull
:当前分支与远程分支合并后,指针指向新建立的快照。git reset [commit_sha]
:当前分支指针重置为指定快照。