#!/usr/bin/php
<?php

// $Id: cvs-release-notes.php,v 1.5 2007/01/10 19:47:57 webchick Exp $

/**
 * @file
 * Parses all CVS log messages between 2 release tags and automatically
 * generates initial HTML for the release notes. This script must be
 * run inside the root directory of a local CVS workspace of the project
 * you want to generate release notes for.  Assumes "cvs" is in your
 * PATH, and that the workspace has already been checked out with the
 * appropriate CVSROOT.
 *
 * Usage:
 * cvs-release-notes.php [previous-release-tag] [current-release-tag]
 *
 * TODO:
 * - Option to include patch committer if "by" isn't included in message
 * - Pretty formatting of previous release version (instead of the tag)
 * - Lookup issues on d.o to group changes by issue type (bug, feature)
 * - Should strip out leading dashes: "- something"
 * - Should remove the word "Patch " before patch #s so they are
 *   formatted consistently.
 *
 * @author Derek Wright (http://drupal.org/user/46549)
 *
 */

if (count($argv) < 3) {
  usage("You must specify the release tags to compare");
}
$prev = $argv[1];
$cur = $argv[2];

if (!is_dir('CVS')) {
  usage("You must run this script in a local CVS workspace for your project");
}

$changes = get_changes($prev, $cur);
print "<p>Changes since $prev:</p>\n";
print_changes($changes);


function usage($msg = NULL) {
  global $argv;
  if (!empty($msg)) {
    print "ERROR: $msg\n";
  }
  print <<<EOF
Usage: $argv[0] [previous_release_tag] [current_release_tag]
For example:
$argv[0] DRUPAL-4-7--1-0 DRUPAL-4-7--1-1

EOF;
  exit(empty($msg) ? 0 : 1);
}

// Based loosely on cvs.module cvs_process_log()
function get_changes($prev, $cur) {
  $changes = array();
  $rval = '';
  $logs = array();
  exec("cvs -qf log -NS -r$prev::$cur 2>&1", $logs, $rval);
  if ($rval) {
    print "ERROR: 'cvs log' returned failure: $rval";
    print implode("\n", $logs);
    exit(1);
  }
  $msg_sep = '----------------------------';
  $file_sep = '=============================================================================';
  $in_log = false;
  while (($line = next($logs)) !== false) {
    if (preg_match('/^cvs log:.*$/', $line)) {
      next;
    }
    if (trim($line) == $msg_sep || $in_log) {
      if (!$in_log) {
        $in_log = true;
      }
      $entry = new stdClass();
      $parts = explode(' ', next($logs));
      $entry->revision = trim($parts[1]);
      $parts = explode(';', next($logs));
      $entry->date = strtotime(cvs_explode($parts[0]));
      $entry->user = cvs_explode($parts[1]);
      $entry->state = cvs_explode($parts[2]);
      $entry->commitid = cvs_explode($parts[4]);
      $parts = explode(' ', cvs_explode($parts[3]));
      $entry->lines_added = abs($parts[0]);
      $entry->lines_removed = abs($parts[1]);
      $temp = next($logs);
      $comment = substr($temp, 0, 9) != 'branches:' ? $temp : '';
      $cur_log = true;
      while ($cur_log && ($line = next($logs))) {
        if (trim($line) == $msg_sep) {
          prev($logs);  // Need to rewind so our outer loop isn't confused.
          $cur_log = false;
        }
        elseif (trim($line) == $file_sep) {
          $cur_log = false;
          $in_log = false;
        }
        else {
          $comment .= "\n" . $line;
        }
      }
      $entry->comment = trim($comment);
      $changes[$entry->commitid] = $entry;
    }
  }
  return $changes;
}

function print_changes($changes) {
  // Sort changes chronologically
  usort($changes, 'log_date_cmp');
  print "<ul>\n";
  foreach ($changes as $k => $obj) {
    print '<li>' . preg_replace('/#(\d+)/', '<a href="/node/$1">#$1</a>', $obj->comment) . "</li>\n";
  }
  print "</ul>\n";
}

function cvs_explode($text, $delim = ':') {
  $parts = explode($delim, $text, 2);
  return trim($parts[1]);
}

function log_date_cmp($a, $b) {
  if ($a->date == $b->date) {
    return 0;
  }
  return ($a->date < $b->date) ? -1 : 1;
}
