执行git命令的时候发生了什么

阅读本文的前提:

  • 读者知道git工作区、暂存区的概念
  • 读者会用git基本命令

本文概要:

  • 受git管理的目录,会有.git子目录
  • .git子目录有着完整的历史数据,其下的objects目录,以二进制的形式,保存了文档、目录在所有提交节点的完整信息
  • 分支名称是历史提交记录的引用

主要参考文档:

目录

git低级命令

git init

git add 

文件内容存入objects目录

index文件

git commit

index文件没有变化

objects目录多出来的文件

引用

logs目录

回顾

git diff

git回退

git checkout

git reset

区别


git低级命令

    常用的git add、git commit等命令,称作“高级命令”,实际上做了不止一个操作。git内部还有做基础操作的“低级命令”,平时并不会用到。

    可以认为,高级命令是串行调用若干个低级命令。

git init

    在一个目录下执行git init,或者是git clone一个远程仓库到本地,目录下会出现.git子目录,表示该目录受git管理,目录中除.git子目录外,都是“工作区”。

    在一个空目录下执行git init,以下是.git子目录的结构示例:

$ tree .git
.git
├── HEAD
├── config
├── hooks
│   └── commit-msg
├── objects
│   ├── info
│   └── pack
└── refs
    ├── heads
    └── tags

    在以下各常用命令的讲解过程中,会逐个说明其中各个文件的作用。

git add 

命令的作用:将工作区中的内容移动到暂存区。

    在刚才的目录下新增若干文件(内容都是只有一行的文本),结构如下:

$ tree
.
├── lib
│   ├── liba.txt
│   └── libc.txt
└── readme.txt

    执行git add .命令之后,.git目录变成如下图所示:

$ tree .git
.git
├── HEAD
├── config
├── hooks
│   └── commit-msg
├── index
├── objects
│   ├── 94
│   │   └── 5592fff7cc0b96426bf58f5a1537e9cb1fef68
│   ├── c1
│   │   └── 024330cfb9c6ba5285423aa4df8cf34fc41cf2
│   ├── e6
│   │   └── 3ae029b9c966ec4c0a00b80f26c25ed99da120
│   ├── info
│   └── pack
└── refs
    ├── heads
    └── tags

文件内容存入objects目录

    工作区中的每个文件,都已二进制的形式存入了objects目录中:objects目录下多出来的3个文件,正好对应工作区中的三个文件。

    objects文件名:“构造的文件头部”+文件内容,计算出的SHA-1值,取前两个字母作为子目录的名称,剩下的作为文件名

    objects文件内容:“构造的文件头部”+文件内容,zlib压缩得到

参考git官方文档:Git - Git 对象 (“对象存储”一节)

    SHA-1值冲突的概率不高,如果真的出现冲突,git diff可以发现(其内容会很奇怪,此时可以加个注释、加个空行等方法解决)。

相关的低级命令:

  • git hash-object <filename>,计算其hash值,在终端显示。
    • 这里的计算结果是945592fff7cc0b96426bf58f5a1537e9cb1fef68,目录名为94,文件名为剩下的5592fff7cc0b96426bf58f5a1537e9cb1fef68
$ git hash-object readme.txt
945592fff7cc0b96426bf58f5a1537e9cb1fef68
  • git hash-object -w <filename>,计算其hash值,在终端显示;计算objects文件内容,存入objects目录中

  • git cat-file -t <hash值>,得到objects文件对应的原类型(此时只有blob,表示这个objects文件对应的是一个文件,后面还会看到别的类型)
$ git cat-file -t 945592fff7cc0b96426bf58f5a1537e9cb1fef68
blob
  • git cat-file -p <hash值>,得到objects文件对应的内容(注意,只有文件内容,没有文件名)
    • 如果hash值不冲突的,短一些也可以
$ git cat-file -p 945592fff7cc0b96426bf58f5a1537e9cb1fef68
content abcd
# 注:这就是readme.txt文件的内容

$ git cat-file -p 945592
content abcd

index文件

    index文件就是git中的“暂存区”,这也是一个二进制文件,保存了git add .之后,暂存区中所有文件的列表及对应的hash值。

相关的低级命令:

  • git update-index --add --cacheinfo <rwx> <hash> <filename>,将文件加入暂存区
$ git update-index --add --cacheinfo 100644 945592fff7cc0b96426bf58f5a1537e9cb1fef68 readme.txt

可参考:Git - git-update-index Documentation  update-index命令的用法(英文)

git update-index (Plumbing Commands) - Git 中文开发手册 - 开发者手册 - 云+社区 - 腾讯云

  • git ls-files -s 查看index文件中,工作区中文件的状态,其中四列分别是:rwx、hash、commit time、带路径的文件名
$ git ls-files -s
100644 e63ae029b9c966ec4c0a00b80f26c25ed99da120 0	lib/liba.txt
100644 c1024330cfb9c6ba5285423aa4df8cf34fc41cf2 0	lib/libc.txt
100644 945592fff7cc0b96426bf58f5a1537e9cb1fef68 0	readme.txt

git commit

命令的作用:创建一次提交。

    执行git commit -m 'first cccommit' 命令之后,.git目录变化如下:

$ tree .git
.git
├── COMMIT_EDITMSG
├── HEAD
├── config
├── hooks
│   └── commit-msg
├── index
├── logs
│   ├── HEAD
│   └── refs
│       └── heads
│           └── master
├── objects
│   ├── 0a
│   │   └── 844532018cfec018d05a717e0e6f530a72fe8e
│   ├── 6d
│   │   └── f54361d4e32fab7aad3b1f34ad41e0b5f25d6c
│   ├── 94
│   │   └── 5592fff7cc0b96426bf58f5a1537e9cb1fef68
│   ├── c1
│   │   └── 024330cfb9c6ba5285423aa4df8cf34fc41cf2
│   ├── e6
│   │   └── 3ae029b9c966ec4c0a00b80f26c25ed99da120
│   ├── ef
│   │   └── 216e815c7efacda84cc7d844f529bf53694e5e
│   ├── info
│   └── pack
└── refs
    ├── heads
    │   └── master
    └── tags

index文件没有变化

    再次执行git ls-files -s 命令,查看index文件内容,和commit之前完全一样:

$ git ls-files -s
100644 e63ae029b9c966ec4c0a00b80f26c25ed99da120 0	lib/liba.txt
100644 c1024330cfb9c6ba5285423aa4df8cf34fc41cf2 0	lib/libc.txt
100644 945592fff7cc0b96426bf58f5a1537e9cb1fef68 0	readme.txt

objects目录多出来的文件

    objects目录刚才只有3个文件,现在多出3个文件。执行git cat-file -p 命令,查看这三个objects的类型:

$ git cat-file -t 6df54361d4e32fab7aad3b1f34ad41e0b5f25d6c
commit

$ git cat-file -t 0a844532018cfec018d05a717e0e6f530a72fe8e
tree

$ git cat-file -t ef216e815c7efacda84cc7d844f529bf53694e5e
tree

    相比之前的blob(文件object),多出了tree(目录object)、commit(提交object)。查看这几个文件的内容: 

$ git cat-file -p 6df54361d4e32fab7aad3b1f34ad41e0b5f25d6c
tree 0a844532018cfec018d05a717e0e6f530a72fe8e
author xxxx <xxx@xxx.com> 1634464533 +0800
committer xxxx <xxx@xxx.com> 1634464533 +0800

first cccommit

$ git cat-file -p 0a844532018cfec018d05a717e0e6f530a72fe8e
040000 tree ef216e815c7efacda84cc7d844f529bf53694e5e	lib
100644 blob 945592fff7cc0b96426bf58f5a1537e9cb1fef68	readme.txt

$ git cat-file -p ef216e815c7efacda84cc7d844f529bf53694e5e
100644 blob e63ae029b9c966ec4c0a00b80f26c25ed99da120	liba.txt
100644 blob c1024330cfb9c6ba5285423aa4df8cf34fc41cf2	libc.txt

    后2个object都是tree,表示一个目录,其中保存了下级各个文件/目录的rwx、类型、hash、文件/目录名称(注意,这里是树状结构,和index中保存带路径的文件名称不一样)。

    第一个object是一个提交object,其中第一行保存了本次提交的根目录object,后面是本次提交的信息。

    相关的低级命令:

  • git write-tree,根据index建立各个tree object
  • git read-tree <hash值>,将hash值代表的tree object还原,并写入index中;工作区无任何改动

参考:git write-tree (Plumbing Commands) - Git 中文开发手册 - 开发者手册 - 云+社区 - 腾讯云

git read-tree (Plumbing Commands) - Git 中文开发手册 - 开发者手册 - 云+社区 - 腾讯云

  • (补充生成commit object的命令)

引用

    查看refs目录、HEAD文件的内容:

$ more .git/refs/heads/master
6df54361d4e32fab7aad3b1f34ad41e0b5f25d6c

$ more .git/HEAD
ref: refs/heads/master

    这表明:master分支目前指向 6df54361d4e32fab7aad3b1f34ad41e0b5f25d6c 这个commit object,而HEAD(当前的活动分支)是master。

    可以将分支的“名字”,看作是某个commit object的引用。发生git commit或者git reset的时候,分支“名字”指向的commit object发生变化。

    相关的低级命令:

  • git update-ref <要更新的引用路径> <commit object hash>,更新分支引用指向的commit object hash
$ git update-ref refs/heads/master 6df54361d4e32fab7aad3b1f34ad41e0b5f25d6c

    从当前分支创建分支,实际上也用到这个命令,创建一个新的引用,指向当前的commit object 。

  • git symbolic-ref HEAD <分支名>,更新HEAD分支的指向
$ git symbolic-ref HEAD refs/heads/master

参考:Git - Git 引用

logs目录

    .git目录下,新增了logs目录:

├── logs
│   ├── HEAD
│   └── refs
│       └── heads
│           └── master

    查看其中logs/refs/heads/master文件的内容:

$ more logs/refs/heads/master
0000000000000000000000000000000000000000 6df54361d4e32fab7aad3b1f34ad41e0b5f25d6c xxx <xxx@xxx.com> 1634464533 +0800   commit (initial): first cccommit

    目前认为:其中保存了master分支的提交历史记录。

再次git commit

    增加readme.md文件,再次git add、git commit

$ echo xxx >> readme.md
$ git add .
$ git commit -m 'second commit'
[master fe39e76] second commit
 1 file changed, 1 insertion(+)
 create mode 100644 readme.md

objects中commit object的变化

    找到最新的commit object,查看其中的内容:

$ git cat-file -p fe39e76df285901e38be1bca675f99cc1f359ba0
tree cb2c335799c256f9be3000d7ff9d7d09e30d29be
parent 6df54361d4e32fab7aad3b1f34ad41e0b5f25d6c
author xxx <xxx@xxx.com> 1634999223 +0800
committer xxx <xxx@xxx.com> 1634999223 +0800

second commit

Change-Id: I450abec88e8f6ad1aa2f5bd510e56501189217c9

    可见,其中除了指向目录tree object的指针之外,还有一个parent,指向上一个提交的commit object。

log目录的变化

    查看 .git/logs/refs/heads/master 文件的内容:

$ more .git/logs/refs/heads/master
0000000000000000000000000000000000000000 6df54361d4e32fab7aad3b1f34ad41e0b5f25d6c xxx <xxx@xxxx.com> 1634464533 +0800   commit (initial): first cccommit
6df54361d4e32fab7aad3b1f34ad41e0b5f25d6c fe39e76df285901e38be1bca675f99cc1f359ba0 xxx <xxx@xxxx.com> 1634999223 +0800   commit: second commit

    记录了提交的历史。但如果从现在的master分支,创建一个名为test的分支呢?

$ git checkout -b test
$ more .git/logs/refs/heads/test
0000000000000000000000000000000000000000 fe39e76df285901e38be1bca675f99cc1f359ba0 xxx <xxxx@xxx.com> 1634999624 +0800   branch: Created from HEAD

    test分支的log文件只有一行,并没有完整的信息。

    这说明git log获取提交信息的时候,分支的上一个版本是从commit hash object中获取到的,而非从.git/logs中获取到。

    至于.git/logs/HEAD 文件中的内容就更杂了,记录了HEAD指针的变化(commit、checkout、reset……),不等于当前分支对应的 .git/logs/xxx文件。

回顾

.git
├── COMMIT_EDITMSG
├── HEAD HEAD引用,当前的活动分支
├── config git仓库的远程配置、分支对应关系
├── hooks
├── index 暂存区,保存暂存区内所有文件的object的hash
├── logs 保存分支的提交历史,只能追溯到分支创建时
│   ├── HEAD
│   └── refs
│       └── heads 本地分支
│           └── master
│       └── remotes 远程分支
│           └── master
├── objects 所有提交的二进制的文件、目录、提交信息
│   ├── 0a
│   │   └── 844532018cfec018d05a717e0e6f530a72fe8e
│   ├── ......略
│   ├── info
│   └── pack 二进制文件太多时,使用此目录压缩保存
└── refs
    ├── heads 本地所有分支的最近一次commit的hash
    │   └── master 
    ├── remotes 远程所有分支的最近一次commit的hash
    │   └── master
    └── tags 标签对应的commit的hash

    在git pull的时候,要更新所有远程分支的信息:

  • 所有远程分支的最近一次提交的hash值,到refs/remotes
  • 所有标签对应的hash值,到refs/tags
  • 所有远程分支的提交历史,logs/refs/remotes
  • 所有远程分支的所有提交的文件对应的二进制文件,到objects
  • 如果当前的分支也在git pull中拉取到更新,更新index

    所以修改提交频率较高的库,分支多,objects文件也多,git pull一次要花费很多时间。

    git pull origin <分支名>可解,只要拉取一个分支的信息。

git diff

    用法:git diff <提交> <filename>,使用指定的提交对比指定文件

    <filename>如果不传,则默认为.(当前目录)。"提交"可以是:

  • 某次提交的hash
  • 分支名,等效于分支最新一次commit对应的hash
  • HEAD,等效于HEAD中的分支名
  • 如果不填,默认值就是index(暂存区)

    取“比较基准”的路径:

  • 确定比较基准的commit hash,或者是暂存区
  • 如果是commit hash,使用hash值在objects中找到对应的提交,根据要对比的文件的路径,一步步解析到对应文件的hash,找到对应的二进制文件
  • 如果是暂存区,解析index文件,找到要对比的文件的hash,在objects中找到对应的二进制文件
  • 反序列化找到的objects blob文件,并与工作区中的文件对比

git回退

git checkout

    用法1:git checkout <提交 不可为空>,此时更换工作分支

  • HEAD引用变化,但如果<提交> 是一个commit hash,则进入“分离头指针状态”,此时不在任何一个分支上,可以查看、修改,但是无法提交
  • 暂存区变化为本次<提交>对应的状态
  • 工作区变化为本次<提交>对应的状态
  • .git/logs、git/refs都不变,即所有分支的提交状态都不发生变化

    用法2:git checkout <提交> <filename>,此时将文件更新为某次提交的状态,但不更改提交历史

  • 还在当前分支上,HEAD引用不变,.git/logs、git/refs都不变
  • <提交>可以没有,如果没有就是暂存区
  • 工作区的对应文件,更新到指定提交的状态
  • 如果<提交>不为空,暂存区中,此文件也会更新到指定提交的状态    

    例如,某个文件被git add <file> 加入暂存区,现在想直接回退到最开始的版本,可以git checkout HEAD <file>,工作区、暂存区中的此文件都会被还原。

git reset

    用法1:git reset [--soft / --hard] <提交>,将当前的分支回退到某次提交的状态,且回退提交历史

    是git commit的逆操作。

  • HEAD引用不变,还在此分支上
  • 但是提交历史回退,因此.git/logs、git/refs都发生变化
  • 暂存区、工作区的变化,根据参数而定:
工作区暂存区效果
--hard倒退到指定提交倒退到指定提交相当于改动彻底没了,彻底倒退回指定提交的状态
保持原状倒退到指定提交

相当于修改了、但没有git add的时候

--soft保持原状保持原状只是提交没了,相当于git add之后、还没git commit的时候

 

    用法2:git reset <filename>,将暂存区中的指定文件倒退回HEAD,但保留文件改动

    相当于上一改动中不带--hard,也不带--soft的用法,是git add <filename> 的逆操作。

    如果想当暂存区中的文件彻底退回到一开始的样子,改动也不要了,应当使用git checkout HEAD <filename>。

区别

    git checkout不更改提交历史,只是切换分支、更新文件、更新暂存区,但是git checkout如果是倒退了未提交的改动,就找不回来了。

    git reset会修改提交历史,根据参数选择是否保留改动。但是git reset --hard了没有git push的分支,就找不回来了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值