1. 使用分支
在实际工作中,往往需要通过创建不同的分支,实现多个开发任务的并行进行,从而提高团队的协作效率、代码的安全性,并降低开发过程中的冲突和问题。Git 提供了一系列命令以支持这种工作方式,主要有 git branch 命令允许创建、查看、重命名和删除分支,与之紧密集成的还有 git checkout 命令切换分支,以及单独成章的分支整合相关命令等。
1.1. 创建分支
xml
git branch <new_branch_name> [<base_branch_name>]
这将创建一个名为 <new_branch_name> 的新分支。<base_branch_name> 是可选的参数,用于指定基于哪个分支创建新分支,新分支会继承 base branch 的所有提交历史和文件内容,如果省略,默认使用当前所在分支作为新分支的基础。
⚠️ 需要注意的是,这个命令只是创建分支,并没有自动切换到该分支。
1.2. 重命名分支
xml
git branch -m [<old_branch_name>] <new_branch_name>
这个命令的主要目的是将分支重命名。如果提供了 <old_branch_name> 参数,它将会重命名指定的分支;如果没有提供,它将默认重命名当前所在的分支。
1.3. 切换分支
csharp
# 旧版本Git
git checkout <branch_name>
# 新版本Git
git switch <branch_name>
从当前分支切换到其他分支。在新版本的 Git 中,git checkout 仍然可以使用,但如果所使用的 Git 版本支持新的命令 git switch,最好遵循新的推荐做法。
1.4. 创建并切换到新分支 *
ini
# 旧版本Git
git checkout -b <new_branch_name> [<base_branch_name>]
# 新版本Git
git switch -c <new_branch_name> [<base_branch_name>]
这会创建并切换到名为 <new_branch_name> 的新分支,这是一个常用的组合命令,一步完成创建和切换操作。同样的,可以提供 <base_branch_name> 来指定基于哪个分支创建新分支,不提供该参数会默认基于当前分支创建。
1.5. 创建远程分支 *
如果希望在远程仓库中创建分支,可以使用以下命令:
perl
# remote_name 是远程仓库的名称,通常默认为 origin
git push --set-upstream <remote_name> <branch_name>
# 简写形式
git push -u <remote_name> <branch_name>
这将把当前本地分支推送到远程仓库,即在远程仓库中创建同名的分支,同时建立本地分支与远程分支之间的追踪关系,以便在后续的推送操作中简化命令。具体可参考 3.1 小节 git push 相关内容。
1.6. 查看分支
bash
# 列出当前仓库中所有的本地分支,当前分支前面会有一个星号 (*) 标识
git branch
# 列出所有的远程分支
git branch -r
# 列出所有的本地和远程分支
git branch -a
1.7. 删除本地分支
css
# -d 选项是 --delete 的别名,仅当分支已完全合并到其他分支
git branch -d <branch_name>
# -D 选项是 --delete --force 的别名,允许删除分支,无论其合并状态如何
git branch -D <branch_name>
总的来说,想要删除本地分支,可以选择使用 -d 进行安全删除,或者 -D 进行强制删除。下图是使用 -d 选项删除 mynewtest 分支时的报错信息:
⚠️ 如果还在需要删除的分支上,Git 是不允许删除这个分支的,因此需要先切到其他分支进行删除。
1.8. 删除远程分支
perl
# remote_name 是远程仓库的名称,通常默认的远程仓库名称是 origin
# branch-name 是要删除的远程分支的名称
git push <remote_name> --delete <branch-name>
# 或者
git push <remote_name> :<branch_name>
1.9. 同步本地和远程分支 *
如果本地仓库比远程仓库多了很多分支,可以使用以下步骤来同步:
bash
# 首先,获取远程仓库的最新信息,可以看到哪些分支在远程仓库中已经不存在了
git fetch origin --prune
# 或使用简写形式
git fetch origin -p
# 手动逐个删除本地的那些多余分支
git branch -d/---D <branch_name>
Tips:git fetch origin --prune 命令具体参考 3.2 小节 git fetch
如果有很多分支需要删除,手动删除可能会很繁琐。在这种情况下,可以使用 Bash 命令行来自动删除那些在远程仓库中不存在的本地分支:
bash
git fetch origin -p && for branch in $(git branch -vv | grep ': gone]' | awk '{print $1}'); do git branch -D $branch; done
这个命令首先运行 git fetch origin -p 来更新远程分支列表,然后使用 for 循环,遍历所有在远程仓库中已经不存在的本地分支,并删除它们。
2. 代码提交
创建一个新的分支后,接下来对代码的修改通常发生在工作区,也就是项目目录中的文件上。当我们完成了本次代码编辑后,接下来的工作主要分为 3 个步骤,将修改过的文件是用 git add . 添加到暂存区,然后将暂存区的内容使用 git commit 提交到本地仓库,最后将本地仓库使用 git push 推送到远程仓库。这三个步骤确保了代码的变更不仅在本地保存,也在远程仓库中备份和共享。
2.1. 添加到暂存区
在工作区对代码文件进行修改后,这些修改并不会自动被 Git 记录,需要明确的指令来告诉 Git 哪些修改需要被保存,也就是将修改添加到暂存区,然后将暂存区的修改提交到仓库。
添加到暂存区是使用 git add 命令,使用方法如下:
arduino
# 将名为 file_name 的文件添加到暂存区(单个文件),不常用
git add <file_name>
# 将名为 A.text B.text C.text 的文件添加到暂存区(多个文件),不常用
git add A.text B.text C.text
# 当前所有文件添加到暂存区(所有文件),常用
git add .
⚠️ 需要注意,如果文件位于当前工作目录下,可以直接指定文件名,如果文件不在当前目录,需要提供相对于当前目录的路径。举个例子,假设有一个名为 app.js 的文件位于当前的工作目录下,可以使用 git add app.js 添加它到暂存区;如果 app.js 文件在一个名为 src 的子目录下,则需要使用 git add src/app.js;或者,如果想添加 src 目录下所有文件到暂存区,可以使用 git add src/* , * 是通配符。
如果不是要一次性将所有变更都添加到暂存区(可能想将变更拆成几个 commit),其实用命令添加到暂存区的方式不是很方便,可以直接在 vscode 上的源代码管理(source control)侧边栏上操作,在上面可以选择性地将部分修改添加到暂存区,实现对单个文件或多个文件进行更细粒度的提交。如果一定要使用命令添加,也可以先使用 git status 命令(详细参考 2.5 小节的文件状态查看),该命令会列出当前的更改状态,有每个更改文件的所在路径,复制所需文件路径后再使用 git add 即可。
2.2. 提交到本地仓库
一旦文件被添加到暂存区,可以使用 git commit 命令将这些暂存的修改提交到本地仓库。
bash
# 这个命令执行了以下操作:
# 1.将暂存区的所有更改生成一个新的提交
# 2.提交会包含一个指定的提交信息(-m 参数后的内容),用于描述这次提交所做的更改
git commit -m "Commit message describing the changes"
如果不使用 -m 参数,Git 会打开 Vim 文本编辑器来输入提交信息,在编辑器中输入提交信息后保存并关闭编辑器即可完成提交。除此之外,还有一个组合操作命令:
sql
# git add . 和 git commit -m "Commit message describing the changes" 的组合
git commit -am "Commit message describing the changes"
⚠️ 需要注意的是,该组合操作命令与 git add . 还是有一些差别,git add . 实际上有两个作用,不仅可以暂存已修改的文件,还可以跟踪新文件,将新文件也加入暂存区;而这个组合操作则只会作用于已经被跟踪的文件,将所有已经被跟踪的、已修改过的文件添加到暂存区(不包括新添加的文件)。
如果不是特别清楚什么是已经被跟踪的文件,可以查看 2.4 小节对于文件状态的描述。
2.3. 修改已经提交的内容 *
修改已经提交的内容(包括 Commit Message 和 File),主要分为 2 种情况:修改最近的提交和修改更早的提交。
修改最近的提交:
快速提交了代码后,稍后的开发过程中突然发现之前提交消息可能不符合项目的提交消息规范,有拼写错误,或者描述得不够清楚,需要进行修正,可以使用如下命令修改最近的提交消息:
sql
git commit --amend -m "新的提交消息"
如果要修改最近的提交内容,比如提交后发现忘记了添加一个文件,可以像下面这样操作:
sql
$ git commit -m 'initial commit'
$ git add <forgotten_file>
$ git commit --amend
最终只会有一个提交,即第二次提交将代替第一次提交的结果。因为使用 --amend 会使 Git 使用新的提交信息覆盖先前的提交,从效果上来说,就像是旧有的提交从未存在过一样,它并不会出现在仓库的历史中。它最明显的价值是可以稍微改进最近的提交,而不会让"啊,忘了添加一个文件"或者 "小修补,修正笔误"这种提交信息弄乱仓库历史。
⚠️ 需要注意的是,如果在本地使用了 git commit --amend 修改了一个已经推送到远程仓库的提交,那么本地提交历史和远程仓库的提交历史就会不再兼容,因为本地提交历史现在有一个新的提交替换了原来的提交。这就像是本地提交历史和远程仓库的提交历史走出了两条不同的路径,形成了一个"分叉"。在这种情况下,当尝试推送本地更改时,Git 不知道如何合并这两个不兼容的提交历史,所以它会拒绝这个操作,除非明确的告诉它当前想要覆盖远程仓库的提交历史,也就是使用强制推送命令,具体可参考 3.1 小节的 git push 相关内容。
修改更早的提交(使用场景较低):
在开发过程中,commit 了好几次,但是突然发现有文件漏了提交,而且还是前几次的,希望能够将遗漏的文件直接插入已经提交过的历史版本中,或者想修改更早的提交信息,可以通过交互式 rebase 来更改。
2.4. 文件状态查看
Pro Git书籍 第 2.2 小节(Git 基础 - 记录每次更新到仓库)中对工作目录下的每一个文件的状态描述如下:
请记住,你工作目录下的每一个文件都不外乎这两种状态:已跟踪或未跟踪。
已跟踪的文件是指那些被纳入了版本控制的文件,在上一次快照中有它们的记录,在工作一段时间后, 它们的状态可能是未修改,已修改或已放入暂存区。简而言之,已跟踪的文件就是 Git 已经知道的文件。
工作目录中除已跟踪文件外的其它所有文件都属于未跟踪文件,它们既不存在于上次快照的记录中,也没有被放入暂存区。 初次克隆某个仓库的时候,工作目录中的所有文件都属于已跟踪文件,并处于未修改状态,因为 Git 刚刚检出了它们, 而你尚未编辑过它们。
编辑过某些文件之后,由于自上次提交后你对它们做了修改,Git 将它们标记为已修改文件。 在工作时,你可以选择性地将这些修改过的文件放入暂存区,然后提交所有已暂存的修改,如此反复。
总结起来就是:
在 Git 中,工作目录下的每一个文件可以分为已跟踪(tracked)和未跟踪(untracked)两种状态。
已跟踪的文件:这些是被 Git 管理的文件,在之前的提交中有被纳入版本控制。它们又可以分为三种状态:
- 未修改(unmodified):当前文件版本与最后一次提交的版本一致,没有被修改;
- 已修改(modified):当前文件有被修改,但还没有被提交到暂存区;
- 已暂存(staged):已修改的文件被添加到了暂存区,等待被提交到仓库。
未跟踪的文件:这些文件并不在 Git 的版本控制之下,Git 不会跟踪它们的变化。这些文件可能是新创建的,或者之前未被 Git 管理过的文件。
如何查看文件的状态呢?使用 git status 命令。该命令会显示工作区、暂存区和本地仓库中文件的状态信息,帮助开发者了解哪些文件已修改、哪些文件已暂存、哪些文件还未被跟踪,以及当前所处的分支等重要信息。举个例子,现在刚刚把 feature/20231121 分支上的提交推送到远程仓库,紧接着执行 git status,会看到如下信息:
这说明当前工作目录相当干净。换句话说,所有已跟踪文件在上次提交后都未被更改过。 此外,上面的信息还表明,当前目录下没有出现任何处于未跟踪状态的新文件,否则 Git 会在这里列出来。最后,该命令还显示了当前所在分支,并告诉这个分支同远程服务器上对应的分支没有偏离。
然后我们在工作区对 src/app/config 路径下的 menu.ts 文件进行修改,并新增一个 test.ts 文件,再执行 git status:
可以看到,Git 列出了已修改但还没有添加到暂存区的 menu.ts,以及处于未跟踪状态的 test.ts 新文件。现在我们把这两个文件都添加到暂存区,再执行 git status:
在这里,Git 列出了已经暂存但尚未提交的更改,"Changes to be committed" 表示这些更改已经被暂存,即将包含在下一次的提交中。随后,我们将暂存区的内容提交到本地仓库,执行 git status:
Git 提示当前所在的分支比远程仓库中的同名分支 origin/feature/20231121 超前了一个提交,可以通过 git push 如果将本地的更改推送到远程仓库。当然,有时候当前所在的分支也可能落后于远程仓库中对应的分支,在这种情况下执行 git status 可以看到:
2.5. 交互式 rebase *
交互式 rebase 是一种强大的 Git 工具,允许对提交历史进行重新排序、将多个提交合并为一个、修改提交消息、丢弃某些不需要的提交等操作,可以说是对提交历史进行高度定制。这个功能对于整理提交历史非常有用,提供了对提交历史进行细粒度操作的能力,但需要谨慎使用,因为重写提交历史可能会对团队合作造成困扰。⚠️ 最好在个人分支上进行这种操作,避免影响共享分支的提交历史。
使用方式
bash
# n 是想要包括的最近提交的数量
git rebase -i HEAD~n
# 或者使用 commit_SHA
# 将会展示从指定的 <commit_SHA> 开始,到当前分支最新提交之间的提交历史
# 需要特别注意,展示的提交历史不包括 commit_SHA
git rebase -i <commit_SHA>
执行上述命令即可。例如,git rebase -i HEAD~3 表示 rebase 当前分支的最近三个提交。执行上述命令后,Git 会打开一个交互式界面(Vim 文本编辑器),按时间顺序倒序排列展示选定的最近 n 条提交历史(从上往下第一条是最早的提交,而最后一条则是最近的提交)。每个提交前面都有一个指令,默认给出的是 pick 指令,如下图所示:
可以看到,Vim 文本编辑器中不仅显示了选定的最近 n 条提交历史,还有一些可选的指令介绍,这些指定可以对每个提交执行不同的操作,几种常用的指令含义具体如下:
- pick:保留该提交状态不变。
- reword:修改提交信息。
- squash:将当前选定的提交与前一个提交合并,并生成一个新的合并提交。
- fixup:将当前提交的更改直接合并到前一个提交中。
- edit:暂停 rebase 过程,在该提交之后允许修改文件、添加新文件等。
⚠️ 注意:squash 和 fixup 虽然都用于合并提交,但有些微妙的区别。squash 会生成一个新的合并提交,会提供一个合并后的提交信息供编辑,这个合并后的提交信息默认是当前提交和前一个提交的提交信息;而 fixup 不会保留当前提交的提交信息,它是将当前提交的更改直接合并到前一个提交中。
在编辑器中,可以根据需求选择想要的指令进行操作,具体可以参考下面的 Demo。若需要对提交历史进行重新排序,直接在文本编辑器中修改交换提交的位置顺序即可。
Demo1(修改 commit message)
首先确保当前在要执行交互式 rebase 的分支上,然后输入 git rebase -i HEAD~n 命令以启动交互式 rebase,Vim 文本编辑器会打开,显示了一个类似于下面的列表:
sql
pick abc123 Your commit message here
pick def456 Another commit message
pick 789ghi Yet another commit message
按 i 键以进入 Vim 编辑模式,将想要修改的 commit 前面的指令从 "pick" 改为 "reword" 或者简写为 "r":
sql
reword abc123 Your commit message here
pick def456 Another commit message
pick 789ghi Yet another commit message
在 Vim 编辑模式下,按 Esc 键,然后输入 :wq 以保存并关闭编辑器。随后,另一个 Vim 文本编辑器会打开,允许编辑选定的提交的 commit message,同样的修改完后保存并关闭。⚠️ 对提交消息进行修改会改变提交的哈希值,因此如果选定的提交在之前已经被推送到远程仓库,这会导致本地仓库和远程仓库的历史分叉,最后需要使用强制推送同步至远程仓库。
Demo2(合并提交)
在开发中,常会遇到在一个分支上产生了很多的无效的提交,这种情况下使用 rebase 的交互式模式可以把已经发生的多次提交压缩成一次提交,得到了一个干净的提交历史。其操作和 Demo1 类似,首先确保当前在要执行交互式 rebase 的分支上,然后输入 git rebase -i HEAD~n 命令以启动交互式 rebase,Vim 文本编辑器会打开,其中包含 n 条指定的提交历史,假设如下:
sql
pick a1b2c3d Your commit message 1
pick x4y5z6e Your commit message 2
pick f7g8h9i Your commit message 3
若要压缩多个提交为一个提交,将除第一个 pick 命令外的所有其他行的 pick 更改为 squash 或 s(表示合并):
sql
pick a1b2c3d Your commit message 1
squash x4y5z6e Your commit message 2
squash f7g8h9i Your commit message 3
这将告诉 Git 将第二和第三个提交合并到第一个提交中。保存并关闭编辑器后,另一个 Vim 文本编辑器会打开,Git 将提示编辑合并提交的提交消息,可以选择保留第一个提交的消息或编辑成新的消息,编辑完成后退出即可。最后,如果需要,将更改推送到远程仓库。该操作也会改变提交历史,因此需要强制推送以更新远程分支。
Demo3(修改之前的提交内容)
比如,在开发过程中发现某些文件漏提交到之前的提交中时,可以使用 Git 的交互式 rebase 来将这些遗漏的文件插入到已经提交的历史版本中。假设要将文件添加到倒数第二个提交,首先确保当前在要执行交互式 rebase 的分支上,在命令行中执行 git rebase -i HEAD~2,Git 打开的 Vim 文本编辑器展示的提交历史如下:
sql
pick a1b2c3d Your commit message 1
pick x4y5z6e Your commit message 2
编辑 rebase 列表,在需要插入文件的那个提交行前,修改命令为 edit:
sql
pick a1b2c3d Your commit message 1
edit x4y5z6e Your commit message 2
执行 git add 命令将遗漏的文件添加到暂存区,然后再执行 git commit --amend 命令,将遗漏的文件加入到之前的提交中,此时 Git 会再次打开一个 Vim 文本编辑器来编辑提交信息。因为这里只是更改提交内容,不需要更改提交信息,因此直接退出文本编辑器即可。
这样文件就会被添加到之前的提交中,如果足够细心,可以发现 vscode 右下角显示分支名的地方会变为一个新的哈希值,并且带有括号标识 Rebasing,这是因为在执行交互式 rebase 过程中,Git 可能会暂停并等待用户处理一些事情,例如解决冲突或者编辑提交信息。
当提交信息编辑完成或者冲突解决完成后,只需执行 git rebase --continue 继续 rebase 进程。最后,如果需要,将更改推送到远程仓库。该操作也会改变提交历史,因此需要强制推送以更新远程分支。
3. 代码同步
代码同步指的是确保本地代码库与远程代码库之间的数据保持一致。主要的代码同步方式包括:使用 git push 命令将本地存储库内容上传到远程存储库,以及使用 git fetch 或者 git pull 命令从远程存储库获取和下载内容。
3.1. git push
git push 命令的主要功能是将本地仓库的更改推送到远程仓库。具体来说,当在本地仓库进行了一些更改(例如,提交了新的代码更改)并想要将这些更改共享给其他人或者保存在远程仓库中时,就可以使用 git push 命令。
使用方式
perl
# <remote_name> 是远程仓库的名称
# <branch_name> 是希望推送的本地分支名称
git push <remote_name> <branch_name>
# 例如,如果你想将本地的 dvelop 分支推送到远程仓库的 dvelop 分支,可以使用以下命令:
git push origin dvelop
在默认情况下,git push 会尝试将当前分支推送到同名的远程分支:
- 如果远程分支不存在,那么它将被创建;
- 如果远程分支存在,但是比你的本地分支落后,那么它将被更新;
- 如果远程仓库的分支比本地分支领先,那么 git push 将失败,这是因为 Git 不希望开发者无意中覆盖远程仓库的更改,在这种情况下,如果确定要覆盖远程仓库的提交,可以使用 --force 选项强制推送。
设置上游分支
上游分支是指与本地分支相关联的远程分支。设置上游分支的好处是可以简化 Git 的命令。例如,设置了上游分支后可以直接使用 git push 和 git pull 来推送拉取代码,而不需要每次都指定远程仓库名和分支名。设置上游分支命令如下:
xml
git push --set-upstream <remote_name> <branch_name>
# 简写形式
git push -u <remote_name> <branch_name>
在实际开发中,该命令最实用的场景就是在本地创建新分支后推送到远程,可以按照以下步骤进行操作:
- 创建本地分支:使用 git checkout -b <branch_name> 命令在本地创建一个新分支并切换到该分支;
- 推送到远程:使用 git push -u origin <branch_name> 命令将新分支推送到远程仓库,同时建立本地分支与远程分支之间的追踪关系,以便在后续的推送操作中简化命令。
后续均以简化命令来介绍(即不带 remote_name 和 branch_name)
强制推送
在使用 git commit --amend 修改最近的提交时,以及使用交互式 rebase 时,通常最后都需要使用强制推送将更改同步至远程仓库,强制推送的命令如下:
perl
# 强制推送本地更改并覆盖远程仓库上的对应分支的提交历史
git push --force
# 或者简写为
git push --f
这是一个潜在风险很高的 Git 命令,它会强制将本地分支的提交历史覆盖到远程分支上,这意味着远程分支的历史将被本地历史所替代。这意味着,如果远程仓库有本地没有的提交,那么 git push --force 会导致这些提交被删除并且是不可逆的。因此需要谨慎使用 --force 参数,尤其是在与他人共享的分支中,这可能会导致他人的工作丢失。就算在强制推送之前先使用 git pull 将远程分支当前最新的更改拉取合并到本地分支也是不安全的,因为这个操作到推送之间依然存在时间差,别人可能在这个时间差内又提交了新的更改。
如果确实需要使用强制推送,建议使用一种更安全的替代命令:
csharp
# 若是远程有新的提交,此次强推失败,反之成功
git push --force-with-lease
当执行该命令时,Git 会先检查本地版本的远程分支状态,也就是上次使用 fetch / pull 拉取或推送后的状态与实际的远程分支是否一致。如果它们不一致(也就是说,远程分支已经被其他人更新了),那么强制推送将会失败。这可以防止无意中覆盖其他人的更改。换句话说,--force-with-lease 参数告诉 Git:"我知道我正在尝试强制推送,但如果有人已经推送了一些我还没有的提交,那么请拒绝我的推送"。
⚠️ 在执行强制推送之前,最好确保有备份数据,以防止意外的数据丢失。
3.2. git fetch
git fetch 命令用于从远程仓库拉取最新变更,该命令不会影响当前的本地工作,对当前的代码没有任何影响。因为它只是会将远程仓库的所有更新下载到本地(也就是更新本地的远程跟踪分支),不会将更改自动合并到当前的工作分支中。这使得 git fetch 成为了一种安全的方式来查看别人的新提交,可以先查看更新,然后再决定是否合并这些更改。
使用方式
xml
# 将远程仓库的所有更新下载到本地(更新本地的远程跟踪分支),包括新的提交、新的分支和新的标签
git fetch <remote_name>
# 只取回特定分支的更新
git fetch <remote_name> <branch_name>
本地的远程跟踪分支是远程仓库分支的镜像。例如,远程仓库 origin 有一个 master 分支,那么执行 git fetch origin 命令时,Git 会在本地创建一个叫做 origin/master 的远程跟踪分支,它是远程 master 分支的镜像,包含远程 master 分支的所有提交,当再次执行 git fetch origin 命令时,如果远程的 master 分支有新的提交,那本地的 origin/master 分支也会被更新。⚠️ 需要注意的是,远程跟踪分支只是用来跟踪远程仓库中特定分支的变化,不可以用来进行直接的工作和更改的。因此,就算使用 git checkout origin/main 命令进行分之切换,也看不到远程 main 分支的更新,需要通过其他方式去查看。
--prune 选项
git fetch <remote_name> 命令可以获取到的远程更新只包括新的提交、新的分支和新的标签,如果远程仓库有分支被删除,仅使用该命令是获取不到这个信息的,这可能会导致本地仓库中有一些"僵尸"分支,这些分支在远程仓库中已经不存在了,但在本地仓库中仍然存在。加上 --prune 选项就可以解决这个问题:
sql
# 从远程仓库获取最新的内容,并删除那些在远程仓库中已经被删除的分支的本地追踪引用
git fetch origin --prune
# 简写形式
git fetch origin -p
综上所述,--prune 选项的作用就是清理已经在远程仓库中被删除但本地仍然存在的跟踪分支。例如,在本地创建了一个分支 hotfix/20231129 并推送到了远程,在远程仓库提交 MR 将这个 hotfix 分支合并到 master 的时候勾选了合并后删除该分支,后续在本地使用 git fetch origin -p 命令时可以得知此信息,同时会将本地的 origin/hotfix/20231129 给删除。⚠️ 需要注意的是,该命令仅删除远程跟踪分支,本地分支并不会被删除。如果想要删除本地的 hotfix/20231129 分支,需要手动执行如下命令:
bash
git branch -d hotfix/20231129
# 或者
git branch -D hotfix/20231129
Demo 示例
以下是一个 git fetch origin 的示例输出:
bash
$ git fetch origin
remote: Enumerating objects: 5, done.
remote: Counting objects: 100% (5/5), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), done.
From https://github.com/user/repo
3a4b4fb..8e2ce2d main -> origin/main
* [new branch] feature1 -> origin/feature1
* [new tag] v1.0.0 -> v1.0.0
在使用 git fetch 命令获取最新的远程仓库状态之后,可以使用 7.2 小节比较两个分支的差异方法来审查当前本地分支和远程分支的提交差异。确认没有问题后,可以使用 git merge 命令将远程分支合并到当前工作分支,使本地分支和远程仓库保持同步。以上述 git fetch origin 的示例输出为基础,假设当前工作分支就是 main 分支:
bash
# 查看远程更新的提交
git log HEAD..origin/main
# 合并远程的更新到当前分支
git merge origin/main
3.3. git pull
git pull 命令实际上是 git fetch 和 git merge 的组合,它从远程仓库获取最新更新,并尝试自动合并到当前分支。
4. 撤销提交
在实际开发中,有时候可能需要处理提交错误、回退到先前的代码状态或者撤销不必要的更改。撤销提交是版本控制中的一个关键操作,主要的撤销提交的工具就是 reset 和 revert。这两者在实现上有一些不同,适用于不同的应用场景。
4.1. reset 重置
git reset 命令是通过移动当前分支的 HEAD 指针来回滚到特定的提交,它有几种模式:
- --soft 模式:仅重置分支指针,保留暂存区和工作目录中的更改。
- --mixed 模式 (默认模式):重置分支指针和暂存区,但保留工作目录中的更改。
- --hard 模式:彻底重置分支指针、暂存区和工作目录,删除所有更改。
使用方式
perl
# 撤销最后一次提交,但将更改保留在暂存区
git reset --soft HEAD~1
# 撤销最后一次提交,但将更改还原到工作目录中
git reset HEAD~1
# 撤销最后一次提交,并完全丢弃已暂存和工作目录中的所有更改
git reset --hard HEAD~1
以上命令中的数字 1 可以随意指定为数字 n,代表想要将当前分支的指针(HEAD)向后移动 n 次提交。例如,想要撤销最近 3 次提交,可以将 1 更改为 3。除了使用相对引用 HEAD~n 这种形式,也可以使用 <commit_SHA> 来将分支指针移动到指定的提交,相当于撤销 <commit_SHA> 之后的所有提交:
perl
# 将分支指针移动到指定的提交,但将更改保留在暂存区
git reset --soft <commit_SHA>
# 将分支指针移动到指定的提交,但将更改还原到工作目录中
git reset <commit_SHA>
# 将分支指针移动到指定的提交,并完全丢弃已暂存和工作目录中的所有更改
git reset --hard <commit_SHA>
⚠️ 三种模式需要特别注意 Hard Reset,这种方式会永久丢失本地的未提交更改,请慎重使用。
4.2. revert 还原
相较于 reset 重置会把历史上某个提交及之后所有的提交都移除掉而言,git revert 命令并不会从项目的历史记录中删除任何提交。相反,它通过创建一个新的提交来撤销旧提交所引入的更改,这个新提交的内容与旧提交的内容正好相反。这样,git revert 可以有效地还原旧提交的更改,同时保持项目的提交历史不变,这对于那些已经发布到共享仓库的提交来说这是一个安全的操作。
撤销普通的提交(非合并提交)
xml
# 撤销特定提交所做的更改
git revert <commit_SHA>
# 撤销最新的提交所做的更改
git revert HEAD
# 撤销从 <start_commit_SHA> 到 <end_commit_SHA> 之间的所有提交所做的更改
# 不包括 <start_commit_SHA>
git revert <start_commit_SHA>^..<end_commit_SHA>
在执行 git revert 后,Git 会打开文本编辑器以编写撤销提交的提交消息。保存并关闭编辑器后,Git 将创建一个新的提交来撤销指定提交所做的更改,并保留提交历史。
revert 可以将提交历史中的任何一个提交撤销,但如果试图撤销的提交对后续提交有重要影响,比如后续的提交建立在该提交之上,可能会导致冲突或需要手动解决合并。一般来说,Git 会尽力撤销指定的提交,但若存在依赖关系可能会出现一些复杂情况。最好在实际操作前做好备份,并且根据可能出现的冲突情况谨慎处理。最后,将这些撤销的更改再次推送到远程分支上即可。
撤销合并提交
sql
# n 是数字
git revert -m n <merge-commit-hash>
git revert -m 1 <merge-commit-hash>
当想要撤销一个合并提交时,-m 选项是必需的,其后面的数字用来指定想要撤销哪个父提交的更改。这是因为在一个合并提交中,通常有两个父提交,合并进来的分支是第一个父提交 Parent 1,被合并的分支是第二个父提交 Parent 2,因此通常使用的是 -m 1。如果不指定 -m 选项,Git 就不知道当前想要撤销哪个父提交的更改,所以会给出一个错误信息,如下图所示:
4.3. 两种方式的使用时机
撤销提交主要包括两种场景:撤销 commit 到本地仓库的提交,但是还没有 push 到远程仓库;以及对已经推送到远程共享仓库的提交撤销更改。针对这两种场景,何时使用 reset 以及何时使用 revert,具体可以参考下述内容:
撤销本地的提交: 当在本地进行了一些提交,但是还没有 push 到远程仓库,然后意识到这些提交是错误的,或者就是单纯不想要它们了,使用 reset 和 revert 都可以,但是使用 git revert 会创建一个新的提交,如果不希望在历史记录中看到这个撤销操作,可以选择使用 git reset,这样可以使分支历史记录保持干净简洁;
撤销已经推送到远程共享仓库的提交: 如果已经将一些提交推送到了远程共享仓库,然后意识到这些提交是错误的,到底是使用 reset 还是使用 revert 进行撤销操作需要视具体情况而定:
- 如果需要撤销的提交后面没有其他人的提交,和撤销本地提交一样,使用 reset 还是使用 revert 取决于是否想在历史记录中看到这个撤销操作;
- 如果需要撤销的提交后面有其他人的提交,那么应该使用 git revert 来撤销,因为 reset 重置会把历史上某个提交及之后所有的提交都移除掉,这可能会影响其他人的工作。
⚠️ 不管是使用哪种方式进行撤销,操作时都需要谨慎处理,在不确定的情况下,最好先做一次备份。
5. 暂存未提交的内容
有时,我们想暂存未提交的内容,如何暂存呢?针对这个问题的答案是 git stash 命令。git stash 会将未完成的修改保存到一个栈上, 并且可以在任何时候重新应用这些改动(甚至在不同的分支上)。
5.1. 使用场景
场景 1(切换分支)
当正在某个分支上进行功能开发(即当前分支有未提交的修改),但需要切换到其他分支来处理一些紧急任务,此时进行分支切换,Git 可能会给出 "Your local changes would be overwritten by checkout" 的警告,并且分支切换不成功,同时会有提示信息 "Please commit your changes or stash them before you switch branches."
但实际上当前分支上正在开发的功能并未完成,你不想直接提交未完成的工作,因为这会导致不必要的 commit 记录。在这种情况下,可以使用 git stash 来暂存未提交的修改,以便稍后再恢复它们。
📚 Tips:在 Git 中,如果在一个分支上做了修改但尚未提交,并尝试切换到另一个分支,Git 会根据修改的内容以及目标分支的状态来决定是否可以顺利切换。如果 Git 检测到切换分支可能会导致这些修改被覆盖或者与目标分支冲突,就会给出上述的警告信息。如果 Git 检测到这些修改可能不会导致冲突,就能成功切换到新的分支,并且自动将未提交的修改带入新的分支。
场景 2(临时保存工作目录状态)
如果当前正在进行一些实验性的修改,但突然需要正式开发功能。此时不想丢弃当前的实验性的修改,因为后续可能会用到,但同时又想让当前分支保持一个干净状态,以便进行功能开发,也可以使用 git stash 将当前的修改保存起来,完成功能开发后,再恢复它们。
实际上,不管是使用场景 1 还是使用场景 2,核心本质都是相同的,那就是临时保存当前工作进度,这就是 git stash 的作用,在有未提交的工作目录和暂存区更改时,提供一种临时保存这些更改的机制,以便可以切换到其他分支、处理紧急任务或者简单地保持工作目录处于干净状态,并且后续可以重新应用这些暂存的更改。
5.2. 基本命令
ini
# 将当前工作目录中所有未提交的更改保存到一个新的存储项中
# 并将工作目录恢复到最后一次提交的状态
git stash push -m "Your stash message"
# 列出所有的存储项
# 每个存储项都有一个唯一的名称,如 stash@{0}、stash@{1}
git stash list
# 将指定的存储项应用到当前分支,并保留存储项
# 如果不指定 stash_name,则会默认使用最新的存储项
git stash apply [<stash_name>]
# 将指定的存储项应用到当前分支,并从存储项列表中删除该存储项
# 如果不指定 stash_name,则会默认使用最新的存储项
git stash pop [<stash_name>]
# 从存储项列表中删除指定的存储项
# 如果不指定 stash_name,则会默认删除最新的存储项
git stash drop [<stash_name>]
# 清空所有存储项
git stash clear
6. 历史查看与分支比较
git log 也是一个非常有用的命令,通过该命令我们能够深入了解每一次代码更改的细节,包括作者、日期、提交消息等。同时,还可以使用该命令来比较两个分支之间的差异,帮助我们了解项目在不同分支上的演变过程。
6.1. 查看 Git 提交历史
使用示例如下图所示:
当运行该命令时,Git 会按时间先后顺序列出项目当前分支的所有提交历史,包括每个提交的作者、日期、提交消息以及提交的哈希值。
上图括号里是指向这个 commit 的引用。具体来说,(HEAD -> develop, origin/develop) 表示当前所在的分支是 develop 分支。并且本地分支 develop 与远程分支 origin/develop 均指向这个 commit 的引用,这意味着本地分支 develop 与远程分支 origin/develop 保持一致,两者是同步的。
常用的命令选项:
- --oneline:以简洁的一行格式显示提交历史,每行包括提交的哈希值(前七个字符)和提交消息
- --graph:以图形化的方式显示所有分支的提交历史
- -n:仅显示最近的 n 个提交,例如 git log -5 将显示最近的 5 个提交
- --all:显示项目所有分支上的提交记录,并将它们按照提交的时间顺序列出
- --since=<date>/--after=<date> 和 --until=<date>/--before=<date>:根据日期范围过滤提交历史
常用的命令参数:
- <commit>:从特定提交开始显示提交历史,其中 <commit> 是提交的哈希值
- <branch>:显示特定分支的提交历史,其中 <branch> 是分支名称
这些选项和参数可以根据需求来组合使用。例如,可以运行 git log --oneline --graph --all 以图形的方式来查看项目的分支结构和每个提交的摘要,或者运行 git log --oneline --graph master 以图形的方式来查看 master 分支上每个提交的摘要。
翻阅提交历史记录:
Git 使用 Less 命令翻页,常用的命令如下:
- 向下滚动一行,使用 j 键或 ↓ 键
- 向上滚动一行,使用 k 键或 ↑ 键
- 要向下滚动一页,使用空格键
- 要向上滚动一页,使用 b 键
less 的翻页命令在 Git 提交历史查看中非常有用,特别是当提交历史较长时。可以使用这些键盘快捷键在提交历史中浏览,并找到需要提交信息。
Tips:当需要更方便地查看 Git 提交历史,有一些图形界面的插件可以帮助更直观地分析提交历史和分支结构。比如 VS Code 插件 GitLens 和 Git Graph 等,它们可以提供更好的 Git 提交历史可视化。
6.2. 比较两个分支的差异 *
git log 命令除了可以用来查看 Git 提交历史以外,还可以用来比较两个分支之间的差异。
bash
git log branch1..branch2
这会显示在 branch2 上存在但不在 branch1 上的提交,包括提交的详细信息。
一个常见的使用场景就是查看本地未推送的 commit 列表,如下图所示:
可以使用 HEAD 进行简写,得到的结果不变,HEAD 是当前分支的引用,指向当前工作目录所在的分支。