Git命令工作机制

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;编辑源文件