Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/python | 1 #!/usr/bin/python |
| 2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 """Snapshot Build Bisect Tool | 6 """Snapshot Build Bisect Tool |
| 7 | 7 |
| 8 This script bisects a snapshot archive using binary search. It starts at | 8 This script bisects a snapshot archive using binary search. It starts at |
| 9 a bad revision (it will try to guess HEAD) and asks for a last known-good | 9 a bad revision (it will try to guess HEAD) and asks for a last known-good |
| 10 revision. It will then binary search across this revision range by downloading, | 10 revision. It will then binary search across this revision range by downloading, |
| 11 unzipping, and opening Chromium for you. After testing the specific revision, | 11 unzipping, and opening Chromium for you. After testing the specific revision, |
| 12 it will ask you whether it is good or bad before continuing the search. | 12 it will ask you whether it is good or bad before continuing the search. |
| 13 """ | 13 """ |
| 14 | 14 |
| 15 # The root URL for storage. | 15 # The root URL for storage. |
| 16 BASE_URL = 'http://commondatastorage.googleapis.com/chromium-browser-snapshots' | 16 BASE_URL = 'http://commondatastorage.googleapis.com/chromium-browser-snapshots' |
| 17 BASE_URL_RECENT = 'http://build.chromium.org/f/chromium/snapshots' | |
| 17 | 18 |
| 18 # URL to the ViewVC commit page. | 19 # URL to the ViewVC commit page. |
| 19 BUILD_VIEWVC_URL = 'http://src.chromium.org/viewvc/chrome?view=rev&revision=%d' | 20 BUILD_VIEWVC_URL = 'http://src.chromium.org/viewvc/chrome?view=rev&revision=%d' |
| 20 | 21 |
| 21 # Changelogs URL. | 22 # Changelogs URL. |
| 22 CHANGELOG_URL = 'http://build.chromium.org/f/chromium/' \ | 23 CHANGELOG_URL = 'http://build.chromium.org/f/chromium/' \ |
| 23 'perf/dashboard/ui/changelog.html?url=/trunk/src&range=%d:%d' | 24 'perf/dashboard/ui/changelog.html?url=/trunk/src&range=%d:%d' |
| 24 | 25 |
| 26 # For BASE_URL_RECENT, the string prefix before each revision number. | |
| 27 RECENT_REV_HTML_PREFIX = \ | |
| 28 '<img src="/icons/folder.gif" alt="[DIR]"></td><td><a href="' | |
| 29 | |
| 25 ############################################################################### | 30 ############################################################################### |
| 26 | 31 |
| 27 import math | 32 import math |
| 28 import optparse | 33 import optparse |
| 29 import os | 34 import os |
| 30 import pipes | 35 import pipes |
| 31 import re | 36 import re |
| 32 import shutil | 37 import shutil |
| 33 import sys | 38 import sys |
| 34 import tempfile | 39 import tempfile |
| 35 import urllib | 40 import urllib |
| 36 from xml.etree import ElementTree | 41 from xml.etree import ElementTree |
| 37 import zipfile | 42 import zipfile |
| 38 | 43 |
| 39 class PathContext(object): | 44 class PathContext(object): |
| 40 """A PathContext is used to carry the information used to construct URLs and | 45 """A PathContext is used to carry the information used to construct URLs and |
| 41 paths when dealing with the storage server and archives.""" | 46 paths when dealing with the storage server and archives.""" |
| 42 def __init__(self, platform, good_revision, bad_revision): | 47 def __init__(self, platform, good_revision, bad_revision, is_recent): |
|
Robert Sesek
2011/07/20 19:05:11
|is_recent| sounds like it's describing the contex
jbates
2011/07/20 21:08:42
Done.
| |
| 43 super(PathContext, self).__init__() | 48 super(PathContext, self).__init__() |
| 44 # Store off the input parameters. | 49 # Store off the input parameters. |
| 45 self.platform = platform # What's passed in to the '-a/--archive' option. | 50 self.platform = platform # What's passed in to the '-a/--archive' option. |
| 46 self.good_revision = good_revision | 51 self.good_revision = good_revision |
| 47 self.bad_revision = bad_revision | 52 self.bad_revision = bad_revision |
| 53 self.is_recent = is_recent | |
| 48 | 54 |
| 49 # The name of the ZIP file in a revision directory on the server. | 55 # The name of the ZIP file in a revision directory on the server. |
| 50 self.archive_name = None | 56 self.archive_name = None |
| 51 | 57 |
| 52 # Set some internal members: | 58 # Set some internal members: |
| 53 # _listing_platform_dir = Directory that holds revisions. Ends with a '/'. | 59 # _listing_platform_dir = Directory that holds revisions. Ends with a '/'. |
| 54 # _archive_extract_dir = Uncompressed directory in the archive_name file. | 60 # _archive_extract_dir = Uncompressed directory in the archive_name file. |
| 55 # _binary_name = The name of the executable to run. | 61 # _binary_name = The name of the executable to run. |
| 56 if self.platform == 'linux' or self.platform == 'linux64': | 62 if self.platform == 'linux' or self.platform == 'linux64': |
| 57 self._listing_platform_dir = 'Linux/' | 63 self._listing_platform_dir = 'Linux/' |
| (...skipping 17 matching lines...) Expand all Loading... | |
| 75 raise Exception("Invalid platform") | 81 raise Exception("Invalid platform") |
| 76 | 82 |
| 77 def GetListingURL(self, marker=None): | 83 def GetListingURL(self, marker=None): |
| 78 """Returns the URL for a directory listing, with an optional marker.""" | 84 """Returns the URL for a directory listing, with an optional marker.""" |
| 79 marker_param = '' | 85 marker_param = '' |
| 80 if marker: | 86 if marker: |
| 81 marker_param = '&marker=' + str(marker) | 87 marker_param = '&marker=' + str(marker) |
| 82 return BASE_URL + '/?delimiter=/&prefix=' + self._listing_platform_dir + \ | 88 return BASE_URL + '/?delimiter=/&prefix=' + self._listing_platform_dir + \ |
| 83 marker_param | 89 marker_param |
| 84 | 90 |
| 91 def GetListingURLRecent(self): | |
| 92 """Returns the URL for a directory listing of recent builds.""" | |
| 93 return BASE_URL_RECENT + '/' + self._listing_platform_dir | |
| 94 | |
| 85 def GetDownloadURL(self, revision): | 95 def GetDownloadURL(self, revision): |
| 86 """Gets the download URL for a build archive of a specific revision.""" | 96 """Gets the download URL for a build archive of a specific revision.""" |
| 87 return "%s/%s%d/%s" % ( | 97 if self.is_recent: |
| 88 BASE_URL, self._listing_platform_dir, revision, self.archive_name) | 98 return "%s/%s%d/%s" % ( |
| 99 BASE_URL_RECENT, self._listing_platform_dir, revision, | |
| 100 self.archive_name) | |
| 101 else: | |
| 102 return "%s/%s%d/%s" % ( | |
| 103 BASE_URL, self._listing_platform_dir, revision, self.archive_name) | |
| 89 | 104 |
| 90 def GetLastChangeURL(self): | 105 def GetLastChangeURL(self): |
| 91 """Returns a URL to the LAST_CHANGE file.""" | 106 """Returns a URL to the LAST_CHANGE file.""" |
| 92 return BASE_URL + '/' + self._listing_platform_dir + 'LAST_CHANGE' | 107 return BASE_URL + '/' + self._listing_platform_dir + 'LAST_CHANGE' |
| 93 | 108 |
| 94 def GetLaunchPath(self): | 109 def GetLaunchPath(self): |
| 95 """Returns a relative path (presumably from the archive extraction location) | 110 """Returns a relative path (presumably from the archive extraction location) |
| 96 that is used to run the executable.""" | 111 that is used to run the executable.""" |
| 97 return os.path.join(self._archive_extract_dir, self._binary_name) | 112 return os.path.join(self._archive_extract_dir, self._binary_name) |
| 98 | 113 |
| (...skipping 76 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 175 (revisions, next_marker) = _FetchAndParse(context.GetListingURL()) | 190 (revisions, next_marker) = _FetchAndParse(context.GetListingURL()) |
| 176 # If the result list was truncated, refetch with the next marker. Do this | 191 # If the result list was truncated, refetch with the next marker. Do this |
| 177 # until an entire directory listing is done. | 192 # until an entire directory listing is done. |
| 178 while next_marker: | 193 while next_marker: |
| 179 (new_revisions, next_marker) = _FetchAndParse( | 194 (new_revisions, next_marker) = _FetchAndParse( |
| 180 context.GetListingURL(next_marker)) | 195 context.GetListingURL(next_marker)) |
| 181 revisions.extend(new_revisions) | 196 revisions.extend(new_revisions) |
| 182 | 197 |
| 183 return revisions | 198 return revisions |
| 184 | 199 |
| 200 def ParseDirectoryIndexRecent(context): | |
| 201 """Parses the recent builds directory listing into a list of revision | |
| 202 numbers.""" | |
| 203 revisions = [] | |
| 204 handle = urllib.urlopen(context.GetListingURLRecent()) | |
| 205 document = handle.read() | |
| 206 | |
| 207 index = document.find(RECENT_REV_HTML_PREFIX) | |
|
Robert Sesek
2011/07/20 19:05:11
Why not use a regex to do this? Use re.compile for
jbates
2011/07/20 21:08:42
Done.
| |
| 208 while index > 0: | |
| 209 index += len(RECENT_REV_HTML_PREFIX) | |
| 210 end_slash_index = document.find('/', index) | |
| 211 rev = int(document[index: end_slash_index]) | |
| 212 revisions.append(rev) | |
| 213 index = document.find(RECENT_REV_HTML_PREFIX, index) | |
| 214 | |
| 215 return revisions | |
| 216 | |
| 185 | 217 |
| 186 def GetRevList(context): | 218 def GetRevList(context): |
| 187 """Gets the list of revision numbers between |good_revision| and | 219 """Gets the list of revision numbers between |good_revision| and |
| 188 |bad_revision| of the |context|.""" | 220 |bad_revision| of the |context|.""" |
| 189 # Download the revlist and filter for just the range between good and bad. | 221 # Download the revlist and filter for just the range between good and bad. |
| 190 rev_range = range(context.good_revision, context.bad_revision) | 222 rev_range = range(context.good_revision, context.bad_revision) |
| 191 revlist = map(int, ParseDirectoryIndex(context)) | 223 revisions = [] |
| 224 if context.is_recent: | |
| 225 revisions = ParseDirectoryIndexRecent(context) | |
| 226 else: | |
| 227 revisions = ParseDirectoryIndex(context) | |
| 228 revlist = map(int, revisions) | |
| 192 revlist = filter(lambda r: r in rev_range, revlist) | 229 revlist = filter(lambda r: r in rev_range, revlist) |
| 193 revlist.sort() | 230 revlist.sort() |
| 194 return revlist | 231 return revlist |
| 195 | 232 |
| 196 | 233 |
| 197 def TryRevision(context, rev, profile, args): | 234 def TryRevision(context, rev, profile, args): |
| 198 """Downloads revision |rev|, unzips it, and opens it for the user to test. | 235 """Downloads revision |rev|, unzips it, and opens it for the user to test. |
| 199 |profile| is the profile to use.""" | 236 |profile| is the profile to use.""" |
| 200 # Do this in a temp dir so we don't collide with user files. | 237 # Do this in a temp dir so we don't collide with user files. |
| 201 cwd = os.getcwd() | 238 cwd = os.getcwd() |
| (...skipping 10 matching lines...) Expand all Loading... | |
| 212 size = min(totalsize, size) | 249 size = min(totalsize, size) |
| 213 progress = "Received %d of %d bytes, %.2f%%" % ( | 250 progress = "Received %d of %d bytes, %.2f%%" % ( |
| 214 size, totalsize, 100.0 * size / totalsize) | 251 size, totalsize, 100.0 * size / totalsize) |
| 215 # Send a \r to let all progress messages use just one line of output. | 252 # Send a \r to let all progress messages use just one line of output. |
| 216 sys.stdout.write("\r" + progress) | 253 sys.stdout.write("\r" + progress) |
| 217 sys.stdout.flush() | 254 sys.stdout.flush() |
| 218 try: | 255 try: |
| 219 print 'Fetching ' + download_url | 256 print 'Fetching ' + download_url |
| 220 urllib.urlretrieve(download_url, context.archive_name, _ReportHook) | 257 urllib.urlretrieve(download_url, context.archive_name, _ReportHook) |
| 221 print | 258 print |
| 259 # Throw an exception if the download was less than 1000 bytes. | |
| 260 if os.path.getsize(context.archive_name) < 1000: raise Exception() | |
| 222 except Exception, e: | 261 except Exception, e: |
| 223 print('Could not retrieve the download. Sorry.') | 262 print('Could not retrieve the download. Sorry.') |
| 224 sys.exit(-1) | 263 sys.exit(-1) |
| 225 | 264 |
| 226 # Unzip the file. | 265 # Unzip the file. |
| 227 print 'Unzipping ...' | 266 print 'Unzipping ...' |
| 228 UnzipFilenameToDir(context.archive_name, os.curdir) | 267 UnzipFilenameToDir(context.archive_name, os.curdir) |
| 229 | 268 |
| 230 # Tell the system to open the app. | 269 # Tell the system to open the app. |
| 231 args = ['--user-data-dir=%s' % profile] + args | 270 args = ['--user-data-dir=%s' % profile] + args |
| (...skipping 12 matching lines...) Expand all Loading... | |
| 244 | 283 |
| 245 def AskIsGoodBuild(rev): | 284 def AskIsGoodBuild(rev): |
| 246 """Ask the user whether build |rev| is good or bad.""" | 285 """Ask the user whether build |rev| is good or bad.""" |
| 247 # Loop until we get a response that we can parse. | 286 # Loop until we get a response that we can parse. |
| 248 while True: | 287 while True: |
| 249 response = raw_input('\nBuild %d is [(g)ood/(b)ad]: ' % int(rev)) | 288 response = raw_input('\nBuild %d is [(g)ood/(b)ad]: ' % int(rev)) |
| 250 if response and response in ('g', 'b'): | 289 if response and response in ('g', 'b'): |
| 251 return response == 'g' | 290 return response == 'g' |
| 252 | 291 |
| 253 | 292 |
| 254 def Bisect(good, | 293 def Bisect(revlist, |
| 255 bad, | |
| 256 revlist, | |
| 257 context, | 294 context, |
| 258 try_args=(), | 295 try_args=(), |
| 259 profile='profile', | 296 profile='profile', |
| 260 predicate=AskIsGoodBuild): | 297 predicate=AskIsGoodBuild): |
| 261 """Tries to find the exact commit where a regression was introduced by | 298 """Tries to find the exact commit where a regression was introduced by |
| 262 running a binary search on all archived builds in a given revision range. | 299 running a binary search on all archived builds in a given revision range. |
| 263 | 300 |
| 264 @param good The index in revlist of the last known good revision. | |
| 265 @param bad The index in revlist of the first known bad revision. | |
| 266 @param revlist A list of chromium revision numbers to check. | 301 @param revlist A list of chromium revision numbers to check. |
| 267 @param context A PathContext object. | 302 @param context A PathContext object. |
| 268 @param try_args A tuple of arguments to pass to the predicate function. | 303 @param try_args A tuple of arguments to pass to the predicate function. |
| 269 @param profile The user profile with which to run chromium. | 304 @param profile The user profile with which to run chromium. |
| 270 @param predicate A predicate function which returns True iff the argument | 305 @param predicate A predicate function which returns True iff the argument |
| 271 chromium revision is good. | 306 chromium revision is good. |
| 272 """ | 307 """ |
| 273 | 308 |
| 309 good = 0 | |
| 310 bad = len(revlist) - 1 | |
|
jbates
2011/07/20 00:47:17
Cleanup
| |
| 274 last_known_good_rev = revlist[good] | 311 last_known_good_rev = revlist[good] |
| 275 first_known_bad_rev = revlist[bad] | 312 first_known_bad_rev = revlist[bad] |
| 276 | 313 |
| 277 # Binary search time! | 314 # Binary search time! |
| 278 while good < bad: | 315 while good < bad: |
| 279 candidates = revlist[good:bad] | 316 candidates = revlist[good:bad] |
| 280 num_poss = len(candidates) | 317 num_poss = len(candidates) |
| 281 if num_poss > 10: | 318 if num_poss > 10: |
| 282 print('%d candidates. %d tries left.' % | 319 print('%d candidates. %d tries left.' % |
| 283 (num_poss, round(math.log(num_poss, 2)))) | 320 (num_poss, round(math.log(num_poss, 2)))) |
| 284 else: | 321 else: |
| 285 print('Candidates: %s' % revlist[good:bad]) | 322 print('Candidates: %s' % revlist[good:bad]) |
| 286 | 323 |
| 287 # Cut the problem in half... | 324 # Cut the problem in half... |
| 288 test = int((bad - good) / 2) + good | 325 test = int((bad - good) / 2) + good |
| 289 test_rev = revlist[test] | 326 test_rev = revlist[test] |
| 290 | 327 |
| 291 # Let the user give this rev a spin (in her own profile, if she wants). | 328 # Let the user give this rev a spin (in her own profile, if she wants). |
| 292 TryRevision(context, test_rev, profile, try_args) | 329 TryRevision(context, test_rev, profile, try_args) |
| 293 if predicate(test_rev): | 330 if predicate(test_rev): |
| 294 last_known_good_rev = revlist[good] | 331 last_known_good_rev = test_rev |
|
jbates
2011/07/20 00:47:17
This fixes a bug that sometimes caused the reporte
Robert Sesek
2011/07/20 19:05:11
Looks like this fixes http://code.google.com/p/chr
jbates
2011/07/20 21:08:42
Done.
| |
| 295 good = test + 1 | 332 good = test + 1 |
| 296 else: | 333 else: |
| 297 bad = test | 334 bad = test |
| 298 | 335 |
| 299 return (last_known_good_rev, first_known_bad_rev) | 336 return (last_known_good_rev, first_known_bad_rev) |
| 300 | 337 |
| 301 | 338 |
| 302 def main(): | 339 def main(): |
| 303 usage = ('%prog [options] [-- chromium-options]\n' | 340 usage = ('%prog [options] [-- chromium-options]\n' |
| 304 'Perform binary search on the snapshot builds.\n' | 341 'Perform binary search on the snapshot builds.\n' |
| 305 '\n' | 342 '\n' |
| 306 'Tip: add "-- --no-first-run" to bypass the first run prompts.') | 343 'Tip: add "-- --no-first-run" to bypass the first run prompts.') |
| 307 parser = optparse.OptionParser(usage=usage) | 344 parser = optparse.OptionParser(usage=usage) |
| 308 # Strangely, the default help output doesn't include the choice list. | 345 # Strangely, the default help output doesn't include the choice list. |
| 309 choices = ['mac', 'win', 'linux', 'linux64'] | 346 choices = ['mac', 'win', 'linux', 'linux64'] |
| 310 # linux-chromiumos lacks a continuous archive http://crbug.com/78158 | 347 # linux-chromiumos lacks a continuous archive http://crbug.com/78158 |
| 311 parser.add_option('-a', '--archive', | 348 parser.add_option('-a', '--archive', |
| 312 choices = choices, | 349 choices = choices, |
| 313 help = 'The buildbot archive to bisect [%s].' % | 350 help = 'The buildbot archive to bisect [%s].' % |
| 314 '|'.join(choices)) | 351 '|'.join(choices)) |
| 315 parser.add_option('-b', '--bad', type = 'int', | 352 parser.add_option('-b', '--bad', type = 'int', |
| 316 help = 'The bad revision to bisect to.') | 353 help = 'The bad revision to bisect to.') |
| 317 parser.add_option('-g', '--good', type = 'int', | 354 parser.add_option('-g', '--good', type = 'int', |
| 318 help = 'The last known good revision to bisect from.') | 355 help = 'The last known good revision to bisect from.') |
| 319 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str', | 356 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str', |
| 320 help = 'Profile to use; this will not reset every run. ' + | 357 help = 'Profile to use; this will not reset every run. ' + |
| 321 'Defaults to a clean profile.', default = 'profile') | 358 'Defaults to a clean profile.', default = 'profile') |
| 359 parser.add_option('-r', '--recent', | |
| 360 dest = "recent", | |
| 361 default = False, | |
| 362 action = "store_true", | |
| 363 help = 'Use recent builds from about the last 2 months ' + | |
| 364 'for higher granularity bisecting.') | |
| 365 parser.add_option('-h', '--help', | |
|
Robert Sesek
2011/07/20 19:05:11
I think this is done for you automatically by the
jbates
2011/07/20 21:08:42
Done.
| |
| 366 dest = "help", | |
| 367 default = False, | |
| 368 action = "store_true", | |
| 369 help = 'Show usage.') | |
| 322 (opts, args) = parser.parse_args() | 370 (opts, args) = parser.parse_args() |
| 323 | 371 |
| 372 if opts.help: | |
| 373 parser.print_help() | |
| 374 return 0 | |
| 375 | |
| 324 if opts.archive is None: | 376 if opts.archive is None: |
| 325 print 'Error: missing required parameter: --archive' | 377 print 'Error: missing required parameter: --archive' |
| 326 print | 378 print |
| 327 parser.print_help() | 379 parser.print_help() |
| 328 return 1 | 380 return 1 |
| 329 | 381 |
| 330 if opts.bad and opts.good and (opts.good > opts.bad): | 382 if opts.bad and opts.good and (opts.good > opts.bad): |
| 331 print ('The good revision (%d) must precede the bad revision (%d).\n' % | 383 print ('The good revision (%d) must precede the bad revision (%d).\n' % |
| 332 (opts.good, opts.bad)) | 384 (opts.good, opts.bad)) |
| 333 parser.print_help() | 385 parser.print_help() |
| 334 return 1 | 386 return 1 |
| 335 | 387 |
| 336 # Create the context. Initialize 0 for the revisions as they are set below. | 388 # Create the context. Initialize 0 for the revisions as they are set below. |
| 337 context = PathContext(opts.archive, 0, 0) | 389 context = PathContext(opts.archive, 0, 0, False) |
| 338 | 390 |
| 339 # Pick a starting point, try to get HEAD for this. | 391 # Pick a starting point, try to get HEAD for this. |
| 340 if opts.bad: | 392 if opts.bad: |
| 341 bad_rev = opts.bad | 393 bad_rev = opts.bad |
| 342 else: | 394 else: |
| 343 bad_rev = 0 | 395 bad_rev = 0 |
| 344 try: | 396 try: |
| 345 # Location of the latest build revision number | 397 # Location of the latest build revision number |
| 346 nh = urllib.urlopen(context.GetLastChangeURL()) | 398 nh = urllib.urlopen(context.GetLastChangeURL()) |
| 347 latest = int(nh.read()) | 399 latest = int(nh.read()) |
| (...skipping 12 matching lines...) Expand all Loading... | |
| 360 else: | 412 else: |
| 361 good_rev = 0 | 413 good_rev = 0 |
| 362 try: | 414 try: |
| 363 good_rev = int(raw_input('Last known good [0]: ')) | 415 good_rev = int(raw_input('Last known good [0]: ')) |
| 364 except Exception, e: | 416 except Exception, e: |
| 365 pass | 417 pass |
| 366 | 418 |
| 367 # Set the input parameters now that they've been validated. | 419 # Set the input parameters now that they've been validated. |
| 368 context.good_revision = good_rev | 420 context.good_revision = good_rev |
| 369 context.bad_revision = bad_rev | 421 context.bad_revision = bad_rev |
| 422 context.is_recent = opts.recent | |
|
Robert Sesek
2011/07/20 19:05:11
You're not validating this, so why not pass it dir
jbates
2011/07/20 21:08:42
Done.
| |
| 370 | 423 |
| 371 # Get a list of revisions to bisect across. | 424 # Get a list of revisions to bisect across. |
| 372 revlist = GetRevList(context) | 425 revlist = GetRevList(context) |
| 373 if len(revlist) < 2: # Don't have enough builds to bisect | 426 if len(revlist) < 2: # Don't have enough builds to bisect |
| 374 print 'We don\'t have enough builds to bisect. revlist: %s' % revlist | 427 print 'We don\'t have enough builds to bisect. revlist: %s' % revlist |
| 375 sys.exit(1) | 428 sys.exit(1) |
| 376 | 429 |
| 377 # If we don't have a |good_rev|, set it to be the first revision possible. | |
| 378 if good_rev == 0: | |
| 379 good_rev = revlist[0] | |
|
jbates
2011/07/20 00:47:17
good_rev is not used anymore, so this was unnecess
| |
| 380 | |
| 381 # These are indexes of |revlist|. | |
| 382 good = 0 | |
| 383 bad = len(revlist) - 1 | |
|
jbates
2011/07/20 00:47:17
Moved to inside Bisect -- it already is passed the
| |
| 384 | |
| 385 (last_known_good_rev, first_known_bad_rev) = Bisect( | 430 (last_known_good_rev, first_known_bad_rev) = Bisect( |
| 386 good, bad, revlist, context, args, opts.profile) | 431 revlist, context, args, opts.profile) |
| 387 | 432 |
| 388 # We're done. Let the user know the results in an official manner. | 433 # We're done. Let the user know the results in an official manner. |
| 389 print('You are probably looking for build %d.' % first_known_bad_rev) | 434 print('You are probably looking for build %d.' % first_known_bad_rev) |
| 390 print('CHANGELOG URL:') | 435 print('CHANGELOG URL:') |
| 391 print(CHANGELOG_URL % (last_known_good_rev, first_known_bad_rev)) | 436 print(CHANGELOG_URL % (last_known_good_rev, first_known_bad_rev)) |
| 392 print('Built at revision:') | 437 print('Built at revision:') |
| 393 print(BUILD_VIEWVC_URL % first_known_bad_rev) | 438 print(BUILD_VIEWVC_URL % first_known_bad_rev) |
| 394 | 439 |
| 395 if __name__ == '__main__': | 440 if __name__ == '__main__': |
| 396 sys.exit(main()) | 441 sys.exit(main()) |
| OLD | NEW |