795d405b534ac6eb800c9a4e53afcdc63702da0b
[people/salgado/patchwork.git] / apps / patchmetrics / gerrit.py
1 from datetime import datetime
2 import email
3 import urllib2
4
5 import json
6
7 from patchwork.models import Patch, Project, State
8 from patchmetrics.models import GerritChange
9
10
11 ROOT_URL = 'https://android-review.googlesource.com/gerrit/rpc'
12 # A mapping from AOSP states to our internal ones.
13 STATE_MAP = {'MERGED': 'Accepted', 'NEW': 'New', 'ABANDONED': 'Rejected'}
14
15
16 class GerritError(Exception):
17     """The gerrit server returned an error in response to a query."""
18
19
20 def jsonrpc_request(path, method, params):
21     headers = {
22         'Accept': 'application/json,application/jsonrequest',
23         'Content-type': 'application/json; charset=UTF-8'}
24     data = json.dumps({
25         'jsonrpc': '2.0',
26         'method': method,
27         'params': params,
28         'id': 1})
29     url = ROOT_URL + path
30     response = json.loads(
31         urllib2.urlopen(urllib2.Request(url, data, headers)).read())
32     if 'error' in response:
33         msg = "Got an error on %s, with method=%s and params=%s: %s" % (
34             url, method, params, response['error']['message'])
35         raise GerritError(msg)
36     return response.get('result')
37
38
39 def get_aosp_change_details(change_id):
40     """Get the details of the Gerrit change with the given ID."""
41     return jsonrpc_request(
42         '/ChangeDetailService',
43         'changeDetail', [dict(id=change_id)])['change']
44
45
46 def get_aosp_changes_from(email, start_after=None, page_size=100):
47     """Get the list of changes authored by the given email address.
48
49     :param start_after: Only include changes whose sortKey is lower than
50                         this. Use None if you want to start from the beginning.
51     :param page_size: Number of items to include in the list.
52     """
53     if start_after is None:
54         start_after = 'z'
55     query = "owner: %s" % email
56     return jsonrpc_request(
57         '/ChangeListService',
58         'allQueryNext', [query, start_after, page_size])
59
60
61 def create_or_update_patches_from_aosp_changes(changes, author):
62     """Create or update Patch objects from the given list of changes.
63
64     For each of the changes in the list, make sure there's a matching Patch
65     (and GerritChange) object in our DB matching it and update it to be in
66     sync with the remote change.
67
68     Return a list with they IDs of the created/updated changes.
69     """
70     print "Checking changes submitted by %s ..." % author.email
71     updated_changes = []
72     for change in changes:
73         gerrit_id = change['id']['id']
74         last_updated_on = datetime.strptime(
75             change['lastUpdatedOn'], '%Y-%m-%d %H:%M:%S.%f000')
76         try:
77             existing = GerritChange.objects.get(gerrit_id=gerrit_id)
78         except GerritChange.DoesNotExist:
79             existing = None
80         # Must get the change's details because we need their creation date.
81         details = get_aosp_change_details(gerrit_id)
82         date_created = datetime.strptime(
83             details['createdOn'], '%Y-%m-%d %H:%M:%S.%f000')
84         change_dict = dict(
85             id=gerrit_id, project=change['project']['key']['name'],
86             last_updated_on=last_updated_on, status=change['status'],
87             subject=change['subject'], date_created=date_created,
88             change_id=change['key']['id'])
89         if existing is None:
90             create_patch_from_aosp_change(change_dict, author)
91             updated_changes.append(gerrit_id)
92         elif existing.last_updated_on < last_updated_on:
93             update_local_gerrit_change_from_aosp_change(existing, change_dict)
94             updated_changes.append(gerrit_id)
95         else:
96             # The changes are sorted most recently updated first, so as soon
97             # as we see one that doesn't need to be updated in our DB we know
98             # for sure that the following ones don't need to be updated
99             # either.
100             break
101     print "Done"
102     print "#" * 60, "\n"
103     return updated_changes
104
105
106 def get_state_for_aosp_status(aosp_status):
107     return State.objects.get(name=STATE_MAP[aosp_status])
108
109
110 def get_project_for_aosp_name(aosp_name):
111     name = get_project_name_for_aosp_name(aosp_name)
112     try:
113         return Project.objects.get(linkname=name)
114     except Project.DoesNotExist:
115         project = Project(linkname=name, name="AOSP %s" % aosp_name,
116                           listid=name, listemail=name)
117         project.save()
118         return project
119
120
121 def get_project_name_for_aosp_name(aosp_name):
122     return 'aosp-' + aosp_name.replace('/', '-')
123
124
125 def update_local_gerrit_change_from_aosp_change(gerrit_change, change_dict):
126     """Update the local GerritChange from the given change_dict."""
127     print "Updating GerritChange/Patch for %s" % gerrit_change.url
128     gerrit_change.last_updated_on = change_dict['last_updated_on']
129     assert gerrit_change.gerrit_id == change_dict['id'], (
130         "The ID of the local GerritChange (%d) doesn't match that of the "
131         "remote change (%d)" % (gerrit_change.gerrit_id, change_dict['id']))
132     # This will take care of updating the state of the Patch object linked to
133     # this GerritChange.
134     gerrit_change.status = change_dict['status']
135     gerrit_change.save()
136     patch = gerrit_change.patch
137     patch.name = change_dict['subject']
138     # XXX: Is it possible to change the project of a gerrit change, after it's
139     # created?
140     # patch.project = get_project_for_aosp_name(change_dict['project'])
141     # XXX: Is this appropriate?  Need to find out what can cause
142     # lastUpdatedOn to change on gerrit.
143     # patch.date_last_state_change = change_dict['last_updated_on']
144     patch.save()
145
146
147 def create_patch_from_aosp_change(change, author):
148     """Create a Patch and GerritChange for the given change and author.
149
150     :param change: A dict with the following keys: id, change_id, project,
151                    date_created, subject, status, last_updated_on.
152     :param author: The Person who authored this change.
153     """
154     # This is the unique identifier that gerrit includes in the changes'
155     # commit message to later identify when it's committed.
156     change_id = change['change_id']
157     project = get_project_for_aosp_name(change['project'])
158     date = change['date_created']
159     name = change['subject']
160     # Patch.msgid can't be None, so we make a dummy one here.
161     msgid = email.utils.make_msgid()
162     # We don't set the patch state because it will be set when we save the
163     # GerritChange that is linked to it.
164     patch = Patch(author=author, submitter=author, date=date, name=name,
165                   project=project, msgid=msgid)
166     patch.save()
167     gerrit_change = GerritChange(
168         gerrit_id=change['id'], patch=patch, status=change['status'],
169         change_id=change_id, last_updated_on=change['last_updated_on'])
170     gerrit_change.save()
171
172     if patch.date_last_state_change is not None:
173         # We're creating a new patch and it's already had a state change so
174         # it's best to use the last_updated_on date as date_last_state_change
175         # than to leave it as datetime.now(), which was set above when we
176         # saved the GerritChange instance.
177         patch.date_last_state_change = change['last_updated_on']
178         patch.save()
179
180     print "Created GerritChange/Patch for %s" % gerrit_change.url
181     return patch