课程大纲:
*版本控制系统
*分支模型
*Git
一、版本控制VCS
定义:版本控制系统即VCS(version control system)是一种记录若干文件的修订记录的系统,它帮助我们查阅或回到某个历史版本。
-“人肉”VCS
-LVCS 本地(如:RCS),不足:不能多人协同开发
-CVCS 集中式(如SVN),若中央管理器发生故障。。。
-DVCS 分布式(如Git,Mercurial),解决了前面的问题
二、分支模型
*分支:从目标仓库获得一份项目拷贝,每条拷贝都有和原仓库功能一样的开发线。
*分支模型 / 工作流:一个围绕项目【开发 / 部署 / 测试】等工作流程的分支操作(创建,合并等)规范集合。
1、产品级的分支模型
常驻分支:(一旦被创建就不会被修改)
* development
——从master创建
* production(master)
——默认分支
活动分支:(可以根据需要删除修改)
* feature:
——从development创建
* hotfix:如hotfix-36(bug修复分支)
——从master创建
* release:如release-110(标识一个产品被正式发布)
——从development分支创建
2、不同环境下的分支:
开发环境(下一个release的特性分支)——测试环境(release / development分支)——预发布环境(release分支)——生产环境(master分支)
三、Git
1、git介绍
* git是一个免费开源的分布式版本控制系统(DVCS)
* git是一个基于内容寻址的存储系统
2、优势
* 快
* 完全的分布式
* 轻量级的分支操作
* Git已经成为现实意义上的标准
* 社区成熟活跃(这里注意:git不等于github)
3、基础操作
git-config
*用户配置:
-------------- git config --global user.name "John EveryThing"
-------------- git config --global user.email test@example.com
*配置级别(为什么上述要用 global)
-------------- local【默认,高优先级】:只影响本仓库 .git/config
-------------- global【中优先级】:影响到所有当前用户的git仓库 ~/.gitconfig
-------------- system【低优先级】:影响到全系统的git仓库(无论你是哪个用户) /etc/gitconfig
git-init
*初始化仓库
①首先查看仓库状态:
git status
会提示这不是一个仓库,因为没有“.git”这个文件夹。
②然后再初始化:
git init
在当前目录下会出现一个文件夹(.git)。
③再次查看状态:
git status
提示没有东西要提交,并提示使用“git add”。
git-status
A、三对关系:
未跟踪《-》跟踪
工作目录《-》暂存区
暂存区《-》最新提交
B、介绍:
git-status:对状态的跟踪
有两个状态:
内容状态——主要标识着文件内容的改变
文件状态——
git-add:添加文件内容到暂存区(同时文件被跟踪)
①批量添加:
git add .
出现“跟踪了意料之外的文件!”问题:
②忽略文件:
.gitignore
* 在添加时忽略匹配的文件
* 仅作用于未追踪的文件
③从暂存区删除文件
git-rm
* git rm --cached:仅从暂存区删除
* git rm:从暂存区与工作目录删除
* git rm $(git ls-files --deleted):删除所有被跟踪,但是在工作目录被删除的文件
git-commit:根据暂存区内容创建一个提交记录。
git commit -m '......'
-m 参数帮忙注释
①可以将git add 和 git commit合并为一个命令
git commit -a -m '.....'
提交记录去哪儿了?
git-log:显示提交历史信息
git中的alias命令(别名设置):
git config alias.shortname <fullcommand>
git-diff:显示不同版本差异
* git diff:工作目录与暂存区的差异
* git diff --cached [<reference>]:暂存区与某次提交差异,默认为HEAD
* git diff <reference>:工作目录与某次提交的差异
撤销本地修改:
git checkout --<file>:将文件内容从暂存区复制到工作目录
撤销暂存区内容:
git reset HEAD <file>:将文件内容从上次提交复制到暂存区(相当于暂存区没有被修改)
4、分支操作
git-branch:分支的增删查改都靠它。
* git branch <branchName> 创建一个分支
* git branch -d <branchName> 删除指定分支
* git branch -v 显示所有的分支信息
如何使用branch命令:
提交历史是一个不断向前的线,当我们用commit命令创建一个提交对象的时候,它会有一个引用指向前一个提交,默认创建的master分支或者当前所在的分支其实也会跟着走,它会指向当前最新的提交,HEAD也是指向当前最新的提交。当我们使用“git branch next”的时候,它会在HEAD指向的commit对象上创建一个另外的引用“next”,(这里解释为什么说git是轻量级的:使用“cat .git/refs/heads/master”查看文件,其实一个分支的引用只是一个文本文件,在里面只有一个四十个字符长的SHA-1编码,这就是Git分支轻量级的秘密,刚刚的“next”分支也就是里面的“next”文件)。现在使用“git commit”进行提交,同样这里会创建一个提交对象,然后“master”和“HEAD”会向前移动一步,指向新创建的提交对象,但是“next”不会变动(原因:因为我们只是创建了next分支,但是没有切换到next分支)。
如何在next分支上工作:
git-checkout:通过移动HEAD检出版本,可用于分支切换。
* git checkout <branchName>
* git checkout -b <branchName> 创建该分支,并切换到它
* git checkout <reference> 移动到某个引用对象上
使用: “git checkout next”——就是将HEAD指针贴到了next分支指针上。因为所有的提交都是随着HEAD走的,所以现在就可以直接在next分支上进行开发了。
如何回到原来的分支:
git checkout -
查看分支信息:
git branch -v
列出现在存在的分支,前面带有“*”的就是当前所在的分支。
当HEAD移动到某个提交对象上,而不是一个分支时:
git checkout 14d08d1
就会出现“detached head!”现象。当HEAD已经是分离状态,我们就应该尽量避免在这个状态上提交东西。因为没有引用会指向这个提交记录,只有HEAD而已,当HEAD重新回到master时,之前在分离状态上的提交都会被视为没有被引用,最后可能在git内部的垃圾回收时会被回收。
当直接使用commit id的时候(上面这句checkout命令),其实并不是真正的回退,因为master的指针引用并没有回退,只是HEAD回到了前面而已,分支并没有发生变化。
如何完全回退:
git reset:将当前分支回退到历史某个版本。
* git reset --mixed <commit> (默认)(将回退到的内容复制到暂存区)
* git reset --soft <commit>(暂存区和工作目录保持最新的状态,不回退,仅仅只是指针(master和HEAD)发生了改变)
* git reset --hard <commit>(将回退到的内容复制到暂存区和工作目录)
这三种方式的不同主要体现在:内容是否恢复到工作目录或者是暂存区。
如何找回(log中已经没有最近提交的信息了):
git reflog
reflog会按照你之前经过的commit路径按序来排,因为我们刚刚从最新的提交回来,所以我们现在所在的就是reflog的顶部,可以使用哈希值来进行重新回退到最新提交的版本或者进行其他的操作。需要注意的是这个命令的使用要尽快,因为reflog是不断向前的,所以有些信息可能会丢失。
如何避免输入不直观的hash来定位:
使用捷径:
辨析:reset和checkout
这两个命令同时有两个作用范围:commit操作和file操作(会有一个file参数来指明要影响的file)。
当使用checkout到某个分支出现以下错误:本次checkout默认会被终止。
error: Your local changes to the following files would be overwritten by checkout:
README.md
Please,commit your changes or stash them before you can switch branches.
Aborting
出现这个问题的原因主要是:本地的工作目录或者暂存区有变动,但是还没有被提交,所以没有办法切换到别的分支,一旦被切换我们当前的工作内容就会丢失。
但是我们如果才编辑到一半,不希望现在就提交,但是另外一个分支又有一个非常紧急的任务需要处理,这样我们就选择git为我们提供的第二种解决建议:
git stash:保存目前的工作目录和暂存区状态,并返回到干净的工作空间。
使用:
git stash save 'push to stash area'
git status——发现工作空间变干净了
然后就可以进行分支切换了或者其他操作
git stash list——stash相当于一个栈,我们可以使用这条命令查看当前stash的信息,后面会有之前写下的注解(push to stash area),所以我们要养成写下好的注解的习惯。
git stash apply stash@{0}——用来将保存的内容重新恢复到工作目录
git stash drop stash@{0}——删除这条记录
stash pop = stash apply + stash drop
git merge:合并分支
当前为master分支,现在我们想要将master分支和next分支合并:
git merge next(这里省略了第二个参数):这里是三方合并。合并发生在三个提交节点上:两个分支的共同祖先,以及两个分支节点。合并的结果会被复制到工作目录和暂存区,然后完成一次提交,提交节点会有两个父指向,分别指向两个分支节点。
我们可以从当前的HEAD节点来得到解答:
使用:git cat-file -p HEAD
因为合并是在master分支上发生的,所以next引用并没有发生改变,只是HEAD和master指针向前移动了一位,指向当前最新的提交。
解决merge冲突:
在test.md文件中发生冲突。
git status——出现冲突时先查看。
然后改动冲突文件:文件是由两个箭头系列和一个等号序列分割开的,我们需要将冲突文件改成我们需要的样子。
git add .
git commit -m 'resolve'
当我们是这种情况时:我们在master分支上创建了next分支,但是master分支并没有向前移动,我们仅仅只是在next分支上进行了操作。合并两个分支“git merge next”,仅仅只是将master和HEAD指向了next。
但是我们并不希望有这种结果产生,因为一般merge会有一个分叉产生,他可以告诉我们在这个节点我们发生了一个合并,我们可以通过这个信息知道合并在哪个节点发生,要想解决这个问题:
git merge next --no-ff:它的意思是不要使用fast-forward方式来进行分支的合并。
merge的不足:
有没有方法实现类似fast-forward的线性提交:
git rebase——修剪提交历史基线,俗称“变基”
情景:两个分支:master和feature,HEAD指向feature分支。
git rebase master
首先git会找到三个提交节点,与merge类似,分别是两个分支节点和它们的祖先节点,然后git会找到分支节点与共同节点之间的提交记录:
让这份提交在master分支上重演(不是复制,它们的commit id是不一样的),最后feature和HEAD指针指向了最新的这次提交。
有时候我们并不希望所有的提交都在master分支上重演,我们可以挑选需要的节点在master节点上进行重演:
我们需要某个提交对象后面的提交对象在master上重演:git rebase --onto master 5751363
辨析:rebase vs merge
在同样的情况下,使用rebase将会获得一个线性的提交记录,但是直接使用merge就会有一个合并的过程,他们没有谁好谁坏之分。
注意:勿在共有分支使用rebase。
在master上使用rebase之后,会产生以上的结果,他们有两份不同的提交,但是其实这两份不同的提交是相同的内容,这样会导致其他开发者在进行最新代码拉取的时候必须进行合并,但是这次合并里面会有四份合并是完全相同的。
上面的branch和HEAD都是动态的指针,会随着提交记录发生改变。如何设置一个不变的别名:
git tag——对某个提交设置一个不变的别名
git tag v0.1 e39d0b2
之后就可以直接使用这个标签名进行checkout等操作:
git checkout v0.1
5、远程操作
Git支持本地协议,所以我们可以初始化一个本地的远程服务器。
git init ~/git-server --bare——将当前的仓库初始化为一个裸仓库,裸仓库就是它是没有工作目录的。中央服务器其实也不需要工作目录,如果有工作目录并且提交的话反而会引起整个提交历史的错乱,中央服务器应该是一个被动接受的,它只是同步你和队友之间的分支操作而已。
git push——提交本地历史到远程
第一次提交:我们当前的目录在~/code/tmp/gith/test1(master),我们创建了一个中央仓库叫git-server,我们直接将本地消息推送到中央仓库的master分支。
git push /Users/leeluolee/git-server master
完成了提交历史的复制,同时将中央仓库的master和HEAD向前移动。
如何避免重复输入url:
url和commit id一样,都不是一个很直观的表示。
git remote——远程仓库相关配置操作
配置远程映射——这样就可以通过别名来操作push操作了
git remote add origin ~/git-server——添加一个远程仓库别名,别名为origin
git remote -v——查看远程仓库信息
cat .git/config——查看配置文件,其实这个配置与前面所有的配置一样,都是写在.git/config文件里面
push冲突:
比如有一个用户叫Jerry,有一个远程分支叫origin,这里需要注意的是其实每一个远程分支在本地都是有一个指针的。在这里就是叫origin/master,注意这不是一个分支引用,而是一个类似target那样的指针,不会随着本地commit信息变动,所以你checkout到这个指针下的commit也没有任何意义。现在Jerry使用“git commit”,他本地的master和HEAD会向前移动一步,但是现在origin/master这个指针并没有变动,我们需要用push将其同步到服务器“git push origin master”,现在origin/master也指向了最新,远程的master和HEAD也指向了最新的记录,这个时候有另外一个用户Tom,他和Jerry提交之前一样有一份历史,现在Tom使用“git commit”,他本地的master和HEAD向前移动了一步,现在发现远程的分支开叉了:在他们两个人相同的那个祖先节点衍生了两个提交节点,都是指向这个祖先节点。在这种情况下,git会阻止这次提交,它会要求Tom先同步代码。
git fetch——获取远程仓库的提交历史
我们可以使用git fetch + merge来解决上面的问题:
git fetch origin master——fetch的时候会使origin/master指针发生变化,虽然它不会因为本地的commit发生变化,但是fetch会使它同步:
git merge origin/master——合并远程分支,这里完成的还是一个三方合并,最终结果是完成一个新的提交。
git push origin master——将这份提交推送到服务器。这样导致服务器也产生了一个这样的历史,只是将本地的历史复制到了服务器端,这个时候服务器端的master和HEAD向前移动了。
从服务器上拉去最新的代码:git pull = git fetch + git merge
完整获取远程仓库:
git init + git remote + git pull简单化:
git clone——克隆一个远程仓库作为本地仓库
git clone ~/git-server test2——将远程仓库克隆到了test2下面
git remote -v
其他参考资料:
http://rogerdudler.github.io/git-guide/index.zh.html
try.github.com
《Pro Git》(http://git-scm.com/book/zh/v1/)