<?php

/**
 * @file
 * This module add versioning information to projects checked out of Git.
 */

/**
 * Implements hook_system_info_alter().
 *
 * Provide metadata to drupal from git. We support populating $info['version'],
 * $info['project'] and $info['datestamp'].
 */
function git_deploy_system_info_alter(&$info, $file, $type = NULL) {
  static $additions = array();

  $directory = dirname($file->uri);

  // If the version is specified or the project belongs to the core then we do
  // not have to do anything. Speed optimization.
  if (!empty($info['version']) || strpos($directory, $type) === 0) {
    return;
  }

  while ($directory && !file_exists("$directory/.git")) {
    $directory = substr($directory, 0, strrpos($directory, '/'));
  }

  // Sometimes $git_dir is not a directory, but it is a text file with only one
  // line. Something like this:
  // gitdir: ../../../../../.git/modules/sites/all/modules/contrib/git_deploy
  $git_dir = "$directory/.git";
  if (!$directory || !file_exists($git_dir)) {
    return;
  }

  if (!array_key_exists($git_dir, $additions)) {
    // Git executable with basic parameters.
    $git = 'git --git-dir ' . escapeshellarg($git_dir);

    // Commit hash is required for the cache id.
    exec("$git rev-parse --verify HEAD 2>&1", $head_hash);

    // Cannot live more than one project (profile, module, theme,
    // theme_engine) with same name in one Drupal instance, but this cache id
    // is usable for more Drupal instance too.
    $cache_id = "git_deploy:{$file->name}:{$head_hash[0]}";

    if ($cache = _git_deploy_cache_get($cache_id)) {
      $additions[$git_dir] = $cache->data;
    }
    else {
      // Find first the project name based on fetch URL.
      // Eat error messages. >& is valid on Windows, too. Also, $output does
      // not need initialization because it's taken by reference.
      exec("$git remote show -n origin 2>&1", $output);
      if ($fetch_url = preg_grep('/^\s*Fetch URL:/', $output)) {
        $fetch_url = current($fetch_url);
        $project_name = substr($fetch_url, strrpos($fetch_url, '/') + 1);
        if (substr($project_name, -4) == '.git') {
          $project_name = substr($project_name, 0, -4);
        }
        $additions[$git_dir]['project'] = $project_name;
      }

      // Try to fill in branch and tag.
      exec("$git rev-parse --abbrev-ref HEAD 2>&1", $branch);
      $tag_found = FALSE;
      $last_tag = '';
      if ($branch) {
        $branch = $branch[0];
        // Any Drupal-formatted branch.
        $branch_preg = '\d+\.x-\d+\.';
        if (preg_match('/^' . $branch_preg . 'x$/', $branch)) {
          $additions[$git_dir]['version'] = $branch . '-dev';
          // Nail down the core and the major version now that we know
          // what they are.
          $branch_preg = preg_quote(substr($branch, 0, -1));
        }

        // Now try to find a tag.
        exec("$git rev-list --topo-order --max-count=1 HEAD 2>&1", $last_tag_hash);
        if ($last_tag_hash) {
          exec("$git describe --tags {$last_tag_hash[0]} 2>&1", $last_tag);
          if ($last_tag) {
            $last_tag = $last_tag[0];
            // Make sure the tag starts as Drupal formatted (for eg.
            // 7.x-1.0-alpha1) and if we are on a proper branch (ie. not
            // master) then it's on that branch.
            if (preg_match('/^(' . $branch_preg . '\d+(?:-[^-]+)?)(-(\d+-)g[0-9a-f]{7})?$/', $last_tag, $matches)) {
              $tag_found = TRUE;
              $additions[$git_dir]['version'] = isset($matches[2]) ? $matches[1] . '.' . $matches[3] . 'dev' : $last_tag;
            }
          }
        }
      }

      if (!$tag_found) {
        $last_tag = '';
      }
      // The git log -1 command always succeeds and if we are not on a
      // tag this will happen to return the time of the last commit which
      // is exactly what we wanted.
      exec("$git log -1 --pretty=format:%at $last_tag 2>&1", $datestamp);
      if ($datestamp && is_numeric($datestamp[0])) {
        $additions[$git_dir]['datestamp'] = $datestamp[0];
      }

      // Save the values into cache.
      _git_deploy_cache_set($cache_id, $additions[$git_dir]);
    }
  }

  if (array_key_exists($git_dir, $additions)) {
    foreach ($additions[$git_dir] as $key => $value) {
      $info[$key] = $value;
    }
  }
}

/**
 * Returns cache object from the cache_git_deploy cache bin.
 *
 * If the cache data is not exists in the local cache bin, then attempt to read
 * from the shared file cache.
 *
 * @param string $cache_id
 *   Cache identifier.
 *
 * @return object|bool
 *   Cache object or FALSE.
 */
function _git_deploy_cache_get($cache_id) {
  $cache_bin = 'cache_git_deploy';
  $cache_object = cache_get($cache_id, $cache_bin);
  if (empty($cache_object)) {
    $cache_data = _git_deploy_file_cache_read($cache_id);
    if ($cache_data) {
      cache_set($cache_id, $cache_data, $cache_bin);
      $cache_object = cache_get($cache_id, $cache_bin);
    }
  }

  return $cache_object;
}

/**
 * Save $info additions into cache_git_deploy cache bin and file cache.
 *
 * @param string $cache_id
 *   Cache identifier.
 * @param array $cache_data
 *   Array of system info additions.
 */
function _git_deploy_cache_set($cache_id, $cache_data) {
  cache_set($cache_id, $cache_data, 'cache_git_deploy');
  _git_deploy_file_cache_write($cache_id, $cache_data);
}

/**
 * Convert cache id to file name.
 *
 * @param string $cache_id
 *   Cache identifier.
 *
 * @return string
 *   File name.
 */
function _git_deploy_file_cache_name($cache_id) {
  return str_replace(array(':', '/'), '-', $cache_id);
}

/**
 * Read cache data from the shared file based cache.
 *
 * WebServer writable files can not be trusted and sending untrusted data
 * through unserialize() is dangerous.
 *
 * @see _git_deploy_file_cache_write()
 * @see _git_deploy_file_cache_unserialize()
 *
 * @param string $cache_id
 *   Cache identifier.
 *
 * @return array|null
 *   Cache data. System info additions.
 */
function _git_deploy_file_cache_read($cache_id) {
  if ($directory = variable_get('git_deploy_file_cache_directory', '')) {
    $file_name = str_replace(array(':', '/'), '-', $cache_id);
    if (is_readable("$directory/$file_name") && ($cache_data = @file_get_contents("$directory/$file_name"))) {
      return _git_deploy_file_cache_unserialize($cache_data);
    }
  }

  return NULL;
}

/**
 * Write cache data into the shared file based cache.
 *
 * @see _git_deploy_file_cache_read()
 *
 * @param string $cache_id
 *   Cache identifier.
 * @param array $cache_data
 *   System info additions.
 *
 * @return int|bool
 *   Returns the number of bytes that were written to the file, or FALSE on
 *   failure.
 */
function _git_deploy_file_cache_write($cache_id, $cache_data) {
  if ($directory = variable_get('git_deploy_file_cache_directory', '')) {
    $file_path = $directory . '/' . _git_deploy_file_cache_name($cache_id);
    if (file_exists($file_path)) {
      return TRUE;
    }

    $is_directory = (is_dir($directory) || file_prepare_directory($directory, FILE_CREATE_DIRECTORY));
    $is_writable = (!file_exists($file_path) || is_writable($file_path));
    if ($is_directory && $is_writable) {
      return @file_put_contents($file_path, _git_deploy_file_cache_serialize($cache_data));
    }
  }

  return FALSE;
}

/**
 * Prepare cache data to write into a shared file.
 *
 * @see _git_deploy_file_cache_unserialize()
 *
 * @param array $cache_data
 *   System info additions.
 *
 * @return string
 *   Serialized cache data.
 */
function _git_deploy_file_cache_serialize($cache_data) {
  $text = '';
  foreach ($cache_data as $key => $value) {
    $text .= "$key=$value\n";
  }

  return $text;
}

/**
 * Parse the serialized cache data.
 *
 * @see _git_deploy_file_cache_serialize()
 *
 * @param string $text
 *   Serialized cache data.
 *
 * @return array
 *   System info additions.
 */
function _git_deploy_file_cache_unserialize($text) {
  $cache_data = array();
  $keys = array('version', 'project', 'datetime');
  foreach (explode("\n", trim($text)) as $line) {
    list($key, $value) = explode('=', $line);
    if (in_array($key, $keys)) {
      $cache_data[$key] = check_plain($value);
    }
  }

  return $cache_data;
}

/**
 * Implements hook_flush_caches().
 */
function git_deploy_flush_caches() {
  return array('cache_git_deploy');
}
