前提:已学习教程《Hello world in GitLab》。
适用对象:初次使用 Git 的人群。
演示平台:macOS
学习目标: Git 基础命令行操作和 Git 分支原理。
导入
如果您曾经在 GitLab Web IDE 上编辑文件,是否遇到过以下情况:
- 不小心刷新网页,导致编辑内容消失。
- 希望修改一个文件后立刻“保存”,但不希望产生一个 commit 记录。
- 有时网络不佳,无法访问 GitLab 网页。
在这些情况下,我们希望能够在本地编辑文件 —— Git 能够实现这一点。
我们将通过一个教程来学习 Git 的使用方法和原理。在这个教程中,我们需要完成一个任务:在本地编辑 Exercises 项目 的 README.md 文件,用 “Hello from No.$ID @$name” 的形式说一句 hello。
Git 安装配置
如果您已经安装了 Git ,可以跳过这一步。
Homebrew 安装
安装 Homebrew 是为了安装 Git。
请查看 Homebrew 安装配置教程。
Git 安装
请查看 Git 安装配置教程。
Say hello
Git 工作流程
基本概念:
- repository 版本库/仓库,可以简单理解为一个目录。这个目录里的所有文件都可以通过 Git 来管理。
- remote repository 远程仓库,位于服务器。在 GitLab 网页看到的文件就是远程仓库储存的。
- workspace 工作区,位于本地设备。工作区就是我们能在电脑里看到的目录。
- index / stage 索引/暂存区,位于本地设备。一般存放在
.git
目录下的index
文件中。 - local repository 本地仓库,位于本地设备。
.git
目录是当前工作区的本地版本库。
如果不太理解上面的概念,没关系,下面我们会深入学习它。现在,你只需要理解: Git 仓库分为远程仓库和本地仓库;本地区域并不是只有一个本地仓库,还有划分了其它区域。
在正式开始任务之前,先了解一般工作流程:
- 克隆远程仓库的 Git 资源到本地仓库。
- 在本地修改、添加、删除文件。
- 提交本地修改到远程仓库。
Git 工作流程概括起来就是:下拉,修改,上传。
这个工作流程套用到教程任务上:
- 克隆远程仓库的 Exercises 项目到本地。
- 编辑本地 Exercises 项目的 README.md 文件:say hello。
- 提交本地修改到远程仓库的 Exercises 项目。
获取Git 仓库
现在让我们回到任务,我们希望能够在本地编辑文件,第一步:获取 Git 仓库,把远程仓库的 Exercises 项目克隆到本地。
为了方便体验操作,我们建议您在 GitLab 网站上 Exercises 项目点击按钮 Fork
生成属于个人命名空间的 Exercises 项目,并克隆属于个人命名空间的 Exercises 项目到本地仓库(后面会解释这样做的原因)。
通常有两种获取 Git 项目仓库的方式:
- 将尚未进行版本控制的本地目录转换为 Git 仓库。
- 从其它服务器克隆一个已存在的 Git 仓库。
我们选择第二种方式。在项目首页找点击 Clone
按钮,选择 Clone with SSH
,在终端输入 git clone
和复制的 SSH :
$ git clone [SSH]
[SSH]
是选择Clone with SSH
所复制的 SSH。- Git 支持多种数据传输协议,我们也可以点击
Clone with HTTPS
获取地址,使用 HTTP 协议。
克隆成功,输出如下信息:
这会在当前目录下创建一个名为 exercises
的目录,并在这个目录下初始化一个 .git
文件夹,从远程仓库下载所有数据放入 .git
文件夹。在终端输入以下命令,进入 exercises
文件夹,可以看到 README.md 文件和 .git
文件夹:
$ cd exercises
$ ls -a
输出:
. .. .git README.md
为什么获取 Git 仓库到本地?
- 可以使用本地编辑器,编辑文件更加方便。
- 可以离线编辑。
- 可以在不影响远程仓库的情况下,进行开发工作。
- 本地部署运行项目。
- ……
新建分支
第二步:在 master
主分支的基础上,新建分支 say-hello
。
每个 Git 仓库至少有一个分支。“分支”可以理解为项目的快照。
在终端输入命令查看分支:
$ git branch
输出:
* master
*
符号表示当前所在分支。当前我们在master
主分支。master
分支名称:主分支。该命令会列出所有分支,目前我们只有主分支。
在终端输入以下命令,从主分支创建一个分支,并切换到新建分支:
$ git checkout -b say-hello
say-hello
是新建分支的名称。我们推荐使用统一的分支命名方式,比如小写字母和-
符号。git checkout -b
是git branch
(创建分支)和git checkout
(切换分支)的简写。git checkout -b BRANCH_NAME
创建一个名为BRANCH_NAME
的分支,并切换到该分支。
输出:
Switched to a new branch 'say-hello'
Switched to a new branch 'say-hello'
表示已经切换到新建分支say-hello
。
为什么使用分支,而不是直接在已有的主分支上工作?
使用分支就可以在不影响其他分支(比如,主分支)的情况下继续工作。比如,你接受了好几个任务,任务之间互不关联并且需要在同一段时间内进行开发,这时你可以分别为每个任务创建一个基于主分支的分支,这样各个分支(任务)互不影响,也不会在未完成的情况下影响到主分支。如果不使用分支功能,直接在主分支上修改,主分支有可能长期处于“任务进行中”的状态,这显然不是我们想要的效果。
在开发实践中,我们一般不会直接在主分支上进行开发工作,而是新建一个分支,在新分支上完成任务;保留主分支,用于与远程仓库保持同步。
修改并提交
第三步:在新建分支上,编辑文件,提交修改。
在编辑器打开 README.md 文件,输入 “Hello from No.$ID @$name”,保存文件。这里使用的编辑器是 VScode。
输入以下命令,添加文件,提交文件:
$ git add .
$ git commit -m 'Say hello with id and name'
- 我们编辑的文件就是工作区的文件。
git add
把工作区文件添加到暂存区。已经添加到暂存区的文件,才能提交到本地仓库。git add
+.
把当前目录下所有文件添加到暂存区。更换.
为文件名或目录,可以添加指定文件或目录到暂存区。git commit
把暂存区的内容提交到本地仓库。已经添加到本地仓库的文件,才能够提交到远程仓库。'Say hello with id and name'
这是提交信息 commit message,描述本次提交所做的修改。git commit
+-m COMMIT_MESSAGE
把暂存区的内容提交到本地仓库,并带有提交信息COMMIT_MESSAGE
。
推送分支
第四步:推送分支到远程仓库。
初次推送分支到远程仓库:
$ git push --set-upstream origin say-hello
git push origin say-hello
推送本地分支到远程主机origin
的关联分支。如果不存在关联的远程分支,则会新建一个同名的远程分支(仅新建分支,两者不存在关联关系)。--set-upstream
关联远程分支。初次推送时,当前本地分支没有关联的远程分支。该参数会关联本地分支与远程分支。关联后,再次使用git push
或git pull
就不需要指定对应的远程分支了。origin
默认远程主机名。
推送后,打开 GitLab 个人命名空间下 exercises
项目,可以看到新建分支 say-hello
。
以后再次推送,只需要输入以下命令:
$ git push
在开发实践中,Git 鼓励在工作流程中频繁地使用分支与合并。在一天内进行许多次分支合并是正常的。
合并分支
第五步:合并分支到主分支。
推送分支到远程仓库后,可以在网站提交 Merge Request 合并申请,根据任务是否完成,标记为 WIP/draft 状态。具体操作可参考教程《Hello World in GitLab》。
合并后,可以在 master
主分支看到更新内容(这就是克隆属于个人命名空间的 Exercises 项目的原因,你可以自由合并分支):
至此,我们完成了任务 Say Hello !
Git 分支原理
在上一章,我们学习了如何添加文件、提交文件、推送分支到远程仓库,这些操作都是发生在同一个分支上。
现在,让我们把添加文件、提交文件、推送分支都放在一边,专注于分支,学习分支与分支之间的关系。
快照
Git 保存的不是文件的变化或差异,而是一系列不同时刻的快照。
快照是指用 blob 对象保存文件快照。如果有3个将要被暂存和提交的文件 fileA、 fileB、 fileC,Git 将会创建3个 blob 对象来保存这3个文件的快照。然后 Git 会创建一个树对象,树对象记录了目录结构和 blob 对象索引。最后, Git 会创建一个提交对象( commit object ),提交对象包含了指向树对象的指针,指向父对象的指针,提交信息(commit message),提交者的姓名和邮箱。
提交对象,树对象和 blob 对象的关系:
每当使用 git commit
命令进行提交操作,Git 就会产生一个提交对象。除了首次提交生成的提交对象,每个提交对象都至少有一个父对象。
尽管是 blob 对象保存了文件快照,为了便于表述,我们可以视一个树对象为一个“快照”。
下图展示了3个提交对象的父子关系。提交对象 A,经过修改、提交,产生了提交对象 B;提交对象 B,经过修改、提交,产生了提交对象 C。每个提交对象指向各自的快照。
分支模型
说到这里,你可能有个疑惑,为什么不直接用分支名称指代提交对象?比如下图,以第三章的操作为例子,从 master
主分支新建 say-hello
分支:
为什么不直接用分支名称指代快照?因为 Git 的分支本质上仅仅是指向提交对象的可变指针。分支不是提交对象本身,所以分支间不存在直接的不变的“父子关系”。在 Git 的分支模型,分支的创建、切换、合并其实是对指针的操作。
下图才是“从 master
主分支新建 say-hello
分支”的正确表示, master
与 say-hello
没有“父子关系”,两者指向同一个提交对象(同一个快照):
Say hello
仍以以第三章的操作为例子,重温第三章的命令行,让我们从不同角度看看发生了什么。以下的分支图,隐藏了snapshot 快照的表示。
获取 Git 仓库
$ git clone [SSH]
Git 的默认分支名字是 master
。 假设在获取 Git 仓库时, master
主分支已经经过几次提交,初始情况如下:
查看 Git 分支
$ git branch
Git 怎么知道当前在哪一个分支上?Git 有一个名为 HEAD
的特殊指针, HEAD
指针指向当前所在的本地分支(可以把 HEAD
理解为当前分支的别名)。
新建分支
$ git checkout -b say-hello
在第三章中,我们使用了 git checkout -b say-hello
创建并切换分支。为了方便展示,这里我们分别使用 git branch say-hello
和 git checkout say-hello
两条命令。
$ git branch say-hello
$ git checkout say-hello
git branch
新建分支。git checkout
切换分支。
Git 怎么创建新分支?Git 只是创建了一个可以移动的新的指针。
git branch say-hello
命令,在 master
分支上创建一个新分支 say-hello
,并不会自动切换到新分支 ,HEAD
指针仍指向 master
主分支:
切换分支
$ git checkout say-hello
切换分支,其实是切换HEAD指针的指向。使用 git checkout say-hello
命令, 从当前的 master
主分支切换到 say-hello
分支。HEAD指针原指向 master
主分支,切换分支后,HEAD
指针指向 say-hello
分支:
修改并提交
$ git add .
$ git commit -m 'Say hello with id and name'
编辑 README.md 文件完成 say hello 任务后,我们使用了命令 git add .
和 git commit -m 'Say hello with id and name'
。
每次提交后,Git 生成一个新的快照, say-hello
分支指向新的快照。 HEAD
指针跟着 say-hello
分支自动向前移动。
注意到,master
分支仍然指向快照 C。如果此时 say-hello 任务没完成并且有一个更紧急的任务需要立刻完成,我们可以轻松切换到没有发生任何变化的主分支,新建分支来完成新任务。
推送分支
git push --set-upstream origin say-hello
如果远程仓库的 master
主分支没有更新,推送分支前,远程仓库的分支状态如下:
推送分支后,远程仓库的分支状态如下:
为强调远程仓库,上图隐藏了 HEAD
指针。其实只要保持同步,远程仓库和本地仓库的分支状态是一样的。
合并分支
如果远程 master
主分支没有更新(即 master
主分支仍指向 commit object C
), say-hello
分支可以直接合并到 master
主分支。在 GitLab 通过合并申请后,远程仓库的分支状态如下:
有人把 Git 的分支模型称为它的“必杀技特性”,也正因为这一特性,使得 Git 从众多版本控制系统中脱颖而出。 为何 Git 的分支模型如此出众呢? Git 处理分支的方式可谓是难以置信的轻量,创建新分支这一操作几乎能在瞬间完成,并且在不同分支之间的切换操作也是一样便捷。 与许多其它版本控制系统不同,Git 鼓励在工作流程中频繁地使用分支与合并,哪怕一天之内进行许多次。
——《git --fast-version-control》
扩展学习
然而,在实际开发过程中,工作流程却不是这么简单,下面介绍如何处理一些常见情况。
准备工作
注意到,此时远程仓库的 master
主分支已经更新了。在主分支可以看到更新的内容:
更新后,远程仓库分支图如下(如果没有删除 say-hello
分支,该分支仍然存在,因不再关注该分支,隐藏 say-hello
分支的表示):
回到本地仓库,查看本地 master
主分支的 README.md 文件,显然没有更新。
在开始练习前,我们做一些准备工作:在本地 master
主分支上创建一个新分支 add-question
:
$ git checkout master
$ git checkout -b add-question
在 README.md 文件添加:
What day is today?
提交分支:
$ git add .
$ git commit -m "Ask the question"
本地分支状态如下。注意到,远程主分支已经指向 commit object D
,本地主分支仍然指向 commit object C
。因不再关心 say-hello
分支,后面的图不再展示 say-hello
分支。
下载最新分支
我们注意到,远程仓库和本地仓库的主分支内容不一致了:远程仓库 master
主分支指向提交对象 D,本地仓库master
主分支指向提交对象 C。在开发实践中,这种情况十分常见。
在提交前,如果远程仓库分支更新了,可以再次下载 Git 资源,更新本地 分支保持同步。
本地切换到 master
主分支,输入命令,下拉远程仓库 master
主分支,并合并到当前分支(即 master
分支):
$ git checkout master
$ git pull
git pull
从远程仓库获取关联分支的代码,合并到本地分支。
这样,本地主分支与远程仓库的主分支就保持同步了。本地分支状态图如下:
合并分支
现在,这个项目的提交历史已经产生了分叉。分叉,表示分支之间存在矛盾冲突。在教程《Hello world in GitLab》中,我们在 GitLab 网站处理过合并分支的矛盾冲突。在开发实践中,一般会在本地处理好矛盾冲突,再推送分支到远程仓库。
切换到新分支 add-question
,把 master
主分支合并到 add-question
:
$ git checkout add-question
$ git merge master
master
分支名称。git merge
+BRANCH_NAME
合并指定分支到当前分支。
Vscode 编辑器会提示分支的矛盾冲突:
选择 “Accept Both Changes”,保留两个改变:
提交修改:
$ git add .
$ git commit -m 'Merge with master'
本地仓库分支图如下:
常用命令
列举一些常用命令,可以在终端输入 git help
查看具体意义和语法。
克隆仓库:
$ git clone
添加文件:
$ git add
提交文件:
$ git commit
推送分支:
$ git push
下拉分支:
$ git pull
合并分支:
$ git merge
查看状态:
$ git status
删除本地分支:
$ git branch -d BRANCH_NAME
查看提交历史:
$ git log
缓存:
$ git stash
倒出缓存:
$ git stash pop
回退版本:
$ git reset