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 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Provides schema for storing project information.
+ *
+ * @author Jimmy Berry ("boombatower", http://drupal.org/user/214218)
+ */
+
+/**
+ * Implementation of hook_schema().
+ */
+function project_info_schema() {
+  $schema = array();
+
+  $schema['project_info_module'] = array(
+    'description' => '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 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Provides information related to projects, such as dependencies.
+ *
+ * @author Jimmy Berry ("boombatower", http://drupal.org/user/214218)
+ */
+
+/**
+ * Parse Drupal info files for project information.
+ *
+ * @param $rid
+ *   The project release ID.
+ * @param $info_files
+ *   List of .info files to be processed.
+ * @param $uri
+ *   The short name of the project.
+ * @param $version
+ *   The version of the release being processed.
+ */
+function project_info_package_info_process_all($rid, array $info_files, $uri, $version) {
+  // Only process .info files for 6.x API and above.
+  if (($major = array_shift(explode('.', $version, 2))) && $major >= 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 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Parses batch releases to build project_info tables.
+ *
+ * @author Jimmy Berry ("boombatower", http://drupal.org/user/214218)
+ */
+
+/**
+ * Directory to checkout modules during processing.
+ *
+ * The directory should be empty since it will be cleared before each checkout.
+ */
+define('PROJECT_INFO_BATCH_DIRECTORY', '');
+
+/**
+ * Legacy parsing form.
+ *
+ * Provide a form to select the API versions that releases must be compatible
+ * with and initiate the batch process to perform the parsing.
+ */
+function project_info_batch_form(&$form_state) {
+  $form = array();
+
+  $api_vid = _project_release_get_api_vid();
+  $form['filter'] = array(
+    '#type' => '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 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Provides information related to projects, such as dependencies.
+ *
+ * @author Jimmy Berry ("boombatower", http://drupal.org/user/214218)
+ */
+
+/**
+ * Minimum API compatibility to process.
+ */
+define('PROJECT_INFO_MINIMUM_API', 6);
+
+/**
+ * Implementation of hook_menu().
+ */
+function project_info_menu() {
+  $items = array();
+
+  $items['admin/project/project_info/batch'] = array(
+    'title' => '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;
+}
