广西网络网站建设/推广资源网
代码提交的内幕原理
(一)git数据存储机制
- 1)git hash-object
git的数据存储格式实际上是一种非常简单的key-value存储格式。就是说,git支持将任何东西存储到其数据库中,
一般就是存储文件的内容,然后用一个key值来引用它。
首先随便找一个目录,初始化为git项目:
git init
此时去.git/objects目录中检查一下,发现只有两个子目录,info和pack,没有任何文件,此时数据库是空的。
用下面命令,可以手动将一个字符串作为value存储到git的数据库(objects,git的数据库,用了比较简单的方式去存储)中去,会看到返回了一个hash值来引用那个字符串value:
echo 'hello world' | git hash-object -w --stdin
解释一下上面的命令:
git hash-object是git的一个底层命令,一般我们是不会直接用的,是git add,git commit之类的命令在调用这种底层命令。
git hash-object命令,默认的表现是接受一个value,然后针对这个value返回一个hash值
默认是不将value写入自己的数据库的,但是使用了-w之后,git就会将value写入数据库之中,同时用hash值作为key来引用那个value对应的文件。
同时--stdin指的是从命令行接收value,也就是echo ‘test content’中的test content。
如果不使用--stdin,那么要求是在hash-object之后跟一个文件名,它就会将一个文件作为value存储到数据库中去,然后返回一个hash值。
git返回的是SHA-1 hash值,是40位的字符串,是根据文件内容计算出来的一个checksum,校验和。
- 2)git的文件存储机制(轻量级索引机制)
你往git里存储一个文件,git一定会根据文件的内容计算一个40位的hash值出来,作为那个文件的名称,也是指针,40位hash值就可以来引用这个文件。存储的时候,40位hash的头2位,会作为目录名称放在objects里面,然后那个存储的文件会放在那个目录中,文件名是40位hash值的后38位。
git作为一个高性能的版本控制系统,一大特点就是性能超高,在切换分支的时候,都需要进行大量的磁盘读写,磁盘存储机制是比较值得我们注意的
将40位hash值的头2位作为目录,其实是一种轻量级的索引机制
这样的话,有一个好处,就是每次你要根据一个hash值定位一个数据的时候,直接根据头2位先定位到一个目录,然后再在这个目录下去查找,linus写git的时候,就是用了一种非常轻量级的文件索引机制
直接查看git存储的文件的内容是不ok的,要用git专用的底层命令来查看
- 3)git cat-file体验
使用下面的命令可以查看刚存储进去的文件值,git cat-file是另外一个底层命令,就是专门用来查看存储到git中的文件的内容的:
git cat-file -p d670460b4b4aece5915caf5c68d12f560a9fe3e4
- 4)一个文件的多个版本存储机制的体验
下面的命令可以将一个文件存储到git数据库中:
echo 'version 1' > test.txt
git hash-object -w test.txt
echo 'version 2' > test.txt
git hash-object -w test.txt
git会将一个文件的多个不同版本,以完整快照的方式存储在objects数据库中,每个版本都用一个单独的文件来存储,每个版本的文件都有一个不同的hash值。
使用下面的命令可以查看.git/objects中所有的文件:find .git/objects -type f
接着可以试着将这个文件给删除,然后从git数据库中再恢复回来这个文件:
rm -rf test.txt
git cat-file -p 83baae61804e65cc73a7201a7252750c76066a30 > test.txt
cat test.txt
也就是说,我们只要将数据存储到了git中,那么就不用担心丢失了,随时都可以找回来这个文件的任意一个版本
我们用下面的命令可以查看一下git中存储的文件的类型,默认都是blob:
git cat-file -t 294d0170bac2cc1a111770c581f3b990b8032c71
这里,我们就发现跟之前给大家讲解的那个原理就可以串起来了,因为实际的文件的内容,每个文件的版本快照,会作为一个blob object存储在git数据库中
git数据库,git仓库,database,repository
- 5)梳理一下
实际上,就是一个文件的不同的版本,都会用一个完整的快照的形式,作为一个单独的文件存储在git数据库中
所谓的完整快照,每个版本,都是用一个文件存储这个版本的完整的内容的
每个版本的文件内容,都是通过blob object的类型存储在git数据库中的
(二)tree object
git有另外一种object叫做tree object
每次将一堆文件的一个版本存储到git仓库中,都是用blob object来存储那些文件的版本
然后用一个tree object来引用多个blob object。
tree object通常会包含指向其他多个blob的指针,或者是其他tree object的指针。
用下面的命令,可以查看master分支指针指向的那个tree object:
git cat-file -p master^{tree}
git通常是基于暂存区中的内容来创建一个tree object的,我们首先需要在暂存区中加入一些内容,然后可以试着手动创建一个tree object。
可以将仓库中存储的某个commit object的内容放入暂存区中:
git update-index --add --cacheinfo 100644 83baae61804e65cc73a7201a7252750c76066a30 test.txt
git update-index:底层命令,一般是git add命令去调用的,将数据库中存储的某个blob数据的引用放到暂存区中
--add:因为暂存区中还没有这个文件,你可以认为这个文件是第一次进入暂存区,所以需要这个选项
--cacheinfo:不是从工作区中加入文件放到暂存区,是从git仓库(git数据库)中加入文件
100644 mode:这种模式的意思是,这个是一个普通的文件
接着使用下面的命令基于暂存区中的内容,创建一个tree object:
git write-tree:将暂存区中的内容创建为一个tree object,tree object也是放入了git仓库,主要是包含了对一个blob obejct的引用
git cat-file -p d8329fc1cc938780ffdd9f94e0d364e0ea74f579
git cat-file -t d8329fc1cc938780ffdd9f94e0d364e0ea74f579
下面我们再加入一个new.txt文件放入暂存区中,同时将test.txt文件的另外一个版本放入暂存区中,再次创建一个tree object:
echo 'new file' > new.txt
git update-index --add new.txt,这个就是将工作区里的内容直接放入暂存区
git update-index --add --cacheinfo 100644 1f7a7a472abf3dd9643fd615f6da379c4acb3e3a test.txt
git write-tree
git cat-file -p 0155eb4229851634a0f03eb265b69f5a2d56f341
此时会发现这个tree object对应着两个文件
下面的命令可以从仓库中读取一个tree object放入暂存区,然后再基于这个tree object创建一个新的tree object出来:
所谓的将仓库里的东西读出来放到暂存区,实际上只是将一些引用放到暂存区,但是blob,tree,实际还是存储在objects中的
git read-tree --prefix=bak d8329fc1cc938780ffdd9f94e0d364e0ea74f579
git write-tree
git cat-file -p 3c4e9cd789d88d8d89c1073707c3585e41b0e614
此时tree object包含两个文件和一个tree object,如果将这个tree object的数据恢复到工作区中,就会有两个文件和一个叫做bak的文件夹,bak文件夹中也包含了一个文件。
(三)commit object
随着你不断的在工作区中编写代码,每次如果执行了一些相关的操作,可以将当前这个时刻,有变化的文件的版本都存储到git仓库中,同时放入暂存区中。
此时,暂存区中,没有变化的文件,还是保持着之前的版本;有变化的文件,暂存区中会存放最新版本的blob对应的引用
然后,如果此时创建一个tree object,就是基于暂存区中当前所有的文件的版本的blob,创建一个tree
此时,这个tree object就代表了这个项目的所有代码的此时此刻的一个完整的快照
现在我们实际上是有3个tree object,分别代表了项目的不同时刻的快照。但是此时此刻,我们有个文件,就是不知道那些tree object是谁添加的,为什么添加,什么时候添加的,什么都不知道。
但是,光有tree object是不够的
此时就需要commit object了。
用下面的命令,基于已有的tree object创建一个commit object,同时给这个commit object一个提交备注:
echo 'first commit' | git commit-tree d8329f
然后查看一下这个commit object,会发现包含了一个tree object,同时又作者,提交日期,以及提交备注:
git cat-file -p fdf4fc3
commit object的含义很简单,就是指向了一个tree object,而那个tree object指向了多个blob,其实这个tree object就代表了某一时刻项目中所有代码文件的快照版本,同时那个commit object包含了作者、提交时间、提交备注,此外还包含了指向上一次commit object的指针。
同样的,我们可以为另外两颗tree object分别创建一个commit object,代表了另外两次提交:
echo 'second commit' | git commit-tree 0155eb -p fdf4fc3
echo 'third commit' | git commit-tree 3c4e9c -p cac0cab
此时相当于我们就有了三次跟真实一样的提交了,可以用git log来查看一下提交历史:
git log --stat
就会看到最近的3次提交
其实上面的实验步骤就彻底揭示了git的底层原理了git add和git commit命令执行时使用的就是上面那些低级别的命令:
git add:相当于是将你的有修改的文件作为一个新的版本,采用单独的blob文件存储到仓库中去,同时将仓库中的文件更新到暂存区中
git commit:相当于就是将暂存区中的文件快照,也就是对应的blob指针创建一个tree object,写入仓库中,同时再创建一个commit object包含了提交人/提交时间/提交备注,来指向那个tree object,代表了一次提交对应的仓库快照
blob、tree和commit,三种object都是作为一个独立的文件在objects目录中存储的,存储时的子目录命名和文件名,都是基于hash值来的
最后用一张图来说明,到目前为止,整个commit object、tree object和blob的关系。
(四)SHA-1 hash值如何计算
首先blob的内容分为两块,一个是header,一个是content。header就是类型+空格+长度+\u0000。
然后会用header+content来计算出一个SHA-1校验和,作为hash值。
(五)梳理一下
git update-index --add 文件名:其实就是,我们在工作区里修改了文件以后,
会用这个底层命令,将工作区中有修改的文件的一个版本,放入git仓库中作为一个blob去存储这个版本,同时将这个blob的引用放入暂存区中
多次有git update-index之后,git仓库中会存储项目中每个文件的所有版本,同时在暂存区中保留了每个文件的最新版本,没有变化的就保留值钱的版本
执行git write-tree,就可以针对暂存区中当前项目的最新版本,创建一个tree object,引用项目中所有的文件当前此时此刻的一个版本对应的blob,tree object,就代表了项目当前的一个完整快照版本
接着执行git commit-tree,就可以基于这个tree object创建一个commit object,包含了tree object的创建人,创建时间,备注信息,等等,作为一次提交
(六)将上层命令和底层命令结合起来
1)我们首先在工作里各种新增、修改文件
2)我们执行git add --all .这个命令:此时会将工作区中有变化的文件,作为一个新的版本,直接存储到git仓库中去,作为一个blob来存储;同时在暂存区中加入这些最新版本的blob的引用,替换同一个文件之前在暂存区中的版本
git update-index
3)我们执行git commit,同时给出一个提交备注:此时会针对暂存区中现在的所有的版本,创建一个tree object,作为项目此时此刻的一个快照版本,放入仓库;另外再创建一个commit object,包含了提交人,提交时间,提交备注,来引用这个tree object
4)到此为止,完成一次git add和git commit
5)整个git内幕原理,剖析的非常清楚了
实践一下
1)编写两个文件,test1.txt和test2.txt
2)git add --all .
将两个文件的第一个版本,作为两个blob保存到了Git仓库中,同时也放入了暂存区中
3)git commit -m "第一次提交"
基于暂存区里的两个文件的第一个版本,创建了一个tree
然后基于提交信息创建了一个commit,引用了那个tree,此时这个commit就代表当前这个项目此时此刻的一个完整的版本
4)修改test1.txt位第二个版本
5)git add --all .
将test1.txt的第二个版本作为blob存储到git仓库中
同时将test1.txt的第二个版本覆盖了暂存区中的test1.txt的第一个版本
6)git commit -m "第二次提交"
创建了一个tree,引用了test1.txt的第二个版本,test2.txt的第一个版本
另外创建了一个commit,引用了那个tree,代表了此时此刻项目的一个完整的版本
7)每个commit,就是当前项目的一个完整的快照版本,包含了项目的所有文件的一个最新版本
8)为什么大公司里,规范项目里,对git commit都有要求的,不要随便瞎提交,都是完成一个完整的功能或者模块之后再提交一次,尤其是每天提交一次,或者每天提交多次,每次代表了一个完整的功能和模块
9)整个你的项目的提交历史,就是项目版本的变迁的一个完整的历史,每个commit都代表引入了一个新的功能或者是模块
指针的内幕原理
git里面有很多种指针,tree/commit里面本来就含有指针,指向其他的blob或者是tree,暂存区里面也是指针
分支、HEAD、tag、remote
.git/refs这个目录里面的内容
(一)分支指针
如果我们要查看某个commit,那么还是要知道它的hash值,然后用git log hash_value来查看这个commit。
但是这还是很麻烦,所以最好是可以有个文件存储那个hash值,然后我们直接用一个较为简单的名称来引用。
.git/refs目录中,有几个子目录,包括了heads和tags两个子目录,在这里就有各种指向objects中的指针。
.git/refs/heads目录下,有一个master文件,实际上是git自动创建一个初始的分支
分支其实就是一个指针而已,指向了某个commit object的
在master文件中就包含了一个40位的hash值,当前就是指向了最近一次commit object
.git/refs/heads目录下,会包含很多个文件,就是每次创建一个分支,就会在这个里面对应新建一个文件,文件名称就是分支的名称,
文件里的内容就是分支指针当前指向的那个commit object的hash值,代表着这个分支当前指向了哪个commit object
git里面的分支的内幕原理,大家就彻底清除了,新建一个分支是很轻量级的一个事情,不是像其他的版本控制系统那样,新建一个分支,是将所有的文件内容和版本都拷贝一份
其实在git中,提交历史,blob+tree+commit,是最核心的,所有的数据都是通过这个方式来存储一份
然后呢?新建一个分支,实际上就是在.git/refs/heads下面新建对应的目录和文件,在文件里面维护一个指针,一个hash值,指向了.git/objects中某个commit object的hash值
通常不建议直接自己手动修改refs文件的内容,可以用下面的命令更新某个refs文件指针指向的commit object:
git update-ref refs/heads/feature/001 1a410efbd13591db07496601ebc7a059dd55cfe9
其实上面就已经揭示出来了,这就是git中的分支机制的根本原理,分支就是指针而已,分支指针指向了不同的commit object。用下面的命令可以再创建一个test分支,让其指向某个commit object:
git update-ref refs/heads/test cac0ca
实际我们如果执行git branch命令,就是执行update-ref命令,创建那个分支对应的文件,然后文件内容就是对应的commit object的SHA-1 hash值。
删除一个分支很简单的,就是删除.git/refs/heads下面的分支对应的目录和文件即可
(二)HEAD指针
HEAD文件中,保存了对某个分支指针的引用,其实就是某个分支对应的文件。
HEAD其实也是一个文件,但是也是一个指针,这个HEAD呢,就是指向了当前你所处的那个分支而已,保存的是refs/heads下面的分支对应的目录和文件名
比如当前你在master分支,那么HEAD文件中的内容就是refs/heads/master,代表了HEAD当前指向了master分支
再比如当前你在feature/001分支,那么在HEAD文件中,内容就是refs/heads/feature/001,代表了HEAD当前指向了feature/001分支
因此如果我们比如在master分支上,创建并且切换到了另外一个分支,那么此时会根据HEAD找到当前分支文件,再找到当前分支指向的commit object,那么新建的分支就会同样指向那个commit object。
从比如master分支切换到feature/001分支,实际上就是一个非常轻量级的操作
(1)HEAD文件里的内容,从refs/heads/master,变为refs/heads/feature/001,代表着HEAD指针指向了feature/001分支
(2)将feature/001分支指向的那个commit对应的tree,对应的那些blob对应的文件版本的内容,一次性从仓库里恢复到工作区中,工作区中所有文件的版本和内容,会变成那个分支指向的commit当时的那个快照版本
每次切换分支之后,HEAD都会指向那个分支的文件
可以通过git checkout命令+cat .git/HEAD命令结合起来,然后就可以看到每次切换后的HEAD值
每次我们执行一次git commit操作,都会新建一个commit object出来,然后会让当前分支指向最新的commit object。
通过下面的命令可以读取和修改HEAD文件的指针:
git symbolic-ref HEAD
git symbolic-ref HEAD refs/heads/test
到这里为止,.git/refs下面最重要的东西就讲解完了,分支和HEAD
分支就是一个文件,保存了指向的commit的40位hash置
HEAD也是一个文件,保存了指向的那个分支对应的文件名称
(三)tag object
其实除了blob、tree和commit三种object之外,还有一种object就是tag object。tag object是指向某个commit object的,包含了标签时间,标签名称,等等。而且tag object永远就指向创建时指定的那个commit object,不能修改。
我们可以通过下面的命令创建一个轻量级的tag object
就只是一个指针而已,在refs/tags下面,会有一个tag名称对应的文件,然后里面就是那个tag指向的commit的40位hash值
git tag v1.0:轻量级的tag,直接就指向了当前分支指向的那个commit object,就是在refs/tags下面搞一个问加你,作为一个指针而已
git update-ref refs/tags/v1.0 cac0cab538b970a37ea1e769cbbde608743bc96d
当然也可以创建一个带备注的标签:git tag -a v1.1 -m 'test tag'
此时查看cat .git/refs/tags/v1.1,会发现返回的不是我们指定的那个commit object,是另外一个新创建的tag object的SHA-1 hash
git cat-file -p 9585191f37f7b0fb9444f35a9bf50de191beadc2
再次查看那个tag object的hash值,此时可以显示出来其指向的那个commit object,同时包含了tag的创建时间、创建人、备注,等等信息。
(四)remote
最后一种引用类型,就是remote。
如果我们添加了一个remote,同时push了一些东西到那个remote,git会在refs/remotes中保留对每个分支,最近一次push到远程服务器对应的commit。
比如下面的命令:
git remote add origin git@github.com:schacon/simplegit-progit.git
git push origin master
cat .git/refs/remotes/origin/master
查看一下上面的文件,会看到一个SHA-1 hash值,就是这个分支最近一次push到服务器的object commit的SHA-1 hash值。
FETCH_HEAD,就是最近一次fetch下来的每个分支对应的commit hash值,就在这个里面
(五)梳理一下
分支:.git/refs/heads
HEAD:.git/HEAD
tag:.git/refs/tags
remote:.git/refs/remotes
FETCH_EHAD:.git/FETCH_HEAD
文件合并的内幕原理
哪怕是很大的文件,比如说20kb的一个代码文件,每次改动一点代码,git也是存储改动后的版本对应的一个完整的文件;
如果这个20kb的代码文件修改了100次,会怎么样?存储100个版本对应的文件,也就是100个文件,2000kb ~ 2MB。所以这样的话对存储的压力还是蛮大的。。。。
git实际上是可以更加智能的处理这种文件改动的。默认情况下,git会使用loose格式存储文件(loose,松散格式,采取这个文件的每个版本,就存储一个完整的单独的文件),但是一段时间过后,git会将多个loose格式的松散文件打包到一个packfile二进制文件中,来节省磁盘空间。
什么时候git会进行packfile打包的操作呢?
两个时机:
我们手动执行git gc操作,
或者是将文件push到远程服务器的时候,
git会看一下loose格式文件是不是太多了,如果是,则进行打包合并。
git gc:garbage collector,jvm gc是不一样的,git用来处理自己的文件的
如果我们将本地的commit数据push到了远程服务器后,就代表远程服务器包含了我们的数据了,此时git就可能会执行一次packfile打包
此时我们可以手动执行一下git gc命令,来看看效果
此时再次用下面的命令看一下objects目录下的内容:find .git/objects -type f,之前应该是一堆hash值组成的目录和文件,包含完了十几个object(blob,tree,commit),会发现出现了objects/info/packs和objects/pack等新的目录。
我们是将之前所有的hash值组成的文件打包成了一个packfile文件,进行了压缩以及合并
pack包含了两个文件,一个是packfile包含了打包的所有数据,一个是index文件,包含了每个blob object在packfile中的offset,通过.idx文件标识出每个object在.pack文件里是哪一个部分
git做文件存储的优化,两块:
多个文件打包成给一个,压缩,二进制格式,packfile,节省空间;
在打packfile的时候,就会对我们刚才说的那种情况,大ruby文件,仅仅保留最后一个版本对应的完整内容,然后之后的版本都是直接保留的是它的那些delta增量内容,后面的版本就不是全部保留全量内容了,进一步节约空间
git,git gc;git push,自动会做打packfile
实际上,在git进行打包packfile的时候,就会对一个文件的多个版本,仅仅保留其每次增量修改数据,避免对一个文件保留多个全量的版本快照。
用下面的命令看一下:
git verify-pack -v .git/objects/pack/pack-978e03944f5c581011e6998cd0e9e30000905586.idx
会发现ruby大文件,第一个版本仅仅包含9kb的内容,但是第二个版本包含了22kb的内容,所以这里就做了增量处理和优化,仅仅对一个大文件的最新的版本保留了全量的内容,但是对之前的版本都是增量内容
总结一下
(1)git gc / git push,会执行packfile的压缩优化
(2)将.git/objects下的各种object对应的文件,压缩成一个packfile
(3)每个packfile都对应一个.pack文件和一个.idx文件,.pack文件包含了多个object的内容,.idx文件包含了每个object在那个.pack里面的offset偏移量
(4)在打packfile的意义:多个文件合并一个,节省空间:对大文件进行增量处理,仅仅是最新的版本保留全量内容,之前的版本保留增量内容
(5)用git verify-pack -v命令,可以查看.pack文件里包含的具体内容
远程分支的内幕原理
(一)、origin/master
本地追踪分支,跟远程的master完全是需要对应起来的,远程的master指向哪个commit,每次git fetch origin,就会将远程仓库的提交历史拉取到本地跟本地的提交历史进行合并,同时将本地的origin/master指向远程仓库的master指向的那个commit
本地分支(origin/*),追踪了远程的分支(*),每次git fetch之后,提交历史合并之后,本地分支(origin/*)指向的commit都会跟远程分支保持一致
不只是origin/master,origin/feature/001,origin/feature/002,origin/release/v1.0
(二)、master,跟origin/master关联起来的
- 本地的master,是跟本地的origin/master关联起来的,作用是在git pull和git push
(1)如果执行git pull,会执行远程和本地的提交历史进行合并,origin/master会指向远程仓库master指向的那个commit,同时将origin/master指向的commit跟本地的master指向的commit进行合并,合并之后会出现一个新的commit,同时master会指向那个合并后的commit,然后那个origin/master不变还是指向原来的那个commit
git pull,相当于是两个命令:git fetch origin + git merge origin/master
相当于就是将远程仓库的提交历史拉取下来,然后合并在一起,本地的origin/master指向最新的commit,接着将本地的master与origin/master进行合并
1)将远程仓库的提交历史拉取到本地进行合并,一个commit可能会出来多个分叉
2)同时将origin/master跟远程仓库的master指向一个commit
3)将本地的master和origin/master进行合并,可能需要解决冲突
(2)如果执行git push,会将本地的origin/master指向master指向的那个指针,同时会将本地提交历史推送到远程进行合并,然后远程仓库的master分支会指向最新的那个commit
每次执行git remote add命令之后,实际上就会在.git/config文件中加入一段配置,类似下面:
[remote "origin"]
url = git@192.168.31.80:OA/test-project.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "feature/001"]
remote = origin
merge = refs/heads/feature/001
[branch "master"]
remote = origin
merge = refs/heads/master
我们之前跟大家说过,如果你在本地想搞一个跟类似origin/feature/001关联起来的本地分支,让git push和git pull有效果,git checkout -b feature/001 origin/feature/001,这段命令执行之后,就会在.git/config中加入[branch]配置,标注了本地分支跟origin/*追踪分支之间的关联关系
那么什么是refspce呢?实际上就是fetch后面的那串东西,+refs/heads/*:refs/remotes/origin/*,就是refspce
refs/heads/*,代表的是远程仓库被追踪的分支,远程仓库的master
refs/remotes/origin/*,代表的是本地仓库用来追踪远程仓库的分支,本地仓库的origin/master分支
+,代表的是在执行git pull的时候,不是要将远程分支和本地分支进行merge么?即使不是fast-forward的类型,是3-way merge也是要去执行的
在执行git remote add命令的时候,本地的git客户端会执行fetch命令获取远程仓库的refs/heads/*下面的分支,然后将其写入本地的refs/remotes/origin/*下面去。所以之前说的那些本地追踪远程的分支,比如origin/master之类的,实际上指的就是refs/remotes/origin/master代表的分支
origin/master等等追踪分支,在本地是放在哪儿的呢?
我们是可以手动指定多个refspce的
根本就不用的下面的语法,介绍一下,refspec指定多个,一般不用
[remote "origin"]
url = https://github.com/schacon/simplegit-progit
fetch = +refs/heads/master:refs/remotes/origin/master
fetch = +refs/heads/qa/*:refs/remotes/origin/qa/*
[remote "origin"]
url = https://github.com/schacon/simplegit-progit
fetch = +refs/heads/*:refs/remotes/origin/*
push = refs/heads/master:refs/heads/qa/master