牛逼的Git!!!!!!!
大家好,我是沉默王二。
顾名思义,版本控制系统(Version Control System)是一类用于追踪源代码改动的工具,这些工具可以帮助我们管理代码的历史记录,不仅如此,还可以让协作编码变得轻而易举。
VCS 通过一系列的快照(Snapshots)将某个文件夹以及内容保存起来,每个快照都包含了文件夹的完整状态。同时,VCS 还维护者快照的创建者信息以及其他相关信息。
大家都知道,版本控制系统非常重要!!!!!!即便你只是一个人在编码,它也可以帮助你创建项目的快照、记录每个改动、创建不同的分支等等。如果你参与的是多人协作,它更是一个无价之宝,你不仅可以看到别人对代码的修改,还可以同时解决由于并行开发带来的冲突。
版本控制系统可以轻松地帮助我们解决这些棘手的问题:
当前模块是谁编写的? 这个文件的这一行代码是什么时候被修改的?是谁做出的修改?修改的原因是什么? 最近的 100 个版本中,哪个版本导致单元测试失败了?
版本控制系统有很多,其中最突出的代表就是 Git——关于它诞生的历史,可以参照我之前分享的一篇内容:一次改变世界的代码提交。
如果我们从命令行接口开始学习 Git 的话,会感到非常的困惑,很多时候只能死记硬背一些命令行,然后像变魔法一样使用它们,一旦出现问题,就只能先保存一个分支,然后删掉当前项目,重新下载一份新的拷贝。
尽管 Git 的接口有些难懂,但它底层的设计和思想却非常的优雅。难懂的接口只能靠死记硬背,但优雅的底层设计则非常容易理解。我们可以通过一种自底向上的方式来学习 Git,先了解底层的数据模型,再学习它的接口。可以这么说,一旦搞懂了 Git 的数据模型,再学习它的接口并理解这些接口是如何操作数据模型的就非常容易了。
进行版本控制的方法很多,Git 拥有一个精心设计的模型,这使其能够支持版本控制所需的所有特性,比如维护历史记录、支持分支和团队协作。
Git 将顶级目录中的文件和文件夹称作集合,并通过一系列快照来管理历史记录。在 Git 的术语中,文件被称为 blob 对象(数据对象),也就是一组数据。目录则被称为 tree(树),目录中可以包含文件和子目录。
<root> (tree)
|
+- foo (tree)
| |
| + bar.txt (blob, contents = "hello world")
|
+- baz.txt (blob, contents = "git is wonderful")
顶层的树(也就是 root) 包含了两个元素,一个名为 foo 的子树(包含了一个 blob 对象“bar.txt”),和一个 blob 对象“baz.txt”。
版本控制系统是如何和快照进行关联的呢?线性历史记录是一种最简单的模型,它包含了一组按照时间顺序线性排列的快照。不过,出于种种原因,Git 没有采用这种模型。
在 Git 中,历史记录是一个由快照组成的有向无环图。“有向无环图”,听起来很高大上,但其实并不难理解。我们只需要知道这代表 Git 中的每个快照都有一系列的父辈,也就是之前的一系列快照。这些快照通常被称为“commit”,看起来好像是下面这样:
o <-- o <-- o <-- o
^
\
--- o <-- o
o 表示一次 commit,也就是一次快照。箭头指向了当前 commit 的父辈。在第三次 commit 之后,历史记录分叉成了两条独立的分支,这可能是因为要同时开发两个不同的特性,它们之间是相互独立的。开发完成后,这些分支可能会被合并为一个新的 commit,这个新的 commit 会同时包含这些特性,看起来好像是下面这样:
o <-- o <-- o <-- o <---- o
^ /
\ v
--- o <-- o
Git 中的 commit 是不可改变的。当然了,这并不意味着不能被修改,只不过这种“修改”实际上是创建了一个全新的提交记录。
以伪代码的形式来学习 Git 的数据模型,可能更加通俗易懂。
// 文件是一组数据
type blob = array<byte>
// 一个包含了文件和子目录的目录
type tree = map<string, tree | file>
// 每个 commit 都包含了一个父辈,元数据和顶层树
type commit = struct {
parent: array<commit> // 父辈
author: string // 作者
message: string // 信息
snapshot: tree // 快照
}
Git 中的对象可以是 blob、tree 或者 commit:
type object = blob | tree | commit
Git 在存储数据的时候,所有的对象都会基于它们的安全散列算法进行寻址。
objects = map<string, object>
def store(object):
id = sha1(object)
objects[id] = object
def load(id):
return objects[id]
blob、tree 和 commit 一样,都是对象。当它们引用其他对象时,并没有真正在硬盘上保存这些对象,而是仅仅保存了它们的哈希值作为引用。
还记得之前的例子吗?
root 引用的 foo 和 baz.txt 就像下面这样:
100644 blob 4448adbf7ecd394f42ae135bbeed9676e894af85 baz.txt
040000 tree c68d233a33c5c06e0340e4c224f0afca87c8ce87 foo
所有的快照都可以通过它们的哈希值来标记,但 40 位的十六进制字符实在是太难记了,很不方便。针对这个问题,Git 的解决办法是给这些哈希值赋予一个可读的名字,也就是引用(reference),引用是指向 commit 的指针,与对象不同,它是可变的,可以被更新,指向新的 commit。通常,master 引用通常会指向主分支的最新一次 commit。
references = map<string, string>
def update_reference(name, id):
references[name] = id
def read_reference(name):
return references[name]
def load_reference(name_or_id):
if name_or_id in references:
return load(references[name_or_id])
else:
return load(name_or_id)
这样,Git 就可以使用“master”这样容易被记住的名称来表示历史记录中特定的 commit,而不需要再使用一长串的十六进制字符了。
在 Git 中,当前的位置有一个特殊的索引,它就是“HEAD”。
在硬盘上,Git 仅存储对象和引用,因为其数据模型仅包含这些东西。所有的 git 命令都对应着对 commit 树的操作。
Git 中还包含了一个和数据模型完全不行管的概念,叫做“暂存区”,它运行我们指定下次快照中要包含哪些改动。
下面,我们来看一下常用的 git 命令行接口,包含基础、分支与合并、远端操作、撤销和高级操作。
1)基础
git help <command>
: 获取 git 命令的帮助信息git init
: 创建一个新的 git 仓库,其数据会存放在一个名为.git
的目录下git status
: 显示当前的仓库状态git add <filename>
: 添加文件到暂存区git commit
: 创建一个新的提交git log
: 显示历史日志git log --all --graph --decorate
: 可视化历史记录(有向无环图)git diff <filename>
: 显示与上一次提交之间的差异git diff <revision> <filename>
: 显示某个文件两个版本之间的差异git checkout <revision>
: 更新 HEAD 和目前的分支
2)分支与合并
git branch
: 显示分支git branch <name>
: 创建分支git checkout -b <name>
: 创建分支并切换到该分支git merge <revision>
: 合并到当前分支git mergetool
: 使用工具来处理合并冲突
3)远端操作
git remote
: 列出远端git remote add <name> <url>
: 添加一个远端git push <remote> <local branch>:<remote branch>
: 将对象传送至远端并更新远端引用git branch --set-upstream-to=<remote>/<remote branch>
: 创建本地和远端分支的关联关系git fetch
: 从远端获取对象/索引git pull
: 相当于git fetch; git merge
git clone
: 从远端下载仓库
4)撤销
git commit --amend
: 编辑提交的内容或信息git reset HEAD <file>
: 恢复暂存的文件git checkout -- <file>
: 丢弃修改
5)高级操作
git config
: 定制化git clone --shallow
: 克隆仓库,但是不包括版本历史信息git add -p
: 交互式暂存git blame
: 查看最后修改某行的人git stash
: 暂时移除工作目录下的修改内容git bisect
: 通过二分查找搜索历史记录.gitignore
: 指定不追踪的文件
怎么样?这样学 Git 是不是就容易多了?先从跟上理解了 Git 的数据模型,然后在执行命令的时候去思考这些命令是如何操作数目模型的,就会不那么枯燥了。
另外,强烈推荐大家阅读一下这份 Pro Git 中文版:
我已经下载好了,大家可以在我的公众号「沉默王二」后台回复「git」获取(无套路,没加密)。
大家记得帮二哥点赞了,这样更新起来的动力会更足,笔芯~