2013年12月17日

git入门初级教程

作者 非鱼

git相对于subversion的好处是显而易见的。只不过,以前git对windows的支持不太好。现在看来好多了。

1、有了http://bonobogitserver.com,windows版的git server。下载后解压出来一个目录,在IIS里建一个站点指向这个目录,就算建好了。然后登录进去就可以管理用户,管理资源库了。在mac里的项目修改了remote url以后直接push,成功。

2、有了https://code.google.com/p/tortoisegit/,windows下的git client。跟小乌龟tortoisesvn同出一家(不过没有官方不知道什么原因)。长的也跟tortoisesvn完全一样,安装使用都非常简单。

既然解决了这两个问题,那转换也就没什么阻碍了。不过git的使用跟svn有很大的不同,这个一定要理解的透彻,才能用好git,否则的话,就变成了每次commit都要跟着做一次push,那反而划不来了。git与svn的区别,第一是本地版本库,第二是强大的分支功能。为了增强理解,在学习的过程中专门写下此文,也为以后给大家培训做一下准备。

最首要的一点要理解的是:git是一个基于本地文件系统的版本管理工具,它可以完全无需服务器的工作(当然是指这个项目只有你一个人用,不需要跟其他人协作的情况下。当然,即便是有远程资源库的情况下,你本地的git库里也是保存了所有的完整的版本信息的,而不像svn那样本地只需要保存一份最新的版本。)因此,要想用好git,先要掌握好本地git库的使用方法,假如这个项目没有远程git库的话:

(以下过程均使用git命令行和tortoisegit的菜单的对比方式描述,后者简称win下)

在任意目录执行 git init 就完成了这个项目的git初始化,它就已经在git的版本管理之下了。win下在该目录右键,Git Create repository here…,这时候会弹出一个选项,这个选项的意思是你要把这个目录当成一个工作目录,还是一个单纯的库目录。如果选了作为库目录,这个文件夹其实就相当于一个远程server了,跟你的项目工作目录是分离的,等于有了两份备份。还是先不要选吧,点确定,就初始化好了。初始化完了貌似也没什么变化嘛,它其实只是在当前目录下建了一个.git的目录,所有的版本管理都在这个神奇的目录下。它也不会像svn那样在第一级子目录下建一个.svn目录。(对于大型项目很恐怖吧?)

初始化之后实际的文件还没有进入版本管理,先 git add .或者add *或者add指定的文件名,把文件添加到项目树中,再git commit,这才提交了一个版本进去。你可以随时使用git status查看本地项目的文件状态,git会列出来哪些文件是已修改的,已删除的,已添加的,和新增未添加到git的文件。另一个跟svn的很大的不同是,每个本地文件有三种状态:已提交,已暂存,已修改。已经commit过然后没有修改过的文件就是属于已提交状态,本地修改过但是没有git add过的文件就属于已修改状态,而git add过的文件就属于已暂存。暂存是个git专有的概念,是指把这次你想要提交的文件,放到了一个临时存储区(包括文件内容),但是并没有commit进版本库。如果你使用git commit命令,它只会commit暂存过的内容,其它的修改过的文件也不会自动提交。

git add是个多功能的命令,但是实际的作用就是把准备提交的文件添加到暂存区。如果文件是新的,add会把它添加到暂存区,如果文件修改过,add把它添加到暂存区(准备提交列表),如果一个文件产生了冲突,add把它标记为已解决,准备提交。修改过的文件在调用了add之后再修改,然后直接commit,它提交的是上次add时的内容,而不是最后修改的内容。git diff命令默认显示的是本地修改过的文件,和暂存区中的文件的差异,而不是和版本库中的文件的差异。

如果不想每次提交还要先add一遍,可以直接git commit -a自动add所有修改过的文件(不包含新增但是没有手动add过的文件)。在win下只要右键,Git Commit ->master就可以了。commit看上去跟svn是一样的命令,但是它们最大的差异也在这里。git commit只是把修改过的文件提交到了这个项目目录下的.git目录下去,都是本地操作,所以速度极快。电脑没有网络的情况下也完全没关系。(git commit的日志是强制必填的,这样就不会出现commit了很多次,但是忘了每次是改了什么东西,现在到底应该回滚到哪个版本去的问题。)

切记,本地修改的文件,在commit之后只是提交到本地库,直到push才会提交到远程服务器上。本地的每一次commit都是一个版本,你同样可以回滚到任意一次commit的版本去。比如,如果你本地的某个文件改错了或者被误删了,想回到上个版本,直接git checkout — filename就可以还原(等于svn中的revert)。如果你想回滚某一类文件,比如所有的c源文件,还可以直接git checkout — “*.c”。如果要回滚到两个版本之前,可以git checkout master~2 — filename。如果想把add过的文件取消暂存,避免这次跟其它文件一起提交,可以用git reset HEAD filename,这个文件的本地内容不会变,但是已经取消了暂存状态。

git rm filename可以从版本管理中删除一个文件或目录(包括自动删除本地文件),然后commit。git mv filename newfile可以给文件重命名。git log可以查看commit的日志记录。本地项目根目录下有个.gitignore文件,保存着本项目的忽略文件的列表,一行一个,可以指定具体的文件名和目录名,也可以用泛字符。

如果你commit了之后,发现漏掉了某些文件忘了add,想要补进去,除了补一次commit外,如果不想多占用一次commit,可以使用命令git commit –amend,它可以用来弥补上一次commit的内容,(相当于还原上次commit时的所有文件到暂存状态,再加上你后来add的文件一起提交,而且你还可以重新修改提交日志)。

远程资源库

在建立了远程git项目库的情况下,如果要以你本地的这个目录为初始提交依据,在这个时候在本地执行git remote add origin <address>,这样就建立了本地库和远程库(origin)的关联,然后git push -u origin master,这样你本地的库就跟远程同步了。其它人就可以以这个远程库为基础建立自己的本地库,只要在一个空目录下git clone <address>,就建立了一个资源库的本地克隆,就可以在这个目录里开始工作了。任何一个本地克隆都包含了当前节点服务器端所有的数据库。如果服务器端数据损坏了,还可以以任何一个本地克隆来还原出服务器端的资源库。

一个本地项目,可以保存多个远程地址,这几个远程资源库里的内容可能也是不一样的,比如这个项目的每个用户都有一个自己的远程项目地址。你可以git remote add <remotename> <address>,上面那一段的添加命令里面的origin也就是一个远程资源库的名字,你也可以给它别的名字。不过因为当你clone一个远程库的时候,这个初始的远程地址默认的名字就是origin,所以本地新建的时候建议也用这个名字。

如果你有多个远程地址,你可以选择git push <remotename> master把本地的更新推送到某一个远程服务器上去(如果有权限的话)。git fetch <remotename>会把这个远程资源库的所有版本信息下载下来到本地资源库,但是并不会影响你的当前工作目录的内容。(如果你是一个开源项目的主维护人,其他人clone了你的代码然后把修改后的内容提交到了新建的一个远程库里,你就可以fetch他的新内容下来,把部分修改的内容合并到自己的项目里。)

你可以用git tag <tagname>给最近一次commit的记录打上tag标签,比如某个固定的版本。这样以后很容易的fetch出这个版本的代码进行发布。也可以用git log查看历史提交的信息,然后用git tag -a v1.2 9fceb02(某个commit的校验码的前几位,唯一即可)打标签。本地的标签并不会在push的时候自动推送到服务器上,需要git push origin <tagname>显示推送,或者git push origin –tags推送所有的标签信息。

注意:如果你的项目没有远程地址,或者没有往远程地址上push过内容,那如果你删了本地项目里的.git目录,你所有的历史版本就都丢了。

强大的分支(branch)功能

git的最凶猛之处,莫过于branch了。前面出现的master这个名字,就是你在初始化一个git项目的时候自动创建的一个默认分支。如果你的项目一直都只在一个分支上完成,那可能还体会不到git的美妙。在svn里面,创建和合并分支是一件令人望而却步的操作,而对git来讲,却是一件自然而然的事情。

工作场景:如果你在一个比较复杂的项目上工作,当前发布了一个版本到服务器对外运行。然后接下来需要做一个比较大的改版,涉及的文件比较多,需要的工作时间也很长。当你正做到一半的时候,可能改动了项目里接近1/4的文件,这时候,发布的服务器上发现了一个非常非常紧急的Bug需要修复。你需要立即回到那个版本修改这个Bug并发布到服务器上,然后回到原来的工作中继续改版,并且可能需要把那个Bug的修改也放进来。

这一过程在subversion下实现是极其痛苦的,试过的就知道了。但是在git里要简单的多。

git建议整个项目的主发布版本就命名为master,所以当你有了一个发布版本,而下一个发布版本还需要开发一段时间的时候,就建立一个新的分支来工作。当commit了所有的内容以后,使用命令git checkout -b <branchname>,这个命令相当于先用git branch <branchname>在版本库中新建一个分支,再用git checkout <branchname>切换到这个分支上来工作。所谓分支切换,就是把你当前的工作目录的文件切换到这个分支的最后状态。(建议使用一定的例句规则来区分branch和tag的名字,假设新建的分支叫bv2,为第2版准备的分支,)因为bv2分支是在master的基础上新建的,这时候它的状态跟master完全一样,所以这次切换你的工作目录看上去没有发生任何变化。这时候你开始做大改版的工作。

改版进行到一半的时候,服务器上发现了严重的需要立即修复的Bug,这时候你需要切换回当时发布的master分支的最后状态去修改这个Bug。提交所有的已修改的内容,然后git checkout master,这时你的工作目录中后来修改的东西都消失了,回到了当时master的状态。此时不建议直接在master分支上进行修改,而是新建一个分支来修改。git checkout -b bug33,这样你已经切换到了这个分支上,而它的内容跟master是一致的。修改了N个文件解决了Bug以后,再把这个分支合并到master里去。先git checkout master切换回master分支,然后git merge bug33,因为bug33是直接在master上修改的,不存在冲突的可能,所以这次合并几乎是瞬间完成的。此时master已经是最新的代码,跟bug33分支的内容一样,bug33留着也已经没用了。git branch -d bug33删除这个分支。然后git checkout bv2将当前目录切换回原来改版的内容,继续改版工作。

当v2版已经完成,通过了测试,可以进行发布的时候,将它合并到主分支上。先切换到master分支,git checkout master,然后合并,git merge bv2(建议合并之前对master打一个v1版本的tag),如果一切顺利,合并会自动完成,master就拥有了bv2的所有修改,同时又有bug33所修改的内容。但是如果这两个分支中修改了同一个文件的同一段内容,合并不会自动完成,这个(或者一些)文件会标记为冲突,用git status可以查看哪些文件冲突,打开文件的内容,冲突的标记方式跟svn是一样的,有=======这种标记来区分,修改文件的内容,只留下想要的部分,然后git add filename,把它标记为已解决冲突,可以提交,然后git commit就可以了。这次合并就完成了。然后你可以删除掉bv2分支。正式发布以后你就可以再开一个新的分支继续下一次修改(无论大小)。这样可以保证你的服务器上发布的内容永远是稳定的,而不是总是被迫把改了一半的内容更新上去。

git的分支创建动作几乎不修改文件,所以是瞬间完成的,而分支切换动作是从本地的版本库中取出指定的分支版本的内容解压覆盖当前工作目录的差异内容,速度取决于版本的差异和项目的大小,但是无论如何都比svn这种从服务器上下载一个完整的分支所包含的完整的项目文件要快的多。而且git的合并分支所使用的版本计算和差异计算的方式也跟svn有很大的区别,几乎是无痛的。

git branch可以查看本地包含的分支,git branch –merge或者–no-merge可以查看已合并到当前或没合并的分支。用-d删除未合并的分支会报错,因为这意味着你会丢掉那个分支上所有的修改,但是你仍然可以用-D选项强制删除未合并的分支,并放弃那个分支上所做的修改。

记住:在 Git 中,一天之内建立、使用、合并再删除多个分支是常见的事。你可以用分支来管理一个新的测试版本,一个新的功能点的开发,或者只是一个Bug的修复,然后合并并删除它。而在合并之前,这个修改不会对你其它的分支产生任何影响。你可以在一个分支上的开发中途再新建另一个分支,最终将这个新的合并到主分支,并放弃第一个分支的部分内容(建立了新分支以后commit的内容)。你可以在master之外同时有多个不同的分支用来开发不同的部分的新代码,然后同时将它们合并进master分支。这些都不是问题。而且,这些全部都是在你的本地完成的。当然,如果你在本地新建了很多个分支,并同时保持,那你一定要记清楚每个分支是干什么的,它们的代码有什么区别。

远程分支的使用

当一个分支是用来完成一个大的项目的开发的时候,这个分支就不是你自己在用了,需要提交到远程与他人协作,另外也需要通过这个分支来发布到测试服务器。

当你在本地增加了一个远程服务器的时候,服务器本身有一个名字,而服务器上所拥有的分支的名字在你的本地看起来就像<remotename>/<branchname>这样,比如origin/master,这个就是当你克隆一个服务器端最新代码的时候系统默认创建的一个origin远程名字和它里面的master主分支,然后你本地的master分支是从这个origin/master上延伸出来的。你不能修改和commit远程分支的内容,必须从远程分支的基础上创建一个本地分支(git内这个叫作跟踪远程分支track),然后在这个本地分支上修改内容,commit,然后push到远程分支上去。别人在fetch或者pull的时候再来获取你更新过的分支内容,和自己本地跟踪的同一个分支的内容进行合并。如果你们在同一个分支里修改了同一个文件的同一部分,合并过程中需要手动处理冲突。

一般情况下,当你git push的时候,你本地创建的分支是不会同步到服务器上的,所以有些分支你在本地创建,开发,合并,然后删除,其它队员是不可见的。如果你想把一个分支发布到服务器上,让其他人也在这个分支进行开发,比如bv2,你可以指定git push origin bv2,这样就会把你的bv2分支的所有提交同步到远程。别人在git fetch origin服务器以后,就可以看到origin/bv2分支,如果他想把自己的工作目录切换到这个分支下工作,需要git checkout -b bv2 origin/bv2来创建并导出一个本地跟踪分支,然后开始工作,并在push的时候把自己的修改合并上去,你再git pull的时候,就会获取远程别人提交的这个分支的内容,同时合并修改内容到你当前的工作目录(如果你仍然在bv2下工作的话)。

结语

有了以上这些内容,基本上应该可以完整的体验git所带来的好处了,再也不用忍受svn的速度问题了。

不过,在这之前,可能你还需要一个能够方便的测试的本地测试服务器,这样你开发的内容就不需要频繁的更新到远程服务器上才能进行测试。当然,你也可以在项目中使用多个远程git资源库地址,一个用来跟踪主发布进程,一个用来提交分支进程到服务器上,然后在测试服务器上更新这个分支的内容做测试发布。最后再把本地的内容合并到master,然后发布到主git资源库并推送到发布服务器上。