Git 是当前最广泛使用的版本控制系统,具备非常强大的版本控制能力。Git 有非常多的命令,很多人被种类繁多的命令搞的非常头大,也会经常忘记。本文尝试从原理角度来介绍 Git 常用命令,以便于加深对 Git 原理的理解。只有理解了原理,才能知道这些命令到底对仓库做了什么,进而才能更好使用命令,也更加不容易忘记。作为一个版本控制系统,理解其原理也是十分必要的,否则,很多命令根本不敢用,用错了可能会造成严重后果,搞丢代码的成本是非常大的,谁都不想自己或他人做工作被一键抹除。知道原理后,即使一不小心操作错了,也可以坦然处之,轻松恢复。

相比命令行工具,可视化的 GUI 就要直观和方便很多,但是很多时候一样可能因为对于具体做什么不了解而操作错误,并且,目前常用的 GUI 软件,都未提供全部的命令行操作能力,GUI 很多时候,也没有命令方便快捷。

仓库概述

Git 是一个分布式的版本控制系统,不同于 svn 只有一个中心仓库,必须能连接到 svn 服务才能提交,Git 可以随时提交而不依赖于中心仓库,因为本地存在一个独立的仓库,完全支持离线操作。本地仓库和远程仓库是一种比较松散的关系。

Git 中,每个仓库可以存在多个仓库副本,这个仓库暂且称为 “原始仓库”,“原始仓库” 因为需要备份或协作等方面的需要通常存储在托管服务器上。

仓库副本通常是通过 cloneFork(本质其实也是clone) 创建的,仓库副本和原始仓库是上下游关系,这些副本仓库可能在本地,也可能在某台服务器上。

原始仓库如果托管在托管服务器上,通过 Fork 的方式可以创建很多位于托管服务上的副本,这些副本在不同的账户下,Fork 并不是 Git 提供的,而是托管服务提供的。Fork 出来的仓库副本与原始仓库之间的关系也由托管服务记录和维护的,这些副本仓库都有一个相同的 上游仓库(即原始仓库),同时副本仓库也可以继续被 Fork 产生多个 下游仓库,下游仓库可以通过 PullRequest/MergeRequest 向上游仓库发起跨仓库的 merge 请求,进行仓库间的交互。这些位于托管服务上的仓库,不论是原始仓库还是Fork出来的副本仓库,都可以被 clone 到本地创建本地仓库,本地仓库也是一个副本。

本地仓库可以是原始仓库的副本,也可以是原始仓库的某副本的副本,相对于本地仓库,这些仓库称之为远程仓库,本地版本库可以同时对应多个远程仓库。本地仓库通过 pushfetch 等命令和远程仓库同步。

通过前面的描述,引申出了几个概念:原始仓库 <-> 副本仓库、上游仓库 <-> 下游仓库、远程仓库 <-> 本地仓库,这些概念都是相对而言的,便于进行角色区分。

仓库间的交互:

  1. 远程仓库 和 本地仓库 之间,主要进行的是同步,本地修改同步到远程,或远程同步到本地。
  2. 远程仓库 和 远程仓库 之间,主要进行的是跨仓库的分支合并,也就是 PullRequest/MergeRequest

在未使用 Fork 的模式中,就没有 2 这种情况了。

本地仓库主要用来完成内容变更,远程仓库主要用来完成多人协作与数据备份。

托管服务上,Fork 创建的仓库副本关系如下:

1
2
3
4
5
6
7
8
base/Base
├──── user_a1/Base
├──── user_a2/Base
├──── user_a3/Base
├──── user_a4/Base
│ ├─── user_b1/Base
│ └─── user_b2/Base
└──── user_a5/Base

这些仓库都可被有权限的用户 pull 本地仓库,并正在修改之后 push 远程仓库。

为什么需要 Fork

  1. 避免分配原始仓库的访问权限给无关用户,防止仓库被破坏
  2. 本地仓库无法直接 Merge 到远程仓库,远程副本仓库能够提供了合并到原始仓库的能力
  3. 合并需要发起 PullRequest/MergeRequest 提供了代码审核的窗口

Git 的单个仓库的工作原理是比较简单的,但是当多个不同角色的仓库同时存在协同工作的时候,确实非常复杂的,有非常多的玩法。只有真正的理解内部原理,方能运用自如。

Git 的命令是建立在这些底层模型上的,命令也正是为处理并维护这些关系而设计的。所以也只要有理解原理,才能熟练运用这些命令。

下面开始进入正题。

仓库创建

任何操作开始前,首先要有个一个仓库。

Init

核心功能:

  1. 创建一个新的空 Git 仓库,或者重新初始化一个已经存在的 Git 仓库。

常规方式创建一个仓库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
$ git init workspace
Initialized empty Git repository in /Users/liuyanjie/git-learn/workspace/.git/

$ tree -ar
.
└── workspace
└── .git
├── refs
│   ├── tags
│   └── heads
├── objects
│   ├── pack
│   └── info
├── info
│   └── exclude
├── hooks
│   ├── update.sample
│   ├── prepare-commit-msg.sample
│   ├── pre-receive.sample
│   ├── pre-rebase.sample
│   ├── pre-push.sample
│   ├── pre-commit.sample
│   ├── pre-applypatch.sample
│   ├── post-update.sample
│   ├── fsmonitor-watchman.sample
│   ├── commit-msg.sample
│   └── applypatch-msg.sample
├── description
├── config
└── HEAD

10 directories, 15 files

以上创建的仓库,仓库文件 存放在 工作区目录 workspace 的子目录 .git 下。

分离的方式创建一个仓库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
$ git init --separate-git-dir=.tig workspace
Initialized empty Git repository in /Users/liuyanjie/git-learn/.tig/

$ tree -ar
.
├── workspace
│   └── .git
└── .tig
├── refs
│   ├── tags
│   └── heads
├── objects
│   ├── pack
│   └── info
├── info
│   └── exclude
├── hooks
│   ├── update.sample
│   ├── prepare-commit-msg.sample
│   ├── pre-receive.sample
│   ├── pre-rebase.sample
│   ├── pre-push.sample
│   ├── pre-commit.sample
│   ├── pre-applypatch.sample
│   ├── post-update.sample
│   ├── fsmonitor-watchman.sample
│   ├── commit-msg.sample
│   └── applypatch-msg.sample
├── description
├── config
└── HEAD

10 directories, 16 files

$ cat workspace/.git
gitdir: /Users/liuyanjie/git-learn/.tig

从上面的目录结构可以看到,相比常规方式创建仓库,通过分离的方式创建的仓库将 仓库(.git)工作区目录(workspace) 中分离出去。仓库(.git) 不再是一个目录,而是包含指向仓库路径的一个文件,实际上利用这一特性,可以创建多个工作区共享同一仓库,也就可以支持同一个仓库拥有多个工作区。

另外也可以通过环境变量 GIT_DIR=path/to/repo.git 指定 仓库(.git) 的路径,下面这种方式和上面的方式是等价的(注意:相对路径不同)。

1
2
$ GIT_DIR=../.tig git init workspace
Initialized empty Git repository in /Users/liuyanjie/git-learn/.tig/

不仅如此,Git 还支持将 Objects 从 仓库(.git) 中移出,通过 GIT_OBJECT_DIRECTORY=$GIT_DIR/objects Git 对象存储路径,不过这种用法在本地很少见。

有兴趣可以用以下命令做实验:

1
$ GIT_OBJECT_DIRECTORY=../objects GIT_DIR=../.tig git init workspace

默认情况下,仓库在创建的过程中,拷贝了一些的模版文件到 仓库(.git) 目录下,默认的模版路径和文件示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ tree /usr/local/Cellar/git/2.18.0/share/git-core/templates
/usr/local/Cellar/git/2.18.0/share/git-core/templates
├── description
├── hooks
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── fsmonitor-watchman.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   ├── pre-receive.sample
│   ├── prepare-commit-msg.sample
│   └── update.sample
└── info
└── exclude

2 directories, 13 files

可以看到,模版目录里面的内容,和实际仓库的内容相同的,用模版创建仓库时,就是原封不动的将仓库内容拷贝到仓库目录下。
基于这一点,我们可以以一个已经存在的仓库作为模版,创建另一个仓库,Git 会把模板路径下的文件的拷贝到新的仓库下。

可以在运行的时候通过 --template=GIT_TEMPLATE_DIR 环境变量 或 init.templateDir 配置 指定模版路径位置。

还可以通过 git init --bare 参数创建一个裸仓库,裸仓库的指没有工作区的仓库。

1
2
3
4
5
6
$ git init --bare          
Initialized empty Git repository in /Users/liuyanjie/git-learn/

# liuyanjie @ bogon in /Users/liuyanjie/git-learn on git:master o [16:48:01]
$ ls
HEAD config description hooks info objects refs

因为无工作区,仓库文件直接放在当前目录下了,可以手动创建工作区并链接到仓库目录。

在一个已经存在的仓库目录中运行 init 命令是安全的,它不会覆盖原来已经存在的文件(包括仓库和工作区)。
重新运行 init 的可以用来安装新添加的模版,或者用来将仓库分离到其他的位置。

新创建的仓库的仓库配置:

1
2
3
4
5
6
7
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true

只有 core 相关的几个配置项。

通过以上对命令及参数的效果的参考,可以了解到 Git 文档中不为人知的一些内容。

初始化仓库是 Git 工作流中的第一步,通常发生在本地,在托管服务创建仓库之后,仓库并未初始化,需要在本地创建并初始化仓库后,同步到远程,然后再同步本地。

通常情况下,都是需要两个仓库的,一个本地仓库,一个远程仓库,以支持复杂的开发工作流。

Clone

核心功能:

  1. 初始化并配置仓库,记录本地仓库和远程仓库的对应关系,包括仓库和分支映射关系
    1. 克隆仓库到一个新的目录,实际上就是 git init
    2. 为每一个被克隆仓库中的分支创建对应的远程追踪分支
    3. 从克隆仓库的当前活动分支创建并检出初始分支到工作区目录
  2. 同步仓库
    1. 通过 git fetch 更新所有远程追踪分支
    2. 通过 git pull 合并远程 master 分支到本地 master 分支,快速合并

类似于如下过程:

git init -> git remote set-url origin git://... -> git fetch -> git pull -> git checkout HEAD

通过以上核心功能可以看到,Clone 实际上是对多个命令功能的组合,并进行了一些配置工作。这样我们可以猜测,Clone 命令的很多参数应该是从其他命令继承过来的。

克隆一个远程仓库需要存在一个远程仓库,并且有一个可以访问的远程仓库的地址,Git支持多种访问协议,最常见的如 git://https://
详见:GIT-URLS

下面是一个常见的 clone 操作:

1
2
3
4
5
6
$ git clone git@github.com:liuyanjie/spec.git
Cloning into 'spec'...
remote: Counting objects: 49, done.
remote: Total 49 (delta 0), reused 0 (delta 0), pack-reused 49
Receiving objects: 100% (49/49), 49.83 KiB | 25.00 KiB/s, done.
Resolving deltas: 100% (15/15), done.

通过输出内容可以看到,Clone 主要都做了哪些事情,可以对比上面的过程描述。

1
2
3
4
5
6
7
$ cat spec/.git/config
[remote "origin"]
url = git@github.com:liuyanjie/spec.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "master"]
remote = origin
merge = refs/heads/master

git clone 之后的仓库配置中,可以看到多出了以上内容,配置了对应的远程仓库地址及追踪关系,配置了本地 master 对应的远程分支,该配置为 Git 默认配置,一般不需要修改。

常见用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 克隆本地仓库 默认是硬链接的,关闭需要加 --no-hardlinks
git clone path/to/local/git/repository

# 克隆远程仓库
$ git clone git@github.com:liuyanjie/knowledge.git
Cloning into 'knowledge'...
remote: Counting objects: 495, done.
remote: Compressing objects: 100% (141/141), done.

# 克隆远程仓库 只克隆最后一个Commit提速
$ git clone git@github.com:liuyanjie/knowledge.git --depth=1
Cloning into 'knowledge'...
remote: Counting objects: 300, done.
remote: Compressing objects: 100% (247/247), done.

# 克隆远程仓库 只克隆最后一个Commit提速,并通过另外一个仓库加速
$ git clone \
--depth=1 \
--reference-if-able=/Volumes/Data/Data/ws/knowledge \
git@github.com:liuyanjie/knowledge.git
Cloning into 'knowledge'...
remote: Total 0 (delta 0), reused 0 (delta 0), pack-reused 0

Clone 的过程中,通过一些参数可以有效的减少 Clone 的等待时间,如在 CI 的构建流程中,可以提高构建时间。

了解 git clone 命令的实际工作流程,能够了解 clone 的过程做了什么以及能做到什么,日常使用也用不到很多复杂的操作,关于更多的命令参数,可自行通过文档了解。

仓库同步

因为 Git 是一个分布式的版本控制系统,同时存在多个仓库副本,仓库副本之间的同步是非常重要的一环。不同于许多分布式系统(例如分布式数据库)能够自动完成节点间的数据同步,Git 无法自动的完成仓库同步,所以仓库同步完全依赖于使用者自行通过各类操作界面完成。

数据同步的内容主要有:分支、标签、数据等内容

数据同步基于 RefSpec ,它描述了本地仓库与远程仓库间分支和标签的映射关系及同步策略。

下面示例中 +refs/heads/*:refs/remotes/origin/* 即为 RefSpec

1
2
3
4
5
6
$ cat .git/config
[core]
...
[remote "origin"]
url = git@github.com:liuyanjie/knowledge.git
fetch = +refs/heads/*:refs/remotes/origin/*

Ref

示例中的 RefSpec 表明:远程仓库中所有分支 refs/heads/*,对应到本地仓库下所有分支 refs/remotes/origin/*,分支名称不变。如果需要改变分支名称,则需要配置针对分支特定的 RefSpec

在了解 RefSpec 之前,需要先了解下 RefGit 内部原理 - Git References

如同描述的一样,RefSpec 描述了 remote-refslocal-refs 的对应关系。

RefSpec 写法示例:

1
2
3
4
5
6
+refs/heads/*:refs/remotes/origin/*
+refs/heads/master:master
+master:refs/remotes/origin/master
master:master
:master
master:

RefSpec 的格式是一个可选的 + 号,接着是 <src>:<dst> 的格式,这里 <src> 是远程仓库的引用格式,<dst> 是将要记录在本地仓库的引用格式。可选的 + 号告诉 Git 在即使不能快速演进的情况下,也去强制更新它,也就是与远程保持强一致的同步。

从远程仓库获取指定数据到本地仓库,如:

1
2
3
branch master <==> +refs/heads/master:+refs/remotes/origin/master
branch A:a <==> +refs/heads/A:+refs/remotes/origin/a
tag <tag> <==> +refs/tags/<tag>:refs/tags/<tag>

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ cat .git/config
[core]
...
[remote "origin"]
url = git@github.com:liuyanjie/knowledge.git
fetch = +refs/heads/*:refs/remotes/origin/*

$ tree .git/refs
.git/refs
├── heads
│ ├── feature
│ │ └── travis-ci
│ └── master
├── remotes
│ └── origin
│ ├── feature
│ │ └── travis-ci
│ └── master
└── tags
└── v0.0.0

以上对应关系:

local-branch@local remote-branch@local remote-branch@remote
master origin/master master
feature/travis-ci origin/feature/travis-ci feature/travis-ci
refs/heads/feature/travis-ci refs/remotes/origin/feature/travis-ci refs/heads/feature/travis-ci

表格中,只描述了存在一个远程仓库 origin 的情形,实际上是可能存在多个远程仓库的。

注意:RefSpec 描述了 remote-branch@localremote-branch@remote 之间的对应关系,并不是 local-branch@localremote-branch@remote 之间的关系,它们之间的存在的追踪关系在其他配置项中描述。

local-branch@local 下的 分支,是在本地存在的分支,可能从远程某个分支 checkout,也可能是本地新建的。

RefSpec 可以应用在命令行中,但是一般不会出现在命令行中,而是由某些命令自动写在配置文件中,并在某些命令执行时自动应用配置。

例如:git remote add remote-name,Git 会获取远端上 refs/heads/ 下面的所有引用,并将它写入到本地的 refs/remotes/remote-name

1
2
3
4
5
6
7
8
9
10
git remote add liuyanjie git@github.com:liuyanjie/knowledge.git

$ cat .git/config
[remote "origin"]
url = git@github.com:liuyanjie/knowledge.git
fetch = +refs/heads/*:refs/remotes/origin/*

[remote "liuyanjie"]
url = git@github.com:liuyanjie/knowledge.git
fetch = +refs/heads/*:refs/remotes/liuyanjie/*

以下几种方式是等价的:

1
2
3
4
5
6
7
git log master
git log heads/master
git log refs/heads/master

git log origin/master
git log remotes/origin/master
git log refs/remotes/origin/master

通常都是使用省略 refs/heads/refs/remotes/ 的形式。

以上示例中 RefSpec 中包含 * 会使 Git 拉取所有远程分支到本地,如果想让Git只拉取固定的分支,可以将 * 修改为指定的分支名。

也可以在命令行上指定多个 RefSpec,如:

1
git fetch origin master:refs/remotes/origin/master topic:refs/remotes/origin/topic

同样,也可以将以上命令行中的 RefSpec 写入配置中:

1
2
3
4
5
[remote "origin"]
url = git@github.com:liuyanjie/knowledge.git
fetch = +refs/heads/master:refs/remotes/origin/master
fetch = +refs/heads/develop:refs/remotes/origin/develop
fetch = +refs/heads/feature/*:refs/remotes/origin/feature/*

以上,feature 可以看做是命名空间,划分不同的分支类型。

上面描述都是拉取时 RefSpec 的作用,同样推送是也需要 RefSpec

1
git push origin master:refs/heads/qa/master

推送一个空分支可以删除远程分支

1
git push origin :refs/heads/qa/master

RefSpec 描述了本地仓库分支和远程仓库分支的对应关系。很多时候可以省略,因为 Git 包含了很多默认行为。

远程仓库 refs/heads/* 中 的分支大都是 其他 本地仓库 同步到远程的。

远程仓库 refs/heads/*创建 的新分支,在同步数据的时候默认会被拉到本地,删除 的分支默认不会在本地进行同步删除,修改 的分支会被更新,并与本地追踪的开发分支进行合并。

更多阅读:Git 内部原理 - The Refspec

以上,通过 RefSpec 描述的 本地仓库 和 远程仓库 中 分支 是如何对应的,了解了 本地仓库 和 远程仓库 之间的对应关系。

git remote

管理本地仓库对应的一组远程仓库,包括 查看、更新、添加、删除、重命名、设置 等一系列操作

该命令的主要工作是在维护配置文件,也就是维护 .git/config,通常当不记得命令的时候,可以直接修改配置文件,因为配置文件格式很简单,很容易记忆。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
git remote [-v | --verbose]
git remote [-v | --verbose] show [-n] <name>…​
git remote [-v | --verbose] update [-p | --prune] [(<group> | <remote>)…​]

git remote add [-t <branch>] [-m <master>] [-f] [--[no-]tags] [--mirror=<fetch|push>] <name> <url>
git remote remove <name>
git remote rename <old> <new>

git remote set-head <name> (-a | --auto | -d | --delete | <branch>)
git remote set-branches [--add] <name> <branch>…​

git remote get-url [--push] [--all] <name>
git remote set-url [--push] <name> <new-url> [<old-url>]
git remote set-url --add [--push] <name> <new-url>
git remote set-url --delete [--push] <name> <url>

git remote [-v | --verbose] show [-n] <name>…​

git remote prune [-n | --dry-run] <name>…​

git remote [-v | --verbose] update [-p | --prune] [(<group> | <remote>)…​]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
git remote                                                  # 列出已经存在的远程分支
git remote -v # 查看远程主机的地址
git remote show remote_name # 查看该远程主机的详细信息
git remote add remote_name remote_url # 添加远程主机
git remote remove remote_name # 删除远程主机
git remote rename remote_name new_remote_name # 重命名远程主机

git remote set-head remote_name branch_name --auto # 查询远程获得默认分支
git remote set-head remote_name branch_name --delete # 删除默认分支

git remote set-branches [--add] remote_name branch_name # 设置 RefSpec, [remote "remote_name"].fetch

git remote get-url remote_name # 查看远程主机地址 [remote "remote_name"].url
git remote set-url remote_name git://new.url.here # 设置远程主机地址
git remote set-url remote_name --push git://new.url.here # 修改远程主机地址
git remote set-url remote_name --add git://new.url.here # 修改远程主机地址
git remote set-url remote_name --delete git://new.url.here # 删除远程主机地址

git remote prune [-n | --dry-run] <remote_name>…​ # 删除某个远程名下过期(即不存在)的分支

# see http://stackoverflow.com/questions/1856499/differences-between-git-remote-update-and-fetch
git remote [-v | --verbose] update [-p | --prune] [(<group> | <remote>)…​]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ git remote show origin
* remote origin
Fetch URL: git@github.com:liuyanjie/knowledge.git
Push URL: git@github.com:liuyanjie/knowledge.git
HEAD branch: master
Remote branches:
feature/travis-ci tracked
master tracked
Local branches configured for 'git pull':
feature/travis-ci merges with remote feature/travis-ci
master merges with remote master
Local refs configured for 'git push':
feature/travis-ci pushes to feature/travis-ci (up to date)
master pushes to master (up to date)

git fetch

从另外一个仓库下载 Refs,以及完成他们的变更历史所需要的 Objects,追踪的远程分支将会被更新。

从一个或多个其他存储库中获取分支,以及完成它们的历史记录所需的对象,追踪的远程分支将会被更新(具体策略取决于 RefSpec)。

默认情况下,还会获取指向要获取分支的历史记录上的标签,效果是获取指向您感兴趣的分支的标签。分支和标签统称为 Refs。也可以改变这种行为。

git fetch 的主要工作就是和远程同步 Refs,而 Refs 可以 被 创建、修改、删除,所以 fetch 操作必然应该能够同步这些变化。

.git/FETCH_HEAD:是一个版本链接,记录在本地的一个文件中,指向着目前已经从远程仓库取下来的分支的末端版本。

1
2
3
4
5
$ cat .git/FETCH_HEAD                                
25f8a1026c24d8dee71a7ffd43310588d01c246f branch 'master' of github.com:liuyanjie/knowledge
0d572bc6b622355f930688af4f44ae8f3416e12b not-for-merge branch 'feature/travis-ci' of github.com:liuyanjie/knowledge
58a6618947d44720494860fbb77a6a22c9a30ddb not-for-merge branch 'feature/vpn' of github.com:liuyanjie/knowledge
c761eb7a69dc54260b88c271b6271df559e7bce0 not-for-merge branch 'php-lang' of github.com:liuyanjie/knowledge

执行过 fetch 操作的项目都会存在一个 FETCH_HEAD 文件,其中每一行对应于远程服务器的一个分支。当前分支指向的 FETCH_HEAD,就是这个文件第一行对应的那个分支。

从本质上来说,唯一能从服务器下拉取数据的只有 fetch,其他命令的下拉数据的操作都是基于 fetch 的,所以 fetch 必然需要能够尽可能处理所有下拉数据时可能出现的情况。

Options:

  • [shallow] 限制下拉指定的提交数:

    • --depth=<depth>
    • --deepen=<depth>
  • [shallow]限制下拉指定的提交时间:

    • --shallow-since=<date>
    • --shallow-exclude=<revision>
  • [deep]

    • --unshallowdeep clone
    • --update-shallow
  • [prune] 剪枝操作

    远程仓库可能对已有的分支标签进行删除,而本地仓库并未删除,需要同步删除操作

    • -p --prune
    • -p --prune-tags
  • [tags] 默认情况下,git fetch 会下拉 tag

    • -t --tags 【默认】下拉标签
    • -n --no-tags 不下拉标签
  • 子模块

    • --recurse-submodules-default=[yes|on-demand]
    • --recurse-submodules[=yes|on-demand|no]
    • --no-recurse-submodules
    • --submodule-prefix=<path>

再看对应关系:

local-branch@local remote-branch@local remote-branch@remote
master origin/master master
feature/travis-ci origin/feature/travis-ci feature/travis-ci
refs/heads/feature/travis-ci refs/remotes/origin/feature/travis-ci refs/heads/feature/travis-ci

git fetchremote-branch@remote fetch remote-branch@local,而 RefSpec(+refs/heads/*:refs/remotes/origin/*) 前面的 + 使 Git 在不能快速前进的情况下也强制更新,所以不会出现 remote-branch@remote --merge--> remote-branch@local 的情况,实际上合并是不合理的行为,因为本地的 refs/remotes/origin/* 就是与远程保持同步的,如果合并了,就不同步了,更重要的是,远程分支可能修改了分支历史,如果合并,修改前的内容又合并进版本库了,有可能还需要解决冲突,而之后的 remote-branch@local --merge--> local-branch@local 又会有可能合并。

1
2
3
4
5
6
7
8
9
10
11
12
git fetch                                         # 获取 所有远程仓库 上的所有分支,将其记录到 .git/FETCH_HEAD 文件中
git fetch -all # 获取 所有远程仓库 上的所有分支
git fetch remote # 获取 remote 上的所有分支
git fetch remote branch-name # 获取 remote 上的分支:branch-name
git fetch remote branch-name:local-branch-name # 获取 remote 上的分支:branch-name,并在本地创建对应分支
git fetch remote branch-name:local-branch-name -f # 获取 remote 上的分支:branch-name,并在本地创建对应分支,[强制]
git fetch -f | --force # 当使用 refspec(<branch>:<branch>) 时,跳过亲子关系检查,强制更新本地分支
git fetch -p | --prune # 获取所有远程分支并清除服务器上已删掉的分支
git fetch -t | --tags # 从远程获取数据时获取tags
git fetch -n | --no-tags # 从远程获取数据时去除tags
git fetch --progress --verbose # 显示进度及冗长日志
git fetch --dry-run # 显示做了什么,但是并不实际修改
1
2
git fetch --depth=3 --no-tags --progress origin +refs/heads/master:refs/remotes/origin/master  +refs/heads/release/*:refs/remotes/origin/release/*
git fetch --depth=3 --no-tags --progress git@github.com:liuyanjie/knowledge.git +refs/heads/master:refs/remotes/origin/master +refs/heads/release/*:refs/remotes/origin/release/*

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ git fetch --prune --progress --verbose --dry-run
From github.com:remote-name/branch-name
- [deleted] (none) -> origin/feature/abcd
- [deleted] (none) -> origin/feature/efg
remote: Counting objects: 34, done.
remote: Compressing objects: 100% (18/18), done.
remote: Total 34 (delta 18), reused 24 (delta 16), pack-reused 0
Unpacking objects: 100% (34/34), done.
f4e75b13a..6a338066c master -> origin/master
+ c29324269...641076244 develop -> origin/develop (forced update)
= [up to date] release/1.0.0 -> origin/release/1.0.0
* [new branch] release/1.1.0 -> origin/release/1.1.0
* [new tag] v1.1.0 -> v1.1.0

--prune 只能清理 .git/refs/remotes/remote-name 目录下的远程追踪分支,而不会删除 .git/refs/heads 下的本地分支,即使这些分支已经合并,这些分支的清理需要特定的命令。

清理本地已合并的分支:

1
2
git branch --merged | egrep -v "(^\*|master|develop|release)" # 查看确认
git branch --merged | egrep -v "(^\*|master|develop|release)" | xargs git branch -d
1
2
3
4
5
6
7
8
$ git branch --merged | egrep -v "(^\*|master|develop|release)" | xargs git branch -d
Deleted branch feature/auto-tag-ci (was 98147f0e3).
Deleted branch feature/build-optimize (was d359f4179).
Deleted branch feature/contract (was c0c4bdaa8).
Deleted branch feature/cross-domain (was 2e9b25c82).
Deleted branch feature/deploy (was 3650db271).
Deleted branch feature/nvmrc (was 1d174fcd8).
Deleted branch feature/winston-logstash (was f13700c66).

清理远程已合并的分支:

1
2
$ git branch -r --merged | egrep -v "(^\*|master|develop|release)" | sed 's/origin\//:/' # 查看确认
$ git branch -r --merged | egrep -v "(^\*|master|develop|release)" | sed 's/origin\//:/' | xargs -n 1 git push origin
1
2
3
$ git branch -r --merged | egrep -v "(^\*|master|develop|release)" | sed 's/origin\//:/' | xargs -n 1 git push origin
To github.com:liuyanjie/knowledge.git
- [deleted] feature/xxxx
1
2
3
4
$ git fetch origin master:refs/remotes/origin/master topic:refs/remotes/origin/topic
From git@github.com:schacon/simple
! [rejected] master -> origin/master (non fast forward)
* [new branch] topic -> origin/topic

在上面这个例子中, master 分支因为不是一个可以 快速演进 的引用而拉取操作被拒绝。你可以在 RefSpec 之前使用一个 + 号来重载这种行为。

输出格式:

1
<flag> <summary> <from> -> <to> [<reason>]

输出格式详细介绍见:OUTPUT

fetch 负责将 远程仓库 更新到 远程仓库在本地的对应部分,其他工作又其他 命令 负责。

在实际使用中,大多数时候都是使用 pull 间接的使用 fetch

git pull

将来自远程存储库的更改合并到当前分支中

1
git pull [options] [<repository> [<refspec>…​]]
1
git pull origin master  # 获取远程分支 master 并 merge 到当前分支

默认模式下,git pull 等价于以下两步:

1
2
git fetch
git merge FETCH_HEAD

特例:

1
2
3
git fetch
git checkout master
git merge origin/master

更确切的说,git pull 已指定的参数运行 git fetch,然后 调用 git merge 合并 检索到的分支头到当前分支,通过 --rebase 参数,git merge 也可以被替换成 git rebase

假定有如下的历史,并且当前分支是 master

1
2
3
4
5
6
7
              master on origin

A---B---C
/
D---E---F---G ← master

origin/master in your repository

调用 git pull 时,首先需要 fetch 变更从远处分支,下拉之后的仓库状态:

1
2
3
4
5
              master on origin

A---B---C ← origin/master in your repository
/
D---E---F---G ← master

因为 远程分支 master (C) 已经和 本地分支 master (G) 已经处于分离状态,此时,git merge 合并 origin/mastermaster

1
2
3
4
5
              master on origin

A---B---C ← origin/master
/ \
D---E---F---G---H ← master

以上过程发生了一次 远程 合并到 本地 的情形,git 会自动生成类似下面的 commit message

1
Merge branch 'master' of github.com:liuyanjie/knowledge into master

出现 远程 合并到 本地 的情形 在 Git 中是一种不良好的实践,应该极力避免甚至是禁止出现,这种情形在多个人同时在同一个分支上开发的时候非常容易出现。

记住一点:一般来书,分支是要合并到远程服务器上的分支,而不是远程服务分支合并到本地分支的。

在实际开发过程中,所有的合并操作都应该发生在远程服务器上,保持所有的分支有清晰的历史。同样,也应该避免不必要的合并,甚至是禁止合并。

一般情况下,创建了分支必然需要通过合并来将分支上的内容整合到分支的基上,但是也有不合并的其他方法

合并产生的 Commit 并未给版本库带来新的改变,但是却使版本历史不够清晰了。

合并使分支历史从单向链表变成了有向图,一堆线杂乱无章交错,分支历史难以理解。

合并产生的 Commit 有两个或多个父 CommitReset 难以进行。

如何避免 本地合并?

  1. commit 之前先 pull,避免分叉。
  2. commit 之后立即 push,使其他人的本地仓库能及时获取到最新的 commit

知道一定会 发生本地 合并时如何处理?

  1. git pull --ff-only or git fetch
  2. git rebase origin/master

已经出现 本地合并 如何解决?

  1. git reset C 重置当前分支到 CF G 会重新回到暂存区。
  2. git commit -am "commit message" 重新提交。
  3. git push

解决之后的分支图:

1
2
3
4
5
6
7
              master on origin

origin/master

A---B---C---F---G ← master
/
D---E

假设版本库当前的状态如下:

1
2
3
4
5
6
7
              master on origin

A---B---C
/
D---E ← master

origin/master in your repository

以上版本库库满足快速前进的条件,可以进行快速前进 --ff

1
2
3
4
5
              master on origin

A---B---C ← master
/ ↑
D---E origin/master in your repository

以上版本库满足快速前进的条件,可以进行快速前进 --ff

1
2
3
4
5
6
7
              master on origin

A---B---C
/
D---E ← master

origin/master in your repository

快速前进不产生新的 Commit,效果上只移动分支头即可,默认情况下进行就是快速前进

在能够进行快速前进的情况下,也可以强制进行合并,如下:

1
2
3
4
5
              master on origin

A---B---C ← origin/master
/ \
D---E-----------H ← master

所以 git pull 的参数主要由 git fetchgit merge 的参数组成。

git pull 的运行过程:

  1. 首先,基于本地的 FETCH_HEAD 记录,比对本地的 FETCH_HEAD 记录与远程仓库的版本号
  2. 然后通过 git fetch 获得当前指向的远程分支的后续版本的数据
  3. 最后通过 git merge 将其与本地的当前分支合并

若有多个 remote,git pull remote_name 所做的事情是:

  • 获取 [remote_name] 下的所有分支
  • 寻找本地分支有没有 tracking 这些分支的,若有则 merge 这些分支,若没有则 merge 当前分支

另外,若只有一个 remote,假设叫 origin,那么 git pull 等价于 git pull origin;平时养成好习惯,没谱的时候都把【来源】带上。

怎么知道 tracking 了没有?

  • 如果你曾经这么推过:git push -u origin master,那么你执行这条命令时所在的分支就已经 tracking to origin/master
  • 如果你记不清了:cat .git/config,由此可见,tracking 的本质就是指明 pullmerge 动作来源

总结:

  • git pull = git fetch + git merge
  • git fetch 拿到了远程所有分支的更新,cat .git/FETCH_HEAD 可以看到其状态,若是 not-for-merge 则不会有接下来的 merge 动作
  • merge 动作的默认目标是当前分支,若要切换目标,可以直接切换分支
  • merge 动作的来源则取决于你是否有 tracking,若有则读取配置自动完成,若无则请指明【来源】

pull 时还可能存在远程分支不存在的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ git checkout -b test
$ git pull
There is no tracking information for the current branch.
Please specify which branch you want to merge with.
See git-pull(1) for details.

git pull <remote> <branch>

If you wish to set tracking information for this branch you can do so with:

git branch --set-upstream-to=origin/<branch> test

$ git branch --set-upstream-to=origin/test test
error: the requested upstream branch 'origin/test' does not exist
hint:
hint: If you are planning on basing your work on an upstream
hint: branch that already exists at the remote, you may need to
hint: run "git fetch" to retrieve it.
hint:
hint: If you are planning to push out a new local branch that
hint: will track its remote counterpart, you may want to use
hint: "git push -u" to set the upstream config as you push.
1
2
3
4
5
6
7
8
9
$ git pull
remote: Counting objects: 81, done.
remote: Compressing objects: 100% (29/29), done.
remote: Total 81 (delta 42), reused 81 (delta 42), pack-reused 0
Unpacking objects: 100% (81/81), done.
From github.com:liuyanjie/knowledge
2f977e2..be00fff feature/x -> origin/feature/x
Your configuration specifies to merge with the ref 'refs/heads/feature/abc'
from the remote, but no such ref was fetched.

需要提及的一点是:

pull 操作,不应该涉及 合并变基 操作,即 pull 应该总是 快速前进 的。

再看对应关系:

head@local remote@local remote@remote
master origin/master master
feature/travis-ci origin/feature/travis-ci feature/travis-ci
refs/heads/feature/travis-ci refs/remotes/origin/feature/travis-ci refs/heads/feature/travis-ci

git pullgit fetch 的基础之上增加了 git merge,将 远程分支对应的本地分支 合并到 追踪的本地开发分支

git push

使用本地引用更新远程引用,同时发送完成给定引用所必需的对象。

git push 是与 git pull 相对应的推送操作,同样需要能够推送本地的多种情形的变更到远程仓库。git 向远程仓库推送的操作只有 push

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
git push
[--all | --mirror | --tags]
[--follow-tags]
[--atomic]
[-n | --dry-run]
[--receive-pack=<git-receive-pack>]
[--repo=<repository>]
[-f | --force]
[-d | --delete]
[--prune]
[-v | --verbose]
[-u | --set-upstream]
[--push-option=<string>]
[--[no-]signed|--sign=(true|false|if-asked)]
[--force-with-lease[=<ref-name>[:<expect>]]]
[--no-verify]
[<repository> [<refspec>…​]]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
git push                                 # 如果当前分支只有一个追踪分支,那么主机名都可以省略
git push origin HEAD # 将 当前 分支 推送 到远程 master 分支
git push origin master # 将 master 分支 推送 到远程 master 分支
git push origin master -u # 将 master 分支 推送 到远程 master 分支,并建立追踪关系
git push origin master --set-upstream # 同上
git push origin --all # 将所有本地分支都推送到origin主机
git push origin --force # 强制推送更新远程分支

git push origin :hotfix/xxxx # 删除远程仓库的 hotfix/xxxx 分支
git push origin :master # 删除远程仓库的 master 分支
git push origin --delete master # 删除远程仓库的 master 分支

git push origin --prune # 删除在本地没有对应分支的远程分支

git push --tags # 把所有tag推送到远程仓库

推送模式:

  • simple 模式: 不带任何参数的git push,默认只推送当前分支。2.0以上版本,默认此方式。
  • matching模式: 会推送所有有对应的远程分支的本地分支。
1
2
git config --global push.default matching
git config --global push.default simple
1
2
3
4
5
6
7
8
9
10
$ git push
Enumerating objects: 9, done.
Counting objects: 100% (9/9), done.
Delta compression using up to 8 threads.
Compressing objects: 100% (6/6), done.
Writing objects: 100% (6/6), 1.25 KiB | 1.25 MiB/s, done.
Total 6 (delta 2), reused 0 (delta 0)
remote: Resolving deltas: 100% (2/2), completed with 2 local objects.
To github.com:liuyanjie/knowledge.git
d26f671..e081fb3 master -> master
1
git push --delete ref...

推送代码到服务器与拉取代码到本地其实是相同的,所以服务代码推送到服务全之后,同样有可能出现需要合并的情况,如推送者本地仓库在没有 pull 后进行 commitpush,导致本地代码和远程服务器代码分叉,此时服务端也要面临合并问题,合并就有可能产生冲突,但是服务端没有解决冲突的能力,所以实质上服务端是禁止发生合并的,只能进行快速前进。当不能快速前进,服务端会返回错误给客户端,错误会提示先 pullpush。此时,pull 操作是一定会进行 merge 的,可能需要处理 merge,此时就需要处理前面提到的处理本地合并的问题了。

git submodule

初始化、更新或检查子模块

gitsubmodules - mounting one repository inside another

1
2
3
4
5
6
7
8
9
10
11
12
13
git submodule [--quiet] add [-b <branch>] [-f|--force] [--name <name>]
[--reference <repository>] [--depth <depth>] [--] <repository> [<path>]
git submodule [--quiet] status [--cached] [--recursive] [--] [<path>…​]
git submodule [--quiet] init [--] [<path>…​]
git submodule [--quiet] deinit [-f|--force] (--all|[--] <path>…​)
git submodule [--quiet] update [--init] [--remote] [-N|--no-fetch]
[--[no-]recommend-shallow] [-f|--force] [--rebase|--merge]
[--reference <repository>] [--depth <depth>] [--recursive]
[--jobs <n>] [--] [<path>…​]
git submodule [--quiet] summary [--cached|--files] [(-n|--summary-limit) <n>]
[commit] [--] [<path>…​]
git submodule [--quiet] foreach [--recursive] <command>
git submodule [--quiet] sync [--recursive] [--] [<path>…​]

添加

1
git submodule add -b master --name knowledge --reference=/Volumes/Data/Data/ws/knowledge -- git@github.com:liuyanjie/knowledge.git ./third_parts/knowledge
1
2
3
4
5
6
7
8
9
10
$ git status
On branch master
Your branch is ahead of 'origin/master' by 1 commit.
(use "git push" to publish your local commits)

Changes to be committed:
(use "git reset HEAD <file>..." to unstage)

new file: .gitmodules
new file: third_parts
1
2
3
4
5
$ git commit -m "..."
[master 83506db] ...
2 files changed, 5 insertions(+)
create mode 100644 .gitmodules
create mode 160000 third_parts
1
2
3
4
5
6
7
8
9
10
$ git push
Enumerating objects: 7, done.
Counting objects: 100% (7/7), done.
Delta compression using up to 8 threads.
Compressing objects: 100% (6/6), done.
Writing objects: 100% (6/6), 5.31 KiB | 5.31 MiB/s, done.
Total 6 (delta 1), reused 0 (delta 0)
remote: Resolving deltas: 100% (1/1), done.
To github.com:liuyanjie/about.git
53abb09..83506db master -> master

分支管理

Git 是一个分布式的结构,有本地版本库和远程版本库,便有了本地分支和远程分支的区别了。

本地分支和远程分支在 git push 的时候可以随意指定,交错对应,只要不出现版本从图即可。

git-branch

创建、修改、删除、查看、重命名、复制分支

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 创建分支
git branch (-t | --[no-]track) (-l | --[no-]create-reflog) [-f | --force] <branch-name> [<start-point>]

# 设置/修改上游分支
git branch [-u | --set-upstream-to=] <upstream> [<branch-name>]

# 查看分支
git branch -a --all
git branch -r
git branch --list <pattern>...
git branch --list --[no-]contains [<commit>]
git branch --list --[no-]merged

# 重置分支
git branch (-f --force) <branch-name> <start-point>

# 重命名分支
git branch (-m --move | -M) [<old-branch>] <new-branch>
git branch (-m --move) --force [<old-branch>] <new-branch>
git branch -M [<old-branch>] <new-branch>

# 复制分支
git branch (-c --copy) [<old-branch>] <new-branch>
git branch (-c --copy) --force [<old-branch>] <new-branch>
git branch -C [<old-branch>] <new-branch>

# 删除分支
# -r 可以同时删除远程追踪分支,但是只有在远程分支删除的情况下才有意义,否则会fetch回来
git branch (-d --delete) [-r] <branchname>…​
git branch (-d --delete) --force [-r] <branchname>…​
git branch -D [-r] <branchname>…​

# 编辑分支描述
git branch --edit-description [<branchname>]

git branch 只能操作本地仓库,无法直接操作远程仓库,操作远程仓库必须通过 git push

remotes/origin/* 下的分支删除:

1
2
git push --delete <branch-name>
git push :<branch-name>

以上命令在删除远程仓库的分支的同时,同步删除 remotes/origin/* 下的分支

1
git branch -d <remote-name>/<branch-name>

以上命令删除 remotes/origin/* 下的分支,但是远程分支并未删除,在 git fetch 后还会拉下来,所以这种删除无意义。

分支类型:

  • 远程分支(remote-branch),远程服务器上的分支,refs/heads/*@remote,是远程追踪分支上游分支
  • 远程追踪分支(remote-tracking branch),远程服务器对应在本地的分支,与远程分支存在追踪关系,可能是本地分支上游分支
  • 本地分支(local branch),refs/heads/*@local,可能与远程追踪分支存在追踪关系。

分支关系:

  • 追踪分支(tracking branch),能够主动追踪其他分支,自动跟随其他分支变化更新的分支。
  • 上游分支(upstream branch),被追踪的分支。

Checking out a local branch from a remote-tracking branch automatically creates what is called a “tracking branch” (and the branch it tracks is called an “upstream branch”).

只有把概念定义清楚,才能够进行准确的描述,要不然都可能带来理解上的偏差。

git-tag

创建、删除、查看、校验标签

1
2
3
4
5
6
7
git tag [-a | -s | -u <keyid>] [-f] [-m <msg> | -F <file>] [-e] <tagname> [<commit> | <object>]
git tag -d <tagname>…​
git tag [-n[<num>]] -l [--contains <commit>] [--no-contains <commit>]
[--points-at <object>] [--column[=<options>] | --no-column]
[--create-reflog] [--sort=<key>] [--format=<format>]
[--[no-]merged [<commit>]] [<pattern>…​]
git tag -v [--format=<format>] <tagname>…​
1
2
3
4
5
6
7
8
9
10
11
12
# 查看分支
git tag
git tag -l --list "v*"

# 创建分支
git tag -a v1.0.0 -m "tagging version 1.0.0"
git tag -a --force v1.0.0 -m "tagging version 1.0.0"
git tag -a v1.0.0 --file=<file>
git tag -a v1.0.0 <commit-id>

# 删除分支
git tag -d v1.0.0

与分支不同,git push 默认不推送标签到远程,所以需要主动推送标签:

1
git push --tags

同样,git tag 只能操作本地仓库,无法直接操作远程仓库,操作远程仓库必须通过 git push,通常也不会直接操作远程仓库。

1
2
git push --delete <tag-name>
git push --delete v1.0.0

清理 远程不能存在本地存在 的标签:

1
git tag -l | xargs git tag -d ; git fetch --tags

标签并不像分支那样,存在远程标签/本地标签等区分,所以也不存在本地标签与远程标签之间的对应关系,自然也就不需要维护对应关系。

git-checkout

  • 切换分支并检出内容到工作区,也可创建分支

检出已存在的分支

1
2
git checkout    <branch>
git checkout -b <branch> --track <remote>/<branch>

创建并检出分支

1
git checkout -b|-B <new_branch> [<start-point>]

检出tree-ish

1
git checkout [<tree-ish>] [--] <pathspec>…​

检出内容到本地的时候会发生什么?

  1. 本地是干净的,无任何修改
  2. 本地存在新增加的文件
  3. 本地存在修改后未提交的文件

Ref:DETACHED HEAD

HEAD 通常指向某一个分支,这一分支即是当前工作的分支。当 HEAD 不再指向分支的时候,仓库即处于 DETACHED HEAD 状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ git checkout ccdd28a
Note: checking out 'ccdd28a'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

git checkout -b <new-branch-name>

HEAD is now at ccdd28a git update

$ git status
HEAD detached at ccdd28a
nothing to commit, working tree clean

处于这种状态下的仓库,如果进行修改并且提交,就会很危险,因为没有任何分支指向新的提交,当 HEAD 切换到其他位置的时候,当前的修改就不容易找不到了。

如果需要基于此节点进行修改,需要先基于此节点创建分支。

git-merge

将两个或多个分支历史合并在一起

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
git merge
[-q --quiet]
[-v --verbose]
[--[no-]progress]
[--commit] [--no-commit]
[-e | --edit] [--no-edit]
[-ff] [--no-ff] [--ff-only]
[--log[=<n>]] [--no-log]
[-n] [--stat] [--no-stat]
[--[no-]squash]
[--[no-]signoff]
[-s <strategy>] [--strategy=<strategy>]
[-X <strategy-option>] [--strategy-option=<option>]
[-S[<keyid>]] --gpg-sign[=<keyid>]
[--[no-]verify-signatures]
[--[no-]summary]
[--[no-]allow-unrelated-histories]
[--[no-]rerere-autoupdate]
[-m <msg>]
[<commit>…​]

https://stackoverflow.com/questions/11646107/you-have-not-concluded-your-merge-merge-head-exists

# git merge --abort is equivalent to git reset --merge when MERGE_HEAD is present.
# 中断 merge,当发生冲突时,可以通过中断合并回到合并前的状态
git merge --abort

# 继续 merge,当发生冲突时,需要解决冲突,解决冲突后,继续执行合并
git merge --continue

有如下版本库:

1
2
3
      A---B---C topic
/
D---E---F---G master
1
git merge topic

合并后

1
2
3
      A---B---C topic
/ \
D---E---F---G---H master

squash mode

1
2
git merge --squash topic
git commit -m "message"
1
2
3
      A---B---C topic
/
D---E---F---G---(ABC) master

--squash 效果相当于将 topic 分支上的多个 commit A-B-C 合并成一个 ABC,放在当前分支上,原来的 commit 历史则没有拿过来。

判断是否使用 --squash 选项最根本的标准是,待合并分支上的历史是否有意义。版本历史记录的应该是代码的发展,而不是开发者在编码时的活动。

只有在开发分支上每个 commit 都有其独自存在的意义,并且能够编译通过的情况下,才应该选择缺省的合并方式来保留 commit 历史。

fast forward mode

1
2
3
              A---B---C topic
/
D---E---F---G master
1
git merge --ff topic
1
2
3
              A---B---C topic master
/
D---E---F---G

合并的前提是:准备合并的两个 commit 不在一条直线上,在一条直线上可以进行快速前进,也可以使用 --no-ff 强制合并(无意义)。

合并的过程中需要处理可能得冲突,未冲突的文件将会进行自动合并,在新版本的tree中产生一个新版本的blob,所以Git能够完整检出不需要依赖历史中的commit,只需要当前的commit

合并的结果是:产生一个新的 commit,实际上,squash mode fast forward mode 并不是真正意义上的合并。

冲突:

冲突有两种类型,一种是树冲突,修改/删除同一文件,另一种是文件冲突,修改了同一文件中的相同内容。

冲突是如何判断的?

1
2
3
      A---B---C topic
/
D---E---F---G master

假如有文件 README.mdE,且 topicmaster 都有修改此文件,合并 topicmaster 时,冲突检查的依据不是对比 README.md@topicREADME.md@master 是否相同,而是对比 README.md@topicREADME.md@master 相对于 E 的变化。即使是 README.md 文件在被修改后的内容是相同的,也会产生冲突。而冲突产生的文件,就是将 相对于 E,都合并到同一个文件中,并交由用户解决。

1
2
3
4
5
<<<<<<< yours:sample.txt
Git makes conflict resolution easy.
=======
Git makes conflict resolution easy.
>>>>>>> theirs:sample.txt

最佳实践:

  1. 尽量避免在本地使用 merge,也尽量避免在本地发生 Merge
  2. merge 时,本地不要有未提交的更改,这些修改可能会在中断合并时丢失。

基本操作

git add

添加文件到索引中,为下一次提交准备内容。

将工作区的修改添加到暂存区中,此命令使用在工作树中找到的最新内容更新索引,以准备为下次提交暂存内容。

典型的情况下,将整个文件添加到暂存区中,通过特定的选项,也可以将工作区修改的部分内容加到暂存区中。

暂存区保存工作树内容的快照,并将此快照作为下一次提交的内容。因此,在对工作树进行任何更改之后,在运行commit命令之前,必须使用add命令将任何新的或修改的文件添加到索引中。

默认情况下,git add 不会添加忽略的文件,git add -f 会进行强制添加。

git rm

从工作区和暂存区移除文件

1
git rm [-f | --force] [-n] [-r] [--cached] [--ignore-unmatch] [--quiet] [--] <file>…​
1
git rm *.txt

等价于:

1
2
rm *.txt
git add *.txt

仅从暂存区删除内容

1
git rm --cached *.txt

git mv

重命名或移动文件,同步更新暂存区

1
2
3
git mv <options>…​ <args>…​
git mv [-v] [-f] [-n] [-k] <source> <destination>
git mv [-v] [-f] [-n] [-k] <source> ... <destination directory>
1
2
3
4
5
git mv old_name new_name            # 重命名
git mv -f old_name new_name # 强制重命名,即时目标名称已经存在
git mv -k old_name new_name # 跳过会导致错误的动作
git mv -v old_name new_name # 报告被移动文件
git mv --dry-run old_name new_name # 只显示将会发生什么

git diff

Show changes between commits, commit and working tree, etc

1
2
3
4
5
git diff [options] [<commit>] [--] [<path>…​]
git diff [options] --cached [<commit>] [--] [<path>…​]
git diff [options] <commit> <commit> [--] [<path>…​]
git diff [options] <blob> <blob>
git diff [options] [--no-index] [--] <path> <path>
1
2
3
4
5
6
7
8
9
10
11
git diff                # 查看尚未暂存的文件更新了哪些部分,不加参数直接输入。
git diff --cached # 查看已经暂存起来的文件(staged)和上次提交时的快照之间(HEAD)的差异
git diff --staged # 显示的是下一次 commit 时会提交到HEAD的内容(不带-a情况下)
git diff HEAD # 显示工作版本(Working tree)和HEAD的差别
git diff topic master # 直接将两个分支上最新的提交做diff
git diff topic...master # 输出自 topic 和 master 分别开发以来,master 分支上的 changed。
git diff --stat # 查看简单的diff结果,可以加上--stat参数
git diff test # 查看当前目录和另外一个分支的差别 显示当前目录和另一个叫 test 分支的差别
git diff HEAD -- ./lib # 显示当前目录下的lib目录和上次提交之间的差别(更准确的说是在当前分支下)
git diff HEAD^ HEAD # 比较上次提交commit和上上次提交
git diff SHA1 SHA2 # 比较两个历史版本之间的差异

Diff_utility

git commit

Record changes to the repository

1
2
3
4
5
6
7
8
git commit                          # 提交的是暂存区里面的内容,也就是 Changes to be committed 中的文件。
git commit -a # 除了将暂存区里的文件提交外,还提交 Changes bu not updated 中的文件。
git commit -a -m 'commit info' # 注释,如果没有 -m,会默认会使用vi编辑注释。
git commit -am "This is a commit" # 同上,合并提交,将 add 和 commit 合为一步
git commit --amend # 对上一次提交进行修改,合并上一次提交(用于反复修改)
git commit --amend -a # 提交时忘记使用 -a 选项,导致 Changes bu not updated 中的内容没有被提交
git commit --author=<author> # 设置作者,与提交者分开
git commit --file=<file> # 注释从文件中读取

对于 commit 来说,最重要的是,每一次 commit 都应该是一个完整的提交,而且应该有个规范清晰的注释。

Commit message 和 Change log 编写指南

git status

显示工作树状态

1
git status [<options>…​] [--] [<pathspec>…​]
1
2
3
git status     # 显示状态
git status -s # 显示简短信息
git status -b # 显示分支状态

git reset

重置工作区,将当前分支回退到某一节点

1
2
3
git reset [-q] [<tree-ish>] [--] <paths>…​
git reset (--patch | -p) [<tree-ish>] [--] [<paths>…​]
git reset [--soft | --mixed [-N] | --hard | --merge | --keep] [-q] [<commit>]

git reset 会修改当前分支头从某一个 <commit-id> 移动到另外的一个指定的 <commit-id>

如:

1
2
3
4
5
6
7
              HEAD

topic

A---B---C
/
D---E---F---G master

当前活跃的分支是 topic

1
git reset A

执行以上操作后:

1
2
3
4
5
6
7
      HEAD

topic

A---B---C
/
D---E---F---G master

此时,HEAD -> topic -> A,B、C 此时处于悬挂状态,如同普通的对象一样,没有任何引用后,会被 Git GC 回收。

执行此操作后,B、C 两点虽然依然存在于仓库中,但是它们已经逻辑上脱离了Git。

此时,B、C 两点提交的内容怎么办?是不是就丢失了呢?

Git 给了我们多种选择:

  • –soft,B、C 提交的内容不会回到工作区和暂存区。因为当前工作区内容是基于 C 修改的,所以实际上并无内容丢失。
  • –mixed,B、C 提交的内容回到暂存区,但是工作区内容不变,也就是某些文件处于 修改未提交状态。同上,也无内容丢失。
  • –hard,B、C 提交的内容不会回到工作区和暂存区,同时暂存区和工作区回到A点状态,B、C 提交的内容以及工作区后续的修改全部丢失。

如果 reset 误操作操作怎么办?

  1. 如果存在上游分支,可以通过上游分支恢复

    1
    2
    git reset master^2
    git reset origin/master
  2. 可以通过 reflog 恢复

    reflog 记录 HEAD 的变化,所以可以通过 reflog 找到 reset 之前的 HEAD 的位置,但是前提是后续节点未被垃圾回收。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    git reflog
    58c1d5d (HEAD -> master, origin/master) HEAD@{0}: commit: update git
    ccdd28a (test2, test1, test) HEAD@{1}: checkout: moving from test to master
    ccdd28a (test2, test1, test) HEAD@{2}: checkout: moving from master to test
    ccdd28a (test2, test1, test) HEAD@{3}: commit: git update
    e081fb3 HEAD@{4}: commit: update python
    d26f671 HEAD@{5}: commit: update
    33db13f HEAD@{6}: commit (amend): update and format
    5c41033 HEAD@{7}: commit: update and format
    e56ec4e HEAD@{8}: commit: 移除乱码字符
    6595b95 HEAD@{9}: commit: feat(): add hexo.yaml

未提交的内容是很难就行恢复的,所有在进行 reset 操作时,要将工作区的内容提交。

reset 除了将工作区回退到某个节点之外,常用的应用就是将后续的多个提交合并为一个提交,因为后续提交的内容可以回到暂存区或工作区中。

在某些Git工作流中,要求将多个提交合并成一个之后才能合并到上游分支。

git revert

1
2
3
4
git revert [--[no-]edit] [-n] [-m parent-number] [-s] [-S[<keyid>]] <commit>…​
git revert --continue
git revert --quit
git revert --abort
1
2
git revert HEAD~3
git revert -n master~5..master~2

git revert 用于撤销一个或多个提交,并建立一个新的提交。commit 中所做的修改都会被移除掉,相当于 commit 反向操作。

git revert 通常用户快速回滚。

示例如下:

1
2
3
4
5
            HEAD

master

A---B---C---D
1
git revert C
1
2
3
4
5
                HEAD

master

A---B---C---D---C'

C' 是一个全新的 CommitC 是不同的,但是这种情况下,C'C 中的 tree 是相同的。

git rebase

变基操作,基指的是起始提交,即参数中常见的

1
2
3
4
5
6
7
8
9
10
11
12
git rebase [-i | --interactive] [<options>] [--exec <cmd>] [--onto <newbase>] [<upstream> [<branch>]]
git rebase [-i | --interactive] [<options>] [--exec <cmd>] [--onto <newbase>] --root [<branch>]
git rebase --continue | --skip | --abort | --quit | --edit-todo | --show-current-patch

# 解决冲突之后继续 rebase
git rebase --continue

# 跳过
git rebase --skip

# 中断 rebase
git rebase --abort

示例:

1
2
3
      A---B---C ← topic
/
D---E---F---G ← master
1
git rebase master
1
2
3
              A'--B'--C' ← topic
/
D---E---F---G ← master

以上,通过变基操作,将topic分支的 E 调整到了 G

变基操作的原理:将 A B C 基于 G 重新提交,提交的过程可能与 F G 存在冲突,需要解决冲突。

变基操作的应用:

  1. 保持与上游分支同步,同步上游分支的最新版本
  2. 合并时存在冲突,通过变基操作解决冲突

git cherry-pick


查看源文件&nbsp;&nbsp;编辑源文件

Git是一个非常强大的版本管理工具,有非常多的概念和命令,也有非常多且复杂的用法。但是其底层模型相对而言却非常简单,了解底层对象模型将非常有助于理解上层命令到底对仓库做了什么。如果想要精通Git,了解底层对象模型也是必不可少的。本文将会通过基本的命令操作,分析每一步操作对仓库数据做了哪些改动,进而分析出Git底层对象模型。

Git基本概念

SHA

所有用来表示项目历史信息的文件,都是通过一个40个字符的(40-digit)“对象名”来索引的,对象名看起来像这样:

6ff87c4664981e4397625791c8ea3bbb5f2279a3

在后边使用对象名时,只使用前面几个字符即可,但是最少需要4个。

你会在Git里到处看到这种“40个字符”字符串。每一个“对象名”都是对“对象”内容做SHA1哈希计算得来的。这样就意味着两个不同内容的对象几乎不可能(理论上是可能发生碰撞的)有相同的“对象名”。

这样做会有几个好处:

  • 只要比较对象名,就可以很快的判断两个对象是否相同。

    因为在每个仓库(repository)的“对象名”的计算方法都完全一样,如果同样的内容存在两个不同的仓库中,就会存在相同的“对象名”下。

  • 还可以通过检查对象内容的SHA1的哈希值和“对象名”是否相同,来判断对象内容是否正确。

对象

每个对象(object) 包括三个部分:类型、大小和内容。大小就是指内容的大小,内容取决于对象的类型。

有四种类型的对象:"blob""tree""commit""tag"

几乎所有的Git功能都是使用这四个简单的对象类型来完成的。它就像是在你本机的文件系统之上构建一个小的文件系统。

blob (文件) 对象

blob 用来存储文件数据,通常是一个文件。

blob

tree (目录) 对象

tree 像一个目录,管理 tree(子目录)blob(文件)

tree

commit (提交) 对象

一个 commit 只指向一个 tree,它用来标记项目某一个特定时间点的状态。commit 保存了树根的 对象名

它包括一些关于时间点的元数据,如 时间戳最近一次提交的作者指向上次提交(commits)的指针 等等。

tree

1
2
3
4
5
6
7
$ git cat-file -p 830f4857e9f579818c5e69104d3e2cc30f1f0d0d
tree f25061fa4f8b3bffbf8ebcc3ab2351efdad2f605
parent 06e13701ac86eb09c2035329a5e1c18f95898cf2
author liuyanjie <x@gmail.com> 1526828841 +0800
committer liuyanjie <x@gmail.com> 1526828841 +0800

commit message

tag (标签) 对象

tag

一个 tag 是来标记某一个 commit 的方法。

实际上 tag 本身是文件名,内容是 commit 的对象名。tagcommit 的别名,类似于域名和ip地址的关系。

1
2
$ cat .git/refs/tags/v0.0.0
830f4857e9f579818c5e69104d3e2cc30f1f0d0d

commit -> tree -> blob

object-c-t-b

从图上可以看出:一个 commit 指向了一棵由 treeblob 构成的 Git 对象树。

与其他版本控制系统的区别

Git与你熟悉的大部分版本控制系统的差别是很大的。

也许你熟悉 SubversionCVSPerforceMercurial 等等,他们使用 “增量文件系统” (Delta Storage systems), 就是说它们存储每次提交(commit)之间的差异。

Git正好与之相反,它会把你的每次提交的文件的全部内容(snapshot)都会记录下来。

这会是在使用Git时的一个很重要的理念。

综上,Git对象模型非常简单,与普通的文件系统非常的相似。

一步一步了解Git在做什么

初始化仓库

初始化目录并创建一个Git仓库

1
2
3
4
5
$ cd git-obj-model

$ git init
Initialized empty Git repository in /Users/liuyanjie/git-obj-model/.git/

看一下初始化的git仓库中到底有些什么内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ tree .git
.git
├── HEAD
├── config
├── description
├── hooks
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── fsmonitor-watchman.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   ├── pre-receive.sample
│   ├── prepare-commit-msg.sample
│   └── update.sample
├── info
│   └── exclude
├── objects
│   ├── info
│   └── pack
└── refs
├── heads
└── tags

8 directories, 15 files
1
2
3
4
5
6
7
8
$ cat .git/config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
1
2
$ cat .git/HEAD
ref: refs/heads/master

查看仓库状态

1
2
3
4
5
6
7
$ git status
On branch master

No commits yet

nothing to commit (create/copy files and use "git add" to track)

后面忽略 .git/hooks/ 下的文件。

增加一个文件README.md

现在创建一个 README.md,查看 .git 目录,发现没有什么变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ echo "# Readme" > README.md

$ tree .git
.git
├── HEAD
├── config
├── description
├── info
│   └── exclude
├── objects
│   ├── info
│   └── pack
└── refs
├── heads
└── tags

8 directories, 15 files

查看当前状态,可以看到,此时有一个未追踪的 README.md 文件,此文件存在于 工作区 中。

1
2
3
4
5
6
7
8
9
10
11
12
$ git status
On branch master

No commits yet

Untracked files:
(use "git add <file>..." to include in what will be committed)

README.md

nothing added to commit but untracked files present (use "git add" to track)

执行 git add 命令后,可以看到增加了两个文件:

  • .git/index
  • .git/objects/f3/954314c1026028e77ea3a765aadefa67b45195

git add 把文件暂存到索引中为下一次提交做准备,git commit 创建新的提交。

应该知道,这应该是一个 blob 类型的文件,里面存储文件内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ git add README.md

$ tree .git
.git
├── HEAD
├── config
├── description
├── index
├── info
│   └── exclude
├── objects
│   ├── f3
│   │   └── 954314c1026028e77ea3a765aadefa67b45195
│   ├── info
│   └── pack
└── refs
├── heads
└── tags

9 directories, 17 files

查看当前状态

1
2
3
4
5
6
7
8
9
10
$ git status
On branch master

No commits yet

Changes to be committed:
(use "git rm --cached <file>..." to unstage)

new file: README.md

查看对象内容

1
2
3
$ git cat-file -p f39543
# Readme

查看暂存区

1
2
3
$ git ls-files --stage
100644 f3954314c1026028e77ea3a765aadefa67b45195 0 README.md

.git/index 是 Git索引文件,是一个在 工作区仓库 间的 暂存区域(staging area)

索引是一个二进制格式的文件,里面存放了与当前暂存内容相关的信息,包括暂存的文件名文件内容的SHA1哈希串值文件访问权限,整个索引文件的内容以暂存的文件名进行排git ls-files –stage序保存的。

因为这个文件记录了将要提交的文件, 所以我们才能够多次修改一起提交(commit)。

所以创建了一个新的提交(commit),提交的一般是暂存区里的内容, 而不是工作目录中的内容。

一个Git项目中文件的状态大概分成下面的两大类,而第二大类又分为三小类:

  1. 未被跟踪的文件(Untracked files)
  2. 已被跟踪的文件(Tracked files)
    1. 被修改 未暂存 的文件(Changed but not updated 或 Modified)
    2. 被修改 已暂存 可以 被提交 的文件(Changes to be committed 或 Staged)
    3. 未修改 的文件(自上次提交以来)(Clean 或 Unmodified)
1
2
3
4
5
6
Changes to be committed:
(no files)
Changes not staged for commit:
(no files)
Untracked files:
? .gitignore

执行 git commit 命令,发现路径下又增加了两个文件:

  • .git/objects/9d/978d59f2f22062c0382c859f4c3ef929026303
  • .git/objects/c1/067ba0a7ba51f937518c9bc051ea744ca748fe

从上面的结构图,可以想到:

提交可定会产生一个提交对象,提交对象指向一个树对象,树对象包含上一步添加的blob对象。

下面将通过查看文件内容验证这一点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
$ git commit -m "First Commmit" README.md

[master (root-commit) 033fa1a] First Commmit
1 file changed, 1 insertion(+)
create mode 100644 README.md

$ tree .git
.git
├── COMMIT_EDITMSG
├── HEAD
├── config
├── description
├── index
├── info
│   └── exclude
├── logs
│   ├── HEAD
│   └── refs
│   └── heads
│   └── master
├── objects
│   ├── 03
│   │   └── 3fa1ab71f0d54f348f07a3a0ffcefd52804df5 +
│   ├── c1
│   │   └── 067ba0a7ba51f937518c9bc051ea744ca748fe +
│   ├── f3
│   │   └── 954314c1026028e77ea3a765aadefa67b45195
│   ├── info
│   └── pack
└── refs
├── heads
│   └── master
└── tags

14 directories, 23 files

查看 033f 对象内容,此时不知道这个文件类型及内容是什么

此时,commit 对象为初始提交,所以并无 parent 引用。

1
2
3
4
5
6
7
$ git cat-file -p 033f
tree c1067ba0a7ba51f937518c9bc051ea744ca748fe
author liuyanjie <x@gmail.com> 1527521894 +0800
committer liuyanjie <x@gmail.com> 1527521894 +0800

First Commmit

查看 c106 对象内容,通过上一步,已知此对象是一个 tree 类型的对象,通过内容可以看到,树类型的对象实际就是存储每行一条数据的列表。

1
2
3
$ git cat-file -p c106
100644 blob f3954314c1026028e77ea3a765aadefa67b45195 README.md

再看 .git/refs/heads/master 文件内容

文件内容只有一行,内容是 033f 对象。master 即是 master 分支的物理表示。

1
2
3
$ cat .git/refs/heads/master
033fa1ab71f0d54f348f07a3a0ffcefd52804df5

同时,还可看到,新增了 .git/logs 目录及内容

该目录下保存了各个分支的提交记录

1
2
3
4
5
6
$ cat .git/logs/refs/heads/master
0000000000000000000000000000000000000000 033fa1ab71f0d54f348f07a3a0ffcefd52804df5 liuyanjie <x@gmail.com> 1527521894 +0800 commit (initial): First Commmit

$ cat .git/logs/HEAD
0000000000000000000000000000000000000000 033fa1ab71f0d54f348f07a3a0ffcefd52804df5 liuyanjie <x@gmail.com> 1527521894 +0800 commit (initial): First Commmit

Tag

1
2
$ git tag v0.0.1 -m 'tag v0.0.1'

查看目录文件变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
$ tree .git
.git
├── COMMIT_EDITMSG
├── HEAD
├── config
├── description
├── index
├── info
│   └── exclude
├── logs
│   ├── HEAD
│   └── refs
│   └── heads
│   └── master
├── objects
│   ├── 03
│   │   └── 3fa1ab71f0d54f348f07a3a0ffcefd52804df5
│   ├── c1
│   │   └── 067ba0a7ba51f937518c9bc051ea744ca748fe
│   ├── cc
│   │   └── f88a6f1649213499841c33f9bb36d1d8756fb7
│   ├── f3
│   │   └── 954314c1026028e77ea3a765aadefa67b45195
│   ├── info
│   └── pack
└── refs
├── heads
│   └── master
└── tags
└── v0.0.1

15 directories, 25 files

查看 .git/refs/tags/v0.0.1 文件内容,可以看到内容恰好是新增的 objects 对象名,所以可想而知 ccf8 是一个 tag 类型的对象

1
2
3
$ cat .git/refs/tags/v0.0.1
ccf88a6f1649213499841c33f9bb36d1d8756fb7

查看新增的 ccf8 文件内容,文件指向了 commit 对象 033f

1
2
3
4
5
6
7
8
$ git cat-file -p ccf8
object 033fa1ab71f0d54f348f07a3a0ffcefd52804df5
type commit
tag v0.0.1
tagger liuyanjie <x@gmail.com> 1527523284 +0800

tag v0.0.1

通过以上查看 .git 目录的变化过程,可以大致分析出Git是如何存储这些文件内容的。

上面只执行了 git addgit commit 两条操作。

继续添加文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ echo "# CHANGELOG" > CHANGELOG.md

$ echo "# CONTRIBUTING" > CONTRIBUTING.md

$ git status
On branch master
Untracked files:
(use "git add <file>..." to include in what will be committed)

CHANGELOG.md
CONTRIBUTING.md

nothing added to commit but untracked files present (use "git add" to track)

$ git add CHANGELOG.md CONTRIBUTING.md

$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)

new file: CHANGELOG.md
new file: CONTRIBUTING.md

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
$ tree .git
.git
├── COMMIT_EDITMSG
├── HEAD
├── config
├── description
├── index
├── info
│   └── exclude
├── logs
│   ├── HEAD
│   └── refs
│   └── heads
│   └── master
├── objects
│   ├── 03
│   │   └── 3fa1ab71f0d54f348f07a3a0ffcefd52804df5
│   ├── a0
│   │   └── cf709bc0991b5340080f944d02894dc1596d46
│   ├── c1
│   │   └── 067ba0a7ba51f937518c9bc051ea744ca748fe
│   ├── c6
│   │   └── b9e95b39b8cd8ead8bbf4b118104741017de1b
│   ├── cc
│   │   └── f88a6f1649213499841c33f9bb36d1d8756fb7
│   ├── f3
│   │   └── 954314c1026028e77ea3a765aadefa67b45195
│   ├── info
│   └── pack
└── refs
├── heads
│   └── master
└── tags
└── v0.0.1

17 directories, 27 files

1
2
3
4
5
$ git ls-files --stage
100644 a0cf709bc0991b5340080f944d02894dc1596d46 0 CHANGELOG.md
100644 c6b9e95b39b8cd8ead8bbf4b118104741017de1b 0 CONTRIBUTING.md
100644 f3954314c1026028e77ea3a765aadefa67b45195 0 README.md

分别查看一下各个文件的内容

1
2
3
4
5
6
7
8
9
$ git cat-file -p a0cf
# CHANGELOG

$ git cat-file -p c6b9
# CONTRIBUTING

$ git cat-file -p f395
# Readme

再查看一下树对象的内容,然而并没有任何变化

1
2
3
$ git cat-file -p c106
100644 blob f3954314c1026028e77ea3a765aadefa67b45195 README.md

下面提交这两个文件,可以通过日志方便的查看信息。

1
2
3
4
5
6
$ git commit --all -m "add CHANGELOG.md and CONTRIBUTING.md files"
[master 28baf4f] add CHANGELOG.md and CONTRIBUTING.md files
2 files changed, 2 insertions(+)
create mode 100644 CHANGELOG.md
create mode 100644 CONTRIBUTING.md

同样再看一下.git目录下的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
$ tree .git
.git
├── COMMIT_EDITMSG
├── HEAD
├── config
├── description
├── index
├── info
│   └── exclude
├── logs
│   ├── HEAD
│   └── refs
│   └── heads
│   └── master
├── objects
│   ├── 03
│   │   └── 3fa1ab71f0d54f348f07a3a0ffcefd52804df5
│   ├── 28
│   │   └── baf4f77fb49abf99c18bc1c12363d898f3ced7
│   ├── 2c
│   │   └── affd90cd736e58f516e3988e3af84f5fa42b4f
│   ├── a0
│   │   └── cf709bc0991b5340080f944d02894dc1596d46
│   ├── c1
│   │   └── 067ba0a7ba51f937518c9bc051ea744ca748fe
│   ├── c6
│   │   └── b9e95b39b8cd8ead8bbf4b118104741017de1b
│   ├── cc
│   │   └── f88a6f1649213499841c33f9bb36d1d8756fb7
│   ├── f3
│   │   └── 954314c1026028e77ea3a765aadefa67b45195
│   ├── info
│   └── pack
└── refs
├── heads
│   └── master
└── tags
└── v0.0.1

19 directories, 29 files

查看一下master上的提交日志,刚刚提交内容在新的一行,与首次提交稍微有点差别,首次提交是commit (initial)。

还可以发现第二次提交的第一列和第一次提交的第二列一样,可以猜到,第一列指向上一次的提交对象。

1
2
3
4
$ cat .git/logs/refs/heads/master
0000000000000000000000000000000000000000 033fa1ab71f0d54f348f07a3a0ffcefd52804df5 liuyanjie <x@gmail.com> 1527521894 +0800 commit (initial): First Commmit
033fa1ab71f0d54f348f07a3a0ffcefd52804df5 28baf4f77fb49abf99c18bc1c12363d898f3ced7 liuyanjie <x@gmail.com> 1527524452 +0800 commit: add CHANGELOG.md and CONTRIBUTING.md files

查看 28ba

1
2
3
4
5
6
7
8
$ git cat-file -p 28ba
tree 2caffd90cd736e58f516e3988e3af84f5fa42b4f
parent 033fa1ab71f0d54f348f07a3a0ffcefd52804df5
author liuyanjie <x@gmail.com> 1527524452 +0800
committer liuyanjie <x@gmail.com> 1527524452 +0800

add CHANGELOG.md and CONTRIBUTING.md files

相比 033f28ba 多了 parent 字段 且 parent 字段值是 033f

同时 master 也指向了新的提交对象。

第一次产生的提交对象同样存在于目录当中。

1
2
3
$ cat .git/refs/heads/master
28baf4f77fb49abf99c18bc1c12363d898f3ced7

在看树对象2caf,发现相比之前,多了两行,分别指向新增加的文件。

README.md 文件出现在了 2caf 对象中,实际上,它同时还存在于 c106 对象中。

1
2
3
4
$ git cat-file -p 2caf
100644 blob a0cf709bc0991b5340080f944d02894dc1596d46 CHANGELOG.md
100644 blob c6b9e95b39b8cd8ead8bbf4b118104741017de1b CONTRIBUTING.md
100644 blob f3954314c1026028e77ea3a765aadefa67b45195 README.md
1
2
3
$ git cat-file -p c106
100644 blob f3954314c1026028e77ea3a765aadefa67b45195 README.md

到现在为止,整个目录下有3个文件,而 .git 目录下已经有多个文件,为了跟踪记录版本。

1
2
3
$ ls
CHANGELOG.md CONTRIBUTING.md README.md

通过以上 .git 目录变化,可以发现:

每次提交都会产出一 commit 对象,这些 commit 通过 parent,形成一个由高版本到低版本的链表,追溯这个链表,可以回溯到任意版本。

commit 对象保存一个 tree 的根节点,根节点下面再包含 blobtree,类似普通文件系统结构,和上面的图一致,从根节点开始,可以找到某一般版本下的所有文件。

在每一颗树下,因为都是使用类似指针的结构,所以每次修改都是将变化的文件,重新创建一个blob文件,并修改相应指针。

目录中的 .git/refs/heads/master 指向某一次提交,当由另外一个分支的时候,会有 .git/refs/heads/branch-xxx 文件指向另外一次提交,而初始时与父分支指向相同。

再添加一个带有目录文件

1
2
3
4
5
6
7
8
9
10
11
$ mkdir lib

$ echo "// Author: liuyanjie" > ./lib/index.js

$ git add lib/index.js

$ git commit -m "add lib/index.js" lib/index.js
[master 2b9fc85] add lib/index.js
1 file changed, 1 insertion(+)
create mode 100644 lib/index.js

1
2
3
4
5
6
7
8
$ git cat-file -p 2b9fc85
tree 9609811e44367d44f2915435f4454716e1e535fd
parent 28baf4f77fb49abf99c18bc1c12363d898f3ced7
author liuyanjie <x@gmail.com> 1527556745 +0800
committer liuyanjie <x@gmail.com> 1527556745 +0800

add lib/index.js

1
2
3
4
5
$ git cat-file -p 9609
100644 blob a0cf709bc0991b5340080f944d02894dc1596d46 CHANGELOG.md
100644 blob c6b9e95b39b8cd8ead8bbf4b118104741017de1b CONTRIBUTING.md
100644 blob f3954314c1026028e77ea3a765aadefa67b45195 README.md
040000 tree 2fb9045bb558889ea2bd8cc5d8fe45e7247706da lib
1
2
3
$ git cat-file -p 2fb9
100644 blob 39a204af28de9b4f0411735e597e0da7416ca35a index.js

上面连续几步和之前的效果一样,但是可以看到,在树对象 9609 中,包含了另一个树对象 2fb9 ,这个对象的内容指向index.js文件。

1
2
3
4
5
$ cat .git/logs/refs/heads/master
0000000000000000000000000000000000000000 033fa1ab71f0d54f348f07a3a0ffcefd52804df5 liuyanjie <x@gmail.com> 1527521894 +0800 commit (initial): First Commmit
033fa1ab71f0d54f348f07a3a0ffcefd52804df5 28baf4f77fb49abf99c18bc1c12363d898f3ced7 liuyanjie <x@gmail.com> 1527524452 +0800 commit: add CHANGELOG.md and CONTRIBUTING.md files
28baf4f77fb49abf99c18bc1c12363d898f3ced7 2b9fc8524bac21d5d5c2f988b5793315ce93abc6 liuyanjie <x@gmail.com> 1527556745 +0800 commit: add lib/index.js

git log

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
commit 2b9fc8524bac21d5d5c2f988b5793315ce93abc6 (HEAD -> master)
Author: liuyanjie <x@gmail.com>
Date: Tue May 29 09:19:05 2018 +0800

add lib/index.js

commit 28baf4f77fb49abf99c18bc1c12363d898f3ced7
Author: liuyanjie <x@gmail.com>
Date: Tue May 29 00:20:52 2018 +0800

add CHANGELOG.md and CONTRIBUTING.md files

commit 033fa1ab71f0d54f348f07a3a0ffcefd52804df5 (tag: v0.0.1)
Author: liuyanjie <x@gmail.com>
Date: Mon May 28 23:38:14 2018 +0800

First Commmit

开始创建分支

创建并切换到分支 feature-a

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ git branch -v

* master 2b9fc85 add lib/index.js

$ git checkout -b feature-a
Switched to a new branch 'feature-a'

# liuyanjie @ bmw in ~/git-obj-model on git:feature-a o [9:25:01]
$ git branch -v

* feature-a 2b9fc85 add lib/index.js
master 2b9fc85 add lib/index.js

$ git status
On branch feature-a
nothing to commit, working tree clean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
$ tree .git
.git
├── COMMIT_EDITMSG
├── HEAD
├── config
├── description
├── index
├── info
│   └── exclude
├── logs
│   ├── HEAD
│   └── refs
│   └── heads
│   ├── feature-a
│   └── master
├── objects
│   ├── 03
│   │   └── 3fa1ab71f0d54f348f07a3a0ffcefd52804df5
│   ├── 28
│   │   └── baf4f77fb49abf99c18bc1c12363d898f3ced7
│   ├── 2b
│   │   └── 9fc8524bac21d5d5c2f988b5793315ce93abc6
│   ├── 2c
│   │   └── affd90cd736e58f516e3988e3af84f5fa42b4f
│   ├── 2f
│   │   └── b9045bb558889ea2bd8cc5d8fe45e7247706da
│   ├── 39
│   │   └── a204af28de9b4f0411735e597e0da7416ca35a
│   ├── 96
│   │   └── 09811e44367d44f2915435f4454716e1e535fd
│   ├── a0
│   │   └── cf709bc0991b5340080f944d02894dc1596d46
│   ├── c1
│   │   └── 067ba0a7ba51f937518c9bc051ea744ca748fe
│   ├── c6
│   │   └── b9e95b39b8cd8ead8bbf4b118104741017de1b
│   ├── cc
│   │   └── f88a6f1649213499841c33f9bb36d1d8756fb7
│   ├── f3
│   │   └── 954314c1026028e77ea3a765aadefa67b45195
│   ├── info
│   └── pack
└── refs
├── heads
│   ├── feature-a
│   └── master
└── tags
└── v0.0.1

23 directories, 35 files

看一下.git文件内容,可以看到.git/refs/heads目录下多了个feature-a

查看一下 feature-a 的相关内容,指向的提交对象和 master 一样,并指明 Created from HEAD

从内容中可以看出,两个分支指向同一个提交对象 1ad0e5,但是两个分支的日志不同。

日志记录了分支的历史,而提交对象记录了分支的数据内容和所有分支的历史。

1
2
3
4
5
$ cat .git/refs/heads/feature-a
2b9fc8524bac21d5d5c2f988b5793315ce93abc6

$ cat .git/refs/heads/master
2b9fc8524bac21d5d5c2f988b5793315ce93abc6
1
2
3
4
5
6
7
$ cat .git/logs/refs/heads/feature-a
0000000000000000000000000000000000000000 2b9fc8524bac21d5d5c2f988b5793315ce93abc6 liuyanjie <x@gmail.com> 1527557101 +0800 branch: Created from HEAD

$ cat .git/logs/refs/heads/master
0000000000000000000000000000000000000000 033fa1ab71f0d54f348f07a3a0ffcefd52804df5 liuyanjie <x@gmail.com> 1527521894 +0800 commit (initial): First Commmit
033fa1ab71f0d54f348f07a3a0ffcefd52804df5 28baf4f77fb49abf99c18bc1c12363d898f3ced7 liuyanjie <x@gmail.com> 1527524452 +0800 commit: add CHANGELOG.md and CONTRIBUTING.md files
28baf4f77fb49abf99c18bc1c12363d898f3ced7 2b9fc8524bac21d5d5c2f988b5793315ce93abc6 liuyanjie <x@gmail.com> 1527556745 +0800 commit: add lib/index.js

初始化package.json

1
2
$ npm init
Is this ok? (yes)

安装 bluebird

1
2
3
4
5
6
7
8
$ npm install bluebird --save
npm notice created a lockfile as package-lock.json. You should commit this file.
npm WARN git-obj-model@1.0.0 No description
npm WARN git-obj-model@1.0.0 No repository field.

+ bluebird@3.5.1
added 1 package in 1.999s

添加文件到版本库,并查看变化。

1
2
3
4
5
6
7
8
$ git add package.json

$ cat .git/refs/heads/feature-a
2b9fc8524bac21d5d5c2f988b5793315ce93abc6

$ cat .git/refs/heads/master
2b9fc8524bac21d5d5c2f988b5793315ce93abc6

提交文件到版本库,并查看变化,提交指针已经指向新的提交对象,分支的版本超前于master,因为parent指向1ad0e5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$ git commit package.json -m 'add package.json'
[feature-a 74ce192] add package.json
1 file changed, 17 insertions(+)
create mode 100644 package.json

$ cat .git/refs/heads/master
2b9fc8524bac21d5d5c2f988b5793315ce93abc6

$ cat .git/refs/heads/feature-a
74ce19245be785772b33e1193df0d2c6a940ed10

$ git cat-file -p 74ce
tree 9d23f8ad2ecc9f8e49d78174bcc6e4668ca7f031
parent 2b9fc8524bac21d5d5c2f988b5793315ce93abc6
author liuyanjie <x@gmail.com> 1527557656 +0800
committer liuyanjie <x@gmail.com> 1527557656 +0800

add package.json

$ git cat-file -p 9d23
100644 blob a0cf709bc0991b5340080f944d02894dc1596d46 CHANGELOG.md
100644 blob c6b9e95b39b8cd8ead8bbf4b118104741017de1b CONTRIBUTING.md
100644 blob f3954314c1026028e77ea3a765aadefa67b45195 README.md
040000 tree 2fb9045bb558889ea2bd8cc5d8fe45e7247706da lib
100644 blob 5ee275ce022007c38d187c58aef13f8d9e04b553 package.json

合并分支 feature-amaster 分支,可以看到 master 分支跟上了 feature-a,只改指针既可,非常快。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ git checkout master
Switched to branch 'master'

$ git merge feature-a --no-ff
Merge made by the 'recursive' strategy.
package.json | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
create mode 100644 package.json

$ cat .git/refs/heads/master
b98c36254dba6e169429c2be57b1bbeccb6e1f28

$ cat .git/refs/heads/feature-a
74ce19245be785772b33e1193df0d2c6a940ed10

1
2
3
4
5
6
7
8
$ tig

2018-05-29 09:37 liuyanjie M─┐ [master] Merge branch 'feature-a'
2018-05-29 09:34 liuyanjie │ o [feature-a] add package.json
2018-05-29 09:19 liuyanjie o─┘ add lib/index.js
2018-05-29 00:20 liuyanjie o add CHANGELOG.md and CONTRIBUTING.md files
2018-05-28 23:38 liuyanjie I <v0.0.1> First Commmit

修改 master 分支,编辑文件并提交

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ git checkout master
Already on 'master'

$ echo -e "\n\n# About" >> README.md

$ git commit README.md -m "add About Me"
[master cc52745] add About Me
1 file changed, 4 insertions(+)

$ cat README.md
# Readme


# About

修改 feature-a 分支

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$ git checkout feature-a
Switched to branch 'feature-a'

$ cat README.md
# Readme

$ echo -e "\n\n# About" >> README.md

$ cat README.md
# Readme


# About

$ git add README.md

$ git commit README.md -m "add About Me"
[feature-a fd885cf] add About Me
1 file changed, 3 insertions(+)

查看分支头和历史记录,可以看到分支头指向不同的提交对象,而提交对象,又来源于同一个提交对象0cb214741c044af3fb4677fe72e3ae175f3e0358,两个分支之间存在交叉。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ cat .git/refs/heads/feature-a
74ce19245be785772b33e1193df0d2c6a940ed10

$ cat .git/refs/heads/master
cc52745b13b00c672c7ac9b1dc42336953293b7a

$ cat .git/logs/refs/heads/feature-a
0000000000000000000000000000000000000000 2b9fc8524bac21d5d5c2f988b5793315ce93abc6 liuyanjie <x@gmail.com> 1527557101 +0800 branch: Created from HEAD
2b9fc8524bac21d5d5c2f988b5793315ce93abc6 74ce19245be785772b33e1193df0d2c6a940ed10 liuyanjie <x@gmail.com> 1527557656 +0800 commit: add package.json
74ce19245be785772b33e1193df0d2c6a940ed10 fd885cfcd548a24fabb2f5b49ea60beadc8c5912 liuyanjie <x@gmail.com> 1527558668 +0800 commit: add About Me

$ cat .git/logs/refs/heads/master
0000000000000000000000000000000000000000 033fa1ab71f0d54f348f07a3a0ffcefd52804df5 liuyanjie <x@gmail.com> 1527521894 +0800 commit (initial): First Commmit
033fa1ab71f0d54f348f07a3a0ffcefd52804df5 28baf4f77fb49abf99c18bc1c12363d898f3ced7 liuyanjie <x@gmail.com> 1527524452 +0800 commit: add CHANGELOG.md and CONTRIBUTING.md files
28baf4f77fb49abf99c18bc1c12363d898f3ced7 2b9fc8524bac21d5d5c2f988b5793315ce93abc6 liuyanjie <x@gmail.com> 1527556745 +0800 commit: add lib/index.js
2b9fc8524bac21d5d5c2f988b5793315ce93abc6 b98c36254dba6e169429c2be57b1bbeccb6e1f28 liuyanjie <x@gmail.com> 1527557853 +0800 merge feature-a: Merge made by the 'recursive' strategy.
b98c36254dba6e169429c2be57b1bbeccb6e1f28 cc52745b13b00c672c7ac9b1dc42336953293b7a liuyanjie <x@gmail.com> 1527558514 +0800 commit: add About Me

切换到 master 分支,再次合并 feature-amaster

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ git checkout master
Switched to branch 'master'

$ git merge feature-a
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
Automatic merge failed; fix conflicts and then commit the result.

$ cat README.md
# Readme


<<<<<<< HEAD
## About

=======
## About
>>>>>>> feature-a

解决冲突之后的文件

1
2
3
4
5
6
7
$ cat README.md
# Readme


## About


1
2
3
4
5
6
7
8
9
10
11
$ git status
On branch master
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)

Unmerged paths:
(use "git add <file>..." to mark resolution)

both modified: README.md

1
2
3
4
5
$ git commit README.md -m 'fix conflict'
fatal: cannot do a partial commit during a merge.

$ git commit -a
[master 5a68c74] Merge branch 'feature-a'
1
2
3
4
5
$ cat .git/refs/heads/feature-a
fd885cfcd548a24fabb2f5b49ea60beadc8c5912

$ cat .git/refs/heads/master
5a68c7402c30ae44fd82dadba2d8f148efb75541
1
2
3
4
5
6
7
8
$ cat .git/logs/refs/heads/master
0000000000000000000000000000000000000000 033fa1ab71f0d54f348f07a3a0ffcefd52804df5 liuyanjie <x@gmail.com> 1527521894 +0800 commit (initial): First Commmit
033fa1ab71f0d54f348f07a3a0ffcefd52804df5 28baf4f77fb49abf99c18bc1c12363d898f3ced7 liuyanjie <x@gmail.com> 1527524452 +0800 commit: add CHANGELOG.md and CONTRIBUTING.md files
28baf4f77fb49abf99c18bc1c12363d898f3ced7 2b9fc8524bac21d5d5c2f988b5793315ce93abc6 liuyanjie <x@gmail.com> 1527556745 +0800 commit: add lib/index.js
2b9fc8524bac21d5d5c2f988b5793315ce93abc6 b98c36254dba6e169429c2be57b1bbeccb6e1f28 liuyanjie <x@gmail.com> 1527557853 +0800 merge feature-a: Merge made by the 'recursive' strategy.
b98c36254dba6e169429c2be57b1bbeccb6e1f28 cc52745b13b00c672c7ac9b1dc42336953293b7a liuyanjie <x@gmail.com> 1527558514 +0800 commit: add About Me
cc52745b13b00c672c7ac9b1dc42336953293b7a 5a68c7402c30ae44fd82dadba2d8f148efb75541 liuyanjie <x@gmail.com> 1527559277 +0800 commit (merge): Merge branch 'feature-a'

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ git merge --stat feature-a
Already up to date.

$ git branch -d feature-a
Deleted branch feature-a (was fd885cf).

$ git branch

$ git cat-file -p fd88
tree 82e47e13f87031cab7c47b455744bc4ed3fdba3c
parent 74ce19245be785772b33e1193df0d2c6a940ed10
author liuyanjie <x@gmail.com> 1527558668 +0800
committer liuyanjie <x@gmail.com> 1527558668 +0800

add About Me

$ git cat-file -p 5a68
tree 42564bab966d59e3501a1903c1cb2066adc2bd2d
parent cc52745b13b00c672c7ac9b1dc42336953293b7a
parent fd885cfcd548a24fabb2f5b49ea60beadc8c5912
author liuyanjie <x@gmail.com> 1527559277 +0800
committer liuyanjie <x@gmail.com> 1527559277 +0800

Merge branch 'feature-a'
1
2
3
4
5
6
7
8
9
10
11
12
13
$ git log --graph --all --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit --date=relative
* 5a68c74 - (HEAD -> master) Merge branch 'feature-a' (7 minutes ago) <liuyanjie>
|\
| * fd885cf - add About Me (17 minutes ago) <liuyanjie>
* | cc52745 - add About Me (20 minutes ago) <liuyanjie>
* | b98c362 - Merge branch 'feature-a' (31 minutes ago) <liuyanjie>
|\ \
| |/
| * 74ce192 - add package.json (34 minutes ago) <liuyanjie>
|/
* 2b9fc85 - add lib/index.js (49 minutes ago) <liuyanjie>
* 28baf4f - add CHANGELOG.md and CONTRIBUTING.md files (10 hours ago) <liuyanjie>
* 033fa1a - (tag: v0.0.1) First Commmit (11 hours ago) <liuyanjie>

配置命令别名

1
2
$ git config --global alias.lg "log --graph --all --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit --date=relative"
git lg

打标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ git tag -a v0.0.2 -m "v0.0.2"
v0.0.1
v0.0.2

$ cat .git/refs/tags/v0.0.2
174c530154ab3b4d4d322bcbd66cdde18882eb00

$ git cat-file -p 174c
object 5a68c7402c30ae44fd82dadba2d8f148efb75541
type commit
tag v0.0.2
tagger liuyanjie <x@gmail.com> 1527559851 +0800

v0.0.2

$ git cat-file -p 5a68
tree 42564bab966d59e3501a1903c1cb2066adc2bd2d
parent cc52745b13b00c672c7ac9b1dc42336953293b7a
parent fd885cfcd548a24fabb2f5b49ea60beadc8c5912
author liuyanjie <x@gmail.com> 1527559277 +0800
committer liuyanjie <x@gmail.com> 1527559277 +0800

Merge branch 'feature-a'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
$ tree .git
.git
├── COMMIT_EDITMSG
├── HEAD
├── ORIG_HEAD
├── co.gitup.mac
│   ├── cache.db
│   ├── info.plist
│   └── snapshots.data
├── config
├── description
├── hooks
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── fsmonitor-watchman.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   ├── pre-receive.sample
│   ├── prepare-commit-msg.sample
│   └── update.sample
├── index
├── info
│   └── exclude
├── logs
│   ├── HEAD
│   └── refs
│   └── heads
│   └── master
├── objects
│   ├── 03
│   │   └── 3fa1ab71f0d54f348f07a3a0ffcefd52804df5
│   ├── 17
│   │   └── 4c530154ab3b4d4d322bcbd66cdde18882eb00
│   ├── 28
│   │   └── baf4f77fb49abf99c18bc1c12363d898f3ced7
│   ├── 2b
│   │   └── 9fc8524bac21d5d5c2f988b5793315ce93abc6
│   ├── 2c
│   │   └── affd90cd736e58f516e3988e3af84f5fa42b4f
│   ├── 2f
│   │   └── b9045bb558889ea2bd8cc5d8fe45e7247706da
│   ├── 30
│   │   └── be8054b61a4a124e4cd8a1201de8474cb45e12
│   ├── 39
│   │   └── a204af28de9b4f0411735e597e0da7416ca35a
│   ├── 42
│   │   └── 564bab966d59e3501a1903c1cb2066adc2bd2d
│   ├── 44
│   │   └── 2b821b9b4acb5b0e632042542b3b29e2cf721e
│   ├── 48
│   │   └── 1cab7467cdb697bf7affdba0f2a5673a03366d
│   ├── 5a
│   │   └── 68c7402c30ae44fd82dadba2d8f148efb75541
│   ├── 5e
│   │   └── e275ce022007c38d187c58aef13f8d9e04b553
│   ├── 74
│   │   └── ce19245be785772b33e1193df0d2c6a940ed10
│   ├── 82
│   │   └── e47e13f87031cab7c47b455744bc4ed3fdba3c
│   ├── 96
│   │   └── 09811e44367d44f2915435f4454716e1e535fd
│   ├── 9d
│   │   └── 23f8ad2ecc9f8e49d78174bcc6e4668ca7f031
│   ├── a0
│   │   └── cf709bc0991b5340080f944d02894dc1596d46
│   ├── b9
│   │   └── 8c36254dba6e169429c2be57b1bbeccb6e1f28
│   ├── c1
│   │   └── 067ba0a7ba51f937518c9bc051ea744ca748fe
│   ├── c6
│   │   └── b9e95b39b8cd8ead8bbf4b118104741017de1b
│   ├── cc
│   │   ├── 52745b13b00c672c7ac9b1dc42336953293b7a
│   │   └── f88a6f1649213499841c33f9bb36d1d8756fb7
│   ├── f3
│   │   └── 954314c1026028e77ea3a765aadefa67b45195
│   ├── fd
│   │   └── 885cfcd548a24fabb2f5b49ea60beadc8c5912
│   ├── info
│   └── pack
└── refs
├── heads
│   └── master
└── tags
├── v0.0.1
└── v0.0.2

36 directories, 51 files

https://stackoverflow.com/questions/964876/head-and-orig-head-in-git

.git/HEAD 指明了当前活跃分支是哪个分支

1
2
3
$ cat .git/HEAD
ref: refs/heads/master

svg版

git-obj-model


查看源文件&nbsp;&nbsp;编辑源文件

多线程在程序设计中是常用的提升并发计算能力、提升吞吐量的常用手段,线程通常事先创建好,形成线程池,来对线程进行管理。

libuv 内部也实现了线程池,主要用于支持异步任务,在 libuv 中,线程池是和事件循环配合工作的。

libuv 提供可用于执行用户代码的线程池,并且能够在任务完成时,向事件循环线程发送消息通知主线程完成收尾工作。

默认情况下,线程池的大小是 4,但是可以在启动阶段通过设置 UV_THREADPOOL_SIZE 环境变量进行修改,最大为 128

初始化

线程池是全局的结构,所以所有的事件循环实例共享同一个线程池,当特定的函数使用线程池时(例如,调用 uv_queue_work()),libuv 通过 init_threads 函数预分配和初始化一定数量的线程,初始化函数只会被调用一次,这会带来一定的内存开销,但是可以提升运行时性能。

线程池初始化

线程池是由 init_threads 函数初始化的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
static void init_threads(void) {
unsigned int i;
const char* val;
uv_sem_t sem;

nthreads = ARRAY_SIZE(default_threads);
val = getenv("UV_THREADPOOL_SIZE");
if (val != NULL)
nthreads = atoi(val);
if (nthreads == 0)
nthreads = 1;
if (nthreads > MAX_THREADPOOL_SIZE)
nthreads = MAX_THREADPOOL_SIZE;

threads = default_threads;
if (nthreads > ARRAY_SIZE(default_threads)) {
threads = uv__malloc(nthreads * sizeof(threads[0]));
if (threads == NULL) {
nthreads = ARRAY_SIZE(default_threads);
threads = default_threads;
}
}

if (uv_cond_init(&cond))
abort();

if (uv_mutex_init(&mutex))
abort();

QUEUE_INIT(&wq);
QUEUE_INIT(&slow_io_pending_wq);
QUEUE_INIT(&run_slow_work_message);

if (uv_sem_init(&sem, 0))
abort();

for (i = 0; i < nthreads; i++)
if (uv_thread_create(threads + i, worker, &sem))
abort();

for (i = 0; i < nthreads; i++)
uv_sem_wait(&sem);

uv_sem_destroy(&sem);
}

初始化逻辑如下:

  1. 线程池中线程数量,并分配用于存储线程信息的内存空间;
  2. 初始化静态全局的 线程锁线程条件变量
  3. 初始化静态全局 uv__work 队列;
    1. wq 待执行的任务队列,未执行完毕,loop->wq 同为任务队列,但是保持的是执行完毕的任务;
    2. slow_io_pending_wq 慢IO延迟任务队列;
    3. run_slow_work_message 慢IO延迟任务队列代表,当存在慢IO延迟任务队列时,run_slow_work_message 被插入到 wq 中代替所有慢IO任务排队;
  4. 创建一定数量的线程;
  5. 等待所以线程创建完成。

在创建线程的时候,线程执行的函数是 worker,该函数负责在线程中处理 wq 上的任务。

worker 实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
/* To avoid deadlock with uv_cancel() it's crucial that the worker
* never holds the global mutex and the loop-local mutex at the same time.
*/
static void worker(void* arg) {
struct uv__work* w;
QUEUE* q;
int is_slow_work;

uv_sem_post((uv_sem_t*) arg);
arg = NULL;

// 加锁 mutex
// 因为只有一个线程能抢占锁,所以多个线程也只能一个接一个的进入循环
// 因为整个线程池中线程创建过程中不会出现其他线程在其他位置抢占并锁定 mutex 的情形出现,
// 所以只有该位置会抢占加锁,而后很快释放锁,所以线程池中的线程之后短暂的阻塞在这里。
// 工作线程需要不断的等待处理任务,所以需要进入死循环
uv_mutex_lock(&mutex);
for (;;) {
/* `mutex` should always be locked at this point. */

/* Keep waiting while either no work is present or only slow I/O
and we're at the threshold for that. */
// 条件满足时,没有任务需要处理,线程进入挂起等待状态,等待被唤醒。
while (
// 任务队列为空
QUEUE_EMPTY(&wq) ||
// 任务队列非空,但是
// 队列头部被标记为慢速IO任务
// 且该队列中只有run_slow_work_message一个数据节点
// 且正在处理的慢IO任务超过阈值(默认2)
// 该一个条件避免太多线程同时都在处理慢IO操作
// 达到阈值后空闲的线程不再接慢IO任务而是挂起,等待非慢IO操作任务 能有机会尽快得到处理
// 正在进行的慢IO任务完成后,阈值限制解除,可以接慢IO任务
// 最终,保证了最多只有 `(nthreads + 1) / 2` 个线程处理慢IO
// 区分了快车道和慢车道后,能有效避免慢车堵快车,提升性能
(QUEUE_HEAD(&wq) == &run_slow_work_message
&& QUEUE_NEXT(&run_slow_work_message) == &wq
&& slow_io_work_running >= slow_work_thread_threshold())) {
// 进入休息区,注意某线程在执行 while 循环时该线程一定抢占了 mutex,不论是首次还是后续执行
// 线程挂起,等待唤醒
// uv_cond_wait 会使线程挂起等待cond上的信号,为防止多线程同时调用 uv_cond_wait,必须提前加锁
// uv_cond_wait 在挂起前会释放 mutex,其他阻塞在 mutex 上的线程会在 mutex 释放时被唤醒,并在唤醒时重新抢占 mutex,即只能唤醒一个
// 所以,阻塞在for循环外的多个线程中的某一个会重新抢占 mutex 执行到达此处挂起,又继续唤醒其他线程
// 也可能唤醒 阻塞在 uv__work_submit -> post 函数提交任务的抢占锁的位置的线程(通常为主事件循环线程)
// 挂起的线程都是空闲的线程,被唤醒后为非空闲的线程,所以需要更新空闲线程计数
idle_threads += 1;
uv_cond_wait(&cond, &mutex);
idle_threads -= 1;
// 挂起的线程在被唤醒后,一定不满足再次进入循环的条件,会继续向下执行
}

// 进入工作区,一共有三个区间,前后两个区间都有锁,中间的区间执行用户代码无锁

// 线程被唤醒,开始干活

// 以下操作因线程被唤醒时会自动对mutex上锁
// 所以以下解锁前的区域对共享变量的操作都是安全的
// 锁定区间代码同一时段只能有一个线程在执行
// 因并无耗时任务,所以不会影响性能

// 获取任务
q = QUEUE_HEAD(&wq);
// 如果收到线程退出消息,跳出循环,线程声明周期结束
// 在外部发送消息通知线程主动退出,也可在外部kill线程
if (q == &exit_message) {
uv_cond_signal(&cond);
uv_mutex_unlock(&mutex);
break;
}

// 将任务摘出来
QUEUE_REMOVE(q);
QUEUE_INIT(q); /* Signal uv_cancel() that the work req is executing. */

// 初始化慢IO操作标记为0,即非慢IO操作
is_slow_work = 0;
if (q == &run_slow_work_message) {
// 该任务为慢IO任务
// 通常情况下,while 的第二个条件成立才能进入此段代码
// 此时 q 只是一个慢IO任务标记,真正的任务在 slow_io_pending_wq 中
// 所以需要特殊处理,获取真正的任务 q

/* If we're at the slow I/O threshold, re-schedule until after all
other work in the queue is done. */
// 如果当前运行的慢IO操作的线程数达到阈值(2个线程)
// 则将这些操作插入到 wq 队列末尾,延迟处理
// 避免多个线程同时处理慢IO
// 临界状态:已经有达到阈值限制个数的线程进入工作区处理慢IO任务,但是还没执行更新慢IO线程计数器代码,
// 后续被慢IO任务唤醒的线程线程可能因为慢IO线程计数器未更新而满足进入条件。
// 但是,因为该区间锁定了 mutex,阻塞在 uv_cond_wait 处的代码无法抢占锁无法执行,也就是无法跳出 while 循环,
// 到 mutex 释放时,被唤醒的线程能够抢占锁时,计数器已经被更新了,前面所说的进入条件不再满足了。
// 所以,条件满足时不能动,能动了条件又不满足了,本质上,两次判断在同一段锁定区间,所以以下情形应该难以出现,难道还有其他情况?
if (slow_io_work_running >= slow_work_thread_threshold()) {
QUEUE_INSERT_TAIL(&wq, q);
continue;
}

/* If we encountered a request to run slow I/O work but there is none
to run, that means it's cancelled => Start over. */
// 如果慢IO队列为空,可能任务被取消
if (QUEUE_EMPTY(&slow_io_pending_wq))
continue;

// 注意以上两处不需要 uv_mutex_unlock(&mutex)

// 标记该线程正在处理慢IO操作,同时增加慢IO线程计数器
is_slow_work = 1;
slow_io_work_running++;

// 从慢IO队列中重新获取任务
q = QUEUE_HEAD(&slow_io_pending_wq);
QUEUE_REMOVE(q);
QUEUE_INIT(q);

/* If there is more slow I/O work, schedule it to be run as well. */
// 如果还有更多的慢IO操作,则将这些任务插入到 wq 队列末尾,本次只能处理 q 这一个任务
if (!QUEUE_EMPTY(&slow_io_pending_wq)) {
QUEUE_INSERT_TAIL(&wq, &run_slow_work_message);
// 如果有空闲线程,唤醒
if (idle_threads > 0)
uv_cond_signal(&cond);
}
}

// 解锁 mutex
uv_mutex_unlock(&mutex);

// 只有以下两行不涉及竞态资源读写,不需要加锁,实际也不能锁
// 慢IO任务还是非慢IO任务,指的是w->work
w = QUEUE_DATA(q, struct uv__work, wq);
w->work(w);

// 因为 loop 在多线程中共享,所以访问 loop 需要加锁
uv_mutex_lock(&w->loop->wq_mutex);
w->work = NULL; /* Signal uv_cancel() that the work req is done
executing. */
// 将完成的任务插入到 loop->wq 队列中,在主事件循环线程中处理
QUEUE_INSERT_TAIL(&w->loop->wq, &w->wq);
// 发送完成信号,唤醒事件询线程并处理
uv_async_send(&w->loop->wq_async);
uv_mutex_unlock(&w->loop->wq_mutex);

/* Lock `mutex` since that is expected at the start of the next
* iteration. */
uv_mutex_lock(&mutex);
if (is_slow_work) {
/* `slow_io_work_running` is protected by `mutex`. */
slow_io_work_running--;
}
}
}

uv_async_send 已经分析过了,它向事件循环线程发送消息唤醒事件循环线程

主线程中的初始化工作

主线程中的初始化工作是先于线程池初始化的,这部分初始化完成了用于接收 work 线程消息的 AsyncHandle 的初始化工作。

uv_async_send 通过 loop->wq_async Handle 发送了消息,字段定义如下:

1
2
3
#define UV_LOOP_PRIVATE_FIELDS                                                \
uv_mutex_t wq_mutex; \
uv_async_t wq_async; \

loop->wq_async 是在 uv_loop_init 初始化的,如下:

1
2
3
4
5
6
7
8
9
10
int uv_loop_init(uv_loop_t* loop) {
...
err = uv_async_init(loop, &loop->wq_async, uv__work_done);
if (err)
goto fail_async_init;

uv__handle_unref(&loop->wq_async);
loop->wq_async.flags |= UV_HANDLE_INTERNAL;
...
}

loop->wq_async 被解引用了,所以并不会影响 loop 的活动状态。

loop->wq_async 的事件处理函数是 uv__work_done,该函数在事件循环线程中执行,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void uv__work_done(uv_async_t* handle) {
struct uv__work* w;
uv_loop_t* loop;
QUEUE* q;
QUEUE wq;
int err;

// 取出所有已完成的work,因与其他线程共享此变量,所以需要同步,因此此处可能会导致事件循环线程短暂阻塞
loop = container_of(handle, uv_loop_t, wq_async);
uv_mutex_lock(&loop->wq_mutex);
QUEUE_MOVE(&loop->wq, &wq);
uv_mutex_unlock(&loop->wq_mutex);

// 遍历所有已完成的work,调用 w->done,done 函数由用户提供
while (!QUEUE_EMPTY(&wq)) {
q = QUEUE_HEAD(&wq);
QUEUE_REMOVE(q);

w = container_of(q, struct uv__work, wq);
err = (w->work == uv__cancelled) ? UV_ECANCELED : 0;
w->done(w, err);
}
}

至此,从线程池初始化到线程处理任务再到线程与事件循环线程通信最后事件循环线程清理已完成的任务的整个流程已经分析完成。

下面,该了解一下,如何向线程池提交任务任务了。

任务提交

向线程池提交任务的 API 是 uv_queue_work,也实现线程池唯一对外暴露的 API,下面我们看它的具体实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int uv_queue_work(uv_loop_t* loop,
uv_work_t* req,
uv_work_cb work_cb,
uv_after_work_cb after_work_cb) {
if (work_cb == NULL)
return UV_EINVAL;

uv__req_init(loop, req, UV_WORK);
req->loop = loop;
req->work_cb = work_cb;
req->after_work_cb = after_work_cb;
uv__work_submit(loop,
&req->work_req,
UV__WORK_CPU,
uv__queue_work,
uv__queue_done);
return 0;
}

uv_queue_work 初始化了一个 uv_work_t 类型的 requestwork_cb 为线程池中线程执行的函数,after_work_cbwork_cb 执行完成之后在事件循环线程中执行的函数,req->work_req 是队列节点。最后通过 uv__work_submit 向线程池中提交任务。

最后通过调用 uv__work_submit 向线程池中提交任务,uv__work_submit 的两个实参 uv__queue_workuv__queue_done 分别对 work_cbafter_work_cb 进行简单的封装。实现如下:

1
2
3
4
5
static void uv__queue_work(struct uv__work* w) {
uv_work_t* req = container_of(w, uv_work_t, work_req);

req->work_cb(req);
}
1
2
3
4
5
6
7
8
9
10
11
static void uv__queue_done(struct uv__work* w, int err) {
uv_work_t* req;

req = container_of(w, uv_work_t, work_req);
uv__req_unregister(req->loop, req);

if (req->after_work_cb == NULL)
return;

req->after_work_cb(req, err);
}

uv__work_submit 的实现如下:

1
2
3
4
5
6
7
8
9
10
11
void uv__work_submit(uv_loop_t* loop,
struct uv__work* w,
enum uv__work_kind kind,
void (*work)(struct uv__work* w),
void (*done)(struct uv__work* w, int status)) {
uv_once(&once, init_once);
w->loop = loop;
w->work = work;
w->done = done;
post(&w->wq, kind);
}

uv__work_submit 通过调用 init_once 初始化线程池,uv_once 确保线程池初始化函数 init_once 只会被调用一次。

然后对 uv__work 进行初始化,w->work 在工作线程 worker 中调用,w->done 在事件循环线程 uv__work_done 中调用

最后通过调用 post 提交任务,post 实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static void post(QUEUE* q, enum uv__work_kind kind) {
uv_mutex_lock(&mutex);
if (kind == UV__WORK_SLOW_IO) {
/* Insert into a separate queue. */
QUEUE_INSERT_TAIL(&slow_io_pending_wq, q);
if (!QUEUE_EMPTY(&run_slow_work_message)) {
/* Running slow I/O tasks is already scheduled => Nothing to do here.
The worker that runs said other task will schedule this one as well. */
uv_mutex_unlock(&mutex);
return;
}
q = &run_slow_work_message;
}

QUEUE_INSERT_TAIL(&wq, q);
if (idle_threads > 0)
uv_cond_signal(&cond);
uv_mutex_unlock(&mutex);
}

因为任务队列会被线程池中的多个线程并发访问,所以在操作队列之前需要先加锁,完成之后需要解锁。如果有空闲的线程,则立即唤醒它们进行工作。

post 中,慢IO任务被插入到 slow_io_pending_wq 队列中,如果 run_slow_work_message 不在 wq 中,则需要将 run_slow_work_message 插入 wq 队列尾部,标识 slow_io_pending_wq 中存在任务,当 run_slow_work_message 得到被处理机会时,处理慢任务队列中的任务。

uv_queue_work 中的 uv__work_submit 调用时,传递的是 UV__WORK_CPU 表示 CPU 密集型任务。

任务可能在任意一个线程中提交,通常是在事件循环线程中提交,但是也有可能在work线程中提交,即,w->workw->done 这两个函数中都有可能调用 uv__work_submit,这取决于实现。

将任务提交到工作队列中,这一阶段的工作就已经完成了,线程池中的线程可以开始工作了。

至此,整个线程池的工作原理已经分析完成,整个工作流程大致可分为三个阶段:

  1. 提交任务;
  2. work线程处理任务,完成后通知事件循环线程;
  3. 事件循环线程收到通知后完成收尾工作。

在接口使用中,是不需要太关心以上流程和工作原理的,更应该关系 work_cbafter_work_cb 以及其他逻辑的实现。

Example

线程池在 libuv 内部用于完成所有文件系统操作(requests),也用于实现 getaddrinfogetnameinfo 等 DNS 相关的操作(requests)。搜索 uv_queue_work 可找到相关使用位置。可以这些内部实现作为使用示例,在内部,并不通过 uv_queue_work 提交任务,而是直接调用 uv__work_submit,因为它们都有各自不同的 uv__x_workuv__x_done 实现。

文档


查看源文件&nbsp;&nbsp;编辑源文件

Async 允许用户在其他线程中唤醒主事件循环线程并触发回调函数调用。

事件循环线程在运行到 Pool 阶段会因为 epoll_pwait 调用阻塞一定的时间,libuv 会根据事件循环信息预估阻塞多长时间合适,也就是 timeout。但是在某些情境下,libuv 是无法准确预估的,例如线程池支持的异步文件操作,这些其他线程中的任务是无法有效判断多久能够运行完成的,在 libuv 中,其他线程工作完成之后,执行结果需要交给主事件循环线程,而事件循环线程可能恰好阻塞在 epoll_pwait 上,这时为了能够让其他线程的执行结果能够快速得到处理,需要唤醒主事件循环线程,也就是 epoll_pwait,而 Async 正式用来至此唤醒主事件循环的机制,简单的调用 uv_async_send 即可。线程池中的线程也正是利用这个机制和主事件循环线程通讯。

通过前文中对IO观察者的分析,我们知道,让 epoll_pwait 返回的方式,就是让 epoll_pwait 轮询的文件描述符中有I/O事件发生,Async 就是这么做的,通过 uv_async_send 向某个固定的文件描述符发送数据,使 epoll_pwait 返回。

Async 的入口函数共用两个:

  • uv_async_init 初始化 Async Handle
  • uv_async_send 发送消息唤醒事件循环线程并触发回调函数调用

首先,看一下 Async Handle 结构 uv_async_s 的定义:

https://github.com/libuv/libuv/blob/v1.28.0/include/uv.h#L789

1
2
3
4
struct uv_async_s {
UV_HANDLE_FIELDS
UV_ASYNC_PRIVATE_FIELDS
};
1
2
3
4
#define UV_ASYNC_PRIVATE_FIELDS                                               \
uv_async_cb async_cb; \
void* queue[2]; \
int pending; \

结构比较简单,async_cb 保存回调函数指针,queue 作为队列节点接入 loop->async_handlespending 字段表示已发送了唤醒信号,初始化为 0, 在调用唤醒函数之后会被设置为 1

继续看 uv_async_init

注意:该初始化函数不同于其他初始化函数,该函数会立即启动 Handle,所以没有 Start

https://github.com/libuv/libuv/blob/view-v1.28.0/src/unix/async.c#L40

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int uv_async_init(uv_loop_t* loop, uv_async_t* handle, uv_async_cb async_cb) {
int err;

err = uv__async_start(loop);
if (err)
return err;

uv__handle_init(loop, (uv_handle_t*)handle, UV_ASYNC);
handle->async_cb = async_cb;
handle->pending = 0;

QUEUE_INSERT_TAIL(&loop->async_handles, &handle->queue);
uv__handle_start(handle);

return 0;
}

uv__async_start 初始化并启动了 loop->async_io_watcher,使事件循环能够通过 loop->async_io_watcher 接收到其他线程发送的唤醒消息。

在进行简单的初始化后,直接启动了 handle,并不需要像其他 handle 一样提供 uv_async_start 这样的方法。

我们继续看一下 uv__async_start 如何工作:

https://github.com/libuv/libuv/blob/v1.28.0/src/unix/async.c#L156

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
static int uv__async_start(uv_loop_t* loop) {
int pipefd[2];
int err;

if (loop->async_io_watcher.fd != -1)
return 0;

err = uv__async_eventfd();
if (err >= 0) {
pipefd[0] = err;
pipefd[1] = -1;
}
else if (err == UV_ENOSYS) {
err = uv__make_pipe(pipefd, UV__F_NONBLOCK);
#if defined(__linux__)
/* Save a file descriptor by opening one of the pipe descriptors as
* read/write through the procfs. That file descriptor can then
* function as both ends of the pipe.
*/
if (err == 0) {
char buf[32];
int fd;

snprintf(buf, sizeof(buf), "/proc/self/fd/%d", pipefd[0]);
fd = uv__open_cloexec(buf, O_RDWR);
if (fd >= 0) {
uv__close(pipefd[0]);
uv__close(pipefd[1]);
pipefd[0] = fd;
pipefd[1] = fd;
}
}
#endif
}

if (err < 0)
return err;

uv__io_init(&loop->async_io_watcher, uv__async_io, pipefd[0]);
uv__io_start(loop, &loop->async_io_watcher, POLLIN);
loop->async_wfd = pipefd[1];

return 0;
}

函数中,初始化并启动了 loop->async_io_watcher,该函数中创建了管道,其本质是一个内核缓冲区(4k),有两个文件描述符引用,用于有血缘关系的进程和线程间进行数据传递(通信),pipefd 保存了管道的两端的文件描述符,pipefd[0] 用于读数据,pipefd[1] 用于写数据,pipefd[1] 被保存到了 loop->async_wfd,通过I/O观察者监听 pipefd[0] 即可接收消息,通过向 loop->async_wfd 写数据,即可发送消息。uv__async_start 在已经初始化 loop->async_io_watcher 的情况下,无需再次初始化。

需要注意的是,uv_async_init 可能调用多次用于初始化多个不同的 Async Handle,但是 loop->async_io_watcher 只有一个,也就是这些 Async Handle 共享了 loop->async_io_watcher,那么在 loop->async_io_watcher 上有I/O事件时,并不知道是哪个Async Handle发送的。

loop->async_io_watcher 上的I/O事件,由 uv__async_io 处理,它的实现如下:

https://github.com/libuv/libuv/blob/view-v1.28.0/src/unix/async.c#L76

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
static void uv__async_io(uv_loop_t* loop, uv__io_t* w, unsigned int events) {
char buf[1024];
ssize_t r;
QUEUE queue;
QUEUE* q;
uv_async_t* h;

assert(w == &loop->async_io_watcher);

for (;;) {
r = read(w->fd, buf, sizeof(buf));

if (r == sizeof(buf))
continue;

if (r != -1)
break;

if (errno == EAGAIN || errno == EWOULDBLOCK)
break;

if (errno == EINTR)
continue;

abort();
}

QUEUE_MOVE(&loop->async_handles, &queue);
while (!QUEUE_EMPTY(&queue)) {
q = QUEUE_HEAD(&queue);
h = QUEUE_DATA(q, uv_async_t, queue);

QUEUE_REMOVE(q);
QUEUE_INSERT_TAIL(&loop->async_handles, q);

if (cmpxchgi(&h->pending, 1, 0) == 0)
continue;

if (h->async_cb == NULL)
continue;

h->async_cb(h);
}
}

逻辑如下:

  1. 不断的读取 w->fd 上的数据到 buf 中直到为空,buf 中的数据无实际用途;
  2. 遍历 loop->async_handles 队列,调用所有 h->pending 值为 1handleasync_cb 函数如果存在的话。

h->pending 是在 uv_async_send 中被设置为 1。因为 h->pending 会在多线程中被访问到,所以存在资源争抢的临界状态,cmpxchgi 是原子操作,在这段代码中,如果 h->pending == 1 会被原子的 修改成 0,其他线程中对 h->pending 的读写也通过 cmpxchgi 进行原子操作,防止同时读写程序异常。

如上文所述,uv__async_io 并不知道是哪个 Async Handle 上调用的,uv__async_io 实际上调用了所有的 h->pending 值为 1 也就是发送过唤醒信号的 handle。实际上,Async 的设计的目的是能够唤醒主事件循环线程,所以 libuv 并需要关心是哪个 Async Handle 发送的信号,有可能同时发送。

接下来 我们了解一下 如何唤醒事件循环,简单的调用 uv_async_send 即可:

https://github.com/libuv/libuv/blob/v1.28.0/src/unix/async.c#L58

1
2
3
4
5
6
7
8
9
10
int uv_async_send(uv_async_t* handle) {
/* Do a cheap read first. */
if (ACCESS_ONCE(int, handle->pending) != 0)
return 0;

if (cmpxchgi(&handle->pending, 0, 1) == 0)
uv__async_send(handle->loop);

return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
static void uv__async_send(uv_loop_t* loop) {
const void* buf;
ssize_t len;
int fd;
int r;

buf = "";
len = 1;
fd = loop->async_wfd;

#if defined(__linux__)
if (fd == -1) {
static const uint64_t val = 1;
buf = &val;
len = sizeof(val);
fd = loop->async_io_watcher.fd; /* eventfd */
}
#endif

do
r = write(fd, buf, len);
while (r == -1 && errno == EINTR);

if (r == len)
return;

if (r == -1)
if (errno == EAGAIN || errno == EWOULDBLOCK)
return;

abort();
}

uv_async_send 可能在多个线程中同时调用,而且有可能在同一个 Async Handle 上调用,所以要求对 handle->pending 进行原子性读写。

uv__async_send 为实际进行写操作,因为管道中存在缓存区,所以需要不断的向 loop->async_wfd 写入数据,直到阻塞为止。

以上,就是 Async 唤醒事件循环线程的实现方式,很简单,核心在于竞态问题的解决。


查看源文件&nbsp;&nbsp;编辑源文件

Stream 提供一个全双工的通信信道的抽象,uv_stream_t 是一个抽象数据类型,libuv 提供了 uv_tcp_tuv_pipe_tuv_tty_t 3Stream 实现。

uv_stream_t

uv_stream_t 并未直接提供初始化函数,如同 uv_handle_t 一样,uv_stream_t 是在派生类型初始化的时候间接初始化的。派生类型的初始化函数中都调用了 uv__stream_init 函数对 uv_stream_t 进行初始化。

先介绍一下 uv_stream_t 的各个字段的含义

https://github.com/libuv/libuv/blob/view-v1.28.0/include/uv.h#L470

1
2
3
4
5
6
7
8
9
10
11
/*
* uv_stream_t is a subclass of uv_handle_t.
*
* uv_stream is an abstract class.
*
* uv_stream_t is the parent class of uv_tcp_t, uv_pipe_t and uv_tty_t.
*/
struct uv_stream_s {
UV_HANDLE_FIELDS
UV_STREAM_FIELDS
};

https://github.com/libuv/libuv/blob/view-v1.28.0/include/uv.h#L462

1
2
3
4
5
6
7
#define UV_STREAM_FIELDS                        \
/* number of bytes queued for writing */ \ 共有字段:
size_t write_queue_size; \ 等待写的字节数
uv_alloc_cb alloc_cb; \ 用于分配空间的函数指针
uv_read_cb read_cb; \ 读取数据完成之后的回调函数
/* private */ \
UV_STREAM_PRIVATE_FIELDS

https://github.com/libuv/libuv/blob/view-v1.28.0/include/uv/unix.h#L283

1
2
3
4
5
6
7
8
9
10
11
#define UV_STREAM_PRIVATE_FIELDS                 \ 私有字段:
uv_connect_t *connect_req; \ 连接请求
uv_shutdown_t *shutdown_req; \ 关闭请求
uv__io_t io_watcher; \ I/O观察者(has-a)
void* write_queue[2]; \ 写数据队列
void* write_completed_queue[2]; \ 完成的写数据队列
uv_connection_cb connection_cb; \ 有新连接时的回调函数
int delayed_error; \ 延迟的错误
int accepted_fd; \ 对端的fd
void* queued_fds; \ 排队的文件描述符列表
UV_STREAM_PRIVATE_PLATFORM_FIELDS \

Init

https://github.com/libuv/libuv/blob/v1.x/src/unix/stream.c#L84

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
void uv__stream_init(uv_loop_t* loop,
uv_stream_t* stream,
uv_handle_type type) {
int err;

uv__handle_init(loop, (uv_handle_t*)stream, type);
stream->read_cb = NULL;
stream->alloc_cb = NULL;
stream->close_cb = NULL;
stream->connection_cb = NULL;
stream->connect_req = NULL;
stream->shutdown_req = NULL;
stream->accepted_fd = -1;
stream->queued_fds = NULL;
stream->delayed_error = 0;
QUEUE_INIT(&stream->write_queue);
QUEUE_INIT(&stream->write_completed_queue);
stream->write_queue_size = 0;

if (loop->emfile_fd == -1) {
err = uv__open_cloexec("/dev/null", O_RDONLY);
if (err < 0)
/* In the rare case that "/dev/null" isn't mounted open "/"
* instead.
*/
err = uv__open_cloexec("/", O_RDONLY);
if (err >= 0)
loop->emfile_fd = err;
}

#if defined(__APPLE__)
stream->select = NULL;
#endif /* defined(__APPLE_) */

uv__io_init(&stream->io_watcher, uv__stream_io, -1);
}

uv__stream_init 的整体工作逻辑如下:

  1. 首先调用基类(uv_handle_t)初始化函数 uv__handle_init 对基类进行初始化;
  2. stream 结构进行初始化;
    1. 初始化相关字段;
    2. 初始化 stream->write_queue 写队列;
    3. 初始化 stream->write_completed_queue 写完成队列;为什么有两个写相关的队列?写操作为了实现异步非阻塞,上层的写操作并不能直接写,而是丢到队列中,当下层I/O观察者触发可写事件时,在进行写入操作。
  3. 最后调用I/O观察者初始化函数 uv__io_initstream->io_watcher 进行初始化,初始化传递了异步回调函数 uv__stream_io

uv__stream_inituv_stream_t 的派生类型的初始化函数 uv_tcp_inituv_pipe_inituv_tty_init 中被调用。

接下来看看 uv__stream_io 都做了什么

uv__stream_io

uv__stream_iouv_stream_t I/O事件的处理函数,实现如下:

https://github.com/libuv/libuv/blob/v1.28.0/src/unix/stream.c#L1281

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
static void uv__stream_io(uv_loop_t* loop, uv__io_t* w, unsigned int events) {
uv_stream_t* stream;

// 取得原 stream 实例
stream = container_of(w, uv_stream_t, io_watcher);

// 断言
assert(stream->type == UV_TCP ||
stream->type == UV_NAMED_PIPE ||
stream->type == UV_TTY);
assert(!(stream->flags & UV_HANDLE_CLOSING));

// 如果 stream 上存在 连接请求,则首选需要建立连接
if (stream->connect_req) {
uv__stream_connect(stream);
return;
}

// 断言存在文件描述符
assert(uv__stream_fd(stream) >= 0);

// 满足读数据条件,进行数据读取,读取成功后继续向下执行,读取需要多久?
/* Ignore POLLHUP here. Even if it's set, there may still be data to read. */
if (events & (POLLIN | POLLERR | POLLHUP))
uv__read(stream);

// read_cb 可能会关闭 stream
if (uv__stream_fd(stream) == -1)
return; /* read_cb closed stream. */

/* Short-circuit iff POLLHUP is set, the user is still interested in read
* events and uv__read() reported a partial read but not EOF. If the EOF
* flag is set, uv__read() called read_cb with err=UV_EOF and we don't
* have to do anything. If the partial read flag is not set, we can't
* report the EOF yet because there is still data to read.
*/
if ((events & POLLHUP) &&
(stream->flags & UV_HANDLE_READING) &&
(stream->flags & UV_HANDLE_READ_PARTIAL) &&
!(stream->flags & UV_HANDLE_READ_EOF)) {
uv_buf_t buf = { NULL, 0 };
uv__stream_eof(stream, &buf);
}

// read_cb 可能会关闭 stream
if (uv__stream_fd(stream) == -1)
return; /* read_cb closed stream. */

// 满足写数据条件,进行数据写入,写入成功后继续向下执行,读取需要多久?
if (events & (POLLOUT | POLLERR | POLLHUP)) {
uv__write(stream);
uv__write_callbacks(stream);

/* Write queue drained. */
if (QUEUE_EMPTY(&stream->write_queue))
uv__drain(stream);
}
}

在调用 uv__stream_io 时,传递了事件循环对象、I/O观察者对象、事件类型等信息。

执行逻辑如下:

  1. 首先,通过 container_of 将I/O观察者对象地址换算成 stream 对象地址,再进行强制类型转换,进而还原出 stream 类型;
  2. 验证 stream 类型已经状态是否正常;
  3. 如果 stream->connect_req 存在,说明 该 stream 需要 进行 connect,于是调用 uv__stream_connect
  4. 如果 满足 可读条件 调用 uv__read 进行数据读操作,读的数据来源于对应的文件描述符,内部调用 stream->alloc_cb 分配 uv_buf_t 进行数据存储空间分配,然后进行数据读取,读取完成后调用读完成回调 stream->read_cb
  5. 如果 满足 流结束条件 调用 uv__stream_eof 进行相关处理;
  6. 如果 满足 可写条件 调用 uv__write 进行数据写操作,写数据需要事先准备好,这些数据被放到了 stream->write_queue 写队列上,当底层描述符可写时,将队列上的数据写入。

后续,继续分析 uv__read uv__write uv__stream_eof 的相关实现逻辑,因为不影响大的逻辑,所以暂时可以先留空。

uv__read

https://github.com/libuv/libuv/blob/view-v1.28.0/src/unix/stream.c#L1110

当I/O观察者存在可读事件时,函数 uv__read 会被调用,当 uv__read 调用时,会通过 read 从底层文件描述符读取数据,读取的数据写到由 stream->alloc_cb 分配到内存中,并在完成读取后由 stream->read_cb 回调给用户层代码。因为可读数据已经由底层准备好,所以读取速度是非常快的,不需要等待。

默认情况下,当底层没有数据的情况时,read 系统调用会阻塞,但是此处因为文件描述符工作在非阻塞模式下,所有即使没有数据,read 也会立即返回。所以事件循环不好因为 uv__read 调用而耗时过长。

uv__write

https://github.com/libuv/libuv/blob/view-v1.28.0/src/unix/stream.c#L801

当I/O观察者存在可写事件时,函数 uv__write 会被调用,当 uv__write 调用时,数据已经在 stream->write_queue 队列上排好了,这个队列是 uv_write_t 类型的数据,如果队列为空没有数据可以写。用户在进行 uv_write() API 调用时,因为是异步操作,所以数据并不会直接执行真正的写操作,而是丢到写请求队列中后直接返回了,待到 stream 处于可写状态,事件处理含数 uv__stream_io 被调用,开始调用系统API进行真正的数据写入。

默认情况下,当底层没有更多内存缓冲区可用时,write 系统调用会阻塞,但是此处因为文件描述符工作在非阻塞模式下,所有即使缓冲区用完,write 也会立即返回。所以事件循环不好因为 uv__write 调用而耗时过长。

uv__write_callbacks

清理 stream->write_completed_queue 已完成写请求的队列,清理空间,并调用回调函数。

https://github.com/libuv/libuv/blob/view-v1.28.0/src/unix/stream.c#L926

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
static void uv__write_callbacks(uv_stream_t* stream) {
uv_write_t* req;
QUEUE* q;
QUEUE pq;

if (QUEUE_EMPTY(&stream->write_completed_queue))
return;

QUEUE_MOVE(&stream->write_completed_queue, &pq);

while (!QUEUE_EMPTY(&pq)) {
/* Pop a req off write_completed_queue. */
q = QUEUE_HEAD(&pq);
req = QUEUE_DATA(q, uv_write_t, queue);
QUEUE_REMOVE(q);
uv__req_unregister(stream->loop, req);

if (req->bufs != NULL) {
stream->write_queue_size -= uv__write_req_size(req);
if (req->bufs != req->bufsml)
uv__free(req->bufs);
req->bufs = NULL;
}

/* NOTE: call callback AFTER freeing the request data. */
if (req->cb)
req->cb(req, req->error);
}
}

uv__drain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
static void uv__drain(uv_stream_t* stream) {
uv_shutdown_t* req;
int err;

assert(QUEUE_EMPTY(&stream->write_queue));
uv__io_stop(stream->loop, &stream->io_watcher, POLLOUT);
uv__stream_osx_interrupt_select(stream);

/* Shutdown? */
if ((stream->flags & UV_HANDLE_SHUTTING) &&
!(stream->flags & UV_HANDLE_CLOSING) &&
!(stream->flags & UV_HANDLE_SHUT)) {
assert(stream->shutdown_req);

req = stream->shutdown_req;
stream->shutdown_req = NULL;
stream->flags &= ~UV_HANDLE_SHUTTING;
uv__req_unregister(stream->loop, req);

err = 0;
if (shutdown(uv__stream_fd(stream), SHUT_WR))
err = UV__ERR(errno);

if (err == 0)
stream->flags |= UV_HANDLE_SHUT;

if (req->cb != NULL)
req->cb(req, err);
}
}

Read

不同于其他类型的 handle,提供了 uv_timer_start 等方法,Stream 的 Start 在命名上略有不同,对 Stream 来说,有 uv_read_start 和 uv_write 以及其他的 Start 方式。

Start:uv_read_start

https://github.com/libuv/libuv/blob/view-v1.28.0/src/win/stream.c#L67

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
int uv_read_start(uv_stream_t* stream,
uv_alloc_cb alloc_cb,
uv_read_cb read_cb) {
assert(stream->type == UV_TCP || stream->type == UV_NAMED_PIPE ||
stream->type == UV_TTY);

if (stream->flags & UV_HANDLE_CLOSING)
return UV_EINVAL;

if (!(stream->flags & UV_HANDLE_READABLE))
return -ENOTCONN;

/* The UV_HANDLE_READING flag is irrelevant of the state of the tcp - it just
* expresses the desired state of the user.
*/
stream->flags |= UV_HANDLE_READING;

/* TODO: try to do the read inline? */
/* TODO: keep track of tcp state. If we've gotten a EOF then we should
* not start the IO watcher.
*/
assert(uv__stream_fd(stream) >= 0);
assert(alloc_cb);

stream->read_cb = read_cb;
stream->alloc_cb = alloc_cb;

uv__io_start(stream->loop, &stream->io_watcher, POLLIN);
uv__handle_start(stream);
uv__stream_osx_interrupt_select(stream);

return 0;
}

uv_read_start 有三个参数:

  1. stream,数据源;
  2. alloc_cb,读取数据时调用该函数分配内存空间;
  3. read_cb,读取成功后触发异步回调。

可以看到,启动过程同样没做什么特别的事情,将I/O观察者加入到队列中后,以便在事件循环的特定阶段进行处理。

Stop:uv_read_stop

https://github.com/libuv/libuv/blob/view-v1.28.0/src/unix/stream.c#L1584

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int uv_read_stop(uv_stream_t* stream) {
if (!(stream->flags & UV_HANDLE_READING))
return 0;

stream->flags &= ~UV_HANDLE_READING;
uv__io_stop(stream->loop, &stream->io_watcher, POLLIN);
if (!uv__io_active(&stream->io_watcher, POLLOUT))
uv__handle_stop(stream);
uv__stream_osx_interrupt_select(stream);

stream->read_cb = NULL;
stream->alloc_cb = NULL;
return 0;
}

Write

https://github.com/libuv/libuv/blob/view-v1.28.0/src/unix/stream.c#L1483

1
2
3
4
5
6
7
8
9
10
/* The buffers to be written must remain valid until the callback is called.
* This is not required for the uv_buf_t array.
*/
int uv_write(uv_write_t* req,
uv_stream_t* handle,
const uv_buf_t bufs[],
unsigned int nbufs,
uv_write_cb cb) {
return uv_write2(req, handle, bufs, nbufs, NULL, cb);
}

https://github.com/libuv/libuv/blob/view-v1.28.0/src/unix/stream.c#L1387

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
int uv_write2(uv_write_t* req,
uv_stream_t* stream,
const uv_buf_t bufs[],
unsigned int nbufs,
uv_stream_t* send_handle,
uv_write_cb cb) {
int empty_queue;

assert(nbufs > 0);
assert((stream->type == UV_TCP ||
stream->type == UV_NAMED_PIPE ||
stream->type == UV_TTY) &&
"uv_write (unix) does not yet support other types of streams");

if (uv__stream_fd(stream) < 0)
return UV_EBADF;

if (!(stream->flags & UV_HANDLE_WRITABLE))
return -EPIPE;

if (send_handle) {
if (stream->type != UV_NAMED_PIPE || !((uv_pipe_t*)stream)->ipc)
return UV_EINVAL;

/* XXX We abuse uv_write2() to send over UDP handles to child processes.
* Don't call uv__stream_fd() on those handles, it's a macro that on OS X
* evaluates to a function that operates on a uv_stream_t with a couple of
* OS X specific fields. On other Unices it does (handle)->io_watcher.fd,
* which works but only by accident.
*/
if (uv__handle_fd((uv_handle_t*) send_handle) < 0)
return UV_EBADF;

#if defined(__CYGWIN__) || defined(__MSYS__)
/* Cygwin recvmsg always sets msg_controllen to zero, so we cannot send it.
See https://github.com/mirror/newlib-cygwin/blob/86fc4bf0/winsup/cygwin/fhandler_socket.cc#L1736-L1743 */
return UV_ENOSYS;
#endif
}

/* It's legal for write_queue_size > 0 even when the write_queue is empty;
* it means there are error-state requests in the write_completed_queue that
* will touch up write_queue_size later, see also uv__write_req_finish().
* We could check that write_queue is empty instead but that implies making
* a write() syscall when we know that the handle is in error mode.
*/
empty_queue = (stream->write_queue_size == 0);

/* Initialize the req */
uv__req_init(stream->loop, req, UV_WRITE);
req->cb = cb;
req->handle = stream;
req->error = 0;
req->send_handle = send_handle;
QUEUE_INIT(&req->queue);

req->bufs = req->bufsml;
if (nbufs > ARRAY_SIZE(req->bufsml))
req->bufs = uv__malloc(nbufs * sizeof(bufs[0]));

if (req->bufs == NULL)
return UV_ENOMEM;

memcpy(req->bufs, bufs, nbufs * sizeof(bufs[0]));
req->nbufs = nbufs;
req->write_index = 0;
stream->write_queue_size += uv__count_bufs(bufs, nbufs);

/* Append the request to write_queue. */
QUEUE_INSERT_TAIL(&stream->write_queue, &req->queue);

/* If the queue was empty when this function began, we should attempt to
* do the write immediately. Otherwise start the write_watcher and wait
* for the fd to become writable.
*/
if (stream->connect_req) {
/* Still connecting, do nothing. */
}
else if (empty_queue) {
uv__write(stream);
}
else {
/*
* blocking streams should never have anything in the queue.
* if this assert fires then somehow the blocking stream isn't being
* sufficiently flushed in uv__write.
*/
assert(!(stream->flags & UV_HANDLE_BLOCKING_WRITES));
uv__io_start(stream->loop, &stream->io_watcher, POLLOUT);
uv__stream_osx_interrupt_select(stream);
}

return 0;
}

https://github.com/libuv/libuv/blob/view-v1.28.0/src/unix/stream.c#L1501

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
int uv_try_write(uv_stream_t* stream,
const uv_buf_t bufs[],
unsigned int nbufs) {
int r;
int has_pollout;
size_t written;
size_t req_size;
uv_write_t req;

/* Connecting or already writing some data */
if (stream->connect_req != NULL || stream->write_queue_size != 0)
return UV_EAGAIN;

has_pollout = uv__io_active(&stream->io_watcher, POLLOUT);

r = uv_write(&req, stream, bufs, nbufs, uv_try_write_cb);
if (r != 0)
return r;

/* Remove not written bytes from write queue size */
written = uv__count_bufs(bufs, nbufs);
if (req.bufs != NULL)
req_size = uv__write_req_size(&req);
else
req_size = 0;
written -= req_size;
stream->write_queue_size -= req_size;

/* Unqueue request, regardless of immediateness */
QUEUE_REMOVE(&req.queue);
uv__req_unregister(stream->loop, &req);
if (req.bufs != req.bufsml)
uv__free(req.bufs);
req.bufs = NULL;

/* Do not poll for writable, if we wasn't before calling this */
if (!has_pollout) {
uv__io_stop(stream->loop, &stream->io_watcher, POLLOUT);
uv__stream_osx_interrupt_select(stream);
}

if (written == 0 && req_size != 0)
return UV_EAGAIN;
else
return written;
}

查看源文件&nbsp;&nbsp;编辑源文件

0%