0%

Git实践笔记

基础概念

仓库(Repository) :可以简单理解为一个项目的文件夹,它里面包含了项目的所有文件以及这些文件的修改历史记录等信息。例如你开发一个网站项目,整个网站相关的代码、配置文件等所在的这个 “管理空间” 就是仓库,本地仓库存在于你的电脑上,而远程仓库一般放在类似 GitHub、GitLab 等代码托管平台上。

提交(Commit) :是将你对文件做出的修改记录下来的一个操作,相当于给项目在某个时间点拍了一张 “快照”,记录下当时文件的状态以及你所做的变更描述(通过提交信息体现)。比如你修改了网站项目中某个页面的样式代码,把修改后的代码状态通过提交保存下来,方便后续追溯和管理。

分支(Branch) :分支就是在原有代码基础上开辟出来的一条独立的开发线。例如主分支(通常叫 master 或者 main 分支)一般存放稳定可发布的代码,而当你要开发新功能时,可以创建一个新的分支(如 feature - 登录功能分支),在这个分支上进行功能开发,不会影响主分支的代码,等功能开发完成并测试通过后再合并到主分支上。

远程仓库:指存储在云端的仓库,所有人以该仓库上的代码进行协作

本地仓库:存在于你的电脑上,你可以在它上面进行各种操作,包括提交、查看提交记录、查看状态、查看分支、查看远程仓库等。可以理解是远程仓库的一个副本。

本地仓库与远程仓库:通过push和pull等命令进行同步。Push:将本地仓库中的代码推送到远程仓库,Pull:将远程仓库中的代码拉到本地仓库。

分支策略(团队协作)

Git flow

长期分支:master、develop
临时分支:除master、develop外所有分支,一旦完成开发,它们就会被合并进develop或master,然后被删除。

Git flow的优点是清晰可控,缺点是相对复杂,需要同时维护两个长期分支。大多数工具都将master当作默认分支,可是开发是在develop分支进行的,这导致经常要切换分支,非常烦人。

更大问题在于,这个模式是基于”版本发布”的,目标是一段时间以后产出一个新版本。但是,很多网站项目是”持续发布”,代码一有变动,就部署一次。这时,master分支和develop分支的差别不大,没必要维护两个长期分支。

GitHub Flow

Github flow 是Git flow的简化版,专门配合”持续发布”。它是 Github.com 使用的工作流程。

它只有一个长期分支,就是master,因此用起来非常简单。

第一步:根据需求,从master拉出新分支,不区分功能分支或补丁分支。

第二步:新分支开发完成后,或者需要讨论的时候,就向master发起一个pull request(简称PR)。

第三步:Pull Request既是一个通知,让别人注意到你的请求,又是一种对话机制,大家一起评审和讨论你的代码。对话过程中,你还可以不断提交代码。

第四步:你的Pull Request被接受,合并进master,重新部署后,原来你拉出来的那个分支就被删除。(先部署再合并也可。)

Github flow 的最大优点就是简单,对于”持续发布”的产品,可以说是最合适的流程。

问题在于它的假设:master分支的更新与产品的发布是一致的。也就是说,master分支的最新代码,默认就是当前的线上代码。

可是,有些时候并非如此,代码合并进入master分支,并不代表它就能立刻发布。比如,苹果商店的APP提交审核以后,等一段时间才能上架。这时,如果还有新的代码提交,master分支就会与刚发布的版本不一致。另一个例子是,有些公司有发布窗口,只有指定时间才能发布,这也会导致线上版本落后于master分支。

上面这种情况,只有master一个主分支就不够用了。通常,你不得不在master分支以外,另外新建一个production分支跟踪线上版本。

GitLab Flow

Gitlab flow 是 Git flow 与 Github flow 的综合。它吸取了两者的优点,既有适应不同开发环境的弹性,又有单一主分支的简单和便利。它是 Gitlab.com 推荐的做法。

Gitlab flow 的最大原则叫做”上游优先”(upsteam first),即只存在一个主分支master,它是所有其他分支的”上游”。只有上游分支采纳的代码变化,才能应用到其他分支。

Gitlab flow分为两种:持续发布和版本发布

持续发布

对于”持续发布”的项目,它建议在master分支以外,再建立不同的环境分支。比如,”开发环境”的分支是master,”预发环境”的分支是pre-production,”生产环境”的分支是production。

开发分支是预发分支的”上游”,预发分支又是生产分支的”上游”。代码的变化,必须由”上游”向”下游”发展。比如,生产环境出现了bug,这时就要新建一个功能分支,先把它合并到master,确认没有问题,再cherry-pick到pre-production,这一步也没有问题,才进入production。

只有紧急情况,才允许跳过上游,直接合并到下游分支。

版本发布

bg2015122306 1

对于”版本发布”的项目,建议的做法是每一个稳定版本,都要从master分支拉出一个分支,比如2-3-stable、2-4-stable等等。

以后,只有修补bug,才允许将代码合并到这些分支,并且此时要更新小版本号。

命令

全局配置

1
2
3
4
5
6
7
# 设置个人信息
$ git config --global user.name="user1"
$ git config --global user.email="user1@qq.com"
# 查看
$ git config --global --list
user.name=user1
user.email=user1@qq.com

快捷键及通用指令

快捷键

在部分git命令返回结果中,还可以使用快捷键:

按键 功能
PgUP 上一页
PgDn 下一页
Home 开始页
End 结束页
H 帮助
Q 退出
方式1 方式2 说明
HEAD HEAD~0 当前版本
HEAD^ HEAD~1 上一个版本
HEAD^^ HEAD~2 上上一个版本
HEAD^^^ HEAD~3 上上上一个版本
以此类推 以此类推

创建 init/clone

1
2
3
4
5
6
7
8
9
10
11
12
13
# 1.本地新建,默认分支master
# 后续需要指定远程仓库,才能进行push(提交不需要)
$ git init

# 1.远程拉取
# 注意使用(git bash)拉取会创建repository文件夹,并将相关内容放入该文件夹内
$ git clone <-b 分支名称> https://github.com/user1/repository.git
$ cd repositor

# 查看仓库列表,可以看出拉取的同时有关操作的远程仓库都已指定
$ git remote -v
origin https://github.com/user1/repository.git(fetch)
origin https://github.com/user1/repository.git(push)

git init默认创建的分支名称可更改,从git for windows软件安装过程也可以看出:

查看状态 status/log/show

工作区与缓存区的状态 status

1
2
3
4
5
6
7
8
# 显示工作目录和暂存区的状态,查看哪些修改被暂存,哪些没有
$ git status

# 以精简的方式显示文件状态
$ git status -s

# 列出被忽略的文件
$ git status --ignored

状态码:

状态码 描述
’ ’ unmodified
M modified
A added
D deleted
R renamed
C copied
U updated but unmerged
?? untracked
!! ignored

已提交的记录 log/show

log
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
# 显示完整项目历史
$ git log

# 使用limit限制显示的提交数量,比如:git log -n 3
$ git log -n <limit>

# 将每个提交压缩成一行
$ git log --oneline

# 除了 git log 信息之外,包含哪些文件被更改了,以及每个文件相对的增删行数
$ git log --stat

# 显示代表每个提交的一堆信息。显示每个提交全部的差异(diff),这也是项目历史中最详细的视图。
$ git log -p

# 搜索特定作者的提交。<pattern> 可以是字符串或正则表达式。
$ git log --author="<pattern>"

# 搜索提交信息匹配特定 <pattern> 的提交。<pattern> 可以是字符串或正则表达式。
$ git log --grep="<pattern>"

# 只显示发生在 <since> 和 <until> 之间的提交。两个参数可以是提交 ID、分支名、HEAD 或是任何一种引用。
$ git log <since>..<until>

# 只显示包含特定文件的提交。查找特定文件的历史这样做会很方便。
$ git log <file>

# 还有一些有用的选项。--graph 标记会绘制一幅字符组成的图形,左边是提交,右边是提交信息。
# --decorate 标记会加上提交所在的分支名称和标签。--oneline 标记将提交信息显示在同一行,一目了然。
$ git log --graph --decorate --oneline
git show
1
2
3
4
5
6
7
8
9
10
11
# 查看最后一次的commit记录
$ git show

# 查看最后xx次的提交记录
$ git show -<limit>

# 查看某个特定提交的信息
$ git show <commit-id>

# 查看某个文件在特定提交下的状态:
$ git show <commit-id>:<path/to/file>

.gitignore

  1. 注释:#
  2. 忽略文件和目录:folderName。忽略folderName同名文件和目录,包括多级目录
  3. 忽略文件:folderName!folderName/。仅忽略 folderName 文件,而不忽略 folderName 目录
  4. 忽略目录:folderName/。包括多级目录
  5. 通配符:*。匹配多个字符
  6. 通配符:?。匹配除/以外的任意一个字符
  7. 方括号:[]。匹配多个列表中的任意一个字符,如*.[io].i.o文件都会被忽略
  8. 方括号-短划线:[X-X]。所有在这两个字符范围内的都可以匹配,如[0-9]表示匹配所有0到9的数字
  9. 反向操作:!
  10. 多级目录:**
  11. 防止递归:以/开头

注意:

  1. git跟踪文件,而不是目录
  2. .gitignore文件中,每行表示一种模式
  3. 如果本地仓库文件已被跟踪,那么即使在.gitignore中设置了忽略,也不起作用

添加 add

1
2
3
4
# 将本地所有untrack文件加入暂存区,并根据.gitignore过滤
$ git add .
# 同上,但忽略.gitignore不作过滤
$ git add *

暂存 stash

stash:将本地没提交的内容(已加入版本控制)进行缓存并从当前分支移除,缓存的数据结构为堆栈,先进后出.

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
# 1. 暂存
# 将没有提交的内容缓存并移除,名称为最后一次提交信息(commit -m的内容)
# 如果本地没有提交,则拉去远程仓库的最后一次提交信息
$ git stash
# 自定义名称
$ git stash save ""

# 2. 列表
$ git stash list
stash@{0}: WIP on master: 4fae394 a


# 3. 提取暂存内容并从暂存中移除
$ git stash pop [stash@{0}]

# 4.提取暂存内容但不移除
$ git stash apply [stash@{0}]

# 5. 移除暂存区特定缓存
$ git stash drop stash@{0}

# 6. 移除所有暂存区缓存
$ git stash clear

# 7. 显示缓存与当前分支的差异
$ git stash show [stash@{0}]
# 详细差异
$ git stash show [stash@{0}] -p

# 8. 指定或最新缓存创建分支
$ git stash branch

查看不同 diff

工作区的比较 diff

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 比较工作区(git add前)和暂存区(git add后)所有文件差异
$ git diff

# 工作区与git仓库之间的所有文件差异
$ git diff HEAD

# 工作区与上次提交所有文件差异
$ git diff HEAD^

# 工作区与上两次提交所有文件差异
$ git diff HEAD^2

# 工作区与具体某个版本的所有文件差异
$ git diff 具体某个版本

# 查看具体文件,多个文件[空格]隔开
$ git diff -- 文件名1 文件名2
$ git diff HEAD -- 文件名1 文件名2
$ git diff HEAD^ -- 文件名1 文件名2
$ git diff HEAD^2 -- 文件名1 文件名2
$ git diff 具体某个提交版本 -- 文件名1 文件名2

暂存区的比较 diff –cached

1
2
3
4
5
6
7
8
9
# 比较暂存区和上一次提交所有文件差异
$ git diff --cached

# 比较暂存区和指定版本所有文件差异
$ git diff --cached 版本号

# 查看具体文件,多个文件[空格]隔开
$ git diff --cached -- 文件名1 文件名2
$ git diff --cached 版本号 -- 文件名1 文件名2

不同版本库的比较 diff

1
2
3
4
5
6
7
8
9
10
11
12
13
# 查看两个(提交)版本之间的差异
$ git diff 版本号1 版本号2

# 查看两个版本之间的改动的文件列表
$ git diff 版本号1 版本号2 --stat

# 查看两个版本之间的文件夹 src 的差异
$ git diff 版本号1 版本号2 src


# 查看具体文件,多个文件[空格]隔开
$ git diff --cached -- 文件名1 文件名2
$ git diff --cached 版本号 -- 文件名1 文件名2

重置 reset

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# git reset 命令用于回退版本,可以指定退回某一次提交的版本。
$ git reset [--soft | --mixed | --hard] [HEAD]

# --mixed 为默认,可以不用带该参数,用于重置暂存区的文件与上一次的提交(commit)保持一致,工作区文件内容保持不变
$ git reset [HEAD]
# 实例
$ git reset HEAD^ # 回退所有内容到上一个版本
$ git reset HEAD^ hello.php # 回退 hello.php 文件的版本到上一个版本
$ git reset 052e # 回退到指定版本

# --soft,该命令仅仅修改分支中的HEAD指针的位置,不会改变工作区与暂存区中的文件的版本。
$ git reset --soft HEAD

# --hard,撤销工作区中所有未提交的修改内容,将暂存区与工作区都回到上一次版本,并删除之前的所有信息提交。
# untracked的文件不会被删除
$ git reset --hard HEAD
# 实例
$ git reset --hard HEAD~3 # 回退上上上一个版本
$ git reset –hard bae128 # 回退到某个版本回退点之前的所有信息。
$ git reset --hard origin/master # 将本地的状态回退到和远程的一样

清除 Untracked File

1
2
3
4
5
6
# 删除单个文件
$ git status
$ git remove 文件名

# 清除所有
$ git clean -f

提交 commit

1
$ git commit -m "第一次提交"

指定远程仓库 remote

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 指定仓库地址
$ git remote add origin https://github.com/user1/repository.git
# 查看仓库地址列表
$ git remote -v
origin https://github.com/user1/repository.git(fetch)
origin https://github.com/user1/repository.git(push)
# 再添加一个
$ git remote add upstream https://github.com/user2/repository.git
# 查看仓库地址列表
$ git remote -v
origin https://github.com/user1/repository.git(fetch)
origin https://github.com/user1/repository.git(push)
upstream https://github.com/user2/repository.git(fetch)
upstream https://github.com/user2/repository.git(push)

推送 push

1
2
3
4
5
6
# -u: 指定远程仓库(upstream)和远程分支,使用该命令后,以后使用git push 或 git pull 都是默认对应origin下的master分支
一般用于存在多个远程仓库或修改默认提交分支情况下使用,其它使用 git push 即可
$ git push -u orgin master

# 强制推送fouce,添加-f
$ git push -f origin master

拉取 pull

1
2
3
4
5
6
7
8
9
10
11
# fetch:获取远程内容,类似SVN-UPDATE,但不更新本地
$ git fetch

# pull:获取最新内容,并和本地分支合并
# git pull 其实就是 git fetch 和 git merge FETCH_HEAD 的简写
$ git pull

# 强制获取最新内容,不合并,以远程仓库为准
$ git reset --hard origin/master
# 再执行git pull
$ git pull

切换分支 checkout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 切换分支
$ git checkout branchName

# 新建分支同时切换
$ git checkout -b|-B <new_branch> [<start point>]
# 新建本地分支并切换
$ git checkout -b newBranchName
# 切换远程分支,将远程仓库里指定的分支拉取到本地,
# 并在本地创建一个分支与指定远程仓库分支关联起来。
# 并切换到新建的本地分支中
$ git checkout -b 本地分支名 origin/远程分支名

# 检出某一个特定文件,但如果不填写commit id,
# 则默认会从暂存区检出该文件,如果暂存区为空,
# 则该文件会回滚到最近一次的提交状态。
git checkout [<commit id>] [--] <paths>
# 因此可以撤销对某个文件的修改
$ git checkout [--] <paths>
# 撤销所有(工作区)文件修改
$ git checkout
# 撤销所有工作区和暂存区文件修改(untracked的文件不会被删除)
$ git checkout -f

合并分支 merge/rebase

merge

转载git merge详解

1
2
3
4
5
# git-merge命令是用于从指定的commit(s)合并到当前分支的操作
$ git merge [-n] [--stat] [--no-commit] [--squash] [--[no-]edit]
[-s <strategy>] [-X <strategy-option>] [-S[<keyid>]]
[--[no-]allow-unrelated-histories]
[--[no-]rerere-autoupdate] [-m <msg>] [<commit>…​]

有以下两种用途:

  1. 用于git-pull中,来整合另一代码仓库中的变化(即:git pull = git fetch + git merge)
  2. 用于从一个分支到另一个分支的合并

注意:当合并开始时如果存在未commit的文件,则不能重建合并前状态。因此非常不鼓励在使用git-merge时存在未commit的文件,建议使用git-stash命令将这些未commit文件暂存起来,并在解决冲突以后使用git stash pop把这些未commit文件还原出来

参数
参数 作用
–commit 使得合并后产生一个合并结果的commit节点。该参数可以覆盖–no-commit
–no-commit 使得合并后,为了防止合并失败并不自动提交,能够给使用者一个机会在提交前审视和修改合并结果
–edit
-e
用于在成功合并、提交前调用编辑器来进一步编辑自动生成的合并信息。因此使用者能够进一步解释和判断合并的结果
–no-edit 用于接受自动合并的信息(通常情况下并不鼓励这样做)
–ff 指fast-forward命令。当使用fast-forward模式进行合并时,将不会创造一个新的commit节点。默认情况下,git-merge采用fast-forward模式
–no-ff 即使可以使用fast-forward模式,也要创建一个新的合并节点。这是当git merge在合并一个tag时的默认行为
–ff-only 除非当前HEAD节点已经up-to-date(更新指向到最新节点)或者能够使用fast-forward模式进行合并,否则的话将拒绝合并,并返回一个失败状态
–log[=<n>] 将在合并提交时,除了含有分支名以外,还将含有最多n个被合并commit节点的日志信息
–no-log 不会列出该信息
–stat 将会在合并结果的末端显示文件差异的状态。文件差异的状态也可以在git配置文件中的merge.stat配置
–no-stat
-n
将不会显示该信息
–squash 当一个合并发生时,从当前分支和对方分支的共同祖先节点之后的对方分支节点,一直到对方分支的顶部节点将会压缩在一起,使用者可以经过审视后进行提交,产生一个新的节点(与–no-ff冲突)
–no-squash 相反
–strategy=<strategy>
-s <strategy>
用于指定合并的策略。默认情况如果没有指定该参数,git将按照下列情况采用默认的合并策略:
1.合并节点只含有单个父节点时(如采用fast-forward模式时),采用recursive策略(下文介绍)
2.合并节点含有多个父节点时(如采用no-fast-forward模式时),采用octopus策略(下文介绍)
–strategy-option=<option>
-X <option>
在-s <strategy>时指定该策略的具体参数(下文介绍)
–verify-signatures
–no-verify-signatures
用于验证被合并的节点是否带有GPG签名,并在合并中忽略那些不带有GPG签名验证的节点
–quiet
-q
静默操作,不显示合并进度信息
–verbose
-v
显示详细的合并结果信息
–progress
–no-progress
切换是否显示合并的进度信息。如果二者都没有指定,那么在标准错误发生时,将在连接的终端显示信息。请注意,并不是所有的合并策略都支持进度报告
–gpg-sign[=<keyid>]
-S[<keyid>]
GPG签名
-m <msg> 设置用于创建合并节点时的提交信息。如果指定了–log参数,那么commit节点的短日志将会附加在提交信息里
–[no-]rerere-autoupdate rerere即reuse recorded resolution,重复使用已经记录的解决方案。它允许你让 Git 记住解决一个块冲突的方法,这样在下一次看到相同冲突时,Git 可以为你自动地解决它
–abort 抛弃当前合并冲突的处理过程并尝试重建合并前的状态(未commit的文件可能不会正确回到合并前状态)
关于合并的其他概念
合并前的检测

在合并外部分支时,你应当保持自己分支的整洁,否则的话当存在合并冲突时将会带来很多麻烦。
为了避免在合并提交时记录不相关的文件,如果有任何在index所指向的HEAD节点中登记的未提交文件,git-pull和git-merge命令将会停止。

fast-forward合并

通常情况下分支合并都会产生一个合并节点,但是在某些特殊情况下例外。例如调用git pull命令更新远端代码时,如果本地的分支没有任何的提交,那么没有必要产生一个合并节点。这种情况下将不会产生一个合并节点,HEAD直接指向更新后的顶端代码,这种合并的策略就是fast-forward合并。

合并细节

除了上文所提到的fast-forward合并模式以外,被合并的分支将会通过一个合并节点和当前分支绑在一起,该合并节点同时拥有合并前的当前分支顶部节点和对方分支顶部节点,共同作为父节点。

一个合并了的版本将会使所有相关分支的变化一致,包括提交节点,HEAD节点和index指针以及节点树都会被更新。只要这些节点中的文件没有重叠的地方,那么这些文件的变化都会在节点树中改动并更新保存。

如果无法明显地合并这些变化,将会发生以下的情况:

  1. HEAD指针所指向的节点保持不变
  2. MERGE_HEAD指针被置于其他分支的顶部
  3. 已经合并干净的路径在index文件和节点树中同时更新
  4. 对于冲突路径,index文件记录了三个版本:版本1记录了二者共同的祖先节点,版本2记录了当前分支的顶部,即HEAD,版本3记录了MERGE_HEAD。节点树中的文件包含了合并程序运行后的结果。例如三路合并算法会产生冲突
  5. 其他方面没有任何变化。特别地,你之前进行的本地修改将继续保持原样。如果你尝试了一个导致非常复杂冲突的合并,并想重新开始,那么可以使用git merge --abort

三路合并算法

三路合并算法是用于解决冲突的一种方式,当产生冲突时,三路合并算法会获取三个节点:本地冲突的B节点,对方分支的C节点,B,C节点的共同最近祖先节点A。三路合并算法会根据这三个节点进行合并。具体过程是,B,C节点和A节点进行比较,如果B,C节点的某个文件和A节点中的相同,那么不产生冲突;如果B或C只有一个和A节点相比发生变化,那么该文件将会采用该变化了的版本;如果B和C和A相比都发生了变化,且变化不相同,那么则需要手动去合并;如果B,C都发生了变化,且变化相同,那么并不产生冲突,会自动采用该变化的版本。最终合并后会产生D节点,D节点有两个父节点,分别为B和C。

合并tag

当合并一个tag时,Git总是创建一个合并的提交,即使这时能够使用fast-forward模式。该提交信息的模板预设为该tag的信息。额外地,如果该tag被签名,那么签名的检测信息将会附加在提交信息模板中。

冲突是如何表示的

当产生合并冲突时,该部分会以<<<<<<<, ``=======>>>>>>>表示。在=======之前的部分是当前分支这边的情况,在=======`之后的部分是对方分支的情况。

如何解决冲突

在看到冲突以后,你可以选择以下两种方式:

  1. 决定不合并。这时,唯一要做的就是重置index到HEAD节点。git merge --abort用于这种情况。
  2. 解决冲突。Git会标记冲突的地方,解决完冲突的地方后使用git add加入到index中,然后使用git commit产生合并节点。

可以用以下工具来解决冲突:

  1. 使用合并工具。git mergetool将会调用一个可视化的合并工具来处理冲突合并。
  2. 查看差异。git diff将会显示三路差异(三路合并中所采用的三路比较算法)。
  3. 查看每个分支的差异。git log --merge -p \<path\>将会显示HEAD版本和MERGE_HEAD版本的差异。
  4. 查看合并前的版本。git show :1:文件名显示共同祖先的版本,git show :2:文件名显示当前分支的HEAD版本,git show :3:文件名显示对方分支的MERGE_HEAD版本。
合并策略
resolve

仅仅使用三路合并算法合并两个分支的顶部节点(例如当前分支和你拉取下来的另一个分支)。这种合并策略遵循三路合并算法,由两个分支的HEAD节点以及共同子节点进行三路合并。
当然,真正会困扰我们的其实是交叉合并(criss-cross merge)这种情况。所谓的交叉合并,是指共同祖先节点有多个的情况,例如在两个分支合并时,很有可能出现共同祖先节点有两个的情况发生,这时候无法按照三路合并算法进行合并(因为共同祖先节点不唯一)。resolve策略在解决交叉合并问题时是这样处理的,这里参考《Version Control with Git》:

In criss-cross merge situations, where there is more than one possible merge basis, the resolve strategy works like this: pick one of the possible merge bases, and hope for the best. This is actually not as bad as it sounds. It often turns out that the users have been working on different parts of the code. In that case, Git detects that it’s remerging some changes that are already in place and skips the duplicate changes, avoiding the conflict. Or, if these are slight changes that do cause conflict, at least the conflict should be easy for the developer to handle

这里简单翻译一下:在交叉合并的情况时有一个以上的合并基准点(共同祖先节点),resolve策略是这样工作的:选择其中一个可能的合并基准点并期望这是合并最好的结果。实际上这并没有听起来的那么糟糕。通常情况下用户修改不同部分的代码,在这种情况下,很多的合并冲突其实是多余和重复的。而使用resolve进行合并时,产生的冲突也较易于处理,真正会遗失代码的情况很少。

recursive

仅仅使用三路合并算法合并两个分支。和resolve不同的是,在交叉合并的情况时,这种合并方式是递归调用的,从共同祖先节点之后两个分支的不同节点开始递归调用三路合并算法进行合并,如果产生冲突,那么该文件不再继续合并,直接抛出冲突;其他未产生冲突的文件将一直执行到顶部节点。额外地,这种方式也能够检测并处理涉及修改文件名的操作。这是git合并和拉取代码的默认合并操作。
recursive合并策略有以下参数:
|参数|作用|
|—|—|
|ours|该参数将强迫冲突发生时,自动使用当前分支的版本。这种合并方式不会产生任何困扰情况,甚至git都不会去检查其他分支版本所包含的冲突内容这种方式会抛弃对方分支任何冲突内容|
|theirs|正好和ours相反。theirs和ours参数都适用于合并二进制文件冲突的情况|
|patience|在这种参数下,git merge-recursive花费一些额外的时间来避免错过合并一些不重要的行(如函数的括号)。如果当前分支和对方分支的版本分支分离非常大时,建议采用这种合并方式|
|diff-algorithm=[patience|minimal|histogram|myers]|告知git merge-recursive使用不同的比较算法。|
|ignore-space-change
ignore-all-space
ignore-space-at-eol|根据指定的参数来对待空格冲突:
如果对方的版本仅仅添加了空格的变化,那么冲突合并时采用我们自己的版本
如果我们的版本含有空格,但是对方的版本包含大量的变化,那么冲突合并时采用对方的版本
采用正常的处理过程|
|no-renames|该选项是subtree合并策略的高级形式,将会猜测两颗节点树在合并的过程中如何移动。不同的是,指定的路径将在合并开始时除去,以使得其他路径能够在寻找子树的时候进行匹配。(关于subtree合并策略详见下文)|
|octopus|这种合并方式用于两个以上的分支,但是在遇到冲突需要手动合并时会拒绝合并。这种合并方式更适合于将多个分支捆绑在一起的情况,也是多分支合并的默认合并策略|
|ours|这种方式可以合并任意数量的分支,但是节点树的合并结果总是当前分支所冲突的部分。这种方式能够在替代旧版本时具有很高的效率。请注意,这种方式和recursive策略下的ours参数是不同的|
|subtree|subtree是修改版的recursive策略。当合并树A和树B时,如果B是A的子树,B首先调整至匹配A的树结构,而不是读取相同的节点|

总结

在使用三路合并的策略时(指默认的recursive策略),如果一个文件(或一行代码)在当前分支和对方分支都产生变化,但是稍后又在其中一个分支回退,那么这种回退的变化将会在结果中体现。这一点可能会使一些人感到困惑。这是由于在合并的过程中,git仅仅关注共同祖先节点以及两个分支的HEAD节点,而不是两个分支的所有节点。因此,合并算法将会把被回退的部分认为成没有变化,这样,合并后的结果就会变为另一个分支中变化的部分。

示例
1
2
3
4
5
6
7
8
# 1. 合并分支fixes和enhancements在当前分支的顶部,使它们合并:
$ git merge fixes enhancements
# 2. 合并obsolete分支到当前分支,使用ours合并策略:
$ git merge -s ours obsolete
# 3. 将分支maint合并到当前分支中,但不要自动进行新的提交:
$ git merge --no-commit maint
# 4. 将分支dev合并到当前分支中,自动进行新的提交:
$ git merge dev

rebase

当执行rebase操作时,git会从两个分支的共同祖先开始提取待变基分支上的修改,然后将待变基分支指向基分支的最新提交,最后将刚才提取的修改应用到基分支的最新提交的后面。

比如:

  1. master: A–>B–>M
  2. feature: A–>B–>C–>D

在feature分支上执行git rebase master时,git会从master和featuer的共同祖先B开始提取feature分支上的修改,也就是C和D两个提交,先提取到。然后将feature分支指向master分支的最新提交上,也就是M。最后把提取的C和D接到M后面,注意这里的接法,官方没说清楚,实际是会依次拿M和C、D内容分别比较,处理冲突后生成新的C’和D’。一定注意,这里新C’、D’和之前的C、D已经不一样了,是我们处理冲突后的新内容,feature指针自然最后也是指向D’

合并本地多调提交记录

我们通常会通过 git rebase -i 命令,-i 参数表示交互(interactive),该命令会进入到一个交互界面中,其实就是 Vim 编辑器。

这个交互界面会首先列出给定之前(不包括,越下面越新)的所有 commit,每个 commit 前面有一个操作命令,默认是 pick。我们可以选择不同的 commit,并修改 commit 前面的命令,来对该 commit 执行不同的变更操作。

命令 目的
p,pick 不对该commit做任何处理
r,reword 保留该commit,但修改提交信息
s,squash 保留该commit,但会将当前commit与上一个合并
f,fixup 与squash相同,但不会保存commit信息
x,exec 执行其他shell命令
d,drop 删除该commit

除此之外,还需要注意以下几点:

  1. 删除某个 commit 行,则该 commit 会丢失掉。
  2. 删除所有的 commit 行,则 rebase 会被终止掉。
  3. 可以对 commits 进行排序,git 会从上到下进行合并。
分支合并

使用git rebase合并分支会导致节点先后顺序乱,谨慎使用。

可用于拉取公共分支最新代码(git pull -rgit pull --rebase)。这样的好处:提交记录会比较简洁。但缺点:rebase以后我就不知道我的当前分支最早是从哪个分支拉出来的.

推送代码绝对不要使用rebase,一旦提交历史信息错乱,不好追溯。

中断与继续 abort与continue

1
2
3
4
5
6
7
# 该命令仅仅在合并后导致冲突时才使用。git merge --abort将会抛弃合并过程并且尝试重建合并前的状态。但是,当合并开始时如果存在未commit的文件,git merge --abort在某些情况下将无法重现合并前的状态。(特别是这些未commit的文件在合并的过程中将会被修改时)
$ git merge --abort
$ git rebase --abort

# 解决冲突后,继续合并(提交)
$ git merge --continue
$ git rebase --continue

标签tag

tag 就是对某次 commit 的一个标识,相当于起了一个别名。分为两种:

  1. 轻量标签:只是某个 commit 的引用,可以理解为是一个 commit 的别名
  2. 附注标签:是存储在git仓库中的一个完整对象,包含打标签者的名字、电子邮件地址、日期时间以及其他的标签信息。它是可以被校验的,可以使用 GNU Privacy Guard (GPG) 签名并验证。

本地标签

创建标签
1
2
3
4
5
6
7
8
9
10
11
12
13
# 轻量标签
# 给当前的提交版本创建一个轻量标签
$ git tag [标签名]
# 给指定的提交版本创建一个轻量标签
$ git tag [标签名] [提交版本]

# 附注标签
# -a:annotated 的首字符,表示附注标签
# -m:指定附注信息
# 直接给当前的提交版本创建一个 【附注标签】
$ git tag -a [标签名称] -m [附注信息]
# 给指定的提交版本创建一个【附注标签】
$ git tag -a [标签名称] [提交版本号] -m [附注信息]
查看标签
1
2
3
4
5
6
7
8
9
10
11
12
# 查看所有标签列表
$ git tag

# 查看模糊匹配的标签列表
$ git tag -l [*标签名称筛选字符串*]
$ git tag --list [*标签名称筛选字符串*]

# 查看标签的信息,(轻量标签 和 附注标签 的信息是不一样的)
$ git show [标签名]

# 在提交历史中查看标签
$ git log --oneline --graph
删除标签
1
$ git tag -d [标签名称]

远程标签

推送标签到远程仓库
1
2
3
4
5
# 将指定的标签上传到远程仓库
$ git push origin [标签名称]

# 将所有不在远程仓库中的标签上传到远程仓库
$ git push origin --tags

注:默认情况下,git push 命令并不会把标签推送到远程仓库中,必须手动地将 本地的标签 推送到远程仓库中。

删除远程标签
1
2
3
$ git push origin  :regs/tags/[标签名称]

$ git push origin --delete [标签名称]

检出标签

检出标签的理解:是在这个标签的基础上进行其他的开发或操作。

检出标签的操作实质:是以标签指定的版本为基础版本,新建一个分支,继续其他的操作。

因此 ,就是新建分支的操作了。

1
$ git checkout -b [分支名称] [标签名称]

PR流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 1. Fork项目
$ git clone 项目地址
# 2. 创建本地分支
$ cd 项目目录 && git checkout -b 分支名称
# 3. 提交和PUSH
$ git commit -m "第一次PR" && git push
# 4. 创建PR(合并)
$ git checkout master && git pull && git merge 分支名称 && git push
# 5. 创建PR(发起合并请求)
# 在你的仓库页面上,通常会有一个 “Pull Requests” 或 “Merge Requests” 选项卡。点击进去后,通常会看到一个 New Pull Request(或 Create Merge Request)按钮。
# 创建新的 Pull Request.选择源分支、目标分支,填写标题和描述,发起Pull Request.
# 审查者收到通知: 一旦 Pull Request 被创建,相关的团队成员或项目维护者会收到通知。
# 代码审查: 审查者可以查看代码的更改、进行评论,并提出修改建议。通常,他们会逐行检查代码,确认更改是否符合项目的标准。
# 讨论和修改: 如果审查者提出修改建议,你可以在你的本地仓库中进行更改,并将其推送到远程分支,Pull Request 会自动更新。
# 一旦所有人都确认通过,并且没有其他问题,Pull Request 可以被合并到 master 分支。通常这可以在 Pull Request 页面上通过 Merge 或 Squash and Merge 按钮完成。

工作应用

1
2
3
4
5
1. git stash保存未提交的代码,并恢复本地仓库到修改前状态
2. git checkout master切换到主干分支,git pull拉取最新内容
3. git checkout 个人分支,通过git merge/rebase合并master到个人分支
4. git stash pop 将修改内容取出,git commit,git push 提交到个人分支
5. 合并个人分支到master分支(git merge,不要使用rebase)

如何保持分支和上游分支(upstream)同步?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1. 查看git状态
$ git remote -v
origin FORK分支地址(fetch)
origin FORK分支地址(push)

2. 指定上游地址,这里upstream名字可自定义,但一般都把上游分支叫做upstream
$ git remote add upstream 上游分支

3. 查看是否成功
$ git remote -v

4. 从上游分支拉取最新代码
$ git fetch upstream

5. 切换到自己fork分支
$ git checkout Fork分支

6. 把upstream获取到的更新内容合并到Fork分支
$ git merge upstream/Fork分支

7. 再提交进行PR

经验

  1. 多提交,少推送。多人协作时,推送会频繁地带来合并冲突的问题,影响效率。因此,尽量多使用提交命令,减少合并的使用,这样会节省很多时间。
  2. 使用Git流(Git Flow)
  3. 使用分支,保持主分支的整洁。这是我强烈推荐的一点,在分支进行提交,然后切到主分支更新(git pull —rebase),再合并分支、推送。这样的流程会避免交叉合并的情况出现(不会出现共同祖先节点为多个的情况)。事实上,git合并操作让很多人感到不知所措的原因就是各种原因所产生的交叉合并问题,从而造成在合并的过程中丢失某些代码。保持主分支的整洁能够避免交叉合并的情况出现。
  4. 禁用fast-forward模式。在拉取代码的时候使用rebase参数(前提是保持主分支的整洁)、合并的时候使用—no-ff参数禁用fast-forward模式,这样做既能保证节点的清晰,又避免了交叉合并的情况出现。

问题

push:HTTP 413 curl 22 The requested URL returned error: 413 Request Entity Too Large

使用ssh提交替换掉HTTP,如下:

1
2
3
$ git remote set-url origin git@github.com:GitRepoName.git
# 重新提交即可
$ git push

不同项目的合并

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
# 现在有两个仓库 [leader/kkt](https://www.leader755.com) (主仓库) 和 [leader/kkt-next](https://www.leader7555.com)(子仓库)
# 我们需要将 kkt-next 仓库合并到 kkt 并保留 kkt-next 的所有提交内容。

# 1. 克隆主仓库代码
$ git clone git@github.com:kktjs/kkt.git

# 2. 将 kkt-next(子) 作为远程仓库,添加到 kkt 中,设置别名为 other
$ git remote add other git@github.com:kktjs/kkt-next.git

# 3. 从 kkt-next(子) 仓库中拉取数据到本仓库
$ git fetch other

# 4. 将 kkt-next(子) 仓库拉取的 master 分支作为新分支 checkout 到本地,新分支名设定为 other
$ git checkout -b other other/master

# 5. 切换回 kkt(主) 的 master 分支
$ git checkout master

# 6. 更新本地分支
# 更新主仓库代码
# 先切换到master
$ git checkout master
# 从默认的远程仓库更新
$ git pull origin master


# 更新子仓库代码
# 先切换到other
$ git checkout other
# 从其他的远程仓库更新
$ git pull other master

# 7. 将 kkt-next(子) 合并入 kkt (主)的 master 分支
# 切换到主仓库的 master 分支
$ git checkout master
$ git merge other
# 如果报错 `fatal: refusing to merge unrelated histories`
# 请执行下面命令 ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
$ git merge other --allow-unrelated-histories
# 在合并时有可能两个分支对同一个文件都做了修改,这时需要解决冲突,对文本文件来说很简单,根据需要对冲突的位置进行处理就可以。
# 对于二进制文件,需要用到如下命令:
$ git checkout --theirs YOUR_BINARY_FILES # 保留需要合并进来的分支的修改
$ git checkout --ours YOUR_BINARY_FILES # 保留自己的修改
$ git add YOUR_BINARY_FILES

参考文章

您的支持是对我最大的动力 (●'◡'●)