一、Git概述
版本控制系统(VCS)
目的:是为了持续性的对代码或文档进行备份和存档,对变更进行追踪管理。
发展进程:SCCS源码控制系统–>RCS修订控制系统(本地)–>CVS并行版本系统–>Subversion(SVN)(集中化)–>Git(分布式)
Git简史
Git,『官网传送门』,由大神Linus Torvalds发明,git本意是无用的人、饭桶,令人舒服的解释是Global Information Tracker。
之前Linux开源社区用的BitKeeper,05年开始收费后,迫使开发自己的版本系统。起初用来管理Linux内核的开发工作。于05年4月托管,Linux内核提交git,包含670万行代码。
Git的目标是需要以下特性:
- 完全分布式
- 免费开源
- 能胜任上千开发人员的规模,允许独立且同时开发、离线开发
- 非线性分支管理,支持并鼓励基于分支的开发
- 性能优异,近乎所有操作都是本地执行。解决网络传输瓶颈,保证执行速度
- 保持完整性和可靠性,数据存储前使用SHA-1散列来计算校验和,会发现丢失或损坏的文件
二、Git起步
客户端安装配置
- 可以直接去Git官网下载。
- Windows可使用cygwin版本的Git,或原生版本的msysGit。或者直接安装GitHub for Windows。
- Mac系统使用Xcode Command Line Tools。或者直接安装GitHub for Mac。
- Linux可使用yum或apt-get安装。
git支持不同层级的配置文件
1 | .git/config #版本库级配置 |
配置提交作者,根据上述层级分别对应,默认为global
1 | $ git config -h # 查看git config帮助 |
服务器端安装配置
- Gitlab
- Github,如访问慢可以使用镜像网站加速
- Gitee
git传输协议有以下4种
- 本地协议
- HTTP协议
- SSH协议
- GIT协议
通用命令
1 | $ git # 弹出usage |
理解短和长选项命令
1 | $ git commit -m "fixed bug" |
常用命令如下:
初始版本库命令
1 | $ mkdir test |
Git三个区域
- 工作目录,用来使用或修改
- 暂存区域,保存了下次提交的文件列表信息,是一个index文件,有时被叫做索引
- 版本库,用来保存项目元数据和对象数据的地方
修改和提交的基础命令
add暂存,然后commit提交
1 | $ git add . #将文件放入暂存区域。文件状态从Untracked到Staged,或者从Modified到Staged |
三、Git原理
版本库(repository)
版本库位于.git文件夹下,维护了两个主要的数据结构,对象库object store和索引index。
对象库
1、对象库,包含以下4种类型:
- 块blob,存放任意文件的数据
- 目录树tree,代表一层目录信息。
- 提交commit、一个提交对象保存版本库中每一次变化的元数据,每一个提交对象指向一个目录树对象。
- 标签tag,给提交对象取的tag。
可以通过以下命令git cat-file -t查看对象是哪个类型。
以下的图示,名为master分支和名为V1.0的标签都指向id为1492的提交对象(commit),提交对象指向任何指定的树对象(tree,可根据此树对象找到索引),树对象指向若干blob对象或其他树对象。
索引
1、索引,是一个动态的二进制文件,描述版本库的目录结构。
2、Git的工作原理,在工作目录和版本库之间加设了一层索引(index),用来暂存(stage)。一次提交的过程:暂存变更和提交变更。
3、Git的索引,不包含任何文件内容,它仅仅追踪你想要提交的那些内容。当执行git commit命令的时候,会去检查索引,而不是工作目录来找到提交的内容。任何时候都可以通过git status查询索引的状态。
可寻址内容、追踪内容、打包文件等概念
- 可寻址内容名称,对象库的每一个对象,都有一个唯一的名称,SHA1散列值,160位的2进制=40位的16进制数。对同样的内容始终产生同样的id。因此任何变化都会导致SHA1改变,从而被编入索引。散列函数,提供了一种有效的方法来比较两个对象,甚至是非常大而复杂的数据结构,而不需要完全传输。大部分命令可以通过前几位来替代40位hash。
- Git追踪的是内容,如两个文件内容一样,则对象库只保存一份blob副本,并将blob放入对象库,并以SHA1值作为索引。当文件版本变更时,git会存储每个文件的每个版本,而不是他们的差异。
- Git使用打包文件(pack file)来高效的存储每个文件的每个版本,并实现版本库的高效数据传输。
实例探究
1 | $ mkdir hello |
原理总结:
- git通过目录树(tree)的对象来跟踪文件的路径名,当使用git add、git rm、git mv等命令时,会用新的路径名和blob信息来更新索引(位于ccc)。
- 任何时候,都可以使用git write-tree从当前索引创建一个树对象(tree),来捕获索引当前信息的快照。且每次对相同的索引计算一个树对象,SHA1散列值都是一样的。
- 使用git commit-tree可以生成提交对象,包含上一步的索引生成的树对象,作者、时间、提交信息等。
- 使用git update-ref来更新分支指向commit对象
- 实际应用时,可以直接使用git commit命令,整合git write-tree、git commit-tree和git update-ref步骤。
Git对象模型和文件的详细视图
模拟修改文件内容后的视图演变,有以下4步。
- 矩形代表块blob
- 三角代表目录树tree
- 圆圈代表提交commit
- 图中的“索引”区域,可使用git ls-files -s带查看
(1)初始文件和对象
(2)编辑file1之后
(3)在git add之后
(4)在git commit之后,更新的ref分支在refs/heads/master文件中,为最后一次commit的提交树对象。
四、文件管理和索引
Git中的文件状态
1 | $ git status #查看状态 |
子目录以tree存储
新增一个完全相同的副本,发现子目录是tree。
1 | $ mkdir subdir |
git rm
1 | $ git rm OTHER.md #将文件从索引和工作目录都删除。使用该命令删除暂存区的文件,并删除工作区文件。后面需要提交,来移除Git仓库的文件。 |
git mv
如需要移动或重命名文件,可直接使用git mv。相当于先rm再add。
1 | $ git mv README.md README #更名,相当于以下3条命令: |
VCS当重命名后回丢失追踪,SVN需要使用svn mv命令显式的追踪每一次重命名。
而Git可以保留,当Git碰到重命名时,只会影响树对象,不会影响blob内容对象。查看两棵树差异就可发现blob被移动到新的地方。
1 | # 两个树对象指向的blob一样,名称不一样,是两个不同的树。 |
.gitignore文件
文件格式:
空行会被忽略,#开头的行用于注释。
目录名由反斜线/标记。
可使用shell通配符,如*。
起始的感叹号用于取反。
示例:
1 | *.a # 忽略所有 .a 结尾的文件 |
GitHub 有一个十分详细的针对数十种项目及语言的 .gitignore 文件列表,你可以在 『Github gitignore项目』 找到它。
层次优先级:
- 1、命令行上指定的模式。
- 2、相同目录的.gitignore文件
- 3、上层目录
- 4、.git/info/exclude文件
- 5、配置变量core.excludedfile指定的文件
但是有时候在项目开发过程中,突然心血来潮想把某些目录或文件加入忽略规则,按照上述方法定义后发现并未生效,原因是.gitignore只能忽略那些原来没有被track的文件,如果某些文件已经被纳入了版本管理中,则修改.gitignore是无效的。那么解决方法就是先把本地缓存删除(改变成未track状态),然后再提交:
1 | $ git rm -r --cached . # --cached代表只从索引移除,本地保留。-r代表递归 |
五、提交
提交(commit)流程
提交是用来记录版本库的变更的。会记录索引的快照并把快照放进对象库。Git会将当前索引的状态与之前的快照做一个比较,并派生出一个受影响的文件和目录列表。对不同的对象和状态,git的动作如下:
对象 | 是否修改 | git动作 |
---|---|---|
文件 | 是 | 创建新的blob对象 |
文件 | 否 | 沿用原有blob对象 |
目录 | 是 | 创建新的树对象 |
目录 | 否 | 沿用原有树对象 |
提交的快照是串联在一起的,需要和之前某个状态的索引进行比较,git也可以通过修剪有相同内容的子树来避免大量递归比较。提交是将变更引入版本库的唯一方法,且任何变更都必须由一个提交引入。Git非常适合频繁的提交。
原子集变更。一个提交,无论有哪些改变,要么全部应用,要么全部拒绝。
常用commit命令
1 | #当添加或更改文件时,允许合成一步 |
识别提交,显示引用(散列ID)和隐式引用(HEAD就是一种)。
1、绝对提交名,散列ID表示唯一确定的一个提交。Git允许使用唯一的前缀来缩短这个数字。
2、引用(ref)和符号引用(symbolic reference)。ref通常指向提交对象。symref间接指向Git对象。
.git/refs/中,refs/heads/REFNAME代表本地分支,refs/remotes/REFNAME代表远程跟踪分支,refs/tags/REFNAME代表标签。他们都是引用。
Git自动维护几个用于特定目的的特殊符号引用。可用在提交的任何地方使用。
符号引用 | 作用 |
---|---|
HEAD | 默认指向当前分支的最后一次提交 |
ORIG_HEAD | 使用它恢复或回滚到之前的状态 |
FETCH_HEAD | 最近抓取的分支HEAD的简写 |
MERGE_HEAD | 正在合并进行中HEAD的提交 |
通过符号^和~来确定相对提交名。
checkout只会移动HEAD指针,reset会改变HEAD的引用值。
1 | $ git show-branch --more=35| tail -10 |
查看提交历史
查看提交日志
1 | $ git log |
查看某次提交的详细信息
1 | $ git show d9b1305573c70e8c1111b038311f4d8eff7cc79d |
查看两次提交之间的差异。git diff本身只显示尚未暂存的改动,不加参数,比较的是工作区与暂存区的差异。如只有一个参数,比较的工作区与指定commit-id的差异。如有两个参数,比较的是两个commit-id之间的差异。
1 | $ git diff #查看工作区与暂存区的差异,即未暂存的改动 |
远程仓库
1 | $ git clone https://gitee.com/iherr/test.git #远程库克隆 |
打标签
Git 使用两种主要类型的标签:轻量标签(lightweight)与附注标签(annotated)。
- 一个轻量标签很像一个不会改变的分支 - 它只是一个特定提交的引用。
- 附注标签是存储在 Git 数据库中的一个完整对象。 它们是可以被校验的;其中包含打标签者的名字、电子邮件地址、日期时间;还有一个标签信息;并且可以使用签名与验证。
1 | $ git tag -a v1.0 -m 'version 1.0'#对当前提交打标签,-a代表附注标签 |
使用git push不会传送标签到远程仓库服务器上,在创建完标签后你必须显式地推送。
1 | $ git push origin v0.1 #推送特定标签到远程仓库 |
DAG(Directed Acyclic Graph)
Git通过有向无环图(DAG)实现版本库的提交历史记录,时间轴从左到右,不关心提交时间,只关心指向。
主分支从提交A、B、C、D开始,pr-17分支从A、B、E、F、G,并且提交H是一个合并提交,将pr-17合并到master分支。
一般的提交有且只有一个父提交。
初始提交没有父提交。
合并提交拥有多个父提交。
1 | $ git push <远程主机名> <本地分支名>:<远程分支名> |
六、分支
定义
分支,是启动一条单独的开发线的基本方法。
分支的本质,就是一个可以移动的指向最新commit的指针。由于 Git 的分支实质上仅是包含所指对象校验和(长度为 40 的 SHA-1 值字符串)的文件,所以它的创建和销毁都异常高效。创建一个新分支就相当于往一个文件中写入 41 个字节(40 个字符和 1 个换行符)。
- 一个分支通常代表一个单独的客户发布版。
- 可以封装一个开发阶段
- 可以隔离一个特性的开发或者复杂的bug
- 可以代表某个贡献者的工作
标签和分支都是引用,那么它们存储的内容也是类似的。都存储在refs/tags和refs/heads里。分支指向一个commit对象,标签指向一个标签对象,该标签对象的object属性也指向一个commit对象。
使用分支和标签的区别,tag的位置是固定的,在给指定提交打好标签以后,它就固定于此位置。分支的位置会不断变动的,随着分支的向前推移或者向后回滚,都在不断变化。
图解分支的新建与合并
1、在master默认分支上工作,有C0,C1,C2共3个提交
2、创建分支,解决#53问题。
1 | $ git checkout -b iss53 #新建并切换至新分支 |
3、在分支上提交C3。
4、有个着急的fix。
1 | $ git checkout master #切换回master分支 |
5、合并hotfix分支,启用Fast-forward,由于当前 master 分支所指向的提交是你当前提交(有关 hotfix 的提交)的直接上游,所以Git只是简单的将指针向前移动。master 被快进到 hotfix。
1 | $ git checkout master |
6、删除hotfix分支,并切换回iss53分支,继续修复bug并提交C5。
1 | #由于hotfix和master指向一个commit,可删除hotfix分支 |
7、将iss53合并回master, 因为master 分支所在提交并不是 iss53 分支所在提交的直接祖先,Git 不得不做一些额外的工作。 出现这种情况的时候,Git 会使用两个分支的末端所指的快照(C4 和 C5)以及这两个分支的工作祖先(C2),做一个简单的三方合并。
1 | $ git checkout master |
8、一次典型合并中所用到的三个快照
和之前将分支指针向前推进所不同的是,Git 将此次三方合并的结果做了一个新的快照并且自动创建一个新的提交指向它。 这个被称作一次合并提交,它的特别之处在于他有不止一个父提交。
9、后续可删除iss53
1 | $ git branch -d iss53 |
遇到冲突的解决方案
1、查看有冲突的文件
1 | $ git merge iss53 |
2、修改冲突的文件
1 | <<<<<< < HEAD:index.html |
这表示 HEAD 所指示的版本(也就是你的 master 分支所在的位置,因为你在运行 merge 命令的时候已经检出到了这个分支)在这个区段的上半部分(======= 的上半部分),而 iss53 分支所指示的版本在 ======= 的下半部分。 为了解决冲突,你必须选择使用由 ======= 分割的两部分中的一个,或者你也可以自行合并这些内容。 例如,你可以通过把这段内容换成下面的样子来解决冲突:
1 | <div id="footer"> |
3、可再次查看status,冲突解决后可commit进行提交
1 | $ git status |
分支管理
1 | $ git branch #查看本地分支列表 |
远程分支
clone后本地分支和远程分支的关系
1 | $ git branch -r #查看远程分支 |
变基-rebase
在 Git 中整合来自不同分支的修改主要有两种方法:merge 以及 rebase。
- merge对应的观点是,仓库的提交历史即是“记录实际发生过什么”。它是针对历史的文档,本身就有价值,不能乱改。如果由合并产生的提交历史是一团糟怎么办? 既然事实就是如此,那么这些痕迹就应该被保留下来,让后人能够查阅。
- rebase对应的观点则正好相反,他们认为提交历史是“项目过程中发生的事”。
总的原则是,只对尚未推送或分享给别人的本地修改执行变基操作清理历史,从不对已推送至别处的提交执行变基操作。变基的风险,要用它得遵守一条准则:不要对在你的仓库外有副本的分支执行变基。
说的白话一点,对只自己用,且有代码提交洁癖的,用rebase。对团队协作的,那就不要用了,否则人民群众会仇恨你,嘲笑你,唾弃你。
1、有两个分支
2、可采用merge进行分支合并
3、当然也可以使用rebase,将 C4 中的修改变基到 C3 上。将提交到某一分支上的所有修改都移至另一分支上,就好像“重新播放”一样。它的原理是首先找到这两个分支(即当前分支 experiment、变基操作的目标基底分支 master)的最近共同祖先 C2,然后对比当前分支相对于该祖先的历次提交,提取相应的修改并存为临时文件,然后将当前分支指向目标基底 C3, 最后以此将之前另存为临时文件的修改依序应用。
1 | $ git checkout experiment |
4、现在回到 master 分支,进行一次快进合并。
1 | $ git checkout master |
cherry-pick
cherry-pick命令的作用,就是将指定的提交(commit)应用于其他分支。
1 | $ git cherry-pick <Hash> #将其他分支的某个commit,提交到当前分支 |
七、撤销相关
储藏-stash
经常有这样的事情发生,当你正在进行项目中某一部分的工作,您不想直接提交该工作,而你又必须转到其他分支上工作,或者需要git pull更新时,可以使用stash。会有以下提示:
1 | $ git pull origin master |
常用命令:
1 | $ git stash #实施stash,这样工作目录就干净了 |
文件修改后未执行git add操作,撤消对未暂存的文件的修改
1 | #checkout用于拉取暂存区文件,覆盖工作区对应文件。 |
已执行git add操作,需要撤销暂存区的某个文件提交。较少用到,因为IDE一般使用commit替换了add和commit两步操作
1 | # reset HEAD,仅改变暂存区 |
已执行git add操作,并撤销修改。
1 | $ git reset HEAD hello.txt # 先撤销暂存区 |
需要恢复指定文件到指定commit的版本
1 | # 查看指定文件的历史版本 |
已执行git commit操作,想撤销commit
可使用git reset或者git revert。如和他人协作,建议使用revert,这样可保留提交记录。但如果只是自己使用,且不想保留提交记录,可以使用reset。
- reset是恢复到之前某个commit的版本,且那个版本之后提交的版本我们都不要了。即将HEAD指针和master直接指向指定commit
- revert是撤销某个版本,revert后生成一个新的undo的commit,且会记录下整个版本的流程。
1 | $ git reset HEAD^ # 撤销到上一个版本,HEAD^等价于HEAD~1 |
1 | $ git reset 499b95 #默认使用--mix参数,代表就将头恢复掉,已经add的缓存也会丢失掉,需要重新add,工作空间的文件不变。 |
已经git push,撤销远程版本库的commit
1 | # commit的撤销有2种方式,revert和reset。 |
revert可能会出现失败,如下:
1 | $ git revert d6130 #revert 某个commitid,是恢复到parent节点,因为该commitid是merge,有两个parent,系统无法判定是哪个。 |
迁移git项目,保留所有commit记录
1 | $ git clone --mirror https://gitlab.xxx.com/old.git # 克隆mirror老项目 |
合并多个commit为1个(强迫症患者)
1 | $ git rebase -i HEAD~4 #对最近的4个commit进行rebase变基操作,进行提交的压缩; |
checkout和reset命令区别
1 | # 两者命令结构类似,可以操作commit提交层级或者path文件层级。由于commit和path参数都可选,因此中间的--符号用于消除歧义。 |
操作范围 | 命令 | HEAD是否移动 | index是否更新 | 工作目录是否更新 | 工作目录是否安全 | 说明 |
---|---|---|---|---|---|---|
Commit | reset –soft COMMIT | REF | NO | NO | YES | 移动HEAD |
Commit | reset –mixed(默认) COMMIT – | REF | YES | NO | YES | 更新索引区 |
Commit | reset –hard COMMIT | REF | YES | YES | NO | 更新工作目录 |
Commit | reset –keep COMMIT | REF | YES | SOME | YES | |
Commit | checkout COMMIT – | HEAD | YES | YES | YES | |
Path | reset [COMMIT] – PATH | NO | YES | NO | YES | 将指定文件更新到索引区 |
Path | checkout [COMMIT] – PATH | NO | YES | YES | NO |
git commit后修改提交信息
1 |
|
同步远程所有分支
1 | $ git fetch origin --recurse-submodules=no --progress --prune |
八、最佳实践
相关分支解释如下:
- master,主分支,长期存在。用于生产部署的版本,只能从release和hotfix分支merge,不能在此分支直接提交。部署到生产后打tag。
- develop,主开发分支,长期存在。用于汇集开发代码,主要用于合并feature分支,一些简单的修改可直接在此分支提交。
- feature,新功能分支,临时存在。用于新功能或者大的fix。基于最新的develop分支创建,命名为feat-xxx或者fix-xxx,完成后申请合到develop分支,合并验证通过后删除。
- release,发布版本分支,临时存在。基于最新的develop分支创建,命名为release-xxx,创建后发布到测试环境,如需要修改,直接在此分支进行提交,待测试通过后,合并到master发布生产。如有临时bug修改提交,需要合回develop分支。
- hotfix,线上版本热修分支,临时存在,命名为hot-xxx,从master分支创建,完成hotfix后合并回master和develop分支。验证通过后删除。