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