Missing Semester Notes - Version Control (git)

2020-05-22

版本控制系统就是用来追踪文件或文件夹改变的工具。一方面可以用来维护文件修改的历史,另一方面它促进了多人协作。版本控制这么有用是为啥呢?就算你一个人工作,你也可以通过查看历史变更或者快照来理解当时的情景,或者是能让你同时在多个分支上并行的工作。如果与他人合作的话,那更是个无价之宝了。

原文链接: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: 指定一些文件不被跟踪

资源

Missing Semesternotesgit

Missing Semester Notes - Debugging and Profiling

Missing Semester Notes - Command-line Environment