版本控制系统就是用来追踪文件或文件夹改变的工具。一方面可以用来维护文件修改的历史,另一方面它促进了多人协作。版本控制这么有用是为啥呢?就算你一个人工作,你也可以通过查看历史变更或者快照来理解当时的情景,或者是能让你同时在多个分支上并行的工作。如果与他人合作的话,那更是个无价之宝了。
原文链接:https://missing.csail.mit.edu/2020/version-control/
现代版本控制系统可以方便的给你这些问题的答案:
- 这玩意谁写的
- 这行谁改的?为啥改的?
- 在过去的1000个版本中,啥时候以及为啥导致某个东西崩了
Git就是一个非常牛逼的版本管理系统。Git有一个抽象的命令行界面,如果你想从命令行界面入手学习Git(也就是背命令)会不那么舒服。但是Git的内在是非常美丽的,我们从原理与设计开始自底向上的解释Git。
Git的数据模型
版本控制的方法有很多。Git使用了一个经过精心设计的模型,使得它可以使用所有的版本控制特性,例如维护历史,支持分直,可以多人协作等。
快照
Git将顶层目录中的文件与文件夹的集合的变更历史建模为一系列快照。在Git术语中,一个文件被称为一个"blob”,并视为一个字节串。一个目录被称为"tree”,它将名称映射到对应的"blob"或"tree"上(所以目录才可以嵌套)。一个快照是一个被跟踪的顶层"tree”。例如我们有如下的目录结构:
<root> (tree)
|
+- foo (tree)
| |
| + bar.txt (blob, contents = "hello world")
|
+- baz.txt (blob, contents = "git is wonderful")
顶层tree包含了两个元素,一个tree “foo”(它自己还包含了一个元素,blob “bar.txt”),和一个blob “baz.txt”。
建模历史:相关快照
版本控制系统怎么将快照之间建立联系呢?一个简单的model将会仅仅有一个线性的历史。一个历史将会是一个遵循时间顺序的快照序列。基于多种原因,Git不会使用这种简单的模型。
在Git中,历史使用快照的有向无环图来表示。这表示每一个快照都有一个"parents"集合,表示该快照的来源(不是仅有一个,因为一个快照可能有平行的分支们合并而来)。
Git中称快照为"commit”。可视化一个commit历史我们大概能看到这样的东西:
o <-- o <-- o <-- o
^
\
--- o <-- o
“图”中的o
表示每一个commit(快照)。箭头指向commit的parents,也就是来源。所以图上表示在第三个commit之后,新的分支出现了。在实际中这可能代表在开发中并行开发两个新的独立的特性。当这些特性都开发完毕并且要合并并创建一个新的快照时,新的历史可能长这个样子:
o <-- o <-- o <-- o <---- o
^ /
\ v
--- o <-- o
Commit在Git中是不可修改的。这不意味着犯了错你无法修正,而是意味着你的修改将会创建一个全新的commit,并使得所有直接或间接指向它的commit发生改变。
数据模型的伪代码表示
看看这段伪代码:
// a file is a bunch of bytes
type blob = array<byte>
// a directory contains named files and directories
type tree = map<string, tree | file>
// a commit has parents, metadata, and the top-level tree
type commit = struct {
parent: array<commit>
author: string
message: string
snapshot: tree
}
对象与内容寻址
一个对象可以是blob,tree或者commit
type object = blob | tree | commit
在Git的数据存储中,所有的对象都用它们的SHA-1哈希值来寻址。
objects = map<string, object>
def store(object):
id = sha1(object)
objects[id] = object
def load(id):
return objects[id]
Blob,tree和commit使用这种方法进行了统一:他们都是对象。当它们引用其他对象的时候,它们并不真的包含了它们在硬盘上的内容,而是直接引用他们的哈希值。
举个栗子,上面作为例子的目录结构(使用git cat-file -p 698281bc680d1995c5f4caaf3359721a5a58d48d
进行可视化的)会看起来是这样:
100644 blob 4448adbf7ecd394f42ae135bbeed9676e894af85 baz.txt
040000 tree c68d233a33c5c06e0340e4c224f0afca87c8ce87 foo
tree自己包含有指向它的内容的指针:一个blobbaz.txt
与一个treefoo
。如果我们查看baz.txt对应的哈希内容(使用git cat-file -p 4448adbf7ecd394f42ae135bbeed9676e894af85
),我们会得到
git is wonderful
引用
好了,现在所有的快照都可以被它们的SHA-1哈希表示了。不太方便的是它们实在太长了,谁能随便记住40位十六进制数呢?
Git的解决方案是给这些哈希值一个可读的名字,叫做他们的引用。引用是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)
有了这个,我们就可以给特定的一个或者一些commit起名字而不是用哈希值来表示了。
我们经常需要一个名字来表示我们当前指向的快照,这样我们在创建新的快照的时候就可以直接知道它的parent该指向谁了。在Git中这个引用叫HEAD
仓库
最终,我们可以粗糙的定义一下Git仓库:它是数据对象和引用。
在硬盘中,Git存储的是对象和引用,这就是Git数据模型的全部。所有的Git命令都对应着在commit的有向无环图上的操作,通过增加对象和增改引用。
不论何时你键入何种命令,想想它对应在“图”上的哪种操作吧!相反的,当你想要在commit有向无环图上做任何事情的时候,都有对应的命令可以做到。例如你想丢弃尚未commit的更改并使得master
指向commit5d83f9e
,它对应的命令是git checkout master; git reset --hard 5d83f9e
。
暂存区
这是一个与数据模型正交的概念,它是创建commit的界面。
一种你可以想象到的创建如上文所述的快照的方法是通过一跳"create snapshot"命令创造一个基于当前状态的快照。一些版本控制系统这样搞但是Git不是。我们需要一个干净的快照,而且直接从当前的状态创建很可能不太合适。例如,想象一个场景实现了两个特性,你希望创建两个分开的commit。或者这样一个场景,你在你修bug的时候加了一些调试输出命令然而你只需要把你修bug的代码交上去。
所以Git允许你指定你要提交的修改,这个机制叫“暂存区”
Git命令行界面
这里就不仔细介绍每个命令了,去看Pro Git吧!当然也有视频课程。
基础
- git help
: 帮助 - git init: 原地见库,数据放在
.git
目录里 - git status: 现在库的状态
- git add
: 扔文件进暂存区 - git commit: 创建commit
- git log: 展开式的显示log
- git log –all –graph –decorate: 以有向无环图的形式展示log
- git diff
: 从上次提交到现在的变化 - git diff
: 两次快照之间的变化 - git checkout
: 更新当前分支与 HEAD
引用
分支与合并
- git branch: 显示分支
- git branch
: 创建分支 - git checkout -b
: 创建分支并切换过去,与 same as git branch <name>; git checkout <name>
等价 - git merge
: 合并入当前分支 - git mergetool: 用一个牛逼的工具解决合并冲突
- git rebase: 变…变基
远程
- git remote: 列出远端
- git remote add
: 增加一个远端 - git push
: : 上传对象到远程端,并更新远程端指针 - git branch –set-upstream-to=
/ : 设置本地与远端的对应关系 - git fetch: 从远程端检索对象或引用
- git pull:
git fetch; git merge
- git clone: 从远端拖库下来
Undo
- git commit –amend: 编辑当前commit的内容和提交信息
- git reset HEAD
: 从暂存区踢掉文件 - git checkout –
: 丢弃变化
高级话题
- git config: Git是高度可配置的
- git clone –shallow: 在拖库的时候不把整个历史弄下来
- git add -p: 交互式暂存
- git rebase -i: 交互式变基
- git blame: 看看是谁改的这一行
- git stash: 暂时性的从工作区中移除改变
- git bisect: 二分搜索
- .gitignore: 指定一些文件不被跟踪
资源
- Pro Git一定要看,1-5章教你怎么好好用Git。在最后一章有些高级话题。
- Oh Shit,Git!? 很短的指南,告诉你犯错了怎么办
- Git for Computer Scientists简要的介绍了Git的数据模型,又很多比这篇文章还好看的图标
- Git from the Bottom Up解释了Git的一些实现细节
- How to explain git in simple words
- Learn Git Branching基于浏览器的游戏来教你用Git