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