前端项目管理-Git篇

ObjectKaz Lv4

初体验

这一部分,将会以一个具体案例,体会 Git 是如何使用在项目中的。

下载和安装 Git

Windows 安装

前往 Git 官网 ,点击如图所示按钮即可下载 Windows 安装包:

运行安装包,按照指引安装后即可。

Ubuntu 安装

使用 apt 安装即可:

1
sudo apt install git-all

配置全局设置

安装完成后,记得配置你的用户名和邮箱:

1
2
git config --global user.name 用户名
git config --global user.email 邮箱

你可能还需要配置一下默认编辑器,:

1
git config --global core.editor "编辑器可执行文件路径" --wait

你可以运行下面的命令来查看配置:

1
2
$ git config --global core.editor
"D:\Microsoft\Microsoft VS Code\Code.exe" --wait

还可以运行下面的命令查看所有的配置:

1
git config --list

安装 Github Desktop

Github Desktop:httpss://desktop.github.com/

从官网上下载安装即可。

创建第一个版本库

Git 安装以后,我们就可以创建版本库了。

这里,我们以一个简单的 node 项目来进行演示。

先新建一个目录,名为 my-project

接下来运行 git init 来初始化我们的项目。

1
git init

这时候,你的项目多了一个 .git 的文件夹,它是 Git 的内部数据库。

初始化 git 后,我们需要继续使用 npm init 初始化 node 项目。

1
npm init

接下来,我们安装 chalk 包,这个包用来在控制台输出彩色的文字。

1
npm install chalk --save

然后,我们新建一个 index.js 文件:

1
2
3
const chalk = require("chalk");

console.log(chalk.bold.blue("Hello World"));

运行这个文件:

1
node index.js

可以得到一个蓝色加粗的 Hello World

忽略某些文件

接下来我们打开 Github Desktop:

发现里面多了很多文件。但实际上,node_modules 这个文件夹并不需要添加进仓库,因为通过 npm install 我们可以重新得到这些文件。将 node_modules 加入仓库中,无疑会占用更多的数据空间。

所以,我们得避免 node_modules 进入数据库。方法就是在项目根目录添加一个 .gitignore 文件:

1
node_modules

这样,node_modules 就不会出现在代码列表里了。

除了通过 Github Desktop 查看这些文件,我们还可以通过命令行查看状态:

1
git status

可以看到,文件列表里面的文件也出现在这里了。

但是,你的所有修改并没有进入 Git 的内部数据库。

提交代码到仓库

要想把文件添加到仓库中,得运行命令:

1
git add 文件路径...

例如,我们要把这三个文件添加到仓库:

1
git add index.js .gitignore package.json

但是,如果文件比较多,这么写肯定非常麻烦。

不过,git add 有一个参数 -A,它可以把一个目录下所有的文件添加进来。

例如,下面的命令便会把仓库内所有的文件添加进来。

1
git add -A

在 Github Desktop 中,你可以通过将文件打勾,来将文件添加进来中。

不过,这些文件只是进入了一个叫做 暂存区的地方,并没有进入数据库。

接下来需要运行:

1
git commit -m "初始化"

来把暂存区的代码提交到数据库。

发布代码到服务器

代码提交以后,你的代码就进入了 git 的数据库中。

现在,你可以把你的代码提交到服务器,从而与他人分享你的代码。

目前,比较流行的服务器有 码云GitHub ,同时它们也是规模超大的开源社区,你可以在上面自由地分享你的仓库。

这里我们以码云为例。点击下面的链接来创建一个仓库:

创建仓库

你可以选择开源还是私有。

  • 对于开源项目,你的代码将会向全世界公开,所有人都可以下载你的代码,但只有仓库成员可以提交;
  • 对于私有项目,你的代码只有仓库成员可见,只有仓库成员可以下载和提交代码。

下面的三个复选框不要勾选,避免仓库不一致。

现在,我们创建好了仓库。

接下来,我们可以看到它提供了 httpssSSH 两种链接。

  • httpss链接:下载和提交代码需要身份认证(输入码云的账号和密码)。
  • ssh链接:使用 SSH 公私钥进行认证,无需输入账号密码。

接下来,我们来以 ssh 为例。

首先,你需要在电脑上生成一对密钥(使用 Git Bash 运行):

1
ssh-keygen -t rsa -b 4096 -C "随便输入"

查看公钥:

1
cat ~/.ssh/id_rsa.pub

进入码云的 SSH 公钥管理界面 ,把上面显示的公钥复制到上面,点击确定即可。

接下来,在本地仓库运行:

1
git remote add origin 仓库地址

运行下面命令,你就可以把它发布到服务器了:

1
git push origin master

接下来访问你的代码仓库,便可以看到你的代码了。

还注意到,你在 git commit 上填写的一些信息也显示在上面了。

实际上,填写的这些信息是对你修改的部分的一些描述。

下载代码仓库

你可以从 码云GitHub 上下载需要的仓库。

拿到仓库的 git 链接后,输入以下指令即可拷贝仓库:

1
git clone 仓库地址

例如,下载 vue-next 的仓库:

1
git clone git@github.com:vuejs/vue-next.git

介绍

代码管理的各种方式

人工管理

我们先思考一个问题:

当代码发生修改时,如何保存修改前和修改后的版本?

一个最为简单粗暴的方法,便是将原来的代码再拷贝一份。我们只需要修改文件夹名称,把前后两个版本区分开来就行了。

1
2
3
4
projects
├─ 1.0.0
├─ 1.0.1
└ 1.0.2

有时候为了搞清楚更改的时间,我们会加上时间:

1
2
3
4
projects
├─ 1.0.0-20210101
├─ 1.0.1-20210103
└ 1.0.2-20210105

但别忘了,前端项目一般可是有 node_modules 这个文件夹的,这个文件夹通常有非常多的小文件,复制粘贴的话,这个文件夹的复制粘贴得耽误不少时间。(Windows 环境下)

就算你想办法删除,也得花费不少时间。

删除的话,我们使用一个 rimrafnpm 包,它可以快速删除文件夹,类似 LINUX 系统的 rm

复制前把 node_modules 删除:

1
2
npm install rimraf -g
rimraf node_modules

复制后,再使用 npm install 重新把包安装回来就行了。

这个文件似乎是解决了。

实际上,这样的逻辑一般是差不多的,你甚至可以把这样的逻辑写成一个批处理脚本:

copy.bat
1
2
3
4
5
6
@echo off
call rimraf %1/node_modules
md %2
xcopy %1 %2 /e
cd %2
npm install

运行命令

1
copy.bat 源目录 目标目录

我们再考虑另外一个问题:

现在你和另外一个人在协作,他修改了一些代码,但你也修改了一些代码,现在得想个办法把代码合并起来。

graph TB
ORIGIN(上一个版本)-->MINE(我的版本)
ORIGIN-->HIS(他的版本)

但是,你并不清除他改了哪些代码,这使得代码合并异常困难。

如果是一个小项目,还可以一个个人工对比。但是,一旦项目规模大了,文件多了,一个个人工对比得耗费大量的时间了。

本地管理

一些比较大的项目会使用一些本地代码管理工具来管理代码,它们通常使用一些简单的数据库。

实际上,在 LINUX 平台上,有一个专门的本地代码管理工具,叫做 RCS(Revision Control System) ,它通过保存文件修改记录,通过对这些文件修改日志进行合并,就可以得到相应版本的代码。

graph LR
LOG1(修改日志)-->LOG2(修改日志)-->LOG3(修改日志)-->LOG4(修改日志)-->CURRENT[版本]

所以,你可以先保存你的代码,再将他的代码合并到你的代码上。

(实际上,这仍存在一些问题,会在代码冲突这一块提到。)

集中管理

上面的问题其实假设了你可以拿到他的代码。但实际上,如果开发的人很多的话,每个人得一个个把他们的代码拷贝到你手上,然后一个个地合并。这是比较麻烦的,而且代码合并完后,你得重新把最新的代码发回给他们。

这时候,就需要一个服务器来集中管理代码了。集中管理理解起来是非常容易的,它和大多数桌面应用和 APP 的运作模式是一样的,都是基于 C/S 架构。

graph TB
SERVER(服务器) --- CLIENT1(开发者)
SERVER --- CLIENT2(开发者)
SERVER --- CLIENT3(开发者)

在这种模式下,服务器保存代码的所有修订记录,而开发者则通过客户端连接到服务器,来获取最新的代码,或者提交代码。此外,通过服务器,可以管理开发人员的权限,也可以看到其他人在做什么。

这种模式因管理便捷,曾被普遍采用。这种模式下,一些著名的版本管理系统有 CVS(Concurrent Versions System) 和 SVN(Subversion)。

但集中管理仍然带来的一些问题。一旦服务器出现故障(通常称为 单点故障),那么开发者无法拉取代码,也无法提交代码。如果磁盘损坏,整个代码数据都会丢失。

分布式管理

为了解决集中管理的单点故障问题,分布式管理 的概念便被提出来了。

相比于集中管理,在拉取代码时,会把整个代码仓库下载下来,包括完整的代码修改记录。如果服务器出现了故障,就可以使用任意一个本地的代码仓库来恢复。

此外,分布式管理使得用户可以在多个不同的代码管理系统中管理代码,也可以和不同的小组进行协作。这是传统的集中管理无法做到的。

基于分布式管理的代码管理系统中,最著名的莫过于 Git 了。

你可以在 WikiMedia 上找到各种版本控制软件。

Git 是什么

基于快照流

Subversion 等版本管理系统通常将它们存储的信息看作是一组基本文件和每个文件随时间逐步累积的差异。这称为 基于差异 的版本控制。[1]

graph TB
    subgraph v1
    f1(文件1)
    f2(文件2)
    f3(文件3)
    end
    subgraph v2
    f1-->f1-1(修改1-1)
    end
    subgraph v3
    f2-->f2-1(修改2-1)
    end
    subgraph v4
    f1-1-->f1-2(修改1-2)
    f2-1-->f2-2(修改2-2)
    f3-->f3-1(修改3-1)
    end

这种模式下,版本管理系统存储了初始文件,以及各个版本中,文件的差异。

但是, Git 采用了一种 基于快照 的方式,来管理代码的版本。对于每个版本,它都会保存所有文件。但为了避免占用不必要的空间,它不会保存没有修改过的文件,而是引用之前存储的文件。

(这个例子使用虚线表示引用,实现表示文件更新)

graph TB
    subgraph v1
    f1(文件1)
    f2(文件2)
    f3(文件3)
    end
    subgraph v2
    f1-->f1-1(文件1-1)
    f2-.-f2-1(文件2)
    f3-.-f3-1(文件3)
    end
    subgraph v3
    f1-1-.-f1-2(文件1-1)
    f2-1-->f2-2(文件2-1)
    f3-1-.-f3-2(文件3)
    end
    subgraph v4
    f1-2-->f1-3(文件1-2)
    f2-2-->f2-3(文件2-2)
    f3-2-->f3-3(文件3-2)
    end

几乎本地执行

Git 中绝大多数操作都是在本地执行,相比于集中式版本管理,它减小了很多的网络开销。因为本地磁盘上有项目的完整历史,所以大部分操作看起来瞬间完成。[1:1]

保护文件完整性

Git 中所有的数据在存储前都计算校验和,然后以校验和来引用。这样避免了外部代码对 Git 内部数据的篡改。在传送过程中丢失信息或损坏文件,Git 就能发现。[1:2]

只添加数据

所有的 Git 操作几乎只会在 Git 本地数据库中增加数据。这样几乎避免了数据的丢失。

管理文件

基本概念

三种状态

Git 管理的代码有三种状态:

  • 已修改 (modified) 表示修改了代码,但还没保存到数据库中。[1:3]

  • 已暂存 (staged) 表示对一个已修改代码的当前版本做了标记,使之包含在下次提交的快照中。[1:4]

  • 已提交 (committed) 表示数据已经保存在数据库中。[1:5]

在开始那一部分,运行 git add 以前的代码便是 已修改状态,运行 git add之后,git commit 之前的代码便是 已暂存状态,运行 git commit 之后的代码便是 已提交状态

各种空间

Git 也具有三种空间:

  • 工作区 项目的根目录就是工作区。它其实是对项目的某个版本独立提取出来的内容。这些从 Git 仓库的压缩数据库中提取出来的文件,放在磁盘上供你使用或修改。[1:6]

  • 暂存区 是一个文件,保存了将要提交的文件列表信息,一般在 .git 目录中。[1:7]

  • Git 仓库 保存项目的元数据和对象数据库的地方,一般在 .git 目录中。 这是 Git 中最重要的部分,从其它计算机克隆仓库时,复制的就是这里的数据。[1:8]

跟踪状态

对于工作区下的每个文件,可以分为 已跟踪的(tracked) 和 未跟踪的(untracked)。对于已跟踪的文件,又分为 未修改(unmodified)、已修改(modified) 和 已暂存(staged)。

graph LR
FILE(文件状态)---UNTRACKED(未跟踪)
FILE---TRACKED(已跟踪)
TRACKED---UNMODIFIED(未修改)
TRACKED---MODIFIED(已修改)
TRACKED---STAGED(已暂存)

我们先从一个新文件开始探讨它的状态变化。

当一个新文件在工作区被创建时,它还没有被提交到 Git 仓库中,它便是 未跟踪的

当这个新文件第一次被添加(git add)到 暂存区 时,它便会成为 已暂存,因为 Git 仓库记录了这个文件。

新文件被提交(git commit)以后,它便成为 未修改 状态。当文件被改动后,便成为 已修改 状态。这时,当修改的代码被暂存(git add)时,它便成为 已暂存

对于网上拷贝(git clone)的代码,它便是 未修改 的。

我们用一个状态图来理解这个过程:

stateDiagram-v2
    [*] --> untracked: 创建文件
    [*] --> unmodified: 拷贝代码
    untracked --> staged: 暂存
    staged --> unmodified: 提交
    unmodified --> modified: 修改
    modified --> staged: 暂存

查看文件状态

还记得在开始那一节,我们可以使用 git status 可以查看文件的状态:

当出现新的修改时,成了这样:

当有修改被暂存时,成了这样:

当修改被提交后,成了这样:

但是这看起来挺复杂,我们可以添加 -s 参数来让他显得简洁一些:

1
git status -s

M README
MM Rakefile
A lib/git.rb
M lib/simplegit.rb
?? LICENSE.txt

文件这里有两个标识符,我们来解释一下。

第一个标识符是暂存区的状态,第二个标识符是工作区的状态。各种含义如下:

标识符含义
?未跟踪
A暂存的新文件
M修改的文件
D删除的文件
R重命名的文件
C复制的文件
U未合并的文件

当然,这只能看到文件名的一些信息。
如果需要查看 未暂存文件的 详细信息,可以使用 diff 指令:

1
git diff

如果需要查看 已暂存文件的 详细信息,可以使用 --staged 参数:

1
git diff -- staged

添加文件到暂存区

使用 git add 文件名/目录名 添加新的改变到暂存区。当操作的对象是一个目录时,会将其下所有文件目录添加到暂存区。

例如,把当前目录的所有改变添加到暂存区:

1
git add .

有时候,用户只希望添加某些类型的文件,可以额外的添加这些参数:

命令解释未跟踪已修改已删除被忽略
git add -A添加仓库内所有文件×
git add .添加仓库内所有文件×
git add -u .更新、删除指定文件或目录下所有文件××
git add --ignore-removal .更新、删除指定文件或目录下所有文件×
git add -f .强制添加已忽略的文件

对于 Git 1.x,git add . 命令不会添加当前目录中,已删除的文件。

将文件移出暂存区

使用下面的命令可以将文件移出暂存区:

1
git restore --stage [files...]

例如移出当前目录下的所有文件:

1
git restore --stage .

放弃修改

这个操作将会丢失数据,请慎重使用

使用下面的命令可以将未存入缓冲区的文件恢复到上一次提交的版本:

1
git restore [files...]

例如恢复当前目录下的所有文件到之前的版本:

1
git restore .

这种操作对于某些情况非常有用。

比如,你为了测试某个功能,临时写死了一些数据:

1
let data = [{name: "foo", id: 1}, {name: "bar", id: 2}];

在测试完成后,可以直接使用 git 将相关代码回滚到修改前的版本。

提交暂存区代码到仓库

使用 git commit 指令可以将代码提交到仓库。

默认情况下,它会打开配置指定的编辑器,让你填写提交信息。保存时,提交便会生效:

1
git commit

如果不需要打开编辑器,可以直接使用 -m 参数:

1
git commit -m '提交信息'

有时候使用暂存区会显得挺麻烦,你可以手动加上 -a 选项,则会把所有 已跟踪过 的文件暂存起来一起提交。

忽略一些文件

还记得我们在初体验添加的 .gitignore

1
node_modules

它的作用很简单,就是绕过对 node_modules 的跟踪。

下面介绍这个文件的一些语法规则:[2]

  • 所有空行或者以 # 开头的行都会被 Git 忽略。

  • 可以使用标准的 glob 模式匹配,它会递归地应用在整个工作区中。

  • 匹配模式可以以(/)开头防止递归。

  • 匹配模式可以以(/)结尾指定目录。

  • 要忽略指定模式以外的文件或目录,可以在模式前加上叹号(!)取反。

我们来看一一些具体的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 忽略所有的 .o 文件
*.o

# 保留 lib.o 文件,无论
!lib.o

# 忽略 node_modules 文件及文件夹
node_modules

# 忽略 node_modules 结尾的文件夹
node_modules/

# 忽略 node_modules 开头的文件夹
/node_modules

# 忽略 doc/ 目录及其所有子目录下的 .pdf 文件
doc/**/*.pdf

针对不同类型的项目,github/gitignore 这个仓库提供了不同的代码模板。这里给出前端常见的代码模板:

删除文件

如果要从 Git 中移除一些文件(如编译后的 dist 文件夹),可以使用 git rm 命令来完成工作:

1
git rm [文件名...]

它会将文件从暂存区和工作目录中删除。

但对于 node_modules这样的文件夹,你并不希望它进入暂存区,但希望它留在硬盘上。这时,你可以加上 --cache 参数:

1
git rm --cache node_modules

这种操作在忘记将 node_modulesdist 等文件夹加入 .girignore时,显得非常有用。

移动文件

和 LINUX 一样,git mv 用于移动文件,它会移动文件并自动暂存修改。

提交历史

当你运行时 git commit 时,Git 便会将你的提交信息、文件镜像等信息加入到提交历史中。

查看最近的一次提交

使用 git show 指令可以查看最近一次的提交:

回顾提交历史

如果你想看看这个项目之前做的所有提交,可以使用 git log 命令。没有传入任何参数的情况下,git log 会按时间先后顺序列出所有的提交,最近的更新排在最上面。

如果还需要查看每次提交时,文件的修改信息,可以添加 --patch 选项:

1
git log --patch

如果只需要快速浏览一下每个文件的修改状况,可以使用 --stat 选项:

1
git log --stat

如果需要跳过合并提交记录,还可以使用 --no-merges 选项。

如果需要对所有提交记录进行筛选,还使用以下几个选项:[3]

选项说明
-<n>仅显示最近的 n 条提交。
--since, --after仅显示指定时间之后的提交。
--until, --before仅显示指定时间之前的提交。
--author仅显示作者匹配指定字符串的提交。
--committer仅显示提交者匹配指定字符串的提交。
--grep仅显示提交说明中包含指定字符串的提交。
-S仅显示添加或删除内容匹配指定字符串的提交。

除此之外,还有一个非常强大的 --pretty 选项,它可以自定义每条提交的输出格式,这非常适合面向 Git 的程序。

例如,设置 --pretty=oneline,可以简化每条信息的显示:

1
git log --pretty=oneline

查看历史版本的工作区

通过 git log,你可以查看所有的提交历史,同时,你还能将工作区切换到历史版本:

1
git checkout 历史提交的hash或部分

这个操作不会影响当前的暂存区以及已修改的代码。

在这种状态下,如果你做了某些更改然后提交它们,标签不会发生变化, 但你的新提交将不属于任何分支,并且将无法访问,除非通过确切的提交哈希才能访问。对于这种问题,我们会在多分支技术提到。 [4]

如果想切换回来,可以使用:

1
git checkout master

重新提交

有时候,在提交之后,突然发现提交信息写错了,或者有几个修改忘记提交了。这时候可以使用 git commit --amend 指令来重新提交代码。

如果是提交信息写错了,直接运行 git commit --amend ,它会启动指定的编辑器让你修改信息。

如果是修改忘记提交了,需要先将需要提交的修改存入暂存区,再执行 git commit --amend,修改信息后,它会将暂存区的修改一并提交。

我们以“几个修改忘记提交” 为例。

先康康提交的状况:

1
git log --pretty=oneline

先查看暂存区的状况:

1
git status

发现有内容没有存入暂存区,这时我们把它存入暂存区:

1
git add -A

然后,我们来重新提交:

1
git commit --amend

输入下面的提交信息

保存后关闭编辑器。代码就重新提交了。

再运行 git log --pretty=oneline,可以看到最新一条提交信息已经变了。

回滚版本

reset

Git 可以让你的代码回滚到整个提交历史上的任何一个版本。

想要回滚,首先得知道是哪个版本吧。在 Git 中,通常用 HEAD 来表示当前的版本,而用 HEAD^表示上一个版本,HEAD^^ 表示上上个版本。还可以使用 版本号~n 表示回滚 n 个版本,如 HEAD~100 表示回滚 100 个版本。

回滚分为下面三种:

  • 软回滚 soft:只移动指针到对应版本号
  • 混合回滚 mixed:移动指针到对应版本号,并且回滚暂存区的修改
  • 硬回滚 hard:移动指针到对应版本号,并且回滚暂存区的修改和工作区的修改

硬回滚是非常危险的

1
2
3
4
5
6
7
8

回滚的命令如下:

```shell
git reset --回滚类型 版本号

# 硬回滚:
git reset --hard 版本号

如果不小心搞错了,也可以回滚回来,只需要知道回滚前的版本号,再进行一次 reset 就行了。

如果你的命令行没有关闭,你可以从之前的命令行窗口中找到版本信息。

但很不巧,你可能找不到回滚前的版本号了。不过,你还可以使用下面的指令来查询你做过的操作:

1
git reflog

revert

还有一种回滚的方法,就是 revert。它可以恢复某个版本产生的修改,并且产生一个新的提交信息:

1
git revert <提交ID>

标签

Git 中,可以对整个仓库的某个提交打上标签。通常,开发者会利用标签来标记一个版本的发布,如 v1.0v2.0

列出标签

列出标签是非常简单的,只需要使用:

1
git tag

v1.0
v2.0

通常,它们会按照字母方式[4:1],来对标签进行排序。

也可以通过 -l 属性来搜索标签:

1
git tag -l "v1.8.5*"

新建标签

Git 支持创建 轻量标签lightweight)和 附注标签annotated)。

其中轻量标签只是对某个提交的引用,而附注标签则是一个完整的对象,其中包含打标签者的名字、电子邮件地址、日期时间。[4:2]

打附注标签的方式很简单,只需要使用 -a 选项即可:

1
git tag -a v1.0.0 -m "version 1.0.0"

其中,后面的 -m 可以为提交指定额外的信息。

通过使用 git show 命令可以看到标签信息和与之对应的提交信息。

不使用 -a 指令,则会打轻量标签:

1
git tag v1.0.0

还可以给历史版本打上一个标签,方法是再加上版本的 hash

1
git tag v1.0.0 4fdb4a

同样的,如果想看某个标签的代码,也可以使用 checkout 命令:

1
git checkout v1.0.0

删除标签

使用 -d 选项,则会删除标签:

1
git tag -d v1.0.0

多分支管理

版本信息的存储

链式结构

Git 在每次提交时,会保存一个存储提交信息的对象。由于Git能够顺着提交信息找到更早的提交信息。这意味着它得有一个指向上一次提交信息的指针。

为了方便表示每一次提交,暂存操作会为每一个文件计算校验和,提交时,便会将校验和作为这个提交的标识符。

如果 Git 中有三个提交,那么每个提交可以简化成这样的链表:

graph RL
    COMMIT3(e1ffb5) --> COMMIT2(4fdb4a)
    COMMIT2 --> COMMIT1(f7585f)

那问题又来了,当前工作区的版本是哪个?

所以,我们还得有一个另外的指针,指向着当前的版本。

HEAD 表示当前的版本

记得在“回滚版本”这里,我们提到了HEAD 被通常来表示当前的版本。它实际上相当于一个指针,指向着当前的提交:

graph RL
	BRANCH1[[HEAD]] --> COMMIT3
    COMMIT3(e1ffb5) --> COMMIT2(4fdb4a)
    COMMIT2 --> COMMIT1(f7585f)

当然,我们还漏掉了Git默认创建的 master 分支:

graph RL
	BRANCH1[[HEAD]] --> COMMIT3
    COMMIT3(e1ffb5) --> COMMIT2(4fdb4a)
    COMMIT2 --> COMMIT1(f7585f)

checkout 做了什么

当你使用 checkout 指令切换到了一个较老的版本(比如说,切换到 4fdb4a 版本):

graph RL
	BRANCHM[[master]] --> COMMIT3
	BRANCH1[[HEAD]] --> COMMIT3
    COMMIT3(e1ffb5) --> COMMIT2(4fdb4a)
    COMMIT2 --> COMMIT1(f7585f)

实际上,只是让 HEAD 这个指针往前移动了一位。

如果你在这个时候提交了东西上去,那么提交信息会变成这样:

graph RL
	BRANCHM[[master]] --> COMMIT3
	BRANCH1[[HEAD]] --> COMMIT4
	COMMIT4(12c716) --> COMMIT2(4fdb4a)
    COMMIT3(e1ffb5) --> COMMIT2(4fdb4a)
    COMMIT2 --> COMMIT1(f7585f)

但这时候,你没创建分支,一旦你切换 master 分支了:

1
git checkout master
graph RL
	BRANCH1[[HEAD]] --> BRANCHM
	BRANCHM[[master]] --> COMMIT3
	COMMIT4(12c716) --> COMMIT2(4fdb4a)
    COMMIT3(e1ffb5) --> COMMIT2(4fdb4a)
    COMMIT2 --> COMMIT1(f7585f)

那么,除非你知道 12c716 这个标识,否则,你就无法找不到这个版本了。

reset指令做了什么

而当你使用 reset 指令让版本回滚了(比如说,回滚到 4fdb4a 版本),也只是修改了 masterHEAD 两个指针:

graph RL
	BRANCHM[[master]] --> COMMIT2
	BRANCH1[[HEAD]] --> BRANCHM
    COMMIT3(e1ffb5) --> COMMIT2(4fdb4a)
    COMMIT2 --> COMMIT1(f7585f)

这意味着操作过程中,Git 并没有直接删除快照,而且就算移动了,只要知道版本号,也可以找回这个版本。

分支操作

创建分支

理解了Git提交信息的存储结构,我们就可以来看一下,git 创建分支时,做了什么。

git 创建分支的指令如下:

1
git branch 分支名

需要注意,它只是创建分支,工作区仍然在原来的分支。

例如,创建一个 develop 分支:

1
git branch develop

实际上,它只是了创建了一个指向某个版本的指针:

graph RL
	BRANCHD[[develop]] --> COMMIT3
	BRANCHM[[master]] --> COMMIT3
	BRANCH1[[HEAD]] --> BRANCHM
    COMMIT3(e1ffb5) --> COMMIT2(4fdb4a)
    COMMIT2 --> COMMIT1(f7585f)

你可能会问,创建完分支后,Git 怎么知道我们在哪个分支?

但别忘了 HEAD 这个指针,它指向的其实是当前这个分支(也可以是某个版本)。

但是对于分支比较多的情况下,它优先指向分支。

切换分支

前面了解到,我们使用 checkout 切换到历史版本,切换到某个标签,对于分支,也可以使用它来切换:

1
git checkout develop

这样 HEAD 就指向 develop 分支了。

graph RL
	BRANCHD[[develop]] --> COMMIT3
	BRANCHM[[master]] --> COMMIT3
	BRANCH1[[HEAD]] --> BRANCHD
    COMMIT3(e1ffb5) --> COMMIT2(4fdb4a)
    COMMIT2 --> COMMIT1(f7585f)

在这个develop分支上,如果我们新建了一个提交,那么 develop 便会往后移动:

graph RL
	BRANCHD[[develop]] --> COMMIT4
	BRANCHM[[master]] --> COMMIT3
	BRANCH1[[HEAD]] --> BRANCHD
	COMMIT4(12c716) --> COMMIT3
    COMMIT3(e1ffb5) --> COMMIT2(4fdb4a)
    COMMIT2 --> COMMIT1(f7585f)

如果希望分支一创建,就立即切换到分支,可以在 checkout指令使用 -b 选项:

1
git checkout -b develop

创建分支时,默认是基于当前分支的,如果需要基于其他分支,可以增加一个参数,如:

1
git checkout -b develop origin/develop

develop 切换回 master 也是很简单的:

1
git checkout master

在切换分支的时候,如果工作区和暂存区还有修改,那么Git可能会因为内容冲突而阻止切换分支。一个最好的办法是在切换分支之前进行提交。

分支合并

现在我们在 master 分支基础上再创建一个临时分支 topic

1
git checkout -b topic

然后修改一些文件,进行提交。这时 Git 版本历史便会成这样:

graph RL
	BRANCHT[[topic]] --> COMMIT5
	BRANCHD[[develop]] --> COMMIT4
	BRANCHM[[master]] --> COMMIT3
	BRANCH1[[HEAD]] --> BRANCHT
	COMMIT5(09b1c2) --> COMMIT3
	COMMIT4(12c716) --> COMMIT3
    COMMIT3(e1ffb5) --> COMMIT2(4fdb4a)
    COMMIT2 --> COMMIT1(f7585f)

如果想把 topic 分支上的最新改动合并到 master 上,则需要先检出到 master 上,再使用 merge 指令:

1
2
git checkout master
git merge topic

这种情况下, master 分支和 topic 分支在同一条路径上,合并分支,只需要将 master 节点移动就行。

graph RL
	BRANCHT[[topic]] --> COMMIT5
	BRANCHD[[develop]] --> COMMIT4
	BRANCHM[[master]] --> COMMIT5
	BRANCH1[[HEAD]] --> BRANCHM
	COMMIT5(09b1c2) --> COMMIT3
	COMMIT4(12c716) --> COMMIT3
    COMMIT3(e1ffb5) --> COMMIT2(4fdb4a)
    COMMIT2 --> COMMIT1(f7585f)

这种只需要移动指针就可以合并的情况称为 快速合并

接下来,我们尝试将develop 分支的代码合并到 master 分支。但这出现了一个新的问题,这两个分支在 e1ffb5 这个版本上进行了分叉,而不在一条路径上,使用之前的 “移动指针”的方法似乎不管用。

这时候,Git 会创建一个指向两个父版本的版本,在新版本中对文件进行简单的合并:

graph RL
	BRANCHT[[topic]] --> COMMIT5
	BRANCHD[[develop]] --> COMMIT6
	BRANCHM[[master]] --> COMMIT6
	BRANCH1[[HEAD]] --> BRANCHM
	COMMIT6(559805) --> COMMIT5
	COMMIT6 --> COMMIT4
	COMMIT5(09b1c2) --> COMMIT3
	COMMIT4(12c716) --> COMMIT3
    COMMIT3(e1ffb5) --> COMMIT2(4fdb4a)
    COMMIT2 --> COMMIT1(f7585f)

实际上,在主分支额外创建一个版本保存合并信息,有利于记录完整的合并信息,以后如果某个分支删除了,那么这些合并信息依然是存在的。

但是快速合并就不会产生这样的信息了,所以快速合并有时候并不是我们想要的。这时候可以使用 --no-ff 选项,来关闭快速合并:

1
git merge topic --no-ff

删除分支

通常在合并完后,topic 这个临时分支就没有什么用了,可以删掉。

使用 git branch -d删除分支 topic

1
git branch -d topic

如果分支没有合并,像这样:

graph RL
	BRANCHT[[topic]] --> COMMIT5
	BRANCHD[[develop]] --> COMMIT4
	BRANCHM[[master]] --> COMMIT5
	BRANCH1[[HEAD]] --> BRANCHM
	COMMIT5(09b1c2) --> COMMIT3
	COMMIT4(12c716) --> COMMIT3
    COMMIT3(e1ffb5) --> COMMIT2(4fdb4a)
    COMMIT2 --> COMMIT1(f7585f)

那么,Git则会提示错误:

因为,一旦这个分支删除了,12c716 这个版本就可能找不回来了(除非记得这个版本号)。为了避免这种情况,Git默认不让删除。当然,按照上面的提示,如果需要强制删除,可以使用:

1
git branch -D develop

合并冲突

有时候,合并并不是那么顺利。比如我们在 develop分支, index.js 中将输出颜色从蓝色改成红色:

1
2
-console.log(chalk.bold.blue("Hello World"));
+console.log(chalk.bold.red("Hello World"));

master 分支, index.js 中将输出颜色从蓝色改成绿色:

1
2
-console.log(chalk.bold.blue("Hello World"));
+console.log(chalk.bold.green("Hello World"));

那合并的时候,到底是将 蓝色改成红色,还是应该将 蓝色改成绿色 呢?Git 不知道怎么做。

所以,在这个时候,使用 git merge 就会报错:

1
git merge develop

这个时候,查看一下状况:

1
git status

任何出现冲突未合并的文件,会被标识为 unmerged,且文件内会添加冲突标记 ,像这样:

1
2
3
4
5
<<<<<<< HEAD
console.log(chalk.bold.green("Hello World"));
=======
console.log(chalk.bold.red("Hello World"));
>>>>>>> develop

它包含了各个版本的信息,例如我们在 master 上(当前分支,HEAD 指向此分支),则有显示为绿色的代码;在 develop 上,则有显示为红色的代码。

为了解决冲突,你可以选择其中的一个,也可以自行合并。

1
console.log(chalk.bold.red("Hello World"));

修改完后,使用 add 指令加入暂存区:

1
git add .

然后重新提交:

1
git commit -m "merge index.js"

查看分支

git branch 不仅仅可以增加/删除分支,如果没有参数,它会显示所有分支:

1
git branch

左边还会使用 * 表示当前所在的分支。

如果需要查看每一个分支的最后一次提交,可以添加一个 -v 选项:

1
git branch -v

  • --merged则可以过滤掉列表中未合并到当前分支的分支。
  • --no-merged 则相反,只显示未合并到当前分支的分支。

使用 git branch --no-merged 命令,可以查看未合并的分支。其中,列表中显示的分支是不可以使用 git branch -d直接删除的,只能强制删除。

变基

在前面的小节中,我们使用 merge来将两个分支整合到一个,而这里将提供另外一个方法——变基(rebase)。

我们还是回到这个分叉的例子:

graph RL
	BRANCHT[[topic]] --> COMMIT5
	BRANCHD[[develop]] --> COMMIT4
	BRANCHM[[master]] --> COMMIT5
	BRANCH1[[HEAD]] --> BRANCHM
	COMMIT5(09b1c2) --> COMMIT3
	COMMIT4(12c716) --> COMMIT3
    COMMIT3(e1ffb5) --> COMMIT2(4fdb4a)
    COMMIT2 --> COMMIT1(f7585f)

我们还记得,合并是创建一个新的版本记录,并连接到两个父版本。而变基,则是将子分支中,新增的版本记录原封不动的移动到主分支上。

例如,我们需要将 develop 分支变基到 master 分支上,结果是这样:

graph RL
	BRANCHT[[topic]] --> COMMIT5
	BRANCHD[[develop]] --> COMMIT4
	BRANCHM[[master]] --> COMMIT5
	BRANCH1[[HEAD]] --> BRANCHM
	COMMIT5(09b1c2) --> COMMIT3
	COMMIT4(12c716) --> COMMIT5
    COMMIT3(e1ffb5) --> COMMIT2(4fdb4a)
    COMMIT2 --> COMMIT1(f7585f)

实际上就是把分叉开始时所有的更新移动到了 master 分支上。

要进行变基,需要先切换到 master 分支上:

1
git checkout master

然后执行变基指令:

1
git rebase develop

如果需要撤回变基,只需要执行:

1
git rebase --abort

相比于 mergerebase 将提交信息连成一条链,可以保持commit记录的整洁。但 rebase 更容易出现冲突。

远程仓库

介绍

Git 是一个分布式的管理系统,Git的相关信息除了保存在本地,还可以保存在服务器。

其中 GithubGitlab 是最著名的两个 Git服务器。

此外,你还可以自己搭建一个Git服务器。下面给出了两个常用服务器的搭建:

对于任何一个仓库,除了保存本地分支,还会保存远程分支。而远程分支是对远程跟踪分支的一个引用,其分支名为 <远端名称>/<远端分支名称>

拷贝仓库到本地

你可以使用 git clone 命令来拷贝一个仓库

1
git clone <仓库地址>

如果需要指定要拷贝的分支,可以添加 -b 参数:

1
git clone <仓库地址> -b <分支名>

有时候,拷贝整个项目,其体积非常大(例如某个用户中间拷贝了1G的文件上去又删掉了),这时候可以可以指定深度,这样,就只会保留最近一次的提交:

当你不是开发者且项目很大时,你可以通过这个来减小项目体积

1
git clone <仓库地址> --depth=1

默认情况下,当你使用 git clone 命令拷贝分支时,会自动创建一个远程仓库名,名为 remote

查看远程仓库

通过下面的命令,可以查看所有远程仓库以及它们的地址:

1
git remote -v

远程仓库名

通过下面命令可以添加远程仓库名:

1
git remote add <远程仓库名> <地址>

通过下面的命令可以移除远程仓库:

1
git remote remove <远程仓库名>

通过下面的命令可以重命名远程仓库:

1
git remote rename <远程仓库名> <新远程仓库名>

从远程仓库中抓取

从远程仓库中获得数据,可以执行:

1
git fetch <远程仓库名> <分支名>

例如下载 origin 上的 master 分支到 temp 分支:

1
git fetch origin master:temp

这时上次拉取以后远程分支仓库数据便会下载到本地,但它并不会自动合并或修改你当前的工作 ,这时候需要手动合并分支。

例如,合并刚刚下载的 temp分支到当前分支 HEAD

1
git merge temp

这两条命令可以使用 pull 命令合并到一个:

1
git pull <远程仓库名> <分支名>

这条命令则会从远端下载指定分支到本地,然后合并到当前的分支 HEAD。例如,从远端下载 master 分支到本地并合并:

1
git pull origin master

推送到仓库

使用下面的命令,可以将当前分支上的提交推送到远程仓库:

1
git push <远程仓库名> <分支名>

团队协作:Git 分支模型

在实际做项目的过程中,往往是多个人共同完成一个项目。对于这种情况,管理者需要额外花费一些时间去规划和管理Git工作流,否则一大堆杂乱无章的提交。会让项目变得难以协调和维护。

在这里,将介绍几种常用的分支管理模型:

  • TBD
  • Git-Flow
  • Github-Flow
  • Gitlab-Flow

TBD

介绍

TBD(Trunk Based Development) 则是基于主干的开发模式。

和 Git Flow 不同,这种开发模式只有一个主干——master,而没有其他任何长期存在的分支。所有的开发在主干分支上。

当有新版本发布时,则会额外创建一个名为 release-xxx 的发布分支,用于软件发布。

如果版本发布时,代码有缺陷时,则现将代码发布到主干分支,再发布到 release 分支。

TBD开发模式 图源

这种模式非常适合 Git Hooks,当代码提交发生的时候,就会触发 Git Hooks 来自动完成后面的测试和部署流程。

但为了保证测试和部署的效率,通常会要求每次代码提交的变更要小。

此外,有些提交并不代表一些新功能开发完成了,自动部署可能是没有必要的。为了解决这个问题,通常会为程序增加一个 特性开关 (Feature Toggle)的方式,只有功能开发完成了,才打开开关,让程序进行自动部署,否则只做一些测试工作。

优缺点

优点:

  1. 这种分支模式就显得非常简单了,只有一个主干分支,避免了不必要的合并冲突。
  2. 分支少,可以方便的进行自动化测试和部署。

缺点:

  1. 这种模式本身不支持并行开发,这意味着,多人项目如需要并行工作,需要在软件架构、人员分配上花费一些功夫。
  2. 对于开发纪律有较高的要求。
  3. 可能需要配套的集成设施和方法。

Git Flow

Vincent Driessen 在 A successful Git branching model 提出了一个经典而完整的分支管理模型,这里则对它的核心思想进行一个阐述。

主分支

在整个项目中,通常有两条主分支:

  • master
  • develop

其中 master 分支是 Git 默认创建的,仓库一建立完成,我们就可以建立 develop 分支。

master 分支通常保存的是 稳定的代码,分支上的代码一般是可以上线运行的。

develop 分支通常保存的是 最新的代码,这些代码通常是最新提交上去的,往往不稳定。

develop 分支的代码经过测试,没有已知 BUG,达到可以上线运行的水平时,develop 分支的代码便会合并到 master 分支上,并为这个版本打上版本号的 tag

master分支和 develop分支的关系 图源

我们现在可以探讨一下为什么这样分。

先得提一下 Git Hooks,它可以在某些动作(如代码提交、合并、打标签)触发时,执行一些操作。这给开发人员带来的极大的方便。比如说,对于一个 Vue 项目,我们可以通过 git 设置当 “代码提交” 这个事件触发时,npm run test 被自动执行,当 npm run test 执行成功后,npm run build 就会被自动执行。甚至还可以结合主流的 Docker 等容器技术,在某些动作被执行时自动将代码部署到服务器上。Git Hooks 的具体细节不在这里探讨。

在具体实践的过程中,masterdevelop 两个分支会设置不一样的 Git Hooks

  • 对于 master 分支,如果收到了新的合并,则会将 master 分支上的代码部署到正式服务器上。正式服务器则是可以直接给用户使用的。
  • 对于 develop 分支,如果收到了新的合并或推送,则会将 develop 分支上的代码部署到测试服务器上。测试服务器通常是给测试人员用的,它通常用于发现产品中遇到的BUG。这样的版本通常也称为 Nightly Build

在以前,机器性能不强的情况下,编译一个软件往往要好几个小时,为了避免白天的工作被影响,通常编译任务会放到半夜来执行。这便是 Nightly Build的含义。但是现在机器性能非常强大了,构建一个项目往往只需要很短的时间,但这个词仍然被保留到现在。

配套分支

masterdevelop 是整个项目开发的主线,它们会在整个项目周期都会存在。而下面的分支,则是基于这些分支进行一些属性工作。这些分支通常生命周期都比较短,在功能完成后,合并到主分支。

需要注意一点,在合并的时候,需要添加 --no-ff 选项,使得分支合并时,在主分支强制创建一个新的提交,这样可以保留合并的记录,避免配套分支删除后数据丢失。

配套分支来源合并目标命名规范解释
feature 分支developdevelop没有特别的规范通常用于新功能的开发。当功能开发完成后,将它合并回主分支。
hotfix 分支mastermasterdevelophotfix-*通常用于修复软件中的一些BUG
release 分支developmasterdeveloprelease-*翻译起来好像是发布分支,但它实际上是做一些发布前的准备工作,例如修改软件的版本号,或者修复一些小错误

配套分支和主分支 图源

缺点

  1. 这种分支模式看上去非常专业,但是 过于繁杂 ,对于团队来说,这可能是个很大的负担。
  2. feature 分支生命周期如果太长,容易会引发代码冲突,这是令人头疼的事情。
  3. master 分支只做版本发布,但其实也没有太大的意义。通过标签,就可以管理所有已发布的版本。这意味着,develop 分支可以代替 master 分支用来管理代码流,但这么搞 hotfix 分支和 feature 分支就只有字面上的差别了。
  4. 对于持续交付的项目,往往不需要 release 分支和 hotfix 分支。

Github Flow

官方文档:https://docs.github.com/cn/get-started/quickstart/github-flow
官方介绍:https://guides.github.com/introduction/flow/

主要流程

Github Flow 是一种在 Github 上非常流行的工作流,它的主要步骤如下:

  • 开启一个新的分支
    对于开源项目,你可以 fork 这个项目;对于自己的项目,你可以直接开一个新分支
  • 提交代码
    你可以任意地提交你的代码到你新开的分支上
  • 发起合并请求 Pull Request
    新分支开发完成后,或者需要讨论的时候,就向原分支发起一个合并请求。
  • 代码评审
    原分支管理者收到请求后可以对代码进行预览和评审
  • 测试
    在生产环境下进行代码测试
  • 代码合并
    合并请求通过后,代码便会合并到原分支上

需要注意几点:

  1. 如果上游分支改了,需要从上游分支拉取最新的代码,可以使用 变基 :
1
2
# 在 个人开发分支上
git pull origin master --rebase

这样就不会产生 merge 的提交信息,使得提交简洁清爽。

如果出现了冲突,则解决冲突后再变基:

1
git rebase --continue
  1. 提交合并请求后,如果没有通过,在代码修改完成后,可以使用下面的命令将上一条提交和本次提交合并:
1
git commit --amend --no-edit

理解

  1. master 分支上的代码是最新的,已经发布(正式上线)的代码
  2. 每个单独的分支和 master 分支并行开发,开发分支不影响正式上线的分支

优缺点

  1. 优点:Github Flow非常简单,适合持续发布的产品
  2. 缺点:对于大多数开源项目来说,master 分支上的代码和产品发布是一致的。但对于APP、网站等应用,可能会有一些上线前的审核之类的内容,master 分支所在版本不代表立即发布的版本,也就是 master 和上线的代码仍然存在距离。

GitLab Flow

针对持续发布的产品

对于一般的网页端应用,用户拿到往往都是最新的版本,这时候就没有必要使用一个专门的版本号,只需要将最新的代码发布到生产环境即可。

GitLab Flow 在 GitHub Flow 基础上,设置了了环境分支,如:

  • 开发分支 master
  • 预发布分支(可选) pre-production
  • 生产分支 production

这三个分支就像河流一样,开发分支是上游,预发布分支是中游,生产分支是下游。除非是特别紧急的情况,否则所有的代码必须从上游往下游发展。例如,生产环境出现了问题,那么需要在开发分支上新建一个功能分支,再合并到 master,确认没问题了再合并到 pre-productionproduction

针对版本发布的产品

对于移动APP、开源软件,不同的用户往往拿到的是不同的版本,这时候就需要使用版本号加以区分。

对于版本发布的产品,每当一个版本稳定后,从master 分支开启一个新的版本分支,如 3.0-stable

如果这个文档版本出现了问题,则在 master 分支上修改后,再合并到例如 3.0-stable 这样的版本分支,并且增加修订版本号。

优缺点

优点:

  1. 结合了 GitHub Flow 和 Git Flow 的优点
  2. 对于持续发布和版本发布都非常友好

一些技巧

合并请求

  • 这在 Github 上叫 Pull Request,在 Gitlab 上叫 Merge Request
  • 它的目的是通知分支的相关人员,让他们注意到你的请求。[5]

保护分支

主流的代码开发平台都提供了保护分支的功能,它可以防止负责人以外的人员随意推送代码。

小结

  1. TBD是一种最为简单粗暴的方法,但由于它不支持并行开发,对项目的结构以及开发人员要求较高。
  2. Git Flow 是一种非常复杂的方法,大多数项目是用不上的。
  3. Github Flow 是一种较为简单的方法,适合开源项目、持续发布的项目,但不支持需要多个环境(开发环境、生产环境)的项目。
  4. Gitlab Flow 是一种介于 Git Flow 和 Github Flow 之间的方法,适合大多数持续发布或者版本发布的项目。

Git 钩子及应用

Git 钩子可以在一些重要操作执行之前,先执行一些操作,例如在提交前对代码进行 lint 或者进行测试。

Git 有两组这样的钩子:客户端的和服务器端的。 客户端钩子由诸如提交和合并这样的操作所调用,而服务器端钩子作用于诸如接收被推送的提交这样的联网操作。

一般最为常用的客户端钩子有两个:

钩子发生时机用途
pre-commit键入提交信息前运行的操作对代码进行检查
commit-msg键入提交信息后,提交前运行的操作对提交信息检查

其他的客户端钩子和联网钩子我们不在这里详细说明,可以自行查看官网

husky

建议使用 husky 4.0的版本

Git 钩子默认存放在 .git/hooks 文件夹,用户可以手动在这些钩子里面添加一些 shell 命令。

但是,它不能在其他仓库直接传输。

所以,对于前端用户来说,有一个更好用的NPM包 husky,它可以帮我们管理所有的客户端 githooks。

运行下面的命令安装 husky 到本地。

1
npm i husky@^4.2.5 -D

接下来在 package.json 添加一个属性 husky,然后添加一个 pre-commit 的 hook,使得提交前进行测试:

1
2
3
4
5
6
7
8
{
"husky": {
"hooks": {
"pre-commit": "npm run test"
}
},
...
}

除了 husky,还有 yorkie 等工具可以完成相同的工作。

lint-staged

lint-staged 是一个代码检查和规范化工具,它可以对即将提交进Git的代码进行校验,防止一些糟糕的代码进入 Git 仓库。

要安装 lint-staged,使用下面的命令:

1
npm i lint-staged -D

然后,在 package.json 加上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"lint-staged": {
"*.{js,ts,vue}": [
"eslint --fix",
"git add"
],
"*.{css,scss}": [
"stylelint --fix",
"git add"
],
"package.json": [
"sort-package-json",
"git add"
]
},
...
}

其中,*.{css,scss}*.{js,ts,vue} 是一个 glob 表达式(参考:glob )。

每一个表达式后面接受一个数组,用来表示需要执行的shell操作。这些操作会按照定义的顺序依次执行。

lint-staged 通常配合 husky 使用:

1
2
3
4
5
6
7
8
{
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
...
}

提交规范

在大家学习Git的过程中,可能有一个很重要问题:为什么每一个提交需要写一个 message

不少人写 Git 的提交信息也是十分随意,这对于规模越来越大的项目是有害的,因为这使得项目的历史难以追踪。

所以社区渐渐出现了一些写提交信息的规范,这里只介绍 angular 提交规范。

angular 提交规范

angular 提交信息规范的格式如下:

1
2
3
4
5
<type>(<scope>): <subject>

<body>

<footer>

Header部分只有一行,包括三个字段:

  • type(必需)
  • scope(可选)
  • subject(必需)。

其中,type 用来指定提交信息的类别

  1. feat:新功能
  2. fix:修补 bug
  3. docs:文档改动
  4. style: 格式修改
  5. refactor:重构(即不是新增功能,也不是修改 bug 的代码变动)
  6. test:增加测试
  7. chore:构建过程或辅助工具的变动

scope 用来指定提交信息的范围

subject 描述提交信息的主题

需要注意的时,如果是撤销一个提交,则应该再加上一个 revert: 前缀:

1
revert: fix(user): page do not refresh after logged in

BODY(可选)

Body 部分是对本次 commit 的详细描述,并且应当遵守下面两个规则:

  • 用第一人称现在时
  • 应该说明代码变动的动机,以及与以前行为的对比。
    [6]

FOOTER(可选)

Footer 通常用于下面的两种情况:

  • 不兼容的变动(BREAKING CHANGE):以 BREAKING CHANGE: 开头,后面是对变动的描述、变动理由和迁移方法。
  • 关闭一个 Issue:Closes #xxx,#xxx
    [6:1]

执行规范

  1. 安装 commitizen
1
npm install -g commitizen
  1. 初始化提交信息规范
1
commitizen init cz-conventional-changelog --save --save-exact
  1. 之后,使用下面任意一个命令来执行提交
1
2
git cz
git-cz

对于非 node 项目,可以使用全局的git-cz包来执行规范:

1
npm install -g commitizen git-cz

changelog 自动生成

conventional-changelog-cli 是一个自动生成 changelog 的工具。如果你的所有提交信息遵循 angular 规范,那么它就可以根据你的提交信息自动生成提交记录。

一份提交记录通常包括:

  • 新的功能,对应 feat 类型的提交
  • 修复的BUG,对应 fix 类型的提交
  • 不兼容的改变,对应 BREAKING CHANGE 尾注

你还可以手动指定需要展示的内容。此外,每个部分都会罗列相关的 commit ,并且有指向这些 commit 的链接。[5:1]不过,它不会自动帮你把 changelog 进行提交,这意味着你可以手动修改你的 changelog

一个具体的CHANGELOG 案例,可以参考 vue-clivue-next

使用下面的命令安装 conventional-changelog-cli

1
npm i -g conventional-changelog-cli

使用下面的命令便可以自动生成CHANGELOG:

1
conventional-changelog -p angular -i CHANGELOG.md -s

如果需要生成所有版本的日志,则可以使用:

1
conventional-changelog -p angular -i CHANGELOG.md -s -r 0

小结

提交规范对于一个规模较大的项目是比较重要的,它可以做到下面的三点:

  • 方便追踪项目的历史
  • 方便查找和过滤历史记录
  • 方便生成更新日志

高级特性

贮藏区

假如你正在做一个项目,为了方便协作,你开启了特性分支 feat-A 和特性分支 feat-B。当你正在完善 feat-A,且手头上还有一些代码没有提交,但突然,你需要去操作 feat-B 上的内容。

如果我们直接 checkoutfeat-B 分支,那么你暂存区中的代码会留存,而且,如果切换过程中,feat-B 如果会覆盖你正在做的工作,它会拒绝执行操作并报错。

除了暂存区、工作区,Git还提供了一个栈结构的贮藏区,它可以将你正在修改的部分暂存到贮藏区:

1
git stash

执行完后,暂存区和工作区的未提交内容会清空。(但只是转移了,可以恢复)

当你处理完 feat-B 的工作时,你可以使用下面的命令来恢复暂存区的内容:

1
git stash pop

  1. Pro Git V2 - Git 是什么 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  2. Pro Git V2 - Git-基础-记录每次更新到仓库 ↩︎

  3. Pro Git V2 - 查看提交历史 ↩︎

  4. Pro Git V2 - Git 基础 - 打标签 ↩︎ ↩︎ ↩︎

  5. Git 工作流程 ↩︎ ↩︎

  6. Commit message 和 Change log 编写指南 ↩︎ ↩︎

  • 标题: 前端项目管理-Git篇
  • 作者: ObjectKaz
  • 创建于: 2021-05-16 04:46:54
  • 更新于: 2023-05-25 17:08:17
  • 链接: https://www.objectkaz.cn/a5d0689f4541.html
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。