社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Git

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

前端早读课 • 5 年前 • 1023 次点击  

前言

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

正文从这开始~~

文章导读

  • Git的分区(工作区,暂存区,版本库)

  • Git的原理

  • Git分支

  • 版本的回滚(revert,reset)

  • 代码暂存(stash)

小概述-何为Git

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

一、Git的分区

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

1.1 三大分区
  • 工作区,也叫Working Directory

  • 暂存区,也叫stage,index

  • 版本库,也叫本地仓库,commit History

当我们把代码从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

简单来说,文件都被存储为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 1576979515+0800

  10. committer eleme 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

整理一下各自类型:

  • 9dae-blob

  • 180c-blob

  • 21d0-tree

  • a16b-commit

  • a76c-tree

仔细一想其实也就通了,两个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 --abort:遇到冲突时放弃合并,回到rebase操作之前的状态。

  • git rebase --continue:合并冲突,结合"git add 文件"命令一起,一步一步地解决冲突。

  • git rebase --skip:将引起冲突的commits丢弃掉。

小例子

这里引用一个网上归纳的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是否一致,如果不一致说明一定有文件发生了修改。接下来,对于某一个文件来说,分几种情况:

  • 文件在节点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的内容。

  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文件夹正名

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/55134
 
1023 次点击