分享项目中使用Git的一些经验

目录

  • 前言
  • Git必须了解的一些知识
  • Git的GUI客户端
  • 经验之谈

前言

回想了这一年来的经验,觉得好饱满,团队磨合得很快,也非常顺利,一切都有条不紊地进行着。总结一下这一年来使用Git的经验,特别是在项目使用过程中解决提交冲突、版本发布处理和代码回滚修复BUG这些方面的一些经验。

本文不讲使用git命令或者使用Git客户端的操作,这些会另开一篇文章讲解,只讲原理。

文中斜体字内容是引用自网上。

Git必须了解的一些知识

以前在做C++项目时用的是SVN,切换到Git需要一点点适应和慢慢地一些试错,从当前项目的需求来说,两者在提高工作效率方面达到的效果是差不多的。(当然,程序员总是偏执的,大部分总是觉得Git比SVN好,就像觉得Unix/Linux比Windows好)。网上可以搜到很多关于他们区别的文章,有兴趣可以看下。

下面的一些知识,我觉得是比较重要的,特别是有助于准确地解决代码冲突、回滚修复BUG方面。

什么是版本控制

我举个简单例子就明白了:原先没有版本控制时,我们习惯用复制整个项目目录的方式来保存不同的版本,用注明时间或者版本号的方法来区分。而现在我们只需打个branch就行了,我们还可以具体看到每个版本间的区别,通过每个文件的log看到文件经过哪几次,什么样的修改。

这样的优劣不明而喻了。

从根本上来讲 Git 是一套内容寻址 (content-addressable) 文件系统,我觉得这句话说得很不错!

分布式

Git也有集中式版本库或服务器,但更倾向于被使用于分布式模式,所以我们从中心版本库/服务器上chect out代码后会在自己的机器上克隆一个自己的版本库。

在不联网的情况下,仍然能够提交文件,查看历史版本记录,创建项目分支等。所以可以一直本地git commit,等到功能模块完成再git push上去。在实际工作中,我更喜欢短暂性地忽略远程仓库。

可以想像到的一个场景是:在同一个项目中分别和不同工作小组的人相互协作哈哈。

SVN是集中式的,我的理解是,集中式是为了让在不同系统上的开发者协同工作,但显而易见的缺点是中央服务器故障会严重影响协同工作,要是没有备份会有丢失数据的风险。

文件的三种状态

对于任何一个文件,在Git内都只有三种状态:已提交(committed),已修改(modified)和已暂存(staged)。已提交表示该文件已经被安全地保存在本地数据库 中了;已修改表示修改了某个文件,但还没有提交保存;已暂存表示把已修改的文件放在下次提交时要保存的清单中。

由此可以看到,本地文件流转有三个工作区域:

  1. 本地仓库,如果git clone出来的话,就是其中.git的目录。
  2. Git的工作目录,从项目中取出某个版本的所有文件和目录,用以开始后续工作的叫做工作目录。这些文件实际上都是从 Git 目录中的压缩对象数据库中提取出来的。
  3. 暂存区域。

直接记录快照,而非差异比较

Git只关心文件数据的整体是否发生变化,而大多数其他系统则只关心文件内容的具体差异。

Git并不保存这些前后变化的差异数据。实际上,Git 更像是把变化的文件作快照后,记录在一个微型的文件系统中。每次提交更新时,它会纵览一遍所有文件的指纹信息并对文件作一快照,然后保存一个指向这次快照的索引。

在保存到Git之前,所有数据都要进行内容的校验和(checksum)计算,并将此结果作为数据的唯一标识和索引。换句话说,不可能在你修改了文件或目录之后,Git一无所知。这项特性作为Git的设计哲学,建在整体架构的最底层。所以如果文件在传输时变得不完整,或者磁盘损坏导致文件数据缺失,Git都能立即察觉。

Git 的工作完全依赖于这类指纹字串,所以你会经常看到这样的哈希值。实际上,所有保存在 Git 数据库中的东西都是用此哈希值来作索引的,而不是靠文件名。

在 Git 中提交时,会保存一个提交(commit)对象,该对象包含一个指向暂存内容快照的指针,包含本次提交的作者等相关附属信息,包含零个或多个指向该提交对象的父对象指针:首次提交是没有直接祖先的,普通提交有一个祖先,由两个或多个分支合并产生的提交则有多个祖先。

在解决冲突的时候经常会看到一串字母,这串字母就是这些索引了。

指针

我的理解,Git的任何的东西,如分支、记录等的实现都是依靠指针。

比如创建一个分支,其实就是创建一个新的分支指针,然后这个指针指向了当前指定的commit对象。

同样的,Git如何知道你当前在哪个分支上工作?它保存着一个名为HEAD的特别指针,指向你正在工作中的本地分支的指针(类似当前分支的别名)。可以试着查看下它的内容:

1
2
$ cat .git/HEAD
ref: refs/heads/master

所以有时解决冲突的时候,或者发生什么错误的时候经常可以看到HEAD这个单词。

在解决很多问题的时候,我觉得把所有提交(commit)对象想像成叶子,组成一棵树,所有的分支等都是指针,指向某个commit对象,这样子可以想通很多事情。

传输数据的协议

Git可以使用四种主要的协议来传输数据:本地传输,SSH协议,Git协议和HTTP协议。下面分别介绍一下哪些情形应该使用(或避免使用)这些协议。

具体上网看看咯,我们用HTTP/S协议。

Git的GUI客户端

出于提高工作效率的目的下,选择一款GUI客户端是必要的(不要争论用命令行更好,实践证明很多复杂的操作用客户端更直观实际,这也是客户端存在的原因)。

网上比较多的有几款:

  1. Github for Windows,官方客户端,比较美观,功能略少不过也够用
  2. SmartGit,功能较全,UI中规中矩
  3. SourceTree,Msysgit,TortoiseGit

我用的是SmartGit,从普通的提交、拉取、建立分支、打Tag,到进一步的代码回滚、版本控制、解决冲突,都能满足我项目的需求。

经验之谈

纯粹是个人经验,有纰漏有错误的地方恳请指出。

很多东西通过SmartGit完成,限于篇幅和时间关系,这里我只介绍一些原理,可能另开一篇讲解SmartGit的使用,特别是代码回滚和版本打包方面。

正常操作流程

正常流程一般是:

  1. 功能模块完整完成,并且至少是编译通过的
  2. 选择变动的文件(包括删除的、添加的),将它们变为staged
  3. commit提交
  4. 然后git pull,这时候会拉取线上的修改,可以看到工作目录的内容发生改变
  5. 有冲突的话,解决冲突。将它们变为staged,然后重新commit
  6. git push到所要的分支

冲突解决

这一方面Git帮我们处理得很不错了,至少目前发现过任何差错。Git一般会自动帮助我们进行代码的合并,在一些它无法决定的地方会提示出来让我们自己解决。

所以其实冲突解决并不是什么可怕的事情,如果工作组中每个开发人员懂得如何处理,可以省去很多麻烦(不用争着谁先上传谁慢上传)。

冲突发生在拉取远程仓库代码时,本地修改的数据和远程代码有冲突。于是实质就是,两个不同的提交对象造成的冲突。在冲突的文件中,Git会用类似于下面的格式将两个提交对象不同的地方区分开来:

1
2
3
4
5
<<<<<<< HEAD
zzz
=======
zzzzz
>>>>>>> 6853e5ff961e684d3a6c02d4d06183b5ff330dcc

“<<<<<<<”与“=======”之间的内容为本地的内容,“=======”与“>>>>>>>”之间的内容为远程的内容。

上面的冲突情况是:本地某行内容是zzz的与线上代码冲突,冲突内容是zzzzz。你可以选择保留“zzz”或者“zzzzz”,只需把其他无用信息删除即可。

在iOS开发中,冲突有另外两种特别的情况,分别是Storyboard文件冲突和项目文件冲突,特别是项目文件一旦冲突Xcode就无法识别项目文件的组织结构,导致无法显示项目的所有文件。其实解决办法也是一样的,查看该文件内容的时候,也是用上面提到的格式区分开的。

Storyboard文件可以右键open as —> source code,修改冲突的地方后open as —> interface Builder,即可查看是否修改成功。

项目文件只需用能打开文本的方式打开即可,如Vi之类的。

版本提交处理和回滚修复BUG

我们团队目前打包提交审核由我一人处理,所以我们的分工和流程如下:

  1. 版本开发完提交测试时,我开始在master分支最后一个commit对象处新建一个分支,命名为“当前外部版本号内部版本号”,如“2.8.3/38”。(其实内部版本号对于提交测试时,是个很有用的东西,在提交到App Store被退回的时候我才意识到,但我们一直没怎么用到)
  2. 把该分支push到服务器上
  3. 此时其他iOS开发人员是不需pull该分支的,可以照常在主分支上开发下一个迭代的需求。
  4. 当上一个迭代的版本出现BUG需要修复时,有两个解决方式:一个是在当前主分支修复,push,然后再切换到上一个版本的分支,cherry pick下主分支上修复BUG的commit(有可能需要解决冲突),然后再push,此时就push到上一个迭代版本的分支了。然后再发布上一个版本。二是切换到上一个版本的分支,修复后再回主分支cherry pick。
  5. 我们的流程是,如果是其他开发人员修复,则在主分支修复并push上去,然后我切换到上一个版本的分支,cherry pick后push,然后发布版本。如果是我修复,则看情况,一般采用第一种方便。
  6. 如果修复的内容在当前版本是已经不需要的了(如功能模块已经移除),则切换到上一个版本的分支修复后,主分支不需再cherry pick即可。

关于打Tag

Tag对象非常像一个commit对象——包含一个标签,一组数据,一个消息和一个指针。最主要的区别就是Tag对象指向一个commit而不是一个tree。它就像是一个分支引用,但是不会变化——永远指向同一个commit,仅仅是提供一个更加友好的名字。

基于以上对于Tag对象的解释,我暂时还没发现Tag在自己项目中能有什么作用,最多就备注一些信息,但是每个commit提交时都会注明修改了什么,所以。。。

-END-
欢迎到我的博客交流:https://zackzheng.info