是一款开源的分布式版本控制系统,它的出现和Linux紧密相关。Linux内核项目组为了能更好地管理和维护Linux内核开发,于2002年开始启用商业的分布式版本控制系统BitKeeper。虽然软件开发商授权了Linux社区能免费使用,但是好景不长,到了2005年,BitKeeper的开发商由于某些原因终止了与Linux社区的合作关系。于是Linux的作者Linus Torvalds就决定开发一款能替代BitKeeper的分布式版本控制系统(即Git),在花费十天的时间后发布了Git的第一个版本。
一、版本控制系统
版本控制系统(Version Control System,VCS)能管理文件内容的变更记录,即可追踪文件的修订历史,确保不同的人在编辑同一文件时能保持同步。该系统不仅能应用于保存源码的文本文件,还能对图像、Word文档等各种类型的文件进行版本控制。有了版本控制系统之后,就能很方便的回退文件到某个状态、比较文件变更前后的区别、查询到修改文件的人等。目前市面上的版本控制系统大致可分为两种:集中式和分布式,下面会对它们做单独的讲解。
1)集中式
当需要多人协同工作时,就得让集中式版本控制系统(Centralized Version Control Systems,CVCS)登场了。
这类系统包括CVS、Subversion等都是在一台中央服务器中建立一个仓库,专门用来管理文件的修订版本,客户端可通过网络连接到这台服务器,取出最新文件或提交变更,如图3所示。
图3 集中式版本控制系统
虽然使用CVCS带来了诸多便利,但是其缺点也很明显,例如中央服务器一旦宕机,那么客户端将无法提交变更和协同工作;或者中央服务器磁盘故障,没有备份仓库,那么将丢失所有的变更记录。
2)分布式
为了解决CVCS所带来的问题,又有人提出了分布式版本控制系统(Distributed Version Control System,DVCS)。
这类系统包括Git、BitKeeper等都会将服务器中的仓库完整的克隆到本地,这么一来,即使网络断开,也能继续工作,并且受服务器故障的影响也能降低到最小。在图4中,服务器和两台电脑三者之间都能相互推送各自的修订记录,并且每台电脑上都保存了一个本地仓库。
图4 分布式版本控制系统
由此可知,DVCS剥夺了服务器的生杀大权,只让它负责中转大家的修订记录,即使缺了服务器,也不影响协同工作。
二、快照
Git不会像CVS、Subversion等版本控制系统那样存储每个文件所变更的内容以及修订文件版本之间的关系。Git更像是一个文件系统,它存储的是文件快照(Snapshot)。
Git会先将那些变更的文件拷贝一份,然后把备份文件转换成Blob对象,并对其进行压缩,再把文件各自的内容通过SHA-1哈希运算出对应Blob的名称(即版本号),如下所示,最后由这些哈希值作为索引组成一个快照(即版本信息),而通过快照就能反推出该版本中所有发生变更的文件内容。
fbcceef922ce47253804cf00c72c2e955b8bc1b3
每次提交都会生成一个新的快照,而对于未更改的文件,则直接链接到上一个版本。
三、工作区域
Git的工作区域包含三部分:工作目录、暂存区和仓库,它们的说明如下所列:
(1)工作目录(Working Directory)就是去除项目版本信息后的目录,即磁盘上实际操作的目录。
(2)暂存区(Stage)也叫索引区(Index),是一个记录了变更信息的文件,即保存着变更文件的当前快照,为提交到仓库中做准备。
(3)仓库(Repository)也叫版本库,是一个名为.git的隐藏目录,保存着项目的元数据、快照等信息,Git的仓库可分为远程和本地两种。
1)工作流程
在图5中,通过工作区域的三部分描绘出了Git基本的工作流程,简单的将其分为四步,并给出了相应的Git命令。
图5 Git的工作流程
首先在工作目录中修改源文件,然后用add命令将变更记录保存到暂存区,再用commit命令将暂存区中的文件提交给本地仓库,最后用push命令将本地仓库的信息推送给远程仓库。图中的云朵表示网络,因为一般远程仓库都会被托管在某台服务器中,需要通过网络将本地信息传送过去。
2)三种状态
Git中的文件有多种状态,此处列出其中的三种:已修改(modified)、已暂存(staged)和已提交(committed)。
(1)已修改是还未提交的变更文件。
(2)已暂存是记录在当前快照中的变更文件。
(3)已提交是保存在本地仓库中的变更文件。
四、常用命令
本节将列举出Git常用的命令,包括创建仓库、提交更新、查看信息等。
1)创建仓库
如果要在当前目录中创建仓库,那么可以用下面的初始化命令。
git init
在执行该命令后,就会生成名为.git的隐藏目录。
2)克隆仓库
GitHub是一个提供Git仓库托管服务的网站,如果要在本机获取网站中已经存在的仓库,那么可以使用克隆命令,如下所示。注意,这两条命令的效果是相同的,因为Git支持HTTPS和SSH两种数据传输协议。
git clone https://github.com/pwstrick/demo.gitgit clone git@github.com:pwstrick/demo.git
在执行该命令后,就会创建一个名为demo的目录。默认情况下,远程仓库中的每个文件的每个版本都会被拉取下来。
3)提交更新
工作目录中的文件可分为两大类:已跟踪(tracked)和未跟踪(untracked)。已跟踪的文件是指已经被纳入版本控制的文件,它们的状态可以是已修改、已暂存或已提交;而未跟踪的文件正好与之相反,并且不会被记录在之前的快照中。当初始化工作目录时,其中的文件都是未跟踪的;而当克隆仓库时,其中的文件都是已跟踪的。
使用add命令,开始跟踪文件,其后面跟的参数既可以是文件路径,也可以是目录路径,下面命令中的点号表示跟踪当前目录中的所有文件。
git add .
在执行add命令后,工作目录中新增或变更的文件就会被记录在暂存区,并且确定了下次提交时的快照内容。
当暂存区已准备完毕可以提交时,就能执行commit命令了,如下所示,其中“-m”参数能为此次提交添加备注说明,注意,得用引号包裹。
git commit -m "add files"
commit命令能为本地仓库创建一个持久快照,其内容来自于暂存区的临时快照。commit命令还有一个“-a”参数,可以让已经跟踪过的文件直接到暂存区,然后一并提交给本地仓库,从而就能跳过add命令,如果要与“-m”一起使用,可以像下面这样。
git commit -am "add files"
当然,对于未跟踪的文件,无法省略add命令,仍然得执行。
4)忽略文件
在工作目录中创建一个名为.gitignore的文件,列出要忽略的匹配模式(如下所示),就能让Git不再管理相关的文件或目录,例如忽略日志、编译生成的临时文件、Node.js的安装目录等。
# system*.docxnode_modules/
第一行相当于注释,第二行是告诉Git忽略后缀为“.docx”的文件,第三行是忽略node_modules目录。.gitignore文件支持模式匹配(即简化过的正则表达式),另外还有一些格式规范:
(1)所有空行或以“#”开头的行都会被Git忽略。
(2)匹配模式能以“/”开头防止递归。
(3)匹配模式能以“/”结尾指定目录。
(4)只要在模式前加上“!”取反,就能忽略该模式以外的文件或目录。
5)删除文件
Git提供了rm命令,可删除工作目录中的指定文件,并且能消除它在暂存区中的记录,如下所示。
git rm demo.txt
rm命令的后面既可以跟文件或目录的名称,也能跟Glob模式的正则表达式,例如像下面这样删除后缀为“.log”的文件。
git rm *.log
结合commit命令就能将删除的操作告知本地仓库。
6)撤销操作
如果要撤销工作目录中的文件变更,可以使用checkout命令,如下所示,撤销了demo.txt文件中的变更。注意,命令中的“--”不能省略,否则就会执行切换分支的操作。
git checkout -- demo.txt
如果要撤销暂存区中的文件变更,可以使用reset命令,如下所示,其中HEAD是一个指针,指向当前分支,通过它能得到该分支上最后一次提交的快照。
git reset HEAD demo.txt
注意,上述两种撤销都是在commit命令之前执行的。有些情况下的撤销需要依次执行上面两条命令才能完成,例如demo.txt修改了多次,并且执行了多次“git add .”命令,然后要撤销所有的更改。
7)回退版本
如果要实现版本回退,那么首先得知道版本号,而通过log命令就能获取到当前分支的历史版本,如下所示,包括版本号、作者、备注等信息。
$ git logcommit 00ef86de2fe382c5e1a9185a182a35bbbd34c0fdAuthor: strickDate: Tue Jul 2 14:26:31 2019 +0800 testcommit 9a701365eeb47cf876c726cf5e321af5ce9cabbfAuthor: strick Date: Tue Jul 2 14:13:19 2019 +0800 add demo.txt
现在就能通过reset命令,指定版本号,实现回退,如下所示。由于HEAD指针的存在,Git的版本回退其实就是移动指向,因此其速度是非常快的。
git reset --hard 9a701365eeb47cf876c726cf5e321af5ce9cabbf
8)查看信息
在Git中用于查看信息的命令包括log、status、diff和reflog等,其中log命令已在前文介绍过,用来查看提交的版本历史,按提交时间倒序排列。接下来会先修改demo.txt文件,然后再执行相关的查看命令,其中命令可选择的参数由于篇幅原因都没有给出。
status命令用来查看工作目录的状态,显示变更信息以及提示可用命令,如下所示。
$ git statusOn branch masterYour branch is up-to-date with 'origin/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: demo.txtno changes added to commit (use "git add" and/or "git commit -a")
diff命令用来查看工作目录和暂存区之间的差异,如下所示,其中a和b表示的是同一个文件的不同版本。
$ git diffdiff --git a/demo.txt b/demo.txtindex d28d40b..30d74d2 100644--- a/demo.txt+++ b/demo.txt@@ -1 +1 @@-add\ No newline at end of file+test\ No newline at end of file
diff命令还能比较暂存区和本地仓库、两次commit提交、文件修改前后等之间的差异。
reflog命令用来查看当前分支的所有操作记录,包括commit、reset、pull等,如下所示。
e1ce188 HEAD@{0}: pull origin master: Fast-forward9a70136 HEAD@{ 1}: reset: moving to 9a701365eeb47cf876c726cf5e321af5ce9cabbf......fb8e5df HEAD@{ 13}: commit: demof1f30d6 HEAD@{ 14}: clone: from https://github.com/pwstrick/demo.git
9)远程仓库
与远程仓库相关的命令有remote、fetch、pull和push,其中remote可与其它命令(例如add、show、rename等)配合实现扩展功能。
remote命令可列出所有远程仓库的简称,在执行克隆命令时,Git会默认为其添加一个名为origin的远程仓库。如果为remote命令添加“-v”参数,那么会显示远程仓库的简称和其读写的URL。
$ git remote -vorigin https://github.com/pwstrick/demo.git (fetch)origin https://github.com/pwstrick/demo.git (push)
fetch命令可从远程仓库中拉取本地没有的数据,例如缺失的分支。
pull命令可抓取远程分支的最新变更,并将其自动合并到当前分支中,在pull后面会紧跟远程仓库的简称和分支名称,如下所示。
git pull origin master
push命令可将本地快照、变更的文件等信息推送到远程仓库中,其格式与pull命令类似,如下所示。
git push origin master
五、分支
Git之所以能高性能的处理分支,主要得益于其与众不同的存储设计:弃文件变化,取文件快照。每次commit提交,Git都会创建一个提交对象(Commit Object),该对象包含本次快照的指针、父对象的指针(即上一个提交对象)、当前工作目录的结构和相关附属信息(例如作者、备注等),注意,首次提交产生的提交对象没有父对象。
Git的分支本质上就是指向提交对象的可变指针,其默认分支叫master,如图6所示,用圆代表提交对象,圆内的字符串表示版本号的前5位。
图6 分支和其提交历史
1)创建
创建分支其实就是新建一个新的可移动指针,如果要创建一条develop分支,那么执行的命令可以像下面这样。
git branch develop
现在就会变成两条分支,如图7所示。
图7 两条分支
执行“git branch”命令能够查看已有的分支,在当前分支的名称前会加“*”标记,如下所示。
$ git branch develop* master
2)切换
在Git中,有一个特殊的HEAD指针,之前曾提到过它能指向当前分支,相当于当前分支的别名,如图8所示。
图8 HEAD指向当前分支
切换分支其实就是改变HEAD的指向,例如切换到develop分支,其命令如下所示,内部操作如图9所示。
git checkout develop
图9 切换分支
有一条快捷命令,可以创建一个分支并自动切换到那个分支,如下所示,给checkout命令添加了“-b”参数。
git checkout -b develop
3)合并
有了分支后,就能在各自的分支工作,并且互不干扰,例如在develop分支提交了一个版本,如图10所示,develop分支向前移动了,而master分支仍在原地。
图10 HEAD自动向前移动
现在用“git checkout master”命令将分支切换回master,HEAD会指向master分支,并且工作目录将恢复成master分支中的快照。如果要将develop分支中的变更记录合并(Merge)到master分支中,那么可以使用merge命令,如下所示。
$ git merge developUpdating 9a9636b..64ea4b2Fast-forward demo.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)
注意上面的Fast-forward,说明这次合并使用了快进模式,即直接将指针向前移动。举例来说,当前开发处在develop分支,在提交一系列变更后,切换到master分支,而master分支没有提交新的变更,此时将develop分支合并到master分支就会采用快进模式。
4)冲突
如果两个分支对同一文件的同一行都做了不同的修改,那么在合并时就会发生冲突(Conflict),如下所示,都修改了demo.txt文件的第一行。
$ git merge developAuto-merging demo.txtCONFLICT (content): Merge conflict in demo.txtAutomatic merge failed; fix conflicts and then commit the result.
此时,Git就会停止工作,等待冲突的解决。使用status命令可以查看所有冲突的文件,如下所示。
$ git statusOn branch masterYour branch is up-to-date with 'origin/master'.You have unmerged paths. (fix conflicts and run "git commit")Unmerged paths: (use "git add..." to mark resolution) both modified: demo.txtno changes added to commit (use "git add" and/or "git commit -a")
查看demo.txt文件,可以看到下面的特殊区段。
<<<<<<< HEADtest=======minus>>>>>>> develop
Git用<<<<<<<、=======和>>>>>>>标记出不同分支的变更,其中上半部分是HEAD所指的master分支,下半部分是develop分支。要解决冲突,就得手动去掉Git生成的多余字符,以及保留其中一个分支的修改,在完成这一系列的操作后,demo.txt文件中的内容就会变成下面这样。
minus
现在只要将冲突文件的变更保存到暂存区(如下所示),就能将它们标记成已解决,继续下面的工作了。
git add .git commit -am "conflict"
当冲突的文件特别多时,一个个的查找难免会费时费力,改用Git客户端工具会便捷很多,例如TortoiseGit,它不仅能列出冲突的文件,还能提供图11中的可视化编辑界面。
图11 TortoiseGit冲突解决
5)远程
之前通过push命令将本地分支的信息推送到了远程仓库中(如下所示),与此同时,如果没有远程的同名分支,那么Git会自动为其创建一个,并且还会为它们两个建立跟踪关系。
git push origin master
利用branch命令可在本地查看远程仓库(如下所示),分支会以“远程仓库/分支名称”的形式显示,例如origin/master。
$ git branch -r origin/develop origin/master
如果当前分支与远程分支存在跟踪关系,那么在push或pull时可省略分支名称,如下所示。
git pull origingit push origin
Git允许手动建立跟踪关系,只要为branch命令添加“-u”或“--set-upstream-to”参数即可(如下所示),从而就能让一个本地分支跟踪多个远程分支。
$ git branch -u origin/strickBranch develop set up to track remote branch strick from origin.
如果当前分支只有一个可跟踪的远程分支,那么在push或pull时连仓库名称都能省略,如下所示。
git pullgit push
6)删除
如果要删除分支,那么有两条命令可供选择,下面的第一条可删除本地分支,第二条可删除远程分支,本质上它们做的只是移除相应的指针。
git branch -d developgit push origin --delete develop
7)变基
变基(Rebase)能将一条分支上的变更转移到另一条分支上,以图12中的master和develop两条分支为例。
图12 变基前的两条分支
假设当前分支是develop,变基的目标分支是master,执行rebase命令后的分支情况如图13所示。
图13 变基后的两条分支
接下来会通过一个例子来讲解变基的用法,首先通过log命令查看master分支的提交历史,如下所示,其中“number”是修改demo.txt文件时所提交的备注。
$ git logcommit 7c27be31904221f3bb4556ca39dd7c8dea071178Author: strickDate: Wed Jul 3 17:48:47 2019 +0800 number
然后切换到develop分支,并执行rebase命令,如下所示。注意,变基目标是master而不是develop。
$ git checkout develop$ git rebase masterFirst, rewinding head to replay your work on top of it...Applying: digitUsing index info to reconstruct a base tree...M demo.txtFalling back to patching base and 3-way merge...Auto-merging demo.txtCONFLICT (content): Merge conflict in demo.txtFailed to merge in the changes.Patch failed at 0001 digitThe copy of the patch that failed is found in: D:/demo/.git/rebase-apply/patchWhen you have resolved this problem, run "git rebase --continue".If you prefer to skip this patch, run "git rebase --skip" instead.To check out the original branch and stop rebasing, run "git rebase --abort".
由于两条分支同时修改了demo.txt文件中的内容,因此产生了冲突,必须先将其解决,才能继续后面的操作。注意,develop分支的修改记录先于master分支提交。
当解决了所有冲突之后,先执行add命令,注意,此时不需要commit命令。然后再为rebase命令添加“--continue”(如下所示),就能完成变基,其中“digit”也是修改demo.txt文件时所提交的备注。
$ git add .$ git rebase --continueApplying: digit
接着切换回master分支,并将develop分支中的修改合并进来,注意,此时开启了快进模式。
$ git checkout master$ git merge developUpdating 7c27be3..24c4b5aFast-forward demo.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)
最后执行log命令,就能看到develop分支所提交的版本添加到了master分支的后面,注意,它没有根据提交时间按顺序插入。
$ git logcommit 24c4b5adb332f8c4f2e5ec39a7c77e0fc224b065Author: strickDate: Wed Jul 3 17:48:00 2019 +0800 digitcommit 7c27be31904221f3bb4556ca39dd7c8dea071178Author: strick Date: Wed Jul 3 17:48:47 2019 +0800 number
变基的一大特点就是能将分叉的提交历史梳理成一条直线,另一个特点是它会改变提交历史,这与合并完全不同。变基还有一条基本原则,即只对尚未推送和分享给他人的本地修改允许执行变基操作。