git基础原理和使用参考

Git基础原理和使用参考

什么是git?


git是一款用来执行版本控制的软件,和Subversion、CVS具有相似的特性,例如可以保存修改历史、能恢复某些历史的修改等等,同时,git与传统的版本控制软件在设计思想和实现上差异比较大。
早期版本控制软件,例如Subversion,其依赖中心服务器以及客户端到服务器的网络连通性,服务器充当中心结点,所有客户端通过网络协议执行版本控制操作,如果无法连接网络服务,则无法执行相关操作,将这种控制软件方式称为集中式版本控制,其优点在于部署简便,不需要考虑多个客户端如何协作。这种软件架构在Linux内核这种强调协作开发模式时逐渐暴露缺陷,绝大多数的 Linux 内核维护工作都花在了提交补丁和保存归档的繁琐事务上。
与集中式对应的则是分布式版本控制,git就是分布式版本控制软件的一种,每个客户端都拥有完整的仓库代码,除非需要推送到远程,大部分操作是发生在本地文件系统,摆脱对于网络连通性的强依赖,甚至中心服务器故障时,可以使用某个客户端充当新的中心节点。

Linux内核开源社区从2002年开始使用BitKeeper,同样是一款分布式版本控制软件,在2005年BitKeeper的商业公司同Linux内核开源社区的合作关系结束,于是Linux内核开源社区不得不另起炉灶。最初他们对git有以下期待:

  • 速度
  • 简单的设计
  • 支持非线性开发模式即允许大量并行开发分支
  • 完全分布式
  • 有能力高效管理类似 Linux 内核一样的超大规模项目

除了分布式之外,git对待版本更新的方法和Subversion有些不同,Subversion会把版本看成是一组文件随时间的累积结果,基于差异去控制版本,如下所示
svn.png
git把每次提交保存为文件系统的快照,出于效率考虑如果没有修改则不保存,只是保留指向之前版本的链接,整体上看git更像是个内容寻址文件**系统**,如下所示
git.png

基础概念和原理

对象状态

在git中操作对象叫做文件对象,包括三种状态,分别是已提交(commited)、已修改(modified)、已暂存(staged)

  • 已修改表示已经修改了文件,但是还没有保存到本地文件数据库
  • 已暂存表示对一个已修改文件的当前版本做了标记,使之包含在下次提交的快照中
  • 已提交表示数据已经安全地保存在本地数据库中

以上三种状态对象的集合则是git目录、工作区和暂存区,其中已提交对应git目录,已修改对应工作区,已暂存对应暂存区

工作区是对项目的某个版本独立提取出来的内容。 这些从 Git 仓库的压缩数据库中提取出来的文件,放在磁盘上供你使用或修改。
暂存区是一个文件,保存了下次将要提交的文件列表信息,一般在 Git 仓库目录中。 按照 Git 的术语叫做“索引”,不过一般说法还是叫“暂存区”。
Git目录是 Git 用来保存项目的元数据和对象数据库的地方。 这是 Git 中最重要的部分,从其它计算机克隆仓库时,复制的就是这里的数据

对象和对象数据库

所有git相关的数据都是存储在.git目录下,其中git对象存储在.git/objects即对象数据库。
可以通过如下代码,实现插入一个对象到对象数据库中

1
2
echo 'add demo object' > demo.txt
git hash-object -w demo.txt

这时系统会返回长度为40的字符哈希校验和,注意不同的运行环境所得到的哈希校验和可能不同

1
effc69903ad516acb116f3405664ae909efaaefb

这是将待存储的数据和必要的头信息组合并使用SHA-1算法生成,
此时执行以下指令,就可以看到对象数据库新增1个对象

1
find .git/objects -type f

系统返回如下

1
.git/objects/ef/fc69903ad516acb116f3405664ae909efaaefb

如果我想要查看这个对象的值,则可以使用刚刚返回的哈希校验和获取

1
git cat-file -p effc69903ad516acb116f3405664ae909efaaefb

系统返回如下

1
add demo object

如果我们修改demo.txt文件内容并再次添加到对象数据库中,这时查看对象数据库则会发现又新增1个对象

1
2
echo 'add another object' > demo.txt
git hash-object -w demo.txt

系统返回如下

1
38b902fd6011c714c82785d9cd12421fe21f48e0

查看对象数据库

1
find .git/objects -type f

系统返回

1
2
.git/objects/38/b902fd6011c714c82785d9cd12421fe21f48e0
.git/objects/ef/fc69903ad516acb116f3405664ae909efaaefb

依然可以使用git cat-file -p获取到任何一个对象,而这就是git核心特性之一,将数据保存到本地对象数据库中,其中key是哈希校验和
只要有哈希校验和和完整对象数据库,我们可以获取任何一个版本的数据对象

树对象

数据都存储在对象数据库中,但是存取的路径和文件名还没有保存,git以一种类似于 UNIX 文件系统的方式存储,所有内容均以树对象和数据对象的形式存储。
其中树对象对应了 UNIX 中的目录项,数据对象则大致上对应了 inodes 或文件内容
一个树对象包含了一条或多条树对象记录(tree entry),每条记录含有一个指向数据对象或者子树对象的 SHA-1 指针以及相应的模式、类型、文件名信息。
首先创造1个新对象,如下所示

1
2
3
mkdir -p path
echo 'add path object' > path/demo.txt
git hash-object -w path/demo.txt

系统返回

1
a2d4add8726a7ca24590a8a3199fda8ac5f93e48

Git 通常根据某一时刻暂存区(即 index 区域,下同)所表示的状态创建并记录一个对应的树对象, 如此重复便可依次记录(某个时间段内)一系列的树对象,如下所示

1
2
git update-index --add --cacheinfo 100644 a2d4add8726a7ca24590a8a3199fda8ac5f93e48 path/demo.txt
git update-index --add --cacheinfo 100644 38b902fd6011c714c82785d9cd12421fe21f48e0 demo.txt

通过git update-index将文件提交到暂存区,再通过git write-tree将暂存区对象写入树对象,如下所示

1
git write-tree

系统返回

1
d31a14e8590e683e30a754e5f846bb61cb99ae5d

这时对象数据库中就会新增一个对象类型,通过git cat-file可以看到对象类型是树对象,如下所示

1
git cat-file -p d31a14e8590e683e30a754e5f846bb61cb99ae5d

系统返回

1
2
100644 blob 38b902fd6011c714c82785d9cd12421fe21f48e0	demo.txt
040000 tree 654277737a15bb7db0276f585f711d4175cbcfd8 path

由于树对象是和UNIX文件系统对应,树对象可以引用另一个树对象,继续使用git cat-file可以查看

1
git cat-file -p 654277737a15bb7db0276f585f711d4175cbcfd8

系统返回

1
100644 blob a2d4add8726a7ca24590a8a3199fda8ac5f93e48	demo.txt

提交对象

提交对象是引用树对象并记录谁在什么时候增加树对象,相当于树对象的操作记录,通过

1
echo 'new tree' | git commit-tree d31a14e8590e683e30a754e5f846bb61cb99ae5d

系统返回

1
25b8654df0b5571aa8e76d6a2d83fe124d365948

这时通过git log就可以看到提交的记录

1
git log 25b8654df0b5571aa8e76d6a2d83fe124d365948

git所做的工作实质就是将被改写的文件保存为数据对象, 更新暂存区,记录树对象,最后创建一个指明了顶层树对象和父提交的提交对象。
这三种主要的 Git 对象——数据对象、树对象、提交对象——最终均以单独文件的形式保存在 .git/objects 目录下

引用对象

实际上引用对象就是提交对象的别名,我们可以用git update-ref创建引用对象,如下所示

1
git update-ref refs/heads/demo 25b8654df0b5571aa8e76d6a2d83fe124d365948

这时我们就可以使用refs/heads/demo来引用提交对象,这样的表示方式更加方便记忆,而这基本就是git分支的本质,存储在.git/refs/heads下的对提交对象的别名
在这些引用对象中有一个HEAD对象,通常指向为目前所在的分支,当用git commit创建新的提交对象后,并用HEAD文件中那个引用所指向的SHA-1值设置其父提交字段

常见使用场景

初始化git目录

对于任意的目录,都可以通过git init来初始化

1
git init

获取远程git目录到本地

使用git clone指令来获取远程的git数据

1
git clone git@github.com:git/git.git

回顾变更

可以使用git log来查看提交对象,如下所示

1
git log --pretty=raw

检查工作区和暂存区状态

使用git status查看工作区和暂存区的对象状态,如下所示

1
2
echo 'hello,new world' > hello.txt
git status

系统返回

1
2
3
4
5
6
7
On branch master
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: hello.txt

no changes added to commit (use "git add" and/or "git commit -a")

这时表明hello.txt文件还没有添加到暂存区,仍在工作区

如何提交变更

  1. 在工作区修改文件
  2. 通过git add将想要的变更文件选择性地添加到暂存区
  3. 通过git commit将暂存区的文件生成快照存储到git目录的提交对象,并更新HEAD引用
1
2
3
echo 'hello,world' > hello.txt
git add hello.txt
git commit -m "hello,world"

系统返回

1
2
3
[master 0a7d684] hello,world
1 file changed, 1 insertion(+)
create mode 100644 hello.txt

有时刚刚commit后发现少提交一些文件,如果再次commit一次会把提交日志搞得奇怪,而且还得再写一次提交的message,这时可以默认使用上一条提交message

1
git commit --amend

临时保留修改

有时在工作区的修改还没有达到提交状态,这时如果有一个临时的想法想去实现,可以使用git stash来临时保存工作区状态

1
2
echo 'hello,new world' > hello.txt
git stash

当临时想法实现完毕后,可以使用git stash apply来恢复刚刚的工作区

1
git stash apply

恢复完毕后,建议清理

1
git stash clean

撤销工作区修改

这时使用git checkout来恢复工作区

1
2
echo 'hello,new world' > hello.txt
git checkout -- hello.txt

撤销提交到暂存区

1
2
echo 'hello,new world' > hello.txt
git add hello.txt

这时就需要使用git reset来恢复暂存区

1
git reset HEAD hello.txt

撤销提交

这时需要使用git reset –hard来实现,这个指令可以提供回滚到上一个版本,也可以通过提供SHA-1回滚到任意提交对象

1
git reset --hard HEAD^1

如果又想反悔,也只需要找到那个提交对象,就可以找回某次的提交

1
git reset --hard 0a7d684

新建分支

在git中新建分支非常容易,O(1)复杂度,实际上就是对当前的HEAD对象创建1个引用对象,使用git switch -c可以创建分支后自动切换到新分支

1
git switch -c feature

切换分支

1
git switch feature

查看分支

使用如下指令可以检查git的分支以及当前所在的分支,当前分支前面会标一个*号

1
git branch

删除分支

删除分支依旧使用git branch,只是需要新增参数-d和分支名

1
git branch -d feature

合并分支

在大型项目开发中,合并分支往往非常频繁,这是因为需要经常集成其他人的贡献和代码,最常用的合并是通过git merge实现,如下就是实现master分支和feature分支合并

1
2
git switch master
git merge feature

值得注意的是,这里的合并是指本地对象合并,并不是远程的对象
然而,合并并不总是那么愉快,有时会伴随冲突,如果当前分支的父提交不是要合并的分支,则无法使用快进合并,如下为快速合并场景

1
2
3
4
5
6
7
git switch master
git switch -c test
echo 'hello,new world' > hello.txt
git add hello.txt
git commit -m "merge branch"
git switch master
git merge test

系统返回

1
2
3
4
Updating 0a7d684..44d6a4c
Fast-forward
hello.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)

可以看到由于test分支的父提交和当前master的提交一致,符合快速合并原则,可以使用使用test分支的最新提交并设置父提交为master的最新提交
如果test分支创建后,master分支又产生新的提交对象,就不符合快速合并原则,需要人工处理

1
2
3
4
5
6
7
8
9
10
git switch master
git switch -c test
echo 'hello,new world' > hello.txt
git add hello.txt
git commit -m "merge branch"
git switch master
echo 'hello,master world' > hello.txt
git add hello.txt
git commit -m "master branch"
git merge test

系统返回

1
2
3
Auto-merging hello.txt
CONFLICT (content): Merge conflict in hello.txt
Automatic merge failed; fix conflicts and then commit the result

冲突的问题会将两个版本进行标记

1
2
3
4
5
<<<<<<< HEAD
hello,master world
=======
hello, new world
>>>>>>> test

这时就需要人工处理好文件后,重新添加到暂存区并提交

与远程协作

将本地的分支关联到远程分支,实际上就是创建一类特殊的远程引用对象,这个引用对象是只读的
通过git push将本地的对象提交到远程,如下就是把本地的master分支推送到远程,前提是需要对目标仓库有写权限

1
2
git remote add origin git@github.com:git/git.git
git push origin master

值得注意的是,如果使用git commit –amend则在push时,需要使用-f参数,如下所示

1
git push -f origin master

使用git pull来更新远程分支到本地

1
git pull