##// END OF EJS Templates
changeset_templater: remove use_template method
Matt Mackall -
r20667:e96e9f80 default
parent child Browse files
Show More
@@ -1,915 +1,914
1 1 # bugzilla.py - bugzilla integration for mercurial
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 4 # Copyright 2011-2 Jim Hague <jim.hague@acm.org>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 '''hooks for integrating with the Bugzilla bug tracker
10 10
11 11 This hook extension adds comments on bugs in Bugzilla when changesets
12 12 that refer to bugs by Bugzilla ID are seen. The comment is formatted using
13 13 the Mercurial template mechanism.
14 14
15 15 The bug references can optionally include an update for Bugzilla of the
16 16 hours spent working on the bug. Bugs can also be marked fixed.
17 17
18 18 Three basic modes of access to Bugzilla are provided:
19 19
20 20 1. Access via the Bugzilla XMLRPC interface. Requires Bugzilla 3.4 or later.
21 21
22 22 2. Check data via the Bugzilla XMLRPC interface and submit bug change
23 23 via email to Bugzilla email interface. Requires Bugzilla 3.4 or later.
24 24
25 25 3. Writing directly to the Bugzilla database. Only Bugzilla installations
26 26 using MySQL are supported. Requires Python MySQLdb.
27 27
28 28 Writing directly to the database is susceptible to schema changes, and
29 29 relies on a Bugzilla contrib script to send out bug change
30 30 notification emails. This script runs as the user running Mercurial,
31 31 must be run on the host with the Bugzilla install, and requires
32 32 permission to read Bugzilla configuration details and the necessary
33 33 MySQL user and password to have full access rights to the Bugzilla
34 34 database. For these reasons this access mode is now considered
35 35 deprecated, and will not be updated for new Bugzilla versions going
36 36 forward. Only adding comments is supported in this access mode.
37 37
38 38 Access via XMLRPC needs a Bugzilla username and password to be specified
39 39 in the configuration. Comments are added under that username. Since the
40 40 configuration must be readable by all Mercurial users, it is recommended
41 41 that the rights of that user are restricted in Bugzilla to the minimum
42 42 necessary to add comments. Marking bugs fixed requires Bugzilla 4.0 and later.
43 43
44 44 Access via XMLRPC/email uses XMLRPC to query Bugzilla, but sends
45 45 email to the Bugzilla email interface to submit comments to bugs.
46 46 The From: address in the email is set to the email address of the Mercurial
47 47 user, so the comment appears to come from the Mercurial user. In the event
48 48 that the Mercurial user email is not recognized by Bugzilla as a Bugzilla
49 49 user, the email associated with the Bugzilla username used to log into
50 50 Bugzilla is used instead as the source of the comment. Marking bugs fixed
51 51 works on all supported Bugzilla versions.
52 52
53 53 Configuration items common to all access modes:
54 54
55 55 bugzilla.version
56 56 The access type to use. Values recognized are:
57 57
58 58 :``xmlrpc``: Bugzilla XMLRPC interface.
59 59 :``xmlrpc+email``: Bugzilla XMLRPC and email interfaces.
60 60 :``3.0``: MySQL access, Bugzilla 3.0 and later.
61 61 :``2.18``: MySQL access, Bugzilla 2.18 and up to but not
62 62 including 3.0.
63 63 :``2.16``: MySQL access, Bugzilla 2.16 and up to but not
64 64 including 2.18.
65 65
66 66 bugzilla.regexp
67 67 Regular expression to match bug IDs for update in changeset commit message.
68 68 It must contain one "()" named group ``<ids>`` containing the bug
69 69 IDs separated by non-digit characters. It may also contain
70 70 a named group ``<hours>`` with a floating-point number giving the
71 71 hours worked on the bug. If no named groups are present, the first
72 72 "()" group is assumed to contain the bug IDs, and work time is not
73 73 updated. The default expression matches ``Bug 1234``, ``Bug no. 1234``,
74 74 ``Bug number 1234``, ``Bugs 1234,5678``, ``Bug 1234 and 5678`` and
75 75 variations thereof, followed by an hours number prefixed by ``h`` or
76 76 ``hours``, e.g. ``hours 1.5``. Matching is case insensitive.
77 77
78 78 bugzilla.fixregexp
79 79 Regular expression to match bug IDs for marking fixed in changeset
80 80 commit message. This must contain a "()" named group ``<ids>` containing
81 81 the bug IDs separated by non-digit characters. It may also contain
82 82 a named group ``<hours>`` with a floating-point number giving the
83 83 hours worked on the bug. If no named groups are present, the first
84 84 "()" group is assumed to contain the bug IDs, and work time is not
85 85 updated. The default expression matches ``Fixes 1234``, ``Fixes bug 1234``,
86 86 ``Fixes bugs 1234,5678``, ``Fixes 1234 and 5678`` and
87 87 variations thereof, followed by an hours number prefixed by ``h`` or
88 88 ``hours``, e.g. ``hours 1.5``. Matching is case insensitive.
89 89
90 90 bugzilla.fixstatus
91 91 The status to set a bug to when marking fixed. Default ``RESOLVED``.
92 92
93 93 bugzilla.fixresolution
94 94 The resolution to set a bug to when marking fixed. Default ``FIXED``.
95 95
96 96 bugzilla.style
97 97 The style file to use when formatting comments.
98 98
99 99 bugzilla.template
100 100 Template to use when formatting comments. Overrides style if
101 101 specified. In addition to the usual Mercurial keywords, the
102 102 extension specifies:
103 103
104 104 :``{bug}``: The Bugzilla bug ID.
105 105 :``{root}``: The full pathname of the Mercurial repository.
106 106 :``{webroot}``: Stripped pathname of the Mercurial repository.
107 107 :``{hgweb}``: Base URL for browsing Mercurial repositories.
108 108
109 109 Default ``changeset {node|short} in repo {root} refers to bug
110 110 {bug}.\\ndetails:\\n\\t{desc|tabindent}``
111 111
112 112 bugzilla.strip
113 113 The number of path separator characters to strip from the front of
114 114 the Mercurial repository path (``{root}`` in templates) to produce
115 115 ``{webroot}``. For example, a repository with ``{root}``
116 116 ``/var/local/my-project`` with a strip of 2 gives a value for
117 117 ``{webroot}`` of ``my-project``. Default 0.
118 118
119 119 web.baseurl
120 120 Base URL for browsing Mercurial repositories. Referenced from
121 121 templates as ``{hgweb}``.
122 122
123 123 Configuration items common to XMLRPC+email and MySQL access modes:
124 124
125 125 bugzilla.usermap
126 126 Path of file containing Mercurial committer email to Bugzilla user email
127 127 mappings. If specified, the file should contain one mapping per
128 128 line::
129 129
130 130 committer = Bugzilla user
131 131
132 132 See also the ``[usermap]`` section.
133 133
134 134 The ``[usermap]`` section is used to specify mappings of Mercurial
135 135 committer email to Bugzilla user email. See also ``bugzilla.usermap``.
136 136 Contains entries of the form ``committer = Bugzilla user``.
137 137
138 138 XMLRPC access mode configuration:
139 139
140 140 bugzilla.bzurl
141 141 The base URL for the Bugzilla installation.
142 142 Default ``http://localhost/bugzilla``.
143 143
144 144 bugzilla.user
145 145 The username to use to log into Bugzilla via XMLRPC. Default
146 146 ``bugs``.
147 147
148 148 bugzilla.password
149 149 The password for Bugzilla login.
150 150
151 151 XMLRPC+email access mode uses the XMLRPC access mode configuration items,
152 152 and also:
153 153
154 154 bugzilla.bzemail
155 155 The Bugzilla email address.
156 156
157 157 In addition, the Mercurial email settings must be configured. See the
158 158 documentation in hgrc(5), sections ``[email]`` and ``[smtp]``.
159 159
160 160 MySQL access mode configuration:
161 161
162 162 bugzilla.host
163 163 Hostname of the MySQL server holding the Bugzilla database.
164 164 Default ``localhost``.
165 165
166 166 bugzilla.db
167 167 Name of the Bugzilla database in MySQL. Default ``bugs``.
168 168
169 169 bugzilla.user
170 170 Username to use to access MySQL server. Default ``bugs``.
171 171
172 172 bugzilla.password
173 173 Password to use to access MySQL server.
174 174
175 175 bugzilla.timeout
176 176 Database connection timeout (seconds). Default 5.
177 177
178 178 bugzilla.bzuser
179 179 Fallback Bugzilla user name to record comments with, if changeset
180 180 committer cannot be found as a Bugzilla user.
181 181
182 182 bugzilla.bzdir
183 183 Bugzilla install directory. Used by default notify. Default
184 184 ``/var/www/html/bugzilla``.
185 185
186 186 bugzilla.notify
187 187 The command to run to get Bugzilla to send bug change notification
188 188 emails. Substitutes from a map with 3 keys, ``bzdir``, ``id`` (bug
189 189 id) and ``user`` (committer bugzilla email). Default depends on
190 190 version; from 2.18 it is "cd %(bzdir)s && perl -T
191 191 contrib/sendbugmail.pl %(id)s %(user)s".
192 192
193 193 Activating the extension::
194 194
195 195 [extensions]
196 196 bugzilla =
197 197
198 198 [hooks]
199 199 # run bugzilla hook on every change pulled or pushed in here
200 200 incoming.bugzilla = python:hgext.bugzilla.hook
201 201
202 202 Example configurations:
203 203
204 204 XMLRPC example configuration. This uses the Bugzilla at
205 205 ``http://my-project.org/bugzilla``, logging in as user
206 206 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
207 207 collection of Mercurial repositories in ``/var/local/hg/repos/``,
208 208 with a web interface at ``http://my-project.org/hg``. ::
209 209
210 210 [bugzilla]
211 211 bzurl=http://my-project.org/bugzilla
212 212 user=bugmail@my-project.org
213 213 password=plugh
214 214 version=xmlrpc
215 215 template=Changeset {node|short} in {root|basename}.
216 216 {hgweb}/{webroot}/rev/{node|short}\\n
217 217 {desc}\\n
218 218 strip=5
219 219
220 220 [web]
221 221 baseurl=http://my-project.org/hg
222 222
223 223 XMLRPC+email example configuration. This uses the Bugzilla at
224 224 ``http://my-project.org/bugzilla``, logging in as user
225 225 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
226 226 collection of Mercurial repositories in ``/var/local/hg/repos/``,
227 227 with a web interface at ``http://my-project.org/hg``. Bug comments
228 228 are sent to the Bugzilla email address
229 229 ``bugzilla@my-project.org``. ::
230 230
231 231 [bugzilla]
232 232 bzurl=http://my-project.org/bugzilla
233 233 user=bugmail@my-project.org
234 234 password=plugh
235 235 version=xmlrpc
236 236 bzemail=bugzilla@my-project.org
237 237 template=Changeset {node|short} in {root|basename}.
238 238 {hgweb}/{webroot}/rev/{node|short}\\n
239 239 {desc}\\n
240 240 strip=5
241 241
242 242 [web]
243 243 baseurl=http://my-project.org/hg
244 244
245 245 [usermap]
246 246 user@emaildomain.com=user.name@bugzilladomain.com
247 247
248 248 MySQL example configuration. This has a local Bugzilla 3.2 installation
249 249 in ``/opt/bugzilla-3.2``. The MySQL database is on ``localhost``,
250 250 the Bugzilla database name is ``bugs`` and MySQL is
251 251 accessed with MySQL username ``bugs`` password ``XYZZY``. It is used
252 252 with a collection of Mercurial repositories in ``/var/local/hg/repos/``,
253 253 with a web interface at ``http://my-project.org/hg``. ::
254 254
255 255 [bugzilla]
256 256 host=localhost
257 257 password=XYZZY
258 258 version=3.0
259 259 bzuser=unknown@domain.com
260 260 bzdir=/opt/bugzilla-3.2
261 261 template=Changeset {node|short} in {root|basename}.
262 262 {hgweb}/{webroot}/rev/{node|short}\\n
263 263 {desc}\\n
264 264 strip=5
265 265
266 266 [web]
267 267 baseurl=http://my-project.org/hg
268 268
269 269 [usermap]
270 270 user@emaildomain.com=user.name@bugzilladomain.com
271 271
272 272 All the above add a comment to the Bugzilla bug record of the form::
273 273
274 274 Changeset 3b16791d6642 in repository-name.
275 275 http://my-project.org/hg/repository-name/rev/3b16791d6642
276 276
277 277 Changeset commit comment. Bug 1234.
278 278 '''
279 279
280 280 from mercurial.i18n import _
281 281 from mercurial.node import short
282 282 from mercurial import cmdutil, mail, templater, util
283 283 import re, time, urlparse, xmlrpclib
284 284
285 285 testedwith = 'internal'
286 286
287 287 class bzaccess(object):
288 288 '''Base class for access to Bugzilla.'''
289 289
290 290 def __init__(self, ui):
291 291 self.ui = ui
292 292 usermap = self.ui.config('bugzilla', 'usermap')
293 293 if usermap:
294 294 self.ui.readconfig(usermap, sections=['usermap'])
295 295
296 296 def map_committer(self, user):
297 297 '''map name of committer to Bugzilla user name.'''
298 298 for committer, bzuser in self.ui.configitems('usermap'):
299 299 if committer.lower() == user.lower():
300 300 return bzuser
301 301 return user
302 302
303 303 # Methods to be implemented by access classes.
304 304 #
305 305 # 'bugs' is a dict keyed on bug id, where values are a dict holding
306 306 # updates to bug state. Recognized dict keys are:
307 307 #
308 308 # 'hours': Value, float containing work hours to be updated.
309 309 # 'fix': If key present, bug is to be marked fixed. Value ignored.
310 310
311 311 def filter_real_bug_ids(self, bugs):
312 312 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
313 313 pass
314 314
315 315 def filter_cset_known_bug_ids(self, node, bugs):
316 316 '''remove bug IDs where node occurs in comment text from bugs.'''
317 317 pass
318 318
319 319 def updatebug(self, bugid, newstate, text, committer):
320 320 '''update the specified bug. Add comment text and set new states.
321 321
322 322 If possible add the comment as being from the committer of
323 323 the changeset. Otherwise use the default Bugzilla user.
324 324 '''
325 325 pass
326 326
327 327 def notify(self, bugs, committer):
328 328 '''Force sending of Bugzilla notification emails.
329 329
330 330 Only required if the access method does not trigger notification
331 331 emails automatically.
332 332 '''
333 333 pass
334 334
335 335 # Bugzilla via direct access to MySQL database.
336 336 class bzmysql(bzaccess):
337 337 '''Support for direct MySQL access to Bugzilla.
338 338
339 339 The earliest Bugzilla version this is tested with is version 2.16.
340 340
341 341 If your Bugzilla is version 3.4 or above, you are strongly
342 342 recommended to use the XMLRPC access method instead.
343 343 '''
344 344
345 345 @staticmethod
346 346 def sql_buglist(ids):
347 347 '''return SQL-friendly list of bug ids'''
348 348 return '(' + ','.join(map(str, ids)) + ')'
349 349
350 350 _MySQLdb = None
351 351
352 352 def __init__(self, ui):
353 353 try:
354 354 import MySQLdb as mysql
355 355 bzmysql._MySQLdb = mysql
356 356 except ImportError, err:
357 357 raise util.Abort(_('python mysql support not available: %s') % err)
358 358
359 359 bzaccess.__init__(self, ui)
360 360
361 361 host = self.ui.config('bugzilla', 'host', 'localhost')
362 362 user = self.ui.config('bugzilla', 'user', 'bugs')
363 363 passwd = self.ui.config('bugzilla', 'password')
364 364 db = self.ui.config('bugzilla', 'db', 'bugs')
365 365 timeout = int(self.ui.config('bugzilla', 'timeout', 5))
366 366 self.ui.note(_('connecting to %s:%s as %s, password %s\n') %
367 367 (host, db, user, '*' * len(passwd)))
368 368 self.conn = bzmysql._MySQLdb.connect(host=host,
369 369 user=user, passwd=passwd,
370 370 db=db,
371 371 connect_timeout=timeout)
372 372 self.cursor = self.conn.cursor()
373 373 self.longdesc_id = self.get_longdesc_id()
374 374 self.user_ids = {}
375 375 self.default_notify = "cd %(bzdir)s && ./processmail %(id)s %(user)s"
376 376
377 377 def run(self, *args, **kwargs):
378 378 '''run a query.'''
379 379 self.ui.note(_('query: %s %s\n') % (args, kwargs))
380 380 try:
381 381 self.cursor.execute(*args, **kwargs)
382 382 except bzmysql._MySQLdb.MySQLError:
383 383 self.ui.note(_('failed query: %s %s\n') % (args, kwargs))
384 384 raise
385 385
386 386 def get_longdesc_id(self):
387 387 '''get identity of longdesc field'''
388 388 self.run('select fieldid from fielddefs where name = "longdesc"')
389 389 ids = self.cursor.fetchall()
390 390 if len(ids) != 1:
391 391 raise util.Abort(_('unknown database schema'))
392 392 return ids[0][0]
393 393
394 394 def filter_real_bug_ids(self, bugs):
395 395 '''filter not-existing bugs from set.'''
396 396 self.run('select bug_id from bugs where bug_id in %s' %
397 397 bzmysql.sql_buglist(bugs.keys()))
398 398 existing = [id for (id,) in self.cursor.fetchall()]
399 399 for id in bugs.keys():
400 400 if id not in existing:
401 401 self.ui.status(_('bug %d does not exist\n') % id)
402 402 del bugs[id]
403 403
404 404 def filter_cset_known_bug_ids(self, node, bugs):
405 405 '''filter bug ids that already refer to this changeset from set.'''
406 406 self.run('''select bug_id from longdescs where
407 407 bug_id in %s and thetext like "%%%s%%"''' %
408 408 (bzmysql.sql_buglist(bugs.keys()), short(node)))
409 409 for (id,) in self.cursor.fetchall():
410 410 self.ui.status(_('bug %d already knows about changeset %s\n') %
411 411 (id, short(node)))
412 412 del bugs[id]
413 413
414 414 def notify(self, bugs, committer):
415 415 '''tell bugzilla to send mail.'''
416 416 self.ui.status(_('telling bugzilla to send mail:\n'))
417 417 (user, userid) = self.get_bugzilla_user(committer)
418 418 for id in bugs.keys():
419 419 self.ui.status(_(' bug %s\n') % id)
420 420 cmdfmt = self.ui.config('bugzilla', 'notify', self.default_notify)
421 421 bzdir = self.ui.config('bugzilla', 'bzdir',
422 422 '/var/www/html/bugzilla')
423 423 try:
424 424 # Backwards-compatible with old notify string, which
425 425 # took one string. This will throw with a new format
426 426 # string.
427 427 cmd = cmdfmt % id
428 428 except TypeError:
429 429 cmd = cmdfmt % {'bzdir': bzdir, 'id': id, 'user': user}
430 430 self.ui.note(_('running notify command %s\n') % cmd)
431 431 fp = util.popen('(%s) 2>&1' % cmd)
432 432 out = fp.read()
433 433 ret = fp.close()
434 434 if ret:
435 435 self.ui.warn(out)
436 436 raise util.Abort(_('bugzilla notify command %s') %
437 437 util.explainexit(ret)[0])
438 438 self.ui.status(_('done\n'))
439 439
440 440 def get_user_id(self, user):
441 441 '''look up numeric bugzilla user id.'''
442 442 try:
443 443 return self.user_ids[user]
444 444 except KeyError:
445 445 try:
446 446 userid = int(user)
447 447 except ValueError:
448 448 self.ui.note(_('looking up user %s\n') % user)
449 449 self.run('''select userid from profiles
450 450 where login_name like %s''', user)
451 451 all = self.cursor.fetchall()
452 452 if len(all) != 1:
453 453 raise KeyError(user)
454 454 userid = int(all[0][0])
455 455 self.user_ids[user] = userid
456 456 return userid
457 457
458 458 def get_bugzilla_user(self, committer):
459 459 '''See if committer is a registered bugzilla user. Return
460 460 bugzilla username and userid if so. If not, return default
461 461 bugzilla username and userid.'''
462 462 user = self.map_committer(committer)
463 463 try:
464 464 userid = self.get_user_id(user)
465 465 except KeyError:
466 466 try:
467 467 defaultuser = self.ui.config('bugzilla', 'bzuser')
468 468 if not defaultuser:
469 469 raise util.Abort(_('cannot find bugzilla user id for %s') %
470 470 user)
471 471 userid = self.get_user_id(defaultuser)
472 472 user = defaultuser
473 473 except KeyError:
474 474 raise util.Abort(_('cannot find bugzilla user id for %s or %s')
475 475 % (user, defaultuser))
476 476 return (user, userid)
477 477
478 478 def updatebug(self, bugid, newstate, text, committer):
479 479 '''update bug state with comment text.
480 480
481 481 Try adding comment as committer of changeset, otherwise as
482 482 default bugzilla user.'''
483 483 if len(newstate) > 0:
484 484 self.ui.warn(_("Bugzilla/MySQL cannot update bug state\n"))
485 485
486 486 (user, userid) = self.get_bugzilla_user(committer)
487 487 now = time.strftime('%Y-%m-%d %H:%M:%S')
488 488 self.run('''insert into longdescs
489 489 (bug_id, who, bug_when, thetext)
490 490 values (%s, %s, %s, %s)''',
491 491 (bugid, userid, now, text))
492 492 self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
493 493 values (%s, %s, %s, %s)''',
494 494 (bugid, userid, now, self.longdesc_id))
495 495 self.conn.commit()
496 496
497 497 class bzmysql_2_18(bzmysql):
498 498 '''support for bugzilla 2.18 series.'''
499 499
500 500 def __init__(self, ui):
501 501 bzmysql.__init__(self, ui)
502 502 self.default_notify = \
503 503 "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
504 504
505 505 class bzmysql_3_0(bzmysql_2_18):
506 506 '''support for bugzilla 3.0 series.'''
507 507
508 508 def __init__(self, ui):
509 509 bzmysql_2_18.__init__(self, ui)
510 510
511 511 def get_longdesc_id(self):
512 512 '''get identity of longdesc field'''
513 513 self.run('select id from fielddefs where name = "longdesc"')
514 514 ids = self.cursor.fetchall()
515 515 if len(ids) != 1:
516 516 raise util.Abort(_('unknown database schema'))
517 517 return ids[0][0]
518 518
519 519 # Bugzilla via XMLRPC interface.
520 520
521 521 class cookietransportrequest(object):
522 522 """A Transport request method that retains cookies over its lifetime.
523 523
524 524 The regular xmlrpclib transports ignore cookies. Which causes
525 525 a bit of a problem when you need a cookie-based login, as with
526 526 the Bugzilla XMLRPC interface.
527 527
528 528 So this is a helper for defining a Transport which looks for
529 529 cookies being set in responses and saves them to add to all future
530 530 requests.
531 531 """
532 532
533 533 # Inspiration drawn from
534 534 # http://blog.godson.in/2010/09/how-to-make-python-xmlrpclib-client.html
535 535 # http://www.itkovian.net/base/transport-class-for-pythons-xml-rpc-lib/
536 536
537 537 cookies = []
538 538 def send_cookies(self, connection):
539 539 if self.cookies:
540 540 for cookie in self.cookies:
541 541 connection.putheader("Cookie", cookie)
542 542
543 543 def request(self, host, handler, request_body, verbose=0):
544 544 self.verbose = verbose
545 545 self.accept_gzip_encoding = False
546 546
547 547 # issue XML-RPC request
548 548 h = self.make_connection(host)
549 549 if verbose:
550 550 h.set_debuglevel(1)
551 551
552 552 self.send_request(h, handler, request_body)
553 553 self.send_host(h, host)
554 554 self.send_cookies(h)
555 555 self.send_user_agent(h)
556 556 self.send_content(h, request_body)
557 557
558 558 # Deal with differences between Python 2.4-2.6 and 2.7.
559 559 # In the former h is a HTTP(S). In the latter it's a
560 560 # HTTP(S)Connection. Luckily, the 2.4-2.6 implementation of
561 561 # HTTP(S) has an underlying HTTP(S)Connection, so extract
562 562 # that and use it.
563 563 try:
564 564 response = h.getresponse()
565 565 except AttributeError:
566 566 response = h._conn.getresponse()
567 567
568 568 # Add any cookie definitions to our list.
569 569 for header in response.msg.getallmatchingheaders("Set-Cookie"):
570 570 val = header.split(": ", 1)[1]
571 571 cookie = val.split(";", 1)[0]
572 572 self.cookies.append(cookie)
573 573
574 574 if response.status != 200:
575 575 raise xmlrpclib.ProtocolError(host + handler, response.status,
576 576 response.reason, response.msg.headers)
577 577
578 578 payload = response.read()
579 579 parser, unmarshaller = self.getparser()
580 580 parser.feed(payload)
581 581 parser.close()
582 582
583 583 return unmarshaller.close()
584 584
585 585 # The explicit calls to the underlying xmlrpclib __init__() methods are
586 586 # necessary. The xmlrpclib.Transport classes are old-style classes, and
587 587 # it turns out their __init__() doesn't get called when doing multiple
588 588 # inheritance with a new-style class.
589 589 class cookietransport(cookietransportrequest, xmlrpclib.Transport):
590 590 def __init__(self, use_datetime=0):
591 591 if util.safehasattr(xmlrpclib.Transport, "__init__"):
592 592 xmlrpclib.Transport.__init__(self, use_datetime)
593 593
594 594 class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport):
595 595 def __init__(self, use_datetime=0):
596 596 if util.safehasattr(xmlrpclib.Transport, "__init__"):
597 597 xmlrpclib.SafeTransport.__init__(self, use_datetime)
598 598
599 599 class bzxmlrpc(bzaccess):
600 600 """Support for access to Bugzilla via the Bugzilla XMLRPC API.
601 601
602 602 Requires a minimum Bugzilla version 3.4.
603 603 """
604 604
605 605 def __init__(self, ui):
606 606 bzaccess.__init__(self, ui)
607 607
608 608 bzweb = self.ui.config('bugzilla', 'bzurl',
609 609 'http://localhost/bugzilla/')
610 610 bzweb = bzweb.rstrip("/") + "/xmlrpc.cgi"
611 611
612 612 user = self.ui.config('bugzilla', 'user', 'bugs')
613 613 passwd = self.ui.config('bugzilla', 'password')
614 614
615 615 self.fixstatus = self.ui.config('bugzilla', 'fixstatus', 'RESOLVED')
616 616 self.fixresolution = self.ui.config('bugzilla', 'fixresolution',
617 617 'FIXED')
618 618
619 619 self.bzproxy = xmlrpclib.ServerProxy(bzweb, self.transport(bzweb))
620 620 ver = self.bzproxy.Bugzilla.version()['version'].split('.')
621 621 self.bzvermajor = int(ver[0])
622 622 self.bzverminor = int(ver[1])
623 623 self.bzproxy.User.login(dict(login=user, password=passwd))
624 624
625 625 def transport(self, uri):
626 626 if urlparse.urlparse(uri, "http")[0] == "https":
627 627 return cookiesafetransport()
628 628 else:
629 629 return cookietransport()
630 630
631 631 def get_bug_comments(self, id):
632 632 """Return a string with all comment text for a bug."""
633 633 c = self.bzproxy.Bug.comments(dict(ids=[id], include_fields=['text']))
634 634 return ''.join([t['text'] for t in c['bugs'][str(id)]['comments']])
635 635
636 636 def filter_real_bug_ids(self, bugs):
637 637 probe = self.bzproxy.Bug.get(dict(ids=sorted(bugs.keys()),
638 638 include_fields=[],
639 639 permissive=True))
640 640 for badbug in probe['faults']:
641 641 id = badbug['id']
642 642 self.ui.status(_('bug %d does not exist\n') % id)
643 643 del bugs[id]
644 644
645 645 def filter_cset_known_bug_ids(self, node, bugs):
646 646 for id in sorted(bugs.keys()):
647 647 if self.get_bug_comments(id).find(short(node)) != -1:
648 648 self.ui.status(_('bug %d already knows about changeset %s\n') %
649 649 (id, short(node)))
650 650 del bugs[id]
651 651
652 652 def updatebug(self, bugid, newstate, text, committer):
653 653 args = {}
654 654 if 'hours' in newstate:
655 655 args['work_time'] = newstate['hours']
656 656
657 657 if self.bzvermajor >= 4:
658 658 args['ids'] = [bugid]
659 659 args['comment'] = {'body' : text}
660 660 if 'fix' in newstate:
661 661 args['status'] = self.fixstatus
662 662 args['resolution'] = self.fixresolution
663 663 self.bzproxy.Bug.update(args)
664 664 else:
665 665 if 'fix' in newstate:
666 666 self.ui.warn(_("Bugzilla/XMLRPC needs Bugzilla 4.0 or later "
667 667 "to mark bugs fixed\n"))
668 668 args['id'] = bugid
669 669 args['comment'] = text
670 670 self.bzproxy.Bug.add_comment(args)
671 671
672 672 class bzxmlrpcemail(bzxmlrpc):
673 673 """Read data from Bugzilla via XMLRPC, send updates via email.
674 674
675 675 Advantages of sending updates via email:
676 676 1. Comments can be added as any user, not just logged in user.
677 677 2. Bug statuses or other fields not accessible via XMLRPC can
678 678 potentially be updated.
679 679
680 680 There is no XMLRPC function to change bug status before Bugzilla
681 681 4.0, so bugs cannot be marked fixed via XMLRPC before Bugzilla 4.0.
682 682 But bugs can be marked fixed via email from 3.4 onwards.
683 683 """
684 684
685 685 # The email interface changes subtly between 3.4 and 3.6. In 3.4,
686 686 # in-email fields are specified as '@<fieldname> = <value>'. In
687 687 # 3.6 this becomes '@<fieldname> <value>'. And fieldname @bug_id
688 688 # in 3.4 becomes @id in 3.6. 3.6 and 4.0 both maintain backwards
689 689 # compatibility, but rather than rely on this use the new format for
690 690 # 4.0 onwards.
691 691
692 692 def __init__(self, ui):
693 693 bzxmlrpc.__init__(self, ui)
694 694
695 695 self.bzemail = self.ui.config('bugzilla', 'bzemail')
696 696 if not self.bzemail:
697 697 raise util.Abort(_("configuration 'bzemail' missing"))
698 698 mail.validateconfig(self.ui)
699 699
700 700 def makecommandline(self, fieldname, value):
701 701 if self.bzvermajor >= 4:
702 702 return "@%s %s" % (fieldname, str(value))
703 703 else:
704 704 if fieldname == "id":
705 705 fieldname = "bug_id"
706 706 return "@%s = %s" % (fieldname, str(value))
707 707
708 708 def send_bug_modify_email(self, bugid, commands, comment, committer):
709 709 '''send modification message to Bugzilla bug via email.
710 710
711 711 The message format is documented in the Bugzilla email_in.pl
712 712 specification. commands is a list of command lines, comment is the
713 713 comment text.
714 714
715 715 To stop users from crafting commit comments with
716 716 Bugzilla commands, specify the bug ID via the message body, rather
717 717 than the subject line, and leave a blank line after it.
718 718 '''
719 719 user = self.map_committer(committer)
720 720 matches = self.bzproxy.User.get(dict(match=[user]))
721 721 if not matches['users']:
722 722 user = self.ui.config('bugzilla', 'user', 'bugs')
723 723 matches = self.bzproxy.User.get(dict(match=[user]))
724 724 if not matches['users']:
725 725 raise util.Abort(_("default bugzilla user %s email not found") %
726 726 user)
727 727 user = matches['users'][0]['email']
728 728 commands.append(self.makecommandline("id", bugid))
729 729
730 730 text = "\n".join(commands) + "\n\n" + comment
731 731
732 732 _charsets = mail._charsets(self.ui)
733 733 user = mail.addressencode(self.ui, user, _charsets)
734 734 bzemail = mail.addressencode(self.ui, self.bzemail, _charsets)
735 735 msg = mail.mimeencode(self.ui, text, _charsets)
736 736 msg['From'] = user
737 737 msg['To'] = bzemail
738 738 msg['Subject'] = mail.headencode(self.ui, "Bug modification", _charsets)
739 739 sendmail = mail.connect(self.ui)
740 740 sendmail(user, bzemail, msg.as_string())
741 741
742 742 def updatebug(self, bugid, newstate, text, committer):
743 743 cmds = []
744 744 if 'hours' in newstate:
745 745 cmds.append(self.makecommandline("work_time", newstate['hours']))
746 746 if 'fix' in newstate:
747 747 cmds.append(self.makecommandline("bug_status", self.fixstatus))
748 748 cmds.append(self.makecommandline("resolution", self.fixresolution))
749 749 self.send_bug_modify_email(bugid, cmds, text, committer)
750 750
751 751 class bugzilla(object):
752 752 # supported versions of bugzilla. different versions have
753 753 # different schemas.
754 754 _versions = {
755 755 '2.16': bzmysql,
756 756 '2.18': bzmysql_2_18,
757 757 '3.0': bzmysql_3_0,
758 758 'xmlrpc': bzxmlrpc,
759 759 'xmlrpc+email': bzxmlrpcemail
760 760 }
761 761
762 762 _default_bug_re = (r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
763 763 r'(?P<ids>(?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
764 764 r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
765 765
766 766 _default_fix_re = (r'fix(?:es)?\s*(?:bugs?\s*)?,?\s*'
767 767 r'(?:nos?\.?|num(?:ber)?s?)?\s*'
768 768 r'(?P<ids>(?:#?\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
769 769 r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
770 770
771 771 _bz = None
772 772
773 773 def __init__(self, ui, repo):
774 774 self.ui = ui
775 775 self.repo = repo
776 776
777 777 def bz(self):
778 778 '''return object that knows how to talk to bugzilla version in
779 779 use.'''
780 780
781 781 if bugzilla._bz is None:
782 782 bzversion = self.ui.config('bugzilla', 'version')
783 783 try:
784 784 bzclass = bugzilla._versions[bzversion]
785 785 except KeyError:
786 786 raise util.Abort(_('bugzilla version %s not supported') %
787 787 bzversion)
788 788 bugzilla._bz = bzclass(self.ui)
789 789 return bugzilla._bz
790 790
791 791 def __getattr__(self, key):
792 792 return getattr(self.bz(), key)
793 793
794 794 _bug_re = None
795 795 _fix_re = None
796 796 _split_re = None
797 797
798 798 def find_bugs(self, ctx):
799 799 '''return bugs dictionary created from commit comment.
800 800
801 801 Extract bug info from changeset comments. Filter out any that are
802 802 not known to Bugzilla, and any that already have a reference to
803 803 the given changeset in their comments.
804 804 '''
805 805 if bugzilla._bug_re is None:
806 806 bugzilla._bug_re = re.compile(
807 807 self.ui.config('bugzilla', 'regexp',
808 808 bugzilla._default_bug_re), re.IGNORECASE)
809 809 bugzilla._fix_re = re.compile(
810 810 self.ui.config('bugzilla', 'fixregexp',
811 811 bugzilla._default_fix_re), re.IGNORECASE)
812 812 bugzilla._split_re = re.compile(r'\D+')
813 813 start = 0
814 814 hours = 0.0
815 815 bugs = {}
816 816 bugmatch = bugzilla._bug_re.search(ctx.description(), start)
817 817 fixmatch = bugzilla._fix_re.search(ctx.description(), start)
818 818 while True:
819 819 bugattribs = {}
820 820 if not bugmatch and not fixmatch:
821 821 break
822 822 if not bugmatch:
823 823 m = fixmatch
824 824 elif not fixmatch:
825 825 m = bugmatch
826 826 else:
827 827 if bugmatch.start() < fixmatch.start():
828 828 m = bugmatch
829 829 else:
830 830 m = fixmatch
831 831 start = m.end()
832 832 if m is bugmatch:
833 833 bugmatch = bugzilla._bug_re.search(ctx.description(), start)
834 834 if 'fix' in bugattribs:
835 835 del bugattribs['fix']
836 836 else:
837 837 fixmatch = bugzilla._fix_re.search(ctx.description(), start)
838 838 bugattribs['fix'] = None
839 839
840 840 try:
841 841 ids = m.group('ids')
842 842 except IndexError:
843 843 ids = m.group(1)
844 844 try:
845 845 hours = float(m.group('hours'))
846 846 bugattribs['hours'] = hours
847 847 except IndexError:
848 848 pass
849 849 except TypeError:
850 850 pass
851 851 except ValueError:
852 852 self.ui.status(_("%s: invalid hours\n") % m.group('hours'))
853 853
854 854 for id in bugzilla._split_re.split(ids):
855 855 if not id:
856 856 continue
857 857 bugs[int(id)] = bugattribs
858 858 if bugs:
859 859 self.filter_real_bug_ids(bugs)
860 860 if bugs:
861 861 self.filter_cset_known_bug_ids(ctx.node(), bugs)
862 862 return bugs
863 863
864 864 def update(self, bugid, newstate, ctx):
865 865 '''update bugzilla bug with reference to changeset.'''
866 866
867 867 def webroot(root):
868 868 '''strip leading prefix of repo root and turn into
869 869 url-safe path.'''
870 870 count = int(self.ui.config('bugzilla', 'strip', 0))
871 871 root = util.pconvert(root)
872 872 while count > 0:
873 873 c = root.find('/')
874 874 if c == -1:
875 875 break
876 876 root = root[c + 1:]
877 877 count -= 1
878 878 return root
879 879
880 880 mapfile = self.ui.config('bugzilla', 'style')
881 881 tmpl = self.ui.config('bugzilla', 'template')
882 t = cmdutil.changeset_templater(self.ui, self.repo,
883 False, None, mapfile, False)
884 882 if not mapfile and not tmpl:
885 883 tmpl = _('changeset {node|short} in repo {root} refers '
886 884 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
887 885 if tmpl:
888 886 tmpl = templater.parsestring(tmpl, quoted=False)
889 t.use_template(tmpl)
887 t = cmdutil.changeset_templater(self.ui, self.repo,
888 False, None, tmpl, mapfile, False)
890 889 self.ui.pushbuffer()
891 890 t.show(ctx, changes=ctx.changeset(),
892 891 bug=str(bugid),
893 892 hgweb=self.ui.config('web', 'baseurl'),
894 893 root=self.repo.root,
895 894 webroot=webroot(self.repo.root))
896 895 data = self.ui.popbuffer()
897 896 self.updatebug(bugid, newstate, data, util.email(ctx.user()))
898 897
899 898 def hook(ui, repo, hooktype, node=None, **kwargs):
900 899 '''add comment to bugzilla for each changeset that refers to a
901 900 bugzilla bug id. only add a comment once per bug, so same change
902 901 seen multiple times does not fill bug with duplicate data.'''
903 902 if node is None:
904 903 raise util.Abort(_('hook type %s does not pass a changeset id') %
905 904 hooktype)
906 905 try:
907 906 bz = bugzilla(ui, repo)
908 907 ctx = repo[node]
909 908 bugs = bz.find_bugs(ctx)
910 909 if bugs:
911 910 for bug in bugs:
912 911 bz.update(bug, bugs[bug], ctx)
913 912 bz.notify(bugs, util.email(ctx.user()))
914 913 except Exception, e:
915 914 raise util.Abort(_('Bugzilla error: %s') % e)
@@ -1,204 +1,204
1 1 # churn.py - create a graph of revisions count grouped by template
2 2 #
3 3 # Copyright 2006 Josef "Jeff" Sipek <jeffpc@josefsipek.net>
4 4 # Copyright 2008 Alexander Solovyov <piranha@piranha.org.ua>
5 5 #
6 6 # This software may be used and distributed according to the terms of the
7 7 # GNU General Public License version 2 or any later version.
8 8
9 9 '''command to display statistics about repository history'''
10 10
11 11 from mercurial.i18n import _
12 12 from mercurial import patch, cmdutil, scmutil, util, templater, commands
13 13 import os
14 14 import time, datetime
15 15
16 16 testedwith = 'internal'
17 17
18 18 def maketemplater(ui, repo, tmpl):
19 19 tmpl = templater.parsestring(tmpl, quoted=False)
20 20 try:
21 t = cmdutil.changeset_templater(ui, repo, False, None, None, False)
21 t = cmdutil.changeset_templater(ui, repo, False, None, tmpl,
22 None, False)
22 23 except SyntaxError, inst:
23 24 raise util.Abort(inst.args[0])
24 t.use_template(tmpl)
25 25 return t
26 26
27 27 def changedlines(ui, repo, ctx1, ctx2, fns):
28 28 added, removed = 0, 0
29 29 fmatch = scmutil.matchfiles(repo, fns)
30 30 diff = ''.join(patch.diff(repo, ctx1.node(), ctx2.node(), fmatch))
31 31 for l in diff.split('\n'):
32 32 if l.startswith("+") and not l.startswith("+++ "):
33 33 added += 1
34 34 elif l.startswith("-") and not l.startswith("--- "):
35 35 removed += 1
36 36 return (added, removed)
37 37
38 38 def countrate(ui, repo, amap, *pats, **opts):
39 39 """Calculate stats"""
40 40 if opts.get('dateformat'):
41 41 def getkey(ctx):
42 42 t, tz = ctx.date()
43 43 date = datetime.datetime(*time.gmtime(float(t) - tz)[:6])
44 44 return date.strftime(opts['dateformat'])
45 45 else:
46 46 tmpl = opts.get('template', '{author|email}')
47 47 tmpl = maketemplater(ui, repo, tmpl)
48 48 def getkey(ctx):
49 49 ui.pushbuffer()
50 50 tmpl.show(ctx)
51 51 return ui.popbuffer()
52 52
53 53 state = {'count': 0}
54 54 rate = {}
55 55 df = False
56 56 if opts.get('date'):
57 57 df = util.matchdate(opts['date'])
58 58
59 59 m = scmutil.match(repo[None], pats, opts)
60 60 def prep(ctx, fns):
61 61 rev = ctx.rev()
62 62 if df and not df(ctx.date()[0]): # doesn't match date format
63 63 return
64 64
65 65 key = getkey(ctx).strip()
66 66 key = amap.get(key, key) # alias remap
67 67 if opts.get('changesets'):
68 68 rate[key] = (rate.get(key, (0,))[0] + 1, 0)
69 69 else:
70 70 parents = ctx.parents()
71 71 if len(parents) > 1:
72 72 ui.note(_('revision %d is a merge, ignoring...\n') % (rev,))
73 73 return
74 74
75 75 ctx1 = parents[0]
76 76 lines = changedlines(ui, repo, ctx1, ctx, fns)
77 77 rate[key] = [r + l for r, l in zip(rate.get(key, (0, 0)), lines)]
78 78
79 79 state['count'] += 1
80 80 ui.progress(_('analyzing'), state['count'], total=len(repo))
81 81
82 82 for ctx in cmdutil.walkchangerevs(repo, m, opts, prep):
83 83 continue
84 84
85 85 ui.progress(_('analyzing'), None)
86 86
87 87 return rate
88 88
89 89
90 90 def churn(ui, repo, *pats, **opts):
91 91 '''histogram of changes to the repository
92 92
93 93 This command will display a histogram representing the number
94 94 of changed lines or revisions, grouped according to the given
95 95 template. The default template will group changes by author.
96 96 The --dateformat option may be used to group the results by
97 97 date instead.
98 98
99 99 Statistics are based on the number of changed lines, or
100 100 alternatively the number of matching revisions if the
101 101 --changesets option is specified.
102 102
103 103 Examples::
104 104
105 105 # display count of changed lines for every committer
106 106 hg churn -t "{author|email}"
107 107
108 108 # display daily activity graph
109 109 hg churn -f "%H" -s -c
110 110
111 111 # display activity of developers by month
112 112 hg churn -f "%Y-%m" -s -c
113 113
114 114 # display count of lines changed in every year
115 115 hg churn -f "%Y" -s
116 116
117 117 It is possible to map alternate email addresses to a main address
118 118 by providing a file using the following format::
119 119
120 120 <alias email> = <actual email>
121 121
122 122 Such a file may be specified with the --aliases option, otherwise
123 123 a .hgchurn file will be looked for in the working directory root.
124 124 Aliases will be split from the rightmost "=".
125 125 '''
126 126 def pad(s, l):
127 127 return (s + " " * l)[:l]
128 128
129 129 amap = {}
130 130 aliases = opts.get('aliases')
131 131 if not aliases and os.path.exists(repo.wjoin('.hgchurn')):
132 132 aliases = repo.wjoin('.hgchurn')
133 133 if aliases:
134 134 for l in open(aliases, "r"):
135 135 try:
136 136 alias, actual = l.rsplit('=' in l and '=' or None, 1)
137 137 amap[alias.strip()] = actual.strip()
138 138 except ValueError:
139 139 l = l.strip()
140 140 if l:
141 141 ui.warn(_("skipping malformed alias: %s\n") % l)
142 142 continue
143 143
144 144 rate = countrate(ui, repo, amap, *pats, **opts).items()
145 145 if not rate:
146 146 return
147 147
148 148 if opts.get('sort'):
149 149 rate.sort()
150 150 else:
151 151 rate.sort(key=lambda x: (-sum(x[1]), x))
152 152
153 153 # Be careful not to have a zero maxcount (issue833)
154 154 maxcount = float(max(sum(v) for k, v in rate)) or 1.0
155 155 maxname = max(len(k) for k, v in rate)
156 156
157 157 ttywidth = ui.termwidth()
158 158 ui.debug("assuming %i character terminal\n" % ttywidth)
159 159 width = ttywidth - maxname - 2 - 2 - 2
160 160
161 161 if opts.get('diffstat'):
162 162 width -= 15
163 163 def format(name, diffstat):
164 164 added, removed = diffstat
165 165 return "%s %15s %s%s\n" % (pad(name, maxname),
166 166 '+%d/-%d' % (added, removed),
167 167 ui.label('+' * charnum(added),
168 168 'diffstat.inserted'),
169 169 ui.label('-' * charnum(removed),
170 170 'diffstat.deleted'))
171 171 else:
172 172 width -= 6
173 173 def format(name, count):
174 174 return "%s %6d %s\n" % (pad(name, maxname), sum(count),
175 175 '*' * charnum(sum(count)))
176 176
177 177 def charnum(count):
178 178 return int(round(count * width / maxcount))
179 179
180 180 for name, count in rate:
181 181 ui.write(format(name, count))
182 182
183 183
184 184 cmdtable = {
185 185 "churn":
186 186 (churn,
187 187 [('r', 'rev', [],
188 188 _('count rate for the specified revision or range'), _('REV')),
189 189 ('d', 'date', '',
190 190 _('count rate for revisions matching date spec'), _('DATE')),
191 191 ('t', 'template', '{author|email}',
192 192 _('template to group changesets'), _('TEMPLATE')),
193 193 ('f', 'dateformat', '',
194 194 _('strftime-compatible format for grouping by date'), _('FORMAT')),
195 195 ('c', 'changesets', False, _('count rate by number of changesets')),
196 196 ('s', 'sort', False, _('sort by key (default: sort by count)')),
197 197 ('', 'diffstat', False, _('display added/removed lines separately')),
198 198 ('', 'aliases', '',
199 199 _('file with email aliases'), _('FILE')),
200 200 ] + commands.walkopts,
201 201 _("hg churn [-d DATE] [-r REV] [--aliases FILE] [FILE]")),
202 202 }
203 203
204 204 commands.inferrepo += " churn"
@@ -1,277 +1,276
1 1 # Copyright (C) 2007-8 Brendan Cully <brendan@kublai.com>
2 2 #
3 3 # This software may be used and distributed according to the terms of the
4 4 # GNU General Public License version 2 or any later version.
5 5
6 6 """hooks for integrating with the CIA.vc notification service
7 7
8 8 This is meant to be run as a changegroup or incoming hook. To
9 9 configure it, set the following options in your hgrc::
10 10
11 11 [cia]
12 12 # your registered CIA user name
13 13 user = foo
14 14 # the name of the project in CIA
15 15 project = foo
16 16 # the module (subproject) (optional)
17 17 #module = foo
18 18 # Append a diffstat to the log message (optional)
19 19 #diffstat = False
20 20 # Template to use for log messages (optional)
21 21 #template = {desc}\\n{baseurl}{webroot}/rev/{node}-- {diffstat}
22 22 # Style to use (optional)
23 23 #style = foo
24 24 # The URL of the CIA notification service (optional)
25 25 # You can use mailto: URLs to send by email, e.g.
26 26 # mailto:cia@cia.vc
27 27 # Make sure to set email.from if you do this.
28 28 #url = http://cia.vc/
29 29 # print message instead of sending it (optional)
30 30 #test = False
31 31 # number of slashes to strip for url paths
32 32 #strip = 0
33 33
34 34 [hooks]
35 35 # one of these:
36 36 changegroup.cia = python:hgcia.hook
37 37 #incoming.cia = python:hgcia.hook
38 38
39 39 [web]
40 40 # If you want hyperlinks (optional)
41 41 baseurl = http://server/path/to/repo
42 42 """
43 43
44 44 from mercurial.i18n import _
45 45 from mercurial.node import bin, short
46 46 from mercurial import cmdutil, patch, templater, util, mail
47 47 import email.Parser
48 48
49 49 import socket, xmlrpclib
50 50 from xml.sax import saxutils
51 51 testedwith = 'internal'
52 52
53 53 socket_timeout = 30 # seconds
54 54 if util.safehasattr(socket, 'setdefaulttimeout'):
55 55 # set a timeout for the socket so you don't have to wait so looooong
56 56 # when cia.vc is having problems. requires python >= 2.3:
57 57 socket.setdefaulttimeout(socket_timeout)
58 58
59 59 HGCIA_VERSION = '0.1'
60 60 HGCIA_URL = 'http://hg.kublai.com/mercurial/hgcia'
61 61
62 62
63 63 class ciamsg(object):
64 64 """ A CIA message """
65 65 def __init__(self, cia, ctx):
66 66 self.cia = cia
67 67 self.ctx = ctx
68 68 self.url = self.cia.url
69 69 if self.url:
70 70 self.url += self.cia.root
71 71
72 72 def fileelem(self, path, uri, action):
73 73 if uri:
74 74 uri = ' uri=%s' % saxutils.quoteattr(uri)
75 75 return '<file%s action=%s>%s</file>' % (
76 76 uri, saxutils.quoteattr(action), saxutils.escape(path))
77 77
78 78 def fileelems(self):
79 79 n = self.ctx.node()
80 80 f = self.cia.repo.status(self.ctx.p1().node(), n)
81 81 url = self.url or ''
82 82 if url and url[-1] == '/':
83 83 url = url[:-1]
84 84 elems = []
85 85 for path in f[0]:
86 86 uri = '%s/diff/%s/%s' % (url, short(n), path)
87 87 elems.append(self.fileelem(path, url and uri, 'modify'))
88 88 for path in f[1]:
89 89 # TODO: copy/rename ?
90 90 uri = '%s/file/%s/%s' % (url, short(n), path)
91 91 elems.append(self.fileelem(path, url and uri, 'add'))
92 92 for path in f[2]:
93 93 elems.append(self.fileelem(path, '', 'remove'))
94 94
95 95 return '\n'.join(elems)
96 96
97 97 def sourceelem(self, project, module=None, branch=None):
98 98 msg = ['<source>', '<project>%s</project>' % saxutils.escape(project)]
99 99 if module:
100 100 msg.append('<module>%s</module>' % saxutils.escape(module))
101 101 if branch:
102 102 msg.append('<branch>%s</branch>' % saxutils.escape(branch))
103 103 msg.append('</source>')
104 104
105 105 return '\n'.join(msg)
106 106
107 107 def diffstat(self):
108 108 class patchbuf(object):
109 109 def __init__(self):
110 110 self.lines = []
111 111 # diffstat is stupid
112 112 self.name = 'cia'
113 113 def write(self, data):
114 114 self.lines += data.splitlines(True)
115 115 def close(self):
116 116 pass
117 117
118 118 n = self.ctx.node()
119 119 pbuf = patchbuf()
120 120 cmdutil.export(self.cia.repo, [n], fp=pbuf)
121 121 return patch.diffstat(pbuf.lines) or ''
122 122
123 123 def logmsg(self):
124 124 diffstat = self.cia.diffstat and self.diffstat() or ''
125 125 self.cia.ui.pushbuffer()
126 126 self.cia.templater.show(self.ctx, changes=self.ctx.changeset(),
127 127 baseurl=self.cia.ui.config('web', 'baseurl'),
128 128 url=self.url, diffstat=diffstat,
129 129 webroot=self.cia.root)
130 130 return self.cia.ui.popbuffer()
131 131
132 132 def xml(self):
133 133 n = short(self.ctx.node())
134 134 src = self.sourceelem(self.cia.project, module=self.cia.module,
135 135 branch=self.ctx.branch())
136 136 # unix timestamp
137 137 dt = self.ctx.date()
138 138 timestamp = dt[0]
139 139
140 140 author = saxutils.escape(self.ctx.user())
141 141 rev = '%d:%s' % (self.ctx.rev(), n)
142 142 log = saxutils.escape(self.logmsg())
143 143
144 144 url = self.url
145 145 if url and url[-1] == '/':
146 146 url = url[:-1]
147 147 url = url and '<url>%s/rev/%s</url>' % (saxutils.escape(url), n) or ''
148 148
149 149 msg = """
150 150 <message>
151 151 <generator>
152 152 <name>Mercurial (hgcia)</name>
153 153 <version>%s</version>
154 154 <url>%s</url>
155 155 <user>%s</user>
156 156 </generator>
157 157 %s
158 158 <body>
159 159 <commit>
160 160 <author>%s</author>
161 161 <version>%s</version>
162 162 <log>%s</log>
163 163 %s
164 164 <files>%s</files>
165 165 </commit>
166 166 </body>
167 167 <timestamp>%d</timestamp>
168 168 </message>
169 169 """ % \
170 170 (HGCIA_VERSION, saxutils.escape(HGCIA_URL),
171 171 saxutils.escape(self.cia.user), src, author, rev, log, url,
172 172 self.fileelems(), timestamp)
173 173
174 174 return msg
175 175
176 176
177 177 class hgcia(object):
178 178 """ CIA notification class """
179 179
180 180 deftemplate = '{desc}'
181 181 dstemplate = '{desc}\n-- \n{diffstat}'
182 182
183 183 def __init__(self, ui, repo):
184 184 self.ui = ui
185 185 self.repo = repo
186 186
187 187 self.ciaurl = self.ui.config('cia', 'url', 'http://cia.vc')
188 188 self.user = self.ui.config('cia', 'user')
189 189 self.project = self.ui.config('cia', 'project')
190 190 self.module = self.ui.config('cia', 'module')
191 191 self.diffstat = self.ui.configbool('cia', 'diffstat')
192 192 self.emailfrom = self.ui.config('email', 'from')
193 193 self.dryrun = self.ui.configbool('cia', 'test')
194 194 self.url = self.ui.config('web', 'baseurl')
195 195 # Default to -1 for backward compatibility
196 196 self.stripcount = int(self.ui.config('cia', 'strip', -1))
197 197 self.root = self.strip(self.repo.root)
198 198
199 199 style = self.ui.config('cia', 'style')
200 200 template = self.ui.config('cia', 'template')
201 201 if not template:
202 202 template = self.diffstat and self.dstemplate or self.deftemplate
203 203 template = templater.parsestring(template, quoted=False)
204 204 t = cmdutil.changeset_templater(self.ui, self.repo, False, None,
205 style, False)
206 t.use_template(template)
205 template, style, False)
207 206 self.templater = t
208 207
209 208 def strip(self, path):
210 209 '''strip leading slashes from local path, turn into web-safe path.'''
211 210
212 211 path = util.pconvert(path)
213 212 count = self.stripcount
214 213 if count < 0:
215 214 return ''
216 215 while count > 0:
217 216 c = path.find('/')
218 217 if c == -1:
219 218 break
220 219 path = path[c + 1:]
221 220 count -= 1
222 221 return path
223 222
224 223 def sendrpc(self, msg):
225 224 srv = xmlrpclib.Server(self.ciaurl)
226 225 res = srv.hub.deliver(msg)
227 226 if res is not True and res != 'queued.':
228 227 raise util.Abort(_('%s returned an error: %s') %
229 228 (self.ciaurl, res))
230 229
231 230 def sendemail(self, address, data):
232 231 p = email.Parser.Parser()
233 232 msg = p.parsestr(data)
234 233 msg['Date'] = util.datestr(format="%a, %d %b %Y %H:%M:%S %1%2")
235 234 msg['To'] = address
236 235 msg['From'] = self.emailfrom
237 236 msg['Subject'] = 'DeliverXML'
238 237 msg['Content-type'] = 'text/xml'
239 238 msgtext = msg.as_string()
240 239
241 240 self.ui.status(_('hgcia: sending update to %s\n') % address)
242 241 mail.sendmail(self.ui, util.email(self.emailfrom),
243 242 [address], msgtext)
244 243
245 244
246 245 def hook(ui, repo, hooktype, node=None, url=None, **kwargs):
247 246 """ send CIA notification """
248 247 def sendmsg(cia, ctx):
249 248 msg = ciamsg(cia, ctx).xml()
250 249 if cia.dryrun:
251 250 ui.write(msg)
252 251 elif cia.ciaurl.startswith('mailto:'):
253 252 if not cia.emailfrom:
254 253 raise util.Abort(_('email.from must be defined when '
255 254 'sending by email'))
256 255 cia.sendemail(cia.ciaurl[7:], msg)
257 256 else:
258 257 cia.sendrpc(msg)
259 258
260 259 n = bin(node)
261 260 cia = hgcia(ui, repo)
262 261 if not cia.user:
263 262 ui.debug('cia: no user specified')
264 263 return
265 264 if not cia.project:
266 265 ui.debug('cia: no project specified')
267 266 return
268 267 if hooktype == 'changegroup':
269 268 start = repo.changelog.rev(n)
270 269 end = len(repo.changelog)
271 270 for rev in xrange(start, end):
272 271 n = repo.changelog.node(rev)
273 272 ctx = repo.changectx(n)
274 273 sendmsg(cia, ctx)
275 274 else:
276 275 ctx = repo.changectx(n)
277 276 sendmsg(cia, ctx)
@@ -1,736 +1,735
1 1 # keyword.py - $Keyword$ expansion for Mercurial
2 2 #
3 3 # Copyright 2007-2012 Christian Ebert <blacktrash@gmx.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 or any later version.
7 7 #
8 8 # $Id$
9 9 #
10 10 # Keyword expansion hack against the grain of a Distributed SCM
11 11 #
12 12 # There are many good reasons why this is not needed in a distributed
13 13 # SCM, still it may be useful in very small projects based on single
14 14 # files (like LaTeX packages), that are mostly addressed to an
15 15 # audience not running a version control system.
16 16 #
17 17 # For in-depth discussion refer to
18 18 # <http://mercurial.selenic.com/wiki/KeywordPlan>.
19 19 #
20 20 # Keyword expansion is based on Mercurial's changeset template mappings.
21 21 #
22 22 # Binary files are not touched.
23 23 #
24 24 # Files to act upon/ignore are specified in the [keyword] section.
25 25 # Customized keyword template mappings in the [keywordmaps] section.
26 26 #
27 27 # Run "hg help keyword" and "hg kwdemo" to get info on configuration.
28 28
29 29 '''expand keywords in tracked files
30 30
31 31 This extension expands RCS/CVS-like or self-customized $Keywords$ in
32 32 tracked text files selected by your configuration.
33 33
34 34 Keywords are only expanded in local repositories and not stored in the
35 35 change history. The mechanism can be regarded as a convenience for the
36 36 current user or for archive distribution.
37 37
38 38 Keywords expand to the changeset data pertaining to the latest change
39 39 relative to the working directory parent of each file.
40 40
41 41 Configuration is done in the [keyword], [keywordset] and [keywordmaps]
42 42 sections of hgrc files.
43 43
44 44 Example::
45 45
46 46 [keyword]
47 47 # expand keywords in every python file except those matching "x*"
48 48 **.py =
49 49 x* = ignore
50 50
51 51 [keywordset]
52 52 # prefer svn- over cvs-like default keywordmaps
53 53 svn = True
54 54
55 55 .. note::
56 56
57 57 The more specific you are in your filename patterns the less you
58 58 lose speed in huge repositories.
59 59
60 60 For [keywordmaps] template mapping and expansion demonstration and
61 61 control run :hg:`kwdemo`. See :hg:`help templates` for a list of
62 62 available templates and filters.
63 63
64 64 Three additional date template filters are provided:
65 65
66 66 :``utcdate``: "2006/09/18 15:13:13"
67 67 :``svnutcdate``: "2006-09-18 15:13:13Z"
68 68 :``svnisodate``: "2006-09-18 08:13:13 -700 (Mon, 18 Sep 2006)"
69 69
70 70 The default template mappings (view with :hg:`kwdemo -d`) can be
71 71 replaced with customized keywords and templates. Again, run
72 72 :hg:`kwdemo` to control the results of your configuration changes.
73 73
74 74 Before changing/disabling active keywords, you must run :hg:`kwshrink`
75 75 to avoid storing expanded keywords in the change history.
76 76
77 77 To force expansion after enabling it, or a configuration change, run
78 78 :hg:`kwexpand`.
79 79
80 80 Expansions spanning more than one line and incremental expansions,
81 81 like CVS' $Log$, are not supported. A keyword template map "Log =
82 82 {desc}" expands to the first line of the changeset description.
83 83 '''
84 84
85 85 from mercurial import commands, context, cmdutil, dispatch, filelog, extensions
86 86 from mercurial import localrepo, match, patch, templatefilters, templater, util
87 87 from mercurial import scmutil, pathutil
88 88 from mercurial.hgweb import webcommands
89 89 from mercurial.i18n import _
90 90 import os, re, shutil, tempfile
91 91
92 92 commands.optionalrepo += ' kwdemo'
93 93 commands.inferrepo += ' kwexpand kwfiles kwshrink'
94 94
95 95 cmdtable = {}
96 96 command = cmdutil.command(cmdtable)
97 97 testedwith = 'internal'
98 98
99 99 # hg commands that do not act on keywords
100 100 nokwcommands = ('add addremove annotate bundle export grep incoming init log'
101 101 ' outgoing push tip verify convert email glog')
102 102
103 103 # hg commands that trigger expansion only when writing to working dir,
104 104 # not when reading filelog, and unexpand when reading from working dir
105 105 restricted = 'merge kwexpand kwshrink record qrecord resolve transplant'
106 106
107 107 # names of extensions using dorecord
108 108 recordextensions = 'record'
109 109
110 110 colortable = {
111 111 'kwfiles.enabled': 'green bold',
112 112 'kwfiles.deleted': 'cyan bold underline',
113 113 'kwfiles.enabledunknown': 'green',
114 114 'kwfiles.ignored': 'bold',
115 115 'kwfiles.ignoredunknown': 'none'
116 116 }
117 117
118 118 # date like in cvs' $Date
119 119 def utcdate(text):
120 120 ''':utcdate: Date. Returns a UTC-date in this format: "2009/08/18 11:00:13".
121 121 '''
122 122 return util.datestr((util.parsedate(text)[0], 0), '%Y/%m/%d %H:%M:%S')
123 123 # date like in svn's $Date
124 124 def svnisodate(text):
125 125 ''':svnisodate: Date. Returns a date in this format: "2009-08-18 13:00:13
126 126 +0200 (Tue, 18 Aug 2009)".
127 127 '''
128 128 return util.datestr(text, '%Y-%m-%d %H:%M:%S %1%2 (%a, %d %b %Y)')
129 129 # date like in svn's $Id
130 130 def svnutcdate(text):
131 131 ''':svnutcdate: Date. Returns a UTC-date in this format: "2009-08-18
132 132 11:00:13Z".
133 133 '''
134 134 return util.datestr((util.parsedate(text)[0], 0), '%Y-%m-%d %H:%M:%SZ')
135 135
136 136 templatefilters.filters.update({'utcdate': utcdate,
137 137 'svnisodate': svnisodate,
138 138 'svnutcdate': svnutcdate})
139 139
140 140 # make keyword tools accessible
141 141 kwtools = {'templater': None, 'hgcmd': ''}
142 142
143 143 def _defaultkwmaps(ui):
144 144 '''Returns default keywordmaps according to keywordset configuration.'''
145 145 templates = {
146 146 'Revision': '{node|short}',
147 147 'Author': '{author|user}',
148 148 }
149 149 kwsets = ({
150 150 'Date': '{date|utcdate}',
151 151 'RCSfile': '{file|basename},v',
152 152 'RCSFile': '{file|basename},v', # kept for backwards compatibility
153 153 # with hg-keyword
154 154 'Source': '{root}/{file},v',
155 155 'Id': '{file|basename},v {node|short} {date|utcdate} {author|user}',
156 156 'Header': '{root}/{file},v {node|short} {date|utcdate} {author|user}',
157 157 }, {
158 158 'Date': '{date|svnisodate}',
159 159 'Id': '{file|basename},v {node|short} {date|svnutcdate} {author|user}',
160 160 'LastChangedRevision': '{node|short}',
161 161 'LastChangedBy': '{author|user}',
162 162 'LastChangedDate': '{date|svnisodate}',
163 163 })
164 164 templates.update(kwsets[ui.configbool('keywordset', 'svn')])
165 165 return templates
166 166
167 167 def _shrinktext(text, subfunc):
168 168 '''Helper for keyword expansion removal in text.
169 169 Depending on subfunc also returns number of substitutions.'''
170 170 return subfunc(r'$\1$', text)
171 171
172 172 def _preselect(wstatus, changed):
173 173 '''Retrieves modified and added files from a working directory state
174 174 and returns the subset of each contained in given changed files
175 175 retrieved from a change context.'''
176 176 modified, added = wstatus[:2]
177 177 modified = [f for f in modified if f in changed]
178 178 added = [f for f in added if f in changed]
179 179 return modified, added
180 180
181 181
182 182 class kwtemplater(object):
183 183 '''
184 184 Sets up keyword templates, corresponding keyword regex, and
185 185 provides keyword substitution functions.
186 186 '''
187 187
188 188 def __init__(self, ui, repo, inc, exc):
189 189 self.ui = ui
190 190 self.repo = repo
191 191 self.match = match.match(repo.root, '', [], inc, exc)
192 192 self.restrict = kwtools['hgcmd'] in restricted.split()
193 193 self.postcommit = False
194 194
195 195 kwmaps = self.ui.configitems('keywordmaps')
196 196 if kwmaps: # override default templates
197 197 self.templates = dict((k, templater.parsestring(v, False))
198 198 for k, v in kwmaps)
199 199 else:
200 200 self.templates = _defaultkwmaps(self.ui)
201 201
202 202 @util.propertycache
203 203 def escape(self):
204 204 '''Returns bar-separated and escaped keywords.'''
205 205 return '|'.join(map(re.escape, self.templates.keys()))
206 206
207 207 @util.propertycache
208 208 def rekw(self):
209 209 '''Returns regex for unexpanded keywords.'''
210 210 return re.compile(r'\$(%s)\$' % self.escape)
211 211
212 212 @util.propertycache
213 213 def rekwexp(self):
214 214 '''Returns regex for expanded keywords.'''
215 215 return re.compile(r'\$(%s): [^$\n\r]*? \$' % self.escape)
216 216
217 217 def substitute(self, data, path, ctx, subfunc):
218 218 '''Replaces keywords in data with expanded template.'''
219 219 def kwsub(mobj):
220 220 kw = mobj.group(1)
221 ct = cmdutil.changeset_templater(self.ui, self.repo,
222 False, None, '', False)
223 ct.use_template(self.templates[kw])
221 ct = cmdutil.changeset_templater(self.ui, self.repo, False, None
222 self.templates[kw], '', False)
224 223 self.ui.pushbuffer()
225 224 ct.show(ctx, root=self.repo.root, file=path)
226 225 ekw = templatefilters.firstline(self.ui.popbuffer())
227 226 return '$%s: %s $' % (kw, ekw)
228 227 return subfunc(kwsub, data)
229 228
230 229 def linkctx(self, path, fileid):
231 230 '''Similar to filelog.linkrev, but returns a changectx.'''
232 231 return self.repo.filectx(path, fileid=fileid).changectx()
233 232
234 233 def expand(self, path, node, data):
235 234 '''Returns data with keywords expanded.'''
236 235 if not self.restrict and self.match(path) and not util.binary(data):
237 236 ctx = self.linkctx(path, node)
238 237 return self.substitute(data, path, ctx, self.rekw.sub)
239 238 return data
240 239
241 240 def iskwfile(self, cand, ctx):
242 241 '''Returns subset of candidates which are configured for keyword
243 242 expansion but are not symbolic links.'''
244 243 return [f for f in cand if self.match(f) and 'l' not in ctx.flags(f)]
245 244
246 245 def overwrite(self, ctx, candidates, lookup, expand, rekw=False):
247 246 '''Overwrites selected files expanding/shrinking keywords.'''
248 247 if self.restrict or lookup or self.postcommit: # exclude kw_copy
249 248 candidates = self.iskwfile(candidates, ctx)
250 249 if not candidates:
251 250 return
252 251 kwcmd = self.restrict and lookup # kwexpand/kwshrink
253 252 if self.restrict or expand and lookup:
254 253 mf = ctx.manifest()
255 254 if self.restrict or rekw:
256 255 re_kw = self.rekw
257 256 else:
258 257 re_kw = self.rekwexp
259 258 if expand:
260 259 msg = _('overwriting %s expanding keywords\n')
261 260 else:
262 261 msg = _('overwriting %s shrinking keywords\n')
263 262 for f in candidates:
264 263 if self.restrict:
265 264 data = self.repo.file(f).read(mf[f])
266 265 else:
267 266 data = self.repo.wread(f)
268 267 if util.binary(data):
269 268 continue
270 269 if expand:
271 270 if lookup:
272 271 ctx = self.linkctx(f, mf[f])
273 272 data, found = self.substitute(data, f, ctx, re_kw.subn)
274 273 elif self.restrict:
275 274 found = re_kw.search(data)
276 275 else:
277 276 data, found = _shrinktext(data, re_kw.subn)
278 277 if found:
279 278 self.ui.note(msg % f)
280 279 fp = self.repo.wopener(f, "wb", atomictemp=True)
281 280 fp.write(data)
282 281 fp.close()
283 282 if kwcmd:
284 283 self.repo.dirstate.normal(f)
285 284 elif self.postcommit:
286 285 self.repo.dirstate.normallookup(f)
287 286
288 287 def shrink(self, fname, text):
289 288 '''Returns text with all keyword substitutions removed.'''
290 289 if self.match(fname) and not util.binary(text):
291 290 return _shrinktext(text, self.rekwexp.sub)
292 291 return text
293 292
294 293 def shrinklines(self, fname, lines):
295 294 '''Returns lines with keyword substitutions removed.'''
296 295 if self.match(fname):
297 296 text = ''.join(lines)
298 297 if not util.binary(text):
299 298 return _shrinktext(text, self.rekwexp.sub).splitlines(True)
300 299 return lines
301 300
302 301 def wread(self, fname, data):
303 302 '''If in restricted mode returns data read from wdir with
304 303 keyword substitutions removed.'''
305 304 if self.restrict:
306 305 return self.shrink(fname, data)
307 306 return data
308 307
309 308 class kwfilelog(filelog.filelog):
310 309 '''
311 310 Subclass of filelog to hook into its read, add, cmp methods.
312 311 Keywords are "stored" unexpanded, and processed on reading.
313 312 '''
314 313 def __init__(self, opener, kwt, path):
315 314 super(kwfilelog, self).__init__(opener, path)
316 315 self.kwt = kwt
317 316 self.path = path
318 317
319 318 def read(self, node):
320 319 '''Expands keywords when reading filelog.'''
321 320 data = super(kwfilelog, self).read(node)
322 321 if self.renamed(node):
323 322 return data
324 323 return self.kwt.expand(self.path, node, data)
325 324
326 325 def add(self, text, meta, tr, link, p1=None, p2=None):
327 326 '''Removes keyword substitutions when adding to filelog.'''
328 327 text = self.kwt.shrink(self.path, text)
329 328 return super(kwfilelog, self).add(text, meta, tr, link, p1, p2)
330 329
331 330 def cmp(self, node, text):
332 331 '''Removes keyword substitutions for comparison.'''
333 332 text = self.kwt.shrink(self.path, text)
334 333 return super(kwfilelog, self).cmp(node, text)
335 334
336 335 def _status(ui, repo, wctx, kwt, *pats, **opts):
337 336 '''Bails out if [keyword] configuration is not active.
338 337 Returns status of working directory.'''
339 338 if kwt:
340 339 return repo.status(match=scmutil.match(wctx, pats, opts), clean=True,
341 340 unknown=opts.get('unknown') or opts.get('all'))
342 341 if ui.configitems('keyword'):
343 342 raise util.Abort(_('[keyword] patterns cannot match'))
344 343 raise util.Abort(_('no [keyword] patterns configured'))
345 344
346 345 def _kwfwrite(ui, repo, expand, *pats, **opts):
347 346 '''Selects files and passes them to kwtemplater.overwrite.'''
348 347 wctx = repo[None]
349 348 if len(wctx.parents()) > 1:
350 349 raise util.Abort(_('outstanding uncommitted merge'))
351 350 kwt = kwtools['templater']
352 351 wlock = repo.wlock()
353 352 try:
354 353 status = _status(ui, repo, wctx, kwt, *pats, **opts)
355 354 modified, added, removed, deleted, unknown, ignored, clean = status
356 355 if modified or added or removed or deleted:
357 356 raise util.Abort(_('outstanding uncommitted changes'))
358 357 kwt.overwrite(wctx, clean, True, expand)
359 358 finally:
360 359 wlock.release()
361 360
362 361 @command('kwdemo',
363 362 [('d', 'default', None, _('show default keyword template maps')),
364 363 ('f', 'rcfile', '',
365 364 _('read maps from rcfile'), _('FILE'))],
366 365 _('hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...'))
367 366 def demo(ui, repo, *args, **opts):
368 367 '''print [keywordmaps] configuration and an expansion example
369 368
370 369 Show current, custom, or default keyword template maps and their
371 370 expansions.
372 371
373 372 Extend the current configuration by specifying maps as arguments
374 373 and using -f/--rcfile to source an external hgrc file.
375 374
376 375 Use -d/--default to disable current configuration.
377 376
378 377 See :hg:`help templates` for information on templates and filters.
379 378 '''
380 379 def demoitems(section, items):
381 380 ui.write('[%s]\n' % section)
382 381 for k, v in sorted(items):
383 382 ui.write('%s = %s\n' % (k, v))
384 383
385 384 fn = 'demo.txt'
386 385 tmpdir = tempfile.mkdtemp('', 'kwdemo.')
387 386 ui.note(_('creating temporary repository at %s\n') % tmpdir)
388 387 repo = localrepo.localrepository(repo.baseui, tmpdir, True)
389 388 ui.setconfig('keyword', fn, '')
390 389 svn = ui.configbool('keywordset', 'svn')
391 390 # explicitly set keywordset for demo output
392 391 ui.setconfig('keywordset', 'svn', svn)
393 392
394 393 uikwmaps = ui.configitems('keywordmaps')
395 394 if args or opts.get('rcfile'):
396 395 ui.status(_('\n\tconfiguration using custom keyword template maps\n'))
397 396 if uikwmaps:
398 397 ui.status(_('\textending current template maps\n'))
399 398 if opts.get('default') or not uikwmaps:
400 399 if svn:
401 400 ui.status(_('\toverriding default svn keywordset\n'))
402 401 else:
403 402 ui.status(_('\toverriding default cvs keywordset\n'))
404 403 if opts.get('rcfile'):
405 404 ui.readconfig(opts.get('rcfile'))
406 405 if args:
407 406 # simulate hgrc parsing
408 407 rcmaps = ['[keywordmaps]\n'] + [a + '\n' for a in args]
409 408 fp = repo.opener('hgrc', 'w')
410 409 fp.writelines(rcmaps)
411 410 fp.close()
412 411 ui.readconfig(repo.join('hgrc'))
413 412 kwmaps = dict(ui.configitems('keywordmaps'))
414 413 elif opts.get('default'):
415 414 if svn:
416 415 ui.status(_('\n\tconfiguration using default svn keywordset\n'))
417 416 else:
418 417 ui.status(_('\n\tconfiguration using default cvs keywordset\n'))
419 418 kwmaps = _defaultkwmaps(ui)
420 419 if uikwmaps:
421 420 ui.status(_('\tdisabling current template maps\n'))
422 421 for k, v in kwmaps.iteritems():
423 422 ui.setconfig('keywordmaps', k, v)
424 423 else:
425 424 ui.status(_('\n\tconfiguration using current keyword template maps\n'))
426 425 if uikwmaps:
427 426 kwmaps = dict(uikwmaps)
428 427 else:
429 428 kwmaps = _defaultkwmaps(ui)
430 429
431 430 uisetup(ui)
432 431 reposetup(ui, repo)
433 432 ui.write('[extensions]\nkeyword =\n')
434 433 demoitems('keyword', ui.configitems('keyword'))
435 434 demoitems('keywordset', ui.configitems('keywordset'))
436 435 demoitems('keywordmaps', kwmaps.iteritems())
437 436 keywords = '$' + '$\n$'.join(sorted(kwmaps.keys())) + '$\n'
438 437 repo.wopener.write(fn, keywords)
439 438 repo[None].add([fn])
440 439 ui.note(_('\nkeywords written to %s:\n') % fn)
441 440 ui.note(keywords)
442 441 wlock = repo.wlock()
443 442 try:
444 443 repo.dirstate.setbranch('demobranch')
445 444 finally:
446 445 wlock.release()
447 446 for name, cmd in ui.configitems('hooks'):
448 447 if name.split('.', 1)[0].find('commit') > -1:
449 448 repo.ui.setconfig('hooks', name, '')
450 449 msg = _('hg keyword configuration and expansion example')
451 450 ui.note(("hg ci -m '%s'\n" % msg))
452 451 repo.commit(text=msg)
453 452 ui.status(_('\n\tkeywords expanded\n'))
454 453 ui.write(repo.wread(fn))
455 454 shutil.rmtree(tmpdir, ignore_errors=True)
456 455
457 456 @command('kwexpand', commands.walkopts, _('hg kwexpand [OPTION]... [FILE]...'))
458 457 def expand(ui, repo, *pats, **opts):
459 458 '''expand keywords in the working directory
460 459
461 460 Run after (re)enabling keyword expansion.
462 461
463 462 kwexpand refuses to run if given files contain local changes.
464 463 '''
465 464 # 3rd argument sets expansion to True
466 465 _kwfwrite(ui, repo, True, *pats, **opts)
467 466
468 467 @command('kwfiles',
469 468 [('A', 'all', None, _('show keyword status flags of all files')),
470 469 ('i', 'ignore', None, _('show files excluded from expansion')),
471 470 ('u', 'unknown', None, _('only show unknown (not tracked) files')),
472 471 ] + commands.walkopts,
473 472 _('hg kwfiles [OPTION]... [FILE]...'))
474 473 def files(ui, repo, *pats, **opts):
475 474 '''show files configured for keyword expansion
476 475
477 476 List which files in the working directory are matched by the
478 477 [keyword] configuration patterns.
479 478
480 479 Useful to prevent inadvertent keyword expansion and to speed up
481 480 execution by including only files that are actual candidates for
482 481 expansion.
483 482
484 483 See :hg:`help keyword` on how to construct patterns both for
485 484 inclusion and exclusion of files.
486 485
487 486 With -A/--all and -v/--verbose the codes used to show the status
488 487 of files are::
489 488
490 489 K = keyword expansion candidate
491 490 k = keyword expansion candidate (not tracked)
492 491 I = ignored
493 492 i = ignored (not tracked)
494 493 '''
495 494 kwt = kwtools['templater']
496 495 wctx = repo[None]
497 496 status = _status(ui, repo, wctx, kwt, *pats, **opts)
498 497 cwd = pats and repo.getcwd() or ''
499 498 modified, added, removed, deleted, unknown, ignored, clean = status
500 499 files = []
501 500 if not opts.get('unknown') or opts.get('all'):
502 501 files = sorted(modified + added + clean)
503 502 kwfiles = kwt.iskwfile(files, wctx)
504 503 kwdeleted = kwt.iskwfile(deleted, wctx)
505 504 kwunknown = kwt.iskwfile(unknown, wctx)
506 505 if not opts.get('ignore') or opts.get('all'):
507 506 showfiles = kwfiles, kwdeleted, kwunknown
508 507 else:
509 508 showfiles = [], [], []
510 509 if opts.get('all') or opts.get('ignore'):
511 510 showfiles += ([f for f in files if f not in kwfiles],
512 511 [f for f in unknown if f not in kwunknown])
513 512 kwlabels = 'enabled deleted enabledunknown ignored ignoredunknown'.split()
514 513 kwstates = zip(kwlabels, 'K!kIi', showfiles)
515 514 fm = ui.formatter('kwfiles', opts)
516 515 fmt = '%.0s%s\n'
517 516 if opts.get('all') or ui.verbose:
518 517 fmt = '%s %s\n'
519 518 for kwstate, char, filenames in kwstates:
520 519 label = 'kwfiles.' + kwstate
521 520 for f in filenames:
522 521 fm.startitem()
523 522 fm.write('kwstatus path', fmt, char,
524 523 repo.pathto(f, cwd), label=label)
525 524 fm.end()
526 525
527 526 @command('kwshrink', commands.walkopts, _('hg kwshrink [OPTION]... [FILE]...'))
528 527 def shrink(ui, repo, *pats, **opts):
529 528 '''revert expanded keywords in the working directory
530 529
531 530 Must be run before changing/disabling active keywords.
532 531
533 532 kwshrink refuses to run if given files contain local changes.
534 533 '''
535 534 # 3rd argument sets expansion to False
536 535 _kwfwrite(ui, repo, False, *pats, **opts)
537 536
538 537
539 538 def uisetup(ui):
540 539 ''' Monkeypatches dispatch._parse to retrieve user command.'''
541 540
542 541 def kwdispatch_parse(orig, ui, args):
543 542 '''Monkeypatch dispatch._parse to obtain running hg command.'''
544 543 cmd, func, args, options, cmdoptions = orig(ui, args)
545 544 kwtools['hgcmd'] = cmd
546 545 return cmd, func, args, options, cmdoptions
547 546
548 547 extensions.wrapfunction(dispatch, '_parse', kwdispatch_parse)
549 548
550 549 def reposetup(ui, repo):
551 550 '''Sets up repo as kwrepo for keyword substitution.
552 551 Overrides file method to return kwfilelog instead of filelog
553 552 if file matches user configuration.
554 553 Wraps commit to overwrite configured files with updated
555 554 keyword substitutions.
556 555 Monkeypatches patch and webcommands.'''
557 556
558 557 try:
559 558 if (not repo.local() or kwtools['hgcmd'] in nokwcommands.split()
560 559 or '.hg' in util.splitpath(repo.root)
561 560 or repo._url.startswith('bundle:')):
562 561 return
563 562 except AttributeError:
564 563 pass
565 564
566 565 inc, exc = [], ['.hg*']
567 566 for pat, opt in ui.configitems('keyword'):
568 567 if opt != 'ignore':
569 568 inc.append(pat)
570 569 else:
571 570 exc.append(pat)
572 571 if not inc:
573 572 return
574 573
575 574 kwtools['templater'] = kwt = kwtemplater(ui, repo, inc, exc)
576 575
577 576 class kwrepo(repo.__class__):
578 577 def file(self, f):
579 578 if f[0] == '/':
580 579 f = f[1:]
581 580 return kwfilelog(self.sopener, kwt, f)
582 581
583 582 def wread(self, filename):
584 583 data = super(kwrepo, self).wread(filename)
585 584 return kwt.wread(filename, data)
586 585
587 586 def commit(self, *args, **opts):
588 587 # use custom commitctx for user commands
589 588 # other extensions can still wrap repo.commitctx directly
590 589 self.commitctx = self.kwcommitctx
591 590 try:
592 591 return super(kwrepo, self).commit(*args, **opts)
593 592 finally:
594 593 del self.commitctx
595 594
596 595 def kwcommitctx(self, ctx, error=False):
597 596 n = super(kwrepo, self).commitctx(ctx, error)
598 597 # no lock needed, only called from repo.commit() which already locks
599 598 if not kwt.postcommit:
600 599 restrict = kwt.restrict
601 600 kwt.restrict = True
602 601 kwt.overwrite(self[n], sorted(ctx.added() + ctx.modified()),
603 602 False, True)
604 603 kwt.restrict = restrict
605 604 return n
606 605
607 606 def rollback(self, dryrun=False, force=False):
608 607 wlock = self.wlock()
609 608 try:
610 609 if not dryrun:
611 610 changed = self['.'].files()
612 611 ret = super(kwrepo, self).rollback(dryrun, force)
613 612 if not dryrun:
614 613 ctx = self['.']
615 614 modified, added = _preselect(self[None].status(), changed)
616 615 kwt.overwrite(ctx, modified, True, True)
617 616 kwt.overwrite(ctx, added, True, False)
618 617 return ret
619 618 finally:
620 619 wlock.release()
621 620
622 621 # monkeypatches
623 622 def kwpatchfile_init(orig, self, ui, gp, backend, store, eolmode=None):
624 623 '''Monkeypatch/wrap patch.patchfile.__init__ to avoid
625 624 rejects or conflicts due to expanded keywords in working dir.'''
626 625 orig(self, ui, gp, backend, store, eolmode)
627 626 # shrink keywords read from working dir
628 627 self.lines = kwt.shrinklines(self.fname, self.lines)
629 628
630 629 def kw_diff(orig, repo, node1=None, node2=None, match=None, changes=None,
631 630 opts=None, prefix=''):
632 631 '''Monkeypatch patch.diff to avoid expansion.'''
633 632 kwt.restrict = True
634 633 return orig(repo, node1, node2, match, changes, opts, prefix)
635 634
636 635 def kwweb_skip(orig, web, req, tmpl):
637 636 '''Wraps webcommands.x turning off keyword expansion.'''
638 637 kwt.match = util.never
639 638 return orig(web, req, tmpl)
640 639
641 640 def kw_amend(orig, ui, repo, commitfunc, old, extra, pats, opts):
642 641 '''Wraps cmdutil.amend expanding keywords after amend.'''
643 642 wlock = repo.wlock()
644 643 try:
645 644 kwt.postcommit = True
646 645 newid = orig(ui, repo, commitfunc, old, extra, pats, opts)
647 646 if newid != old.node():
648 647 ctx = repo[newid]
649 648 kwt.restrict = True
650 649 kwt.overwrite(ctx, ctx.files(), False, True)
651 650 kwt.restrict = False
652 651 return newid
653 652 finally:
654 653 wlock.release()
655 654
656 655 def kw_copy(orig, ui, repo, pats, opts, rename=False):
657 656 '''Wraps cmdutil.copy so that copy/rename destinations do not
658 657 contain expanded keywords.
659 658 Note that the source of a regular file destination may also be a
660 659 symlink:
661 660 hg cp sym x -> x is symlink
662 661 cp sym x; hg cp -A sym x -> x is file (maybe expanded keywords)
663 662 For the latter we have to follow the symlink to find out whether its
664 663 target is configured for expansion and we therefore must unexpand the
665 664 keywords in the destination.'''
666 665 wlock = repo.wlock()
667 666 try:
668 667 orig(ui, repo, pats, opts, rename)
669 668 if opts.get('dry_run'):
670 669 return
671 670 wctx = repo[None]
672 671 cwd = repo.getcwd()
673 672
674 673 def haskwsource(dest):
675 674 '''Returns true if dest is a regular file and configured for
676 675 expansion or a symlink which points to a file configured for
677 676 expansion. '''
678 677 source = repo.dirstate.copied(dest)
679 678 if 'l' in wctx.flags(source):
680 679 source = pathutil.canonpath(repo.root, cwd,
681 680 os.path.realpath(source))
682 681 return kwt.match(source)
683 682
684 683 candidates = [f for f in repo.dirstate.copies() if
685 684 'l' not in wctx.flags(f) and haskwsource(f)]
686 685 kwt.overwrite(wctx, candidates, False, False)
687 686 finally:
688 687 wlock.release()
689 688
690 689 def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts):
691 690 '''Wraps record.dorecord expanding keywords after recording.'''
692 691 wlock = repo.wlock()
693 692 try:
694 693 # record returns 0 even when nothing has changed
695 694 # therefore compare nodes before and after
696 695 kwt.postcommit = True
697 696 ctx = repo['.']
698 697 wstatus = repo[None].status()
699 698 ret = orig(ui, repo, commitfunc, *pats, **opts)
700 699 recctx = repo['.']
701 700 if ctx != recctx:
702 701 modified, added = _preselect(wstatus, recctx.files())
703 702 kwt.restrict = False
704 703 kwt.overwrite(recctx, modified, False, True)
705 704 kwt.overwrite(recctx, added, False, True, True)
706 705 kwt.restrict = True
707 706 return ret
708 707 finally:
709 708 wlock.release()
710 709
711 710 def kwfilectx_cmp(orig, self, fctx):
712 711 # keyword affects data size, comparing wdir and filelog size does
713 712 # not make sense
714 713 if (fctx._filerev is None and
715 714 (self._repo._encodefilterpats or
716 715 kwt.match(fctx.path()) and 'l' not in fctx.flags() or
717 716 self.size() - 4 == fctx.size()) or
718 717 self.size() == fctx.size()):
719 718 return self._filelog.cmp(self._filenode, fctx.data())
720 719 return True
721 720
722 721 extensions.wrapfunction(context.filectx, 'cmp', kwfilectx_cmp)
723 722 extensions.wrapfunction(patch.patchfile, '__init__', kwpatchfile_init)
724 723 extensions.wrapfunction(patch, 'diff', kw_diff)
725 724 extensions.wrapfunction(cmdutil, 'amend', kw_amend)
726 725 extensions.wrapfunction(cmdutil, 'copy', kw_copy)
727 726 for c in 'annotate changeset rev filediff diff'.split():
728 727 extensions.wrapfunction(webcommands, c, kwweb_skip)
729 728 for name in recordextensions.split():
730 729 try:
731 730 record = extensions.find(name)
732 731 extensions.wrapfunction(record, 'dorecord', kw_dorecord)
733 732 except KeyError:
734 733 pass
735 734
736 735 repo.__class__ = kwrepo
@@ -1,414 +1,413
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 or any later version.
7 7
8 8 '''hooks for sending email push notifications
9 9
10 10 This extension implements hooks to send email notifications when
11 11 changesets are sent from or received by the local repository.
12 12
13 13 First, enable the extension as explained in :hg:`help extensions`, and
14 14 register the hook you want to run. ``incoming`` and ``changegroup`` hooks
15 15 are run when changesets are received, while ``outgoing`` hooks are for
16 16 changesets sent to another repository::
17 17
18 18 [hooks]
19 19 # one email for each incoming changeset
20 20 incoming.notify = python:hgext.notify.hook
21 21 # one email for all incoming changesets
22 22 changegroup.notify = python:hgext.notify.hook
23 23
24 24 # one email for all outgoing changesets
25 25 outgoing.notify = python:hgext.notify.hook
26 26
27 27 This registers the hooks. To enable notification, subscribers must
28 28 be assigned to repositories. The ``[usersubs]`` section maps multiple
29 29 repositories to a given recipient. The ``[reposubs]`` section maps
30 30 multiple recipients to a single repository::
31 31
32 32 [usersubs]
33 33 # key is subscriber email, value is a comma-separated list of repo patterns
34 34 user@host = pattern
35 35
36 36 [reposubs]
37 37 # key is repo pattern, value is a comma-separated list of subscriber emails
38 38 pattern = user@host
39 39
40 40 A ``pattern`` is a ``glob`` matching the absolute path to a repository,
41 41 optionally combined with a revset expression. A revset expression, if
42 42 present, is separated from the glob by a hash. Example::
43 43
44 44 [reposubs]
45 45 */widgets#branch(release) = qa-team@example.com
46 46
47 47 This sends to ``qa-team@example.com`` whenever a changeset on the ``release``
48 48 branch triggers a notification in any repository ending in ``widgets``.
49 49
50 50 In order to place them under direct user management, ``[usersubs]`` and
51 51 ``[reposubs]`` sections may be placed in a separate ``hgrc`` file and
52 52 incorporated by reference::
53 53
54 54 [notify]
55 55 config = /path/to/subscriptionsfile
56 56
57 57 Notifications will not be sent until the ``notify.test`` value is set
58 58 to ``False``; see below.
59 59
60 60 Notifications content can be tweaked with the following configuration entries:
61 61
62 62 notify.test
63 63 If ``True``, print messages to stdout instead of sending them. Default: True.
64 64
65 65 notify.sources
66 66 Space-separated list of change sources. Notifications are activated only
67 67 when a changeset's source is in this list. Sources may be:
68 68
69 69 :``serve``: changesets received via http or ssh
70 70 :``pull``: changesets received via ``hg pull``
71 71 :``unbundle``: changesets received via ``hg unbundle``
72 72 :``push``: changesets sent or received via ``hg push``
73 73 :``bundle``: changesets sent via ``hg unbundle``
74 74
75 75 Default: serve.
76 76
77 77 notify.strip
78 78 Number of leading slashes to strip from url paths. By default, notifications
79 79 reference repositories with their absolute path. ``notify.strip`` lets you
80 80 turn them into relative paths. For example, ``notify.strip=3`` will change
81 81 ``/long/path/repository`` into ``repository``. Default: 0.
82 82
83 83 notify.domain
84 84 Default email domain for sender or recipients with no explicit domain.
85 85
86 86 notify.style
87 87 Style file to use when formatting emails.
88 88
89 89 notify.template
90 90 Template to use when formatting emails.
91 91
92 92 notify.incoming
93 93 Template to use when run as an incoming hook, overriding ``notify.template``.
94 94
95 95 notify.outgoing
96 96 Template to use when run as an outgoing hook, overriding ``notify.template``.
97 97
98 98 notify.changegroup
99 99 Template to use when running as a changegroup hook, overriding
100 100 ``notify.template``.
101 101
102 102 notify.maxdiff
103 103 Maximum number of diff lines to include in notification email. Set to 0
104 104 to disable the diff, or -1 to include all of it. Default: 300.
105 105
106 106 notify.maxsubject
107 107 Maximum number of characters in email's subject line. Default: 67.
108 108
109 109 notify.diffstat
110 110 Set to True to include a diffstat before diff content. Default: True.
111 111
112 112 notify.merge
113 113 If True, send notifications for merge changesets. Default: True.
114 114
115 115 notify.mbox
116 116 If set, append mails to this mbox file instead of sending. Default: None.
117 117
118 118 notify.fromauthor
119 119 If set, use the committer of the first changeset in a changegroup for
120 120 the "From" field of the notification mail. If not set, take the user
121 121 from the pushing repo. Default: False.
122 122
123 123 If set, the following entries will also be used to customize the
124 124 notifications:
125 125
126 126 email.from
127 127 Email ``From`` address to use if none can be found in the generated
128 128 email content.
129 129
130 130 web.baseurl
131 131 Root repository URL to combine with repository paths when making
132 132 references. See also ``notify.strip``.
133 133
134 134 '''
135 135
136 136 import email, socket, time
137 137 # On python2.4 you have to import this by name or they fail to
138 138 # load. This was not a problem on Python 2.7.
139 139 import email.Parser
140 140 from mercurial.i18n import _
141 141 from mercurial import patch, cmdutil, templater, util, mail
142 142 import fnmatch
143 143
144 144 testedwith = 'internal'
145 145
146 146 # template for single changeset can include email headers.
147 147 single_template = '''
148 148 Subject: changeset in {webroot}: {desc|firstline|strip}
149 149 From: {author}
150 150
151 151 changeset {node|short} in {root}
152 152 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
153 153 description:
154 154 \t{desc|tabindent|strip}
155 155 '''.lstrip()
156 156
157 157 # template for multiple changesets should not contain email headers,
158 158 # because only first set of headers will be used and result will look
159 159 # strange.
160 160 multiple_template = '''
161 161 changeset {node|short} in {root}
162 162 details: {baseurl}{webroot}?cmd=changeset;node={node|short}
163 163 summary: {desc|firstline}
164 164 '''
165 165
166 166 deftemplates = {
167 167 'changegroup': multiple_template,
168 168 }
169 169
170 170 class notifier(object):
171 171 '''email notification class.'''
172 172
173 173 def __init__(self, ui, repo, hooktype):
174 174 self.ui = ui
175 175 cfg = self.ui.config('notify', 'config')
176 176 if cfg:
177 177 self.ui.readconfig(cfg, sections=['usersubs', 'reposubs'])
178 178 self.repo = repo
179 179 self.stripcount = int(self.ui.config('notify', 'strip', 0))
180 180 self.root = self.strip(self.repo.root)
181 181 self.domain = self.ui.config('notify', 'domain')
182 182 self.mbox = self.ui.config('notify', 'mbox')
183 183 self.test = self.ui.configbool('notify', 'test', True)
184 184 self.charsets = mail._charsets(self.ui)
185 185 self.subs = self.subscribers()
186 186 self.merge = self.ui.configbool('notify', 'merge', True)
187 187
188 188 mapfile = self.ui.config('notify', 'style')
189 189 template = (self.ui.config('notify', hooktype) or
190 190 self.ui.config('notify', 'template'))
191 self.t = cmdutil.changeset_templater(self.ui, self.repo,
192 False, None, mapfile, False)
193 191 if not mapfile and not template:
194 192 template = deftemplates.get(hooktype) or single_template
195 193 if template:
196 194 template = templater.parsestring(template, quoted=False)
197 self.t.use_template(template)
195 self.t = cmdutil.changeset_templater(self.ui, self.repo, False, None,
196 template, mapfile, False)
198 197
199 198 def strip(self, path):
200 199 '''strip leading slashes from local path, turn into web-safe path.'''
201 200
202 201 path = util.pconvert(path)
203 202 count = self.stripcount
204 203 while count > 0:
205 204 c = path.find('/')
206 205 if c == -1:
207 206 break
208 207 path = path[c + 1:]
209 208 count -= 1
210 209 return path
211 210
212 211 def fixmail(self, addr):
213 212 '''try to clean up email addresses.'''
214 213
215 214 addr = util.email(addr.strip())
216 215 if self.domain:
217 216 a = addr.find('@localhost')
218 217 if a != -1:
219 218 addr = addr[:a]
220 219 if '@' not in addr:
221 220 return addr + '@' + self.domain
222 221 return addr
223 222
224 223 def subscribers(self):
225 224 '''return list of email addresses of subscribers to this repo.'''
226 225 subs = set()
227 226 for user, pats in self.ui.configitems('usersubs'):
228 227 for pat in pats.split(','):
229 228 if '#' in pat:
230 229 pat, revs = pat.split('#', 1)
231 230 else:
232 231 revs = None
233 232 if fnmatch.fnmatch(self.repo.root, pat.strip()):
234 233 subs.add((self.fixmail(user), revs))
235 234 for pat, users in self.ui.configitems('reposubs'):
236 235 if '#' in pat:
237 236 pat, revs = pat.split('#', 1)
238 237 else:
239 238 revs = None
240 239 if fnmatch.fnmatch(self.repo.root, pat):
241 240 for user in users.split(','):
242 241 subs.add((self.fixmail(user), revs))
243 242 return [(mail.addressencode(self.ui, s, self.charsets, self.test), r)
244 243 for s, r in sorted(subs)]
245 244
246 245 def node(self, ctx, **props):
247 246 '''format one changeset, unless it is a suppressed merge.'''
248 247 if not self.merge and len(ctx.parents()) > 1:
249 248 return False
250 249 self.t.show(ctx, changes=ctx.changeset(),
251 250 baseurl=self.ui.config('web', 'baseurl'),
252 251 root=self.repo.root, webroot=self.root, **props)
253 252 return True
254 253
255 254 def skipsource(self, source):
256 255 '''true if incoming changes from this source should be skipped.'''
257 256 ok_sources = self.ui.config('notify', 'sources', 'serve').split()
258 257 return source not in ok_sources
259 258
260 259 def send(self, ctx, count, data):
261 260 '''send message.'''
262 261
263 262 # Select subscribers by revset
264 263 subs = set()
265 264 for sub, spec in self.subs:
266 265 if spec is None:
267 266 subs.add(sub)
268 267 continue
269 268 revs = self.repo.revs('%r and %d:', spec, ctx.rev())
270 269 if len(revs):
271 270 subs.add(sub)
272 271 continue
273 272 if len(subs) == 0:
274 273 self.ui.debug('notify: no subscribers to selected repo '
275 274 'and revset\n')
276 275 return
277 276
278 277 p = email.Parser.Parser()
279 278 try:
280 279 msg = p.parsestr(data)
281 280 except email.Errors.MessageParseError, inst:
282 281 raise util.Abort(inst)
283 282
284 283 # store sender and subject
285 284 sender, subject = msg['From'], msg['Subject']
286 285 del msg['From'], msg['Subject']
287 286
288 287 if not msg.is_multipart():
289 288 # create fresh mime message from scratch
290 289 # (multipart templates must take care of this themselves)
291 290 headers = msg.items()
292 291 payload = msg.get_payload()
293 292 # for notification prefer readability over data precision
294 293 msg = mail.mimeencode(self.ui, payload, self.charsets, self.test)
295 294 # reinstate custom headers
296 295 for k, v in headers:
297 296 msg[k] = v
298 297
299 298 msg['Date'] = util.datestr(format="%a, %d %b %Y %H:%M:%S %1%2")
300 299
301 300 # try to make subject line exist and be useful
302 301 if not subject:
303 302 if count > 1:
304 303 subject = _('%s: %d new changesets') % (self.root, count)
305 304 else:
306 305 s = ctx.description().lstrip().split('\n', 1)[0].rstrip()
307 306 subject = '%s: %s' % (self.root, s)
308 307 maxsubject = int(self.ui.config('notify', 'maxsubject', 67))
309 308 if maxsubject:
310 309 subject = util.ellipsis(subject, maxsubject)
311 310 msg['Subject'] = mail.headencode(self.ui, subject,
312 311 self.charsets, self.test)
313 312
314 313 # try to make message have proper sender
315 314 if not sender:
316 315 sender = self.ui.config('email', 'from') or self.ui.username()
317 316 if '@' not in sender or '@localhost' in sender:
318 317 sender = self.fixmail(sender)
319 318 msg['From'] = mail.addressencode(self.ui, sender,
320 319 self.charsets, self.test)
321 320
322 321 msg['X-Hg-Notification'] = 'changeset %s' % ctx
323 322 if not msg['Message-Id']:
324 323 msg['Message-Id'] = ('<hg.%s.%s.%s@%s>' %
325 324 (ctx, int(time.time()),
326 325 hash(self.repo.root), socket.getfqdn()))
327 326 msg['To'] = ', '.join(sorted(subs))
328 327
329 328 msgtext = msg.as_string()
330 329 if self.test:
331 330 self.ui.write(msgtext)
332 331 if not msgtext.endswith('\n'):
333 332 self.ui.write('\n')
334 333 else:
335 334 self.ui.status(_('notify: sending %d subscribers %d changes\n') %
336 335 (len(subs), count))
337 336 mail.sendmail(self.ui, util.email(msg['From']),
338 337 subs, msgtext, mbox=self.mbox)
339 338
340 339 def diff(self, ctx, ref=None):
341 340
342 341 maxdiff = int(self.ui.config('notify', 'maxdiff', 300))
343 342 prev = ctx.p1().node()
344 343 ref = ref and ref.node() or ctx.node()
345 344 chunks = patch.diff(self.repo, prev, ref, opts=patch.diffopts(self.ui))
346 345 difflines = ''.join(chunks).splitlines()
347 346
348 347 if self.ui.configbool('notify', 'diffstat', True):
349 348 s = patch.diffstat(difflines)
350 349 # s may be nil, don't include the header if it is
351 350 if s:
352 351 self.ui.write('\ndiffstat:\n\n%s' % s)
353 352
354 353 if maxdiff == 0:
355 354 return
356 355 elif maxdiff > 0 and len(difflines) > maxdiff:
357 356 msg = _('\ndiffs (truncated from %d to %d lines):\n\n')
358 357 self.ui.write(msg % (len(difflines), maxdiff))
359 358 difflines = difflines[:maxdiff]
360 359 elif difflines:
361 360 self.ui.write(_('\ndiffs (%d lines):\n\n') % len(difflines))
362 361
363 362 self.ui.write("\n".join(difflines))
364 363
365 364 def hook(ui, repo, hooktype, node=None, source=None, **kwargs):
366 365 '''send email notifications to interested subscribers.
367 366
368 367 if used as changegroup hook, send one email for all changesets in
369 368 changegroup. else send one email per changeset.'''
370 369
371 370 n = notifier(ui, repo, hooktype)
372 371 ctx = repo[node]
373 372
374 373 if not n.subs:
375 374 ui.debug('notify: no subscribers to repository %s\n' % n.root)
376 375 return
377 376 if n.skipsource(source):
378 377 ui.debug('notify: changes have source "%s" - skipping\n' % source)
379 378 return
380 379
381 380 ui.pushbuffer()
382 381 data = ''
383 382 count = 0
384 383 author = ''
385 384 if hooktype == 'changegroup' or hooktype == 'outgoing':
386 385 start, end = ctx.rev(), len(repo)
387 386 for rev in xrange(start, end):
388 387 if n.node(repo[rev]):
389 388 count += 1
390 389 if not author:
391 390 author = repo[rev].user()
392 391 else:
393 392 data += ui.popbuffer()
394 393 ui.note(_('notify: suppressing notification for merge %d:%s\n')
395 394 % (rev, repo[rev].hex()[:12]))
396 395 ui.pushbuffer()
397 396 if count:
398 397 n.diff(ctx, repo['tip'])
399 398 else:
400 399 if not n.node(ctx):
401 400 ui.popbuffer()
402 401 ui.note(_('notify: suppressing notification for merge %d:%s\n') %
403 402 (ctx.rev(), ctx.hex()[:12]))
404 403 return
405 404 count += 1
406 405 n.diff(ctx)
407 406
408 407 data += ui.popbuffer()
409 408 fromauthor = ui.config('notify', 'fromauthor')
410 409 if author and fromauthor:
411 410 data = '\n'.join(['From: %s' % author, data])
412 411
413 412 if count:
414 413 n.send(ctx, count, data)
@@ -1,2318 +1,2315
1 1 # cmdutil.py - help for command processing in mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.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 or any later version.
7 7
8 8 from node import hex, nullid, nullrev, short
9 9 from i18n import _
10 10 import os, sys, errno, re, tempfile
11 11 import util, scmutil, templater, patch, error, templatekw, revlog, copies
12 12 import match as matchmod
13 13 import context, repair, graphmod, revset, phases, obsolete, pathutil
14 14 import changelog
15 15 import bookmarks
16 16 import lock as lockmod
17 17
18 18 def parsealiases(cmd):
19 19 return cmd.lstrip("^").split("|")
20 20
21 21 def findpossible(cmd, table, strict=False):
22 22 """
23 23 Return cmd -> (aliases, command table entry)
24 24 for each matching command.
25 25 Return debug commands (or their aliases) only if no normal command matches.
26 26 """
27 27 choice = {}
28 28 debugchoice = {}
29 29
30 30 if cmd in table:
31 31 # short-circuit exact matches, "log" alias beats "^log|history"
32 32 keys = [cmd]
33 33 else:
34 34 keys = table.keys()
35 35
36 36 for e in keys:
37 37 aliases = parsealiases(e)
38 38 found = None
39 39 if cmd in aliases:
40 40 found = cmd
41 41 elif not strict:
42 42 for a in aliases:
43 43 if a.startswith(cmd):
44 44 found = a
45 45 break
46 46 if found is not None:
47 47 if aliases[0].startswith("debug") or found.startswith("debug"):
48 48 debugchoice[found] = (aliases, table[e])
49 49 else:
50 50 choice[found] = (aliases, table[e])
51 51
52 52 if not choice and debugchoice:
53 53 choice = debugchoice
54 54
55 55 return choice
56 56
57 57 def findcmd(cmd, table, strict=True):
58 58 """Return (aliases, command table entry) for command string."""
59 59 choice = findpossible(cmd, table, strict)
60 60
61 61 if cmd in choice:
62 62 return choice[cmd]
63 63
64 64 if len(choice) > 1:
65 65 clist = choice.keys()
66 66 clist.sort()
67 67 raise error.AmbiguousCommand(cmd, clist)
68 68
69 69 if choice:
70 70 return choice.values()[0]
71 71
72 72 raise error.UnknownCommand(cmd)
73 73
74 74 def findrepo(p):
75 75 while not os.path.isdir(os.path.join(p, ".hg")):
76 76 oldp, p = p, os.path.dirname(p)
77 77 if p == oldp:
78 78 return None
79 79
80 80 return p
81 81
82 82 def bailifchanged(repo):
83 83 if repo.dirstate.p2() != nullid:
84 84 raise util.Abort(_('outstanding uncommitted merge'))
85 85 modified, added, removed, deleted = repo.status()[:4]
86 86 if modified or added or removed or deleted:
87 87 raise util.Abort(_('uncommitted changes'))
88 88 ctx = repo[None]
89 89 for s in sorted(ctx.substate):
90 90 if ctx.sub(s).dirty():
91 91 raise util.Abort(_("uncommitted changes in subrepo %s") % s)
92 92
93 93 def logmessage(ui, opts):
94 94 """ get the log message according to -m and -l option """
95 95 message = opts.get('message')
96 96 logfile = opts.get('logfile')
97 97
98 98 if message and logfile:
99 99 raise util.Abort(_('options --message and --logfile are mutually '
100 100 'exclusive'))
101 101 if not message and logfile:
102 102 try:
103 103 if logfile == '-':
104 104 message = ui.fin.read()
105 105 else:
106 106 message = '\n'.join(util.readfile(logfile).splitlines())
107 107 except IOError, inst:
108 108 raise util.Abort(_("can't read commit message '%s': %s") %
109 109 (logfile, inst.strerror))
110 110 return message
111 111
112 112 def loglimit(opts):
113 113 """get the log limit according to option -l/--limit"""
114 114 limit = opts.get('limit')
115 115 if limit:
116 116 try:
117 117 limit = int(limit)
118 118 except ValueError:
119 119 raise util.Abort(_('limit must be a positive integer'))
120 120 if limit <= 0:
121 121 raise util.Abort(_('limit must be positive'))
122 122 else:
123 123 limit = None
124 124 return limit
125 125
126 126 def makefilename(repo, pat, node, desc=None,
127 127 total=None, seqno=None, revwidth=None, pathname=None):
128 128 node_expander = {
129 129 'H': lambda: hex(node),
130 130 'R': lambda: str(repo.changelog.rev(node)),
131 131 'h': lambda: short(node),
132 132 'm': lambda: re.sub('[^\w]', '_', str(desc))
133 133 }
134 134 expander = {
135 135 '%': lambda: '%',
136 136 'b': lambda: os.path.basename(repo.root),
137 137 }
138 138
139 139 try:
140 140 if node:
141 141 expander.update(node_expander)
142 142 if node:
143 143 expander['r'] = (lambda:
144 144 str(repo.changelog.rev(node)).zfill(revwidth or 0))
145 145 if total is not None:
146 146 expander['N'] = lambda: str(total)
147 147 if seqno is not None:
148 148 expander['n'] = lambda: str(seqno)
149 149 if total is not None and seqno is not None:
150 150 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
151 151 if pathname is not None:
152 152 expander['s'] = lambda: os.path.basename(pathname)
153 153 expander['d'] = lambda: os.path.dirname(pathname) or '.'
154 154 expander['p'] = lambda: pathname
155 155
156 156 newname = []
157 157 patlen = len(pat)
158 158 i = 0
159 159 while i < patlen:
160 160 c = pat[i]
161 161 if c == '%':
162 162 i += 1
163 163 c = pat[i]
164 164 c = expander[c]()
165 165 newname.append(c)
166 166 i += 1
167 167 return ''.join(newname)
168 168 except KeyError, inst:
169 169 raise util.Abort(_("invalid format spec '%%%s' in output filename") %
170 170 inst.args[0])
171 171
172 172 def makefileobj(repo, pat, node=None, desc=None, total=None,
173 173 seqno=None, revwidth=None, mode='wb', modemap=None,
174 174 pathname=None):
175 175
176 176 writable = mode not in ('r', 'rb')
177 177
178 178 if not pat or pat == '-':
179 179 fp = writable and repo.ui.fout or repo.ui.fin
180 180 if util.safehasattr(fp, 'fileno'):
181 181 return os.fdopen(os.dup(fp.fileno()), mode)
182 182 else:
183 183 # if this fp can't be duped properly, return
184 184 # a dummy object that can be closed
185 185 class wrappedfileobj(object):
186 186 noop = lambda x: None
187 187 def __init__(self, f):
188 188 self.f = f
189 189 def __getattr__(self, attr):
190 190 if attr == 'close':
191 191 return self.noop
192 192 else:
193 193 return getattr(self.f, attr)
194 194
195 195 return wrappedfileobj(fp)
196 196 if util.safehasattr(pat, 'write') and writable:
197 197 return pat
198 198 if util.safehasattr(pat, 'read') and 'r' in mode:
199 199 return pat
200 200 fn = makefilename(repo, pat, node, desc, total, seqno, revwidth, pathname)
201 201 if modemap is not None:
202 202 mode = modemap.get(fn, mode)
203 203 if mode == 'wb':
204 204 modemap[fn] = 'ab'
205 205 return open(fn, mode)
206 206
207 207 def openrevlog(repo, cmd, file_, opts):
208 208 """opens the changelog, manifest, a filelog or a given revlog"""
209 209 cl = opts['changelog']
210 210 mf = opts['manifest']
211 211 msg = None
212 212 if cl and mf:
213 213 msg = _('cannot specify --changelog and --manifest at the same time')
214 214 elif cl or mf:
215 215 if file_:
216 216 msg = _('cannot specify filename with --changelog or --manifest')
217 217 elif not repo:
218 218 msg = _('cannot specify --changelog or --manifest '
219 219 'without a repository')
220 220 if msg:
221 221 raise util.Abort(msg)
222 222
223 223 r = None
224 224 if repo:
225 225 if cl:
226 226 r = repo.changelog
227 227 elif mf:
228 228 r = repo.manifest
229 229 elif file_:
230 230 filelog = repo.file(file_)
231 231 if len(filelog):
232 232 r = filelog
233 233 if not r:
234 234 if not file_:
235 235 raise error.CommandError(cmd, _('invalid arguments'))
236 236 if not os.path.isfile(file_):
237 237 raise util.Abort(_("revlog '%s' not found") % file_)
238 238 r = revlog.revlog(scmutil.opener(os.getcwd(), audit=False),
239 239 file_[:-2] + ".i")
240 240 return r
241 241
242 242 def copy(ui, repo, pats, opts, rename=False):
243 243 # called with the repo lock held
244 244 #
245 245 # hgsep => pathname that uses "/" to separate directories
246 246 # ossep => pathname that uses os.sep to separate directories
247 247 cwd = repo.getcwd()
248 248 targets = {}
249 249 after = opts.get("after")
250 250 dryrun = opts.get("dry_run")
251 251 wctx = repo[None]
252 252
253 253 def walkpat(pat):
254 254 srcs = []
255 255 badstates = after and '?' or '?r'
256 256 m = scmutil.match(repo[None], [pat], opts, globbed=True)
257 257 for abs in repo.walk(m):
258 258 state = repo.dirstate[abs]
259 259 rel = m.rel(abs)
260 260 exact = m.exact(abs)
261 261 if state in badstates:
262 262 if exact and state == '?':
263 263 ui.warn(_('%s: not copying - file is not managed\n') % rel)
264 264 if exact and state == 'r':
265 265 ui.warn(_('%s: not copying - file has been marked for'
266 266 ' remove\n') % rel)
267 267 continue
268 268 # abs: hgsep
269 269 # rel: ossep
270 270 srcs.append((abs, rel, exact))
271 271 return srcs
272 272
273 273 # abssrc: hgsep
274 274 # relsrc: ossep
275 275 # otarget: ossep
276 276 def copyfile(abssrc, relsrc, otarget, exact):
277 277 abstarget = pathutil.canonpath(repo.root, cwd, otarget)
278 278 if '/' in abstarget:
279 279 # We cannot normalize abstarget itself, this would prevent
280 280 # case only renames, like a => A.
281 281 abspath, absname = abstarget.rsplit('/', 1)
282 282 abstarget = repo.dirstate.normalize(abspath) + '/' + absname
283 283 reltarget = repo.pathto(abstarget, cwd)
284 284 target = repo.wjoin(abstarget)
285 285 src = repo.wjoin(abssrc)
286 286 state = repo.dirstate[abstarget]
287 287
288 288 scmutil.checkportable(ui, abstarget)
289 289
290 290 # check for collisions
291 291 prevsrc = targets.get(abstarget)
292 292 if prevsrc is not None:
293 293 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
294 294 (reltarget, repo.pathto(abssrc, cwd),
295 295 repo.pathto(prevsrc, cwd)))
296 296 return
297 297
298 298 # check for overwrites
299 299 exists = os.path.lexists(target)
300 300 samefile = False
301 301 if exists and abssrc != abstarget:
302 302 if (repo.dirstate.normalize(abssrc) ==
303 303 repo.dirstate.normalize(abstarget)):
304 304 if not rename:
305 305 ui.warn(_("%s: can't copy - same file\n") % reltarget)
306 306 return
307 307 exists = False
308 308 samefile = True
309 309
310 310 if not after and exists or after and state in 'mn':
311 311 if not opts['force']:
312 312 ui.warn(_('%s: not overwriting - file exists\n') %
313 313 reltarget)
314 314 return
315 315
316 316 if after:
317 317 if not exists:
318 318 if rename:
319 319 ui.warn(_('%s: not recording move - %s does not exist\n') %
320 320 (relsrc, reltarget))
321 321 else:
322 322 ui.warn(_('%s: not recording copy - %s does not exist\n') %
323 323 (relsrc, reltarget))
324 324 return
325 325 elif not dryrun:
326 326 try:
327 327 if exists:
328 328 os.unlink(target)
329 329 targetdir = os.path.dirname(target) or '.'
330 330 if not os.path.isdir(targetdir):
331 331 os.makedirs(targetdir)
332 332 if samefile:
333 333 tmp = target + "~hgrename"
334 334 os.rename(src, tmp)
335 335 os.rename(tmp, target)
336 336 else:
337 337 util.copyfile(src, target)
338 338 srcexists = True
339 339 except IOError, inst:
340 340 if inst.errno == errno.ENOENT:
341 341 ui.warn(_('%s: deleted in working copy\n') % relsrc)
342 342 srcexists = False
343 343 else:
344 344 ui.warn(_('%s: cannot copy - %s\n') %
345 345 (relsrc, inst.strerror))
346 346 return True # report a failure
347 347
348 348 if ui.verbose or not exact:
349 349 if rename:
350 350 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
351 351 else:
352 352 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
353 353
354 354 targets[abstarget] = abssrc
355 355
356 356 # fix up dirstate
357 357 scmutil.dirstatecopy(ui, repo, wctx, abssrc, abstarget,
358 358 dryrun=dryrun, cwd=cwd)
359 359 if rename and not dryrun:
360 360 if not after and srcexists and not samefile:
361 361 util.unlinkpath(repo.wjoin(abssrc))
362 362 wctx.forget([abssrc])
363 363
364 364 # pat: ossep
365 365 # dest ossep
366 366 # srcs: list of (hgsep, hgsep, ossep, bool)
367 367 # return: function that takes hgsep and returns ossep
368 368 def targetpathfn(pat, dest, srcs):
369 369 if os.path.isdir(pat):
370 370 abspfx = pathutil.canonpath(repo.root, cwd, pat)
371 371 abspfx = util.localpath(abspfx)
372 372 if destdirexists:
373 373 striplen = len(os.path.split(abspfx)[0])
374 374 else:
375 375 striplen = len(abspfx)
376 376 if striplen:
377 377 striplen += len(os.sep)
378 378 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
379 379 elif destdirexists:
380 380 res = lambda p: os.path.join(dest,
381 381 os.path.basename(util.localpath(p)))
382 382 else:
383 383 res = lambda p: dest
384 384 return res
385 385
386 386 # pat: ossep
387 387 # dest ossep
388 388 # srcs: list of (hgsep, hgsep, ossep, bool)
389 389 # return: function that takes hgsep and returns ossep
390 390 def targetpathafterfn(pat, dest, srcs):
391 391 if matchmod.patkind(pat):
392 392 # a mercurial pattern
393 393 res = lambda p: os.path.join(dest,
394 394 os.path.basename(util.localpath(p)))
395 395 else:
396 396 abspfx = pathutil.canonpath(repo.root, cwd, pat)
397 397 if len(abspfx) < len(srcs[0][0]):
398 398 # A directory. Either the target path contains the last
399 399 # component of the source path or it does not.
400 400 def evalpath(striplen):
401 401 score = 0
402 402 for s in srcs:
403 403 t = os.path.join(dest, util.localpath(s[0])[striplen:])
404 404 if os.path.lexists(t):
405 405 score += 1
406 406 return score
407 407
408 408 abspfx = util.localpath(abspfx)
409 409 striplen = len(abspfx)
410 410 if striplen:
411 411 striplen += len(os.sep)
412 412 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
413 413 score = evalpath(striplen)
414 414 striplen1 = len(os.path.split(abspfx)[0])
415 415 if striplen1:
416 416 striplen1 += len(os.sep)
417 417 if evalpath(striplen1) > score:
418 418 striplen = striplen1
419 419 res = lambda p: os.path.join(dest,
420 420 util.localpath(p)[striplen:])
421 421 else:
422 422 # a file
423 423 if destdirexists:
424 424 res = lambda p: os.path.join(dest,
425 425 os.path.basename(util.localpath(p)))
426 426 else:
427 427 res = lambda p: dest
428 428 return res
429 429
430 430
431 431 pats = scmutil.expandpats(pats)
432 432 if not pats:
433 433 raise util.Abort(_('no source or destination specified'))
434 434 if len(pats) == 1:
435 435 raise util.Abort(_('no destination specified'))
436 436 dest = pats.pop()
437 437 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
438 438 if not destdirexists:
439 439 if len(pats) > 1 or matchmod.patkind(pats[0]):
440 440 raise util.Abort(_('with multiple sources, destination must be an '
441 441 'existing directory'))
442 442 if util.endswithsep(dest):
443 443 raise util.Abort(_('destination %s is not a directory') % dest)
444 444
445 445 tfn = targetpathfn
446 446 if after:
447 447 tfn = targetpathafterfn
448 448 copylist = []
449 449 for pat in pats:
450 450 srcs = walkpat(pat)
451 451 if not srcs:
452 452 continue
453 453 copylist.append((tfn(pat, dest, srcs), srcs))
454 454 if not copylist:
455 455 raise util.Abort(_('no files to copy'))
456 456
457 457 errors = 0
458 458 for targetpath, srcs in copylist:
459 459 for abssrc, relsrc, exact in srcs:
460 460 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
461 461 errors += 1
462 462
463 463 if errors:
464 464 ui.warn(_('(consider using --after)\n'))
465 465
466 466 return errors != 0
467 467
468 468 def service(opts, parentfn=None, initfn=None, runfn=None, logfile=None,
469 469 runargs=None, appendpid=False):
470 470 '''Run a command as a service.'''
471 471
472 472 def writepid(pid):
473 473 if opts['pid_file']:
474 474 mode = appendpid and 'a' or 'w'
475 475 fp = open(opts['pid_file'], mode)
476 476 fp.write(str(pid) + '\n')
477 477 fp.close()
478 478
479 479 if opts['daemon'] and not opts['daemon_pipefds']:
480 480 # Signal child process startup with file removal
481 481 lockfd, lockpath = tempfile.mkstemp(prefix='hg-service-')
482 482 os.close(lockfd)
483 483 try:
484 484 if not runargs:
485 485 runargs = util.hgcmd() + sys.argv[1:]
486 486 runargs.append('--daemon-pipefds=%s' % lockpath)
487 487 # Don't pass --cwd to the child process, because we've already
488 488 # changed directory.
489 489 for i in xrange(1, len(runargs)):
490 490 if runargs[i].startswith('--cwd='):
491 491 del runargs[i]
492 492 break
493 493 elif runargs[i].startswith('--cwd'):
494 494 del runargs[i:i + 2]
495 495 break
496 496 def condfn():
497 497 return not os.path.exists(lockpath)
498 498 pid = util.rundetached(runargs, condfn)
499 499 if pid < 0:
500 500 raise util.Abort(_('child process failed to start'))
501 501 writepid(pid)
502 502 finally:
503 503 try:
504 504 os.unlink(lockpath)
505 505 except OSError, e:
506 506 if e.errno != errno.ENOENT:
507 507 raise
508 508 if parentfn:
509 509 return parentfn(pid)
510 510 else:
511 511 return
512 512
513 513 if initfn:
514 514 initfn()
515 515
516 516 if not opts['daemon']:
517 517 writepid(os.getpid())
518 518
519 519 if opts['daemon_pipefds']:
520 520 lockpath = opts['daemon_pipefds']
521 521 try:
522 522 os.setsid()
523 523 except AttributeError:
524 524 pass
525 525 os.unlink(lockpath)
526 526 util.hidewindow()
527 527 sys.stdout.flush()
528 528 sys.stderr.flush()
529 529
530 530 nullfd = os.open(os.devnull, os.O_RDWR)
531 531 logfilefd = nullfd
532 532 if logfile:
533 533 logfilefd = os.open(logfile, os.O_RDWR | os.O_CREAT | os.O_APPEND)
534 534 os.dup2(nullfd, 0)
535 535 os.dup2(logfilefd, 1)
536 536 os.dup2(logfilefd, 2)
537 537 if nullfd not in (0, 1, 2):
538 538 os.close(nullfd)
539 539 if logfile and logfilefd not in (0, 1, 2):
540 540 os.close(logfilefd)
541 541
542 542 if runfn:
543 543 return runfn()
544 544
545 545 def tryimportone(ui, repo, hunk, parents, opts, msgs, updatefunc):
546 546 """Utility function used by commands.import to import a single patch
547 547
548 548 This function is explicitly defined here to help the evolve extension to
549 549 wrap this part of the import logic.
550 550
551 551 The API is currently a bit ugly because it a simple code translation from
552 552 the import command. Feel free to make it better.
553 553
554 554 :hunk: a patch (as a binary string)
555 555 :parents: nodes that will be parent of the created commit
556 556 :opts: the full dict of option passed to the import command
557 557 :msgs: list to save commit message to.
558 558 (used in case we need to save it when failing)
559 559 :updatefunc: a function that update a repo to a given node
560 560 updatefunc(<repo>, <node>)
561 561 """
562 562 tmpname, message, user, date, branch, nodeid, p1, p2 = \
563 563 patch.extract(ui, hunk)
564 564
565 565 editor = commiteditor
566 566 if opts.get('edit'):
567 567 editor = commitforceeditor
568 568 update = not opts.get('bypass')
569 569 strip = opts["strip"]
570 570 sim = float(opts.get('similarity') or 0)
571 571 if not tmpname:
572 572 return (None, None)
573 573 msg = _('applied to working directory')
574 574
575 575 try:
576 576 cmdline_message = logmessage(ui, opts)
577 577 if cmdline_message:
578 578 # pickup the cmdline msg
579 579 message = cmdline_message
580 580 elif message:
581 581 # pickup the patch msg
582 582 message = message.strip()
583 583 else:
584 584 # launch the editor
585 585 message = None
586 586 ui.debug('message:\n%s\n' % message)
587 587
588 588 if len(parents) == 1:
589 589 parents.append(repo[nullid])
590 590 if opts.get('exact'):
591 591 if not nodeid or not p1:
592 592 raise util.Abort(_('not a Mercurial patch'))
593 593 p1 = repo[p1]
594 594 p2 = repo[p2 or nullid]
595 595 elif p2:
596 596 try:
597 597 p1 = repo[p1]
598 598 p2 = repo[p2]
599 599 # Without any options, consider p2 only if the
600 600 # patch is being applied on top of the recorded
601 601 # first parent.
602 602 if p1 != parents[0]:
603 603 p1 = parents[0]
604 604 p2 = repo[nullid]
605 605 except error.RepoError:
606 606 p1, p2 = parents
607 607 else:
608 608 p1, p2 = parents
609 609
610 610 n = None
611 611 if update:
612 612 if p1 != parents[0]:
613 613 updatefunc(repo, p1.node())
614 614 if p2 != parents[1]:
615 615 repo.setparents(p1.node(), p2.node())
616 616
617 617 if opts.get('exact') or opts.get('import_branch'):
618 618 repo.dirstate.setbranch(branch or 'default')
619 619
620 620 files = set()
621 621 patch.patch(ui, repo, tmpname, strip=strip, files=files,
622 622 eolmode=None, similarity=sim / 100.0)
623 623 files = list(files)
624 624 if opts.get('no_commit'):
625 625 if message:
626 626 msgs.append(message)
627 627 else:
628 628 if opts.get('exact') or p2:
629 629 # If you got here, you either use --force and know what
630 630 # you are doing or used --exact or a merge patch while
631 631 # being updated to its first parent.
632 632 m = None
633 633 else:
634 634 m = scmutil.matchfiles(repo, files or [])
635 635 n = repo.commit(message, opts.get('user') or user,
636 636 opts.get('date') or date, match=m,
637 637 editor=editor)
638 638 else:
639 639 if opts.get('exact') or opts.get('import_branch'):
640 640 branch = branch or 'default'
641 641 else:
642 642 branch = p1.branch()
643 643 store = patch.filestore()
644 644 try:
645 645 files = set()
646 646 try:
647 647 patch.patchrepo(ui, repo, p1, store, tmpname, strip,
648 648 files, eolmode=None)
649 649 except patch.PatchError, e:
650 650 raise util.Abort(str(e))
651 651 memctx = context.makememctx(repo, (p1.node(), p2.node()),
652 652 message,
653 653 opts.get('user') or user,
654 654 opts.get('date') or date,
655 655 branch, files, store,
656 656 editor=commiteditor)
657 657 repo.savecommitmessage(memctx.description())
658 658 n = memctx.commit()
659 659 finally:
660 660 store.close()
661 661 if opts.get('exact') and hex(n) != nodeid:
662 662 raise util.Abort(_('patch is damaged or loses information'))
663 663 if n:
664 664 # i18n: refers to a short changeset id
665 665 msg = _('created %s') % short(n)
666 666 return (msg, n)
667 667 finally:
668 668 os.unlink(tmpname)
669 669
670 670 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
671 671 opts=None):
672 672 '''export changesets as hg patches.'''
673 673
674 674 total = len(revs)
675 675 revwidth = max([len(str(rev)) for rev in revs])
676 676 filemode = {}
677 677
678 678 def single(rev, seqno, fp):
679 679 ctx = repo[rev]
680 680 node = ctx.node()
681 681 parents = [p.node() for p in ctx.parents() if p]
682 682 branch = ctx.branch()
683 683 if switch_parent:
684 684 parents.reverse()
685 685 prev = (parents and parents[0]) or nullid
686 686
687 687 shouldclose = False
688 688 if not fp and len(template) > 0:
689 689 desc_lines = ctx.description().rstrip().split('\n')
690 690 desc = desc_lines[0] #Commit always has a first line.
691 691 fp = makefileobj(repo, template, node, desc=desc, total=total,
692 692 seqno=seqno, revwidth=revwidth, mode='wb',
693 693 modemap=filemode)
694 694 if fp != template:
695 695 shouldclose = True
696 696 if fp and fp != sys.stdout and util.safehasattr(fp, 'name'):
697 697 repo.ui.note("%s\n" % fp.name)
698 698
699 699 if not fp:
700 700 write = repo.ui.write
701 701 else:
702 702 def write(s, **kw):
703 703 fp.write(s)
704 704
705 705
706 706 write("# HG changeset patch\n")
707 707 write("# User %s\n" % ctx.user())
708 708 write("# Date %d %d\n" % ctx.date())
709 709 write("# %s\n" % util.datestr(ctx.date()))
710 710 if branch and branch != 'default':
711 711 write("# Branch %s\n" % branch)
712 712 write("# Node ID %s\n" % hex(node))
713 713 write("# Parent %s\n" % hex(prev))
714 714 if len(parents) > 1:
715 715 write("# Parent %s\n" % hex(parents[1]))
716 716 write(ctx.description().rstrip())
717 717 write("\n\n")
718 718
719 719 for chunk, label in patch.diffui(repo, prev, node, opts=opts):
720 720 write(chunk, label=label)
721 721
722 722 if shouldclose:
723 723 fp.close()
724 724
725 725 for seqno, rev in enumerate(revs):
726 726 single(rev, seqno + 1, fp)
727 727
728 728 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
729 729 changes=None, stat=False, fp=None, prefix='',
730 730 listsubrepos=False):
731 731 '''show diff or diffstat.'''
732 732 if fp is None:
733 733 write = ui.write
734 734 else:
735 735 def write(s, **kw):
736 736 fp.write(s)
737 737
738 738 if stat:
739 739 diffopts = diffopts.copy(context=0)
740 740 width = 80
741 741 if not ui.plain():
742 742 width = ui.termwidth()
743 743 chunks = patch.diff(repo, node1, node2, match, changes, diffopts,
744 744 prefix=prefix)
745 745 for chunk, label in patch.diffstatui(util.iterlines(chunks),
746 746 width=width,
747 747 git=diffopts.git):
748 748 write(chunk, label=label)
749 749 else:
750 750 for chunk, label in patch.diffui(repo, node1, node2, match,
751 751 changes, diffopts, prefix=prefix):
752 752 write(chunk, label=label)
753 753
754 754 if listsubrepos:
755 755 ctx1 = repo[node1]
756 756 ctx2 = repo[node2]
757 757 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
758 758 tempnode2 = node2
759 759 try:
760 760 if node2 is not None:
761 761 tempnode2 = ctx2.substate[subpath][1]
762 762 except KeyError:
763 763 # A subrepo that existed in node1 was deleted between node1 and
764 764 # node2 (inclusive). Thus, ctx2's substate won't contain that
765 765 # subpath. The best we can do is to ignore it.
766 766 tempnode2 = None
767 767 submatch = matchmod.narrowmatcher(subpath, match)
768 768 sub.diff(ui, diffopts, tempnode2, submatch, changes=changes,
769 769 stat=stat, fp=fp, prefix=prefix)
770 770
771 771 class changeset_printer(object):
772 772 '''show changeset information when templating not requested.'''
773 773
774 774 def __init__(self, ui, repo, patch, diffopts, buffered):
775 775 self.ui = ui
776 776 self.repo = repo
777 777 self.buffered = buffered
778 778 self.patch = patch
779 779 self.diffopts = diffopts
780 780 self.header = {}
781 781 self.hunk = {}
782 782 self.lastheader = None
783 783 self.footer = None
784 784
785 785 def flush(self, rev):
786 786 if rev in self.header:
787 787 h = self.header[rev]
788 788 if h != self.lastheader:
789 789 self.lastheader = h
790 790 self.ui.write(h)
791 791 del self.header[rev]
792 792 if rev in self.hunk:
793 793 self.ui.write(self.hunk[rev])
794 794 del self.hunk[rev]
795 795 return 1
796 796 return 0
797 797
798 798 def close(self):
799 799 if self.footer:
800 800 self.ui.write(self.footer)
801 801
802 802 def show(self, ctx, copies=None, matchfn=None, **props):
803 803 if self.buffered:
804 804 self.ui.pushbuffer()
805 805 self._show(ctx, copies, matchfn, props)
806 806 self.hunk[ctx.rev()] = self.ui.popbuffer(labeled=True)
807 807 else:
808 808 self._show(ctx, copies, matchfn, props)
809 809
810 810 def _show(self, ctx, copies, matchfn, props):
811 811 '''show a single changeset or file revision'''
812 812 changenode = ctx.node()
813 813 rev = ctx.rev()
814 814
815 815 if self.ui.quiet:
816 816 self.ui.write("%d:%s\n" % (rev, short(changenode)),
817 817 label='log.node')
818 818 return
819 819
820 820 log = self.repo.changelog
821 821 date = util.datestr(ctx.date())
822 822
823 823 hexfunc = self.ui.debugflag and hex or short
824 824
825 825 parents = [(p, hexfunc(log.node(p)))
826 826 for p in self._meaningful_parentrevs(log, rev)]
827 827
828 828 # i18n: column positioning for "hg log"
829 829 self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode)),
830 830 label='log.changeset changeset.%s' % ctx.phasestr())
831 831
832 832 branch = ctx.branch()
833 833 # don't show the default branch name
834 834 if branch != 'default':
835 835 # i18n: column positioning for "hg log"
836 836 self.ui.write(_("branch: %s\n") % branch,
837 837 label='log.branch')
838 838 for bookmark in self.repo.nodebookmarks(changenode):
839 839 # i18n: column positioning for "hg log"
840 840 self.ui.write(_("bookmark: %s\n") % bookmark,
841 841 label='log.bookmark')
842 842 for tag in self.repo.nodetags(changenode):
843 843 # i18n: column positioning for "hg log"
844 844 self.ui.write(_("tag: %s\n") % tag,
845 845 label='log.tag')
846 846 if self.ui.debugflag and ctx.phase():
847 847 # i18n: column positioning for "hg log"
848 848 self.ui.write(_("phase: %s\n") % _(ctx.phasestr()),
849 849 label='log.phase')
850 850 for parent in parents:
851 851 # i18n: column positioning for "hg log"
852 852 self.ui.write(_("parent: %d:%s\n") % parent,
853 853 label='log.parent changeset.%s' % ctx.phasestr())
854 854
855 855 if self.ui.debugflag:
856 856 mnode = ctx.manifestnode()
857 857 # i18n: column positioning for "hg log"
858 858 self.ui.write(_("manifest: %d:%s\n") %
859 859 (self.repo.manifest.rev(mnode), hex(mnode)),
860 860 label='ui.debug log.manifest')
861 861 # i18n: column positioning for "hg log"
862 862 self.ui.write(_("user: %s\n") % ctx.user(),
863 863 label='log.user')
864 864 # i18n: column positioning for "hg log"
865 865 self.ui.write(_("date: %s\n") % date,
866 866 label='log.date')
867 867
868 868 if self.ui.debugflag:
869 869 files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
870 870 for key, value in zip([# i18n: column positioning for "hg log"
871 871 _("files:"),
872 872 # i18n: column positioning for "hg log"
873 873 _("files+:"),
874 874 # i18n: column positioning for "hg log"
875 875 _("files-:")], files):
876 876 if value:
877 877 self.ui.write("%-12s %s\n" % (key, " ".join(value)),
878 878 label='ui.debug log.files')
879 879 elif ctx.files() and self.ui.verbose:
880 880 # i18n: column positioning for "hg log"
881 881 self.ui.write(_("files: %s\n") % " ".join(ctx.files()),
882 882 label='ui.note log.files')
883 883 if copies and self.ui.verbose:
884 884 copies = ['%s (%s)' % c for c in copies]
885 885 # i18n: column positioning for "hg log"
886 886 self.ui.write(_("copies: %s\n") % ' '.join(copies),
887 887 label='ui.note log.copies')
888 888
889 889 extra = ctx.extra()
890 890 if extra and self.ui.debugflag:
891 891 for key, value in sorted(extra.items()):
892 892 # i18n: column positioning for "hg log"
893 893 self.ui.write(_("extra: %s=%s\n")
894 894 % (key, value.encode('string_escape')),
895 895 label='ui.debug log.extra')
896 896
897 897 description = ctx.description().strip()
898 898 if description:
899 899 if self.ui.verbose:
900 900 self.ui.write(_("description:\n"),
901 901 label='ui.note log.description')
902 902 self.ui.write(description,
903 903 label='ui.note log.description')
904 904 self.ui.write("\n\n")
905 905 else:
906 906 # i18n: column positioning for "hg log"
907 907 self.ui.write(_("summary: %s\n") %
908 908 description.splitlines()[0],
909 909 label='log.summary')
910 910 self.ui.write("\n")
911 911
912 912 self.showpatch(changenode, matchfn)
913 913
914 914 def showpatch(self, node, matchfn):
915 915 if not matchfn:
916 916 matchfn = self.patch
917 917 if matchfn:
918 918 stat = self.diffopts.get('stat')
919 919 diff = self.diffopts.get('patch')
920 920 diffopts = patch.diffopts(self.ui, self.diffopts)
921 921 prev = self.repo.changelog.parents(node)[0]
922 922 if stat:
923 923 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
924 924 match=matchfn, stat=True)
925 925 if diff:
926 926 if stat:
927 927 self.ui.write("\n")
928 928 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
929 929 match=matchfn, stat=False)
930 930 self.ui.write("\n")
931 931
932 932 def _meaningful_parentrevs(self, log, rev):
933 933 """Return list of meaningful (or all if debug) parentrevs for rev.
934 934
935 935 For merges (two non-nullrev revisions) both parents are meaningful.
936 936 Otherwise the first parent revision is considered meaningful if it
937 937 is not the preceding revision.
938 938 """
939 939 parents = log.parentrevs(rev)
940 940 if not self.ui.debugflag and parents[1] == nullrev:
941 941 if parents[0] >= rev - 1:
942 942 parents = []
943 943 else:
944 944 parents = [parents[0]]
945 945 return parents
946 946
947 947
948 948 class changeset_templater(changeset_printer):
949 949 '''format changeset information.'''
950 950
951 def __init__(self, ui, repo, patch, diffopts, mapfile, buffered):
951 def __init__(self, ui, repo, patch, diffopts, tmpl, mapfile, buffered):
952 952 changeset_printer.__init__(self, ui, repo, patch, diffopts, buffered)
953 953 formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
954 954 defaulttempl = {
955 955 'parent': '{rev}:{node|formatnode} ',
956 956 'manifest': '{rev}:{node|formatnode}',
957 957 'file_copy': '{name} ({source})',
958 958 'extra': '{key}={value|stringescape}'
959 959 }
960 960 # filecopy is preserved for compatibility reasons
961 961 defaulttempl['filecopy'] = defaulttempl['file_copy']
962 962 self.t = templater.templater(mapfile, {'formatnode': formatnode},
963 963 cache=defaulttempl)
964 self.cache = {}
964 if tmpl:
965 self.t.cache['changeset'] = tmpl
965 966
966 def use_template(self, t):
967 '''set template string to use'''
968 self.t.cache['changeset'] = t
967 self.cache = {}
969 968
970 969 def _meaningful_parentrevs(self, ctx):
971 970 """Return list of meaningful (or all if debug) parentrevs for rev.
972 971 """
973 972 parents = ctx.parents()
974 973 if len(parents) > 1:
975 974 return parents
976 975 if self.ui.debugflag:
977 976 return [parents[0], self.repo['null']]
978 977 if parents[0].rev() >= ctx.rev() - 1:
979 978 return []
980 979 return parents
981 980
982 981 def _show(self, ctx, copies, matchfn, props):
983 982 '''show a single changeset or file revision'''
984 983
985 984 showlist = templatekw.showlist
986 985
987 986 # showparents() behaviour depends on ui trace level which
988 987 # causes unexpected behaviours at templating level and makes
989 988 # it harder to extract it in a standalone function. Its
990 989 # behaviour cannot be changed so leave it here for now.
991 990 def showparents(**args):
992 991 ctx = args['ctx']
993 992 parents = [[('rev', p.rev()), ('node', p.hex())]
994 993 for p in self._meaningful_parentrevs(ctx)]
995 994 return showlist('parent', parents, **args)
996 995
997 996 props = props.copy()
998 997 props.update(templatekw.keywords)
999 998 props['parents'] = showparents
1000 999 props['templ'] = self.t
1001 1000 props['ctx'] = ctx
1002 1001 props['repo'] = self.repo
1003 1002 props['revcache'] = {'copies': copies}
1004 1003 props['cache'] = self.cache
1005 1004
1006 1005 # find correct templates for current mode
1007 1006
1008 1007 tmplmodes = [
1009 1008 (True, None),
1010 1009 (self.ui.verbose, 'verbose'),
1011 1010 (self.ui.quiet, 'quiet'),
1012 1011 (self.ui.debugflag, 'debug'),
1013 1012 ]
1014 1013
1015 1014 types = {'header': '', 'footer':'', 'changeset': 'changeset'}
1016 1015 for mode, postfix in tmplmodes:
1017 1016 for type in types:
1018 1017 cur = postfix and ('%s_%s' % (type, postfix)) or type
1019 1018 if mode and cur in self.t:
1020 1019 types[type] = cur
1021 1020
1022 1021 try:
1023 1022
1024 1023 # write header
1025 1024 if types['header']:
1026 1025 h = templater.stringify(self.t(types['header'], **props))
1027 1026 if self.buffered:
1028 1027 self.header[ctx.rev()] = h
1029 1028 else:
1030 1029 if self.lastheader != h:
1031 1030 self.lastheader = h
1032 1031 self.ui.write(h)
1033 1032
1034 1033 # write changeset metadata, then patch if requested
1035 1034 key = types['changeset']
1036 1035 self.ui.write(templater.stringify(self.t(key, **props)))
1037 1036 self.showpatch(ctx.node(), matchfn)
1038 1037
1039 1038 if types['footer']:
1040 1039 if not self.footer:
1041 1040 self.footer = templater.stringify(self.t(types['footer'],
1042 1041 **props))
1043 1042
1044 1043 except KeyError, inst:
1045 1044 msg = _("%s: no key named '%s'")
1046 1045 raise util.Abort(msg % (self.t.mapfile, inst.args[0]))
1047 1046 except SyntaxError, inst:
1048 1047 raise util.Abort('%s: %s' % (self.t.mapfile, inst.args[0]))
1049 1048
1050 1049 def gettemplate(ui, tmpl, style):
1051 1050 """
1052 1051 Find the template matching the given template spec or style.
1053 1052 """
1054 1053
1055 1054 # ui settings
1056 1055 if not tmpl and not style:
1057 1056 tmpl = ui.config('ui', 'logtemplate')
1058 1057 if tmpl:
1059 1058 try:
1060 1059 tmpl = templater.parsestring(tmpl)
1061 1060 except SyntaxError:
1062 1061 tmpl = templater.parsestring(tmpl, quoted=False)
1063 1062 else:
1064 1063 style = util.expandpath(ui.config('ui', 'style', ''))
1065 1064
1066 1065 if style:
1067 1066 mapfile = style
1068 1067 if not os.path.split(mapfile)[0]:
1069 1068 mapname = (templater.templatepath('map-cmdline.' + mapfile)
1070 1069 or templater.templatepath(mapfile))
1071 1070 if mapname:
1072 1071 mapfile = mapname
1073 1072 return None, mapfile
1074 1073
1075 1074 return tmpl, None
1076 1075
1077 1076 def show_changeset(ui, repo, opts, buffered=False):
1078 1077 """show one changeset using template or regular display.
1079 1078
1080 1079 Display format will be the first non-empty hit of:
1081 1080 1. option 'template'
1082 1081 2. option 'style'
1083 1082 3. [ui] setting 'logtemplate'
1084 1083 4. [ui] setting 'style'
1085 1084 If all of these values are either the unset or the empty string,
1086 1085 regular display via changeset_printer() is done.
1087 1086 """
1088 1087 # options
1089 1088 patch = None
1090 1089 if opts.get('patch') or opts.get('stat'):
1091 1090 patch = scmutil.matchall(repo)
1092 1091
1093 1092 tmpl, mapfile = gettemplate(ui, opts.get('template'), opts.get('style'))
1094 1093
1095 1094 if not tmpl and not mapfile:
1096 1095 return changeset_printer(ui, repo, patch, opts, buffered)
1097 1096
1098 1097 try:
1099 t = changeset_templater(ui, repo, patch, opts, mapfile, buffered)
1098 t = changeset_templater(ui, repo, patch, opts, tmpl, mapfile, buffered)
1100 1099 except SyntaxError, inst:
1101 1100 raise util.Abort(inst.args[0])
1102 if tmpl:
1103 t.use_template(tmpl)
1104 1101 return t
1105 1102
1106 1103 def showmarker(ui, marker):
1107 1104 """utility function to display obsolescence marker in a readable way
1108 1105
1109 1106 To be used by debug function."""
1110 1107 ui.write(hex(marker.precnode()))
1111 1108 for repl in marker.succnodes():
1112 1109 ui.write(' ')
1113 1110 ui.write(hex(repl))
1114 1111 ui.write(' %X ' % marker._data[2])
1115 1112 ui.write('{%s}' % (', '.join('%r: %r' % t for t in
1116 1113 sorted(marker.metadata().items()))))
1117 1114 ui.write('\n')
1118 1115
1119 1116 def finddate(ui, repo, date):
1120 1117 """Find the tipmost changeset that matches the given date spec"""
1121 1118
1122 1119 df = util.matchdate(date)
1123 1120 m = scmutil.matchall(repo)
1124 1121 results = {}
1125 1122
1126 1123 def prep(ctx, fns):
1127 1124 d = ctx.date()
1128 1125 if df(d[0]):
1129 1126 results[ctx.rev()] = d
1130 1127
1131 1128 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
1132 1129 rev = ctx.rev()
1133 1130 if rev in results:
1134 1131 ui.status(_("found revision %s from %s\n") %
1135 1132 (rev, util.datestr(results[rev])))
1136 1133 return str(rev)
1137 1134
1138 1135 raise util.Abort(_("revision matching date not found"))
1139 1136
1140 1137 def increasingwindows(windowsize=8, sizelimit=512):
1141 1138 while True:
1142 1139 yield windowsize
1143 1140 if windowsize < sizelimit:
1144 1141 windowsize *= 2
1145 1142
1146 1143 class FileWalkError(Exception):
1147 1144 pass
1148 1145
1149 1146 def walkfilerevs(repo, match, follow, revs, fncache):
1150 1147 '''Walks the file history for the matched files.
1151 1148
1152 1149 Returns the changeset revs that are involved in the file history.
1153 1150
1154 1151 Throws FileWalkError if the file history can't be walked using
1155 1152 filelogs alone.
1156 1153 '''
1157 1154 wanted = set()
1158 1155 copies = []
1159 1156 minrev, maxrev = min(revs), max(revs)
1160 1157 def filerevgen(filelog, last):
1161 1158 """
1162 1159 Only files, no patterns. Check the history of each file.
1163 1160
1164 1161 Examines filelog entries within minrev, maxrev linkrev range
1165 1162 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
1166 1163 tuples in backwards order
1167 1164 """
1168 1165 cl_count = len(repo)
1169 1166 revs = []
1170 1167 for j in xrange(0, last + 1):
1171 1168 linkrev = filelog.linkrev(j)
1172 1169 if linkrev < minrev:
1173 1170 continue
1174 1171 # only yield rev for which we have the changelog, it can
1175 1172 # happen while doing "hg log" during a pull or commit
1176 1173 if linkrev >= cl_count:
1177 1174 break
1178 1175
1179 1176 parentlinkrevs = []
1180 1177 for p in filelog.parentrevs(j):
1181 1178 if p != nullrev:
1182 1179 parentlinkrevs.append(filelog.linkrev(p))
1183 1180 n = filelog.node(j)
1184 1181 revs.append((linkrev, parentlinkrevs,
1185 1182 follow and filelog.renamed(n)))
1186 1183
1187 1184 return reversed(revs)
1188 1185 def iterfiles():
1189 1186 pctx = repo['.']
1190 1187 for filename in match.files():
1191 1188 if follow:
1192 1189 if filename not in pctx:
1193 1190 raise util.Abort(_('cannot follow file not in parent '
1194 1191 'revision: "%s"') % filename)
1195 1192 yield filename, pctx[filename].filenode()
1196 1193 else:
1197 1194 yield filename, None
1198 1195 for filename_node in copies:
1199 1196 yield filename_node
1200 1197
1201 1198 for file_, node in iterfiles():
1202 1199 filelog = repo.file(file_)
1203 1200 if not len(filelog):
1204 1201 if node is None:
1205 1202 # A zero count may be a directory or deleted file, so
1206 1203 # try to find matching entries on the slow path.
1207 1204 if follow:
1208 1205 raise util.Abort(
1209 1206 _('cannot follow nonexistent file: "%s"') % file_)
1210 1207 raise FileWalkError("Cannot walk via filelog")
1211 1208 else:
1212 1209 continue
1213 1210
1214 1211 if node is None:
1215 1212 last = len(filelog) - 1
1216 1213 else:
1217 1214 last = filelog.rev(node)
1218 1215
1219 1216
1220 1217 # keep track of all ancestors of the file
1221 1218 ancestors = set([filelog.linkrev(last)])
1222 1219
1223 1220 # iterate from latest to oldest revision
1224 1221 for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
1225 1222 if not follow:
1226 1223 if rev > maxrev:
1227 1224 continue
1228 1225 else:
1229 1226 # Note that last might not be the first interesting
1230 1227 # rev to us:
1231 1228 # if the file has been changed after maxrev, we'll
1232 1229 # have linkrev(last) > maxrev, and we still need
1233 1230 # to explore the file graph
1234 1231 if rev not in ancestors:
1235 1232 continue
1236 1233 # XXX insert 1327 fix here
1237 1234 if flparentlinkrevs:
1238 1235 ancestors.update(flparentlinkrevs)
1239 1236
1240 1237 fncache.setdefault(rev, []).append(file_)
1241 1238 wanted.add(rev)
1242 1239 if copied:
1243 1240 copies.append(copied)
1244 1241
1245 1242 return wanted
1246 1243
1247 1244 def walkchangerevs(repo, match, opts, prepare):
1248 1245 '''Iterate over files and the revs in which they changed.
1249 1246
1250 1247 Callers most commonly need to iterate backwards over the history
1251 1248 in which they are interested. Doing so has awful (quadratic-looking)
1252 1249 performance, so we use iterators in a "windowed" way.
1253 1250
1254 1251 We walk a window of revisions in the desired order. Within the
1255 1252 window, we first walk forwards to gather data, then in the desired
1256 1253 order (usually backwards) to display it.
1257 1254
1258 1255 This function returns an iterator yielding contexts. Before
1259 1256 yielding each context, the iterator will first call the prepare
1260 1257 function on each context in the window in forward order.'''
1261 1258
1262 1259 follow = opts.get('follow') or opts.get('follow_first')
1263 1260
1264 1261 if opts.get('rev'):
1265 1262 revs = scmutil.revrange(repo, opts.get('rev'))
1266 1263 elif follow:
1267 1264 revs = repo.revs('reverse(:.)')
1268 1265 else:
1269 1266 revs = revset.baseset(repo)
1270 1267 revs.reverse()
1271 1268 if not revs:
1272 1269 return []
1273 1270 wanted = set()
1274 1271 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1275 1272 fncache = {}
1276 1273 change = repo.changectx
1277 1274
1278 1275 # First step is to fill wanted, the set of revisions that we want to yield.
1279 1276 # When it does not induce extra cost, we also fill fncache for revisions in
1280 1277 # wanted: a cache of filenames that were changed (ctx.files()) and that
1281 1278 # match the file filtering conditions.
1282 1279
1283 1280 if not slowpath and not match.files():
1284 1281 # No files, no patterns. Display all revs.
1285 1282 wanted = revs
1286 1283
1287 1284 if not slowpath and match.files():
1288 1285 # We only have to read through the filelog to find wanted revisions
1289 1286
1290 1287 try:
1291 1288 wanted = walkfilerevs(repo, match, follow, revs, fncache)
1292 1289 except FileWalkError:
1293 1290 slowpath = True
1294 1291
1295 1292 # We decided to fall back to the slowpath because at least one
1296 1293 # of the paths was not a file. Check to see if at least one of them
1297 1294 # existed in history, otherwise simply return
1298 1295 for path in match.files():
1299 1296 if path == '.' or path in repo.store:
1300 1297 break
1301 1298 else:
1302 1299 return []
1303 1300
1304 1301 if slowpath:
1305 1302 # We have to read the changelog to match filenames against
1306 1303 # changed files
1307 1304
1308 1305 if follow:
1309 1306 raise util.Abort(_('can only follow copies/renames for explicit '
1310 1307 'filenames'))
1311 1308
1312 1309 # The slow path checks files modified in every changeset.
1313 1310 # This is really slow on large repos, so compute the set lazily.
1314 1311 class lazywantedset(object):
1315 1312 def __init__(self):
1316 1313 self.set = set()
1317 1314 self.revs = set(revs)
1318 1315
1319 1316 # No need to worry about locality here because it will be accessed
1320 1317 # in the same order as the increasing window below.
1321 1318 def __contains__(self, value):
1322 1319 if value in self.set:
1323 1320 return True
1324 1321 elif not value in self.revs:
1325 1322 return False
1326 1323 else:
1327 1324 self.revs.discard(value)
1328 1325 ctx = change(value)
1329 1326 matches = filter(match, ctx.files())
1330 1327 if matches:
1331 1328 fncache[value] = matches
1332 1329 self.set.add(value)
1333 1330 return True
1334 1331 return False
1335 1332
1336 1333 def discard(self, value):
1337 1334 self.revs.discard(value)
1338 1335 self.set.discard(value)
1339 1336
1340 1337 wanted = lazywantedset()
1341 1338
1342 1339 class followfilter(object):
1343 1340 def __init__(self, onlyfirst=False):
1344 1341 self.startrev = nullrev
1345 1342 self.roots = set()
1346 1343 self.onlyfirst = onlyfirst
1347 1344
1348 1345 def match(self, rev):
1349 1346 def realparents(rev):
1350 1347 if self.onlyfirst:
1351 1348 return repo.changelog.parentrevs(rev)[0:1]
1352 1349 else:
1353 1350 return filter(lambda x: x != nullrev,
1354 1351 repo.changelog.parentrevs(rev))
1355 1352
1356 1353 if self.startrev == nullrev:
1357 1354 self.startrev = rev
1358 1355 return True
1359 1356
1360 1357 if rev > self.startrev:
1361 1358 # forward: all descendants
1362 1359 if not self.roots:
1363 1360 self.roots.add(self.startrev)
1364 1361 for parent in realparents(rev):
1365 1362 if parent in self.roots:
1366 1363 self.roots.add(rev)
1367 1364 return True
1368 1365 else:
1369 1366 # backwards: all parents
1370 1367 if not self.roots:
1371 1368 self.roots.update(realparents(self.startrev))
1372 1369 if rev in self.roots:
1373 1370 self.roots.remove(rev)
1374 1371 self.roots.update(realparents(rev))
1375 1372 return True
1376 1373
1377 1374 return False
1378 1375
1379 1376 # it might be worthwhile to do this in the iterator if the rev range
1380 1377 # is descending and the prune args are all within that range
1381 1378 for rev in opts.get('prune', ()):
1382 1379 rev = repo[rev].rev()
1383 1380 ff = followfilter()
1384 1381 stop = min(revs[0], revs[-1])
1385 1382 for x in xrange(rev, stop - 1, -1):
1386 1383 if ff.match(x):
1387 1384 wanted = wanted - [x]
1388 1385
1389 1386 # Now that wanted is correctly initialized, we can iterate over the
1390 1387 # revision range, yielding only revisions in wanted.
1391 1388 def iterate():
1392 1389 if follow and not match.files():
1393 1390 ff = followfilter(onlyfirst=opts.get('follow_first'))
1394 1391 def want(rev):
1395 1392 return ff.match(rev) and rev in wanted
1396 1393 else:
1397 1394 def want(rev):
1398 1395 return rev in wanted
1399 1396
1400 1397 it = iter(revs)
1401 1398 stopiteration = False
1402 1399 for windowsize in increasingwindows():
1403 1400 nrevs = []
1404 1401 for i in xrange(windowsize):
1405 1402 try:
1406 1403 rev = it.next()
1407 1404 if want(rev):
1408 1405 nrevs.append(rev)
1409 1406 except (StopIteration):
1410 1407 stopiteration = True
1411 1408 break
1412 1409 for rev in sorted(nrevs):
1413 1410 fns = fncache.get(rev)
1414 1411 ctx = change(rev)
1415 1412 if not fns:
1416 1413 def fns_generator():
1417 1414 for f in ctx.files():
1418 1415 if match(f):
1419 1416 yield f
1420 1417 fns = fns_generator()
1421 1418 prepare(ctx, fns)
1422 1419 for rev in nrevs:
1423 1420 yield change(rev)
1424 1421
1425 1422 if stopiteration:
1426 1423 break
1427 1424
1428 1425 return iterate()
1429 1426
1430 1427 def _makegraphfilematcher(repo, pats, followfirst):
1431 1428 # When displaying a revision with --patch --follow FILE, we have
1432 1429 # to know which file of the revision must be diffed. With
1433 1430 # --follow, we want the names of the ancestors of FILE in the
1434 1431 # revision, stored in "fcache". "fcache" is populated by
1435 1432 # reproducing the graph traversal already done by --follow revset
1436 1433 # and relating linkrevs to file names (which is not "correct" but
1437 1434 # good enough).
1438 1435 fcache = {}
1439 1436 fcacheready = [False]
1440 1437 pctx = repo['.']
1441 1438 wctx = repo[None]
1442 1439
1443 1440 def populate():
1444 1441 for fn in pats:
1445 1442 for i in ((pctx[fn],), pctx[fn].ancestors(followfirst=followfirst)):
1446 1443 for c in i:
1447 1444 fcache.setdefault(c.linkrev(), set()).add(c.path())
1448 1445
1449 1446 def filematcher(rev):
1450 1447 if not fcacheready[0]:
1451 1448 # Lazy initialization
1452 1449 fcacheready[0] = True
1453 1450 populate()
1454 1451 return scmutil.match(wctx, fcache.get(rev, []), default='path')
1455 1452
1456 1453 return filematcher
1457 1454
1458 1455 def _makegraphlogrevset(repo, pats, opts, revs):
1459 1456 """Return (expr, filematcher) where expr is a revset string built
1460 1457 from log options and file patterns or None. If --stat or --patch
1461 1458 are not passed filematcher is None. Otherwise it is a callable
1462 1459 taking a revision number and returning a match objects filtering
1463 1460 the files to be detailed when displaying the revision.
1464 1461 """
1465 1462 opt2revset = {
1466 1463 'no_merges': ('not merge()', None),
1467 1464 'only_merges': ('merge()', None),
1468 1465 '_ancestors': ('ancestors(%(val)s)', None),
1469 1466 '_fancestors': ('_firstancestors(%(val)s)', None),
1470 1467 '_descendants': ('descendants(%(val)s)', None),
1471 1468 '_fdescendants': ('_firstdescendants(%(val)s)', None),
1472 1469 '_matchfiles': ('_matchfiles(%(val)s)', None),
1473 1470 'date': ('date(%(val)r)', None),
1474 1471 'branch': ('branch(%(val)r)', ' or '),
1475 1472 '_patslog': ('filelog(%(val)r)', ' or '),
1476 1473 '_patsfollow': ('follow(%(val)r)', ' or '),
1477 1474 '_patsfollowfirst': ('_followfirst(%(val)r)', ' or '),
1478 1475 'keyword': ('keyword(%(val)r)', ' or '),
1479 1476 'prune': ('not (%(val)r or ancestors(%(val)r))', ' and '),
1480 1477 'user': ('user(%(val)r)', ' or '),
1481 1478 }
1482 1479
1483 1480 opts = dict(opts)
1484 1481 # follow or not follow?
1485 1482 follow = opts.get('follow') or opts.get('follow_first')
1486 1483 followfirst = opts.get('follow_first') and 1 or 0
1487 1484 # --follow with FILE behaviour depends on revs...
1488 1485 startrev = revs[0]
1489 1486 followdescendants = (len(revs) > 1 and revs[0] < revs[1]) and 1 or 0
1490 1487
1491 1488 # branch and only_branch are really aliases and must be handled at
1492 1489 # the same time
1493 1490 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
1494 1491 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
1495 1492 # pats/include/exclude are passed to match.match() directly in
1496 1493 # _matchfiles() revset but walkchangerevs() builds its matcher with
1497 1494 # scmutil.match(). The difference is input pats are globbed on
1498 1495 # platforms without shell expansion (windows).
1499 1496 pctx = repo[None]
1500 1497 match, pats = scmutil.matchandpats(pctx, pats, opts)
1501 1498 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1502 1499 if not slowpath:
1503 1500 for f in match.files():
1504 1501 if follow and f not in pctx:
1505 1502 raise util.Abort(_('cannot follow file not in parent '
1506 1503 'revision: "%s"') % f)
1507 1504 filelog = repo.file(f)
1508 1505 if not filelog:
1509 1506 # A zero count may be a directory or deleted file, so
1510 1507 # try to find matching entries on the slow path.
1511 1508 if follow:
1512 1509 raise util.Abort(
1513 1510 _('cannot follow nonexistent file: "%s"') % f)
1514 1511 slowpath = True
1515 1512
1516 1513 # We decided to fall back to the slowpath because at least one
1517 1514 # of the paths was not a file. Check to see if at least one of them
1518 1515 # existed in history - in that case, we'll continue down the
1519 1516 # slowpath; otherwise, we can turn off the slowpath
1520 1517 if slowpath:
1521 1518 for path in match.files():
1522 1519 if path == '.' or path in repo.store:
1523 1520 break
1524 1521 else:
1525 1522 slowpath = False
1526 1523
1527 1524 if slowpath:
1528 1525 # See walkchangerevs() slow path.
1529 1526 #
1530 1527 if follow:
1531 1528 raise util.Abort(_('can only follow copies/renames for explicit '
1532 1529 'filenames'))
1533 1530 # pats/include/exclude cannot be represented as separate
1534 1531 # revset expressions as their filtering logic applies at file
1535 1532 # level. For instance "-I a -X a" matches a revision touching
1536 1533 # "a" and "b" while "file(a) and not file(b)" does
1537 1534 # not. Besides, filesets are evaluated against the working
1538 1535 # directory.
1539 1536 matchargs = ['r:', 'd:relpath']
1540 1537 for p in pats:
1541 1538 matchargs.append('p:' + p)
1542 1539 for p in opts.get('include', []):
1543 1540 matchargs.append('i:' + p)
1544 1541 for p in opts.get('exclude', []):
1545 1542 matchargs.append('x:' + p)
1546 1543 matchargs = ','.join(('%r' % p) for p in matchargs)
1547 1544 opts['_matchfiles'] = matchargs
1548 1545 else:
1549 1546 if follow:
1550 1547 fpats = ('_patsfollow', '_patsfollowfirst')
1551 1548 fnopats = (('_ancestors', '_fancestors'),
1552 1549 ('_descendants', '_fdescendants'))
1553 1550 if pats:
1554 1551 # follow() revset interprets its file argument as a
1555 1552 # manifest entry, so use match.files(), not pats.
1556 1553 opts[fpats[followfirst]] = list(match.files())
1557 1554 else:
1558 1555 opts[fnopats[followdescendants][followfirst]] = str(startrev)
1559 1556 else:
1560 1557 opts['_patslog'] = list(pats)
1561 1558
1562 1559 filematcher = None
1563 1560 if opts.get('patch') or opts.get('stat'):
1564 1561 if follow:
1565 1562 filematcher = _makegraphfilematcher(repo, pats, followfirst)
1566 1563 else:
1567 1564 filematcher = lambda rev: match
1568 1565
1569 1566 expr = []
1570 1567 for op, val in opts.iteritems():
1571 1568 if not val:
1572 1569 continue
1573 1570 if op not in opt2revset:
1574 1571 continue
1575 1572 revop, andor = opt2revset[op]
1576 1573 if '%(val)' not in revop:
1577 1574 expr.append(revop)
1578 1575 else:
1579 1576 if not isinstance(val, list):
1580 1577 e = revop % {'val': val}
1581 1578 else:
1582 1579 e = '(' + andor.join((revop % {'val': v}) for v in val) + ')'
1583 1580 expr.append(e)
1584 1581
1585 1582 if expr:
1586 1583 expr = '(' + ' and '.join(expr) + ')'
1587 1584 else:
1588 1585 expr = None
1589 1586 return expr, filematcher
1590 1587
1591 1588 def getgraphlogrevs(repo, pats, opts):
1592 1589 """Return (revs, expr, filematcher) where revs is an iterable of
1593 1590 revision numbers, expr is a revset string built from log options
1594 1591 and file patterns or None, and used to filter 'revs'. If --stat or
1595 1592 --patch are not passed filematcher is None. Otherwise it is a
1596 1593 callable taking a revision number and returning a match objects
1597 1594 filtering the files to be detailed when displaying the revision.
1598 1595 """
1599 1596 if not len(repo):
1600 1597 return [], None, None
1601 1598 limit = loglimit(opts)
1602 1599 # Default --rev value depends on --follow but --follow behaviour
1603 1600 # depends on revisions resolved from --rev...
1604 1601 follow = opts.get('follow') or opts.get('follow_first')
1605 1602 possiblyunsorted = False # whether revs might need sorting
1606 1603 if opts.get('rev'):
1607 1604 revs = scmutil.revrange(repo, opts['rev'])
1608 1605 # Don't sort here because _makegraphlogrevset might depend on the
1609 1606 # order of revs
1610 1607 possiblyunsorted = True
1611 1608 else:
1612 1609 if follow and len(repo) > 0:
1613 1610 revs = repo.revs('reverse(:.)')
1614 1611 else:
1615 1612 revs = revset.baseset(repo.changelog)
1616 1613 revs.reverse()
1617 1614 if not revs:
1618 1615 return [], None, None
1619 1616 revs = revset.baseset(revs)
1620 1617 expr, filematcher = _makegraphlogrevset(repo, pats, opts, revs)
1621 1618 if possiblyunsorted:
1622 1619 revs.sort(reverse=True)
1623 1620 if expr:
1624 1621 # Revset matchers often operate faster on revisions in changelog
1625 1622 # order, because most filters deal with the changelog.
1626 1623 revs.reverse()
1627 1624 matcher = revset.match(repo.ui, expr)
1628 1625 # Revset matches can reorder revisions. "A or B" typically returns
1629 1626 # returns the revision matching A then the revision matching B. Sort
1630 1627 # again to fix that.
1631 1628 revs = matcher(repo, revs)
1632 1629 revs.sort(reverse=True)
1633 1630 if limit is not None:
1634 1631 revs = revs[:limit]
1635 1632
1636 1633 return revs, expr, filematcher
1637 1634
1638 1635 def displaygraph(ui, dag, displayer, showparents, edgefn, getrenamed=None,
1639 1636 filematcher=None):
1640 1637 seen, state = [], graphmod.asciistate()
1641 1638 for rev, type, ctx, parents in dag:
1642 1639 char = 'o'
1643 1640 if ctx.node() in showparents:
1644 1641 char = '@'
1645 1642 elif ctx.obsolete():
1646 1643 char = 'x'
1647 1644 copies = None
1648 1645 if getrenamed and ctx.rev():
1649 1646 copies = []
1650 1647 for fn in ctx.files():
1651 1648 rename = getrenamed(fn, ctx.rev())
1652 1649 if rename:
1653 1650 copies.append((fn, rename[0]))
1654 1651 revmatchfn = None
1655 1652 if filematcher is not None:
1656 1653 revmatchfn = filematcher(ctx.rev())
1657 1654 displayer.show(ctx, copies=copies, matchfn=revmatchfn)
1658 1655 lines = displayer.hunk.pop(rev).split('\n')
1659 1656 if not lines[-1]:
1660 1657 del lines[-1]
1661 1658 displayer.flush(rev)
1662 1659 edges = edgefn(type, char, lines, seen, rev, parents)
1663 1660 for type, char, lines, coldata in edges:
1664 1661 graphmod.ascii(ui, state, type, char, lines, coldata)
1665 1662 displayer.close()
1666 1663
1667 1664 def graphlog(ui, repo, *pats, **opts):
1668 1665 # Parameters are identical to log command ones
1669 1666 revs, expr, filematcher = getgraphlogrevs(repo, pats, opts)
1670 1667 revdag = graphmod.dagwalker(repo, revs)
1671 1668
1672 1669 getrenamed = None
1673 1670 if opts.get('copies'):
1674 1671 endrev = None
1675 1672 if opts.get('rev'):
1676 1673 endrev = max(scmutil.revrange(repo, opts.get('rev'))) + 1
1677 1674 getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
1678 1675 displayer = show_changeset(ui, repo, opts, buffered=True)
1679 1676 showparents = [ctx.node() for ctx in repo[None].parents()]
1680 1677 displaygraph(ui, revdag, displayer, showparents,
1681 1678 graphmod.asciiedges, getrenamed, filematcher)
1682 1679
1683 1680 def checkunsupportedgraphflags(pats, opts):
1684 1681 for op in ["newest_first"]:
1685 1682 if op in opts and opts[op]:
1686 1683 raise util.Abort(_("-G/--graph option is incompatible with --%s")
1687 1684 % op.replace("_", "-"))
1688 1685
1689 1686 def graphrevs(repo, nodes, opts):
1690 1687 limit = loglimit(opts)
1691 1688 nodes.reverse()
1692 1689 if limit is not None:
1693 1690 nodes = nodes[:limit]
1694 1691 return graphmod.nodes(repo, nodes)
1695 1692
1696 1693 def add(ui, repo, match, dryrun, listsubrepos, prefix, explicitonly):
1697 1694 join = lambda f: os.path.join(prefix, f)
1698 1695 bad = []
1699 1696 oldbad = match.bad
1700 1697 match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
1701 1698 names = []
1702 1699 wctx = repo[None]
1703 1700 cca = None
1704 1701 abort, warn = scmutil.checkportabilityalert(ui)
1705 1702 if abort or warn:
1706 1703 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
1707 1704 for f in repo.walk(match):
1708 1705 exact = match.exact(f)
1709 1706 if exact or not explicitonly and f not in repo.dirstate:
1710 1707 if cca:
1711 1708 cca(f)
1712 1709 names.append(f)
1713 1710 if ui.verbose or not exact:
1714 1711 ui.status(_('adding %s\n') % match.rel(join(f)))
1715 1712
1716 1713 for subpath in sorted(wctx.substate):
1717 1714 sub = wctx.sub(subpath)
1718 1715 try:
1719 1716 submatch = matchmod.narrowmatcher(subpath, match)
1720 1717 if listsubrepos:
1721 1718 bad.extend(sub.add(ui, submatch, dryrun, listsubrepos, prefix,
1722 1719 False))
1723 1720 else:
1724 1721 bad.extend(sub.add(ui, submatch, dryrun, listsubrepos, prefix,
1725 1722 True))
1726 1723 except error.LookupError:
1727 1724 ui.status(_("skipping missing subrepository: %s\n")
1728 1725 % join(subpath))
1729 1726
1730 1727 if not dryrun:
1731 1728 rejected = wctx.add(names, prefix)
1732 1729 bad.extend(f for f in rejected if f in match.files())
1733 1730 return bad
1734 1731
1735 1732 def forget(ui, repo, match, prefix, explicitonly):
1736 1733 join = lambda f: os.path.join(prefix, f)
1737 1734 bad = []
1738 1735 oldbad = match.bad
1739 1736 match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
1740 1737 wctx = repo[None]
1741 1738 forgot = []
1742 1739 s = repo.status(match=match, clean=True)
1743 1740 forget = sorted(s[0] + s[1] + s[3] + s[6])
1744 1741 if explicitonly:
1745 1742 forget = [f for f in forget if match.exact(f)]
1746 1743
1747 1744 for subpath in sorted(wctx.substate):
1748 1745 sub = wctx.sub(subpath)
1749 1746 try:
1750 1747 submatch = matchmod.narrowmatcher(subpath, match)
1751 1748 subbad, subforgot = sub.forget(ui, submatch, prefix)
1752 1749 bad.extend([subpath + '/' + f for f in subbad])
1753 1750 forgot.extend([subpath + '/' + f for f in subforgot])
1754 1751 except error.LookupError:
1755 1752 ui.status(_("skipping missing subrepository: %s\n")
1756 1753 % join(subpath))
1757 1754
1758 1755 if not explicitonly:
1759 1756 for f in match.files():
1760 1757 if f not in repo.dirstate and not os.path.isdir(match.rel(join(f))):
1761 1758 if f not in forgot:
1762 1759 if os.path.exists(match.rel(join(f))):
1763 1760 ui.warn(_('not removing %s: '
1764 1761 'file is already untracked\n')
1765 1762 % match.rel(join(f)))
1766 1763 bad.append(f)
1767 1764
1768 1765 for f in forget:
1769 1766 if ui.verbose or not match.exact(f):
1770 1767 ui.status(_('removing %s\n') % match.rel(join(f)))
1771 1768
1772 1769 rejected = wctx.forget(forget, prefix)
1773 1770 bad.extend(f for f in rejected if f in match.files())
1774 1771 forgot.extend(forget)
1775 1772 return bad, forgot
1776 1773
1777 1774 def duplicatecopies(repo, rev, fromrev):
1778 1775 '''reproduce copies from fromrev to rev in the dirstate'''
1779 1776 for dst, src in copies.pathcopies(repo[fromrev], repo[rev]).iteritems():
1780 1777 # copies.pathcopies returns backward renames, so dst might not
1781 1778 # actually be in the dirstate
1782 1779 if repo.dirstate[dst] in "nma":
1783 1780 repo.dirstate.copy(src, dst)
1784 1781
1785 1782 def commit(ui, repo, commitfunc, pats, opts):
1786 1783 '''commit the specified files or all outstanding changes'''
1787 1784 date = opts.get('date')
1788 1785 if date:
1789 1786 opts['date'] = util.parsedate(date)
1790 1787 message = logmessage(ui, opts)
1791 1788
1792 1789 # extract addremove carefully -- this function can be called from a command
1793 1790 # that doesn't support addremove
1794 1791 if opts.get('addremove'):
1795 1792 scmutil.addremove(repo, pats, opts)
1796 1793
1797 1794 return commitfunc(ui, repo, message,
1798 1795 scmutil.match(repo[None], pats, opts), opts)
1799 1796
1800 1797 def amend(ui, repo, commitfunc, old, extra, pats, opts):
1801 1798 ui.note(_('amending changeset %s\n') % old)
1802 1799 base = old.p1()
1803 1800
1804 1801 wlock = lock = newid = None
1805 1802 try:
1806 1803 wlock = repo.wlock()
1807 1804 lock = repo.lock()
1808 1805 tr = repo.transaction('amend')
1809 1806 try:
1810 1807 # See if we got a message from -m or -l, if not, open the editor
1811 1808 # with the message of the changeset to amend
1812 1809 message = logmessage(ui, opts)
1813 1810 # ensure logfile does not conflict with later enforcement of the
1814 1811 # message. potential logfile content has been processed by
1815 1812 # `logmessage` anyway.
1816 1813 opts.pop('logfile')
1817 1814 # First, do a regular commit to record all changes in the working
1818 1815 # directory (if there are any)
1819 1816 ui.callhooks = False
1820 1817 currentbookmark = repo._bookmarkcurrent
1821 1818 try:
1822 1819 repo._bookmarkcurrent = None
1823 1820 opts['message'] = 'temporary amend commit for %s' % old
1824 1821 node = commit(ui, repo, commitfunc, pats, opts)
1825 1822 finally:
1826 1823 repo._bookmarkcurrent = currentbookmark
1827 1824 ui.callhooks = True
1828 1825 ctx = repo[node]
1829 1826
1830 1827 # Participating changesets:
1831 1828 #
1832 1829 # node/ctx o - new (intermediate) commit that contains changes
1833 1830 # | from working dir to go into amending commit
1834 1831 # | (or a workingctx if there were no changes)
1835 1832 # |
1836 1833 # old o - changeset to amend
1837 1834 # |
1838 1835 # base o - parent of amending changeset
1839 1836
1840 1837 # Update extra dict from amended commit (e.g. to preserve graft
1841 1838 # source)
1842 1839 extra.update(old.extra())
1843 1840
1844 1841 # Also update it from the intermediate commit or from the wctx
1845 1842 extra.update(ctx.extra())
1846 1843
1847 1844 if len(old.parents()) > 1:
1848 1845 # ctx.files() isn't reliable for merges, so fall back to the
1849 1846 # slower repo.status() method
1850 1847 files = set([fn for st in repo.status(base, old)[:3]
1851 1848 for fn in st])
1852 1849 else:
1853 1850 files = set(old.files())
1854 1851
1855 1852 # Second, we use either the commit we just did, or if there were no
1856 1853 # changes the parent of the working directory as the version of the
1857 1854 # files in the final amend commit
1858 1855 if node:
1859 1856 ui.note(_('copying changeset %s to %s\n') % (ctx, base))
1860 1857
1861 1858 user = ctx.user()
1862 1859 date = ctx.date()
1863 1860 # Recompute copies (avoid recording a -> b -> a)
1864 1861 copied = copies.pathcopies(base, ctx)
1865 1862
1866 1863 # Prune files which were reverted by the updates: if old
1867 1864 # introduced file X and our intermediate commit, node,
1868 1865 # renamed that file, then those two files are the same and
1869 1866 # we can discard X from our list of files. Likewise if X
1870 1867 # was deleted, it's no longer relevant
1871 1868 files.update(ctx.files())
1872 1869
1873 1870 def samefile(f):
1874 1871 if f in ctx.manifest():
1875 1872 a = ctx.filectx(f)
1876 1873 if f in base.manifest():
1877 1874 b = base.filectx(f)
1878 1875 return (not a.cmp(b)
1879 1876 and a.flags() == b.flags())
1880 1877 else:
1881 1878 return False
1882 1879 else:
1883 1880 return f not in base.manifest()
1884 1881 files = [f for f in files if not samefile(f)]
1885 1882
1886 1883 def filectxfn(repo, ctx_, path):
1887 1884 try:
1888 1885 fctx = ctx[path]
1889 1886 flags = fctx.flags()
1890 1887 mctx = context.memfilectx(fctx.path(), fctx.data(),
1891 1888 islink='l' in flags,
1892 1889 isexec='x' in flags,
1893 1890 copied=copied.get(path))
1894 1891 return mctx
1895 1892 except KeyError:
1896 1893 raise IOError
1897 1894 else:
1898 1895 ui.note(_('copying changeset %s to %s\n') % (old, base))
1899 1896
1900 1897 # Use version of files as in the old cset
1901 1898 def filectxfn(repo, ctx_, path):
1902 1899 try:
1903 1900 return old.filectx(path)
1904 1901 except KeyError:
1905 1902 raise IOError
1906 1903
1907 1904 user = opts.get('user') or old.user()
1908 1905 date = opts.get('date') or old.date()
1909 1906 editmsg = False
1910 1907 if not message:
1911 1908 editmsg = True
1912 1909 message = old.description()
1913 1910
1914 1911 pureextra = extra.copy()
1915 1912 extra['amend_source'] = old.hex()
1916 1913
1917 1914 new = context.memctx(repo,
1918 1915 parents=[base.node(), old.p2().node()],
1919 1916 text=message,
1920 1917 files=files,
1921 1918 filectxfn=filectxfn,
1922 1919 user=user,
1923 1920 date=date,
1924 1921 extra=extra)
1925 1922 if editmsg:
1926 1923 new._text = commitforceeditor(repo, new, [])
1927 1924
1928 1925 newdesc = changelog.stripdesc(new.description())
1929 1926 if ((not node)
1930 1927 and newdesc == old.description()
1931 1928 and user == old.user()
1932 1929 and date == old.date()
1933 1930 and pureextra == old.extra()):
1934 1931 # nothing changed. continuing here would create a new node
1935 1932 # anyway because of the amend_source noise.
1936 1933 #
1937 1934 # This not what we expect from amend.
1938 1935 return old.node()
1939 1936
1940 1937 ph = repo.ui.config('phases', 'new-commit', phases.draft)
1941 1938 try:
1942 1939 repo.ui.setconfig('phases', 'new-commit', old.phase())
1943 1940 newid = repo.commitctx(new)
1944 1941 finally:
1945 1942 repo.ui.setconfig('phases', 'new-commit', ph)
1946 1943 if newid != old.node():
1947 1944 # Reroute the working copy parent to the new changeset
1948 1945 repo.setparents(newid, nullid)
1949 1946
1950 1947 # Move bookmarks from old parent to amend commit
1951 1948 bms = repo.nodebookmarks(old.node())
1952 1949 if bms:
1953 1950 marks = repo._bookmarks
1954 1951 for bm in bms:
1955 1952 marks[bm] = newid
1956 1953 marks.write()
1957 1954 #commit the whole amend process
1958 1955 if obsolete._enabled and newid != old.node():
1959 1956 # mark the new changeset as successor of the rewritten one
1960 1957 new = repo[newid]
1961 1958 obs = [(old, (new,))]
1962 1959 if node:
1963 1960 obs.append((ctx, ()))
1964 1961
1965 1962 obsolete.createmarkers(repo, obs)
1966 1963 tr.close()
1967 1964 finally:
1968 1965 tr.release()
1969 1966 if (not obsolete._enabled) and newid != old.node():
1970 1967 # Strip the intermediate commit (if there was one) and the amended
1971 1968 # commit
1972 1969 if node:
1973 1970 ui.note(_('stripping intermediate changeset %s\n') % ctx)
1974 1971 ui.note(_('stripping amended changeset %s\n') % old)
1975 1972 repair.strip(ui, repo, old.node(), topic='amend-backup')
1976 1973 finally:
1977 1974 if newid is None:
1978 1975 repo.dirstate.invalidate()
1979 1976 lockmod.release(lock, wlock)
1980 1977 return newid
1981 1978
1982 1979 def commiteditor(repo, ctx, subs):
1983 1980 if ctx.description():
1984 1981 return ctx.description()
1985 1982 return commitforceeditor(repo, ctx, subs)
1986 1983
1987 1984 def commitforceeditor(repo, ctx, subs):
1988 1985 edittext = []
1989 1986 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
1990 1987 if ctx.description():
1991 1988 edittext.append(ctx.description())
1992 1989 edittext.append("")
1993 1990 edittext.append("") # Empty line between message and comments.
1994 1991 edittext.append(_("HG: Enter commit message."
1995 1992 " Lines beginning with 'HG:' are removed."))
1996 1993 edittext.append(_("HG: Leave message empty to abort commit."))
1997 1994 edittext.append("HG: --")
1998 1995 edittext.append(_("HG: user: %s") % ctx.user())
1999 1996 if ctx.p2():
2000 1997 edittext.append(_("HG: branch merge"))
2001 1998 if ctx.branch():
2002 1999 edittext.append(_("HG: branch '%s'") % ctx.branch())
2003 2000 if bookmarks.iscurrent(repo):
2004 2001 edittext.append(_("HG: bookmark '%s'") % repo._bookmarkcurrent)
2005 2002 edittext.extend([_("HG: subrepo %s") % s for s in subs])
2006 2003 edittext.extend([_("HG: added %s") % f for f in added])
2007 2004 edittext.extend([_("HG: changed %s") % f for f in modified])
2008 2005 edittext.extend([_("HG: removed %s") % f for f in removed])
2009 2006 if not added and not modified and not removed:
2010 2007 edittext.append(_("HG: no files changed"))
2011 2008 edittext.append("")
2012 2009 # run editor in the repository root
2013 2010 olddir = os.getcwd()
2014 2011 os.chdir(repo.root)
2015 2012 text = repo.ui.edit("\n".join(edittext), ctx.user(), ctx.extra())
2016 2013 text = re.sub("(?m)^HG:.*(\n|$)", "", text)
2017 2014 os.chdir(olddir)
2018 2015
2019 2016 if not text.strip():
2020 2017 raise util.Abort(_("empty commit message"))
2021 2018
2022 2019 return text
2023 2020
2024 2021 def commitstatus(repo, node, branch, bheads=None, opts={}):
2025 2022 ctx = repo[node]
2026 2023 parents = ctx.parents()
2027 2024
2028 2025 if (not opts.get('amend') and bheads and node not in bheads and not
2029 2026 [x for x in parents if x.node() in bheads and x.branch() == branch]):
2030 2027 repo.ui.status(_('created new head\n'))
2031 2028 # The message is not printed for initial roots. For the other
2032 2029 # changesets, it is printed in the following situations:
2033 2030 #
2034 2031 # Par column: for the 2 parents with ...
2035 2032 # N: null or no parent
2036 2033 # B: parent is on another named branch
2037 2034 # C: parent is a regular non head changeset
2038 2035 # H: parent was a branch head of the current branch
2039 2036 # Msg column: whether we print "created new head" message
2040 2037 # In the following, it is assumed that there already exists some
2041 2038 # initial branch heads of the current branch, otherwise nothing is
2042 2039 # printed anyway.
2043 2040 #
2044 2041 # Par Msg Comment
2045 2042 # N N y additional topo root
2046 2043 #
2047 2044 # B N y additional branch root
2048 2045 # C N y additional topo head
2049 2046 # H N n usual case
2050 2047 #
2051 2048 # B B y weird additional branch root
2052 2049 # C B y branch merge
2053 2050 # H B n merge with named branch
2054 2051 #
2055 2052 # C C y additional head from merge
2056 2053 # C H n merge with a head
2057 2054 #
2058 2055 # H H n head merge: head count decreases
2059 2056
2060 2057 if not opts.get('close_branch'):
2061 2058 for r in parents:
2062 2059 if r.closesbranch() and r.branch() == branch:
2063 2060 repo.ui.status(_('reopening closed branch head %d\n') % r)
2064 2061
2065 2062 if repo.ui.debugflag:
2066 2063 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx.hex()))
2067 2064 elif repo.ui.verbose:
2068 2065 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx))
2069 2066
2070 2067 def revert(ui, repo, ctx, parents, *pats, **opts):
2071 2068 parent, p2 = parents
2072 2069 node = ctx.node()
2073 2070
2074 2071 mf = ctx.manifest()
2075 2072 if node == parent:
2076 2073 pmf = mf
2077 2074 else:
2078 2075 pmf = None
2079 2076
2080 2077 # need all matching names in dirstate and manifest of target rev,
2081 2078 # so have to walk both. do not print errors if files exist in one
2082 2079 # but not other.
2083 2080
2084 2081 names = {}
2085 2082
2086 2083 wlock = repo.wlock()
2087 2084 try:
2088 2085 # walk dirstate.
2089 2086
2090 2087 m = scmutil.match(repo[None], pats, opts)
2091 2088 m.bad = lambda x, y: False
2092 2089 for abs in repo.walk(m):
2093 2090 names[abs] = m.rel(abs), m.exact(abs)
2094 2091
2095 2092 # walk target manifest.
2096 2093
2097 2094 def badfn(path, msg):
2098 2095 if path in names:
2099 2096 return
2100 2097 if path in ctx.substate:
2101 2098 return
2102 2099 path_ = path + '/'
2103 2100 for f in names:
2104 2101 if f.startswith(path_):
2105 2102 return
2106 2103 ui.warn("%s: %s\n" % (m.rel(path), msg))
2107 2104
2108 2105 m = scmutil.match(ctx, pats, opts)
2109 2106 m.bad = badfn
2110 2107 for abs in ctx.walk(m):
2111 2108 if abs not in names:
2112 2109 names[abs] = m.rel(abs), m.exact(abs)
2113 2110
2114 2111 # get the list of subrepos that must be reverted
2115 2112 targetsubs = sorted(s for s in ctx.substate if m(s))
2116 2113 m = scmutil.matchfiles(repo, names)
2117 2114 changes = repo.status(match=m)[:4]
2118 2115 modified, added, removed, deleted = map(set, changes)
2119 2116
2120 2117 # if f is a rename, also revert the source
2121 2118 cwd = repo.getcwd()
2122 2119 for f in added:
2123 2120 src = repo.dirstate.copied(f)
2124 2121 if src and src not in names and repo.dirstate[src] == 'r':
2125 2122 removed.add(src)
2126 2123 names[src] = (repo.pathto(src, cwd), True)
2127 2124
2128 2125 def removeforget(abs):
2129 2126 if repo.dirstate[abs] == 'a':
2130 2127 return _('forgetting %s\n')
2131 2128 return _('removing %s\n')
2132 2129
2133 2130 revert = ([], _('reverting %s\n'))
2134 2131 add = ([], _('adding %s\n'))
2135 2132 remove = ([], removeforget)
2136 2133 undelete = ([], _('undeleting %s\n'))
2137 2134
2138 2135 disptable = (
2139 2136 # dispatch table:
2140 2137 # file state
2141 2138 # action if in target manifest
2142 2139 # action if not in target manifest
2143 2140 # make backup if in target manifest
2144 2141 # make backup if not in target manifest
2145 2142 (modified, revert, remove, True, True),
2146 2143 (added, revert, remove, True, False),
2147 2144 (removed, undelete, None, True, False),
2148 2145 (deleted, revert, remove, False, False),
2149 2146 )
2150 2147
2151 2148 for abs, (rel, exact) in sorted(names.items()):
2152 2149 mfentry = mf.get(abs)
2153 2150 target = repo.wjoin(abs)
2154 2151 def handle(xlist, dobackup):
2155 2152 xlist[0].append(abs)
2156 2153 if (dobackup and not opts.get('no_backup') and
2157 2154 os.path.lexists(target) and
2158 2155 abs in ctx and repo[None][abs].cmp(ctx[abs])):
2159 2156 bakname = "%s.orig" % rel
2160 2157 ui.note(_('saving current version of %s as %s\n') %
2161 2158 (rel, bakname))
2162 2159 if not opts.get('dry_run'):
2163 2160 util.rename(target, bakname)
2164 2161 if ui.verbose or not exact:
2165 2162 msg = xlist[1]
2166 2163 if not isinstance(msg, basestring):
2167 2164 msg = msg(abs)
2168 2165 ui.status(msg % rel)
2169 2166 for table, hitlist, misslist, backuphit, backupmiss in disptable:
2170 2167 if abs not in table:
2171 2168 continue
2172 2169 # file has changed in dirstate
2173 2170 if mfentry:
2174 2171 handle(hitlist, backuphit)
2175 2172 elif misslist is not None:
2176 2173 handle(misslist, backupmiss)
2177 2174 break
2178 2175 else:
2179 2176 if abs not in repo.dirstate:
2180 2177 if mfentry:
2181 2178 handle(add, True)
2182 2179 elif exact:
2183 2180 ui.warn(_('file not managed: %s\n') % rel)
2184 2181 continue
2185 2182 # file has not changed in dirstate
2186 2183 if node == parent:
2187 2184 if exact:
2188 2185 ui.warn(_('no changes needed to %s\n') % rel)
2189 2186 continue
2190 2187 if pmf is None:
2191 2188 # only need parent manifest in this unlikely case,
2192 2189 # so do not read by default
2193 2190 pmf = repo[parent].manifest()
2194 2191 if abs in pmf and mfentry:
2195 2192 # if version of file is same in parent and target
2196 2193 # manifests, do nothing
2197 2194 if (pmf[abs] != mfentry or
2198 2195 pmf.flags(abs) != mf.flags(abs)):
2199 2196 handle(revert, False)
2200 2197 else:
2201 2198 handle(remove, False)
2202 2199 if not opts.get('dry_run'):
2203 2200 _performrevert(repo, parents, ctx, revert, add, remove, undelete)
2204 2201
2205 2202 if targetsubs:
2206 2203 # Revert the subrepos on the revert list
2207 2204 for sub in targetsubs:
2208 2205 ctx.sub(sub).revert(ui, ctx.substate[sub], *pats, **opts)
2209 2206 finally:
2210 2207 wlock.release()
2211 2208
2212 2209 def _performrevert(repo, parents, ctx, revert, add, remove, undelete):
2213 2210 """function that actually perform all the action computed for revert
2214 2211
2215 2212 This is an independent function to let extension to plug in and react to
2216 2213 the imminent revert.
2217 2214
2218 2215 Make sure you have the working directory locked when caling this function.
2219 2216 """
2220 2217 parent, p2 = parents
2221 2218 node = ctx.node()
2222 2219 def checkout(f):
2223 2220 fc = ctx[f]
2224 2221 repo.wwrite(f, fc.data(), fc.flags())
2225 2222
2226 2223 audit_path = pathutil.pathauditor(repo.root)
2227 2224 for f in remove[0]:
2228 2225 if repo.dirstate[f] == 'a':
2229 2226 repo.dirstate.drop(f)
2230 2227 continue
2231 2228 audit_path(f)
2232 2229 try:
2233 2230 util.unlinkpath(repo.wjoin(f))
2234 2231 except OSError:
2235 2232 pass
2236 2233 repo.dirstate.remove(f)
2237 2234
2238 2235 normal = None
2239 2236 if node == parent:
2240 2237 # We're reverting to our parent. If possible, we'd like status
2241 2238 # to report the file as clean. We have to use normallookup for
2242 2239 # merges to avoid losing information about merged/dirty files.
2243 2240 if p2 != nullid:
2244 2241 normal = repo.dirstate.normallookup
2245 2242 else:
2246 2243 normal = repo.dirstate.normal
2247 2244 for f in revert[0]:
2248 2245 checkout(f)
2249 2246 if normal:
2250 2247 normal(f)
2251 2248
2252 2249 for f in add[0]:
2253 2250 checkout(f)
2254 2251 repo.dirstate.add(f)
2255 2252
2256 2253 normal = repo.dirstate.normallookup
2257 2254 if node == parent and p2 == nullid:
2258 2255 normal = repo.dirstate.normal
2259 2256 for f in undelete[0]:
2260 2257 checkout(f)
2261 2258 normal(f)
2262 2259
2263 2260 copied = copies.pathcopies(repo[parent], ctx)
2264 2261
2265 2262 for f in add[0] + undelete[0] + revert[0]:
2266 2263 if f in copied:
2267 2264 repo.dirstate.copy(copied[f], f)
2268 2265
2269 2266 def command(table):
2270 2267 '''returns a function object bound to table which can be used as
2271 2268 a decorator for populating table as a command table'''
2272 2269
2273 2270 def cmd(name, options=(), synopsis=None):
2274 2271 def decorator(func):
2275 2272 if synopsis:
2276 2273 table[name] = func, list(options), synopsis
2277 2274 else:
2278 2275 table[name] = func, list(options)
2279 2276 return func
2280 2277 return decorator
2281 2278
2282 2279 return cmd
2283 2280
2284 2281 # a list of (ui, repo) functions called by commands.summary
2285 2282 summaryhooks = util.hooks()
2286 2283
2287 2284 # A list of state files kept by multistep operations like graft.
2288 2285 # Since graft cannot be aborted, it is considered 'clearable' by update.
2289 2286 # note: bisect is intentionally excluded
2290 2287 # (state file, clearable, allowcommit, error, hint)
2291 2288 unfinishedstates = [
2292 2289 ('graftstate', True, False, _('graft in progress'),
2293 2290 _("use 'hg graft --continue' or 'hg update' to abort")),
2294 2291 ('updatestate', True, False, _('last update was interrupted'),
2295 2292 _("use 'hg update' to get a consistent checkout"))
2296 2293 ]
2297 2294
2298 2295 def checkunfinished(repo, commit=False):
2299 2296 '''Look for an unfinished multistep operation, like graft, and abort
2300 2297 if found. It's probably good to check this right before
2301 2298 bailifchanged().
2302 2299 '''
2303 2300 for f, clearable, allowcommit, msg, hint in unfinishedstates:
2304 2301 if commit and allowcommit:
2305 2302 continue
2306 2303 if repo.vfs.exists(f):
2307 2304 raise util.Abort(msg, hint=hint)
2308 2305
2309 2306 def clearunfinished(repo):
2310 2307 '''Check for unfinished operations (as above), and clear the ones
2311 2308 that are clearable.
2312 2309 '''
2313 2310 for f, clearable, allowcommit, msg, hint in unfinishedstates:
2314 2311 if not clearable and repo.vfs.exists(f):
2315 2312 raise util.Abort(msg, hint=hint)
2316 2313 for f, clearable, allowcommit, msg, hint in unfinishedstates:
2317 2314 if clearable and repo.vfs.exists(f):
2318 2315 util.unlink(repo.join(f))
General Comments 0
You need to be logged in to leave comments. Login now