我们想用两个博客的篇幅来学习一下git这个工具.git在我们日常的开发中使用的是非常的频繁的,我们需要学习一下.下面说一下我们的目标.
技术目标同时也是我们的知识储备,它包含下面的几个部分.
学习企业级常见分支策略(master/release/develop/feature/hotfix等),理解不同公司,不同环境下适合的分支模型。结合案例,引入工程师,测试人员,技术经理等角色,展现项目开发过程的全貌,深刻理解开发的整体流程,俯视Git在其中的作用
这个我们想通过一个多个人开发的模型和大家演示,这样更加容易理解.
先来让我们简单的理解一下Git是什么.我们从一个故事出发.
假设你是一个大学生,此时面临毕业,此时你把你的毕业论文给导师先看看,此时导师说你这个写的不太行,这里面缺少一部分东西,需要改,你把你的论文改了一下,此时我们称为版本二,第一次的为版本一,导师说还是不行,你还要修改,此时诞生出了版本三,后面依次是版本四,版本五…等你拿着最终的版本给导师的时候,此时导师说还是版本一最好,交版本一就可以了.那么此时你就会为难了,你没有保留原来的版本,也就是原来的找不到了.
等到下一次面临类似的情况,你已经学聪明了,每一个版本你都保留了下来,这样可以解决我们要寻找之前版本的问题,可是随着我们版本的增多,我们有极大的可能会把每一个版本的增加的内容搞混,此时我们还是存在问题的.
上面的例子我们可以得到两个结论,我们需要可以拿到每一个版本,同时也需要知道每一个版本的差异.此时我们是否可以有一个工具完成这些呢?看下面
为了能够更方便我们管理这些不同版本的文件,便有了版本控制器。所谓的版本控制器,就是能让你了解到一个文件的历史,以及它的发展过程的系统。通俗的讲就是一个可以记录工程的每一次改动和版本迭代的一个管理系统,同时也方便多人协同作业。
目前最主流的版本控制器就是 Git 。Git 可以控制电脑上所有格式的文件,例如 doc、excel、dwg、dgn、rvt等等。对于我们开发人员来说,Git 最重要的就是可以帮助我们管理软件开发项目中的源代码文件!
上面我们说Git 可以控制电脑上所有格式的文件,这句话我们需要理解一下.我们需要知道的是Git是可以分辩出我们的文本文件是增加了一行还是减少了一行,但是对于二进制文件,例如图片,视频等,Git只能知道他大小的变换,例如由100k变为120k,至于他们的具体修改了什么,Git是不知道的
下面我们在Centos 7环境下下安装Git,我们执行下面的指令就可以了.
[qkj@Qkj ~]$ sudo yum -y install git
当我们执行好了上面的指令,可以看一下我们Git的版本.
[qkj@Qkj ~]$ git --version
下面我们开始Git的基本操作,这里面我们学习一些基本的内容.
要提前说的是,仓库是进行版本控制的一个文件目录。我们要想对文件进行版本控制,就必须先创建一个仓库出来。
[qkj@Qkj ~]$ mkdir gitcode
[qkj@Qkj ~]$ cd gitcode/
[qkj@Qkj gitcode]$ git init
我们发现这里出现了一个隐藏的文件夹,我们可以看看这个文件夹里面的内容.
.git 目录是 Git 来跟踪管理仓库的,不要手动修改这个目录里面的文件,不然改乱了,就把 Git 仓库给破坏了。
[qkj@Qkj gitcode]$ tree .git/
.git/
├── branches
├── config
├── description
├── HEAD
├── hooks
│ ├── applypatch-msg.sample
│ ├── commit-msg.sample
│ ├── post-update.sample
│ ├── pre-applypatch.sample
│ ├── pre-commit.sample
│ ├── prepare-commit-msg.sample
│ ├── pre-push.sample
│ ├── pre-rebase.sample
│ └── update.sample
├── info
│ └── exclude
├── objects
│ ├── info
│ └── pack
└── refs
├── heads
└── tags
9 directories, 13 files
这里我们需要配置两个内容,先看一下下面的指令.
[qkj@Qkj gitcode]$ git config -l
我们这里只需要配置name和email就可以了,下面有两种配置的方法,我们分别谈一下.
先说第一种,我们这个方法的配置只适合当前的仓库.
[qkj@Qkj gitcode]$ git config user.name "zhangsan"
[qkj@Qkj gitcode]$ git config user.email "[email protected]"
下面我们重新创建一个仓库,看看他的配置是否是和我们的同步的,我们发现是不行的.
这里我们可以配置,请问如果我们想要把这个配置给重置了,我们该用哪一种指令?这里也是很简单的.
[qkj@Qkj gitcode]$ git config --unset user.name
[qkj@Qkj gitcode]$ git config --unset user.email
下面说第二种方法,这也是我们比较常用的,我们给全局的Git做配置,也就是配置了这一次,后面的其他的仓库和这个是同步的.
[qkj@Qkj gitcode]$ git config --global user.name "qkja"
[qkj@Qkj gitcode]$ git config --global user.email "[email protected]"
我去验证一下这个是不是同步的,这里新创建一个仓库,发现我们是正确的.
注意,对于全局的配置,我们不能再用局部的方法进行重置了,这里我们验证一下.
和上面配置一样,这里我们也是需要加上--global
选项的,结果这里我们就不演示了,毕竟我们需要配置Git,大家有兴趣的可以私下做一遍.
[qkj@Qkj gitcode]$ git config --global --unset user.email
[qkj@Qkj gitcode]$ git config --global --unset user.name
上面我们已经创建号本地仓库并且对其进行一定的配置,下面我们想问的是如果我们在这个gitcode文件夹里面创建一个普通的文本文件,例如下面的
[qkj@Qkj gitcode]$ touch ReadMe
那么此时git 是否可以管理这个 ReadMe文件?首先我们需要明确的,这里是不行的.我们要谈几个概念.前面我们一直说仓库,大家可能会认为gitcode这个文件夹是仓库,实际上不是的,
.git
这个隐藏文件夹才是我们真正的仓库,也叫版本库
.这个时候我们就疑惑了既然我们.git是仓库,那么我们把ReadMe放在这里.git可以吗?不行的,这是不被允许的,我们上面说了我们禁止手动修改.git文件.
其实我们上面ReadMe文件的位置是正确的,只不过他的位置是叫做工作区
,那么如何把工作区的内容提交到版本库中,主要是下面两步,先看下面的一张图.
我们发现上面有一个叫做暂存区
的东西,它里面存储的是一些索引,这里我想说明的是我们可以认为暂存区是不属于版本库的.
下面我们开始说我们该如何把工作区的代码提交到版本库.
这里有一个很重要的意思,请问我们提交到版本库的是什么?是文件吗?不是的,实际上我们每一次提交都会创建一个.git对象,此时我们每一次修改工作区的内容提交到版本库中都会创建一个对象,此时的对象是存储在对象库中的,上面我们说的暂存区的索引也就是这些是对象的哈希出来的.那么为何上面的图中我们表示出来呢?这是由于我们仓库刚刚被创建出来,此时还没有做提交的事情,下面我们给补充一下.
下面我们谈两个知识点,解释上面两个名词.
下面我们开始正式开始Git的基本操作,我们分为下面几种情况进行讨论.
我们添加文件分为两个场景来和大家分析,顺便我们看一下.git文件的变化.
在包含 .git 的目录下新建一个 ReadMe 文件,我们把这个提交到版本库.
[qkj@Qkj gitcode]$ touch ReadMe
[qkj@Qkj gitcode]$ vim ReadMe
[qkj@Qkj gitcode]$ cat ReadMe
hello git
[qkj@Qkj gitcode]$ git add ReadMe
下面我们可以看看我们.git仓库里面的变化.
下面我们把这个提交到版本库里面
[qkj@Qkj gitcode]$ git commit -m "add first"
[master (root-commit) 3f20e54] add first
1 file changed, 1 insertion(+)
create mode 100644 ReadMe
[qkj@Qkj gitcode]$
我们要说明的是双引号里面的是我们对于这一次提交版本的一些说明,这些说明可以简单说明我们的代码,注意这个说明我们一定要好好写.
下面继续看一下我们.git的变化.
Git除了可以一次提交一个文件之外,也可以提交多个文件.
[qkj@Qkj gitcode]$ touch file1 file2 file3
[qkj@Qkj gitcode]$ ll
total 4
-rw-rw-r-- 1 qkj qkj 0 Jul 24 15:22 file1
-rw-rw-r-- 1 qkj qkj 0 Jul 24 15:22 file2
-rw-rw-r-- 1 qkj qkj 0 Jul 24 15:22 file3
-rw-rw-r-- 1 qkj qkj 10 Jul 24 15:16 ReadMe
[qkj@Qkj gitcode]$
[qkj@Qkj gitcode]$ git add file1 file2
[qkj@Qkj gitcode]$ git commit -m "add 2 file"
[master bbcdb6b] add 2 file
2 files changed, 0 insertions(+), 0 deletions(-)
create mode 100644 file1
create mode 100644 file2
[qkj@Qkj gitcode]$
除了上述的方法,我们也是可以一次性提交所有的文件,包含文件内容的修改,看一下
[qkj@Qkj gitcode]$ git add .
[qkj@Qkj gitcode]$ git commit -m "add file3"
[master f52956b] add file3
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 file3
[qkj@Qkj gitcode]$
当我们学习了如何提交文件之后,这里我们需要看一下.git是如何变化的,同时我们需要知道他们的相关的原理
[qkj@Qkj gitcode]$ git log
下面我们要说的很重要,绿色框内的一串数字我们称之为commit id,他是由哈希计算出来的.先看下面的图片
这些索引实际上就是对象库一一匹配的,上面的日志我们可以简单的打印一下,这个换一个选项
[qkj@Qkj gitcode]$ git log --pretty=oneline
上面我们已经说了HEAD就是一个指针,这里我们要查看一下
[qkj@Qkj gitcode]$ cat .git/HEAD
这个指针指向的内容我们也要去看一下
[qkj@Qkj gitcode]$ cat .git/refs/heads/master
如何,你看到这一串数字是不是很熟悉,这不就是我们最新一次提交的commit id吗,实际上是的.我们这里需要看一下commit id究竟是什么.
[qkj@Qkj gitcode]$ git cat-file -p f52956b05d1a069239577af34965b16053d63fb7
可以很容易的发现这里面是我们的一些信息,我们这里重点关注两个东西
[qkj@Qkj gitcode]$ git cat-file -p 15a37e9ef171cca4a5d985fccd1fcf9414b2c7cf
我们这里又得到几个数字,其实这些数字对应是文件的索引,我们拿第一来看看,实际上这里存储的是我们文件里面内容的修改
[qkj@Qkj gitcode]$ git cat-file -p 8d0e41234f24b6da002d962a26c2495ea16a425f
Git 比其他版本控制系统设计得优秀,因为 Git 跟踪并管理的是修改,而非文件。什么是修改?比如你新增了一行,这就是一个修改,删除了一行,也是一个修改,更改了某些字符,也是一个修改,删了一些又加了一些,也是一个修改,甚至创建一个新文件,也算一个修改。让我们将 ReadMe 文件进行一次修改,看一下
[qkj@Qkj gitcode]$ vim ReadMe
[qkj@Qkj gitcode]$ cat ReadMe
hello git
hello world
[qkj@Qkj gitcode]$
此时我们就可以看一下Git的状态了.
[qkj@Qkj gitcode]$ git status
此时我们发现ReadMe文件被修改了,但还没有完成添加与提交.这个时候如果我们可以看一下我们修改的是什么就更好了,有的人会想这不是显而易见的吗,我们增加了一行hello world.这是由于我们的代码非常少,如果我们代码上万行呢?实际上我们是可以看到这些修改的,下面看一下我们的方法,然后我说如何分辨哪些是被修改的.
[qkj@Qkj gitcode]$ git diff ReadMe
git diff
命令用来显示暂存区和工作区文件的差异,显示的格式正是Unix通用的 diff 格式,我们说一下如何看.
下面我们把工作区的内容提交版本库一下,然后继续看一下我们的转态.
此时我们就明白了我们下一步就是需要commit了,不会有no changes added to commit (use "git add" and/or "git commit -a")
的消息了。接下来让我们继续 git commit 即可.
[qkj@Qkj gitcode]$ git commit -m "modify ReadMe"
[master 37e17a3] modify ReadMe
1 file changed, 1 insertion(+)
[qkj@Qkj gitcode]$ git status
# On branch master
nothing to commit, working directory clean
[qkj@Qkj gitcode]$
之前我们也提到过,Git 能够管理文件的历史版本,这也是版本控制器重要的能力。如果有一天你发现之前前的工作做的出现了很大的问题,需要在某个特定的历史版本重新开始,这个时候,就需要版本回退的功能了。版本回退的能力是非常重要的,我们知道我们可以把文件夹划分为三个区域,这里的版本回退就是这三个区域的回退.下面我们按照ReadMe的例子来和大家举例子,我们知道了ReadMe里面的内容是分为两个版本的.
执行 git reset 命令用于回退版本,可以指定退回某一次提交的版本。要解释一下“回退”本质是要将版本库中的内容进行回退,工作区或暂存区是否回退由命令参数决定:
git reset 命令语法格式为: git reset [–soft | --mixed | --hard] [HEAD]
下面我们分别解释一下这三个参数
我们先来演示--hard
选项,切记工作区有未提交的代码时不要用这个命令,因为工作区会回滚,你没有提交的代码就再也找不回了,所以使用该参数前一定要慎重,看看现象.
[qkj@Qkj gitcode]$ cat ReadMe
hello git
hello world
hello 测试
[qkj@Qkj gitcode]$ git log --pretty=oneline
37e17a32d3f84c8fe2b35de549c8de7c897e7203 modify ReadMe
f52956b05d1a069239577af34965b16053d63fb7 add file3
bbcdb6b643e76074deb999ff8485a8785c27f5bf add 2 file
3f20e54191f35ee4b0ee73e394039f3dc1cd8ad8 add first
[qkj@Qkj gitcode]$ git reset --hard f52956b05d1a069239577af34965b16053d63fb7
HEAD is now at f52956b add file3
这里我们继续看一下日志,我们发现日志也被更新了.
[qkj@Qkj gitcode]$ git log --pretty=oneline
f52956b05d1a069239577af34965b16053d63fb7 add file3
bbcdb6b643e76074deb999ff8485a8785c27f5bf add 2 file
3f20e54191f35ee4b0ee73e394039f3dc1cd8ad8 add first
[qkj@Qkj gitcode]$
如果我们要是后悔了,请问我们是不是可以重新回到版本二呢?是的,只要我们知道了commit id,我们想会回退到哪一个版本都是可以的.
[qkj@Qkj gitcode]$ git reset --hard 37e17a32d3f84c8fe2b35de549c8de7c897e7203
HEAD is now at 37e17a3 modify ReadMe
[qkj@Qkj gitcode]$ cat ReadMe
hello git
hello world
[qkj@Qkj gitcode]$
这里我们存在一个问题,如果我们没有记住我们之前的commit id怎么办?不要着急,Git总是有后悔的,看下面的指令
[qkj@Qkj gitcode]$ git reflog
37e17a3 HEAD@{0}: reset: moving to 37e17a32d3f84c8fe2b35de549c8de7c897e7203
f52956b HEAD@{1}: reset: moving to f52956b05d1a069239577af34965b16053d63fb7
37e17a3 HEAD@{2}: reset: moving to 37e17a32d3f84c8fe2b35de549c8de7c897e7203
f52956b HEAD@{3}: reset: moving to f52956b05d1a069239577af34965b16053d63fb7
37e17a3 HEAD@{4}: commit: modify ReadMe
f52956b HEAD@{5}: commit: add file3
bbcdb6b HEAD@{6}: commit: add 2 file
3f20e54 HEAD@{7}: commit (initial): add first
[qkj@Qkj gitcode]$
这样,你就可以很方便的找到你的所有操作记录了,但37e17a3 这个是啥东西?这个是版本二的commit id 的部分。没错,Git 版本回退的时候,也可以使用部分commit id 来代表目标版本。
可往往是理想很丰满,现实很骨感。在实际开发中,由于长时间的开发了,导致 commit id 早就找不到了,或者他们早就被冲刷掉了.
下面我们继续解释一个问题,为何Git的回退是可以非常快的,这和Git的机制有关,我们知道了所谓的版本实际上就是.git对象,而恰巧我们HEAD指针里面是保存我们该分支最新的commit id,所谓的版本回退就是把我们的HEAD指向的refs/heads/master里面的commit id给替换了就可以了.
如果我们在我们的工作区写了很长时间代码,越写越写不下去,觉得自己写的实在是垃圾,想恢复到上一个版本。下面就是我们需要讨论的情况.
情况 | 工作区 | 暂存区 | 版本库 |
---|---|---|---|
未add | xxxcodexxx | ||
已add,未commit | xxxcodexxx | xxxcodexxx | |
已commit | xxxcodexxx | xxxcodexxx | xxxcodexxx |
add
这种情况我们可以有两个方法来解决.
git checkout -- 文件名
指令,注意 --
是一定需要带上的,一旦省略,该命令就变为其他意思了,后面我们再说[qkj@Qkj gitcode]$ cat ReadMe
hello git
hello world
[qkj@Qkj gitcode]$ vim ReadMe
[qkj@Qkj gitcode]$ cat ReadMe
hello git
hello world
xxxxcodexxx
[qkj@Qkj gitcode]$ git checkout -- ReadMe
[qkj@Qkj gitcode]$ cat ReadMe
hello git
hello world
[qkj@Qkj gitcode]$
add
,但没有commit
我们先把情况给演示出来.
[qkj@Qkj gitcode]$ cat ReadMe
hello git
hello world
xxxxcodexxx
[qkj@Qkj gitcode]$ git add .
[qkj@Qkj gitcode]$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD ..." to unstage)
#
# modified: ReadMe
#
[qkj@Qkj gitcode]$
这里我们就可以用到版本回退里面git reset的指令了,此时我们我们有两个方法.
使用–hard选项,一步到位
[qkj@Qkj gitcode]$ git reset --hard HEAD
HEAD is now at 37e17a3 modify ReadMe
[qkj@Qkj gitcode]$ cat ReadMe
hello git
hello world
[qkj@Qkj gitcode]$ git status
# On branch master
nothing to commit, working directory clean
[qkj@Qkj gitcode]$
解释一下我们这里为何没用commit id,这是因为HEAD也是可以作为当前版本的commit id,算是Git提供的一个便利吧,下面是HEAD的用法
方法二,我们使用–mixed选项,让暂存区回退到上一个版本,让后使用
git checkout -- 文件名
指令
[qkj@Qkj gitcode]$ vim ReadMe
[qkj@Qkj gitcode]$ git add .
[qkj@Qkj gitcode]$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD ..." to unstage)
#
# modified: ReadMe
#
[qkj@Qkj gitcode]$ git reset HEAD
Unstaged changes after reset:
M ReadMe
[qkj@Qkj gitcode]$ git status
# On branch master
# Changes not staged for commit:
# (use "git add ..." to update what will be committed)
# (use "git checkout -- ..." to discard changes in working directory)
#
# modified: ReadMe
#
no changes added to commit (use "git add" and/or "git commit -a")
[qkj@Qkj gitcode]$ cat ReadMe
hello git
hello world
xxxxcdexxxx
[qkj@Qkj gitcode]$ git checkout -- ReadMe
[qkj@Qkj gitcode]$ cat ReadMe
hello git
hello world
[qkj@Qkj gitcode]$
add
,并且也commit
这个更加简单了,我们只需要让commit id回退到上一个版本就可以了
[qkj@Qkj gitcode]$ vim ReadMe
[qkj@Qkj gitcode]$ cat ReadMe
hello git
hello world
xxxcodexx
[qkj@Qkj gitcode]$ git add .
[qkj@Qkj gitcode]$ git commit -m "modify ReadMe"
[master e6c10be] modify ReadMe
1 file changed, 1 insertion(+)
[qkj@Qkj gitcode]$ git reset --hard HEAD^
HEAD is now at 37e17a3 modify ReadMe
[qkj@Qkj gitcode]$ cat ReadMe
hello git
hello world
[qkj@Qkj gitcode]$
在 Git 中,删除也是一个修改操作,我们实战一下, 如果要删除file3
文件,怎么搞呢?如果你仅仅是在工作区删除这个文件,那么你这是你还需要再一次commit一次.
[qkj@Qkj gitcode]$ rm file3
[qkj@Qkj gitcode]$ git status
# On branch master
# Changes not staged for commit:
# (use "git add/rm ..." to update what will be committed)
# (use "git checkout -- ..." to discard changes in working directory)
#
# deleted: file3
#
no changes added to commit (use "git add" and/or "git commit -a")
[qkj@Qkj gitcode]$ git add --all
[qkj@Qkj gitcode]$
[qkj@Qkj gitcode]$ git commit -m "delete file3"
[master 2643e9d] delete file3
1 file changed, 0 insertions(+), 0 deletions(-)
delete mode 100644 file3
[qkj@Qkj gitcode]$ git status
# On branch master
nothing to commit, working directory clean
[qkj@Qkj gitcode]$
除了上面方法,git也给我们提供了删除文件的操作,这把上面的三步变为了两步.
[qkj@Qkj gitcode]$ git rm file2
rm 'file2'
[qkj@Qkj gitcode]$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD ..." to unstage)
#
# deleted: file2
#
[qkj@Qkj gitcode]$ git commit -m "delete file2"
[master 074e76b] delete file2
1 file changed, 0 insertions(+), 0 deletions(-)
delete mode 100644 file2
[qkj@Qkj gitcode]$ git status
# On branch master
nothing to commit, working directory clean
[qkj@Qkj gitcode]$
分支管理是Git里面杀手锏的技能,这一点非常重要.我们需要理解一下分支.
分支就是科幻电影里面的平行宇宙,当你正在电脑前努力学习 C++ 的时候,另一个你正在另一个平行宇宙里努力学习 JAVA。
如果两个平行宇宙互不干扰,那对现在的你也没啥影响。不过,在某个时间点,两个平行宇宙合并了,结果,你既学会了 C++ 又学会了 JAVA!
在版本回退里,你已经知道,每次提交,Git都把它们串成一条时间线,这条时间线就可以理解为是一个分支。截止到目前,只有一条时间线,在Git里,这个分支叫主分支,即 master 分支。
这里我们继续要说一下HEAD,HEAD是可以指向任何分支的,只不过们当前只有一个分支master,所以他就指向了master分支.每次提交,master分支都会向前移动一步,这样,随着你不断提交,master分支的线也越来越长,而HEAD只要一直指向master分支即可指向当前分支。
[qkj@Qkj gitcode]$ cat .git/HEAD
ref: refs/heads/master
[qkj@Qkj gitcode]$ cat .git/refs/heads/master
074e76bd35002c34b7817cd9c1de3b52da1e71a8
[qkj@Qkj gitcode]$
这里我们先查看一下我们的分支.
[qkj@Qkj gitcode]$ git branch
* master
[qkj@Qkj gitcode]$
Git 支持我们查看或创建其他分支,在这里我们来创建第一个自己的分支 dev.
[qkj@Qkj gitcode]$ git branch dev
[qkj@Qkj gitcode]$ git branch
dev
* master
[qkj@Qkj gitcode]$
现在我们就可以看到我们已经创建了一个名为dev的分支.我们需要看一下这个分支的最新的commit id
[qkj@Qkj gitcode]$ ll .git/refs/heads/
total 8
-rw-rw-r-- 1 qkj qkj 41 Jul 24 18:12 dev
-rw-rw-r-- 1 qkj qkj 41 Jul 24 18:03 master
[qkj@Qkj gitcode]$ cat .git/refs/heads/master
074e76bd35002c34b7817cd9c1de3b52da1e71a8
[qkj@Qkj gitcode]$ cat .git/refs/heads/dev
074e76bd35002c34b7817cd9c1de3b52da1e71a8
[qkj@Qkj gitcode]$
这里我们就可以明白了我们新创建的分支是在master分支最新的commit id那里创建的
既然我们已经创建好了一个新的分支,我们该如何切换到另一个分支?这里很简单.
[qkj@Qkj gitcode]$ git checkout dev
Switched to branch 'dev'
[qkj@Qkj gitcode]$
我们这里看一下HEAD指向的分支现在是哪一个.
[qkj@Qkj gitcode]$ cat .git/HEAD
ref: refs/heads/dev
[qkj@Qkj gitcode]$
这里我们查看一下分支,这里有一个知识点和大家分享.
[qkj@Qkj gitcode]$ git branch
* dev
master
[qkj@Qkj gitcode]$
这里我们可以看到原本master分支前的*
变到了dev分支前面.或是这么说*
在那个分支前面,那个分支就是我们的当前的分支,也就是HEAD指向的分支.
既然我们已经在dev分支了,这我们修改一下ReadMe文件.
[qkj@Qkj gitcode]$ cat ReadMe
hello git
hello world
i am dev
[qkj@Qkj gitcode]$ git add .
[qkj@Qkj gitcode]$ git commit -m "modify ReadMe: i am dev"
[dev 7f5256c] modify ReadMe: i am dev
1 file changed, 1 insertion(+)
[qkj@Qkj gitcode]$
可是如果我们在这个时候切换到master分支,我们继续看一下ReadMe的内容
[qkj@Qkj gitcode]$ git checkout master
Switched to branch 'master'
[qkj@Qkj gitcode]$ cat ReadMe
hello git
hello world
[qkj@Qkj gitcode]$
这里我们就会发现我们修改的ReadMe文件内容没有了,赶紧回dev分支看看.
[qkj@Qkj gitcode]$ git checkout dev
Switched to branch 'dev'
[qkj@Qkj gitcode]$ cat ReadMe
hello git
hello world
i am dev
[qkj@Qkj gitcode]$
其实还在的,实际上此时我们可以认为dev和master分支是两个平行宇宙.我们看看他们两个分支最新的commit id.
[qkj@Qkj gitcode]$ cat .git/refs/heads/master
074e76bd35002c34b7817cd9c1de3b52da1e71a8
[qkj@Qkj gitcode]$ cat .git/refs/heads/dev
7f5256c2f548420b9933eeab6ceeb35eaf1ce9af
[qkj@Qkj gitcode]$
看到这里就能明白了,因为我们是在dev分支上提交的,而master分支此刻的提交点并没有变,此时的状态如图如下所示。
为了在 master 主分支上能看到新的提交,就需要将dev 分支合并到master 分支,这既是我们分支的合并.那么我们该如何合并呢?
如果我们想要将dev 分支合并到master 分支,我们需要先切换到master分支上,然后执行合并的操作.
[qkj@Qkj gitcode]$ git checkout master
Switched to branch 'master'
[qkj@Qkj gitcode]$ cat ReadMe
hello git
hello world
[qkj@Qkj gitcode]$ git merge dev
Updating 074e76b..7f5256c
Fast-forward
ReadMe | 1 +
1 file changed, 1 insertion(+)
[qkj@Qkj gitcode]$ cat ReadMe
hello git
hello world
i am dev
[qkj@Qkj gitcode]$
git merge
命令用于合并指定分支到当前分支。合并后,master 就能看到 dev 分支提交的内容了。此时的状态如图如下所示
其中我们Fast-forward 代表“快进模式”,也就是直接把master指向dev的当前提交,所以合并速度非常快,后面我们会谈的.
既然dev被合并了,那么此时dev就没有作用了,我们可以把它删除掉,不过删除dev分支时,我们当前分支不能是dev.
[qkj@Qkj gitcode]$ git branch
* dev
master
[qkj@Qkj gitcode]$ git branch -d dev
error: Cannot delete the branch 'dev' which you are currently on.
[qkj@Qkj gitcode]$ git checkout master
Switched to branch 'master'
[qkj@Qkj gitcode]$ git branch -d dev
Deleted branch dev (was 7f5256c).
[qkj@Qkj gitcode]$ git branch
* master
[qkj@Qkj gitcode]$
此时的状态如图如下所示。
因为创建、合并和删除分支非常快,所以Git鼓励你使用分支完成某个任务,合并后再删掉分支,这和直接在master分支上工作效果是一样的,但过程更安全,至于为何安全后面我们会谈.
首先我们要说的不是所有的合并都是让我们称心的,有极大的可能会出现合并冲突,这里我们演示一下.
[qkj@Qkj gitcode]$ cat ReadMe
hello git
hello world
aaaaa xxxxxxxxxxx
[qkj@Qkj gitcode]$ git status
# On branch master
nothing to commit, working directory clean
[qkj@Qkj gitcode]$
下面我们创建一个新的分支dev1.
[qkj@Qkj gitcode]$ git checkout -b dev1 # 创建dev1分支并且切换
Switched to a new branch 'dev1'
[qkj@Qkj gitcode]$ git branch
* dev1
master
[qkj@Qkj gitcode]$
此时我们修改下ReadMe的内容,然后提交到版本库
[qkj@Qkj gitcode]$ cat ReadMe
hello git
hello world
bbbbb xxxxxxxxxxx
[qkj@Qkj gitcode]$ git status
# On branch dev1
nothing to commit, working directory clean
[qkj@Qkj gitcode]$
之后我们切换回master分支,并且对master分支上的ReadMe进行一次修改和提交.
[qkj@Qkj gitcode]$ cat ReadMe
hello git
hello world
ccccc xxxxxxxxxxx
[qkj@Qkj gitcode]$ git status
# On branch master
nothing to commit, working directory clean
[qkj@Qkj gitcode]$
这个时候我们的状况是这样的.
如果我们想要把dev1里面的内容合并到master中,此时我们就会出现合并冲突,看一下.
[qkj@Qkj gitcode]$ git branch
dev1
* master
[qkj@Qkj gitcode]$ git merge dev1
Auto-merging ReadMe
CONFLICT (content): Merge conflict in ReadMe
Automatic merge failed; fix conflicts and then commit the result.
注意,我们所说的冲突是内容冲突,但是他还是合并了.
那么此时这里我们因该如何办呢?很简单,我们需要手动修改的,分析一下我们那些内容属于哪些分支.
这里我们保留dev1分支的内容和就可以了.
然后重新提交一下,这即使我们解决合并冲突的方法.
[qkj@Qkj gitcode]$ git add .
[qkj@Qkj gitcode]$ git commit -m "merge dev1: bbb"
[master 7b19e81] merge dev1: bbb
[qkj@Qkj gitcode]$ git status
# On branch master
nothing to commit, working directory clean
[qkj@Qkj gitcode]$
这里我们扩展一下我们日志的打印.
[qkj@Qkj gitcode]$ git log --graph --abbrev-commit
上面我们出现了快进模式(Fast-forward,后面简称ff)的模式,我们这里演示一下.
[qkj@Qkj gitcode]$ git checkout -b dev2
Switched to a new branch 'dev2'
[qkj@Qkj gitcode]$ vim ReadMe
[qkj@Qkj gitcode]$ git add .
[qkj@Qkj gitcode]$ git commit -m "md ReadMe : dev2"
[dev2 70b70e3] md ReadMe : dev2
1 file changed, 1 insertion(+)
[qkj@Qkj gitcode]$ git checkout master
Switched to branch 'master'
[qkj@Qkj gitcode]$ git merge dev2
Updating 7b19e81..70b70e3
Fast-forward
ReadMe | 1 +
1 file changed, 1 insertion(+)
[qkj@Qkj gitcode]$ git log --graph --abbrev-commit
我们看一下日志是如何的
在这种Fast forward 模式下,删除分支后,查看分支历史时,会丢掉分支信息,看不出来最新提交到底是 merge 进来的还是正常提交的。但是如果我们是出现合并冲突的分支合并,那么此时可以分辨出来这些是不是合并的内容,那么这就不是Fast forward 模式了,这样的好处是,从分支历史上就可以看出分支信息。例如我们现在已经删除了在合并冲突部分创建的dev1 分支,但依旧能看到 master 其实是由其他分支合并得到
Git 支持我们强制禁用 Fast forward 模式,那么就会在 merge 时生成一个新的commit ,这样从分支历史上就可以看出分支信息。下面我们实战一下–no-ff 方式的 git merge 。
[qkj@Qkj gitcode]$ git checkout -b dev3
Switched to a new branch 'dev3'
[qkj@Qkj gitcode]$ vim ReadMe
[qkj@Qkj gitcode]$ git add .
[qkj@Qkj gitcode]$ git commit -m "md ReadMe : dev3"
[dev3 887c9d9] md ReadMe : dev3
1 file changed, 1 insertion(+)
[qkj@Qkj gitcode]$ git checkout master
Switched to branch 'master'
[qkj@Qkj gitcode]$ git merge --no-ff -m "merge no-ff" dev3
Merge made by the 'recursive' strategy.
ReadMe | 1 +
1 file changed, 1 insertion(+)
[qkj@Qkj gitcode]$
请注意–no-ff 参数,表示禁用 Fast forward 模式。禁用 Fast forward 模式后合并会创建一个新的commit ,所以加上-m 参数,把描述写进去.
在实际开发中,我们应该按照以下基本原则进行分支管理:
首先,master分支应该是非常稳定的,也就是仅用来发布新版本,平时不能在上面干活;那在哪干活呢?干活都在dev分支上,也就是说,dev分支是不稳定的,到某个时候,比如1.0版本发布时,再把dev分支合并到master上,在master分支发布1.0版本;你和你的小伙伴们每个人都在dev分支上干活,每个人都有自己的分支,时不时地往dev分支上合并就可以了。
假如我们现在正在 dev2 分支上进行开发,开发到一半,突然发现master 分支上面有 bug,需要解决。在Git中,每个 bug 都可以通过一个新的临时分支来修复,修复后,合并分支,然后将临时分支删除。可现在 dev2 的代码在工作区中开发了一半,还无法提交,怎么办?例如:
[qkj@Qkj gitcode]$ git checkout -b dev1
Switched to a new branch 'dev1'
[qkj@Qkj gitcode]$ vim ReadMe
[qkj@Qkj gitcode]$ cat ReadMe
hello git
hello world
I am coding ....
[qkj@Qkj gitcode]$ git status
# On branch dev1
# Changes not staged for commit:
# (use "git add ..." to update what will be committed)
# (use "git checkout -- ..." to discard changes in working directory)
#
# modified: ReadMe
#
no changes added to commit (use "git add" and/or "git commit -a")
[qkj@Qkj gitcode]$ git checkout master
M ReadMe
Switched to branch 'master'
[qkj@Qkj gitcode]$ cat ReadMe
hello git
hello world
I am coding ....
[qkj@Qkj gitcode]$
你会发现我们master里面的内容被修改了,此时我们不想master被修改,此时需要下面的指令.
[qkj@Qkj gitcode]$ git checkout dev1
M ReadMe
Switched to branch 'dev1'
[qkj@Qkj gitcode]$ git stash
Saved working directory and index state WIP on dev1: 640e4dd md ReadMe
HEAD is now at 640e4dd md ReadMe
[qkj@Qkj gitcode]$ git checkout master
Switched to branch 'master'
[qkj@Qkj gitcode]$ cat ReadMe
hello git
hello world
[qkj@Qkj gitcode]$ git status
# On branch dev1
nothing to commit, working directory clean
[qkj@Qkj gitcode]$
Git 提供了git stash 命令,可以将当前的工作区信息进行储藏,被储藏的内容可以在将来某个时间恢复出来.
储藏dev1 工作区之后,由于我们要基于master分支修复 bug,所以需要切回master 分支,再新建临时分支来修复 bug,示例如下:
[qkj@Qkj gitcode]$ git checkout -b fix_bug
Switched to a new branch 'fix_bug'
[qkj@Qkj gitcode]$ vim ReadMe
[qkj@Qkj gitcode]$ cat ReadMe
hello git
hello world bug 已经被修复
[qkj@Qkj gitcode]$ git add .
[qkj@Qkj gitcode]$ git commit -m "fix_bug"
[fix_bug 193fdff] fix_bug
1 file changed, 1 insertion(+), 1 deletion(-)
[qkj@Qkj gitcode]$
然后我们把修复好的代码合并到master分支上.
[qkj@Qkj gitcode]$ git checkout master
Switched to branch 'master'
[qkj@Qkj gitcode]$ git merge --no-ff -m "merge fix_bug" fix_bug
Merge made by the 'recursive' strategy.
ReadMe | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
[qkj@Qkj gitcode]$ cat ReadMe
hello git
hello world bug 已经被修复
[qkj@Qkj gitcode]$
让后我们继续回到dev1分支上,拿出来我们存在stash区域的内容,继续编写内容.这里我们要说明的是stach分区只能保存已经被追踪的内容.
[qkj@Qkj gitcode]$ git checkout dev1
Switched to branch 'dev1'
[qkj@Qkj gitcode]$ touch file2
[qkj@Qkj gitcode]$ git status
# On branch dev1
# Untracked files:
# (use "git add ..." to include in what will be committed)
#
# file2
nothing added to commit but untracked files present (use "git add" to track)
[qkj@Qkj gitcode]$ git stash
No local changes to save
[qkj@Qkj gitcode]$
现在我们要把我们保存的内容拿出来了.
[qkj@Qkj gitcode]$ git stash list
stash@{0}: WIP on dev1: 640e4dd md ReadMe
[qkj@Qkj gitcode]$ git stash pop
# On branch dev1
# Changes not staged for commit:
# (use "git add ..." to update what will be committed)
# (use "git checkout -- ..." to discard changes in working directory)
#
# modified: ReadMe
#
# Untracked files:
# (use "git add ..." to include in what will be committed)
#
# file2
no changes added to commit (use "git add" and/or "git commit -a")
Dropped refs/stash@{0} (cefa25e24fed182266e9ff0731d97bee138ee664)
[qkj@Qkj gitcode]$ cat ReadMe
hello git
hello world
I am coding ....
[qkj@Qkj gitcode]$
我们继续编写内容,把这个功能编写完毕,然后提交到版本库,现在我们的情况如下图.
Master 分支目前最新的提交,是要领先于新建dev2 时基于的master 分支的提交的,所以我们在dev2 中当然看不见修复 bug 的相关代码。我们的最终目的是要让master 合并dev2 分支的,那么正常情况下我们切回master 分支直接合
并即可,但这样其实是有一定风险的。是因为在合并分支时可能会有冲突,而代码冲突需要我们手动解决(在master 上解决)。我们无法保证对于冲突问题可以正确地一次性解决掉,因为在实际的项目中,代码冲突不只一两行那么简单,有可能几十上百行,甚至更多,解决的过程中难免手误出错,导致错误的代码被合并到master 上。此时的状态为:
所以我们需要先把master分支上的合并到dev1分支上,然后解决一下合并冲突.
[qkj@Qkj gitcode]$ git checkout dev1
Switched to branch 'dev1'
[qkj@Qkj gitcode]$ cat ReadMe
hello git
hello world
I am coding done
[qkj@Qkj gitcode]$ git merge --no-ff -m "merge master"
fatal: No commit specified and merge.defaultToUpstream not set.
[qkj@Qkj gitcode]$ git merge --no-ff -m "merge master" master
Auto-merging ReadMe
CONFLICT (content): Merge conflict in ReadMe
Automatic merge failed; fix conflicts and then commit the result.
[qkj@Qkj gitcode]$ vim ReadMe
[qkj@Qkj gitcode]$ cat ReadMe
hello git
hello world bug 已经被修复
I am coding done
[qkj@Qkj gitcode]$ git add .
[qkj@Qkj gitcode]$ git commit -m "fix_bug and merge master"
[dev1 571c100] fix_bug and merge master
[qkj@Qkj gitcode]$
然后我们把dev1的内容合并到master上就可以了.
[qkj@Qkj gitcode]$ git checkout master
Switched to branch 'master'
[qkj@Qkj gitcode]$ git merge --no-ff -m "merge dev1" dev1
Merge made by the 'recursive' strategy.
ReadMe | 1 +
1 file changed, 1 insertion(+)
[qkj@Qkj gitcode]$ cat ReadMe
hello git
hello world bug 已经被修复
I am coding done
[qkj@Qkj gitcode]$
软件开发中,总有无穷无尽的新的功能要不断添加进来。
添加一个新功能时,你肯定不希望因为一些实验性质的代码,把主分支搞乱了,所以,每添加一个新功能,最好新建一个分支,我们可以将其称之为 feature 分支,在上面开发,完成后,合并,最后,删除该 feature 分支。可是,如果我们今天正在某个 feature 分支上开发了一半,被产品经理突然叫停,说是要停止新功能的开发。虽然白干了,但是这个 feature 分支还是必须就地销毁,留着无用了。这时使用传统的git branch -d 命令删除分支的方法是不行的。演示如下:
[qkj@Qkj gitcode]$ git checkout -b dev4
Switched to a new branch 'dev4'
[qkj@Qkj gitcode]$ vim ReadMe
[qkj@Qkj gitcode]$ git add .
[qkj@Qkj gitcode]$ git commit -m "md ReadMe"
[dev4 ecb4c7b] md ReadMe
1 file changed, 1 insertion(+)
[qkj@Qkj gitcode]$ git checkout master
Switched to branch 'master'
[qkj@Qkj gitcode]$ git branch -d dev4
error: The branch 'dev4' is not fully merged.
If you are sure you want to delete it, run 'git branch -D dev4'.
[qkj@Qkj gitcode]$
这里是由每一个分支Git都会认为他是有效的,所以在合并之前是不允许我们删除的,除非我们使用下面的方法
[qkj@Qkj gitcode]$ git branch -D dev4
Deleted branch dev4 (was ecb4c7b).
[qkj@Qkj gitcode]$ git branch
dev1
dev2
dev3
* master
[qkj@Qkj gitcode]$
我个人认为分支具有下面几个优点: