Git-深刻一点点

本文来自半个月前我在我司内部进行的分享。一直以为 Git 是一个很是值得深刻学习的工具,准备此次内部分享用了好久的时间,不过关于Git的讲解仍是有不少不足之处,你们有什么建议,欢迎来本文的 githug地址讨论,咱们一块儿把 Git 学得更深一点。

Git是一个CLI(Common line interface),咱们与其的交互经常发生在命令行,(固然有时候也会使用GUI,如sourcetree,Github等等),因为咱们的使用方式,咱们经常会忽略git仓库自己是一个没那么复杂的文件系统,咱们输入git命令时其实就是对这个文件系统进行操做。html

对 Git 文件系统的定义有一种更专业的说法,「从根本上来说 Git 是一个内容寻址(content-addressable)文件系统,并在此之上提供了一个版本控制系统的用户界面」。git

…from [Git - 关于版本控制](https://git-scm.com/book/zh/v2/%E8%B5%B7%E6%AD%A5-%E5%85%B3%E4%BA%8E%E7%89%88%E6%9C%AC%E6%8E%A7%E5%88%B6)

Git作为文件系统长什么样子

找一个空文件夹,执行git init后咱们会发现其中会多出一个隐藏文件夹.git,其文件结构以下:github

➜ mkdir gitDemo && cd gitDemo && git init && tree -a
Initialized empty Git repository in /Users/zhangwang/Documents/personal/Test/gitDemo/.git/
.
└── .git
    ├── HEAD
    ├── branches
    ├── config
    ├── description
    ├── hooks
    │   ├── applypatch-msg.sample
    │   ├── commit-msg.sample
    │   ├── post-update.sample
    │   ├── pre-applypatch.sample
    │   ├── pre-commit.sample
    │   ├── pre-push.sample
    │   ├── pre-rebase.sample
    │   ├── pre-receive.sample
    │   ├── prepare-commit-msg.sample
    │   └── update.sample
    ├── info
    │   └── exclude
    ├── objects
    │   ├── info
    │   └── pack
    └── refs
        ├── heads
        └── tags

10 directories, 14 files

几乎 Git 相关的全部操做都和这个文件夹相关,若是你是第一次见到这个文件系统,以为陌生也很正常,不过读完本文,每一项都会变得清晰了。算法

咱们先想另一个问题,作为版本控制系统的 Git ,究竟会存储那些内容在上述文件系统中,这些内容又是如何被存储的呢?数据库

「分支」,「commit」,「原始的文件」,「diff」…缓存

…from 热烈的讨论中

好吧,不卖关子,实际上在上述文件系统中 Git 为咱们存储了五种对象,这些对象存储在/objects/refs文件夹中。安全

Git 中存储的五种对象

  • Blobs, which are the most basic data type in Git. Essentially, a blob is just a bunch of bytes; usually a binary representation of a file.
Blobs是Git中最基础的数据类型,一个 blob对象就是一堆字节,一般是一个文件的二进制表示
  • Tree objects, which are a bit like directories. Tree objects can contain pointers to blobs and other tree objects.
tree,有点相似于目录,其内容由对其它 treeblobs的指向构成;
  • Commit objects, which point to a single tree object, and contain some metadata including the commit author and any parent commits.
commit,指向一个树对象,并包含一些代表做者及父 commit 的元数据
  • Tag objects, which point to a single commit object, and contain some metadata.
Tag,指向一个commit对象,并包含一些元数据
  • References, which are pointers to a single object (usually a commit or tag object).
References,指向一个 commit或者 tag对象

blobs , tree , commit ,以及声明式的 tag 这四种对象会存储在 .git/object 文件夹中。这些对象的名称是一段40位的哈希值,此名称由其内容依据sha-1算法生成,具体到.git/object文件夹下,会取该hash值的前 2 位为子文件夹名称,剩余 38 位为文件名,这四类对象都是二进制文件,其内容格式依据类型有所不一样。下面咱们一项项来看:bash

Blobs

咱们都经常使用git add这个命令,也都据说过,此命令会把文件添加到缓存区(index)。可是有没有想过「把文件添加到缓存区」是一种很奇怪的说法,若是说这个文件咱们曾经add过,为何咱们须要在修改事后再次添加到缓存区?服务器

咱们确实须要把文件从新添加到缓存区,其实每次修改后的文件,对 git 来讲都是一个新文件,每次 add 一个文件,就会添加一个 Blob 对象。并发

blobs是二进制文件,咱们不能直接查看,不过经过 Git 提供的一些更底层的命令如 git show [hash] 或者 git cat-file -p [hash] 咱们就能够查看 .git/object 文件夹下任一文件的内容。

➜ git cat-file -p 47ca
abc
456

从上面的内容中就能够看出,blob 对象中仅仅存储了文件的内容,若是咱们想要完整还原工做区的内容,咱们还须要把这些文件有序组合起来,这就涉及到 Git 中存储的另一个重要的对象:tree

Tree objects

tree 对象记录了咱们的文件结构,更形象的说法是,某个 tree 对象记录了某个文件夹的结构,包含文件以及子文件夹。tree 对象的名称也是一个40位的哈希值,文件名依据内容生成,所以若是一个文件夹中的结构有所改变,在 .git/object/ 中就会出现一个新的 tree object, 一个典型的 tree object 的内容以下:

➜ git ls-tree bb4a8638f1431e9832cfe149d7f32f31ebaa77ef
100644 blob 4be9cb419da86f9cbdc6d2ad4db763999a0b86f2    .gitignore
040000 tree dccea6a66df035ac506ab8ca6d2735f9b64f66c1    01_introduction_to_algorithms
040000 tree 363813a5406b072ec65867c6189e6894b152a7e5    02_selection_sort
040000 tree 5efc07910021b8a2de0291218cb1ec2555d06589    03_recursion
040000 tree cc15fd67f464c29495437aa81868be67cd9688b2    04_quicksort
040000 tree 9f09206e367567bf3fe0f9b96f3609eb929840f1    05_hash_tables
040000 tree c8b7b793b0318d13b25098548effde96fc9f1377    06_breadth-first_search
040000 tree 7f111006c8a37eab06a3d8931e83b00463ae0518    07_dijkstras_algorithm
040000 tree 9f6d831e5880716e0eda2d9312ea2689a8cc1439    08_greedy_algorithms
040000 tree 692a9b39721744730ad1b29c052e288aeb89c2ac    09_dynamic_programming
100644 blob 290689b29c24d3406a1ed863077a01393ae2aff3    LICENSE
100644 blob 9017b1121945799e97825f996bc0cefe3422cbaf    README.md
040000 tree ce710aa0b6c23b7f81dbd582aad6f9435988a8b4    images

咱们能够看过,tree 中包含两种类型的文件,treeblob,这就把文件有序的组合起来了,若是咱们知道了根 tree(能够理解为root文件夹对应的tree),咱们就有能力依据此tree还原整个工做区。

可能咱们很早就据说过 Git 中的每个 commit 存储的都是一个「快照」。理解了tree对象,咱们就能够较容易的理解「快照」这个词了 ,接下来咱们看看 commit object

commit object

咱们知道,commit记录了咱们的提交历史,存储着提交时的 message,Git 分支中的一个个的节点也是由 commit 构成。一个典型的 commit object 内容以下:

➜ git cat-file -p e655
tree 73aff116086bc78a29fd31ab3fbd7d73913cf958
parent 8da64ce1d90be7e40d6bad5dd1cb1a3c135806a2
author zhangwang <zhangwang2014@iCloud.com> 1521620446 +0800
committer zhangwang <zhangwang2014@iCloud.com> 1521620446 +0800

bc

咱们来看看其中每一项的意义:

  • tree:告诉咱们当前 commit 对应的根 tree,依据此值咱们还原此 commit 对应的工做区;
  • parent:父 commit 的 hash 值,依据此值,咱们能够记录提交历史;
  • author:记录着此commit的修改内容由谁修改;
  • committer:记录着当前 commit 由谁提交;
  • ...bc: commit message;

commit 经常位于 Git 分支上,分支每每也是由咱们主动添加的,Git 提供了一种名为 References 的对象供咱们存储「类分支」资源。

References

References 对象存储在/git/refs/文件夹下,该文件夹结构以下:

➜ tree .git/refs
.git/refs
├── heads
│   ├── master
│   ├── meta-school-za
│   └── ...
├── remotes
│   ├── origin
│   │   ├── ANDROIDBUG-4845
│   │   ├── ActivityCard-za
│   │   ├── ...
├── stash
└── tags

其中 heads 文件夹中的每个文件其实就对应着一条本地分支,已咱们最熟悉的 master 分支为例,咱们看看其中的内容:

➜ cat .git/refs/heads/master
603bdb03d7134bbcaf3f84b21c9dbe902cce0e79

有没有发现,文件 master 中的内容看起来好眼熟,它实际上是就是一个指针,指向当前分支最新的 commit 对象。因此说 Git 中的分支是很是轻量级的,弄清分支在 Git 内部是这样存储以后,也许咱们能够更容易理解相似下面这种图了。

咱们再看看 .git/refs 文件夹中其它的内容:

  • .git/refs/remotes 中记录着远程仓库分支的本地映射,其内容只读;
  • .git/refs/stashgit stash 命令相关,后文会详细讲解;
  • .git/refs/tag, 轻量级的tag,与 git tag 命令相关,它也是一个指向某个commit 对象的指针;
tag是一种辅助 Git 作版本控制的对象,上面这种 tag 只是「轻量级tag」 ,此外还存在另外一种「声明式tag」,声明式 tag 对象能够存储更多的信息,其存在于 .git/object/下。

Tag objects

上文已经说过 Git 中存在两种 tag

  • lightweight tags,轻量标签很像一个不会改变的分支,其内容是对一个特定提交的引用,这种 tag 存储在.git/refs/tag/文件夹下;
  • annotated tags: 声明式的标签会在object下添加tag object,此种 tag 能记录更多的信息;

两种 tag 的内容差异较大:

# lightweight tags
$ git tag 0.1
# 指向添加tag时的commit hash值
➜ cat 0.1 
e9f249828f3b6d31b895f7bc3588df7abe5cfeee

# annotated tags
$ git tag -a -m 'Tagged1.0' 1.0
➜ git cat-file -p 52c2
object e9f249828f3b6d31b895f7bc3588df7abe5cfeee
type commit
tag 1.0
tagger zhangwang <zhangwang2014@iCloud.com> 1521625083 +0800

Tagged1.0

对比能够发现,声明式的 tag 不只记录了对应的 commit ,标签号,额外还记录了打标签的人,并且还能够额外添加 tag message(上面的-m 'Tagged1.0')。

值得额外说明的是,默认状况下, git push 命令并不会推送标签到远程仓库服务器上。 想要传送,必须显式地推送标签到共享服务器上。 推送方法为 git push origin [tagname],若是要推送全部的标签,可使用 git push origin --tags

另外咱们也能够在后期给某次 commit 打上标签,如:git tag -a v1.2 9fceb02

至此,咱们已经理解了 Git 中的这几类资源,接下来咱们看看 Git 命令是如何操做这些资源的。

常见git命令与上述资源间的映射

依据场景,咱们能够粗略按照操做的是本地仓库仍是远程仓库,把 Git 命令分为本地命令和远程命令,咱们先看本地命令,咱们本地可供操做的 Git 仓库每每是经过 git clone 或者 git init 生成。咱们先看git init作了些什么。

本地命令

git init && git init --bare

git init:在当前文件夹下新建一个本地仓库,在文件系统上表现为在当前文件夹中新增一个 .git 的隐藏文件夹
如:

gitDemo on  master 
➜ ls -a
.     ..    .git  a.txt data

Git 中还存在一种被称为裸仓库的特殊仓库,使用命令 git init --bare 能够初始化一个裸仓库

其目录结构以下:

➜ mkdir gitDemoBear && cd gitDemoBear && git init --bare && tree
Initialized empty Git repository in some/path/gitDemoBear/
.
├── branches
├── hooks
├── info
├── objects
│   ├── info
│   └── pack
└── refs
    ├── heads
    └── tags

9 directories, 14 files

和普通仓库相比,裸仓库没有工做区,因此并不会存在在裸仓库上直接提交变动的状况,这种仓库会直接把 .git 文件夹中的内容置于初始化的文件夹下。此外

在 config 文件下咱们会看到 bare = true 这代表当前仓库是一个裸仓库:

# normal
    bare = false
    logallrefupdates = true

# bare
    bare = true
普通的方法是不能修改裸仓库中的内容的。裸仓库只容许贡献者 clone, push, pull

git add

咱们都知道 git add [file] 会把文件添加到缓存区。那缓存区本质上是什么呢?

为了理清这个问题,咱们先看下图:

image.png

不少地方会说,git 命令操做的是三棵树。三棵树对应的就是上图中的工做区( working directory )、缓存区( Index )、以及 HEAD。

工做区比较好理解,就是可供咱们直接修改的区域,HEAD 实际上是一个指针,指向最近的一次 commit 对象,这个咱们以后会详述。Index 就是咱们说的缓存区了,它是下次 commit 涉及到的全部文件的列表。

回到git add [file],这个命令会依次作下面两件事情:

  1. .git/object/ 文件夹中添加修改或者新增文件对应的 blob 对象;
  2. .git/index 文件夹中写入该文件的名称及对应的 blob 对象名称;

经过命令 git ls-files -s 能够查看全部位于.git/index中的文件,以下:

➜ git ls-files -s
100644 8baef1b4abc478178b004d62031cf7fe6db6f903 0    a.txt
100644 aceb8a25000b1c680a1a83c032daff4d800c8b95 0    b.txt
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0    c.txt
100644 0932cc0d381ab943f3618e6125995f643cad4425 0    data/d.txt

其中各项的含义以下:

  • 100644100表明regular file,644表明文件权限
  • 8baef1b4abc478178b004d62031cf7fe6db6f903:blob对象的名称;
  • 0:当前文件的版本,若是出现冲突,咱们会看到12
  • data/d.txt: 该文件的完整路径

Git 还额外提供了一个命令来帮我咱们查看文件在这三棵树中的状态,git status

git status

git status有三个做用:

  1. 查看当前所在分支;
  2. 列出已经缓存,未缓存,未追踪的文件(依据上文中的三棵树生成);
  3. 给下一步的操做必定的提示;

通常来讲 .git/HEAD 文件中存储着 Git 仓库当前位于的分支:

➜ cat .git/HEAD
ref: refs/heads/mate-school--encodeUri

当咱们 git add 某个文件后,git 下一步每每会提示咱们commit它。咱们接下来看看,commit过程发生了什么。

git commit

对应到文件层面,git commit作了以下几件事情:

  1. 新增tree对象,有多少个修改过的文件夹,就会添加多少个tree对象;
  2. 新增commit对象,其中的的tree指向最顶端的tree,此外还包含一些其它的元信息,commit对象中的内容,上文已经见到过, tree对象中会包含一级目录下的子tree对象及blob对象,由此可构建当前commit的文档快照;;
经过 git cat-file -p hash可查看某个对象中的内容
经过 git cat-file -t hash可查看某个对象的类型

当咱们 git add 某个文件后,下一步咱们每每须要执行 git commit 。接下来咱们看看,commit过程发生了什么。

git branch

前文咱们提到过,分支在本质上仅仅是「指向提交对象的可变指针」,其内容为所指对象校验和(长度为 40 的 SHA-1 值字符串)的文件(一个commit对象),因此分支的建立和销毁都异常高效,建立一个新分支就至关于往一个文件中写入 41 个字节(40 个字符和 1 个换行符),足见 Git 的分支多么轻量级。
此外上文中提到的 HEAD 也能够看作一个指向当前所在的本地分支的特殊指针。
在开发过程当中咱们会建立不少分支,全部的分支都存在于.git/refs文件夹中。

➜ tree .git/refs
.git/refs
├── heads
│   ├── master
│   ├── meta-school-za
│   └── ...
├── remotes
│   ├── origin
│   │   ├── ANDROIDBUG-4845
│   │   ├── ActivityCard-za
│   │   ├── ...
├── stash
└── tags

➜ cat heads/feature
0cdc9f42882f032c5a556d32ed4d8f9f5af182ed

存在两种分支,本地分支远程分支
本地分支:

对应存储在 .git/refs/heads中;
还存在一种叫作「跟踪分支」(也叫「上游分支」)的本地分支,此类分支从一个远程跟踪分支检出,是与远程分支有直接关系的本地分支。 若是在一个跟踪分支上输入 git pull,Git 能自动地识别去那个远程仓库上的那个分支抓取并合并代码。

远程分支:

对应存储在 .git/refs/remotes中,能够看作远程仓库的分支在本地的备份,其内容在本地是只读的。

.git/config文件中信息进一步指明了远程分支与本地分支之间的关系:

➜ cat .git/config
...
[remote "origin"]
    url = git@git.in.zhihu.com:zhangwang/zhihu-lite.git
    fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
    remote = origin
    merge = refs/heads/master
[remote "wxa"]
    url = https://git.in.zhihu.com/wxa/zhihu-lite.git
    fetch = +refs/heads/*:refs/remotes/wxa/*

使用 git branch [newBranchName] 能够建立新分支 newBranchName。不过一个更常见的用法是git checkout -b [newBranchName],此命令在本地建立了分支 newBranchName,并切换到了分支 newBranchName。咱们看看git checkout 究竟作了些什么

git checkout

还记得前面咱们提到过的HEAD吗?git checkout 实际上就是在操做HEAD
前文中咱们提到过通常状况下 .git/HEAD 指向本地仓库当前操做的分支。那只是通常状况,更准确的说法是 .git/HEAD 直接或者间接指向某个 commit 对象。
咱们知道每个 commit 对象都对应着一个快照。可依据其恢复本地的工做目录。 HEAD 指向的 commit 是判断工做区有何更改的基础。
Git 中有一个比较难理解的概念叫作「HEAD分离」,映射到文件层面,其实指的是 .git/HEAD 直接指向某个commit对象。
咱们来看git checkout的具体用法

  1. git checkout <file>:

此命令能够用来清除未缓存的更改,它能够看作是 git checkout HEAD <file> 的简写,
映射到文件层面,其操做为恢复文件<file>的内容为,HEAD对应的快照时的内容。其不会影响已经缓存的更改的缘由在于,其实缓存过的文件就是另一个文件啦。
相应的命令还有 git checkout <commit> <file> 能够用来恢复某文件为某个提交时的状态。

  1. git checkout <branch>

切换分支到 <branch> 其其实是修改 .git/HEAD 中的内容为 <branch>,更新工做区内容为 <branch> 所指向的 commit 对象的内容。

➜ cat .git/HEAD
ref: refs/heads/master
  1. git checkout <hash|tag>

HEAD直接指向一个commit对象,更新工做区内容为该commit对象对应的快照,此时为HEAD分离状态,切换到其它分支或者新建分支git branch -b new-branch|| git checkout branch可使得HEAD再也不分离。

➜ cat .git/HEAD
8e1dbd367283a34a57cb226d23417b95122e5754

在分支上进行了一些操做后,下一步咱们要作的就是合并不一样分支上的代码了,接下来咱们看看git merge 是如何工做的。

git merge

Git 中分支合并有两种算法,快速向前合并三路合并

快速向前合并:

此种状况下,主分支没有改动,所以在基于主分支生成的分支上作的更改,必定不会和主分支上的代码冲突,能够直接合并,在底层至关于修改 .refs/heads/ 下主分支的内容为最新的 commit 对象。

image.png

三路合并:

新的feature分支在开发过程当中,主分支上的代码也作了修改并添加了新的 commit ,此时合并,须要对比 feature 分支上最新的 commit,feature 分支的 base commit 以及 master 分支上最新的 commit 这三个commit的快照。若是一切顺利,这种合并会生成新的合并 commit ,格式以下:
➜ git cat-file -p 43cfbd24b7812b7cde0ca2799b5e3305bd66a9b3
tree 78f3bc25445be087a08c75ca62ca1708a9d2e33a
parent 51b45f5892f640b8e9b1fec2f91a99e0d855c077
parent 96e66a5b587b074d834f50d6f6b526395b1598e5
author zhangwang <zhangwang2014@iCloud.com> 1521714339 +0800
committer zhangwang <zhangwang2014@iCloud.com> 1521714339 +0800

Merge branch 'feature'

和普通的 commit 对象的区别在于其有两个parent,分别指向被合并的两个commit

不过三路合并每每没有那么顺利,每每会有冲突,此时须要咱们解决完冲突后,再合并,三路合并的详细过程以下(为了叙述便利,假设合并发生在 master 分支与 feature 分支之间):

  1. Git 将接收 commit 的哈希值写入文件 .git/MERGE_HEAD。此文件的存在说明 Git 正在作合并操做。(记录合并提交的状态)
  2. Git 查找 base commit:被合并的两个分支的第一个共有祖先 commit
  3. Git 基于 base commitmaster commitfeature commit 建立索引;
  4. Git 基于 base commit — master commitbase commit — feature commit 分别生成 diff,diff 是一个包含文件路径的列表,其中包含添加、移除、修改或冲突等变化;
  5. Git 将 diff 应用到工做区;
  6. Git 将 diff 应用到 index,若是某文件有冲突,其在index中将存在三份;
  7. 若是存在冲突,须要手动解决冲突
  8. git add 以更新 index 被提交, git commit基于此 index 生成新的commit;
  9. 将主分支.git/refs/heads/master中的内容指向第8步中新生成的 commit,至此三路合并完成;

git cherry-pick(待进一步补充)

Git 中的一些命令是以引入的变动即提交这样的概念为中心的,这样一系列的提交,就是一系列的补丁。 这些命令以这样的方式来管理你的分支。
git cherry-pick作的事情是将一个或者多个commit应用到当前commit的顶部,复制commit,会保留对应的二进制文件,可是会修改parent信息。

image.png

在D commit上执行,git cherry-pick F 会将F复制一份到D上,复制的缘由在于,F的父commit变了,可是内容又须要保持不可变。

一个常见的工做流以下:

$ git checkout master
$ git checkout -b foo-tmp
$ git cherry-pick C D
# 将foo指向foo-tmp,reset将HEAD指向了某个特殊的commit
$ git checkout foo
$ git reset --hard foo-tmp
$ git branch -D foo-tmp

git revert 命令本质上就是一个逆向的 git cherry-pick 操做。 它将你提交中的变动的以彻底相反的方式的应用到一个新建立的提交中,本质上就是撤销或者倒转。

有时候咱们会想要撤销一些commit,这时候咱们就会用到git reset

git reset

git reset 具备如下常见用法:

  1. git reset <file>:从缓存区移除特定文件,可是不会改变工做区的内容
  2. git reset : 重设缓存区,会取消全部文件的缓存
  3. git reset --hard : 重置缓存区和工做区,修改其内容对最新的一次 commit 对应的内容
  4. git reset <commit> : 移动当前分支的末端到指定的commit
  5. git reset --hard <commit>: 重置缓存区和工做区,修改其内容为指定 commit 对应的内容

相对而言,git reset是一个相对危险的操做,其危险之处在于可能会让本地的修改丢失,可能会让分支历史难以寻找。

咱们看看git reset的原理

  1. 移动HEAD所指向的分支的指向:若是你正在 master 分支上工做,执行 git reset 9e5e64a 将会修改 master 让指向 哈希值为 9e5e64acommit object
  • 不管你是怎么使用的git reset,上述过程都会发生,不一样用法的区别在于会如何修改工做区及缓存区的内容,若是你用的是 git reset --soft,将仅仅执行上述过程;
  • git reset本质上是撤销了上一次的 git commit 命令。
执行 git commit ,Git 会建立一个新的 commit 对象,并移动 HEAD 所指向的分支指向该commit。 而执行 git reset会修改 HEAD 所指向的分支指向 HEAD~(HEAD 的父提交),也就是把该分支的指向修改成原来的指向,此过程不会改变 index和工做目录的内容。
  1. 加上 —mixed 会更新索引:git reset --mixedgit reset 效果一致,这是git reset的默认选项,此命令除了会撤销一上次提交外,还会重置index,至关于咱们回滚到了 git addgit commit 前的状态。
  2. 添加—hard会修改工做目录中的内容:除了发生上述过程外,还会恢复工做区为 上一个 commit对应的快照的内容,换句话说,是会清空工做区所作的任何更改。
—hard 能够算是 reset 命令惟一的危险用法,使用它会真的销毁数据。

若是你给 git reset 指定了一个路径,git reset 将会跳过第 1 步,将它的做用范围限定为指定的文件或文件夹。 此时分支指向不会移动,不过索引和工做目录的内容则能够完成局部的更改,会只针对这些内容执行上述的第 二、3 步。

git reset file.txt 实际上是 git reset --mixed HEAD file.txt 的简写形式,他会修改当前 index看起来像 HEAD 对应的 commit所依据的索引,所以能够达到取消文件缓存的做用。

git stash

有时候,咱们在新分支上的feature开发到一半的时候接到通知须要去修复一个线上的紧急bug?,这时候新feature还达不到该提交的程度,命令git stash就派上了用场。

git stash被用来保存当前分支的工做状态,便于再次切换回本分支时恢复。其具体用法以下:

  1. feature分支上执行git stash 或 git stash save,保存当前分支的工做状态;
  2. 切换到其它分支,修复bug,并提交
  3. 切换回feature分支,执行git stash list,列出保存的全部stash,执行 git stash apply,恢复最新的stash到工做区;
也能够覆盖老一些的 stash, 用法如 git stash apply stash@{2};

关于git stash还有其它一些值得关注的点:

  1. 直接执行git stash会恢复全部以前的文件到工做区,也就是说以前添加到缓存区的文件不会再存在于缓存区,使用 git stash apply --index 命令,则能够恢复工做区和缓存区与以前同样;
  2. 默认状况下,git stash 只会储藏已经在索引中的文件。 使用 git stash —include-untrackedgit stash -u 命令,Git 才会将任何未跟踪的文件添加到stash;
  3. 使用命令git stash pop 命令能够用来应用最新的stash,并当即从stash栈上扔掉它;
  4. 使用命令 git stash —patch ,可触发交互式stash会提示哪些改动想要储藏、哪些改动须要保存在工做目录中。
➜ git stash --patch
diff --git a/src/pages/index/index.mina b/src/pages/index/index.mina
index 6e11ce3..038163c 100644
--- a/src/pages/index/index.mina
+++ b/src/pages/index/index.mina
@@ -326,6 +326,7 @@ Page<Props, Data, {}>({
   },

   onPageScroll({scrollTop}) {
+    // abc
 //    TODO: cover-view 的 fixed top 样式和 pullDownRefresh 有严重冲突。
 //    当 bug 解决时,能够在 TabNav 内使用 <cover-view> 配合滚动实现 iOS 的磁铁效果

Stash this hunk [y,n,q,a,d,/,e,?]?
  1. 使用命令git stash branch <new branch>:构建一个名为new branch的新分支,并将stash中的内容写入该分支

说完了git stash的基本用法,咱们来看看,其在底层的实现原理:

上文中咱们提到过,Git 操做的是 工做区,缓存区及 HEAD 三棵文件树,咱们也知道,commit 中包含的根 tree 对象指向,能够看作文档树的快照。

当咱们执行git stash时,实际上咱们就是依据工做区,缓存区及HEAD这三棵文件树分别生成commit对象,以后以这三个commit 为 parent 生成新的 commit对象,表明这次stash,并把这个 commit 的 hash值存到.git/refs/stash中。

当咱们执行git stash apply时,就能够依据存在 .git/refs/stash 文件中的 commit 对象找到 stash 时工做区,缓存区及HEAD这三棵文件树的状态,进而能够恢复其内容。

gitDemo on  master [$]
➜ cat .git/refs/stash
68e5413895acd479daad0c96815cdb69a3c61bef

gitDemo on  master [$]
➜ git cat-file -p 68e5
tree 4b825dc642cb6eb9a060e54bf8d69288fbee4904
parent aade8236c7c291f927f0be3f51ae57f5388eafcc
parent 408ef43aacaf7c255a0c3ea4f82196626a28a39b
parent 6bacdafcddf0685d8e4a0b364ea346ff209a87be
author zhangwang <zhangwang2014@iCloud.com> 1522397172 +0800
committer zhangwang <zhangwang2014@iCloud.com> 1522397172 +0800

WIP on master: aade823 first commit
暂留的疑问?
.git/refs/stash文件中只存有最新的 stash commit值, git stash list是如何生效的。

git clean

使用git clean命令能够去除冗余文件或者清理工做目录。 使用git clean -f -d命令能够用来移除工做目录中全部未追踪的文件以及空的子目录。

此命令真的会从工做目录中移除未被追踪的文件。 所以若是你改变主意了,不必定能找回来那些文件的内容。 一个更安全的命令是运行 git stash --all 来移除每一项更新,可是能够从stash栈中找到并恢复它们。。

git clean -n 命令能够告诉咱们git clean的结果是什么,以下:

$ git clean -d -n
Would remove test.o
Would remove tmp/

全部在不知道 git clean 命令的后果是什么的时候,不要使用-f,推荐先使用 -n 来看看会有什么后果。

讲到这里,经常使用的操做本地仓库的命令就基本上说完了,下面咱们看看 Git 提供的一些操做远程仓库的命令。

远程命令

若是咱们是中途加入某个项目,每每咱们的开发会创建在已有的仓库之上。若是使用github或者gitlab,像已有仓库提交代码的常见工做流是

  1. fork一份主仓库的代码到本身的远程仓库;
  2. clone 本身远程仓库代码到本地;
  3. 添加主仓库为本地仓库的远程仓库,git remote add ...,便于以后保持本地仓库与主仓库同步git pull
  4. 在本地分支上完成开发,推送本地分支到我的远程仓库某分支git push
  5. 基于我的远程仓库的分支向主仓库对应分支提交MR,待review经过合并代码到主仓库;

这期间涉及不少远程命令,咱们接触到的第一个命令极可能是git clone,咱们先看这个命令作了些什么

git clone

git clone的通常用法为git clone <url>
<url>部分支持四种协议:本地协议(Local),HTTP 协议,SSH(Secure Shell)协议及 Git 协议。典型的用法以下:

$ git clone git://github.com/schacon/ticgit.git
Cloning into 'ticgit'...
remote: Reusing existing pack: 1857, done.
remote: Total 1857 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (1857/1857), 374.35 KiB | 193.00 KiB/s, done.
Resolving deltas: 100% (772/772), done.
Checking connectivity... done.

git clone作了如下三件事情

  1. 复制远程仓库objects/文件夹中的内容到本地仓库; (对应Receiving objects);
  2. 为所接收到的文件建立索引(对应Resolving deltas);
  3. 为全部的远程分支建立本地的跟踪分支,存储在.git/refs/remote/xxx/下;
  4. 检测远程分支上当前的活跃分支(.git/HEAD文件中存储的内容);
  5. 在当前分支上执行git pull,保证当前分支和工做区与远程分支一致;
参考 What is git actually doing when it says it is “resolving deltas”? - Stack Overflow

除此以外,git会自动在.git/config文件中写入部份内容,

[remote "origin"]
        url = git@git.in.zhihu.com:zhangwang/zhihu-lite.git
        fetch = +refs/heads/*:refs/remotes/origin/*

默认状况下会把clone的源仓库取名origin,在.git/config中存储其对应的地址,本地分支与远程分支的对应规则等。

除了git clone另外一个与远程仓库创建链接的命令为git remote

git remote

git remote 为咱们提供了管理远程仓库的途径。
对远程仓库的管理包括,查看,添加,移除,对远程分支的管理等等。

  1. 查看远程仓库 git remote
$ git remote
origin

# 添加 -v,可查看对应的连接
$ git remote -v
origin    https://github.com/schacon/ticgit (fetch)
origin    https://github.com/schacon/ticgit (push)

# git remote show [remote-name] 可查看更加详细的信息
$ git remote show origin
* remote origin
  Fetch URL: https://github.com/schacon/ticgit
  Push  URL: https://github.com/schacon/ticgit
  HEAD branch: master
  Remote branches:
    master                               tracked
    dev-branch                           tracked
  Local branch configured for 'git pull':
    master merges with remote master
  Local ref configured for 'git push':
    master pushes to master (up to date)
  1. 添加远程仓库 git remote add <shortname> <url>
$ git remote add pb https://github.com/paulboone/ticgit
$ git remote -v
origin    https://github.com/schacon/ticgit (fetch)
origin    https://github.com/schacon/ticgit (push)
pb    https://github.com/paulboone/ticgit (fetch)
pb    https://github.com/paulboone/ticgit (push)
  1. 远程仓库重命名 git remote rename
$ git remote rename pb paul
$ git remote
origin
paul
  1. 远程仓库的移除 git remote rm <name>
$ git remote rm paul
$ git remote
origin
上述示例代码参照 Git - 远程仓库的使用

本地对远程仓库的记录存在于.git/config文件中,在.git/config中咱们能够看到以下格式的内容:

# .git/config
[remote "github"]
    url = https://github.com/zhangwang1990/weixincrawler.git
    fetch = +refs/heads/*:refs/remotes/github/*
[remote "zhangwang"]
    url = https://github.com/zhangwang1990/weixincrawler.git
    fetch = +refs/heads/*:refs/remotes/zhangwang/*
  • [remote] "github":表明远程仓库的名称;
  • url:表明远程仓库的地址
  • fetch:表明远程仓库与本地仓库的对应规则,这里涉及到另一个 Git 命令,git fetch

git fetch

咱们先看git fetch的做用:

  1. git fetch <some remote branch> :同步某个远程分支的改变到本地,会下载本地没有的数据,更新本地数据库,并移动本地对应分支的指向。
  2. git fetch --all会拉取全部的远程分支的更改到本地

咱们继续看看git fetch是如何工做的:

# config中的配置
[remote "origin"]
    url = /home/demo/bare-repo/
    fetch = +refs/heads/*:refs/remotes/origin/* #<remote-refs>:<local-refs> 远程的对应本地的存储位置

fetch的格式为fetch = +<src>:<dst>,其中

  • +号是可选的,用来告诉 Git 即便在不能采用「快速向前合并」也要(强制)更新引用;
  • <src>表明远程仓库中分支的位置;
  • <dst> 远程分支对应的本地位置。

咱们来看一个git fetch的实例,看看此命令是怎么做用于本地仓库的:

git fetch origin

  1. 会在本地仓库中建立.git/refs/remotes/origin文件夹;
  2. 会建立一个名为.git/FETCH_HEAD的特殊文件,其中记录着远程分支所指向的commit 对象;
  3. 若是咱们执行 git fetch origin feature-branch,Git并不会为咱们建立一个对应远程分支的本地分支,可是会更新本地对应的远程分支的指向;
  4. 若是咱们再执行git checkout feature-branch,git 会基于记录在.git/FETCH_HEA中的内容新建本地分支,并在.git/config中添加以下内容,用以保证本地分支与远程分支future-branch的一致
[branch "feature-branch"]
    remote = origin
    merge = refs/heads/feature-branch
git 每次执行 git fetch都会重写 .git/FETCH_HEA

上述fetch的格式也能帮咱们理解git push的一些用法

git push

咱们在本地某分支开发完成以后,会须要推送到远程仓库,这时候咱们会执行以下代码:

git push origin featureBranch:featureBranch
此命令会帮咱们在远程创建分支featureBranch,之因此要这样作的缘由也在于上面定义的fetch模式。
由于引用规格(的格式)是 <src>:<dst>,因此其实会在远程仓库创建分支featureBranch,从这里咱们也能够看出,分支确实是很是轻量级的。

此外,若是咱们执行 git push origin :topic:,这里咱们把 <src>留空,这意味着把远程版本库的 topic 分支定义为空值,也就说会删除对应的远程分支。

回到git push,咱们从资源的角度看看发生了什么?

  1. 从本地仓库的.git/objects/目录,上传到远程仓库的/objects/下;
  2. 更新远程仓库的refs/heads/master内容,指向本地最新的commit;
  3. 更新文件.git/refs/remotes/delta/master内容,指向最新的commit;

说完git push,咱们再来看看 git pull

git pull

此命令的通用格式为 git pull <remote> <branch>
它作了如下几件事情:

  1. git fetch <remote>:下载最新的内容
  2. 查询.git/FETCH_HEAD找到应该合并到的本地分支;
  3. 若是知足要求,没有冲突,执行git merge

git pull 在大多数状况下它的含义是一个 git fetch 紧接着一个 git merge 命令。

至此,经常使用的git命令原理咱们都基本讲解完了。若是你们有一些其它想要了解的命令,咱们能够再一块儿探讨,补充。

一些推荐的 git 资料

Home · geeeeeeeeek/git-recipes Wiki · GitHub
gitlet.js
git-from-the-inside-out
A Hacker’s Guide to Git | Wildly Inaccurate
githug

相关文章
相关标签/搜索