Index: project.api.php
===================================================================
RCS file: project.api.php
diff -N project.api.php
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ project.api.php	13 Aug 2010 06:31:37 -0000
@@ -0,0 +1,103 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * API documentation for hooks invokved (provided) by the Project module.
+ */
+
+/**
+ * Info hook to advertise per-project permissions supported by a module.
+ *
+ * @return
+ *   Nested array of permission information. The keys of this array should be
+ *   the lower-case English version of the permission name, as used throughout
+ *   the code. The values of the array should be associative arrays of
+ *   information about each permission. These subarrays for each permission
+ *   must include a 'title' key for the human-readable (and translatable)
+ *   version of the permission for display in the UI, and an optional
+ *   'description' key for a description of what the permission allows a
+ *   project maintainer to do.
+ */
+function hook_project_permission_info() {
+  return array(
+    'some permission name' => array(
+      'title' => t('Name of this permission'),
+      'description' => t('Description of what this this permission allows project maintainers to do.'),
+    ),
+  );
+}
+
+/**
+ * Alter hook for per-project permissions supported by a module.
+ *
+ * @param $permissions
+ *   Reference to an array of all the permissions defined via
+ *   hook_project_permission_info().
+ *
+ * @see hook_project_permission_info().
+ * @see project_permission_load().
+ * @see drupal_alter()
+ */
+function hook_project_permission_alter(&$permissions) {
+  // I can't yet fathom why we need an alter hook here, but we might need it
+  // and it was free to include it, so why not? ;)
+}
+
+/**
+ * Invoked whenever a project maintainer is added.
+ *
+ * This gives any modules that are providing their own per-project permissions
+ * a chance to store the data about a given maintainer's permissions.
+ *
+ * @param $nid
+ *   The Project NID to add the maintainer to.
+ * @param $uid
+ *   The user ID of the maintainer to add.
+ * @param array $permissions
+ *   Associative array of which project-level permissions the maintainer
+ *   should have. The keys are permission names, and the values are if the
+ *   permission should be granted or not.
+ *
+ * @see project_maintainer_add()
+ * @see hook_project_permission_info()
+ */
+function hook_project_maintainer_add($nid, $uid, $permissions) {
+  // TODO
+}
+
+/**
+ * Invoked whenever a project maintainer is updated.
+ *
+ * This gives any modules that are providing their own per-project permissions
+ * a chance to store the data about an updated maintainer's permissions.
+ *
+ * @param $nid
+ *   The Project NID to update the maintainer for.
+ * @param $uid
+ *   The user ID of the maintainer to update.
+ * @param array $permissions
+ *   Associative array of which project-level permissions the maintainer
+ *   should have. The keys are permission names, and the values are if the
+ *   permission should be granted or not.
+ *
+ * @see project_maintainer_update()
+ * @see hook_project_permission_info()
+ */
+function hook_project_maintainer_update($nid, $uid, $permissions) {
+  // TODO
+}
+
+/**
+ * Invoked whenever a maintainer is removed from a given project.
+ *
+ * @param $nid
+ *   The Project NID to remove the maintainer from.
+ * @param $uid
+ *   The user ID of the maintainer to remove.
+ *
+ * @see project_maintainer_remove()
+ */
+function hook_project_maintainer_remove($nid, $uid) {
+  // TODO
+}
Index: project.inc
===================================================================
RCS file: /Users/wright/drupal/local_repo/contributions/modules/project/project.inc,v
retrieving revision 1.150
diff -u -p -r1.150 project.inc
--- project.inc	30 Jul 2010 21:59:28 -0000	1.150
+++ project.inc	13 Aug 2010 06:31:37 -0000
@@ -425,11 +425,15 @@ function project_project_nodeapi(&$node,
 function project_project_insert($node) {
   db_query("INSERT INTO {project_projects} (nid, uri, homepage, changelog, cvs, demo, screenshots, documentation, license) VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s')", $node->nid, $node->project['uri'], $node->project['homepage'], $node->project['changelog'], $node->project['cvs'], $node->project['demo'], $node->project['screenshots'], $node->project['documentation'], $node->project['license']);
 //  project_release_scan_directory($node->project['uri']);
+  $perms = array_fill_keys(array_keys(project_permission_load()), 1);
+  project_maintainer_add($node->nid, $node->uid, $perms);
 }
 
 function project_project_update($node) {
   db_query("UPDATE {project_projects} SET uri = '%s', homepage = '%s', changelog = '%s', cvs = '%s', demo = '%s', screenshots = '%s', documentation = '%s', license = '%s' WHERE nid = %d", $node->project['uri'], $node->project['homepage'], $node->project['changelog'], $node->project['cvs'], $node->project['demo'], $node->project['screenshots'], $node->project['documentation'], $node->project['license'], $node->nid);
 //  project_release_scan_directory($node->project['uri']);
+  $perms = array_fill_keys(array_keys(project_permission_load()), 1);
+  project_maintainer_update($node->nid, $node->uid, $perms);
 }
 
 function project_project_delete($node) {
Index: project.install
===================================================================
RCS file: /Users/wright/drupal/local_repo/contributions/modules/project/project.install,v
retrieving revision 1.28
diff -u -p -r1.28 project.install
--- project.install	22 Apr 2010 06:10:24 -0000	1.28
+++ project.install	13 Aug 2010 06:37:40 -0000
@@ -104,6 +104,41 @@ function project_schema() {
       'project_projects_uri' => array(array('uri', 8)),
     ),
   );
+  $schema['project_maintainer'] = array(
+    'description' => t('Users who have various per-project maintainer permissions.'),
+    'fields' => array(
+      'nid' => array(
+        'description' => t('Foreign key: {project_projects}.nid of the project.'),
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'uid' => array(
+        'description' => t('Foreign key: {users}.uid of a user with any project maintainer permissions.'),
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'administer_project' => array(
+        'description' => t('Can this user edit the given project.'),
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'administer_project_maintainers' => array(
+        'description' => t('Can this user manipulate the maintainers for the given project.'),
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+    ),
+    'primary key' => array('nid', 'uid'),
+  );
+
   return $schema;
 }
 
@@ -137,3 +172,58 @@ function project_update_6001() {
   return $ret;
 }
 
+/**
+ * Add the {project_maintainer} table.
+ */
+function project_update_6002() {
+  $ret = array();
+  $table = array(
+    'description' => t('Users who have various per-project maintainer permissions.'),
+    'fields' => array(
+      'nid' => array(
+        'description' => t('Foreign key: {project_projects}.nid of the project.'),
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'uid' => array(
+        'description' => t('Foreign key: {users}.uid of a user with any project maintainer permissions.'),
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'administer_project' => array(
+        'description' => t('Can this user edit the given project.'),
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'administer_project_maintainers' => array(
+        'description' => t('Can this user manipulate the maintainers for the given project.'),
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+    ),
+    'primary key' => array('nid', 'uid'),
+  );
+  db_create_table($ret, 'project_maintainer', $table);
+
+  // Initially populate the table so that every project owner has full
+  // powers on their own projects.
+  $ret[] = update_sql("INSERT INTO {project_maintainer} (nid, uid, administer_project, administer_project_maintainers) SELECT nid, uid, 1, 1 FROM {node} WHERE type = 'project_project'");
+
+  // If CVS module is enabled, also populate the table from the
+  // {cvs_project_maintainers} table so that anyone with CVS access
+  // who is not the project owner can administer the project but not
+  // manipulate the per-project permissions.
+  if (module_exists('cvs')) {
+    $ret[] = update_sql("INSERT INTO {project_maintainer} (nid, uid, administer_project, administer_project_maintainers) SELECT cpm.nid, cpm.uid, 1, 0 FROM {cvs_project_maintainers} cpm INNER JOIN {node} n ON cpm.nid = n.nid WHERE cpm.uid != n.uid");
+  }
+
+  return $ret;
+}
Index: project.module
===================================================================
RCS file: /Users/wright/drupal/local_repo/contributions/modules/project/project.module,v
retrieving revision 1.361
diff -u -p -r1.361 project.module
--- project.module	23 Jul 2010 04:12:12 -0000	1.361
+++ project.module	13 Aug 2010 06:45:59 -0000
@@ -518,6 +518,24 @@ function project_menu() {
     'type' => MENU_NORMAL_ITEM,
   );
 
+  $items['node/%project_node/maintainers'] = array(
+    'title' => 'Maintainers',
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('project_maintainers_form', 1),
+    'access callback' => 'project_check_admin_access',
+    'access arguments' => array(1, 'administer project maintainers'),
+    'file' => 'includes/project_maintainers.inc',
+    'type' => MENU_LOCAL_TASK,
+  );
+  $items['node/%project_node/maintainers/delete/%user'] = array(
+    'page callback' => 'drupal_get_form',
+    'page arguments' => array('project_maintainer_delete_confirm', 1, 4),
+    'access callback' => 'project_check_admin_access',
+    'access arguments' => array(1, 'administer project maintainers'),
+    'file' => 'includes/project_maintainers.inc',
+    'type' => MENU_CALLBACK,
+  );
+
   $items['node/%project_edit_project/edit/project'] = array(
     'title' => 'Project',
     'page callback' => 'node_page',
@@ -567,7 +585,10 @@ function project_edit_project_load($nid)
   return project_node_load($nid);
 }
 
-function project_check_admin_access($project, $cvs_access = NULL) {
+/**
+ * See if the current user has permission to administer the given project.
+ */
+function project_check_admin_access($project, $permission = 'administer projects') {
   global $user;
   if (empty($user->uid)) {
     return FALSE;
@@ -578,31 +599,27 @@ function project_check_admin_access($pro
     return FALSE;
   }
 
-  if (user_access('administer projects')) {
+  // If the current user has the site-wide permission, always grant access.
+  if (user_access($permission)) {
      return TRUE;
   }
 
-  // If $cvs_access is not defined, check to make sure the user has cvs access
-  // and that the user's cvs account is approved.
-  if (project_use_cvs($project_obj) && !isset($cvs_access)) {
-    if (db_result(db_query("SELECT COUNT(*) FROM {cvs_accounts} WHERE uid = %d AND status = %d", $user->uid, CVS_APPROVED))) {
-      $cvs_access = TRUE;
-    }
-    else {
-      $cvs_access = FALSE;
-    }
-  }
-
   if (user_access('maintain projects')) {
+    // Project owners are treated as super users and can always access.
     if ($user->uid == $project_obj->uid) {
        return TRUE;
     }
-    if (project_use_cvs($project_obj) && $cvs_access) {
-      if (db_result(db_query("SELECT COUNT(*) FROM {cvs_project_maintainers} WHERE uid = %d AND nid = %d", $user->uid, $project_obj->nid))) {
-        return TRUE;
-      }
+
+    // Otherwise, see if the user has the right permission for this project.
+    // Special-case: the per-project permission for 'administer projects' is
+    // singular, 'administer project', so handle that here.
+    if ($permission == 'administer projects') {
+      $permission = 'administer project';
     }
+    return !empty($project_obj->project['maintainers'][$user->uid]['permissions'][$permission]);
   }
+
+  // If we haven't granted access yet, deny it.
   return FALSE;
 }
 
@@ -730,12 +747,125 @@ function project_project_access($op, $no
 }
 
 /**
+ * Load all per-project permission information and return it.
+ *
+ * This invokes hook_project_permission_info() and
+ * hook_project_permission_alter(), and caches the results in RAM.
+ *
+ * @see hook_project_permission_info()
+ * @see hook_project_permission_alter()
+ * @see drupal_alter()
+ */
+function project_permission_load() {
+  static $project_permissions = array();
+  if (empty($project_permissions)) {
+    $project_permissions = module_invoke_all('project_permission_info');
+    drupal_alter('project_permission', $project_permissions);
+  }
+  return $project_permissions;
+}
+
+/**
+ * Implement hook_project_permission_info()
+ */
+function project_project_permission_info() {
+  return array(
+    'administer project' => array(
+      'title' => t('Administer project'),
+      'description' => t('Allows a user to edit a project, and other related operations.'),
+    ),
+    'administer project maintainers' => array(
+      'title' => t('Administer project maintainers'),
+      'description' => t('Allows a user to add, remove, or update the permissions on other project maintainers.'),
+    ),
+  );
+}
+
+/**
+ * Add a maintainer with the specified permissions to a given project.
+ *
+ * @param $nid
+ *   The Project NID to add the maintainer to.
+ * @param $uid
+ *   The user ID of the maintainer to add.
+ * @param array $permissions
+ *   Associative array of which project-level permissions the maintainer
+ *   should have. The keys are permission names, and the values are if the
+ *   permission should be granted or not.
+ */
+function project_maintainer_add($nid, $uid, $permissions = array()) {
+  db_query("INSERT INTO {project_maintainer} (nid, uid, administer_project, administer_project_maintainers) VALUES (%d, %d, %d, %d)", $nid, $uid, !empty($permissions['administer project']), !empty($permissions['administer project maintainers']));
+
+  // Invoke hook_project_maintainer_add() to let other modules know this
+  // maintainer now exists so they can take any actions or record any data
+  // they need to.
+  module_invoke_all('project_maintainer_add', $nid, $uid, $permissions);
+}
+
+/**
+ * Update the permissions associated with a maintainer for a given project.
+ *
+ * @param $nid
+ *   The Project NID to update the maintainer for.
+ * @param $uid
+ *   The user ID of the maintainer to update.
+ * @param array $permissions
+ *   Associative array of which project-level permissions the maintainer
+ *   should have. The keys are permission names, and the values are if the
+ *   permission should be granted or not.
+ */
+function project_maintainer_update($nid, $uid, $permissions = array()) {
+  db_query("UPDATE {project_maintainer} SET administer_project = %d, administer_project_maintainers = %d WHERE nid = %d AND uid = %d", !empty($permissions['administer project']), !empty($permissions['administer project maintainers']), $nid, $uid);
+
+  // Make sure we actually updated a record for this maintainer.
+  if (!db_affected_rows()) {
+    // Didn't update anything, add this as a new maintainer, instead.
+    project_maintainer_add($nid, $uid, $permissions);
+  }
+  else {
+    // Invoke hook_project_maintainer_update() to let other modules know this
+    // maintainer is being updated so they can take any actions or record any
+    // data they need to.
+    module_invoke_all('project_maintainer_update', $nid, $uid, $permissions);
+  }
+}
+
+/**
+ * Remove a maintainer from a given project.
+ *
+ * @param $nid
+ *   The Project NID to remove the maintainer from.
+ * @param $uid
+ *   The user ID of the maintainer to remove.
+ */
+function project_maintainer_remove($nid, $uid) {
+  db_query("DELETE FROM {project_maintainer} WHERE nid = %d and uid = %d", $nid, $uid);
+
+  // Invoke hook_project_maintainer_remove() to let other modules know this
+  // maintainer is being removed so they can take any actions or record any
+  // data they need to.
+  module_invoke_all('project_maintainer_remove', $nid, $uid);
+}
+
+/**
  * Implement hook_load().
  */
 function project_project_load($node) {
   $additions = db_fetch_array(db_query('SELECT * FROM {project_projects} WHERE nid = %d', $node->nid));
   $project = new stdClass;
   $project->project = $additions;
+  // We don't want to load all the permissions here, just the ones that
+  // Project itself is responsible for, so we use our implementation of the
+  // hook, instead of the global load function.
+  $project_perms = project_project_permission_info();
+  $maintainers = db_query('SELECT u.name, pm.* FROM {project_maintainer} pm INNER JOIN {users} u ON pm.uid = u.uid WHERE pm.nid = %d ORDER BY u.name', $node->nid);
+  while ($maintainer = db_fetch_object($maintainers)) {
+    $project->project['maintainers'][$maintainer->uid]['name'] = $maintainer->name;
+    foreach ($project_perms as $perm_name => $perm_info) {
+      $db_field = str_replace(' ', '_', $perm_name);
+      $project->project['maintainers'][$maintainer->uid]['permissions'][$perm_name] = $maintainer->$db_field;
+    }
+  }
   return $project;
 }
 
@@ -996,6 +1126,12 @@ function project_theme() {
         'title' => NULL,
       ),
     ),
+    'project_maintainers_form' => array(
+      'file' => 'includes/project_maintainers.inc',
+      'arguments' => array(
+        'form' => NULL,
+      ),
+    ),
     'project_project_node_form_taxonomy' => array(
       'file' => 'project.inc',
       'arguments' => array(
Index: includes/project_maintainers.inc
===================================================================
RCS file: includes/project_maintainers.inc
diff -N includes/project_maintainers.inc
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ includes/project_maintainers.inc	13 Aug 2010 06:50:49 -0000
@@ -0,0 +1,219 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Code for the node/N/maintainers tab on project nodes.
+ */
+
+/**
+ * Build the form for the 
+ */
+function project_maintainers_form($form_state, $project) {
+  // Load all the info about per-project permissions on this site.
+  $project_perms = project_permission_load();
+
+  $form = array();
+  $form['#tree'] = TRUE;
+  $form['#header'] = array();
+  $form['#project'] = $project;
+  $form['#header']['username'] = array('data' => t('User'));
+  foreach ($project_perms as $perm_name => $perm_info) {
+    $form['#header'][$perm_name] = array('data' => $perm_info['title']);
+  }
+  $form['#header']['operations'] = array('data' => t('Operations'));
+
+  if (!empty($project->project['maintainers'])) {
+    foreach ($project->project['maintainers'] as $uid => $maintainer) {
+      $form['maintainers'][$uid] = array();
+      $form['maintainers'][$uid]['name'] = array(
+        '#type' => 'value',
+        '#value' => $maintainer['name'],
+      );
+      foreach ($project_perms as $perm_name => $perm_info) {
+        $form['maintainers'][$uid]['permissions'][$perm_name] = array(
+          '#type' => 'checkbox',
+          '#default_value' => !empty($maintainer['permissions'][$perm_name]),
+        );
+      }
+      $form['maintainers'][$uid]['operations'] = array();
+      if ($uid == $project->uid) {
+        // We special-case the project owner with disabled checkboxes.
+        foreach ($project_perms as $perm_name => $perm_info) {
+          $form['maintainers'][$uid]['permissions'][$perm_name]['#disabled'] = TRUE;
+        }
+        $form['maintainers'][$uid]['operations']['delete'] = array(
+          '#value' => t('locked'),
+        );
+      }
+      else {
+        $form['maintainers'][$uid]['operations']['delete'] = array(
+          '#value' => l(t('delete'), "node/$project->nid/maintainers/delete/$uid"),
+        );
+      }
+    }
+  }
+
+  $form['new_maintainer'] = array();
+  $form['new_maintainer']['user'] = array(
+    '#type' => 'textfield',
+    '#size' => 20,
+    '#maxlength' => 40,
+    '#autocomplete_path' => 'user/autocomplete',
+  );
+  // we'll fill this in with a real value during validate()
+  $form['new_maintainer']['uid'] = array(
+    '#type' => 'value',
+    '#value' => 0,
+  );
+  foreach ($project_perms as $perm_name => $perm_info) {
+    $form['new_maintainer']['permissions'][$perm_name] = array(
+      '#type' => 'checkbox',
+    );
+  }
+
+  $form['submit'] = array('#type' => 'submit', '#value' => t('Update'));
+
+  return $form;
+}
+
+function theme_project_maintainers_form($form) {
+  $output = '';
+
+  $header = $form['#header'];
+  $rows = array();
+
+  // Render all the existing maintainers.
+  if (is_array($form['maintainers'])) {
+    foreach (element_children($form['maintainers']) as $uid) {
+      $row = array();
+      $account = new stdClass;
+      $account->uid = $uid;
+      $account->name = $form['maintainers'][$uid]['name']['#value'];
+      $row[] = theme('username', $account);
+      foreach (element_children($form['maintainers'][$uid]['permissions']) as $perm) {
+        $row[] = drupal_render($form['maintainers'][$uid]['permissions'][$perm]);
+      }
+      $row[] = drupal_render($form['maintainers'][$uid]['operations']);
+      if ($form['#project']->uid == $uid) {
+        $owner_row = $row;
+      }
+      else {
+        $rows[] = $row;
+      }
+    }
+  }
+
+  // Create the final row for adding a new maintainer.
+  $row = array();
+  $row[] = drupal_render($form['new_maintainer']['user']);
+  foreach (element_children($form['new_maintainer']['permissions']) as $perm) {
+    $row[] = drupal_render($form['new_maintainer']['permissions'][$perm]);
+  }
+  $row[] = ''; // Empty cell for the 'Operations' column on new maintainers.
+  $rows[] = $row;
+
+  // Always put the owner row at the top of the table.
+  $rows = array_merge(array($owner_row), $rows);
+
+  // Although using named keys in the $header array makes this form easier to
+  // alter, theme_table() freaks out if the $header array has non-numeric
+  // keys. So we ditch the keys at this point to avoid notices.
+  $output .= theme('table', array_values($header), $rows);
+  $output .= drupal_render($form);
+  return $output;
+}
+
+/**
+ * Validation callback for the project maintainers form.
+ */
+function project_maintainers_form_validate($form, &$form_state) {
+  $new_maintainer = $form_state['values']['new_maintainer'];
+  if (!empty($new_maintainer['user'])) {
+    $user_result = db_fetch_object(db_query("SELECT name, uid FROM {users} WHERE name = '%s'", $new_maintainer['user']));
+    if (empty($user_result->uid)) {
+      form_set_error('new_maintainer][user', t('%user is not a valid user on this site.', array('%user' => $new_maintainer['user'])), 'error');
+      return;
+    }
+    if (!empty($form['#project']->project['maintainers'][$user_result->uid])) {
+      form_set_error('new_maintainer][user', t('%user is already a maintainer of this project.', array('%user' => $new_maintainer['user'])), 'error');
+      return;
+    }
+    // Save the uid in the form so we don't have to look it up again at submit.
+    form_set_value($form['new_maintainer']['uid'], $user_result->uid, $form_state);
+  }
+  else {
+    foreach ($new_maintainer['permissions'] as $name => $value) {
+      if (!empty($value)) {
+        form_set_error('new_maintainer][user', t('You must specify a valid user name to grant permissions.'));
+      }
+    }
+  }
+}
+
+/**
+ * Submit callback for the project maintainers form.
+ */
+function project_maintainers_form_submit($form, &$form_state) {
+  $project_nid = $form['#project']->nid;
+
+  // Loop over all the maintainers and update their permissions accordingly.
+  if (!empty($form_state['values']['maintainers'])) {
+    foreach ($form_state['values']['maintainers'] as $uid => $maintainer) {
+      // Just to be extra safe, always give the project owner full permissions.
+      if ($uid == $form['#project']->uid) {
+        $perms = array_fill_keys(array_keys(project_permission_load()), 1);
+      }
+      else {
+        $perms = $maintainer['permissions'];
+      }
+      project_maintainer_update($project_nid, $uid, $perms);
+    }
+  }
+
+  // See if we need to insert a record for a new maintainer.
+  if (!empty($form_state['values']['new_maintainer']['uid'])) {
+    project_maintainer_add($project_nid, $form_state['values']['new_maintainer']['uid'], $form_state['values']['new_maintainer']['permissions']);
+  }
+
+}
+
+/**
+ * Confirm form for removing a uid as a cvs maintainer from a given project.
+ */
+function project_maintainer_delete_confirm($form_state, $project, $user) {
+  if ($user->uid == $project->uid) {
+    drupal_set_message(t('You can not delete the project owner (!user) as a maintainer.', array('!user' => theme('username', $user))), 'error');
+    return drupal_goto("node/$project->nid/maintainers/");
+  }
+
+  $form['nid'] = array('#type' => 'value', '#value' => $project->nid);
+  $form['uid'] = array('#type' => 'value', '#value' => $user->uid);
+
+  return confirm_form($form,
+           t('Are you sure you want to delete !user as a maintainer of !project?',
+             array(
+               '!user' => theme('username', $user),
+               '!project' => l($project->title, "node/$project->nid"),
+             )),
+           "node/$project->nid/maintainers",
+           t('This action cannot be undone.'),
+           t('Delete'),
+           t('Cancel'));
+}
+
+/**
+ * Delete the requested user as a maintainer.
+ *
+ * Invoked when the delete button on the confirm_form() page is pressed.
+ */
+function project_maintainer_delete_confirm_submit($form, &$form_state) {
+  $nid = $form_state['values']['nid'];
+  $uid = $form_state['values']['uid'];
+  $user = user_load(array('uid' => $uid));
+
+  project_maintainer_remove($nid, $uid);
+
+  drupal_set_message(t('Removed !user as a maintainer.', array('!user' => theme('username', $user))));
+  $form_state['redirect'] = "node/$nid/maintainers";
+}
Index: release/project_release.install
===================================================================
RCS file: /Users/wright/drupal/local_repo/contributions/modules/project/release/project_release.install,v
retrieving revision 1.31
diff -u -p -r1.31 project_release.install
--- release/project_release.install	7 Jun 2010 22:45:39 -0000	1.31
+++ release/project_release.install	13 Aug 2010 06:37:48 -0000
@@ -351,6 +351,35 @@ function project_release_schema() {
       'expire' => array('expire')
     ),
   );
+
+  $schema['project_release_project_maintainer'] = array(
+    'description' => t('Users who have various per-project maintainer permissions.'),
+    'fields' => array(
+      'nid' => array(
+        'description' => t('Foreign key: {project_projects}.nid of the project.'),
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'uid' => array(
+        'description' => t('Foreign key: {users}.uid of a user with any project maintainer permissions.'),
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'administer_project_releases' => array(
+        'description' => t('Can this user create and administer releases for the given project.'),
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+    ),
+    'primary key' => array('nid', 'uid'),
+  );
+
   return $schema;
 }
 
@@ -706,3 +735,52 @@ function project_release_update_6009() {
   }
   return $ret;
 }
+
+/**
+ * Add the {project_release_project_maintainer} table.
+ */
+function project_release_update_6010() {
+  $ret = array();
+
+  $table = array(
+    'description' => t('Users who have various per-project maintainer permissions.'),
+    'fields' => array(
+      'nid' => array(
+        'description' => t('Foreign key: {project_projects}.nid of the project.'),
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'uid' => array(
+        'description' => t('Foreign key: {users}.uid of a user with any project maintainer permissions.'),
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+      'administer_project_releases' => array(
+        'description' => t('Can this user create and administer releases for the given project.'),
+        'type' => 'int',
+        'unsigned' => TRUE,
+        'not null' => TRUE,
+        'default' => 0,
+      ),
+    ),
+    'primary key' => array('nid', 'uid'),
+  );
+  db_create_table($ret, 'project_release_project_maintainer', $table);
+
+  // Initially populate the table so that every project owner has full
+  // powers on their own projects.
+  $ret[] = update_sql("INSERT INTO {project_release_project_maintainer} (nid, uid, administer_project_releases) SELECT nid, uid, 1 FROM {node} WHERE type = 'project_project'");
+
+  // If CVS module is enabled, also populate the table from the
+  // {cvs_project_maintainers} table so that anyone with CVS access
+  // who is not the project owner can administer releases.
+  if (module_exists('cvs')) {
+    $ret[] = update_sql("INSERT INTO {project_release_project_maintainer} (nid, uid, administer_project_releases) SELECT cpm.nid, cpm.uid, 1 FROM {cvs_project_maintainers} cpm INNER JOIN {node} n ON cpm.nid = n.nid WHERE cpm.uid != n.uid");
+  }
+
+  return $ret;
+}
Index: release/project_release.module
===================================================================
RCS file: /Users/wright/drupal/local_repo/contributions/modules/project/release/project_release.module,v
retrieving revision 1.153
diff -u -p -r1.153 project_release.module
--- release/project_release.module	8 Jul 2010 23:16:17 -0000	1.153
+++ release/project_release.module	13 Aug 2010 06:31:37 -0000
@@ -40,8 +40,8 @@ function project_release_menu() {
     'title' => 'Releases',
     'page callback' => 'project_release_project_edit_releases',
     'page arguments' => array(1),
-    'access callback' => 'node_access',
-    'access arguments' => array('update', 1),
+    'access callback' => 'project_check_admin_access',
+    'access arguments' => array(1, 'administer project releases'),
     'type' => MENU_LOCAL_TASK,
     'file' => 'includes/project_edit_releases.inc',
   );
@@ -120,7 +120,7 @@ function project_release_access($op, $no
       // We can't just use project_project_access() here, since we
       // need to check access to the project itself, not the release
       // node, so we use the helper method and pass the project id.
-      return project_check_admin_access($node->project_release['pid']);
+      return project_check_admin_access($node->project_release['pid'], 'administer project releases');
     case 'delete':
       // No one should ever delete a release node, only unpublish it.
       return FALSE;
@@ -142,6 +142,43 @@ function project_release_node_info() {
 }
 
 /**
+ * Implement hook_project_permission_info()
+ */
+function project_release_project_permission_info() {
+  return array(
+    'administer project releases' => array(
+      'title' => t('Administer project releases'),
+      'description' => t('Allows a user to create and update releases, and to control which branches are recommeneded or supported.'),
+    ),
+  );
+}
+
+/**
+ * Implement hook_project_maintainer_add()
+ */
+function project_release_project_maintainer_add($nid, $uid, $permissions = array()) {
+  db_query("INSERT INTO {project_release_project_maintainer} (nid, uid, administer_project_releases) VALUES (%d, %d, %d)", $nid, $uid, !empty($permissions['administer project releases']));
+}
+
+/**
+ * Implement hook_project_maintainer_update()
+ */
+function project_release_project_maintainer_update($nid, $uid, $permissions = array()) {
+  db_query("UPDATE {project_release_project_maintainer} SET administer_project_releases = %d WHERE nid = %d AND uid = %d", !empty($permissions['administer project releases']), $nid, $uid);
+  if (!db_affected_rows()) {
+    // If we didn't have a record to update, add this as a new maintainer. 
+    project_release_project_maintainer_add($nid, $uid, $permissions);
+  }
+}
+
+/**
+ * Implement hook_project_maintainer_remove()
+ */
+function project_release_project_maintainer_remove($nid, $uid) {
+  db_query("DELETE FROM {project_release_project_maintainer} WHERE nid = %d and uid = %d", $nid, $uid);
+}
+
+/**
  * Implement of hook_form() for project_release nodes.
  */
 function project_release_form(&$release, &$form_state) {
@@ -609,7 +646,7 @@ function project_release_view($node, $te
   }
 
   // Display packaging errors to admins.
-  if (project_check_admin_access($node->project_release['pid'])) {
+  if (project_check_admin_access($node->project_release['pid'], 'administer project releases')) {
     $rows = array();
     $result = db_query('SELECT * FROM {project_release_package_errors} WHERE nid = %d', $node->nid);
     $error = db_fetch_object($result);
@@ -690,7 +727,7 @@ function project_release_get_releases($p
   $where = '';
   $join = '';
   $args = array($project->nid);
-  if (!project_check_admin_access($project)) {
+  if (!project_check_admin_access($project, 'administer project releases')) {
     if (!empty($rids)) {
       $where = "AND (n.status = %d OR n.nid IN (". db_placeholders($rids) ."))";
       $args[] = 1;
@@ -895,6 +932,15 @@ function project_release_project_nodeapi
       $node->project_release['project_release_show_snapshots'] = TRUE;
     }
   }
+
+  // Also load project_release project maintainer info.
+  $maintainers = db_query('SELECT u.uid, u.name, prpm.administer_project_releases FROM {project_release_project_maintainer} prpm INNER JOIN {users} u ON prpm.uid = u.uid WHERE prpm.nid = %d', $node->nid);
+  while ($maintainer = db_fetch_object($maintainers)) {
+    if (empty($node->project['maintainers'][$maintainer->uid])) {
+      $node->project['maintainers'][$maintainer->uid]['name'] = $maintainer->name;
+    }
+    $node->project['maintainers'][$maintainer->uid]['permissions']['administer project releases'] = $maintainer->administer_project_releases;
+  }
 }
 
 /**
@@ -1230,7 +1276,7 @@ function project_release_project_page_li
     ),
   );
 
-  if (project_check_admin_access($node->nid)) {
+  if (project_check_admin_access($node->nid, 'administer project releases')) {
     $links['project_release']['links']['add_new_release'] = l(t('Add new release'), 'node/add/project_release/'. $node->nid);
     $links['project_release']['links']['administer_releases'] = l(t('Administer releases'), 'node/'. $node->nid .'/edit/releases');
   }
Index: release/includes/release_node_form.inc
===================================================================
RCS file: /Users/wright/drupal/local_repo/contributions/modules/project/release/includes/release_node_form.inc,v
retrieving revision 1.11
diff -u -p -r1.11 release_node_form.inc
--- release/includes/release_node_form.inc	30 Jan 2010 02:33:40 -0000	1.11
+++ release/includes/release_node_form.inc	13 Aug 2010 06:31:37 -0000
@@ -24,7 +24,7 @@ function _project_release_form(&$release
     }
     // Make sure this user should have permissions to add releases for
     // the requested project
-    if (!project_check_admin_access($project)) {
+    if (!project_check_admin_access($project, 'administer project releases')) {
       drupal_access_denied();
       module_invoke_all('exit');
       exit;
