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