Git的诞生要从Linux说起,大佬Linus在1991年创建了开源的Linux系统,从此,Linux系统不断发展,由全世界热心的志愿者参与编写代码,如今已经成为最大的服务器系统软件。
早期Linux的代码是如何管理的呢?
事实上,从Linux系统诞生之后的十年间,源代码文件是世界各地的开发者通过diff的方式发给Linus,然后由Linus本人手动合并的!
为什么不使用版本控制软件呢?
当时确实是存在一些商用的版本控制系统,以及一些开源的软件比如CVS和SVN,但是Linus都没有采用,拒绝前者的原因是付费软件不符合Linux的开源精神,而拒绝后者的原因的是Linus本人坚定地嫌弃CVS和SVN这些集中式的版本控制系统(下部分详述)。
Git诞生的直接原因
随着Linux的代码库越来越庞大,即便Linus本人再偏执,也终究要考虑Linux开源社区贡献者们的强烈不满,于是接受了商业版本控制系统BitKeeper的免费使用的授权。就这样岁月静好了三年,社区的另一个大佬 Andrew(Samba作者)尝试破解BitKeeper的协议,事情被发现之后,Linux的授权被收回。Linus得知之后对这一行为很气愤,然后花了两周时间用C语言写了一个分布式版本控制系统,Git自此诞生。
Git的发展
Git灵活方便,以及分布式的特点顺应趋势,很快流行了起来。随后GitHub网站上线,为开源项目免费提供Git存储,无数开源项目迁移到GitHub上,自此Git的地位无可撼动。
本节介绍让Linus如此嫌弃的集中式版本控制系统与Git(分布式版本控制系统)有何区别
集中式版本控制系统
集中式版本控制系统,版本库是集中存放在中央服务器的,而干活的时候,用的都是自己的电脑,所以要先从中央服务器取得最新的版本,然后开始干活,干完活了,再把自己的活推送给中央服务器。中央服务器就好比是一个图书馆,你要改一本书,必须先从图书馆借出来,然后回到家自己改,改完了,再放回图书馆。
集中式版本控制系统最大的毛病就是必须联网才能工作,如果在局域网内还好,带宽够大,速度够快,可如果在互联网上,遇到网速慢的话,可能提交一个10M的文件就需要5分钟,这还不得把人给憋死啊。
分布式版本控制系统
分布式版本控制系统根本没有“中央服务器”,每个人的电脑上都是一个完整的版本库。和集中式版本控制系统相比,分布式版本控制系统的安全性要高很多,因为每个人电脑里都有完整的版本库,某一个人的电脑坏掉了不要紧,随便从其他人那里复制一个就可以了。而集中式版本控制系统的中央服务器要是出了问题,所有人都没法干活了。任何两台电脑之间,都可以互相推送自己修改过的版本。
事实上,分布式版本控制系统通常也有一台充当“中央服务器”的电脑,不过它并不是必需的,而是在多人协同开发的场景中为了方便统一版本才这么做的,如果当前的中央服务器爆炸了,它也可以是其他的任意一台电脑;而如果SVN中的中央服务器爆炸了,那开发组就真的爆炸了。
Linux
可选源码安装,也可以使用Linux系统的包管理器安装
1 | yum install -y git |
Windows
在Git官网下载安装包,然后根据引导安装即可。Windows下需要配置用户名和邮箱。
1 | git config --global user.name "Your Name" |
工作区、暂存区、版本库的概念
要轻松地使用Git,首先必须了解工作区、暂存区和版本库的概念
工作区(Working Directory):就是电脑里显示的目录
版本库(Repository):是一个隐藏的仓库,存储了提交之后的各个版本,可以理解为一个快照
暂存区(stage):介于工作区和版本库之间,暂存修改之后的工作区文件,一般文件修改之后,不会立即提交版本库,一般是把修改过的文件都放到暂存区,然后,一次性提交暂存区的所有修改。
从上图中可以看到,在版本库的内部有个指向当前版本的HEAD
指针,当需要回退版本的时候,Git把HEAD从当前版本指向需要回退到的版本。
查看仓库状态
1 | git status |
将工作区文件添加进暂存区
1 | git add <file> # 添加某个文件进暂存区 |
删除文件
场景:删除一个工作区中的文件之后,可以再把文件从暂存区删除,然后再提交到版本库。
1 | # 此命令是从暂存区中删除文件 |
将暂存区文件提交到版本库
1 | git commit -m "some message" # 提交后将会生成一个版本,提交之后暂存区就是“干净”的了 |
查看Git版本库时间线
1 | git log # 显示从最近到最远的版本信息,可以看到版本号,作者信息,日期,以及提交的版本信息 |
查看Git命令日志
1 | git reflog # 每一次命令都会被记录在这里,也可以看到历史的提交记录,用来找丢失的版本号很方便 |
撤销修改
场景1:在工作区修改了,还没添加到暂存区,想要撤销修改
1 | git checkout -- <file> # 撤销工作区中的修改,回到最近一次的`git add`或者`git commit`的状态 |
场景2:在工作区修改了之后,又添加到暂存区了,想要撤销修改
1 | git reset HEAD <file> # 撤销暂存区中的修改,撤销之后暂存区就是干净的了,如果想要进一步撤销工作区的修改,可以参考场景1 |
场景3:在工作区修改了之后,添加到了暂存区,又提交到了版本库,想要回退版本
1 | git reset --hard HEAD^ # 回到上个版本,HEAD表示当前版本,HEAD^表示上个版本,HEAD^^表示上上个版本,以此类推…… |
场景4:回退版本之后,又想要回到回退之前的那个版本
1 | # 这个场景用上个命令无法实现,因为HEAD无法表示回退之前的那个版本 |
在本地创建了一个Git仓库后,还可以在GitHub上创建一个Git仓库,并且让这两个仓库进行远程同步,这样,GitHub上的仓库既可以作为备份,又可以让其他人通过该仓库来协作。
将本地仓库与远程仓库关联
1 | # 添加一个远程仓库,git remote add origin [git远程仓库地址] |
把本地库分支推送到远程仓库
1 | # 把本地库的master分支上的内容推送到远程仓库的master分支,用git push命令 |
注意事项:
在多人协作中,哪些分支需要推送,哪些分支不需要
master
分支是主分支,因此要时刻与远程同步;dev
分支是开发分支,团队所有成员都需要在上面工作,所以也需要与远程同步;从远程仓库拉取分支
1 | git clone git@github.com:michaelliao/gitskills.git # 如果是新建的本地库,默认只能看到master分支 |
注意:
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
,如果有冲突,要先处理冲突。删除一个远程仓库
此处的 “删除” 其实是解除本地库和远程仓库的绑定关系,并不是物理上删除了远程库。远程库本身并没有任何改动。
如果添加的时候地址写错了,或者就是想删除远程库,可以解除本地库与远程库之间的关联,
1 | git remote -v # 使用前,建议先查看远程库信息 |
分支管理的概念
讲起分支,一个离不开的概念就是指针,在我的理解中,一个分支就是一个指针。
Git中,分支的管理非常重要,在之前的本地仓库管理的版本库概念介绍中,HEAD
指针好像是指向了最新的提交,其实并不是这样的,HEAD
指针永远都指向某一个分支指针。
在只有一条分支,即只有一条master
分支的情况下,HEAD
指针指向master
指针,而master
指针指向最新的提交,所以看起来是HEAD
指针指向了最新的提交。
当我们创建了另一条分支的时候,比如使用git branch dev
命令创建一个dev
分支,Git只是添加了一个dev
指针,让它指向了当前所在的提交,其他什么都没做。然后我们可以使用git switch dev
命令切换到dev
分支,本质上是让HEAD
指针指向dev
指针。不过现在dev
指针和master
指针指向的是同一个提交。
接下来,在本地仓库中,对工作区的修改和提交就是针对dev
分支了,与master
分支无关。比如新提交一次后,dev
指针会移向最新的提交,向前移动一格,而master
分支不动。
一般来说,dev
分支是为了开发工作而创建的,当我们开发好了之后,必然要合并到master
分支上去,这个合并的操作也很简单,需要先回到master
分支,git switch master
,然后使用命令git merge dev
,就完成了合并。其实本质上只是简单地把master
指针指向了dev
指针。
这种方式是Fast-Forward
模式,即快进模式。
当然,也不是每次合并都能Fast-forward
,后面会讲其他方式的合并。
合并之后就可以删除dev
分支了,使用命令git branch -d dev
基本操作命令
创建分支
1 | git branch <name> |
调整到某分支
1 | git switch <name> # 旧命令是git checkout <name>,但是容易与前面的撤销修改混淆,不建议使用 |
创建并调整到某分支
1 | git switch -c <name> # 旧命令:git checkout -b <name>,不建议使用 |
合并某分支到当前分支
1 | git merge <name> |
删除某分支
1 | git branch -d <name> # 分支合并到主分支后,可以使用此命令删掉分支 |
merge的动作是把目标分支的改动也加到当前分支,本节需要介绍一下分支合并的三种方式,Fast-forward
模式、普通模式、和 强制Fast-forward
模式
Fast-forward
模式是指master
合并dev
时候发现master
节点一直和dev
的根节点相同,没有发生改变,那么master
快速移动头指针到dev
的位置,所以 Fast-forward 并不会发生真正的合并,只是通过移动指针造成合并的假象,这也体现 Git 设计的巧妙之处。
Fast-Forward
的合并方式固然简单,但是也有缺点,那就是删除了dev
分支之后,就没办法看到它的记录了,好像它没有存在过一样,只剩下了线性的master
分支。
如果想要保留一份合并的记录,可以使用non-Fast-forward
模式,也就是普通模式,指定--no-ff
参数,这样就可以强制禁用Fast-Forward
模式了,这样做的好处是在任何情况下都会创建新的 commit 进行合并(即使当前节点相对于目标分支的根节点并没有改动),它的命令是merge --no-ff -m "some message" <目标分支>
,可以体现相对真实的merge记录。
最后一种情况就是强制Fast-forward
模式,它只会按照 Fast-forward
模式进行合并,如果不符合条件,则会拒绝合并请求并且退出,这种模式只适合追求干净线性git记录的小型团队。
合并分支并不是每次都能一帆风顺的,偶尔也会产生冲突。不过也不用过于担心,因为只要不是两个分支同时编辑了同一个文件的相同部分,就不会有冲突。
像下面这种情况,当前分支和目标分支都有不同的改动,并且恰好改动的是同一个文件的相同部分
此时想要将feature1
分支的改动加入到master
分支中,就会产生冲突,Git会提示我们哪个文件产生了冲突,需要手动解决冲突然后再提交一次。
在产生冲突的文件中,Git会在需要解决冲突的部分用<<<<<<<
,=======
,>>>>>>>
标记出来,修改之后使用命令git add <冲突文件名>
,git commit -m "message"
就行了。此时的状态如下图所示。使用git log --gragh
命令可以查看分支合并图。
确认没问题之后,可以删除掉feature1
分支。
发布一个版本时,我们通常先在版本库中打一个标签(tag),这样方便我们查看版本信息。
本质上来说,标签也是一个指针,他是指向某次提交的死指针,给某次提交打上标签之后,就一直跟某次提交绑定在一起。
创建标签
1 | git tag <tagname> # 给当前提交创建一个标签,比如 git tag v1.0 |
查看标签信息
1 | git tag # 查看已创建的标签名称,这个排序是按字母顺序的,跟时间无关 |
推送某个标签到远程
1 | git push origin <tagname> # 推送一个标签到远程 |
删除标签
1 | git tag -d <tagname> # 本地删除一个标签 |
有些时候,Git的工作目录中有一些文件必须存在,但又不能提交它们,比如下面这些:
.class
文件;要解决这个问题,需要在Git的工作目录中添加一个文件.gitignore
,在这个文件中写入想要忽略的文件名
1 | Thumbs.db # 具体的文件名 |