Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Side by Side Diff: sitescripts/hg/bin/update_issues.py

Issue 29339623: Issue 3681 - Add suport for "Fixes XXXX - ..." commit messages (Closed)
Patch Set: Address review comments Created May 2, 2016, 4:46 p.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
OLDNEW
1 # This file is part of Adblock Plus <https://adblockplus.org/>, 1 # This file is part of Adblock Plus <https://adblockplus.org/>,
2 # Copyright (C) 2006-2016 Eyeo GmbH 2 # Copyright (C) 2006-2016 Eyeo GmbH
3 # 3 #
4 # Adblock Plus is free software: you can redistribute it and/or modify 4 # Adblock Plus is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License version 3 as 5 # it under the terms of the GNU General Public License version 3 as
6 # published by the Free Software Foundation. 6 # published by the Free Software Foundation.
7 # 7 #
8 # Adblock Plus is distributed in the hope that it will be useful, 8 # Adblock Plus is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details. 11 # GNU General Public License for more details.
12 # 12 #
13 # You should have received a copy of the GNU General Public License 13 # You should have received a copy of the GNU General Public License
14 # along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. 14 # along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
15 15
16 """ 16 """Hooks for integrating Mercurial with Trac.
17 This module implements a changegroup (or pretxnchangegroup) hook that inspects 17
18 all commit messages and checks if issues from the Adblock Plus issue tracker are 18 Update the issues that are referenced in commit messages when the commits
19 being referenced. If there are, it updates them with the respective changeset 19 are pushed and and "master" bookmark is moved. See README.md for more
20 URLs. 20 information on behavior and configuration.
21 """ 21 """
22 22
23 import collections
24 import contextlib
23 import posixpath 25 import posixpath
24 import re 26 import re
25 import xmlrpclib 27 import xmlrpclib
26 28
27 from sitescripts.utils import get_config, get_template 29 from sitescripts.utils import get_config, get_template
28 30
29 31
30 def _generate_comments(repository_name, changes_by_issue): 32 _IssueRef = collections.namedtuple('IssueRef', 'id commits is_fixed')
31 comments = {} 33
32 template = get_template("hg/template/issue_commit_comment.tmpl", 34 ISSUE_NUMBER_REGEX = re.compile(r'\b(issue|fixes)\s+(\d+)\b', re.I)
33 autoescape=False) 35 NOISSUE_REGEX = re.compile(r'^noissue\b', re.I)
34 for issue_id, changes in changes_by_issue.iteritems(): 36 COMMIT_MESSAGE_REGEX = re.compile('\[\S+\ ([^\]]+)\]')
35 comments[issue_id] = template.render({"repository_name": repository_name ,
36 "changes": changes})
37 return comments
38 37
39 38
40 def _post_comment(issue_id, comment): 39 @contextlib.contextmanager
41 issue_id = int(issue_id) 40 def _trac_proxy(ui, config, action_descr):
42 url = get_config().get("hg", "trac_xmlrpc_url") 41 trac_url = config.get('hg', 'trac_xmlrpc_url')
43 server = xmlrpclib.ServerProxy(url) 42 try:
44 attributes = server.ticket.get(issue_id)[3] 43 yield xmlrpclib.ServerProxy(trac_url)
45 server.ticket.update(issue_id, comment, 44 except Exception as exc:
46 {"_ts": attributes["_ts"], "action": "leave"}, True) 45 if getattr(exc, 'faultCode', 0) == 404:
46 ui.warn('warning: 404 (not found) while {}\n'.format(action_descr))
47 else:
48 ui.warn('error: {} while {}\n'.format(exc, action_descr))
47 49
48 50
49 def hook(ui, repo, node, **kwargs): 51 def _update_issue(ui, config, issue_id, changes, comment=''):
kzar 2016/05/03 06:47:36 Nit: Seems weird to have a blank string mean there
Vasily Kuznetsov 2016/05/03 10:11:02 `comment` is passed to `ticket.update` at the end
kzar 2016/05/03 10:13:49 Ah, missed that. Fair enough.
50 first_change = repo[node] 52 issue_url_template = config.get('hg', 'issue_url_template')
51 issue_number_regex = re.compile(r"\bissue\s+(\d+)\b", re.I) 53 issue_url = issue_url_template.format(id=issue_id)
52 noissue_regex = re.compile(r"^noissue\b", re.I)
53 changes_by_issue = {}
54 for revision in xrange(first_change.rev(), len(repo)):
55 change = repo[revision]
56 description = change.description()
57 issue_ids = issue_number_regex.findall(description)
58 if issue_ids:
59 for issue_id in issue_ids:
60 changes_by_issue.setdefault(issue_id, []).append(change)
61 elif not noissue_regex.search(description):
62 # We should just reject all changes when one of them has an invalid
63 # commit message format, see: https://issues.adblockplus.org/ticket/ 3679
64 ui.warn("warning: invalid commit message format in changeset %s\n" %
65 change)
66 54
67 repository_name = posixpath.split(repo.url())[1] 55 updates = []
68 comments = _generate_comments(repository_name, changes_by_issue) 56 if comment:
57 for message in COMMIT_MESSAGE_REGEX.findall(comment):
58 updates.append(' - referenced a commit: ' + message)
59 if 'milestone' in changes:
60 updates.append(' - set milestone to {}'.format(changes['milestone']))
kzar 2016/05/03 06:47:36 Nit: Seems inconsistent with above where you just
Vasily Kuznetsov 2016/05/03 10:11:02 Yep. The style guide now recommends using + in thi
61 if changes['action'] == 'resolve':
62 updates.append(' - closed')
63 if not updates:
64 return
69 65
70 issue_url_template = get_config().get("hg", "issue_url_template") 66 with _trac_proxy(ui, config, 'updating issue {}'.format(issue_id)) as tp:
71 for issue_id, comment in comments.iteritems(): 67 tp.ticket.update(issue_id, comment, changes, True)
68 ui.status('updated {}:\n{}\n'.format(issue_url, '\n'.join(updates)))
69
70
71 def _post_comments(ui, repo, config, refs):
72 repo_name = posixpath.split(repo.url())[1]
73 template = get_template('hg/template/issue_commit_comment.tmpl',
74 autoescape=False)
75 for ref in refs:
76 comment_text = template.render({'repository_name': repo_name,
77 'changes': ref.commits})
78 with _trac_proxy(ui, config, 'getting issue {}'.format(ref.id)) as tp:
79 attrs = tp.ticket.get(ref.id)[3]
80 changes = {'_ts': attrs['_ts'], 'action': 'leave'}
81 _update_issue(ui, config, ref.id, changes, comment_text)
82
83
84 def _compile_module_regexps(ui, config, modules):
85 for module, regexp in config.items('hg_module_milestones'):
72 try: 86 try:
73 _post_comment(issue_id, comment) 87 yield module, re.compile('^' + regexp + '$', re.I)
74 ui.status("updating %s\n" % issue_url_template.format(id=issue_id)) 88 except Exception as exc:
75 except: 89 ui.warn('warning: skipped invalid regexp for module {} in '
76 ui.warn("warning: failed to update %s\n" % 90 "[hg_module_milestones] config: '{}' ({})\n"
77 issue_url_template.format(id=issue_id)) 91 .format(module, regexp, exc))
92
93
94 def _get_module_milestones(ui, config, modules):
95 module_regexps = dict(_compile_module_regexps(ui, config, modules))
96 modules = module_regexps.keys()
97 if not modules:
98 return []
99
100 milestones_by_module = {}
101 with _trac_proxy(ui, config, 'getting milestones') as tp:
102 milestone_names = [
103 name for name in tp.ticket.milestone.getAll()
104 if any(regexp.search(name) for regexp in module_regexps.values())
105 ]
106 # Using a MultiCall is better because we might have many milestones.
107 get_milestones = xmlrpclib.MultiCall(tp)
108 for name in milestone_names:
109 get_milestones.ticket.milestone.get(name)
110 milestones = [ms for ms in get_milestones() if not ms['completed']]
111 for module in modules:
112 for milestone in milestones:
113 if module_regexps[module].search(milestone['name']):
114 milestones_by_module[module] = milestone['name']
115 break
116
117 return milestones_by_module.items()
118
119
120 def _declare_fixed(ui, config, refs):
121 updates = []
122 # Changes that need milestones added to them, indexed by module.
123 need_milestones = collections.defaultdict(list)
124
125 for ref in refs:
126 with _trac_proxy(ui, config, 'getting issue {}'.format(ref.id)) as tp:
127 attrs = tp.ticket.get(ref.id)[3]
128 changes = {
129 '_ts': attrs['_ts'],
130 'action': 'leave'
131 }
132 actions = tp.ticket.getActions(ref.id)
133 if any(action[0] == 'resolve' for action in actions):
134 changes['action'] = 'resolve'
135 if not attrs['milestone']:
136 need_milestones[attrs['component']].append(changes)
137 updates.append((ref.id, changes))
138
139 for module, milestone in _get_module_milestones(ui, config,
140 need_milestones.keys()):
141 for changes in need_milestones[module]:
142 changes['milestone'] = milestone
143
144 for issue_id, changes in updates:
145 _update_issue(ui, config, issue_id, changes)
146
147
148 def _collect_references(ui, commits):
149 commits_by_issue = collections.defaultdict(list)
150 fixed_issues = set()
151
152 for commit in commits:
153 message = commit.description()
154 if ' - ' not in message:
155 ui.warn("warning: invalid commit message format: '{}'\n"
156 .format(message))
157 continue
158
159 refs, rest = message.split(" - ", 1)
160 issue_refs = ISSUE_NUMBER_REGEX.findall(refs)
161 if issue_refs:
162 for ref_type, issue_id in issue_refs:
163 issue_id = int(issue_id)
164 commits_by_issue[issue_id].append(commit)
165 if ref_type.lower() == 'fixes':
166 fixed_issues.add(issue_id)
167 elif not NOISSUE_REGEX.search(refs):
168 ui.warn("warning: no issue reference in commit message: '{}'\n"
169 .format(message))
170
171 for issue_id, commits in sorted(commits_by_issue.items()):
172 yield _IssueRef(issue_id, commits, is_fixed=issue_id in fixed_issues)
173
174
175 def changegroup_hook(ui, repo, node, **kwargs):
176 config = get_config()
177 first_rev = repo[node].rev()
178 pushed_commits = repo[first_rev:]
179 refs = _collect_references(ui, pushed_commits)
180 _post_comments(ui, repo, config, refs)
181
182
183 def pushkey_hook(ui, repo, **kwargs):
184 if (kwargs['namespace'] != 'bookmarks' or # Not a bookmark move.
185 kwargs['key'] != 'master' or # Not master bookmark.
186 not kwargs['old']): # The bookmark is just created.
187 return
188
189 config = get_config()
190 old_master_rev = repo[kwargs['old']].rev()
191 new_master_rev = repo[kwargs['new']].rev()
192 added_revs = repo.changelog.findmissingrevs([old_master_rev],
193 [new_master_rev])
194 added_commits = [repo[rev] for rev in added_revs]
195 refs = [ref for ref in _collect_references(ui, added_commits)
196 if ref.is_fixed]
197 _declare_fixed(ui, config, refs)
198
199
200 # Alias for backward compatibility.
201 hook = changegroup_hook
OLDNEW

Powered by Google App Engine
This is Rietveld