git原理和命令详解:当我们操作git时发生了什么

1,133 阅读25分钟

git是一个版本管理系统,通过版本控制可以记录一个或若干文件的内容变化,以便将来查阅某个版本的文件。
在我们开发过程中,git用于代码版本控制,本文将深入讨论git原理和我们所使用的各种命令所代表的具体含义。
主要内容分为以下几个部分

  1. git原理
  2. git命令
    1. 项目相关
    2. 快照相关
    3. 分支相关
    4. 查看和比较
    5. 补丁
    6. 其他高级命令
    7. 底层命令
  3. git hooks
  4. 总结

git原理

git要想成为一个及格的版本控制工具,要实现以下功能

  1. 单文件的保存
  2. 多文件的关联
  3. 多版本的管理
  4. 文件的存储及优化

要想使用git.我们要在项目文件中执行git init生成一个.git目录对git进行初始化,可以按顺序看,也可以先去本节第4部分看一下具体什么含义,然后再回头读。

下面我们从底层命令角度来看git是怎么实现以上功能的(其中涉及到较多底层命令,可以先翻到后面相关章节做一下了解,如果实际操作过程中获得hash值和实例中不一样,则可能是默认编码的问题)。

1. 单文件的保存

一个项目是由单个或多个文件组成的,我们首先看单文件的保存,创建一个文件test.txt,并使用git hash-object写入git,返回对应文件的hash值。

echo 'version 1' > test.txt
git hash-object -w test.txt
//83baae61804e65cc73a7201a7252750c76066a30

我们可用git cat-file使用hash值将文件取出(-p表示Pretty-print contents,打印出内容,-t表示type)

git cat-file -p 83baae61804e65cc73a7201a7252750c76066a30
//version 1
git cat-file -t 83baae61804e65cc73a7201a7252750c76066a30
//blob

由此我们完成了单个文件的存取,git实现了一个key-value数据库的功能,单个文件保存的类型为blob,这是git种的对象之一。

2. 多文件的关联

blob对象只保存了文件的内容,丢失了关键信息:文件名,为了解决这个问题,且将多个文件关联成一个整体,我们引入了tree对象,一个tree对象可以看成一个目录,一个目录种包含多个文件(blob对象)和其他目录(其他tree对象) tree对象是根据某一时刻的暂存区域内容来生成的,因此我们先使用git update-index将文件放入暂存区。

git update-index --add --cacheinfo 100644 83baae61804e65cc73a7201a7252750c76066a30 test.txt

将暂存区使用git write-tree 写入tree对象

git write-tree
//674d4d31b97233152f3be1825cc9e765fa2b2859
git cat-file -p 674d4
//100644 blob 83baae61804e65cc73a7201a7252750c76066a30    test.txt
git cat-file -t 674d4
//tree

当然也可以将多个文件加入暂存区后或者将另一个tree对象写入tree对象,现在将多个blob文件写入tree对象。

echo 'another file' > another.txt
git hash-object -w another.txt 
//17d5d9edf31a80878ad4911017cbd6d1d03322b8
git update-index --add --cacheinfo 100644 17d5d9edf31a80878ad4911017cbd6d1d03322b8 another.txt

//把刚才的blob对象也加进去
git update-index --add --cacheinfo 100644 83baae61804e65cc73a7201a7252750c76066a30 test.txt

git write-tree
//18ccf7f4dc54058357925cdf2014a9210b0a21a8

3. 多版本的管理

现在我们可以创建一个个tree对象,但是离版本的概念还差一步,那就是将tree对象按照某种结构存储以方便对版本管理的进一步处理。在git种这个数据结构就是就是个有向图,每个节点是我们即将讨论的commit对象,有一个指针指向父commit,其中保存着一个tree对象和一些提交信息。
使用git commit-tree将指定的tree对象写入commit对象,通过-m添加备注信息,通过-p指定父提交对象

//创建第一个commit对象
git commit-tree 674d4d31b97233 -m '第一个提交'
//e9cd7b7e8b607ce2881d2512e420c2e301310809

//创建第二个commit对象,并以第一个为父提交对象
git commit-tree 18ccf7f4dc5405 -m '第二个提交' -p e9cd7b7e8b607ce2881d2512e420c2e301310809
//f0c44a930d40d5ee4be2698c786f2f5a83ef4b80


//查看commit对象
git cat-file -p f0c44a930

每个commit相当于一个版本,为了管理各个版本。我们给其取一个可读性强的名字

 git update-ref refs/heads/test f0c44a930d40d5ee4be2698c786f2f5a83ef4b80

即把.git/refs/heads/test内容改为对应commit对象hash值,这便是git分支的本质,即一个指向commit对象的指针,该commit对象和它的所有上级提交构成了这个分支的内容,新创建一个指向某个commit对象的指针便生成了另一个分支。

我们把指向某个commit对象的指针称为引用(references,refs),每个引用就是指.git文件夹下的一个文件,其内容是对应hash值或者另一个引用,除了分支引用,还有

  • HEAD 指向当前所在的分支,默认master分支(初始化时该分支上没有任何commit对象,被称为unborn branch,可通过git fsck查看),随着操作分支的切换而变化
  • tag 在git中有四种主要对象,包括前面提的blob对象、tree对象和commit对象,还有这里的tag对象,tag对象包含一个标签创建者信息、一个日期、一段注释信息,以及一个指向一个commit对象的固定指针,是对单次commit对象的引用。
  • remote 远程引用是本地分支对应的远程分支,在push或fetch时会对其进行更新,访问时可以用以下三种方式,以master为例,它们是等效的
$ git log origin/master
$ git log remotes/origin/master
$ git log refs/remotes/origin/master

4. 文件的存储及优化

这部分讨论我们以上操作的各种对象,乃至日常管理的代码是怎么存储的。 前面我们对git初始化时生成了一个.git目录,绝大部分细节都保存其中,看一下其中的内容

├── hooks/ 就是当程序运行到某些特定阶段要执行的某些命令,比如提交之前运行pre-commit。该目录下包含各种hook的模板文件。
├── info/
   ├── exclude 用于放置那些不希望被记录在.gitignore文件的忽略模式
├── logs/ 对各个引用的操作记录
├── objects/ 各版本文件的具体保存位置
├── refs/
   ├── heads/ 保存了本地分支
   ├── tags/ 保存了标签对象
   ├── stash 保存了stash entries
   ├── remotes/ 远程引用
      ├── origin/ 版本库默认名为origin,同时也可以有其他远程版本库,保存了该仓库的远程分支 
├── config 对当前项目的git相关配置,包含远程仓库的url、不同本地分支的upstream(即git fetch或 git push时本地分支对应的远程分支,后续会多次提到)
├── description 供gitweb使用的介绍
├── HEAD head引用,保存了当前正在操作的分支
├── index 暂存区内容
├── COMMIT_EDITMSG 保存着上次的提交信息,在git hook部分中使用

当我们在本地存取一个文件时,都是在操作.git/objects这个目录的内容。

$ find .git/objects -type f
.git/objects/17/d5d9edf31a80878ad4911017cbd6d1d03322b8
.git/objects/18/ccf7f4dc54058357925cdf2014a9210b0a21a8
.git/objects/4b/825dc642cb6eb9a060e54bf8d69288fbee4904
.git/objects/59/4dc0e39bc4468ee19c67e65d37b97eb963b68b
.git/objects/67/4d4d31b97233152f3be1825cc9e765fa2b2859
.git/objects/e9/cd7b7e8b607ce2881d2512e420c2e301310809
.git/objects/f0/c44a930d40d5ee4be2698c786f2f5a83ef4b80

在本地的git仓库里保存着所有历史版本的文件,每次clone相当于对远程仓库的一次全量备份。
按照git的存储策略,会对每个版本的文件完整保存,并按照一定条件对文件进行打包为二进制文件packfile,另提供一个索引文件快速定位每个对象,打包过程可以通过git gc手动执行

$ git gc
Enumerating objects: 6, done.
Counting objects: 100% (6/6), done.
Delta compression using up to 6 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (6/6), done.
Total 6 (delta 0), reused 0 (delta 0)
Computing commit graph generation numbers: 100% (2/2), done.

打包结果

$ find .git/objects -type f
.git/objects/4b/825dc642cb6eb9a060e54bf8d69288fbee4904
.git/objects/info/commit-graph
.git/objects/info/packs
.git/objects/pack/pack-1b0fbf5e906f40ad4387962cf3efe5ede74aa2a0.idx
.git/objects/pack/pack-1b0fbf5e906f40ad4387962cf3efe5ede74aa2a0.pack

git命令

git命令是git提供与其交互的api,在讲解具体命令之前,再对git宇宙中的概念以及和现实中对应的具体文件做一下对应。
我这里将其称为五大空间、三个状态和一条主线(为了系统化理解git,此类概念非官方定义,只为助记,如有必要请注意区分),基本上所有的操作都是在各空间各状态之间流转,而将这些串联起来的就是一条主线,这是我提炼的git精髓。如图

五大空间分别为

  1. working directory,当前工作目录,又称working tree,即是当前我们正在修改的项目文件夹。
  2. index,暂存区,也叫staging area,是从工作目录到本地仓库的中间状态,方便多次修改,位于.git/index文件
  3. local repository,本地仓库,本地仓库中的文件保存在.git/objects/目录下,包含本地的所有引用指向的对象和所有历史版本。我们这里主要关注.git/refs目录下的两种引用
    • heads,保存了本地分支的指针,比如master分支,当我们执行git commit时会把指针指向新生成的根据index生成的commit对象,执行其他影响本地分支内容的操作也类似。
    • remotes,是远程仓库的本地副本,是已知的对应远程分支的最新状态,执行git fetch时从远程仓库将文件同步到这里,以便进行进一步merge等处理,执行git push时从本地分支同步到这里。和本地分支的区别是不能编辑,即不能将HEAD指向它,可以作为远程仓库分支同步到本地分支的一个中转站。
  4. remote repository,远程仓库,用于多人协作和线上备份,位于我们线上的仓库,比如github。
  5. stack,git stash操作时代码临时存放的地方,当有多个stask entries时先进后出,位于.git/refs/stash,内容是一个commit对象的hash值。

三种状态分别为

  • tracked 已跟踪的文件,即被git管理的文件,根据所在的空间又可细分为:
    • commited,位于本地仓库和远程仓库,已经使用git commit提交到仓库的代码
    • staged,index中的代码,又称为cached
    • unmodified,位于工作目录,和index中版本一致
    • modified,位于工作目录,和index中同时存在的文件但内容有所修改
    • stashing,位于stack
  • untracked,位于工作目录,但是index不存在,刚添加未跟踪的文件
  • discarded,不在git世界五大空间,被丢弃的代码,丢弃动作不可逆

一条主线是
一个项目一般只有一个主分支master,其他分支都是基于master某个commit对象的分支。为了更好的理解后面命令中的操作细节,在这里我们回顾以下前面提到的一些概念:
当我们执行git init时会默认在一个空分支master上,该分支上没有任何commit,当我们执行其他操作时并最终git commit时,这边成了真正意义上的分支。

当我们基于一个分支想访问其某个父提交时有两种常用的标记,分别为~n和^n,其中前者比如head~n表示head的第n代祖先提交对象,n默认为1,后者head^n表示当有多个父提交时,选择第n个父提交,n默认为1,比如

G   H   I   J
 \ /     \ /
  D   E   F
   \  |  / \
    \ | /   |
     \|/    |
      B     C
       \   /
        \ /
         A

A =      = A^0
B = A^   = A^1     = A~1
C = A^2
D = A^^  = A^1^1   = A~2
E = B^2  = A^^2
F = B^3  = A^^3
G = A^^^ = A^1^1^1 = A~3
H = D^2  = B^^2    = A^^^2  = A~2^2
I = F^   = B^3^    = A^^3^
J = F^2  = B^3^2   = A^^3^2

版本管理的一个版本指的是一个commit对象,指向commit对象的引用有很多,包括

  • 这里说的分支,一系列commit组成的链叫做分支,分支名总是指向最新的一个commit,接下来的操作会基于该最新的commit对象向后延伸,每次创建新的commit时,分支便指向新的commit对象。
  • tag 指向固定的commit对象
  • head 指向正在工作的分支,可以通过切换分支来实现切换head

现在我们已经对git中各种概念有了直观的理解,在介绍后面的命令时,会介绍对应命令是怎么影到以上三个主要概念的。

1. 项目相关

这里介绍的是项目级的命令,是其他命令的基础

init

初始化一个git仓库

config

对git进行相关配置,配置文件一般分为项目级和全局级,后者添加--global参数。可通过git config -l查看当前配置

clone

根据提供的url克隆远程仓库,是一系列命令的封装:

  1. 新建目录
  2. 进入目录
  3. git init初始化一个新的仓库
  4. git remote add origin [repo-url] 用指定将远程仓库添加为origin,url为指定url
  5. git fetch 将远程仓库同步到本地仓库
  6. git checkout 检出当前分支最后一次commit

2. 快照相关

这里指的是不涉及跨分支的基本操作。

add

使用working tree的内容更新index

commit

根据index创建一个新的commit,head指向新的commit。

  1. 使用-C 或--reuse-message= 复用一个commit的信息
  2. 使用 --amend --no-edit 创建新的commit来代替之前最新的commit,并复用其commit信息

rm

将指定文件在working tree和index中删除,或者只在index中删除,如果只在working tree中删除,请用/bin/rm,--ignore-unmatch表示没有匹配到文件也不报错

git rm  [-r]   [--] [<pathspec> //index和working tree中删除,在working tree中文件消失,在index中是待提交
git rm --cached [-r] [--] [<pathspec>  //index中删除,但是保留在working tree,文件在working tree中状态为未追踪,在index中是待提交

mv

add和rm的快捷方式,用于文件改名

restore

根据指定来源恢复一些指定路径,如果一个路径被跟踪且指定来源里没有,就会直接删除,比如

git restore [--] <pathspec>
//等效于下面命令,表示从index恢复working tree指定路径
git restore --worktree [--] <pathspec>

git restore  [--source=<tree>] --staged [--] <pathspec> //从指定commit(默认head)恢复index的指定路径

git restore  --source=<tree> --staged --worktree   [--] <pathspec> //从指定commit同时恢复index和working tree

clean

将工作区untracked且不在.gitignore中的文件删除,需要添加-f强制删除或者-i进入交互页面

stash

将工作区和index的修改清理干净,并将修改的内容保存入栈

git stash //将工作区和暂存区的修改压栈,同时清空工作区的修改
git stash pop [–index] [stash_id] //默认把最新的栈更新到工作区,添加--index可以按照保存时两个空间的状态还原,使用stash_id可以指定要恢复的stash项,执行结束后对应stash entry(stash项)将清除
git stash apply [–index] [stash_id] //和pop一样,但不清除对应stash entry
git stash drop [stash_id] //删除一个stash entry
git stash clear //清除所有stash entries

reset

修改head指针的指向,其中涉及到工作区、暂存区以及当前指向的commit以及最新指向的commit之间的修改。语法介绍两种:

第一种:

git reset [<tree-ish>] [--] <pathspec>…

其中tree-ish可以认为是commit对象(默认head),具体点击,默认head,其中路径pathspec必选,表示不移动head且不修改工作区,但是要把指定commit放进index,比如使用默认的head,则index和head相同,则意味着将index的部分退回到工作区

第二种:

git reset [--soft | --mixed  | --hard | --merge | --keep]  [<commit>]

分别在参数中指定模式和commitID,切换head到指定commit,不同模式表示的含义为

  • --soft 把修改head带来的变化添加到index,原有的worktree不变,index增加
  • --mixed 默认模式,将index中的修改和切换head带来的变化都添加到working tree,清空index
  • --hard 清空index和working tree,丢弃因切换head带来的变化
  • --merge 相当于保留当前working tree未add部分得hard reset,如果丢弃的部分和未add的文件有重叠,则失败,用于重置merge
  • --keep 将未提交的修改放在working tree,丢弃切换head带来的变化,如果切换head变化的文件在本地有修改,则失败。

3. 分支相关

这里指的涉及到不同分支的操作

branch

对分支进行操作

git branch --list//或者不带参数,都是表示列出本地分支分支,-r显示远程分支,-a显示所有分支
git branch --set-upstream-to=<remote>/<branch> <branch>  //将本地分支branch关联到远程对应分支,具体信息在.git/config文件里。

switch

切换分支,如果切换分支时index或working tree与新的分支有冲突,则需要先将index或working涉及到的代码提交或者添加--discard-changes或-f丢弃,或者添加--merge进行合并
添加-c可以自动创建一个新分支。

checkout

同时承担了git switch或git restore的工作,后两个命令是对前者的替换,因为前者承担了太多的工作,因此不深入讨论。

tag

对tag引用的增删改查

fetch

从另一个仓库下载引用(分支或标签等)到本地仓库远程分支的副本(.git/refs/remotes目录下),默认从本地分支的updtream下载。添加--prune或-p可以在下载之前去除已经不在远程分支上的分支

push

将本地的commit,上传到远程仓库,使用-f或--force可能会修改历史

merge

两个或更多分支合并到一个分支(这里只讨论两个分支合并),有两种语法,前一种是指导合并无法一次性完成时的行为,--continue或--abort其中一项,表示合并继续和合并取消,第二种语法是指示怎么合并,如下面介绍的修改合并过程。

常规的合并场景有以下几种:

  1. 当两个分支,其中一个分支的commit集合是另一个commit集合的子集,且本地没有造成冲突的修改,合并时会直接完成,不产生新的commit,这被称为快速合并(fast-forward merge),此时不能对合并过程进行干预。
  2. 两个一般且没有冲突的分支,会直接合并,并产生一个新的commit提交变化的代码
  3. 如果两个分支合并有冲突,又分为
    1. 如果因为本地修改造成的冲突,则合并失败,需要对本地修改进一步处理
    2. 如果因为两个分支上的commit造成的冲突,则其中没冲突的部分会保存在index,有冲突的部分保存在working tree等待解决冲突,如果不想解决可以git merge --abort 取消合并,否则解决完冲突,git add ,然后可以直接git commit,也可以使用这里的命令 git merge continue,此时进入vi编辑器,对提交信息进行修改完成合并.

如果采用添加其他参数可以修改合并过程:

  1. --no-commit,对于非快速合并且没冲突的合并有效,会在产生commit前暂停,此时两者不同的部分保存在index,可以git merge --abort取消,可以git commit或者 git merge continue 继续。
  2. --ff 默认,如果能快速合并就快速合并,不然就创一个commit进行合并
  3. --ff-only 当有冲突时,自动取消
  4. --no-ff 每次都创建一个commit
  5. --squash 如果没有冲突,把两个分支的不同之处都放在当前分支的index,不显示合并记录;否则没冲突的部分在index,有冲突的部分在working tree等待解决冲突,没有合并记录,

pull

默认情况下是git fetch+git merge FETCH_HEAD 的缩写,带--rebase 则执行git rebase代替git merge

4. 查看和比较

这里指的是对git不做修改仅用来查看的操作

diff

用来比较working tree和index、index和某次commit、不同tree对象、不同blob对象乃至不同文件。

git diff  [--] [<path>…] //比较working tree和index的区别
git diff  --cached [<commit>] [--] [<path>…] //比较index和指定commit的区别,commit默认为head
git diff <commit>..<commit> [--] [<path>…] //比较两个commit

status

通过显式index和commit、working tree和index之间的区别,以及working tree没跟踪的文件来表示当前仓库的状态

log

显式commit历史记录

git log --oneline //一行展示
git log --graph //展示存在多个父分支时的合并情况

show

显示不同类型的对象(blobs, trees, tags and commits)

reflog

即Reference logs,是在本地对引用操作时的记录,可以用来查找对分支的操作的历史记录,比如删除一个分支后找回。

bisect

利用二分法找出出错的commit,比如一次修改引入了一个bug,我们需要找出哪次提交改错了。
执行git bisect start [终点commit] [起点commit]在对应范围查找,代码切换到中间的一次提交,如果没bug则标记git bisect good,否则标记git bisect bad,直到找到最终的一次,提示 **is the first bad commit,执行git bisect reset 退出查错,

blame

查出某个文件的每一行的创建者和修改者

git blame [--] <file>  |grep <搜索文字> //对应文件的每一个行的修改记录,
git blame -L 起始行数,结束行数  [--] <file>  //对应文件的对应行数的修改记录

fsck

即file system check,验证数据库中对象的连接性和有效性,会显示没有脱离分支引用的对象,用于找回删除的分支

filter-branch

用来筛选出分支然后重写,可以用来修订历史记录(修改后的对象会改名),比如删除大的文件或者侵权的文件、密码等

git filter-branch --tree-filter 'rm filename' HEAD //在所有commit中删除文件 
git filter-branch --index-filter 'git rm --cached --ignore-unmatch filename' HEAD //在所有commit中对某文件停止追踪

5. 补丁

这里指的是将某一部分的修改得以复用的操作。

rebase

把部分commit接在其他commit上,即变基。
和git merge类似,语法也分为两类,第一类用于指导无法一次性完成rebase时的行为,除了--abort和--continue,还有--skip表示跳过发生冲突的commit。 第二类语法指示怎么rebase,包括

git rebase [ <upstream> [<branch>] ] 

默认以upstream 为基,即本地代码和远程代码不同的这部分修改会添加到现在远程代码的最新进度后面,如果指定branch,相当于首先切换到对应分支,再执行git rebase <upstream> 我们不仅可以以upstream为基,还可以指定比如另一个分支,这样两个分支中不同的部分就会接在指定分支的后面。
或者添加--onto参数

git rebase --onto <newbase> [ <upstream> [<branch>] ] 

如果我们加了--onto,就会以newbase为基,将 []中间(左开右闭)的这一段commit添加到newbase后面

可以在以上操作上加-i,即--interactive,对rebase过程中移动的commit进行细粒化操作,比如排序或移除。比如git rebase head~2 -i便可以对最近两次commit进行处理,执行命令后进入编辑页面可以对每个commit进行进一步编辑,处理每个指令时会按照commit从早到晚的顺序依次执行指定的命令,比如后面会讲的edit或者交换顺序时遇到冲突,相关命令分别是

  • pick:默认使用原来的commit
  • reword:使用这条commit的修改,但是要编辑这条备注,退出当前编辑对话后会进入对这条备注的编辑页面
  • edit:打断这次rebase对指定commit备注进行编辑,退出 当前编辑对话有两个选项,一个是执行git commit --amend对刚才所选的commit进行编辑,如果修改满意则执行git rebase --continue继续
  • squash:使用这次的修改,但是要合并到前一条commit中,如果没有上一条则error,退出当前对话后对合并后的commit信息进行编辑
  • drop:丢弃这次commit

排序时交换不同commit顺序即可(在vi编辑器,yy复制整行。dd剪切整行,p粘贴,按v计入可视模式,y复制选定块,d剪切选定块,wq保存退出,q不保存退出)

cherry-pick

在当前分支应用一些commit产生的变化,包括对应的commit备注,和rebase类似包括两类语法,这里只介绍怎么用,使用该命令时要保证working tree没对head进行修改

git cherry-pick  <commit>

其中…可以是一个commit,也可以是空格分隔的多个commit,也可以是两个点分隔的两个commit表示区间(前开后闭),-n 只更新index和working tree,-e编辑commit

revert

改回指定的commit,即生成相同个新的commit抵消对应commit的修改,语法参考cherry-pick

git revert <commit>

diff/apply

利用diff生成补丁文件,使用apply应用补丁,比如

git diff  >  [diff文件名]
git apply [diff文件名]

(如果应用补丁报错,试着Encoding改为utf-8,end of line sequence改为LF)

format-patch/am

这种方式生成的补丁带有commit信息 git format-patch生成补丁,git am应用补丁

6. 其他高级命令

这里是个筐,不属于前面分类的高级命令,都往里面装。

worktree

用来管理多个working tree的仓库,相当于多个目录下的working tree,共用主working tree目录下的.git目录,不同working tree需要检出不同分支

git worktree add [-f] [--detach] [--checkout] [--lock] [-b <new-branch>] <path> [<commit-ish>] //添加working tree
git worktree list //列表
git worktree remove [-f] <worktree> //移除

submodule

在一个git仓库中添加一个或多个独立的子仓库,比如可以将公用代码作为子仓库,实现各项目和公用代码的独立更新。

7. 底层命令

这里指前面介绍git原理涉及到的命令,仅作参考。

hash-object

计算一个文件的hash值,并可选的创建一个名为对应hash作为id的blob对象

git hash-object -w <file> //将指定文件作为一个blob对象写入数据库,并返回id,  -w表示将对象写入数据库

cat-file

列出指定对象的信息,包括blob, tree, commit, tag对象,-t type,-p Pretty-print the contents,-s size,-e 检查指定对象是否存在且有效

update-index

将指定对象加入index,如

git update-index --add --cacheinfo 100644 594dc0e39bc4468ee19c67e65d37b97eb963b68b test.txt

其中cacheinfo表示从数据库中读取,100644表示文件模式,即普通文件

write-tree

将index写入tree对象

commit-tree

创建一个commit对象

git commit-tree [(-p <parent>)…] [(-m <message>)…] <tree> 

git hooks

git钩子可以使git在特定的动作发生时触发自定义脚本,分为客户端钩子和服务器钩子。 如果想了解更多hook相关,可参考设计模式中的模板方法
钩子被存储在.git/hooks/内,以.sample为后缀,使用时编辑对应文件去掉后缀即可生效。

客户端钩子

包括提交工作流钩子、电子邮件钩子和其他钩子。
我们这里只关注提交工作流钩子,可用于在提交前对代码使用lint工具检查以及检查commit信息,具体使用可参考前端代码规范工具原理和最佳实践:eslint+prettier+gitHooks,想要绕过这些检查可添加--no-verify或者-n选项。

  • pre-commit 在处理提交信息前执行,可用于检查即将提交的快照
  • prepare-commit-msg 钩子在启动提交信息编辑器之前,默认信息被创建之后运行。 它允许你编辑提交者所看到的默认信息。
  • commit-msg 钩子接收一个参数,此参数即上文提到的,存有当前提交信息的临时文件的路径。
  • post-commit 钩子在整个提交过程完成后运行。 它不接收任何参数,但你可以很容易地通过运行 git log -1 HEAD 来获得最后一次的提交信息。 该钩子一般用于通知之类的事情。

服务器端钩子

服务端钩子在推送到服务器之前或之后运行

  • pre-receive 处理来自客户端的推送操作时,最先被调用的脚本是 pre-receive, 你可以用这个钩子阻止对引用进行非快进(non-fast-forward)的更新,或者对该推送所修改的所有引用和文件进行访问控制。
  • update update 脚本和 pre-receive 脚本十分类似,不同之处在于它会为每一个准备更新的分支各运行一次。 假如推送者同时向多个分支推送内容,pre-receive 只运行一次,相比之下 update 则会为每一个被推送的分支各运行一次。
  • post-receive 挂钩在整个过程完结以后运行,可以用来更新其他系统服务或者通知用户

总结

本文是对本人对git的理解,其中一些细节不免有些疏忽,如果发现后续会及时更新并补充一些示意图。

参考