likes
comments
collection
share

拒绝硬背命令,提升git内功!从git的数据模型视角深入理解git

作者站长头像
站长
· 阅读数 15

背景

笔者近来写代码时,由于没使用版本管理系统导致代码版本极为混乱,于是决定学习一下git的使用,看了很多博客之后,发现很多博客都是从git的命令入手,讲解命令的用处,对于git系统本身实际上没有进行太多讲解,更未提及git的数据模型等底层方案。学完之后对于git本身还是没能产生太多认知。于是查找多方资料后撰写此文,希望和我一样对于git本身认知不多的同学能够提升对于git的理解并快速上手git~

基础概念

git是什么

git是版本控制系统的一个标准,而不是一个特定的版本控制系统。市面上的版本控制系统有很多,如github,gitee,bitbucket等,但是这些版本控制系统的共同标准就是git。

git的数据对象

git中的数据对象可以分为三种:blob对象、树(tree)、提交(commit)

拒绝硬背命令,提升git内功!从git的数据模型视角深入理解git

blob对象

在git中,一个blob对象指代的就是一个任意的文件。如上图所示,图中的README、LICENSE文件都是一个blob对象。

tree对象

一个tree对象指代的是一个目录,如上图所示,宽松的说,一个tree对象指的就是一个文件夹,和文件夹一样,目录中可以有任意blob对象或者嵌套任意目录。

快照

快照是最顶层的树,或者说,如果我们把一个仓库看成一棵树的话,如下所示:

拒绝硬背命令,提升git内功!从git的数据模型视角深入理解git

这棵树表示了我作为示例的github仓库的文件结构。一个快照包括所有的目录对象与blob对象。或者说,每一个blob对象都可以看成文件树中的叶子节点,而tree对象则是所有非根且非叶子的节点,而 快照则指代的是根节点是整个树而不是某一子树。

历史记录的建模方案

我们是希望通过git管理某一个项目的历史记录,那么历史记录和快照有什么关系呢?

在 Git 中,历史记录是一个由快照组成的有向无环图。如果读者不了解有向无环图的概念,这就是指 Git 中的每个快照都有一系列的“父辈”,也就是其之前的一系列快照。注意,快照具有多个“父辈”而非一个,因为某个快照可能由多个父辈而来。 以下方的历史记录为例:

拒绝硬背命令,提升git内功!从git的数据模型视角深入理解git 假设某一仓库的历史记录如上图所示,蓝圈代表一个快照,绿色箭头代表快照间的前后关系。在历史纪录中,我们将每一个快照都称为一次提交

每一个箭头指向的是前一版本的提交,或者说每个快照都指向了自己作为修改基础的快照。在第三次提交之后,历史记录分岔成了两条独立的分支。这可能因为此时需要同时开发两个不同的特性,它们之间是相互独立的。开发完成后,这些分支可能会被合并,并创建一个新的提交,这个新的提交会同时包含这些特性。新的提交会创建一个新的历史记录。

git中的提交是不可改变的,如果想修复某个提交中代码的错误,就只能在该提交基础上修改完错误后创建一个新的提交。

数据模型及其伪代码表示

为更加清晰的表达数据模型中的基础概念,以伪代码的形式来描述 Git 的数据模型:

// 文件就是一组数据
type blob = array<byte>

// 一个包含文件和目录的目录
type tree = map<string, tree | blob>

// 每个提交都包含一个父辈,元数据和顶层树
type commit = struct {
    parent: array<commit>
    author: string
    message: string
    snapshot: tree
}

对象与寻址索引

Git 中的对象可以是 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]

Blobs、树和提交都一样,它们都是对象。当它们引用其他对象时,他们仅仅保存了被引用对象的哈希值,类似于浅拷贝,而没有真的拷贝一份被引用的对象。

上一章中,作为例子的github项目,如果采用哈希值进行表示,可能如下所示:

100644 blob 4448adbf7ecd394f42ae135bbeed9676e894af85    README.md
040000 tree c68d233a33c5c06e0340e4c224f0afca87c8ce87    目录(文件夹)
100752 blob 698281bc680d1995c5f4caaf3359721a5a58d48d    LICENSE

通过git cat-file -p 4448adbf7ecd394f42ae135bbeed9676e894af85命令,可以查看README.md文件中所存储的信息。由此也可证明通过哈希值来对文件进行索引。

引用(references)

为了让git的使用者能方便地找到某一次提交,而又不需要记忆提交的哈希值,git给这些哈希值赋予人类可读的名字,这个名字就是引用(references) 引用是指向提交的指针(注意,这里的指针的概念与C++/C中的指针含义相近,却又稍有不同)。与对象不同的是,引用是可变的(引用所指的内容可以被更新,指向新的提交),仍以github仓库为例:

拒绝硬背命令,提升git内功!从git的数据模型视角深入理解git

拒绝硬背命令,提升git内功!从git的数据模型视角深入理解git

假设我们在上一章的仓库新上传了一个文件,我们可以发现,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):
    if name_or_id in references:
        return load(references[name_or_id])
    else:
        return load(name_or_id)

这样,Git 就可以使用诸如 main 这样人类可读的名称来表示历史记录中某个特定的提交,而不需要在使用一长串十六进制字符了。

git仓库

最后,我们可以粗略地给出 Git 仓库的定义了:一个git仓库包括两部分:对象 和 引用

在硬盘上,Git 仅存储对象和引用:因为其数据模型仅包含这些东西。所有的 git 命令都对应着对提交树的操作,例如增加对象,增加或删除引用。

既然已经看到这里,当读者输入某个指令时,请思考一下这条命令是如何对底层的图数据结构进行操作的。另一方面,当读者希望修改提交树,例如“丢弃未提交的修改和将 ‘master’ 引用指向提交 5d83f9e 时,有什么命令可以完成该操作(针对这个具体问题,您可以使用 git checkout master; git reset --hard 5d83f9e

Git 的命令行接口

为了避免冗余信息,本文将不会详细解释以下命令行。

基础

  • git help <command>: 获取 git 命令的帮助信息
  • git init: 创建一个新的 git 仓库,其数据会存放在一个名为 .git 的目录下
  • git status: 显示当前的仓库状态
  • git add <filename>: 添加文件到暂存区
  • git commit: 创建一个新的提交
  • git log: 显示历史日志
  • git log --all --graph --decorate: 可视化历史记录(有向无环图)
  • git diff <filename>: 显示与暂存区文件的差异
  • git diff <revision> <filename>: 显示某个文件两个版本之间的差异
  • git checkout <revision>: 更新 HEAD 和目前的分支

分支和合并

  • git branch: 显示分支
  • git branch <name>: 创建分支
  • git checkout -b <name>: 创建分支并切换到该分支
    • 相当于 git branch <name>; git checkout <name>
  • git merge <revision>: 合并到当前分支
  • git mergetool: 使用工具来处理合并冲突
  • git rebase: 将一系列补丁变基(rebase)为新的基线

远端操作

  • git remote: 列出远端
  • git remote add <name> <url>: 添加一个远端
  • git push <remote> <local branch>:<remote branch>: 将对象传送至远端并更新远端引用
  • git branch --set-upstream-to=<remote>/<remote branch>: 创建本地和远端分支的关联关系
  • git fetch: 从远端获取对象/索引
  • git pull: 相当于 git fetch; git merge
  • git clone: 从远端下载仓库

撤销

  • git commit --amend: 编辑提交的内容或信息
  • git reset HEAD <file>: 恢复暂存的文件
  • git checkout -- <file>: 丢弃修改
  • git restore: git2.32版本后取代git reset 进行许多撤销操作

Git 高级操作

  • git config: Git 是一个 高度可定制的 工具
  • git clone --depth=1: 浅克隆(shallow clone),不包括完整的版本历史信息
  • git add -p: 交互式暂存
  • git rebase -i: 交互式变基
  • git blame: 查看最后修改某行的人
  • git stash: 暂时移除工作目录下的修改内容
  • git bisect: 通过二分查找搜索历史记录
  • .gitignore: 指定故意不追踪的文件

本文引用

  1. Pro Git
  2. Git for Computer Scientists 
  3. missing.csail.mit.edu/