| Monty Taylor | f45f6ca | 2012-05-01 17:11:48 -0400 | [diff] [blame] | 1 | #!/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 |  | 
|  | 20 | from launchpadlib.launchpad import Launchpad | 
|  | 21 | from launchpadlib.uris import LPNET_SERVICE_ROOT | 
|  | 22 | import os | 
|  | 23 | import argparse | 
|  | 24 | import re | 
|  | 25 | import subprocess | 
|  | 26 |  | 
|  | 27 |  | 
|  | 28 | BASE_DIR = '/home/gerrit2/review_site' | 
|  | 29 | GERRIT_CACHE_DIR = os.path.expanduser(os.environ.get('GERRIT_CACHE_DIR', | 
|  | 30 | '~/.launchpadlib/cache')) | 
|  | 31 | GERRIT_CREDENTIALS = os.path.expanduser(os.environ.get('GERRIT_CREDENTIALS', | 
|  | 32 | '~/.launchpadlib/creds')) | 
|  | 33 |  | 
|  | 34 |  | 
|  | 35 | def add_change_proposed_message(bugtask, change_url, project, branch): | 
|  | 36 | subject = 'Fix proposed to %s (%s)' % (short_project(project), branch) | 
|  | 37 | body = 'Fix proposed to branch: %s\nReview: %s' % (branch, change_url) | 
|  | 38 | bugtask.bug.newMessage(subject=subject, content=body) | 
|  | 39 |  | 
|  | 40 |  | 
|  | 41 | def add_change_merged_message(bugtask, change_url, project, commit, | 
|  | 42 | submitter, branch, git_log): | 
|  | 43 | subject = 'Fix merged to %s (%s)' % (short_project(project), branch) | 
|  | 44 | git_url = 'http://github.com/%s/commit/%s' % (project, commit) | 
|  | 45 | body = '''Reviewed:  %s | 
|  | 46 | Committed: %s | 
|  | 47 | Submitter: %s | 
|  | 48 | Branch:    %s\n''' % (change_url, git_url, submitter, branch) | 
|  | 49 | body = body + '\n' + git_log | 
|  | 50 | bugtask.bug.newMessage(subject=subject, content=body) | 
|  | 51 |  | 
|  | 52 |  | 
|  | 53 | def set_in_progress(bugtask, launchpad, uploader, change_url): | 
|  | 54 | """Set bug In progress with assignee being the uploader""" | 
|  | 55 |  | 
|  | 56 | # Retrieve uploader from Launchpad. Use email as search key if | 
|  | 57 | # provided, and only set if there is a clear match. | 
|  | 58 | try: | 
|  | 59 | searchkey = uploader[uploader.rindex("(") + 1:-1] | 
|  | 60 | except ValueError: | 
|  | 61 | searchkey = uploader | 
|  | 62 | persons = launchpad.people.findPerson(text=searchkey) | 
|  | 63 | if len(persons) == 1: | 
|  | 64 | bugtask.assignee = persons[0] | 
|  | 65 |  | 
|  | 66 | bugtask.status = "In Progress" | 
|  | 67 | bugtask.lp_save() | 
|  | 68 |  | 
|  | 69 |  | 
|  | 70 | def set_fix_committed(bugtask): | 
|  | 71 | """Set bug fix committed""" | 
|  | 72 |  | 
|  | 73 | bugtask.status = "Fix Committed" | 
|  | 74 | bugtask.lp_save() | 
|  | 75 |  | 
|  | 76 |  | 
| Monty Taylor | 962d8ea | 2012-05-19 10:04:01 -0400 | [diff] [blame] | 77 | def set_fix_released(bugtask): | 
|  | 78 | """Set bug fix released""" | 
|  | 79 |  | 
|  | 80 | bugtask.status = "Fix Released" | 
|  | 81 | bugtask.lp_save() | 
|  | 82 |  | 
|  | 83 |  | 
| Monty Taylor | f45f6ca | 2012-05-01 17:11:48 -0400 | [diff] [blame] | 84 | def release_fixcommitted(bugtask): | 
|  | 85 | """Set bug FixReleased if it was FixCommitted""" | 
|  | 86 |  | 
|  | 87 | if bugtask.status == u'Fix Committed': | 
| Monty Taylor | 962d8ea | 2012-05-19 10:04:01 -0400 | [diff] [blame] | 88 | set_fix_released(bugtask) | 
| Monty Taylor | f45f6ca | 2012-05-01 17:11:48 -0400 | [diff] [blame] | 89 |  | 
|  | 90 |  | 
|  | 91 | def tag_in_branchname(bugtask, branch): | 
|  | 92 | """Tag bug with in-branch-name tag (if name is appropriate)""" | 
|  | 93 |  | 
|  | 94 | lp_bug = bugtask.bug | 
|  | 95 | branch_name = branch.replace('/', '-') | 
|  | 96 | if branch_name.replace('-', '').isalnum(): | 
|  | 97 | lp_bug.tags = lp_bug.tags + ["in-%s" % branch_name] | 
|  | 98 | lp_bug.tags.append("in-%s" % branch_name) | 
|  | 99 | lp_bug.lp_save() | 
|  | 100 |  | 
|  | 101 |  | 
|  | 102 | def short_project(full_project_name): | 
|  | 103 | """Return the project part of the git repository name""" | 
|  | 104 | return full_project_name.split('/')[-1] | 
|  | 105 |  | 
|  | 106 |  | 
|  | 107 | def git2lp(full_project_name): | 
|  | 108 | """Convert Git repo name to Launchpad project""" | 
|  | 109 | project_map = { | 
| Monty Taylor | de5f3f8 | 2012-05-03 13:11:53 -0700 | [diff] [blame] | 110 | 'openstack/python-cinderclient': 'cinder', | 
| Monty Taylor | f45f6ca | 2012-05-01 17:11:48 -0400 | [diff] [blame] | 111 | 'openstack/python-glanceclient': 'glance', | 
|  | 112 | 'openstack/python-keystoneclient': 'keystone', | 
|  | 113 | 'openstack/python-melangeclient': 'melange', | 
|  | 114 | 'openstack/python-novaclient': 'nova', | 
|  | 115 | 'openstack/python-quantumclient': 'quantum', | 
|  | 116 | 'openstack/openstack-ci-puppet': 'openstack-ci', | 
|  | 117 | 'openstack-ci/devstack-gate': 'openstack-ci', | 
| Monty Taylor | 144a722 | 2012-06-10 15:33:11 -0400 | [diff] [blame^] | 118 | 'openstack-ci/gerrit': 'openstack-ci', | 
| Monty Taylor | 962d8ea | 2012-05-19 10:04:01 -0400 | [diff] [blame] | 119 | 'openstack-ci/lodgeit': 'openstack-ci', | 
|  | 120 | 'openstack-ci/meetbot': 'openstack-ci', | 
| Monty Taylor | f45f6ca | 2012-05-01 17:11:48 -0400 | [diff] [blame] | 121 | } | 
|  | 122 | return project_map.get(full_project_name, short_project(full_project_name)) | 
|  | 123 |  | 
|  | 124 |  | 
| Monty Taylor | 962d8ea | 2012-05-19 10:04:01 -0400 | [diff] [blame] | 125 | def is_direct_release(full_project_name): | 
|  | 126 | """Test against a list of projects who directly release changes.""" | 
| Thierry Carrez | 617b8d7 | 2012-05-23 14:18:43 +0200 | [diff] [blame] | 127 | return full_project_name in [ | 
| Monty Taylor | 962d8ea | 2012-05-19 10:04:01 -0400 | [diff] [blame] | 128 | 'openstack-ci/devstack-gate', | 
|  | 129 | 'openstack-ci/lodgeit', | 
|  | 130 | 'openstack-ci/meetbot', | 
|  | 131 | 'openstack-dev/devstack', | 
|  | 132 | 'openstack/openstack-ci', | 
|  | 133 | 'openstack/openstack-ci-puppet', | 
|  | 134 | 'openstack/openstack-manuals', | 
|  | 135 | ] | 
|  | 136 |  | 
|  | 137 |  | 
| Monty Taylor | f45f6ca | 2012-05-01 17:11:48 -0400 | [diff] [blame] | 138 | def process_bugtask(launchpad, bugtask, git_log, args): | 
|  | 139 | """Apply changes to bugtask, based on hook / branch...""" | 
|  | 140 |  | 
|  | 141 | if args.hook == "change-merged": | 
|  | 142 | if args.branch == 'master': | 
| Monty Taylor | 962d8ea | 2012-05-19 10:04:01 -0400 | [diff] [blame] | 143 | if is_direct_release(args.project): | 
|  | 144 | set_fix_released(bugtask) | 
|  | 145 | else: | 
|  | 146 | set_fix_committed(bugtask) | 
| Monty Taylor | f45f6ca | 2012-05-01 17:11:48 -0400 | [diff] [blame] | 147 | elif args.branch == 'milestone-proposed': | 
|  | 148 | release_fixcommitted(bugtask) | 
| Thierry Carrez | 7b961cb | 2012-05-23 16:48:26 +0200 | [diff] [blame] | 149 | elif args.branch.startswith('stable/'): | 
|  | 150 | series = args.branch[7:] | 
|  | 151 | # Look for a related task matching the series | 
|  | 152 | for reltask in bugtask.related_tasks: | 
|  | 153 | if reltask.bug_target_name.endswith("/" + series): | 
|  | 154 | # Use fixcommitted if there is any | 
|  | 155 | set_fix_committed(reltask) | 
|  | 156 | break | 
|  | 157 | else: | 
|  | 158 | # Use tagging if there isn't any | 
|  | 159 | tag_in_branchname(bugtask, args.branch) | 
|  | 160 |  | 
| Monty Taylor | f45f6ca | 2012-05-01 17:11:48 -0400 | [diff] [blame] | 161 | add_change_merged_message(bugtask, args.change_url, args.project, | 
|  | 162 | args.commit, args.submitter, args.branch, | 
|  | 163 | git_log) | 
|  | 164 |  | 
|  | 165 | if args.hook == "patchset-created": | 
|  | 166 | if args.branch == 'master': | 
|  | 167 | set_in_progress(bugtask, launchpad, args.uploader, args.change_url) | 
| Thierry Carrez | 7b961cb | 2012-05-23 16:48:26 +0200 | [diff] [blame] | 168 | elif args.branch.startswith('stable/'): | 
|  | 169 | series = args.branch[7:] | 
|  | 170 | for reltask in bugtask.related_tasks: | 
|  | 171 | if reltask.bug_target_name.endswith("/" + series): | 
|  | 172 | set_in_progress(reltask, launchpad, | 
|  | 173 | args.uploader, args.change_url) | 
|  | 174 | break | 
|  | 175 |  | 
| Monty Taylor | f45f6ca | 2012-05-01 17:11:48 -0400 | [diff] [blame] | 176 | if args.patchset == '1': | 
|  | 177 | add_change_proposed_message(bugtask, args.change_url, | 
|  | 178 | args.project, args.branch) | 
|  | 179 |  | 
|  | 180 |  | 
|  | 181 | def find_bugs(launchpad, git_log, args): | 
|  | 182 | """Find bugs referenced in the git log and return related bugtasks""" | 
|  | 183 |  | 
|  | 184 | bug_regexp = r'([Bb]ug|[Ll][Pp])[\s#:]*(\d+)' | 
|  | 185 | tokens = re.split(bug_regexp, git_log) | 
|  | 186 |  | 
|  | 187 | # Extract unique bug tasks | 
|  | 188 | bugtasks = {} | 
|  | 189 | for token in tokens: | 
|  | 190 | if re.match('^\d+$', token) and (token not in bugtasks): | 
|  | 191 | try: | 
|  | 192 | lp_bug = launchpad.bugs[token] | 
|  | 193 | for lp_task in lp_bug.bug_tasks: | 
|  | 194 | if lp_task.bug_target_name == git2lp(args.project): | 
|  | 195 | bugtasks[token] = lp_task | 
|  | 196 | break | 
|  | 197 | except KeyError: | 
|  | 198 | # Unknown bug | 
|  | 199 | pass | 
|  | 200 |  | 
|  | 201 | return bugtasks.values() | 
|  | 202 |  | 
|  | 203 |  | 
|  | 204 | def extract_git_log(args): | 
|  | 205 | """Extract git log of all merged commits""" | 
|  | 206 | cmd = ['git', | 
|  | 207 | '--git-dir=' + BASE_DIR + '/git/' + args.project + '.git', | 
|  | 208 | 'log', '--no-merges', args.commit + '^1..' + args.commit] | 
|  | 209 | return subprocess.Popen(cmd, stdout=subprocess.PIPE).communicate()[0] | 
|  | 210 |  | 
|  | 211 |  | 
|  | 212 | def main(): | 
|  | 213 | parser = argparse.ArgumentParser() | 
|  | 214 | parser.add_argument('hook') | 
|  | 215 | #common | 
|  | 216 | parser.add_argument('--change', default=None) | 
|  | 217 | parser.add_argument('--change-url', default=None) | 
|  | 218 | parser.add_argument('--project', default=None) | 
|  | 219 | parser.add_argument('--branch', default=None) | 
|  | 220 | parser.add_argument('--commit', default=None) | 
|  | 221 | #change-merged | 
|  | 222 | parser.add_argument('--submitter', default=None) | 
|  | 223 | #patchset-created | 
|  | 224 | parser.add_argument('--uploader', default=None) | 
|  | 225 | parser.add_argument('--patchset', default=None) | 
|  | 226 |  | 
|  | 227 | args = parser.parse_args() | 
|  | 228 |  | 
|  | 229 | # Connect to Launchpad | 
|  | 230 | launchpad = Launchpad.login_with('Gerrit User Sync', LPNET_SERVICE_ROOT, | 
|  | 231 | GERRIT_CACHE_DIR, | 
|  | 232 | credentials_file=GERRIT_CREDENTIALS, | 
|  | 233 | version='devel') | 
|  | 234 |  | 
|  | 235 | # Get git log | 
|  | 236 | git_log = extract_git_log(args) | 
|  | 237 |  | 
|  | 238 | # Process bugtasks found in git log | 
|  | 239 | for bugtask in find_bugs(launchpad, git_log, args): | 
|  | 240 | process_bugtask(launchpad, bugtask, git_log, args) | 
|  | 241 |  | 
|  | 242 |  | 
|  | 243 | if __name__ == '__main__': | 
|  | 244 | main() |