版本控制——Git

·  阅读 959
版本控制——Git

什么是版本控制

版本控制是一种记录若干文件内容变化,以便将来查阅特定版本修订情况的系统
复制代码

集中化的版本控制系统

  诸如 CVS,Subversion 以及 Perforce 等,都有一个单一的集中管理的服务器,保存所有文件的修订版本,而协同工作的人们都通过客户端连到这台服务器,取出最新的文件或者提交更新。缺点是中央服务器的单点故障。若是宕机一小时,那么在这一小时内,谁都无法提交更新,也就无法协同工作。

image.png

分布式版本控制系统

  诸如 Git,Mercurial,Bazaar 还有 Darcs 等,客户端并不只提取最新版本的文件快照,而是把原始的代码 仓库完整地镜像下来。这么一来,任何一处协同工作用的服务器发生故障,事后都可以用任何一个镜像出来的本地仓库恢复。

img

Git 基础要点

  Git 和其他版本控制系统的主要差别在于,Git 更像是个小型的文件系统,Git 只关心文件数据的整体是否发生变化。Git 并不保存这些前后变化的差异数据。实际上,Git 更像是把变化的文件作快照后,记录在一个微型的文件系统中。每次提交更新时,它会纵览一遍所有文件的指纹信息并对文件作一快照,然后保存一个指向这次快照的索引。为提高性能,若文件没有变化,Git 不会再次保存,而只对上次保存的快照作一连接。

image.png

数据完整性

  在保存到 Git 之前,所有数据都要进行内容的校验和(checksum)计算,并将此结果作为数据的唯一标识和索引。Git 使用 SHA-1 算法计算数据的校验和,通过对文件的内容或目录的结构计算出一个 SHA-1 哈希值,作为指纹字符串。该字串由 40 个十六进制字符(0-9 及 a-f)组成:

24b9da6552252987aa493b52f8696cd6d3b00373
复制代码

  Git 的工作完全依赖于这类指纹字串,所以你会经常看到这样的哈希值。实际上,所有保存在 Git 数据库中的东西都是用此哈希值来作索引的,而不是靠文件名。

三种状态

  对于任何一个文件,在 Git 内都只有三种状态:

-   已提交(committed):表示该文件已经被安全地保存在本地数据库中了
-   已修改(modified):表示修改了某个文件,但还没有提交保存
-   已暂存(staged):表示把已修改的文件放在下次提交时要保存的清单中
复制代码

取得项目的 Git 仓库

  第一种是在现存的目录下,通过导入所有文件来创建新的 Git 仓库。

初始化后,在当前目录下会出现一个名为 .git 的目录
$ git init
$ git add *.java
$ git add README
$ git commit -m 'initial project version'
复制代码

  第二种是从已有的 Git 仓库克隆出一个新的镜像仓库来。

$ git clone git://github.com.git
复制代码

Git 内部原理

  从根本上来讲 Git 是一套内容寻址 (content-addressable) 文件系统。从内部来看,Git 是简单的 key-value 数据存储。它允许插入任意类型的内容,并会返回一个键值,通过该键值可以在任何时候再取出该内容。

目录结构

  每个项目都有一个 git 目录,它是 Git 用来保存元数据和对象数据库的地方。

image.png

  • config: 文件包含了项目特有的配置选项
  • hooks: 客户端或服务端钩子脚本
  • objects: 目录存储所有数据内容
  • refs: 目录存储指向数据 (分支) 的提交对象的指针
  • HEAD: 文件指向当前分支
  • index:文件保存了暂存区域信息
  • info:包含git仓库的一些信息
  • description: 用于 GitWeb 程序

Git 对象

1.初始化一个仓库,Git 初始化了 objects 目录,同时在该目录下创建了 pack 和 info 子目录,但是该目录下没有其他常规文件。

$ git init  git-test
复制代码

2.提交一个文本 111.txt ,内容为 hello word

$ git add  111.txt
$ git commit -m '1'
复制代码

  此时 objects 目录新增3个目录,这便是 Git 存储数据内容的方式──为每份内容生成一个文件,取得该内容与头信息的 SHA-1 校验和,创建以该校验和前两个字符为名称的子目录,并以 (校验和) 剩下 38个字符为文件命名 (保存至子目录下)。

image.png 3. 通过 cat-file 命令可以将数据内容取回,依次查看3个数据

tree (树) 对象

$ git cat-file -p 6dfa3fb63d7fa3f6240dc4ec4f8324f04f31a96e
100644 blob 40d8cf85db392991a45f8fdfda2b5b04f9a49b89    111.txt
复制代码

  tree 对象可以存储文件名,同时也允许存储一组文件。 tree 对象类型于文件系统中的目录。一个单独的 tree 对象包含一条或多条 tree 记录,每一条记录含有一个指向 blob 或子 tree 对象的 SHA-1 指针

image.png

commit (提交) 对象

$ git cat-file -p 08c0d79e1721c7a7297fefef65f9c863bd696e39
tree 6dfa3fb63d7fa3f6240dc4ec4f8324f04f31a96e
author 也许明天 1633934712 +0800
committer xxx@qq.com 1633934712 +0800
复制代码

  commit 对象有格式很简单:指明了该时间点项目快照的顶层树对象、作者/提交者信息(从 Git 配置的 user.name 和 user.email 中获得)以及当前时间戳、一个空行,以及提交注释信息。

blob 对象

$ git cat-file -p 40d8cf85db392991a45f8fdfda2b5b04f9a49b89
hello word
复制代码

  Git 使用 blob 类型的对象存储这些快照,blob 对象则大致对应于文件内容

Git 引用

  在 git 的 refs 目录存储指向数据 (分支) 的提交对象的指针。里面存在 heads 目录与 tags 目录

image.png

HEAD 标记

  HEAD 文件是一个指向你当前所在分支的引用标识符。这样的引用标识符——它看起来并不像一个普通的引用——其实并不包含 SHA-1 值,而是一个指向另外一个引用的指针。

$ git branch test
复制代码

image.png

每当你执行 git branch (分支名称) 这样的命令,会创建一个当前分支文件,把你现在所在分支中最后一次提交的 SHA-1 值,添加到你要创建的分支的引用。

## test 文件内容 
08c0d79e1721c7a7297fefef65f9c863bd696e39
## 就是你最后一次提交对象信息
$ git cat-file -p 08c0d79e1721c7a7297fefef65f9c863bd696e39
tree 6dfa3fb63d7fa3f6240dc4ec4f8324f04f31a96e
author 也许明天 1633934712 +0800
committer xxx@qq.com 1633934712 +0800
复制代码

Tags

  Tag 对象非常像一个 commit 对象——包含一个标签,一组数据,一个消息和一个指针。最主要的区别就是 Tag 对象指向一个 commit 而不是一个 tree。它就像是一个分支引用,但是不会变化——永远指向同一个 commit,仅仅是提供一个更加友好的名字。

Remotes

  remote reference(远程引用)。如果你添加了一个 远程仓库,然后 PUSH 推送代码过去,Git 会把你最后一次推送到这个 remote 的每个分支的值都记录在 refs/remotes 目录下。

小结

  当使用 git commit 新建一个提交对象前,Git 会先计算每一个子目录的校验和,然后在 Git 仓库中将这些目录保存为树(tree)对象。之后 Git 创建的提交对象,除了包含相关提交信息以外,还包含着指向这个树对象(项目根目录)的指针,如此它就可以在将来需要的时候,重现此次快照的内容了。

  Git 仓库中有五个对象:三个表示文件快照内容的 blob 对象;一个记录着目录树内容及其中各个文件对应 blob 对象索引的 tree 对象;以及一个包含指向 tree 对象(根目录)的索引和其他提交信息元数据的 commit 对象。

image.png

  修改后再次提交,那么这次的提交对象会包含一个指向上次提交对象的指针。两次提交后,仓库历史会变成:

image.png

何谓分支

  基本上 Git 中的一个分支其实就是一个指向某个工作版本一条 HEAD 记录的指针或引用。是个指向 commit 对象的可变指针。Git 会使用 master 作为分支的默认名字。在若干次提交后,你其实已经有了一个指向最后一次提交对象的 master 分支,它在每次提交的时候都会自动向前移动。

image.png   Git 又是如何创建一个新的分支,这会在当前 commit 对象上新建一个分支指针

image.png

  由于 Git 中的分支实际上仅是一个包含所指对象校验和(40 个字符长度 SHA-1 字串)的文件,所以新建一个分支就是向一个文件写入 41 个字节(外加一个换行符)那么简单。

merge(合并) 和 rebase(衍合)

  一般最简单的情形,是在 master 分支中维护稳定代码,然后在特性分支上开发新功能,或是审核测试别人贡献的代码,接着将它并入主干,最后删除这个特性分支。

  最容易的整合分支的方法是 merge 命令,它会把两个分支最新的快照(C3 和 C4)以及二者最新的共同祖先(C2)进行三方合并。

  Git 没有简单地把分支指针右移,而是对三方合并的结果作一新的快照,并自动创建一个指向它的 commit(C5))。这个特殊的 commit 称作合并提交(merge commit),因为它的祖先不止一个。

image.png

image.png

  还有另外一个选择:你可以把在 C3 里产生的变化补丁重新在 C4 的基础上打一遍。在 Git 里,这种操作叫做衍合(rebase)。有了 rebase 命令,就可以把在一个分支里提交的改变在另一个分支里重放一遍。

  它的原理是回到两个分支(你所在的分支和你想要衍合进去的分支)的共同祖先,提取你所在分支每次提交时产生的差异(diff),把这些差异分别保存到临时文件里,然后从当前分支转换到你需要衍合入的分支,依序施用每一个差异补丁文件。

image.png

  合并结果中最后一次提交所指向的快照,无论是通过一次衍合还是一次三方合并,都是同样的快照内容,只是提交的历史不同罢了。衍合按照每行改变发生的次序重演发生的改变,而合并是把最终结果合在一起。

  在衍合的时候,实际上抛弃了一些现存的 commit 而创造了一些类似但不同的新 commit。如果你把commit推送到某处然后其他人下载并在其基础上工作,然后你用 git rebase 重写了这些 commit 再推送一次,你的合作者就不得不重新合并他们的工作,这样当你再次从他们那里获取内容的时候事情就会变得一团糟。永远不要衍合那些已经推送到公共仓库的更新。

参考

《Pro Git(中文版)》

分类:
开发工具
标签:
分类:
开发工具
标签: