内容来源:https://learngitbranching.js.org/?locale=zh_CN
1.1 提交
git add (添加要提交的文件,‘.’表示添加所有文件)
git commit (提交版本,使用-m添加提交的信息)
1.2 创建分支
1.2.1 以当前分支为根基,创建分支并切换(早建分支!多用分支!)
git checkout -b xxx
或者
git branch xxx && git checkout xxx
1.2.2 以指定节点创建分支
git branch xxx master
1.3 分支合并
1.3.1 merge
c0 -> c1 <- master*
-> c2 -> c3 <- xxx
git merge xxx (把xxx合并到当前分支 ,实际不修改各分支记录,只是多一个版本记录,所以merge会出现分叉)
c0 -> c1 --> c4 <- master*
\ /
-> c2 -> c3 <- xxx
1.3.2 rebase
c0 -> c1 <- master
-> c2 -> c3 <- xxx*
git rebase master (当前是xxx,暂存xxx的提交记录,然后以master为根,逐个取xxx的提交记录副本,添加到该分支上,并移动xxx节点)
Rebase 实际上就是取出一系列的提交记录,“复制”它们,然后在另外一个地方逐个的放下去。
Rebase 的优势就是可以创造更线性的提交历史。如果只允许使用 Rebase 的话,代码库的提交历史将会变得异常清晰。
c0 -> c1(master) -> c2' -> c3' <- xxx*
-> c2 -> c3
2.1 HEAD
git 是一个版本树
每次提交会创建一个版本节点,每个节点都有一个hash值
分支是版本树不同节点的引用
HEAD总是指向当前检出节点,有可能是一个引用,也可以是指定的节点
如果想看 HEAD 指向,可以通过 cat .git/HEAD 查看, 如果 HEAD 指向的是一个引用,还可以用 git symbolic-ref HEAD 查看它的指向
2.2 相对引用
通过指定提交记录哈希值的方式在 Git 中移动不太方便。所以你就不得不用 git log 来查查看提交记录的哈希值。
并且哈希值在真实的 Git 世界中也会更长(注:基于 SHA-1,共 40 位)。例如前一关的介绍中的提交记录的哈希值可能是 fed2da64c0efc5293610bdd892f82a58e8cbc5d8。
比较令人欣慰的是,Git 对哈希的处理很智能。你只需要提供能够唯一标识提交记录的前几个字符即可。因此可以仅输入fed2 而不是上面的一长串字符。
正如我前面所说,通过哈希值指定提交记录很不方便,所以 Git 引入了相对引用。
使用相对引用的话,你就可以从一个易于记忆的地方(比如 bugFix 分支或 HEAD)开始计算。
相对引用非常给力,这里我介绍两个简单的用法:
使用 ^ 向上移动 1 个提交记录: git checkout master^
使用 ~
2.3 强制修改分支位置
我使用相对引用最多的就是移动分支。可以直接使用 -f 选项让分支指向另一个提交。例如:
c0 -> c1 -> c2 -> c3 -> c4 -> c5 <- master*
git branch -f master HEAD~3
c0 -> c1 -> c2(master*) -> c3 -> c4 -> c5
上面的命令会将 master 分支强制指向 HEAD 的第 3 级父提交。
2.4 撤销变更
在 Git 里撤销变更的方法很多。和提交一样,撤销变更由底层部分(暂存区的独立文件或者片段)和上层部分(变更到底是通过哪种方式被撤销的)组成。我们这个应用主要关注的是后者。
主要有两种方法用来撤销变更 —— 一是 git reset,还有就是 git revert。
2.4.1 Git Reset
c0 -> c1 -> c2 -> c3 -> c4 -> c5 <- master*
git reset HEAD~3
c0 -> c1 -> c2(master*) -> c3 -> c4 -> c5
git reset 通过把分支记录回退几个提交记录来实现撤销改动。你可以将这想象成“改写历史”。git reset 向上移动分支,原来指向的提交记录就跟从来没有提交过一样。
2.4.2 Git Revert
虽然在你的本地分支中使用 git reset 很方便,但是这种“改写历史”的方法对大家一起使用的远程分支是无效的哦!
为了撤销更改并分享给别人,我们需要使用 git revert。
c0 -> c1 -> c2 -> c3 -> c4 -> c5 <- master*
git revert HEAD~3
c0 -> c1 -> c2 -> c3 -> c4 -> c5 -> c2' <- master*
git revert 会在我们要撤销的提交记录后面多了一个新提交!这是因为新提交记录 C2' 引入了更改 —— 这些更改刚好是用来撤销 C2 这个提交的。也就是说 C2' 的状态与 C1 是相同的。
revert 之后就可以把你的更改推送到远程仓库与别人分享啦。(注意:revert每次只能撤销一个节点的修改)
3 整理提交记录
3.1 Git Cherry-pick
本系列的第一个命令是 git cherry-pick, 命令形式为:
git cherry-pick <提交号>...
如果你想将一些提交复制到当前所在的位置(HEAD)下面的话, Cherry-pick 是最直接的方式了。我个人非常喜欢 cherry-pick,因为它特别简单。
c0 -> c1 -> c5 <- master*
-> c2 -> c3 -> c4 <- side
例如:git cherry-pick c2 c4
当前分支是master, 我们只需要提交记录 C2 和 C4,所以 Git 就将被它们抓过来放到当前分支下了。
c0 -> c1 -> c5 -> c2' -> c4' <- master*
\-> c2 -> c3 -> c4 <- side
3.2 交互式的 rebase
当你知道你所需要的提交记录(并且还知道这些提交记录的哈希值)时, 用 cherry-pick 再好不过了 —— 没有比这更简单的方式了。
但是如果你不清楚你想要的提交记录的哈希值呢? 幸好 Git 帮你想到了这一点, 我们可以利用交互式的 rebase —— 如果你想从一系列的提交记录中找到想要的记录, 这就是最好的方法了
交互式 rebase 指的是使用带参数 --interactive 的 rebase 命令, 简写为 -i
如果你在命令后增加了这个选项, Git 会打开一个 UI 界面并列出将要被复制到目标分支的备选提交记录,
它还会显示每个提交记录的哈希值和提交说明,提交说明有助于你理解这个提交进行了哪些更改。
在实际使用命令时,所谓的 UI 窗口一般会在文本编辑器 —— 如 Vim —— 中打开一个文件。
如果是在IDEA中使用git rebase,可以修改参数,使用--interactive,打开一个交互式界面。
c0 -> c1 -> c2 -> c3 -> c4 -> c5 <- master*
git rebase -i HEAD~4
(去掉c3,c5测试节点)
c0 -> c1 -> c2' -> c4' <- master*
\-> c2 -> c3 -> c4 -> c5
4 本地栈式提交
4.1 来看一个在开发中经常会遇到的情况:我正在解决某个特别棘手的 Bug,为了便于调试而在代码中添加了一些调试命令并向控制台打印了一些信息。
这些调试和打印语句都在它们各自的提交记录里。最后我终于找到了造成这个 Bug 的根本原因,解决掉以后觉得沾沾自喜!
最后就差把 bugFix 分支里的工作合并回 master 分支了。你可以选择通过 fast-forward 快速合并到 master 分支上,但这样的话 master 分支就会包含我这些调试语句了。
你肯定不想这样,应该还有更好的方式……
实际我们只要让 Git 复制解决问题的那一个提交记录就可以了。跟之前我们在“整理提交记录”中学到的一样,我们可以使用
git rebase -i
git cherry-pick
来达到目的。
c0 -> c1 -> c2(master) -> c3 -> c4 -> c5 <- dev*
git rebase -i HEAD~3 (去掉c3,c4测试节点)
c0 -> c1 -> c2(master) -> c5' <- dev*
\-> c3 -> c4 -> c5
git branch -f master dev
c0 -> c1 -> c2 -> c5' <- (master)dev*
\-> c3 -> c4 -> c5
4.2 接下来这种情况也是很常见的:你之前在 newImage 分支上进行了一次提交,然后又基于它创建了 caption 分支,然后又提交了一次。
此时你想对的某个以前的提交记录进行一些小小的调整。比如设计师想修改一下 newImage 中图片的分辨率,尽管那个提交记录并不是最新的了。
我们可以通过下面的方法来克服困难:
先用 git rebase -i 将提交重新排序,然后把我们想要修改的提交记录挪到最前
然后用 git commit --amend 来进行一些小修改
接着再用 git rebase -i 来将他们调回原来的顺序
最后我们把 master 移到修改的最前端(用你自己喜欢的方法),就大功告成啦!
当然完成这个任务的方法不止上面提到的一种(我知道你在看 cherry-pick 啦),之后我们会多点关注这些技巧啦,但现在暂时只专注上面这种方法。
最后有必要说明一下目标状态中的那几个' —— 我们把这个提交移动了两次,每移动一次会产生一个 ';
而 C2 上多出来的那个是我们在使用了 amend 参数提交时产生的,所以最终结果就是这样了。
c0 -> c1 -> c2(master) -> c3 -> c4 -> c5 <- dev*
git rebase -i HEAD~2 (修改c3,c4顺序)
c0 -> c1 -> c2(master) -> c3 -> c5' -> c4' <- dev*
\-> c4 -> c5
git commit --amend
/-> c4'' <- dev*
c0 -> c1 -> c2(master) -> c3 -> c5' -> c4'
\-> c4 -> c5
git rebase -i HEAD~2 (修改c3,c4顺序)
/-> c4''' -> c5'' <- dev*
/ /-> c4''
c0 -> c1 -> c2(master) -> c3 -> c5' -> c4'
\-> c4 -> c5
4.3
正如你在上面所见到的,我们可以使用 rebase -i 对提交记录进行重新排序。
只要把我们想要的提交记录挪到最前端,我们就可以很轻松的用 --amend 修改它,然后把它们重新排成我们想要的顺序。
但这样做就唯一的问题就是要进行两次排序,而这有可能造成由 rebase 而导致的冲突。下面还是看看 git cherry-pick 是怎么做的吧。
要在心里牢记 cherry-pick 可以将提交树上任何地方的提交记录取过来追加到 HEAD 上(只要不是 HEAD 上游的提交就没问题)。
c0 -> c1 -> c2(master) -> c3 -> c4 -> c5 <- dev*
git branch -f dev HEAD~2
c0 -> c1 -> c2(master) -> c3(dev*) -> c4 -> c5
git cherry-pick c4
c0 -> c1 -> c2(master) -> c3 -> c4 -> c5
\-> c4' <- dev*
git commit --amend
/-> c4'' <- dev*
c0 -> c1 -> c2(master) -> c3 -> c4 -> c5
\-> c4'
git cherry-pick c4
/-> c4'' -> c5' <- dev*
c0 -> c1 -> c2(master) -> c3 -> c4 -> c5
\-> c4'
4.4 相信通过前面课程的学习你已经发现了:分支很容易被人为移动,并且当有新的提交时,它也会移动。分支很容易被改变,大部分分支还只是临时的,并且还一直在变。
你可能会问了:有没有什么可以永远指向某个提交记录的标识呢,比如软件发布新的大版本,或者是修正一些重要的 Bug 或是增加了某些新特性,有没有比分支更好的可以永远指向这些提交的方法呢?
当然有了!Git 的 tag 就是干这个用的啊,它们可以(在某种程度上 —— 因为标签可以被删除后重新在另外一个位置创建同名的标签)永久地将某个特定的提交命名为里程碑,
然后就可以像分支一样引用了。
更难得的是,它们并不会随着新的提交而移动。你也不能检出到某个标签上面进行修改提交,它就像是提交树上的一个锚点,标识了某个特定的位置。
咱们来看看标签到底是什么样。
咱们先建立一个标签,指向提交记录 C1,表示这是我们 1.0 版本。
git tag v1 C1
很容易吧!我们将这个标签命名为 v1,并且明确地让它指向提交记录 C1,如果你不指定提交记录,Git 会用 HEAD 所指向的位置。
在这个关卡中,按照目标建立两个标签,然后检出到 v1 上面,要注意你会进到分离 HEAD 的状态 —— 这是因为不能直接在v1 上面做 commit。
4.5 Git Describe
由于标签在代码库中起着“锚点”的作用,Git 还为此专门设计了一个命令用来描述离你最近的锚点(也就是标签),它就是 git describe!
Git Describe 能帮你在提交历史中移动了多次以后找到方向;当你用 git bisect(一个查找产生 Bug 的提交记录的指令)找到某个提交记录时,
或者是当你坐在你那刚刚度假回来的同事的电脑前时, 可能会用到这个命令。
git describe 的语法是:
git describe 的语法是:
可以是任何能被 Git 识别成提交记录的引用,如果你没有指定的话,Git 会以你目前所检出的位置(HEAD)。
它输出的结果是这样的:
tag 表示的是离 ref 最近的标签, numCommits 是表示这个 ref 与 tag 相差有多少个提交记录, hash 表示的是你所给定的 ref 所表示的提交记录哈希值的前几位。
当 ref 提交记录上有某个标签时,则只输出标签名称
5.1 多分支rebase
git rebase a b
以a为根,合并到b分支,并切换到b分支
5.2 选择父提交记录
操作符 ^ 与 ~ 符一样,后面也可以跟一个数字。
但是该操作符后面的数字与 ~ 后面的不同,并不是用来指定向上返回几代,而是指定合并提交记录的某个父提交。还记得前面提到过的一个合并提交有两个父提交吧,所以遇到这样的节点时该选择哪条路径就不是很清晰了。
Git 默认选择合并提交的“第一个”父提交,在操作符 ^ 后跟一个数字可以改变这一默认行为。
废话不多说,举个例子
这里有一个合并提交记录。如果不加数字修改符直接检出 master^,会回到第一个父提交记录。
(在我们的图示中,第一个父提交记录是指合并提交记录正上方的那个提交记录。)
git checkout master^
现在来试试选择另一个父提交……
git checkout master^2
使用 ^ 和 ~ 可以自由地在提交树中移动,非常给力:
git checkout HEAD~; git checkout HEAD^2; git checkout HEAD~2
更厉害的是,这些操作符还支持链式操作!试一下这个:
git checkout HEAD~^2~2
6.1
现在我们的 master 分支是比 one、two 和 three 要多几个提交。出于某种原因,我们需要把 master 分支上最近的几次提交做不同的调整后,分别添加到各个的分支上。
one 需要重新排序并删除 C5,two 仅需要重排排序,而 three 只需要提交一次。
慢慢来,你会找到答案的 —— 记得通关之后用 show solution 看看我们的答案哦。
7 远程仓库
7.1 远程仓库并不复杂, 在如今的云计算盛行的世界很容易把远程仓库想象成一个富有魔力的东西, 但实际上它们只是你的仓库在另个一台计算机上的拷贝。
你可以通过因特网与这台计算机通信 —— 也就是增加或是获取提交记录
话虽如此, 远程仓库却有一系列强大的特性
首先也是最重要的的点, 远程仓库是一个强大的备份。本地仓库也有恢复文件到指定版本的能力, 但所有的信息都是保存在本地的。
有了远程仓库以后,即使丢失了本地所有数据, 你仍可以通过远程仓库拿回你丢失的数据。
还有就是, 远程让代码社交化了! 既然你的项目被托管到别的地方了, 你的朋友可以更容易地为你的项目做贡献(或者拉取最新的变更)
git clone
7.2 既然你已经看过 git clone 命令了,咱们深入地看一下发生了什么。
你可能注意到的第一个事就是在我们的本地仓库多了一个名为 o/master 的分支, 这种类型的分支就叫远程分支。由于远程分支的特性导致其拥有一些特殊属性。
远程分支反映了远程仓库(在你上次和它通信时)的状态。这会有助于你理解本地的工作与公共工作的差别 —— 这是你与别人分享工作成果前至关重要的一步.
远程分支有一个特别的属性,在你检出时自动进入分离 HEAD 状态。Git 这么做是出于不能直接在这些分支上进行操作的原因, 你必须在别的地方完成你的工作,
(更新了远程分支之后)再用远程分享你的工作成果。
Git 远程仓库相当的操作实际可以归纳为两点:向远程仓库传输数据以及从远程仓库获取数据。
既然我们能与远程仓库同步,那么就可以分享任何能被 Git 管理的更新(因此可以分享代码、文件、想法、情书等等)。
本节课我们将学习如何从远程仓库获取数据 —— 命令如其名,它就是 git fetch。
你会看到当我们从远程仓库获取数据时, 远程分支也会更新以反映最新的远程仓库。在上一节课程中我们已经提及过这一点了。
判断当前分支A是不是B的祖先
git merge-base --is-ancestor A B