Git简介
什么是版本控制
版本控制系统(Version Control System,简称VCS)是一种记录一个或若干文件内容变化,以便将来查阅特定版本修订情况的系统。
按类型可以分为:
本地版本控制系统
例如RCS(至少我是从来没有用过)
本地版本控制系统解决了版本的管理问题,再也不用时不时的把工程目录,通过手工拷贝的方式来存档了。但本地版本控制系统的缺点是,无法解决多人协作的问题。集中化的版本控制系统
例如CVS,SVN等(公司中SVN应该用的比较多)
有一个集中管理的服务器,所有开发人员通过客户端连到这台服务器,取出最新的 文件 或者提交更新。管理员可以掌控每个开发者的权限。
集中化的VCS不但解决了版本控制问题,还可以多人协作。但缺点也是有的,就是太依赖于远程服务器,CVS服务器宕机后,会影响所有人的工作。版本记录只保存在一台服务器上,会有数据丢失风险。分布式版本控制系统
例如Git
客户端并不只提取最新版本的文件,而是把 代码仓库 完整地镜像下来。每一次的提取操作,实际上都是一次对 代码仓库 的完整备份。
所以并没有”中心服务器”的概念,所谓的”Git服务器”,也同每个人的电脑一样,只是为了多人协作时,方便大家交换数据而已。
什么是Git
Git是目前世界上最先进的分布式版本控制系统(没有之一)
好不好用,看看它的开发者是谁就知道了:Linux之父 Linus Torvalds
小历史: Linux内核社区原本使用的是名为BitKeeper的商业化版本控制工具,2005年,因为社区内有人试图破解BitKeeper的协议,BitMover公司收回了免费使用BitKeeper的权力。
Linus原本可以出面道个歉,继续使用BitKeeper,然而并没有。。。Linus大神仅用了两周时间,自已用C写了一个分布式版本控制系统,于是Git诞生了!
为什么要使用Git
为什么要使用Git,或者说Git相比SVN有什么优势呢?
分布式
分支管理
GitHub
安装Git
- 大多数Linux发行版已经预装了Git,系统默认自带,如果不带。。可以源码make安装或使用yum/apt等直接安装,过程不赘述了。
- macOS下,安装Xcode后,它的CLI工具里应该会包含Git了。或者使用brew手工安装一下。
- Windows下,可以直接下载安装 msysGit 。 或者如果你的机器上已经有Cygwin,也可以直接用在它下面安装Git。
- 图形工具推荐使用 SourceTree,查看分支非常直观 。IntelliJ IDEA等IDE也会自带一些图形化的工具,在合并代码时很高效。
学习路径
- 首先,忘掉SVN/CVS,不要把Git的各种操作与它们做类比,切记。
- 刚开始不要依赖图形客户端。首先应该将精力用在理解原理上 -> 然后掌握一些基本CLI命令,动手操作实践 -> 最后在实际工作中使用GUI工具以提高效率。
- 重度Windows用户使用Git时,与平时熟悉GUI的环境会有些违和感,毕竟Git是Linux下的产物,Git遵循Linux的哲学,Simple,简单直接,但Simple并不等于Easy。需要转换一下思维。
了解Git的工作原理
记录文件整体快照
Git和其他版本控制系统的主要差别在于,Git只关心文件数据的 整体 是否发生变化,而大多数其他系统则只关心 文件内容 的具体差异。
SVN在每个版本中,以单一文件为单位,记录各个文件的差异:
Git在每个版本中,以当时的全部文件为单位,记录一个快照:
大多数操作都在本地执行
Git的绝大多数操作都只需要访问本地文件和资源,不用连网。因为你的本机上,就已经是完整的代码库了。这样一来,在无法连接公司内网的环境中,也可以愉快的写代码了。
例如,如果想看当前版本的文件和一个月前的版本之间有何差异,Git会取出一个月前的快照和当前文件作一次差异运算,而不用每次都请求远程服务器。
时刻保持数据完整性
在保存到Git之前,所有数据都要进行内容的校验和(checksum)计算,并将此结果作为数据的唯一标识和索引。
这项特性作为Git的设计哲学,建在整体架构的最底层。所以如果文件在传输时变得不完整,或者磁盘损坏导致文件数据缺失,Git都能立即察觉。
Git使用SHA-1算法计算数据的校验和,通过对文件的内容或目录的结构计算出一个SHA-1哈希值,作为指纹字符串。该字串由40个十六进制字符组成,看起来就像是:24b9da6552252987aa493b52f8696cd6d3b00373
Git的工作完全依赖于这类指纹字串,所以你会经常看到这样的哈希值。实际上,所有保存在 Git数据库中的东西都是用此哈希值来作索引的,而不是靠文件名。
多数操作仅添加数据
常用的Git操作大多仅仅是把数据添加到数据库,很难让Git执行任何不可逆操作。在Git中一旦提交快照之后就完全不用担心丢失数据,特别是养成定期推送到其他仓库的习惯的话。
文件的三种状态
对于任何一个文件,在 Git 内都只有三种状态:已提交(committed) 已修改(modified) 已暂存(staged)
已提交表示该文件已经被安全地保存在本地数据库中了;
已修改表示修改了某个文件,但还没有提交保存;
已暂存表示把已修改的文件放在下次提交时要保存的清单中。
由此我们看到 Git 管理项目时,文件流转的三个工作区域:Git 的工作目录,暂存区域,以及本地仓库。
每个项目都有一个名为.git的目录,它是 Git用来保存元数据和对象数据库的地方。该目录非常重要,每次克隆镜像仓库的时候,实际拷贝的就是这个目录里面的数据。
从项目中取出某个版本的所有文件和目录,用以开始后续工作的叫做工作目录。这些文件实际上都是从Git目录中的压缩对象数据库中提取出来的,接下来就可以在工作目录中对这些文件进行编辑。
所谓的暂存区域只不过是个简单的文件,一般都放在 Git 目录中。有时候人们会把这个文件叫做索引文件,不过标准说法还是叫暂存区域。
基本的 Git 工作流程如下:
- 在工作目录中修改某些文件。
- 对修改后的文件进行快照,然后保存到暂存区域。
- 提交更新,将保存在暂存区域的文件快照永久转储到 Git 目录中。
所以,我们可以从文件所处的位置来判断状态:如果是Git目录中保存着的特定版本文件,就属于已提交状态;如果作了修改并已放入暂存区域,就属于已暂存状态;如果自上次取出后,作了修改但还没有放到暂存区域,就是已修改状态。
创建版本库
有两种取得Git项目仓库的方法。第一种是在现存的目录下,通过导入所有文件来创建新的Git仓库。 第二种是从已有的Git仓库克隆出一个新的镜像仓库来。
在目录中创建新仓库
如果一个目录还没有使用Git进行管理,只需到此项目所在的目录,执行git init
,初始化后,在当前目录下会出现一个名为.git的目录
1 | $ mkdir learngit |
从已有的仓库克隆
如果Git项目已经存在,可以使用git clone
从远程服务器上复制一份出来,Git支持多种协议:
1 | $ git clone mobgit@134.32.51.60:learngit.git #使用SSH传输协议 |
版本库基本操作
检查当前文件状态
使用git status
命令可以查看文件的状态
1 | $ git status |
出现如上的提示,说明现在的工作目录相当干净,所有已跟踪文件在上次提交后都未被更改过。
现在我们做一些改动,添加一个readme.txt进去,然后再看一下状态
1 | $ cat>readme.txt |
Untracked files显示了这个新创建的readme.txt处于未跟跟踪状态
跟踪新文件
使用git add
命令开始跟踪一个新文件
1 | $ git status |
readme.txt已 被跟踪 ,并处于 暂存状态
将本次修改暂存
现在我们再对readme.txt进行修改,添加一行,再执行git status
查看状态
1 | $ git status |
可以看到readme.txt 不仅出现在了Changes to be committed,还出现在了Changes not staged for commit
由此可见,Git关心的是 Changes ,而不是文件本身。
再次执行git add
,可以将 本次修改 提交到暂存区,Changes not staged for commit提示消失
提交更新
使用git commit
命令将暂存区中的内容提交至版本库,工作区又是干净的了
1 | $ git commit -m "my first commit" |
注意:一定要使用-m参数加入注释,认真描述本次的提交具体做了些什么,这对于以后我们查询历史记录非常重要。
如果觉得使用暂存区过于繁琐,可以在commit时直接使用-a参数,Git就会自动把所有已经跟踪过的文件暂存起来一并提交,从而跳过git add步骤。
1 | $ git commit -a -m "my first commit" |
查看历史
使用git log
命令可以查看历史记录
1 | $ git log |
可以看到,每次更新都有一个SHA-1校验和、作者的名字和电子邮件地址、提交时间、提交说明。
撤消操作
撤消操作在这里这里不做重点描述了,只列出几个常用命令。
修改最后一次提交:
git commit –amend
取消已经暂存的文件:
git reset HEAD readme.txt
取消对文件的修改:
git checkout – readme.txt
远程仓库
之前介绍了在本地仓库的一些操作。但当与他人协作开发某个项目时,需要至少使用一个远程仓库,以便推送或拉取数据,分享各自的工作进展。
克隆远程库
之前已经在讲新建仓库时已经提到,如何克隆远程库,这里再重复列一遍:
1 | $ git clone mobgit@134.32.51.60:learngit.git #使用SSH传输协议 |
查看绑定的远程库
如果之前我们使用的git clone
命令直接克隆了一个远程仓库到本机,Git就已经默认绑定了一个名为origin的远程库。当然我们还可以手工绑定其它远程库,远程仓库可以有多个。
使用git remote -v
命令列出我们绑定了哪些远程库:
1 | $ git remote -v |
接下来还可以使用git remote show origin
来查看这个名为origin的远程库的更详细的信息,这里先不细讲
1 | $ git remote show origin |
手工添加一个远程仓库
我们先让管理员新建一个名为learngit2的远程仓库,再使用remote add
命令将它添加进来,取名为repo2
1 | $ git remote add repo2 mobgit@134.32.51.60:learngit2.git |
现在我们有origin和repo2两个远程仓库了
从远程仓库抓取数据
使用git fetch [remote-name]
从远程仓库抓取数据,注意fetch命令只是将远端的数据拉到本地仓库,并不自动合并到当前工作分支(关于分支稍后讲解)
例如要抓取名为origin远程仓库:
1 | $ git fetch origin |
推送数据到远程仓库
使用git push [remote-name] [branch-name]
将本机的工作成果推送到远程仓库
例如要将本地的master分支推送到origin远程仓库上:
1 | $ git push origin master |
分支
也许到之前为止,大家会觉得Git和Svn除了实现原理不同以及实现了分布式之外,在日常使用上并没有什么太大的区别(甚至更繁琐)。但接下来的分支,才是Git的精髓部分。
为什么要使用分支
举个简单的例子:假设你准备开发一个新功能,但是需要两周才能完成,第一周你写了50%的代码,如果立刻提交,由于代码还没写完,不完整的代码库会导致别人不能干活了。如果等代码全部写完再一次提交,又存在丢失每天进度的巨大风险。
于是你创建了一个属于你自己的分支,别人看不到,还继续在原来的分支上正常工作,而你在自己的分支上干活,想提交就提交,直到开发完毕后,再一次性合并到原来的分支上,这样,既安全,又不影响别人工作。
相比于Svn等工具,Git创建、切换分支的开销是非常小的,Git鼓励 频繁使用分支
分支的原理
要理解分支,需要继续深入一下Git的工作原理
Git如何储存数据
在Git中提交时,会保存一个提交对象(commit object),该对象包含一个指向暂存内容快照的指针,并同时包含本次提交的作者等相关附属信息,包含零个或多个指向该提交对象的父对象指针(首次提交是没有直接祖先的,普通提交有一个祖先,由两个或多个分支合并产生的提交则有多个祖先)。
假设在工作目录中有三个文件已经 修改 过,准备将它们暂存后提交。git add
暂存操作时,会对 每一个文件 计算校验和,然后把当前版本的文件快照使用 blog对象 保存到Git仓库中(为提高性能,若文件没有变化,Git不会再次保存)。将它们的SHA-1校验和加入到暂存区域等待提交。git commit
提交操作,时,Git首先会计算 每一个子目录 的校验和,然后将这些校验和保存为 tree对象 。 然后Git会创建一个 commit对象 ,它包含指向这个树对象的指针及注释、提交人、邮箱等信息。
现在,Git仓库中有五个对象:三个blob 对象(保存着文件快照);一个树对象(记录着目录结构和blob对象索引)以及一个提交对象(包含着指向前述树对象的指针和所有提交信息)。
单个提交对象在仓库中的数据结构:
多个提交对象之间的链接关系:
分支是什么
Git 中的分支,其实本质上仅仅是个指向commit对象的可变指针。Git会使用master作为分支的默认名字。在若干次提交后,你其实已经有了一个指向最后一次commit对象的master分支。它在每次提交的时候都会自动向前移动。
创建名为testing的新的分支,本质上就是创建一个指针,可以使用git branch
命令:
1 | $ git branch testing |
当前工作在哪个分支
Git 是如何知道你当前在哪个分支上工作的呢?其实答案也很简单,它还保存着一个名为HEAD的特别指针。它是一个指向你正在工作中的本地分支的指针。
切换分支时发生了什么
切换分支,本质上就是移动HEAD指针。
要切换到其他分支,可以执行git checkout
命令。我们现在转换到刚才新建的testing分支:
1 | $ git checkout testing |
分支切换的实际操作
为了更好的理解分支,我们接下来模拟实际工作中的场景,进行一系列的切换操作。
现在我们已经处于testing分支了,目前testing分支和master分支都是指向同一个commit,所以我们的工作区的内容现在还没有什么变化。
现在,我们要在testing分支上做一些文件修改,然后commit:
1 | echo "testing branch">>readme.txt |
提交后,产生了一个新的commit对象,并且HEAD随着当前testing分支一起向前移动。而master分支则是停在原地不动。
我们可以试着使用git checkout
命令切回master分支,看看发生了什么:
1 | $ git checkout master |
这条命令做了两件事:
- 它把HEAD指针移回到 master 分支。
- 把工作目录中的文件换成了master分支所指向的快照内容。
我们试着在master上再做一些改动并commit:
1 | echo "testing master">>readme.txt |
现在分支变成了上图所示,我们可以在master与testing间随时切换,并修改工作区的文件内容。必要时再将这两个分支合并。
分支新建与合并的实际操作
接下来,再以一个比较长的真实的工作场景进行举例
我们首先在master分支上进行工作,并提交了几次更新,测试无误后编译发布至生产系统。
之后我们决定要修补问题追踪系统上的53号问题,这时可以使用git checkout -b
命令快速创建一个分支并切换过去:
1 | $ git checkout -b iss53 |
这相当于执行了下面这两条命令:
1 | $ git branch iss53 |
我们在iss53分支上写了一些代码,并commit
1 | $ vi index.html |
iss53上的工作还没完成,突然接到通知,生产系统有一个紧急BUG需要立刻修复。所以我们首先切回master分支,然后在master的基础上,又新建出一个hotfix分支来修复BUG。
1 | $ git checkout master #回到master分支 |
在hotfix分支上搞定BUG之后,我们切回master分支,使用git merge
把刚才的hotfix合并进来
1 | $ git checkout master #切换回master分支 |
备注:本次合并时出现了“Fast forward”的提示。由于当前 master 分支所在的提交对象是要并入的 hotfix 分支的直接上游,Git 只需把 master 分支指针直接右移。换句话说,如果顺着一个分支走下去可以到达另一个分支的话,那么 Git在合并两者时,只会简单地把指针右移,因为这种单线的历史分支不存在任何需要解决的分歧,所以这种合并过程可以称为快进(Fast forward)。
这时hotfix分支已经没用了,可以删掉了
1 | $ git branch -d hotfix #只是删除了一个指针 |
现在回到之前未完成的53号问题上,继续写一些代码
1 | $ git checkout iss53 |
在问题53相关的工作完成之后,可以合并回master分支。实际操作同前面合并hotfix分支差不多,只需回到master分支,运行git merge命令指定要合并进来的分支。
1 | $ git checkout master |
请注意,这次合并操作的底层实现,并不同于之前 hotfix 的并入方式。因为这次你的开发历史是从更早的地方开始分叉的。由于当前 master 分支所指向的提交对象(C4)并不是 iss53 分支的直接祖先,Git 不得不进行一些额外处理。就此例而言,Git 会用两个分支的末端(C4 和 C5)以及它们的共同祖先(C2)进行一次简单的三方合并计算。
这次,Git 没有简单地把分支指针右移,而是对三方合并后的结果重新做一个新的快照,并自动创建一个指向它的提交对象(C6)。这个提交对象比较特殊,它有两个祖先(C4 和 C5)。
有时候合并操作并不会如此顺利。如果在不同的分支中都修改了同一个文件的同一部分,需要手工来处理冲突。
1 | $ git merge iss53 |
Git作了合并,但没有提交,它会停下来等你解决冲突。
1 | $ git status |
任何包含未解决冲突的文件都会以未合并(unmerged)的状态列出。Git 会在有冲突的文件里加入标准的冲突解决标记。
1 | $ vi index.html |
可以看到 ======= 隔开的上半部分是 HEAD,即master,下半部分是在iss53分支中的内容。
手工合并代码后,把 <<<<<<<,======= 和 >>>>>>> 这些行也一并删除。这时可以用git commit来提交了。
分支策略
实际开发中,对于分支的管理,已经有很多最佳实践,大多数情况下,我们只需要遵守一些基本原则:
- 首先,master分支应该是非常稳定的,也就是仅用来发布新版本,平时不能在上面工作。
- 平时的开发工作都放在dev分支上,也就是说,dev分支是不稳定的。到某个时候,比如测试通过,需要1.2版本发布时,再把dev分支合并到master上,在master分支编译发布1.2版本。
- 针对新需求、修复等具体的任务,每次都在dev分支上开一个新的任务分支出来,工作完成后,再向dev分支上合并就可以了。名称没有特别的规范,可以是人名,例如:zhangsan,也可以是任务名、需求编号等,例如:iss03、feature04、hotfix。
远程分支
之前讨论过远程仓库,接着又学习了分支,当二者结合到一起时,又会产生一些有趣的东西。
远程分支的概念
远程分支(remote branch),即远程仓库中的分支。同步到本地后,与本地分支不同的是,它们 无法移动 ;且只有在Git进行网络交互时才会更新。远程分支就像是书签,提醒着你上次连接远程仓库时上面各分支的位置。我们用 (远程仓库名)/(分支名) 这样的形式表示远程分支(例如origin/master)。
如果我们在本地master分支做了些改动,与此同时,其他人向远程仓库推送了他们的更新,那么服务器上的master分支就会向前推进,而于此同时,我们在本地的提交历史正朝向不同方向发展。(不过只要你不和服务器通讯,你的 origin/master 指针仍然保持原位不会移动。)
可以运行git fetch origin
来同步远程服务器上的数据到本地。该命令首先找到origin是哪个服务器,然后从上面获取你尚未拥有的数据,更新你本地的数据库,然后把origin/master的指针移到它最新的位置上。
可以使用git remote
命令查看远程仓库的详情
1 | $ git remote -v #列出远程服务器清单 |
跟踪远程分支
从远程分支checkout出来的本地分支,称为跟踪分支 (tracking branch)。跟踪分支是一种和某个远程分支有直接联系的本地分支。
在跟踪分支里输入 git push
,Git 会自行推断应该向哪个服务器的哪个分支推送数据。同样,在这些分支里运行 git pull
会获取所有远程索引,并把它们的数据都合并到本地分支中来。
在克隆仓库时,Git 通常会自动创建一个名为 master 的分支来跟踪 origin/master。这正是 git push 和 git pull 一开始就能正常工作的原因。
1 | $ git checkout -b serverfix origin/serverfix |
这会新建并切换到serverfix本地分支,其内容同远程分支origin/serverfix一致。
推送本地分支
要想和其他人分享某个本地分支,你需要把它推送到一个你拥有写权限的远程仓库。
例如本地有一个serverfix分支需要和他人一起开发,可以运行 git push (远程仓库名) (分支名):
1 | $ git push origin serverfix |
或者加入–set-upstream设置跟踪后,以后直接使用git push
就可以推送了:
1 | git push --set-upstream origin serverfix |
GitHub
[GitHub](https://github.com)是一个面向开源及私有软件项目的托管平台,因为只支持Git作为唯一的版本库格式进行托管,故名GitHub。
GitHub本身没有什么好学的,随便看就知道怎么用了 知乎:怎样使用GitHub
重点是,GitHub上有非常多优秀的个人项目值得我们学习,我们也可以将自已的代码发布上去。可以看成是程序员的博客吧,只贴代码,不废话。
在GitHub上发布开源项目是免费的,但是私有项目收费。
GitLab
GitLab是一个用Ruby on Rails写的开源的版本管理系统,实现一个自托管的Git项目仓库,可通过Web界面进行访问公开的或者私人项目。它拥有与Github类似的功能,能够浏览源代码,管理缺陷和注释。
可以管理团队对仓库的访问,它非常易于浏览提交过的版本并提供一个文件历史库。团队成员可以利用内置的简单聊天程序(Wall)进行交流。它还提供一个代码片段收集功能可以轻松实现代码复用,便于日后有需要的时候进行查找。
GitLab是目前搭建内部Git服务器的首选,当然如果要求不高的话,我们也可以直接使用SSH协议来快速搭建Git服务端。
常用Git命令清单
更多内容请直接参考 阮一峰的网络日志
推荐文档
不要指忘2小时的培训能带来多大的收益,最简单高效的方式,还是要多看优秀的文档。
本文大量参(chao)考(xi)了以下两部文档:
廖雪峰的在线教程 适合快速上手
Pro Git中文版 中文第一版