# 前言

Git 是程序员必备技能，在能熟练使用 Git GUI 客户端之后（如 [SoureTree](https://www.sourcetreeapp.com/)、[Github Desktop](https://desktop.github.com/)，或者是 VSCode 的 git 面板或 [GitLens](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens) 插件），也许你希望能了解到更多关于 Git 的知识点，并用于解决实际项目遇到的问题。

《Git 进阶指南》聚焦于介绍那些 Git 中容易被忽视，而项目中又时常会遇到的知识点，同时又保持足够的精简。

**在这本书中你能学到：**

* 大量提升操作效率的 git alias
* git stash / tag / submodule 等如章节所示知识点
* 项目中回滚、解决冲突的最佳实践
* 常见的配置问题

**本书不包含的内容：**

* 基础的 Git 命令使用
* Git 文件类型与版本实现原理
* 强大而又复杂的 `git rebase -i` 操作
* Git Hooks / CI 的使用

## 推荐资料

虽然本书不包含那些过于简单、过于理论和过于复杂的知识点，但以下这些参考资料或许会对你有所帮助：

* [Learn Git Branching](https://learngitbranching.js.org) 知名的 Git 教学游戏，通过可视化的动画和交互，帮助理解所有分支操作的概念和方法。如果你打算自学，这些命令会用得上：`show solution`、`levels`、`undo`、`reset`。
* [Pro Git](https://git-scm.com/book/zh/v2) 专业全面的 git 教程


# Git SSH Keys 配置

在执行 git pull / push 时，需要相应的用户权限，如果使用远程仓库的 HTTPS 地址，则每次同步都需要输入用户名密码。

**因此，从安全性和便利性的角度考虑，应当始终使用远程仓库的 SSH 地址。**

![Clone with SSH](/files/-LXhEIsqyNYA22CUPBoe)

这就要求我们进行 SSH Keys 的配置。SSH 密钥对包含一组「私钥」和「公钥」，私钥存在用户本地，公钥存放在远程主机。

Git 服务提供商（Github、Gitlab 等）会在用户设置界面提供一个录入用户公钥的入口，我们只需将生成好的公钥提交保存上去，即可实现免密 git pull / push。

## 1、生成 SSH 私钥、公钥对

```
# 运行密钥生成命令，一路回车
ssh-keygen
```

注：`ssh-keygen` 命令有诸多参数用法，如果你使用 `-f` 参数项指定了密钥对的名称，那么在 macOS 上还需要使用 `ssh-add` 命令将密钥对加入 SSH agent 中，或者在 SSH config 文件中配置主机和密钥的使用。

```
ssh-keygen -f ~/.ssh/github
ssh-add -K ~/.ssh/github
```

## 2、复制公钥信息

SSH 默认私钥地址为：`~/.ssh/id_rsa` ，对应的公钥为：`~/.ssh/id_rsa.pub`，使用编辑器或 `cat` 命令输出公钥内容拷贝至剪贴板，然后将公钥粘贴至对应的配置后台。如，Github 的地址为 <https://github.com/settings/keys>

```
cat ~/.ssh/id_rsa.pub
```

## 3、测试 SSH 是否连接成功

```
# 这里以 Github 服务为例
ssh -T git@github.com
```


# Git Alias Cheat Sheet

## 1、设置 git log alias

`git log` 是一个十分强大的命令，使用以下命令可以查看每次提交的文件修改列表，和分支变化的图形信息。

```
git log --name-status --oneline --graph
```

但作为一个地球人，我们是很难记住这些复杂选项的，查手册也是相当费时，这里就需要用到 `git alias` 的用法。我们可以通过以下语句，配置出一个 `git ls` 的命令，以实现相同的效果。

```
git config --global alias.ls 'log --name-status --oneline --graph'

# 运行新命令即可查看效果
git ls
```

## 2、ZSH Git Plugin Alias

如果你使用了 `zsh`（参考《[全新 Mac 安装指南（编程篇）](https://github.com/kaiye/kaiye.github.com/issues/2)》 ），那么开启 [zsh git plugin](https://github.com/robbyrussell/oh-my-zsh/wiki/Plugin:git) 后，你将会直接获得一大群好用的 git alias。

| Alias       | Command                                                                                                  | Remark                               |
| ----------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------ |
| `grv`       | `git remote -v`                                                                                          | 查看远程分支列表                             |
| `gf`        | `git fetch`                                                                                              |                                      |
| `gl`        | `git pull`                                                                                               |                                      |
| `gup`       | `git pull --rebase`                                                                                      |                                      |
| `gupa`      | `git pull --rebase --autostash`                                                                          |                                      |
| `glum`      | `git pull upstream master`                                                                               |                                      |
| `grb`       | `git rebase`                                                                                             |                                      |
| `gm`        | `git merge`                                                                                              |                                      |
| `gba`       | `git branch -a`                                                                                          |                                      |
| `gsu`       | `git submodule update`                                                                                   | 更新子仓库                                |
| `gst`       | `git status`                                                                                             |                                      |
| `gaa`       | `git add -all`                                                                                           | 将当前版本库所有改动提交至暂存区                     |
| `gp`        | `git push`                                                                                               |                                      |
| `ggp`       | `git push origin "${*}"`                                                                                 | 可指定分支或标签名，默认当前分支                     |
| `ggsup`     | `git branch --set-upstream-to=origin/$(git_current_branch)`                                              | 当前分支与远程同名分支关联                        |
| `gcmsg`     | `git commit -m`                                                                                          |                                      |
| `gco`       | `git checkout`                                                                                           |                                      |
| `gcb`       | `git checkout -b`                                                                                        | 从当前分支新建并转向新分支                        |
| `glol`      | `git log --graph --pretty='%Cred%h%Creset -%C(auto)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset'` | 格式化版本号、提交时间、作者信息的日志                  |
| `gwip`      | `git add -A; git rm $(git ls-files --deleted) 2> /dev/null; git commit --no-verify -m "--wip--"`         | 将当前新增与改动的文件做一次临时提交                   |
| `gunwip`    | `git log -n 1 \| grep -q -c "--wip--" && git reset HEAD~1`                                               | 从上一次 gwip 恢复                         |
| `grhh`      | `git reset HEAD --hard`                                                                                  |                                      |
| `gclean`    | `git clean -df`                                                                                          |                                      |
| `gpristine` | `git reset --hard && git clean -dfx`                                                                     | 注意 .gitignore 里的 node\_modules 也会被清除 |
| `gsta`      | `git stash save`                                                                                         |                                      |
| `gstl`      | `git stash list`                                                                                         |                                      |
| `gstp`      | `git stash pop`                                                                                          |                                      |
| `gtv`       | `git tag \| sort -V`                                                                                     |                                      |
| `gdct`      | `` git describe --tags `git rev-list --tags --max-count=1` ``                                            | 当前最近 tag 版本信息                        |
| `gca!`      | `git commit -v -a --amend`                                                                               | 将当前修改追加至上一次提交，并修改提交记录                |

**注：本书所有章节 git 命令行代码，会在行内注释中标注对应的 zsh alias 。**


# origin 与 upstream

Git 是一个去中心化的版本管理工具，这意味着一个 Git 仓库，既可以没有远程仓库，也可以有多个远程仓库，其中默认的远程仓库我们称之为 `origin`。

在 Github 上，我们可以 fork 任意开源项目到个人 ID 下，当我们把个人 ID 下的仓库 clone 到本地时，除了默认的 origin 仓库外，我们还需要配置一个 upstream 仓库，指向原始的开源项目地址，保证可以定期同步源仓库的更新。

注：upstream 只是一个命名约定，你可以换成任意的命名。

## 1、查看远程仓库列表

```
# 查看远程仓库列表
git remote -v           # grv

# 查看具体仓库细节
git remote show origin  # gr show origin
```

## 2、添加 upstream 仓库信息

```
git remote add upstream <git_repository_url> # gra upstream
```

## 3、删除 upstream 仓库信息

```
git remote remove upstream # grrm upstream
```


# Git Stash 与 gwip

## 1、如何在不提交修改的前提下，执行 pull 或分支切换的操作？

我见过最圡方法是，将修改的文件 copy 到 git 仓库之外的目录临时存放，pull 或分支切换操作完成之后，再 copy 回来。

**拷贝覆盖文件的做法，一是效率不高，二是可能会遗漏潜在的冲突。**

事实上，如果你的 git 版本大于 2.10，那么只需以下一行命令就能自动完成 git stash -> git pull -> git stash pop 的操作。

```
git pull --rebase --autostash # gupa
```

## 2、git stash

在当前任务未完成前，我们可能需要执行本地与远程分支的同步或本地不同分支的 merge 操作。这时候如果将未完成修改的文件直接 commit，会导致 log 中记录许多无效的 commit 和 merge 记录。

git stash 可以将当前工作状态（WIP，work in progress）临时存放在 stash 列表中，待 pull / merge 操作完成后，再从 stash 中重新应用这些修改。

以下是 git stash 常用命令：

```
# 查看 stash 列表中已暂存了多少 WIP
git stash list              # gstl
​
# 添加当前 WIP
# -u 参数表明新增的文件也一起 stash
git stash -u save 'message' # gsta -u

# 恢复上一次的 WIP 状态，并从列表中移除
git stash pop               # gstp

# 恢复指定编号的 WIP，同时从列表中移除
git stash pop stash@{num}   # gstp 0
​
# 恢复指定编号的 WIP，但不从列表中移除
git stash apply stash@{num} # gstaa 0
```

**注：如果 git stash 的文件刚好被其他提交修改过了，那么在 pop 时会自动将修改过的文件标记为 conflict 状态。需要注意的是，pop 出来的文件，是 --theirs 而非 --ours。**

## 3、gwip 与 gunwip

Stash 的记录只保存在本地，如果需要在不同电脑间（例如公司和家里）同步临时性的修改，则无法通过 git stash 完成。

Git 社区中有一条约定是，在 commit 中通过 **wip** 关键词表示，这是一次 **work in progress** 的临时提交。借助 zsh git alias，我们可以通过一组 `gwip` 和 `gunwip` 命令来实现同步。

其实现原理是，gwip 提交了特定的 commit message（即，`--wip-- [skip ci]`），gunwip 判断如果上一次提交为 gwip 提交，则自动进行 reset，以下是 alias 的细节。

```
alias gunwip='git log -n 1 | grep -q -c "\-\-wip\-\-" && git reset HEAD~1'
alias gwip='git add -A; git rm $(git ls-files --deleted) 2> /dev/null; git commit --no-verify -m "--wip-- [skip ci]"'
```


# Git Tag 与 Describe

Git tag 常用于发布版本的标注，可以理解为 tag 是对某一次 commit hash 的别名设置。需要注意的是，git tag 的同步与删除，需要显式地指定其名称。

## 1、查看 tag 列表

```
# 列出所有本地 tag
git tag
​
# 过滤出 v2. 开头的 tag
git tag -l 'v2.*'
​
# 使用管道输出 tag 列表，避免进入 visual 模式
git tag | sort -V # gtv
```

## 2、新增 tag

Tag 包含两种，一种是轻量 tag，只设置别名，另外一种为附注 tag，能包含这个 tag 的作者和额外的备注信息。

```
# 创建一个名为 v0.0.1 的轻量 tag
git tag v0.0.1
​
# 创建一个名为 v1.0.0 的附注 tag
git tag -a v1.0.0 -m 'tag description'
​
# 将 tag 推送至远程
git push origin v1.0.0 # ggp v1.0.0
```

## 3、删除 tag

```
# 删除本地 tag
git tag -d v1.0.0
​
# 删除远程 tag
git push origin -d v1.0.0 # gp origin :v1.0.0
```

## 4、语义化版本与 git describe

清晰的 tag 版本信息，不但有助于发布版本的选择和回滚，同时能十分方便的查看到分支的版本提交进度。

```
git describe master --tags
```

假如以上命令输出为：v1.1.0-kaiye-gitbook-2-g51e992f

它表明，本地 master 分支当前 commit hash 为 `51e992f`，距离上一次的 tag 版本 `v1.1.0-kaiye-gitbook` 之间已有 `2` 次提交。

如果需要知道本地与远程最近发布的一次 tag 版本是什么，直接运行 `gdct` 即可。

```
alias gdct='git describe --tags `git rev-list --tags --max-count=1`'
```


# Subtree 与 Submodule

subtree 和 submodule 的目的都是用于 git 子仓库管理，二者的主要区别在于，subtree 属于拷贝子仓库，而 submodule 属于引用子仓库。

## 1、Subtree vs Submodule

| 维度     | subtree                                                                              | submodule                                                                         | 优劣对比         |
| ------ | ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------- | ------------ |
| 空间占用   | subtree 在初始化 add 时，会将子仓库 copy 到父仓库中，并产生至少一次 merge 记录。所以会占用大量父仓库空间                    | submodule 在初始化 add 时，会在父仓库新建一个 .gitmodules 文件，用于保存子仓库的 commit hash 引用。所以不会占用父仓库空间 | submodule 更优 |
| clone  | subtree add 至父仓库之后，后续的 clone 操作与单一仓库操作相同                                             | 后续 clone 时 submodule 还需要 init/update 操作，且 submodule 子仓库有自己的分支                     | subtree 更优   |
| update | 子仓库更新后，父仓库需要 subtree pull 操作，且命令行略长，需要指定 --prefix 参数。由于无法感知子仓库的存在，可能会产生 merge 冲突需要处理 | 子仓库更新后，父仓库需要 submodule update 操作。父仓库只需变动子仓库 hash 引用，不会出现冲突                        | submodule 更优 |
| commit | 父仓库直接提交父子仓库目录里的变动。若修改了子仓库的文件，则需要执行 subtree push                                      | 父子仓库的变动需要单独分别提交。且注意先提交子仓库再提交父仓库                                                   | subtree 更优   |

## 2、Subtree 命令行简化

subtree 在操作时，命令行较长，可以使用 remote 配置简化，例如

```
# 以下为标准 subtree add 命令行示例
git subtree add --prefix=centos-config --squash git@github.com:kaiye/centos-config.git master
​
# 可以简化为
# 1. 先为远程子仓库配置一个别名，便于后续的 pull 与 push 操作，这里例子以 centos 为别名
git remote add centos git@github.com:kaiye/centos-config.git # gra centos ...
​
# 2. 其中 --prefix= 简写为 -P，配置 --squash 表示不拉取子仓库的历史提交记录
git subtree add -P centos-config --squash centos master
​
# 后续更新子仓库可以使用
git subtree pull -P centos-config centos master
​
# 若发生 fatal: refusing to merge unrelated histories 报错，加上 --squash 参数即可
```

## 3、git submodule update 出错解决方案

假如在执行 git submodule update 时出现以下类似错误信息：

```
fatal: reference is not a tree: f869da471c5d8a185cd110bbe4842d6757b002f5
Unable to checkout 'f869da471c5d8a185cd110bbe4842d6757b002f5' in submodule path 'centos-config'
```

发生错误的原因是，centos-config 子仓库在某电脑 A 的「本地」commit 了新的版本 「f869da471c5d8a185cd110bbe4842d6757b002f5」，且该次 commit 未 push origin。但其父级仓库中引用了该子仓库的版本号，且将引用记录 push origin，导致其他用户无法 update 。

解决方案是，在电脑 A 上将子仓库 push origin 后，在其他客户机上执行 git submodule update 。或者使用 git reset，将子仓库的引用版本号还原成 origin 上存在的最新版本号。


# Cherry Pick 的使用场景

当你需要将某些特定的 commit，合并进当前分支时，可以使用该命令。

**cherry-pick 时，极其容易发生冲突。一般来说，如果项目中使用了规范的 git 分支操作流，那么基本上不会用到 cherry-pick。**

但如果出现了以下三种情况，则可能会使用到。

* 需求发布窗口发生变更，某个特性需要从版本中单独拎出来，放入另外的迭代中；
* 同一分支中某些特性改错了，但提交更新次数过多，需要选中某些历史提交，再次合并至 head；
* 项目最开始的基准分支切错了，后续需要重新切基准分支，并将错误分支上的提交记录移植到正确分支上。

```
# cherry-pick 指定 commit，可同时传入多个 commit
git cherry-pick <commit_hash>   # gcp
​
# 放弃 cherry-pick
git cherry-pick --abort         # gcpa
​
# cherry-pick 多个 commit 时，解决冲突后继续下次合并
git cherry-pick --continue      # gcpc
```


# 如何配置多个提交用户？

## 1、git config user

假如你在 Github 上有个人项目，而公司项目存放在公司内网 gitlab 上，如何为这两种不同类型的项目配置不一样的提交用户呢？

Git 全局的用户配置文件存放在 `~/.gitconfig` 中，我们可以通过以下命令设置全局默认的用户名和邮箱。

```
git config --global user.name "<Your Name>"
git config --global user.email "<Your Email>"
```

如果需要为某一个项目配置不同的用户，则可以进入到项目对应的目录下，使用以下命令。它会在项目根目录的 `.git/config` 文件中保存这些配置信息：

```
git config user.name "<Another Name>"
git config user.email "<Another Email>"
```

## 2、Conditional Includes

在 git 2.13 版本中，增加了 [conditional includes](https://git-scm.com/docs/git-config#_includes) 配置，可以创建多个 gitconfig 文件，并针对不同的根目录使用不同的配置文件。例如，以下全局配置文件 `~/.gitconfig` 中包含以下用户配置信息，当项目 clone 在 `~/dev/` 目录下时，会自动使用另外一份配置文件：

```
[user]
    name = Your Name
    email = your_email@example.com
[includeIf "gitdir:~/dev/"]
    path = .gitconfig-dev
```

以下是 `~/.gitconfig-dev` 文件的配置：

```
[user]
    name = Another Name
    email = another_email@example.com
```

**注意：强烈建议将 global user 配置为你的 Github ID，避免默认提交中有效用户信息的缺失，导致 Github contributions 不准确。同时，不设置为公司邮箱，可以避免邮箱信息泄露。**


# 如何撤销修改？

修改包含四种情况，需单独区分。

## 1、新建的文件和目录，且从未提交至版本库

此类文件的状态为 `Untracked files` ，撤销方法如下：

```
git clean -fd . # gclean .
```

其中，`.` 表示当前目录及所有子目录中的文件，也可以直接指定对应的文件路径，以下其他情况类似。

## 2、提交过版本库，但未提交至暂存区的文件（未执行 git add）

此类文件的状态为 `Changes not staged for commit`，撤销方法：

```
git checkout . # gco .
```

## 3、已提交至暂存区的文件

此类文件的状态为 `Changes to be committed`，撤销方法：

```
git reset . # grh .
```

执行之后文件将会回到以上的 1 或者 2 状态，可继续按以上步骤执行撤销，若 git reset 同时加上 `--hard` 参数（alias 为 `grhh`），将会把修改过的文件也还原成版本库中的版本。

## 4、已提交至版本库（执行了 git commit）

每次提交都会生成一个 commit hash，通过以下命令可查阅 hash id 并将其回滚：

```
git log # glol
git reset <commit_hash> # grh
```

如果需要「回滚至上一次提交」，可直接使用以下命令：

```
git reset head~1 # grh head~1
```

执行之后，再按照 1 或者 2 状态进行处理即可，如果回滚之后的代码同时需要提交至 origin 仓库（即回滚 origin 线上仓库的代码），需要使用 `-f` 强制提交参数，且当前用户需要具备「强制提交的权限」。

## 5、如果回滚了之后又不想回滚了怎么办？

如果是以上的情况 1 或者 2，只能歇屁了，因为修改没入过版本库，无法回滚。

如果是情况 4，回滚之后通过 git log 将看不到回滚之前的 commit hash，但可通过 `git reflog` 命令（所有记录过的 commit hash）找到回滚之前的 commit hash，然后 `git reset <commit_hash>` 。

## 6、git reset 与 revert 的区别

二者都是做回滚操作，不同之处在于：

* `reset` 直接将 head 指向了回滚的记录，丢弃了这条记录之后所有的提交时间线；
* `revert` 更类似 `cherry-pick` 的反向操作，它不会改变历史时间线。在选中某次特定的历史提交后，它在时间上新增一条提交记录，还原了那次特定提交的所有变更。


# 遇到冲突了怎么解决？

两个分支进行合并时，可能会遇到冲突，同时被修改的文件会进入 `both modified` 状态，需要解决冲突。

## 1、最快的办法

大部分时候，「最快解决冲突」的办法是：直接选择当前 HEAD 的版本（ours），或合并进来的分支版本（theirs）。

```
# 使用当前分支 HEAD 版本，冲突源文件的 <<<<<<< 标记部分，======= 的上方
git checkout --ours <文件名> # gco --ours
​
# 使用合并分支版本，冲突源文件的 >>>>>>> 标记部分
git checkout --theirs <文件名>
​
# 标记为解决状态加入暂存区
git add <文件名> # ga
```

## 2、最普通的办法

你可以直接用编辑器打开冲突的源文件进行修改，但需要注意删除冲突标记，你也可以使用体验更加友好的 Three-Way Merge 工具，借助 `git mergetool` 命令来完成。

如何配置 git merge tool？请参见后续章节。

## 3、好的习惯

以下好的习惯，可以减少代码的冲突的发生：

* 在开始修改代码前先 `git pull` 一下；
* 将业务代码进行划分，尽量不要多个人在同一时间段修改同一文件；
* 通过 [Gitflow 工作流](http://www.ituring.com.cn/article/56870) 也可以提升 git 流程效率，减少发生冲突的可能性。

## 4、最复杂的情况

如果你的项目周期比较长，还应该养成「定期 rebase 的习惯」，`git pull --rebase` 可以让分支的代码和 origin 仓库的代码保持兼容，同时还不会破坏线上代码的可靠性。

它的大概原理是，先将 origin 仓库的代码按 origin 的时间流在本地分支中提交，再将本地分支的修改记录追加到 origin 分支上。如果发生冲突，则可以即时的发现问题并解决，否则到项目上线时再解决冲突，可能会发生额外的风险。

rebase 大概的操作步骤如下：

```
# 将当前分支的版本追加到从远程 pull 回来的节点之后
git pull --rebase       # gup
​
# 若发生冲突，则按以上其他方法进行解决，解决后继续
git rebase --continue   # grbc
​
# 直到所有冲突得以解决，待项目最后上线前再执行
git push origin         # ggp
​
# 若多次提交修改了同一文件，可能需要直接跳过后续提交，按提示操作即可
git rebase --skip       # grbs
```

需要注意的是，rebase 默认以 [bog-standard flattening](https://stackoverflow.com/questions/11863785/make-git-pull-rebase-preserve-merge-commits/11863842) 模式将提交的时间线进行扁平化处理，如果希望保留两个版本的合并记录，可以使用 `git pull --rebase=preserve` 参数，或直接将该参数设置为全局配置：

```
git config --global pull.rebase preserve
```


# 如何配置 git merge tool？

当文件进入冲突状态时，可以配置一个好用的 git merge tool 来解决冲突。

当前版本的 vscode 虽然能标注 git conflict 状态，但还未能实现 three-way merge 功能（#[5770](https://github.com/Microsoft/vscode/issues/5770)），文本以老牌对比工具 [Beyond Compare](http://www.scootersoftware.com/support.php?zz=kb_vcs_osx#git) 为例，介绍如何配置 git merge tool。

## 1、命令行中的配置

首先从 Beyond Compare 菜单中选择 **Install Command Line Tools**，然后运行以下配置命令：

```
git config --global merge.tool bc3
git config --global mergetool.bc3 trustExitCode true
git config --global mergetool.keepBackup false # 关闭生成 .orig 备份文件
```

设置完成后，即可使用 Beyond Compare Text Merge 功能打开待解决冲突的文件：

```
git mergetool <文件名>
```

## 2、Three-way Merge 界面

在 Beyond Compare 的 merge 界面中，「上左」与「上右」面板为合并发生冲突的两次提交版本，「上中」为前次历史版本，「下方」为合并后的代码。

「下方」面板中，「红色！」表示当前行代码发生了冲突，点击「蓝色⇨」或「紫色⇨」，可以分别选择上方需要的代码。合并完成后，保存并关闭窗口，文件会被自动标记为已解决状态，添加至暂存区。

![Beyond Compare Merge](/files/-LXiOcFI_vLOYU39arz2)

## 3、Source Tree 中的配置

打开 Source Tree，配置外部对比、合并工具为 Beyond Compare。

![](/files/-LXiOcFKtiYTVQRzGnZn)

在 SourceTree 中右键选中冲突的文件，使用指定工具解决冲突

![](/files/-LXiOcFMYkmG0DpUS6-h)


# fetch pull merge rebase 的关系

## 1、我应该用 git pull 还是 git fetch？

`pull` 与 `fetch` 的关系是：

```
git pull = git fetch + merge
git pull --rebase = git fetch + rebase
```

## 2、merge 和 rebase 有什么区别？

如果有疑问，推荐去玩一下本书前言提到的 [Learn Git Branching](https://learngitbranching.js.org) 游戏。

主要的差异是：

* rebase 改变了时间线，而 merge 没有；
* rebase 扁平化时间线后，历史记录变得更加简单和直观。

**二者的选择只是个人倾向性问题，底线建议是，不要改变 origin 仓库的时间线，避免找不到背锅侠。**


# 分支操作流示例

![来源 https://yq.aliyun.com/articles/280233](/files/-LXiObj-Fs97F0c-xkRy)

```
# 查看所有本地与远程分支
git branch -a               # gba
​
# 新建分支 feature/v1，并切换过去
git checkout -b feature/v1  # gcb feature/v1
​
# 在本地 feature/v1 分支上，拉取远程 master 分支合并进来
git pull origin master      # ggl master
​
# 开发过程中将 feature/v1 分支推至 origin 同名分支
git push origin feature/v1  # ggp
​
# feature/v1 开发完成后，切换回 master 分支，将其修改合并进来
git checkout master         # gco master
git merge feature/v1        # gm feature/v1
​
# 在 master 分支上，删除已完成合并的分支 feature/v1
git branch -d feature/v1    # gbd feature/v1
​
# 删除远程 feature/v1 分支
git push origin --delete feature/v1 # gp -d origin feature/v1
```


# 其他问题汇总

## 1、解决文件名中文乱码

git 默认将非 ASCII 编码的目录与文件名以八进制编码展示，使用以下命令可支持中文目录与文件名的显示。

```
git config --global core.quotepath off
```

## 2、set-upstream 同步

设置本地分支与远程分支保持同步，在第一次 git push 的时候带上 `-u` 参数即可，或使用 `ggsup` alias 进行关联。

```
git push origin master -u # gp -u
```

## 3、怎样修改 commit message？

如果你需要批量修改历史时间线中的记录，最好的办法是使用 `git rebase -i`，这里有份详细的指南《[Git-工具-重写历史](https://git-scm.com/book/zh/v2/Git-工具-重写历史)》。

如果只想修改上一次的提交的日志信息，使其符合团队规范（如社区广泛使用的 [AngularJS Git Commit Message Conventions](https://gist.github.com/stephenparish/9941e89d80e2bc58a153) 或 [gitmoji](https://github.com/carloscuesta/gitmoji/)），可使用

```
git commit --amend # gc!
```

## 4、操作符 ^ 和 \~ 什么区别？

二者都是向上回溯提交记录的位置选择符，默认值为 1。`^` 用于分支路径选择，`~` 用于提交记录选择。例如 `git checkout v1^~^` 等同于：

```
git checkout v1 # gco v1
git checkout head~3 # gco head~3
```

一般来说，记住 `head~N` 之类的用法即可，它表示当前 head 指针往回移 N 位。

## 5、git push origin :master 是个什么鬼操作？

喔，要小心了，这个操作有点类似 `rm -rf /` ，用于删除远程 master 分支。原因是 push 命令中的 `:` 参数分隔符前面可以选任意分支，如果留空，则表示删除的意思。

```
# 表示将本地 dev 分支的前一个版本推至远程 master
git push origin dev^:master
```

所以一个好的习惯是，在 git 后台设置 protect 好 master，并且在 push 时，不要使用强制参数 `-f`。

## 6、如何快速生成 .gitignore 文件？

```
# 安装 gi 命令，来自 https://github.com/joeblau/gitignore.io
echo "function gi() { curl -L -s https://www.gitignore.io/api/\$@ ;}" >> ~/.zshrc && source ~/.zshrc

# 往项目中追加 node ignore file
gi node >> .gitignore
```


