| #!/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()) |