version_major . '-';
if (isset($node->version_minor)) {
$tag .= $node->version_minor . '-';
}
$tag .= $node->version_patch;
if (!empty($node->version_extra)) {
$tag .= '-' . strtoupper(preg_replace('/(.+)(\d+)/', '\1-\2', $node->version_extra));
}
return $tag;
}
/**
* Iterates through the {project_projects} table and adds the
* appropriate record to the {project_release_projects} table for each
* entry.
*
* BEWARE: This function contains drupal.org-specific code. Please
* modify the arrays and setting commented below to suit your own
* site's needs.
*/
function populate_project_release_projects() {
list($usec, $sec) = explode(' ', microtime());
$start = (float)$usec + (float)$sec;
$num_prp = db_result(db_query("SELECT count(nid) FROM {project_release_projects}"));
$num_projects = db_result(db_query("SELECT count(nid) FROM {project_projects}"));
if ($num_prp == $num_projects) {
print t("The {project_release_projects} table is already full") .'
';
return;
}
// First, insert a record with the right nid for all projects
db_query("DELETE FROM {project_release_projects}");
db_query("INSERT INTO {project_release_projects} (nid, releases) SELECT nid, 1 FROM {project_projects}");
// Now, special-cases we need to handle:
// All the projects that will not want releases enabled
// BEWARE: drupal.org-specific
$no_release_projects = array(
3202 => 'drupal.org maintenance',
3213 => 'user experience',
18753 => 'documentation',
43378 => 'worldpay (ecommerce contrib)',
67060 => 'event_views (event contrib)',
67375 => 'location_views (location contrib)',
75541 => 'inventorymangement (ecommerce contrib)',
);
// Any projects with a custom version format string (just core)
// BEWARE: drupal.org-specific
$version_formats = array(
3060 => '!major%minor%patch#extra',
11093 => '!major%minor%patch#extra',
);
// Set the right site-wide default for everything else...
// BEWARE: drupal.org-specific
variable_set('project_release_default_version_format', '!api#major%patch#extra');
foreach ($no_release_projects as $nid => $name) {
db_query("UPDATE {project_release_projects} SET releases = 0 WHERE nid = %d", $nid);
}
foreach ($version_formats as $nid => $format) {
db_query("UPDATE {project_release_projects} SET version_format = '%s' WHERE nid = %d", $format, $nid);
}
$num_prp = db_result(db_query("SELECT count(nid) FROM {project_release_projects}"));
list($usec, $sec) = explode(' ', microtime());
$stop = (float)$usec + (float)$sec;
$diff = round(($stop - $start) * 1000, 2);
print t('Added %num records to the {project_release_projects} table in %ms ms', array('%num' => $num_prp, '%ms' => $diff)) .'
';
}
/**
* Iterates through the entire {project_releases} table and converts
* each entry into a new release node.
*/
function convert_all_releases() {
// First, re-load into memory mappings that we completed on previous runs
global $nids_by_rid, $api_taxonomy;
$api_taxonomy = project_release_get_api_taxonomy();
$query = db_query("SELECT nid, rid FROM {project_release_legacy}");
while ($result = db_fetch_object($query)) {
$nids_by_rid[$result->rid] = $result->nid;
if (module_exists('project_issue')) {
db_query("UPDATE {project_issues} SET rid = %d WHERE rid = %d", $result->nid, $result->rid);
}
}
// We desperately need an index on rid for the {project_issues}
// table while doing the conversion, otherwise we spend between
// 30-60 minutes with the UPDATE to repair the rid. With this key,
// it only takes about 1 minute on the whole d.o DB.
db_query("ALTER TABLE {project_issues} ADD KEY rid (rid)");
$num_converted = 0;
$num_considered = 0;
$start_time = time();
$releases = db_query("SELECT pr.*, n.uid, n.title AS project_title FROM {project_releases} pr INNER JOIN {node} n ON pr.nid = n.nid LEFT JOIN {project_release_legacy} prl ON pr.rid = prl.rid WHERE prl.rid IS NULL");
while ($old_release = db_fetch_object($releases)) {
if (convert_release($old_release)) {
$num_converted++;
}
$num_considered++;
}
print t('Considered %num_considered releases, converted %num_converted into nodes in %interval', array('%num_considered' => $num_considered, '%num_converted' => $num_converted, '%interval' => format_interval(time() - $start_time))) .'
';
}
/**
* Determines if a given entry in the {project_releases} table
* corresponds to a real release, or a nightly development snapshot,
* and converts it into the appropriate kind of release node.
*
* BEWARE: This is drupal.org specific code. In our case, the core
* drupal system (project nid #3060) is the only project that's ever
* had "real" releases so far. Everything else has been a nightly dev
* snapshot release. If your site has a different usage, please modify
* the logic in here to meet your needs.
*/
function convert_release($old_release) {
list($usec, $sec) = explode(' ', microtime());
$start = (float)$usec + (float)$sec;
global $nids_by_rid, $api_taxonomy;
// First, save everything that's shared, regardless of the version/type
// Things that go in {node} or {node_revisions}
$node->type = 'project_release';
$node->uid = $old_release->uid ? $old_release->uid : 1;
$node->created = $old_release->created;
$node->changed = $old_release->changed;
$node->body = $old_release->changes ? $old_release->changes : '';
$node->teaser = node_teaser($node->body);
$node->filter = variable_get('filter_default_format', 1);
$node->status = $old_release->status;
$node->revision = 1;
$node->promote = 0;
$node->comment = 0;
// Things that go in {project_release_nodes} (that don't depend on version)
$node->pid = $old_release->nid;
$node->file_path = $old_release->path;
$node->file_date = $old_release->changed;
$node->file_hash = $old_release->hash;
// Now, depending on the project and version, fill in the rest.
if ($old_release->nid == 3060) {
if ($old_release->version == 'cvs') {
// TODO: maybe we should just leave this "release" alone,
$node->version_major = 5;
$node->version_patch = 'x';
$node->version_extra = 'dev';
$node->rebuild = 1;
$node->tag = 'HEAD';
$target_api = '5.x';
}
elseif (preg_match('/^(\d+)\.(\d+)(-.+)?$/', $old_release->version, $matches)) {
// Handle "5.x" style releases
$node->version_major = $matches[1];
$node->version_patch = $matches[2];
$node->version_extra = preg_replace('/^-(.+)$/', '\1', $matches[3]);
$node->tag = generate_core_tag($node);
$node->rebuild = 0;
$target_api = "$matches[1].x";
}
elseif (preg_match('/^(\d+)\.(\d+)\.(\d+)(-.+)?$/', $old_release->version, $matches)) {
// Handle "4.7.x" style releases
$node->version_major = $matches[1];
$node->version_minor = $matches[2];
$node->version_patch = $matches[3];
$node->version_extra = preg_replace('/^-(.+)$/', '\1', $matches[4]);
$node->tag = generate_core_tag($node);
$node->rebuild = 0;
$target_api = "$matches[1].$matches[2].x";
}
else {
print t('ERROR: release %old_release_rid of %old_release_project_title has malformed version (%old_release_version)', array('%old_release_rid' => $old_release->rid, '%old_release_project_title' => $old_release->project_title, '%old_release_version' => $old_release->version)) .'
';
return false;
}
}
elseif ($old_release->version == 'cvs') {
// The "cvs" version is a nightly tarball from the trunk
$node->version_extra = 'dev';
$node->tag = 'HEAD';
$node->rebuild = 1;
}
else {
// Nightly tarball from a specific branch.
preg_match('/^(\d+)\.(\d+)\.(\d+)$/', $old_release->version, $matches);
if ($matches[3] != 0) {
print t('WARNING: release %old_release_rid of %old_release_project_title has unexpected patch-level version (%matches)', array('%old_release_rid' => $old_release->rid, '%old_release_project_title' => $old_release->project_title, '%matches' => $matches[3])) .'
';
}
$target_api = "$matches[1].$matches[2].x";
$node->version_major = 1;
$node->version_patch = 'x';
$node->version_extra = 'dev';
$node->tag = 'DRUPAL-' . $matches[1] . '-' . $matches[2];
$node->rebuild = 1;
}
if (isset($target_api)) {
foreach ($api_taxonomy as $i => $term) {
if ($term->name == $target_api) {
$node->taxonomy[$term->tid] = $term->tid;
$node->version_api_tid = $term->tid;
break;
}
}
}
// Now, set the right kind of title.
$version = '';
if ($node->tag == 'HEAD') {
if ($old_release->nid == 3060) {
$verson = '5.x-dev';
}
else {
$version = t('HEAD');
}
}
else {
$version = project_release_get_version($node);
}
$node->title = t('%project %version', array('%project' => $old_release->project_title, '%version' => $version));
$node->version = $version;
list($usec, $sec) = explode(' ', microtime());
$pre_save = (float)$usec + (float)$sec;
// Now, we can actually create the node.
node_save($node);
list($usec, $sec) = explode(' ', microtime());
$post_save = (float)$usec + (float)$sec;
// Grab these values for a few additional conversions.
$nid = $node->nid;
$rid = $old_release->rid;
// While we're iterating over all the old releases, we can already
// convert all the project_issue nodes to the new value. We'll have
// to fix all the followup comments only after we have the complete
// mapping of rid -> nid
if (module_exists('project_issue')) {
list($usec, $sec) = explode(' ', microtime());
$pre_update = (float)$usec + (float)$sec;
db_query("UPDATE {project_issues} SET rid = %d WHERE rid = %d", $nid, $rid);
list($usec, $sec) = explode(' ', microtime());
$post_update = (float)$usec + (float)$sec;
}
// Keep track of it in our array in RAM for converting issue comments
$nids_by_rid[$rid] = $nid;
// See how long it took.
list($usec, $sec) = explode(' ', microtime());
$stop = (float)$usec + (float)$sec;
$diff = round(($stop - $start) * 1000, 2);
$save_diff = round(($post_save - $pre_save) * 1000, 2);
$update_diff = round(($post_update - $pre_update) * 1000, 2);
// Finally, add an entry to the {project_release_legacy} table so we
// know the mapping of the old rid to the new nid.
db_query("INSERT INTO {project_release_legacy} (rid, nid, pid, time, save_ms, update_ms) VALUES (%d, %d, %d, %d, %d, %d)", $rid, $nid, $old_release->pid, $diff, $save_diff, $update_diff);
return true;
}
function convert_issue_followups() {
if (!module_exists('project_issue')) {
return;
}
global $nids_by_rid;
$start_time = time();
$num = 0;
$errors = 0;
$query = db_query("SELECT pc.*, pi.pid FROM {project_comments} pc INNER JOIN {project_issues} pi ON pc.nid = pi.nid WHERE pc.data RLIKE 'rid'");
while ($comment = db_fetch_object($query)) {
$error_old = 0;
$error_new = 0;
$data = unserialize($comment->data);
$old_rid = $data['old']->rid;
$new_rid = $data['new']->rid;
if ($old_rid) {
if (isset($nids_by_rid[$old_rid])) {
$data['old']->rid = $nids_by_rid[$old_rid];
}
else {
$error_old = $old_rid;
$data['old']->rid = 0;
}
}
if ($new_rid) {
if (isset($nids_by_rid[$new_rid])) {
$data['new']->rid = $nids_by_rid[$new_rid];
}
else {
$error_new = $new_rid;
$data['new']->rid = 0;
}
}
if ($error_old || $error_new) {
// Evil, this comment refers to a rid that wasn't in
// {project_releases}, record it for post-mortem analysis...
db_query("INSERT INTO {project_comments_conversion_errors} (cid, pid, old_rid, new_rid) VALUES (%d, %d, %d, %d)", $comment->cid, $comment->pid, $error_old, $error_new);
$errors++;
}
db_query("UPDATE {project_comments} SET data = '%s' WHERE cid = %d", serialize($data), $comment->cid);
$num++;
}
print t('Converted %num issue followups in %interval', array('%num' => $num, '%interval' => format_interval(time() - $start_time))) .'
';
if ($errors) {
print ''. t('ERROR: problem during conversion of %num issue followups', array('%num' => $errors)) .'
';
}
}
/**
* Updates the {project_projects} table to fix the the "version" field
* (the default release for download) to use the nid for the release
* node that the old rid was turned into.
*/
function convert_default_downloads() {
db_query("UPDATE {project_projects} pp, {project_release_legacy} prl SET pp.version = prl.nid WHERE pp.version = prl.rid");
}
function create_legacy_tables() {
switch ($GLOBALS['db_type']) {
case 'mysql':
case 'mysqli':
db_query("CREATE TABLE IF NOT EXISTS {project_release_legacy} (
rid int(10) unsigned NOT NULL default '0',
nid int(10) unsigned NOT NULL default '0',
pid int(10) unsigned NOT NULL default '0',
time int(10) unsigned NOT NULL default '0',
save_ms int(10) unsigned NOT NULL default '0',
update_ms int(10) unsigned NOT NULL default '0',
PRIMARY KEY (`rid`),
KEY project_release_legacy_pid (`pid`),
KEY project_release_legacy_nid (`nid`)
) /*!40100 DEFAULT CHARACTER SET utf8 */;");
db_query("CREATE TABLE IF NOT EXISTS {project_comments_conversion_errors} (
cid int(10) unsigned NOT NULL default '0',
pid int(10) unsigned NOT NULL default '0',
old_rid int(10) NOT NULL default '-1',
new_rid int(10) NOT NULL default '-1',
PRIMARY KEY (`cid`),
KEY project_comments_conversion_errors_pid (`pid`),
KEY project_comments_conversion_errors_old_rid (`old_rid`),
KEY project_comments_conversion_errors_new_rid (`new_rid`)
) /*!40100 DEFAULT CHARACTER SET utf8 */;");
break;
case 'pgsql':
if (!project_db_table_exists('project_release_legacy')) {
db_query("CREATE TABLE {project_release_legacy} (
rid int(10) unsigned NOT NULL default '0',
nid int(10) unsigned NOT NULL default '0',
pid int(10) unsigned NOT NULL default '0',
time int(10) unsigned NOT NULL default '0',
save_ms int(10) unsigned NOT NULL default '0',
update_ms int(10) unsigned NOT NULL default '0',
PRIMARY KEY (`rid`),
KEY project_release_legacy_pid (`pid`),
KEY project_release_legacy_nid (`nid`)
);");
}
if (!project_db_table_exists('project_comments_conversion_errors')) {
db_query("CREATE TABLE {project_comments_conversion_errors} (
cid int(10) unsigned NOT NULL default '0',
pid int(10) unsigned NOT NULL default '0',
old_rid int(10) NOT NULL default '-1',
new_rid int(10) NOT NULL default '-1',
PRIMARY KEY (`cid`),
KEY project_comments_conversion_errors_pid (`pid`),
KEY project_comments_conversion_errors_old_rid (`old_rid`),
KEY project_comments_conversion_errors_new_rid (`new_rid`)
);");
}
break;
}
}
function populate_project_release_api_taxonomy() {
$vid = _project_release_get_api_vid();
// First, customize the vocabulary itself for our needs:
$vocab['vid'] = $vid;
$vocab['name'] = 'Drupal Core compatibility';
$vocab['nodes']['project_release'] = 1;
$vocab['help'] = 'Specify what version of Drupal Core this release is compatible with.';
$vocab['hierarchy'] = 0;
$vocab['required'] = 1;
$vocab['weight'] = -5;
$vocab['module'] = 'project_release';
taxonomy_save_vocabulary($vocab);
// Now, populate the terms we'll need:
$terms[] = '5.x';
for ($i=7; $i>=0; $i--) {
$terms[] = "4.$i.x";
}
foreach ($terms as $weight => $name) {
$edit = array();
$edit['vid'] = $vid;
$edit['name'] = $name;
$edit['description'] = "Releases of Drupal contributions that are compatible with version $name of Drupal Core";
$edit['weight'] = $weight;
taxonomy_save_term($edit);
}
}
/*
*------------------------------------------------------------
* Real work of this script
*------------------------------------------------------------
*/
include_once './includes/bootstrap.inc';
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);
// If not in 'safe mode', increase the maximum execution time:
if (!ini_get('safe_mode')) {
set_time_limit(2000);
}
if (!module_exists('project_release')) {
print '' . t('ERROR: project_release_update.php requires that you first install the project_release.module') . '';
exit(1);
}
// Pull in the copy of project_db_table_exists()
$path = drupal_get_path('module', 'project');
if (file_exists("$path/project.install")) {
require_once "$path/project.install";
}
$nids_by_rid = array();
populate_project_release_api_taxonomy();
create_legacy_tables();
populate_project_release_projects();
convert_all_releases();
convert_issue_followups();
convert_default_downloads();
// TODO: more user feedback, progress, etc.
// TODO: LOCK relevant tables during conversion?