第四章 Git

  首先推荐大家先去看《Git教程-廖雪峰》,笔者也是跟随该作者入门的,之所以还要在此重复造轮子,主要是因为:

-  一来能按照自己的思路组织知识,便于记忆。
-  二来写一遍比看一遍的印象好深很多。
-  三来防止原作者博客地址变更。

  笔者不会对Git的基础知识冗述,如果你需要了解,请阅读《Git教程-廖雪峰》后再继续向下阅读。

SVN与Git

  由于SVN等工具出现的年头很长了,所以在设计上有缺陷是难免的,基于SVN的各类缺点Git就诞生了,笔者接下来就说一下自己认为的Git相比于SVN所增加的优点。


  首当其冲的就是网络问题:

-  SVN是集中式的,各个客户端共同维护一个代码库,开发时需要联网才能把代码提交到代码库。
-  上面这句话的言外之意就是,如果你没有网那么你就没法提交代码,没法提交就意味着你对文件的修改是没法被记录的,即你无意中修改了某个文件的内容,然后关闭编辑器后,就没法还原了。
-  Git是分布式的,各个客户端虽然仍然是共同维护一个代码库,但是每个客户端在下载代码时,会同时在本地建立代码库副本,就算开发者没有网络,开发者也可以肆无忌惮的工作,在处于重要的工作节点时可以对本地的代码库进行add、commit等操作。
-  换句话说,SVN没有网络时我们的开发是投鼠忌器的感觉,不敢乱搞代码,因为这些文件没被记录到代码库中,也就谈不上帮我们恢复了。而Git由于本地就有一个代码库副本,所以就可以放心写代码。


  事实上,Git会在本地创建代码库的特性,给开发者带来个很多好处:

-  方便的fork别人的项目,并做出相应的修改,同时还不会影响项目的主人。
-  离线开发,连线上传代码等。


  关于Git其他的优点就不多说了,各位自己总结就可以了。

基础入门

  首先创建一个名为test的目录,然后在命令行中进入到该目录下。

  范例1:创建本地版本库。

1
git init

语句解释:
-  在命令中执行上面的命令就可以创建一个空的版本库。
-  命令执行之后,test目录下会出现一个名为.git的隐藏目录,里面保存着各类信息,请不要手工修改其内的任何文件。


  然后再创建一个a.txt,并将66666写入到其内。

  范例2:查看本地版本库状态。

1
2
3
4
5
6
7
8
9
10
11
cutler@cutler-OptiPlex-7040:~/workspace/test$ git status
位于分支 master

初始提交

未跟踪的文件:
(使用 "git add <file>..." 以包含要提交的内容)

a.txt

提交为空,但是存在尚未跟踪的文件(使用 "git add" 建立跟踪)

语句解释:
-  “未跟踪的文件”是指未被纳入到版本控制中,也就是说如果我们此时执行提交命令,是不会将a.txt添加到版本库中的。


  范例3:提交代码。

1
git commit

语句解释:
-  这是最简单的提交命令,后面我们还会介绍commit命令的更详细的用法。
-  正如上面说的那样,此时是无法提交成功的,git会提示“提交为空,但是存在尚未跟踪的文件”。


  为了让代码顺利提交到版本库中,我们需要执行另一个命令。
  范例4:添加到版本库。

1
git add a.txt

语句解释:
-  使用add命令就可以将指定的文件加入到版本库的控制中。
-  然后再次执行git status命令就可以看到git提示a.txt已经被加入到版本控制中了。
-  需要注意的是add命令只是把a.txt给标识了一下,即告诉git当用户下次执行commit命令时,要将a.txt提交到版本库中。


  范例5:批量添加到版本库。

1
git add -A

语句解释:
-  在add命令之后加上-A参数就可以递归的将当前以及当前子目录下的所有文件纳入到版本控制中。
-  将文件add进去后就可以使用commit命令提交了,比如:
   -  git commit -m '提交日志'
-  其中-m参数用来设置提交的日志,提交之后可以再次执行git status命令查看状态。


  出于谨慎起见,在正式开发时,我们通常在提交代码之前都会再检查一遍自己所有修改的文件,看看有没有什么问题。但是如果代码改的比较多,在提交的时候就很容易忘了自己之前改了哪些代码。
  在Git中可以使用命令来查看改动,比如我们现在把a.txt的内容修改为77777,并保存。


  范例6:比较文件。

1
2
3
4
5
6
7
8
9
cutler@cutler-OptiPlex-7040:~/workspace/test$ git diff a.txt

diff --git a/a.txt b/a.txt
index e83d667..272de1f 100644
--- a/a.txt
+++ b/a.txt
@@ -1 +1 @@
-66666
+77777

语句解释:
-  使用“git diff 文件名”就可以查看指定文件的改动情况。
-  减号表示删除的行,加号表示增加的行。


  代码提交完毕后,还可以用git log命令查看提交记录。

  范例7:查看提交记录。

1
2
3
4
5
6
7
8
9
10
11
12
cutler@cutler-OptiPlex-7040:~/workspace/test$ git log
commit 8bbb78125ce72d3c92e16d0fce03e8c0185424bf
Author: cuihu <cutler@xxxxx.com>
Date: Thu May 26 20:49:20 2016 +0800

second

commit c1f300f7b03a5263e2e6f30e512919737a749b85
Author: cuihu <cutler@xxxxx.com>
Date: Thu May 26 20:32:13 2016 +0800

first

语句解释:
-  如果嫌输出信息太多,看得眼花缭乱的,可以在命令后面加上--pretty=oneline参数。
-  需要注意的是,上面那一大串类似3628164...882e1e0的是commit id(版本号)。
-  和SVN不一样,Git的commit id不是1,2,3……递增的数字,而是一个SHA1计算出来的一个非常大的数字,用十六进制表示。而且你看到的commit id和我的肯定不一样,以你自己的为准。
-  为什么commit id需要用这么一大串数字表示呢?因为Git是分布式的版本控制系统,如果大家都用1,2,3……作为版本号,那肯定就冲突了。
-  如果想退出git log的控制,按q就可以。


  很多时候执行完commit之后就会后悔,Git也给了我们反悔的机会。
  在Git中,用HEAD表示当前版本,也就是上面的8bbb7…4bf(注意我的提交ID和你的肯定不一样),上一个版本就是HEAD^,上上一个版本就是HEAD^^,当然往上100个版本写100个^比较容易数不过来,所以写成HEAD~100

  范例8:撤销提交。

1
git reset --hard HEAD^

语句解释:
-  使用reset命令就可以将当前版本库还原到指定的版本中,此时你再打开a.txt就会发现里面的数字变成6了。
-  其中--hard的作用稍后介绍。


  如果再还原之后,你又后悔了,想还原到刚才77777的那个版本,那也可以,在Git中你总有后悔药可以吃。

  范例9:查看执行的操作历史。

1
2
3
4
cutler@cutler-OptiPlex-7040:~/workspace/gittest$ git reflog
c1f300f HEAD@{0}: reset: moving to HEAD^
8bbb781 HEAD@{1}: commit: second
c1f300f HEAD@{2}: commit (initial): first

语句解释:
-  Git提供了一个命令git reflog用来记录你的每一次命令。
-  从这个记录中我们可以看到版本库编号的前半部分(回滚时只要输入版本号的前缀git就可以知道要恢复到哪个版本上了)。
-  也就是说,只需要执行“git reset --hard 8bbb781”就能将内容还原到77777。
-  Git的版本回退速度非常快,因为Git在内部有个指向当前版本的HEAD指针,当你回退版本的时候,Git仅仅是把HEAD从指向目标版本。


  如果你不知道什么是工作区和暂存区,请点击 这里 ,下面列出几条结论:

-  隐藏目录.git就是版本库,而.git目录所在的目录就被称为工作区。
-  版本库里存了很多东西,其中最重要的就是称为stage(或者叫index)的暂存区,还有Git为我们自动创建的第一个分支master,以及指向master的一个指针叫HEAD。
-  前面讲了我们把文件往Git版本库里添加的时候,是分两步执行的:
   -  第一步是用git add把文件添加进去,实际上就是把文件修改添加到暂存区;
   -  第二步是用git commit提交更改,实际上就是把暂存区的所有内容提交到当前分支。
   -  也就是说,若修改了文件的内容,但是并没有用add再次把该文件放入暂存区,那么当执行commit是不会提交该文件的。
-  因为我们创建Git版本库时,Git自动为我们创建了唯一一个master分支,所以现在git commit就是往master分支上提交更改。


  总有时候我们把某个文件加入到缓存区之后就又后悔了,想把它撤出来。

  范例10:撤出缓存区。

1
git reset HEAD a.txt

语句解释:
-  使用“git reset HEAD 文件名”就可以将该文件撤出缓存区。


  还有一种情况,我们在一个类中写了半天的代码,最后发现写了还不如不写,但是由于写的太多了,使用ctrl+z已经没法将文件退回到最初的状态了,此时我们同样有方法。

  范例11:恢复文件到初始状态。

1
git checkout -- a.txt

语句解释:
-  在介绍checkout命令之前,我们需要知道,每当我们对a.txt修改之后,都需要再次将其add到暂存区才行。
   -  比如a.txt最初的内容是111,我们将它修改为222,若想让a.txt被提交到版本库,必须得把它add到暂存区中才行。
   -  若此时,我们再次将a.txt的内容修改为333,则仍然需要再add一次a.txt文件,否则提交的时候它的内容将是222。
-  而checkout命令就是把a.txt文件的最后一次修改给撤销。
   -  若a.txt被修改了,但是没被加入到缓存区中,则会直接将它的内容还原。
   -  若a.txt之前被修改了,同时当时也把它放入缓存区了,但是随后你又修改了a.txt文件,此时checkout命令只会将a.txt文件恢复到最近一次add到缓存区时的状态。
   -  如果感觉我说的还是有歧义,那么请自行去测试。
-  需要注意的是--和a.txt之间是有空格存在的。


  当我们不需要某个文件时,可以直接在目录中将它给删除,这个时候Git知道你删除了文件,因此工作区和版本库就不一致了,git status命令会立刻告诉你哪些文件被删除了。
  此时你有两个选择,一是确实要从版本库中删除该文件,那就用命令git rm删掉,并且提交。
  另一种情况是删错了,因为版本库里还有呢,所以可以很轻松地把误删的文件恢复到最新版本:

1
git checkout -- a.txt

语句解释:
-  也就是说,checkout不仅能撤销修改,也能撤销删除。

远程仓库

  同样的,你需要先去看一下 这里 在继续向下看。

  这里简单的列一下仓库的常识:

-  在Git中也存在远程仓库的概念,各个员工可以把自己本地库中的代码push到远程仓库中去,以此来完成团队开发的目的。
-  目前来说有两种类型的仓库:
   -  自己搭建git仓库。 如果公司对代码的安全和保密性要求的比较高,那么就会搭建自己的git服务器来管理代码。比如BAT、小米等巨头公司肯定都是有自己的git服务器的。
   -  使用第三方git仓库,目前常见的仓库有:Github、CSDN、开源中国等等,它们各有各的特点,不再冗述。


  不论你是使用自己的Git库还是第三方的,想成功从git远程仓库中下载和上传代码,一般都需要经历如下几个步骤:

-  首先,去git服务器注册一个账号。
-  第二,在本地创建你自己的ssh秘钥,并把公钥配置到git服务器中。
-  第三,当你从git服务器下载或上传代码时,服务器就会通过公钥来验证你的身份。


  至于如何创建ssh密钥,网上有很多教程, 这里 是以Github为例介绍了配置步骤,如果觉得不详细可以自行搜索。

分支管理

  在Git里,在创建仓库的同时会创建一个分支,这个分支叫主分支,即master分支。
  另外,HEAD严格来说是指向当前分支,即默认情况下HEAD指向的是master

  当我们创建新的分支例如dev时,Git会同时新建了一个指针叫dev,它指向与master相同的提交,再把HEAD指向dev,就表示当前分支在dev上。


  范例1:创建分支。

1
git checkout -b dev

语句解释:
-  使用上面这条命令就可以创建并切换到dev分支,它相当于连续执行了下面两条语句:
   -  创建分支:git branch dev
   -  切换分支:git checkout dev
-  执行“git checkout”命令可以列出所有的分支,其中当前分支前面会标一个*号。


  需要知道的是,我们在dev分支上执行的操作(add、commit等)不会影响到其它分支。
  比如我们在dev分支中添加一个b.txt,然后提交,确保提交成功后再切回到master分支,然后你就会发现在文件夹中是看不到b.txt的。


  范例2:合并分支。

1
2
3
4
5
6
cutler@cutler-OptiPlex-7040:~/workspace/gittest$ git merge dev
更新 2e61159..2474a7b
Fast-forward
b.txt | 1 +
1 file changed, 1 insertion(+)
create mode 100644 b.txt

语句解释:
-  git merge命令用于合并指定分支到当前分支。合并后,就可以在目录中看到b.txt了。
-  注意到上面的Fast-forward信息,Git告诉我们,这次合并是“快进模式”,也就是直接把master指向dev的当前提交,所以合并速度非常快。
-  当然,也不是每次合并都能Fast-forward,我们后面会讲其他方式的合并。


  合并完成后,就可以放心地删除dev分支了。
  范例3:删除分支。

1
git branch -d dev

语句解释:
-  如你所见。


  开发中的情况往往会更加复杂,如果master和dev分支同时修改了1个文件,那么合并时就会出现冲突。
  这种情况下,Git无法执行“快速合并”,只能试图把各自的修改合并起来,但这种合并就可能会有冲突。
  范例4:合并分支时产生冲突。

1
2
3
4
cutler@cutler-OptiPlex-7040:~/workspace/gittest$ git merge dev1
自动合并 b.txt
冲突(内容):合并冲突于 b.txt
自动合并失败,修正冲突然后提交修正的结果。

语句解释:
-  Git告诉我们,readme.txt文件存在冲突,需要手动解决冲突后再提交。


  我们可以直接打开b.txt并查看它的内容:

1
2
3
4
5
<<<<<<< HEAD
Creating a new branch is quick & simple.
=======
Creating a new branch is quick AND simple.
>>>>>>> dev1

语句解释:
-  Git用<<<<<<<,=======,>>>>>>>标记出不同分支的内容。
-  解决冲突的办法也很简单,只需要打开这个文件,把你不想保留的内容给删掉就可以了。
-  接着就是执行add命令将文件添加到缓存区,最后是commit。


  现在有这么一个问题:

-  你现在正在开发新版本的功能,但是突然分配给你一个紧急的bug,而你由于手头上的代码没有完成(比如逻辑没写完或者还存在编译错误等),立刻提交的话肯定是不行的。
-  但是bug也是非常紧急的,需要立刻去处理。
-  为了解决这个问题,Git提供了stash命令,它可以把当前工作现场“储藏”起来,等以后恢复现场后继续工作。


  通常情况下,我们会在dev分支上开发新功能,而master分支保存的是稳定版的代码,主要用来打包用的。因此为了解决bug,我们会以master分支为基础,创建一个新的bug分支。
  更多关于bug分支的介绍可以看 这里 ,唯一需要补充的就是:

-  如果我们在dev分支下添加了新的文件,那么不论这个新文件是否被加入到版本控制中。
-  当我们把分支从dev切换到其他分支时,这些文件都不会消失(除非它们被提交到某个分支上了)。
-  这样一来就会乱了,开发一段时间后,我们不知道这个文件应该保存到哪个分支下。
-  所以,在我们准备从dev分支切走之前,执行一下git stash命令就可以保存现存,切换分支时与新增的文件也会随之消失。
-  还原分支可以使用:git stash pop命令。
-  查看分支列表可以使用:git stash list。