| 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: |