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