Git 数据模型与命令行
学习 Git 需要自顶向下学习,先了解数据模型,再去学习命令行,思考命令行对数据模型做了哪些操作。
概览
版本控制系统 (VCSs) 是一类用于追踪源代码(或其他文件、文件夹)改动的工具。顾名思义,这些工具可以帮助我们管理代码的修改历史;不仅如此,它还可以让协作编码变得更方便。VCS通过一系列的快照将某个文件夹及其内容保存了起来,每个快照都包含了文件或文件夹的完整状态。同时它还维护了快照创建者的信息以及每个快照的相关信息等等。
尽管版本控制系统有很多, 其事实上的标准则是 Git 。
但因为 Git 接口的抽象泄漏(leaky abstraction)问题,通过自顶向下的方式(从命令行接口开始)学习 Git 可能会让人感到非常困惑。
尽管 Git 的接口有些丑陋,但是它的底层设计和思想却是非常优雅的。因此,我们将通过一种自底向上的方式向您介绍 Git,从数据模型开始,最后再学习它的接口。
Git 的数据模型
进行版本控制的方法很多。Git 拥有一个经过精心设计的模型,这使其能够支持版本控制所需的所有特性,例如维护历史记录、支持分支和促进协作。
快照
Git 将顶级目录中的文件和文件夹作为集合,并通过一系列快照来管理其历史记录。在Git的术语里,文件被称作Blob对象(数据对象),也就是一组数据。目录则被称之为“树”,它将名字与 Blob 对象或树对象进行映射(使得目录中可以包含其他目录)。快照则是被追踪的最顶层的树。例如,一个树看起来可能是这样的:
|
|
历史记录建模:关联快照
在 Git 中,历史记录是一个由快照组成的有向无环图。
下面是一个 ASCII 码构成的简图,其中的 o
表示一次提交(快照)。
箭头指向了当前提交的父辈(这是一种“在…之前”,而不是“在…之后”的关系)。在第三次提交之后,历史记录分岔成了两条独立的分支。这可能因为此时需要同时开发两个不同的特性,它们之间是相互独立的。开发完成后,这些分支可能会被合并并创建一个新的提交,这个新的提交会同时包含这些特性。新的提交会创建一个新的历史记录,看上去像这样(最新的合并提交用粗体标记):
|
|
Git 中的提交是不可改变的。但这并不代表错误不能被修改,只不过这种“修改”实际上是创建了一个全新的提交记录。而引用(参见下文)则被更新为指向这些新的提交。
数据模型及其伪代码表示
以伪代码的形式来学习 Git 的数据模型,可能更加清晰:
|
|
这是一种简洁的历史模型。
对象和内存寻址
Git 中的对象可以是 blob、树或提交:
|
|
Git 在储存数据时,所有的对象都会基于它们的 SHA-1 哈希 进行寻址。
|
|
Blobs、树和提交都一样,它们都是对象。当它们引用其他对象时,它们并没有真正的在硬盘上保存这些对象,而是仅仅保存了它们的哈希值作为引用。
例如,上面例子中的树(可以通过 git cat-file -p 698281bc680d1995c5f4caaf3359721a5a58d48d
来进行可视化),看上去是这样的:
|
|
树本身会包含一些指向其他内容的指针,例如 baz.txt
(blob) 和 foo
(树)。如果我们用 git cat-file -p 4448adbf7ecd394f42ae135bbeed9676e894af85
,即通过哈希值查看 baz.txt 的内容,会得到以下信息:
|
|
引用
现在,所有的快照都可以通过它们的 SHA-1 哈希值来标记了。但这也太不方便了,谁也记不住一串 40 位的十六进制字符。
针对这一问题,Git 的解决方法是给这些哈希值赋予人类可读的名字,也就是引用(references)。引用是指向提交的指针。与对象不同的是,它是可变的(引用可以被更新,指向新的提交)。例如,master
引用通常会指向主分支的最新一次提交。
|
|
这样,Git 就可以使用诸如 “master” 这样人类可读的名称来表示历史记录中某个特定的提交,而不需要在使用一长串十六进制字符了。
有一个细节需要我们注意, 通常情况下,我们会想要知道“我们当前所在位置”,并将其标记下来。这样当我们创建新的快照的时候,我们就可以知道它的相对位置(如何设置它的“父辈”)。在 Git 中,我们当前的位置有一个特殊的索引,它就是 “HEAD”。
仓库
最后,我们可以粗略地给出 Git 仓库的定义了:对象
和 引用
。
在硬盘上,Git 仅存储对象和引用:因为其数据模型仅包含这些东西。所有的 git
命令都对应着对提交树的操作,例如增加对象,增加或删除引用。
当您输入某个指令时,请思考一下这条命令是如何对底层的图数据结构进行操作的。另一方面,如果您希望修改提交树,例如“丢弃未提交的修改和将 ‘master’ 引用指向提交 5d83f9e
时,有什么命令可以完成该操作(针对这个具体问题,您可以使用 git checkout master; git reset --hard 5d83f9e
)
暂存区
Git 中还包括一个和数据模型完全不相关的概念,但它确是创建提交的接口的一部分。
就上面介绍的快照系统来说,您也许会期望它的实现里包括一个 “创建快照” 的命令,该命令能够基于当前工作目录的当前状态创建一个全新的快照。
Git 使用一种叫做 “暂存区(staging area)”的机制,它允许您指定下次快照中要包括那些改动。
Git 的命令行接口
为了避免重复信息,我们将不会详细解释以下命令行。强烈推荐您阅读 Pro Git 中文版,同时完成Learn Git Branching练习。
基础
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 <basebranch> <topicbranch>
: 将一系列提交按照原有次序依次应用到另一分支上
远端操作
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>
: 恢复暂存的文件,将文件从暂存区(staging)移到工作区(working)git checkout -- <file>
: 丢弃工作区(working)修改,git restore
: git2.32版本后取代git reset 进行许多撤销操作
Git 高级操作
git config
: Git 是一个 高度可定制的 工具git clone --depth=1
: 浅克隆(shallow clone),不包括完整的版本历史信息git add -i /git add -p
: 交互式暂存、文件部分暂存,输入 ? 显示所有可以使用的命令列表git rebase -i
: 交互式变基git blame -L 33,17 <ref/hash>
: 查看最后修改某行的人git stash branch <branch>
: 贮藏工作目录下的修改内容git clean
:清理工作目录git bisect
: 通过二分查找搜索历史记录.gitignore
: 指定 故意不追踪的文件git log origin/master..HEAD
,查看当前分支不在远程的提交--patch -p
:可以使用git reset --patch
命令的补丁模式来部分重置文件, 通过git checkout --patch
命令来部分检出文件,git stash save --patch
命令来部分暂存文件。git grep
:查找工作目录的文件git log -S <string>
:查找日志git reset [--soft,--mixed,--hard] HEAD~
:撤销修改git ls-files -o
: 列出没有被 git 追踪的文件
杂项
图形用户界面
Git 的 图形用户界面客户端 有很多,但是我们自己并不使用这些图形用户界面的客户端,我们选择使用命令行接口
集成
- Shell 集成: 将 Git 状态集成到您的 shell 中会非常方便。(zsh, bash)。Oh My Zsh这样的框架中一般以及集成了这一功能
- 编辑器集成: 和上面一条类似,将 Git 集成到编辑器中好处多多。fugitive.vim 是 Vim 中集成 GIt 的常用插件
工作流
有一个创建优质提交信息的习惯会使 Git 的使用与协作容易的多。 一般情况下,信息应当以少于 50 个字符(25个汉字)的单行开始且简要地描述变更,接着是一个空白行,再接着是一个更详细的解释。 Git 项目要求一个更详细的解释,包括做改动的动机和它的实现与之前行为的对比——这是一个值得遵循的好规则。 使用指令式的语气来编写提交信息,比如使用“Fix bug”而非“Fixed bug”或“Fixes bug”。 这里是一份最初由 Tim Pope 写的模板:
|
|
如果你所有的提交信息都遵循此模版,那么对你和与你协作的其他开发者来说事情会变得非常容易。 Git 项目有一个良好格式化的提交信息——尝试在那儿运行 git log –no-merges 来看看漂亮的格式化的项目提交历史像什么样。
Git 提供商
GitHub: Git 并不等同于 GitHub。 在 GitHub 中您需要使用一个被称作拉取请求(pull request)的方法来向其他项目贡献代码
其他 Git 提供商: GitHub 并不是唯一的。还有像 GitLab 和 BitBucket 这样的平台。
More
- Pro Git ,强烈推荐!学习前五章的内容可以教会您流畅使用 Git 的绝大多数技巧,因为您已经理解了 Git 的数据模型。后面的章节提供了很多有趣的高级主题。(Pro Git 中文版);
- Oh Shit, Git!?! ,简短的介绍了如何从 Git 错误中恢复;
- Git for Computer Scientists ,简短的介绍了 Git 的数据模型,与本文相比包含较少量的伪代码以及大量的精美图片;
- Git from the Bottom Up详细的介绍了 Git 的实现细节,而不仅仅局限于数据模型。好奇的同学可以看看;
- How to explain git in simple words;
- Learn Git Branching 通过基于浏览器的游戏来学习 Git ;