Git 基本概念
关于版本控制
版本控制(Version Control)是一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统。最重要的功能是可以记录文件修改历史记录,从而让用户能够查看历史版本,方便版本切换。
常见的版本控制系统有三类:
本地版本控制系统
采用某种简单的数据库来记录文件的历次更新差异。
例如在硬盘上保存补丁集(补丁是指文件修订前后的变化);通过应用所有的补丁,可以重新计算出各个版本的文件内容。

集中化的版本控制系统
单一的集中管理的服务器保存所有文件的修订版本。
协同工作的人们都通过客户端连到这台服务器,取出最新的文件或者提交更新。

分布式版本控制系统:
客户端把代码仓库完整地镜像下来,包括完整的历史记录。
任何一处协同工作用的服务器发生故障,事后都可以用任何一个镜像出来的本地仓库恢复。

Git简史
Git 是一个免费的 、开源的分布式版本控制系统,用于敏捷高效地处理任何或小或大的项目,并且易于学习, 占用空间小,性能极快,优于传统 SCM(Source code management)工具,如 Subversion、CVS、Perforce 和 ClearCase等非分布式版本控制系统。
Git 应用于整个软件生存期,具有廉价的本地分支、方便的暂存区域和多个工作流等功能。它保存的不是文件的变化或者差异,而是一系列不同时刻的快照。

Git 基本概念
文件基本状态
Git 中文件有两种基本状态:已跟踪(tracked)、未跟踪(untracked)。
- 已跟踪: 被纳入了版本控制的文件,在上一次的快照中有它们的记录
- 未跟踪: 除已跟踪之外的所有文件都是未跟踪,既不存在于上次快照的记录中,也没有被放入暂存区
已跟踪文件状态
已跟踪的文件又分为三个状态: 已提交(committed)、已修改(modified) 和已暂存(staged)。
- 已修改:修改了文件,但还没保存到数据库中
- 已暂存:对一个已修改文件的当前版本做了标记,使之包含在下次提交的快照中
- 已提交:数据已经安全地保存在本地数据库中
存储区域
三种文件状态对应了 Git 存储的三个区域:工作区、暂存区、Git 仓库

- 工作区(Working Directory):包含已经更改的文件,添加、修改、删除文件的操作都发生在工作区,这些操作尚未提交至本地仓库。
- 暂存区(Staging Area):工作区中文件的变化的文件并不会直接被提交至本地仓库,而是需要先将修改进行标记,经过更改并进行过标记而尚未提交的文件会被存储在暂存区,提交更改是提交暂存区记录的那些更改。暂存区只会记录文件更改信息,所记录的信息只是一个索引(index),而不会真正地存储文件的拷贝,因此暂存区也被称为“索引区”,索引文件是本地仓库中
.git/index文件。- 添加了一个文件时,这个文件默认不会被提交,而是需要先将其添加至暂存区后才能提交,这避免了提交不需要提交的文件。避免提交不需要提交的文件更通用的做法是在
.gitignore文件标记不希望提交的文件。
- 添加了一个文件时,这个文件默认不会被提交,而是需要先将其添加至暂存区后才能提交,这避免了提交不需要提交的文件。避免提交不需要提交的文件更通用的做法是在
- 版本库(Repository):创建 Git 存储仓库后,仓库根目录下会生成一个
.git目录,该目录包含项目的元数据及对象数据库,也是一个 Git 存储仓库最重要的部分,其中包含已提交的数据。已提交意味着数据已经安全地存储在本地存储库中,通过存储库也可以将工作目录恢复到任意一次提交时的状态。
Git 对象
blob 对象(数据对象)
- 单个文件内容的快照,不保存文件名。
- 不可变性: 由于 Git 对象的不可变性,一旦创建了
blob对象,其内容不会再改变。如果文件内容发生变化,Git 将创建一个新的blob对象来存储新的内容。 - 哈希引用:
blob对象的唯一标识是其内容的哈希值。这意味着相同内容的文件会生成相同的blob对象,而不管它们的位置或文件名
树对象
- 解决文件名保存的问题,将多个文件组织到一起。
- Git 以简化版 UNIX 文件系统的方式存储内容,所有内容以树对象和数据对象形式存储。
- 树对象对应 UNIX 中的目录项。
- 一个树对象包含了一条或多条树对象记录(tree entry),每条记录含有一个指向数据对象或者子树对象的 SHA-1 指针, 以及相应的模式、类型、文件名信息。
提交对象
当使用 git commit 进行提交操作时,Git 会创建一个提交对象(commits),该对象包含:
- 一个指向顶层树对象的指针,代表项目快照
- 指向父对象(父提交)的指针
- 作者的姓名和邮箱
- 时间戳
- 提交时输入的信息
首次提交产生的提交对象没有父对象,普通提交操 作产生的提交对象有一个父对象, 而由多个分支合并产生的提交对象有多个父对象。
Git 分支
暂存操作会使用 SHA-1 哈希算法计算每个文件的校验和,然后把文件快照保存到 blob 对象中,最终将校验和加入到暂存区域等待提交:
# 添加文件到暂存区
$ git add README.md hello.c .gitignore
# 提交修改
$ git commit -m 'add README.md hello.c .gitignore'当使用 git commit 进行提交操作时,Git 会先计算每一个子目录的校验和, 然后在 Git 仓库中将这些校验和保存为树对象。然后创建一个提交对象, 包含指向这个树对象的指针。 如此一来,Git 就可以重现此次保存的快照。
现在仓库中存有五个对象:三个 blob 对象(文件快照)、一个树对象(目录结构)、一个提交对象(指向树对象的指针和提交信息):

做些修改后再次提交,那么这次产生的提交对象会包含一个指向上次提交对象(父对象)的指针:

Git 分支的本质是指向提交对象的可变指针,这些指针指向的是分支内的最新提交对象,在每次提交时会自动移动。例如下图有三个分支,Master、Develop、Topic,他们分别指向的是提交对象 C1、C4、C5。

理解 Git 中的三棵树
“树”意为“文件的集合”,而不是特定的数据结构。Git 中有下面三棵树:
| 树 | 用途 |
| HEAD | 上一次提交的快照,下一次提交的父节点 |
| Index | 预期的下一次提交的快照 |
| Working Directory | 沙盒 |

HEAD
HEAD 是当前分支引用的指针,它总是指向该分支上的最后一次提交。 这表示 HEAD 将是下一次提交的父结点。 理解 HEAD 的最简方式,就是将它看做 该分支上的最后一次提交 的快照。
Index
索引是 预期的下一次提交。也被称为 Git 的“暂存区”,它就是运行 git commit 时 Git 存储的快照。
Git 将上一次检出到工作目录中的所有文件填充到索引区,它们看起来就像最初被检出时的样子。之后开发者将其中一些文件替换为新版本,接着通过 git commit 将它们转换为树来用作新的提交。
确切来说,索引在技术上并非树结构,它其实是以扁平的清单实现的。
Working Directory
工作目录(通常也叫 工作区),开发者在这里直接修改代码文件。
另外两棵树以一种高效但并不直观的方式,将它们的内容存储在 .git 文件夹中。工作目录会将它们解包为实际的文件以便编辑。可以把工作目录理解为沙盒,在将修改提交到暂存区并记录到历史之前,可以随意更改。
分支的本质
A branch in Git is simply a lightweight movable pointer to one of these commits. Create a new branch means createing a new pointer to the same commit you’re currently on. How does Git know what branch you’re currently on? It keeps a special pointer called HEAD, a pointer to the local branch you’re currently on.

Git 基础操作
本地 Git 常用的操作如下图所示:

获取 Git 仓库
通常有两种获取 Git 项目仓库的方式:
- 将尚未进行版本控制的本地目录转换为 Git 仓库。
- 从其它服务器克隆 一个已存在的 Git 仓库。
在已存在目录中初始化仓库
要用 Git 来控制一个尚未进行版本控制的项目目录,首先需要进入该项目目录中执行初始化git init。
$ cd /home/user/my_project
$ git init该命令将创建:
- 名为
.git的子目录,含有初始化 Git 仓库中所有的必须文件,这些文件是 Git 仓库的骨干。 但是项目里的文件还没有被跟踪。 - 一个没有任何 commit(提交记录)的初始分支,这一分支默认名称为
master。
克隆现有的仓库
Git 是去中心化的源码管理工具,每个开发人员设备上都是一个完整的库。通过 Git 托管平台,即可实现开发者之间仓库的同步。在托管平台上,仓库也是以完整的库的形式保存的。
git clone用来获取一份现有 Git 仓库的拷贝,例如从 github 上克隆 flowable-engine 的仓库:
git clone https://github.com/flowable/flowable-engine.gitGit 克隆的是该 Git 仓库服务器上的几乎所有数据,这意味着即使服务器数据丢失,也可以通过本地存在的镜像仓库恢复。
国际主流 Git 代码托管平台:GitHub、Gitlab等。
国内主流 Git 代码托管平台:Gitee、阿里云 Codeup、华为云 CodeHub 等。
将文件纳入 Git 控制
工作目录下的每一个文件都不外乎这两种状态:已跟踪或未跟踪,简而言之,已跟踪的文件就是纳入 Git 控制的文件。
自上次提交之后,编辑过的已跟踪文件会被 Git 标记为已修改。可以选择性将这些修改过的文件放入暂存区,然后提交所有已暂存的更改,如此反复。

检查当前文件状态
使用git status查看文件所处的状态,一般会显示四部分内容:
- 当前所在分支
- 暂存区的文件(包括已跟踪的新文件)
- 已修改的文件
- 处于未跟踪状态的新文件
$ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: hello.c
new file: tracked.c
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: README.md
Untracked files:
(use "git add <file>..." to include in what will be committed)
untracked.c如果在克隆仓库之后立刻使用该命令,会看到类似这样的输出:
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
nothing to commit, working directory clean这意味着:
- 当前处于 master 分支
- 所有已跟踪的文件在上次提交后都没被修改,当前目录下也没有出现处于未跟踪态的新文件,否则 Git 会在输出中罗列出来
如果在工作目录下创建一个新的 README.md 文件,再使用git status会看到 Git 列出了这个新的未跟踪文件:
$ echo 'This is my project' > README.md
$ 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 <filename>开始跟踪一个文件,指定的文件被添加到暂存区,将包含在下一次提交中。
要追踪 README.md 文件,只需执行:
$ git add README.md
$ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
new file: README.md暂存已修改的文件
如果修改 README.md文件,在git status中会出现如下输出:
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: README.md使用git add <filename>暂存这次更新,这和开始跟踪一个新文件执行的命令一样,可以把新建文件也理解为一次要暂存的更新。
git add本质上记录文件的快照,提交到仓库存储的是这个快照,而不是工作目录里的文件。如果在已经暂存文件的基础上,又修改了文件 README.md,可以看到git status中有如下输出:
# 暂存区
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: README.md
# 非暂存区
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: README.mdREADME.md 即在暂存区、又在非暂存区,这并不矛盾,因为暂存区的只是文件的一个快照。如果在暂存之后又修改了文件,需要再次使用git add暂存新的修改。
提交更新
使用git commit -m "<message>"将暂存区的更新提交到仓库中,未暂存的修改将不会被提交。所以在提交前务必使用git status确认是否遗漏其它需要暂存的文件。
参数message是此次提交的描述信息,方便自己或其他开发者日后检索更改。
例如提交 README.md 的更新,需要执行下面的命令:
$ git commit -m "modified README.md"
[master 329d542] modified README.md
1 file changed, 1 insertion(+), 1 deletion(-)忽略文件
有些文件是不需要(不应该)加入版本控制的,例如编译的临时文件、日志文件。通过创建.gitignore文件来配置要忽略的文件模式。
文件.gitignore的格式规范如下:
- 所有空行或者以 # 开头的行都会被 Git 忽略
- 可以使用标准的 glob 模式匹配,它会递归地应用在整个工作区中
- 匹配模式可以以(/)开头防止递归
- 匹配模式可以以(/)结尾指定目录
- 要忽略指定模式以外的文件或目录,可以在模式前加上叹号(!)取反
# .gitiggnore
# 忽略任何目录下的 build 目录
build/
# 忽略当前目录下的 hello.txt 文件,而不忽略子目录下的
/hello.txt
# 忽略所有以 .log 结尾的文件
*.log
# 忽略 doc 目录下的所有 .txt 结尾的文件
doc/*.txt
# 跟踪 doc/config.txt,即使前面忽略了 doc/*.txt
!doc/*.txt查看提交历史
在开发中,有时需要从宏观角度看项目的发展情况、回顾提交历史,可以使用git log命令。
$ git log
commit 329d5426ce2a3e6d4fc3b6f5898f94ab69fc4eb3 (HEAD -> master)
Author: author
Date: Thu Aug 10 20:36:48 2023 +0800
modified README.md
commit ce3f589374e6551c2705f54c28e5a32e2acaeee5
Author: author
Date: Thu Aug 10 20:33:49 2023 +0800
add README.md不传入任何参数的默认情况下,git log会按时间先后顺序列出所有的提交,最近的更新排在最上面。它会列出每个提交的校验和、作者名和邮件、提交时间、提交说明。
撤销操作
修补最近一次提交
使用git commit --amend修补最近一次提交,具体来说:在最近一次提交的基础之上,追加当前暂存区的修改,并且覆盖提交信息。原来的提交被完全替换,不会出现在仓库的历史中。
下面的例子展示了,如何向最近一次提交内追加内容。可以看到 “add newfile1” 完全被替换了,没有出现在历史记录中。
# 新建文件 newfile1 并提交
$ touch newfile1
$ git add newfile1
$ git commit -m "add newfile1"
[master 71f7292] add newfile1
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 newfile1
$ git log
commit 71f7292cd47547ec7ad787db25cc537612c9b14f (HEAD -> master)
Author: author
Date: Thu Aug 10 21:29:05 2023 +0800
add newfile1
commit 329d5426ce2a3e6d4fc3b6f5898f94ab69fc4eb3
Author: forza.cbw <849085093@qq.com>
Date: Thu Aug 10 20:36:48 2023 +0800
modified README.md
# 新建文件 newfile2 并追加到最近一次提交
$ touch newfile2
$ git add newfile2
$ git commit --amend -m "add newfile1 & newfile2"
[master 19c2a08] add newfile1 & newfile2
Date: Thu Aug 10 21:29:05 2023 +0800
2 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 newfile1
create mode 100644 newfile2
$ git log
commit 19c2a084a11976121aed6a348c6bf5a5ceac9f4f (HEAD -> master)
Author: author
Date: Thu Aug 10 21:29:05 2023 +0800
add newfile1 & newfile2
commit 329d5426ce2a3e6d4fc3b6f5898f94ab69fc4eb3
Author: author
Date: Thu Aug 10 20:36:48 2023 +0800
modified README.md取消暂存
当不小心添加了多余的文件到到暂存区时,使用git reset HEAD <filename>取消暂存。
下面的例子展示了取消暂存 newfile2 的步骤:
# 暂存 newfile1 & newfile2
$ git add newfile1
$ git add newfile2
$ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: newfile1
modified: newfile2
# 取消暂存 newfile2
$ git reset HEAD newfile2
Unstaged changes after reset:
M newfile2
$ git status
On branch master
Your branch is up to date with 'origin/master'.
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: newfile1
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: newfile2撤销修改
如果不想保留对文件的修改,可以使用git checkout -- <filename>撤销修改,Git 会把指定文件恢复到上次提交的样子,而那个文件在工作目录下的任何修改都会丢失!
--强调传入的参数是文件名,而不是分支名,避免文件与分支同名产生的二义性。
下面的例子展示了撤销修改 newfile2 的步骤:
# 查看原始内容
$ cat newfile2
This is newfile2.
# 修改
$ echo 'Hello world!' >> newfile2
$ cat newfile2
This is newfile2.
Hello world!
# 撤销修改
$ git checkout -- newfile2
$ cat newfile2
This is newfile2.远程仓库的使用
远程仓库是托管在网络中的版本库,开发者公用该仓库从而及时同步进展、方便项目协作。“远程”只是一个相对的概念,表示它相对于工作目录是在别处的,即使在存在本地也可以称为远程仓库。
查看远程仓库
如果想查看已经配置的远程仓库服务器,可以运行git remote -v命令,它会列出本地指定的远程服务器简称对应的地址。如果是克隆下来的仓库,至少能看到 origin — 这是 Git 给克隆的仓库服务器的默认 名字:
# 在克隆下来的仓库内
$ git remote -v
origin git@codeup.aliyun.com:64cbae3cbebb3e7debf54315/my_project.git (fetch)
origin git@codeup.aliyun.com:64cbae3cbebb3e7debf54315/my_project.git (push)添加远程仓库
git clone命令会自动添加远程仓库,如果前面是在已存在的目录中初始化仓库的话,不会绑定有远程仓库。使用git remote add <shortname> <url>添加一个新的远程 Git 仓库,<short> 指定仓库的简称。
# 在已存在的目录中初始化的仓库内
$ git remote -v
$ git remote add repo git@codeup.aliyun.com:64cbae3cbebb3e7debf54315/my_project.git
$ git remote -v
repo git@codeup.aliyun.com:64cbae3cbebb3e7debf54315/my_project.git (fetch)
repo git@codeup.aliyun.com:64cbae3cbebb3e7debf54315/my_project.git (push)从远程仓库获取数据
使用git fetch <remote>从远程仓库获得数据,这个命令执行后:
- 从远程仓库拉取所有本地没有的数据
- 获得远程仓库中所有分支的引用
- 不会自动合并或修改当前的工作目录
# 从 origin 仓库抓取内容
$ git fetch origin如果当前分支设置了跟踪远程分支, 可以用git pull命令 来自动抓取后合并该远程分支到当前分支。在本地新建、并推送到远程的分支,默认跟踪了远程对应的新分支;在本地克隆下来的分支默认也跟踪了对应的远程分支。
推送到远程仓库
当需要分享项目时,使用git push <remote> <branch>将其推送到远程仓库。
# 将 master 分支推送到 origin 服务器
$ git push origin master只有当你有所克隆服务器的写入权限,并且之前没有人推送过时,这条命令才能生效。 当你和其他人在同一时间克隆,他们先推送到上游然后你再推送到上游,你的推送就会毫无疑问地被拒绝。 你必须先抓取他们的工作并将其合并进你的工作后才能推送。
Git 分支管理
在版本控制过程中,可能同时推进多个任务,每个任务可以有自己单独的分支,使用分支意味着程序员可以把自己的工作从开发主线上分离出来,开发自己分支的时候不会影响主线分支的运行,因此分支可以简单理解为一个单独的副本。使用分支可以并行推进多个功能开发,提高开发效率。
许多使用 Git 的开发者都喜欢使用这种方式来工作,比如只在master分支上保留完全稳定的代码——有可能仅仅是已经发布或即将发布的代码。 他们还有一些名为develop或者next的平行分支,被用来做后续开发或者测试稳定性——这些分支不必保持绝对稳定,但是一旦达到稳定状态,它们就可以被合并入master分支了。
分支操作
列出分支
使用git branch列出本地分支,使用参数-r列出远程分支、-a列出本地和远程的分支。当前所在的分支由*标识,git 使用 HEAD 指针指向当前所在的本地分支。
# 列出本地分支
$ git branch
* master
# 列出远程分支
$ git branch -r
origin/master
# 列出本地和远程分支
$ git branch -a
* master
remotes/origin/master新建分支
使用git branch <branch_name>创建一个新的分支。实际上 Git 只是创建了一个可以移动的新的指针,比如创建 Develop 分支:
$ git branch
* master
# 创建 Develop 分支
$ git branch Develop
$ git branch
Develop
* master
# 查看各个分支所指的对象
$ git log --oneline --decorate
19c2a08 (HEAD -> master, Develop) add newfile1 & newfile2
329d542 modified README.md
切换分支
使用git checkout <branch_name>切换到一个已存在的分支,这条命令会做两件事:
- 移动 HEAD 到目标分支上
- 将工作目录恢复成目标分支所指向的快照内容
如果工作目录有没提交的修改,git 会拒绝切换分支,你会看到类似的报错信息。根据提示,可以先提交修改或使用git stash将修改临时入栈,然后再切换分支。
$ echo "newline" >> newfile1
# 切换到 master 分支
$ git checkout master
error: Your local changes to the following files would be overwritten by checkout:
newfile1
Please commit your changes or stash them before you switch branches.
Aborting提交完所有修改后切换到 Develop 分支,可以看到 HEAD 指向了 Develop 分支。但是因为 master 和 Develop 分支指向的是同一个提交对象(快照内容),工作目录并不会有变化。
# 切换到 Develop 分支
$ git checkout Develop
Switched to branch 'Develop'
$ git log --oneline --decorate
19c2a08 (HEAD -> Develop, master) add newfile1 & newfile2
329d542 modified README.md
对 Develop 分支进行修改,再次观察指针,发现 Develop 指针移到了最新的提交上:
$ echo "This is newfile1" >> newfile1
$ git add newfile1
$ git commit -m "modified newfile1"
[Develop 649fb31] modified newfile1
1 file changed, 1 insertion(+)
$ git log --oneline --decorate
649fb31 (HEAD -> Develop) modified newfile1
19c2a08 (master) add newfile1 & newfile2
329d542 modified README.md
合并分支
一般来说,master 分支是用于发布的稳定版本,develop 是用于开发的分支。当 develop 分支开发到一个阶段时,需要将其合并到 matser 分支用于发布。
使用git merge <branch_name>,将指定的分支合并到当前所在分支上。例如把 Develop 分支的修改合并到 master 上:
$ git checkout master
Switched to branch 'master'
Your branch is up to date with 'origin/master'.
# 将 Develop 合并到当前分支 master 上
$ git merge Develop
Updating 19c2a08..649fb31
Fast-forward
newfile1 | 1 +
1 file changed, 1 insertion(+)
$ git log --oneline --decorate
649fb31 (HEAD -> master, Develop) modified newfile1
19c2a08 (origin/master, master) add newfile1 & newfile2
329d542 modified README.md
代码冲突
在 Git 中,冲突(Conflict)是指在合并(Merge)或变基(Rebase)操作时,发现两个分支上对同一部分代码进行了不同的修改,导致无法自动合并这些修改的情况。当 Git 无法确定如何将两个分支的修改整合在一起时,就会发生冲突。
这种情况在多人协作开发时很容易出现,因此需要具备解决冲突的能力。
冲突案例
在多人协同开发的情景下,push 与 pull 很容易产生冲突。考虑这样一种场景:user_a 与 user_b 两位开发者在获取代码后同时对代码进行了修改,并先后进行了提交,先提交的user_a 自然不会遭遇任何异常,后提交的 user_b 则会看见错误提示如下:
[user_b] $ git push
To xxx/xxx.git
! [rejected] main -> main (fetch first)
error: failed to push some refs to 'xxx/xxx.git'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first integrate the remote changes
hint: (e.g., 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.在 push 的时候,git 会比对本地与远程仓库的 commit 是否匹配。因为远程包含了本地没有的提交记录,因此 push 被拒绝了。如下图所示:

要解决这一问题,user_b 必须将远程仓库中的最新代码拉取回本地,解决冲突后再重新提交:
[user_b] $ git pull
git pull
remote: Enumerating objects: 4, done.
remote: Counting objects: 100% (4/4), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 1), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), 244 bytes | 122.00 KiB/s, done.
From xxx/xxx
3acb560..e8b3207 main -> origin/main
CONFLICT (add/add): Merge conflict in conflict_demo.txt
Auto-merging conflict_demo.txt
Automatic merge failed; fix conflicts and then commit the result.
[user_b] $ cat conflict_demo.txt
aaa
<<<<<<< HEAD
I'm userB
=======
I'm userA
>>>>>>> e8b32079d5995effd93d4a0e4c7f4ba3762db59c
bbbgit pull 后的提示中,我们可以得知 conflict_demo.txt 出现了冲突,GIt 很聪明地帮助我们对文本进行了对比,并将不同之处标记了出来:HEAD 表示本地的内容,那一串哈希表示是远程服务器中存储的内容,我们现在就可以顺着 conflict_demo.txt 中的提示将文件修改成我们希望的样子,再重新 commit,如下图所示:

因此,在 push 前先进行 pull 解决冲突问题是一种好的实践。Git 也有一定的自动解决冲突的能力,例如 A、B 添加了不同的文件,Git 可以将文件进行自动合并,遇到冲突也可以使用 git stash 命令解决。但这种简单的合并方式会引发许多意想不到的问题,Git 还没有智能到可以分析解决代码中依赖、冲突的问题。在实践中,团队需要选择并遵守相应的 Git 应用方法论,遵循一定的规范,外加对 分支的使用,才是解决这类问题行之有效的方法。
分支开发工作流
Git 功能强大,它允许多种分支策略与工作流程。但在协作开发的背景下,如果组织没有采取统一的 Git 应用方法论,则很有可能出现工作流程不明确、过于复杂的问题,从而难以追踪查找问题,代码也容易产生冲突。因此,有许多 Git 应用的方法论被提出来,它们也被称为 workflow。
主流的 Git 应用方法论包括 Git Flow、Github Flow 以及 Gitlab Flow 等。
Git Flow
Git Flow 概述
是著名代码版本控制软件 Gitkraken 使用的模式:A successful Git branching model
- 根据 Git Flow 的建议,代码主要的分支有
master、develop、hotfix、release以及feature这五种分支。 - 其中
master以及develop这两个分支又被称作长期分支,因为他们会一直存活在整个 Git Flow 里,而其它的分支大多会因任务结束而被删除。

图片出处:https://nvie.com/posts/a-successful-git-branching-model/
Git Flow 分支功能
master分支- 主要是用来放稳定、随时可上线的版本。这个分支的来源只能从別的分支合并过来,开发者不会直接 Commit 到这个分支。因为是稳定版本,所以通常也会在这个分支上的 Commit 上打上版本 tag。
develop分支- 主要是所有开发的基础分支,当要新增功能的时候,所有的
feature分支都是从这个分支切出去的。而feature分支的功能完成后,也都会合并回这个分支。
- 主要是所有开发的基础分支,当要新增功能的时候,所有的
hotfix分支- 当线上产品发生紧急问题的時候,会从
master分支开一个hotfix分支出来进行修复,hotfix分支修复完成之后,会合并回master分支,也同时会合并一份到develop分支。
- 当线上产品发生紧急问题的時候,会从
release分支- 当认为
develop分支够成熟了,就可以将develop分支合并到release分支,在这里进行上线前的最后测试。测试完成后,release分支将会同时合并到master以及develop这两个分支上。
- 当认为
feature分支- 当要开始新增功能的时候,就使用
feature分支。feature分支都是从develop分支来的,完成之后会再并回develop分支。
- 当要开始新增功能的时候,就使用
Git Flow 的局限性
- Git Flow 相对复杂,需要同时维护两个长期分支;
- Git Flow 适合需要“多个版本共存”的场景,但现在的互联网产品不需要“多版本共存”,只需要部署最新版,不适合持续交付。
Gitlab Flow
Gitlab Flow 是前两者的综合,吸收了前两者的优点,针对 Github Flow 中“每次合并功能分支时即部署到生产环境”这一操作进行了优化,提出创建一个反映代码部署的生产分支production,通过将开发master分支合并到production来部署最新版本,且支持定时部署。

图片出处:Gitlab flow | Workflow | Help | GitLab
基本思想:代码依然如 Github Flow 那样合并入master,但通过专用production分支进行部署,当master足够稳定时才会并入 production 分支,甚至可以增加 pre-production 进行缓冲。
