大家好,日拱一卒,我是梁唐。
今天我们继续聊聊麻省理工的missing smester,消失的学期,讲述课堂上不会涉及,但又非常重要的知识和技能。
这一节课主要讲的内容是git的基本原理以及常见命令,git对于工程师的重要性相信不用我多说,绝对是所有程序员必学的技能之一。属于不一定要很精通,但至少得懂一点的领域之一。
和之前一样,这节课的note质量同样非常高。
本文是基于本节课note以及老师上课演示的内容,还有我个人的一些理解做的翻译整理版本。日拱一卒,欢迎大家打卡一起学习。
前言
版本控制系统(VCS)是用来追踪源代码(或其他文件、文件夹的集合)变更的工具。正如其名,这些工具帮助我们维护一个变更的历史。不仅如此,还让协同开发变得更加方便。VCS通过创建一系列快照的方式追踪一个文件夹和它当中所有内容的变更,每个快照都包含了文件/文件夹的完整的状态。VCS同样维护一些元信息,比如谁创建了快照,每个快照的备注信息等。
为什么我们要用版本控制呢?即使你独自工作,它也可以让你查看项目的历史版本,维护改动的历史,允许我们并行开发。当我们和其他人合作的时候,它更是无价之宝,因为我们可以看到其他人的修改,并且可以解决并行开发导致的冲突。
现代VCS同样让你能够很轻易地回答下列问题:
- 当前模块是谁编写的?
- 这个文件的这一行是什么时候被编辑的?是谁作出的修改?修改原因是什么呢?
- 最近的1000个版本中,何时/为什么导致了单元测试失败?
虽然还有其他的VCS,但事实上的标准是git。这里有一篇关于git的漫画,很有意思:

因为git的界面过于抽象(leaky abstraction),通过自顶向下的方式学习git充满了困惑。很多时候只能死记硬背一些命令,像是魔法一样使用它们。一旦遇到问题,就只能像是漫画里说的那样去处理了。
尽管git的界面有些捡漏,它底层的设计和思想却非常出彩。丑陋的接口只能死记硬背,而优秀的设计值得花时间理解。因此,我们将提供一个自底向上的对于git的解释,从它的数据模型开始,然后再学习它的命令行接口。当数据模型被理解了之后,再理解命令以及它们是如何生成底层数据模型的就非常容易了。
Git 数据模型
进行版本控制的方法有很多,git拥有一个深思熟虑的模型是的它支持版本控制当中许多出彩的特性。比如维护历史、支持分支以及允许协同合作。
Snapshots
git将历史变更抽象成顶级目录下的一系列文件和文件夹的快照。在git术语中,文件被称为blob,会被视为是一系列字节(byte)。一个文件夹被叫做tree,它存储一系列blob和tree和名称的映射(文件夹可以包含文件夹)。快照是被追踪的最顶层的树,比如一个树看起来可能是这样的:
顶层的树包含两个元素,一个叫做foo的tree(foo当中又包含一个叫做bar.txt的blob),和一个叫做baz.txt的blob。
历史变更建模:关联快照
版本控制系统是如何关联快照的呢?一种简单的方式是线性历史。一个历史变更是由一系列快照按照时间顺序排列组成的。因为种种原因,git没有使用这样的模型。
在git当中,历史变更是一个快照构成的DAG(有向无环图)。这看起来似乎很高大上,但不用害怕只是一个很简单的概念。这表明了git当中的每一个快照可能有多个父节点。注意,快照有多个父节点而非一个,因为某一个快照可能是由多个父节点生成的,比如由于合并了两个并行开发的分支而创建的节点,就会有多个父节点。
git将这些快照叫做commit。将一个commit的历史可视化,看起来可能是这样的:
上面是一个由ASCII构成的简图,o表示一个独立的commit(快照)。箭头指向了每个commit的父节点(箭头方向是时间更早的方向)。第三个commit之后,历史记录分岔成了两个不同的分治。这可能由于两个独立的特性被并行开发。未来这些分支会被合并成一个新的快照,它会包含所有的特性。新的提交会创建一个新的历史记录,看起来像是这样,新创建的节点被加粗显示:
git中的commit是不可修改的。这不意味着错误不能被修改,而是我们修改变更历史实际上是创建的新的commit,而引用(参考下文)则被更新并指向这些新节点。
数据模型的伪代码展示
可能用伪代码的形式表示git中的数据模型更加清晰:
非常简洁易懂
Objects 和内容寻址
object可以是一个blob、tree或commit:
在git数据存储当中,所有的objects都会基于它们SHA-1 哈希之后的结果进行寻址,SHA-1是一种哈希算法。会将传入的结果映射成一个字符串,算法会尽可能保证映射之后的字符串唯一。
blob、tree、commit就以这种方式被整合在了一起:它们都是object。当它们引用其他object时,它并没有真正将这些值存储下来,仅仅是引用了它们的hash值。
举个例子,刚才例子当中的tree可以通过git cat-file -p 698281bc680d1995c5f4caaf3359721a5a58d48d
来进行可视化,看上去是这样的:
树本身会包含它当中内容的指针,baz.txt(blobl)和foo(tree)。如果我们查看baz.txt这个hash值中对应的内容,命令git cat-file -p 4448adbf7ecd394f42ae135bbeed9676e894af85
,我们可以看到如下结果:
引用
现在,所有的快照都可以通过它们的hash值来定位。这并不方便,因为对人类来说记住长度40的16进制字符串是非常不方便的。
git的做法是给这些hash值赋予人类能理解的名字,叫做引用。引用是指向commit的指针。和object不可变不同,引用是可变的,可以指向新的commit。比如,master是一个引用,通常用来指向主分支中最新的commit。
通过引用,git就使用了人类可读的诸如master这样的名字来指代历史中的快照了。
一个细节是,我们经常想要知道我们当前所在的位置。这样当我们创建新的快照时,我们就知道它关联哪些快照。在git当中,我们现在所在的位置也是一个特殊的引用,叫做HEAD。
仓库
最后,我们可以粗略地定义git仓库了:数据object和引用。
在磁盘上,所有记录都以object和引用的方式存储:因为数据模型当中只有这两个概念。所有的git命令都对应commit DAG上的一些操作,比如添加object,添加或更新引用。
当你输入命令的时候,思考一下命令背后对于底层数据结构进行的操作。相反,如果你做出对commit DAG进行具体的修改,比如抛弃未提交的变更,或者是让master引用指向5d83f9e commit,这通常都是有办法的。(上述的例子当中,可以使用git checkout master; git reset --hard 5d83f9e
)
暂存区(Staging area)
git当中还包含一个和数据模型不相关的概念,但它是创建commit接口的一部分。
你可能觉得上面说的创建快照的命令类似于create snapshot,一些VCS的确是这样,但git不是。我们希望干净的快照,每次都从当前状态创建快照在一些情况并不理想。比如,想象一个场景,你开发完了两个功能,你想要创建两个不同的分支。第一个分支包含第一个功能,第二个分支包含第二个。或者,你在代码当中加入了一些debug信息,在提交的时候你希望能不要带上这些debug代码。
git处理这些场景的方式是使用一种叫做暂存区(staing area)的机制,它允许你指定下一次快照会包含的内容。
Git 命令行
为了避免重复信息,我们将不会详细解释下面的命令。强烈推荐阅读一下Pro Git这本书获得更多信息,或者观看课程视频。
Basics
-
git help : 获取 git 命令的帮助信息
-
git init: 创建一个新的 git 仓库,其数据会存放在一个名为 .git 的目录下
-
git status: 显示当前的仓库状态
-
git add : 添加文件到暂存区
-
git commit: 创建一个新的提交
-
git log: 显示历史日志
-
git log --all --graph --decorate: 可视化历史记录(有向无环图)
-
git diff : 显示与暂存区文件的差异
-
git diff : 显示某个文件两个版本之间的差异
-
git checkout : 更新 HEAD 和目前的分支
分支和合并
-
git branch: 显示分支
-
git branch : 创建分支
-
git checkout -b : 创建分支并切换到该分支
- 相当于 git branch ; git checkout
-
git merge : 合并到当前分支
-
git mergetool: 使用工具来处理合并冲突
-
git rebase: 将一系列补丁变基(rebase)为新的基线
远端操作
- git remote: 列出远端
- git remote add : 添加一个远端
- git push :: 将对象传送至远端并更新远端引用
- git branch --set-upstream-to=/: 创建本地和远端分支的关联关系
- git fetch: 从远端获取对象/索引
- git pull: 相当于 git fetch; git merge
- git clone: 从远端下载仓库
撤销
- git commit --amend: 编辑提交的内容或信息
- git reset HEAD : 恢复暂存的文件
- git checkout -- : 丢弃修改
- 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: 指定 故意不追踪的文件
杂项
- 图形用户界面: Git 的 图形用户界面客户端 有很多,但是我们自己并不使用这些图形用户界面的客户端,我们选择使用命令行接口
- Shell 集成: 将 Git 状态集成到您的 shell 中会非常方便。(zsh, bash)。Oh My Zsh这样的框架中一般以及集成了这一功能
- 编辑器集成: 和上面一条类似,将 Git 集成到编辑器中好处多多。fugitive.vim 是 Vim 中集成 GIt 的常用插件
- 工作流: 我们已经讲解了数据模型与一些基础命令,但还没讨论到进行大型项目时的一些惯例 ( 有很多 不同的 处理方法)
- GitHub: Git 并不等同于 GitHub。 在 GitHub 中您需要使用一个被称作拉取请求(pull request)的方法来向其他项目贡献代码
- 其他 Git 提供商: GitHub 并不是唯一的。还有像 GitLab 和 BitBucket 这样的平台。
资源
- Pro Git ,强烈推荐!学习前五章的内容可以教会您流畅使用 Git 的绝大多数技巧,因为您已经理解了 Git 的数据模型。后面的章节提供了很多有趣的高级主题。(Pro Git 中文版:git-scm.com/book/zh/v2)…
- Oh Shit, Git!?! :ohshitgit.com/,简短的介绍了如何从 Git 错误中恢复;
- Git for Computer Scientists :eagain.net/articles/gi… Git 的数据模型,与本文相比包含较少量的伪代码以及大量的精美图片;
- Git from the Bottom Up:jwiegley.github.io/git-from-th… Git 的实现细节,而不仅仅局限于数据模型。好奇的同学可以看看;
- How to explain git in simple words:smusamashah.github.io/blog/2017/1…
- Learn Git Branching:learngitbranching.js.org/通过基于浏览器的游戏来… Git ;
练习
-
如果您之前从来没有用过 Git,推荐您阅读 Pro Git 的前几章,或者完成像 Learn Git Branching这样的教程。重点关注 Git 命令和数据模型相关内容;
-
- 将版本历史可视化并进行探索
- 是谁最后修改了 README.md文件?(提示:使用 git log 命令并添加合适的参数)
- 最后一次修改_config.yml 文件中 collections: 行时的提交信息是什么?(提示:使用 git blame 和 git show)
-
使用 Git 时的一个常见错误是提交本不应该由 Git 管理的大文件,或是将含有敏感信息的文件提交给 Git 。尝试向仓库中添加一个文件并添加提交信息,然后将其从历史中删除 ( 这篇文章也许会有帮助:help.github.com/articles/re…
-
从 GitHub 上克隆某个仓库,修改一些文件。当您使用 git stash 会发生什么?当您执行 git log --all --oneline 时会显示什么?通过 git stash pop 命令来撤销 git stash 操作,什么时候会用到这一技巧?
-
与其他的命令行工具一样,Git 也提供了一个名为 ~/.gitconfig 配置文件 (或 dotfile)。请在 ~/.gitconfig 中创建一个别名,使您在运行 git graph 时,您可以得到 git log --all --graph --decorate --oneline 的输出结果;
-
您可以通过执行 git config --global core.excludesfile ~/.gitignore_global 在 ~/.gitignore_global 中创建全局忽略规则。配置您的全局 gitignore 文件来自动忽略系统或编辑器的临时文件,例如 .DS_Store;
-
克隆 本课程网站的仓库:github.com/missing-sem… GitHub 上发起拉取请求(Pull Request);