##// END OF EJS Templates
notify: send changesets on 'outgoing' hook, updated doc
Ingo Bressler -
r14617:23f4e1e4 default
parent child Browse files
Show More
@@ -1,316 +1,319
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 notifications at commit/push time
8 '''hooks for sending email notifications at commit/push time
9
9
10 Subscriptions can be managed through a hgrc file. Default mode is to
10 Subscriptions can be managed through a hgrc file. Default mode is to
11 print messages to stdout, for testing and configuring.
11 print messages to stdout, for testing and configuring.
12
12
13 To use, configure the notify extension and enable it in hgrc like
13 To use, configure the notify extension and enable it in hgrc like
14 this::
14 this::
15
15
16 [extensions]
16 [extensions]
17 notify =
17 notify =
18
18
19 [hooks]
19 [hooks]
20 # one email for each incoming changeset
20 # one email for each incoming changeset
21 incoming.notify = python:hgext.notify.hook
21 incoming.notify = python:hgext.notify.hook
22 # batch emails when many changesets incoming at one time
22 # batch emails when many changesets incoming at one time
23 changegroup.notify = python:hgext.notify.hook
23 changegroup.notify = python:hgext.notify.hook
24 # batch emails when many changesets outgoing at one time (client side)
25 outgoing.notify = python:hgext.notify.hook
24
26
25 [notify]
27 [notify]
26 # config items go here
28 # config items go here
27
29
28 Required configuration items::
30 Required configuration items::
29
31
30 config = /path/to/file # file containing subscriptions
32 config = /path/to/file # file containing subscriptions
31
33
32 Optional configuration items::
34 Optional configuration items::
33
35
34 test = True # print messages to stdout for testing
36 test = True # print messages to stdout for testing
35 strip = 3 # number of slashes to strip for url paths
37 strip = 3 # number of slashes to strip for url paths
36 domain = example.com # domain to use if committer missing domain
38 domain = example.com # domain to use if committer missing domain
37 style = ... # style file to use when formatting email
39 style = ... # style file to use when formatting email
38 template = ... # template to use when formatting email
40 template = ... # template to use when formatting email
39 incoming = ... # template to use when run as incoming hook
41 incoming = ... # template to use when run as incoming hook
40 changegroup = ... # template when run as changegroup hook
42 outgoing = ... # template to use when run as outgoing hook
43 changegroup = ... # template to use when run as changegroup hook
41 maxdiff = 300 # max lines of diffs to include (0=none, -1=all)
44 maxdiff = 300 # max lines of diffs to include (0=none, -1=all)
42 maxsubject = 67 # truncate subject line longer than this
45 maxsubject = 67 # truncate subject line longer than this
43 diffstat = True # add a diffstat before the diff content
46 diffstat = True # add a diffstat before the diff content
44 sources = serve # notify if source of incoming changes in this list
47 sources = serve # notify if source of incoming changes in this list
45 # (serve == ssh or http, push, pull, bundle)
48 # (serve == ssh or http, push, pull, bundle)
46 merge = False # send notification for merges (default True)
49 merge = False # send notification for merges (default True)
47 [email]
50 [email]
48 from = user@host.com # email address to send as if none given
51 from = user@host.com # email address to send as if none given
49 [web]
52 [web]
50 baseurl = http://hgserver/... # root of hg web site for browsing commits
53 baseurl = http://hgserver/... # root of hg web site for browsing commits
51
54
52 The notify config file has same format as a regular hgrc file. It has
55 The notify config file has same format as a regular hgrc file. It has
53 two sections so you can express subscriptions in whatever way is
56 two sections so you can express subscriptions in whatever way is
54 handier for you.
57 handier for you.
55
58
56 ::
59 ::
57
60
58 [usersubs]
61 [usersubs]
59 # key is subscriber email, value is ","-separated list of glob patterns
62 # key is subscriber email, value is ","-separated list of glob patterns
60 user@host = pattern
63 user@host = pattern
61
64
62 [reposubs]
65 [reposubs]
63 # key is glob pattern, value is ","-separated list of subscriber emails
66 # key is glob pattern, value is ","-separated list of subscriber emails
64 pattern = user@host
67 pattern = user@host
65
68
66 Glob patterns are matched against path to repository root.
69 Glob patterns are matched against path to repository root.
67
70
68 If you like, you can put notify config file in repository that users
71 If you like, you can put notify config file in repository that users
69 can push changes to, they can manage their own subscriptions.
72 can push changes to, they can manage their own subscriptions.
70 '''
73 '''
71
74
72 from mercurial.i18n import _
75 from mercurial.i18n import _
73 from mercurial import patch, cmdutil, templater, util, mail
76 from mercurial import patch, cmdutil, templater, util, mail
74 import email.Parser, email.Errors, fnmatch, socket, time
77 import email.Parser, email.Errors, fnmatch, socket, time
75
78
76 # template for single changeset can include email headers.
79 # template for single changeset can include email headers.
77 single_template = '''
80 single_template = '''
78 Subject: changeset in {webroot}: {desc|firstline|strip}
81 Subject: changeset in {webroot}: {desc|firstline|strip}
79 From: {author}
82 From: {author}
80
83
81 changeset {node|short} in {root}
84 changeset {node|short} in {root}
82 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
85 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
83 description:
86 description:
84 \t{desc|tabindent|strip}
87 \t{desc|tabindent|strip}
85 '''.lstrip()
88 '''.lstrip()
86
89
87 # template for multiple changesets should not contain email headers,
90 # template for multiple changesets should not contain email headers,
88 # because only first set of headers will be used and result will look
91 # because only first set of headers will be used and result will look
89 # strange.
92 # strange.
90 multiple_template = '''
93 multiple_template = '''
91 changeset {node|short} in {root}
94 changeset {node|short} in {root}
92 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
95 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
93 summary: {desc|firstline}
96 summary: {desc|firstline}
94 '''
97 '''
95
98
96 deftemplates = {
99 deftemplates = {
97 'changegroup': multiple_template,
100 'changegroup': multiple_template,
98 }
101 }
99
102
100 class notifier(object):
103 class notifier(object):
101 '''email notification class.'''
104 '''email notification class.'''
102
105
103 def __init__(self, ui, repo, hooktype):
106 def __init__(self, ui, repo, hooktype):
104 self.ui = ui
107 self.ui = ui
105 cfg = self.ui.config('notify', 'config')
108 cfg = self.ui.config('notify', 'config')
106 if cfg:
109 if cfg:
107 self.ui.readconfig(cfg, sections=['usersubs', 'reposubs'])
110 self.ui.readconfig(cfg, sections=['usersubs', 'reposubs'])
108 self.repo = repo
111 self.repo = repo
109 self.stripcount = int(self.ui.config('notify', 'strip', 0))
112 self.stripcount = int(self.ui.config('notify', 'strip', 0))
110 self.root = self.strip(self.repo.root)
113 self.root = self.strip(self.repo.root)
111 self.domain = self.ui.config('notify', 'domain')
114 self.domain = self.ui.config('notify', 'domain')
112 self.test = self.ui.configbool('notify', 'test', True)
115 self.test = self.ui.configbool('notify', 'test', True)
113 self.charsets = mail._charsets(self.ui)
116 self.charsets = mail._charsets(self.ui)
114 self.subs = self.subscribers()
117 self.subs = self.subscribers()
115 self.merge = self.ui.configbool('notify', 'merge', True)
118 self.merge = self.ui.configbool('notify', 'merge', True)
116
119
117 mapfile = self.ui.config('notify', 'style')
120 mapfile = self.ui.config('notify', 'style')
118 template = (self.ui.config('notify', hooktype) or
121 template = (self.ui.config('notify', hooktype) or
119 self.ui.config('notify', 'template'))
122 self.ui.config('notify', 'template'))
120 self.t = cmdutil.changeset_templater(self.ui, self.repo,
123 self.t = cmdutil.changeset_templater(self.ui, self.repo,
121 False, None, mapfile, False)
124 False, None, mapfile, False)
122 if not mapfile and not template:
125 if not mapfile and not template:
123 template = deftemplates.get(hooktype) or single_template
126 template = deftemplates.get(hooktype) or single_template
124 if template:
127 if template:
125 template = templater.parsestring(template, quoted=False)
128 template = templater.parsestring(template, quoted=False)
126 self.t.use_template(template)
129 self.t.use_template(template)
127
130
128 def strip(self, path):
131 def strip(self, path):
129 '''strip leading slashes from local path, turn into web-safe path.'''
132 '''strip leading slashes from local path, turn into web-safe path.'''
130
133
131 path = util.pconvert(path)
134 path = util.pconvert(path)
132 count = self.stripcount
135 count = self.stripcount
133 while count > 0:
136 while count > 0:
134 c = path.find('/')
137 c = path.find('/')
135 if c == -1:
138 if c == -1:
136 break
139 break
137 path = path[c + 1:]
140 path = path[c + 1:]
138 count -= 1
141 count -= 1
139 return path
142 return path
140
143
141 def fixmail(self, addr):
144 def fixmail(self, addr):
142 '''try to clean up email addresses.'''
145 '''try to clean up email addresses.'''
143
146
144 addr = util.email(addr.strip())
147 addr = util.email(addr.strip())
145 if self.domain:
148 if self.domain:
146 a = addr.find('@localhost')
149 a = addr.find('@localhost')
147 if a != -1:
150 if a != -1:
148 addr = addr[:a]
151 addr = addr[:a]
149 if '@' not in addr:
152 if '@' not in addr:
150 return addr + '@' + self.domain
153 return addr + '@' + self.domain
151 return addr
154 return addr
152
155
153 def subscribers(self):
156 def subscribers(self):
154 '''return list of email addresses of subscribers to this repo.'''
157 '''return list of email addresses of subscribers to this repo.'''
155 subs = set()
158 subs = set()
156 for user, pats in self.ui.configitems('usersubs'):
159 for user, pats in self.ui.configitems('usersubs'):
157 for pat in pats.split(','):
160 for pat in pats.split(','):
158 if fnmatch.fnmatch(self.repo.root, pat.strip()):
161 if fnmatch.fnmatch(self.repo.root, pat.strip()):
159 subs.add(self.fixmail(user))
162 subs.add(self.fixmail(user))
160 for pat, users in self.ui.configitems('reposubs'):
163 for pat, users in self.ui.configitems('reposubs'):
161 if fnmatch.fnmatch(self.repo.root, pat):
164 if fnmatch.fnmatch(self.repo.root, pat):
162 for user in users.split(','):
165 for user in users.split(','):
163 subs.add(self.fixmail(user))
166 subs.add(self.fixmail(user))
164 return [mail.addressencode(self.ui, s, self.charsets, self.test)
167 return [mail.addressencode(self.ui, s, self.charsets, self.test)
165 for s in sorted(subs)]
168 for s in sorted(subs)]
166
169
167 def url(self, path=None):
170 def url(self, path=None):
168 return self.ui.config('web', 'baseurl') + (path or self.root)
171 return self.ui.config('web', 'baseurl') + (path or self.root)
169
172
170 def node(self, ctx, **props):
173 def node(self, ctx, **props):
171 '''format one changeset, unless it is a suppressed merge.'''
174 '''format one changeset, unless it is a suppressed merge.'''
172 if not self.merge and len(ctx.parents()) > 1:
175 if not self.merge and len(ctx.parents()) > 1:
173 return False
176 return False
174 self.t.show(ctx, changes=ctx.changeset(),
177 self.t.show(ctx, changes=ctx.changeset(),
175 baseurl=self.ui.config('web', 'baseurl'),
178 baseurl=self.ui.config('web', 'baseurl'),
176 root=self.repo.root, webroot=self.root, **props)
179 root=self.repo.root, webroot=self.root, **props)
177 return True
180 return True
178
181
179 def skipsource(self, source):
182 def skipsource(self, source):
180 '''true if incoming changes from this source should be skipped.'''
183 '''true if incoming changes from this source should be skipped.'''
181 ok_sources = self.ui.config('notify', 'sources', 'serve').split()
184 ok_sources = self.ui.config('notify', 'sources', 'serve').split()
182 return source not in ok_sources
185 return source not in ok_sources
183
186
184 def send(self, ctx, count, data):
187 def send(self, ctx, count, data):
185 '''send message.'''
188 '''send message.'''
186
189
187 p = email.Parser.Parser()
190 p = email.Parser.Parser()
188 try:
191 try:
189 msg = p.parsestr(data)
192 msg = p.parsestr(data)
190 except email.Errors.MessageParseError, inst:
193 except email.Errors.MessageParseError, inst:
191 raise util.Abort(inst)
194 raise util.Abort(inst)
192
195
193 # store sender and subject
196 # store sender and subject
194 sender, subject = msg['From'], msg['Subject']
197 sender, subject = msg['From'], msg['Subject']
195 del msg['From'], msg['Subject']
198 del msg['From'], msg['Subject']
196
199
197 if not msg.is_multipart():
200 if not msg.is_multipart():
198 # create fresh mime message from scratch
201 # create fresh mime message from scratch
199 # (multipart templates must take care of this themselves)
202 # (multipart templates must take care of this themselves)
200 headers = msg.items()
203 headers = msg.items()
201 payload = msg.get_payload()
204 payload = msg.get_payload()
202 # for notification prefer readability over data precision
205 # for notification prefer readability over data precision
203 msg = mail.mimeencode(self.ui, payload, self.charsets, self.test)
206 msg = mail.mimeencode(self.ui, payload, self.charsets, self.test)
204 # reinstate custom headers
207 # reinstate custom headers
205 for k, v in headers:
208 for k, v in headers:
206 msg[k] = v
209 msg[k] = v
207
210
208 msg['Date'] = util.datestr(format="%a, %d %b %Y %H:%M:%S %1%2")
211 msg['Date'] = util.datestr(format="%a, %d %b %Y %H:%M:%S %1%2")
209
212
210 # try to make subject line exist and be useful
213 # try to make subject line exist and be useful
211 if not subject:
214 if not subject:
212 if count > 1:
215 if count > 1:
213 subject = _('%s: %d new changesets') % (self.root, count)
216 subject = _('%s: %d new changesets') % (self.root, count)
214 else:
217 else:
215 s = ctx.description().lstrip().split('\n', 1)[0].rstrip()
218 s = ctx.description().lstrip().split('\n', 1)[0].rstrip()
216 subject = '%s: %s' % (self.root, s)
219 subject = '%s: %s' % (self.root, s)
217 maxsubject = int(self.ui.config('notify', 'maxsubject', 67))
220 maxsubject = int(self.ui.config('notify', 'maxsubject', 67))
218 if maxsubject:
221 if maxsubject:
219 subject = util.ellipsis(subject, maxsubject)
222 subject = util.ellipsis(subject, maxsubject)
220 msg['Subject'] = mail.headencode(self.ui, subject,
223 msg['Subject'] = mail.headencode(self.ui, subject,
221 self.charsets, self.test)
224 self.charsets, self.test)
222
225
223 # try to make message have proper sender
226 # try to make message have proper sender
224 if not sender:
227 if not sender:
225 sender = self.ui.config('email', 'from') or self.ui.username()
228 sender = self.ui.config('email', 'from') or self.ui.username()
226 if '@' not in sender or '@localhost' in sender:
229 if '@' not in sender or '@localhost' in sender:
227 sender = self.fixmail(sender)
230 sender = self.fixmail(sender)
228 msg['From'] = mail.addressencode(self.ui, sender,
231 msg['From'] = mail.addressencode(self.ui, sender,
229 self.charsets, self.test)
232 self.charsets, self.test)
230
233
231 msg['X-Hg-Notification'] = 'changeset %s' % ctx
234 msg['X-Hg-Notification'] = 'changeset %s' % ctx
232 if not msg['Message-Id']:
235 if not msg['Message-Id']:
233 msg['Message-Id'] = ('<hg.%s.%s.%s@%s>' %
236 msg['Message-Id'] = ('<hg.%s.%s.%s@%s>' %
234 (ctx, int(time.time()),
237 (ctx, int(time.time()),
235 hash(self.repo.root), socket.getfqdn()))
238 hash(self.repo.root), socket.getfqdn()))
236 msg['To'] = ', '.join(self.subs)
239 msg['To'] = ', '.join(self.subs)
237
240
238 msgtext = msg.as_string()
241 msgtext = msg.as_string()
239 if self.test:
242 if self.test:
240 self.ui.write(msgtext)
243 self.ui.write(msgtext)
241 if not msgtext.endswith('\n'):
244 if not msgtext.endswith('\n'):
242 self.ui.write('\n')
245 self.ui.write('\n')
243 else:
246 else:
244 self.ui.status(_('notify: sending %d subscribers %d changes\n') %
247 self.ui.status(_('notify: sending %d subscribers %d changes\n') %
245 (len(self.subs), count))
248 (len(self.subs), count))
246 mail.sendmail(self.ui, util.email(msg['From']),
249 mail.sendmail(self.ui, util.email(msg['From']),
247 self.subs, msgtext)
250 self.subs, msgtext)
248
251
249 def diff(self, ctx, ref=None):
252 def diff(self, ctx, ref=None):
250
253
251 maxdiff = int(self.ui.config('notify', 'maxdiff', 300))
254 maxdiff = int(self.ui.config('notify', 'maxdiff', 300))
252 prev = ctx.p1().node()
255 prev = ctx.p1().node()
253 ref = ref and ref.node() or ctx.node()
256 ref = ref and ref.node() or ctx.node()
254 chunks = patch.diff(self.repo, prev, ref, opts=patch.diffopts(self.ui))
257 chunks = patch.diff(self.repo, prev, ref, opts=patch.diffopts(self.ui))
255 difflines = ''.join(chunks).splitlines()
258 difflines = ''.join(chunks).splitlines()
256
259
257 if self.ui.configbool('notify', 'diffstat', True):
260 if self.ui.configbool('notify', 'diffstat', True):
258 s = patch.diffstat(difflines)
261 s = patch.diffstat(difflines)
259 # s may be nil, don't include the header if it is
262 # s may be nil, don't include the header if it is
260 if s:
263 if s:
261 self.ui.write('\ndiffstat:\n\n%s' % s)
264 self.ui.write('\ndiffstat:\n\n%s' % s)
262
265
263 if maxdiff == 0:
266 if maxdiff == 0:
264 return
267 return
265 elif maxdiff > 0 and len(difflines) > maxdiff:
268 elif maxdiff > 0 and len(difflines) > maxdiff:
266 msg = _('\ndiffs (truncated from %d to %d lines):\n\n')
269 msg = _('\ndiffs (truncated from %d to %d lines):\n\n')
267 self.ui.write(msg % (len(difflines), maxdiff))
270 self.ui.write(msg % (len(difflines), maxdiff))
268 difflines = difflines[:maxdiff]
271 difflines = difflines[:maxdiff]
269 elif difflines:
272 elif difflines:
270 self.ui.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
273 self.ui.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
271
274
272 self.ui.write("\n".join(difflines))
275 self.ui.write("\n".join(difflines))
273
276
274 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
277 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
275 '''send email notifications to interested subscribers.
278 '''send email notifications to interested subscribers.
276
279
277 if used as changegroup hook, send one email for all changesets in
280 if used as changegroup hook, send one email for all changesets in
278 changegroup. else send one email per changeset.'''
281 changegroup. else send one email per changeset.'''
279
282
280 n = notifier(ui, repo, hooktype)
283 n = notifier(ui, repo, hooktype)
281 ctx = repo[node]
284 ctx = repo[node]
282
285
283 if not n.subs:
286 if not n.subs:
284 ui.debug('notify: no subscribers to repository %s\n' % n.root)
287 ui.debug('notify: no subscribers to repository %s\n' % n.root)
285 return
288 return
286 if n.skipsource(source):
289 if n.skipsource(source):
287 ui.debug('notify: changes have source "%s" - skipping\n' % source)
290 ui.debug('notify: changes have source "%s" - skipping\n' % source)
288 return
291 return
289
292
290 ui.pushbuffer()
293 ui.pushbuffer()
291 data = ''
294 data = ''
292 count = 0
295 count = 0
293 if hooktype == 'changegroup':
296 if hooktype == 'changegroup' or hooktype == 'outgoing':
294 start, end = ctx.rev(), len(repo)
297 start, end = ctx.rev(), len(repo)
295 for rev in xrange(start, end):
298 for rev in xrange(start, end):
296 if n.node(repo[rev]):
299 if n.node(repo[rev]):
297 count += 1
300 count += 1
298 else:
301 else:
299 data += ui.popbuffer()
302 data += ui.popbuffer()
300 ui.note(_('notify: suppressing notification for merge %d:%s\n') %
303 ui.note(_('notify: suppressing notification for merge %d:%s\n') %
301 (rev, repo[rev].hex()[:12]))
304 (rev, repo[rev].hex()[:12]))
302 ui.pushbuffer()
305 ui.pushbuffer()
303 if count:
306 if count:
304 n.diff(ctx, repo['tip'])
307 n.diff(ctx, repo['tip'])
305 else:
308 else:
306 if not n.node(ctx):
309 if not n.node(ctx):
307 ui.popbuffer()
310 ui.popbuffer()
308 ui.note(_('notify: suppressing notification for merge %d:%s\n') %
311 ui.note(_('notify: suppressing notification for merge %d:%s\n') %
309 (ctx.rev(), ctx.hex()[:12]))
312 (ctx.rev(), ctx.hex()[:12]))
310 return
313 return
311 count += 1
314 count += 1
312 n.diff(ctx)
315 n.diff(ctx)
313
316
314 data += ui.popbuffer()
317 data += ui.popbuffer()
315 if count:
318 if count:
316 n.send(ctx, count, data)
319 n.send(ctx, count, data)
@@ -1,353 +1,356
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 notifications at commit/push time
20 notify extension - hooks for sending email notifications at commit/push time
21
21
22 Subscriptions can be managed through a hgrc file. Default mode is to print
22 Subscriptions can be managed through a hgrc file. Default mode is to print
23 messages to stdout, for testing and configuring.
23 messages to stdout, for testing and configuring.
24
24
25 To use, configure the notify extension and enable it in hgrc like this:
25 To use, configure the notify extension and enable it in hgrc like this:
26
26
27 [extensions]
27 [extensions]
28 notify =
28 notify =
29
29
30 [hooks]
30 [hooks]
31 # one email for each incoming changeset
31 # one email for each incoming changeset
32 incoming.notify = python:hgext.notify.hook
32 incoming.notify = python:hgext.notify.hook
33 # batch emails when many changesets incoming at one time
33 # batch emails when many changesets incoming at one time
34 changegroup.notify = python:hgext.notify.hook
34 changegroup.notify = python:hgext.notify.hook
35 # batch emails when many changesets outgoing at one time (client side)
36 outgoing.notify = python:hgext.notify.hook
35
37
36 [notify]
38 [notify]
37 # config items go here
39 # config items go here
38
40
39 Required configuration items:
41 Required configuration items:
40
42
41 config = /path/to/file # file containing subscriptions
43 config = /path/to/file # file containing subscriptions
42
44
43 Optional configuration items:
45 Optional configuration items:
44
46
45 test = True # print messages to stdout for testing
47 test = True # print messages to stdout for testing
46 strip = 3 # number of slashes to strip for url paths
48 strip = 3 # number of slashes to strip for url paths
47 domain = example.com # domain to use if committer missing domain
49 domain = example.com # domain to use if committer missing domain
48 style = ... # style file to use when formatting email
50 style = ... # style file to use when formatting email
49 template = ... # template to use when formatting email
51 template = ... # template to use when formatting email
50 incoming = ... # template to use when run as incoming hook
52 incoming = ... # template to use when run as incoming hook
51 changegroup = ... # template when run as changegroup hook
53 outgoing = ... # template to use when run as outgoing hook
54 changegroup = ... # template to use when run as changegroup hook
52 maxdiff = 300 # max lines of diffs to include (0=none, -1=all)
55 maxdiff = 300 # max lines of diffs to include (0=none, -1=all)
53 maxsubject = 67 # truncate subject line longer than this
56 maxsubject = 67 # truncate subject line longer than this
54 diffstat = True # add a diffstat before the diff content
57 diffstat = True # add a diffstat before the diff content
55 sources = serve # notify if source of incoming changes in this list
58 sources = serve # notify if source of incoming changes in this list
56 # (serve == ssh or http, push, pull, bundle)
59 # (serve == ssh or http, push, pull, bundle)
57 merge = False # send notification for merges (default True)
60 merge = False # send notification for merges (default True)
58 [email]
61 [email]
59 from = user@host.com # email address to send as if none given
62 from = user@host.com # email address to send as if none given
60 [web]
63 [web]
61 baseurl = http://hgserver/... # root of hg web site for browsing commits
64 baseurl = http://hgserver/... # root of hg web site for browsing commits
62
65
63 The notify config file has same format as a regular hgrc file. It has two
66 The notify config file has same format as a regular hgrc file. It has two
64 sections so you can express subscriptions in whatever way is handier for you.
67 sections so you can express subscriptions in whatever way is handier for you.
65
68
66 [usersubs]
69 [usersubs]
67 # key is subscriber email, value is ","-separated list of glob patterns
70 # key is subscriber email, value is ","-separated list of glob patterns
68 user@host = pattern
71 user@host = pattern
69
72
70 [reposubs]
73 [reposubs]
71 # key is glob pattern, value is ","-separated list of subscriber emails
74 # key is glob pattern, value is ","-separated list of subscriber emails
72 pattern = user@host
75 pattern = user@host
73
76
74 Glob patterns are matched against path to repository root.
77 Glob patterns are matched against path to repository root.
75
78
76 If you like, you can put notify config file in repository that users can push
79 If you like, you can put notify config file in repository that users can push
77 changes to, they can manage their own subscriptions.
80 changes to, they can manage their own subscriptions.
78
81
79 no commands defined
82 no commands defined
80 $ hg init a
83 $ hg init a
81 $ echo a > a/a
84 $ echo a > a/a
82
85
83 commit
86 commit
84
87
85 $ hg --cwd a commit -Ama -d '0 0'
88 $ hg --cwd a commit -Ama -d '0 0'
86 adding a
89 adding a
87
90
88
91
89 clone
92 clone
90
93
91 $ hg --traceback clone a b
94 $ hg --traceback clone a b
92 updating to branch default
95 updating to branch default
93 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
96 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
94 $ echo a >> a/a
97 $ echo a >> a/a
95
98
96 commit
99 commit
97
100
98 $ hg --traceback --cwd a commit -Amb -d '1 0'
101 $ hg --traceback --cwd a commit -Amb -d '1 0'
99
102
100 on Mac OS X 10.5 the tmp path is very long so would get stripped in the subject line
103 on Mac OS X 10.5 the tmp path is very long so would get stripped in the subject line
101
104
102 $ cat <<EOF >> $HGRCPATH
105 $ cat <<EOF >> $HGRCPATH
103 > [notify]
106 > [notify]
104 > maxsubject = 200
107 > maxsubject = 200
105 > EOF
108 > EOF
106
109
107 the python call below wraps continuation lines, which appear on Mac OS X 10.5 because
110 the python call below wraps continuation lines, which appear on Mac OS X 10.5 because
108 of the very long subject line
111 of the very long subject line
109 pull (minimal config)
112 pull (minimal config)
110
113
111 $ hg --traceback --cwd b pull ../a | \
114 $ hg --traceback --cwd b pull ../a | \
112 > python -c 'import sys,re; print re.sub("\n[\t ]", " ", sys.stdin.read()),'
115 > python -c 'import sys,re; print re.sub("\n[\t ]", " ", sys.stdin.read()),'
113 pulling from ../a
116 pulling from ../a
114 searching for changes
117 searching for changes
115 adding changesets
118 adding changesets
116 adding manifests
119 adding manifests
117 adding file changes
120 adding file changes
118 added 1 changesets with 1 changes to 1 files
121 added 1 changesets with 1 changes to 1 files
119 Content-Type: text/plain; charset="us-ascii"
122 Content-Type: text/plain; charset="us-ascii"
120 MIME-Version: 1.0
123 MIME-Version: 1.0
121 Content-Transfer-Encoding: 7bit
124 Content-Transfer-Encoding: 7bit
122 Date: * (glob)
125 Date: * (glob)
123 Subject: changeset in $TESTTMP/b: b
126 Subject: changeset in $TESTTMP/b: b
124 From: test
127 From: test
125 X-Hg-Notification: changeset 0647d048b600
128 X-Hg-Notification: changeset 0647d048b600
126 Message-Id: <*> (glob)
129 Message-Id: <*> (glob)
127 To: baz, foo@bar
130 To: baz, foo@bar
128
131
129 changeset 0647d048b600 in $TESTTMP/b
132 changeset 0647d048b600 in $TESTTMP/b
130 details: $TESTTMP/b?cmd=changeset;node=0647d048b600
133 details: $TESTTMP/b?cmd=changeset;node=0647d048b600
131 description: b
134 description: b
132
135
133 diffs (6 lines):
136 diffs (6 lines):
134
137
135 diff -r cb9a9f314b8b -r 0647d048b600 a
138 diff -r cb9a9f314b8b -r 0647d048b600 a
136 --- a/a Thu Jan 01 00:00:00 1970 +0000
139 --- a/a Thu Jan 01 00:00:00 1970 +0000
137 +++ b/a Thu Jan 01 00:00:01 1970 +0000
140 +++ b/a Thu Jan 01 00:00:01 1970 +0000
138 @@ -1,1 +1,2 @@ a
141 @@ -1,1 +1,2 @@ a
139 +a
142 +a
140 (run 'hg update' to get a working copy)
143 (run 'hg update' to get a working copy)
141 $ cat <<EOF >> $HGRCPATH
144 $ cat <<EOF >> $HGRCPATH
142 > [notify]
145 > [notify]
143 > config = `pwd`/.notify.conf
146 > config = `pwd`/.notify.conf
144 > domain = test.com
147 > domain = test.com
145 > strip = 42
148 > strip = 42
146 > template = Subject: {desc|firstline|strip}\nFrom: {author}\nX-Test: foo\n\nchangeset {node|short} in {webroot}\ndescription:\n\t{desc|tabindent|strip}
149 > template = Subject: {desc|firstline|strip}\nFrom: {author}\nX-Test: foo\n\nchangeset {node|short} in {webroot}\ndescription:\n\t{desc|tabindent|strip}
147 >
150 >
148 > [web]
151 > [web]
149 > baseurl = http://test/
152 > baseurl = http://test/
150 > EOF
153 > EOF
151
154
152 fail for config file is missing
155 fail for config file is missing
153
156
154 $ hg --cwd b rollback
157 $ hg --cwd b rollback
155 repository tip rolled back to revision 0 (undo pull)
158 repository tip rolled back to revision 0 (undo pull)
156 working directory now based on revision 0
159 working directory now based on revision 0
157 $ hg --cwd b pull ../a 2>&1 | grep 'error.*\.notify\.conf' > /dev/null && echo pull failed
160 $ hg --cwd b pull ../a 2>&1 | grep 'error.*\.notify\.conf' > /dev/null && echo pull failed
158 pull failed
161 pull failed
159 $ touch ".notify.conf"
162 $ touch ".notify.conf"
160
163
161 pull
164 pull
162
165
163 $ hg --cwd b rollback
166 $ hg --cwd b rollback
164 repository tip rolled back to revision 0 (undo pull)
167 repository tip rolled back to revision 0 (undo pull)
165 working directory now based on revision 0
168 working directory now based on revision 0
166 $ hg --traceback --cwd b pull ../a | \
169 $ hg --traceback --cwd b pull ../a | \
167 > python -c 'import sys,re; print re.sub("\n\t", " ", sys.stdin.read()),'
170 > python -c 'import sys,re; print re.sub("\n\t", " ", sys.stdin.read()),'
168 pulling from ../a
171 pulling from ../a
169 searching for changes
172 searching for changes
170 adding changesets
173 adding changesets
171 adding manifests
174 adding manifests
172 adding file changes
175 adding file changes
173 added 1 changesets with 1 changes to 1 files
176 added 1 changesets with 1 changes to 1 files
174 Content-Type: text/plain; charset="us-ascii"
177 Content-Type: text/plain; charset="us-ascii"
175 MIME-Version: 1.0
178 MIME-Version: 1.0
176 Content-Transfer-Encoding: 7bit
179 Content-Transfer-Encoding: 7bit
177 X-Test: foo
180 X-Test: foo
178 Date: * (glob)
181 Date: * (glob)
179 Subject: b
182 Subject: b
180 From: test@test.com
183 From: test@test.com
181 X-Hg-Notification: changeset 0647d048b600
184 X-Hg-Notification: changeset 0647d048b600
182 Message-Id: <*> (glob)
185 Message-Id: <*> (glob)
183 To: baz@test.com, foo@bar
186 To: baz@test.com, foo@bar
184
187
185 changeset 0647d048b600 in b
188 changeset 0647d048b600 in b
186 description: b
189 description: b
187 diffs (6 lines):
190 diffs (6 lines):
188
191
189 diff -r cb9a9f314b8b -r 0647d048b600 a
192 diff -r cb9a9f314b8b -r 0647d048b600 a
190 --- a/a Thu Jan 01 00:00:00 1970 +0000
193 --- a/a Thu Jan 01 00:00:00 1970 +0000
191 +++ b/a Thu Jan 01 00:00:01 1970 +0000
194 +++ b/a Thu Jan 01 00:00:01 1970 +0000
192 @@ -1,1 +1,2 @@
195 @@ -1,1 +1,2 @@
193 a
196 a
194 +a
197 +a
195 (run 'hg update' to get a working copy)
198 (run 'hg update' to get a working copy)
196
199
197 $ cat << EOF >> $HGRCPATH
200 $ cat << EOF >> $HGRCPATH
198 > [hooks]
201 > [hooks]
199 > incoming.notify = python:hgext.notify.hook
202 > incoming.notify = python:hgext.notify.hook
200 >
203 >
201 > [notify]
204 > [notify]
202 > sources = pull
205 > sources = pull
203 > diffstat = True
206 > diffstat = True
204 > EOF
207 > EOF
205
208
206 pull
209 pull
207
210
208 $ hg --cwd b rollback
211 $ hg --cwd b rollback
209 repository tip rolled back to revision 0 (undo pull)
212 repository tip rolled back to revision 0 (undo pull)
210 working directory now based on revision 0
213 working directory now based on revision 0
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 diffstat:
235 diffstat:
233
236
234 a | 1 +
237 a | 1 +
235 1 files changed, 1 insertions(+), 0 deletions(-)
238 1 files changed, 1 insertions(+), 0 deletions(-)
236
239
237 diffs (6 lines):
240 diffs (6 lines):
238
241
239 diff -r cb9a9f314b8b -r 0647d048b600 a
242 diff -r cb9a9f314b8b -r 0647d048b600 a
240 --- a/a Thu Jan 01 00:00:00 1970 +0000
243 --- a/a Thu Jan 01 00:00:00 1970 +0000
241 +++ b/a Thu Jan 01 00:00:01 1970 +0000
244 +++ b/a Thu Jan 01 00:00:01 1970 +0000
242 @@ -1,1 +1,2 @@
245 @@ -1,1 +1,2 @@
243 a
246 a
244 +a
247 +a
245 (run 'hg update' to get a working copy)
248 (run 'hg update' to get a working copy)
246
249
247 test merge
250 test merge
248
251
249 $ cd a
252 $ cd a
250 $ hg up -C 0
253 $ hg up -C 0
251 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
254 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
252 $ echo a >> a
255 $ echo a >> a
253 $ hg ci -Am adda2 -d '2 0'
256 $ hg ci -Am adda2 -d '2 0'
254 created new head
257 created new head
255 $ hg merge
258 $ hg merge
256 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
259 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
257 (branch merge, don't forget to commit)
260 (branch merge, don't forget to commit)
258 $ hg ci -m merge -d '3 0'
261 $ hg ci -m merge -d '3 0'
259 $ cd ..
262 $ cd ..
260 $ hg --traceback --cwd b pull ../a | \
263 $ hg --traceback --cwd b pull ../a | \
261 > python -c 'import sys,re; print re.sub("\n\t", " ", sys.stdin.read()),'
264 > python -c 'import sys,re; print re.sub("\n\t", " ", sys.stdin.read()),'
262 pulling from ../a
265 pulling from ../a
263 searching for changes
266 searching for changes
264 adding changesets
267 adding changesets
265 adding manifests
268 adding manifests
266 adding file changes
269 adding file changes
267 added 2 changesets with 0 changes to 0 files
270 added 2 changesets with 0 changes to 0 files
268 Content-Type: text/plain; charset="us-ascii"
271 Content-Type: text/plain; charset="us-ascii"
269 MIME-Version: 1.0
272 MIME-Version: 1.0
270 Content-Transfer-Encoding: 7bit
273 Content-Transfer-Encoding: 7bit
271 X-Test: foo
274 X-Test: foo
272 Date: * (glob)
275 Date: * (glob)
273 Subject: adda2
276 Subject: adda2
274 From: test@test.com
277 From: test@test.com
275 X-Hg-Notification: changeset 0a184ce6067f
278 X-Hg-Notification: changeset 0a184ce6067f
276 Message-Id: <*> (glob)
279 Message-Id: <*> (glob)
277 To: baz@test.com, foo@bar
280 To: baz@test.com, foo@bar
278
281
279 changeset 0a184ce6067f in b
282 changeset 0a184ce6067f in b
280 description: adda2
283 description: adda2
281 diffstat:
284 diffstat:
282
285
283 a | 1 +
286 a | 1 +
284 1 files changed, 1 insertions(+), 0 deletions(-)
287 1 files changed, 1 insertions(+), 0 deletions(-)
285
288
286 diffs (6 lines):
289 diffs (6 lines):
287
290
288 diff -r cb9a9f314b8b -r 0a184ce6067f a
291 diff -r cb9a9f314b8b -r 0a184ce6067f a
289 --- a/a Thu Jan 01 00:00:00 1970 +0000
292 --- a/a Thu Jan 01 00:00:00 1970 +0000
290 +++ b/a Thu Jan 01 00:00:02 1970 +0000
293 +++ b/a Thu Jan 01 00:00:02 1970 +0000
291 @@ -1,1 +1,2 @@
294 @@ -1,1 +1,2 @@
292 a
295 a
293 +a
296 +a
294 Content-Type: text/plain; charset="us-ascii"
297 Content-Type: text/plain; charset="us-ascii"
295 MIME-Version: 1.0
298 MIME-Version: 1.0
296 Content-Transfer-Encoding: 7bit
299 Content-Transfer-Encoding: 7bit
297 X-Test: foo
300 X-Test: foo
298 Date: * (glob)
301 Date: * (glob)
299 Subject: merge
302 Subject: merge
300 From: test@test.com
303 From: test@test.com
301 X-Hg-Notification: changeset 6a0cf76b2701
304 X-Hg-Notification: changeset 6a0cf76b2701
302 Message-Id: <*> (glob)
305 Message-Id: <*> (glob)
303 To: baz@test.com, foo@bar
306 To: baz@test.com, foo@bar
304
307
305 changeset 6a0cf76b2701 in b
308 changeset 6a0cf76b2701 in b
306 description: merge
309 description: merge
307 (run 'hg update' to get a working copy)
310 (run 'hg update' to get a working copy)
308
311
309 truncate multi-byte subject
312 truncate multi-byte subject
310
313
311 $ cat <<EOF >> $HGRCPATH
314 $ cat <<EOF >> $HGRCPATH
312 > [notify]
315 > [notify]
313 > maxsubject = 4
316 > maxsubject = 4
314 > EOF
317 > EOF
315 $ echo a >> a/a
318 $ echo a >> a/a
316 $ hg --cwd a --encoding utf-8 commit -A -d '0 0' \
319 $ hg --cwd a --encoding utf-8 commit -A -d '0 0' \
317 > -m `python -c 'print "\xc3\xa0\xc3\xa1\xc3\xa2\xc3\xa3\xc3\xa4"'`
320 > -m `python -c 'print "\xc3\xa0\xc3\xa1\xc3\xa2\xc3\xa3\xc3\xa4"'`
318 $ hg --traceback --cwd b --encoding utf-8 pull ../a | \
321 $ hg --traceback --cwd b --encoding utf-8 pull ../a | \
319 > python -c 'import sys,re; print re.sub("\n\t", " ", sys.stdin.read()),'
322 > python -c 'import sys,re; print re.sub("\n\t", " ", sys.stdin.read()),'
320 pulling from ../a
323 pulling from ../a
321 searching for changes
324 searching for changes
322 adding changesets
325 adding changesets
323 adding manifests
326 adding manifests
324 adding file changes
327 adding file changes
325 added 1 changesets with 1 changes to 1 files
328 added 1 changesets with 1 changes to 1 files
326 Content-Type: text/plain; charset="us-ascii"
329 Content-Type: text/plain; charset="us-ascii"
327 MIME-Version: 1.0
330 MIME-Version: 1.0
328 Content-Transfer-Encoding: 8bit
331 Content-Transfer-Encoding: 8bit
329 X-Test: foo
332 X-Test: foo
330 Date: * (glob)
333 Date: * (glob)
331 Subject: \xc3\xa0... (esc)
334 Subject: \xc3\xa0... (esc)
332 From: test@test.com
335 From: test@test.com
333 X-Hg-Notification: changeset 7ea05ad269dc
336 X-Hg-Notification: changeset 7ea05ad269dc
334 Message-Id: <*> (glob)
337 Message-Id: <*> (glob)
335 To: baz@test.com, foo@bar
338 To: baz@test.com, foo@bar
336
339
337 changeset 7ea05ad269dc in b
340 changeset 7ea05ad269dc in b
338 description: \xc3\xa0\xc3\xa1\xc3\xa2\xc3\xa3\xc3\xa4 (esc)
341 description: \xc3\xa0\xc3\xa1\xc3\xa2\xc3\xa3\xc3\xa4 (esc)
339 diffstat:
342 diffstat:
340
343
341 a | 1 +
344 a | 1 +
342 1 files changed, 1 insertions(+), 0 deletions(-)
345 1 files changed, 1 insertions(+), 0 deletions(-)
343
346
344 diffs (7 lines):
347 diffs (7 lines):
345
348
346 diff -r 6a0cf76b2701 -r 7ea05ad269dc a
349 diff -r 6a0cf76b2701 -r 7ea05ad269dc a
347 --- a/a Thu Jan 01 00:00:03 1970 +0000
350 --- a/a Thu Jan 01 00:00:03 1970 +0000
348 +++ b/a Thu Jan 01 00:00:00 1970 +0000
351 +++ b/a Thu Jan 01 00:00:00 1970 +0000
349 @@ -1,2 +1,3 @@
352 @@ -1,2 +1,3 @@
350 a
353 a
351 a
354 a
352 +a
355 +a
353 (run 'hg update' to get a working copy)
356 (run 'hg update' to get a working copy)
General Comments 0
You need to be logged in to leave comments. Login now