文章略长,预计阅读时间28分钟
一.git init
知识点:
- 创建版本库的两种方式
- 可以创建
git-demo目录,然后再git init创建版本库 - 也可以
git init git-demo,自动完成目录的创建并创建版本库 
 - 可以创建
 - 
ls -aF(-a表示列出所有的文件,包括.开头的文件,-F表示列出文件的类型标识符,比如/表示目录)可以看的隐藏目录.git,这个.git目录就是git版本库,又叫仓库(repository),版本库位于工作区的根目录下 - 
.git版本库所在的目录,叫做工作区(这里先提出版本库和工作区的概念和它们表示的文件,具体它们之间的关系请见下面的【工作区、暂存区、版本库】段落) 
操作:

- 
我们初始化了一个仓库,可以看到会出现一个
.git文件夹,关注一下我们要关注的,看一下刚初始化出来的版本库中都有什么:有一个
HEAD文件(头指针HEAD,指向了某个分支)有一个
config文件- 
hooks文件夹- 里面有很多
.sample文件 
 - 里面有很多
 - 
object文件夹- 有一个
info文件夹,里面是空的 - 有一个
pack文件夹,里面是空的 
 - 有一个
 - 
refs文件夹- 有一个
head文件夹,里面是空的 - 有一个
tag文件夹,里面是空的 
 - 有一个
 
 bat具体请见:https://github.com/sharkdp/bat
二.git config
知识点:
- 
配置文件分三个级别,它们优先级从高到低分别是
- 版本库级别的配置文件
- 
git config -e会打开版本库级别的配置文件,即工作区里的.git/config 
 - 
 - 全局配置文件
- 
git config -e --global将会打开用户主目录下的全局配置文件进行编辑,即~/.gitconfig 
 - 
 - 系统级的配置文件
- 
git config -e --system会打开系统级的配置文件,即/usr/local/etc/gitconfig 
 - 
 
 - 版本库级别的配置文件
 - 
git config配置git config <section>.<key>例:git config core.bare可以获取某个配置的值- 
it config <section>.<key> <value>例:git config a.b xxx可以设置某个配置的值,打开配置文件可以看见这样的格式[a] b = xxx //git全局设置设置用户名和邮箱 git config --global user.name "xxx" git config --global user.email xxx.@xxx.com //删git全局设置中的变量 git config --unset --global user.name git config --unset --global user.email //设置别名让所有用户都能使用 sudo git config --system alias.st status //在本用户的全局配置中添加git命令别名 git config --global alias.st status //别名可以包含命令参数 git config --global alias.co "commit -m"
 
操作:

- 这边在全局配置文件中设置了两个别名,分别是以下两个操作,我们会在后面使用他们
- 
git status可以用git st来使用 - 
git log --graph --pretty=oneline --stat可以用git ld来使用(--graph可以看到一条跟踪链,--pretty=oneline使用精简输出显示日志,以便更简洁和清晰地看到提交的历史,--stat可以看到每次提交的文件变更统计,即显示改动了哪些文件) 
 - 
 - 如果你用了
on-my-zsh的git插件,你可以用gst来表示git status,具体请见:https://github.com/ohmyzsh/ohmyzsh/blob/master/plugins/git/git.plugin.zsh 
三.git add、git commit和Git对象
知识点:
git add做了什么操作
将本地文件的时间戳、长度、当前文档对象的id等信息保存到一个树形目录中去(
index,即暂存区)将本地文件的内容做快照保存到git的对象库中
暂存区实际上是一个包含文件索引的目录树,记录了文件名、文件的状态信息(时间戳、文件长度),文件的内容并不存储其中,而是保存在Git对象库中,文件索引建立了文件和对象库中对象实体之间的对应
- 
添加到暂存区的文件分为三种类型
- 修改的文件
 - 新增的文件
 - 删除的文件
 
 
git commit做了什么操作
将暂存区中的内容提交
如果没有对工作区的文件进行任何修改,Git默认不会执行提交,使用
--allow-empty参数可以允许执行空白提交,即git commit --allow-empty -m "xxx"
- 当你的提交说明写错时,
git commit --amend -m "新的提交说明"可以帮助你,它可以直接修改提交说明 
什么是Git对象
- 
我们Git中经常会看到一个40位的字符串,这个哈希值都是对对象内容做SHA1哈希计算出来的(SHA1是一种哈希算法),这个值可以表示Git中的四种类型对象,分别是
- 文件内容(blob)
 - 目录树(tree)
 - 提交(commit)
 - 里程碑(tag)【具体看下面Tag的段落】
 
 可以用
git cat-file命令来查看这个SHA1哈希值
- 其中
-t可以查看这个哈希值的类型,即git cat-file -t <SHA1哈希值> - 
-p可以查看这个哈希值的内容git cat-file -p <SHA1哈希值> 
这些对象保存在
.git/objects中,其中前2位为目录名,后38位为文件名几乎所有的Git功能都是使用这四个
blob、tree、commit、tag对象完成的
操作:
git add

我们先将文字
readme写入README.md文件再用
git add .将其添加进暂存区我们进入
.git/objects看一下,发现多了一个81文件夹,再进去看到一长串的数字,正如前面说的,前2位是文件夹,后38位是文件名,再用cat-file命令看这个哈希值,发现内容正是我们写进去的readme,类型是blob在进入
.git和最初我们在git init那里看到的,多了一个index,这和前面说的【git add做了什么操作】相一致这时候我们进入
.git/refs/heads发现是空的,这里存储的是分支,所以git branch输出的也是空的,所以到现在为止,初始化的版本库是没有分支的,这个命令行展示是不准确的
git commit

git cz实现的是git commit的功能,请见:https://github.com/commitizen/cz-cli提交以后可以发现
.git下多了几个文件和文件夹COMMIT_EDITMSG(这个文件保存的是上次的提交日志)logs文件夹 (日志文件记录了分支的变更,具体见【reflog】段落)refs文件夹(分支指向文件夹,具体见【分支】段落,这时候用git branch查看分支可以发现有master了,所以第一次commit的时候会创建master分支)objects文件夹下多了两个文件夹76和d6我们通过
cat-file -t命令可以看到76开头的SHA1哈希值是tree,d6开头的SHA1哈希值是commit,通过cat-file -p可以看到commit中输出了类型为tree的哈希值,tree的哈希值输出了类型为blob的哈希值,所以它们之间的联系是这样的,

- 但也不全是这样,比如
 
- 新建一个
test的文件夹,里面新建内容是cat-file的cat-file.md文件 - 这时候发现
.git/objects文件夹中多个四个文件夹,分别是cd、92、b9、75 

- 他们分别是什么,可以前面设置
git ld查下,可以看到cd开头的是个commit 

- 因为前面我们可以看到
commit是开头,所以可以通过commit往下找,可以看到92开头的是tree,b9开头的是tree,75开头的是blob,内容是这次新增的cat-file,而81开头的blob是上次的内容,即readme 

- 所以这次它们之间的联系是这样的
 

四.工作区、暂存区、版本库
知识点:
工作区
1.git rev-parse --show-toplevel可以知道工作区目录
暂存区
- 在版本库
.git目录下有一个index文件 - 
git status命令或git diff,扫描工作区的改动的时候,先根据.git/index文件中的记录的用于跟踪工作区文件的时间戳,长度等信息判断工作区文件是否改变,如果工作区文件的时间戳改变了,则文件的内容可能被改变了,需要打开文件,读取文件内容,与更改前的文件相比较,判断文件内容是否更改 - 
.git/index实际上是一个包含文件索引的目录树,像是一个虚拟的工作区,在这个虚拟工作区的目录树中,记录了文件名和文件的状态信息,即时间戳和文件长度,文件的内容并没有存储在其中,而是保存在Git对象库.git/objects目录中,文件索引建立了文件和对象库中对象实体之间的对应 - 暂存区是介于工作区和版本库的中间状态,执行提交是将暂存区的内容提交到版本库中
 - 对工作区的修改或新增的文件执行
git add命令时,暂存区的目录树将被更新,同时工作区修改或新增的文件内容会被写入对象库中的一个新的对象中,而该对象的ID被记录在暂存区的文件索引中 - 
git commit时,暂存区的目录树会写到版本库(对象库)中 - 
git ls-files --directory可以看到暂存区的的目录树 
版本库
- 
git rev-parse --git-dir可以知道版本库(.git目录)所在的目录 - 
git ls-tree -l HEAD可以看到版本库的目录树 
操作

这里可以看到
.git是文件夹是版本库,而.git版本库所在的目录,叫做工作区前面【知识点】中说了,
git status会扫描工作区的改动,这里用git status命令测试了一下,可以看到在工作区中是可以正常操作的,一旦进入了版本库.git文件夹中,则会提示 '该操作必须在一个工作区中运行'而暂存区前面有讲到,则是
.git文件夹下的index文件根据上面操作可以知道,工作区、暂存区、版本库之间的关系如下

五.git reset、git reflog
知识点
这里把git reset和git reflog放在一起的原因是,git reset就是在当前分支的前后commit中移动(reset是可以是可以重置到任意分支的commit上的,所以前面这种说法是相对于在当前分支上reset而言的),而git reflog正是把这种前后移动的操作记录下来
git reset
- 
git reset HEAD^将当前分支重置到上一个老的提交上 - 
git reset --mixed <commit>和不使用参数一致,默认为--mixed,即等同于git reset <commit>,会更改版本库中的指向以及重置暂存区,但是不改变工作区 - 
git reset --soft <commit>,只改变版本库中的指向,不改变暂存区和工作区 - 
git reset --hard <commit>- 会替换版本库中引用的指向
 - 会替换暂存区,替换后,暂存区的内容和版本库中的目录树一致
 - 会替换工作区,替换后,工作区的内容变得和暂存区一致,也和版本库所指向的目录树内容一致
 
 - 
git reset的实质其实是改变.git/refs/heads/master的指向(现在我们在master) 
git reflog
- 默认
git reflog展示的是HEAD头指针的变迁记录,可以用git reset HEAD@{n}命令来重置回原来的提交,是一个挽救错误和回到过去的一个方法 - 
git reflog展示的是.git/logs/HEAD中的内容 - 
git reflog show master这个展示的是master分支的变迁记录,即.git/logs/refs/heads/master中的内容,git reset master@{n}将master重置为两次改变之前的值,其余分支同理 
操作
git reset

绿框中的是用
git reflog重置回原来的提交,在每次操作以前的重置操作,绿框间隔的分别是git reset --mixed <commit>、git reset --soft <commit>、git reset --hard <commit>- 
【工作区、暂存区、版本库】段落中说到
git rev-parse --show-toplevel可以知道工作区目录git ls-files --directory可以看到暂存区的的目录树因为我们先加了
readme.md文件,后面加了test/cat-file.md,并提交了,所以暂存区和版本库中的目录应该是相等的,如上图所示
 - 
如前面【知识点】所说,
git reset --mixed HEAD^也即默认的git reset HEAD^,会重置暂存区和版本库,所以我们看到的结果是他们是一致的,只有README.md文件git reset --soft HEAD^是只改变版本库,所以我们可以看到暂存区中有test文件夹,而版本库中已经没了git reset --hard HEAD^同时改变工作区、暂存区、版本库,所以你看到的是这三个地方都是一致的,只有README.md文件
 

- 
上面的图可以看到
- 左边是当前
.git/refs/heads/master的指向,右边是git log的信息,可以看到最新的的commitID是和.git/refs/heads/master的指向一致的 - 
git reset HEAD^以后 - 左边和右边依旧是一致的
 
 - 左边是当前
 即
git log中master的指向就是.git/refs/heads/master中的内容,所以git reset的实质其实是改变.git/refs/heads/master的指向(我们在master分支下的前提)
六.头指针HEAD、git checkout、分支和git branch
HEAD
- 
头指针HEAD是当前分支引用的指针,它总是指向该分支上的最后一次提交。通常,理解HEAD的最简方式,就是将它看做该分支上的最后一次提交的快照 
git checkout
- 
git checkout的用法- 
不改变头指针,用指定版本的文件覆盖工作区对应的文件,如果省略
commit,则会用暂存区中的文件覆盖工作区中的文件,否则用指定提交中的文件覆盖暂存区和工作区中的文件- 
git checkout --. 等同于git checkout .会取消本地所有的修改(相对于暂存区),相当于用暂存区的所有文件直接覆盖本地文件 - 
git checkout master -- README.md会用master中的README.md替换现有分支中的README.md 
 - 
 - 
会改变头指针,这个用法最主要的作用就是切换到分支
- 
git checkout -b branch检出branch分支,更新HEAD指向branch分支,以及用branch指向的树来更新暂存区和工作区 
 - 
 
 - 
 
分支
- 分支的存在方式是在
.git/refs/heads/目录下的文件(或称引用),当前分支记录在头指针文件.git/HEAD中,切换分支命令git checkout对文件.git/HEAD的内容进行更新 
git branch
- 
git branch用于显示本地分支列表,当前分支在输出中会用*标出 - 
git branch <branchname>基于当前头指针(HEAD)指向的提交创建分支,git branch <branchname> <start-point>基于提交<start-point>创建新分支 - 
git branch -d <branchname>删除分支时会检查所要删除的分支是否已经合并到其他分支中,否则拒绝删除,git branch -D <branchname>强制删除分支,即使该分支没有合并到任何一个分支中 - 
git push origin :feature1可以删除远程版本库的feature1分支 
操作
HEAD 以及 git checkout 会改变HEAD的用法

- 
上图可以看到
左上角:
git branch看到当前分支为master右上角:
.git/HEAD的输出refs/heads/master左下角1:切换到一个新分支
feat1左下角2:
git branch看到当前分支为feat1右下角:
.git/HEAD的输出refs/heads/feat1
 即
git checkout切换分支就是改变头指针HEAD的指向,也就是改变.git/HEAD的指向
分支

- 
上图可知
左边:
.git/HEAD指向的refs/heads/feat1,那么这个指向的又是什么呢,这个refs/heads/feat1就是指向了.git/refs/heads/feat1文件前面说了,分支的存在方式是在
.git/refs/heads/目录下的文件,当前我们有两个分支feat1和master,进入到这个目录查看,确实有且只有这两个文件然后我们查看一下这个
.git/refs/heads/feat1的内容,发现这里存储着一个SHA1哈希值,这个值和右边git ld查看到的最新的SHA1哈希值是一致的
 所以头指针
HEAD和分支以及分支最新的commit之间的关系是下面图中所示的关系

git checkout 不会改变HEAD的用法
1.  git checkout .(清空工作区的修改,即用暂存区的内容覆盖工作区的内容)

首先看一下
README.md的内容,输出为readme再在这个文件上加一行
in feat1git checkout .做了什么呢,它会取消本地所有的修改(相对于暂存区),相当于用暂存区的所有文件直接覆盖本地文件因为我们还没有执行
git add,所以我们新加的in feat1还在我们的工作区当中,暂存区中README.md的内容还是readme,所以用暂存区的内容覆盖本地的以后,我们看到的内容就只是readme了
2. git branch <branchname> <start-point>

默认情况下,创建分支是基于当前
头指针(HEAD)指向(即最新的commit)的提交创建的,但是有时候我们希望从某个commit开始创建一个分支,就会用到git branch <branchname> <start-point>的命令左边是
master分支的git log记录,可以看到现在有两个提交我们从第一个提交创建一个分支,并切换到这个分支,再查看一下
git log会发现这个分支确实是从指定的commit的d6cee5a中创建出来的
3. git checkout master -- README.md

- 有时候会遇到想拿另一个分支的文件覆盖当前分支的这个文件的情况,这时候,
git checkout master -- xxx.md命令就派上了用场 - 左边:现在
master分支上的README.md文件加了一行in master - 右边:切换到
feat1分支,使用git checkout master -- xxx.md可以看到使用前没有新加的内容,使用后确实把master分支上的内容给拿过来了 
七.git merge、git rebase、git cherry-pick、git revert
知识点:
git merge
- 合并操作的大多数情况下,只需要提供一个
<commit>(提交ID或对应的引用:分支、里程碑等)作为参数,(git merge origin/master)合并操作将<commit>对应的目录树和当前工作分支的目录树的内容进行合并,合并后的提交以当前分支的提交作为第一个父提交,以<commit>为第二个父提交 
git rebase
- 
不同分支中:在
feature1分支上git rebase master做了什么操作- 将
feature1分支中的commit先保存在一个补丁文件 - 重置到
master的最新提交 - 将
feature1分支中的保存的补丁文件重放 
 - 将
 相同分支中:去掉中间某两个提交
git cherry-pick
- 实现提交在新的分支上重放,即从众多的提交中挑选出一个提交应用在当前的工作分支中,该命令需要提供一个提交ID作为参数,操作过程相当于将该提交导出为补丁文件,然后在当前HEAD上重放,形成无论是内容还是提交说明都一致的提交(
git cherry-pick后面可以跟tag,如git cherry-pick F) - 去掉中间某两个提交
 
git revert
- 
git revert <commit>可以对某个提交进行撤销 
操作
git merge

- 左上角:是
master上的git log记录,可以看到现在master上有三个提交(d6,cd,23),最后一个提交的操作即前面的操作(在README.md中加了一行in master) - 左下角:切换到
feat1分支,在README.md中加了一行in feat1 - 右上角:
feat1分支的的git log记录,也有三个提交(d6,cd,7e) - 右下角:在
feat1分支执行git merge master,因为修改的是同一个文件,会产生冲突,解决冲突后的记录如下 

可以看更直观的这个图

- 
feat1分支是在cd这个提交的时候从master中切过来的,切了分支以后,master分支上新加了一个commit是23,feat1分支新加了一个提交是7e,然后在feat1分支中git merge master会产生一个新的提交b3,它的第一个父提交是7e,第二个父提交是23,可以用git cat-file看一下b3这个提交,如下图,可以看到确实如此 

git rebase

- 首先在
feat1分支回退到合并以前的那个提交 - 左上角:是
master上的git log记录,可以看到现在master上有三个提交(d6,cd,23) - 左下角:
feat1分支的的git log记录,也有三个提交(d6,cd,7e) - 右上角:
feat1分支上执行git rebase master,因为修改的同一文件,先解决冲突,再用git rebase --continue继续rebase - 右下角:
rebase完成以后的git log图 - 可以看下面这个图
 

- 
feat1分支上git rebase master会将提交7e保存在一个补丁文件中,然后重置到master的最新提交,也就是23,再将补丁文件重放,即提交74,可以看到rebase也是会产生一个新的提交,只不过是在一条直线上的 
git cherry-pick

- 左上角:
feat1分支的git log记录 - 左下角:
master分支的git log记录,经过上面的git rebase操作,feat1分支上比master多了一个提交 - 右上角:这时候我们到
master分支把想把feat1分支上最新的那个提交(74)拿过来,即git cherry-pick 745996477503f07285753359ee1b9c14495f875c - 右下角:这时候可以看到
master分支上也有这个提交了,只是提交ID已经改变了,因为git cherry-pick也会将74这个提交先存在一个补丁文件中,再在master分支上重放 
git revert

- 这时候我突然不想要某个提交了,可以使用
git revert - 左上角:
master分支上是有test文件夹的,是cd这个提交新增的 - 左下角:
master分支的git log记录 - 右上角:
git revert cdab8eea0aa2525434084ab7129526bc751d8748以后可以看到已经没有test文件夹了 - 右下角:
master分支的git log记录,可以看到多了一个revert开头的提交 
八.git tag和git rev-parse
知识点
git tag
- 
里程碑:(还有带签名的里程碑,日常没有用到,这边不详细介绍)
轻量级里程碑,
git tag v1.0.0创建里程碑后,会在.git/refs/tags目录下创建一个新文件,查看一个这个文件的内容,是一个SHA1哈希值,指向的是一个提交,轻量级里程碑的创建过程没有记录,因此无法知道是谁创建的里程碑,何时创建的里程碑,所以尽量不要使用这种方式带说明的里程碑:
git tag -m "my info" v1.0.0这个命令创建了带说明的里程碑v1.1.0后,会在版本库的.git/refs/tags目录下创建一个新的引用文件,这个文件也是SHA1哈希值
 - 
查看里程碑的具体信息
用
git cat-file -t v1.0.0可以看到,这个SHA1哈希值指向的不再是一个提交,而是一个tag对象同时
git cat-file -p v1.0.0可以看到里面包含了创建里程碑时的说明,以及对应的提交ID等信息
 删除本地里程碑:
git tag -d v1.0.0,里程碑没有类似reflog的变更记录机制,一旦删除不易恢复,慎用,在删除里程碑的命令输出中,会显示该里程碑所对应的提交ID,一旦发现删除错误,可以用git tag v1.0.0 <commit>进行重建删除远程里程碑:
git push <remote-url> :<tagname>该命令的最后一个参数实际上是一个引用表达式,引用表达式的格式一般为<ref>:<ref>,该推送命令使用的引用表达式冒号前的引用被省略,其含义是将一个空值推送到远程版本库对应的引用中,即删除远程版本库中相关的引Git没有提供对里程碑重命名的命令,如果对里程碑名字不满意,可以删除旧的里程碑,然后用新的名称创建里程碑。之所以没有提供里程碑重命名的命令,是因为里程碑的名字不但反映在
.git/refs/tags引用目录下的文件名,对于带说明的里程碑,里程碑的名字还反映在tag对象的内容中,里程碑建立后,如果需要修改,可以使用同样的里程碑名称重新建立,不过需要加上-f参数或--force参数强制覆盖已有的里程碑推送里程碑:
git push不会将里程碑推送到上游,创建的里程碑,默认只在本地版本库中可见,不会因为对分支执行推送而将里程碑也推送到远程版本库,git push origin v1.0.0显式推送以共享里程碑获取里程碑:
git pull可以获取远程最新的里程碑,只会获取远程分支所包含的新里程碑同步到本地,不会将远程版本库的其他分支中的里程碑获取到本地
git rev-parse
- 
git rev-parse HEAD输出HEAD的commit - 
git rev-parse master输出master最新的commit - 
git rev-parse 1.0.0输出tag 1.0.1指向的tag对象 - 
git rev-parse 1.0.0^{}输出的是tag对象指向的commit - 
git rev-parse 1.0.0^{tree}输出的是tag对象指向的树目录 
操作

前面说了,创建
tag后会在.git/refs/tags目录下创建一个新文件,所以我们先进这个目录看看,发现是空然后我们创建一个带说明的里程碑
v1.0.0发现
.git/refs/tags目录下多了一个v1.0.0文件,我们看下这个文件的内容,是一个SHA1哈希值那么这个SHA1哈希值到底是什么呢,我们可以用
git cat-file命令看下发现它是一个tag,那么tag里存储的又是什么呢,我们一步步往下找,发现它指向了一个commit,commit指向blob,是不是很熟悉,就是我们在前面【Git对象】段落讲到的指向,至此,Git对象中的最后一个对象类型,即Tag,终于出现了那么这边为什么把
git tag和git rev-parse放在一起呢,git rev-parse是个很神奇的命令,前面【工作区、暂存区、版本库】段落说到,它可以查看工作区和版本库的目录,而我把它放在这里的原因在于,我称它在这里是一个将git cat-file一步到位的命令,为什么这么说呢,请看下面

- 可以看到
- 
git rev-parse v1.0.0的内容就是.git/refs/tags/v1.0.0中存储的内容,即tag v1.0.0的SHA1哈希值 - 
git rev-parse v1.0.0^{}则是tag中指向的commit - 
git rev-parse v1.0.0^{tree}则是commit指向的tree - 所以如果对于他们之间的关系清楚了的话,这个命令是可以将
git cat-file一步到位的 
 - 
 - 综上,所以以上的关系为
 

九.git stash
知识点
用
git stash保存进度,实际上会将进度保存在.git/refs/stash所指向的提交中,多次的进度保存,实际上相当于引用.git/refs/stash一次又一次变化,而cat-file一下.git/refs/stash,你会发现这是个commit,保存的就是我们写到一半的东西而
.git/refs/stash的变化则由reflog,即.git/logs/refs/stash所记录下必须要
git add以后才会生效git stash用于保存当前进度git stash save "message..."可以在保存工作进度的时候使用指定的说明git stash list显示进度列表,可以保存多次工作进度,并且在恢复的时候进行选择git stash pop从最近保存的进度进行恢复,并将恢复的工作进度从存储的工作进度列表中删除git stash clear删除所有存储的进度git fsck可以看到版本库中包含的没有被任何引用关联的松散对象
操作

有时候某个分支写到一半,要到另一个分支中加点东西,就可以使用这个命令
首先进入
.git/refs/目录中发现里面只有存储分支的heads和存储tag的tags然后在
README.md加入一行修改,并用git stash保存然后发现
.git/refs/目录中出现了一个stash文件,可以查看一下这个文件内容,发现是个SHA1哈希值,再用git cat-file看一下,竟然是个commit,按照【Git对象】段落里说的指向,我们可以一路找到blob,查看一下,发现它的内容是我们前面修改的内容,事情好像很清楚了,但是还没有结束前面说过
git reset的任何操作,git reflog都能追踪得到,而这里的左上角是git stash list的内容,而右下角就是.git/logs/refs/stash的内容,可以看到这两者内容是一样的,只是展现方式不一致,所以git stash的列表,都在.git/logs/refs/stash中记录下来

不同的是
git reset的记录我们一般很少去清除它,它也是90天前的数据才会过期,所以很安全,但是git stash就不一样了,比如你执行了git stash clear了,然后.git/refs/stash和.git/logs/refs/stash的内容都被清空了,然后你发现操作错误了,但是还是没有关系git fsck可以看到版本库中包含的没有被任何引用关联的松散对象,没有被引用是什么意思,一个commit可以在分支中的git log中看到,在git stash list中用到了,那它就是有被引用,否则就是个悬空对象。松散对象又是什么,Git对于SHA1哈希值作为目录名和文件名保存的对象有一个术语,称为松散对象,也就是前面【Git对象】段落说的,钱2位是文件夹,后38位是文件,这样存储的对象叫做松散对象,松散对象打包后可以提高访问效率(这个本文不再展开)看到那个标着红框的
commit了吗,你有没有发现就是前面没有git stash clear以前的.git/refs/stash里存储的commit,然后你可以git stash apply <红框里的commit>来恢复它

你以前有没有遇到你新增了一个文件,然后你想
git stash,然后你发现没成功,切换到其他分支以后这个文件还在,然后你觉得这个git stash真难用左上角:我先新建了一个
stash.md文件,然后我想git stash save "stash stash.md"一下,发现提示"没有要保存的本地修改",然后看一下这个文件的状态,发现并没有在暂存区里,而是在工作区中左下角:我切换到
feat1分支,发现这个文件被带过来了右上角:前面有说,必须要
git add以后才会生效,然后我git add一下,然后git stash成功了右下角:
git stash list输出的结果是不是有点神奇,但是如果你了解了前面说的
git stash保存的是一个commit,然后你了解了工作区、暂存区以后,是不是可以理解了,没有git add以前,新增文件是在工作区中的,然后你生成一个commit,不会被保存是对的,所以切换分支以后,这个文件没有被追踪,所以另一个分支中也能看到这个文件,但是第一个例子为什么没有这个问题呢,因为READM.md文件是修改的文件,本身已经在暂存区中了,然后你git stash生成的commit会将它的内容保存下来其实我们可以用
git reset来实现这种效果,每次写到一半先做一个提交,然后切其他分支操作,切换回来以后,先git reset HEAD^,然后继续修改,再提交
十.git blame文件追溯命令
知识点
- 
git blame README.md可以看到是谁在什么时候,以及什么提交引入了什么东西 

操作
- 上面就是
git blame README.md输出的结果,vs code和vim都有类似的工具,可以叫它专业甩锅神器 
十一.冲突
知识点
- 文件
.git/MERGE_HEAD记录所合并的提交ID - 文件
.git/MERGE_MSG记录合并失败的信息 - 版本库暂存区会记录冲突文件的多个不同版本,可以用
git ls-files -s查看 - 暂存区编号为1的,用于保存冲突文件修改之前的的副本,即冲突双方共同的祖先版本,可以用
git show :1:README.md访问(共同祖先版本) - 暂存区编号为2的,用于保存当前冲突文件在当前分支中修改的副本,即
<<<<<<<(七个小于号)和=======(七个等号)之间的内容(当前分支修改的版本) - 暂存区编号为3的,用于保存在当前冲突文件在合并版本(分支)中修改的副本,即
=======(七个等号)和>>>>>>>(七个大于号)之间的内容(他人修改的版本) - 处于合并冲突状态时,无法再执行提交操作,可以放弃合并,即重置暂存区(
git reset),或者对冲突进行解决 

左上角:我们在
master分支上修改了README.md文件左上角:我们在
feat1分支上修改了README.md文件右边:我们在
feat1分支上merge master,这时候冲突了,我们看下暂存区里面的文件,发现同一个文件有三个,第三列分别是1,2,3,这是什么东西呢,就是暂存区编号,当冲突发生的时候,会用到0以上的暂存区编号文件
.git/MERGE_HEAD记录所合并的提交ID,现在是feat1分支合并master,则是master中最新的提交,你可以看到输出确实是master分支中的最新提交文件
.git/MERGE_MSG中可以看到,是feat1分支合并master分支的时候冲突了

那么暂存区编号为1,2,3的文件到底是什么呢
可以看到
git show :1:README.md访问暂存区编号1中README.md的内容,它展现的是冲突双方共同的祖先版本,可以从前面的git log记录中看到,在23这个提交以前,是他们共同的版本,即右边第一个框中的内容git show :2:README.md是当前分支(feat1)中修改的内容,也就是右边第二个框中的内容,也是左边<<<<<<<和=======之间的修改同理,
git show :3:README.md是合并分支(master)中修改的内容,也就是右边第三个框中的内容,也是左边=======和>>>>>>>之间的修改了解了这些以后,以后遇到冲突都不用慌了,总能找回来的
十二.git diff、git status、git clean
知识点
git diff
工作区vs暂存区:
git diff工作区vs版本库:
git diff HEAD暂存区vs版本库:
git diff --cached或git diff --staged暂存区vs里程碑:
git diff --cached v1.0.0工作区vs里程碑:
git diff v1.0.0里程碑vs里程碑:
git diff v1.0.0 v1.0.1
git status
- 可以看到当前文件的状态,现在到底在暂存区还是工作区
 
git clean
git clean -nd可以查看哪些文件和目录会被删除git clean -fd可以清除当前工作区中没有加入版本库的文件和目录(非跟踪文件和目录)
操作
git diff
- 我们在修改了文件要加入暂存区以前都会
git diff一下,看一下我们到底改了什么东西,这个命令对比的就是工作区和暂存区之间的区别,其实我们还可以比较工作区和版本库,暂存区和版本库,甚至里程碑和里程碑等等,比较简单,这里不再演示 
git clean
- 前面说过,
git checkout .会取消本地所有的修改(相对于暂存区),相当于用暂存区的所有文件直接覆盖本地文件,可是有时候你会发现清不掉,比如这样 

- 你新增了一个
clean.md,它并没有加入暂存区,然后你使用了git checkout .,但你发现并没有清除,因为这个命令是用暂存区的内容来覆盖本地所有的修改,注意,是修改,而我们现在是新增,对于新增文件,你可以用git clean -nd命令来查看哪些文件和目录会被删除,然后git clean -fd清除它 
十三.git pull、git push、远程版本库
知识点
git clone
git clone <respository> <directory>
远程版本库
- 注册远程版本库
git remote add origin <repository>,.git/config里会看到配置,远程版本库名称为origin 
git pull
- 
git pull实际上是由两个步骤组成一个是获取操作git fetch和另一个合并操作git merge - 
git fetch将共享版本库yourbranch分支的最新提交获取到本地,并更新到本地版本库特定的引用refs/remotes/origin/yourbranch(简称为origin/yourbranch) - 获取操作是将远程的共享版本库的提交、里程碑、分支等复制到本地
 - 不带分支参数时会根据
.git/config中分支的配置进行拉取和推送 - 
git config branch.<branchname>.rebase true可以在<branchname>中执行git pull命令时,遇到本地分支和远程分支出现偏离的时候,会采用变基操作,而不是默认的合并操作,或者git pull --rebase 
git push
- Git通过检查推送操作是不是"快进式"的操作,从而保证用户的提交不会相互覆盖
 - 一般情况下,推送只允许"快进式"推送,所谓快进式推送,就是要推送的本地版本库的提交是建立在远程版本库相应分支的现有提交基础上的,即远程版本库相应分支的最新提交是本地版本库最新提交的祖先提交
 - 
git rev-list HEAD可以看到本地版本库的最新提交及其历史提交的SHA1哈希值,git ls-remote origin可以看到远程版本库的引用对应的SHA1哈希值,可以看到远程版本库所包含的最新提交是不是本地最新提交的祖先提交,当用户执行推送的时候,Git就是利用类似方法判断出当前的推送是不是一个快进式推送,如果不是,会产生警告并终止 - 执行
git push的时候,如果没有设定推送的分支,而且当前分支也没有注册到远程的某个分支,将检查远程分支是否有和本地同名的分支名,如果有则推送,没有则报错 
操作
这一段到底讲了什么呢,可以用下面的图来展示,具体操作git push和git pull操作这里不再赘述

十四.思维导图压轴

写在最后:
Git实在太强大,本文只是讲到常用的功能,还有更多可以探索的如:Git协议、分离头指针、文件归档、git bisect、裸版本库、git-gc、合并策略、git submodule、子树合并、钩子脚本、Git模版等
感谢阅读