'Project release integration', 'description' => 'Configure how project release nodes will be integrated with version control systems.', 'page callback' => 'drupal_get_form', 'page arguments' => array('versioncontrol_release_admin_form'), 'access callback' => 'versioncontrol_admin_access', 'type' => MENU_LOCAL_TASK, ); return $items; } /** * Return TRUE if the given backend implements all functionality that is * required for proper version control / release node integration. */ function versioncontrol_release_is_supported_backend($vcs) { $required_functions = array( 'get_item', 'get_parallel_items', 'export_directory', ); foreach ($required_functions as $function) { if (!versioncontrol_backend_implements($vcs, $function)) { return FALSE; } } return TRUE; } /** * Form callback for 'admin/project/versioncontrol-settings/project-release': * Global settings for this module. */ function versioncontrol_release_admin_form(&$form_state) { $form['versioncontrol_release_message_new_release_branch'] = array( '#title' => t('Message when new releases are added from a branch'), '#type' => 'textarea', '#default_value' => variable_get('versioncontrol_release_message_new_release_branch', ''), '#description' => t('The message to show to project maintainers when they add a new development snapshot release from a branch. Leave empty to not show any specific message.'), ); $form['versioncontrol_release_message_new_release_tag'] = array( '#title' => t('Message when new releases are added from a tag'), '#type' => 'textarea', '#default_value' => variable_get('versioncontrol_release_message_new_release_tag', ''), '#description' => t('The message to show to project maintainers when they add a new official release from a tag. Leave empty to not show any specific message.'), ); return system_settings_form($form); } /** * Return an object of version number values based on the given VCS label, * or FALSE if no version object can be constructed. * * If there is a local, site-specific implementation, use that. Otherwise, * the tag is inserted directly into the "extra" field in the version object. */ function versioncontrol_release_get_version_from_label($label, $project_node) { if (function_exists('versioncontrol_release_local_get_version_from_label')) { return versioncontrol_release_local_get_version_from_label($label, $project_node); } // Try to provide a sensible default behavior by extracting number parts // from the label name and using any non-numeric suffix as version extra. $fields = array('version_major', 'version_minor', 'version_patch'); $version = array(); $matched = preg_match_all('/[^\.\-]+/', $label['name'], $matches, PREG_OFFSET_CAPTURE); $label_parts = $matched ? $matches[0] : array(); while (!empty($fields)) { // Try to fill all regular version fields. $current_field = array_shift($fields); while (!empty($label_parts)) { $label_part = array_shift($label_parts); if (is_numeric($label_part[0])) { $version[$current_field] = $label_part[0]; break; // next version field } elseif (!empty($version)) { // Non-numeric field after a numeric one was already assigned: // that sounds like a suffix like "beta1" or similar. Put back the // current label part so that it can still be extracted afterwards, // and fill up the regular fields in order to exit both loops. array_unshift($label_parts, $label_part); while (!empty($fields)) { $remaining_field = array_shift($fields); $version[$remaining_field] = FALSE; } break; } } } // Remove any fields padded with FALSE. $positive_version_numbers = FALSE; foreach ($version as $field => $number) { if ($number === FALSE) { unset($version[$field]); } elseif ($number > 0) { $positive_version_numbers = TRUE; } } if (!empty($positive_version_numbers)) { if ($label['type'] == VERSIONCONTROL_OPERATION_BRANCH) { // Branches always get a "-dev" appended. Looks good, and helps with // reverse engineering the version number. $version['version_extra'] = 'dev'; } elseif (!empty($label_parts)) { // The next label part is the one after the last numeric part, like "beta1". // From that position on, use the rest of $label['name'] as version extra. $label_part = array_shift($label_parts); $version['version_extra'] = substr($label['name'], $label_part[1]); } return (object) $version; } else { return empty($version) ? FALSE : ((object) $version); } } /** * Return an array of labels that are still unused for the given project, * with the label_id of each label array used as array key. */ function versioncontrol_release_get_possible_labels($project_node) { if (!versioncontrol_project_node_uses_versioncontrol($project_node)) { return array(); } $project = $project_node->versioncontrol_project; $repository = versioncontrol_get_repository($project['repo_id']); if (empty($repository)) { return array(); } if (!versioncontrol_release_is_supported_backend($repository['vcs'])) { return array(); } $directory_item = versioncontrol_get_item($repository, $project['directory']); if (empty($directory_item)) { return array(); } $parallel_items = versioncontrol_get_parallel_items($repository, $directory_item); if (empty($parallel_items)) { return array(); } $result = db_query('SELECT DISTINCT(label.label_id), label.name, label.repo_id, label.type, rlabel.release_nid FROM {versioncontrol_labels} label LEFT JOIN {versioncontrol_release_labels} rlabel ON label.label_id = rlabel.label_id WHERE rlabel.project_nid = %d ORDER BY label.type DESC, label.name DESC', $project_node->nid); $db_labels = array(); $released_labels = array(); while ($dblabel = db_fetch_object($result)) { $db_labels[$dblabel->repo_id][$dblabel->type][$dblabel->name] = $dblabel->label_id; if (!empty($dblabel->release_nid)) { $released_labels[$dblabel->label_id] = TRUE; } } foreach ($parallel_items as $item) { $label = versioncontrol_get_item_selected_label($repository, $item); if (!empty($label)) { if (isset($db_labels[$label['repo_id']][$label['type']][$label['name']])) { $label['label_id'] = $db_labels[$label['repo_id']][$label['type']][$label['name']]; } else { $label = versioncontrol_ensure_label($repository, $label); } if (!isset($released_labels[$label['label_id']])) { $labels[$label['label_id']] = $label; } } } return $labels; } function versioncontrol_release_get_label_caption($label, $version, $project_node) { if (empty($version)) { return t('!name', array('!name' => $label['name'])); } else { return t('!name (!version)', array( '!name' => $label['name'], '!version' => project_release_get_version($version, $project_node), )); } } /** * Implementation of hook_form_alter(). * We do this instead of hook_form_[form_id]_alter() because it gets called * later, which means we can really unset all elements added to the forms. * (As required by the label selector form for the "add" form.) */ function versioncontrol_release_form_alter(&$form, &$form_state, $form_id) { if ($form_id == 'project_release_node_form') { if (function_exists('versioncontrol_release_local_project_release_form_alter')) { versioncontrol_release_local_project_release_form_alter($form, $form_state); } // If we're not adding it, call a separate method that just worries // about how to alter the edit form, since the add form is so complex. if (arg(1) == 'add') { versioncontrol_release_project_release_form_alter_add($form, $form_state); } else { $release_node = $form['#node']; $release_label = $release_node->versioncontrol_release['label']; if (!empty($release_label)) { versioncontrol_release_project_release_form_alter_edit( $form, $form_state, $release_node, $release_label ); } } } } /** * Implementation of hook_form_alter() for the "add" version of the * release node form. */ function versioncontrol_release_project_release_form_alter_add(&$form, &$form_state) { $project_node = $form['project']['#value']; // Check to see if this project has a repository set. If not, then don't // alter the form so that the project can have releases just as if // versioncontrol_releases.module were not enabled at all. if (!versioncontrol_project_node_uses_versioncontrol($project_node)) { return; } $project = $project_node->versioncontrol_project; $repository = versioncontrol_get_repository($project['repo_id']); if (empty($repository)) { return; } if (!versioncontrol_release_is_supported_backend($repository['vcs'])) { return; } if (empty($project_node->project_release['releases'])) { return; // This project does not support releases, nothing to alter. } if (isset($form_state['storage']['versioncontrol_release_label_id'])) { $label_id = $form_state['storage']['versioncontrol_release_label_id']; } if (isset($form['#versioncontrol_release_label_id'])) { $label_id = $form['#versioncontrol_release_label_id']; } if (!empty($label_id)) { $label = db_fetch_array(db_query( 'SELECT label_id, repo_id, name, type FROM {versioncontrol_labels} WHERE label_id = %d', $label_id )); } // cvs.module fetches the tag from project_release as follows. // Imho, label information should not be stored in project_release's tables, // so versioncontrol_release refrains from accessing that information. //elseif (isset($form_state['values']['project_release']['tag'])) { // $label_name = $form_state['values']['project_release']['tag']; //} if (empty($label)) { // Page #1: No release tag or branch has been selected yet. // Clear the whole form in favor of a simple label selector. versioncontrol_release_project_release_form_alter_add_select_label($form, $form_state, $project_node); } else { // Page #2: The release tag or branch has been selected. // Alter the "add" form accordingly. $form['#versioncontrol_release_label_id'] = $label_id; versioncontrol_release_project_release_form_alter_add_node_form($form, $form_state, $project_node, $label); } } /** * Implementation of hook_form_alter() for the "add" version of the * release node form as long as no release tag or branch has been selected. */ function versioncontrol_release_project_release_form_alter_add_select_label(&$form, &$form_state, $project_node) { // Rip out everything else that might be in this form. _versioncontrol_release_project_release_form_alter_unset_all($form); unset($form['validate_version']); // Gather possible values for a label selector. $possible_labels = versioncontrol_release_get_possible_labels($project_node); $tags_t = t('Tags'); $branches_t = t('Branches'); $label_options = array(); foreach ($possible_labels as $label_id => $label) { $category = ($label['type'] == VERSIONCONTROL_OPERATION_TAG) ? $tags_t : $branches_t; $version = versioncontrol_release_get_version_from_label($label, $project_node); $label_caption = versioncontrol_release_get_label_caption($label, $version, $project_node); $label_options[$category][$label_id] = $label_caption; } if (!empty($label_options)) { $form['versioncontrol_release'] = array( '#type' => 'markup', '#value' => '', '#weight' => -4, ); $form['versioncontrol_release']['versioncontrol_release_label_id'] = array( '#type' => 'select', '#title' => t('Release tag or branch'), '#options' => $label_options, '#required' => TRUE, '#description' => t('Select the repository tag or branch (and therefore version number) for this release.'), ); } else { // TODO: make this a setting or generate the text in some other way? $err = t('There are no tags for this module that do not already have a release associated with them.'); $err .= '
' . t('To create a release, you must first create either a new tag on one of the existing branches for this project, or you must add a new branch.') . '
'; $err .= '' . t('Once you have created a tag or branch that should be used for your new release, try pressing the %retry link to continue.', array('%retry' => t('Retry'))) . '
'; drupal_set_message($err, 'warning'); unset($form['buttons']['preview']); $form['retry'] = array( '#type' => 'markup', '#value' => l(t('Retry'), 'node/add/project_release/' . $project_node->nid), ); } } /** * Helper function to unset all the elements in the release node form * that we don't want if we're on one of the preliminary pages to get * the tag and/or version info before we present the final form. */ function _versioncontrol_release_project_release_form_alter_unset_all(&$form, $whitelist = array()) { foreach (element_children($form) as $child) { if ($child != 'buttons' && (empty($form[$child]['#type']) || ($form[$child]['#type'] != 'hidden' && $form[$child]['#type'] != 'value' && $form[$child]['#type'] != 'token' && (!in_array($child, $whitelist))))) { $form[$child]['#access'] = FALSE; } } // Change the "Preview" button to "Next" and hide the "Save" button. $form['buttons']['preview'] = array( '#type' => 'submit', '#value' => t('Next'), '#weight' => 50, '#submit' => array('versioncontrol_release_form_next_submit'), ); $form['buttons']['submit']['#access'] = FALSE; } /** * Submit callback for the "Next" button on the release label selection form. */ function versioncontrol_release_form_next_submit($form, &$form_state) { if (isset($form_state['values']['versioncontrol_release_label_id'])) { $form_state['storage']['versioncontrol_release_label_id'] = $form_state['values']['versioncontrol_release_label_id']; } $vocab_id = _project_release_get_api_vid(); if (isset($form_state['values']['taxonomy'][$vocab_id])) { $form_state['storage']['project_release']['version_api_tid'] = $form_state['values']['taxonomy'][$vocab_id]; } foreach (array('version', 'version_major', 'version_minor', 'version_patch', 'version_extra') as $field) { if (empty($form_state['storage']['project_release'][$field]) && isset($form_state['values']['project_release'][$field])) { $form_state['storage']['project_release'][$field] = $form_state['values']['project_release'][$field]; $form_state['node']['project_release'][$field] = $form_state['values']['project_release'][$field]; } } $form_state['rebuild'] = TRUE; } /** * Implementation of hook_form_alter() for the "add" version of the * release node form once the release tag or branch has been selected. */ function versioncontrol_release_project_release_form_alter_add_node_form(&$form, &$form_state, $project_node, $label) { $fields = array('version_major', 'version_minor', 'version_patch'); $vocab_id = _project_release_get_api_vid(); ///TODO: private function? baaad. $label_type_string = ($label['type'] == VERSIONCONTROL_OPERATION_TAG) ? t('tag') : t('branch'); $in_use = db_result(db_query( 'SELECT COUNT(label_id) FROM {versioncontrol_release_labels} WHERE project_nid = %d AND label_id = %d', $project_node->nid, $label['label_id'] )); if ($in_use) { form_set_error('tag', t('The !labeltype you have selected is already in use by another release.', array('!labeltype' => $label_type_string))); } $version = versioncontrol_release_get_version_from_label($label, $project_node); if (empty($version)) { // This is for labels that cannot automatically be assigned a // version object (and in turn, a version string). This includes default // branches like HEAD or master, as well as labels which have been left // without a fixed version by the site operators. $version = array(); foreach ($fields as $field) { if (isset($form_state['storage']['project_release'][$field])) { $form_val = $form_state['storage']['project_release'][$field]; if ($form_val !== '' && (is_numeric($form_val) || $form_val == 'x')) { $version[$field] = $form_val; } } } if (isset($form_state['storage']['project_release']['version_extra']) && $form_state['storage']['project_release']['version_extra'] !== '') { $version['version_extra'] = $form_state['storage']['project_release']['version_extra']; } if (isset($form_state['storage']['project_release']['version_api_tid'])) { $version_api_term_id = $form_state['storage']['project_release']['version_api_tid']; if (is_numeric($version_api_term_id) && !empty($version_api_term_id) && db_result(db_query('SELECT COUNT(*) FROM {term_data} WHERE tid = %d AND vid = %d', $version_api_term_id, $vocab_id))) { $version['version_api_tid'] = $version_api_term_id; } } // If we could retrieve a version, great. Otherwise, keep stuff as is. if (!empty($version)) { $version = (object) $version; if (function_exists('versioncontrol_project_local_version_is_valid') && !versioncontrol_project_local_version_is_valid($version, $label, $project_node)) { $version = FALSE; } } } if (!empty($version)) { $version_string = project_release_get_version($version, $project_node); // Stash this in a form value so it'll make it through to validation // where the title of the release node is set. $form['#versioncontrol_release_version'] = array_merge( (array) $version, array('version' => $version_string) ); } unset($form['tag']); // jpetso: why? what does it do otherwise? please explain. $label_options[$label['label_id']] = $label['name']; $form['versioncontrol_release'] = array( '#type' => 'markup', '#value' => '', '#weight' => -4, ); $form['versioncontrol_release']['versioncontrol_release_label_id'] = array( '#type' => 'select', '#title' => t('Repository !labeltype', array('!labeltype' => $label_type_string)), '#options' => $label_options, '#default_value' => $label['label_id'], '#required' => TRUE, ); if (!empty($version_string)) { // Since this is the final page, turn this into a fieldset with all the // nice float/clear goodness. $form['versioncontrol_release']['#type'] = 'fieldset'; $form['versioncontrol_release']['#collapsible'] = TRUE; $form['versioncontrol_release']['#title'] = t('Release identification'); $form['versioncontrol_release']['#prefix'] = '