git(版本控制)
版本控制系统 (VCSs) 是一类用于追踪源代码(或其他文件、文件夹)改动的工具。顾名思义,这些工具可以帮助我们管理代码的修改历史;不仅如此,它还可以让协作编码变得更方便。VCS 通过一系列的快照将某个文件夹及其内容保存了起来,每个快照都包含了顶级目录中所有的文件或文件夹的完整状态。同时它还维护了快照创建者的信息以及每个快照的相关信息等等。
为什么说版本控制系统非常有用?即使您只是一个人进行编程工作,它也可以帮您创建项目的快照,记录每个改动的目的、基于多分支并行开发等等。和别人协作开发时,它更是一个无价之宝,您可以看到别人对代码进行的修改,同时解决由于并行开发引起的冲突。
现代的版本控制系统可以帮助您轻松地(甚至自动地)回答以下问题:
- 当前模块是谁编写的?
- 这个文件的这一行是神魔时候被编辑的?谁提出的修改?修改原因是什么呢?
- 最近的 1000 个版本中,何时/为什么导致了单元测试失败?
尽管版本控制系统有很多, 其事实上的标准则是 Git 。而这篇 XKCD 漫画 则反映出了人们对 Git 的评价:
这是 Git。它通过一个漂亮的分布式图论树模型追踪项目中的协作工作。
酷。我们该怎么用?
不知道。只需死记硬背这些 Shell 命令并输入它们。如果遇到报错,就保存你的工作目录,删掉整个项目,再重新下载一份干净的副本。
因为 Git 接口的抽象泄漏(leaky abstraction)问题,通过自顶向下的方式(从命令行接口开始)学习 Git 可能会让人感到非常困惑。很多时候您只能死记硬背一些命令行,然后像使用魔法一样使用它们,一旦出现问题,就只能像上面那幅漫画里说的那样去处理了。
尽管 Git 的接口有些丑陋,但是它的底层设计和思想却是非常优雅的。丑陋的接口只能靠死记硬背,而优雅的底层设计则非常容易被人理解。因此,我们将通过一种自底向上的方式向您介绍 Git。我们会从数据模型开始,最后再学习它的接口。一旦您搞懂了 Git 的数据模型,再学习其接口并理解这些接口是如何操作数据模型的就非常容易了。
Git 的数据模型
进行版本控制的方法很多。Git 拥有一个经过精心设计的模型,这使其能够支持版本控制所需的所有特性,例如维护历史记录、支持分支和促进协作。
快照
Git 将顶级目录中的文件和文件夹作为集合,并通过一系列快照来管理其历史记录。在 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 中,这些快照被称为“提交”。通过可视化的方式来表示这些历史提交记录时,看起来差不多是这样的:
o -- o -- o -- o
^
\
--- o -- o
上面是一个 ASCII 码构成的简图,其中的 o 表示一次提交(快照)。
箭头指向了当前提交的父辈(这是一种“在…之前”,而不是“在…之后”的关系)。在第三次提交之后,历史记录分岔成了两条独立的分支。这可能因为此时需要同时开发两个不同的特性,它们之间是相互独立的。开发完成后,这些分支可能会被合并并创建一个新的提交,这个新的提交会同时包含这些特性。新的提交会创建一个新的历史记录,看上去像这样(最新的合并提交用粗体标记):
o -- o -- o -- o ---- o
^ /
\ v
--- o -- o
Git 中的提交是不可改变的。但这并不代表错误不能被修改,只不过这种“修改”实际上是创建了一个全新的提交记录。而引用(参见下文)则被更新为指向这些新的提交。
仓库
最后,我们可以粗略地给出 Git 仓库的定义了:"对象" 和 "引用"。
在硬盘上,Git 仅存储对象和引用:因为其数据模型仅包含这些东西。所有的 git
命令都对应着对提交树的操作,例如增加对象,增加或删除引用。
当您输入某个指令时,请思考一下这条命令是如何对底层的图数据结构进行操作的。另一方面,如果您希望修改提交树,例如“丢弃未提交的修改和将 ‘master’ 引用指向提交 5d83f9e 时,有什么命令可以完成该操作(针对这个具体问题,您可以使用 git checkout master; git reset --hard 5d83f9e
)
暂存区
Git 中还包括一个和数据模型完全不相关的概念,但它确是创建提交的接口的一部分。
就上面介绍的快照系统来说,您也许会期望它的实现里包括一个 “创建快照” 的命令,该命令能够基于当前工作目录的当前状态创建一个全新的快照。有些版本控制系统确实是这样工作的,但 Git 不是。我们希望简洁的快照,而且每次从当前状态创建快照可能效果并不理想。例如,考虑如下场景,您开发了两个独立的特性,然后您希望创建两个独立的提交,其中第一个提交仅包含第一个特性,而第二个提交仅包含第二个特性。或者,假设您在调试代码时添加了很多打印语句,然后您仅仅希望提交和修复 bug 相关的代码而丢弃所有的打印语句。
Git 处理这些场景的方法是使用一种叫做 “暂存区(staging area)”的机制,它允许您指定下次快照中要包括那些改动。
Git 的命令行接口
为了避免重复信息,我们将不会详细解释以下命令行。强烈推荐您阅读 Pro 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
:指定 Git 忽略不追踪的文件