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("<", "&lt;")
49
    s = s.replace(">", "&gt;")
50
    s = s.replace("\"", "&quot;")
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: