Git常用操作详解
本文主要介绍git常用命令的执行情况,重点在于帮助理解git常用命令,属于git入门教程。
# 1、基础篇
# 1.1、提交
Git 仓库中的提交记录保存的是你的目录下所有文件的快照,就像是把整个目录复制,然后再粘贴一样,但比复制粘贴优雅许多!Git 希望提交记录尽可能地轻量,因此在你每次进行提交时,它并不会盲目地复制整个目录。条件允许的情况下,它会将当前版本与仓库中的上一个版本进行对比,并把所有的差异打包到一起作为一个提交记录。
Git 还保存了提交的历史记录。这也是为什么大多数提交记录的上面都有父节点的原因 —— 我们会在图示中用箭头来表示这种关系。对于项目组的成员来说,维护提交历史对大家都有好处。
你可以把提交记录看作是项目的快照。提交记录非常轻量,可以快速地在这些提交记录之间切换!
如下展示了一个(小型)Git代码库。当前有两个提交记录,初始提交 C0
和其后可能包含某些有用修改的提交 C1
,使用git commit
后创建了一个新的提交记录。
上面修改了代码库,并把这些修改保存成了一个提交记录 C2
。C2
的父节点是 C1
,父节点是当前提交中变更的基础。
# 1.2、分支
Git 的分支也非常轻量。它们只是简单地指向某个提交纪录 —— 仅此而已。所以许多 Git 爱好者传颂:
早建分支!多用分支!
这是因为即使创建再多的分支也不会造成储存或内存上的开销,并且按逻辑分解工作到不同的分支要比维护那些特别臃肿的分支简单多了。在将分支和提交记录结合起来后,会看到两者如何协作。现在只要记住使用分支其实就相当于在说:“我想基于这个提交以及它所有的父提交进行新的工作。”
使用git branch
创建一个名为newImage的分支。新创建的分支 newImage
指向的是提交记录 C1
。
创建新分支后直接提交会出现下面情况。
main
分支前进了,但 newImage
分支还待在原地,这是因为没有在这个新分支上看到 main
分支上的那个星号,这表示当前所在的分支是 main
。
使用git checkout <name>
来切换到新的分支上。
使用git checkout newImage; git commit
就能先切换分支然后将修改保存到新的分支上了。也可以直接使用git checkout -b newImage
一次完成创建分支和切换分支的操作。
注意:在 Git 2.23 版本中,引入了一个名为 git switch
的新命令,最终会取代 git checkout
,因为 checkout
作为单个命令有点超载(它承载了很多独立的功能)。 由于现在很多人还无法使用 switch
,后面仍然使用 checkout
而不是 switch
# 1.3、合并
已经知道如何提交以及如何使用分支了,接下来看看如何将两个分支合并到一起。就是说新建一个分支并在其上开发某个新功能,开发完成后再合并回主线。
# 1.3.1、git merge
先来看一下第一种方法 —— git merge
。在 Git 中合并两个分支时会产生一个特殊的提交记录,它有两个父节点。翻译成自然语言相当于:“我要把这两个父节点本身及它们所有的祖先都包含进来。”
下面有两个分支,每个分支上各有一个独有的提交。这意味着没有一个分支包含了修改的所有内容。可以通过合并这两个分支来解决这个问题。使用git merge bugFix
(后跟合并谁)把 bugFix
合并到 main
里。
下图中main
现在指向了一个拥有两个父节点的提交记录。假如从 main
开始沿着箭头向上看,在到达起点的路上会经过所有的提交记录。这意味着 main
包含了对代码库的所有修改。还有每个分支都有不同的颜色,而每个提交记录的颜色是所有包含该提交记录的分支的颜色混合之后的颜色。所以,main
分支的颜色被混入到所有的提交记录,但 bugFix
没有。
使用git checkout bugFix; git merge main
再把 main
分支合并到 bugFix
。
如下图因为 main
继承自 bugFix
,Git 什么都不用做,只是简单地把 bugFix
移动到 main
所指向的那个提交记录。现在所有提交记录的颜色都一样了,这表明每一个分支都包含了代码库的所有修改。
# 1.3.2、git rebase
第二种合并分支的方法是 git rebase
。Rebase 实际上就是取出一系列的提交记录,复制它们然后在另外一个地方逐个的放下去。
Rebase
的优势就是可以创造更线性的提交历史,这听上去有些难以理解。如果只允许使用 Rebase
的话,代码库的提交历史将会变得异常清晰。
下面准备了两个分支,注意当前所在的分支是 bugFix
,想要把 bugFix
分支里的工作直接移到 main
分支上。移动以后会使得两个分支的功能看起来像是按顺序开发,但实际上它们是并行开发的。
下图是使用git rebase main
(后跟合并到谁)后,现在 bugFix 分支上的工作在 main 的最顶端,同时我们也得到了一个更线性的提交序列。
注意,提交记录 C3 依然存在,而 C3' 是我们 Rebase 到 main 分支上的 C3 的副本。
现在唯一的问题就是 main 还没有更新。
下图是使用git rebase bugFix
后,由于 bugFix
继承自 main
,所以 Git 只是简单的把 main
分支的引用向前移动了一下而已。
# 2、高级篇
# 2.1、在提交树上移动
在接触 Git 更高级功能之前,有必要先学习在你项目的提交树上前后移动的几种方法。一旦熟悉了如何在 Git 提交树上移动,你驾驭其它命令的能力也将水涨船高!
# 2.1.1、分离的HEAD
首先看一下 “HEAD”。 HEAD 是一个对当前检出记录的符号引用 —— 也就是指向你正在其基础上进行工作的提交记录。HEAD 总是指向当前分支上最近一次提交记录。大多数修改提交树的 Git 命令都是从改变 HEAD 的指向开始的。HEAD 通常情况下是指向分支名的(如 bugFix)。在你提交时,改变了 bugFix 的状态,这一变化通过 HEAD 变得可见。
下面实际操作观察提交前后 HEAD 的位置。下面是提交前的。
使用git checkout C1; git checkout main; git commit; git checkout C2
演示。
下面HEAD 指向了 main
,说明是随着提交而向前移动。
上面用到的git checkout C1
是分离的HEAD,分离的 HEAD 就是让其指向了某个具体的提交记录而不是分支名。
在命令执行之前的状态是:HEAD -> main -> C1 在命令执行之后的状态是:HEAD -> C1
# 2.1.2、相对引用
通过指定提交记录哈希值的方式在 Git 中移动不太方便。在实际应用时,并没有像上图中这么漂亮的可视化提交树供你参考,所以你就不得不用 git log
来查看提交记录的哈希值。并且哈希值在真实的 Git 世界中也会更长,共40位。比较令人欣慰的是,Git 对哈希的处理很智能。你只需要提供能够唯一标识提交记录的前几个字符即可。
通过哈希值指定提交记录很不方便,所以 Git 引入了相对引用。这个就很厉害了!使用相对引用的话,你就可以从一个易于记忆的地方(比如 bugFix
分支或 HEAD
)开始计算。
相对引用非常给力,这里介绍两个简单的用法:
- 使用
^
向上移动 1 个提交记录 - 使用
~<num>
向上移动多个提交记录,如~3
首先看看操作符 (^)。把这个符号加在引用名称的后面,表示让 Git 寻找指定提交记录的父提交。
使用命令前。
下面是使用git checkout main^
命令后。
也可以将 HEAD
作为相对引用的参照。下面就用 HEAD
在提交树中向上移动几次。
下面是使用git checkout C3; git checkout HEAD^; git checkout HEAD^; git checkout HEAD^
后。
可以一直使用 HEAD^
向上移动。
想在提交树中向上移动很多步的话,可以用 ~
操作符,后面可以跟一个数字(可选,不跟数字时与 ^
相同,向上移动一次),指定向上移动多少次。
用 ~<num>
一次后退四步。
下面是使用git checkout HEAD~4
后。
多么的简洁 —— 相对引用就是方便啊!
# 2.1.3、强制移动分支
使用相对引用最多的就是移动分支。可以直接使用 -f
选项让分支指向另一个提交。例如:git branch -f main HEAD~3
上面的命令会将 main 分支强制指向 HEAD 的第 3 级父提交,是HEAD的第3级父提交不是main的。 使用前。
使用后。
相对引用为我们提供了一种简洁的引用提交记录 C1
的方式, 而 -f
则容许我们将分支强制移动到那个位置。
# 2.2、撤销变更
在 Git 里撤销变更的方法很多。和提交一样,撤销变更由底层部分(暂存区的独立文件或者片段)和上层部分(变更到底是通过哪种方式被撤销的)组成。这里主要关注的是后者。
主要有两种方法用来撤销变更 —— 一是 git reset
,还有就是 git revert
。
# 2.2.1、git reset
git reset
通过把分支记录回退几个提交记录来实现撤销改动。你可以将这想象成“改写历史”。git reset
向上移动分支,原来指向的提交记录就跟从来没有提交过一样。
下面是使用git reset HEAD~1
后。
Git 把 main 分支移回到 C1
;现在我们的本地代码库根本就不知道有 C2
这个提交了。(在reset后, C2
所做的变更还在,但是处于未加入暂存区状态。)
# 2.2.2、git revert
虽然在你的本地分支中使用 git reset
很方便,但是这种“改写历史”的方法对大家一起使用的远程分支是无效的。
为了撤销更改并分享给别人,我们需要使用 git revert
。
使用前。
使用git revert HEAD
后。
在我们要撤销的提交记录后面居然多了一个新提交!这是因为新提交记录 C2'
引入了更改 —— 这些更改刚好是用来撤销 C2
这个提交的。也就是说 C2'
的状态与 C1
是相同的。
revert 之后就可以把你的更改推送到远程仓库与别人分享啦。
# 3、移动提交记录
上面已经学习了 Git 的常用知识 —— 提交、分支以及在提交树上移动。 这些概念涵盖了 Git 90% 的功能,同样也足够满足开发者的日常需求。
然而, 剩余的 10% 在处理复杂的工作流时(或者当你陷入困惑时)可能就显得尤为重要了。接下来要讨论的这个话题是“整理提交记录” —— 开发人员有时会说“我想要把这个提交放到这里, 那个提交放到刚才那个提交的后面”, 而接下来就讲的就是它的实现方式,非常清晰、灵活,还很生动。
看起来挺复杂, 其实是个很简单的概念。
# 3.1、git cherry-pick
第一个命令是 git cherry-pick
, 命令形式为:
git cherry-pick <提交号>...
如果你想将一些提交复制到当前所在的位置(HEAD
)下面的话, Cherry-pick 是最直接的方式了。我个人非常喜欢 cherry-pick
,因为它特别简单。
这里有一个仓库, 我们想将 side
分支上的工作复制到 main
分支,你立刻想到了之前学过的 rebase
了吧,但是这里先看看 cherry-pick
有什么本领吧。
使用前。
使用git cherry-pick C2 C4
后。
只需要提交记录 C2
和 C4
,所以 Git 就将被它们抓过来放到当前分支下了。 就是这么简单!
# 3.2、交互式的rebase
当你知道你所需要的提交记录(并且还知道这些提交记录的哈希值)时, 用 cherry-pick 再好不过了,没有比这更简单的方式了。但是如果你不清楚你想要的提交记录的哈希值呢? 幸好 Git 帮你想到了这一点, 我们可以利用交互式的 rebase —— 如果你想从一系列的提交记录中找到想要的记录, 这就是最好的方法了。
交互式 rebase 指的是使用带参数 --interactive
的 rebase 命令, 简写为 -i
。如果你在命令后增加了这个选项, Git 会打开一个 UI 界面并列出将要被复制到目标分支的备选提交记录,它还会显示每个提交记录的哈希值和提交说明,提交说明有助于你理解这个提交进行了哪些更改。在实际使用时,所谓的 UI 窗口一般会在文本编辑器 —— 如 Vim中打开一个文件。
当 rebase UI界面打开时, 你能做3件事:
- 调整提交记录的顺序(通过鼠标拖放来完成)
- 删除你不想要的提交(通过切换
pick
的状态来完成,关闭就意味着你不想要这个提交记录) - 合并提交。 这里不介绍这个操作。简而言之,它允许你把多个提交记录合并成一个。
比如使用git rebase -i HEAD~4
后会打开界面让操作。
# 4、杂项
Git 技术、技巧与贴士大集合。
# 4.1、本地栈式提交
来看一个在开发中经常会遇到的情况: 我正在解决某个特别棘手的 Bug,为了便于调试而在代码中添加了一些调试命令并向控制台打印了一些信息。
这些调试和打印语句都在它们各自的提交记录里。最后我终于找到了造成这个 Bug 的根本原因,解决掉以后觉得沾沾自喜!
最后就差把 bugFix
分支里的工作合并回 main
分支了。你可以选择通过 fast-forward 快速合并到 main
分支上,但这样的话 main
分支就会包含我这些调试语句了。你肯定不想这样,应该还有更好的方式……
实际我们只要让 Git 复制解决问题的那一个提交记录就可以了。跟之前我们在“整理提交记录”中学到的一样,我们可以使用
git rebase -i
git cherry-pick
来达到目的。 使用前。
使用git rebase -i HEAD~3; git branch -f main HEAD
后。
# 4.2、提交的技巧1
还有一种常见情况:你之前在 newImage
分支上进行了一次提交,然后又基于它创建了 caption
分支,然后又提交了一次。
此时你想对某个以前的提交记录进行一些小小的调整。比如设计师想修改一下 newImage
中图片的分辨率,尽管那个提交记录并不是最新的了。
可以通过下面的方法来克服困难:
- 先用
git rebase -i
将提交重新排序,然后把我们想要修改的提交记录挪到最前 - 然后用
git commit --amend
来进行一些小修改 - 接着再用
git rebase -i
来将他们调回原来的顺序 - 最后我们把 main 移到修改的最前端(用你自己喜欢的方法),就大功告成啦! 当然完成这个任务的方法不止上面提到的一种。
# 4.3、提交的技巧2
正如上面所见到的,可以使用 rebase -i
对提交记录进行重新排序。只要把我们想要的提交记录挪到最前端,我们就可以很轻松的用 --amend
修改它,然后把它们重新排成我们想要的顺序。
但这样做就唯一的问题就是要进行两次排序,而这有可能造成由 rebase 而导致的冲突。可以使用 git cherry-pick
移动到前面用 --amend
修改后再移动回去。要牢记 cherry-pick 可以将提交树上任何地方的提交记录取过来追加到 HEAD 上(只要不是 HEAD 上游的提交就没问题)。
# 4.4、Git Tags
前面的学习你已经发现了,分支很容易被人为移动,并且当有新的提交时,它也会移动。分支很容易被改变,大部分分支还只是临时的,并且还一直在变。
你可能会问了,有没有什么可以永远指向某个提交记录的标识呢,比如软件发布新的大版本,或者是修正一些重要的 Bug 或是增加了某些新特性,有没有比分支更好的可以永远指向这些提交的方法呢。
Git 的 tag 就是干这个用的,它们可以(在某种程度上 —— 因为标签可以被删除后重新在另外一个位置创建同名的标签)永久地将某个特定的提交命名为里程碑,然后就可以像分支一样引用了。更难得的是,它们并不会随着新的提交而移动。你也不能切换到某个标签上面进行修改提交,它就像是提交树上的一个锚点,标识了某个特定的位置。
下面使用git tag v1 C1
建立一个标签指向C1来表示1.0版本。
如果你不指定提交记录,Git 会用 HEAD
所指向的位置。
# 4.5、Git Describe
由于标签在代码库中起着“锚点”的作用,Git 还为此专门设计了一个命令用来描述离你最近的锚点(也就是标签),它就是 git describe
!
Git Describe 能帮你在提交历史中移动了多次以后找到方向;当你用 git bisect
(一个查找产生 Bug 的提交记录的指令)找到某个提交记录时,或者是当你坐在你那刚刚度假回来的同事的电脑前时, 可能会用到这个命令。
git describe
的语法是:git describe <ref>
<ref>
可以是任何能被 Git 识别成提交记录的引用,如果你没有指定的话,Git 会以你目前所检出的位置(HEAD
)。
它输出的结果是这样的:<tag>_<numCommits>_g<hash>
tag
表示的是离 ref
最近的标签, numCommits
是表示这个 ref
与 tag
相差有多少个提交记录, hash
表示的是你所给定的 ref
所表示的提交记录哈希值的前几位。
当 ref
提交记录上有某个标签时,则只输出标签名称。
如下图,git describe main
会输出:v1_2_gC2
git describe side
会输出:v2_1_gC4
。
# 5、高级话题
# 5.1、选择父提交记录
操作符 ^
与 ~
符一样,后面也可以跟一个数字。但是该操作符后面的数字与 ~
后面的不同,并不是用来指定向上返回几代,而是指定合并提交记录的某个父提交。还记得前面提到过的一个合并提交有两个父提交吧,所以遇到这样的节点时该选择哪条路径就不是很清晰了。
Git 默认选择合并提交的“第一个”父提交,在操作符 ^
后跟一个数字可以改变这一默认行为。这里有一个合并提交记录。如果不加数字修改符直接检出 main^
,会回到第一个父提交记录。(在我们的图示中,第一个父提交记录是指合并提交记录正上方的那个提交记录。)
使用git checkout main^
后。
使用git checkout main^
2后。
使用 ^
和 ~
可以自由地在提交树中移动。
更厉害的是,这些操作符还支持链式操作!比如:git checkout HEAD~^2~2
。
# 5.2、多分支 rebase
现在又很多分支,要把这些分支 rebase 到 main 上,并且领导要求 —— 希望得到有序的提交历史,也就是最终的结果应该是 C6'
在 C7'
上面, C5'
在 C6'
上面,依此类推。
操作前:
操作后:
使用git rebase main bugFix; git rebase bugFix side; git rebase side another; git rebase another main
即可实现。
# 5.3、纠缠不清的分支
现在main
分支是比 one
、two
和 three
要多几个提交。出于某种原因,需要把 main
分支上最近的几次提交做不同的调整后,分别添加到各个的分支上。one
需要重新排序并删除 C5
,two
仅需要重排排序,而 three
只需要提交一次。
操作前:
操作后:
使用git checkout one; git cherry-pick C4 C3 C2; git checkout two; git cherry-pick C5 C4 C3 C2; git branch -f three C2
即可实现。
# 总结
以上是git常用命令其中一部分的详解,适合入门学习,还有部分常用命令后面会更新。
🎁 公众号
小伙伴们大家好,上方扫码关注公众号「大数据技术开发」,与你分享我的成长历程与技术感悟~