# 一文搞定Git

* [快速上手](#快速上手)
* [进阶](#进阶)
  * [查看分支之`git branch`](#查看分支)
  * [拉取远端分支之`git fetch` 和 `git pull`](#拉取远端分支)
  * [创建并切换分支之`git checkout`](#创建并切换分支)
  * [将修改保存进缓冲堆栈之`git stash`](#将修改保存进缓冲堆栈)
  * [查看提交历史之`git log`](#查看提交历史)
  * [查看文件修改差异之`git diff`](#查看文件修改差异)
  * [合并分支之`git merge`](#合并分支)
  * [合并分支、提交之`git rebase`](#合并多个提交rebase)
  * [重置提交之`git reset`](#重置提交reset)
  * [提取commit内容之`git cherry-pick`](#提取commit)
  * [给commit打上标签之`git tag`](#给commit打上标签)

## Git

通俗来讲Git就是一个代码版本控制系统，多个开发人员可以协同开发，开发时可以将代码提交到本地，开发完成后，push到远端即可，不用到处备份copy代码，方便查看版本间的diff。

## 安装git

1. Windows

   访问 [Git官网](https://git-scm.com/download/win) 下载安装
2. Mac OS X

   终端执行

   ```bash
    $git --version
   ```
3. Linux

   访问 [Git官网](https://git-scm.com/download/linux) 下载安装

## 快速上手

1. 首先我们需要在终端界面初始化git文件夹，这里我们创建了demo文件夹并初始化

   ```bash
    $ mkdir demo && cd demo
    $ git init 
    已初始化空的 Git 仓库于 /private/tmp/demo/.git/
   ```

   ![](https://ipasser.oss-cn-beijing.aliyuncs.com/images/blog20201108144138.png)

   在初始化后，本地目录的文件就是在工作区中，在执行add后就添加到了暂存区，commit之后就正式存在本地仓库了
2. 创建一个README.md文件，并添加Git中

   ```bash
    $ touch README.md
    $ echo '# git demo' > README.md
    $ cat README.md  
    # git demo
   ```
3. 添加到stage中

   ```bash
    $ git add README.md
   ```
4. 生成一个提交到本地仓库, -m后跟的内容为本次提交的描述信息，提交的文件为已经add到stage中的文件

   ```bash
    $ git commit -m "add README.md" 
    [master（根提交） 8c28b6f] add README.md
    1 file changed, 1 insertion(+)
    create mode 100644 README.md
   ```
5. 随时查看本地git状态

   ```bash
    $ git status 
    位于分支 master
    无文件要提交，干净的工作区
   ```

   如图就表示工作区中的文件均已提交到本地git仓库中
6. 添加远端仓库（可以到[Github](https://github.com/icankeep/blog/tree/7cefec597118b265e94d06165f34e5d36a139a71/2020-11/github.com)中创建远端仓库，获取地址），origin为远端仓库名，可以自行指定 ![](https://ipasser.oss-cn-beijing.aliyuncs.com/images/blog20201107220421.png)

   ```bash
    git remote add origin git@github.com:icankeep/demo.git
   ```

   可以通过下面命令查看是否添加远端仓库成功

   ```
    $ git remote show  origin 
    * 远程 origin
    获取地址：git@github.com:icankeep/demo.git
    推送地址：git@github.com:icankeep/demo.git
    HEAD 分支：（未知）
   ```
7. push本地提交到远端仓库

   ```bash
    $ git push origin master    
    总共 0（差异 0），复用 0（差异 0），包复用 0
    remote: 
    remote: Create a pull request for 'master' on GitHub by visiting:
    remote:      https://github.com/icankeep/demo/pull/new/master
    remote: 
    To github.com:icankeep/demo.git
    * [new branch]      master -> master
   ```

以上我们就完成了最简单的git提交

## 进阶

### 查看分支

1. 查看本地分支

   ```bash
    $ git branch   
    * dev
    master
    merge_demo
    rebase_demo
   ```
2. 查看所有分支

   ```
    $ git branch -a
    dev
    * master
    merge_demo
    rebase_demo
    remotes/origin/dev
    remotes/origin/master
   ```

### 创建并切换分支

一般来说，一个项目组的主分支为master，当我们开发时，会创建一个dev分支，在本地开发测试完成后会合并到线上的分支，如staging，提交到线上staging测试完成后，再合并到主分支发布。我们可以使用下面的命令进行创建并切换分支

```bash
$ git checkout -b dev
切换到一个新分支 'dev'
```

`git checkout`一般有以下几种用途： 1. 切换创建新分支

1. 检出commit，该操作会检出该commit，并detach(和`git reset`不一样，reset是在当前分支回退，checkout不影响当前分支，只是检出)。如下，switch是新版本git引入切换分支的命令

   ```
    $ git checkout 2ce5a34
    注意：正在切换到 '2ce5a34'。

    您正处于分离头指针状态。您可以查看、做试验性的修改及提交，并且您可以在切换
    回一个分支时，丢弃在此状态下所做的提交而不对分支造成影响。

    如果您想要通过创建分支来保留在此状态下所做的提交，您可以通过在 switch 命令
    中添加参数 -c 来实现（现在或稍后）。例如：

    git switch -c <新分支名>

    或者撤销此操作：

    git switch -

    通过将配置变量 advice.detachedHead 设置为 false 来关闭此建议

    HEAD 目前位于 2ce5a34 master demo
   ```
2. 去除对暂存区中文件的修改

   ```
    git checkout -- filename
   ```

### 拉取远端分支

1. `git fetch`会将远端commit更新到本地，但并不会主动合并

   ```
    $ git fetch origin master
    来自 github.com:icankeep/demo
    * branch            master     -> FETCH_HEAD
   ```
2. `git pull`就相当于`git fetch origin master && git merge origin/master`

### 将修改保存进缓冲堆栈

如果暂存区的内容发生了改变，想要将这个改变暂时保存，可以使用`git stash`

如下，我们修改了README.md，新增了test文件

```
$ git status 
位于分支 dev
要提交的变更：
  （使用 "git restore --staged <文件>..." 以取消暂存）
        修改：     README.md

尚未暂存以备提交的变更：
  （使用 "git add <文件>..." 更新要提交的内容）
  （使用 "git restore <文件>..." 丢弃工作区的改动）
        修改：     test
```

按照下面命令就可以保存啦

```
$ git stash          
保存工作目录和索引状态 WIP on dev: ea15522 Merge branch 'merge_demo'
```

可以使用`git stash list`和`git stash show stash@{0}`查看stash信息

```
$ git stash list  
stash@{0}: WIP on dev: ea15522 Merge branch 'merge_demo'
```

```
$ git stash show stash@{0}
 README.md | 1 +
 test      | 1 +
 2 files changed, 2 insertions(+)
```

将缓冲堆栈中内容提取出来

```
$ git stash pop  
位于分支 dev
尚未暂存以备提交的变更：
  （使用 "git add <文件>..." 更新要提交的内容）
  （使用 "git restore <文件>..." 丢弃工作区的改动）
        修改：     README.md
        修改：     test

修改尚未加入提交（使用 "git add" 和/或 "git commit -a"）
丢弃了 refs/stash@{0}（f6965744573d97e52b165e886d625ae1ddccf36a）
```

### 查看提交历史

当我们开发完代码git commit之后，我们就可以通过下面的命令查看提交历史

```bash
$ git log 
commit abf83aa02ceb51e71d209777e2411b27a0f6b417
Author: passer <whzhoua@gmail.com>
Date:   Sat Nov 7 21:32:32 2020 +0800

    add README.md
```

如图，每个commit都有对应的一个hash值（基本不可能碰撞）

### 查看文件修改差异

后续如果我们需要开发，可以通过git diff等查看修改后的文件和之前commit文件的差异

```bash
$ git diff README.md
diff --git a/README.md b/README.md
index 7b81bad..b4f319c 100644
--- a/README.md
+++ b/README.md
@@ -4,3 +4,4 @@ master demo
 1
 2
 3
+modify some things
```

### 合并分支

当代码在本地开发并测试完成后，我们一般需要将代码合并到其他正式分支，解决冲突的具体操作我们在后面rebase中说明

```bash
$ git checkout master
$ git merge dev
```

### 合并多个提交rebase

rebase是git中非常强大的功能，既可以merge分支，也可以合并修改commit信息等。当我们本地开发时，可能会经常遇到刚提交完一个commit，发现代码中还有一些错误，于是又提交一个commit，会造成一个功能太多个commit，提交信息混乱，于是我们就需要合并这些无用的commit。

1. git rebase 分支名

   `git rebase demo`指当前分支合并demo分支，`git merge`和`git rebase`的差异在于如果当前分支已经有了新的提交（demo分支中不存在的提交），`git merge`就会产生一个merge commit（即一个额外的提交），`git rebase`则不会。实践出真知，直接开始操作

   在之前master分支"add README.md"提交之后切出新创建的rebase\_demo分支和merge\_demo分支

   ```bash
    git checkout -b rebase_demo
    echo 'rebase demo' >> README.md
    git commit -am "rebase demo"
   ```

   通过git log查看提交历史，发现已经提交

   ```bash
    $ git log --graph --pretty=oneline --abbrev-commit    
    * 0eec621 (HEAD -> rebase_demo) rebase demo
    * abf83aa (master) README.md
   ```

   切换到master分支rebase我们的rebase\_demo分支

   ```
    $ git checkout master
    切换到分支 'master'
   ```

   ```
    $ git rebase rebase_demo
    成功变基并更新 refs/heads/master。
   ```

   ```
    $ git log --graph --pretty=oneline --abbrev-commit    
    * 0eec621 (HEAD -> master, rebase_demo) rebase demo
    * abf83aa README.md
   ```

   将rebase\_demo分支合并到master分支成功，并且没有产生新的提交，我们接着通过merge的方式来看有没有新的提交

   ```bash
    git checkout -b merge_demo
    echo 'merge demo' >> README.md
    git commit -am "merge demo"
   ```

   通过`git log`查看提交历史，发现已经提交

   ```bash
    $ git log --graph --pretty=oneline --abbrev-commit     
    * 65d472a (HEAD -> merge_demo) merge demo
    * abf83aa (master) README.md
   ```

   ```
    git checkout master
    git merge merge_demo
   ```

   ```
    $ git log --graph --pretty=oneline --abbrev-commit      
    * 65d472a (HEAD -> master) merge demoREADME.md
    * abf83aa README.md
   ```

   可知，当发起合并的分支中没有新commit时，rebase和merge都是不会产生新的提交的，我们现在来看看当存在新提交时，会是什么情况

   首先，我们来看下当前的分支情况

   ```
    $ git log --graph --pretty=oneline --abbrev-commit  --all
    * 1fe102c (merge_demo) merge demo
    | * 0eec621 (rebase_demo) rebase demo
    |/  
    * abf83aa (HEAD -> master) README.md
   ```

   master当前位于第一个提交，而rebase\_demo分支提交了一个rebase demo, merge\_demo提交了一个merge demo，为了产生冲突，我们在master分支提交一个master demo即可

   ```
    echo 'master demo' >> README.md
    git commit -am "master demo"
   ```

   ```
    $ git log --graph --pretty=oneline --abbrev-commit  --all
    * 2ce5a34 (HEAD -> master) master demo
    | * 1fe102c (merge_demo) merge demo
    |/  
    | * 0eec621 (rebase_demo) rebase demo
    |/  
    * abf83aa README.md
   ```

   此时每个分支都对文件README.md做出了修改，这时将rebase\_demo或merge\_demo合并到master，都将产生冲突

   ```
    $ git rebase rebase_demo   
    自动合并 README.md
    冲突（内容）：合并冲突于 README.md
    error: 不能应用 2ce5a34... master demo
    Resolve all conflicts manually, mark them as resolved with
    "git add/rm <conflicted_files>", then run "git rebase --continue".
    You can instead skip this commit: run "git rebase --skip".
    To abort and get back to the state before "git rebase", run "git rebase --abort".
    不能应用 2ce5a34... master demo
   ```

   通过git status查看状态

   ```
    $ git status   
    交互式变基操作正在进行中；至 0eec621
    最后一条命令已完成（1 条命令被执行）：
    pick 2ce5a34 master demo
    未剩下任何命令。
    您在执行将分支 'master' 变基到 '0eec621' 的操作。
    （解决冲突，然后运行 "git rebase --continue"）
    （使用 "git rebase --skip" 跳过此补丁）
    （使用 "git rebase --abort" 以检出原有分支）

    未合并的路径：
    （使用 "git restore --staged <文件>..." 以取消暂存）
    （使用 "git add <文件>..." 标记解决方案）
            双方修改：   README.md

    修改尚未加入提交（使用 "git add" 和/或 "git commit -a"）
   ```

   修改冲突文件，将<<<<<<< HEAD >>>>>>> 2ce5a34... master demo之间的内容修改成最终的,wq保存退出，重新`git add`

   ```
    $ vim README.md

    # git demo
    <<<<<<< HEAD
    rebase demo
    =======
    # master
    master demo
    >>>>>>> 2ce5a34... master demo
   ```

   ```
    $ git add README.md
    $ git rebase --continue
   ```

   rebase\_demo分支已经rebase到master分支，并没有产生新的提交

   ```
    $ git log --graph --pretty=oneline --abbrev-commit  --all
    * 24e6dfa (HEAD -> master) master demo
    * 0eec621 (rebase_demo) rebase demo
    | * 1fe102c (merge_demo) merge demo
    |/  
    * abf83aa README.md
   ```

   回到最初状态继续尝试下merge

   通过git reflog可以查看所以git操作的id，随时可以回退到对应的id

   ```
    $ git reflog 
    24e6dfa (HEAD -> master) HEAD@{0}: rebase (continue) (finish): returning to refs/heads/master
    24e6dfa (HEAD -> master) HEAD@{1}: rebase (continue): master demo
    0eec621 (rebase_demo) HEAD@{2}: rebase (start): checkout rebase_demo
    2ce5a34 HEAD@{3}: commit: master demo
    ...
   ```

   我们通过git reset回到对应操作id上

   ```
    $ git reset --hard 2ce5a34  
    HEAD 现在位于 2ce5a34 master demo
   ```

   ```
    $ git log --graph --pretty=oneline --abbrev-commit  --all
    * 2ce5a34 (HEAD -> master) master demo
    | * 1fe102c (merge_demo) merge demo
    |/  
    | * 0eec621 (rebase_demo) rebase demo
    |/  
    * abf83aa README.md
   ```

   一切如初，继续尝试merge

   ```
    $ git merge merge_demo
    自动合并 README.md
    冲突（内容）：合并冲突于 README.md
    自动合并失败，修正冲突然后提交修正的结果。
   ```

   ```
    $ git status   
    位于分支 master
    您有尚未合并的路径。
    （解决冲突并运行 "git commit"）
    （使用 "git merge --abort" 终止合并）

    未合并的路径：
    （使用 "git add <文件>..." 标记解决方案）
            双方修改：   README.md

    修改尚未加入提交（使用 "git add" 和/或 "git commit -a"）
   ```

   ```
    vim README.md

    # git demo
    <<<<<<< HEAD
    # master
    master demo
    =======
    merge demo
    >>>>>>> merge_demo
   ```

   发现解决完冲突之后，会提示需要commit来结束冲突，所以当存在冲突时merge会产生额外一个commit

   ```
    git add README.md
    $ git status 
    位于分支 master
    所有冲突已解决但您仍处于合并中。
    （使用 "git commit" 结束合并）
   ```

   ```
    $ git commit -m "merge" 
    [master 52ecde5] merge
   ```

   ```
    $ git log --graph --pretty=oneline --abbrev-commit  --all
    *   52ecde5 (HEAD -> master) merge
    |\  
    | * 1fe102c (merge_demo) merge demo
    * | 2ce5a34 master demo
    |/  
    | * 0eec621 (rebase_demo) rebase demo
    |/  
    * abf83aa README.md
   ```

   可知产生了52ecde5这个额外的commit，以上就是rebase和merge合并分支上的区别
2. `git rebase`合并commit

   查看当前master分支状态

   ```
    $ git log --graph --pretty=oneline --abbrev-commit 
    * d576335 (HEAD -> master) 开发测试3
    * b30a41e 开发测试2
    * 209a0c1 开发测试
    * 2ce5a34 master demo
    * abf83aa README.md
   ```

   可知，存在一些开发测试过程中的commit，我们需要对其进行合并，即将3个开发测试的commit合并成一个，可以执行如下命令，3为合并的commit数目

   ```
    git rebase -i HEAD~3
   ```

   就会进入如下界面

   ```
    pick 209a0c1 开发测试
    pick b30a41e 开发测试2
    pick d576335 开发测试3

    # 变基 2ce5a34..d576335 到 2ce5a34（3 个提交）
    #
    # 命令:
    # p, pick <提交> = 使用提交
    # r, reword <提交> = 使用提交，但修改提交说明
    # e, edit <提交> = 使用提交，进入 shell 以便进行提交修补
    # s, squash <提交> = 使用提交，但融合到前一个提交
    # f, fixup <提交> = 类似于 "squash"，但丢弃提交说明日志
    # x, exec <命令> = 使用 shell 运行命令（此行剩余部分）
    # b, break = 在此处停止（使用 'git rebase --continue' 继续变基）
    # d, drop <提交> = 删除提交
    # l, label <label> = 为当前 HEAD 打上标记
    # t, reset <label> = 重置 HEAD 到该标记
    # m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
    # .       创建一个合并提交，并使用原始的合并提交说明（如果没有指定
    # .       原始提交，使用注释部分的 oneline 作为提交说明）。使用
    # .       -c <提交> 可以编辑提交说明。
    #
    # 可以对这些行重新排序，将从上至下执行。
    #
    # 如果您在这里删除一行，对应的提交将会丢失。
    #
    # 然而，如果您删除全部内容，变基操作将会终止。
   ```

   按照提示修改pick为f，保存并退出即可

   ```
    pick 209a0c1 开发测试
    f b30a41e 开发测试2
    f d576335 开发测试3
   ```

   发现commit已经合并了

   ```
    $ git log --graph --pretty=oneline --abbrev-commit
    * 86b2868 (HEAD -> master) 开发测试
    * 2ce5a34 master demo
    * abf83aa README.md
   ```

   当rebase用于合并commit时

   如果需要修改commit信息，可以使用如下命令

   ```
    git commit --amend
   ```

   进入如下界面，修改并保存即可

   ```
    开发测试

    # 请为您的变更输入提交说明。以 '#' 开始的行将被忽略，而一个空的提交
    # 说明将会终止提交。
    #
    # 日期：  Sat Nov 7 23:20:29 2020 +0800
    #
    # 位于分支 master
    # 要提交的变更：
    #       修改：     README.md
    #
   ```

   发现commit信息已经被修正

   ```
    $ git log --graph --pretty=oneline --abbrev-commit 
    * 5d0d00a (HEAD -> master) 正式提交
    * 2ce5a34 master demo
    * abf83aa README.md
   ```

### 重置提交reset

简单粗暴，直接命令上手，下面命令将commitId改成要回退到的commit hash值在执行就可以回退到指定的commit

```
git reset --hard commitId
```

除了--hard还有--mixed和--soft, git reset默认是--mixed

1. mixed

   ```
    $ git reset --mixed 2ce5a34 
    重置后取消暂存的变更：
    M       README.md
   ```

   使用git status查看状态，发现之前commit的文件已经回退到工作区

   ```
    $ git status   
    位于分支 master
    尚未暂存以备提交的变更：
    （使用 "git add <文件>..." 更新要提交的内容）
    （使用 "git restore <文件>..." 丢弃工作区的改动）
            修改：     README.md

    修改尚未加入提交（使用 "git add" 和/或 "git commit -a"）
   ```
2. soft

   ```
    $ git reset --soft 2ce5a34
    $ git status 
    位于分支 master
    要提交的变更：
    （使用 "git restore --staged <文件>..." 以取消暂存）
            修改：     README.md
   ```

   发现使用soft重置之后，修改内容会添加到暂存区
3. hard

   ```
    $ git reset --hard  2ce5a34  
    HEAD 现在位于 2ce5a34 master demo
   ```

   ```
    $ git status 
    位于分支 master
    无文件要提交，干净的工作区
   ```

   使用hard重置提交，提交的内容均会消失，如果不小心重置了，可以用git reflog查看并重置到指定的操作上哦！

### 提取commit

我们在一个分支中可能会需要另一个分支的一个commit，这个时候我们就可以用到下面这个命令进行提取

如下状态，我们如果在rebase\_demo分支获取master分支5d0d00a这个commit的内容，我们就可以使用cherry-pick

```
$ git log --graph --pretty=oneline --abbrev-commit --all
* 5d0d00a (HEAD -> master) 正式提交
* 2ce5a34 master demo
| * 1fe102c (merge_demo) merge demo
|/  
| * 0eec621 (rebase_demo) rebase demo
|/  
* abf83aa README.md
```

```bash
git cherry-pick 5d0d00a
```

### 给commit打上标签

给当前分支最新的提交打上标签

```
$ git tag v1.0
```

查看标签

```
$ git tag   
v1.0
```

```
$ git log --graph --pretty=oneline --abbrev-commit 
*   ea15522 (HEAD -> dev, tag: v1.0, master) Merge branch 'merge_demo'
```

将标签推送到远端仓库

```
$ git push origin v1.0
总共 0（差异 0），复用 0（差异 0），包复用 0
To github.com:icankeep/demo.git
 * [new tag]         v1.0 -> v1.0
```

推送本地的所有tag

```
git push origin --tags
```

将标签打到具体的commit上

```
$ git tag v0.7 8c28b6f
```

可以看到v0.7已经打到8c28b6f上了

```
$ git show v0.7   
commit 8c28b6fbe67d37723f43f1df20a0d360cabf14f5 (tag: v0.7)
Author: passer <whzhoua@gmail.com>
Date:   Sun Nov 8 13:51:04 2020 +0800

    add README.md
```

如果打错标签了，需要删除，可以按照下面的命令

```
$ git tag -d v0.7 
已删除标签 'v0.7'（曾为 8c28b6f）
```

当然，这只是在本地删除标签，如果该标签已经推送到远端，想要删除的话，还得加上下面这一步

```
$ git push origin :refs/tags/v0.7
To github.com:icankeep/demo.git
 - [deleted]         v0.7
```
