Index: release/package-release-nodes.php
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/project/release/package-release-nodes.php,v
retrieving revision 1.62
diff -u -r1.62 package-release-nodes.php
--- release/package-release-nodes.php	3 Dec 2009 01:51:26 -0000	1.62
+++ release/package-release-nodes.php	3 Feb 2010 02:55:12 -0000
@@ -311,6 +311,12 @@
   global $tmp_dir, $repositories, $dest_root, $dest_rel;
   global $cvs, $tar, $gzip, $rm;
 
+  // If project_info module exists then load package.inc.
+  static $project_info;
+  if (!isset($project_info) && ($project_info = module_exists('project_info'))) {
+    module_load_include('drupal.inc', 'project_info');
+  }
+
   if (!drupal_chdir($tmp_dir)) {
     return false;
   }
@@ -348,6 +354,11 @@
     }
   }
 
+  // Allow project_info to process the module .info files.
+  if ($project_info) {
+    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 false;
   }
@@ -374,6 +385,12 @@
   global $drush, $drush_make_dir;
   global $license, $trans_install;
 
+  // If project_info module exists then load package.inc.
+  static $project_info;
+  if (!isset($project_info) && ($project_info = module_exists('project_info'))) {
+    module_load_include('drupal.inc', 'project_info');
+  }
+
   // Files to ignore when checking timestamps:
   $exclude = array('.', '..', 'LICENSE.txt');
 
@@ -428,6 +445,11 @@
     }
   }
 
+  // Allow project_info to process the module .info files.
+  if ($project_info) {
+    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 false;
Index: info/project_info.drupal.inc
===================================================================
RCS file: info/project_info.drupal.inc
diff -N info/project_info.drupal.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ info/project_info.drupal.inc	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,377 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Provides Drupal implementation of project_info parsing and batch API.
+ *
+ * @author Jimmy Berry ("boombatower", http://drupal.org/user/214218)
+ */
+
+module_load_include('package.inc', 'project_info');
+
+/**
+ * Minimum API compatibility to process.
+ */
+define('PROJECT_INFO_MINIMUM_API', 6);
+
+/**
+ * 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 package_release_info_process_all($rid, array $info_files, $uri, $version) {
+  // Only process .info file for modules that meet the minimum API requirement.
+  if (($major = array_shift(explode('.', $version, 2))) && $major >= PROJECT_INFO_MINIMUM_API) {
+    // Clear previous records for the release.
+    project_info_package_clear($rid);
+
+    $info = package_release_info_parse($info_files);
+
+    // Store list of components.
+    $records = project_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_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_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_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;
+  }
+
+  $dependecy_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.
+          $dependecy_components[$dependency] = $best_release['component_id'];
+          break;
+        }
+      }
+
+      // If no releases found, then generate error.
+      if (empty($dependecy_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) {
+          $dependecy_components[$dependency] = $release['component_id'];
+        }
+      }
+
+      // If no releases found, then generate error.
+      if (empty($dependecy_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_info_package_dependencies_store($component['component_id'], $dependecy_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.rid
+                      FROM {project_info_component} 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, $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<operation>!=|==|=|<|<=|>|>=|<>)?';
+  // 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<major>\d+)';
+  // By setting the minor version to x, branches can be matched.
+  $p_minor = '(?P<minor>(?:\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_info_batch_process_release(array $release) {
+  $directory_original = getcwd();
+
+  // Use batch directory.
+  chdir(PROJECT_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_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;
+}
+
+function project_info_batch_process_dependencies($rid, array $component, array $dependencies) {
+  package_release_info_process_dependencies($rid, $component, $dependencies);
+}
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,8 @@
+; $Id$
+name = Project info
+description = Provides component list and component-component dependecy information for releases.
+package = Project
+core = 6.x
+dependencies[] = project_release
+
+test_dependencies[] = pift
Index: info/tests/foo/project_info_foo.info
===================================================================
RCS file: info/tests/foo/project_info_foo.info
diff -N info/tests/foo/project_info_foo.info
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ info/tests/foo/project_info_foo.info	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,6 @@
+; $Id$
+name = Project info foo
+description = Test module for project_info. (do not enable)
+package = Project
+version = 1.0
+core = 6.x
Index: info/tests/bar/project_info_bar.module
===================================================================
RCS file: info/tests/bar/project_info_bar.module
diff -N info/tests/bar/project_info_bar.module
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ info/tests/bar/project_info_bar.module	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,9 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Dummy module file.
+ *
+ * @author Jimmy Berry ("boombatower", http://drupal.org/user/214218)
+ */
Index: info/project_info.test
===================================================================
RCS file: info/project_info.test
diff -N info/project_info.test
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ info/project_info.test	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,210 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Provide test of functionality.
+ *
+ * @author Jimmy Berry ("boombatower", http://drupal.org/user/214218)
+ */
+
+module_load_include('test', 'pift');
+
+/**
+ * Base project_info test case.
+ */
+class ProjectInfoTestCase extends PIFTTestCase {
+
+  /**
+   * Assert that a component has the proper values.
+   *
+   * @param $component
+   *   Component information to check.
+   * @param $component_id
+   *   Component ID.
+   * @param $rid
+   *   Project release ID of which the component is apart.
+   * @param $project
+   *   Project name of which the component is apart.
+   */
+  protected function assertComponent(array $component, $component_id, $rid, $project) {
+    $this->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_info');
+
+    // Ensure that default data is provided properly.
+    $component_id = 1;
+    foreach ($this->releases as $project => $release) {
+      $components = project_info_component_list_get($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_info_component_get($component['component_id']);
+        $this->assertComponent($component, $component_id++, $release->nid, $name);
+
+        $name .= $project;
+      }
+
+      // Ensure that dependencies are provided properly.
+      $dependencies = project_info_dependency_list_get($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_info_component_dependencies_get(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_info_component_dependencies_get(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_info_component_list_get($this->releases['foo']->nid);
+    $this->assertComponent($components['project_info_foo'], $component_id++, $this->releases['foo']->nid, 'project_info_foo');
+
+    $components = project_info_component_list_get($this->releases['bar']->nid);
+    $this->assertComponent($components['project_info_bar'], $component_id++, $this->releases['bar']->nid, 'project_info_bar');
+    $this->assertComponent($components['project_info_barbar'], $component_id++, $this->releases['bar']->nid, 'project_info_barbar');
+
+    debug(project_info_component_list_get($this->releases['foo']->nid));
+    debug(project_info_component_list_get($this->releases['bar']->nid));
+
+    debug(project_info_component_dependencies_get(7));
+    debug(project_info_component_dependencies_get(8));
+    debug(project_info_component_dependencies_get(9));
+
+    // Ensure that dependencies were parsed properly.
+    debug(project_info_dependency_list_get($this->releases['foo']->nid));
+    debug(project_info_dependency_list_get($this->releases['bar']->nid));
+
+    // Should have error since there is no stable release of project_info_foo.
+    $this->assertPackageError('ERROR: No development release with a corresponding stable release was ' .
+      'found that matches the requirements for dependency [project_info_foo] of [project_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_info data.
+    db_query('DELETE FROM {project_info_dependency}');
+    db_query('DELETE FROM {project_info_component}');
+
+    // Reset packaging errors.
+    wd_err(FALSE, TRUE);
+
+    // Load Drupal specific implementation.
+    module_load_include('drupal.inc', 'project_info');
+
+    // Parse test modules related to foo and bar projects.
+    $info_files = array(
+      drupal_get_path('module', 'project_info_foo') . '/project_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_info_bar') . '/project_info_bar.info',
+      drupal_get_path('module', 'project_info_barbar') . '/project_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: info/tests/bar/project_info_bar.info
===================================================================
RCS file: info/tests/bar/project_info_bar.info
diff -N info/tests/bar/project_info_bar.info
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ info/tests/bar/project_info_bar.info	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,9 @@
+; $Id$
+name = Project info bar
+description = Test module for project_info. (do not enable)
+package = Project
+version = 1.0
+core = 6.x
+dependencies[] = project_info_foo
+
+test_dependencies[] = project_info_barbar
Index: info/tests/foo/project_info_foo.module
===================================================================
RCS file: info/tests/foo/project_info_foo.module
diff -N info/tests/foo/project_info_foo.module
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ info/tests/foo/project_info_foo.module	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,9 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Dummy module file.
+ *
+ * @author Jimmy Berry ("boombatower", http://drupal.org/user/214218)
+ */
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,98 @@
+<?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_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,
+      ),
+      'rid' => 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(
+      'rid' => array('rid'),
+      'name' => array('name'),
+    ),
+  );
+
+  $schema['project_info_dependency'] = array(
+    'description' => 'The dependencies of a component.',
+    'fields' => array(
+      'component_id' => array(
+        'description' => 'ID of a component.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+      ),
+      'dependency_id' => array(
+        'description' => 'ID of a component that the component is dependent upon.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+      ),
+      'required' => array(
+        'description' => 'ID of a component that the component is dependent upon.',
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+      ),
+    ),
+    'primary key' => array('component_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,72 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Provides storage functions for use during release packaging.
+ *
+ * @author Jimmy Berry ("boombatower", http://drupal.org/user/214218)
+ */
+
+/**
+ * Clear the list of components 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 component_id IN (
+              SELECT component_id
+              FROM {project_info_component}
+              WHERE rid = %d
+            )', $rid);
+
+  db_query('DELETE FROM {project_info_component} WHERE rid = %d', $rid);
+}
+
+/**
+ * Store the list of components for a release.
+ *
+ * @param $rid
+ *   The project release ID.
+ * @param $info
+ *   Associative array of information for component keyed by the component name
+ *   and providing the following keys: name, title, description.
+ * @return
+ *   Associative array of stored records keyed by the component name with all
+ *   the previous keys and the generated component_id.
+ */
+function project_info_package_list_store($rid, array $info) {
+  $records = array();
+  foreach ($info as $component => $component_info) {
+    $info = array(
+      'rid' => $rid,
+      'name' => $component_info['name'],
+      'title' => $component_info['title'],
+      'description' => $component_info['description'],
+    );
+    drupal_write_record('project_info_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_info_package_dependencies_store($component_id, array $dependencies) {
+  foreach ($dependencies as $dependency_id) {
+    // Store the dependency if a best release was found.
+    $info = array(
+      'component_id' => $component_id,
+      'dependency_id' => $dependency_id,
+    );
+    drupal_write_record('project_info_dependency', $info);
+  }
+}
Index: info/tests/bar/barbar/project_info_barbar.info
===================================================================
RCS file: info/tests/bar/barbar/project_info_barbar.info
diff -N info/tests/bar/barbar/project_info_barbar.info
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ info/tests/bar/barbar/project_info_barbar.info	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,9 @@
+; $Id$
+name = Project info barbar
+description = Test module for project_info. (do not enable)
+package = Project
+version = 1.2
+core = 6.x
+dependencies[] = project_info_foo
+
+test_dependencies[] = project_info_foo (1.x)
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,215 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Parses a batch of 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', '/home/boombatower/software/drupal-6/sites/pift.d6x.loc/tmp');
+
+/**
+ * Load Drupal implementation by default.
+ */
+module_load_include('drupal.inc', 'project_info');
+
+/**
+ * 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 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_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_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_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/tests/bar/barbar/project_info_barbar.module
===================================================================
RCS file: info/tests/bar/barbar/project_info_barbar.module
diff -N info/tests/bar/barbar/project_info_barbar.module
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ info/tests/bar/barbar/project_info_barbar.module	1 Jan 1970 00:00:00 -0000
@@ -0,0 +1,9 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Dummy module file.
+ *
+ * @author Jimmy Berry ("boombatower", http://drupal.org/user/214218)
+ */
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,136 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Provides base retrieval API.
+ *
+ * @author Jimmy Berry ("boombatower", http://drupal.org/user/214218)
+ */
+
+/**
+ * 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('parse project_info batch'),
+    'file' => 'project_info.batch.inc',
+    'type' => MENU_CALLBACK,
+  );
+
+  return $items;
+}
+
+/**
+ * Implementation of hook_perm().
+ */
+function project_info_perm() {
+  return array(
+    'parse project_info batch',
+  );
+}
+
+/**
+ * Load a component.
+ *
+ * @param $component_id
+ *   Component ID to load.
+ * @return
+ *   Array of component information.
+ */
+function project_info_component_get($component_id) {
+  static $components = array();
+
+  if (!isset($components[$component_id])) {
+    $result = db_query('SELECT * FROM {project_info_component} WHERE component_id = %d', $component_id);
+    $components[$component_id] = db_fetch_array($result);
+  }
+  return $components[$component_id];
+}
+
+/**
+ * Load the list of components contained by a project release.
+ *
+ * @param $rid
+ *   Project release ID.
+ * @return
+ *   Associative array of components keyed by component name and containing
+ *   array keys: 'name', 'title', 'description'.
+ */
+function project_info_component_list_get($rid) {
+  $result = db_query('SELECT * FROM {project_info_component} WHERE rid = %d', $rid);
+  $components = array();
+  while ($component = db_fetch_array($result)) {
+    $components[$component['name']] = $component;
+  }
+  return $components;
+}
+
+/**
+ * Get a list of dependency components for a component.
+ *
+ * @param $component_id
+ *   Component ID to get dependencies for.
+ * @return
+ *   List of dependencies.
+ */
+function project_info_component_dependencies_get($component_id) {
+  $result = db_query('SELECT dependency_id FROM {project_info_dependency} WHERE component_id = %d', $component_id);
+  $dependencies = array();
+  while ($dependency = db_result($result)) {
+    $dependencies[] = project_info_component_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) {
+  $components = project_info_component_list_get($rid);
+  return project_info_dependency_list_get_components($components);
+}
+
+/**
+ * Recursively determine 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_info_dependency_list_get_components(array $components, array $dependencies = array()) {
+  foreach ($components as $component) {
+    // Get all dependencies of the component.
+    $dependencies_new = project_info_component_dependencies_get($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_info_dependency_list_get_components($dependencies_new, $dependencies);
+  }
+  return $dependencies;
+}
