1.Git 简介

Git是一种开源分布式版本控制系统(VCS),也是 Github 的核心。Git 具有简单,快速,高效,可扩展性好等优点,所以 Git 也是目前最受欢迎的 VCS。(Git 简史)

Git 提供了多种使用模式,桌面客户端或者是命令行模式,以方便用户进行项目版本管理,以及不同用户之间的协同项目处理。

命令行模式下,用户可以使用所有的命令操作实现 Git 全部功能,相较之下,GUI 模式只是 Git 简化版。

Git 本质上是一个内容寻址文件系统 (所以 Git 的核心部分是一个简单的键值对数据库)。通过对文件制作快照并保存其索引,以实现记录和追踪文件数据的修改变更,甚至包括数据撤销恢复等操作。

Git 中的快照,相当于一种备份,但是 Git 会对改写后的文件数据进行特殊压缩,打包,存储等处理以提升效率,节省空间。同时 Git 会根据文件内容生成校验和 (哈希值),并以此作为索引。改动文件时,Git 会根据校验和是否变化来判断是否生成新快照。否则只是保留链接指向之前的存储文件。(Git Book 里面关于 Git原理已经介绍的非常详细了,另外这篇博客总结的比较简洁)

2.初次安装与配置:

1
2
3
4
5
6
7
8
9
10
11
12
# 安装 Git
apt-get install git # install git on ubuntu

# 设置 Git config 变量
git config --global user.name "xxx" # 设置 user name 以及 email
git config --global user.email xxx@email.com

# 设置文本编辑器
git config --global core.editor vim # git commit提交命令会使用编辑器以记录每次提交的信息

# 查看 Git config 信息
git config --list

3.获取仓库 (Repository):

使用 Git 对项目或者文件进行版本管理时,需要先配置初始化仓库--repository 以存储各种项目文件.通常有两种方式:

1. 直接从本地文件夹建立repository

1
2
3
4
git init    # 将当前目录变成仓库,会在当前文件夹生成 ,git文件夹,其中包含了 repository 框架文件
git add .  # 保存快照,并添加文件索引信息至暂存区(索引)
git add LICENSE
git commit -m "init project version"    # 提交更新,生成 commit 对象

2. Clone 已有 repository 至本地

1
git clone  https://github.com/repository   # 拷贝远程仓库所有数据至本地

4.Git 环境基本概念:

     Git workflow

文件状态

Git 仓库下的文件要么处于已被追踪状态 (tracked),要么属于未被追踪状态(untracked),执行 git add,该文件就会被追踪,只有被追踪的文件 (tracked) 才会被纳入版本管理。被追踪的文件通常有几种状态: 已修改(modified),已暂存(staged),已提交(committed):

  • Modified — 表示文件已经被修改,但还未保存至仓库数据库。
  • Staged — 表示对已改动文件的当前版本做了记录,并进行了保存以便下次提交。
  • Commited — 表示文件已经永久的保存在本地仓库。

    未被追踪状态文件在 Git 环境下相当于被忽略状态。无论怎么改动文件,Git 并不会保存记录。
    通过 git rm 可以取消文件的被追踪状态或者直接删除文件,另外也可以利用.gitignore 文件,取消某类特定格式文件或者整个子文件夹内容的追踪。

仓库区域

根据仓库文件状态,可以引申出三个区域的概念: 工作目录(Working directory / Working Tree),暂存区(Index / Staging area),以及仓库目录(Git directory)

  • Working directory / Working Tree — 从Git数据库提取的项目某一个版本的数据。

    关于工作目录,Git book中文版不太好理解,按英文版的话,应该是指当前项目的某一个版本的内容 (one version of the project)。工作目录下修改完后的文件添加暂存并提交后,就会保存在 Git 数据库。多次提交就会保存多个版本,可以提取任意一个版本的数据至工作目录。直观上可以将 working directory 理解为当前建立本地仓库的文件夹(local repository)。

  • Index / Stage — 索引文件, 用于存储下一次待提交的文件索引信息(文件名,文件校验和等)。

  • Git directory — 用于存储项目所有元数据以及对象数据库。

    仓库目录即 整个 .git 文件夹,不过基本上本地修改文件,版本管理结果都存在对象数据库 (.git/objects 文件夹)
    暂存区就在仓库目录下,暂存区域实际上是一个文件 (.git/objects/index),存储了暂存文件列表信息(文件名,文件校验和等内容)。每次暂存操作会更新暂存文件的索引信息至该区域。

    

因此,简单来讲,Git 基本工作流程包括:

  1. 修改操作: 在工作区修改文件  
  2. 暂存操作: 保存文件快照,并更新文件索引信息至暂存区
  3. 提交操作: 提交版本更新,永久存储至 Git 仓库中的对象数据库

5.Git 核心操作原理:

在此之前需要先说明几个基本的概念:

  • Git 对象 — 主要包括数据对象(blob object), 树对象 (tree object), 以及提交对象 (commit object)。Git 内部就是通 过创建一系列 Git 对象来实现版本的更新和记录。

    blob object 对象用来存储文件内容; tree object 用来存储文件名,校验和等信息; commit object 用来存储提交的内容,包括树对象指针,父提交对象指针,作者信息,提交注释信息。
    数据对象、树对象、提交对象——均以单独文件的形式保存在 .git/objects 目录下。

  • SHA-1 校验和 — 40 位字符, Git 内部根据对象类型和文件内容进行 SHA-1 校验运算得到的哈希值,作为文件保存地址。

  • 指针 — 在 Git 环境下指针就是校验和,SHA-1 哈希值,是 Git 对象保存的地址。

    所以通过校验和(指针)就能找到保存的文件对象,文件内容发生变化时,指针也会相应变化。
    因此在 Git 环境下,可以说一个文件的指针,校验和,哈希值都是同样的意思,都是指文件在 Git 仓库保存的地址。
    Git 内部文件存储都是通过校验和机制,即先计算文件内容校验和,再以校验和为地址来保存文件

Git 管理版本更新时,核心操作包括暂存操作(Git add)和提交操作 (Git commit)。Git 所做的实质工作——对被追踪的文件生成校验和,将改写的文件内容保存为数据对象 (blob object),更新暂存区,记录树对象 (tree object),最后创建提交对象 (commit object)。

5.1 Git add — 暂存操作

暂存操作(git add)时, Git 对待暂存的每个文件计算校验和(40个字符的SHA-1 哈希值),并将文件保存成 blob 对象,最后将文件索引信息(文件名,校验和等)更新至暂存区 (index)。

因此,暂存区包含的文件索引信息实际上是: 每个暂存文件的文件模式(参考 linuc chmod),文件类型 (blob, tree, 或者 commit),文件校验和,文件名。
文件校验和对应文件在 Git 仓库数据库保存的地址,文件名其实对应的文件在工作目录下的地址,因此,通过索引文件,就可以找到每个文件保存在仓库数据库的 blob 对象,同时,根据文件名恢复 blob 对象至工作目录。

此时,可以在 .git/objects 目录下看到生成对应的子文件夹, 每个子文件夹含一个存储文件,表示一个被保存的 blob 数据。对于每个保存的内容, Git生成的 40 位字符校验和, 前 2 位作为文件目录名, 后 38 位作为目录里的文件名。

通常每个文件被暂存时,.git/objects 都会生成一个对应子文件夹,以及包含在内的 blog 对象。但是如果文件没有被修改,或者两个同类型文件包含相同内容,Git 都只会保存一个相同的 blob 对象。除非其内容发生变化,才会生成新的 blob 对象。

5.2 Git commit — 提交操作

在进行提交操作 (git commit) 时,Git 会生成树对象 (tree object),提交对象(commit object)并对其进行保存。其中, tree object 储存了当前暂存区文件索引信息; commit object 内部包含了当前树对象指针,上一次提交对象指针,以及提交信息 (git status 内容),作者信息。(这部分内容是 Gir Pro book 3.1 以及 10.2章节)。

树对象其实有点类似 linux 中的目录项,记录了工作区目录下的被暂存文件索引信息。树对象记录的内容跟 index 里的内容相近,但是树对象中的一条内容可以是一个文件的索引信息,也可以是一个文件夹的索引信息,并采用 blob 或者 tree 进行标记区分。所以一个树对象可以包含一个子树对象。
顶层树对应工作区根目录项,如果根目录还有子目录,那么就会生成一个子树对象,子树对象包含子目录里的文件索引信息。同时子树对象索引信息被保存在顶层树对象。

1
2
3
4
$ git cat-file -p d8bd5ddf4d9ec46bf5c4c1d30f3771143c5c5bfe   # tree 对象, 其中 new_filefolder 是子目录
100644 blob 99d2ea6b6b4b5efa58cc270524193118625921ec index.txt
040000 tree 9cb543fcd992ed55915adcaef3d77f8001d8dae7 new_filefolder
100644 blob 7fd7b19585f7ffcda9ce662b0e535ac21a2ac6b1 test.txt

准确说法应该是,commit object 包括: 指向顶层树对象的指针, 父提交对象指针, 作者信息,以及提交时的注释信息。

图片参考 how git work

5.3 底层工作原理

执行 git add 时,Git 内部通过生成 blob 对象来保存文件内容, 每次对文件修改之后,都会保存一个 blob 对象。在此过程中存在两个问题: 

  1. 文件每次改动都会保存一个版本,不可能记住所有历史版本对应的文件索引信息;
  2. 每次保存只是文件内容,虽然暂存区临时记录了文件索引信息,但是每次改动都会被更新掉,因此没有真正保存文件名等信息。

对此,Git 内部采用构建树对象的方式来解决文件索引信息保存的问题,以及将多个文件索引信息组织到一起。
Git 根据某一时刻暂存区(index)所表示的状态创建并记录一个对应的树对象. (所以这也是为什么 git commit 之前需要先执行 git add)。

git add 操作其实相当于一种缓冲操作,Git 内部也可以直接通过 git commit -a 一次性提交保存所有被追踪内容。但是不如使用 git add 更具灵活性,比如只需要提交部分文件。

具体地,通过 Git 底层命令来研究内部工作原理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 第一次文件改动操作
$ echo 'version1' > test.txt # 写入内容至 test.txt

# 第一次暂存操作 step 1 --- 生成 blob 对象
$ git hash-object -w test.txt # 将修改后的文件保存成 blob 对象, 该命令返回一个40位字符的哈希值(校验和)
                    此时会在 .git/objects 目录下生成一个子文件夹,文件夹名为校验和字符的前 2 位,
                    文件夹下的 blob 对象文件名为校验和后 38 位

5bdcfc19f119febc749eef9a9551bc335cb965e2 # 此时子文件夹名为'5b',其中的文件名为余下的 38 位字符

# 第一次暂存操作 step 2 --- 更新索引区
# 这里将第一次改动操作保存的文件校验和(blob 对象地址),文件名,文件模式存至 index
$ git update-index --add --cacheinfo 10064 5bdcfc19f119febc749eef9a9551bc335cb965e2 test.txt

# 第一次提交操作 step 1 --- 构建树对象
$ git write-tree # 将 index 内容写入 tree 对象, 该命令同样返回40位字符的校验和
c89f1e026cabe89e7e80a139544a9ae5efc9b470   # 同样的,会以校验和命名子文件夹和文件,并保存至.git/objects 目录

$ git cat-file -p c89f1e026cabe89e7e80a139544a9ae5efc9b470  # 查看该 tree 对象可以发现其中包含了
100644 blob 5bdcfc19f119febc749eef9a9551bc335cb965e2 test.txt   文件模式, 类型, 校验和, 文件名信息

# 第一次提交操作 step 2 --- 生成提交对象
$ echo 'first commit' | git commit-tree c89f1e # 将树对象以及提交信息写入提交对象
73fe6c400b3e46adb03b1c035e85d6625cbaba31

$ git cat-file -p 73fe6c400b3e46adb03b1c035e85d6625cbaba31 # 查看 commit 对象, 其中包含
tree c89f1e026cabe89e7e80a139544a9ae5efc9b470             tree 对象指针,作者信息,提交者信息
author Zakiyi <yishon555@outlook.com> 1555512144 +0800      以及提交信息
committer Zakiyi <yishon555@outlook.com> 1555512144 +0800

first commit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 第二次改动操作
# 写入新的内容至 test.txt,并添加新文件 index.txt,同时写入内容
$ echo 'version2' > test.txt     # 再次修改 test.txt 内容
$ echo 'version1 index' > index.txt # 添加新文件 index.txt,并写入内容

# 第二次暂存操作 step 1 --- 生成 blob 对象
$ git hash-object -w test.txt   # 将修改的 test.txt 文件保存成 blob 对象
df7af2c382e49245443687973ceb711b2b74cb4a

$ git hash-object -w index.txt    # 将新添加的 index.txt 保存成 blob 对象
01dde182b1618e0449b63071ac62c5df0adf9358 

# 第二次暂存操作 step 2 --- 更新索引文件
# 两个改动文件的信息添加至索引区 
$ git update-index --add --cacheinfo 100644 df7af2c382e49245443687973ceb711b2b74cb4a test.txt
$ git update-index --add --cacheinfo 100644 01dde182b1618e0449b63071ac62c5df0adf9358 index.txt

# 第二次提交操作 step 1 --- 生成树对象
$ git write-tree
e7b27cffc040f6ea742670afc4ae145ef60cbff1

$ git cat-file -p e7b27cffc040f6ea742670afc4ae145ef60cbff1 # 查看新的树对象内容
100644 blob 01dde182b1618e0449b63071ac62c5df0adf9358 index.txt
100644 blob df7af2c382e49245443687973ceb711b2b74cb4a test.txt

# 第二次提交操作 step 2 --- 生成提交对象
$ echo 'second commit' | git commit-tree e7b27c -p 73fe6c4      # 第二次提交需要指定上次提交对象指针
f9f76c463ab290ac4aa7dd3eeca7d7e9e1e79f27

$ git cat-file -p f9f76c463ab290ac4aa7dd3eeca7d7e9e1e79f27      # 查看第二次提交对象内容
tree e7b27cffc040f6ea742670afc4ae145ef60cbff1
parent 73fe6c400b3e46adb03b1c035e85d6625cbaba31
author Zakiyi <yishon555@outlook.com> 1555594132 +0800
committer Zakiyi <yishon555@outlook.com> 1555594132 +0800

second commit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 第三次改动操作
# 当前工作区建立子文件夹 new_filefolder, 在其中添加 subtest.txt 并写入内容
$ mkdir new_filefolder
$ echo 'version1 subtest' > new_filefolder/subtest.txt

# 第三次暂存操作 step 1 --- 生成 blob 对象
$ git hash-object -w new_filefolder/subtest.txt
a0f53bfeaae48f55f3015e4cdafa2561af594b28

# 第三次暂存操作 step 2 --- 更新索引文件
$ git update-index --add --cacheinfo 100644 a0f53bfeaae48f55f3015e4cdafa2561af594b28\
new_filefolder/subtest.txt      # 添加()工作目录)子文件夹下文件信息至暂存区时,需要指明子文件夹名
如果这里不加'new_filefolder', 生成的树对象就不会有子树对象

# 第三次提交操作 step 1 --- 生成树对象
$ git write-tree                 # 由于添加了子文件夹内容,因此会生成子树对象,.git/objects 下会
                            保存两个文件
f45d55ce40612653c0a048e354561872307255f3

$ git cat-file -p f45d55ce40612653c0a048e354561872307255f3      # 查看新的树对象内容
100644 blob 01dde182b1618e0449b63071ac62c5df0adf9358 index.txt
040000 tree 9cb543fcd992ed55915adcaef3d77f8001d8dae7 new_filefolder # 新增的子树对象信息
100644 blob df7af2c382e49245443687973ceb711b2b74cb4a test.txt

# 第三次提交操作 step 2 --- 生成提交对象
$ echo 'third commit' | git commit-tree f45d55 -p f9f76c4
aa540b90ac49fff6a9d4abe175d4974f5ef7bec2

$ git cat-file -p aa540b90ac49fff6a9d4abe175d4974f5ef7bec2       # 查看第三次提交对象内容
tree f45d55ce40612653c0a048e354561872307255f3
parent f9f76c463ab290ac4aa7dd3eeca7d7e9e1e79f27
author Zakiyi <yishon555@outlook.com> 1555597319 +0800
committer Zakiyi <yishon555@outlook.com> 1555597319 +0800

third commit

可以发现每次文件改动 Git 都保存一个新的版本数据,其实是比较浪费空间。 为了解决这一问题,Git 最初向磁盘中存储对象时所使用的格式被称为“松散(loose)”对象格式。 但是,Git 会时不时地将多个这些对象打包成一个称为“包文件(packfile)”的二进制文件,以节省空间和提高效率。 (10.4章节)

总结就是,暂存操作时,生成 blob 对象,保存文件数据; 提交操作时,生成 tree 对象保存文件索引信息(文件名,校验和等),同时生成 commit 对象(包含当前 tree 对象指针,上一次 commit 对象指针,以及作者信息,提交注释信息)。
所以,通过当前 commit 对象,根据其中 tree 对象中的文件索引信息就能找到过去每一次提交的文件内容快照(blob 对象),从而实现版本数据的恢复和更新。

6.Git 引用和分支:

6.1 Git 引用

每次提交版本修改,Git 都会生成对应的提交对象。当多次提交后,为了方便定位某个提交对象,Git 提出了引用的概念,即建立引用文件 (references,或缩写为 refs) 来保存提交对象校验和,同时以引用文件命名来替代校验和,作为提交对象指针,或者说最为该提交对象别名。

可以在 .git/refs 目录下找到这类含有 SHA-1 值的文件。 gir branch branchname 就是创建一个引用文件,并将最新提交对象校验和内容写进该文件。

1
$ find .git/refs

1
2
3
4
# 创建引用
$ echo "1a410efbd13591db07496601ebc7a059dd55cfe9" > .git/refs/heads/master
# 更新引用
$ git update-ref refs/heads/master 1a410efbd13591db07496601ebc7a059dd55cfe9

除了普通分支引用外,其他特殊引用类型: HEAD 引用,标签引用,远程引用

  • HEAD 引用 — 如何知道最新提交对象的校验和(SHA-1 值)呢? 答案就是 Head 引用,在.git/HEAD 文件中指明了最新提交对象的引用。
  • 标签引用 — 一个固定的引用,指向给定的 Git 对象, 在 .git/refs/tags 目录下。
  • 远程引用 — 远程引用是对远程仓库的引用(指针),远程引用是只读的。(.git/refs/remotes 目录下)

6.2 Git 分支:

Git 的分支,其实本质上仅仅是指向提交对象的可变指针(引用),Git 的默认分支名字是 master,每次提交操作时会自动更新该分支引用。

Git 的分支实质上仅是包含所指对象校验和(长度为 40 的 SHA-1 值字符串)的文件,所以它的创建和销毁都异常高效。 创建一个新分支就相当于往一个文件中写入 41 个字节(40 个字符和 1 个换行符)。在 .git\refs\heads 文件夹下, 可以看到默认分支 master 引用文件,其中包含最新一次提交对象的校验和。

当存在远程仓库时,通常做法会在本地设置远程跟踪分支 (Remote-tracking branch) 来指示远程分支的状态,其功能类似与远程分支的书签。被追踪的分支称之为上游分支 (upstream branch)。

.git/refs/remotes/origin 文件夹下可以看到 HEAD, mater 等文件,其表示的是远程仓库的 HEAD 引用, mster 等分支引用。

6.2.1 分支基本操作:
  • 新建 & 切换 — 通过 git branch 命令可以新建分支,该分支指向当前最新提交对象。当存在多个分支时,通过 git checkout 命令可以切换指定分支。

  • 删除 & 合并 — git branch -d 以及 git merge 可以实现分支的删除与合并,比如需要修改某个问题时,可以在默认分支 master上 新建一个分支 hotfix,改完提交后,将其部署至线上分支 master 进行合并。

1
2
3
4
5
6
7
8
创建分支:
git branch testing     # 该命令创建新分支 testing,指针指向当前最新提交对象

切换分支:
git checkout testing

新建以及同时切换分支
$ git checkout -b testing
1
2
3
4
$ git checkout master

$ git merge hotfix # 合并分支
$ git branch -d hotfix # 删除 hotfix 分支

注意,如果在两个分支中,对同一个文件的同一个部分进行了不同的修改,Git 会出现合并失败,此时需要手动去解决。

6.2.2 远程分支操作:

远程分支操作主要包括:设置远程跟踪分支,同步,推送

  • 设置远程跟踪分支 — 将本地仓库分支与本地的远程跟踪分支相关联
1
2
3
4
5
6
7
8
9
10
$ git clone     # Git 会自动将本地 master 分支与远程 master 分支进行关联

# 本地仓库分支已存在,指定其上游跟踪分支
$ git branch local_branch -u remote_name/remote_branch # 指定local branch 的远程跟踪分支

# 本地仓库分支不存在时,创建指定新分支,并指定其上游跟踪分支
$ git checkout -b local_branch remotename/remote_branch # 新建本地分支,同时指定远程跟踪分支

# 或者直接简化
$ git checkout --track remote_name/remote_branch # 新建本地分支,命名与其指定远程跟踪分支相同
  • 拉取远程分支 — 拉取远程仓库数据至本地,并更新本地仓库数据库
1
2
3
4
git fetch remote_name   # 拉取远程库数据至本地数据库,同时更新本地远程跟踪分支状态,即更新 .git/refs/remotes 引用文件信息

git pull remote_name  # 拉取远程数据库数据,并合并分支,注意当存在多个跟踪分支时,需要再次执行 git pull, 以合并本地其他分支
                与其他对应的上游跟踪分支
  • 推送远程分支
1
2
3
4
5
# 本地与远程仓库相同分支推送
git push origin local_branch    # 本地仓库分支 local_branch 推送至相同名的远程仓库分支

# 本地仓库与远程仓库不同分支推送
git push origin local_branch:remote_branch    # 推送本地仓库指定分支至远程仓库指定分支

总结:

  • 引用是提交对象的指针或者别名,在 .git/refs 文件夹下以文件形式存在,文件内容包含了提交对象的校验和。
  • 分支是可变的引用,可以对其进行删除合并切换等操作,以实现版本数据的修改与管理。
  • 远程跟踪分支是建立在本地仓库的远程分支,用于跟踪记录远程分支状态。