Git之原理篇[懂了?]

Get Started

安装完Git,让我们开始第一次Git之旅。在终端运行下面的命令

$git config --global user.name  "gaopo"
$git config --global user.email "gaopo@blook.me"
  
$git init git-demo
$cd git-demo
$echo "Git学习" > README.md
$git add README.md
$git commit -m 'init repo and add README.md'
$git log
复制代码

执行完以上命令,我们就已经建立了一个Git仓库,并且将文件README.md交由Git管理。 连接远程版本库,把这个README.md分享给别人

$git remote add origin git@gitlab.blook.me:fe/git-demo.git
$git push origin master
复制代码
  • git config 设置 user.name user.email 只需在首次使用git时设置,之后就无需再设置了。 如果是首次连接使用远程版本库的话,需要把当前用户的公钥传到远程版本库上,否则无法使用ssh协议

基本使用

团队开发中使用Git的基本流程:

  1. 克隆远程版本库
  2. 基于远程develop分支建立本地develop分支
  3. 基于develop分支建立本地特性分支feature
  4. 在feature分支编写程序
  5. 切换到develop分支,合并feature的修改
  6. 把本地develop分支的修改推到远程develop

实际代码

$git clone git@gitlab.blook.me:fe/git-demo.git

cd git-demo
# 基于远程develop分支建立本地develop分支
$git checkout -b develop origin/develop

# 基于develop分支建立本地特性分支feature
$git branch feature
$git checkout feature

# 在feature分支编写程序 后提交
$git add README.md a.txt
$git commit -m 'add a.txt , change README.md'

# 切换到develop分支,合并feature的修改
$git checkout develop
$git pull
$git merge feature


# 把本地develop分支的修改推到远程develop
$git push
复制代码

Git中的对象及背后原理

目录分类

  • Git最重要的概念就是工作区,暂存区,版本库,Git对象。
  • 执行 git init 或 git clone 之后会生成一个目录,这个目录叫做项目目录。
  • 在项目目录下有一个Git目录,除了Git目录之外的都是工作目录

在我们的例子中对应关系如下。

项目目录:git-demo

Git目录:git-demo/.git

工作目录:git-demo下除了.git目录之外的全部

.git目录详解

'Git目录'是项目存储所有历史和元信息的目录 - 包括所有的对象(commits,trees,blobs,tags).

$cd .git ; tree -L 1
|-- HEAD         # 记录当前处在哪个分支里
|-- config       # 项目的配置信息,git config命令会改动它
|-- description  # 项目的描述信息
|-- hooks/       # 系统默认钩子脚本目录
|-- index        # 索引文件
|-- logs/        # 各个refs的历史信息
|-- objects/     # Git本地仓库的所有对象 (commits, trees, blobs, tags)
|-- refs/        # 标识每个分支指向了哪个提交(commit)。
复制代码

这个.git目录中还有几个其他的文件和目录,但都不是很重要。不用太关注。

HEAD文件

HEAD文件就是一个只有一行信息的纯文本文件。这行内容记录的是当前头指针的引用,通常是指向一个分支的引用 ,有时也是一个提交(commit)的SHA值

$ cat .git/HEAD
ref: refs/heads/master  #HEAD文件的内容只有这一行,表明当前处于master分支
$ git checkout dd98199
Note: checking out 'dd98199'.
  
You are in 'detached HEAD' state.
...
  
$ cat .git/HEAD
dd981999876726a1d31110479807e71bba979c44 #这种情况是”头指针分离“模式,不处于任何分支下。HEAD的值就是某一次commit的SHA
复制代码

config文件

config文件记录着项目的配置信息,也是一个普通的纯文本文件。git config命令会改动它(当然也可以手工编辑)。

这个文件里面配置了当前这个版本库的基本属性信息,远程版本库信息,本地分支与远程的映射关系,命令别名等。

总之是一个很有用的文件。在你的.git目录里看到的config文件内容基本上是下面的样子。

#基本配置
[core]
    repositoryformatversion = 0
    filemode = true
    bare = false
    logallrefupdates = true
    ignorecase = true
    precomposeunicode = true
#上游版本库
[remote "origin"]
    url = http://git.blook.me/fe/git-demo.git
    fetch = +refs/heads/*:refs/remotes/origin/*
#本地分支与上游版本库分支的映射
[branch "master"]
    remote = origin
    merge = refs/heads/master
#当前仓库Git命令别名
[alias]
    st = status
复制代码
  • 如果没有添加远程版本库,[remote "origin"]和[branch "master"]是不存在的;如果没有设置alias那么[alias]也是不存在的。
  • 所以如果仅仅是git init之后的一个本地仓库,那么只有[core]配置项

hooks目录

钩子(hooks)是一些在.git/hooks目录的脚本, 在被特定的事件触发后被调用。当git init命令被 调用后, 一些非常有用的示例钩子脚本被拷到新仓库的hooks目录中; 但是在默认情况下它们是不生效的。 把这些钩 子文件的".sample"文件名后缀去掉就可以使它们生效。

  • 在前端提交前进行eslint,就是pre-commit
  • commit时对说明信息进行格式校验,就是commit-msg
  • 如果没有添加远程版本库,[remote "origin"]和[branch "master"]是不存在的;如果没有设置alias那么[alias]也是不存在的。
  • 所以如果仅仅是git init之后的一个本地仓库,那么只有[core]配置项

index文件

git暂存区存放index文件中,所以我们把暂存区有时也叫作索引(index)。索引是一个二进制格式的文件,里面存放了与当前暂存内容相关的信息,包括暂存的文件名、文件内容的SHA1哈希串值和文件访问权限。暂存区是贯穿于整个Git使用流程的重要概念,所以index文件就很重要。由于是二进制所以我们无法查看具体内容,但是可以用git ls-files --stage 命令查看暂存区里面的文件

$git ls-files --stage
100644 44601d12328ea8e04367337184dcccb85859610e 0    README.md
复制代码

objects目录

Git对象(blob,tree,commit,tag)都保存在objects目录里面,所以objects目录就是真正的仓库。objects里面的目录结构组织的很有特点,是以SHA值的前2位作为目录,后38位作为这个目录下的文件名。

我们的工作目录里的所有文件,代码、库文件、图片等都会变成git对象存在这个objects目录下。每个文件都是一个二进制文件。可以通过 git cat-file -p SHA值来查看文件的内容。

refs目录

refs目录下面是一些纯文本文件,分别记录着本地分支和远程分支的SHA哈希值。文件的数量取决于分支的数量。

$tree refs
refs
├── heads
│   ├── develop     # 记录本地develop分支的SHA哈希值
│   └── master      # 记录本地master分支的SHA哈希值
├── remotes
│   └── origin
│       ├── develop # 记录远程版本库develop分支的SHA哈希值
│       └── master  # 记录远程版本库master分支的SHA哈希值
└── tags
    └── v1.0         # 记录里程碑V1.0的SHA哈希值
复制代码
  • 回想前面介绍的 HEAD文件, HEAD文件的内容记录了当前处于哪个分支,值是 ref: refs/heads/master 。
  • 而 refs/heads/master文件 记录了master分支的最新提交的SHA哈希值 ,Git就是通过HEAD文件和refs/heads下面的文件来判断当前分支及分支最新提交的。
$cat HEAD
ref: refs/heads/master                      # 说明当前处于master分支
  
$cat refs/heads/master
dd981999876726a1d31110479807e71bba979c44    # master分支的最新提交SHA哈希值
复制代码

logs目录

logs目录下面是几个纯文本文件,分别保存着HEAD文件和refs文件内容的历史变化。由于HEAD文件和refs文件的内容就是SHA值,所以log文件的内容就是这些SHA值的变化历史。

  • logs目录的结构和refs几乎一样,都是分支名

  • 在实际使用中,我们经常需要把代码整体回滚到一个历史状态,这是需要用到 git reflog 命令,这个命令其实就是读取的logs目录里的日志文件。就是这么简单。

每次 commit 加一条记录,发现是个链表结构,首尾相连。 到目前为止 .git目录 里的重要文件目录已经都介绍完了,大家掌握这些就可以了,

Git对象存储及管理

SHA

所有用来表示项目历史信息的文件,是通过一个40个字符的“对象名”来索引的,对象名看起来像这样:

dd981999876726a1d31110479807e71bba979c44

你会在Git里到处看到这种“40个字符”字符串。每一个“对象名”都是对“对象”内容做SHA1哈希计算得来的。

这样就意味着两个不同内容的对象不可能有相同的“对象名”。

对象分类

每个对象(object) 包括三个部分:类型,大小和内容。大小就是指内容的大小,内容取决于对象的类型,Git有四种类型的对象:" blob"、" tree"、 " commit" 和" tag"。

BLOB

  • 用来存储文件数据,通常是一个文件。

可以使用 git show 或 git cat-file -p 命令来查看一个blob对象里的内容。在我们的例子中 README.md文件对应的 blob对象的SHA1哈希值是 44601d12328ea8e04367337184dcccb85859610e,我们可以通过下面的的命令来查看blob文件内容:

$ git show 44601d1
Git学习
复制代码

可以通过 git hash-object 命令生成文件的SHA哈希值,如果加上 -w 参数,会把这个文件生成blob对象并写入对象库。 hash-object 命令是个Git比较底层的命令,平时正常使用Git几乎用不到。

$git hash-object README.md
44601d12328ea8e04367337184dcccb85859610e
#这只会显示READMD.md文件blob对象的SHA值,并不会生成blob文件。
#git hash-object -w README.md ,则会真正把README.md生blob对象并写入对象库
复制代码
  • 总之,被Git管理的所有文件都会生成一个blob对象
  • 不需要写完整40位的SHA哈希值,只写前7位就可以

TREE

“tree”有点像一个目录,它管理一些“tree”或是 “blob”(就像文件和子目录) git ls-tree 或 git cat-file -p 命令还可以用来查看tree对象,现在我们查看刚刚最新提交对应的Tree对象 我们可以像下面一样来查看它:


$git ls-tree HEAD^{tree}
100644 blob 44601d12328ea8e04367337184dcccb85859610e    README.md
040000 tree 16a87dbed191bcfb19a4af9d0cc569f6448a01cc    script
复制代码

就如同你所见,一个tree对象包括一串(list)条目,每一个条目包括:mode、对象类型、SHA1值 和名字(这串条目是按名字排序的)。它用来表示一个目录树的内容。 一个tree对象可以指向(reference): 一个包含文件内容的blob对象, 也可以是其它包含某个子目录内容的其它tree对象. Tree对象、blob对象和其它所有的对象一样,都用其内容的SHA1哈希值来命名的;只有当两个tree对象的内容完全相同(包括其所指向所有子对象)时,它的名字才会一样,反之亦然。这样就能让Git仅仅通过比较两个相关的tree对象的名字是否相同,来快速的判断其内容是否不同。tree对象存储的是指针(tree和blob的SHA哈希值),不存储真正的对象。tree对象可以理解为就是一个目录,目录里包含子目录(tree的SHA值)和文件(blob的SHA值).而SHA值所对应的真正的对象文件存在 .git/objects下面。

COMMIT

一个“commit”只指向一个"tree",它用来标记项目某一个特定时间点的状态。它包括一些关于时间点的元数据,如时间戳、最近一次提交的作者、指向上次提交(commits)的指针等等。

  • 有时也叫做快照。"commit对象"还带有相关的描述信息.

可以用 git log -1 --pretty=raw 或 git show -s --pretty=raw 或 git cat-file -p <commit>

➜  rrc git:(develop) git cat-file -p HEAD^^
tree f829a37e520fde788a93067d5de87f53e39ffbda
parent 604c39395fbcd4d6ada81207d359974196741085
author gaopo <gaopo@renrenche.com> 1557138527 +0800
committer gaopo <gaopo@renrenche.com> 1557138527 +0800

feat(form): 添加password input
复制代码
  • 提交(commit)由以下的部分组成: 一个 tree对象: tree对象的SHA1签名, 代表着目录在某一时间点的内容.

父提交 (parent(s)): 提交(commit)的SHA1签名代表着当前提交前一步的项目历史. 上面的那个例子就只有一个父对象; 合并的提交(merge commits)可能会有不只一个父对象. 如果一个提交没有父对象, 那么我们就叫它“根提交"(root commit), 它就代表着项目最初的一个版本(revision). 每个项目必须有至少有一个“根提交"(root commit)。Git就是通过父提交把每个提交联系起来,也就是我们一般所说的提交历史。父提交就是当前提交上一版本。

作者 : 做了此次修改的人的名字, 还有修改日期.

提交者(committer): 实际创建提交(commit)的人的名字, 同时也带有提交日期. TA可能会和作者不是同一个人; 例如作者写一个补丁(patch)并把它用邮件发给提交者, 由他来创建提交(commit).

提交说明 :用来描述此次提交.

一个提交(commit)本身并没有包括任何信息来说明其做了哪些修改; 所有的修改(changes)都是通过与父提交(parents)的内容比较而得出的。 一般用 git commit 来创建一个提交(commit), 这个提交(commit)的父对象一般是当前分支(current HEAD), 同时把存储在当前索引(index)的内容全部提交.

commit是使用频率最高的对象,一般在使用Git时,我们直接接触的就是commit。我们 commit代码, merge代码, pull / push代码,重置版本库,查看历史,切换分支这些在开发流程中的基本操作都是直接和commit对象打交道。

TAG

一个“tag”是来标记某一个提交(commit) 的方法。

一个标签对象包括一个对象名, 对象类型, 标签名, 标签创建人的名字("tagger"), 还有一条可能包含有签名(signature)的消息. 你可以用 git cat-file -p 命令来查看这些信息。

  • 现在我们的git对象库里还没有一个tag对象,我们先用 git tag -m [] 命令创建一个tag。
$git tag -m 'create tag from demo' v1.0  #基于当前HEAD建立一个tag,所以tag指向的就是HEAD的引用
$git tag
v1.0
$git cat-file -p v1.0
object e6361ed35aa40f5bae8bd52867885a2055d60ea2
type commit
tag v1.0
tagger gp <gp@demo.com> 1494406971 +0800
  
create tag from demo

复制代码

Tag对象就是里程碑的作用,一般在我们正式发布代码是需要建立一个里程碑。

对象模型组合关系

现在我们已经了解了3种主要对象类型(blob, tree 和 commit), 好现在就让我们大概了解一下它们怎么组合到一起的.

回忆一下现在项目的目录结构:

$tree
.
 
├── README.md
 
└── script
 
    ├── perl
 
    │   └── test2.pl
 
    └── test1.sh
 
 
 
2 directories, 3 files
复制代码

在Git中它们的存储结构看起来就如下图:

每个目录都创建了 tree对象, 每个文件都创建了一个对应的 blob对象 . 最后有一个 commit对象 来指向根tree对象(root of trees), 这样我们就可以追踪项目每一项提交内容。除了第一个commit,每个commit对象都有一个父commit对象,父commit就是上一次的提交(历史 history),这样就形成了一条提交历史链。Git就是通过这种方式组成了git版本库

几乎所有的Git功能都是使用这四个简单的对象类型来完成的。它就像是在你本机的文件系统之上构建一个小的文件系统。这个小型的文件系统就是 .git/objects目录。

工作区、暂存区、版本库

Git对于我们的代码管理分了3个区域,分别是工作区,暂存区和版本库。这是Git完全不同于SVN的地方。Git之所以强大很大程度上就是因为它设计了3个区域,但同时也是因为这个设计让Git学习起来比较难,上手也比较难,比较难理解。凡事都是有利有弊。

工作区(Working Directory):

就是电脑上的一个目录,里面是正在开发的工程代码。执行git init 命令后,生成的这个目录除了.git目录,就是工作区。

暂存区(Stage / Index):

暂存区最不好理解的一个概念,可以先认为需要提交的文件要先放到暂存区才能提交。所以暂存区可以理解为“提交任务”。是代码提交到版本库前的一个缓冲区域。

  • 暂存区其实就是 .git/index文件

历史库(History):

Git的版本库指的是本地仓库,这里面存放着文件的各个版本的数据。其实就是 .git/objects 目录。Git对象都存在这个目录里

历史库,版本库,Git仓库,History,叫法不同而已,其实指的是同一回事。就是执行了 git commit 后,生成了commit对象。现在我们知道commit对象包含了提交时间,提交人,提交说明以及提交时的目录树和父提交。那么上面这些都是构成版本库的要素。由于Git管理的所有对象文件都在 .git/objects 目录中,所以版本库概念可以具体形象的理解成 .git/objects 目录里面的对象文件。.git/objects 目录就是版本库,虽然这么说不是十分的准确,但是非常便于记忆和理解。

暂存区,Stage,Cached,Index ,叫法不同而已,其实指的是同一回事。暂存区就是 .git/index 这个二进制文件,记录着目录和文件的引用(SHA1值),而不是真正的文件对象。真正的文件对象存在于 .git/objects目录里面。

工作区其实就没啥可说的了,就是真实的,看的见摸得到的,正在写的代码。就是你的IDE里面打开的这一堆东西。

再次加强一下记忆: 暂存区就是 .git/index文件 ,版本库就是 .git/objects目录

三区关系

下面的图例展示了3个区域的关系以及涉及到的主要命令 :git add , git commit , git reset , git checkout

因为有了这3个区域,在使用Git时,文件经常处于不同的状态:

  • 未被跟踪的文件(untracked file)
  • 已被跟踪的文件(tracked file)
    • 被修改但未被暂存的文件(changed but not updated或modified)
    • 已暂存可以被提交的文件(changes to be committed 或staged)
    • 自上次提交以来,未修改的文件(clean 或 unmodified)

命令 git diff 用来进行具体文件的变动对比,通常用来进行工作区与暂存区之间的对比,实质上是用 git objects 库中的快照与工作区文件的内容的对比。

add,commit和暂存区的关系

git add 把当前工作目录中的文件放入暂存区域。

准确的说法是 git add files 做了两件事:

将本地文件的时间戳、长度,当前文档对象的id等信息保存到一个树形目录中去(.git/index,即暂存区)

将本地文件的内容做快照并保存到Git 的对象库(.git/object) 。

从命令的角度来看,git add 可以分两条底层命令实现:

  1. git hash-object
  2. git update-index --add
$git hash-object a.txt
$git update-index --add a.txt
#以上两条命令等价于 git add a.txt
复制代码

git commit 命令就是生成一个新的提交,主要干了这么几件事:

  1. 生成一个commit对象
  2. 把暂存区的目录树写到版本库中,也就是生成一个tree对象,这个tree里面的引用和暂存区index里的引用一样
  3. 跟新当前分支ref文件的引用,指向最新生成的这个commit的SHA1哈希值

git reset HEAD 命令,暂存区的目录树会被重写,被最新提交的目录树所替换,但是工作区不受影响。

“git checkout .” 或者 “git checkout -- <file>” 命令,会用暂存区全部或指定的文件替换工作区的文件。这个操作很危险,会清除工作区中未添加到暂存区的改动。

“git checkout HEAD .” 或者 “git checkout HEAD <file>” 命令,会用最新提交的全部或者部分文件替换暂存区和以及工作区中的文件。这个命令也是极具危险性的,因为不但会清除工作区中未提交的改动,也会清除暂存区中未提交的改动。

恭喜!Git的原理篇就到此。

敬请期待……

  • 命令详解及实战
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值