| 1 |
#!/usr/bin/env python |
| 2 |
# -*- coding: utf-8; -*- |
| 3 |
|
| 4 |
import os |
| 5 |
import sys |
| 6 |
import optparse |
| 7 |
from subprocess import Popen, PIPE |
| 8 |
import urllib |
| 9 |
import re |
| 10 |
import string |
| 11 |
from datetime import date |
| 12 |
|
| 13 |
import unicodedata |
| 14 |
import smtplib |
| 15 |
from email.mime.multipart import MIMEMultipart |
| 16 |
from email.mime.text import MIMEText |
| 17 |
|
| 18 |
# export PYTHONPATH="/path/to/your/webkit//WebKitTools/Scripts/" |
| 19 |
|
| 20 |
env = os.environ; |
| 21 |
sys.path.append("%s/dev/webkit/land/WebKitTools/Scripts/" % env["HOME"]) |
| 22 |
if env.has_key("WEBKITDIR"): |
| 23 |
sys.path.append("%s/WebKitTools/Scripts/" % env["WEBKITDIR"]) |
| 24 |
sys.path.append("%s/WebKitTools/Scripts/" % os.getcwd()) |
| 25 |
|
| 26 |
# XXX fscking python2 ascii/utf8 handling |
| 27 |
# http://stackoverflow.com/questions/492483/setting-the-correct-encoding-when-piping-stdout-in-python |
| 28 |
import sys |
| 29 |
import codecs |
| 30 |
sys.stdout = codecs.getwriter('utf8')(sys.stdout) |
| 31 |
|
| 32 |
from webkitpy.common.checkout.changelog import parse_bug_id |
| 33 |
from webkitpy.common.net.bugzilla import Bugzilla |
| 34 |
from webkitpy.thirdparty.BeautifulSoup import BeautifulSoup, SoupStrainer |
| 35 |
from webkitpy.common.system.user import User |
| 36 |
|
| 37 |
def die(msg): |
| 38 |
sys.stderr.write(msg + "\n") |
| 39 |
sys.exit(1) |
| 40 |
|
| 41 |
def normalizeBranchName(branch): |
| 42 |
return branch.translate(string.maketrans("._/", "---")) # XXX |
| 43 |
|
| 44 |
def run(cmd, ignore_error=False): |
| 45 |
return Popen(cmd, stdout=PIPE).communicate()[0] |
| 46 |
|
| 47 |
def escapeHtml(s): |
| 48 |
s = s.replace("<", "<") |
| 49 |
s = s.replace(">", ">") |
| 50 |
s = s.replace("\"", """) |
| 51 |
return s |
| 52 |
|
| 53 |
def lastTag(): |
| 54 |
branch = run(["git", "symbolic-ref", "HEAD"])[11:-1] |
| 55 |
base = run(["git", "merge-base", "HEAD", "master"]).strip() |
| 56 |
tags = run(["git", "tag", "--contains", base, "-l", "%s*" % branch]) |
| 57 |
|
| 58 |
if tags: |
| 59 |
return tags.strip().splitlines()[-1] |
| 60 |
else: |
| 61 |
return base |
| 62 |
|
| 63 |
def extractShortChangeLog(sha1): |
| 64 |
log = run(["git", "log", "-1", sha1]).splitlines() |
| 65 |
|
| 66 |
# merge commit |
| 67 |
if log[1].startswith("Merge: "): |
| 68 |
return log[5].strip() |
| 69 |
|
| 70 |
for line in log[4:]: |
| 71 |
line = line.strip() |
| 72 |
if len(line) == 0: |
| 73 |
continue |
| 74 |
if re.search("20..-..-.. .*<.*>", line): |
| 75 |
continue |
| 76 |
if re.search("reviewed by |unreviewed|patch by", line, re.IGNORECASE): |
| 77 |
continue |
| 78 |
if re.search("^http.?://bugs.webkit", line): |
| 79 |
break; |
| 80 |
if re.search("^\*|^\(", line, re.IGNORECASE): |
| 81 |
break; |
| 82 |
return line |
| 83 |
|
| 84 |
# just in case |
| 85 |
return log[4].strip() |
| 86 |
|
| 87 |
class MailSender(object): |
| 88 |
def __init__(self): |
| 89 |
env = os.environ |
| 90 |
if env.has_key("WEBKIT_EMAILSENDER"): |
| 91 |
self.src = env["WEBKIT_EMAILSENDER"] |
| 92 |
# accept "Foobar <foo@bar.com>" or "foo@bar.com" |
| 93 |
self.sender = self.src.split("<")[-1:][0].strip(">") |
| 94 |
else: |
| 95 |
self.src = "WebKit Reports <no-reply@webkit.org>" |
| 96 |
self.sender = "no-reply@webkit.org" |
| 97 |
def send(self, dest, subject, htmlMessage, textMessage): |
| 98 |
msg = MIMEMultipart('alternative') |
| 99 |
msg["Subject"] = subject |
| 100 |
msg["From"] = self.src |
| 101 |
msg["To"] = dest |
| 102 |
# python 2.x unicode handling is a mess and the email modules fail to handle unicode messages |
| 103 |
htmlAscii = unicodedata.normalize('NFKD', htmlMessage).encode('ascii', 'ignore') |
| 104 |
textAscii = unicodedata.normalize('NFKD', textMessage).encode('ascii', 'ignore') |
| 105 |
part1 = MIMEText(textAscii, 'plain') |
| 106 |
part2 = MIMEText(htmlAscii, 'html') |
| 107 |
msg.attach(part1) |
| 108 |
msg.attach(part2) |
| 109 |
try: |
| 110 |
smtp = smtplib.SMTP("localhost") |
| 111 |
smtp.sendmail(self.sender, dest.split(","), msg.as_string()) |
| 112 |
smtp.quit() |
| 113 |
print "Email has been sent" |
| 114 |
except smtplib.SMTPException: |
| 115 |
print "Error: unable to send email" |
| 116 |
|
| 117 |
|
| 118 |
class HtmlReleaseNotes(list): |
| 119 |
def _(self, text): |
| 120 |
return escapeHtml(text).decode("utf-8") |
| 121 |
def append(self, text): |
| 122 |
if text == "": |
| 123 |
list.append(self, u"<br>") |
| 124 |
else: |
| 125 |
list.append(self, u"<p>%s</p>" % self._(text)) |
| 126 |
def startList(self, title): |
| 127 |
list.append(self, u"<p><strong>%s</strong></p>" % self._(title)) |
| 128 |
list.append(self, u"<ul>") |
| 129 |
def endList(self): |
| 130 |
list.append(self, u"</ul>") |
| 131 |
def appendBug(self, id, title): |
| 132 |
list.append(self, u"<li><a href=\"https://webkit.org/b/%s\">#%s</a>: %s" % (id, id, self._(title))) |
| 133 |
def appendCommit(self, commit): |
| 134 |
title = extractShortChangeLog(commit) |
| 135 |
if not title.startswith("Merge branch "): |
| 136 |
list.append(self, u"<li><a href=\"http://gitorious.org/webkit/qtwebkit/commit/%s\">%s</a>: %s" % (commit[:7], commit[:7], self._(title))) |
| 137 |
|
| 138 |
class WikiReleaseNotes(list): |
| 139 |
def _(self, text): |
| 140 |
text = text.replace("^", "{{{^}}}") |
| 141 |
return text.decode("utf-8") |
| 142 |
def append(self, text): |
| 143 |
list.append(self, u"%s" % self._(text)) |
| 144 |
list.append(self, u"") |
| 145 |
def startList(self, title): |
| 146 |
list.append(self, u"=== %s ===" % self._(title)) |
| 147 |
def endList(self): |
| 148 |
list.append(self, u"") |
| 149 |
def appendBug(self, id, title): |
| 150 |
list.append(self, u" * [https://webkit.org/b/%s #%s]: %s" % (id, id, self._(title))) |
| 151 |
def appendCommit(self, commit): |
| 152 |
title = extractShortChangeLog(commit) |
| 153 |
if not title.startswith("Merge branch "): |
| 154 |
list.append(self, u" * [http://gitorious.org/webkit/qtwebkit/commit/%s %s]: %s" % (commit[:7], commit[:7], self._(title))) |
| 155 |
|
| 156 |
|
| 157 |
# XXX I'm not proud of this, but I'm recycling old code that just works :P |
| 158 |
class ReleaseNotes(): |
| 159 |
def __init__(self): |
| 160 |
self.__html = HtmlReleaseNotes() |
| 161 |
self.__wiki = WikiReleaseNotes() |
| 162 |
def html(self): |
| 163 |
return u"\n".join(self.__html) |
| 164 |
def wiki(self): |
| 165 |
return u"\n".join(self.__wiki) |
| 166 |
def append(self, text): |
| 167 |
self.__html.append(text) |
| 168 |
self.__wiki.append(text) |
| 169 |
def startList(self, title): |
| 170 |
self.__html.startList(title) |
| 171 |
self.__wiki.startList(title) |
| 172 |
def endList(self): |
| 173 |
self.__html.endList() |
| 174 |
self.__wiki.endList() |
| 175 |
def appendBug(self, id, title): |
| 176 |
self.__html.appendBug(id, title) |
| 177 |
self.__wiki.appendBug(id, title) |
| 178 |
def appendCommit(self, commit): |
| 179 |
self.__html.appendCommit(commit) |
| 180 |
self.__wiki.appendCommit(commit) |
| 181 |
|
| 182 |
|
| 183 |
def main(): |
| 184 |
op = optparse.OptionParser(usage="usage: %prog [options] [base commit]", |
| 185 |
epilog="If no [base commit] is given, all commits since the branch creation (the fork point) will be considered") |
| 186 |
op.add_option("", "--since-last-tag", default=False, action="store_true", help="use last tag (or the branch fork point) as the base commit") |
| 187 |
op.add_option("", "--up-to", type="string", default="HEAD", action="store", help="last commit considered (default: HEAD)") |
| 188 |
op.add_option("", "--html", default=False, action="store_true", help="output in HTML format (default is wiki formatting)") |
| 189 |
op.add_option("", "--commits", default=False, action="store_true", help="also list commits made to the branch") |
| 190 |
op.add_option("", "--email", type="string", action="store", help="send an e-mail to EMAIL (comma separated list). Designed to be used weekly") |
| 191 |
op.add_option("", "--announce", default=False, action="store_true", help="send an e-mail to the QtWebKit announce mailing list") |
| 192 |
options, args = op.parse_args() |
| 193 |
|
| 194 |
if options.since_last_tag: |
| 195 |
base = lastTag() |
| 196 |
elif args: |
| 197 |
base = args[0] |
| 198 |
else: |
| 199 |
base = run(["git", "merge-base", "HEAD", "master"]).strip() |
| 200 |
|
| 201 |
top = options.up_to |
| 202 |
|
| 203 |
commits = run(["git", "rev-list", "--reverse", base + ".." + top]).strip().splitlines() |
| 204 |
|
| 205 |
if not commits or not commits[0]: |
| 206 |
print "No commits found. Try an older revision (tag)" |
| 207 |
sys.exit(1) |
| 208 |
|
| 209 |
# (Strip off refs/heads/ and newline) |
| 210 |
branchName = run(["git", "symbolic-ref", "HEAD"])[11:-1] |
| 211 |
|
| 212 |
bugzilla = Bugzilla(dryrun = False) |
| 213 |
|
| 214 |
# Some bugs are not detected by the script and are handled by hand |
| 215 |
# They can be stored inside git-config as below: |
| 216 |
# [extraBugs] |
| 217 |
# <normalized branch name> = <bugId>-<commit sha1> |
| 218 |
# <normalized branch name> = <bugId>-<commit sha1> |
| 219 |
# ... |
| 220 |
extraBugs = run(["git", "config", "--get-all", "extraBugs.%s" % normalizeBranchName(branchName)]).strip().splitlines() |
| 221 |
|
| 222 |
bugs = set() |
| 223 |
|
| 224 |
for bug in extraBugs: |
| 225 |
id = bug.partition("-")[0] |
| 226 |
sha1 = bug.partition("-")[2] |
| 227 |
if sha1 in commits: |
| 228 |
print >> sys.stderr, "Warning: bug #%s is from 'git config', (%s)" % (id, sha1) |
| 229 |
bugs.add(int(id)) |
| 230 |
|
| 231 |
for commit in commits: |
| 232 |
log = run(["git", "cat-file", "commit", commit]) |
| 233 |
if re.search("\nThis reverts commit ", log): |
| 234 |
continue |
| 235 |
bug_id = parse_bug_id(log) |
| 236 |
if bug_id: |
| 237 |
bugs.add(int(bug_id)) |
| 238 |
continue |
| 239 |
|
| 240 |
bugs = list(bugs) |
| 241 |
bugs.sort() |
| 242 |
|
| 243 |
print "%s bugs and %s commits from %s to %s" % (len(bugs), len(commits), base, top) |
| 244 |
|
| 245 |
notes = ReleaseNotes() |
| 246 |
|
| 247 |
dateTime = date.today() |
| 248 |
currentWeek = dateTime.isocalendar()[1] |
| 249 |
previousWeek = currentWeek - 1 |
| 250 |
|
| 251 |
notes.append("Release Notes for %s-week%02d - Changes since %s" % (branchName, previousWeek, base)) |
| 252 |
notes.append("%s bugs, %s commits" % (len(bugs), len(commits))) |
| 253 |
notes.append("") |
| 254 |
|
| 255 |
notes.startList("Bugs fixed / Tasks done:") |
| 256 |
for bug_id in bugs: |
| 257 |
try: |
| 258 |
bug = bugzilla.fetch_bug_dictionary(bug_id) |
| 259 |
title = escapeHtml(bug["title"]) |
| 260 |
notes.appendBug(bug_id, title) |
| 261 |
except KeyboardInterrupt: |
| 262 |
raise |
| 263 |
except: |
| 264 |
notes.appendBug(bug_id, "(restricted bug)") |
| 265 |
notes.endList() |
| 266 |
|
| 267 |
if options.commits: |
| 268 |
notes.startList("Commits added or cherry-picked:") |
| 269 |
for commit in commits: |
| 270 |
print commit |
| 271 |
notes.appendCommit(commit) |
| 272 |
notes.endList() |
| 273 |
|
| 274 |
mail = MailSender() |
| 275 |
textBody = notes.wiki() |
| 276 |
htmlBody = notes.html() |
| 277 |
|
| 278 |
if options.email: |
| 279 |
subject = "[announce] %s-week%02d status report - changes since %s" % (branchName, previousWeek, base) |
| 280 |
mail.send(options.email, subject, htmlBody, textBody) |
| 281 |
|
| 282 |
if options.announce: |
| 283 |
subject = "%s-week%02d status report - changes since %s" % (branchName, previousWeek, base) |
| 284 |
response = User.prompt("Really send e-mail to announcement mailing list? (y/n) ") |
| 285 |
if response != "y": |
| 286 |
print "announcement e-mail not sent!" |
| 287 |
else: |
| 288 |
mail.send("qtwebkit-announce@qt.nokia.com", subject, htmlBody, textBody) |
| 289 |
|
| 290 |
if options.html: |
| 291 |
print htmlBody |
| 292 |
else: |
| 293 |
print "---------------" |
| 294 |
print textBody |
| 295 |
print "---------------" |
| 296 |
|
| 297 |
|
| 298 |
if __name__ == "__main__": |
| 299 |
try: |
| 300 |
main() |
| 301 |
except KeyboardInterrupt: |
| 302 |
sys.exit(1) |
| 303 |
|
| 304 |
# vim:et tw=0 ts=4 sw=4: |