Речь в данной статье пойдет о том, как автоматически получать свежую версию исходников из основной ветки вашего репозитория и разворачивать из нее проект на виртуальном хостинге. Сразу хочу отметить, что с GitHub'ом и Git'ом я познакомился только вчера. Поэтому матерым веб–программистам эта статья может показаться тривиальной. А тем, кто еще только начинает свой путь веб–программиста, надеюсь, поможет.
У меня есть небольшой вебсайт на виртуальном хостинге. На нем нет полноценного шелл–доступа и скрипты ограничены в некоторых правах. К примеру, я не могу использовать функйию PHP system и функци file_get_contents. После того, как я создал репозиторий на GitHub'е, научился немного работать с изменениями и обновил исходный код, настало время задуматься о том, что же делать дальше. Мне хотелось посмотреть свои изменения в действии, но при этом так, чтобы основной сайт продолжал работать.
Из доступных мне скриптовых языков я знаю только PHP. Выбор на чем писать был сделан автоматически. Я понимал, что мой скрипт должен каким–то образом получать уведомления об обновлении с GitHub'а и скачивать исходный код. Я решил сделать субдомен development.mysite.com и выкладывать последнюю версию исходников именно туда. Кроме того, у меня есть файл конфигурации вебсайта с паролями для базы данных, который я не выложил в общий доступ на GitHub'е. Этот файл нужно добавлять к скаченным исходникам, чтобы все заработало.
Таким образом, весь процесс можно разбить на следующие этапы:
Здесь все совсем просто. GitHub поддерживает хуки. Прописываем в Post-Receive URLs адрес нашего скрипта и все. Он будет вызываться при каждом изменении основной ветки репозитория. Подробнее об этом на сайте GitHub'а (на англ. языке). При этом я в своем скрипте никак не обрабатываю информацию о последнем транзакции (комите).
У разработчика есть два варианта для скачивания кода:
С помощью программного интерфейсам можно получить информацию о последнем комите. В ней находится идентификатор Tree SHA. Этот идентификатор позволяет последовательно получить список и содержимое всех файлов проекта.
Пример использования GitHub API на PHP описан в блоге Дэвида Волша. Возьмем оттуда несколько полезных функций и добавим свои. Начнем писать наш скрипт. В первую очередь параметры
Копия исходного кода будет создаваться в директории, в которой находится наш скрипт. Дальше вставляем функцию для получения данных по адресу, подсмотренную у Дэвида:
Затем идет функция, которая находит Tree SHA и начинает скачивать файлы:
Данная функция вызывает функцию get_repo, которая рекурсивно проходит по всем директориям проекта.
Стоит обратить внимание, что мы сразу получаем содержимое файла, без какой–либо дополнительной информации. Поэтому нам не нужно вызывать функцию json_decode как в случаях с вызовами других функций API.
У этого скрипта есть два существенных недостатка:
Кроме того, сама идея перекачивать проект по отдельным файлам представляется идеологически неправильной.
Потыкав в кнопки GitHub'а, я обнаружил возможность скачать заархивированную версию исходных файлов. Такой подход гораздо лучше! Скачать можно на выбор либо Zip архив, либо Tar. Мой выбор пал на Zip, потому что его проще распаковать на виртуальном хостинге. Посмотрим же на скрипт.
Переменные download, unzip и move управляют ходом программы и позволяют отключать её части. Они могут использоваться для отладки. Например, если архив уже скачан, но не распаковывается, то нет смысла скачивать его снова.
GitHub делает несколько перенаправлений, которые по какой–то причине не выполняются с CURL'ом. Поэтому с помощью него мы находим алрес первого перенаправления, затем пытаемся перейти по нему. Получаем еще один адрес перенаправления и наконец добираемся до заветного архива. Функция http_get_file, использованная в коде выше:
Обнаружилась странная функциональность. После выполнения функции parse_url на последнем адресе, указывающим на реальный архив, к концу имени файла добавляется символ подчеркивания.
Распаковываем архив:
Внутри архива нас ждет папка с названием, состоящем из имени пользователя, имени репозитория и кусочка SHA кода комита. А внутри этой папки находятся файлы проекта. У меня это папка code. Следующим шагом я переношу папку code на уровень выше. Это нужно для того, чтобы правильно сработало отображение субдомена. Субдомен настроен, например, на папку /public_html/development. Архив распаковывается в /public_html/development/<user>_<repo>_<sha>/<files>.
И в завершении я копирую файл с конфигурацией, который находится в папке /public_html/development/
Скрипт делает то, что нужно! =)
В данной статье были рассмотрены подходы к созданию тестового программного окружения для вебсайта, исходный код которого хранится в системе GitHub. Предложенные подходы в будущем можно объединить. Сначала создавать полную копию репозитория, скачав архив, а затем отслеживать какие файлы были изменены в последнем комите и обновлять только их.
Решение, которое я применил для создания тестового программного окружения, может быть неидеальным. Мне очень интересно узнать как это делают другие люди. Поделитесь своими знаниями!
P.S. Данная статья вышла в свет при поддержке хабраюзера dive'а, который прислал мне инвайт. Спасибо!
Введение
У меня есть небольшой вебсайт на виртуальном хостинге. На нем нет полноценного шелл–доступа и скрипты ограничены в некоторых правах. К примеру, я не могу использовать функйию 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 описан в блоге Дэвида Волша. Возьмем оттуда несколько полезных функций и добавим свои. Начнем писать наш скрипт. В первую очередь параметры
- <?php
- /* static settings */
- $user = '<github_username>';
- $repo = '<github_reponame>';
- $user_repo = $user . '/' . $repo;
- $tree_base_url = "http://github.com/api/v2/json/tree/show/" . $user_repo;
-
- // path on the server where your repository will go
- $stage_dir = $_SERVER['DOCUMENT_ROOT'] . dirname($_SERVER['SCRIPT_NAME']);
- ?>
Копия исходного кода будет создаваться в директории, в которой находится наш скрипт. Дальше вставляем функцию для получения данных по адресу, подсмотренную у Дэвида:
- <?php
- /* gets url */
- function get_content_from_github($url)
- {
- $ch = curl_init();
- curl_setopt($ch, CURLOPT_URL, $url);
- curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
- curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 1);
- echo "Getting: {$url}";
- $content = curl_exec($ch);
- curl_close($ch);
- return $content;
- }
- ?>
Затем идет функция, которая находит Tree SHA и начинает скачивать файлы:
- <?php
- function get_repo_json()
- {
- global $user, $repo, $user_repo, $tree_base_url, $stage_dir;
-
- $json = array();
- $list_commits_url = 'http://github.com/api/v2/json/commits/list/' . $user_repo . '/master';
-
- echo "Master branch url: {$list_commits_url}\n<br>";
- $json['commit'] = json_decode(get_content_from_github($list_commits_url), true);
-
- // get sha for the latest tree
- $tree_sha = $json['commit']['commits'][0]['tree'];
- echo "Tree sha: {$tree_sha}\n<br>";
- $cont_str = $tree_base_url . "/{$tree_sha}";
- $base = json_decode(get_content_from_github($cont_str), true);
-
- // output project structure
- echo "<pre>";
- get_repo($base['tree'], 0, $stage_dir);
- echo "</pre>";
- }
- ?>
Данная функция вызывает функцию get_repo, которая рекурсивно проходит по всем директориям проекта.
- <?php
- function get_repo($objects, $level = 0, $current_dir)
- {
- global $tree_base_url, $user_repo;
-
- chdir($current_dir);
-
- foreach ($objects as &$object)
- {
- $type = $object['type'];
- $sha = $object['sha'];
- $name = $object['name'];
-
- // add padding
- echo str_pad("", $level, "\t");
- echo $name . "\n";
-
- if (strcmp($type, "tree") == 0)
- {
- mkdir($name);
-
- $new_dir = $current_dir . '/' . $name;
-
- $tree = $tree_base_url . '/' . $sha;
- $new_objects = json_decode(get_content_from_github($tree), true);
-
- get_repo($new_objects['tree'], $level + 1, $new_dir);
-
- // change current directory back
- chdir($current_dir);
- }
- else
- {
- // get file content
- $blob_url = "http://github.com/api/v2/json/blob/show/" . $user_repo . "/" . $sha;
- $data = get_content_from_github($blob_url);
-
- $filename = $current_dir . '/' . $name;
- file_put_contents($filename, $data);
- }
- }
- }
- ?>
Стоит обратить внимание, что мы сразу получаем содержимое файла, без какой–либо дополнительной информации. Поэтому нам не нужно вызывать функцию json_decode как в случаях с вызовами других функций API.
Результаты получения исходников через API
У этого скрипта есть два существенных недостатка:
- он работает достаточно медленно;
- мне так и не удалось скачать весь проект целиком. CURL завершается по таймауту, весь процесс останавливается, не скачав и трети исходных файлов.
Кроме того, сама идея перекачивать проект по отдельным файлам представляется идеологически неправильной.
Архив с основной веткой проекта
Потыкав в кнопки GitHub'а, я обнаружил возможность скачать заархивированную версию исходных файлов. Такой подход гораздо лучше! Скачать можно на выбор либо Zip архив, либо Tar. Мой выбор пал на Zip, потому что его проще распаковать на виртуальном хостинге. Посмотрим же на скрипт.
- <?php
- $download = true;
- $unzip = true;
- $move = true;
-
- $stage_dir = $_SERVER['DOCUMENT_ROOT'] . dirname($_SERVER['SCRIPT_NAME']);
- $filepath = $stage_dir . '/' . 'master.zip';
-
- echo "<pre>";
- ?>
Переменные download, unzip и move управляют ходом программы и позволяют отключать её части. Они могут использоваться для отладки. Например, если архив уже скачан, но не распаковывается, то нет смысла скачивать его снова.
- <?php
- if ($download)
- {
- $url = "http://github.com/<your_github_username>/<your_github_repo_name>/zipball/master";
-
- $ch = curl_init();
- curl_setopt($ch, CURLOPT_URL, $url);
- curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
- curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 1);
- echo "Getting: {$url}\n";
- $content = curl_exec($ch);
- echo "Got \"{$content}\"\n";
- curl_close($ch);
-
- $dom = new DOMDocument();
- @$dom->loadHTML($content);
-
- $xpath = new DOMXPath($dom);
- $hrefs = $xpath->evaluate("/html/body//a");
-
- $href = $hrefs->item(0);
- $zipurl = $href->getAttribute('href');
- echo "Zip url: {$zipurl}\n";
-
- $data = http_get_file($zipurl);
- if (substr($data, "http://"))
- {
- $data = http_get_file($data);
- }
-
- file_put_contents($filepath, $data);
- }
- ?>
GitHub делает несколько перенаправлений, которые по какой–то причине не выполняются с CURL'ом. Поэтому с помощью него мы находим алрес первого перенаправления, затем пытаемся перейти по нему. Получаем еще один адрес перенаправления и наконец добираемся до заветного архива. Функция http_get_file, использованная в коде выше:
- <?php
- function http_get_file($url)
- {
- $url_stuff = parse_url($url);
- $port = isset($url_stuff['port']) ? $url_stuff['port']:80;
-
- $path = $url_stuff['path'];
- $last = $path[strlen($path)-1];
- if (strcmp($last, "_") == 0)
- {
- $path = substr_replace($path ,"",-1);
- }
-
- $fp = fsockopen($url_stuff['host'], $port);
-
- $query = 'GET ' . $path . " HTTP/1.0\n";
- $query .= 'Host: ' . $url_stuff['host'];
- $query .= "\n\n";
-
- fwrite($fp, $query);
-
- while ($line = fread($fp, 1024))
- {
- $buffer .= $line;
- }
- if (preg_match('/^Location: (.+?)$/m', $buffer, $matches))
- {
- return $matches[1];
- }
-
- preg_match('/Content-Length: ([0-9]+)/', $buffer, $parts);
- return substr($buffer, - $parts[1]);
- }
- ?>
Обнаружилась странная функциональность. После выполнения функции parse_url на последнем адресе, указывающим на реальный архив, к концу имени файла добавляется символ подчеркивания.
Распаковываем архив:
- <?php
- if ($unzip)
- {
- echo "Uncompressing archive... \n";
- $zip = new ZipArchive;
- $res = $zip->open($filepath);
- if ($res === TRUE)
- {
- $zip->extractTo($stage_dir);
- $zip->close();
- echo "Done! \n";
- } else
- {
- echo "Failed \n";
- exit(1);
- }
- }
- ?>
Внутри архива нас ждет папка с названием, состоящем из имени пользователя, имени репозитория и кусочка SHA кода комита. А внутри этой папки находятся файлы проекта. У меня это папка code. Следующим шагом я переношу папку code на уровень выше. Это нужно для того, чтобы правильно сработало отображение субдомена. Субдомен настроен, например, на папку /public_html/development. Архив распаковывается в /public_html/development/<user>_<repo>_<sha>/<files>.
- <?php
- if ($move)
- {
- $files = scandir($stage_dir);
- $match_array = preg_grep( '/<user_name>*/', $files);
- if (is_array($match_array))
- {
- // remove all directory if any
- delete_directory("code");
- $dir_name = current($match_array);
- $rep_dir = $dir_name . "/code";
- echo "Try to move {$rep_dir} to code\n";
- rename($rep_dir, "code");
- rmdir($dir_name);
- echo "Done moving files\n";
- }
- }
-
- function delete_directory($dirname)
- {
- if (is_dir($dirname))
- $dir_handle = opendir($dirname);
- if (!$dir_handle)
- return false;
- while($file = readdir($dir_handle))
- {
- if ($file != "." && $file != "..")
- {
- if (!is_dir($dirname."/".$file))
- unlink($dirname."/".$file);
- else
- delete_directory($dirname.'/'.$file);
- }
- }
- closedir($dir_handle);
- rmdir($dirname);
- }
- ?>
И в завершении я копирую файл с конфигурацией, который находится в папке /public_html/development/
- <?php
- copy("config.php", "<new_path>/core.php");
- echo "All jobs have been done! \n";
- echo "</pre>";
- ?>
* This source code was highlighted with Source Code Highlighter.
Результаты скрипта, скачивающего заархивированный исходный код
Скрипт делает то, что нужно! =)
Заключение
В данной статье были рассмотрены подходы к созданию тестового программного окружения для вебсайта, исходный код которого хранится в системе GitHub. Предложенные подходы в будущем можно объединить. Сначала создавать полную копию репозитория, скачав архив, а затем отслеживать какие файлы были изменены в последнем комите и обновлять только их.
Решение, которое я применил для создания тестового программного окружения, может быть неидеальным. Мне очень интересно узнать как это делают другие люди. Поделитесь своими знаниями!
P.S. Данная статья вышла в свет при поддержке хабраюзера dive'а, который прислал мне инвайт. Спасибо!