Lecture6 版本控制
版本控制系统 (VCSs) 是一类用于追踪源代码改动的工具,可以帮助管理代码的修改历史,还可以让协作编码变得更方便。
VCS通过一系列的 快照 保存源代码,每个快照都包含文件或文件夹的完整状态,还维护了快照创建者的信息以及每个快照的相关信息等等。
因为 Git 接口的抽象泄漏(leaky abstraction)问题,通过自顶向下的方式(从命令行接口开始)学习 Git 可能会让人感到非常困惑,很多时候只能死记硬背一些命令行,然后像使用魔法一样使用它们。
尽管 Git 的接口有些丑陋,但是它的底层设计和思想却是非常优雅的。丑陋的接口只能靠死记硬背,而优雅的底层设计则非常容易被人理解。
我们将通过一种自底向上的方式向您介绍 Git。
Git 的数据模型
快照
Git 将顶级目录中的文件和文件夹作为集合,并通过一系列 快照(snapshot) 来管理其历史记录。
在Git的术语里,文件被称作Blob对象(数据对象),也就是一组数据。
目录则被称之为“树”,它将名字与 Blob 对象或树对象进行映射(使得目录中可以包含其他目录)。
快照则是被追踪的最顶层的树。例如,一个树看起来可能是这样的:
<root> (tree)
|
+- foo (tree)
| |
| + bar.txt (blob, contents = "hello world")
|
+- baz.txt (blob, contents = "git is wonderful")
这个顶层的树包含了两个元素,一个名为 “foo” 的树(它本身包含了一个blob对象 “bar.txt”),以及一个 blob 对象 “baz.txt”。
历史记录建模:关联快照
线性历史记录是一种最简单的模型,它包含了一组按照时间顺序线性排列的快照。不过git不用这个。
在 Git 中,历史记录是一个由快照组成的 有向无环图,这代表 Git 中的每个快照都有一系列的“父辈”。注意,快照具有多个“父辈”而非一个,因为某个快照可能由多个父辈而来。
在 Git 中,这些快照被称为“提交”。通过可视化的方式来表示这些历史提交记录时,看起来差不多是这样的:
上面是一个 ASCII 码构成的简图,其中的
o
表示一次提交(快照)。
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、树或提交:
Git 在储存数据时,所有的对象都会基于它们的 SHA-1 哈希 进行寻址。
引用
所有的快照都可以通过它们的 SHA-1 哈希值来标记。但这也太不方便了,谁也记不住一串 40 位的十六进制字符。
Git 的解决方法是给这些哈希值赋予人类可读的名字,也就是引用(references)。
引用是指向提交的指针。与对象不同的是,它是可变的(引用可以被更新,指向新的提交)。例如,master
引用通常会指向主分支的最新一次提交。
这样,Git 就可以使用诸如 “master” 这样人类可读的名称来表示历史记录中某个特定的提交,而不需要在使用一长串十六进制字符了。
仓库
我们可以粗略地给出 Git 仓库的定义:对象
和 引用
。
在硬盘上,Git 仅存储对象和引用:因为其数据模型仅包含这些东西。所有的 git
命令都对应着对提交树的操作,例如增加对象,增加或删除引用。
暂存区
暂存区(staging area)允许指定下次快照中要包括那些改动。
Git 的命令行接口
为了避免重复信息,我们将不会详细解释以下命令行。强烈推荐您阅读 Pro Git 中文版或可以观看本讲座的视频来学习。
基础
git help <command>
: 获取 git 命令的帮助信息
git init
: 创建一个新的 git 仓库,其数据会存放在一个名为.git
的目录下
git status
: 显示当前的仓库状态
git add <filename>
: 添加文件到暂存区
git log
: 显示历史日志
git log --all --graph --decorate
: 可视化历史记录(有向无环图)
git diff <filename>
: 显示与暂存区文件的差异
git diff <revision> <filename>
: 显示某个文件两个版本之间的差异
git checkout <revision>
: 更新 HEAD 和目前的分支
分支和合并
git branch
: 显示分支
git branch <name>
: 创建分支
-
plaintext 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
: 指定 故意不追踪的文件
杂项
- 图形用户界面: 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 中文版);
- 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 ;
课后练习
- 如果您之前从来没有用过 Git,推荐您阅读 Pro Git 的前几章,或者完成像 Learn Git Branching这样的教程。重点关注 Git 命令和数据模型相关内容;
- Fork 本课程网站的仓库
- 将版本历史可视化并进行探索
- 是谁最后修改了
README.md
文件?(提示:使用git log
命令并添加合适的参数) - 最后一次修改
_config.yml
文件中collections:
行时的提交信息是什么?(提示:使用git blame
和git show
)
- 使用 Git 时的一个常见错误是提交本不应该由 Git 管理的大文件,或是将含有敏感信息的文件提交给 Git 。尝试向仓库中添加一个文件并添加提交信息,然后将其从历史中删除 ( 这篇文章也许会有帮助);
- 从 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 上发起拉取请求(Pull Request);