目录
本篇的内容,来自于对《Pro Git》这本书学习的总结。
分支的原理
假设现在有一个工作目录,里面包含了三个将要被暂存和提交的文件。 暂存操作会为每一个文件计算校验和(使用SHA-1 哈希算法),然后会把当前版本的文件快照保存到Git 仓库中(Git 使用 blob 对象来保存它们),最终将校验和加入到暂存区域等待提交:
$ git add README test.rb LICENSE
$ git commit -m 'The initial commit of my project'
当使用 git commit 进行提交操作时,Git 会先计算每一个子目录(本例中只有项目根目录)的校验和,然后在Git 仓库中这些校验和保存为树对象。 随后,Git 便会创建一个提交对象,它除了包含上面提到的那些信息外,还包含指向这个树对象(项目根目录)的指针。如此一来,Git 就可以在需要的时候重现此次保存的快照。
现在,Git 仓库中有五个对象:三个 blob 对象(保存着文件快照)、一个树对象(记录着目录结构和 blob 对象索引)以及一个提交对象(包含着指向前述树对象的指针和所有提交信息)。Git 的分支,其实本质上仅仅是指向提交对象的可变指针。
$ git checkout -b iss53
新建一个分支并同时切换到那个分支上
等效于:
$ git branch iss53
$ git checkout iss53
Git 的默认分支名字是 master。 它会在每次的提交操作中自动向前移动。
git branch 查看分支
git branch -v 如果需要查看每一个分支的最后一次提交。--merged 与 --no-merged 这两个有用的选项可以过滤这个列表中已经合并或尚未合并到当前分支的分支。
git branch -d testing 删除分支。当分支包含了还未合并的工作,尝试使用 git branch -d 命令删除它时会失败,使用git branch -D testing可以强制删除。
$ git merge iss53 分支合并
遇到冲突时,Git 会暂停下来,等待你去解决合并产生的冲突。你可以在合并冲突后的任意时刻使用 git status 命令来查看那些因包含合并冲突而处于未合并(unmerged)状态的文件 。解决文件里的冲突后,使用 git add来将其标记为冲突已解决,然后再git commit 来完成合并提交
$ git merge --no-ff -m "merged bug fix 101" iss53 选项“--no--ff”关闭快速合并,默认开启了快速合并的选项
远程分支
远程跟踪分支是远程分支状态的引用。 它们是你不能移动的本地引用,当你做任何网络通信操作时,它们会自动移动。它们以 (remote)/(branch) 形式命名。
$ git push origin serverfix:awesomebranch 来将本地的 serverfix 分支推送到远程仓库上的 awesomebranch 分支
$ git checkout -b serverfix origin/serverfix(或git checkout --track origin/serverfix) 基于远程分支,建一个自己的新分支
$ git branch -u origin/serverfix
设置已有的本地分支跟踪一个刚刚拉取下来的远程分支,或者想要修改正在跟踪的上游分支,你可以在任意时间使用 -u 或 --set-upstream-to 选项运行 git branch 来显式地设置。当设置好跟踪分支后,可以通过 @{upstream} 或 @{u} 快捷方式来引用它。
$ git branch --set-upstream-to=origin/dev dev 指定本地 dev 分支与远程 origin/dev 分支的链接
git branch -vv 查看所有本地分支及设置的跟踪分支
git fetch --all 抓取所有的远程仓库
git fetch 拉取分支
git pull 拉取并自动合并分支
git push origin --delete serverfix 删除一个远程分支
变基
变基是将一系列提交按照原有次序依次应用到另一分支上,而合并是把最终结果合在一起。一般是为了确保在向远程分支推送时能保持提交历史的整洁。
假如有2个分支master和experiment,将 C4 中的修改变基到 C3 上,操作如下:
$ git checkout experiment
$ git rebase master
它的原理是首先找到这两个分支(即当前分支 experiment、变基操作的目标基底分支 master)的最近共同祖先 C2,然后对比当前分支相对于该祖先的历次提交,提取相应的修改并存为临时文件,然后将当前分支指向目标基底 C3, 最后以此将之前另存为临时文件的修改依序应用。
现在回到 master 分支,进行一次快进合并
$ git checkout master
$ git merge experiment
发生rebase冲突时,
git add 更新这些内容的索引
git rebase --continue 继续应用(apply)余下的补丁
git rebase --abort 终止rebase的操作,回到rebase开始前的状态
在对两个分支进行变基时,所生成的“重放”并不一定要在目标分支上应用,你也可以指定另外的一个分支进行应用。
git rebase --onto master server client
选中在 client 分支里但不在server 分支里的修改(即 C8 和 C9),将它们在 master 分支上重放,之后,再切换到master分支,进行快进合并。
git rebase master server 将 server 中的修改变基到 master 上。这样做能省去你先切换到server 分支,再对其执行变基命令的多个步骤。
变基操作的实质是丢弃一些现有的提交,然后相应地新建一些内容一样但实际上不同的提交。
变基的风险:不要对在你的仓库外有副本的分支执行变基。
git内部原理
.git目录下有4个重要的目录/文件。
$ ll .git/
total 18
-rw-r--r-- 1 kuang 197609 13 8月 5 23:01 COMMIT_EDITMSG
-rw-r--r-- 1 kuang 197609 130 7月 26 10:23 config
-rw-r--r-- 1 kuang 197609 73 7月 26 10:23 description
-rw-r--r-- 1 kuang 197609 23 8月 5 23:01 HEAD
drwxr-xr-x 1 kuang 197609 0 7月 26 10:23 hooks/
-rw-r--r-- 1 kuang 197609 209 8月 5 23:01 index
drwxr-xr-x 1 kuang 197609 0 7月 26 10:23 info/
drwxr-xr-x 1 kuang 197609 0 7月 26 12:29 logs/
drwxr-xr-x 1 kuang 197609 0 8月 5 23:01 objects/
-rw-r--r-- 1 kuang 197609 41 7月 29 23:18 ORIG_HEAD
drwxr-xr-x 1 kuang 197609 0 7月 26 10:23 refs/
$ cat .git/HEAD
ref: refs/heads/master
objects 目录存储所有数据内容;
refs 目录存储指向数据(分支)的提交对象的指针;
HEAD 文件指向目前被检出的分支;
index 文件保存暂存区信息。
数据对象(blob object)
Git 是一个内容寻址文件系统。这意味着,Git 的核心部分是一个简单的键值对数据库(key-value data store)。
$ echo 'test content' | git hash-object -w --stdin
d670460b4b4aece5915caf5c68d12f560a9fe3e4
或者
$ git hash-object -w test.txt
83baae61804e65cc73a7201a7252750c76066a30
git hash-object命令可将任意数据保存于 .git 目录,并返回相应的键值。该命令输出一个长度为 40 个字符的校验和。 这是一个 SHA-1 哈希值——一个将待存储的数据外加一个头部信息(header)一起做 SHA-1 校验运算而得的校验和。
$ git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
test content
git cat-file 指定 -p 选项可指示该命令自动判断内容的类型,并为我们显示格式友好的内容。利用 cat-file -t 命令,可以让 Git 告诉我们其内部存储的任何对象类型
树对象(tree object)
它能解决文件名保存的问题,也允许我们将多个文件组织到一起。
git cat-file -p master^{tree}
master^{tree} 语法表示 master 分支上最新的提交所指向的树对象。
通常,Git 根据某一时刻暂存区(即 index 区域,下同)所表示的状态创建并记录一个对应的树对象。创建一个树对象,首先需要通过暂存一些文件来创建一个暂存区。 可以通过底层命令 update-index 为一个单独文件创建一个暂存区。可以通过 write-tree 命令将暂存区内容写入一个树对象。
此外,还可以通过调用 read-tree 命令,可以把树对象读入暂存区。 本例中,可以通过对 read-tree 指定 --prefix 选项,将一个已有的树对象作为子树读入暂存区
提交对象(commit object)
可以通过调用 commit-tree 命令创建一个提交对象,为此需要指定一个树对象的 SHA-1 值,以及该提交的父提交对象(如果有的话)。
$ echo 'first commit' | git commit-tree d8329f
fdf4fc3344e67ab068f836878b6c4951e3b15f3d
Git 所做的实质工作——将被改写的文件保存为数据对象,更新暂存区,记录树对象,最后创建一个指明了顶层树对象和父提交的提交对象。 这三种主要的 Git 对象——数据对象、树对象、提交对象——最初均以单独文件的形式保存在 .git/objects 目录下。 下面列出了目前示例目录内的所有对象,辅以各自所保存内容的注释:
$ find .git/objects -type f
.git/objects/01/55eb4229851634a0f03eb265b69f5a2d56f341 # tree 2
.git/objects/1a/410efbd13591db07496601ebc7a059dd55cfe9 # commit 3
.git/objects/1f/7a7a472abf3dd9643fd615f6da379c4acb3e3a # test.txt v2
.git/objects/3c/4e9cd789d88d8d89c1073707c3585e41b0e614 # tree 3
.git/objects/83/baae61804e65cc73a7201a7252750c76066a30 # test.txt v1
.git/objects/ca/c0cab538b970a37ea1e769cbbde608743bc96d # commit 2
.git/objects/d6/70460b4b4aece5915caf5c68d12f560a9fe3e4 # 'test content'
.git/objects/d8/329fc1cc938780ffdd9f94e0d364e0ea74f579 # tree 1
.git/objects/fa/49b077972391ad58037050f2a75f74e3671e92 # new.txt
.git/objects/fd/f4fc3344e67ab068f836878b6c4951e3b15f3d # commit 1
如果跟踪所有的内部指针,将得到一个类似下面的对象关系图:
对象存储
Git 以对象类型作为开头来构造一个头部信息,本例中是一个“blob”字符串。 接着 Git 会添加一个空格,随后是数据内容的长度,最后是一个空字节(null byte):
>> header = "blob #{content.length}\0"
=> "blob 16\u0000"
Git 会将上述头部信息和原始数据拼接起来,并计算出这条新内容的 SHA-1 校验和。
Git 会通过 zlib 压缩这条新内容。最后,需要将这条经由 zlib 压缩的内容写入磁盘上的某个对象。 要先确定待写入对象的路径(SHA-1 值的前两个字符作为子目录名称,后 38 个字符则作为子目录内文件的名称)。
就这样创建了一个有效的 Git 数据对象。 所有的 Git 对象均以这种方式存储,区别仅在于类型标识——另两种对象类型的头部信息以字符串“commit”或“tree”开头,而不是“blob”。 另外,虽然数据对象的内容几乎可以是任何东西,但提交对象和树对象的内容却有各自固定的格式。