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