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