1
#!/usr/bin/env python
2
3
import os
4
import sys
5
import optparse
6
import string
7
from subprocess import Popen, PIPE
8
import urllib
9
import re
10
11
masterBugsForBranch = {
12
    "393a82ea2fb151c5c47a7c86a48a41617f608dc1": [68616],
13
    "f48a90f4577389c423ae2ea783a6ba31c7fbaae7": [59935],
14
    "54ecd4d4473ddea4fe630726cab29ff06278ca2e": [39121],
15
    "d362c2e1c4559bf5ab4b4efaf3b8b09613f7c3fd": [54202]
16
}
17
18
19
# export PYTHONPATH="/path/to/your/webkit//WebKitTools/Scripts/"
20
21
env = os.environ;
22
23
# called from toplevel?
24
if os.path.exists("%s/WebKitTools/Scripts" % os.getcwd()):
25
    sys.path.append("%s/WebKitTools/Scripts" % os.getcwd())
26
27
if env.has_key("WEBKITDIR"):
28
    sys.path.append("%s/WebKitTools/Scripts/" % env["WEBKITDIR"])
29
30
# XXX last fallback
31
sys.path.append("%s/dev/webkit/land/WebKitTools/Scripts/" % env["HOME"])
32
33
from webkitpy.common.net.bugzilla import Bugzilla
34
from webkitpy.thirdparty.BeautifulSoup import BeautifulStoneSoup, 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
class CommentBugzilla(Bugzilla):
42
    def _parse_bug_dictionary_from_xml(self, page):
43
        dict = Bugzilla._parse_bug_dictionary_from_xml(self, page)
44
        page.seek(0)
45
        soup = BeautifulStoneSoup(page)
46
        dict['comments'] = []
47
        for element in soup.findAll('thetext'):
48
            encodedString = element.string
49
            decodedString = unicode(BeautifulStoneSoup(encodedString, convertEntities=BeautifulStoneSoup.XML_ENTITIES))
50
            dict["comments"].append(decodedString)
51
        dict["blocked"] = [int(element.string) for element in soup.findAll("blocked")]
52
        dict["dependson"] = [int(element.string) for element in soup.findAll("dependson")]
53
        return dict
54
55
    def update_blockers_and_submit_comment(self, bug_id, new_blockers=None, comment_text="", cc=None):
56
        self.authenticate()
57
        self.browser.open(self.bug_url_for_bug_id(bug_id))
58
        self.browser.select_form(name="changeform")
59
60
        # blockers
61
        if new_blockers is not None:
62
            blockString = ""
63
            for blocker in new_blockers:
64
                if len(blockString) > 0:
65
                    blockString += ","
66
                blockString += str(blocker)
67
            self.browser['blocked'] = blockString
68
69
        # comment
70
        if comment_text:
71
            self.browser["comment"] = comment_text
72
            if cc:
73
                self.browser["newcc"] = ", ".join(cc)
74
75
        self.browser.submit()
76
77
def run(cmd, ignore_error=False):
78
    return Popen(cmd, stdout=PIPE).communicate()[0]
79
80
def commentForCherryPick(revision, sha1, branch):
81
    return "Revision r%s cherry-picked into %s with commit %s <http://gitorious.org/webkit/qtwebkit/commit/%s>" % (revision, branch, sha1[:7], sha1[:7])
82
83
def determineIfCherryPickedFromComments(comments, revision, sha1, branch):
84
    # matches new and old style comments (with/without the URL)
85
    commentText = "Revision r%s cherry-picked into %s with commit %s" % (revision, branch, sha1[:7])
86
    for comment in comments:
87
        if comment.find(commentText) != -1:
88
            return True
89
90
    return False
91
92
def normalizeBranchName(branch):
93
    branch = branch.translate(string.maketrans("._/", "---")) # XXX
94
    return branch;
95
96
def prompt(message):
97
    if options.always_no:
98
        print message, "n"
99
        return "n"
100
    return User.prompt(message)
101
102
103
def securityBugsInRange(releaseBranch, commitsRange, ignoreSkipped=False):
104
    skippedBugs = run(["git", "config", "--get-all", "skippedbugs.%s" % normalizeBranchName(releaseBranch)])
105
106
    fullLog = run(["git", "log", "%s" % commitsRange, "--grep=bugs.webkit.org/show_bug.cgi", "--grep=webkit.org/b/"])
107
108
    bugs = []
109
    for line in fullLog.split("\n"):
110
        if "bugs.webkit.org/show_bug.cgi" in line or "webkit.org/b/" in line:
111
            match = re.search("show_bug.cgi.id=(?P<bugId>\d+)(>|\.| )?", line)
112
            if not match:
113
                match = re.search("webkit.org/b/(?P<bugId>\d+)(>|\.| )?", line)
114
            if not match:
115
                continue
116
117
            bugId = match.group("bugId")
118
            bugs.append(bugId)
119
120
    bugs = set(bugs)
121
122
    print "%d bugs to check." % (len(bugs))
123
124
    secBugs = []
125
    count = 0
126
    for bug in bugs:
127
        count += 1
128
        if bug in skippedBugs and not ignoreSkipped:
129
            print "Warning: skipping bug %s" % bug
130
            continue
131
132
        url = "https://bugs.webkit.org/show_bug.cgi?id=%s" % bug
133
        print "[%03d/%03d] Checking %s" % (count, len(bugs), url)
134
135
        f = urllib.urlopen(url)
136
        page = "".join(f.readlines())
137
        if "<title>Access Denied</title>" in page:
138
            secBugs.append(bug)
139
            print " * security bug: %s" % url
140
141
    secBugs.sort()
142
    return secBugs
143
144
def bugsThatNeedToBeCherryPicked(masterBugs, releaseBranch, ignoreSkipped=False):
145
    skippedBugs = run(["git", "config", "--get-all", "skippedbugs.%s" % normalizeBranchName(releaseBranch)])
146
    bugs = []
147
148
    for masterBug in masterBugs:
149
        print "Querying master bug %s" % masterBug
150
        f = urllib.urlopen("https://bugs.webkit.org/showdependencytree.cgi?id=%s&hide_resolved=0" % masterBug)
151
        data = f.readlines()
152
        for line in data:
153
            if "view as bug list" in line:
154
                line = line.strip()
155
                match = re.search('href="(.*?)"', line)
156
                buglist = "https://bugs.webkit.org/%s&bug_status=RESOLVED&bug_status=VERIFIED&resolution=FIXED&ctype=csv" % match.group(1)
157
                f = urllib.urlopen(buglist)
158
                data = f.readlines()
159
                data = data[1:]
160
                for bug in data:
161
                    id = bug.split(",")[0]
162
                    if id in skippedBugs and not ignoreSkipped:
163
                        print "Warning: skipping bug %s" % id
164
                        continue
165
                    bugs.append(id)
166
167
    l = list(set(bugs))
168
    l.sort()
169
    return l
170
171
def addBugToSkipList(bug, branch):
172
    branch = normalizeBranchName(branch)
173
    run(["git", "config", "--add", "skippedbugs.%s" % branch, bug])
174
175
176
def main():
177
    releaseBranch = run(["git", "symbolic-ref", "HEAD"])[11:-1]
178
179
    masterBugs = []
180
    for commit in masterBugsForBranch.keys():
181
        branches = run(["git", "branch", "--contains=%s" % commit]).splitlines()
182
        for branch in branches:
183
            if branch.startswith("* "):
184
                masterBugs = masterBugsForBranch[commit]
185
                break
186
        if masterBugs:
187
            break
188
189
    if not masterBugs:
190
        die("Error, cannot find a master bug for the current branch!")
191
192
    if not options.no_git_pull:
193
        print "Running 'git pull' to make sure the branch is updated..."
194
        run(["git", "pull"])
195
196
    if options.base_commit:
197
        baseCommit = options.base_commit.strip()
198
    else:
199
        baseCommit = run(["git", "merge-base", "HEAD", "master"]).strip()
200
201
    baseRevision = int(run(["git", "svn", "find-rev", "%s" % baseCommit]).strip())
202
    print "Base commit for this branch: %s:r%d" % (baseCommit, baseRevision)
203
204
    if options.bugs:
205
        bugs = options.bugs.split(",")
206
    elif options.security_bugs_from:
207
        print "Checking which bugs are security-related in the range %s" % options.security_bugs_from
208
        bugs = securityBugsInRange(releaseBranch, options.security_bugs_from, options.ignore_skipped)
209
    else:
210
        print "Querying master bugs for a list of bugs with patches that need to be cherry-picked"
211
        bugs = bugsThatNeedToBeCherryPicked(masterBugs, releaseBranch, options.ignore_skipped)
212
213
    print "%s bugs: %s" % (len(bugs), [ int(i) for i in bugs ])
214
    if len(bugs) is 0:
215
        sys.exit(0)
216
217
    bugzilla = CommentBugzilla(dryrun = False)
218
    bugzilla.authenticate()
219
220
    commits = []
221
    count = 0
222
    for bug in bugs:
223
        count = count + 1
224
        dict = bugzilla.fetch_bug_dictionary(bug)
225
226
        print "--- [%03d of %03d] https://bugs.webkit.org/show_bug.cgi?id=%s - %s" % (count, len(bugs), dict["id"], dict["title"])
227
228
        # Figure out which revisions  this bug has been landed as
229
        revisions = set()
230
        for comment in dict['comments']:
231
            # Look for "landed in r1233"
232
            match = re.search("(comm?itt?ed|landed) ?(in)? ?(revision|r) ?(?P<revision>\d+)", comment, re.IGNORECASE)
233
            if not match:
234
                match = re.search("Landed .*in r?(?P<revision>\d+)\.?", comment, re.IGNORECASE)
235
            if not match:
236
                match = re.search("(Landed|comm?itt?ed)\n?.*\n??<?http://trac.webkit.org/changeset/(?P<revision>\d+)\.?", comment, re.IGNORECASE)
237
            if not match:
238
                match = re.search("Landed .*as r?(?P<revision>\d+)\.?", comment, re.IGNORECASE)
239
            if not match:
240
                match = re.search("Landed .*at r?(?P<revision>\d+)\.?", comment, re.IGNORECASE)
241
            if not match:
242
                match = re.search("Fixed in r?(?P<revision>\d+)\.?", comment, re.IGNORECASE)
243
            if not match:
244
                match = re.search("cherry-pick-for-backport: ?<r(?P<revision>\d+)>", comment, re.IGNORECASE)
245
            if not match:
246
                match = re.search("<cherry-pick-for-backport: ?r?(?P<revision>\d+)>", comment, re.IGNORECASE)
247
            if not match:
248
                match = re.search("^r?(?P<revision>\d+)\.?$", comment, re.IGNORECASE)
249
            if not match:
250
                match = re.search("^<?http://trac.webkit.org/changeset/(?P<revision>\d+)>?$", comment, re.IGNORECASE)
251
            if match:
252
                rev = int(match.group('revision'))
253
                if rev > baseRevision / 3: ## XXX arbitrary workaround for a few bogus detections such as "fixed in 4.7" (qt)
254
                    revisions.add(rev)
255
256
        if len(revisions) == 0:
257
            # TODO: Look in the repo for references to the bug URL
258
            print "Could not deduct commit revisions for this bug, please resolve manually."
259
            continue
260
261
        revisions = sorted(revisions)
262
263
        print "  Revisions: %s" % revisions
264
265
        if options.list_only:
266
            continue
267
268
        for rev in revisions:
269
            if rev <= baseRevision:
270
                continue
271
            found = run(["git", "log", "%s.." % baseCommit, "--pretty=found %H", "--grep=git-svn-id:.*@%s " % rev]).strip()
272
            if not "found" in found:
273
                print "  Revision %s has not been cherry-picked into this branch" % rev
274
                sha = run(["git", "svn", "find-rev", "r%s" % rev]).strip()
275
                if len(sha) == 0:
276
                    die("Cannot determine sha1 of revision %s. Did you forget to run git-svn fetch?" % rev)
277
                cmd = "git cherry-pick %s" % sha
278
                if prompt("Cherry-pick revision %s (%s)? (y/n) " % (rev, sha)) == "y":
279
                    if os.system(cmd) != 0:
280
                        die("Executing %s failed." % cmd)
281
282
        allCherryPicked = True
283
        foundCommentsForAllCherryPicks = True
284
        comment = ""
285
        for rev in revisions:
286
            if rev <= baseRevision:
287
                continue
288
            found = run(["git", "log", "%s.." % baseCommit, "--pretty=found %H", "--grep=git-svn-id:.*@%s " % rev]).strip()
289
            if "found" in found:
290
                sha1 = found[6:]
291
                print "%s for revision %s"  %(found, rev)
292
                if determineIfCherryPickedFromComments(dict["comments"], rev, sha1, releaseBranch):
293
                    print "Looks like it was already cherry-picked"
294
                elif prompt("Post comment about cherry-pick? (y/n) ") == "y":
295
                    comment += commentForCherryPick(rev, sha1, releaseBranch) + "\n"
296
                else:
297
                    foundCommentsForAllCherryPicks = False
298
            else:
299
                allCherryPicked = False
300
                foundCommentsForAllCherryPicks = False
301
302
        newBlockers = None
303
        if allCherryPicked:
304
            blockedBugs = set(dict["blocked"]).intersection(set(masterBugs))
305
            if blockedBugs:
306
                response = prompt("Bug has all patches cherry-picked and there are comments for all of them. Remove it from the blockers list? (y/n) ")
307
                if response == "y":
308
                    blockers = dict["blocked"]
309
                    [blockers.remove(bug) for bug in blockedBugs]
310
                    newBlockers = blockers
311
            elif foundCommentsForAllCherryPicks:
312
                print "Bug has all patches cherry-picked and there are comments for all of them. But the bug isn't directly blocking any master bug, so let's skip it."
313
                if not options.no_auto_skip:
314
                    print "(bug has been added to the skip list)"
315
                    addBugToSkipList(bug, releaseBranch)
316
            else:
317
                print "Bug has all patches cherry-picked but there are no comments in there... Please check manually."
318
319
        if comment or newBlockers is not None:
320
            print "Committing changes to bugzilla..."
321
            bugzilla.update_blockers_and_submit_comment(dict["id"], newBlockers, comment)
322
323
if __name__ == "__main__":
324
    try:
325
        op = optparse.OptionParser(usage="usage: %prog [options]")
326
        op.add_option("", "--no-auto-skip", default=False, action="store_true", help="Don't add bugs to the skipped list automatically")
327
        op.add_option("", "--no-git-pull", default=False, action="store_true", help="Don't run 'git pull' on the branch in the beginning")
328
        op.add_option("", "--ignore-skipped", default=False, action="store_true", help="Ignore the skipped list")
329
        op.add_option("", "--always-no", default=False, action="store_true", help="Automatically answer no to all questions")
330
        op.add_option("", "--bugs", type="string", action="store", help="Instead of querying the master bugs, use the list from BUGS (a comma separated list)")
331
        op.add_option("", "--base-commit", type="string", action="store", help="Use BASE_COMMIT as the starting point to check for cherry-picks")
332
        op.add_option("", "--security-bugs-from", type="string", action="store", help="Cherry-pick fixes for all security bugs in the given git range")
333
        op.add_option("", "--list-only", default=False, action="store_true", help="Only list the bugs, don't try to cherry-pick them")
334
335
        options = op.parse_args()[0]
336
        main()
337
    except KeyboardInterrupt:
338
        sys.exit(1)
339
340
# vim:et tw=0 ts=4 sw=4: