| OLD | NEW |
| 1 # Copyright 2015 The LUCI Authors. All rights reserved. | 1 # Copyright 2015 The LUCI Authors. All rights reserved. |
| 2 # Use of this source code is governed under the Apache License, Version 2.0 | 2 # Use of this source code is governed under the Apache License, Version 2.0 |
| 3 # that can be found in the LICENSE file. | 3 # that can be found in the LICENSE file. |
| 4 | 4 |
| 5 import copy | 5 import copy |
| 6 import difflib | 6 import difflib |
| 7 import json | 7 import json |
| 8 import logging | 8 import logging |
| 9 import operator | 9 import operator |
| 10 import os | 10 import os |
| (...skipping 42 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 53 return os.path.dirname( # <repo root> | 53 return os.path.dirname( # <repo root> |
| 54 os.path.dirname( # infra | 54 os.path.dirname( # infra |
| 55 os.path.dirname( # config | 55 os.path.dirname( # config |
| 56 os.path.abspath(recipes_cfg)))) # recipes.cfg | 56 os.path.abspath(recipes_cfg)))) # recipes.cfg |
| 57 | 57 |
| 58 | 58 |
| 59 class ProtoFile(object): | 59 class ProtoFile(object): |
| 60 """A collection of functions operating on a proto path. | 60 """A collection of functions operating on a proto path. |
| 61 | 61 |
| 62 This is an object so that it can be mocked in the tests. | 62 This is an object so that it can be mocked in the tests. |
| 63 | |
| 64 Proto files read will always be upconverted to the current proto in | |
| 65 package.proto, and will be written back in their original format. | |
| 66 """ | 63 """ |
| 67 API_VERSIONS = (1, 2) | |
| 68 | |
| 69 def __init__(self, path): | 64 def __init__(self, path): |
| 70 self._path = path | 65 self._path = path |
| 71 | 66 |
| 72 @property | 67 @property |
| 73 def path(self): | 68 def path(self): |
| 74 return os.path.realpath(self._path) | 69 return os.path.realpath(self._path) |
| 75 | 70 |
| 76 def read_raw(self): | 71 def read_raw(self): |
| 77 with open(self._path, 'r') as fh: | 72 with open(self._path, 'r') as fh: |
| 78 return fh.read() | 73 return fh.read() |
| 79 | 74 |
| 80 def read(self): | 75 def read(self): |
| 81 obj = json.loads(self.read_raw()) | 76 text = self.read_raw() |
| 82 | |
| 83 vers = obj.get('api_version') | |
| 84 assert vers in self.API_VERSIONS, ( | |
| 85 'expected %r to be one of %r' % (vers, self.API_VERSIONS) | |
| 86 ) | |
| 87 | |
| 88 # upconvert old deps-as-a-list to deps-as-a-dict | |
| 89 if 'deps' in obj and vers == 1: | |
| 90 obj['deps'] = {d.pop('project_id'): d for d in obj['deps']} | |
| 91 | |
| 92 buf = package_pb2.Package() | 77 buf = package_pb2.Package() |
| 93 json_format.ParseDict(obj, buf, ignore_unknown_fields=True) | 78 json_format.Parse(text, buf, ignore_unknown_fields=True) |
| 94 return buf | 79 return buf |
| 95 | 80 |
| 96 def to_raw(self, buf): | 81 def to_raw(self, buf): |
| 97 obj = json_format.MessageToDict(buf, preserving_proto_field_name=True) | 82 obj = json_format.MessageToDict(buf, preserving_proto_field_name=True) |
| 98 | |
| 99 # downconvert if api_version is 1 | |
| 100 if buf.deps and buf.api_version < 2: | |
| 101 deps = [] | |
| 102 for pid, d in sorted(obj['deps'].iteritems()): | |
| 103 d['project_id'] = pid | |
| 104 deps.append(d) | |
| 105 obj['deps'] = deps | |
| 106 | |
| 107 return json.dumps(obj, indent=2, sort_keys=True).replace(' \n', '\n') | 83 return json.dumps(obj, indent=2, sort_keys=True).replace(' \n', '\n') |
| 108 | 84 |
| 109 def write(self, buf): | 85 def write(self, buf): |
| 110 with open(self._path, 'w') as fh: | 86 with open(self._path, 'w') as fh: |
| 111 fh.write(self.to_raw(buf)) | 87 fh.write(self.to_raw(buf)) |
| 112 | 88 |
| 113 | 89 |
| 114 class PackageContext(object): | 90 class PackageContext(object): |
| 115 """Contains information about where the root package and its dependency | 91 """Contains information about where the root package and its dependency |
| 116 checkouts live. | 92 checkouts live. |
| (...skipping 105 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 222 checkout_dir = self._dep_dir(context) | 198 checkout_dir = self._dep_dir(context) |
| 223 self.backend.checkout( | 199 self.backend.checkout( |
| 224 self.repo, self.revision, checkout_dir, context.allow_fetch) | 200 self.repo, self.revision, checkout_dir, context.allow_fetch) |
| 225 cleanup_pyc(checkout_dir) | 201 cleanup_pyc(checkout_dir) |
| 226 | 202 |
| 227 def repo_root(self, context): | 203 def repo_root(self, context): |
| 228 return os.path.join(self._dep_dir(context), self.path) | 204 return os.path.join(self._dep_dir(context), self.path) |
| 229 | 205 |
| 230 def dump(self): | 206 def dump(self): |
| 231 buf = package_pb2.DepSpec( | 207 buf = package_pb2.DepSpec( |
| 208 project_id=self.project_id, |
| 232 url=self.repo, | 209 url=self.repo, |
| 233 branch=self.branch, | 210 branch=self.branch, |
| 234 revision=self.revision) | 211 revision=self.revision) |
| 235 if self.path: | 212 if self.path: |
| 236 buf.path_override = self.path | 213 buf.path_override = self.path |
| 237 | 214 |
| 238 # Only dump repo_type if it's different from default. This preserves | 215 # Only dump repo_type if it's different from default. This preserves |
| 239 # compatibility e.g. with recipes.py bootstrap scripts in client repos | 216 # compatibility e.g. with recipes.py bootstrap scripts in client repos |
| 240 # which may not handle repo_type correctly. | 217 # which may not handle repo_type correctly. |
| 241 # TODO(phajdan.jr): programmatically extract the default value. | 218 # TODO(phajdan.jr): programmatically extract the default value. |
| (...skipping 104 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 346 Requires a good checkout.""" | 323 Requires a good checkout.""" |
| 347 return ProtoFile(InfraRepoConfig().to_recipes_cfg(self.path)) | 324 return ProtoFile(InfraRepoConfig().to_recipes_cfg(self.path)) |
| 348 | 325 |
| 349 def updates(self, _context, _other_revision=None): | 326 def updates(self, _context, _other_revision=None): |
| 350 """Returns (empty) list of potential updates for this spec.""" | 327 """Returns (empty) list of potential updates for this spec.""" |
| 351 return [] | 328 return [] |
| 352 | 329 |
| 353 def dump(self): | 330 def dump(self): |
| 354 """Returns the package.proto DepSpec form of this RepoSpec.""" | 331 """Returns the package.proto DepSpec form of this RepoSpec.""" |
| 355 return package_pb2.DepSpec( | 332 return package_pb2.DepSpec( |
| 333 project_id=self.project_id, |
| 356 url="file://"+self.path) | 334 url="file://"+self.path) |
| 357 | 335 |
| 358 def __eq__(self, other): | 336 def __eq__(self, other): |
| 359 if not isinstance(other, type(self)): | 337 if not isinstance(other, type(self)): |
| 360 return False | 338 return False |
| 361 return self.path == other.path | 339 return self.path == other.path |
| 362 | 340 |
| 363 | 341 |
| 364 class RootRepoSpec(RepoSpec): | 342 class RootRepoSpec(RepoSpec): |
| 365 def __init__(self, proto_file): | 343 def __init__(self, proto_file): |
| (...skipping 111 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 477 # Prevent rolling backwards. | 455 # Prevent rolling backwards. |
| 478 more_recent_revision = other_spec.get_more_recent_revision( | 456 more_recent_revision = other_spec.get_more_recent_revision( |
| 479 self._context, current_revision, other_spec.revision) | 457 self._context, current_revision, other_spec.revision) |
| 480 if more_recent_revision != other_spec.revision: | 458 if more_recent_revision != other_spec.revision: |
| 481 return False | 459 return False |
| 482 | 460 |
| 483 self._updates[other_spec.project_id] = other_spec | 461 self._updates[other_spec.project_id] = other_spec |
| 484 | 462 |
| 485 def get_rolled_spec(self): | 463 def get_rolled_spec(self): |
| 486 """Returns a PackageSpec with all the deps updates from this roll.""" | 464 """Returns a PackageSpec with all the deps updates from this roll.""" |
| 465 # TODO(phajdan.jr): does this preserve comments? should it? |
| 487 new_deps = _updated( | 466 new_deps = _updated( |
| 488 self._package_spec.deps, | 467 self._package_spec.deps, |
| 489 { project_id: spec for project_id, spec in | 468 { project_id: spec for project_id, spec in |
| 490 self._updates.iteritems() }) | 469 self._updates.iteritems() }) |
| 491 return PackageSpec( | 470 return PackageSpec( |
| 492 self._package_spec.api_version, | |
| 493 self._package_spec.project_id, | 471 self._package_spec.project_id, |
| 494 self._package_spec.recipes_path, | 472 self._package_spec.recipes_path, |
| 495 new_deps) | 473 new_deps) |
| 496 | 474 |
| 497 def get_commit_infos(self): | 475 def get_commit_infos(self): |
| 498 """Returns a mapping project_id -> list of commits from that repo | 476 """Returns a mapping project_id -> list of commits from that repo |
| 499 that are getting pulled by this roll. | 477 that are getting pulled by this roll. |
| 500 """ | 478 """ |
| 501 commit_infos = {} | 479 commit_infos = {} |
| 502 | 480 |
| (...skipping 11 matching lines...) Expand all Loading... |
| 514 | 492 |
| 515 def get_diff(self): | 493 def get_diff(self): |
| 516 """Returns a unified diff between original package spec and one after roll. | 494 """Returns a unified diff between original package spec and one after roll. |
| 517 """ | 495 """ |
| 518 orig = str(self._package_spec.dump()).splitlines() | 496 orig = str(self._package_spec.dump()).splitlines() |
| 519 new = str(self.get_rolled_spec().dump()).splitlines() | 497 new = str(self.get_rolled_spec().dump()).splitlines() |
| 520 return '\n'.join(difflib.unified_diff(orig, new, lineterm='')) | 498 return '\n'.join(difflib.unified_diff(orig, new, lineterm='')) |
| 521 | 499 |
| 522 | 500 |
| 523 class PackageSpec(object): | 501 class PackageSpec(object): |
| 524 def __init__(self, api_version, project_id, recipes_path, deps): | 502 API_VERSION = 1 |
| 525 self._api_version = api_version | 503 |
| 504 def __init__(self, project_id, recipes_path, deps): |
| 526 self._project_id = project_id | 505 self._project_id = project_id |
| 527 self._recipes_path = recipes_path | 506 self._recipes_path = recipes_path |
| 528 self._deps = deps | 507 self._deps = deps |
| 529 | 508 |
| 530 def __repr__(self): | 509 def __repr__(self): |
| 531 return 'PackageSpec(%s, %s, %r)' % (self._project_id, self._recipes_path, | 510 return 'PackageSpec(%s, %s, %r)' % (self._project_id, self._recipes_path, |
| 532 self._deps) | 511 self._deps) |
| 533 | 512 |
| 534 @classmethod | 513 @classmethod |
| 535 def load_proto(cls, proto_file): | 514 def load_proto(cls, proto_file): |
| 536 buf = proto_file.read() | 515 buf = proto_file.read() |
| 516 assert buf.api_version == cls.API_VERSION |
| 537 | 517 |
| 538 deps = { pid: cls.spec_for_dep(pid, dep) | 518 deps = { str(dep.project_id): cls.spec_for_dep(dep) |
| 539 for pid, dep in buf.deps.iteritems() } | 519 for dep in buf.deps } |
| 540 return cls(buf.api_version, str(buf.project_id), str(buf.recipes_path), | 520 return cls(str(buf.project_id), str(buf.recipes_path), deps) |
| 541 deps) | |
| 542 | 521 |
| 543 @classmethod | 522 @classmethod |
| 544 def spec_for_dep(cls, project_id, dep): | 523 def spec_for_dep(cls, dep): |
| 545 """Returns a RepoSpec for the given dependency protobuf.""" | 524 """Returns a RepoSpec for the given dependency protobuf.""" |
| 546 url = str(dep.url) | 525 url = str(dep.url) |
| 547 if url.startswith("file://"): | 526 if url.startswith("file://"): |
| 548 return PathRepoSpec(str(project_id), url[len("file://"):]) | 527 return PathRepoSpec(str(dep.project_id), url[len("file://"):]) |
| 549 | 528 |
| 550 if dep.repo_type in (package_pb2.DepSpec.GIT, package_pb2.DepSpec.GITILES): | 529 if dep.repo_type in (package_pb2.DepSpec.GIT, package_pb2.DepSpec.GITILES): |
| 551 if dep.repo_type == package_pb2.DepSpec.GIT: | 530 if dep.repo_type == package_pb2.DepSpec.GIT: |
| 552 backend = fetch.GitBackend() | 531 backend = fetch.GitBackend() |
| 553 elif dep.repo_type == package_pb2.DepSpec.GITILES: | 532 elif dep.repo_type == package_pb2.DepSpec.GITILES: |
| 554 backend = fetch.GitilesBackend() | 533 backend = fetch.GitilesBackend() |
| 555 return GitRepoSpec(str(project_id), | 534 return GitRepoSpec(str(dep.project_id), |
| 556 url, | 535 url, |
| 557 str(dep.branch), | 536 str(dep.branch), |
| 558 str(dep.revision), | 537 str(dep.revision), |
| 559 str(dep.path_override), | 538 str(dep.path_override), |
| 560 backend) | 539 backend) |
| 561 | 540 |
| 562 assert False, 'Unexpected repo type: %s' % dep | 541 assert False, 'Unexpected repo type: %s' % dep |
| 563 | 542 |
| 564 @property | 543 @property |
| 565 def project_id(self): | 544 def project_id(self): |
| 566 return self._project_id | 545 return self._project_id |
| 567 | 546 |
| 568 @property | 547 @property |
| 569 def recipes_path(self): | 548 def recipes_path(self): |
| 570 return self._recipes_path | 549 return self._recipes_path |
| 571 | 550 |
| 572 @property | 551 @property |
| 573 def deps(self): | 552 def deps(self): |
| 574 return self._deps | 553 return self._deps |
| 575 | 554 |
| 576 @property | |
| 577 def api_version(self): | |
| 578 return self._api_version | |
| 579 | |
| 580 def dump(self): | 555 def dump(self): |
| 581 return package_pb2.Package( | 556 return package_pb2.Package( |
| 582 api_version=self._api_version, | 557 api_version=self.API_VERSION, |
| 583 project_id=self._project_id, | 558 project_id=self._project_id, |
| 584 recipes_path=self._recipes_path, | 559 recipes_path=self._recipes_path, |
| 585 deps={k: v.dump() for k, v in self._deps.iteritems()}) | 560 deps=[ self._deps[dep].dump() for dep in sorted(self._deps.keys()) ]) |
| 586 | 561 |
| 587 def roll_candidates(self, root_spec, context): | 562 def roll_candidates(self, root_spec, context): |
| 588 """Returns list of consistent roll candidates, and rejected roll candidates. | 563 """Returns list of consistent roll candidates, and rejected roll candidates. |
| 589 | 564 |
| 590 The first one is sorted by score, descending. The more commits are pulled by | 565 The first one is sorted by score, descending. The more commits are pulled by |
| 591 the roll, the higher score. | 566 the roll, the higher score. |
| 592 | 567 |
| 593 Second list is included to distinguish between a situation where there are | 568 Second list is included to distinguish between a situation where there are |
| 594 no roll candidates from one where there are updates but they're not | 569 no roll candidates from one where there are updates but they're not |
| 595 consistent. | 570 consistent. |
| (...skipping 121 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 717 >>> d = { 'x': 1, 'y': 2 } | 692 >>> d = { 'x': 1, 'y': 2 } |
| 718 >>> sorted(_updated(d, { 'y': 3, 'z': 4 }).items()) | 693 >>> sorted(_updated(d, { 'y': 3, 'z': 4 }).items()) |
| 719 [('x', 1), ('y', 3), ('z', 4)] | 694 [('x', 1), ('y', 3), ('z', 4)] |
| 720 >>> sorted(d.items()) | 695 >>> sorted(d.items()) |
| 721 [('x', 1), ('y', 2)] | 696 [('x', 1), ('y', 2)] |
| 722 """ | 697 """ |
| 723 | 698 |
| 724 d = copy.copy(d) | 699 d = copy.copy(d) |
| 725 d.update(updates) | 700 d.update(updates) |
| 726 return d | 701 return d |
| OLD | NEW |