Mercurial > hg
comparison hgext/notify.py @ 2203:9569eea1707c
add email notification hook. hook written in python.
email headers and body can be customized using template code.
author | Vadim Gelfer <vadim.gelfer@gmail.com> |
---|---|
date | Thu, 04 May 2006 15:07:35 -0700 |
parents | f15056b29472 |
children | 05b6c13f43c6 |
comparison
equal
deleted
inserted
replaced
2202:bc35cd725c37 | 2203:9569eea1707c |
---|---|
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: |
19 c = path.find('/') | 125 c = path.find('/') |
20 if c == -1: | 126 if c == -1: |
21 break | 127 break |
22 path = path[c+1:] | 128 path = path[c+1:] |
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]))) | 222 |
55 sio.write('See %s?cmd=changeset;node=%s for full details\n' % | 223 def diff(self, node): |
56 (self.url(), short(node))) | 224 maxdiff = int(self.ui.config('notify', 'maxdiff', 300)) |
57 sio.write('\nDescription:\n') | 225 if maxdiff == 0: |
58 sio.write(templater.indent(changes[4], ' ')) | 226 return |
59 msg = email.MIMEText.MIMEText(sio.getvalue(), 'plain') | 227 fp = templater.stringio() |
60 firstline = changes[4].lstrip().split('\n', 1)[0].rstrip() | 228 commands.dodiff(fp, self.ui, self.repo, node, |
61 subject = '%s %s: %s' % (self.root, self.repo.rev(node), firstline) | 229 self.repo.changelog.tip()) |
62 if seen: | 230 difflines = fp.getvalue().splitlines(1) |
63 subject = '[merge] ' + subject | 231 if maxdiff > 0 and len(difflines) > maxdiff: |
64 if subject.endswith('.'): | 232 self.sio.write(_('\ndiffs (truncated from %d to %d lines):\n\n') % |
65 subject = subject[:-1] | 233 (len(difflines), maxdiff)) |
66 if len(subject) > 67: | 234 difflines = difflines[:maxdiff] |
67 subject = subject[:64] + '...' | 235 elif difflines: |
68 msg['Subject'] = subject | 236 self.sio.write(_('\ndiffs (%d lines):\n\n') % len(difflines)) |
69 msg['X-Hg-Repo'] = self.root | 237 self.sio.write(*difflines) |
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 | |
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 | |
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 |