# git

Git 有三种状态,你的文件可能处于其中之一: 已提交(committed)、已修改(modified) 和 已暂存(staged)。

  • 已修改表示修改了文件,但还没保存到数据库中。

  • 已暂存表示对一个已修改文件的当前版本做了标记,使之包含在下次提交的快照中。

  • 已提交表示数据已经安全地保存在本地数据库中。

这会让我们的 Git 项目拥有三个阶段:工作区、暂存区以及 Git 目录。

基本的 Git 工作流程如下:

  1. 在工作区中修改文件。

  2. 将你想要下次提交的更改选择性地暂存,这样只会将更改的部分添加到暂存区。

  3. 提交更新,找到暂存区的文件,将快照永久性存储到 Git 目录。

如果 Git 目录中保存着特定版本的文件,就属于 已提交 状态。 如果文件已修改并放入暂存区,就属于 已暂存 状态。 如果自上次检出后,作了修改但还没有放到暂存区域,就是 已修改 状态。

# 初次运行 Git 前的配置

每台计算机上只需要配置一次,程序升级时会保留配置信息。 你可以在任何时候再次通过运行命令来修改它们。

# 用户信息

安装完 Git 之后,要做的第一件事就是设置你的用户名和邮件地址。 这一点很重要,因为每一个 Git 提交都会使用这些信息,它们会写入到你的每一次提交中,不可更改:

# 配置用户昵称,用于今后在项目展示
git config --global user.name <Your Name>

# 配置 email ,用于今后在项目展示
git config --global user.email <Your Email>

# 查看当前 git 配置
git config --list

# 查看 git 的某一项配置,比如用户昵称
git config user.name
1
2
3
4
5
6
7
8
9
10
11

由于 Git 会从多个文件中读取同一配置变量的不同值,因此你可能会在其中看到意料之外的值而不知道为什么。 此时,你可以查询 Git 中该变量的 原始 值,它会告诉你哪一个配置文件最后设置了该值。

git config --show-origin rerere.autoUpdate
1

# 获取帮助

# 获取帮助, <verb> 是某一项,也可以为空
git <verb> --help

# 获取所有帮助
git --help

# 获取 config 相关的帮助
git config --help

# 获取简易帮助, --help 是完整帮助信息, -h 是简易帮助信息
git config -h
1
2
3
4
5
6
7
8
9
10
11

# 基础使用

注意,之后的命令均需要终端位置处于项目的目录下运行。

# 创建或克隆仓库

# 创建 git 仓库

# 初始化仓库
git init
1
2

该命令将创建一个名为 .git 的子目录,这个子目录含有你初始化的 Git 仓库中所有的必须文件,这些文件是 Git 仓库的骨干。 但是,在这个时候,我们仅仅是做了一个初始化的操作,你的项目里的文件还没有被跟踪。

# 将所有文件添加到暂存区, 点 . 表示所有文件
git add .

# 将已经添加到暂存区的文件进行正式提交
git commit -m '说明本次提交了什么修改'
1
2
3
4
5

# 克隆已有的远程在线仓库仓库

# 克隆一个远程仓库到电脑本地
git clone <url>

# 克隆远程仓库到本地时,通过 <name> 指定本地仓库的名称
git clone <url> <name>
1
2
3
4
5

这会在当前目录下创建一个目录,目录名为 git 仓库名或自己指定的名称,并在这个目录下初始化一个 .git 文件夹, 从远程仓库拉取到的所有数据均会放入 .git 文件夹,然后从中读取最新版本的文件的拷贝放到项目中。

# 记录文件状态

# 文件状态

所有文件都有这两种状态:已跟踪 或 未跟踪。

已跟踪的文件是指那些被纳入了版本控制的文件,在上一次快照中有它们的记录,在工作一段时间后, 它们的状态可能是未修改,已修改或已放入暂存区。简而言之,已跟踪的文件就是 Git 已经知道的文件。

工作目录中除已跟踪文件外的其它所有文件都属于未跟踪文件,它们既不存在于上次快照的记录中,也没有被放入暂存区。

初次克隆某个仓库的时候,工作目录中的所有文件都属于已跟踪文件,并处于未修改状态,因为 Git 刚刚检出了它们, 而你尚未编辑过它们。

编辑过某些文件之后,由于自上次提交后你对它们做了修改,Git 将它们标记为已修改文件。 在工作时,你可以选择性地将这些修改过的文件放入暂存区,然后提交所有已暂存的修改,如此反复。

# 查看当前工作区状态,
git status

# 查看状态的简易概览
git status --short

# 等同于
git status -s
1
2
3
4
5
6
7
8

状态值:

  • ?? 新添加的文件,未追踪状态
  • A 新添加到暂存区中的文件
  • M 被修改的文件
  • MM 修改且暂存后,再次做了修改的文件

# 忽略文件

指定某些文件无需纳入 Git 的管理。

在根目录下创建 .gitignore 文件,并在其中指定忽略的文件。

  • 所有空行或者以 # 开头的行都会被 Git 忽略。

  • 可以使用标准的 glob 模式匹配,它会递归地应用在整个工作区中。

  • 匹配模式可以以 / 开头防止递归。

  • 匹配模式可以以 / 结尾指定目录。

  • 要忽略指定模式以外的文件或目录,可以在模式前加上叹号 ! 取反。

glob 模式是指 shell 所使用的简化了的正则表达式。

  • * 匹配零个或多个任意字符;
  • [abc] 匹配且只匹配一个列在方括号中的字符,比如 b
  • ? 只匹配一个任意字符;
  • [0-9] 短划线分隔的两个字符,匹配且只匹配一个包含在字符范围内的字符;比如 5
  • ** 表示匹配任意中间目录,比如 a/**/z 可以匹配 a/z, a/b/z, a/b/c/z
  • 等等...
# 忽略所有的 .a 文件
*.a

# 但跟踪所有的 lib.a,即便你在前面忽略了 .a 文件
!lib.a

# 只忽略当前目录下的 TODO 文件,而不忽略 subdir/TODO
/TODO

# 忽略任何目录下名为 build 的文件夹
build/

# 忽略 doc/notes.txt,但不忽略 doc/server/arch.txt
doc/*.txt

# 忽略 doc/ 目录及其所有子目录下的 .pdf 文件
doc/**/*.pdf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

GitHub 有一个十分详细的针对数十种项目及语言的 .gitignore 文件列表, 你可以在 https://github.com/github/gitignore (opens new window) 找到它。

一个仓库有可能只有根目录下有一个 .gitignore 文件,它递归地应用到整个仓库中。 然而,子目录下也可以有额外的 .gitignore 文件。子目录中的 .gitignore 文件中的规则只作用于它所在的目录中。

# 对比更改文件

# 查看工作区尚未暂存的文件做了哪些修改
git diff

# 查看已暂存的文件做了哪些修改
# 将比对已暂存文件与最后一次提交的文件差异
git diff --staged

# 等同于(--staged 和 --cached 是同义词)
git diff --cached
1
2
3
4
5
6
7
8
9

# 提交更新

# 提交更新,运行后会启动文本编辑器来输入 提交说明信息
git commit

# 提交,同时输入 提交说明信息, 不用开启文本编辑器
git commit -m '提交说明信息'

# 提交前,自动 add 所有已追踪文件到暂存区,然后执行提交操作
# 注意,新增的未被追踪的文件不会执行任何操作
git commit -a -m '提交说明信息'
1
2
3
4
5
6
7
8
9

# 移除文件

手动删除一个已追踪文件后,本次删除操作会被记入文件更改列表(未暂存工作区),需要手动 add 。

使用命令 git rm 方式删除,本次删除操作记录直接被放到了暂存区,相当于会自动执行 手动删除 和 add 的操作。

# 删除某个文件或目录
git rm <文件或目录名>

# 强制删除(force),当要删除的文件处于修改状态或暂存区状态时
# 为了防止误操作,需使用 -f 进行强制删除
git rm -f <文件或目录名>

# 仅取消 git 追踪,但不删除文件,保留文件
# 运行后,操作的文件会变成未追踪状态(U),就像是新增文件似的
git rm --cached <文件或目录名>
1
2
3
4
5
6
7
8
9
10

# 移动文件/重命名

# 将当前目录的 a.md 重命名为 b.md
git mv a.md b.md

# 将 dir 目录下的 a.md 移动到 dir2 目录中
git mv dir/a.md dir2/a.md

# 将 dir 目录下的 a.md 移动到 dir2 目录中并重命名为 b.md
git mv dir/a.md dir2/b.md
1
2
3
4
5
6
7
8

其实,运行 git mv就相当于运行了下面三条命令:

mv a.md b.md
git rm a.md
git add b.md
1
2
3

# 查看提交历史

# 查看提交记录
git log


# 显示每次提交所引入的差异(按 补丁 的格式输出)。
git log -p

# 等同于
git log --patch


# 限制显示的日志条目数量为 2 条
git log -p -2

# 查看每次提交的简略统计信息,包括每个文件变更行数
git log --stat

# 每次提交记录用一行显示
git log --pretty=oneline

# 简短显示
git log --pretty=short

# 完整显示
git log --pretty=full

# 更完整的提交信息
git log --pretty=fuller

# 自定义格式化显示提交记录
git log --pretty=format:"%h - %an, %ar : %s"
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

不传入任何参数的默认情况下,git log 会按时间先后顺序列出所有的提交,最近的更新排在最上面。

这个命令会列出每个提交的 SHA-1 校验和、作者的名字和电子邮件地址、提交时间以及提交说明。

git log --pretty=format:" " 字符串占位符

%H 提交的完整哈希值

%h 提交的简写哈希值

%T 树的完整哈希值

%t 树的简写哈希值

%P 父提交的完整哈希值

%p 父提交的简写哈希值

%an 作者名字

%ae 作者的电子邮件地址

%ad 作者修订日期(可以用 --date=选项 来定制格式)

%ar 作者修订日期,按多久以前的方式显示

%cn 提交者的名字

%ce 提交者的电子邮件地址

%cd 提交日期

%cr 提交日期(距今多长时间)

%s 提交说明
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 log 常用选项:

-p # 按补丁格式显示每个提交引入的差异。

--stat # 显示每次提交的文件修改统计信息。

--shortstat # 只显示 --stat 中最后的行数修改添加移除统计。

--name-only # 仅在提交信息后显示已修改的文件清单。

--name-status # 显示新增、修改、删除的文件清单。

--abbrev-commit # 仅显示 SHA-1 校验和所有 40 个字符中的前几个字符。

--relative-date # 使用较短的相对时间而不是完整格式显示日期(比如“2 weeks ago”)。

--graph # 在日志旁以 ASCII 图形显示分支与合并历史。

--pretty # 使用其他格式显示历史提交信息。可用的选项包括 oneline、short、full、fuller 和 format(用来定义自己的格式)。

--oneline # --pretty=oneline --abbrev-commit 合用的简写。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

过滤选项:

-<n> # 仅显示最近的 n 条提交。

--since, --after # 仅显示指定时间之后的提交。

--until, --before # 仅显示指定时间之前的提交。

--author # 仅显示作者匹配指定字符串的提交。

--committer # 仅显示提交者匹配指定字符串的提交。

--grep # 仅显示提交说明中包含指定字符串的提交。

-S # 仅显示添加或删除内容匹配指定字符串的提交。

--no-merges # 不显示做合并操作的提交记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 撤销操作

在 Git 中任何 已提交(commit) 的东西几乎总是可以恢复的。 甚至那些被删除的分支中的提交或使用 --amend 选项覆盖的提交也可以恢复。

但是记住,任何你 未提交(已修改或已暂存) 的东西丢失后很可能再也找不到了。

# 补充提交文件或提交信息

比如提交后发现漏掉了几个文件没有添加,或者提交说明信息写错了。

# 修正提交信息,将暂存区中的文件提交,并修改上一次提交说明信息
git commit --amend
1
2

如果自上次提交以来你还未做任何修改,例如,在上次提交后马上执行了此命令,那么快照会保持不变,而你所修改的只是提交信息。

运行命令后,会启动文本编辑器,可以看到之前的提交信息。 编辑后保存会覆盖原来的提交信息。

最终你只会有一个提交——第二次提交将代替第一次提交的结果。

从效果上来说,就像是旧有的提交从未存在过一样,它并不会出现在仓库的历史中。

# 取消暂存的文件

# 取消暂存文件,将暂存状态撤为 已修改未暂存 状态
git reset HEAD <file>
1
2

# 撤销已修改未暂存的文件

# 撤销已修改未暂存的文件,变回上一次 commit 的样子
git checkout -- <file>
1
2

注意, git checkout -- <file> 是一个危险的命令。 你对那个文件在本地的任何修改都会消失,Git 会用最近提交的版本覆盖掉它。

如果你仍然想保留对那个文件做出的修改,但是现在又需要撤消,我们将会在 Git 分支 介绍保存进度与分支,这通常是更好的做法。

# 远程仓库

远程仓库指在线上或本地服务器,甚至是本机启动的 git 服务管理中心。

# 查看远程仓库名称,默认叫做 origin
git remote

# 查看关联的所有远程仓库名称及对应的 URL
git remote -v

# 给本地仓库添加一个要关联的远程仓库,自定义名称及对应 URL
git remote add <name> <url>

# 查看某个远程仓库的信息
git remote show <name>

# 重命名,将 旧名称 重命名为 新名称
git remote rename <oldName> <newName>

# 移除和某个远程仓库的关联, remove 可简写为 rm
git remote remove <name>

# 从远程仓库拉取数据,不会与本地修改自动合并
git fetch <name>

# 自动拉取并合并该远程分支到当前分支
git pull

# 推送本地提交到某个远程仓库的某个分支
git push <name> <branch>

# 推送到 origin 仓库的 master 分支
git push origin master
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 clone 命令会自动设置本地 master 分支跟踪克隆的远程仓库的 master 分支(或其它名字的默认分支)。 运行 git pull 通常会从最初克隆的服务器上抓取数据并自动尝试合并到当前所在的分支。

只有当你有所克隆服务器的写入权限,并且之前没有人 push 过时,push 操作才能成功。 当有其他人比你先 push 时,必须先拉取,合并之后才能 push 成功。

# 标签 Tag

Git 可以给仓库历史中的某一个提交打上标签,以示重要。 比较有代表性的是人们会使用这个功能来标记发布结点( v1.0 、 v2.0 等等)。

# 查看已有 tag
git tag

# 可选参数 --list 或 -l,效果和 git tag 相同
git tag -l

# 只查看 tag 以 v1.8.5 开头的标签
# 注意,这种筛选, -l 或 --list 是必须的
git tag -l "v1.8.5*"
1
2
3
4
5
6
7
8
9

# 给本次提交创建标签

Git 支持两种标签:轻量标签(lightweight)与附注标签(annotated)。

轻量标签很像一个不会改变的分支——它只是某个特定提交的引用。

附注标签是存储在 Git 数据库中的一个完整对象, 它们是可以被校验的,其中包含打标签者的名字、电子邮件地址、日期时间, 此外还有一个标签信息,并且可以使用 GNU Privacy Guard (GPG)签名并验证。

通常会建议创建附注标签,这样你可以拥有以上所有信息。但是如果你只是想用一个临时的标签, 或者因为某些原因不想要保存这些信息,那么也可以用轻量标签。

# -a 添加附注标签,-m 同 commit -m 中一样,添加信息
git tag -a v1.4 -m "my version 1.4"

# 未使用 -m 时,和 commit 一样会启动文本编辑器进行输入
git tag -a v1.4

# 显示打标签人、日期、附注信息,和提交(commit)信息。
git show v1.4

# 创建轻量标签,不需要使用 -a、-s 或 -m 选项
# 轻量标签本质是就是某一次提交记录的引用
git tag v1.4-lw

# 查看轻量标签其实相当于查看那一次的提交记录
git show v1.4-lw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 给过去的提交创建标签

# 打标签时指定过去某次提交记录的 “校验和(SHA-1)” 即可
git tag -a v1.2 9fceb02
1
2

# 共享标签

默认情况下,git push 命令并不会传送标签到远程仓库服务器上。 在创建完标签后你必须显式地推送标签到共享服务器上。 这个过程就像共享远程分支一样——你可以运行 git push origin <tagname>

# 将 v1.5 标签提交到 origin 远程
git push origin v1.5

# push 所有不在远程仓库服务器上的标签
git push origin --tags
1
2
3
4
5

# 删除标签

# 删除某个标签
# 注意,该命令并不会从任何远程仓库中移除这个标签
git tag -d <tagname>

# 从某个远程移除某个标签
git push <remote> :refs/tags/<tagname>

# 比如:
# 含义是,将冒号前面的 空值 推送到远程标签名,从而删除它。
git push origin :refs/tags/v1.4-lw

# 更直观的删除远程标签的方式
git push origin --delete <tagname>
1
2
3
4
5
6
7
8
9
10
11
12
13

# 检出标签

如果你想查看某个标签所指向的文件版本,可以使用 git checkout 命令, 虽然这会使你的仓库处于“分离头指针(detached HEAD)”的状态——这个状态有些不好的副作用:

在“分离头指针”状态下,如果你做了某些更改然后提交它们,标签不会发生变化, 但你的新提交将不属于任何分支,并且将无法访问,除非通过确切的提交哈希才能访问。 因此,如果你需要进行更改,比如你要修复旧版本中的错误,那么通常需要创建一个新分支。

# 查看 2.0.0 标签指向的文件版本
git checkout 2.0.0

# checkout 同时想修改部分内容提交时,需要创建一个分支
git checkout -b version2 v2.0.0
1
2
3
4
5

# 别名

用于自定义 git 命令的别名,通常用于缩短命令字符长度。

比如:

# git checkout 简写为 git co
git config --global alias.co checkout

# git branch 简写为 git br
git config --global alias.br branch

# git commit 简写为 git ci
git config --global alias.ci commit

# git status 简写为 git st
git config --global alias.st status
1
2
3
4
5
6
7
8
9
10
11

# 分支

# 创建分支

Git 的 master 分支并不是一个特殊分支。 它就跟其它分支完全没有区别。 之所以几乎每一个仓库都有 master 分支,是因为 git init 命令默认创建它,并且大多数人都懒得去改动它。

# 创建一个指定名字的分支,不会自动切换到新分支
git branch <branchName>

# 创建一个新分支的并且切换到该分支
git checkout -b <newbranchname>
1
2
3
4
5

# 切换分支

# 切换到指定名称的分支,切换后 HEAD 会指向该分支最新提交
git checkout <branchName>
1
2

git 就是通过 HEAD 来确认当前处于哪一个分支上。

# 双分支工作流流程

为了方便描述,以下使用 a, b, c, ... 表示每一次提交的哈希值 (SHA-1 值)。

  • 每个分支永远指向自己的分支上最后一次提交记录(SHA-1 值), 不随着分支切换而切换,只会随着新的提交记录而指向最新的提交记录

  • 同时还有一个叫做 HEAD 的指针,永远指向当前所在分支上的最后一次提交记录,也就是说 HEAD 指针会随着分支的切换而切换。其实,将 HEAD 想象为当前分支的别名即可,当处于 master 分支时, HEAD 就和 master 一样指向同一个提交记录,当处于 dev 分支时, HEAD 就和 dev 分支一样指向同一个提交记录。

  1. 一开始,在 master 分支, HEAD 指向 master 分支上最后一次提交记录(SHA-1 值为 a);

  2. 创建并切换到 dev 分支,此时 HEAD 指向的也是同一个提交记录(SHA-1 值为 a);

  3. 在 dev 分支上修改文件并提交,此时 HEAD 指向 dev 分支上新产生的最后一次提交记录(SHA-1 值为 b);

  4. 切换回 master 分支,HEAD 随即指向 master 分支上最后一次提交记录(SHA-1 值为 a);,此时放佛 dev 做的更改和提交根本就不存在似的;

  5. 在 master 分支上做一些修改并提交,HEAD 指向 master 分支上新的提交记录(SHA-1 值为 c);

  6. 现在,这个项目的提交历史已经产生了分叉, master 分支从指向 a 变为了指向 c, dev 分支从指向 a 变为了指向 b

  7. 可以使用 git log 命令查看分叉历史,运行 git log --oneline --decorate --graph --all ,它会输出你的提交历史、各个分支的指向以及项目的分支分叉情况。

  8. 在 master 分支上执行 git merge dev 将 dev 分支的最新提交内容合并到 master 分支上来,如果有冲突则需要解决冲突文件。合并提交后 master 分支再度生成一条最新提交记录(SHA-1 值为 d ),此时 dev 分支最新提交依然为 b

  9. 切换到 dev 分支,执行 git merge master 操作,将 dev 分支的最新提交记录指向 master 分支的提交记录 d 。至此,master 分支和 dev 分支再次共同指向同一次提交。

# 多分支工作流程

  • 将 master 分支作为线上正式版仓库,不用于开发或修复,只用于合并其他分支提交和发布。
  1. master 分支,最新提交记录哈希值为 a,新建一个 dev 分支,同样指向提交记录 a

  2. 在 dev 分支上做正常开发,开发到半途时,突然发现线上版本的一个 bug ,但 dev 的开发没做完,不能在 dev 上修复 bug ,否则会将没做完的工作一起提交。应该切回 master 分支后,再从线上版本 master 上创建一个分支来修复这个 bug ,但在切回 master 分支之前,先将 dev 上的修改做一次临时的提交,此时 dev 指向提交记录 b

  3. 切回 master 分支,然后创建并切换到 fix 分支用于修复 bug ,此时 fix 分支和 master 分支一样指向提交记录 a

  4. 在 fix 分支上进行 bug 的修复工作,修复完成后,进行提交,此时 fix 分支指向提新的交记录 c

  5. 切回 master 分支,将 fix 分支合并到 master 分支上,合并后, master 分支的指向也指向到了 fix 分支的提交记录 c 。在合并的时候,提示了 fast-forward 这个词,意为快进。 由于想要合并的 fix 分支所指向的提交记录 c 是当前 master 的提交记录 a 的直接后继, 因此 Git 会直接将 master 的指针向前移动到 c ,而不会新建一条最新的合并提交记录。换句话说,当试图合并两个分支时, 如果顺着一个分支走下去能够到达另一个分支,那么 Git 在合并两者的时候, 只会简单的将指针移到最新提交记录,而不会新增一条合并记录,因为这种情况下的合并操作没有需要解决的分歧——这就叫做 fast-forward (快进)。

  6. 因为此时 master 分支和 fix 分支都指向提交记录 c 了,所以此时已经可以删除多余的 fix 分支了。 执行 git branch -d fix 删除 fix 分支。

  7. 现在可以回到 dev 分支继续未完成的开发工作了,切到 dev 分支后, dev 指向的是刚才半途提交的记录 b 。继续完成剩余开发,然后提交,此时, dev 分支指向自己的最新提交记录 d

  8. 切回 master 分支准备合并工作然后发布,此时 master 指向记录 c , dev 指向记录 d 。先回顾一下, master 分支的最新记录 c 是从记录 a 到记录 c ,dev 分支的最新记录 d 是从记录 a 到记录 b 再到的记录 d 。也就是 master 分支和 dev 分支最近一次的共同指向(也就是记录交叉)是指向的记录 a ,但此时 master 分支已经没有指向记录 a 了。所以 master 分支和 dev 分支其实没在一条提交记录线上,而是在分叉的两条线上。

  9. 现在正式开始合并 dev 分支的提交到 master 分支上,执行 git merge dev 后,和之前简单地将 master 分支指针向前推进所不同的是,因为 master 当前指向 c, dev 指向 d ,他们是从 a 开始分叉开的,因为 c 并不是 d 的直接祖先,Git 不得不做一些额外的工作,出现这种情况的时候,Git 会使用两个分支的末端所指的快照(cd)以及这两个分支的公共祖先(a),做一个三方合并,这种合并会产生一条新提交记录,master 分支新生成一条提交记录 e ,这种提交被称作一次合并提交。 它的特别之处在于他有不止一个父提交(cd)。 但 dev 分支此时依然还指向自己的记录 d 。现在合并完成,可以将 master 分支进行正式发布了。

  10. 继续开发,切换到 dev 分支时,因为此时 dev 分支不是 master 分支已发布的最新提交,需要先将 master 分支的最新内容合并到 dev 分支,执行 git merge master 后,dev 的指针指向了 master 的最新记录 e ,所以 dev 上的文件内容和 master 的已经完全一致了,现在可以继续进行开发了,又回到了开始的样子,就这样循环往复... 。

  11. 补充,刚才的合并是很顺利的,Git 自动合并然后创建一个新的合并提交记录。但很多时候,合并没有那么顺利,如果 dev 分支和 fix 分支修改了同一个文件的同一处地方,Git 就没法干净的合并它们,因为他们有冲突的地方,在合并它们的时候其他没有冲突的文件会正常进行自动合并,但这个冲突文件需要手动解决,命令行会提示 Automatic merge failed; fix conflicts and then commit the result. 告诉你自动合并失败,需要手动修复冲突然后进行手动提交操作。

Git 会在有冲突的文件中加入标准的冲突解决标记,这样你可以打开这些包含冲突的文件然后手动解决冲突。 出现冲突的文件会包含一些特殊区段,看起来像下面这个样子:

<<<<<<< HEAD:
当前分支更改的部分
=======
要合并的分支更改的部分
>>>>>>>
1
2
3
4
5

冲突标记由 ======= 分隔开,上半部分(<<<<<<<) 表示当前分支做的修改,下半部分(>>>>>>>)表示要合并的分区做的修改,可以自行选择接受哪一部分修改,或者是两部分都接受。

解决冲突后,<<<<<<< , ======= , >>>>>>> 这些特殊标记被删除,就可以对有冲突的文件执行暂存(add)然后 提交(commit) 了,提交就会产生一条合并提交记录,对应的就是在没有冲突时自动完成合并生成的一条合并记录。

# 管理分支

# 查看当前所有分支,其中有一个分支前有星号(*)
# * 表示 HEAD 指针所在位置,也就是当前所处的分支
git branch

# 查看所有分支,以及每个分支的最后一次提交记录
git branch -v

# 查看已经合并到当前分支的分支
# 在这个命令展示的列表中,分支名字前没有 * 号的分支通常可以删除掉
# 因为没有 * 号的分支内容已经被合并,所以并不会失去任何东西。
git branch --merged

# 查看所有未合并到当前分支的分支
# 这里显示的分支无法被删除,因为它包含了还未合并的工作
git branch --no-merged

# 查看未合并到 branchName 分支的所有分支
# 使用这个命令就不需要先切换到 branchName 分支去
git branch --no-merged <branchName>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

如果要强行删除未被合并的分支,丢弃未被合并的内容,可以使用 git branch -D <branchName> 进行强制删除并丢弃。

# 常见分支工作流

# 长期分支

比如只在 master 分支上保留完全稳定的代码,有可能仅仅是已经发布或即将发布的代码。将 master 分支作为长期分支。

同时,项目上还有一些短期的平行分支,也叫主题分支,意思就是针对某个单一的特性主题而创建的分支,比如叫做 dev 或者 next 分支,被用来做后续开发或者测试稳定性,这些分支不必保持绝对稳定,但是一旦达到稳定状态,它们就可以被合并入 master 分支了。

这样,在确保这些已完成的短期分支能够通过所有测试,并且不会引入更多 bug 之后,就可以合并入主干分支中,等待下一次的发布。

通常来说,这样的分支管理方式,随着其他分支不断进行提交, master 分支会落后其他分支很多次提交后,才会将其他可以发布的分支合并到 master 分支上。