什么是 Git ?
Git 是一个开源的分布式 版本控制系统,可以快速地处理从很小到非常大的项目版本管理。版本控制是一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统。
Git 的来由?
Git 官网中有提到 --- Git - Git 简史 (git-scm.com)
Git 是由 Linux 之父 --- Linus torvalds 创作的,创作 git 是为了管理 Linux 内核的开发。由于 Linux 的代码编写有很多人的参与,并且当时是通过手工的方式来进行的代码合并。而随着 Linux 的持续发展,其代码库变得比较庞大,没有办法继续通过手工的方式去进行管理,在当时选择了一个商业版的版本控制系统 Bit keeper 来进行代码的管理,并且当时还给 Linux 社区授予了免费使用权。几年之后, Linux 内核社区与 bit keeper 商业公司关系破裂,并且也撤销了 Linux 内核社区的免费使用权。然后 Linus 决定自己去开发一个版本控制系统,然后就有了 Git。
Git (分布式) 与 SVN (集中式) 的区别
Git 官网中有提到 --- Git - 关于版本控制 (git-scm.com)
集中式的 SVN
- 管理员可掌握其开发权限
- 服务器单点故障
- 容错性差
分布式的 Git
- 本地代码仓库存在完整的历史记录
- 代码保密性差(每个人都有完整的代码版本,警惕工作中可能会产生的意外提交)
Git 原理
.git 文件目录
git init
之后,会在当前目录下新建一个 .git 的文件夹,基本结构如下:
bash
./.git
./.git/config
./.git/description
./.git/HEAD
./.git/hooks
./.git/info
./.git/info/exclude
./.git/objects
./.git/objects/info
./.git/objects/pack
./.git/refs
./.git/refs/heads
./.git/refs/tags
.git/config
可以通过 git config user.name "xx"
令来覆盖全局的 user 配置。全局的 git 配置文件目录是~/.gitconfig
。配置全局的需要加上 global:git config --global user.name "xx"
。
.git/hook
hooks
目录包含客户端或服务端的钩子脚本(hook scripts),用于在 git 命令前后做检查或做些自定义动作。
.git/info
info
目录包含一个全局性排除(global exclude)文件, 用以放置那些不希望被记录在 .gitignore
文件中的忽略模式(ignored patterns)。
.git/objects
objects
目录存储所有数据内容(git 的数据库)。通过 git cat-file -t <校验和>
可以查看其中文件的类型,类型包括【blob | tree | commit】三种,-s
可以查看内容的长度,-p
可以查看到文件具体的内容。
.git/refs
refs
目录存储指向数据(分支、远程仓库和标签等)的提交对象的指针。
.git/HEAD
HEAD
文件指向目前被检出的分支。
bash
$ cat .git/HEAD
ref: refs/heads/master
.git/index
index
文件保存暂存区信息。通过 git ls-files
可以查看到暂存区中的所有文件。git ls-files -s
可以查看除文件以外的更多信息。如:100644 190a18037c64c43e6b11489df4bf0b9eb6d2c9bf 0 t1.txt
。
Git 存储
Git 是一个内容寻址文件系统,其存储内容都是通过内容地址维护,可以把它理解成一个键值对存储方式:即给定一个存储文件,该系统根据文件信息和内容,使用 SHA-1 算法计算,返回一个由 40 个十六进制字符(0-9 和 a-f)组成的唯一字符串,之后只需要通过该字符串(键)即可访问该文件,这个字符串就是 Git 中通常所说的校验和。
Git 对象
git add
暂存操作 会为每一个文件计算校验和(SHA-1 哈希算法),然后会把当前版本的文件快照保存到 Git 仓库中 (Git 使用 blob 对象 来保存它们)。当使用 git commit
进行提交操作 时,Git 会先计算每一个子目录的校验和,然后在 Git 仓库中将这些校验和保存为树对象 。随后,Git 便会创建一个提交对象, 它除了包含上面提到的那些信息外,还包含指向这个树对象(项目根目录)的指针。
📌 Git 对象一旦创建,不可变更。
Blob 对象
数据对象,git add
将文件加入暂存区时产生,保存着文件快照。如下:
shell
$ echo '123' > t1.txt # 将123输入到 t1.txt 文件
$ git add t1.txt
$ find .git/objects -type f # 查看.git/objects目录下的所有文件
.git/objects/19/0a18037c64c43e6b11489df4bf0b9eb6d2c9bf
$ git cat-file -p 190a # 查看该HASH值对应的具体内容
123
$ git cat-file -t 190a # 查看该HASH值存储的文件类型
blob
$ git hash-object t1.txt # 返回存储在 Git 仓库中的唯一键
190a18037c64c43e6b11489df4bf0b9eb6d2c9bf
若添加的不同文件中的内容相同时,只会产生一个 blob 类型的文件。因为 blob 对象存储的只有文件的内容,而且 SHA-1 计算校验和时也是针对文件中具体的内容,所以相同的内容计算出相同的校验和是必然的。
Tip: 计算文件具体内容的校验和时,会在最前面加上 blob 和文件的长度,如:blob 10\0123
。"\0"
代表字符串的结束符。
Tree 对象
树对象,记录着目录结构和 blob 对象索引,解决文件名的存储问题。为 tree 对象类型的文件中所保存的信息,例如下:
css
$ git cat-file -p c114
100644 blob 190a18037c64c43e6b11489df4bf0b9eb6d2c9bf t1.txt
commit 对象
提交对象,包含一个树对象、【上一个 commit 】以及提交者的信息和注释,通过包含的树对象就可以找到所有提交的数据对象,从而形成一个 git 版本。为 commit 对象类型的文件中所保存的信息,例如下:
1️⃣ 进行第一次commit提交之后:
ruby
# 第一次 commit
$ git cat-file -p abdc
tree c1140d41172dce873fcf6177e8bd20b034fd9fba
author duqian01 <duqian01@qianxin.com> 1642730112 +0800
committer duqian01 <duqian01@qianxin.com> 1642730112 +0800
lst commit
$ git cat-file -p c114
100644 blob 190a18037c64c43e6b11489df4bf0b9eb6d2c9bf t1.txt

2️⃣ 进行第二次commit提交之后:
sql
# 修改t1.txt进行第二次 commit后,会多一个 parent 指针,它指向的就是上一次提交的commit 对象
$ git cat-file -p 3bc4
tree e601a5808de751f128820189e03a1ec94a9fd160
parent abdc97446af96108f709480a5996ce81339120e2
author duqian01 <duqian01@qianxin.com> 1642730924 +0800
committer duqian01 <duqian01@qianxin.com> 1642730924 +0800
2nd commit
$ git cat-file -p e601
100644 blob 81c545efebe5f57d4cab2ba9ec294c4b0cadf672 t1.txt

3️⃣ 添加t3.txt文件之后,进行第三次commit提交:
sql
# 添加t2.txt文件进行第三次提交,该commit对象中存在两个文件,其中t1.txt还是会用上一个commit中的
$ echo '12345' > t2.txt
$ git add t2.txt
$ git commit -m '3rd commit'
$ git cat-file -p 23e3
tree 417b026c3dbbb53a96e10a62a72fa3df0f052131
parent 3bc4b63ddd6199a2fab2545da5eae88d9fb24e02
author duqian01 <duqian01@qianxin.com> 1642733174 +0800
committer duqian01 <duqian01@qianxin.com> 1642733174 +0800
3rd commit
$ git cat-file -p 417b
100644 blob 81c545efebe5f57d4cab2ba9ec294c4b0cadf672 t1.txt
100644 blob e56e15bb7ddb6bd0b6d924b18fcee53d8713d7ea t2.txt

4️⃣ 新建一个目录与文件后,进行第4次commit提交:
shell
# 新建一个目录与文件后,进行第4次commit提交
$ mkdir d1
$ cd d1/
$ echo '123456' > t3.txt
$ cd ..
$ git add d1/
$ git commit -m '4th commit'
$ git cat-file -p 4d3f
tree 0a9b004d5dfc0f3539a2d80678356683d0b136e7
parent 23e311bd307c7f92414dadf2089fec27a0ebe591
author duqian01 <duqian01@qianxin.com> 1642734614 +0800
committer duqian01 <duqian01@qianxin.com> 1642734614 +0800
4th commit
$ git cat-file -p 0a9b
040000 tree 684add08d1249cfde1071ba3fc8272238c960ac4 d1
100644 blob 81c545efebe5f57d4cab2ba9ec294c4b0cadf672 t1.txt
100644 blob e56e15bb7ddb6bd0b6d924b18fcee53d8713d7ea t2.txt

此时.git/objects的目录结构如下:
lua
$ tree .git/objects/
.git/objects/
|-- 0a
| `-- 9b004d5dfc0f3539a2d80678356683d0b136e7
|-- 19
| `-- 0a18037c64c43e6b11489df4bf0b9eb6d2c9bf
|-- 23
| `-- e311bd307c7f92414dadf2089fec27a0ebe591
|-- 3b
| `-- c4b63ddd6199a2fab2545da5eae88d9fb24e02
|-- 41
| `-- 7b026c3dbbb53a96e10a62a72fa3df0f052131
|-- 4d
| `-- 3f8671088fbcb20749ce029ad58302af25a54e
|-- 68
| `-- 4add08d1249cfde1071ba3fc8272238c960ac4
|-- 81
| `-- c545efebe5f57d4cab2ba9ec294c4b0cadf672
|-- 9f
| `-- 358a4addefcab294b83e4282bfef1f9625a249
|-- ab
| `-- dc97446af96108f709480a5996ce81339120e2
|-- c1
| `-- 140d41172dce873fcf6177e8bd20b034fd9fba
|-- e5
| `-- 6e15bb7ddb6bd0b6d924b18fcee53d8713d7ea
|-- e6
| `-- 01a5808de751f128820189e03a1ec94a9fd160
|-- info
`-- pack
15 directories, 13 files
Git 对象的压缩
每次 commit,即便是很小的改动,Git 存储的都是全新的文件快照。git 中有对象的压缩机制,通过执行 git gc
可以将对象进行压缩。压缩后的文件会存储到 .git\objects\pack
目录中:其中以 .pack 结尾的文件是包文件,这个文件包含了从文件系统中移除的所有对象的内容; 以 .idx 结尾的文件是索引文件,这个文件包含了包文件的偏移信息。可以通过 git verify-pack -v <path>
来查看已打包内容。
git 对象被压缩之后,仍然能够通过对象 hash 值来获得其具体的存储内容。
bash
$ git gc
Enumerating objects: 16, done.
Counting objects: 100% (16/16), done.
Delta compression using up to 8 threads
Compressing objects: 100% (8/8), done.
Writing objects: 100% (16/16), done.
Total 16 (delta 2), reused 0 (delta 0), pack-reused 0
$ tree .git/objects/pack/
.git/objects/pack/
|-- pack-b3d65c905509214afb33da26b6ce2284b4d40c9c.idx
`-- pack-b3d65c905509214afb33da26b6ce2284b4d40c9c.pack
0 directories, 2 files
$ git verify-pack -v .git/objects/pack/pack-b3d65c905509214afb33da26b6ce2284b4d40c9c.idx
Git 对象的解压缩
通过 git unpack-objects < <path>
可以将被压缩的 .pack
文件进行解压,需要主要的是,不可以在当前目录下进行解压,可以将 .pack
压缩文件移动(mv)到 .git/objects
目录下之后再进行解压。
Git 垃圾对象的清理
什么是垃圾对象?
若一个文件进行多次修改并添加到暂存区,创建了多个 git blob 对象,将该文件进行 commit 提交。此时,去执行 git gc 压缩命令后存在的未被进行压缩的对象。例如下:
sql
$ touch t1.txt
$ echo 'abc' > t1.txt
$ git add t1.txt
$ echo 'abcd' > t1.txt
$ git add t1.txt
$ echo 'abcde' > t1.txt
$ git add t1.txt
# 此时查看 objects 目录
$ tree .git/objects/
.git/objects/
|-- 00
| `-- dedf6bd5f3e493ce8b03c889912f47b01297d4
|-- 8b
| `-- aef1b4abc478178b004d62031cf7fe6db6f903
|-- ac
| `-- be86c7c89586e0912a0a851bacf309c595c308
|-- info
`-- pack
5 directories, 3 files
$ git commit -m 'first commit'
# 进行 commit 提交之后,objects 目录会多增加两个对象文件
$ tree .git/objects/
.git/objects/
|-- 00
| `-- dedf6bd5f3e493ce8b03c889912f47b01297d4
|-- 8b
| `-- aef1b4abc478178b004d62031cf7fe6db6f903
|-- a8
| `-- f47426fce77344845c02c4f32d04db7c12a2ae
|-- ac
| `-- be86c7c89586e0912a0a851bacf309c595c308
|-- e9
| `-- 0cefdcfb7e54748efab54e94833515b8754eff
|-- info
`-- pack
7 directories, 5 files
$ git gc
Enumerating objects: 3, done.
Counting objects: 100% (3/3), done.
Writing objects: 100% (3/3), done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
$ tree .git/objects/
.git/objects/
|-- 8b
| `-- aef1b4abc478178b004d62031cf7fe6db6f903
|-- ac
| `-- be86c7c89586e0912a0a851bacf309c595c308
|-- info
| |-- commit-graph
| `-- packs
`-- pack
|-- pack-2cbc97311f4d9d1be5a68822b7d66e6b7589d47b.idx
`-- pack-2cbc97311f4d9d1be5a68822b7d66e6b7589d47b.pack
4 directories, 6 files
如何清理?
可以通过执行 git prune
命令来删除垃圾对象,在执行该命令之前可以先通过 git prune -n | git fsck
命令去查看 Git 存在哪些垃圾对象。
Git 状态
Git 的三种状态: 已提交 (committed)、已修改 (modified) 和 已暂存(staged)。
- 已提交表示数据已经安全地保存在本地数据库中 → 对应 Git 本地仓库目录。
- 已修改表示修改了文件,但还没保存到数据库中 → 对应工作区。
- 已暂存表示对一个已修改文件的当前版本做了标记,使之包含在下次提交的快照中 → 对应暂存区。
此外,还存在一种未跟踪的文件状态,它们既不存在于上次快照的记录中,也没有被放入暂存区。
远程仓库的添加
执行 git remote add origin
之后,会在 .git/config
文件中配置一个名字为 origin 的远程仓库。首次执行 git push -u origin master
之后,会在 .git 文件中新增 4个目录与 2 个文件:refs 目录中增加 remote/origin/master
;logs/refs 目录中增加 remote/origin/master
。其中在 .git/refs 目录的 master 文件中存储的是最新一次提交到远端的 commit 对象 hash 值。
Git 基本工作流程
本地仓库

为什么存在暂存区?
- 一个扩展的选择性提交概念
- 为方便于git命令行操作而进行设计,对文件的改动进行分组提交
远程仓库

Git 协议
Git 协议
Git 官方文档中提到 --- Git协议。
Git 有四种不同的传输协议:本地协议(Local),HTTP 协议,SSH(Secure Shell)协议及 Git 协议。其中,工作中常用到的就是 SSH 协议。
Git 分支
分支?
Git 官方文档中提到 --- Git分支简介
分支:由每次提交的代码,串成的一条时间线。Git 的分支,其实本质上仅仅是一个指向某一系列提交之首的指针或引用。
为什么需要分支?
- 需求迭代
- 定制版项目
- 历史遗留版本 bug 修复
- 尝试性的模块功能开发 ......
Git 分支的新建与合并
进行 git branch testing
创建分支时,就是创建了一个可以移动的新的指针,会在当前所在的提交对象上创建一个指针。可以通过 HEAD 指针来获得当前在哪个分支上。
git checkout testing
切换分支之后,当前所使用的分支就是 testing
,HEAD
就会指向 testing
分支。若此时再将 master
分支切回,会做两件事:一是使 HEAD 指回 master
分支,二是将工作目录恢复成 master
分支所指向的快照内容。
git merge testing
进行分支合并,这时 master 指针与 Head 指针就会指向 testting 的位置,并且这次合并是一个 快进(fast-forward)。有时候合并操作不会如此顺利。 如果你在两个不同的分支中,对同一个文件的同一个部分进行了不同的修改,Git 就没法干净的合并它们,在合并它们的时候就会产生合并冲突。
遇到冲突时的分支合并
当合并遇到冲突时,Git 会暂停下来,等待你去解决合并产生的冲突。并且可以使用 git status
来查看那些因包含合并冲突而处于未合并(unmerged)状态的文件。另外,产生冲突的文件会包含特殊字符<<<<<<<
。手动解决冲突之后,对文件使用 git add
命令来将其标记为冲突已解决。
快进(fast-forward)
由于想要合并的分支 hotfix
所指向的提交 C4
是你所在的提交 C2
的直接后继, 因此 Git 会直接将指针向前移动。换句话说,当你试图合并两个分支时, 如果顺着一个分支走下去能够到达另一个分支,那么 Git 在合并两者的时候, 只会简单的将指针向前推进(指针右移),因为这种情况下的合并操作没有需要解决的分歧------这就叫做 "快进(fast-forward)"。
变基
Git 官方文档中提到 --- Git - 变基。
合并分支的方法
merge
merge
命令。 它会把两个分支的最新快照(C3
和 C4
)以及二者最近的共同祖先(C2
)进行三方合并,合并的结果是生成一个新的快照(并提交)。
rebase
这两种整合方法的最终结果没有任何区别,但是变基使得提交历史更加整洁。 你在查看一个经过变基的分支的历史记录时会发现,尽管实际的开发工作是并行的, 但它们看上去就像是串行的一样,提交历史是一条直线没有分叉。
变基是将一系列提交按照原有次序依次应用到另一分支上,而合并是把最终结果合在一起。
变基的风险
📌 如果提交存在于你的仓库之外,而别人可能基于这些提交进行开发,那么不要执行变基。
变基操作的实质是丢弃一些现有的提交,然后相应地新建一些内容一样但实际上不同的提交。 如果你已经将提交推送至某个仓库,而其他人也已经从该仓库拉取提交并进行了后续工作,此时,如果你用 git rebase 命令重新整理了提交并再次推送,你的同伴因此将不得不再次将他们手头的工作与你的提交进行整合,如果接下来你还要拉取并整合他们修改过的提交,事情就会变得一团糟。
Git 规范
Git commit 规范
Commit日志规范一般包括:Header(类型, 改动范围,精简总结)、Body、footer。
简单参考规范: <type>: <JIRA ID> <subject>
type: 本次修改的动作类型,可分为:
- feat:新增 xxx 功能
- fix:修复 xxx Bug
- docs:变更 xxx 文档
- style:变更 xxx 代码格式或注释,注意不是 css 修改
- refactor:重构 xxx 功能或方法
- test: 更新测试代码
- chore: 构建过程或辅助工具的变动
- perf: 性能优化(不涉及功能变更)
- build: 建议使用 chore
- workflow: 建议使用 chore
- ci: 建议使用 chore
JIRAID: 可以跟踪需求、缺陷
subject: commit 的概述,建议不超过50个字符
Body: 本次commit的详细描述,可以分成多行,建议以72个字符换行
xml
<type>: <JIRA ID> <subject>
<BLANK LINE>
<body>
<BLANK LINE>
<footer>
原子性提交
原子性:在一个大型系统中,形成一个不可分割的最简单元或组件。
当代码变动时你想创建提交时,这个提交应该尽可能的小量,并且包含一个不可分割的特性(feature)、修复(fix)或优化(improved)。
原子性提交的好处:
- Code reviews会更简单
- 更加容易回滚
Git Flow 规范
Git Flow 工作流定义了一个围绕项目发布的严格分支模型,为不同的分支分配一个很明确的角色。
- Master分支:存储正式发布的历史
- Develop分支:开发分支(功能的集成分支)
- Feature分支:功能分支,由develop分支作为父分支,新功能完成后,合并回develop分支
- Release分支:发布分支(预发布版本),从develop分支拉出,该分支不再加入新功能,可做bug修复,测试成功之后合并到master分支并分配好一个版本号打好tag
- Hotfix分支:维护分支(热修复),用于快速给产品发布版本打补丁,基于发布的上一个版本来创建临时分支,修复完成后直接合并到master的下一个tag
