Changeset 277102 in webkit
- Timestamp:
- May 6, 2021 11:32:40 AM (3 years ago)
- Location:
- trunk/Tools
- Files:
-
- 16 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/Tools/ChangeLog
r277101 r277102 1 2021-05-06 Jonathan Bedard <jbedard@apple.com> 2 3 [webkitcorepy] Add API to efficiently create a sequence of commits 4 https://bugs.webkit.org/show_bug.cgi?id=224890 5 <rdar://problem/76975733> 6 7 Rubber-stamped by Aakash Jain. 8 9 While it is possible to simple iterate through a range of commits to define them, 10 every API we use to define commits has much more efficient techniques. 11 12 * Scripts/libraries/webkitscmpy/setup.py: Bump version. 13 * Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py: Ditto. 14 * Scripts/libraries/webkitscmpy/webkitscmpy/contributor.py: 15 (Contributor): Add revision to SVN_AUTHOR_RE and add regex without lines. 16 (Contributor.from_scm_log): Strip leading whitespace from author. 17 * Scripts/libraries/webkitscmpy/webkitscmpy/local/git.py: 18 (Git._args_from_content): 19 (Git.commits): Use `git log` to efficiently compute a range of commits. 20 * Scripts/libraries/webkitscmpy/webkitscmpy/local/svn.py: 21 (Svn._args_from_content): 22 (Svn.commits): Use `svn log` to efficiently compute a range of commits. 23 * Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py: 24 (Git.__init__): Add `git log` mock. 25 * Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/svn.py: 26 (Svn.__init__): Add `svn log` mock and more explicit `svn info` mock. 27 (Svn._log_range): 28 * Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/git_hub.py: 29 (GitHub._commits_response): Return all parent commits to provided ref. 30 (GitHub.request): 31 * Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/svn.py: 32 (Svn.range): More efficiently compute the range. 33 (Svn.request): 34 * Scripts/libraries/webkitscmpy/webkitscmpy/remote/git_hub.py: 35 (GitHub.request): Allow caller to disable pagination. 36 (GitHub.commit): Reduce number of requests required to compute order. 37 (GitHub.commits): Using the `commits` endpoint, more efficiently 38 compute a range of commits. 39 * Scripts/libraries/webkitscmpy/webkitscmpy/remote/svn.py: 40 (Svn): Generalize HISTORY_RE to match any single-line SVN XML response. 41 (Svn._cache_revisions): Replace HISTORY_RE with DATA_RE. 42 (Svn.commits): Use svn/rvr to efficiently compute a range of commits. 43 * Scripts/libraries/webkitscmpy/webkitscmpy/scm_base.py: 44 (ScmBase._commit_range): Return a pair of commits representing the range 45 the caller is requesting, and preform some basic sanity checks. 46 (ScmBase.commits): Declare function implemented by decedents. 47 * Scripts/libraries/webkitscmpy/webkitscmpy/test/find_unittest.py: 48 * Scripts/libraries/webkitscmpy/webkitscmpy/test/git_unittest.py: 49 (TestGit.test_commits): 50 (TestGit.test_commits_branch): 51 (TestGitHub.test_commits): 52 (TestGitHub.test_commits_branch): 53 * Scripts/libraries/webkitscmpy/webkitscmpy/test/svn_unittest.py: 54 (TestLocalSvn.test_commits): 55 (TestLocalSvn.test_commits_branch): 56 (TestRemoteSvn.test_commits): 57 (TestRemoteSvn.test_commits_branch): 58 1 59 2021-05-06 Chris Dumez <cdumez@apple.com> 2 60 -
trunk/Tools/Scripts/libraries/webkitscmpy/setup.py
r275635 r277102 30 30 setup( 31 31 name='webkitscmpy', 32 version='0.1 3.9',32 version='0.14.0', 33 33 description='Library designed to interact with git and svn repositories.', 34 34 long_description=readme(), -
trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py
r275635 r277102 47 47 ) 48 48 49 version = Version(0, 1 3, 9)49 version = Version(0, 14, 0) 50 50 51 51 AutoInstall.register(Package('fasteners', Version(0, 15, 0))) -
trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/contributor.py
r275352 r277102 33 33 UNKNOWN_AUTHOR = re.compile(r'Author: (?P<author>.*) <None>') 34 34 EMPTY_AUTHOR = re.compile(r'Author: (?P<author>.*) <>') 35 SVN_AUTHOR_RE = re.compile(r'r\d+ \| (?P<email>.*) \| (?P<date>.*) \| \d+ lines?') 35 SVN_AUTHOR_RE = re.compile(r'r(?P<revision>\d+) \| (?P<email>.*) \| (?P<date>.*) \| \d+ lines?') 36 SVN_AUTHOR_Q_RE = re.compile(r'r(?P<revision>\d+) \| (?P<email>.*) \| (?P<date>.*)') 36 37 SVN_PATCH_FROM_RE = re.compile(r'Patch by (?P<author>.*) <(?P<email>.*)> on \d+-\d+-\d+') 37 38 … … 116 117 author = None 117 118 118 for expression in [cls.GIT_AUTHOR_RE, cls.SVN_AUTHOR_RE, cls.SVN_PATCH_FROM_RE, cls.AUTOMATED_CHECKIN_RE, cls.UNKNOWN_AUTHOR, cls.EMPTY_AUTHOR]: 119 for expression in [ 120 cls.GIT_AUTHOR_RE, 121 cls.SVN_AUTHOR_RE, 122 cls.SVN_PATCH_FROM_RE, 123 cls.AUTOMATED_CHECKIN_RE, 124 cls.UNKNOWN_AUTHOR, 125 cls.EMPTY_AUTHOR, 126 cls.SVN_AUTHOR_Q_RE, 127 ]: 119 128 match = expression.match(line) 120 129 if match: 121 130 if 'author' in expression.groupindex: 122 author = match.group('author') 131 author = match.group('author').lstrip() 123 132 if '(no author)' in author or 'Automated Checkin' in author or 'Unknown' in author: 124 133 author = None -
trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/local/git.py
r276856 r277102 21 21 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 22 22 23 import calendar 23 24 import logging 24 25 import os 25 26 import re 26 27 import six 27 28 from webkitcorepy import run, decorators, TimeoutExpired 28 import subprocess 29 import sys 30 import time 31 32 from datetime import datetime, timedelta 33 34 from webkitcorepy import run, decorators 29 35 from webkitscmpy.local import Scm 30 36 from webkitscmpy import Commit, Contributor, log … … 301 307 ) 302 308 309 def _args_from_content(self, content, include_log=True): 310 author = None 311 timestamp = None 312 313 for line in content.splitlines()[:4]: 314 split = line.split(': ') 315 if split[0] == 'Author': 316 author = Contributor.from_scm_log(line.lstrip(), self.contributors) 317 elif split[0] == 'CommitDate': 318 tz_diff = line.split(' ')[-1] 319 date = datetime.strptime(split[1].lstrip()[:-len(tz_diff)], '%a %b %d %H:%M:%S %Y ') 320 date += timedelta( 321 hours=int(tz_diff[1:3]), 322 minutes=int(tz_diff[3:5]), 323 ) * (1 if tz_diff[0] == '-' else -1) 324 timestamp = int(calendar.timegm(date.timetuple())) - time.timezone 325 326 message = '' 327 for line in content.splitlines()[5:]: 328 message += line[4:] + '\n' 329 matches = self.GIT_SVN_REVISION.findall(message) 330 331 return dict( 332 revision=int(matches[-1].split('@')[0]) if matches else None, 333 author=author, 334 timestamp=timestamp, 335 message=message.rstrip() if include_log else None, 336 ) 337 338 def commits(self, begin=None, end=None, include_log=True, include_identifier=True): 339 begin, end = self._commit_range(begin=begin, end=end, include_identifier=include_identifier) 340 341 try: 342 log = None 343 log = subprocess.Popen( 344 [self.executable(), 'log', '--format=fuller', '{}...{}'.format(end.hash, begin.hash)], 345 cwd=self.root_path, 346 stdout=subprocess.PIPE, 347 stderr=subprocess.PIPE, 348 **(dict(encoding='utf-8') if sys.version_info > (3, 0) else dict()) 349 ) 350 if log.poll(): 351 raise self.Exception("Failed to construct history for '{}'".format(end.branch)) 352 353 line = log.stdout.readline() 354 previous = [end] 355 while line: 356 if not line.startswith('commit '): 357 raise OSError('Failed to parse `git log` format') 358 branch_point = previous[0].branch_point 359 identifier = previous[0].identifier 360 hash = line.split(' ')[-1].rstrip() 361 if hash != previous[0].hash: 362 identifier -= 1 363 364 if not identifier: 365 identifier = branch_point 366 branch_point = None 367 368 content = '' 369 line = log.stdout.readline() 370 while line and not line.startswith('commit '): 371 content += line 372 line = log.stdout.readline() 373 374 commit = Commit( 375 repository_id=self.id, 376 hash=hash, 377 branch=end.branch if identifier and branch_point else self.default_branch, 378 identifier=identifier if include_identifier else None, 379 branch_point=branch_point if include_identifier else None, 380 order=0, 381 **self._args_from_content(content, include_log=include_log) 382 ) 383 384 # Ensure that we don't duplicate the first and last commits 385 if commit.hash == previous[0].hash: 386 previous[0] = commit 387 388 # If we share a timestamp with the previous commit, that means that this commit has an order 389 # less than the set of commits cached in previous 390 elif commit.timestamp == previous[0].timestamp: 391 for cached in previous: 392 cached.order += 1 393 previous.append(commit) 394 395 # If we don't share a timestamp with the previous set of commits, we should return all commits 396 # cached in previous. 397 else: 398 for cached in previous: 399 yield cached 400 previous = [commit] 401 402 for cached in previous: 403 cached.order += begin.order 404 yield cached 405 finally: 406 if log: 407 log.kill() 408 303 409 def find(self, argument, include_log=True, include_identifier=True): 304 410 if not isinstance(argument, six.string_types): -
trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/local/svn.py
r276856 r277102 402 402 ) 403 403 404 def _args_from_content(self, content, include_log=True): 405 leading = content.splitlines()[0] 406 match = Contributor.SVN_AUTHOR_RE.match(leading) or Contributor.SVN_AUTHOR_Q_RE.match(leading) 407 if not match: 408 return {} 409 410 tz_diff = match.group('date').split(' ', 2)[-1] 411 date = datetime.strptime(match.group('date')[:-len(tz_diff)], '%Y-%m-%d %H:%M:%S ') 412 date += timedelta( 413 hours=int(tz_diff[1:3]), 414 minutes=int(tz_diff[3:5]), 415 ) * (1 if tz_diff[0] == '-' else -1) 416 417 return dict( 418 revision=int(match.group('revision')), 419 timestamp=int(calendar.timegm(date.timetuple())), 420 author=Contributor.from_scm_log(leading, self.contributors), 421 message='\n'.join(content.splitlines()[2:]).rstrip() if include_log else None, 422 ) 423 424 425 def commits(self, begin=None, end=None, include_log=True, include_identifier=True): 426 begin, end = self._commit_range(begin=begin, end=end, include_identifier=include_identifier) 427 previous = end 428 if end.branch == self.default_branch or '/' in end.branch: 429 branch_arg = '^/{}'.format(end.branch) 430 else: 431 branch_arg = '^/branches/{}'.format(end.branch) 432 433 try: 434 log = None 435 log = subprocess.Popen( 436 [self.executable(), 'log', '-r', '{}:{}'.format( 437 end.revision, begin.revision, 438 ), branch_arg] + ([] if include_log else ['-q']), 439 cwd=self.root_path, 440 stdout=subprocess.PIPE, 441 stderr=subprocess.PIPE, 442 **(dict(encoding='utf-8') if sys.version_info > (3, 0) else dict()) 443 ) 444 if log.poll(): 445 raise self.Exception('Failed to find commits between {} and {} on {}'.format(begin, end, branch_arg)) 446 447 content = '' 448 line = log.stdout.readline() 449 divider = '-' * 72 450 while True: 451 if line and line.rstrip() != divider: 452 content += line 453 line = log.stdout.readline() 454 continue 455 456 if not content: 457 line = log.stdout.readline() 458 continue 459 460 branch_point = previous.branch_point if include_identifier else None 461 identifier = previous.identifier if include_identifier else None 462 463 args = self._args_from_content(content, include_log=include_log) 464 if args['revision'] != previous.revision: 465 yield previous 466 identifier -= 1 467 if not identifier: 468 identifier = branch_point 469 branch_point = None 470 471 previous = Commit( 472 repository_id=self.id, 473 branch=end.branch if branch_point else self.default_branch, 474 identifier=identifier, 475 branch_point=branch_point, 476 **args 477 ) 478 content = '' 479 if not line: 480 break 481 line = log.stdout.readline() 482 483 yield previous 484 485 finally: 486 if log: 487 log.kill() 488 404 489 def checkout(self, argument): 405 490 commit = self.find(argument) -
trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py
r275353 r277102 24 24 import os 25 25 import re 26 import time 26 27 27 28 from datetime import datetime … … 229 230 author=self.find(args[2]).author.name, 230 231 email=self.find(args[2]).author.email, 231 date=datetime. fromtimestamp(self.find(args[2]).timestamp).strftime('%a %b %d %H:%M:%S %Y'),232 date=datetime.utcfromtimestamp(self.find(args[2]).timestamp + time.timezone).strftime('%a %b %d %H:%M:%S %Y +0000'), 232 233 log='\n'.join([ 233 234 (' ' + line) if line else '' for line in self.find(args[2]).message.splitlines() … … 240 241 ), 241 242 ) if self.find(args[2]) else mocks.ProcessCompletion(returncode=128), 243 ), mocks.Subprocess.Route( 244 self.executable, 'log', '--format=fuller', re.compile(r'.+\.\.\..+'), 245 cwd=self.path, 246 generator=lambda *args, **kwargs: mocks.ProcessCompletion( 247 returncode=0, 248 stdout='\n'.join([ 249 'commit {hash}\n' 250 'Author: {author} <{email}>\n' 251 'AuthorDate: {date}\n' 252 'Commit: {author} <{email}>\n' 253 'CommitDate: {date}\n' 254 '\n{log}'.format( 255 hash=commit.hash, 256 author=commit.author.name, 257 email=commit.author.email, 258 date=datetime.utcfromtimestamp(commit.timestamp + time.timezone).strftime('%a %b %d %H:%M:%S %Y +0000'), 259 log='\n'.join([ 260 (' ' + line) if line else '' for line in commit.message.splitlines() 261 ] + ([ 262 ' git-svn-id: https://svn.{}/repository/{}/trunk@{} 268f45cc-cd09-0410-ab3c-d52691b4dbfc'.format( 263 self.remote.split('@')[-1].split(':')[0], 264 os.path.basename(path), 265 commit.revision, 266 )] if git_svn else []), 267 )) for commit in self.commits_in_range(args[3].split('...')[-1], args[3].split('...')[0]) 268 ]) 269 ) 242 270 ), mocks.Subprocess.Route( 243 271 self.executable, 'rev-list', '--count', '--no-merges', re.compile(r'.+'), … … 464 492 stdout=stdout.getvalue(), 465 493 ) 494 495 def commits_in_range(self, begin, end): 496 branches = [self.default_branch] 497 for branch, commits in self.commits.items(): 498 if branch == self.default_branch: 499 continue 500 for commit in commits: 501 if commit.hash == end: 502 branches.insert(0, branch) 503 break 504 if len(branches) > 1: 505 break 506 507 in_range = False 508 previous = None 509 for branch in branches: 510 for commit in reversed(self.commits[branch]): 511 if commit.hash == end: 512 in_range = True 513 if in_range and (not previous or commit.hash != previous.hash): 514 yield commit 515 previous = commit 516 if commit.hash == begin: 517 in_range = False 518 in_range = False 519 if not previous or branch == self.default_branch: 520 continue 521 522 for commit in reversed(self.commits[self.default_branch]): 523 if previous.branch_point == commit.identifier: 524 end = commit.hash -
trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/svn.py
r272172 r277102 66 66 super(Svn, self).__init__( 67 67 mocks.Subprocess.Route( 68 self.executable, 'info', self.BRANCH_RE, 69 cwd=self.path, 70 generator=lambda *args, **kwargs: self._info(branch=self.BRANCH_RE.match(args[2]).group('branch'), cwd=kwargs.get('cwd', '')) 71 ), mocks.Subprocess.Route( 72 self.executable, 'info', '-r', re.compile(r'\d+'), 73 cwd=self.path, 74 generator=lambda *args, **kwargs: self._info(revision=int(args[3]), cwd=kwargs.get('cwd', '')) 75 ), mocks.Subprocess.Route( 68 76 self.executable, 'info', 69 77 cwd=self.path, 70 78 generator=lambda *args, **kwargs: self._info(cwd=kwargs.get('cwd', '')) 71 ), mocks.Subprocess.Route(72 self.executable, 'info', self.BRANCH_RE,73 cwd=self.path,74 generator=lambda *args, **kwargs: self._info(branch=self.BRANCH_RE.match(args[2]).group('branch'), cwd=kwargs.get('cwd', ''))75 79 ), mocks.Subprocess.Route( 76 80 self.executable, 'list', '^/branches', … … 118 122 branch=self.BRANCH_RE.match(args[6]).group('branch'), 119 123 revision=args[5], 124 ) if self.connected else mocks.ProcessCompletion(returncode=1) 125 ), mocks.Subprocess.Route( 126 self.executable, 'log', '-r', re.compile(r'\d+:\d+'), self.BRANCH_RE, 127 cwd=self.path, 128 generator=lambda *args, **kwargs: self._log_range( 129 branch=self.BRANCH_RE.match(args[4]).group('branch'), 130 end=int(args[3].split(':')[0]), 131 begin=int(args[3].split(':')[-1]), 120 132 ) if self.connected else mocks.ProcessCompletion(returncode=1) 121 133 ), mocks.Subprocess.Route( … … 205 217 ) 206 218 219 def _log_range(self, branch=None, end=None, begin=None): 220 if end < begin: 221 return mocks.ProcessCompletion(returncode=1) 222 223 output = '' 224 previous = None 225 for b in [branch, 'trunk']: 226 for candidate in reversed(self.commits.get(b, [])): 227 if candidate.revision > end or candidate.revision < begin: 228 continue 229 if previous and previous.revision <= candidate.revision: 230 continue 231 previous = candidate 232 output += ('------------------------------------------------------------------------\n' 233 '{line} | {lines} lines\n\n' 234 '{log}\n').format( 235 line=self.log_line(candidate), 236 lines=len(candidate.message.splitlines()), 237 log=candidate.message 238 ) 239 return mocks.ProcessCompletion(returncode=0, stdout=output) 240 207 241 def find(self, branch=None, revision=None): 208 242 if not branch and not revision: -
trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/git_hub.py
r275195 r277102 133 133 ], url=url) 134 134 135 def _commits_response(self, url, ref): 136 from datetime import datetime, timedelta 137 138 base = self.commit(ref) 139 if not base: 140 return mocks.Response( 141 status_code=404, 142 url=url, 143 text=json.dumps(dict(message='No commit found for SHA: {}'.format(ref))), 144 ) 145 146 response = [] 147 for branch in [self.default_branch] if base.branch == self.default_branch else [base.branch, self.default_branch]: 148 in_range = False 149 previous = None 150 for commit in reversed(self.commits[branch]): 151 if commit.hash == ref: 152 in_range = True 153 if not in_range: 154 continue 155 previous = commit 156 response.append({ 157 'sha': commit.hash, 158 'commit': { 159 'author': { 160 'name': commit.author.name, 161 'email': commit.author.email, 162 'date': datetime.utcfromtimestamp(commit.timestamp - timedelta(hours=7).seconds).strftime('%Y-%m-%dT%H:%M:%SZ'), 163 }, 'committer': { 164 'name': commit.author.name, 165 'email': commit.author.email, 166 'date': datetime.utcfromtimestamp(commit.timestamp - timedelta(hours=7).seconds).strftime('%Y-%m-%dT%H:%M:%SZ'), 167 }, 'message': commit.message + ('\ngit-svn-id: https://svn.example.org/repository/webkit/{}@{} 268f45cc-cd09-0410-ab3c-d52691b4dbfc\n'.format( 168 'trunk' if commit.branch == self.default_branch else commit.branch, commit.revision, 169 ) if commit.revision else ''), 170 'url': 'https://{}/git/commits/{}'.format(self.api_remote, commit.hash), 171 }, 'url': 'https://{}/commits/{}'.format(self.api_remote, commit.hash), 172 'html_url': 'https://{}/commit/{}'.format(self.remote, commit.hash), 173 }) 174 if branch != self.default_branch: 175 for commit in reversed(self.commits[self.default_branch]): 176 if previous.branch_point == commit.identifier: 177 ref = commit.hash 178 179 return mocks.Response.fromJson(response, url=url) 180 135 181 def _commit_response(self, url, ref): 136 182 from datetime import datetime, timedelta … … 154 200 'email': commit.author.email, 155 201 'date': datetime.utcfromtimestamp(commit.timestamp - timedelta(hours=7).seconds).strftime('%Y-%m-%dT%H:%M:%SZ'), 156 }, 'message': commit.message + '\ngit-svn-id: https://svn.example.org/repository/webkit/{}@{} 268f45cc-cd09-0410-ab3c-d52691b4dbfc\n'.format(202 }, 'message': commit.message + ('\ngit-svn-id: https://svn.example.org/repository/webkit/{}@{} 268f45cc-cd09-0410-ab3c-d52691b4dbfc\n'.format( 157 203 'trunk' if commit.branch == self.default_branch else commit.branch, commit.revision, 158 ) if commit.revision else '' ,204 ) if commit.revision else ''), 159 205 'url': 'https://{}/git/commits/{}'.format(self.api_remote, commit.hash), 160 206 }, 'url': 'https://{}/commits/{}'.format(self.api_remote, commit.hash), … … 234 280 ) 235 281 236 def request(self, method, url, data=None, **kwargs):282 def request(self, method, url, data=None, params=None, **kwargs): 237 283 if not url.startswith('http://') and not url.startswith('https://'): 238 284 return mocks.Response.create404(url) 239 285 286 params = params or {} 240 287 stripped_url = url.split('://')[-1] 241 288 … … 247 294 if stripped_url in ['{}/branches'.format(self.api_remote), '{}/tags'.format(self.api_remote)]: 248 295 return self._list_refs_response(url=url, type=stripped_url.split('/')[-1]) 296 297 # Return a commit and it's parents 298 if stripped_url == '{}/commits'.format(self.api_remote) and params.get('sha'): 299 return self._commits_response(url=url, ref=params['sha']) 249 300 250 301 # Extract single commit -
trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/svn.py
r274366 r277102 107 107 if category and category.startswith('branches/'): 108 108 category = category.split('/')[-1] 109 110 if not category: 111 for commits in self.commits.values(): 112 for commit in commits: 113 if commit.revision == start: 114 category = commit.branch 115 break 116 117 if not category: 118 return [] 119 120 result = [commit for commit in reversed(self.commits[category])] 121 if self.commits[category][0].branch_point: 122 result += [commit for commit in reversed(self.commits['trunk'][:self.commits[category][0].branch_point])] 123 124 for index in reversed(range(len(result))): 125 if result[index].revision < end: 126 result = result[:index] 127 continue 128 if result[index].revision > start: 129 result = result[index:] 130 break 131 132 return result 109 category = [category] if category else self.branches(start) + self.tags(start) 110 111 previous = None 112 for b in category + ['trunk']: 113 for candidate in reversed(self.commits.get(b, [])): 114 if candidate.revision > start or candidate.revision < end: 115 continue 116 if previous and previous.revision <= candidate.revision: 117 continue 118 previous = candidate 119 yield candidate 133 120 134 121 def request(self, method, url, data=None, **kwargs): … … 267 254 # Log for commit 268 255 if method == 'REPORT' and stripped_url.startswith('{}!'.format(self.remote)) and match and data.get('S:log-report'): 269 commits = self.range(256 commits = list(self.range( 270 257 category=match.group('category'), 271 258 start=int(data['S:log-report']['S:start-revision']), 272 259 end=int(data['S:log-report']['S:end-revision']), 273 ) 260 )) 274 261 275 262 limit = int(data['S:log-report'].get('S:limit', 0)) -
trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/git_hub.py
r276856 r277102 76 76 return True 77 77 78 def request(self, path=None, params=None, headers=None, authenticated=None ):78 def request(self, path=None, params=None, headers=None, authenticated=None, paginate=True): 79 79 headers = {key: value for key, value in headers.items()} if headers else dict() 80 80 headers['Accept'] = headers.get('Accept', 'application/vnd.github.v3+json') … … 106 106 result = response.json() 107 107 108 while isinstance(response.json(), list) and len(response.json()) == params['per_page']:108 while paginate and isinstance(response.json(), list) and len(response.json()) == params['per_page']: 109 109 params['page'] += 1 110 110 response = requests.get(url, params=params, headers=headers, auth=auth) … … 288 288 # zero-indexed "order" within it's timestamp. 289 289 order = 0 290 while not identifier or order + 1 < identifier + (branch_point or 0): 291 response = self.request('commits/{}'.format('{}~{}'.format(commit_data['sha'], order + 1))) 292 if not response: 290 lhash = commit_data['sha'] 291 while lhash: 292 response = self.request('commits', paginate=False, params=dict(sha=lhash, per_page=20)) 293 if len(response) <= 1: 293 294 break 294 parent_timestamp = int(calendar.timegm(datetime.strptime( 295 response['commit']['committer']['date'], '%Y-%m-%dT%H:%M:%SZ', 296 ).timetuple())) 297 if parent_timestamp != timestamp: 298 break 299 order += 1 295 for c in response: 296 if lhash == c['sha']: 297 continue 298 parent_timestamp = int(calendar.timegm(datetime.strptime( 299 c['commit']['committer']['date'], '%Y-%m-%dT%H:%M:%SZ', 300 ).timetuple())) 301 if parent_timestamp != timestamp: 302 lhash = None 303 break 304 lhash = c['sha'] 305 order += 1 300 306 301 307 return Commit( … … 314 320 ) 315 321 322 def commits(self, begin=None, end=None, include_log=True, include_identifier=True): 323 begin, end = self._commit_range(begin=begin, end=end, include_identifier=include_identifier) 324 325 previous = end 326 cached = [previous] 327 while previous: 328 response = self.request('commits', paginate=False, params=dict(sha=previous.hash)) 329 if not response: 330 break 331 for commit_data in response: 332 branch_point = previous.branch_point 333 identifier = previous.identifier 334 if commit_data['sha'] == previous.hash: 335 cached = cached[:-1] 336 else: 337 identifier -= 1 338 339 if not identifier: 340 identifier = branch_point 341 branch_point = None 342 343 matches = self.GIT_SVN_REVISION.findall(commit_data['commit']['message']) 344 revision = int(matches[-1].split('@')[0]) if matches else None 345 346 email_match = self.EMAIL_RE.match(commit_data['commit']['author']['email']) 347 timestamp = int(calendar.timegm(datetime.strptime( 348 commit_data['commit']['committer']['date'], '%Y-%m-%dT%H:%M:%SZ', 349 ).timetuple())) 350 351 previous = Commit( 352 repository_id=self.id, 353 hash=commit_data['sha'], 354 revision=revision, 355 branch=end.branch if identifier and branch_point else self.default_branch, 356 identifier=identifier if include_identifier else None, 357 branch_point=branch_point if include_identifier else None, 358 timestamp=timestamp, 359 author=self.contributors.create( 360 commit_data['commit']['author']['name'], 361 email_match.group('email') if email_match else None, 362 ), order=0, 363 message=commit_data['commit']['message'] if include_log else None, 364 ) 365 if not cached or cached[0].timestamp != previous.timestamp: 366 for c in cached: 367 yield c 368 cached = [previous] 369 else: 370 for c in cached: 371 c.order += 1 372 cached.append(previous) 373 374 if previous.hash == begin.hash or previous.timestamp < begin.timestamp: 375 previous = None 376 break 377 378 for c in cached: 379 c.order += begin.order 380 yield c 381 382 316 383 def find(self, argument, include_log=True, include_identifier=True): 317 384 if not isinstance(argument, six.string_types): -
trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/svn.py
r275635 r277102 33 33 from datetime import datetime 34 34 35 from webkitcorepy import log, run, decorators35 from webkitcorepy import decorators, string_utils 36 36 from webkitscmpy.remote.scm import Scm 37 from webkitscmpy import Commit, Contributor,Version37 from webkitscmpy import Commit, Version 38 38 39 39 40 40 class Svn(Scm): 41 41 URL_RE = re.compile(r'\Ahttps?://svn.(?P<host>\S+)/repository/\S+\Z') 42 HISTORY_RE = re.compile(b'<D:version-name>(?P<revision>\d+)</D:version-name>')42 DATA_RE = re.compile(b'<[SD]:(?P<tag>\S+)>(?P<content>.*)</[SD]:.+>') 43 43 CACHE_VERSION = Version(1) 44 44 … … 245 245 default_count = 0 246 246 for line in response.iter_lines(): 247 match = self. HISTORY_RE.match(line)248 if not match :247 match = self.DATA_RE.match(line) 248 if not match or match.group('tag') != b'version-name': 249 249 continue 250 250 … … 255 255 did_warn = True 256 256 257 revision = int(match.group(' revision'))257 revision = int(match.group('content')) 258 258 if pos > 0 and self._metadata_cache[branch][pos - 1] == revision: 259 259 break … … 456 456 message=message, 457 457 ) 458 459 def _args_from_content(self, content, include_log=True): 460 xml = xmltodict.parse(content) 461 date = datetime.strptime(string_utils.decode(xml['S:log-item']['S:date']).split('.')[0], '%Y-%m-%dT%H:%M:%S') 462 name = string_utils.decode(xml['S:log-item']['D:creator-displayname']) 463 464 return dict( 465 revision=int(xml['S:log-item']['D:version-name']), 466 author=self.contributors.create(name, name) if name and '@' in name else self.contributors.create(name), 467 timestamp=int(calendar.timegm(date.timetuple())), 468 message=string_utils.decode(xml['S:log-item']['D:comment']) if include_log else None, 469 ) 470 471 def commits(self, begin=None, end=None, include_log=True, include_identifier=True): 472 begin, end = self._commit_range(begin=begin, end=end, include_identifier=include_identifier) 473 previous = end 474 475 content = b'' 476 with requests.request( 477 method='REPORT', 478 url='{}!svn/rvr/{}/{}'.format( 479 self.url, 480 end.revision, 481 end.branch if end.branch == self.default_branch or '/' in end.branch else 'branches/{}'.format(end.branch), 482 ), stream=True, 483 headers={ 484 'Content-Type': 'text/xml', 485 'Accept-Encoding': 'gzip', 486 'DEPTH': '1', 487 }, data='<S:log-report xmlns:S="svn:">\n' 488 '<S:start-revision>{end}</S:start-revision>\n' 489 '<S:end-revision>{begin}</S:end-revision>\n' 490 '<S:path></S:path>\n' 491 '</S:log-report>\n'.format(end=end.revision, begin=begin.revision), 492 ) as response: 493 if response.status_code != 200: 494 raise self.Exception("Failed to construct branch history for '{}'".format(branch)) 495 for line in response.iter_lines(): 496 if line == b'<S:log-item>': 497 content = line + b'\n' 498 else: 499 content += line + b'\n' 500 if line != b'</S:log-item>': 501 continue 502 503 args = self._args_from_content(content, include_log=include_log) 504 505 branch_point = previous.branch_point if include_identifier else None 506 identifier = previous.identifier if include_identifier else None 507 if args['revision'] != previous.revision: 508 identifier -= 1 509 if not identifier: 510 identifier = branch_point 511 branch_point = None 512 513 previous = Commit( 514 repository_id=self.id, 515 branch=end.branch if branch_point else self.default_branch, 516 identifier=identifier, 517 branch_point=branch_point, 518 **args 519 ) 520 yield previous 521 content = b'' -
trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/scm_base.py
r275353 r277102 72 72 73 73 def commit(self, hash=None, revision=None, identifier=None, branch=None, tag=None, include_log=True, include_identifier=True): 74 raise NotImplementedError() 75 76 def _commit_range(self, begin=None, end=None, include_log=False, include_identifier=True): 77 begin_args = begin or dict() 78 end_args = end or dict() 79 80 if not begin_args: 81 raise TypeError("_commit_range() missing required 'begin' arguments") 82 if not end_args: 83 raise TypeError("_commit_range() missing required 'end' arguments") 84 85 if list(begin_args.keys()) == ['argument']: 86 begin_result = self.find(include_log=include_log, include_identifier=False, **begin_args) 87 else: 88 begin_result = self.commit(include_log=include_log, include_identifier=False, **begin_args) 89 90 if list(end_args.keys()) == ['argument']: 91 end_result = self.find(include_log=include_log, include_identifier=include_identifier, **end_args) 92 else: 93 end_result = self.commit(include_log=include_log, include_identifier=include_identifier, **end_args) 94 95 if not begin_result: 96 raise TypeError("'{}' failed to define begin in _commit_range()".format(begin_args)) 97 if not end_result: 98 raise TypeError("'{}' failed to define begin in _commit_range()".format(end_args)) 99 if begin_result.timestamp > end_result.timestamp: 100 raise TypeError("'{}' pre-dates '{}' in _commit_range()".format(begin_result, end_result)) 101 if end_result.branch == self.default_branch and begin_result.branch != self.default_branch: 102 raise TypeError("'{}' and '{}' do not share linear history".format(begin_result, end_result)) 103 return begin_result, end_result 104 105 def commits(self, begin=None, end=None, include_log=True, include_identifier=True): 74 106 raise NotImplementedError() 75 107 -
trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/find_unittest.py
r275353 r277102 232 232 Revision: 4 233 233 Identifier: 3@trunk 234 '''.format(datetime.fromtimestamp(160168 6700).strftime('%a %b %d %H:%M:%S %Y')),234 '''.format(datetime.fromtimestamp(1601684700).strftime('%a %b %d %H:%M:%S %Y')), 235 235 ) 236 236 -
trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/git_unittest.py
r274830 r277102 29 29 from webkitcorepy import LoggerCapture, OutputCapture 30 30 from webkitcorepy.mocks import Time as MockTime 31 from webkitscmpy import local, mocks, remote31 from webkitscmpy import Commit, local, mocks, remote 32 32 33 33 … … 284 284 self.assertEqual(1, local.Git(self.path).commit(hash='d8bce26fa65c').order) 285 285 286 def test_commits(self): 287 for mock in [mocks.local.Git(self.path), mocks.local.Git(self.path, git_svn=True)]: 288 with mock: 289 git = local.Git(self.path) 290 self.assertEqual(Commit.Encoder().default([ 291 git.commit(hash='bae5d1e9'), 292 git.commit(hash='1abe25b4'), 293 git.commit(hash='fff83bb2'), 294 git.commit(hash='9b8311f2'), 295 ]), Commit.Encoder().default(list(git.commits(begin=dict(hash='9b8311f2'), end=dict(hash='bae5d1e9'))))) 296 297 def test_commits_branch(self): 298 for mock in [mocks.local.Git(self.path), mocks.local.Git(self.path, git_svn=True)]: 299 with mock: 300 git = local.Git(self.path) 301 self.assertEqual(Commit.Encoder().default([ 302 git.commit(hash='621652ad'), 303 git.commit(hash='a30ce849'), 304 git.commit(hash='fff83bb2'), 305 git.commit(hash='9b8311f2'), 306 ]), Commit.Encoder().default(list(git.commits(begin=dict(argument='9b8311f2'), end=dict(argument='621652ad'))))) 307 286 308 287 309 class TestGitHub(unittest.TestCase): … … 416 438 self.assertEqual(remote.GitHub(self.remote).id, 'webkit') 417 439 440 def test_commits(self): 441 with mocks.remote.GitHub(): 442 git = remote.GitHub(self.remote) 443 self.assertEqual(Commit.Encoder().default([ 444 git.commit(hash='bae5d1e9'), 445 git.commit(hash='1abe25b4'), 446 git.commit(hash='fff83bb2'), 447 git.commit(hash='9b8311f2'), 448 ]), Commit.Encoder().default(list(git.commits(begin=dict(hash='9b8311f2'), end=dict(hash='bae5d1e9'))))) 449 450 def test_commits_branch(self): 451 with mocks.remote.GitHub(): 452 git = remote.GitHub(self.remote) 453 self.assertEqual(Commit.Encoder().default([ 454 git.commit(hash='621652ad'), 455 git.commit(hash='a30ce849'), 456 git.commit(hash='fff83bb2'), 457 git.commit(hash='9b8311f2'), 458 ]), Commit.Encoder().default(list(git.commits(begin=dict(argument='9b8311f2'), end=dict(argument='621652ad'))))) 459 460 418 461 419 462 class TestBitBucket(unittest.TestCase): -
trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/svn_unittest.py
r275604 r277102 28 28 from datetime import datetime, timedelta 29 29 from webkitcorepy import OutputCapture 30 from webkitscmpy import local, mocks, remote30 from webkitscmpy import Commit, local, mocks, remote 31 31 32 32 … … 232 232 self.assertIsNone(local.Svn(self.path).find('trunk', include_identifier=False).identifier) 233 233 234 def test_commits(self): 235 with mocks.local.Svn(self.path), OutputCapture(): 236 svn = local.Svn(self.path) 237 self.assertEqual(Commit.Encoder().default([ 238 svn.commit(revision='r6'), 239 svn.commit(revision='r4'), 240 svn.commit(revision='r2'), 241 svn.commit(revision='r1'), 242 ]), Commit.Encoder().default(list(svn.commits(begin=dict(revision='r1'), end=dict(revision='r6'))))) 243 244 def test_commits_branch(self): 245 with mocks.local.Svn(self.path), OutputCapture(): 246 svn = local.Svn(self.path) 247 self.assertEqual(Commit.Encoder().default([ 248 svn.commit(revision='r7'), 249 svn.commit(revision='r3'), 250 svn.commit(revision='r2'), 251 svn.commit(revision='r1'), 252 ]), Commit.Encoder().default(list(svn.commits(begin=dict(argument='r1'), end=dict(argument='r7'))))) 253 234 254 235 255 class TestRemoteSvn(unittest.TestCase): … … 332 352 def test_id(self): 333 353 self.assertEqual(remote.Svn(self.remote).id, 'webkit') 354 355 def test_commits(self): 356 self.maxDiff = None 357 with mocks.remote.Svn(): 358 svn = remote.Svn(self.remote) 359 self.assertEqual(Commit.Encoder().default([ 360 svn.commit(revision='r6'), 361 svn.commit(revision='r4'), 362 svn.commit(revision='r2'), 363 svn.commit(revision='r1'), 364 ]), Commit.Encoder().default(list(svn.commits(begin=dict(revision='r1'), end=dict(revision='r6'))))) 365 366 def test_commits_branch(self): 367 with mocks.remote.Svn(), OutputCapture(): 368 svn = remote.Svn(self.remote) 369 self.assertEqual(Commit.Encoder().default([ 370 svn.commit(revision='r7'), 371 svn.commit(revision='r3'), 372 svn.commit(revision='r2'), 373 svn.commit(revision='r1'), 374 ]), Commit.Encoder().default(list(svn.commits(begin=dict(argument='r1'), end=dict(argument='r7')))))
Note: See TracChangeset
for help on using the changeset viewer.