array( // The user-visible name of the VCS. 'name' => 'CVS', // A short description of the VCS, if possible not longer than one or two sentences. 'description' => t('CVS (Concurrent Versions System) is a slightly older code management system that supports file revisions, tags and branches, but lacks atomic commits, advanced merge functionality and support for renaming items.'), // A list of optional capabilities, in addition to the required retrieval // of detailed commit information. 'capabilities' => array( // Able to cancel commits if the committer lacks permissions // to commit to specific paths and/or branches. VERSIONCONTROL_CAPABILITY_COMMIT_RESTRICTIONS, // Able to cancel branch or tag assignments if the committer lacks // permissions to create/update/delete those. VERSIONCONTROL_CAPABILITY_BRANCH_TAG_RESTRICTIONS, ), // An array listing which tables should be managed by Version Control API // instead of doing it manually in the backend. 'flags' => array( // versioncontrol_insert_repository() will automatically insert // array elements from $repository['cvs_specific'] into // {versioncontrol_cvs_repositories} and versioncontrol_get_repositories() // will automatically fetch it from there. VERSIONCONTROL_FLAG_AUTOADD_REPOSITORIES, ), ), ); } /** * Implementation of hook_menu(). */ function versioncontrol_cvs_menu($may_cache) { global $user; $items = array(); $admin_access = user_access('administer version control systems'); if ($may_cache) { $items[] = array( 'path' => 'admin/project/versioncontrol-repositories/update/cvs', 'title' => t('Fetch log'), 'callback' => 'versioncontrol_cvs_update_repository_callback', 'access' => $admin_access, 'type' => MENU_CALLBACK, ); } return $items; } /** * Implementation of hook_cron(): * Update repositories that have log fetching enabled. */ function versioncontrol_cvs_cron() { $result = db_query("SELECT repo_id FROM {versioncontrol_cvs_repositories} WHERE update_method = %d", VERSIONCONTROL_CVS_UPDATE_CRON); // Set timeout limit to 3600 seconds as it can take a long time to process // the log initially. (And hook_cron() might be called by poormanscron.) if (!ini_get('safe_mode')) { set_time_limit(3600); } while ($repo = db_fetch_object($result)) { $repository = versioncontrol_get_repository($repo->repo_id); if (isset($repository)) { _versioncontrol_cvs_update_repository($repository); } } } /** * Include the cvslib.inc helper library so that its functions can be called. */ function _versioncontrol_cvs_init_cvslib() { include_once(drupal_get_path('module', 'versioncontrol_cvs') .'/cvslib/cvslib.inc'); } /** * Implementation of [versioncontrol_backend]_get_item(): * Try to retrieve a given item in a repository. */ function versioncontrol_cvs_get_item($repository, $path, $revision = '') { _versioncontrol_cvs_init_cvslib(); $label = array( 'name' => 'HEAD', 'type' => VERSIONCONTROL_OPERATION_BRANCH, ); $directory_item = array( 'path' => $path, 'revision' => '', 'type' => VERSIONCONTROL_ITEM_DIRECTORY, ); // Special casing the root directory, because it's always the same. if ($path == '/') { return array('item' => $directory_item, 'selected_label' => $label); } // Use ls to see whether the item is a directory. $ls_constraints = array( 'show_dead_files' => TRUE, 'show_empty_dirs' => TRUE, ); if (!empty($revision)) { $ls_constraints['revision'] = $revision; } $contents = cvslib_ls($repository['root'], dirname($path), $ls_constraints); if (!isset($contents[$path])) { return NULL; // does not exist (at least not at the given revision) } else if ($contents[$path]->directory) { return array('item' => $directory_item, 'selected_label' => $label); } else { // The item exists and is a file. We want more info (dead or not, label) // so let's retrieve further info with cvs log. $file_revisions = cvslib_log($repository['root'], $path, array( 'revision' => empty($revision) ? 'HEAD.' : $revision, 'recursive' => FALSE, )); if ($file_revisions === FALSE || empty($file_revisions)) { return NULL; } $file_revision = reset($file_revisions); // first array element if ($file_revision->path != $path) { // Should not happen, but let's avoid subtle bugs and race conditions. return NULL; } $item = array( 'path' => $file_revision->path, 'revision' => $file_revision->revision, 'type' => $file_revision->dead ? VERSIONCONTROL_ITEM_FILE_DELETED : VERSIONCONTROL_ITEM_FILE, ); if (empty($file_revision->branch)) { $label = NULL; // should not happen except in unknown edge cases } else { $label['name'] = $file_revision->branch; } return array('item' => $item, 'selected_label' => $label); } } /** * Implementation of [versioncontrol_backend]_get_directory_contents(): * Retrieve the set of files and directories that exist at a specified revision * in the given directory inside the repository. */ function versioncontrol_cvs_get_directory_contents($repository, $directory_item, $recursive = FALSE) { _versioncontrol_cvs_init_cvslib($repository); $selected_label = versioncontrol_get_item_selected_label($repository, $directory_item); $labelname = empty($selected_label) ? 'HEAD' : $selected_label['name']; $contents = cvslib_ls($repository['root'], $directory_item['path'], array( 'revision' => $labelname, 'recursive' => $recursive, )); if ($contents === FALSE) { return NULL; } $items = array(); foreach ($contents as $path => $item_info) { $items[$path] = array( 'item' => array( 'path' => $item_info->path, 'revision' => ($item_info->directory) ? '' : $item_info->revision, 'type' => ($item_info->directory) ? VERSIONCONTROL_ITEM_DIRECTORY : VERSIONCONTROL_ITEM_FILE, ), 'selected_label' => array( 'type' => empty($selected_label) ? VERSIONCONTROL_OPERATION_BRANCH : $selected_label['type'], 'name' => $labelname, ), ); } return $items; } /** * Implementation of [versioncontrol_backend]_get_file_copy(): * Retrieve a copy of the contents of a given item in the repository. * (You won't get the original because repositories can often be remote.) */ function versioncontrol_cvs_get_file_copy($repository, $file_item, $destination) { _versioncontrol_cvs_init_cvslib($repository); $success = cvslib_cat($destination, $repository['root'], $file_item['path'], array('revision' => $file_item['revision'])); return $success; } /** * Implementation of [versioncontrol_backend]_get_selected_label_from_operation(): * Retrieve the tag or branch that applied to that item during the given * operation. The result of this function will be used for the selected label * property of each item, which is necessary to provide a starting point for * branch and tag navigation. */ function versioncontrol_cvs_get_selected_label_from_operation($operation, $target_item) { // Tag renames are represented by two labels, "added" and "deleted".) // In that case, make sure that the "added" label will be selected. if (count($operation['labels'] > 1)) { foreach ($operation['labels'] as $label) { if ($label['action'] == VERSIONCONTROL_ACTION_ADDED) { return $label; } } } // Otherwise, each operation by the CVS backend has exactly one branch or tag // assigned, so we can just return that one for all items in any operation. // NOTE: That might change though, in case the CVS backend gets functionality // to add branches to commits later - because when another branch is // forked off, all previous commits to a file are now both in the // original branch *and* in the new branch. Very low priority, though. return $operation['labels'][0]; } /** * Implementation of [versioncontrol_backend]_get_selected_label_from_other_item(): * Retrieve a valid label (tag or branch) for a new @p $target_item that is * (hopefully) similar or related to that of the given @p $other_item which * already has a selected label assigned. If the backend cannot find a related * label, return any valid label. The result of this function will be used for * the selected label property of each item, which is necessary to preserve the * item state throughout navigational API functions. */ function versioncontrol_cvs_get_selected_label_from_other_item($repository, $target_item, &$other_item, $other_item_tags = array()) { // First up, optimizations - maybe we can do without the generic // "label transfer" code from further down and use assumptions instead. // A directory can always have the same tag name as another item, in the // worst case a directory content listing just won't list any files. if (versioncontrol_is_directory_item($target_item)) { return versioncontrol_get_item_selected_label($repository, $other_item); } // Each source item is also available on the same branch as its successor. // That means we can take the label as is - if it's actually a branch. if ($other_item == 'successor_item') { $label = versioncontrol_get_item_selected_label($repository, $other_item); if (isset($label['type']) && $label['type'] == VERSIONCONTROL_OPERATION_BRANCH) { return $label; } } // Otherwise we can't really figure out a label for this item - // it's a tag label array or NULL - so we either have to query CVS itself // (most correct solution except when invoking CVS fails for any reason), // or look up in the database if we know the commit associated to this item // revision (correct solution if the commit has been recorded and the // database has captured all branch/tag renames correctly). In case both // of those don't work, we can only return NULL - which is not really a nice // thing to do, as normally the CVS backend always provides a branch. // But that's how it goes, I guess. // Try 1 (more performant than process invocation): // Look if the database contains the associated commit. if (versioncontrol_fetch_item_revision_id($repository, $target_item)) { $constraints = array( 'item_revision_id' => array($target_item['item_revision_id']), ); $commit_operations = versioncontrol_get_commit_operations($constraints); if (!empty($commit_operations)) { // yo, found the associated commit! $commit_operation = reset($commit_operations); // first (only) element return $commit_operation['labels'][0]; } } // Hm, didn't work, so... // Try 2: Get the information directly from the CVS process. // TODO: implement? or just leave it at that as "good enough"? // No label could be retrieved by looking at the other item, sorry. return NULL; } /** * CVS tells us that the file was modified (has a previous revision) even if * it has been deleted before. Technically that's correct, but we'd like to * have it show up as "added", so this function tries to be smart and alters * an operation items array accordingly if the database tells us that the * previous revision of a file was dead. */ function _versioncontrol_cvs_fix_commit_operation_items($operation, &$operation_items) { foreach ($operation_items as $path => $item) { if ($item['action'] != VERSIONCONTROL_ACTION_MODIFIED) { continue; } $repo_id = isset($operation['repository']) ? $operation['repository']['repo_id'] : $operation['repo_id']; $count = db_result(db_query( "SELECT COUNT(*) FROM {versioncontrol_item_revisions} ir INNER JOIN {versioncontrol_operation_items} opitem ON ir.item_revision_id = opitem.item_revision_id INNER JOIN {versioncontrol_operations} op ON opitem.vc_op_id = op.vc_op_id WHERE op.repo_id = %d AND op.type = %d AND ir.type = %d AND ir.path = '%s' AND ir.revision = '%s'", $repo_id, VERSIONCONTROL_OPERATION_COMMIT, VERSIONCONTROL_ITEM_FILE_DELETED, $path, $action['source_items'][0]['revision'] )); if ($count > 0) { $operation_items[$path]['action'] = VERSIONCONTROL_ACTION_ADDED; $operation_items[$path]['source_items'] = array(); } } } /** * Implementation of [versioncontrol_backend]_account(): * Manage (insert, update or delete) additional CVS user account data * in the database. * * @param $op * Either 'insert' when the account is in the process of being created, * or 'update' when username or additional module data change, * or 'delete' if it will be deleted after this function has been called. * @param $uid * The Drupal user id corresponding to the VCS account. * @param $username * The VCS specific username (a string). * @param $repository * The repository where the user has its VCS account. * @param $additional_data * An array of additional author information. */ function versioncontrol_cvs_account($op, $uid, $username, $repository, $additional_data = array()) { $cvs_specific = $additional_data['cvs_specific']; switch ($op) { case 'insert': if (!isset($cvs_specific) || !isset($cvs_specific['password'])) { drupal_set_message(t('Error: no CVS password given on account creation!'), 'error'); return; } db_query("INSERT INTO {versioncontrol_cvs_accounts} (uid, repo_id, password) VALUES (%d, %d, '%s')", $uid, $repository['repo_id'], $cvs_specific['password']); break; case 'update': if (!isset($cvs_specific) || !isset($cvs_specific['password'])) { return; // the user didn't update the password in the process. } db_query("UPDATE {versioncontrol_cvs_accounts} SET password = '%s' WHERE uid = %d AND repo_id = %d", $cvs_specific['password'], $uid, $repository['repo_id']); if (!user_access('administer version control systems')) { // Admins get "The account has been updated successfully" anyways. drupal_set_message(t('The CVS password has been updated successfully.')); } break; case 'delete': db_query('DELETE FROM {versioncontrol_cvs_accounts} WHERE uid = %d AND repo_id = %d', $uid, $repository['repo_id']); break; } } /** * Implementation of [vcs_backend]_import_accounts(): * Import accounts into a repository, given text data from the accounts file. * No accounts are deleted, new accounts are inserted, and existing accounts * are updated with imported ones. * * @param $repository * The repository where the accounts will be imported. * @param $data * The contents of the "account data" text area where the user has to * enter/copy the contents of the version control system's accounts file. */ function versioncontrol_cvs_import_accounts($repository, $data) { $lines = explode("\n", $data); $names = array(); foreach ($lines as $line) { if (preg_match('/^\s*(#.*)?$/', $line)) { // filter out empty and commented lines continue; } // Extract the account information and create or update the user accounts. list($username, $password, $run_as_user) = explode(':', $line); if (!empty($username) && !empty($password)) { $additional_data = array( 'cvs_specific' => array('password' => $password), ); $uid = versioncontrol_get_account_uid_for_username($repository['repo_id'], $username, TRUE); if (isset($uid)) { versioncontrol_update_account($repository, $uid, $username, $additional_data); $names[] = t('updated !username', array('!username' => $username)); } else { $uid = db_result(db_query("SELECT uid FROM {users} WHERE name = '%s'", $username)); if ($uid) { versioncontrol_insert_account($repository, $uid, $username, $additional_data); $names[] = t('added !username', array('!username' => $username)); } else { $names[] = t('didn\'t add !username (no matching Drupal username exists)', array('!username' => $username)); } } } } if (empty($names)) { drupal_set_message(t('Failed to import CVS accounts.'), 'error'); } else { drupal_set_message(theme('item_list', $names, t('The import of CVS accounts has been completed successfully:'))); } } /** * Implementation of [vcs_backend]_export_accounts(): * Export accounts of a repository to text data that is suitable for * copying to the version control system's accounts file. * * @param $repository * The repository whose accounts will be exported. * @param $accounts * The list (array) of accounts that should be exported, given in the same * format as the return value of versioncontrol_get_accounts(). * All accounts in this list are from the above repository. * * @return * The exported textual representation of the account list. */ function versioncontrol_cvs_export_accounts($repository, $accounts) { if (empty($accounts)) { return '# '. t('no user accounts available to export'); } $accounts_flat = array(); $uid_constraints = array(); $params = array($repository['repo_id']); foreach ($accounts as $uid => $usernames_per_repository) { foreach ($usernames_per_repository as $repo_id => $username) { $accounts_flat[$uid] = array('uid' => $uid, 'username' => $username); $uid_constraints[] = 'uid = %d'; $params[] = $uid; } } $result = db_query('SELECT uid, password FROM {versioncontrol_cvs_accounts} WHERE repo_id = %d AND ('. implode(' OR ', $uid_constraints) .')', $params); while ($account = db_fetch_object($result)) { $accounts_flat[$account->uid]['password'] = $account->password; } $run_as_user = ''; if (!empty($repository['run_as_user'])) { $run_as_user = ':'. $repository['run_as_user']; } $data = ''; foreach ($accounts_flat as $uid => $account) { $data .= '# '. url('user/'. $uid, NULL, NULL, TRUE) ."\n"; $data .= $account['username'] .':'. $account['password'] . $run_as_user ."\n\n"; } return $data; } /** * Menu callback for 'admin/project/versioncontrol-repositories/update/cvs' * (expecting a $repo_id as one more path argument): * Retrieve/validate the specified repository, fetch new commits, tags * and branches by invoking the cvs executable, output messages and * redirect back to the repository page. */ function versioncontrol_cvs_update_repository_callback($repo_id) { if (is_numeric($repo_id)) { $repository = versioncontrol_get_repository($repo_id); if (isset($repository)) { $update_method = $repository['cvs_specific']['update_method']; } } if (isset($update_method) && $update_method == VERSIONCONTROL_CVS_UPDATE_CRON) { // Set timeout limit to 3600 seconds as it can take a long time // to process the log initially. if (!ini_get('safe_mode')) { set_time_limit(3600); } if (_versioncontrol_cvs_update_repository($repository)) { drupal_set_message(t('Fetched new log entries.')); } } else { // $repo_id is not a number or doesn't correlate to any repository. drupal_set_message(t('No such repository, did not fetch anything.')); } drupal_goto('admin/project/versioncontrol-repositories'); } /** * Actually update the repository by fetching commits and other stuff * directly from the repository, invoking the cvs executable. * * @return * TRUE if the logs were updated, or FALSE if fetching and updating the logs * failed for whatever reason. */ function _versioncontrol_cvs_update_repository(&$repository) { include_once(drupal_get_path('module', 'versioncontrol_cvs') .'/versioncontrol_cvs.log.inc'); return _versioncontrol_cvs_log_update_repository($repository); } /** * Calculate the previous revision number of file under version control, * given the current revision. This can be done in a purely programmatical way * because of the quite special numbering scheme of CVS (so no database queries * need to be done in order to get the result). * * @return * The previous revision number (e.g. "1.1" for a given "1.2", * or "1.3" for a given "1.3.2.1"), or NULL if $current_revision is "1.1" * (which obviously means that there is no previous revision number). */ function versioncontrol_cvs_get_previous_revision_number($current_revision) { if ($current_revision === '1.1') { return NULL; } $parts = explode('.', $current_revision); $last_part = array_pop($parts); // For the first commit to a new branch, cut off the two rightmost parts // in order to get the previous revision number. (e.g. "1.3.4.1" is the // first commit on branch 1.3.0.4 which originated in "1.3" from HEAD.) if ($last_part === '1') { array_pop($parts); // e.g., removes the "2" from the example above return implode('.', $parts); } // If we don't have a "1" as last part, we can just decrease this by one // and implode it again to get the previous revision. $last_part = ((int) $last_part) - 1; $parts[] = (string) $last_part; return implode('.', $parts); }