基于git的自动集成php脚本


利用git进行版本管理我现在在做的东西,也不是一天两天了….

大概隔3-5天都会release一个版本放到正式线上去,但是同步代码的时候出现了一些困扰…..

正式线上的环境和本地的debug环境还有些不同,要发布的话需要编译一些文件,整理一些配置,添加ga代码等等操作,并不能直接patch到线上

之前一直都是靠人工的去看( 泥垢了! ),但是在项目代码规模日益庞大,开发进展迅速的现在有点撑不住了….= =

每天的提交修改量都在30Commits..(虽然有效的只有10+…..),但是去记住并验证修改了哪些文件已经不太方便了…

至少靠我一个人有点力不从心了…

最近一周花了点业余时间总算写出来了一个勉强能过去的ci脚本,放在这里供大家扔扔西瓜皮啊香蕉皮啥的…嗯(尼玛你这都算ci?别开玩笑了!噗~)

问题描述

首先得说明下,我确实没有玩过专业的ci工具,之前尝试gitlab-ci还安装失败了…..(太弱了!)

其实git本身提供了很好的diff -p的工具,能生成全世界通用的patch补丁,但是这也有一些问题.

  • 首先是直接生成的补丁只能是一个或者多个,一个的话有些debug代码很难在线上直接patch,多个文件的话(一次release有时有上百个改动的文件)查找和管理又比较麻烦(貌似没有保证目录结构的patch?( 求指教! )
  • 接着是线上线下环境有比较大的不同,对于我们的系统来说,需要做很多环境的改变,比如关闭调试模式,不显示运行时间,更新数据库链接地址和用户名密码等等….
    • 就算有很好的逻辑结构化的patch文件也很难直接更新到线上..( 吧?)
  • patch文件只记录了改动的行数以及附近前后的几行,要编译或者搜索这些文件的话完全没有办法……..

基本上面的就是我现在的东西没法采用git自带diff -p的原因了.当然有可能只是针对我们现在的搓A项目来说确实不行…(如果我对项目或者git的理解有问题请务必交流!)

解决方案

既然git本身做不到,那么我们自己写个小小的脚本来解决问题就行了.(确实够 的..擦)

基于上面描述的问题,设计了下面这样的一套ci流程:

流程图

好吧.随便找了个在线画流程图的来了个惨不忍睹的示意图…

不过想要表达的都应该能说明了…..

说明

  • 为了避免直接patch文件的不好操作性,采用了新的命令流程来一个个的处理文件
  • 删除的文件不会进行实际操作,但是会在终端输出删除的文件名
  • 新增和修改统一处理,在复制文件前调用hook,根据返回值来判断是否需要继续处理
  • 处理win和lin下面对unicode的问题
  • 所有文件处理结束之后的钩子
  • 为什么要使用tag命令?至少从我了解的角度来说:
    1. tag是最不破坏当前库的workingtree的方式
    2. tag不是强制提交的话不会污染整个公有库
    3. tag可以随便打,打错了删掉也很方便( git真好 )
    4. 就算是提交的话也很有规律,在gitlab啥的查看也很方便

源码开始!

好吧.刚才说了那么多= =coder还是用自己的方式说话比较好~

需求:

  • 软件         : 系统path里面能直接执行php与git.php拥有mkdir,chdir,copy函数调用权限
  • 版本         : 不限.测试环境:
    • php:5.4.9
    • git:1.8.0
  • 操作系统 : 任意(木有mac,所以没法测试,其他的系统测试没问题)

最近php写太多了…本来计划C++实现的…啥的…(你们什么都没看到!)

<?php
if( substr(php_sapi_name(), 0, 3) != 'cli' ){
    die('You Should Use CGI/CLI To Run This Program.');
}
//win下存在unicode编码问题
$is_win = strpos(strtolower(PHP_OS),'win') !== false;

//start
//现在写死的参数,之后改成支持传参的方式
//git执行路径
$git_exec = 'git';
//版本库所在路径(相对路径)
//TODO 这里的处理,导致只能支持文件与项目目录平级,得想办法改成支持任何路径的方式
$project = 'abc-test';
//tag标签与消息的后缀
$version = date('Ymd');

//初始化需要的数据
//下面的tag名字和tag的msg让参数输入不太好,没法匹配了...写死的话也不太好,没法配置了...唉
$tag_msg = 'release of '.$version;
$tag_name = 'ci/release-'.$version;
//参见TODO
$current_path = getcwd();
$project_path = $current_path.'/'.$project;

//加载hook文件
//加载当前路径的hook.php文件,没有的话也无所谓
include('hook.php');
if(!function_exists('_before_copy_one')){
    function _before_copy_one (){ return true; }
}
if(!function_exists('_after_all_copyed')){
    function _after_all_copyed(){}
}
//切换php与git的工作目录
chdir ($project_path);

//检查当前的HEAD分支是否已有tag
exec ($git_exec.' log --oneline --decorate -n1 ',$out);
$has_tag = explode(' ',$out[0]);
if($has_tag[2] == 'tag:'){
    //tag名字如果和预设的相同可以继续操作
    $has_tag[3] = substr($has_tag[3],0,-1);
    if($has_tag[3] != $tag_name){
        die('You already have one tag on ref/HEAD named: '.$has_tag[3]);
    }
}else{
    //打上tag
    exec($git_exec.' tag -m "'.$tag_msg.'" '.$tag_name);
}
unset($out);

//获取上次的relasetag标签
//下面的这句git语句是精髓吧...以打tag的时间的倒序的方式显示符合筛选条件的tag的前两个
//能够无视干扰的项目本身的tag,而且和tag名字后缀的啥的字符顺序无关
exec($git_exec.' for-each-ref --sort="-*authordate" --format="%(tag)" --count=2 refs/tags/ci/release-*',$out);
//这里检查了tag的情况,针对上一步中的tag情况再次验证
if($out[0] != $tag_name){
    die('git tag ref/HEAD failed!');
}
$old_tag = $out[1];
unset($out);

//获取两次tag之间的全部文件变动
//diff的参数也是现学现卖的....最后的那个命令能巧妙的最简格式化输出,各位感兴趣的话可以在终端直接执行试试~
exec($git_exec.' --no-pager diff '.$old_tag.'..'.$tag_name.' --name-status',$out);
//判断只有一行或者输出失败的情况
if(count($out) < 2){
     die('no file changed');
}
//开始复制
//下面的代码比较凌乱,没太多参考价值了....just works
$release_path = $current_path.'/'.$project.'_'.$version;
mkdir($release_path);
chdir ($release_path);
$files_count = count($out);
$files_deal = 0;
foreach($out as $one_line){
     $action = substr($one_line,0,1);
     $file = trim(substr($one_line,2));
     if(!$file){
         continue;
     }

     //该死的操作系统问题
     if($is_win){
         $file = iconv('UTF-8','gbk',$file);
     }

     $src_path = $project_path.'/'.$file;
     $remote_path = $release_path.'/'.$file;
     //先默认增加一次成功
     $files_deal++;

     //hook before real deal one file
     if(!_before_copy_one($src_path,$action)){
         continue;
     }

     //判断操作类型
     switch($action){
         case 'A':
         case 'M':
             //复制文件
             $dir_path = dirname($remote_path);
             if(!file_exists($dir_path)){
                 @mkdir($dir_path,0755,true);
             }
             $one_copy = copy($src_path,$remote_path);
             if(!$one_copy){
                 //虽然有判断.但是实际使用的几次都没出现这个提示...
                 echo 'copy file failed: ',$file,chr(10);
                 $files_deal--;
             }
             break;
         case 'D':
             //删除文件的话给予提示就行
             echo 'delete one file: ',$file,chr(10);
             break;
     }
 }

//hook after all files have dealed
_after_all_copyed($files_deal,$files_count);

echo 'files dealed:',$files_deal,'/',$files_count,chr(10);

?>

两个hook只是为了对付我现在遇到的问题.没有什么通用性……

但是整个的流程就这么简单.增加hook啥的非常方便= =( 喂!,这破流程有和没有一样嘛! )

继续

如果看了源码之后对您有所启发的话那就再好不过了.

如果想更进一步的了解我是怎么使用这两个hook的话,下面可以接着着看一段使用的hook代码:

//file hook.php
<?php
function _before_copy_one($file_path,$type){
     if(strtolower(end(explode('.',$file_path))) == 'less'){
         global $has_less;
         if($has_less){
             //已经编译过了不再需要copy less文件
             return false;
         }else{
             echo 'find less file add/modify, build css file.';
             //编译less文件
             //下面的代码就是调用实际编译的php程序了.关于这个在我之前的blog有详细的说明~
             //也是和git集成了的哦~
             chdir('lessphp');

             //只能那个通过ob的方式才能完整的获取raw的gz数据
             ob_start();
             //gz之后的数据是bin的格式,直接用exec的话会出问题
             passthru('php build.php');
             $data=ob_get_contents();
             ob_end_clean();
             echo '.';

             //由于输出带有header信息,去掉再gz解码
             //这句是网上的神代码......超级好用
             $data = gzinflate(substr($data,10,-8));
             echo '.';
             if($data){
                 //处理data.比如file_put_content到一个css文件啥的= =
            }else{
                 echo 'failed: '.$data;
            }
            echo chr(10);
            $has_less = true;
            return false;
          }
     }
     return true;
}
function _after_all_copyed($dealed,$all){
    include('pclzip.lib.php');
    $zip = new PclZip("{$release_path}.zip");
    //就这么简单....
    $v_list = $zip->create($project_path);
    if($v_list == 0){
        echo 'zip error'.$zip->errorInfo(true);
    }else{
        echo 'zip dir done!';
    }
}
?>

说明:

这里需要注意的有两个地方吧:

  • 通过一个global变量来判断一些已经处理过的参数,比如这里的$has_less,因为项目的设计,less只需要编译一遍,所以只要第一次遇到less有修改的话就可以保存这个状态,之后再调用的时候直接返回false,即不需要处理 这也是我设计单个文件的处理前hook的时候设计了返回值的意义
  • 关于那个啥, php编译Less的程序 ,参见:( 全模式下的后台编译less
  • 如果获取外部程序的二进制输出,这里给了个例子.推荐…
  • 最后就是全部文件搞定之后的hook.直接打包成zip文件……一步到位

最后的自嘲

断断续续的花了5天时间来写这么一个破脚本代码…..最后的实用性还很低…唉.

不过还是那句话,重要的是启发大家思考解决问题.才能共同进步

如果有任何建议–意见–西瓜皮–口水啥的(喂)  热!烈!欢!迎!吐!槽!

原文地址: 基于git的自动集成php脚本 作者 : ZZJIN

转载请注明出处.

comments powered by Disqus