【第1864期】手撕Git,告别盲目记忆

作者:噜噜呀

前言

今日早读文章由@噜噜呀授权分享。

正文从这开始~~

文章导读

小概述-何为Git

Git是一个分布式版本控制系统,为了快速高效地处理小到大型项目的所有内容。通过对信息的压缩和摘要,使得所占空间非常小,但能够支持项目版本迅速迭代的开发工具。

一、Git的分区

本章主要从基础入手,先介绍git的分区。

1.1 三大分区

当我们把代码从git hub档下来或者说初始化git项目后,便有了这三个分区的概念。

工作区

工作区应该不陌生,就是我们能看见,直接编辑的区域。对于一些新增的文件,如果没有被add到暂存区,就会以红色的形式放置在工作区。

暂存区

数据暂时存放的区域,对于add git版本控制的文件,就算是进入暂存区啦。可以理解为数据进入本地代码仓库之前存放的区域。由于还没对本地仓库生效,所以是数据暂时存放的区域。

对暂存区的文件修改后,会以蓝色的形式显示。如果第一次创建并add到暂存区的文件,由于远程仓库没有同步,所以会显示绿色。

注:存放在 ".git目录下" 下的index文件(.git/index)中

版本库

在暂存区commit的代码会被放入版本库中。可以理解为一个本地的代码仓库,push的时候,才会把版本库的数据全都发送到远程仓库中。

注:存放在工作区中“.git”目录下。

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的隐藏目录,相信大家都不陌生。

  1. MacBook-Pro:wuya eleme$ git init

  2. 已初始化空的 Git仓库于 /Users/eleme/wuya/.git/

然后查看一下.git的目录树

  1. MacBook-Pro:wuya eleme$ tree -a

  2. .

  3. └── .git

  4. ├── HEAD

  5. ├── config

  6. ├── description

  7. ├── hooks

  8. │ ├── applypatch-msg.sample

  9. │ ├── commit-msg.sample

  10. │ ├── fsmonitor-watchman.sample

  11. │ ├── post-update.sample

  12. │ ├── pre-applypatch.sample

  13. │ ├── pre-commit.sample

  14. │ ├── pre-push.sample

  15. │ ├── pre-rebase.sample

  16. │ ├── pre-receive.sample

  17. │ ├── prepare-commit-msg.sample

  18. │ └── update.sample

  19. ├── info

  20. │ └── exclude

  21. ├── objects

  22. │ ├── info

  23. │ └── pack

  24. └── refs

  25. ├── heads

  26. └── tags


  27. 9 directories, 15 files

我们会发现,有一个叫Objects的目录。这个目录就是存储文件变化的核心。我们往工作区中存入一个测试文件a.md和一个test文件夹并查看objects发生的变化。

  1. MacBook-Pro:wuya eleme$ echo 'test1'> a.md

  2. MacBook-Pro:wuya eleme$ mkdir test

  3. MacBook-Pro:wuya eleme$ echo 'test2'> test/b.md

  4. MacBook-Pro:wuya eleme$ git add a.md test

  5. MacBook-Pro:wuya eleme$ tree -a .git/objects

  6. .git/objects

  7. ├── 18

  8. │ └── 0cf8328022becee9aaa2577a8f84ea2b9f3827

  9. ├── 9d

  10. │ └── aeafb9864cf43055ae93beb0afd6c7d144bfa4

  11. ├── info

  12. └── pack


  13. 4 directories, 2 files

注意,文件夹放入到暂存区后,并不会马上在objects中显示,commit后才会。此时多了两个文件,其实就是修改过的两个文件以及修改内容。

Objects下存放的文件名就是根据SHA1算法哈希的“指纹”,为了能够在本仓库中和其他文件区分出来。文件内容就是Git将信息压缩后形成的二进制文件。

通过git cat-file [-t] [-p],可以看到Object的类型与文件的内容。

  1. MacBook-Pro:wuya eleme$ git cat-file -t 9dae

  2. blob

  3. MacBook-Pro:wuya eleme$ git cat-file -p 9dae

  4. test1

通过git hash-object a.md能够显示该文件在本仓库生成的hash值,与之前的目录树显示是对应的。

  1. MacBook-Pro:wuya eleme$ git hash-object a.md

  2. 9daeafb9864cf43055ae93beb0afd6c7d144bfa4

2.2 git Object的类型

git Object有三种类型:

简单来说,文件都被存储为Blob类型,文件夹则为Tree类型,每次提交的节点被存储为Commit类型数据。因此,Git会以这三种类型来存储我们的文件。简单看下目录存储的映射关系:

初步猜想,如果把这些文件都commit到代码库,objects目录应该会有4个目录。即2个blob,1个tree,1个commit。

  1. MacBook-Pro:wuya eleme$ git commit -a -m "加入到代码库中,观察objects目录变化"

  2. [master(根提交) a16b538] 加入到代码库中,观察objects目录变化

  3. 2 files changed, 2 insertions(+)

  4. create mode 100644 a.md

  5. create mode 100644 test/b.md

  6. MacBook-Pro:wuya eleme$ tree -a .git/objects

  7. .git/objects

  8. ├── 18

  9. │ └── 0cf8328022becee9aaa2577a8f84ea2b9f3827

  10. ├── 21

  11. │ └── d0758079bdf2c8f7514687174454c804eb0c74

  12. ├── 9d

  13. │ └── aeafb9864cf43055ae93beb0afd6c7d144bfa4

  14. ├── a1

  15. │ └── 6b5382a9b646a7df8d21301391f29b2f7bfb65

  16. ├── a7

  17. │ └── 6c93bb75184ef4b34c88a301c2351ae2219407

  18. ├── info

  19. └── pack


  20. 7 directories, 5 files

然鹅事实却是....5个目录!多出的那一个是什么?一个一个输出看看。

  1. MacBook-Pro:wuya eleme$ git cat-file -p 9dae

  2. test1

  3. MacBook-Pro:wuya eleme$ git cat-file -p 180c

  4. test2

  5. MacBook-Pro:wuya eleme$ git cat-file -p 21d0

  6. 100644 blob 180cf8328022becee9aaa2577a8f84ea2b9f3827 b.md

  7. MacBook-Pro:wuya eleme$ git cat-file -p a16b

  8. tree a76c93bb75184ef4b34c88a301c2351ae2219407

  9. author eleme <xxxx@qq.com> 1576979515+0800

  10. committer eleme <xxxx@qq.com> 1576979515+0800


  11. 加入到代码库中,观察objects目录变化

  12. MacBook-Pro:wuya eleme$ git cat-file -p a76c

  13. 100644 blob 9daeafb9864cf43055ae93beb0afd6c7d144bfa4 a.md

  14. 040000 tree 21d0758079bdf2c8f7514687174454c804eb0c74 test

整理一下各自类型:

仔细一想其实也就通了,两个tree是git根目录和test目录。

可以得出这样一个结论:每一次commit,都会生成与之对应的commit hash值。查看历史commit也很容易得出这个结论:

三、Git分支

3.1 初探Git分支

在学习Git分支之前,还是从git的目录树入手。

  1. MacBook-Pro:wuya eleme$ tree -a .git

  2. .git

  3. ├── ......

  4. ├── HEAD

  5. └── refs

  6. ├── heads

  7. │ └── master

  8. ├── remotes

  9. │ └── origin

  10. │ └── HEAD

  11. └── tags

不难看出refs目录就是用来记录当前对分支的引用信息,包括本地分支,远程分支,标签。

heads记录的是本地所有分支,remotes和HEAD一样,指向对应的某个远程分支。

  1. MacBook-Pro:wuya eleme$ cat .git/refs/heads/master

  2. a16b5382a9b646a7df8d21301391f29b2f7bfb65

细心些就会发现,这个hash值就是commit节点的hash值。

而HEAD就是存储当前在哪个本地分支。查看其内容,可以发现:

  1. MacBook-Pro:.git eleme$ cat HEAD

  2. ref: refs/heads/master

也就意味着,我们在本地的master上。除此之外,还可以通过git branch来创建其他分支。

  1. MacBook-Pro:.git eleme$ git branch feature/dev

  2. MacBook-Pro:.git eleme$ git branch feature/wuya

切换到其他分支并查看分支信息:

  1. elemedeMacBook-Pro:wuya eleme$ git checkout feature/dev

  2. 切换到分支 'feature/dev'

  3. elemedeMacBook-Pro:wuya eleme$ git branch -vv

  4. * feature/dev a16b538 加入到代码库中,观察objects目录变化

  5. feature/wuya a16b538 加入到代码库中,观察objects目录变化

  6. master a16b538 加入到代码库中,观察objects目录变化

因此可知分支当前的指针指向最近一次commit的节点。通过谁创建的分支,就沿用谁的指针。注:未被放入代码库的文件会在分支切换时被抛弃,造成严重后果。

3.2 分支的合并

分支的合并有两种方式,merge和rebase。

相同点:都是从一个分支获取并合并到当前分支。

merge:自动创建一个新的commit,如果遇到冲突,仅需要修改后重新commit。

每次都记录了真实详细的commit,但是在commit频繁的时候,会看到分支比较乱。比如这样,全是merge产生的节点:

rebase:找公共的节点,直接合并之前commit历史。

这样能得到简洁的分支发展历史,去掉了merge commit。但是如果合并时出现了问题,没有留下痕迹,不好定位。

小例子

这里引用一个网上归纳的git rebase工作流:

  1. git rebase

  2. while(存在冲突) {

  3. //找到当前冲突文件,编辑解决冲突

  4. git status

  5. git add -u

  6. git rebase --continue

  7. if( git rebase --abort )

  8. break;

  9. }

注:最好不要在公共分支上使用rebase,如果前后基本上不会有别人改动你的分支,那么推荐rebase。

3.3 分支的冲突

冲突的产生

冲突是从合并的时候产生的。git分支的合并,其实就是tree和tree的合并。我们在feature/dev上执行git merge master时。git会先找到这两个分支是从哪个指针创建出来的,称之为“merge base”。然后检查这两次的tree是否一致,如果不一致说明一定有文件发生了修改。接下来,对于某一个文件来说,分几种情况:

此时就需要开发人员商定,解决冲突。

四、版本的回滚

如果想要版本回退,就离不开reset和revert。

4.1 revert

这个就一目了然了,执行git revert后,将回退到上一个commit的版本。

4.2 reset

前段时间,线上出了好多空指针的bug,当我查看日志定位到某一代码行时,发现该行定位不到对应的方法中。这时候就必须切换到线上的代码版本进行排查了。

git reset分为三种模式:

由于每一次的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的内容。

  1. MacBook-Pro:young eleme$ git stash

  2. 保存工作目录和索引状态 WIP on wuya: 82371a5上一次commit写的message

git stash list

显示之前压栈的所有记录。

  1. MacBook-Pro:young eleme$ git stash list

  2. stash@{0}: WIP on aaa: 82371a5上一次commit写的message

git stash clear

清空Git栈。

git stash apply

从Git栈中读取上一次暂存的那些代码,恢复工作区。

  1. MacBook-Pro:young eleme$ git stash apply

  2. 位于分支 wuya

  3. 您的分支与上游分支 'origin/wuya'一致。


  4. 尚未暂存以备提交的变更:

  5. (使用 "git add <文件>..."更新要提交的内容)

  6. (使用 "git checkout -- <文件>..."丢弃工作区的改动)


  7. 修改:src/main/java/com/young/test/test1.java


  8. 修改尚未加入提交(使用 "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

为你推荐


【第1823期】Git子仓库深入浅出


【第1739期】为Git仓库里的.idea文件夹正名