美团一面:Git 是如何工作的?(推荐阅读)

你好,我是猿java

Git 是如何工作的?暂停 10秒:你能在脑海里构思出一幅 Git工作的全景图吗?

最近,有小伙伴私信我,说他因为这个问题,倒在了美团一面的路上。借此机会,特来聊聊 Git是如何工作的?整体大纲如下图:

Git是什么

老规矩,在讲解一个技术点之前,先弄清楚它的概念,看看官方是怎么推销自己的,如下图:

大致意思是:Git 是一个免费的开源分布式版本控制系统,旨在快速高效地处理从小型到超大型项目的所有内容。




Git 易于学习,占用空间小,性能快如闪电。它超越了 Subversion、CVS、Perforce 和 ClearCase 等 SCM 工具,具有廉价的本地分支、方便的暂存区域和多个工作流程等功能。




开源、分布式、快如闪电、易学习、优于其他SCM 工具,官方的描述是不是太诱惑了?不过,作为技术人员,我们显然不会为这种宣传式的介绍买单,有没有干货呢?




当然有,直接上菜!

Git对比其它SCM

Git 对比其他 SCM工具,最大的差异点有 3点:架构,数据存储,完整性保证。

架构

Git采用的是分布式设计,每个克隆都是完整的版本库,因此能更好地支持离线操作和本地提交;其它 SCM是集中式管理,重度依赖网络,离线模式工作困难,另外,集中式还会让我们直接想到单点故障。

数据存储

其它类型的SCM(CVS、Subversion、Perforce ),数据保存都是基于 delta差异存储,如下图:

而 Git是全量快照存储,另外,Git为了效率,如果文件没有被修改,Git不再重新存储该文件,而只保留一个链接指向之前存储的文件。如下图,虚线是对实线的链接指向:

关于 Git在数据存储上的优势,在下面 git branch 部分就能很好地体现。

保证完整性

Git 中所有的数据在存储前都需要通过 SHA-1散列(一种 hash算法),计算出一个由 40 个十六进制字符(0-9 和 a-f)组成的字符串校验和(哈希码),然后以该哈希码作为引用。因此,当任何文件内容或目录内容被修改,通过这个哈希码就能很快被发现。

3种状态

在 Git中,文件有三种状态:modified(已修改)、staged(已暂存)、committed(已提交)。

  • 已修改:表示文件在工作区被修改了,但还未进入缓存区;

  • 已暂存:表示已修改的文件进入了缓存区,即将被提交;

  • 已提交:表示缓存区的数据被提交到本地仓库中;

4个核心区域

Git 包含 4个核心区域:

  • 工作区: Working Directory

  • 暂存区: Staging Area

  • 本地仓库: Local Repository

  • 远程仓库: Remote Repository

工作区

工作区,顾名思义,就是我们干活的区域,一般是电脑上肉眼可见的目录。如下图:磁盘上 yuanjava/git-project 目录就是一个工作区。

暂存区

暂存区,有点类似于草稿箱,用 Git 的术语叫做"index(索引)",本质是一个二进制文件,即 {工作目录}/.git/index,如下图:

****

本地仓库

本地仓库,就是 .git目录,它完整地保存了 git 项目的历史记录,包含了项目的所有文件、提交历史、分支、标签等信息。通过执行 git init 命令就能在工作区下生成一个 .git目录。如下图:

远程仓库

远程仓库,其实就是代码的远程服务器。比如:GitHub、GitLab 或自建的服务器。远程仓库可以被多个开发者访问和操作,允许团队成员协同工作,共享代码并进行版本控制。

最后,用一张图来形象地描述 3 种状态和 4个核心区域:

为了更好理解上图的核心内容,我们以代码提交这个过程为例:

  1. 在工作区中进行文件操作,被修改后的文件,状态就会变成 modified;

  2. 执行 git add 命令,Git的文件状态变成 staged,更改的部分会被添加到暂存区;

  3. 执行 git commit 命令,已暂存的文件会被提交到本地仓库,状态变为committed;

  4. 执行 git push,本地仓库的代码会被同步到远程仓库;

上述描述的 4个核心区域,其实都是 Git的一些逻辑结构,为了更深入地了解这些逻辑结构对应的物理结构,也就是磁盘目录,我们就不得不扒一扒 .git 这个神秘的隐藏目录。

.git目录详解

.git 是一个隐藏目录,位于工作目录下,它是通过执行 'git init ' 或者 'git clone ' 命令生成的。如下图,执行 ll -a 指令,可以看到 .git目录的所有信息:

.git 目录下的子目录和文件比较多,这里重点讲解:HEAD,refs,index,objects, logs:

HEAD

HEAD 文件是一个特殊的指针,指向当前所在的本地分支,HEAD 文件的内容是:"ref:refs/heads/{分支名}"。

如下图:当分支切换为 master时,HEAD文件的内容为"ref:refs/heads/master",当分支切换为 test时,HEAD文件的内容为"ref:refs/heads/test"

refs

refs是一个目录,包含 3个子目录:

  1. heads:存储本地分支引用
  2. remote:存储远程分支引用
  3. tags:存储标签的引用

如下图:

目录 refs/heads/ 下面存放的是一些以分支名命名的文件, refs/heads/ {分支名}的结构和上文 HEAD 文件中的内容是对应的,每个分支名对应一个文件,记录了该分支最后一个commit的校验和,而这个校验和就是用来定位 .git/object/{校验和前2位}/{校验和剩余38位} 这个文件,如下图:

objects

objects 是一个相当重要的目录,存储了所有分支所有版本的数据,包括文件内容、目录结构、提交历史等。

objects的目录设计也是相当的精巧,结构如下:

  1. 子目录:Git 使用 SHA-1哈希算法对对象内容进行哈希计算,然后将哈希值的前两个字符作为目录名,将后 38 个字符作为文件名。这样的目录结构有助于将大量的对象分散存储,提高效率。

    例如,如果文件哈希值是 ab123ds...,那么文件路径就是 .git/objects/ab/123ds...

  2. 文件:每个文件的内容存储在与其哈希值对应的文件中。文件可以是 blob 对象(存储文件内容)、tree 对象(存储目录结构)、commit 对象(存储提交信息)等。

    • Blob 对象:存储文件的真实内容,对应于工作目录中的文件;
    • Tree 对象:存储目录结构,包括文件和子目录;
    • Commit 对象:存储提交的元数据,包括作者、提交时间、指向树对象的引用等;
  3. 信息压缩:对象文件是经过 zlib 压缩的二进制数据。这种压缩有助于减小存储空间,并提高传输效率。

index

index 是一个二进制文件,也就是上文说的暂存区,它记录当前目录结构和文件的索引信息,用于构建下一次提交的快照;初始状态下不存在该文件,需要执行第一个init add 命令后生成该文件;如下图:

logs

logs 是一个目录,存储引用的更新历史,例如分支的移动、合并等操作记录,初始状态下该目录不存在,在执行第一个git commit文件时生成,logs/HEAD 记录的是commit的log,可以通过 git log 命令查看;

总结,通过上面对.git目录的分析,我们能看出每个逻辑结构后面对应的真实磁盘目录是什么,同时,还可以梳理出在 Git 查找当前分支代码的整套流程:

  1. 通过 .git/HEAD 文件找出当前指向的分支名A;
  2. 查看 .git/refs/heads/A 文件,文件的内容就是分支A最后一次提交的哈希值;
  3. 定位 .git/object/{哈希值前2位}/{哈希值剩余38位} 二进制文件,该文件就是分支A 最后提交对应的数据仓库;
  4. 对于 .git/index文件,当分支切换后,会用步骤 3中状态为 staged 文件信息进行数据恢复;
    1. Git分支

有了上文内容的铺垫,我们再来分析 Git分支就会轻松很多,这里挑选日常开发工使用频率最高的几个功能,另外,在图形描绘时,我会标注磁盘目录,方便大家更好地对照逻辑变更与磁盘目录变更的关系。

分支创建****

git 创建分支最常用的命令是:

css 复制代码
git branch 分支名git checkout -b 分支名

在默认情况下,.git/HEAD 指向的是工程创建时最原始的分支,这里以 master为例。如下图:

当我们基于 master创建一个新分支 test时,其实就是创建了一个可移动的新指针,如下图:

在 test新分支上做修改,并且产生了一个新的提交 c3,test指针会移动 c3,如下图:

分支合并

分支合并最常用的命令

bash 复制代码
# 将两个或更多的开发历史合并在一起git merge  
# 将提交重新应用到另一个基准提交的顶部git rebase 

快进合并(fast-forward)****

如下图:假如把 test 分支合并到 master分支,由于 test 所指向的提交 c3 是 c2 的直接后继, 因此 Git 会直接将 master指针向前移动。换句话说,当合并两个分支时, 如果顺着一个分支走下去能够到达另一个分支,那么 Git 在合并的时候, 只会简单地将指针向前推进(指针右移),再加上合并不存在冲突,因此,这种合并叫做 "快进(fast-forward)"。

三方合并(three-way merge)

如下图:假如线上出现紧急bug,需要从 master 切出一个新分支进行修改,并且该分支最终被合并到 master,即 c2 移动到 c4,此时 mater指向了 c4。

在处理完 bug后,重新切到 test分支继续工作,在一切准备 ok后,需要把 test 合并到 master分支。

因为 master不能顺着某个分支到达 test,Git 会使用两个分支所指的快照(c3 和 c4)以及这两个分支的公共祖先(c2),做一个简单的三方合并,这种方法叫做三方合并。此时合并会出现两种情况:

  1. c3 和 c4 不存在冲突,Git会自动合并;
  2. c3 和 c4 存在冲突,Git不会自动合并,而是停下来,需要我们手动解决冲突,然后重新提交;

rebase 合并

rebase:中文翻译 变基 或者 重新设置基线;

git rebase:将提交重新应用到另一个基线提交的顶部。这句话看起来很晦涩,但是如果把句子补全成"把当前分支的基线应用到另外一个分支基线的顶部",这样是不是就更容易理解?这里的是指两个分支最近的公共祖先。

如下图:master 和 test 两个分支最近的公共祖先是 c2, 因此基线就是 c2。

git rebase master,把 test的基线 c2变成 master 基线c2的顶部 c4,也就是把 c3 指向 c2 变成 c3指向 c4。因为 test分支在 rebase之后内容变更了,所以 c4对应的磁盘目录也就变成了 .git/refs/c4'。

反之,git rebase test,那就把 master的基线 c2变成 test 基线c2的顶部 c3,也就是把 c4 指向 c2 变成 c4指向 c3。因为 master在 rebase之后内容变更了,所以 c3对应的磁盘目录也就变成了 .git/refs/c3'。

最后,更直白地描述 git rebase:把另一个分支线性地包含到当前分支。

git merge 和 git rebase对比

git merge 保留了每个分支上的提交,合并会导致提交历史中可能出现较多的分叉和合并点;git rebase 会保持线性的提交历史;

git merge 对于冲突,Git 会自动创建合并提交,并在冲突解决后完成合并;git rebase 当冲突发生时,Git 会在重新应用提交的过程中停止,让用户解决冲突后继续,这意味着在 rebase 过程中可能会多次冲突处理。

  1. Git常用命令

git init

git init 命令的功能如下:

  • 初始化仓库:将当前工作区初始化成一个新的 git仓库;
  • 生成隐藏的 .git目录:创建一个名为.git的子目录,其中包含了Git用于跟踪项目历史、版本控制等所需的全部内容。
  • 默认配置:初始化 git仓库时,会生成默认的配置文件(如 .git/config),用于设置 git的行为和属性。

git init 命令执行后,在磁盘上的表现是工作区添加一个.git隐藏目录,该目录包含了一些默认的子目录和文件:

git add

'git add' 命令是将工作区文件添加到暂存区,实质上是在创建/更新 .git/index 文件,主要的过程为:

  1. 更新暂存区状态:git add 会将工作区中指定的文件更改或新文件的当前状态记录到 .git/index 文件中;
  2. 创建/更新索引记录:给被操作的文件生成一个哈希值,并将该文件的元数据(如文件名、文件类型、权限等)保存到 .git/index 文件中;
  3. 暂存文件快照: .git/index 文件会记录每个被暂存的文件的快照信息,用于创建提交时的快照;

如下图,在 test分支的工作区添加一个 test.txt文件,执行'git add . ' 命令后会在.git目录下生成一个 index文件,这个就是暂存区的核心。

将上述过程抽象成下面的模型:

git commit

'git commit' 命令是将暂存区的内容提交到本地仓库。对.git的修改为:

  1. 更新 .git/objects

在 .git/objects 目录中创建新的对象,这些对象会使用 SHA-1 哈希值作为标识,用于在仓库中唯一标识每个提交对象、树对象和文件对象。

因此,每一次 commit都会在 .git/objects下创建一个目录,目录名为此次提交到哈希值的前两位,如下图:

  1. 更新索引文件(index)

在 .git/index 中更新索引文件,记录新提交的快照和元数据信息;

  1. 更新 .git/refs

在 .git/refs/heads 中增加一个以分支名命名的文件(.git/refs/heads/test),文件中记录了该分支最后一次commit的hash值,如下图;

再次修改 test.txt并且commit,.git/refs/heads/test内容为:

  1. 更新HEAD指针:

HEAD 指向的分支(当前为 test 分支)指向新的提交对象,表示当前分支已经包含了这个新提交;抽象成下面的模型:

git pull

****'git pull' 命令用于从远程仓库中获取最新的提交,并将它们合并到当前所在的本地分支。它实际上是 'git fetch''git merge' 两个命令的组合。

  1. 执行 git fetch:

git pull 首先会执行 git fetch 命令,从远程仓库中拉取最新的提交、分支信息和其他更新;

git fetch 不会修改本地工作区的内容,而是将远程仓库的内容下载到本地仓库,更新远程跟踪分支(如 origin/master)指向最新提交;

  1. 执行 git merge:

git pull 接着会执行 git merge 命令,将远程仓库拉取的内容与当前分支进行合并;

如果没有冲突,git 会尝试自动合并更新到当前分支中。如果有冲突,则需要手动解决冲突后再提交;

git fetch

'git pull' 命令用于从远程仓库中获取最新的提交,并且将代码拉取到本地仓库。 'git pull'对 .git目录的影响如下:

  1. 更新 .git/refs/remotes/origin 目录,在该目录下创建一个远程分支名的目录,更新或创建远程分支的引用;
  2. 更新.git/FETCH_HEAD 文件中记录最新的 fetch 操作信息;

git merge

'git merge' 命令是指要将哪个分支的内容合并到当前分支。它对 .git目录的影响如下:修改 .git/refs/heads 目录

git push

git push 命令用于将本地分支的提交推送(同步)到远程仓库中。它对 .git目录的影响如下:

  1. 更新 .git/refs/remotes 或 .git/refs/heads

git push 会在 .git/refs/remotes 或 .git/refs/heads 目录下更新或创建远程引用;

  1. 更新 .git/config 文件

执行 git push 也会修改 .git/config 文件中关于远程仓库的配置信息,比如远程仓库的URL、远程分支的映射关系等;

git reset

``'git reset'**** 命令用于移动分支的指向以及更改暂存区和工作目录的状态。对 .git目录的影响如下:

  1. 更新 .git/refs/heads 目录下的分支引用文件;

  2. 更新暂存区;

  3. 修改本地工作区;

使用 git reset --hard ,回退暂存区,修改工作目录的状态,删除未提交的更改;如下图,执行 git reset --hard 命令,会把暂存区的修改回退,使得工作区是干净的。

  1. 总结

本文重点分析了 git 的3种状态,4个核心区域,.git目录,分支以及常见指令。因为 Git的知识点太多,无法一一讲解,只要抓住 Git的核心,也就是下面这张图,那么我们再去分析 Git其它的问题就会轻松很多。因此,回到文章标题:Git如何工作?其实这个问题的答案比较宽泛自由,只要围绕下图核心灵活作答即可。

参考资料

Git官方文档英文版:git-scm.com/book/en/v2G...

官方文档英文版:git-scm.com/book/en/v2

原创好文:
相关推荐
xoxo-Rachel5 分钟前
(超级详细!!!)解决“com.mysql.jdbc.Driver is deprecated”警告:详解与优化
java·数据库·mysql
乌啼霜满天2497 分钟前
JDBC编程---Java
java·开发语言·sql
Smile丶凉轩8 分钟前
微服务即时通讯系统的实现(服务端)----(1)
c++·git·微服务·github
色空大师20 分钟前
23种设计模式
java·开发语言·设计模式
闲人一枚(学习中)21 分钟前
设计模式-创建型-建造者模式
java·设计模式·建造者模式
2202_7544215438 分钟前
生成MPSOC以及ZYNQ的启动文件BOOT.BIN的小软件
java·linux·开发语言
蓝染-惣右介41 分钟前
【MyBatisPlus·最新教程】包含多个改造案例,常用注解、条件构造器、代码生成、静态工具、类型处理器、分页插件、自动填充字段
java·数据库·tomcat·mybatis
小林想被监督学习42 分钟前
idea怎么打开两个窗口,运行两个项目
java·ide·intellij-idea
HoneyMoose43 分钟前
IDEA 2024.3 版本更新主要功能介绍
java·ide·intellij-idea