patch_manager.module in Patch manager 7
Same filename and directory in other branches
Patch manager provides developers with tools for managing patches.
File
patch_manager.moduleView 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
Constants
Name | Description |
---|---|
PATCH_MANAGER_ERROR | |
PATCH_MANAGER_SUCCESS | Return values for the patch function |
PATCH_MANAGER_WARNING |