本文来自半个月前我在我司内部进行的分享。一直以为 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 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
文件夹中。安全
Blobs
是Git中最基础的数据类型,一个blob
对象就是一堆字节,一般是一个文件的二进制表示
tree
,有点相似于目录,其内容由对其它tree
及blobs
的指向构成;
commit
,指向一个树对象,并包含一些代表做者及父 commit 的元数据
Tag
,指向一个commit对象,并包含一些元数据
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
中包含两种类型的文件,tree
和 blob
,这就把文件有序的组合起来了,若是咱们知道了根 tree
(能够理解为root
文件夹对应的tree
),咱们就有能力依据此tree
还原整个工做区。
可能咱们很早就据说过 Git 中的每个 commit
存储的都是一个「快照」。理解了tree
对象,咱们就能够较容易的理解「快照」这个词了 ,接下来咱们看看 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
对象存储在/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/stash
与 git stash
命令相关,后文会详细讲解;.git/refs/tag
, 轻量级的tag,与 git tag
命令相关,它也是一个指向某个commit
对象的指针;tag
是一种辅助 Git 作版本控制的对象,上面这种 tag 只是「轻量级tag」 ,此外还存在另外一种「声明式tag」,声明式 tag 对象能够存储更多的信息,其存在于.git/object/
下。
上文已经说过 Git 中存在两种 tag
:
.git/refs/tag/
文件夹下;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 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]
会把文件添加到缓存区。那缓存区本质上是什么呢?
为了理清这个问题,咱们先看下图:
不少地方会说,git 命令操做的是三棵树。三棵树对应的就是上图中的工做区( working directory )、缓存区( Index )、以及 HEAD。
工做区比较好理解,就是可供咱们直接修改的区域,HEAD
实际上是一个指针,指向最近的一次 commit 对象,这个咱们以后会详述。Index
就是咱们说的缓存区了,它是下次 commit 涉及到的全部文件的列表。
回到git add [file]
,这个命令会依次作下面两件事情:
.git/object/
文件夹中添加修改或者新增文件对应的 blob
对象;.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
其中各项的含义以下:
100644
: 100
表明regular file,644
表明文件权限8baef1b4abc478178b004d62031cf7fe6db6f903
:blob对象的名称;0
:当前文件的版本,若是出现冲突,咱们会看到1
,2
;data/d.txt
: 该文件的完整路径Git 还额外提供了一个命令来帮我咱们查看文件在这三棵树中的状态,git status
。
git status
git status
有三个做用:
通常来讲 .git/HEAD
文件中存储着 Git 仓库当前位于的分支:
➜ cat .git/HEAD ref: refs/heads/mate-school--encodeUri
当咱们 git add
某个文件后,git 下一步每每会提示咱们commit
它。咱们接下来看看,commit
过程发生了什么。
git commit
对应到文件层面,git commit
作了以下几件事情:
tree
对象,有多少个修改过的文件夹,就会添加多少个tree
对象;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
的具体用法
git checkout <file>
:此命令能够用来清除未缓存的更改,它能够看作是 git checkout HEAD <file>
的简写,
映射到文件层面,其操做为恢复文件<file>
的内容为,HEAD对应的快照时的内容。其不会影响已经缓存的更改的缘由在于,其实缓存过的文件就是另一个文件啦。
相应的命令还有 git checkout <commit> <file>
能够用来恢复某文件为某个提交时的状态。
git checkout <branch>
切换分支到 <branch> 其其实是修改 .git/HEAD
中的内容为 <branch>
,更新工做区内容为 <branch>
所指向的 commit
对象的内容。
➜ cat .git/HEAD ref: refs/heads/master
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 对象。
三路合并:
新的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 分支之间):
.git/MERGE_HEAD
。此文件的存在说明 Git 正在作合并操做。(记录合并提交的状态)git add
以更新 index 被提交, git commit
基于此 index 生成新的commit
;.git/refs/heads/master
中的内容指向第8步中新生成的 commit
,至此三路合并完成;Git 中的一些命令是以引入的变动即提交这样的概念为中心的,这样一系列的提交,就是一系列的补丁。 这些命令以这样的方式来管理你的分支。git cherry-pick
作的事情是将一个或者多个commit应用到当前commit的顶部,复制commit,会保留对应的二进制文件,可是会修改parent
信息。
在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
具备如下常见用法:
git reset <file>
:从缓存区移除特定文件,可是不会改变工做区的内容git reset
: 重设缓存区,会取消全部文件的缓存git reset --hard
: 重置缓存区和工做区,修改其内容对最新的一次 commit 对应的内容git reset <commit>
: 移动当前分支的末端到指定的commit
处git reset --hard <commit>
: 重置缓存区和工做区,修改其内容为指定 commit 对应的内容相对而言,git reset
是一个相对危险的操做,其危险之处在于可能会让本地的修改丢失,可能会让分支历史难以寻找。
咱们看看git reset
的原理
HEAD
所指向的分支的指向:若是你正在 master 分支上工做,执行 git reset 9e5e64a
将会修改 master
让指向 哈希值为 9e5e64a
的 commit object
。git reset
,上述过程都会发生,不一样用法的区别在于会如何修改工做区及缓存区的内容,若是你用的是 git reset --soft
,将仅仅执行上述过程;git reset
本质上是撤销了上一次的 git commit
命令。执行git commit
,Git 会建立一个新的 commit 对象,并移动HEAD
所指向的分支指向该commit。 而执行git reset
会修改HEAD
所指向的分支指向HEAD~
(HEAD 的父提交),也就是把该分支的指向修改成原来的指向,此过程不会改变index
和工做目录的内容。
—mixed
会更新索引:git reset --mixed
和 git reset
效果一致,这是git reset
的默认选项,此命令除了会撤销一上次提交外,还会重置index
,至关于咱们回滚到了 git add
和 git commit
前的状态。—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
被用来保存当前分支的工做状态,便于再次切换回本分支时恢复。其具体用法以下:
feature
分支上执行git stash 或 git stash save
,保存当前分支的工做状态;feature
分支,执行git stash list
,列出保存的全部stash
,执行 git stash apply
,恢复最新的stash
到工做区;也能够覆盖老一些的stash
, 用法如git stash apply stash@{2}
;
关于git stash
还有其它一些值得关注的点:
git stash
会恢复全部以前的文件到工做区,也就是说以前添加到缓存区的文件不会再存在于缓存区,使用 git stash apply --index
命令,则能够恢复工做区和缓存区与以前同样;git stash
只会储藏已经在索引中的文件。 使用 git stash —include-untracked
或 git stash -u
命令,Git 才会将任何未跟踪的文件添加到stash
;git stash pop
命令能够用来应用最新的stash
,并当即从stash
栈上扔掉它;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,?]?
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
,像已有仓库提交代码的常见工做流是
fork
一份主仓库的代码到本身的远程仓库;clone
本身远程仓库代码到本地;git remote add ...
,便于以后保持本地仓库与主仓库同步git pull
;git push
;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
作了如下三件事情
objects/
文件夹中的内容到本地仓库; (对应Receiving objects
);Resolving deltas
);.git/refs/remote/xxx/
下;.git/HEAD
文件中存储的内容);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
为咱们提供了管理远程仓库的途径。
对远程仓库的管理包括,查看,添加,移除,对远程分支的管理等等。
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)
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)
git remote rename
$ git remote rename pb paul $ git remote origin paul
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
的做用:
git fetch <some remote branch>
:同步某个远程分支的改变到本地,会下载本地没有的数据,更新本地数据库,并移动本地对应分支的指向。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
.git/refs/remotes/origin
文件夹;.git/FETCH_HEAD
的特殊文件,其中记录着远程分支所指向的commit
对象;git fetch origin feature-branch
,Git并不会为咱们建立一个对应远程分支的本地分支,可是会更新本地对应的远程分支的指向;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
,咱们从资源的角度看看发生了什么?
.git/objects/
目录,上传到远程仓库的/objects/
下;refs/heads/master
内容,指向本地最新的commit;.git/refs/remotes/delta/master
内容,指向最新的commit
;说完git push
,咱们再来看看 git pull
。
git pull
此命令的通用格式为 git pull <remote> <branch>
它作了如下几件事情:
git fetch <remote>
:下载最新的内容.git/FETCH_HEAD
找到应该合并到的本地分支;git merge
git pull
在大多数状况下它的含义是一个 git fetch
紧接着一个 git merge
命令。
至此,经常使用的git
命令原理咱们都基本讲解完了。若是你们有一些其它想要了解的命令,咱们能够再一块儿探讨,补充。
Home · geeeeeeeeek/git-recipes Wiki · GitHub
gitlet.js
git-from-the-inside-out
A Hacker’s Guide to Git | Wildly Inaccurate
githug