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