Как стать автором
Обновить

GitHub, вебсайт и автоматическое создание тестового сайта из последней версии исходных кодов

Время на прочтение11 мин
Количество просмотров3.6K
Речь в данной статье пойдет о том, как автоматически получать свежую версию исходников из основной ветки вашего репозитория и разворачивать из нее проект на виртуальном хостинге. Сразу хочу отметить, что с GitHub'ом и Git'ом я познакомился только вчера. Поэтому матерым веб–программистам эта статья может показаться тривиальной. А тем, кто еще только начинает свой путь веб–программиста, надеюсь, поможет.


Введение


У меня есть небольшой вебсайт на виртуальном хостинге. На нем нет полноценного шелл–доступа и скрипты ограничены в некоторых правах. К примеру, я не могу использовать функйию PHP system и функци file_get_contents. После того, как я создал репозиторий на GitHub'е, научился немного работать с изменениями и обновил исходный код, настало время задуматься о том, что же делать дальше. Мне хотелось посмотреть свои изменения в действии, но при этом так, чтобы основной сайт продолжал работать.

Из доступных мне скриптовых языков я знаю только PHP. Выбор на чем писать был сделан автоматически. Я понимал, что мой скрипт должен каким–то образом получать уведомления об обновлении с GitHub'а и скачивать исходный код. Я решил сделать субдомен development.mysite.com и выкладывать последнюю версию исходников именно туда. Кроме того, у меня есть файл конфигурации вебсайта с паролями для базы данных, который я не выложил в общий доступ на GitHub'е. Этот файл нужно добавлять к скаченным исходникам, чтобы все заработало.

Таким образом, весь процесс можно разбить на следующие этапы:
  • разобраться как получать уведомления от GitHub'а;
  • скачать исходные коды;
  • произвести необходимые изменения с ними.


Уведомления от GitHub'а


Здесь все совсем просто. GitHub поддерживает хуки. Прописываем в Post-Receive URLs адрес нашего скрипта и все. Он будет вызываться при каждом изменении основной ветки репозитория. Подробнее об этом на сайте GitHub'а (на англ. языке). При этом я в своем скрипте никак не обрабатываю информацию о последнем транзакции (комите).

Скачивание исходников


У разработчика есть два варианта для скачивания кода:
  • использовать GitHub API;
  • скачать заархивированную версию основной ветки.


GitHub API

С помощью программного интерфейсам можно получить информацию о последнем комите. В ней находится идентификатор Tree SHA. Этот идентификатор позволяет последовательно получить список и содержимое всех файлов проекта.

Пример использования GitHub API на PHP описан в блоге Дэвида Волша. Возьмем оттуда несколько полезных функций и добавим свои. Начнем писать наш скрипт. В первую очередь параметры
  1. <?php
  2. /* static settings */
  3. $user = '<github_username>';
  4. $repo = '<github_reponame>';
  5. $user_repo = $user . '/' . $repo;
  6. $tree_base_url = "http://github.com/api/v2/json/tree/show/" . $user_repo;
  7.  
  8. // path on the server where your repository will go
  9. $stage_dir = $_SERVER['DOCUMENT_ROOT'] . dirname($_SERVER['SCRIPT_NAME']);
  10. ?>


Копия исходного кода будет создаваться в директории, в которой находится наш скрипт. Дальше вставляем функцию для получения данных по адресу, подсмотренную у Дэвида:
  1. <?php
  2. /* gets url */
  3. function get_content_from_github($url)
  4. {
  5.     $ch = curl_init();
  6.     curl_setopt($ch, CURLOPT_URL, $url);
  7.     curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  8.     curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 1);
  9.     echo "Getting: {$url}";
  10.     $content = curl_exec($ch);
  11.     curl_close($ch);
  12.     return $content;
  13. }
  14. ?>


Затем идет функция, которая находит Tree SHA и начинает скачивать файлы:
  1. <?php
  2. function get_repo_json()
  3. {
  4.      global $user, $repo, $user_repo, $tree_base_url, $stage_dir;
  5.  
  6.      $json = array();
  7.      $list_commits_url = 'http://github.com/api/v2/json/commits/list/' . $user_repo . '/master';
  8.             
  9.      echo "Master branch url: {$list_commits_url}\n<br>";
  10.      $json['commit'] = json_decode(get_content_from_github($list_commits_url), true);
  11.     
  12.      // get sha for the latest tree
  13.      $tree_sha = $json['commit']['commits'][0]['tree'];
  14.      echo "Tree sha: {$tree_sha}\n<br>";
  15.      $cont_str = $tree_base_url . "/{$tree_sha}";
  16.      $base = json_decode(get_content_from_github($cont_str), true);
  17.  
  18.      // output project structure
  19.      echo "<pre>";
  20.      get_repo($base['tree'], 0, $stage_dir);
  21.      echo "</pre>";
  22. }
  23. ?>


Данная функция вызывает функцию get_repo, которая рекурсивно проходит по всем директориям проекта.
  1. <?php
  2. function get_repo($objects, $level = 0, $current_dir)
  3. {
  4.      global $tree_base_url, $user_repo;
  5.     
  6.      chdir($current_dir);
  7.     
  8.      foreach ($objects as &$object)
  9.      {
  10.          $type = $object['type'];
  11.          $sha = $object['sha'];
  12.          $name = $object['name'];
  13.         
  14.          // add padding
  15.          echo str_pad("", $level, "\t");
  16.          echo $name . "\n";
  17.  
  18.          if (strcmp($type, "tree") == 0)
  19.          {
  20.               mkdir($name);
  21.             
  22.               $new_dir = $current_dir . '/' . $name;
  23.             
  24.               $tree = $tree_base_url . '/' . $sha;
  25.               $new_objects = json_decode(get_content_from_github($tree), true);
  26.             
  27.               get_repo($new_objects['tree'], $level + 1, $new_dir);
  28.             
  29.               // change current directory back
  30.               chdir($current_dir);
  31.          }
  32.          else
  33.          {
  34.               // get file content
  35.               $blob_url = "http://github.com/api/v2/json/blob/show/" . $user_repo . "/" . $sha;
  36.               $data = get_content_from_github($blob_url);
  37.             
  38.               $filename = $current_dir . '/' . $name;
  39.               file_put_contents($filename, $data);
  40.          }
  41.      }
  42. }
  43. ?>


Стоит обратить внимание, что мы сразу получаем содержимое файла, без какой–либо дополнительной информации. Поэтому нам не нужно вызывать функцию json_decode как в случаях с вызовами других функций API.

Результаты получения исходников через API

У этого скрипта есть два существенных недостатка:
  • он работает достаточно медленно;
  • мне так и не удалось скачать весь проект целиком. CURL завершается по таймауту, весь процесс останавливается, не скачав и трети исходных файлов.

Кроме того, сама идея перекачивать проект по отдельным файлам представляется идеологически неправильной.

Архив с основной веткой проекта

Потыкав в кнопки GitHub'а, я обнаружил возможность скачать заархивированную версию исходных файлов. Такой подход гораздо лучше! Скачать можно на выбор либо Zip архив, либо Tar. Мой выбор пал на Zip, потому что его проще распаковать на виртуальном хостинге. Посмотрим же на скрипт.
  1. <?php
  2. $download = true;
  3. $unzip = true;
  4. $move = true;
  5.  
  6. $stage_dir = $_SERVER['DOCUMENT_ROOT'] . dirname($_SERVER['SCRIPT_NAME']);
  7. $filepath = $stage_dir . '/' . 'master.zip';
  8.  
  9. echo "<pre>";
  10. ?>


Переменные download, unzip и move управляют ходом программы и позволяют отключать её части. Они могут использоваться для отладки. Например, если архив уже скачан, но не распаковывается, то нет смысла скачивать его снова.
  1. <?php
  2. if ($download)
  3. {
  4.      $url = "http://github.com/<your_github_username>/<your_github_repo_name>/zipball/master";
  5.  
  6.      $ch = curl_init();
  7.      curl_setopt($ch, CURLOPT_URL, $url);
  8.      curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  9.      curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 1);
  10.      echo "Getting: {$url}\n";
  11.      $content = curl_exec($ch);
  12.      echo "Got \"{$content}\"\n";
  13.      curl_close($ch);
  14.  
  15.      $dom = new DOMDocument();
  16.      @$dom->loadHTML($content);
  17.  
  18.      $xpath = new DOMXPath($dom);
  19.      $hrefs = $xpath->evaluate("/html/body//a");
  20.  
  21.              $href = $hrefs->item(0);
  22.      $zipurl = $href->getAttribute('href');
  23.      echo "Zip url: {$zipurl}\n";
  24.     
  25.      $data = http_get_file($zipurl);
  26.      if (substr($data, "http://"))
  27.      {
  28.          $data = http_get_file($data);
  29.      }
  30.     
  31.      file_put_contents($filepath, $data);
  32. }
  33. ?>


GitHub делает несколько перенаправлений, которые по какой–то причине не выполняются с CURL'ом. Поэтому с помощью него мы находим алрес первого перенаправления, затем пытаемся перейти по нему. Получаем еще один адрес перенаправления и наконец добираемся до заветного архива. Функция http_get_file, использованная в коде выше:
  1. <?php
  2. function http_get_file($url)    
  3. {
  4.      $url_stuff = parse_url($url);
  5.      $port = isset($url_stuff['port']) ? $url_stuff['port']:80;
  6.  
  7.      $path = $url_stuff['path'];
  8.      $last = $path[strlen($path)-1];
  9.      if (strcmp($last, "_") == 0)
  10.      {
  11.          $path = substr_replace($path ,"",-1);
  12.      }
  13.  
  14.      $fp = fsockopen($url_stuff['host'], $port);
  15.  
  16.         $query = 'GET ' . $path . " HTTP/1.0\n";
  17.      $query .= 'Host: ' . $url_stuff['host'];
  18.      $query .= "\n\n";
  19.  
  20.      fwrite($fp, $query);
  21.  
  22.      while ($line = fread($fp, 1024))
  23.      {
  24.          $buffer .= $line;
  25.      }
  26.      if (preg_match('/^Location: (.+?)$/m', $buffer, $matches))
  27.      {
  28.          return $matches[1];
  29.      }
  30.  
  31.      preg_match('/Content-Length: ([0-9]+)/', $buffer, $parts);
  32.      return substr($buffer, - $parts[1]);
  33. }
  34. ?>


Обнаружилась странная функциональность. После выполнения функции parse_url на последнем адресе, указывающим на реальный архив, к концу имени файла добавляется символ подчеркивания.

Распаковываем архив:
  1. <?php
  2. if ($unzip)
  3. {
  4.      echo "Uncompressing archive... \n";
  5.      $zip = new ZipArchive;
  6.      $res = $zip->open($filepath);
  7.      if ($res === TRUE)
  8.      {
  9.           $zip->extractTo($stage_dir);
  10.           $zip->close();
  11.          echo "Done! \n";
  12.          } else
  13.          {
  14.          echo "Failed \n";
  15.          exit(1);
  16.      }
  17. }
  18. ?>


Внутри архива нас ждет папка с названием, состоящем из имени пользователя, имени репозитория и кусочка SHA кода комита. А внутри этой папки находятся файлы проекта. У меня это папка code. Следующим шагом я переношу папку code на уровень выше. Это нужно для того, чтобы правильно сработало отображение субдомена. Субдомен настроен, например, на папку /public_html/development. Архив распаковывается в /public_html/development/<user>_<repo>_<sha>/<files>.
  1. <?php
  2. if ($move)
  3. {
  4.     $files = scandir($stage_dir);
  5.     $match_array = preg_grep( '/<user_name>*/', $files);
  6.     if (is_array($match_array))
  7.     {
  8.         // remove all directory if any
  9.         delete_directory("code");
  10.         $dir_name = current($match_array);
  11.         $rep_dir = $dir_name . "/code";
  12.         echo "Try to move {$rep_dir} to code\n";
  13.         rename($rep_dir, "code");
  14.         rmdir($dir_name);
  15.         echo "Done moving files\n";
  16.     }
  17. }
  18.  
  19. function delete_directory($dirname)
  20. {
  21.     if (is_dir($dirname))
  22.         $dir_handle = opendir($dirname);
  23.     if (!$dir_handle)
  24.         return false;
  25.     while($file = readdir($dir_handle))
  26.     {
  27.         if ($file != "." && $file != "..")
  28.         {
  29.             if (!is_dir($dirname."/".$file))
  30.                 unlink($dirname."/".$file);
  31.             else
  32.                 delete_directory($dirname.'/'.$file);    
  33.         }
  34.     }
  35.     closedir($dir_handle);
  36.     rmdir($dirname);
  37. }
  38. ?>


И в завершении я копирую файл с конфигурацией, который находится в папке /public_html/development/
  1. <?php
  2. copy("config.php", "<new_path>/core.php");
  3. echo "All jobs have been done! \n";
  4. echo "</pre>";
  5. ?>
* This source code was highlighted with Source Code Highlighter.


Результаты скрипта, скачивающего заархивированный исходный код

Скрипт делает то, что нужно! =)

Заключение


В данной статье были рассмотрены подходы к созданию тестового программного окружения для вебсайта, исходный код которого хранится в системе GitHub. Предложенные подходы в будущем можно объединить. Сначала создавать полную копию репозитория, скачав архив, а затем отслеживать какие файлы были изменены в последнем комите и обновлять только их.

Решение, которое я применил для создания тестового программного окружения, может быть неидеальным. Мне очень интересно узнать как это делают другие люди. Поделитесь своими знаниями!

P.S. Данная статья вышла в свет при поддержке хабраюзера dive'а, который прислал мне инвайт. Спасибо!
Теги:
Хабы:
+8
Комментарии5

Публикации