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