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 # 具体的文件名 |