##// END OF EJS Templates
hgext: enable extensions without "hgext." prefix in help texts
Martin Geisler -
r10112:703db37d stable
parent child Browse files
Show More
@@ -1,107 +1,107 b''
1 # acl.py - changeset access control for mercurial
1 # acl.py - changeset access control 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 of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2, incorporated herein by reference.
7 #
7 #
8
8
9 '''hooks for controlling repository access
9 '''hooks for controlling repository access
10
10
11 This hook makes it possible to allow or deny write access to portions
11 This hook makes it possible to allow or deny write access to portions
12 of a repository when receiving incoming changesets.
12 of a repository when receiving incoming changesets.
13
13
14 The authorization is matched based on the local user name on the
14 The authorization is matched based on the local user name on the
15 system where the hook runs, and not the committer of the original
15 system where the hook runs, and not the committer of the original
16 changeset (since the latter is merely informative).
16 changeset (since the latter is merely informative).
17
17
18 The acl hook is best used along with a restricted shell like hgsh,
18 The acl hook is best used along with a restricted shell like hgsh,
19 preventing authenticating users from doing anything other than
19 preventing authenticating users from doing anything other than
20 pushing or pulling. The hook is not safe to use if users have
20 pushing or pulling. The hook is not safe to use if users have
21 interactive shell access, as they can then disable the hook.
21 interactive shell access, as they can then disable the hook.
22 Nor is it safe if remote users share an account, because then there
22 Nor is it safe if remote users share an account, because then there
23 is no way to distinguish them.
23 is no way to distinguish them.
24
24
25 To use this hook, configure the acl extension in your hgrc like this::
25 To use this hook, configure the acl extension in your hgrc like this::
26
26
27 [extensions]
27 [extensions]
28 hgext.acl =
28 acl =
29
29
30 [hooks]
30 [hooks]
31 pretxnchangegroup.acl = python:hgext.acl.hook
31 pretxnchangegroup.acl = python:hgext.acl.hook
32
32
33 [acl]
33 [acl]
34 # Check whether the source of incoming changes is in this list
34 # Check whether the source of incoming changes is in this list
35 # ("serve" == ssh or http, "push", "pull", "bundle")
35 # ("serve" == ssh or http, "push", "pull", "bundle")
36 sources = serve
36 sources = serve
37
37
38 The allow and deny sections take a subtree pattern as key (with a glob
38 The allow and deny sections take a subtree pattern as key (with a glob
39 syntax by default), and a comma separated list of users as the
39 syntax by default), and a comma separated list of users as the
40 corresponding value. The deny list is checked before the allow list
40 corresponding value. The deny list is checked before the allow list
41 is. ::
41 is. ::
42
42
43 [acl.allow]
43 [acl.allow]
44 # If acl.allow is not present, all users are allowed by default.
44 # If acl.allow is not present, all users are allowed by default.
45 # An empty acl.allow section means no users allowed.
45 # An empty acl.allow section means no users allowed.
46 docs/** = doc_writer
46 docs/** = doc_writer
47 .hgtags = release_engineer
47 .hgtags = release_engineer
48
48
49 [acl.deny]
49 [acl.deny]
50 # If acl.deny is not present, no users are refused by default.
50 # If acl.deny is not present, no users are refused by default.
51 # An empty acl.deny section means all users allowed.
51 # An empty acl.deny section means all users allowed.
52 glob pattern = user4, user5
52 glob pattern = user4, user5
53 ** = user6
53 ** = user6
54 '''
54 '''
55
55
56 from mercurial.i18n import _
56 from mercurial.i18n import _
57 from mercurial import util, match
57 from mercurial import util, match
58 import getpass, urllib
58 import getpass, urllib
59
59
60 def buildmatch(ui, repo, user, key):
60 def buildmatch(ui, repo, user, key):
61 '''return tuple of (match function, list enabled).'''
61 '''return tuple of (match function, list enabled).'''
62 if not ui.has_section(key):
62 if not ui.has_section(key):
63 ui.debug('acl: %s not enabled\n' % key)
63 ui.debug('acl: %s not enabled\n' % key)
64 return None
64 return None
65
65
66 pats = [pat for pat, users in ui.configitems(key)
66 pats = [pat for pat, users in ui.configitems(key)
67 if user in users.replace(',', ' ').split()]
67 if user in users.replace(',', ' ').split()]
68 ui.debug('acl: %s enabled, %d entries for user %s\n' %
68 ui.debug('acl: %s enabled, %d entries for user %s\n' %
69 (key, len(pats), user))
69 (key, len(pats), user))
70 if pats:
70 if pats:
71 return match.match(repo.root, '', pats)
71 return match.match(repo.root, '', pats)
72 return match.exact(repo.root, '', [])
72 return match.exact(repo.root, '', [])
73
73
74
74
75 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
75 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
76 if hooktype != 'pretxnchangegroup':
76 if hooktype != 'pretxnchangegroup':
77 raise util.Abort(_('config error - hook type "%s" cannot stop '
77 raise util.Abort(_('config error - hook type "%s" cannot stop '
78 'incoming changesets') % hooktype)
78 'incoming changesets') % hooktype)
79 if source not in ui.config('acl', 'sources', 'serve').split():
79 if source not in ui.config('acl', 'sources', 'serve').split():
80 ui.debug('acl: changes have source "%s" - skipping\n' % source)
80 ui.debug('acl: changes have source "%s" - skipping\n' % source)
81 return
81 return
82
82
83 user = None
83 user = None
84 if source == 'serve' and 'url' in kwargs:
84 if source == 'serve' and 'url' in kwargs:
85 url = kwargs['url'].split(':')
85 url = kwargs['url'].split(':')
86 if url[0] == 'remote' and url[1].startswith('http'):
86 if url[0] == 'remote' and url[1].startswith('http'):
87 user = urllib.unquote(url[3])
87 user = urllib.unquote(url[3])
88
88
89 if user is None:
89 if user is None:
90 user = getpass.getuser()
90 user = getpass.getuser()
91
91
92 cfg = ui.config('acl', 'config')
92 cfg = ui.config('acl', 'config')
93 if cfg:
93 if cfg:
94 ui.readconfig(cfg, sections = ['acl.allow', 'acl.deny'])
94 ui.readconfig(cfg, sections = ['acl.allow', 'acl.deny'])
95 allow = buildmatch(ui, repo, user, 'acl.allow')
95 allow = buildmatch(ui, repo, user, 'acl.allow')
96 deny = buildmatch(ui, repo, user, 'acl.deny')
96 deny = buildmatch(ui, repo, user, 'acl.deny')
97
97
98 for rev in xrange(repo[node], len(repo)):
98 for rev in xrange(repo[node], len(repo)):
99 ctx = repo[rev]
99 ctx = repo[rev]
100 for f in ctx.files():
100 for f in ctx.files():
101 if deny and deny(f):
101 if deny and deny(f):
102 ui.debug('acl: user %s denied on %s\n' % (user, f))
102 ui.debug('acl: user %s denied on %s\n' % (user, f))
103 raise util.Abort(_('acl: access denied for changeset %s') % ctx)
103 raise util.Abort(_('acl: access denied for changeset %s') % ctx)
104 if allow and not allow(f):
104 if allow and not allow(f):
105 ui.debug('acl: user %s not allowed on %s\n' % (user, f))
105 ui.debug('acl: user %s not allowed on %s\n' % (user, f))
106 raise util.Abort(_('acl: access denied for changeset %s') % ctx)
106 raise util.Abort(_('acl: access denied for changeset %s') % ctx)
107 ui.debug('acl: allowing changeset %s\n' % ctx)
107 ui.debug('acl: allowing changeset %s\n' % ctx)
@@ -1,439 +1,439 b''
1 # bugzilla.py - bugzilla integration for mercurial
1 # bugzilla.py - bugzilla integration 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 of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2, incorporated herein by reference.
7
7
8 '''hooks for integrating with the Bugzilla bug tracker
8 '''hooks for integrating with the Bugzilla bug tracker
9
9
10 This hook extension adds comments on bugs in Bugzilla when changesets
10 This hook extension adds comments on bugs in Bugzilla when changesets
11 that refer to bugs by Bugzilla ID are seen. The hook does not change
11 that refer to bugs by Bugzilla ID are seen. The hook does not change
12 bug status.
12 bug status.
13
13
14 The hook updates the Bugzilla database directly. Only Bugzilla
14 The hook updates the Bugzilla database directly. Only Bugzilla
15 installations using MySQL are supported.
15 installations using MySQL are supported.
16
16
17 The hook relies on a Bugzilla script to send bug change notification
17 The hook relies on a Bugzilla script to send bug change notification
18 emails. That script changes between Bugzilla versions; the
18 emails. That script changes between Bugzilla versions; the
19 'processmail' script used prior to 2.18 is replaced in 2.18 and
19 'processmail' script used prior to 2.18 is replaced in 2.18 and
20 subsequent versions by 'config/sendbugmail.pl'. Note that these will
20 subsequent versions by 'config/sendbugmail.pl'. Note that these will
21 be run by Mercurial as the user pushing the change; you will need to
21 be run by Mercurial as the user pushing the change; you will need to
22 ensure the Bugzilla install file permissions are set appropriately.
22 ensure the Bugzilla install file permissions are set appropriately.
23
23
24 The extension is configured through three different configuration
24 The extension is configured through three different configuration
25 sections. These keys are recognized in the [bugzilla] section:
25 sections. These keys are recognized in the [bugzilla] section:
26
26
27 host
27 host
28 Hostname of the MySQL server holding the Bugzilla database.
28 Hostname of the MySQL server holding the Bugzilla database.
29
29
30 db
30 db
31 Name of the Bugzilla database in MySQL. Default 'bugs'.
31 Name of the Bugzilla database in MySQL. Default 'bugs'.
32
32
33 user
33 user
34 Username to use to access MySQL server. Default 'bugs'.
34 Username to use to access MySQL server. Default 'bugs'.
35
35
36 password
36 password
37 Password to use to access MySQL server.
37 Password to use to access MySQL server.
38
38
39 timeout
39 timeout
40 Database connection timeout (seconds). Default 5.
40 Database connection timeout (seconds). Default 5.
41
41
42 version
42 version
43 Bugzilla version. Specify '3.0' for Bugzilla versions 3.0 and later,
43 Bugzilla version. Specify '3.0' for Bugzilla versions 3.0 and later,
44 '2.18' for Bugzilla versions from 2.18 and '2.16' for versions prior
44 '2.18' for Bugzilla versions from 2.18 and '2.16' for versions prior
45 to 2.18.
45 to 2.18.
46
46
47 bzuser
47 bzuser
48 Fallback Bugzilla user name to record comments with, if changeset
48 Fallback Bugzilla user name to record comments with, if changeset
49 committer cannot be found as a Bugzilla user.
49 committer cannot be found as a Bugzilla user.
50
50
51 bzdir
51 bzdir
52 Bugzilla install directory. Used by default notify. Default
52 Bugzilla install directory. Used by default notify. Default
53 '/var/www/html/bugzilla'.
53 '/var/www/html/bugzilla'.
54
54
55 notify
55 notify
56 The command to run to get Bugzilla to send bug change notification
56 The command to run to get Bugzilla to send bug change notification
57 emails. Substitutes from a map with 3 keys, 'bzdir', 'id' (bug id)
57 emails. Substitutes from a map with 3 keys, 'bzdir', 'id' (bug id)
58 and 'user' (committer bugzilla email). Default depends on version;
58 and 'user' (committer bugzilla email). Default depends on version;
59 from 2.18 it is "cd %(bzdir)s && perl -T contrib/sendbugmail.pl
59 from 2.18 it is "cd %(bzdir)s && perl -T contrib/sendbugmail.pl
60 %(id)s %(user)s".
60 %(id)s %(user)s".
61
61
62 regexp
62 regexp
63 Regular expression to match bug IDs in changeset commit message.
63 Regular expression to match bug IDs in changeset commit message.
64 Must contain one "()" group. The default expression matches 'Bug
64 Must contain one "()" group. The default expression matches 'Bug
65 1234', 'Bug no. 1234', 'Bug number 1234', 'Bugs 1234,5678', 'Bug
65 1234', 'Bug no. 1234', 'Bug number 1234', 'Bugs 1234,5678', 'Bug
66 1234 and 5678' and variations thereof. Matching is case insensitive.
66 1234 and 5678' and variations thereof. Matching is case insensitive.
67
67
68 style
68 style
69 The style file to use when formatting comments.
69 The style file to use when formatting comments.
70
70
71 template
71 template
72 Template to use when formatting comments. Overrides style if
72 Template to use when formatting comments. Overrides style if
73 specified. In addition to the usual Mercurial keywords, the
73 specified. In addition to the usual Mercurial keywords, the
74 extension specifies::
74 extension specifies::
75
75
76 {bug} The Bugzilla bug ID.
76 {bug} The Bugzilla bug ID.
77 {root} The full pathname of the Mercurial repository.
77 {root} The full pathname of the Mercurial repository.
78 {webroot} Stripped pathname of the Mercurial repository.
78 {webroot} Stripped pathname of the Mercurial repository.
79 {hgweb} Base URL for browsing Mercurial repositories.
79 {hgweb} Base URL for browsing Mercurial repositories.
80
80
81 Default 'changeset {node|short} in repo {root} refers '
81 Default 'changeset {node|short} in repo {root} refers '
82 'to bug {bug}.\\ndetails:\\n\\t{desc|tabindent}'
82 'to bug {bug}.\\ndetails:\\n\\t{desc|tabindent}'
83
83
84 strip
84 strip
85 The number of slashes to strip from the front of {root} to produce
85 The number of slashes to strip from the front of {root} to produce
86 {webroot}. Default 0.
86 {webroot}. Default 0.
87
87
88 usermap
88 usermap
89 Path of file containing Mercurial committer ID to Bugzilla user ID
89 Path of file containing Mercurial committer ID to Bugzilla user ID
90 mappings. If specified, the file should contain one mapping per
90 mappings. If specified, the file should contain one mapping per
91 line, "committer"="Bugzilla user". See also the [usermap] section.
91 line, "committer"="Bugzilla user". See also the [usermap] section.
92
92
93 The [usermap] section is used to specify mappings of Mercurial
93 The [usermap] section is used to specify mappings of Mercurial
94 committer ID to Bugzilla user ID. See also [bugzilla].usermap.
94 committer ID to Bugzilla user ID. See also [bugzilla].usermap.
95 "committer"="Bugzilla user"
95 "committer"="Bugzilla user"
96
96
97 Finally, the [web] section supports one entry:
97 Finally, the [web] section supports one entry:
98
98
99 baseurl
99 baseurl
100 Base URL for browsing Mercurial repositories. Reference from
100 Base URL for browsing Mercurial repositories. Reference from
101 templates as {hgweb}.
101 templates as {hgweb}.
102
102
103 Activating the extension::
103 Activating the extension::
104
104
105 [extensions]
105 [extensions]
106 hgext.bugzilla =
106 bugzilla =
107
107
108 [hooks]
108 [hooks]
109 # run bugzilla hook on every change pulled or pushed in here
109 # run bugzilla hook on every change pulled or pushed in here
110 incoming.bugzilla = python:hgext.bugzilla.hook
110 incoming.bugzilla = python:hgext.bugzilla.hook
111
111
112 Example configuration:
112 Example configuration:
113
113
114 This example configuration is for a collection of Mercurial
114 This example configuration is for a collection of Mercurial
115 repositories in /var/local/hg/repos/ used with a local Bugzilla 3.2
115 repositories in /var/local/hg/repos/ used with a local Bugzilla 3.2
116 installation in /opt/bugzilla-3.2. ::
116 installation in /opt/bugzilla-3.2. ::
117
117
118 [bugzilla]
118 [bugzilla]
119 host=localhost
119 host=localhost
120 password=XYZZY
120 password=XYZZY
121 version=3.0
121 version=3.0
122 bzuser=unknown@domain.com
122 bzuser=unknown@domain.com
123 bzdir=/opt/bugzilla-3.2
123 bzdir=/opt/bugzilla-3.2
124 template=Changeset {node|short} in {root|basename}.
124 template=Changeset {node|short} in {root|basename}.
125 {hgweb}/{webroot}/rev/{node|short}\\n
125 {hgweb}/{webroot}/rev/{node|short}\\n
126 {desc}\\n
126 {desc}\\n
127 strip=5
127 strip=5
128
128
129 [web]
129 [web]
130 baseurl=http://dev.domain.com/hg
130 baseurl=http://dev.domain.com/hg
131
131
132 [usermap]
132 [usermap]
133 user@emaildomain.com=user.name@bugzilladomain.com
133 user@emaildomain.com=user.name@bugzilladomain.com
134
134
135 Commits add a comment to the Bugzilla bug record of the form::
135 Commits add a comment to the Bugzilla bug record of the form::
136
136
137 Changeset 3b16791d6642 in repository-name.
137 Changeset 3b16791d6642 in repository-name.
138 http://dev.domain.com/hg/repository-name/rev/3b16791d6642
138 http://dev.domain.com/hg/repository-name/rev/3b16791d6642
139
139
140 Changeset commit comment. Bug 1234.
140 Changeset commit comment. Bug 1234.
141 '''
141 '''
142
142
143 from mercurial.i18n import _
143 from mercurial.i18n import _
144 from mercurial.node import short
144 from mercurial.node import short
145 from mercurial import cmdutil, templater, util
145 from mercurial import cmdutil, templater, util
146 import re, time
146 import re, time
147
147
148 MySQLdb = None
148 MySQLdb = None
149
149
150 def buglist(ids):
150 def buglist(ids):
151 return '(' + ','.join(map(str, ids)) + ')'
151 return '(' + ','.join(map(str, ids)) + ')'
152
152
153 class bugzilla_2_16(object):
153 class bugzilla_2_16(object):
154 '''support for bugzilla version 2.16.'''
154 '''support for bugzilla version 2.16.'''
155
155
156 def __init__(self, ui):
156 def __init__(self, ui):
157 self.ui = ui
157 self.ui = ui
158 host = self.ui.config('bugzilla', 'host', 'localhost')
158 host = self.ui.config('bugzilla', 'host', 'localhost')
159 user = self.ui.config('bugzilla', 'user', 'bugs')
159 user = self.ui.config('bugzilla', 'user', 'bugs')
160 passwd = self.ui.config('bugzilla', 'password')
160 passwd = self.ui.config('bugzilla', 'password')
161 db = self.ui.config('bugzilla', 'db', 'bugs')
161 db = self.ui.config('bugzilla', 'db', 'bugs')
162 timeout = int(self.ui.config('bugzilla', 'timeout', 5))
162 timeout = int(self.ui.config('bugzilla', 'timeout', 5))
163 usermap = self.ui.config('bugzilla', 'usermap')
163 usermap = self.ui.config('bugzilla', 'usermap')
164 if usermap:
164 if usermap:
165 self.ui.readconfig(usermap, sections=['usermap'])
165 self.ui.readconfig(usermap, sections=['usermap'])
166 self.ui.note(_('connecting to %s:%s as %s, password %s\n') %
166 self.ui.note(_('connecting to %s:%s as %s, password %s\n') %
167 (host, db, user, '*' * len(passwd)))
167 (host, db, user, '*' * len(passwd)))
168 self.conn = MySQLdb.connect(host=host, user=user, passwd=passwd,
168 self.conn = MySQLdb.connect(host=host, user=user, passwd=passwd,
169 db=db, connect_timeout=timeout)
169 db=db, connect_timeout=timeout)
170 self.cursor = self.conn.cursor()
170 self.cursor = self.conn.cursor()
171 self.longdesc_id = self.get_longdesc_id()
171 self.longdesc_id = self.get_longdesc_id()
172 self.user_ids = {}
172 self.user_ids = {}
173 self.default_notify = "cd %(bzdir)s && ./processmail %(id)s %(user)s"
173 self.default_notify = "cd %(bzdir)s && ./processmail %(id)s %(user)s"
174
174
175 def run(self, *args, **kwargs):
175 def run(self, *args, **kwargs):
176 '''run a query.'''
176 '''run a query.'''
177 self.ui.note(_('query: %s %s\n') % (args, kwargs))
177 self.ui.note(_('query: %s %s\n') % (args, kwargs))
178 try:
178 try:
179 self.cursor.execute(*args, **kwargs)
179 self.cursor.execute(*args, **kwargs)
180 except MySQLdb.MySQLError:
180 except MySQLdb.MySQLError:
181 self.ui.note(_('failed query: %s %s\n') % (args, kwargs))
181 self.ui.note(_('failed query: %s %s\n') % (args, kwargs))
182 raise
182 raise
183
183
184 def get_longdesc_id(self):
184 def get_longdesc_id(self):
185 '''get identity of longdesc field'''
185 '''get identity of longdesc field'''
186 self.run('select fieldid from fielddefs where name = "longdesc"')
186 self.run('select fieldid from fielddefs where name = "longdesc"')
187 ids = self.cursor.fetchall()
187 ids = self.cursor.fetchall()
188 if len(ids) != 1:
188 if len(ids) != 1:
189 raise util.Abort(_('unknown database schema'))
189 raise util.Abort(_('unknown database schema'))
190 return ids[0][0]
190 return ids[0][0]
191
191
192 def filter_real_bug_ids(self, ids):
192 def filter_real_bug_ids(self, ids):
193 '''filter not-existing bug ids from list.'''
193 '''filter not-existing bug ids from list.'''
194 self.run('select bug_id from bugs where bug_id in %s' % buglist(ids))
194 self.run('select bug_id from bugs where bug_id in %s' % buglist(ids))
195 return sorted([c[0] for c in self.cursor.fetchall()])
195 return sorted([c[0] for c in self.cursor.fetchall()])
196
196
197 def filter_unknown_bug_ids(self, node, ids):
197 def filter_unknown_bug_ids(self, node, ids):
198 '''filter bug ids from list that already refer to this changeset.'''
198 '''filter bug ids from list that already refer to this changeset.'''
199
199
200 self.run('''select bug_id from longdescs where
200 self.run('''select bug_id from longdescs where
201 bug_id in %s and thetext like "%%%s%%"''' %
201 bug_id in %s and thetext like "%%%s%%"''' %
202 (buglist(ids), short(node)))
202 (buglist(ids), short(node)))
203 unknown = set(ids)
203 unknown = set(ids)
204 for (id,) in self.cursor.fetchall():
204 for (id,) in self.cursor.fetchall():
205 self.ui.status(_('bug %d already knows about changeset %s\n') %
205 self.ui.status(_('bug %d already knows about changeset %s\n') %
206 (id, short(node)))
206 (id, short(node)))
207 unknown.discard(id)
207 unknown.discard(id)
208 return sorted(unknown)
208 return sorted(unknown)
209
209
210 def notify(self, ids, committer):
210 def notify(self, ids, committer):
211 '''tell bugzilla to send mail.'''
211 '''tell bugzilla to send mail.'''
212
212
213 self.ui.status(_('telling bugzilla to send mail:\n'))
213 self.ui.status(_('telling bugzilla to send mail:\n'))
214 (user, userid) = self.get_bugzilla_user(committer)
214 (user, userid) = self.get_bugzilla_user(committer)
215 for id in ids:
215 for id in ids:
216 self.ui.status(_(' bug %s\n') % id)
216 self.ui.status(_(' bug %s\n') % id)
217 cmdfmt = self.ui.config('bugzilla', 'notify', self.default_notify)
217 cmdfmt = self.ui.config('bugzilla', 'notify', self.default_notify)
218 bzdir = self.ui.config('bugzilla', 'bzdir', '/var/www/html/bugzilla')
218 bzdir = self.ui.config('bugzilla', 'bzdir', '/var/www/html/bugzilla')
219 try:
219 try:
220 # Backwards-compatible with old notify string, which
220 # Backwards-compatible with old notify string, which
221 # took one string. This will throw with a new format
221 # took one string. This will throw with a new format
222 # string.
222 # string.
223 cmd = cmdfmt % id
223 cmd = cmdfmt % id
224 except TypeError:
224 except TypeError:
225 cmd = cmdfmt % {'bzdir': bzdir, 'id': id, 'user': user}
225 cmd = cmdfmt % {'bzdir': bzdir, 'id': id, 'user': user}
226 self.ui.note(_('running notify command %s\n') % cmd)
226 self.ui.note(_('running notify command %s\n') % cmd)
227 fp = util.popen('(%s) 2>&1' % cmd)
227 fp = util.popen('(%s) 2>&1' % cmd)
228 out = fp.read()
228 out = fp.read()
229 ret = fp.close()
229 ret = fp.close()
230 if ret:
230 if ret:
231 self.ui.warn(out)
231 self.ui.warn(out)
232 raise util.Abort(_('bugzilla notify command %s') %
232 raise util.Abort(_('bugzilla notify command %s') %
233 util.explain_exit(ret)[0])
233 util.explain_exit(ret)[0])
234 self.ui.status(_('done\n'))
234 self.ui.status(_('done\n'))
235
235
236 def get_user_id(self, user):
236 def get_user_id(self, user):
237 '''look up numeric bugzilla user id.'''
237 '''look up numeric bugzilla user id.'''
238 try:
238 try:
239 return self.user_ids[user]
239 return self.user_ids[user]
240 except KeyError:
240 except KeyError:
241 try:
241 try:
242 userid = int(user)
242 userid = int(user)
243 except ValueError:
243 except ValueError:
244 self.ui.note(_('looking up user %s\n') % user)
244 self.ui.note(_('looking up user %s\n') % user)
245 self.run('''select userid from profiles
245 self.run('''select userid from profiles
246 where login_name like %s''', user)
246 where login_name like %s''', user)
247 all = self.cursor.fetchall()
247 all = self.cursor.fetchall()
248 if len(all) != 1:
248 if len(all) != 1:
249 raise KeyError(user)
249 raise KeyError(user)
250 userid = int(all[0][0])
250 userid = int(all[0][0])
251 self.user_ids[user] = userid
251 self.user_ids[user] = userid
252 return userid
252 return userid
253
253
254 def map_committer(self, user):
254 def map_committer(self, user):
255 '''map name of committer to bugzilla user name.'''
255 '''map name of committer to bugzilla user name.'''
256 for committer, bzuser in self.ui.configitems('usermap'):
256 for committer, bzuser in self.ui.configitems('usermap'):
257 if committer.lower() == user.lower():
257 if committer.lower() == user.lower():
258 return bzuser
258 return bzuser
259 return user
259 return user
260
260
261 def get_bugzilla_user(self, committer):
261 def get_bugzilla_user(self, committer):
262 '''see if committer is a registered bugzilla user. Return
262 '''see if committer is a registered bugzilla user. Return
263 bugzilla username and userid if so. If not, return default
263 bugzilla username and userid if so. If not, return default
264 bugzilla username and userid.'''
264 bugzilla username and userid.'''
265 user = self.map_committer(committer)
265 user = self.map_committer(committer)
266 try:
266 try:
267 userid = self.get_user_id(user)
267 userid = self.get_user_id(user)
268 except KeyError:
268 except KeyError:
269 try:
269 try:
270 defaultuser = self.ui.config('bugzilla', 'bzuser')
270 defaultuser = self.ui.config('bugzilla', 'bzuser')
271 if not defaultuser:
271 if not defaultuser:
272 raise util.Abort(_('cannot find bugzilla user id for %s') %
272 raise util.Abort(_('cannot find bugzilla user id for %s') %
273 user)
273 user)
274 userid = self.get_user_id(defaultuser)
274 userid = self.get_user_id(defaultuser)
275 user = defaultuser
275 user = defaultuser
276 except KeyError:
276 except KeyError:
277 raise util.Abort(_('cannot find bugzilla user id for %s or %s') %
277 raise util.Abort(_('cannot find bugzilla user id for %s or %s') %
278 (user, defaultuser))
278 (user, defaultuser))
279 return (user, userid)
279 return (user, userid)
280
280
281 def add_comment(self, bugid, text, committer):
281 def add_comment(self, bugid, text, committer):
282 '''add comment to bug. try adding comment as committer of
282 '''add comment to bug. try adding comment as committer of
283 changeset, otherwise as default bugzilla user.'''
283 changeset, otherwise as default bugzilla user.'''
284 (user, userid) = self.get_bugzilla_user(committer)
284 (user, userid) = self.get_bugzilla_user(committer)
285 now = time.strftime('%Y-%m-%d %H:%M:%S')
285 now = time.strftime('%Y-%m-%d %H:%M:%S')
286 self.run('''insert into longdescs
286 self.run('''insert into longdescs
287 (bug_id, who, bug_when, thetext)
287 (bug_id, who, bug_when, thetext)
288 values (%s, %s, %s, %s)''',
288 values (%s, %s, %s, %s)''',
289 (bugid, userid, now, text))
289 (bugid, userid, now, text))
290 self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
290 self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
291 values (%s, %s, %s, %s)''',
291 values (%s, %s, %s, %s)''',
292 (bugid, userid, now, self.longdesc_id))
292 (bugid, userid, now, self.longdesc_id))
293 self.conn.commit()
293 self.conn.commit()
294
294
295 class bugzilla_2_18(bugzilla_2_16):
295 class bugzilla_2_18(bugzilla_2_16):
296 '''support for bugzilla 2.18 series.'''
296 '''support for bugzilla 2.18 series.'''
297
297
298 def __init__(self, ui):
298 def __init__(self, ui):
299 bugzilla_2_16.__init__(self, ui)
299 bugzilla_2_16.__init__(self, ui)
300 self.default_notify = "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
300 self.default_notify = "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
301
301
302 class bugzilla_3_0(bugzilla_2_18):
302 class bugzilla_3_0(bugzilla_2_18):
303 '''support for bugzilla 3.0 series.'''
303 '''support for bugzilla 3.0 series.'''
304
304
305 def __init__(self, ui):
305 def __init__(self, ui):
306 bugzilla_2_18.__init__(self, ui)
306 bugzilla_2_18.__init__(self, ui)
307
307
308 def get_longdesc_id(self):
308 def get_longdesc_id(self):
309 '''get identity of longdesc field'''
309 '''get identity of longdesc field'''
310 self.run('select id from fielddefs where name = "longdesc"')
310 self.run('select id from fielddefs where name = "longdesc"')
311 ids = self.cursor.fetchall()
311 ids = self.cursor.fetchall()
312 if len(ids) != 1:
312 if len(ids) != 1:
313 raise util.Abort(_('unknown database schema'))
313 raise util.Abort(_('unknown database schema'))
314 return ids[0][0]
314 return ids[0][0]
315
315
316 class bugzilla(object):
316 class bugzilla(object):
317 # supported versions of bugzilla. different versions have
317 # supported versions of bugzilla. different versions have
318 # different schemas.
318 # different schemas.
319 _versions = {
319 _versions = {
320 '2.16': bugzilla_2_16,
320 '2.16': bugzilla_2_16,
321 '2.18': bugzilla_2_18,
321 '2.18': bugzilla_2_18,
322 '3.0': bugzilla_3_0
322 '3.0': bugzilla_3_0
323 }
323 }
324
324
325 _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
325 _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
326 r'((?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)')
326 r'((?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)')
327
327
328 _bz = None
328 _bz = None
329
329
330 def __init__(self, ui, repo):
330 def __init__(self, ui, repo):
331 self.ui = ui
331 self.ui = ui
332 self.repo = repo
332 self.repo = repo
333
333
334 def bz(self):
334 def bz(self):
335 '''return object that knows how to talk to bugzilla version in
335 '''return object that knows how to talk to bugzilla version in
336 use.'''
336 use.'''
337
337
338 if bugzilla._bz is None:
338 if bugzilla._bz is None:
339 bzversion = self.ui.config('bugzilla', 'version')
339 bzversion = self.ui.config('bugzilla', 'version')
340 try:
340 try:
341 bzclass = bugzilla._versions[bzversion]
341 bzclass = bugzilla._versions[bzversion]
342 except KeyError:
342 except KeyError:
343 raise util.Abort(_('bugzilla version %s not supported') %
343 raise util.Abort(_('bugzilla version %s not supported') %
344 bzversion)
344 bzversion)
345 bugzilla._bz = bzclass(self.ui)
345 bugzilla._bz = bzclass(self.ui)
346 return bugzilla._bz
346 return bugzilla._bz
347
347
348 def __getattr__(self, key):
348 def __getattr__(self, key):
349 return getattr(self.bz(), key)
349 return getattr(self.bz(), key)
350
350
351 _bug_re = None
351 _bug_re = None
352 _split_re = None
352 _split_re = None
353
353
354 def find_bug_ids(self, ctx):
354 def find_bug_ids(self, ctx):
355 '''find valid bug ids that are referred to in changeset
355 '''find valid bug ids that are referred to in changeset
356 comments and that do not already have references to this
356 comments and that do not already have references to this
357 changeset.'''
357 changeset.'''
358
358
359 if bugzilla._bug_re is None:
359 if bugzilla._bug_re is None:
360 bugzilla._bug_re = re.compile(
360 bugzilla._bug_re = re.compile(
361 self.ui.config('bugzilla', 'regexp', bugzilla._default_bug_re),
361 self.ui.config('bugzilla', 'regexp', bugzilla._default_bug_re),
362 re.IGNORECASE)
362 re.IGNORECASE)
363 bugzilla._split_re = re.compile(r'\D+')
363 bugzilla._split_re = re.compile(r'\D+')
364 start = 0
364 start = 0
365 ids = set()
365 ids = set()
366 while True:
366 while True:
367 m = bugzilla._bug_re.search(ctx.description(), start)
367 m = bugzilla._bug_re.search(ctx.description(), start)
368 if not m:
368 if not m:
369 break
369 break
370 start = m.end()
370 start = m.end()
371 for id in bugzilla._split_re.split(m.group(1)):
371 for id in bugzilla._split_re.split(m.group(1)):
372 if not id: continue
372 if not id: continue
373 ids.add(int(id))
373 ids.add(int(id))
374 if ids:
374 if ids:
375 ids = self.filter_real_bug_ids(ids)
375 ids = self.filter_real_bug_ids(ids)
376 if ids:
376 if ids:
377 ids = self.filter_unknown_bug_ids(ctx.node(), ids)
377 ids = self.filter_unknown_bug_ids(ctx.node(), ids)
378 return ids
378 return ids
379
379
380 def update(self, bugid, ctx):
380 def update(self, bugid, ctx):
381 '''update bugzilla bug with reference to changeset.'''
381 '''update bugzilla bug with reference to changeset.'''
382
382
383 def webroot(root):
383 def webroot(root):
384 '''strip leading prefix of repo root and turn into
384 '''strip leading prefix of repo root and turn into
385 url-safe path.'''
385 url-safe path.'''
386 count = int(self.ui.config('bugzilla', 'strip', 0))
386 count = int(self.ui.config('bugzilla', 'strip', 0))
387 root = util.pconvert(root)
387 root = util.pconvert(root)
388 while count > 0:
388 while count > 0:
389 c = root.find('/')
389 c = root.find('/')
390 if c == -1:
390 if c == -1:
391 break
391 break
392 root = root[c+1:]
392 root = root[c+1:]
393 count -= 1
393 count -= 1
394 return root
394 return root
395
395
396 mapfile = self.ui.config('bugzilla', 'style')
396 mapfile = self.ui.config('bugzilla', 'style')
397 tmpl = self.ui.config('bugzilla', 'template')
397 tmpl = self.ui.config('bugzilla', 'template')
398 t = cmdutil.changeset_templater(self.ui, self.repo,
398 t = cmdutil.changeset_templater(self.ui, self.repo,
399 False, None, mapfile, False)
399 False, None, mapfile, False)
400 if not mapfile and not tmpl:
400 if not mapfile and not tmpl:
401 tmpl = _('changeset {node|short} in repo {root} refers '
401 tmpl = _('changeset {node|short} in repo {root} refers '
402 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
402 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
403 if tmpl:
403 if tmpl:
404 tmpl = templater.parsestring(tmpl, quoted=False)
404 tmpl = templater.parsestring(tmpl, quoted=False)
405 t.use_template(tmpl)
405 t.use_template(tmpl)
406 self.ui.pushbuffer()
406 self.ui.pushbuffer()
407 t.show(ctx, changes=ctx.changeset(),
407 t.show(ctx, changes=ctx.changeset(),
408 bug=str(bugid),
408 bug=str(bugid),
409 hgweb=self.ui.config('web', 'baseurl'),
409 hgweb=self.ui.config('web', 'baseurl'),
410 root=self.repo.root,
410 root=self.repo.root,
411 webroot=webroot(self.repo.root))
411 webroot=webroot(self.repo.root))
412 data = self.ui.popbuffer()
412 data = self.ui.popbuffer()
413 self.add_comment(bugid, data, util.email(ctx.user()))
413 self.add_comment(bugid, data, util.email(ctx.user()))
414
414
415 def hook(ui, repo, hooktype, node=None, **kwargs):
415 def hook(ui, repo, hooktype, node=None, **kwargs):
416 '''add comment to bugzilla for each changeset that refers to a
416 '''add comment to bugzilla for each changeset that refers to a
417 bugzilla bug id. only add a comment once per bug, so same change
417 bugzilla bug id. only add a comment once per bug, so same change
418 seen multiple times does not fill bug with duplicate data.'''
418 seen multiple times does not fill bug with duplicate data.'''
419 try:
419 try:
420 import MySQLdb as mysql
420 import MySQLdb as mysql
421 global MySQLdb
421 global MySQLdb
422 MySQLdb = mysql
422 MySQLdb = mysql
423 except ImportError, err:
423 except ImportError, err:
424 raise util.Abort(_('python mysql support not available: %s') % err)
424 raise util.Abort(_('python mysql support not available: %s') % err)
425
425
426 if node is None:
426 if node is None:
427 raise util.Abort(_('hook type %s does not pass a changeset id') %
427 raise util.Abort(_('hook type %s does not pass a changeset id') %
428 hooktype)
428 hooktype)
429 try:
429 try:
430 bz = bugzilla(ui, repo)
430 bz = bugzilla(ui, repo)
431 ctx = repo[node]
431 ctx = repo[node]
432 ids = bz.find_bug_ids(ctx)
432 ids = bz.find_bug_ids(ctx)
433 if ids:
433 if ids:
434 for id in ids:
434 for id in ids:
435 bz.update(id, ctx)
435 bz.update(id, ctx)
436 bz.notify(ids, util.email(ctx.user()))
436 bz.notify(ids, util.email(ctx.user()))
437 except MySQLdb.MySQLError, err:
437 except MySQLdb.MySQLError, err:
438 raise util.Abort(_('database error: %s') % err[1])
438 raise util.Abort(_('database error: %s') % err[1])
439
439
@@ -1,316 +1,316 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 of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2, incorporated herein by reference.
7
7
8 '''hooks for sending email notifications at commit/push time
8 '''hooks for sending email notifications at commit/push time
9
9
10 Subscriptions can be managed through a hgrc file. Default mode is to
10 Subscriptions can be managed through a hgrc file. Default mode is to
11 print messages to stdout, for testing and configuring.
11 print messages to stdout, for testing and configuring.
12
12
13 To use, configure the notify extension and enable it in hgrc like
13 To use, configure the notify extension and enable it in hgrc like
14 this::
14 this::
15
15
16 [extensions]
16 [extensions]
17 hgext.notify =
17 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 here
26 # config items go here
27
27
28 Required configuration items::
28 Required configuration items::
29
29
30 config = /path/to/file # file containing subscriptions
30 config = /path/to/file # file containing subscriptions
31
31
32 Optional configuration items::
32 Optional configuration items::
33
33
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 merge = False # send notification for merges (default True)
46 merge = False # send notification for merges (default True)
47 [email]
47 [email]
48 from = user@host.com # email address to send as if none given
48 from = user@host.com # email address to send as if none given
49 [web]
49 [web]
50 baseurl = http://hgserver/... # root of hg web site for browsing commits
50 baseurl = http://hgserver/... # root of hg web site for browsing commits
51
51
52 The notify config file has same format as a regular hgrc file. It has
52 The notify config file has same format as a regular hgrc file. It has
53 two sections so you can express subscriptions in whatever way is
53 two sections so you can express subscriptions in whatever way is
54 handier for you.
54 handier for you.
55
55
56 ::
56 ::
57
57
58 [usersubs]
58 [usersubs]
59 # key is subscriber email, value is ","-separated list of glob patterns
59 # key is subscriber email, value is ","-separated list of glob patterns
60 user@host = pattern
60 user@host = pattern
61
61
62 [reposubs]
62 [reposubs]
63 # key is glob pattern, value is ","-separated list of subscriber emails
63 # key is glob pattern, value is ","-separated list of subscriber emails
64 pattern = user@host
64 pattern = user@host
65
65
66 Glob patterns are matched against path to repository root.
66 Glob patterns are matched against path to repository root.
67
67
68 If you like, you can put notify config file in repository that users
68 If you like, you can put notify config file in repository that users
69 can push changes to, they can manage their own subscriptions.
69 can push changes to, they can manage their own subscriptions.
70 '''
70 '''
71
71
72 from mercurial.i18n import _
72 from mercurial.i18n import _
73 from mercurial import patch, cmdutil, templater, util, mail
73 from mercurial import patch, cmdutil, templater, util, mail
74 import email.Parser, email.Errors, fnmatch, socket, time
74 import email.Parser, email.Errors, fnmatch, socket, time
75
75
76 # template for single changeset can include email headers.
76 # template for single changeset can include email headers.
77 single_template = '''
77 single_template = '''
78 Subject: changeset in {webroot}: {desc|firstline|strip}
78 Subject: changeset in {webroot}: {desc|firstline|strip}
79 From: {author}
79 From: {author}
80
80
81 changeset {node|short} in {root}
81 changeset {node|short} in {root}
82 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
82 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
83 description:
83 description:
84 \t{desc|tabindent|strip}
84 \t{desc|tabindent|strip}
85 '''.lstrip()
85 '''.lstrip()
86
86
87 # template for multiple changesets should not contain email headers,
87 # template for multiple changesets should not contain email headers,
88 # because only first set of headers will be used and result will look
88 # because only first set of headers will be used and result will look
89 # strange.
89 # strange.
90 multiple_template = '''
90 multiple_template = '''
91 changeset {node|short} in {root}
91 changeset {node|short} in {root}
92 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
92 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
93 summary: {desc|firstline}
93 summary: {desc|firstline}
94 '''
94 '''
95
95
96 deftemplates = {
96 deftemplates = {
97 'changegroup': multiple_template,
97 'changegroup': multiple_template,
98 }
98 }
99
99
100 class notifier(object):
100 class notifier(object):
101 '''email notification class.'''
101 '''email notification class.'''
102
102
103 def __init__(self, ui, repo, hooktype):
103 def __init__(self, ui, repo, hooktype):
104 self.ui = ui
104 self.ui = ui
105 cfg = self.ui.config('notify', 'config')
105 cfg = self.ui.config('notify', 'config')
106 if cfg:
106 if cfg:
107 self.ui.readconfig(cfg, sections=['usersubs', 'reposubs'])
107 self.ui.readconfig(cfg, sections=['usersubs', 'reposubs'])
108 self.repo = repo
108 self.repo = repo
109 self.stripcount = int(self.ui.config('notify', 'strip', 0))
109 self.stripcount = int(self.ui.config('notify', 'strip', 0))
110 self.root = self.strip(self.repo.root)
110 self.root = self.strip(self.repo.root)
111 self.domain = self.ui.config('notify', 'domain')
111 self.domain = self.ui.config('notify', 'domain')
112 self.test = self.ui.configbool('notify', 'test', True)
112 self.test = self.ui.configbool('notify', 'test', True)
113 self.charsets = mail._charsets(self.ui)
113 self.charsets = mail._charsets(self.ui)
114 self.subs = self.subscribers()
114 self.subs = self.subscribers()
115 self.merge = self.ui.configbool('notify', 'merge', True)
115 self.merge = self.ui.configbool('notify', 'merge', True)
116
116
117 mapfile = self.ui.config('notify', 'style')
117 mapfile = self.ui.config('notify', 'style')
118 template = (self.ui.config('notify', hooktype) or
118 template = (self.ui.config('notify', hooktype) or
119 self.ui.config('notify', 'template'))
119 self.ui.config('notify', 'template'))
120 self.t = cmdutil.changeset_templater(self.ui, self.repo,
120 self.t = cmdutil.changeset_templater(self.ui, self.repo,
121 False, None, mapfile, False)
121 False, None, mapfile, False)
122 if not mapfile and not template:
122 if not mapfile and not template:
123 template = deftemplates.get(hooktype) or single_template
123 template = deftemplates.get(hooktype) or single_template
124 if template:
124 if template:
125 template = templater.parsestring(template, quoted=False)
125 template = templater.parsestring(template, quoted=False)
126 self.t.use_template(template)
126 self.t.use_template(template)
127
127
128 def strip(self, path):
128 def strip(self, path):
129 '''strip leading slashes from local path, turn into web-safe path.'''
129 '''strip leading slashes from local path, turn into web-safe path.'''
130
130
131 path = util.pconvert(path)
131 path = util.pconvert(path)
132 count = self.stripcount
132 count = self.stripcount
133 while count > 0:
133 while count > 0:
134 c = path.find('/')
134 c = path.find('/')
135 if c == -1:
135 if c == -1:
136 break
136 break
137 path = path[c+1:]
137 path = path[c+1:]
138 count -= 1
138 count -= 1
139 return path
139 return path
140
140
141 def fixmail(self, addr):
141 def fixmail(self, addr):
142 '''try to clean up email addresses.'''
142 '''try to clean up email addresses.'''
143
143
144 addr = util.email(addr.strip())
144 addr = util.email(addr.strip())
145 if self.domain:
145 if self.domain:
146 a = addr.find('@localhost')
146 a = addr.find('@localhost')
147 if a != -1:
147 if a != -1:
148 addr = addr[:a]
148 addr = addr[:a]
149 if '@' not in addr:
149 if '@' not in addr:
150 return addr + '@' + self.domain
150 return addr + '@' + self.domain
151 return addr
151 return addr
152
152
153 def subscribers(self):
153 def subscribers(self):
154 '''return list of email addresses of subscribers to this repo.'''
154 '''return list of email addresses of subscribers to this repo.'''
155 subs = set()
155 subs = set()
156 for user, pats in self.ui.configitems('usersubs'):
156 for user, pats in self.ui.configitems('usersubs'):
157 for pat in pats.split(','):
157 for pat in pats.split(','):
158 if fnmatch.fnmatch(self.repo.root, pat.strip()):
158 if fnmatch.fnmatch(self.repo.root, pat.strip()):
159 subs.add(self.fixmail(user))
159 subs.add(self.fixmail(user))
160 for pat, users in self.ui.configitems('reposubs'):
160 for pat, users in self.ui.configitems('reposubs'):
161 if fnmatch.fnmatch(self.repo.root, pat):
161 if fnmatch.fnmatch(self.repo.root, pat):
162 for user in users.split(','):
162 for user in users.split(','):
163 subs.add(self.fixmail(user))
163 subs.add(self.fixmail(user))
164 return [mail.addressencode(self.ui, s, self.charsets, self.test)
164 return [mail.addressencode(self.ui, s, self.charsets, self.test)
165 for s in sorted(subs)]
165 for s in sorted(subs)]
166
166
167 def url(self, path=None):
167 def url(self, path=None):
168 return self.ui.config('web', 'baseurl') + (path or self.root)
168 return self.ui.config('web', 'baseurl') + (path or self.root)
169
169
170 def node(self, ctx, **props):
170 def node(self, ctx, **props):
171 '''format one changeset, unless it is a suppressed merge.'''
171 '''format one changeset, unless it is a suppressed merge.'''
172 if not self.merge and len(ctx.parents()) > 1:
172 if not self.merge and len(ctx.parents()) > 1:
173 return False
173 return False
174 self.t.show(ctx, changes=ctx.changeset(),
174 self.t.show(ctx, changes=ctx.changeset(),
175 baseurl=self.ui.config('web', 'baseurl'),
175 baseurl=self.ui.config('web', 'baseurl'),
176 root=self.repo.root, webroot=self.root, **props)
176 root=self.repo.root, webroot=self.root, **props)
177 return True
177 return True
178
178
179 def skipsource(self, source):
179 def skipsource(self, source):
180 '''true if incoming changes from this source should be skipped.'''
180 '''true if incoming changes from this source should be skipped.'''
181 ok_sources = self.ui.config('notify', 'sources', 'serve').split()
181 ok_sources = self.ui.config('notify', 'sources', 'serve').split()
182 return source not in ok_sources
182 return source not in ok_sources
183
183
184 def send(self, ctx, count, data):
184 def send(self, ctx, count, data):
185 '''send message.'''
185 '''send message.'''
186
186
187 p = email.Parser.Parser()
187 p = email.Parser.Parser()
188 try:
188 try:
189 msg = p.parsestr(data)
189 msg = p.parsestr(data)
190 except email.Errors.MessageParseError, inst:
190 except email.Errors.MessageParseError, inst:
191 raise util.Abort(inst)
191 raise util.Abort(inst)
192
192
193 # store sender and subject
193 # store sender and subject
194 sender, subject = msg['From'], msg['Subject']
194 sender, subject = msg['From'], msg['Subject']
195 del msg['From'], msg['Subject']
195 del msg['From'], msg['Subject']
196
196
197 if not msg.is_multipart():
197 if not msg.is_multipart():
198 # create fresh mime message from scratch
198 # create fresh mime message from scratch
199 # (multipart templates must take care of this themselves)
199 # (multipart templates must take care of this themselves)
200 headers = msg.items()
200 headers = msg.items()
201 payload = msg.get_payload()
201 payload = msg.get_payload()
202 # for notification prefer readability over data precision
202 # for notification prefer readability over data precision
203 msg = mail.mimeencode(self.ui, payload, self.charsets, self.test)
203 msg = mail.mimeencode(self.ui, payload, self.charsets, self.test)
204 # reinstate custom headers
204 # reinstate custom headers
205 for k, v in headers:
205 for k, v in headers:
206 msg[k] = v
206 msg[k] = v
207
207
208 msg['Date'] = util.datestr(format="%a, %d %b %Y %H:%M:%S %1%2")
208 msg['Date'] = util.datestr(format="%a, %d %b %Y %H:%M:%S %1%2")
209
209
210 # try to make subject line exist and be useful
210 # try to make subject line exist and be useful
211 if not subject:
211 if not subject:
212 if count > 1:
212 if count > 1:
213 subject = _('%s: %d new changesets') % (self.root, count)
213 subject = _('%s: %d new changesets') % (self.root, count)
214 else:
214 else:
215 s = ctx.description().lstrip().split('\n', 1)[0].rstrip()
215 s = ctx.description().lstrip().split('\n', 1)[0].rstrip()
216 subject = '%s: %s' % (self.root, s)
216 subject = '%s: %s' % (self.root, s)
217 maxsubject = int(self.ui.config('notify', 'maxsubject', 67))
217 maxsubject = int(self.ui.config('notify', 'maxsubject', 67))
218 if maxsubject and len(subject) > maxsubject:
218 if maxsubject and len(subject) > maxsubject:
219 subject = subject[:maxsubject-3] + '...'
219 subject = subject[:maxsubject-3] + '...'
220 msg['Subject'] = mail.headencode(self.ui, subject,
220 msg['Subject'] = mail.headencode(self.ui, subject,
221 self.charsets, self.test)
221 self.charsets, self.test)
222
222
223 # try to make message have proper sender
223 # try to make message have proper sender
224 if not sender:
224 if not sender:
225 sender = self.ui.config('email', 'from') or self.ui.username()
225 sender = self.ui.config('email', 'from') or self.ui.username()
226 if '@' not in sender or '@localhost' in sender:
226 if '@' not in sender or '@localhost' in sender:
227 sender = self.fixmail(sender)
227 sender = self.fixmail(sender)
228 msg['From'] = mail.addressencode(self.ui, sender,
228 msg['From'] = mail.addressencode(self.ui, sender,
229 self.charsets, self.test)
229 self.charsets, self.test)
230
230
231 msg['X-Hg-Notification'] = 'changeset %s' % ctx
231 msg['X-Hg-Notification'] = 'changeset %s' % ctx
232 if not msg['Message-Id']:
232 if not msg['Message-Id']:
233 msg['Message-Id'] = ('<hg.%s.%s.%s@%s>' %
233 msg['Message-Id'] = ('<hg.%s.%s.%s@%s>' %
234 (ctx, int(time.time()),
234 (ctx, int(time.time()),
235 hash(self.repo.root), socket.getfqdn()))
235 hash(self.repo.root), socket.getfqdn()))
236 msg['To'] = ', '.join(self.subs)
236 msg['To'] = ', '.join(self.subs)
237
237
238 msgtext = msg.as_string()
238 msgtext = msg.as_string()
239 if self.test:
239 if self.test:
240 self.ui.write(msgtext)
240 self.ui.write(msgtext)
241 if not msgtext.endswith('\n'):
241 if not msgtext.endswith('\n'):
242 self.ui.write('\n')
242 self.ui.write('\n')
243 else:
243 else:
244 self.ui.status(_('notify: sending %d subscribers %d changes\n') %
244 self.ui.status(_('notify: sending %d subscribers %d changes\n') %
245 (len(self.subs), count))
245 (len(self.subs), count))
246 mail.sendmail(self.ui, util.email(msg['From']),
246 mail.sendmail(self.ui, util.email(msg['From']),
247 self.subs, msgtext)
247 self.subs, msgtext)
248
248
249 def diff(self, ctx, ref=None):
249 def diff(self, ctx, ref=None):
250
250
251 maxdiff = int(self.ui.config('notify', 'maxdiff', 300))
251 maxdiff = int(self.ui.config('notify', 'maxdiff', 300))
252 prev = ctx.parents()[0].node()
252 prev = ctx.parents()[0].node()
253 ref = ref and ref.node() or ctx.node()
253 ref = ref and ref.node() or ctx.node()
254 chunks = patch.diff(self.repo, prev, ref, opts=patch.diffopts(self.ui))
254 chunks = patch.diff(self.repo, prev, ref, opts=patch.diffopts(self.ui))
255 difflines = ''.join(chunks).splitlines()
255 difflines = ''.join(chunks).splitlines()
256
256
257 if self.ui.configbool('notify', 'diffstat', True):
257 if self.ui.configbool('notify', 'diffstat', True):
258 s = patch.diffstat(difflines)
258 s = patch.diffstat(difflines)
259 # s may be nil, don't include the header if it is
259 # s may be nil, don't include the header if it is
260 if s:
260 if s:
261 self.ui.write('\ndiffstat:\n\n%s' % s)
261 self.ui.write('\ndiffstat:\n\n%s' % s)
262
262
263 if maxdiff == 0:
263 if maxdiff == 0:
264 return
264 return
265 elif maxdiff > 0 and len(difflines) > maxdiff:
265 elif maxdiff > 0 and len(difflines) > maxdiff:
266 msg = _('\ndiffs (truncated from %d to %d lines):\n\n')
266 msg = _('\ndiffs (truncated from %d to %d lines):\n\n')
267 self.ui.write(msg % (len(difflines), maxdiff))
267 self.ui.write(msg % (len(difflines), maxdiff))
268 difflines = difflines[:maxdiff]
268 difflines = difflines[:maxdiff]
269 elif difflines:
269 elif difflines:
270 self.ui.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
270 self.ui.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
271
271
272 self.ui.write("\n".join(difflines))
272 self.ui.write("\n".join(difflines))
273
273
274 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
274 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
275 '''send email notifications to interested subscribers.
275 '''send email notifications to interested subscribers.
276
276
277 if used as changegroup hook, send one email for all changesets in
277 if used as changegroup hook, send one email for all changesets in
278 changegroup. else send one email per changeset.'''
278 changegroup. else send one email per changeset.'''
279
279
280 n = notifier(ui, repo, hooktype)
280 n = notifier(ui, repo, hooktype)
281 ctx = repo[node]
281 ctx = repo[node]
282
282
283 if not n.subs:
283 if not n.subs:
284 ui.debug('notify: no subscribers to repository %s\n' % n.root)
284 ui.debug('notify: no subscribers to repository %s\n' % n.root)
285 return
285 return
286 if n.skipsource(source):
286 if n.skipsource(source):
287 ui.debug('notify: changes have source "%s" - skipping\n' % source)
287 ui.debug('notify: changes have source "%s" - skipping\n' % source)
288 return
288 return
289
289
290 ui.pushbuffer()
290 ui.pushbuffer()
291 data = ''
291 data = ''
292 count = 0
292 count = 0
293 if hooktype == 'changegroup':
293 if hooktype == 'changegroup':
294 start, end = ctx.rev(), len(repo)
294 start, end = ctx.rev(), len(repo)
295 for rev in xrange(start, end):
295 for rev in xrange(start, end):
296 if n.node(repo[rev]):
296 if n.node(repo[rev]):
297 count += 1
297 count += 1
298 else:
298 else:
299 data += ui.popbuffer()
299 data += ui.popbuffer()
300 ui.note(_('notify: suppressing notification for merge %d:%s\n') %
300 ui.note(_('notify: suppressing notification for merge %d:%s\n') %
301 (rev, repo[rev].hex()[:12]))
301 (rev, repo[rev].hex()[:12]))
302 ui.pushbuffer()
302 ui.pushbuffer()
303 if count:
303 if count:
304 n.diff(ctx, repo['tip'])
304 n.diff(ctx, repo['tip'])
305 else:
305 else:
306 if not n.node(ctx):
306 if not n.node(ctx):
307 ui.popbuffer()
307 ui.popbuffer()
308 ui.note(_('notify: suppressing notification for merge %d:%s\n') %
308 ui.note(_('notify: suppressing notification for merge %d:%s\n') %
309 (ctx.rev(), ctx.hex()[:12]))
309 (ctx.rev(), ctx.hex()[:12]))
310 return
310 return
311 count += 1
311 count += 1
312 n.diff(ctx)
312 n.diff(ctx)
313
313
314 data += ui.popbuffer()
314 data += ui.popbuffer()
315 if count:
315 if count:
316 n.send(ctx, count, data)
316 n.send(ctx, count, data)
@@ -1,69 +1,69 b''
1 # pager.py - display output using a pager
1 # pager.py - display output using a pager
2 #
2 #
3 # Copyright 2008 David Soria Parra <dsp@php.net>
3 # Copyright 2008 David Soria Parra <dsp@php.net>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2, incorporated herein by reference.
7 #
7 #
8 # To load the extension, add it to your .hgrc file:
8 # To load the extension, add it to your .hgrc file:
9 #
9 #
10 # [extension]
10 # [extension]
11 # hgext.pager =
11 # pager =
12 #
12 #
13 # Run "hg help pager" to get info on configuration.
13 # Run "hg help pager" to get info on configuration.
14
14
15 '''browse command output with an external pager
15 '''browse command output with an external pager
16
16
17 To set the pager that should be used, set the application variable::
17 To set the pager that should be used, set the application variable::
18
18
19 [pager]
19 [pager]
20 pager = LESS='FSRX' less
20 pager = LESS='FSRX' less
21
21
22 If no pager is set, the pager extensions uses the environment variable
22 If no pager is set, the pager extensions uses the environment variable
23 $PAGER. If neither pager.pager, nor $PAGER is set, no pager is used.
23 $PAGER. If neither pager.pager, nor $PAGER is set, no pager is used.
24
24
25 If you notice "BROKEN PIPE" error messages, you can disable them by
25 If you notice "BROKEN PIPE" error messages, you can disable them by
26 setting::
26 setting::
27
27
28 [pager]
28 [pager]
29 quiet = True
29 quiet = True
30
30
31 You can disable the pager for certain commands by adding them to the
31 You can disable the pager for certain commands by adding them to the
32 pager.ignore list::
32 pager.ignore list::
33
33
34 [pager]
34 [pager]
35 ignore = version, help, update
35 ignore = version, help, update
36
36
37 You can also enable the pager only for certain commands using
37 You can also enable the pager only for certain commands using
38 pager.attend. Below is the default list of commands to be paged::
38 pager.attend. Below is the default list of commands to be paged::
39
39
40 [pager]
40 [pager]
41 attend = annotate, cat, diff, export, glog, log, qdiff
41 attend = annotate, cat, diff, export, glog, log, qdiff
42
42
43 Setting pager.attend to an empty value will cause all commands to be
43 Setting pager.attend to an empty value will cause all commands to be
44 paged.
44 paged.
45
45
46 If pager.attend is present, pager.ignore will be ignored.
46 If pager.attend is present, pager.ignore will be ignored.
47
47
48 To ignore global commands like "hg version" or "hg help", you have to
48 To ignore global commands like "hg version" or "hg help", you have to
49 specify them in the global .hgrc
49 specify them in the global .hgrc
50 '''
50 '''
51
51
52 import sys, os, signal
52 import sys, os, signal
53 from mercurial import dispatch, util, extensions
53 from mercurial import dispatch, util, extensions
54
54
55 def uisetup(ui):
55 def uisetup(ui):
56 def pagecmd(orig, ui, options, cmd, cmdfunc):
56 def pagecmd(orig, ui, options, cmd, cmdfunc):
57 p = ui.config("pager", "pager", os.environ.get("PAGER"))
57 p = ui.config("pager", "pager", os.environ.get("PAGER"))
58 if p and sys.stdout.isatty() and '--debugger' not in sys.argv:
58 if p and sys.stdout.isatty() and '--debugger' not in sys.argv:
59 attend = ui.configlist('pager', 'attend', attended)
59 attend = ui.configlist('pager', 'attend', attended)
60 if (cmd in attend or
60 if (cmd in attend or
61 (cmd not in ui.configlist('pager', 'ignore') and not attend)):
61 (cmd not in ui.configlist('pager', 'ignore') and not attend)):
62 sys.stderr = sys.stdout = util.popen(p, "wb")
62 sys.stderr = sys.stdout = util.popen(p, "wb")
63 if ui.configbool('pager', 'quiet'):
63 if ui.configbool('pager', 'quiet'):
64 signal.signal(signal.SIGPIPE, signal.SIG_DFL)
64 signal.signal(signal.SIGPIPE, signal.SIG_DFL)
65 return orig(ui, options, cmd, cmdfunc)
65 return orig(ui, options, cmd, cmdfunc)
66
66
67 extensions.wrapfunction(dispatch, '_runcommand', pagecmd)
67 extensions.wrapfunction(dispatch, '_runcommand', pagecmd)
68
68
69 attended = ['annotate', 'cat', 'diff', 'export', 'glog', 'log', 'qdiff']
69 attended = ['annotate', 'cat', 'diff', 'export', 'glog', 'log', 'qdiff']
@@ -1,158 +1,158 b''
1 # win32text.py - LF <-> CRLF/CR translation utilities for Windows/Mac users
1 # win32text.py - LF <-> CRLF/CR translation utilities for Windows/Mac users
2 #
2 #
3 # Copyright 2005, 2007-2009 Matt Mackall <mpm@selenic.com> and others
3 # Copyright 2005, 2007-2009 Matt Mackall <mpm@selenic.com> and others
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2, incorporated herein by reference.
6 # GNU General Public License version 2, incorporated herein by reference.
7
7
8 '''perform automatic newline conversion
8 '''perform automatic newline conversion
9
9
10 To perform automatic newline conversion, use::
10 To perform automatic newline conversion, use::
11
11
12 [extensions]
12 [extensions]
13 hgext.win32text =
13 win32text =
14 [encode]
14 [encode]
15 ** = cleverencode:
15 ** = cleverencode:
16 # or ** = macencode:
16 # or ** = macencode:
17
17
18 [decode]
18 [decode]
19 ** = cleverdecode:
19 ** = cleverdecode:
20 # or ** = macdecode:
20 # or ** = macdecode:
21
21
22 If not doing conversion, to make sure you do not commit CRLF/CR by accident::
22 If not doing conversion, to make sure you do not commit CRLF/CR by accident::
23
23
24 [hooks]
24 [hooks]
25 pretxncommit.crlf = python:hgext.win32text.forbidcrlf
25 pretxncommit.crlf = python:hgext.win32text.forbidcrlf
26 # or pretxncommit.cr = python:hgext.win32text.forbidcr
26 # or pretxncommit.cr = python:hgext.win32text.forbidcr
27
27
28 To do the same check on a server to prevent CRLF/CR from being
28 To do the same check on a server to prevent CRLF/CR from being
29 pushed or pulled::
29 pushed or pulled::
30
30
31 [hooks]
31 [hooks]
32 pretxnchangegroup.crlf = python:hgext.win32text.forbidcrlf
32 pretxnchangegroup.crlf = python:hgext.win32text.forbidcrlf
33 # or pretxnchangegroup.cr = python:hgext.win32text.forbidcr
33 # or pretxnchangegroup.cr = python:hgext.win32text.forbidcr
34 '''
34 '''
35
35
36 from mercurial.i18n import _
36 from mercurial.i18n import _
37 from mercurial.node import short
37 from mercurial.node import short
38 from mercurial import util
38 from mercurial import util
39 import re
39 import re
40
40
41 # regexp for single LF without CR preceding.
41 # regexp for single LF without CR preceding.
42 re_single_lf = re.compile('(^|[^\r])\n', re.MULTILINE)
42 re_single_lf = re.compile('(^|[^\r])\n', re.MULTILINE)
43
43
44 newlinestr = {'\r\n': 'CRLF', '\r': 'CR'}
44 newlinestr = {'\r\n': 'CRLF', '\r': 'CR'}
45 filterstr = {'\r\n': 'clever', '\r': 'mac'}
45 filterstr = {'\r\n': 'clever', '\r': 'mac'}
46
46
47 def checknewline(s, newline, ui=None, repo=None, filename=None):
47 def checknewline(s, newline, ui=None, repo=None, filename=None):
48 # warn if already has 'newline' in repository.
48 # warn if already has 'newline' in repository.
49 # it might cause unexpected eol conversion.
49 # it might cause unexpected eol conversion.
50 # see issue 302:
50 # see issue 302:
51 # http://mercurial.selenic.com/bts/issue302
51 # http://mercurial.selenic.com/bts/issue302
52 if newline in s and ui and filename and repo:
52 if newline in s and ui and filename and repo:
53 ui.warn(_('WARNING: %s already has %s line endings\n'
53 ui.warn(_('WARNING: %s already has %s line endings\n'
54 'and does not need EOL conversion by the win32text plugin.\n'
54 'and does not need EOL conversion by the win32text plugin.\n'
55 'Before your next commit, please reconsider your '
55 'Before your next commit, please reconsider your '
56 'encode/decode settings in \nMercurial.ini or %s.\n') %
56 'encode/decode settings in \nMercurial.ini or %s.\n') %
57 (filename, newlinestr[newline], repo.join('hgrc')))
57 (filename, newlinestr[newline], repo.join('hgrc')))
58
58
59 def dumbdecode(s, cmd, **kwargs):
59 def dumbdecode(s, cmd, **kwargs):
60 checknewline(s, '\r\n', **kwargs)
60 checknewline(s, '\r\n', **kwargs)
61 # replace single LF to CRLF
61 # replace single LF to CRLF
62 return re_single_lf.sub('\\1\r\n', s)
62 return re_single_lf.sub('\\1\r\n', s)
63
63
64 def dumbencode(s, cmd):
64 def dumbencode(s, cmd):
65 return s.replace('\r\n', '\n')
65 return s.replace('\r\n', '\n')
66
66
67 def macdumbdecode(s, cmd, **kwargs):
67 def macdumbdecode(s, cmd, **kwargs):
68 checknewline(s, '\r', **kwargs)
68 checknewline(s, '\r', **kwargs)
69 return s.replace('\n', '\r')
69 return s.replace('\n', '\r')
70
70
71 def macdumbencode(s, cmd):
71 def macdumbencode(s, cmd):
72 return s.replace('\r', '\n')
72 return s.replace('\r', '\n')
73
73
74 def cleverdecode(s, cmd, **kwargs):
74 def cleverdecode(s, cmd, **kwargs):
75 if not util.binary(s):
75 if not util.binary(s):
76 return dumbdecode(s, cmd, **kwargs)
76 return dumbdecode(s, cmd, **kwargs)
77 return s
77 return s
78
78
79 def cleverencode(s, cmd):
79 def cleverencode(s, cmd):
80 if not util.binary(s):
80 if not util.binary(s):
81 return dumbencode(s, cmd)
81 return dumbencode(s, cmd)
82 return s
82 return s
83
83
84 def macdecode(s, cmd, **kwargs):
84 def macdecode(s, cmd, **kwargs):
85 if not util.binary(s):
85 if not util.binary(s):
86 return macdumbdecode(s, cmd, **kwargs)
86 return macdumbdecode(s, cmd, **kwargs)
87 return s
87 return s
88
88
89 def macencode(s, cmd):
89 def macencode(s, cmd):
90 if not util.binary(s):
90 if not util.binary(s):
91 return macdumbencode(s, cmd)
91 return macdumbencode(s, cmd)
92 return s
92 return s
93
93
94 _filters = {
94 _filters = {
95 'dumbdecode:': dumbdecode,
95 'dumbdecode:': dumbdecode,
96 'dumbencode:': dumbencode,
96 'dumbencode:': dumbencode,
97 'cleverdecode:': cleverdecode,
97 'cleverdecode:': cleverdecode,
98 'cleverencode:': cleverencode,
98 'cleverencode:': cleverencode,
99 'macdumbdecode:': macdumbdecode,
99 'macdumbdecode:': macdumbdecode,
100 'macdumbencode:': macdumbencode,
100 'macdumbencode:': macdumbencode,
101 'macdecode:': macdecode,
101 'macdecode:': macdecode,
102 'macencode:': macencode,
102 'macencode:': macencode,
103 }
103 }
104
104
105 def forbidnewline(ui, repo, hooktype, node, newline, **kwargs):
105 def forbidnewline(ui, repo, hooktype, node, newline, **kwargs):
106 halt = False
106 halt = False
107 seen = set()
107 seen = set()
108 # we try to walk changesets in reverse order from newest to
108 # we try to walk changesets in reverse order from newest to
109 # oldest, so that if we see a file multiple times, we take the
109 # oldest, so that if we see a file multiple times, we take the
110 # newest version as canonical. this prevents us from blocking a
110 # newest version as canonical. this prevents us from blocking a
111 # changegroup that contains an unacceptable commit followed later
111 # changegroup that contains an unacceptable commit followed later
112 # by a commit that fixes the problem.
112 # by a commit that fixes the problem.
113 tip = repo['tip']
113 tip = repo['tip']
114 for rev in xrange(len(repo)-1, repo[node].rev()-1, -1):
114 for rev in xrange(len(repo)-1, repo[node].rev()-1, -1):
115 c = repo[rev]
115 c = repo[rev]
116 for f in c.files():
116 for f in c.files():
117 if f in seen or f not in tip or f not in c:
117 if f in seen or f not in tip or f not in c:
118 continue
118 continue
119 seen.add(f)
119 seen.add(f)
120 data = c[f].data()
120 data = c[f].data()
121 if not util.binary(data) and newline in data:
121 if not util.binary(data) and newline in data:
122 if not halt:
122 if not halt:
123 ui.warn(_('Attempt to commit or push text file(s) '
123 ui.warn(_('Attempt to commit or push text file(s) '
124 'using %s line endings\n') %
124 'using %s line endings\n') %
125 newlinestr[newline])
125 newlinestr[newline])
126 ui.warn(_('in %s: %s\n') % (short(c.node()), f))
126 ui.warn(_('in %s: %s\n') % (short(c.node()), f))
127 halt = True
127 halt = True
128 if halt and hooktype == 'pretxnchangegroup':
128 if halt and hooktype == 'pretxnchangegroup':
129 crlf = newlinestr[newline].lower()
129 crlf = newlinestr[newline].lower()
130 filter = filterstr[newline]
130 filter = filterstr[newline]
131 ui.warn(_('\nTo prevent this mistake in your local repository,\n'
131 ui.warn(_('\nTo prevent this mistake in your local repository,\n'
132 'add to Mercurial.ini or .hg/hgrc:\n'
132 'add to Mercurial.ini or .hg/hgrc:\n'
133 '\n'
133 '\n'
134 '[hooks]\n'
134 '[hooks]\n'
135 'pretxncommit.%s = python:hgext.win32text.forbid%s\n'
135 'pretxncommit.%s = python:hgext.win32text.forbid%s\n'
136 '\n'
136 '\n'
137 'and also consider adding:\n'
137 'and also consider adding:\n'
138 '\n'
138 '\n'
139 '[extensions]\n'
139 '[extensions]\n'
140 'hgext.win32text =\n'
140 'hgext.win32text =\n'
141 '[encode]\n'
141 '[encode]\n'
142 '** = %sencode:\n'
142 '** = %sencode:\n'
143 '[decode]\n'
143 '[decode]\n'
144 '** = %sdecode:\n') % (crlf, crlf, filter, filter))
144 '** = %sdecode:\n') % (crlf, crlf, filter, filter))
145 return halt
145 return halt
146
146
147 def forbidcrlf(ui, repo, hooktype, node, **kwargs):
147 def forbidcrlf(ui, repo, hooktype, node, **kwargs):
148 return forbidnewline(ui, repo, hooktype, node, '\r\n', **kwargs)
148 return forbidnewline(ui, repo, hooktype, node, '\r\n', **kwargs)
149
149
150 def forbidcr(ui, repo, hooktype, node, **kwargs):
150 def forbidcr(ui, repo, hooktype, node, **kwargs):
151 return forbidnewline(ui, repo, hooktype, node, '\r', **kwargs)
151 return forbidnewline(ui, repo, hooktype, node, '\r', **kwargs)
152
152
153 def reposetup(ui, repo):
153 def reposetup(ui, repo):
154 if not repo.local():
154 if not repo.local():
155 return
155 return
156 for name, fn in _filters.iteritems():
156 for name, fn in _filters.iteritems():
157 repo.adddatafilter(name, fn)
157 repo.adddatafilter(name, fn)
158
158
@@ -1,219 +1,219 b''
1 notify extension - hooks for sending email notifications at commit/push time
1 notify extension - hooks for sending email notifications at commit/push time
2
2
3 Subscriptions can be managed through a hgrc file. Default mode is to print
3 Subscriptions can be managed through a hgrc file. Default mode is to print
4 messages to stdout, for testing and configuring.
4 messages to stdout, for testing and configuring.
5
5
6 To use, configure the notify extension and enable it in hgrc like this:
6 To use, configure the notify extension and enable it in hgrc like this:
7
7
8 [extensions]
8 [extensions]
9 hgext.notify =
9 notify =
10
10
11 [hooks]
11 [hooks]
12 # one email for each incoming changeset
12 # one email for each incoming changeset
13 incoming.notify = python:hgext.notify.hook
13 incoming.notify = python:hgext.notify.hook
14 # batch emails when many changesets incoming at one time
14 # batch emails when many changesets incoming at one time
15 changegroup.notify = python:hgext.notify.hook
15 changegroup.notify = python:hgext.notify.hook
16
16
17 [notify]
17 [notify]
18 # config items go here
18 # config items go here
19
19
20 Required configuration items:
20 Required configuration items:
21
21
22 config = /path/to/file # file containing subscriptions
22 config = /path/to/file # file containing subscriptions
23
23
24 Optional configuration items:
24 Optional configuration items:
25
25
26 test = True # print messages to stdout for testing
26 test = True # print messages to stdout for testing
27 strip = 3 # number of slashes to strip for url paths
27 strip = 3 # number of slashes to strip for url paths
28 domain = example.com # domain to use if committer missing domain
28 domain = example.com # domain to use if committer missing domain
29 style = ... # style file to use when formatting email
29 style = ... # style file to use when formatting email
30 template = ... # template to use when formatting email
30 template = ... # template to use when formatting email
31 incoming = ... # template to use when run as incoming hook
31 incoming = ... # template to use when run as incoming hook
32 changegroup = ... # template when run as changegroup hook
32 changegroup = ... # template when run as changegroup hook
33 maxdiff = 300 # max lines of diffs to include (0=none, -1=all)
33 maxdiff = 300 # max lines of diffs to include (0=none, -1=all)
34 maxsubject = 67 # truncate subject line longer than this
34 maxsubject = 67 # truncate subject line longer than this
35 diffstat = True # add a diffstat before the diff content
35 diffstat = True # add a diffstat before the diff content
36 sources = serve # notify if source of incoming changes in this list
36 sources = serve # notify if source of incoming changes in this list
37 # (serve == ssh or http, push, pull, bundle)
37 # (serve == ssh or http, push, pull, bundle)
38 merge = False # send notification for merges (default True)
38 merge = False # send notification for merges (default True)
39 [email]
39 [email]
40 from = user@host.com # email address to send as if none given
40 from = user@host.com # email address to send as if none given
41 [web]
41 [web]
42 baseurl = http://hgserver/... # root of hg web site for browsing commits
42 baseurl = http://hgserver/... # root of hg web site for browsing commits
43
43
44 The notify config file has same format as a regular hgrc file. It has two
44 The notify config file has same format as a regular hgrc file. It has two
45 sections so you can express subscriptions in whatever way is handier for you.
45 sections so you can express subscriptions in whatever way is handier for you.
46
46
47 [usersubs]
47 [usersubs]
48 # key is subscriber email, value is ","-separated list of glob patterns
48 # key is subscriber email, value is ","-separated list of glob patterns
49 user@host = pattern
49 user@host = pattern
50
50
51 [reposubs]
51 [reposubs]
52 # key is glob pattern, value is ","-separated list of subscriber emails
52 # key is glob pattern, value is ","-separated list of subscriber emails
53 pattern = user@host
53 pattern = user@host
54
54
55 Glob patterns are matched against path to repository root.
55 Glob patterns are matched against path to repository root.
56
56
57 If you like, you can put notify config file in repository that users can push
57 If you like, you can put notify config file in repository that users can push
58 changes to, they can manage their own subscriptions.
58 changes to, they can manage their own subscriptions.
59
59
60 no commands defined
60 no commands defined
61 % commit
61 % commit
62 adding a
62 adding a
63 % clone
63 % clone
64 updating to branch default
64 updating to branch default
65 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
65 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
66 % commit
66 % commit
67 % pull (minimal config)
67 % pull (minimal config)
68 pulling from ../a
68 pulling from ../a
69 searching for changes
69 searching for changes
70 adding changesets
70 adding changesets
71 adding manifests
71 adding manifests
72 adding file changes
72 adding file changes
73 added 1 changesets with 1 changes to 1 files
73 added 1 changesets with 1 changes to 1 files
74 Content-Type: text/plain; charset="us-ascii"
74 Content-Type: text/plain; charset="us-ascii"
75 MIME-Version: 1.0
75 MIME-Version: 1.0
76 Content-Transfer-Encoding: 7bit
76 Content-Transfer-Encoding: 7bit
77 Date:
77 Date:
78 Subject: changeset in test-notify/b: b
78 Subject: changeset in test-notify/b: b
79 From: test
79 From: test
80 X-Hg-Notification: changeset 0647d048b600
80 X-Hg-Notification: changeset 0647d048b600
81 Message-Id:
81 Message-Id:
82 To: baz, foo@bar
82 To: baz, foo@bar
83
83
84 changeset 0647d048b600 in test-notify/b
84 changeset 0647d048b600 in test-notify/b
85 details: test-notify/b?cmd=changeset;node=0647d048b600
85 details: test-notify/b?cmd=changeset;node=0647d048b600
86 description: b
86 description: b
87
87
88 diffs (6 lines):
88 diffs (6 lines):
89
89
90 diff -r cb9a9f314b8b -r 0647d048b600 a
90 diff -r cb9a9f314b8b -r 0647d048b600 a
91 --- a/a Thu Jan 01 00:00:00 1970 +0000
91 --- a/a Thu Jan 01 00:00:00 1970 +0000
92 +++ b/a Thu Jan 01 00:00:01 1970 +0000
92 +++ b/a Thu Jan 01 00:00:01 1970 +0000
93 @@ -1,1 +1,2 @@
93 @@ -1,1 +1,2 @@
94 a
94 a
95 +a
95 +a
96 (run 'hg update' to get a working copy)
96 (run 'hg update' to get a working copy)
97 % fail for config file is missing
97 % fail for config file is missing
98 rolling back last transaction
98 rolling back last transaction
99 pull failed
99 pull failed
100 % pull
100 % pull
101 rolling back last transaction
101 rolling back last transaction
102 pulling from ../a
102 pulling from ../a
103 searching for changes
103 searching for changes
104 adding changesets
104 adding changesets
105 adding manifests
105 adding manifests
106 adding file changes
106 adding file changes
107 added 1 changesets with 1 changes to 1 files
107 added 1 changesets with 1 changes to 1 files
108 Content-Type: text/plain; charset="us-ascii"
108 Content-Type: text/plain; charset="us-ascii"
109 MIME-Version: 1.0
109 MIME-Version: 1.0
110 Content-Transfer-Encoding: 7bit
110 Content-Transfer-Encoding: 7bit
111 X-Test: foo
111 X-Test: foo
112 Date:
112 Date:
113 Subject: b
113 Subject: b
114 From: test@test.com
114 From: test@test.com
115 X-Hg-Notification: changeset 0647d048b600
115 X-Hg-Notification: changeset 0647d048b600
116 Message-Id:
116 Message-Id:
117 To: baz@test.com, foo@bar
117 To: baz@test.com, foo@bar
118
118
119 changeset 0647d048b600
119 changeset 0647d048b600
120 description:
120 description:
121 b
121 b
122 diffs (6 lines):
122 diffs (6 lines):
123
123
124 diff -r cb9a9f314b8b -r 0647d048b600 a
124 diff -r cb9a9f314b8b -r 0647d048b600 a
125 --- a/a Thu Jan 01 00:00:00 1970 +0000
125 --- a/a Thu Jan 01 00:00:00 1970 +0000
126 +++ b/a Thu Jan 01 00:00:01 1970 +0000
126 +++ b/a Thu Jan 01 00:00:01 1970 +0000
127 @@ -1,1 +1,2 @@
127 @@ -1,1 +1,2 @@
128 a
128 a
129 +a
129 +a
130 (run 'hg update' to get a working copy)
130 (run 'hg update' to get a working copy)
131 % pull
131 % pull
132 rolling back last transaction
132 rolling back last transaction
133 pulling from ../a
133 pulling from ../a
134 searching for changes
134 searching for changes
135 adding changesets
135 adding changesets
136 adding manifests
136 adding manifests
137 adding file changes
137 adding file changes
138 added 1 changesets with 1 changes to 1 files
138 added 1 changesets with 1 changes to 1 files
139 Content-Type: text/plain; charset="us-ascii"
139 Content-Type: text/plain; charset="us-ascii"
140 MIME-Version: 1.0
140 MIME-Version: 1.0
141 Content-Transfer-Encoding: 7bit
141 Content-Transfer-Encoding: 7bit
142 X-Test: foo
142 X-Test: foo
143 Date:
143 Date:
144 Subject: b
144 Subject: b
145 From: test@test.com
145 From: test@test.com
146 X-Hg-Notification: changeset 0647d048b600
146 X-Hg-Notification: changeset 0647d048b600
147 Message-Id:
147 Message-Id:
148 To: baz@test.com, foo@bar
148 To: baz@test.com, foo@bar
149
149
150 changeset 0647d048b600
150 changeset 0647d048b600
151 description:
151 description:
152 b
152 b
153 diffstat:
153 diffstat:
154
154
155 a | 1 +
155 a | 1 +
156 1 files changed, 1 insertions(+), 0 deletions(-)
156 1 files changed, 1 insertions(+), 0 deletions(-)
157
157
158 diffs (6 lines):
158 diffs (6 lines):
159
159
160 diff -r cb9a9f314b8b -r 0647d048b600 a
160 diff -r cb9a9f314b8b -r 0647d048b600 a
161 --- a/a Thu Jan 01 00:00:00 1970 +0000
161 --- a/a Thu Jan 01 00:00:00 1970 +0000
162 +++ b/a Thu Jan 01 00:00:01 1970 +0000
162 +++ b/a Thu Jan 01 00:00:01 1970 +0000
163 @@ -1,1 +1,2 @@
163 @@ -1,1 +1,2 @@
164 a
164 a
165 +a
165 +a
166 (run 'hg update' to get a working copy)
166 (run 'hg update' to get a working copy)
167 % test merge
167 % test merge
168 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
168 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
169 created new head
169 created new head
170 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
170 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
171 (branch merge, don't forget to commit)
171 (branch merge, don't forget to commit)
172 pulling from ../a
172 pulling from ../a
173 searching for changes
173 searching for changes
174 adding changesets
174 adding changesets
175 adding manifests
175 adding manifests
176 adding file changes
176 adding file changes
177 added 2 changesets with 0 changes to 0 files
177 added 2 changesets with 0 changes to 0 files
178 Content-Type: text/plain; charset="us-ascii"
178 Content-Type: text/plain; charset="us-ascii"
179 MIME-Version: 1.0
179 MIME-Version: 1.0
180 Content-Transfer-Encoding: 7bit
180 Content-Transfer-Encoding: 7bit
181 X-Test: foo
181 X-Test: foo
182 Date:
182 Date:
183 Subject: adda2
183 Subject: adda2
184 From: test@test.com
184 From: test@test.com
185 X-Hg-Notification: changeset 0a184ce6067f
185 X-Hg-Notification: changeset 0a184ce6067f
186 Message-Id:
186 Message-Id:
187 To: baz@test.com, foo@bar
187 To: baz@test.com, foo@bar
188
188
189 changeset 0a184ce6067f
189 changeset 0a184ce6067f
190 description:
190 description:
191 adda2
191 adda2
192 diffstat:
192 diffstat:
193
193
194 a | 1 +
194 a | 1 +
195 1 files changed, 1 insertions(+), 0 deletions(-)
195 1 files changed, 1 insertions(+), 0 deletions(-)
196
196
197 diffs (6 lines):
197 diffs (6 lines):
198
198
199 diff -r cb9a9f314b8b -r 0a184ce6067f a
199 diff -r cb9a9f314b8b -r 0a184ce6067f a
200 --- a/a Thu Jan 01 00:00:00 1970 +0000
200 --- a/a Thu Jan 01 00:00:00 1970 +0000
201 +++ b/a Thu Jan 01 00:00:02 1970 +0000
201 +++ b/a Thu Jan 01 00:00:02 1970 +0000
202 @@ -1,1 +1,2 @@
202 @@ -1,1 +1,2 @@
203 a
203 a
204 +a
204 +a
205 Content-Type: text/plain; charset="us-ascii"
205 Content-Type: text/plain; charset="us-ascii"
206 MIME-Version: 1.0
206 MIME-Version: 1.0
207 Content-Transfer-Encoding: 7bit
207 Content-Transfer-Encoding: 7bit
208 X-Test: foo
208 X-Test: foo
209 Date:
209 Date:
210 Subject: merge
210 Subject: merge
211 From: test@test.com
211 From: test@test.com
212 X-Hg-Notification: changeset 22c88b85aa27
212 X-Hg-Notification: changeset 22c88b85aa27
213 Message-Id:
213 Message-Id:
214 To: baz@test.com, foo@bar
214 To: baz@test.com, foo@bar
215
215
216 changeset 22c88b85aa27
216 changeset 22c88b85aa27
217 description:
217 description:
218 merge
218 merge
219 (run 'hg update' to get a working copy)
219 (run 'hg update' to get a working copy)
General Comments 0
You need to be logged in to leave comments. Login now