文章略长,预计阅读时间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 feat1
-
git 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模版等
-
感谢阅读
网友评论