凌云的博客

行胜于言

跟我一起学Git (二) 基本概念

分类:Git| 发布时间:2014-02-28 16:32:32


Git的基本概念

本文主要介绍Git的一些基本概念,这些概念都是在使用Git的过程中经常遇到的。 理解这些概念可以更好的使用Git。

仓库(Repositorie)

一个Git仓库是一个包含所有用于维护和管理项目的版本和历史信息的数据库。 Git会在各个仓库中维护一系列的配置项。 和其他元数据不一样,在你克隆一个仓库的时候,并不会复制这个仓库的配置项。 在仓库中Git主要维护两种数据: object store 以及 index 。 所有这些数据都保存在你的项目顶层目录的隐藏的子目录.git中。

Git对象类型

在object store中有以下四种类型的对象:

  • blobs(binary large object)
  • trees
  • commits
  • tags

索引(Index)

index是一个临时以及动态的二进制文件,用于描述项目在某个时间点的结构。

可定位内容的名称

Git的object store是由可定位内容的存储系统组织和实现的。 也就是说,Git会根据每个对象的内容生成一个SHA1码, 如果两个对象SHA1码是一样的那么这两个对象的内容就是完全一样的(除非出现碰撞)。 这个SHA1码又被称为哈希值、对象ID。

Git的内容追踪

Git不单是一个版本控制系统,还是一个内容追踪系统。 Git的内容追踪主要有两个方面与其他大部分的版本控制系统不一样: 首先,Git的对象存储主要是通过计算它的对象内容的SHA1值来进行存储的, 而不是根据原始的目录/文件结构存储的; 其次,Git的内部数据库会保存每个文件的所有版本的全部内容而不是它们的差异。 因为Git是基于对象内容的SHA1值来保存的,所以不能只保存他们的差异。

路径和内容(Pathname Versus Content)

和其他版本控制系统不一样,Git将文件的内容和它的路径分离了。 当Git保存一个文件时,他会将文件的内容保存为一个blob对象,而它的路径会保存在tree对象中。 这种做法的好处在于将文件的内容和文件的路径属性等分离出来了,如果有两个文件内容一样, Git只会为它保存一份拷贝。

实际环境中的Git概念(Git Concepts at Work)

本小节主要通过创建一个新的代码库然后研究它内部的文件来查看之前介绍的一些概念是怎么组成代码库的。

探索.git目录

首先键入以下命令创建一个空的代码库

$ mkdir hello
$ cd hello/
$ git init
Initialized empty Git repository in g:/src/hello/.git/

$ find .
.
./.git
./.git/config
./.git/description
./.git/HEAD
./.git/hooks
./.git/hooks/applypatch-msg.sample
./.git/hooks/commit-msg.sample
./.git/hooks/post-commit.sample
./.git/hooks/post-receive.sample
./.git/hooks/post-update.sample
./.git/hooks/pre-applypatch.sample
./.git/hooks/pre-commit.sample
./.git/hooks/pre-rebase.sample
./.git/hooks/prepare-commit-msg.sample
./.git/hooks/update.sample
./.git/info
./.git/info/exclude
./.git/objects
./.git/objects/info
./.git/objects/pack
./.git/refs
./.git/refs/heads
./.git/refs/tags

初始情况下.git/objects文件夹除了一些预留文件外没有其他的内容。 这个文件夹会存放各个对象的实际内容,接下来会重点讲这个文件夹。

对象,哈希值以及Blobs

在初始化代码库后,我们先将添加一个文件到代码库中,看看这个代码库会有什么变化:

$ echo "hello world" > hello.txt
$ git add hello.txt
$ find .git/objects/
.git/objects/
.git/objects/3b
.git/objects/3b/18e512dba79e4c8300dd08aeb37f8e728b8dad
.git/objects/info
.git/objects/pack

字符串 3b18e512dba79e4c8300dd08aeb37f8e728b8dad 就是根据文件 hello.txt 的内容生成的SHA1码 (直接通过sha1sum查看文件的SHA1码并不是这个,有可能Git是先对文件进行压缩再计算文件的SHA1码)。 Git会将计算出来的SHA1码的前两位作为目录,其他的作为文件名, 这是因为在某些系统中大量文件放在同一个目录会有性能的问题。

这时我们可以通过 git cat-file 来查看 3b18e512dba79e4c8300dd08aeb37f8e728b8dad 的内容是不是就是我们刚刚添加的hello.txt的内容

$ git cat-file -p 3b18e512dba79e4c8300dd08aeb37f8e728b8dad
hello world

Git的开发者也知道让人手动输入 3b18e512dba79e4c8300dd08aeb37f8e728b8dad 这么一长串字符是很不爽的。 实际上你可以不用输入完整的SHA1码, 而只用输入它的唯一前缀就行了,比如:

$ git cat-file -p 3b18e

又或者,你可以通过 git rev-parse 命令来查看唯一前缀对应的完整SHA1码

$ git rev-parse 3b18e
3b18e512dba79e4c8300dd08aeb37f8e728b8dad

文件和目录(Files and Trees)

我们添加了一个文件,发现它的内容存放在 .git/objects 目录下,那么他的路径呢?哪里去了? 实际上,它的路径将会放在另一个对象中(tree object)。 当你使用 git add 添加一个文件时,Git会为这个文件创建一个blob对象, 但是不会马上创建关于目录树的对象。 文件的路径信息会临时地保存在 .git/index 文件中。 当你使用 git add,git rm 或 git mv等命令时, Git都会更新.git/index文件。 可以通过git ls-files来查看index和工作目录的信息

$ git ls-files -s
100644 3b18e512dba79e4c8300dd08aeb37f8e728b8dad 0       hello.txt

我们可以通过 git write-tree 命令来根据index文件生成tree对象

$ git write-tree
68aba62e560c0ebc3396e8ae9335232cd93a3f60
$ find .git/objects/
.git/objects/
.git/objects/3b
.git/objects/3b/18e512dba79e4c8300dd08aeb37f8e728b8dad
.git/objects/68
.git/objects/68/aba62e560c0ebc3396e8ae9335232cd93a3f60
.git/objects/info
.git/objects/pack

可以看到执行这条命令后生成了一个 SHA1为68aba62e560c0ebc3396e8ae9335232cd93a3f60 的对象, 使用前面介绍的 cat-file 命令来查看这个命令的内容。

$ git cat-file -p 68aba62e560c0ebc3396e8ae9335232cd93a3f60
100644 blob 3b18e512dba79e4c8300dd08aeb37f8e728b8dad    hello.txt

目录层次(Tree Hierarchies)

接下来,我们来看看当添加一个目录后git会怎样处理

$ mkdir subdir
$ cp hello.txt subdir/
$ git add subdir/hello.txt
$ git ls-files -s
100644 3b18e512dba79e4c8300dd08aeb37f8e728b8dad 0       hello.txt
100644 3b18e512dba79e4c8300dd08aeb37f8e728b8dad 0       subdir/hello.txt

$ git write-tree
492413269336d21fac079d4a4672e55d5d2147ac

$ git cat-file -p 49241
100644 blob 3b18e512dba79e4c8300dd08aeb37f8e728b8dad    hello.txt
040000 tree 68aba62e560c0ebc3396e8ae9335232cd93a3f60    subdir

$ find .git/objects/
.git/objects/
.git/objects/3b
.git/objects/3b/18e512dba79e4c8300dd08aeb37f8e728b8dad
.git/objects/49
.git/objects/49/2413269336d21fac079d4a4672e55d5d2147ac
.git/objects/68
.git/objects/68/aba62e560c0ebc3396e8ae9335232cd93a3f60
.git/objects/info
.git/objects/pack

上面有几点需要注意的,首先hello.txt和subdir/hello.txt的SHA1码是一样的, 这证实了前面所说的git是根据文件的内容生成SHA1码,而不关心它的路径。 其次是git cat-file -p 49241命令中subdir的SHA1码和之前工作目录的SHA1码是一样的, 这是因为之前的工作目录和现在的subdir的目录结构是一样的。git根据目录的结构来生成tree对象的SHA1码。

提交(Commits)

在你使用git add添加文件,以及使用git write-tree生成tree对象后可以使用以下命令生成提交

$ echo -n "Commit a file that says hello\n" | git commit-tree 492413269336d21fa
c079d4a4672e55d5d2147ac
76a6d99444aa499b2fc305358d1d0c078e8c834c

然后可以查看这次提交的内容:

$ git cat-file -p 76a6d
tree 492413269336d21fac079d4a4672e55d5d2147ac
author Jianlong Chen <jianlong99@gmail.com> 1390802623 +0800
committer Jianlong Chen <jianlong99@gmail.com> 1390802623 +0800

Commit a file that says hello\n

如果你是跟着本文尝试的话,那么在你的机器上生成的commit对象的SHA1码应该是不一样的。 因为我们commit含有作者信息和提交时间,而在你的机器上这些信息跟我的应该是不一样的。

当然,上面的 git write-tree 和 git commit-tree 命令在正常的使用中是不会用到的, 通常情况下你应该使用git commit来完成这两个工作。

一个commit对象通常包含以下信息:

  • 关联的tree对象的标记
  • 作者信息
  • 提交时间
  • 原因

标签(Tags)

Git有两种的标签:一种是轻量级的,这种标签只存在本代码库中,不会生成相应的对象; 另一种是注释标签(这种标签是永久的,会生成相应的对象并且可以提供签名)。 大部分的Git实现只支持“永久”标签。

可以通过git tag命令生成标签

$ git tag -m"Tag version 1.0" V1.0 76a6d
$ git rev-parse V1.0
e61ec01bb57007b3a053bf235a49897544a89b73
$ git cat-file -p e61ec
object 76a6d99444aa499b2fc305358d1d0c078e8c834c
type commit
tag V1.0
tagger Jianlong Chen <jianlong99@gmail.com> Mon Jan 27 14:46:41 2014 +0800

Tag version 1.0