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