上周项目组的代码库从svn版本管理切换到了git,随着开源社区github以及开源文化的近年来的火热,git作为开源的分布式版本控制系统,被越来越多开发者和团队使用。在没用使用Git之前,大家对Git的看法可能都是入门比较困能。这周在开发的过程中,我整理了一下Git的一些基础概念和需要注意的实现,和大家一起分享一下。
在使用Git之前,大家对Git的认识可能仅仅就是,它是一个分布式的版本控制系统,所有的本地仓库都含有全部版本仓库的数据,不会存在一个中心服务器,因此不会存在中心仓库出问题导致版本数据丢失以及开发者不能开发的问题。
版本控制的历史
版本控制系统的历史经历了以下三个历史:
- 本地版本控制系统:例如RCS,我也没有经历过,它的实现机制是将文件修改的差异保存在本地硬盘上,这样通过所有的补丁可以重新计算出各个版本的文件内容。
- 集中化版本控制系统(CVCS):大家最常用的SVN,集中化版本系统解决的一大痛点就是开发者协同工作。集中化版本控制系统在实现上也是通过保存文件的差异。协调开发者通过客户端连接到中心版本服务器进行文件的提交更新,这样每个人都可以知道其他人在做什么。集中化版本系统的问题是,如果中心服务器出问题,就会导致所有数据丢失或者项目无法工作,而开发者本地保存的快照仅仅是最后拉取的数据,所有历史提交记录都有丢失的可能性。
- 分布式版本控制系统:以Git为代表,客户端每次拉取最新版本的文件快照,会把代码仓库完整地镜像下来。 这么一来,任何一处协同工作用的服务器发生故障,事后都可以用任何一个镜像出来的本地仓库恢复。 因为每一次的克隆操作,实际上都是一次对代码仓库的完整备份。
对于我们今天说的Git,除了拥有分布式的版本控制系统的特性,还有一个重要的实现差别就是:Git对待文件的变更是通过保存整个文件的快照来实现,而不是像其他系统将文件变更的差异进行保存来实现。
Git设计哲学-快照-哈希索引
前面讲到Git对待文件的变更是通过保存整个文件的快照来实现,而不是像其他系统将文件变更的差异进行保存来实现的。在保存到Git之前,所有的数据都需要通过SHA-1算法对内容计算校验和,并将此校验和即哈希值作为数据的唯一标识和索引。所以如果文件在传输时变得不完整,或者磁盘损坏导致文件数据缺失,Git 都能立即察觉。
Git 从核心上来看不过是简单地存储键值对(key-value)。它允许插入任意类型的内容,并会返回一个键值,通过该键值可以在任何时候再取出该内容。Git存储的索引内容包含三种对象:
- commit对象:每次提交都会至少产生一个commit对象,它的内容包括:指向parent commit对象,根tree对象。
- tree对象:类似于目录,tree对象中包含多条记录,每条记录保存了本次快照的所有tree对象和blob对象。
- blob对象:类似于文件,保存具体的文件内容。
每个对象在生成的时候都是以key-value的方式创建,都是通过hash值来进行管理。如下图:每次提交后都会产生新的commit对象,tree对象,blob对象。
Git存储这些对象的方式:为每个对象生成文件,然后根据文件的内容和头部信息进行HASH,将生成的HASH值的前两个字符作为目录名,后38个字符作为文件名存放在’./git/objects/‘目录下面。
git cat-file
git cat-file是查看git对象的瑞士军刀 ,可以通过该命令查看任意git 对象的内容,类型等数据。如下,最新的commit对象是一个Merge产生的:
1 | // 查看最近一条日志 |
通过cat-file 查看该对象的内容如下:
- 指向tree对象的key,每个commit(是不是全部?)都会修该版本库中的文件,所以就会生成新的tree对象。
- 指向parent commit对象的key,由于本次提交是合并产生的,所有本次commit对象指向两个parent commit对象。
- 本次commit的author,commiter,相关日志信息。
我们再通过cat-file查看这一次提交的tree对象,可以看到每次提交的tree对象都是指向版本库的根目录,因为每次提交版本库都会发生变化,依次向上最终根目录tree对象的也会发生变化,所以每次提交根目录的tree对象必然会发生变化。
1 | $ git cat-file -p 2470df763aa82e59b700b8c341184b581a3a8774 |
Git基本操作
Git基本配置
Git的对于版本仓库的配置分为三个级别,分别对应三个不同文件:
- /etc/gitconfig,本机全局配置,本系统上所有人所有仓库共享的配置,可以通过命令:git config –system 来进行设置;
- ~/.gitconfig,本用户的所有仓库的配置,可以通过命令: git config –global来设置;
- .git/config ,本仓库的配置,可以通过命令: git config 来设置;
以下的配置都是建议大家在一开始的时候都进行设置的,否者可能回遇到一些坑。
Git仓库在进行commit时,必须要进行的配置就是提交人和提交人的邮箱:
1 | git config --global user.name 'walkerdu' |
Git仓库支持HTTP和SVN协议进行通信,当采用口令方式减去时(而不是公私钥方式),需要多长输入用户名和密码,可以通过如下命令来保存密钥信息:
1 | git config --global credential.helper store |
Git在合并过程进程会遇到需要修改提交(默认emacs,针对很不好用),以及diff对比的工具,通过一下方式修改:
1 | git config --global core.editor vim |
Git Windows版本默认会将版本库中linux换行符的文件拉取到本地的时候转换成Windows换行符,然后保存的时候再修改回linux换行符,这个出发点虽然很好:屏蔽不同平台开发者的差异,但这可能会带来其他问题,例如Windows开发者将文件拷贝到linux下提交。再者一个项目团队对于项目都是有自己的要去,像这种换行符的问题都有自己的标准,Git不应该做这种修改项目本身数据的东西。
1 | git config --global core.autocrlf false |
Git基本操作
一开始就说到,大家对Git的第一印象就是门槛比较高,命令太多太复杂,下面是一些简单的基本命令,使用频率比较高的:
- git clone: 克隆仓库到本地
- git add:将未跟踪或已修改的文件提交到stage区域
- git commit:提交修改到本地版本库中。
- git pull –rebase:拉取远程仓库的数据到本地远程分支,并进行rebase合并。
- git fetch:拉取最新远程分支数据。
- git push:将本地仓库分支推送到远程仓库。
- git merge / rebase: 合并分支
Git的文件状态会处于以下四种之一,通过不同的操作会对文件的状态进行修改:
- 未跟踪:对于新添加的文件都处于这种状态;通过git add命令添加到暂存区;
- 已跟踪且未修改:该状态就是版本库中被纳入版本管理,且未被修改的状态。通过git commit命令可以将暂存区文件或已修改的未暂存的文件纳入此状态;
- 已修改:对版本库中的文件进行修改后,处于这种状态。可以通过git add+commit或者直接通过git commit进行提交修改;
- 已暂存:最新的修改已经保存在暂存区,等待commit到版本库中;
Git日志
Git的日志系统是Git实现的一个真实反映,每个commit都会有个对应的commit-id哈希值,类似于SVN的版本号,Git日志会随着多人合作出现分叉,以反映出Git版本的走向。以下是git log比较常用的选项:
-p: 显示每次提交的内容差异,当然也可以通过git show命令来完成;
–pretty: 自定义日志输出格式,目前我的配置是:
1
alias git-log='git log --graph --pretty=format:'\''%Cred%h%Creset -%C(yellow)%d%Creset %cn %s %Cgreen(%ad)%Creset'\'' --abbrev-commit --date='\''iso'\'''
日志过滤
- -n:仅显示最近的 n 条提交;
- –since, –after 仅显示指定时间之后的提交;
- –until, –before 仅显示指定时间之前的提交;
- –author 仅显示指定作者相关的提交;
- –committer 仅显示指定提交者相关的提交;
–name-only: 显示文件列表
以下是我配置的git log的输出效果:
Git 分支
Git默认从远程仓库拉取下来的分支名为master,前面介绍了Git的内部实现,Git的内部是通过commit,tree,blob三种对象组合而成的,每次的提交都会创建commit对象,而Git的分支本质上仅仅是个指向 commit 对象的可变指针。Git通过保存着一个名为 HEAD 的特别指针来表示当前在哪个分支上工作。
1 | $ cat .git/HEAD |
通过命令git branch testing
来创建一个名为testing的分支,其实际上是在当前HEAD指向的commit对象上创建一个分支指针。而该分支指针只是一个包含其指向commit对象的hash值的文件。
创建完testing分支后我们可以查看当前所有本地分支的信息:
1 | $ls .git/refs/heads/ |
和上图展示的一致,master和testing分支都是指向最新的commit对象。我们通过git checkout testing
来切换到testing分支,切换之后的示意图如下:
切到testing分支后,HEAD指针指向了testing分支:
1 | $cat .git/HEAD |
通过上面的实现,我们可以知道Git 的实现与项目复杂度无关,它可以在几毫秒内完成分支的创建和切换。再加上每次提交的commit对象都有记录其父节点的信息,进行分支合并时只需要找到共同祖先就可以非常容易的实现合并。这就是为什么Git建议大家多采用拉取分支方式进行开发。因为随意拉取分支的代价太小了。
Git远程分支
Git远程分支地对远程仓库中的分支的索引,它们是一些无法移动和修改的本地分支,只有在 Git 进行网络交互时才会更新。远程分支就像是书签,标识上次连接远程仓库时上面各分支的位置。如下,可以看到远程仓库remotes:
1 | $ git branch -a |
与远程分支对应的就是跟踪分支。所有从远程分支checkout产生的本地分支都称为跟踪分支, 它和远程分支有直接关联。在本地分支里对分支 merge/rebase/push/pull等操作都默认会关联到远程分支。查看本地分支跟踪的远程分支 ,可以通过以下三种方式:
- git branch -vv
- git remote show origin
- cat .git/config
下面的流程就是一个文件在整个Git版本库中流转的过程:
Git工作流
Git流行的三种工作流包括:Git Flow, Github Flow, GitLab Flow;这三种工作流都有各自的特点:
Git Flow
Git Flow工作流的特点是项目会长期存在两个分支:
- 主分支master:对外发布版本;
- 开发分支develop:日常开发版本;
除了以上两个长期分支外,还会存在三种短期分支,开发完即合入master or develop,然后删除:
- 功能分支(feature branch)
- 补丁分支(hotfix branch)
- 预发分支(release branch)
Git flow的优点是清晰可控,缺点是需要同时维护两个长期分支。且该模式适合于”版本发布”的工作模式,即周期新的产出一个版本,即有特定的发布窗口。但对于**”持续发布”,每次代码在master提交都需要进行部署发布的项目就没有意义**,因为master和develop分支差别不大,还要维护两个长期版本。
GitHub Flow
顾名思义,这是GitHub推荐的一种工作流模式。长期只有一个master分支,根据需求从master拉取新分支,开发完成后向master发起一个Pull Request(PR),PR是一个通知,大家一起进行代码的评审和讨论,此过程中也可以不断提交修改,最后PR被接受,合入master,然后进行部署,删除分支,整个流程就结束了。如下图:
和Git Flow相比,正好相反,GitHub Flow适合于“持续发布”,每次代码在master提交都需要进行部署发布的项目。而对于**”版本发布”的项目并不合适**。
GitLab Flow
顾名思义,这是GitLab推荐的工作流。它其实是一个Git Flow和GitHub Flow的一个结合。它上游只有一个master分支,且其作为上游分支,根据不同的环境建立不同的分支,所有的修改必须由”上游”向”下游”进行。
- 针对”持续发布”的项目,每个不同的环境建立不同的分支,例如:开发环境:master,预发布:pre_release,真实环境:online等等。
- 针对”版本发布”的项目,除了master分支外,针对稳定版本拉取一个分支,例如proj_stable_1.1。
所有的修改都必须先在上游master进行修复,然后合入对应的分支。这样GitLab Flow就可以很好的支持Git Flow和GitHub Flow。
参考
A successful Git branching model