| #!/usr/bin/env python | |
| # Copyright 2017 The Fuchsia Authors. All rights reserved. | |
| # Use of this source code is governed by a BSD-style license that can be | |
| # found in the LICENSE file. | |
| ### view commits not yet rolled to a layer | |
| import argparse | |
| from argparse import RawTextHelpFormatter | |
| import base64 | |
| from datetime import datetime, timedelta, tzinfo | |
| import json | |
| import sys | |
| import urllib2 | |
| import xml.etree.ElementTree as xml | |
| LAYERS = [ | |
| 'vendor', # For checkouts building on top of Topaz. | |
| 'topaz', | |
| 'peridot', | |
| 'garnet', | |
| 'zircon', | |
| ] | |
| # Authors whose commits are not displayed. | |
| IGNORED_AUTHORS = [ | |
| 'skia-fuchsia-autoroll@skia-buildbots.google.com.iam.gserviceaccount.com', | |
| 'third-party-roller', | |
| 'topaz-roller', | |
| 'peridot-roller', | |
| 'garnet-roller', | |
| 'zircon-roller', | |
| ] | |
| def http_get(url): | |
| """Fetches the content at a given URL.""" | |
| try: | |
| target = urllib2.urlopen(url) | |
| return target.read() | |
| finally: | |
| if target: | |
| target.close() | |
| def get_lower_layer(layer): | |
| """Returns the layer immediately below the given layer.""" | |
| if layer not in LAYERS: | |
| # Special case for "build" and "scripts". | |
| return None | |
| index = LAYERS.index(layer) | |
| if index == len(LAYERS) - 1: | |
| return None | |
| return LAYERS[index + 1] | |
| def get_lower_layer_commit(layer, at, lower_layer): | |
| """Returns the pinned revision of a layer below the given layer.""" | |
| url = ('https://fuchsia.googlesource.com/%s/+/%s/manifest/%s?format=TEXT' | |
| % (layer, at, layer)) | |
| content = http_get(url) | |
| content = base64.b64decode(content) | |
| manifest = xml.fromstring(content) | |
| nodes = manifest.findall('./imports/import[@name="%s"]' % lower_layer) | |
| return (lower_layer, nodes[0].get('revision')) | |
| def get_lower_layer_commits(layer, at): | |
| """Returns the pinned revision of the 'layers' below the given layer. | |
| Multiple 'layers' are needed for the Garnet case, where we want to be | |
| able to inspect the revisions of //build and //scripts. | |
| """ | |
| if layer != 'garnet': | |
| lower_layer = get_lower_layer(layer) | |
| if not lower_layer: | |
| return [] | |
| return [get_lower_layer_commit(layer, at, lower_layer)] | |
| return [ | |
| get_lower_layer_commit('garnet', at, 'zircon'), | |
| get_lower_layer_commit('garnet', at, 'build'), | |
| get_lower_layer_commit('garnet', at, 'scripts'), | |
| ] | |
| def get_commits(layer, revision): | |
| """Returns the commits in the given layer up to a given commit.""" | |
| url = 'https://fuchsia.googlesource.com/%s/+log/master?format=JSON' % layer | |
| def get_more(result, start=None): | |
| get_url = url | |
| if start: | |
| get_url = '%s&s=%s' % (url, start) | |
| content = http_get(get_url) | |
| # Remove the anti-XSSI header. | |
| content = content[5:] | |
| data = json.loads(content) | |
| for commit in data['log']: | |
| if commit['commit'] == revision: | |
| return | |
| result.append(commit) | |
| get_more(result, start=content['next']) | |
| result = [] | |
| get_more(result) | |
| return result | |
| def filter_commit(commit): | |
| """Returns True if a commit should be listed.""" | |
| return commit['author']['name'] not in IGNORED_AUTHORS | |
| class MyTimezone(tzinfo): | |
| """Simple timezone implementation, since for some reason Python 2.7 doesn't | |
| provide one. | |
| """ | |
| def __init__(self, data=None): | |
| self.data = data if data else '+0000' | |
| def utcoffset(self, dt): | |
| hours = int(self.data[1:3]) | |
| minutes = int(self.data[3:5]) | |
| delta = timedelta(hours=hours, minutes=minutes) | |
| if self.data[0] == '-': | |
| delta = -delta | |
| return delta | |
| def tzname(self, dt): | |
| return 'Bogus' | |
| def dst(self, dt): | |
| return timedelta(0) | |
| def get_time_since(timestamp): | |
| """Returns a string describing the amount of time elapsed since the given | |
| timestamp. | |
| Timestamp format: Sat Feb 10 03:17:06 2018 +0000 | |
| """ | |
| timestamp_no_tz = timestamp[:-6] | |
| date_no_tz = datetime.strptime(timestamp_no_tz, '%a %b %d %H:%M:%S %Y') | |
| date = date_no_tz.replace(tzinfo=MyTimezone(timestamp[-5:])) | |
| now = datetime.utcnow().replace(tzinfo=MyTimezone()) | |
| delta = now - date | |
| if delta.days >= 1: | |
| return '>1d' | |
| hours = delta.seconds / 3600 | |
| if hours >= 1: | |
| return '%sh' % hours | |
| minutes = (delta.seconds % 3600) / 60 | |
| return '%sm' % minutes | |
| def print_commits(layer, commits): | |
| """Prints the given commits in a user=pleasing format.""" | |
| commits = [c for c in commits if filter_commit(c)] | |
| if commits: | |
| timestamp = commits[-1]['committer']['time'] | |
| elapsed_time = get_time_since(timestamp) | |
| else: | |
| elapsed_time = '' | |
| print('--------------') | |
| print('| %s | %s' % ('{:^10}'.format(layer), elapsed_time)) | |
| print('--------------') | |
| for commit in commits: | |
| print('%s | %s | %s' % (commit['commit'][:7], | |
| commit['author']['name'][:15].ljust(15), | |
| commit['message'].splitlines()[0])) | |
| if not commits: | |
| print('None') | |
| def main(): | |
| parser = argparse.ArgumentParser(formatter_class=RawTextHelpFormatter, | |
| description="""Displays the commits not yet rolled to a given layer. | |
| For vendor layers, specify the Topaz revision with --topaz-revision.""") | |
| parser.add_argument('--layer', | |
| help='Name of the layer to inspect', | |
| choices=LAYERS, | |
| default='topaz') | |
| parser.add_argument('--topaz-revision', | |
| help='(vendor only) revision of the Topaz layer') | |
| args = parser.parse_args() | |
| layer = args.layer | |
| revision = args.topaz_revision | |
| if layer != 'vendor' and revision: | |
| print('Warning: Topaz revision only used with vendor layers.') | |
| if layer == 'vendor' and not revision: | |
| print('Error: vendor layers require a Topaz revision.') | |
| return 1 | |
| print('Pending commits for: %s' % layer.upper()) | |
| if layer == 'vendor': | |
| commits = get_commits('topaz', revision) | |
| print_commits('topaz', commits) | |
| layer = 'topaz' | |
| else: | |
| revision = 'master' | |
| next_layers = [(layer, revision)] | |
| while next_layers: | |
| layer, revision = next_layers.pop(0) | |
| layer_refs = get_lower_layer_commits(layer, revision) | |
| if not layer_refs: | |
| continue | |
| for lower_layer, lower_revision in layer_refs: | |
| commits = get_commits(lower_layer, lower_revision) | |
| print_commits(lower_layer, commits) | |
| next_layers.extend(layer_refs) | |
| return 0 | |
| if __name__ == "__main__": | |
| sys.exit(main()) |