You are here

patch_manager.module in Patch manager 7

Same filename and directory in other branches
  1. 6 patch_manager.module

Patch manager provides developers with tools for managing patches.

File

patch_manager.module
View source
<?php

/**
 * @file
 * Patch manager provides developers with tools for managing patches.
 */

/**
 * Return values for the patch function
 *
 * patch's exit status is 0 if all hunks are applied successfully,
 * 1 if some hunks cannot be applied,
 * and 2 if  there  is  more  serious trouble.
 */
define('PATCH_MANAGER_SUCCESS', 0);
define('PATCH_MANAGER_WARNING', 1);
define('PATCH_MANAGER_ERROR', 2);

/**
 * Implements hook_menu().
 */
function patch_manager_menu() {
  $items = array();
  $items['admin/structure/patch'] = array(
    'title' => 'Patches',
    'description' => 'Patch management of core and contributed modules.',
    'access arguments' => array(
      'administer patch manager',
    ),
    'page callback' => 'patch_manager_list',
    'type' => MENU_NORMAL_ITEM,
  );
  $items['admin/structure/patch/list'] = array(
    'title' => 'List',
    'description' => 'List patches stored with the patch manager.',
    'access arguments' => array(
      'administer patch manager',
    ),
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
  $items['admin/structure/patch/configure'] = array(
    'title' => 'Configure',
    'description' => 'Configure settings for patch manager.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'patch_manager_settings_form',
    ),
    'access arguments' => array(
      'administer patch manager',
    ),
    'weight' => 30,
    'type' => MENU_LOCAL_TASK,
  );
  $items['admin/structure/patch/add'] = array(
    'title' => 'Add',
    'description' => 'Add a new patch.',
    'page callback' => 'drupal_goto',
    'page arguments' => array(
      'node/add/patch',
    ),
    'access arguments' => array(
      'administer patch manager',
    ),
    'weight' => 20,
    'type' => MENU_LOCAL_TASK,
  );
  $items['admin/structure/patch/scan'] = array(
    'title' => 'Scan',
    'description' => 'Scan for patches.',
    'page callback' => 'patch_manager_page_scan',
    'access arguments' => array(
      'administer patch manager',
    ),
    'weight' => 40,
    'type' => MENU_LOCAL_TASK,
  );
  return $items;
}

/**
 * Implements hook_permission().
 */
function patch_manager_permission() {
  return array(
    'administer patch manager' => array(
      'title' => t('administer patch manager'),
      'description' => t('Patch manager provides developers with tools for managing patches.'),
    ),
  );
}

/**
 * Implements hook_node_access().
 */
function patch_manager_node_access($node, $op, $account) {
  switch ($op) {
    case 'create':
    case 'update':
    case 'delete':
      return user_access('administer patch manager', $account);
  }
}

/**
 * Implements hook_form().
 */
function patch_manager_form(&$node) {
  $type = node_type_get_type($node);
  $form['title'] = array(
    '#type' => 'textfield',
    '#description' => t('Short description of the patch.'),
    '#title' => check_plain($type->title_label),
    '#required' => TRUE,
    '#size' => 50,
    '#weight' => -6,
    '#attributes' => array(
      'style' => 'width: auto',
    ),
    '#default_value' => !empty($node->title) ? $node->title : NULL,
  );
  return $form;
}

/**
 * Implements hook_views_api().
 */
function patch_manager_views_api() {
  return array(
    'api' => 3,
    'path' => drupal_get_path('module', 'patch_manager'),
  );
}

/**
 * Implements hook_node_info().
 *
 * @todo Remove default publish to front page
 */
function patch_manager_node_info() {
  $items = array();
  $items['patch'] = array(
    'name' => t('Patch'),
    'base' => 'patch_manager',
    'description' => t('A <em>patch</em> is used by developers to store a patch file, and keep track of information related to that patch.'),
    'title_label' => t('Patch name'),
    'has_body' => 1,
    'body_label' => t('Patch notes'),
    'locked' => 0,
  );
  return $items;
}

/**
 * Implements hook_node_view().
 */
function patch_manager_node_view($node, $view_mode = 'full') {
  if ($node->type === 'patch' && user_access('administer patch manager')) {
    $form = drupal_get_form('patch_manager_node_actions_form', $node);
    $node->content['patch_manager'] = array(
      '#value' => $form,
    );
  }
}

/**
 * Implements hook_action_info().
 */
function patch_manager_action_info() {
  return array(
    'patch_manager_apply_action' => array(
      'type' => 'node',
      'label' => t('Apply patch'),
      'configurable' => FALSE,
      'triggers' => array(
        'any' => TRUE,
      ),
    ),
    'patch_manager_revert_action' => array(
      'type' => 'node',
      'label' => t('Revert patch'),
      'configurable' => FALSE,
      'triggers' => array(
        'any' => TRUE,
      ),
    ),
  );
}

/**
 * Implements hook_field_formatter_info().
 */
function patch_manager_field_formatter_info() {
  $formats = array();
  $formats['issuelink'] = array(
    'label' => t('Issue link'),
    'field types' => array(
      'text',
    ),
    'settings' => array(
      'multiple values' => FIELD_BEHAVIOR_DEFAULT,
    ),
  );
  return $formats;
}

/**
 * Implements hook_field_formatter_view().
 */
function patch_manager_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
  $elements = array();

  // Get the issue status from drupal.org.
  // We couldn't use hook_field_formatter_prepare_view for it, because
  // the items come nested into redundant array.
  $issue_statuses =& drupal_static(__FUNCTION__, array());
  foreach ($items as $delta => $item) {
    $issue = $item['value'];

    // Get the issue status.
    if (empty($issue_statuses[$issue])) {

      // Check cache.
      if (!($issue_status = cache_get("patch_manager:issue_status:{$issue}")->data)) {

        // Request to drupal.org to get the status.
        $response_json = drupal_http_request("http://drupal.org/node/{$issue}/project-issue/json");

        // Parse JSON-response to get item we need.
        $response = drupal_json_decode($response_json->data);
        $issue_status = $response['status'];

        // Set cache for future requests.
        cache_set("patch_manager:issue_status:{$issue}", $issue_status);
      }

      // Set static value.
      $issue_statuses[$issue] = $issue_status;
    }

    // Set the status.
    $item['status'] = $issue_statuses[$issue];

    // Set the element.
    $elements[$delta] = array(
      '#markup' => theme_patch_manager_issuelink($item),
    );
  }
  return $elements;
}

/**
 * Configuration form for patches
 */
function patch_manager_settings_form($form, &$form_state) {
  $form = array();
  $form['patch_manager_path_patch'] = array(
    '#type' => 'textfield',
    '#title' => t('Patch binary path'),
    '#description' => t('Enter the full path to your systems patch binary.'),
    '#default_value' => variable_get('patch_manager_path_patch', '/usr/bin/patch'),
    '#validate' => array(
      'patch_manager_settings_form_validate',
    ),
    '#required' => TRUE,
  );
  return system_settings_form($form);
}

/**
 * Form submit handler
 */
function patch_manager_settings_form_validate($form, $form_state) {
  $status = _patch_manager_status($form_state['values']['patch_manager_path_patch']);
  if (!$status) {
    form_set_error('patch_manager_path_patch', t('Unable to execute patch binary, check that the binary exists and is executable.'));
  }
}

/**
 * Discover patches
 *
 * This is more for debugging than anything useful.
 */
function patch_manager_page_scan() {

  // Scan for patches
  $headers = array(
    t('Patch'),
    t('Path'),
  );
  $dir = dirname($_SERVER['SCRIPT_FILENAME']);
  $mask = '/.patch$|.diff$/';
  $patches = file_scan_directory($dir, $mask);
  $rows = array();
  foreach ($patches as $patch) {
    $filename = str_replace("{$dir}/", '', $patch->filename);
    $rows[] = array(
      $patch->filename,
      l($filename, $filename),
    );
  }
  $output = theme('table', array(
    'header' => $headers,
    'rows' => $rows,
  ));

  // Registered patches
  $headers = array(
    t('Title'),
    t('Patch'),
    t('Module'),
    t('Issue'),
    t('Description'),
    t('Patchdir'),
  );
  $rows = patch_manager_list_patches();
  $output .= theme('table', array(
    'header' => $headers,
    'rows' => $rows,
  ));
  return $output;
}

/**
 * Render the list depending on what we have available (simple, or views_bulk_operations).
 */
function patch_manager_list() {
  $display_id = module_exists('views_bulk_operations') ? 'bulklist' : 'simplelist';
  $view = views_get_view('patches');
  if (!$view || !$view
    ->access($display_id)) {
    return drupal_not_found();
  }
  drupal_set_title($view
    ->get_title());
  return $view
    ->preview($display_id);
}

/**
 * Get the list of patches.
 *
 * This function invokes hook_patch().
 */
function patch_manager_list_patches() {
  $list = module_invoke_all('patch');
  drupal_alter('patch', $list);
  return $list;
}

/**
 * Provide a form for nodes to perform simple actions
 */
function patch_manager_node_actions_form($form, $form_state, $node) {
  $form = array();
  $form['#node'] = $node;
  $form['patch_manager_apply'] = array(
    '#type' => 'submit',
    '#value' => 'Apply patch',
    '#submit' => array(
      'patch_manager_node_actions_form_apply_submit',
    ),
  );
  $form['patch_manager_revert'] = array(
    '#type' => 'submit',
    '#value' => 'Revert patch',
    '#submit' => array(
      'patch_manager_node_actions_form_reverse_submit',
    ),
  );
  return $form;
}

/**
 * Reverse the patch in a node view context
 *
 * @todo Reduce code duplication
 */
function patch_manager_node_actions_form_reverse_submit($form, $form_state) {
  $result = patch_manager_runpatch($form['#node'], '-R');
  if ($result->status === PATCH_MANAGER_SUCCESS) {
    drupal_set_message(t('All parts of the patch were successfully reverted.'));
  }
  else {
    _patch_manager_display_errors($result);
  }
}

/**
 * Apply the patch in a node view context
 *
 * @todo Reduce code duplication
 */
function patch_manager_node_actions_form_apply_submit($form, $form_state) {
  $result = patch_manager_runpatch($form['#node']);
  if ($result->status === PATCH_MANAGER_SUCCESS) {
    drupal_set_message(t('All parts of the patch were applied successfully.'));
  }
  else {
    _patch_manager_display_errors($result);
  }
}

/**
 * Run a patch operation
 * 
 * @todo Get field value by appropriate way.
 */
function patch_manager_runpatch($node, $flags = '') {

  // Pull the values from the node
  // TODO Get field value by appropriate way.
  $patchfile = $node->field_patch['und'][0]['uri'];
  $module = $node->field_module['und'][0]['value'];

  // Get the path from which to apply this patch
  // We start with the path to drupal core, then if it's a contrib module
  // try and find that patch. If we can't find the contrib module, stay with
  // drupal core.
  $root = dirname($_SERVER['SCRIPT_FILENAME']);
  if ($module !== 'core') {
    if ($modulepath = drupal_get_path('module', $module)) {
      $root = realpath($modulepath);
    }
    else {
      drupal_set_message(t('Unable to find the specified module ... trying anyway.'), 'warning');
    }
  }

  // Give the patchfile an absolute path
  $patchfile = drupal_realpath($patchfile);

  // Run the command
  $patch = variable_get('patch_manager_path_patch', '/usr/bin/patch');
  foreach (array(
    '-p1',
    '-p0',
  ) as $pn) {
    $cmd = sprintf('%s %s --verbose %s -d %s -i %s', $patch, $pn, $flags, escapeshellarg($root), escapeshellarg($patchfile));
    exec($cmd, $output, $ret);
    if ($ret < 2) {
      break;

      // ret = 0: success, ret = 1: partial apply
    }
  }
  watchdog('patch_manager', 'Ran shell command (%command) which finished with status @status', array(
    '%command' => $cmd,
    '@status' => $ret,
  ));

  // Return the results
  $status = new stdClass();
  $status->cmd = $cmd;
  $status->output = $output;
  $status->status = (int) $ret;
  return $status;
}

/**
 * Patch apply action
 *
 * @todo Reduce code duplication with below function
 */
function patch_manager_apply_action(&$object, $context = array()) {
  $result = patch_manager_runpatch($object);
  if ($result->status === PATCH_MANAGER_SUCCESS) {
    drupal_set_message(t('Patch (@title) was applied successfully', array(
      '@title' => $object->title,
    )));
  }
  else {
    _patch_manager_display_errors($result);
  }
}

/**
 * Patch revert action
 *
 * @todo Reduce code duplication with above function
 */
function patch_manager_revert_action(&$object, $context = array()) {
  $result = patch_manager_runpatch($object, '-R');
  if ($result->status === PATCH_MANAGER_SUCCESS) {
    drupal_set_message(t('Patch (@title) was reversed successfully', array(
      '@title' => $object->title,
    )));
  }
  else {
    _patch_manager_display_errors($result);
  }
}

/**
 * Check status of the patch binary
 */
function _patch_manager_status($path) {
  if (!$path) {
    $path = variable_get('patch_manager_path_patch', '/usr/bin/patch');
  }
  if (file_exists($path) && is_executable($path)) {
    return TRUE;
  }
  return FALSE;
}

/**
 * Quick and nasty function because I'm not sure what to do with errors
 */
function _patch_manager_display_errors($result) {
  drupal_set_message(t('Patching did not go smoothly.'));
  drupal_set_message(t('This command was issued: %command', array(
    '%command' => $result->cmd,
  )));
  drupal_set_message(t('This was the output from patch: <pre>@output</pre>', array(
    '@output' => implode("\n", $result->output),
  )));
}

/**
 * Theme an issue link
 */
function theme_patch_manager_issuelink($item) {
  $nid = $item['value'];
  if (!$nid) {
    return NULL;
  }

  // Parse out the comment ID
  if (strpos($nid, '/') !== FALSE) {
    list($nid, $comment) = explode('/', $nid);
  }

  // URL options.
  $options = array();
  $options['attributes']['title'] = 'Issue status at drupal.org: ' . $item['status'];

  // Add class for coloring depending on issue status.
  $status = $item['status'];

  // Map issue status with class.
  $map = array(
    'active' => 'state-1',
    'fixed' => 'state-2',
    'closed (duplicate)' => 'state-3',
    'postponed' => 'state-4',
    "closed (won't fix)" => 'state-5',
    'closed (works as designed)' => 'state-6',
    'closed (fixed)' => 'state-7',
    'needs review' => 'state-8',
    'needs work' => 'state-13',
    'reviewed & tested by the communty' => 'state-14',
    'patch (to be ported)' => 'state-15',
    'postponed (maintainer needs more info)' => 'state-16',
    'closed (cannot reproduce)' => 'state-18',
  );
  $class = $map[$status];
  $options['attributes']['class'] = array(
    $class,
  );

  // Add CSS.
  drupal_add_css(drupal_get_path('module', 'patch_manager') . '/patch_manager.css');

  // Display.
  return l($nid, 'http://drupal.org/node/' . $nid, $options);
}

Functions

Namesort descending Description
patch_manager_action_info Implements hook_action_info().
patch_manager_apply_action Patch apply action
patch_manager_field_formatter_info Implements hook_field_formatter_info().
patch_manager_field_formatter_view Implements hook_field_formatter_view().
patch_manager_form Implements hook_form().
patch_manager_list Render the list depending on what we have available (simple, or views_bulk_operations).
patch_manager_list_patches Get the list of patches.
patch_manager_menu Implements hook_menu().
patch_manager_node_access Implements hook_node_access().
patch_manager_node_actions_form Provide a form for nodes to perform simple actions
patch_manager_node_actions_form_apply_submit Apply the patch in a node view context
patch_manager_node_actions_form_reverse_submit Reverse the patch in a node view context
patch_manager_node_info Implements hook_node_info().
patch_manager_node_view Implements hook_node_view().
patch_manager_page_scan Discover patches
patch_manager_permission Implements hook_permission().
patch_manager_revert_action Patch revert action
patch_manager_runpatch Run a patch operation
patch_manager_settings_form Configuration form for patches
patch_manager_settings_form_validate Form submit handler
patch_manager_views_api Implements hook_views_api().
theme_patch_manager_issuelink Theme an issue link
_patch_manager_display_errors Quick and nasty function because I'm not sure what to do with errors
_patch_manager_status Check status of the patch binary

Constants

Namesort descending Description
PATCH_MANAGER_ERROR
PATCH_MANAGER_SUCCESS Return values for the patch function
PATCH_MANAGER_WARNING