##// END OF EJS Templates
extensions: change descriptions for hook-providing extensions...
Dirkjan Ochtman -
r8935:f4f0e902 default
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 '''control access to a repository using simple hooks
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 hgext.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
38 The allow and deny sections take a subtree pattern as key (with a
39 glob syntax by default), and a comma separated list of users as
39 glob syntax by default), and a comma separated list of users as
40 the corresponding value. The deny list is checked before the allow
40 the corresponding value. The deny list is checked before the allow
41 list is.
41 list 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[2])
87 user = urllib.unquote(url[2])
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,416 +1,416 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 '''integrate Mercurial with a 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 Configuring the extension:
24 Configuring the extension:
25
25
26 [bugzilla]
26 [bugzilla]
27
27
28 host Hostname of the MySQL server holding the Bugzilla
28 host Hostname of the MySQL server holding the Bugzilla
29 database.
29 database.
30 db Name of the Bugzilla database in MySQL. Default 'bugs'.
30 db Name of the Bugzilla database in MySQL. Default 'bugs'.
31 user Username to use to access MySQL server. Default 'bugs'.
31 user Username to use to access MySQL server. Default 'bugs'.
32 password Password to use to access MySQL server.
32 password Password to use to access MySQL server.
33 timeout Database connection timeout (seconds). Default 5.
33 timeout Database connection timeout (seconds). Default 5.
34 version Bugzilla version. Specify '3.0' for Bugzilla versions
34 version Bugzilla version. Specify '3.0' for Bugzilla versions
35 3.0 and later, '2.18' for Bugzilla versions from 2.18
35 3.0 and later, '2.18' for Bugzilla versions from 2.18
36 and '2.16' for versions prior to 2.18.
36 and '2.16' for versions prior to 2.18.
37 bzuser Fallback Bugzilla user name to record comments with, if
37 bzuser Fallback Bugzilla user name to record comments with, if
38 changeset committer cannot be found as a Bugzilla user.
38 changeset committer cannot be found as a Bugzilla user.
39 bzdir Bugzilla install directory. Used by default notify.
39 bzdir Bugzilla install directory. Used by default notify.
40 Default '/var/www/html/bugzilla'.
40 Default '/var/www/html/bugzilla'.
41 notify The command to run to get Bugzilla to send bug change
41 notify The command to run to get Bugzilla to send bug change
42 notification emails. Substitutes from a map with 3
42 notification emails. Substitutes from a map with 3
43 keys, 'bzdir', 'id' (bug id) and 'user' (committer
43 keys, 'bzdir', 'id' (bug id) and 'user' (committer
44 bugzilla email). Default depends on version; from 2.18
44 bugzilla email). Default depends on version; from 2.18
45 it is "cd %(bzdir)s && perl -T contrib/sendbugmail.pl
45 it is "cd %(bzdir)s && perl -T contrib/sendbugmail.pl
46 %(id)s %(user)s".
46 %(id)s %(user)s".
47 regexp Regular expression to match bug IDs in changeset commit
47 regexp Regular expression to match bug IDs in changeset commit
48 message. Must contain one "()" group. The default
48 message. Must contain one "()" group. The default
49 expression matches 'Bug 1234', 'Bug no. 1234', 'Bug
49 expression matches 'Bug 1234', 'Bug no. 1234', 'Bug
50 number 1234', 'Bugs 1234,5678', 'Bug 1234 and 5678' and
50 number 1234', 'Bugs 1234,5678', 'Bug 1234 and 5678' and
51 variations thereof. Matching is case insensitive.
51 variations thereof. Matching is case insensitive.
52 style The style file to use when formatting comments.
52 style The style file to use when formatting comments.
53 template Template to use when formatting comments. Overrides
53 template Template to use when formatting comments. Overrides
54 style if specified. In addition to the usual Mercurial
54 style if specified. In addition to the usual Mercurial
55 keywords, the extension specifies:
55 keywords, the extension specifies:
56 {bug} The Bugzilla bug ID.
56 {bug} The Bugzilla bug ID.
57 {root} The full pathname of the Mercurial
57 {root} The full pathname of the Mercurial
58 repository.
58 repository.
59 {webroot} Stripped pathname of the Mercurial
59 {webroot} Stripped pathname of the Mercurial
60 repository.
60 repository.
61 {hgweb} Base URL for browsing Mercurial
61 {hgweb} Base URL for browsing Mercurial
62 repositories.
62 repositories.
63 Default 'changeset {node|short} in repo {root} refers '
63 Default 'changeset {node|short} in repo {root} refers '
64 'to bug {bug}.\\ndetails:\\n\\t{desc|tabindent}'
64 'to bug {bug}.\\ndetails:\\n\\t{desc|tabindent}'
65 strip The number of slashes to strip from the front of {root}
65 strip The number of slashes to strip from the front of {root}
66 to produce {webroot}. Default 0.
66 to produce {webroot}. Default 0.
67 usermap Path of file containing Mercurial committer ID to
67 usermap Path of file containing Mercurial committer ID to
68 Bugzilla user ID mappings. If specified, the file
68 Bugzilla user ID mappings. If specified, the file
69 should contain one mapping per line,
69 should contain one mapping per line,
70 "committer"="Bugzilla user". See also the [usermap]
70 "committer"="Bugzilla user". See also the [usermap]
71 section.
71 section.
72
72
73 [usermap]
73 [usermap]
74 Any entries in this section specify mappings of Mercurial
74 Any entries in this section specify mappings of Mercurial
75 committer ID to Bugzilla user ID. See also [bugzilla].usermap.
75 committer ID to Bugzilla user ID. See also [bugzilla].usermap.
76 "committer"="Bugzilla user"
76 "committer"="Bugzilla user"
77
77
78 [web]
78 [web]
79 baseurl Base URL for browsing Mercurial repositories. Reference
79 baseurl Base URL for browsing Mercurial repositories. Reference
80 from templates as {hgweb}.
80 from templates as {hgweb}.
81
81
82 Activating the extension:
82 Activating the extension:
83
83
84 [extensions]
84 [extensions]
85 hgext.bugzilla =
85 hgext.bugzilla =
86
86
87 [hooks]
87 [hooks]
88 # run bugzilla hook on every change pulled or pushed in here
88 # run bugzilla hook on every change pulled or pushed in here
89 incoming.bugzilla = python:hgext.bugzilla.hook
89 incoming.bugzilla = python:hgext.bugzilla.hook
90
90
91 Example configuration:
91 Example configuration:
92
92
93 This example configuration is for a collection of Mercurial
93 This example configuration is for a collection of Mercurial
94 repositories in /var/local/hg/repos/ used with a local Bugzilla 3.2
94 repositories in /var/local/hg/repos/ used with a local Bugzilla 3.2
95 installation in /opt/bugzilla-3.2.
95 installation in /opt/bugzilla-3.2.
96
96
97 [bugzilla]
97 [bugzilla]
98 host=localhost
98 host=localhost
99 password=XYZZY
99 password=XYZZY
100 version=3.0
100 version=3.0
101 bzuser=unknown@domain.com
101 bzuser=unknown@domain.com
102 bzdir=/opt/bugzilla-3.2
102 bzdir=/opt/bugzilla-3.2
103 template=Changeset {node|short} in {root|basename}.\\n{hgweb}/{webroot}/rev/{node|short}\\n\\n{desc}\\n
103 template=Changeset {node|short} in {root|basename}.\\n{hgweb}/{webroot}/rev/{node|short}\\n\\n{desc}\\n
104 strip=5
104 strip=5
105
105
106 [web]
106 [web]
107 baseurl=http://dev.domain.com/hg
107 baseurl=http://dev.domain.com/hg
108
108
109 [usermap]
109 [usermap]
110 user@emaildomain.com=user.name@bugzilladomain.com
110 user@emaildomain.com=user.name@bugzilladomain.com
111
111
112 Commits add a comment to the Bugzilla bug record of the form:
112 Commits add a comment to the Bugzilla bug record of the form:
113
113
114 Changeset 3b16791d6642 in repository-name.
114 Changeset 3b16791d6642 in repository-name.
115 http://dev.domain.com/hg/repository-name/rev/3b16791d6642
115 http://dev.domain.com/hg/repository-name/rev/3b16791d6642
116
116
117 Changeset commit comment. Bug 1234.
117 Changeset commit comment. Bug 1234.
118 '''
118 '''
119
119
120 from mercurial.i18n import _
120 from mercurial.i18n import _
121 from mercurial.node import short
121 from mercurial.node import short
122 from mercurial import cmdutil, templater, util
122 from mercurial import cmdutil, templater, util
123 import re, time
123 import re, time
124
124
125 MySQLdb = None
125 MySQLdb = None
126
126
127 def buglist(ids):
127 def buglist(ids):
128 return '(' + ','.join(map(str, ids)) + ')'
128 return '(' + ','.join(map(str, ids)) + ')'
129
129
130 class bugzilla_2_16(object):
130 class bugzilla_2_16(object):
131 '''support for bugzilla version 2.16.'''
131 '''support for bugzilla version 2.16.'''
132
132
133 def __init__(self, ui):
133 def __init__(self, ui):
134 self.ui = ui
134 self.ui = ui
135 host = self.ui.config('bugzilla', 'host', 'localhost')
135 host = self.ui.config('bugzilla', 'host', 'localhost')
136 user = self.ui.config('bugzilla', 'user', 'bugs')
136 user = self.ui.config('bugzilla', 'user', 'bugs')
137 passwd = self.ui.config('bugzilla', 'password')
137 passwd = self.ui.config('bugzilla', 'password')
138 db = self.ui.config('bugzilla', 'db', 'bugs')
138 db = self.ui.config('bugzilla', 'db', 'bugs')
139 timeout = int(self.ui.config('bugzilla', 'timeout', 5))
139 timeout = int(self.ui.config('bugzilla', 'timeout', 5))
140 usermap = self.ui.config('bugzilla', 'usermap')
140 usermap = self.ui.config('bugzilla', 'usermap')
141 if usermap:
141 if usermap:
142 self.ui.readconfig(usermap, sections=['usermap'])
142 self.ui.readconfig(usermap, sections=['usermap'])
143 self.ui.note(_('connecting to %s:%s as %s, password %s\n') %
143 self.ui.note(_('connecting to %s:%s as %s, password %s\n') %
144 (host, db, user, '*' * len(passwd)))
144 (host, db, user, '*' * len(passwd)))
145 self.conn = MySQLdb.connect(host=host, user=user, passwd=passwd,
145 self.conn = MySQLdb.connect(host=host, user=user, passwd=passwd,
146 db=db, connect_timeout=timeout)
146 db=db, connect_timeout=timeout)
147 self.cursor = self.conn.cursor()
147 self.cursor = self.conn.cursor()
148 self.longdesc_id = self.get_longdesc_id()
148 self.longdesc_id = self.get_longdesc_id()
149 self.user_ids = {}
149 self.user_ids = {}
150 self.default_notify = "cd %(bzdir)s && ./processmail %(id)s %(user)s"
150 self.default_notify = "cd %(bzdir)s && ./processmail %(id)s %(user)s"
151
151
152 def run(self, *args, **kwargs):
152 def run(self, *args, **kwargs):
153 '''run a query.'''
153 '''run a query.'''
154 self.ui.note(_('query: %s %s\n') % (args, kwargs))
154 self.ui.note(_('query: %s %s\n') % (args, kwargs))
155 try:
155 try:
156 self.cursor.execute(*args, **kwargs)
156 self.cursor.execute(*args, **kwargs)
157 except MySQLdb.MySQLError:
157 except MySQLdb.MySQLError:
158 self.ui.note(_('failed query: %s %s\n') % (args, kwargs))
158 self.ui.note(_('failed query: %s %s\n') % (args, kwargs))
159 raise
159 raise
160
160
161 def get_longdesc_id(self):
161 def get_longdesc_id(self):
162 '''get identity of longdesc field'''
162 '''get identity of longdesc field'''
163 self.run('select fieldid from fielddefs where name = "longdesc"')
163 self.run('select fieldid from fielddefs where name = "longdesc"')
164 ids = self.cursor.fetchall()
164 ids = self.cursor.fetchall()
165 if len(ids) != 1:
165 if len(ids) != 1:
166 raise util.Abort(_('unknown database schema'))
166 raise util.Abort(_('unknown database schema'))
167 return ids[0][0]
167 return ids[0][0]
168
168
169 def filter_real_bug_ids(self, ids):
169 def filter_real_bug_ids(self, ids):
170 '''filter not-existing bug ids from list.'''
170 '''filter not-existing bug ids from list.'''
171 self.run('select bug_id from bugs where bug_id in %s' % buglist(ids))
171 self.run('select bug_id from bugs where bug_id in %s' % buglist(ids))
172 return sorted([c[0] for c in self.cursor.fetchall()])
172 return sorted([c[0] for c in self.cursor.fetchall()])
173
173
174 def filter_unknown_bug_ids(self, node, ids):
174 def filter_unknown_bug_ids(self, node, ids):
175 '''filter bug ids from list that already refer to this changeset.'''
175 '''filter bug ids from list that already refer to this changeset.'''
176
176
177 self.run('''select bug_id from longdescs where
177 self.run('''select bug_id from longdescs where
178 bug_id in %s and thetext like "%%%s%%"''' %
178 bug_id in %s and thetext like "%%%s%%"''' %
179 (buglist(ids), short(node)))
179 (buglist(ids), short(node)))
180 unknown = set(ids)
180 unknown = set(ids)
181 for (id,) in self.cursor.fetchall():
181 for (id,) in self.cursor.fetchall():
182 self.ui.status(_('bug %d already knows about changeset %s\n') %
182 self.ui.status(_('bug %d already knows about changeset %s\n') %
183 (id, short(node)))
183 (id, short(node)))
184 unknown.discard(id)
184 unknown.discard(id)
185 return sorted(unknown)
185 return sorted(unknown)
186
186
187 def notify(self, ids, committer):
187 def notify(self, ids, committer):
188 '''tell bugzilla to send mail.'''
188 '''tell bugzilla to send mail.'''
189
189
190 self.ui.status(_('telling bugzilla to send mail:\n'))
190 self.ui.status(_('telling bugzilla to send mail:\n'))
191 (user, userid) = self.get_bugzilla_user(committer)
191 (user, userid) = self.get_bugzilla_user(committer)
192 for id in ids:
192 for id in ids:
193 self.ui.status(_(' bug %s\n') % id)
193 self.ui.status(_(' bug %s\n') % id)
194 cmdfmt = self.ui.config('bugzilla', 'notify', self.default_notify)
194 cmdfmt = self.ui.config('bugzilla', 'notify', self.default_notify)
195 bzdir = self.ui.config('bugzilla', 'bzdir', '/var/www/html/bugzilla')
195 bzdir = self.ui.config('bugzilla', 'bzdir', '/var/www/html/bugzilla')
196 try:
196 try:
197 # Backwards-compatible with old notify string, which
197 # Backwards-compatible with old notify string, which
198 # took one string. This will throw with a new format
198 # took one string. This will throw with a new format
199 # string.
199 # string.
200 cmd = cmdfmt % id
200 cmd = cmdfmt % id
201 except TypeError:
201 except TypeError:
202 cmd = cmdfmt % {'bzdir': bzdir, 'id': id, 'user': user}
202 cmd = cmdfmt % {'bzdir': bzdir, 'id': id, 'user': user}
203 self.ui.note(_('running notify command %s\n') % cmd)
203 self.ui.note(_('running notify command %s\n') % cmd)
204 fp = util.popen('(%s) 2>&1' % cmd)
204 fp = util.popen('(%s) 2>&1' % cmd)
205 out = fp.read()
205 out = fp.read()
206 ret = fp.close()
206 ret = fp.close()
207 if ret:
207 if ret:
208 self.ui.warn(out)
208 self.ui.warn(out)
209 raise util.Abort(_('bugzilla notify command %s') %
209 raise util.Abort(_('bugzilla notify command %s') %
210 util.explain_exit(ret)[0])
210 util.explain_exit(ret)[0])
211 self.ui.status(_('done\n'))
211 self.ui.status(_('done\n'))
212
212
213 def get_user_id(self, user):
213 def get_user_id(self, user):
214 '''look up numeric bugzilla user id.'''
214 '''look up numeric bugzilla user id.'''
215 try:
215 try:
216 return self.user_ids[user]
216 return self.user_ids[user]
217 except KeyError:
217 except KeyError:
218 try:
218 try:
219 userid = int(user)
219 userid = int(user)
220 except ValueError:
220 except ValueError:
221 self.ui.note(_('looking up user %s\n') % user)
221 self.ui.note(_('looking up user %s\n') % user)
222 self.run('''select userid from profiles
222 self.run('''select userid from profiles
223 where login_name like %s''', user)
223 where login_name like %s''', user)
224 all = self.cursor.fetchall()
224 all = self.cursor.fetchall()
225 if len(all) != 1:
225 if len(all) != 1:
226 raise KeyError(user)
226 raise KeyError(user)
227 userid = int(all[0][0])
227 userid = int(all[0][0])
228 self.user_ids[user] = userid
228 self.user_ids[user] = userid
229 return userid
229 return userid
230
230
231 def map_committer(self, user):
231 def map_committer(self, user):
232 '''map name of committer to bugzilla user name.'''
232 '''map name of committer to bugzilla user name.'''
233 for committer, bzuser in self.ui.configitems('usermap'):
233 for committer, bzuser in self.ui.configitems('usermap'):
234 if committer.lower() == user.lower():
234 if committer.lower() == user.lower():
235 return bzuser
235 return bzuser
236 return user
236 return user
237
237
238 def get_bugzilla_user(self, committer):
238 def get_bugzilla_user(self, committer):
239 '''see if committer is a registered bugzilla user. Return
239 '''see if committer is a registered bugzilla user. Return
240 bugzilla username and userid if so. If not, return default
240 bugzilla username and userid if so. If not, return default
241 bugzilla username and userid.'''
241 bugzilla username and userid.'''
242 user = self.map_committer(committer)
242 user = self.map_committer(committer)
243 try:
243 try:
244 userid = self.get_user_id(user)
244 userid = self.get_user_id(user)
245 except KeyError:
245 except KeyError:
246 try:
246 try:
247 defaultuser = self.ui.config('bugzilla', 'bzuser')
247 defaultuser = self.ui.config('bugzilla', 'bzuser')
248 if not defaultuser:
248 if not defaultuser:
249 raise util.Abort(_('cannot find bugzilla user id for %s') %
249 raise util.Abort(_('cannot find bugzilla user id for %s') %
250 user)
250 user)
251 userid = self.get_user_id(defaultuser)
251 userid = self.get_user_id(defaultuser)
252 user = defaultuser
252 user = defaultuser
253 except KeyError:
253 except KeyError:
254 raise util.Abort(_('cannot find bugzilla user id for %s or %s') %
254 raise util.Abort(_('cannot find bugzilla user id for %s or %s') %
255 (user, defaultuser))
255 (user, defaultuser))
256 return (user, userid)
256 return (user, userid)
257
257
258 def add_comment(self, bugid, text, committer):
258 def add_comment(self, bugid, text, committer):
259 '''add comment to bug. try adding comment as committer of
259 '''add comment to bug. try adding comment as committer of
260 changeset, otherwise as default bugzilla user.'''
260 changeset, otherwise as default bugzilla user.'''
261 (user, userid) = self.get_bugzilla_user(committer)
261 (user, userid) = self.get_bugzilla_user(committer)
262 now = time.strftime('%Y-%m-%d %H:%M:%S')
262 now = time.strftime('%Y-%m-%d %H:%M:%S')
263 self.run('''insert into longdescs
263 self.run('''insert into longdescs
264 (bug_id, who, bug_when, thetext)
264 (bug_id, who, bug_when, thetext)
265 values (%s, %s, %s, %s)''',
265 values (%s, %s, %s, %s)''',
266 (bugid, userid, now, text))
266 (bugid, userid, now, text))
267 self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
267 self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
268 values (%s, %s, %s, %s)''',
268 values (%s, %s, %s, %s)''',
269 (bugid, userid, now, self.longdesc_id))
269 (bugid, userid, now, self.longdesc_id))
270 self.conn.commit()
270 self.conn.commit()
271
271
272 class bugzilla_2_18(bugzilla_2_16):
272 class bugzilla_2_18(bugzilla_2_16):
273 '''support for bugzilla 2.18 series.'''
273 '''support for bugzilla 2.18 series.'''
274
274
275 def __init__(self, ui):
275 def __init__(self, ui):
276 bugzilla_2_16.__init__(self, ui)
276 bugzilla_2_16.__init__(self, ui)
277 self.default_notify = "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
277 self.default_notify = "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
278
278
279 class bugzilla_3_0(bugzilla_2_18):
279 class bugzilla_3_0(bugzilla_2_18):
280 '''support for bugzilla 3.0 series.'''
280 '''support for bugzilla 3.0 series.'''
281
281
282 def __init__(self, ui):
282 def __init__(self, ui):
283 bugzilla_2_18.__init__(self, ui)
283 bugzilla_2_18.__init__(self, ui)
284
284
285 def get_longdesc_id(self):
285 def get_longdesc_id(self):
286 '''get identity of longdesc field'''
286 '''get identity of longdesc field'''
287 self.run('select id from fielddefs where name = "longdesc"')
287 self.run('select id from fielddefs where name = "longdesc"')
288 ids = self.cursor.fetchall()
288 ids = self.cursor.fetchall()
289 if len(ids) != 1:
289 if len(ids) != 1:
290 raise util.Abort(_('unknown database schema'))
290 raise util.Abort(_('unknown database schema'))
291 return ids[0][0]
291 return ids[0][0]
292
292
293 class bugzilla(object):
293 class bugzilla(object):
294 # supported versions of bugzilla. different versions have
294 # supported versions of bugzilla. different versions have
295 # different schemas.
295 # different schemas.
296 _versions = {
296 _versions = {
297 '2.16': bugzilla_2_16,
297 '2.16': bugzilla_2_16,
298 '2.18': bugzilla_2_18,
298 '2.18': bugzilla_2_18,
299 '3.0': bugzilla_3_0
299 '3.0': bugzilla_3_0
300 }
300 }
301
301
302 _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
302 _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
303 r'((?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)')
303 r'((?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)')
304
304
305 _bz = None
305 _bz = None
306
306
307 def __init__(self, ui, repo):
307 def __init__(self, ui, repo):
308 self.ui = ui
308 self.ui = ui
309 self.repo = repo
309 self.repo = repo
310
310
311 def bz(self):
311 def bz(self):
312 '''return object that knows how to talk to bugzilla version in
312 '''return object that knows how to talk to bugzilla version in
313 use.'''
313 use.'''
314
314
315 if bugzilla._bz is None:
315 if bugzilla._bz is None:
316 bzversion = self.ui.config('bugzilla', 'version')
316 bzversion = self.ui.config('bugzilla', 'version')
317 try:
317 try:
318 bzclass = bugzilla._versions[bzversion]
318 bzclass = bugzilla._versions[bzversion]
319 except KeyError:
319 except KeyError:
320 raise util.Abort(_('bugzilla version %s not supported') %
320 raise util.Abort(_('bugzilla version %s not supported') %
321 bzversion)
321 bzversion)
322 bugzilla._bz = bzclass(self.ui)
322 bugzilla._bz = bzclass(self.ui)
323 return bugzilla._bz
323 return bugzilla._bz
324
324
325 def __getattr__(self, key):
325 def __getattr__(self, key):
326 return getattr(self.bz(), key)
326 return getattr(self.bz(), key)
327
327
328 _bug_re = None
328 _bug_re = None
329 _split_re = None
329 _split_re = None
330
330
331 def find_bug_ids(self, ctx):
331 def find_bug_ids(self, ctx):
332 '''find valid bug ids that are referred to in changeset
332 '''find valid bug ids that are referred to in changeset
333 comments and that do not already have references to this
333 comments and that do not already have references to this
334 changeset.'''
334 changeset.'''
335
335
336 if bugzilla._bug_re is None:
336 if bugzilla._bug_re is None:
337 bugzilla._bug_re = re.compile(
337 bugzilla._bug_re = re.compile(
338 self.ui.config('bugzilla', 'regexp', bugzilla._default_bug_re),
338 self.ui.config('bugzilla', 'regexp', bugzilla._default_bug_re),
339 re.IGNORECASE)
339 re.IGNORECASE)
340 bugzilla._split_re = re.compile(r'\D+')
340 bugzilla._split_re = re.compile(r'\D+')
341 start = 0
341 start = 0
342 ids = set()
342 ids = set()
343 while True:
343 while True:
344 m = bugzilla._bug_re.search(ctx.description(), start)
344 m = bugzilla._bug_re.search(ctx.description(), start)
345 if not m:
345 if not m:
346 break
346 break
347 start = m.end()
347 start = m.end()
348 for id in bugzilla._split_re.split(m.group(1)):
348 for id in bugzilla._split_re.split(m.group(1)):
349 if not id: continue
349 if not id: continue
350 ids.add(int(id))
350 ids.add(int(id))
351 if ids:
351 if ids:
352 ids = self.filter_real_bug_ids(ids)
352 ids = self.filter_real_bug_ids(ids)
353 if ids:
353 if ids:
354 ids = self.filter_unknown_bug_ids(ctx.node(), ids)
354 ids = self.filter_unknown_bug_ids(ctx.node(), ids)
355 return ids
355 return ids
356
356
357 def update(self, bugid, ctx):
357 def update(self, bugid, ctx):
358 '''update bugzilla bug with reference to changeset.'''
358 '''update bugzilla bug with reference to changeset.'''
359
359
360 def webroot(root):
360 def webroot(root):
361 '''strip leading prefix of repo root and turn into
361 '''strip leading prefix of repo root and turn into
362 url-safe path.'''
362 url-safe path.'''
363 count = int(self.ui.config('bugzilla', 'strip', 0))
363 count = int(self.ui.config('bugzilla', 'strip', 0))
364 root = util.pconvert(root)
364 root = util.pconvert(root)
365 while count > 0:
365 while count > 0:
366 c = root.find('/')
366 c = root.find('/')
367 if c == -1:
367 if c == -1:
368 break
368 break
369 root = root[c+1:]
369 root = root[c+1:]
370 count -= 1
370 count -= 1
371 return root
371 return root
372
372
373 mapfile = self.ui.config('bugzilla', 'style')
373 mapfile = self.ui.config('bugzilla', 'style')
374 tmpl = self.ui.config('bugzilla', 'template')
374 tmpl = self.ui.config('bugzilla', 'template')
375 t = cmdutil.changeset_templater(self.ui, self.repo,
375 t = cmdutil.changeset_templater(self.ui, self.repo,
376 False, None, mapfile, False)
376 False, None, mapfile, False)
377 if not mapfile and not tmpl:
377 if not mapfile and not tmpl:
378 tmpl = _('changeset {node|short} in repo {root} refers '
378 tmpl = _('changeset {node|short} in repo {root} refers '
379 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
379 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
380 if tmpl:
380 if tmpl:
381 tmpl = templater.parsestring(tmpl, quoted=False)
381 tmpl = templater.parsestring(tmpl, quoted=False)
382 t.use_template(tmpl)
382 t.use_template(tmpl)
383 self.ui.pushbuffer()
383 self.ui.pushbuffer()
384 t.show(ctx, changes=ctx.changeset(),
384 t.show(ctx, changes=ctx.changeset(),
385 bug=str(bugid),
385 bug=str(bugid),
386 hgweb=self.ui.config('web', 'baseurl'),
386 hgweb=self.ui.config('web', 'baseurl'),
387 root=self.repo.root,
387 root=self.repo.root,
388 webroot=webroot(self.repo.root))
388 webroot=webroot(self.repo.root))
389 data = self.ui.popbuffer()
389 data = self.ui.popbuffer()
390 self.add_comment(bugid, data, util.email(ctx.user()))
390 self.add_comment(bugid, data, util.email(ctx.user()))
391
391
392 def hook(ui, repo, hooktype, node=None, **kwargs):
392 def hook(ui, repo, hooktype, node=None, **kwargs):
393 '''add comment to bugzilla for each changeset that refers to a
393 '''add comment to bugzilla for each changeset that refers to a
394 bugzilla bug id. only add a comment once per bug, so same change
394 bugzilla bug id. only add a comment once per bug, so same change
395 seen multiple times does not fill bug with duplicate data.'''
395 seen multiple times does not fill bug with duplicate data.'''
396 try:
396 try:
397 import MySQLdb as mysql
397 import MySQLdb as mysql
398 global MySQLdb
398 global MySQLdb
399 MySQLdb = mysql
399 MySQLdb = mysql
400 except ImportError, err:
400 except ImportError, err:
401 raise util.Abort(_('python mysql support not available: %s') % err)
401 raise util.Abort(_('python mysql support not available: %s') % err)
402
402
403 if node is None:
403 if node is None:
404 raise util.Abort(_('hook type %s does not pass a changeset id') %
404 raise util.Abort(_('hook type %s does not pass a changeset id') %
405 hooktype)
405 hooktype)
406 try:
406 try:
407 bz = bugzilla(ui, repo)
407 bz = bugzilla(ui, repo)
408 ctx = repo[node]
408 ctx = repo[node]
409 ids = bz.find_bug_ids(ctx)
409 ids = bz.find_bug_ids(ctx)
410 if ids:
410 if ids:
411 for id in ids:
411 for id in ids:
412 bz.update(id, ctx)
412 bz.update(id, ctx)
413 bz.notify(ids, util.email(ctx.user()))
413 bz.notify(ids, util.email(ctx.user()))
414 except MySQLdb.MySQLError, err:
414 except MySQLdb.MySQLError, err:
415 raise util.Abort(_('database error: %s') % err[1])
415 raise util.Abort(_('database error: %s') % err[1])
416
416
@@ -1,246 +1,246 b''
1 # Copyright (C) 2007-8 Brendan Cully <brendan@kublai.com>
1 # Copyright (C) 2007-8 Brendan Cully <brendan@kublai.com>
2 # Published under the GNU GPL
2 # Published under the GNU GPL
3
3
4 """integrate Mercurial with a CIA notification service
4 """hooks for integrating with the CIA.vc notification service
5
5
6 This is meant to be run as a changegroup or incoming hook.
6 This is meant to be run as a changegroup or incoming hook.
7 To configure it, set the following options in your hgrc:
7 To configure it, set the following options in your hgrc:
8
8
9 [cia]
9 [cia]
10 # your registered CIA user name
10 # your registered CIA user name
11 user = foo
11 user = foo
12 # the name of the project in CIA
12 # the name of the project in CIA
13 project = foo
13 project = foo
14 # the module (subproject) (optional)
14 # the module (subproject) (optional)
15 #module = foo
15 #module = foo
16 # Append a diffstat to the log message (optional)
16 # Append a diffstat to the log message (optional)
17 #diffstat = False
17 #diffstat = False
18 # Template to use for log messages (optional)
18 # Template to use for log messages (optional)
19 #template = {desc}\\n{baseurl}/rev/{node}-- {diffstat}
19 #template = {desc}\\n{baseurl}/rev/{node}-- {diffstat}
20 # Style to use (optional)
20 # Style to use (optional)
21 #style = foo
21 #style = foo
22 # The URL of the CIA notification service (optional)
22 # The URL of the CIA notification service (optional)
23 # You can use mailto: URLs to send by email, eg
23 # You can use mailto: URLs to send by email, eg
24 # mailto:cia@cia.vc
24 # mailto:cia@cia.vc
25 # Make sure to set email.from if you do this.
25 # Make sure to set email.from if you do this.
26 #url = http://cia.vc/
26 #url = http://cia.vc/
27 # print message instead of sending it (optional)
27 # print message instead of sending it (optional)
28 #test = False
28 #test = False
29
29
30 [hooks]
30 [hooks]
31 # one of these:
31 # one of these:
32 changegroup.cia = python:hgcia.hook
32 changegroup.cia = python:hgcia.hook
33 #incoming.cia = python:hgcia.hook
33 #incoming.cia = python:hgcia.hook
34
34
35 [web]
35 [web]
36 # If you want hyperlinks (optional)
36 # If you want hyperlinks (optional)
37 baseurl = http://server/path/to/repo
37 baseurl = http://server/path/to/repo
38 """
38 """
39
39
40 from mercurial.i18n import _
40 from mercurial.i18n import _
41 from mercurial.node import *
41 from mercurial.node import *
42 from mercurial import cmdutil, patch, templater, util, mail
42 from mercurial import cmdutil, patch, templater, util, mail
43 import email.Parser
43 import email.Parser
44
44
45 import xmlrpclib
45 import xmlrpclib
46 from xml.sax import saxutils
46 from xml.sax import saxutils
47
47
48 socket_timeout = 30 # seconds
48 socket_timeout = 30 # seconds
49 try:
49 try:
50 # set a timeout for the socket so you don't have to wait so looooong
50 # set a timeout for the socket so you don't have to wait so looooong
51 # when cia.vc is having problems. requires python >= 2.3:
51 # when cia.vc is having problems. requires python >= 2.3:
52 import socket
52 import socket
53 socket.setdefaulttimeout(socket_timeout)
53 socket.setdefaulttimeout(socket_timeout)
54 except:
54 except:
55 pass
55 pass
56
56
57 HGCIA_VERSION = '0.1'
57 HGCIA_VERSION = '0.1'
58 HGCIA_URL = 'http://hg.kublai.com/mercurial/hgcia'
58 HGCIA_URL = 'http://hg.kublai.com/mercurial/hgcia'
59
59
60
60
61 class ciamsg(object):
61 class ciamsg(object):
62 """ A CIA message """
62 """ A CIA message """
63 def __init__(self, cia, ctx):
63 def __init__(self, cia, ctx):
64 self.cia = cia
64 self.cia = cia
65 self.ctx = ctx
65 self.ctx = ctx
66 self.url = self.cia.url
66 self.url = self.cia.url
67
67
68 def fileelem(self, path, uri, action):
68 def fileelem(self, path, uri, action):
69 if uri:
69 if uri:
70 uri = ' uri=%s' % saxutils.quoteattr(uri)
70 uri = ' uri=%s' % saxutils.quoteattr(uri)
71 return '<file%s action=%s>%s</file>' % (
71 return '<file%s action=%s>%s</file>' % (
72 uri, saxutils.quoteattr(action), saxutils.escape(path))
72 uri, saxutils.quoteattr(action), saxutils.escape(path))
73
73
74 def fileelems(self):
74 def fileelems(self):
75 n = self.ctx.node()
75 n = self.ctx.node()
76 f = self.cia.repo.status(self.ctx.parents()[0].node(), n)
76 f = self.cia.repo.status(self.ctx.parents()[0].node(), n)
77 url = self.url or ''
77 url = self.url or ''
78 elems = []
78 elems = []
79 for path in f[0]:
79 for path in f[0]:
80 uri = '%s/diff/%s/%s' % (url, short(n), path)
80 uri = '%s/diff/%s/%s' % (url, short(n), path)
81 elems.append(self.fileelem(path, url and uri, 'modify'))
81 elems.append(self.fileelem(path, url and uri, 'modify'))
82 for path in f[1]:
82 for path in f[1]:
83 # TODO: copy/rename ?
83 # TODO: copy/rename ?
84 uri = '%s/file/%s/%s' % (url, short(n), path)
84 uri = '%s/file/%s/%s' % (url, short(n), path)
85 elems.append(self.fileelem(path, url and uri, 'add'))
85 elems.append(self.fileelem(path, url and uri, 'add'))
86 for path in f[2]:
86 for path in f[2]:
87 elems.append(self.fileelem(path, '', 'remove'))
87 elems.append(self.fileelem(path, '', 'remove'))
88
88
89 return '\n'.join(elems)
89 return '\n'.join(elems)
90
90
91 def sourceelem(self, project, module=None, branch=None):
91 def sourceelem(self, project, module=None, branch=None):
92 msg = ['<source>', '<project>%s</project>' % saxutils.escape(project)]
92 msg = ['<source>', '<project>%s</project>' % saxutils.escape(project)]
93 if module:
93 if module:
94 msg.append('<module>%s</module>' % saxutils.escape(module))
94 msg.append('<module>%s</module>' % saxutils.escape(module))
95 if branch:
95 if branch:
96 msg.append('<branch>%s</branch>' % saxutils.escape(branch))
96 msg.append('<branch>%s</branch>' % saxutils.escape(branch))
97 msg.append('</source>')
97 msg.append('</source>')
98
98
99 return '\n'.join(msg)
99 return '\n'.join(msg)
100
100
101 def diffstat(self):
101 def diffstat(self):
102 class patchbuf(object):
102 class patchbuf(object):
103 def __init__(self):
103 def __init__(self):
104 self.lines = []
104 self.lines = []
105 # diffstat is stupid
105 # diffstat is stupid
106 self.name = 'cia'
106 self.name = 'cia'
107 def write(self, data):
107 def write(self, data):
108 self.lines.append(data)
108 self.lines.append(data)
109 def close(self):
109 def close(self):
110 pass
110 pass
111
111
112 n = self.ctx.node()
112 n = self.ctx.node()
113 pbuf = patchbuf()
113 pbuf = patchbuf()
114 patch.export(self.cia.repo, [n], fp=pbuf)
114 patch.export(self.cia.repo, [n], fp=pbuf)
115 return patch.diffstat(pbuf.lines) or ''
115 return patch.diffstat(pbuf.lines) or ''
116
116
117 def logmsg(self):
117 def logmsg(self):
118 diffstat = self.cia.diffstat and self.diffstat() or ''
118 diffstat = self.cia.diffstat and self.diffstat() or ''
119 self.cia.ui.pushbuffer()
119 self.cia.ui.pushbuffer()
120 self.cia.templater.show(self.ctx, changes=self.ctx.changeset(),
120 self.cia.templater.show(self.ctx, changes=self.ctx.changeset(),
121 url=self.cia.url, diffstat=diffstat)
121 url=self.cia.url, diffstat=diffstat)
122 return self.cia.ui.popbuffer()
122 return self.cia.ui.popbuffer()
123
123
124 def xml(self):
124 def xml(self):
125 n = short(self.ctx.node())
125 n = short(self.ctx.node())
126 src = self.sourceelem(self.cia.project, module=self.cia.module,
126 src = self.sourceelem(self.cia.project, module=self.cia.module,
127 branch=self.ctx.branch())
127 branch=self.ctx.branch())
128 # unix timestamp
128 # unix timestamp
129 dt = self.ctx.date()
129 dt = self.ctx.date()
130 timestamp = dt[0]
130 timestamp = dt[0]
131
131
132 author = saxutils.escape(self.ctx.user())
132 author = saxutils.escape(self.ctx.user())
133 rev = '%d:%s' % (self.ctx.rev(), n)
133 rev = '%d:%s' % (self.ctx.rev(), n)
134 log = saxutils.escape(self.logmsg())
134 log = saxutils.escape(self.logmsg())
135
135
136 url = self.url and '<url>%s/rev/%s</url>' % (saxutils.escape(self.url),
136 url = self.url and '<url>%s/rev/%s</url>' % (saxutils.escape(self.url),
137 n) or ''
137 n) or ''
138
138
139 msg = """
139 msg = """
140 <message>
140 <message>
141 <generator>
141 <generator>
142 <name>Mercurial (hgcia)</name>
142 <name>Mercurial (hgcia)</name>
143 <version>%s</version>
143 <version>%s</version>
144 <url>%s</url>
144 <url>%s</url>
145 <user>%s</user>
145 <user>%s</user>
146 </generator>
146 </generator>
147 %s
147 %s
148 <body>
148 <body>
149 <commit>
149 <commit>
150 <author>%s</author>
150 <author>%s</author>
151 <version>%s</version>
151 <version>%s</version>
152 <log>%s</log>
152 <log>%s</log>
153 %s
153 %s
154 <files>%s</files>
154 <files>%s</files>
155 </commit>
155 </commit>
156 </body>
156 </body>
157 <timestamp>%d</timestamp>
157 <timestamp>%d</timestamp>
158 </message>
158 </message>
159 """ % \
159 """ % \
160 (HGCIA_VERSION, saxutils.escape(HGCIA_URL),
160 (HGCIA_VERSION, saxutils.escape(HGCIA_URL),
161 saxutils.escape(self.cia.user), src, author, rev, log, url,
161 saxutils.escape(self.cia.user), src, author, rev, log, url,
162 self.fileelems(), timestamp)
162 self.fileelems(), timestamp)
163
163
164 return msg
164 return msg
165
165
166
166
167 class hgcia(object):
167 class hgcia(object):
168 """ CIA notification class """
168 """ CIA notification class """
169
169
170 deftemplate = '{desc}'
170 deftemplate = '{desc}'
171 dstemplate = '{desc}\n-- \n{diffstat}'
171 dstemplate = '{desc}\n-- \n{diffstat}'
172
172
173 def __init__(self, ui, repo):
173 def __init__(self, ui, repo):
174 self.ui = ui
174 self.ui = ui
175 self.repo = repo
175 self.repo = repo
176
176
177 self.ciaurl = self.ui.config('cia', 'url', 'http://cia.vc')
177 self.ciaurl = self.ui.config('cia', 'url', 'http://cia.vc')
178 self.user = self.ui.config('cia', 'user')
178 self.user = self.ui.config('cia', 'user')
179 self.project = self.ui.config('cia', 'project')
179 self.project = self.ui.config('cia', 'project')
180 self.module = self.ui.config('cia', 'module')
180 self.module = self.ui.config('cia', 'module')
181 self.diffstat = self.ui.configbool('cia', 'diffstat')
181 self.diffstat = self.ui.configbool('cia', 'diffstat')
182 self.emailfrom = self.ui.config('email', 'from')
182 self.emailfrom = self.ui.config('email', 'from')
183 self.dryrun = self.ui.configbool('cia', 'test')
183 self.dryrun = self.ui.configbool('cia', 'test')
184 self.url = self.ui.config('web', 'baseurl')
184 self.url = self.ui.config('web', 'baseurl')
185
185
186 style = self.ui.config('cia', 'style')
186 style = self.ui.config('cia', 'style')
187 template = self.ui.config('cia', 'template')
187 template = self.ui.config('cia', 'template')
188 if not template:
188 if not template:
189 template = self.diffstat and self.dstemplate or self.deftemplate
189 template = self.diffstat and self.dstemplate or self.deftemplate
190 template = templater.parsestring(template, quoted=False)
190 template = templater.parsestring(template, quoted=False)
191 t = cmdutil.changeset_templater(self.ui, self.repo, False, None,
191 t = cmdutil.changeset_templater(self.ui, self.repo, False, None,
192 style, False)
192 style, False)
193 t.use_template(template)
193 t.use_template(template)
194 self.templater = t
194 self.templater = t
195
195
196 def sendrpc(self, msg):
196 def sendrpc(self, msg):
197 srv = xmlrpclib.Server(self.ciaurl)
197 srv = xmlrpclib.Server(self.ciaurl)
198 srv.hub.deliver(msg)
198 srv.hub.deliver(msg)
199
199
200 def sendemail(self, address, data):
200 def sendemail(self, address, data):
201 p = email.Parser.Parser()
201 p = email.Parser.Parser()
202 msg = p.parsestr(data)
202 msg = p.parsestr(data)
203 msg['Date'] = util.datestr(format="%a, %d %b %Y %H:%M:%S %1%2")
203 msg['Date'] = util.datestr(format="%a, %d %b %Y %H:%M:%S %1%2")
204 msg['To'] = address
204 msg['To'] = address
205 msg['From'] = self.emailfrom
205 msg['From'] = self.emailfrom
206 msg['Subject'] = 'DeliverXML'
206 msg['Subject'] = 'DeliverXML'
207 msg['Content-type'] = 'text/xml'
207 msg['Content-type'] = 'text/xml'
208 msgtext = msg.as_string(0)
208 msgtext = msg.as_string(0)
209
209
210 self.ui.status(_('hgcia: sending update to %s\n') % address)
210 self.ui.status(_('hgcia: sending update to %s\n') % address)
211 mail.sendmail(self.ui, util.email(self.emailfrom),
211 mail.sendmail(self.ui, util.email(self.emailfrom),
212 [address], msgtext)
212 [address], msgtext)
213
213
214
214
215 def hook(ui, repo, hooktype, node=None, url=None, **kwargs):
215 def hook(ui, repo, hooktype, node=None, url=None, **kwargs):
216 """ send CIA notification """
216 """ send CIA notification """
217 def sendmsg(cia, ctx):
217 def sendmsg(cia, ctx):
218 msg = ciamsg(cia, ctx).xml()
218 msg = ciamsg(cia, ctx).xml()
219 if cia.dryrun:
219 if cia.dryrun:
220 ui.write(msg)
220 ui.write(msg)
221 elif cia.ciaurl.startswith('mailto:'):
221 elif cia.ciaurl.startswith('mailto:'):
222 if not cia.emailfrom:
222 if not cia.emailfrom:
223 raise util.Abort(_('email.from must be defined when '
223 raise util.Abort(_('email.from must be defined when '
224 'sending by email'))
224 'sending by email'))
225 cia.sendemail(cia.ciaurl[7:], msg)
225 cia.sendemail(cia.ciaurl[7:], msg)
226 else:
226 else:
227 cia.sendrpc(msg)
227 cia.sendrpc(msg)
228
228
229 n = bin(node)
229 n = bin(node)
230 cia = hgcia(ui, repo)
230 cia = hgcia(ui, repo)
231 if not cia.user:
231 if not cia.user:
232 ui.debug(_('cia: no user specified'))
232 ui.debug(_('cia: no user specified'))
233 return
233 return
234 if not cia.project:
234 if not cia.project:
235 ui.debug(_('cia: no project specified'))
235 ui.debug(_('cia: no project specified'))
236 return
236 return
237 if hooktype == 'changegroup':
237 if hooktype == 'changegroup':
238 start = repo.changelog.rev(n)
238 start = repo.changelog.rev(n)
239 end = len(repo.changelog)
239 end = len(repo.changelog)
240 for rev in xrange(start, end):
240 for rev in xrange(start, end):
241 n = repo.changelog.node(rev)
241 n = repo.changelog.node(rev)
242 ctx = repo.changectx(n)
242 ctx = repo.changectx(n)
243 sendmsg(cia, ctx)
243 sendmsg(cia, ctx)
244 else:
244 else:
245 ctx = repo.changectx(n)
245 ctx = repo.changectx(n)
246 sendmsg(cia, ctx)
246 sendmsg(cia, ctx)
@@ -1,289 +1,289 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 '''send e-mail notifications for commits/pushes
8 '''hooks for sending email notifications at commit/push time
9
9
10 Subscriptions can be managed through hgrc. Default mode is to print
10 Subscriptions can be managed through hgrc. Default mode is to print
11 messages to stdout, for testing and configuring.
11 messages to stdout, for testing and configuring.
12
12
13 To use, configure notify extension and enable in hgrc like this:
13 To use, configure notify extension and enable in hgrc like this:
14
14
15 [extensions]
15 [extensions]
16 hgext.notify =
16 hgext.notify =
17
17
18 [hooks]
18 [hooks]
19 # one email for each incoming changeset
19 # one email for each incoming changeset
20 incoming.notify = python:hgext.notify.hook
20 incoming.notify = python:hgext.notify.hook
21 # batch emails when many changesets incoming at one time
21 # batch emails when many changesets incoming at one time
22 changegroup.notify = python:hgext.notify.hook
22 changegroup.notify = python:hgext.notify.hook
23
23
24 [notify]
24 [notify]
25 # config items go in here
25 # config items go in here
26
26
27 config items:
27 config items:
28
28
29 REQUIRED:
29 REQUIRED:
30 config = /path/to/file # file containing subscriptions
30 config = /path/to/file # file containing subscriptions
31
31
32 OPTIONAL:
32 OPTIONAL:
33 test = True # print messages to stdout for testing
33 test = True # print messages to stdout for testing
34 strip = 3 # number of slashes to strip for url paths
34 strip = 3 # number of slashes to strip for url paths
35 domain = example.com # domain to use if committer missing domain
35 domain = example.com # domain to use if committer missing domain
36 style = ... # style file to use when formatting email
36 style = ... # style file to use when formatting email
37 template = ... # template to use when formatting email
37 template = ... # template to use when formatting email
38 incoming = ... # template to use when run as incoming hook
38 incoming = ... # template to use when run as incoming hook
39 changegroup = ... # template when run as changegroup hook
39 changegroup = ... # template when run as changegroup hook
40 maxdiff = 300 # max lines of diffs to include (0=none, -1=all)
40 maxdiff = 300 # max lines of diffs to include (0=none, -1=all)
41 maxsubject = 67 # truncate subject line longer than this
41 maxsubject = 67 # truncate subject line longer than this
42 diffstat = True # add a diffstat before the diff content
42 diffstat = True # add a diffstat before the diff content
43 sources = serve # notify if source of incoming changes in this list
43 sources = serve # notify if source of incoming changes in this list
44 # (serve == ssh or http, push, pull, bundle)
44 # (serve == ssh or http, push, pull, bundle)
45 [email]
45 [email]
46 from = user@host.com # email address to send as if none given
46 from = user@host.com # email address to send as if none given
47 [web]
47 [web]
48 baseurl = http://hgserver/... # root of hg web site for browsing commits
48 baseurl = http://hgserver/... # root of hg web site for browsing commits
49
49
50 notify config file has same format as regular hgrc. it has two
50 notify config file has same format as regular hgrc. it has two
51 sections so you can express subscriptions in whatever way is handier
51 sections so you can express subscriptions in whatever way is handier
52 for you.
52 for you.
53
53
54 [usersubs]
54 [usersubs]
55 # key is subscriber email, value is ","-separated list of glob patterns
55 # key is subscriber email, value is ","-separated list of glob patterns
56 user@host = pattern
56 user@host = pattern
57
57
58 [reposubs]
58 [reposubs]
59 # key is glob pattern, value is ","-separated list of subscriber emails
59 # key is glob pattern, value is ","-separated list of subscriber emails
60 pattern = user@host
60 pattern = user@host
61
61
62 glob patterns are matched against path to repository root.
62 glob patterns are matched against path to repository root.
63
63
64 if you like, you can put notify config file in repository that users
64 if you like, you can put notify config file in repository that users
65 can push changes to, they can manage their own subscriptions.'''
65 can push changes to, they can manage their own subscriptions.'''
66
66
67 from mercurial.i18n import _
67 from mercurial.i18n import _
68 from mercurial import patch, cmdutil, templater, util, mail
68 from mercurial import patch, cmdutil, templater, util, mail
69 import email.Parser, fnmatch, socket, time
69 import email.Parser, fnmatch, socket, time
70
70
71 # template for single changeset can include email headers.
71 # template for single changeset can include email headers.
72 single_template = '''
72 single_template = '''
73 Subject: changeset in {webroot}: {desc|firstline|strip}
73 Subject: changeset in {webroot}: {desc|firstline|strip}
74 From: {author}
74 From: {author}
75
75
76 changeset {node|short} in {root}
76 changeset {node|short} in {root}
77 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
77 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
78 description:
78 description:
79 \t{desc|tabindent|strip}
79 \t{desc|tabindent|strip}
80 '''.lstrip()
80 '''.lstrip()
81
81
82 # template for multiple changesets should not contain email headers,
82 # template for multiple changesets should not contain email headers,
83 # because only first set of headers will be used and result will look
83 # because only first set of headers will be used and result will look
84 # strange.
84 # strange.
85 multiple_template = '''
85 multiple_template = '''
86 changeset {node|short} in {root}
86 changeset {node|short} in {root}
87 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
87 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
88 summary: {desc|firstline}
88 summary: {desc|firstline}
89 '''
89 '''
90
90
91 deftemplates = {
91 deftemplates = {
92 'changegroup': multiple_template,
92 'changegroup': multiple_template,
93 }
93 }
94
94
95 class notifier(object):
95 class notifier(object):
96 '''email notification class.'''
96 '''email notification class.'''
97
97
98 def __init__(self, ui, repo, hooktype):
98 def __init__(self, ui, repo, hooktype):
99 self.ui = ui
99 self.ui = ui
100 cfg = self.ui.config('notify', 'config')
100 cfg = self.ui.config('notify', 'config')
101 if cfg:
101 if cfg:
102 self.ui.readconfig(cfg, sections=['usersubs', 'reposubs'])
102 self.ui.readconfig(cfg, sections=['usersubs', 'reposubs'])
103 self.repo = repo
103 self.repo = repo
104 self.stripcount = int(self.ui.config('notify', 'strip', 0))
104 self.stripcount = int(self.ui.config('notify', 'strip', 0))
105 self.root = self.strip(self.repo.root)
105 self.root = self.strip(self.repo.root)
106 self.domain = self.ui.config('notify', 'domain')
106 self.domain = self.ui.config('notify', 'domain')
107 self.test = self.ui.configbool('notify', 'test', True)
107 self.test = self.ui.configbool('notify', 'test', True)
108 self.charsets = mail._charsets(self.ui)
108 self.charsets = mail._charsets(self.ui)
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, None, mapfile, False)
115 False, None, 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 subs = set()
149 subs = set()
150 for user, pats in self.ui.configitems('usersubs'):
150 for user, pats in self.ui.configitems('usersubs'):
151 for pat in pats.split(','):
151 for pat in pats.split(','):
152 if fnmatch.fnmatch(self.repo.root, pat.strip()):
152 if fnmatch.fnmatch(self.repo.root, pat.strip()):
153 subs.add(self.fixmail(user))
153 subs.add(self.fixmail(user))
154 for pat, users in self.ui.configitems('reposubs'):
154 for pat, users in self.ui.configitems('reposubs'):
155 if fnmatch.fnmatch(self.repo.root, pat):
155 if fnmatch.fnmatch(self.repo.root, pat):
156 for user in users.split(','):
156 for user in users.split(','):
157 subs.add(self.fixmail(user))
157 subs.add(self.fixmail(user))
158 return [mail.addressencode(self.ui, s, self.charsets, self.test)
158 return [mail.addressencode(self.ui, s, self.charsets, self.test)
159 for s in sorted(subs)]
159 for s in sorted(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, ctx):
164 def node(self, ctx):
165 '''format one changeset.'''
165 '''format one changeset.'''
166 self.t.show(ctx, changes=ctx.changeset(),
166 self.t.show(ctx, changes=ctx.changeset(),
167 baseurl=self.ui.config('web', 'baseurl'),
167 baseurl=self.ui.config('web', 'baseurl'),
168 root=self.repo.root, webroot=self.root)
168 root=self.repo.root, webroot=self.root)
169
169
170 def skipsource(self, source):
170 def skipsource(self, source):
171 '''true if incoming changes from this source should be skipped.'''
171 '''true if incoming changes from this source should be skipped.'''
172 ok_sources = self.ui.config('notify', 'sources', 'serve').split()
172 ok_sources = self.ui.config('notify', 'sources', 'serve').split()
173 return source not in ok_sources
173 return source not in ok_sources
174
174
175 def send(self, ctx, count, data):
175 def send(self, ctx, count, data):
176 '''send message.'''
176 '''send message.'''
177
177
178 p = email.Parser.Parser()
178 p = email.Parser.Parser()
179 msg = p.parsestr(data)
179 msg = p.parsestr(data)
180
180
181 # store sender and subject
181 # store sender and subject
182 sender, subject = msg['From'], msg['Subject']
182 sender, subject = msg['From'], msg['Subject']
183 del msg['From'], msg['Subject']
183 del msg['From'], msg['Subject']
184 # store remaining headers
184 # store remaining headers
185 headers = msg.items()
185 headers = msg.items()
186 # create fresh mime message from msg body
186 # create fresh mime message from msg body
187 text = msg.get_payload()
187 text = msg.get_payload()
188 # for notification prefer readability over data precision
188 # for notification prefer readability over data precision
189 msg = mail.mimeencode(self.ui, text, self.charsets, self.test)
189 msg = mail.mimeencode(self.ui, text, self.charsets, self.test)
190 # reinstate custom headers
190 # reinstate custom headers
191 for k, v in headers:
191 for k, v in headers:
192 msg[k] = v
192 msg[k] = v
193
193
194 msg['Date'] = util.datestr(format="%a, %d %b %Y %H:%M:%S %1%2")
194 msg['Date'] = util.datestr(format="%a, %d %b %Y %H:%M:%S %1%2")
195
195
196 # try to make subject line exist and be useful
196 # try to make subject line exist and be useful
197 if not subject:
197 if not subject:
198 if count > 1:
198 if count > 1:
199 subject = _('%s: %d new changesets') % (self.root, count)
199 subject = _('%s: %d new changesets') % (self.root, count)
200 else:
200 else:
201 s = ctx.description().lstrip().split('\n', 1)[0].rstrip()
201 s = ctx.description().lstrip().split('\n', 1)[0].rstrip()
202 subject = '%s: %s' % (self.root, s)
202 subject = '%s: %s' % (self.root, s)
203 maxsubject = int(self.ui.config('notify', 'maxsubject', 67))
203 maxsubject = int(self.ui.config('notify', 'maxsubject', 67))
204 if maxsubject and len(subject) > maxsubject:
204 if maxsubject and len(subject) > maxsubject:
205 subject = subject[:maxsubject-3] + '...'
205 subject = subject[:maxsubject-3] + '...'
206 msg['Subject'] = mail.headencode(self.ui, subject,
206 msg['Subject'] = mail.headencode(self.ui, subject,
207 self.charsets, self.test)
207 self.charsets, self.test)
208
208
209 # try to make message have proper sender
209 # try to make message have proper sender
210 if not sender:
210 if not sender:
211 sender = self.ui.config('email', 'from') or self.ui.username()
211 sender = self.ui.config('email', 'from') or self.ui.username()
212 if '@' not in sender or '@localhost' in sender:
212 if '@' not in sender or '@localhost' in sender:
213 sender = self.fixmail(sender)
213 sender = self.fixmail(sender)
214 msg['From'] = mail.addressencode(self.ui, sender,
214 msg['From'] = mail.addressencode(self.ui, sender,
215 self.charsets, self.test)
215 self.charsets, self.test)
216
216
217 msg['X-Hg-Notification'] = 'changeset %s' % ctx
217 msg['X-Hg-Notification'] = 'changeset %s' % ctx
218 if not msg['Message-Id']:
218 if not msg['Message-Id']:
219 msg['Message-Id'] = ('<hg.%s.%s.%s@%s>' %
219 msg['Message-Id'] = ('<hg.%s.%s.%s@%s>' %
220 (ctx, int(time.time()),
220 (ctx, int(time.time()),
221 hash(self.repo.root), socket.getfqdn()))
221 hash(self.repo.root), socket.getfqdn()))
222 msg['To'] = ', '.join(self.subs)
222 msg['To'] = ', '.join(self.subs)
223
223
224 msgtext = msg.as_string(0)
224 msgtext = msg.as_string(0)
225 if self.test:
225 if self.test:
226 self.ui.write(msgtext)
226 self.ui.write(msgtext)
227 if not msgtext.endswith('\n'):
227 if not msgtext.endswith('\n'):
228 self.ui.write('\n')
228 self.ui.write('\n')
229 else:
229 else:
230 self.ui.status(_('notify: sending %d subscribers %d changes\n') %
230 self.ui.status(_('notify: sending %d subscribers %d changes\n') %
231 (len(self.subs), count))
231 (len(self.subs), count))
232 mail.sendmail(self.ui, util.email(msg['From']),
232 mail.sendmail(self.ui, util.email(msg['From']),
233 self.subs, msgtext)
233 self.subs, msgtext)
234
234
235 def diff(self, ctx, ref=None):
235 def diff(self, ctx, ref=None):
236
236
237 maxdiff = int(self.ui.config('notify', 'maxdiff', 300))
237 maxdiff = int(self.ui.config('notify', 'maxdiff', 300))
238 prev = ctx.parents()[0].node()
238 prev = ctx.parents()[0].node()
239 ref = ref and ref.node() or ctx.node()
239 ref = ref and ref.node() or ctx.node()
240 chunks = patch.diff(self.repo, prev, ref, opts=patch.diffopts(self.ui))
240 chunks = patch.diff(self.repo, prev, ref, opts=patch.diffopts(self.ui))
241 difflines = ''.join(chunks).splitlines()
241 difflines = ''.join(chunks).splitlines()
242
242
243 if self.ui.configbool('notify', 'diffstat', True):
243 if self.ui.configbool('notify', 'diffstat', True):
244 s = patch.diffstat(difflines)
244 s = patch.diffstat(difflines)
245 # s may be nil, don't include the header if it is
245 # s may be nil, don't include the header if it is
246 if s:
246 if s:
247 self.ui.write('\ndiffstat:\n\n%s' % s)
247 self.ui.write('\ndiffstat:\n\n%s' % s)
248
248
249 if maxdiff == 0:
249 if maxdiff == 0:
250 return
250 return
251 elif maxdiff > 0 and len(difflines) > maxdiff:
251 elif maxdiff > 0 and len(difflines) > maxdiff:
252 msg = _('\ndiffs (truncated from %d to %d lines):\n\n')
252 msg = _('\ndiffs (truncated from %d to %d lines):\n\n')
253 self.ui.write(msg % (len(difflines), maxdiff))
253 self.ui.write(msg % (len(difflines), maxdiff))
254 difflines = difflines[:maxdiff]
254 difflines = difflines[:maxdiff]
255 elif difflines:
255 elif difflines:
256 self.ui.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
256 self.ui.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
257
257
258 self.ui.write("\n".join(difflines))
258 self.ui.write("\n".join(difflines))
259
259
260 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
260 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
261 '''send email notifications to interested subscribers.
261 '''send email notifications to interested subscribers.
262
262
263 if used as changegroup hook, send one email for all changesets in
263 if used as changegroup hook, send one email for all changesets in
264 changegroup. else send one email per changeset.'''
264 changegroup. else send one email per changeset.'''
265
265
266 n = notifier(ui, repo, hooktype)
266 n = notifier(ui, repo, hooktype)
267 ctx = repo[node]
267 ctx = repo[node]
268
268
269 if not n.subs:
269 if not n.subs:
270 ui.debug(_('notify: no subscribers to repository %s\n') % n.root)
270 ui.debug(_('notify: no subscribers to repository %s\n') % n.root)
271 return
271 return
272 if n.skipsource(source):
272 if n.skipsource(source):
273 ui.debug(_('notify: changes have source "%s" - skipping\n') % source)
273 ui.debug(_('notify: changes have source "%s" - skipping\n') % source)
274 return
274 return
275
275
276 ui.pushbuffer()
276 ui.pushbuffer()
277 if hooktype == 'changegroup':
277 if hooktype == 'changegroup':
278 start, end = ctx.rev(), len(repo)
278 start, end = ctx.rev(), len(repo)
279 count = end - start
279 count = end - start
280 for rev in xrange(start, end):
280 for rev in xrange(start, end):
281 n.node(repo[rev])
281 n.node(repo[rev])
282 n.diff(ctx, repo['tip'])
282 n.diff(ctx, repo['tip'])
283 else:
283 else:
284 count = 1
284 count = 1
285 n.node(ctx)
285 n.node(ctx)
286 n.diff(ctx)
286 n.diff(ctx)
287
287
288 data = ui.popbuffer()
288 data = ui.popbuffer()
289 n.send(ctx, count, data)
289 n.send(ctx, count, data)
@@ -1,507 +1,507 b''
1 # patchbomb.py - sending Mercurial changesets as patch emails
1 # patchbomb.py - sending Mercurial changesets as patch emails
2 #
2 #
3 # Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others
3 # Copyright 2005-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 '''command to send changesets as (a series of) patch e-mails
8 '''command to send changesets as (a series of) patch emails
9
9
10 The series is started off with a "[PATCH 0 of N]" introduction, which
10 The series is started off with a "[PATCH 0 of N]" introduction, which
11 describes the series as a whole.
11 describes the series as a whole.
12
12
13 Each patch email has a Subject line of "[PATCH M of N] ...", using the
13 Each patch email has a Subject line of "[PATCH M of N] ...", using the
14 first line of the changeset description as the subject text. The
14 first line of the changeset description as the subject text. The
15 message contains two or three body parts:
15 message contains two or three body parts:
16
16
17 The changeset description.
17 The changeset description.
18
18
19 [Optional] The result of running diffstat on the patch.
19 [Optional] The result of running diffstat on the patch.
20
20
21 The patch itself, as generated by "hg export".
21 The patch itself, as generated by "hg export".
22
22
23 Each message refers to the first in the series using the In-Reply-To
23 Each message refers to the first in the series using the In-Reply-To
24 and References headers, so they will show up as a sequence in threaded
24 and References headers, so they will show up as a sequence in threaded
25 mail and news readers, and in mail archives.
25 mail and news readers, and in mail archives.
26
26
27 With the -d/--diffstat option, you will be prompted for each changeset
27 With the -d/--diffstat option, you will be prompted for each changeset
28 with a diffstat summary and the changeset summary, so you can be sure
28 with a diffstat summary and the changeset summary, so you can be sure
29 you are sending the right changes.
29 you are sending the right changes.
30
30
31 To configure other defaults, add a section like this to your hgrc
31 To configure other defaults, add a section like this to your hgrc
32 file:
32 file:
33
33
34 [email]
34 [email]
35 from = My Name <my@email>
35 from = My Name <my@email>
36 to = recipient1, recipient2, ...
36 to = recipient1, recipient2, ...
37 cc = cc1, cc2, ...
37 cc = cc1, cc2, ...
38 bcc = bcc1, bcc2, ...
38 bcc = bcc1, bcc2, ...
39
39
40 Then you can use the "hg email" command to mail a series of changesets
40 Then you can use the "hg email" command to mail a series of changesets
41 as a patchbomb.
41 as a patchbomb.
42
42
43 To avoid sending patches prematurely, it is a good idea to first run
43 To avoid sending patches prematurely, it is a good idea to first run
44 the "email" command with the "-n" option (test only). You will be
44 the "email" command with the "-n" option (test only). You will be
45 prompted for an email recipient address, a subject and an introductory
45 prompted for an email recipient address, a subject and an introductory
46 message describing the patches of your patchbomb. Then when all is
46 message describing the patches of your patchbomb. Then when all is
47 done, patchbomb messages are displayed. If the PAGER environment
47 done, patchbomb messages are displayed. If the PAGER environment
48 variable is set, your pager will be fired up once for each patchbomb
48 variable is set, your pager will be fired up once for each patchbomb
49 message, so you can verify everything is alright.
49 message, so you can verify everything is alright.
50
50
51 The -m/--mbox option is also very useful. Instead of previewing each
51 The -m/--mbox option is also very useful. Instead of previewing each
52 patchbomb message in a pager or sending the messages directly, it will
52 patchbomb message in a pager or sending the messages directly, it will
53 create a UNIX mailbox file with the patch emails. This mailbox file
53 create a UNIX mailbox file with the patch emails. This mailbox file
54 can be previewed with any mail user agent which supports UNIX mbox
54 can be previewed with any mail user agent which supports UNIX mbox
55 files, e.g. with mutt:
55 files, e.g. with mutt:
56
56
57 % mutt -R -f mbox
57 % mutt -R -f mbox
58
58
59 When you are previewing the patchbomb messages, you can use `formail'
59 When you are previewing the patchbomb messages, you can use `formail'
60 (a utility that is commonly installed as part of the procmail
60 (a utility that is commonly installed as part of the procmail
61 package), to send each message out:
61 package), to send each message out:
62
62
63 % formail -s sendmail -bm -t < mbox
63 % formail -s sendmail -bm -t < mbox
64
64
65 That should be all. Now your patchbomb is on its way out.
65 That should be all. Now your patchbomb is on its way out.
66
66
67 You can also either configure the method option in the email section
67 You can also either configure the method option in the email section
68 to be a sendmail compatible mailer or fill out the [smtp] section so
68 to be a sendmail compatible mailer or fill out the [smtp] section so
69 that the patchbomb extension can automatically send patchbombs
69 that the patchbomb extension can automatically send patchbombs
70 directly from the commandline. See the [email] and [smtp] sections in
70 directly from the commandline. See the [email] and [smtp] sections in
71 hgrc(5) for details.'''
71 hgrc(5) for details.'''
72
72
73 import os, errno, socket, tempfile, cStringIO
73 import os, errno, socket, tempfile, cStringIO
74 import email.MIMEMultipart, email.MIMEBase
74 import email.MIMEMultipart, email.MIMEBase
75 import email.Utils, email.Encoders, email.Generator
75 import email.Utils, email.Encoders, email.Generator
76 from mercurial import cmdutil, commands, hg, mail, patch, util
76 from mercurial import cmdutil, commands, hg, mail, patch, util
77 from mercurial.i18n import _
77 from mercurial.i18n import _
78 from mercurial.node import bin
78 from mercurial.node import bin
79
79
80 def prompt(ui, prompt, default=None, rest=': ', empty_ok=False):
80 def prompt(ui, prompt, default=None, rest=': ', empty_ok=False):
81 if not ui.interactive():
81 if not ui.interactive():
82 return default
82 return default
83 if default:
83 if default:
84 prompt += ' [%s]' % default
84 prompt += ' [%s]' % default
85 prompt += rest
85 prompt += rest
86 while True:
86 while True:
87 r = ui.prompt(prompt, default=default)
87 r = ui.prompt(prompt, default=default)
88 if r:
88 if r:
89 return r
89 return r
90 if default is not None:
90 if default is not None:
91 return default
91 return default
92 if empty_ok:
92 if empty_ok:
93 return r
93 return r
94 ui.warn(_('Please enter a valid value.\n'))
94 ui.warn(_('Please enter a valid value.\n'))
95
95
96 def cdiffstat(ui, summary, patchlines):
96 def cdiffstat(ui, summary, patchlines):
97 s = patch.diffstat(patchlines)
97 s = patch.diffstat(patchlines)
98 if summary:
98 if summary:
99 ui.write(summary, '\n')
99 ui.write(summary, '\n')
100 ui.write(s, '\n')
100 ui.write(s, '\n')
101 ans = prompt(ui, _('does the diffstat above look okay? '), 'y')
101 ans = prompt(ui, _('does the diffstat above look okay? '), 'y')
102 if not ans.lower().startswith('y'):
102 if not ans.lower().startswith('y'):
103 raise util.Abort(_('diffstat rejected'))
103 raise util.Abort(_('diffstat rejected'))
104 return s
104 return s
105
105
106 def makepatch(ui, repo, patch, opts, _charsets, idx, total, patchname=None):
106 def makepatch(ui, repo, patch, opts, _charsets, idx, total, patchname=None):
107
107
108 desc = []
108 desc = []
109 node = None
109 node = None
110 body = ''
110 body = ''
111
111
112 for line in patch:
112 for line in patch:
113 if line.startswith('#'):
113 if line.startswith('#'):
114 if line.startswith('# Node ID'):
114 if line.startswith('# Node ID'):
115 node = line.split()[-1]
115 node = line.split()[-1]
116 continue
116 continue
117 if line.startswith('diff -r') or line.startswith('diff --git'):
117 if line.startswith('diff -r') or line.startswith('diff --git'):
118 break
118 break
119 desc.append(line)
119 desc.append(line)
120
120
121 if not patchname and not node:
121 if not patchname and not node:
122 raise ValueError
122 raise ValueError
123
123
124 if opts.get('attach'):
124 if opts.get('attach'):
125 body = ('\n'.join(desc[1:]).strip() or
125 body = ('\n'.join(desc[1:]).strip() or
126 'Patch subject is complete summary.')
126 'Patch subject is complete summary.')
127 body += '\n\n\n'
127 body += '\n\n\n'
128
128
129 if opts.get('plain'):
129 if opts.get('plain'):
130 while patch and patch[0].startswith('# '):
130 while patch and patch[0].startswith('# '):
131 patch.pop(0)
131 patch.pop(0)
132 if patch:
132 if patch:
133 patch.pop(0)
133 patch.pop(0)
134 while patch and not patch[0].strip():
134 while patch and not patch[0].strip():
135 patch.pop(0)
135 patch.pop(0)
136
136
137 if opts.get('diffstat'):
137 if opts.get('diffstat'):
138 body += cdiffstat(ui, '\n'.join(desc), patch) + '\n\n'
138 body += cdiffstat(ui, '\n'.join(desc), patch) + '\n\n'
139
139
140 if opts.get('attach') or opts.get('inline'):
140 if opts.get('attach') or opts.get('inline'):
141 msg = email.MIMEMultipart.MIMEMultipart()
141 msg = email.MIMEMultipart.MIMEMultipart()
142 if body:
142 if body:
143 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
143 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
144 p = mail.mimetextpatch('\n'.join(patch), 'x-patch', opts.get('test'))
144 p = mail.mimetextpatch('\n'.join(patch), 'x-patch', opts.get('test'))
145 binnode = bin(node)
145 binnode = bin(node)
146 # if node is mq patch, it will have the patch file's name as a tag
146 # if node is mq patch, it will have the patch file's name as a tag
147 if not patchname:
147 if not patchname:
148 patchtags = [t for t in repo.nodetags(binnode)
148 patchtags = [t for t in repo.nodetags(binnode)
149 if t.endswith('.patch') or t.endswith('.diff')]
149 if t.endswith('.patch') or t.endswith('.diff')]
150 if patchtags:
150 if patchtags:
151 patchname = patchtags[0]
151 patchname = patchtags[0]
152 elif total > 1:
152 elif total > 1:
153 patchname = cmdutil.make_filename(repo, '%b-%n.patch',
153 patchname = cmdutil.make_filename(repo, '%b-%n.patch',
154 binnode, seqno=idx, total=total)
154 binnode, seqno=idx, total=total)
155 else:
155 else:
156 patchname = cmdutil.make_filename(repo, '%b.patch', binnode)
156 patchname = cmdutil.make_filename(repo, '%b.patch', binnode)
157 disposition = 'inline'
157 disposition = 'inline'
158 if opts.get('attach'):
158 if opts.get('attach'):
159 disposition = 'attachment'
159 disposition = 'attachment'
160 p['Content-Disposition'] = disposition + '; filename=' + patchname
160 p['Content-Disposition'] = disposition + '; filename=' + patchname
161 msg.attach(p)
161 msg.attach(p)
162 else:
162 else:
163 body += '\n'.join(patch)
163 body += '\n'.join(patch)
164 msg = mail.mimetextpatch(body, display=opts.get('test'))
164 msg = mail.mimetextpatch(body, display=opts.get('test'))
165
165
166 subj = desc[0].strip().rstrip('. ')
166 subj = desc[0].strip().rstrip('. ')
167 if total == 1 and not opts.get('intro'):
167 if total == 1 and not opts.get('intro'):
168 subj = '[PATCH] ' + (opts.get('subject') or subj)
168 subj = '[PATCH] ' + (opts.get('subject') or subj)
169 else:
169 else:
170 tlen = len(str(total))
170 tlen = len(str(total))
171 subj = '[PATCH %0*d of %d] %s' % (tlen, idx, total, subj)
171 subj = '[PATCH %0*d of %d] %s' % (tlen, idx, total, subj)
172 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
172 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
173 msg['X-Mercurial-Node'] = node
173 msg['X-Mercurial-Node'] = node
174 return msg, subj
174 return msg, subj
175
175
176 def patchbomb(ui, repo, *revs, **opts):
176 def patchbomb(ui, repo, *revs, **opts):
177 '''send changesets by email
177 '''send changesets by email
178
178
179 By default, diffs are sent in the format generated by hg export,
179 By default, diffs are sent in the format generated by hg export,
180 one per message. The series starts with a "[PATCH 0 of N]"
180 one per message. The series starts with a "[PATCH 0 of N]"
181 introduction, which describes the series as a whole.
181 introduction, which describes the series as a whole.
182
182
183 Each patch email has a Subject line of "[PATCH M of N] ...", using
183 Each patch email has a Subject line of "[PATCH M of N] ...", using
184 the first line of the changeset description as the subject text.
184 the first line of the changeset description as the subject text.
185 The message contains two or three parts. First, the changeset
185 The message contains two or three parts. First, the changeset
186 description. Next, (optionally) if the diffstat program is
186 description. Next, (optionally) if the diffstat program is
187 installed and -d/--diffstat is used, the result of running
187 installed and -d/--diffstat is used, the result of running
188 diffstat on the patch. Finally, the patch itself, as generated by
188 diffstat on the patch. Finally, the patch itself, as generated by
189 "hg export".
189 "hg export".
190
190
191 By default the patch is included as text in the email body for
191 By default the patch is included as text in the email body for
192 easy reviewing. Using the -a/--attach option will instead create
192 easy reviewing. Using the -a/--attach option will instead create
193 an attachment for the patch. With -i/--inline an inline attachment
193 an attachment for the patch. With -i/--inline an inline attachment
194 will be created.
194 will be created.
195
195
196 With -o/--outgoing, emails will be generated for patches not found
196 With -o/--outgoing, emails will be generated for patches not found
197 in the destination repository (or only those which are ancestors
197 in the destination repository (or only those which are ancestors
198 of the specified revisions if any are provided)
198 of the specified revisions if any are provided)
199
199
200 With -b/--bundle, changesets are selected as for --outgoing, but a
200 With -b/--bundle, changesets are selected as for --outgoing, but a
201 single email containing a binary Mercurial bundle as an attachment
201 single email containing a binary Mercurial bundle as an attachment
202 will be sent.
202 will be sent.
203
203
204 Examples:
204 Examples:
205
205
206 hg email -r 3000 # send patch 3000 only
206 hg email -r 3000 # send patch 3000 only
207 hg email -r 3000 -r 3001 # send patches 3000 and 3001
207 hg email -r 3000 -r 3001 # send patches 3000 and 3001
208 hg email -r 3000:3005 # send patches 3000 through 3005
208 hg email -r 3000:3005 # send patches 3000 through 3005
209 hg email 3000 # send patch 3000 (deprecated)
209 hg email 3000 # send patch 3000 (deprecated)
210
210
211 hg email -o # send all patches not in default
211 hg email -o # send all patches not in default
212 hg email -o DEST # send all patches not in DEST
212 hg email -o DEST # send all patches not in DEST
213 hg email -o -r 3000 # send all ancestors of 3000 not in default
213 hg email -o -r 3000 # send all ancestors of 3000 not in default
214 hg email -o -r 3000 DEST # send all ancestors of 3000 not in DEST
214 hg email -o -r 3000 DEST # send all ancestors of 3000 not in DEST
215
215
216 hg email -b # send bundle of all patches not in default
216 hg email -b # send bundle of all patches not in default
217 hg email -b DEST # send bundle of all patches not in DEST
217 hg email -b DEST # send bundle of all patches not in DEST
218 hg email -b -r 3000 # bundle of all ancestors of 3000 not in default
218 hg email -b -r 3000 # bundle of all ancestors of 3000 not in default
219 hg email -b -r 3000 DEST # bundle of all ancestors of 3000 not in DEST
219 hg email -b -r 3000 DEST # bundle of all ancestors of 3000 not in DEST
220
220
221 Before using this command, you will need to enable email in your
221 Before using this command, you will need to enable email in your
222 hgrc. See the [email] section in hgrc(5) for details.
222 hgrc. See the [email] section in hgrc(5) for details.
223 '''
223 '''
224
224
225 _charsets = mail._charsets(ui)
225 _charsets = mail._charsets(ui)
226
226
227 def outgoing(dest, revs):
227 def outgoing(dest, revs):
228 '''Return the revisions present locally but not in dest'''
228 '''Return the revisions present locally but not in dest'''
229 dest = ui.expandpath(dest or 'default-push', dest or 'default')
229 dest = ui.expandpath(dest or 'default-push', dest or 'default')
230 revs = [repo.lookup(rev) for rev in revs]
230 revs = [repo.lookup(rev) for rev in revs]
231 other = hg.repository(cmdutil.remoteui(repo, opts), dest)
231 other = hg.repository(cmdutil.remoteui(repo, opts), dest)
232 ui.status(_('comparing with %s\n') % dest)
232 ui.status(_('comparing with %s\n') % dest)
233 o = repo.findoutgoing(other)
233 o = repo.findoutgoing(other)
234 if not o:
234 if not o:
235 ui.status(_("no changes found\n"))
235 ui.status(_("no changes found\n"))
236 return []
236 return []
237 o = repo.changelog.nodesbetween(o, revs or None)[0]
237 o = repo.changelog.nodesbetween(o, revs or None)[0]
238 return [str(repo.changelog.rev(r)) for r in o]
238 return [str(repo.changelog.rev(r)) for r in o]
239
239
240 def getpatches(revs):
240 def getpatches(revs):
241 for r in cmdutil.revrange(repo, revs):
241 for r in cmdutil.revrange(repo, revs):
242 output = cStringIO.StringIO()
242 output = cStringIO.StringIO()
243 patch.export(repo, [r], fp=output,
243 patch.export(repo, [r], fp=output,
244 opts=patch.diffopts(ui, opts))
244 opts=patch.diffopts(ui, opts))
245 yield output.getvalue().split('\n')
245 yield output.getvalue().split('\n')
246
246
247 def getbundle(dest):
247 def getbundle(dest):
248 tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-')
248 tmpdir = tempfile.mkdtemp(prefix='hg-email-bundle-')
249 tmpfn = os.path.join(tmpdir, 'bundle')
249 tmpfn = os.path.join(tmpdir, 'bundle')
250 try:
250 try:
251 commands.bundle(ui, repo, tmpfn, dest, **opts)
251 commands.bundle(ui, repo, tmpfn, dest, **opts)
252 return open(tmpfn, 'rb').read()
252 return open(tmpfn, 'rb').read()
253 finally:
253 finally:
254 try:
254 try:
255 os.unlink(tmpfn)
255 os.unlink(tmpfn)
256 except:
256 except:
257 pass
257 pass
258 os.rmdir(tmpdir)
258 os.rmdir(tmpdir)
259
259
260 if not (opts.get('test') or opts.get('mbox')):
260 if not (opts.get('test') or opts.get('mbox')):
261 # really sending
261 # really sending
262 mail.validateconfig(ui)
262 mail.validateconfig(ui)
263
263
264 if not (revs or opts.get('rev')
264 if not (revs or opts.get('rev')
265 or opts.get('outgoing') or opts.get('bundle')
265 or opts.get('outgoing') or opts.get('bundle')
266 or opts.get('patches')):
266 or opts.get('patches')):
267 raise util.Abort(_('specify at least one changeset with -r or -o'))
267 raise util.Abort(_('specify at least one changeset with -r or -o'))
268
268
269 if opts.get('outgoing') and opts.get('bundle'):
269 if opts.get('outgoing') and opts.get('bundle'):
270 raise util.Abort(_("--outgoing mode always on with --bundle;"
270 raise util.Abort(_("--outgoing mode always on with --bundle;"
271 " do not re-specify --outgoing"))
271 " do not re-specify --outgoing"))
272
272
273 if opts.get('outgoing') or opts.get('bundle'):
273 if opts.get('outgoing') or opts.get('bundle'):
274 if len(revs) > 1:
274 if len(revs) > 1:
275 raise util.Abort(_("too many destinations"))
275 raise util.Abort(_("too many destinations"))
276 dest = revs and revs[0] or None
276 dest = revs and revs[0] or None
277 revs = []
277 revs = []
278
278
279 if opts.get('rev'):
279 if opts.get('rev'):
280 if revs:
280 if revs:
281 raise util.Abort(_('use only one form to specify the revision'))
281 raise util.Abort(_('use only one form to specify the revision'))
282 revs = opts.get('rev')
282 revs = opts.get('rev')
283
283
284 if opts.get('outgoing'):
284 if opts.get('outgoing'):
285 revs = outgoing(dest, opts.get('rev'))
285 revs = outgoing(dest, opts.get('rev'))
286 if opts.get('bundle'):
286 if opts.get('bundle'):
287 opts['revs'] = revs
287 opts['revs'] = revs
288
288
289 # start
289 # start
290 if opts.get('date'):
290 if opts.get('date'):
291 start_time = util.parsedate(opts.get('date'))
291 start_time = util.parsedate(opts.get('date'))
292 else:
292 else:
293 start_time = util.makedate()
293 start_time = util.makedate()
294
294
295 def genmsgid(id):
295 def genmsgid(id):
296 return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn())
296 return '<%s.%s@%s>' % (id[:20], int(start_time[0]), socket.getfqdn())
297
297
298 def getdescription(body, sender):
298 def getdescription(body, sender):
299 if opts.get('desc'):
299 if opts.get('desc'):
300 body = open(opts.get('desc')).read()
300 body = open(opts.get('desc')).read()
301 else:
301 else:
302 ui.write(_('\nWrite the introductory message for the '
302 ui.write(_('\nWrite the introductory message for the '
303 'patch series.\n\n'))
303 'patch series.\n\n'))
304 body = ui.edit(body, sender)
304 body = ui.edit(body, sender)
305 return body
305 return body
306
306
307 def getpatchmsgs(patches, patchnames=None):
307 def getpatchmsgs(patches, patchnames=None):
308 jumbo = []
308 jumbo = []
309 msgs = []
309 msgs = []
310
310
311 ui.write(_('This patch series consists of %d patches.\n\n')
311 ui.write(_('This patch series consists of %d patches.\n\n')
312 % len(patches))
312 % len(patches))
313
313
314 name = None
314 name = None
315 for i, p in enumerate(patches):
315 for i, p in enumerate(patches):
316 jumbo.extend(p)
316 jumbo.extend(p)
317 if patchnames:
317 if patchnames:
318 name = patchnames[i]
318 name = patchnames[i]
319 msg = makepatch(ui, repo, p, opts, _charsets, i + 1,
319 msg = makepatch(ui, repo, p, opts, _charsets, i + 1,
320 len(patches), name)
320 len(patches), name)
321 msgs.append(msg)
321 msgs.append(msg)
322
322
323 if len(patches) > 1 or opts.get('intro'):
323 if len(patches) > 1 or opts.get('intro'):
324 tlen = len(str(len(patches)))
324 tlen = len(str(len(patches)))
325
325
326 subj = '[PATCH %0*d of %d] %s' % (
326 subj = '[PATCH %0*d of %d] %s' % (
327 tlen, 0, len(patches),
327 tlen, 0, len(patches),
328 opts.get('subject') or
328 opts.get('subject') or
329 prompt(ui, 'Subject:',
329 prompt(ui, 'Subject:',
330 rest=' [PATCH %0*d of %d] ' % (tlen, 0, len(patches))))
330 rest=' [PATCH %0*d of %d] ' % (tlen, 0, len(patches))))
331
331
332 body = ''
332 body = ''
333 if opts.get('diffstat'):
333 if opts.get('diffstat'):
334 d = cdiffstat(ui, _('Final summary:\n'), jumbo)
334 d = cdiffstat(ui, _('Final summary:\n'), jumbo)
335 if d:
335 if d:
336 body = '\n' + d
336 body = '\n' + d
337
337
338 body = getdescription(body, sender)
338 body = getdescription(body, sender)
339 msg = mail.mimeencode(ui, body, _charsets, opts.get('test'))
339 msg = mail.mimeencode(ui, body, _charsets, opts.get('test'))
340 msg['Subject'] = mail.headencode(ui, subj, _charsets,
340 msg['Subject'] = mail.headencode(ui, subj, _charsets,
341 opts.get('test'))
341 opts.get('test'))
342
342
343 msgs.insert(0, (msg, subj))
343 msgs.insert(0, (msg, subj))
344 return msgs
344 return msgs
345
345
346 def getbundlemsgs(bundle):
346 def getbundlemsgs(bundle):
347 subj = (opts.get('subject')
347 subj = (opts.get('subject')
348 or prompt(ui, 'Subject:', 'A bundle for your repository'))
348 or prompt(ui, 'Subject:', 'A bundle for your repository'))
349
349
350 body = getdescription('', sender)
350 body = getdescription('', sender)
351 msg = email.MIMEMultipart.MIMEMultipart()
351 msg = email.MIMEMultipart.MIMEMultipart()
352 if body:
352 if body:
353 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
353 msg.attach(mail.mimeencode(ui, body, _charsets, opts.get('test')))
354 datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
354 datapart = email.MIMEBase.MIMEBase('application', 'x-mercurial-bundle')
355 datapart.set_payload(bundle)
355 datapart.set_payload(bundle)
356 bundlename = '%s.hg' % opts.get('bundlename', 'bundle')
356 bundlename = '%s.hg' % opts.get('bundlename', 'bundle')
357 datapart.add_header('Content-Disposition', 'attachment',
357 datapart.add_header('Content-Disposition', 'attachment',
358 filename=bundlename)
358 filename=bundlename)
359 email.Encoders.encode_base64(datapart)
359 email.Encoders.encode_base64(datapart)
360 msg.attach(datapart)
360 msg.attach(datapart)
361 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
361 msg['Subject'] = mail.headencode(ui, subj, _charsets, opts.get('test'))
362 return [(msg, subj)]
362 return [(msg, subj)]
363
363
364 sender = (opts.get('from') or ui.config('email', 'from') or
364 sender = (opts.get('from') or ui.config('email', 'from') or
365 ui.config('patchbomb', 'from') or
365 ui.config('patchbomb', 'from') or
366 prompt(ui, 'From', ui.username()))
366 prompt(ui, 'From', ui.username()))
367
367
368 # internal option used by pbranches
368 # internal option used by pbranches
369 patches = opts.get('patches')
369 patches = opts.get('patches')
370 if patches:
370 if patches:
371 msgs = getpatchmsgs(patches, opts.get('patchnames'))
371 msgs = getpatchmsgs(patches, opts.get('patchnames'))
372 elif opts.get('bundle'):
372 elif opts.get('bundle'):
373 msgs = getbundlemsgs(getbundle(dest))
373 msgs = getbundlemsgs(getbundle(dest))
374 else:
374 else:
375 msgs = getpatchmsgs(list(getpatches(revs)))
375 msgs = getpatchmsgs(list(getpatches(revs)))
376
376
377 def getaddrs(opt, prpt, default = None):
377 def getaddrs(opt, prpt, default = None):
378 addrs = opts.get(opt) or (ui.config('email', opt) or
378 addrs = opts.get(opt) or (ui.config('email', opt) or
379 ui.config('patchbomb', opt) or
379 ui.config('patchbomb', opt) or
380 prompt(ui, prpt, default)).split(',')
380 prompt(ui, prpt, default)).split(',')
381 return [mail.addressencode(ui, a.strip(), _charsets, opts.get('test'))
381 return [mail.addressencode(ui, a.strip(), _charsets, opts.get('test'))
382 for a in addrs if a.strip()]
382 for a in addrs if a.strip()]
383
383
384 to = getaddrs('to', 'To')
384 to = getaddrs('to', 'To')
385 cc = getaddrs('cc', 'Cc', '')
385 cc = getaddrs('cc', 'Cc', '')
386
386
387 bcc = opts.get('bcc') or (ui.config('email', 'bcc') or
387 bcc = opts.get('bcc') or (ui.config('email', 'bcc') or
388 ui.config('patchbomb', 'bcc') or '').split(',')
388 ui.config('patchbomb', 'bcc') or '').split(',')
389 bcc = [mail.addressencode(ui, a.strip(), _charsets, opts.get('test'))
389 bcc = [mail.addressencode(ui, a.strip(), _charsets, opts.get('test'))
390 for a in bcc if a.strip()]
390 for a in bcc if a.strip()]
391
391
392 ui.write('\n')
392 ui.write('\n')
393
393
394 parent = opts.get('in_reply_to') or None
394 parent = opts.get('in_reply_to') or None
395 # angle brackets may be omitted, they're not semantically part of the msg-id
395 # angle brackets may be omitted, they're not semantically part of the msg-id
396 if parent is not None:
396 if parent is not None:
397 if not parent.startswith('<'):
397 if not parent.startswith('<'):
398 parent = '<' + parent
398 parent = '<' + parent
399 if not parent.endswith('>'):
399 if not parent.endswith('>'):
400 parent += '>'
400 parent += '>'
401
401
402 first = True
402 first = True
403
403
404 sender_addr = email.Utils.parseaddr(sender)[1]
404 sender_addr = email.Utils.parseaddr(sender)[1]
405 sender = mail.addressencode(ui, sender, _charsets, opts.get('test'))
405 sender = mail.addressencode(ui, sender, _charsets, opts.get('test'))
406 sendmail = None
406 sendmail = None
407 for m, subj in msgs:
407 for m, subj in msgs:
408 try:
408 try:
409 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
409 m['Message-Id'] = genmsgid(m['X-Mercurial-Node'])
410 except TypeError:
410 except TypeError:
411 m['Message-Id'] = genmsgid('patchbomb')
411 m['Message-Id'] = genmsgid('patchbomb')
412 if parent:
412 if parent:
413 m['In-Reply-To'] = parent
413 m['In-Reply-To'] = parent
414 m['References'] = parent
414 m['References'] = parent
415 if first:
415 if first:
416 parent = m['Message-Id']
416 parent = m['Message-Id']
417 first = False
417 first = False
418
418
419 m['User-Agent'] = 'Mercurial-patchbomb/%s' % util.version()
419 m['User-Agent'] = 'Mercurial-patchbomb/%s' % util.version()
420 m['Date'] = email.Utils.formatdate(start_time[0])
420 m['Date'] = email.Utils.formatdate(start_time[0])
421
421
422 start_time = (start_time[0] + 1, start_time[1])
422 start_time = (start_time[0] + 1, start_time[1])
423 m['From'] = sender
423 m['From'] = sender
424 m['To'] = ', '.join(to)
424 m['To'] = ', '.join(to)
425 if cc:
425 if cc:
426 m['Cc'] = ', '.join(cc)
426 m['Cc'] = ', '.join(cc)
427 if bcc:
427 if bcc:
428 m['Bcc'] = ', '.join(bcc)
428 m['Bcc'] = ', '.join(bcc)
429 if opts.get('test'):
429 if opts.get('test'):
430 ui.status(_('Displaying '), subj, ' ...\n')
430 ui.status(_('Displaying '), subj, ' ...\n')
431 ui.flush()
431 ui.flush()
432 if 'PAGER' in os.environ:
432 if 'PAGER' in os.environ:
433 fp = util.popen(os.environ['PAGER'], 'w')
433 fp = util.popen(os.environ['PAGER'], 'w')
434 else:
434 else:
435 fp = ui
435 fp = ui
436 generator = email.Generator.Generator(fp, mangle_from_=False)
436 generator = email.Generator.Generator(fp, mangle_from_=False)
437 try:
437 try:
438 generator.flatten(m, 0)
438 generator.flatten(m, 0)
439 fp.write('\n')
439 fp.write('\n')
440 except IOError, inst:
440 except IOError, inst:
441 if inst.errno != errno.EPIPE:
441 if inst.errno != errno.EPIPE:
442 raise
442 raise
443 if fp is not ui:
443 if fp is not ui:
444 fp.close()
444 fp.close()
445 elif opts.get('mbox'):
445 elif opts.get('mbox'):
446 ui.status(_('Writing '), subj, ' ...\n')
446 ui.status(_('Writing '), subj, ' ...\n')
447 fp = open(opts.get('mbox'), 'In-Reply-To' in m and 'ab+' or 'wb+')
447 fp = open(opts.get('mbox'), 'In-Reply-To' in m and 'ab+' or 'wb+')
448 generator = email.Generator.Generator(fp, mangle_from_=True)
448 generator = email.Generator.Generator(fp, mangle_from_=True)
449 date = util.datestr(start_time, '%a %b %d %H:%M:%S %Y')
449 date = util.datestr(start_time, '%a %b %d %H:%M:%S %Y')
450 fp.write('From %s %s\n' % (sender_addr, date))
450 fp.write('From %s %s\n' % (sender_addr, date))
451 generator.flatten(m, 0)
451 generator.flatten(m, 0)
452 fp.write('\n\n')
452 fp.write('\n\n')
453 fp.close()
453 fp.close()
454 else:
454 else:
455 if not sendmail:
455 if not sendmail:
456 sendmail = mail.connect(ui)
456 sendmail = mail.connect(ui)
457 ui.status(_('Sending '), subj, ' ...\n')
457 ui.status(_('Sending '), subj, ' ...\n')
458 # Exim does not remove the Bcc field
458 # Exim does not remove the Bcc field
459 del m['Bcc']
459 del m['Bcc']
460 fp = cStringIO.StringIO()
460 fp = cStringIO.StringIO()
461 generator = email.Generator.Generator(fp, mangle_from_=False)
461 generator = email.Generator.Generator(fp, mangle_from_=False)
462 generator.flatten(m, 0)
462 generator.flatten(m, 0)
463 sendmail(sender, to + bcc + cc, fp.getvalue())
463 sendmail(sender, to + bcc + cc, fp.getvalue())
464
464
465 emailopts = [
465 emailopts = [
466 ('a', 'attach', None, _('send patches as attachments')),
466 ('a', 'attach', None, _('send patches as attachments')),
467 ('i', 'inline', None, _('send patches as inline attachments')),
467 ('i', 'inline', None, _('send patches as inline attachments')),
468 ('', 'bcc', [], _('email addresses of blind carbon copy recipients')),
468 ('', 'bcc', [], _('email addresses of blind carbon copy recipients')),
469 ('c', 'cc', [], _('email addresses of copy recipients')),
469 ('c', 'cc', [], _('email addresses of copy recipients')),
470 ('d', 'diffstat', None, _('add diffstat output to messages')),
470 ('d', 'diffstat', None, _('add diffstat output to messages')),
471 ('', 'date', '', _('use the given date as the sending date')),
471 ('', 'date', '', _('use the given date as the sending date')),
472 ('', 'desc', '', _('use the given file as the series description')),
472 ('', 'desc', '', _('use the given file as the series description')),
473 ('f', 'from', '', _('email address of sender')),
473 ('f', 'from', '', _('email address of sender')),
474 ('n', 'test', None, _('print messages that would be sent')),
474 ('n', 'test', None, _('print messages that would be sent')),
475 ('m', 'mbox', '',
475 ('m', 'mbox', '',
476 _('write messages to mbox file instead of sending them')),
476 _('write messages to mbox file instead of sending them')),
477 ('s', 'subject', '',
477 ('s', 'subject', '',
478 _('subject of first message (intro or single patch)')),
478 _('subject of first message (intro or single patch)')),
479 ('', 'in-reply-to', '',
479 ('', 'in-reply-to', '',
480 _('message identifier to reply to')),
480 _('message identifier to reply to')),
481 ('t', 'to', [], _('email addresses of recipients')),
481 ('t', 'to', [], _('email addresses of recipients')),
482 ]
482 ]
483
483
484
484
485 cmdtable = {
485 cmdtable = {
486 "email":
486 "email":
487 (patchbomb,
487 (patchbomb,
488 [('g', 'git', None, _('use git extended diff format')),
488 [('g', 'git', None, _('use git extended diff format')),
489 ('', 'plain', None, _('omit hg patch header')),
489 ('', 'plain', None, _('omit hg patch header')),
490 ('o', 'outgoing', None,
490 ('o', 'outgoing', None,
491 _('send changes not found in the target repository')),
491 _('send changes not found in the target repository')),
492 ('b', 'bundle', None,
492 ('b', 'bundle', None,
493 _('send changes not in target as a binary bundle')),
493 _('send changes not in target as a binary bundle')),
494 ('', 'bundlename', 'bundle',
494 ('', 'bundlename', 'bundle',
495 _('name of the bundle attachment file')),
495 _('name of the bundle attachment file')),
496 ('r', 'rev', [], _('a revision to send')),
496 ('r', 'rev', [], _('a revision to send')),
497 ('', 'force', None,
497 ('', 'force', None,
498 _('run even when remote repository is unrelated '
498 _('run even when remote repository is unrelated '
499 '(with -b/--bundle)')),
499 '(with -b/--bundle)')),
500 ('', 'base', [],
500 ('', 'base', [],
501 _('a base changeset to specify instead of a destination '
501 _('a base changeset to specify instead of a destination '
502 '(with -b/--bundle)')),
502 '(with -b/--bundle)')),
503 ('', 'intro', None,
503 ('', 'intro', None,
504 _('send an introduction email for a single patch')),
504 _('send an introduction email for a single patch')),
505 ] + emailopts + commands.remoteopts,
505 ] + emailopts + commands.remoteopts,
506 _('hg email [OPTION]... [DEST]...'))
506 _('hg email [OPTION]... [DEST]...'))
507 }
507 }
@@ -1,502 +1,502 b''
1 % help
1 % help
2 keyword extension - expand keywords in tracked files
2 keyword extension - expand keywords in tracked files
3
3
4 This extension expands RCS/CVS-like or self-customized $Keywords$ in
4 This extension expands RCS/CVS-like or self-customized $Keywords$ in
5 tracked text files selected by your configuration.
5 tracked text files selected by your configuration.
6
6
7 Keywords are only expanded in local repositories and not stored in the
7 Keywords are only expanded in local repositories and not stored in the
8 change history. The mechanism can be regarded as a convenience for the
8 change history. The mechanism can be regarded as a convenience for the
9 current user or for archive distribution.
9 current user or for archive distribution.
10
10
11 Configuration is done in the [keyword] and [keywordmaps] sections of
11 Configuration is done in the [keyword] and [keywordmaps] sections of
12 hgrc files.
12 hgrc files.
13
13
14 Example:
14 Example:
15
15
16 [keyword]
16 [keyword]
17 # expand keywords in every python file except those matching "x*"
17 # expand keywords in every python file except those matching "x*"
18 **.py =
18 **.py =
19 x* = ignore
19 x* = ignore
20
20
21 Note: the more specific you are in your filename patterns
21 Note: the more specific you are in your filename patterns
22 the less you lose speed in huge repositories.
22 the less you lose speed in huge repositories.
23
23
24 For [keywordmaps] template mapping and expansion demonstration and
24 For [keywordmaps] template mapping and expansion demonstration and
25 control run "hg kwdemo".
25 control run "hg kwdemo".
26
26
27 An additional date template filter {date|utcdate} is provided.
27 An additional date template filter {date|utcdate} is provided.
28
28
29 The default template mappings (view with "hg kwdemo -d") can be
29 The default template mappings (view with "hg kwdemo -d") can be
30 replaced with customized keywords and templates. Again, run "hg
30 replaced with customized keywords and templates. Again, run "hg
31 kwdemo" to control the results of your config changes.
31 kwdemo" to control the results of your config changes.
32
32
33 Before changing/disabling active keywords, run "hg kwshrink" to avoid
33 Before changing/disabling active keywords, run "hg kwshrink" to avoid
34 the risk of inadvertently storing expanded keywords in the change
34 the risk of inadvertently storing expanded keywords in the change
35 history.
35 history.
36
36
37 To force expansion after enabling it, or a configuration change, run
37 To force expansion after enabling it, or a configuration change, run
38 "hg kwexpand".
38 "hg kwexpand".
39
39
40 Also, when committing with the record extension or using mq's qrecord,
40 Also, when committing with the record extension or using mq's qrecord,
41 be aware that keywords cannot be updated. Again, run "hg kwexpand" on
41 be aware that keywords cannot be updated. Again, run "hg kwexpand" on
42 the files in question to update keyword expansions after all changes
42 the files in question to update keyword expansions after all changes
43 have been checked in.
43 have been checked in.
44
44
45 Expansions spanning more than one line and incremental expansions,
45 Expansions spanning more than one line and incremental expansions,
46 like CVS' $Log$, are not supported. A keyword template map
46 like CVS' $Log$, are not supported. A keyword template map
47 "Log = {desc}" expands to the first line of the changeset description.
47 "Log = {desc}" expands to the first line of the changeset description.
48
48
49 list of commands:
49 list of commands:
50
50
51 kwdemo print [keywordmaps] configuration and an expansion example
51 kwdemo print [keywordmaps] configuration and an expansion example
52 kwexpand expand keywords in the working directory
52 kwexpand expand keywords in the working directory
53 kwfiles print files currently configured for keyword expansion
53 kwfiles print files currently configured for keyword expansion
54 kwshrink revert expanded keywords in the working directory
54 kwshrink revert expanded keywords in the working directory
55
55
56 enabled extensions:
56 enabled extensions:
57
57
58 keyword expand keywords in tracked files
58 keyword expand keywords in tracked files
59 mq manage a stack of patches
59 mq manage a stack of patches
60 notify send e-mail notifications for commits/pushes
60 notify hooks for sending email notifications at commit/push time
61
61
62 use "hg -v help keyword" to show aliases and global options
62 use "hg -v help keyword" to show aliases and global options
63 % hg kwdemo
63 % hg kwdemo
64 [extensions]
64 [extensions]
65 hgext.keyword =
65 hgext.keyword =
66 [keyword]
66 [keyword]
67 * =
67 * =
68 b = ignore
68 b = ignore
69 demo.txt =
69 demo.txt =
70 [keywordmaps]
70 [keywordmaps]
71 RCSFile = {file|basename},v
71 RCSFile = {file|basename},v
72 Author = {author|user}
72 Author = {author|user}
73 Header = {root}/{file},v {node|short} {date|utcdate} {author|user}
73 Header = {root}/{file},v {node|short} {date|utcdate} {author|user}
74 Source = {root}/{file},v
74 Source = {root}/{file},v
75 Date = {date|utcdate}
75 Date = {date|utcdate}
76 Id = {file|basename},v {node|short} {date|utcdate} {author|user}
76 Id = {file|basename},v {node|short} {date|utcdate} {author|user}
77 Revision = {node|short}
77 Revision = {node|short}
78 $RCSFile: demo.txt,v $
78 $RCSFile: demo.txt,v $
79 $Author: test $
79 $Author: test $
80 $Header: /TMP/demo.txt,v xxxxxxxxxxxx 2000/00/00 00:00:00 test $
80 $Header: /TMP/demo.txt,v xxxxxxxxxxxx 2000/00/00 00:00:00 test $
81 $Source: /TMP/demo.txt,v $
81 $Source: /TMP/demo.txt,v $
82 $Date: 2000/00/00 00:00:00 $
82 $Date: 2000/00/00 00:00:00 $
83 $Id: demo.txt,v xxxxxxxxxxxx 2000/00/00 00:00:00 test $
83 $Id: demo.txt,v xxxxxxxxxxxx 2000/00/00 00:00:00 test $
84 $Revision: xxxxxxxxxxxx $
84 $Revision: xxxxxxxxxxxx $
85 [extensions]
85 [extensions]
86 hgext.keyword =
86 hgext.keyword =
87 [keyword]
87 [keyword]
88 * =
88 * =
89 b = ignore
89 b = ignore
90 demo.txt =
90 demo.txt =
91 [keywordmaps]
91 [keywordmaps]
92 Branch = {branches}
92 Branch = {branches}
93 $Branch: demobranch $
93 $Branch: demobranch $
94 % kwshrink should exit silently in empty/invalid repo
94 % kwshrink should exit silently in empty/invalid repo
95 pulling from test-keyword.hg
95 pulling from test-keyword.hg
96 requesting all changes
96 requesting all changes
97 adding changesets
97 adding changesets
98 adding manifests
98 adding manifests
99 adding file changes
99 adding file changes
100 added 1 changesets with 1 changes to 1 files
100 added 1 changesets with 1 changes to 1 files
101 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
101 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
102 % cat
102 % cat
103 expand $Id$
103 expand $Id$
104 do not process $Id:
104 do not process $Id:
105 xxx $
105 xxx $
106 ignore $Id$
106 ignore $Id$
107 % addremove
107 % addremove
108 adding a
108 adding a
109 adding b
109 adding b
110 % status
110 % status
111 A a
111 A a
112 A b
112 A b
113 % default keyword expansion including commit hook
113 % default keyword expansion including commit hook
114 % interrupted commit should not change state or run commit hook
114 % interrupted commit should not change state or run commit hook
115 abort: empty commit message
115 abort: empty commit message
116 % status
116 % status
117 A a
117 A a
118 A b
118 A b
119 % commit
119 % commit
120 a
120 a
121 b
121 b
122 overwriting a expanding keywords
122 overwriting a expanding keywords
123 running hook commit.test: cp a hooktest
123 running hook commit.test: cp a hooktest
124 committed changeset 1:ef63ca68695bc9495032c6fda1350c71e6d256e9
124 committed changeset 1:ef63ca68695bc9495032c6fda1350c71e6d256e9
125 % status
125 % status
126 ? hooktest
126 ? hooktest
127 % identify
127 % identify
128 ef63ca68695b
128 ef63ca68695b
129 % cat
129 % cat
130 expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $
130 expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $
131 do not process $Id:
131 do not process $Id:
132 xxx $
132 xxx $
133 ignore $Id$
133 ignore $Id$
134 % hg cat
134 % hg cat
135 expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $
135 expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $
136 do not process $Id:
136 do not process $Id:
137 xxx $
137 xxx $
138 ignore $Id$
138 ignore $Id$
139 a
139 a
140 % diff a hooktest
140 % diff a hooktest
141 % removing commit hook from config
141 % removing commit hook from config
142 % bundle
142 % bundle
143 2 changesets found
143 2 changesets found
144 % notify on pull to check whether keywords stay as is in email
144 % notify on pull to check whether keywords stay as is in email
145 % ie. if patch.diff wrapper acts as it should
145 % ie. if patch.diff wrapper acts as it should
146 % pull from bundle
146 % pull from bundle
147 pulling from ../kw.hg
147 pulling from ../kw.hg
148 requesting all changes
148 requesting all changes
149 adding changesets
149 adding changesets
150 adding manifests
150 adding manifests
151 adding file changes
151 adding file changes
152 added 2 changesets with 3 changes to 3 files
152 added 2 changesets with 3 changes to 3 files
153
153
154 diff -r 000000000000 -r a2392c293916 sym
154 diff -r 000000000000 -r a2392c293916 sym
155 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
155 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
156 +++ b/sym Sat Feb 09 20:25:47 2008 +0100
156 +++ b/sym Sat Feb 09 20:25:47 2008 +0100
157 @@ -0,0 +1,1 @@
157 @@ -0,0 +1,1 @@
158 +a
158 +a
159 \ No newline at end of file
159 \ No newline at end of file
160
160
161 diff -r a2392c293916 -r ef63ca68695b a
161 diff -r a2392c293916 -r ef63ca68695b a
162 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
162 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
163 +++ b/a Thu Jan 01 00:00:00 1970 +0000
163 +++ b/a Thu Jan 01 00:00:00 1970 +0000
164 @@ -0,0 +1,3 @@
164 @@ -0,0 +1,3 @@
165 +expand $Id$
165 +expand $Id$
166 +do not process $Id:
166 +do not process $Id:
167 +xxx $
167 +xxx $
168 diff -r a2392c293916 -r ef63ca68695b b
168 diff -r a2392c293916 -r ef63ca68695b b
169 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
169 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
170 +++ b/b Thu Jan 01 00:00:00 1970 +0000
170 +++ b/b Thu Jan 01 00:00:00 1970 +0000
171 @@ -0,0 +1,1 @@
171 @@ -0,0 +1,1 @@
172 +ignore $Id$
172 +ignore $Id$
173 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
173 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
174 % remove notify config
174 % remove notify config
175 % touch
175 % touch
176 % status
176 % status
177 % update
177 % update
178 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
178 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
179 % cat
179 % cat
180 expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $
180 expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $
181 do not process $Id:
181 do not process $Id:
182 xxx $
182 xxx $
183 ignore $Id$
183 ignore $Id$
184 % check whether expansion is filewise
184 % check whether expansion is filewise
185 % commit c
185 % commit c
186 adding c
186 adding c
187 % force expansion
187 % force expansion
188 overwriting a expanding keywords
188 overwriting a expanding keywords
189 overwriting c expanding keywords
189 overwriting c expanding keywords
190 % compare changenodes in a c
190 % compare changenodes in a c
191 expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $
191 expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $
192 do not process $Id:
192 do not process $Id:
193 xxx $
193 xxx $
194 $Id: c,v 40a904bbbe4c 1970/01/01 00:00:01 user $
194 $Id: c,v 40a904bbbe4c 1970/01/01 00:00:01 user $
195 tests for different changenodes
195 tests for different changenodes
196 % qinit -c
196 % qinit -c
197 % qimport
197 % qimport
198 % qcommit
198 % qcommit
199 % keywords should not be expanded in patch
199 % keywords should not be expanded in patch
200 # HG changeset patch
200 # HG changeset patch
201 # User User Name <user@example.com>
201 # User User Name <user@example.com>
202 # Date 1 0
202 # Date 1 0
203 # Node ID 40a904bbbe4cd4ab0a1f28411e35db26341a40ad
203 # Node ID 40a904bbbe4cd4ab0a1f28411e35db26341a40ad
204 # Parent ef63ca68695bc9495032c6fda1350c71e6d256e9
204 # Parent ef63ca68695bc9495032c6fda1350c71e6d256e9
205 cndiff
205 cndiff
206
206
207 diff -r ef63ca68695b -r 40a904bbbe4c c
207 diff -r ef63ca68695b -r 40a904bbbe4c c
208 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
208 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
209 +++ b/c Thu Jan 01 00:00:01 1970 +0000
209 +++ b/c Thu Jan 01 00:00:01 1970 +0000
210 @@ -0,0 +1,2 @@
210 @@ -0,0 +1,2 @@
211 +$Id$
211 +$Id$
212 +tests for different changenodes
212 +tests for different changenodes
213 % qpop
213 % qpop
214 patch queue now empty
214 patch queue now empty
215 % qgoto - should imply qpush
215 % qgoto - should imply qpush
216 applying mqtest.diff
216 applying mqtest.diff
217 now at: mqtest.diff
217 now at: mqtest.diff
218 % cat
218 % cat
219 $Id: c,v 40a904bbbe4c 1970/01/01 00:00:01 user $
219 $Id: c,v 40a904bbbe4c 1970/01/01 00:00:01 user $
220 tests for different changenodes
220 tests for different changenodes
221 % qpop and move on
221 % qpop and move on
222 patch queue now empty
222 patch queue now empty
223 % copy
223 % copy
224 % kwfiles added
224 % kwfiles added
225 a
225 a
226 c
226 c
227 % commit
227 % commit
228 c
228 c
229 c: copy a:0045e12f6c5791aac80ca6cbfd97709a88307292
229 c: copy a:0045e12f6c5791aac80ca6cbfd97709a88307292
230 overwriting c expanding keywords
230 overwriting c expanding keywords
231 committed changeset 2:e22d299ac0c2bd8897b3df5114374b9e4d4ca62f
231 committed changeset 2:e22d299ac0c2bd8897b3df5114374b9e4d4ca62f
232 % cat a c
232 % cat a c
233 expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $
233 expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $
234 do not process $Id:
234 do not process $Id:
235 xxx $
235 xxx $
236 expand $Id: c,v e22d299ac0c2 1970/01/01 00:00:01 user $
236 expand $Id: c,v e22d299ac0c2 1970/01/01 00:00:01 user $
237 do not process $Id:
237 do not process $Id:
238 xxx $
238 xxx $
239 % touch copied c
239 % touch copied c
240 % status
240 % status
241 % kwfiles
241 % kwfiles
242 a
242 a
243 c
243 c
244 % diff --rev
244 % diff --rev
245 diff -r ef63ca68695b c
245 diff -r ef63ca68695b c
246 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
246 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
247 @@ -0,0 +1,3 @@
247 @@ -0,0 +1,3 @@
248 +expand $Id$
248 +expand $Id$
249 +do not process $Id:
249 +do not process $Id:
250 +xxx $
250 +xxx $
251 % rollback
251 % rollback
252 rolling back last transaction
252 rolling back last transaction
253 % status
253 % status
254 A c
254 A c
255 % update -C
255 % update -C
256 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
256 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
257 % custom keyword expansion
257 % custom keyword expansion
258 % try with kwdemo
258 % try with kwdemo
259 [extensions]
259 [extensions]
260 hgext.keyword =
260 hgext.keyword =
261 [keyword]
261 [keyword]
262 * =
262 * =
263 b = ignore
263 b = ignore
264 demo.txt =
264 demo.txt =
265 [keywordmaps]
265 [keywordmaps]
266 Xinfo = {author}: {desc}
266 Xinfo = {author}: {desc}
267 $Xinfo: test: hg keyword config and expansion example $
267 $Xinfo: test: hg keyword config and expansion example $
268 % cat
268 % cat
269 expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $
269 expand $Id: a,v ef63ca68695b 1970/01/01 00:00:00 user $
270 do not process $Id:
270 do not process $Id:
271 xxx $
271 xxx $
272 ignore $Id$
272 ignore $Id$
273 % hg cat
273 % hg cat
274 expand $Id: a ef63ca68695b Thu, 01 Jan 1970 00:00:00 +0000 user $
274 expand $Id: a ef63ca68695b Thu, 01 Jan 1970 00:00:00 +0000 user $
275 do not process $Id:
275 do not process $Id:
276 xxx $
276 xxx $
277 ignore $Id$
277 ignore $Id$
278 a
278 a
279 % interrupted commit should not change state
279 % interrupted commit should not change state
280 abort: empty commit message
280 abort: empty commit message
281 % status
281 % status
282 M a
282 M a
283 ? c
283 ? c
284 ? log
284 ? log
285 % commit
285 % commit
286 a
286 a
287 overwriting a expanding keywords
287 overwriting a expanding keywords
288 committed changeset 2:bb948857c743469b22bbf51f7ec8112279ca5d83
288 committed changeset 2:bb948857c743469b22bbf51f7ec8112279ca5d83
289 % status
289 % status
290 ? c
290 ? c
291 % verify
291 % verify
292 checking changesets
292 checking changesets
293 checking manifests
293 checking manifests
294 crosschecking files in changesets and manifests
294 crosschecking files in changesets and manifests
295 checking files
295 checking files
296 3 files, 3 changesets, 4 total revisions
296 3 files, 3 changesets, 4 total revisions
297 % cat
297 % cat
298 expand $Id: a bb948857c743 Thu, 01 Jan 1970 00:00:02 +0000 user $
298 expand $Id: a bb948857c743 Thu, 01 Jan 1970 00:00:02 +0000 user $
299 do not process $Id:
299 do not process $Id:
300 xxx $
300 xxx $
301 $Xinfo: User Name <user@example.com>: firstline $
301 $Xinfo: User Name <user@example.com>: firstline $
302 ignore $Id$
302 ignore $Id$
303 % hg cat
303 % hg cat
304 expand $Id: a bb948857c743 Thu, 01 Jan 1970 00:00:02 +0000 user $
304 expand $Id: a bb948857c743 Thu, 01 Jan 1970 00:00:02 +0000 user $
305 do not process $Id:
305 do not process $Id:
306 xxx $
306 xxx $
307 $Xinfo: User Name <user@example.com>: firstline $
307 $Xinfo: User Name <user@example.com>: firstline $
308 ignore $Id$
308 ignore $Id$
309 a
309 a
310 % annotate
310 % annotate
311 1: expand $Id$
311 1: expand $Id$
312 1: do not process $Id:
312 1: do not process $Id:
313 1: xxx $
313 1: xxx $
314 2: $Xinfo$
314 2: $Xinfo$
315 % remove
315 % remove
316 committed changeset 3:d14c712653769de926994cf7fbb06c8fbd68f012
316 committed changeset 3:d14c712653769de926994cf7fbb06c8fbd68f012
317 % status
317 % status
318 ? c
318 ? c
319 % rollback
319 % rollback
320 rolling back last transaction
320 rolling back last transaction
321 % status
321 % status
322 R a
322 R a
323 ? c
323 ? c
324 % revert a
324 % revert a
325 % cat a
325 % cat a
326 expand $Id: a bb948857c743 Thu, 01 Jan 1970 00:00:02 +0000 user $
326 expand $Id: a bb948857c743 Thu, 01 Jan 1970 00:00:02 +0000 user $
327 do not process $Id:
327 do not process $Id:
328 xxx $
328 xxx $
329 $Xinfo: User Name <user@example.com>: firstline $
329 $Xinfo: User Name <user@example.com>: firstline $
330 % clone to test incoming
330 % clone to test incoming
331 requesting all changes
331 requesting all changes
332 adding changesets
332 adding changesets
333 adding manifests
333 adding manifests
334 adding file changes
334 adding file changes
335 added 2 changesets with 3 changes to 3 files
335 added 2 changesets with 3 changes to 3 files
336 updating working directory
336 updating working directory
337 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
337 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
338 % incoming
338 % incoming
339 comparing with test-keyword/Test
339 comparing with test-keyword/Test
340 searching for changes
340 searching for changes
341 changeset: 2:bb948857c743
341 changeset: 2:bb948857c743
342 tag: tip
342 tag: tip
343 user: User Name <user@example.com>
343 user: User Name <user@example.com>
344 date: Thu Jan 01 00:00:02 1970 +0000
344 date: Thu Jan 01 00:00:02 1970 +0000
345 summary: firstline
345 summary: firstline
346
346
347 % commit rejecttest
347 % commit rejecttest
348 a
348 a
349 overwriting a expanding keywords
349 overwriting a expanding keywords
350 committed changeset 2:85e279d709ffc28c9fdd1b868570985fc3d87082
350 committed changeset 2:85e279d709ffc28c9fdd1b868570985fc3d87082
351 % export
351 % export
352 % import
352 % import
353 applying ../rejecttest.diff
353 applying ../rejecttest.diff
354 % cat
354 % cat
355 expand $Id: a 4e0994474d25 Thu, 01 Jan 1970 00:00:03 +0000 user $ rejecttest
355 expand $Id: a 4e0994474d25 Thu, 01 Jan 1970 00:00:03 +0000 user $ rejecttest
356 do not process $Id: rejecttest
356 do not process $Id: rejecttest
357 xxx $
357 xxx $
358 $Xinfo: User Name <user@example.com>: rejects? $
358 $Xinfo: User Name <user@example.com>: rejects? $
359 ignore $Id$
359 ignore $Id$
360
360
361 % rollback
361 % rollback
362 rolling back last transaction
362 rolling back last transaction
363 % clean update
363 % clean update
364 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
364 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
365 % kwexpand/kwshrink on selected files
365 % kwexpand/kwshrink on selected files
366 % copy a x/a
366 % copy a x/a
367 % kwexpand a
367 % kwexpand a
368 overwriting a expanding keywords
368 overwriting a expanding keywords
369 % kwexpand x/a should abort
369 % kwexpand x/a should abort
370 abort: outstanding uncommitted changes
370 abort: outstanding uncommitted changes
371 x/a
371 x/a
372 x/a: copy a:779c764182ce5d43e2b1eb66ce06d7b47bfe342e
372 x/a: copy a:779c764182ce5d43e2b1eb66ce06d7b47bfe342e
373 overwriting x/a expanding keywords
373 overwriting x/a expanding keywords
374 committed changeset 3:cfa68229c1167443337266ebac453c73b1d5d16e
374 committed changeset 3:cfa68229c1167443337266ebac453c73b1d5d16e
375 % cat a
375 % cat a
376 expand $Id: x/a cfa68229c116 Thu, 01 Jan 1970 00:00:03 +0000 user $
376 expand $Id: x/a cfa68229c116 Thu, 01 Jan 1970 00:00:03 +0000 user $
377 do not process $Id:
377 do not process $Id:
378 xxx $
378 xxx $
379 $Xinfo: User Name <user@example.com>: xa $
379 $Xinfo: User Name <user@example.com>: xa $
380 % kwshrink a inside directory x
380 % kwshrink a inside directory x
381 overwriting x/a shrinking keywords
381 overwriting x/a shrinking keywords
382 % cat a
382 % cat a
383 expand $Id$
383 expand $Id$
384 do not process $Id:
384 do not process $Id:
385 xxx $
385 xxx $
386 $Xinfo$
386 $Xinfo$
387 % kwexpand nonexistent
387 % kwexpand nonexistent
388 nonexistent:
388 nonexistent:
389 % hg serve
389 % hg serve
390 % expansion
390 % expansion
391 % hgweb file
391 % hgweb file
392 200 Script output follows
392 200 Script output follows
393
393
394 expand $Id: a bb948857c743 Thu, 01 Jan 1970 00:00:02 +0000 user $
394 expand $Id: a bb948857c743 Thu, 01 Jan 1970 00:00:02 +0000 user $
395 do not process $Id:
395 do not process $Id:
396 xxx $
396 xxx $
397 $Xinfo: User Name <user@example.com>: firstline $
397 $Xinfo: User Name <user@example.com>: firstline $
398 % no expansion
398 % no expansion
399 % hgweb annotate
399 % hgweb annotate
400 200 Script output follows
400 200 Script output follows
401
401
402
402
403 user@1: expand $Id$
403 user@1: expand $Id$
404 user@1: do not process $Id:
404 user@1: do not process $Id:
405 user@1: xxx $
405 user@1: xxx $
406 user@2: $Xinfo$
406 user@2: $Xinfo$
407
407
408
408
409
409
410
410
411 % hgweb changeset
411 % hgweb changeset
412 200 Script output follows
412 200 Script output follows
413
413
414
414
415 # HG changeset patch
415 # HG changeset patch
416 # User User Name <user@example.com>
416 # User User Name <user@example.com>
417 # Date 3 0
417 # Date 3 0
418 # Node ID cfa68229c1167443337266ebac453c73b1d5d16e
418 # Node ID cfa68229c1167443337266ebac453c73b1d5d16e
419 # Parent bb948857c743469b22bbf51f7ec8112279ca5d83
419 # Parent bb948857c743469b22bbf51f7ec8112279ca5d83
420 xa
420 xa
421
421
422 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
422 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
423 +++ b/x/a Thu Jan 01 00:00:03 1970 +0000
423 +++ b/x/a Thu Jan 01 00:00:03 1970 +0000
424 @@ -0,0 +1,4 @@
424 @@ -0,0 +1,4 @@
425 +expand $Id$
425 +expand $Id$
426 +do not process $Id:
426 +do not process $Id:
427 +xxx $
427 +xxx $
428 +$Xinfo$
428 +$Xinfo$
429
429
430 % hgweb filediff
430 % hgweb filediff
431 200 Script output follows
431 200 Script output follows
432
432
433
433
434 --- a/a Thu Jan 01 00:00:00 1970 +0000
434 --- a/a Thu Jan 01 00:00:00 1970 +0000
435 +++ b/a Thu Jan 01 00:00:02 1970 +0000
435 +++ b/a Thu Jan 01 00:00:02 1970 +0000
436 @@ -1,3 +1,4 @@
436 @@ -1,3 +1,4 @@
437 expand $Id$
437 expand $Id$
438 do not process $Id:
438 do not process $Id:
439 xxx $
439 xxx $
440 +$Xinfo$
440 +$Xinfo$
441
441
442
442
443
443
444
444
445 % errors encountered
445 % errors encountered
446 % merge/resolve
446 % merge/resolve
447 % simplemerge
447 % simplemerge
448 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
448 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
449 created new head
449 created new head
450 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
450 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
451 (branch merge, don't forget to commit)
451 (branch merge, don't forget to commit)
452 $Id: m 8731e1dadc99 Thu, 01 Jan 1970 00:00:00 +0000 test $
452 $Id: m 8731e1dadc99 Thu, 01 Jan 1970 00:00:00 +0000 test $
453 foo
453 foo
454 % conflict
454 % conflict
455 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
455 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
456 created new head
456 created new head
457 merging m
457 merging m
458 warning: conflicts during merge.
458 warning: conflicts during merge.
459 merging m failed!
459 merging m failed!
460 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
460 0 files updated, 0 files merged, 0 files removed, 1 files unresolved
461 use 'hg resolve' to retry unresolved file merges or 'hg up --clean' to abandon
461 use 'hg resolve' to retry unresolved file merges or 'hg up --clean' to abandon
462 % keyword stays outside conflict zone
462 % keyword stays outside conflict zone
463 $Id$
463 $Id$
464 <<<<<<< local
464 <<<<<<< local
465 bar
465 bar
466 =======
466 =======
467 foo
467 foo
468 >>>>>>> other
468 >>>>>>> other
469 % resolve to local
469 % resolve to local
470 $Id: m 43dfd2854b5b Thu, 01 Jan 1970 00:00:00 +0000 test $
470 $Id: m 43dfd2854b5b Thu, 01 Jan 1970 00:00:00 +0000 test $
471 bar
471 bar
472 % switch off expansion
472 % switch off expansion
473 % kwshrink with unknown file u
473 % kwshrink with unknown file u
474 overwriting a shrinking keywords
474 overwriting a shrinking keywords
475 overwriting m shrinking keywords
475 overwriting m shrinking keywords
476 overwriting x/a shrinking keywords
476 overwriting x/a shrinking keywords
477 % cat
477 % cat
478 expand $Id$
478 expand $Id$
479 do not process $Id:
479 do not process $Id:
480 xxx $
480 xxx $
481 $Xinfo$
481 $Xinfo$
482 ignore $Id$
482 ignore $Id$
483 % hg cat
483 % hg cat
484 expand $Id: a bb948857c743 Thu, 01 Jan 1970 00:00:02 +0000 user $
484 expand $Id: a bb948857c743 Thu, 01 Jan 1970 00:00:02 +0000 user $
485 do not process $Id:
485 do not process $Id:
486 xxx $
486 xxx $
487 $Xinfo: User Name <user@example.com>: firstline $
487 $Xinfo: User Name <user@example.com>: firstline $
488 ignore $Id$
488 ignore $Id$
489 a
489 a
490 % cat
490 % cat
491 expand $Id$
491 expand $Id$
492 do not process $Id:
492 do not process $Id:
493 xxx $
493 xxx $
494 $Xinfo$
494 $Xinfo$
495 ignore $Id$
495 ignore $Id$
496 % hg cat
496 % hg cat
497 expand $Id$
497 expand $Id$
498 do not process $Id:
498 do not process $Id:
499 xxx $
499 xxx $
500 $Xinfo$
500 $Xinfo$
501 ignore $Id$
501 ignore $Id$
502 a
502 a
@@ -1,219 +1,219 b''
1 notify extension - send e-mail notifications for commits/pushes
1 notify extension - hooks for sending email notifications at commit/push time
2
2
3 Subscriptions can be managed through hgrc. Default mode is to print
3 Subscriptions can be managed through hgrc. 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 notify extension and enable in hgrc like this:
6 To use, configure notify extension and enable in hgrc like this:
7
7
8 [extensions]
8 [extensions]
9 hgext.notify =
9 hgext.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 in here
18 # config items go in here
19
19
20 config items:
20 config items:
21
21
22 REQUIRED:
22 REQUIRED:
23 config = /path/to/file # file containing subscriptions
23 config = /path/to/file # file containing subscriptions
24
24
25 OPTIONAL:
25 OPTIONAL:
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 [email]
38 [email]
39 from = user@host.com # email address to send as if none given
39 from = user@host.com # email address to send as if none given
40 [web]
40 [web]
41 baseurl = http://hgserver/... # root of hg web site for browsing commits
41 baseurl = http://hgserver/... # root of hg web site for browsing commits
42
42
43 notify config file has same format as regular hgrc. it has two
43 notify config file has same format as regular hgrc. it has two
44 sections so you can express subscriptions in whatever way is handier
44 sections so you can express subscriptions in whatever way is handier
45 for you.
45 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
57 if you like, you can put notify config file in repository that users
58 can push changes to, they can manage their own subscriptions.
58 can push 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 working directory
64 updating working directory
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 1 files
177 added 2 changesets with 0 changes to 1 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