Index: info/project_info.info =================================================================== RCS file: info/project_info.info diff -N info/project_info.info --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ info/project_info.info 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,6 @@ +; $Id$ +name = Project info +description = Provides information related to projects, such as dependencies. +package = Project +dependencies[] = project_release +core = 6.x Index: info/project_info.install =================================================================== RCS file: info/project_info.install diff -N info/project_info.install --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ info/project_info.install 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,92 @@ + 'The modules contained by a project release.', + 'fields' => array( + 'module_id' => array( + 'description' => 'Unique module ID.', + 'type' => 'serial', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'rid' => array( + 'description' => 'The {node}.nid of the project_release node that includes a module.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'name' => array( + 'description' => 'The machine readable name of a module contained by the release.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + 'default' => '', + ), + 'title' => array( + 'description' => 'The human readable name of a module as contained in the module .info file.', + 'type' => 'varchar', + 'length' => 255, + 'not null' => TRUE, + ), + 'description' => array( + 'description' => 'A description of a module as contained in the module .info file.', + 'type' => 'text', + 'size' => 'medium' + ), + ), + 'primary key' => array('module_id'), + 'indexes' => array( + 'rid' => array('rid'), + 'name' => array('name'), + ), + ); + + $schema['project_info_dependency'] = array( + 'description' => 'The dependencies of a module.', + 'fields' => array( + 'module_id' => array( + 'description' => 'ID of a module.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + 'dependency_id' => array( + 'description' => 'ID of a module that the module is dependent upon.', + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + ), + ), + 'primary key' => array('module_id', 'dependency_id'), + ); + + return $schema; +} + +/** + * Implementation of hook_install(). + */ +function project_info_install() { + drupal_install_schema('project_info'); +} + +/** + * Implementation of hook_uninstall(). + */ +function project_info_uninstall() { + drupal_uninstall_schema('project_info'); +} Index: info/project_info.package.inc =================================================================== RCS file: info/project_info.package.inc diff -N info/project_info.package.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ info/project_info.package.inc 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,226 @@ += PROJECT_INFO_MINIMUM_API) { + // Clear previous records for the release. + project_info_package_clear($rid); + + $info = project_info_package_parse($info_files); + + // Store list of modules. + $records = project_info_package_list_store($rid, $info); + + // Store list of dependencies. + project_info_package_info_process($rid, $records, $info); + } +} + +/** + * Parse all the .info files. + * + * @param $info_files + * List of module .info files. + * @return + * Associative array of module information keyed by module name and merged + * with an array of defaults. + */ +function project_info_package_parse(array $info_files) { + $defaults = array( + 'name' => 'Unknown', + 'description' => '', + 'dependencies' => array(), + ); + + $info = array(); + foreach ($info_files as $file) { + $module = basename($file, '.info'); + $info[$module] = drupal_parse_info_file($file) + $defaults; + + // Remove any version identifiers from depenencies since those are not + // processed yet. + foreach ($info[$module]['dependencies'] as &$dependency) { + list($dependency) = explode(' ', $dependency, 2); + } + } + return $info; +} + +/** + * Clear the list of modules and dependencies for a release. + * + * @param $rid + * The project release ID. + */ +function project_info_package_clear($rid) { + db_query('DELETE FROM {project_info_dependency} + WHERE module_id IN ( + SELECT module_id + FROM {project_info_module} + WHERE rid = %d + )', $rid); + + db_query('DELETE FROM {project_info_module} WHERE rid = %d', $rid); +} + +/** + * Store the list of modules for a release. + * + * @param $rid + * The project release ID. + * @param $info + * Associative array of information from module .info files keyed by module + * name and merged with a set of detaults from project_info_package_parse(). + * @see project_info_package_parse() + */ +function project_info_package_list_store($rid, array $info) { + $records = array(); + foreach ($info as $module => $module_info) { + $info = array( + 'rid' => $rid, + 'name' => $module, + 'title' => $module_info['name'], + 'description' => $module_info['description'], + ); + drupal_write_record('project_info_module', $info); + $records[$module] = $info; + } + return $records; +} + +/** + * Determine and store the list of dependencies for a release. + * + * @param $rid + * The project release ID. + * @param $info + * Associative array of information from module .info files keyed by module + * name and merged with a set of detaults from project_info_package_parse(). + */ +function project_info_package_info_process($rid, array $records, array $info) { + foreach ($info as $module => $module_info) { + project_info_package_info_process_dependencies($rid, $records[$module], $module_info['dependencies']); + } +} + +/** + * Store the list of dependencies for a release. + * + * @param $rid + * The project release ID. + * @param $module + * Module information. + * @param $dependencies + * List of dependency module names. + */ +function project_info_package_info_process_dependencies($rid, array $module, array $dependencies) { + // Load the release and determine is core API version. + $release = node_load($rid); + $api_tid = project_info_package_core_api($release); + + foreach ($dependencies as $dependency) { + // 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. + $releases = project_info_package_releases_get($dependency, $api_tid); + $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. + break; + } + } + + // Store the dependency if a best release was found. + if ($best_release) { + $info = array( + 'module_id' => $module['module_id'], + 'dependency_id' => $best_release['module_id'], + ); + drupal_write_record('project_info_dependency', $info); + } + } +} + +/** + * Get the releases that contain a module and are compatible with an API tid. + * + * @param $module + * Module name. + * @param $api_tid + * Core API compatibility tid. + * @return + * List of releases with keys: 'module_id', 'rid'. + */ +function project_info_package_releases_get($module, $api_tid) { + $result = db_query("SELECT DISTINCT p.module_id, p.rid + FROM {project_info_module} p + INNER JOIN {project_release_nodes} r + ON p.rid = r.nid + INNER JOIN {term_node} t + ON p.rid = t.nid + WHERE t.tid = %d + AND p.name = '%s' + ORDER BY r.version DESC, t.vid DESC", $api_tid, $module); + + $releases = array(); + while ($release = db_fetch_array($result)) { + $releases[] = $release; + } + return $releases; +} + +/** + * Attempt to determine the Drupal core API tid. + * + * @param $node + * Node object. + * @return + * Core API tid to which the node belongs, otherwise FALSE. + */ +function project_info_package_core_api($node) { + static $api_tids = array(); + + if (!isset($api_tids[$node->nid])) { + $api_tids[$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_tids[$node->nid] = $term->tid; + } + } + } + } + return $api_tids[$node->nid]; +} Index: info/project_info.batch.inc =================================================================== RCS file: info/project_info.batch.inc diff -N info/project_info.batch.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ info/project_info.batch.inc 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,272 @@ + 'fieldset', + '#title' => t('Filter'), + ); + $form['filter']['api'] = array( + '#type' => 'checkboxes', + '#title' => t('API'), + '#description' => t('Select the core 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_INFO_MINIMUM_API) { + $form['filter']['api']['#default_value'][] = $term->tid; + } + } + + $form['op'] = array( + '#type' => 'submit', + '#value' => t('Submit'), + ); + + return $form; +} + +/** + * Ensure that the directory constant has been set. + */ +function project_info_batch_form_validate($form, &$form_state) { + if (!PROJECT_INFO_BATCH_DIRECTORY) { + form_set_error('op', t('The constant PROJECT_INFO_BATCH_DIRECTORY must be set.')); + } +} + +/** + * Start the batch to process batch releases. + */ +function project_info_batch_form_submit($form, &$form_state) { + $tids = array(); + foreach ($form_state['values']['api'] as $tid => $selected) { + if ($selected) { + $tids[] = $tid; + } + } + + $batch = array( + 'title' => t('Processing releases'), + 'operations' => array( + array('project_info_batch_batch_operation_parse', array($tids)), + array('project_info_batch_batch_operation_dependencies', array()), + ), + 'finished' => 'project_info_batch_batch_finished', + 'init_message' => t('Determining releases to parse...'), + 'file' => drupal_get_path('module', 'project_info') . '/project_info.batch.inc', + ); + batch_set($batch); +} + +/** + * Parse the modules contained within a project and their dependencies. + */ +function project_info_batch_batch_operation_parse(array $tids, &$context) { + if (!isset($context['sandbox']['max'])) { + // First iteration, initialize working values. + $context['sandbox']['max'] = project_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_info'); + + $releases = project_info_batch_releases($tids, $context['sandbox']['release_id']); + foreach ($releases as $release) { + if (($dependencies = project_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_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_info'); + + $i = 0; + foreach ($context['results']['dependencies'] as $rid => $modules_dependencies) { + $modules = project_info_module_list_get($rid); + foreach ($modules_dependencies as $module => $dependencies) { + project_info_package_info_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_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'); + } +} + +/** + * 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_info_batch_process_release(array $release) { + $directory_original = getcwd(); + + // Use batch directory. + chdir(PROJECT_INFO_BATCH_DIRECTORY); + + // Clean directory. + exec('rm -r *'); + + // Checkout release. + 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_INFO_BATCH_DIRECTORY . '/' . $directory, '\.info$'); + $info_files = array(); + foreach ($files as $file) { + $info_files[] = $file->filename; + } + $info = project_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_info_package_clear($release['nid']); + + // Store the list of modules contained by the project. + project_info_package_list_store($release['nid'], $info); + + return $dependencies; +} + +/** + * Select all releases that are compatible with the API tids. + * + * @return + * List of release information: rid, project uri, and cvs directory. + */ +function project_info_batch_releases(array $tids, $release_id, $count = FALSE) { + $sql = 'SELECT ' . ($count ? 'COUNT(r.nid)' : 'r.nid, r.tag, p.uri, cp.directory') . ' + 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 (' . db_placeholders($tids, 'int') . ') + 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: info/project_info.module =================================================================== RCS file: info/project_info.module diff -N info/project_info.module --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ info/project_info.module 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,140 @@ + 'Parse batch releases', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('project_info_batch_form'), + 'access arguments' => array('project_info batch parse'), + 'file' => 'project_info.batch.inc', + 'type' => MENU_CALLBACK, + ); + + return $items; +} + +/** + * Implementation of hook_perm(). + */ +function project_info_perm() { + return array( + 'project_info batch parse', + ); +} + +/** + * Load the list of modules contained by a project release. + * + * @param $rid + * Project release ID. + * @return + * Associative array of modules keyed by module name and containing array + * keys: 'name', 'title', 'description'. + */ +function project_info_module_list_get($rid) { + $result = db_query('SELECT * FROM {project_info_module} WHERE rid = %d', $rid); + $modules = array(); + while ($module = db_fetch_array($result)) { + $modules[$module['name']] = $module; + } + return $modules; +} + +/** + * Load a module. + * + * @param $module_id + * Module ID to load. + * @return + * Array of module information. + */ +function project_info_module_get($module_id) { + static $modules = array(); + + if (!isset($modules[$module_id])) { + $result = db_query('SELECT * FROM {project_info_module} WHERE module_id = %d', $module_id); + $modules[$module_id] = db_fetch_array($result); + } + return $modules[$module_id]; +} + +/** + * Get a list of dependency modules for a module. + * + * @param $module_id + * Module ID to get dependencies for. + * @return + * List of dependencies. + */ +function project_info_module_depdencies_get($module_id) { + $result = db_query('SELECT dependency_id FROM {project_info_dependency} WHERE module_id = %d', $module_id); + $dependencies = array(); + while ($dependency = db_result($result)) { + $dependencies[] = project_info_module_get($dependency); + } + return $dependencies; +} + +/** + * Recursively determine the list of dependencies of a release. + * + * @param $rid + * Project release ID. + * @return + * List of dependencies of the release. + */ +function project_info_dependency_list_get($rid) { + $modules = project_info_module_list_get($rid); + return project_info_dependency_list_get_modules($modules); +} + +/** + * Recursively determine the list of dependencies for a list of modules. + * + * @param array $modules + * List of modules to determine the dependencies for. + * @param $dependencies + * (Internal) Array of already determined dependencies. + * @return + * Associative array of dependencies keyed by module name and containing all + * module info keys: 'module_id', 'rid', 'name', 'title', and 'description'. + */ +function project_info_dependency_list_get_modules(array $modules, array $dependencies = array()) { + foreach ($modules as $module) { + // Get all dependencies of the module. + $dependencies_new = project_info_module_depdencies_get($module['module_id']); + + // Add the new dependencies to the list of dependencies and remove the + // dependencies that have already been added. + foreach ($dependencies_new as $module_name => $dependency_new) { + if (!isset($dependencies[$module_name])) { + $dependencies[$module_name] = $dependency_new; + } + else { + unset($dependencies_new[$module_name]); + } + } + + // Process the dependencies of all the new dependency modules. + $dependencies = project_info_dependency_list_get_modules($dependencies_new, $dependencies); + } + return $dependencies; +}