拒绝硬背命令,提升git内功!从git的数据模型视角深入理解git
背景
笔者近来写代码时,由于没使用版本管理系统导致代码版本极为混乱,于是决定学习一下git的使用,看了很多博客之后,发现很多博客都是从git的命令入手,讲解命令的用处,对于git系统本身实际上没有进行太多讲解,更未提及git的数据模型等底层方案。学完之后对于git本身还是没能产生太多认知。于是查找多方资料后撰写此文,希望和我一样对于git本身认知不多的同学能够提升对于git的理解并快速上手git~
基础概念
git是什么
git是版本控制系统的一个标准,而不是一个特定的版本控制系统。市面上的版本控制系统有很多,如github,gitee,bitbucket等,但是这些版本控制系统的共同标准就是git。
git的数据对象
git中的数据对象可以分为三种:blob对象、树(tree)、提交(commit)
blob对象
在git中,一个blob对象指代的就是一个任意的文件。如上图所示,图中的README、LICENSE文件都是一个blob对象。
tree对象
一个tree对象指代的是一个目录,如上图所示,宽松的说,一个tree对象指的就是一个文件夹,和文件夹一样,目录中可以有任意blob对象或者嵌套任意目录。
快照
快照是最顶层的树,或者说,如果我们把一个仓库看成一棵树的话,如下所示:
这棵树表示了我作为示例的github仓库的文件结构。一个快照包括所有的目录对象与blob对象。或者说,每一个blob对象都可以看成文件树中的叶子节点,而tree对象则是所有非根且非叶子的节点,而 快照则指代的是根节点。是整个树而不是某一子树。
历史记录的建模方案
我们是希望通过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仓库为例:
假设我们在上一章的仓库新上传了一个文件,我们可以发现,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
: 指定故意不追踪的文件
本文引用
转载自:https://juejin.cn/post/7273756059495497763