##// END OF EJS Templates
notify: add option for writing to mbox...
Mads Kiilerich -
r15561:ca572e94 default
parent child Browse files
Show More
@@ -1,360 +1,364 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 of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 '''hooks for sending email push notifications
9 9
10 10 This extension let you run hooks sending email notifications when
11 11 changesets are being pushed, from the sending or receiving side.
12 12
13 13 First, enable the extension as explained in :hg:`help extensions`, and
14 14 register the hook you want to run. ``incoming`` and ``outgoing`` hooks
15 15 are run by the changesets receiver while the ``outgoing`` one is for
16 16 the sender::
17 17
18 18 [hooks]
19 19 # one email for each incoming changeset
20 20 incoming.notify = python:hgext.notify.hook
21 21 # one email for all incoming changesets
22 22 changegroup.notify = python:hgext.notify.hook
23 23
24 24 # one email for all outgoing changesets
25 25 outgoing.notify = python:hgext.notify.hook
26 26
27 27 Now the hooks are running, subscribers must be assigned to
28 28 repositories. Use the ``[usersubs]`` section to map repositories to a
29 29 given email or the ``[reposubs]`` section to map emails to a single
30 30 repository::
31 31
32 32 [usersubs]
33 33 # key is subscriber email, value is a comma-separated list of glob
34 34 # patterns
35 35 user@host = pattern
36 36
37 37 [reposubs]
38 38 # key is glob pattern, value is a comma-separated list of subscriber
39 39 # emails
40 40 pattern = user@host
41 41
42 42 Glob patterns are matched against absolute path to repository
43 43 root. The subscriptions can be defined in their own file and
44 44 referenced with::
45 45
46 46 [notify]
47 47 config = /path/to/subscriptionsfile
48 48
49 49 Alternatively, they can be added to Mercurial configuration files by
50 50 setting the previous entry to an empty value.
51 51
52 52 At this point, notifications should be generated but will not be sent until you
53 53 set the ``notify.test`` entry to ``False``.
54 54
55 55 Notifications content can be tweaked with the following configuration entries:
56 56
57 57 notify.test
58 58 If ``True``, print messages to stdout instead of sending them. Default: True.
59 59
60 60 notify.sources
61 61 Space separated list of change sources. Notifications are sent only
62 62 if it includes the incoming or outgoing changes source. Incoming
63 63 sources can be ``serve`` for changes coming from http or ssh,
64 64 ``pull`` for pulled changes, ``unbundle`` for changes added by
65 65 :hg:`unbundle` or ``push`` for changes being pushed
66 66 locally. Outgoing sources are the same except for ``unbundle`` which
67 67 is replaced by ``bundle``. Default: serve.
68 68
69 69 notify.strip
70 70 Number of leading slashes to strip from url paths. By default, notifications
71 71 references repositories with their absolute path. ``notify.strip`` let you
72 72 turn them into relative paths. For example, ``notify.strip=3`` will change
73 73 ``/long/path/repository`` into ``repository``. Default: 0.
74 74
75 75 notify.domain
76 76 If subscribers emails or the from email have no domain set, complete them
77 77 with this value.
78 78
79 79 notify.style
80 80 Style file to use when formatting emails.
81 81
82 82 notify.template
83 83 Template to use when formatting emails.
84 84
85 85 notify.incoming
86 86 Template to use when run as incoming hook, override ``notify.template``.
87 87
88 88 notify.outgoing
89 89 Template to use when run as outgoing hook, override ``notify.template``.
90 90
91 91 notify.changegroup
92 92 Template to use when running as changegroup hook, override
93 93 ``notify.template``.
94 94
95 95 notify.maxdiff
96 96 Maximum number of diff lines to include in notification email. Set to 0
97 97 to disable the diff, -1 to include all of it. Default: 300.
98 98
99 99 notify.maxsubject
100 100 Maximum number of characters in emails subject line. Default: 67.
101 101
102 102 notify.diffstat
103 103 Set to True to include a diffstat before diff content. Default: True.
104 104
105 105 notify.merge
106 106 If True, send notifications for merge changesets. Default: True.
107 107
108 notify.mbox
109 If set, append mails to this mbox file instead of sending. Default: None.
110
108 111 If set, the following entries will also be used to customize the notifications:
109 112
110 113 email.from
111 114 Email ``From`` address to use if none can be found in generated email content.
112 115
113 116 web.baseurl
114 117 Root repository browsing URL to combine with repository paths when making
115 118 references. See also ``notify.strip``.
116 119
117 120 '''
118 121
119 122 from mercurial.i18n import _
120 123 from mercurial import patch, cmdutil, templater, util, mail
121 124 import email.Parser, email.Errors, fnmatch, socket, time
122 125
123 126 # template for single changeset can include email headers.
124 127 single_template = '''
125 128 Subject: changeset in {webroot}: {desc|firstline|strip}
126 129 From: {author}
127 130
128 131 changeset {node|short} in {root}
129 132 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
130 133 description:
131 134 \t{desc|tabindent|strip}
132 135 '''.lstrip()
133 136
134 137 # template for multiple changesets should not contain email headers,
135 138 # because only first set of headers will be used and result will look
136 139 # strange.
137 140 multiple_template = '''
138 141 changeset {node|short} in {root}
139 142 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
140 143 summary: {desc|firstline}
141 144 '''
142 145
143 146 deftemplates = {
144 147 'changegroup': multiple_template,
145 148 }
146 149
147 150 class notifier(object):
148 151 '''email notification class.'''
149 152
150 153 def __init__(self, ui, repo, hooktype):
151 154 self.ui = ui
152 155 cfg = self.ui.config('notify', 'config')
153 156 if cfg:
154 157 self.ui.readconfig(cfg, sections=['usersubs', 'reposubs'])
155 158 self.repo = repo
156 159 self.stripcount = int(self.ui.config('notify', 'strip', 0))
157 160 self.root = self.strip(self.repo.root)
158 161 self.domain = self.ui.config('notify', 'domain')
162 self.mbox = self.ui.config('notify', 'mbox')
159 163 self.test = self.ui.configbool('notify', 'test', True)
160 164 self.charsets = mail._charsets(self.ui)
161 165 self.subs = self.subscribers()
162 166 self.merge = self.ui.configbool('notify', 'merge', True)
163 167
164 168 mapfile = self.ui.config('notify', 'style')
165 169 template = (self.ui.config('notify', hooktype) or
166 170 self.ui.config('notify', 'template'))
167 171 self.t = cmdutil.changeset_templater(self.ui, self.repo,
168 172 False, None, mapfile, False)
169 173 if not mapfile and not template:
170 174 template = deftemplates.get(hooktype) or single_template
171 175 if template:
172 176 template = templater.parsestring(template, quoted=False)
173 177 self.t.use_template(template)
174 178
175 179 def strip(self, path):
176 180 '''strip leading slashes from local path, turn into web-safe path.'''
177 181
178 182 path = util.pconvert(path)
179 183 count = self.stripcount
180 184 while count > 0:
181 185 c = path.find('/')
182 186 if c == -1:
183 187 break
184 188 path = path[c + 1:]
185 189 count -= 1
186 190 return path
187 191
188 192 def fixmail(self, addr):
189 193 '''try to clean up email addresses.'''
190 194
191 195 addr = util.email(addr.strip())
192 196 if self.domain:
193 197 a = addr.find('@localhost')
194 198 if a != -1:
195 199 addr = addr[:a]
196 200 if '@' not in addr:
197 201 return addr + '@' + self.domain
198 202 return addr
199 203
200 204 def subscribers(self):
201 205 '''return list of email addresses of subscribers to this repo.'''
202 206 subs = set()
203 207 for user, pats in self.ui.configitems('usersubs'):
204 208 for pat in pats.split(','):
205 209 if fnmatch.fnmatch(self.repo.root, pat.strip()):
206 210 subs.add(self.fixmail(user))
207 211 for pat, users in self.ui.configitems('reposubs'):
208 212 if fnmatch.fnmatch(self.repo.root, pat):
209 213 for user in users.split(','):
210 214 subs.add(self.fixmail(user))
211 215 return [mail.addressencode(self.ui, s, self.charsets, self.test)
212 216 for s in sorted(subs)]
213 217
214 218 def node(self, ctx, **props):
215 219 '''format one changeset, unless it is a suppressed merge.'''
216 220 if not self.merge and len(ctx.parents()) > 1:
217 221 return False
218 222 self.t.show(ctx, changes=ctx.changeset(),
219 223 baseurl=self.ui.config('web', 'baseurl'),
220 224 root=self.repo.root, webroot=self.root, **props)
221 225 return True
222 226
223 227 def skipsource(self, source):
224 228 '''true if incoming changes from this source should be skipped.'''
225 229 ok_sources = self.ui.config('notify', 'sources', 'serve').split()
226 230 return source not in ok_sources
227 231
228 232 def send(self, ctx, count, data):
229 233 '''send message.'''
230 234
231 235 p = email.Parser.Parser()
232 236 try:
233 237 msg = p.parsestr(data)
234 238 except email.Errors.MessageParseError, inst:
235 239 raise util.Abort(inst)
236 240
237 241 # store sender and subject
238 242 sender, subject = msg['From'], msg['Subject']
239 243 del msg['From'], msg['Subject']
240 244
241 245 if not msg.is_multipart():
242 246 # create fresh mime message from scratch
243 247 # (multipart templates must take care of this themselves)
244 248 headers = msg.items()
245 249 payload = msg.get_payload()
246 250 # for notification prefer readability over data precision
247 251 msg = mail.mimeencode(self.ui, payload, self.charsets, self.test)
248 252 # reinstate custom headers
249 253 for k, v in headers:
250 254 msg[k] = v
251 255
252 256 msg['Date'] = util.datestr(format="%a, %d %b %Y %H:%M:%S %1%2")
253 257
254 258 # try to make subject line exist and be useful
255 259 if not subject:
256 260 if count > 1:
257 261 subject = _('%s: %d new changesets') % (self.root, count)
258 262 else:
259 263 s = ctx.description().lstrip().split('\n', 1)[0].rstrip()
260 264 subject = '%s: %s' % (self.root, s)
261 265 maxsubject = int(self.ui.config('notify', 'maxsubject', 67))
262 266 if maxsubject:
263 267 subject = util.ellipsis(subject, maxsubject)
264 268 msg['Subject'] = mail.headencode(self.ui, subject,
265 269 self.charsets, self.test)
266 270
267 271 # try to make message have proper sender
268 272 if not sender:
269 273 sender = self.ui.config('email', 'from') or self.ui.username()
270 274 if '@' not in sender or '@localhost' in sender:
271 275 sender = self.fixmail(sender)
272 276 msg['From'] = mail.addressencode(self.ui, sender,
273 277 self.charsets, self.test)
274 278
275 279 msg['X-Hg-Notification'] = 'changeset %s' % ctx
276 280 if not msg['Message-Id']:
277 281 msg['Message-Id'] = ('<hg.%s.%s.%s@%s>' %
278 282 (ctx, int(time.time()),
279 283 hash(self.repo.root), socket.getfqdn()))
280 284 msg['To'] = ', '.join(self.subs)
281 285
282 286 msgtext = msg.as_string()
283 287 if self.test:
284 288 self.ui.write(msgtext)
285 289 if not msgtext.endswith('\n'):
286 290 self.ui.write('\n')
287 291 else:
288 292 self.ui.status(_('notify: sending %d subscribers %d changes\n') %
289 293 (len(self.subs), count))
290 294 mail.sendmail(self.ui, util.email(msg['From']),
291 self.subs, msgtext)
295 self.subs, msgtext, mbox=self.mbox)
292 296
293 297 def diff(self, ctx, ref=None):
294 298
295 299 maxdiff = int(self.ui.config('notify', 'maxdiff', 300))
296 300 prev = ctx.p1().node()
297 301 ref = ref and ref.node() or ctx.node()
298 302 chunks = patch.diff(self.repo, prev, ref, opts=patch.diffopts(self.ui))
299 303 difflines = ''.join(chunks).splitlines()
300 304
301 305 if self.ui.configbool('notify', 'diffstat', True):
302 306 s = patch.diffstat(difflines)
303 307 # s may be nil, don't include the header if it is
304 308 if s:
305 309 self.ui.write('\ndiffstat:\n\n%s' % s)
306 310
307 311 if maxdiff == 0:
308 312 return
309 313 elif maxdiff > 0 and len(difflines) > maxdiff:
310 314 msg = _('\ndiffs (truncated from %d to %d lines):\n\n')
311 315 self.ui.write(msg % (len(difflines), maxdiff))
312 316 difflines = difflines[:maxdiff]
313 317 elif difflines:
314 318 self.ui.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
315 319
316 320 self.ui.write("\n".join(difflines))
317 321
318 322 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
319 323 '''send email notifications to interested subscribers.
320 324
321 325 if used as changegroup hook, send one email for all changesets in
322 326 changegroup. else send one email per changeset.'''
323 327
324 328 n = notifier(ui, repo, hooktype)
325 329 ctx = repo[node]
326 330
327 331 if not n.subs:
328 332 ui.debug('notify: no subscribers to repository %s\n' % n.root)
329 333 return
330 334 if n.skipsource(source):
331 335 ui.debug('notify: changes have source "%s" - skipping\n' % source)
332 336 return
333 337
334 338 ui.pushbuffer()
335 339 data = ''
336 340 count = 0
337 341 if hooktype == 'changegroup' or hooktype == 'outgoing':
338 342 start, end = ctx.rev(), len(repo)
339 343 for rev in xrange(start, end):
340 344 if n.node(repo[rev]):
341 345 count += 1
342 346 else:
343 347 data += ui.popbuffer()
344 348 ui.note(_('notify: suppressing notification for merge %d:%s\n') %
345 349 (rev, repo[rev].hex()[:12]))
346 350 ui.pushbuffer()
347 351 if count:
348 352 n.diff(ctx, repo['tip'])
349 353 else:
350 354 if not n.node(ctx):
351 355 ui.popbuffer()
352 356 ui.note(_('notify: suppressing notification for merge %d:%s\n') %
353 357 (ctx.rev(), ctx.hex()[:12]))
354 358 return
355 359 count += 1
356 360 n.diff(ctx)
357 361
358 362 data += ui.popbuffer()
359 363 if count:
360 364 n.send(ctx, count, data)
@@ -1,247 +1,247 b''
1 1 # mail.py - mail sending bits for mercurial
2 2 #
3 3 # Copyright 2006 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from i18n import _
9 9 import util, encoding
10 10 import os, smtplib, socket, quopri, time
11 11 import email.Header, email.MIMEText, email.Utils
12 12
13 13 _oldheaderinit = email.Header.Header.__init__
14 14 def _unifiedheaderinit(self, *args, **kw):
15 15 """
16 16 Python2.7 introduces a backwards incompatible change
17 17 (Python issue1974, r70772) in email.Generator.Generator code:
18 18 pre-2.7 code passed "continuation_ws='\t'" to the Header
19 19 constructor, and 2.7 removed this parameter.
20 20
21 21 Default argument is continuation_ws=' ', which means that the
22 22 behaviour is different in <2.7 and 2.7
23 23
24 24 We consider the 2.7 behaviour to be preferable, but need
25 25 to have an unified behaviour for versions 2.4 to 2.7
26 26 """
27 27 # override continuation_ws
28 28 kw['continuation_ws'] = ' '
29 29 _oldheaderinit(self, *args, **kw)
30 30
31 31 email.Header.Header.__dict__['__init__'] = _unifiedheaderinit
32 32
33 33 def _smtp(ui):
34 34 '''build an smtp connection and return a function to send mail'''
35 35 local_hostname = ui.config('smtp', 'local_hostname')
36 36 tls = ui.config('smtp', 'tls', 'none')
37 37 # backward compatible: when tls = true, we use starttls.
38 38 starttls = tls == 'starttls' or util.parsebool(tls)
39 39 smtps = tls == 'smtps'
40 40 if (starttls or smtps) and not util.safehasattr(socket, 'ssl'):
41 41 raise util.Abort(_("can't use TLS: Python SSL support not installed"))
42 42 if smtps:
43 43 ui.note(_('(using smtps)\n'))
44 44 s = smtplib.SMTP_SSL(local_hostname=local_hostname)
45 45 else:
46 46 s = smtplib.SMTP(local_hostname=local_hostname)
47 47 mailhost = ui.config('smtp', 'host')
48 48 if not mailhost:
49 49 raise util.Abort(_('smtp.host not configured - cannot send mail'))
50 50 mailport = util.getport(ui.config('smtp', 'port', 25))
51 51 ui.note(_('sending mail: smtp host %s, port %s\n') %
52 52 (mailhost, mailport))
53 53 s.connect(host=mailhost, port=mailport)
54 54 if starttls:
55 55 ui.note(_('(using starttls)\n'))
56 56 s.ehlo()
57 57 s.starttls()
58 58 s.ehlo()
59 59 username = ui.config('smtp', 'username')
60 60 password = ui.config('smtp', 'password')
61 61 if username and not password:
62 62 password = ui.getpass()
63 63 if username and password:
64 64 ui.note(_('(authenticating to mail server as %s)\n') %
65 65 (username))
66 66 try:
67 67 s.login(username, password)
68 68 except smtplib.SMTPException, inst:
69 69 raise util.Abort(inst)
70 70
71 71 def send(sender, recipients, msg):
72 72 try:
73 73 return s.sendmail(sender, recipients, msg)
74 74 except smtplib.SMTPRecipientsRefused, inst:
75 75 recipients = [r[1] for r in inst.recipients.values()]
76 76 raise util.Abort('\n' + '\n'.join(recipients))
77 77 except smtplib.SMTPException, inst:
78 78 raise util.Abort(inst)
79 79
80 80 return send
81 81
82 82 def _sendmail(ui, sender, recipients, msg):
83 83 '''send mail using sendmail.'''
84 84 program = ui.config('email', 'method')
85 85 cmdline = '%s -f %s %s' % (program, util.email(sender),
86 86 ' '.join(map(util.email, recipients)))
87 87 ui.note(_('sending mail: %s\n') % cmdline)
88 88 fp = util.popen(cmdline, 'w')
89 89 fp.write(msg)
90 90 ret = fp.close()
91 91 if ret:
92 92 raise util.Abort('%s %s' % (
93 93 os.path.basename(program.split(None, 1)[0]),
94 94 util.explainexit(ret)[0]))
95 95
96 96 def _mbox(mbox, sender, recipients, msg):
97 97 '''write mails to mbox'''
98 98 fp = open(mbox, 'ab+')
99 99 # Should be time.asctime(), but Windows prints 2-characters day
100 100 # of month instead of one. Make them print the same thing.
101 101 date = time.strftime('%a %b %d %H:%M:%S %Y', time.localtime())
102 102 fp.write('From %s %s\n' % (sender, date))
103 103 fp.write(msg)
104 104 fp.write('\n\n')
105 105 fp.close()
106 106
107 107 def connect(ui, mbox=None):
108 108 '''make a mail connection. return a function to send mail.
109 109 call as sendmail(sender, list-of-recipients, msg).'''
110 110 if mbox:
111 111 open(mbox, 'wb').close()
112 112 return lambda s, r, m: _mbox(mbox, s, r, m)
113 113 if ui.config('email', 'method', 'smtp') == 'smtp':
114 114 return _smtp(ui)
115 115 return lambda s, r, m: _sendmail(ui, s, r, m)
116 116
117 def sendmail(ui, sender, recipients, msg):
118 send = connect(ui)
117 def sendmail(ui, sender, recipients, msg, mbox=None):
118 send = connect(ui, mbox=mbox)
119 119 return send(sender, recipients, msg)
120 120
121 121 def validateconfig(ui):
122 122 '''determine if we have enough config data to try sending email.'''
123 123 method = ui.config('email', 'method', 'smtp')
124 124 if method == 'smtp':
125 125 if not ui.config('smtp', 'host'):
126 126 raise util.Abort(_('smtp specified as email transport, '
127 127 'but no smtp host configured'))
128 128 else:
129 129 if not util.findexe(method):
130 130 raise util.Abort(_('%r specified as email transport, '
131 131 'but not in PATH') % method)
132 132
133 133 def mimetextpatch(s, subtype='plain', display=False):
134 134 '''If patch in utf-8 transfer-encode it.'''
135 135
136 136 enc = None
137 137 for line in s.splitlines():
138 138 if len(line) > 950:
139 139 s = quopri.encodestring(s)
140 140 enc = "quoted-printable"
141 141 break
142 142
143 143 cs = 'us-ascii'
144 144 if not display:
145 145 try:
146 146 s.decode('us-ascii')
147 147 except UnicodeDecodeError:
148 148 try:
149 149 s.decode('utf-8')
150 150 cs = 'utf-8'
151 151 except UnicodeDecodeError:
152 152 # We'll go with us-ascii as a fallback.
153 153 pass
154 154
155 155 msg = email.MIMEText.MIMEText(s, subtype, cs)
156 156 if enc:
157 157 del msg['Content-Transfer-Encoding']
158 158 msg['Content-Transfer-Encoding'] = enc
159 159 return msg
160 160
161 161 def _charsets(ui):
162 162 '''Obtains charsets to send mail parts not containing patches.'''
163 163 charsets = [cs.lower() for cs in ui.configlist('email', 'charsets')]
164 164 fallbacks = [encoding.fallbackencoding.lower(),
165 165 encoding.encoding.lower(), 'utf-8']
166 166 for cs in fallbacks: # find unique charsets while keeping order
167 167 if cs not in charsets:
168 168 charsets.append(cs)
169 169 return [cs for cs in charsets if not cs.endswith('ascii')]
170 170
171 171 def _encode(ui, s, charsets):
172 172 '''Returns (converted) string, charset tuple.
173 173 Finds out best charset by cycling through sendcharsets in descending
174 174 order. Tries both encoding and fallbackencoding for input. Only as
175 175 last resort send as is in fake ascii.
176 176 Caveat: Do not use for mail parts containing patches!'''
177 177 try:
178 178 s.decode('ascii')
179 179 except UnicodeDecodeError:
180 180 sendcharsets = charsets or _charsets(ui)
181 181 for ics in (encoding.encoding, encoding.fallbackencoding):
182 182 try:
183 183 u = s.decode(ics)
184 184 except UnicodeDecodeError:
185 185 continue
186 186 for ocs in sendcharsets:
187 187 try:
188 188 return u.encode(ocs), ocs
189 189 except UnicodeEncodeError:
190 190 pass
191 191 except LookupError:
192 192 ui.warn(_('ignoring invalid sendcharset: %s\n') % ocs)
193 193 # if ascii, or all conversion attempts fail, send (broken) ascii
194 194 return s, 'us-ascii'
195 195
196 196 def headencode(ui, s, charsets=None, display=False):
197 197 '''Returns RFC-2047 compliant header from given string.'''
198 198 if not display:
199 199 # split into words?
200 200 s, cs = _encode(ui, s, charsets)
201 201 return str(email.Header.Header(s, cs))
202 202 return s
203 203
204 204 def _addressencode(ui, name, addr, charsets=None):
205 205 name = headencode(ui, name, charsets)
206 206 try:
207 207 acc, dom = addr.split('@')
208 208 acc = acc.encode('ascii')
209 209 dom = dom.decode(encoding.encoding).encode('idna')
210 210 addr = '%s@%s' % (acc, dom)
211 211 except UnicodeDecodeError:
212 212 raise util.Abort(_('invalid email address: %s') % addr)
213 213 except ValueError:
214 214 try:
215 215 # too strict?
216 216 addr = addr.encode('ascii')
217 217 except UnicodeDecodeError:
218 218 raise util.Abort(_('invalid local address: %s') % addr)
219 219 return email.Utils.formataddr((name, addr))
220 220
221 221 def addressencode(ui, address, charsets=None, display=False):
222 222 '''Turns address into RFC-2047 compliant header.'''
223 223 if display or not address:
224 224 return address or ''
225 225 name, addr = email.Utils.parseaddr(address)
226 226 return _addressencode(ui, name, addr, charsets)
227 227
228 228 def addrlistencode(ui, addrs, charsets=None, display=False):
229 229 '''Turns a list of addresses into a list of RFC-2047 compliant headers.
230 230 A single element of input list may contain multiple addresses, but output
231 231 always has one address per item'''
232 232 if display:
233 233 return [a.strip() for a in addrs if a.strip()]
234 234
235 235 result = []
236 236 for name, addr in email.Utils.getaddresses(addrs):
237 237 if name or addr:
238 238 result.append(_addressencode(ui, name, addr, charsets))
239 239 return result
240 240
241 241 def mimeencode(ui, s, charsets=None, display=False):
242 242 '''creates mime text object, encodes it if needed, and sets
243 243 charset and transfer-encoding accordingly.'''
244 244 cs = 'us-ascii'
245 245 if not display:
246 246 s, cs = _encode(ui, s, charsets)
247 247 return email.MIMEText.MIMEText(s, 'plain', cs)
@@ -1,397 +1,451 b''
1 1
2 2 $ cat <<EOF >> $HGRCPATH
3 3 > [extensions]
4 4 > notify=
5 5 >
6 6 > [hooks]
7 7 > incoming.notify = python:hgext.notify.hook
8 8 >
9 9 > [notify]
10 10 > sources = pull
11 11 > diffstat = False
12 12 >
13 13 > [usersubs]
14 14 > foo@bar = *
15 15 >
16 16 > [reposubs]
17 17 > * = baz
18 18 > EOF
19 19 $ hg help notify
20 20 notify extension - hooks for sending email push notifications
21 21
22 22 This extension let you run hooks sending email notifications when changesets
23 23 are being pushed, from the sending or receiving side.
24 24
25 25 First, enable the extension as explained in "hg help extensions", and register
26 26 the hook you want to run. "incoming" and "outgoing" hooks are run by the
27 27 changesets receiver while the "outgoing" one is for the sender:
28 28
29 29 [hooks]
30 30 # one email for each incoming changeset
31 31 incoming.notify = python:hgext.notify.hook
32 32 # one email for all incoming changesets
33 33 changegroup.notify = python:hgext.notify.hook
34 34
35 35 # one email for all outgoing changesets
36 36 outgoing.notify = python:hgext.notify.hook
37 37
38 38 Now the hooks are running, subscribers must be assigned to repositories. Use
39 39 the "[usersubs]" section to map repositories to a given email or the
40 40 "[reposubs]" section to map emails to a single repository:
41 41
42 42 [usersubs]
43 43 # key is subscriber email, value is a comma-separated list of glob
44 44 # patterns
45 45 user@host = pattern
46 46
47 47 [reposubs]
48 48 # key is glob pattern, value is a comma-separated list of subscriber
49 49 # emails
50 50 pattern = user@host
51 51
52 52 Glob patterns are matched against absolute path to repository root. The
53 53 subscriptions can be defined in their own file and referenced with:
54 54
55 55 [notify]
56 56 config = /path/to/subscriptionsfile
57 57
58 58 Alternatively, they can be added to Mercurial configuration files by setting
59 59 the previous entry to an empty value.
60 60
61 61 At this point, notifications should be generated but will not be sent until
62 62 you set the "notify.test" entry to "False".
63 63
64 64 Notifications content can be tweaked with the following configuration entries:
65 65
66 66 notify.test
67 67 If "True", print messages to stdout instead of sending them. Default: True.
68 68
69 69 notify.sources
70 70 Space separated list of change sources. Notifications are sent only if it
71 71 includes the incoming or outgoing changes source. Incoming sources can be
72 72 "serve" for changes coming from http or ssh, "pull" for pulled changes,
73 73 "unbundle" for changes added by "hg unbundle" or "push" for changes being
74 74 pushed locally. Outgoing sources are the same except for "unbundle" which is
75 75 replaced by "bundle". Default: serve.
76 76
77 77 notify.strip
78 78 Number of leading slashes to strip from url paths. By default, notifications
79 79 references repositories with their absolute path. "notify.strip" let you
80 80 turn them into relative paths. For example, "notify.strip=3" will change
81 81 "/long/path/repository" into "repository". Default: 0.
82 82
83 83 notify.domain
84 84 If subscribers emails or the from email have no domain set, complete them
85 85 with this value.
86 86
87 87 notify.style
88 88 Style file to use when formatting emails.
89 89
90 90 notify.template
91 91 Template to use when formatting emails.
92 92
93 93 notify.incoming
94 94 Template to use when run as incoming hook, override "notify.template".
95 95
96 96 notify.outgoing
97 97 Template to use when run as outgoing hook, override "notify.template".
98 98
99 99 notify.changegroup
100 100 Template to use when running as changegroup hook, override
101 101 "notify.template".
102 102
103 103 notify.maxdiff
104 104 Maximum number of diff lines to include in notification email. Set to 0 to
105 105 disable the diff, -1 to include all of it. Default: 300.
106 106
107 107 notify.maxsubject
108 108 Maximum number of characters in emails subject line. Default: 67.
109 109
110 110 notify.diffstat
111 111 Set to True to include a diffstat before diff content. Default: True.
112 112
113 113 notify.merge
114 114 If True, send notifications for merge changesets. Default: True.
115 115
116 notify.mbox
117 If set, append mails to this mbox file instead of sending. Default: None.
118
116 119 If set, the following entries will also be used to customize the
117 120 notifications:
118 121
119 122 email.from
120 123 Email "From" address to use if none can be found in generated email content.
121 124
122 125 web.baseurl
123 126 Root repository browsing URL to combine with repository paths when making
124 127 references. See also "notify.strip".
125 128
126 129 no commands defined
127 130 $ hg init a
128 131 $ echo a > a/a
129 132
130 133 commit
131 134
132 135 $ hg --cwd a commit -Ama -d '0 0'
133 136 adding a
134 137
135 138
136 139 clone
137 140
138 141 $ hg --traceback clone a b
139 142 updating to branch default
140 143 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
141 144 $ echo a >> a/a
142 145
143 146 commit
144 147
145 148 $ hg --traceback --cwd a commit -Amb -d '1 0'
146 149
147 150 on Mac OS X 10.5 the tmp path is very long so would get stripped in the subject line
148 151
149 152 $ cat <<EOF >> $HGRCPATH
150 153 > [notify]
151 154 > maxsubject = 200
152 155 > EOF
153 156
154 157 the python call below wraps continuation lines, which appear on Mac OS X 10.5 because
155 158 of the very long subject line
156 159 pull (minimal config)
157 160
158 161 $ hg --traceback --cwd b pull ../a | \
159 162 > python -c 'import sys,re; print re.sub("\n[\t ]", " ", sys.stdin.read()),'
160 163 pulling from ../a
161 164 searching for changes
162 165 adding changesets
163 166 adding manifests
164 167 adding file changes
165 168 added 1 changesets with 1 changes to 1 files
166 169 Content-Type: text/plain; charset="us-ascii"
167 170 MIME-Version: 1.0
168 171 Content-Transfer-Encoding: 7bit
169 172 Date: * (glob)
170 173 Subject: changeset in $TESTTMP/b: b
171 174 From: test
172 175 X-Hg-Notification: changeset 0647d048b600
173 176 Message-Id: <*> (glob)
174 177 To: baz, foo@bar
175 178
176 179 changeset 0647d048b600 in $TESTTMP/b (glob)
177 180 details: $TESTTMP/b?cmd=changeset;node=0647d048b600
178 181 description: b
179 182
180 183 diffs (6 lines):
181 184
182 185 diff -r cb9a9f314b8b -r 0647d048b600 a
183 186 --- a/a Thu Jan 01 00:00:00 1970 +0000
184 187 +++ b/a Thu Jan 01 00:00:01 1970 +0000
185 188 @@ -1,1 +1,2 @@ a
186 189 +a
187 190 (run 'hg update' to get a working copy)
188 191 $ cat <<EOF >> $HGRCPATH
189 192 > [notify]
190 193 > config = `pwd`/.notify.conf
191 194 > domain = test.com
192 195 > strip = 42
193 196 > template = Subject: {desc|firstline|strip}\nFrom: {author}\nX-Test: foo\n\nchangeset {node|short} in {webroot}\ndescription:\n\t{desc|tabindent|strip}
194 197 >
195 198 > [web]
196 199 > baseurl = http://test/
197 200 > EOF
198 201
199 202 fail for config file is missing
200 203
201 204 $ hg --cwd b rollback
202 205 repository tip rolled back to revision 0 (undo pull)
203 206 $ hg --cwd b pull ../a 2>&1 | grep 'error.*\.notify\.conf' > /dev/null && echo pull failed
204 207 pull failed
205 208 $ touch ".notify.conf"
206 209
207 210 pull
208 211
209 212 $ hg --cwd b rollback
210 213 repository tip rolled back to revision 0 (undo pull)
211 214 $ hg --traceback --cwd b pull ../a | \
212 215 > python -c 'import sys,re; print re.sub("\n\t", " ", sys.stdin.read()),'
213 216 pulling from ../a
214 217 searching for changes
215 218 adding changesets
216 219 adding manifests
217 220 adding file changes
218 221 added 1 changesets with 1 changes to 1 files
219 222 Content-Type: text/plain; charset="us-ascii"
220 223 MIME-Version: 1.0
221 224 Content-Transfer-Encoding: 7bit
222 225 X-Test: foo
223 226 Date: * (glob)
224 227 Subject: b
225 228 From: test@test.com
226 229 X-Hg-Notification: changeset 0647d048b600
227 230 Message-Id: <*> (glob)
228 231 To: baz@test.com, foo@bar
229 232
230 233 changeset 0647d048b600 in b
231 234 description: b
232 235 diffs (6 lines):
233 236
234 237 diff -r cb9a9f314b8b -r 0647d048b600 a
235 238 --- a/a Thu Jan 01 00:00:00 1970 +0000
236 239 +++ b/a Thu Jan 01 00:00:01 1970 +0000
237 240 @@ -1,1 +1,2 @@
238 241 a
239 242 +a
240 243 (run 'hg update' to get a working copy)
241 244
242 245 $ cat << EOF >> $HGRCPATH
243 246 > [hooks]
244 247 > incoming.notify = python:hgext.notify.hook
245 248 >
246 249 > [notify]
247 250 > sources = pull
248 251 > diffstat = True
249 252 > EOF
250 253
251 254 pull
252 255
253 256 $ hg --cwd b rollback
254 257 repository tip rolled back to revision 0 (undo pull)
255 258 $ hg --traceback --cwd b pull ../a | \
256 259 > python -c 'import sys,re; print re.sub("\n\t", " ", sys.stdin.read()),'
257 260 pulling from ../a
258 261 searching for changes
259 262 adding changesets
260 263 adding manifests
261 264 adding file changes
262 265 added 1 changesets with 1 changes to 1 files
263 266 Content-Type: text/plain; charset="us-ascii"
264 267 MIME-Version: 1.0
265 268 Content-Transfer-Encoding: 7bit
266 269 X-Test: foo
267 270 Date: * (glob)
268 271 Subject: b
269 272 From: test@test.com
270 273 X-Hg-Notification: changeset 0647d048b600
271 274 Message-Id: <*> (glob)
272 275 To: baz@test.com, foo@bar
273 276
274 277 changeset 0647d048b600 in b
275 278 description: b
276 279 diffstat:
277 280
278 281 a | 1 +
279 282 1 files changed, 1 insertions(+), 0 deletions(-)
280 283
281 284 diffs (6 lines):
282 285
283 286 diff -r cb9a9f314b8b -r 0647d048b600 a
284 287 --- a/a Thu Jan 01 00:00:00 1970 +0000
285 288 +++ b/a Thu Jan 01 00:00:01 1970 +0000
286 289 @@ -1,1 +1,2 @@
287 290 a
288 291 +a
289 292 (run 'hg update' to get a working copy)
290 293
291 294 test merge
292 295
293 296 $ cd a
294 297 $ hg up -C 0
295 298 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
296 299 $ echo a >> a
297 300 $ hg ci -Am adda2 -d '2 0'
298 301 created new head
299 302 $ hg merge
300 303 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
301 304 (branch merge, don't forget to commit)
302 305 $ hg ci -m merge -d '3 0'
303 306 $ cd ..
304 307 $ hg --traceback --cwd b pull ../a | \
305 308 > python -c 'import sys,re; print re.sub("\n\t", " ", sys.stdin.read()),'
306 309 pulling from ../a
307 310 searching for changes
308 311 adding changesets
309 312 adding manifests
310 313 adding file changes
311 314 added 2 changesets with 0 changes to 0 files
312 315 Content-Type: text/plain; charset="us-ascii"
313 316 MIME-Version: 1.0
314 317 Content-Transfer-Encoding: 7bit
315 318 X-Test: foo
316 319 Date: * (glob)
317 320 Subject: adda2
318 321 From: test@test.com
319 322 X-Hg-Notification: changeset 0a184ce6067f
320 323 Message-Id: <*> (glob)
321 324 To: baz@test.com, foo@bar
322 325
323 326 changeset 0a184ce6067f in b
324 327 description: adda2
325 328 diffstat:
326 329
327 330 a | 1 +
328 331 1 files changed, 1 insertions(+), 0 deletions(-)
329 332
330 333 diffs (6 lines):
331 334
332 335 diff -r cb9a9f314b8b -r 0a184ce6067f a
333 336 --- a/a Thu Jan 01 00:00:00 1970 +0000
334 337 +++ b/a Thu Jan 01 00:00:02 1970 +0000
335 338 @@ -1,1 +1,2 @@
336 339 a
337 340 +a
338 341 Content-Type: text/plain; charset="us-ascii"
339 342 MIME-Version: 1.0
340 343 Content-Transfer-Encoding: 7bit
341 344 X-Test: foo
342 345 Date: * (glob)
343 346 Subject: merge
344 347 From: test@test.com
345 348 X-Hg-Notification: changeset 6a0cf76b2701
346 349 Message-Id: <*> (glob)
347 350 To: baz@test.com, foo@bar
348 351
349 352 changeset 6a0cf76b2701 in b
350 353 description: merge
351 354 (run 'hg update' to get a working copy)
352 355
353 truncate multi-byte subject
356 non-ascii content and truncation of multi-byte subject
354 357
355 358 $ cat <<EOF >> $HGRCPATH
356 359 > [notify]
357 360 > maxsubject = 4
358 361 > EOF
359 362 $ echo a >> a/a
360 363 $ hg --cwd a --encoding utf-8 commit -A -d '0 0' \
361 364 > -m `python -c 'print "\xc3\xa0\xc3\xa1\xc3\xa2\xc3\xa3\xc3\xa4"'`
362 365 $ hg --traceback --cwd b --encoding utf-8 pull ../a | \
363 366 > python -c 'import sys,re; print re.sub("\n\t", " ", sys.stdin.read()),'
364 367 pulling from ../a
365 368 searching for changes
366 369 adding changesets
367 370 adding manifests
368 371 adding file changes
369 372 added 1 changesets with 1 changes to 1 files
370 373 Content-Type: text/plain; charset="us-ascii"
371 374 MIME-Version: 1.0
372 375 Content-Transfer-Encoding: 8bit
373 376 X-Test: foo
374 377 Date: * (glob)
375 378 Subject: \xc3\xa0... (esc)
376 379 From: test@test.com
377 380 X-Hg-Notification: changeset 7ea05ad269dc
378 381 Message-Id: <*> (glob)
379 382 To: baz@test.com, foo@bar
380 383
381 384 changeset 7ea05ad269dc in b
382 385 description: \xc3\xa0\xc3\xa1\xc3\xa2\xc3\xa3\xc3\xa4 (esc)
383 386 diffstat:
384 387
385 388 a | 1 +
386 389 1 files changed, 1 insertions(+), 0 deletions(-)
387 390
388 391 diffs (7 lines):
389 392
390 393 diff -r 6a0cf76b2701 -r 7ea05ad269dc a
391 394 --- a/a Thu Jan 01 00:00:03 1970 +0000
392 395 +++ b/a Thu Jan 01 00:00:00 1970 +0000
393 396 @@ -1,2 +1,3 @@
394 397 a
395 398 a
396 399 +a
397 400 (run 'hg update' to get a working copy)
401
402 long lines
403
404 $ cat <<EOF >> $HGRCPATH
405 > [notify]
406 > maxsubject = 67
407 > test = False
408 > mbox = mbox
409 > EOF
410 $ python -c 'print "no" * 500' >> a/a
411 $ hg --cwd a commit -A -m "long line"
412 $ hg --traceback --cwd b pull ../a
413 pulling from ../a
414 searching for changes
415 adding changesets
416 adding manifests
417 adding file changes
418 added 1 changesets with 1 changes to 1 files
419 notify: sending 2 subscribers 1 changes
420 (run 'hg update' to get a working copy)
421 $ python -c 'import sys,re; print re.sub("\n\t", " ", file("b/mbox").read()),'
422 From test@test.com ... ... .. ..:..:.. .... (re)
423 Content-Type: text/plain; charset="us-ascii"
424 MIME-Version: 1.0
425 Content-Transfer-Encoding: 7bit
426 X-Test: foo
427 Date: * (glob)
428 Subject: long line
429 From: test@test.com
430 X-Hg-Notification: changeset e0be44cf638b
431 Message-Id: <hg.e0be44cf638b.*.*@*> (glob)
432 To: baz@test.com, foo@bar
433
434 changeset e0be44cf638b in b
435 description: long line
436 diffstat:
437
438 a | 1 +
439 1 files changed, 1 insertions(+), 0 deletions(-)
440
441 diffs (8 lines):
442
443 diff -r 7ea05ad269dc -r e0be44cf638b a
444 --- a/a Thu Jan 01 00:00:00 1970 +0000
445 +++ b/a Thu Jan 01 00:00:00 1970 +0000
446 @@ -1,3 +1,4 @@
447 a
448 a
449 a
450 +nononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononononono
451
General Comments 0
You need to be logged in to leave comments. Login now