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

Delta Between Two Patch Sets: sitescripts/hg/bin/update_issues.py

Issue 29339623: Issue 3681 - Add suport for "Fixes XXXX - ..." commit messages (Closed)
Left Patch Set: Address review comments Created May 2, 2016, 4:46 p.m.
Right Patch Set: Fix Strunk+White violations Created May 18, 2016, 8:29 a.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
Left: Side by side diff | Download
Right: Side by side diff | Download
« no previous file with change/comment | « sitescripts/hg/README.md ('k') | sitescripts/hg/template/issue_commit_comment.tmpl » ('j') | no next file with change/comment »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
LEFTRIGHT
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 """Hooks for integrating Mercurial with Trac. 16 """Hooks for integrating Mercurial with Trac.
17 17
18 Update the issues that are referenced in commit messages when the commits 18 Update the issues that are referenced in commit messages when the commits
19 are pushed and and "master" bookmark is moved. See README.md for more 19 are pushed and `master` bookmark is moved. See README.md for more
20 information on behavior and configuration. 20 information on behavior and configuration.
21 """ 21 """
22 22
23 import collections 23 import collections
24 import contextlib 24 import contextlib
25 import posixpath 25 import posixpath
26 import re 26 import re
27 import xmlrpclib 27 import xmlrpclib
28 28
29 from sitescripts.utils import get_config, get_template 29 from sitescripts.utils import get_config, get_template
30 30
31 31
32 _IssueRef = collections.namedtuple('IssueRef', 'id commits is_fixed') 32 _IssueRef = collections.namedtuple('IssueRef', 'id commits is_fixed')
33 33
34 ISSUE_NUMBER_REGEX = re.compile(r'\b(issue|fixes)\s+(\d+)\b', re.I) 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) 35 NOISSUE_REGEX = re.compile(r'^noissue\b', re.I)
36 COMMIT_MESSAGE_REGEX = re.compile('\[\S+\ ([^\]]+)\]') 36 COMMIT_MESSAGE_REGEX = re.compile(r'\[\S+\ ([^\]]+)\]')
37 37
38 38
39 @contextlib.contextmanager 39 @contextlib.contextmanager
40 def _trac_proxy(ui, config, action_descr): 40 def _trac_proxy(ui, config, action_descr):
41 trac_url = config.get('hg', 'trac_xmlrpc_url') 41 trac_url = config.get('hg', 'trac_xmlrpc_url')
42 try: 42 try:
43 yield xmlrpclib.ServerProxy(trac_url) 43 yield xmlrpclib.ServerProxy(trac_url)
44 except Exception as exc: 44 except Exception as exc:
45 if getattr(exc, 'faultCode', 0) == 404: 45 if getattr(exc, 'faultCode', 0) == 404:
46 ui.warn('warning: 404 (not found) while {}\n'.format(action_descr)) 46 ui.warn('warning: 404 (not found) while {}\n'.format(action_descr))
47 else: 47 else:
48 ui.warn('error: {} while {}\n'.format(exc, action_descr)) 48 ui.warn('error: {} while {}\n'.format(exc, action_descr))
49 49
50 50
51 def _update_issue(ui, config, issue_id, changes, comment=''): 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.
52 issue_url_template = config.get('hg', 'issue_url_template') 52 issue_url_template = config.get('hg', 'issue_url_template')
53 issue_url = issue_url_template.format(id=issue_id) 53 issue_url = issue_url_template.format(id=issue_id)
54 54
55 updates = [] 55 updates = []
56 if comment: 56 if comment:
57 for message in COMMIT_MESSAGE_REGEX.findall(comment): 57 for message in COMMIT_MESSAGE_REGEX.findall(comment):
58 updates.append(' - referenced a commit: ' + message) 58 updates.append(' - referenced a commit: ' + message)
59 if 'milestone' in changes: 59 if 'milestone' in changes:
60 updates.append(' - set milestone to {}'.format(changes['milestone'])) 60 updates.append(' - set milestone to ' + 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': 61 if changes['action'] == 'resolve':
62 updates.append(' - closed') 62 updates.append(' - closed')
63 if not updates: 63 if not updates:
64 return 64 return
65 65
66 with _trac_proxy(ui, config, 'updating issue {}'.format(issue_id)) as tp: 66 with _trac_proxy(ui, config, 'updating issue {}'.format(issue_id)) as tp:
67 tp.ticket.update(issue_id, comment, changes, True) 67 tp.ticket.update(issue_id, comment, changes, True)
68 ui.status('updated {}:\n{}\n'.format(issue_url, '\n'.join(updates))) 68 ui.status('updated {}:\n{}\n'.format(issue_url, '\n'.join(updates)))
69 69
70 70
71 def _post_comments(ui, repo, config, refs): 71 def _post_comments(ui, repo, config, refs):
72 repo_name = posixpath.split(repo.url())[1] 72 repo_name = posixpath.split(repo.url())[1]
73 template = get_template('hg/template/issue_commit_comment.tmpl', 73 template = get_template('hg/template/issue_commit_comment.tmpl',
74 autoescape=False) 74 autoescape=False)
75 for ref in refs: 75 for ref in refs:
76 comment_text = template.render({'repository_name': repo_name, 76 comment_text = template.render({'repository_name': repo_name,
77 'changes': ref.commits}) 77 'changes': ref.commits})
78 with _trac_proxy(ui, config, 'getting issue {}'.format(ref.id)) as tp: 78 with _trac_proxy(ui, config, 'getting issue {}'.format(ref.id)) as tp:
79 attrs = tp.ticket.get(ref.id)[3] 79 attrs = tp.ticket.get(ref.id)[3]
80 changes = {'_ts': attrs['_ts'], 'action': 'leave'} 80 changes = {'_ts': attrs['_ts'], 'action': 'leave'}
81 _update_issue(ui, config, ref.id, changes, comment_text) 81 _update_issue(ui, config, ref.id, changes, comment_text)
82 82
83 83
84 def _compile_module_regexps(ui, config, modules): 84 def _compile_module_regexps(ui, config, modules):
85 for module, regexp in config.items('hg_module_milestones'): 85 for module, regexp in config.items('hg_module_milestones'):
86 try: 86 try:
87 yield module, re.compile('^' + regexp + '$', re.I) 87 yield module, re.compile('^{}$'.format(regexp), re.I)
88 except Exception as exc: 88 except Exception as exc:
89 ui.warn('warning: skipped invalid regexp for module {} in ' 89 ui.warn('warning: skipped invalid regexp for module {} in '
90 "[hg_module_milestones] config: '{}' ({})\n" 90 "[hg_module_milestones] config: '{}' ({})\n"
91 .format(module, regexp, exc)) 91 .format(module, regexp, exc))
92 92
93 93
94 def _get_module_milestones(ui, config, modules): 94 def _get_module_milestones(ui, config, modules):
95 module_regexps = dict(_compile_module_regexps(ui, config, modules)) 95 module_regexps = dict(_compile_module_regexps(ui, config, modules))
96 modules = module_regexps.keys() 96 modules = module_regexps.keys()
97 if not modules: 97 if not modules:
(...skipping 51 matching lines...) Expand 10 before | Expand all | Expand 10 after
149 commits_by_issue = collections.defaultdict(list) 149 commits_by_issue = collections.defaultdict(list)
150 fixed_issues = set() 150 fixed_issues = set()
151 151
152 for commit in commits: 152 for commit in commits:
153 message = commit.description() 153 message = commit.description()
154 if ' - ' not in message: 154 if ' - ' not in message:
155 ui.warn("warning: invalid commit message format: '{}'\n" 155 ui.warn("warning: invalid commit message format: '{}'\n"
156 .format(message)) 156 .format(message))
157 continue 157 continue
158 158
159 refs, rest = message.split(" - ", 1) 159 refs, rest = message.split(' - ', 1)
160 issue_refs = ISSUE_NUMBER_REGEX.findall(refs) 160 issue_refs = ISSUE_NUMBER_REGEX.findall(refs)
161 if issue_refs: 161 if issue_refs:
162 for ref_type, issue_id in issue_refs: 162 for ref_type, issue_id in issue_refs:
163 issue_id = int(issue_id) 163 issue_id = int(issue_id)
164 commits_by_issue[issue_id].append(commit) 164 commits_by_issue[issue_id].append(commit)
165 if ref_type.lower() == 'fixes': 165 if ref_type.lower() == 'fixes':
166 fixed_issues.add(issue_id) 166 fixed_issues.add(issue_id)
167 elif not NOISSUE_REGEX.search(refs): 167 elif not NOISSUE_REGEX.search(refs):
168 ui.warn("warning: no issue reference in commit message: '{}'\n" 168 ui.warn("warning: no issue reference in commit message: '{}'\n"
169 .format(message)) 169 .format(message))
170 170
171 for issue_id, commits in sorted(commits_by_issue.items()): 171 for issue_id, commits in sorted(commits_by_issue.items()):
172 yield _IssueRef(issue_id, commits, is_fixed=issue_id in fixed_issues) 172 yield _IssueRef(issue_id, commits, is_fixed=issue_id in fixed_issues)
173 173
174 174
175 def changegroup_hook(ui, repo, node, **kwargs): 175 def changegroup_hook(ui, repo, node, **kwargs):
176 config = get_config() 176 config = get_config()
177 first_rev = repo[node].rev() 177 first_rev = repo[node].rev()
178 pushed_commits = repo[first_rev:] 178 pushed_commits = repo[first_rev:]
179 refs = _collect_references(ui, pushed_commits) 179 refs = _collect_references(ui, pushed_commits)
180 _post_comments(ui, repo, config, refs) 180 _post_comments(ui, repo, config, refs)
181 181
182 182
183 def pushkey_hook(ui, repo, **kwargs): 183 def pushkey_hook(ui, repo, **kwargs):
184 if (kwargs['namespace'] != 'bookmarks' or # Not a bookmark move. 184 if (kwargs['namespace'] != 'bookmarks' or # Not a bookmark move.
185 kwargs['key'] != 'master' or # Not master bookmark. 185 kwargs['key'] != 'master' or # Not `master` bookmark.
186 not kwargs['old']): # The bookmark is just created. 186 not kwargs['old']): # The bookmark is just created.
187 return 187 return
188 188
189 config = get_config() 189 config = get_config()
190 old_master_rev = repo[kwargs['old']].rev() 190 old_master_rev = repo[kwargs['old']].rev()
191 new_master_rev = repo[kwargs['new']].rev() 191 new_master_rev = repo[kwargs['new']].rev()
192 added_revs = repo.changelog.findmissingrevs([old_master_rev], 192 added_revs = repo.changelog.findmissingrevs([old_master_rev],
193 [new_master_rev]) 193 [new_master_rev])
194 added_commits = [repo[rev] for rev in added_revs] 194 added_commits = [repo[rev] for rev in added_revs]
195 refs = [ref for ref in _collect_references(ui, added_commits) 195 refs = [ref for ref in _collect_references(ui, added_commits)
196 if ref.is_fixed] 196 if ref.is_fixed]
197 _declare_fixed(ui, config, refs) 197 _declare_fixed(ui, config, refs)
198 198
199 199
200 # Alias for backward compatibility. 200 # Alias for backward compatibility.
201 hook = changegroup_hook 201 hook = changegroup_hook
LEFTRIGHT

Powered by Google App Engine
This is Rietveld