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: Created April 8, 2016, 7:32 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
« no previous file with comments | « .sitescripts.example ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 #!/usr/bin/env python
2
3 # This file is part of Adblock Plus <https://adblockplus.org/>, 1 # This file is part of Adblock Plus <https://adblockplus.org/>,
4 # Copyright (C) 2006-2016 Eyeo GmbH 2 # Copyright (C) 2006-2016 Eyeo GmbH
5 # 3 #
6 # 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
7 # 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
8 # published by the Free Software Foundation. 6 # published by the Free Software Foundation.
9 # 7 #
10 # 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,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details. 11 # GNU General Public License for more details.
14 # 12 #
15 # 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
16 # 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/>.
17 15
18 """ 16 """Hook for integrating Mercurial with Trac.
kzar 2016/04/18 14:48:30 Mind putting this documentation in a README (sites
Vasily Kuznetsov 2016/04/24 21:49:50 Done.
19 This module implements a changegroup (or pretxnchangegroup) hook that inspects 17
20 all commit messages and checks if issues from the Adblock Plus issue tracker are 18 The function called `hook` in this module should be installed as `changegroup`
21 being referenced. If there are, it updates them with the respective changeset 19 or `pretxnchangegroup` in target Mercurial repositories. It will recognise
22 URLs. 20 issue references in commit messages and update referenced issues in Adblock
21 Plus issue tracker.
22
23 The canonical format of the commit messages is "ISSUE-REFERENCE - MESSAGE"
24 where ISSUE-REFERENCE is one of "Noissue", "Issue NUMBER" or "Fixes NUMBER".
25 Several "Issue" and "Fixes" references can be present in the same commit
26 message. Such commit will affect all the referenced issues.
27
28 For "Issue" references a comment is posted into the referenced issue informing
29 that a related commit has landed and providing a link to the commit.
30
31 For "Fixes" references, in addition to posting the comment, the issue will be
32 closed (unless it's already closed) and a module-dependent milestone will be
33 assigned to it if configured and unless the issue already has a milestone.
kzar 2016/04/18 14:48:30 We should also avoid assigning milestones and clos
Vasily Kuznetsov 2016/04/24 21:49:50 Oh damn! I missed this in the original code. Now t
34
35 # Configuration
36
37 This hook is configured via `sitescripts.ini` using [hg] and
38 [hg_module_milestones] sections.
39
40 `track_xmlrpc_url` key from [hg] is used to determine the address of XMLRPC
41 interface of Trac and `issue_url_template` as a template for producing links to
42 the referenced issues that are displayed in the log.
43
44 The keys of [hg_module_milestones] section are module names and the values are
45 corresponding milestone regular expressions. The first open milestone that
46 matches the regular expression of issue's module will be assigned to the issue
47 when a commit fixing it arrives.
23 """ 48 """
24 49
50 import collections
25 import posixpath 51 import posixpath
26 import re 52 import re
27 import xmlrpclib 53 import xmlrpclib
28 54
29 from sitescripts.utils import get_config, get_template 55 from sitescripts.utils import get_config, get_template
30 56
31 def _generate_comments(repository_name, changes_by_issue): 57 class _Update(object):
kzar 2016/04/18 14:48:30 I guess I don't understand the need for this class
Vasily Kuznetsov 2016/04/24 21:49:50 Tried to make the code below prettier/more readabl
32 comments = {} 58 def __init__(self, issue_id, commits, is_fixed):
59 self.issue_id = issue_id
60 self.commits = commits
61 self.is_fixed = is_fixed
62
63 def _create_issue_updates(ui, repo, node):
64 issue_number_regex = re.compile(r"\b(issue|fixes)\s+(\d+)\b", re.I)
65 noissue_regex = re.compile(r"^noissue\b", re.I)
66 commits_by_issue = collections.defaultdict(list)
67 fixed_issues = set()
68 first_rev = repo[node].rev()
69 for change in repo[first_rev:]:
70 message = change.description()
71 if ' - ' not in message:
72 ui.warn("warning: invalid commit message format: '{}'\n".format(message))
73 continue
74 refs, rest = message.split(' - ', 1)
75 issue_refs = issue_number_regex.findall(refs)
76 if issue_refs:
77 for ref_type, issue_id in issue_refs:
78 issue_id = int(issue_id)
79 commits_by_issue[issue_id].append(change)
80 if ref_type.lower() == 'fixes':
81 fixed_issues.add(issue_id)
82 elif not noissue_regex.search(refs):
83 ui.warn("warning: no issue reference in commit message: '{}'\n"
84 .format(message))
85 for issue_id, commits in sorted(commits_by_issue.items()):
86 yield _Update(issue_id, commits, is_fixed=issue_id in fixed_issues)
87
88 def _prepare_changes(ui, trac, updates):
89 for update in updates:
90 try:
91 update.issue_attrs = trac.ticket.get(update.issue_id)[3]
92 update.changes = {"_ts": update.issue_attrs["_ts"], "action": "leave"}
93 if update.is_fixed:
94 actions = trac.ticket.getActions(update.issue_id)
95 if any(action[0] == "resolve" for action in actions):
96 update.changes["action"] = "resolve"
97 yield update
98 except Exception as exc:
99 if getattr(exc, 'faultCode', 0) == 404: # Not found
100 ui.warn("warning: reference to a non-existent issue: {}\n"
101 .format(update.issue_id))
102 else:
103 ui.warn("warning: error while getting issue {}: {}\n"
104 .format(update.issue_id, exc))
105
106 def _compile_module_regexps(ui, config, modules):
107 for module, regexp in config.items("hg_module_milestones"):
108 try:
109 yield module, re.compile("^" + regexp + "$", re.I)
kzar 2016/04/18 14:48:30 I don't think this regexp should be case-insensiti
Vasily Kuznetsov 2016/04/24 21:49:50 My reasoning was that we want to be more forgiving
kzar 2016/04/29 14:05:42 Well the issue doesn't say the regexp here is matc
Vasily Kuznetsov 2016/05/02 16:54:02 I changed the README and the issue to reflect the
110 except Exception as exc:
111 ui.warn("warning: invalid module milestone regexp in config: '{}' ({})\n"
112 .format(regexp, exc))
113
114 def _get_milestone_by_module(ui, config, trac, updates):
115 modules = {update.issue_attrs["component"] for update in updates
116 if update.is_fixed and not update.issue_attrs["milestone"]}
117 module_regexps = list(_compile_module_regexps(ui, config, modules))
118
119 def get_milestone_module(milestone_name):
120 for module, regex in module_regexps:
121 if regex.search(milestone_name):
122 return module
123 return None
124
125 milestones_by_module = collections.defaultdict(str)
126 if modules & {module for module, regexp in module_regexps}:
127 try:
128 milestone_names = filter(get_milestone_module,
129 trac.ticket.milestone.getAll())
130 # Using a MultiCall is better because we might have many milestones.
131 multicall = xmlrpclib.MultiCall(trac)
132 for name in milestone_names:
133 multicall.ticket.milestone.get(name)
134 milestones = multicall()
135 except Exception as exc:
136 ui.warn("warning: unable to get milestones from trac: {}\n".format(exc))
137 else:
138 for milestone in milestones:
139 if not milestone["completed"]:
140 module = get_milestone_module(milestone["name"])
141 if module not in milestones_by_module:
142 milestones_by_module[module] = milestone["name"]
143 return milestones_by_module
144
145 def _assign_milestones(ui, config, trac, updates):
146 updates = list(updates)
147 milestones_by_module = _get_milestone_by_module(ui, config, trac, updates)
148 for update in updates:
149 if update.is_fixed and not update.issue_attrs["milestone"]:
150 component = update.issue_attrs["component"]
151 if milestones_by_module[component]:
152 update.changes["milestone"] = milestones_by_module[component]
153 yield update
154
155 def _format_comments(repo, updates):
156 repository_name = posixpath.split(repo.url())[1]
33 template = get_template("hg/template/issue_commit_comment.tmpl", 157 template = get_template("hg/template/issue_commit_comment.tmpl",
34 autoescape=False) 158 autoescape=False)
35 for issue_id, changes in changes_by_issue.iteritems(): 159 for update in updates:
36 comments[issue_id] = template.render({"repository_name": repository_name, 160 update.comment = template.render({"repository_name": repository_name,
37 "changes": changes}) 161 "changes": update.commits})
38 return comments 162 yield update
39 163
40 def _post_comment(issue_id, comment): 164 def _apply_updates(ui, config, trac, updates):
41 issue_id = int(issue_id) 165 issue_url_template = config.get("hg", "issue_url_template")
42 url = get_config().get("hg", "trac_xmlrpc_url") 166 for update in updates:
43 server = xmlrpclib.ServerProxy(url) 167 issue_url = issue_url_template.format(id=update.issue_id)
44 attributes = server.ticket.get(issue_id)[3] 168 try:
45 server.ticket.update(issue_id, comment, 169 trac.ticket.update(update.issue_id, update.comment, update.changes, True)
46 {"_ts": attributes["_ts"], "action": "leave"}, True) 170 updates = ["posted comment"]
171 if "milestone" in update.changes:
172 updates.append("set milestone: {}".format(update.changes["milestone"]))
173 if update.changes["action"] == "resolve":
174 updates.append("closed")
175 ui.status("updated {} ({})\n".format(issue_url, ', '.join(updates)))
176 except Exception as exc:
177 ui.warn("warning: failed to update {} ({})\n".format(issue_url, exc))
47 178
48 def hook(ui, repo, node, **kwargs): 179 def hook(ui, repo, node, **kwargs):
49 first_change = repo[node] 180 config = get_config()
50 issue_number_regex = re.compile(r"\bissue\s+(\d+)\b", re.I) 181 trac_url = config.get("hg", "trac_xmlrpc_url")
51 noissue_regex = re.compile(r"^noissue\b", re.I) 182 trac = xmlrpclib.ServerProxy(trac_url)
52 changes_by_issue = {} 183 updates = _create_issue_updates(ui, repo, node)
kzar 2016/04/18 14:48:30 Couldn't we instead iterate through the updates on
Vasily Kuznetsov 2016/04/24 21:49:50 We're basically creating a stream of updates and s
53 for revision in xrange(first_change.rev(), len(repo)): 184 updates = _prepare_changes(ui, trac, updates)
54 change = repo[revision] 185 updates = _assign_milestones(ui, config, trac, updates)
55 description = change.description() 186 updates = _format_comments(repo, updates)
56 issue_ids = issue_number_regex.findall(description) 187 _apply_updates(ui, config, trac, updates)
57 if issue_ids:
58 for issue_id in issue_ids:
59 changes_by_issue.setdefault(issue_id, []).append(change)
60 elif not noissue_regex.search(description):
61 # We should just reject all changes when one of them has an invalid
62 # commit message format, see: https://issues.adblockplus.org/ticket/3679
63 ui.warn("warning: invalid commit message format in changeset %s\n" %
64 change)
65
66 repository_name = posixpath.split(repo.url())[1]
67 comments = _generate_comments(repository_name, changes_by_issue)
68
69 issue_url_template = get_config().get("hg", "issue_url_template")
70 for issue_id, comment in comments.iteritems():
71 try:
72 _post_comment(issue_id, comment)
73 ui.status("updating %s\n" % issue_url_template.format(id=issue_id))
74 except:
75 ui.warn("warning: failed to update %s\n" %
76 issue_url_template.format(id=issue_id))
OLDNEW
« no previous file with comments | « .sitescripts.example ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld