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

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

Issue 30048555: Issue 7334 - Remove handling of "Fixes XXXX - ..." commit messages (Closed) Base URL: https://hg.adblockplus.org/sitescripts
Patch Set: Created April 18, 2019, 3:48 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-present eyeo GmbH 2 # Copyright (C) 2006-present 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 """Hooks for integrating Mercurial with Trac. 16 """A changegroup (or pretxnchangegroup) hook for Trac integration.
mathias 2019/05/14 09:32:02 Why do you insist on naming the type(s) of hook?
Vasily Kuznetsov 2019/05/14 10:28:59 This is the intended use and I haven't checked if
17 17
18 Update the issues that are referenced in commit messages when the commits 18 Checks commit messages for issue references and posts comments linking to the
19 are pushed and `master` bookmark is moved. See README.md for more 19 commits into referenced issues.
20 information on behavior and configuration.
21 """ 20 """
22 21
23 import collections 22 import collections
24 import contextlib
25 import posixpath 23 import posixpath
26 import re 24 import re
27 import xmlrpclib 25 import xmlrpclib
28 26
29 from sitescripts.utils import get_config, get_template 27 from sitescripts.utils import get_config, get_template
30 28
31 29
32 _IssueRef = collections.namedtuple('IssueRef', 'id commits is_fixed') 30 ISSUE_NUMBER_REGEX = re.compile(r'\bissue\s+(\d+)\b', re.I)
mathias 2019/05/13 12:23:43 Shouldn't this one include the separating dash? I.
Vasily Kuznetsov 2019/05/13 15:14:15 Yeah I see what you mean, but I'd rather leave it
mathias 2019/05/14 09:32:01 Acknowledged.
33
34 ISSUE_NUMBER_REGEX = re.compile(r'\b(issue|fixes)\s+(\d+)\b', re.I)
35 NOISSUE_REGEX = re.compile(r'^noissue\b', re.I) 31 NOISSUE_REGEX = re.compile(r'^noissue\b', re.I)
36 COMMIT_MESSAGE_REGEX = re.compile(r'\[\S+\ ([^\]]+)\]')
37
38
39 @contextlib.contextmanager
40 def _trac_proxy(ui, config, action_descr):
41 trac_url = config.get('hg', 'trac_xmlrpc_url')
42 try:
43 yield xmlrpclib.ServerProxy(trac_url)
44 except Exception as exc:
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))
49
50
51 def _update_issue(ui, config, issue_id, changes, comment=''):
52 issue_url_template = config.get('hg', 'issue_url_template')
53 issue_url = issue_url_template.format(id=issue_id)
54
55 updates = []
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 ' + changes['milestone'])
61 if changes['action'] == 'resolve':
62 updates.append(' - closed')
63 if not updates:
64 return
65
66 with _trac_proxy(ui, config, 'updating issue {}'.format(issue_id)) as tp:
67 tp.ticket.update(issue_id, comment, changes, True)
68 ui.status('updated {}:\n{}\n'.format(issue_url, '\n'.join(updates)))
69 32
70 33
71 def _format_description(change): 34 def _format_description(change):
72 lines = change.description().splitlines() 35 lines = change.description().splitlines()
73 message = lines[0].rstrip() 36 message = lines[0].rstrip()
74 if len(lines) == 1 or lines[1].strip() == '': 37 if len(lines) == 1 or lines[1].strip() == '':
75 return message 38 return message
76 return message.rstrip('.') + '...' 39 return message.rstrip('.') + '...'
77 40
78 41
79 def _post_comments(ui, repo, config, refs): 42 def _generate_comments(repository_name, changes_by_issue):
80 repo_name = posixpath.split(repo.url())[1] 43 comments = {}
81 template = get_template('hg/template/issue_commit_comment.tmpl', 44 template = get_template('hg/template/issue_commit_comment.tmpl',
82 autoescape=False) 45 autoescape=False)
83 for ref in refs: 46 for issue_id, changes in changes_by_issue.items():
84 comment_text = template.render({ 47 comments[issue_id] = template.render({
85 'repository_name': repo_name, 48 'repository_name': repository_name,
86 'changes': ref.commits, 49 'changes': changes,
87 'format_description': _format_description, 50 'format_description': _format_description,
88 }) 51 })
89 with _trac_proxy(ui, config, 'getting issue {}'.format(ref.id)) as tp: 52 return comments
90 attrs = tp.ticket.get(ref.id)[3]
91 changes = {'_ts': attrs['_ts'], 'action': 'leave'}
92 _update_issue(ui, config, ref.id, changes, comment_text)
93 53
94 54
95 def _compile_module_regexps(ui, config, modules): 55 def _post_comment(issue_id, comment):
96 for module, regexp in config.items('hg_module_milestones'): 56 issue_id = int(issue_id)
97 try: 57 url = get_config().get('hg', 'trac_xmlrpc_url')
98 yield module, re.compile('^{}$'.format(regexp), re.I) 58 server = xmlrpclib.ServerProxy(url)
99 except Exception as exc: 59 attributes = server.ticket.get(issue_id)[3]
100 ui.warn('warning: skipped invalid regexp for module {} in ' 60 server.ticket.update(
101 "[hg_module_milestones] config: '{}' ({})\n" 61 issue_id,
102 .format(module, regexp, exc)) 62 comment,
63 {
64 '_ts': attributes['_ts'],
65 'action': 'leave',
66 },
67 True,
68 )
103 69
104 70
105 def _get_module_milestones(ui, config, modules): 71 def hook(ui, repo, node, **kwargs):
106 module_regexps = dict(_compile_module_regexps(ui, config, modules)) 72 """Post commit references into Trac issues."""
107 modules = module_regexps.keys() 73 changes_by_issue = collections.defaultdict(list)
108 if not modules:
109 return []
110 74
111 milestones_by_module = {} 75 first_rev = repo[node].rev()
112 with _trac_proxy(ui, config, 'getting milestones') as tp: 76 commits = repo[first_rev:]
113 milestone_names = [ 77 for commit in commits:
114 name for name in tp.ticket.milestone.getAll() 78 description = commit.description()
115 if any(regexp.search(name) for regexp in module_regexps.values()) 79 issue_ids = ISSUE_NUMBER_REGEX.findall(description)
116 ] 80 if issue_ids:
117 # Using a MultiCall is better because we might have many milestones. 81 for issue_id in issue_ids:
118 get_milestones = xmlrpclib.MultiCall(tp) 82 changes_by_issue[issue_id].append(commit)
119 for name in milestone_names: 83 elif not NOISSUE_REGEX.search(description):
120 get_milestones.ticket.milestone.get(name) 84 ui.warn('warning: invalid commit message format in changeset {}\n'
121 milestones = [ms for ms in get_milestones() if not ms['completed']] 85 .format(commit))
122 for module in modules:
123 for milestone in milestones:
124 if module_regexps[module].search(milestone['name']):
125 milestones_by_module[module] = milestone['name']
126 break
127 86
128 return milestones_by_module.items() 87 repository_name = posixpath.split(repo.url())[1]
88 comments = _generate_comments(repository_name, changes_by_issue)
129 89
90 issue_url_template = get_config().get('hg', 'issue_url_template')
91 for issue_id, comment in comments.items():
92 issue_url = issue_url_template.format(id=issue_id)
93 try:
94 _post_comment(issue_id, comment)
95 ui.status('updating {}\n'.format(issue_url))
mathias 2019/05/14 09:32:02 This should not be in the `try` block. It should e
Vasily Kuznetsov 2019/05/14 10:28:59 Makes sense. Done.
96 except Exception as exc:
97 ui.warn('warning: failed to update {}\n'.format(issue_url))
98 ui.warn('error message: {}\n'.format(exc))
130 99
131 def _declare_fixed(ui, config, refs): 100 # Alias for smooth migration.
132 updates = [] 101 changegroup_hook = hook
mathias 2019/05/14 09:32:01 This should not be necessary at all: hg@hg-2:
Vasily Kuznetsov 2019/05/14 10:29:00 👍 Done!
133 # Changes that need milestones added to them, indexed by module.
134 need_milestones = collections.defaultdict(list)
135
136 for ref in refs:
137 with _trac_proxy(ui, config, 'getting issue {}'.format(ref.id)) as tp:
138 attrs = tp.ticket.get(ref.id)[3]
139 changes = {
140 '_ts': attrs['_ts'],
141 'action': 'leave',
142 }
143 actions = tp.ticket.getActions(ref.id)
144 if any(action[0] == 'resolve' for action in actions):
145 changes['action'] = 'resolve'
146 if not attrs['milestone']:
147 need_milestones[attrs['component']].append(changes)
148 updates.append((ref.id, changes))
149
150 for module, milestone in _get_module_milestones(ui, config,
151 need_milestones.keys()):
152 for changes in need_milestones[module]:
153 changes['milestone'] = milestone
154
155 for issue_id, changes in updates:
156 _update_issue(ui, config, issue_id, changes)
157
158
159 def _collect_references(ui, commits):
160 commits_by_issue = collections.defaultdict(list)
161 fixed_issues = set()
162
163 for commit in commits:
164 message = commit.description()
165 if ' - ' not in message:
166 ui.warn("warning: invalid commit message format: '{}'\n"
167 .format(message))
168 continue
169
170 refs, rest = message.split(' - ', 1)
171 issue_refs = ISSUE_NUMBER_REGEX.findall(refs)
172 if issue_refs:
173 for ref_type, issue_id in issue_refs:
174 issue_id = int(issue_id)
175 commits_by_issue[issue_id].append(commit)
176 if ref_type.lower() == 'fixes':
177 fixed_issues.add(issue_id)
178 elif not NOISSUE_REGEX.search(refs):
179 ui.warn("warning: no issue reference in commit message: '{}'\n"
180 .format(message))
181
182 for issue_id, commits in sorted(commits_by_issue.items()):
183 yield _IssueRef(issue_id, commits, is_fixed=issue_id in fixed_issues)
184
185
186 def changegroup_hook(ui, repo, node, **kwargs):
187 config = get_config()
188 first_rev = repo[node].rev()
189 pushed_commits = repo[first_rev:]
190 refs = _collect_references(ui, pushed_commits)
191 _post_comments(ui, repo, config, refs)
192
193
194 def pushkey_hook(ui, repo, **kwargs):
195 if (kwargs['namespace'] != 'bookmarks' or # Not a bookmark move.
196 kwargs['key'] != 'master' or # Not `master` bookmark.
197 not kwargs['old']): # The bookmark is just created.
198 return
199
200 config = get_config()
201 old_master_rev = repo[kwargs['old']].rev()
202 new_master_rev = repo[kwargs['new']].rev()
203 added_revs = repo.changelog.findmissingrevs([old_master_rev],
204 [new_master_rev])
205 added_commits = [repo[rev] for rev in added_revs]
206 refs = [ref for ref in _collect_references(ui, added_commits)
207 if ref.is_fixed]
208 _declare_fixed(ui, config, refs)
209
210
211 # Alias for backward compatibility.
212 hook = changegroup_hook
OLDNEW

Powered by Google App Engine
This is Rietveld