Git 重学指南

不要因为学不会 Git 而选择 remake。在那之前,先来 remake 一下你的 Git 知识。

这是一篇 Git 重学指南,顾名思义,帮助你重新学习 Git。它适用于已经学习过 Git,但是还想再学一遍的人。

为什么要再学一遍?

我相信很多人和我一样,第一次知道 Git 是因为 GitHub。为了把自己的代码上传到 GitHub,上网随便找了一篇 Git 教程,匆匆浏览一遍,大概记住了几个命令,然后就开始使用 Git。最开始还没有什么问题,但是当你的项目做大了,或者有人向你发来了第一个 Pull Request,或者你想给项目发布第一个 Release,问题就接踵而至。

  • 那些教程里讲的分支、合并、回滚,我应该在什么时候用?
  • Pull Request 里的合并冲突为什么要在本地解决?
  • Release 里的 tag 到底是打在哪里的?
  • 我的项目依赖是否应该用 submodule 引入?

假如你对这些问题还是一头雾水,这说明,你的 Git 知识需要 remake 了。继续阅读本书,一定会对你有所帮助。

当然,如果你能清晰地回答出每个问题的正确答案,那么你的知识水平已经超过我了 :D。你可以继续往后翻一翻,看看文章中是不是有错误,指出来,或者帮我改正它。

如果我没学过 Git 呢?

如果你从来没有学过 Git,我的建议是先去阅读其他的教程,比如 git-recipes廖大的教程,他们对初学者来说,绝对比这本书友好。

这本书会讲什么?

你可以看一眼旁边的目录。嗯,这个目录似乎和其他的 Git 教程没有什么区别。不过,我可以保证,这本书里的内容会和那些入门向的教程大不相同。

章节架构的相似是刻意为之。我希望这是一本由浅入深再浅出的书,按照相似于初次学习时走过的路径,再次看到那些命令和概念时,会有不一样的收获。

第一遍学到某个东西时,也许难免会产生一些误解。这里就是澄清误解,看透本质的好地方。

这里讲的一定是正确的吗?

老实说,我不确定。因为我本人也是正处于 remake 我的 Git 知识的阶段,这本书相当于我重学的笔记,所以难免会有一些错误。

另一方面,这本书会涉及到很多“最佳实践”。众所周知,“最佳实践”永远不是最完美的解决方案。而且,“最佳实践”难免带有我个人的喜好。我会尽可能做到客观,还希望诸位自行甄别扬弃。

许可

知识共享许可协议

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

安装

既然是重新学习,那么安装想必是已经会了。

不过还是简单说一下,就当是参考手册吧。

下载

Linux

大部分情况下,可以用发行版自带的软件包管理器安装。

# Debian/Ubuntu
sudo apt-get install git

# Debian/Ubuntu (old version)
sudo apt-get install git-core

# RedHat/CentOS
sudo yum install git

Mac OS

使用 Homebrew。

brew install git

Windows

Windows 永远是安装开发工具最麻烦的那一个。

在 Windows 上安装 Git 的最佳选择是使用 Git for Windows,它自带了一个精简的 MSYS2 环境。

使用 winget

新版本 Windows 11 和 Windows 10 已经支持 winget,它可以打造像 Linux 发行版那样一键安装的良好体验。

如果你的 Windows 10 没有更新到最新版,可以先从 Micosoft Store 安装「应用安装程序」(其实就是 winget)。

winget install --id Git.Git -e --source winget

唯一的缺点是它会自动连接 GitHub,在网络环境不那么通畅的时候,懂的都懂。

下载安装程序

如果不使用 winget,可以下载安装程序,然后手动安装。安装程序可以从官网,或者国内访问速度更快的淘宝镜像获取。

Git for Windows 的安装配置

Git for Windows 在安装时提供的可配置选项极其复杂。虽然默认配置已经很不错了,不过想自己配置一下也是可以的。

下面对几个常用但有些困惑的选项进行解释。

Windows Explorer integration

向右键菜单中添加「Git Bash Here」「Git GUI Here」选项。

大多数情况下,Windows Terminal 和代码编辑器的 git 集成可以完全代替这两个功能,所以不建议打开。

Adjusting your PATH environment

决定将什么添加进 PATH 环境变量中。

默认的选项,也是推荐的选项,是只添加 git 命令。

第三个选项是将整个 MSYS2 环境添加进 PATH 环境变量中,但不要以为就可以借此使用 MSYS2 了,因为 Git for Windows 的 MSYS2 精简掉了很多东西,如果想用 MSYS2,还是单独装一个吧。

Configuring the line ending conversions

设置换行符的转换。

这是一个 Windows 独有的选项,因为只有 Windows 系统的换行符是 CRLF。

默认的选项是推荐的选项,表示在本地的代码中统一使用 CRLF,但在 git 储存中统一使用 LF。这表示你的代码在 git add 的时候会经过从 CRLF 到 LF 的转换,而在 git clone 或者 git checkout 的时候会经过从 LF 到 CRLF 的转换。

配置

名字和邮箱

在命令行输入:

git config --global user.name "Your Name"
git config --global user.email "email@example.com"

因为 Git 是分布式版本控制系统,所以,每个机器都必须自报家门:你的名字和邮箱。大多数情况下,Git 就是靠这些来确定身份的。

注意 git config 命令的 --global 参数,这表示将配置设置为全局的。全局设置对本机的每个仓库都生效。

SSH 密钥

SSH 密钥用于连接 GitHub 或者类似的托管服务。

首先查看你的 SSH 密钥是否已经生成。

ls ~/.ssh

如果你没有生成过 SSH 密钥,那么这个目录应该是不存在的。你可以使用 ssh-keygen 命令来生成一个新的密钥。

ssh-keygen -t rsa

如果在 Windows 系统上找不到 ssh-keygen 命令,那么可以在 Git Bash 中运行。

生成完成后,查看生成的公钥:

cat ~/.ssh/id_rsa.pub

在 GitHub 上添加这个公钥,然后检测一下是否成功:

$ ssh git@github.com
PTY allocation request failed on channel 0
Hi Wybxc! You've successfully authenticated, but GitHub does not provide shell access.
Connection to github.com closed.

看到这样的欢迎信息,说明 SSH 密钥添加成功。

GPG 密钥

上面提到 Git 在确定身份的时候用的是名字和邮箱,但是,这也意味着只要知道了别人的名字和邮箱,就可以冒充他的身份。理论上,这种冒充是无法甄别的,甚至 GitHub 也无法避免。

所以,在名字和邮箱以外,Git 还有另一重保护措施:GPG 密钥。你可以使用自己的 GPG 私钥对提交进行签名,这样别人就能通过公钥鉴定身份。如果你的所有提交都经过签名,那么别人就知道没有签名的那些提交就是冒牌货。

生成 GPG 密钥:

gpg --full-generate-key

如果在 Windows 系统上提示找不到 gpg 命令,可以在 Git Bash 中运行。

基本按照默认配置即可。生成时需要填写名字和邮箱,需要和 Git 的配置一致,而且邮箱需要和 GitHub 账号认证的邮箱一致。

如果生成的是 RSA 密钥,需要设置密钥长度。GitHub 要求 RSA 密钥是 4096 位。

生成过程中会给密钥设置密码,这个密码是解锁私钥的关键。

生成完成后,查看生成的密钥:

$ gpg --list-secret-keys --keyid-format LONG
~/.gnupg/pubring.kbx
-----------------------------------------------
sec   ed25519/85E18D73E1B2E193 2022-06-15 [SC]
      6BBEA37A752DBC75008ABB6B85E18D73E1B2E193
uid                 [ultimate] 忘忧北萱草 <wybxc@qq.com>
ssb   cv25519/FB6CDE598D2CD679 2022-06-15 [E]

这里的 85E18D73E1B2E193 就是密钥 ID。

将 GPG 与 git 相关联:

git config --global user.signingkey 85E18D73E1B2E193
git config --global commit.gpgsign true

此时可以随便开一个 Git 仓库,测试一下 GPG 签名是否可用:

$ git init test
$ cd test
$ echo "test" > test.txt
$ git add .
$ git commit -m "test"
$ git cat-file commit HEAD
tree 02bdfa7bef60afbeb20ae09bb3145e2a4b1cb977
author 忘忧北萱草 <wybxc@qq.com> 1655478113 +0800
committer 忘忧北萱草 <wybxc@qq.com> 1655478113 +0800
gpgsig -----BEGIN PGP SIGNATURE-----

 iQIzBAABCgAdFiEEpRDvWeHZl2iUnVm+/Xek1OdFr8MFAmKsl2EACgkQ/Xek1OdF
 r8PWLQ/+JO2J99tFrMqK4eFPbhhDVbQJBxmYcJHS/iSVqxm6c8S97itQ//7QnLIe
 rpk1/jspbLm4E5f4EzYeoX4v9UstSDBpgrYnAQj81txwZ8l4Kps/XGv4lXDaaTrH
 E8V3Bk3zRj57RjLQwRC2TZGFZXJy6/bQUjgMS3jkiGdcpUsBzrVXmo5sOmPtM8GN
 m1yB1mgy/jjs8nuJIKG8hf1NuAaG+ST8hToEGDb5rYdBCLc4R8dPkp4Ha+3SHcZy
 MMBvAf5STWQAW7gq6Vwsdqj9IqF4DQga5yWvodnJkKtbkJY5RIvgh1NLrT4/M266
 KnVbRgd6ifPzUJb4yyhuOUR32y52JqYXz3SGb7HMkN541HtquaClSCt45lsTACzO
 2AhrtlMl3GYCL9EedQ8ql9cCn+Aj2Ol/PnizK49dE+Vfc/KtzUU+9vsMOD1aZ2vW
 2M8Nz5GLiLX7lpnOUviYnMf7/VStTbeielvcKnC4FQDbJK86yEmjjK+tCTgoiGo3
 Eive7LPB0H9ruenXx26hlOwP1vb2PUSWxXXM0bwEjktrvCRX8byE4w/q9gN8G8TZ
 kZJbvyPh5JBDnq1AIhhyPu9nnizmFml10ynWxqwPQplVG4g1SkV2kYTd8rvt2Jit
 Dj/ka9f1Sh6T2r5r06WNA+DvRJrbYWDovOU68Edj7XALTfmZNeM=
 =pF8U
 -----END PGP SIGNATURE-----

能够在 commit 中看到 GPG 签名,说明 GPG 签名已经在 Git 中配置成功。

如果配置 GPG 之后,提交时 Git 找不到 GPG,那么可以手动设置一下 GPG 的路径:

$ which gpg
/usr/bin/gpg
$ git config --global gpg.program /usr/bin/gpg

如果遇到 error: gpg failed to sign the data 的问题,可以尝试把 GPG_TTY 环境变量设置为 $(tty)参考):

$ git config user.signingKey 38AF394C
$ git config commit.gpgSign true
$ echo "test" | gpg --clearsign
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256

test
gpg: signing failed: Inappropriate ioctl for device
gpg: [stdin]: clear-sign failed: Inappropriate ioctl for device

$ export GPG_TTY=$(tty)
$ echo "test" | gpg --clearsign
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256

test
-----BEGIN PGP SIGNATURE-----
XXXXXX SUCCESS XXXX
-----END PGP SIGNATURE-----


ref: https://github.com/keybase/keybase-issues/issues/2798

接下来导出 GPG 公钥,然后添加到 GitHub 中:

$ gpg --armor --export 85E18D73E1B2E193
-----BEGIN PGP PUBLIC KEY BLOCK-----

mDMEYqn6LBYJKwYBBAHaRw8BAQdAdUKrYViGQwyP6OvG0zi4BtkztQSHmVSzJweG
RH5YbkC0HuW/mOW/p+WMl+iQseiNiSA8d3lieGNAcXEuY29tPoiTBBMWCgA7FiEE
a76jenUtvHUAirtrheGNc+Gy4ZMFAmKp+iwCGwMFCwkIBwICIgIGFQoJCAsCBBYC
AwECHgcCF4AACgkQheGNc+Gy4ZMLvgD+LPHZ/61I3V2QtcQxjhE7Mmx/zzf7nMu9
yoKsBS0DoHsBAOmGcnz8Ldxt8g5annPoS4JXhyC0vUgvBl5ks+LQ6WILuDgEYqn6
LBIKKwYBBAGXVQEFAQEHQKwoXBZT20+8mEfaqAuSEyMi4eeiE1Adjkj0TCZuV2UE
AwEIB4h4BBgWCgAgFiEEa76jenUtvHUAirtrheGNc+Gy4ZMFAmKp+iwCGwwACgkQ
heGNc+Gy4ZOtkwEAuyAAI2Va4xNuXwWf+WZg7Bt5KcXfcezYGG3EDFstaJMA/i6O
B1wiq7kByLBFmRVBGewKpDA2Z2UGnAQuSvCY5nwF
=FzfD
-----END PGP PUBLIC KEY BLOCK-----

之后,提交的 commit 在 GitHub 上就会显示 verified,表明这条 commit 是通过了 GPG 签名认证的。

储存库

储存库,或者叫版本库、代码仓库,英文名 repository,是 Git 用来存储和管理程序的源代码的结构。我们在本地开发的大部分时间,都是在和储存库斗智斗勇。

实际上,储存库是 Git 中唯一核心的概念。所有的 Git 操作,都是围绕着管理和同步储存库来进行的。

创建储存库

本节部分内容参考自 git-recipes,在原文基础上有一定修改。

git init

git init 命令创建一个新的 Git 储存库。它可以在一个已经有文件的储存库中使用,用来将已存在但还没有版本控制的项目转换成一个 Git 储存库,或者在一个空文件夹里使用,创建一个空的新储存库。

运行 git init 命令会在你项目的根目录下创建一个新的 .git 目录,其中包含了你项目必需的所有元数据。除了 .git 目录之外,已经存在的文件不会被改变。

详细用法

点击展开
git init

将当前的目录转换成一个 Git 储存库。它在当前的目录下增加了一个 .git 目录,于是就可以开始记录项目版本了。

git init <directory>

在指定目录创建一个空的 Git 储存库。运行这个命令会在当前目录下创建一个名为 directory,只包含 .git 子目录的空目录。

git init --bare <directory>.git

初始化一个裸的 Git 储存库(无工作树)。这个目录会创建一个名为 <directory>.git 的目录,其中包含了一个储存库的元信息。裸的储存库一般用作服务器上的共享储存库。关于裸的储存库,后文会详细解释。

git clone

git clone 命令拷贝整个 Git 仓库。这个命令获取到的副本是一个完备的Git仓库——它包含自己的历史,管理自己的文件,并且环境和原仓库完全隔离。

为了方便起见,Git 在 clone 时会自动创建一个名为 origin 的远程连接,指向原有仓库。这让和中央仓库之间的交互更加简单。

如果项目在远程仓库已经设置完毕,git clone 是用户获取开发副本最常见的方式。和 git init 相似,clone 通常也是一次性的操作——只要开发者获得了一份工作副本,所有版本控制操作和协作管理都是在本地仓库中完成的。

详细用法

点击展开
git clone <repo>

将位于 repo 的仓库克隆到本地机器。原仓库可以在本地文件系统中,或是通过 HTTP 或 SSH 连接的远程机器。

git clone <repo> <directory>

将位于 repo 的仓库克隆到本地机器上的 directory 目录。

谁是本体?

当我们用 git init 创建储存库的时候,Git 只是在当前目录下创建了一个 .git 目录。而如果我们手动把这个目录删掉,它就又从储存库变回原本平平无奇的文件夹了。整个过程中,Git 没有对原本的文件做出任何破坏性的改变。

看来所有的秘密都藏在这个 .git 目录中了。Git 到底对这个目录用了什么魔法?

当我们在开发时,99.999% 的时间都是在和我们自己的代码打交道,.git 文件夹对我们而言是一个黑箱。所以,有的时候,我们可能会产生一种错觉,认为我们能看到的代码是本体,而 .git 只是 Git 提供的附属品而已。

但事实并非如此。从我们敲下 git init 的那一刻开始,世界在 Git 的眼中就发生了改变。在 Git 看来,那个不起眼的 .git 才是储存库的本体,其他的一切都是它的附属品。

没错,就和一些漫画中的人物一样,「眼镜才是本体」。

储存库和工作树

什么是工作树?

在 Git 眼中,真正的储存库只是那一个 .git 文件夹。而在外面天天被我们盯着看的东西,在 Git 的世界里叫做「工作树 (worktree)」。

一个 Git 储存库可以有一个或多个工作树,甚至可以没有工作树。我们用 git initgit clone 建立的,都是一个储存库和一个工作树的组合,这样建立的工作树称为「主工作树」。

git worktree list 命令,可以查看当前 Git 储存库中的所有工作树。

$ git worktree list
E:/git-remake-guide  5c64c4d [创建储存库]

Git 的文件系统

在 Git 中,文件以两种形式存放:在储存库中,按照 Git 的格式将每一个版本储存为一个个 object,并按照更改历史、提交信息等等建立索引;在工作树中,按照一般的操作系统可识别的文件系统的方式存放。由于在储存库中,文件存在多个版本,而且并不是按照目录结构存放的,所以,对储存库中的文件不能直接编辑,而是要通过 Git 命令提取到工作树之后再编辑。

从储存库中取出文件,将其置入工作树中的操作,称为「签出(checkout)」。没错,就是熟悉的 git checkout 命令。

而与之对应的,将工作树中的文件写入储存库的操作,称为「签入」。签入操作没有一个单独的命令对应,而是通过 git add git commit 等一系列命令来完成。

一般情况下,开发的流程是这样的:先从储存库中签出文件,然后在工作树中修改文件,最后进行签入,把工作树中的文件写入储存库。

所以,Git 的储存库不仅包含着工作树中全部的内容,也包含着工作树全部的历史。就算把工作树删掉,依然可以从储存库中签出一个工作树的版本出来。

裸仓库

工作树的存在,是为了方便对储存库内容进行编辑。有的时候,我们只需要 Git 储存库,而不需要工作树。比如,像 GitHub 这样的托管服务上,只需要知道用户的储存库,而不用管理一个工作树。

像这样没有工作树,只有储存库的情况,称为「裸仓库」。

git init --bare <directory>.git 命令,可以创建一个裸仓库。这个命令会在当前文件夹下建立一个名为 directory.git 的文件夹,其中的文件结构和一般的 .git 如出一辙。

Git 约定裸仓库的名称以 .git 结尾。这就是为什么我们看到的 GitHub 上的储存库链接的结尾都是 XXX.git;实际上,这就是一个裸仓库的文件夹名。

既然我们可以克隆一个 GitHub 上的裸仓库,我们当然也能克隆一般的储存库。你可以找一个本地的 Git 储存库,然后克隆它试试:

$ git clone E:\git-remake-guide\.git
Cloning into 'git-remake-guide'...
done.
$ lsd --tree --classic .\git-remake-guide\
git-remake-guide
├── assets
│   ├── custom.css
│   ├── sidetoc.css
│   └── sidetoc.js
├── book.toml
├── README.md
├── src
│   ├── Git-重学指南.md
│   ├── SUMMARY.md
└── theme
    ├── favicon.png
    ├── favicon.svg
    └── index.hbs

完美。

管理暂存区

本节部分内容参考自 Pro Gitgit-recipes,在原文基础上有一定修改。

在开始之前,先上一张图:

Git 文件状态

看完这张图,我相信你已经懂了。你可以跳过下面的内容继续看下一节了。

文件的状态变化周期

工作目录下的每一个文件都不外乎两种状态:已跟踪 (tracked)未跟踪 (untracked)。已跟踪的文件是指那些被纳入了版本控制的文件,在上一次快照中有它们的记录,在工作一段时间后,它们的状态可能是未修改 (unmodified)已修改 (modified)已暂存 (staged)。简而言之,已跟踪的文件就是 Git 已经知道的文件。

工作目录中除已跟踪文件外的其它所有文件都属于未跟踪文件,它们既不存在于上次快照的记录中,也没有被放入暂存区。初次克隆某个仓库的时候,工作目录中的所有文件都属于已跟踪文件,并处于未修改状态,因为 Git 刚刚签出了它们,而你尚未编辑过任何东西。

编辑过某些文件之后,由于自上次提交后你对它们做了修改,Git 将它们标记为已修改文件。在工作时,你可以选择性地将这些修改过的文件放入暂存区,然后提交所有已暂存的修改,如此反复。

总结一下,在 Git 中,正常的文件状态变化周期是:在已跟踪的三种状态(未修改、已修改、已暂存)中不断循环,或者进入和退出未跟踪状态。

查看文件状态

git status

git status 命令显示工作目录和暂存区的状态。你可以看到哪些更改被暂存了,哪些还没有,以及哪些还未被 Git 追踪。

$ git status
On branch 管理暂存区
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   src/储存库/Git-文件状态.svg

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   src/SUMMARY.md
        deleted:    test.txt

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        src/储存库/管理暂存区.md

no changes added to commit (use "git add" and/or "git commit -a")

这就是一个典型的 git status 的输出,它显示了哪些文件是已暂存的,哪些文件是已修改的,哪些文件是未跟踪的。

状态简览

git status 命令的输出十分详细,但其用语有些繁琐。Git 有一个选项可以帮你缩短状态命令的输出,这样可以以简洁的方式查看更改。如果你使用 git status -s 命令或 git status --short 命令,你将得到一种格式更为紧凑的输出。

$ git status -s
 M README
MM Rakefile
A  lib/git.rb
M  lib/simplegit.rb
?? LICENSE.txt

新添加的未跟踪文件前面有 ?? 标记,新添加到暂存区中的文件前面有 A 标记,修改过的文件前面有 M 标记。输出中有两栏,左栏指明了暂存区的状态,右栏指明了工作区的状态。例如,上面的状态报告显示: README 文件在工作区已修改但尚未暂存,而 lib/simplegit.rb 文件已修改且已暂存。Rakefile 文件已修改,暂存后又作了修改,因此该文件的修改中既有已暂存的部分,又有未暂存的部分。

暂存新文件

git add

git add 命令将文件提交到暂存区。使用这个命令提交的文件,会直接变成已暂存的状态。最开始的那张图上,所有指向「已暂存」的箭头,都是通过 git add 命令实现的。

git add 的另一个作用是将在工作目录中删除的文件在暂存区标记为已删除。当 Git 发现 git add 提交的文件在暂存区或储存库中存在,而在工作目录中不存在时,会将一条删除记录添加到暂存区,表示该文件已被删除。

git add 作用于目录时,Git 会对目录中所有的文件递归地应用 git add 的功能。所以 git add . 命令可以一次性将工作目录中所有的新增、修改、删除的文件都提交到暂存区。

详细用法

点击展开
git add <file>

<file> 中的更改加入下次提交的缓存。

git add <directory>

<directory> 下的更改加入下次提交的缓存。

git add -i

开始交互式的缓存,你可以选择文件的一部分加入到下次提交缓存。它会向你展示一堆更改,等待你输入一个命令。y 将这块更改加入缓存,n 忽略这块更改,s 将它分割成更小的块,e 手动编辑这块更改,以及 q 退出。

git rm

git rm 命令用于删除工作目录或暂存区中的文件。它可以将删除记录写入暂存区,或者将文件从暂存区删除。

git rm 是安全的。只有当 Git 确信删除操作是可恢复的时候,才会直接执行删除操作。比如说,如果试图 git rm 一个已修改的文件,Git 就会给出提醒,当然你可以用 -f 无视这一安全性,强行删除一个已修改的文件。

如果你想保留工作目录中的文件,但是删除对应暂存区中的文件,请使用 --cached 选项。为什么会有这种奇怪的需求呢?假设这样一种场景,你忘记了添加 .gitignore 文件,不小心把很多本应忽略的文件加到了暂存区,这时候就需要这里介绍的做法了。

git mv

不像其它的 VCS 系统,Git 并不显式跟踪文件移动操作。如果在 Git 中重命名了某个文件,仓库中存储的元数据并不会体现出这是一次改名操作。不过 Git 非常聪明,它会推断出究竟发生了什么。

你依然可以使用 git mv file_from file_to 对文件改名。它会恰如预期般正常工作。实际上,即便此时查看状态信息,也会明白无误地看到关于重命名操作的说明:

$ git mv README.md README
$ git status
On branch master
Changes to be committed:
    (use "git reset HEAD <file>..." to unstage)
        renamed: README.md -> README        

事实上,运行 git mv 就相当于运行了下面三条命令:

mv README.md README
git rm README.md
git add README

如此分开操作,Git 也会意识到这是一次改名,所以不管何种方式结果都一样。两者唯一的区别是,git mv 是一条命令而另一种方式需要三条命令,直接用 git mv 轻便得多。不过有时候用其他工具批处理改名的话,要记得在提交前 git rm 删除旧文件名,再 git add 添加新文件名。

忽略文件

一般我们总会有些文件无需纳入 Git 的管理,也不希望它们总出现在未跟踪文件列表。通常都是些自动生成的文件,比如日志文件,或者编译过程中创建的临时文件等。在这种情况下,我们可以创建一个名为 .gitignore 的文件,列出要忽略的文件的模式。来看一个实际的 .gitignore 例子:

$ cat .gitignore
*.[oa]
*~

第一行告诉 Git 忽略所有以 .o.a 结尾的文件。一般这类对象文件和存档文件都是编译过程中出现的。第二行告诉 Git 忽略所有名字以波浪符(~)结尾的文件,许多文本编辑软件(比如 Emacs)都用这样的文件名保存副本。此外,你可能还需要忽略 logtmp 或者 pid 目录,以及自动生成的文档等等。要养成一开始就为你的新仓库设置好 .gitignore 文件的习惯,以免将来误提交这类无用的文件。

文件 .gitignore 的格式规范如下:

  • 所有空行或者以 # 开头的行都会被 Git 忽略。
  • 可以使用标准的 glob 模式匹配,它会递归地应用在整个工作区中。
  • 匹配模式可以以(/)开头防止递归。
  • 匹配模式可以以(/)结尾指定目录。
  • 要忽略指定模式以外的文件或目录,可以在模式前加上叹号(!)取反。

所谓的 glob 模式是指 shell 所使用的简化了的正则表达式。星号(*)匹配零个或多个任意字符;[abc] 匹配任何一个列在方括号中的字符 (这个例子要么匹配一个 a,要么匹配一个 b,要么匹配一个 c); 问号(?)只匹配一个任意字符;如果在方括号中使用短划线分隔两个字符,表示所有在这两个字符范围内的都可以匹配(比如 [0-9] 表示匹配所有 0 到 9 的数字)。使用两个星号(**)表示匹配任意中间目录,比如 a/**/z 可以匹配 a/za/b/za/b/c/z 等。

我们再看一个 .gitignore 文件的例子:

# 忽略所有的 .a 文件
*.a

# 但跟踪所有的 lib.a,即便你在前面忽略了 .a 文件
!lib.a

# 只忽略当前目录下的 TODO 文件,而不忽略 subdir/TODO
/TODO

# 忽略任何目录下名为 build 的文件夹
build/

# 忽略 doc/notes.txt,但不忽略 doc/server/arch.txt
doc/*.txt

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

跳出状态周期循环

让我们先复习一下上面说的 Git 文件状态周期循环。

              +----------------------+
              |                      |
          +---v----+           +-----+-----+
    +-----> Staged +-----+     | Untracked |
    |     +--------+     |     +-----^-----+
    |                    |           |
    |                    |           |
+---+------+      +------v-----+     |
| Modified <------+ Unmodified +-----+
+----------+      +------------+

“不正常”的状态周期

在上面我也提到了,这张图是“正常的”状态周期。那么,有没有“不正常”的状态周期呢?

实际上我们已经见识过了,就是 git rm 命令的两个选项:--cached-f。前者可以把已暂存的文件变成其他状态,后者更是可以强行把任何文件变成未跟踪状态并且从硬盘上删除。这些操作对应的箭头,都是在正常的状态周期中没有出现的。

下面这个表格展示了在文件在不同状态之间的转换:

从「未跟踪」从「未修改」从「已修改」从「已暂存」
到「未跟踪」-git rmgit rm -fgit rm --cached
到「未修改」--git restoregit commit git checkout
到「已修改」-edit file-git restore --staged
到「已暂存」git add-git add-

表格中 - 表示无需转换或不能转换。斜体标出的是所谓的“不正常”状态周期操作。

不正常 vs 不允许

其实,所谓的“不正常”是我自造的一个说法。这些“不正常”的操作,并非不允许这么做,实际上它们完全是被允许的,不然 Git 就不会提供干这些事情的命令了。

我把它们叫做“不正常”,是因为在某些情况下,它们是不安全的,也就是说,有可能造成不可逆的后果。在敲下这些命令之前,请再三确认,你确实知道你在做什么,并且对于这些命令的结果有清晰的预期。如果因为这些命令搞乱了你的仓库,你需要为自己负责。

关于「未跟踪」

什么是「未跟踪」?

所谓「未跟踪」,就是字面意义上的未跟踪,Git 对这个文件的状态一无所知,只知道这是一个「未跟踪」的文件。当一个文件第一次被建立,在 Git 中还没有历史记录的时候,它就是「未跟踪」的。

听起来好像没什么,只是翻来覆去说同一件事而已。但实际上,Git 很多出人意料的行为,都和这个「未跟踪」有关。

怪异之一:我在 stash 什么?

git stash 是一个很实用的命令。当你在一个分支有未完成的工作,这时想要切换到另一个分支,但不想草率地把做到一半的工作给提交,就可以用 git stash 临时贮藏起来,等到另一个分支上的工作完成之后,再 git stash pop 释放出未完成的部分。

现在问题来了,当前工作区的状态是这样的:

 M a.txt
A  b.txt
M  c.txt
?? d.txt

请问:git stash 之后,当前工作区的状态是什么样子?

答案是:a.txt b.txt c.txt 都被成功贮藏,并且恢复到了最后一次提交的状态。而 d.txt 没有被贮藏,依然是「未跟踪」。

很出人意料。明明在 d.txt 里也有未完成的工作,为什么就不能贮藏呢?

原因就是 d.txt 是一个「未跟踪」的文件,Git 不知道关于它的细节,所以不会贮藏它。

(实际上,我在写作这一节的时候就被这个现象坑到了。如果不是我意识到似乎有什么不对,提前复制了文件的内容,大家可能就见不到这一节了。)

怪异之二:我在 checkout 什么?

众所周知,在工作目录里有修改过但未提交的文件时,是不能切换到别的分支的。

但是如果工作目录里除去未修改的文件,只有未跟踪的文件,这种情况下是可以切换到别的分支的。并且切换过去之后,这些未跟踪的文件不会发生什么变化,依然是未跟踪文件。

如果这个时候你不小心用了 git add .,那么恭喜你,你成功把一个不相干的文件放进了暂存区。

你可能会认为是 Git 不知道未跟踪文件的存在,所以无视了它。但是,如果在切换到别的分支时,有另一个分支的文件会把这个未跟踪文件覆盖掉,那么 Git 会坚决阻止这次切换。那么,Git 到底是以怎样的态度对待未跟踪文件的?为什么一会无视,一会又在保护它呢?

Git 在任何情况下不会隐式地修改未跟踪的文件

其实,这些怪异之处,都能用一条逻辑来概括:Git 在任何情况下不会隐式地修改未跟踪的文件。

stash 的例子中,如果 Git 把未跟踪文件贮藏了,那么意味着 Git 需要删除一个未跟踪的文件。这是不被允许的,因为 git stash 不具有删除的语义。

checkout 的例子中,如果 Git 允许切换分支时彻底无视未跟踪文件,那么其他分支的文件就有可能把未跟踪文件覆盖掉,这也是不被允许的。

至于为什么在工作目录里有未跟踪文件时还允许签出,我没有想到一个很完美的解释。一种可能是这种行为没有违背不修改未跟踪文件的规则,另一种可能是为了允许先新增文件,再新建分支。

当然,Git 从来不会彻底阻止你干什么事情的。显式地修改未跟踪地文件是允许的。比如 git clean,它的意思是删掉所有未跟踪的文件。Git 知道当你打下这句命令时,你确实清楚你要对未跟踪的文件进行操作,所以 Git 不会阻止你。

Git 处处都在保证你的文件的安全性,但也不会阻止你意图明确地做一些不安全的事情。这是 Git 的哲学中最耐人寻味之处,只有真正的相互理解,才能托付彼此的真心。

提交更改

本节部分内容参考自 Pro Gitgit-recipes,在原文基础上有一定修改。

git commit

git commit 命令将缓存的快照提交到项目历史。提交的快照可以认为是项目安全的版本,Git 永远不会改变它们,除非你这么要求。和 git add 一样,这是最重要的 Git 命令之一。

最常用的方式是 git commit -m "<message>",这会将暂存区的内容提交到历史,并且附加一个提交信息。

在文本编辑器中输入提交信息

如果不指定 -m 参数,Git 会打开你的文本编辑器,等待你输入提交信息。这在想要输入多行的提交信息时非常有用。

Git 会使用什么文本编辑器?在 Linux 上,默认会按照 $EDITOR 环境变量的指定,一般是 GNU nano 或者 vim;而在 Windows 上,是在安装 Git for Windows 时设置的,一般是 VSCode。如果想要更改 Git 使用的文本编辑器,可以使用 git config --global core.editor 设置。

编辑器会显示类似下面的文本信息(本例选用 Vim 的屏显方式展示):


# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# Your branch is up-to-date with 'origin/master'.
#
# Changes to be committed:
#	new file:   README
#	modified:   CONTRIBUTING.md
#
~
~
~
".git/COMMIT_EDITMSG" 9L, 283C

可以看到,默认的提交消息包含最后一次运行 git status 的输出,放在注释行里,另外开头还有一个空行,供你输入提交说明。你完全可以去掉这些注释行,不过留着也没关系,多少能帮你回想起这次更新的内容有哪些。

更详细的内容修改提示可以用 -v 选项查看,这会将你所作的更改的 diff 输出呈现在编辑器中,以便让你知道本次提交具体作出哪些修改。

退出编辑器时,Git 会丢弃注释行,用你输入的提交说明生成一次提交。

提交前暂存

默认情况下,git commit 只会提交暂存区中的文件,那些更改过但未暂存的文件不会被提交。

如果想要在提交之前自动暂存,可以指定 -a 参数,这会提交已暂存的文件和已更改的文件,并且文件的更改就算没有暂存也包括在内。注意,与 git add . 不同,未跟踪的文件不会被提交

重视每一次提交

Git 初学者最容易犯的一个问题就是随意地使用 git commit 提交,甚至把提交当成随时可用的存档点,把很多做到一半的工作提交进去。

好吧,对于初学者来说,也无可厚非。多用一点 Git 命令,熟悉一下也是好的。毕竟每个人都有起步的过程嘛。

但是,这是一篇「重学指南」,当你再一次学习 git commit 时,你就必须开始养成重视提交的习惯了

重视提交的内容

首先要重视提交的内容。最好的情况下,每一次提交都包含一次完整的功能变更,没有任何尚未完成的搁置的工作。你不应该把写到一半的功能提交进储存库。

当然,这只是理想的情况。实际肯定不会总是这么顺利。还有一些不那么好的情况,也是允许的。

  • 上上策:提交中不含未完成的工作,并且仅包含一个功能变动。
  • 上策:提交中不含未完成的工作。
  • 中策:本地的提交可以包含未完成的工作,但不能把这样的提交推送到远程。
  • 下策:不完整的提交被推送上去,现在所有人都看到你做了多么糟糕的事情。

这几种情况中,除去下策,其他都是在一个科学管理的项目中允许发生的。

如果不得不……

有的时候,出于某些特殊的目的,我们不得不提交一些不完整的代码。但是在这么做之前,请想一想,有没有别的更好的方式来让我完成这件事?

  • 我想临时切换到别的分支:请优先使用 git stash,它就是为这一需求设计的。如果你需要在另一个分支上长期工作,也可以考虑用 git worktree 建立一个新的工作树,这样两个分支的内容就不会相互干扰了。
  • 我需要在别的机器上继续这项工作:的确是棘手的情况。这种时候,允许你偷偷地把未完成的工作提交上去,在提交消息里警告别人不要在这个节点签出。工作完成后,用下面所说的修改提交或者 git rebase -i(将在后面的文章里介绍)覆盖掉之前未完成的提交。
  • 我不小心在提交里漏了一个文件:这种事情在所难免。下文的修改提交会对你有帮助。

重视提交消息

其次要重视提交消息。最好的情况下,提交消息要符合一定的格式规范,并且清晰描述了这次提交做出的变更

为什么格式规范那么重要?第一个原因是能够让别人快速理解你的项目历史,第二个原因是这有助于使用自动化工具。

提交的格式规范有很多种,没有哪个比哪个更好。常见的提交格式规范有「约定式提交」和「gitmoji」等等,关于它们的详细信息可以看附录中的内容。

修改提交

上面我们提到了一种情况:在本地的提交中可以包含未完成的工作,但这样的提交不能推送到远程。

你可能会疑惑:git push 的时候,所有的提交历史都会推送,那么怎么保证那些不完整的提交不被推送呢?

很简单。因为保存未完成工作的提交永远是最后一个提交,所以只要我们用新的提交替换掉它就可以了。

完成这一操作的命令,是 git commit --amend

“amend”是修正的意思。用这条命令产生的提交,会替换掉上一次的提交。

比如说,在提交之前,提交的历史记录是这样的:

* b9d4ede :construction: [WIP] Some work in progress
* 78e1527 :zap: Improve UI performance
* 34a1e52 :sparkles: Connect to the database
* f6a09fe :bug: Fix stats not updating

然后产生一条新的 amend 提交:

git commit --amend -m ":bug: Fix layout issues"

新的历史记录会是这样:

* d5b5ad3 :bug: Fix layout issues
* 78e1527 :zap: Improve UI performance
* 34a1e52 :sparkles: Connect to the database
* f6a09fe :bug: Fix stats not updating

可以看到,原本的包含未完成工作的提交,被新的提交替换掉了。现在所有的提交都是完整又可爱的。

需要注意的是,提交的哈希值在 git commit --amend 之后发生了变化。这意味着如果那条被替换的提交已经推送到远程,你需要用 git push --force 来强制覆盖它。

查看历史

本节部分内容参考自 Pro Gitgit-recipes,在原文基础上有一定修改。

git log

在提交了若干更新,又或者克隆了某个项目之后,你也许想回顾下提交历史。完成这个任务最简单而又有效的工具是 git log 命令。

下面是一个典型的 git log 输出:

$ git log
commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date:   Mon Mar 17 21:52:11 2008 -0700

    changed the version number

commit 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
Author: Scott Chacon <schacon@gee-mail.com>
Date:   Sat Mar 15 16:40:33 2008 -0700

    removed unnecessary test

commit a11bef06a3f659402fe7563abf99ad00de2209e6
Author: Scott Chacon <schacon@gee-mail.com>
Date:   Sat Mar 15 10:31:28 2008 -0700

    first commit

不传入任何参数的默认情况下,git log 会按时间先后顺序列出所有的提交,最近的更新排在最上面。正如你所看到的,这个命令会列出每个提交的 SHA-1 校验和、作者的名字和电子邮件地址、提交时间以及提交说明。

有的时候,在 Windows 上使用 git log 时,会在输出一段信息后卡住。这是因为 git log 在输出过长时,会调用分页器来实现分屏输出,但 Git for Windows 自带的分页器有一定的不兼容。这种情况下,可以使用 git config --global core.pager cat 来禁用分页功能。

筛选历史

git log -n <limit>

显示前 <limit> 个提交。比如 git log -n 3 只会显示 3 个提交。-n 3 也可以写成 -3

git log --author="<pattern>"

搜索特定作者的提交。<pattern> 可以是字符串或正则表达式。

git log --grep="<pattern>"

搜索特定提交信息的提交。<pattern> 可以是字符串或正则表达式。

git log <since>..<until>

只显示发生在 <since><until> 之间的提交。两个参数可以是提交 ID、分支名、HEAD 或是任何一种引用。

git log <file>

只显示包含特定文件的提交。查找特定文件的历史这样做会很方便。

git log --since=<since>
git log --until=<until>

仅显示指定时间之后或之前的提交。<since><until> 可以为多种时间格式,可以是类似 "2008-01-15" 的具体的某一天,也可以是类似 "2 years 1 day 3 minutes ago" 的相对日期。

git log -S <name>

仅显示添加或删除特定字符串的提交。比如,搜索代码中添加或删除了对某一个特定函数的引用的提交。

显示更多信息

-p--patch 选项会显示每次提交所引入的差异(按补丁的格式输出)。

$ git log -p -1
commit ca82a6dff817ec66f44342007202690a93763949
Author: Scott Chacon <schacon@gee-mail.com>
Date:   Mon Mar 17 21:52:11 2008 -0700

    changed the version number

diff --git a/Rakefile b/Rakefile
index a874b73..8f94139 100644
--- a/Rakefile
+++ b/Rakefile
@@ -5,7 +5,7 @@ require 'rake/gempackagetask'
 spec = Gem::Specification.new do |s|
     s.platform  =   Gem::Platform::RUBY
     s.name      =   "simplegit"
-    s.version   =   "0.1.0"
+    s.version   =   "0.1.1"
     s.author    =   "Scott Chacon"
     s.email     =   "schacon@gee-mail.com"
     s.summary   =   "A simple gem for using Git in Ruby code."

--stat 选项可以显示每次提交的简略统计信息,包括所有被修改过的文件、有多少文件被修改了以及被修改过的文件的哪些行被移除或是添加。

$ git log --stat -1
commit a11bef06a3f659402fe7563abf99ad00de2209e6
Author: Scott Chacon <schacon@gee-mail.com>
Date:   Sat Mar 15 10:31:28 2008 -0700

    first commit

 README           |  6 ++++++
 Rakefile         | 23 +++++++++++++++++++++++
 lib/simplegit.rb | 25 +++++++++++++++++++++++++
 3 files changed, 54 insertions(+)

格式化输出

--format 选项可以定制记录的显示格式。这样的输出对后期提取分析格外有用——因为你知道输出的格式不会随着 Git 的更新而发生改变:

$ git log --pretty=format:"%h - %an, %ar : %s"
ca82a6d - Scott Chacon, 6 years ago : changed the version number
085bb3b - Scott Chacon, 6 years ago : removed unnecessary test
a11bef0 - Scott Chacon, 6 years ago : first commit

下面列出了 --format 接受的常用格式占位符的写法及其代表的意义。

占位符说明
%H提交的完整哈希值
%h提交的短哈希值
%T树的完整哈希值
%t树的短哈希值
%P父提交的完整哈希值
%p父提交的简写哈希值
%an作者名字
%ae作者的电子邮件地址
%ad作者修订日期(可以用 --date=选项 来定制格式)
%ar作者修订日期,按多久以前的方式显示
%cn提交者的名字
%ce提交者的电子邮件地址
%cd提交日期
%cr提交日期(距今多长时间)
%s提交说明

你一定奇怪作者和提交者之间究竟有何差别,其实作者指的是实际作出修改的人,提交者指的是最后将此工作成果提交到仓库的人。所以,当你为某个项目发布补丁,然后某个核心成员将你的补丁并入项目时,你就是作者,而那个核心成员就是提交者。关于其中具体的区别,我们会在以后的章节中详细讨论。

format 与另一个 log 选项 --graph 结合使用时尤其有用。 这个选项添加了一些 ASCII 字符串来形象地展示你的分支、合并历史:

$ git log --pretty=format:"%h %s" --graph
* 2d3acf9 ignore errors from SIGCHLD on trap
*  5e3ee11 Merge branch 'master' of git://github.com/dustin/grit
|\
| * 420eac9 Added a method for getting the current branch.
* | 30e367c timeout code and tests
* | 5a09431 add timeout protection to grit
* | e1193f8 support for heads with slashes in them
|/
* d6016bc require time for xmlschema
*  11d191e Merge branch 'defunkt' into local

下面是我很喜欢的一种 log 格式,在纵览项目历史时很有用:

git log --no-merges --color --graph --date=format:'%Y-%m-%d %H:%M:%S' --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cd) %C(bold blue)<%an>%Creset' --abbrev-commit

可以为这一长串命令起一个别名:

git config --global alias.ls "log --no-merges --color --graph --date=format:'%Y-%m-%d %H:%M:%S' --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cd) %C(bold blue)<%an>%Creset' --abbrev-commit"

之后,就可以用 git ls 来快速调用这个命令。

$ git ls -10
* e7a595e - (HEAD -> 查看历史) :adhesive_bandage: 更正关于 Linux 上的文本编辑器的描述 (2022-06-17 23:19:29) <忘忧北萱草>
* 0089bc1 - :sparkles: 更新关于 GPG 出错的解决方案 (2022-06-17 23:16:33) <忘忧北萱草>
* 23b7ae1 - :sparkles: 提交更改 (2022-06-17 18:20:09) <忘忧北萱草>
* 4fb486c - :sparkles: 本项目的 gitmoji 规范 (2022-06-17 16:08:40) <忘忧北萱草>
* 35ecf52 - :adhesive_bandage: 修正关于储存库中文件储存方式的描述 (2022-06-17 15:57:25) <忘忧北萱草>
* b571156 - :zap: 添加 giscus 支持,以及修复导航按钮问题 (2022-06-17 01:59:31) <忘忧北萱草>
* 4e84d9d - :sparkles: 管理暂存区 (2022-06-16 23:05:09) <忘忧北萱草>
* ad3b4fc - :pencil2: 规范标点的使用 (2022-06-16 19:13:06) <忘忧北萱草>
* 64273e0 - :memo: 增加 badges 和链接 (2022-06-16 17:28:53) <忘忧北萱草>
* 30cae30 - :lipstick: 优化点击跳转的平滑滚动的方式和效果 (2022-06-16 16:38:37) <忘忧北萱草>

在钩子中使用 git log

后来我发现在 pre-push 钩子里使用交互式命令会破坏一些自动化工具的使用,所以是否使用下面的钩子,还是要看你自己的情况。

上一节我们说过,必须重视每一次提交,保证不完整的提交记录不会被推送到远程。

但是,Git 并非一个所见即所得的系统,有时候你做出了一些更改,并不能立刻想到结果是到底是什么样的。这时 git log 会是很好的帮助,它可以帮你检查你做出的更改是否有问题。只要更改还留在本地,你永远有修正的机会。

不过,假如在提推送前你忘记了用 git log 查看一下,就有可能发生一些不好的事情。所以,我们需要一个东西,来提醒我们在每次推送之前 git log 一下。这种东西就是 Git 钩子

关于钩子的详细说明,将会在以后的章节涉及。这里就简单举一个使用钩子的实例。

储存库所使用的钩子存放在 .git/hooks 目录中。将其中 pre-push.sample 文件改名为 pre-push,去掉 .sample 来启用它。这个钩子会在每次推送前被调用。

pre-push 的内容修改为:

#!/bin/bash

remote="$1"
url="$2"

echo "You are pushing to $remote($url), please check the commits history: "

git log --no-merges --color --graph --date=format:'%Y-%m-%d %H:%M:%S' --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cd) %C(bold blue)<%an>%Creset' --abbrev-commit -10

echo ""
read -r -p "Type 'y' to continue or 'n' to abort: " confirm < /dev/tty
if [[ $confirm =~ ^[Yy]$ ]]; then
    exit 0
else
    echo "Abort."
    exit 1
fi

这样,每次推送前,都会自动显示最近10条提交记录,并询问是否继续,这就给了我们反悔的机会。

$ git push
You are pushing to origin(git@github.com:Wybxc/git-remake-guide.git), please check the commits history:
* e7a595e - (HEAD -> master) :adhesive_bandage: 更正关于 Linux 上的文本编辑器的描述 (2022-06-17 23:19:29) <忘忧北萱草>
* 0089bc1 - :sparkles: 更新关于 GPG 出错的解决方案 (2022-06-17 23:16:33) <忘忧北萱草>
* 23b7ae1 - :sparkles: 提交更改 (2022-06-17 18:20:09) <忘忧北萱草>
* 4fb486c - :sparkles: 本项目的 gitmoji 规范 (2022-06-17 16:08:40) <忘忧北萱草>
* 35ecf52 - :adhesive_bandage: 修正关于储存库中文件储存方式的描述 (2022-06-17 15:57:25) <忘忧北萱草>
* b571156 - :zap: 添加 giscus 支持,以及修复导航按钮问题 (2022-06-17 01:59:31) <忘忧北萱草>
* 4e84d9d - :sparkles: 管理暂存区 (2022-06-16 23:05:09) <忘忧北萱草>
* ad3b4fc - :pencil2: 规范标点的使用 (2022-06-16 19:13:06) <忘忧北萱草>
* 64273e0 - :memo: 增加 badges 和链接 (2022-06-16 17:28:53) <忘忧北萱草>
* 30cae30 - :lipstick: 优化点击跳转的平滑滚动的方式和效果 (2022-06-16 16:38:37) <忘忧北萱草>
Type 'y' to continue or 'n' to abort: y
Enumerating objects: 3, done.
Counting objects: 100% (3/3), done.
Writing objects: 100% (3/3), 882 bytes | 882.00 KiB/s, done.
Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
To git@github.com:Wybxc/git-remake-guide.git
 * [new branch]      master -> master
branch 'master' set up to track 'origin/master'.

如果你觉得这个钩子很好,并且想要给其他的仓库也启用,你可以修改 Git 创建新储存库所用的模板仓库,它的目录在 /usr/share/git-core/templates 或者 <Git for Windows 目录>/mingw64/share/git-core/templates。这会影响之后新建或者新克隆的储存库,而之前已经建立的储存库是不受影响的。

撤销操作

本节部分内容参考自 Pro Git 的《撤销操作》《重置揭秘》,在原文基础上有一定修改。

git restore

取消暂存

有的时候,我们需要取消一些文件的暂存状态。例如,你已经修改了两个文件,并且想要将它们作为两次独立的修改提交,但是却意外地输入 git add * 暂存了它们两个。如何只取消暂存两个中的一个呢?git status 命令提示了你:

$ git add *
$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)

    renamed:    README.md -> README
    modified:   CONTRIBUTING.md

在 “Changes to be committed” 文字正下方,提示使用 git restore --staged <file>... 来取消暂存。所以,我们可以这样来取消暂存 CONTRIBUTING.md 文件:

$ git restore --staged CONTRIBUTING.md
$ git status
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)

    renamed:    README.md -> README

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)

    modified:   CONTRIBUTING.md

正如你看到的一样,CONTRIBUTING.md 文件已经是修改未暂存的状态了。

git restore 是在 Git 2.23.0 中引入的新命令。在以前的 Git 版本中,git status 给出的提示命令是 git reset HEAD <file>...。这条命令和上面的命令的效果和用法都是一样的,只不过它更多涉及到一些底层的复杂操作,而 git restore 对这些操作给出了一个易于理解的上层抽象(详见下文)。

撤消对文件的修改

如果你并不想保留对 CONTRIBUTING.md 文件的修改怎么办?你该如何方便地撤消修改——将它还原成上次提交时的样子(或者刚克隆完的样子,或者刚把它放入工作目录时的样子)?幸运的是,git status 也告诉了你应该如何做。在最后一个例子中,未暂存区域是这样:

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)

    modified:   CONTRIBUTING.md

它非常清楚地告诉了你如何撤消之前所做的修改。让我们来按照提示执行:

$ git restore CONTRIBUTING.md
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    renamed:    README.md -> README

可以看到那些修改已经被撤消了。

请务必记得 git restore <file> 是一个危险的命令。你对那个文件在本地的任何修改都会消失——Git 会用最近提交的版本覆盖掉它。除非你确实清楚不想要对那个文件的本地修改了,否则请不要使用这个命令。

记住,在 Git 中任何已提交的东西几乎总是可以恢复的。甚至那些被删除的分支中的提交或使用 --amend 选项覆盖的提交也可以恢复。然而,任何你未提交的东西丢失后很可能再也找不到了。

同样,在 Git 的旧版本中,完成这一任务的命令是 git checkout -- <file>...。用法和效果和上面也是一样的。

详细用法

点击展开
git restore <file>...

将文件恢复为暂存区的状态。这会覆盖文件当前的内容。

git restore --staged <file>...

将暂存区的文件恢复为上一次提交的状态,不会影响工作树中的文件。

git restore --staged --worktree <file>...

将暂存区和工作树的文件恢复为上一次提交的状态。

git restore -s <tree> <file>...

将文件恢复到 <tree> 所对应的快照的状态。

撤销的原理

快照

理解 git restore 的最简方法,就是以 Git 的思维框架(将其作为内容管理器)来管理三棵不同的「树」。

用途
HEAD上一次提交的快照
Index(索引/暂存区)预期的下一次提交的快照
工作目录沙盒

这里涉及到一个 Git 的概念:快照。快照是对 Git 中工作目录的状态的一次完整记录。Git 所管理的版本历史便是由一个个快照组成的。

必须要澄清一个误区:虽然 Git 似乎无时无刻不在“比较”两个文件的版本,但在 Git 中,保存的并非是文件的差异,而是每一次都保存文件的完整内容。事实上,我们在 Git 中看到的各种比较结果,都真的是在比较两个文件,文件的差异是临时算出来的

你可能会疑惑:如果每一次提交都保存了整个工作区的内容,那么储存库不是会飞速地膨胀吗?其实不用担心。对于两次提交中没有改变的文件,Git 在储存库中只会保存一份内容。所以 Git 的储存方式还是比较精简的。

顺带一提,Git 的这种保存策略,无意中鼓励我们把大文件拆分为多个小文件。大多数情况下,能够进行这种拆分,是程序模块化设计良好的表象。

三棵树

HEAD 是当前分支引用的指针,它总是指向该分支上的最后一次提交。这表示 HEAD 将是下一次提交的父结点。通常,理解 HEAD 的最简方式,就是将它看做该分支上的最后一次提交的快照

其实,查看快照的样子很容易。下例就显示了 HEAD 快照实际的目录列表,以及其中每个文件的 SHA-1 校验和:

$ git cat-file -p HEAD
tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf
author Scott Chacon  1301511835 -0700
committer Scott Chacon  1301511835 -0700

initial commit

$ git ls-tree -r HEAD
100644 blob a906cb2a4a904a152...   README
100644 blob 8f94139338f9404f2...   Rakefile
040000 tree 99f1a6d12cb4b6f19...   lib

Git 的 cat-filels-tree 是底层命令,它们一般用于底层工作,在日常工作中并不使用。不过它们能帮助我们了解到底发生了什么。

索引(暂存区)

索引是你的预期的下一次提交。我们也会将这个概念引用为 Git 的「暂存区」,这就是当你运行 git commit 时 Git 看起来的样子。

Git 将上一次检出到工作目录中的所有文件填充到索引区,它们看起来就像最初被检出时的样子。之后你会将其中一些文件替换为新版本,接着通过 git commit 将它们转换为树来用作新的提交。

$ git ls-files -s
100644 a906cb2a4a904a152e80877d4088654daad0c859 0	README
100644 8f94139338f9404f26296befa88755fc2598c289 0	Rakefile
100644 47c6340d6459e05787f644c2447d2595f5d3a54b 0	lib/simplegit.rb

再说一次,我们在这里又用到了 git ls-files 这个幕后的命令,它会显示出索引当前的样子。

确切来说,索引在技术上并非树结构,它其实是以扁平的清单实现的。不过对我们而言,把它当做树就够了。

工作目录

最后,你就有了自己的工作目录(通常也叫工作区工作树)。另外两棵树以一种高效但并不直观的方式,将它们的内容存储在 .git 文件夹中。工作目录会将它们解包为实际的文件以便编辑。你可以把工作目录当做一个沙盒。在你将修改提交到暂存区并记录到历史之前,可以随意更改。

$ tree
.
├── README
├── Rakefile
└── lib
    └── simplegit.rb

1 directory, 3 files

git restore 的工作

git restore 是一条很新的命令。在它诞生之前,完成对应的工作的命令是 git resetgit checkout。这两个命令的功能很强大,同时原理也很复杂,需要到下一章再详细讲解。git restore 从它们的功能中抽取了一个子集,并建立了一种易于理解的接口。

git restore 所做的工作只有一个:把文件从一棵树复制到另一棵树。比如说,当我们执行 git restore <file>... 时,是把文件从索引树,也就是暂存区,复制到工作目录中。当我们执行 git restore --staged <file>... 时,是把文件从 HEAD 指向的树复制到暂存区中。因为复制的来源总是比目标要旧,表现出来的就是撤销了对文件的修改。

git restore--staged--worktree 参数分别指定了复制的目标是索引树和工作目录。复制的来源一般是索引树,而如果指定了 --staged,则以 HEAD 指向的树为来源。用 -s <tree> 或者 --source=<tree> 可以自行指定来源。

下表展示了 git restore 在不同参数下的复制来源与目标的设定。

默认-s <tree>
默认暂存区 -> 工作树tree -> 工作树
--stagedHEAD -> 暂存区tree -> 暂存区
--worktree暂存区 -> 工作树tree -> 工作树
--staged --worktreeHEAD -> 工作树&暂存区tree -> 工作树&暂存区

远程和分支

如果说储存库是 Git 的基础,那么分支则是 Git 的精华。

分支最容易理解的意义,是表示两个平行存在的版本,它们在历史的某一点发生分歧,走向不同,也可以在未来的某一点重新合并。Git 的分支非常轻量,因为 Git 鼓励用户通过大量地创建和合并分支,来完成协作工作。

远程对大多数人来说也许更加熟悉,像 GitHub、GitLab、Bitbucket 这些,都是远程储存库的托管网站。远程是一个或多个非本机的储存库。Git 是一个分布式的协作系统,这意味着各个储存库在地位上是等同的。储存库间的协作,围绕着远程储存库的同步展开。

Git 对远程的管理方式根植于它的分支模型。因此,我们把远程和分支放在同一章节讲解。

分支基础

本节部分内容参考自 Pro Git,在原文基础上有一定修改。

什么是分支?

分支代表了一条独立的开发流水线,它从项目历史中分叉(fork)出去,当新的提交被存放在当前分支的历史后。就与原本的历史产生了区别。分支是平行进展的,当一个新的分支创建之后,如果不执行合并操作,那么它不会影响原本的分支上的工作。

这些是关于分支在上层的概念。至于 Git 管理分支的方式,要先理解 Git 管理项目历史的模型。

Git 指针

Git 使用一种类似于反向链表的结构来组织不同版本的快照。每一个节点表示一次提交,其中包含了一个版本快照,加上一条提交消息,以及一个指向上一个版本的指针。

比如,我们这样子创建一个有三个版本的储存库:

git init
echo "Sample" > README
git add README
git commit -m "ver 1.0"

echo "2.0" > a.txt
git add a.txt
git commit -m "ver 2.0"

echo "3.0" >> a.txt
git add a.txt
git commit -m "ver 3.0"

那么,现在它的提交历史是这样的:

graph RL
A[commit<br/>5a52af6<hr/>ver 1.0<hr/>blob README]
B[commit<br/>ea71414<hr/>ver 2.0<hr/>blob README<br/>blob a.txt]
C[commit<br/>eec6df0<hr/>ver 3.0<hr/>blob README<br/>blob a.txt]
C-->B-->A

不要对「指针」这个说法感到迷惑。很多关于 Git 的书籍都采用这个说法,因为 Git 的工作原理真的非常像内存分配器。关于工作原理的仔细讨论,我们留到后面文章,这里只需要知道:在 Git 中,有一些「Git 指针」,它们可以指向 Git 中保存的数据,比如一次提交,或者一个文件

指针和分支

有一个指针我们已经见过了。它叫做 HEAD,一般指向当前分支的最后一次提交。所以,上面那个图的完整版本应该是这样的:

graph RL
A[commit<br/>5a52af6<hr/>ver 1.0<hr/>blob README]
B[commit<br/>ea71414<hr/>ver 2.0<hr/>blob README<br/>blob a.txt]
C[commit<br/>eec6df0<hr/>ver 3.0<hr/>blob README<br/>blob a.txt]
M(main):::pnode-->C
HEAD([HEAD]):::ppnode-->M
C-->B-->A

这里假设当前分支的名称叫做 main

从图里可以看出来,分支实际上就是指向一次提交的指针。而 HEAD 指针,在此时指向的是一个分支,也就是说,它是一个二重指针

二重指针听起来挺唬人,但在 Git 里几重指针并没有那么重要。Git 在访问时,不论几重指针,都会反复解引用找到它实际指向的东西。HEAD 指针也不一定是二重指针,有的时候它可以直接指向一个具体的提交,至于这种情况,我们会在后面的文章中讨论。

分叉(fork)

这里所说的 fork 和 GitHub 的 fork 还有点不同。这里的分叉,指的是 Git 提交历史的分叉,也就是平行分支的诞生。

Git 采用反向的链表而非正向链表来记录历史,就是为了应对分叉的情形。毕竟一次提交只能基于一个历史,却可以有无数种未来发展的可能。

当分叉发生之后,历史记录会变成类似这样:

graph RL
A[commit<br/>5a52af6<hr/>ver 1.0<hr/>blob README]
B[commit<br/>ea71414<hr/>ver 2.0<hr/>blob README<br/>blob a.txt]
C[commit<br/>eec6df0<hr/>ver 3.0<hr/>blob README<br/>blob a.txt]
D[commit<br/>eec6df0<hr/>ver 2.1<hr/>blob README<br/>blob a.txt<br/>hotfix.patch]
M(main):::pnode-->C
BR(dev):::pnode-->D
HEAD([HEAD]):::ppnode-->BR
C-->B-->A
D-->B

ver 2.0 之后,发生了分叉,出现了一个新的分支 dev,这个分支和原本的 main 分支是平行进展的。HEAD 指针指向 dev,表示当前正在 dev 分支上工作。

切换分支

在切换分支时,发生了两件事情:HEAD 指针指向新的分支,随后工作树的内容会变成新切换的分支指向的快照

分叉之后,我们切换到原本的 main 分支:

graph RL
A[commit<br/>5a52af6<hr/>ver 1.0<hr/>blob README]
B[commit<br/>ea71414<hr/>ver 2.0<hr/>blob README<br/>blob a.txt]
C[commit<br/>eec6df0<hr/>ver 3.0<hr/>blob README<br/>blob a.txt]
D[commit<br/>7cd1352<hr/>ver 2.1<hr/>blob README<br/>blob a.txt<br/>hotfix.patch]
M(main):::pnode-->C
BR(dev):::pnode-->D
HEAD([HEAD]):::ppnode-->M
C-->B-->A
D-->B

看起来几乎没有什么变化。这就是 Git 分支轻量快速的原因。

删除分支

删除分支,真的只是删除“分支”本身。Git 只会删掉分支的指针,别的都不会变。

比如说,我们删掉 dev 分支:

graph RL
A[commit<br/>5a52af6<hr/>ver 1.0<hr/>blob README]
B[commit<br/>ea71414<hr/>ver 2.0<hr/>blob README<br/>blob a.txt]
C[commit<br/>eec6df0<hr/>ver 3.0<hr/>blob README<br/>blob a.txt]
D[commit<br/>7cd1352<hr/>ver 2.1<hr/>blob README<br/>blob a.txt<br/>hotfix.patch]
M(main):::pnode-->C
HEAD([HEAD]):::ppnode-->M
C-->B-->A
D-->B

现在指向 7cd1352 这次提交的指针消失了。但是等等,这次提交还保存着一个快照,不是在浪费我们的空间吗?

没错。所以 Git 会提醒你,dev 分支的工作没有完全并入主分支,这时删除需要谨慎。你可以用命令来强制删除 dev 分支,那么 7cd1352 这次提交就无法被任何分支访问了。

这种不可及的快照,会被 Git 的垃圾回收机制删除。这是 Git 工作起来确实像一个内存分配器的另一个表现。关于垃圾回收机制,我们会在后续的文章展开讲解。

创建和切换分支

本节部分内容参考自 git-recipes 的《检出以前的提交》《使用分支》,在原文基础上有一定修改。

git switch

git switch 命令用于切换和创建分支。

切换分支

在一条分支上的工作完成后,要到其他分支进行别的工作,此时需要切换分支。切换分支的命令为:

git switch <branch-name>

其中,<branch-name> 为要切换到的分支的名称。

切换分支前,需要先检查当前分支中已跟踪的文件是否有未暂存的修改,如果有,则需要将修改暂存或临时储藏。未跟踪或已暂存的修改不会影响切换分支,除非切换时会导致这些修改被覆盖。

在不冲突的情况下,已暂存的修改在分支切换后保持不变。这允许先作出修改,然后切换分支,已经做出的修改会被带到新的分支。这一特性在某些工作流(见以后章节)中会很有用。

git revert 一样,git switch 也是一条很新的命令,甚至目前为止它的行为还没有稳定。在旧版本中,切换分支的命令为 git checkout <branch-name>

创建分支

git switch <branch-name> 不能切换到一个不存在的分支,要创建一个新的分支,可以使用 -c 选项:

git switch -c <branch-name>

<branch-name> 为新分支的名称。这条命令会在创建新分支的同时切换过去。

-c 选项只用于创建新分支。如果 <branch-name> 已存在,则会报错。

新创建的分支将会和当前的 HEAD 有相同的内容。从底层看,其原理就是新建一个指向 HEAD 指向的内容的分支指针,然后将 HEAD 指向它。

同样,新创建分支会保留暂存区中的修改。

在旧版本中,创建并切换分支的命令为 git checkout -b <branch-name>

从某一点创建分支

默认情形下,git switch -c 创建的分支会基于当前的 HEAD。其实,还可以指定一个额外的参数 <start-point>,指定从历史中的某一点创建分支:

git switch -c <branch-name> <start-point>

这里的 <start-point> 采用一种 Git 专用的选择提交历史的语法,大致的语法有以下几种:

  1. <branch-name> 表示某个分支指针指向的提交。
  2. <commit-id> 为某个提交的哈希值的前几位(在无歧义的情况下至少 4 位)。
  3. <tag-name> 表示某个标签所在的提交。
  4. HEAD 表示 HEAD 指向的提交。
  5. <start-point>~N 表示某个提交的第 N 级父提交。
  6. @{-N} 倒数第 N 次使用同一命令时指定的提交。可用 - 作为 @{-1} 的简写。
  7. A...B 两个提交的最近共同祖先,若省略一端,视为 HEAD

这些语法经常被用在各种需要指定一次提交的地方。不过,在不同命令里,这些语法并非都是可用的,一般前五条对大多数命令都是可用的。

graph RL
A[commit<br/>5a52af6<hr/>ver 1.0<hr/>blob README]
B[commit<br/>ea71414<hr/>ver 2.0<hr/>blob README<br/>blob a.txt]
C[commit<br/>eec6df0<hr/>ver 3.0<hr/>blob README<br/>blob a.txt]
D[commit<br/>7cd1352<hr/>ver 2.1<hr/>blob README<br/>blob a.txt<br/>hotfix.patch]
M(main):::pnode-->C
BR(dev):::pnode-->D
HEAD([HEAD]):::ppnode-->M
C-->B-->A
D-->B

以上图为例,要想指向 ea71414 这次提交,可以使用以下的方式:ea71 HEAD~1 main~1 main...dev

git branch

git branch管理分支用的命令,它允许你创建、列出、重命名和删除分支,但没有修改工作树和历史记录的功能,因此不能切换和合并分支。

详细用法

git branch

列出仓库中所有分支。

git branch <branch>

创建一个名为 <branch> 的分支。不会自动切换到那个分支去。

git branch <branch> <start-point>

<start-point> 创建分支。

git branch -d <branch>

删除指定分支。这是一个安全的操作,Git 会阻止你删除包含未合并更改的分支。

git branch -D <branch>

强制删除指定分支,即使包含未合并更改。如果你希望永远删除某条开发线的所有提交,你应该用这个命令。

git branch -m <branch>

将当前分支命名为 <branch>

签出

之前的章节,我们简单提及了「签出」的概念:从储存库中提取文件,释放到工作树中的过程,称为签出。

切换分支的过程会发生签出。在切换分支时,实际上发生了两个操作:将 HEAD 指针指向目标分支,然后签出当前的 HEAD

git checkout

git checkout 是用于进行签出的命令,它有两种功能:签出某个文件,签出以前的提交。

签出提交会使工作目录和这个提交完全匹配。你可以用它来查看项目之前的状态,而不改变当前的状态。

签出文件使你能够查看某个特定文件的旧版本,而工作目录中剩下的文件不变。

详细用法

git checkout <start-point>

签出 <start-point> 指定的提交。同上面的规则一样,<start-point> 可以是一个分支名、提交,或者表示提交的表达式。这条命令会使 HEAD 指向签出的提交,并且更新工作目录中的所有文件,使得和某个特定提交中的文件一致。

git checkout <start-point> <file>

查看文件之前的版本。它将工作目录中的 <file> 文件变成 <start-point> 中那个文件的拷贝,并将它加入缓存区。

git checkout -- <file>

从 HEAD 签出某个文件。这表现为放弃这个文件在工作目录中的修改,并将它从缓存区中删除。-- 是为了消除命令歧义,否则 Git 会把文件名识别为提交的表示。

git checkout -b <branch>

从 HEAD 创建一个新的分支,并签出它。这等同于 git switch -c <branch>

分离头部

“分离头部”是英文 Detached Head 的直译,它完整的含义是:HEAD 指针从一个分支上分离

大多数时候,HEAD 指针是一个二级指针,它指向一个分支,表示当前进行的工作所在的分支。

graph RL
A[commit<br/>5a52af6<hr/>ver 1.0<hr/>blob README]
B[commit<br/>ea71414<hr/>ver 2.0<hr/>blob README<br/>blob a.txt]
C[commit<br/>eec6df0<hr/>ver 3.0<hr/>blob README<br/>blob a.txt]
M(main):::pnode-->C
HEAD([HEAD]):::ppnode-->M
C-->B-->A

如果 HEAD 从一个分支上分离,直接指向一个提交,那么 HEAD 就从二重指针变成了一重指针:

graph RL
A[commit<br/>5a52af6<hr/>ver 1.0<hr/>blob README]
B[commit<br/>ea71414<hr/>ver 2.0<hr/>blob README<br/>blob a.txt]
C[commit<br/>eec6df0<hr/>ver 3.0<hr/>blob README<br/>blob a.txt]
M(main):::pnode-->C
HEAD(HEAD):::ppnode-->B
C-->B-->A

这种 HEAD 直接指向一个提交的情况,就叫做 Detached Head。

产生 Detached Head 的原因,是签出了一个不是某一分支最新提交的提交,比如用 git checkout <commit-id> 直接指定一个提交进行签出。

在 Detached Head 状态下,更改、暂存等对工作树的操作都是可以进行的。提交也可以进行,这会导致一段新的历史被分叉出去。比如,在上面的图中,再发起一次提交:

graph RL
A[commit<br/>5a52af6<hr/>ver 1.0<hr/>blob README]
B[commit<br/>ea71414<hr/>ver 2.0<hr/>blob README<br/>blob a.txt]
C[commit<br/>eec6df0<hr/>ver 3.0<hr/>blob README<br/>blob a.txt]
D[commit<br/>44882b6<hr/>ver 2.1<hr/>blob README<br/>blob a.txt]
M(main):::pnode-->C
HEAD(HEAD):::ppnode-->D
C-->B-->A
D-->B

新的提交产生后,HEAD 指向了新的提交,但是仍然出于 Detached Head 状态。如果此时试图签出 main 分支,Git 会提醒你,之前所在的提交记录没有任何分支指向它。

$ git checkout main
Warning: you are leaving 1 commit behind, not connected to
any of your branches:

  44882b6 ver 2.1

If you want to keep it by creating a new branch, this may be a good time
to do so with:

 git branch <new-branch-name> 44882b6

Switched to branch 'main'

按照提示,可以用 git branch 新建一个分支,保存在 Detached Head 时做出的变更。如果此时不建立分支,那么这个提交将只能通过哈希值来访问。假如忘记了哈希值,那么这里的发生的变更将会永远无法找回。(事实上,还是有 reflog 这种补救措施的。)

约定式提交

部分内容参考自约定式提交的官方网站,在原文基础上有一定修改。

什么是约定式提交?

约定式提交规范是一种基于提交信息的轻量级约定,换句话说,它规定了你应该用什么格式写提交信息。

一段符合约定式提交的 git log 看起来是这样的(来自 unocss):

* 1762ef93 - (tag: v0.33.3) release v0.33.3
* 7b3b8a97 - chore: update deps
* eccaed9d - chore: bump pnpm
* a1fac007 - feat(vite): support build watch mode (#963)
* fa1e8202 - fix(vite): emit better warning in build (#961)
* 89003436 - feat(preset-mini): add break-spaces to white-space (#962)
* b0fd9a83 - feat(vscode): support loading from iles.config file (#956)
* bcaa6eed - docs: update
* 6e6f07e5 - docs: update features
* 31a24fd1 - (tag: v0.33.2) release v0.33.2
* d2d0c497 - chore: lint
* d051b5c0 - feat(compile-class): new transformer (#950)
* 10c0ae28 - docs: fix query loading
* 59403676 - (tag: v0.33.1) release v0.33.1

为什么要使用约定式提交?

  • 自动化生成 CHANGELOG。
  • 基于提交的类型,自动决定语义化的版本变更。
  • 向同事、公众与其他利益关系者传达变化的性质。
  • 触发构建和部署流程。
  • 让人们探索一个更加结构化的提交历史,以便降低对你的项目做出贡献的难度。

约定式提交的格式是怎样的?

一般格式

对于单行提交,约定式提交的格式是这样的:

type(scope): description

对于多行提交,约定式提交的格式是这样的:

type(scope): description

body

footers

其中,type 是提交做出更改的类型,它可以为:

  • fix:表示在代码库中修复了一个 bug(这和语义化版本中的 PATCH 相对应)。
  • feat:表示在代码库中新增了一个功能(这和语义化版本中的 MINOR 相对应)。
  • fix:feat: 之外,也可以使用其它提交类型,例如 @commitlint/config-conventional(基于 Angular 约定)中推荐的 build:chore:ci:docs:style:refactor:perf:test:,等等。

scope 是此次更改涉及到的项目子模块范围。如果不能明确,这一部分可以省略。

description 是描述此次更改的简短描述。

破坏性更改

当提交的内容包含破坏性更改时,必须在描述中指出,例如:

feat!: send an email to the customer when a product is shipped
feat: allow provided config object to extend other configs

BREAKING CHANGE: `extends` key in config file is now used for extending other config files

在冒号前面加上 ! ,或者在脚注中表明 BREAKING CHANGE,就表示此次更改是破坏性更改。

约定式提交的正式规范

详见官方网站

gitmoji 速查表

下面是一些常用的 gitmoji。完整的列表可以参见 Gitmoji 的网站

提交类型Emoji代码
创世提交。🎉:tada:
引入不兼容改动。💥:boom:
性能改善。⚡️:zap:
删除代码或者文件。🔥:fire:
修了一个 BUG。🐛:bug:
重大热修复。🚑:ambulance:
修复安全问题。🔒:lock:
引入新的特性。:sparkles:
结构改进 / 格式化代码。🎨:art:
更新界面与样式文件。💄:lipstick:
更新测试。:white_check_mark:
代码重构。♻️:recycle:
写文档。📝:pencil:
发布 / 版本标签🔖:bookmark:
进行中。🚧:construction:
改变配置文件。🔧:wrench:
国际化与本地化。🌐:globe_with_meridians:
修正拼写错误。✏️:pencil2:
回滚改动。:rewind:
合并分支。🔀:twisted_rightwards_arrows:
更新编译后的文件或者包。📦:package:
添加或者更新许可。📄:page_facing_up:
添加一个彩蛋。🥚:egg:
添加或者更新 .gitignore 文件。🙈:see_no_evil:

本项目采用的 gitmoji 规范

因为本项目是一个电子书,所以一般的代码仓库的 gitmoji 规范不完全适用。下面是本仓库采用的 gitmoji 规范:

  • 🎉 创世 /remake。
  • ✨ 添加新章节。
  • 🩹 修正文章内容中的错误。
  • ♻️ 重新组织文章结构。
  • 💄 页面美化。
  • ⚡ 新的页面功能。
  • 🐛 修复页面功能的错误。
  • 📝 修改 README。
  • ✏️ 修正 README 中的错误。
  • 🚀 部署相关。
  • 🙈 .gitignore 相关。
  • 📄 LICENSE 相关。