blob: e86be8debc230c0996d5b85df080c80df3962ee9 [file] [log] [blame]
Monty Taylor6c9634c2012-07-28 11:27:47 -05001#!/usr/bin/env python
2# Copyright (c) 2011 OpenStack, LLC.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16# This is designed to be called by a gerrit hook. It searched new
17# patchsets for strings like "bug FOO" and updates corresponding Launchpad
18# bugs status.
19
20from launchpadlib.launchpad import Launchpad
21from launchpadlib.uris import LPNET_SERVICE_ROOT
Jeremy Stanleyeb0d6982012-12-04 17:26:28 +000022import jeepyb.gerritdb
Monty Taylor6c9634c2012-07-28 11:27:47 -050023import os
24import argparse
25import re
26import subprocess
27
28
29BASE_DIR = '/home/gerrit2/review_site'
Monty Taylorda3bada2012-11-22 09:38:22 -080030GERRIT_CACHE_DIR = os.path.expanduser(
31 os.environ.get('GERRIT_CACHE_DIR',
32 '~/.launchpadlib/cache'))
33GERRIT_CREDENTIALS = os.path.expanduser(
34 os.environ.get('GERRIT_CREDENTIALS',
35 '~/.launchpadlib/creds'))
Monty Taylor6c9634c2012-07-28 11:27:47 -050036
37
38def add_change_proposed_message(bugtask, change_url, project, branch):
39 subject = 'Fix proposed to %s (%s)' % (short_project(project), branch)
40 body = 'Fix proposed to branch: %s\nReview: %s' % (branch, change_url)
41 bugtask.bug.newMessage(subject=subject, content=body)
42
43
44def add_change_merged_message(bugtask, change_url, project, commit,
45 submitter, branch, git_log):
46 subject = 'Fix merged to %s (%s)' % (short_project(project), branch)
47 git_url = 'http://github.com/%s/commit/%s' % (project, commit)
48 body = '''Reviewed: %s
49Committed: %s
50Submitter: %s
51Branch: %s\n''' % (change_url, git_url, submitter, branch)
52 body = body + '\n' + git_log
53 bugtask.bug.newMessage(subject=subject, content=body)
54
55
56def set_in_progress(bugtask, launchpad, uploader, change_url):
57 """Set bug In progress with assignee being the uploader"""
58
Jeremy Stanleyeb0d6982012-12-04 17:26:28 +000059 # Retrieve uploader from Launchpad by correlating Gerrit E-mail
60 # address to OpenID, and only set if there is a clear match.
Monty Taylor6c9634c2012-07-28 11:27:47 -050061 try:
62 searchkey = uploader[uploader.rindex("(") + 1:-1]
63 except ValueError:
64 searchkey = uploader
Jeremy Stanleyeb0d6982012-12-04 17:26:28 +000065
66 # The counterintuitive query is due to odd database schema choices
67 # in Gerrit. For example, an account with a secondary E-mail
68 # address added looks like...
69 # select email_address,external_id from account_external_ids
70 # where account_id=1234;
71 # +-----------------+-----------------------------------------+
72 # | email_address | external_id |
73 # +-----------------+-----------------------------------------+
74 # | plugh@xyzzy.com | https://login.launchpad.net/+id/fR0bnU1 |
75 # | bar@foo.org | mailto:bar@foo.org |
76 # | NULL | username:quux |
77 # +-----------------+-----------------------------------------+
78 # ...thus we need a join on a secondary query to search against
79 # all the user's configured E-mail addresses.
80 #
81 query = """SELECT t.external_id FROM account_external_ids t
82 INNER JOIN (
83 SELECT t.account_id FROM account_external_ids t
84 WHERE t.email_address = %s )
85 original ON t.account_id = original.account_id
James E. Blaire903d592013-01-11 14:31:26 -080086 AND t.external_id LIKE 'https://login.launchpad.net%%'"""
Jeremy Stanleyeb0d6982012-12-04 17:26:28 +000087
88 cursor = jeepyb.gerritdb.connect().cursor()
89 cursor.execute(query, searchkey)
90 data = cursor.fetchone()
91 if data:
Jeremy Stanley4d25a7c2013-01-11 22:00:58 +000092 assignee = launchpad.people.getByOpenIDIdentifier(identifier=data[0])
93 if assignee:
94 bugtask.assignee = assignee
Monty Taylor6c9634c2012-07-28 11:27:47 -050095
96 bugtask.status = "In Progress"
97 bugtask.lp_save()
98
99
100def set_fix_committed(bugtask):
101 """Set bug fix committed"""
102
103 bugtask.status = "Fix Committed"
104 bugtask.lp_save()
105
106
107def set_fix_released(bugtask):
108 """Set bug fix released"""
109
110 bugtask.status = "Fix Released"
111 bugtask.lp_save()
112
113
114def release_fixcommitted(bugtask):
115 """Set bug FixReleased if it was FixCommitted"""
116
117 if bugtask.status == u'Fix Committed':
118 set_fix_released(bugtask)
119
120
121def tag_in_branchname(bugtask, branch):
122 """Tag bug with in-branch-name tag (if name is appropriate)"""
123
124 lp_bug = bugtask.bug
125 branch_name = branch.replace('/', '-')
126 if branch_name.replace('-', '').isalnum():
127 lp_bug.tags = lp_bug.tags + ["in-%s" % branch_name]
128 lp_bug.tags.append("in-%s" % branch_name)
129 lp_bug.lp_save()
130
131
132def short_project(full_project_name):
133 """Return the project part of the git repository name"""
134 return full_project_name.split('/')[-1]
135
136
137def git2lp(full_project_name):
138 """Convert Git repo name to Launchpad project"""
139 project_map = {
140 'openstack/openstack-ci-puppet': 'openstack-ci',
141 'openstack-ci/devstack-gate': 'openstack-ci',
142 'openstack-ci/gerrit': 'openstack-ci',
143 'openstack-ci/lodgeit': 'openstack-ci',
144 'openstack-ci/meetbot': 'openstack-ci',
Thierry Carreze0d18ce2012-12-03 12:11:09 +0100145 'openstack-ci/jeepyb': 'openstack-ci',
annegentle6e13f9d2012-12-04 13:47:37 -0600146 'openstack/api-site': 'openstack-api-site',
Thierry Carreze0d18ce2012-12-03 12:11:09 +0100147 'openstack/oslo-incubator': 'oslo',
Monty Taylorda3bada2012-11-22 09:38:22 -0800148 }
Monty Taylor6c9634c2012-07-28 11:27:47 -0500149 return project_map.get(full_project_name, short_project(full_project_name))
150
151
152def is_direct_release(full_project_name):
153 """Test against a list of projects who directly release changes."""
154 return full_project_name in [
155 'openstack-ci/devstack-gate',
156 'openstack-ci/lodgeit',
157 'openstack-ci/meetbot',
158 'openstack-dev/devstack',
159 'openstack/openstack-ci',
160 'openstack/openstack-ci-puppet',
Thierry Carreze0d18ce2012-12-03 12:11:09 +0100161 'openstack-ci/jeepyb',
Monty Taylor6c9634c2012-07-28 11:27:47 -0500162 'openstack/openstack-manuals',
annegentle6e13f9d2012-12-04 13:47:37 -0600163 'openstack/api-site',
Thierry Carrez101d17e2012-09-28 11:54:38 +0200164 'openstack/tempest',
Monty Taylorda3bada2012-11-22 09:38:22 -0800165 ]
Monty Taylor6c9634c2012-07-28 11:27:47 -0500166
167
168def process_bugtask(launchpad, bugtask, git_log, args):
169 """Apply changes to bugtask, based on hook / branch..."""
170
171 if args.hook == "change-merged":
172 if args.branch == 'master':
173 if is_direct_release(args.project):
174 set_fix_released(bugtask)
175 else:
Thierry Carrez8ea78ac2012-11-14 15:47:57 +0100176 if bugtask.status != u'Fix Released':
177 set_fix_committed(bugtask)
Monty Taylor6c9634c2012-07-28 11:27:47 -0500178 elif args.branch == 'milestone-proposed':
179 release_fixcommitted(bugtask)
180 elif args.branch.startswith('stable/'):
181 series = args.branch[7:]
182 # Look for a related task matching the series
183 for reltask in bugtask.related_tasks:
Thierry Carrez8ea78ac2012-11-14 15:47:57 +0100184 if (reltask.bug_target_name.endswith("/" + series) and
Monty Taylorda3bada2012-11-22 09:38:22 -0800185 reltask.status != u'Fix Released'):
Monty Taylor6c9634c2012-07-28 11:27:47 -0500186 # Use fixcommitted if there is any
187 set_fix_committed(reltask)
188 break
189 else:
190 # Use tagging if there isn't any
191 tag_in_branchname(bugtask, args.branch)
192
193 add_change_merged_message(bugtask, args.change_url, args.project,
194 args.commit, args.submitter, args.branch,
195 git_log)
196
197 if args.hook == "patchset-created":
198 if args.branch == 'master':
Thierry Carrez8ea78ac2012-11-14 15:47:57 +0100199 if bugtask.status not in [u'Fix Committed', u'Fix Released']:
200 set_in_progress(bugtask, launchpad, args.uploader,
201 args.change_url)
Monty Taylor6c9634c2012-07-28 11:27:47 -0500202 elif args.branch.startswith('stable/'):
203 series = args.branch[7:]
204 for reltask in bugtask.related_tasks:
Thierry Carrez8ea78ac2012-11-14 15:47:57 +0100205 if (reltask.bug_target_name.endswith("/" + series) and
Monty Taylorda3bada2012-11-22 09:38:22 -0800206 reltask.status not in [u'Fix Committed',
207 u'Fix Released']):
Monty Taylor6c9634c2012-07-28 11:27:47 -0500208 set_in_progress(reltask, launchpad,
209 args.uploader, args.change_url)
210 break
211
212 if args.patchset == '1':
213 add_change_proposed_message(bugtask, args.change_url,
214 args.project, args.branch)
215
216
217def find_bugs(launchpad, git_log, args):
218 """Find bugs referenced in the git log and return related bugtasks"""
219
220 bug_regexp = r'([Bb]ug|[Ll][Pp])[\s#:]*(\d+)'
221 tokens = re.split(bug_regexp, git_log)
222
223 # Extract unique bug tasks
224 bugtasks = {}
225 for token in tokens:
226 if re.match('^\d+$', token) and (token not in bugtasks):
227 try:
228 lp_bug = launchpad.bugs[token]
229 for lp_task in lp_bug.bug_tasks:
230 if lp_task.bug_target_name == git2lp(args.project):
231 bugtasks[token] = lp_task
232 break
233 except KeyError:
234 # Unknown bug
235 pass
236
237 return bugtasks.values()
238
239
240def extract_git_log(args):
241 """Extract git log of all merged commits"""
242 cmd = ['git',
243 '--git-dir=' + BASE_DIR + '/git/' + args.project + '.git',
244 'log', '--no-merges', args.commit + '^1..' + args.commit]
245 return subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0]
246
247
248def main():
249 parser = argparse.ArgumentParser()
250 parser.add_argument('hook')
251 #common
252 parser.add_argument('--change', default=None)
253 parser.add_argument('--change-url', default=None)
254 parser.add_argument('--project', default=None)
255 parser.add_argument('--branch', default=None)
256 parser.add_argument('--commit', default=None)
257 #change-merged
258 parser.add_argument('--submitter', default=None)
259 #patchset-created
260 parser.add_argument('--uploader', default=None)
261 parser.add_argument('--patchset', default=None)
262
263 args = parser.parse_args()
264
265 # Connect to Launchpad
266 launchpad = Launchpad.login_with('Gerrit User Sync', LPNET_SERVICE_ROOT,
267 GERRIT_CACHE_DIR,
268 credentials_file=GERRIT_CREDENTIALS,
269 version='devel')
270
271 # Get git log
272 git_log = extract_git_log(args)
273
274 # Process bugtasks found in git log
275 for bugtask in find_bugs(launchpad, git_log, args):
276 process_bugtask(launchpad, bugtask, git_log, args)