Git HTTP Server的实现流程及php实现

Posted 代码与远方

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Git HTTP Server的实现流程及php实现相关的知识,希望对你有一定的参考价值。


文/ 邓小龙博客


感谢邓小龙博客授权转载


团队内部的版本工具,从svn换到git之后,先用了一小段时间基于ssh的git服务以后,果断换到了高大上的gitlab。后期,随着git项目的不断扩大,到gitlab的不堪重负,以及升级的各种阵痛。再到后来团队对于代码规划化和文档规范的需求,我们基于php实现了一套完全自主的Git HTTP Server。

Git HTTP Server 第一版

早期的Git HTTP Server,由于对nginx的坚持,以及git-http-backend对fastcgi的不支持,我们在中间加了facgiwrap作为粘合剂,结合nginx的basic 验证,使得整套系统能正常运转起来。这个时候,git项目只有我一个人参与。

大概架构为:


Nginx => 通过网址规则捕获git原生请求 => basic校验 => fcgiwrap => git-http-backend


这样做下来,已经能满足我们的基本需求了。项目组加入了新成员,我们对git的对比、review、合并、审查等流程都多了大量的实现,使得代码管理这一块比以前更加专业和可靠了。

Git HTTP Server 第二版

不过,这套体系对于权限控制的这块太过粗细条了。每个项目需要有一个单独的basic授权文件,记录拥有团队项目权限的帐号和密码,如果有人离开团队,需要将其从这个文件删除,如果修改密码,也需要在有此用户的帐号里的git项目里挨个更新对应的授权文件。所以,我们又开始谋划更加高级的授权2.0了。

这次,我们采用了lua和nginx结合来做项目的授权。为了简化对于lua的使用,业务流程还是放在php里边,通过lua调用php,根据返回状态码决定是否能访问该项目或者像该项目提交内容。这样改动以后,用户的授权不再基于nginx的basic验证了,而直接通过php页面来判断即可。

这个版本的架构为:


Nginx => 通过网址规则捕获git原生请求 => Lua + php鉴权页面 => fcgiwrap => git-http-backend



在此之后,用户鉴权的功能更加自由。团队也注入了新鲜的血液,我们又加入了git库用户组的概念,这样,新来一个员工,不需要给他挨个分配git权限了,只需要将其分配到一个用户组即可。

Git HTTP Server 第三版

但是,这样的结构就ok了吗?就靠谱了吗?随着又一次git项目的迁移,对此的疑问达到了一个顶峰。因为这个git http server依赖的中间环节太多了,迁移和搭建的成本还是蛮大的,哪个环节配置出问题,都可能导致系统不能正常工作起来。所以,我开始寻找新的突破口,在看了git-http-backend源码,结合gogit、以及《自己动手写 Git HTTP Server》等文章的消化,最终开始下手着手第3版Git HTTP Server了,它必须基于php语言,必须足够简单可靠,必须能替换掉lua、fcgiwrap、git-http-backend。

来看一下我理解的Git HTTP Server的工作流程:


原图:https://www.processon.com/view/link/594946bee4b0e1bb14fdeb21

剩下来的,也不多说了,看一下核心的php代码:

  1. <?php

  2. /**

  3. * Git HTTP Server实现

  4. * @author: dengxiaolong

  5. * @since: 2017/6/21 22:19

  6. * @copyright: 2017@dengxiaolong.com

  7. * @filesource: index.php

  8. */

  9. // 记录日志

  10. function writeLog($obj)

  11. {

  12.    if (is_scalar($obj)) {

  13.        $msg = $obj;

  14.    } else {

  15.        $msg = var_export($obj);

  16.    }

  17.    file_put_contents(__DIR__ . '/tmp', $msg . PHP_EOL, FILE_APPEND);

  18. }

  19. // gzip解压内容

  20. function gzBody($gzData)

  21. {

  22.    // 根据header头判断是否需要对内容进行gzip解压缩

  23.    $encoding = $_SERVER['HTTP_CONTENT_ENCODING'];

  24.    $gzip = ($encoding == 'gzip' || $encoding == 'x-gzip');

  25.    if (!$gzip) {

  26.        return $gzData;

  27.    }

  28.    $i = 10;

  29.    $flg = ord(substr($gzData, 3, 1));

  30.    if ($flg > 0) {

  31.        if ($flg & 4) {

  32.            list($xlen) = unpack('v', substr($gzData, $i, 2));

  33.            $i = $i + 2 + $xlen;

  34.        }

  35.        if ($flg & 8) $i = strpos($gzData, "\0", $i) + 1;

  36.        if ($flg & 16) $i = strpos($gzData, "\0", $i) + 1;

  37.        if ($flg & 2) $i = $i + 2;

  38.    }

  39.    return gzinflate(substr($gzData, $i, -8));

  40. }

  41. // 定义git库根目录

  42. define('GIT_DIR', __DIR__ . '/repos/');

  43. // 根据网址解析出git库对应的目录

  44. $uri = $_SERVER['REQUEST_URI'];

  45. $info = parse_url($uri);

  46. $path = $info['path'];

  47. $arr = explode('/', trim($path, '/'));

  48. $git['group'] = array_shift($arr);

  49. $git['name'] = rtrim(array_shift($arr), '.git');

  50. $path = sprintf('%s/%s/%s.git', GIT_DIR, $git['group'], $git['name']);

  51. $action = implode('/', $arr);

  52. switch ($action) {

  53.    case 'info/refs':

  54.        $service = $_GET['service'];

  55.        header('Content-type: application/x-' . $service . '-advertisement');

  56.        $cmd = sprintf('git %s --stateless-rpc --advertise-refs %s', substr($service, 4), $path);

  57.        writeLog('cmd:' . $cmd);

  58.        exec($cmd, $outputs);

  59.        $serverAdvert = sprintf('# service=%s', $service);

  60.        $length = strlen($serverAdvert) + 4;

  61.        echo sprintf('%04x%s0000', $length, $serverAdvert);

  62.        echo implode(PHP_EOL, $outputs);

  63.        unset($outputs);

  64.        break;

  65.    case 'git-receive-pack':

  66.    case 'git-upload-pack':

  67.        $input = file_get_contents('php://input');

  68.        // 需要指定返回内容的Content-type

  69.        header(sprintf('Content-type: application/x-%s-result', $action));

  70.        $input = gzBody($input);

  71.        // writeLog("input:".$input);

  72.        $cmd = sprintf('git %s --stateless-rpc %s', substr($action, 4), $path);

  73.        $descs = [

  74.            0 => ['pipe', 'r'],

  75.            1 => ['pipe', 'w'],

  76.            2 => ['pipe', 'w'],

  77.        ];

  78.        writeLog('cmd:' . $cmd);

  79.        $process = proc_open($cmd, $descs, $pipes);

  80.        if (is_resource($process)) {

  81.            fwrite($pipes[0], $input);

  82.            fclose($pipes[0]);

  83.            while (!feof($pipes[1])) {

  84.                $data = fread($pipes[1], 4096);

  85.                echo $data;

  86.            }

  87.            fclose($pipes[1]);

  88.            fclose($pipes[2]);

  89.            $return_value = proc_close($process);

  90.        }

  91.        // 如果上传git内容,需要更新服务器端的/info/refs文件

  92.        if ($action == 'git-receive-pack') {

  93.            $cmd = sprintf('git --git-dir %s update-server-info', $path);

  94.            writeLog('cmd:' . $cmd);

  95.            exec($cmd);

  96.        }

  97.        break;

  98. }        

这段代码看似平常,实则是和同事经历了很多波折才予以实现的,其中最麻烦的地方在于在git库分支特别多的情况下,post上来的分支会通过gzip先压缩一下。但是一开始,我们对于这个压缩的事情完全没有概念,探索了很多也无从解决。即使在知道有可能是gzip压缩的情况下,通过简单的解压缩函数也是无法还原内容的。最后,带着疑问,终于在github上找到了一段代码,完成解压缩数据的目的。

要简单地尝试上述代码的效果,可以直接运行代码即可:


php -S 0.0.0.0:10000 index.php



并在当前目录建立子目录repos,在里边按二级存放git库,即可通过http协议对其进行fetch和push等git远程命令。

经历过这番变革后,我们终于让架构简化得差不多了:


Nginx => 通过网址规则捕获git原生请求 => php => git命令


这三次进化,得益于团队规模的扩大,以及我们对于git理解的日益精深,但是,要有坚信自己的力量,不断追求完美,才能使得解决方案更加简单和完美。


以上是关于Git HTTP Server的实现流程及php实现的主要内容,如果未能解决你的问题,请参考以下文章

#私藏项目实操分享#Alibaba中间件技术系列「RocketMQ技术专题」Broker配置介绍及发送流程异常(XX Busy)问题分析

centos6下基于http2.2搭建论坛博客系统及phpMyAdmin应用程序的实现

Apache Http-Server的安装及配置

lanczos算法及C++实现实对称三对角阵特征值分解的分治算法

SVN及Git版本控制系列

MONAI Label 安装流程及使用攻略