/**
 * Figure out what project and API terms to generate the history for.
 */
function project_release_history_generate_all($project_id = 0)
{
    if (!empty($project_id)) {
        if (is_numeric($project_id)) {
            $project_nid = $project_id;
        } else {
            $project_nid = db_result(db_query("SELECT nid FROM {project_projects} WHERE uri = '%s'", $project_id));
        }
        if (empty($project_nid)) {
            wd_err(array('message' => 'Project ID %id not found', 'args' => array('%id' => $project_id)));
            return FALSE;
        }
        wd_msg(array('message' => 'Generating XML release history files for project: %id.', 'args' => array('%id' => $project_id)));
    } else {
        wd_msg(array('message' => 'Generating XML release history files for all projects.', 'args' => array()));
    }
    $api_terms = project_release_compatibility_list();
    $i = 0;
    if (empty($project_nid)) {
        // Generate all.xml files for projects with releases.
        $query = db_query("SELECT DISTINCT(pid) FROM {project_release_nodes}");
        while ($project = db_fetch_object($query)) {
            project_release_history_generate_project_xml($project->pid);
            $i++;
        }
    } else {
        project_release_history_generate_project_xml($project_nid);
        $i++;
    }
    if ($i == 1) {
        wd_msg(array('message' => 'Generated an XML release history summary for a project.'));
    } else {
        wd_msg(array('message' => 'Generated XML release history summaries for @count projects.', 'args' => array('@count' => $i)));
    }
    // Generate XML files based on API compatibility.
    $i = 0;
    $args = array_keys($api_terms);
    $placeholders = db_placeholders($args);
    $where = '';
    if (!empty($project_nid)) {
        $args[] = $project_nid;
        $where = 'AND pid = %d';
    }
    $query = db_query("SELECT DISTINCT(pid), version_api_tid FROM {project_release_nodes} WHERE version_api_tid IN ({$placeholders}) {$where}", $args);
    while ($project = db_fetch_object($query)) {
        project_release_history_generate_project_xml($project->pid, $project->version_api_tid);
        $i++;
    }
    if ($i == 1) {
        wd_msg(array('message' => 'Completed XML release history files for 1 project/version pair'));
    } else {
        wd_msg(array('message' => 'Completed XML release history files for @count project/version pairs', 'args' => array('@count' => $i)));
    }
    return TRUE;
}
function package_release_contrib($type, $nid, $project_short_name, $version, $tag, $release_dir)
{
    global $tmp_dir, $repositories, $dest_root, $dest_rel;
    global $cvs, $tar, $gzip, $rm, $ln;
    global $drush, $drush_make_dir;
    global $license, $trans_install;
    // Files to ignore when checking timestamps:
    $exclude = array('.', '..', 'LICENSE.txt');
    $parts = split('/', $release_dir);
    // modules, themes, theme-engines, profiles, or translations
    $contrib_type = $parts[1];
    // specific directory (same as uri)
    $project_short_name = $parts[2];
    $project_build_root = "{$tmp_dir}/{$project_short_name}";
    $cvs_export_dir = "{$repositories[DRUPAL_CONTRIB_REPOSITORY_ID]['modules']}/{$contrib_type}/{$project_short_name}";
    $release_file_id = $project_short_name . '-' . $version;
    $release_node_view_link = l(t('view'), 'node/' . $nid);
    $file_path_tgz = $dest_rel . '/' . $release_file_id . '.tar.gz';
    $full_dest_tgz = $dest_root . '/' . $file_path_tgz;
    // Remember if the tar.gz version of this release file already exists.
    $tgz_exists = is_file($full_dest_tgz);
    // Clean up any old build directory if it exists.
    // Don't use drupal_exec or return if this fails, we expect it to be empty.
    exec("{$rm} -rf {$project_build_root}");
    // Make a fresh build directory and move inside it.
    if (!mkdir($project_build_root) || !drupal_chdir($project_build_root)) {
        return 'error';
    }
    // Checkout this release from CVS, and see if we need to rebuild it
    if (!drupal_exec("{$cvs} -q export -r {$tag} -d {$project_short_name} {$cvs_export_dir}")) {
        return 'error';
    }
    if (!is_dir("{$project_build_root}/{$project_short_name}")) {
        wd_err("ERROR: %dir does not exist after cvs export -r %tag -d %dir %cvs_export_dir", array('%dir' => $project_short_name, '%rev' => $tag, '%cvs_export_dir' => $cvs_export_dir), $release_node_view_link);
        return 'error';
    }
    $info_files = array();
    $youngest = file_find_youngest($project_short_name, 0, $exclude, $info_files);
    if ($type == 'branch' && $tgz_exists && filemtime($full_dest_tgz) + 300 > $youngest) {
        // The existing tarball for this release is newer than the youngest
        // file in the directory, we're done.
        return 'no-op';
    }
    // Update any .info files with packaging metadata.
    foreach ($info_files as $file) {
        if (!fix_info_file_version($file, $project_short_name, $version)) {
            wd_err("ERROR: Failed to update version in %file, aborting packaging", array('%file' => $file), $release_node_view_link);
            return 'error';
        }
    }
    // Link not copy, since we want to preserve the date...
    if (!drupal_exec("{$ln} -sf {$license} {$project_short_name}/LICENSE.txt")) {
        return 'error';
    }
    // Do we want a subdirectory in the tarball or not?
    $tarball_needs_subdir = TRUE;
    if ($contrib_type == 'translations' && $project_short_name != 'drupal-pot') {
        // Translation projects are packaged differently based on core version.
        if (intval($version) > 5) {
            if (!($to_tar = package_release_contrib_d6_translation($project_short_name, $version, $release_node_view_link))) {
                // Return on error.
                return 'error';
            }
            $tarball_needs_subdir = FALSE;
        } elseif (!($to_tar = package_release_contrib_pre_d6_translation($project_short_name, $version, $release_node_view_link))) {
            // Return on error.
            return 'error';
        }
    } else {
        // Not a translation: just grab the whole directory.
        $to_tar = $project_short_name;
    }
    if (!$tarball_needs_subdir) {
        if (!drupal_chdir($project_short_name)) {
            return 'error';
        }
    }
    // 'h' is for dereference, we want to include the files, not the links
    if (!drupal_exec("{$tar} -ch --file=- {$to_tar} | {$gzip} -9 --no-name > {$full_dest_tgz}")) {
        return 'error';
    }
    $files[] = $file_path_tgz;
    // Start with no package contents, since this is only valid for profiles.
    $package_contents = array();
    // This is a profile, so invoke the drush_make routines to package core
    // and/or any other contrib releases specified in the profile's .make file.
    if ($contrib_type == 'profiles') {
        // Move inside the profile directory.
        if (!drupal_chdir("{$project_build_root}/{$project_short_name}")) {
            return 'error';
        }
        // In order for extended packaging to take place, the profile must have a
        // file named drupal-org.make in the main directory of their profile.
        $drupalorg_makefile = 'drupal-org.make';
        if (file_exists($drupalorg_makefile)) {
            // Search the .make file for the required 'core' attribute.
            $info = drupal_parse_info_file($drupalorg_makefile);
            // Only proceed if a core release was found.
            if (!isset($info['core'])) {
                wd_err("ERROR: %profile does not have the required 'core' attribute.", array('%profile' => $release_file_id), $release_node_view_link);
                return 'error';
            } else {
                // Basic sanity check for the format of the attribute. The CVS checkout
                // attempt of core will handle the rest of the validation (ie, it will
                // fail if a non-existant tag is specified.
                if (!preg_match("/^(\\d+)\\.(\\d+)(-[a-z0-9]+)?\$/", $info['core'], $matches)) {
                    wd_err("ERROR: %profile specified an invalid 'core' attribute -- both API version and release are required.", array('%profile' => $release_file_id), $release_node_view_link);
                    return 'error';
                } else {
                    // Compare the Drupal API version in the profile's version string with
                    // the API version of core specificed in the .make file -- these should
                    // match.
                    $profile_api_version = $matches[1];
                    $parts = explode('.', $version);
                    $release_api_version = $parts[0];
                    if ($profile_api_version != $release_api_version) {
                        wd_err("ERROR: %profile specified an invalid 'core' attribute -- the API version must match the API version of the release.", array('%profile' => $release_file_id), $release_node_view_link);
                        return 'error';
                    }
                }
                // NO-CORE DISTRIBUTION.
                $no_core_id = "{$release_file_id}-no-core";
                // Build the drupal file path and the full file path.
                $no_core_file_path = "{$dest_rel}/{$no_core_id}.tar.gz";
                $no_core_full_dest = "{$dest_root}/{$no_core_file_path}";
                // Run drush_make to build the profile's contents.
                // --drupal-org: Invoke drupal.org specific validation/processing.
                // --drupal-org-build-root: Let the script know where to place it's
                //   build-related files.
                // --drupal-org-log-errors-to-file: Store build errors for later output.
                // --drupal-org-log-package-items-to-file: Store package items for
                //   later recording in the database.
                if (!drupal_exec("{$drush} --include={$drush_make_dir} make --drupal-org --drupal-org-build-root={$project_build_root} --drupal-org-log-errors-to-file --drupal-org-log-package-items-to-file {$drupalorg_makefile} .")) {
                    // The build failed, get any output error messages and include them
                    // in the packaging error report.
                    $build_errors_file = "{$project_build_root}/build_errors.txt";
                    if (file_exists($build_errors_file)) {
                        $lines = file($build_errors_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
                        foreach ($lines as $line) {
                            wd_err("ERROR: {$line}");
                        }
                    }
                    wd_err("ERROR: Build for %profile failed.", array('%profile' => $no_core_id), $release_node_view_link);
                    return 'error';
                }
                // Change into the profile build directory.
                if (!drupal_chdir($project_build_root)) {
                    return 'error';
                }
                // Package the no-core distribution.
                // 'h' is for dereference, we want to include the files, not the links
                if (!drupal_exec("{$tar} -ch --file=- {$project_short_name} | {$gzip} -9 --no-name > {$no_core_full_dest}")) {
                    return 'error';
                }
                $files[] = $no_core_file_path;
                // CORE DISTRIBUTION.
                // Write a small .make file used to build core.
                $core_version = $info['core'];
                $core_build_dir = "drupal-{$core_version}";
                $core_makefile = "{$core_build_dir}.make";
                file_put_contents($core_makefile, core_make_file($core_version));
                // Run drush_make to build core.
                if (!drupal_exec("{$drush} --include={$drush_make_dir} make {$core_makefile} {$core_build_dir}")) {
                    // The build failed, bail out.
                    wd_err("ERROR: Build for %core failed.", array('%core' => $core_build_dir), $release_node_view_link);
                    return 'error';
                }
                // Move the profile into place inside core.
                if (!rename($project_short_name, "{$core_build_dir}/profiles/{$project_short_name}")) {
                    return 'error';
                }
                $core_id = "{$release_file_id}-core";
                // Build the drupal file path and the full file path.
                $core_file_path = "{$dest_rel}/{$core_id}.tar.gz";
                $core_full_dest = "{$dest_root}/{$core_file_path}";
                // Package the core distribution.
                // 'h' is for dereference, we want to include the files, not the links
                if (!drupal_exec("{$tar} -ch --file=- {$core_build_dir} | {$gzip} -9 --no-name > {$core_full_dest}")) {
                    return 'error';
                }
                $files[] = $core_file_path;
                // Development releases may have changed package contents -- clear out
                // their package item summary so a fresh item summary will be inserted.
                if ($type == 'branch' && module_exists('project_package')) {
                    db_query("DELETE FROM {project_package_local_release_item} WHERE package_nid = %d", $nid);
                }
                // Core was built without the drupal.org drush extension, so the
                // package item for core isn't in the package contents file. Retrieve
                // it manually.
                $core_tag = 'DRUPAL-' . str_replace('.', '-', $core_version);
                if (!($core_release_nid = db_result(db_query("SELECT nid FROM {project_release_nodes} WHERE tag = '%s'", $core_tag)))) {
                    return 'error';
                }
                $package_contents[] = $core_release_nid;
                // Retrieve the package contents for the release.
                $package_contents_file = "{$project_build_root}/package_contents.txt";
                if (file_exists($package_contents_file)) {
                    $lines = file($package_contents_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
                    foreach ($lines as $line) {
                        if (is_numeric($line)) {
                            $package_contents[] = $line;
                        }
                    }
                } else {
                    wd_err("ERROR: %file does not exist for %profile release.", array('%file' => $package_contents_file, '%profile' => $release_file_id), $release_node_view_link);
                    return 'error';
                }
            }
        } else {
            wd_msg("No makefile for %profile profile -- skipping extended packaging.", array('%profile' => $release_file_id), $release_node_view_link);
        }
    }
    // As soon as the tarball exists, update the DB
    package_release_update_node($nid, $files, $package_contents);
    if ($tgz_exists) {
        wd_msg("%id has changed, re-packaged.", array('%id' => $release_file_id), $view_link);
    } else {
        wd_msg("Packaged %id.", array('%id' => $release_file_id), $view_link);
    }
    // Don't consider failure to remove this directory a build failure.
    drupal_exec("{$rm} -rf {$project_build_root}");
    return 'success';
}
function package_release($release_nid, $project, $repository, $version, $label)
{
    global $tmp_dir, $drupal_root, $dest_root, $dest_rel;
    global $tar, $gzip, $rm, $ln, $mkdir;
    global $license;
    $project_directory_item = versioncontrol_get_item($repository, $project['directory'], array('label' => $label));
    if (empty($project_directory_item)) {
        wd_err('ERROR: Could not retrieve project directory item.');
    }
    // In Version Control API, item paths start with a slash.
    $relative_project_dir = escapeshellcmd(substr($project['directory'], 1));
    $uri = escapeshellcmd($project['uri']);
    ///TODO: drupal.org specific hack, get rid of it somehow
    $is_core = $site_name == 'drupal.org' && $repository['repo_id'] == 1 && $uri == 'drupal';
    $is_contrib = $site_name == 'drupal.org' && $repository['repo_id'] == 2;
    $id = $uri . '-' . $version;
    $view_link = l(t('view'), 'node/' . $release_nid);
    $file_name = $id . '.tar.gz';
    $file_path = $dest_rel . '/' . $file_name;
    $full_dest = $dest_root . '/' . $file_path;
    $export_dir = $tmp_dir . '/' . $id;
    if ($is_contrib) {
        ///TODO: drupal.org specific hack, get rid of it somehow
        $export_dir = $tmp_dir . '/' . $relative_project_dir;
    }
    $success = versioncontrol_export_directory($repository, $project_directory_item, $export_dir);
    if (!$success) {
        wd_err('ERROR: %dir @ !labeltype %labelname could not be exported', array('%dir' => $export_dir, '!labeltype' => $label['type'] == VERSIONCONTROL_OPERATION_BRANCH ? t('branch') : t('tag'), '%labelname' => $label['name']), $view_link);
        return FALSE;
    }
    $info_files = array();
    // Files to ignore when checking timestamps:
    $exclude = array('.', '..', 'LICENSE.txt');
    $youngest = file_find_youngest($export_dir, 0, $exclude, $info_files);
    if (is_file($full_dest) && filectime($full_dest) + 300 > $youngest) {
        // The existing tarball for this release is newer than the youngest
        // file in the directory, we're done.
        return FALSE;
    }
    ///TODO: drupal.org specific hack, get rid of it somehow
    // Fix any .info files.
    foreach ($info_files as $file) {
        if (!fix_info_file_version($file, $uri, $version)) {
            wd_err('ERROR: Failed to update version in %file, aborting packaging', array('%file' => $file), $view_link);
            return FALSE;
        }
    }
    // Do we want a subdirectory in the tarball or not?
    $tarball_needs_subdir = TRUE;
    ///TODO: drupal.org specific hack, get rid of it somehow
    if ($is_contrib) {
        // Link not copy, since we want to preserve the date.
        if (!drupal_exec("{$ln} -sf {$license} {$export_dir}/LICENSE.txt")) {
            return FALSE;
        }
        $parts = split('/', $relative_project_dir);
        $contrib_type = $parts[1];
        // modules, themes, theme-engines, or translations
        if ($contrib_type == 'translations' && $project['uri'] != 'drupal-pot') {
            ///TODO: drupal.org specific hack (contd.), get rid of it somehow
            // Translation projects are packaged differently based on core version.
            if (intval($version) == 6) {
                if (!($to_tar = package_release_contrib_d6_translation($export_dir, $uri, $version, $view_link))) {
                    // Return on error.
                    return FALSE;
                }
                $tarball_needs_subdir = FALSE;
            } elseif (!($to_tar = package_release_contrib_pre_d6_translation($export_dir, $uri, $version, $view_link))) {
                // Return on error.
                return FALSE;
            }
        }
    }
    if (empty($to_tar)) {
        // Not a translation: just grab the whole directory.
        $to_tar = basename($export_dir);
    }
    // We want tar to get a relative list of paths, so we tell it to change into
    // the parent directory, except for D6 translations which get special cased.
    $tar_dir = $tarball_needs_subdir ? dirname($export_dir) : $export_dir;
    // 'h' is for dereference, we want to include the files, not the links
    if (!drupal_exec("{$tar} -ch --directory {$tar_dir} --file=- {$to_tar} | {$gzip} -9 --no-name > {$full_dest}")) {
        return FALSE;
    }
    // As soon as the tarball exists, we want to update the DB about it.
    package_release_update_node($release_nid, $file_path);
    wd_msg("%id has changed, re-packaged.", array('%id' => $id), $view_link);
    // Don't consider failure to remove this directory a build failure.
    drupal_exec("{$rm} -rf {$export_dir}");
    return TRUE;
}
function package_releases($type, $project_id = 0)
{
    global $drupal_root, $dest_root, $dest_rel, $tmp_dir, $wd_err_msg;
    global $php, $project_release_create_history;
    if (!empty($project_id)) {
        if (is_numeric($project_id)) {
            $project_nid = $project_id;
        } else {
            $project_nid = db_result(db_query("SELECT nid FROM {project_projects} WHERE uri = '%s'", $project_id));
        }
        // We repeatedly clear the node_load() cache, but we have our own cache
        // for loading the project nodes since those tend to repeat and we need to
        // load less of them.
        $project_node = project_release_packager_node_load($project_nid);
        if (empty($project_node)) {
            wd_err('ERROR: Project ID %id not found', array('%id' => $project_id));
            return FALSE;
        }
    }
    $rel_node_join = '';
    $where_args = array();
    if ($type == 'tag') {
        $where = " AND (prn.rebuild = %d) AND (f.filepath IS NULL OR f.filepath = '')";
        $where_args[] = 0;
        // prn.rebuild
        $plural = t('tags');
    } elseif ($type == 'branch') {
        $rel_node_join = " INNER JOIN {node} nr ON prn.nid = nr.nid";
        $where = " AND (prn.rebuild = %d) AND ((f.filepath IS NULL) OR (f.filepath = '') OR (nr.status = %d))";
        $where_args[] = 1;
        // prn.rebuild
        $where_args[] = 1;
        // nr.status
        $plural = t('branches');
        if (empty($project_node)) {
            wd_msg("Starting to package all snapshot releases.");
        } else {
            wd_msg("Starting to package snapshot releases for project id: %project_short_name.", array('%project_short_name' => $project_node->project['uri']), l(t('view'), 'node/' . $project_node->nid));
        }
    } else {
        wd_err("ERROR: package_releases() called with unknown type: %type", array('%type' => $type));
        return FALSE;
    }
    $args = array();
    $args[] = 1;
    // Account for np.status = 1.
    $args[] = 1;
    // Account for prp.releases = 1.
    if (!empty($project_node)) {
        $where .= ' AND prn.pid = %d';
        $where_args[] = $project_node->nid;
    }
    $args = array_merge($args, $where_args);
    $query = db_query("SELECT prn.nid FROM {project_release_nodes} prn {$rel_node_join} LEFT JOIN {project_release_file} prf ON prn.nid = prf.nid LEFT JOIN {files} f ON prf.fid = f.fid INNER JOIN {project_projects} pp ON prn.pid = pp.nid INNER JOIN {node} np ON prn.pid = np.nid INNER JOIN {project_release_projects} prp ON prp.nid = prn.pid WHERE np.status = %d AND prp.releases = %d " . $where . ' ORDER BY pp.uri', $args);
    $num_built = 0;
    $num_considered = 0;
    $project_nids = array();
    // Read everything out of the query immediately so that we don't leave the
    // query object/connection open while doing other queries.
    $releases = array();
    while ($release = db_fetch_object($query)) {
        // This query could pull multiple rows of the same release since multiple
        // files per release node are allowed. Account for this by keying on
        // release nid.
        $releases[$release->nid] = $release->nid;
    }
    foreach ($releases as $release_nid) {
        $wd_err_msg = array();
        // We don't want to waste too much RAM by leaving all these loaded nodes
        // in RAM, so we reset the node_load() cache each time we call it.
        $release_node = node_load($release_nid, NULL, TRUE);
        if (empty($release_node)) {
            wd_err("ERROR: Can't load release node for release ID %nid", array('%nid' => $release_nid));
            continue;
        }
        $packager = project_release_get_packager_plugin($release_node, $dest_root, $dest_rel, $tmp_dir);
        if (empty($packager)) {
            wd_err("ERROR: Can't find packager plugin to use for %release", array('%release' => $release_node->title));
            continue;
        }
        db_query("DELETE FROM {project_release_package_errors} WHERE nid = %d", $release_node->nid);
        chdir($drupal_root);
        $files = array();
        $contents = array();
        $rval = $packager->createPackage($files, $contents);
        $num_considered++;
        chdir($drupal_root);
        switch ($rval) {
            case 'success':
            case 'rebuild':
                project_release_packager_update_node($release_node, $dest_root, $files, $contents);
                module_invoke_all('project_release_create_package', $project_node, $release_node);
                $num_built++;
                $packager->cleanupSuccessfulBuild();
                $release_pid = $release_node->project_release['pid'];
                $project_nids[$release_pid] = TRUE;
                $release_node_view_link = l(t('View'), 'node/' . $release_node->nid);
                if ($rval == 'rebuild') {
                    $msg = '%release_title has changed, re-packaged.';
                } else {
                    $msg = 'Packaged %release_title.';
                }
                wd_msg($msg, array('%release_title' => $release_node->title), $release_node_view_link);
                break;
            case 'error':
                $packager->cleanupFailedBuild();
                break;
        }
        if (count($wd_err_msg)) {
            db_query("INSERT INTO {project_release_package_errors} (nid, messages) values (%d, '%s')", $release_node->nid, serialize($wd_err_msg));
        }
    }
    if ($num_built || $type == 'branch') {
        if (!empty($project_node)) {
            wd_msg("Done packaging releases for @project_short_name from !plural: !num_built built, !num_considered considered.", array('@project_short_name' => $project_node->project['uri'], '!plural' => $plural, '!num_built' => $num_built, '!num_considered' => $num_considered));
        } else {
            wd_msg("Done packaging releases from !plural: !num_built built, !num_considered considered.", array('!plural' => $plural, '!num_built' => $num_built, '!num_considered' => $num_considered));
        }
    }
    // Finally, regenerate release history XML files for all projects we touched.
    if (!empty($project_nids) && !empty($project_release_create_history)) {
        wd_msg('Re-generating release history XML files');
        $i = $fails = 0;
        foreach ($project_nids as $project_nid => $value) {
            if (drupal_exec("{$php} {$project_release_create_history} {$project_nid}")) {
                $i++;
            } else {
                $fails++;
            }
        }
        if (!empty($fails)) {
            wd_msg('ERROR: Failed to re-generate release history XML files for !num project(s)', array('!num' => $fails));
        }
        wd_msg('Done re-generating release history XML files for !num project(s)', array('!num' => $i));
    }
}