[TOC]
聊聊Git的使用
一、Git简介
Git是目前世界上最先进的分布式版本控制系统(没有之一)。这个人说的——廖雪峰。
1.1 分布式vs集中式
先说集中式版本控制系统,版本库是集中存放在中央服务器的,而干活的时候,用的都是自己的电脑,所以要先从中央服务器取得最新的版本,然后开始干活,干完活了,再把自己的活推送给中央服务器。中央服务器就好比是一个图书馆,你要改一本书,必须先从图书馆借出来,然后回到家自己改,改完了,再放回图书馆。
<center><font color="#888888" size=2>图一:集中式</font></center>
分布式版本控制系统没有“中央服务器”,每个人的电脑上都是一个完整的版本库,这样,你工作的时候,就不需要联网了,因为版本库就在你自己的电脑上。既然每个人电脑上都有一个完整的版本库,那多个人如何协作呢?比方说你在自己电脑上改了文件A,你的同事也在他的电脑上改了文件A,这时,你们俩之间只需把各自的修改推送给对方,就可以互相看到对方的修改了。分布式版本控制系统通常也有一台充当“中央服务器”的电脑,但这个服务器的作用仅仅是用来方便“交换”大家的修改。
<center><font color="#888888" size=2>图二:分布式</font></center>
1.2 Git相对SVN的优势
- Git每个机器都保存了代码库的完整副本;
- Git 的内容完整性要优于SVN,Git使用文件的哈希值索引文件而不是文件名;
- Git的Commit操作不需要联网,速度快、独立化有利于模块化开发;
- Git查看log等日志文件不需要联网,速度快;
- Git的分支管理机制远远领先于SVN:
- 速度:SVN建立新分支是拷贝出一个当前分支的副本,是一个新的完整的目录,而Git相当只拷贝出一个指针因此速度不在一个量级上;
- 独立性:新建了SVN分支,则SVN会在远端立刻生成一个新的分支,此时团队每个成员均可以获取到该分支代码,而Git可在本地完成所有必要的修改再提交到远端,甚至可以在本地合并到主分支再推送到远端;
- 灵活性:譬如SVN在切换分支前,必须将代码推送到远端,而Git则既可以提交到本地分支,又可以灵活地使用git stash push/pop命令暂存/恢复工作区代码;
1.3 Git仓库组成
打开terminal工具,新建一个Git版本库仅需要两行命令:
mkdir testgit && cd testgit # 新建工作目录后进入工作目录
git init # 初始化版本库
git init
命令执行完成后,会在当前工作目录生成一个.git文件夹,当前工作目录就成为新建版本库的工作区(Working Directory),而.git文件夹则保存了git版本管理所必须的文件也就是真正的版本库所在,版本库中包含了暂存区(Index/Stage)、仓库区(Repository)如下图所示:
<center><font color="#888888" size=2>图一:工作区、暂存区、仓库区</font></center>
打开.git目录,可看到目录中主要包括,其中index文件对应上图中的stage暂存区,:
- COMMIT_EDITMSG
- config
- description
- HEAD:HEAD指向的快照object的引用
- index:暂存区(工作区的修改通过
git add
添加到暂存区时记录到index文件,该文件是git仓库文件系统的索引文件,记录了工作区中所有文件的快照object的索引)。若要看看里面有什么内容可以通过git ls-files --stage
命令,打印的内容如下:- 第一列:
- 第二列:暂存区中该文件最近一次快照object的哈希值;
- 第三列:
- 第四列:文件名;
- [branches]
- [hooks]:
- [info]:
- [logs]:本地日志
- [objects]:版本库整个生命周期内生成的所有快照object(区分快照和备份)。若要看看里面有什么内容可以通过
git cat-file -p <object hash>
,其主要内容包括了- tree:工作目录的快照object(tree)
- parent:上一次提交的快照object(blob),合并的节点会有两个parent
- author:作者+邮箱+时间戳
- committer:提交者+邮箱+时间戳
- commit message:提交时
-m
指定的提交日志
- [refs]:
- stash:指向stash栈的快照object的引用
- [heads]:指向所有本地版本库的分支快照object的引用
- [remotes]:指向所有远程版本库的分支快照object的引用
- [tags]:指向所有tag的快照object的引用
至此,上文已经提到了诸多关于Git的概念如工作区、暂存区、仓库区、HEAD引用、分支等名词,这里先不作深究,后文中具体使用时自然可以很好的理解。
二、Hello Git
该部分介绍git的基本使用。
2.1 添加并提交修改
上一部分,我们新建了一个testgit版本库,现在我们往版本库中加入文件:
vim readme.md # 在vim中输入Hello git!后使用`:wq`命令保存并退出
# 其他编辑文件命令
# rm -rvf <file or directory> # 该删除命令十分强力需谨慎使用
# mv <file or directory> <file or directory> # 移动或重命名
# mkdir <directory> # 新建文件夹
# touch <directory> # 新建文件
可以看到目录中增加了readme.md文本文件,现在我们查看一下版本库的变更:
git status
更具体的文件内容的变更,用diff命令查看,使用例如git diff > ~/Desktop/diff-out.txt
命令可以将diff内容输出到指定文本文件。一般指向当前分支顶部的HEAD
指针,在diff命令中可以当作<commit id>使用,表示HEAD指向的分支的最近一次提交,HEAD~3
是指HEAD指向的分支的最近3次提交提交,HEAD^
则是HEAD指向的分支的最近一次提交的上一次提交:
git diff <file or directory> # 比较工作区与暂存区
git diff --cached <file or directory> # 比较暂存区与HEAD
git diff <commit id> <file or directory> # 比较工作区与指定commit
git diff --cached <commit id> <file or directory> # 比较暂存区与指定commit
git diff <commit id> <commit id> # 比较两次提交,注意第一个<commit id>一定要早于第二个<commit id>,否则
可以看到Git追踪到的工作区变动,然后将工作区变动添加到暂存区:
git add <file or directory> #支持通配符*(任意多个字符)、?(任意一个字符)
执行git add
命令时,git对指定的文件和目录生成快照object保存到objects文件夹并记录到暂存区。先暂存后提交的好处是可以将生成文件快照object的性能负载分散到各次add操作中,在下面Commit提交时仅需对暂存区中的内容,生成提交的哈希值(快照object文件路径)、提交的log message(提交日志)、并将暂存区中的快照object拷贝到objects文件夹特定目录(根据上面的哈希值)中生成一次提交即可。提交的命令如下:
git commit <file or directory> -m "<commit message>" # 提交暂存区内容
git commit -a -m <file or directory> -m "<commit message>" # 将指定文件或目录,无论有没有追加到暂存区,均生成快照并提交
git commit --amend <file or directory> # 也叫追加提交,它可以在不增加一个新的commit id的情况下将新修改的代码追加到前一次的<commit id>中
2.2 修改撤销
2.2.1 Git支持的撤销操作
从添加文件、编辑文件、add、commit整个本地的管理代码文件的过程,用户有三次“反悔”的机会,分别是工作区修改撤销、暂存区修改撤销、提交回滚。
- 工作区撤销:
增删改工作区的文件,git可以追踪到工作区修改的内容(工作区与暂存区的差异),由上文可知可以通过git status
命令查看修改的文件及目录列表,通过git diff
可以查看工作区的具体修改内容,此时代码仍在工作区,若想撤销工作区的修改,可以使用git checkout
系列命令实现:
git checkout -- <file or directory> # 将工作区中文件或目录恢复到暂存区所记录的对应文件或目录的状态,其中“--”符号可以省略
- 暂存区撤销:
若文件已经添加到暂存区,可以分两步撤销。先将暂存区中的代码恢复到工作区,然后再用上面的方法撤销工作区的修改。将暂存区的代码恢复到工作区的命令是
git reset HEAD <file or directory>
-
版本回滚:
若文件已经提交并生成一个新的commit提交,撤销提交的操作也叫版本回滚,版本回滚的原理可以看以下两张图示,实际上就是将HEAD指针指向之前的版本,然后根据需要处理工作区、暂存区中的内容
<center><font color="#888888" size=2>图一:HEAD初始指向</font></center>
<center><font color="#888888" size=2>图二:版本回滚后的HEAD指向</font></center>
注意:
git checkout
和git reset
的区别是,前者是直接把HEAD指针指向指定的节点,当前的分支的指针仍然指向分支顶部节点,HEAD指针不是指向某个分支的指针,此时处于detached state,一般不在这种状态下开发代码只做查看;后者是把分支的指针指向指定的节点,同时HEAD指针也指向分支的指针。
2.2.2 版本回滚命令详解
版本回滚同样通过reset
系列命令实现,版本回滚有三种方式--soft
、--mixed
、--hard
,默认是--mixed
方式。回滚命令如下所示:
git reset --soft <file or directory> # HEAD从源commit指向回滚版本commit,原暂存区及工作区的修改集不变,回滚版本commit到源commit所做的修改集恢复到“暂存区”中;
git reset --mixed <file or directory> # HEAD从源commit指向回滚版本commit,原暂存区中的修改集回退到工作区中,原工作区中的修改集不变,回滚版本commit到源commit所做的修改集恢复到“工作区”中;
git reset --hard <file or directory> # HEAD从源commit指向回滚版本commit,丢弃原暂存区与工作区中的修改集;
<center><font color="#888888" size=2>图一:初始化仓库</font></center>
<center><font color="#888888" size=2>图二:使用soft回滚选项</font></center>
<center><font color="#888888" size=2>图三:使用mixed回滚选项</font></center>
<center><font color="#888888" size=2>图四:使用hard回滚选项</font></center>
版本回滚还可以使用git revert
命令,与git reset
的区别是,使用git reset
回滚是直接改变HEAD指针的指向,而git revert
则是将代码恢复到指定节点的状态,并生成一个新的提交把HEAD指向该提交。且git revert
不可以针对单文件回滚。前者版本树丢弃了回滚目标节点之后的修改(实际上只要记住版本号还是可以检索到被丢弃的节点),后者保留。
三、Git分支
分支是Git的精髓所在,其最大的特点就是快。分支的概念可以用一个成语形容殊途同归。其意义在于分支上的开发不会受到其他分支的开发代码的影响,而且能够自由控制在何时将开发代码提交到主干版本,同样也能自由合并其他分支的代码。
试想当我们接到多个新需求,这些需求下个版本可能只上线其中一部分,若直接在master分支中开发显然是不合理,我们要么需要在上线前加上临时的功能屏蔽代码,或者先开发下个版本发布的功能 而在下个版本正式发布后才启动其他功能的开发,前者容易出现屏蔽的疏漏、后者可能导致开发资源分配不合理。分支能够很好地解决上述的问题,我们为需求开辟新的分支,然后在该需求的分支上开发,仅当功能在本地测试通过后再合并到master提交测试,使用分支的一般操作如下:
一、创建分支前,仓库一般有一个默认的master分支,当前的HEAD指针一般指向master分支的顶端,如下图所示,
二、通过git branch
命令创建dev分支,此时HEAD指针仍指向master分支,再通过git checkout
命令检出到dev分支,此时HEAD指针转而指向dev分支,检出dev分支后如下图所示
git branch <branch name>
三、在dev分支上完成开发并提交后,先检出分支到master,此时HEAD指针指回master分支,然后使用git merge
命令将dev分支合并到master分支:
git checkout master
git merge dev
若提示存在冲突(conflict),按以下步骤解决:
先
git status
查看both modify的文件然后在文件中搜索“>>>>>”或“<<<<<”或“=====”,根据实际需求选择代码是全部保留、或是只保留其中一个分支、或者重新编写冲突部分的逻辑的解决方法,强烈反对在没弄清代码逻辑之前,直接使用自己分支的代码 或保留双方代码来解决冲突;
完成修改后提交代码,使用命令
git commit -m "<commit message>"
。注意出现冲突时不能部分提交,只能全量提交;
四、通过git branch
命令删除dev分支,删除dev分支时当前分支一定不能是dev,否则不能删除。注意删除分支的操作最好是在需求发布之后,在需要代码回滚时,有原分支的情况下会有更多可选的解决方案
git branch -d <branch name> # 若有代码未合并会先报错,此时确认代码可丢弃后用git branch -D <branch name>可强制删除分支
注意:分支合并有两种方式no-fast-forward和fast-forward,其区别是
--no-ff
生成合并Commit日志并保留原分支的提交时间线;而--ff
直接将目标分支的提交并入主分支的时间线。默认是fast-forward。两者区别如下图所示
四、Git实用举例
4.1 常用命令
- 牛逼的查看日志命令:
git log --oneline # 只打印短版本号和日志
git log -p # 打印具体变更内容
git log --stat # 打印变更文件列表
git log --committer <committer> # 只打印由某人提交的代码
git log --grep "<commit message keyword>" # 根据提交的message关键字搜索日志
git log --before "<date>" # 只打印<date>之前的日志,例如git log --before "2017-10-10 12:00:00"
git log --after "<date>" # 只打印<date>之后的日志
git log --graph # 显示ASCII图形表示的分支合并历史
git log -G "<content keyword>" # 根据提交的内容搜索,由于搜索范围较大因此速度一般会比较慢
git log --no-merges # 过滤掉合并日志
git log --first-parent # 过滤掉其他分支上提交的日志只显示其合并日志
git log > <file path> # 输出日志内容到文件
- 本地分支推送到远端:
git push --set-upstream origin <branch name>
- 删除远程分支:
git push origin --delete <branch name>
- 删除本地缓存的已删除的远程分支:
git remote update # 若远端创建分支后,在本地创建远程追踪分支
git remote prune origin --dry-run # 查看哪些分支需要清理
git remote prune origin # 清理失效分支
单文件版本回滚:(见前文,具体push时可能需要加
--force
)全量版本回滚:(见前文,具体push时可能需要加
--force
,若合并分支后解决冲突的过程中又想放弃刚刚的合并可使用命令git merge --abort
撤销上一次合并操作)如果本地已经修改了一些文件,然后由于某些需求必须先合并另外一个分支,而此时又不想生成一次新的提交,可以将代码暂存到暂存栈,待合并完成后再从暂存栈推出(这种情况下,先生成一个提交后续再用
git commit --amend
追加提交也是可以的):
git stash push # 将修改内容推入暂存栈
git status # 当前工作区是否clean
git merge master # 完成合并分支、解决冲突等操作
git stash pop # 推出修改内容
指定版本库忽略文件:使用.gitignore文件指定
远程相关命令:
git remote get-url origin # 获取git远程仓库的地址
git remote set-url origin <git-url> # 设置git远程仓库的地址
4.2 结合实际场景
4.2.1 场景1:上线当天版本回退
实际开发中有时会出现原先规划在下个版本上线的需求,延期到后续的版本再上线,而此时代码已经提交的master分支。遇到这种情况该如何解决呢?
方法一:若功能的入口非常明确,可以用少量的代码屏蔽,则增加暂时屏蔽的代码(可选择#if 0预编译指令、#ifdef/#ifndef DEBUG预编译指令、注释代码等任意一种方法),注意最好打上// TODO:
待办事件注释以备忘;
方法二:若需求提交节点离当前HEAD节点很近,则参照上文使用全量代码版本回退的方法进行回退,注意在需求上线以前一定要保留其对应的feature分支,这样即使master分支代码回滚后,还可以很容易地找回代码;
方法三:当需求分支涉及的文件比较少,其独立于其他需求,则参照上文使用部分代码版本回退的方法进行回退;
以上涉及版本回滚的操作,一般需要以遵循以下规则:
- 尽量保持分支独立,新建分支后尽量不合并其他分支代码,包括master分支;
- 使用
--amend
选项提交,减少不必要的日志; - 重视
-m
字段的书写,有利于日志搜索; - 合并代码时最好使用
--no-ff
选项; - 对分支修改了哪些文件需要了然于心,至少要可以通过灵活使用log命令查看;
下面以最近一次升级的智能组网增加字段的需求上线当天版本回退的场景为例。该需求的开发的具体情况是:1、智能组网需求是分期交付且前后经过两人开发,三期的扩展方式是拷贝了二期的一个副本然后编辑,而由于系统中还存在一些遗留的二期工单,因此二期的页面也没有废弃,因此本次开发将二期三期页面整合为一个文件,避免一个功能需两处改动的麻烦;2、开发过程中有反馈智能组网模块的问题,自测中也有发现一些漏洞,直接在新整合页面中解决(对旧文件为何出现此种情况不太清楚);3、页面中的XIB文件有改动。针对以上情况,制定的回滚策略是:1、回退XIB文件(本场景中XIB文件的布局改动较大);2、保留.h、.m文件的整合逻辑(保留解决遗留Bug的代码),用//TODO:
注释暂时屏蔽新功能逻辑。
4.2.1.1 XIB文件版本回退;
- 切到智能组网分支
git checkout dev-1212-智能组网业务服开接口增加字段`
- 合并master上的修改;
git merge master
- 查看XIB提交日志
git log --online GDWWNOP/家宽开通/工单详情/待处理信息/智能组网/二期/View/GDIntelNetInstallInfoSectionCell.xib`,
输出以下内容:
aae2ae42 1、智能组网业务服开接口增加字段:上门测试及整改增加界面图片框、剩余具体字段的处理,2、智能组网二期三期整合(yanyongcai)
bb753795 智能组网增加字段(chenhuijin)
c1a01372 智能组网增加字段(chenhuijin)
fabc49ea 家宽开通虚拟文件夹创建(liupengcheng)
- 回退XIB文件
git reset bb753795 GDWWNOP/家宽开通/工单详情/待处理信息/智能组网/二期/View/GDIntelNetInstallInfoSectionCell.xib`
执行
git status
,查看到工作区中有本地修改,此时运行Target得到的结果跟回滚前是一样的,原因是使用缺省的--mixed
版本回滚所需修改在暂存区中,回滚版本后续的修改在工作区中,所以彼此抵消了;丢弃工作区中的本地修改
git checkout GDWWNOP/家宽开通/工单详情/待处理信息/智能组网/二期/View/GDIntelNetInstallInfoSectionCell.xib`
4.2.1.2 Controller文件使用TODO注释屏蔽;
修改GDJKKTIntelNetVisitTestController.m 文件,注释添加字段相关内容并添加“//TODO: 屏蔽智能组网增加字段” 的注释;
修改GDJKKTLightChangeController.m 文件,注释添加字段相关内容并添加“//TODO: 屏蔽智能组网增加字段” 的注释;
提交回退及修改,生成的提交号为(a9978564):
git add .
git commit -m "智能组网业务服开接口增加字段:回退XIB,并屏蔽GDJKKTIntelNetVisitTestController.m、GDJKKTLightChangeController.m中的相关代码"
git checkout master && git merge dev-1212-智能组网业务服开接口增加字段
至此完成版本回退,可提交代码发布版本。
4.2.1.3 恢复所需的操作
由于需求是延期上线,并不是永久废弃新功能代码,因此我们需要在智能组网分支中将之前的回滚和修改再恢复原样。此时直接使用git reset --hard
全量回滚到合并前的版本是不可行的,因为该命令不会生成日志,而是直接把HEAD指针往回移动,因此合并到master分支后,上一部分的回滚提交仍然是位于智能组网分支的HEAD指针之后,因此合并智能组网分支不会对master产生任何作用。而使用git reset <file or directory>
命令,会在智能组网分支生成一个新的提交,这个提交是在回滚节点之后,因此合并智能组网分支恢复操作同样能作用到master分支。
- 检出到智能组网分支
git checkout dev-1212-智能组网业务服开接口增加字段
- 回滚文件,此时回滚所作修改保存在暂存区,回滚版本后续的修改保存在工作
git reset 2c20de30 GDWWNOP/家宽开通/工单详情/待处理信息/智能组网/二期/View/GDIntelNetInstallInfoSectionCell.xib GDWWNOP/家宽开通/工单详情/待处理信息/智能组网/GDJKKTIntelNetVisitTestController.m GDWWNOP/家宽开通/工单详情/光功率整改/质检整改/GDJKKTLightChangeController.m
- 废弃工作区中回滚版本后续的修改
git checkout GDWWNOP/家宽开通/工单详情/待 处理信息/智能组网/二期/View/GDIntelNetInstallInfoSectionCell.xib GDWWNOP/家宽开通/工单详情/待处理信息/智能组网/GDJKKTIntelNetVisitTestController.m GDWWNOP/家宽开通/工单详情/光功率整改/质检整改/GDJKKTLightChangeController.m
- 全量提交代码
git commit -m "智能组网业务服开接口增加字 段:恢复增加字段"
至此,到该需求上线时,再将dev-1212-智能组网业务服开接口增加字段
分支,合并回master
即可。
五、Git远程仓库搭建
5.1 利用Docker在MacOS上运行Gitlab服务器
安装Docker 注意下载前必须注册Docker账号,注册过程中引用到的i'm not a robot插件必须翻墙才能使用。下载图形界面客户端Kitematic可以更方便地使用Docker,接下来的步骤都是使用Docker实现;
-
下载gitlab-ce镜像
docker pull gitlab/gitlab-ce
-
运行gitlab实例
GITLAB_HOME=`pwd`/data/gitlab \ docker run -d \ --hostname gitlab \ --publish 8443:443 --publish 80:80 --publish 2222:22 \ --name gitlab \ --restart always \ --volume $GITLAB_HOME/config:/etc/gitlab \ --volume $GITLAB_HOME/logs:/var/log/gitlab \ --volume $GITLAB_HOME/data:/var/opt/gitlab \ gitlab/gitlab-ce
-
用Docker配置gitlab-ce实例,先配置邮箱
docker exec -t -i gitlab vim /etc/gitlab/gitlab.rb
用
/gitlab_rails['smtp_enable']
命令搜索到目标行,完成后:wq
命令保存退出,该配置文件并不是在系统的/etc目录下,而是在Docker的gitlab-ce容器中。以163邮箱为例。注意xxxx@163.com
代表用户名,即邮箱地址,而xxxxpassword
不是邮箱的登陆密码而是网易邮箱的客户端授权密码, 再网易邮箱web页面的设置-POP3/SMTP/IMAP-客户端授权密码
查看。gitlab_rails['smtp_enable'] = true gitlab_rails['smtp_address'] = "smtp.163.com" gitlab_rails['smtp_port'] = 25 gitlab_rails['smtp_user_name'] = "xxxx@163.com" gitlab_rails['smtp_password'] = "xxxxpassword" gitlab_rails['smtp_domain'] = "163.com" gitlab_rails['smtp_authentication'] = "login" gitlab_rails['smtp_enable_starttls_auto'] = false gitlab_rails['smtp_openssl_verify_mode'] = "peer" gitlab_rails['gitlab_email_from'] = "xxxx@163.com" user["git_user_email"] = "xxxx@163.com"
-
配置gitlab外部访问url,这个必须配置,否则默认以容器的主机名作为URL,刚开始由于做了端口映射80->8080, 因此设置为
external_url "http://192.168.0.1:8080"
后来发现external_url只能配置ip或者域名,不能有端口,否则不能启动。于是只能把端口设置为80->80,然后external_url设置为:
external_url "http://192.168.0.1"
-
重启gitlab
docker exec -t -i gitlab vim /etc/gitlab/gitlab.rb
六、总结
Git仓库使用的不仅仅在于代码管理,如果合理地利用起Gitlab强大的钩子程序,可以使项目实现持续集成、自动化测试(单元测试)以及自动化打包发布等等,可以大大提高开发效率。
参考文献:
[1] Git教程-廖雪峰的官方网站
[2] 详细透彻解读Git与SVN的区别
[3] Git Reference