3

Missing Semester Notes - Version Control (git)

 2 years ago
source link: https://www.saltyfish.win/posts/missing-semester-notes-05/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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: 从远端拖库下来
  • 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: 指定一些文件不被跟踪

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK