Skip to main content

Hello World in Git

· 约23分钟

前提:已学习教程《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-partition-01

在正式开始任务之前,先了解一般工作流程:

  • 克隆远程仓库的 Git 资源到本地仓库。
  • 在本地修改、添加、删除文件。
  • 提交本地修改到远程仓库。

Git 工作流程概括起来就是:下拉,修改,上传。

这个工作流程套用到教程任务上:

  • 克隆远程仓库的 Exercises 项目到本地。
  • 编辑本地 Exercises 项目的 README.md 文件:say hello。
  • 提交本地修改到远程仓库的 Exercises 项目。

获取Git 仓库

现在让我们回到任务,我们希望能够在本地编辑文件,第一步:获取 Git 仓库,把远程仓库的 Exercises 项目克隆到本地。

为了方便体验操作,我们建议您在 GitLab 网站上 Exercises 项目点击按钮 Fork 生成属于个人命名空间的 Exercises 项目,并克隆属于个人命名空间的 Exercises 项目到本地仓库(后面会解释这样做的原因)。

gitlab-01

gitlab-02

通常有两种获取 Git 项目仓库的方式:

  1. 将尚未进行版本控制的本地目录转换为 Git 仓库。
  2. 从其它服务器克隆一个已存在的 Git 仓库。

我们选择第二种方式。在项目首页找点击 Clone 按钮,选择 Clone with SSH ,在终端输入 git clone 和复制的 SSH : gitlab-03

$ git clone [SSH]
  • [SSH] 是选择 Clone with SSH 所复制的 SSH。
  • Git 支持多种数据传输协议,我们也可以点击 Clone with HTTPS 获取地址,使用 HTTP 协议。

克隆成功,输出如下信息: terminal

这会在当前目录下创建一个名为 exercises 的目录,并在这个目录下初始化一个 .git 文件夹,从远程仓库下载所有数据放入 .git 文件夹。在终端输入以下命令,进入 exercises 文件夹,可以看到 README.md 文件和 .git 文件夹:

$ cd exercises
$ ls -a

输出:

.       ..      .git        README.md

git-partition-02

为什么获取 Git 仓库到本地?

  • 可以使用本地编辑器,编辑文件更加方便。
  • 可以离线编辑。
  • 可以在不影响远程仓库的情况下,进行开发工作。
  • 本地部署运行项目。
  • ……

新建分支

第二步:在 master 主分支的基础上,新建分支 say-hello

每个 Git 仓库至少有一个分支。“分支”可以理解为项目的快照。

在终端输入命令查看分支:

$ git branch

输出:

* master
  • * 符号表示当前所在分支。当前我们在 master 主分支。
  • master 分支名称:主分支。该命令会列出所有分支,目前我们只有主分支。

在终端输入以下命令,从主分支创建一个分支,并切换到新建分支

$ git checkout -b say-hello
  • say-hello 是新建分支的名称。我们推荐使用统一的分支命名方式,比如小写字母和 - 符号。
  • git checkout -bgit 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。

vscode-01

输入以下命令,添加文件,提交文件:

$ 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-partition-03

推送分支

第四步:推送分支到远程仓库。

初次推送分支到远程仓库:

$ git push --set-upstream origin say-hello
  • git push origin say-hello 推送本地分支到远程主机 origin 的关联分支。如果不存在关联的远程分支,则会新建一个同名的远程分支(仅新建分支,两者不存在关联关系)。
  • --set-upstream 关联远程分支。初次推送时,当前本地分支没有关联的远程分支。该参数会关联本地分支与远程分支。关联后,再次使用 git pushgit pull 就不需要指定对应的远程分支了。
  • origin 默认远程主机名。

推送后,打开 GitLab 个人命名空间下 exercises 项目,可以看到新建分支 say-hello

gitlab-04

以后再次推送,只需要输入以下命令:

$ git push

git-partition-04

在开发实践中,Git 鼓励在工作流程中频繁地使用分支与合并。在一天内进行许多次分支合并是正常的。

合并分支

第五步:合并分支到主分支。

推送分支到远程仓库后,可以在网站提交 Merge Request 合并申请,根据任务是否完成,标记为 WIP/draft 状态。具体操作可参考教程《Hello World in GitLab》。

合并后,可以在 master 主分支看到更新内容(这就是克隆属于个人命名空间的 Exercises 项目的原因,你可以自由合并分支):

gitlab-05

至此,我们完成了任务 Say Hello !

Git 分支原理

在上一章,我们学习了如何添加文件、提交文件、推送分支到远程仓库,这些操作都是发生在同一个分支上。

现在,让我们把添加文件、提交文件、推送分支都放在一边,专注于分支,学习分支与分支之间的关系。

快照

Git 保存的不是文件的变化或差异,而是一系列不同时刻的快照。

快照是指用 blob 对象保存文件快照。如果有3个将要被暂存和提交的文件 fileA、 fileB、 fileC,Git 将会创建3个 blob 对象来保存这3个文件的快照。然后 Git 会创建一个树对象,树对象记录了目录结构和 blob 对象索引。最后, Git 会创建一个提交对象( commit object ),提交对象包含了指向树对象的指针,指向父对象的指针,提交信息(commit message),提交者的姓名和邮箱。

提交对象,树对象和 blob 对象的关系:

snapshot

每当使用 git commit 命令进行提交操作,Git 就会产生一个提交对象。除了首次提交生成的提交对象,每个提交对象都至少有一个父对象。

尽管是 blob 对象保存了文件快照,为了便于表述,我们可以视一个树对象一个“快照”

下图展示了3个提交对象的父子关系。提交对象 A,经过修改、提交,产生了提交对象 B;提交对象 B,经过修改、提交,产生了提交对象 C。每个提交对象指向各自的快照。

branch-model-01

分支模型

说到这里,你可能有个疑惑,为什么不直接用分支名称指代提交对象?比如下图,以第三章的操作为例子,从 master 主分支新建 say-hello 分支:

branch-model-02

为什么不直接用分支名称指代快照?因为 Git 的分支本质上仅仅是指向提交对象的可变指针。分支不是提交对象本身,所以分支间不存在直接的不变的“父子关系”。在 Git 的分支模型,分支的创建、切换、合并其实是对指针的操作。

下图才是“从 master 主分支新建 say-hello 分支”的正确表示, mastersay-hello 没有“父子关系”,两者指向同一个提交对象(同一个快照):

branch-model-03

Say hello

仍以以第三章的操作为例子,重温第三章的命令行,让我们从不同角度看看发生了什么。以下的分支图,隐藏了snapshot 快照的表示

获取 Git 仓库

$ git clone [SSH]

Git 的默认分支名字是 master。 假设在获取 Git 仓库时, master 主分支已经经过几次提交,初始情况如下:

branch-model-04

查看 Git 分支

$ git branch

Git 怎么知道当前在哪一个分支上?Git 有一个名为 HEAD 的特殊指针, HEAD 指针指向当前所在的本地分支(可以把 HEAD 理解为当前分支的别名)。

branch-model-05

新建分支

$ git checkout -b say-hello

在第三章中,我们使用了 git checkout -b say-hello 创建并切换分支。为了方便展示,这里我们分别使用 git branch say-hellogit 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 主分支:

branch-model-06

切换分支

$ git checkout say-hello

切换分支,其实是切换HEAD指针的指向。使用 git checkout say-hello 命令, 从当前的 master 主分支切换到 say-hello 分支。HEAD指针原指向 master 主分支,切换分支后,HEAD 指针指向 say-hello 分支:

branch-model-07

修改并提交

$ 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 分支自动向前移动。

branch-model-08

注意到,master 分支仍然指向快照 C。如果此时 say-hello 任务没完成并且有一个更紧急的任务需要立刻完成,我们可以轻松切换到没有发生任何变化的主分支,新建分支来完成新任务。

推送分支

git push --set-upstream origin say-hello

如果远程仓库的 master 主分支没有更新,推送分支前,远程仓库的分支状态如下:

branch-model-04

推送分支后,远程仓库的分支状态如下:

branch-model-09

为强调远程仓库,上图隐藏了 HEAD 指针。其实只要保持同步,远程仓库和本地仓库的分支状态是一样的。

合并分支

如果远程 master 主分支没有更新(即 master 主分支仍指向 commit object C), say-hello 分支可以直接合并到 master 主分支。在 GitLab 通过合并申请后,远程仓库的分支状态如下:

branch-model-10

有人把 Git 的分支模型称为它的“必杀技特性”,也正因为这一特性,使得 Git 从众多版本控制系统中脱颖而出。 为何 Git 的分支模型如此出众呢? Git 处理分支的方式可谓是难以置信的轻量,创建新分支这一操作几乎能在瞬间完成,并且在不同分支之间的切换操作也是一样便捷。 与许多其它版本控制系统不同,Git 鼓励在工作流程中频繁地使用分支与合并,哪怕一天之内进行许多次。

——《git --fast-version-control

扩展学习

然而,在实际开发过程中,工作流程却不是这么简单,下面介绍如何处理一些常见情况。

准备工作

注意到,此时远程仓库的 master 主分支已经更新了。在主分支可以看到更新的内容:

gitlab-05

更新后,远程仓库分支图如下(如果没有删除 say-hello 分支,该分支仍然存在,因不再关注该分支,隐藏 say-hello 分支的表示):

branch-model-11

回到本地仓库,查看本地 master 主分支的 README.md 文件,显然没有更新。

vscode-02

在开始练习前,我们做一些准备工作:在本地 master 主分支上创建一个新分支 add-question

$ git checkout master
$ git checkout -b add-question

在 README.md 文件添加:

What day is today?

vscode-03

提交分支:

$ git add .
$ git commit -m "Ask the question"

本地分支状态如下。注意到,远程主分支已经指向 commit object D,本地主分支仍然指向 commit object C。因不再关心 say-hello 分支,后面的图不再展示 say-hello 分支。

branch-model-12

下载最新分支

我们注意到,远程仓库和本地仓库的主分支内容不一致了:远程仓库 master 主分支指向提交对象 D,本地仓库master 主分支指向提交对象 C。在开发实践中,这种情况十分常见。

在提交前,如果远程仓库分支更新了,可以再次下载 Git 资源,更新本地 分支保持同步。

本地切换到 master 主分支,输入命令,下拉远程仓库 master 主分支,并合并到当前分支(即 master 分支):

$ git checkout master
$ git pull
  • git pull 从远程仓库获取关联分支的代码,合并到本地分支。

这样,本地主分支与远程仓库的主分支就保持同步了。本地分支状态图如下:

branch-model-13

合并分支

现在,这个项目的提交历史已经产生了分叉。分叉,表示分支之间存在矛盾冲突。在教程《Hello world in GitLab》中,我们在 GitLab 网站处理过合并分支的矛盾冲突。在开发实践中,一般会在本地处理好矛盾冲突,再推送分支到远程仓库。

切换到新分支 add-question,把 master 主分支合并到 add-question

$ git checkout add-question
$ git merge master
  • master 分支名称。
  • git merge + BRANCH_NAME 合并指定分支到当前分支。

Vscode 编辑器会提示分支的矛盾冲突:

vscode-04

选择 “Accept Both Changes”,保留两个改变:

vscode-05

提交修改:

$ git add .
$ git commit -m 'Merge with master'

本地仓库分支图如下:

branch-model-14

常用命令

列举一些常用命令,可以在终端输入 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