' . "\n"); } function get_tag_url($major, $minor, $micro) { return TAGROOT . "/release-$major.$minor.$micro"; } function get_tag_copy_rev($tag_url) { return trim(`svn log --stop-on-copy --verbose --xml $tag_url | grep copyfrom-rev | cut -d "\"" -f 2`); } function get_changelog($from_rev, $to_rev, $url) { $xml = simplexml_load_string(`svn log -r$from_rev:$to_rev --xml --verbose $url`); if ($xml === FALSE) die("Error retrieving SVN log for range $from_rev:$to_rev from $url\n"); return $xml; } function print_preamble($major, $minor, $micro) { print '

AbiWord v' . "$major.$minor.$micro" . ' Changelog

'; } function print_postamble() { print ''; } function is_backport_blocking($msg) { // check if this is an svnmerge.py blocked merge return preg_match('/^Blocked revision.*\.\.\.\.\.\.\.\.(.*)\.\.\.\.\.\.\.\..*$/ms', $msg); } function detect_embedded_commit($msg) { // check if this is an svnmerge.py recorded merge if (!preg_match('/^Recorded merge of.*\.\.\.\.\.\.\.\.(.*)\.\.\.\.\.\.\.\..*$/ms', $msg, $matches)) return FALSE; $lines = explode("\n", trim($matches[1])); if (!preg_match('/.*\|(.*)\|.*\|.*$/', $lines[0], $matches)) return FALSE; $author = trim($matches[1]); $msg = trim(implode(' ', array_slice($lines, 1))); return array($author, $msg); } function human_readable_author($author) { $table = array( 'asfrent' => 'Andrei Sfrent', 'cjl' => 'Chris Leonard', 'dom' => 'Dominic Lachowicz', 'hub' => 'Hubert Figuière', 'ib' => 'Ingo Brückl', 'jbrefort' => 'Jean Bréfort', 'monkeyiq' => 'Ben Martin', 'msevior' => 'Martin Sevior', 'pradeeban' => 'Kathiravelu Pradeeban', 'slaroche' => 'Simon Larochelle', 'strbafridrich' => 'Fridrich Strba', 'uwog' => 'Marc Maurer', 'volodymyr' => 'Volodymyr Rudyj', ); if (array_key_exists($author, $table)) return $table[$author]; return $author; } function strip_backports($msg) { if (preg_match('/(.*)\(?(Please backport|Can be backported|Feel free to backport)\.?\)?(.*)/i', $msg, $matches)) $msg = $matches[1] . $matches[3]; return $msg; } function add_bugzilla_links($msg) { if (preg_match('/(.*?)((Bug\s|Fix\s)?(\d\d\d+))(.*)/', $msg, $matches)) { $msg = $matches[1] . '' . $matches[2] . '' . $matches[5]; } return $msg; } function classify_topic($paths, $msg) { $topic = 'Core'; $sub_topic = '-'; // path based classification foreach ($paths as $path) { // Feel free to expand this list to reduce the manual work if (preg_match('/.*\/opendocument\/.*/', $path)) { $topic = 'Import/Export'; $sub_topic = 'OpenDocument'; break; } if (preg_match('/.*\/openxml\/.*/', $path)) { $topic = 'Import/Export'; $sub_topic = 'Office Open XML'; break; } if (preg_match('/.*\/goffice\/.*/', $path)) { $topic = 'Plugins'; $sub_topic = 'GNOME Office'; break; } if (preg_match('/.*\/collab\/.*/', $path)) { $topic = 'Plugins'; $sub_topic = 'Collaboration'; break; } if (preg_match('/.*\/cocoa\/.*/', $path)) { $topic = 'Interface'; $sub_topic = 'Mac OSX'; break; } if (preg_match('/.*MSWrite.*/', $path)) { $topic = 'Plugins'; $sub_topic = 'MS Write'; break; } } // commit message based classification if (preg_match('/.*translation.*/', $msg)) { $topic = 'Internationalization'; } if (preg_match('/.*buildfix.*/i', $msg)) { $topic = 'Development'; } if (preg_match('/.*bump version.*/i', $msg)) { $topic = 'Development'; } return array($topic, $sub_topic); } function parse_logentries($xml) { $result = array(); foreach ($xml->logentry as $logentry) { // get the author $author = trim((string)$logentry->author); // get the commit message if this commit was not a blocking of // blocking (and strip out all the svnmerge.py output if present) $msg = trim((string)$logentry->msg); if (is_backport_blocking($msg)) continue; $commit = detect_embedded_commit($msg); if ($commit !== FALSE) { list($author, $msg) = $commit; } // get the affected paths $paths = array(); foreach ($logentry->paths->path as $path) { $paths[] = (string)$path; } // convert the svn usernames into real-life names $author = human_readable_author($author); // strip double spaces from commit messages $msg = preg_replace('/\s\s+/', ' ', $msg); // strip the common "Please backport" and friends from commit messages $msg = strip_backports($msg); // add convenient links to bugzilla for bug numbers $msg = add_bugzilla_links($msg); // figure out which topic this commit belongs to list($topic, $sub_topic) = classify_topic($paths, $msg); $result[$topic][$sub_topic][] = array(trim($msg), $author); } return $result; } function print_changelog_section($logentries) { print "\n"; } function print_changelog($xml) { $topics = parse_logentries($xml); foreach ($topics as $topic => $sub_topics) { print "

$topic

\n"; // print items without a subtopic first... if (isset($sub_topics['-'])) print_changelog_section($sub_topics['-']); // ... and then print all the subtopics foreach (array_diff_key($sub_topics, array('-' => '')) as $sub_topic => $logentries) { print "

$sub_topic

\n"; print_changelog_section($logentries); } } } function main() { if (sizeof($_SERVER['argv']) != 2) help(); // get the version number list($major, $minor, $micro) = explode('.', $_SERVER['argv'][1]); if (!is_numeric($major) || !is_numeric($minor) || !is_numeric($micro)) help(); if ($micro == 0) die("Sorry, you'll have to do x.y.0 releases by hand :)\n"); // get the revision range between the current and previous tag $tag_url = get_tag_url($major, $minor, $micro); $prev_tag_url = get_tag_url($major, $minor, $micro - 1); $tag_copy_rev = get_tag_copy_rev($tag_url); $prev_tag_copy_rev = get_tag_copy_rev($prev_tag_url) + 1; // retrieve the changelog for this release $xml = get_changelog($prev_tag_copy_rev, $tag_copy_rev, $tag_url); // process the changelog and output a nicely formatted piece of HTML print_preamble($major, $minor, $micro); print_changelog($xml); print_postamble(); } main(); ?>