最近在做关于SCM这种体量比较大的项目时,原本以为简单的Git基础命令就能够完成版本的控制和分支合并等流程,但是经过实战,感觉自己在这方面还是有很多的不足,因此花费一点时间去深入理解Git。本文是对Git的文档进行了总结,列出一些在开发过程中经常用的功能,如果想快速上手,那么本文非常适合你,如果想深入学习Git的相关知识的话,可以学习Git的官方文档。
Git是一个分布式版本控制系统,在这类系统中,客户端不仅仅是提取最新版本的文件快照,而是把代码仓库完整地镜像下来。这么一来,任何一处协同工作中的服务器发生故障,事后都可以用任何一个镜像出来的本地仓库恢复。因为每一次的提取操作,实际上都是一次对代码仓库的完整备份。
本文中接下来会从两个方面去理解Git,一方面是介绍Git基础,另一方面是介绍Git分支,这部分是Git的核心所在,也是本文中会详细阐述的部分。
对于任何一个文件,在Git中只会出现以下三种状态:
由文件的三种状态,我们引出文件流转的三个工作区域:本地仓库、Git工作目录、暂存区域。
一般,Git的基本流程为:
在工作目录中,所有文件只有两种状态:已跟踪或者未跟踪,其中:
在编辑过某些文件之后,Git将这些文件标为已修改。我们逐步把这些修改过的文件放到暂存区域,直到最后一次性提交所有这些暂存起来的文件,如此重复。
上图为文件状态的变化周期。
如果需要查看哪些文件处于什么状态,我们可以通过git status
命令来查看文件的状态。如果仓库没有对文件进行修改,那么运行这个命令的结果为:
$ git status
On branch master
nothing to commit, working tree clean
在这种情况下说明你的工作目录非常的干净,所有已跟踪文件在上次提交后都没有被修改过。
然后我们在该目录下创建一个新的文件README,如果该文件从来没有在工作目录出现过,使用git status
将会看到一个新的未跟踪的文件。
$ echo 'My Project' > README
$ git status
On branch master
Untracked files:
(use "git add ..." to include in what will be committed)
README
nothing added to commit but untracked files present (use "git add" to track)
我们可以看到新建的README文件出现在Untracked files:
下面,这说明了未跟踪的文件之前从来没有在之前的快照中出现。因此Git也不会把此文件纳入跟踪的范围内。
在上面我们新建了一个README文件,但是此文件是没有被跟踪的,如果想被纳入跟踪范围,那么需要运行如下命令:
$ git add README
此时再运行git status
的话,新建的文件已经被跟踪,并且处于暂存的状态。
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD ..." to unstage)
new file: README
只要在Changes to be committed
这行下面的,就说明是已暂存状态。 如果此时提交,那么该文件此时此刻的版本将被留存在历史记录中。
现在的暂存区域已经准备妥当可以提交了。 在此之前,请一定要确认还有什么修改过的或新建的文件还没有git add
过,否则提交的时候不会记录这些还没暂存起来的变化。 这些修改过的文件只保留在本地磁盘。 所以,每次准备提交前,先用git status
看下,是不是都已暂存起来了, 然后再运行提交命令:
$ git commit -m "Story 182: Fix benchmarks for speed"
[master 463dc4f] Story 182: Fix benchmarks for speed
2 files changed, 2 insertions(+)
create mode 100644 README
提交后它会告诉你,当前是在哪个分支(master)提交的,本次提交的完整SHA-1校验和是什么(463dc4f),以及在本次提交中,有多少文件修订过,多少行添加和删改过。
提交时记录的是放在暂存区域的快照。 任何还未暂存的仍然保持已修改状态,可以在下次提交时纳入版本管理。 每一次运行提交操作,都是对你项目作一次快照,以后可以回到这个状态,或者进行比较。
使用分支意味着我们可以把自己的工作从开发主线上分离开来,以免影响主线。而Git能够很好的支持这一特性,Git创建分支这一操作几乎可以在一瞬间就能够完成,而且分支之间切换还非常的方便。
要想真正了解Git处理分支的方式,首先需要了解一下Git是如何保存数据的。Git保存的不是文件的变化或者差异,而是一系列不同时刻的文件快照。在进行提交操作时,Git 会保存一个提交对象(commit object)。知道了 Git 保存数据的方式,我们可以很自然的想到—该提交对象会包含一个指向暂存内容快照的指针。那么,首次提交产生的提交对象没有父对象,普通提交操作产生的提交对象有一个父对象,而由多个分支合并产生的提交对象有多个父对象。
让我们来看一个简单的分支新建与分支合并的例子,实际工作中你可能会用到类似的工作流。 你将经历如下步骤:
1.开发某个网站。
2.为实现某个新的需求,创建一个分支。
3.在这个分支上开展工作。
正在此时,你突然接到一个电话说有个很严重的问题需要紧急修补。 你将按照如下方式来处理:
1.切换到你的线上分支(production branch)。
2.为这个紧急任务新建一个分支,并在其中修复它。
3.在测试通过之后,切换回线上分支,然后合并这个修补分支,最后将改动推送到线上分支。
4.切换回你最初工作的分支上,继续工作。
首先假设我们正在某个项目上工作,并且已经有了一些提交。
其中C0、C1、C2为提交的对象。现在你要开发一个代号为A的新需求,那么你可以新建一个分支并同时切换到那个分支上工作。那么Git又是怎么知道当前在哪一个分支上呢? 也很简单,它有一个名为HEAD的特殊指针。在 Git 中,它是一个指针,指向当前所在的本地分支。
$ git branch A
$ git checkout A
其中git branch
命令是创建一个新的分支,而git checkout
是切换到新分支上去。
然后继续在该分支上进行工作,并且做了一些提交,在此过程中,A分支在不断的向前推进,因为你已经切换到该分支(也就是说,你的HEAD指针指向了A分支)。
现在,突然该应用出现了一个bug, 在Git的帮助下,我们不需要把这个bug和需求A的修改混在一起,也不需要花大力气来还原关于需求A的修改,然后再添加关于这个紧急问题的修改,最后将这个修改提交到线上分支。 我们所要做的仅仅是切换回master分支。
但是,在这么做之前,要留意工作目录和暂存区里那些还没有被提交的修改,它可能会和即将切换的分支产生冲突从而阻止Git切换到该分支。 最好的方法是,在你切换分支之前,保持好一个干净的状态。那么现在,我们假设已经把修改全部提交了,这时可以切换回master分支了:
$ git checkout master
Switched to branch 'master'
这个时候,工作目录和在开始A问题之前一模一样,现在可以修复bug了。 请牢记:当你切换分支的时候,Git会重置你的工作目录,使其看起来像回到了你在那个分支上最后一次提交的样子。 Git会自动添加、删除、修改文件以确保此时你的工作目录和这个分支最后一次提交时的样子一模一样。
接下来就可以新建一个修复bug的分支,在该分支上进行解决。
$ git branch hotfix
$ git checkout hotfix
Switched to a new branch 'hotfix'
如果最后修改无误,那么就可以将分支hotfix合并到该master分支来部署到线上。使用的具体命令为:
$ git checkout master
$ git merge hotfix
Updating f42c576..3a0874c
Fast-forward
在合并的时候,会提示fast-forward这个词。也就是快进的意思。由于当前master分支所指向的提交是你当前提交(有关hotfix的提交)的直接上游,所以Git只是简单的将指针向前移动。换句话说,当你试图合并两个分支时,如果顺着一个分支走下去能够到达另一个分支,那么Git在合并两者的时候,只会简单的将指针向前推进(指针右移),因为这种情况下的合并操作没有需要解决的分歧——这就叫做“快进”。
bug修复完成后,应该回到需求A的工作中,由于bug已经修复,那么hotfix这个分支就不需要了,因为现在该分支和master分支指向了同一个位置,因此,可以用命令来删除该分支:
$ git branch -d hotfix
Deleted branch hotfix (3a0874c).
现在可以切换到分支A继续进行工作。
$ git checkout A
Switched to branch "A"
此时,在hotfix分支上所做的工作并没有包含到分支A中。 如果你需要拉取hotfix所做的修改,你可以使用git merge master
命令将 master分支合并入A分支,或者你也可以等到A分支完成其使命,再将其合并回master分支。
现在需求A已经完成,并且打算将自己的工作合并入master分支。那么和上面叙述的合并hotfix分支所做的工作差不多。我们只需要切换到想要合并的分支,然后运行git merge
命令。
$ git checkout master
Switched to branch 'master'
$ git merge A
Merge made by the 'recursive' strategy.
我们可以看到这和之前合并hotfix分支的时候看起来有一点不一样。这是因为我们的开发历史从一个更早的地方开始分叉开来。 现在master分支所在提交并不是分支A所在提交的直接祖先,因此Git不得不做一些额外的工作。 出现这种情况的时候,Git 会使用两个分支的末端所指的快照(C4 和C5)以及这两个分支的工作祖先(C2)做一个简单的三方合并。
和之前将分支指针向前推进所不同的是,Git 将此次三方合并的结果做了一个新的快照并且自动创建一个新的提交指向它。 这个被称作一次合并提交,它的特别之处在于他有不止一个父提交。
现在修改已经合并到主分支当中去了,此时分支A已经不需要了,那直接删除就可以了。
$ git branch -d A
在什么情况下回出现分支合并的冲突呢?如果你在两个不同的分支中,对同一个文件的同一个部分进行了不同的修改,Git就没法合并它们。如果你对面上hotfix分支和A分支对同一个文件的同意部分进行修改的话,在合并的时候就会产生冲突。
$ git merge A
Auto-merging index.html
CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.
此时Git做了合并,但是没有自动创建一个新的合并提交。 Git会暂停下来,等待你去解决合并产生的冲突。 你可以在合并冲突后的任意时刻使用 git status命令来查看那些因包含合并冲突而处于未合并(unmerged)状态的文件:
$ git status
On branch master
You have unmerged paths.
(fix conflicts and run "git commit")
Unmerged paths:
(use "git add ..." to mark resolution)
both modified: index.html
no changes added to commit (use "git add" and/or "git commit -a")
任何因包含合并冲突而有待解决的文件,都会以未合并状态标识出来。 Git 会在有冲突的文件中加入标准的冲突解决标记,这样你可以打开这些包含冲突的文件然后手动解决冲突。 出现冲突的文件会包含一些特殊区段,看起来像下面这个样子:
<<<<<<< HEAD:index.html
=======
>>>>>>> A:index.html
这表示HEAD所指示的版本(也就是你的master分支所在的位置,因为你在运行merge命令的时候已经切换到了这个分支)在这个区段的上半部分(======= 的上半部分),而A分支所指示的版本在 ======= 的下半部分。 为了解决冲突,你必须选择使用由 ======= 分割的两部分中的一个,或者你也可以自行合并这些内容。 例如,你可以通过把这段内容换成下面的样子来解决冲突:
上述的冲突解决方案仅保留了其中一个分支的修改,并且 <<<<<<< , ======= , 和 >>>>>>> 这些行被完全删除了。 在你解决了所有文件里的冲突之后,对每个文件使用git add
命令来将其标记为冲突已解决。 一旦暂存这些原本有冲突的文件,Git就会将它们标记为冲突已解决。