| #!env /usr/bin/env python3 |
| # -*- coding: utf-8 -*- |
| # Licensed to the Apache Software Foundation (ASF) under one or more |
| # contributor license agreements. See the NOTICE file distributed with |
| # this work for additional information regarding copyright ownership. |
| # The ASF licenses this file to You under the Apache License, Version 2.0 |
| # (the "License"); you may not use this file except in compliance with |
| #the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| import re |
| import requests |
| import hashlib |
| from dateutil import parser |
| import time |
| import json |
| |
| title = "Scanner for Gerrit Code Review" |
| version = "0.1.1" |
| |
| |
| CHANGES_URL = "%s/changes/%s" |
| PROJECT_LIST_URL = "%s/projects/" |
| ACCOUNTS_URL = "%s/accounts/%d" |
| COMMIT_ID_RE = re.compile(" Change-Id: (.*)") |
| |
| def accepts(source): |
| """ Do we accept this source?? """ |
| if source['type'] == "gerrit": |
| return True |
| return False |
| |
| def getjson(response): |
| response.raise_for_status() |
| return json.loads(response.text[4:]) |
| |
| def get(url, params=None): |
| resp = requests.get(url, params=params) |
| return getjson(resp) |
| |
| def changes(base_url, params=None): |
| return get(CHANGES_URL % (base_url, ''), params=params) |
| |
| def change_details(base_url, change): |
| if isinstance(change, dict): |
| id = change['change_id'] |
| else: |
| id = change |
| |
| return get(CHANGES_URL % (base_url, id) + "/detail") |
| |
| def get_commit_id(commit_message): |
| all = COMMIT_ID_RE.findall(commit_message) |
| if all: |
| return all[0] |
| return None |
| |
| def get_all(base_url, f, params={}): |
| acc = [] |
| |
| while True: |
| items = f(base_url, params=params) |
| if not items: |
| break |
| |
| acc.extend(items) |
| params.update({"start": len(acc)}) |
| |
| return acc |
| |
| def format_date(d, epoch=False): |
| if not d: |
| return |
| parsed = parser.parse(d) |
| |
| if epoch: |
| return time.mktime(parsed.timetuple()) |
| |
| return time.strftime("%Y/%m/%d %H:%M:%S", parsed.timetuple()) |
| |
| def make_hash(repo, change): |
| return hashlib.sha224(("%s-%s-%s" % (repo['organisation'], |
| repo['sourceID'], |
| change['change_id'])).encode('ascii', |
| errors='replace')).hexdigest() |
| |
| def is_closed(change): |
| return change['status'] == 'MERGED' or change['status'] == 'ABANDONED' |
| |
| def make_issue(repo, base_url, change): |
| key = change['change_id'] |
| dhash = make_hash(repo, change) |
| |
| closed_date = None |
| if is_closed(change): |
| closed_date = change['updated'] |
| |
| if not 'email' in change['owner']: |
| change['owner']['email'] = "%u@invalid.gerrit" % change['owner']['_account_id'] |
| owner_email = change['owner']['email'] |
| |
| messages = [] |
| for message in change.get('messages', []): |
| messages.append(message.get('message', "")) |
| |
| return { |
| 'id': dhash, |
| 'key': key, |
| 'organisation': repo['organisation'], |
| 'sourceID': repo['sourceID'], |
| 'url': base_url + "/#/q/" + key, |
| 'status': change['status'], |
| 'created': format_date(change['created'], epoch=True), |
| 'closed': format_date(closed_date, epoch=True), |
| 'issueCloser': owner_email, |
| 'createdDate': format_date(change['created']), |
| 'closedDate': format_date(closed_date), |
| 'changeDate': format_date(closed_date |
| if closed_date |
| else change['created']), |
| 'assignee': owner_email, |
| 'issueCreator': owner_email, |
| 'comments': len(messages), |
| 'title': change['subject'] |
| } |
| |
| def make_person(repo, raw_person): |
| email = raw_person['email'] |
| id = hashlib.sha1(("%s%s" % (repo['organisation'], |
| email)).encode('ascii', |
| errors='replace')).hexdigest() |
| return {'email': email, 'id': id, 'organisation': repo['organisation'], |
| 'name': raw_person['name'] if 'name' in raw_person else "%u" % raw_person['_account_id']} |
| |
| def update_issue(KibbleBit, issue): |
| id = issue['id'] |
| KibbleBit.pprint("Updating issue: " + id) |
| KibbleBit.index('issue', id, issue) |
| |
| def update_person(KibbleBit, person): |
| KibbleBit.pprint("Updating person: " + person['name'] + " - " + person['email']) |
| KibbleBit.index('person', person['id'], {'doc': person, 'doc_as_upsert': True}) |
| |
| def status_changed(stored_change, change): |
| if not stored_change or not change: |
| return True |
| return stored_change['status'] != change['status'] |
| |
| def scan(KibbleBit, source): |
| source['steps']['issues'] = { |
| 'time': time.time(), |
| 'status': 'Analyzing Gerrit tickets...', |
| 'running': True, |
| 'good': True |
| } |
| KibbleBit.updateSource(source) |
| |
| url = source['sourceURL'] |
| # Try matching foo.bar/r/project/subfoo |
| m = re.match(r"(.+://.+?/r)/(.+)", url) |
| if m: |
| base_url = m.group(1) |
| project_name = m.group(2) |
| # Fall back to old splitty split |
| else: |
| url = re.sub(r"^git://", "http://", url) |
| source_parts = url.split('/') |
| project_name = source_parts[-1] |
| base_url = '/'.join(source_parts[:-1]) # remove the trailing /blah/ |
| |
| # TODO: figure out branch from current checkout |
| q = "(is:open OR is:new OR is:closed OR is:merged OR is:abandoned) AND project:\"%s\"" % project_name |
| all_changes = get_all(base_url, changes, |
| {'q': q, |
| 'o': ['LABELS', 'DETAILED_ACCOUNTS']}) |
| |
| print("Found " + str(len(all_changes)) + " changes for project: " + |
| project_name) |
| |
| people = {} |
| for change in all_changes: |
| try: |
| # TODO: check if needs updating here before getting details |
| dhash = make_hash(source, change) |
| |
| stored_change = None |
| if KibbleBit.exists('issue', dhash): |
| stored_change = KibbleBit.get('issue', dhash) |
| |
| if not status_changed(stored_change, change): |
| #print("change %s seen already and status unchanged. Skipping." % |
| # change['change_id']) |
| continue |
| |
| details = change_details(base_url, change) |
| |
| issue_doc = make_issue(source, base_url, details) |
| update_issue(KibbleBit, issue_doc) |
| |
| labels = details['labels'] |
| change_people = [] |
| |
| if 'owner' in details: |
| change_people.append(details['owner']) |
| if 'Module-Owner' in labels and 'all' in labels['Module-Owner']: |
| change_people.extend(labels['Module-Owner']['all']) |
| if 'Code-Review' in labels and 'all' in labels['Code-Review']: |
| change_people.extend(labels['Code-Review']['all']) |
| if 'Verified' in labels and 'all' in labels['Verified']: |
| change_people.extend(labels['Verified']['all']) |
| |
| print(change['change_id'] + " -> " + str(len(change_people)) + |
| " people.") |
| |
| for person in change_people: |
| if 'email' in person and person['email'] not in people: |
| people[person['email']] = person |
| update_person(KibbleBit, make_person(source, person)) |
| |
| except requests.HTTPError as e: |
| print(e) |
| |
| source['steps']['issues'] = { |
| 'time': time.time(), |
| 'status': 'Done analyzing tickets!', |
| 'running': False, |
| 'good': True |
| } |
| KibbleBit.updateSource(source) |