前言
集中式版本控制系统(比如CVS,SVN或者Subversion等)的布局是有一个中央服务器, 每个开发者在他个人电脑上有一个包含项目文件的工作目录,也就是工作区. 开发者本地工作区的修改需要提交给中央服务器, 一般提交之前需要从中央服务器上或者最新代码. 中央服务器存储着文件的当前和历史版本信息. 集中式版本控制系统比较明显的缺点是中央服务器一旦故障,整个项目就跪了,这一点明显区别与分布式版本控制系统. 其次集中式版本控制系统是需要联网使用的.
对于分布式版本控制系统, 开发者环境和中央服务器环境没有绝对的界限. 他们之间地位是相等的, 因为任何一个开发者都拥有一个用于当前文件操作的开发区和用于存储项目所有版本,分支以及标签的本地版本库(也称为克隆). 通过push和pull命令,我们可以将修改从一个版本库传送到另一个版本库. 由此可知, 分布式版本控制系统的高可用和强大的数据备份就体现在开发过程中任一开发者环境版本库故障了,只需要从其他版本库克隆一份就可以了. 开发者的开发工作可直接在本地进行,无需网络, 这一工作方式有效提升了分布式版本控制系统的高性能(无网络延迟), 若需要传送到中央服务器或者其他开发版本库时才需要网络. 此外, 这样的模式同时也方便了维护人员, 比如一些重构工作,我们就可以直接克隆一个版本库副本出来用于尝试,避免对原始版本库造成影响.
分布式版本控制系统的一些称谓解释:
项目版本库: 主要用于存储’官方’创建并发行的版本
共享版本库: 主要用于开发团队内成员之间的文件交换
工作流版本库: 用于填充那些代表工作流中某种特定进展状态的修改,比如审核通过的状态等
派生版本库: 主要用于从开发主线分离出某部分内容,或者隔离出可能永远不会被包含在主线中的,用于实验的那部分开发进展
版本库本质上是一个高效的数据存储结构, 它包括以下三部分:
文件(blob): 包括文本也包括二进制数据,这些数据不以文件名形式保存
目录(Tree): 目录中保存的是与文件名相关联的内容,也会把偶偶其他目录
版本(commit): 每一个版本所定义的都是相应目录的某个可恢复的状态.
对于所有的数据, 它们会被计算成一个16进制散列值, 这个值就作为相关对象的引用以及日后恢复数据时需要的键值
Git基本操作
git 安装
Git官方下载地址: http://git-scm.com/download
系统环境: centos7
安装git
$ yum install -y git
查看git版本
$ git --version
配置用户和邮箱(需要进入工作目录)
$ git config --global user.name “your name”
$ git config --global user.email “your email”
创建第一个Git项目
1, 选择一个目录作为项目工作区, 进入目录
$ cd /projects/helloworld
2, 初始化目录为工作区
$ git init
3, 首次提交
$ git add readme.txt
$ git commit -m “first commit”
4, 检查状态
$ git status
5, 显示历史
$ git log
Git的协作能力
作为一个新开发者,首先去远程仓库克隆项目当前版本,当前就会成为工作区
$ git clone https://github.com/XXX/xxx.git
或者, 如果是克隆本机上的一个版本库到另一个目录下继续开展工作的话(可在任意目录下执行)
$ git clone /projects/helloworld /projects/helloworld-clone
查看版本库日志
$ cd /projects/helloworld
$ git log --oneline
协作开发时, 你需要获取其他开发者的修改, 假设 /projects/helloworld中做了一些修改, 而克隆版本库/projects/helloworld-clone需要获取修改内容
$ cd /projects/helloworld-clone
$ git pull(假设刚才以本机克隆情况)
可以直接git pull是因为创建克隆版本库时,原版本库的路径会被存储在克隆体中,当然也可以指定版本库
如果克隆版本库里也做了一些修改,则需要在工作区中合并两边的代码,然后提交. 这个过程也就是合并
查看合并图
$ git log --graph
将克隆版本库中的修改pull到原版本库(指定了版本库和分支)
$ cd /projects/helloworld
$ git pull /projects/helloworld-clone master
push命令用于将提交传送给其他版本库,但是,push命令只适用于那些没有开发者在上面开展具体工作的版本库. 也就是常说的裸版本库,即不带工作区的版本库. 裸版本库一般用来充当开发者们传递提交的汇聚点, 这样方便开发者拉回其他开发者的修改
创建裸版本库
$ git clone --bare /projects/helloworld /projects/helloworld-bare.git
假设版本库/projects/helloworld又做了一些修改, 需要push到共享版本库
$ cd /projects/helloworld
$ git push /projects/helloworld-bare.git master
提交本质
Git管理的是版本库的版本, 而版本库的版本是以提交形式保存的. 任意一次提交都覆盖了整个项目, 也就是一次提交过程中, 版本库保存了整个项目的文件.
每一次提交, Git都会计算一个16进制的由40个字符组成的唯一编码,也被称为提交散列值(commit hash). 只要知道散列值就能恢复到对应提交被创建的时间点上. 在Git里, 恢复到某一个版本的操作被称为检出(checkout)
Git会保存每个文件的访问权限, 但不会保留文件的修改时间. 因为在执行检出操作时, 文件的修改时间就会被设置为当前时间.
一般, 提交中会包括当前所有的修改, 既有新增文件也有被删除文件.不过除了.gitignore文件. 一次提交分以下两步
1, 注册修改
$ git add --all #将当前目录下所有修改纳入下次提交
2, 创建提交
$ git commit -m “xxx”
再看散列值, 散列值是本地生成的, 不需与其他计算机通信. 散列值是根据文件内容和元数据(作者,提交时间)计算出来的. 两次不用修改得到同以散列值的概率是非常低的.
$ git fsck #查看版本库是否完整
$ git checkout commit_id #恢复到指定commit_id版本库
$ git tag #给当前commit打一个标签,之后可以基于标签回退版本
$ git tag <commit_id> # 给指定版本打标签
版本库中包含的提交并非独立的,它们彼此之间会形成一个关系图,反映整个开发过程.
$ git diff 77d231f HEAD # 比较两次提交间的差异
$ git diff 77d231f HEAD – readme.txt #指定比较文件
$ git log -n 3 #显示最新三次提交信息
$ git log --oneline #以每个版本库一行内容形式显示版本库信息
$ git log --graph --oneline #图形化显示版本库信息
多次提交
我们说提交通常分add和commit两步.首先, add命令将当前工作区中相关修改纳入到一个缓存区,这个缓存区称为暂存区或者索引. 然后, commit命令将暂存区中的修改传送到版本库中.
通过status命令可以查看当前工作区中发生的修改以及那些修改被提交到暂存区中以作为下次的提交.
$ git diff # 显示工作区和暂存区的区别
$ git diff --staged # 显示当前版本库中HEAD提交与暂存区之间的区别
撤销修改 git checkout VS git reset
场景一: xxx.py文件修改了,但没有add
$ git checkout --xxx.py
撤销修改后,工作区和版本库状态一样
场景二: xxx.py 执行了git add后又修改了
$ git checkout --xxx.py
上述操作后回到git add后但没修改时状态
注意: git checkout --xxx.py表示回退到最近一次git commit(场景一)或者最近一次git add状态(场景二)
场景三: 想要撤销暂存区里的修改
$ git reset HEAD xxx.py
上述操作会恢复到xxx.py没有add前状态
场景四: 如果文件不仅add而且commit了,想要撤销的话直接版本回退
$ git reset --hard HEAD^ #回退到上一个版本
或者
$ git checkout [last_commit_id]
删除文件
场景一: commit了一个错误文件
$ git rm xxx.py #可删除版本库中文件,工作区中自行删除
场景二: 工作区中删错了一个文件, 该文件之前提交过
$ git checkout --xxx.py #执行后,版本库版本会替换工作区的内容
通常我们在项目的根目录中创建一个.gitignore文件, 该文件里列出一些不想被Git跟踪管理的文件 注意一点: 比如想要忽略项目里generated/目录下文件一定要写成/generated/,否则git会把类似src/demo/generated/等路径都忽略的
储藏
开发过程中,如果突然需要紧急修改某个问题,此时可用git stash命令将当前工作区和暂存区的修改保存在储藏栈缓存区,日后可恢复出来继续工作
$ git stash pop #恢复栈顶的被储藏修改
$ git stash list # 查看储藏栈的修改内容
版本库
Git的核心是一个对象数据库.该数据库可用来存储文本和二进制数据.比如,我们可以用hash-object命令将一条记录插入数据库
$ git hash-object -w xxx.py # Git会返回一个40个字符的代码
$ git cat-file -p [commit-id] # cat-file访问对象 -p表示打印
Git非常适合用于拥有大量小型源文件的项目.它的性能瓶颈只有在总数据量非常巨大的时候才会体现出来.
对于文件和目录的存储, Git使用一种包含两种节点类型的简单树结构. 文件内容保持不变,以blob对象按字节被存储到对象数据库中. 目录则用tree对象来表示.
出于节约空间的目的, git对于相同的数据只存储一次. 可以通过以下命令验证
$ git hash-object -w xxx.py
$ git hash-object -w xxx-copy.py
上述两条命令返回的散列值是同一个.
通过这种方式不仅能节约空间,性能也会得到提升. Git的许多操作快是因为算法层面其实只是在比较散列值,而不是真正去比较实际内容
假如新文件是在原有文件基础上做了微小改变时, git会使用增量方法来存储这些文件,这种情况下,包文件里将只存储原始版本后来被改变的那一小部分. 当执行gc命令时,git会删除所有多余,不再接受任何分支头访问的提交,并将剩下提交存储到包文件里.这其实就是实现了高压缩处理
不同内容的文件出现散列值相同的状况被称为敬列冲突. 敬列冲突是非常罕见的, 当然它出现的会有严重后果, 因为git本身就是靠散列值识别内容的. 但目前而言, 我们认为git是安全的
分支
$ git branch # 显示本地分支, 其中带*标记的为当前分支
$ git branch -a # 显示远程和本地分支
$ git branch -r # 显示远程分支
$ git branch dev # 为当前提交创建分支
$ git branch dev commit_id #为指定提交创建新分支
$ git checkout dev # 切换分支
$ git checkout -b dev # 创建分支并切换到新分支
$ git branch -d dev # (当前不处于要删除分支上)删除分支
$ git branch -D dev # 当前在分支dev上,并删除分支dev
注意, 删除一个分支只是删除了指向相关提交的指针,其提交对象还是保存在版本库中的. 如果要恢复一个被删除的分支的话,以下情况
1, 已知被删除分支指向提交的散列值
$ git branch dev commit-id
2, 不知道散列值时, 先确定散列值再恢复
$ git reflog # 先查看分支散列值信息, reflog记录着分支指针修改信息
$ git branch dev [HEAD@{1}]
当我们切换分支时可能被拒绝,以下处理方式
1, 切换之前的修改先提交再切换
$ git commit --all
$ git checkout dev
2, 直接强制切换放弃当前修改内容
$ git checkout --force dev
3, 储藏修改并切换,之后用stash pop命令恢复
$ git stash
$ git checkout dev