##// 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 from mercurial.demandload import *
65 from mercurial.demandload import *
2 from mercurial.i18n import gettext as _
66 from mercurial.i18n import gettext as _
3 from mercurial.node import *
67 from mercurial.node import *
4 demandload(globals(), 'email.MIMEText mercurial:templater,util fnmatch socket')
68 demandload(globals(), 'email.Parser mercurial:commands,templater,util')
5 demandload(globals(), 'time')
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 class notifier(object):
95 class notifier(object):
8 def __init__(self, ui, repo):
96 '''email notification class.'''
97
98 def __init__(self, ui, repo, hooktype):
9 self.ui = ui
99 self.ui = ui
10 self.ui.readconfig(self.ui.config('notify', 'config'))
100 self.ui.readconfig(self.ui.config('notify', 'config'))
11 self.repo = repo
101 self.repo = repo
12 self.stripcount = self.ui.config('notify', 'strip')
102 self.stripcount = int(self.ui.config('notify', 'strip', 0))
13 self.root = self.strip(self.repo.root)
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 def strip(self, path):
119 def strip(self, path):
120 '''strip leading slashes from local path, turn into web-safe path.'''
121
16 path = util.pconvert(path)
122 path = util.pconvert(path)
17 count = self.stripcount
123 count = self.stripcount
18 while path and count >= 0:
124 while path and count >= 0:
@@ -23,73 +129,130 b' class notifier(object):'
23 count -= 1
129 count -= 1
24 return path
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 def subscribers(self):
143 def subscribers(self):
27 subs = []
144 '''return list of email addresses of subscribers to this repo.'''
28 for user, pat in self.ui.configitems('usersubs'):
145
29 if fnmatch.fnmatch(self.root, pat):
146 subs = {}
30 subs.append(user)
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 for pat, users in self.ui.configitems('reposubs'):
151 for pat, users in self.ui.configitems('reposubs'):
32 if fnmatch.fnmatch(self.root, pat):
152 if fnmatch.fnmatch(self.repo.root, pat):
33 subs.extend([u.strip() for u in users.split(',')])
153 for user in users.split(','):
154 subs[self.fixmail(user)] = 1
155 subs = subs.keys()
34 subs.sort()
156 subs.sort()
35 return subs
157 return subs
36
158
37 def seen(self, node):
38 pass
39
40 def url(self, path=None):
159 def url(self, path=None):
41 return self.ui.config('web', 'baseurl') + (path or self.root)
160 return self.ui.config('web', 'baseurl') + (path or self.root)
42
161
43 def message(self, node, changes):
162 def node(self, node):
44 sio = templater.stringio()
163 '''format one changeset.'''
45 seen = self.seen(node)
164
46 if seen:
165 self.t.show(changenode=node, changes=self.repo.changelog.read(node),
47 seen = self.strip(seen)
166 baseurl=self.ui.config('web', 'baseurl'),
48 sio.write('Changeset %s merged to %s\n' %
167 root=self.repo.root,
49 (short(node), self.url()))
168 webroot=self.root)
50 sio.write('First seen in %s\n' % self.url(seen))
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 else:
219 else:
52 sio.write('Changeset %s new to %s\n' % (short(node), self.url()))
220 mail = self.ui.sendmail()
53 sio.write('Committed by %s at %s\n' %
221 mail.sendmail(templater.email(msg['From']), self.subs, msgtext)
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
79
222
80 def node(self, node):
223 def diff(self, node):
81 mail = self.ui.sendmail()
224 maxdiff = int(self.ui.config('notify', 'maxdiff', 300))
82 changes = self.repo.changelog.read(node)
225 if maxdiff == 0:
83 fromaddr = self.ui.config('email', 'from', changes[1])
226 return
84 msg = self.message(node, changes)
227 fp = templater.stringio()
85 subs = self.subscribers()
228 commands.dodiff(fp, self.ui, self.repo, node,
86 msg['To'] = ', '.join(subs)
229 self.repo.changelog.tip())
87 msgtext = msg.as_string(0)
230 difflines = fp.getvalue().splitlines(1)
88 mail.sendmail(templater.email(fromaddr),
231 if maxdiff > 0 and len(difflines) > maxdiff:
89 [templater.email(s) for s in subs],
232 self.sio.write(_('\ndiffs (truncated from %d to %d lines):\n\n') %
90 msgtext)
233 (len(difflines), maxdiff))
91
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 def hook(ui, repo, hooktype, node=None, **kwargs):
239 def hook(ui, repo, hooktype, node=None, **kwargs):
94 n = notifier(ui, repo)
240 '''send email notifications to interested subscribers.
95 n.node(bin(node))
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