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 |