##// END OF EJS Templates
notify: fix diffstat printing...
divy@chelsio.com -
r6979:a3fd4aa1 default
parent child Browse files
Show More
@@ -1,281 +1,283 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 # diffstat = True # add a diffstat before the diff content
43 # diffstat = True # add a diffstat before the diff content
44 # sources = serve # notify if source of incoming changes in this list
44 # sources = serve # notify if source of incoming changes in this list
45 # # (serve == ssh or http, push, pull, bundle)
45 # # (serve == ssh or http, push, pull, bundle)
46 # [email]
46 # [email]
47 # from = user@host.com # email address to send as if none given
47 # from = user@host.com # email address to send as if none given
48 # [web]
48 # [web]
49 # baseurl = http://hgserver/... # root of hg web site for browsing commits
49 # baseurl = http://hgserver/... # root of hg web site for browsing commits
50 #
50 #
51 # notify config file has same format as regular hgrc. it has two
51 # notify config file has same format as regular hgrc. it has two
52 # sections so you can express subscriptions in whatever way is handier
52 # sections so you can express subscriptions in whatever way is handier
53 # for you.
53 # for you.
54 #
54 #
55 # [usersubs]
55 # [usersubs]
56 # # key is subscriber email, value is ","-separated list of glob patterns
56 # # key is subscriber email, value is ","-separated list of glob patterns
57 # user@host = pattern
57 # user@host = pattern
58 #
58 #
59 # [reposubs]
59 # [reposubs]
60 # # key is glob pattern, value is ","-separated list of subscriber emails
60 # # key is glob pattern, value is ","-separated list of subscriber emails
61 # pattern = user@host
61 # pattern = user@host
62 #
62 #
63 # glob patterns are matched against path to repo root.
63 # glob patterns are matched against path to repo root.
64 #
64 #
65 # if you like, you can put notify config file in repo that users can
65 # if you like, you can put notify config file in repo that users can
66 # push changes to, they can manage their own subscriptions.
66 # push changes to, they can manage their own subscriptions.
67
67
68 from mercurial.i18n import _
68 from mercurial.i18n import _
69 from mercurial.node import bin, short
69 from mercurial.node import bin, short
70 from mercurial import patch, cmdutil, templater, util, mail
70 from mercurial import patch, cmdutil, templater, util, mail
71 import email.Parser, fnmatch, socket, time
71 import email.Parser, 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 cfg = self.ui.config('notify', 'config')
102 cfg = self.ui.config('notify', 'config')
103 if cfg:
103 if cfg:
104 self.ui.readsections(cfg, 'usersubs', 'reposubs')
104 self.ui.readsections(cfg, 'usersubs', 'reposubs')
105 self.repo = repo
105 self.repo = repo
106 self.stripcount = int(self.ui.config('notify', 'strip', 0))
106 self.stripcount = int(self.ui.config('notify', 'strip', 0))
107 self.root = self.strip(self.repo.root)
107 self.root = self.strip(self.repo.root)
108 self.domain = self.ui.config('notify', 'domain')
108 self.domain = self.ui.config('notify', 'domain')
109 self.subs = self.subscribers()
109 self.subs = self.subscribers()
110
110
111 mapfile = self.ui.config('notify', 'style')
111 mapfile = self.ui.config('notify', 'style')
112 template = (self.ui.config('notify', hooktype) or
112 template = (self.ui.config('notify', hooktype) or
113 self.ui.config('notify', 'template'))
113 self.ui.config('notify', 'template'))
114 self.t = cmdutil.changeset_templater(self.ui, self.repo,
114 self.t = cmdutil.changeset_templater(self.ui, self.repo,
115 False, mapfile, False)
115 False, mapfile, False)
116 if not mapfile and not template:
116 if not mapfile and not template:
117 template = deftemplates.get(hooktype) or single_template
117 template = deftemplates.get(hooktype) or single_template
118 if template:
118 if template:
119 template = templater.parsestring(template, quoted=False)
119 template = templater.parsestring(template, quoted=False)
120 self.t.use_template(template)
120 self.t.use_template(template)
121
121
122 def strip(self, path):
122 def strip(self, path):
123 '''strip leading slashes from local path, turn into web-safe path.'''
123 '''strip leading slashes from local path, turn into web-safe path.'''
124
124
125 path = util.pconvert(path)
125 path = util.pconvert(path)
126 count = self.stripcount
126 count = self.stripcount
127 while count > 0:
127 while count > 0:
128 c = path.find('/')
128 c = path.find('/')
129 if c == -1:
129 if c == -1:
130 break
130 break
131 path = path[c+1:]
131 path = path[c+1:]
132 count -= 1
132 count -= 1
133 return path
133 return path
134
134
135 def fixmail(self, addr):
135 def fixmail(self, addr):
136 '''try to clean up email addresses.'''
136 '''try to clean up email addresses.'''
137
137
138 addr = util.email(addr.strip())
138 addr = util.email(addr.strip())
139 if self.domain:
139 if self.domain:
140 a = addr.find('@localhost')
140 a = addr.find('@localhost')
141 if a != -1:
141 if a != -1:
142 addr = addr[:a]
142 addr = addr[:a]
143 if '@' not in addr:
143 if '@' not in addr:
144 return addr + '@' + self.domain
144 return addr + '@' + self.domain
145 return addr
145 return addr
146
146
147 def subscribers(self):
147 def subscribers(self):
148 '''return list of email addresses of subscribers to this repo.'''
148 '''return list of email addresses of subscribers to this repo.'''
149
149
150 subs = {}
150 subs = {}
151 for user, pats in self.ui.configitems('usersubs'):
151 for user, pats in self.ui.configitems('usersubs'):
152 for pat in pats.split(','):
152 for pat in pats.split(','):
153 if fnmatch.fnmatch(self.repo.root, pat.strip()):
153 if fnmatch.fnmatch(self.repo.root, pat.strip()):
154 subs[self.fixmail(user)] = 1
154 subs[self.fixmail(user)] = 1
155 for pat, users in self.ui.configitems('reposubs'):
155 for pat, users in self.ui.configitems('reposubs'):
156 if fnmatch.fnmatch(self.repo.root, pat):
156 if fnmatch.fnmatch(self.repo.root, pat):
157 for user in users.split(','):
157 for user in users.split(','):
158 subs[self.fixmail(user)] = 1
158 subs[self.fixmail(user)] = 1
159 return util.sort(subs)
159 return util.sort(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, data):
177 def send(self, node, count, data):
178 '''send message.'''
178 '''send message.'''
179
179
180 p = email.Parser.Parser()
180 p = email.Parser.Parser()
181 msg = p.parsestr(data)
181 msg = p.parsestr(data)
182
182
183 def fix_subject():
183 def fix_subject():
184 '''try to make subject line exist and be useful.'''
184 '''try to make subject line exist and be useful.'''
185
185
186 subject = msg['Subject']
186 subject = msg['Subject']
187 if not subject:
187 if not subject:
188 if count > 1:
188 if count > 1:
189 subject = _('%s: %d new changesets') % (self.root, count)
189 subject = _('%s: %d new changesets') % (self.root, count)
190 else:
190 else:
191 changes = self.repo.changelog.read(node)
191 changes = self.repo.changelog.read(node)
192 s = changes[4].lstrip().split('\n', 1)[0].rstrip()
192 s = changes[4].lstrip().split('\n', 1)[0].rstrip()
193 subject = '%s: %s' % (self.root, s)
193 subject = '%s: %s' % (self.root, s)
194 maxsubject = int(self.ui.config('notify', 'maxsubject', 67))
194 maxsubject = int(self.ui.config('notify', 'maxsubject', 67))
195 if maxsubject and len(subject) > maxsubject:
195 if maxsubject and len(subject) > maxsubject:
196 subject = subject[:maxsubject-3] + '...'
196 subject = subject[:maxsubject-3] + '...'
197 del msg['Subject']
197 del msg['Subject']
198 msg['Subject'] = subject
198 msg['Subject'] = subject
199
199
200 def fix_sender():
200 def fix_sender():
201 '''try to make message have proper sender.'''
201 '''try to make message have proper sender.'''
202
202
203 sender = msg['From']
203 sender = msg['From']
204 if not sender:
204 if not sender:
205 sender = self.ui.config('email', 'from') or self.ui.username()
205 sender = self.ui.config('email', 'from') or self.ui.username()
206 if '@' not in sender or '@localhost' in sender:
206 if '@' not in sender or '@localhost' in sender:
207 sender = self.fixmail(sender)
207 sender = self.fixmail(sender)
208 del msg['From']
208 del msg['From']
209 msg['From'] = sender
209 msg['From'] = sender
210
210
211 msg['Date'] = util.datestr(format="%a, %d %b %Y %H:%M:%S %1%2")
211 msg['Date'] = util.datestr(format="%a, %d %b %Y %H:%M:%S %1%2")
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 self.ui.status(_('notify: sending %d subscribers %d changes\n') %
228 self.ui.status(_('notify: sending %d subscribers %d changes\n') %
229 (len(self.subs), count))
229 (len(self.subs), count))
230 mail.sendmail(self.ui, util.email(msg['From']),
230 mail.sendmail(self.ui, util.email(msg['From']),
231 self.subs, msgtext)
231 self.subs, msgtext)
232
232
233 def diff(self, node, ref):
233 def diff(self, node, ref):
234 maxdiff = int(self.ui.config('notify', 'maxdiff', 300))
234 maxdiff = int(self.ui.config('notify', 'maxdiff', 300))
235 prev = self.repo.changelog.parents(node)[0]
235 prev = self.repo.changelog.parents(node)[0]
236
236 self.ui.pushbuffer()
237 self.ui.pushbuffer()
237 patch.diff(self.repo, prev, ref, opts=patch.diffopts(self.ui))
238 patch.diff(self.repo, prev, ref, opts=patch.diffopts(self.ui))
238 difflines = self.ui.popbuffer().splitlines(1)
239 difflines = self.ui.popbuffer().splitlines()
240
239 if self.ui.configbool('notify', 'diffstat', True):
241 if self.ui.configbool('notify', 'diffstat', True):
240 s = patch.diffstat(difflines)
242 s = patch.diffstat(difflines)
241 # s may be nil, don't include the header if it is
243 # s may be nil, don't include the header if it is
242 if s:
244 if s:
243 self.ui.write('\ndiffstat:\n\n%s' % s)
245 self.ui.write('\ndiffstat:\n\n%s' % s)
244 if maxdiff == 0:
246 if maxdiff == 0:
245 return
247 return
246 if maxdiff > 0 and len(difflines) > maxdiff:
248 if maxdiff > 0 and len(difflines) > maxdiff:
247 self.ui.write(_('\ndiffs (truncated from %d to %d lines):\n\n') %
249 self.ui.write(_('\ndiffs (truncated from %d to %d lines):\n\n') %
248 (len(difflines), maxdiff))
250 (len(difflines), maxdiff))
249 difflines = difflines[:maxdiff]
251 difflines = difflines[:maxdiff]
250 elif difflines:
252 elif difflines:
251 self.ui.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
253 self.ui.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
252 self.ui.write(*difflines)
254 self.ui.write("\n".join(difflines))
253
255
254 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
256 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
255 '''send email notifications to interested subscribers.
257 '''send email notifications to interested subscribers.
256
258
257 if used as changegroup hook, send one email for all changesets in
259 if used as changegroup hook, send one email for all changesets in
258 changegroup. else send one email per changeset.'''
260 changegroup. else send one email per changeset.'''
259 n = notifier(ui, repo, hooktype)
261 n = notifier(ui, repo, hooktype)
260 if not n.subs:
262 if not n.subs:
261 ui.debug(_('notify: no subscribers to repo %s\n') % n.root)
263 ui.debug(_('notify: no subscribers to repo %s\n') % n.root)
262 return
264 return
263 if n.skipsource(source):
265 if n.skipsource(source):
264 ui.debug(_('notify: changes have source "%s" - skipping\n') %
266 ui.debug(_('notify: changes have source "%s" - skipping\n') %
265 source)
267 source)
266 return
268 return
267 node = bin(node)
269 node = bin(node)
268 ui.pushbuffer()
270 ui.pushbuffer()
269 if hooktype == 'changegroup':
271 if hooktype == 'changegroup':
270 start = repo[node].rev()
272 start = repo[node].rev()
271 end = len(repo)
273 end = len(repo)
272 count = end - start
274 count = end - start
273 for rev in xrange(start, end):
275 for rev in xrange(start, end):
274 n.node(repo[rev].node())
276 n.node(repo[rev].node())
275 n.diff(node, repo.changelog.tip())
277 n.diff(node, repo.changelog.tip())
276 else:
278 else:
277 count = 1
279 count = 1
278 n.node(node)
280 n.node(node)
279 n.diff(node, node)
281 n.diff(node, node)
280 data = ui.popbuffer()
282 data = ui.popbuffer()
281 n.send(node, count, data)
283 n.send(node, count, data)
General Comments 0
You need to be logged in to leave comments. Login now