> For the complete documentation index, see [llms.txt](https://lizh.gitbook.io/knowledge/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://lizh.gitbook.io/knowledge/tools/0101git-ji-chu-jiao-cheng.md).

# 01 Git基础教程

Git 是用 C 语言写成的开源**分布式版本控制系统**，由 Linus Torvalds 在 2005 年为管理 Linux 内核开发而创建。它记录项目的每一次快照，能查看历史、回到过去、开分支并行开发，并在多人之间同步改动。

CVS、SVN 是集中式的：版本库放在中央服务器，离线几乎干不了版本管理；Git 把完整历史放在每个人的本地仓库里，联网主要用于和别人交换提交。

## 为什么使用版本控制

开发很少是一次写完、再也不改。个人需要备份和回溯，团队需要合并彼此的改动。最原始的办法是：复制一份代码、标上版本号、压缩后丢到硬盘。版本一多，磁盘会被占满，也难以对比差异、回退，更谈不上多人同时改同一批文件。

版本控制系统把「某次改动」做成可检索的记录：随时看旧内容、随时回到某次提交，合并时由工具处理大部分冲突，不必靠共享文件夹或定期打包。

早期开源集中式系统里，CVS 免费，但提交文件会不完整，版本库也容易损坏。SVN 修了 CVS 的不少稳定性问题，是集中式里用得最广的一种。今天新项目几乎都用 Git；遇到仍在跑的 SVN 仓库，差别见文末对比。

## Git 的诞生

1991 年 Linus 发布 Linux 内核。内核靠全球志愿者一起写，代码必须有办法汇总。

2002 年以前，志愿者把补丁以 diff 发给 Linus，由他手工合并。CVS、SVN 这类集中式系统速度慢、还得联网；商用系统更好用，但收费，和 Linux 的开源方式不合。内核已经发展十年，手工合并撑不住，社区也强烈不满。Linus 改用商业系统 BitKeeper，BitMover 公司授权 Linux 社区免费使用。

2005 年局面被打破：开发 Samba 的 Andrew Tridgell 等人尝试破解 BitKeeper 协议，被 BitMover 发现后收回了免费使用权。Linus 没有去道歉、换回集中式工具，而是用大约两周时间用 C 写出了 Git。一个月内，Linux 内核源码已经交由 Git 管理。

2008 年 GitHub 上线，为开源项目提供免费的 Git 托管，jQuery、PHP、Ruby 等大量项目迁过去。之后 GitLab、Gitee 等同类托管也普及开来。收回 BitKeeper 授权，直接促成了今天这个免费、离线也能用的 Git。

## 集中式与分布式

集中式版本控制系统里，**版本库集中放在中央服务器**。每人电脑上只有一份工作目录（工作区）。改完代码后定期提交到服务器；更新时再从服务器取回别人的提交。当前版本和历史都在服务器上。

![集中式版本控制](https://my-files-1259410276.cos.ap-chengdu.myqcloud.com/md_images/%E9%9B%86%E4%B8%AD%E5%BC%8F%E7%89%88%E6%9C%AC%E6%8E%A7%E5%88%B6.jpg)

分布式版本控制系统**不依赖必须在线的中央服务器**。每人同时拥有：

* 工作区：当前正在改的文件；
* 本地版本库：该项目的提交、分支、标签等完整历史（在 `.git` 里）。

改动先提交到**自己的**本地仓库，别人不会自动看到。要用 `fetch`、`pull`、`push` 在仓库之间传送提交。协作时通常约定一台机器或托管服务当「总仓库」，大家向它推、从它拉，避免每人向其他所有人各推一次。这个总仓库只是交换提交的便利点，不是版本管理本身的前提。

![分布式版本控制](https://my-files-1259410276.cos.ap-chengdu.myqcloud.com/md_images/%E5%88%86%E5%B8%83%E5%BC%8F%E7%89%88%E6%9C%AC%E6%8E%A7%E5%88%B6.jpg)

集中式的主要缺点：**必须联网才能做版本操作，大文件上传慢**。开工前先从服务器取最新版，做完再传回去。

分布式的特点：**远程可有可无**。

优点主要是：

* **离线也能工作**：提交、分支、看历史都在本地完成；
* **副本多、更耐删**：远程仓库删了，可以从任意一个完整的本地仓库再复制回去，历史是等价的。

## Git 安装和基本配置

**安装：**

* Windows：[Git for Windows](https://gitforwindows.org/)
* macOS：`xcode-select --install`，或用 Homebrew 安装 `git`
* Linux：用发行版包管理器安装，例如 `sudo apt install git`

Git 用 `git config` 读写配置。

配置决定 Git 在各环节怎么工作，分三层，**近的覆盖远的**：

* **系统级 `/etc/gitconfig`**：对本机所有用户生效。`git config --system` 读写这一层。Windows 上对应 Git 安装目录里的 `etc/gitconfig`（常见为 `C:\Program Files\Git\etc\gitconfig`）。
* **全局 `~/.gitconfig`**：只对当前用户生效。`git config --global` 读写这一层。也可以是 XDG 路径 `~/.config/git/config`。Windows 用户主目录是 `C:\Users\<用户名>`，不是 `C:\User\`。
* **仓库级 `.git/config`**：只对当前仓库生效。`git config --local` 读写这一层。

因此 `.git/config` 里的同名项会盖过全局和系统配置。

```bash
git config -l
git config --list --show-origin

git config --system -l
git config --global -l
# 常见项示例：
# user.email=用户电子邮箱
# user.name=用户名
# core.quotepath=false
# core.autocrlf=true
# gui.encoding=utf-8
# i18n.commitencoding=utf-8
# i18n.logoutputencoding=utf-8
git config --local -l
git config --global user.name
```

```bash
# 修改配置
git config --global user.email 'newEmail@example.com'

# 删除配置
git config --global --unset user.email
```

`user.name`、`user.email` 会出现在每次提交里，需要先配好。Windows 上 `core.autocrlf=true` 表示检出成 CRLF、提交时转成 LF，减少跨系统换行符冲突；Linux、macOS 上常用 `input` 或 `false`。

新仓库的默认分支名可用 `init.defaultBranch` 统一。GitHub 等托管上常见 `main`；本地未配置时，Git 2.x 仍经常是 `master`。下文示例两种都会出现，以仓库里实际分支名为准：

```bash
git config --global init.defaultBranch main
```

**配置编辑器：** Git 需要你写提交说明时打开的编辑器。未配置时用系统默认，Linux、macOS 上常见是 vi 或 vim。

```bash
git config --global core.editor emacs
```

**配置比较、合并工具：**

```bash
git config --global merge.tool vimdiff
```

Git 接受 kdiff3、tkdiff、meld、xxdiff、emerge、vimdiff、gvimdiff、ecmerge、opendiff 作为合并工具。

**配置客户端对比工具（Beyond Compare 4）：**

```bash
# 将 bc4 设置为默认 difftool
git config --local diff.tool bc4
git config --local difftool.bc4.cmd "\"C:\\Program Files\\Beyond Compare 4\\BCompare.exe\" \"\$LOCAL\" \"\$REMOTE\""
git config --local difftool.prompt false

# 将 bc4 设置为默认 mergetool
git config --global merge.tool bc4
git config --global mergetool.bc4.cmd "\"C:\\Program Files\\Beyond Compare 4\\BCompare.exe\" \"\$LOCAL\" \"\$REMOTE\" \"\$BASE\" \"\$MERGED\""
# 退出 bc4 时视为合并完成
git config --global mergetool.bc4.trustExitCode true
git config --global mergetool.prompt false
git config --global mergetool.keepBackup false
```

`"\"C:\\Program Files\\Beyond Compare 4\\BCompare.exe\" \"\$LOCAL\" \"\$REMOTE\""` 表示启动 `BCompare.exe`，后面跟参数。`\"`、`\$` 是转义。

difftool 两个参数：

* `$LOCAL`：旧版本（pre-image）的临时文件；
* `$REMOTE`：新版本（post-image）的临时文件。

mergetool 四个参数：

* `$LOCAL`：当前分支（ours）；
* `$REMOTE`：对方分支（theirs）；
* `$BASE`：共同祖先；
* `$MERGED`：把合并结果写回这个路径。

之后用 `git difftool` 代替 `git diff`。

## Git 基本概念

### Git 的四个区域

* **工作区：** 平时放项目代码的目录；
* **暂存区：** 即将提交的文件列表，实际是 `.git/index` 这个文件，所以也叫索引（index）；
* **版本库、本地仓库：** 目录里的隐藏文件夹 `.git`，里面是对象、引用、配置；
* **远程仓库：** 用来托管、交换代码的另一份仓库，如 GitHub、GitLab、Gitee。

### 文件的四种状态

* **未跟踪：** 文件在工作区里，从未加入版本库，Git 不管它。
* **未修改：** 已跟踪，工作区和 HEAD 里的快照一致。接下来要么改成「已修改」，要么被删除（`git status` 显示为 deleted；提交删除后这个路径从树里消失，不是变成未跟踪）。
* **已修改：** 工作区相对上次提交有改动，还没 `git add`。可以 `git add` 进入暂存，或用 `git restore`、`git checkout --` 丢弃工作区改动，回到未修改。
* **暂存状态：** 已 `git add`，`git commit` 后进入本地仓库，工作区与 HEAD 再次一致，回到未修改。`git reset HEAD` 或 `git restore --staged` 取消暂存，回到已修改。

### Git 工作流程

* 创建或克隆 Git 仓库；
* 在工作区添加、修改文件；
* 把工作区改动放入暂存区；
* 把暂存区提交到本地仓库；
* 把本地提交推到远程仓库。

## 创建仓库

### 新建一个仓库

**语句：** `git init`

很多 Git 命令只能在仓库里运行，所以 `git init` 往往是第一条。先进入项目目录：

* Linux：`cd /home/user/my_project`
* macOS：`cd /Users/user/my_project`
* Windows（Git Bash）：`cd /c/Users/user/my_project`

`git init` 会创建 `.git` 子目录，里面是仓库运转所需的文件。此时工作区里已有的文件**还没有被跟踪**。

```bash
git init

# 指定目录作为 Git 仓库
git init [directory]
```

### 克隆已有仓库

**语句：** `git clone`

```bash
git clone <repo> <directory>
```

克隆本地或远程仓库：

```bash
git clone /path/to/repository
git clone https://github.com/1927344728/xxxx
git clone https://github.com/1927344728/xxxx my-folder
```

默认在当前目录下建一个与源仓库同名的文件夹，内含 `.git`（历史）以及检出的文件。第三种写法自定义本地目录名。

传输协议常用 **HTTPS**（`https://github.com/user/repo.git`）和 **SSH**（`git@github.com:user/repo.git`）。`git://` 协议已基本淘汰，GitHub 也不再提供。

仓库很大时，可只拉最近一次提交：

```bash
git clone --depth=1 https://github.com/xxxx/xxxx.git
```

以后若要完整历史：

```bash
git fetch --unshallow
```

`git clone` 特别慢，常见原因是 `github.global.ssl.fastly.net` 等域名访问差，见 [Git 速度太慢问题](/knowledge/tools/0102git-chang-jian-wen-ti.md)。

远程私有仓库每次 HTTPS 都要账号密码，可改用 [GitHub SSH](/knowledge/tools/0102git-chang-jian-wen-ti.md)。

## 远程仓库管理

本地已经 `git init`，又在 GitHub 上建了一个空仓库，把两者关联后，远程既当备份，也供别人协作。

```bash
# 列出已配置的远程名
git remote

# 列出远程名和 URL。未配置则没有输出
git remote -v

# 添加远程。origin 是习惯名，可改
# 同一本地仓库可以关联多个远程
git remote add origin git@github.com:xxx/xxx.git
git remote add gitee git@gitee.com:xxx/xxx.git

# 删除远程配置（不删远程服务器上的仓库）
git remote rm origin
```

## Git 基本操作

Git 的工作是创建项目快照，并和以后的快照对比。除了 `git init`、`git clone`，日常还有：`git status`、`git add`、`git commit`、`git reset`、`git checkout`（以及 2.23 起的 `git switch`、`git restore`）、`git pull`、`git push`。

### 查看文件状态

**语句：** `git status`

查看工作区、暂存区相对 HEAD 的状态，也可指定路径。

```bash
git status [file]
```

### 工作区 → 暂存区

**语句：** `git add`

把指定文件、目录或当前目录下的改动放入暂存区。

```bash
git add .
git add [dir]
git add [file1] [file2] ...

# -f：强行添加已被 .gitignore 忽略的文件
git add -f <file>
```

### 暂存区 → 本地仓库

**语句：** `git commit`

把暂存区做成一次提交，写入本地仓库。

```bash
# 提交暂存区全部内容（或指定路径），-m 后面是提交说明
git commit -m [message]
git commit [file1] [file2] ... -m [message]

# -a：把已跟踪文件的工作区改动一并提交，可省略事先 git add
# 不会添加从未跟踪过的新文件
git commit -am [message]

# 用暂存区内容改写最近一次提交（说明、树都可以变）
git commit --amend
# 只改说明，不把当前暂存区的新改动并进去
git commit --amend --only -m <message>
```

`--amend` 会改写历史。若这次提交已经 `push` 过，再改需要强制推送，协作仓库上不要用。

### 暂存区、本地仓库 → 工作区、暂存区

**语句：** `git reset [--soft | --mixed | --hard] [HEAD]`

移动当前分支（HEAD）指向的提交，用来回退版本。三种模式差在工作区和暂存区留到哪一步：

```bash
# 取消暂存：暂存区回到 HEAD，工作区改动仍在
git reset HEAD [filename]

# --mixed 是默认，可省略
# 分支回到指定提交；改动留在工作区，不暂存（相当于那次提交 add 之前）
git reset --mixed HEAD^
git reset HEAD^
git reset [commitId]

# 分支回到指定提交；改动全部留在暂存区（相当于 add 之后、commit 之前）
git reset --soft HEAD^
git reset --soft [commitId]

# 分支回到指定提交；工作区和暂存区都丢掉，与该提交完全一致
git reset --hard HEAD^
git reset --hard [commitId]

# 把当前分支重置到另一分支目前的提交
git reset [branch]
```

HEAD 写法：

* `HEAD`：当前提交
* `HEAD^`、`HEAD^1`、`HEAD~`、`HEAD~1`：第一父提交，线性历史上就是上一次提交
* `HEAD^^`、`HEAD~2`：沿第一父提交往回数 2
* `HEAD~n`：往回数 n

**注意：** `HEAD^2` 是**当前提交的第二父提交**（合并提交才有），不是「往回数 2」。往回数 2 要用 `HEAD~2` 或 `HEAD^^`。

`--hard` 会丢掉未提交改动，执行前用 `git status`、`git stash` 确认。已推到远程的提交用 reset 改历史，推送需要 `--force`，协作时优先 `git revert`。

### 恢复工作区、切换分支

**语句：** `git checkout`

一个命令做两件事：切分支、恢复文件。Git 2.23 起拆成 `git switch`（切分支）和 `git restore`（恢复文件），语义更清楚；`git checkout` 仍可用。

```bash
# 切换分支
git checkout [branch]
git switch [branch]

# 创建并切换。工作区未提交改动在不冲突时会带到新分支
git checkout -b [branch]
git switch -c [branch]

# 丢弃工作区改动，恢复成暂存区（暂存区为空则与 HEAD 一致）
git checkout .
git checkout [file]
git restore .
git restore [file]
```

### 远程仓库 → 本地仓库

**语句：** `git pull <远程主机名> <远程分支名>:<本地分支名>`

从远程取回提交并合并进本地。默认是 fetch + merge；若配置了 `pull.rebase`，则是 fetch + rebase。只下载、不合并用 `git fetch`。三种命令的差别见 [Git 常见问题](/knowledge/tools/0102git-chang-jian-wen-ti.md)。

以下示例假定已经 `git remote add` 并设好上游。

```bash
# 按当前分支跟踪的上游拉取并合并
git pull

# 把远程 master 合并进当前分支
git pull origin master

# 把远程 master 取到本地 myBranch 再合并
git pull origin master:myBranch
```

### 本地仓库 → 远程仓库

**语句：** `git push <远程主机名> <本地分支名>:<远程分支名>`

同样假定已关联远程。

```bash
# 推当前分支到它跟踪的远程分支
git push

# 本地 master → 远程 master
git push origin master

# 本地 master → 远程 removeBranch
git push origin master:removeBranch

# 强制用本地历史覆盖远程。会丢掉远程上你还没 fetch 到的别人的提交。协作仓库慎用
git push --force

# 删除远程 removeBranch 分支
git push origin --delete removeBranch
```

已有人在同一远程分支上推送时，优先 `git push --force-with-lease`：远程若比你记忆中的更新，推送会失败，避免静默覆盖别人。

第一次把本地分支推上去并建立跟踪：

```bash
git push -u origin master
```

远程还没有该分支时，`-u` 会创建并关联。

## 分支管理

分支让你从主线上分开干活，互不影响，完成后再合并。Git 里的分支是指向某次提交的指针，创建、切换很快。

包括本地分支、远程跟踪分支，以及两者的同步。

```bash
# 创建分支（不切换）
git branch [branch]

# 查看当前分支
git branch
# 查看本地分支及最后一次提交
git branch -v
# 查看远程跟踪分支
git branch -r
# 查看本地分支及其上游
git branch -vv
# 查看本地加远程跟踪分支
git branch -a

# 切换；-b、-c 为创建后切换
git checkout -b [branch]
git checkout [branch]
git switch -c [branch]
git switch [branch]

# 删除本地分支。-d 在未合并时拒绝；-D 强制删除
git branch -d [branch]
git branch -D [branch]
```

不能删除**当前所在**的本地分支，先切到别的分支再删。

```bash
# 删除远程分支
git push origin --delete [branch]
# 只删本地的远程跟踪引用（远程上的分支还在）
git branch -r -d origin/[branch]
# 旧写法：推送空引用以删除远程分支
git push origin :branch-name
```

```bash
# 检出远程分支并建立跟踪
git checkout --track origin/[branch]
git switch --track origin/[branch]

# 把本地 master 的上游设为 origin/master
git branch --set-upstream-to=origin/master master

# 推当前分支到远程 master；远程没有则创建并关联
git push -u origin master

# 基于远程分支创建本地分支并跟踪
git checkout -b <branch> origin/[branch]
git switch -c <branch> origin/[branch]

# 查看本地还记着、远程已经不存在的跟踪分支（不真删）
git remote prune origin --dry-run
# 删掉这些过期的远程跟踪引用
git remote prune origin
# 从远程更新引用，并顺便 prune
git remote update origin --prune
```

`git fetch --prune` 效果同类：取回远程更新，并清掉远程已删分支对应的本地跟踪引用。

### 合并分支

**语句：** `git merge`

把一个或多个分支的历史合并进**当前**分支。`git pull` 内部也会 merge（未改成 rebase 时）。

```bash
git merge <branch>
git merge <branch1> <branch2>
```

两条以上是 octopus 合并。两人改了同一文件的同一区域，Git 无法自动决定时会产生冲突。冲突块用 `<<<<<<<`、`=======`、`>>>>>>>` 标出：`=======` 之前是当前分支，之后是被合并进来的分支。

两种处理：

* **放弃这次合并：** `git merge --abort`，索引回到 HEAD。
* **解决冲突：** 手工改文件，去掉冲突标记，留下该保留的内容，然后 `git add`，再 `git commit` 生成合并提交。

`git merge` 与 `git rebase` 的差别见 [Git 常见问题](/knowledge/tools/0102git-chang-jian-wen-ti.md)。

## 查看提交历史

### 查看提交记录

**语句：** `git log`

查看当前分支能到达的提交（默认）。

**推荐别名：**

```bash
git config --global alias.lg "log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit --date=relative"
```

配好后执行 `git lg`。

```bash
# 一行摘要
git log --oneline
# 用 ASCII 图画分支结构
git log --graph
# 显示 HEAD、分支名、远程分支、标签等引用名
git log --decorate

# 最近 n 条。-n 后面必须跟数字，也可写成 git log -5
git log -n 5
# 每次提交涉及的文件与行数统计
git log --stat
# 每次提交的完整补丁
git log -p

# 按作者归类：提交次数和说明
git shortlog

# 自定义格式
git log --pretty=format:"%h %s"
# %H 提交的完整哈希
# %h 短哈希
# %s 提交说明
# %T 树对象完整哈希
# %t 树对象短哈希
# %P 父提交完整哈希
# %p 父提交短哈希
# %an 作者名
# %ae 作者邮箱
# %ad 作者日期（可用 --date= 改格式）
# %ar 作者日期，相对时间
# %cn 提交者名
# %ce 提交者邮箱
# %cd 提交日期
# %cr 提交日期，相对时间

# 按作者、提交者搜索，支持正则
git log --author=<pattern>
git log --committer=<pattern>

# 按时间。日期如 2020.3.1、2020.03.01
git log --since=<date>
git log --after=<date>
git log --until=<date>
git log --before=<date>
git log --since="2020.03.01" --until="2020.03.05"

# 按提交说明搜索，支持正则
git log --grep=<pattern>
git log --grep="aaa\|bbb"

# 只看某路径的历史。-- 用来和修订名消歧义
git log -- <path>
git log src/aaa/bbb.vue
git log -- src/aaa/bbb.vue

# 某字符串或正则在补丁中的出现、消失
git log -S <string>
git log -G <regex>
git log -S 'aa'
git log -G '^aa'

# 只看合并提交，或排除合并提交
git log --merges
git log --no-merges
```

**语句：** `git reflog`

记录 HEAD 的移动：commit、reset、checkout、rebase 等，包括 `git log` 已经看不到的「被丢掉的」提交。误 reset、误 rebase 之后，常用 reflog 找回哈希再恢复。

### 查看指定文件逐行历史

**语句：** `git blame <file>`

按行列出最后改这一行的提交、作者和时间。

## 其他操作

### 对比操作

**语句：** `git diff [file]`

对比工作区、暂存区、提交之间的差异。

```bash
# 工作区 vs 暂存区
git diff
git diff <file|directory>

# 暂存区 vs HEAD（--cached 与 --staged 相同）
git diff --cached
git diff --staged

# 两次提交之间。路径后可用 ':!目录' 排除
git diff <commit>..<commit> -- . ':![排除的文件夹]'

# 两分支之间。省略第一个时，对比当前分支与 <branch>
git diff <branch>
git diff <branch> <branch>
```

### 标签管理

**语句：** `git tag [-a] [-f] [-m <msg>] <tagname> [<commit>]`

给某次提交贴名字，常用来标发布版本，例如 `v1.0`、`v1.2`。`-a` 是附注标签（有说明、打标人），不加 `-a` 是轻量标签（只是引用）。列出、删除、校验 GPG 签名也走这个命令。

```bash
# 查看标签
git tag
git tag -l 'v1.4.2.*'

# 在当前提交上打附注标签
git tag -a <tagname> -m "备注信息"
# 给指定提交打标签
git tag -a <tagname> <commithash>

# 查看标签指向的提交信息
git show <tagname>

# 删除本地标签
git tag -d <tagname>

# 推送一个标签；--tags 推送全部本地标签
git push origin <tagname>
git push origin --tags
```

### 储藏工作区

**语句：** `git stash`

把工作区和暂存区的改动存起来，使工作区回到与 HEAD 一致。效果上像 `git checkout .`、`git restore .`，但改动没有丢，以后可以取回。

`git stash save` 是旧接口，现在等价于 `git stash push`。

```bash
# 储藏暂存区和工作区（默认不含未跟踪文件）
git stash
git stash save
git stash save <message>
git stash push -m <message>
# 只储藏指定文件（-- 后面是路径，不是逗号分隔）
git stash push -- <file> <file>
git stash push -m <message> -- <file>

# 列出储藏
git stash list
git stash show <stash>
# git show 也能看，例如 git show stash@{0}，序号来自 git stash list

# 取出最近一条并删除这条记录。默认改动都回到工作区
git stash pop
# 尽量把当初暂存的部分恢复到暂存区
git stash pop --index
# 取出指定记录并删除
git stash pop <stash>

# 取出但不删除记录
git stash apply
git stash apply <stash>

# 删除一条；省略 <stash> 则删最近一条
git stash drop <stash>
# 清空全部储藏
git stash clear
# 从某条储藏新建分支并应用
git stash branch <branch> [<stash>]
```

### 合并多个提交

临时需求在 `master` 上拉出 `temp`，完工时 `temp` 上有 10 个 commit。直接 `git merge temp` 会把这 10 个都留在主线上。若希望主线只出现一个提交，可以把它们压成一次。

**方法一：** 在 `temp` 上把分支指针软重置到 `master`，改动全部留在暂存区，再交一次。

```bash
git reset --soft master
git commit -m "New awesome feature"
```

**方法二：** 交互式 rebase，需要保留或改写各次说明时用。

```bash
# 改写最近 2 个提交
git rebase -i HEAD~2
# 以该提交为起点（它本身不参与 squash，只当坐标）
git rebase -i <commit>

# 变基到 master 的尖端，并交互改写
git rebase -i master
```

`HEAD~n` 表示拿出最近 n 个提交来改写。若列表里要出现 3 条，用 `git rebase -i HEAD~3`。

终端会打开编辑器，类似：

```
pick 3ca6ec3 '注释**********'
pick 1b40566 '注释*********'
pick 53f244a '注释**********'
```

把要并进去的行的 `pick` 改成 `squash` 或 `s`（并入上一条并保留说明供编辑），保存退出：

```
pick 3ca6ec3 '注释**********'
s 1b40566 '注释*********'
s 53f244a '注释**********'
```

有冲突时先改文件（看清要留哪边），然后：

```bash
git add .
git rebase --continue
```

放弃这次 rebase：`git rebase --abort`。

无冲突或冲突已解决后，编辑器会让你写合并后的说明：

```
# This is a combination of 4 commits.
# The first commit's message is:
注释......
# The 2nd commit's message is:
注释......
# The 3rd commit's message is:
注释......
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
```

在 vim 里改好说明后 `:wq` 保存退出，完成压缩。

**方法三：** squash 合并。在目标分支上执行，把 `my-branch` 的改动全部放入暂存区，**不自动提交**，也不生成 merge 节点，随后自己 `git commit`：

```bash
git merge --squash my-branch
git commit -m "New awesome feature"
```

### cherry-pick

把指定提交的补丁应用到当前分支，生成新提交。

```bash
# 拣一次提交
git cherry-pick <commit>
# 拣该分支当前尖端那一次
git cherry-pick <branch>
# 拣多次（按给出的顺序）
git cherry-pick <commit1> <commit2>

# commit1 到 commit2 之间，不含 commit1
# commit1 必须是 commit2 的祖先，否则失败
git cherry-pick <commit1>..<commit2>
# 含 commit1
git cherry-pick <commit1>^..<commit2>

# -n、--no-commit：只改工作区和暂存区，不生成提交
git cherry-pick -n <commit>
```

### 代码回滚

三套命令职责不同：**reset** 移动分支指针；**revert** 用新提交抵消旧提交；**restore**、**checkout** 恢复文件内容。

**git reset：** 把当前 HEAD（当前分支）指到指定提交，用来丢掉后面的提交。三种 `--soft`、`--mixed`、`--hard` 见上文。已推送的历史不要在共享分支上 reset。

**git revert：** 保留全部历史，另外提交一次「反向改动」。

```bash
git revert HEAD              # 撤销最近一次提交（生成新提交）
git revert HEAD^             # 撤销 HEAD 的第一父提交（不是「撤销最近两次」）
git revert [commitId]        # 撤销指定提交
git revert -n [commitId1]..[commitIdn]    # 撤销一个区间，先不提交
git revert -n [commitId1]^..[commitIdn]  # 区间含 commitId1
```

`git revert <branch>` 撤销的是该分支**当前指向的那一次提交**，不会把当前分支整段重置成另一分支。整段对齐用 `git reset`。

**git checkout、git restore：**

```bash
git checkout [branch]                 # 切换分支
git checkout -b [branch]              # 创建并切换
git switch [branch]
git switch -c [branch]

git checkout .                        # 丢弃工作区全部改动
git checkout filename                 # 丢弃某个文件的工作区改动
git restore .
git restore filename

# 进入游离 HEAD：仓库内容变成该提交，但当前不在任何分支上
git checkout <commit>
git switch --detach <commit>

# 只把某文件恢复成某次提交里的样子。省略 <commit> 时，源是暂存区；暂存区没有则相当于 HEAD
git checkout [<commit>] -- <filepath>
git restore -s <commit> -- <filepath>
```

**revert 与 reset：**

* 工作区效果都可以「回到某次改动之前」，但历史不同。
* **reset** 把分支指针挪走，后面的提交在当前分支上消失（reflog 里暂时还能找到）。推到远程需要 `--force`，容易覆盖别人的提交。
* **revert** 在当前尖端再记一次反向提交，别人照常 `git pull` 即可，适合已经公开的分支。

**注意：** 若当前分支曾经 merge 过另一条分支，你又 `revert` 了那次合并，之后再 merge 同一条源分支，Git 会认为那些提交已经在历史上出现过，**不会把代码再带回来**。处理办法：把那次 revert 再 revert 一次，或在源分支上制造新的提交（cherry-pick、把多次合成一次再提交）。

更细的对比见 [Git 常见问题](/knowledge/tools/0102git-chang-jian-wen-ti.md)。

## 文件管理

### 工作区文件

```bash
# 删除未跟踪文件；-d 含目录；-x 连 .gitignore 里的也删（构建产物等）
git clean -f
git clean -fd
git clean -xfd

# 删除已跟踪文件（工作区和索引都删）
git rm <file>
# 若工作区还有未提交改动，需要 -f
git rm -f <file>
# 只从暂存区（索引）拿掉，工作区文件保留，常用于停止跟踪
git rm --cached <file>
# 递归删除目录
git rm -r <directory>
```

`git clean` 会删掉尚未跟踪的文件，先 `git clean -n` 看将要删除什么。

### alias 写在哪

两套东西不要混：

* **Git 别名：** 写在 `~/.gitconfig` 或当前仓库 `.git/config` 的 `[alias]` 段，Windows 也在 `C:\Users\<用户名>\.gitconfig`。前面的 `alias.lg` 就是这一类。
* **Shell 别名：** Bash 看 `/etc/bashrc`、`~/.bash_profile`、`/etc/profile`；zsh 看 `~/.zshrc`、`~/.oh-my-zsh/plugins/git/git.plugin.zsh`。Git Bash 在 Windows 上也可以用 `C:\Users\<用户名>\.bash_profile`。

### .gitignore 文件

在工作区根目录创建 `.gitignore`，一行一个模式。之后 `git add .` 会跳过匹配的路径。

**已经跟踪的文件不会因为后来写入 ignore 就自动忽略。** 要停止跟踪并保留本地文件：`git rm --cached <file>`。

```plaintext
# general
*~
*.log
tmp
dump
**/.DS_Store
.svn/
.idea/
.metadata/
.project
Thumbs.db
*.iml

# node
**/node_modules/

# bower
**/bower_components
_book

# sublime project config file
**/*.sublime-project
**/*.sublime-workspace
```

## Git 与 SVN 有什么区别

* **拓扑：** Git 是分布式，每人一份完整仓库；SVN 是集中式，历史以服务器为准。
* **存储：** Git 把内容做成 blob、tree、commit 等对象，用哈希寻址；SVN 以文件为中心，元数据放在 `.svn` 这类目录里。CVS 则是 `.cvs`。
* **分支：** Git 的分支是指向提交的指针，创建几乎是瞬间；SVN 的分支是版本库里的另一棵目录拷贝。
* **版本号：** SVN 有整个仓库递增的修订号；Git 用提交哈希（默认 SHA-1；Git 3.0 起新仓库计划默认 SHA-256，现有仓库仍是 SHA-1）。没有「第 123 号修订」这种全局整数，但哈希能校验内容，磁盘或传输损坏时更容易发现。

把工作区、暂存区、本地仓库、远程这四层分清，日常就是 add、commit、push；分支用来并行，reset 移动指针，revert 留下反向提交，restore 只动文件。命令很多，职责其实就这几类。

## 参考链接

[Git 官方文档](https://git-scm.com/doc)

[Pro Git 中文版](https://git-scm.com/book/zh/v2)

[runoob.com - Git 教程](https://www.runoob.com/git/git-tutorial.html)

[易百 - Git 教程](https://www.yiibai.com/git)

[git - 简明指南（入门推荐）](https://rogerdudler.github.io/git-guide/index.zh.html)

[Git 教程 - 廖雪峰的官方网站](https://www.liaoxuefeng.com/wiki/896043488029600)
