以一个git新手的角度来阐述项目中git的使用,理解git的基本原理,总结出操作手册,期待能以这篇记录做一次轻量级的分享。
svn
是典型的集中式的版本控制系统,版本库集中存放在中央服务器,可以多人共同维护一份中央服务器的代码,在我过去使用svn
这种集中式的版本控制系统,有时候总能感受到多人协作时对整体项目的不可控风险的痛点,SVN的处理方式在我看来略有欠缺。
比如 想多人开发同一个前端项目时,不同人修改了不同文件,在以前前端没有构建流程整体打包的情况下发布流程还算好,我们只需要保证我们本次提交的单个或几个代码文件的可上线状态,其他修改文件不部署线上,就没有关系。
但是现代化的前端工程化流程来了,面临上线时候我们通常需要整体打包构建,这个时候我们必须要不能提交的代码文件会滚到可上线的稳定版本,然后构建打包,构建出一个整体dist
代码版本,这个过程中,需要部署工程师本人对项目的整个开发改动非常熟悉,才能保证,上线前构建的每一个代码文件都是稳定可上线的。
上完线之后,我们需要把之前功能迭代的非上线代码给回滚回来,继续功能开发。这个过程非常的手工化,非常的蛋疼和危险。
有时候,我们当然也会使用SVN的Branch分支功能,但是因为SVN的分支功能过笨重,每次分支合并到主干几乎都是一次战斗,我们要谨防出现冲突和手工记录此次的合并操作,因为SVN无法识别合并和手工修改。
Git传说当中是由Linux之父为了解决Linux代码维护管理的问题花了2周时间用C语言写出的分布式版本控制系统。Git诞生以来,主要是依靠它极其强大且没有包袱的分支管理,把SVN等远远抛在了后面,当然Git还有一个区别SVN的优点,就是Git的commit不需要联网,我们可以仅仅依靠本地commit记录版本的提交信息。
我们的代码库都应该又一个主分支。所有提供给用户使用的正式版本,都在这个分支上发布。
主分支只用来分布重大版本,日常开发应该在另一条分支上完成。我们把开发用的分支,叫做Develop。
版本库又名仓库,英文名 repository。我们通过创建仓库,让一个目录下所有的文件都被Git版本控制和追踪。
git init
只需要一条命令,瞬间就在当前文件夹创建好了一个Git仓库。
第一步,用命令git add
告诉Git,把文件添加到仓库。
第二步,用命令git commit
告诉Git,把文件和提交信息提交到仓库。
这个过程中请注意,Unix的哲学是“没有消息就是好消息”,如果没有错误提示,那就是成功了。
git add readme.md
git commit -m ‘wrote a readme file’
我们使用git status
来查看仓库当前的状态,使用git diff
来查看修改的内容。
我们有时候需要回退版本,查看历史记录,我们使用git log
命令查看,如果觉得输出信息太多,眼花缭乱,我们可以试试加上git log --pretty=oneline
参数。
$ git log --pretty=oneline
3628164fb26d48395383f8f31179f24e0882e1e0 third commit
ea34578d5496d7dd233c827ed32a8cd576c5ee85 second commit
cb926e7ea50ad11b8f9e909c05226233bf755030 first commit
我们看到的一大串类似3628164fb26d48395383f8f31179f24e0882e1e0
的就是我们git的commmit id
(版本号),和svn版本号的1,2,3...递增不一样,Git因为是分布式的版本控制系统,如果大家都是1,2,3,4就肯定不好处理了,所以我们用一个SHA1计算出的一个不会重复的非常大的16进制数字串,作为每一次提交的版本号。
接下来,我们想回退到上一个版本。在Git中,用HEAD
表示当前版本,也就是最新的提交third commit
,上一个版本就是HEAD^
,上上个版本就是HEAD^^
,我们要是回退比较多,可以表示成这样HEAD-100
。
现在我们使用git reset
命令来回退到上一个版本:
git reset --hard HEAD^
--hard
参数的意义,在这里暂时留下一个疑问,后面进行解释。
同样如果没有提示错误,我们就成功了。但是此时,我们再使用git log
,发现最新的版本已经不见了。
其实还是有办法,我们使用git reflog
命令找到以前的commit id,再使用
git reflog
ea34578 HEAD@{0}: reset: moving to HEAD^
3628164 HEAD@{1}: commit: third commit
ea34578 HEAD@{2}: commit: second commit
cb926e7 HEAD@{3}: commit (initial): first commit
git reset --hard 3628164
版本号其实没不需要写全,前几位就可以了,Git会自动查找。Git版本回退的速度非常快,因为在Git内部有个指向当前版本的HEAD指针,当你回退版本的时候,Git迅速把HEAD的指向改变,然后改变去更新文件库文件。
小结:
HEAD
指向的版本就是当前版本,Git的版本回退的命令为git reset --hard commit_id
git log
可以查看历史提交信息,以便确定要回到哪个版本。- 若要重返未来,用
git reflog
查看历史命令信息,以便确定要回到未来的哪个版本。
工作区也就是我们的系统文件夹内,有一个隐藏目录.git
,是Git的版本库。
我们把文件往Git版本库里添加的时候,是分两步执行的:
第一步是用git add把文件添加进去,实际上就是把文件修改添加到暂存区;
第二步是用git commit提交更改,实际上就是把暂存区的所有内容提交到当前分支。
git add命令实际上就是把要提交的所有修改放到暂存区(Stage),然后,执行git commit就可以一次性把暂存区的所有修改提交到分支。
为什么Git比其他版本控制系统设计得优秀,因为Git跟踪并管理的是修改,而非文件。
Git是如何跟踪修改的,每次修改,如果不add到暂存区,那就不会加入到commit中。
git checkout -- file
可以丢弃工作区的修改,把指定文件在工作区的修改全部撤销。
git checkout -- file
命令中的--
很重要,没有--
,就变成了“切换到另一个分支”的命令。
git rm test.txt
命令git rm
用于删除一个文件。如果一个文件已经被提交到版本库,那么你永远不用担心误删,但是要小心,你只能恢复文件到最新版本,你会丢失最近一次提交后你修改的内容。
- 添加远程库
要关联一个远程库,使用命令git remote add origin git@server-name:path/repo-name.git
关联后,使用命令git push -u origin master
第一次推送master分支的所有内容;
此后,每次本地提交后,只要有必要,就可以使用命令git push origin master
推送最新修改;
分布式版本系统的最大好处之一是在本地工作完全不需要考虑远程库的存在,也就是有没有联网都可以正常工作,而SVN在没有联网的时候是拒绝干活的!当有网络的时候,再把本地提交推送一下就完成了同步,真是太方便了!
- 从远程库克隆
要克隆一个仓库,首先必须知道仓库的地址,然后使用git clone
命令克隆。
Git支持多种协议,包括https,但通过ssh支持的原生git协议速度最快。
分支在实际中有什么用呢?假设你准备开发一个新功能,但是需要两周才能完成,第一周你写了50%的代码,如果立刻提交,由于代码还没写完,不完整的代码库会导致别人不能干活了。如果等代码全部写完再一次提交,又存在丢失每天进度的巨大风险。
现在有了分支,就不用怕了。你创建了一个属于你自己的分支,别人看不到,还继续在原来的分支上正常工作,而你在自己的分支上干活,想提交就提交,直到开发完毕后,再一次性合并到原来的分支上,这样,既安全,又不影响别人工作。
其他版本控制系统如SVN等都有分支管理,但是用过之后你会发现,这些版本控制系统创建和切换分支比蜗牛还慢,简直让人无法忍受,结果分支功能成了摆设,大家都不去用。
但Git的分支是与众不同的,无论创建、切换和删除分支,Git在1秒钟之内就能完成!无论你的版本库是1个文件还是1万个文件。
git的切换分支其实只是改变了HEAD
的指向。
git鼓励大量使用分支来管理项目的开发流程协作。
查看分支:git branch
创建分支:git branch <name>
切换分支:git checkout <name>
创建+切换分支:git checkout -b <name>
合并某分支到当前分支:git merge <name>
删除分支:git branch -d <name>
当两个分支merge出现相同工作区的编辑时,就不能快速自动合并,git会把各种的修改保存,让我们手动解决冲突。
git log --graph
命令可以查看分支合并图
通常,合并分支时,如果可能,Git会用Fast forward模式,但这种模式下,删除分支后,会丢掉分支信息。
如果要强制禁用Fast forward模式,Git就会在merge时生成一个新的commit,这样,从分支历史上就可以看出分支信息。
--no-ff
方式的git merge
:
git merge --no--ff -m ’merge width --no--ff‘ dev
这样方式的合并要创建一个新的commit,所以加上-m
参数,把commit描述写进去。
小结
合并分支时,加上--no-ff参数就可以用普通模式合并,合并后的历史有分支,能看出来曾经做过合并,而fast forward合并就看不出来曾经做过合并。
在实际开发中,我们应该按照几个基本原则进行分支管理:
首先,master分支应该是非常稳定的,也就是仅用来发布新版本,平时不能在上面干活;
那在哪干活呢?干活都在dev分支上,也就是说,dev分支是不稳定的,到某个时候,比如1.0版本发布时,再把dev分支合并到master上,在master分支发布1.0版本;
你和你的小伙伴们每个人都在从dev划出的分支上干活,每个人都有自己的分支,时不时地往dev分支上合并就可以了。
有时候我们在一个分支开发功能,任务还没有完成,需要立即切换分支修复master问题,我们又不想把已改内容作为一次commit,幸好,Git还提供了一个stash功能,可以把当前工作现场“储藏”起来,等以后恢复现场后继续工作:
git stash
完成了工作后,我们切回之前未完成工作的分支,对于之前暂存stash内容,首先可以查看:
git stash list
我们可以得知工作现场还在,Git把stash内容存在某个地方了,但是需要恢复一下,有两个办法:
git stash apply
这种方式恢复,stash内容并不删除,你需要用
git stash drop
这种方式,恢复的同时把stash内容也删了,此时再用git stash list
查看,就看不到任何stash内容了。
软件开发中,总有无穷无尽的新的功能要不断添加进来。
添加一个新功能时,你肯定不希望因为一些实验性质的代码,把主分支搞乱了,所以,每添加一个新功能,最好新建一个feature分支,在上面开发,完成后,合并,最后,删除该feature分支。
git checkout -b feature/0707
我们从当前分支切出一个feature分支,开始进行开发:
git add .
git status
git commit -m 'xx feature'
开发完毕,切回dev
,准备合并:
git checkout dev
然后我们再进行合并代码操作即可。 但是,有一个情况,就在此时,接到上级命令,因经费不足,新功能必须取消!虽然白干了,但是这个分支还是必须就地销毁:
$ git branch -d feature/0707
error: The branch 'feature/0707' is not fully merged.
If you are sure you want to delete it, run 'git branch -D feature/0707'.
销毁分支失败,Git提醒,feature/0707
分支还没有被合并,如果删除,将丢失掉修改,如果要强行删除,需要使用命令
$ git branch -D feature/0707
当你从远程仓库克隆时,实际上Git自动把本地的master分支和远程的master分支对应起来了,并且,远程仓库的默认名称是origin。
要查看远程库的信息,用git remote:
$ git remote
origin
或者,用git remote -v
显示更详细的信息
$ git remote -v
origin [email protected]:michaelliao/learngit.git (fetch)
origin [email protected]:michaelliao/learngit.git (push)
上面显示了可以抓取和推送的origin
的地址。如果没有推送权限,就看不到push的地址。
推送分支,就是把该分支上的所有本地提交推送到远程库。推送时,要指定本地分支,这样,Git就会把该分支推送到远程库对应的远程分支上:
$ git push origin master
如果要推送当前分支,可以省略掉分支名也可。
$ git push
但是,并不是一定要把本地分支往远程推送,那么,哪些分支需要推送,哪些不需要呢?
- master分支是主分支,因此要时刻与远程同步;
- dev分支是开发分支,团队所有成员都需要在上面工作,所以也需要与远程同步;
- bug分支只用于在本地修复bug,就没必要推到远程了,除非老板要看看你每周到底修复了几个bug;
- feature分支是否推到远程,取决于你是否和你的小伙伴合作在上面开发。
总之,就是在Git中,分支完全可以在本地自己藏着玩,是否推送,视你的心情而定!
多人的工作模式通常是这样:
首先,可以试图用git push origin branch-name
推送自己的修改;
如果推送失败,则因为远程分支比你的本地更新,需要先用git pull试图合并;
如果合并有冲突,则解决冲突,并在本地提交;
没有冲突或者解决掉冲突后,再用git push origin branch-name
推送就能成功!
如果git pull提示“no tracking information”,则说明本地分支和远程分支的链接关系没有创建,用命令git branch --set-upstream branch-name origin/branch-name
。
这就是多人协作的工作模式,一旦熟悉了,就非常简单。
小结
查看远程库信息,使用git remote -v
;
本地新建的分支如果不推送到远程,对其他人就是不可见的;
从本地推送分支,使用git push origin branch-name
,如果推送失败,先用git pull
抓取远程的新提交;
在本地创建和远程分支对应的分支,使用git checkout -b branch-name origin/branch-name
,本地和远程分支的名称最好一致;
建立本地分支和远程分支的关联,使用git branch --set-upstream branch-name origin/branch-name
;
从远程抓取分支,使用git pull
,如果有冲突,要先处理冲突。
why?我们为什么需要标签?因为git的commit不便记忆,是机器标示,是反人类的。。。而我们有时候对于重要版本需要精准的人类语言来描述和记忆版本。
在Git中打标签非常简单,首先,切换到需要打标签的分支上:
$ git branch
$ git checkout master
然后,使用命令git tag <name>
打上一个新标签
$ git tag v1.0
可以用命令git tag
查看所有标签
$ git tag
v1.0
默认标签是打在最新提交的commit上的。
有时候,如果忘了打标签,比如,现在已经是周五了,但应该在周一打的标签没有打,怎么办?方法是找到历史提交的commit id,然后打上就可以了:
$ git log --pretty=online --abbrev-commit
在原本git tag
命令的后面跟上指定的commit id
就可以
$ git tag v1.1 9f294s1
此时再用git tag
查看标签:
$ git tag
v1.0
v1.1
注意,标签不是按时间顺序列出,而是按字母排序的。可以用git show <tagname>
查看标签信息:
$ git show v1.1
还可以创建带有说明的标签,用-a指定标签名,-m指定说明文字:
$ git tag -a v1.2 -m "version 1.1 released" 3628164
小结
- 命令git tag 用于新建一个标签,默认为HEAD,也可以指定一个commit id;
- git tag -a -m "blablabla..."可以指定标签信息;
- git tag -s -m "blablabla..."可以用PGP签名标签;
- 命令git tag可以查看所有标签。
$ git tag -d v1.1
因为创建的标签都只存储在本地,不会自动推送到远程。所以,打错的标签可以在本地安全删除。
如果要推送某个标签到远程,使用命令git push origin <tagname>
$ git push origin v1.0
或者,一次性推送全部尚未推送到远程的本地标签:
$ git push origin --tags
如果标签已经推送到远程,要删除远程标签就麻烦一点,先从本地删除:
$ git tag -d v1.0
然后,从远程删除。删除命令也是push,但是格式如下:
$ git push origin :refs/tags/v1.0
小结
- 命令git push origin 可以推送一个本地标签;
- 命令git push origin --tags可以推送全部未推送过的本地标签;
- 命令git tag -d 可以删除一个本地标签;
- 命令git push origin :refs/tags/可以删除一个远程标签。