git基础原理和使用参考
Git基础原理和使用参考
什么是git?
git是一款用来执行版本控制的软件,和Subversion、CVS具有相似的特性,例如可以保存修改历史、能恢复某些历史的修改等等,同时,git与传统的版本控制软件在设计思想和实现上差异比较大。
早期版本控制软件,例如Subversion,其依赖中心服务器以及客户端到服务器的网络连通性,服务器充当中心结点,所有客户端通过网络协议执行版本控制操作,如果无法连接网络服务,则无法执行相关操作,将这种控制软件方式称为集中式版本控制,其优点在于部署简便,不需要考虑多个客户端如何协作。这种软件架构在Linux内核这种强调协作开发模式时逐渐暴露缺陷,绝大多数的 Linux 内核维护工作都花在了提交补丁和保存归档的繁琐事务上。
与集中式对应的则是分布式版本控制,git就是分布式版本控制软件的一种,每个客户端都拥有完整的仓库代码,除非需要推送到远程,大部分操作是发生在本地文件系统,摆脱对于网络连通性的强依赖,甚至中心服务器故障时,可以使用某个客户端充当新的中心节点。
Linux内核开源社区从2002年开始使用BitKeeper,同样是一款分布式版本控制软件,在2005年BitKeeper的商业公司同Linux内核开源社区的合作关系结束,于是Linux内核开源社区不得不另起炉灶。最初他们对git有以下期待:
- 速度
- 简单的设计
- 支持非线性开发模式即允许大量并行开发分支
- 完全分布式
- 有能力高效管理类似 Linux 内核一样的超大规模项目
除了分布式之外,git对待版本更新的方法和Subversion有些不同,Subversion会把版本看成是一组文件随时间的累积结果,基于差异去控制版本,如下所示
git把每次提交保存为文件系统的快照,出于效率考虑如果没有修改则不保存,只是保留指向之前版本的链接,整体上看git更像是个内容寻址文件**系统**,如下所示
基础概念和原理
对象状态
在git中操作对象叫做文件对象,包括三种状态,分别是已提交(commited)、已修改(modified)、已暂存(staged)
- 已修改表示已经修改了文件,但是还没有保存到本地文件数据库
- 已暂存表示对一个已修改文件的当前版本做了标记,使之包含在下次提交的快照中
- 已提交表示数据已经安全地保存在本地数据库中
以上三种状态对象的集合则是git目录、工作区和暂存区,其中已提交对应git目录,已修改对应工作区,已暂存对应暂存区
工作区是对项目的某个版本独立提取出来的内容。 这些从 Git 仓库的压缩数据库中提取出来的文件,放在磁盘上供你使用或修改。
暂存区是一个文件,保存了下次将要提交的文件列表信息,一般在 Git 仓库目录中。 按照 Git 的术语叫做“索引”,不过一般说法还是叫“暂存区”。
Git目录是 Git 用来保存项目的元数据和对象数据库的地方。 这是 Git 中最重要的部分,从其它计算机克隆仓库时,复制的就是这里的数据
对象和对象数据库
所有git相关的数据都是存储在.git目录下,其中git对象存储在.git/objects即对象数据库。
可以通过如下代码,实现插入一个对象到对象数据库中
1 | echo 'add demo object' > 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 | echo 'add another object' > demo.txt |
系统返回如下
1 | 38b902fd6011c714c82785d9cd12421fe21f48e0 |
查看对象数据库
1 | find .git/objects -type f |
系统返回
1 | .git/objects/38/b902fd6011c714c82785d9cd12421fe21f48e0 |
依然可以使用git cat-file -p获取到任何一个对象,而这就是git核心特性之一,将数据保存到本地对象数据库中,其中key是哈希校验和
只要有哈希校验和和完整对象数据库,我们可以获取任何一个版本的数据对象
树对象
数据都存储在对象数据库中,但是存取的路径和文件名还没有保存,git以一种类似于 UNIX 文件系统的方式存储,所有内容均以树对象和数据对象的形式存储。
其中树对象对应了 UNIX 中的目录项,数据对象则大致上对应了 inodes 或文件内容
一个树对象包含了一条或多条树对象记录(tree entry),每条记录含有一个指向数据对象或者子树对象的 SHA-1 指针以及相应的模式、类型、文件名信息。
首先创造1个新对象,如下所示
1 | mkdir -p path |
系统返回
1 | a2d4add8726a7ca24590a8a3199fda8ac5f93e48 |
Git 通常根据某一时刻暂存区(即 index 区域,下同)所表示的状态创建并记录一个对应的树对象, 如此重复便可依次记录(某个时间段内)一系列的树对象,如下所示
1 | git update-index --add --cacheinfo 100644 a2d4add8726a7ca24590a8a3199fda8ac5f93e48 path/demo.txt |
通过git update-index将文件提交到暂存区,再通过git write-tree将暂存区对象写入树对象,如下所示
1 | git write-tree |
系统返回
1 | d31a14e8590e683e30a754e5f846bb61cb99ae5d |
这时对象数据库中就会新增一个对象类型,通过git cat-file可以看到对象类型是树对象,如下所示
1 | git cat-file -p d31a14e8590e683e30a754e5f846bb61cb99ae5d |
系统返回
1 | 100644 blob 38b902fd6011c714c82785d9cd12421fe21f48e0 demo.txt |
由于树对象是和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 | echo 'hello,new world' > hello.txt |
系统返回
1 | On branch master |
这时表明hello.txt文件还没有添加到暂存区,仍在工作区
如何提交变更
- 在工作区修改文件
- 通过git add将想要的变更文件选择性地添加到暂存区
- 通过git commit将暂存区的文件生成快照存储到git目录的提交对象,并更新HEAD引用
1 | echo 'hello,world' > hello.txt |
系统返回
1 | [master 0a7d684] hello,world |
有时刚刚commit后发现少提交一些文件,如果再次commit一次会把提交日志搞得奇怪,而且还得再写一次提交的message,这时可以默认使用上一条提交message
1 | git commit --amend |
临时保留修改
有时在工作区的修改还没有达到提交状态,这时如果有一个临时的想法想去实现,可以使用git stash来临时保存工作区状态
1 | echo 'hello,new world' > hello.txt |
当临时想法实现完毕后,可以使用git stash apply来恢复刚刚的工作区
1 | git stash apply |
恢复完毕后,建议清理
1 | git stash clean |
撤销工作区修改
这时使用git checkout来恢复工作区
1 | echo 'hello,new world' > hello.txt |
撤销提交到暂存区
1 | echo 'hello,new world' > 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 | git switch master |
值得注意的是,这里的合并是指本地对象合并,并不是远程的对象
然而,合并并不总是那么愉快,有时会伴随冲突,如果当前分支的父提交不是要合并的分支,则无法使用快进合并,如下为快速合并场景
1 | git switch master |
系统返回
1 | Updating 0a7d684..44d6a4c |
可以看到由于test分支的父提交和当前master的提交一致,符合快速合并原则,可以使用使用test分支的最新提交并设置父提交为master的最新提交
如果test分支创建后,master分支又产生新的提交对象,就不符合快速合并原则,需要人工处理
1 | git switch master |
系统返回
1 | Auto-merging hello.txt |
冲突的问题会将两个版本进行标记
1 | <<<<<<< HEAD |
这时就需要人工处理好文件后,重新添加到暂存区并提交
与远程协作
将本地的分支关联到远程分支,实际上就是创建一类特殊的远程引用对象,这个引用对象是只读的
通过git push将本地的对象提交到远程,如下就是把本地的master分支推送到远程,前提是需要对目标仓库有写权限
1 | git remote add origin git@github.com:git/git.git |
值得注意的是,如果使用git commit –amend则在push时,需要使用-f参数,如下所示
1 | git push -f origin master |
使用git pull来更新远程分支到本地
1 | git pull |