1 # Copyright (C) 2007-8 Brendan Cully <brendan@kublai.com> |
|
2 # |
|
3 # This software may be used and distributed according to the terms of the |
|
4 # GNU General Public License version 2 or any later version. |
|
5 |
|
6 """hooks for integrating with the CIA.vc notification service |
|
7 |
|
8 This is meant to be run as a changegroup or incoming hook. To |
|
9 configure it, set the following options in your hgrc:: |
|
10 |
|
11 [cia] |
|
12 # your registered CIA user name |
|
13 user = foo |
|
14 # the name of the project in CIA |
|
15 project = foo |
|
16 # the module (subproject) (optional) |
|
17 #module = foo |
|
18 # Append a diffstat to the log message (optional) |
|
19 #diffstat = False |
|
20 # Template to use for log messages (optional) |
|
21 #template = {desc}\\n{baseurl}{webroot}/rev/{node}-- {diffstat} |
|
22 # Style to use (optional) |
|
23 #style = foo |
|
24 # The URL of the CIA notification service (optional) |
|
25 # You can use mailto: URLs to send by email, e.g. |
|
26 # mailto:cia@cia.vc |
|
27 # Make sure to set email.from if you do this. |
|
28 #url = http://cia.vc/ |
|
29 # print message instead of sending it (optional) |
|
30 #test = False |
|
31 # number of slashes to strip for url paths |
|
32 #strip = 0 |
|
33 |
|
34 [hooks] |
|
35 # one of these: |
|
36 changegroup.cia = python:hgcia.hook |
|
37 #incoming.cia = python:hgcia.hook |
|
38 |
|
39 [web] |
|
40 # If you want hyperlinks (optional) |
|
41 baseurl = http://server/path/to/repo |
|
42 """ |
|
43 |
|
44 from mercurial.i18n import _ |
|
45 from mercurial.node import bin, short |
|
46 from mercurial import cmdutil, patch, util, mail, error |
|
47 import email.Parser |
|
48 |
|
49 import socket, xmlrpclib |
|
50 from xml.sax import saxutils |
|
51 # Note for extension authors: ONLY specify testedwith = 'internal' for |
|
52 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should |
|
53 # be specifying the version(s) of Mercurial they are tested with, or |
|
54 # leave the attribute unspecified. |
|
55 testedwith = 'internal' |
|
56 |
|
57 socket_timeout = 30 # seconds |
|
58 if util.safehasattr(socket, 'setdefaulttimeout'): |
|
59 # set a timeout for the socket so you don't have to wait so looooong |
|
60 # when cia.vc is having problems. requires python >= 2.3: |
|
61 socket.setdefaulttimeout(socket_timeout) |
|
62 |
|
63 HGCIA_VERSION = '0.1' |
|
64 HGCIA_URL = 'http://hg.kublai.com/mercurial/hgcia' |
|
65 |
|
66 |
|
67 class ciamsg(object): |
|
68 """ A CIA message """ |
|
69 def __init__(self, cia, ctx): |
|
70 self.cia = cia |
|
71 self.ctx = ctx |
|
72 self.url = self.cia.url |
|
73 if self.url: |
|
74 self.url += self.cia.root |
|
75 |
|
76 def fileelem(self, path, uri, action): |
|
77 if uri: |
|
78 uri = ' uri=%s' % saxutils.quoteattr(uri) |
|
79 return '<file%s action=%s>%s</file>' % ( |
|
80 uri, saxutils.quoteattr(action), saxutils.escape(path)) |
|
81 |
|
82 def fileelems(self): |
|
83 n = self.ctx.node() |
|
84 f = self.cia.repo.status(self.ctx.p1().node(), n) |
|
85 url = self.url or '' |
|
86 if url and url[-1] == '/': |
|
87 url = url[:-1] |
|
88 elems = [] |
|
89 for path in f.modified: |
|
90 uri = '%s/diff/%s/%s' % (url, short(n), path) |
|
91 elems.append(self.fileelem(path, url and uri, 'modify')) |
|
92 for path in f.added: |
|
93 # TODO: copy/rename ? |
|
94 uri = '%s/file/%s/%s' % (url, short(n), path) |
|
95 elems.append(self.fileelem(path, url and uri, 'add')) |
|
96 for path in f.removed: |
|
97 elems.append(self.fileelem(path, '', 'remove')) |
|
98 |
|
99 return '\n'.join(elems) |
|
100 |
|
101 def sourceelem(self, project, module=None, branch=None): |
|
102 msg = ['<source>', '<project>%s</project>' % saxutils.escape(project)] |
|
103 if module: |
|
104 msg.append('<module>%s</module>' % saxutils.escape(module)) |
|
105 if branch: |
|
106 msg.append('<branch>%s</branch>' % saxutils.escape(branch)) |
|
107 msg.append('</source>') |
|
108 |
|
109 return '\n'.join(msg) |
|
110 |
|
111 def diffstat(self): |
|
112 class patchbuf(object): |
|
113 def __init__(self): |
|
114 self.lines = [] |
|
115 # diffstat is stupid |
|
116 self.name = 'cia' |
|
117 def write(self, data): |
|
118 self.lines += data.splitlines(True) |
|
119 def close(self): |
|
120 pass |
|
121 |
|
122 n = self.ctx.node() |
|
123 pbuf = patchbuf() |
|
124 cmdutil.export(self.cia.repo, [n], fp=pbuf) |
|
125 return patch.diffstat(pbuf.lines) or '' |
|
126 |
|
127 def logmsg(self): |
|
128 if self.cia.diffstat: |
|
129 diffstat = self.diffstat() |
|
130 else: |
|
131 diffstat = '' |
|
132 self.cia.ui.pushbuffer() |
|
133 self.cia.templater.show(self.ctx, changes=self.ctx.changeset(), |
|
134 baseurl=self.cia.ui.config('web', 'baseurl'), |
|
135 url=self.url, diffstat=diffstat, |
|
136 webroot=self.cia.root) |
|
137 return self.cia.ui.popbuffer() |
|
138 |
|
139 def xml(self): |
|
140 n = short(self.ctx.node()) |
|
141 src = self.sourceelem(self.cia.project, module=self.cia.module, |
|
142 branch=self.ctx.branch()) |
|
143 # unix timestamp |
|
144 dt = self.ctx.date() |
|
145 timestamp = dt[0] |
|
146 |
|
147 author = saxutils.escape(self.ctx.user()) |
|
148 rev = '%d:%s' % (self.ctx.rev(), n) |
|
149 log = saxutils.escape(self.logmsg()) |
|
150 |
|
151 url = self.url |
|
152 if url and url[-1] == '/': |
|
153 url = url[:-1] |
|
154 url = url and '<url>%s/rev/%s</url>' % (saxutils.escape(url), n) or '' |
|
155 |
|
156 msg = """ |
|
157 <message> |
|
158 <generator> |
|
159 <name>Mercurial (hgcia)</name> |
|
160 <version>%s</version> |
|
161 <url>%s</url> |
|
162 <user>%s</user> |
|
163 </generator> |
|
164 %s |
|
165 <body> |
|
166 <commit> |
|
167 <author>%s</author> |
|
168 <version>%s</version> |
|
169 <log>%s</log> |
|
170 %s |
|
171 <files>%s</files> |
|
172 </commit> |
|
173 </body> |
|
174 <timestamp>%d</timestamp> |
|
175 </message> |
|
176 """ % \ |
|
177 (HGCIA_VERSION, saxutils.escape(HGCIA_URL), |
|
178 saxutils.escape(self.cia.user), src, author, rev, log, url, |
|
179 self.fileelems(), timestamp) |
|
180 |
|
181 return msg |
|
182 |
|
183 |
|
184 class hgcia(object): |
|
185 """ CIA notification class """ |
|
186 |
|
187 deftemplate = '{desc}' |
|
188 dstemplate = '{desc}\n-- \n{diffstat}' |
|
189 |
|
190 def __init__(self, ui, repo): |
|
191 self.ui = ui |
|
192 self.repo = repo |
|
193 |
|
194 self.ciaurl = self.ui.config('cia', 'url', 'http://cia.vc') |
|
195 self.user = self.ui.config('cia', 'user') |
|
196 self.project = self.ui.config('cia', 'project') |
|
197 self.module = self.ui.config('cia', 'module') |
|
198 self.diffstat = self.ui.configbool('cia', 'diffstat') |
|
199 self.emailfrom = self.ui.config('email', 'from') |
|
200 self.dryrun = self.ui.configbool('cia', 'test') |
|
201 self.url = self.ui.config('web', 'baseurl') |
|
202 # Default to -1 for backward compatibility |
|
203 self.stripcount = int(self.ui.config('cia', 'strip', -1)) |
|
204 self.root = self.strip(self.repo.root) |
|
205 |
|
206 style = self.ui.config('cia', 'style') |
|
207 template = self.ui.config('cia', 'template') |
|
208 if not template: |
|
209 if self.diffstat: |
|
210 template = self.dstemplate |
|
211 else: |
|
212 template = self.deftemplate |
|
213 t = cmdutil.changeset_templater(self.ui, self.repo, False, None, |
|
214 template, style, False) |
|
215 self.templater = t |
|
216 |
|
217 def strip(self, path): |
|
218 '''strip leading slashes from local path, turn into web-safe path.''' |
|
219 |
|
220 path = util.pconvert(path) |
|
221 count = self.stripcount |
|
222 if count < 0: |
|
223 return '' |
|
224 while count > 0: |
|
225 c = path.find('/') |
|
226 if c == -1: |
|
227 break |
|
228 path = path[c + 1:] |
|
229 count -= 1 |
|
230 return path |
|
231 |
|
232 def sendrpc(self, msg): |
|
233 srv = xmlrpclib.Server(self.ciaurl) |
|
234 res = srv.hub.deliver(msg) |
|
235 if res is not True and res != 'queued.': |
|
236 raise error.Abort(_('%s returned an error: %s') % |
|
237 (self.ciaurl, res)) |
|
238 |
|
239 def sendemail(self, address, data): |
|
240 p = email.Parser.Parser() |
|
241 msg = p.parsestr(data) |
|
242 msg['Date'] = util.datestr(format="%a, %d %b %Y %H:%M:%S %1%2") |
|
243 msg['To'] = address |
|
244 msg['From'] = self.emailfrom |
|
245 msg['Subject'] = 'DeliverXML' |
|
246 msg['Content-type'] = 'text/xml' |
|
247 msgtext = msg.as_string() |
|
248 |
|
249 self.ui.status(_('hgcia: sending update to %s\n') % address) |
|
250 mail.sendmail(self.ui, util.email(self.emailfrom), |
|
251 [address], msgtext) |
|
252 |
|
253 |
|
254 def hook(ui, repo, hooktype, node=None, url=None, **kwargs): |
|
255 """ send CIA notification """ |
|
256 def sendmsg(cia, ctx): |
|
257 msg = ciamsg(cia, ctx).xml() |
|
258 if cia.dryrun: |
|
259 ui.write(msg) |
|
260 elif cia.ciaurl.startswith('mailto:'): |
|
261 if not cia.emailfrom: |
|
262 raise error.Abort(_('email.from must be defined when ' |
|
263 'sending by email')) |
|
264 cia.sendemail(cia.ciaurl[7:], msg) |
|
265 else: |
|
266 cia.sendrpc(msg) |
|
267 |
|
268 n = bin(node) |
|
269 cia = hgcia(ui, repo) |
|
270 if not cia.user: |
|
271 ui.debug('cia: no user specified') |
|
272 return |
|
273 if not cia.project: |
|
274 ui.debug('cia: no project specified') |
|
275 return |
|
276 if hooktype == 'changegroup': |
|
277 start = repo.changelog.rev(n) |
|
278 end = len(repo.changelog) |
|
279 for rev in xrange(start, end): |
|
280 n = repo.changelog.node(rev) |
|
281 ctx = repo.changectx(n) |
|
282 sendmsg(cia, ctx) |
|
283 else: |
|
284 ctx = repo.changectx(n) |
|
285 sendmsg(cia, ctx) |
|