git_deploy.module in Git Deploy 6
Same filename and directory in other branches
Adds project, version and date information to projects checked out with Git.
This module provides metadata about modules installed via Git for Drupal's updates listing. As it is intended to provide the same functionality as CVS Deploy but for Git modules, this module is modeled after CVS Deploy, which was written by Derek Wright (dww).
The intention of this module is to support the commit tagging styles displayed at http://git.drupalcode.org/, which is a picture of how Drupal modules will look after conversion to Git. This tagging convention is incompatible with the CVS tagging convention.
File
git_deploy.moduleView source
<?php
/**
* @file
* Adds project, version and date information to projects checked out with Git.
*
* This module provides metadata about modules installed via Git for Drupal's
* updates listing. As it is intended to provide the same functionality as CVS
* Deploy but for Git modules, this module is modeled after CVS Deploy, which
* was written by Derek Wright (dww).
*
* The intention of this module is to support the commit tagging styles
* displayed at http://git.drupalcode.org/, which is a picture of how Drupal
* modules will look after conversion to Git. This tagging convention is
* incompatible with the CVS tagging convention.
*/
/**
* Implements hook_help().
*/
function git_deploy_help($path, $arg) {
if ($path == 'admin/reports/updates' && !module_exists('mydropwizard')) {
return '<p><strong>' . t('Drupal 6 has reached its community End-of-Life (EOL) date and is now in a Long-Term Support (LTS) phase, where support is provided by a <a href="http://drupal.org/project/d6lts">small group of vendors</a>. To get accurate update status information, enable the <a href="https://www.drupal.org/project/mydropwizard">myDropWizard</a> module.') . '</strong></p>';
}
}
/**
* Stores shared static variables for Git Deploy.
*
* @param string|NULL $name
* Unique variable name. If omitted, all variables.
* @param $default_value
* Value to store.
* @param bool $reset
* Internal: TRUE to reset one or all variables.
*/
function &_git_deploy_static($name, $default_value = NULL, $reset = FALSE) {
static $data = array(), $default = array();
if (isset($name)) {
if (array_key_exists($name, $data)) {
if ($reset) {
$data[$name] = $default[$name];
}
}
else {
$data[$name] = $default[$name] = $default_value;
}
return $data[$name];
}
elseif ($reset) {
foreach ($default as $name => $value) {
$data[$name] = $default[$name];
}
}
return $data;
}
/**
* Resets one or all Git Deploy static variables.
*
* @param string|null $name
* Name of static variable to reset. Omit to reset all variables.
*/
function _git_deploy_static_reset($name = NULL) {
_git_deploy_static($name, NULL, TRUE);
}
/**
* Implements hook_system_info_alter().
*
* Provides metadata to Drupal from Git. We support populating $info['version']
* and $info['project'], as CVS Deploy does.
*/
function git_deploy_system_info_alter(&$info, $file) {
// Use _git_deploy_static() so we can share static variables and free up
// memory later.
$projects =& _git_deploy_static('projects', array());
$available =& _git_deploy_static('available');
$update =& _git_deploy_static('update', array());
$last_check =& _git_deploy_static('last_check');
// Include the glip library.
if (module_exists('libraries')) {
require_once libraries_get_path('glip') . '/lib/glip.php';
}
else {
require_once variable_get('git_deploy_glip_path', 'sites/all/libraries/glip') . '/lib/glip.php';
}
// Core has hard-coded version numbers, so we need to verify them. Otherwise,
// a valid version number indicates that this project was not installed with
// Git.
$is_core = isset($info['package']) && strpos($info['package'], 'Core -') !== FALSE;
if (empty($info['version']) || ($is_core ? strstr($info['version'], '-dev') == '-dev' : $info['version'] == VERSION || !preg_match('/^6\\.x-\\d+\\..+/', $info['version']))) {
// Verify that we are in a Git repository.
$path_components = explode('/', preg_replace('/^' . preg_quote(DRUPAL_ROOT . '/', '/') . '/', '', realpath($file->filename), 1));
$path = array_pop($path_components);
while (!empty($path_components) && !file_exists(implode('/', $path_components) . '/.git')) {
$path = array_pop($path_components) . "/{$path}";
}
$path_components[] = '.git';
$directory = implode('/', $path_components);
// Handle submodules.
if (is_file($directory)) {
$gitdir = str_replace('gitdir: ', '', trim(current(file($directory))));
$directory = preg_replace('/^' . preg_quote(DRUPAL_ROOT . '/', '/') . '/', '', realpath(dirname($directory) . "/{$gitdir}"), 1);
}
if (file_exists($directory)) {
// Only check Git once per repository.
if (!isset($projects[$directory])) {
$projects[$directory] = array();
// Ensure file is in repository.
$git = new Git($directory);
if (@$git
->getObject($git
->revParse())
->getTree()
->find($path)) {
// Get Git configuration.
$git_config = parse_ini_file($directory . '/config', TRUE);
// Get upstream info.
$upstream = _git_deploy_get_upstream($git, $is_core ? '6' : '6.x-*', $git_config);
if ($is_core) {
$project_name = 'drupal';
}
elseif (isset($upstream['remote']) && $git_config) {
// Find the project name based on fetch URL.
$remote = isset($git_config["remote {$upstream['remote']}"]) ? "remote {$upstream['remote']}" : current(preg_grep('/^remote\\s.+/', array_keys($git_config)));
if (!empty($remote)) {
$project_name = basename($git_config[$remote]['url'], '.git');
$projects[$directory]['project'] = $project_name;
}
}
// Set project datestamp.
if (isset($upstream['datestamp'])) {
$projects[$directory]['datestamp'] = $upstream['datestamp'];
// The '_info_file_ctime' should always get the latest value.
if (empty($info['_info_file_ctime'])) {
$projects[$directory]['_info_file_ctime'] = $upstream['datestamp'];
}
else {
$projects[$directory]['_info_file_ctime'] = max($info['_info_file_ctime'], $upstream['datestamp']);
}
}
// Set version from tag.
if (isset($upstream['tag'])) {
$projects[$directory]['version'] = $upstream['tag'];
}
elseif (isset($upstream['branch'])) {
if ($upstream['branch'] != 'master') {
$projects[$directory]['version'] = "{$upstream['branch']}-dev";
}
if (module_exists('mydropwizard') && ($upstream['synced'] || $upstream['branch'] == 'master')) {
if (!isset($available)) {
// We only get this from the cache because an update status
// query is supposed to done only after processing all enabled
// modules and themes.
if (($cache = _mydropwizard_cache_get('mydropwizard_available_releases')) && $cache->expire > time()) {
$available = $cache->data;
$last_check = variable_get('mydropwizard_last_check', 0);
}
}
if (!empty($available[$project_name]['releases'])) {
if ($upstream['branch'] == 'master') {
// If there's available update_status data, we can use the
// version string the release node pointing to HEAD really
// has.
foreach ($available[$project_name]['releases'] as $release) {
if (isset($release['tag']) && $release['tag'] == 'HEAD') {
$projects[$directory]['version'] = $release['version'];
break;
}
}
}
if ($upstream['synced']) {
// This project's datestamp needs to be synced with upstream
// release.
$version = $projects[$directory]['version'];
if (!isset($available[$project_name]['releases'][$version]) || $projects[$directory]['_info_file_ctime'] > $last_check) {
// We need to update available release data for this
// project.
$update[] = $project_name;
}
else {
_git_deploy_datestamp_sync($projects[$directory], $available[$project_name]['releases'][$version]);
}
}
}
}
}
}
}
$info = $projects[$directory] + $info;
}
}
}
/**
* Gets upstream info.
*
* @param Git $git
* A Git instance.
* @param string $pattern
* Pattern for matching branch names, without trailing ".x". Must not include
* repository name. Also used to check for release tags.
* @param array|false $git_config
* Git configuration if found.
*
* @return string[]
* Array with the following keys, if found:
* - branch: Best matching remote branch.
* - remote: Remote repository containing best matching branch.
* - tag: Release tag from last common commit in matching branch.
* - datestamp: Unix timestamp of last common commit.
*/
function _git_deploy_get_upstream(Git $git, $pattern = '*', $git_config = FALSE) {
$pattern = str_replace(array(
'.',
'*',
), array(
'\\.',
'\\d+',
), $pattern);
$upstream = array();
// List references in descending version order.
$refs = $git
->getrefs();
uksort($refs, 'version_compare');
$refs = array_reverse($refs);
// List remote branches matching pattern. Make special exception for master.
// If origin exists, don't check any other remote repositories.
$remotes = preg_grep("/^refs\\/remotes\\/origin\\/(?:{$pattern}\\.x|master)\$/", array_keys($refs));
if (empty($remotes)) {
$remotes = preg_grep("/^refs\\/remotes\\/.+\\/(?:{$pattern}\\.x|master)\$/", array_keys($refs));
}
$remotes = array_intersect_key($refs, array_flip($remotes));
if (!empty($remotes)) {
list(, , $upstream['remote']) = explode('/', key($remotes), 4);
// List tags matching pattern.
$tags = array_intersect_key($refs, array_flip(preg_grep("/^refs\\/tags\\/{$pattern}\\.\\d+.*/", array_keys($refs))));
// Check for tag on current commit.
$githash = _git_deploy_tag($git, $tags, array(
$git
->revParse(),
), $upstream);
if (!isset($upstream['tag'])) {
// Check if we are tracking a valid remote branch.
$head = trim(current(file($git->dir . '/HEAD')));
if (strpos($head, 'ref: refs/heads/') === 0) {
// Get local branch name.
$head = str_replace('ref: refs/heads/', '', $head);
if (!empty($git_config) && isset($git_config["branch {$head}"]['remote']) && isset($git_config["branch {$head}"]['merge'])) {
// Get remote branch name.
$ref = str_replace('refs/heads/', 'refs/remotes/' . $git_config["branch {$head}"]['remote'] . '/', $git_config["branch {$head}"]['merge']);
if (isset($remotes[$ref])) {
$local = _git_deploy_base($git, array(
$ref => $remotes[$ref],
), $githash, $upstream);
// If most recent common commit is not the current commit, check for
// tag on most recent common commit.
if (reset($local) != $githash) {
$githash = _git_deploy_tag($git, $tags, $local, $upstream);
}
}
}
}
// If we are not tracking a valid remote branch, find best best matching
// remote branch.
if (!isset($upstream['branch'])) {
$local = _git_deploy_base($git, $remotes, $githash, $upstream);
// If most recent common commit is not the current commit, check for tag
// on most recent common commit.
if (reset($local) != $githash) {
$githash = _git_deploy_tag($git, $tags, $local, $upstream);
}
}
}
// Find the timestamp for the current commit.
$upstream['datestamp'] = @$git
->getObject($githash)->committer->time;
}
return $upstream;
}
/**
* Checks for most recent local commit in a set of remote branches.
*
* @param Git $git
* A Git instance.
* @param string[] $remotes
* Remote branch commit hashes keyed by reference, in descending order.
* @param string $githash
* Local commit hash.
* @param array $upstream
* Things we need to know to determine status.
*
* @return string[]
* Local history starting with most recent common commit hash.
*/
function _git_deploy_base(Git $git, array $remotes, $githash, array &$upstream) {
$local_queue = array(
@$git
->getObject($githash),
);
$local = $remote = array();
foreach ($remotes as $ref => $hash) {
if ($hash == $githash) {
// If remote branch matches local branch, it is the best match.
$local = array(
$githash,
);
list(, , $upstream['remote'], $upstream['branch']) = explode('/', $ref, 4);
// Local history contains last remote commit.
$upstream['synced'] = TRUE;
break;
}
elseif (in_array($hash, $remote)) {
// Last remote branch was more recent. For performance, stop looking.
break;
}
elseif (in_array($hash, $local)) {
// This remote branch is more recent.
$githash = $hash;
list(, , $upstream['remote'], $upstream['branch']) = explode('/', $ref, 4);
// Local history contains last remote commit.
$upstream['synced'] = TRUE;
}
else {
// Find most recent common commit.
$remote_queue = array(
@$git
->getObject($hash),
);
$remote = array();
// See if remote commit has been found in local history.
while ($commit = array_pop($remote_queue)) {
// Add remote commit to remote history.
array_unshift($remote, $commit
->getName());
if (!in_array($commit
->getName(), $local)) {
// Add parent commits to remote queue to be checked.
foreach ($commit->parents as $parent) {
// Only check parent if not already in remote history.
if (!in_array($parent, $remote)) {
$remote_queue[] = @$git
->getObject($parent);
}
}
// See if local commit has been found in remote history.
$commit = array_pop($local_queue);
// Add local commit to local history.
array_unshift($local, $commit
->getName());
if (!in_array($commit
->getName(), $remote)) {
// Add parent commits to local queue to be checked.
foreach ($commit->parents as $parent) {
// Only check parent if not already in local history.
if (!in_array($parent, $local)) {
$local_queue[] = @$git
->getObject($parent);
}
}
}
elseif (!isset($last_commit) || in_array($last_commit, $remote)) {
// Found most recent common commit in local history.
list(, , $upstream['remote'], $upstream['branch']) = explode('/', $ref, 4);
$last_commit = $githash = $commit
->getName();
break;
}
}
elseif (!isset($last_commit) || in_array($last_commit, $remote)) {
// Found most recent common commit in remote history.
list(, , $upstream['remote'], $upstream['branch']) = explode('/', $ref, 4);
$last_commit = $githash = $commit
->getName();
break;
}
}
// If we did not find a common commit, check rest of remote queue.
if (!in_array($githash, $remote)) {
while ($commit = array_pop($remote_queue)) {
// Add remote commit to remote history.
array_unshift($remote, $commit
->getName());
if (!in_array($commit
->getName(), $local)) {
// Add parent commits to remote queue to be checked.
foreach ($commit->parents as $parent) {
// Only check parent if not already in remote history.
if (!in_array($parent, $remote)) {
$remote_queue[] = @$git
->getObject($parent);
}
}
}
elseif (!isset($last_commit) || in_array($last_commit, $remote)) {
// Found most recent common commit in remote history.
list(, , $upstream['remote'], $upstream['branch']) = explode('/', $ref, 4);
$last_commit = $githash = $commit
->getName();
break;
}
}
}
elseif (!in_array($githash, $local)) {
while ($commit = array_pop($local_queue)) {
// Add local commit to local history.
array_unshift($local, $commit
->getName());
if (!in_array($commit
->getName(), $remote)) {
// Add parent commits to local queue to be checked.
foreach ($commit->parents as $parent) {
// Only check parent if not already in local history.
if (!in_array($parent, $local)) {
$local_queue[] = @$git
->getObject($parent);
}
}
}
elseif (!isset($last_commit) || in_array($last_commit, $remote)) {
// Found most recent common commit in local history.
list(, , $upstream['remote'], $upstream['branch']) = explode('/', $ref, 4);
$last_commit = $githash = $commit
->getName();
break;
}
}
}
// Look for last commit in local history for update status.
$upstream['synced'] = in_array($hash, $local);
}
}
return array_slice($local, array_search($githash, $local));
}
/**
* Checks for most recent tag in local history.
*
* @param Git $git
* A Git instance.
* @param string[] $tags
* Tag hashes keyed by reference, in descending order.
* @param string[] $local
* Local history starting with most recent common commit hash.
* @param array $upstream
* Things we need to know to determine status.
*/
function _git_deploy_tag(Git $git, array $tags, $local, array &$upstream) {
$githash = array_shift($local);
// Check for tags that do not exist on a remote branch.
$ref = key(array_intersect($tags, $local));
if (!empty($ref)) {
$upstream['tag'] = str_replace('refs/tags/', '', $ref);
$githash = $tags[$ref];
}
elseif (in_array($githash, $tags)) {
$upstream['tag'] = str_replace('refs/tags/', '', array_search($githash, $tags));
}
else {
// Check for annotated tag.
foreach ($tags as $ref => $hash) {
$tag = @$git
->getObject($hash);
if ($tag
->getType() == Git::OBJ_TAG) {
$hash = $tag->object;
// Check for tags that do not exist on a remote branch.
if (in_array($hash, $local)) {
$upstream['tag'] = str_replace('refs/tags/', '', $ref);
$githash = $hash;
break;
}
elseif ($hash == $githash) {
$upstream['tag'] = $tag->tag;
break;
}
}
}
}
return $githash;
}
/**
* Implements hook_mydropwizard_projects_alter().
*/
function git_deploy_mydropwizard_projects_alter(&$projects) {
// Get projects whose datestamps need to be synced with upstream releases.
$update = _git_deploy_static('update', array());
// Free up memory.
_git_deploy_static_reset();
if (!empty($update)) {
// Fetch updated release data.
$available = _git_deploy_update_refresh($projects);
foreach ($update as $project_name) {
$project =& $projects[$project_name];
$version = $project['info']['version'];
if (isset($available[$project_name]['releases'][$version])) {
_git_deploy_datestamp_sync($project, $available[$project_name]['releases'][$version]);
}
}
}
}
/**
* Updates available release data given a list of projects.
*
* We need this because loading projects within the function that fetches
* available releases would start an endless loop.
*
* @param array[] $projects
* List of available projects.
*
* @return array[]
* List of available releases, keyed by project name.
*
* @see _mydropwizard_refresh()
*/
function _git_deploy_update_refresh($projects) {
static $fail = array();
global $base_url;
module_load_include('inc', 'mydropwizard', 'mydropwizard.compare');
// Since we're fetching new available update data, we want to clear our cache
// of both the projects we care about, and the current update status of the
// site. We do *not* want to clear the cache of available releases just yet,
// since that data (even if it's stale) can be useful during
// mydropwizard_get_projects(); for example, to modules that implement
// hook_system_info_alter() such as cvs_deploy.
_mydropwizard_cache_clear('mydropwizard_project_projects');
_mydropwizard_cache_clear('mydropwizard_project_data');
$available = array();
$data = array();
$site_key = md5($base_url . drupal_get_private_key());
// Now that we have the list of projects, we should also clear our cache of
// available release data, since even if we fail to fetch new data, we need
// to clear out the stale data at this point.
_mydropwizard_cache_clear('mydropwizard_available_releases');
$max_fetch_attempts = variable_get('mydropwizard_max_fetch_attempts', UPDATE_MAX_FETCH_ATTEMPTS);
// Call back to the server to share staistics, and see if we're using an
// out-of-date version of the mydropwizard module.
$project_versions = array();
foreach ($projects as $key => $project) {
if (strpos($project['project_type'], 'disabled') === FALSE) {
if (!empty($project['info']['version'])) {
$version = $project['info']['version'];
}
else {
// Stub version to encode the core compatibility.
$version = DRUPAL_CORE_COMPATIBILITY;
}
$project_versions[$key] = $version;
}
}
$statistics_url = variable_get('mydropwizard_statistics_url', MYDROPWIZARD_STATISTICS_URL);
$statistics_data = drupal_to_js(array(
'site_key' => $site_key,
'projects' => $project_versions,
'mydropwizard_key' => variable_get('mydropwizard_customer_key', ''),
));
if (variable_get('mydropwizard_http_post_disabled', FALSE)) {
// This is a hack for a customer site that seemingly can't do HTTP POST
// for mysterious reasons. I don't like it, but it works.
$response = drupal_http_request($statistics_url . '?data=' . base64_encode($statistics_data));
}
else {
$response = drupal_http_request($statistics_url, array(
'Content-type' => 'application/json',
), 'POST', $statistics_data);
}
$update_status = $response->data;
variable_set('mydropwizard_update_status', $update_status);
// If the server says we're good, then pull the individual status for each project.
if ($update_status == 'OK' || variable_get('mydropwizard_ignore_statistics_errors', FALSE)) {
module_load_include('inc', 'mydropwizard', 'mydropwizard.fetch');
foreach ($projects as $key => $project) {
$url = _mydropwizard_build_fetch_url($project, $site_key);
$fetch_url_base = _mydropwizard_get_fetch_url_base($project);
if (empty($fail[$fetch_url_base]) || count($fail[$fetch_url_base]) < $max_fetch_attempts) {
$xml = drupal_http_request($url);
if (isset($xml->data)) {
$data[] = $xml->data;
}
else {
// Connection likely broken; prepare to give up.
$fail[$fetch_url_base][$key] = 1;
}
}
else {
// Didn't bother trying to fetch.
$fail[$fetch_url_base][$key] = 1;
}
}
}
if ($data) {
$parser = new mydropwizard_xml_parser();
$available = $parser
->parse($data);
}
if (!empty($available) && is_array($available)) {
// Record the projects where we failed to fetch data.
foreach ($fail as $fetch_url_base => $failures) {
foreach ($failures as $key => $value) {
$available[$key]['project_status'] = 'not-fetched';
}
}
$frequency = variable_get('mydropwizard_check_frequency', 1);
_mydropwizard_cache_set('mydropwizard_available_releases', $available, time() + 60 * 60 * 24 * $frequency);
watchdog('mydropwizard', 'Attempted to fetch information about all available new releases and updates.', array(), WATCHDOG_NOTICE, l(t('view'), 'admin/reports/updates'));
}
else {
watchdog('mydropwizard', 'Unable to fetch any information about available new releases and updates.', array(), WATCHDOG_ERROR, l(t('view'), 'admin/reports/updates'));
}
// Whether this worked or not, we did just (try to) check for updates.
variable_set('mydropwizard_last_check', time());
return $available;
}
/**
* Syncs the project datestamp to the release datestamp.
*
* @param array $project
* Project data. Datestamp will be synced if up to date with release.
* @param array $release
* Release data.
*/
function _git_deploy_datestamp_sync(array &$project, array $release) {
// We need to compare commit time to release time because our remote tracking
// branch could be out of date. Allow a 12 hour time difference between
// release and last commit, because dev releases are packaged only twice a
// day. Add a 100-second buffer to account for packaging time.
if ($project['datestamp'] + 43200 + 100 > $release['date']) {
// A dev release for the latest commit may be created later than an official
// release, so use release time only if it is later than the commit time.
$project['datestamp'] = max($release['date'], $project['datestamp']);
}
}
/**
* Implements hook_mydropwizard_status_alter().
*/
function git_deploy_mydropwizard_status_alter(&$projects) {
// Git Deploy for Drupal 6 is still supported.
if ($projects['git_deploy']['status'] == UPDATE_NOT_SUPPORTED && $projects['git_deploy']['project_status'] == 'unsupported') {
array_shift($projects['git_deploy']['extra']);
switch ($projects['git_deploy']['install_type']) {
case 'official':
if ($projects['git_deploy']['existing_version'] === $projects['git_deploy']['recommended'] || $projects['git_deploy']['existing_version'] === $projects['git_deploy']['latest_version']) {
$projects['git_deploy']['status'] = UPDATE_CURRENT;
}
else {
$projects['git_deploy']['status'] = UPDATE_NOT_CURRENT;
}
break;
case 'dev':
if (isset($projects['git_deploy']['dev_version']) && $projects['git_deploy']['releases'][$projects['git_deploy']['dev_version']]['date'] > $projects['git_deploy']['releases'][$projects['git_deploy']['latest_version']]['date']) {
$latest = $projects['git_deploy']['releases'][$projects['git_deploy']['dev_version']];
}
else {
$latest = $projects['git_deploy']['releases'][$projects['git_deploy']['latest_version']];
}
if (empty($projects['git_deploy']['datestamp'])) {
$projects['git_deploy']['status'] = UPDATE_NOT_CHECKED;
$projects['git_deploy']['reason'] = t('Unknown release date');
}
elseif ($projects['git_deploy']['datestamp'] + 100 > $latest['date']) {
$projects['git_deploy']['status'] = UPDATE_CURRENT;
}
else {
$projects['git_deploy']['status'] = UPDATE_NOT_CURRENT;
}
break;
default:
$projects['git_deploy']['status'] = UPDATE_UNKNOWN;
$projects['git_deploy']['reason'] = t('Invalid info');
}
}
}
Functions
Name![]() |
Description |
---|---|
git_deploy_help | Implements hook_help(). |
git_deploy_mydropwizard_projects_alter | Implements hook_mydropwizard_projects_alter(). |
git_deploy_mydropwizard_status_alter | Implements hook_mydropwizard_status_alter(). |
git_deploy_system_info_alter | Implements hook_system_info_alter(). |
_git_deploy_base | Checks for most recent local commit in a set of remote branches. |
_git_deploy_datestamp_sync | Syncs the project datestamp to the release datestamp. |
_git_deploy_get_upstream | Gets upstream info. |
_git_deploy_static | Stores shared static variables for Git Deploy. |
_git_deploy_static_reset | Resets one or all Git Deploy static variables. |
_git_deploy_tag | Checks for most recent tag in local history. |
_git_deploy_update_refresh | Updates available release data given a list of projects. |