1.Git 简介
Git是一种开源分布式版本控制系统(VCS),也是 Github 的核心。Git 具有简单,快速,高效,可扩展性好等优点,所以 Git 也是目前最受欢迎的 VCS。(Git 简史)
Git 提供了多种使用模式,桌面客户端或者是命令行模式,以方便用户进行项目版本管理,以及不同用户之间的协同项目处理。
命令行模式下,用户可以使用所有的命令操作实现 Git 全部功能,相较之下,GUI 模式只是 Git 简化版。
Git 本质上是一个内容寻址文件系统 (所以 Git 的核心部分是一个简单的键值对数据库)。通过对文件制作快照并保存其索引,以实现记录和追踪文件数据的修改变更,甚至包括数据撤销恢复等操作。
Git 中的快照,相当于一种备份,但是 Git 会对改写后的文件数据进行特殊压缩,打包,存储等处理以提升效率,节省空间。同时 Git 会根据文件内容生成校验和 (哈希值),并以此作为索引。改动文件时,Git 会根据校验和是否变化来判断是否生成新快照。否则只是保留链接指向之前的存储文件。(Git Book 里面关于 Git原理已经介绍的非常详细了,另外这篇博客总结的比较简洁)
2.初次安装与配置:
1 | # 安装 Git |
3.获取仓库 (Repository):
使用 Git 对项目或者文件进行版本管理时,需要先配置初始化仓库--repository 以存储各种项目文件.通常有两种方式:
1. 直接从本地文件夹建立repository1
2
3
4git 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 仓库下的文件要么处于已被追踪状态 (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 基本工作流程包括:
- 修改操作: 在工作区修改文件
- 暂存操作: 保存文件快照,并更新文件索引信息至暂存区
- 提交操作: 提交版本更新,永久存储至 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 对象。在此过程中存在两个问题:
- 文件每次改动都会保存一个版本,不可能记住所有历史版本对应的文件索引信息;
- 每次保存只是文件内容,虽然暂存区临时记录了文件索引信息,但是每次改动都会被更新掉,因此没有真正保存文件名等信息。
对此,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 | # 第二次改动操作 |
1 | # 第三次改动操作 |
可以发现每次文件改动 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 | # 创建引用 |
除了普通分支引用外,其他特殊引用类型: 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 | 创建分支: |
1 | $ git checkout master |
注意,如果在两个分支中,对同一个文件的同一个部分进行了不同的修改,Git 会出现合并失败,此时需要手动去解决。
6.2.2 远程分支操作:
远程分支操作主要包括:设置远程跟踪分支,同步,推送
- 设置远程跟踪分支 — 将本地仓库分支与本地的远程跟踪分支相关联
1 | $ git clone # Git 会自动将本地 master 分支与远程 master 分支进行关联 |
- 拉取远程分支 — 拉取远程仓库数据至本地,并更新本地仓库数据库
1 | git fetch remote_name # 拉取远程库数据至本地数据库,同时更新本地远程跟踪分支状态,即更新 .git/refs/remotes 引用文件信息 |
- 推送远程分支
1 | # 本地与远程仓库相同分支推送 |
总结:
- 引用是提交对象的指针或者别名,在 .git/refs 文件夹下以文件形式存在,文件内容包含了提交对象的校验和。
- 分支是可变的引用,可以对其进行删除合并切换等操作,以实现版本数据的修改与管理。
- 远程跟踪分支是建立在本地仓库的远程分支,用于跟踪记录远程分支状态。