手撕Git,告别盲目记
手撕Git,告别盲目记忆
引言
Git在工作中经常用到,但是指令太多,网上的说法又不太通俗。总会让想要学习的同学抓不到重点,或者望而却步。这篇文章的目的就是希望读后能够按照原理,系统地记忆一些常用/关键的命令。也算是我这个刚入互联网的小白对社会的一些小小福报~
其中若有不当之处,欢迎大佬指出。
开局一个赞 ,内容一看就懂~
友情提示:文章有连贯性,跳着看可能会比较蛋疼…
文章导读
- Git的分区(工作区,暂存区,版本库)
- Git的原理
- Git分支
- 版本的回滚(revert,reset)
- 代码暂存(stash)
小概述-何为Git
Git是一个分布式版本控制系统,为了快速高效地处理小到大型项目的所有内容。通过对信息的压缩和摘要,使得所占空间非常小,但能够支持项目版本迅速迭代的开发工具。
一、Git的分区
本章主要从基础入手,先介绍git的分区。
1.1 三大分区
- 工作区,也叫Working Directory
- 暂存区,也叫stage,index
- 版本库,也叫本地仓库,commit History
当我们把代码从git hub档下来或者说初始化git项目后,便有了这三个分区的概念。
文件在Git不同分区中的表现
工作区
工作区应该不陌生,就是我们能看见,直接编辑的区域。对于一些新增的文件,如果没有被add到暂存区,就会以红色的形式放置在工作区。
暂存区
数据暂时存放的区域,对于add git版本控制的文件,就算是进入暂存区啦。可以理解为数据进入本地代码仓库之前存放的区域。由于还没对本地仓库生效,所以是数据暂时存放的区域。
对暂存区的文件修改后,会以蓝色的形式显示。如果第一次创建并add到暂存区的文件,由于远程仓库没有同步,所以会显示绿色。
注:存放在 “.git目录下” 下的index文件(.git/index)中
版本库
在暂存区commit的代码会被放入版本库中。可以理解为一个本地的代码仓库,push的时候,才会把版本库的数据全都发送到远程仓库中。
注:存放在工作区中“.git”目录下。
图片来源于网络
扩展阅读:
https://juejin.im/post/5b6c4eeff265da0f4d0da3fa
https://www.runoob.com/git/git-workspace-index-repo.html
1.2 涉及指令
1.2.1 分区转换指令
git add
数据从工作区转移至暂存区
git commit
数据从暂存区转移至版本库,也就是本地仓库
git push
数据从版本库中发送到远程仓库
指令太多?一张图就能记下~
1.2.2 分区对比指令
git diff
工作区与暂存区对比
git diff head
工作区与版本库对比
git diff --cached
暂存区与版本库对比
指令太多?一张图就能记下~
二、Git的原理
操作Git代码库前,一定要了解Git是怎么记录每次提交的代码变化的?换句话说,每一次commit在保证开发效率的前提下,都提交了什么?
2.1 git如何存储文件/目录信息
首先我们使用git init
,初始化一个新的git项目。这个目录会在项目的根目录下创建.git的隐藏目录,相信大家都不陌生。
MacBook-Pro:wuya eleme$ git init
已初始化空的 Git 仓库于 /Users/eleme/wuya/.git/
然后查看一下.git的目录树
MacBook-Pro:wuya eleme$ tree -a
.
└── .git
├── HEAD
├── config
├── description
├── hooks
│ ├── applypatch-msg.sample
│ ├── commit-msg.sample
│ ├── fsmonitor-watchman.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
9 directories, 15 files
我们会发现,有一个叫Objects的目录。这个目录就是存储文件变化的核心。我们往工作区中存入一个测试文件a.md和一个test文件夹并查看objects发生的变化。
MacBook-Pro:wuya eleme$ echo 'test1' > a.md
MacBook-Pro:wuya eleme$ mkdir test
MacBook-Pro:wuya eleme$ echo 'test2' > test/b.md
MacBook-Pro:wuya eleme$ git add a.md test
MacBook-Pro:wuya eleme$ tree -a .git/objects
.git/objects
├── 18
│ └── 0cf8328022becee9aaa2577a8f84ea2b9f3827
├── 9d
│ └── aeafb9864cf43055ae93beb0afd6c7d144bfa4
├── info
└── pack
4 directories, 2 files
注意,文件夹放入到暂存区后,并不会马上在objects中显示,commit后才会。此时多了两个文件,其实就是修改过的两个文件以及修改内容。
Objects下存放的文件名就是根据SHA1算法哈希的“指纹”,为了能够在本仓库中和其他文件区分出来。文件内容就是Git将信息压缩后形成的二进制文件。
通过git cat-file [-t] [-p]
,可以看到Object的类型与文件的内容。
MacBook-Pro:wuya eleme$ git cat-file -t 9dae
blob
MacBook-Pro:wuya eleme$ git cat-file -p 9dae
test1
通过git hash-object a.md
能够显示该文件在本仓库生成的hash值,与之前的目录树显示是对应的。
MacBook-Pro:wuya eleme$ git hash-object a.md
9daeafb9864cf43055ae93beb0afd6c7d144bfa4
2.2 git Object的类型
git Object有三种类型:
- Blob
- Tree
- Commit
简单来说,文件都被存储为Blob类型,文件夹则为Tree类型,每次提交的节点被存储为Commit类型数据。因此,Git会以这三种类型来存储我们的文件。简单看下目录存储的映射关系:
初步猜想,如果把这些文件都commit到代码库,objects目录应该会有4个目录。即2个blob,1个tree,1个commit。
MacBook-Pro:wuya eleme$ git commit -a -m "加入到代码库中,观察objects目录变化"
[master(根提交) a16b538] 加入到代码库中,观察objects目录变化
2 files changed, 2 insertions(+)
create mode 100644 a.md
create mode 100644 test/b.md
MacBook-Pro:wuya eleme$ tree -a .git/objects
.git/objects
├── 18
│ └── 0cf8328022becee9aaa2577a8f84ea2b9f3827
├── 21
│ └── d0758079bdf2c8f7514687174454c804eb0c74
├── 9d
│ └── aeafb9864cf43055ae93beb0afd6c7d144bfa4
├── a1
│ └── 6b5382a9b646a7df8d21301391f29b2f7bfb65
├── a7
│ └── 6c93bb75184ef4b34c88a301c2351ae2219407
├── info
└── pack
7 directories, 5 files
然鹅事实却是…5个目录!多出的那一个是什么?一个一个输出看看。
MacBook-Pro:wuya eleme$ git cat-file -p 9dae
test1
MacBook-Pro:wuya eleme$ git cat-file -p 180c
test2
MacBook-Pro:wuya eleme$ git cat-file -p 21d0
100644 blob 180cf8328022becee9aaa2577a8f84ea2b9f3827 b.md
MacBook-Pro:wuya eleme$ git cat-file -p a16b
tree a76c93bb75184ef4b34c88a301c2351ae2219407
author eleme <xxxx@qq.com> 1576979515 +0800
committer eleme <xxxx@qq.com> 1576979515 +0800
加入到代码库中,观察objects目录变化
MacBook-Pro:wuya eleme$ git cat-file -p a76c
100644 blob 9daeafb9864cf43055ae93beb0afd6c7d144bfa4 a.md
040000 tree 21d0758079bdf2c8f7514687174454c804eb0c74 test
整理一下各自类型:
- 9dae-blob
- 180c-blob
- 21d0-tree
- a16b-commit
- a76c-tree
仔细一想其实也就通了,两个tree是git根目录和test目录。
可以得出这样一个结论:每一次commit,都会生成与之对应的commit hash值。查看历史commit也很容易得出这个结论:
扩展阅读:
https://mp.weixin.qq.com/s/d4WA02Y22gdWRbmmwfPEHQ
三、Git分支
3.1 初探Git分支
在学习Git分支之前,还是从git的目录树入手。
MacBook-Pro:wuya eleme$ tree -a .git
.git
├── ......
├── HEAD
└── refs
├── heads
│ └── master
├── remotes
│ └── origin
│ └── HEAD
└── tags
不难看出refs目录就是用来记录当前对分支的引用信息,包括本地分支,远程分支,标签。
heads记录的是本地所有分支,remotes和HEAD一样,指向对应的某个远程分支。
MacBook-Pro:wuya eleme$ cat .git/refs/heads/master
a16b5382a9b646a7df8d21301391f29b2f7bfb65
细心些就会发现,这个hash值就是commit节点的hash值。
而HEAD就是存储当前在哪个本地分支。查看其内容,可以发现:
MacBook-Pro:.git eleme$ cat HEAD
ref: refs/heads/master
也就意味着,我们在本地的master上。除此之外,还可以通过git branch
来创建其他分支。
MacBook-Pro:.git eleme$ git branch feature/dev
MacBook-Pro:.git eleme$ git branch feature/wuya
切换到其他分支并查看分支信息:
elemedeMacBook-Pro:wuya eleme$ git checkout feature/dev
切换到分支 'feature/dev'
elemedeMacBook-Pro:wuya eleme$ git branch -vv
* feature/dev a16b538 加入到代码库中,观察objects目录变化
feature/wuya a16b538 加入到代码库中,观察objects目录变化
master a16b538 加入到代码库中,观察objects目录变化
因此可知分支当前的指针指向最近一次commit的节点。通过谁创建的分支,就沿用谁的指针。注:未被放入代码库的文件会在分支切换时被抛弃,造成严重后果。
3.2 分支的合并
分支的合并有两种方式,merge和rebase。
相同点:都是从一个分支获取并合并到当前分支。
merge:自动创建一个新的commit,如果遇到冲突,仅需要修改后重新commit。
每次都记录了真实详细的commit,但是在commit频繁的时候,会看到分支比较乱。比如这样,全是merge产生的节点:
rebase:找公共的节点,直接合并之前commit历史。
这样能得到简洁的分支发展历史,去掉了merge commit。但是如果合并时出现了问题,没有留下痕迹,不好定位。
- git rebase --abort:遇到冲突时放弃合并,回到rebase操作之前的状态。
- git rebase --continue:合并冲突,结合"git add 文件"命令一起,一步一步地解决冲突。
- git rebase --skip:将引起冲突的commits丢弃掉。
小例子
这里引用一个网上归纳的git rebase工作流:
git rebase
while(存在冲突) {
//找到当前冲突文件,编辑解决冲突
git status
git add -u
git rebase --continue
if( git rebase --abort )
break;
}
注:最好不要在公共分支上使用rebase,如果前后基本上不会有别人改动你的分支,那么推荐rebase。
扩展阅读:
https://blog.csdn.net/chenansic/article/details/44122107
3.3 分支的冲突
冲突的产生
冲突是从合并的时候产生的。git分支的合并,其实就是tree和tree的合并。我们在feature/dev上执行git merge master
时。**git会先找到这两个分支是从哪个指针创建出来的,称之为“merge base”。然后检查这两次的tree是否一致,如果不一致说明一定有文件发生了修改。**接下来,对于某一个文件来说,分几种情况:
- 文件在节点6,节点3,merge base中的hash值都相同。说明没有被修改过。不会有冲突。
- 文件在节点6和merge base或者节点3和merge base的hash值相同时,此时直接更新文件的变化。
- 文件在节点6,merge request,master上的hash值都不同,冲突就产生了。
此时就需要开发人员商定,解决冲突。
四、版本的回滚
如果想要版本回退,就离不开reset和revert。
4.1 revert
这个就一目了然了,执行git revert
后,将回退到上一个commit的版本。
4.2 reset
前段时间,线上出了好多空指针的bug,当我查看日志定位到某一代码行时,发现该行定位不到对应的方法中。这时候就必须切换到线上的代码版本进行排查了。
git reset分为三种模式:
- soft
- mixed
- hard
由于每一次的commit都会产生与之对应的hash值,所以借助这个进行重置就轻松多了。
git reset --hard commit的hash值
会重置暂存区和工作区,完全重置为指定的commit节点。当前分支没有commit的代码必然会被清除。
git reset --soft commit的hash值
会保留工作目录,并把指定的commit节点与当前分支的差异都存入暂存区。也就是说,没有被commit的代码也能够保留下来。
git reset commit的hash值
不带参数,也就是mixed模式。将会保留工作目录,并且把工作区,暂存区以及与reset的差异都放到工作区,然后清空暂存区。因此执行后,只要有所差异,文件都会变成红色,变得难以区分。
一般情况下,我们使用soft模式,既能保留暂存区,又能reset到某个分支。
五、代码暂存
当我们在当前分支工作时,不得已需要切换到其他分支处理事情而不想commit时(如果commit多了,会污染log),可以使用git stash
将那些数据都暂存到Git提供的栈中。用法很简单~
git stash
暂存修改过的代码,保存在Git栈中,然后将工作区还原成上一次commit的内容。
MacBook-Pro:young eleme$ git stash
保存工作目录和索引状态 WIP on wuya: 82371a5 上一次commit写的message
git stash list
显示之前压栈的所有记录。
MacBook-Pro:young eleme$ git stash list
stash@{0}: WIP on aaa: 82371a5 上一次commit写的message
git stash clear
清空Git栈。
git stash apply
从Git栈中读取上一次暂存的那些代码,恢复工作区。
MacBook-Pro:young eleme$ git stash apply
位于分支 wuya
您的分支与上游分支 'origin/wuya' 一致。
尚未暂存以备提交的变更:
(使用 "git add <文件>..." 更新要提交的内容)
(使用 "git checkout -- <文件>..." 丢弃工作区的改动)
修改: src/main/java/com/young/test/test1.java
修改尚未加入提交(使用 "git add" 和/或 "git commit -a")
参考
https://juejin.im/post/5b6c4eeff265da0f4d0da3fa https://www.runoob.com/git/git-workspace-index-repo.html https://mp.weixin.qq.com/s/d4WA02Y22gdWRbmmwfPEHQ https://blog.csdn.net/chenansic/article/details/44122107 https://zhuanlan.zhihu.com/p/96631135
关于本文 作者:@噜噜呀 原文:https://zhuanlan.zhihu.com/p/98679880