最近重新学了一遍Git工具的使用,在此记录一下。
Git是什么?
Git是属于分布式版本控制系统,同属于的还有Mercurial、Bazaar等。
Subversion(svn)属于集中式的版本控制系统,同属于的还有CVS、Perforce等。
两种类型版本控制系统有什么区别?
集中式的版本控制系统,以单台中央服务器为中心,将所有的版本控制历史都集中放在这台服务器上,而各个协同开发者的计算机上只保存着一个快照版本,在这种情况下,开发者每次想要提交或者查看文件的历史等,都需要请求网络,向刚刚所说的中央服务器获取或者提交数据。
分布式版本控制系统与集中式版本控制系统不同,它在本地计算机上建立代码仓库,保存着所有的版本控制历史的数据,在合适的时机与远程服务器代码仓库同步。在这种情况下,开发者每次想要提交或者查看文件的历史等,无需请求网络,直接向本地获取或者提交数据即可,因为本地保存着所有的版本历史数据。
分布式版本控制系统的优点:
开发者所有的Git操作都在本地代码仓库进行,不用依赖于网络。
不用担心代码仓库服务器宕机或者数据丢失等情况,因为每个开发者计算机上都保存有完整的版本历史数据,可以随时将开发者主机上的数据恢复至服务器代码仓库。
限于篇幅,本文接下去的内容不讨论svn等集中式的版本控制系统与分布式版本系统的用法差异。
为了更加贴近于实际的开发,我们将从Git仓库的创建到版本的提交为顺序讲述,最后还简述一下分支的用法和概念。
注意:本文是在本地代码仓库上的操作,还未涉及到远程仓库的连接,读者先不要将远程仓库的思想混进来,容易造成混乱。
先介绍几个Git中的基本概念。
Git中的文件区域
在git中总共有三个区域。分别是工作区、暂存区、git仓库区。
我们所有的文件修改以及变更都是在工作区进行的,然后保存至暂存区(git add命令),最后保存到git仓库区(git commit命令)。可能有读者会问git为什么要设计一个暂存区,为什么不支持从工作区直接将文件保存至git仓库区。这是因为这个暂存区作为一个中间区域,它的作用就是存放即将要提交到git仓库的文件。
有这个区域的存在,开发者可以将想要提交到仓库的文件保存到暂存区,不想提交的文件可以继续存放在工作区,这样子下次执行提交操作的时候,Git只会将暂存区所有的文件提交到仓库中。
开发者提交文件更加灵活。开发者假如后悔不想要提交某个文件(此时这个文件已经在暂存区),那么开发者可以将此文件从暂存区中移出(git reset HEAD <file>命令),下次提交就不会提交这个文件。
Git中的文件状态
在git中,文件有四种状态。未跟踪、未修改、已修改、已暂存。
已跟踪的文件是指本来就被纳入版本控制管理的文件,在上次快照中有它们的记录,工作一段时间后,它们的状态可能是未修改,已修改或者已暂存。
未跟踪文件既没有在git仓库中存有快照,也还未将文件快照保存到当前的暂存区域。初次克隆某个仓库时,工作目录中的所有文件都属于已跟踪文件,且状态为未修改。
Git仓库的创建
Git仓库的创建非常简单,在本地设备有安装Git情况下,直接在项目根目录下运行shell命令git init
即可。
$ git init
git status
git status这个命令是用来检查当前文件的状态的。为什么我这么早就介绍这个命令?因为这个命令能给我们提供很多有用的信息,帮助我们了解Git。这个命令应该算是有git最常用的命令了,它能够显示当前工作区文件的状态(已跟踪或未跟踪)、暂存区文件状态、文件冲突状态、当前所在的分支等。
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: README
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git checkout -- <file>..." to discard changes in working directory)
modified: benchmarks.rb
从输出中可以看到当前处于master分支,新创建的文件README
已经被保存至暂存区域,benchmarks.rb
在工作区域已被修改,但是还没被保存至暂存区域,下次提交时候只会提交README
文件到代码仓库。
括号中的文本是git的辅助提示信息。比如这里git告诉你使用git reset HEAD <file> ...
可以将文件从暂存区域移除。git add <file> ...
用来添加或者更新文件到暂存区域等到下次提交。用git checkout --<file>
命令来还原工作区文件的改变,其实就是将工作区某个或某些文件还原为未修改的状态。
git add
git add
的作用是将工作区的文件保存至暂存区。
重要是事情说三遍!
git 保存的不是文件差异或者变化量,而只是一系列文件快照。
git 保存的不是文件差异或者变化量,而只是一系列文件快照。
git 保存的不是文件差异或者变化量,而只是一系列文件快照。
这个命令的后面可以带有文件或者文件夹路径作为参数,当参数为目录时候,git add
的效果会递归至目录下所有的文件。
- 如果文件在工作区域已被跟踪,那么直接将文件快照保存至暂存区。
- 执行
git add
命令后,git会计算该文件的校验和,生成该文件快照,将校验和保存到暂存区,这里是保存文件快照。
注意:如果执行所要执行git add的目标文件处于未跟踪状态,此命令会先将目标文件标识为已跟踪文件。
在仓库建立之后,因为此时本地代码仓库和暂存区都是空的,并没有任何文件的快照。所以此时所有的文件状态都是未跟踪状态。
此时执行git status命令:
$ vim README
$ git status
On branch master
Untracked files:
(use "git add <file>..." to include in what will be committed)
README
nothing added to commit but untracked files present (use "git add" to track)
我们工作目录下有一个名为README
文件,此时显示它的状态就是未跟踪的。同时git也提示我们可以使用git add
来跟踪以及保存到暂存区。
此时我们执行git add *
命令(git 命令支持通配符,*表示将当前工作目录下所有的非忽略文件提交至暂存区,包括所有子文件夹的下的所有文件,当前这里你也可以直接使用git add README
命令,指定只作用于README
文件)。然后再次执行git status
:
$ git status
On branch master
Changes to be committed:
(use "git reset HEAD <file>..." to unstage)
new file: README
从输出中我们可以看到暂存区已经保存着README
文件的快照,并且等待提交。
git commit
git commit这个命令是用来将文件提交到本地代码仓库的。
当你用Git commit命令执行一个新的提交时候,Git只保存一个指向这次提交快照的索引。
可以看到这里有三个文件,file A、file B、file C。这里以在提交一个新的版本后,git会遍历一边所有的文件的指纹信息,如果发现新版本的文件指纹信息与上一个版本不一致,说明该文件发生了变化,此时git会将新的文件快照索引保存至本地的微型数据库中,如果文件未发生变化,那么会将上个版本的保存的快照索引直接复用写入微型数据库中。
注:git所有的数据都保存在本地项目仓库的.git文件夹下,这个文件夹默认为隐藏文件夹。
实际上,我们执行一次提交操作就是保存一个提交对象。
当执行git commit命令后,git会先计算每一个子目录的校验和(注意,git add是计算文件的校验和),然后在git仓库中将这些目录保存为树对象,然后git会创建一个提交对象,这个提交对象出了包含相关的提交信息以外,还包含着指向这个树对象的指针,如此它就可以在将来需要的时候,重现此次快照的内容了。git commit会将暂存区的所有文件提交到本地数据库中。保存的内容如下。
多次提交之后:
分支
分支应该算是版本控制系统最强大的功能之一了。
前面我们说了,在执行git提交时,git会创建并且生成一个提交对象,该对象包含一个指向文件树的指针,包含本次提交的作者等相关附属信息,包含零个或者多个指向该提交对象的父对象指针:首次提交时没有直接祖先的,普通提交有一个祖先,由两个或者多个分支合并产生的提交则有多个祖先。
其实分支的概念很简单,分支本质上仅仅是个指向某个提交对象的可变指针。
在创建一个新的代码仓库时,git会默认创建一个名为master的分支。我们的每次提交操作,都是基于某个分支的基础上进行的。在若干次提交操作之后,分支会自动向前移动。
分支的创建很简单。
git branch testing
这会在当前的提交对象上新建一个新的分支指针。如下:
那么,git是如何知道我们当前是在哪个分支上工作的呢?其实git保存着一个名为HEAD的特别指针。每当我们切换执行切换分支操作的时候,git其实只是将HEAD指针移动到目标分支上,这样子我们接下去的提交等操作都是针对于这个分支进行的。
在上面命令中,我们执行了
git branch
命令,这个命令仅仅是创建一个分支,而不会切换到新建的这个分支中。要切换分支,可以执行一下命令。
git checkout testing
这样子,HEAD就指向了testing分支。这样子就完成了分支的切换。
此时我们如果进行修改一个文件,然后提交。结果如下。
vim test.rb
git commit -a -m 'made a change'
注意,git commit 后面-a表示直接跳过保存至暂存区,直接提交到数据库。
好了。我们把分支切回master。然后再次修改一个文件,最后提交。
vim test.rb
git commit -a -m 'made other changes'
结果如下:
可以看到,我们的项目提交历史产生了分叉。这是因为我们的master和testing分支的父提交对象都是
f30ab
,我们后面可以在合适的时机将两个分支合并。
关于分支的合并见下章,未完待续。