不要再一条 main 走到黑了。
同步工具
在很久很久以前,好吧其实也没多久,WinXP 上存在一个叫公文包的神奇软件。
它用于在多台不联网的计算机上同步文件,在一台电脑上将文件拖入u盘内的公文包中,再在另一台电脑上插入u盘,修改里面的文件,当u盘插回原来的电脑并打开公文包后,便会提示更新的文件,并可选同步至本地。
其本质是一个文件夹,保存有原文件的拷贝外加一个隐藏的公文包数据库配置文件。
是的,这就是 SVN、Git、云同步的老祖宗,但公文包还缺少了版本控制,并且无法支持多人修改解决冲突。
版本控制系统
最原始的版本控制是按照修改时间拷贝出多个副本,这么做污染工作空间,并且在文件较多、多人修改解决冲突时极为低效。
写论文时通常会这么做,但对于文件和成员繁多的软件项目来说不可能这么做。
版本控制系统解决了这些问题,其目的是跟踪文件变化、让项目成员间协作更加高效。
- 跟踪代码历史记录
- 以团队形式协作编写代码
- 查看谁做了哪些更改
类型:
- 集中式:SVN、CVS。文件保存在中央服务器上,本地只保留文件副本。在修改文件前需要先从服务器上拉取最新版本,修改后再推送至服务器。优点是使用简单,缺点是服务器宕机后无法工作,且需要联网。
- 分布式:Git、Mercurial。每个本地都有完整的版本库,可以直接在本地处理文件,在需要时多个本地互相同步更新文件,或通过远端仓库同步。但也意味着解决冲突、合并分支等操作更复杂,当然这是以可以通过高内聚低耦合、良好的分支设计来解决的。
开始使用Git
前往Git 官网下载安装包,就像其它软件一样,Linux 通过 apt 安装即可。
相关文档:
Git 官方文档
GitHub-开始使用 Git
常用 Git 命令清单-阮一峰
DevOps Guidebook-Git
使用 Git 的方式有很多:命令行、GUI、IDE 集成等。本文主要使用命令行和 VSCode。
常用命令图:
Git中有许多操作可以使用多个命令完成,本文以操作进行划分,将常用的命令列出。若一个操作只有一个常用的命令,则在目录中会直接列出该命令。
git config 基本配置
使用 git config
命令配置用户名和邮箱。Git官网-初次运行 Git 前的配置
作用范围:
--global
:全局配置,只需配置一次。最常用。--local
或省略:只对当前仓库有效。--system
:对系统所有用户有效。
1 | # 配置用户名,有空格需加引号 |
git config --list
查看所有配置信息。git config --get <name>
查看单个配置信息。
配置的用户名和邮箱会出现在 commits 提交记录中,仅作为标识,不作为身份验证。
配置的是 Github 绑定的邮箱,则显示对应的账号,否则显示配置的用户名。
删除配置:
1 | git config --global --unset user.name |
初始化仓库
git init
用于初始化一个新的仓库,会在当前目录下创建一个隐藏的 .git
文件夹,用于保存版本库和配置信息。
1 | git init # 直接在当前文件夹初始化仓库 |
这就像公文包一样,当你删除 .git 文件夹,那么该根文件夹也将不再是一个 Git 仓库。
原本默认的分支名是 master,但现在已经改为 main,github/renaming,通过下面命令修改默认分支名:
1 | git config --global init.defaultbranch main |
除此之外,git clone
可以克隆一个远端仓库到本地。
组成与文件状态
Git 仓库由三部分组成:
- 工作区 Working Directory:当前工作目录,对项目的某个版本独立提取出来的内容,供用户修改。
- 暂存区 Staging Area、索引:一个文件,记录了将要提交的已修改的文件列表信息。
- 版本库、本地仓库 Repository:即 .git 文件夹,包含项目的元数据和对象数据库。
工作流程:
- 在工作区修改文件。
- 将修改后的文件添加到暂存区。
- 将暂存区的文件提交到本地仓库。
文件状态:
- 未跟踪 untracked:文件未被 Git 管理,通常是新建的文件。
- 已修改 modified:文件已被修改,但未添加到暂存区。
- 已暂存 staged:文件已添加到暂存区,但未提交。
- 已提交 committed:本地仓库保存着的特定版本的文件。
还有些说法存在已跟踪状态,通常是为了方便理解,即文件已经被 Git 管理,首次将文件添加到暂存go会变为已跟踪状态。
更改文件状态
git status
用于查看仓库的状态,-s
精简显示。
在仓库中新建文件并查看状态,显示了未跟踪 untracked 的文件列表。
1 | On branch main |
添加到暂存区
使用 git add
将已修改、未跟踪的文件添加到暂存区,再次查看状态,显示了已暂存 staged 的文件列表。
git add <file1> <file2>
添加指定的若干个文件。git add .
添加工作区所有文件。git add <folder>
添加指定文件夹下的所有文件,包括子目录。git add *.js
支持通配符,添加所有后缀为 .js 的文件。
Git中 . 号表示当前目录,包括其子目录,使一个命令对当前目录下的所有文件生效。大多数命令都支持这种用法。后文的
<target>
即表示通用的目标表示。<file>
则是必须明确指定文件path。
1 | On branch main |
从暂存区移除
git rm <file>
对于已提交未修改的文件,将文件从工作区移除,并暂存删除操作。
1 | > git rm 01.js |
若文件已修改或已暂存,则需要使用下面两个命令,否则会报错。git rm --cached <file>
可以将文件从暂存区和版本库中移除,但保留工作区文件及其修改,文件会变为未跟踪状态。git rm -f <file>
强制移除,将文件从暂存区、工作区移除,并暂存删除操作。
保留跟踪状态:git reset HEAD <target>
和 git restore --staged <target>
也可以将文件从暂存区移除,且文件仍然是跟踪状态,保留工作区文件及其修改。
HEAD 是一个特殊的指针,它指向当前分支的最新提交。当你切换分支或创建新的提交时,HEAD 会自动移动,始终指向当前分支的最新提交。
放弃工作区修改
放弃工作区的修改:就是将文件恢复到最近一次提交的状态。
下面几个命令都可以用来放弃工作区的修改,行为是一致的,但不影响暂存区,仅将工作区文件恢复到最近一次提交、未修改的状态。
git restore <target>
放弃工作区修改。只作用于已跟踪的文件。git checkout <target>
多用于切换分支,但也可以用来还原工作区。只作用于已跟踪的文件。
下面的命令会同时将文件从暂存区移除,再放弃工作区的修改。
git reset --hard HEAD
清空暂存区,并将最后一次提交的版本恢复到工作区。只作用于已跟踪的文件。git restore --staged --worktree <target>
将文件从暂存区移除,再放弃工作区的修改。只作用于已跟踪的文件。
上面的命令只作用于已跟踪的文件,不会对未跟踪的新文件产生影响。
对于刚新建的文件,其状态是未跟踪,需要使用 git clean
,默认不删除 .gitignore 指定的文件夹和文件,参数列表如下,可混合使用。
-f <target>
删除指定路径未跟踪文件,不包括子目录。-d
递归删除,时包括子目录。-x
删除 .gitignore 中忽略的文件。-n
显示将要删除的文件,但不删除。-i
进入交互模式,可以选择性删除文件。
将工作区还原到最后一次 commit 状态:
1 | git clean -df # 删除.gitignore指定之外的未跟踪文件,包括子目录 |
git commit 提交到本地仓库
git commit
用于将已暂存的文件提交到本地仓库。每次提交会形成一个新版本,commit id 就相当于版本号。
可以加上 -m <message>
参数以快速设置提交信息。
否则会进入系统配置的编辑器,通常是vim,可以通过 git config --global core.editor <editor>
修改。
1 | > git commit -m "一次提交" |
命令行会告诉你本次提交的分支名、commit id前若干位(能唯一标识的最短位数),以及修改的文件数量、插入和删除的行数。
-a
参数把所有已修改的文件(不包括未跟踪的文件)都添加到暂存区中,并直接进行提交。
1 | > git commit -am "111" |
—amend 修改最后提交
1、修改 message:git commit --amend -m <message>
修改最后一次提交的提交信息。
2、增补修改:git commit --amend <target>
会将已暂存的文件和上一次提交的文件合并,形成一个新的提,会弹出编辑器,可以修改提交信息。
3、修改提交者:git commit --amend --author='Name <email>'
修改提交者信息。
其它参数:--no-edit
不修改提交信息。--reset-author
使用新配置本地用户的信息。
git log 查看提交历史
git log
用于查看提交历史,包括作者、时间、commit id、分支信息、提交信息。
1 | > git log |
(HEAD -> main)
表示当前 HEAD 指针指向 main 分支的最新提交。
--oneline
参数可以将每次提交压缩为一行,只显示简短commit id和提交信息。
1 | > git log --oneline |
其它参数:--grep
搜索包含指定关键字的提交信息。--author
搜索指定作者的提交记录。--since
--after
搜索指定时间之后的提交记录。--until
--before
搜索指定时间之前的提交记录。--no-merges
不显示合并提交。--graph
以图形化方式显示提交历史。
1 | git log --grep="feat" |
git reset 回退版本
git reset <HEAD/commit id>
用于回退版本,本质是更改 HEAD 指针的指向。
三个参数:
--mixed
默认参数,清空暂存区,工作区不变。--soft
工作区和暂存区都保持不变。--hard
清空暂存区,工作区回退到指定版本的状态。
直接执行该命令等同于 git reset --mixed HEAD
,回退到当前分支的最后一个提交版本,清空暂存区,工作区不变。
版本的指定方式:
HEAD
:当前分支的最新提交,HEAD^
表示上一个版本,HEAD~<number>
表示前 number 个版本。commit id
可以是完整的 commit id,也可以是前几位,只要能唯一标识即可。
1 | > git log |
回退后,会提示未暂存的更改。
1 | 2722e35 (HEAD -> main) test提交 |
可以看到,其本质是更改了 HEAD 指针的指向,而 git log
是从 HEAD 开始查看提交历史的。
git reflog 引用日志
回退后的提交历史并没有被删除,只是不再在提交历史显示,这确保了 Git 的所有操作都是可回溯的。
使用 git reflog
查看引用日志,所有引起 HEAD 指针变动的操作,都会被记录。
注意:reflog 并不是 Git 仓库的一部分,它单独存储,是纯本地的。
1 | > git reflog |
可以看到 b13367c 版本仍然存在,这就是后悔药,可以通过 git reset
恢复到回退前的版本。
1 | > git reset b13367c |
即使有了 reflog,但它也只能回溯已提交的版本,对于暂存区和工作区的数据丢失,还是无能为力的。
git diff 查看文件差异
git diff
用于查看文件的差异,包括不同状态、不同版本、不同分支的差异。
当然,这种 diff 对比通常使用 VSCode 或其它 GUI 工具更方便直观,但该命令是基石,还是有必要学习的。
直接执行该命令,存在三种情况:
- 已暂存的文件:不显示
- 已修改未暂存的文件:查看工作区和最新版本的差异。
- 暂存后又有修改的文件:查看工作区和暂存区的差异。
1 | > git diff |
显示了所有已修改的文件的差异,包括文件路径、hash值、内容的变动。
其它用法:
git diff --cached
只查看暂存区和最新版本的差异。git diff <file>
查看指定文件的差异。git diff HEAD
查看所有已修改的文件和最新版本的差异。git diff <commit id>
查看所有已修改的文件和指定版本的差异。git diff <commit id1> <commit id2>
查看两个版本的差异。git diff <branch1> <branch2>
查看两个分支的差异。
参数前后位置关系:第二个版本相较于一个版本的差异,也就是第二个版本相较于第一个版本变动了什么。
git ls-files 查看文件列表
git ls-files
用于查看 git 的文件列表
它有很多参数,以查看不同状态的文件列表。
--cached(-c)
查看已跟踪(暂存区+版本库)的文件列表。--stage(-s)
在 -c 基础上显示更详细的信息。--deleted(-d)
查看工作区已删除的文件列表。--modified(-m)
查看工作区已修改的文件列表。--others(-o)
查看工作区未跟踪的文件列表,包括忽略的文件。--unmerged(-u)
查看未合并的文件列表。--killed(-k)
显示文件系统上因文件/目录冲突而需要删除的文件。--ignored(-i)
查看被忽略的文件列表。
同时使用多个参数,取并集。
1 | > git ls-files -s |
.gitignore 忽略文件
.gitignore
文件用于指定不需要 Git 管理的文件或文件夹,通常是编译文件、日志文件、临时文件等。
注意:只能忽略未跟踪的文件,已跟踪的文件需使用 git rm --cached
将其变为未跟踪状态再忽略。
匹配规则:从上到下,先匹配先生效,后匹配的会覆盖前面的匹配。
#
号开头的行为注释。- 使用标准的 glob 模式,
*
匹配零个或多个字符,?
匹配一个字符,[abc]
匹配 a、b、c 中的一个字符。 **
匹配多级目录,a/**/b
匹配 a 目录下任意层级的 b 文件。[0-9]
任意一位数字,[a-z]
任意一位小写字母。!
取反,!*.log
表示不忽略所有 .log 文件,优先级比忽略文件夹低。/
当前目录。
1 | # 忽略所有的log文件 |
github 提供了许多语言的模板,可以直接使用:github/gitignore
远端仓库与GitHub
远端仓库用于多人协作,方便各个本地仓库同步和贡献代码,可以是自己搭建的 Git 服务器,也可以是第三方的 Git 服务,如 GitHub、GitLab、Gitee 等。
GitHub 是一个基于 Git 的代码托管平台,并提供了issues、拉取请求、代码审查、actions等功能。关于 GitHub 和 Git
GitHub 提供了两种远程仓库地址:
- HTTPS 在 push 时需要输入用户名和密码,且 GitHub 在 2021 年 8 月 13 日后不再支持密码验证,需要使用 token。好处是 clone 时比较方便。
- SSH 通过 SSH 密钥验证,不需要输入用户名和密码,且更方便安全,但必须提前在 GitHub 配置本地 SSH 的公钥,否则 clone 和 push 都无权限。
配置SSH
先进入用户主目录,查看是否存在 .ssh
文件夹,若不存在则创建。
1 | > cd ~/.ssh |
查看是否存在公钥和私钥。
1 | > ls |
known_hosts:当首次与一个SSH服务器建立连接时,客户端会记录下该服务器返回的的公钥,并保存在known_hosts文件中,以后每次连接该服务器时,客户端都会验证该服务器返回的公钥是否与known_hosts文件中保存的一致。
接着使用 ssh-keygen
生成 SSH 密钥对。
-t
指定密钥类型,默认 rsa。-C
添加注释,一般是邮箱。-f
指定密钥文件名。默认为 id_rsa。-b
指定密钥长度,默认 2048。
Enter passphrase 这一步通常直接回车,不设置密码,否则每次 clone 和 push 都需要输入密码。
1 | > ssh-keygen -b 4096 |
再次 ls,可以看到生成的 test 私钥和 test.pub 公钥。
1 | > ls |
复制公钥内容,进入 GitHub 设置页面 SSH and GPG keys,添加 SSH 公钥。
1 | > cat test.pub |
如果是默认的 id_rsa,则现在已经完成了配置,但如果自定义了 SSH 密钥文件名,则需要在 ~/.ssh/config
文件中添加配置,指定在访问 GitHub 时使用该密钥。
config 格式,详见 [SSH]客户端配置文件config
1 | Host 名称,用于标识某个特定的配置 |
1 | > vim ~/.ssh/config |
测试配置是否成功
1 | > ssh -T git@github.com |
这里将 Host 和 HostName 都设置为 github.com,若有多个 SSH 密钥以连接不同 GitHub 账户,可以通过 Host 区分,但推送和拉取时需改为对应 Host。
1 | > vim ~/.ssh/config |
git remote 操作远端仓库
git remote
查看已关联的远端仓库,-v
显示详细信息。
git remote add <name> <url>
添加远端仓库
- name 一般是 origin,也可以是其它名字,用于区分多个远端仓库。
- url 远端仓库地址,可以是 HTTPS 或 SSH。
1 | > git remote add test git@github.com:qxchuckle/git-test-2.git |
git remote show <name>
查看远端仓库的详细信息。
1 | > git remote show test |
git remote rm <name>
移除远端仓库。
git remote rename <old> <new>
重命名远端仓库。
git push 推送
git push <remote> <branch:remoteBranch>
推送本地分支到远端仓库的指定分支,若远端分支不存在则会自动创建。
省略写法:
- 冒号可以省略,简写为
<branch>
,表示将指定本地分支推送到远端同名分支。 - 省略分支名,表示将当前分支推送到远端同名分支。
- 省略远程仓库名和分支名,将当前分支推送到与之存在追踪关系的远程分支(即追踪分支)。
1 | # 将本地 main 分支推送到远端 test 仓库的同名 main 分支 |
追踪关系
与远程分支建立追踪关系,则该远程分支就是本地分支的追踪分支,也称为上游。
push 的 -u
参数可以将本地分支和远程分支建立追踪关系,在下次推送该分支时,可以省略远程仓库名和分支名。-u
是 --set-upstream
的简写形式。
git branch -vv
查看本地分支和远程分支的追踪关系。
1 | # 将本地 main 分支推送到远端 test 仓库的 other 分支,并建立追踪关系 |
一个本地分支只能有一个追踪分支,但一个远程分支可以被多个本地分支追踪。
还可以通过 git branch
建立追踪关系:git branch --set-upstream-to=<remote>/<branch> <branch>
删除追踪关系:git branch --unset-upstream <branch>
在 clone 的时候,所有本地分支默认与远程的同名分支建立了追踪关系。
注意:
在默认的 simple 推送策略下,如果本地分支和追踪分支不同名,那么下次推送时还是需要指定远程仓库名和分支名。
推送策略
对于不带任何参数的 git push
的推送策略:
- nothing 什么都不做。
- simple 推送当前分支到同名追踪分支。
- matching 推送所有本地分支到同名追踪分支。
- upstream 推送当前分支到追踪分支。
在 Git 2.0 之前,默认是 matching 模式,2.0 之后默认是 simple 模式。
可以通过 git config --global push.default <策略名>
修改推送策略。但通常不要修改。
影响:
即使你将本地 main 分支关联到了 test 仓库的 other 分支,但如果推送策略是默认的 simple,那么下次推送时还是需要指定远程仓库名和分支名。
1 | > git push test |
删除远端分支
如果省略本地分支名,则表示删除指定的远程分支,因为这等同于推送一个空的本地分支到远程分支。
或者使用 git push <remote> --delete <branch>
删除远端分支。
1 | > git push origin :master |
强制推送
如果远程主机的版本比本地版本更新,推送时Git会报错,要求先在本地做合并差异,然后再推送到远程主机。
可以使用 --force
强制推送,但一般不要使用,因为会覆盖远程仓库的提交记录。
1 | > git push test |
解决冲突
当远程仓库和本地仓库的提交记录不一致时,会产生冲突,此时需要先解决冲突再推送。
1 | > git push |
先 git pull
拉取远程仓库的更新。若存在冲突的文件,就会像下面这样,需要手动解决冲突。
1 | > git pull |
打开冲突的文件,可以看到 git 已经标记了冲突的地方。
使用 =======
分隔开两个不同版本的内容,上面的 <<<<<< HEAD
是本地仓库的内容,下面的 >>>>>> CID
是远程仓库最新版本的内容。
1 | 111111111111111111 |
修改好最终的内容后,再次提交。
1 | > git commit -am "解决冲突" |
若 pull 能正常拉取完成功,说明没有文件冲突,直接 push 即可。
git fetch 拉取
git fetch <remote>
拉取远端仓库的所有分支到本地仓库,也可以指定拉取某个分支。
在本地使用 <remote>/<branch>
访问远程分支。
1 | > git branch -r |
在 fetch 后 可以在其基础上使用 git checkout
命令创建一个新的分支并切换过去,这通常是想先看看远程分支的情况。
1 | > git checkout -b newBrach origin/main |
审查没问题后,就可以使用 merge 或 rebase 合并到本地分支。
1 | > git merge origin/main # git merge FETCH_HEAD |
另一个流程:
在 fetch 时也可以直接拉取到一个新的分支。
1 | > git fetch origin main:main-tmp |
git fetch
拉取的远程分支最新的 Commit-ID 会记录在 .git/FETCH_HEAD
文件中。
若有多个分支,FETCH_HEAD 内会有多行数据。
本地的 FETCH_HEAD 是一个特殊的指针,始终指向执行 fetch 时对应的远程分支的最新版本(Commit-ID),可以通过 git merge FETCH_HEAD
合并到当前分支。
这就会出现一个小问题,当在 B 分支上执行 git fetch
后,FETCH_HEAD
指向了远程的 B 分支,此时切换到 A 分支,执行 git merge FETCH_HEAD
会合并远程的 B 分支到 A 分支。所以要小心使用 FETCH_HEAD。
git pull 拉取合并
git pull
用于拉取远程分支的更新,并与本地指定分支合并
基本用法:git pull <remote> <remoteBranch:branch>
拉取远端仓库的指定分支到本地仓库的指定分支。若本地分支不存在,则会自动创建。
省略冒号时,表示拉取到当前活动分支。
在当前分支具有上游跟踪分支的情况下,可以省略远程仓库名和分支名。
1 | > git pull |
相当于 fetch 后 merge。
1 | git pull origin main |
其余参数:--rebase
参数:合并时采用 rebase 模式。-p
在本地删除远程已经删除的分支。而默认情况下不会在拉取远程分支的时候,删除对应的本地分支。
1 | git pull = git fetch + git merge FETCH_HEAD |
git branch 分支
在之前操作,大多都是在 main 分支上进行,但实际开发中,会创建多个分支,用于不同的功能开发和版本维护。
基本用法:git branch
查看分支列表git branch -v
查看分支列表详情git branch -vv
查看追踪关系git branch <name>
创建分支git branch -d <name>
删除已合并分支,若分支未合并,需使用 -D
强制删除git branch -m <old> <new>
重命名分支
切换分支:git checkout <branch>
或 git switch <branch>
checkout 还可以用来还原工作区,所以在 Git 2.23 之后,推荐使用 switch。
切换分支时,HEAD 指针会指向新的分支最后一次的提交,工作区的内容也会变为新分支的内容。
1 | > git branch |
注意:新建的分支就像新的树枝一样,从原来的主干上分岔出来,所以一个新的分支仍然包含了之前所有的提交记录,其工作区内容也是最新版本的。
git merge 合并分支
在 dev 分支开发完成后,先切换至 main 分支,再使用 git merge <branch>
将其合并到当前分支,会产生一个新的提交记录(没触发快进时)。
在 VSCode 中可以很直观看到不同分支的提交与合并。
也可以使用 git log --oneline --graph
查看分支合并情况。
1 | > git log --oneline --graph |
合并分支时也可能会产生冲突,规则和 pull 一样,需要手动解决冲突后再提交。
git merge --abort
可以放弃合并,回到合并前的状态。
--squash
用于将多个待合并的提交合并为一个。不自动产生一个新提交,而是将变动暂存,需手动发起 commit。
快进模式:
Git 默认采取了 fast-forward(快进模式),当 main 分支上没有新的提交,会直接将 main 的 HEAD 指向合并后最新的提交,而不会产生新的提交记录。--no-ff
参数可以禁用 fast-forward 模式,即使 main 分支上没有新的提交,也会创建一个新的提交记录。
git rebase 变基
git rebase
用于轻松更改一系列提交。
- 编辑之前的提交消息
- 将多个提交合并为一个
- 删除或还原不再必要的提交
因为 rebase 会改变提交记录,所以不要在公共分支和已推送到远端的提交上使用 rebase,否则造成提交记录不一致会出现问题。
转移提交
git rebase <branch>
将当前分支的差异提交记录移动到指定分支的最后,使得提交记录成线性更加整洁。git rebase <a> <b>
将 b 分支的提交记录移动到 a 分支的最后。
merge 在合并时会产生一个新的提交记录,而 rebase 只是单纯的变基,不会产生新的提交记录。
但产生冲突时,需要逐个 commit 解决,解决后(add)使用 git rebase --continue
继续变基。
变基规则:
- 先找到两个分支的最近共同祖先 commit 节点。
- 将当前分支从祖先节点开始的 commit 记录变基到目标分支的最后。
注意:基分支的 HEAD 指针并没有变,仍然需要使用 merge,目的是将指针指向最新的提交。
-i 交互式操作
git rebase -i
启动交互式 rebase 用于操作提交记录。
指定要操作的 commit 范围:
git rebase -i [start] [end]
(start, end] 的提交。git rebase -i [start]^ [end]
[start, end] 的提交。- 不指定 end 则表示到最新提交。
git rebase -i HEAD~3
最近 3 次提交。
1 | > git rebase -i HEAD~3 |
修改每条记录前的命令(pick),可以对提交记录进行操作:
pick
使用该提交,在变基进行时重新排列 pick 命令的顺序会更改提交的顺序。reword
使用该提交,但变基过程会暂停,可以修改提交注释。edit
与 reword 类似,但可以完全修改提交,可以创建更多提交后再继续变基。squash
使用该提交,但将其合并到前一个提交,可以修改提交注释。fixup
与 squash 类似,但不保留提交注释。drop
丢弃该提交。删除整行效果一样。exec
执行 shell 命令。break
暂停变基,使用git rebase --continue
以继续。
在编辑器出现异常退出时,可以使用 git rebase --edit-todo
重新打开编辑器。
随时都可以使用 git rebase --abort
放弃变基,回到变基前的状态。
合并提交
squash 合并提交后,保存退出 rebase,会马上弹出 commit 编辑器,并展示了所有要合并的 commit 的注释,可以修改合并后的提交注释。
1 | # This is a combination of 2 commits. |
与 merge 的区别
HEAD 指向,以 dev -> main 为例:
- merge:main 的 HEAD 指向合并后的最新提交,dev 的 HEAD 指向不变(即最后一次修改文件的提交)。没触发快进时,dev 会落后一个提交,反之都指向最新的提交。
- rebase:main 和 dev 的 HEAD 指向都不变。但因为变基了,dev 从公共祖先开始的提交记录会被移动到 main 的最后,所以 main 落后于 dev。main 可以使用 merge 快进到此时的最新提交。
由于 rebase 不改变 HEAD 的特性,可以用于其它分支同步主干分支的最新提交历史。毕竟嫁接过去,主干的提交历史也就成了自己的提交历史。而主干分支的提交历史不会受到影响,因为 HEAD 指向不变。
最后经审核后,再使用 merge 快进到最新提交。这样可以使提交历史尽可能成一个线性,更加整洁。
git cherry-pick 挑选提交
合并一个分支所有的提交使用 merge 或 rebase
而 cherry-pick 可以挑选某个分支的某些提交合并到当前分支。
这对于紧急修复 bug 或者合并某个特定的功能非常有用。
基本使用:git cherry-pick <commit id>
挑选某个提交应用到当前分支。会产生一个新的提交记录。
如下图,将 dev 分支的 fix bug 提交先应用到 main 分支。
git cherry-pick <branch>
表示挑选指定分支的最新提交应用到当前分支。
也可以一次挑选多个提交,git cherry-pick <id1> <id2>
,会产生多个新的提交记录。
应用一系列提交:git cherry-pick <start>..<end>
挑选 (start, end] 的提交。git cherry-pick <start>^..<end>
挑选从 [start, end] 的提交。
发生冲突时需要逐个解决:
--continue
手动解决冲突(git add)后执行,继续应用后续提交。--abort
放弃所有挑选,回到挑选前的状态。--skip
跳过当前存在冲突的提交,应用后续提交。
其它参数:
-e
--edit
应用提交时打开编辑器,可以修改提交信息。-n
只更新工作区和暂存区,不产生新的提交。-s
--signoff
在提交信息的末尾追加一行操作者的签名,表示是谁进行了这个操作。-x
在提交信息的末尾追加一行(cherry picked from commit …),方便以后查到这个提交是如何产生的。
git stash 储藏
当在一个分支上开发到一半,需要切换到另一个分支进行开发时,会爆出错误:
1 | > git switch dev |
这是因为当前工作区有未提交的更改,尽管后续可以在推送前使用 rebase 合并提交,但直接提交开发到一半的代码显然是不合适的。
这时就可以用 git stash
将当前的更改储藏起来,获得一个干净的工作区,然后切换分支。
它会保存暂存区和工作区中已跟踪文件的修改,这些修改会保存在一个栈上。
1 | > git stash |
git stash save [<message>]
可以自定义描述信息。
--all
-a
储藏所有已跟踪和未跟踪的文件。--include-untracked
-u
未跟踪的文件也会被储藏,如新建的文件。--keep-index
-k
默认,只储藏已跟踪的文件。
git stash list
查看储藏列表。
1 | > git stash save "main第一次存储" |
新的储藏会被添加到栈顶,stash@{<num>}
是会变动的id。
恢复储藏
git stash apply [<stash>]
恢复储藏,但不会删除储藏记录,不带参数默认恢复最新的储藏。git stash pop [<stash>]
恢复储藏并删除记录,不带参数默认恢复最新的储藏。
注意:恢复前若有未提交的更改,可能会产生冲突,需要手动解决。
恢复时,暂存区会被清空,其更改会被应用到工作区,需重新 add。可以添加 --index
尝试恢复暂存区的内容。
1 | > git stash pop |
其它命令:
git stash clear
清空所有储藏。git stash drop [-q|--quiet] [<stash>]
删除储藏,不带参数默认删除最新的储藏。-q
静默模式。git stash show [-p] [<stash>]
diff查看储藏的文件。-p
显示详细内容。git stash branch <branch> [<stash>]
创建一个新分支,并将储藏的内容应用到新分支,若分支已存在,则会失败。
注意:储藏栈是所有分支共享的,需注意操作时所在的分支。
git revert 撤销远端提交
对于还未推送到远端的提交,通常回退版本以撤销提交:
1 | # 回退到上一个提交,--hard 放弃工作区的更改 |
这会使 HEAD 指针指向上一个提交
但如果已经推送到远端,git push -f
会破坏提交历史,造成团队成员之间提交历史不一致。
git revert
用于撤销某次远端提交,本质是创建一个新的提交,并对文件进行相反操作(相对于要撤销的提交)。
1 | # 撤销最新的提交 |