概述

本文是学习Git官方文档Git Pro的笔记和记录。

什么是Git

Git是一种用于代码管理的分布式版本控制工具,可以实现大型软件项目的多人协作。

Git的出现是为了解决大型软件项目开发过程中遇到的一些问题:

  • 在代码编写的过程中发现新的代码实现存在一定的问题,需要回退到之前的版本时怎么办?

    编译器的撤销快捷键可能有一点用,但是这种方式能存储的撤销步数有一个上限且每次关闭IDE之后这些短暂的记忆就消失了。仅能适用于临时的一些回退。

  • 多人同时修改一个文件怎么进行代码的合并、并能有效处理分布式开发的源码冲突? 串行的方式可以避免冲突但是开发效率上低的可怕

  • 想在已经稳定的版本上开发新的功能害怕把已有的代码版本破坏应该怎么办? 手动的复制需要人工管理繁琐的版本、麻烦且不直观

为了解决这个问题,Linus Torvalds设计了Git解决这一问题。

Git的需求来源于Linux的开发过程:Linux是一个由社区驱动的开源项目,其中有一部分秉持自由软件思想的开发者对Linux使用商用闭源的代码管理工具十分不满(说的就是你GNU和Richard Stallman)。他们对现有的代码管理工具进行修改导致厂商收回了授权。最终,Linus决定自己开发一个代码管理工具并将其命名为git——意为“愚蠢的”。 尽管Linus是Linux(或者说GNU/Linux)项目最早的开发者,但其仅负责Linux系统内核(kernel)的开发和维护。在社区中,GNU基金会作为自由软件思想的先锋为Linux系统做出了卓绝的贡献(GCC、GDB等等)。但因为理念和其他的冲突(开源和自由软件之争、GNU/Linux的命名权争议等),Linux社群中也常常存在矛盾。某种程度来讲,git就诞生于Linux社群内部的矛盾。

Git是怎么工作的

Git的设计继承了许多以前VCS(版本控制系统)的方法,但是和CVS等传统的集中式版本控制系统(CVCS)有着本质的不同。

  • Git基于快照机制进行文件存储

    在 Git 中,每当你更新代码并想要将其提交到本地仓库时,它会对当时的全部文件创建一个快照(称为一个提交commit)。在提交存储到代码仓库之前,git会计算一个校验和(传统 Git 使用 SHA-1 作为对象哈希;新版本 Git 已支持 SHA-256,但 SHA-1 仍是事实标准。)作为索引,并保存这个快照的索引(commmit的哈希值)。

    每个用户在本地都拥有代码仓库的完整备份(一连串的文件快照/提交链),而不是仅仅拥有一个快照(这是CVCS的方式)。

    如果提交时某文件没有修改,Git 不再重新存储该文件,而是只保留一个链接指向之前存储的文件。

  • Git进行分布式的版本管理

    当需要提交时,用户需要设定一个远端仓库(一般是服务器里的裸仓库)然后由用户通过各种分布式的操作保持本地仓库和远程仓库的一致性。

    远端仓库一般以裸仓库的形式在服务器上存放,裸仓库没有工作目录不能直接进行代码的修改。非裸仓库具有工作目录,可以直接进行代码修改,但是一般不能作为Git push的对象。

    在实际的工程实践中,往往使用一个拥有所有代码仓库的Git服务器进行版本控制。每个用户都把Git 服务器中代码仓库(裸仓库形式)设置为远程仓库。 尽管如此,Git依然进行的是分布式的版本控制——中心节点崩溃以后,任何一个和之前的代码仓库保持了同步的仓库都可以作为新的仓库(git仓库保存的是完整的备份)。

  • Git的文件管理和版本控制

    在Git中,文件有几种状态:未追踪Untracked、已修改Modified、已暂存Staged、已提交Committed。

    未追踪的文件、已修改的文件在工作目录(Working Tree)之中需要通过git add保存到暂存区(Staging Area)。暂存时,git可以灵活的控制每一个文件(甚至是每一个代码块)是否需要暂存,然后进行统一的提交。最终实现灵活的部分提交。

    在确认过暂存区的更改之后,可以将使用git commit将已暂存的文件提交到代码库(Repository)的分支(branch)。

    在代码仓库里,往往具有一个代码的许多分支(branch), 用来管理代码的不同版本。

Git本质

Git的对象

Git本质上是一个键值对的数据库,值是一个数据对象,键是根据数据计算的SHA1哈希值。

Git中的数据对象有四种,都会存放在.git/objects/下:

  • 存储文件内容的Blob对象:

    Blob对象是实际的存储载体,是某个文件内容的快照。根据数据内容具有唯一的HASH值,不具有文件名。对文件修改后就会创建新的Blob对象,相当于新文件的一个快照。Git的实际存储是快照式的,不是增量式的

  • 存储目录信息的Tree对象

    Tree对象是用来描述目录信息,具有指向Blob或者其他Tree的指针。根据Tree对象和Blob对象,可以恢复目录和其下的文件某一时刻下的状态。

  • 存储每一次提交的内容,构成数据历史的Commit对象

    一次提交由提交信息(提交者、提交的名称描述、时间等等)、Tree对象(描述当前提交下的目录情况)、指向父提交的指针(用来组成提交历史)

  • 存储永久指向某个提交的指针的Tag对象

    git add 会将文件内容写入对象数据库(形成 blob),并更新暂存区(index);git commit 则基于 index 构造 tree 和 commit 对象,然后更新HEAD,写入Reflog

通过这四种对象,Git可以将提交组织成一张有向无环图。这个有向无环图很可能没有覆盖数据库中的所有的键值对,这些没有被引用指向的提交不在有向无环图中最后会并定期的git机制回收,是使用时应该避免的。

Git的引用

正如之前说到的,git的提交使用一个指向父提交的提交对象构成了一个提交树(显然的,它是有向无环图)。然而Git 的提交是用 SHA-1(或 SHA-256)这样的哈希标识的,对于人而言他只是一长串不可读的16进制序列。这些哈希是给机器用的,但人类无法记忆、也无法操作它们。

为了解决这一问题,Git使用的方法是引用(reference)。引用的本质是一个文件,内容是某一个提交的Hash值,通过引用相当于给某个提交进行了命名。引用从本质上被分为两种——分支branch 和 标签tag。这两者本质是一样的,指向某个提交,区别在于这个引用怎么被使用。

对于分支branch来说,他关注的是当前的引用和其所有父提交追溯起来组成的一条线性的历史,因此他把这个引用叫做这个分支的head。对于标签tag来说,他关注的是当前提交被打上什么样的标记。没有被任何引用(branch、tag)指向的提交称为游离提交(dangling commit),这些提交在一段时间内仍然存在于对象数据库中。但如果长期未被引用,最终可能会被 Git 的垃圾回收机制清理。

对于某个分支而言,其head引用在 .git/refs/heads/<branch_name> 下,每次对分支进行提交相当于在创建blob、tree、commit对象之后移动这个branch的heads到新的commit。不是提交在某个分支上,而是分支指向了某个提交(和其向前追溯的一串提交历史)。这是git的面向变更的对象进行管理的底层设计哲学。

.git/HEAD是一个特别的引用,他代表当前工作区所在的位置。在大多数情况下,HEAD 本身并不直接指向某个分支或提交,而是符号引用(symbolic reference)到某个分支,如:ref: refs/heads/<branch_name> 。 但在某些情况下(例如checkout到了某个具体的commit),HEAD 会直接指向一个提交哈希值,这种状态称为 分离 HEAD(detached HEAD)。

Git的记录

使用这种方式进行提交历史的管理可能导致一个问题:分支的head并没有指向最新的commit,在head指针指向的coommit之后还有commit。例如使用了git reset --hard这样的命令。这种情况下需要有办法让我们知道发生了什么(当前的HEAD或者某个分支的head是怎么变化的),git给出的办法就是reflog。

reflog记录了某个引用在本地“曾经指向过哪里”。.git/logs/HEAD 记录 HEAD 的变动历史``.git/logs/refs/heads/` 记录 分支的变动历史,每一次commit、checkout、reset、rebase,只要导致引用发生变化,Git 就会在 reflog 中追加一条记录。reflog 本质是一种本地的、面向操作的引用日志。

梳理

作者:d41d8c 来源:知乎

链接:https://www.zhihu.com/question/329750471/answer/1984293412913030104

当你运行git commit的时候,git会:对每一个内容有改动的文件,复制一份,压缩后保存到.git/objects目录。该副本的文件名是根据文件内容算出的乱数。这叫“blob object”。对每一个有改动的目录(文件夹), 创建一个文件,记录该目录下的文件名和上述乱数的对应关系,同样压缩后保存到.git/objects目录。该文件的文件名同样是乱数。这叫“tree object”。再创建一个文件,内容是对应于项目根目录的tree object的文件名(乱数),加上你在git中设置的姓名、邮箱,加上当前时间戳,加上你写的commit message,还有其他一些内容。同样压缩后保存到.git/objects目录。该文件的文件名同样是乱数。这叫“commit object”。从.git/HEAD文件读取一个文件路径。像.git/HEAD这样内容是文件名或路径的文件叫“reference”。通常,.git/HEAD的内容是以refs/heads开头的路径。这种内容不是乱数的reference,叫做“symbolic reference”。.git/refs/heads目录下的reference对git有特殊意义。它们又叫“branch”。把刚刚创建的commit object的文件名,写入从.git/HEAD读到的那个.git/refs/heads下的文件名所对应的文件中。向.git/logs/HEAD文件添加一行,内容为commit object的文件名,时间戳等等。这叫“reflog”。把.git/logs和上面读到的文件路径拼在一起得到一个文件名(类似.git/logs/refs/heads/)。同样向该文件添加上述内容。这也是reflog。通过reflog可以撤回对包括branch在内的各种reference进行的修改。但reflog只在本地有效,不会和其他人同步。