blob: e0adea74e01c07b6216a2dbc4cc207f92e7a79cc [file] [log] [blame]
# Copyright 2015 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
import collections
import contextlib
import json
import os
import re
from infra.libs.gitiles import gitiles
from infra.tools.cros_pin.logger import LOGGER
class InvalidPinError(Exception):
pass
class ReadOnlyError(Exception):
pass
# Named Chromite pins to their checkout-relative paths.
Config = collections.namedtuple('Config',
('name', 'base', 'json_subpath', 'masters'))
EXTERNAL = Config(
'external',
('build',),
('scripts', 'common', 'cros_chromite_pins.json'),
('chromiumos',))
INTERNAL = Config(
'internal',
('build_internal',),
('scripts', 'common_internal', 'cros_chromite_internal_pins.json'),
('chromeos', 'chromeos_release'))
PinUpdate = collections.namedtuple('PinUpdate',
('name', 'fr', 'to'))
class Editor(object):
# Regular expression to match a Git commit (SHA1)
RE_COMMIT_SHA1 = re.compile(r'^[a-fA-F0-9]{40}$')
def __init__(self, checkout_path, gitiles_repo, validate=True):
self._checkout_path = checkout_path
self._gitiles = None
self._validate = validate
self._gitiles = gitiles_repo
def load(self, pin):
return self.File(self, pin)
def get_commit(self, branch):
try:
return self._gitiles.ref_info(branch)['commit']
except gitiles.GitilesError:
raise InvalidPinError("Pin ref [%s] does not exist." % (branch,))
def validate_pin(self, ref):
if not self.RE_COMMIT_SHA1.match(ref):
raise InvalidPinError("Not a valid SHA1 hash")
self.get_commit(ref)
class File(object):
def __init__(self, editor, pin):
self._editor = editor
self._pin = pin
self._path = os.path.join(editor._checkout_path,
*(pin.base + pin.json_subpath))
@contextlib.contextmanager
def edit(self):
d = self.load()
orig = d.copy()
try:
yield d
finally:
if d != orig:
self.save(**d)
def load(self):
with open(self._path, 'r') as fd:
return json.load(fd)
def save(self, **pins):
for k, v in pins.iteritems():
assert isinstance(k, basestring)
assert isinstance(v, basestring)
LOGGER.debug('Writing pin file [%s]: %s', self._path, pins)
with open(self._path, 'w') as fd:
json.dump(pins, fd, indent=2, sort_keys=True)
def has_pin(self, pin_name):
return pin_name in self.load()
def update(self, pin_name, create=False, version=None):
"""Updates a single pin value."""
if not version:
LOGGER.debug('Resolving version for pin [%s]', pin_name)
version = self._editor.get_commit(pin_name)
elif self._editor._validate:
LOGGER.debug('Validating pin [%s]', pin_name)
self._editor.validate_pin(version)
with self.edit() as pins:
current = pins.get(pin_name)
if current == version:
LOGGER.warning('Pin [%s.%s] is already at version [%s]',
self._pin.name, pin_name, current)
return None
LOGGER.info('Updating pin [%s.%s]: [%s] => [%s]',
self._pin.name, pin_name, current, version)
if not (current or create):
raise ReadOnlyError("Pin does not exist [%s]" % (pin_name,))
pins[pin_name] = version
return PinUpdate(pin_name, current, version)
def remove(self, pin_name):
"""Removes a single pin from the pin list."""
with self.edit() as pins:
cur = pins.pop(pin_name, None)
if cur is None:
return None
return PinUpdate(pin_name, cur, None)
def iterpins(self):
"""Returns a list of pins."""
return self.load().iteritems()