##// END OF EJS Templates
registrar: host "dynamicdefault" constant by configitem object...
Yuya Nishihara -
r34918:ee924371 stable
parent child Browse files
Show More
@@ -1,1126 +1,1125 b''
1 1 # bugzilla.py - bugzilla integration for mercurial
2 2 #
3 3 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 4 # Copyright 2011-4 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 Four basic modes of access to Bugzilla are provided:
19 19
20 20 1. Access via the Bugzilla REST-API. Requires bugzilla 5.0 or later.
21 21
22 22 2. Access via the Bugzilla XMLRPC interface. Requires Bugzilla 3.4 or later.
23 23
24 24 3. Check data via the Bugzilla XMLRPC interface and submit bug change
25 25 via email to Bugzilla email interface. Requires Bugzilla 3.4 or later.
26 26
27 27 4. Writing directly to the Bugzilla database. Only Bugzilla installations
28 28 using MySQL are supported. Requires Python MySQLdb.
29 29
30 30 Writing directly to the database is susceptible to schema changes, and
31 31 relies on a Bugzilla contrib script to send out bug change
32 32 notification emails. This script runs as the user running Mercurial,
33 33 must be run on the host with the Bugzilla install, and requires
34 34 permission to read Bugzilla configuration details and the necessary
35 35 MySQL user and password to have full access rights to the Bugzilla
36 36 database. For these reasons this access mode is now considered
37 37 deprecated, and will not be updated for new Bugzilla versions going
38 38 forward. Only adding comments is supported in this access mode.
39 39
40 40 Access via XMLRPC needs a Bugzilla username and password to be specified
41 41 in the configuration. Comments are added under that username. Since the
42 42 configuration must be readable by all Mercurial users, it is recommended
43 43 that the rights of that user are restricted in Bugzilla to the minimum
44 44 necessary to add comments. Marking bugs fixed requires Bugzilla 4.0 and later.
45 45
46 46 Access via XMLRPC/email uses XMLRPC to query Bugzilla, but sends
47 47 email to the Bugzilla email interface to submit comments to bugs.
48 48 The From: address in the email is set to the email address of the Mercurial
49 49 user, so the comment appears to come from the Mercurial user. In the event
50 50 that the Mercurial user email is not recognized by Bugzilla as a Bugzilla
51 51 user, the email associated with the Bugzilla username used to log into
52 52 Bugzilla is used instead as the source of the comment. Marking bugs fixed
53 53 works on all supported Bugzilla versions.
54 54
55 55 Access via the REST-API needs either a Bugzilla username and password
56 56 or an apikey specified in the configuration. Comments are made under
57 57 the given username or the user associated with the apikey in Bugzilla.
58 58
59 59 Configuration items common to all access modes:
60 60
61 61 bugzilla.version
62 62 The access type to use. Values recognized are:
63 63
64 64 :``restapi``: Bugzilla REST-API, Bugzilla 5.0 and later.
65 65 :``xmlrpc``: Bugzilla XMLRPC interface.
66 66 :``xmlrpc+email``: Bugzilla XMLRPC and email interfaces.
67 67 :``3.0``: MySQL access, Bugzilla 3.0 and later.
68 68 :``2.18``: MySQL access, Bugzilla 2.18 and up to but not
69 69 including 3.0.
70 70 :``2.16``: MySQL access, Bugzilla 2.16 and up to but not
71 71 including 2.18.
72 72
73 73 bugzilla.regexp
74 74 Regular expression to match bug IDs for update in changeset commit message.
75 75 It must contain one "()" named group ``<ids>`` containing the bug
76 76 IDs separated by non-digit characters. It may also contain
77 77 a named group ``<hours>`` with a floating-point number giving the
78 78 hours worked on the bug. If no named groups are present, the first
79 79 "()" group is assumed to contain the bug IDs, and work time is not
80 80 updated. The default expression matches ``Bug 1234``, ``Bug no. 1234``,
81 81 ``Bug number 1234``, ``Bugs 1234,5678``, ``Bug 1234 and 5678`` and
82 82 variations thereof, followed by an hours number prefixed by ``h`` or
83 83 ``hours``, e.g. ``hours 1.5``. Matching is case insensitive.
84 84
85 85 bugzilla.fixregexp
86 86 Regular expression to match bug IDs for marking fixed in changeset
87 87 commit message. This must contain a "()" named group ``<ids>` containing
88 88 the bug IDs separated by non-digit characters. It may also contain
89 89 a named group ``<hours>`` with a floating-point number giving the
90 90 hours worked on the bug. If no named groups are present, the first
91 91 "()" group is assumed to contain the bug IDs, and work time is not
92 92 updated. The default expression matches ``Fixes 1234``, ``Fixes bug 1234``,
93 93 ``Fixes bugs 1234,5678``, ``Fixes 1234 and 5678`` and
94 94 variations thereof, followed by an hours number prefixed by ``h`` or
95 95 ``hours``, e.g. ``hours 1.5``. Matching is case insensitive.
96 96
97 97 bugzilla.fixstatus
98 98 The status to set a bug to when marking fixed. Default ``RESOLVED``.
99 99
100 100 bugzilla.fixresolution
101 101 The resolution to set a bug to when marking fixed. Default ``FIXED``.
102 102
103 103 bugzilla.style
104 104 The style file to use when formatting comments.
105 105
106 106 bugzilla.template
107 107 Template to use when formatting comments. Overrides style if
108 108 specified. In addition to the usual Mercurial keywords, the
109 109 extension specifies:
110 110
111 111 :``{bug}``: The Bugzilla bug ID.
112 112 :``{root}``: The full pathname of the Mercurial repository.
113 113 :``{webroot}``: Stripped pathname of the Mercurial repository.
114 114 :``{hgweb}``: Base URL for browsing Mercurial repositories.
115 115
116 116 Default ``changeset {node|short} in repo {root} refers to bug
117 117 {bug}.\\ndetails:\\n\\t{desc|tabindent}``
118 118
119 119 bugzilla.strip
120 120 The number of path separator characters to strip from the front of
121 121 the Mercurial repository path (``{root}`` in templates) to produce
122 122 ``{webroot}``. For example, a repository with ``{root}``
123 123 ``/var/local/my-project`` with a strip of 2 gives a value for
124 124 ``{webroot}`` of ``my-project``. Default 0.
125 125
126 126 web.baseurl
127 127 Base URL for browsing Mercurial repositories. Referenced from
128 128 templates as ``{hgweb}``.
129 129
130 130 Configuration items common to XMLRPC+email and MySQL access modes:
131 131
132 132 bugzilla.usermap
133 133 Path of file containing Mercurial committer email to Bugzilla user email
134 134 mappings. If specified, the file should contain one mapping per
135 135 line::
136 136
137 137 committer = Bugzilla user
138 138
139 139 See also the ``[usermap]`` section.
140 140
141 141 The ``[usermap]`` section is used to specify mappings of Mercurial
142 142 committer email to Bugzilla user email. See also ``bugzilla.usermap``.
143 143 Contains entries of the form ``committer = Bugzilla user``.
144 144
145 145 XMLRPC and REST-API access mode configuration:
146 146
147 147 bugzilla.bzurl
148 148 The base URL for the Bugzilla installation.
149 149 Default ``http://localhost/bugzilla``.
150 150
151 151 bugzilla.user
152 152 The username to use to log into Bugzilla via XMLRPC. Default
153 153 ``bugs``.
154 154
155 155 bugzilla.password
156 156 The password for Bugzilla login.
157 157
158 158 REST-API access mode uses the options listed above as well as:
159 159
160 160 bugzilla.apikey
161 161 An apikey generated on the Bugzilla instance for api access.
162 162 Using an apikey removes the need to store the user and password
163 163 options.
164 164
165 165 XMLRPC+email access mode uses the XMLRPC access mode configuration items,
166 166 and also:
167 167
168 168 bugzilla.bzemail
169 169 The Bugzilla email address.
170 170
171 171 In addition, the Mercurial email settings must be configured. See the
172 172 documentation in hgrc(5), sections ``[email]`` and ``[smtp]``.
173 173
174 174 MySQL access mode configuration:
175 175
176 176 bugzilla.host
177 177 Hostname of the MySQL server holding the Bugzilla database.
178 178 Default ``localhost``.
179 179
180 180 bugzilla.db
181 181 Name of the Bugzilla database in MySQL. Default ``bugs``.
182 182
183 183 bugzilla.user
184 184 Username to use to access MySQL server. Default ``bugs``.
185 185
186 186 bugzilla.password
187 187 Password to use to access MySQL server.
188 188
189 189 bugzilla.timeout
190 190 Database connection timeout (seconds). Default 5.
191 191
192 192 bugzilla.bzuser
193 193 Fallback Bugzilla user name to record comments with, if changeset
194 194 committer cannot be found as a Bugzilla user.
195 195
196 196 bugzilla.bzdir
197 197 Bugzilla install directory. Used by default notify. Default
198 198 ``/var/www/html/bugzilla``.
199 199
200 200 bugzilla.notify
201 201 The command to run to get Bugzilla to send bug change notification
202 202 emails. Substitutes from a map with 3 keys, ``bzdir``, ``id`` (bug
203 203 id) and ``user`` (committer bugzilla email). Default depends on
204 204 version; from 2.18 it is "cd %(bzdir)s && perl -T
205 205 contrib/sendbugmail.pl %(id)s %(user)s".
206 206
207 207 Activating the extension::
208 208
209 209 [extensions]
210 210 bugzilla =
211 211
212 212 [hooks]
213 213 # run bugzilla hook on every change pulled or pushed in here
214 214 incoming.bugzilla = python:hgext.bugzilla.hook
215 215
216 216 Example configurations:
217 217
218 218 XMLRPC example configuration. This uses the Bugzilla at
219 219 ``http://my-project.org/bugzilla``, logging in as user
220 220 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
221 221 collection of Mercurial repositories in ``/var/local/hg/repos/``,
222 222 with a web interface at ``http://my-project.org/hg``. ::
223 223
224 224 [bugzilla]
225 225 bzurl=http://my-project.org/bugzilla
226 226 user=bugmail@my-project.org
227 227 password=plugh
228 228 version=xmlrpc
229 229 template=Changeset {node|short} in {root|basename}.
230 230 {hgweb}/{webroot}/rev/{node|short}\\n
231 231 {desc}\\n
232 232 strip=5
233 233
234 234 [web]
235 235 baseurl=http://my-project.org/hg
236 236
237 237 XMLRPC+email example configuration. This uses the Bugzilla at
238 238 ``http://my-project.org/bugzilla``, logging in as user
239 239 ``bugmail@my-project.org`` with password ``plugh``. It is used with a
240 240 collection of Mercurial repositories in ``/var/local/hg/repos/``,
241 241 with a web interface at ``http://my-project.org/hg``. Bug comments
242 242 are sent to the Bugzilla email address
243 243 ``bugzilla@my-project.org``. ::
244 244
245 245 [bugzilla]
246 246 bzurl=http://my-project.org/bugzilla
247 247 user=bugmail@my-project.org
248 248 password=plugh
249 249 version=xmlrpc+email
250 250 bzemail=bugzilla@my-project.org
251 251 template=Changeset {node|short} in {root|basename}.
252 252 {hgweb}/{webroot}/rev/{node|short}\\n
253 253 {desc}\\n
254 254 strip=5
255 255
256 256 [web]
257 257 baseurl=http://my-project.org/hg
258 258
259 259 [usermap]
260 260 user@emaildomain.com=user.name@bugzilladomain.com
261 261
262 262 MySQL example configuration. This has a local Bugzilla 3.2 installation
263 263 in ``/opt/bugzilla-3.2``. The MySQL database is on ``localhost``,
264 264 the Bugzilla database name is ``bugs`` and MySQL is
265 265 accessed with MySQL username ``bugs`` password ``XYZZY``. It is used
266 266 with a collection of Mercurial repositories in ``/var/local/hg/repos/``,
267 267 with a web interface at ``http://my-project.org/hg``. ::
268 268
269 269 [bugzilla]
270 270 host=localhost
271 271 password=XYZZY
272 272 version=3.0
273 273 bzuser=unknown@domain.com
274 274 bzdir=/opt/bugzilla-3.2
275 275 template=Changeset {node|short} in {root|basename}.
276 276 {hgweb}/{webroot}/rev/{node|short}\\n
277 277 {desc}\\n
278 278 strip=5
279 279
280 280 [web]
281 281 baseurl=http://my-project.org/hg
282 282
283 283 [usermap]
284 284 user@emaildomain.com=user.name@bugzilladomain.com
285 285
286 286 All the above add a comment to the Bugzilla bug record of the form::
287 287
288 288 Changeset 3b16791d6642 in repository-name.
289 289 http://my-project.org/hg/repository-name/rev/3b16791d6642
290 290
291 291 Changeset commit comment. Bug 1234.
292 292 '''
293 293
294 294 from __future__ import absolute_import
295 295
296 296 import json
297 297 import re
298 298 import time
299 299
300 300 from mercurial.i18n import _
301 301 from mercurial.node import short
302 302 from mercurial import (
303 303 cmdutil,
304 configitems,
305 304 error,
306 305 mail,
307 306 registrar,
308 307 url,
309 308 util,
310 309 )
311 310
312 311 xmlrpclib = util.xmlrpclib
313 312
314 313 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
315 314 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
316 315 # be specifying the version(s) of Mercurial they are tested with, or
317 316 # leave the attribute unspecified.
318 317 testedwith = 'ships-with-hg-core'
319 318
320 319 configtable = {}
321 320 configitem = registrar.configitem(configtable)
322 321
323 322 configitem('bugzilla', 'apikey',
324 323 default='',
325 324 )
326 325 configitem('bugzilla', 'bzdir',
327 326 default='/var/www/html/bugzilla',
328 327 )
329 328 configitem('bugzilla', 'bzemail',
330 329 default=None,
331 330 )
332 331 configitem('bugzilla', 'bzurl',
333 332 default='http://localhost/bugzilla/',
334 333 )
335 334 configitem('bugzilla', 'bzuser',
336 335 default=None,
337 336 )
338 337 configitem('bugzilla', 'db',
339 338 default='bugs',
340 339 )
341 340 configitem('bugzilla', 'fixregexp',
342 341 default=(r'fix(?:es)?\s*(?:bugs?\s*)?,?\s*'
343 342 r'(?:nos?\.?|num(?:ber)?s?)?\s*'
344 343 r'(?P<ids>(?:#?\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
345 344 r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
346 345 )
347 346 configitem('bugzilla', 'fixresolution',
348 347 default='FIXED',
349 348 )
350 349 configitem('bugzilla', 'fixstatus',
351 350 default='RESOLVED',
352 351 )
353 352 configitem('bugzilla', 'host',
354 353 default='localhost',
355 354 )
356 355 configitem('bugzilla', 'notify',
357 default=configitems.dynamicdefault,
356 default=configitem.dynamicdefault,
358 357 )
359 358 configitem('bugzilla', 'password',
360 359 default=None,
361 360 )
362 361 configitem('bugzilla', 'regexp',
363 362 default=(r'bugs?\s*,?\s*(?:#|nos?\.?|num(?:ber)?s?)?\s*'
364 363 r'(?P<ids>(?:\d+\s*(?:,?\s*(?:and)?)?\s*)+)'
365 364 r'\.?\s*(?:h(?:ours?)?\s*(?P<hours>\d*(?:\.\d+)?))?')
366 365 )
367 366 configitem('bugzilla', 'strip',
368 367 default=0,
369 368 )
370 369 configitem('bugzilla', 'style',
371 370 default=None,
372 371 )
373 372 configitem('bugzilla', 'template',
374 373 default=None,
375 374 )
376 375 configitem('bugzilla', 'timeout',
377 376 default=5,
378 377 )
379 378 configitem('bugzilla', 'user',
380 379 default='bugs',
381 380 )
382 381 configitem('bugzilla', 'usermap',
383 382 default=None,
384 383 )
385 384 configitem('bugzilla', 'version',
386 385 default=None,
387 386 )
388 387
389 388 class bzaccess(object):
390 389 '''Base class for access to Bugzilla.'''
391 390
392 391 def __init__(self, ui):
393 392 self.ui = ui
394 393 usermap = self.ui.config('bugzilla', 'usermap')
395 394 if usermap:
396 395 self.ui.readconfig(usermap, sections=['usermap'])
397 396
398 397 def map_committer(self, user):
399 398 '''map name of committer to Bugzilla user name.'''
400 399 for committer, bzuser in self.ui.configitems('usermap'):
401 400 if committer.lower() == user.lower():
402 401 return bzuser
403 402 return user
404 403
405 404 # Methods to be implemented by access classes.
406 405 #
407 406 # 'bugs' is a dict keyed on bug id, where values are a dict holding
408 407 # updates to bug state. Recognized dict keys are:
409 408 #
410 409 # 'hours': Value, float containing work hours to be updated.
411 410 # 'fix': If key present, bug is to be marked fixed. Value ignored.
412 411
413 412 def filter_real_bug_ids(self, bugs):
414 413 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
415 414
416 415 def filter_cset_known_bug_ids(self, node, bugs):
417 416 '''remove bug IDs where node occurs in comment text from bugs.'''
418 417
419 418 def updatebug(self, bugid, newstate, text, committer):
420 419 '''update the specified bug. Add comment text and set new states.
421 420
422 421 If possible add the comment as being from the committer of
423 422 the changeset. Otherwise use the default Bugzilla user.
424 423 '''
425 424
426 425 def notify(self, bugs, committer):
427 426 '''Force sending of Bugzilla notification emails.
428 427
429 428 Only required if the access method does not trigger notification
430 429 emails automatically.
431 430 '''
432 431
433 432 # Bugzilla via direct access to MySQL database.
434 433 class bzmysql(bzaccess):
435 434 '''Support for direct MySQL access to Bugzilla.
436 435
437 436 The earliest Bugzilla version this is tested with is version 2.16.
438 437
439 438 If your Bugzilla is version 3.4 or above, you are strongly
440 439 recommended to use the XMLRPC access method instead.
441 440 '''
442 441
443 442 @staticmethod
444 443 def sql_buglist(ids):
445 444 '''return SQL-friendly list of bug ids'''
446 445 return '(' + ','.join(map(str, ids)) + ')'
447 446
448 447 _MySQLdb = None
449 448
450 449 def __init__(self, ui):
451 450 try:
452 451 import MySQLdb as mysql
453 452 bzmysql._MySQLdb = mysql
454 453 except ImportError as err:
455 454 raise error.Abort(_('python mysql support not available: %s') % err)
456 455
457 456 bzaccess.__init__(self, ui)
458 457
459 458 host = self.ui.config('bugzilla', 'host')
460 459 user = self.ui.config('bugzilla', 'user')
461 460 passwd = self.ui.config('bugzilla', 'password')
462 461 db = self.ui.config('bugzilla', 'db')
463 462 timeout = int(self.ui.config('bugzilla', 'timeout'))
464 463 self.ui.note(_('connecting to %s:%s as %s, password %s\n') %
465 464 (host, db, user, '*' * len(passwd)))
466 465 self.conn = bzmysql._MySQLdb.connect(host=host,
467 466 user=user, passwd=passwd,
468 467 db=db,
469 468 connect_timeout=timeout)
470 469 self.cursor = self.conn.cursor()
471 470 self.longdesc_id = self.get_longdesc_id()
472 471 self.user_ids = {}
473 472 self.default_notify = "cd %(bzdir)s && ./processmail %(id)s %(user)s"
474 473
475 474 def run(self, *args, **kwargs):
476 475 '''run a query.'''
477 476 self.ui.note(_('query: %s %s\n') % (args, kwargs))
478 477 try:
479 478 self.cursor.execute(*args, **kwargs)
480 479 except bzmysql._MySQLdb.MySQLError:
481 480 self.ui.note(_('failed query: %s %s\n') % (args, kwargs))
482 481 raise
483 482
484 483 def get_longdesc_id(self):
485 484 '''get identity of longdesc field'''
486 485 self.run('select fieldid from fielddefs where name = "longdesc"')
487 486 ids = self.cursor.fetchall()
488 487 if len(ids) != 1:
489 488 raise error.Abort(_('unknown database schema'))
490 489 return ids[0][0]
491 490
492 491 def filter_real_bug_ids(self, bugs):
493 492 '''filter not-existing bugs from set.'''
494 493 self.run('select bug_id from bugs where bug_id in %s' %
495 494 bzmysql.sql_buglist(bugs.keys()))
496 495 existing = [id for (id,) in self.cursor.fetchall()]
497 496 for id in bugs.keys():
498 497 if id not in existing:
499 498 self.ui.status(_('bug %d does not exist\n') % id)
500 499 del bugs[id]
501 500
502 501 def filter_cset_known_bug_ids(self, node, bugs):
503 502 '''filter bug ids that already refer to this changeset from set.'''
504 503 self.run('''select bug_id from longdescs where
505 504 bug_id in %s and thetext like "%%%s%%"''' %
506 505 (bzmysql.sql_buglist(bugs.keys()), short(node)))
507 506 for (id,) in self.cursor.fetchall():
508 507 self.ui.status(_('bug %d already knows about changeset %s\n') %
509 508 (id, short(node)))
510 509 del bugs[id]
511 510
512 511 def notify(self, bugs, committer):
513 512 '''tell bugzilla to send mail.'''
514 513 self.ui.status(_('telling bugzilla to send mail:\n'))
515 514 (user, userid) = self.get_bugzilla_user(committer)
516 515 for id in bugs.keys():
517 516 self.ui.status(_(' bug %s\n') % id)
518 517 cmdfmt = self.ui.config('bugzilla', 'notify', self.default_notify)
519 518 bzdir = self.ui.config('bugzilla', 'bzdir')
520 519 try:
521 520 # Backwards-compatible with old notify string, which
522 521 # took one string. This will throw with a new format
523 522 # string.
524 523 cmd = cmdfmt % id
525 524 except TypeError:
526 525 cmd = cmdfmt % {'bzdir': bzdir, 'id': id, 'user': user}
527 526 self.ui.note(_('running notify command %s\n') % cmd)
528 527 fp = util.popen('(%s) 2>&1' % cmd)
529 528 out = fp.read()
530 529 ret = fp.close()
531 530 if ret:
532 531 self.ui.warn(out)
533 532 raise error.Abort(_('bugzilla notify command %s') %
534 533 util.explainexit(ret)[0])
535 534 self.ui.status(_('done\n'))
536 535
537 536 def get_user_id(self, user):
538 537 '''look up numeric bugzilla user id.'''
539 538 try:
540 539 return self.user_ids[user]
541 540 except KeyError:
542 541 try:
543 542 userid = int(user)
544 543 except ValueError:
545 544 self.ui.note(_('looking up user %s\n') % user)
546 545 self.run('''select userid from profiles
547 546 where login_name like %s''', user)
548 547 all = self.cursor.fetchall()
549 548 if len(all) != 1:
550 549 raise KeyError(user)
551 550 userid = int(all[0][0])
552 551 self.user_ids[user] = userid
553 552 return userid
554 553
555 554 def get_bugzilla_user(self, committer):
556 555 '''See if committer is a registered bugzilla user. Return
557 556 bugzilla username and userid if so. If not, return default
558 557 bugzilla username and userid.'''
559 558 user = self.map_committer(committer)
560 559 try:
561 560 userid = self.get_user_id(user)
562 561 except KeyError:
563 562 try:
564 563 defaultuser = self.ui.config('bugzilla', 'bzuser')
565 564 if not defaultuser:
566 565 raise error.Abort(_('cannot find bugzilla user id for %s') %
567 566 user)
568 567 userid = self.get_user_id(defaultuser)
569 568 user = defaultuser
570 569 except KeyError:
571 570 raise error.Abort(_('cannot find bugzilla user id for %s or %s')
572 571 % (user, defaultuser))
573 572 return (user, userid)
574 573
575 574 def updatebug(self, bugid, newstate, text, committer):
576 575 '''update bug state with comment text.
577 576
578 577 Try adding comment as committer of changeset, otherwise as
579 578 default bugzilla user.'''
580 579 if len(newstate) > 0:
581 580 self.ui.warn(_("Bugzilla/MySQL cannot update bug state\n"))
582 581
583 582 (user, userid) = self.get_bugzilla_user(committer)
584 583 now = time.strftime('%Y-%m-%d %H:%M:%S')
585 584 self.run('''insert into longdescs
586 585 (bug_id, who, bug_when, thetext)
587 586 values (%s, %s, %s, %s)''',
588 587 (bugid, userid, now, text))
589 588 self.run('''insert into bugs_activity (bug_id, who, bug_when, fieldid)
590 589 values (%s, %s, %s, %s)''',
591 590 (bugid, userid, now, self.longdesc_id))
592 591 self.conn.commit()
593 592
594 593 class bzmysql_2_18(bzmysql):
595 594 '''support for bugzilla 2.18 series.'''
596 595
597 596 def __init__(self, ui):
598 597 bzmysql.__init__(self, ui)
599 598 self.default_notify = \
600 599 "cd %(bzdir)s && perl -T contrib/sendbugmail.pl %(id)s %(user)s"
601 600
602 601 class bzmysql_3_0(bzmysql_2_18):
603 602 '''support for bugzilla 3.0 series.'''
604 603
605 604 def __init__(self, ui):
606 605 bzmysql_2_18.__init__(self, ui)
607 606
608 607 def get_longdesc_id(self):
609 608 '''get identity of longdesc field'''
610 609 self.run('select id from fielddefs where name = "longdesc"')
611 610 ids = self.cursor.fetchall()
612 611 if len(ids) != 1:
613 612 raise error.Abort(_('unknown database schema'))
614 613 return ids[0][0]
615 614
616 615 # Bugzilla via XMLRPC interface.
617 616
618 617 class cookietransportrequest(object):
619 618 """A Transport request method that retains cookies over its lifetime.
620 619
621 620 The regular xmlrpclib transports ignore cookies. Which causes
622 621 a bit of a problem when you need a cookie-based login, as with
623 622 the Bugzilla XMLRPC interface prior to 4.4.3.
624 623
625 624 So this is a helper for defining a Transport which looks for
626 625 cookies being set in responses and saves them to add to all future
627 626 requests.
628 627 """
629 628
630 629 # Inspiration drawn from
631 630 # http://blog.godson.in/2010/09/how-to-make-python-xmlrpclib-client.html
632 631 # http://www.itkovian.net/base/transport-class-for-pythons-xml-rpc-lib/
633 632
634 633 cookies = []
635 634 def send_cookies(self, connection):
636 635 if self.cookies:
637 636 for cookie in self.cookies:
638 637 connection.putheader("Cookie", cookie)
639 638
640 639 def request(self, host, handler, request_body, verbose=0):
641 640 self.verbose = verbose
642 641 self.accept_gzip_encoding = False
643 642
644 643 # issue XML-RPC request
645 644 h = self.make_connection(host)
646 645 if verbose:
647 646 h.set_debuglevel(1)
648 647
649 648 self.send_request(h, handler, request_body)
650 649 self.send_host(h, host)
651 650 self.send_cookies(h)
652 651 self.send_user_agent(h)
653 652 self.send_content(h, request_body)
654 653
655 654 # Deal with differences between Python 2.6 and 2.7.
656 655 # In the former h is a HTTP(S). In the latter it's a
657 656 # HTTP(S)Connection. Luckily, the 2.6 implementation of
658 657 # HTTP(S) has an underlying HTTP(S)Connection, so extract
659 658 # that and use it.
660 659 try:
661 660 response = h.getresponse()
662 661 except AttributeError:
663 662 response = h._conn.getresponse()
664 663
665 664 # Add any cookie definitions to our list.
666 665 for header in response.msg.getallmatchingheaders("Set-Cookie"):
667 666 val = header.split(": ", 1)[1]
668 667 cookie = val.split(";", 1)[0]
669 668 self.cookies.append(cookie)
670 669
671 670 if response.status != 200:
672 671 raise xmlrpclib.ProtocolError(host + handler, response.status,
673 672 response.reason, response.msg.headers)
674 673
675 674 payload = response.read()
676 675 parser, unmarshaller = self.getparser()
677 676 parser.feed(payload)
678 677 parser.close()
679 678
680 679 return unmarshaller.close()
681 680
682 681 # The explicit calls to the underlying xmlrpclib __init__() methods are
683 682 # necessary. The xmlrpclib.Transport classes are old-style classes, and
684 683 # it turns out their __init__() doesn't get called when doing multiple
685 684 # inheritance with a new-style class.
686 685 class cookietransport(cookietransportrequest, xmlrpclib.Transport):
687 686 def __init__(self, use_datetime=0):
688 687 if util.safehasattr(xmlrpclib.Transport, "__init__"):
689 688 xmlrpclib.Transport.__init__(self, use_datetime)
690 689
691 690 class cookiesafetransport(cookietransportrequest, xmlrpclib.SafeTransport):
692 691 def __init__(self, use_datetime=0):
693 692 if util.safehasattr(xmlrpclib.Transport, "__init__"):
694 693 xmlrpclib.SafeTransport.__init__(self, use_datetime)
695 694
696 695 class bzxmlrpc(bzaccess):
697 696 """Support for access to Bugzilla via the Bugzilla XMLRPC API.
698 697
699 698 Requires a minimum Bugzilla version 3.4.
700 699 """
701 700
702 701 def __init__(self, ui):
703 702 bzaccess.__init__(self, ui)
704 703
705 704 bzweb = self.ui.config('bugzilla', 'bzurl')
706 705 bzweb = bzweb.rstrip("/") + "/xmlrpc.cgi"
707 706
708 707 user = self.ui.config('bugzilla', 'user')
709 708 passwd = self.ui.config('bugzilla', 'password')
710 709
711 710 self.fixstatus = self.ui.config('bugzilla', 'fixstatus')
712 711 self.fixresolution = self.ui.config('bugzilla', 'fixresolution')
713 712
714 713 self.bzproxy = xmlrpclib.ServerProxy(bzweb, self.transport(bzweb))
715 714 ver = self.bzproxy.Bugzilla.version()['version'].split('.')
716 715 self.bzvermajor = int(ver[0])
717 716 self.bzverminor = int(ver[1])
718 717 login = self.bzproxy.User.login({'login': user, 'password': passwd,
719 718 'restrict_login': True})
720 719 self.bztoken = login.get('token', '')
721 720
722 721 def transport(self, uri):
723 722 if util.urlreq.urlparse(uri, "http")[0] == "https":
724 723 return cookiesafetransport()
725 724 else:
726 725 return cookietransport()
727 726
728 727 def get_bug_comments(self, id):
729 728 """Return a string with all comment text for a bug."""
730 729 c = self.bzproxy.Bug.comments({'ids': [id],
731 730 'include_fields': ['text'],
732 731 'token': self.bztoken})
733 732 return ''.join([t['text'] for t in c['bugs'][str(id)]['comments']])
734 733
735 734 def filter_real_bug_ids(self, bugs):
736 735 probe = self.bzproxy.Bug.get({'ids': sorted(bugs.keys()),
737 736 'include_fields': [],
738 737 'permissive': True,
739 738 'token': self.bztoken,
740 739 })
741 740 for badbug in probe['faults']:
742 741 id = badbug['id']
743 742 self.ui.status(_('bug %d does not exist\n') % id)
744 743 del bugs[id]
745 744
746 745 def filter_cset_known_bug_ids(self, node, bugs):
747 746 for id in sorted(bugs.keys()):
748 747 if self.get_bug_comments(id).find(short(node)) != -1:
749 748 self.ui.status(_('bug %d already knows about changeset %s\n') %
750 749 (id, short(node)))
751 750 del bugs[id]
752 751
753 752 def updatebug(self, bugid, newstate, text, committer):
754 753 args = {}
755 754 if 'hours' in newstate:
756 755 args['work_time'] = newstate['hours']
757 756
758 757 if self.bzvermajor >= 4:
759 758 args['ids'] = [bugid]
760 759 args['comment'] = {'body' : text}
761 760 if 'fix' in newstate:
762 761 args['status'] = self.fixstatus
763 762 args['resolution'] = self.fixresolution
764 763 args['token'] = self.bztoken
765 764 self.bzproxy.Bug.update(args)
766 765 else:
767 766 if 'fix' in newstate:
768 767 self.ui.warn(_("Bugzilla/XMLRPC needs Bugzilla 4.0 or later "
769 768 "to mark bugs fixed\n"))
770 769 args['id'] = bugid
771 770 args['comment'] = text
772 771 self.bzproxy.Bug.add_comment(args)
773 772
774 773 class bzxmlrpcemail(bzxmlrpc):
775 774 """Read data from Bugzilla via XMLRPC, send updates via email.
776 775
777 776 Advantages of sending updates via email:
778 777 1. Comments can be added as any user, not just logged in user.
779 778 2. Bug statuses or other fields not accessible via XMLRPC can
780 779 potentially be updated.
781 780
782 781 There is no XMLRPC function to change bug status before Bugzilla
783 782 4.0, so bugs cannot be marked fixed via XMLRPC before Bugzilla 4.0.
784 783 But bugs can be marked fixed via email from 3.4 onwards.
785 784 """
786 785
787 786 # The email interface changes subtly between 3.4 and 3.6. In 3.4,
788 787 # in-email fields are specified as '@<fieldname> = <value>'. In
789 788 # 3.6 this becomes '@<fieldname> <value>'. And fieldname @bug_id
790 789 # in 3.4 becomes @id in 3.6. 3.6 and 4.0 both maintain backwards
791 790 # compatibility, but rather than rely on this use the new format for
792 791 # 4.0 onwards.
793 792
794 793 def __init__(self, ui):
795 794 bzxmlrpc.__init__(self, ui)
796 795
797 796 self.bzemail = self.ui.config('bugzilla', 'bzemail')
798 797 if not self.bzemail:
799 798 raise error.Abort(_("configuration 'bzemail' missing"))
800 799 mail.validateconfig(self.ui)
801 800
802 801 def makecommandline(self, fieldname, value):
803 802 if self.bzvermajor >= 4:
804 803 return "@%s %s" % (fieldname, str(value))
805 804 else:
806 805 if fieldname == "id":
807 806 fieldname = "bug_id"
808 807 return "@%s = %s" % (fieldname, str(value))
809 808
810 809 def send_bug_modify_email(self, bugid, commands, comment, committer):
811 810 '''send modification message to Bugzilla bug via email.
812 811
813 812 The message format is documented in the Bugzilla email_in.pl
814 813 specification. commands is a list of command lines, comment is the
815 814 comment text.
816 815
817 816 To stop users from crafting commit comments with
818 817 Bugzilla commands, specify the bug ID via the message body, rather
819 818 than the subject line, and leave a blank line after it.
820 819 '''
821 820 user = self.map_committer(committer)
822 821 matches = self.bzproxy.User.get({'match': [user],
823 822 'token': self.bztoken})
824 823 if not matches['users']:
825 824 user = self.ui.config('bugzilla', 'user')
826 825 matches = self.bzproxy.User.get({'match': [user],
827 826 'token': self.bztoken})
828 827 if not matches['users']:
829 828 raise error.Abort(_("default bugzilla user %s email not found")
830 829 % user)
831 830 user = matches['users'][0]['email']
832 831 commands.append(self.makecommandline("id", bugid))
833 832
834 833 text = "\n".join(commands) + "\n\n" + comment
835 834
836 835 _charsets = mail._charsets(self.ui)
837 836 user = mail.addressencode(self.ui, user, _charsets)
838 837 bzemail = mail.addressencode(self.ui, self.bzemail, _charsets)
839 838 msg = mail.mimeencode(self.ui, text, _charsets)
840 839 msg['From'] = user
841 840 msg['To'] = bzemail
842 841 msg['Subject'] = mail.headencode(self.ui, "Bug modification", _charsets)
843 842 sendmail = mail.connect(self.ui)
844 843 sendmail(user, bzemail, msg.as_string())
845 844
846 845 def updatebug(self, bugid, newstate, text, committer):
847 846 cmds = []
848 847 if 'hours' in newstate:
849 848 cmds.append(self.makecommandline("work_time", newstate['hours']))
850 849 if 'fix' in newstate:
851 850 cmds.append(self.makecommandline("bug_status", self.fixstatus))
852 851 cmds.append(self.makecommandline("resolution", self.fixresolution))
853 852 self.send_bug_modify_email(bugid, cmds, text, committer)
854 853
855 854 class NotFound(LookupError):
856 855 pass
857 856
858 857 class bzrestapi(bzaccess):
859 858 """Read and write bugzilla data using the REST API available since
860 859 Bugzilla 5.0.
861 860 """
862 861 def __init__(self, ui):
863 862 bzaccess.__init__(self, ui)
864 863 bz = self.ui.config('bugzilla', 'bzurl')
865 864 self.bzroot = '/'.join([bz, 'rest'])
866 865 self.apikey = self.ui.config('bugzilla', 'apikey')
867 866 self.user = self.ui.config('bugzilla', 'user')
868 867 self.passwd = self.ui.config('bugzilla', 'password')
869 868 self.fixstatus = self.ui.config('bugzilla', 'fixstatus')
870 869 self.fixresolution = self.ui.config('bugzilla', 'fixresolution')
871 870
872 871 def apiurl(self, targets, include_fields=None):
873 872 url = '/'.join([self.bzroot] + [str(t) for t in targets])
874 873 qv = {}
875 874 if self.apikey:
876 875 qv['api_key'] = self.apikey
877 876 elif self.user and self.passwd:
878 877 qv['login'] = self.user
879 878 qv['password'] = self.passwd
880 879 if include_fields:
881 880 qv['include_fields'] = include_fields
882 881 if qv:
883 882 url = '%s?%s' % (url, util.urlreq.urlencode(qv))
884 883 return url
885 884
886 885 def _fetch(self, burl):
887 886 try:
888 887 resp = url.open(self.ui, burl)
889 888 return json.loads(resp.read())
890 889 except util.urlerr.httperror as inst:
891 890 if inst.code == 401:
892 891 raise error.Abort(_('authorization failed'))
893 892 if inst.code == 404:
894 893 raise NotFound()
895 894 else:
896 895 raise
897 896
898 897 def _submit(self, burl, data, method='POST'):
899 898 data = json.dumps(data)
900 899 if method == 'PUT':
901 900 class putrequest(util.urlreq.request):
902 901 def get_method(self):
903 902 return 'PUT'
904 903 request_type = putrequest
905 904 else:
906 905 request_type = util.urlreq.request
907 906 req = request_type(burl, data,
908 907 {'Content-Type': 'application/json'})
909 908 try:
910 909 resp = url.opener(self.ui).open(req)
911 910 return json.loads(resp.read())
912 911 except util.urlerr.httperror as inst:
913 912 if inst.code == 401:
914 913 raise error.Abort(_('authorization failed'))
915 914 if inst.code == 404:
916 915 raise NotFound()
917 916 else:
918 917 raise
919 918
920 919 def filter_real_bug_ids(self, bugs):
921 920 '''remove bug IDs that do not exist in Bugzilla from bugs.'''
922 921 badbugs = set()
923 922 for bugid in bugs:
924 923 burl = self.apiurl(('bug', bugid), include_fields='status')
925 924 try:
926 925 self._fetch(burl)
927 926 except NotFound:
928 927 badbugs.add(bugid)
929 928 for bugid in badbugs:
930 929 del bugs[bugid]
931 930
932 931 def filter_cset_known_bug_ids(self, node, bugs):
933 932 '''remove bug IDs where node occurs in comment text from bugs.'''
934 933 sn = short(node)
935 934 for bugid in bugs.keys():
936 935 burl = self.apiurl(('bug', bugid, 'comment'), include_fields='text')
937 936 result = self._fetch(burl)
938 937 comments = result['bugs'][str(bugid)]['comments']
939 938 if any(sn in c['text'] for c in comments):
940 939 self.ui.status(_('bug %d already knows about changeset %s\n') %
941 940 (bugid, sn))
942 941 del bugs[bugid]
943 942
944 943 def updatebug(self, bugid, newstate, text, committer):
945 944 '''update the specified bug. Add comment text and set new states.
946 945
947 946 If possible add the comment as being from the committer of
948 947 the changeset. Otherwise use the default Bugzilla user.
949 948 '''
950 949 bugmod = {}
951 950 if 'hours' in newstate:
952 951 bugmod['work_time'] = newstate['hours']
953 952 if 'fix' in newstate:
954 953 bugmod['status'] = self.fixstatus
955 954 bugmod['resolution'] = self.fixresolution
956 955 if bugmod:
957 956 # if we have to change the bugs state do it here
958 957 bugmod['comment'] = {
959 958 'comment': text,
960 959 'is_private': False,
961 960 'is_markdown': False,
962 961 }
963 962 burl = self.apiurl(('bug', bugid))
964 963 self._submit(burl, bugmod, method='PUT')
965 964 self.ui.debug('updated bug %s\n' % bugid)
966 965 else:
967 966 burl = self.apiurl(('bug', bugid, 'comment'))
968 967 self._submit(burl, {
969 968 'comment': text,
970 969 'is_private': False,
971 970 'is_markdown': False,
972 971 })
973 972 self.ui.debug('added comment to bug %s\n' % bugid)
974 973
975 974 def notify(self, bugs, committer):
976 975 '''Force sending of Bugzilla notification emails.
977 976
978 977 Only required if the access method does not trigger notification
979 978 emails automatically.
980 979 '''
981 980 pass
982 981
983 982 class bugzilla(object):
984 983 # supported versions of bugzilla. different versions have
985 984 # different schemas.
986 985 _versions = {
987 986 '2.16': bzmysql,
988 987 '2.18': bzmysql_2_18,
989 988 '3.0': bzmysql_3_0,
990 989 'xmlrpc': bzxmlrpc,
991 990 'xmlrpc+email': bzxmlrpcemail,
992 991 'restapi': bzrestapi,
993 992 }
994 993
995 994 def __init__(self, ui, repo):
996 995 self.ui = ui
997 996 self.repo = repo
998 997
999 998 bzversion = self.ui.config('bugzilla', 'version')
1000 999 try:
1001 1000 bzclass = bugzilla._versions[bzversion]
1002 1001 except KeyError:
1003 1002 raise error.Abort(_('bugzilla version %s not supported') %
1004 1003 bzversion)
1005 1004 self.bzdriver = bzclass(self.ui)
1006 1005
1007 1006 self.bug_re = re.compile(
1008 1007 self.ui.config('bugzilla', 'regexp'), re.IGNORECASE)
1009 1008 self.fix_re = re.compile(
1010 1009 self.ui.config('bugzilla', 'fixregexp'), re.IGNORECASE)
1011 1010 self.split_re = re.compile(r'\D+')
1012 1011
1013 1012 def find_bugs(self, ctx):
1014 1013 '''return bugs dictionary created from commit comment.
1015 1014
1016 1015 Extract bug info from changeset comments. Filter out any that are
1017 1016 not known to Bugzilla, and any that already have a reference to
1018 1017 the given changeset in their comments.
1019 1018 '''
1020 1019 start = 0
1021 1020 hours = 0.0
1022 1021 bugs = {}
1023 1022 bugmatch = self.bug_re.search(ctx.description(), start)
1024 1023 fixmatch = self.fix_re.search(ctx.description(), start)
1025 1024 while True:
1026 1025 bugattribs = {}
1027 1026 if not bugmatch and not fixmatch:
1028 1027 break
1029 1028 if not bugmatch:
1030 1029 m = fixmatch
1031 1030 elif not fixmatch:
1032 1031 m = bugmatch
1033 1032 else:
1034 1033 if bugmatch.start() < fixmatch.start():
1035 1034 m = bugmatch
1036 1035 else:
1037 1036 m = fixmatch
1038 1037 start = m.end()
1039 1038 if m is bugmatch:
1040 1039 bugmatch = self.bug_re.search(ctx.description(), start)
1041 1040 if 'fix' in bugattribs:
1042 1041 del bugattribs['fix']
1043 1042 else:
1044 1043 fixmatch = self.fix_re.search(ctx.description(), start)
1045 1044 bugattribs['fix'] = None
1046 1045
1047 1046 try:
1048 1047 ids = m.group('ids')
1049 1048 except IndexError:
1050 1049 ids = m.group(1)
1051 1050 try:
1052 1051 hours = float(m.group('hours'))
1053 1052 bugattribs['hours'] = hours
1054 1053 except IndexError:
1055 1054 pass
1056 1055 except TypeError:
1057 1056 pass
1058 1057 except ValueError:
1059 1058 self.ui.status(_("%s: invalid hours\n") % m.group('hours'))
1060 1059
1061 1060 for id in self.split_re.split(ids):
1062 1061 if not id:
1063 1062 continue
1064 1063 bugs[int(id)] = bugattribs
1065 1064 if bugs:
1066 1065 self.bzdriver.filter_real_bug_ids(bugs)
1067 1066 if bugs:
1068 1067 self.bzdriver.filter_cset_known_bug_ids(ctx.node(), bugs)
1069 1068 return bugs
1070 1069
1071 1070 def update(self, bugid, newstate, ctx):
1072 1071 '''update bugzilla bug with reference to changeset.'''
1073 1072
1074 1073 def webroot(root):
1075 1074 '''strip leading prefix of repo root and turn into
1076 1075 url-safe path.'''
1077 1076 count = int(self.ui.config('bugzilla', 'strip'))
1078 1077 root = util.pconvert(root)
1079 1078 while count > 0:
1080 1079 c = root.find('/')
1081 1080 if c == -1:
1082 1081 break
1083 1082 root = root[c + 1:]
1084 1083 count -= 1
1085 1084 return root
1086 1085
1087 1086 mapfile = None
1088 1087 tmpl = self.ui.config('bugzilla', 'template')
1089 1088 if not tmpl:
1090 1089 mapfile = self.ui.config('bugzilla', 'style')
1091 1090 if not mapfile and not tmpl:
1092 1091 tmpl = _('changeset {node|short} in repo {root} refers '
1093 1092 'to bug {bug}.\ndetails:\n\t{desc|tabindent}')
1094 1093 spec = cmdutil.logtemplatespec(tmpl, mapfile)
1095 1094 t = cmdutil.changeset_templater(self.ui, self.repo, spec,
1096 1095 False, None, False)
1097 1096 self.ui.pushbuffer()
1098 1097 t.show(ctx, changes=ctx.changeset(),
1099 1098 bug=str(bugid),
1100 1099 hgweb=self.ui.config('web', 'baseurl'),
1101 1100 root=self.repo.root,
1102 1101 webroot=webroot(self.repo.root))
1103 1102 data = self.ui.popbuffer()
1104 1103 self.bzdriver.updatebug(bugid, newstate, data, util.email(ctx.user()))
1105 1104
1106 1105 def notify(self, bugs, committer):
1107 1106 '''ensure Bugzilla users are notified of bug change.'''
1108 1107 self.bzdriver.notify(bugs, committer)
1109 1108
1110 1109 def hook(ui, repo, hooktype, node=None, **kwargs):
1111 1110 '''add comment to bugzilla for each changeset that refers to a
1112 1111 bugzilla bug id. only add a comment once per bug, so same change
1113 1112 seen multiple times does not fill bug with duplicate data.'''
1114 1113 if node is None:
1115 1114 raise error.Abort(_('hook type %s does not pass a changeset id') %
1116 1115 hooktype)
1117 1116 try:
1118 1117 bz = bugzilla(ui, repo)
1119 1118 ctx = repo[node]
1120 1119 bugs = bz.find_bugs(ctx)
1121 1120 if bugs:
1122 1121 for bug in bugs:
1123 1122 bz.update(bug, bugs[bug], ctx)
1124 1123 bz.notify(bugs, util.email(ctx.user()))
1125 1124 except Exception as e:
1126 1125 raise error.Abort(_('Bugzilla error: %s') % e)
@@ -1,1635 +1,1634 b''
1 1 # histedit.py - interactive history editing for mercurial
2 2 #
3 3 # Copyright 2009 Augie Fackler <raf@durin42.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 """interactive history editing
8 8
9 9 With this extension installed, Mercurial gains one new command: histedit. Usage
10 10 is as follows, assuming the following history::
11 11
12 12 @ 3[tip] 7c2fd3b9020c 2009-04-27 18:04 -0500 durin42
13 13 | Add delta
14 14 |
15 15 o 2 030b686bedc4 2009-04-27 18:04 -0500 durin42
16 16 | Add gamma
17 17 |
18 18 o 1 c561b4e977df 2009-04-27 18:04 -0500 durin42
19 19 | Add beta
20 20 |
21 21 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
22 22 Add alpha
23 23
24 24 If you were to run ``hg histedit c561b4e977df``, you would see the following
25 25 file open in your editor::
26 26
27 27 pick c561b4e977df Add beta
28 28 pick 030b686bedc4 Add gamma
29 29 pick 7c2fd3b9020c Add delta
30 30
31 31 # Edit history between c561b4e977df and 7c2fd3b9020c
32 32 #
33 33 # Commits are listed from least to most recent
34 34 #
35 35 # Commands:
36 36 # p, pick = use commit
37 37 # e, edit = use commit, but stop for amending
38 38 # f, fold = use commit, but combine it with the one above
39 39 # r, roll = like fold, but discard this commit's description and date
40 40 # d, drop = remove commit from history
41 41 # m, mess = edit commit message without changing commit content
42 42 # b, base = checkout changeset and apply further changesets from there
43 43 #
44 44
45 45 In this file, lines beginning with ``#`` are ignored. You must specify a rule
46 46 for each revision in your history. For example, if you had meant to add gamma
47 47 before beta, and then wanted to add delta in the same revision as beta, you
48 48 would reorganize the file to look like this::
49 49
50 50 pick 030b686bedc4 Add gamma
51 51 pick c561b4e977df Add beta
52 52 fold 7c2fd3b9020c Add delta
53 53
54 54 # Edit history between c561b4e977df and 7c2fd3b9020c
55 55 #
56 56 # Commits are listed from least to most recent
57 57 #
58 58 # Commands:
59 59 # p, pick = use commit
60 60 # e, edit = use commit, but stop for amending
61 61 # f, fold = use commit, but combine it with the one above
62 62 # r, roll = like fold, but discard this commit's description and date
63 63 # d, drop = remove commit from history
64 64 # m, mess = edit commit message without changing commit content
65 65 # b, base = checkout changeset and apply further changesets from there
66 66 #
67 67
68 68 At which point you close the editor and ``histedit`` starts working. When you
69 69 specify a ``fold`` operation, ``histedit`` will open an editor when it folds
70 70 those revisions together, offering you a chance to clean up the commit message::
71 71
72 72 Add beta
73 73 ***
74 74 Add delta
75 75
76 76 Edit the commit message to your liking, then close the editor. The date used
77 77 for the commit will be the later of the two commits' dates. For this example,
78 78 let's assume that the commit message was changed to ``Add beta and delta.``
79 79 After histedit has run and had a chance to remove any old or temporary
80 80 revisions it needed, the history looks like this::
81 81
82 82 @ 2[tip] 989b4d060121 2009-04-27 18:04 -0500 durin42
83 83 | Add beta and delta.
84 84 |
85 85 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
86 86 | Add gamma
87 87 |
88 88 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
89 89 Add alpha
90 90
91 91 Note that ``histedit`` does *not* remove any revisions (even its own temporary
92 92 ones) until after it has completed all the editing operations, so it will
93 93 probably perform several strip operations when it's done. For the above example,
94 94 it had to run strip twice. Strip can be slow depending on a variety of factors,
95 95 so you might need to be a little patient. You can choose to keep the original
96 96 revisions by passing the ``--keep`` flag.
97 97
98 98 The ``edit`` operation will drop you back to a command prompt,
99 99 allowing you to edit files freely, or even use ``hg record`` to commit
100 100 some changes as a separate commit. When you're done, any remaining
101 101 uncommitted changes will be committed as well. When done, run ``hg
102 102 histedit --continue`` to finish this step. If there are uncommitted
103 103 changes, you'll be prompted for a new commit message, but the default
104 104 commit message will be the original message for the ``edit`` ed
105 105 revision, and the date of the original commit will be preserved.
106 106
107 107 The ``message`` operation will give you a chance to revise a commit
108 108 message without changing the contents. It's a shortcut for doing
109 109 ``edit`` immediately followed by `hg histedit --continue``.
110 110
111 111 If ``histedit`` encounters a conflict when moving a revision (while
112 112 handling ``pick`` or ``fold``), it'll stop in a similar manner to
113 113 ``edit`` with the difference that it won't prompt you for a commit
114 114 message when done. If you decide at this point that you don't like how
115 115 much work it will be to rearrange history, or that you made a mistake,
116 116 you can use ``hg histedit --abort`` to abandon the new changes you
117 117 have made and return to the state before you attempted to edit your
118 118 history.
119 119
120 120 If we clone the histedit-ed example repository above and add four more
121 121 changes, such that we have the following history::
122 122
123 123 @ 6[tip] 038383181893 2009-04-27 18:04 -0500 stefan
124 124 | Add theta
125 125 |
126 126 o 5 140988835471 2009-04-27 18:04 -0500 stefan
127 127 | Add eta
128 128 |
129 129 o 4 122930637314 2009-04-27 18:04 -0500 stefan
130 130 | Add zeta
131 131 |
132 132 o 3 836302820282 2009-04-27 18:04 -0500 stefan
133 133 | Add epsilon
134 134 |
135 135 o 2 989b4d060121 2009-04-27 18:04 -0500 durin42
136 136 | Add beta and delta.
137 137 |
138 138 o 1 081603921c3f 2009-04-27 18:04 -0500 durin42
139 139 | Add gamma
140 140 |
141 141 o 0 d8d2fcd0e319 2009-04-27 18:04 -0500 durin42
142 142 Add alpha
143 143
144 144 If you run ``hg histedit --outgoing`` on the clone then it is the same
145 145 as running ``hg histedit 836302820282``. If you need plan to push to a
146 146 repository that Mercurial does not detect to be related to the source
147 147 repo, you can add a ``--force`` option.
148 148
149 149 Config
150 150 ------
151 151
152 152 Histedit rule lines are truncated to 80 characters by default. You
153 153 can customize this behavior by setting a different length in your
154 154 configuration file::
155 155
156 156 [histedit]
157 157 linelen = 120 # truncate rule lines at 120 characters
158 158
159 159 ``hg histedit`` attempts to automatically choose an appropriate base
160 160 revision to use. To change which base revision is used, define a
161 161 revset in your configuration file::
162 162
163 163 [histedit]
164 164 defaultrev = only(.) & draft()
165 165
166 166 By default each edited revision needs to be present in histedit commands.
167 167 To remove revision you need to use ``drop`` operation. You can configure
168 168 the drop to be implicit for missing commits by adding::
169 169
170 170 [histedit]
171 171 dropmissing = True
172 172
173 173 By default, histedit will close the transaction after each action. For
174 174 performance purposes, you can configure histedit to use a single transaction
175 175 across the entire histedit. WARNING: This setting introduces a significant risk
176 176 of losing the work you've done in a histedit if the histedit aborts
177 177 unexpectedly::
178 178
179 179 [histedit]
180 180 singletransaction = True
181 181
182 182 """
183 183
184 184 from __future__ import absolute_import
185 185
186 186 import errno
187 187 import os
188 188
189 189 from mercurial.i18n import _
190 190 from mercurial import (
191 191 bundle2,
192 192 cmdutil,
193 configitems,
194 193 context,
195 194 copies,
196 195 destutil,
197 196 discovery,
198 197 error,
199 198 exchange,
200 199 extensions,
201 200 hg,
202 201 lock,
203 202 merge as mergemod,
204 203 mergeutil,
205 204 node,
206 205 obsolete,
207 206 registrar,
208 207 repair,
209 208 scmutil,
210 209 util,
211 210 )
212 211
213 212 pickle = util.pickle
214 213 release = lock.release
215 214 cmdtable = {}
216 215 command = registrar.command(cmdtable)
217 216
218 217 configtable = {}
219 218 configitem = registrar.configitem(configtable)
220 219 configitem('experimental', 'histedit.autoverb',
221 220 default=False,
222 221 )
223 222 configitem('histedit', 'defaultrev',
224 default=configitems.dynamicdefault,
223 default=configitem.dynamicdefault,
225 224 )
226 225 configitem('histedit', 'dropmissing',
227 226 default=False,
228 227 )
229 228 configitem('histedit', 'linelen',
230 229 default=80,
231 230 )
232 231 configitem('histedit', 'singletransaction',
233 232 default=False,
234 233 )
235 234
236 235 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
237 236 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
238 237 # be specifying the version(s) of Mercurial they are tested with, or
239 238 # leave the attribute unspecified.
240 239 testedwith = 'ships-with-hg-core'
241 240
242 241 actiontable = {}
243 242 primaryactions = set()
244 243 secondaryactions = set()
245 244 tertiaryactions = set()
246 245 internalactions = set()
247 246
248 247 def geteditcomment(ui, first, last):
249 248 """ construct the editor comment
250 249 The comment includes::
251 250 - an intro
252 251 - sorted primary commands
253 252 - sorted short commands
254 253 - sorted long commands
255 254 - additional hints
256 255
257 256 Commands are only included once.
258 257 """
259 258 intro = _("""Edit history between %s and %s
260 259
261 260 Commits are listed from least to most recent
262 261
263 262 You can reorder changesets by reordering the lines
264 263
265 264 Commands:
266 265 """)
267 266 actions = []
268 267 def addverb(v):
269 268 a = actiontable[v]
270 269 lines = a.message.split("\n")
271 270 if len(a.verbs):
272 271 v = ', '.join(sorted(a.verbs, key=lambda v: len(v)))
273 272 actions.append(" %s = %s" % (v, lines[0]))
274 273 actions.extend([' %s' for l in lines[1:]])
275 274
276 275 for v in (
277 276 sorted(primaryactions) +
278 277 sorted(secondaryactions) +
279 278 sorted(tertiaryactions)
280 279 ):
281 280 addverb(v)
282 281 actions.append('')
283 282
284 283 hints = []
285 284 if ui.configbool('histedit', 'dropmissing'):
286 285 hints.append("Deleting a changeset from the list "
287 286 "will DISCARD it from the edited history!")
288 287
289 288 lines = (intro % (first, last)).split('\n') + actions + hints
290 289
291 290 return ''.join(['# %s\n' % l if l else '#\n' for l in lines])
292 291
293 292 class histeditstate(object):
294 293 def __init__(self, repo, parentctxnode=None, actions=None, keep=None,
295 294 topmost=None, replacements=None, lock=None, wlock=None):
296 295 self.repo = repo
297 296 self.actions = actions
298 297 self.keep = keep
299 298 self.topmost = topmost
300 299 self.parentctxnode = parentctxnode
301 300 self.lock = lock
302 301 self.wlock = wlock
303 302 self.backupfile = None
304 303 if replacements is None:
305 304 self.replacements = []
306 305 else:
307 306 self.replacements = replacements
308 307
309 308 def read(self):
310 309 """Load histedit state from disk and set fields appropriately."""
311 310 try:
312 311 state = self.repo.vfs.read('histedit-state')
313 312 except IOError as err:
314 313 if err.errno != errno.ENOENT:
315 314 raise
316 315 cmdutil.wrongtooltocontinue(self.repo, _('histedit'))
317 316
318 317 if state.startswith('v1\n'):
319 318 data = self._load()
320 319 parentctxnode, rules, keep, topmost, replacements, backupfile = data
321 320 else:
322 321 data = pickle.loads(state)
323 322 parentctxnode, rules, keep, topmost, replacements = data
324 323 backupfile = None
325 324
326 325 self.parentctxnode = parentctxnode
327 326 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
328 327 actions = parserules(rules, self)
329 328 self.actions = actions
330 329 self.keep = keep
331 330 self.topmost = topmost
332 331 self.replacements = replacements
333 332 self.backupfile = backupfile
334 333
335 334 def write(self, tr=None):
336 335 if tr:
337 336 tr.addfilegenerator('histedit-state', ('histedit-state',),
338 337 self._write, location='plain')
339 338 else:
340 339 with self.repo.vfs("histedit-state", "w") as f:
341 340 self._write(f)
342 341
343 342 def _write(self, fp):
344 343 fp.write('v1\n')
345 344 fp.write('%s\n' % node.hex(self.parentctxnode))
346 345 fp.write('%s\n' % node.hex(self.topmost))
347 346 fp.write('%s\n' % self.keep)
348 347 fp.write('%d\n' % len(self.actions))
349 348 for action in self.actions:
350 349 fp.write('%s\n' % action.tostate())
351 350 fp.write('%d\n' % len(self.replacements))
352 351 for replacement in self.replacements:
353 352 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
354 353 for r in replacement[1])))
355 354 backupfile = self.backupfile
356 355 if not backupfile:
357 356 backupfile = ''
358 357 fp.write('%s\n' % backupfile)
359 358
360 359 def _load(self):
361 360 fp = self.repo.vfs('histedit-state', 'r')
362 361 lines = [l[:-1] for l in fp.readlines()]
363 362
364 363 index = 0
365 364 lines[index] # version number
366 365 index += 1
367 366
368 367 parentctxnode = node.bin(lines[index])
369 368 index += 1
370 369
371 370 topmost = node.bin(lines[index])
372 371 index += 1
373 372
374 373 keep = lines[index] == 'True'
375 374 index += 1
376 375
377 376 # Rules
378 377 rules = []
379 378 rulelen = int(lines[index])
380 379 index += 1
381 380 for i in xrange(rulelen):
382 381 ruleaction = lines[index]
383 382 index += 1
384 383 rule = lines[index]
385 384 index += 1
386 385 rules.append((ruleaction, rule))
387 386
388 387 # Replacements
389 388 replacements = []
390 389 replacementlen = int(lines[index])
391 390 index += 1
392 391 for i in xrange(replacementlen):
393 392 replacement = lines[index]
394 393 original = node.bin(replacement[:40])
395 394 succ = [node.bin(replacement[i:i + 40]) for i in
396 395 range(40, len(replacement), 40)]
397 396 replacements.append((original, succ))
398 397 index += 1
399 398
400 399 backupfile = lines[index]
401 400 index += 1
402 401
403 402 fp.close()
404 403
405 404 return parentctxnode, rules, keep, topmost, replacements, backupfile
406 405
407 406 def clear(self):
408 407 if self.inprogress():
409 408 self.repo.vfs.unlink('histedit-state')
410 409
411 410 def inprogress(self):
412 411 return self.repo.vfs.exists('histedit-state')
413 412
414 413
415 414 class histeditaction(object):
416 415 def __init__(self, state, node):
417 416 self.state = state
418 417 self.repo = state.repo
419 418 self.node = node
420 419
421 420 @classmethod
422 421 def fromrule(cls, state, rule):
423 422 """Parses the given rule, returning an instance of the histeditaction.
424 423 """
425 424 rulehash = rule.strip().split(' ', 1)[0]
426 425 try:
427 426 rev = node.bin(rulehash)
428 427 except TypeError:
429 428 raise error.ParseError("invalid changeset %s" % rulehash)
430 429 return cls(state, rev)
431 430
432 431 def verify(self, prev, expected, seen):
433 432 """ Verifies semantic correctness of the rule"""
434 433 repo = self.repo
435 434 ha = node.hex(self.node)
436 435 try:
437 436 self.node = repo[ha].node()
438 437 except error.RepoError:
439 438 raise error.ParseError(_('unknown changeset %s listed')
440 439 % ha[:12])
441 440 if self.node is not None:
442 441 self._verifynodeconstraints(prev, expected, seen)
443 442
444 443 def _verifynodeconstraints(self, prev, expected, seen):
445 444 # by default command need a node in the edited list
446 445 if self.node not in expected:
447 446 raise error.ParseError(_('%s "%s" changeset was not a candidate')
448 447 % (self.verb, node.short(self.node)),
449 448 hint=_('only use listed changesets'))
450 449 # and only one command per node
451 450 if self.node in seen:
452 451 raise error.ParseError(_('duplicated command for changeset %s') %
453 452 node.short(self.node))
454 453
455 454 def torule(self):
456 455 """build a histedit rule line for an action
457 456
458 457 by default lines are in the form:
459 458 <hash> <rev> <summary>
460 459 """
461 460 ctx = self.repo[self.node]
462 461 summary = _getsummary(ctx)
463 462 line = '%s %s %d %s' % (self.verb, ctx, ctx.rev(), summary)
464 463 # trim to 75 columns by default so it's not stupidly wide in my editor
465 464 # (the 5 more are left for verb)
466 465 maxlen = self.repo.ui.configint('histedit', 'linelen')
467 466 maxlen = max(maxlen, 22) # avoid truncating hash
468 467 return util.ellipsis(line, maxlen)
469 468
470 469 def tostate(self):
471 470 """Print an action in format used by histedit state files
472 471 (the first line is a verb, the remainder is the second)
473 472 """
474 473 return "%s\n%s" % (self.verb, node.hex(self.node))
475 474
476 475 def run(self):
477 476 """Runs the action. The default behavior is simply apply the action's
478 477 rulectx onto the current parentctx."""
479 478 self.applychange()
480 479 self.continuedirty()
481 480 return self.continueclean()
482 481
483 482 def applychange(self):
484 483 """Applies the changes from this action's rulectx onto the current
485 484 parentctx, but does not commit them."""
486 485 repo = self.repo
487 486 rulectx = repo[self.node]
488 487 repo.ui.pushbuffer(error=True, labeled=True)
489 488 hg.update(repo, self.state.parentctxnode, quietempty=True)
490 489 stats = applychanges(repo.ui, repo, rulectx, {})
491 490 if stats and stats[3] > 0:
492 491 buf = repo.ui.popbuffer()
493 492 repo.ui.write(*buf)
494 493 raise error.InterventionRequired(
495 494 _('Fix up the change (%s %s)') %
496 495 (self.verb, node.short(self.node)),
497 496 hint=_('hg histedit --continue to resume'))
498 497 else:
499 498 repo.ui.popbuffer()
500 499
501 500 def continuedirty(self):
502 501 """Continues the action when changes have been applied to the working
503 502 copy. The default behavior is to commit the dirty changes."""
504 503 repo = self.repo
505 504 rulectx = repo[self.node]
506 505
507 506 editor = self.commiteditor()
508 507 commit = commitfuncfor(repo, rulectx)
509 508
510 509 commit(text=rulectx.description(), user=rulectx.user(),
511 510 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
512 511
513 512 def commiteditor(self):
514 513 """The editor to be used to edit the commit message."""
515 514 return False
516 515
517 516 def continueclean(self):
518 517 """Continues the action when the working copy is clean. The default
519 518 behavior is to accept the current commit as the new version of the
520 519 rulectx."""
521 520 ctx = self.repo['.']
522 521 if ctx.node() == self.state.parentctxnode:
523 522 self.repo.ui.warn(_('%s: skipping changeset (no changes)\n') %
524 523 node.short(self.node))
525 524 return ctx, [(self.node, tuple())]
526 525 if ctx.node() == self.node:
527 526 # Nothing changed
528 527 return ctx, []
529 528 return ctx, [(self.node, (ctx.node(),))]
530 529
531 530 def commitfuncfor(repo, src):
532 531 """Build a commit function for the replacement of <src>
533 532
534 533 This function ensure we apply the same treatment to all changesets.
535 534
536 535 - Add a 'histedit_source' entry in extra.
537 536
538 537 Note that fold has its own separated logic because its handling is a bit
539 538 different and not easily factored out of the fold method.
540 539 """
541 540 phasemin = src.phase()
542 541 def commitfunc(**kwargs):
543 542 overrides = {('phases', 'new-commit'): phasemin}
544 543 with repo.ui.configoverride(overrides, 'histedit'):
545 544 extra = kwargs.get('extra', {}).copy()
546 545 extra['histedit_source'] = src.hex()
547 546 kwargs['extra'] = extra
548 547 return repo.commit(**kwargs)
549 548 return commitfunc
550 549
551 550 def applychanges(ui, repo, ctx, opts):
552 551 """Merge changeset from ctx (only) in the current working directory"""
553 552 wcpar = repo.dirstate.parents()[0]
554 553 if ctx.p1().node() == wcpar:
555 554 # edits are "in place" we do not need to make any merge,
556 555 # just applies changes on parent for editing
557 556 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
558 557 stats = None
559 558 else:
560 559 try:
561 560 # ui.forcemerge is an internal variable, do not document
562 561 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
563 562 'histedit')
564 563 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
565 564 finally:
566 565 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
567 566 return stats
568 567
569 568 def collapse(repo, first, last, commitopts, skipprompt=False):
570 569 """collapse the set of revisions from first to last as new one.
571 570
572 571 Expected commit options are:
573 572 - message
574 573 - date
575 574 - username
576 575 Commit message is edited in all cases.
577 576
578 577 This function works in memory."""
579 578 ctxs = list(repo.set('%d::%d', first, last))
580 579 if not ctxs:
581 580 return None
582 581 for c in ctxs:
583 582 if not c.mutable():
584 583 raise error.ParseError(
585 584 _("cannot fold into public change %s") % node.short(c.node()))
586 585 base = first.parents()[0]
587 586
588 587 # commit a new version of the old changeset, including the update
589 588 # collect all files which might be affected
590 589 files = set()
591 590 for ctx in ctxs:
592 591 files.update(ctx.files())
593 592
594 593 # Recompute copies (avoid recording a -> b -> a)
595 594 copied = copies.pathcopies(base, last)
596 595
597 596 # prune files which were reverted by the updates
598 597 files = [f for f in files if not cmdutil.samefile(f, last, base)]
599 598 # commit version of these files as defined by head
600 599 headmf = last.manifest()
601 600 def filectxfn(repo, ctx, path):
602 601 if path in headmf:
603 602 fctx = last[path]
604 603 flags = fctx.flags()
605 604 mctx = context.memfilectx(repo,
606 605 fctx.path(), fctx.data(),
607 606 islink='l' in flags,
608 607 isexec='x' in flags,
609 608 copied=copied.get(path))
610 609 return mctx
611 610 return None
612 611
613 612 if commitopts.get('message'):
614 613 message = commitopts['message']
615 614 else:
616 615 message = first.description()
617 616 user = commitopts.get('user')
618 617 date = commitopts.get('date')
619 618 extra = commitopts.get('extra')
620 619
621 620 parents = (first.p1().node(), first.p2().node())
622 621 editor = None
623 622 if not skipprompt:
624 623 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
625 624 new = context.memctx(repo,
626 625 parents=parents,
627 626 text=message,
628 627 files=files,
629 628 filectxfn=filectxfn,
630 629 user=user,
631 630 date=date,
632 631 extra=extra,
633 632 editor=editor)
634 633 return repo.commitctx(new)
635 634
636 635 def _isdirtywc(repo):
637 636 return repo[None].dirty(missing=True)
638 637
639 638 def abortdirty():
640 639 raise error.Abort(_('working copy has pending changes'),
641 640 hint=_('amend, commit, or revert them and run histedit '
642 641 '--continue, or abort with histedit --abort'))
643 642
644 643 def action(verbs, message, priority=False, internal=False):
645 644 def wrap(cls):
646 645 assert not priority or not internal
647 646 verb = verbs[0]
648 647 if priority:
649 648 primaryactions.add(verb)
650 649 elif internal:
651 650 internalactions.add(verb)
652 651 elif len(verbs) > 1:
653 652 secondaryactions.add(verb)
654 653 else:
655 654 tertiaryactions.add(verb)
656 655
657 656 cls.verb = verb
658 657 cls.verbs = verbs
659 658 cls.message = message
660 659 for verb in verbs:
661 660 actiontable[verb] = cls
662 661 return cls
663 662 return wrap
664 663
665 664 @action(['pick', 'p'],
666 665 _('use commit'),
667 666 priority=True)
668 667 class pick(histeditaction):
669 668 def run(self):
670 669 rulectx = self.repo[self.node]
671 670 if rulectx.parents()[0].node() == self.state.parentctxnode:
672 671 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
673 672 return rulectx, []
674 673
675 674 return super(pick, self).run()
676 675
677 676 @action(['edit', 'e'],
678 677 _('use commit, but stop for amending'),
679 678 priority=True)
680 679 class edit(histeditaction):
681 680 def run(self):
682 681 repo = self.repo
683 682 rulectx = repo[self.node]
684 683 hg.update(repo, self.state.parentctxnode, quietempty=True)
685 684 applychanges(repo.ui, repo, rulectx, {})
686 685 raise error.InterventionRequired(
687 686 _('Editing (%s), you may commit or record as needed now.')
688 687 % node.short(self.node),
689 688 hint=_('hg histedit --continue to resume'))
690 689
691 690 def commiteditor(self):
692 691 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
693 692
694 693 @action(['fold', 'f'],
695 694 _('use commit, but combine it with the one above'))
696 695 class fold(histeditaction):
697 696 def verify(self, prev, expected, seen):
698 697 """ Verifies semantic correctness of the fold rule"""
699 698 super(fold, self).verify(prev, expected, seen)
700 699 repo = self.repo
701 700 if not prev:
702 701 c = repo[self.node].parents()[0]
703 702 elif not prev.verb in ('pick', 'base'):
704 703 return
705 704 else:
706 705 c = repo[prev.node]
707 706 if not c.mutable():
708 707 raise error.ParseError(
709 708 _("cannot fold into public change %s") % node.short(c.node()))
710 709
711 710
712 711 def continuedirty(self):
713 712 repo = self.repo
714 713 rulectx = repo[self.node]
715 714
716 715 commit = commitfuncfor(repo, rulectx)
717 716 commit(text='fold-temp-revision %s' % node.short(self.node),
718 717 user=rulectx.user(), date=rulectx.date(),
719 718 extra=rulectx.extra())
720 719
721 720 def continueclean(self):
722 721 repo = self.repo
723 722 ctx = repo['.']
724 723 rulectx = repo[self.node]
725 724 parentctxnode = self.state.parentctxnode
726 725 if ctx.node() == parentctxnode:
727 726 repo.ui.warn(_('%s: empty changeset\n') %
728 727 node.short(self.node))
729 728 return ctx, [(self.node, (parentctxnode,))]
730 729
731 730 parentctx = repo[parentctxnode]
732 731 newcommits = set(c.node() for c in repo.set('(%d::. - %d)', parentctx,
733 732 parentctx))
734 733 if not newcommits:
735 734 repo.ui.warn(_('%s: cannot fold - working copy is not a '
736 735 'descendant of previous commit %s\n') %
737 736 (node.short(self.node), node.short(parentctxnode)))
738 737 return ctx, [(self.node, (ctx.node(),))]
739 738
740 739 middlecommits = newcommits.copy()
741 740 middlecommits.discard(ctx.node())
742 741
743 742 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
744 743 middlecommits)
745 744
746 745 def skipprompt(self):
747 746 """Returns true if the rule should skip the message editor.
748 747
749 748 For example, 'fold' wants to show an editor, but 'rollup'
750 749 doesn't want to.
751 750 """
752 751 return False
753 752
754 753 def mergedescs(self):
755 754 """Returns true if the rule should merge messages of multiple changes.
756 755
757 756 This exists mainly so that 'rollup' rules can be a subclass of
758 757 'fold'.
759 758 """
760 759 return True
761 760
762 761 def firstdate(self):
763 762 """Returns true if the rule should preserve the date of the first
764 763 change.
765 764
766 765 This exists mainly so that 'rollup' rules can be a subclass of
767 766 'fold'.
768 767 """
769 768 return False
770 769
771 770 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
772 771 parent = ctx.parents()[0].node()
773 772 repo.ui.pushbuffer()
774 773 hg.update(repo, parent)
775 774 repo.ui.popbuffer()
776 775 ### prepare new commit data
777 776 commitopts = {}
778 777 commitopts['user'] = ctx.user()
779 778 # commit message
780 779 if not self.mergedescs():
781 780 newmessage = ctx.description()
782 781 else:
783 782 newmessage = '\n***\n'.join(
784 783 [ctx.description()] +
785 784 [repo[r].description() for r in internalchanges] +
786 785 [oldctx.description()]) + '\n'
787 786 commitopts['message'] = newmessage
788 787 # date
789 788 if self.firstdate():
790 789 commitopts['date'] = ctx.date()
791 790 else:
792 791 commitopts['date'] = max(ctx.date(), oldctx.date())
793 792 extra = ctx.extra().copy()
794 793 # histedit_source
795 794 # note: ctx is likely a temporary commit but that the best we can do
796 795 # here. This is sufficient to solve issue3681 anyway.
797 796 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
798 797 commitopts['extra'] = extra
799 798 phasemin = max(ctx.phase(), oldctx.phase())
800 799 overrides = {('phases', 'new-commit'): phasemin}
801 800 with repo.ui.configoverride(overrides, 'histedit'):
802 801 n = collapse(repo, ctx, repo[newnode], commitopts,
803 802 skipprompt=self.skipprompt())
804 803 if n is None:
805 804 return ctx, []
806 805 repo.ui.pushbuffer()
807 806 hg.update(repo, n)
808 807 repo.ui.popbuffer()
809 808 replacements = [(oldctx.node(), (newnode,)),
810 809 (ctx.node(), (n,)),
811 810 (newnode, (n,)),
812 811 ]
813 812 for ich in internalchanges:
814 813 replacements.append((ich, (n,)))
815 814 return repo[n], replacements
816 815
817 816 @action(['base', 'b'],
818 817 _('checkout changeset and apply further changesets from there'))
819 818 class base(histeditaction):
820 819
821 820 def run(self):
822 821 if self.repo['.'].node() != self.node:
823 822 mergemod.update(self.repo, self.node, False, True)
824 823 # branchmerge, force)
825 824 return self.continueclean()
826 825
827 826 def continuedirty(self):
828 827 abortdirty()
829 828
830 829 def continueclean(self):
831 830 basectx = self.repo['.']
832 831 return basectx, []
833 832
834 833 def _verifynodeconstraints(self, prev, expected, seen):
835 834 # base can only be use with a node not in the edited set
836 835 if self.node in expected:
837 836 msg = _('%s "%s" changeset was an edited list candidate')
838 837 raise error.ParseError(
839 838 msg % (self.verb, node.short(self.node)),
840 839 hint=_('base must only use unlisted changesets'))
841 840
842 841 @action(['_multifold'],
843 842 _(
844 843 """fold subclass used for when multiple folds happen in a row
845 844
846 845 We only want to fire the editor for the folded message once when
847 846 (say) four changes are folded down into a single change. This is
848 847 similar to rollup, but we should preserve both messages so that
849 848 when the last fold operation runs we can show the user all the
850 849 commit messages in their editor.
851 850 """),
852 851 internal=True)
853 852 class _multifold(fold):
854 853 def skipprompt(self):
855 854 return True
856 855
857 856 @action(["roll", "r"],
858 857 _("like fold, but discard this commit's description and date"))
859 858 class rollup(fold):
860 859 def mergedescs(self):
861 860 return False
862 861
863 862 def skipprompt(self):
864 863 return True
865 864
866 865 def firstdate(self):
867 866 return True
868 867
869 868 @action(["drop", "d"],
870 869 _('remove commit from history'))
871 870 class drop(histeditaction):
872 871 def run(self):
873 872 parentctx = self.repo[self.state.parentctxnode]
874 873 return parentctx, [(self.node, tuple())]
875 874
876 875 @action(["mess", "m"],
877 876 _('edit commit message without changing commit content'),
878 877 priority=True)
879 878 class message(histeditaction):
880 879 def commiteditor(self):
881 880 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
882 881
883 882 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
884 883 """utility function to find the first outgoing changeset
885 884
886 885 Used by initialization code"""
887 886 if opts is None:
888 887 opts = {}
889 888 dest = ui.expandpath(remote or 'default-push', remote or 'default')
890 889 dest, revs = hg.parseurl(dest, None)[:2]
891 890 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
892 891
893 892 revs, checkout = hg.addbranchrevs(repo, repo, revs, None)
894 893 other = hg.peer(repo, opts, dest)
895 894
896 895 if revs:
897 896 revs = [repo.lookup(rev) for rev in revs]
898 897
899 898 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
900 899 if not outgoing.missing:
901 900 raise error.Abort(_('no outgoing ancestors'))
902 901 roots = list(repo.revs("roots(%ln)", outgoing.missing))
903 902 if 1 < len(roots):
904 903 msg = _('there are ambiguous outgoing revisions')
905 904 hint = _("see 'hg help histedit' for more detail")
906 905 raise error.Abort(msg, hint=hint)
907 906 return repo.lookup(roots[0])
908 907
909 908 @command('histedit',
910 909 [('', 'commands', '',
911 910 _('read history edits from the specified file'), _('FILE')),
912 911 ('c', 'continue', False, _('continue an edit already in progress')),
913 912 ('', 'edit-plan', False, _('edit remaining actions list')),
914 913 ('k', 'keep', False,
915 914 _("don't strip old nodes after edit is complete")),
916 915 ('', 'abort', False, _('abort an edit in progress')),
917 916 ('o', 'outgoing', False, _('changesets not found in destination')),
918 917 ('f', 'force', False,
919 918 _('force outgoing even for unrelated repositories')),
920 919 ('r', 'rev', [], _('first revision to be edited'), _('REV'))],
921 920 _("[OPTIONS] ([ANCESTOR] | --outgoing [URL])"))
922 921 def histedit(ui, repo, *freeargs, **opts):
923 922 """interactively edit changeset history
924 923
925 924 This command lets you edit a linear series of changesets (up to
926 925 and including the working directory, which should be clean).
927 926 You can:
928 927
929 928 - `pick` to [re]order a changeset
930 929
931 930 - `drop` to omit changeset
932 931
933 932 - `mess` to reword the changeset commit message
934 933
935 934 - `fold` to combine it with the preceding changeset (using the later date)
936 935
937 936 - `roll` like fold, but discarding this commit's description and date
938 937
939 938 - `edit` to edit this changeset (preserving date)
940 939
941 940 - `base` to checkout changeset and apply further changesets from there
942 941
943 942 There are a number of ways to select the root changeset:
944 943
945 944 - Specify ANCESTOR directly
946 945
947 946 - Use --outgoing -- it will be the first linear changeset not
948 947 included in destination. (See :hg:`help config.paths.default-push`)
949 948
950 949 - Otherwise, the value from the "histedit.defaultrev" config option
951 950 is used as a revset to select the base revision when ANCESTOR is not
952 951 specified. The first revision returned by the revset is used. By
953 952 default, this selects the editable history that is unique to the
954 953 ancestry of the working directory.
955 954
956 955 .. container:: verbose
957 956
958 957 If you use --outgoing, this command will abort if there are ambiguous
959 958 outgoing revisions. For example, if there are multiple branches
960 959 containing outgoing revisions.
961 960
962 961 Use "min(outgoing() and ::.)" or similar revset specification
963 962 instead of --outgoing to specify edit target revision exactly in
964 963 such ambiguous situation. See :hg:`help revsets` for detail about
965 964 selecting revisions.
966 965
967 966 .. container:: verbose
968 967
969 968 Examples:
970 969
971 970 - A number of changes have been made.
972 971 Revision 3 is no longer needed.
973 972
974 973 Start history editing from revision 3::
975 974
976 975 hg histedit -r 3
977 976
978 977 An editor opens, containing the list of revisions,
979 978 with specific actions specified::
980 979
981 980 pick 5339bf82f0ca 3 Zworgle the foobar
982 981 pick 8ef592ce7cc4 4 Bedazzle the zerlog
983 982 pick 0a9639fcda9d 5 Morgify the cromulancy
984 983
985 984 Additional information about the possible actions
986 985 to take appears below the list of revisions.
987 986
988 987 To remove revision 3 from the history,
989 988 its action (at the beginning of the relevant line)
990 989 is changed to 'drop'::
991 990
992 991 drop 5339bf82f0ca 3 Zworgle the foobar
993 992 pick 8ef592ce7cc4 4 Bedazzle the zerlog
994 993 pick 0a9639fcda9d 5 Morgify the cromulancy
995 994
996 995 - A number of changes have been made.
997 996 Revision 2 and 4 need to be swapped.
998 997
999 998 Start history editing from revision 2::
1000 999
1001 1000 hg histedit -r 2
1002 1001
1003 1002 An editor opens, containing the list of revisions,
1004 1003 with specific actions specified::
1005 1004
1006 1005 pick 252a1af424ad 2 Blorb a morgwazzle
1007 1006 pick 5339bf82f0ca 3 Zworgle the foobar
1008 1007 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1009 1008
1010 1009 To swap revision 2 and 4, its lines are swapped
1011 1010 in the editor::
1012 1011
1013 1012 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1014 1013 pick 5339bf82f0ca 3 Zworgle the foobar
1015 1014 pick 252a1af424ad 2 Blorb a morgwazzle
1016 1015
1017 1016 Returns 0 on success, 1 if user intervention is required (not only
1018 1017 for intentional "edit" command, but also for resolving unexpected
1019 1018 conflicts).
1020 1019 """
1021 1020 state = histeditstate(repo)
1022 1021 try:
1023 1022 state.wlock = repo.wlock()
1024 1023 state.lock = repo.lock()
1025 1024 _histedit(ui, repo, state, *freeargs, **opts)
1026 1025 finally:
1027 1026 release(state.lock, state.wlock)
1028 1027
1029 1028 goalcontinue = 'continue'
1030 1029 goalabort = 'abort'
1031 1030 goaleditplan = 'edit-plan'
1032 1031 goalnew = 'new'
1033 1032
1034 1033 def _getgoal(opts):
1035 1034 if opts.get('continue'):
1036 1035 return goalcontinue
1037 1036 if opts.get('abort'):
1038 1037 return goalabort
1039 1038 if opts.get('edit_plan'):
1040 1039 return goaleditplan
1041 1040 return goalnew
1042 1041
1043 1042 def _readfile(ui, path):
1044 1043 if path == '-':
1045 1044 with ui.timeblockedsection('histedit'):
1046 1045 return ui.fin.read()
1047 1046 else:
1048 1047 with open(path, 'rb') as f:
1049 1048 return f.read()
1050 1049
1051 1050 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1052 1051 # TODO only abort if we try to histedit mq patches, not just
1053 1052 # blanket if mq patches are applied somewhere
1054 1053 mq = getattr(repo, 'mq', None)
1055 1054 if mq and mq.applied:
1056 1055 raise error.Abort(_('source has mq patches applied'))
1057 1056
1058 1057 # basic argument incompatibility processing
1059 1058 outg = opts.get('outgoing')
1060 1059 editplan = opts.get('edit_plan')
1061 1060 abort = opts.get('abort')
1062 1061 force = opts.get('force')
1063 1062 if force and not outg:
1064 1063 raise error.Abort(_('--force only allowed with --outgoing'))
1065 1064 if goal == 'continue':
1066 1065 if any((outg, abort, revs, freeargs, rules, editplan)):
1067 1066 raise error.Abort(_('no arguments allowed with --continue'))
1068 1067 elif goal == 'abort':
1069 1068 if any((outg, revs, freeargs, rules, editplan)):
1070 1069 raise error.Abort(_('no arguments allowed with --abort'))
1071 1070 elif goal == 'edit-plan':
1072 1071 if any((outg, revs, freeargs)):
1073 1072 raise error.Abort(_('only --commands argument allowed with '
1074 1073 '--edit-plan'))
1075 1074 else:
1076 1075 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1077 1076 raise error.Abort(_('history edit already in progress, try '
1078 1077 '--continue or --abort'))
1079 1078 if outg:
1080 1079 if revs:
1081 1080 raise error.Abort(_('no revisions allowed with --outgoing'))
1082 1081 if len(freeargs) > 1:
1083 1082 raise error.Abort(
1084 1083 _('only one repo argument allowed with --outgoing'))
1085 1084 else:
1086 1085 revs.extend(freeargs)
1087 1086 if len(revs) == 0:
1088 1087 defaultrev = destutil.desthistedit(ui, repo)
1089 1088 if defaultrev is not None:
1090 1089 revs.append(defaultrev)
1091 1090
1092 1091 if len(revs) != 1:
1093 1092 raise error.Abort(
1094 1093 _('histedit requires exactly one ancestor revision'))
1095 1094
1096 1095 def _histedit(ui, repo, state, *freeargs, **opts):
1097 1096 goal = _getgoal(opts)
1098 1097 revs = opts.get('rev', [])
1099 1098 rules = opts.get('commands', '')
1100 1099 state.keep = opts.get('keep', False)
1101 1100
1102 1101 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1103 1102
1104 1103 # rebuild state
1105 1104 if goal == goalcontinue:
1106 1105 state.read()
1107 1106 state = bootstrapcontinue(ui, state, opts)
1108 1107 elif goal == goaleditplan:
1109 1108 _edithisteditplan(ui, repo, state, rules)
1110 1109 return
1111 1110 elif goal == goalabort:
1112 1111 _aborthistedit(ui, repo, state)
1113 1112 return
1114 1113 else:
1115 1114 # goal == goalnew
1116 1115 _newhistedit(ui, repo, state, revs, freeargs, opts)
1117 1116
1118 1117 _continuehistedit(ui, repo, state)
1119 1118 _finishhistedit(ui, repo, state)
1120 1119
1121 1120 def _continuehistedit(ui, repo, state):
1122 1121 """This function runs after either:
1123 1122 - bootstrapcontinue (if the goal is 'continue')
1124 1123 - _newhistedit (if the goal is 'new')
1125 1124 """
1126 1125 # preprocess rules so that we can hide inner folds from the user
1127 1126 # and only show one editor
1128 1127 actions = state.actions[:]
1129 1128 for idx, (action, nextact) in enumerate(
1130 1129 zip(actions, actions[1:] + [None])):
1131 1130 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1132 1131 state.actions[idx].__class__ = _multifold
1133 1132
1134 1133 # Force an initial state file write, so the user can run --abort/continue
1135 1134 # even if there's an exception before the first transaction serialize.
1136 1135 state.write()
1137 1136
1138 1137 total = len(state.actions)
1139 1138 pos = 0
1140 1139 tr = None
1141 1140 # Don't use singletransaction by default since it rolls the entire
1142 1141 # transaction back if an unexpected exception happens (like a
1143 1142 # pretxncommit hook throws, or the user aborts the commit msg editor).
1144 1143 if ui.configbool("histedit", "singletransaction"):
1145 1144 # Don't use a 'with' for the transaction, since actions may close
1146 1145 # and reopen a transaction. For example, if the action executes an
1147 1146 # external process it may choose to commit the transaction first.
1148 1147 tr = repo.transaction('histedit')
1149 1148 with util.acceptintervention(tr):
1150 1149 while state.actions:
1151 1150 state.write(tr=tr)
1152 1151 actobj = state.actions[0]
1153 1152 pos += 1
1154 1153 ui.progress(_("editing"), pos, actobj.torule(),
1155 1154 _('changes'), total)
1156 1155 ui.debug('histedit: processing %s %s\n' % (actobj.verb,\
1157 1156 actobj.torule()))
1158 1157 parentctx, replacement_ = actobj.run()
1159 1158 state.parentctxnode = parentctx.node()
1160 1159 state.replacements.extend(replacement_)
1161 1160 state.actions.pop(0)
1162 1161
1163 1162 state.write()
1164 1163 ui.progress(_("editing"), None)
1165 1164
1166 1165 def _finishhistedit(ui, repo, state):
1167 1166 """This action runs when histedit is finishing its session"""
1168 1167 repo.ui.pushbuffer()
1169 1168 hg.update(repo, state.parentctxnode, quietempty=True)
1170 1169 repo.ui.popbuffer()
1171 1170
1172 1171 mapping, tmpnodes, created, ntm = processreplacement(state)
1173 1172 if mapping:
1174 1173 for prec, succs in mapping.iteritems():
1175 1174 if not succs:
1176 1175 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1177 1176 else:
1178 1177 ui.debug('histedit: %s is replaced by %s\n' % (
1179 1178 node.short(prec), node.short(succs[0])))
1180 1179 if len(succs) > 1:
1181 1180 m = 'histedit: %s'
1182 1181 for n in succs[1:]:
1183 1182 ui.debug(m % node.short(n))
1184 1183
1185 1184 if not state.keep:
1186 1185 if mapping:
1187 1186 movetopmostbookmarks(repo, state.topmost, ntm)
1188 1187 # TODO update mq state
1189 1188 else:
1190 1189 mapping = {}
1191 1190
1192 1191 for n in tmpnodes:
1193 1192 mapping[n] = ()
1194 1193
1195 1194 # remove entries about unknown nodes
1196 1195 nodemap = repo.unfiltered().changelog.nodemap
1197 1196 mapping = {k: v for k, v in mapping.items()
1198 1197 if k in nodemap and all(n in nodemap for n in v)}
1199 1198 scmutil.cleanupnodes(repo, mapping, 'histedit')
1200 1199
1201 1200 state.clear()
1202 1201 if os.path.exists(repo.sjoin('undo')):
1203 1202 os.unlink(repo.sjoin('undo'))
1204 1203 if repo.vfs.exists('histedit-last-edit.txt'):
1205 1204 repo.vfs.unlink('histedit-last-edit.txt')
1206 1205
1207 1206 def _aborthistedit(ui, repo, state):
1208 1207 try:
1209 1208 state.read()
1210 1209 __, leafs, tmpnodes, __ = processreplacement(state)
1211 1210 ui.debug('restore wc to old parent %s\n'
1212 1211 % node.short(state.topmost))
1213 1212
1214 1213 # Recover our old commits if necessary
1215 1214 if not state.topmost in repo and state.backupfile:
1216 1215 backupfile = repo.vfs.join(state.backupfile)
1217 1216 f = hg.openpath(ui, backupfile)
1218 1217 gen = exchange.readbundle(ui, f, backupfile)
1219 1218 with repo.transaction('histedit.abort') as tr:
1220 1219 bundle2.applybundle(repo, gen, tr, source='histedit',
1221 1220 url='bundle:' + backupfile)
1222 1221
1223 1222 os.remove(backupfile)
1224 1223
1225 1224 # check whether we should update away
1226 1225 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1227 1226 state.parentctxnode, leafs | tmpnodes):
1228 1227 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1229 1228 cleanupnode(ui, repo, tmpnodes)
1230 1229 cleanupnode(ui, repo, leafs)
1231 1230 except Exception:
1232 1231 if state.inprogress():
1233 1232 ui.warn(_('warning: encountered an exception during histedit '
1234 1233 '--abort; the repository may not have been completely '
1235 1234 'cleaned up\n'))
1236 1235 raise
1237 1236 finally:
1238 1237 state.clear()
1239 1238
1240 1239 def _edithisteditplan(ui, repo, state, rules):
1241 1240 state.read()
1242 1241 if not rules:
1243 1242 comment = geteditcomment(ui,
1244 1243 node.short(state.parentctxnode),
1245 1244 node.short(state.topmost))
1246 1245 rules = ruleeditor(repo, ui, state.actions, comment)
1247 1246 else:
1248 1247 rules = _readfile(ui, rules)
1249 1248 actions = parserules(rules, state)
1250 1249 ctxs = [repo[act.node] \
1251 1250 for act in state.actions if act.node]
1252 1251 warnverifyactions(ui, repo, actions, state, ctxs)
1253 1252 state.actions = actions
1254 1253 state.write()
1255 1254
1256 1255 def _newhistedit(ui, repo, state, revs, freeargs, opts):
1257 1256 outg = opts.get('outgoing')
1258 1257 rules = opts.get('commands', '')
1259 1258 force = opts.get('force')
1260 1259
1261 1260 cmdutil.checkunfinished(repo)
1262 1261 cmdutil.bailifchanged(repo)
1263 1262
1264 1263 topmost, empty = repo.dirstate.parents()
1265 1264 if outg:
1266 1265 if freeargs:
1267 1266 remote = freeargs[0]
1268 1267 else:
1269 1268 remote = None
1270 1269 root = findoutgoing(ui, repo, remote, force, opts)
1271 1270 else:
1272 1271 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1273 1272 if len(rr) != 1:
1274 1273 raise error.Abort(_('The specified revisions must have '
1275 1274 'exactly one common root'))
1276 1275 root = rr[0].node()
1277 1276
1278 1277 revs = between(repo, root, topmost, state.keep)
1279 1278 if not revs:
1280 1279 raise error.Abort(_('%s is not an ancestor of working directory') %
1281 1280 node.short(root))
1282 1281
1283 1282 ctxs = [repo[r] for r in revs]
1284 1283 if not rules:
1285 1284 comment = geteditcomment(ui, node.short(root), node.short(topmost))
1286 1285 actions = [pick(state, r) for r in revs]
1287 1286 rules = ruleeditor(repo, ui, actions, comment)
1288 1287 else:
1289 1288 rules = _readfile(ui, rules)
1290 1289 actions = parserules(rules, state)
1291 1290 warnverifyactions(ui, repo, actions, state, ctxs)
1292 1291
1293 1292 parentctxnode = repo[root].parents()[0].node()
1294 1293
1295 1294 state.parentctxnode = parentctxnode
1296 1295 state.actions = actions
1297 1296 state.topmost = topmost
1298 1297 state.replacements = []
1299 1298
1300 1299 # Create a backup so we can always abort completely.
1301 1300 backupfile = None
1302 1301 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1303 1302 backupfile = repair._bundle(repo, [parentctxnode], [topmost], root,
1304 1303 'histedit')
1305 1304 state.backupfile = backupfile
1306 1305
1307 1306 def _getsummary(ctx):
1308 1307 # a common pattern is to extract the summary but default to the empty
1309 1308 # string
1310 1309 summary = ctx.description() or ''
1311 1310 if summary:
1312 1311 summary = summary.splitlines()[0]
1313 1312 return summary
1314 1313
1315 1314 def bootstrapcontinue(ui, state, opts):
1316 1315 repo = state.repo
1317 1316
1318 1317 ms = mergemod.mergestate.read(repo)
1319 1318 mergeutil.checkunresolved(ms)
1320 1319
1321 1320 if state.actions:
1322 1321 actobj = state.actions.pop(0)
1323 1322
1324 1323 if _isdirtywc(repo):
1325 1324 actobj.continuedirty()
1326 1325 if _isdirtywc(repo):
1327 1326 abortdirty()
1328 1327
1329 1328 parentctx, replacements = actobj.continueclean()
1330 1329
1331 1330 state.parentctxnode = parentctx.node()
1332 1331 state.replacements.extend(replacements)
1333 1332
1334 1333 return state
1335 1334
1336 1335 def between(repo, old, new, keep):
1337 1336 """select and validate the set of revision to edit
1338 1337
1339 1338 When keep is false, the specified set can't have children."""
1340 1339 ctxs = list(repo.set('%n::%n', old, new))
1341 1340 if ctxs and not keep:
1342 1341 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1343 1342 repo.revs('(%ld::) - (%ld)', ctxs, ctxs)):
1344 1343 raise error.Abort(_('can only histedit a changeset together '
1345 1344 'with all its descendants'))
1346 1345 if repo.revs('(%ld) and merge()', ctxs):
1347 1346 raise error.Abort(_('cannot edit history that contains merges'))
1348 1347 root = ctxs[0] # list is already sorted by repo.set
1349 1348 if not root.mutable():
1350 1349 raise error.Abort(_('cannot edit public changeset: %s') % root,
1351 1350 hint=_("see 'hg help phases' for details"))
1352 1351 return [c.node() for c in ctxs]
1353 1352
1354 1353 def ruleeditor(repo, ui, actions, editcomment=""):
1355 1354 """open an editor to edit rules
1356 1355
1357 1356 rules are in the format [ [act, ctx], ...] like in state.rules
1358 1357 """
1359 1358 if repo.ui.configbool("experimental", "histedit.autoverb"):
1360 1359 newact = util.sortdict()
1361 1360 for act in actions:
1362 1361 ctx = repo[act.node]
1363 1362 summary = _getsummary(ctx)
1364 1363 fword = summary.split(' ', 1)[0].lower()
1365 1364 added = False
1366 1365
1367 1366 # if it doesn't end with the special character '!' just skip this
1368 1367 if fword.endswith('!'):
1369 1368 fword = fword[:-1]
1370 1369 if fword in primaryactions | secondaryactions | tertiaryactions:
1371 1370 act.verb = fword
1372 1371 # get the target summary
1373 1372 tsum = summary[len(fword) + 1:].lstrip()
1374 1373 # safe but slow: reverse iterate over the actions so we
1375 1374 # don't clash on two commits having the same summary
1376 1375 for na, l in reversed(list(newact.iteritems())):
1377 1376 actx = repo[na.node]
1378 1377 asum = _getsummary(actx)
1379 1378 if asum == tsum:
1380 1379 added = True
1381 1380 l.append(act)
1382 1381 break
1383 1382
1384 1383 if not added:
1385 1384 newact[act] = []
1386 1385
1387 1386 # copy over and flatten the new list
1388 1387 actions = []
1389 1388 for na, l in newact.iteritems():
1390 1389 actions.append(na)
1391 1390 actions += l
1392 1391
1393 1392 rules = '\n'.join([act.torule() for act in actions])
1394 1393 rules += '\n\n'
1395 1394 rules += editcomment
1396 1395 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'},
1397 1396 repopath=repo.path, action='histedit')
1398 1397
1399 1398 # Save edit rules in .hg/histedit-last-edit.txt in case
1400 1399 # the user needs to ask for help after something
1401 1400 # surprising happens.
1402 1401 f = open(repo.vfs.join('histedit-last-edit.txt'), 'w')
1403 1402 f.write(rules)
1404 1403 f.close()
1405 1404
1406 1405 return rules
1407 1406
1408 1407 def parserules(rules, state):
1409 1408 """Read the histedit rules string and return list of action objects """
1410 1409 rules = [l for l in (r.strip() for r in rules.splitlines())
1411 1410 if l and not l.startswith('#')]
1412 1411 actions = []
1413 1412 for r in rules:
1414 1413 if ' ' not in r:
1415 1414 raise error.ParseError(_('malformed line "%s"') % r)
1416 1415 verb, rest = r.split(' ', 1)
1417 1416
1418 1417 if verb not in actiontable:
1419 1418 raise error.ParseError(_('unknown action "%s"') % verb)
1420 1419
1421 1420 action = actiontable[verb].fromrule(state, rest)
1422 1421 actions.append(action)
1423 1422 return actions
1424 1423
1425 1424 def warnverifyactions(ui, repo, actions, state, ctxs):
1426 1425 try:
1427 1426 verifyactions(actions, state, ctxs)
1428 1427 except error.ParseError:
1429 1428 if repo.vfs.exists('histedit-last-edit.txt'):
1430 1429 ui.warn(_('warning: histedit rules saved '
1431 1430 'to: .hg/histedit-last-edit.txt\n'))
1432 1431 raise
1433 1432
1434 1433 def verifyactions(actions, state, ctxs):
1435 1434 """Verify that there exists exactly one action per given changeset and
1436 1435 other constraints.
1437 1436
1438 1437 Will abort if there are to many or too few rules, a malformed rule,
1439 1438 or a rule on a changeset outside of the user-given range.
1440 1439 """
1441 1440 expected = set(c.node() for c in ctxs)
1442 1441 seen = set()
1443 1442 prev = None
1444 1443
1445 1444 if actions and actions[0].verb in ['roll', 'fold']:
1446 1445 raise error.ParseError(_('first changeset cannot use verb "%s"') %
1447 1446 actions[0].verb)
1448 1447
1449 1448 for action in actions:
1450 1449 action.verify(prev, expected, seen)
1451 1450 prev = action
1452 1451 if action.node is not None:
1453 1452 seen.add(action.node)
1454 1453 missing = sorted(expected - seen) # sort to stabilize output
1455 1454
1456 1455 if state.repo.ui.configbool('histedit', 'dropmissing'):
1457 1456 if len(actions) == 0:
1458 1457 raise error.ParseError(_('no rules provided'),
1459 1458 hint=_('use strip extension to remove commits'))
1460 1459
1461 1460 drops = [drop(state, n) for n in missing]
1462 1461 # put the in the beginning so they execute immediately and
1463 1462 # don't show in the edit-plan in the future
1464 1463 actions[:0] = drops
1465 1464 elif missing:
1466 1465 raise error.ParseError(_('missing rules for changeset %s') %
1467 1466 node.short(missing[0]),
1468 1467 hint=_('use "drop %s" to discard, see also: '
1469 1468 "'hg help -e histedit.config'")
1470 1469 % node.short(missing[0]))
1471 1470
1472 1471 def adjustreplacementsfrommarkers(repo, oldreplacements):
1473 1472 """Adjust replacements from obsolescence markers
1474 1473
1475 1474 Replacements structure is originally generated based on
1476 1475 histedit's state and does not account for changes that are
1477 1476 not recorded there. This function fixes that by adding
1478 1477 data read from obsolescence markers"""
1479 1478 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1480 1479 return oldreplacements
1481 1480
1482 1481 unfi = repo.unfiltered()
1483 1482 nm = unfi.changelog.nodemap
1484 1483 obsstore = repo.obsstore
1485 1484 newreplacements = list(oldreplacements)
1486 1485 oldsuccs = [r[1] for r in oldreplacements]
1487 1486 # successors that have already been added to succstocheck once
1488 1487 seensuccs = set().union(*oldsuccs) # create a set from an iterable of tuples
1489 1488 succstocheck = list(seensuccs)
1490 1489 while succstocheck:
1491 1490 n = succstocheck.pop()
1492 1491 missing = nm.get(n) is None
1493 1492 markers = obsstore.successors.get(n, ())
1494 1493 if missing and not markers:
1495 1494 # dead end, mark it as such
1496 1495 newreplacements.append((n, ()))
1497 1496 for marker in markers:
1498 1497 nsuccs = marker[1]
1499 1498 newreplacements.append((n, nsuccs))
1500 1499 for nsucc in nsuccs:
1501 1500 if nsucc not in seensuccs:
1502 1501 seensuccs.add(nsucc)
1503 1502 succstocheck.append(nsucc)
1504 1503
1505 1504 return newreplacements
1506 1505
1507 1506 def processreplacement(state):
1508 1507 """process the list of replacements to return
1509 1508
1510 1509 1) the final mapping between original and created nodes
1511 1510 2) the list of temporary node created by histedit
1512 1511 3) the list of new commit created by histedit"""
1513 1512 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
1514 1513 allsuccs = set()
1515 1514 replaced = set()
1516 1515 fullmapping = {}
1517 1516 # initialize basic set
1518 1517 # fullmapping records all operations recorded in replacement
1519 1518 for rep in replacements:
1520 1519 allsuccs.update(rep[1])
1521 1520 replaced.add(rep[0])
1522 1521 fullmapping.setdefault(rep[0], set()).update(rep[1])
1523 1522 new = allsuccs - replaced
1524 1523 tmpnodes = allsuccs & replaced
1525 1524 # Reduce content fullmapping into direct relation between original nodes
1526 1525 # and final node created during history edition
1527 1526 # Dropped changeset are replaced by an empty list
1528 1527 toproceed = set(fullmapping)
1529 1528 final = {}
1530 1529 while toproceed:
1531 1530 for x in list(toproceed):
1532 1531 succs = fullmapping[x]
1533 1532 for s in list(succs):
1534 1533 if s in toproceed:
1535 1534 # non final node with unknown closure
1536 1535 # We can't process this now
1537 1536 break
1538 1537 elif s in final:
1539 1538 # non final node, replace with closure
1540 1539 succs.remove(s)
1541 1540 succs.update(final[s])
1542 1541 else:
1543 1542 final[x] = succs
1544 1543 toproceed.remove(x)
1545 1544 # remove tmpnodes from final mapping
1546 1545 for n in tmpnodes:
1547 1546 del final[n]
1548 1547 # we expect all changes involved in final to exist in the repo
1549 1548 # turn `final` into list (topologically sorted)
1550 1549 nm = state.repo.changelog.nodemap
1551 1550 for prec, succs in final.items():
1552 1551 final[prec] = sorted(succs, key=nm.get)
1553 1552
1554 1553 # computed topmost element (necessary for bookmark)
1555 1554 if new:
1556 1555 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1557 1556 elif not final:
1558 1557 # Nothing rewritten at all. we won't need `newtopmost`
1559 1558 # It is the same as `oldtopmost` and `processreplacement` know it
1560 1559 newtopmost = None
1561 1560 else:
1562 1561 # every body died. The newtopmost is the parent of the root.
1563 1562 r = state.repo.changelog.rev
1564 1563 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1565 1564
1566 1565 return final, tmpnodes, new, newtopmost
1567 1566
1568 1567 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
1569 1568 """Move bookmark from oldtopmost to newly created topmost
1570 1569
1571 1570 This is arguably a feature and we may only want that for the active
1572 1571 bookmark. But the behavior is kept compatible with the old version for now.
1573 1572 """
1574 1573 if not oldtopmost or not newtopmost:
1575 1574 return
1576 1575 oldbmarks = repo.nodebookmarks(oldtopmost)
1577 1576 if oldbmarks:
1578 1577 with repo.lock(), repo.transaction('histedit') as tr:
1579 1578 marks = repo._bookmarks
1580 1579 changes = []
1581 1580 for name in oldbmarks:
1582 1581 changes.append((name, newtopmost))
1583 1582 marks.applychanges(repo, tr, changes)
1584 1583
1585 1584 def cleanupnode(ui, repo, nodes):
1586 1585 """strip a group of nodes from the repository
1587 1586
1588 1587 The set of node to strip may contains unknown nodes."""
1589 1588 with repo.lock():
1590 1589 # do not let filtering get in the way of the cleanse
1591 1590 # we should probably get rid of obsolescence marker created during the
1592 1591 # histedit, but we currently do not have such information.
1593 1592 repo = repo.unfiltered()
1594 1593 # Find all nodes that need to be stripped
1595 1594 # (we use %lr instead of %ln to silently ignore unknown items)
1596 1595 nm = repo.changelog.nodemap
1597 1596 nodes = sorted(n for n in nodes if n in nm)
1598 1597 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1599 1598 if roots:
1600 1599 repair.strip(ui, repo, roots)
1601 1600
1602 1601 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1603 1602 if isinstance(nodelist, str):
1604 1603 nodelist = [nodelist]
1605 1604 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1606 1605 state = histeditstate(repo)
1607 1606 state.read()
1608 1607 histedit_nodes = {action.node for action
1609 1608 in state.actions if action.node}
1610 1609 common_nodes = histedit_nodes & set(nodelist)
1611 1610 if common_nodes:
1612 1611 raise error.Abort(_("histedit in progress, can't strip %s")
1613 1612 % ', '.join(node.short(x) for x in common_nodes))
1614 1613 return orig(ui, repo, nodelist, *args, **kwargs)
1615 1614
1616 1615 extensions.wrapfunction(repair, 'strip', stripwrapper)
1617 1616
1618 1617 def summaryhook(ui, repo):
1619 1618 if not os.path.exists(repo.vfs.join('histedit-state')):
1620 1619 return
1621 1620 state = histeditstate(repo)
1622 1621 state.read()
1623 1622 if state.actions:
1624 1623 # i18n: column positioning for "hg summary"
1625 1624 ui.write(_('hist: %s (histedit --continue)\n') %
1626 1625 (ui.label(_('%d remaining'), 'histedit.remaining') %
1627 1626 len(state.actions)))
1628 1627
1629 1628 def extsetup(ui):
1630 1629 cmdutil.summaryhooks.add('histedit', summaryhook)
1631 1630 cmdutil.unfinishedstates.append(
1632 1631 ['histedit-state', False, True, _('histedit in progress'),
1633 1632 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
1634 1633 cmdutil.afterresolvedstates.append(
1635 1634 ['histedit-state', _('hg histedit --continue')])
@@ -1,155 +1,154 b''
1 1 # Copyright 2009-2010 Gregory P. Ward
2 2 # Copyright 2009-2010 Intelerad Medical Systems Incorporated
3 3 # Copyright 2010-2011 Fog Creek Software
4 4 # Copyright 2010-2011 Unity Technologies
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 '''track large binary files
10 10
11 11 Large binary files tend to be not very compressible, not very
12 12 diffable, and not at all mergeable. Such files are not handled
13 13 efficiently by Mercurial's storage format (revlog), which is based on
14 14 compressed binary deltas; storing large binary files as regular
15 15 Mercurial files wastes bandwidth and disk space and increases
16 16 Mercurial's memory usage. The largefiles extension addresses these
17 17 problems by adding a centralized client-server layer on top of
18 18 Mercurial: largefiles live in a *central store* out on the network
19 19 somewhere, and you only fetch the revisions that you need when you
20 20 need them.
21 21
22 22 largefiles works by maintaining a "standin file" in .hglf/ for each
23 23 largefile. The standins are small (41 bytes: an SHA-1 hash plus
24 24 newline) and are tracked by Mercurial. Largefile revisions are
25 25 identified by the SHA-1 hash of their contents, which is written to
26 26 the standin. largefiles uses that revision ID to get/put largefile
27 27 revisions from/to the central store. This saves both disk space and
28 28 bandwidth, since you don't need to retrieve all historical revisions
29 29 of large files when you clone or pull.
30 30
31 31 To start a new repository or add new large binary files, just add
32 32 --large to your :hg:`add` command. For example::
33 33
34 34 $ dd if=/dev/urandom of=randomdata count=2000
35 35 $ hg add --large randomdata
36 36 $ hg commit -m "add randomdata as a largefile"
37 37
38 38 When you push a changeset that adds/modifies largefiles to a remote
39 39 repository, its largefile revisions will be uploaded along with it.
40 40 Note that the remote Mercurial must also have the largefiles extension
41 41 enabled for this to work.
42 42
43 43 When you pull a changeset that affects largefiles from a remote
44 44 repository, the largefiles for the changeset will by default not be
45 45 pulled down. However, when you update to such a revision, any
46 46 largefiles needed by that revision are downloaded and cached (if
47 47 they have never been downloaded before). One way to pull largefiles
48 48 when pulling is thus to use --update, which will update your working
49 49 copy to the latest pulled revision (and thereby downloading any new
50 50 largefiles).
51 51
52 52 If you want to pull largefiles you don't need for update yet, then
53 53 you can use pull with the `--lfrev` option or the :hg:`lfpull` command.
54 54
55 55 If you know you are pulling from a non-default location and want to
56 56 download all the largefiles that correspond to the new changesets at
57 57 the same time, then you can pull with `--lfrev "pulled()"`.
58 58
59 59 If you just want to ensure that you will have the largefiles needed to
60 60 merge or rebase with new heads that you are pulling, then you can pull
61 61 with `--lfrev "head(pulled())"` flag to pre-emptively download any largefiles
62 62 that are new in the heads you are pulling.
63 63
64 64 Keep in mind that network access may now be required to update to
65 65 changesets that you have not previously updated to. The nature of the
66 66 largefiles extension means that updating is no longer guaranteed to
67 67 be a local-only operation.
68 68
69 69 If you already have large files tracked by Mercurial without the
70 70 largefiles extension, you will need to convert your repository in
71 71 order to benefit from largefiles. This is done with the
72 72 :hg:`lfconvert` command::
73 73
74 74 $ hg lfconvert --size 10 oldrepo newrepo
75 75
76 76 In repositories that already have largefiles in them, any new file
77 77 over 10MB will automatically be added as a largefile. To change this
78 78 threshold, set ``largefiles.minsize`` in your Mercurial config file
79 79 to the minimum size in megabytes to track as a largefile, or use the
80 80 --lfsize option to the add command (also in megabytes)::
81 81
82 82 [largefiles]
83 83 minsize = 2
84 84
85 85 $ hg add --lfsize 2
86 86
87 87 The ``largefiles.patterns`` config option allows you to specify a list
88 88 of filename patterns (see :hg:`help patterns`) that should always be
89 89 tracked as largefiles::
90 90
91 91 [largefiles]
92 92 patterns =
93 93 *.jpg
94 94 re:.*\\.(png|bmp)$
95 95 library.zip
96 96 content/audio/*
97 97
98 98 Files that match one of these patterns will be added as largefiles
99 99 regardless of their size.
100 100
101 101 The ``largefiles.minsize`` and ``largefiles.patterns`` config options
102 102 will be ignored for any repositories not already containing a
103 103 largefile. To add the first largefile to a repository, you must
104 104 explicitly do so with the --large flag passed to the :hg:`add`
105 105 command.
106 106 '''
107 107 from __future__ import absolute_import
108 108
109 109 from mercurial import (
110 configitems,
111 110 hg,
112 111 localrepo,
113 112 registrar,
114 113 )
115 114
116 115 from . import (
117 116 lfcommands,
118 117 overrides,
119 118 proto,
120 119 reposetup,
121 120 uisetup as uisetupmod,
122 121 )
123 122
124 123 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
125 124 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
126 125 # be specifying the version(s) of Mercurial they are tested with, or
127 126 # leave the attribute unspecified.
128 127 testedwith = 'ships-with-hg-core'
129 128
130 129 configtable = {}
131 130 configitem = registrar.configitem(configtable)
132 131
133 132 configitem('largefiles', 'minsize',
134 default=configitems.dynamicdefault,
133 default=configitem.dynamicdefault,
135 134 )
136 135 configitem('largefiles', 'patterns',
137 136 default=list,
138 137 )
139 138 configitem('largefiles', 'usercache',
140 139 default=None,
141 140 )
142 141
143 142 reposetup = reposetup.reposetup
144 143
145 144 def featuresetup(ui, supported):
146 145 # don't die on seeing a repo with the largefiles requirement
147 146 supported |= {'largefiles'}
148 147
149 148 def uisetup(ui):
150 149 localrepo.localrepository.featuresetupfuncs.add(featuresetup)
151 150 hg.wirepeersetupfuncs.append(proto.wirereposetup)
152 151 uisetupmod.uisetup(ui)
153 152
154 153 cmdtable = lfcommands.cmdtable
155 154 revsetpredicate = overrides.revsetpredicate
@@ -1,1140 +1,1143 b''
1 1 # configitems.py - centralized declaration of configuration option
2 2 #
3 3 # Copyright 2017 Pierre-Yves David <pierre-yves.david@octobus.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 from __future__ import absolute_import
9 9
10 10 import functools
11 11 import re
12 12
13 13 from . import (
14 14 encoding,
15 15 error,
16 16 )
17 17
18 18 def loadconfigtable(ui, extname, configtable):
19 19 """update config item known to the ui with the extension ones"""
20 20 for section, items in configtable.items():
21 21 knownitems = ui._knownconfig.setdefault(section, itemregister())
22 22 knownkeys = set(knownitems)
23 23 newkeys = set(items)
24 24 for key in sorted(knownkeys & newkeys):
25 25 msg = "extension '%s' overwrite config item '%s.%s'"
26 26 msg %= (extname, section, key)
27 27 ui.develwarn(msg, config='warn-config')
28 28
29 29 knownitems.update(items)
30 30
31 31 class configitem(object):
32 32 """represent a known config item
33 33
34 34 :section: the official config section where to find this item,
35 35 :name: the official name within the section,
36 36 :default: default value for this item,
37 37 :alias: optional list of tuples as alternatives,
38 38 :generic: this is a generic definition, match name using regular expression.
39 39 """
40 40
41 41 def __init__(self, section, name, default=None, alias=(),
42 42 generic=False, priority=0):
43 43 self.section = section
44 44 self.name = name
45 45 self.default = default
46 46 self.alias = list(alias)
47 47 self.generic = generic
48 48 self.priority = priority
49 49 self._re = None
50 50 if generic:
51 51 self._re = re.compile(self.name)
52 52
53 53 class itemregister(dict):
54 54 """A specialized dictionary that can handle wild-card selection"""
55 55
56 56 def __init__(self):
57 57 super(itemregister, self).__init__()
58 58 self._generics = set()
59 59
60 60 def update(self, other):
61 61 super(itemregister, self).update(other)
62 62 self._generics.update(other._generics)
63 63
64 64 def __setitem__(self, key, item):
65 65 super(itemregister, self).__setitem__(key, item)
66 66 if item.generic:
67 67 self._generics.add(item)
68 68
69 69 def get(self, key):
70 70 baseitem = super(itemregister, self).get(key)
71 71 if baseitem is not None and not baseitem.generic:
72 72 return baseitem
73 73
74 74 # search for a matching generic item
75 75 generics = sorted(self._generics, key=(lambda x: (x.priority, x.name)))
76 76 for item in generics:
77 77 # we use 'match' instead of 'search' to make the matching simpler
78 78 # for people unfamiliar with regular expression. Having the match
79 79 # rooted to the start of the string will produce less surprising
80 80 # result for user writing simple regex for sub-attribute.
81 81 #
82 82 # For example using "color\..*" match produces an unsurprising
83 83 # result, while using search could suddenly match apparently
84 84 # unrelated configuration that happens to contains "color."
85 85 # anywhere. This is a tradeoff where we favor requiring ".*" on
86 86 # some match to avoid the need to prefix most pattern with "^".
87 87 # The "^" seems more error prone.
88 88 if item._re.match(key):
89 89 return item
90 90
91 91 return None
92 92
93 93 coreitems = {}
94 94
95 95 def _register(configtable, *args, **kwargs):
96 96 item = configitem(*args, **kwargs)
97 97 section = configtable.setdefault(item.section, itemregister())
98 98 if item.name in section:
99 99 msg = "duplicated config item registration for '%s.%s'"
100 100 raise error.ProgrammingError(msg % (item.section, item.name))
101 101 section[item.name] = item
102 102
103 103 # special value for case where the default is derived from other values
104 104 dynamicdefault = object()
105 105
106 106 # Registering actual config items
107 107
108 108 def getitemregister(configtable):
109 return functools.partial(_register, configtable)
109 f = functools.partial(_register, configtable)
110 # export pseudo enum as configitem.*
111 f.dynamicdefault = dynamicdefault
112 return f
110 113
111 114 coreconfigitem = getitemregister(coreitems)
112 115
113 116 coreconfigitem('alias', '.*',
114 117 default=None,
115 118 generic=True,
116 119 )
117 120 coreconfigitem('annotate', 'nodates',
118 121 default=False,
119 122 )
120 123 coreconfigitem('annotate', 'showfunc',
121 124 default=False,
122 125 )
123 126 coreconfigitem('annotate', 'unified',
124 127 default=None,
125 128 )
126 129 coreconfigitem('annotate', 'git',
127 130 default=False,
128 131 )
129 132 coreconfigitem('annotate', 'ignorews',
130 133 default=False,
131 134 )
132 135 coreconfigitem('annotate', 'ignorewsamount',
133 136 default=False,
134 137 )
135 138 coreconfigitem('annotate', 'ignoreblanklines',
136 139 default=False,
137 140 )
138 141 coreconfigitem('annotate', 'ignorewseol',
139 142 default=False,
140 143 )
141 144 coreconfigitem('annotate', 'nobinary',
142 145 default=False,
143 146 )
144 147 coreconfigitem('annotate', 'noprefix',
145 148 default=False,
146 149 )
147 150 coreconfigitem('auth', 'cookiefile',
148 151 default=None,
149 152 )
150 153 # bookmarks.pushing: internal hack for discovery
151 154 coreconfigitem('bookmarks', 'pushing',
152 155 default=list,
153 156 )
154 157 # bundle.mainreporoot: internal hack for bundlerepo
155 158 coreconfigitem('bundle', 'mainreporoot',
156 159 default='',
157 160 )
158 161 # bundle.reorder: experimental config
159 162 coreconfigitem('bundle', 'reorder',
160 163 default='auto',
161 164 )
162 165 coreconfigitem('censor', 'policy',
163 166 default='abort',
164 167 )
165 168 coreconfigitem('chgserver', 'idletimeout',
166 169 default=3600,
167 170 )
168 171 coreconfigitem('chgserver', 'skiphash',
169 172 default=False,
170 173 )
171 174 coreconfigitem('cmdserver', 'log',
172 175 default=None,
173 176 )
174 177 coreconfigitem('color', '.*',
175 178 default=None,
176 179 generic=True,
177 180 )
178 181 coreconfigitem('color', 'mode',
179 182 default='auto',
180 183 )
181 184 coreconfigitem('color', 'pagermode',
182 185 default=dynamicdefault,
183 186 )
184 187 coreconfigitem('commands', 'show.aliasprefix',
185 188 default=list,
186 189 )
187 190 coreconfigitem('commands', 'status.relative',
188 191 default=False,
189 192 )
190 193 coreconfigitem('commands', 'status.skipstates',
191 194 default=[],
192 195 )
193 196 coreconfigitem('commands', 'status.verbose',
194 197 default=False,
195 198 )
196 199 coreconfigitem('commands', 'update.check',
197 200 default=None,
198 201 # Deprecated, remove after 4.4 release
199 202 alias=[('experimental', 'updatecheck')]
200 203 )
201 204 coreconfigitem('commands', 'update.requiredest',
202 205 default=False,
203 206 )
204 207 coreconfigitem('committemplate', '.*',
205 208 default=None,
206 209 generic=True,
207 210 )
208 211 coreconfigitem('debug', 'dirstate.delaywrite',
209 212 default=0,
210 213 )
211 214 coreconfigitem('defaults', '.*',
212 215 default=None,
213 216 generic=True,
214 217 )
215 218 coreconfigitem('devel', 'all-warnings',
216 219 default=False,
217 220 )
218 221 coreconfigitem('devel', 'bundle2.debug',
219 222 default=False,
220 223 )
221 224 coreconfigitem('devel', 'cache-vfs',
222 225 default=None,
223 226 )
224 227 coreconfigitem('devel', 'check-locks',
225 228 default=False,
226 229 )
227 230 coreconfigitem('devel', 'check-relroot',
228 231 default=False,
229 232 )
230 233 coreconfigitem('devel', 'default-date',
231 234 default=None,
232 235 )
233 236 coreconfigitem('devel', 'deprec-warn',
234 237 default=False,
235 238 )
236 239 coreconfigitem('devel', 'disableloaddefaultcerts',
237 240 default=False,
238 241 )
239 242 coreconfigitem('devel', 'warn-empty-changegroup',
240 243 default=False,
241 244 )
242 245 coreconfigitem('devel', 'legacy.exchange',
243 246 default=list,
244 247 )
245 248 coreconfigitem('devel', 'servercafile',
246 249 default='',
247 250 )
248 251 coreconfigitem('devel', 'serverexactprotocol',
249 252 default='',
250 253 )
251 254 coreconfigitem('devel', 'serverrequirecert',
252 255 default=False,
253 256 )
254 257 coreconfigitem('devel', 'strip-obsmarkers',
255 258 default=True,
256 259 )
257 260 coreconfigitem('devel', 'warn-config',
258 261 default=None,
259 262 )
260 263 coreconfigitem('devel', 'warn-config-default',
261 264 default=None,
262 265 )
263 266 coreconfigitem('devel', 'user.obsmarker',
264 267 default=None,
265 268 )
266 269 coreconfigitem('devel', 'warn-config-unknown',
267 270 default=None,
268 271 )
269 272 coreconfigitem('diff', 'nodates',
270 273 default=False,
271 274 )
272 275 coreconfigitem('diff', 'showfunc',
273 276 default=False,
274 277 )
275 278 coreconfigitem('diff', 'unified',
276 279 default=None,
277 280 )
278 281 coreconfigitem('diff', 'git',
279 282 default=False,
280 283 )
281 284 coreconfigitem('diff', 'ignorews',
282 285 default=False,
283 286 )
284 287 coreconfigitem('diff', 'ignorewsamount',
285 288 default=False,
286 289 )
287 290 coreconfigitem('diff', 'ignoreblanklines',
288 291 default=False,
289 292 )
290 293 coreconfigitem('diff', 'ignorewseol',
291 294 default=False,
292 295 )
293 296 coreconfigitem('diff', 'nobinary',
294 297 default=False,
295 298 )
296 299 coreconfigitem('diff', 'noprefix',
297 300 default=False,
298 301 )
299 302 coreconfigitem('email', 'bcc',
300 303 default=None,
301 304 )
302 305 coreconfigitem('email', 'cc',
303 306 default=None,
304 307 )
305 308 coreconfigitem('email', 'charsets',
306 309 default=list,
307 310 )
308 311 coreconfigitem('email', 'from',
309 312 default=None,
310 313 )
311 314 coreconfigitem('email', 'method',
312 315 default='smtp',
313 316 )
314 317 coreconfigitem('email', 'reply-to',
315 318 default=None,
316 319 )
317 320 coreconfigitem('email', 'to',
318 321 default=None,
319 322 )
320 323 coreconfigitem('experimental', 'archivemetatemplate',
321 324 default=dynamicdefault,
322 325 )
323 326 coreconfigitem('experimental', 'bundle-phases',
324 327 default=False,
325 328 )
326 329 coreconfigitem('experimental', 'bundle2-advertise',
327 330 default=True,
328 331 )
329 332 coreconfigitem('experimental', 'bundle2-output-capture',
330 333 default=False,
331 334 )
332 335 coreconfigitem('experimental', 'bundle2.pushback',
333 336 default=False,
334 337 )
335 338 coreconfigitem('experimental', 'bundle2lazylocking',
336 339 default=False,
337 340 )
338 341 coreconfigitem('experimental', 'bundlecomplevel',
339 342 default=None,
340 343 )
341 344 coreconfigitem('experimental', 'changegroup3',
342 345 default=False,
343 346 )
344 347 coreconfigitem('experimental', 'clientcompressionengines',
345 348 default=list,
346 349 )
347 350 coreconfigitem('experimental', 'copytrace',
348 351 default='on',
349 352 )
350 353 coreconfigitem('experimental', 'copytrace.movecandidateslimit',
351 354 default=100,
352 355 )
353 356 coreconfigitem('experimental', 'copytrace.sourcecommitlimit',
354 357 default=100,
355 358 )
356 359 coreconfigitem('experimental', 'crecordtest',
357 360 default=None,
358 361 )
359 362 coreconfigitem('experimental', 'editortmpinhg',
360 363 default=False,
361 364 )
362 365 coreconfigitem('experimental', 'evolution',
363 366 default=list,
364 367 )
365 368 coreconfigitem('experimental', 'evolution.allowdivergence',
366 369 default=False,
367 370 alias=[('experimental', 'allowdivergence')]
368 371 )
369 372 coreconfigitem('experimental', 'evolution.allowunstable',
370 373 default=None,
371 374 )
372 375 coreconfigitem('experimental', 'evolution.createmarkers',
373 376 default=None,
374 377 )
375 378 coreconfigitem('experimental', 'evolution.effect-flags',
376 379 default=False,
377 380 alias=[('experimental', 'effect-flags')]
378 381 )
379 382 coreconfigitem('experimental', 'evolution.exchange',
380 383 default=None,
381 384 )
382 385 coreconfigitem('experimental', 'evolution.bundle-obsmarker',
383 386 default=False,
384 387 )
385 388 coreconfigitem('experimental', 'evolution.track-operation',
386 389 default=True,
387 390 )
388 391 coreconfigitem('experimental', 'maxdeltachainspan',
389 392 default=-1,
390 393 )
391 394 coreconfigitem('experimental', 'mmapindexthreshold',
392 395 default=None,
393 396 )
394 397 coreconfigitem('experimental', 'nonnormalparanoidcheck',
395 398 default=False,
396 399 )
397 400 coreconfigitem('experimental', 'exportableenviron',
398 401 default=list,
399 402 )
400 403 coreconfigitem('experimental', 'extendedheader.index',
401 404 default=None,
402 405 )
403 406 coreconfigitem('experimental', 'extendedheader.similarity',
404 407 default=False,
405 408 )
406 409 coreconfigitem('experimental', 'format.compression',
407 410 default='zlib',
408 411 )
409 412 coreconfigitem('experimental', 'graphshorten',
410 413 default=False,
411 414 )
412 415 coreconfigitem('experimental', 'graphstyle.parent',
413 416 default=dynamicdefault,
414 417 )
415 418 coreconfigitem('experimental', 'graphstyle.missing',
416 419 default=dynamicdefault,
417 420 )
418 421 coreconfigitem('experimental', 'graphstyle.grandparent',
419 422 default=dynamicdefault,
420 423 )
421 424 coreconfigitem('experimental', 'hook-track-tags',
422 425 default=False,
423 426 )
424 427 coreconfigitem('experimental', 'httppostargs',
425 428 default=False,
426 429 )
427 430 coreconfigitem('experimental', 'manifestv2',
428 431 default=False,
429 432 )
430 433 coreconfigitem('experimental', 'mergedriver',
431 434 default=None,
432 435 )
433 436 coreconfigitem('experimental', 'obsmarkers-exchange-debug',
434 437 default=False,
435 438 )
436 439 coreconfigitem('experimental', 'rebase.multidest',
437 440 default=False,
438 441 )
439 442 coreconfigitem('experimental', 'revertalternateinteractivemode',
440 443 default=True,
441 444 )
442 445 coreconfigitem('experimental', 'revlogv2',
443 446 default=None,
444 447 )
445 448 coreconfigitem('experimental', 'spacemovesdown',
446 449 default=False,
447 450 )
448 451 coreconfigitem('experimental', 'sparse-read',
449 452 default=False,
450 453 )
451 454 coreconfigitem('experimental', 'sparse-read.density-threshold',
452 455 default=0.25,
453 456 )
454 457 coreconfigitem('experimental', 'sparse-read.min-gap-size',
455 458 default='256K',
456 459 )
457 460 coreconfigitem('experimental', 'treemanifest',
458 461 default=False,
459 462 )
460 463 coreconfigitem('extensions', '.*',
461 464 default=None,
462 465 generic=True,
463 466 )
464 467 coreconfigitem('extdata', '.*',
465 468 default=None,
466 469 generic=True,
467 470 )
468 471 coreconfigitem('format', 'aggressivemergedeltas',
469 472 default=False,
470 473 )
471 474 coreconfigitem('format', 'chunkcachesize',
472 475 default=None,
473 476 )
474 477 coreconfigitem('format', 'dotencode',
475 478 default=True,
476 479 )
477 480 coreconfigitem('format', 'generaldelta',
478 481 default=False,
479 482 )
480 483 coreconfigitem('format', 'manifestcachesize',
481 484 default=None,
482 485 )
483 486 coreconfigitem('format', 'maxchainlen',
484 487 default=None,
485 488 )
486 489 coreconfigitem('format', 'obsstore-version',
487 490 default=None,
488 491 )
489 492 coreconfigitem('format', 'usefncache',
490 493 default=True,
491 494 )
492 495 coreconfigitem('format', 'usegeneraldelta',
493 496 default=True,
494 497 )
495 498 coreconfigitem('format', 'usestore',
496 499 default=True,
497 500 )
498 501 coreconfigitem('fsmonitor', 'warn_when_unused',
499 502 default=True,
500 503 )
501 504 coreconfigitem('fsmonitor', 'warn_update_file_count',
502 505 default=50000,
503 506 )
504 507 coreconfigitem('hooks', '.*',
505 508 default=dynamicdefault,
506 509 generic=True,
507 510 )
508 511 coreconfigitem('hgweb-paths', '.*',
509 512 default=list,
510 513 generic=True,
511 514 )
512 515 coreconfigitem('hostfingerprints', '.*',
513 516 default=list,
514 517 generic=True,
515 518 )
516 519 coreconfigitem('hostsecurity', 'ciphers',
517 520 default=None,
518 521 )
519 522 coreconfigitem('hostsecurity', 'disabletls10warning',
520 523 default=False,
521 524 )
522 525 coreconfigitem('hostsecurity', 'minimumprotocol',
523 526 default=dynamicdefault,
524 527 )
525 528 coreconfigitem('hostsecurity', '.*:minimumprotocol$',
526 529 default=dynamicdefault,
527 530 generic=True,
528 531 )
529 532 coreconfigitem('hostsecurity', '.*:ciphers$',
530 533 default=dynamicdefault,
531 534 generic=True,
532 535 )
533 536 coreconfigitem('hostsecurity', '.*:fingerprints$',
534 537 default=list,
535 538 generic=True,
536 539 )
537 540 coreconfigitem('hostsecurity', '.*:verifycertsfile$',
538 541 default=None,
539 542 generic=True,
540 543 )
541 544
542 545 coreconfigitem('http_proxy', 'always',
543 546 default=False,
544 547 )
545 548 coreconfigitem('http_proxy', 'host',
546 549 default=None,
547 550 )
548 551 coreconfigitem('http_proxy', 'no',
549 552 default=list,
550 553 )
551 554 coreconfigitem('http_proxy', 'passwd',
552 555 default=None,
553 556 )
554 557 coreconfigitem('http_proxy', 'user',
555 558 default=None,
556 559 )
557 560 coreconfigitem('logtoprocess', 'commandexception',
558 561 default=None,
559 562 )
560 563 coreconfigitem('logtoprocess', 'commandfinish',
561 564 default=None,
562 565 )
563 566 coreconfigitem('logtoprocess', 'command',
564 567 default=None,
565 568 )
566 569 coreconfigitem('logtoprocess', 'develwarn',
567 570 default=None,
568 571 )
569 572 coreconfigitem('logtoprocess', 'uiblocked',
570 573 default=None,
571 574 )
572 575 coreconfigitem('merge', 'checkunknown',
573 576 default='abort',
574 577 )
575 578 coreconfigitem('merge', 'checkignored',
576 579 default='abort',
577 580 )
578 581 coreconfigitem('merge', 'followcopies',
579 582 default=True,
580 583 )
581 584 coreconfigitem('merge', 'on-failure',
582 585 default='continue',
583 586 )
584 587 coreconfigitem('merge', 'preferancestor',
585 588 default=lambda: ['*'],
586 589 )
587 590 coreconfigitem('merge-tools', '.*',
588 591 default=None,
589 592 generic=True,
590 593 )
591 594 coreconfigitem('merge-tools', br'.*\.args$',
592 595 default="$local $base $other",
593 596 generic=True,
594 597 priority=-1,
595 598 )
596 599 coreconfigitem('merge-tools', br'.*\.binary$',
597 600 default=False,
598 601 generic=True,
599 602 priority=-1,
600 603 )
601 604 coreconfigitem('merge-tools', br'.*\.check$',
602 605 default=list,
603 606 generic=True,
604 607 priority=-1,
605 608 )
606 609 coreconfigitem('merge-tools', br'.*\.checkchanged$',
607 610 default=False,
608 611 generic=True,
609 612 priority=-1,
610 613 )
611 614 coreconfigitem('merge-tools', br'.*\.executable$',
612 615 default=dynamicdefault,
613 616 generic=True,
614 617 priority=-1,
615 618 )
616 619 coreconfigitem('merge-tools', br'.*\.fixeol$',
617 620 default=False,
618 621 generic=True,
619 622 priority=-1,
620 623 )
621 624 coreconfigitem('merge-tools', br'.*\.gui$',
622 625 default=False,
623 626 generic=True,
624 627 priority=-1,
625 628 )
626 629 coreconfigitem('merge-tools', br'.*\.priority$',
627 630 default=0,
628 631 generic=True,
629 632 priority=-1,
630 633 )
631 634 coreconfigitem('merge-tools', br'.*\.premerge$',
632 635 default=dynamicdefault,
633 636 generic=True,
634 637 priority=-1,
635 638 )
636 639 coreconfigitem('merge-tools', br'.*\.symlink$',
637 640 default=False,
638 641 generic=True,
639 642 priority=-1,
640 643 )
641 644 coreconfigitem('pager', 'attend-.*',
642 645 default=dynamicdefault,
643 646 generic=True,
644 647 )
645 648 coreconfigitem('pager', 'ignore',
646 649 default=list,
647 650 )
648 651 coreconfigitem('pager', 'pager',
649 652 default=dynamicdefault,
650 653 )
651 654 coreconfigitem('patch', 'eol',
652 655 default='strict',
653 656 )
654 657 coreconfigitem('patch', 'fuzz',
655 658 default=2,
656 659 )
657 660 coreconfigitem('paths', 'default',
658 661 default=None,
659 662 )
660 663 coreconfigitem('paths', 'default-push',
661 664 default=None,
662 665 )
663 666 coreconfigitem('paths', '.*',
664 667 default=None,
665 668 generic=True,
666 669 )
667 670 coreconfigitem('phases', 'checksubrepos',
668 671 default='follow',
669 672 )
670 673 coreconfigitem('phases', 'new-commit',
671 674 default='draft',
672 675 )
673 676 coreconfigitem('phases', 'publish',
674 677 default=True,
675 678 )
676 679 coreconfigitem('profiling', 'enabled',
677 680 default=False,
678 681 )
679 682 coreconfigitem('profiling', 'format',
680 683 default='text',
681 684 )
682 685 coreconfigitem('profiling', 'freq',
683 686 default=1000,
684 687 )
685 688 coreconfigitem('profiling', 'limit',
686 689 default=30,
687 690 )
688 691 coreconfigitem('profiling', 'nested',
689 692 default=0,
690 693 )
691 694 coreconfigitem('profiling', 'output',
692 695 default=None,
693 696 )
694 697 coreconfigitem('profiling', 'showmax',
695 698 default=0.999,
696 699 )
697 700 coreconfigitem('profiling', 'showmin',
698 701 default=dynamicdefault,
699 702 )
700 703 coreconfigitem('profiling', 'sort',
701 704 default='inlinetime',
702 705 )
703 706 coreconfigitem('profiling', 'statformat',
704 707 default='hotpath',
705 708 )
706 709 coreconfigitem('profiling', 'type',
707 710 default='stat',
708 711 )
709 712 coreconfigitem('progress', 'assume-tty',
710 713 default=False,
711 714 )
712 715 coreconfigitem('progress', 'changedelay',
713 716 default=1,
714 717 )
715 718 coreconfigitem('progress', 'clear-complete',
716 719 default=True,
717 720 )
718 721 coreconfigitem('progress', 'debug',
719 722 default=False,
720 723 )
721 724 coreconfigitem('progress', 'delay',
722 725 default=3,
723 726 )
724 727 coreconfigitem('progress', 'disable',
725 728 default=False,
726 729 )
727 730 coreconfigitem('progress', 'estimateinterval',
728 731 default=60.0,
729 732 )
730 733 coreconfigitem('progress', 'format',
731 734 default=lambda: ['topic', 'bar', 'number', 'estimate'],
732 735 )
733 736 coreconfigitem('progress', 'refresh',
734 737 default=0.1,
735 738 )
736 739 coreconfigitem('progress', 'width',
737 740 default=dynamicdefault,
738 741 )
739 742 coreconfigitem('push', 'pushvars.server',
740 743 default=False,
741 744 )
742 745 coreconfigitem('server', 'bundle1',
743 746 default=True,
744 747 )
745 748 coreconfigitem('server', 'bundle1gd',
746 749 default=None,
747 750 )
748 751 coreconfigitem('server', 'bundle1.pull',
749 752 default=None,
750 753 )
751 754 coreconfigitem('server', 'bundle1gd.pull',
752 755 default=None,
753 756 )
754 757 coreconfigitem('server', 'bundle1.push',
755 758 default=None,
756 759 )
757 760 coreconfigitem('server', 'bundle1gd.push',
758 761 default=None,
759 762 )
760 763 coreconfigitem('server', 'compressionengines',
761 764 default=list,
762 765 )
763 766 coreconfigitem('server', 'concurrent-push-mode',
764 767 default='strict',
765 768 )
766 769 coreconfigitem('server', 'disablefullbundle',
767 770 default=False,
768 771 )
769 772 coreconfigitem('server', 'maxhttpheaderlen',
770 773 default=1024,
771 774 )
772 775 coreconfigitem('server', 'preferuncompressed',
773 776 default=False,
774 777 )
775 778 coreconfigitem('server', 'uncompressed',
776 779 default=True,
777 780 )
778 781 coreconfigitem('server', 'uncompressedallowsecret',
779 782 default=False,
780 783 )
781 784 coreconfigitem('server', 'validate',
782 785 default=False,
783 786 )
784 787 coreconfigitem('server', 'zliblevel',
785 788 default=-1,
786 789 )
787 790 coreconfigitem('smtp', 'host',
788 791 default=None,
789 792 )
790 793 coreconfigitem('smtp', 'local_hostname',
791 794 default=None,
792 795 )
793 796 coreconfigitem('smtp', 'password',
794 797 default=None,
795 798 )
796 799 coreconfigitem('smtp', 'port',
797 800 default=dynamicdefault,
798 801 )
799 802 coreconfigitem('smtp', 'tls',
800 803 default='none',
801 804 )
802 805 coreconfigitem('smtp', 'username',
803 806 default=None,
804 807 )
805 808 coreconfigitem('sparse', 'missingwarning',
806 809 default=True,
807 810 )
808 811 coreconfigitem('templates', '.*',
809 812 default=None,
810 813 generic=True,
811 814 )
812 815 coreconfigitem('trusted', 'groups',
813 816 default=list,
814 817 )
815 818 coreconfigitem('trusted', 'users',
816 819 default=list,
817 820 )
818 821 coreconfigitem('ui', '_usedassubrepo',
819 822 default=False,
820 823 )
821 824 coreconfigitem('ui', 'allowemptycommit',
822 825 default=False,
823 826 )
824 827 coreconfigitem('ui', 'archivemeta',
825 828 default=True,
826 829 )
827 830 coreconfigitem('ui', 'askusername',
828 831 default=False,
829 832 )
830 833 coreconfigitem('ui', 'clonebundlefallback',
831 834 default=False,
832 835 )
833 836 coreconfigitem('ui', 'clonebundleprefers',
834 837 default=list,
835 838 )
836 839 coreconfigitem('ui', 'clonebundles',
837 840 default=True,
838 841 )
839 842 coreconfigitem('ui', 'color',
840 843 default='auto',
841 844 )
842 845 coreconfigitem('ui', 'commitsubrepos',
843 846 default=False,
844 847 )
845 848 coreconfigitem('ui', 'debug',
846 849 default=False,
847 850 )
848 851 coreconfigitem('ui', 'debugger',
849 852 default=None,
850 853 )
851 854 coreconfigitem('ui', 'editor',
852 855 default=dynamicdefault,
853 856 )
854 857 coreconfigitem('ui', 'fallbackencoding',
855 858 default=None,
856 859 )
857 860 coreconfigitem('ui', 'forcecwd',
858 861 default=None,
859 862 )
860 863 coreconfigitem('ui', 'forcemerge',
861 864 default=None,
862 865 )
863 866 coreconfigitem('ui', 'formatdebug',
864 867 default=False,
865 868 )
866 869 coreconfigitem('ui', 'formatjson',
867 870 default=False,
868 871 )
869 872 coreconfigitem('ui', 'formatted',
870 873 default=None,
871 874 )
872 875 coreconfigitem('ui', 'graphnodetemplate',
873 876 default=None,
874 877 )
875 878 coreconfigitem('ui', 'http2debuglevel',
876 879 default=None,
877 880 )
878 881 coreconfigitem('ui', 'interactive',
879 882 default=None,
880 883 )
881 884 coreconfigitem('ui', 'interface',
882 885 default=None,
883 886 )
884 887 coreconfigitem('ui', 'interface.chunkselector',
885 888 default=None,
886 889 )
887 890 coreconfigitem('ui', 'logblockedtimes',
888 891 default=False,
889 892 )
890 893 coreconfigitem('ui', 'logtemplate',
891 894 default=None,
892 895 )
893 896 coreconfigitem('ui', 'merge',
894 897 default=None,
895 898 )
896 899 coreconfigitem('ui', 'mergemarkers',
897 900 default='basic',
898 901 )
899 902 coreconfigitem('ui', 'mergemarkertemplate',
900 903 default=('{node|short} '
901 904 '{ifeq(tags, "tip", "", '
902 905 'ifeq(tags, "", "", "{tags} "))}'
903 906 '{if(bookmarks, "{bookmarks} ")}'
904 907 '{ifeq(branch, "default", "", "{branch} ")}'
905 908 '- {author|user}: {desc|firstline}')
906 909 )
907 910 coreconfigitem('ui', 'nontty',
908 911 default=False,
909 912 )
910 913 coreconfigitem('ui', 'origbackuppath',
911 914 default=None,
912 915 )
913 916 coreconfigitem('ui', 'paginate',
914 917 default=True,
915 918 )
916 919 coreconfigitem('ui', 'patch',
917 920 default=None,
918 921 )
919 922 coreconfigitem('ui', 'portablefilenames',
920 923 default='warn',
921 924 )
922 925 coreconfigitem('ui', 'promptecho',
923 926 default=False,
924 927 )
925 928 coreconfigitem('ui', 'quiet',
926 929 default=False,
927 930 )
928 931 coreconfigitem('ui', 'quietbookmarkmove',
929 932 default=False,
930 933 )
931 934 coreconfigitem('ui', 'remotecmd',
932 935 default='hg',
933 936 )
934 937 coreconfigitem('ui', 'report_untrusted',
935 938 default=True,
936 939 )
937 940 coreconfigitem('ui', 'rollback',
938 941 default=True,
939 942 )
940 943 coreconfigitem('ui', 'slash',
941 944 default=False,
942 945 )
943 946 coreconfigitem('ui', 'ssh',
944 947 default='ssh',
945 948 )
946 949 coreconfigitem('ui', 'statuscopies',
947 950 default=False,
948 951 )
949 952 coreconfigitem('ui', 'strict',
950 953 default=False,
951 954 )
952 955 coreconfigitem('ui', 'style',
953 956 default='',
954 957 )
955 958 coreconfigitem('ui', 'supportcontact',
956 959 default=None,
957 960 )
958 961 coreconfigitem('ui', 'textwidth',
959 962 default=78,
960 963 )
961 964 coreconfigitem('ui', 'timeout',
962 965 default='600',
963 966 )
964 967 coreconfigitem('ui', 'traceback',
965 968 default=False,
966 969 )
967 970 coreconfigitem('ui', 'tweakdefaults',
968 971 default=False,
969 972 )
970 973 coreconfigitem('ui', 'usehttp2',
971 974 default=False,
972 975 )
973 976 coreconfigitem('ui', 'username',
974 977 alias=[('ui', 'user')]
975 978 )
976 979 coreconfigitem('ui', 'verbose',
977 980 default=False,
978 981 )
979 982 coreconfigitem('verify', 'skipflags',
980 983 default=None,
981 984 )
982 985 coreconfigitem('web', 'allowbz2',
983 986 default=False,
984 987 )
985 988 coreconfigitem('web', 'allowgz',
986 989 default=False,
987 990 )
988 991 coreconfigitem('web', 'allowpull',
989 992 default=True,
990 993 )
991 994 coreconfigitem('web', 'allow_push',
992 995 default=list,
993 996 )
994 997 coreconfigitem('web', 'allowzip',
995 998 default=False,
996 999 )
997 1000 coreconfigitem('web', 'archivesubrepos',
998 1001 default=False,
999 1002 )
1000 1003 coreconfigitem('web', 'cache',
1001 1004 default=True,
1002 1005 )
1003 1006 coreconfigitem('web', 'contact',
1004 1007 default=None,
1005 1008 )
1006 1009 coreconfigitem('web', 'deny_push',
1007 1010 default=list,
1008 1011 )
1009 1012 coreconfigitem('web', 'guessmime',
1010 1013 default=False,
1011 1014 )
1012 1015 coreconfigitem('web', 'hidden',
1013 1016 default=False,
1014 1017 )
1015 1018 coreconfigitem('web', 'labels',
1016 1019 default=list,
1017 1020 )
1018 1021 coreconfigitem('web', 'logoimg',
1019 1022 default='hglogo.png',
1020 1023 )
1021 1024 coreconfigitem('web', 'logourl',
1022 1025 default='https://mercurial-scm.org/',
1023 1026 )
1024 1027 coreconfigitem('web', 'accesslog',
1025 1028 default='-',
1026 1029 )
1027 1030 coreconfigitem('web', 'address',
1028 1031 default='',
1029 1032 )
1030 1033 coreconfigitem('web', 'allow_archive',
1031 1034 default=list,
1032 1035 )
1033 1036 coreconfigitem('web', 'allow_read',
1034 1037 default=list,
1035 1038 )
1036 1039 coreconfigitem('web', 'baseurl',
1037 1040 default=None,
1038 1041 )
1039 1042 coreconfigitem('web', 'cacerts',
1040 1043 default=None,
1041 1044 )
1042 1045 coreconfigitem('web', 'certificate',
1043 1046 default=None,
1044 1047 )
1045 1048 coreconfigitem('web', 'collapse',
1046 1049 default=False,
1047 1050 )
1048 1051 coreconfigitem('web', 'csp',
1049 1052 default=None,
1050 1053 )
1051 1054 coreconfigitem('web', 'deny_read',
1052 1055 default=list,
1053 1056 )
1054 1057 coreconfigitem('web', 'descend',
1055 1058 default=True,
1056 1059 )
1057 1060 coreconfigitem('web', 'description',
1058 1061 default="",
1059 1062 )
1060 1063 coreconfigitem('web', 'encoding',
1061 1064 default=lambda: encoding.encoding,
1062 1065 )
1063 1066 coreconfigitem('web', 'errorlog',
1064 1067 default='-',
1065 1068 )
1066 1069 coreconfigitem('web', 'ipv6',
1067 1070 default=False,
1068 1071 )
1069 1072 coreconfigitem('web', 'maxchanges',
1070 1073 default=10,
1071 1074 )
1072 1075 coreconfigitem('web', 'maxfiles',
1073 1076 default=10,
1074 1077 )
1075 1078 coreconfigitem('web', 'maxshortchanges',
1076 1079 default=60,
1077 1080 )
1078 1081 coreconfigitem('web', 'motd',
1079 1082 default='',
1080 1083 )
1081 1084 coreconfigitem('web', 'name',
1082 1085 default=dynamicdefault,
1083 1086 )
1084 1087 coreconfigitem('web', 'port',
1085 1088 default=8000,
1086 1089 )
1087 1090 coreconfigitem('web', 'prefix',
1088 1091 default='',
1089 1092 )
1090 1093 coreconfigitem('web', 'push_ssl',
1091 1094 default=True,
1092 1095 )
1093 1096 coreconfigitem('web', 'refreshinterval',
1094 1097 default=20,
1095 1098 )
1096 1099 coreconfigitem('web', 'staticurl',
1097 1100 default=None,
1098 1101 )
1099 1102 coreconfigitem('web', 'stripes',
1100 1103 default=1,
1101 1104 )
1102 1105 coreconfigitem('web', 'style',
1103 1106 default='paper',
1104 1107 )
1105 1108 coreconfigitem('web', 'templates',
1106 1109 default=None,
1107 1110 )
1108 1111 coreconfigitem('web', 'view',
1109 1112 default='served',
1110 1113 )
1111 1114 coreconfigitem('worker', 'backgroundclose',
1112 1115 default=dynamicdefault,
1113 1116 )
1114 1117 # Windows defaults to a limit of 512 open files. A buffer of 128
1115 1118 # should give us enough headway.
1116 1119 coreconfigitem('worker', 'backgroundclosemaxqueue',
1117 1120 default=384,
1118 1121 )
1119 1122 coreconfigitem('worker', 'backgroundcloseminfilecount',
1120 1123 default=2048,
1121 1124 )
1122 1125 coreconfigitem('worker', 'backgroundclosethreadcount',
1123 1126 default=4,
1124 1127 )
1125 1128 coreconfigitem('worker', 'numcpus',
1126 1129 default=None,
1127 1130 )
1128 1131
1129 1132 # Rebase related configuration moved to core because other extension are doing
1130 1133 # strange things. For example, shelve import the extensions to reuse some bit
1131 1134 # without formally loading it.
1132 1135 coreconfigitem('commands', 'rebase.requiredest',
1133 1136 default=False,
1134 1137 )
1135 1138 coreconfigitem('experimental', 'rebaseskipobsolete',
1136 1139 default=True,
1137 1140 )
1138 1141 coreconfigitem('rebase', 'singletransaction',
1139 1142 default=False,
1140 1143 )
General Comments 0
You need to be logged in to leave comments. Login now