Overview

Packages

  • Release

Classes

  • Horde_Release
  • Horde_Release_Freecode
  • Horde_Release_MailingList
  • Horde_Release_Sentinel
  • Horde_Release_Whups
  • Overview
  • Package
  • Class
  • Tree
   1: <?php
   2: /**
   3:  * Class to make an "official" Horde or application release.
   4:  *
   5:  * Copyright 1999 Mike Hardy
   6:  * Copyright 2004-2012 Horde LLC (http://www.horde.org/)
   7:  *
   8:  * @category Horde
   9:  * @package  Release
  10:  * @author   Mike Hardy
  11:  * @author   Jan Schneider <jan@horde.org>
  12:  * @license  http://www.horde.org/licenses/lgpl21 LGPL 2.1
  13:  * @link     http://www.horde.org/libraries/Horde_Release
  14:  */
  15: 
  16: /**
  17:  * Class to make an "official" Horde or application release.
  18:  *
  19:  * Copyright 1999 Mike Hardy
  20:  * Copyright 2004-2012 Horde LLC (http://www.horde.org/)
  21:  *
  22:  * See the enclosed file COPYING for license information (LGPL). If you
  23:  * did not receive this file, see http://www.horde.org/licenses/lgpl21.
  24:  *
  25:  * @category Horde
  26:  * @package  Release
  27:  * @author   Mike Hardy
  28:  * @author   Jan Schneider <jan@horde.org>
  29:  * @license  http://www.horde.org/licenses/lgpl21 LGPL 2.1
  30:  * @link     http://www.horde.org/libraries/Horde_Release
  31:  */
  32: class Horde_Release
  33: {
  34:     /**
  35:      * Default options.
  36:      *
  37:      * @var array
  38:      */
  39:     protected $_options = array(
  40:         'test' => false,
  41:         'nocommit' => false,
  42:         'noftp' => false,
  43:         'noannounce' => false,
  44:         'nofreecode' => false,
  45:         'nowhups' => false,
  46:     );
  47: 
  48:     /* Constants for the release foucs - these are used as tags when sending
  49:      * FM the new release announcement.*/
  50:     const FOCUS_INITIAL = 'Initial announcement';
  51:     const FOCUS_MINORFEATURE = 'Minor feature enhancements';
  52:     const FOCUS_MAJORFEATURE = 'Major feature enhancements';
  53:     const FOCUS_MINORBUG = 'Minor bugfixes';
  54:     const FOCUS_MAJORBUG = 'Major bugfixes';
  55:     const FOCUS_MINORSECURITY = 'Minor security fixes';
  56:     const FOCUS_MAJORSECURITY = 'Major security fixes';
  57:     const FOCUS_DOCS = 'Documentation improvements';
  58: 
  59:     /**
  60:      * Version number of release.
  61:      *
  62:      * @var string
  63:      */
  64:     protected $_sourceVersionString;
  65: 
  66:     /**
  67:      * Version number of previous release.
  68:      *
  69:      * @var string
  70:      */
  71:     protected $_oldSourceVersionString;
  72: 
  73:     /**
  74:      * Version number of next release.
  75:      *
  76:      * @var string
  77:      */
  78:     protected $_newSourceVersionString;
  79: 
  80:     /**
  81:      * Version number of next release for docs/CHANGES.
  82:      *
  83:      * @var string
  84:      */
  85:     protected $_newSourceVersionStringPlain;
  86: 
  87:     /**
  88:      * Major version number of Horde compatible to this release.
  89:      *
  90:      * @var string
  91:      */
  92:     protected $_hordeVersionString;
  93: 
  94:     /**
  95:      * Major version number of Horde compatible to the previous release.
  96:      *
  97:      * @var string
  98:      */
  99:     protected $_oldHordeVersionString;
 100: 
 101:     /**
 102:      * CVS tag of release.
 103:      *
 104:      * @var string
 105:      */
 106:     protected $_tagVersionString;
 107: 
 108:     /**
 109:      * CVS tag of previous release.
 110:      *
 111:      * @var string
 112:      */
 113:     protected $_oldTagVersionString;
 114: 
 115:     /**
 116:      * Revision number of CHANGES file.
 117:      *
 118:      * @var string
 119:      */
 120:     protected $_changelogVersion;
 121: 
 122:     /**
 123:      * Revision number of previous CHANGES file.
 124:      *
 125:      * @var string
 126:      */
 127:     protected $_oldChangelogVersion;
 128: 
 129:     /**
 130:      * Version string to use in Whups
 131:      *
 132:      * @var string
 133:      */
 134:     protected $_ticketVersion;
 135: 
 136:     /**
 137:      * Version description to use in Whups
 138:      *
 139:      * @var string
 140:      */
 141:     protected $_ticketVersionDesc = '';
 142: 
 143:     /**
 144:      * Directory name of unpacked tarball.
 145:      *
 146:      * @var string
 147:      */
 148:     protected $_directoryName;
 149: 
 150:     /**
 151:      * Directory name of unpacked previous tarball.
 152:      *
 153:      * @var string
 154:      */
 155:     protected $_oldDirectoryName;
 156: 
 157:     /**
 158:      * Filename of the tarball.
 159:      *
 160:      * @var string
 161:      */
 162:     protected $_tarballName;
 163: 
 164:     /**
 165:      * MD5 sum of the tarball.
 166:      *
 167:      * @var string
 168:      */
 169:     protected $_tarballMD5;
 170: 
 171:     /**
 172:      * Whether or not to create a patch file.
 173:      *
 174:      * @var boolean
 175:      */
 176:     protected $_makeDiff = false;
 177: 
 178:     /**
 179:      * The list of binary diffs.
 180:      *
 181:      * @var array
 182:      */
 183:     protected $_binaryDiffs = array();
 184: 
 185:     /**
 186:      * Whether or not we have an old version to compare against.
 187:      *
 188:      * @var boolean
 189:      */
 190:     protected $_oldVersion = false;
 191: 
 192:     /**
 193:      * Filename of the gzip'ed patch file (without .gz extension).
 194:      *
 195:      * @var string
 196:      */
 197:     protected $_patchName;
 198: 
 199:     /**
 200:      * MD5 sum of the patch file.
 201:      *
 202:      * @var string
 203:      */
 204:     protected $_patchMD5;
 205: 
 206:     /**
 207:      * Whether or not this is a final release version.
 208:      *
 209:      * @var boolean
 210:      */
 211:     protected $_latest = true;
 212: 
 213:     /**
 214:      * Populated when the RELEASE_NOTES file is included.
 215:      * Should probably be refactored to use a setter for each
 216:      * property the RELEASE_NOTES file sets...
 217:      *
 218:      * @var array
 219:      */
 220:     public $notes = array();
 221: 
 222:     /**
 223:      * Load the configuration
 224:      */
 225:     public function __construct($options = array())
 226:     {
 227:         $this->_options = array_merge($this->_options, $options);
 228:         $cvsroot = getenv('CVSROOT');
 229:         if (empty($cvsroot)) {
 230:             putenv('CVSROOT=:ext:' . $this->_options['horde']['user'] . '@cvs.horde.org:/repository');
 231:         }
 232:         print 'CVSROOT ' . getenv('CVSROOT') . "\n";
 233:         if (!empty($this->_options['cvs']['cvs_rsh'])) {
 234:             putenv('CVS_RSH=' . $this->_options['cvs']['cvs_rsh']);
 235:         }
 236:         print 'CVS_RSH ' . getenv('CVS_RSH') . "\n";
 237:     }
 238: 
 239:     public function __get($property)
 240:     {
 241:         return $this->{'_' . $property};
 242:     }
 243: 
 244:     /**
 245:      * Delete the directory given as an argument
 246:      */
 247:     public function deleteDirectory($directory)
 248:     {
 249:         print "Deleting directory $directory\n";
 250:         system("sudo rm -rf $directory");
 251:     }
 252: 
 253:     /**
 254:      * tar and gzip the directory given as an argument
 255:      */
 256:     public function makeTarball()
 257:     {
 258:         print "Setting owner/group to 0/0\n";
 259:         system("sudo chown -R 0:0 $this->_directoryName");
 260: 
 261:         print "Making tarball\n";
 262:         $this->_tarballName = $this->_directoryName . '.tar.gz';
 263:         if (file_exists($this->_tarballName)) {
 264:             unlink($this->_tarballName);
 265:         }
 266:         system("tar -zcf $this->_tarballName $this->_directoryName");
 267:         exec($this->_options['md5'] . ' ' . $this->_tarballName, $this->_tarballMD5);
 268:     }
 269: 
 270:     /**
 271:      * Label all of the source here with the new label given as an argument
 272:      */
 273:     public function tagSource($directory = null, $version = null)
 274:     {
 275:         if (empty($directory)) {
 276:             $directory = $this->_directoryName;
 277:         }
 278:         if (empty($version)) {
 279:             $version = $this->_tagVersionString;
 280:         }
 281:         if (!$this->_options['nocommit']) {
 282:             print "Tagging source in $directory with tag $version\n";
 283:             system("cd $directory;cvs tag -F $version > /dev/null 2>&1");
 284:         } else {
 285:             print "NOT tagging source in $directory (would have been tag $version)\n";
 286:         }
 287:     }
 288: 
 289:     /**
 290:      * Make a diff of the two directories given as arguments
 291:      */
 292:     public function diff()
 293:     {
 294:         $this->_patchName = 'patch-' . $this->_oldDirectoryName . str_replace($this->_options['module'], '', $this->_directoryName);
 295:         print "Making diff between $this->_oldDirectoryName and $this->_directoryName\n";
 296:         system("diff -uNr $this->_oldDirectoryName $this->_directoryName > $this->_patchName");
 297: 
 298:         // Search for binary diffs
 299:         $this->_binaryDiffs = array();
 300:         $handle = fopen($this->_patchName, 'r');
 301:         if ($handle) {
 302:             while (!feof($handle)) {
 303:                 // GNU diff reports binary diffs as the following:
 304:                 // Binary files ./locale/de_DE/LC_MESSAGES/imp.mo and ../../horde/imp/locale/de_DE/LC_MESSAGES/imp.mo differ
 305:                 if (preg_match("/^Binary files (.+) and (.+) differ$/i", rtrim(fgets($handle)), $matches)) {
 306:                     $this->_binaryDiffs[] = ltrim(str_replace($this->_oldDirectoryName . '/', '', $matches[1]));
 307:                 }
 308:             }
 309:             fclose($handle);
 310:         }
 311:         system("gzip -9f $this->_patchName");
 312:         exec($this->_options['md5'] . ' ' . $this->_patchName . '.gz', $this->_patchMD5);
 313:     }
 314: 
 315:     /**
 316:      * Change the version file for the module in the directory specified to
 317:      * the version specified
 318:      */
 319:     public function updateVersionFile($directory, $version_string)
 320:     {
 321:         $module = $this->_options['module'];
 322:         $all_caps_module = strtoupper($module);
 323:         print "Updating version file for $module\n";
 324: 
 325:         // construct the filenames
 326:         $filename_only = 'version.php';
 327:         $filename = $directory . '/lib/' . $filename_only;
 328:         $newfilename = $filename . '.new';
 329: 
 330:         $oldfp = fopen($filename, 'r');
 331:         $newfp = fopen($newfilename, 'w');
 332:         while ($line = fgets($oldfp)) {
 333:             if (strstr($line, 'VERSION')) {
 334:                 fwrite($newfp, "<?php define('{$all_caps_module}_VERSION', '$version_string') ?>\n");
 335:             } else {
 336:                 fwrite($newfp, $line);
 337:             }
 338:         }
 339:         fclose($oldfp);
 340:         fclose($newfp);
 341: 
 342:         system("mv -f $newfilename $filename");
 343:         if (!$this->_options['nocommit']) {
 344:             system("cd $directory/lib/; cvs commit -f -m \"Tarball script: building new $module release - $version_string\" $filename_only > /dev/null 2>&1");
 345:         }
 346:     }
 347: 
 348:     /**
 349:      * Update the CHANGES file with the new version number
 350:      */
 351:     public function updateSentinel()
 352:     {
 353:         $module = $this->_options['module'];
 354: 
 355:         print "Updating CHANGES file for $module\n";
 356: 
 357:         $filename_only = 'CHANGES';
 358:         $updater = new Horde_Release_Sentinel($this->_directoryName);
 359:         $updater->updateChanges($this->_newSourceVersionStringPlain);
 360: 
 361:         if (!$this->_options['nocommit']) {
 362:             system("cd {$this->_directoryName}/docs/; cvs commit -f -m \"Tarball script: building new $module release - {$this->_newSourceVersionString}\" $filename_only > /dev/null 2>&1");
 363:         }
 364:     }
 365: 
 366:     /**
 367:      * get and save the revision number of the CHANGES file
 368:      */
 369:     public function saveChangelog($old = false, $directory = null)
 370:     {
 371:         if (empty($directory)) {
 372:             if ($old) {
 373:                 $directory = './' . $this->_oldDirectoryName . '/docs';
 374:             } else {
 375:                 $directory = './' . $this->_directoryName . '/docs';
 376:             }
 377:         }
 378:         if (!$old) {
 379:             include "$directory/RELEASE_NOTES";
 380:             if (strlen($this->notes['fm']['changes']) > 600) {
 381:                 print "WARNING: freecode release notes are longer than 600 characters!\n";
 382:             }
 383:         }
 384:         exec("cd $directory; cvs status CHANGES", $output);
 385:         foreach ($output as $line) {
 386:             if (preg_match('/Repository revision:\s+([\d.]+)/', $line, $matches)) {
 387:                 if ($old) {
 388:                     $this->_oldChangelogVersion = $matches[1];
 389:                 } else {
 390:                     $this->_changelogVersion = $matches[1];
 391:                 }
 392:                 break;
 393:             }
 394:         }
 395:     }
 396: 
 397:     /**
 398:      * work through the source directory given, cleaning things up by removing
 399:      * directories and files we don't want in the tarball
 400:      */
 401:     public function cleanDirectories($directory)
 402:     {
 403:         print "Cleaning source tree\n";
 404:         $directories = explode("\n", `find $directory -type d \\( -name CVS -o -name packaging -o -name framework \\) -print | sort -r`);
 405:         foreach ($directories as $dir) {
 406:             system("rm -rf $dir");
 407:         }
 408:         $cvsignores = explode("\n", `find $directory -name .cvsignore -print`);
 409:         foreach ($cvsignores as $file) {
 410:             if (!empty($file)) {
 411:                 unlink($file);
 412:             }
 413:         }
 414:     }
 415: 
 416:     /**
 417:      * Check out the tag we've been given to work with and move it to the
 418:      * directory name given
 419:      */
 420:     public function checkOutTag($mod_version, $directory, $module = null)
 421:     {
 422:         if (empty($module)) {
 423:             $module = $this->_options['module'];
 424:         }
 425: 
 426:         if (@is_dir($module)) {
 427:             system("rm -rf $module");
 428:         }
 429: 
 430:         // Use CVS to check the source out
 431:         if ($mod_version == 'HEAD') {
 432:             print "Checking out HEAD for $module\n";
 433:             $cmd = "cvs -q co -P $module > /dev/null";
 434:             system($cmd, $status);
 435:         } else {
 436:             print "Checking out tag $mod_version for $module\n";
 437:             $cmd = "cvs -q co -P -r$mod_version $module > /dev/null";
 438:             system($cmd, $status);
 439:         }
 440:         if ($status) {
 441:             die("\nThere was an error running the command\n$cmd\n");
 442:         }
 443: 
 444:         // Move the source into the directory specified
 445:         print "Moving $module to $directory\n";
 446:         if (@is_dir($directory)) {
 447:             system("rm -rf $directory");
 448:         }
 449:         system("mv -f $module $directory");
 450:     }
 451: 
 452:     /**
 453:      * Checkout and install framework
 454:      */
 455:     public function checkOutFramework($mod_version, $directory)
 456:     {
 457:         if ($this->_options['module'] == 'horde') {
 458:             if ($this->_options['branch'] == 'HEAD') {
 459:                 print "Checking out HEAD for framework\n";
 460:             } else {
 461:                 print "Checking out tag $mod_version for framework\n";
 462:             }
 463:             $cmd = "cd $directory; cvs co -P -r$mod_version framework > /dev/null 2>&1; cd ..";
 464:             system($cmd, $status);
 465:             if ($status) {
 466:                 die("\nThere was an error running the command\n$cmd\n");
 467:             }
 468: 
 469:             $bin_path = isset($this->_options['framework_bin'])
 470:                 ? rtrim($this->_options['framework_bin'], '/') . '/'
 471:                 : '';
 472: 
 473:             print "Installing framework packages\n";
 474:             passthru($bin_path . "install_framework --copy --src ./$directory/framework --horde /tmp --dest ./$directory/lib", $result);
 475:             if ($result) {
 476:                 exit;
 477:             }
 478: 
 479:             print "Setting include path\n";
 480:             $filename = $directory . '/lib/core.php';
 481:             $newfilename = $filename . '.new';
 482:             $oldfp = fopen($filename, 'r');
 483:             $newfp = fopen($newfilename, 'w');
 484:             while ($line = fgets($oldfp)) {
 485:                 fwrite($newfp, str_replace('// ini_set(\'include_path\'', 'ini_set(\'include_path\'', $line));
 486:             }
 487:             fclose($oldfp);
 488:             fclose($newfp);
 489:             system("mv -f $newfilename $filename");
 490:         }
 491:     }
 492: 
 493:     /**
 494:      * Upload tarball to the FTP server
 495:      */
 496:     public function upload()
 497:     {
 498:         $module = $this->_options['module'];
 499:         $user = $this->_options['horde']['user'];
 500:         $identity = empty($this->_options['ssh']['identity']) ? '' : ' -i ' . $this->_options['ssh']['identity'];
 501:         $chmod = "chmod 664 /horde/ftp/pub/$module/$this->_tarballName;";
 502:         if ($this->_makeDiff) {
 503:             $chmod .= " chmod 664 /horde/ftp/pub/$module/patches/$this->_patchName.gz;";
 504:         }
 505:         if ($this->_latest &&
 506:             strpos($this->_options['branch'], 'RELENG') !== 0) {
 507:             $chmod .= " ln -sf $this->_tarballName /horde/ftp/pub/$module/$module-latest.tar.gz;";
 508:         }
 509: 
 510:         if (!$this->_options['noftp']) {
 511:             print "Uploading $this->_tarballName to $user@ftp.horde.org:/horde/ftp/pub/$module/\n";
 512:             system("scp -P 35$identity $this->_tarballName $user@ftp.horde.org:/horde/ftp/pub/$module/");
 513:             if ($this->_makeDiff) {
 514:                 print "Uploading $this->_patchName.gz to $user@ftp.horde.org:/horde/ftp/pub/$module/patches/\n";
 515:                 system("scp -P 35$identity $this->_patchName.gz $user@ftp.horde.org:/horde/ftp/pub/$module/patches/");
 516:             }
 517:             print "Executing $chmod\n";
 518:             system("ssh -p 35 -l $user$identity ftp.horde.org '$chmod'");
 519:         } else {
 520:             print "NOT uploading $this->_tarballName to ftp.horde.org:/horde/ftp/pub/$module/\n";
 521:             if ($this->_makeDiff) {
 522:                 print "NOT uploading $this->_patchName.gz to $user@ftp.horde.org:/horde/ftp/pub/$module/patches/\n";
 523:             }
 524:             print "NOT executing $chmod\n";
 525:         }
 526:     }
 527: 
 528:     /**
 529:      * announce release to mailing lists and freecode.
 530:      */
 531:     public function announce($doc_dir = null)
 532:     {
 533:         $module = $this->_options['module'];
 534:         if (!isset($this->notes)) {
 535:             print "NOT announcing release, RELEASE_NOTES missing.\n";
 536:             return;
 537:         }
 538:         if (!empty($this->_options['noannounce']) ||
 539:             !empty($this->_options['nofreecode'])) {
 540:             print "NOT announcing release on freecode.com\n";
 541:         } else {
 542:             print "Announcing release on freecode.com\n";
 543:         }
 544: 
 545:         if (empty($doc_dir)) {
 546:             $doc_dir = $module . '/docs';
 547:         }
 548: 
 549:         $url_changelog = $this->_oldVersion
 550:             ? "http://cvs.horde.org/diff.php/$doc_dir/CHANGES?rt=horde&r1={$this->_oldChangelogVersion}&r2={$this->_changelogVersion}&ty=h"
 551:             : '';
 552: 
 553:         // Params to add new release on FM
 554:         $version = array('version' => $this->_sourceVersionString,
 555:                          'changelog' => $this->notes['fm']['changes']);
 556: 
 557:         if (is_array($this->notes['fm']['focus'])) {
 558:             $version['tag_list'] = $this->notes['fm']['focus'];
 559:         } else {
 560:             $version['tag_list'] = array($this->notes['fm']['focus']);
 561:         }
 562: 
 563:         // Params to update the various project links on FM
 564:         $links = array();
 565:         $links[] = array('label' => 'Tar/GZ',
 566:                          'location' => "ftp://ftp.horde.org/pub/$module/{$this->_tarballName}");
 567:         if (!empty($url_changelog)) {
 568:             $links[] =  array('label' => 'Changelog',
 569:                               'location' => $url_changelog);
 570:         }
 571: 
 572:         if (!empty($this->_options['noannounce']) ||
 573:             !empty($this->_options['nofreecode'])) {
 574: 
 575:             print "Announcement data:\n";
 576:             print_r($version);
 577:             print_r($links);
 578:         } else {
 579:             try {
 580:                 $fm = $this->_fmPublish($version);
 581:                 $fm = $this->_fmUpdateLinks($links);
 582:             } catch (Horde_Exception $e) {
 583:                 print "Error publishing to FM:\n";
 584:                 print $e->getMessage();
 585:             }
 586:         }
 587: 
 588:         $mailer = new Horde_Release_MailingList(
 589:             $module,
 590:             $this->notes['name'],
 591:             $this->_hordeVersionString,
 592:             $this->_options['ml']['from'],
 593:             isset($this->notes['list']) ? $this->notes['list'] : null,
 594:             $this->_ticketVersion,
 595:             $version['tag_list']
 596:         );
 597: 
 598:         $headers = $mailer->getHeaders();
 599: 
 600:         if (!empty($this->_options['noannounce'])) {
 601:             print "NOT announcing release on " . $headers['To'] . "\n";
 602:         } else {
 603:             print "Announcing release to " . $headers['To'] . "\n";
 604:         }
 605: 
 606:         // Building message text
 607:         $mailer->append($this->notes['ml']['changes']);
 608:         if ($this->_oldVersion) {
 609:             $mailer->append("\n\n" .
 610:                 sprintf('The full list of changes (from version %s) can be viewed here:', $this->_oldSourceVersionString) .
 611:                 "\n\n" .
 612:                 $url_changelog
 613:             );
 614:         }
 615:         $mailer->append("\n\n" .
 616:             sprintf('The %s %s distribution is available from the following locations:', $this->notes['name'], $this->_sourceVersionString) .
 617:             "\n\n" .
 618:             sprintf('    ftp://ftp.horde.org/pub/%s/%s', $module, $this->_tarballName) . "\n" .
 619:             sprintf('    http://ftp.horde.org/pub/%s/%s', $module, $this->_tarballName)
 620:         );
 621:         if ($this->_makeDiff) {
 622:             $mailer->append("\n\n" .
 623:                 sprintf('Patches against version %s are available at:', $this->_oldSourceVersionString) .
 624:                 "\n\n" .
 625:                 sprintf('    ftp://ftp.horde.org/pub/%s/patches/%s.gz', $module, $this->_patchName) . "\n" .
 626:                 sprintf('    http://ftp.horde.org/pub/%s/patches/%s.gz', $module, $this->_patchName)
 627:             );
 628: 
 629:             if (!empty($this->_binaryDiffs)) {
 630:                 $mailer->append("\n\n" .
 631:                     'NOTE: Patches do not contain differences between files containing binary data.' . "\n" .
 632:                     'These files will need to be updated via the distribution files:' . "\n\n    " .
 633:                     implode("\n    ", $this->_binaryDiffs)
 634:                 );
 635:             }
 636:         }
 637:         $mailer->append("\n\n" .
 638:             'MD5 sums for the packages are as follows:' .
 639:             "\n\n" .
 640:             '    ' . $this->_tarballMD5[0] . "\n" .
 641:             '    ' . $this->_patchMD5[0] .
 642:             "\n\n" .
 643:             'Have fun!' .
 644:             "\n\n" .
 645:             'The Horde Team.'
 646:         );
 647: 
 648:         if (!empty($this->_options['noannounce'])) {
 649:             print "Message headers:\n";
 650:             print_r($headers);
 651:             print "Message body:\n" . $mailer->getBody() . "\n";
 652:             return;
 653:         }
 654: 
 655:         // Building and sending message
 656:         try {
 657:             $class = 'Horde_Mail_Transport_' . ucfirst($this->_options['mailer']['type']);
 658:             $mailer->getMail()->send(new $class($this->_options['mailer']['params']));
 659:         } catch (Horde_Mime_Exception $e) {
 660:             print $e->getMessage() . "\n";
 661:         }
 662:     }
 663: 
 664:     /**
 665:      * Attempt to publish the new release to the fm restful api.
 666:      *
 667:      * @param array $params  The array of fm release parameters
 668:      *
 669:      * @return mixed Result of the attempt / PEAR_Error on failure
 670:      */
 671:     protected function _fmPublish($params)
 672:     {
 673:         $key = $this->_options['fm']['user_token'];
 674:         $params['tag_list'] = implode(', ', $params['tag_list']);
 675:         $fm_params = array('auth_code' => $key,
 676:                            'release' => $params);
 677:         $http = new Horde_Http_Client();
 678:         try {
 679:             $response = $http->post('http://freecode.com/projects/' . $this->notes['fm']['project'] . '/releases.json',
 680:                                     Horde_Serialize::serialize($fm_params, Horde_Serialize::JSON),
 681:                                     array('Content-Type' => 'application/json'));
 682:         } catch (Horde_Http_Exception $e) {
 683:             if (strpos($e->getMessage(), '201 Created') === false) {
 684:                 throw new Horde_Exception_Wrapped($e);
 685:             } else {
 686:                 return '';
 687:             }
 688:         }
 689: 
 690:         // 201 Created
 691:         return $response->getBody();
 692:     }
 693: 
 694:     /**
 695:      * Attempt to update FM project links
 696:      */
 697:     public function _fmUpdateLinks($links)
 698:     {
 699:         // Need to get the list of current URLs first, then find the one we want
 700:         // to update.
 701:         $http = new Horde_Http_Client();
 702:         try {
 703:             $response = $http->get('http://freecode.com/projects/' . $this->notes['fm']['project'] . '/urls.json?auth_code=' . $this->_options['fm']['user_token']);
 704:         } catch (Horde_Http_Exception $e) {
 705:             throw new Horde_Exception_Wrapped($e);
 706:         }
 707: 
 708:         $url_response = Horde_Serialize::unserialize($response->getBody(), Horde_Serialize::JSON);
 709:         if (!is_array($url_response)) {
 710:             $url_response = array();
 711:         }
 712: 
 713:         // Should be an array of URL info in response...go through our requested
 714:         // updates and see if we can find the correct 'permalink' parameter.
 715:         foreach ($links as $link) {
 716:             $permalink = '';
 717:             foreach ($url_response as $url) {
 718:                 // FM docs contradict this, but each url entry in the array is
 719:                 // wrapped in a 'url' property.
 720:                 $url = $url->url;
 721:                 if ($link['label'] == $url->label) {
 722:                     $permalink = $url->permalink;
 723:                     break;
 724:                 }
 725:             }
 726:             $link = array('auth_code' => $this->_options['fm']['user_token'],
 727:                           'url' => $link);
 728:             $http = new Horde_Http_Client();
 729:             if (empty($permalink)) {
 730:                 // No link found to update...create it.
 731:                 try {
 732:                     $response = $http->post('http://freecode.com/projects/' . $this->notes['fm']['project'] . '/urls.json',
 733:                                             Horde_Serialize::serialize($link, Horde_Serialize::JSON),
 734:                                             array('Content-Type' => 'application/json'));
 735:                     $response = $response->getBody();
 736:                 } catch (Horde_Http_Exception $e) {
 737:                     if (strpos($e->getMessage(), '201 Created') === false) {
 738:                         throw new Horde_Exception_Wrapped($e);
 739:                     } else {
 740:                         $response = '';
 741:                     }
 742:                 }
 743:             } else {
 744:                 // Found the link to update...update it.
 745:                 try {
 746:                     $response = $http->put('http://freecode.com/projects/' . $this->notes['fm']['project'] . '/urls/' . $permalink . '.json',
 747:                                            Horde_Serialize::serialize($link, Horde_Serialize::JSON),
 748:                                            array('Content-Type' => 'application/json'));
 749:                     $response = $response->getBody();
 750:                     // Status: 200???
 751:                 } catch (Horde_Http_Exception $e) {
 752:                     throw new Horde_Exception_Wrapped($e);
 753:                 }
 754:             }
 755:         }
 756: 
 757:         return true;
 758:     }
 759: 
 760:     /**
 761:      * Do testing (development only)
 762:      */
 763:     public function test()
 764:     {
 765:         if (!$this->_options['test']) {
 766:             return;
 767:         }
 768: 
 769:         print "options['version']={$this->_options['version']}\n";
 770:         print "options['oldversion']={$this->_options['oldversion']}\n";
 771:         print "options['module']={$this->_options['module']}\n";
 772:         print "options['branch']={$this->_options['branch']}\n";
 773: 
 774:         $this->setVersionStrings();
 775: 
 776:         print "hordeVersionString={$this->_hordeVersionString}\n";
 777:         print "oldHordeVersionString={$this->_oldHordeVersionString}\n";
 778:         print "makeDiff={$this->_makeDiff}\n";
 779:         print "oldVersion={$this->_oldVersion}\n";
 780:         print "directoryName={$this->_directoryName}\n";
 781:         if ($this->_oldVersion) {
 782:             print "oldDirectoryName={$this->_oldDirectoryName}\n";
 783:         }
 784:         print "tagVersionString={$this->_tagVersionString}\n";
 785:         if ($this->_oldVersion) {
 786:             print "oldTagVersionString={$this->_oldTagVersionString}\n";
 787:         }
 788:         print "sourceVersionString={$this->_sourceVersionString}\n";
 789:         if ($this->_oldVersion) {
 790:             print "oldSourceVersionString={$this->_oldSourceVersionString}\n";
 791:         }
 792:         print "newSourceVersionString={$this->_newSourceVersionString}\n";
 793:         print "newSourceVersionStringPlain={$this->_newSourceVersionStringPlain}\n";
 794:         print "ticketVersion={$this->_ticketVersion}\n";
 795:         print "ticketVersionDesc=MODULE{$this->_ticketVersionDesc}\n";
 796:         if ($this->_latest) {
 797:             print "This is a production release\n";
 798:         }
 799:         exit(0);
 800:     }
 801: 
 802:     /**
 803:      * Add the new version to bugs.horde.org
 804:      */
 805:     public function addWhupsVersion()
 806:     {
 807:         if (!isset($this->notes)) {
 808:             print "\nNOT updating bugs.horde.org, RELEASE_NOTES missing.\n";
 809:             return;
 810:         }
 811:         $this->_ticketVersionDesc = $this->notes['name'] . $this->_ticketVersionDesc;
 812: 
 813:         $params = array('url' => 'https://dev.horde.org/horde/rpc.php',
 814:                         'user' => $this->_options['horde']['user'],
 815:                         'pass' => $this->_options['horde']['pass']);
 816:         $whups = new Horde_Release_Whups($params);
 817: 
 818:         if (!$this->_options['nowhups']) {
 819:             print "Adding new versions to bugs.horde.org: ";
 820:             /* Set the new version in the queue */
 821:             try {
 822:                 $whups->addNewVersion($this->_options['module'], $this->_ticketVersion, $this->_ticketVersionDesc);
 823:                 print "OK\n";
 824:             } catch (Horde_Exception $e) {
 825:                 print "Failed:\n";
 826:                 print $e->getMessage() . "\n";
 827:             }
 828:         } else {
 829:             print "NOT updating bugs.horde.org:\n";
 830:             print "New ticket version WOULD have been {$this->_ticketVersion}\n";
 831:             print "New ticket version description WOULD have been {$this->_ticketVersionDesc}\n";
 832: 
 833:             /* Perform some sanity checks on bugs.horde.org */
 834:             try {
 835:                 $queue = $whups->getQueueId($this->_options['module']);
 836: 
 837:                 if ($queue === false) {
 838:                     print "Was UNABLE to locate the queue id for {$this->_options['module']}\n";
 839:                 } else {
 840:                     print "The queue id on bugs.horde.org is $queue \n";
 841:                 }
 842:             } catch (Horde_Exception $e) {
 843:                 print "Will be UNABLE to update bugs.horde.org:\n";
 844:                 print $e->getMessage() . "\n";
 845:             }
 846:         }
 847:     }
 848: 
 849:     /**
 850:      * Set the version strings to use given the arguments
 851:      */
 852:     public function setVersionStrings()
 853:     {
 854:         $ver = explode('.', $this->_options['version']);
 855:         if (preg_match('/(\d+)\-(.*)/', $ver[count($ver) - 1], $matches)) {
 856:             $ver[count($ver) - 1] = $matches[1];
 857:             $plus = $matches[2];
 858:         }
 859:         if (preg_match('/(H\d)-(\d+)/', $ver[0], $matches)) {
 860:             $ver[0] = $matches[2];
 861:             $this->_hordeVersionString = $matches[1];
 862:         }
 863:         if (count($ver) > 2 && $ver[count($ver) - 1] == '0') {
 864:             die("version {$this->_options['version']} should not have the trailing 3rd-level .0\n");
 865:         }
 866: 
 867:         // check if --oldversion is empty or 0
 868:         if (!empty($this->_options['oldversion'])) {
 869:             $this->_oldVersion = true;
 870:         }
 871:         $oldver = explode('.', $this->_options['oldversion']);
 872:         if (preg_match('/(\d+)\-(.*)/', $oldver[count($oldver) - 1], $matches)) {
 873:             $oldver[count($oldver) - 1] = $matches[1];
 874:             $oldplus = $matches[2];
 875:         }
 876:         if (preg_match('/(H\d)-(\d+)/', $oldver[0], $matches)) {
 877:             $oldver[0] = $matches[2];
 878:             $this->_oldHordeVersionString = $matches[1];
 879:         }
 880: 
 881:         // set the string to use as the tag name in CVS
 882:         $this->_tagVersionString = strtoupper($this->_options['module'] . '_' . preg_replace('/\W/', '_', implode('_', $ver)));
 883:         if (isset($plus)) {
 884:             $this->_tagVersionString .= '_' . $plus;
 885:         }
 886: 
 887:         // create patches only if not a major version change
 888:         if ($this->_options['oldversion'] && $ver[0] == $oldver[0]) {
 889:             $this->_makeDiff = true;
 890:         }
 891: 
 892:         // is this really a production release?
 893:         if (isset($plus) && !preg_match('/^pl\d/', $plus)) {
 894:             $this->_latest = false;
 895:         }
 896: 
 897:         // set the string to insert into the source version file
 898:         $this->_sourceVersionString = implode('.', $ver);
 899:         if (isset($plus)) {
 900:             $this->_sourceVersionString .= '-' . $plus;
 901:         }
 902: 
 903:         // set the string to be used for the directory to package from
 904:         $this->_directoryName = $this->_options['module'] . '-';
 905:         if (!empty($this->_hordeVersionString)) {
 906:             $this->_directoryName .= $this->_hordeVersionString . '-';
 907:         }
 908:         $this->_directoryName = strtolower($this->_directoryName . $this->_sourceVersionString);
 909: 
 910:         if (!empty($this->_hordeVersionString)) {
 911:             $this->_sourceVersionString = $this->_hordeVersionString . ' (' . $this->_sourceVersionString . ')';
 912:         }
 913: 
 914:         if ($this->_oldVersion) {
 915:             $this->_oldSourceVersionString = implode('.', $oldver);
 916:             if (isset($oldplus)) {
 917:                 $this->_oldSourceVersionString .= '-' . $oldplus;
 918:             }
 919:             $this->_oldTagVersionString = strtoupper($this->_options['module'] . '_' . implode('_', $oldver));
 920:             if (isset($oldplus)) {
 921:                 $this->_oldTagVersionString .= '_' . $oldplus;
 922:             }
 923:             $this->_oldDirectoryName = strtolower($this->_options['module'] . '-' . $this->_oldHordeVersionString . $this->_oldSourceVersionString);
 924:             $this->_oldDirectoryName = $this->_options['module'] . '-';
 925:             if (!empty($this->_oldHordeVersionString)) {
 926:                 $this->_oldDirectoryName .= $this->_oldHordeVersionString . '-';
 927:             }
 928:             $this->_oldDirectoryName = strtolower($this->_oldDirectoryName . $this->_oldSourceVersionString);
 929: 
 930:             if (!empty($this->_oldHordeVersionString)) {
 931:                 $this->_oldSourceVersionString = $this->_oldHordeVersionString . ' (' . $this->_oldSourceVersionString . ')';
 932:             }
 933:         }
 934: 
 935:         // Set string to use for updating ticketing system.
 936:         $this->_ticketVersion = implode('.', $ver);
 937:         if (!empty($plus)) {
 938:             $this->_ticketVersion .= '-' . $plus;
 939:         }
 940: 
 941:         if (!empty($this->_hordeVersionString)) {
 942:             $this->_ticketVersionDesc .= ' ' . $this->_hordeVersionString;
 943:         }
 944: 
 945:         // Account for the 'special' case of the horde module.
 946:         if ($this->_options['module'] == 'horde') {
 947:             $this->_ticketVersionDesc .= ' ' . implode('.', $ver);
 948:         } else {
 949:             $this->_ticketVersionDesc .= ' (' . implode('.', $ver) . ')';
 950:         }
 951: 
 952:         // See if we have a 'Final', 'Alpha', or 'RC' to add.
 953:         if ($this->_latest) {
 954:             $this->_ticketVersionDesc .= ' Final';
 955:         } elseif (!empty($plus) &&
 956:                   preg_match('/^RC(\d+)/', $plus, $matches)) {
 957:             $this->_ticketVersionDesc .= ' Release Candidate ' . $matches[1];
 958: 
 959:         } elseif (!empty($plus) && strtolower($plus) == 'alpha') {
 960:             $this->_ticketVersionDesc .= ' Alpha';
 961:         }
 962: 
 963:         // set the name of the string to put into the source version file when
 964:         // done
 965:         if (!isset($plus)) {
 966:             while (count($ver) < 3) {
 967:                 $ver[] = '0';
 968:             }
 969:             $ver[count($ver) - 1] += 1;
 970:         }
 971:         $this->_newSourceVersionString = implode('.', $ver) . '-cvs';
 972:         $this->_newSourceVersionStringPlain = $this->_newSourceVersionString;
 973: 
 974:         if (!empty($this->_hordeVersionString)) {
 975:             $this->_newSourceVersionString = $this->_hordeVersionString .
 976:                 ' (' . $this->_newSourceVersionString . ')';
 977:         }
 978: 
 979:     }
 980: 
 981:     /**
 982:      * Get all of the command-line arguments from the user
 983:      */
 984:     public function getArguments()
 985:     {
 986:         global $argv;
 987: 
 988:         // Parse the command-line arguments
 989:         array_shift($argv);
 990:         foreach ($argv as $arg) {
 991:             if (preg_match('/--module=(.*)/', $arg, $matches)) {
 992:                 // Check to see if they gave us a module
 993:                 $this->_options['module'] = $matches[1];
 994: 
 995:             } elseif (preg_match('/--version=(.*)/', $arg, $matches)) {
 996:                 // Check to see if they tell us the version of the tarball to make
 997:                 $this->_options['version']= $matches[1];
 998: 
 999:             } elseif (preg_match('/--oldversion=(.*)/', $arg, $matches)) {
1000:                 // Check to see if they tell us the last release version
1001:                 $this->_options['oldversion']= $matches[1];
1002: 
1003:             } elseif (preg_match('/--branch=(.*)/', $arg, $matches)) {
1004:                 // Check to see if they tell us which branch to work with
1005:                 $this->_options['branch']= $matches[1];
1006: 
1007:             } elseif (strstr($arg, '--nocommit')) {
1008:                 // Check to see if they tell us not to commit or tag
1009:                 $this->_options['nocommit']= true;
1010: 
1011:             } elseif (strstr($arg, '--noftp')) {
1012:                 // Check to see if they tell us not to upload
1013:                 $this->_options['noftp']= true;
1014: 
1015:             } elseif (strstr($arg, '--noannounce')) {
1016:                 // Check to see if they tell us not to announce
1017:                 $this->_options['noannounce']= true;
1018: 
1019:             } elseif (strstr($arg, '--nofreecode')) {
1020:                 // Check to see if they tell us not to announce
1021:                 $this->_options['nofreecode']= true;
1022: 
1023:             } elseif (strstr($arg, '--noticketversion')) {
1024:                 // Check to see if they tell us not to add new ticket versions
1025:                 $this->_options['nowhups'] = true;
1026: 
1027:             } elseif (strstr($arg, '--dryrun')) {
1028:                 // Check to see if they tell us to do a dry run
1029:                 $this->_options['nocommit'] = true;
1030:                 $this->_options['noftp'] = true;
1031:                 $this->_options['noannounce'] = true;
1032:                 $this->_options['nowhups'] = true;
1033:                 $this->_options['nofreecode']= true;
1034: 
1035:             } elseif (strstr($arg, '--test')) {
1036:                 // Check to see if they tell us to test (for development only)
1037:                 $this->_options['test']= true;
1038:                 // safety first
1039:                 $this->_options['nocommit'] = true;
1040:                 $this->_options['noftp'] = true;
1041:                 $this->_options['noannounce'] = true;
1042:                 $this->_options['nowhups'] = true;
1043:                 $this->_options['nofreecode']= true;
1044: 
1045:             } elseif (strstr($arg, '--help')) {
1046:                 // Check for help usage.
1047:                 $this->print_usage();
1048:                 exit;
1049: 
1050:             } else {
1051:                 // We have no idea what this is
1052:                 $this->print_usage('You have used unknown arguments: ' . $arg);
1053:                 exit;
1054:             }
1055:         }
1056:     }
1057: 
1058:     /**
1059:      * Check the command-line arguments and set some internal defaults
1060:      */
1061:     public function checkArguments()
1062:     {
1063:         // Make sure that we have a module defined
1064:         if (!isset($this->_options['module'])) {
1065:             $this->print_usage('You must define which module to package.');
1066:             exit;
1067:         }
1068: 
1069:         // Let's make sure that there are valid version strings in here...
1070:         if (!isset($this->_options['version'])) {
1071:             $this->print_usage('You must define which version to package.');
1072:             exit;
1073:         }
1074:         if (!preg_match('/\d+\.\d+.*/', $this->_options['version'])) {
1075:             $this->print_usage('Incorrect version string.');
1076:             exit;
1077:         }
1078:         if (!isset($this->_options['oldversion'])) {
1079:             $this->print_usage('You must define last release\'s version.');
1080:             exit;
1081:         }
1082:         if (!preg_match('/\d+(\.\d+.*)?/', $this->_options['oldversion'])) {
1083:             $this->print_usage('Incorrect old version string.');
1084:             exit;
1085:         }
1086: 
1087:         // Make sure we have a horde.org user
1088:         if (empty($this->_options['horde']['user'])) {
1089:             $this->print_usage('You must define a horde.org user.');
1090:             exit;
1091:         }
1092: 
1093:         // If there is no branch defined, we're using the tip revisions.
1094:         // These releases are always developmental, and should use the HEAD "branch" name.
1095:         if (!isset($this->_options['branch'])) {
1096:             $this->_options['branch'] = 'HEAD';
1097:         }
1098:     }
1099: 
1100:     /**
1101:      * Check the command-line arguments and set some internal defaults
1102:      */
1103:     public function checkSetSystem()
1104:     {
1105:         // Set umask
1106:         umask(022);
1107:     }
1108: 
1109:     /**
1110:      * Show people how to use the damned thing
1111:      */
1112:     public function print_usage($message = null)
1113:     {
1114:         if (!is_null($message)) {
1115:             print "\n***  ERROR: $message  ***\n";
1116:         }
1117: 
1118:         print <<<USAGE
1119: 
1120: make-release.php: Horde release generator.
1121: 
1122:    This script takes as arguments the module to make a release of, the
1123:    version of the release, and the branch:
1124: 
1125:       horde-make-release.php --module=<name>
1126:                          --version=[Hn-]xx.yy[.zz[-<string>]]
1127:                          --oldversion=[Hn-]xx[.yy[.zz[-<string>]]]
1128:                          [--branch=<branchname>] [--nocommit] [--noftp]
1129:                          [--noannounce] [--nofreecode] [--noticketversion]
1130:                          [--test] [--dryrun] [--help]
1131: 
1132:    If you omit the branch, it will implicitly work with the HEAD branch.
1133:    If you release a new major version use the --oldversion=0 option.
1134:    Use the --nocommit option to do a test build (without touching the CVS
1135:    repository).
1136:    Use the --noftp option to not upload any files on the FTP server.
1137:    Use the --noannounce option to not send any release announcements.
1138:    Use the --nofreecode option to not send any freecode announcements.
1139:    Use the --noticketversion option to not update the version information on
1140:    bugs.horde.org.
1141:    The --dryrun option is an alias for:
1142:      --nocommit --noftp --noannounce --nofreecode --noticketversion.
1143:    The --test option is for debugging purposes only.
1144: 
1145:    EXAMPLES:
1146: 
1147:    To make a new development release of Horde:
1148:       horde-make-release.php --module=horde --version=2.1-dev --oldversion=2.0
1149: 
1150:    To make a new stable release of Turba:
1151:       horde-make-release.php --module=turba --version=H3-2.0.2 \
1152:         --oldversion=H3-2.0.1 --branch=FRAMEWORK_3
1153: 
1154:    To make a new stable release of IMP 3:
1155:       horde-make-release.php --module=imp --version=3.0 --oldversion=2.3.7 \
1156:         --branch=RELENG_3
1157: 
1158:    To make a brand new Alpha/Beta/RC release of Luxor:
1159:       horde-make-release.php --module=luxor --version=H3-1.0-ALPHA \
1160:         --oldversion=0
1161: 
1162: USAGE;
1163:     }
1164: 
1165: }
1166: 
API documentation generated by ApiGen