git理论知识

本文翻译自:https://missing.csail.mit.edu/2020/version-control/

版本控制系统(VCSs)是一个追踪源代码改变的工具。

他的名字表明,这些工具帮助维持代码变更的历史,更进一步,这些工具能够促进团队的合作。VCSs在一系列快照中追踪文件夹和其内容的改变,每一个快照中封装了顶级文件夹中的文件/文件夹的整个状态。VCSs中也包含一些元信息,比如谁创建了每一个快照,快照所关联的信息,等等。

为什么VCSs是有用的?甚至当你独自工作的时候,它能够让你查看一个项目的历史快照,维护一个日志,记录每一个变更的具体原因,并行地工作在开发中的分支上,还有更多。当和他人协同工作的时候,它是一个珍贵的工具,可以查看其他人改变了什么,并且可以解决当前开发中的冲突。

现代化CSVs能够让你轻松地(并且通常是自动地)回答一些问题,比如:

  • 谁写了这些模块
  • 特定文件的特定行是什么时候修改的?被谁修改的?为什么被编辑?
  • 在过去的1000条修订中,什么时候/为什么一个特定的单元测试停止工作。

虽然存在其他的VCSs,但是git是版本控制的实际标准。

下面来自[XKCD comic][]的一张图包含了git的全部荣誉:

xkcd 1597

因为git的接口是十分抽象的,因此自顶向下学习git(从他的gui界面/命令行界面)学习会造成许多不必要的困惑。我们需要记住一些命令,可以解决许多常见的技术问题。

虽然git的界面十分丑陋,但是他潜在的设计和一些想法是十分优秀的。虽然丑陋的命令行界面能够被记住,更重要的是去理解git的优秀的设计。因此,我们自底向上对git进行解释,从他的数据模型开始,并且随后介绍他的命令行界面。一旦理解了数据模型,我们能够更好的按照他是如何去操控底层的数据模型的方式去理解命令。

Git的数据模型

你能够采取许多临时方法进行版本控制。git有一个经过深思熟虑的模型,这个模型能够让很多版本控制的特性得以实现,例如保持历史,支持分支,并且支持协同工作。

快照

git对一些顶层文件夹下面的文件夹和文件的集合的历史建模成一些快照。在git的术语中,一个文件被称作blob,并且他只是一堆字节。一个文件夹被称为tree,并且他能够映射names到blob或者其他的tree(文件夹中可以包含其他的文件夹)。一个快照是一个顶级的tree,这个tree是被追踪的。例如,我们可能有一个tree如下面所示:

<root> (tree)
|
+- foo (tree)
|  |
|  + bar.txt (blob, contents = "hello world")
|
+- baz.txt (blob, contents = "git is wonderful")

顶层的tree包含两个元素,一个baz.txt这个blob和一个foo tree,foo tree中包含bar.txt这个blob。

建模历史:关联快照

VCSs如何关联到快照?一个简单的模型是建立一个线性的history。这个history有一系列按照时间排序的快照。出于许多原因,git并没有使用这种简单的模型。

在git中,历史记录会形成一个快照的有向无环图。这听起来像一个奇特的数学单词,但是不要担心。这仅仅意味着git中的每一个快照都引用之前的快照。他有一组parent而不是单一的parent(这里的parent应该指的是较新的提交)。(因为后者会形成线性的history),因为当进行合并并行的分支的时候,parent快照会减少。

git称这些快照为commits(提交)。一个提交历史的可视化可能看上去像这样:

o <-- o <-- o <-- o
            ^
             \
              --- o <-- o

在上图中,o代表单次的提交(快照)。在第三次提交过后,这个历史分支变成了两条分离的分支。这可能会相当于,例如,两条分离的分支并行进行开发,彼此独立。在未来,这些分支可能可能会被合并并且创建一个新的提交,这个提交会结合这两条分支的内容,产生一个新的history看起来像这样:新创建的合并提交进行了加粗:

o <-- o <-- o <-- o <---- o
            ^            /
             \          v
              --- o <-- o

git中的提交是不可变的,然而:这并不意味着错误不能够被修正。仅仅是对历史提交的编辑事实上会创建一个新的提交,并且引用被更新指向最新的一次提交。

数据模型伪代码

看一下用伪代码写下的git数据模型可能会很有启发:

// a file is a bunch of bytes
type blob = array<byte>

// a directory contains named files and directories
type tree = map<string, tree | blob>

// a commit has parents, metadata, and the top-level tree
type commit = struct {
    parents: array<commit>
    author: string
    message: string
    snapshot: tree
}

这是一个历史的干净简单的模型。

对象和内容寻址

一个对象是一个blob,tree或者一次提交:

type object = blob | tree | commit

在git的数据仓库中,所有的对象都通过SHA-1 hash进行内容寻址。

objects = map<string, object>

def store(object):
    id = sha1(object)
    objects[id] = object

def load(id):
    return objects[id]

blobs,trees和提交以这种方式进行统一:他们全部都是objects。当它们引用其他的objects的时候,他们并不是以硬盘的表现形式拥有这些objects,但是通过hash对他们进行引用。

例如,上面的示例目录结构的tree(git cat-file -p 698281bc680d1995c5f4caaf3359721a5a58d48d)看起来像这样:

100644 blob 4448adbf7ecd394f42ae135bbeed9676e894af85    baz.txt
040000 tree c68d233a33c5c06e0340e4c224f0afca87c8ce87    foo

这个tree本身包含一个指向他自己内容的指针,baz.txt(blob)和foo(tree)。如果我们查看内容,通过指令git cat-file -p 4448adbf7ecd394f42ae135bbeed9676e894af85对baz.txt进行寻址,我们可能会的呆如下的内容:

git is wonderful

引用refrence

现在,所有的快照都能通过SHA-1hash进行标识。那是非常不方便的,因为人类不擅长记忆40个十六进制的字符。

git对这个问题的解决办法是对SHA-1hash创造一个人类可读的名字,这个名字被称为references。References是commit的指针。不像不可变的对象,引用是可变的(它能够被更新从而指向一个新的commit)。例如,master引用通常指向main分支中最新的提交。

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):
		//如果在references中存在name
    if name_or_id in references:
        return load(references[name_or_id])
    else:
	 //直接去store中load这个id
        return load(name_or_id)

通过这种方式,git能够使用人类可读的name,比如master去引用历史中的一个特定的快照,而不是使用冗长的十六进制字符串。

其中一个细节是,我们经常需要一个历史中 “当前位置 “的概念,这样当我们获取一个新快照时,就能知道它相对于什么(我们如何设置提交的父级字段)。在 Git 中,“当前位置 “是一个特殊的引用,叫做 HEAD

仓库Repos

最后,我们能够定义(粗糙)什么是git repos:他是objects和references组成的数据。

在磁盘上,所有 Git 存储的都是对象和引用:这就是 Git 数据模型的全部。所有 git 命令都是通过添加对象、添加/更新引用来操作提交 DAG 的。

无论何时你在敲任何git命令的时候,思考清楚这个命令在背后对图数据结构执行什么操作。反过来说,如果你想对提交 DAG 做某种特定的改动,比如 “丢弃未提交的改动,让’master’引用指向 5d83f9e 提交”,很可能就有命令可以做到(比如在本例中,git checkout master; git reset —hard 5d83f9e)。

暂存区域

这是另一个与数据模型无关的概念,但它是创建提交界面的一部分。

实现上述快照功能的一种方法是使用 “创建快照”(create snapshot)命令,根据工作目录的当前状态创建新的快照。有些版本控制工具可以这样做,但 Git 不行。我们需要的是干净的快照,而从当前状态创建快照并不总是理想的做法。举个例子,假设你实现了两个不同的功能,你想创建两个不同的提交,第一个引入第一个功能,下一个引入第二个功能。或者设想一下,在代码中添加了大量调试打印语句,同时还进行了错误修复;你想提交错误修复,同时丢弃所有打印语句。

Git 通过 “暂存区域 “机制,允许你指定哪些修改应包含在下一个快照中。

git命令行接口

为了避免重复的信息,我们不准备详细解释下面的命令。

Basics

  • git help <command>: get help for a git command
  • git init: creates a new git repo, with data stored in the .git directory
  • git status: tells you what’s going on
  • git add <filename>: adds files to staging area
  • git commit: creates a new commit
  • git log: shows a flattened log of history
  • git log --all --graph --decorate: visualizes history as a DAG
  • git diff <filename>: show changes you made relative to the staging area
  • git diff <revision> <filename>: shows differences in a file between snapshots
  • git checkout <revision>: updates HEAD and current branch

Branching and merging

  • git branch: shows branches
  • git branch <name>: creates a branch
  • git checkout -b <name>: creates a branch and switches to it
    • same as git branch <name>; git checkout <name>
  • git merge <revision>: merges into current branch
  • git mergetool: use a fancy tool to help resolve merge conflicts
  • git rebase: rebase set of patches onto a new base

Remotes

  • git remote: list remotes
  • git remote add <name> <url>: add a remote
  • git push <remote> <local branch>:<remote branch>: send objects to remote, and update remote reference
  • git branch --set-upstream-to=<remote>/<remote branch>: set up correspondence between local and remote branch
  • git fetch: retrieve objects/references from a remote
  • git pull: same as git fetch; git merge
  • git clone: download repository from remote

Undo

  • git commit --amend: edit a commit’s contents/message
  • git reset HEAD <file>: unstage a file
  • git checkout -- <file>: discard changes
  • git reset HEAD~ --soft;取消上一次提交的结果

Advanced Git

  • git config: Git is highly customizable
  • git clone --depth=1: shallow clone, without entire version history
  • git add -p: interactive staging
  • git rebase -i: interactive rebasing
  • git blame: show who last edited which line
  • git stash: temporarily remove modifications to working directory
  • git bisect: binary search history (e.g. for regressions)
  • .gitignore: specify intentionally untracked files to ignore