Index: release/package-release-nodes.php =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/project/release/package-release-nodes.php,v retrieving revision 1.68 diff -u -p -r1.68 package-release-nodes.php --- release/package-release-nodes.php 30 Jul 2010 20:42:27 -0000 1.68 +++ release/package-release-nodes.php 14 Oct 2010 21:12:02 -0000 @@ -316,6 +316,8 @@ function package_release_core($type, $ni global $tmp_dir, $repositories, $dest_root, $dest_rel; global $cvs, $tar, $gzip, $rm; + module_load_include('drupal.inc', 'project_release'); + if (!drupal_chdir($tmp_dir)) { return 'error'; } @@ -353,6 +355,9 @@ function package_release_core($type, $ni } } + // Process the module .info files. + package_release_info_process_all($nid, $info_files, $project_short_name, $version); + if (!drupal_exec("$tar -c --file=- $release_file_id | $gzip -9 --no-name > $full_dest_tgz")) { return 'error'; } @@ -379,6 +384,8 @@ function package_release_contrib($type, global $drush, $drush_make_dir; global $license, $trans_install; + module_load_include('drupal.inc', 'project_release'); + // Files to ignore when checking timestamps: $exclude = array('.', '..', 'LICENSE.txt'); @@ -433,6 +440,9 @@ function package_release_contrib($type, } } + // Process the module .info files. + package_release_info_process_all($nid, $info_files, $project_short_name, $version); + // Link not copy, since we want to preserve the date... if (!drupal_exec("$ln -sf $license $project_short_name/LICENSE.txt")) { return 'error'; Index: release/project_release.batch.inc =================================================================== RCS file: release/project_release.batch.inc diff -N release/project_release.batch.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ release/project_release.batch.inc 3 Sep 2010 20:28:37 -0000 @@ -0,0 +1,203 @@ + 'fieldset', + '#title' => t('Filter'), + ); + $form['filter']['api'] = array( + '#type' => 'checkboxes', + '#title' => t('API'), + '#description' => t('Select the API versions that releases must be compatible with.'), + '#options' => array(), + '#default_value' => array(), + ); + $terms = project_release_get_api_taxonomy(); + foreach ($terms as $term) { + $form['filter']['api']['#options'][$term->tid] = $term->name; + // Defaultly select + if (($major = array_shift(explode('.', $term->name, 2))) && $major >= PROJECT_RELEASE_INFO_MINIMUM_API) { + $form['filter']['api']['#default_value'][] = $term->tid; + } + } + $form['op'] = array('#type' => 'submit', '#value' => t('Submit')); + + return $form; +} + +/** + * Start the batch to process batch releases. + */ +function project_release_info_batch_form_submit($form, &$form_state) { + $tids = array_keys(array_filter($form_state['values']['api'])); + + $batch = array( + 'title' => t('Processing releases'), + 'operations' => array( + array('project_release_info_batch_batch_operation_parse', array($tids)), + array('project_release_info_batch_batch_operation_dependencies', array()), + ), + 'finished' => 'project_release_info_batch_batch_finished', + 'init_message' => t('Determining releases to parse...'), + 'file' => drupal_get_path('module', 'project_release_info') . '/project_release_info.batch.inc', + ); + batch_set($batch); +} + +/** + * Parse the modules contained within a project and their dependencies. + */ +function project_release_info_batch_batch_operation_parse(array $tids, &$context) { + if (!isset($context['sandbox']['max'])) { + // First iteration, initialize working values. + $context['sandbox']['max'] = project_release_info_batch_releases($tids, 0, TRUE); + $context['sandbox']['release_id'] = 0; + $context['sandbox']['progress'] = 0; + $context['results']['dependencies'] = array(); + $context['results']['count'] = 0; + } + + // Load the package include that contains functions to save and parse data. + module_load_include('package.inc', 'project_release'); + + $releases = project_release_info_batch_releases($tids, $context['sandbox']['release_id']); + foreach ($releases as $release) { + if (($dependencies = project_release_info_batch_process_release($release)) === FALSE) { + $context['results']['error'] = $release; + $context['finished'] = 1; + return; + } + $context['results']['dependencies'][$release['nid']] = $dependencies; + $context['sandbox']['release_id'] = $release['nid']; + $context['sandbox']['progress']++; + $context['results']['count']++; + } + + $context['message'] = t('Last parsed %module release %tag (@progress of @max).', array( + '%module' => $release['uri'], + '%tag' => $release['tag'], + '@progress' => number_format($context['sandbox']['progress']), + '@max' => number_format($context['sandbox']['max']), + )); + + $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max']; +} + +/** + * Determine dependencies of releases. + */ +function project_release_info_batch_batch_operation_dependencies(&$context) { + if (!isset($context['sandbox']['max'])) { + $context['sandbox']['max'] = count($context['results']['dependencies']); + $context['sandbox']['progress'] = 0; + } + + // Load the package include that contains functions to save and parse data. + module_load_include('package.inc', 'project_release'); + + $i = 0; + foreach ($context['results']['dependencies'] as $rid => $modules_dependencies) { + $modules = project_release_info_module_list_get($rid); + foreach ($modules_dependencies as $module => $dependencies) { + project_release_info_batch_process_dependencies($rid, $modules[$module], $dependencies); + } + + // Remove already processed releases. + unset($context['results']['dependencies'][$rid]); + + $context['sandbox']['progress']++; + + // Only process 20 releases in a single batch operation. + if ($i++ == 20) { + break; + } + } + + $context['message'] = t('Processed @progress of @max release dependencies.', array( + '@progress' => number_format($context['sandbox']['progress']), + '@max' => number_format($context['sandbox']['max']), + )); + + $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max']; +} + +/** + * Display relevant finished message after batch run. + */ +function project_release_info_batch_batch_finished($success, $results, $operations) { + if ($success && !isset($results['error'])) { + drupal_set_message(t('Processed @count release(s).', array( + '@count' => number_format($results['count']), + ))); + } + else { + drupal_set_message(t('Failed to process @module release #@rid.', array( + '@module' => $results['error']['uri'], + '@rid' => $results['error']['nid'], + )), 'error'); + } +} + +/** + * Select all releases that are compatible with the API tids. + * + * @return + * List of release information: rid, project uri, and cvs directory. + */ +function project_release_info_batch_releases(array $tids, $release_id, $count = FALSE) { + $fields = ($count ? 'COUNT(r.nid)' : 'r.nid, r.tag, p.uri, cp.directory'); + $placeholders = db_placeholders($tids, 'int'); + $sql = "SELECT $fields + FROM {project_release_nodes} r + INNER JOIN {node} n ON r.nid = n.nid + INNER JOIN {term_node} t ON n.vid = t.vid + INNER JOIN {project_projects} p ON r.pid = p.nid + INNER JOIN {cvs_projects} cp ON p.nid = cp.nid + WHERE t.tid IN ($placeholders) AND r.nid > %d"; + $args = array_merge($tids, array($release_id)); + + if ($count) { + return db_result(db_query($sql, $args)); + } + + $sql .= ' ORDER BY r.nid'; + $result = db_query_range($sql, $args, 0, 10); + $releases = array(); + while ($release = db_fetch_array($result)) { + $releases[] = $release; + } + return $releases; +} + Index: release/project_release.drupal.inc =================================================================== RCS file: release/project_release.drupal.inc diff -N release/project_release.drupal.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ release/project_release.drupal.inc 3 Sep 2010 20:53:39 -0000 @@ -0,0 +1,380 @@ += PROJECT_RELEASE_INFO_MINIMUM_API) { + // Clear previous records for the release. + project_release_info_package_clear($rid); + + $info = package_release_info_parse($info_files); + + // Store list of components. + $records = project_release_info_package_list_store($rid, $info); + + // Process list of dependencies to determine the release related to each + // dependency. + package_release_info_process($rid, $records, $info); + } +} + +/** + * Parse all the .info files. + * + * @param $info_files + * List of component .info files. + * + * @return + * Associative array of component information keyed by component name and + * merged with an array of defaults. + */ +function package_release_info_parse(array $info_files) { + $defaults = array( + 'name' => 'Unknown', + 'description' => '', + 'dependencies' => array(), + 'test_dependencies' => array(), + ); + + $info = array(); + foreach ($info_files as $file) { + $component = basename($file, '.info'); + $info[$component] = drupal_parse_info_file($file) + $defaults; + + // Change info keys to suite project_release_info. + $info[$component]['title'] = $info[$component]['name']; + $info[$component]['name'] = $component; + } + return $info; +} + +/** + * Determine and store the list of dependencies for a release. + * + * @param $rid + * The project release ID. + * @param $records + * List of saved component information from + * project_release_info_package_list_store(). + * @param $info + * Associative array of information from component .info files keyed by component + * name and merged with a set of detaults from package_release_info_parse(). + */ +function package_release_info_process($rid, array $records, array $info) { + foreach ($info as $component => $component_info) { + // Merge required dependencies and test dependencies into one array. + $dependencies = array_merge($component_info['dependencies'], $component_info['test_dependencies']); + package_release_info_process_dependencies($rid, $records[$component], $dependencies); + } +} + +/** + * Store the list of dependencies for a component of a release. + * + * @param $rid + * The project release ID. + * @param $component + * Component information, returned from project_release_info_package_list_store(). + * @param $dependencies + * List of dependency component names. + */ +function package_release_info_process_dependencies($rid, array $component, array $dependencies) { + // Load the release and determine is core API version. + $release = node_load($rid); + if (!($api_term = package_release_info_core_api($release))) { + wd_err('ERROR: No core release API term found.'); + return; + } + + // Preprocess dependencies looking for duplications, or overrides. Since test + // dependencies were appended to regular dependencies they should override. + $dependency_info = array(); + foreach ($dependencies as $dependency) { + $info = drupal_parse_dependency($dependency, $api_term->name); + if (empty($info['name'])) { + wd_err('ERROR: Invalid dependency found [' . $dependency . '] of [' . $component['name'] . '].'); + return; + } + $dependency_info[$info['name']] = $info; + } + + $dependency_components = array(); + foreach ($dependency_info as $dependency => $info) { + $releases = package_release_info_releases_get($dependency, $api_term->tid); + + // Check for dependency version restriction information. + if (empty($info['versions'])) { + // Cycle through the releases made by the project until a dev branch is + // found of the latest stable series that matches the core API. The + // releases are in descending order from the largest release version to + // the smallest release version. + $best_release = NULL; + foreach ($releases as $release) { + $release_node = node_load($release['rid']); + if ($release_node->project_release['rebuild']) { + // Release represents a dev branch, store it. + $best_release = $release; + } + elseif ($best_release) { + // Release represents a stable branch, since a dev branch has already + // been found then stop and use the dev branch as the best branch. + $dependency_components[$dependency] = $best_release['component_id']; + break; + } + } + + // If no releases found, then generate error. + if (empty($dependency_components[$dependency])) { + wd_err('ERROR: No development release with a corresponding stable release was found that ' . + 'matches the requirements for dependency [' . $dependency . '] of [' . $component['name'] . '].'); + return; + } + } + else { + // Cycle through each release from highest version number to lowest and + // select the first dev release that matches the criteria. + foreach ($releases as $release) { + $release_node = node_load($release['rid']); + + // Get the release version string without the Drupal API part. + $parts = explode('-', $release_node->project_release['version']); + unset($parts[0]); + $current = implode('-', $parts); + + // Compare the release version string against the requirements. + $valid = TRUE; + foreach ($info['versions'] as $required) { + if ((isset($required_version['op']) && !version_compare($required, $required['version'], $required['op']))) { + $valid = FALSE; + break; + } + } + + // If all the requirements were met then use the release. + if ($valid) { + $dependency_components[$dependency] = $release['component_id']; + } + } + + // If no releases found, then generate error. + if (empty($dependency_components[$dependency])) { + wd_err('ERROR: No release found that matches requirements [' . trim($info['original_version']) . + '] for dependency [' . $dependency . '] of [' . $component['name'] . '].'); + return; + } + } + } + + // Store dependencies for component. + project_release_info_package_dependencies_store($component['component_id'], $dependency_components); +} + +/** + * Get the releases that contain a component and are compatible with an API. + * + * @param $component + * Component name. + * @param $api_tid + * Core API compatibility tid. + * + * @return + * List of releases with keys: 'component_id', 'rid'. + */ +function package_release_info_releases_get($component, $api_tid) { + $result = db_query("SELECT DISTINCT p.component_id, p.release_nid + FROM {project_release_component} p + INNER JOIN {project_release_nodes} r ON p.release_nid = r.nid + INNER JOIN {term_node} t ON p.release_nid = t.nid + WHERE t.tid = %d AND p.name = '%s' + ORDER BY r.version DESC, t.vid DESC + ", $api_tid, $component); + + $releases = array(); + while ($release = db_fetch_array($result)) { + $releases[] = $release; + } + return $releases; +} + +/** + * Attempt to determine the Drupal core API term. + * + * @param $node + * Node object. + * + * @return + * Core API term to which the node belongs, otherwise FALSE. + */ +function package_release_info_core_api($node) { + static $api_terms = array(); + + if (!isset($api_terms[$node->nid])) { + $api_terms[$node->nid] = FALSE; + + if (!empty($node->taxonomy)) { + // Relase API version vocabulary. + $api_vid = _project_release_get_api_vid(); + + foreach ($node->taxonomy as $tid => $term) { + if ($term->vid == $api_vid) { + $api_terms[$node->nid] = $term; + } + } + } + } + return $api_terms[$node->nid]; +} + +/** + * Parse a dependency for comparison by drupal_check_incompatibility(). + * + * @param $dependency + * A dependency string, for example 'foo (>=7.x-4.5-beta5, 3.x)'. + * + * @return + * An associative array with three keys: + * - 'name' includes the name of the thing to depend on (e.g. 'foo'). + * - 'original_version' contains the original version string (which can be + * used in the UI for reporting incompatibilities). + * - 'versions' is a list of associative arrays, each containing the keys + * 'op' and 'version'. 'op' can be one of: '=', '==', '!=', '<>', '<', + * '<=', '>', or '>='. 'version' is one piece like '4.5-beta3'. + * Callers should pass this structure to drupal_check_incompatibility(). + * + * @see drupal_check_incompatibility() + */ +function drupal_parse_dependency($dependency, $core_api) { + // We use named subpatterns and support every op that version_compare + // supports. Also, op is optional and defaults to equals. + $p_op = '(?P!=|==|=|<|<=|>|>=|<>)?'; + // Core version is always optional: 7.x-2.x and 2.x is treated the same. + $p_core = '(?:' . preg_quote($core_api . '.x') . '-)?'; + $p_major = '(?P\d+)'; + // By setting the minor version to x, branches can be matched. + $p_minor = '(?P(?:\d+|x)(?:-[A-Za-z]+\d+)?)'; + $value = array(); + $parts = explode('(', $dependency, 2); + $value['name'] = trim($parts[0]); + if (isset($parts[1])) { + $value['original_version'] = ' (' . $parts[1]; + foreach (explode(',', $parts[1]) as $version) { + if (preg_match("/^\s*$p_op\s*$p_core$p_major\.$p_minor/", $version, $matches)) { + $op = !empty($matches['operation']) ? $matches['operation'] : '='; + if ($matches['minor'] == 'x') { + // Drupal considers "2.x" to mean any version that begins with + // "2" (e.g. 2.0, 2.9 are all "2.x"). PHP's version_compare(), + // on the other hand, treats "x" as a string; so to + // version_compare(), "2.x" is considered less than 2.0. This + // means that >=2.x and <2.x are handled by version_compare() + // as we need, but > and <= are not. + if ($op == '>' || $op == '<=') { + $matches['major']++; + } + // Equivalence can be checked by adding two restrictions. + if ($op == '=' || $op == '==') { + $value['versions'][] = array('op' => '<', 'version' => ($matches['major'] + 1) . '.x'); + $op = '>='; + } + } + $value['versions'][] = array('op' => $op, 'version' => $matches['major'] . '.' . $matches['minor']); + } + } + } + return $value; +} + +/** + * Checkout a release and determine relevant information. + * + * Locate all .info files contained within the project, determine the names of + * the module(s) contained within the project and store the dependencies for + * later processing. + */ +function project_release_info_batch_process_release(array $release) { + $directory_original = getcwd(); + + // Use batch directory. + chdir(PROJECT_RELEASE_INFO_BATCH_DIRECTORY); + + // Clean directory. + exec('rm -r *'); + + // Checkout release. + $release['tag'] = escapeshellcmd($release['tag']); + $release['uri'] = escapeshellcmd($release['uri']); + + if ($release['uri'] == 'drupal') { + $url = ':pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal'; + $branch = $release['tag']; + $module = $release['uri']; + $directory = $release['uri']; + } + else { + $url = ':pserver:anonymous:anonymous@cvs.drupal.org:/cvs/drupal-contrib'; + $branch = $release['tag']; + $module = 'contributions/modules/' . $release['uri']; + $directory = $release['uri']; + } + exec("cvs -z6 -Q -d$url checkout -d$directory -r $branch $module", $output, $status); + + // Return to original directory. + chdir($directory_original); + + // Check for CVS checkout failure. + if ($status != 0) { + return FALSE; + } + + // Scan checkout for .info files and create a list in the same format that + // project release uses, so that standard API functions can be used. + $files = file_scan_directory(PROJECT_RELEASE_INFO_BATCH_DIRECTORY . '/' . $directory, '\.info$'); + $info_files = array(); + foreach ($files as $file) { + $info_files[] = $file->filename; + } + $info = project_release_info_package_parse($info_files); + + $dependencies = array(); + foreach ($info as $module => $module_info) { + $dependencies[$module] = $module_info['dependencies']; + } + + // Clear previous records for the release. + project_release_info_package_clear($release['nid']); + + // Store the list of modules contained by the project. + project_release_info_package_list_store($release['nid'], $info); + + return $dependencies; +} + +function project_release_info_batch_process_dependencies($rid, array $component, array $dependencies) { + package_release_info_process_dependencies($rid, $component, $dependencies); +} + Index: release/project_release.info =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/project/release/project_release.info,v retrieving revision 1.5 diff -u -p -r1.5 project_release.info --- release/project_release.info 12 Jan 2009 22:24:03 -0000 1.5 +++ release/project_release.info 3 Sep 2010 19:50:54 -0000 @@ -7,3 +7,5 @@ dependencies[] = taxonomy dependencies[] = upload dependencies[] = views core = 6.x + +test_dependencies[] = pift Index: release/project_release.install =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/project/release/project_release.install,v retrieving revision 1.32 diff -u -p -r1.32 project_release.install --- release/project_release.install 19 Aug 2010 16:31:17 -0000 1.32 +++ release/project_release.install 14 Oct 2010 21:13:58 -0000 @@ -380,6 +380,72 @@ function project_release_schema() { 'primary key' => array('nid', 'uid'), ); + $schema['project_release_component'] = array( + 'description' => 'The component(s) contained by a project release.', + 'fields' => array( + 'component_id' => array( + 'description' => 'Unique component ID.', + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'release_nid' => array( + 'description' => 'The {node}.nid of the project_release node that includes a component.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'name' => array( + 'description' => 'The machine readable name of a component.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'title' => array( + 'description' => 'The human readable name of a component', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + ), + 'description' => array( + 'description' => 'A description of a component.', + 'type' => 'text', + 'size' => 'medium' + ), + ), + 'primary key' => array('component_id'), + 'indexes' => array( + 'release_nid' => array('release_nid'), + 'name' => array('name'), + ), + ); + + $schema['project_release_dependency'] = array( + 'description' => 'The dependencies of a component.', + 'fields' => array( + 'source_id' => array( + 'description' => 'ID of a component.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'target_id' => array( + 'description' => 'ID of a component that the component is dependent upon.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'dependency_type' => array( + 'description' => '@todo', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + ), + 'primary key' => array('source_id', 'target_id'), + ); + return $schema; } @@ -784,3 +850,77 @@ function project_release_update_6010() { return $ret; } + +/** + * Create project dependency info tables. + */ +function project_release_update_6011() { + $ret = array(); + if (!db_table_exists('project_release_component')) { + db_create_table($ret, 'project_release_component', array( + 'fields' => array( + 'component_id' => array( + 'description' => 'Unique component ID.', + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'release_nid' => array( + 'description' => 'The {node}.nid of the project_release node that includes a component.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'name' => array( + 'description' => 'The machine readable name of a component.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'title' => array( + 'description' => 'The human readable name of a component', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + ), + 'description' => array( + 'description' => 'A description of a component.', + 'type' => 'text', + 'size' => 'medium' + ), + ), + 'primary key' => array('component_id'), + 'indexes' => array( + 'release_nid' => array('release_nid'), + 'name' => array('name'), + ), + )); + } + if (!db_table_exists('project_release_dependency')) { + db_create_table($ret, 'project_release_dependency', array( + 'fields' => array( + 'source_id' => array( + 'description' => 'ID of a component.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'target_id' => array( + 'description' => 'ID of a component that the component is dependent upon.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'dependency_type' => array( + 'description' => '@todo', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + ), + 'primary key' => array('source_id', 'target_id'), + )); + } + return $ret; +} Index: release/project_release.module =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/project/release/project_release.module,v retrieving revision 1.155 diff -u -p -r1.155 project_release.module --- release/project_release.module 22 Aug 2010 00:14:51 -0000 1.155 +++ release/project_release.module 14 Oct 2010 21:14:40 -0000 @@ -30,6 +30,15 @@ function project_release_init() { } /** + * Implementation of hook_perm(). + */ +function project_release_perm() { + return array( + 'parse project_release_info batch', + ); +} + +/** * Implementation of hook_menu() * @ingroup project_release_core */ @@ -66,6 +75,14 @@ function project_release_menu() { 'type' => MENU_NORMAL_ITEM, 'file' => 'includes/admin.settings.inc', ); + $items['admin/project/project_release_info/batch'] = array( + 'title' => 'Parse batch releases', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('project_release_info_batch_form'), + 'access arguments' => array('parse project_release_info batch'), + 'file' => 'project_release.batch.inc', + 'type' => MENU_CALLBACK, + ); // Redirect node/add/project_release/* to node/add/project-release. $items['node/add/project_release'] = array( @@ -2177,3 +2194,117 @@ function project_release_views_default_v _project_release_views_default_views_alter($views); } +/** + * @defgroup project_release_info Project release information + * @{ + */ + +/** + * Loads a component. + * + * @param $component_id + * Component ID to load. + * + * @return + * Array of component information. + */ +function project_release_component_load($component_id) { + static $components = array(); + + if (!isset($components[$component_id])) { + $result = db_query('SELECT * FROM {project_release_component} WHERE component_id = %d', $component_id); + $components[$component_id] = db_fetch_array($result); + } + return $components[$component_id]; +} + +/** + * Loads the list of components contained by a project release. + * + * @param $release_nid + * Project release ID. + * + * @return + * Associative array of components keyed by component name and containing + * array keys: 'name', 'title', 'description'. + */ +function project_release_get_components($release_nid) { + $result = db_query('SELECT * FROM {project_release_component} WHERE release_nid = %d', $release_nid); + $components = array(); + while ($component = db_fetch_array($result)) { + $components[$component['name']] = $component; + } + return $components; +} + +/** + * Gets a list of dependency components for a component. + * + * @param $component_id + * Component ID to get dependencies for. + * + * @return + * List of dependencies. + */ +function project_release_component_dependencies_load_all($component_id) { + $result = db_query('SELECT target_id FROM {project_release_dependency} WHERE source_id = %d', $component_id); + $dependencies = array(); + while ($dependency_id = db_result($result)) { + $dependencies[] = project_release_component_load($dependency_id); + } + return $dependencies; +} + +/** + * Recursively determines the list of dependencies of a release. + * + * @param $release_nid + * Project release ID. + * + * @return + * List of dependencies of the release. + */ +function project_release_get_dependencies($release_nid) { + $components = project_release_get_components($release_nid); + return _project_release_get_component_dependencies($components); +} + +/** + * Recursively determines the list of dependencies for a list of components. + * + * @param array $components + * List of components to determine the dependencies for. + * @param $dependencies + * (Internal) Array of already determined dependencies. + * + * @return + * Associative array of dependencies keyed by component name and containing + * all component info keys: 'component_id', 'rid', 'name', 'title', and + * 'description'. + */ +function _project_release_get_component_dependencies(array $components, array $dependencies = array()) { + foreach ($components as $component) { + // Get all dependencies of the component. + $dependencies_new = project_release_component_dependencies_load_all($component['component_id']); + + // Add the new dependencies to the list of dependencies and remove the + // dependencies that have already been added. + foreach ($dependencies_new as $component_name => $dependency_new) { + if (!isset($dependencies[$component_name])) { + $dependencies[$component_name] = $dependency_new; + } + else { + unset($dependencies_new[$component_name]); + } + } + + // Process the dependencies of all the new dependency components. + $dependencies = _project_release_get_component_dependencies($dependencies_new, $dependencies); + } + return $dependencies; +} + +/** + * @} End of "defgroup project_release_info". + */ + Index: release/project_release.package.inc =================================================================== RCS file: release/project_release.package.inc diff -N release/project_release.package.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ release/project_release.package.inc 3 Sep 2010 20:55:02 -0000 @@ -0,0 +1,69 @@ + $component_info) { + $info = array( + 'rid' => $release_nid, + 'name' => $component_info['name'], + 'title' => $component_info['title'], + 'description' => $component_info['description'], + ); + drupal_write_record('project_release_component', $info); + $records[$component] = $info; + } + return $records; +} + +/** + * Store a list of dependencies for a component. + * + * @param $component_id + * The component ID to which the dependencies relate. + * @param array $dependencies + * List of dependencies component IDs. + */ +function project_release_info_package_dependencies_store($component_id, array $dependencies) { + foreach ($dependencies as $dependency_id) { + // Store the dependency if a best release was found. + $info = array( + 'source_id' => $component_id, + 'target_id' => $dependency_id, + ); + drupal_write_record('project_release_dependency', $info); + } +} Index: release/project_release.test =================================================================== RCS file: release/project_release.test diff -N release/project_release.test --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ release/project_release.test 3 Sep 2010 21:00:18 -0000 @@ -0,0 +1,210 @@ +assertEqual($component['component_id'], $component_id, 'Proper component ID found'); + $this->assertEqual($component['rid'], $rid, 'Proper release ID found'); + $this->assertEqual($component['name'], $project, 'Proper name found'); + $this->assertEqual($component['title'], ucfirst(str_replace('_', ' ', $project)), 'Proper title found'); + $this->assertTrue($component['description'], 'Description found'); + } +} + +/** + * Ensure that the storage and retrieval API works correctly. + */ +class ProjectInfoAPITestCase extends ProjectInfoTestCase { + + public static function getInfo() { + return array( + 'name' => 'API', + 'description' => 'Ensure that the storage and retrieval API works correctly.', + 'group' => 'Project info', + ); + } + + /** + * Ensure that the storage and retrieval API works correctly. + */ + protected function testAPI() { + module_load_include('package.inc', 'project_release'); + + // Ensure that default data is provided properly. + $component_id = 1; + foreach ($this->releases as $project => $release) { + $components = project_release_get_components($release->nid); + $this->assertEqual(count($components), 2, 'Two components found'); + + $name = $project; + foreach ($components as $component) { + $this->assertComponent($component, $component_id, $release->nid, $name); + + $component = project_release_component_load($component['component_id']); + $this->assertComponent($component, $component_id++, $release->nid, $name); + + $name .= $project; + } + + // Ensure that dependencies are provided properly. + $dependencies = project_release_get_dependencies($release->nid); + if ($project == 'drupal') { + $this->assertFalse($dependencies, 'Drupal core has no dependencies'); + } + else if ($project == 'foo') { + $this->assertEqual($dependencies[0]['component_id'], 5, 'Foo depends on bar'); + $dependencies = project_release_component_dependencies_load_all(3); + $this->assertEqual($dependencies[0]['component_id'], 5, 'Foo depends on bar'); + } + else { + $this->assertEqual($dependencies[0]['component_id'], 4, 'Bar depends on foo'); + $dependencies = project_release_component_dependencies_load_all(6); + $this->assertEqual($dependencies[0]['component_id'], 4, 'Bar depends on foo'); + } + } + } +} + +/** + * Ensure that the Drupal parsing functions work properly. + */ +class ProjectInfoDrupalTestCase extends ProjectInfoTestCase { + + public static function getInfo() { + return array( + 'name' => 'Drupal', + 'description' => 'Ensure that the Drupal parsing functions work properly.', + 'group' => 'Project info', + ); + } + + /** + * Ensure that the Drupal parsing functions work properly. + */ + protected function testDrupalParsing() { + $this->process(); + + // Ensure that the components were parsed properly. + $component_id = 7; + $components = project_release_get_components($this->releases['foo']->nid); + $this->assertComponent($components['project_release_info_foo'], $component_id++, $this->releases['foo']->nid, 'project_release_info_foo'); + + $components = project_release_get_components($this->releases['bar']->nid); + $this->assertComponent($components['project_release_info_bar'], $component_id++, $this->releases['bar']->nid, 'project_release_info_bar'); + $this->assertComponent($components['project_release_info_barbar'], $component_id++, $this->releases['bar']->nid, 'project_release_info_barbar'); + + debug(project_release_get_components($this->releases['foo']->nid)); + debug(project_release_get_components($this->releases['bar']->nid)); + + debug(project_release_component_dependencies_load_all(7)); + debug(project_release_component_dependencies_load_all(8)); + debug(project_release_component_dependencies_load_all(9)); + + // Ensure that dependencies were parsed properly. + debug(project_release_get_dependencies($this->releases['foo']->nid)); + debug(project_release_get_dependencies($this->releases['bar']->nid)); + + // Should have error since there is no stable release of project_release_info_foo. + $this->assertPackageError('ERROR: No development release with a corresponding stable release was ' . + 'found that matches the requirements for dependency [project_release_info_foo] of [project_release_info_bar].'); + + $this->createTag('bar', 'DRUPAL-6--1-0'); +// $this->drupalGet('node/add/project-release/' . $this->projects['bar']->nid); + + $this->drupalPost('node/add/project-release/' . $this->projects['bar']->nid, array(), t('Next')); + + $edit = array( + 'body' => $this->randomString(32), + ); + $this->drupalPost(NULL, $edit, t('Save')); + + $this->process(); + + $this->assertNoPackageErrors(); + } + + protected function process() { + // Remove all previously existing project_release_info data. + db_query('DELETE FROM {project_release_dependency}'); + db_query('DELETE FROM {project_release_component}'); + + // Reset packaging errors. + wd_err(FALSE, TRUE); + + // Load Drupal specific implementation. + module_load_include('drupal.inc', 'project_release'); + + // Parse test modules related to foo and bar projects. + $info_files = array( + drupal_get_path('module', 'project_release_info_foo') . '/project_release_info_foo.info', + ); + package_release_info_process_all($this->releases['foo']->nid, $info_files, 'foo', '6.x-1.0'); + + $info_files = array( + drupal_get_path('module', 'project_release_info_bar') . '/project_release_info_bar.info', + drupal_get_path('module', 'project_release_info_barbar') . '/project_release_info_barbar.info', + ); + package_release_info_process_all($this->releases['bar']->nid, $info_files, 'bar', '6.x-1.2'); + } + + protected function createTag($project, $tag, $branch = FALSE) { + db_query("INSERT INTO {cvs_tags} VALUES (%d, '%s', %d, %d)", $this->projects[$project]->nid, $tag, $branch ? 1 : 0, time()); + } + + protected function assertPackageError($message) { + $found = FALSE; + foreach (wd_err() as $error) { + if ($error == $message) { + $found = TRUE; + break; + } + } + return $this->assertTrue($found, 'Found package error [' . $message . ']'); + } + + protected function assertNoPackageErrors() { + if (!$this->assertFalse(wd_err(), 'No packaging errors')) { + foreach (wd_err() as $error) { + $this->fail($error); + } + } + } +} + +function wd_err($message = FALSE, $reset = FALSE) { + static $messages = array(); + + if ($reset) { + $messages = array(); + } + + if ($message) { + $messages[] = $message; + } + return $messages; +} Index: release/tests/bar/project_release_info_bar.info =================================================================== RCS file: release/tests/bar/project_release_info_bar.info diff -N release/tests/bar/project_release_info_bar.info --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ release/tests/bar/project_release_info_bar.info 3 Sep 2010 20:14:19 -0000 @@ -0,0 +1,9 @@ +; $Id$ +name = Project release info bar +description = Test module for project_release_info. (do not enable) +package = Project +version = 1.0 +core = 6.x +dependencies[] = project_release_info_foo + +test_dependencies[] = project_release_info_barbar Index: release/tests/bar/project_release_info_bar.module =================================================================== RCS file: release/tests/bar/project_release_info_bar.module diff -N release/tests/bar/project_release_info_bar.module --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ release/tests/bar/project_release_info_bar.module 3 Sep 2010 19:50:12 -0000 @@ -0,0 +1,9 @@ +