马蹄疾 | 2018(农历年)封山之做,和我一块儿嚼烂Git(两万字长文)

本文是『horseshoe·Git专题』系列文章之一,后续会有更多专题推出git

GitHub地址(持续更新):github.com/veedrin/hor…github

博客地址(文章排版真的很漂亮):matiji.cn算法

若是以为对你有帮助,欢迎来GitHub点Star或者来个人博客亲口告诉我数据库

我刚开始接触git的时候,彻底搞不清楚为何这个操做要用这个命令,而那个操做要用那个命令。bash

由于git不是一套注重用户体验的工具,git有本身的哲学。你首先要理解它的哲学,才能真正理解它是如何运做的。服务器

我也是看了前辈写的文章才在某一刻醍醐灌顶。markdown

git有多强大,想必你们都有所耳闻。git有多使人困惑,想必你们也亲身经历过吧。app

总而言之,学习git有两板斧:其一,理解git的哲学;其二,在复杂实践中积累处理问题的经验。缺一不可。编辑器

这篇文章就是第一板斧。工具

做者我本身也还在路上,毕竟,这篇文章也只是个人学习心得,仍然须要大量的实践。

写git有多个角度,反复权衡,我最终仍是决定从命令的角度铺陈,阅读体验也不至于割裂。

因为超过两万字数限制,掘金没法发布完整版,想阅读完整版请移步个人GitHub或者我的博客

困难年岁,共勉。

01) add

git是一个数据库系统,git是一个内容寻址文件系统,git是一个版本管理系统。

没错,它都是。

不过咱们不纠结于git是什么,咱们单刀直入,介绍git命令。

要将未跟踪的文件和已跟踪文件的改动加入暂存区,咱们能够使用git add命令。

不过不少人嫌git add命令不够语义化,毕竟这一步操做是加入暂存区呀。因此git又增长了另一个命令git stage,它们的效果是如出一辙的。

git仓库、工做区和暂存区

进入主题以前,咱们先要介绍一下git仓库、工做区和暂存区的概念。

git仓库

所谓的git仓库就是一个有.git目录的文件夹。它是和git有关的一切故事开始的地方。

能够使用git init命令初始化一个git仓库。

$ git init
复制代码

也能够使用git clone命令从服务器上克隆仓库到本地。

$ git clone git@github.com:veedrin/horseshoe.git
复制代码

而后你的本地就有了一个和服务器上如出一辙的git仓库。

这里要说明的是,clone操做并非将整个仓库下载下来,而是只下载.git目录。由于关于git的一切秘密都在这个目录里面,只要有了它,git就能复原到仓库的任意版本。

工做区(working directory)

工做区,又叫工做目录,就是不包括.git目录的项目根目录。咱们要在这个目录下进行手头的工做,它就是版本管理的素材库。你甚至能够称任何与工做有关的目录为工做区,只不过没有.git目录git是不认的。

暂存区(stage或者index)

stage在英文中除了有舞台、阶段之意外,还有做为动词的准备、筹划之意,所谓的暂存区就是一个为提交到版本库作准备的地方。

那它为何又被称做index呢?由于暂存区在物理上仅仅是.git目录下的index二进制文件。它就是一个索引文件,将工做区中的文件和暂存区中的备份一一对应起来。

stage是表意的,index是表形的。

你能够把暂存区理解为一个猪猪储钱罐。咱们仍是孩子的时候,手里有一毛钱就会丢进储钱罐里。等到储钱罐摇晃的声音变的浑厚时,或者咱们有一个心愿急需用钱时,咱们就砸开储钱罐,一次性花完。

类比到软件开发,每当咱们写完一个小模块,就能够将它放入暂存区。等到一个完整的功能开发完,咱们就能够从暂存区一次性提交到版本库里。

这样作的好处是明显的:

  • 它能够实现更小颗粒度的撤销。
  • 它能够实现批量提交到版本库。

另外,添加到暂存区其实包含两种操做。一种是将还未被git跟踪过的文件放入暂存区;一种是已经被git跟踪的文件,将有改动的内容放入暂存区。

放入暂存区

git默认是不会把工做区的文件放入暂存区的。

$ git status

On branch master
No commits yet
Untracked files:
  (use "git add <file>..." to include in what will be committed)
    a.md
nothing added to commit but untracked files present (use "git add" to track)
复制代码

咱们看到文件如今被标注为Untracked files。表示git目前还没法追踪它们的变化,也就是说它们还不在暂存区里。

那么咱们如何手动将文件或文件夹放入暂存区呢?

$ git add .
复制代码

上面的命令表示将工做目录全部未放入暂存区的文件都放入暂存区。这时文件的状态已经变成了Changes to be committed,表示文件已经放入暂存区,等待下一步提交。每一次add操做其实就是为加入的文件或内容生成一份备份。

下面的命令也能达到相同的效果。

$ git add -A
复制代码

假如我只想暂存单个文件呢?后跟相对于当前目录的文件名便可。

$ git add README.md
复制代码

暂存整个文件夹也是同样的道理。由于git会递归暂存文件夹下的全部文件。

$ git add src
复制代码

把历来没有被标记过的文件放入暂存区的命令是git add,暂存区中的文件有改动也须要使用git add命令将改动放入暂存区。

这时状态变成了Changes not staged for commit

$ git status

On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)
    modified:   a.md
no changes added to commit (use "git add" and/or "git commit -a")
复制代码

针对已经加入暂存区的文件,要将文件改动加入暂存区,还有一个命令。

$ git add -u
复制代码

它和git add -A命令的区别在于,它只能将已加入暂存区文件的改动放入暂存区,而git add -A通吃两种状况。

跟踪内容

假设咱们已经将文件加入暂存区,如今咱们往文件中添加内容,再次放入暂存区,而后查看状态。

$ git status

On branch master
No commits yet
Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
    new file:   a.md
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)
    modified:   a.md
复制代码

哎,忽然变的有意思了。为何一个文件会同时存在两种状态,它是薛定谔的猫么?

想象一下,我想在一个文件中先修复一个bug而后增长一个feather,我确定但愿分两次放入暂存区,这样能够实现颗粒度更细的撤销和提交。可是若是git是基于文件作版本管理的,它就没法作到。

因此git只能是基于内容作版本管理,而不是基于文件。版本管理的最小单位叫作hunk,所谓的hunk就是一段连续的改动。一个文件同时有两种状态也就不稀奇了。

objects

git项目的.git目录下面有一个目录objects,一开始这个目录下面只有两个空目录:infopack

一旦咱们执行了git add命令,objects目录下面就会多出一些东西。

.git/
.git/objects/
.git/objects/e6/
.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391
复制代码

它多出了一个2个字符命名的目录和一个38个字符命名的文件。加起来正好是40个字符。增长一个2个字符的目录是为了提升检索效率。

SHA-1是一种哈希加密算法,它的特色是只要加密的内容相同,获得的校验和也相同。固然这种说法是不许确的,可是碰撞的几率极低。

git除了用内容来计算校验和以外,还加入了一些其余信息,目的也是为了进一步下降碰撞的几率。

重点是,SHA-1算法是根据内容来计算校验和的,跟前面讲的git跟踪内容相呼应。git被称为一个内容寻址文件系统不是没有道理的。

咱们能够作个实验。初始化本地仓库两次,每次都新建一个markdown文件,里面写## git is awesome,记下完整的40个字符的校验和,看看它们是否同样。

.git/objects/56/46a656f6331e1b30988472fefd48686a99e10f
复制代码

若是你真的作了实验,你会发现即使两个文件的文件名和文件格式都不同,只要内容同样,它们的校验和就是同样的,而且就是上面列出的校验和。

如今你们应该对git跟踪内容这句话有更深的理解了。

相同内容引用一个对象

虽然开发者要极力避免这种状况,可是若是一个仓库有多个内容相同的文件,git会如何处理呢?

咱们初始化一个本地仓库,新建两个不一样名的文件,但文件内容都是## git is awesome。运行git add .命令以后看看神秘的objects目录下会发生什么?

.git/objects/56/46a656f6331e1b30988472fefd48686a99e10f
复制代码

只有一个目录,并且校验和跟以前如出一辙。

其实你们确定早就想到了,git这么优秀的工具,怎么可能会让浪费磁盘空间的事情发生呢?既然多个文件的内容相同,确定只保存一个对象,让它们引用到这里来就行了。

文件改动对应新对象

如今咱们猜想工做区的文件和objects目录中的对象是一一对应起来的。但事实真的是这样吗?

咱们初始化一个本地仓库,新建一个markdown文件,运行git add .命令。如今objects目录中已经有了一个对象。而后往文件中添加内容## git is awesome。再次运行git add .命令。

.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391
.git/objects/56/46a656f6331e1b30988472fefd48686a99e10f
复制代码

哎,objects目录中出现了两个对象。第一个对象确定对应空文件。第二个对象咱们太熟悉了,对应的是添加内容后的文件。

再次强调,git是一个版本管理系统,文件在它这里不是主角,版本才是。刚才咱们暂存了两次,能够认为暂存区如今已经有了两个版本(暂存区的版本其实是内容备份,并非真正的版本)。固然就须要两个对象来保存。

文件改动全量保存

初始化一个本地仓库,往工做区添加lodash.js未压缩版本,版本号是4.17.11,体积大约是540KB。运行git add .命令后objects目录下面出现一个对象,体积大约是96KB

.git/objects/cb/139dd81ebee6f6ed5f5a9198471f5cdc876d70
复制代码

咱们对lodash.js文件内容做一个小小的改动,将版本号从4.17.11改成4.17.10,再次运行git add .命令。而后你们会惊奇的发现objects目录下有两个对象了。惊奇的不是这个,而是第二个对象的体积也是大约96KB

.git/objects/cb/139dd81ebee6f6ed5f5a9198471f5cdc876d70
.git/objects/bf/c087eec7e61f106df8f5149091b8790e6f3636
复制代码

明明只改了一个数字而已,第二个对象却仍是这么大。

前面刚夸git会精打细算,怎么到这里就不知深浅了?这是由于多个文件内容相同的状况,引用到同一个对象并不会形成查询效率的下降,而暂存区的多个对象之间若是只保存增量的话,版本之间的查询和切换须要花费额外的时间,这样作是不划算的。

可是全量保存也不是个办法吧。然而git鱼和熊掌想兼得,它也作到了。后面会讲到。

重命名会拆分红删除和新建两个动做

初始化一个本地仓库,新建一个文件,运行git add .命令。而后重命名该文件,查看状态信息。

$ git status

On branch master
No commits yet
Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
    new file:   a.md
Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)
    deleted:    a.md
Untracked files:
  (use "git add <file>..." to include in what will be committed)
    b.md
复制代码

这是因为git的内部机制致使的。生成对象的时候,它发现仓库中叫这个名字的文件不见了,因而标记为已删除,又发现有一个新的文件名是以前没有标记过的,因而标记为未跟踪。由于它只是重命名而已,文件内容并无改变,因此能够共享对象,并不会影响效率。

blob对象

git的一切秘密都在.git目录里。由于它拥有项目的完整信息,因此git必定是把备份存在了某个地方。git把它们存在了哪里,又是如何存储它们的呢?

这些备份信息,git统一称它们为对象。git总共有四种对象类型,都存在.git/objects目录下。

这一次咱们只介绍blob对象。

它存储文件的内容和大小。当开发者把未跟踪的文件或跟踪文件的改动加入暂存区,就会生成若干blob对象。git会对blob对象进行zlib压缩,以减小空间占用。

由于它只存储内容和大小,因此两个文件即使文件名和格式彻底不同,只要内容相同,就能够共享一个blob对象。

注意blob对象和工做目录的文件并非一一对应的,由于工做目录的文件几乎会被屡次添加到暂存区,这时一个文件会对应多个blob对象。

index

仓库的.git目录下面有一个文件,它就是大名鼎鼎的暂存区。

是的,暂存区并非一块区域,只是一个文件,确切的说,是一个索引文件。

它保存了项目结构、文件名、时间戳以及blob对象的引用。

工做区的文件和blob对象之间就是经过这个索引文件关联起来的。

打包

还记得咱们在文件改动全量保存小节里讲到,git鱼和熊掌想兼得么?

又想全量保存,不下降检索和切换速度,又想尽量压榨体积。git是怎么作到的呢?

git会按期或者在推送到远端以前对git对象进行打包处理。

打包的时候保存文件最新的全量版本,基于该文件的历史版本的改动则只保存diff信息。由于开发者不多会切换到较早的版本中,因此这时候效率就能够部分牺牲。

须要注意的是,全部的git对象都会被打包,而不只仅是blob对象。

git也有一个git gc命令能够手动执行打包。

$ git gc

Counting objects: 11, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (9/9), done.
Writing objects: 100% (11/11), done.
Total 11 (delta 3), reused 0 (delta 0)
复制代码

以前的git对象文件都不见了,pack文件夹多了两个文件。其中 .pack 后缀文件存储的就是打包前git对象文件的实际内容。

.git/objects/
.git/objects/info/
.git/objects/info/packs
.git/objects/pack/
.git/objects/pack/pack-99b4704a207ea3cc4924c9f0febb6ea45d4cdfd2.idx
.git/objects/pack/pack-99b4704a207ea3cc4924c9f0febb6ea45d4cdfd2.pack
复制代码

只能说,git gc的语义化不够好。它的功能不只仅是垃圾回收,还有打包。

02) commit

git是一个版本管理系统。它的终极目的就是将项目特定时间的信息保留成一个版本,以便未来的回退和查阅。

咱们已经介绍了暂存区,暂存区的下一步就是版本库,而促成这一步操做的是git commit命令。

提交

暂存区有待提交内容的状况下,若是直接运行git commit命令,git会跳往默认编辑器要求你输入提交说明,你也能够自定义要跳往的编辑器。

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# Initial commit
# Changes to be committed:
# new file: a.md
复制代码

提交以后咱们就看到这样的信息。

[master (root-commit) 99558b4] commit for nothing
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 a.md
复制代码

若是我就是不写提交说明呢?

Aborting commit due to empty commit message.
复制代码

看到没有,提交信息在git中时必填的。

若是提交说明很少,能够加参数-m直接在命令后面填写提交说明。

$ git commit -m "commit for nothing"
复制代码

你甚至能够将加入暂存区和提交一并作了。

$ git commit -am "commit for nothing"
复制代码

可是要注意,和git add -u命令同样,未跟踪的文件是没法提交上去的。

重写提交

amend翻译成中文是修改的意思。git commit --amend命令容许你修改最近的一次commit。

$ git log --oneline

8274473 (HEAD -> master) commit for nothing
复制代码

目前项目提交历史中只有一个commit。我忽然想起来此次提交中有一个笔误,我把高圆圆写成了高晓松(真的是笔误)。可是呢,我又不想为了这个笔误增长一个commit,毕竟它仅仅是一个小小的笔误而已。最重要的是我想悄无声息的改正它,以避免被别人笑话。

这时我就能够使用git commit --amend命令。

首先修改高晓松高圆圆

而后执行git add a.md命令。

最后重写提交。git会跳往默认或者自定义编辑器提示你修改commit说明。固然你也能够不改。

$ git commit --amend

commit for nothing
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# Date: Thu Jan 3 09:33:56 2019 +0800
# On branch master
# Initial commit
# Changes to be committed:
# new file: a.md
复制代码

咱们再来看提交历史。

$ git log --oneline

8a71ae1 (HEAD -> master) commit for nothing
复制代码

提交历史中一样只有一个commit。可是注意哟,commit已经不是以前的那个commit了,它们的校验和是不同的。这就是所谓的重写。

tree对象和commit对象

commit操做涉及到两个git对象。

第一是tree对象。

它存储子目录和子文件的引用。若是只有blob对象,那版本库将是一团散沙。正由于有tree对象将它们的关系登记在册,才能构成一个有结构的版本库。

添加到暂存区操做并不会生成tree对象,这时项目的结构信息存储在index文件中,直到提交版本库操做,才会为每个目录分别生成tree对象。

第二是commit对象。

它存储每一个提交的信息,包括当前提交的根tree对象的引用,父commit对象的引用,做者和提交者,还有提交信息。所谓的版本,其实指的就是这个commit对象。

做者和提交者一般是一我的,但也存在不一样人的状况。

objects

初始化一个git项目,新建一些文件和目录。

src/
src/a.md
lib/
lib/b.md
复制代码

首先运行git add命令。咱们清楚,这会在.git/objects目录下生成一个blob对象,由于目前两个文件都是空文件,共享一个blob对象。

.git/objects/info/
.git/objects/pack/
.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391
复制代码

如今咱们运行git commit命令,看看有什么变化。

.git/objects/info/
.git/objects/pack/
.git/objects/e6/9de29bb2d1d6434b8b29ae775ad8c2e48c5391
.git/objects/93/810bbde0f994d41ef550324a2c1ad5f9278e19
.git/objects/52/0c9f9f61657ca1e65a288ea77d229a27a8171b
.git/objects/0b/785fa11cd93f95b1cab8b9cbab188edc7e04df
.git/objects/49/11ff67189d8d5cc2f94904fdd398fc16410d56
复制代码

有意思。刚刚只有一个blob对象,怎么忽然蹦出来这么多git对象呢?想想以前说的commit操做涉及到两个git对象这句话,有没有可能多出来的几个,分别是tree对象和commit对象?

咱们使用git底层命令git cat-file -t <commit>查看这些对象的类型发现,其中有一个blob对象,三个tree对象,一个commit对象。

这是第一个tree对象。

$ git cat-file -t 93810bb

tree
复制代码
$ git cat-file -p 93810bb

100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391    b.md
复制代码

这是第二个tree对象。

$ git cat-file -t 520c9f9

tree
复制代码
$ git cat-file -p 520c9f9

100644 blob e69de29bb2d1d6434b8b29ae775ad8c2e48c5391    a.md
复制代码

这是第三个tree对象。

$ git cat-file -t 0b785fa

tree
复制代码
$ git cat-file -p 0b785fa

040000 tree 93810bbde0f994d41ef550324a2c1ad5f9278e19    lib
040000 tree 520c9f9f61657ca1e65a288ea77d229a27a8171b    src
复制代码

能够看到,提交时每一个目录都会生成对应的tree对象。

而后咱们再来看commit对象。

$ git cat-file -t 4911ff6

commit
复制代码
$ git cat-file -p 4911ff6

tree 0b785fa11cd93f95b1cab8b9cbab188edc7e04df
parent c4731cfab38f036c04de93facf07cae496a124a2
author veedrin <veedrin@qq.com> 1546395770 +0800
committer veedrin <veedrin@qq.com> 1546395770 +0800
commit for nothing
复制代码

能够看到,commit会关联根目录的tree对象,由于关联它就能够关联到全部的项目结构信息,所谓擒贼先擒王嘛。它也要关联父commit,也就是它的上一个commit,这样才能组成版本历史。固然,若是是第一个commit那就没有父commit了。而后就是commit说明和一些参与者信息。

咱们总结一下,git add命令会为加入暂存区的内容或文件生成blob对象,git commit命令会为加入版本库的内容或文件生成tree对象和commit对象。至此,四种git对象咱们见识了三种。

为啥不在git add的时候就生成tree对象呢?

所谓暂存区,就是不必定会保存为版本的信息,只是一个准备的临时场所。git认为在git add的时候生成tree对象是不够高效的,彻底能够等版本定型时再生成。而版本定型以前的结构信息存在index文件中就行了。

03) branch

分支是使得git如此灵活的强大武器,正是由于有巧妙的分支设计,众多的git工做流才成为可能。

如今咱们已经知道commit对象其实就是git中的版本。那咱们要在版本之间切换难道只能经过指定commit对象毫无心义的SHA-1值吗?

固然不是。

在git中,咱们能够经过将一些指针指向commit对象来方便操做,这些指针即是分支。

分支在git中是一个模棱两可的概念。

你能够认为它仅仅是一个指针,指向一个commit对象节点。

你也能够认为它是指针指向的commit对象节点追溯到某个交叉节点之间的commit历史。

严格的来讲,一种叫分支指针,一种叫分支历史。不过实际使用中,它们在名字上经常不做区分。

因此咱们须要意会文字背后的意思,它究竟说的是分支指针仍是分支历史。

大多数时候,它指的都是分支指针。

master分支

刚刚初始化的git仓库,会发现.git/refs/heads目录下面是空的。这是由于目前版本库里尚未任何commit对象,而分支必定是指向commit对象的。

一旦版本库里有了第一个commit对象,git都会在.git/refs/heads目录下面自动生成一个master文件,它就是git的默认分支。不过它并不特殊,只是它充当的是一个默认角色而已。

刚刚初始化的git仓库会显示目前在master分支上,其实这个master分支是假的,.git/refs/heads目录下根本没有这个文件。只有等提交历史不为空时才有会真正的默认分支。

咱们看一下master文件到底有什么。

$ cat .git/refs/heads/master

6b5a94158cc141286ac98f30bb189b8a83d61347
复制代码

40个字符,明显是某个git对象的引用。再识别一下它的类型,发现是一个commit对象。

$ git cat-file -t 6b5a941

commit
复制代码

就这么简单,所谓的分支(分支指针)就是一个指向某个commit对象的指针。

HEAD指针

形象的讲,HEAD就是景区地图上标注你当前在哪里的一个图标。

你当前在哪里,HEAD就在哪里。它通常指向某个分支,由于通常咱们都会在某个分支之上。

由于HEAD是用来标注当前位置的,因此一旦HEAD的位置被改变,工做目录就会切换到HEAD指向的分支。

$ git log --oneline

f53aaa7 (HEAD -> master) commit for nothing
复制代码

可是也有例外,好比我直接签出到某个没有分支引用的commit。

$ git log --oneline

cb64064 (HEAD -> master) commit for nothing again
324a3c0 commit for nothing
复制代码
$ git checkout 324a3c0

Note: checking out '324a3c0'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:
  git checkout -b <new-branch-name>
HEAD is now at 324a3c0... commit for nothing
复制代码
$ git log --oneline

324a3c0 commit for nothing
复制代码

这个时候的HEAD就叫作detached HEAD

要知道,只有在初始提交和某个分支之间的commit才是有效的。当你的HEAD处于detached HEAD状态时,在它之上新建的commit没有被任何分支包裹。一旦你切换到别的分支,这个commit(可能)不再会被引用到,最终会被垃圾回收机制删除。所以这是很危险的操做。

324a3c0 -- cb64064(master)
   \
 3899a24(HEAD)
复制代码

若是不当心这么作了,要么在原地新建一个分支,要么将已有的分支强行移动过来。确保它不会被遗忘。

死亡不是终结,遗忘才是。——寻梦环游记

建立

除了默认的master分支,咱们能够随意建立新的分支。

$ git branch dev
复制代码

一个dev分支就建立好了。

查看

或许有时咱们也想要查看本地仓库有多少个分支,由于在git中新建分支实在是太容易了。

$ git branch

  dev
* master
复制代码

当前分支的前面会有一个*号标注。

同时查看本地分支和远端分支引用,添加-a参数。

$ git branch -a

* master
  remotes/origin/HEAD -> origin/master
  remotes/origin/master
复制代码

删除

通常分支合并完以后就再也不须要了,这时就要将它删除。

$ git branch -d dev

Deleted branch dev (was 657142d).
复制代码

有时候咱们会获得不同的提示。

$ git branch -d dev

error: The branch 'dev' is not fully merged.
If you are sure you want to delete it, run 'git branch -D dev'.
复制代码

这是git的一种保护措施。is not fully merged是针对当前分支来讲的,意思是你要删除的分支还有内容没有合并进当前分支,你肯定要删除它吗?

大多数时候,固然是要的。

$ git branch -D dev

Deleted branch dev (was 657142d).
复制代码

-D--delete --force的缩写,你也能够写成-df

须要注意的是,删除分支仅仅是删除一个指针而已,并不会删除对应的commit对象。不过有可能删除分支之后,这一串commit对象就没法再被引用了,从而被垃圾回收机制删除。

04) checkout

在git中,暂存区里有若干备份,版本库里有若干版本。留着这些东西确定是拿来用的对吧,怎么用呢?当我须要哪一份的时候我就切换到哪一份。

git checkout命令就是用来干这个的,官方术语叫作签出

怎么理解checkout这个词呢?checkout本来指的是消费结束服务员要与你核对一下帐单,结完帐以后你就能够走了。在git中核对指的是diff,比较两份版本的差别,若是发现没有冲突那就能够切换过来了。

底层

咱们知道HEAD指针指向当前版本,而git checkout命令的做用是切换版本,它们确定有所关联。

目前HEAD指针指向master分支。

$ cat .git/HEAD

ref: refs/heads/master
复制代码

若是我切换到另外一个分支,会发生什么?

$ git checkout dev

Switched to branch 'dev'
复制代码
$ cat .git/HEAD

ref: refs/heads/dev
复制代码

果真,git checkout命令的原理就是改变了HEAD指针。而一旦HEAD指针改变,git就会取出HEAD指针指向的版本做为当前工做目录的版本。签出到一个没有分支引用的commit也是同样的。

符号

在进入正题以前,咱们要先聊聊git中的两个符号~^

若是咱们要从一个分支切换到另外一个分支,那还好说,足够语义化。可是若是咱们要切换到某个commit,除了兢兢业业的找到它的SHA-1值,还有什么办法快速的引用到它呢?

好比说咱们能够根据commit之间的谱系关系快速定位。

$ git log --graph --oneline

* 4e76510 (HEAD -> master) c4
*   2ec8374 c3
|\  
| * 7c0a8e3 c2
* | fb60f51 c1
|/  
* dc96a29 c0
复制代码

~的做用是在纵向上定位。它能够一直追溯到最先的祖先commit。若是commit历史有分叉,那它就选第一个,也就是主干上的那个。

^的做用是在横向上定位。它没法向上追溯,可是若是commit历史有分叉,它能定位全部分叉中的任意一支。

HEAD不加任何符号、加~0 符号或者加^0符号时,定位的都是当前版本

这个不用说,定位当前commit。

$ git rev-parse HEAD

4e76510fe8bb3c69de12068ab354ef37bba6da9d
复制代码

它表示定位第零代父commit,也就是当前commit。

$ git rev-parse HEAD~0

4e76510fe8bb3c69de12068ab354ef37bba6da9d
复制代码

它表示定位当前commit的第零个父commit,也就是当前commit。

$ git rev-parse HEAD^0

4e76510fe8bb3c69de12068ab354ef37bba6da9d
复制代码

~符号数量的堆砌或者~数量的写法定位第几代父commit

$ git rev-parse HEAD~~

fb60f519a59e9ceeef039f7efd2a8439aa7efd4b
复制代码
$ git rev-parse HEAD~2

fb60f519a59e9ceeef039f7efd2a8439aa7efd4b
复制代码

^数量的写法定位第几个父commit

注意,^定位的是当前基础的父commit。

$ git rev-parse HEAD^

2ec837440051af433677f786e502d1f6cdeb0a4a
复制代码
$ git rev-parse HEAD^1

2ec837440051af433677f786e502d1f6cdeb0a4a
复制代码

由于当前commit只有一个父commit,因此定位第二个父commit会失败。

$ git rev-parse HEAD^2

HEAD^2
fatal: ambiguous argument 'HEAD^2': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'
复制代码

~数量^数量的写法或者^数量^数量的写法定位第几代父commit的第几个父commit

当前commit的第一代父commit的第零个父commit,意思就是第一代父commit咯。

$ git rev-parse HEAD~^0

2ec837440051af433677f786e502d1f6cdeb0a4a
复制代码

好比这里定位的是当前commit的第一代父commit的第一个父commit。再次注意,^定位的是当前基础的父commit。

$ git rev-parse HEAD~^1

fb60f519a59e9ceeef039f7efd2a8439aa7efd4b
复制代码

这里定位的是当前commit的第一代父commit的第二个父commit。

$ git rev-parse HEAD~^2

7c0a8e3a325ce1b5a1cdeb8c89bef1ecf17c10c9
复制代码

一样,定位到一个不存在的commit会失败。

$ git rev-parse HEAD~^3

HEAD~^3
fatal: ambiguous argument 'HEAD~^3': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'
复制代码

~不一样,^2^^的效果是不同的。^2指的是第二个父commit,^^指的是第一个父commit的第一个父commit。

切换到HEAD

git checkout命令若是不带任何参数,默认会加上HEAD参数。而HEAD指针指向的就是当前commit。因此它并不会有任何签出动做。

前面没有提到的是,git checkout命令会有一个顺带效果:比较签出后的版本和暂存区之间的差别。

因此git checkout命令不带任何参数,意思就是比较当前commit和暂存区之间的差别。

$ git checkout

A   b.md
复制代码
$ git checkout HEAD

A   b.md
复制代码

切换到commit

开发者用的最多的固然是切换分支。其实checkout后面不只能够跟分支名,也能够跟commit的校验和,还能够用符号定位commit。

$ git checkout dev

Switched to branch 'dev'
复制代码
$ git checkout acb71fe

Note: checking out 'acb71fe11f78d230b860692ea6648906153f3d27'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:
  git checkout -b <new-branch-name>
HEAD is now at acb71fe... null
复制代码
$ git checkout HEAD~2

Note: checking out 'acb71fe11f78d230b860692ea6648906153f3d27'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:
  git checkout -b <new-branch-name>
HEAD is now at acb71fe... null
复制代码

建立分支并切换

有时候咱们在建立分支时但愿同时切换到建立后的分支,仅仅git branch <branch>是作不到的。这时git checkout命令能够提供一个快捷操做,建立分支和切换分支一步到位。

$ git checkout -b dev

Switched to a new branch 'dev'
复制代码

暂存区文件覆盖工做区文件

git checkout不只能够执行切换commit这种全量切换,它还能以文件为单位执行微观切换。

$ git status

On branch master
No commits yet
Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
    new file:   a.md
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)
    modified:   a.md
复制代码
$ git checkout -- a.md
复制代码
$ git status

On branch master
No commits yet
Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
    new file:   a.md
复制代码

由于暂存区覆盖了工做区,因此工做区的改动就被撤销了,如今只剩下暂存区的改动等待提交。其实至关于撤销文件在工做区的改动,只不过它的语义是覆盖。这个命令没有任何提示,直接撤销工做区改动,要谨慎使用。

咱们看到git提示语中有一个git checkout -- <file>命令,这又是干吗用的呢?

提醒一下,这个参数的写法不是git checkout --<file>,而是git checkout -- <file>

其实它和git checkout <file>的效果是同样的。可是别急,我是说这两个命令想要达到的效果是同样的,但实际效果却有略微的差异。

独立的--参数在Linux命令行中指的是:视后面的参数为文件名。当后面跟的是文件名的时候,最好加上独立的--参数,以避免有歧义。

也就是说,若是该项目正好有一个分支名为a.md(皮一下也不是不行对吧),那加独立的--参数就不会操做分支,而是操做文件。

若是你以为仅仅撤销一个文件在工做区的改动不过瘾,你不是针对谁,你是以为工做区的改动都是垃圾。那么还有一个更危险的命令。

$ git checkout -- .
复制代码

.表明当前目录下的全部文件和子目录。这条命令会撤销全部工做区的改动。

当前commit文件覆盖暂存区文件和工做区文件

若是执行git checkout -- <file>的时候加上一个分支名或者commit的校验和,效果就是该文件的当前版本会同时覆盖暂存区和工做区。至关于同时撤销文件在暂存区和工做区的改动。

$ git status

On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)
    modified:   a.md
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)
    modified:   a.md
复制代码
$ git checkout HEAD -- a.md
复制代码
$ git status

On branch master
nothing to commit, working tree clean
复制代码

最后再提醒一下,运行git checkout命令做用于文件时,即使覆盖内容与被覆盖内容有冲突,也会直接覆盖,因此这真的是闷声打雷式的git命令,必定要抽本身几个耳刮子方可放心食用。

05) merge

能够方便的建立分支是git如此受欢迎的重要缘由,利用git checkout <branch>也让开发者在分支之间穿梭自如。然而百川终入海,其余分支上完成的工做终究是要合并到主分支上去的。

因此咱们来看看git中的合并操做。

首先说明,执行git merge命令以前须要一些准备工做。

$ git merge dev

error: Your local changes to the following files would be overwritten by merge:
    a.md
Please commit your changes or stash them before you merge.
Aborting
复制代码

合并操做以前必须保证暂存区内没有待提交内容,不然git会阻止合并。这是由于合并以后,git会将合并后的版本覆盖暂存区。因此会有丢失工做成果的危险。

至于工做区有待添加到暂存区的内容,git倒不会阻止你。可能git以为它不重要吧。

不过最好仍是保持一个干净的工做区再执行合并操做。

不一样分支的合并

不一样分支指的是要合并的两个commit在某个祖先commit以后开始分叉。

C0 -- C1 -- C2(HEAD -> master)
       \
        C3(dev)
复制代码

git merge后跟合并客体,表示要将它合并进来。

$ git merge dev
复制代码

进行到这里,若是没有冲突,git会弹出默认或者自定义的编辑器,让你填写commit说明。固然它会给你填写一个默认的commit说明。

Merge branch 'dev'

# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
复制代码

为何要你填写commit说明?由于这种状况的git merge实际上会建立一个新的commit对象,记录这次合并的信息,并将当前分支指针移动到它上面来。

C0 -- C1 -- C2 -- C4(HEAD -> master)(merge commit)
       \          /
        \        /
          C3(dev)
复制代码

你们常说不一样分支的git merge操做是一个三方合并,这里的三方指的是合并主体commit合并客体commit以及合并主客体的共同祖先commit

所谓的三方和并究竟是什么意思呢?

git会提取出合并主体commit相对于合并主客体的共同祖先commit的diff与合并客体commit相对于合并主客体的共同祖先commit的diff,再去比较这两份diff有没有修改同一个地方,这里同一个地方的单位是文件的行。若是没有,那就将这两份diff合并生成一个新的commit,当前分支指针向右移。若是有那就要求开发者自行解决。

因此在三方合并中,合并主客体的共同祖先commit只是一个参照物。

合并主体在合并客体的上游

它指的是开发者当前在一个commit节点上,要将同一个分支上更新的commit节点合并进来。

C0 -- C1 -- C2(HEAD -> master) -- C3(dev)
复制代码

这时候会发生什么呢?

这至关于更新当前分支指针,因此只须要将当前分支指针向下游移动,让合并主体与合并客体指向同一个commit便可。这时并不会产生一个新的commit。

用三方合并的概念来理解,合并主体commit合并主客体的共同祖先commit是同一个commit,合并主体commit相对于合并主客体的共同祖先commit的diff为空,合并客体commit相对于合并主客体的共同祖先commit的diff与空diff合并仍是它本身,因此移动过去就好了,并不须要生成一个新的commit。

$ git merge dev

Updating 9242078..631ef3a
Fast-forward
 a.md | 2 ++
 1 file changed, 2 insertions(+)
复制代码
C0 -- C1 -- C2 -- C3(HEAD -> master, dev)
复制代码

这种操做在git中有一个专有名词,叫Fast forward

好比说git pull的时候常常发生这种状况。一般由于远端有更新的commit咱们才须要执行git pull命令,这时远端就是合并客体,本地就是合并主体,远端的分支指针在下游,也会触发Fast forward

合并主体在合并客体的下游

若是合并主体在合并客体的下游,那合并主体自己就包含合并客体,合并操做并不会产生任何效果。

C0 -- C1 -- C2(dev) -- C3(HEAD -> master)
复制代码
$ git merge dev

Already up to date.
复制代码
C0 -- C1 -- C2(dev) -- C3(HEAD -> master)
复制代码

依然用三方合并的概念来理解,这时合并客体commit合并主客体的共同祖先commit是同一个commit,合并客体commit相对于合并主客体的共同祖先commit的diff为空,合并主体commit相对于合并主客体的共同祖先commit的diff与空diff合并仍是它本身。可是这回它都不用移动,由于合并后的diff就是它本身原有的diff。

注意,这时候dev分支指针会不会动呢?

固然不会,git merge操做对合并客体是没有任何影响的。

同时合并多个客体

若是你在git merge后面跟不止一个分支,这意味着你想同时将它们合并进当前分支。

$ git merge aaa bbb ccc

Fast-forwarding to: aaa
Trying simple merge with bbb
Trying simple merge with ccc
Merge made by the 'octopus' strategy.
 aaa.md | 0
 bbb.md | 0
 ccc.md | 0
 3 files changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 aaa.md
 create mode 100644 bbb.md
 create mode 100644 ccc.md
复制代码

git合并有多种策略,上面使用的是'octopus' strategy章鱼策略,由于同时合并的多个分支最终都会指向新的commit,看起来像章鱼的触手。

合并有冲突

git merge操做并不老是如此顺利的。由于有时候要合并的两个分支不是同一我的的,就会有很大的几率遇到两人同时修改文件某一行的状况。git不知道该用谁的版本,它认为两个分支遇到了冲突。

这时就须要开发者手动的解决冲突,才能让git继续合并。

$ git merge dev

Auto-merging a.md
CONFLICT (content): Merge conflict in a.md
Automatic merge failed; fix conflicts and then commit the result.
复制代码

咱们来看一下有冲突的文件是什么样的。

<<<<<<< HEAD
apple
=======
banana
>>>>>>> dev
复制代码

运行git status命令。

$ git status

On branch master
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)
Unmerged paths:
  (use "git add <file>..." to mark resolution)
    both modified:   a.md
no changes added to commit (use "git add" and/or "git commit -a")
复制代码

解决完冲突以后,你须要再提交,告诉git能够完成合并了。

$ git commit -m "fix merge conflict"

U   a.md
error: Committing is not possible because you have unmerged files.
hint: Fix them up in the work tree, and then use 'git add/rm <file>'
hint: as appropriate to mark resolution and make a commit.
fatal: Exiting because of an unresolved conflict.
复制代码

诶,被拒绝了。是否是想起了本身的情场故事?

当咱们解决冲突的时候,工做区已经有改动,因此须要先提交到暂存区。

$ git add a.md
复制代码
$ git commit -m "fix merge conflict"

[master 9b32d4d] fix merge conflict
复制代码

运行git add命令以后你也能够用git merge --continue来替代git commit命令。它会让后面的行为跟没有冲突时的行为表现的同样。

若是你遇到冲突之后不知道如何解决,由于你要去询问你的合做伙伴为何这样改。这时你确定想回到合并之前的状态。

这对git来讲很容易。只须要运行git merge --abort命令便可。

$ git merge --abort
复制代码

该命令没法保证恢复工做区的修改,因此最好是在合并以前先让工做区保持干净。

06) rebase

git merge命令会生成一个新的合并commit。若是你有强迫症,不喜欢这个新的合并commit,git也有更加清爽的方案能够知足你,它就是git rebase命令。

git就是哆啦A梦的口袋。

rebase翻译过来是变基。意思就是将全部要合并进来的commit在新的基础上从新提交一次。

基础用法

git rebase <branch>会计算当前分支和目标分支的最近共同祖先,而后将最近共同祖先与当前分支之间的全部commit都变基到目标分支上,使得提交历史变成一条直线。

C0 -- C1 -- C2 -- C3(master)
       \
        C4 -- C5 -- C6(HEAD -> dev)
复制代码

mergerebase后跟的分支名是不同的。合并是合并进来,变基是变基过去,大家感觉一下。

$ git rebase master

First, rewinding head to replay your work on top of it...
Applying: C4.md
Applying: C5.md
Applying: C6.md
复制代码
C0 -- C1 -- C2 -- C3(master) -- C4' -- C5' -- C6'(HEAD -> dev) \ C4 -- C5 -- C6 复制代码

如今最近共同祖先与当前分支之间的全部commit都被复制到master分支以后,而且将HEAD指针与当前分支指针切换过去。这招移花接木玩的很溜啊,若是你置身其中根本分不出区别。

原来的commit还在吗?还在,若是你记得它的commit校验和,仍然能够切换过去,git会提示你当前处于detached HEAD状态下。只不过没有任何分支指针指向它们,它们已经被抛弃了,剩余的时光就是等待git垃圾回收命令清理它们。

好在,还有人记得它们,不是么?

git rebase完并无结束,由于我变基的目标分支是master,而当前分支是dev。我须要切换到master分支上,而后再合并一次。

$ git checkout master
复制代码
$ git merge dev
复制代码

诶,说来讲去,仍是要合并啊?

别急,这种合并是Fast forward的,并不会生成一个新的合并commit。

若是我要变基的本体分支不是当前分支行不行?也是能够的。

$ git rebase master dev
复制代码

你在任何一个分支上,这种写法均可以将dev分支变基到master分支上,变基完成当前分支会变成dev分支。

裁剪commit变基

变基有点像基因编辑,git有更精确的工具达到你想要的效果。

有了精确的基因编辑技术,妈妈不再用担忧你长的啦。

C0 -- C1 -- C2 -- C3(master)
       \
        C4 -- C5 -- C6(dev)
         \
          C7 -- C8(HEAD -> hotfix)
复制代码
$ git rebase --onto master dev hotfix

First, rewinding head to replay your work on top of it...
Applying: C7.md
Applying: C8.md
复制代码
C0 -- C1 -- C2 -- C3(master) -- C7' -- C8'(HEAD -> hotfix)
       \
        C4 -- C5 -- C6(dev)
         \
          C7 -- C8
复制代码

--onto参数就是那把基因编辑的剪刀。

它会把hotfix分支hotfix分支与dev分支的最近共同祖先之间的commit裁剪下来,复制到目标基础点上。注意,所谓的之间指的都是不包括最近共同祖先commit的范围,好比这里就不会复制C4commit。

$ git rebase --onto master dev

First, rewinding head to replay your work on top of it...
Applying: C7.md
Applying: C8.md
复制代码

若是--onto后面只写两个分支(或者commit)名,第三个分支(或者commit)默认就是HEAD指针指向的分支(或者commit)。

变基冲突解决

变基也会存在冲突的状况,咱们看看冲突怎么解决。

C0 -- C1 -- C2(HEAD -> master)
       \
        C3 -- C4(dev)
复制代码
$ git rebase master dev

First, rewinding head to replay your work on top of it...
Applying: c.md
Applying: a.md add banana
Using index info to reconstruct a base tree...
M   a.md
Falling back to patching base and 3-way merge...
Auto-merging a.md
CONFLICT (content): Merge conflict in a.md
error: Failed to merge in the changes.
Patch failed at 0002 a.md dev
The copy of the patch that failed is found in: .git/rebase-apply/patch
Resolve all conflicts manually, mark them as resolved with
"git add/rm <conflicted_files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".
复制代码

C2和C4同时修改了a.md的某一行,引起冲突。git已经给咱们提示了,大致上和merge的操做一致。

咱们能够手动解决冲突,而后执行git addgit rebase --continue来完成变基。

若是你不想覆盖目标commit的内容,也能够跳过这个commit,执行git rebase --skip。可是注意,这会跳过有冲突的整个commit,而不只仅是有冲突的部分。

后悔药也是有的,执行git rebase --abort,干脆就放弃变基了。

cherry-pick

git rebase --onto命令能够裁剪分支以变基到另外一个分支上。但它依然是挑选连续的一段commit,只是容许你指定头和尾罢了。

别急,git cherry-pick命令虽然是一个独立的git命令,它的效果却仍是变基,并且是commit级别的变基。

git cherry-pick命令能够挑选任意commit变基到目标commit上。你负责挑,它负责基。

用法

只须要在git cherry-pick命令后跟commit校验和,就能够将它应用到目标commit上。

C0 -- C1 -- C2(HEAD -> master)
       \
        C3 -- C4 -- C5(dev)
               \
                C6 -- C7(hotfix)
复制代码

将当前分支切换到master分支。

$ git cherry-pick C6

[master dc342e0] c6
 Date: Mon Dec 24 09:13:57 2018 +0800
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 c6.md
复制代码
C0 -- C1 -- C2 -- C6'(HEAD -> master) \ C3 -- C4 -- C5(dev) \ C6 -- C7(hotfix) 复制代码

C6commit就按原样从新提交到master分支上了。cherry-pick并不会修改原有的commit。

同时挑选多个commit也很方便,日后面叠加就行。

$ git cherry-pick C4 C7

[master ab1e7c7] c4
 Date: Mon Dec 24 09:12:58 2018 +0800
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 c4.md
[master 161d993] c7
 Date: Mon Dec 24 09:14:12 2018 +0800
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 c7.md
复制代码
C0 -- C1 -- C2 -- C4' -- C7'(HEAD -> master)
       \
        C3 -- C4 -- C5(dev)
               \
                C6 -- C7(hotfix)
复制代码

若是这多个commit正好是连续的呢?

$ git cherry-pick C3...C7

[master d16c42e] c4
 Date: Mon Dec 24 09:12:58 2018 +0800
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 c4.md
[master d16c42e] c6
 Date: Mon Dec 24 09:13:57 2018 +0800
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 c6.md
[master a4d5976] c7
 Date: Mon Dec 24 09:14:12 2018 +0800
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 c7.md
复制代码
C0 -- C1 -- C2 -- C4' -- C6' -- C7'(HEAD -> master) \ C3 -- C4 -- C5(dev) \ C6 -- C7(hotfix) 复制代码

须要注意,git所谓的从某某开始,通常都是不包括某某的,这里也同样。

有没有发现操做连续commit的git cherry-pickgit rebase的功能已经很是接近了?因此呀,git cherry-pick也是变基,只不过一边变基一边喂樱桃给你吃。

冲突

git各类命令解决冲突的方法都大同小异。

C0 -- C1(HEAD -> master)
 \
  C2(dev)
复制代码
$ git cherry-pick C2

error: could not apply 051c24c... banana
hint: after resolving the conflicts, mark the corrected paths
hint: with 'git add <paths>' or 'git rm <paths>'
hint: and commit the result with 'git commit'
复制代码

手动解决冲突,执行git add命令而后执行git cherry-pick --continue命令。

若是被唬住了想还原,执行git cherry-pick --abort便可。

变基仍是合并

这是一个哲学问题。

有一种观点认为,仓库的commit历史应该记录实际发生过什么。因此若是你将一个分支合并进另外一个分支,commit历史中就应该有这一次合并的痕迹,由于它是实实在在发生过的。

另外一种观点则认为,仓库的commit历史应该记录项目过程当中发生过什么。合并非项目开发自己带来的,它是一种额外的操做,会使commit历史变的冗长。

我是一个极简主义者,因此我支持首选变基。

07) reset

git checkout命令能够在版本之间随意切换,它的本质是移动HEAD指针。

那git有没有办法移动分支指针呢?

固然有,这就是git reset命令。

底层

git reset命令与git checkout命令的区别在于,它会把HEAD指针和分支指针一块儿移动,若是HEAD指针指向的是一个分支指针的话。

咱们前面说过使用git checkout命令从有分支指向的commit切换到一个没有分支指向的commit上,这个时候的HEAD指针被称为detached HEAD。这是很是危险的。

C0 -- C1 -- C2(HEAD -> master)
复制代码
$ git checkout C1
复制代码
C0 -- C1(HEAD) -- C2(master)
复制代码

可是git reset命令没有这个问题,由于它会把当前的分支指针也带过去。

C0 -- C1 -- C2(HEAD -> master)
复制代码
$ git reset C1
复制代码
C0 -- C1(HEAD -> master) -- C2
复制代码

这就是重置的含义所在。它能够重置分支。

看另外一种状况。若是是从一个没有分支指向的commit切换到另外一个没有分支指向的commit上,那它们就是两个韩国妹子,傻傻分不清楚了。

这是git checkout命令的效果。

C0 -- C1 -- C2(HEAD) -- C3(master)
复制代码
$ git checkout C1
复制代码
C0 -- C1(HEAD) -- C2 -- C3(master)
复制代码

这是git reset命令的效果。

C0 -- C1 -- C2(HEAD) -- C3(master)
复制代码
$ git reset C1
复制代码
C0 -- C1(HEAD) -- C2 -- C3(master)
复制代码

同时重置暂存区和工做区的改动

当你在 git reset 命令后面加 --hard 参数时,暂存区和工做区的内容都会重置为重置后的commit内容。也就是说暂存区和工做区的改动都会清空,至关于撤销暂存区和工做区的改动。

并且是没有确认操做的哟。

$ git status

On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)
    modified:   a.md
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)
    modified:   a.md
复制代码
$ git reset --hard HEAD^

HEAD is now at 58b0040 commit for nothing
复制代码
$ git status

On branch master
nothing to commit, working tree clean
复制代码

仅重置暂存区的改动

git reset 命令后面加 --mixed 参数,或者不加参数,由于--mixed参数是默认值,暂存区的内容会重置为重置后的commit内容,工做区的改动不会清空,至关于撤销暂存区的改动。

一样也是没有确认操做的哟。

$ git status

On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)
    modified:   a.md
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)
    modified:   a.md
复制代码
$ git reset HEAD^

Unstaged changes after reset:
M   a.md
复制代码
$ git status

On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)
    modified:   a.md
no changes added to commit (use "git add" and/or "git commit -a")
复制代码

打个趣,若是git reset命令什么都不加会怎样呢?

你能够脑补一下,git reset命令不加参数默认就是--mixed,不加操做对象默认就是HEAD,因此单纯的git reset命令至关于git reset --mixed HEAD命令。

那这又意味着什么呢?

这意味着从当前commit重置到当前commit,没有变化对吧?可是--mixed参数会撤销暂存区的改动对不对,这就是它的效果。

同时保留暂存区和工做区的改动

若是 git reset 命令后面加 --soft 参数,钢铁直男的温柔,你懂的。仅仅是重置commit而已,暂存区和工做区的改动都会保留下来。

更温柔的是,重置前的commit内容与重置后的commit内容的diff也会放入暂存区。

$ git status

On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)
    modified:   a.md
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)
    modified:   a.md
复制代码
$ git diff --staged

diff --git a/a.md b/a.md
index 4a77268..fde8dcd 100644
--- a/a.md
+++ b/a.md
@@ -1,2 +1,3 @@
 apple
 banana
+cherry
复制代码
$ git reset --soft HEAD^
复制代码
$ git status

On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)
    modified:   a.md
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)
    modified:   a.md
复制代码
$ git diff --staged

diff --git a/a.md b/a.md
index 4a77268..fde8dcd 100644
--- a/a.md
+++ b/a.md
@@ -1 +1,3 @@
 apple
+banana
+cherry
复制代码

banana就是重置前的commit内容与重置后的commit内容的diff,能够看到,它已经在暂存区了。

文件暂存区内容撤回工做区

git reset命令后面也能够跟文件名,它的做用是将暂存区的内容重置为工做区的内容,是git add -- <file>的反向操做。

git reset -- <file>命令是git reset HEAD --mixed -- <file>的简写。在操做文件时,参数只有默认的--mixed一种。

它并不会撤销工做区原有的改动。

$ git status

On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)
    modified:   a.md
复制代码
$ git reset -- a.md

Unstaged changes after reset:
M   a.md
复制代码
$ git status

On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)
    modified:   a.md
no changes added to commit (use "git add" and/or "git commit -a")
复制代码

git checkout命令后面也能够跟文件名,它的做用是撤销工做区的改动,须要注意区分。

文件若干commit版本撤回工做区

若是git reset命令后跟一个commit校验和,它会把该commit与全部后代commit的diff重置到工做区。

意思就是将该文件重置回你指定的commit版本,可是在你指定的commit以后的改动我也给你留着,就放到工做区里吧。

$ git diff --staged

# 空
复制代码
git reset HEAD~4 -- a.md

Unstaged changes after reset:
M   a.md
复制代码
$ git diff --staged

diff --git a/a.md b/a.md
index 6f195b4..72943a1 100644
--- a/a.md
+++ b/a.md
@@ -1,5 +1 @@
 aaa
-bbb
-ccc
-ddd
-eee
复制代码

git diff --staged命令比较工做区和暂存区的内容。能够看到初始工做区和暂存区是一致的,重置文件到4个版本以前,发现工做区比暂存区多了不少改动,这些都是指定commit以后的提交被重置到工做区了。

08) revert

有时候咱们想撤回一个commit,可是这个commit已经在公共的分支上。若是直接修改分支历史,可能会引发一些没必要要的混乱。这个时候,git revert命令就派上用场了。

revert翻译成中文是还原。我以为称它为对冲更合理。对冲指的是同时进行两笔行情相关、方向相反、数量至关、盈亏相抵的交易,这么理解git revert命令一针见血。

由于它的做用就是生成一个新的、彻底相反的commit。

命令

git revert后跟你想要对冲的commit便可。

$ git revert HEAD

Revert "add c.md"
This reverts commit 8a23dad059b60ba847a621b6058fb32fa531b20a.
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# Changes to be committed:
# deleted: c.md
复制代码

git会弹出默认或者自定义的编辑器要求你输入commit信息。而后一个新的commit就生成了。

[master a8c4205] Revert "add c.md"
 1 file changed, 0 insertions(+), 0 deletions(-)
 delete mode 100644 c.md
复制代码

能够看到,本来我添加了一个文件a.mdrevert操做就会执行删除命令。在工做目录看起来就像添加文件操做被撤销了同样,实际上是被对冲了。

它不会改变commit历史,只会增长一个新的对冲commit。这是它最大的优势。

冲突

反向操做也会有冲突?你逗个人吧。

若是你操做的是最新的commit,那固然不会有冲突了。

那要操做的是之前的commit呢?

C0 -- C1 -- C2(HEAD -> master)
复制代码

好比a.mdC0内容为空,C1修改文件内容为appleC2修改文件内容为banana。这时候你想撤销C1的修改。

$ git revert HEAD~

error: could not revert 483b537... apple
hint: after resolving the conflicts, mark the corrected paths
hint: with 'git add <paths>' or 'git rm <paths>'
hint: and commit the result with 'git commit'
复制代码

咱们看一下文件内容。

<<<<<<< HEAD
banana
=======
>>>>>>> parent of 483b537... apple
复制代码

手动解决冲突,执行git add命令而后执行git revert --continue命令完成对冲操做。

取消revert操做只须要执行git revert --abort便可。

09) stash

你在一个分支上开展了一半的工做,忽然有一件急事要你去处理。这时候你得切换到一个新的分支,但是手头上的工做你又不想当即提交。

这种场景就须要用到git的储藏功能。

储藏

想要储藏手头的工做,只需运行git stash命令。

$ git status

On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)
    modified:   a.md
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)
    modified:   b.md
Untracked files:
  (use "git add <file>..." to include in what will be committed)
    c.md
复制代码
$ git stash

Saved working directory and index state WIP on master: 974a2f2 update
复制代码

WIPwork in progress的缩写,指的是进行中的工做。

$ git status

On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)
    c.md
nothing added to commit but untracked files present (use "git add" to track)
复制代码

能够看到,除了未被git跟踪的文件以外,工做区和暂存区的内容都会被储藏起来。如今你能够切换到其余分支进行下一步工做了。

查看

咱们看一下储藏列表。

$ git stash list

stash@{0}: WIP on master: 974a2f2 apple
stash@{1}: WIP on master: c27b351 banana
复制代码

恢复

等咱们完成其余工做,确定要回到这里,继续进行中断的任务。

$ git stash apply

On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)
    modified:   a.md
    modified:   b.md
Untracked files:
  (use "git add <file>..." to include in what will be committed)
    c.md
no changes added to commit (use "git add" and/or "git commit -a")
复制代码

诶,等等。怎么a.md的变动也跑到工做区了?是的,git stash默认会将暂存区和工做区的储藏所有恢复到工做区。若是我就是想原样恢复呢?

$ git stash apply --index

On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)
    modified:   a.md
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)
    modified:   b.md
Untracked files:
  (use "git add <file>..." to include in what will be committed)
    c.md
复制代码

加一个参数--index就会让工做区的归工做区,让暂存区的归暂存区。

还有一点须要注意,恢复储藏的操做能够应用在任何分支,它也不关心即将恢复储藏的分支上,工做区和暂存区是否干净。若是有冲突,自行解决就是了。

咱们浏览过储藏列表,说明git stash apply仅仅是恢复了最新的那一次储藏。

$ git stash apply stash@{1}
复制代码

指定储藏的名字,咱们就能够恢复列表中的任意储藏了。

这个时候咱们再看一下储藏列表。

$ git stash list

stash@{0}: WIP on master: 974a2f2 apple
stash@{1}: WIP on master: c27b351 banana
复制代码

诶,发现仍是两条。我不是已经恢复了一条么?

apply这个词很巧妙,它只是应用,它可不会清理。

清理

想要清理储藏列表,我们得显式的运行git stash drop命令。

$ git stash drop stash@{1}
复制代码
$ git stash list

stash@{0}: WIP on master: 974a2f2 apple
复制代码

如今就真的没有了。但愿你没有喝酒🙃。

git还给咱们提供了一个快捷操做,运行git stash pop命令,同时恢复储藏和清理储藏。

$ git stash pop
复制代码

10) view

有四个git命令能够用来查看git仓库相关信息。

status

git status命令的做用是同时展现工做区和暂存区的diff、暂存区和当前版本的diff、以及没有被git追踪的文件。

$ git status

On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)
    modified:   a.md
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)
    modified:   b.md
Untracked files:
  (use "git add <file>..." to include in what will be committed)
    c.md
复制代码

这个命令应该是最经常使用的git命令之一了,每次提交以前都要看一下。

git status -v命令至关于git status命令和git diff --staged之和。

$ git status -v

On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)
    modified:   a.md
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)
    modified:   b.md
Untracked files:
  (use "git add <file>..." to include in what will be committed)
    c.md
diff --git a/a.md b/a.md
index 5646a65..4c479de 100644
--- a/a.md
+++ b/a.md
@@ -1 +1 @@
-apple
+banana
复制代码

git status -vv命令至关于git status命令和git diff之和。

$ git status -vv

On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)
    modified:   a.md
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)
    modified:   b.md
Untracked files:
  (use "git add <file>..." to include in what will be committed)
    c.md
Changes to be committed:
diff --git c/a.md i/a.md
index 5646a65..4c479de 100644
--- c/a.md
+++ i/a.md
@@ -1 +1 @@
-apple
+banana
--------------------------------------------------
Changes not staged for commit:
diff --git i/b.md w/b.md
index e69de29..637a09b 100644
--- i/b.md
+++ w/b.md
@@ -0,0 +1 @@
+## git is awesome
复制代码

还有一个-s参数,给出的结果颇有意思。

$ git status -s

M  a.md
 M b.md
?? c.md
复制代码

注意看,前面的字母位置是不同的。

第一个位置是该文件在暂存区的状态,第二个位置是该文件在工做区的状态。好比,如下信息显示a.md文件在暂存区有改动待提交,在工做区也有改动待暂存。

MM a.md
复制代码

缩写的状态码主要有这么几种:

状态码 含义
M 文件内容有改动
A 文件被添加
D 文件被删除
R 文件被重命名
C 文件被复制
U 文件冲突未解决
? 文件未被git追踪
! 文件被git忽略

?!所表明的状态由于没有进入git版本系统,因此任什么时候候两个位置都是同样的。就像??或者!!这样。

show

git show命令show的是什么呢?git对象。

$ git show

commit 2bd3c9d7de54cec10f0896db9af04c90a41a8160
Author: veedrin <veedrin@qq.com>
Date:   Fri Dec 28 11:23:27 2018 +0800
    update
diff --git a/README.md b/README.md
index e8ab145..75625ce 100644
--- a/README.md
+++ b/README.md
@@ -5,3 +5,5 @@ one
 two
 three
+
+four
复制代码

git show至关于git show HEAD,显示当前HEAD指向的commit对象的信息。

固然,你也能够查看某个git对象的信息,后面跟上git对象的校验和就行。

$ git show 38728d8

tree 38728d8
README.md
复制代码

diff

git diff命令能够显示两个主体之间的差别。

工做区与暂存区的差别

单纯的git diff命令显示工做区与暂存区之间的差别。

$ git diff

diff --git a/a.md b/a.md
index e69de29..5646a65 100644
--- a/a.md
+++ b/a.md
@@ -0,0 +1 @@
+## git is awesome
复制代码

由于是两个主体之间的比较,git永远将两个主体分别命名为ab

也能够只查看某个文件的diff。固然这里依然是工做区与暂存区之间的差别。

$ git diff a.md
复制代码

暂存区与当前commit的差别

git diff --staged命令显示暂存区与当前commit的差别。

git diff --cached也能够达到相同的效果,它比较老,不如--staged语义化。

$ git diff --staged

diff --git a/b.md b/b.md
index e69de29..4c479de 100644
--- a/b.md
+++ b/b.md
@@ -0,0 +1 @@
+apple
复制代码

一样,显示某个文件暂存区与当前commit的差别。

$ git diff --staged a.md
复制代码

两个commit之间的差别

咱们还能够用git diff查看两个commit之间的差别。

$ git diff C1 C2

diff --git a/a.md b/a.md
index e69de29..5646a65 100644
--- a/a.md
+++ b/a.md
@@ -0,0 +1 @@
+## git is awesome
diff --git a/b.md b/b.md
new file mode 100644
index 0000000..e69de29
复制代码

注意前后顺序很重要,假如我改一下顺序。

$ git diff C2 C1

diff --git a/a.md b/a.md
index 5646a65..e69de29 100644
--- a/a.md
+++ b/a.md
@@ -1 +0,0 @@
-## git is awesome
diff --git a/b.md b/b.md
deleted file mode 100644
index e69de29..0000000
复制代码

比较两个commit之间某个文件的差别。

$ git diff C1:a.md C2:a.md

diff --git a/a.md b/a.md
index e69de29..5646a65 100644
--- a/a.md
+++ b/a.md
@@ -0,0 +1 @@
+## git is awesome
复制代码

log

git log命令显示提交历史。

$ git log

commit 7e2514419ec0f75d1557d3d8165a7e7969f08349
Author: veedrin <veedrin@qq.com>
Date:   Sat Dec 29 11:56:53 2018 +0800
    c.md
commit 4d346773212b208380f71885979f93da65f07ea6
Author: veedrin <veedrin@qq.com>
Date:   Sat Dec 29 11:56:41 2018 +0800
    b.md
commit cde34665b49033d7b8aed3a334c3e2db2200b4dd
Author: veedrin <veedrin@qq.com>
Date:   Sat Dec 29 11:54:59 2018 +0800
    a.md
复制代码

若是要查看每一个commit具体的改动,添加-p参数,它是--patch的缩写。

$ git log -p

commit 7e2514419ec0f75d1557d3d8165a7e7969f08349
Author: veedrin <veedrin@qq.com>
Date:   Sat Dec 29 11:56:53 2018 +0800
    c.md
diff --git a/c.md b/c.md
new file mode 100644
index 0000000..e69de29
commit 4d346773212b208380f71885979f93da65f07ea6
Author: veedrin <veedrin@qq.com>
Date:   Sat Dec 29 11:56:41 2018 +0800
    b.md
diff --git a/b.md b/b.md
new file mode 100644
index 0000000..e69de29
commit cde34665b49033d7b8aed3a334c3e2db2200b4dd
Author: veedrin <veedrin@qq.com>
Date:   Sat Dec 29 11:54:59 2018 +0800
    a.md
diff --git a/a.md b/a.md
new file mode 100644
index 0000000..e69de29
复制代码

你还能够控制显示最近几条。

$ git log -p -1

commit 7e2514419ec0f75d1557d3d8165a7e7969f08349
Author: veedrin <veedrin@qq.com>
Date:   Sat Dec 29 11:56:53 2018 +0800
    c.md
diff --git a/c.md b/c.md
new file mode 100644
index 0000000..e69de29
复制代码

-p有点过于冗余,只是想查看文件修改的统计信息的话,能够使用--stat参数。

$ git log --stat

commit 7e2514419ec0f75d1557d3d8165a7e7969f08349
Author: veedrin <veedrin@qq.com>
Date:   Sat Dec 29 11:56:53 2018 +0800
    c.md
 c.md | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
commit 4d346773212b208380f71885979f93da65f07ea6
Author: veedrin <veedrin@qq.com>
Date:   Sat Dec 29 11:56:41 2018 +0800
    b.md
 b.md | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
commit cde34665b49033d7b8aed3a334c3e2db2200b4dd
Author: veedrin <veedrin@qq.com>
Date:   Sat Dec 29 11:54:59 2018 +0800
    a.md
 a.md | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
复制代码

还以为冗余?只想看提交说明,有一个--oneline能够帮到你。

$ git log --oneline

4ad50f6 (HEAD -> master) 添加c.md文件
4d34677 添加b.md文件
cde3466 添加a.md文件
复制代码

想在命令行工具看git提交历史的树形图表,用--graph参数。

$ git log --graph

* commit 7e2514419ec0f75d1557d3d8165a7e7969f08349 (HEAD -> master)
| Author: veedrin <veedrin@qq.com>
| Date:   Sat Dec 29 11:56:53 2018 +0800
|     c.md
* commit 4d346773212b208380f71885979f93da65f07ea6
| Author: veedrin <veedrin@qq.com>
| Date:   Sat Dec 29 11:56:41 2018 +0800
|     b.md
* commit cde34665b49033d7b8aed3a334c3e2db2200b4dd
  Author: veedrin <veedrin@qq.com>
  Date:   Sat Dec 29 11:54:59 2018 +0800
      a.md
复制代码

我知道大家确定又以为冗余,--graph--oneline食用更佳哟。

$ git log --graph --oneline

* 7e25144 (HEAD -> master) c.md
* 4d34677 b.md
* cde3466 a.md
复制代码

11) position

程序遇到bug的时候,咱们须要快速定位。

定位有两种,第一种是定位bug在哪一个提交上,第二种是定位特定文件的某一行是谁最近提交的。

bisect

有时候咱们发现程序有bug,可是回退几个版本都不解决问题。说明这个bug是一次很老的提交致使的,也不知道当时怎么就没察觉。

那怎么办呢?继续一个一个版本的回退?

估计Linus Torvalds会鄙视你吧。

为了专一于工做,不分心来鄙视你,Linus Torvalds在git中内置了一套定位bug的命令。

你们都玩过猜数字游戏吧。主持人悄悄写下一个数,给你们一个数字区间,而后你们轮流开始切割,谁切到主持人写的那个数就要自罚三杯了。

对,这就是二分法。git利用二分法定位bug的命令是git bisect

使用

假设目前的git项目历史是这样的。

C0 -- C1 -- C2 -- C3 -- C4 -- C5 -- C6 -- C7 -- C8 -- C9(HEAD -> master)
复制代码

这里面有一次commit藏了一个bug,但幸运的是,你不知道是哪一次。

运行git bisect start命令,后跟你要定位的区间中最新的commit和最老的commit。

$ git bisect start HEAD C0

Bisecting: 4 revisions left to test after this (roughly 2 steps)
[ee27077fdfc6c0c9281c1b7f6957ea2b59a461dd] C4
复制代码

而后你就发现HEAD指针自动的指向了C4commit。若是范围是奇数位,那取中间就好了,若是范围是偶数位,则取中间更偏老的那个commit,就好比这里的C4commit。

$ git bisect good

Bisecting: 2 revisions left to test after this (roughly 1 step)
[97cc0e879dc09796bd56cfd7c3a54deb41e447f6] C6
复制代码

HEAD指针指向C4commit后,你应该运行一下程序,若是没问题,那说明有bug的提交在它以后。咱们只须要告诉git当前commit以及更老的commit都是好的。

而后HEAD指针就自动指向C6commit。

继续在C6commit运行程序,结果复现了bug。说明问题就出在C6commit和C4commit之间。

$ git bisect bad

Bisecting: 0 revisions left to test after this (roughly 0 steps)
[a7e09bd3eab7d1e824c0338233f358cafa682af0] C5
复制代码

C6commit标记为bad以后,HEAD指针自动指向C5commit。再次运行程序,依然能复现bug。话很少说,标记C5commit为bad

$ git bisect bad

a7e09bd3eab7d1e824c0338233f358cafa682af0 is the first bad commit
复制代码

由于C4commit和C5commit之间已经不须要二分了,git会告诉你,C5commit是你标记为bad的最先的commit。问题就应该出在C5commit上。

git bisect reset

Previous HEAD position was a7e09bd... C5
Switched to branch 'master'
复制代码

既然找到问题了,那就能够退出git bisect工具了。

另外,git bisect oldgit bisect good的效果相同,git bisect newgit bisect bad的效果相同,这是由于git考虑到,有时候开发者并非想定位bug,只是想定位某个commit,这时候用good bad就会有点别扭。

后悔

git bisect确实很强大,但若是我已经bisect若干次,结果不当心把一个goodcommit标记为bad,或者相反,难道我要reset重来么?

git bisect还有一个log命令,咱们只须要保存bisect日志到一个文件,而后擦除文件中标记错误的日志,而后按新的日志从新开始bisect就行了。

git bisect log > log.txt
复制代码

该命令的做用是将日志保存到log.txt文件中。

看看log.txt文件中的内容。

# bad: [4d5e75c7a9e6e65a168d6a2663e95b19da1e2b21] C9
# good: [c2fa7ca426cac9990ba27466520677bf1780af97] add a.md
git bisect start 'HEAD' 'c2fa7ca426cac9990ba27466520677bf1780af97'
# good: [ee27077fdfc6c0c9281c1b7f6957ea2b59a461dd] C4
git bisect good ee27077fdfc6c0c9281c1b7f6957ea2b59a461dd
# good: [97cc0e879dc09796bd56cfd7c3a54deb41e447f6] C6
git bisect good 97cc0e879dc09796bd56cfd7c3a54deb41e447f6
复制代码

将标记错误的内容去掉。

# bad: [4d5e75c7a9e6e65a168d6a2663e95b19da1e2b21] C9
# good: [c2fa7ca426cac9990ba27466520677bf1780af97] add a.md
git bisect start 'HEAD' 'c2fa7ca426cac9990ba27466520677bf1780af97'
# good: [ee27077fdfc6c0c9281c1b7f6957ea2b59a461dd] C4
git bisect good ee27077fdfc6c0c9281c1b7f6957ea2b59a461dd
复制代码

而后运行git bisect replay log.txt命令。

$ git bisect replay log.txt

Previous HEAD position was ad95ae3... C8
Switched to branch 'master'
Bisecting: 4 revisions left to test after this (roughly 2 steps)
[ee27077fdfc6c0c9281c1b7f6957ea2b59a461dd] C4
Bisecting: 2 revisions left to test after this (roughly 1 step)
[97cc0e879dc09796bd56cfd7c3a54deb41e447f6] C6
复制代码

git会根据log从头开始从新bisect,错误的标记就被擦除了。

而后就是从新作人啦。

blame

一个充分协做的项目,每一个文件可能都被多我的改动过。当出现问题的时候,你们但愿快速的知道,某个文件的某一行是谁最后改动的,以便厘清责任。

git blame就是这样一个命令。blame翻译成中文是归咎于,这个命令就是用来甩锅的。

git blame只能做用于单个文件。

$ git blame a.md

705d9622 (veedrin 2018-12-25 10:09:04 +0800 1) 第一行
74eff2ee (abby 2018-12-25 10:16:44 +0800 2) 第二行
a65b29bd (bob 2018-12-25 10:17:02 +0800 3) 第三行
ee27077f (veedrin 2018-12-25 10:19:05 +0800 4) 第四行
a7e09bd3 (veedrin 2018-12-25 10:19:19 +0800 5) 第五行
97cc0e87 (veedrin 2018-12-25 10:21:55 +0800 6) 第六行
67029a81 (veedrin 2018-12-25 10:22:15 +0800 7) 第七行
ad95ae3f (zhangsan 2018-12-25 10:23:20 +0800 8) 第八行
4d5e75c7 (lisi 2018-12-25 10:23:37 +0800 9) 第九行
复制代码

它会把每一行的修改者信息都列出来。

第一部分是commit哈希值,表示这一行的最近一次修改属于该次提交。

第二部分是做者以及修改时间。

第三部分是行的内容。

若是文件太长,咱们能够截取部分行。

$ git blame -L 1,5 a.md

705d9622 (veedrin 2018-12-25 10:09:04 +0800 1) 第一行
74eff2ee (abby 2018-12-25 10:16:44 +0800 2) 第二行
a65b29bd (bob 2018-12-25 10:17:02 +0800 3) 第三行
ee27077f (veedrin 2018-12-25 10:19:05 +0800 4) 第四行
a7e09bd3 (veedrin 2018-12-25 10:19:19 +0800 5) 第五行
复制代码

或者这样写。

$ git blame -L 1,+4 a.md

705d9622 (veedrin 2018-12-25 10:09:04 +0800 1) 第一行
74eff2ee (abby 2018-12-25 10:16:44 +0800 2) 第二行
a65b29bd (bob 2018-12-25 10:17:02 +0800 3) 第三行
ee27077f (veedrin 2018-12-25 10:19:05 +0800 4) 第四行
复制代码

可是结果不是你预期的那样是吧。1,+4的确切意思是从1开始,显示4行。

若是有人重名,能够显示邮箱来区分。添加参数-e或者--show-email便可。

$ git blame -e a.md

705d9622 (veedrin@qq.com 2018-12-25 10:09:04 +0800 1) 第一行
74eff2ee (abby@qq.com 2018-12-25 10:16:44 +0800 2) 第二行
a65b29bd (bob@qq.com 2018-12-25 10:17:02 +0800 3) 第三行
ee27077f (veedrin@qq.com 2018-12-25 10:19:05 +0800 4) 第四行
a7e09bd3 (veedrin@qq.com 2018-12-25 10:19:19 +0800 5) 第五行
97cc0e87 (veedrin@qq.com 2018-12-25 10:21:55 +0800 6) 第六行
67029a81 (veedrin@qq.com 2018-12-25 10:22:15 +0800 7) 第七行
ad95ae3f (zhangsan@qq.com 2018-12-25 10:23:20 +0800 8) 第八行
4d5e75c7 (lisi@qq.com 2018-12-25 10:23:37 +0800 9) 第九行
复制代码

12) tag

git是一个版本管理工具,但在众多版本中,确定有一些版本是比较重要的,这时候咱们但愿给这些特定的版本打上标签。好比发布一年之后,程序的各项功能都趋于稳定,能够在圣诞节发布v1.0版本。这个v1.0在git中就能够经过标签实现。

而git标签又分为两种,轻量级标签和含附注标签。

轻量级标签和分支的表现形式是同样的,仅仅是一个指向commit的指针而已。只不过它不能切换,一旦贴上就没法再挪动了。

含附注标签才是咱们理解的那种标签,它是一个独立的git对象。包含标签的名字,电子邮件地址和日期,以及标签说明。

建立

建立轻量级标签的命令很简单,运行git tag <tag name>

$ git tag v0.3
复制代码

.git目录中就多了一个指针文件。

.git/refs/tags/v0.3
复制代码

建立含附注标签要加一个参数-a,它是--annotated的缩写。

$ git tag -a v1.0
复制代码

git commit同样,若是不加-m参数,则会弹出默认或者自定义的编辑器,要求你写标签说明。

不写呢?

fatal: no tag message?
复制代码

建立完含附注标签后,.git目录会多出两个文件。

.git/refs/tags/v0.3
复制代码
.git/objects/80/e79e91ce192e22a9fd860182da6649c4614ba1
复制代码

含附注标签不只会建立一个指针,还会建立一个tag对象。

咱们了解过git有四种对象类型,tag类型是咱们认识的最后一种。

咱们看看该对象的类型。

$ git cat-file -t 80e79e9

tag
复制代码

再来看看该对象的内容。

$ git cat-file -p 80e79e9

object 359fd95229532cd352aec43aada8e6cea68d87a9
type commit
tag v1.0
tagger veedrin <veedrin@qq.com> 1545878480 +0800
版本 v1.0
复制代码

它关联的是一个commit对象,包含标签的名称,打标签的人,打标签的时间以及标签说明。

我可不能够给历史commit打标签呢?固然能够。

$ git tag -a v1.0 36ff0f5
复制代码

只需在后面加上commit的校验和。

查看

查看当前git项目的标签列表,运行git tag命令不带任何参数便可。

$ git tag

v0.3
v1.0
复制代码

注意git标签是按字母顺序排列的,而不是按时间顺序排列。

并且我并无找到分别查看轻量级标签和含附注标签的方法。

查看标签详情能够使用git show <tag name>

$ git show v0.3

commit 36ff0f58c8e6b6a441733e909dc95a6136a4f91b (tag: v0.3)
Author: veedrin <veedrin@qq.com>
Date:   Thu Dec 27 11:08:09 2018 +0800
    add a.md
diff --git a/a.md b/a.md
new file mode 100644
index 0000000..e69de29
复制代码
$ git show v1.0

tag v1.0
Tagger: veedrin <veedrin@qq.com>
Date:   Thu Dec 27 11:08:39 2018 +0800
版本 v1.0
commit 6dfdb65ce65b782a6cb57566bcc1141923059d2b (HEAD -> master, tag: v1.0)
Author: veedrin <veedrin@qq.com>
Date:   Thu Dec 27 11:08:33 2018 +0800
    add b.md
diff --git a/b.md b/b.md
new file mode 100644
index 0000000..e69de29
复制代码

删除

虽然git标签不能移动对吧,但咱们能够删除它呀。

$ git tag -d v0.3

Deleted tag 'v0.3' (was 36ff0f5)
复制代码

若是标签已经推送到了远端,也是能够删除的。

$ git push origin -d v0.3

To github.com:veedrin/git.git
 - [deleted]         v0.3
复制代码

推送

默认状况下,git push推送到远端仓库并不会将标签也推送上去。若是想将标签推送到远端与别人共享,咱们得显式的运行命令git push origin <tag name>

$ git push origin v1.0

Counting objects: 1, done.
Writing objects: 100% (1/1), 160 bytes | 160.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0)
To github.com:veedrin/git.git
 * [new tag]         v1.0 -> v1.0
复制代码

这里并不区分轻量级标签和含附注标签。

一次性将本地标签推送到远端仓库也是能够的。

$ git push origin --tags
复制代码

本文是『horseshoe·Git专题』系列文章之一,后续会有更多专题推出

GitHub地址(持续更新):github.com/veedrin/hor…

博客地址(文章排版真的很漂亮):matiji.cn

若是以为对你有帮助,欢迎来GitHub点Star或者来个人博客亲口告诉我

Git专题一览

🎖 add

🎖 commit

🎖 branch

🎖 checkout

🎖 merge

🎖 rebase

🎖 reset

🎖 revert

🎖 stash

🎖 view

🎖 position

🎖 tag

🎖 remote

相关文章
相关标签/搜索