2.5. Git检出

在上一章学习了重置命令(git reset)。重置命令的一个用途就是修改引用(如master)的游标。实际上在执行重置命令的时候没有使用任何参数对所要重置的分支名进行设置,这是因为重置命名实际上所针对的是头指针HEAD。之所以没有改变HEAD的内容是因为HEAD指向了一个引用refs/heads/master,所以重置命令体现为分支“游标”的变更,HEAD本身一直指向的是refs/heads/master,并没有在重置时改变。

如果HEAD的内容不能改变而一直都指向master分支,那么Git如此精妙的分支设计岂不浪费?如果HEAD要改变该如何改变呢?本章将学习检出命令(git checkout),该命令的实质就是修改HEAD本身的指向,该命令不会影响分支“游标”(如master)。

2.5.2. 挽救分离头指针

在“分离头指针”模式下进行的测试提交除了使用提交ID(acc2f69)访问之外,不能通过master分支或其他引用访问到。如果这个提交是master分支所需要的,那么该如何处理呢?如果使用上一章介绍的git reset命令,的确可以将master分支重置到该测试提交acc2f69,但是如果那样就会丢掉master分支原先的提交4902dc3。使用合并操作(git merge)可以实现两者的兼顾。

下面的操作会将提交acc2f69合并到master分支中来。

  • 确认当前处于master分支。

    $ git branch -v
    * master 4902dc3 does master follow this new commit?
    
  • 执行合并操作,将acc2f69提交合并到当前分支。

    $ git merge acc2f69
    Merge made by recursive.
     0 files changed, 0 insertions(+), 0 deletions(-)
     create mode 100644 detached-commit.txt
    
  • 工作区中多了一个detached-commit.txt文件。

    $ ls
    detached-commit.txt  new-commit.txt  welcome.txt
    
  • 查看日志,会看到不一样的分支图。即在e695606提交开始出现了开发分支,而分支在最新的2b31c19提交发生了合并。

    $ git log --graph --pretty=oneline
    *   2b31c199d5b81099d2ecd91619027ab63e8974ef Merge commit 'acc2f69'
    |\
    | * acc2f69cf6f0ae346732382c819080df75bb2191 commit in detached HEAD mode.
    * | 4902dc375672fbf52a226e0354100b75d4fe31e3 does master follow this new commit?
    |/
    * e695606fc5e31b2ff9038a48a3d363f4c21a3d86 which version checked in?
    * a0c641e92b10d8bcca1ed1bf84ca80340fdefee6 who does commit?
    * 9e8a761ff9dd343a1380032884f488a2422c495a initialized.
    
  • 仔细看看最新提交,会看到这个提交有两个父提交。这就是合并的奥秘。

    $ git cat-file -p HEAD
    tree ab676f92936000457b01507e04f4058e855d4df0
    parent 4902dc375672fbf52a226e0354100b75d4fe31e3
    parent acc2f69cf6f0ae346732382c819080df75bb2191
    author Jiang Xin <jiangxin@ossxp.com> 1291535485 +0800
    committer Jiang Xin <jiangxin@ossxp.com> 1291535485 +0800
    
    Merge commit 'acc2f69'
    

2.5.3. 深入了解git checkout命令

检出命令(git checkout)是Git最常用的命令之一,同样也很危险,因为这条命令会重写工作区。

用法一: git checkout [-q] [<commit>] [--] <paths>...
用法二: git checkout [<branch>]
用法三: git checkout [-m] [[-b|--orphan] <new_branch>] [<start_point>]

上面列出的第一种用法和第二种用法的区别在于,第一种用法在命令中包含路径<paths>。为了避免路径和引用(或者提交ID)同名而冲突,可以在<paths>前用两个连续的短线(减号)作为分隔。

第一种用法的<commit>是可选项,如果省略则相当于从暂存区(index)进行检出。这和上一章的重置命令大不相同:重置的默认值是 HEAD,而检出的默认值是暂存区。因此重置一般用于重置暂存区(除非使用--hard参数,否则不重置工作区),而检出命令主要是覆盖工作区(如果<commit>不省略,也会替换暂存区中相应的文件)。

第一种用法(包含了路径<paths>的用法)不会改变HEAD头指针,主要是用于指定版本的文件覆盖工作区中对应的文件。如果省略<commit>,会拿暂存区的文件覆盖工作区的文件,否则用指定提交中的文件覆盖暂存区和工作区中对应的文件。

第二种用法(不使用路径<paths>的用法)则会改变HEAD头指针。之所以后面的参数写作<branch>,是因为只有HEAD切换到一个分支才可以对提交进行跟踪,否则仍然会进入“分离头指针”的状态。在“分离头指针”状态下的提交不能被引用关联到而可能会丢失。所以用法二最主要的作用就是切换到分支。如果省略<branch>则相当于对工作区进行状态检查。

第三种用法主要是创建和切换到新的分支(<new_branch>),新的分支从<start_point>指定的提交开始创建。新分支和我们熟悉的master分支没有什么实质的不同,都是在refs/heads命名空间下的引用。关于分支和git checkout命令的这个用法会在后面的章节做具体的介绍。

下面的版本库模型图描述了git checkout实际完成的操作。

../_images/git-checkout.png

下面通过一些示例,具体的看一下检出命令的不同用法。

  • 命令:git checkout branch

    检出branch分支。要完成如图的三个步骤,更新HEAD以指向branch分支,以branch指向的树更新暂存区和工作区。

  • 命令:git checkout

    汇总显示工作区、暂存区与HEAD的差异。

  • 命令:git checkout HEAD

    同上。

  • 命令:git checkout -- filename

    用暂存区中filename文件来覆盖工作区中的filename文件。相当于取消自上次执行git add filename以来(如果执行过)本地的修改。

    这个命令很危险,因为对于本地的修改会悄无声息的覆盖,毫不留情。

  • 命令:git checkout branch -- filename

    维持HEAD的指向不变。将branch所指向的提交中的filename替换暂存区和工作区中相应的文件。注意会将暂存区和工作区中的filename文件直接覆盖。

  • 命令:git checkout -- . 或写做 git checkout .

    注意:git checkout命令后的参数为一个点(“.”)。这条命令最危险!会取消所有本地的修改(相对于暂存区)。相当于将暂存区的所有文件直接覆盖本地文件,不给用户任何确认的机会!