精通Git--进阶

第一篇《精通Git–基础》已经对 Git 的基本使用有了了解,接下来这一篇来看一下平时工作中会遇到的一些比较棘手的问题。

GitHub 操作演示

远程 git 仓库

在 GitHub 创建仓库有两种方式,一种是当前没有仓库,我们需要新建一个 git 仓库:

还有一种情况,我们事先已经有了一个仓库,需要迁移过来,就可以使用 GitHub 的迁移功能:

本地 git 仓库

当我们远程 Git 仓库创建好后,有两种情况:

第一种情况,本地什么代码都没有,需要新建仓库,新建一个文件夹,例如我们叫 myGitWork,然后进入该目录执行如下命令:

在此之前你也可以添加一些文件,比如 READEME.md, 添加一些内容:

echo "# testGit" >> README.md
git init
git add README.md
git commit -m "first commit"
git remote add origin git@github.com:lxqxsyu/testGit.git
git push -u origin master

此时我们可能不太理解这个 git push -u 中的 -u 参数,使用 git help push 命令查看帮助:

这个 -u 参数在推送本地文件到远程仓库 origin 的同时,指定了 origin 为默认远程主机,后面就不用再加参数,直接使用 git push 命令了。

还有一种情况,我们本地已有仓库,此时只需要给添加远程仓库地址即可:

git remote add origin git@github.com:lxqxsyu/testGit.git
git push -u origin master

版本回退

工作区回退

假设我们此时我们修改了 README.md 文件,新增了一句话:

# testGit

这个是测试 Git 的例子

如果我们要撤销,很简单,执行 git checkout -- <file>:

git checkout -- README.md

这个命令会撤销所有你对该文件的修改,回到本地仓库中的文件状态。

暂存区回退

假设上面的 README.md 文件已经修改,而且我们执行了 git add . 将该文件添加到了暂存区:

$ git status
On branch master
Your branch is up to date with 'origin/master'.

Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

        modified:   README.md

我们如果要回退到工作区,则使用上面建议的 git reset HEAD <file> 命令即可:

git reset HEAD README.md

本地仓库回退

假设我们上面的 README.md 文件的修改我们提交到了本地仓库 git commit -m "change",我们如何回退到之前的版本呢。

首先,我们使用 git log 查看一下最近提交的信息:

$ git log
commit ee3f15b78078da32e65c8fcbfba31c939995b479 (HEAD -> master)
Author: lxqxsyu <lxq_xsyu@163.com>
Date:   Tue Aug 13 10:39:30 2019 +0800

    change

commit ebf060a6a67b003c2fd0609a5e50ca322248489f (origin/master)
Author: lxqxsyu <lxq_xsyu@163.com>
Date:   Tue Aug 13 10:22:43 2019 +0800

    first commit

接下来使用命令 git reset --hard 来回退到某个版本(使用 commit 的 id 前几位即可):

$ git reset --hard ebf0
HEAD is now at ebf060a first commit

此时就强制回退到了上次提交的状态。假设我们还想让上一个版本的修改存在于暂存区怎么办?

$ git reset --soft ebf0

远程仓库回退

实际上远程仓库回退和本地仓库回退是一样的,因为都要通过本地回退,然后 push 到远程仓库,但是 push 的时候会出错:

$ git push
To github.com:lxqxsyu/testGit.git
 ! [rejected]        master -> master (non-fast-forward)
error: failed to push some refs to 'git@github.com:lxqxsyu/testGit.git'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.

此时要使用 git push origin HEAD --force 推到远程仓库:

git push origin HEAD --force

回退的原理

回顾一下上一篇提到的分支部分的示意图, 我们已经知道每一次提交都会产生一个新对象(commit 对象)指向我们的文件新快照,而 HEAD 游标指向的分支(下图的 master 和 testing 是两个分支)会指向这个新的对象,假设当前分支是 master 则 master 分支会向前移动指向新的 commit 对象。

Git分支示意图

HEAD 是当前分支引用的指针,它总是指向该分支上的最后一次提交。 这表示 HEAD 将是下一次提交的父结点。 通常,理解 HEAD 的最简方式,就是将它看做 你的上一次提交 的快照。

查看 HEAD 快照:

$ git cat-file -p HEAD
tree ac029782a50527dc4e717dad7572cc3b2afb5ad8
author lxqxsyu <lxq_xsyu@163.com> 1565662963 +0800
committer lxqxsyu <lxq_xsyu@163.com> 1565662963 +0800

first commit

除了上面的 HEAD 指针外,还有一个 索引 的概念,索引是你的 预期的下一次提交。 我们也会将这个概念引用为 Git 的 暂存区域,这就是当你运行 git commit 时 Git 看起来的样子。

我们前面提到的文件快照(文件的变化)都保存在 .git 文件夹中,工作区,暂存区,HEAD游标的关系如图:

Git工作中指针的变化过程

你可以这样理解,上图中的文件的三个状态都对应着一个文件快照树,我们的状态改变只不过是这个索引的改变而已,或者说是指针的移动而已。

接下来我们来以指针角度分析一下仓库文件变化的过程:

git init

此时 HEAD 和 master (有可能 master 还不存在) 都处于游离状态,没有固定指向。假设我们现在放入一个文件:

echo "# testGit" >> README.md

此时处于工作区,和暂存区(索引),本地仓库(HEAD)没有任何的关系:

git add README.md

此时暂存区的索引指向了此次 git add 的文件快照树:

git commit -m "change"

这个时候 HEAD 索引指向的分支(此时是 master 分支)指向暂存区索引的文件快照树:

接下来修改文件内容:

# testGit

这个是测试 Git 的例子

此时指向 git status 会检查工作区的文件快照和暂存区的文件快照是否一致,不一致就会显示 modified: README.md

此时再执行 git add README.md 暂存区的索引就会指向工作区新的文件快照树的根。

接下来我们执行 git reset 命令:

git reset HEAD README.md

reset(重置) 命令的本质是移动 HEAD 所指向的分支的指针,上面的 git reset HEAD 等价于 git reset --mixed HEAD , 它会把暂存区清空,并把原节点和 reset 节点的差异的文件放在工作目录,总而言之就是,工作目录的修改、暂存区的内容以及由 reset 所导致的新的文件差异,都会被放进工作目录。

我们上面尝试 git reset --hard 来回退本地仓库的代码:

$ git reset --hard ebf0
HEAD is now at ebf060a first commit

这里的 --hard 参数的意思是重置 HEAD 和指向的分支指针位置,同时重置暂存区和工作目录的内容。所以你使用 git status 会看到仓库回退后暂存区和工作区都清空了。

如果我们使用 --soft 参数就可以保留暂存区,并把重置 HEAD 所带来的新的差异放进暂存区,而且会保留工作区内容, 下方示意图的 4 和 3 的差异:

$ git reset --soft ebf0

这里我们已经大概能总结出 git reset 的一些特点了:

操作仓库(commit 状态)

  1. 如果加参数 git reset --hard <commitId> <file> 会清空工作目录和暂存区的改动,回退到上一个 commit.
  2. 如果加参数 git reset --soft <commitId> <file> 会保留工作目录和回退的 commit 版本与 HEAD 指向的 commit 版本的差异保留在暂存区。
  3. 如果不加参数 git reset <commitId> <file> 默认是 --mixed , 则会保留工作区,清空暂存区,将会把所有差异放入工作区。

操作暂存区(stage 状态)

  1. 如果不加参数 git reset HEAD <file> 等效于 git reset --mixed HEAD <file> 是重置暂存区,并把差异放入工作区。