'admin/project/versioncontrol-settings/project', 'title' => t('Project node integration'), 'description' => t('Specify the content types that will be integrated with version control systems, and configure how this will be done.'), 'callback' => 'drupal_get_form', 'callback arguments' => array('versioncontrol_project_admin_form'), 'access' => $admin_access, 'type' => MENU_LOCAL_TASK, ); } else { if (arg(0) == 'node' && is_numeric(arg(1))) { // If a user is viewing a project node that they own (or the user has // the 'administer nodes' permission), add the 'Commit access' tab, // but only if the project is associated with a repository. $node = node_load(arg(1)); if (isset($node->versioncontrol_project)) { $repo_id = $node->versioncontrol_project['repo_id']; $node_access = node_access('view', $node); if ($repo_id > 0) { $items[] = array( 'path' => 'node/'. $node->nid .'/commitlog', 'title' => t('Commits'), 'callback' => 'versioncontrol_project_commitlog', 'callback arguments' => array($node), 'access' => $node_access, 'type' => MENU_CALLBACK, 'weight' => 4, ); $items[] = array( 'path' => 'node/'. $node->nid .'/developers', 'title' => t('Developers'), 'callback' => 'versioncontrol_project_developers', 'callback arguments' => array($node), 'access' => $node_access, 'type' => MENU_CALLBACK, 'weight' => 6, ); $repository = versioncontrol_get_repository($repo_id); if (($node->uid == $user->uid && versioncontrol_is_account_authorized($user->uid, $repository)) || $admin_access || user_access('administer nodes')) { $items[] = array( 'path' => 'node/'. $node->nid .'/commit-access', 'title' => t('Commit access'), 'callback' => 'drupal_get_form', 'callback arguments' => array('versioncontrol_project_commit_access_form', $node), 'access' => $node_access, 'type' => MENU_LOCAL_TASK, 'weight' => 5, ); if (is_numeric(arg(3)) && in_array(arg(3), $node->versioncontrol_project['comaintainer_uids'])) { $deleted_uid = arg(3); $items[] = array( 'path' => 'node/'. $node->nid .'/commit-access/'. $deleted_uid .'/delete', 'callback' => 'drupal_get_form', 'callback arguments' => array( 'versioncontrol_project_commit_access_delete_confirm', $node, $deleted_uid, ), 'access' => $node_access, 'type' => MENU_CALLBACK, ); } } } } } } return $items; } /** * Form callback for 'admin/project/versioncontrol-project': * Global settings for this module. */ function versioncontrol_project_admin_form() { $form = array(); $repositories = versioncontrol_get_repositories(); $repository_options = array(); foreach ($repositories as $repo_id => $repository) { if (empty($repository_options)) { $first_repo_id = $repo_id; } $repository_options[$repo_id] = check_plain($repository['name']); } $form['versioncontrol_project_restrict_commits'] = array( '#type' => 'checkbox', '#title' => t('Enable project-based commit restrictions'), '#description' => t('Restrict commit access to projects that the user maintains. (This feature requires pre-commit hook scripts that integrate with the Version Control API.)'), '#default_value' => variable_get('versioncontrol_project_restrict_commits', 1), '#weight' => -20, ); $form['versioncontrol_project_restrict_creation'] = array( '#title' => t('Restrict project creation to users with VCS accounts'), '#type' => 'checkbox', '#default_value' => variable_get('versioncontrol_project_restrict_creation', 1), '#description' => t('If this box is checked, only users with VCS accounts will be allowed to create project nodes.'), '#weight' => -18, ); $form['versioncontrol_project_content_types'] = array( '#type' => 'checkboxes', '#title' => t('Project node content types'), '#description' => t('The content types that you specify here will be enhanced with version control capabilities.'), '#default_value' => versioncontrol_project_get_content_types(), '#options' => node_get_types('names'), '#weight' => -10, ); // only store checked checkbox keys $form['array_filter'] = array( '#type' => 'value', '#value' => TRUE, ); if (module_exists('project') && project_use_taxonomy()) { $form['type_validation'] = array( '#title' => t('Validate directory by project type'), '#type' => 'fieldset', '#collapsible' => TRUE, '#collapsed' => FALSE, '#weight' => -1, ); $form['type_validation']['versioncontrol_project_dir_validate_by_type'] = array( '#title' => t('Validate directory by project type'), '#type' => 'checkbox', '#default_value' => variable_get('versioncontrol_project_dir_validate_by_type', 1), '#description' => t('If this box is checked, the path specified in the project directory field must match the path given for the respective project type, as defined below. For each type, you can specify a PHP regular expression for allowed project directories. Example: "@^/contributions/modules/(.+)$@". If empty, all directories will be allowed for the respective project type.'), ); $terms = taxonomy_get_tree(_project_get_vid()); foreach ($terms as $term) { // Only use the first-level terms. if ($term->depth == 0) { $form['type_validation']['versioncontrol_project_directory_tid_'. $term->tid] = array( '#title' => t('Directory for %term_name', array('%term_name' => $term->name)), '#type' => 'textfield', '#default_value' => variable_get('versioncontrol_project_directory_tid_'. $term->tid, ''), ); } } } return system_settings_form($form); } /** * Implementation of hook_nodeapi(): * Load the project array into $node->versioncontrol_project if there is * a project for this node, and update/delete the project when the node * is being deleted. */ function versioncontrol_project_nodeapi(&$node, $op, $arg = NULL) { if (in_array($node->type, versioncontrol_project_get_content_types())) { switch ($op) { case 'load': $project = versioncontrol_project_get_project($node->nid, TRUE); if (isset($project)) { $node->versioncontrol_project = $project; } return; case 'validate': // Fail validation if the user doesn't have an account. // This is ugly to do in the validation phase, but the only clean // solution so far as node_access() doesn't provide a general hook. // So, the best thing is probably to keep this as fallback and insert // conditional checks in Project and similar modules. global $user; if (!versioncontrol_project_creation_is_allowed($user->uid)) { form_set_error('nid', t('You cannot create projects without having a version control system account assigned.')); } return; case 'insert': case 'update': // The node array possibly contains repo_id and project_directory // as the submit values of the (form_altered) node edit form. if (!isset($node->repo_id)) { versioncontrol_project_delete_project($node->nid); unset($node->versioncontrol_project); unset($node->repo_id); unset($node->project_directory); } else { $directory = _versioncontrol_project_remove_trailing_slashes($node->project_directory); $project = array( 'nid' => $node->nid, 'owner_uid' => $node->uid, 'repo_id' => $node->repo_id, 'directory' => $node->project_directory, ); $node->versioncontrol_project = versioncontrol_project_set_project($project); } return; case 'delete': versioncontrol_project_delete_project($node->nid); return; default: return; } } } /** * Implementation of hook_form_alter(): * Add a fieldset to the add/edit project form where the user can specify * the project's repository and path. */ function versioncontrol_project_form_alter($form_id, &$form) { if (isset($form['#id']) && $form['#id'] == 'node-form' && in_array($form['#node']->type, versioncontrol_project_get_content_types())) { $node = $form['#node']; $project = isset($node->versioncontrol_project) ? $node->versioncontrol_project : NULL; $accounts = versioncontrol_get_accounts(array('uids' => array($node->uid))); // Default setting: no version control integration at all $form['repo_id'] = array( '#type' => 'value', '#value' => 0, ); // If the user doesn't have commit access to at least one repository, // it makes no sense to present version control integration options. if (empty($accounts)) { return; } // Retrieve the possible repository options. $user_repo_ids = array(); // repositories where the user has an account foreach ($accounts as $uid => $usernames_by_repository) { foreach ($usernames_by_repository as $repo_id => $username) { $user_repo_ids[] = $repo_id; } } $repositories = versioncontrol_get_repositories(array('repo_ids' => $user_repo_ids)); $repository_options = array(0 => t('')); foreach ($repositories as $repo_id => $repository) { if (empty($repository_options)) { $first_repo_id = $repo_id; } $repository_options[$repo_id] = check_plain($repository['name']); } if (empty($repository_options)) { return; } // We're ready to go, add the form elements now. $form['#validate']['versioncontrol_project_form_validate'] = array(); $form['versioncontrol_project'] = array( '#type' => 'fieldset', '#title' => t('Version control integration'), '#collapsible' => TRUE, '#collapsed' => isset($project), ); $form['versioncontrol_project']['new_repository'] = array(); // currently unused $form['versioncontrol_project']['existing_repository'] = array(); if (count($repository_options) > 1) { unset($form['repo_id']); $form['versioncontrol_project']['existing_repository']['repo_id'] = array( '#type' => 'select', '#title' => t('Repository'), '#description' => t('The version control repository where this project is located.'), '#default_value' => isset($project) ? $project['repo_id'] : $first_repo_id, '#options' => $repository_options, ); $form['versioncontrol_project']['project_directory'] = array( '#type' => 'textfield', '#title' => t('Project directory'), '#description' => t("The project's directory within the selected repository. Directory names should start with a leading slash and must be unique for each project. For example: /modules/foo, /themes/foo or /translations/foo. If there is no repository associated with the project, this setting should be left blank."), '#default_value' => isset($project) ? $project['directory'] : '', '#size' => 40, '#maxlength' => 255, ); } } } /** * Validate the add/edit project form before it is submitted. */ function versioncontrol_project_form_validate($form_id, $form_values) { $repo_id = $form_values['repo_id']; $admin_access = user_access('administer version control systems'); // Don't allow changing the repository after the project has been created. if (is_numeric($form_values['nid'])) { $project = versioncontrol_project_get_project($form_values['nid'], TRUE); if (!$admin_access && isset($project) && $repo_id != $project['repo_id']) { form_set_error('repo_id', t('You do not have permission to modify the repository for this project. (The repository can\'t be changed after the project has been created.)')); return; } } if (!$repo_id) { // no version control integration, we don't have to validate return; } $repository = versioncontrol_get_repository($repo_id); if (!isset($repository)) { form_set_error('repo_id', t('You must select a valid repository.')); return; } if (empty($form_values['project_directory'])) { form_set_error('project_directory', t('You can not specify a directory if there is no repository for this project.')); return; } $directory = _versioncontrol_project_remove_trailing_slashes($form_values['project_directory']); if (!preg_match('/^[a-zA-Z0-9\/_-]+$/', $directory)) { form_set_error('project_directory', t("The path of the project directory can only contain letters, numbers, slashes ('/'), hyphens ('-') and underscores ('_').")); return; } // Don't do this if another project with the same directory already exists. $existing_project = versioncontrol_project_get_project_for_item($repository, $directory, TRUE); if (isset($existing_project) && $existing_project['nid'] != $form_values['nid']) { form_set_error('project_directory', t('The specified project directory conflicts with that of an existing project.')); return; } // project.module specific. Not the most ideal place for these checks, // but what the hell, it doesn't hurt nobody. if (module_exists('project')) { if (project_use_taxonomy() && isset($form_values['project_type']) && variable_get('versioncontrol_project_dir_validate_by_type', 1)) { $project_type_tid = $form_values['project_type']; $tree = taxonomy_get_term($project_type_tid); $dir_regexp = variable_get('versioncontrol_project_directory_tid_'. $project_type_tid, ''); if (!empty($dir_regexp)) { // empty means "all directories allowed" $directory_with_slash = ($directory == '/') ? '/' : $directory .'/'; if (!preg_match($dir_regexp, $directory_with_slash)) { form_set_error('project_directory', t("The root of the project directory does not match the selected project type (%type). Given the current project type, the directory path should match the following regular expression: %goal.", array('%type' => $tree->name, '%goal' => $dir_regexp))); return; } } } if (variable_get('versioncontrol_project_validate_by_short_name', 1)) { $last_element = array_pop(explode('/', $directory)); if ($last_element != $form_values['uri']) { form_set_error('project_directory', t("The last part of the project directory (%last) does not match the short project name (%short).", array('%last' => $last_element, '%short' => $form_values['uri']))); return; } } } } /** * Make sure the directory starts with, but does not end with a slash. */ function _versioncontrol_project_remove_trailing_slashes($directory) { $directory = trim($directory); // remove whitespace if (!empty($directory) && $directory != '/') { $directory = '/'. trim($directory, '/'); } return $directory; } /** * Form callback for 'node/$nid/commit-access': * Overview / management of project maintainers. This is just the minimal * version, the real form is done in the theming function further down, * as we don't want to drupal_render() the "new user" textfield and the * "Grant access" button at this point already. */ function versioncontrol_project_commit_access_form($node) { $form = array(); $project = $node->versioncontrol_project; $repository = versioncontrol_get_repository($project['repo_id']); drupal_set_title(t('Commit access for @title', array('@title' => $node->title))); if ($node->type == 'project_project') { project_project_set_breadcrumb($node); } $form['nid'] = array( '#type' => 'value', '#value' => $node->nid, ); $form['repo_id'] = array( '#type' => 'value', '#value' => $project['repo_id'], ); $form['maintainer_uids'] = array( '#type' => 'value', '#value' => implode(',', $project['maintainer_uids']), ); // The user id for a new project co-maintainer, // we'll fill this in with a real value during the validation hook. $form['uid'] = array( '#type' => 'hidden', '#value' => 0, ); $form['introduction'] = array( '#type' => 'markup', '#value' => t( "This page controls commit access for the %title project. Unless otherwise indicated, all users listed in this table have permission to commit and tag files in this project's directory in @repository (%directory). The project owner is listed first and always has full access.", array( '%title' => $node->title, '%directory' => $project['directory'], '@repository' => $repository['name'], )), '#prefix' => '

', '#suffix' => '

', ); // The actual table is created inside the '#after_build' function. $form['table'] = array( '#type' => 'markup', '#value' => '', '#node' => $node, // remember that one for the '#after_build' function '#repository' => $repository, // and this one as well '#after_build' => array('versioncontrol_project_commit_access_table'), ); $form['table']['username'] = array( '#type' => 'textfield', //'#required' => TRUE, // already checked by the validation hook '#size' => 30, '#maxlength' => 60, '#autocomplete_path' => 'versioncontrol/user/autocomplete/'. $project['repo_id'], ); $form['table']['submit'] = array( '#type' => 'submit', '#value' => t('Grant access'), ); return $form; } /** * The maintainers table for the above commit access form, * constructed by means of an '#after_build' callback function. */ function versioncontrol_project_commit_access_table(&$form, $form_values) { $node = $form['#node']; $project = $node->versioncontrol_project; $project_repository = $form['#repository']; $rows = array(); $header = array(); $header[] = array('data' => t('Drupal username'), 'field' => 'name'); $header[] = array('data' => t('Actions')); // The owner of the node automatically gets commit access, list this // user as the first row in the table, and don't allow any operations. $rows[] = array( theme('username', $node), // theme_username() only needs ->uid and ->name. ''. t('locked') .'' ); // ...and now the co-maintainers... foreach ($project['comaintainer_uids'] as $uid) { $user = user_load(array('uid' => $uid)); if (!$user) { continue; // safety check, should not happen } // Indicate any maintainers whose account is no longer approved. if (!versioncontrol_is_account_authorized($uid, $project_repository)) { $username = ''. theme('username', $user) .' ('. t('repository access disabled') .')'; } else { $username = theme('username', $user); } $rows[] = array($username, l( t('Delete'), 'node/'. $node->nid .'/commit-access/'. $uid .'/delete' )); } // The "new user" and "Grant access" controls from the original form. $rows[] = array(drupal_render($form['username']), drupal_render($form['submit'])); // The table is done, rejoice. $form['#value'] = theme('table', $header, $rows); return $form; } /** * The validation hook for the commit access form: * check the validity of the new project maintainer that should be added. */ function versioncontrol_project_commit_access_form_validate($form_id, $form_values, $form) { $username = $form_values['username']; $repo_id = $form_values['repo_id']; $maintainer_uids = explode(',', $form_values['maintainer_uids']); if (empty($username)) { form_set_error('username', t('You must specify a valid user name.')); return; } $result = db_fetch_object(db_query("SELECT name, uid FROM {users} WHERE name = '%s'", $username)); if (!isset($result)) { form_set_error('username', t('%user is not a valid user on this site.', array('%user' => $username)), 'error'); return; } $uid = $result->uid; $vcs_username = versioncontrol_get_account_username_for_uid($repo_id, $uid); if (!isset($vcs_username)) { $repository = versioncontrol_get_repository($repo_id); form_set_error('username', t('%user does not have an account in @repository.', array('%user' => $username, '@repository' => $repository['name'])), 'error'); return; } if (in_array($uid, $maintainer_uids)) { form_set_error('username', t('%user already has commit access for this project.', array('%user' => $username)), 'error'); return; } // save the uid in the form so we don't have to look it up again in // submit(). we also stash user, since it's not set directly when // using the special theme function to generate the form. form_set_value($form['uid'], $uid); } /** * The submit hook for the commit access form: add a new project maintainer. */ function versioncontrol_project_commit_access_form_submit($form_id, $form_values) { $nid = $form_values['nid']; $uid = $form_values['uid']; db_query("INSERT INTO {versioncontrol_project_comaintainers} (nid, uid) VALUES (%d, %d)", $nid, $uid); $user = new stdClass(); $user->uid = $form_values['uid']; $user->name = $form_values['username']; drupal_set_message(t('Commit access has been granted to !user.', array( '!user' => theme('username', $user), ))); } /** * Form callback for 'node/$nid/commit-access/$uid/delete': * Provide a form to confirm deletion of a user's commit access. */ function versioncontrol_project_commit_access_delete_confirm($node, $uid) { $form = array(); $project = $node->versioncontrol_project; if ($node->type == 'project_project') { project_project_set_breadcrumb($node, array(l($node->title, 'node/'. $node->nid))); } if ($uid == $node->uid) { drupal_set_title(t('User is locked')); $form['nodelete'] = array( '#type' => 'markup', '#value' => t('You cannot delete commit access for !user because this user is the owner of the project.', array('!user' => theme('username', $node))), ); return $form; } $user = user_load(array('uid' => $uid)); if (!in_array($uid, $project['comaintainer_uids'])) { drupal_set_title(t('User is not a maintainer')); $form['nodelete'] = array( '#type' => 'markup', '#value' => t('!user does not have commit access so you cannot delete that.', array('!user' => theme('username', $user))), ); return $form; } $form['nid'] = array('#type' => 'value', '#value' => $node->nid); $form['uid'] = array('#type' => 'value', '#value' => $uid); return confirm_form($form, t('Are you sure you want to delete commit access for !user?', array('!user' => theme('username', $user))), 'node/'. $node->nid .'/commit-access', t('This action cannot be undone.'), t('Delete'), t('Cancel') ); } /** * Delete the repository when the confirmation form is submitted. */ function versioncontrol_project_commit_access_delete_confirm_submit($form_id, $form_values) { $nid = $form_values['nid']; $uid = $form_values['uid']; $user = user_load(array('uid' => $uid)); db_query('DELETE FROM {versioncontrol_project_comaintainers} WHERE nid = %d AND uid = %d', $nid, $uid); drupal_set_message(t('Commit access for !user has been deleted.', array('!user' => theme('username', $user)))); return 'node/'. $nid .'/commit-access'; } /** * Callback for "node/$nid/commitlog". */ function versioncontrol_project_commitlog($node) { $title = check_plain($node->title); drupal_set_title(t('Commits for !title', array('!title' => $title))); if ($node->type == 'project_project') { project_project_set_breadcrumb($node, array(l($title, 'node/'. $node->nid))); } $_REQUEST['nids'] = (string) $node->nid; return commitlog_commits_page(); } /** * Callback for "node/$nid/developers". */ function versioncontrol_project_developers($node) { $title = check_plain($node->title); drupal_set_title(t('Developers for !title', array('!title' => $title))); if (module_exists('project')) { project_project_set_breadcrumb($node, array(l($title, 'node/'. $node->nid))); } $project = $node->versioncontrol_project; $constraints = versioncontrol_project_get_commit_constraints( array(), array('nids' => array($project['nid'])) ); return theme('versioncontrol_user_statistics', $constraints); } /** * Implementation of hook_user(): * Add a list of projects to the user page that the user has committed to. */ function versioncontrol_project_user($type, &$edit, &$user, $category = NULL) { if ($type == 'view') { $constraints = array('uids' => array($user->uid)); $statistics = versioncontrol_project_get_statistics($constraints); $project_titles = array(); foreach ($statistics as $project_stats) { $nid = $project_stats['project']['nid']; $project_name = l(check_plain($project_stats['name']), 'node/'. $nid); $number_commits_text = format_plural( $project_stats['number_commits'], '1 commit', '@count commits' ); $project_titles[] = array( 'value' => t('!project-name (!how-many-commits)', array( '!project-name' => $project_name, '!how-many-commits' => $number_commits_text, )), ); } if (!empty($project_titles)) { return array(t('Projects') => $project_titles); } } } /** * Implementation of hook_block(): Present a list of the most active projects. */ function versioncontrol_project_block($op = 'list', $delta = 0) { if ($op == 'list') { $blocks[0]['info'] = t('Most active projects'); return $blocks; } else if ($op == 'view') { if ($delta == 0) { $interval = 7 * 24 * 60 * 60; $limit = 15; $constraints = array('date_lower' => time() - $interval); $statistics = versioncontrol_project_get_statistics($constraints, $limit); $project_titles = array(); foreach ($statistics as $project_stats) { $nid = $project_stats['project']['nid']; $project_titles[] = l(check_plain($project_stats['name']), 'node/'. $nid); } if (!empty($project_titles)) { $block = array( 'subject' => t('Most active projects'), 'content' => theme('item_list', $project_titles), ); return $block; } } } } /** * Return all content types that are marked as "project node" types. */ function versioncontrol_project_get_content_types() { return variable_get('versioncontrol_project_content_types', array()); } /** * Return a modified version of the given commit constraints that includes * the given project specific constraints in a way that the Version Control API * can understand it. * * @param $constraints * The constraints array that you already prepared for passing to * versioncontrol_get_commits(). * * @param $project_constraints * An array of project specific constraints. * Possible array elements handled by this module are: * * - 'nids': An array of project node ids. * If given, only commits for these projects will be returned. * - 'maintainer_uids': An array of Drupal user ids. If given, the result set * will only contain commits that correspond to one of the projects * that any of the specified users maintain. */ function versioncontrol_project_get_commit_constraints($constraints, $project_constraints) { if (isset($project_constraints['maintainer_uids'])) { $paths = array(); $projects = versioncontrol_project_get_projects(array( 'maintainer_uids' => $project_constraints['maintainer_uids'], ), TRUE); foreach ($projects as $nid => $project) { $paths[] = $project->directory; } // In case the caller has also passed a 'paths' constraint, intersect this // with the paths determined by the 'maintainer_uid' constraint // so that there is a real 'AND' condition. $constraints['paths'] = isset($constraints['paths']) ? array_intersect($paths, $constraints['paths']) : $paths; } if (isset($project_constraints['nids'])) { $paths = array(); $projects = versioncontrol_project_get_projects(array( 'nids' => $project_constraints['nids'], ), TRUE); foreach ($projects as $nid => $project) { $paths[] = $project['directory']; } // In case the caller has also passed a 'paths' constraint, intersect this // with the paths determined by the 'nids' constraint so that there is // a real 'AND' condition. $constraints['paths'] = isset($constraints['paths']) ? array_intersect($paths, $constraints['paths']) : $paths; } return $constraints; } /** * Convenience function to retrieve one single project by project node id. * * @param $nid * The node id of the project node. * @param $include_unpublished * If FALSE (which is the default), this function returns NULL * when the project node is unpublished, even if the project exists. * If TRUE, the function doesn't care whether the node is published or not. * * @return * The project array for the given project node, * which consists of the following elements: * * - 'nid': The node id of the project node. * - 'owner_uid': The Drupal user id of the project owner. * - 'repo_id': The repository id of the repository where the project resides. * - 'directory': The project directory inside the repository. * - 'maintainer_uids': An array containing all maintainer uids, that is, * the project owner and the co-maintainers. * - 'comaintainer_uids': An array containing all co-maintainer uids. * * If there is no version control data for that node, NULL is returned. */ function versioncontrol_project_get_project($nid, $include_unpublished = FALSE) { $projects = versioncontrol_project_get_projects(array('nids' => array($nid)), $include_unpublished); foreach ($projects as $nid => $project) { return $project; } return NULL; } /** * Convenience function to retrieve the projects maintained by a given user. * * @param $uid * The user whose projects will be retrieved. * @param $include_unpublished * If FALSE (which is the default), this function does not return projects * where the corresponding node is unpublished. * If TRUE, all projects are returned, regardless of their status. * * @return * An array of projects with the project node id as key, where each element * is again a structured array that consists of the following elements: * * - 'nid': The node id of the project node. * - 'owner_uid': The Drupal user id of the project owner. * - 'repo_id': The repository id of the repository where the project resides. * - 'directory': The project directory inside the repository. * - 'maintainer_uids': An array containing all maintainer uids, that is, * the project owner and the co-maintainers. * - 'comaintainer_uids': An array containing all co-maintainer uids. * * If the given user doesn't maintain at least one project, * an empty array is returned. */ function versioncontrol_project_get_projects_for_maintainer($uid, $include_unpublished = FALSE) { return versioncontrol_project_get_projects( array('maintainer_uids' => array($uid)), $include_unpublished ); } /** * Retrieve a set of projects that match the given constraints. * * @param $constraints * An optional array of constraints. Possible array elements are: * * - 'nids': An array of project node ids. If given, only the corresponding * projects will be returned. * - 'owner_uids': An array of Drupal user ids. If given, only projects with * one of these users as project owner will be returned. * - 'maintainer_uids': An array of Drupal user ids. If given, only projects * with at least one of these users as maintainer will be returned. * * @param $include_unpublished * If FALSE (which is the default), this function does not return projects * where the corresponding node is unpublished. * If TRUE, all projects are returned, regardless of their status. * * @return * An array of projects with the project node id as key, where each element * is again a structured array that consists of the following elements: * * - 'nid': The node id of the project node. * - 'owner_uid': The Drupal user id of the project owner. * - 'repo_id': The repository id of the repository where the project resides. * - 'directory': The project directory inside the repository. * - 'maintainer_uids': An array containing all maintainer uids, that is, * the project owner and the co-maintainers. * - 'comaintainer_uids': An array containing all co-maintainer uids. * * If not a single project matches these constraints, * an empty array is returned. */ function versioncontrol_project_get_projects($constraints = array(), $include_unpublished = FALSE) { static $project_cache = array(); $constraints_serialized = serialize($constraints); if (isset($project_cache[$constraints_serialized])) { return $project_cache[$constraints_serialized]; } $and_constraints = array(); $params = array(); if (!$include_unpublished) { $and_constraints[] = 'n.status = 1'; } // project node constraints if (isset($constraints['nids'])) { if (empty($constraints['nids'])) { $and_constraints[] = 'FALSE'; } else { $or_constraints = array(); foreach ($constraints['nids'] as $nid) { $or_constraints[] = 'p.nid = %d'; $params[] = $nid; } $and_constraints[] = implode(' OR ', $or_constraints); } } // project owner constraints if (isset($constraints['owner_uids'])) { if (empty($constraints['owner_uids'])) { $and_constraints[] = 'FALSE'; } else { foreach ($constraints['owner_uids'] as $uid) { $or_constraints[] = 'p.owner_uid = %d'; $params[] = $uid; } $and_constraints[] = implode(' OR ', $or_constraints); } } // project maintainer (= owner or co-maintainer) constraints $maintainer_join = ''; if (isset($constraints['maintainer_uids'])) { if (empty($constraints['maintainer_uids'])) { $and_constraints[] = 'FALSE'; } else { $maintainer_join = ' LEFT JOIN {versioncontrol_project_comaintainers} c ON p.nid = c.nid'; foreach ($constraints['maintainer_uids'] as $uid) { $or_constraints[] = 'p.owner_uid = %d'; $params[] = $uid; $or_constraints[] = 'c.uid = %d'; $params[] = $uid; } $and_constraints[] = implode(' OR ', $or_constraints); } } $where = empty($and_constraints) ? '' : ' WHERE '. implode(' AND ', $and_constraints); $result = db_query('SELECT DISTINCT(p.nid), p.owner_uid, p.repo_id, p.directory FROM {versioncontrol_project_projects} p INNER JOIN {node} n ON p.nid = n.nid'. $maintainer_join . $where, $params); $projects = array(); while ($project = db_fetch_object($result)) { $comaintainers = _versioncontrol_project_get_comaintainers($project->nid); $projects[$project->nid] = array( 'nid' => $project->nid, 'owner_uid' => $project->owner_uid, 'repo_id' => $project->repo_id, 'directory' => $project->directory, 'maintainer_uids' => array_merge( array($project->owner_uid), $comaintainers ), 'comaintainer_uids' => $comaintainers, ); } $project_cache[$constraints_serialized] = $projects; // cache the results return $projects; } /** * Retrieve the list of all maintainers for the given project. * * @param $nid * The node id of the project node for which the maintainers * should be retrieved. * * @return * An array of maintainer uids for the given project. There is at least * one maintainer for each project (the node owner). */ function _versioncontrol_project_get_comaintainers($nid) { $result = db_query('SELECT uid FROM {versioncontrol_project_comaintainers} c WHERE nid = %d', $nid); $maintainers = array(); while ($maintainer = db_fetch_object($result)) { $maintainers[] = $maintainer->uid; } return $maintainers; } /** * Retrieve the project for the given item (file or directory) in a repository. * * @param $repository * The repository which contains the item. * @param $item_path * The path of the item. * @param $include_unpublished * If FALSE (which is the default), this function returns NULL * when the project node is unpublished, even if the project exists. * If TRUE, the function doesn't care whether the node is published or not. * * @return * The project array of the project that contains the item. * It consists of the following elements: * * - 'nid': The node id of the project node. * - 'owner_uid': The Drupal user id of the project owner. * - 'repo_id': The repository id of the repository where the project resides. * - 'directory': The project directory inside the repository. * - 'maintainer_uids': An array containing all maintainer uids, that is, * the project owner and the co-maintainers. * - 'comaintainer_uids': An array containing all co-maintainer uids. * * If the item doesn't belong to any project, NULL is returned. */ function versioncontrol_project_get_project_for_item($repository, $item_path, $include_unpublished = FALSE) { static $projects_for_item = array(); $repo_id = $repository['repo_id']; if (!isset($projects_for_item[$repo_id])) { $projects_for_item[$repo_id] = array(); } $current_path = $item_path; $current_paths = array(); // Now, we loop over the path, using the most restrictive path first, // and query the database to find a matching published project node. while (TRUE) { $current_paths[] = $current_path; // Use the cache if possible. if (isset($projects_for_item[$repo_id][$current_path])) { $project_for_item = $projects_for_item[$repo_id][$current_path]; } else { $result = db_query("SELECT p.nid, p.owner_uid, p.repo_id, p.directory FROM {versioncontrol_project_projects} p INNER JOIN {node} n ON p.nid = n.nid WHERE p.repo_id = %d AND p.directory = '%s'". ($include_unpublished ? '' : ' AND n.status = 1'), $repo_id, $current_path); if ($project = db_fetch_object($result)) { $comaintainers = _versioncontrol_project_get_comaintainers($project->nid); $project_for_item = array( 'nid' => $project->nid, 'owner_uid' => $project->owner_uid, 'repo_id' => $project->repo_id, 'directory' => $project->directory, 'maintainer_uids' => array_merge( array($project->owner_uid), $comaintainers ), 'comaintainer_uids' => $comaintainers, ); } } if (isset($project_for_item)) { foreach ($current_paths as $path) { $projects_for_item[$repo_id][$path] = $project_for_item; } return ($project_for_item === FALSE) ? NULL : $project_for_item; } if ($current_path == dirname($current_path)) { break; // stop at the top-level directory } $current_path = dirname($current_path); } // If we didn't find it already, cache the answer as not-found foreach ($current_paths as $path) { $projects_for_item[$repo_id][$path] = FALSE; } return NULL; } /** * Retrieve the projects that are associated to the affected items * in the given commit. If no projects can be associated to the commit items, * an empty array is returned. * * @param $commit * The commit for which all associated projects should be retrieved. * @param $include_unpublished * If FALSE (which is the default), this function does not return projects * where the corresponding node is unpublished. * If TRUE, all projects are returned, regardless of their status. * * @return * A structured array containing the list of associated projects. * The project node ids are the array keys, and the complete project arrays * are given in the array values. */ function versioncontrol_project_get_projects_for_commit($commit, $include_unpublished = FALSE) { // First, try if the directory item itself is wholly inside a project. // In that case, we wouldn't need to check each item separately. $commit_directory_item = versioncontrol_get_directory_item($commit); $project = versioncontrol_project_get_project_for_item( $commit['repository'], $commit['directory'], $include_unpublished ); if (isset($project)) { return array($project['nid'] => $project); } // The commit spans zero or multiple projects - we need to check each item. $projects = array(); $commit_actions = versioncontrol_get_commit_actions($commit); foreach ($commit_actions as $path => $action) { $project = versioncontrol_project_get_project_for_item( $commit['repository'], $path, $include_unpublished ); if (isset($project)) { $projects[$project['nid']] = $project; } } return $projects; } /** * Add or update a project. This operation will fail if the given project's nid * doesn't correspond to an existing node. * * @param $project * The project array containing the new or existing repository. * In comparison to standard project arrays, the 'owner uid' and * 'maintainer uids' elements are disregarded, so it only needs to contain * the following elements: * * - 'nid': The node id of the project node. * - 'repo_id': The repository id of the repository where the project resides. * - 'directory': The project directory inside the repository. * - 'comaintainer_uids': Optional. If given, the list of co-maintainer uids * will be updated with this one. * * @return * The complete project array if the project was created, or NULL if not. */ function versioncontrol_project_set_project($project) { if ($project['repo_id'] == 0) { // "don't use version control integration" versioncontrol_project_delete_project($project['nid']); db_query("INSERT INTO {versioncontrol_project_projects} (nid, owner_uid, repo_id, directory) VALUES (%d, %d, %d, '%s')", $project['nid'], 0, 0, ''); return $project; } if (!isset($project['owner_uid'])) { // Get the project node, so we can determine the node owner. $node = node_load($project['nid']); if (!isset($node->nid)) { // the node doesn't exist return NULL; } $project['owner_uid'] = $node->uid; } // If the entry already exists, delete it. db_query('DELETE FROM {versioncontrol_project_projects} WHERE nid = %d', $project['nid']); db_query("INSERT INTO {versioncontrol_project_projects} (nid, owner_uid, repo_id, directory) VALUES (%d, %d, %d, '%s')", $project['nid'], $project['owner_uid'], $project['repo_id'], $project['directory']); if (isset($project['comaintainer_uids'])) { // Delete the list, so we can easily insert them. db_query('DELETE FROM {versioncontrol_project_comaintainers} WHERE nid = %d', $project['nid']); // Insert each comaintainer into the database. foreach ($project['comaintainer_uids'] as $uid) { if ($uid == $project['owner_uid']) { continue; // the owner is not a co-maintainer } db_query('INSERT INTO {versioncontrol_project_comaintainers} (nid, uid) VALUES (%d, %d)', $project['nid'], $uid); } } else { $project['comaintainer_uids'] = array(); } $project['maintainer_uids'] = array($project['owner_uid']); foreach ($project['comaintainer_uids'] as $comaintainer_uid) { $project['maintainer_uids'][] = $comaintainer_uid; } // Insertion/update hooks, if any, will go here. return $project; } /** * Delete all version control data (project information and * maintainer associations) for a given project from the database. * If no project with this node id exists, nothing will be done. * * @param $nid * The node id of the project node whose version control data * should be deleted. */ function versioncontrol_project_delete_project($nid) { $count = db_result(db_query('SELECT COUNT(*) FROM {versioncontrol_project_projects} WHERE nid = %d', $nid)); if (!$count) { return; // nothing to delete } // Deletion hooks, if any, will go here. db_query('DELETE FROM {versioncontrol_project_projects} WHERE nid = %d', $nid); db_query('DELETE FROM {versioncontrol_project_comaintainers} WHERE nid = %d', $nid); } /** * Check if the given user may create project nodes. * In case the "restrict project creation" option on the admin form is enabled * and the user lacks a VCS account, project node creation is not allowed. * * @return * TRUE if the user may create project nodes, or FALSE otherwise. */ function versioncontrol_project_creation_is_allowed($uid) { if (variable_get('versioncontrol_project_restrict_creation', 1)) { if (user_access('administer nodes') || user_access('administer version control systems')) { return TRUE; } $user_accounts = versioncontrol_get_accounts(array('uids' => array($uid))); if (empty($user_accounts)) { return FALSE; } } return TRUE; } /** * Implementation of hook_versioncontrol_commit_access(): * Project based commit restrictions. * * @param $commit * A commit array of the commit that is about to happen. As it's not * committed yet, it's not yet in the database as well, which means that * any commit info retrieval functions won't work on this commit array. * It also means there's no 'commit_id', 'revision' and 'date' elements like * in regular commit arrays. The 'message' element might or might not be set. * @param $commit_actions * The commit actions of the above commit that is about to happen. * Further information retrieval functions won't work on this array as well. * Also, the 'source items' element of each action and the 'revision' element * of each item in these actions might not be set. * @param $branch * The target branch where the commit will happen (a string like 'DRUPAL-5'). * If the respective backend doesn't support branches, * this may be NULL instead. * * @return * An array with error messages (without trailing newlines) if the commit * should not be allowed, or an empty array if we're indifferent, * or TRUE if the commit should be allowed no matter what other * commit access callbacks say. */ function versioncontrol_project_versioncontrol_commit_access($commit, $commit_actions, $branch = NULL) { if (empty($commit_actions)) { return array(); // no idea if this is ever going to happen, but let's be prepared } if (!variable_get('versioncontrol_project_restrict_commits', 1)) { return array(); // we don't need no restrictions } $error_messages = _versioncontrol_project_check_drupal_user($commit, $user); if (!empty($error_messages)) { return $error_messages; } foreach ($commit_actions as $path => $action) { $item = versioncontrol_get_affected_item($action); $error_messages = _versioncontrol_project_check_item_access( $commit['repository'], $item, $user, $commit['directory'], TRUE ); if (!empty($error_messages)) { return $error_messages; } } return array(); } /** * Implementation of hook_versioncontrol_branch_access(): * Determine if the given branch may be assigned to a set of items. * * @param $branch * A structured array that consists of the following elements: * * - 'branch_name': The name of the target branch * (a string like 'DRUPAL-6--1'). * - 'action': Specifies what is going to happen with the branch. This is * VERSIONCONTROL_ACTION_ADDED if the branch is being created, * VERSIONCONTROL_ACTION_MOVED if it's being renamed, * or VERSIONCONTROL_ACTION_DELETED if it is slated for deletion. * - 'uid': The Drupal user id of the committer, or 0 if no Drupal user * could be associated to the committer. * - 'username': The system specific VCS username of the committer. * - 'repository': The repository where the branching occurs, * given as a structured array, like the return value * of versioncontrol_get_repository(). * - 'directory': The deepest-level directory in the repository that is * common to all of the branched items. * * @param $branched_items * An array of all items that are affected by the branching operation. * Compared to standard item arrays, the ones in here may not have the * 'revision' element set and can optionally contain a 'source branch' * element that specifies the original branch name of this item. * (For $op == 'delete', 'source branch' is never set.) * An empty $branched_items array means that the whole repository has been * branched. * * @return * An array with error messages (without trailing newlines) if the branch * may not be assigned, or an empty array if we're indifferent, * or TRUE if the branch may be assigned no matter what other * branch access callbacks say. */ function versioncontrol_project_versioncontrol_branch_access($branch, $branched_items) { return _versioncontrol_project_branch_or_tag_access($branch, $branched_items); } /** * Implementation of hook_versioncontrol_tag_access(): * Determine if the given tag may be assigned to a set of items. * * @param $tag * A structured array that consists of the following elements: * * - 'tag_name': The name of the tag (a string like 'DRUPAL-6--1-1'). * - 'action': Specifies what is going to happen with the tag. This is * VERSIONCONTROL_ACTION_ADDED if the tag is being created, * VERSIONCONTROL_ACTION_MOVED if it's being renamed, * or VERSIONCONTROL_ACTION_DELETED if it is slated for deletion. * - 'uid': The Drupal user id of the committer, or 0 if no Drupal user * could be associated to the committer. * - 'username': The system specific VCS username of the committer. * - 'repository': The repository where the tagging occurs, * given as a structured array, like the return value * of versioncontrol_get_repository(). * - 'directory': The deepest-level directory in the repository that is * common to all of the tagged items. * - 'message': The tag message that the user has given. If the version * control system doesn't support tag messages, this is an empty string. * * @param $tagged_items * An array of all items that are affected by the tagging operation. * Compared to standard item arrays, the ones in here may not have the * 'revision' element set and can optionally contain a 'source branch' * element that specifies the original branch name of this item. * (For $op == 'move' or $op == 'delete', 'source branch' is never set.) * An empty $tagged_items array means that the whole repository has been * tagged. * * @return * An array with error messages (without trailing newlines) if the tag * may not be assigned, or an empty array if we're indifferent, * or TRUE if the tag may be assigned no matter what other * tag access callbacks say. */ function versioncontrol_project_versioncontrol_tag_access($tag, $tagged_items) { return _versioncontrol_project_branch_or_tag_access($tag, $tagged_items); } /** * Shared code between branch_access() and tag_access(): * Disallow assigning tags or branches in directories that don't have * a project assigned. */ function _versioncontrol_project_branch_or_tag_access($branch_or_tag, $items) { if (!variable_get('versioncontrol_project_restrict_commits', 1)) { return array(); // we don't need no restrictions } $error_messages = _versioncontrol_project_check_drupal_user($branch_or_tag, $user); if (!empty($error_messages)) { return $error_messages; } // For version control systems where branching and tagging is done // per-directory (that is, where !empty($items)), check if the given items // are maintained by the user. foreach ($items as $item) { $error_messages = _versioncontrol_project_check_item_access( $branch_or_tag['repository'], $item, $user, $branch_or_tag['directory'], FALSE ); if (!empty($error_messages)) { return $error_messages; } } // No objections, the user may execute the branch or tag operation. return array(); } /** * Determine if the user for the given Drupal user id exists, and return * an appropriate error message array. * * @param $object * The commit or branch/tag operation array that should be checked. * @param $user * A by-reference parameter that will be filled with the return value * of user_load(). That is, if this function returns an empty array * (no errors) then this is the user object of the committer. * * @return * An empty array if the user exists, or an array filled * with an error message if the user doesn't exist. */ function _versioncontrol_project_check_drupal_user($object, &$user) { if ($object['uid'] != 0) { $user = user_load(array('uid' => $object['uid'])); } if (!$user) { $backends = versioncontrol_get_backends(); $backend = $backends[$object['repository']['vcs']]; $error_message = t( "** ERROR: no Drupal user matches !vcs user '!user'. ** Please contact a !vcs administrator for help.", array('!vcs' => $backend['name'], '!user' => $object['username']) ); return array($error_message); } return array(); } /** * Determine if an item is part of one of the projects that are maintained * by the given user, and return an appropriate error message array. * * @param $repository * The repository where the item is located. * @param $item * The item that should be checked on access rights. * @param $user * The author who tries to commit/branch/tag this item. * @param $directory_label * The directory that is being shown in the error message. * @param $default_item_access * Determines if items may be accessed if they don't belong to any project. * TRUE if unassociated items may be accessed, or FALSE if not. * * @return * An empty array if the item is inside one of the user's projects, * or an array filled with an error message if it isn't. */ function _versioncontrol_project_check_item_access($repository, $item, $user, $directory_label, $default_item_access) { $is_allowed = $default_item_access; $project = versioncontrol_project_get_project_for_item($repository, $item['path']); $is_allowed = isset($project) ? in_array($user->uid, $project['maintainer_uids']) : $default_item_access; if (!$is_allowed) { // All projects have been checked and continue has not yet been hit, // which means the item is outside the user's projects. $error_message = t( '** Access denied: !user does not have permission to operate on files in: ** !directory', array('!user' => $user->name, '!directory' => $directory_label) ) . _versioncontrol_project_get_project_error_appendix($project); return array($error_message); } return array(); } function _versioncontrol_project_get_project_error_appendix($project = NULL) { if (isset($project) && $project['owner_uid'] != 0) { $owner = user_load(array('uid' => $project['owner_uid'])); } if (!$owner) { return ''; } return "\n" . t( '** Please contact the owner of this project ** (!user, !user-url) ** and request to be added as a project maintainer.', array('!user' => $owner->name, '!user-url' => 'user/'. $owner->uid) ); } /** * Implementation of hook_versioncontrol_account(): * Delete commit access for accounts that are being deleted. */ function versioncontrol_project_versioncontrol_account($op, $uid, $username, $repository, $additional_data = array()) { if ($op == 'delete') { db_query("UPDATE {versioncontrol_project_projects} SET repo_id = 0, owner_uid = 0, directory = '' WHERE owner_uid = %d", $uid); db_query("DELETE FROM {versioncontrol_project_comaintainers} WHERE uid = %d", $uid); } } /** * Retrieve statistics about projects from a set of commits. * * @param $constraints * The constraints (as passed to versioncontrol_get_commits()) that define * which commits are taken into consideration for the statistics. * @param $limit * If given, only the n most active projects (in terms of commits) * will be included in the result. * * @return * An array of information about the projects. Each element contains * one project's statistics and consists of the following elements: * - 'project': The project array, as returned * by versioncontrol_project_get_project(). * - 'name': The title of the project node. * - 'number_commits': The number of commits to this project * (out of the given commits). * - 'first_commit': The commit array of the first commit to this project * (out of the given commits). * - 'last_commit': The commit array of the last commit to this project * (out of the given commits). * * The array is sorted in descending order of the commit count, * in other words, the first item corresponds to the most active project. * If no commits matching the given constraints can be found or no projects * were affected by this set of commits, an empty array is returned. */ function versioncontrol_project_get_statistics($constraints, $limit = NULL) { $projects = array(); $project_nids = array(); $first_commits = array(); $last_commits = array(); $commits = versioncontrol_get_commits($constraints); // Find out which projects are affected, and how active they have been. foreach ($commits as $commit) { $commit_projects = versioncontrol_project_get_projects_for_commit($commit); foreach ($commit_projects as $nid => $project) { $projects[$nid] = $project; $project_nids[] = $nid; // Write the first commit for each iteration, the last one encountered is // the first one in time. ($commits is sorted reverse chronologically.) $first_commits[$nid] = $commit; // Similar for the last commit which is the first one that we get to see. if (!isset($last_commits[$nid])) { $last_commits[$nid] = $commit; } } } // Sort by commit count. $commit_counts = array_count_values($project_nids); arsort($commit_counts); // Sort by commit count, and if requested by the caller, // limit the output to the n most active projects. $limited_commit_counts = array(); $i = 0; foreach ($commit_counts as $nid => $count) { if (isset($limit) && $i >= $limit) { break; } $limited_commit_counts[$nid] = $count; ++$i; } $commit_counts = $limited_commit_counts; $project_nids = array_unique(array_keys($limited_commit_counts)); // No projects in the result, so we can return right now. if (empty($project_nids)) { return array(); } // Ok, now construct a query and get the desired node titles $placeholders = array(); foreach ($project_nids as $nid) { $placeholders[] = '%d'; } $result = db_query('SELECT nid, title FROM {node} WHERE nid IN ('. implode(',', $placeholders) .')', $project_nids); $statistics = array(); while ($node = db_fetch_object($result)) { $statistics[$node->nid] = array( 'project' => $projects[$node->nid], 'name' => $node->title, 'number_commits' => $commit_counts[$node->nid], 'first_commit' => $first_commits[$node->nid], 'last_commit' => $last_commits[$node->nid], ); } if (empty($statistics)) { return array(); } $sorted_statistics = array(); foreach ($project_nids as $nid) { // the $project_nids array is already in the right order if (isset($statistics[$nid])) { $sorted_statistics[] = $statistics[$nid]; } } return $sorted_statistics; }