##// END OF EJS Templates
notify: use contexts more pervasively
Dirkjan Ochtman -
r7726:2486980f default
parent child Browse files
Show More
@@ -1,290 +1,291 b''
1 1 # notify.py - email notifications for mercurial
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 4 #
5 5 # This software may be used and distributed according to the terms
6 6 # of the GNU General Public License, incorporated herein by reference.
7 7
8 8 '''hook extension to email notifications on commits/pushes
9 9
10 10 Subscriptions can be managed through hgrc. Default mode is to print
11 11 messages to stdout, for testing and configuring.
12 12
13 13 To use, configure notify extension and enable in hgrc like this:
14 14
15 15 [extensions]
16 16 hgext.notify =
17 17
18 18 [hooks]
19 19 # one email for each incoming changeset
20 20 incoming.notify = python:hgext.notify.hook
21 21 # batch emails when many changesets incoming at one time
22 22 changegroup.notify = python:hgext.notify.hook
23 23
24 24 [notify]
25 25 # config items go in here
26 26
27 27 config items:
28 28
29 29 REQUIRED:
30 30 config = /path/to/file # file containing subscriptions
31 31
32 32 OPTIONAL:
33 33 test = True # print messages to stdout for testing
34 34 strip = 3 # number of slashes to strip for url paths
35 35 domain = example.com # domain to use if committer missing domain
36 36 style = ... # style file to use when formatting email
37 37 template = ... # template to use when formatting email
38 38 incoming = ... # template to use when run as incoming hook
39 39 changegroup = ... # template when run as changegroup hook
40 40 maxdiff = 300 # max lines of diffs to include (0=none, -1=all)
41 41 maxsubject = 67 # truncate subject line longer than this
42 42 diffstat = True # add a diffstat before the diff content
43 43 sources = serve # notify if source of incoming changes in this list
44 44 # (serve == ssh or http, push, pull, bundle)
45 45 [email]
46 46 from = user@host.com # email address to send as if none given
47 47 [web]
48 48 baseurl = http://hgserver/... # root of hg web site for browsing commits
49 49
50 50 notify config file has same format as regular hgrc. it has two
51 51 sections so you can express subscriptions in whatever way is handier
52 52 for you.
53 53
54 54 [usersubs]
55 55 # key is subscriber email, value is ","-separated list of glob patterns
56 56 user@host = pattern
57 57
58 58 [reposubs]
59 59 # key is glob pattern, value is ","-separated list of subscriber emails
60 60 pattern = user@host
61 61
62 62 glob patterns are matched against path to repo root.
63 63
64 64 if you like, you can put notify config file in repo that users can
65 65 push changes to, they can manage their own subscriptions.'''
66 66
67 67 from mercurial.i18n import _
68 68 from mercurial.node import bin, short
69 69 from mercurial import patch, cmdutil, templater, util, mail
70 70 import email.Parser, fnmatch, socket, time
71 71
72 72 # template for single changeset can include email headers.
73 73 single_template = '''
74 74 Subject: changeset in {webroot}: {desc|firstline|strip}
75 75 From: {author}
76 76
77 77 changeset {node|short} in {root}
78 78 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
79 79 description:
80 80 \t{desc|tabindent|strip}
81 81 '''.lstrip()
82 82
83 83 # template for multiple changesets should not contain email headers,
84 84 # because only first set of headers will be used and result will look
85 85 # strange.
86 86 multiple_template = '''
87 87 changeset {node|short} in {root}
88 88 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
89 89 summary: {desc|firstline}
90 90 '''
91 91
92 92 deftemplates = {
93 93 'changegroup': multiple_template,
94 94 }
95 95
96 96 class notifier(object):
97 97 '''email notification class.'''
98 98
99 99 def __init__(self, ui, repo, hooktype):
100 100 self.ui = ui
101 101 cfg = self.ui.config('notify', 'config')
102 102 if cfg:
103 103 self.ui.readsections(cfg, 'usersubs', 'reposubs')
104 104 self.repo = repo
105 105 self.stripcount = int(self.ui.config('notify', 'strip', 0))
106 106 self.root = self.strip(self.repo.root)
107 107 self.domain = self.ui.config('notify', 'domain')
108 108 self.test = self.ui.configbool('notify', 'test', True)
109 109 self.charsets = mail._charsets(self.ui)
110 110 self.subs = self.subscribers()
111 111
112 112 mapfile = self.ui.config('notify', 'style')
113 113 template = (self.ui.config('notify', hooktype) or
114 114 self.ui.config('notify', 'template'))
115 115 self.t = cmdutil.changeset_templater(self.ui, self.repo,
116 116 False, mapfile, False)
117 117 if not mapfile and not template:
118 118 template = deftemplates.get(hooktype) or single_template
119 119 if template:
120 120 template = templater.parsestring(template, quoted=False)
121 121 self.t.use_template(template)
122 122
123 123 def strip(self, path):
124 124 '''strip leading slashes from local path, turn into web-safe path.'''
125 125
126 126 path = util.pconvert(path)
127 127 count = self.stripcount
128 128 while count > 0:
129 129 c = path.find('/')
130 130 if c == -1:
131 131 break
132 132 path = path[c+1:]
133 133 count -= 1
134 134 return path
135 135
136 136 def fixmail(self, addr):
137 137 '''try to clean up email addresses.'''
138 138
139 139 addr = util.email(addr.strip())
140 140 if self.domain:
141 141 a = addr.find('@localhost')
142 142 if a != -1:
143 143 addr = addr[:a]
144 144 if '@' not in addr:
145 145 return addr + '@' + self.domain
146 146 return addr
147 147
148 148 def subscribers(self):
149 149 '''return list of email addresses of subscribers to this repo.'''
150
151 150 subs = {}
152 151 for user, pats in self.ui.configitems('usersubs'):
153 152 for pat in pats.split(','):
154 153 if fnmatch.fnmatch(self.repo.root, pat.strip()):
155 154 subs[self.fixmail(user)] = 1
156 155 for pat, users in self.ui.configitems('reposubs'):
157 156 if fnmatch.fnmatch(self.repo.root, pat):
158 157 for user in users.split(','):
159 158 subs[self.fixmail(user)] = 1
160 159 subs = util.sort(subs)
161 160 return [mail.addressencode(self.ui, s, self.charsets, self.test)
162 161 for s in subs]
163 162
164 163 def url(self, path=None):
165 164 return self.ui.config('web', 'baseurl') + (path or self.root)
166 165
167 def node(self, node):
166 def node(self, ctx):
168 167 '''format one changeset.'''
169
170 self.t.show(self.repo[node], changes=self.repo.changelog.read(node),
168 self.t.show(ctx, changes=ctx.changeset(),
171 169 baseurl=self.ui.config('web', 'baseurl'),
172 root=self.repo.root,
173 webroot=self.root)
170 root=self.repo.root, webroot=self.root)
174 171
175 172 def skipsource(self, source):
176 173 '''true if incoming changes from this source should be skipped.'''
177 174 ok_sources = self.ui.config('notify', 'sources', 'serve').split()
178 175 return source not in ok_sources
179 176
180 def send(self, node, count, data):
177 def send(self, ctx, count, data):
181 178 '''send message.'''
182 179
183 180 p = email.Parser.Parser()
184 181 msg = p.parsestr(data)
185 182
186 183 # store sender and subject
187 184 sender, subject = msg['From'], msg['Subject']
188 185 del msg['From'], msg['Subject']
189 186 # store remaining headers
190 187 headers = msg.items()
191 188 # create fresh mime message from msg body
192 189 text = msg.get_payload()
193 190 # for notification prefer readability over data precision
194 191 msg = mail.mimeencode(self.ui, text, self.charsets, self.test)
195 192 # reinstate custom headers
196 193 for k, v in headers:
197 194 msg[k] = v
198 195
199 196 msg['Date'] = util.datestr(format="%a, %d %b %Y %H:%M:%S %1%2")
200 197
201 198 # try to make subject line exist and be useful
202 199 if not subject:
203 200 if count > 1:
204 201 subject = _('%s: %d new changesets') % (self.root, count)
205 202 else:
206 changes = self.repo.changelog.read(node)
207 s = changes[4].lstrip().split('\n', 1)[0].rstrip()
203 s = ctx.description().lstrip().split('\n', 1)[0].rstrip()
208 204 subject = '%s: %s' % (self.root, s)
209 205 maxsubject = int(self.ui.config('notify', 'maxsubject', 67))
210 206 if maxsubject and len(subject) > maxsubject:
211 207 subject = subject[:maxsubject-3] + '...'
212 208 msg['Subject'] = mail.headencode(self.ui, subject,
213 209 self.charsets, self.test)
214 210
215 211 # try to make message have proper sender
216 212 if not sender:
217 213 sender = self.ui.config('email', 'from') or self.ui.username()
218 214 if '@' not in sender or '@localhost' in sender:
219 215 sender = self.fixmail(sender)
220 216 msg['From'] = mail.addressencode(self.ui, sender,
221 217 self.charsets, self.test)
222 218
223 msg['X-Hg-Notification'] = 'changeset ' + short(node)
219 msg['X-Hg-Notification'] = 'changeset %s' % ctx
224 220 if not msg['Message-Id']:
225 221 msg['Message-Id'] = ('<hg.%s.%s.%s@%s>' %
226 (short(node), int(time.time()),
222 (ctx, int(time.time()),
227 223 hash(self.repo.root), socket.getfqdn()))
228 224 msg['To'] = ', '.join(self.subs)
229 225
230 226 msgtext = msg.as_string(0)
231 227 if self.test:
232 228 self.ui.write(msgtext)
233 229 if not msgtext.endswith('\n'):
234 230 self.ui.write('\n')
235 231 else:
236 232 self.ui.status(_('notify: sending %d subscribers %d changes\n') %
237 233 (len(self.subs), count))
238 234 mail.sendmail(self.ui, util.email(msg['From']),
239 235 self.subs, msgtext)
240 236
241 def diff(self, node, ref):
237 def diff(self, ctx, ref=None):
238
242 239 maxdiff = int(self.ui.config('notify', 'maxdiff', 300))
243 prev = self.repo.changelog.parents(node)[0]
244
240 prev = ctx.parents()[0].node()
241 ref = ref and ref.node() or ctx.node()
245 242 chunks = patch.diff(self.repo, prev, ref, opts=patch.diffopts(self.ui))
246 243 difflines = ''.join(chunks).splitlines()
247 244
248 245 if self.ui.configbool('notify', 'diffstat', True):
249 246 s = patch.diffstat(difflines)
250 247 # s may be nil, don't include the header if it is
251 248 if s:
252 249 self.ui.write('\ndiffstat:\n\n%s' % s)
250
253 251 if maxdiff == 0:
254 252 return
255 if maxdiff > 0 and len(difflines) > maxdiff:
256 self.ui.write(_('\ndiffs (truncated from %d to %d lines):\n\n') %
257 (len(difflines), maxdiff))
253 elif maxdiff > 0 and len(difflines) > maxdiff:
254 msg = _('\ndiffs (truncated from %d to %d lines):\n\n')
255 self.ui.write(msg % (len(difflines), maxdiff))
258 256 difflines = difflines[:maxdiff]
259 257 elif difflines:
260 258 self.ui.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
259
261 260 self.ui.write("\n".join(difflines))
262 261
263 262 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
264 263 '''send email notifications to interested subscribers.
265 264
266 265 if used as changegroup hook, send one email for all changesets in
267 266 changegroup. else send one email per changeset.'''
267
268 268 n = notifier(ui, repo, hooktype)
269 ctx = repo[node]
270
269 271 if not n.subs:
270 272 ui.debug(_('notify: no subscribers to repo %s\n') % n.root)
271 273 return
272 274 if n.skipsource(source):
273 ui.debug(_('notify: changes have source "%s" - skipping\n') %
274 source)
275 ui.debug(_('notify: changes have source "%s" - skipping\n') % source)
275 276 return
276 node = bin(node)
277
277 278 ui.pushbuffer()
278 279 if hooktype == 'changegroup':
279 start = repo[node].rev()
280 end = len(repo)
280 start, end = ctx.rev(), len(repo)
281 281 count = end - start
282 282 for rev in xrange(start, end):
283 n.node(repo[rev].node())
284 n.diff(node, repo.changelog.tip())
283 n.node(repo[rev])
284 n.diff(ctx, repo['tip'])
285 285 else:
286 286 count = 1
287 n.node(node)
288 n.diff(node, node)
287 n.node(ctx)
288 n.diff(ctx)
289
289 290 data = ui.popbuffer()
290 n.send(node, count, data)
291 n.send(ctx, count, data)
General Comments 0
You need to be logged in to leave comments. Login now