Git Intro

Git是一种版本控制系统(VCS),用于追踪源代码或者其它文件更改。也就是说,这些工具可以帮助我们管理代码修改的历史记录,并且允许我们在不同版本之间切换。
如果你喜欢玩游戏,比如赛博朋克2077,那么Git对你来说会相当容易理解。想象一下,你在玩一个开放世界的游戏,可以随时保存你的进度。万一boss战的时候一不小心挂了,你可以回到之前的存档点,重新开始战斗,而不是从头再来一遍。Git就像是你游戏中的存档系统,帮助你保存代码的不同版本,让你可以随时回到之前的状态。
但是Git的强大之处不仅在于它能够不时地给代码和文件保存进度,实际上它甚至允许你为当前文件创建分支,这样你就可以在不影响主线代码的情况下进行实验和修改,而在2077中就没办法这么做了,除非你重头再玩一遍。
Git的另一个重要特点是它是分布式的,这意味着每个开发者的机器上都有完整的代码库和历史记录的副本。这样,即使中央服务器出现问题,开发者们仍然可以继续工作,并在服务器恢复后同步他们的更改。这就像是steam云存档功能,即使你换了一台电脑,登陆同一个steam账号,你的游戏进度依然会在云端保存着,反之亦然。
总的来说,Git是一个强大且灵活的工具,适用于个人项目和大型协作开发。通过使用Git,开发者可以更好地管理代码的变化,提高工作效率,并确保代码的安全性和完整性。

然而,Git也有一些缺点,比如抽象泄露问题,即通过自顶向下的学习方式可能让人感到困惑。但是是它的底层思想却是简单优雅的。因此,我们会自底向上开始学习Git,这样再学习它的接口时就会更加容易理解。

Git的数据模型

快照

Git将根目录及其下所有文件和文件夹作为一个整体来管理其历史记录。在Git中,文件被视为一组数据块,作为对象,它被称为Blob(Binary Large Object)对象;文件夹则被称为树(Tree)对象。快照(snapshot)则是被追踪的最顶层的目录的树对象。
那么Git是如何通过这些对象来管理版本历史记录的呢?最简单的方式是线性的历史记录,即包含一系列快照,每个快照包含一个指向前一个快照的引用。但是Git没有采用这样的方式,而是使用了一个由快照组成的有向无环图来存储版本历史记录。在Git中,这些快照被称为提交(commit),可以用下面的图来表示:

1
2
3
4
o<--o<--o<--o
^
|
o<--o

在上面的图中,每个圆圈代表一个快照,而箭头表示快照之间的引用关系。可以看到,Git允许多个快照指向同一个前驱快照,这是因为我们可能需要同时独立开发两个不同的特性,而这两个特性都基于同一个代码版本进行开发。开发完成后,我们可以创建一个提交来合并这两个分支,它将同时包含两个分支的更改。比如:

1
2
3
4
o<--o<--o<--o<--+
^ |
| |
o<--o<--o

在上面的图中,最后一个提交包含了两个前驱提交的更改,这样我们就可以将两个分支的代码合并到一起。Git的提交是不可改变的,但是我们可以通过创建新的提交来记录代码的更改,从而形成一个完整的版本历史记录。

用伪代码表示数据模型

为了更好地理解Git的数据模型,我们可以使用伪代码来表示Git中的对象和它们之间的关系。下面是一个简单的伪代码示例,展示了Git中的Blob对象、Tree对象和Commit对象:

1
2
3
4
5
6
7
8
type blob = array<byte>
type tree = map<string, tree | blob>
type commit = {
parents: array<commit>,
author: string,
message: string,
snapshot: tree,
}

在上面的伪代码中,我们定义了三种类型的对象:

  • Blob对象:表示文件内容,是一个字节数组。
  • Tree对象:表示目录结构,是一个映射,键是文件或子目录的名称,值可以是另一个Tree对象或Blob对象。
  • Commit对象:表示一个提交,包含一个父提交数组、作者信息、提交信息和一个快照(Tree对象)。

通过这种方式,我们可以清晰地看到Git是如何组织和管理代码的版本历史记录的。每个提交都包含一个快照,快照中包含了文件和目录的结构,而这些文件和目录又由Blob和Tree对象组成。这样,我们就可以通过提交来追踪代码的变化,并且可以随时回到之前的版本。

内存对象和寻址

在Git中,所有的对象(Blob、Tree和Commit)都是通过SHA-1哈希值来唯一标识的。SHA-1是一种加密哈希函数,它将任意长度的数据映射为一个固定长度的160位(20字节)哈希值。Git使用SHA-1哈希值作为对象的地址,这样我们就可以通过哈希值来访问和管理这些对象。

1
2
3
4
5
6
7
8
9
type object = blob | tree | commit
objects = map<string, object>

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

def load(id):
return objects[id]

在上面的伪代码中,我们定义了一个对象存储系统,其中store函数用于将对象存储到一个映射中,并返回该对象的SHA-1哈希值作为其唯一标识符。load函数则用于通过哈希值来加载对象。因此,实际上电脑硬盘并没有真的保存这些对象,而只是保存了它们的哈希值作为引用。

引用

现在,所有的快照都可以用它们的哈希值来表示了,但是这也太难记了。所以Git的设计者Linus引入了引用(reference)的概念。引用是一个可变的指针,它指向一个特定的对象(通常是一个提交)。通过使用引用,我们让快照的名字更容易读取和使用了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
references = map<string, string> // name -> object id

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:
id = read_reference(name_or_id)
else:
id = name_or_id
return load(id)

在上面的伪代码中,我们定义了一个引用存储系统,其中update_reference函数用于更新引用的指向,read_reference函数用于读取引用所指向的对象的哈希值。load_reference函数则用于加载引用所指向的对象,无论是通过引用名称还是直接通过哈希值。
这样,我们就可以使用易于理解的名称来引用特定的提交,而不需要记住复杂的哈希值。例如,我们可以使用main作为主分支的引用名称,这样我们就可以通过load_reference("main")来加载主分支的最新提交。同时,通过更新引用,我们可以轻松地将分支指向新的提交,从而实现代码的版本管理。

Git的命令行接口

请阅读Pro Git获取更多信息。

下面是一些常用的Git命令

  • git help:显示Git的帮助信息。

  • git init:初始化一个新的Git仓库,在当前目录下创建一个.git子目录。

  • git status:显示当前工作目录和暂存区的状态,包括已修改但未提交的文件。

  • git add <file>:将指定的文件添加到暂存区,准备提交。

  • git commit -m "message":将暂存区的更改提交到仓库,并附加提交信息。

  • git log:显示提交历史记录,包括每个提交的哈希值、作者和提交信息。

  • git log --all --gragh --decorate:以图形化方式显示所有分支的提交历史记录。

  • git diff:显示工作目录和暂存区之间的差异。

  • git diff <revision> <filename>:显示指定修订版本和当前文件之间的差异。

  • git checkout <branch>:切换到指定的分支。

  • git branch:显示分支。

  • git branch <branch>:创建一个新分支。

  • git checkout -b <branch>:创建并切换到一个新分支。

  • git merge <branch>:将指定分支的更改合并到当前分支。

  • git mergtool:启动合并工具以解决合并冲突。

  • git rebase: 将一系列补丁变基作为新的基线。

  • git remote: 显示远程仓库。

  • git remote add <name> <url>:添加一个新的远程仓库。

  • git fetch <remote>:从远程仓库获取最新的更改,但不合并。

  • git pull <remote> <branch>:从远程仓库拉取最新的更改并合并到当前分支。

  • git push <remote> <branch local>:<branch remote>:将当前分支的更改推送到远程仓库。

  • git clone <url>:克隆一个远程仓库到本地。

  • git commit --amend:修改最后一次提交的信息或内容。

  • git reset HEAD <file>:将指定文件从暂存区移除,但保留工作目录中的更改。

  • git checkout -- <file>:丢弃工作目录中的更改,恢复到上次提交的状态。

  • git restore: 恢复工作目录中的文件到指定状态。

2026年1月13日