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