Git笔记:背景与基础

本文参考资料为Git官网的官方文档 Git - Reference

本文内容存在时效性,请在参考本文学习Git时注意甄别;

前言

站在某种角度观察,程序都是一条 忒修斯之船 。人在一生中几乎代谢更新掉全身的细胞,但记忆作为连续性的载体,使人始终被视为同一个「个体」。程序也是如此,尽管代码在其生命周期中经历多次重构与替换,但我们仍可通过Git窥见它最初的样貌。在以往的学习与实践中,我也使用Git和远程仓库管理代码,但由于缺乏规范,提交记录常常杂乱无序,像是随手拍下的人生片段,往往未能捕捉到真正关键的时刻。借由这篇博客,我希望系统梳理自己的Git学习过程,并尝试为未来的每一个程序版本,留下更具意义的时间切面。

背景

在学习一个程序或系统时,建立对其功能和设计理念的整体认识,往往能为后续理解各个细节提供清晰的框架。如此一来,我们后续学到的每一部分,都能在脑海中找到合适的位置,自然也更容易记忆与应用。因此,在正式进入Git的具体命令之前,不妨先一同了解它背后的设计思路。

当前版本控制系统主要存在两种存储流派

  • 基于差异:这类系统为每个版本存储其与之前版本的差异,当我们想要获取某一个版本的文件源码时,只需要将其之前的变化差异累加起来就可以得到;这类系统的实例有SVN;
  • 快照流:这类系统不同于上述系统,每次记录版本时都为每个文件生成快照,并保存快照索引;特别的是,如果某一文件在版本更新时没有发生变化,这类系统便不会为其生成快照,Git就是属于这种类型;

除了存储模型上的创新,Git的另一个核心特性是其分布式架构。在Git中,开发者本地仓库拥有与远程仓库几乎完全一致的提交历史,包括所有的代码快照、提交记录、分支信息等。这种结构使得我们即使在脱离网络的情况下,也能完成绝大部分操作;只有在需要与远程协作(如pushpull)时才需要联网。

Git不仅在效率上做得出色,它还具备非常强的数据完整性保障机制。Git会对每一个对象使用SHA-1算法进行计算哈希索引,这确保了Git会发现我们对文件的所有修改。

1. Git工作阶段

在使用Git管理项目时,每个被管理的文件都会处于某种状态,表示它当前在Git生命周期中的位置。文件有以下四种工作状态:

状态 所在区域 IDE可视化 说明
untracked 工作区Working Directory file.txt 文件数据未被Git跟踪,且未被.gitignore屏蔽跟踪,git add跟踪暂存
committed
unmodified
工作区
版本库Commit History
file.txt 文件数据已经保存到Git本地版本库,且当前工作区也存在该文件数据
modified 工作区 file.txt 文件数据已被修改,git add暂存
staged 暂存区
Staging Area/Index
file.txt[添加]
file.txt[更新]
file.txt[删除]
已对文件当前版本进行标记,已包含到下次提交快照中;

Git 下文件生命周期图。

figure 1.1. 文件的状态变化周期.

需要注意的是,这里的file.txt[删除]是在暂存区里面标记为删除,但工作区文件任存在时,文件在IDE中显示的可视化状态。此外,还需注意Git中 文件工作状态Git工作区 的区别,一个是文件状态,一种是文件存放区

areas

figure 1.2. 工作目录、暂存区域以及Git仓库.

2. Git配置

我们可以通过git config命令来查看或设置Git的各种行为配置,Git支持多级别的配置系统,每个级别的配置会覆盖其下级配置,从而实现灵活控制:

级别 优先级 存储 说明
系统 | --system + Win:C:\ProgramData\Git\configGit 2.x 以后引入
Unix:/etc/gitconfig
存储系统上所有用户及他们仓库的通用配置;
用户 | --global ++ Win:C:\Users\$USER\.gitconfig
Unix:~/.gitconfigor~/.config/git/config
存储当前用户仓库的通用配置;
本地 |--local +++ All:<repo>/.git/config 存储当前仓库的配置;

在实际开发中,我们常用以下Git配置命令来调整使用体验和工作流程:

⚙ 用户与网络

安装完Git之后,要做的第一件事就是设置我们的用户名和邮件地址。 这一点很重要,因为每一个Git提交都会使用这些信息,它们会写入到我们的每一次提交中;

1
2
git config --global user.name "xxxx"
git config --global user.email "xxx@example.com"

Git配置代理可以让Git请求通过指定的HTTP/HTTPSSOCKS代理服务器转发;关于上述通信协议的区别以及其在代理软件中的使用在这里就不过多介绍,这值得单独在一篇文章中讲解;在配置时需要注意的一点是,Git for WindowsSOCKS5的支持有限,在Windows上Git推荐使用HTTP/HTTPS

1
2
git config --global http.proxy http://127.0.0.1:7890
git config --global https.proxy http://127.0.0.1:7890

🔍️ 配置管理

当我们想查看当前项目下有哪些Git配置时,可以使用如下命令:

1
2
3
git config --list 			# 列出当前所有配置
git config --list --show-origin # 列出当前配置及其来源
git config user.name # 查看单条配置

当某项配置不再需要或设置错误时,我们可以使用--unset参数来删除它。需要注意的是,当我们使用--unset删除配置时,其后面必须跟一个配置key,如果没有则会报错,即我们不能使用--unset重置所有设置:

1
2
3
git config --global <config_name>
git config --local --unset core.autocrlf # 成功执行
git config --local --unset # 报错

Git配置都是保存在配置文本文件中,除了直接编辑对应级别的配置文件,我们还可以使用如下命令编辑配置文件:

1
2
git config --<level> --edit
git config --local --edit # 编辑项目本地Git配置

🗨️ 配置别名

使用Git命令行管理项目时,频繁输入完整的命令既繁琐又耗时。为提升效率,Git支持配置命令别名,让我们通过更简洁的方式执行常用操作。我们可以为Git命令设置缩写形式,也可以使用!前缀配置执行任意Shell命令的快捷方式(更推荐使用terminal自带的别名工具):

类型 命令
SHELL git config --global alias.<cmd_name> '!<shell_cmd>'
GIT git config --global alias.<cmd_name> '<git_cmd>'

下面是设置Git命令缩写和Shell命令别名的示例:

1
2
3
4
5
6
git config --local alias.co 'checkout'		# 执行checkout
git config --local alias.vs '!code' # 打开vscode

# 使用
git co -b new_branch
git vs

☁ 凭证管理

如果不进行任何设置使用Git远端命令控制远端仓库,我们需要频繁输入账户信息。Git为我们提供了 凭证管理工具 | credential.helper 以解决这个问题。Git官方推荐的credential.helper配置即相关软件更新频繁,Git中文文档可能来比较更新,前往Git英文文档查询是较优选择。在我写博客的时候,Git有如下适用于不同版本的凭证管理助手:

平台 工具 说明
Windows git-credential-wincred 凭证存储在Windows凭据管理器中。随Git for Windows提供
Linux git-credential-libsecret
cache
凭证存储在Linux密钥服务中,如GNOME密钥圈或KDE钱包。一般由Linux发行版提供;
Linux也支持短暂(15分钟)存储用户凭证在cache中;
MacOS git-credential-osxkeychain 凭证存储在macOS密钥链中。随macOS Git提供

不同平台查看已保存的凭证的方式各不相同,以下是各个平台的查看方式:

  • Windows:打开“控制面板 → 凭据管理器”;
  • Linux (libsecret)seahorsesecret-tool
  • MacOS:打开“钥匙串访问”;

除了设置上述版本工具,我们还可以进行如下credential.helper设置,我们可以使用git config管理凭证工具的配置,在这里就不过多介绍Git命令;

工具 说明
"" 每次都输入账号密码
cache 仅限Linux/macOS,默认缓存15分钟
store 保存在明文文件中(~/.git-credentials)(不安全

值得一提的是,Git的凭证管理适用于使用HTTP协议管理(pushfetch等)远程仓库;当我们想要 使用SSH协议 管理远程仓库时,则需要配置远程仓库的SSH keys

📓 文本编辑器

Git可以使用core.editor配置项来控制默认编辑器。需要注意的是:Linux下Git默认使用vim作为编辑器;Windows默认使用Git Bash自带vim;以下是Windows下配置notepadvscode作为默认编辑器的代码:

1
2
3
4
5
6
git config --global core.editor "<editor_run_command>"		# 通用配置命令

git config --local core.editor "notepad" # 配置notepad
git config --local core.editor "code --wait --new-window" # 配置vscode

git commit --allow-empty # 提交空信息用于调试

配置VSCode时,code就是VSCode的启动命令,而--wait参数会让Git等待VS Code编辑器关闭。如果我们想每次git commit时都vscode都创建一个新的窗口,可以添加--new-window指令;

🌳 合并/对比工具

Git默认提供简单的命令行合并/对比行为,Git可以通过配置来使用自定义的 mergetool | 合并工具difftool | 对比工具 。这里我只介绍Merge/Diff工具的配置,其具体使用方法会在后面单独介绍。

类型 用途
difftool 显示两个版本(如 commit、branch)的差异
mergetool 在出现冲突时帮助我们对比和合并冲突的文件

以下是Windows下配置vscode作为合并/对比工具:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 通用配置命令
git config --global diff.tool <tool_name> # 可省略
git config --global merge.tool <tool_name> # 可省略

git config --global difftool.<tool_name>.cmd 'tool_run_command'
git config --global mergetool.<tool_name>.cmd 'tool_run_command'

# 配置vscode作为合并/对比工具
git config --local diff.tool vscode
git config --local merge.tool vscode

git config --local difftool.vscode.cmd 'code --wait --diff $LOCAL $REMOTE'
git config --local mergetool.vscode.cmd 'code --wait $MERGED'

# 关闭每次都询问工具选择
git config --local difftool.prompt false
git config --local mergetool.prompt false

需要注意的是,因为我们在配置时使用了$符号,在Window CMD中配置相关工具cmd时,使用双引号“”会导致配置失败。

🪟 Windows配置

在Windows上使用Git时,由于与类Unix系统在文件路径、换行符、文件名大小写敏感等方面存在差异,因此Git提供了一些针对 Windows 系统的专属或高度相关的配置项,用来提高跨平台兼容性和开发体验。

  • core.autocrlf

    在Windows中,文本文件的换行符默认是CRLF\r\n),而类Unix系统中,是 LF\n)。为了避免跨平台协作中出现换行符混乱,我们需要使用core.autocrlf控制Git在提交前和检出后如何处理换行符。以下是各个系统的推荐:

    平台 配置 说明
    Windows true 提交时将CRLF转为LF,检出时再转为CRLF
    Linux input 提交时将CRLF转为LF,检出时不做处理

    除了控制Git提交时换行符的转换,我们还可以使用core.safecrlf来控制待add代码的换行符检查策略:

    设置 说明
    true [默认] 拒绝提交混合换行符的文件
    false 不检查换行符混用问题
    warn 发出警告但允许提交
  • core.ignorecase

    Windows文件系统NTFS默认对文件名大小写不敏感,但Git是大小写敏感的。

    设置值 说明
    true [Windows默认] 忽略文件名大小写
    如果我们使用此设置,在修改文件名大小写时不会触发Git跟踪
    false 严格区分文件名大小写(推荐在跨平台项目中手动设为false

🤟 我的配置

下面介绍以下我是如何配置Git的。我在Windows环境下使用Git for Windows。安装时选择集成Git Bash,这使得我们可以在git config alias中直接执行Shell命令。配置策略上,我仅在Git配置的全局级别(--global)中设置 网络代理用户信息 的设置,项目特定配置则保留在本地级别(--local)。以下是我的Git全局配置命令:

1
2
3
4
5
6
7
8
git config --global user.name "xxxx"
git config --global user.email "xxx@example.com"

git config --global http.proxy http://127.0.0.1:7890
git config --global https.proxy http://127.0.0.1:7890

git config --global alias.pi '!f(){ sh "$HOME/.soppyrc/git-custom-ini.sh"; }; f'
git config --global alias.ei '!code $HOME/.soppyrc/git-custom-ini.sh'

需要注意的是,由于Git Bash的环境特性,$HOME会被自动映射到C:\Users\<username>\目录,因此当我们在使用git pi执行git-custom-ini.sh时,命令能够正常执行。

此外,WindowsGit Bash路径格式转换存在诸多问题,后续会在介绍SHELL的文章中详细讲解,在这里就不过多介绍了。这里只简单介绍我遇到的问题:

问题 $HOME错误转换问题
描述 当我们使用 双引号 添加pi命令(!$HOME/..)时,$HOME会被Git Bash自动转换为Windows路径格式,gitconfig结果为!C:\\Users\\lzz/.soppyrc/git-custom-ini.sh,这路径不会Git Bash被正常解析

解决方法:

  • 直接编辑/更换单引号:直接编辑gitconfig文件,或者更换单引号设置pi解决问题;

    1
    2
    [alias]
    pi = !$HOME/.soppyrc/git-custom-ini.sh
  • 使用SHELL函数:使用如下命令设置别名;

    1
    2
    git config --global alias.pi '!f(){ sh "$HOME/.soppyrc/\
    git-custom-ini.sh"; }; f'
问题 单双引号选择问题
描述 当我们使用 双引号 添加ei命令(code ..)时,$HOME会自动解析为Windows路径格式,格式同上。运行ei命令会打开C:/Userlzz/..文件,这是因为VSCode错误解析该路径

解决方法:

  • 更改路径:给路径添加 单引号gitconfig中路径被添加上单引号,此时ei命令能够成功运行,但是gitconfig中地配置仍为:ei = !code 'C:\\Users\\lzz/.soppyrc/git-custom-ini.sh'

  • 更换单引号:使用单引号添加ei命令,命令运行结果与gitconfig中配置结果均符合预期;

为了使上述配置地别名命令能够正常运行,我们需要在$HOME/.soppyrc文件夹下创建sh脚本,以下是该脚本的具体内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/bin/bash
echo "Setting Git aliases and tools..."

git config --local alias.cre "config --local core.autocrlf true"
git config --local alias.lge "log --oneline --graph --all --decorate"
git config --local alias.cae "commit --amend --no-edit"

git config --local core.editor "code --wait --new-window"

git config --local diff.tool vscode
git config --local merge.tool vscode
git config --local mergetool.vscode.cmd 'code --wait $MERGED'
git config --local difftool.vscode.cmd 'code --wait --diff $LOCAL $REMOTE'

git config --local alias.st status
git config --local alias.co checkout
git config --local alias.sw switch
git config --local alias.br branch
git config --local alias.ci commit
git config --local alias.lg log
git config --local alias.df diff
git config --local alias.ad add

# Windows Only
git config --local core.autocrlf true
git config --local core.ignorecase false

echo "Done and remember to configure your GPG, SSH."

基础命令

在学习Git命令的过程中,我们经常需要查阅某个命令的用法。Git提供了多种方式来获取帮助信息:

  • --help:打开该命令的完整文档,例如:git <command> --help

  • -h:在终端中查看简略帮助信息,例如:git -hgit revert -h

    值得一提的是对于多层命令(例如git stash <sub_cmd>)而言,-h会递归输出该命令的所有子命令的帮助信息,所以不一定需要对每个子命令都使用-h输出帮助信息。

在本章节中,我们将深入学习Git的常用与进阶命令。与其单纯地阅读命令解释,不如通过实际操作来加深理解。因此,我们参考如下指令创建一个Git Mock项目,在真实的仓库环境中练习每个命令的使用方式与效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mkdir git-learn
cd git-learn

code .gitignore # echo ".idea">test.txt

git init
git config --local core.editor "code --wait --new-window"
git config --local alias.lg 'log --oneline --graph --all --decorate'
git config --local alias.st 'status'

code test.txt # echo "first commit">test.txt

git commit -a
git branch -M main

git tag -a first -m 'Tag first commit' # 标记初始节点
# 使用`git checkout first`将HEAD移动到Git开始节点

1. 基础

在Git发布20周年的一次访谈中,Linus提到,他日常使用的Git命令其实只有几种。事实上,对于大多数人来说,只需掌握几条常用命令,就足以应对团队协作或参与开源项目的需求。想要深入学习Git命令,我们不妨先来了解这些常用命令。

🧭 Git引用与运算符

commit是Git中保存代码快照的对象,我们使用Git 引用 | ref 来操作每个具体的commit对象,常见的引用类型有:

  • HEAD:指向当前操作的默认commit,即“当前所在的位置”;

    在Git中,绝大多数操作都会围绕HEAD所指向的commit进行。如果在一些需要 commit的命令中不指定引用,Git默认会使用HEAD作为目标。

    状态 说明
    attached HEAD指向一个分支,此时当前分支始终会跟随最新的提交
    detached HEAD指向一个具体的commit,只有HEAD指向更新的提交
  • branch:指向对应分支的最后一次commit,是一种可移动的引用;

  • tag:指向某一个固定commit,当我使用git checkout切换HEADtag上时,等同于切换到hash id上;

  • hash id:指向固定IDcommithash id是对commit对象的直接引用;

我们可以使用^~运算符找到一个引用的相对引用,以下是详细用法:

符号 说明 示例
~ 表示从当前提交沿着第一父提交的路径向上追溯若干代父提交 - HEAD~1:表示HEAD的父commit
- HEAD~2:表示HEAD的祖父commit
^ 表示当前提交的第几个父节点,常用于合并提交 - HEAD^:通常等同于 HEAD~1,指向第一个父commit
- HEAD^2:表示第二个父commit

需要注意,只有当Git执行octopus merge目前没有冲突处理步骤,只允许无冲突合并)时,合并节点才会有两个以上父节点。此时,父节点对应的<parent_number>遵从使用merge合并时的输入顺序

为了体会<parent_number>的命名顺序,我们不妨运行如下HEAD MOCK,我们按照时间先后顺序在Git的first创建待合并的分支:OM_1OM_2OM_3OM_4。然后执行(HEAD -> OM_1)git merge OM_4 OM_2 OM_3,在合并后的HEAD -> OM_1上使用HAED^<parent_number>,有如下结果

命令 结果
git log --oneline HEAD^2 OM_4
git log --oneline HEAD^3 OM_2
git log --oneline HEAD^4 OM_3

需要注意的是,detached状态下的HEAD的相关查询命令的结果也是如此

👶 git add & commit

addcommit是Git最常用的命令。尽管我们可能在之前已经初步了解了他的用法,这里我们还是简要了解以下其常用参数选项:

git add 说明
. 当前目录下所有更改(新增+修改,不包括删除)
-A/--all 所有更改(包括新增、修改、删除)都加入暂存区
-u/--update 只添加已跟踪文件的更改(含删除),不包括新文件
-p/--patch 交互式选择文件部分变更加入暂存区,注意该选项不能添加文件追踪
-n/--intent-to-add 标记为将添加的新文件,但内容还未加入
-v 显示详细的添加过程
-i/--interactive 打开一个交互式命令界面(比-p更复杂)选择文件变更加入
git commit 说明
-m "msg" 添加提交说明
-a 自动添加已跟踪文件的更改,不需要git add,常用-am更新Modified File
--amend 修改最近一次提交(可改内容和说明)
--no-edit --amend一起使用时,不修改说明
--allow-empty 允许提交一个无变更的空提交
--dry-run 模拟提交过程,不实际提交
--verbose/-v 提交时显示 diff 内容
--date=... 自定义提交时间
--author="..." 自定义提交作者信息
--reset-author 重置作者为当前配置的Git用户
--signoff/-S 添加GPG签名(常用于开源项目

🚫 .gitignore

.gitignore文件是Git项目中的一个特殊文件,用于告诉Git哪些文件或文件夹不应被跟踪,即不纳入版本控制。它是版本控制过程中非常重要的一个工具,尤其是在避免将临时文件、构建输出、敏感信息(如密码配置)等纳入仓库时。

当我们在Windows CMD中使用echo命令创建该文件时,请注意编码格式。这是因为当我们使用.gitignore时,需要考虑编码格式一致性。Git默认文本编码是UTF-8,我们可以通过在仓库根目录创建.gitattributes,并写入如下内容。这样,Git知道工作区.gitignoreGBK编码,提交时会把它转换成UTF-8存入Git本地版本库:

1
.gitignore text working-tree-encoding=GBK

我们可以按照路径匹配模板文件匹配模板的分类分别学习匹配字符串:

路径模板 说明
temp 匹配所有名为temp的文件与文件夹,需要注意这里的temp文件没有ext
temp/ 匹配所有temp文件夹
/<xxx> 匹配根目录下的文件或文件夹,这里匹配模板用/开头表示根目录
**/ 匹配任意层级目录(Git 2.x 以后引入
文件模板 说明
*.log 匹配所有.log结尾的文件
* 匹配零个或多个任意字符
? 匹配一个任意字符
[...] 匹配括号内的任意一个字符
!important.txt 不忽略important.txt,即使之前被忽略

需要注意的是,.gitignore只能影响未被Git跟踪track的文件。如果某一个文件已被跟踪,即使我们后来把它加入 .gitignore,它仍然会被跟踪。我们需要使用git rm命令取消文件跟踪。此外,.gitignore文件还支持在项目子目录中创建,它会覆盖或补充上层目录的规则。

我们可以使用如下命令调试.gitignore,查看某个文件是否被忽略:

1
git check-ignore -v <filepath>

2. 撤销

1. Git工作阶段中,我们已经知道了Git中工作区与文件的联系。当我们使用Git管理我们的项目版本时,主要是在对三种存储区中的文件进行各种操作。我们可以使用git status查看当前项目的状态,查看的文件类型有:

  1. staged:文件已更改,且放入暂存区;
  2. not staged:文件已更改,但未放入暂存区;
  3. untracked:文件未被Git追踪;

我们可以使用git add添加not staged文件到暂存区或者添加并追踪untracked文件,然后使用git commit来提交暂存区的更改到Git本地版本库,这是Git提交的基本流程

1
2
3
4
5
6
7
8
9
10
11
12
git switch main

code test.txt # echo "staged add">>test.txt
git add ./test.txt # 添加test.txt文件到暂存区
code test.txt # echo "not staged add">>test.txt
code new.txt # echo "untracked file">new.txt

git status # git status会显示项目当前的三种存储区的文件
git commit
git status # 只会commit已经存入暂存区的文件

git commit -a # finish experiment

在理想情况下,我们每次都能准确地addcommit正确的修改内容。但现实中错误的操作不可避免:也许我们添加了不该添加的文件,或提交了有问题的代码。遇到这种情况,不必慌张,Git提供了git restoregit resetgit revertgit rm工具来帮我们解决问题。

🌱 git restore

git restore是Git在2.23版本中引入的新命令,是对旧命令的语义拆分。该命令用于撤销文件的更改,类似于ctrl+z。常用选项如下:

选项 说明
--source=<commit> 指定还原的来源,默认是暂存区INDEx,注意我们实际上并不能使用INDEX访问暂存区
--staged 将目标文件从暂存区还原,不影响工作区。该选项相当于取消git add的效果:
- 既可以取消已跟踪文件的暂存操作
- 也可以取消未跟踪文件的跟踪、暂存操作
--worktree 将目标文件还原指定版本到工作区,即取消工作区修改,不影响暂存区。
默认的git restore实际上就是git restore --source=INDEX --worktree
--ours 在合并冲突中使用,选择“我们”的版本(当前分支),用于解决冲突时还原文件内容
--theirs 在合并冲突中使用,选择“对方”的版本(合并进来的分支),同样用于冲突解决

当我们使用merge合并两个有冲突的分支时,Git会自动修改冲突文件(此时,冲突文件中会出现冲突标记<<<<<<<=======>>>>>>>)。此时,冲突文件为not staged状态,我们需要执行:修改冲突 → add到暂存区 → commit提交,才能完成merge流程。而git restore为我们提供了快速合并的方式,--ours就是以当前分支的结果替换掉<<<<<<<...>>>>>>>的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# On branch restore, restore-dev
git checkout restore
git checkout -b restore-dev

code test.txt # echo "dev add" >> test.txt
git commit -a

git checkout main
git merge dev

# Result
# Auto-merging test.txt
# CONFLICT (content): Merge conflict in test.txt
# Automatic merge failed; fix conflicts and then commit the result.
#
# Conflict
# <<<<<<< HEAD
# staged add
# not staged add
# =======
# dev add
# >>>>>>> restore-dev

下面我们分别执行--ours--theirs的合并方式,结果如下:

1
2
3
4
5
6
git restore --ours .\test.txt\
# staged add
# not staged add

git restore --theirs .\test.txt\
# dev add

⚡️ git revert

git revert是Git中用于撤销某次提交的命令,它的特点是不会修改项目的历史记录,而是通过创建一个新的提交来“反向更改”原有提交的改动。常用选项如下:

选项 说明
--edit
--no-edit
控制是否打开editor修改Git自动生成的提交信息(默认--edit
--commit
--no-commit/-n
控制还原后是否提交(默认-n
- --commit:在提交信息(自动/手动)确定后,Git会自动提交撤销记录;
- --no-commit:在提交信息确定后,Git不会提交撤销,这方便我们针对撤销结果继续更改,或者执行新的撤销
-m <parent-number> 当回退一次合并提交时,指定主干父提交的编号,告诉Git应该以哪个父分支为基准进行回退
--continue/--abort
--skip/--quit
用于在回退多个提交或遇到冲突时控制流程
--signoff/-s revert的提交信息中添加Signed-off-by签名
-S[<keyid>] 如果我们配置了GPG密钥,该命令会对回退提交进行签名,用于增强提交的身份验证

光看options列表也许不能帮助我们很好的了解其功能,下面我们结合例子讲解git revert的用法。

  • 撤消多个提交revert支持一次性撤销多个commit

    当我们执行git revert A B C时,Git实际上执行了如下指令:

    1
    2
    3
    git revert A
    git revert B
    git revert C

    在使用revert撤销多个提交时,有如下需要注意的情况:

    • 如果撤销序列正常,且我们添加了-n选项,Git会依次进行撤销,并在最后将是否提交撤销的决策权交予我们;

    • 当我们提供了乱序的commit id时,Git会处理每一次撤销引起的冲突。如果这时我们添加了-n选项,Git仍会执行每一次冲突处理,需要处理前一次冲突提交后,才能执行下一次冲突处理,这时-n没有效果;

    • 我们将撤销多个提交的revert当成多次执行revert命令就能很好的理清Git处理的逻辑;

  • 处理冲突:当我们使用revert时也会遇到冲突,以下是revertConflict Mock

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    git checkout revert

    code revert.txt # echo "revert 1" > revert.txt
    git commit -a
    ...
    code revert.txt # "revert 2" -> "revert 3"
    git commit -a

    git log --oneline --graph --decorate
    # Result
    # * 03323ac (HEAD -> revert) revert 3
    # * 375b19a revert 2
    # * 790281c revert 1
    # * 1aa32af first commit

    如果我们想撤销HEAD提交不会发生冲突,但当我们想撤销HEAD^就会发生冲突。

    • f8319d7(HEAD):内容变更为revert 2revert 3
    • b5b25a8(HEAD^):内容变更为revert 1revert 2,更改后内容为revert 2

    此时,Git无法直接撤销HEAD^提交,Git会进入类似于merge中处理冲突的状态并自动修改冲突文件,其结果如下:

    1
    2
    3
    4
    5
    <<<<<<< HEAD
    revert 3
    =======
    revert 1
    >>>>>>> parent of b5b25a8 (revert 2)

    这时候,Git猜测我们的想法是撤销掉HEAD^的修改结果,故Git会列出基于HEAD^向前撤销向后撤销两种方式,然后我们再执行如下命令即可完成revert操作:

    1
    2
    git add .
    git revert --continue # 此时,会弹出编辑器界面用于commit
  • 撤销合并:我们在执行合并节点的撤销时,需要添加-m <parent_number>选项指定撤回的父节点,以下是revertMerge Mock

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # 准备阶段
    git checkout revert-merge
    code revert.txt
    git commit -a

    git checkout revert
    git merge revert-merge

    git commit -a

    # 撤销合并节点
    git revert -n -m 1 HEAD
    git revert -n -m 2 HEAD

🔥 git reset

git reset也是Git中用于撤销的命令。与生成新提交的git revert不同,它直接将HEAD退回到指定的提交对象上,因此该命令不会新增提交。根据不同的使用场景,我们可以选择不同选项来决定该命令是否影响暂存区工作区

选项 说明
--soft 撤销提交但工作区不变,工作区内容自动add暂存区
--mixed[默认] 撤销提交但工作区不变,工作区内容未被add
--hard 完全回退到指定提交记录,之后的提交改动都会丢失
可以搭配git reflog查看历史并恢复

reset的撤销选项还有--keep--merge。但经过测试,我发现上述功能就能很好的覆盖大部分reset任务,故就不再过多讲解其他的选项。

需要注意的是,对于--soft--mixed模式,其撤回造成的结果中可能存在staged/not staged文件。此时,我们既可以使用restore清空暂存区/工作区的修改,也可以使用git reset --hard HEAD直接撤回到该提交节点的保存状态,两者可以达成一样的效果;

🗑️ git rm

上述命令已经可以执行大部分撤销任务,如果要更加自由的执行撤销任务或者完成其他特殊任务,可以考虑使用git rm命令。git rm是Git中用于从暂存区和工作目录中删除文件的命令。常用选项如下:

选项 说明
--cached 只从暂存区中删除,不删除工作目录
-r/-f -r用于递归删除目录,-f用于强制删除文件(例如删除修改过的not staged文件)
两者常一起使用:-rf
--ignore-unmatch 尝试删除不存在文件时,使用该命令不会报错
--dry-run 模拟删除,显示会删除哪些文件

git rm常在我们更新.gitignore时使用。.gitignore只对未被 Git 跟踪的文件有效。如果我们已经将某个文件提交到了Git中,即使后来加进.gitignore,它仍会被跟踪。这时我们需要使用--cached选项,在暂存区中删除以跟踪文件:

1
2
3
4
5
6
7
8
9
10
# Change
git rm -rf --cached .
git add .

git commit -m 'chore(.gitignore): add file: xxx to ignore'

# Check
git check-ignore xxx
git checkout HEAD^
git check-ignore xxx

需要注意使用git rm -rf删除文件时,Git会自动将删除操作的更改add到暂存区。这时候我们想要还原删除文件可以使用git restore --source=HEAD .从最近提交还原。需要注意的是,这里不能使用git restore .因为该命令默认是从暂存区中还原文件,而暂存区已经被git rm更改了,故会报如下错误:

1
error: pathspec '.' did not match any file(s) known to git