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