array($user->uid))); $accounts_flat = array(); $repo_ids = array(); foreach ($accounts as $uid => $usernames_by_repository) { foreach ($usernames_by_repository as $repo_id => $username) { $accounts_flat[] = array('uid' => $uid, 'username' => $username, 'repo_id' => $repo_id); $repo_ids[] = $repo_id; } } $repositories = versioncontrol_get_repositories(array('repo_ids' => $repo_ids)); foreach ($accounts_flat as $account) { if (isset($repositories[$account['repo_id']])) { versioncontrol_delete_account( $repositories[$account['repo_id']], $account['uid'], $account['username'] ); } } break; } } /** * Implementation of hook_menu(). */ function versioncontrol_menu($may_cache) { global $user; $items = array(); $user_access = user_access('use version control systems'); $admin_access = user_access('administer version control systems'); if ($may_cache) { // If Version Control API is used without the Project module, // we need to define our own version of /admin/project // so the rest of our admin pages all work. if (!module_exists('project')) { $items[] = array( 'path' => 'admin/project', 'title' => t('Project administration'), 'description' => t('Administrative interface for project management and related modules.'), 'callback' => 'system_admin_menu_block_page', 'access' => $admin_access, 'type' => MENU_NORMAL_ITEM, ); } $items[] = array( 'path' => 'admin/project/versioncontrol-settings', 'title' => t('Version control settings'), 'description' => t('Configure settings for Version Control API and related modules.'), 'callback' => 'drupal_get_form', 'callback arguments' => array('versioncontrol_admin_settings'), 'access' => $admin_access, 'type' => MENU_NORMAL_ITEM, ); $items[] = array( 'path' => 'admin/project/versioncontrol-settings/general', 'title' => t('General'), 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => -1, ); $items[] = array( 'path' => 'admin/project/versioncontrol-repositories', 'title' => t('VCS repositories'), 'description' => t('Define and configure what version control repositories are connected to your site, and how to integrate each repository with repository browser tools such as ViewVC or WebSVN.'), 'callback' => 'drupal_get_form', 'callback arguments' => array('versioncontrol_admin_repository_list'), 'access' => $admin_access, 'type' => MENU_NORMAL_ITEM, ); $items[] = array( 'path' => 'admin/project/versioncontrol-repositories/list', 'title' => t('List'), 'type' => MENU_DEFAULT_LOCAL_TASK, ); $backends = versioncontrol_get_backends(); $i = 2; foreach ($backends as $vcs => $backend) { $items[] = array( 'path' => 'admin/project/versioncontrol-repositories/add-'. $vcs, 'title' => t('Add @vcs repository', array('@vcs' => $backend['name'])), 'callback' => 'drupal_get_form', 'callback arguments' => array('versioncontrol_admin_repository_edit', 0, $vcs), 'access' => $admin_access, 'type' => MENU_LOCAL_TASK, 'weight' => $i, ); ++$i; } $items[] = array( 'path' => 'admin/project/versioncontrol-accounts', 'title' => t('VCS accounts'), 'description' => t('Manage associations of Drupal users to VCS user accounts.'), 'callback' => 'versioncontrol_admin_account_list_page', 'access' => $admin_access, 'type' => MENU_NORMAL_ITEM, ); $items[] = array( 'path' => 'admin/project/versioncontrol-accounts/list', 'title' => t('List'), 'type' => MENU_DEFAULT_LOCAL_TASK, 'weight' => 0, ); foreach ($backends as $vcs => $backend) { if (versioncontrol_backend_implements($vcs, 'import_accounts')) { $items[] = array( 'path' => 'admin/project/versioncontrol-accounts/import', 'title' => t('Import'), 'description' => t('Import an existing set of VCS user accounts.'), 'callback' => 'drupal_get_form', 'callback arguments' => array('versioncontrol_admin_account_import_form'), 'access' => $admin_access, 'type' => MENU_LOCAL_TASK, 'weight' => 2, ); break; } } foreach ($backends as $vcs => $backend) { if (versioncontrol_backend_implements($vcs, 'export_accounts')) { $items[] = array( 'path' => 'admin/project/versioncontrol-accounts/export', 'title' => t('Export'), 'description' => t('Export VCS user accounts of a specific repository.'), 'callback' => 'versioncontrol_admin_account_export_page', 'access' => $admin_access, 'type' => MENU_LOCAL_TASK, 'weight' => 3, ); break; } } $items[] = array( 'path' => 'versioncontrol/register', // [."/$uid" [."/$repo_id"]] 'title' => t('Get commit access'), 'callback' => 'versioncontrol_account_register_page', 'access' => TRUE, // access checking is done in the callback 'type' => MENU_SUGGESTED_ITEM, ); // autocomplete callback for Drupal usernames that have access to // the repository given in arg(3). $items[] = array( 'path' => 'versioncontrol/user/autocomplete', // ."/$repo_id/$string" 'title' => t('Version control user autocomplete'), 'callback' => 'versioncontrol_user_autocomplete', 'access' => $user_access, 'type' => MENU_CALLBACK, ); } else { if (is_numeric(arg(4))) { $items[] = array( 'path' => 'admin/project/versioncontrol-repositories/edit/'. arg(4), 'title' => t('Edit repository'), 'callback' => 'drupal_get_form', 'callback arguments' => array('versioncontrol_admin_repository_edit', arg(4)), 'access' => $admin_access, 'type' => MENU_CALLBACK, ); $items[] = array( 'path' => 'admin/project/versioncontrol-repositories/delete/'. arg(4), 'title' => t('Delete repository'), 'callback' => 'drupal_get_form', 'callback arguments' => array('versioncontrol_admin_repository_delete_confirm', arg(4)), 'access' => $admin_access, 'type' => MENU_CALLBACK, ); } if (arg(0) == 'user' && is_numeric(arg(1)) && arg(2) == 'edit') { $items = array_merge($items, _versioncontrol_user_account_edit_items($user)); } } return $items; } /** * This needs to be split out of versioncontrol_menu() because user.module * only sets 'user/$uid/edit/account' as default local task if * hook_user($op='categories) returns more than one entry. Which means * we have to provide those separately from the menu hook. */ function _versioncontrol_user_account_edit_items($user, $op = 'items') { static $items; static $categories; $user_access = user_access('use version control systems'); $admin_access = user_access('administer version control systems'); $account_access = ($user_access && $uid == $user->uid) || $admin_access; // Cache the items, we don't want this being done twice. if (isset($items)) { if ($op == 'categories') { return $categories; } return $items; } $items = array(); $categories = array(); $uid = arg(1); $accounts = versioncontrol_get_accounts(array('uids' => array($uid)), TRUE); if (empty($accounts)) { $items = array(); $categories = array(); return array(); } $repo_ids = array(); foreach ($accounts as $uid => $usernames_by_repository) { foreach ($usernames_by_repository as $repo_id => $username) { $repo_ids[] = $repo_id; } } $repositories = versioncontrol_get_repositories(array('repo_ids' => $repo_ids)); // The form for editing an account. foreach ($accounts as $uid => $usernames_by_repository) { foreach ($usernames_by_repository as $repo_id => $username) { if (isset($repositories[$repo_id])) { $items[] = array( 'path' => 'user/'. $uid .'/edit/versioncontrol/'. $repo_id, 'title' => check_plain($repositories[$repo_id]['name']), 'callback' => 'drupal_get_form', 'callback arguments' => array( 'versioncontrol_account_edit_form', $uid, $username, $repositories[$repo_id] ), 'access' => $account_access, 'type' => MENU_LOCAL_TASK, 'weight' => 99, ); $categories[] = array( 'name' => 'versioncontrol/'. $repo_id, 'title' => check_plain($repositories[$repo_id]['name']), 'weight' => 99, ); } } } if ($op == 'categories') { return $categories; } return $items; } /** * Implementation of hook_perm(). */ function versioncontrol_perm() { return array( 'administer version control systems', 'use version control systems', ); } /** * Get a list of all backends and more detailed information about each of them. * * @return * A structured array containing information about all known backends. * Array keys are the unique string identifier of the version control system. * The corresponding array values are again structured arrays and consist * of elements with the following keys: * * 'name': The user-visible name of the VCS. * 'description': A short description of the backend, if possible not longer * than one or two sentences. * 'capabilities': An array listing optional capabilities, in addition to the * required functionality like retrieval of detailed * commit information. Array values can be an arbitrary * combination of VERSIONCONTROL_CAPABILITY_* values. If no * additional capabilities are supported by the backend, * this array will be empty. * * If no single backends can be found, an empty array is returned. * * A real-life example of such a result array can be found * in the FakeVCS example module. */ function versioncontrol_get_backends() { static $backends; if (!isset($backends)) { $backends = module_invoke_all('versioncontrol_backends'); } return $backends; } /** * Determine if a given backend module implements a specific backend function. * * @param $vcs * The unique string identifier of the version control system. * @param $function * The function name without module prefix. * * @return * TRUE if the backend implements the function, or FALSE otherwise. */ function versioncontrol_backend_implements($vcs, $function) { if (function_exists('versioncontrol_'. $vcs .'_'. $function)) { return TRUE; } return FALSE; } /** * Call a function from the desired VCS backend and return its result value. * * @param $vcs * The unique string identifier of the version control system. * @param $function * The function name without module prefix. * @param $args * An array of arguments that will be passed to the backend function. * * @return * Returns the result of the backend function. The result value of calls * where the backend function has no implementation is undefined, as they * are supposed to be checked with versioncontrol_backend_implements() * before those functions are called. */ function _versioncontrol_call_backend($vcs, $function, $args) { return call_user_func_array('versioncontrol_'. $vcs .'_'. $function, $args); } /** * Call a function from the desired VCS backend, return its result value, * and make sure that the objects parameter (which is a reference) * can be altered by the backend. * * @param $vcs * The unique string identifier of the version control system. * @param $function * The function name without module prefix. * @param $objects * A modifyable object array that will be passed to the backend function. * @param $constraints * An array of VCS specific filter constraints. * * @return * Returns the result of the backend function. The result value of calls * where the backend function has no implementation is undefined, as they * are supposed to be checked with versioncontrol_backend_implements() * before those functions are called. */ function _versioncontrol_call_backend_alter($vcs, $function, &$objects, $constraints = array()) { $function = 'versioncontrol_'. $vcs .'_'. $function; return $function($objects, $constraints); } /** * Determine all user account authorization methods * (site-wide approval, per-repository approval, ...) * by invoking hook_versioncontrol_authorization_methods(). * The default method (no authorization required) is not included. * * @return * A structured array with the unique string identifier of the method as keys * and the user-visible description (wrapped in t()) as values. */ function versioncontrol_get_authorization_methods() { static $methods; if (!isset($methods)) { $methods = module_invoke_all('versioncontrol_authorization_methods'); } return $methods; } /** * Retrieve the currently selected authorization method. * This either 'none' or the unique string identifier * of another module's authentication method. */ function versioncontrol_get_current_authorization_method() { $current_method = variable_get('versioncontrol_authorization_method', 'none'); $methods = versioncontrol_get_authorization_methods(); if ($current_method != 'none' && !isset($methods[$current_method])) { // A (currently) non-existing method is given - reset this to 'none'. variable_set('versioncontrol_authorization_method', 'none'); $current_method = 'none'; } return $current_method; } /** * Convenience function for retrieving one single repository by repository id. * * @return * A single repository array that consists of the following elements: * * 'repo_id': The unique repository id. * 'name': The user-visible name of the repository. * 'vcs': The unique string identifier of the version control system * that powers this repository. * 'root': The root directory of the repository. In most cases, * this will be a local directory (e.g. '/var/repos/drupal'), * but it may also be some specialized string for remote repository * access. How this string may look like depends on the backend. * 'url_backend': The prefix (excluding the trailing underscore) * for URL backend retrieval functions. * '[xxx]_specific': An array of VCS specific additional repository * information. How this array looks like is defined by the * corresponding backend module (versioncontrol_[xxx]). * * If no repository corresponds to the given repository id, NULL is returned. */ function versioncontrol_get_repository($repo_id) { $repos = versioncontrol_get_repositories(array('repo_ids' => array($repo_id))); foreach ($repos as $repo_id => $repository) { return $repository; } return NULL; // in case of empty($repos) } /** * Retrieve a set of repositories that match the given constraints. * * @param $constraints * An optional array of constraints. Possible array elements are: * * 'vcs': An array of strings, like array('cvs', 'svn', 'git'). * If given, only commits for these backends will be returned. * 'repo_ids': An array of repository ids. * If given, only the corresponding repositories will be returned. * 'names': An array of repository names, like * array('Drupal CVS', 'Experimental SVN'). If given, only * repositories with these repository names will be returned. * '[xxx]_specific': An array of VCS specific constraints. How this array * looks like is defined by the corresponding backend * module (versioncontrol_[xxx]). Other backend modules * won't get to see this constraint, so in theory you can * provide one of those for each backend in one * single query. * * @return * An array of repositories where the key of each element is the * repository id. The corresponding value contains a structured array * with the following keys: * * 'repo_id': The unique repository id. * 'name': The user-visible name of the repository. * 'vcs': The unique string identifier of the version control system * that powers this repository. * 'root': The root directory of the repository. In most cases, * this will be a local directory (e.g. '/var/repos/drupal'), * but it may also be some specialized string for remote repository * access. How this string may look like depends on the backend. * 'url_backend': The prefix (excluding the trailing underscore) * for URL backend retrieval functions. * '[xxx]_specific': An array of VCS specific additional repository * information. How this array looks like is defined by the * corresponding backend module (versioncontrol_[xxx]). * * If not a single repository matches these constraints, * an empty array is returned. */ function versioncontrol_get_repositories($constraints = array()) { $backends = versioncontrol_get_backends(); list($and_constraints, $params) = _versioncontrol_construct_repository_constraints($constraints, $backends); // All the constraints have been gathered, assemble them to a WHERE clause. $where = empty($and_constraints) ? '' : ' WHERE '. implode(' AND ', $and_constraints); $result = db_query('SELECT * FROM {versioncontrol_repositories} r'. $where, $params); // Sort the retrieved repositories by backend. $repositories_by_backend = array(); while ($repository = db_fetch_array($result)) { if (!isset($backends[$repository['vcs']])) { // don't include repositories for which no backend module exists continue; } if (!isset($repositories_by_backend[$repository['vcs']])) { $repositories_by_backend[$repository['vcs']] = array(); } $repository[$repository['vcs'] .'_specific'] = array(); $repositories_by_backend[$repository['vcs']][$repository['repo_id']] = $repository; } $repositories_by_backend = _versioncontrol_amend_repositories( $repositories_by_backend, $backends ); // Add the fully assembled repositories to the result array. $result_repositories = array(); foreach ($repositories_by_backend as $vcs => $vcs_repositories) { foreach ($vcs_repositories as $repository) { $result_repositories[$repository['repo_id']] = $repository; } } return $result_repositories; } /** * Assemble a list of query constraints given as string array that's * supposed to be imploded with an SQL "AND", and a $params array containing * the corresponding parameter values for all the '%d' and '%s' placeholders. */ function _versioncontrol_construct_repository_constraints($constraints, $backends) { $and_constraints = array(); $params = array(); // Filter out repositories of which the corresponding backend is not enabled, // and handle the 'vcs' constraint at the same time. $or_constraints = array(); foreach ($backends as $vcs => $backend) { if (isset($constraints['vcs']) && !in_array($vcs, $constraints['vcs'])) { continue; // don't include the vcs if the caller has not specified it } $or_constraints[] = "r.vcs = '%s'"; $params[] = $vcs; } if (!empty($or_constraints)) { $and_constraints[] = '('. implode(' OR ', $or_constraints) .')'; } if (isset($constraints['repo_ids'])) { $or_constraints = array(); foreach ($constraints['repo_ids'] as $repo_id) { $or_constraints[] = "r.repo_id = '%d'"; $params[] = $repo_id; } $and_constraints[] = '('. implode(' OR ', $or_constraints) .')'; } if (isset($constraints['names'])) { $or_constraints = array(); foreach ($constraints['names'] as $name) { $or_constraints[] = "r.name = '%s'"; $params[] = $name; } $and_constraints[] = '('. implode(' OR ', $or_constraints) .')'; } return array($and_constraints, $params); } /** * Fetch VCS specific repository data additions, either by ourselves (if the * VERSIONCONTROL_FLAG_AUTOADD_REPOSITORIES flag has been set by the backend) * and/or by calling [vcs_backend]_alter_repositories(). */ function _versioncontrol_amend_repositories($repositories_by_backend, $backends, $constraints = array()) { foreach ($repositories_by_backend as $vcs => $vcs_repositories) { $is_autoadd = in_array(VERSIONCONTROL_FLAG_AUTOADD_REPOSITORIES, $backends[$vcs]['flags']); if ($is_autoadd) { $repo_ids = array(); foreach ($vcs_repositories as $repo_id => $repository) { $repo_ids[] = $repo_id; } $additions = _versioncontrol_db_get_additions( 'versioncontrol_'. $vcs .'_repositories', 'repo_id', $repo_ids ); foreach ($additions as $repo_id => $additions) { if (isset($vcs_repositories[$repo_id])) { $vcs_repositories[$repo_id][$vcs .'_specific'] = $additions; } } } $vcs_specific_constraints = isset($constraints[$vcs .'_specific']) ? $constraints[$vcs .'_specific'] : array(); // Provide an opportunity for the backend to add its own stuff. if (versioncontrol_backend_implements($vcs, 'alter_repositories')) { _versioncontrol_call_backend_alter( $vcs, 'alter_repositories', $vcs_repositories, $vcs_specific_constraints ); } $repositories_by_backend[$vcs] = $vcs_repositories; } return $repositories_by_backend; } /** * Retrieve a set of commits that match the given constraints. * * @param $constraints * An optional array of constraints. Possible array elements are: * * 'vcs': An array of strings, like array('cvs', 'svn', 'git'). * If given, only commits for these backends will be returned. * 'repo_ids': An array of repository ids. If given, only commits * for the corresponding repositories will be returned. * 'branches': An array of strings, specified separately for each repository, * like array($repo_id => array('HEAD', 'DRUPAL-5')). * If given, only commits on the given branches will be returned. * 'paths': An array of strings (item locations), specified separately * for each repository, like * array($repo_id => array( * '/trunk/contributions/modules/versioncontrol', * '/trunk/contributions/themes/b2', * )). * If given, only commits that affected this item (or its children, * in case the item is a directory) will be returned. * 'commit_ids': An array of commit ids. If given, only commits matching * those ids will be returned. * 'commit_id_lower': A commit id. If given, the result set will not contain * commits earlier than this lower bound. Mind that * commit ids only correspond to chronological order * within the bounds of each repository. * 'commit_id_upper': A commit id. If given, the result set will not contain * commits later than this upper bound. Mind that * commit ids only correspond to chronological order * within the bounds of each repository. * 'date_lower': A Unix timestamp. If given, the result set will not contain * commits earlier than this lower bound. * 'date_upper': A Unix timestamp. If given, the result set will not contain * commits later than this upper bound. * 'uids': An array of Drupal user ids. If given, the result set will only * contain commits that correspond to any of the specified users. * 'usernames': An array of system-specific usernames (the ones that the * version control systems themselves get to see), like * array('dww', 'jpetso'). If given, the result set will only * contain commits that correspond to any of the specified users. * '[xxx]_specific': An array of VCS specific constraints. How this array * looks like is defined by the corresponding backend * module (versioncontrol_[xxx]). Other backend modules * won't get to see this constraint, so in theory you can * provide one of those for each backend in one * single query. * * @param $result_count * This variable will be set to the overall number of commits that matched * the constraints. You're likely to need this if you use paging, in all * other cases this will be the same value as count($commits). * @param $page * If given, this function only return a subset of the result, where * $page == 0 would return the first $limit matching commits, * $page == 1 would return the second set, and so on. * @param $limit * The number of query results to return per page. * * @return * An array of commits, reversely sorted by the time of the commit. * Each element contains a structured array with the following keys: * * 'commit_id': The Drupal-specific commit identifier (a simple integer) * which is unique among all commits in all repositories. * 'repository': The repository where this commit occurred. * This is a structured array, like a single element of * what is returned by versioncontrol_get_repositories(). * 'date': The time when the revision was committed, given as Unix timestamp. * 'uid': The Drupal user id of the committer. * 'username': The system specific VCS username of the committer. * 'directory': The deepest-level directory in the repository that is common * to all the changed items, e.g. '/src' if the commit changed * the files '/src/subdir/code.php' and '/src/README.txt'. * 'message': The commit message. * 'revision': The VCS specific repository-wide revision identifier, * like '' in CVS, '27491' in Subversion or some SHA-1 key * in various distributed version control systems. * If there is no such revision (which may be the case for * version control systems that don't support atomic commits) * then the 'revision' element is an empty string. * '[xxx]_specific': An array of VCS specific additional commit information. * How this array looks like is defined by the * corresponding backend module (versioncontrol_[xxx]). * * If not a single commit matches these constraints, * an empty array is returned. */ function versioncontrol_get_commits($constraints = array(), &$result_count = NULL, $page = NULL, $limit = 10) { $backends = versioncontrol_get_backends(); list($and_constraints, $params) = _versioncontrol_construct_commit_constraints($constraints, $backends); $where = empty($and_constraints) ? '' : ' WHERE '. implode(' AND ', $and_constraints); $result = db_query('SELECT c.commit_id, c.date, c.uid, c.username, c.directory, c.message, c.revision, r.repo_id, r.vcs FROM {versioncontrol_commits} c INNER JOIN {versioncontrol_repositories} r ON c.repo_id = r.repo_id '. $where .' ORDER BY c.date DESC', $params); $commits = array(); $commit_ids_by_backend = array(); $repo_ids = array(); $selected_backends = array(); while ($row = db_fetch_object($result)) { if (!isset($selected_backends[$row->vcs])) { $selected_backends[$row->vcs] = $backends[$row->vcs]; } if (!isset($commit_ids_by_backend[$row->vcs])) { $commit_ids_by_backend[$row->vcs] = array(); } $commit_ids_by_backend[$row->vcs][] = $row->commit_id; $repo_ids[] = $row->repo_id; $commits[] = array( 'commit_id' => $row->commit_id, 'repo_id' => $row->repo_id, // 'repo_id' is replaced by 'repository' further down 'date' => $row->date, 'uid' => $row->uid, 'username' => $row->username, 'directory' => $row->directory, 'message' => $row->message, 'revision' => $row->revision, $row->vcs .'_specific' => array(), ); } if (empty($commits)) { return array(); } $repositories = versioncontrol_get_repositories(array('repo_ids' => $repo_ids)); foreach ($selected_backends as $vcs => $backend) { $is_autoadd = in_array(VERSIONCONTROL_FLAG_AUTOADD_COMMITS, $backend['flags']); _versioncontrol_amend_objects($commits, $commit_ids_by_backend, $repositories, $vcs, $constraints, 'commits', 'commit_id', $is_autoadd); } _versioncontrol_filter_commits($commits, $constraints); $result_count = count($commits); if (isset($page)) { return _versioncontrol_page_commits($commits, $page, $limit); } return $commits; } /** * Assemble a list of query constraints given as string array that's * supposed to be imploded with an SQL "AND", and a $params array containing * the corresponding parameter values for all the '%d' and '%s' placeholders. */ function _versioncontrol_construct_commit_constraints($constraints, $backends) { // Filter by a set of common constraints whose code is shared with the // branch and tag retrieval functions. list($and_constraints, $params) = _versioncontrol_construct_common_constraints($constraints, 'commit_id', 'c', 'r'); // Filter out commits of which the corresponding backend is not enabled, // and handle the 'vcs' constraint at the same time. $or_constraints = array(); foreach ($backends as $vcs => $backend) { if (isset($constraints['vcs']) && !in_array($vcs, $constraints['vcs'])) { continue; // don't include the vcs if the caller has not specified it } $or_constraints[] = "r.vcs = '%s'"; $params[] = $vcs; } if (!empty($or_constraints)) { $and_constraints[] = '('. implode(' OR ', $or_constraints) .')'; } // Filter by item paths. if (isset($constraints['paths'])) { $or_constraints = array(); foreach ($constraints['paths'] as $path) { list($path_or_constraints, $path_params) = _versioncontrol_construct_path_constraint('c.directory', $path); $or_constraints = array_merge($or_constraints, $path_or_constraints); $params = array_merge($params, $path_params); } if (!empty($or_constraints)) { $and_constraints[] = '('. implode(' OR ', $or_constraints) .')'; } } return array($and_constraints, $params); } /** * Assemble a list of SQL "AND" constraints that both the commit and * the branch/tag retrieval functions make use of. */ function _versioncontrol_construct_common_constraints($constraints, $primary_key_name, $table_alias, $repository_table_alias = 'r') { $and_constraints = array(); $params = array(); // Filter by commit ids. if (isset($constraints[$primary_key_name .'s'])) { $or_constraints = array(); foreach ($constraints[$primary_key_name .'s'] as $primary_key) { $or_constraints[] = $table_alias .".". $primary_key_name ." = '%d'"; $params[] = $primary_key; } if (!empty($or_constraints)) { $and_constraints[] = '('. implode(' OR ', $or_constraints) .')'; } } // Filter by lower and/or upper commit id bounds. if (isset($constraints[$primary_key_name .'_lower'])) { $and_constraints[] = "(". $table_alias .".". $primary_key_name ." >= '%d')"; $params[] = $constraints[$primary_key_name .'_lower']; } if (isset($constraints[$primary_key_name .'_upper'])) { $and_constraints[] = "(". $table_alias .".". $primary_key_name ." <= '%d')"; $params[] = $constraints[$primary_key_name .'_upper']; } // Filter by repository ids. if (isset($constraints['repo_ids'])) { $or_constraints = array(); foreach ($constraints['repo_ids'] as $repo_id) { $or_constraints[] = $repository_table_alias .".repo_id = '%d'"; $params[] = $repo_id; } if (!empty($or_constraints)) { $and_constraints[] = '('. implode(' OR ', $or_constraints) .')'; } } // Filter by lower and/or upper date bounds. if (isset($constraints['date_lower'])) { $and_constraints[] = "(". $table_alias .".date >= '%d')"; $params[] = $constraints['date_lower']; } if (isset($constraints['date_upper'])) { $and_constraints[] = "(". $table_alias .".date <= '%d')"; $params[] = $constraints['date_upper']; } // Filter by Drupal user ids. if (isset($constraints['uids'])) { $or_constraints = array(); foreach ($constraints['uids'] as $uid) { $or_constraints[] = $table_alias .".uid = '%d'"; $params[] = $uid; } if (!empty($or_constraints)) { $and_constraints[] = '('. implode(' OR ', $or_constraints) .')'; } } // Filter by VCS specific user names. if (isset($constraints['usernames'])) { $or_constraints = array(); foreach ($constraints['usernames'] as $username) { $or_constraints[] = $table_alias .".username = '%s'"; $params[] = $username; } if (!empty($or_constraints)) { $and_constraints[] = '('. implode(' OR ', $or_constraints) .')'; } } return array($and_constraints, $params); } /** * Construct an SQL constraint with all possible values for the path column * given a path from $constraints['paths']. */ function _versioncontrol_construct_path_constraint($column, $path) { $or_constraints = array(); $paths = array(); $current_path = $path; // Now, we loop over the path, using the most restrictive path first, // and create a condition for each possible path. while (TRUE) { $or_constraints[] = $column ." = '%s'"; $paths[] = $current_path; if ($current_path == dirname($current_path)) { break; // we reached the top-level directory } $current_path = dirname($current_path); } // Also include any children paths of the given one. $or_constraints[] = $column ." LIKE '%s'"; $paths[] = $path . (($path[strlen($path)-1] == '/') ? '%' : '/%'); return array($or_constraints, $paths); } /** * Code that is shared between the commit and branch/tag retrieval functions: * Fetch VCS specific object data additions, either by ourselves * (if $is_autoadd == TRUE) and/or by calling [vcs_backend]_alter_$type(). */ function _versioncontrol_amend_objects(&$objects, $primary_keys_by_backend, $repositories, $vcs, $constraints, $type, $primary_key_name, $is_autoadd) { if ($is_autoadd) { $additions = _versioncontrol_db_get_additions( // e.g. 'versioncontrol_fakevcs_commits' or 'versioncontrol_cvs_tag_operations' 'versioncontrol_'. $vcs .'_'. $type, $primary_key_name, $primary_keys_by_backend[$vcs] ); $additions_by_primary_key = array(); foreach ($additions as $primary_key => $additions) { $additions_by_primary_key[$primary_key] = $additions; } // Apply the additions to their respective commits. foreach ($objects as $key => $object) { $objects[$key]['repository'] = $repositories[$object['repo_id']]; $objects[$key][$vcs .'_specific'] = $additions_by_primary_key[$object[$primary_key_name]]; unset($objects[$key]['repo_id']); } } $vcs_specific_constraints = isset($constraints[$vcs .'_specific']) ? $constraints[$vcs .'_specific'] : NULL; // Provide an opportunity for the backend to add its own stuff. // Calls [xxx]_alter_commits(), [xxx]_alter_branch_operations() // or [xxx]_alter_tag_operations(). if (versioncontrol_backend_implements($vcs, 'alter_'. $type)) { _versioncontrol_call_backend_alter( $vcs, 'alter_'. $type, $objects, $vcs_specific_constraints ); } } /** * Filter out false posivites that could not be filtered by the SQL query. */ function _versioncontrol_filter_commits(&$commits, $constraints) { foreach ($commits as $key => $commit) { // First, branches. That's easier. if (isset($constraints['branches'])) { $branches = versioncontrol_get_commit_branches($commit); $valid_branch = FALSE; foreach ($constraints['branches'] as $branch) { if (in_array($branch, $branches)) { $valid_branch = TRUE; break; } } if (!$valid_branch) { // the commit doesn't match the 'branches' constraint, // don't include it in the result value. unset($commits[$key]); } } // Then, paths. More complicated, but doable. if (isset($constraints['paths'])) { $valid_path = FALSE; foreach ($constraints['paths'] as $path) { // Append a trailing slash to the commit and constraint directories. $commit_directory = ($commit['directory'] == '/') ? '/' : $commit['directory'] .'/'; if ($path[strlen($path)-1] != '/') { $path .= '/'; } // Check if $commit_directory is at least as deep as $path. if (strpos($commit_directory, $path) !== FALSE) { $valid_path = TRUE; break; } } if (!$valid_path) { // Bad luck, we need to check on the single item paths // as the common directory is too high-level to tell. $actions = versioncontrol_get_commit_actions($commit); foreach ($actions as $action) { $items = isset($action['current item']) ? array($action['current item']) : array(); $items = isset($action['source items']) ? array_merge($items, $action['source items']) : $items; foreach ($items as $item) { foreach ($constraints['paths'] as $path) { if ($path[strlen($path)-1] != '/') { $path .= '/'; } if (strpos($item['path'] .'/', $path) !== FALSE) { // The item is the same or a child element of the path constraint $valid_path = TRUE; break; } } } if ($valid_path) { break; } } } if (!$valid_path) { // the commit doesn't match the 'paths' constraint, // don't include it in the result value. unset($commits[$key]); } } } } /** * Return a subset of the given commit according to the $page and $limit values. */ function _versioncontrol_page_commits($commits, $page, $limit) { $i = 0; $paged_commits = array(); foreach ($commits as $commit) { if ($i >= ($page * $limit) && $i < (($page+1) * $limit)) { $paged_commits[] = $commit; } ++$i; } return $paged_commits; } /** * Convenience function for retrieving one single branch operation * by branch op id. * * @return * A single branch operation array that consists of the following elements: * * 'branch_op_id': The unique identifier (a simple integer) * of this branch operation. * 'branch_name': The name of the target branch (a string like 'DRUPAL-6--1'). * 'action': Specifies what happened to the branch. This is * VERSIONCONTROL_ACTION_ADDED if the branch was created, * VERSIONCONTROL_ACTION_MOVED if was renamed, * or VERSIONCONTROL_ACTION_DELETED if was deleted. * 'date': The time when the branching was done, given as Unix timestamp. * 'uid': The Drupal user id of the committer. * 'username': The system specific VCS username of the committer. * 'repository': The repository where the branching occurred, * 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 the branched items, e.g. '/src' if the files * '/src/subdir/code.php' and '/src/README.txt' were branched. * '[xxx]_specific': An array of VCS specific additional branch operation info. * How this array looks like is defined by the * corresponding backend module (versioncontrol_[xxx]). * The 'branch_id' element will already be included in * this array for the convenience of VCS backends. * * If no branch operation corresponds to the given branch op id, * NULL is returned. */ function versioncontrol_get_branch_operation($branch_op_id) { $branches = versioncontrol_get_branch_operations( array('branch_op_ids' => array($branch_op_id)) ); foreach ($branches as $branch_op_id => $branch) { return $branch; } return NULL; // in case of empty($branch_ops) } /** * Convenience function for retrieving one single tag operation by tag op id. * * @return * A single tag operation array that consists of the following elements: * * 'tag_op_id': The unique identifier (a simple integer) * of this tag operation. * 'tag_name': The name of the tag (a string like 'DRUPAL-6--1-1'). * 'action': Specifies what happened to the tag. This is * VERSIONCONTROL_ACTION_ADDED if the tag was created, * VERSIONCONTROL_ACTION_MOVED if was renamed, * or VERSIONCONTROL_ACTION_DELETED if was deleted. * 'date': The time when the tagging was done, given as Unix timestamp. * 'uid': The Drupal user id of the committer. * 'username': The system specific VCS username of the committer. * 'repository': The repository where the tagging occurred, * 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 the tagged items, e.g. '/src' if the files * '/src/subdir/code.php' and '/src/README.txt' were tagged. * '[xxx]_specific': An array of VCS specific additional tag operation info. * How this array looks like is defined by the * corresponding backend module (versioncontrol_[xxx]). * * If no tag operation corresponds to the given tag op id, NULL is returned. */ function versioncontrol_get_tag_operation($tag_op_id) { $tags = versioncontrol_get_tag_operations( array('tag_op_ids' => array($tag_op_id)) ); foreach ($tags as $tag_op_id => $tag) { return $tag; } return NULL; // in case of empty($tag_ops) } /** * Retrieve a set of branch operations that match the given constraints. * (Branch operation means the stuff that happens when commands like * 'cvs tag -b' or 'svn cp /trunk/project /branches/project/2.x' are executed.) * * @param $constraints * An optional array of constraints. Possible array elements are: * * 'repo_ids': An array of repository ids. If given, only branch operations * for the corresponding repositories will be returned. * 'branch_op_ids': An array of branch operation ids. If given, only * branch operations matching those ids will be returned. * 'branch_op_id_lower': A branch operation id. If given, the result set will * not contain branch operations earlier than this * lower bound. Mind that branch operation ids only * correspond to chronological order within the bounds * of each repository. * 'branch_op_id_upper': A branch operation id. If given, the result set will * not contain branch operations later than this * upper bound. Mind that branch operation ids only * correspond to chronological order within the bounds * of each repository. * 'date_lower': A Unix timestamp. If given, the result set will not contain * branch operations earlier than this lower bound. * 'date_upper': A Unix timestamp. If given, the result set will not contain * branch operations later than this upper bound. * 'uids': An array of Drupal user ids. If given, the result set will only * contain branch operations that correspond to any of the * specified users. * 'usernames': An array of system-specific usernames (the ones that the * version control systems themselves get to see), like * array('dww', 'jpetso'). If given, the result set will only * contain branch operations that correspond to any of the * specified users. * '[xxx]_specific': An array of VCS specific constraints. How this array * looks like is defined by the corresponding backend * module (versioncontrol_[xxx]). Other backend modules * won't get to see this constraint, so in theory you can * provide one of those for each backend in one * single query. * * @return * An array of branch operations, reversely sorted by the time of the branching. * Each element contains a structured array with the following keys: * * 'branch_op_id': The unique identifier (a simple integer) * of this branch operation. * 'branch_name': The name of the target branch (a string like 'DRUPAL-6--1'). * 'action': Specifies what happened to the branch. This is * VERSIONCONTROL_ACTION_ADDED if the branch was created, * VERSIONCONTROL_ACTION_MOVED if was renamed, * or VERSIONCONTROL_ACTION_DELETED if was deleted. * 'date': The time when the branching was done, given as Unix timestamp. * 'uid': The Drupal user id of the committer. * 'username': The system specific VCS username of the committer. * 'repository': The repository where the branching occurred, * 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 the branched items, e.g. '/src' if the files * '/src/subdir/code.php' and '/src/README.txt' were branched. * '[xxx]_specific': An array of VCS specific additional branch operation info. * How this array looks like is defined by the * corresponding backend module (versioncontrol_[xxx]). * The 'branch_id' element will already be included in * this array for the convenience of VCS backends. * * If not a single branch operation matches these constraints, * an empty array is returned. */ function versioncontrol_get_branch_operations($constraints = array()) { $backends = versioncontrol_get_backends(); // Filter by a set of common constraints whose code is shared with the // commit and tag retrieval functions. list($and_constraints, $params) = _versioncontrol_construct_common_constraints($constraints, 'branch_op_id', 'bo', 'r'); // Filter by branch names. if (isset($constraints['branch_names'])) { $or_constraints = array(); foreach ($constraints['branch_names'] as $branch_name) { $or_constraints[] = "b.name = '%s'"; $params[] = $branch_name; } if (!empty($or_constraints)) { $and_constraints[] = '('. implode(' OR ', $or_constraints) .')'; } } $where = empty($and_constraints) ? '' : ' WHERE '. implode(' AND ', $and_constraints); $result = db_query('SELECT bo.branch_op_id, bo.branch_id, b.branch_name, bo.action, bo.date, bo.uid, bo.username, bo.directory, r.repo_id, r.vcs FROM {versioncontrol_branch_operations} bo INNER JOIN {versioncontrol_branches} b ON bo.branch_id = b.branch_id INNER JOIN {versioncontrol_repositories} r ON b.repo_id = r.repo_id '. $where .' ORDER BY bo.date DESC', $params); $branches = array(); $branch_op_ids_by_backend = array(); $repo_ids = array(); $selected_backends = array(); while ($row = db_fetch_object($result)) { if (!isset($selected_backends[$row->vcs])) { $selected_backends[$row->vcs] = $backends[$row->vcs]; } if (!isset($branch_op_ids_by_backend[$row->vcs])) { $branch_op_ids_by_backend[$row->vcs] = array(); } $branch_op_ids_by_backend[$row->vcs][] = $row->branch_op_id; $repo_ids[] = $row->repo_id; $branches[] = array( 'branch_op_id' => $row->branch_op_id, 'repo_id' => $row->repo_id, // 'repo_id' is replaced by 'repository' further down 'branch_name' => $row->branch_name, 'action' => $row->action, 'date' => $row->date, 'uid' => $row->uid, 'username' => $row->username, 'directory' => $row->directory, $row->vcs .'_specific' => array( 'branch_id' => $row->branch_id, ), ); } if (empty($branches)) { return array(); } $repositories = versioncontrol_get_repositories(array('repo_ids' => $repo_ids)); foreach ($selected_backends as $vcs => $backend) { $is_autoadd = in_array(VERSIONCONTROL_FLAG_AUTOADD_BRANCH_OPERATIONS, $backend['flags']); _versioncontrol_amend_objects($branches, $branch_op_ids_by_backend, $repositories, $vcs, $constraints, 'branch_operations', 'branch_op_id', $is_autoadd); } return $branches; } /** * Retrieve a set of tag operations that match the given constraints. * (Tag operation means the stuff that happens when commands like 'cvs tag' * or 'svn cp /branches/project/2.x /tags/project/2.x/2.2' are executed.) * * @param $constraints * An optional array of constraints. Possible array elements are: * * 'repo_ids': An array of repository ids. If given, only tag operations * for the corresponding repositories will be returned. * 'tag_op_ids': An array of tag operation ids. If given, only tag operations * matching those ids will be returned. * 'tag_op_id_lower': A tag operation id. If given, the result set will not * contain tag operations earlier than this lower bound. * Mind that tag operation ids only correspond to * chronological order within the bounds * of each repository. * 'tag_op_id_upper': A tag operation id. If given, the result set will not * contain tag operations later than this upper bound. * Mind that tag operation ids only correspond to * chronological order within the bounds * of each repository. * 'date_lower': A Unix timestamp. If given, the result set will not contain * tag operations earlier than this lower bound. * 'date_upper': A Unix timestamp. If given, the result set will not contain * tag operations later than this upper bound. * 'uids': An array of Drupal user ids. If given, the result set will only * contain tag operations that correspond to any of the * specified users. * 'usernames': An array of system-specific usernames (the ones that the * version control systems themselves get to see), like * array('dww', 'jpetso'). If given, the result set will only * contain tag operations that correspond to any of the * specified users. * '[xxx]_specific': An array of VCS specific constraints. How this array * looks like is defined by the corresponding backend * module (versioncontrol_[xxx]). Other backend modules * won't get to see this constraint, so in theory you can * provide one of those for each backend in one * single query. * * @return * An array of tag operations, reversely sorted by the time of the tagging. * Each element contains a structured array with the following keys: * * 'tag_op_id': The unique identifier (a simple integer) * of this tag operation. * 'tag_name': The name of the tag (a string like 'DRUPAL-6--1-1'). * 'action': Specifies what happened to the tag. This is * VERSIONCONTROL_ACTION_ADDED if the tag was created, * VERSIONCONTROL_ACTION_MOVED if was renamed, * or VERSIONCONTROL_ACTION_DELETED if was deleted. * 'date': The time when the tagging was done, given as Unix timestamp. * 'uid': The Drupal user id of the committer. * 'username': The system specific VCS username of the committer. * 'repository': The repository where the tagging occurred, * 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 the tagged items, e.g. '/src' if the files * '/src/subdir/code.php' and '/src/README.txt' were tagged. * '[xxx]_specific': An array of VCS specific additional tag operation info. * How this array looks like is defined by the * corresponding backend module (versioncontrol_[xxx]). * * If not a single tag operation matches these constraints, * an empty array is returned. */ function versioncontrol_get_tag_operations($constraints = array()) { $backends = versioncontrol_get_backends(); // Filter by a set of common constraints whose code is shared with the // commit and branch retrieval functions. list($and_constraints, $params) = _versioncontrol_construct_common_constraints($constraints, 'tag_op_id', 't', 'r'); // Filter by branch names. if (isset($constraints['tag_names'])) { $or_constraints = array(); foreach ($constraints['tag_names'] as $tag_name) { $or_constraints[] = "t.name = '%s'"; $params[] = $tag_name; } if (!empty($or_constraints)) { $and_constraints[] = '('. implode(' OR ', $or_constraints) .')'; } } $where = empty($and_constraints) ? '' : ' WHERE '. implode(' AND ', $and_constraints); $result = db_query('SELECT t.tag_op_id, t.tag_name, t.action, t.date, t.uid, t.username, t.directory, r.repo_id, r.vcs FROM {versioncontrol_tag_operations} t INNER JOIN {versioncontrol_repositories} r ON t.repo_id = r.repo_id '. $where .' ORDER BY t.date DESC', $params); $tags = array(); $tag_op_ids_by_backend = array(); $repo_ids = array(); $selected_backends = array(); while ($row = db_fetch_object($result)) { if (!isset($selected_backends[$row->vcs])) { $selected_backends[$row->vcs] = $backends[$row->vcs]; } if (!isset($tag_op_ids_by_backend[$row->vcs])) { $tag_op_ids_by_backend[$row->vcs] = array(); } $tag_op_ids_by_backend[$row->vcs][] = $row->tag_op_id; $repo_ids[] = $row->repo_id; $tags[] = array( 'tag_op_id' => $row->tag_op_id, 'repo_id' => $row->repo_id, // 'repo_id' is replaced by 'repository' further down 'tag_name' => $row->tag_name, 'action' => $row->action, 'date' => $row->date, 'uid' => $row->uid, 'username' => $row->username, 'directory' => $row->directory, $row->vcs .'_specific' => array(), ); } if (empty($tags)) { return array(); } $repositories = versioncontrol_get_repositories(array('repo_ids' => $repo_ids)); foreach ($selected_backends as $vcs => $backend) { $is_autoadd = in_array(VERSIONCONTROL_FLAG_AUTOADD_TAG_OPERATIONS, $backend['flags']); _versioncontrol_amend_objects($tags, $tag_op_ids_by_backend, $repositories, $vcs, $constraints, 'tag_operations', 'tag_op_id', $is_autoadd); } return $tags; } /** * Retrieve a set of Drupal uid / VCS username mappings * that match the given constraints. * * @param $constraints * An optional array of constraints. Possible array elements are: * * 'uids': An array of Drupal user ids. If given, only accounts that * correspond to these Drupal users will be returned. * 'repo_ids': An array of repository ids. If given, only accounts * in the corresponding repositories will be returned. * 'usernames': An array of system specific VCS usernames, * like array('dww', 'jpetso'). If given, only accounts * with these VCS usernames will be returned. * 'usernames_by_repository': A structured array that looks like * array($repo_id => array('dww', 'jpetso'), ...). * You might want this if you combine multiple username and * repository constraints, otherwise you can well do without. * * @param $include_unauthorized * If FALSE (which is the default), this function does not return accounts * that are pending, queued, disabled, blocked, or otherwise non-approved. * If TRUE, all accounts are returned, regardless of their status. * * @return * A structured array that looks like * array($drupal_uid => array($repo_id => 'VCS username', ...), ...). * If not a single account matches these constraints, * an empty array is returned. */ function versioncontrol_get_accounts($constraints = array(), $include_unauthorized = FALSE) { $and_constraints = array(); $params = array(); // Filter by Drupal user id. if (isset($constraints['uids'])) { $or_constraints = array(); foreach ($constraints['uids'] as $uid) { $or_constraints[] = "uid = '%d'"; $params[] = $uid; } $and_constraints[] = '('. implode(' OR ', $or_constraints) .')'; } // Filter by repository id. if (isset($constraints['repo_ids'])) { $or_constraints = array(); foreach ($constraints['repo_ids'] as $repo_id) { $or_constraints[] = "repo_id = '%d'"; $params[] = $repo_id; } $and_constraints[] = '('. implode(' OR ', $or_constraints) .')'; } // Filter by VCS username. if (isset($constraints['usernames'])) { $or_constraints = array(); foreach ($constraints['usernames'] as $username) { $or_constraints[] = "username = '%s'"; $params[] = $username; } $and_constraints[] = '('. implode(' OR ', $or_constraints) .')'; } // Filter by usernames-by-repository. if (isset($constraints['usernames_by_repository'])) { $or_constraints = array(); foreach ($usernames_by_repository as $repo_id => $usernames) { $repo_constraint = "repo_id = '%d'"; $params[] = $repo_id; $username_constraints = array(); foreach ($usernames as $username) { $username_constraints[] = "username = '%s'"; $params[] = $username; } $or_constraints[] = '('. $repo_constraint .' AND ('. implode(' OR ', $username_constraints) .'))'; } $and_constraints[] = '('. implode(' OR ', $or_constraints) .')'; } // Execute the query. // All the constraints have been gathered, assemble them to a WHERE clause. $where = empty($and_constraints) ? '' : ' WHERE '. implode(' AND ', $and_constraints); $result = db_query('SELECT uid, repo_id, username FROM {versioncontrol_accounts}'. $where, $params); // Assemble the return value. $accounts = array(); while ($account = db_fetch_object($result)) { // Only include approved accounts, except in case the caller said otherwise. if ($include_unauthorized || versioncontrol_is_account_authorized($account->uid, $account->repo_id)) { if (!isset($accounts[$account->uid])) { $accounts[$account->uid] = array(); } $accounts[$account->uid][$account->repo_id] = $account->username; } } return $accounts; } /** * Retrieve the VCS username for a given Drupal user id in a specific * repository. If you need more detailed querying functionality than this * function provides, use versioncontrol_get_accounts() instead. * * @param $repo_id * The repository id of the repository where the user has its VCS account. * @param $username * The VCS specific username (a string) corresponding to the Drupal user. * @param $include_unauthorized * If FALSE (which is the default), this function does not return accounts * that are pending, queued, disabled, blocked, or otherwise non-approved. * If TRUE, all accounts are returned, regardless of their status. * * @return * The Drupal user id that corresponds to the given username and repository, * or NULL if no Drupal user could be associated to those. */ function versioncontrol_get_account_uid_for_username($repo_id, $username, $include_unauthorized = FALSE) { $result = db_query("SELECT uid, repo_id FROM {versioncontrol_accounts} WHERE username = '%s' AND repo_id = '%d'", $username, $repo_id); while ($account = db_fetch_object($result)) { // Only include approved accounts, except in case the caller said otherwise. if ($include_unauthorized || versioncontrol_is_account_authorized($account->uid, $account->repo_id)) { return $account->uid; } } return NULL; } /** * Retrieve the Drupal user id for a given VCS username in a specific * repository. If you need more detailed querying functionality than this * function provides, use versioncontrol_get_accounts() instead. * * @param $repo_id * The repository id of the repository where the user has its VCS account. * @param $uid * The Drupal user id corresponding to the VCS account. * @param $include_unauthorized * If FALSE (which is the default), this function does not return accounts * that are pending, queued, disabled, blocked, or otherwise non-approved. * If TRUE, all accounts are returned, regardless of their status. * * @return * The VCS username (a string) that corresponds to the given Drupal user * and repository, or NULL if no VCS account could be associated to those. */ function versioncontrol_get_account_username_for_uid($repo_id, $uid, $include_unauthorized = FALSE) { $result = db_query("SELECT uid, username, repo_id FROM {versioncontrol_accounts} WHERE uid = '%d' AND repo_id = '%d'", $uid, $repo_id); while ($account = db_fetch_object($result)) { // Only include approved accounts, except in case the caller said otherwise. if ($include_unauthorized || versioncontrol_is_account_authorized($account->uid, $account->repo_id)) { return $account->username; } } return NULL; } /** * Returns TRUE if the account is authorized to commit to the given * repository, or FALSE otherwise. Only call this function on existing * accounts or uid 0, the return value for all other uid/repo_id combinations * is undefined. * * @param $uid * The user id of the checked account. * @param $repo_id * The repository id of the repository where the status should be checked. * (Note that the user's authorization status may differ for each repository.) */ function versioncontrol_is_account_authorized($uid, $repo_id) { if (!$uid) { return FALSE; } $approved = array(); foreach (module_implements('versioncontrol_is_account_authorized') as $module) { $function = $module .'_versioncontrol_is_account_authorized'; // If at least one hook_versioncontrol_is_account_authorized returns FALSE, // the account is assumed not to be approved. if ($function($uid, $repo_id) === FALSE) { return FALSE; } } return TRUE; } /** * Retrieve the URL of the repository viewer that displays the given commit * in the corresponding repository. * * @param $commit * The commit whose view URL should be retrieved. * * @return * The commit view URL corresponding to the given arguments. * An empty string is returned if no commit view URL has been defined, * or if the commit cannot be viewed for any reason. */ function versioncontrol_get_url_commit_view($commit) { if (empty($commit['revision'])) { return ''; } $urls = _versioncontrol_get_repository_urls($commit['repository']); return strtr($urls['commit_view'], array( '%revision' => $commit['revision'], )); } /** * Retrieve the URL of the repository viewer that displays the given item * in the corresponding repository. * * @param $repository * The repository that the item is located in. * @param $item * The item whose view URL should be retrieved. * * @return * The item view URL corresponding to the given arguments. * An empty string is returned if no item view URL has been defined, * or if the item cannot be viewed for any reason. */ function versioncontrol_get_url_item_view($repository, $item) { $urls = _versioncontrol_get_repository_urls($repository); $current_branch = versioncontrol_get_current_item_branch($repository, $item); if ($item['type'] == VERSIONCONTROL_ITEM_FILE) { return strtr($urls['file_view'], array( '%path' => $item['path'], '%revision' => $item['revision'], '%branch' => isset($current_branch) ? '' : $current_branch, )); } else if ($item['type'] == VERSIONCONTROL_ITEM_DIRECTORY) { return strtr($urls['directory_view'], array( '%path' => $item['path'], '%revision' => $item['revision'], '%branch' => isset($current_branch) ? '' : $current_branch, )); } } /** * Retrieve the URL of the repository viewer that displays the diff between * two given files in the corresponding repository. * * @param $repository * The repository that the file items are located in. * @param $file_item_new * The new version of the file that should be diffed. * @param $file_item_old * The old version of the file that should be diffed. * * @return * The diff URL corresponding to the given arguments. * An empty string is returned if no diff URL has been defined, * or if the two items cannot be diffed for any reason. */ function versioncontrol_get_url_diff($repository, $file_item_new, $file_item_old) { $urls = _versioncontrol_get_repository_urls($repository); $current_branch = versioncontrol_get_current_item_branch($repository, $file_item_new); return strtr($urls['diff'], array( '%path' => $file_item_new['path'], '%new-revision' => $file_item_new['revision'], '%old-path' => $file_item_old['path'], '%old-revision' => $file_item_old['revision'], '%branch' => isset($current_branch) ? '' : $current_branch, )); } /** * Retrieve the URL of the issue tracker that displays the issue/case/bug page * of an issue id which presumably has been mentioned in a commit message. * As issue tracker urls are specific to each repository, this also needs * to be given as argument. * * @param $repository * The repository that is covered by the issue tracker. * @param $issue_id * A number that uniquely identifies the mentioned issue/case/bug. * * @return * The issue tracker URL corresponding to the given arguments. * An empty string is returned if no issue tracker URL has been defined. */ function versioncontrol_get_url_tracker($repository, $issue_id) { $urls = _versioncontrol_get_repository_urls($repository); return strtr($urls['tracker'], array('%d' => $issue_id)); } /** * Retrieve the repository viewer URLs from the database (or from the cache). * * @param $repository * The repository for which the URLs should be retrieved. * * @return * A structured array with the repository viewer URLs as values (each of them * can be an empty string), corresponding to the following keys: * * 'commit_view': The overall summary view of a commit/revision. * 'file_view': The 'view' URL of a file in the repository. * 'directory_view': The 'view' URL of a directory in the repository. * 'diff': The difference between two versions of a file * (or of two different files, if the repository viewer supports it). * 'tracker': The issue/bug/case URL of the associated issue/case/bug tracker. */ function _versioncontrol_get_repository_urls($repository) { static $urls_by_repository = array(); if (!isset($urls_by_repository[$repository['repo_id']])) { $result = db_query("SELECT * FROM {versioncontrol_repository_urls} WHERE repo_id = '%d'", $repository['repo_id']); while ($urls = db_fetch_array($result)) { unset($urls['repo_id']); $urls_by_repository[$repository['repo_id']] = $urls; } } if (!isset($urls_by_repository[$repository['repo_id']])) { return array( 'commit_view' => '', 'file_view' => '', 'directory_view' => '', 'diff' => '', 'tracker' => '', ); } return $urls_by_repository[$repository['repo_id']]; } /** * Retrieve detailed information about what happened in a single commit. * * @param $commit * The commit whose actions should be retrieved. * * @return * A structured array containing the exact details of what happened to * each item in this commit. Array keys are the current/new paths, also for * VERSIONCONTROL_ACTION_DELETED actions even if the file actually doesn't * exist anymore. The corresponding array values are again structured arrays * and consist of elements with the following keys: * * 'action': Specifies how the item was modified. * One of the predefined VERSIONCONTROL_ACTION_* values. * 'modified': Boolean value, specifies if a file was modified in addition * to the other action in the 'action' element of the array. * Only exists for the VERSIONCONTROL_ACTION_MOVED * and VERSIONCONTROL_ACTION_COPIED actions. * 'current item': The updated state of the modified item. Exists for all * actions except VERSIONCONTROL_ACTION_DELETED. * 'source items': An array with the previous state(s) of the modified item. * Path and branch will always be the same as in the current * item except for the VERSIONCONTROL_ACTION_MOVED, * VERSIONCONTROL_ACTION_COPIED and * VERSIONCONTROL_ACTION_MERGED actions. * Exists for all actions except VERSIONCONTROL_ACTION_ADDED. * * Item values are structured arrays and consist of elements * with the following keys: * * 'type': Specifies the item type, which is either * VERSIONCONTROL_ITEM_FILE or VERSIONCONTROL_ITEM_DIRECTORY. * 'path': The path of the item at the specific revision. * 'revision': The (file-level) revision when the item was changed. * If there is no such revision (which may be the case for * directory items) then the 'revision' element is * an empty string. * '[xxx]_specific': May be set by the backend to remember additional * item info. ("[xxx]" is the unique string identifier * of the respective version control system.) * * A real-life example of such a result array can be found * in the FakeVCS example module. */ function versioncontrol_get_commit_actions($commit) { return _versioncontrol_call_backend( $commit['repository']['vcs'], 'get_commit_actions', array($commit) ); } /** * Return the 'current item' of a commit action if there is one, * or the first (only) one of the 'source items' if the file has been deleted. * As this function is also being used within access checks, it also works * when the corresponding commit has not yet been inserted into the database. */ function versioncontrol_get_affected_item($commit_action) { return isset($commit_action['current item']) ? $commit_action['current item'] // new revision of the file, or : $commit_action['source items'][0]; // the deleted item } /** * Retrieve the item of the deepest-level directory in the repository that is * common to all the changed items in a commit. In other words, this function * gets you the item for $commit['directory']. * * @param $commit * The commit whose deepest-level changed directory should be retrieved. * * @return * The requested directory item. Item values are structured arrays and * consist of elements with the following keys: * * 'type': Specifies the item type, which in this case can only be * VERSIONCONTROL_ITEM_DIRECTORY. * 'path': The path of the directory at the time of the commit. * 'revision': The (file-level) revision when the item was last changed. * If there is no such revision (which may be the case for * directory items) then the 'revision' element is * an empty string. * '[xxx]_specific': May be set by the backend to remember additional * item info. ("[xxx]" is the unique string identifier * of the respective version control system.) */ function versioncontrol_get_commit_directory_item($commit) { return _versioncontrol_call_backend( $commit['repository']['vcs'], 'get_commit_directory_item', array($commit) ); } /** * Retrieve the branches that have been affected by the given commit. * * @return * An array of strings that identify a branch in the respective repository, * like for example array('DRUPAL-5') or array('trunk', '5.x'). * If no branch was affected by the given commit, an empty array is returned. */ function versioncontrol_get_commit_branches($commit) { return _versioncontrol_call_backend( $commit['repository']['vcs'], 'get_commit_branches', array($commit) ); } /** * Retrieve the set of items that were affected by a branch operation. * * @param $branch * The branch operation whose items should be retrieved. This is an array * like the one returned by versioncontrol_get_branch_operation(). * * @return * An array of all items that were affected by the branching operation. * An empty result array means that the whole repository has been branched. * Item values are structured arrays and consist of elements * with the following keys: * * 'type': Specifies the item type, which is either * VERSIONCONTROL_ITEM_FILE or VERSIONCONTROL_ITEM_DIRECTORY. * 'path': The path of the item at the specific revision. * 'revision': The (file-level) revision when the item was changed. * If there is no such revision (which may be the case for * directory items) then the 'revision' element is * an empty string. * 'source branch': Optional, may be set by the backend if the source branch * (the one that this one branched off) can be retrieved. * If given, this is a string with the original branch name. * '[xxx]_specific': May be set by the backend to remember additional * item info. ("[xxx]" is the unique string identifier * of the respective version control system.) * * A real-life example of such a result array can be found * in the FakeVCS example module. */ function versioncontrol_get_branched_items($branch) { return _versioncontrol_call_backend( $branch['repository']['vcs'], 'get_branched_items', array($branch) ); } /** * Retrieve the set of items that were affected by a tag operation. * * @param $tag * The tag operation whose items should be retrieved. This is an array * like the one returned by versioncontrol_get_tag_operation(). * * @return * An array of all items that were affected by the tagging operation. * An empty result array means that the whole repository has been tagged. * Item values are structured arrays and consist of elements * with the following keys: * * 'type': Specifies the item type, which is either * VERSIONCONTROL_ITEM_FILE or VERSIONCONTROL_ITEM_DIRECTORY. * 'path': The path of the item at the specific revision. * 'revision': The (file-level) revision when the item was changed. * If there is no such revision (which may be the case for * directory items) then the 'revision' element is * an empty string. * 'source tag': Optional, may be set by the backend if the source tag * (the one that this one tagged off) can be retrieved. * If given, this is a string with the original tag name. * '[xxx]_specific': May be set by the backend to remember additional * item info. ("[xxx]" is the unique string identifier * of the respective version control system.) * * A real-life example of such a result array can be found * in the FakeVCS example module. */ function versioncontrol_get_tagged_items($tag) { return _versioncontrol_call_backend( $tag['repository']['vcs'], 'get_tagged_items', array($tag) ); } /** * Retrieve the current branch that this item is in. If this item was part of * the result of versioncontrol_get_commit_actions(), this will probably be * the branch that this item was committed to. If no specific branch is known * or applicable for this item, NULL is returned. The main branch ('HEAD', * 'trunk', 'master' or however it is called in the respective VCS) is also * a valid branch and should be expected as return value. * * @param $repository * The repository that the item is located in. * @param $item * The item whose current branch should be retrieved. * * @return * A string containing the current item branch, or NULL if no branch * is known or applicable. */ function versioncontrol_get_current_item_branch($repository, $item) { return _versioncontrol_call_backend( $repository['vcs'], 'get_current_item_branch', array($repository, $item) ); } /** * Retrieve the current tag operation that has been applied to this item. * If no specific tag operation is known or applicable for this item, * NULL is returned. * * @param $repository * The repository that the item is located in. * @param $item * The item whose current branch should be retrieved. * * @return * A tag operation array like the return value * of versioncontrol_get_tag_operation(), or NULL if no tag * is known or applicable. */ function versioncontrol_get_current_item_tag($repository, $item) { return _versioncontrol_call_backend( $repository['vcs'], 'get_current_item_tag', array($repository, $item) ); } /** * Retrieve the parent (directory) item of a given item. * * @param $repository * The repository that the item is located in. * @param $item * The item whose parent should be retrieved. * @param $parent_path * NULL if the direct parent of the given item should be retrieved, * or a parent path that is further up the directory tree. * * @return * The parent directory item at the same revision as the given item. * If $parent_path is not set and the item is already the topmost one * in the repository, the item is returned as is. It also stays the same * if $parent_path is given and the same as the path of the given item. * If the given directory path does not correspond to a parent item, * NULL is returned. */ function versioncontrol_get_parent_item($repository, $item, $parent_path = NULL) { return _versioncontrol_call_backend( $repository['vcs'], 'get_parent_item', array($repository, $item, $parent_path) ); } /** * Retrieve all branches that exist for the given item, and how this item * is named there. The main branch ('HEAD', 'trunk', 'master' or however it is * called in the respective VCS) is also included in the result. * * This function is optional for VCS backends to implement, be sure to check * with versioncontrol_backend_implements($repository['vcs'], 'get_all_item_branches') * if the particular backend actually implements it. * * @param $repository * The repository that the item is located in. * @param $item * The item whose set of branches should be retrieved. * * @return * A structured array that looks like * array( * $branch1_name => $branch1_item, * $branch2_name => $branch2_item, * ... * ). * * The corresponding item values are again structured arrays * and consist of elements with the following keys: * * 'type': Specifies the item type, which is either * VERSIONCONTROL_ITEM_FILE or VERSIONCONTROL_ITEM_DIRECTORY. * 'path': The path of the item at the specific revision. * 'revision': The (file-level) revision when the item was last changed. * If there is no such revision (which may be the case for * directory items) then the 'revision' element is * an empty string. * '[xxx]_specific': May be set by the backend to remember additional * item info. ("[xxx]" is the unique string identifier * of the respective version control system.) * * An empty array is returned if there are no branches for this item. * NULL is returned if the given item is not inside the repository at the * time of 'revision'. * * A real-life example of such a result array can be found * in the FakeVCS example module. */ function versioncontrol_get_all_item_branches($repository, $item) { return _versioncontrol_call_backend( $repository['vcs'], 'get_all_item_branches', array($repository, $item) ); } /** * Retrieve all tags that exist for the given item $path, and how this item * is named there. * * This function is optional for VCS backends to implement, be sure to check * with versioncontrol_backend_implements($repository['vcs'], 'get_all_item_tags') * if the particular backend actually implements it. * * @param $repository * The repository that the item is located in. * @param $item * The item whose history should be retrieved. * * @return * A structured array that looks like * array( * $tag1_name => $tag1_item, * $tag2_name => $tag2_item, * ... * ). * * The corresponding item values are again structured arrays * and consist of elements with the following keys: * * 'type': Specifies the item type, which is either * VERSIONCONTROL_ITEM_FILE or VERSIONCONTROL_ITEM_DIRECTORY. * 'path': The path of the item at the specific revision. * 'revision': The (file-level) revision when the item was last changed. * If there is no such revision (which may be the case for * directory items) then the 'revision' element is * an empty string. * '[xxx]_specific': May be set by the backend to remember additional * item info. ("[xxx]" is the unique string identifier * of the respective version control system.) * * An empty array is returned if there are no tags for this item. * NULL is returned if the given item is not inside the repository at the * time of 'revision'. * * A real-life example of such a result array can be found * in the FakeVCS example module. */ function versioncontrol_get_all_item_tags($repository, $item) { return _versioncontrol_call_backend( $repository['vcs'], 'get_all_item_tags', array($repository, $item) ); } /** * Retrieve the revisions where the given item has been changed, * in reverse chronological order. * * Only direct descendants or predecessors of this item will be retrieved. * That means that history of the same item in a different (parallel) branch * will not be returned, except if the given state of the item was moved/copied * from there or goes back there in a more recent revision. In short, you'll * not get "1.7" and "1.2.2.4" items side by side if it doesn't make sense. * * @param $repository * The repository that the item is located in. * @param $item * The item whose history should be retrieved. * * @return * An array containing the exact details of all item changes. Each element * of the array is a structured array consisting of elements with the * following keys: * * 'action': Specifies how the item was modified. * One of the predefined VERSIONCONTROL_ACTION_* values. * 'modified': Boolean value, specifies if a file was modified in addition * to the other action in the 'action' element of the array. * Only exists for the VERSIONCONTROL_ACTION_MOVED * and VERSIONCONTROL_ACTION_COPIED actions. * 'current item': The updated state of the modified item. Exists for all * actions except VERSIONCONTROL_ACTION_DELETED. * 'source items': An array with the previous state(s) of the modified item. * Path and branch will always be the same as in the current * item except for the VERSIONCONTROL_ACTION_MOVED, * VERSIONCONTROL_ACTION_COPIED and * VERSIONCONTROL_ACTION_MERGED actions. * Exists for all actions except VERSIONCONTROL_ACTION_ADDED. * * Item values are structured arrays and consist of elements * with the following keys: * * 'type': Specifies the item type, which is either * VERSIONCONTROL_ITEM_FILE or VERSIONCONTROL_ITEM_DIRECTORY. * 'path': The path of the item at the specific revision. * 'revision': The (file-level) revision when the item was changed. * If there is no such revision (which may be the case for * directory items) then the 'revision' element is * an empty string. * '[xxx]_specific': May be set by the backend to remember additional * item info. ("[xxx]" is the unique string identifier * of the respective version control system.) * * NULL is returned if the given item is not under version control, * or was not under version control at the time of the given revision. * * A real-life example of such a result array can be found * in the FakeVCS example module. */ function versioncontrol_get_item_history($repository, $item) { return _versioncontrol_call_backend( $repository['vcs'], 'get_item_history', array($repository, $item) ); } /** * Retrieve the set of files and directories that exist at a specified revision * in the given directory inside the repository. * * This function is optional for VCS backends to implement, be sure to check * with versioncontrol_backend_implements($repository['vcs'], 'get_directory_contents') * if the particular backend actually implements it. * * @param $repository * The repository that the directory item is located in. * @param $directory_item * The parent item of the the items that should be listed. * @param $recursive * If FALSE, only the direct children of $path will be retrieved. * If TRUE, you'll get every single descendant of $path. * * @return * A structured item array containing the exact details of which items have * been inside the directory at the time of the commit, including the * directory itself. Array keys are the current/new paths. * The corresponding item values are again structured arrays * and consist of elements with the following keys: * * 'type': Specifies the item type, which is either * VERSIONCONTROL_ITEM_FILE or VERSIONCONTROL_ITEM_DIRECTORY. * 'path': The path of the item at the specific revision. * 'revision': The (file-level) revision when the item was last changed. * If there is no such revision (which may be the case for * directory items) then the 'revision' element is * an empty string. * '[xxx]_specific': May be set by the backend to remember additional * item info. ("[xxx]" is the unique string identifier * of the respective version control system.) * * NULL is returned if the given item is not inside the repository, * or if it is not a directory item at all. * * A real-life example of such a result array can be found * in the FakeVCS example module. */ function versioncontrol_get_directory_contents($repository, $directory_item, $recursive = FALSE) { if ($directory_item['type'] != VERSIONCONTROL_ITEM_DIRECTORY) { return NULL; } return _versioncontrol_call_backend( $repository['vcs'], 'get_directory_contents', array($repository, $directory_item, $recursive) ); } /** * Retrieve the text or binary data of the given file in the specified commit. * * This function is optional for VCS backends to implement, be sure to check * with versioncontrol_backend_implements($repository['vcs'], 'get_file_contents') * if the particular backend actually implements it. * * @param $repository * The repository that the file item is located in. * @param $file_item * The file item whose contents should be retrieved. * * @return * A structured array that consists of elements with the following keys: * * 'file type': Specifies the file type, which is either * VERSIONCONTROL_FILE_TEXT or VERSIONCONTROL_FILE_BINARY. * 'contents': The raw contents of the file. * * NULL is returned if the given item is not under version control, * or was not under version control at the time of the given revision, * or if it is not a file item at all. * * A real-life example of such a result array can be found * in the FakeVCS example module. */ function versioncontrol_get_file_contents($repository, $file_item) { if (!($file_item['type'] == VERSIONCONTROL_ITEM_FILE || $file_item['type'] == VERSIONCONTROL_ITEM_FILE_DELETED)) { return NULL; } return _versioncontrol_call_backend( $repository['vcs'], 'get_file_contents', array($repository, $file_item) ); } /** * Implementation of hook_block(): * Present a list of the most active developers. */ function versioncontrol_block($op = 'list', $delta = 0) { if ($op == 'list') { $blocks[0]['info'] = t('Most active developers'); return $blocks; } else if ($op == 'view') { if ($delta == 0) { $interval = 7 * 24 * 60 * 60; $length = 15; $result = db_query_range("SELECT COUNT(c.commit_id) AS count, c.uid, u.name FROM {versioncontrol_commits} c INNER JOIN {users} u ON c.uid = u.uid AND c.uid != 0 AND c.date > %d GROUP BY c.uid ORDER BY count DESC", time() - $interval, 0, $length); $usernames = array(); while ($user = db_fetch_object($result)) { $usernames[] = theme('username', $user); } if (!empty($usernames)) { $block = array( 'subject' => t('Most active developers'), 'content' => theme('item_list', $usernames), ); return $block; } } } } /** * Retrieve an array where each element represents a single line of the * given file in the specified commit, annotated with the committer who last * modified that line. Note that annotations are generally a quite slow * operation, so expect this function to take a bit more time as well. * * This function is optional for VCS backends to implement, be sure to check * with versioncontrol_backend_implements($repository['vcs'], 'get_file_annotation') * if the particular backend actually implements it. * * @param $repository * The repository that the file item is located in. * @param $file_item * The file item whose annotation should be retrieved. * * @return * A structured array that consists of one element per line, with * line numbers as keys (starting from 1) and a structured array as values, * where each of them consists of elements with the following keys: * * 'username': The system specific VCS username of the last committer. * 'line': The contents of the line, without linebreak characters. * * NULL is returned if the given item is not under version control, * or was not under version control at the time of the given revision, * or if it is not a file item at all, or if it is marked as binary file, or * if the specific backend module does not implement retrieving annotations. * * A real-life example of such a result array can be found * in the FakeVCS example module. */ function versioncontrol_get_file_annotation($repository, $file_item) { if (!($file_item['type'] == VERSIONCONTROL_ITEM_FILE || $file_item['type'] == VERSIONCONTROL_ITEM_FILE_DELETED)) { return NULL; } return _versioncontrol_call_backend( $repository['vcs'], 'get_file_annotation', array($repository, $file_item) ); } /** * Returns a string of suggestions for Drupal usernames with accounts for * the given repository, formatted to be suitable for use with * JS autocomplete fields. */ function versioncontrol_user_autocomplete($repo_id, $string) { if (!is_numeric($repo_id)) { return drupal_to_js(array()); } $matches = array(); $result = db_query_range("SELECT u.uid, u.name FROM {users} u INNER JOIN {versioncontrol_accounts} a ON u.uid = a.uid WHERE repo_id = '%d' AND LOWER(u.name) LIKE LOWER('%s%%')", $repo_id, $string, 0, 10); while ($user = db_fetch_object($result)) { if (versioncontrol_is_account_authorized($user->uid, $repo_id)) { $matches[$user->name] = check_plain($user->name); } } print drupal_to_js($matches); exit(); } /** * Generate and execute a SELECT query for the given table base on the name * and given values of this table's primary key. This function basically * accomplishes the retrieval part of Version Control API's 'autoadd' feature. * In order to avoid unnecessary complexity, the primary key may not consist * of multiple columns and has to be a numeric value. */ function _versioncontrol_db_get_additions($table_name, $primary_key_name, $keys) { $or_constraints = array(); foreach ($keys as $key) { $or_constraints[] = $primary_key_name ." = '%d'"; } $result = db_query('SELECT * FROM {'. $table_name .'} WHERE '. implode(' OR ', $or_constraints), $keys); $additions = array(); while ($addition = db_fetch_array($result)) { $primary_key = $addition[$primary_key_name]; unset($addition[$primary_key_name]); foreach ($addition as $key => $value) { if (!is_numeric($addition[$key])) { $addition[$key] = unserialize($addition[$key]); } } $additions[$primary_key] = $addition; } return $additions; }