##// END OF EJS Templates
histedit: import chistedit curses UI from hg-experimental...
Augie Fackler -
r40959:c3617545 default
parent child Browse files
Show More
This diff has been collapsed as it changes many lines, (569 lines changed) Show them Hide them
@@ -1,1658 +1,2227 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 fcntl
187 import functools
186 import os
188 import os
189 import struct
190 import termios
187
191
188 from mercurial.i18n import _
192 from mercurial.i18n import _
189 from mercurial import (
193 from mercurial import (
190 bundle2,
194 bundle2,
191 cmdutil,
195 cmdutil,
192 context,
196 context,
193 copies,
197 copies,
194 destutil,
198 destutil,
195 discovery,
199 discovery,
196 error,
200 error,
197 exchange,
201 exchange,
198 extensions,
202 extensions,
199 hg,
203 hg,
200 lock,
204 lock,
205 logcmdutil,
201 merge as mergemod,
206 merge as mergemod,
202 mergeutil,
207 mergeutil,
203 node,
208 node,
204 obsolete,
209 obsolete,
205 pycompat,
210 pycompat,
206 registrar,
211 registrar,
207 repair,
212 repair,
208 scmutil,
213 scmutil,
209 state as statemod,
214 state as statemod,
210 util,
215 util,
211 )
216 )
212 from mercurial.utils import (
217 from mercurial.utils import (
213 stringutil,
218 stringutil,
214 )
219 )
215
220
216 pickle = util.pickle
221 pickle = util.pickle
217 release = lock.release
222 release = lock.release
218 cmdtable = {}
223 cmdtable = {}
219 command = registrar.command(cmdtable)
224 command = registrar.command(cmdtable)
220
225
221 configtable = {}
226 configtable = {}
222 configitem = registrar.configitem(configtable)
227 configitem = registrar.configitem(configtable)
223 configitem('experimental', 'histedit.autoverb',
228 configitem('experimental', 'histedit.autoverb',
224 default=False,
229 default=False,
225 )
230 )
226 configitem('histedit', 'defaultrev',
231 configitem('histedit', 'defaultrev',
227 default=None,
232 default=None,
228 )
233 )
229 configitem('histedit', 'dropmissing',
234 configitem('histedit', 'dropmissing',
230 default=False,
235 default=False,
231 )
236 )
232 configitem('histedit', 'linelen',
237 configitem('histedit', 'linelen',
233 default=80,
238 default=80,
234 )
239 )
235 configitem('histedit', 'singletransaction',
240 configitem('histedit', 'singletransaction',
236 default=False,
241 default=False,
237 )
242 )
243 configitem('ui', 'interface.histedit',
244 default=None,
245 )
238
246
239 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
247 # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for
240 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
248 # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should
241 # be specifying the version(s) of Mercurial they are tested with, or
249 # be specifying the version(s) of Mercurial they are tested with, or
242 # leave the attribute unspecified.
250 # leave the attribute unspecified.
243 testedwith = 'ships-with-hg-core'
251 testedwith = 'ships-with-hg-core'
244
252
245 actiontable = {}
253 actiontable = {}
246 primaryactions = set()
254 primaryactions = set()
247 secondaryactions = set()
255 secondaryactions = set()
248 tertiaryactions = set()
256 tertiaryactions = set()
249 internalactions = set()
257 internalactions = set()
250
258
251 def geteditcomment(ui, first, last):
259 def geteditcomment(ui, first, last):
252 """ construct the editor comment
260 """ construct the editor comment
253 The comment includes::
261 The comment includes::
254 - an intro
262 - an intro
255 - sorted primary commands
263 - sorted primary commands
256 - sorted short commands
264 - sorted short commands
257 - sorted long commands
265 - sorted long commands
258 - additional hints
266 - additional hints
259
267
260 Commands are only included once.
268 Commands are only included once.
261 """
269 """
262 intro = _("""Edit history between %s and %s
270 intro = _("""Edit history between %s and %s
263
271
264 Commits are listed from least to most recent
272 Commits are listed from least to most recent
265
273
266 You can reorder changesets by reordering the lines
274 You can reorder changesets by reordering the lines
267
275
268 Commands:
276 Commands:
269 """)
277 """)
270 actions = []
278 actions = []
271 def addverb(v):
279 def addverb(v):
272 a = actiontable[v]
280 a = actiontable[v]
273 lines = a.message.split("\n")
281 lines = a.message.split("\n")
274 if len(a.verbs):
282 if len(a.verbs):
275 v = ', '.join(sorted(a.verbs, key=lambda v: len(v)))
283 v = ', '.join(sorted(a.verbs, key=lambda v: len(v)))
276 actions.append(" %s = %s" % (v, lines[0]))
284 actions.append(" %s = %s" % (v, lines[0]))
277 actions.extend([' %s' for l in lines[1:]])
285 actions.extend([' %s' for l in lines[1:]])
278
286
279 for v in (
287 for v in (
280 sorted(primaryactions) +
288 sorted(primaryactions) +
281 sorted(secondaryactions) +
289 sorted(secondaryactions) +
282 sorted(tertiaryactions)
290 sorted(tertiaryactions)
283 ):
291 ):
284 addverb(v)
292 addverb(v)
285 actions.append('')
293 actions.append('')
286
294
287 hints = []
295 hints = []
288 if ui.configbool('histedit', 'dropmissing'):
296 if ui.configbool('histedit', 'dropmissing'):
289 hints.append("Deleting a changeset from the list "
297 hints.append("Deleting a changeset from the list "
290 "will DISCARD it from the edited history!")
298 "will DISCARD it from the edited history!")
291
299
292 lines = (intro % (first, last)).split('\n') + actions + hints
300 lines = (intro % (first, last)).split('\n') + actions + hints
293
301
294 return ''.join(['# %s\n' % l if l else '#\n' for l in lines])
302 return ''.join(['# %s\n' % l if l else '#\n' for l in lines])
295
303
296 class histeditstate(object):
304 class histeditstate(object):
297 def __init__(self, repo, parentctxnode=None, actions=None, keep=None,
305 def __init__(self, repo, parentctxnode=None, actions=None, keep=None,
298 topmost=None, replacements=None, lock=None, wlock=None):
306 topmost=None, replacements=None, lock=None, wlock=None):
299 self.repo = repo
307 self.repo = repo
300 self.actions = actions
308 self.actions = actions
301 self.keep = keep
309 self.keep = keep
302 self.topmost = topmost
310 self.topmost = topmost
303 self.parentctxnode = parentctxnode
311 self.parentctxnode = parentctxnode
304 self.lock = lock
312 self.lock = lock
305 self.wlock = wlock
313 self.wlock = wlock
306 self.backupfile = None
314 self.backupfile = None
307 self.stateobj = statemod.cmdstate(repo, 'histedit-state')
315 self.stateobj = statemod.cmdstate(repo, 'histedit-state')
308 if replacements is None:
316 if replacements is None:
309 self.replacements = []
317 self.replacements = []
310 else:
318 else:
311 self.replacements = replacements
319 self.replacements = replacements
312
320
313 def read(self):
321 def read(self):
314 """Load histedit state from disk and set fields appropriately."""
322 """Load histedit state from disk and set fields appropriately."""
315 if not self.stateobj.exists():
323 if not self.stateobj.exists():
316 cmdutil.wrongtooltocontinue(self.repo, _('histedit'))
324 cmdutil.wrongtooltocontinue(self.repo, _('histedit'))
317
325
318 data = self._read()
326 data = self._read()
319
327
320 self.parentctxnode = data['parentctxnode']
328 self.parentctxnode = data['parentctxnode']
321 actions = parserules(data['rules'], self)
329 actions = parserules(data['rules'], self)
322 self.actions = actions
330 self.actions = actions
323 self.keep = data['keep']
331 self.keep = data['keep']
324 self.topmost = data['topmost']
332 self.topmost = data['topmost']
325 self.replacements = data['replacements']
333 self.replacements = data['replacements']
326 self.backupfile = data['backupfile']
334 self.backupfile = data['backupfile']
327
335
328 def _read(self):
336 def _read(self):
329 fp = self.repo.vfs.read('histedit-state')
337 fp = self.repo.vfs.read('histedit-state')
330 if fp.startswith('v1\n'):
338 if fp.startswith('v1\n'):
331 data = self._load()
339 data = self._load()
332 parentctxnode, rules, keep, topmost, replacements, backupfile = data
340 parentctxnode, rules, keep, topmost, replacements, backupfile = data
333 else:
341 else:
334 data = pickle.loads(fp)
342 data = pickle.loads(fp)
335 parentctxnode, rules, keep, topmost, replacements = data
343 parentctxnode, rules, keep, topmost, replacements = data
336 backupfile = None
344 backupfile = None
337 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
345 rules = "\n".join(["%s %s" % (verb, rest) for [verb, rest] in rules])
338
346
339 return {'parentctxnode': parentctxnode, "rules": rules, "keep": keep,
347 return {'parentctxnode': parentctxnode, "rules": rules, "keep": keep,
340 "topmost": topmost, "replacements": replacements,
348 "topmost": topmost, "replacements": replacements,
341 "backupfile": backupfile}
349 "backupfile": backupfile}
342
350
343 def write(self, tr=None):
351 def write(self, tr=None):
344 if tr:
352 if tr:
345 tr.addfilegenerator('histedit-state', ('histedit-state',),
353 tr.addfilegenerator('histedit-state', ('histedit-state',),
346 self._write, location='plain')
354 self._write, location='plain')
347 else:
355 else:
348 with self.repo.vfs("histedit-state", "w") as f:
356 with self.repo.vfs("histedit-state", "w") as f:
349 self._write(f)
357 self._write(f)
350
358
351 def _write(self, fp):
359 def _write(self, fp):
352 fp.write('v1\n')
360 fp.write('v1\n')
353 fp.write('%s\n' % node.hex(self.parentctxnode))
361 fp.write('%s\n' % node.hex(self.parentctxnode))
354 fp.write('%s\n' % node.hex(self.topmost))
362 fp.write('%s\n' % node.hex(self.topmost))
355 fp.write('%s\n' % ('True' if self.keep else 'False'))
363 fp.write('%s\n' % ('True' if self.keep else 'False'))
356 fp.write('%d\n' % len(self.actions))
364 fp.write('%d\n' % len(self.actions))
357 for action in self.actions:
365 for action in self.actions:
358 fp.write('%s\n' % action.tostate())
366 fp.write('%s\n' % action.tostate())
359 fp.write('%d\n' % len(self.replacements))
367 fp.write('%d\n' % len(self.replacements))
360 for replacement in self.replacements:
368 for replacement in self.replacements:
361 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
369 fp.write('%s%s\n' % (node.hex(replacement[0]), ''.join(node.hex(r)
362 for r in replacement[1])))
370 for r in replacement[1])))
363 backupfile = self.backupfile
371 backupfile = self.backupfile
364 if not backupfile:
372 if not backupfile:
365 backupfile = ''
373 backupfile = ''
366 fp.write('%s\n' % backupfile)
374 fp.write('%s\n' % backupfile)
367
375
368 def _load(self):
376 def _load(self):
369 fp = self.repo.vfs('histedit-state', 'r')
377 fp = self.repo.vfs('histedit-state', 'r')
370 lines = [l[:-1] for l in fp.readlines()]
378 lines = [l[:-1] for l in fp.readlines()]
371
379
372 index = 0
380 index = 0
373 lines[index] # version number
381 lines[index] # version number
374 index += 1
382 index += 1
375
383
376 parentctxnode = node.bin(lines[index])
384 parentctxnode = node.bin(lines[index])
377 index += 1
385 index += 1
378
386
379 topmost = node.bin(lines[index])
387 topmost = node.bin(lines[index])
380 index += 1
388 index += 1
381
389
382 keep = lines[index] == 'True'
390 keep = lines[index] == 'True'
383 index += 1
391 index += 1
384
392
385 # Rules
393 # Rules
386 rules = []
394 rules = []
387 rulelen = int(lines[index])
395 rulelen = int(lines[index])
388 index += 1
396 index += 1
389 for i in pycompat.xrange(rulelen):
397 for i in pycompat.xrange(rulelen):
390 ruleaction = lines[index]
398 ruleaction = lines[index]
391 index += 1
399 index += 1
392 rule = lines[index]
400 rule = lines[index]
393 index += 1
401 index += 1
394 rules.append((ruleaction, rule))
402 rules.append((ruleaction, rule))
395
403
396 # Replacements
404 # Replacements
397 replacements = []
405 replacements = []
398 replacementlen = int(lines[index])
406 replacementlen = int(lines[index])
399 index += 1
407 index += 1
400 for i in pycompat.xrange(replacementlen):
408 for i in pycompat.xrange(replacementlen):
401 replacement = lines[index]
409 replacement = lines[index]
402 original = node.bin(replacement[:40])
410 original = node.bin(replacement[:40])
403 succ = [node.bin(replacement[i:i + 40]) for i in
411 succ = [node.bin(replacement[i:i + 40]) for i in
404 range(40, len(replacement), 40)]
412 range(40, len(replacement), 40)]
405 replacements.append((original, succ))
413 replacements.append((original, succ))
406 index += 1
414 index += 1
407
415
408 backupfile = lines[index]
416 backupfile = lines[index]
409 index += 1
417 index += 1
410
418
411 fp.close()
419 fp.close()
412
420
413 return parentctxnode, rules, keep, topmost, replacements, backupfile
421 return parentctxnode, rules, keep, topmost, replacements, backupfile
414
422
415 def clear(self):
423 def clear(self):
416 if self.inprogress():
424 if self.inprogress():
417 self.repo.vfs.unlink('histedit-state')
425 self.repo.vfs.unlink('histedit-state')
418
426
419 def inprogress(self):
427 def inprogress(self):
420 return self.repo.vfs.exists('histedit-state')
428 return self.repo.vfs.exists('histedit-state')
421
429
422
430
423 class histeditaction(object):
431 class histeditaction(object):
424 def __init__(self, state, node):
432 def __init__(self, state, node):
425 self.state = state
433 self.state = state
426 self.repo = state.repo
434 self.repo = state.repo
427 self.node = node
435 self.node = node
428
436
429 @classmethod
437 @classmethod
430 def fromrule(cls, state, rule):
438 def fromrule(cls, state, rule):
431 """Parses the given rule, returning an instance of the histeditaction.
439 """Parses the given rule, returning an instance of the histeditaction.
432 """
440 """
433 ruleid = rule.strip().split(' ', 1)[0]
441 ruleid = rule.strip().split(' ', 1)[0]
434 # ruleid can be anything from rev numbers, hashes, "bookmarks" etc
442 # ruleid can be anything from rev numbers, hashes, "bookmarks" etc
435 # Check for validation of rule ids and get the rulehash
443 # Check for validation of rule ids and get the rulehash
436 try:
444 try:
437 rev = node.bin(ruleid)
445 rev = node.bin(ruleid)
438 except TypeError:
446 except TypeError:
439 try:
447 try:
440 _ctx = scmutil.revsingle(state.repo, ruleid)
448 _ctx = scmutil.revsingle(state.repo, ruleid)
441 rulehash = _ctx.hex()
449 rulehash = _ctx.hex()
442 rev = node.bin(rulehash)
450 rev = node.bin(rulehash)
443 except error.RepoLookupError:
451 except error.RepoLookupError:
444 raise error.ParseError(_("invalid changeset %s") % ruleid)
452 raise error.ParseError(_("invalid changeset %s") % ruleid)
445 return cls(state, rev)
453 return cls(state, rev)
446
454
447 def verify(self, prev, expected, seen):
455 def verify(self, prev, expected, seen):
448 """ Verifies semantic correctness of the rule"""
456 """ Verifies semantic correctness of the rule"""
449 repo = self.repo
457 repo = self.repo
450 ha = node.hex(self.node)
458 ha = node.hex(self.node)
451 self.node = scmutil.resolvehexnodeidprefix(repo, ha)
459 self.node = scmutil.resolvehexnodeidprefix(repo, ha)
452 if self.node is None:
460 if self.node is None:
453 raise error.ParseError(_('unknown changeset %s listed') % ha[:12])
461 raise error.ParseError(_('unknown changeset %s listed') % ha[:12])
454 self._verifynodeconstraints(prev, expected, seen)
462 self._verifynodeconstraints(prev, expected, seen)
455
463
456 def _verifynodeconstraints(self, prev, expected, seen):
464 def _verifynodeconstraints(self, prev, expected, seen):
457 # by default command need a node in the edited list
465 # by default command need a node in the edited list
458 if self.node not in expected:
466 if self.node not in expected:
459 raise error.ParseError(_('%s "%s" changeset was not a candidate')
467 raise error.ParseError(_('%s "%s" changeset was not a candidate')
460 % (self.verb, node.short(self.node)),
468 % (self.verb, node.short(self.node)),
461 hint=_('only use listed changesets'))
469 hint=_('only use listed changesets'))
462 # and only one command per node
470 # and only one command per node
463 if self.node in seen:
471 if self.node in seen:
464 raise error.ParseError(_('duplicated command for changeset %s') %
472 raise error.ParseError(_('duplicated command for changeset %s') %
465 node.short(self.node))
473 node.short(self.node))
466
474
467 def torule(self):
475 def torule(self):
468 """build a histedit rule line for an action
476 """build a histedit rule line for an action
469
477
470 by default lines are in the form:
478 by default lines are in the form:
471 <hash> <rev> <summary>
479 <hash> <rev> <summary>
472 """
480 """
473 ctx = self.repo[self.node]
481 ctx = self.repo[self.node]
474 summary = _getsummary(ctx)
482 summary = _getsummary(ctx)
475 line = '%s %s %d %s' % (self.verb, ctx, ctx.rev(), summary)
483 line = '%s %s %d %s' % (self.verb, ctx, ctx.rev(), summary)
476 # trim to 75 columns by default so it's not stupidly wide in my editor
484 # trim to 75 columns by default so it's not stupidly wide in my editor
477 # (the 5 more are left for verb)
485 # (the 5 more are left for verb)
478 maxlen = self.repo.ui.configint('histedit', 'linelen')
486 maxlen = self.repo.ui.configint('histedit', 'linelen')
479 maxlen = max(maxlen, 22) # avoid truncating hash
487 maxlen = max(maxlen, 22) # avoid truncating hash
480 return stringutil.ellipsis(line, maxlen)
488 return stringutil.ellipsis(line, maxlen)
481
489
482 def tostate(self):
490 def tostate(self):
483 """Print an action in format used by histedit state files
491 """Print an action in format used by histedit state files
484 (the first line is a verb, the remainder is the second)
492 (the first line is a verb, the remainder is the second)
485 """
493 """
486 return "%s\n%s" % (self.verb, node.hex(self.node))
494 return "%s\n%s" % (self.verb, node.hex(self.node))
487
495
488 def run(self):
496 def run(self):
489 """Runs the action. The default behavior is simply apply the action's
497 """Runs the action. The default behavior is simply apply the action's
490 rulectx onto the current parentctx."""
498 rulectx onto the current parentctx."""
491 self.applychange()
499 self.applychange()
492 self.continuedirty()
500 self.continuedirty()
493 return self.continueclean()
501 return self.continueclean()
494
502
495 def applychange(self):
503 def applychange(self):
496 """Applies the changes from this action's rulectx onto the current
504 """Applies the changes from this action's rulectx onto the current
497 parentctx, but does not commit them."""
505 parentctx, but does not commit them."""
498 repo = self.repo
506 repo = self.repo
499 rulectx = repo[self.node]
507 rulectx = repo[self.node]
500 repo.ui.pushbuffer(error=True, labeled=True)
508 repo.ui.pushbuffer(error=True, labeled=True)
501 hg.update(repo, self.state.parentctxnode, quietempty=True)
509 hg.update(repo, self.state.parentctxnode, quietempty=True)
502 stats = applychanges(repo.ui, repo, rulectx, {})
510 stats = applychanges(repo.ui, repo, rulectx, {})
503 repo.dirstate.setbranch(rulectx.branch())
511 repo.dirstate.setbranch(rulectx.branch())
504 if stats.unresolvedcount:
512 if stats.unresolvedcount:
505 buf = repo.ui.popbuffer()
513 buf = repo.ui.popbuffer()
506 repo.ui.write(buf)
514 repo.ui.write(buf)
507 raise error.InterventionRequired(
515 raise error.InterventionRequired(
508 _('Fix up the change (%s %s)') %
516 _('Fix up the change (%s %s)') %
509 (self.verb, node.short(self.node)),
517 (self.verb, node.short(self.node)),
510 hint=_('hg histedit --continue to resume'))
518 hint=_('hg histedit --continue to resume'))
511 else:
519 else:
512 repo.ui.popbuffer()
520 repo.ui.popbuffer()
513
521
514 def continuedirty(self):
522 def continuedirty(self):
515 """Continues the action when changes have been applied to the working
523 """Continues the action when changes have been applied to the working
516 copy. The default behavior is to commit the dirty changes."""
524 copy. The default behavior is to commit the dirty changes."""
517 repo = self.repo
525 repo = self.repo
518 rulectx = repo[self.node]
526 rulectx = repo[self.node]
519
527
520 editor = self.commiteditor()
528 editor = self.commiteditor()
521 commit = commitfuncfor(repo, rulectx)
529 commit = commitfuncfor(repo, rulectx)
522
530
523 commit(text=rulectx.description(), user=rulectx.user(),
531 commit(text=rulectx.description(), user=rulectx.user(),
524 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
532 date=rulectx.date(), extra=rulectx.extra(), editor=editor)
525
533
526 def commiteditor(self):
534 def commiteditor(self):
527 """The editor to be used to edit the commit message."""
535 """The editor to be used to edit the commit message."""
528 return False
536 return False
529
537
530 def continueclean(self):
538 def continueclean(self):
531 """Continues the action when the working copy is clean. The default
539 """Continues the action when the working copy is clean. The default
532 behavior is to accept the current commit as the new version of the
540 behavior is to accept the current commit as the new version of the
533 rulectx."""
541 rulectx."""
534 ctx = self.repo['.']
542 ctx = self.repo['.']
535 if ctx.node() == self.state.parentctxnode:
543 if ctx.node() == self.state.parentctxnode:
536 self.repo.ui.warn(_('%s: skipping changeset (no changes)\n') %
544 self.repo.ui.warn(_('%s: skipping changeset (no changes)\n') %
537 node.short(self.node))
545 node.short(self.node))
538 return ctx, [(self.node, tuple())]
546 return ctx, [(self.node, tuple())]
539 if ctx.node() == self.node:
547 if ctx.node() == self.node:
540 # Nothing changed
548 # Nothing changed
541 return ctx, []
549 return ctx, []
542 return ctx, [(self.node, (ctx.node(),))]
550 return ctx, [(self.node, (ctx.node(),))]
543
551
544 def commitfuncfor(repo, src):
552 def commitfuncfor(repo, src):
545 """Build a commit function for the replacement of <src>
553 """Build a commit function for the replacement of <src>
546
554
547 This function ensure we apply the same treatment to all changesets.
555 This function ensure we apply the same treatment to all changesets.
548
556
549 - Add a 'histedit_source' entry in extra.
557 - Add a 'histedit_source' entry in extra.
550
558
551 Note that fold has its own separated logic because its handling is a bit
559 Note that fold has its own separated logic because its handling is a bit
552 different and not easily factored out of the fold method.
560 different and not easily factored out of the fold method.
553 """
561 """
554 phasemin = src.phase()
562 phasemin = src.phase()
555 def commitfunc(**kwargs):
563 def commitfunc(**kwargs):
556 overrides = {('phases', 'new-commit'): phasemin}
564 overrides = {('phases', 'new-commit'): phasemin}
557 with repo.ui.configoverride(overrides, 'histedit'):
565 with repo.ui.configoverride(overrides, 'histedit'):
558 extra = kwargs.get(r'extra', {}).copy()
566 extra = kwargs.get(r'extra', {}).copy()
559 extra['histedit_source'] = src.hex()
567 extra['histedit_source'] = src.hex()
560 kwargs[r'extra'] = extra
568 kwargs[r'extra'] = extra
561 return repo.commit(**kwargs)
569 return repo.commit(**kwargs)
562 return commitfunc
570 return commitfunc
563
571
564 def applychanges(ui, repo, ctx, opts):
572 def applychanges(ui, repo, ctx, opts):
565 """Merge changeset from ctx (only) in the current working directory"""
573 """Merge changeset from ctx (only) in the current working directory"""
566 wcpar = repo.dirstate.parents()[0]
574 wcpar = repo.dirstate.parents()[0]
567 if ctx.p1().node() == wcpar:
575 if ctx.p1().node() == wcpar:
568 # edits are "in place" we do not need to make any merge,
576 # edits are "in place" we do not need to make any merge,
569 # just applies changes on parent for editing
577 # just applies changes on parent for editing
570 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
578 cmdutil.revert(ui, repo, ctx, (wcpar, node.nullid), all=True)
571 stats = mergemod.updateresult(0, 0, 0, 0)
579 stats = mergemod.updateresult(0, 0, 0, 0)
572 else:
580 else:
573 try:
581 try:
574 # ui.forcemerge is an internal variable, do not document
582 # ui.forcemerge is an internal variable, do not document
575 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
583 repo.ui.setconfig('ui', 'forcemerge', opts.get('tool', ''),
576 'histedit')
584 'histedit')
577 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
585 stats = mergemod.graft(repo, ctx, ctx.p1(), ['local', 'histedit'])
578 finally:
586 finally:
579 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
587 repo.ui.setconfig('ui', 'forcemerge', '', 'histedit')
580 return stats
588 return stats
581
589
582 def collapse(repo, firstctx, lastctx, commitopts, skipprompt=False):
590 def collapse(repo, firstctx, lastctx, commitopts, skipprompt=False):
583 """collapse the set of revisions from first to last as new one.
591 """collapse the set of revisions from first to last as new one.
584
592
585 Expected commit options are:
593 Expected commit options are:
586 - message
594 - message
587 - date
595 - date
588 - username
596 - username
589 Commit message is edited in all cases.
597 Commit message is edited in all cases.
590
598
591 This function works in memory."""
599 This function works in memory."""
592 ctxs = list(repo.set('%d::%d', firstctx.rev(), lastctx.rev()))
600 ctxs = list(repo.set('%d::%d', firstctx.rev(), lastctx.rev()))
593 if not ctxs:
601 if not ctxs:
594 return None
602 return None
595 for c in ctxs:
603 for c in ctxs:
596 if not c.mutable():
604 if not c.mutable():
597 raise error.ParseError(
605 raise error.ParseError(
598 _("cannot fold into public change %s") % node.short(c.node()))
606 _("cannot fold into public change %s") % node.short(c.node()))
599 base = firstctx.parents()[0]
607 base = firstctx.parents()[0]
600
608
601 # commit a new version of the old changeset, including the update
609 # commit a new version of the old changeset, including the update
602 # collect all files which might be affected
610 # collect all files which might be affected
603 files = set()
611 files = set()
604 for ctx in ctxs:
612 for ctx in ctxs:
605 files.update(ctx.files())
613 files.update(ctx.files())
606
614
607 # Recompute copies (avoid recording a -> b -> a)
615 # Recompute copies (avoid recording a -> b -> a)
608 copied = copies.pathcopies(base, lastctx)
616 copied = copies.pathcopies(base, lastctx)
609
617
610 # prune files which were reverted by the updates
618 # prune files which were reverted by the updates
611 files = [f for f in files if not cmdutil.samefile(f, lastctx, base)]
619 files = [f for f in files if not cmdutil.samefile(f, lastctx, base)]
612 # commit version of these files as defined by head
620 # commit version of these files as defined by head
613 headmf = lastctx.manifest()
621 headmf = lastctx.manifest()
614 def filectxfn(repo, ctx, path):
622 def filectxfn(repo, ctx, path):
615 if path in headmf:
623 if path in headmf:
616 fctx = lastctx[path]
624 fctx = lastctx[path]
617 flags = fctx.flags()
625 flags = fctx.flags()
618 mctx = context.memfilectx(repo, ctx,
626 mctx = context.memfilectx(repo, ctx,
619 fctx.path(), fctx.data(),
627 fctx.path(), fctx.data(),
620 islink='l' in flags,
628 islink='l' in flags,
621 isexec='x' in flags,
629 isexec='x' in flags,
622 copied=copied.get(path))
630 copied=copied.get(path))
623 return mctx
631 return mctx
624 return None
632 return None
625
633
626 if commitopts.get('message'):
634 if commitopts.get('message'):
627 message = commitopts['message']
635 message = commitopts['message']
628 else:
636 else:
629 message = firstctx.description()
637 message = firstctx.description()
630 user = commitopts.get('user')
638 user = commitopts.get('user')
631 date = commitopts.get('date')
639 date = commitopts.get('date')
632 extra = commitopts.get('extra')
640 extra = commitopts.get('extra')
633
641
634 parents = (firstctx.p1().node(), firstctx.p2().node())
642 parents = (firstctx.p1().node(), firstctx.p2().node())
635 editor = None
643 editor = None
636 if not skipprompt:
644 if not skipprompt:
637 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
645 editor = cmdutil.getcommiteditor(edit=True, editform='histedit.fold')
638 new = context.memctx(repo,
646 new = context.memctx(repo,
639 parents=parents,
647 parents=parents,
640 text=message,
648 text=message,
641 files=files,
649 files=files,
642 filectxfn=filectxfn,
650 filectxfn=filectxfn,
643 user=user,
651 user=user,
644 date=date,
652 date=date,
645 extra=extra,
653 extra=extra,
646 editor=editor)
654 editor=editor)
647 return repo.commitctx(new)
655 return repo.commitctx(new)
648
656
649 def _isdirtywc(repo):
657 def _isdirtywc(repo):
650 return repo[None].dirty(missing=True)
658 return repo[None].dirty(missing=True)
651
659
652 def abortdirty():
660 def abortdirty():
653 raise error.Abort(_('working copy has pending changes'),
661 raise error.Abort(_('working copy has pending changes'),
654 hint=_('amend, commit, or revert them and run histedit '
662 hint=_('amend, commit, or revert them and run histedit '
655 '--continue, or abort with histedit --abort'))
663 '--continue, or abort with histedit --abort'))
656
664
657 def action(verbs, message, priority=False, internal=False):
665 def action(verbs, message, priority=False, internal=False):
658 def wrap(cls):
666 def wrap(cls):
659 assert not priority or not internal
667 assert not priority or not internal
660 verb = verbs[0]
668 verb = verbs[0]
661 if priority:
669 if priority:
662 primaryactions.add(verb)
670 primaryactions.add(verb)
663 elif internal:
671 elif internal:
664 internalactions.add(verb)
672 internalactions.add(verb)
665 elif len(verbs) > 1:
673 elif len(verbs) > 1:
666 secondaryactions.add(verb)
674 secondaryactions.add(verb)
667 else:
675 else:
668 tertiaryactions.add(verb)
676 tertiaryactions.add(verb)
669
677
670 cls.verb = verb
678 cls.verb = verb
671 cls.verbs = verbs
679 cls.verbs = verbs
672 cls.message = message
680 cls.message = message
673 for verb in verbs:
681 for verb in verbs:
674 actiontable[verb] = cls
682 actiontable[verb] = cls
675 return cls
683 return cls
676 return wrap
684 return wrap
677
685
678 @action(['pick', 'p'],
686 @action(['pick', 'p'],
679 _('use commit'),
687 _('use commit'),
680 priority=True)
688 priority=True)
681 class pick(histeditaction):
689 class pick(histeditaction):
682 def run(self):
690 def run(self):
683 rulectx = self.repo[self.node]
691 rulectx = self.repo[self.node]
684 if rulectx.parents()[0].node() == self.state.parentctxnode:
692 if rulectx.parents()[0].node() == self.state.parentctxnode:
685 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
693 self.repo.ui.debug('node %s unchanged\n' % node.short(self.node))
686 return rulectx, []
694 return rulectx, []
687
695
688 return super(pick, self).run()
696 return super(pick, self).run()
689
697
690 @action(['edit', 'e'],
698 @action(['edit', 'e'],
691 _('use commit, but stop for amending'),
699 _('use commit, but stop for amending'),
692 priority=True)
700 priority=True)
693 class edit(histeditaction):
701 class edit(histeditaction):
694 def run(self):
702 def run(self):
695 repo = self.repo
703 repo = self.repo
696 rulectx = repo[self.node]
704 rulectx = repo[self.node]
697 hg.update(repo, self.state.parentctxnode, quietempty=True)
705 hg.update(repo, self.state.parentctxnode, quietempty=True)
698 applychanges(repo.ui, repo, rulectx, {})
706 applychanges(repo.ui, repo, rulectx, {})
699 raise error.InterventionRequired(
707 raise error.InterventionRequired(
700 _('Editing (%s), you may commit or record as needed now.')
708 _('Editing (%s), you may commit or record as needed now.')
701 % node.short(self.node),
709 % node.short(self.node),
702 hint=_('hg histedit --continue to resume'))
710 hint=_('hg histedit --continue to resume'))
703
711
704 def commiteditor(self):
712 def commiteditor(self):
705 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
713 return cmdutil.getcommiteditor(edit=True, editform='histedit.edit')
706
714
707 @action(['fold', 'f'],
715 @action(['fold', 'f'],
708 _('use commit, but combine it with the one above'))
716 _('use commit, but combine it with the one above'))
709 class fold(histeditaction):
717 class fold(histeditaction):
710 def verify(self, prev, expected, seen):
718 def verify(self, prev, expected, seen):
711 """ Verifies semantic correctness of the fold rule"""
719 """ Verifies semantic correctness of the fold rule"""
712 super(fold, self).verify(prev, expected, seen)
720 super(fold, self).verify(prev, expected, seen)
713 repo = self.repo
721 repo = self.repo
714 if not prev:
722 if not prev:
715 c = repo[self.node].parents()[0]
723 c = repo[self.node].parents()[0]
716 elif not prev.verb in ('pick', 'base'):
724 elif not prev.verb in ('pick', 'base'):
717 return
725 return
718 else:
726 else:
719 c = repo[prev.node]
727 c = repo[prev.node]
720 if not c.mutable():
728 if not c.mutable():
721 raise error.ParseError(
729 raise error.ParseError(
722 _("cannot fold into public change %s") % node.short(c.node()))
730 _("cannot fold into public change %s") % node.short(c.node()))
723
731
724
732
725 def continuedirty(self):
733 def continuedirty(self):
726 repo = self.repo
734 repo = self.repo
727 rulectx = repo[self.node]
735 rulectx = repo[self.node]
728
736
729 commit = commitfuncfor(repo, rulectx)
737 commit = commitfuncfor(repo, rulectx)
730 commit(text='fold-temp-revision %s' % node.short(self.node),
738 commit(text='fold-temp-revision %s' % node.short(self.node),
731 user=rulectx.user(), date=rulectx.date(),
739 user=rulectx.user(), date=rulectx.date(),
732 extra=rulectx.extra())
740 extra=rulectx.extra())
733
741
734 def continueclean(self):
742 def continueclean(self):
735 repo = self.repo
743 repo = self.repo
736 ctx = repo['.']
744 ctx = repo['.']
737 rulectx = repo[self.node]
745 rulectx = repo[self.node]
738 parentctxnode = self.state.parentctxnode
746 parentctxnode = self.state.parentctxnode
739 if ctx.node() == parentctxnode:
747 if ctx.node() == parentctxnode:
740 repo.ui.warn(_('%s: empty changeset\n') %
748 repo.ui.warn(_('%s: empty changeset\n') %
741 node.short(self.node))
749 node.short(self.node))
742 return ctx, [(self.node, (parentctxnode,))]
750 return ctx, [(self.node, (parentctxnode,))]
743
751
744 parentctx = repo[parentctxnode]
752 parentctx = repo[parentctxnode]
745 newcommits = set(c.node() for c in repo.set('(%d::. - %d)',
753 newcommits = set(c.node() for c in repo.set('(%d::. - %d)',
746 parentctx.rev(),
754 parentctx.rev(),
747 parentctx.rev()))
755 parentctx.rev()))
748 if not newcommits:
756 if not newcommits:
749 repo.ui.warn(_('%s: cannot fold - working copy is not a '
757 repo.ui.warn(_('%s: cannot fold - working copy is not a '
750 'descendant of previous commit %s\n') %
758 'descendant of previous commit %s\n') %
751 (node.short(self.node), node.short(parentctxnode)))
759 (node.short(self.node), node.short(parentctxnode)))
752 return ctx, [(self.node, (ctx.node(),))]
760 return ctx, [(self.node, (ctx.node(),))]
753
761
754 middlecommits = newcommits.copy()
762 middlecommits = newcommits.copy()
755 middlecommits.discard(ctx.node())
763 middlecommits.discard(ctx.node())
756
764
757 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
765 return self.finishfold(repo.ui, repo, parentctx, rulectx, ctx.node(),
758 middlecommits)
766 middlecommits)
759
767
760 def skipprompt(self):
768 def skipprompt(self):
761 """Returns true if the rule should skip the message editor.
769 """Returns true if the rule should skip the message editor.
762
770
763 For example, 'fold' wants to show an editor, but 'rollup'
771 For example, 'fold' wants to show an editor, but 'rollup'
764 doesn't want to.
772 doesn't want to.
765 """
773 """
766 return False
774 return False
767
775
768 def mergedescs(self):
776 def mergedescs(self):
769 """Returns true if the rule should merge messages of multiple changes.
777 """Returns true if the rule should merge messages of multiple changes.
770
778
771 This exists mainly so that 'rollup' rules can be a subclass of
779 This exists mainly so that 'rollup' rules can be a subclass of
772 'fold'.
780 'fold'.
773 """
781 """
774 return True
782 return True
775
783
776 def firstdate(self):
784 def firstdate(self):
777 """Returns true if the rule should preserve the date of the first
785 """Returns true if the rule should preserve the date of the first
778 change.
786 change.
779
787
780 This exists mainly so that 'rollup' rules can be a subclass of
788 This exists mainly so that 'rollup' rules can be a subclass of
781 'fold'.
789 'fold'.
782 """
790 """
783 return False
791 return False
784
792
785 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
793 def finishfold(self, ui, repo, ctx, oldctx, newnode, internalchanges):
786 parent = ctx.parents()[0].node()
794 parent = ctx.parents()[0].node()
787 hg.updaterepo(repo, parent, overwrite=False)
795 hg.updaterepo(repo, parent, overwrite=False)
788 ### prepare new commit data
796 ### prepare new commit data
789 commitopts = {}
797 commitopts = {}
790 commitopts['user'] = ctx.user()
798 commitopts['user'] = ctx.user()
791 # commit message
799 # commit message
792 if not self.mergedescs():
800 if not self.mergedescs():
793 newmessage = ctx.description()
801 newmessage = ctx.description()
794 else:
802 else:
795 newmessage = '\n***\n'.join(
803 newmessage = '\n***\n'.join(
796 [ctx.description()] +
804 [ctx.description()] +
797 [repo[r].description() for r in internalchanges] +
805 [repo[r].description() for r in internalchanges] +
798 [oldctx.description()]) + '\n'
806 [oldctx.description()]) + '\n'
799 commitopts['message'] = newmessage
807 commitopts['message'] = newmessage
800 # date
808 # date
801 if self.firstdate():
809 if self.firstdate():
802 commitopts['date'] = ctx.date()
810 commitopts['date'] = ctx.date()
803 else:
811 else:
804 commitopts['date'] = max(ctx.date(), oldctx.date())
812 commitopts['date'] = max(ctx.date(), oldctx.date())
805 extra = ctx.extra().copy()
813 extra = ctx.extra().copy()
806 # histedit_source
814 # histedit_source
807 # note: ctx is likely a temporary commit but that the best we can do
815 # note: ctx is likely a temporary commit but that the best we can do
808 # here. This is sufficient to solve issue3681 anyway.
816 # here. This is sufficient to solve issue3681 anyway.
809 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
817 extra['histedit_source'] = '%s,%s' % (ctx.hex(), oldctx.hex())
810 commitopts['extra'] = extra
818 commitopts['extra'] = extra
811 phasemin = max(ctx.phase(), oldctx.phase())
819 phasemin = max(ctx.phase(), oldctx.phase())
812 overrides = {('phases', 'new-commit'): phasemin}
820 overrides = {('phases', 'new-commit'): phasemin}
813 with repo.ui.configoverride(overrides, 'histedit'):
821 with repo.ui.configoverride(overrides, 'histedit'):
814 n = collapse(repo, ctx, repo[newnode], commitopts,
822 n = collapse(repo, ctx, repo[newnode], commitopts,
815 skipprompt=self.skipprompt())
823 skipprompt=self.skipprompt())
816 if n is None:
824 if n is None:
817 return ctx, []
825 return ctx, []
818 hg.updaterepo(repo, n, overwrite=False)
826 hg.updaterepo(repo, n, overwrite=False)
819 replacements = [(oldctx.node(), (newnode,)),
827 replacements = [(oldctx.node(), (newnode,)),
820 (ctx.node(), (n,)),
828 (ctx.node(), (n,)),
821 (newnode, (n,)),
829 (newnode, (n,)),
822 ]
830 ]
823 for ich in internalchanges:
831 for ich in internalchanges:
824 replacements.append((ich, (n,)))
832 replacements.append((ich, (n,)))
825 return repo[n], replacements
833 return repo[n], replacements
826
834
827 @action(['base', 'b'],
835 @action(['base', 'b'],
828 _('checkout changeset and apply further changesets from there'))
836 _('checkout changeset and apply further changesets from there'))
829 class base(histeditaction):
837 class base(histeditaction):
830
838
831 def run(self):
839 def run(self):
832 if self.repo['.'].node() != self.node:
840 if self.repo['.'].node() != self.node:
833 mergemod.update(self.repo, self.node, branchmerge=False, force=True)
841 mergemod.update(self.repo, self.node, branchmerge=False, force=True)
834 return self.continueclean()
842 return self.continueclean()
835
843
836 def continuedirty(self):
844 def continuedirty(self):
837 abortdirty()
845 abortdirty()
838
846
839 def continueclean(self):
847 def continueclean(self):
840 basectx = self.repo['.']
848 basectx = self.repo['.']
841 return basectx, []
849 return basectx, []
842
850
843 def _verifynodeconstraints(self, prev, expected, seen):
851 def _verifynodeconstraints(self, prev, expected, seen):
844 # base can only be use with a node not in the edited set
852 # base can only be use with a node not in the edited set
845 if self.node in expected:
853 if self.node in expected:
846 msg = _('%s "%s" changeset was an edited list candidate')
854 msg = _('%s "%s" changeset was an edited list candidate')
847 raise error.ParseError(
855 raise error.ParseError(
848 msg % (self.verb, node.short(self.node)),
856 msg % (self.verb, node.short(self.node)),
849 hint=_('base must only use unlisted changesets'))
857 hint=_('base must only use unlisted changesets'))
850
858
851 @action(['_multifold'],
859 @action(['_multifold'],
852 _(
860 _(
853 """fold subclass used for when multiple folds happen in a row
861 """fold subclass used for when multiple folds happen in a row
854
862
855 We only want to fire the editor for the folded message once when
863 We only want to fire the editor for the folded message once when
856 (say) four changes are folded down into a single change. This is
864 (say) four changes are folded down into a single change. This is
857 similar to rollup, but we should preserve both messages so that
865 similar to rollup, but we should preserve both messages so that
858 when the last fold operation runs we can show the user all the
866 when the last fold operation runs we can show the user all the
859 commit messages in their editor.
867 commit messages in their editor.
860 """),
868 """),
861 internal=True)
869 internal=True)
862 class _multifold(fold):
870 class _multifold(fold):
863 def skipprompt(self):
871 def skipprompt(self):
864 return True
872 return True
865
873
866 @action(["roll", "r"],
874 @action(["roll", "r"],
867 _("like fold, but discard this commit's description and date"))
875 _("like fold, but discard this commit's description and date"))
868 class rollup(fold):
876 class rollup(fold):
869 def mergedescs(self):
877 def mergedescs(self):
870 return False
878 return False
871
879
872 def skipprompt(self):
880 def skipprompt(self):
873 return True
881 return True
874
882
875 def firstdate(self):
883 def firstdate(self):
876 return True
884 return True
877
885
878 @action(["drop", "d"],
886 @action(["drop", "d"],
879 _('remove commit from history'))
887 _('remove commit from history'))
880 class drop(histeditaction):
888 class drop(histeditaction):
881 def run(self):
889 def run(self):
882 parentctx = self.repo[self.state.parentctxnode]
890 parentctx = self.repo[self.state.parentctxnode]
883 return parentctx, [(self.node, tuple())]
891 return parentctx, [(self.node, tuple())]
884
892
885 @action(["mess", "m"],
893 @action(["mess", "m"],
886 _('edit commit message without changing commit content'),
894 _('edit commit message without changing commit content'),
887 priority=True)
895 priority=True)
888 class message(histeditaction):
896 class message(histeditaction):
889 def commiteditor(self):
897 def commiteditor(self):
890 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
898 return cmdutil.getcommiteditor(edit=True, editform='histedit.mess')
891
899
892 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
900 def findoutgoing(ui, repo, remote=None, force=False, opts=None):
893 """utility function to find the first outgoing changeset
901 """utility function to find the first outgoing changeset
894
902
895 Used by initialization code"""
903 Used by initialization code"""
896 if opts is None:
904 if opts is None:
897 opts = {}
905 opts = {}
898 dest = ui.expandpath(remote or 'default-push', remote or 'default')
906 dest = ui.expandpath(remote or 'default-push', remote or 'default')
899 dest, branches = hg.parseurl(dest, None)[:2]
907 dest, branches = hg.parseurl(dest, None)[:2]
900 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
908 ui.status(_('comparing with %s\n') % util.hidepassword(dest))
901
909
902 revs, checkout = hg.addbranchrevs(repo, repo, branches, None)
910 revs, checkout = hg.addbranchrevs(repo, repo, branches, None)
903 other = hg.peer(repo, opts, dest)
911 other = hg.peer(repo, opts, dest)
904
912
905 if revs:
913 if revs:
906 revs = [repo.lookup(rev) for rev in revs]
914 revs = [repo.lookup(rev) for rev in revs]
907
915
908 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
916 outgoing = discovery.findcommonoutgoing(repo, other, revs, force=force)
909 if not outgoing.missing:
917 if not outgoing.missing:
910 raise error.Abort(_('no outgoing ancestors'))
918 raise error.Abort(_('no outgoing ancestors'))
911 roots = list(repo.revs("roots(%ln)", outgoing.missing))
919 roots = list(repo.revs("roots(%ln)", outgoing.missing))
912 if len(roots) > 1:
920 if len(roots) > 1:
913 msg = _('there are ambiguous outgoing revisions')
921 msg = _('there are ambiguous outgoing revisions')
914 hint = _("see 'hg help histedit' for more detail")
922 hint = _("see 'hg help histedit' for more detail")
915 raise error.Abort(msg, hint=hint)
923 raise error.Abort(msg, hint=hint)
916 return repo[roots[0]].node()
924 return repo[roots[0]].node()
917
925
926 # Curses Support
927 try:
928 import curses
929 except ImportError:
930 curses = None
931
932 KEY_LIST = ['pick', 'edit', 'fold', 'drop', 'mess', 'roll']
933 ACTION_LABELS = {
934 'fold': '^fold',
935 'roll': '^roll',
936 }
937
938 COLOR_HELP, COLOR_SELECTED, COLOR_OK, COLOR_WARN = 1, 2, 3, 4
939
940 E_QUIT, E_HISTEDIT = 1, 2
941 E_PAGEDOWN, E_PAGEUP, E_LINEUP, E_LINEDOWN, E_RESIZE = 3, 4, 5, 6, 7
942 MODE_INIT, MODE_PATCH, MODE_RULES, MODE_HELP = 0, 1, 2, 3
943
944 KEYTABLE = {
945 'global': {
946 'h': 'next-action',
947 'KEY_RIGHT': 'next-action',
948 'l': 'prev-action',
949 'KEY_LEFT': 'prev-action',
950 'q': 'quit',
951 'c': 'histedit',
952 'C': 'histedit',
953 'v': 'showpatch',
954 '?': 'help',
955 },
956 MODE_RULES: {
957 'd': 'action-drop',
958 'e': 'action-edit',
959 'f': 'action-fold',
960 'm': 'action-mess',
961 'p': 'action-pick',
962 'r': 'action-roll',
963 ' ': 'select',
964 'j': 'down',
965 'k': 'up',
966 'KEY_DOWN': 'down',
967 'KEY_UP': 'up',
968 'J': 'move-down',
969 'K': 'move-up',
970 'KEY_NPAGE': 'move-down',
971 'KEY_PPAGE': 'move-up',
972 '0': 'goto', # Used for 0..9
973 },
974 MODE_PATCH: {
975 ' ': 'page-down',
976 'KEY_NPAGE': 'page-down',
977 'KEY_PPAGE': 'page-up',
978 'j': 'line-down',
979 'k': 'line-up',
980 'KEY_DOWN': 'line-down',
981 'KEY_UP': 'line-up',
982 'J': 'down',
983 'K': 'up',
984 },
985 MODE_HELP: {
986 },
987 }
988
989 def screen_size():
990 return struct.unpack('hh', fcntl.ioctl(1, termios.TIOCGWINSZ, ' '))
991
992 class histeditrule(object):
993 def __init__(self, ctx, pos, action='pick'):
994 self.ctx = ctx
995 self.action = action
996 self.origpos = pos
997 self.pos = pos
998 self.conflicts = []
999
1000 def __str__(self):
1001 # Some actions ('fold' and 'roll') combine a patch with a previous one.
1002 # Add a marker showing which patch they apply to, and also omit the
1003 # description for 'roll' (since it will get discarded). Example display:
1004 #
1005 # #10 pick 316392:06a16c25c053 add option to skip tests
1006 # #11 ^roll 316393:71313c964cc5
1007 # #12 pick 316394:ab31f3973b0d include mfbt for mozilla-config.h
1008 # #13 ^fold 316395:14ce5803f4c3 fix warnings
1009 #
1010 # The carets point to the changeset being folded into ("roll this
1011 # changeset into the changeset above").
1012 action = ACTION_LABELS.get(self.action, self.action)
1013 h = self.ctx.hex()[0:12]
1014 r = self.ctx.rev()
1015 desc = self.ctx.description().splitlines()[0].strip()
1016 if self.action == 'roll':
1017 desc = ''
1018 return "#{0:<2} {1:<6} {2}:{3} {4}".format(
1019 self.origpos, action, r, h, desc)
1020
1021 def checkconflicts(self, other):
1022 if other.pos > self.pos and other.origpos <= self.origpos:
1023 if set(other.ctx.files()) & set(self.ctx.files()) != set():
1024 self.conflicts.append(other)
1025 return self.conflicts
1026
1027 if other in self.conflicts:
1028 self.conflicts.remove(other)
1029 return self.conflicts
1030
1031 # ============ EVENTS ===============
1032 def movecursor(state, oldpos, newpos):
1033 '''Change the rule/changeset that the cursor is pointing to, regardless of
1034 current mode (you can switch between patches from the view patch window).'''
1035 state['pos'] = newpos
1036
1037 mode, _ = state['mode']
1038 if mode == MODE_RULES:
1039 # Scroll through the list by updating the view for MODE_RULES, so that
1040 # even if we are not currently viewing the rules, switching back will
1041 # result in the cursor's rule being visible.
1042 modestate = state['modes'][MODE_RULES]
1043 if newpos < modestate['line_offset']:
1044 modestate['line_offset'] = newpos
1045 elif newpos > modestate['line_offset'] + state['page_height'] - 1:
1046 modestate['line_offset'] = newpos - state['page_height'] + 1
1047
1048 # Reset the patch view region to the top of the new patch.
1049 state['modes'][MODE_PATCH]['line_offset'] = 0
1050
1051 def changemode(state, mode):
1052 curmode, _ = state['mode']
1053 state['mode'] = (mode, curmode)
1054
1055 def makeselection(state, pos):
1056 state['selected'] = pos
1057
1058 def swap(state, oldpos, newpos):
1059 """Swap two positions and calculate necessary conflicts in
1060 O(|newpos-oldpos|) time"""
1061
1062 rules = state['rules']
1063 assert 0 <= oldpos < len(rules) and 0 <= newpos < len(rules)
1064
1065 rules[oldpos], rules[newpos] = rules[newpos], rules[oldpos]
1066
1067 # TODO: swap should not know about histeditrule's internals
1068 rules[newpos].pos = newpos
1069 rules[oldpos].pos = oldpos
1070
1071 start = min(oldpos, newpos)
1072 end = max(oldpos, newpos)
1073 for r in pycompat.xrange(start, end + 1):
1074 rules[newpos].checkconflicts(rules[r])
1075 rules[oldpos].checkconflicts(rules[r])
1076
1077 if state['selected']:
1078 makeselection(state, newpos)
1079
1080 def changeaction(state, pos, action):
1081 """Change the action state on the given position to the new action"""
1082 rules = state['rules']
1083 assert 0 <= pos < len(rules)
1084 rules[pos].action = action
1085
1086 def cycleaction(state, pos, next=False):
1087 """Changes the action state the next or the previous action from
1088 the action list"""
1089 rules = state['rules']
1090 assert 0 <= pos < len(rules)
1091 current = rules[pos].action
1092
1093 assert current in KEY_LIST
1094
1095 index = KEY_LIST.index(current)
1096 if next:
1097 index += 1
1098 else:
1099 index -= 1
1100 changeaction(state, pos, KEY_LIST[index % len(KEY_LIST)])
1101
1102 def changeview(state, delta, unit):
1103 '''Change the region of whatever is being viewed (a patch or the list of
1104 changesets). 'delta' is an amount (+/- 1) and 'unit' is 'page' or 'line'.'''
1105 mode, _ = state['mode']
1106 if mode != MODE_PATCH:
1107 return
1108 mode_state = state['modes'][mode]
1109 num_lines = len(patchcontents(state))
1110 page_height = state['page_height']
1111 unit = page_height if unit == 'page' else 1
1112 num_pages = 1 + (num_lines - 1) / page_height
1113 max_offset = (num_pages - 1) * page_height
1114 newline = mode_state['line_offset'] + delta * unit
1115 mode_state['line_offset'] = max(0, min(max_offset, newline))
1116
1117 def event(state, ch):
1118 """Change state based on the current character input
1119
1120 This takes the current state and based on the current character input from
1121 the user we change the state.
1122 """
1123 selected = state['selected']
1124 oldpos = state['pos']
1125 rules = state['rules']
1126
1127 if ch in (curses.KEY_RESIZE, "KEY_RESIZE"):
1128 return E_RESIZE
1129
1130 lookup_ch = ch
1131 if '0' <= ch <= '9':
1132 lookup_ch = '0'
1133
1134 curmode, prevmode = state['mode']
1135 action = KEYTABLE[curmode].get(lookup_ch, KEYTABLE['global'].get(lookup_ch))
1136 if action is None:
1137 return
1138 if action in ('down', 'move-down'):
1139 newpos = min(oldpos + 1, len(rules) - 1)
1140 movecursor(state, oldpos, newpos)
1141 if selected is not None or action == 'move-down':
1142 swap(state, oldpos, newpos)
1143 elif action in ('up', 'move-up'):
1144 newpos = max(0, oldpos - 1)
1145 movecursor(state, oldpos, newpos)
1146 if selected is not None or action == 'move-up':
1147 swap(state, oldpos, newpos)
1148 elif action == 'next-action':
1149 cycleaction(state, oldpos, next=True)
1150 elif action == 'prev-action':
1151 cycleaction(state, oldpos, next=False)
1152 elif action == 'select':
1153 selected = oldpos if selected is None else None
1154 makeselection(state, selected)
1155 elif action == 'goto' and int(ch) < len(rules) and len(rules) <= 10:
1156 newrule = next((r for r in rules if r.origpos == int(ch)))
1157 movecursor(state, oldpos, newrule.pos)
1158 if selected is not None:
1159 swap(state, oldpos, newrule.pos)
1160 elif action.startswith('action-'):
1161 changeaction(state, oldpos, action[7:])
1162 elif action == 'showpatch':
1163 changemode(state, MODE_PATCH if curmode != MODE_PATCH else prevmode)
1164 elif action == 'help':
1165 changemode(state, MODE_HELP if curmode != MODE_HELP else prevmode)
1166 elif action == 'quit':
1167 return E_QUIT
1168 elif action == 'histedit':
1169 return E_HISTEDIT
1170 elif action == 'page-down':
1171 return E_PAGEDOWN
1172 elif action == 'page-up':
1173 return E_PAGEUP
1174 elif action == 'line-down':
1175 return E_LINEDOWN
1176 elif action == 'line-up':
1177 return E_LINEUP
1178
1179 def makecommands(rules):
1180 """Returns a list of commands consumable by histedit --commands based on
1181 our list of rules"""
1182 commands = []
1183 for rules in rules:
1184 commands.append("{0} {1}\n".format(rules.action, rules.ctx))
1185 return commands
1186
1187 def addln(win, y, x, line, color=None):
1188 """Add a line to the given window left padding but 100% filled with
1189 whitespace characters, so that the color appears on the whole line"""
1190 maxy, maxx = win.getmaxyx()
1191 length = maxx - 1 - x
1192 line = ("{0:<%d}" % length).format(str(line).strip())[:length]
1193 if y < 0:
1194 y = maxy + y
1195 if x < 0:
1196 x = maxx + x
1197 if color:
1198 win.addstr(y, x, line, color)
1199 else:
1200 win.addstr(y, x, line)
1201
1202 def patchcontents(state):
1203 repo = state['repo']
1204 rule = state['rules'][state['pos']]
1205 displayer = logcmdutil.changesetdisplayer(repo.ui, repo, {
1206 'patch': True, 'verbose': True
1207 }, buffered=True)
1208 displayer.show(rule.ctx)
1209 displayer.close()
1210 return displayer.hunk[rule.ctx.rev()].splitlines()
1211
1212 def _chisteditmain(repo, rules, stdscr):
1213 # initialize color pattern
1214 curses.init_pair(COLOR_HELP, curses.COLOR_WHITE, curses.COLOR_BLUE)
1215 curses.init_pair(COLOR_SELECTED, curses.COLOR_BLACK, curses.COLOR_WHITE)
1216 curses.init_pair(COLOR_WARN, curses.COLOR_BLACK, curses.COLOR_YELLOW)
1217 curses.init_pair(COLOR_OK, curses.COLOR_BLACK, curses.COLOR_GREEN)
1218
1219 # don't display the cursor
1220 try:
1221 curses.curs_set(0)
1222 except curses.error:
1223 pass
1224
1225 def rendercommit(win, state):
1226 """Renders the commit window that shows the log of the current selected
1227 commit"""
1228 pos = state['pos']
1229 rules = state['rules']
1230 rule = rules[pos]
1231
1232 ctx = rule.ctx
1233 win.box()
1234
1235 maxy, maxx = win.getmaxyx()
1236 length = maxx - 3
1237
1238 line = "changeset: {0}:{1:<12}".format(ctx.rev(), ctx)
1239 win.addstr(1, 1, line[:length])
1240
1241 line = "user: {0}".format(stringutil.shortuser(ctx.user()))
1242 win.addstr(2, 1, line[:length])
1243
1244 bms = repo.nodebookmarks(ctx.node())
1245 line = "bookmark: {0}".format(' '.join(bms))
1246 win.addstr(3, 1, line[:length])
1247
1248 line = "files: {0}".format(','.join(ctx.files()))
1249 win.addstr(4, 1, line[:length])
1250
1251 line = "summary: {0}".format(ctx.description().splitlines()[0])
1252 win.addstr(5, 1, line[:length])
1253
1254 conflicts = rule.conflicts
1255 if len(conflicts) > 0:
1256 conflictstr = ','.join(map(lambda r: str(r.ctx), conflicts))
1257 conflictstr = "changed files overlap with {0}".format(conflictstr)
1258 else:
1259 conflictstr = 'no overlap'
1260
1261 win.addstr(6, 1, conflictstr[:length])
1262 win.noutrefresh()
1263
1264 def helplines(mode):
1265 if mode == MODE_PATCH:
1266 help = """\
1267 ?: help, k/up: line up, j/down: line down, v: stop viewing patch
1268 pgup: prev page, space/pgdn: next page, c: commit, q: abort
1269 """
1270 else:
1271 help = """\
1272 ?: help, k/up: move up, j/down: move down, space: select, v: view patch
1273 d: drop, e: edit, f: fold, m: mess, p: pick, r: roll
1274 pgup/K: move patch up, pgdn/J: move patch down, c: commit, q: abort
1275 """
1276 return help.splitlines()
1277
1278 def renderhelp(win, state):
1279 maxy, maxx = win.getmaxyx()
1280 mode, _ = state['mode']
1281 for y, line in enumerate(helplines(mode)):
1282 if y >= maxy:
1283 break
1284 addln(win, y, 0, line, curses.color_pair(COLOR_HELP))
1285 win.noutrefresh()
1286
1287 def renderrules(rulesscr, state):
1288 rules = state['rules']
1289 pos = state['pos']
1290 selected = state['selected']
1291 start = state['modes'][MODE_RULES]['line_offset']
1292
1293 conflicts = [r.ctx for r in rules if r.conflicts]
1294 if len(conflicts) > 0:
1295 line = "potential conflict in %s" % ','.join(map(str, conflicts))
1296 addln(rulesscr, -1, 0, line, curses.color_pair(COLOR_WARN))
1297
1298 for y, rule in enumerate(rules[start:]):
1299 if y >= state['page_height']:
1300 break
1301 if len(rule.conflicts) > 0:
1302 rulesscr.addstr(y, 0, " ", curses.color_pair(COLOR_WARN))
1303 else:
1304 rulesscr.addstr(y, 0, " ", curses.COLOR_BLACK)
1305 if y + start == selected:
1306 addln(rulesscr, y, 2, rule, curses.color_pair(COLOR_SELECTED))
1307 elif y + start == pos:
1308 addln(rulesscr, y, 2, rule, curses.A_BOLD)
1309 else:
1310 addln(rulesscr, y, 2, rule)
1311 rulesscr.noutrefresh()
1312
1313 def renderstring(win, state, output):
1314 maxy, maxx = win.getmaxyx()
1315 length = min(maxy - 1, len(output))
1316 for y in range(0, length):
1317 win.addstr(y, 0, output[y])
1318 win.noutrefresh()
1319
1320 def renderpatch(win, state):
1321 start = state['modes'][MODE_PATCH]['line_offset']
1322 renderstring(win, state, patchcontents(state)[start:])
1323
1324 def layout(mode):
1325 maxy, maxx = stdscr.getmaxyx()
1326 helplen = len(helplines(mode))
1327 return {
1328 'commit': (8, maxx),
1329 'help': (helplen, maxx),
1330 'main': (maxy - helplen - 8, maxx),
1331 }
1332
1333 def drawvertwin(size, y, x):
1334 win = curses.newwin(size[0], size[1], y, x)
1335 y += size[0]
1336 return win, y, x
1337
1338 state = {
1339 'pos': 0,
1340 'rules': rules,
1341 'selected': None,
1342 'mode': (MODE_INIT, MODE_INIT),
1343 'page_height': None,
1344 'modes': {
1345 MODE_RULES: {
1346 'line_offset': 0,
1347 },
1348 MODE_PATCH: {
1349 'line_offset': 0,
1350 }
1351 },
1352 'repo': repo,
1353 }
1354
1355 # eventloop
1356 ch = None
1357 stdscr.clear()
1358 stdscr.refresh()
1359 while True:
1360 try:
1361 oldmode, _ = state['mode']
1362 if oldmode == MODE_INIT:
1363 changemode(state, MODE_RULES)
1364 e = event(state, ch)
1365
1366 if e == E_QUIT:
1367 return False
1368 if e == E_HISTEDIT:
1369 return state['rules']
1370 else:
1371 if e == E_RESIZE:
1372 size = screen_size()
1373 if size != stdscr.getmaxyx():
1374 curses.resizeterm(*size)
1375
1376 curmode, _ = state['mode']
1377 sizes = layout(curmode)
1378 if curmode != oldmode:
1379 state['page_height'] = sizes['main'][0]
1380 # Adjust the view to fit the current screen size.
1381 movecursor(state, state['pos'], state['pos'])
1382
1383 # Pack the windows against the top, each pane spread across the
1384 # full width of the screen.
1385 y, x = (0, 0)
1386 helpwin, y, x = drawvertwin(sizes['help'], y, x)
1387 mainwin, y, x = drawvertwin(sizes['main'], y, x)
1388 commitwin, y, x = drawvertwin(sizes['commit'], y, x)
1389
1390 if e in (E_PAGEDOWN, E_PAGEUP, E_LINEDOWN, E_LINEUP):
1391 if e == E_PAGEDOWN:
1392 changeview(state, +1, 'page')
1393 elif e == E_PAGEUP:
1394 changeview(state, -1, 'page')
1395 elif e == E_LINEDOWN:
1396 changeview(state, +1, 'line')
1397 elif e == E_LINEUP:
1398 changeview(state, -1, 'line')
1399
1400 # start rendering
1401 commitwin.erase()
1402 helpwin.erase()
1403 mainwin.erase()
1404 if curmode == MODE_PATCH:
1405 renderpatch(mainwin, state)
1406 elif curmode == MODE_HELP:
1407 renderstring(mainwin, state, __doc__.strip().splitlines())
1408 else:
1409 renderrules(mainwin, state)
1410 rendercommit(commitwin, state)
1411 renderhelp(helpwin, state)
1412 curses.doupdate()
1413 # done rendering
1414 ch = stdscr.getkey()
1415 except curses.error:
1416 pass
1417
1418 def _chistedit(ui, repo, *freeargs, **opts):
1419 """interactively edit changeset history via a curses interface
1420
1421 Provides a ncurses interface to histedit. Press ? in chistedit mode
1422 to see an extensive help. Requires python-curses to be installed."""
1423
1424 if curses is None:
1425 raise error.Abort(_("Python curses library required"))
1426
1427 # disable color
1428 ui._colormode = None
1429
1430 try:
1431 keep = opts.get('keep')
1432 revs = opts.get('rev', [])[:]
1433 cmdutil.checkunfinished(repo)
1434 cmdutil.bailifchanged(repo)
1435
1436 if os.path.exists(os.path.join(repo.path, 'histedit-state')):
1437 raise error.Abort(_('history edit already in progress, try '
1438 '--continue or --abort'))
1439 revs.extend(freeargs)
1440 if not revs:
1441 defaultrev = destutil.desthistedit(ui, repo)
1442 if defaultrev is not None:
1443 revs.append(defaultrev)
1444 if len(revs) != 1:
1445 raise error.Abort(
1446 _('histedit requires exactly one ancestor revision'))
1447
1448 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1449 if len(rr) != 1:
1450 raise error.Abort(_('The specified revisions must have '
1451 'exactly one common root'))
1452 root = rr[0].node()
1453
1454 topmost, empty = repo.dirstate.parents()
1455 revs = between(repo, root, topmost, keep)
1456 if not revs:
1457 raise error.Abort(_('%s is not an ancestor of working directory') %
1458 node.short(root))
1459
1460 ctxs = []
1461 for i, r in enumerate(revs):
1462 ctxs.append(histeditrule(repo[r], i))
1463 rc = curses.wrapper(functools.partial(_chisteditmain, repo, ctxs))
1464 curses.echo()
1465 curses.endwin()
1466 if rc is False:
1467 ui.write(_("chistedit aborted\n"))
1468 return 0
1469 if type(rc) is list:
1470 ui.status(_("running histedit\n"))
1471 rules = makecommands(rc)
1472 filename = repo.vfs.join('chistedit')
1473 with open(filename, 'w+') as fp:
1474 for r in rules:
1475 fp.write(r)
1476 opts['commands'] = filename
1477 return _texthistedit(ui, repo, *freeargs, **opts)
1478 except KeyboardInterrupt:
1479 pass
1480 return -1
1481
918 @command('histedit',
1482 @command('histedit',
919 [('', 'commands', '',
1483 [('', 'commands', '',
920 _('read history edits from the specified file'), _('FILE')),
1484 _('read history edits from the specified file'), _('FILE')),
921 ('c', 'continue', False, _('continue an edit already in progress')),
1485 ('c', 'continue', False, _('continue an edit already in progress')),
922 ('', 'edit-plan', False, _('edit remaining actions list')),
1486 ('', 'edit-plan', False, _('edit remaining actions list')),
923 ('k', 'keep', False,
1487 ('k', 'keep', False,
924 _("don't strip old nodes after edit is complete")),
1488 _("don't strip old nodes after edit is complete")),
925 ('', 'abort', False, _('abort an edit in progress')),
1489 ('', 'abort', False, _('abort an edit in progress')),
926 ('o', 'outgoing', False, _('changesets not found in destination')),
1490 ('o', 'outgoing', False, _('changesets not found in destination')),
927 ('f', 'force', False,
1491 ('f', 'force', False,
928 _('force outgoing even for unrelated repositories')),
1492 _('force outgoing even for unrelated repositories')),
929 ('r', 'rev', [], _('first revision to be edited'), _('REV'))] +
1493 ('r', 'rev', [], _('first revision to be edited'), _('REV'))] +
930 cmdutil.formatteropts,
1494 cmdutil.formatteropts,
931 _("[OPTIONS] ([ANCESTOR] | --outgoing [URL])"),
1495 _("[OPTIONS] ([ANCESTOR] | --outgoing [URL])"),
932 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT)
1496 helpcategory=command.CATEGORY_CHANGE_MANAGEMENT)
933 def histedit(ui, repo, *freeargs, **opts):
1497 def histedit(ui, repo, *freeargs, **opts):
934 """interactively edit changeset history
1498 """interactively edit changeset history
935
1499
936 This command lets you edit a linear series of changesets (up to
1500 This command lets you edit a linear series of changesets (up to
937 and including the working directory, which should be clean).
1501 and including the working directory, which should be clean).
938 You can:
1502 You can:
939
1503
940 - `pick` to [re]order a changeset
1504 - `pick` to [re]order a changeset
941
1505
942 - `drop` to omit changeset
1506 - `drop` to omit changeset
943
1507
944 - `mess` to reword the changeset commit message
1508 - `mess` to reword the changeset commit message
945
1509
946 - `fold` to combine it with the preceding changeset (using the later date)
1510 - `fold` to combine it with the preceding changeset (using the later date)
947
1511
948 - `roll` like fold, but discarding this commit's description and date
1512 - `roll` like fold, but discarding this commit's description and date
949
1513
950 - `edit` to edit this changeset (preserving date)
1514 - `edit` to edit this changeset (preserving date)
951
1515
952 - `base` to checkout changeset and apply further changesets from there
1516 - `base` to checkout changeset and apply further changesets from there
953
1517
954 There are a number of ways to select the root changeset:
1518 There are a number of ways to select the root changeset:
955
1519
956 - Specify ANCESTOR directly
1520 - Specify ANCESTOR directly
957
1521
958 - Use --outgoing -- it will be the first linear changeset not
1522 - Use --outgoing -- it will be the first linear changeset not
959 included in destination. (See :hg:`help config.paths.default-push`)
1523 included in destination. (See :hg:`help config.paths.default-push`)
960
1524
961 - Otherwise, the value from the "histedit.defaultrev" config option
1525 - Otherwise, the value from the "histedit.defaultrev" config option
962 is used as a revset to select the base revision when ANCESTOR is not
1526 is used as a revset to select the base revision when ANCESTOR is not
963 specified. The first revision returned by the revset is used. By
1527 specified. The first revision returned by the revset is used. By
964 default, this selects the editable history that is unique to the
1528 default, this selects the editable history that is unique to the
965 ancestry of the working directory.
1529 ancestry of the working directory.
966
1530
967 .. container:: verbose
1531 .. container:: verbose
968
1532
969 If you use --outgoing, this command will abort if there are ambiguous
1533 If you use --outgoing, this command will abort if there are ambiguous
970 outgoing revisions. For example, if there are multiple branches
1534 outgoing revisions. For example, if there are multiple branches
971 containing outgoing revisions.
1535 containing outgoing revisions.
972
1536
973 Use "min(outgoing() and ::.)" or similar revset specification
1537 Use "min(outgoing() and ::.)" or similar revset specification
974 instead of --outgoing to specify edit target revision exactly in
1538 instead of --outgoing to specify edit target revision exactly in
975 such ambiguous situation. See :hg:`help revsets` for detail about
1539 such ambiguous situation. See :hg:`help revsets` for detail about
976 selecting revisions.
1540 selecting revisions.
977
1541
978 .. container:: verbose
1542 .. container:: verbose
979
1543
980 Examples:
1544 Examples:
981
1545
982 - A number of changes have been made.
1546 - A number of changes have been made.
983 Revision 3 is no longer needed.
1547 Revision 3 is no longer needed.
984
1548
985 Start history editing from revision 3::
1549 Start history editing from revision 3::
986
1550
987 hg histedit -r 3
1551 hg histedit -r 3
988
1552
989 An editor opens, containing the list of revisions,
1553 An editor opens, containing the list of revisions,
990 with specific actions specified::
1554 with specific actions specified::
991
1555
992 pick 5339bf82f0ca 3 Zworgle the foobar
1556 pick 5339bf82f0ca 3 Zworgle the foobar
993 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1557 pick 8ef592ce7cc4 4 Bedazzle the zerlog
994 pick 0a9639fcda9d 5 Morgify the cromulancy
1558 pick 0a9639fcda9d 5 Morgify the cromulancy
995
1559
996 Additional information about the possible actions
1560 Additional information about the possible actions
997 to take appears below the list of revisions.
1561 to take appears below the list of revisions.
998
1562
999 To remove revision 3 from the history,
1563 To remove revision 3 from the history,
1000 its action (at the beginning of the relevant line)
1564 its action (at the beginning of the relevant line)
1001 is changed to 'drop'::
1565 is changed to 'drop'::
1002
1566
1003 drop 5339bf82f0ca 3 Zworgle the foobar
1567 drop 5339bf82f0ca 3 Zworgle the foobar
1004 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1568 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1005 pick 0a9639fcda9d 5 Morgify the cromulancy
1569 pick 0a9639fcda9d 5 Morgify the cromulancy
1006
1570
1007 - A number of changes have been made.
1571 - A number of changes have been made.
1008 Revision 2 and 4 need to be swapped.
1572 Revision 2 and 4 need to be swapped.
1009
1573
1010 Start history editing from revision 2::
1574 Start history editing from revision 2::
1011
1575
1012 hg histedit -r 2
1576 hg histedit -r 2
1013
1577
1014 An editor opens, containing the list of revisions,
1578 An editor opens, containing the list of revisions,
1015 with specific actions specified::
1579 with specific actions specified::
1016
1580
1017 pick 252a1af424ad 2 Blorb a morgwazzle
1581 pick 252a1af424ad 2 Blorb a morgwazzle
1018 pick 5339bf82f0ca 3 Zworgle the foobar
1582 pick 5339bf82f0ca 3 Zworgle the foobar
1019 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1583 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1020
1584
1021 To swap revision 2 and 4, its lines are swapped
1585 To swap revision 2 and 4, its lines are swapped
1022 in the editor::
1586 in the editor::
1023
1587
1024 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1588 pick 8ef592ce7cc4 4 Bedazzle the zerlog
1025 pick 5339bf82f0ca 3 Zworgle the foobar
1589 pick 5339bf82f0ca 3 Zworgle the foobar
1026 pick 252a1af424ad 2 Blorb a morgwazzle
1590 pick 252a1af424ad 2 Blorb a morgwazzle
1027
1591
1028 Returns 0 on success, 1 if user intervention is required (not only
1592 Returns 0 on success, 1 if user intervention is required (not only
1029 for intentional "edit" command, but also for resolving unexpected
1593 for intentional "edit" command, but also for resolving unexpected
1030 conflicts).
1594 conflicts).
1031 """
1595 """
1596 if ui.interface('histedit') == 'curses':
1597 return _chistedit(ui, repo, *freeargs, **opts)
1598 return _texthistedit(ui, repo, *freeargs, **opts)
1599
1600 def _texthistedit(ui, repo, *freeargs, **opts):
1032 state = histeditstate(repo)
1601 state = histeditstate(repo)
1033 try:
1602 try:
1034 state.wlock = repo.wlock()
1603 state.wlock = repo.wlock()
1035 state.lock = repo.lock()
1604 state.lock = repo.lock()
1036 _histedit(ui, repo, state, *freeargs, **opts)
1605 _histedit(ui, repo, state, *freeargs, **opts)
1037 finally:
1606 finally:
1038 release(state.lock, state.wlock)
1607 release(state.lock, state.wlock)
1039
1608
1040 goalcontinue = 'continue'
1609 goalcontinue = 'continue'
1041 goalabort = 'abort'
1610 goalabort = 'abort'
1042 goaleditplan = 'edit-plan'
1611 goaleditplan = 'edit-plan'
1043 goalnew = 'new'
1612 goalnew = 'new'
1044
1613
1045 def _getgoal(opts):
1614 def _getgoal(opts):
1046 if opts.get('continue'):
1615 if opts.get('continue'):
1047 return goalcontinue
1616 return goalcontinue
1048 if opts.get('abort'):
1617 if opts.get('abort'):
1049 return goalabort
1618 return goalabort
1050 if opts.get('edit_plan'):
1619 if opts.get('edit_plan'):
1051 return goaleditplan
1620 return goaleditplan
1052 return goalnew
1621 return goalnew
1053
1622
1054 def _readfile(ui, path):
1623 def _readfile(ui, path):
1055 if path == '-':
1624 if path == '-':
1056 with ui.timeblockedsection('histedit'):
1625 with ui.timeblockedsection('histedit'):
1057 return ui.fin.read()
1626 return ui.fin.read()
1058 else:
1627 else:
1059 with open(path, 'rb') as f:
1628 with open(path, 'rb') as f:
1060 return f.read()
1629 return f.read()
1061
1630
1062 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1631 def _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs):
1063 # TODO only abort if we try to histedit mq patches, not just
1632 # TODO only abort if we try to histedit mq patches, not just
1064 # blanket if mq patches are applied somewhere
1633 # blanket if mq patches are applied somewhere
1065 mq = getattr(repo, 'mq', None)
1634 mq = getattr(repo, 'mq', None)
1066 if mq and mq.applied:
1635 if mq and mq.applied:
1067 raise error.Abort(_('source has mq patches applied'))
1636 raise error.Abort(_('source has mq patches applied'))
1068
1637
1069 # basic argument incompatibility processing
1638 # basic argument incompatibility processing
1070 outg = opts.get('outgoing')
1639 outg = opts.get('outgoing')
1071 editplan = opts.get('edit_plan')
1640 editplan = opts.get('edit_plan')
1072 abort = opts.get('abort')
1641 abort = opts.get('abort')
1073 force = opts.get('force')
1642 force = opts.get('force')
1074 if force and not outg:
1643 if force and not outg:
1075 raise error.Abort(_('--force only allowed with --outgoing'))
1644 raise error.Abort(_('--force only allowed with --outgoing'))
1076 if goal == 'continue':
1645 if goal == 'continue':
1077 if any((outg, abort, revs, freeargs, rules, editplan)):
1646 if any((outg, abort, revs, freeargs, rules, editplan)):
1078 raise error.Abort(_('no arguments allowed with --continue'))
1647 raise error.Abort(_('no arguments allowed with --continue'))
1079 elif goal == 'abort':
1648 elif goal == 'abort':
1080 if any((outg, revs, freeargs, rules, editplan)):
1649 if any((outg, revs, freeargs, rules, editplan)):
1081 raise error.Abort(_('no arguments allowed with --abort'))
1650 raise error.Abort(_('no arguments allowed with --abort'))
1082 elif goal == 'edit-plan':
1651 elif goal == 'edit-plan':
1083 if any((outg, revs, freeargs)):
1652 if any((outg, revs, freeargs)):
1084 raise error.Abort(_('only --commands argument allowed with '
1653 raise error.Abort(_('only --commands argument allowed with '
1085 '--edit-plan'))
1654 '--edit-plan'))
1086 else:
1655 else:
1087 if state.inprogress():
1656 if state.inprogress():
1088 raise error.Abort(_('history edit already in progress, try '
1657 raise error.Abort(_('history edit already in progress, try '
1089 '--continue or --abort'))
1658 '--continue or --abort'))
1090 if outg:
1659 if outg:
1091 if revs:
1660 if revs:
1092 raise error.Abort(_('no revisions allowed with --outgoing'))
1661 raise error.Abort(_('no revisions allowed with --outgoing'))
1093 if len(freeargs) > 1:
1662 if len(freeargs) > 1:
1094 raise error.Abort(
1663 raise error.Abort(
1095 _('only one repo argument allowed with --outgoing'))
1664 _('only one repo argument allowed with --outgoing'))
1096 else:
1665 else:
1097 revs.extend(freeargs)
1666 revs.extend(freeargs)
1098 if len(revs) == 0:
1667 if len(revs) == 0:
1099 defaultrev = destutil.desthistedit(ui, repo)
1668 defaultrev = destutil.desthistedit(ui, repo)
1100 if defaultrev is not None:
1669 if defaultrev is not None:
1101 revs.append(defaultrev)
1670 revs.append(defaultrev)
1102
1671
1103 if len(revs) != 1:
1672 if len(revs) != 1:
1104 raise error.Abort(
1673 raise error.Abort(
1105 _('histedit requires exactly one ancestor revision'))
1674 _('histedit requires exactly one ancestor revision'))
1106
1675
1107 def _histedit(ui, repo, state, *freeargs, **opts):
1676 def _histedit(ui, repo, state, *freeargs, **opts):
1108 opts = pycompat.byteskwargs(opts)
1677 opts = pycompat.byteskwargs(opts)
1109 fm = ui.formatter('histedit', opts)
1678 fm = ui.formatter('histedit', opts)
1110 fm.startitem()
1679 fm.startitem()
1111 goal = _getgoal(opts)
1680 goal = _getgoal(opts)
1112 revs = opts.get('rev', [])
1681 revs = opts.get('rev', [])
1113 # experimental config: ui.history-editing-backup
1682 # experimental config: ui.history-editing-backup
1114 nobackup = not ui.configbool('ui', 'history-editing-backup')
1683 nobackup = not ui.configbool('ui', 'history-editing-backup')
1115 rules = opts.get('commands', '')
1684 rules = opts.get('commands', '')
1116 state.keep = opts.get('keep', False)
1685 state.keep = opts.get('keep', False)
1117
1686
1118 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1687 _validateargs(ui, repo, state, freeargs, opts, goal, rules, revs)
1119
1688
1120 # rebuild state
1689 # rebuild state
1121 if goal == goalcontinue:
1690 if goal == goalcontinue:
1122 state.read()
1691 state.read()
1123 state = bootstrapcontinue(ui, state, opts)
1692 state = bootstrapcontinue(ui, state, opts)
1124 elif goal == goaleditplan:
1693 elif goal == goaleditplan:
1125 _edithisteditplan(ui, repo, state, rules)
1694 _edithisteditplan(ui, repo, state, rules)
1126 return
1695 return
1127 elif goal == goalabort:
1696 elif goal == goalabort:
1128 _aborthistedit(ui, repo, state, nobackup=nobackup)
1697 _aborthistedit(ui, repo, state, nobackup=nobackup)
1129 return
1698 return
1130 else:
1699 else:
1131 # goal == goalnew
1700 # goal == goalnew
1132 _newhistedit(ui, repo, state, revs, freeargs, opts)
1701 _newhistedit(ui, repo, state, revs, freeargs, opts)
1133
1702
1134 _continuehistedit(ui, repo, state)
1703 _continuehistedit(ui, repo, state)
1135 _finishhistedit(ui, repo, state, fm)
1704 _finishhistedit(ui, repo, state, fm)
1136 fm.end()
1705 fm.end()
1137
1706
1138 def _continuehistedit(ui, repo, state):
1707 def _continuehistedit(ui, repo, state):
1139 """This function runs after either:
1708 """This function runs after either:
1140 - bootstrapcontinue (if the goal is 'continue')
1709 - bootstrapcontinue (if the goal is 'continue')
1141 - _newhistedit (if the goal is 'new')
1710 - _newhistedit (if the goal is 'new')
1142 """
1711 """
1143 # preprocess rules so that we can hide inner folds from the user
1712 # preprocess rules so that we can hide inner folds from the user
1144 # and only show one editor
1713 # and only show one editor
1145 actions = state.actions[:]
1714 actions = state.actions[:]
1146 for idx, (action, nextact) in enumerate(
1715 for idx, (action, nextact) in enumerate(
1147 zip(actions, actions[1:] + [None])):
1716 zip(actions, actions[1:] + [None])):
1148 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1717 if action.verb == 'fold' and nextact and nextact.verb == 'fold':
1149 state.actions[idx].__class__ = _multifold
1718 state.actions[idx].__class__ = _multifold
1150
1719
1151 # Force an initial state file write, so the user can run --abort/continue
1720 # Force an initial state file write, so the user can run --abort/continue
1152 # even if there's an exception before the first transaction serialize.
1721 # even if there's an exception before the first transaction serialize.
1153 state.write()
1722 state.write()
1154
1723
1155 tr = None
1724 tr = None
1156 # Don't use singletransaction by default since it rolls the entire
1725 # Don't use singletransaction by default since it rolls the entire
1157 # transaction back if an unexpected exception happens (like a
1726 # transaction back if an unexpected exception happens (like a
1158 # pretxncommit hook throws, or the user aborts the commit msg editor).
1727 # pretxncommit hook throws, or the user aborts the commit msg editor).
1159 if ui.configbool("histedit", "singletransaction"):
1728 if ui.configbool("histedit", "singletransaction"):
1160 # Don't use a 'with' for the transaction, since actions may close
1729 # Don't use a 'with' for the transaction, since actions may close
1161 # and reopen a transaction. For example, if the action executes an
1730 # and reopen a transaction. For example, if the action executes an
1162 # external process it may choose to commit the transaction first.
1731 # external process it may choose to commit the transaction first.
1163 tr = repo.transaction('histedit')
1732 tr = repo.transaction('histedit')
1164 progress = ui.makeprogress(_("editing"), unit=_('changes'),
1733 progress = ui.makeprogress(_("editing"), unit=_('changes'),
1165 total=len(state.actions))
1734 total=len(state.actions))
1166 with progress, util.acceptintervention(tr):
1735 with progress, util.acceptintervention(tr):
1167 while state.actions:
1736 while state.actions:
1168 state.write(tr=tr)
1737 state.write(tr=tr)
1169 actobj = state.actions[0]
1738 actobj = state.actions[0]
1170 progress.increment(item=actobj.torule())
1739 progress.increment(item=actobj.torule())
1171 ui.debug('histedit: processing %s %s\n' % (actobj.verb,\
1740 ui.debug('histedit: processing %s %s\n' % (actobj.verb,\
1172 actobj.torule()))
1741 actobj.torule()))
1173 parentctx, replacement_ = actobj.run()
1742 parentctx, replacement_ = actobj.run()
1174 state.parentctxnode = parentctx.node()
1743 state.parentctxnode = parentctx.node()
1175 state.replacements.extend(replacement_)
1744 state.replacements.extend(replacement_)
1176 state.actions.pop(0)
1745 state.actions.pop(0)
1177
1746
1178 state.write()
1747 state.write()
1179
1748
1180 def _finishhistedit(ui, repo, state, fm):
1749 def _finishhistedit(ui, repo, state, fm):
1181 """This action runs when histedit is finishing its session"""
1750 """This action runs when histedit is finishing its session"""
1182 hg.updaterepo(repo, state.parentctxnode, overwrite=False)
1751 hg.updaterepo(repo, state.parentctxnode, overwrite=False)
1183
1752
1184 mapping, tmpnodes, created, ntm = processreplacement(state)
1753 mapping, tmpnodes, created, ntm = processreplacement(state)
1185 if mapping:
1754 if mapping:
1186 for prec, succs in mapping.iteritems():
1755 for prec, succs in mapping.iteritems():
1187 if not succs:
1756 if not succs:
1188 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1757 ui.debug('histedit: %s is dropped\n' % node.short(prec))
1189 else:
1758 else:
1190 ui.debug('histedit: %s is replaced by %s\n' % (
1759 ui.debug('histedit: %s is replaced by %s\n' % (
1191 node.short(prec), node.short(succs[0])))
1760 node.short(prec), node.short(succs[0])))
1192 if len(succs) > 1:
1761 if len(succs) > 1:
1193 m = 'histedit: %s'
1762 m = 'histedit: %s'
1194 for n in succs[1:]:
1763 for n in succs[1:]:
1195 ui.debug(m % node.short(n))
1764 ui.debug(m % node.short(n))
1196
1765
1197 if not state.keep:
1766 if not state.keep:
1198 if mapping:
1767 if mapping:
1199 movetopmostbookmarks(repo, state.topmost, ntm)
1768 movetopmostbookmarks(repo, state.topmost, ntm)
1200 # TODO update mq state
1769 # TODO update mq state
1201 else:
1770 else:
1202 mapping = {}
1771 mapping = {}
1203
1772
1204 for n in tmpnodes:
1773 for n in tmpnodes:
1205 if n in repo:
1774 if n in repo:
1206 mapping[n] = ()
1775 mapping[n] = ()
1207
1776
1208 # remove entries about unknown nodes
1777 # remove entries about unknown nodes
1209 nodemap = repo.unfiltered().changelog.nodemap
1778 nodemap = repo.unfiltered().changelog.nodemap
1210 mapping = {k: v for k, v in mapping.items()
1779 mapping = {k: v for k, v in mapping.items()
1211 if k in nodemap and all(n in nodemap for n in v)}
1780 if k in nodemap and all(n in nodemap for n in v)}
1212 scmutil.cleanupnodes(repo, mapping, 'histedit')
1781 scmutil.cleanupnodes(repo, mapping, 'histedit')
1213 hf = fm.hexfunc
1782 hf = fm.hexfunc
1214 fl = fm.formatlist
1783 fl = fm.formatlist
1215 fd = fm.formatdict
1784 fd = fm.formatdict
1216 nodechanges = fd({hf(oldn): fl([hf(n) for n in newn], name='node')
1785 nodechanges = fd({hf(oldn): fl([hf(n) for n in newn], name='node')
1217 for oldn, newn in mapping.iteritems()},
1786 for oldn, newn in mapping.iteritems()},
1218 key="oldnode", value="newnodes")
1787 key="oldnode", value="newnodes")
1219 fm.data(nodechanges=nodechanges)
1788 fm.data(nodechanges=nodechanges)
1220
1789
1221 state.clear()
1790 state.clear()
1222 if os.path.exists(repo.sjoin('undo')):
1791 if os.path.exists(repo.sjoin('undo')):
1223 os.unlink(repo.sjoin('undo'))
1792 os.unlink(repo.sjoin('undo'))
1224 if repo.vfs.exists('histedit-last-edit.txt'):
1793 if repo.vfs.exists('histedit-last-edit.txt'):
1225 repo.vfs.unlink('histedit-last-edit.txt')
1794 repo.vfs.unlink('histedit-last-edit.txt')
1226
1795
1227 def _aborthistedit(ui, repo, state, nobackup=False):
1796 def _aborthistedit(ui, repo, state, nobackup=False):
1228 try:
1797 try:
1229 state.read()
1798 state.read()
1230 __, leafs, tmpnodes, __ = processreplacement(state)
1799 __, leafs, tmpnodes, __ = processreplacement(state)
1231 ui.debug('restore wc to old parent %s\n'
1800 ui.debug('restore wc to old parent %s\n'
1232 % node.short(state.topmost))
1801 % node.short(state.topmost))
1233
1802
1234 # Recover our old commits if necessary
1803 # Recover our old commits if necessary
1235 if not state.topmost in repo and state.backupfile:
1804 if not state.topmost in repo and state.backupfile:
1236 backupfile = repo.vfs.join(state.backupfile)
1805 backupfile = repo.vfs.join(state.backupfile)
1237 f = hg.openpath(ui, backupfile)
1806 f = hg.openpath(ui, backupfile)
1238 gen = exchange.readbundle(ui, f, backupfile)
1807 gen = exchange.readbundle(ui, f, backupfile)
1239 with repo.transaction('histedit.abort') as tr:
1808 with repo.transaction('histedit.abort') as tr:
1240 bundle2.applybundle(repo, gen, tr, source='histedit',
1809 bundle2.applybundle(repo, gen, tr, source='histedit',
1241 url='bundle:' + backupfile)
1810 url='bundle:' + backupfile)
1242
1811
1243 os.remove(backupfile)
1812 os.remove(backupfile)
1244
1813
1245 # check whether we should update away
1814 # check whether we should update away
1246 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1815 if repo.unfiltered().revs('parents() and (%n or %ln::)',
1247 state.parentctxnode, leafs | tmpnodes):
1816 state.parentctxnode, leafs | tmpnodes):
1248 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1817 hg.clean(repo, state.topmost, show_stats=True, quietempty=True)
1249 cleanupnode(ui, repo, tmpnodes, nobackup=nobackup)
1818 cleanupnode(ui, repo, tmpnodes, nobackup=nobackup)
1250 cleanupnode(ui, repo, leafs, nobackup=nobackup)
1819 cleanupnode(ui, repo, leafs, nobackup=nobackup)
1251 except Exception:
1820 except Exception:
1252 if state.inprogress():
1821 if state.inprogress():
1253 ui.warn(_('warning: encountered an exception during histedit '
1822 ui.warn(_('warning: encountered an exception during histedit '
1254 '--abort; the repository may not have been completely '
1823 '--abort; the repository may not have been completely '
1255 'cleaned up\n'))
1824 'cleaned up\n'))
1256 raise
1825 raise
1257 finally:
1826 finally:
1258 state.clear()
1827 state.clear()
1259
1828
1260 def _edithisteditplan(ui, repo, state, rules):
1829 def _edithisteditplan(ui, repo, state, rules):
1261 state.read()
1830 state.read()
1262 if not rules:
1831 if not rules:
1263 comment = geteditcomment(ui,
1832 comment = geteditcomment(ui,
1264 node.short(state.parentctxnode),
1833 node.short(state.parentctxnode),
1265 node.short(state.topmost))
1834 node.short(state.topmost))
1266 rules = ruleeditor(repo, ui, state.actions, comment)
1835 rules = ruleeditor(repo, ui, state.actions, comment)
1267 else:
1836 else:
1268 rules = _readfile(ui, rules)
1837 rules = _readfile(ui, rules)
1269 actions = parserules(rules, state)
1838 actions = parserules(rules, state)
1270 ctxs = [repo[act.node] \
1839 ctxs = [repo[act.node] \
1271 for act in state.actions if act.node]
1840 for act in state.actions if act.node]
1272 warnverifyactions(ui, repo, actions, state, ctxs)
1841 warnverifyactions(ui, repo, actions, state, ctxs)
1273 state.actions = actions
1842 state.actions = actions
1274 state.write()
1843 state.write()
1275
1844
1276 def _newhistedit(ui, repo, state, revs, freeargs, opts):
1845 def _newhistedit(ui, repo, state, revs, freeargs, opts):
1277 outg = opts.get('outgoing')
1846 outg = opts.get('outgoing')
1278 rules = opts.get('commands', '')
1847 rules = opts.get('commands', '')
1279 force = opts.get('force')
1848 force = opts.get('force')
1280
1849
1281 cmdutil.checkunfinished(repo)
1850 cmdutil.checkunfinished(repo)
1282 cmdutil.bailifchanged(repo)
1851 cmdutil.bailifchanged(repo)
1283
1852
1284 topmost, empty = repo.dirstate.parents()
1853 topmost, empty = repo.dirstate.parents()
1285 if outg:
1854 if outg:
1286 if freeargs:
1855 if freeargs:
1287 remote = freeargs[0]
1856 remote = freeargs[0]
1288 else:
1857 else:
1289 remote = None
1858 remote = None
1290 root = findoutgoing(ui, repo, remote, force, opts)
1859 root = findoutgoing(ui, repo, remote, force, opts)
1291 else:
1860 else:
1292 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1861 rr = list(repo.set('roots(%ld)', scmutil.revrange(repo, revs)))
1293 if len(rr) != 1:
1862 if len(rr) != 1:
1294 raise error.Abort(_('The specified revisions must have '
1863 raise error.Abort(_('The specified revisions must have '
1295 'exactly one common root'))
1864 'exactly one common root'))
1296 root = rr[0].node()
1865 root = rr[0].node()
1297
1866
1298 revs = between(repo, root, topmost, state.keep)
1867 revs = between(repo, root, topmost, state.keep)
1299 if not revs:
1868 if not revs:
1300 raise error.Abort(_('%s is not an ancestor of working directory') %
1869 raise error.Abort(_('%s is not an ancestor of working directory') %
1301 node.short(root))
1870 node.short(root))
1302
1871
1303 ctxs = [repo[r] for r in revs]
1872 ctxs = [repo[r] for r in revs]
1304 if not rules:
1873 if not rules:
1305 comment = geteditcomment(ui, node.short(root), node.short(topmost))
1874 comment = geteditcomment(ui, node.short(root), node.short(topmost))
1306 actions = [pick(state, r) for r in revs]
1875 actions = [pick(state, r) for r in revs]
1307 rules = ruleeditor(repo, ui, actions, comment)
1876 rules = ruleeditor(repo, ui, actions, comment)
1308 else:
1877 else:
1309 rules = _readfile(ui, rules)
1878 rules = _readfile(ui, rules)
1310 actions = parserules(rules, state)
1879 actions = parserules(rules, state)
1311 warnverifyactions(ui, repo, actions, state, ctxs)
1880 warnverifyactions(ui, repo, actions, state, ctxs)
1312
1881
1313 parentctxnode = repo[root].parents()[0].node()
1882 parentctxnode = repo[root].parents()[0].node()
1314
1883
1315 state.parentctxnode = parentctxnode
1884 state.parentctxnode = parentctxnode
1316 state.actions = actions
1885 state.actions = actions
1317 state.topmost = topmost
1886 state.topmost = topmost
1318 state.replacements = []
1887 state.replacements = []
1319
1888
1320 ui.log("histedit", "%d actions to histedit", len(actions),
1889 ui.log("histedit", "%d actions to histedit", len(actions),
1321 histedit_num_actions=len(actions))
1890 histedit_num_actions=len(actions))
1322
1891
1323 # Create a backup so we can always abort completely.
1892 # Create a backup so we can always abort completely.
1324 backupfile = None
1893 backupfile = None
1325 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1894 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1326 backupfile = repair.backupbundle(repo, [parentctxnode],
1895 backupfile = repair.backupbundle(repo, [parentctxnode],
1327 [topmost], root, 'histedit')
1896 [topmost], root, 'histedit')
1328 state.backupfile = backupfile
1897 state.backupfile = backupfile
1329
1898
1330 def _getsummary(ctx):
1899 def _getsummary(ctx):
1331 # a common pattern is to extract the summary but default to the empty
1900 # a common pattern is to extract the summary but default to the empty
1332 # string
1901 # string
1333 summary = ctx.description() or ''
1902 summary = ctx.description() or ''
1334 if summary:
1903 if summary:
1335 summary = summary.splitlines()[0]
1904 summary = summary.splitlines()[0]
1336 return summary
1905 return summary
1337
1906
1338 def bootstrapcontinue(ui, state, opts):
1907 def bootstrapcontinue(ui, state, opts):
1339 repo = state.repo
1908 repo = state.repo
1340
1909
1341 ms = mergemod.mergestate.read(repo)
1910 ms = mergemod.mergestate.read(repo)
1342 mergeutil.checkunresolved(ms)
1911 mergeutil.checkunresolved(ms)
1343
1912
1344 if state.actions:
1913 if state.actions:
1345 actobj = state.actions.pop(0)
1914 actobj = state.actions.pop(0)
1346
1915
1347 if _isdirtywc(repo):
1916 if _isdirtywc(repo):
1348 actobj.continuedirty()
1917 actobj.continuedirty()
1349 if _isdirtywc(repo):
1918 if _isdirtywc(repo):
1350 abortdirty()
1919 abortdirty()
1351
1920
1352 parentctx, replacements = actobj.continueclean()
1921 parentctx, replacements = actobj.continueclean()
1353
1922
1354 state.parentctxnode = parentctx.node()
1923 state.parentctxnode = parentctx.node()
1355 state.replacements.extend(replacements)
1924 state.replacements.extend(replacements)
1356
1925
1357 return state
1926 return state
1358
1927
1359 def between(repo, old, new, keep):
1928 def between(repo, old, new, keep):
1360 """select and validate the set of revision to edit
1929 """select and validate the set of revision to edit
1361
1930
1362 When keep is false, the specified set can't have children."""
1931 When keep is false, the specified set can't have children."""
1363 revs = repo.revs('%n::%n', old, new)
1932 revs = repo.revs('%n::%n', old, new)
1364 if revs and not keep:
1933 if revs and not keep:
1365 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1934 if (not obsolete.isenabled(repo, obsolete.allowunstableopt) and
1366 repo.revs('(%ld::) - (%ld)', revs, revs)):
1935 repo.revs('(%ld::) - (%ld)', revs, revs)):
1367 raise error.Abort(_('can only histedit a changeset together '
1936 raise error.Abort(_('can only histedit a changeset together '
1368 'with all its descendants'))
1937 'with all its descendants'))
1369 if repo.revs('(%ld) and merge()', revs):
1938 if repo.revs('(%ld) and merge()', revs):
1370 raise error.Abort(_('cannot edit history that contains merges'))
1939 raise error.Abort(_('cannot edit history that contains merges'))
1371 root = repo[revs.first()] # list is already sorted by repo.revs()
1940 root = repo[revs.first()] # list is already sorted by repo.revs()
1372 if not root.mutable():
1941 if not root.mutable():
1373 raise error.Abort(_('cannot edit public changeset: %s') % root,
1942 raise error.Abort(_('cannot edit public changeset: %s') % root,
1374 hint=_("see 'hg help phases' for details"))
1943 hint=_("see 'hg help phases' for details"))
1375 return pycompat.maplist(repo.changelog.node, revs)
1944 return pycompat.maplist(repo.changelog.node, revs)
1376
1945
1377 def ruleeditor(repo, ui, actions, editcomment=""):
1946 def ruleeditor(repo, ui, actions, editcomment=""):
1378 """open an editor to edit rules
1947 """open an editor to edit rules
1379
1948
1380 rules are in the format [ [act, ctx], ...] like in state.rules
1949 rules are in the format [ [act, ctx], ...] like in state.rules
1381 """
1950 """
1382 if repo.ui.configbool("experimental", "histedit.autoverb"):
1951 if repo.ui.configbool("experimental", "histedit.autoverb"):
1383 newact = util.sortdict()
1952 newact = util.sortdict()
1384 for act in actions:
1953 for act in actions:
1385 ctx = repo[act.node]
1954 ctx = repo[act.node]
1386 summary = _getsummary(ctx)
1955 summary = _getsummary(ctx)
1387 fword = summary.split(' ', 1)[0].lower()
1956 fword = summary.split(' ', 1)[0].lower()
1388 added = False
1957 added = False
1389
1958
1390 # if it doesn't end with the special character '!' just skip this
1959 # if it doesn't end with the special character '!' just skip this
1391 if fword.endswith('!'):
1960 if fword.endswith('!'):
1392 fword = fword[:-1]
1961 fword = fword[:-1]
1393 if fword in primaryactions | secondaryactions | tertiaryactions:
1962 if fword in primaryactions | secondaryactions | tertiaryactions:
1394 act.verb = fword
1963 act.verb = fword
1395 # get the target summary
1964 # get the target summary
1396 tsum = summary[len(fword) + 1:].lstrip()
1965 tsum = summary[len(fword) + 1:].lstrip()
1397 # safe but slow: reverse iterate over the actions so we
1966 # safe but slow: reverse iterate over the actions so we
1398 # don't clash on two commits having the same summary
1967 # don't clash on two commits having the same summary
1399 for na, l in reversed(list(newact.iteritems())):
1968 for na, l in reversed(list(newact.iteritems())):
1400 actx = repo[na.node]
1969 actx = repo[na.node]
1401 asum = _getsummary(actx)
1970 asum = _getsummary(actx)
1402 if asum == tsum:
1971 if asum == tsum:
1403 added = True
1972 added = True
1404 l.append(act)
1973 l.append(act)
1405 break
1974 break
1406
1975
1407 if not added:
1976 if not added:
1408 newact[act] = []
1977 newact[act] = []
1409
1978
1410 # copy over and flatten the new list
1979 # copy over and flatten the new list
1411 actions = []
1980 actions = []
1412 for na, l in newact.iteritems():
1981 for na, l in newact.iteritems():
1413 actions.append(na)
1982 actions.append(na)
1414 actions += l
1983 actions += l
1415
1984
1416 rules = '\n'.join([act.torule() for act in actions])
1985 rules = '\n'.join([act.torule() for act in actions])
1417 rules += '\n\n'
1986 rules += '\n\n'
1418 rules += editcomment
1987 rules += editcomment
1419 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'},
1988 rules = ui.edit(rules, ui.username(), {'prefix': 'histedit'},
1420 repopath=repo.path, action='histedit')
1989 repopath=repo.path, action='histedit')
1421
1990
1422 # Save edit rules in .hg/histedit-last-edit.txt in case
1991 # Save edit rules in .hg/histedit-last-edit.txt in case
1423 # the user needs to ask for help after something
1992 # the user needs to ask for help after something
1424 # surprising happens.
1993 # surprising happens.
1425 with repo.vfs('histedit-last-edit.txt', 'wb') as f:
1994 with repo.vfs('histedit-last-edit.txt', 'wb') as f:
1426 f.write(rules)
1995 f.write(rules)
1427
1996
1428 return rules
1997 return rules
1429
1998
1430 def parserules(rules, state):
1999 def parserules(rules, state):
1431 """Read the histedit rules string and return list of action objects """
2000 """Read the histedit rules string and return list of action objects """
1432 rules = [l for l in (r.strip() for r in rules.splitlines())
2001 rules = [l for l in (r.strip() for r in rules.splitlines())
1433 if l and not l.startswith('#')]
2002 if l and not l.startswith('#')]
1434 actions = []
2003 actions = []
1435 for r in rules:
2004 for r in rules:
1436 if ' ' not in r:
2005 if ' ' not in r:
1437 raise error.ParseError(_('malformed line "%s"') % r)
2006 raise error.ParseError(_('malformed line "%s"') % r)
1438 verb, rest = r.split(' ', 1)
2007 verb, rest = r.split(' ', 1)
1439
2008
1440 if verb not in actiontable:
2009 if verb not in actiontable:
1441 raise error.ParseError(_('unknown action "%s"') % verb)
2010 raise error.ParseError(_('unknown action "%s"') % verb)
1442
2011
1443 action = actiontable[verb].fromrule(state, rest)
2012 action = actiontable[verb].fromrule(state, rest)
1444 actions.append(action)
2013 actions.append(action)
1445 return actions
2014 return actions
1446
2015
1447 def warnverifyactions(ui, repo, actions, state, ctxs):
2016 def warnverifyactions(ui, repo, actions, state, ctxs):
1448 try:
2017 try:
1449 verifyactions(actions, state, ctxs)
2018 verifyactions(actions, state, ctxs)
1450 except error.ParseError:
2019 except error.ParseError:
1451 if repo.vfs.exists('histedit-last-edit.txt'):
2020 if repo.vfs.exists('histedit-last-edit.txt'):
1452 ui.warn(_('warning: histedit rules saved '
2021 ui.warn(_('warning: histedit rules saved '
1453 'to: .hg/histedit-last-edit.txt\n'))
2022 'to: .hg/histedit-last-edit.txt\n'))
1454 raise
2023 raise
1455
2024
1456 def verifyactions(actions, state, ctxs):
2025 def verifyactions(actions, state, ctxs):
1457 """Verify that there exists exactly one action per given changeset and
2026 """Verify that there exists exactly one action per given changeset and
1458 other constraints.
2027 other constraints.
1459
2028
1460 Will abort if there are to many or too few rules, a malformed rule,
2029 Will abort if there are to many or too few rules, a malformed rule,
1461 or a rule on a changeset outside of the user-given range.
2030 or a rule on a changeset outside of the user-given range.
1462 """
2031 """
1463 expected = set(c.node() for c in ctxs)
2032 expected = set(c.node() for c in ctxs)
1464 seen = set()
2033 seen = set()
1465 prev = None
2034 prev = None
1466
2035
1467 if actions and actions[0].verb in ['roll', 'fold']:
2036 if actions and actions[0].verb in ['roll', 'fold']:
1468 raise error.ParseError(_('first changeset cannot use verb "%s"') %
2037 raise error.ParseError(_('first changeset cannot use verb "%s"') %
1469 actions[0].verb)
2038 actions[0].verb)
1470
2039
1471 for action in actions:
2040 for action in actions:
1472 action.verify(prev, expected, seen)
2041 action.verify(prev, expected, seen)
1473 prev = action
2042 prev = action
1474 if action.node is not None:
2043 if action.node is not None:
1475 seen.add(action.node)
2044 seen.add(action.node)
1476 missing = sorted(expected - seen) # sort to stabilize output
2045 missing = sorted(expected - seen) # sort to stabilize output
1477
2046
1478 if state.repo.ui.configbool('histedit', 'dropmissing'):
2047 if state.repo.ui.configbool('histedit', 'dropmissing'):
1479 if len(actions) == 0:
2048 if len(actions) == 0:
1480 raise error.ParseError(_('no rules provided'),
2049 raise error.ParseError(_('no rules provided'),
1481 hint=_('use strip extension to remove commits'))
2050 hint=_('use strip extension to remove commits'))
1482
2051
1483 drops = [drop(state, n) for n in missing]
2052 drops = [drop(state, n) for n in missing]
1484 # put the in the beginning so they execute immediately and
2053 # put the in the beginning so they execute immediately and
1485 # don't show in the edit-plan in the future
2054 # don't show in the edit-plan in the future
1486 actions[:0] = drops
2055 actions[:0] = drops
1487 elif missing:
2056 elif missing:
1488 raise error.ParseError(_('missing rules for changeset %s') %
2057 raise error.ParseError(_('missing rules for changeset %s') %
1489 node.short(missing[0]),
2058 node.short(missing[0]),
1490 hint=_('use "drop %s" to discard, see also: '
2059 hint=_('use "drop %s" to discard, see also: '
1491 "'hg help -e histedit.config'")
2060 "'hg help -e histedit.config'")
1492 % node.short(missing[0]))
2061 % node.short(missing[0]))
1493
2062
1494 def adjustreplacementsfrommarkers(repo, oldreplacements):
2063 def adjustreplacementsfrommarkers(repo, oldreplacements):
1495 """Adjust replacements from obsolescence markers
2064 """Adjust replacements from obsolescence markers
1496
2065
1497 Replacements structure is originally generated based on
2066 Replacements structure is originally generated based on
1498 histedit's state and does not account for changes that are
2067 histedit's state and does not account for changes that are
1499 not recorded there. This function fixes that by adding
2068 not recorded there. This function fixes that by adding
1500 data read from obsolescence markers"""
2069 data read from obsolescence markers"""
1501 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
2070 if not obsolete.isenabled(repo, obsolete.createmarkersopt):
1502 return oldreplacements
2071 return oldreplacements
1503
2072
1504 unfi = repo.unfiltered()
2073 unfi = repo.unfiltered()
1505 nm = unfi.changelog.nodemap
2074 nm = unfi.changelog.nodemap
1506 obsstore = repo.obsstore
2075 obsstore = repo.obsstore
1507 newreplacements = list(oldreplacements)
2076 newreplacements = list(oldreplacements)
1508 oldsuccs = [r[1] for r in oldreplacements]
2077 oldsuccs = [r[1] for r in oldreplacements]
1509 # successors that have already been added to succstocheck once
2078 # successors that have already been added to succstocheck once
1510 seensuccs = set().union(*oldsuccs) # create a set from an iterable of tuples
2079 seensuccs = set().union(*oldsuccs) # create a set from an iterable of tuples
1511 succstocheck = list(seensuccs)
2080 succstocheck = list(seensuccs)
1512 while succstocheck:
2081 while succstocheck:
1513 n = succstocheck.pop()
2082 n = succstocheck.pop()
1514 missing = nm.get(n) is None
2083 missing = nm.get(n) is None
1515 markers = obsstore.successors.get(n, ())
2084 markers = obsstore.successors.get(n, ())
1516 if missing and not markers:
2085 if missing and not markers:
1517 # dead end, mark it as such
2086 # dead end, mark it as such
1518 newreplacements.append((n, ()))
2087 newreplacements.append((n, ()))
1519 for marker in markers:
2088 for marker in markers:
1520 nsuccs = marker[1]
2089 nsuccs = marker[1]
1521 newreplacements.append((n, nsuccs))
2090 newreplacements.append((n, nsuccs))
1522 for nsucc in nsuccs:
2091 for nsucc in nsuccs:
1523 if nsucc not in seensuccs:
2092 if nsucc not in seensuccs:
1524 seensuccs.add(nsucc)
2093 seensuccs.add(nsucc)
1525 succstocheck.append(nsucc)
2094 succstocheck.append(nsucc)
1526
2095
1527 return newreplacements
2096 return newreplacements
1528
2097
1529 def processreplacement(state):
2098 def processreplacement(state):
1530 """process the list of replacements to return
2099 """process the list of replacements to return
1531
2100
1532 1) the final mapping between original and created nodes
2101 1) the final mapping between original and created nodes
1533 2) the list of temporary node created by histedit
2102 2) the list of temporary node created by histedit
1534 3) the list of new commit created by histedit"""
2103 3) the list of new commit created by histedit"""
1535 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
2104 replacements = adjustreplacementsfrommarkers(state.repo, state.replacements)
1536 allsuccs = set()
2105 allsuccs = set()
1537 replaced = set()
2106 replaced = set()
1538 fullmapping = {}
2107 fullmapping = {}
1539 # initialize basic set
2108 # initialize basic set
1540 # fullmapping records all operations recorded in replacement
2109 # fullmapping records all operations recorded in replacement
1541 for rep in replacements:
2110 for rep in replacements:
1542 allsuccs.update(rep[1])
2111 allsuccs.update(rep[1])
1543 replaced.add(rep[0])
2112 replaced.add(rep[0])
1544 fullmapping.setdefault(rep[0], set()).update(rep[1])
2113 fullmapping.setdefault(rep[0], set()).update(rep[1])
1545 new = allsuccs - replaced
2114 new = allsuccs - replaced
1546 tmpnodes = allsuccs & replaced
2115 tmpnodes = allsuccs & replaced
1547 # Reduce content fullmapping into direct relation between original nodes
2116 # Reduce content fullmapping into direct relation between original nodes
1548 # and final node created during history edition
2117 # and final node created during history edition
1549 # Dropped changeset are replaced by an empty list
2118 # Dropped changeset are replaced by an empty list
1550 toproceed = set(fullmapping)
2119 toproceed = set(fullmapping)
1551 final = {}
2120 final = {}
1552 while toproceed:
2121 while toproceed:
1553 for x in list(toproceed):
2122 for x in list(toproceed):
1554 succs = fullmapping[x]
2123 succs = fullmapping[x]
1555 for s in list(succs):
2124 for s in list(succs):
1556 if s in toproceed:
2125 if s in toproceed:
1557 # non final node with unknown closure
2126 # non final node with unknown closure
1558 # We can't process this now
2127 # We can't process this now
1559 break
2128 break
1560 elif s in final:
2129 elif s in final:
1561 # non final node, replace with closure
2130 # non final node, replace with closure
1562 succs.remove(s)
2131 succs.remove(s)
1563 succs.update(final[s])
2132 succs.update(final[s])
1564 else:
2133 else:
1565 final[x] = succs
2134 final[x] = succs
1566 toproceed.remove(x)
2135 toproceed.remove(x)
1567 # remove tmpnodes from final mapping
2136 # remove tmpnodes from final mapping
1568 for n in tmpnodes:
2137 for n in tmpnodes:
1569 del final[n]
2138 del final[n]
1570 # we expect all changes involved in final to exist in the repo
2139 # we expect all changes involved in final to exist in the repo
1571 # turn `final` into list (topologically sorted)
2140 # turn `final` into list (topologically sorted)
1572 nm = state.repo.changelog.nodemap
2141 nm = state.repo.changelog.nodemap
1573 for prec, succs in final.items():
2142 for prec, succs in final.items():
1574 final[prec] = sorted(succs, key=nm.get)
2143 final[prec] = sorted(succs, key=nm.get)
1575
2144
1576 # computed topmost element (necessary for bookmark)
2145 # computed topmost element (necessary for bookmark)
1577 if new:
2146 if new:
1578 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
2147 newtopmost = sorted(new, key=state.repo.changelog.rev)[-1]
1579 elif not final:
2148 elif not final:
1580 # Nothing rewritten at all. we won't need `newtopmost`
2149 # Nothing rewritten at all. we won't need `newtopmost`
1581 # It is the same as `oldtopmost` and `processreplacement` know it
2150 # It is the same as `oldtopmost` and `processreplacement` know it
1582 newtopmost = None
2151 newtopmost = None
1583 else:
2152 else:
1584 # every body died. The newtopmost is the parent of the root.
2153 # every body died. The newtopmost is the parent of the root.
1585 r = state.repo.changelog.rev
2154 r = state.repo.changelog.rev
1586 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
2155 newtopmost = state.repo[sorted(final, key=r)[0]].p1().node()
1587
2156
1588 return final, tmpnodes, new, newtopmost
2157 return final, tmpnodes, new, newtopmost
1589
2158
1590 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
2159 def movetopmostbookmarks(repo, oldtopmost, newtopmost):
1591 """Move bookmark from oldtopmost to newly created topmost
2160 """Move bookmark from oldtopmost to newly created topmost
1592
2161
1593 This is arguably a feature and we may only want that for the active
2162 This is arguably a feature and we may only want that for the active
1594 bookmark. But the behavior is kept compatible with the old version for now.
2163 bookmark. But the behavior is kept compatible with the old version for now.
1595 """
2164 """
1596 if not oldtopmost or not newtopmost:
2165 if not oldtopmost or not newtopmost:
1597 return
2166 return
1598 oldbmarks = repo.nodebookmarks(oldtopmost)
2167 oldbmarks = repo.nodebookmarks(oldtopmost)
1599 if oldbmarks:
2168 if oldbmarks:
1600 with repo.lock(), repo.transaction('histedit') as tr:
2169 with repo.lock(), repo.transaction('histedit') as tr:
1601 marks = repo._bookmarks
2170 marks = repo._bookmarks
1602 changes = []
2171 changes = []
1603 for name in oldbmarks:
2172 for name in oldbmarks:
1604 changes.append((name, newtopmost))
2173 changes.append((name, newtopmost))
1605 marks.applychanges(repo, tr, changes)
2174 marks.applychanges(repo, tr, changes)
1606
2175
1607 def cleanupnode(ui, repo, nodes, nobackup=False):
2176 def cleanupnode(ui, repo, nodes, nobackup=False):
1608 """strip a group of nodes from the repository
2177 """strip a group of nodes from the repository
1609
2178
1610 The set of node to strip may contains unknown nodes."""
2179 The set of node to strip may contains unknown nodes."""
1611 with repo.lock():
2180 with repo.lock():
1612 # do not let filtering get in the way of the cleanse
2181 # do not let filtering get in the way of the cleanse
1613 # we should probably get rid of obsolescence marker created during the
2182 # we should probably get rid of obsolescence marker created during the
1614 # histedit, but we currently do not have such information.
2183 # histedit, but we currently do not have such information.
1615 repo = repo.unfiltered()
2184 repo = repo.unfiltered()
1616 # Find all nodes that need to be stripped
2185 # Find all nodes that need to be stripped
1617 # (we use %lr instead of %ln to silently ignore unknown items)
2186 # (we use %lr instead of %ln to silently ignore unknown items)
1618 nm = repo.changelog.nodemap
2187 nm = repo.changelog.nodemap
1619 nodes = sorted(n for n in nodes if n in nm)
2188 nodes = sorted(n for n in nodes if n in nm)
1620 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
2189 roots = [c.node() for c in repo.set("roots(%ln)", nodes)]
1621 if roots:
2190 if roots:
1622 backup = not nobackup
2191 backup = not nobackup
1623 repair.strip(ui, repo, roots, backup=backup)
2192 repair.strip(ui, repo, roots, backup=backup)
1624
2193
1625 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
2194 def stripwrapper(orig, ui, repo, nodelist, *args, **kwargs):
1626 if isinstance(nodelist, str):
2195 if isinstance(nodelist, str):
1627 nodelist = [nodelist]
2196 nodelist = [nodelist]
1628 state = histeditstate(repo)
2197 state = histeditstate(repo)
1629 if state.inprogress():
2198 if state.inprogress():
1630 state.read()
2199 state.read()
1631 histedit_nodes = {action.node for action
2200 histedit_nodes = {action.node for action
1632 in state.actions if action.node}
2201 in state.actions if action.node}
1633 common_nodes = histedit_nodes & set(nodelist)
2202 common_nodes = histedit_nodes & set(nodelist)
1634 if common_nodes:
2203 if common_nodes:
1635 raise error.Abort(_("histedit in progress, can't strip %s")
2204 raise error.Abort(_("histedit in progress, can't strip %s")
1636 % ', '.join(node.short(x) for x in common_nodes))
2205 % ', '.join(node.short(x) for x in common_nodes))
1637 return orig(ui, repo, nodelist, *args, **kwargs)
2206 return orig(ui, repo, nodelist, *args, **kwargs)
1638
2207
1639 extensions.wrapfunction(repair, 'strip', stripwrapper)
2208 extensions.wrapfunction(repair, 'strip', stripwrapper)
1640
2209
1641 def summaryhook(ui, repo):
2210 def summaryhook(ui, repo):
1642 state = histeditstate(repo)
2211 state = histeditstate(repo)
1643 if not state.inprogress():
2212 if not state.inprogress():
1644 return
2213 return
1645 state.read()
2214 state.read()
1646 if state.actions:
2215 if state.actions:
1647 # i18n: column positioning for "hg summary"
2216 # i18n: column positioning for "hg summary"
1648 ui.write(_('hist: %s (histedit --continue)\n') %
2217 ui.write(_('hist: %s (histedit --continue)\n') %
1649 (ui.label(_('%d remaining'), 'histedit.remaining') %
2218 (ui.label(_('%d remaining'), 'histedit.remaining') %
1650 len(state.actions)))
2219 len(state.actions)))
1651
2220
1652 def extsetup(ui):
2221 def extsetup(ui):
1653 cmdutil.summaryhooks.add('histedit', summaryhook)
2222 cmdutil.summaryhooks.add('histedit', summaryhook)
1654 cmdutil.unfinishedstates.append(
2223 cmdutil.unfinishedstates.append(
1655 ['histedit-state', False, True, _('histedit in progress'),
2224 ['histedit-state', False, True, _('histedit in progress'),
1656 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
2225 _("use 'hg histedit --continue' or 'hg histedit --abort'")])
1657 cmdutil.afterresolvedstates.append(
2226 cmdutil.afterresolvedstates.append(
1658 ['histedit-state', _('hg histedit --continue')])
2227 ['histedit-state', _('hg histedit --continue')])
@@ -1,2004 +1,2008 b''
1 # ui.py - user interface bits for mercurial
1 # ui.py - user interface bits for mercurial
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.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
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import collections
10 import collections
11 import contextlib
11 import contextlib
12 import errno
12 import errno
13 import getpass
13 import getpass
14 import inspect
14 import inspect
15 import os
15 import os
16 import re
16 import re
17 import signal
17 import signal
18 import socket
18 import socket
19 import subprocess
19 import subprocess
20 import sys
20 import sys
21 import traceback
21 import traceback
22
22
23 from .i18n import _
23 from .i18n import _
24 from .node import hex
24 from .node import hex
25
25
26 from . import (
26 from . import (
27 color,
27 color,
28 config,
28 config,
29 configitems,
29 configitems,
30 encoding,
30 encoding,
31 error,
31 error,
32 formatter,
32 formatter,
33 progress,
33 progress,
34 pycompat,
34 pycompat,
35 rcutil,
35 rcutil,
36 scmutil,
36 scmutil,
37 util,
37 util,
38 )
38 )
39 from .utils import (
39 from .utils import (
40 dateutil,
40 dateutil,
41 procutil,
41 procutil,
42 stringutil,
42 stringutil,
43 )
43 )
44
44
45 urlreq = util.urlreq
45 urlreq = util.urlreq
46
46
47 # for use with str.translate(None, _keepalnum), to keep just alphanumerics
47 # for use with str.translate(None, _keepalnum), to keep just alphanumerics
48 _keepalnum = ''.join(c for c in map(pycompat.bytechr, range(256))
48 _keepalnum = ''.join(c for c in map(pycompat.bytechr, range(256))
49 if not c.isalnum())
49 if not c.isalnum())
50
50
51 # The config knobs that will be altered (if unset) by ui.tweakdefaults.
51 # The config knobs that will be altered (if unset) by ui.tweakdefaults.
52 tweakrc = b"""
52 tweakrc = b"""
53 [ui]
53 [ui]
54 # The rollback command is dangerous. As a rule, don't use it.
54 # The rollback command is dangerous. As a rule, don't use it.
55 rollback = False
55 rollback = False
56 # Make `hg status` report copy information
56 # Make `hg status` report copy information
57 statuscopies = yes
57 statuscopies = yes
58 # Prefer curses UIs when available. Revert to plain-text with `text`.
58 # Prefer curses UIs when available. Revert to plain-text with `text`.
59 interface = curses
59 interface = curses
60
60
61 [commands]
61 [commands]
62 # Grep working directory by default.
62 # Grep working directory by default.
63 grep.all-files = True
63 grep.all-files = True
64 # Make `hg status` emit cwd-relative paths by default.
64 # Make `hg status` emit cwd-relative paths by default.
65 status.relative = yes
65 status.relative = yes
66 # Refuse to perform an `hg update` that would cause a file content merge
66 # Refuse to perform an `hg update` that would cause a file content merge
67 update.check = noconflict
67 update.check = noconflict
68 # Show conflicts information in `hg status`
68 # Show conflicts information in `hg status`
69 status.verbose = True
69 status.verbose = True
70
70
71 [diff]
71 [diff]
72 git = 1
72 git = 1
73 showfunc = 1
73 showfunc = 1
74 word-diff = 1
74 word-diff = 1
75 """
75 """
76
76
77 samplehgrcs = {
77 samplehgrcs = {
78 'user':
78 'user':
79 b"""# example user config (see 'hg help config' for more info)
79 b"""# example user config (see 'hg help config' for more info)
80 [ui]
80 [ui]
81 # name and email, e.g.
81 # name and email, e.g.
82 # username = Jane Doe <jdoe@example.com>
82 # username = Jane Doe <jdoe@example.com>
83 username =
83 username =
84
84
85 # We recommend enabling tweakdefaults to get slight improvements to
85 # We recommend enabling tweakdefaults to get slight improvements to
86 # the UI over time. Make sure to set HGPLAIN in the environment when
86 # the UI over time. Make sure to set HGPLAIN in the environment when
87 # writing scripts!
87 # writing scripts!
88 # tweakdefaults = True
88 # tweakdefaults = True
89
89
90 # uncomment to disable color in command output
90 # uncomment to disable color in command output
91 # (see 'hg help color' for details)
91 # (see 'hg help color' for details)
92 # color = never
92 # color = never
93
93
94 # uncomment to disable command output pagination
94 # uncomment to disable command output pagination
95 # (see 'hg help pager' for details)
95 # (see 'hg help pager' for details)
96 # paginate = never
96 # paginate = never
97
97
98 [extensions]
98 [extensions]
99 # uncomment these lines to enable some popular extensions
99 # uncomment these lines to enable some popular extensions
100 # (see 'hg help extensions' for more info)
100 # (see 'hg help extensions' for more info)
101 #
101 #
102 # churn =
102 # churn =
103 """,
103 """,
104
104
105 'cloned':
105 'cloned':
106 b"""# example repository config (see 'hg help config' for more info)
106 b"""# example repository config (see 'hg help config' for more info)
107 [paths]
107 [paths]
108 default = %s
108 default = %s
109
109
110 # path aliases to other clones of this repo in URLs or filesystem paths
110 # path aliases to other clones of this repo in URLs or filesystem paths
111 # (see 'hg help config.paths' for more info)
111 # (see 'hg help config.paths' for more info)
112 #
112 #
113 # default:pushurl = ssh://jdoe@example.net/hg/jdoes-fork
113 # default:pushurl = ssh://jdoe@example.net/hg/jdoes-fork
114 # my-fork = ssh://jdoe@example.net/hg/jdoes-fork
114 # my-fork = ssh://jdoe@example.net/hg/jdoes-fork
115 # my-clone = /home/jdoe/jdoes-clone
115 # my-clone = /home/jdoe/jdoes-clone
116
116
117 [ui]
117 [ui]
118 # name and email (local to this repository, optional), e.g.
118 # name and email (local to this repository, optional), e.g.
119 # username = Jane Doe <jdoe@example.com>
119 # username = Jane Doe <jdoe@example.com>
120 """,
120 """,
121
121
122 'local':
122 'local':
123 b"""# example repository config (see 'hg help config' for more info)
123 b"""# example repository config (see 'hg help config' for more info)
124 [paths]
124 [paths]
125 # path aliases to other clones of this repo in URLs or filesystem paths
125 # path aliases to other clones of this repo in URLs or filesystem paths
126 # (see 'hg help config.paths' for more info)
126 # (see 'hg help config.paths' for more info)
127 #
127 #
128 # default = http://example.com/hg/example-repo
128 # default = http://example.com/hg/example-repo
129 # default:pushurl = ssh://jdoe@example.net/hg/jdoes-fork
129 # default:pushurl = ssh://jdoe@example.net/hg/jdoes-fork
130 # my-fork = ssh://jdoe@example.net/hg/jdoes-fork
130 # my-fork = ssh://jdoe@example.net/hg/jdoes-fork
131 # my-clone = /home/jdoe/jdoes-clone
131 # my-clone = /home/jdoe/jdoes-clone
132
132
133 [ui]
133 [ui]
134 # name and email (local to this repository, optional), e.g.
134 # name and email (local to this repository, optional), e.g.
135 # username = Jane Doe <jdoe@example.com>
135 # username = Jane Doe <jdoe@example.com>
136 """,
136 """,
137
137
138 'global':
138 'global':
139 b"""# example system-wide hg config (see 'hg help config' for more info)
139 b"""# example system-wide hg config (see 'hg help config' for more info)
140
140
141 [ui]
141 [ui]
142 # uncomment to disable color in command output
142 # uncomment to disable color in command output
143 # (see 'hg help color' for details)
143 # (see 'hg help color' for details)
144 # color = never
144 # color = never
145
145
146 # uncomment to disable command output pagination
146 # uncomment to disable command output pagination
147 # (see 'hg help pager' for details)
147 # (see 'hg help pager' for details)
148 # paginate = never
148 # paginate = never
149
149
150 [extensions]
150 [extensions]
151 # uncomment these lines to enable some popular extensions
151 # uncomment these lines to enable some popular extensions
152 # (see 'hg help extensions' for more info)
152 # (see 'hg help extensions' for more info)
153 #
153 #
154 # blackbox =
154 # blackbox =
155 # churn =
155 # churn =
156 """,
156 """,
157 }
157 }
158
158
159 def _maybestrurl(maybebytes):
159 def _maybestrurl(maybebytes):
160 return pycompat.rapply(pycompat.strurl, maybebytes)
160 return pycompat.rapply(pycompat.strurl, maybebytes)
161
161
162 def _maybebytesurl(maybestr):
162 def _maybebytesurl(maybestr):
163 return pycompat.rapply(pycompat.bytesurl, maybestr)
163 return pycompat.rapply(pycompat.bytesurl, maybestr)
164
164
165 class httppasswordmgrdbproxy(object):
165 class httppasswordmgrdbproxy(object):
166 """Delays loading urllib2 until it's needed."""
166 """Delays loading urllib2 until it's needed."""
167 def __init__(self):
167 def __init__(self):
168 self._mgr = None
168 self._mgr = None
169
169
170 def _get_mgr(self):
170 def _get_mgr(self):
171 if self._mgr is None:
171 if self._mgr is None:
172 self._mgr = urlreq.httppasswordmgrwithdefaultrealm()
172 self._mgr = urlreq.httppasswordmgrwithdefaultrealm()
173 return self._mgr
173 return self._mgr
174
174
175 def add_password(self, realm, uris, user, passwd):
175 def add_password(self, realm, uris, user, passwd):
176 return self._get_mgr().add_password(
176 return self._get_mgr().add_password(
177 _maybestrurl(realm), _maybestrurl(uris),
177 _maybestrurl(realm), _maybestrurl(uris),
178 _maybestrurl(user), _maybestrurl(passwd))
178 _maybestrurl(user), _maybestrurl(passwd))
179
179
180 def find_user_password(self, realm, uri):
180 def find_user_password(self, realm, uri):
181 mgr = self._get_mgr()
181 mgr = self._get_mgr()
182 return _maybebytesurl(mgr.find_user_password(_maybestrurl(realm),
182 return _maybebytesurl(mgr.find_user_password(_maybestrurl(realm),
183 _maybestrurl(uri)))
183 _maybestrurl(uri)))
184
184
185 def _catchterm(*args):
185 def _catchterm(*args):
186 raise error.SignalInterrupt
186 raise error.SignalInterrupt
187
187
188 # unique object used to detect no default value has been provided when
188 # unique object used to detect no default value has been provided when
189 # retrieving configuration value.
189 # retrieving configuration value.
190 _unset = object()
190 _unset = object()
191
191
192 # _reqexithandlers: callbacks run at the end of a request
192 # _reqexithandlers: callbacks run at the end of a request
193 _reqexithandlers = []
193 _reqexithandlers = []
194
194
195 class ui(object):
195 class ui(object):
196 def __init__(self, src=None):
196 def __init__(self, src=None):
197 """Create a fresh new ui object if no src given
197 """Create a fresh new ui object if no src given
198
198
199 Use uimod.ui.load() to create a ui which knows global and user configs.
199 Use uimod.ui.load() to create a ui which knows global and user configs.
200 In most cases, you should use ui.copy() to create a copy of an existing
200 In most cases, you should use ui.copy() to create a copy of an existing
201 ui object.
201 ui object.
202 """
202 """
203 # _buffers: used for temporary capture of output
203 # _buffers: used for temporary capture of output
204 self._buffers = []
204 self._buffers = []
205 # 3-tuple describing how each buffer in the stack behaves.
205 # 3-tuple describing how each buffer in the stack behaves.
206 # Values are (capture stderr, capture subprocesses, apply labels).
206 # Values are (capture stderr, capture subprocesses, apply labels).
207 self._bufferstates = []
207 self._bufferstates = []
208 # When a buffer is active, defines whether we are expanding labels.
208 # When a buffer is active, defines whether we are expanding labels.
209 # This exists to prevent an extra list lookup.
209 # This exists to prevent an extra list lookup.
210 self._bufferapplylabels = None
210 self._bufferapplylabels = None
211 self.quiet = self.verbose = self.debugflag = self.tracebackflag = False
211 self.quiet = self.verbose = self.debugflag = self.tracebackflag = False
212 self._reportuntrusted = True
212 self._reportuntrusted = True
213 self._knownconfig = configitems.coreitems
213 self._knownconfig = configitems.coreitems
214 self._ocfg = config.config() # overlay
214 self._ocfg = config.config() # overlay
215 self._tcfg = config.config() # trusted
215 self._tcfg = config.config() # trusted
216 self._ucfg = config.config() # untrusted
216 self._ucfg = config.config() # untrusted
217 self._trustusers = set()
217 self._trustusers = set()
218 self._trustgroups = set()
218 self._trustgroups = set()
219 self.callhooks = True
219 self.callhooks = True
220 # Insecure server connections requested.
220 # Insecure server connections requested.
221 self.insecureconnections = False
221 self.insecureconnections = False
222 # Blocked time
222 # Blocked time
223 self.logblockedtimes = False
223 self.logblockedtimes = False
224 # color mode: see mercurial/color.py for possible value
224 # color mode: see mercurial/color.py for possible value
225 self._colormode = None
225 self._colormode = None
226 self._terminfoparams = {}
226 self._terminfoparams = {}
227 self._styles = {}
227 self._styles = {}
228 self._uninterruptible = False
228 self._uninterruptible = False
229
229
230 if src:
230 if src:
231 self._fout = src._fout
231 self._fout = src._fout
232 self._ferr = src._ferr
232 self._ferr = src._ferr
233 self._fin = src._fin
233 self._fin = src._fin
234 self._fmsg = src._fmsg
234 self._fmsg = src._fmsg
235 self._fmsgout = src._fmsgout
235 self._fmsgout = src._fmsgout
236 self._fmsgerr = src._fmsgerr
236 self._fmsgerr = src._fmsgerr
237 self._finoutredirected = src._finoutredirected
237 self._finoutredirected = src._finoutredirected
238 self.pageractive = src.pageractive
238 self.pageractive = src.pageractive
239 self._disablepager = src._disablepager
239 self._disablepager = src._disablepager
240 self._tweaked = src._tweaked
240 self._tweaked = src._tweaked
241
241
242 self._tcfg = src._tcfg.copy()
242 self._tcfg = src._tcfg.copy()
243 self._ucfg = src._ucfg.copy()
243 self._ucfg = src._ucfg.copy()
244 self._ocfg = src._ocfg.copy()
244 self._ocfg = src._ocfg.copy()
245 self._trustusers = src._trustusers.copy()
245 self._trustusers = src._trustusers.copy()
246 self._trustgroups = src._trustgroups.copy()
246 self._trustgroups = src._trustgroups.copy()
247 self.environ = src.environ
247 self.environ = src.environ
248 self.callhooks = src.callhooks
248 self.callhooks = src.callhooks
249 self.insecureconnections = src.insecureconnections
249 self.insecureconnections = src.insecureconnections
250 self._colormode = src._colormode
250 self._colormode = src._colormode
251 self._terminfoparams = src._terminfoparams.copy()
251 self._terminfoparams = src._terminfoparams.copy()
252 self._styles = src._styles.copy()
252 self._styles = src._styles.copy()
253
253
254 self.fixconfig()
254 self.fixconfig()
255
255
256 self.httppasswordmgrdb = src.httppasswordmgrdb
256 self.httppasswordmgrdb = src.httppasswordmgrdb
257 self._blockedtimes = src._blockedtimes
257 self._blockedtimes = src._blockedtimes
258 else:
258 else:
259 self._fout = procutil.stdout
259 self._fout = procutil.stdout
260 self._ferr = procutil.stderr
260 self._ferr = procutil.stderr
261 self._fin = procutil.stdin
261 self._fin = procutil.stdin
262 self._fmsg = None
262 self._fmsg = None
263 self._fmsgout = self.fout # configurable
263 self._fmsgout = self.fout # configurable
264 self._fmsgerr = self.ferr # configurable
264 self._fmsgerr = self.ferr # configurable
265 self._finoutredirected = False
265 self._finoutredirected = False
266 self.pageractive = False
266 self.pageractive = False
267 self._disablepager = False
267 self._disablepager = False
268 self._tweaked = False
268 self._tweaked = False
269
269
270 # shared read-only environment
270 # shared read-only environment
271 self.environ = encoding.environ
271 self.environ = encoding.environ
272
272
273 self.httppasswordmgrdb = httppasswordmgrdbproxy()
273 self.httppasswordmgrdb = httppasswordmgrdbproxy()
274 self._blockedtimes = collections.defaultdict(int)
274 self._blockedtimes = collections.defaultdict(int)
275
275
276 allowed = self.configlist('experimental', 'exportableenviron')
276 allowed = self.configlist('experimental', 'exportableenviron')
277 if '*' in allowed:
277 if '*' in allowed:
278 self._exportableenviron = self.environ
278 self._exportableenviron = self.environ
279 else:
279 else:
280 self._exportableenviron = {}
280 self._exportableenviron = {}
281 for k in allowed:
281 for k in allowed:
282 if k in self.environ:
282 if k in self.environ:
283 self._exportableenviron[k] = self.environ[k]
283 self._exportableenviron[k] = self.environ[k]
284
284
285 @classmethod
285 @classmethod
286 def load(cls):
286 def load(cls):
287 """Create a ui and load global and user configs"""
287 """Create a ui and load global and user configs"""
288 u = cls()
288 u = cls()
289 # we always trust global config files and environment variables
289 # we always trust global config files and environment variables
290 for t, f in rcutil.rccomponents():
290 for t, f in rcutil.rccomponents():
291 if t == 'path':
291 if t == 'path':
292 u.readconfig(f, trust=True)
292 u.readconfig(f, trust=True)
293 elif t == 'items':
293 elif t == 'items':
294 sections = set()
294 sections = set()
295 for section, name, value, source in f:
295 for section, name, value, source in f:
296 # do not set u._ocfg
296 # do not set u._ocfg
297 # XXX clean this up once immutable config object is a thing
297 # XXX clean this up once immutable config object is a thing
298 u._tcfg.set(section, name, value, source)
298 u._tcfg.set(section, name, value, source)
299 u._ucfg.set(section, name, value, source)
299 u._ucfg.set(section, name, value, source)
300 sections.add(section)
300 sections.add(section)
301 for section in sections:
301 for section in sections:
302 u.fixconfig(section=section)
302 u.fixconfig(section=section)
303 else:
303 else:
304 raise error.ProgrammingError('unknown rctype: %s' % t)
304 raise error.ProgrammingError('unknown rctype: %s' % t)
305 u._maybetweakdefaults()
305 u._maybetweakdefaults()
306 return u
306 return u
307
307
308 def _maybetweakdefaults(self):
308 def _maybetweakdefaults(self):
309 if not self.configbool('ui', 'tweakdefaults'):
309 if not self.configbool('ui', 'tweakdefaults'):
310 return
310 return
311 if self._tweaked or self.plain('tweakdefaults'):
311 if self._tweaked or self.plain('tweakdefaults'):
312 return
312 return
313
313
314 # Note: it is SUPER IMPORTANT that you set self._tweaked to
314 # Note: it is SUPER IMPORTANT that you set self._tweaked to
315 # True *before* any calls to setconfig(), otherwise you'll get
315 # True *before* any calls to setconfig(), otherwise you'll get
316 # infinite recursion between setconfig and this method.
316 # infinite recursion between setconfig and this method.
317 #
317 #
318 # TODO: We should extract an inner method in setconfig() to
318 # TODO: We should extract an inner method in setconfig() to
319 # avoid this weirdness.
319 # avoid this weirdness.
320 self._tweaked = True
320 self._tweaked = True
321 tmpcfg = config.config()
321 tmpcfg = config.config()
322 tmpcfg.parse('<tweakdefaults>', tweakrc)
322 tmpcfg.parse('<tweakdefaults>', tweakrc)
323 for section in tmpcfg:
323 for section in tmpcfg:
324 for name, value in tmpcfg.items(section):
324 for name, value in tmpcfg.items(section):
325 if not self.hasconfig(section, name):
325 if not self.hasconfig(section, name):
326 self.setconfig(section, name, value, "<tweakdefaults>")
326 self.setconfig(section, name, value, "<tweakdefaults>")
327
327
328 def copy(self):
328 def copy(self):
329 return self.__class__(self)
329 return self.__class__(self)
330
330
331 def resetstate(self):
331 def resetstate(self):
332 """Clear internal state that shouldn't persist across commands"""
332 """Clear internal state that shouldn't persist across commands"""
333 if self._progbar:
333 if self._progbar:
334 self._progbar.resetstate() # reset last-print time of progress bar
334 self._progbar.resetstate() # reset last-print time of progress bar
335 self.httppasswordmgrdb = httppasswordmgrdbproxy()
335 self.httppasswordmgrdb = httppasswordmgrdbproxy()
336
336
337 @contextlib.contextmanager
337 @contextlib.contextmanager
338 def timeblockedsection(self, key):
338 def timeblockedsection(self, key):
339 # this is open-coded below - search for timeblockedsection to find them
339 # this is open-coded below - search for timeblockedsection to find them
340 starttime = util.timer()
340 starttime = util.timer()
341 try:
341 try:
342 yield
342 yield
343 finally:
343 finally:
344 self._blockedtimes[key + '_blocked'] += \
344 self._blockedtimes[key + '_blocked'] += \
345 (util.timer() - starttime) * 1000
345 (util.timer() - starttime) * 1000
346
346
347 @contextlib.contextmanager
347 @contextlib.contextmanager
348 def uninterruptable(self):
348 def uninterruptable(self):
349 """Mark an operation as unsafe.
349 """Mark an operation as unsafe.
350
350
351 Most operations on a repository are safe to interrupt, but a
351 Most operations on a repository are safe to interrupt, but a
352 few are risky (for example repair.strip). This context manager
352 few are risky (for example repair.strip). This context manager
353 lets you advise Mercurial that something risky is happening so
353 lets you advise Mercurial that something risky is happening so
354 that control-C etc can be blocked if desired.
354 that control-C etc can be blocked if desired.
355 """
355 """
356 enabled = self.configbool('experimental', 'nointerrupt')
356 enabled = self.configbool('experimental', 'nointerrupt')
357 if (enabled and
357 if (enabled and
358 self.configbool('experimental', 'nointerrupt-interactiveonly')):
358 self.configbool('experimental', 'nointerrupt-interactiveonly')):
359 enabled = self.interactive()
359 enabled = self.interactive()
360 if self._uninterruptible or not enabled:
360 if self._uninterruptible or not enabled:
361 # if nointerrupt support is turned off, the process isn't
361 # if nointerrupt support is turned off, the process isn't
362 # interactive, or we're already in an uninterruptable
362 # interactive, or we're already in an uninterruptable
363 # block, do nothing.
363 # block, do nothing.
364 yield
364 yield
365 return
365 return
366 def warn():
366 def warn():
367 self.warn(_("shutting down cleanly\n"))
367 self.warn(_("shutting down cleanly\n"))
368 self.warn(
368 self.warn(
369 _("press ^C again to terminate immediately (dangerous)\n"))
369 _("press ^C again to terminate immediately (dangerous)\n"))
370 return True
370 return True
371 with procutil.uninterruptable(warn):
371 with procutil.uninterruptable(warn):
372 try:
372 try:
373 self._uninterruptible = True
373 self._uninterruptible = True
374 yield
374 yield
375 finally:
375 finally:
376 self._uninterruptible = False
376 self._uninterruptible = False
377
377
378 def formatter(self, topic, opts):
378 def formatter(self, topic, opts):
379 return formatter.formatter(self, self, topic, opts)
379 return formatter.formatter(self, self, topic, opts)
380
380
381 def _trusted(self, fp, f):
381 def _trusted(self, fp, f):
382 st = util.fstat(fp)
382 st = util.fstat(fp)
383 if util.isowner(st):
383 if util.isowner(st):
384 return True
384 return True
385
385
386 tusers, tgroups = self._trustusers, self._trustgroups
386 tusers, tgroups = self._trustusers, self._trustgroups
387 if '*' in tusers or '*' in tgroups:
387 if '*' in tusers or '*' in tgroups:
388 return True
388 return True
389
389
390 user = util.username(st.st_uid)
390 user = util.username(st.st_uid)
391 group = util.groupname(st.st_gid)
391 group = util.groupname(st.st_gid)
392 if user in tusers or group in tgroups or user == util.username():
392 if user in tusers or group in tgroups or user == util.username():
393 return True
393 return True
394
394
395 if self._reportuntrusted:
395 if self._reportuntrusted:
396 self.warn(_('not trusting file %s from untrusted '
396 self.warn(_('not trusting file %s from untrusted '
397 'user %s, group %s\n') % (f, user, group))
397 'user %s, group %s\n') % (f, user, group))
398 return False
398 return False
399
399
400 def readconfig(self, filename, root=None, trust=False,
400 def readconfig(self, filename, root=None, trust=False,
401 sections=None, remap=None):
401 sections=None, remap=None):
402 try:
402 try:
403 fp = open(filename, r'rb')
403 fp = open(filename, r'rb')
404 except IOError:
404 except IOError:
405 if not sections: # ignore unless we were looking for something
405 if not sections: # ignore unless we were looking for something
406 return
406 return
407 raise
407 raise
408
408
409 cfg = config.config()
409 cfg = config.config()
410 trusted = sections or trust or self._trusted(fp, filename)
410 trusted = sections or trust or self._trusted(fp, filename)
411
411
412 try:
412 try:
413 cfg.read(filename, fp, sections=sections, remap=remap)
413 cfg.read(filename, fp, sections=sections, remap=remap)
414 fp.close()
414 fp.close()
415 except error.ConfigError as inst:
415 except error.ConfigError as inst:
416 if trusted:
416 if trusted:
417 raise
417 raise
418 self.warn(_("ignored: %s\n") % stringutil.forcebytestr(inst))
418 self.warn(_("ignored: %s\n") % stringutil.forcebytestr(inst))
419
419
420 if self.plain():
420 if self.plain():
421 for k in ('debug', 'fallbackencoding', 'quiet', 'slash',
421 for k in ('debug', 'fallbackencoding', 'quiet', 'slash',
422 'logtemplate', 'message-output', 'statuscopies', 'style',
422 'logtemplate', 'message-output', 'statuscopies', 'style',
423 'traceback', 'verbose'):
423 'traceback', 'verbose'):
424 if k in cfg['ui']:
424 if k in cfg['ui']:
425 del cfg['ui'][k]
425 del cfg['ui'][k]
426 for k, v in cfg.items('defaults'):
426 for k, v in cfg.items('defaults'):
427 del cfg['defaults'][k]
427 del cfg['defaults'][k]
428 for k, v in cfg.items('commands'):
428 for k, v in cfg.items('commands'):
429 del cfg['commands'][k]
429 del cfg['commands'][k]
430 # Don't remove aliases from the configuration if in the exceptionlist
430 # Don't remove aliases from the configuration if in the exceptionlist
431 if self.plain('alias'):
431 if self.plain('alias'):
432 for k, v in cfg.items('alias'):
432 for k, v in cfg.items('alias'):
433 del cfg['alias'][k]
433 del cfg['alias'][k]
434 if self.plain('revsetalias'):
434 if self.plain('revsetalias'):
435 for k, v in cfg.items('revsetalias'):
435 for k, v in cfg.items('revsetalias'):
436 del cfg['revsetalias'][k]
436 del cfg['revsetalias'][k]
437 if self.plain('templatealias'):
437 if self.plain('templatealias'):
438 for k, v in cfg.items('templatealias'):
438 for k, v in cfg.items('templatealias'):
439 del cfg['templatealias'][k]
439 del cfg['templatealias'][k]
440
440
441 if trusted:
441 if trusted:
442 self._tcfg.update(cfg)
442 self._tcfg.update(cfg)
443 self._tcfg.update(self._ocfg)
443 self._tcfg.update(self._ocfg)
444 self._ucfg.update(cfg)
444 self._ucfg.update(cfg)
445 self._ucfg.update(self._ocfg)
445 self._ucfg.update(self._ocfg)
446
446
447 if root is None:
447 if root is None:
448 root = os.path.expanduser('~')
448 root = os.path.expanduser('~')
449 self.fixconfig(root=root)
449 self.fixconfig(root=root)
450
450
451 def fixconfig(self, root=None, section=None):
451 def fixconfig(self, root=None, section=None):
452 if section in (None, 'paths'):
452 if section in (None, 'paths'):
453 # expand vars and ~
453 # expand vars and ~
454 # translate paths relative to root (or home) into absolute paths
454 # translate paths relative to root (or home) into absolute paths
455 root = root or encoding.getcwd()
455 root = root or encoding.getcwd()
456 for c in self._tcfg, self._ucfg, self._ocfg:
456 for c in self._tcfg, self._ucfg, self._ocfg:
457 for n, p in c.items('paths'):
457 for n, p in c.items('paths'):
458 # Ignore sub-options.
458 # Ignore sub-options.
459 if ':' in n:
459 if ':' in n:
460 continue
460 continue
461 if not p:
461 if not p:
462 continue
462 continue
463 if '%%' in p:
463 if '%%' in p:
464 s = self.configsource('paths', n) or 'none'
464 s = self.configsource('paths', n) or 'none'
465 self.warn(_("(deprecated '%%' in path %s=%s from %s)\n")
465 self.warn(_("(deprecated '%%' in path %s=%s from %s)\n")
466 % (n, p, s))
466 % (n, p, s))
467 p = p.replace('%%', '%')
467 p = p.replace('%%', '%')
468 p = util.expandpath(p)
468 p = util.expandpath(p)
469 if not util.hasscheme(p) and not os.path.isabs(p):
469 if not util.hasscheme(p) and not os.path.isabs(p):
470 p = os.path.normpath(os.path.join(root, p))
470 p = os.path.normpath(os.path.join(root, p))
471 c.set("paths", n, p)
471 c.set("paths", n, p)
472
472
473 if section in (None, 'ui'):
473 if section in (None, 'ui'):
474 # update ui options
474 # update ui options
475 self._fmsgout, self._fmsgerr = _selectmsgdests(self)
475 self._fmsgout, self._fmsgerr = _selectmsgdests(self)
476 self.debugflag = self.configbool('ui', 'debug')
476 self.debugflag = self.configbool('ui', 'debug')
477 self.verbose = self.debugflag or self.configbool('ui', 'verbose')
477 self.verbose = self.debugflag or self.configbool('ui', 'verbose')
478 self.quiet = not self.debugflag and self.configbool('ui', 'quiet')
478 self.quiet = not self.debugflag and self.configbool('ui', 'quiet')
479 if self.verbose and self.quiet:
479 if self.verbose and self.quiet:
480 self.quiet = self.verbose = False
480 self.quiet = self.verbose = False
481 self._reportuntrusted = self.debugflag or self.configbool("ui",
481 self._reportuntrusted = self.debugflag or self.configbool("ui",
482 "report_untrusted")
482 "report_untrusted")
483 self.tracebackflag = self.configbool('ui', 'traceback')
483 self.tracebackflag = self.configbool('ui', 'traceback')
484 self.logblockedtimes = self.configbool('ui', 'logblockedtimes')
484 self.logblockedtimes = self.configbool('ui', 'logblockedtimes')
485
485
486 if section in (None, 'trusted'):
486 if section in (None, 'trusted'):
487 # update trust information
487 # update trust information
488 self._trustusers.update(self.configlist('trusted', 'users'))
488 self._trustusers.update(self.configlist('trusted', 'users'))
489 self._trustgroups.update(self.configlist('trusted', 'groups'))
489 self._trustgroups.update(self.configlist('trusted', 'groups'))
490
490
491 def backupconfig(self, section, item):
491 def backupconfig(self, section, item):
492 return (self._ocfg.backup(section, item),
492 return (self._ocfg.backup(section, item),
493 self._tcfg.backup(section, item),
493 self._tcfg.backup(section, item),
494 self._ucfg.backup(section, item),)
494 self._ucfg.backup(section, item),)
495 def restoreconfig(self, data):
495 def restoreconfig(self, data):
496 self._ocfg.restore(data[0])
496 self._ocfg.restore(data[0])
497 self._tcfg.restore(data[1])
497 self._tcfg.restore(data[1])
498 self._ucfg.restore(data[2])
498 self._ucfg.restore(data[2])
499
499
500 def setconfig(self, section, name, value, source=''):
500 def setconfig(self, section, name, value, source=''):
501 for cfg in (self._ocfg, self._tcfg, self._ucfg):
501 for cfg in (self._ocfg, self._tcfg, self._ucfg):
502 cfg.set(section, name, value, source)
502 cfg.set(section, name, value, source)
503 self.fixconfig(section=section)
503 self.fixconfig(section=section)
504 self._maybetweakdefaults()
504 self._maybetweakdefaults()
505
505
506 def _data(self, untrusted):
506 def _data(self, untrusted):
507 return untrusted and self._ucfg or self._tcfg
507 return untrusted and self._ucfg or self._tcfg
508
508
509 def configsource(self, section, name, untrusted=False):
509 def configsource(self, section, name, untrusted=False):
510 return self._data(untrusted).source(section, name)
510 return self._data(untrusted).source(section, name)
511
511
512 def config(self, section, name, default=_unset, untrusted=False):
512 def config(self, section, name, default=_unset, untrusted=False):
513 """return the plain string version of a config"""
513 """return the plain string version of a config"""
514 value = self._config(section, name, default=default,
514 value = self._config(section, name, default=default,
515 untrusted=untrusted)
515 untrusted=untrusted)
516 if value is _unset:
516 if value is _unset:
517 return None
517 return None
518 return value
518 return value
519
519
520 def _config(self, section, name, default=_unset, untrusted=False):
520 def _config(self, section, name, default=_unset, untrusted=False):
521 value = itemdefault = default
521 value = itemdefault = default
522 item = self._knownconfig.get(section, {}).get(name)
522 item = self._knownconfig.get(section, {}).get(name)
523 alternates = [(section, name)]
523 alternates = [(section, name)]
524
524
525 if item is not None:
525 if item is not None:
526 alternates.extend(item.alias)
526 alternates.extend(item.alias)
527 if callable(item.default):
527 if callable(item.default):
528 itemdefault = item.default()
528 itemdefault = item.default()
529 else:
529 else:
530 itemdefault = item.default
530 itemdefault = item.default
531 else:
531 else:
532 msg = ("accessing unregistered config item: '%s.%s'")
532 msg = ("accessing unregistered config item: '%s.%s'")
533 msg %= (section, name)
533 msg %= (section, name)
534 self.develwarn(msg, 2, 'warn-config-unknown')
534 self.develwarn(msg, 2, 'warn-config-unknown')
535
535
536 if default is _unset:
536 if default is _unset:
537 if item is None:
537 if item is None:
538 value = default
538 value = default
539 elif item.default is configitems.dynamicdefault:
539 elif item.default is configitems.dynamicdefault:
540 value = None
540 value = None
541 msg = "config item requires an explicit default value: '%s.%s'"
541 msg = "config item requires an explicit default value: '%s.%s'"
542 msg %= (section, name)
542 msg %= (section, name)
543 self.develwarn(msg, 2, 'warn-config-default')
543 self.develwarn(msg, 2, 'warn-config-default')
544 else:
544 else:
545 value = itemdefault
545 value = itemdefault
546 elif (item is not None
546 elif (item is not None
547 and item.default is not configitems.dynamicdefault
547 and item.default is not configitems.dynamicdefault
548 and default != itemdefault):
548 and default != itemdefault):
549 msg = ("specifying a mismatched default value for a registered "
549 msg = ("specifying a mismatched default value for a registered "
550 "config item: '%s.%s' '%s'")
550 "config item: '%s.%s' '%s'")
551 msg %= (section, name, pycompat.bytestr(default))
551 msg %= (section, name, pycompat.bytestr(default))
552 self.develwarn(msg, 2, 'warn-config-default')
552 self.develwarn(msg, 2, 'warn-config-default')
553
553
554 for s, n in alternates:
554 for s, n in alternates:
555 candidate = self._data(untrusted).get(s, n, None)
555 candidate = self._data(untrusted).get(s, n, None)
556 if candidate is not None:
556 if candidate is not None:
557 value = candidate
557 value = candidate
558 section = s
558 section = s
559 name = n
559 name = n
560 break
560 break
561
561
562 if self.debugflag and not untrusted and self._reportuntrusted:
562 if self.debugflag and not untrusted and self._reportuntrusted:
563 for s, n in alternates:
563 for s, n in alternates:
564 uvalue = self._ucfg.get(s, n)
564 uvalue = self._ucfg.get(s, n)
565 if uvalue is not None and uvalue != value:
565 if uvalue is not None and uvalue != value:
566 self.debug("ignoring untrusted configuration option "
566 self.debug("ignoring untrusted configuration option "
567 "%s.%s = %s\n" % (s, n, uvalue))
567 "%s.%s = %s\n" % (s, n, uvalue))
568 return value
568 return value
569
569
570 def configsuboptions(self, section, name, default=_unset, untrusted=False):
570 def configsuboptions(self, section, name, default=_unset, untrusted=False):
571 """Get a config option and all sub-options.
571 """Get a config option and all sub-options.
572
572
573 Some config options have sub-options that are declared with the
573 Some config options have sub-options that are declared with the
574 format "key:opt = value". This method is used to return the main
574 format "key:opt = value". This method is used to return the main
575 option and all its declared sub-options.
575 option and all its declared sub-options.
576
576
577 Returns a 2-tuple of ``(option, sub-options)``, where `sub-options``
577 Returns a 2-tuple of ``(option, sub-options)``, where `sub-options``
578 is a dict of defined sub-options where keys and values are strings.
578 is a dict of defined sub-options where keys and values are strings.
579 """
579 """
580 main = self.config(section, name, default, untrusted=untrusted)
580 main = self.config(section, name, default, untrusted=untrusted)
581 data = self._data(untrusted)
581 data = self._data(untrusted)
582 sub = {}
582 sub = {}
583 prefix = '%s:' % name
583 prefix = '%s:' % name
584 for k, v in data.items(section):
584 for k, v in data.items(section):
585 if k.startswith(prefix):
585 if k.startswith(prefix):
586 sub[k[len(prefix):]] = v
586 sub[k[len(prefix):]] = v
587
587
588 if self.debugflag and not untrusted and self._reportuntrusted:
588 if self.debugflag and not untrusted and self._reportuntrusted:
589 for k, v in sub.items():
589 for k, v in sub.items():
590 uvalue = self._ucfg.get(section, '%s:%s' % (name, k))
590 uvalue = self._ucfg.get(section, '%s:%s' % (name, k))
591 if uvalue is not None and uvalue != v:
591 if uvalue is not None and uvalue != v:
592 self.debug('ignoring untrusted configuration option '
592 self.debug('ignoring untrusted configuration option '
593 '%s:%s.%s = %s\n' % (section, name, k, uvalue))
593 '%s:%s.%s = %s\n' % (section, name, k, uvalue))
594
594
595 return main, sub
595 return main, sub
596
596
597 def configpath(self, section, name, default=_unset, untrusted=False):
597 def configpath(self, section, name, default=_unset, untrusted=False):
598 'get a path config item, expanded relative to repo root or config file'
598 'get a path config item, expanded relative to repo root or config file'
599 v = self.config(section, name, default, untrusted)
599 v = self.config(section, name, default, untrusted)
600 if v is None:
600 if v is None:
601 return None
601 return None
602 if not os.path.isabs(v) or "://" not in v:
602 if not os.path.isabs(v) or "://" not in v:
603 src = self.configsource(section, name, untrusted)
603 src = self.configsource(section, name, untrusted)
604 if ':' in src:
604 if ':' in src:
605 base = os.path.dirname(src.rsplit(':')[0])
605 base = os.path.dirname(src.rsplit(':')[0])
606 v = os.path.join(base, os.path.expanduser(v))
606 v = os.path.join(base, os.path.expanduser(v))
607 return v
607 return v
608
608
609 def configbool(self, section, name, default=_unset, untrusted=False):
609 def configbool(self, section, name, default=_unset, untrusted=False):
610 """parse a configuration element as a boolean
610 """parse a configuration element as a boolean
611
611
612 >>> u = ui(); s = b'foo'
612 >>> u = ui(); s = b'foo'
613 >>> u.setconfig(s, b'true', b'yes')
613 >>> u.setconfig(s, b'true', b'yes')
614 >>> u.configbool(s, b'true')
614 >>> u.configbool(s, b'true')
615 True
615 True
616 >>> u.setconfig(s, b'false', b'no')
616 >>> u.setconfig(s, b'false', b'no')
617 >>> u.configbool(s, b'false')
617 >>> u.configbool(s, b'false')
618 False
618 False
619 >>> u.configbool(s, b'unknown')
619 >>> u.configbool(s, b'unknown')
620 False
620 False
621 >>> u.configbool(s, b'unknown', True)
621 >>> u.configbool(s, b'unknown', True)
622 True
622 True
623 >>> u.setconfig(s, b'invalid', b'somevalue')
623 >>> u.setconfig(s, b'invalid', b'somevalue')
624 >>> u.configbool(s, b'invalid')
624 >>> u.configbool(s, b'invalid')
625 Traceback (most recent call last):
625 Traceback (most recent call last):
626 ...
626 ...
627 ConfigError: foo.invalid is not a boolean ('somevalue')
627 ConfigError: foo.invalid is not a boolean ('somevalue')
628 """
628 """
629
629
630 v = self._config(section, name, default, untrusted=untrusted)
630 v = self._config(section, name, default, untrusted=untrusted)
631 if v is None:
631 if v is None:
632 return v
632 return v
633 if v is _unset:
633 if v is _unset:
634 if default is _unset:
634 if default is _unset:
635 return False
635 return False
636 return default
636 return default
637 if isinstance(v, bool):
637 if isinstance(v, bool):
638 return v
638 return v
639 b = stringutil.parsebool(v)
639 b = stringutil.parsebool(v)
640 if b is None:
640 if b is None:
641 raise error.ConfigError(_("%s.%s is not a boolean ('%s')")
641 raise error.ConfigError(_("%s.%s is not a boolean ('%s')")
642 % (section, name, v))
642 % (section, name, v))
643 return b
643 return b
644
644
645 def configwith(self, convert, section, name, default=_unset,
645 def configwith(self, convert, section, name, default=_unset,
646 desc=None, untrusted=False):
646 desc=None, untrusted=False):
647 """parse a configuration element with a conversion function
647 """parse a configuration element with a conversion function
648
648
649 >>> u = ui(); s = b'foo'
649 >>> u = ui(); s = b'foo'
650 >>> u.setconfig(s, b'float1', b'42')
650 >>> u.setconfig(s, b'float1', b'42')
651 >>> u.configwith(float, s, b'float1')
651 >>> u.configwith(float, s, b'float1')
652 42.0
652 42.0
653 >>> u.setconfig(s, b'float2', b'-4.25')
653 >>> u.setconfig(s, b'float2', b'-4.25')
654 >>> u.configwith(float, s, b'float2')
654 >>> u.configwith(float, s, b'float2')
655 -4.25
655 -4.25
656 >>> u.configwith(float, s, b'unknown', 7)
656 >>> u.configwith(float, s, b'unknown', 7)
657 7.0
657 7.0
658 >>> u.setconfig(s, b'invalid', b'somevalue')
658 >>> u.setconfig(s, b'invalid', b'somevalue')
659 >>> u.configwith(float, s, b'invalid')
659 >>> u.configwith(float, s, b'invalid')
660 Traceback (most recent call last):
660 Traceback (most recent call last):
661 ...
661 ...
662 ConfigError: foo.invalid is not a valid float ('somevalue')
662 ConfigError: foo.invalid is not a valid float ('somevalue')
663 >>> u.configwith(float, s, b'invalid', desc=b'womble')
663 >>> u.configwith(float, s, b'invalid', desc=b'womble')
664 Traceback (most recent call last):
664 Traceback (most recent call last):
665 ...
665 ...
666 ConfigError: foo.invalid is not a valid womble ('somevalue')
666 ConfigError: foo.invalid is not a valid womble ('somevalue')
667 """
667 """
668
668
669 v = self.config(section, name, default, untrusted)
669 v = self.config(section, name, default, untrusted)
670 if v is None:
670 if v is None:
671 return v # do not attempt to convert None
671 return v # do not attempt to convert None
672 try:
672 try:
673 return convert(v)
673 return convert(v)
674 except (ValueError, error.ParseError):
674 except (ValueError, error.ParseError):
675 if desc is None:
675 if desc is None:
676 desc = pycompat.sysbytes(convert.__name__)
676 desc = pycompat.sysbytes(convert.__name__)
677 raise error.ConfigError(_("%s.%s is not a valid %s ('%s')")
677 raise error.ConfigError(_("%s.%s is not a valid %s ('%s')")
678 % (section, name, desc, v))
678 % (section, name, desc, v))
679
679
680 def configint(self, section, name, default=_unset, untrusted=False):
680 def configint(self, section, name, default=_unset, untrusted=False):
681 """parse a configuration element as an integer
681 """parse a configuration element as an integer
682
682
683 >>> u = ui(); s = b'foo'
683 >>> u = ui(); s = b'foo'
684 >>> u.setconfig(s, b'int1', b'42')
684 >>> u.setconfig(s, b'int1', b'42')
685 >>> u.configint(s, b'int1')
685 >>> u.configint(s, b'int1')
686 42
686 42
687 >>> u.setconfig(s, b'int2', b'-42')
687 >>> u.setconfig(s, b'int2', b'-42')
688 >>> u.configint(s, b'int2')
688 >>> u.configint(s, b'int2')
689 -42
689 -42
690 >>> u.configint(s, b'unknown', 7)
690 >>> u.configint(s, b'unknown', 7)
691 7
691 7
692 >>> u.setconfig(s, b'invalid', b'somevalue')
692 >>> u.setconfig(s, b'invalid', b'somevalue')
693 >>> u.configint(s, b'invalid')
693 >>> u.configint(s, b'invalid')
694 Traceback (most recent call last):
694 Traceback (most recent call last):
695 ...
695 ...
696 ConfigError: foo.invalid is not a valid integer ('somevalue')
696 ConfigError: foo.invalid is not a valid integer ('somevalue')
697 """
697 """
698
698
699 return self.configwith(int, section, name, default, 'integer',
699 return self.configwith(int, section, name, default, 'integer',
700 untrusted)
700 untrusted)
701
701
702 def configbytes(self, section, name, default=_unset, untrusted=False):
702 def configbytes(self, section, name, default=_unset, untrusted=False):
703 """parse a configuration element as a quantity in bytes
703 """parse a configuration element as a quantity in bytes
704
704
705 Units can be specified as b (bytes), k or kb (kilobytes), m or
705 Units can be specified as b (bytes), k or kb (kilobytes), m or
706 mb (megabytes), g or gb (gigabytes).
706 mb (megabytes), g or gb (gigabytes).
707
707
708 >>> u = ui(); s = b'foo'
708 >>> u = ui(); s = b'foo'
709 >>> u.setconfig(s, b'val1', b'42')
709 >>> u.setconfig(s, b'val1', b'42')
710 >>> u.configbytes(s, b'val1')
710 >>> u.configbytes(s, b'val1')
711 42
711 42
712 >>> u.setconfig(s, b'val2', b'42.5 kb')
712 >>> u.setconfig(s, b'val2', b'42.5 kb')
713 >>> u.configbytes(s, b'val2')
713 >>> u.configbytes(s, b'val2')
714 43520
714 43520
715 >>> u.configbytes(s, b'unknown', b'7 MB')
715 >>> u.configbytes(s, b'unknown', b'7 MB')
716 7340032
716 7340032
717 >>> u.setconfig(s, b'invalid', b'somevalue')
717 >>> u.setconfig(s, b'invalid', b'somevalue')
718 >>> u.configbytes(s, b'invalid')
718 >>> u.configbytes(s, b'invalid')
719 Traceback (most recent call last):
719 Traceback (most recent call last):
720 ...
720 ...
721 ConfigError: foo.invalid is not a byte quantity ('somevalue')
721 ConfigError: foo.invalid is not a byte quantity ('somevalue')
722 """
722 """
723
723
724 value = self._config(section, name, default, untrusted)
724 value = self._config(section, name, default, untrusted)
725 if value is _unset:
725 if value is _unset:
726 if default is _unset:
726 if default is _unset:
727 default = 0
727 default = 0
728 value = default
728 value = default
729 if not isinstance(value, bytes):
729 if not isinstance(value, bytes):
730 return value
730 return value
731 try:
731 try:
732 return util.sizetoint(value)
732 return util.sizetoint(value)
733 except error.ParseError:
733 except error.ParseError:
734 raise error.ConfigError(_("%s.%s is not a byte quantity ('%s')")
734 raise error.ConfigError(_("%s.%s is not a byte quantity ('%s')")
735 % (section, name, value))
735 % (section, name, value))
736
736
737 def configlist(self, section, name, default=_unset, untrusted=False):
737 def configlist(self, section, name, default=_unset, untrusted=False):
738 """parse a configuration element as a list of comma/space separated
738 """parse a configuration element as a list of comma/space separated
739 strings
739 strings
740
740
741 >>> u = ui(); s = b'foo'
741 >>> u = ui(); s = b'foo'
742 >>> u.setconfig(s, b'list1', b'this,is "a small" ,test')
742 >>> u.setconfig(s, b'list1', b'this,is "a small" ,test')
743 >>> u.configlist(s, b'list1')
743 >>> u.configlist(s, b'list1')
744 ['this', 'is', 'a small', 'test']
744 ['this', 'is', 'a small', 'test']
745 >>> u.setconfig(s, b'list2', b'this, is "a small" , test ')
745 >>> u.setconfig(s, b'list2', b'this, is "a small" , test ')
746 >>> u.configlist(s, b'list2')
746 >>> u.configlist(s, b'list2')
747 ['this', 'is', 'a small', 'test']
747 ['this', 'is', 'a small', 'test']
748 """
748 """
749 # default is not always a list
749 # default is not always a list
750 v = self.configwith(config.parselist, section, name, default,
750 v = self.configwith(config.parselist, section, name, default,
751 'list', untrusted)
751 'list', untrusted)
752 if isinstance(v, bytes):
752 if isinstance(v, bytes):
753 return config.parselist(v)
753 return config.parselist(v)
754 elif v is None:
754 elif v is None:
755 return []
755 return []
756 return v
756 return v
757
757
758 def configdate(self, section, name, default=_unset, untrusted=False):
758 def configdate(self, section, name, default=_unset, untrusted=False):
759 """parse a configuration element as a tuple of ints
759 """parse a configuration element as a tuple of ints
760
760
761 >>> u = ui(); s = b'foo'
761 >>> u = ui(); s = b'foo'
762 >>> u.setconfig(s, b'date', b'0 0')
762 >>> u.setconfig(s, b'date', b'0 0')
763 >>> u.configdate(s, b'date')
763 >>> u.configdate(s, b'date')
764 (0, 0)
764 (0, 0)
765 """
765 """
766 if self.config(section, name, default, untrusted):
766 if self.config(section, name, default, untrusted):
767 return self.configwith(dateutil.parsedate, section, name, default,
767 return self.configwith(dateutil.parsedate, section, name, default,
768 'date', untrusted)
768 'date', untrusted)
769 if default is _unset:
769 if default is _unset:
770 return None
770 return None
771 return default
771 return default
772
772
773 def hasconfig(self, section, name, untrusted=False):
773 def hasconfig(self, section, name, untrusted=False):
774 return self._data(untrusted).hasitem(section, name)
774 return self._data(untrusted).hasitem(section, name)
775
775
776 def has_section(self, section, untrusted=False):
776 def has_section(self, section, untrusted=False):
777 '''tell whether section exists in config.'''
777 '''tell whether section exists in config.'''
778 return section in self._data(untrusted)
778 return section in self._data(untrusted)
779
779
780 def configitems(self, section, untrusted=False, ignoresub=False):
780 def configitems(self, section, untrusted=False, ignoresub=False):
781 items = self._data(untrusted).items(section)
781 items = self._data(untrusted).items(section)
782 if ignoresub:
782 if ignoresub:
783 items = [i for i in items if ':' not in i[0]]
783 items = [i for i in items if ':' not in i[0]]
784 if self.debugflag and not untrusted and self._reportuntrusted:
784 if self.debugflag and not untrusted and self._reportuntrusted:
785 for k, v in self._ucfg.items(section):
785 for k, v in self._ucfg.items(section):
786 if self._tcfg.get(section, k) != v:
786 if self._tcfg.get(section, k) != v:
787 self.debug("ignoring untrusted configuration option "
787 self.debug("ignoring untrusted configuration option "
788 "%s.%s = %s\n" % (section, k, v))
788 "%s.%s = %s\n" % (section, k, v))
789 return items
789 return items
790
790
791 def walkconfig(self, untrusted=False):
791 def walkconfig(self, untrusted=False):
792 cfg = self._data(untrusted)
792 cfg = self._data(untrusted)
793 for section in cfg.sections():
793 for section in cfg.sections():
794 for name, value in self.configitems(section, untrusted):
794 for name, value in self.configitems(section, untrusted):
795 yield section, name, value
795 yield section, name, value
796
796
797 def plain(self, feature=None):
797 def plain(self, feature=None):
798 '''is plain mode active?
798 '''is plain mode active?
799
799
800 Plain mode means that all configuration variables which affect
800 Plain mode means that all configuration variables which affect
801 the behavior and output of Mercurial should be
801 the behavior and output of Mercurial should be
802 ignored. Additionally, the output should be stable,
802 ignored. Additionally, the output should be stable,
803 reproducible and suitable for use in scripts or applications.
803 reproducible and suitable for use in scripts or applications.
804
804
805 The only way to trigger plain mode is by setting either the
805 The only way to trigger plain mode is by setting either the
806 `HGPLAIN' or `HGPLAINEXCEPT' environment variables.
806 `HGPLAIN' or `HGPLAINEXCEPT' environment variables.
807
807
808 The return value can either be
808 The return value can either be
809 - False if HGPLAIN is not set, or feature is in HGPLAINEXCEPT
809 - False if HGPLAIN is not set, or feature is in HGPLAINEXCEPT
810 - False if feature is disabled by default and not included in HGPLAIN
810 - False if feature is disabled by default and not included in HGPLAIN
811 - True otherwise
811 - True otherwise
812 '''
812 '''
813 if ('HGPLAIN' not in encoding.environ and
813 if ('HGPLAIN' not in encoding.environ and
814 'HGPLAINEXCEPT' not in encoding.environ):
814 'HGPLAINEXCEPT' not in encoding.environ):
815 return False
815 return False
816 exceptions = encoding.environ.get('HGPLAINEXCEPT',
816 exceptions = encoding.environ.get('HGPLAINEXCEPT',
817 '').strip().split(',')
817 '').strip().split(',')
818 # TODO: add support for HGPLAIN=+feature,-feature syntax
818 # TODO: add support for HGPLAIN=+feature,-feature syntax
819 if '+strictflags' not in encoding.environ.get('HGPLAIN', '').split(','):
819 if '+strictflags' not in encoding.environ.get('HGPLAIN', '').split(','):
820 exceptions.append('strictflags')
820 exceptions.append('strictflags')
821 if feature and exceptions:
821 if feature and exceptions:
822 return feature not in exceptions
822 return feature not in exceptions
823 return True
823 return True
824
824
825 def username(self, acceptempty=False):
825 def username(self, acceptempty=False):
826 """Return default username to be used in commits.
826 """Return default username to be used in commits.
827
827
828 Searched in this order: $HGUSER, [ui] section of hgrcs, $EMAIL
828 Searched in this order: $HGUSER, [ui] section of hgrcs, $EMAIL
829 and stop searching if one of these is set.
829 and stop searching if one of these is set.
830 If not found and acceptempty is True, returns None.
830 If not found and acceptempty is True, returns None.
831 If not found and ui.askusername is True, ask the user, else use
831 If not found and ui.askusername is True, ask the user, else use
832 ($LOGNAME or $USER or $LNAME or $USERNAME) + "@full.hostname".
832 ($LOGNAME or $USER or $LNAME or $USERNAME) + "@full.hostname".
833 If no username could be found, raise an Abort error.
833 If no username could be found, raise an Abort error.
834 """
834 """
835 user = encoding.environ.get("HGUSER")
835 user = encoding.environ.get("HGUSER")
836 if user is None:
836 if user is None:
837 user = self.config("ui", "username")
837 user = self.config("ui", "username")
838 if user is not None:
838 if user is not None:
839 user = os.path.expandvars(user)
839 user = os.path.expandvars(user)
840 if user is None:
840 if user is None:
841 user = encoding.environ.get("EMAIL")
841 user = encoding.environ.get("EMAIL")
842 if user is None and acceptempty:
842 if user is None and acceptempty:
843 return user
843 return user
844 if user is None and self.configbool("ui", "askusername"):
844 if user is None and self.configbool("ui", "askusername"):
845 user = self.prompt(_("enter a commit username:"), default=None)
845 user = self.prompt(_("enter a commit username:"), default=None)
846 if user is None and not self.interactive():
846 if user is None and not self.interactive():
847 try:
847 try:
848 user = '%s@%s' % (procutil.getuser(),
848 user = '%s@%s' % (procutil.getuser(),
849 encoding.strtolocal(socket.getfqdn()))
849 encoding.strtolocal(socket.getfqdn()))
850 self.warn(_("no username found, using '%s' instead\n") % user)
850 self.warn(_("no username found, using '%s' instead\n") % user)
851 except KeyError:
851 except KeyError:
852 pass
852 pass
853 if not user:
853 if not user:
854 raise error.Abort(_('no username supplied'),
854 raise error.Abort(_('no username supplied'),
855 hint=_("use 'hg config --edit' "
855 hint=_("use 'hg config --edit' "
856 'to set your username'))
856 'to set your username'))
857 if "\n" in user:
857 if "\n" in user:
858 raise error.Abort(_("username %r contains a newline\n")
858 raise error.Abort(_("username %r contains a newline\n")
859 % pycompat.bytestr(user))
859 % pycompat.bytestr(user))
860 return user
860 return user
861
861
862 def shortuser(self, user):
862 def shortuser(self, user):
863 """Return a short representation of a user name or email address."""
863 """Return a short representation of a user name or email address."""
864 if not self.verbose:
864 if not self.verbose:
865 user = stringutil.shortuser(user)
865 user = stringutil.shortuser(user)
866 return user
866 return user
867
867
868 def expandpath(self, loc, default=None):
868 def expandpath(self, loc, default=None):
869 """Return repository location relative to cwd or from [paths]"""
869 """Return repository location relative to cwd or from [paths]"""
870 try:
870 try:
871 p = self.paths.getpath(loc)
871 p = self.paths.getpath(loc)
872 if p:
872 if p:
873 return p.rawloc
873 return p.rawloc
874 except error.RepoError:
874 except error.RepoError:
875 pass
875 pass
876
876
877 if default:
877 if default:
878 try:
878 try:
879 p = self.paths.getpath(default)
879 p = self.paths.getpath(default)
880 if p:
880 if p:
881 return p.rawloc
881 return p.rawloc
882 except error.RepoError:
882 except error.RepoError:
883 pass
883 pass
884
884
885 return loc
885 return loc
886
886
887 @util.propertycache
887 @util.propertycache
888 def paths(self):
888 def paths(self):
889 return paths(self)
889 return paths(self)
890
890
891 @property
891 @property
892 def fout(self):
892 def fout(self):
893 return self._fout
893 return self._fout
894
894
895 @fout.setter
895 @fout.setter
896 def fout(self, f):
896 def fout(self, f):
897 self._fout = f
897 self._fout = f
898 self._fmsgout, self._fmsgerr = _selectmsgdests(self)
898 self._fmsgout, self._fmsgerr = _selectmsgdests(self)
899
899
900 @property
900 @property
901 def ferr(self):
901 def ferr(self):
902 return self._ferr
902 return self._ferr
903
903
904 @ferr.setter
904 @ferr.setter
905 def ferr(self, f):
905 def ferr(self, f):
906 self._ferr = f
906 self._ferr = f
907 self._fmsgout, self._fmsgerr = _selectmsgdests(self)
907 self._fmsgout, self._fmsgerr = _selectmsgdests(self)
908
908
909 @property
909 @property
910 def fin(self):
910 def fin(self):
911 return self._fin
911 return self._fin
912
912
913 @fin.setter
913 @fin.setter
914 def fin(self, f):
914 def fin(self, f):
915 self._fin = f
915 self._fin = f
916
916
917 @property
917 @property
918 def fmsg(self):
918 def fmsg(self):
919 """Stream dedicated for status/error messages; may be None if
919 """Stream dedicated for status/error messages; may be None if
920 fout/ferr are used"""
920 fout/ferr are used"""
921 return self._fmsg
921 return self._fmsg
922
922
923 @fmsg.setter
923 @fmsg.setter
924 def fmsg(self, f):
924 def fmsg(self, f):
925 self._fmsg = f
925 self._fmsg = f
926 self._fmsgout, self._fmsgerr = _selectmsgdests(self)
926 self._fmsgout, self._fmsgerr = _selectmsgdests(self)
927
927
928 def pushbuffer(self, error=False, subproc=False, labeled=False):
928 def pushbuffer(self, error=False, subproc=False, labeled=False):
929 """install a buffer to capture standard output of the ui object
929 """install a buffer to capture standard output of the ui object
930
930
931 If error is True, the error output will be captured too.
931 If error is True, the error output will be captured too.
932
932
933 If subproc is True, output from subprocesses (typically hooks) will be
933 If subproc is True, output from subprocesses (typically hooks) will be
934 captured too.
934 captured too.
935
935
936 If labeled is True, any labels associated with buffered
936 If labeled is True, any labels associated with buffered
937 output will be handled. By default, this has no effect
937 output will be handled. By default, this has no effect
938 on the output returned, but extensions and GUI tools may
938 on the output returned, but extensions and GUI tools may
939 handle this argument and returned styled output. If output
939 handle this argument and returned styled output. If output
940 is being buffered so it can be captured and parsed or
940 is being buffered so it can be captured and parsed or
941 processed, labeled should not be set to True.
941 processed, labeled should not be set to True.
942 """
942 """
943 self._buffers.append([])
943 self._buffers.append([])
944 self._bufferstates.append((error, subproc, labeled))
944 self._bufferstates.append((error, subproc, labeled))
945 self._bufferapplylabels = labeled
945 self._bufferapplylabels = labeled
946
946
947 def popbuffer(self):
947 def popbuffer(self):
948 '''pop the last buffer and return the buffered output'''
948 '''pop the last buffer and return the buffered output'''
949 self._bufferstates.pop()
949 self._bufferstates.pop()
950 if self._bufferstates:
950 if self._bufferstates:
951 self._bufferapplylabels = self._bufferstates[-1][2]
951 self._bufferapplylabels = self._bufferstates[-1][2]
952 else:
952 else:
953 self._bufferapplylabels = None
953 self._bufferapplylabels = None
954
954
955 return "".join(self._buffers.pop())
955 return "".join(self._buffers.pop())
956
956
957 def _isbuffered(self, dest):
957 def _isbuffered(self, dest):
958 if dest is self._fout:
958 if dest is self._fout:
959 return bool(self._buffers)
959 return bool(self._buffers)
960 if dest is self._ferr:
960 if dest is self._ferr:
961 return bool(self._bufferstates and self._bufferstates[-1][0])
961 return bool(self._bufferstates and self._bufferstates[-1][0])
962 return False
962 return False
963
963
964 def canwritewithoutlabels(self):
964 def canwritewithoutlabels(self):
965 '''check if write skips the label'''
965 '''check if write skips the label'''
966 if self._buffers and not self._bufferapplylabels:
966 if self._buffers and not self._bufferapplylabels:
967 return True
967 return True
968 return self._colormode is None
968 return self._colormode is None
969
969
970 def canbatchlabeledwrites(self):
970 def canbatchlabeledwrites(self):
971 '''check if write calls with labels are batchable'''
971 '''check if write calls with labels are batchable'''
972 # Windows color printing is special, see ``write``.
972 # Windows color printing is special, see ``write``.
973 return self._colormode != 'win32'
973 return self._colormode != 'win32'
974
974
975 def write(self, *args, **opts):
975 def write(self, *args, **opts):
976 '''write args to output
976 '''write args to output
977
977
978 By default, this method simply writes to the buffer or stdout.
978 By default, this method simply writes to the buffer or stdout.
979 Color mode can be set on the UI class to have the output decorated
979 Color mode can be set on the UI class to have the output decorated
980 with color modifier before being written to stdout.
980 with color modifier before being written to stdout.
981
981
982 The color used is controlled by an optional keyword argument, "label".
982 The color used is controlled by an optional keyword argument, "label".
983 This should be a string containing label names separated by space.
983 This should be a string containing label names separated by space.
984 Label names take the form of "topic.type". For example, ui.debug()
984 Label names take the form of "topic.type". For example, ui.debug()
985 issues a label of "ui.debug".
985 issues a label of "ui.debug".
986
986
987 When labeling output for a specific command, a label of
987 When labeling output for a specific command, a label of
988 "cmdname.type" is recommended. For example, status issues
988 "cmdname.type" is recommended. For example, status issues
989 a label of "status.modified" for modified files.
989 a label of "status.modified" for modified files.
990 '''
990 '''
991 self._write(self._fout, *args, **opts)
991 self._write(self._fout, *args, **opts)
992
992
993 def write_err(self, *args, **opts):
993 def write_err(self, *args, **opts):
994 self._write(self._ferr, *args, **opts)
994 self._write(self._ferr, *args, **opts)
995
995
996 def _write(self, dest, *args, **opts):
996 def _write(self, dest, *args, **opts):
997 if self._isbuffered(dest):
997 if self._isbuffered(dest):
998 if self._bufferapplylabels:
998 if self._bufferapplylabels:
999 label = opts.get(r'label', '')
999 label = opts.get(r'label', '')
1000 self._buffers[-1].extend(self.label(a, label) for a in args)
1000 self._buffers[-1].extend(self.label(a, label) for a in args)
1001 else:
1001 else:
1002 self._buffers[-1].extend(args)
1002 self._buffers[-1].extend(args)
1003 else:
1003 else:
1004 self._writenobuf(dest, *args, **opts)
1004 self._writenobuf(dest, *args, **opts)
1005
1005
1006 def _writenobuf(self, dest, *args, **opts):
1006 def _writenobuf(self, dest, *args, **opts):
1007 self._progclear()
1007 self._progclear()
1008 msg = b''.join(args)
1008 msg = b''.join(args)
1009
1009
1010 # opencode timeblockedsection because this is a critical path
1010 # opencode timeblockedsection because this is a critical path
1011 starttime = util.timer()
1011 starttime = util.timer()
1012 try:
1012 try:
1013 if dest is self._ferr and not getattr(self._fout, 'closed', False):
1013 if dest is self._ferr and not getattr(self._fout, 'closed', False):
1014 self._fout.flush()
1014 self._fout.flush()
1015 if getattr(dest, 'structured', False):
1015 if getattr(dest, 'structured', False):
1016 # channel for machine-readable output with metadata, where
1016 # channel for machine-readable output with metadata, where
1017 # no extra colorization is necessary.
1017 # no extra colorization is necessary.
1018 dest.write(msg, **opts)
1018 dest.write(msg, **opts)
1019 elif self._colormode == 'win32':
1019 elif self._colormode == 'win32':
1020 # windows color printing is its own can of crab, defer to
1020 # windows color printing is its own can of crab, defer to
1021 # the color module and that is it.
1021 # the color module and that is it.
1022 color.win32print(self, dest.write, msg, **opts)
1022 color.win32print(self, dest.write, msg, **opts)
1023 else:
1023 else:
1024 if self._colormode is not None:
1024 if self._colormode is not None:
1025 label = opts.get(r'label', '')
1025 label = opts.get(r'label', '')
1026 msg = self.label(msg, label)
1026 msg = self.label(msg, label)
1027 dest.write(msg)
1027 dest.write(msg)
1028 # stderr may be buffered under win32 when redirected to files,
1028 # stderr may be buffered under win32 when redirected to files,
1029 # including stdout.
1029 # including stdout.
1030 if dest is self._ferr and not getattr(self._ferr, 'closed', False):
1030 if dest is self._ferr and not getattr(self._ferr, 'closed', False):
1031 dest.flush()
1031 dest.flush()
1032 except IOError as err:
1032 except IOError as err:
1033 if (dest is self._ferr
1033 if (dest is self._ferr
1034 and err.errno in (errno.EPIPE, errno.EIO, errno.EBADF)):
1034 and err.errno in (errno.EPIPE, errno.EIO, errno.EBADF)):
1035 # no way to report the error, so ignore it
1035 # no way to report the error, so ignore it
1036 return
1036 return
1037 raise error.StdioError(err)
1037 raise error.StdioError(err)
1038 finally:
1038 finally:
1039 self._blockedtimes['stdio_blocked'] += \
1039 self._blockedtimes['stdio_blocked'] += \
1040 (util.timer() - starttime) * 1000
1040 (util.timer() - starttime) * 1000
1041
1041
1042 def _writemsg(self, dest, *args, **opts):
1042 def _writemsg(self, dest, *args, **opts):
1043 _writemsgwith(self._write, dest, *args, **opts)
1043 _writemsgwith(self._write, dest, *args, **opts)
1044
1044
1045 def _writemsgnobuf(self, dest, *args, **opts):
1045 def _writemsgnobuf(self, dest, *args, **opts):
1046 _writemsgwith(self._writenobuf, dest, *args, **opts)
1046 _writemsgwith(self._writenobuf, dest, *args, **opts)
1047
1047
1048 def flush(self):
1048 def flush(self):
1049 # opencode timeblockedsection because this is a critical path
1049 # opencode timeblockedsection because this is a critical path
1050 starttime = util.timer()
1050 starttime = util.timer()
1051 try:
1051 try:
1052 try:
1052 try:
1053 self._fout.flush()
1053 self._fout.flush()
1054 except IOError as err:
1054 except IOError as err:
1055 if err.errno not in (errno.EPIPE, errno.EIO, errno.EBADF):
1055 if err.errno not in (errno.EPIPE, errno.EIO, errno.EBADF):
1056 raise error.StdioError(err)
1056 raise error.StdioError(err)
1057 finally:
1057 finally:
1058 try:
1058 try:
1059 self._ferr.flush()
1059 self._ferr.flush()
1060 except IOError as err:
1060 except IOError as err:
1061 if err.errno not in (errno.EPIPE, errno.EIO, errno.EBADF):
1061 if err.errno not in (errno.EPIPE, errno.EIO, errno.EBADF):
1062 raise error.StdioError(err)
1062 raise error.StdioError(err)
1063 finally:
1063 finally:
1064 self._blockedtimes['stdio_blocked'] += \
1064 self._blockedtimes['stdio_blocked'] += \
1065 (util.timer() - starttime) * 1000
1065 (util.timer() - starttime) * 1000
1066
1066
1067 def _isatty(self, fh):
1067 def _isatty(self, fh):
1068 if self.configbool('ui', 'nontty'):
1068 if self.configbool('ui', 'nontty'):
1069 return False
1069 return False
1070 return procutil.isatty(fh)
1070 return procutil.isatty(fh)
1071
1071
1072 def disablepager(self):
1072 def disablepager(self):
1073 self._disablepager = True
1073 self._disablepager = True
1074
1074
1075 def pager(self, command):
1075 def pager(self, command):
1076 """Start a pager for subsequent command output.
1076 """Start a pager for subsequent command output.
1077
1077
1078 Commands which produce a long stream of output should call
1078 Commands which produce a long stream of output should call
1079 this function to activate the user's preferred pagination
1079 this function to activate the user's preferred pagination
1080 mechanism (which may be no pager). Calling this function
1080 mechanism (which may be no pager). Calling this function
1081 precludes any future use of interactive functionality, such as
1081 precludes any future use of interactive functionality, such as
1082 prompting the user or activating curses.
1082 prompting the user or activating curses.
1083
1083
1084 Args:
1084 Args:
1085 command: The full, non-aliased name of the command. That is, "log"
1085 command: The full, non-aliased name of the command. That is, "log"
1086 not "history, "summary" not "summ", etc.
1086 not "history, "summary" not "summ", etc.
1087 """
1087 """
1088 if (self._disablepager
1088 if (self._disablepager
1089 or self.pageractive):
1089 or self.pageractive):
1090 # how pager should do is already determined
1090 # how pager should do is already determined
1091 return
1091 return
1092
1092
1093 if not command.startswith('internal-always-') and (
1093 if not command.startswith('internal-always-') and (
1094 # explicit --pager=on (= 'internal-always-' prefix) should
1094 # explicit --pager=on (= 'internal-always-' prefix) should
1095 # take precedence over disabling factors below
1095 # take precedence over disabling factors below
1096 command in self.configlist('pager', 'ignore')
1096 command in self.configlist('pager', 'ignore')
1097 or not self.configbool('ui', 'paginate')
1097 or not self.configbool('ui', 'paginate')
1098 or not self.configbool('pager', 'attend-' + command, True)
1098 or not self.configbool('pager', 'attend-' + command, True)
1099 or encoding.environ.get('TERM') == 'dumb'
1099 or encoding.environ.get('TERM') == 'dumb'
1100 # TODO: if we want to allow HGPLAINEXCEPT=pager,
1100 # TODO: if we want to allow HGPLAINEXCEPT=pager,
1101 # formatted() will need some adjustment.
1101 # formatted() will need some adjustment.
1102 or not self.formatted()
1102 or not self.formatted()
1103 or self.plain()
1103 or self.plain()
1104 or self._buffers
1104 or self._buffers
1105 # TODO: expose debugger-enabled on the UI object
1105 # TODO: expose debugger-enabled on the UI object
1106 or '--debugger' in pycompat.sysargv):
1106 or '--debugger' in pycompat.sysargv):
1107 # We only want to paginate if the ui appears to be
1107 # We only want to paginate if the ui appears to be
1108 # interactive, the user didn't say HGPLAIN or
1108 # interactive, the user didn't say HGPLAIN or
1109 # HGPLAINEXCEPT=pager, and the user didn't specify --debug.
1109 # HGPLAINEXCEPT=pager, and the user didn't specify --debug.
1110 return
1110 return
1111
1111
1112 pagercmd = self.config('pager', 'pager', rcutil.fallbackpager)
1112 pagercmd = self.config('pager', 'pager', rcutil.fallbackpager)
1113 if not pagercmd:
1113 if not pagercmd:
1114 return
1114 return
1115
1115
1116 pagerenv = {}
1116 pagerenv = {}
1117 for name, value in rcutil.defaultpagerenv().items():
1117 for name, value in rcutil.defaultpagerenv().items():
1118 if name not in encoding.environ:
1118 if name not in encoding.environ:
1119 pagerenv[name] = value
1119 pagerenv[name] = value
1120
1120
1121 self.debug('starting pager for command %s\n' %
1121 self.debug('starting pager for command %s\n' %
1122 stringutil.pprint(command))
1122 stringutil.pprint(command))
1123 self.flush()
1123 self.flush()
1124
1124
1125 wasformatted = self.formatted()
1125 wasformatted = self.formatted()
1126 if util.safehasattr(signal, "SIGPIPE"):
1126 if util.safehasattr(signal, "SIGPIPE"):
1127 signal.signal(signal.SIGPIPE, _catchterm)
1127 signal.signal(signal.SIGPIPE, _catchterm)
1128 if self._runpager(pagercmd, pagerenv):
1128 if self._runpager(pagercmd, pagerenv):
1129 self.pageractive = True
1129 self.pageractive = True
1130 # Preserve the formatted-ness of the UI. This is important
1130 # Preserve the formatted-ness of the UI. This is important
1131 # because we mess with stdout, which might confuse
1131 # because we mess with stdout, which might confuse
1132 # auto-detection of things being formatted.
1132 # auto-detection of things being formatted.
1133 self.setconfig('ui', 'formatted', wasformatted, 'pager')
1133 self.setconfig('ui', 'formatted', wasformatted, 'pager')
1134 self.setconfig('ui', 'interactive', False, 'pager')
1134 self.setconfig('ui', 'interactive', False, 'pager')
1135
1135
1136 # If pagermode differs from color.mode, reconfigure color now that
1136 # If pagermode differs from color.mode, reconfigure color now that
1137 # pageractive is set.
1137 # pageractive is set.
1138 cm = self._colormode
1138 cm = self._colormode
1139 if cm != self.config('color', 'pagermode', cm):
1139 if cm != self.config('color', 'pagermode', cm):
1140 color.setup(self)
1140 color.setup(self)
1141 else:
1141 else:
1142 # If the pager can't be spawned in dispatch when --pager=on is
1142 # If the pager can't be spawned in dispatch when --pager=on is
1143 # given, don't try again when the command runs, to avoid a duplicate
1143 # given, don't try again when the command runs, to avoid a duplicate
1144 # warning about a missing pager command.
1144 # warning about a missing pager command.
1145 self.disablepager()
1145 self.disablepager()
1146
1146
1147 def _runpager(self, command, env=None):
1147 def _runpager(self, command, env=None):
1148 """Actually start the pager and set up file descriptors.
1148 """Actually start the pager and set up file descriptors.
1149
1149
1150 This is separate in part so that extensions (like chg) can
1150 This is separate in part so that extensions (like chg) can
1151 override how a pager is invoked.
1151 override how a pager is invoked.
1152 """
1152 """
1153 if command == 'cat':
1153 if command == 'cat':
1154 # Save ourselves some work.
1154 # Save ourselves some work.
1155 return False
1155 return False
1156 # If the command doesn't contain any of these characters, we
1156 # If the command doesn't contain any of these characters, we
1157 # assume it's a binary and exec it directly. This means for
1157 # assume it's a binary and exec it directly. This means for
1158 # simple pager command configurations, we can degrade
1158 # simple pager command configurations, we can degrade
1159 # gracefully and tell the user about their broken pager.
1159 # gracefully and tell the user about their broken pager.
1160 shell = any(c in command for c in "|&;<>()$`\\\"' \t\n*?[#~=%")
1160 shell = any(c in command for c in "|&;<>()$`\\\"' \t\n*?[#~=%")
1161
1161
1162 if pycompat.iswindows and not shell:
1162 if pycompat.iswindows and not shell:
1163 # Window's built-in `more` cannot be invoked with shell=False, but
1163 # Window's built-in `more` cannot be invoked with shell=False, but
1164 # its `more.com` can. Hide this implementation detail from the
1164 # its `more.com` can. Hide this implementation detail from the
1165 # user so we can also get sane bad PAGER behavior. MSYS has
1165 # user so we can also get sane bad PAGER behavior. MSYS has
1166 # `more.exe`, so do a cmd.exe style resolution of the executable to
1166 # `more.exe`, so do a cmd.exe style resolution of the executable to
1167 # determine which one to use.
1167 # determine which one to use.
1168 fullcmd = procutil.findexe(command)
1168 fullcmd = procutil.findexe(command)
1169 if not fullcmd:
1169 if not fullcmd:
1170 self.warn(_("missing pager command '%s', skipping pager\n")
1170 self.warn(_("missing pager command '%s', skipping pager\n")
1171 % command)
1171 % command)
1172 return False
1172 return False
1173
1173
1174 command = fullcmd
1174 command = fullcmd
1175
1175
1176 try:
1176 try:
1177 pager = subprocess.Popen(
1177 pager = subprocess.Popen(
1178 procutil.tonativestr(command), shell=shell, bufsize=-1,
1178 procutil.tonativestr(command), shell=shell, bufsize=-1,
1179 close_fds=procutil.closefds, stdin=subprocess.PIPE,
1179 close_fds=procutil.closefds, stdin=subprocess.PIPE,
1180 stdout=procutil.stdout, stderr=procutil.stderr,
1180 stdout=procutil.stdout, stderr=procutil.stderr,
1181 env=procutil.tonativeenv(procutil.shellenviron(env)))
1181 env=procutil.tonativeenv(procutil.shellenviron(env)))
1182 except OSError as e:
1182 except OSError as e:
1183 if e.errno == errno.ENOENT and not shell:
1183 if e.errno == errno.ENOENT and not shell:
1184 self.warn(_("missing pager command '%s', skipping pager\n")
1184 self.warn(_("missing pager command '%s', skipping pager\n")
1185 % command)
1185 % command)
1186 return False
1186 return False
1187 raise
1187 raise
1188
1188
1189 # back up original file descriptors
1189 # back up original file descriptors
1190 stdoutfd = os.dup(procutil.stdout.fileno())
1190 stdoutfd = os.dup(procutil.stdout.fileno())
1191 stderrfd = os.dup(procutil.stderr.fileno())
1191 stderrfd = os.dup(procutil.stderr.fileno())
1192
1192
1193 os.dup2(pager.stdin.fileno(), procutil.stdout.fileno())
1193 os.dup2(pager.stdin.fileno(), procutil.stdout.fileno())
1194 if self._isatty(procutil.stderr):
1194 if self._isatty(procutil.stderr):
1195 os.dup2(pager.stdin.fileno(), procutil.stderr.fileno())
1195 os.dup2(pager.stdin.fileno(), procutil.stderr.fileno())
1196
1196
1197 @self.atexit
1197 @self.atexit
1198 def killpager():
1198 def killpager():
1199 if util.safehasattr(signal, "SIGINT"):
1199 if util.safehasattr(signal, "SIGINT"):
1200 signal.signal(signal.SIGINT, signal.SIG_IGN)
1200 signal.signal(signal.SIGINT, signal.SIG_IGN)
1201 # restore original fds, closing pager.stdin copies in the process
1201 # restore original fds, closing pager.stdin copies in the process
1202 os.dup2(stdoutfd, procutil.stdout.fileno())
1202 os.dup2(stdoutfd, procutil.stdout.fileno())
1203 os.dup2(stderrfd, procutil.stderr.fileno())
1203 os.dup2(stderrfd, procutil.stderr.fileno())
1204 pager.stdin.close()
1204 pager.stdin.close()
1205 pager.wait()
1205 pager.wait()
1206
1206
1207 return True
1207 return True
1208
1208
1209 @property
1209 @property
1210 def _exithandlers(self):
1210 def _exithandlers(self):
1211 return _reqexithandlers
1211 return _reqexithandlers
1212
1212
1213 def atexit(self, func, *args, **kwargs):
1213 def atexit(self, func, *args, **kwargs):
1214 '''register a function to run after dispatching a request
1214 '''register a function to run after dispatching a request
1215
1215
1216 Handlers do not stay registered across request boundaries.'''
1216 Handlers do not stay registered across request boundaries.'''
1217 self._exithandlers.append((func, args, kwargs))
1217 self._exithandlers.append((func, args, kwargs))
1218 return func
1218 return func
1219
1219
1220 def interface(self, feature):
1220 def interface(self, feature):
1221 """what interface to use for interactive console features?
1221 """what interface to use for interactive console features?
1222
1222
1223 The interface is controlled by the value of `ui.interface` but also by
1223 The interface is controlled by the value of `ui.interface` but also by
1224 the value of feature-specific configuration. For example:
1224 the value of feature-specific configuration. For example:
1225
1225
1226 ui.interface.histedit = text
1226 ui.interface.histedit = text
1227 ui.interface.chunkselector = curses
1227 ui.interface.chunkselector = curses
1228
1228
1229 Here the features are "histedit" and "chunkselector".
1229 Here the features are "histedit" and "chunkselector".
1230
1230
1231 The configuration above means that the default interfaces for commands
1231 The configuration above means that the default interfaces for commands
1232 is curses, the interface for histedit is text and the interface for
1232 is curses, the interface for histedit is text and the interface for
1233 selecting chunk is crecord (the best curses interface available).
1233 selecting chunk is crecord (the best curses interface available).
1234
1234
1235 Consider the following example:
1235 Consider the following example:
1236 ui.interface = curses
1236 ui.interface = curses
1237 ui.interface.histedit = text
1237 ui.interface.histedit = text
1238
1238
1239 Then histedit will use the text interface and chunkselector will use
1239 Then histedit will use the text interface and chunkselector will use
1240 the default curses interface (crecord at the moment).
1240 the default curses interface (crecord at the moment).
1241 """
1241 """
1242 alldefaults = frozenset(["text", "curses"])
1242 alldefaults = frozenset(["text", "curses"])
1243
1243
1244 featureinterfaces = {
1244 featureinterfaces = {
1245 "chunkselector": [
1245 "chunkselector": [
1246 "text",
1246 "text",
1247 "curses",
1247 "curses",
1248 ]
1248 ],
1249 "histedit": [
1250 "text",
1251 "curses",
1252 ],
1249 }
1253 }
1250
1254
1251 # Feature-specific interface
1255 # Feature-specific interface
1252 if feature not in featureinterfaces.keys():
1256 if feature not in featureinterfaces.keys():
1253 # Programming error, not user error
1257 # Programming error, not user error
1254 raise ValueError("Unknown feature requested %s" % feature)
1258 raise ValueError("Unknown feature requested %s" % feature)
1255
1259
1256 availableinterfaces = frozenset(featureinterfaces[feature])
1260 availableinterfaces = frozenset(featureinterfaces[feature])
1257 if alldefaults > availableinterfaces:
1261 if alldefaults > availableinterfaces:
1258 # Programming error, not user error. We need a use case to
1262 # Programming error, not user error. We need a use case to
1259 # define the right thing to do here.
1263 # define the right thing to do here.
1260 raise ValueError(
1264 raise ValueError(
1261 "Feature %s does not handle all default interfaces" %
1265 "Feature %s does not handle all default interfaces" %
1262 feature)
1266 feature)
1263
1267
1264 if self.plain() or encoding.environ.get('TERM') == 'dumb':
1268 if self.plain() or encoding.environ.get('TERM') == 'dumb':
1265 return "text"
1269 return "text"
1266
1270
1267 # Default interface for all the features
1271 # Default interface for all the features
1268 defaultinterface = "text"
1272 defaultinterface = "text"
1269 i = self.config("ui", "interface")
1273 i = self.config("ui", "interface")
1270 if i in alldefaults:
1274 if i in alldefaults:
1271 defaultinterface = i
1275 defaultinterface = i
1272
1276
1273 choseninterface = defaultinterface
1277 choseninterface = defaultinterface
1274 f = self.config("ui", "interface.%s" % feature)
1278 f = self.config("ui", "interface.%s" % feature)
1275 if f in availableinterfaces:
1279 if f in availableinterfaces:
1276 choseninterface = f
1280 choseninterface = f
1277
1281
1278 if i is not None and defaultinterface != i:
1282 if i is not None and defaultinterface != i:
1279 if f is not None:
1283 if f is not None:
1280 self.warn(_("invalid value for ui.interface: %s\n") %
1284 self.warn(_("invalid value for ui.interface: %s\n") %
1281 (i,))
1285 (i,))
1282 else:
1286 else:
1283 self.warn(_("invalid value for ui.interface: %s (using %s)\n") %
1287 self.warn(_("invalid value for ui.interface: %s (using %s)\n") %
1284 (i, choseninterface))
1288 (i, choseninterface))
1285 if f is not None and choseninterface != f:
1289 if f is not None and choseninterface != f:
1286 self.warn(_("invalid value for ui.interface.%s: %s (using %s)\n") %
1290 self.warn(_("invalid value for ui.interface.%s: %s (using %s)\n") %
1287 (feature, f, choseninterface))
1291 (feature, f, choseninterface))
1288
1292
1289 return choseninterface
1293 return choseninterface
1290
1294
1291 def interactive(self):
1295 def interactive(self):
1292 '''is interactive input allowed?
1296 '''is interactive input allowed?
1293
1297
1294 An interactive session is a session where input can be reasonably read
1298 An interactive session is a session where input can be reasonably read
1295 from `sys.stdin'. If this function returns false, any attempt to read
1299 from `sys.stdin'. If this function returns false, any attempt to read
1296 from stdin should fail with an error, unless a sensible default has been
1300 from stdin should fail with an error, unless a sensible default has been
1297 specified.
1301 specified.
1298
1302
1299 Interactiveness is triggered by the value of the `ui.interactive'
1303 Interactiveness is triggered by the value of the `ui.interactive'
1300 configuration variable or - if it is unset - when `sys.stdin' points
1304 configuration variable or - if it is unset - when `sys.stdin' points
1301 to a terminal device.
1305 to a terminal device.
1302
1306
1303 This function refers to input only; for output, see `ui.formatted()'.
1307 This function refers to input only; for output, see `ui.formatted()'.
1304 '''
1308 '''
1305 i = self.configbool("ui", "interactive")
1309 i = self.configbool("ui", "interactive")
1306 if i is None:
1310 if i is None:
1307 # some environments replace stdin without implementing isatty
1311 # some environments replace stdin without implementing isatty
1308 # usually those are non-interactive
1312 # usually those are non-interactive
1309 return self._isatty(self._fin)
1313 return self._isatty(self._fin)
1310
1314
1311 return i
1315 return i
1312
1316
1313 def termwidth(self):
1317 def termwidth(self):
1314 '''how wide is the terminal in columns?
1318 '''how wide is the terminal in columns?
1315 '''
1319 '''
1316 if 'COLUMNS' in encoding.environ:
1320 if 'COLUMNS' in encoding.environ:
1317 try:
1321 try:
1318 return int(encoding.environ['COLUMNS'])
1322 return int(encoding.environ['COLUMNS'])
1319 except ValueError:
1323 except ValueError:
1320 pass
1324 pass
1321 return scmutil.termsize(self)[0]
1325 return scmutil.termsize(self)[0]
1322
1326
1323 def formatted(self):
1327 def formatted(self):
1324 '''should formatted output be used?
1328 '''should formatted output be used?
1325
1329
1326 It is often desirable to format the output to suite the output medium.
1330 It is often desirable to format the output to suite the output medium.
1327 Examples of this are truncating long lines or colorizing messages.
1331 Examples of this are truncating long lines or colorizing messages.
1328 However, this is not often not desirable when piping output into other
1332 However, this is not often not desirable when piping output into other
1329 utilities, e.g. `grep'.
1333 utilities, e.g. `grep'.
1330
1334
1331 Formatted output is triggered by the value of the `ui.formatted'
1335 Formatted output is triggered by the value of the `ui.formatted'
1332 configuration variable or - if it is unset - when `sys.stdout' points
1336 configuration variable or - if it is unset - when `sys.stdout' points
1333 to a terminal device. Please note that `ui.formatted' should be
1337 to a terminal device. Please note that `ui.formatted' should be
1334 considered an implementation detail; it is not intended for use outside
1338 considered an implementation detail; it is not intended for use outside
1335 Mercurial or its extensions.
1339 Mercurial or its extensions.
1336
1340
1337 This function refers to output only; for input, see `ui.interactive()'.
1341 This function refers to output only; for input, see `ui.interactive()'.
1338 This function always returns false when in plain mode, see `ui.plain()'.
1342 This function always returns false when in plain mode, see `ui.plain()'.
1339 '''
1343 '''
1340 if self.plain():
1344 if self.plain():
1341 return False
1345 return False
1342
1346
1343 i = self.configbool("ui", "formatted")
1347 i = self.configbool("ui", "formatted")
1344 if i is None:
1348 if i is None:
1345 # some environments replace stdout without implementing isatty
1349 # some environments replace stdout without implementing isatty
1346 # usually those are non-interactive
1350 # usually those are non-interactive
1347 return self._isatty(self._fout)
1351 return self._isatty(self._fout)
1348
1352
1349 return i
1353 return i
1350
1354
1351 def _readline(self):
1355 def _readline(self):
1352 # Replacing stdin/stdout temporarily is a hard problem on Python 3
1356 # Replacing stdin/stdout temporarily is a hard problem on Python 3
1353 # because they have to be text streams with *no buffering*. Instead,
1357 # because they have to be text streams with *no buffering*. Instead,
1354 # we use rawinput() only if call_readline() will be invoked by
1358 # we use rawinput() only if call_readline() will be invoked by
1355 # PyOS_Readline(), so no I/O will be made at Python layer.
1359 # PyOS_Readline(), so no I/O will be made at Python layer.
1356 usereadline = (self._isatty(self._fin) and self._isatty(self._fout)
1360 usereadline = (self._isatty(self._fin) and self._isatty(self._fout)
1357 and procutil.isstdin(self._fin)
1361 and procutil.isstdin(self._fin)
1358 and procutil.isstdout(self._fout))
1362 and procutil.isstdout(self._fout))
1359 if usereadline:
1363 if usereadline:
1360 try:
1364 try:
1361 # magically add command line editing support, where
1365 # magically add command line editing support, where
1362 # available
1366 # available
1363 import readline
1367 import readline
1364 # force demandimport to really load the module
1368 # force demandimport to really load the module
1365 readline.read_history_file
1369 readline.read_history_file
1366 # windows sometimes raises something other than ImportError
1370 # windows sometimes raises something other than ImportError
1367 except Exception:
1371 except Exception:
1368 usereadline = False
1372 usereadline = False
1369
1373
1370 # prompt ' ' must exist; otherwise readline may delete entire line
1374 # prompt ' ' must exist; otherwise readline may delete entire line
1371 # - http://bugs.python.org/issue12833
1375 # - http://bugs.python.org/issue12833
1372 with self.timeblockedsection('stdio'):
1376 with self.timeblockedsection('stdio'):
1373 if usereadline:
1377 if usereadline:
1374 line = encoding.strtolocal(pycompat.rawinput(r' '))
1378 line = encoding.strtolocal(pycompat.rawinput(r' '))
1375 # When stdin is in binary mode on Windows, it can cause
1379 # When stdin is in binary mode on Windows, it can cause
1376 # raw_input() to emit an extra trailing carriage return
1380 # raw_input() to emit an extra trailing carriage return
1377 if pycompat.oslinesep == b'\r\n' and line.endswith(b'\r'):
1381 if pycompat.oslinesep == b'\r\n' and line.endswith(b'\r'):
1378 line = line[:-1]
1382 line = line[:-1]
1379 else:
1383 else:
1380 self._fout.write(b' ')
1384 self._fout.write(b' ')
1381 self._fout.flush()
1385 self._fout.flush()
1382 line = self._fin.readline()
1386 line = self._fin.readline()
1383 if not line:
1387 if not line:
1384 raise EOFError
1388 raise EOFError
1385 line = line.rstrip(pycompat.oslinesep)
1389 line = line.rstrip(pycompat.oslinesep)
1386
1390
1387 return line
1391 return line
1388
1392
1389 def prompt(self, msg, default="y"):
1393 def prompt(self, msg, default="y"):
1390 """Prompt user with msg, read response.
1394 """Prompt user with msg, read response.
1391 If ui is not interactive, the default is returned.
1395 If ui is not interactive, the default is returned.
1392 """
1396 """
1393 return self._prompt(msg, default=default)
1397 return self._prompt(msg, default=default)
1394
1398
1395 def _prompt(self, msg, **opts):
1399 def _prompt(self, msg, **opts):
1396 default = opts[r'default']
1400 default = opts[r'default']
1397 if not self.interactive():
1401 if not self.interactive():
1398 self._writemsg(self._fmsgout, msg, ' ', type='prompt', **opts)
1402 self._writemsg(self._fmsgout, msg, ' ', type='prompt', **opts)
1399 self._writemsg(self._fmsgout, default or '', "\n",
1403 self._writemsg(self._fmsgout, default or '', "\n",
1400 type='promptecho')
1404 type='promptecho')
1401 return default
1405 return default
1402 self._writemsgnobuf(self._fmsgout, msg, type='prompt', **opts)
1406 self._writemsgnobuf(self._fmsgout, msg, type='prompt', **opts)
1403 self.flush()
1407 self.flush()
1404 try:
1408 try:
1405 r = self._readline()
1409 r = self._readline()
1406 if not r:
1410 if not r:
1407 r = default
1411 r = default
1408 if self.configbool('ui', 'promptecho'):
1412 if self.configbool('ui', 'promptecho'):
1409 self._writemsg(self._fmsgout, r, "\n", type='promptecho')
1413 self._writemsg(self._fmsgout, r, "\n", type='promptecho')
1410 return r
1414 return r
1411 except EOFError:
1415 except EOFError:
1412 raise error.ResponseExpected()
1416 raise error.ResponseExpected()
1413
1417
1414 @staticmethod
1418 @staticmethod
1415 def extractchoices(prompt):
1419 def extractchoices(prompt):
1416 """Extract prompt message and list of choices from specified prompt.
1420 """Extract prompt message and list of choices from specified prompt.
1417
1421
1418 This returns tuple "(message, choices)", and "choices" is the
1422 This returns tuple "(message, choices)", and "choices" is the
1419 list of tuple "(response character, text without &)".
1423 list of tuple "(response character, text without &)".
1420
1424
1421 >>> ui.extractchoices(b"awake? $$ &Yes $$ &No")
1425 >>> ui.extractchoices(b"awake? $$ &Yes $$ &No")
1422 ('awake? ', [('y', 'Yes'), ('n', 'No')])
1426 ('awake? ', [('y', 'Yes'), ('n', 'No')])
1423 >>> ui.extractchoices(b"line\\nbreak? $$ &Yes $$ &No")
1427 >>> ui.extractchoices(b"line\\nbreak? $$ &Yes $$ &No")
1424 ('line\\nbreak? ', [('y', 'Yes'), ('n', 'No')])
1428 ('line\\nbreak? ', [('y', 'Yes'), ('n', 'No')])
1425 >>> ui.extractchoices(b"want lots of $$money$$?$$Ye&s$$N&o")
1429 >>> ui.extractchoices(b"want lots of $$money$$?$$Ye&s$$N&o")
1426 ('want lots of $$money$$?', [('s', 'Yes'), ('o', 'No')])
1430 ('want lots of $$money$$?', [('s', 'Yes'), ('o', 'No')])
1427 """
1431 """
1428
1432
1429 # Sadly, the prompt string may have been built with a filename
1433 # Sadly, the prompt string may have been built with a filename
1430 # containing "$$" so let's try to find the first valid-looking
1434 # containing "$$" so let's try to find the first valid-looking
1431 # prompt to start parsing. Sadly, we also can't rely on
1435 # prompt to start parsing. Sadly, we also can't rely on
1432 # choices containing spaces, ASCII, or basically anything
1436 # choices containing spaces, ASCII, or basically anything
1433 # except an ampersand followed by a character.
1437 # except an ampersand followed by a character.
1434 m = re.match(br'(?s)(.+?)\$\$([^\$]*&[^ \$].*)', prompt)
1438 m = re.match(br'(?s)(.+?)\$\$([^\$]*&[^ \$].*)', prompt)
1435 msg = m.group(1)
1439 msg = m.group(1)
1436 choices = [p.strip(' ') for p in m.group(2).split('$$')]
1440 choices = [p.strip(' ') for p in m.group(2).split('$$')]
1437 def choicetuple(s):
1441 def choicetuple(s):
1438 ampidx = s.index('&')
1442 ampidx = s.index('&')
1439 return s[ampidx + 1:ampidx + 2].lower(), s.replace('&', '', 1)
1443 return s[ampidx + 1:ampidx + 2].lower(), s.replace('&', '', 1)
1440 return (msg, [choicetuple(s) for s in choices])
1444 return (msg, [choicetuple(s) for s in choices])
1441
1445
1442 def promptchoice(self, prompt, default=0):
1446 def promptchoice(self, prompt, default=0):
1443 """Prompt user with a message, read response, and ensure it matches
1447 """Prompt user with a message, read response, and ensure it matches
1444 one of the provided choices. The prompt is formatted as follows:
1448 one of the provided choices. The prompt is formatted as follows:
1445
1449
1446 "would you like fries with that (Yn)? $$ &Yes $$ &No"
1450 "would you like fries with that (Yn)? $$ &Yes $$ &No"
1447
1451
1448 The index of the choice is returned. Responses are case
1452 The index of the choice is returned. Responses are case
1449 insensitive. If ui is not interactive, the default is
1453 insensitive. If ui is not interactive, the default is
1450 returned.
1454 returned.
1451 """
1455 """
1452
1456
1453 msg, choices = self.extractchoices(prompt)
1457 msg, choices = self.extractchoices(prompt)
1454 resps = [r for r, t in choices]
1458 resps = [r for r, t in choices]
1455 while True:
1459 while True:
1456 r = self._prompt(msg, default=resps[default], choices=choices)
1460 r = self._prompt(msg, default=resps[default], choices=choices)
1457 if r.lower() in resps:
1461 if r.lower() in resps:
1458 return resps.index(r.lower())
1462 return resps.index(r.lower())
1459 # TODO: shouldn't it be a warning?
1463 # TODO: shouldn't it be a warning?
1460 self._writemsg(self._fmsgout, _("unrecognized response\n"))
1464 self._writemsg(self._fmsgout, _("unrecognized response\n"))
1461
1465
1462 def getpass(self, prompt=None, default=None):
1466 def getpass(self, prompt=None, default=None):
1463 if not self.interactive():
1467 if not self.interactive():
1464 return default
1468 return default
1465 try:
1469 try:
1466 self._writemsg(self._fmsgerr, prompt or _('password: '),
1470 self._writemsg(self._fmsgerr, prompt or _('password: '),
1467 type='prompt', password=True)
1471 type='prompt', password=True)
1468 # disable getpass() only if explicitly specified. it's still valid
1472 # disable getpass() only if explicitly specified. it's still valid
1469 # to interact with tty even if fin is not a tty.
1473 # to interact with tty even if fin is not a tty.
1470 with self.timeblockedsection('stdio'):
1474 with self.timeblockedsection('stdio'):
1471 if self.configbool('ui', 'nontty'):
1475 if self.configbool('ui', 'nontty'):
1472 l = self._fin.readline()
1476 l = self._fin.readline()
1473 if not l:
1477 if not l:
1474 raise EOFError
1478 raise EOFError
1475 return l.rstrip('\n')
1479 return l.rstrip('\n')
1476 else:
1480 else:
1477 return getpass.getpass('')
1481 return getpass.getpass('')
1478 except EOFError:
1482 except EOFError:
1479 raise error.ResponseExpected()
1483 raise error.ResponseExpected()
1480
1484
1481 def status(self, *msg, **opts):
1485 def status(self, *msg, **opts):
1482 '''write status message to output (if ui.quiet is False)
1486 '''write status message to output (if ui.quiet is False)
1483
1487
1484 This adds an output label of "ui.status".
1488 This adds an output label of "ui.status".
1485 '''
1489 '''
1486 if not self.quiet:
1490 if not self.quiet:
1487 self._writemsg(self._fmsgout, type='status', *msg, **opts)
1491 self._writemsg(self._fmsgout, type='status', *msg, **opts)
1488
1492
1489 def warn(self, *msg, **opts):
1493 def warn(self, *msg, **opts):
1490 '''write warning message to output (stderr)
1494 '''write warning message to output (stderr)
1491
1495
1492 This adds an output label of "ui.warning".
1496 This adds an output label of "ui.warning".
1493 '''
1497 '''
1494 self._writemsg(self._fmsgerr, type='warning', *msg, **opts)
1498 self._writemsg(self._fmsgerr, type='warning', *msg, **opts)
1495
1499
1496 def error(self, *msg, **opts):
1500 def error(self, *msg, **opts):
1497 '''write error message to output (stderr)
1501 '''write error message to output (stderr)
1498
1502
1499 This adds an output label of "ui.error".
1503 This adds an output label of "ui.error".
1500 '''
1504 '''
1501 self._writemsg(self._fmsgerr, type='error', *msg, **opts)
1505 self._writemsg(self._fmsgerr, type='error', *msg, **opts)
1502
1506
1503 def note(self, *msg, **opts):
1507 def note(self, *msg, **opts):
1504 '''write note to output (if ui.verbose is True)
1508 '''write note to output (if ui.verbose is True)
1505
1509
1506 This adds an output label of "ui.note".
1510 This adds an output label of "ui.note".
1507 '''
1511 '''
1508 if self.verbose:
1512 if self.verbose:
1509 self._writemsg(self._fmsgout, type='note', *msg, **opts)
1513 self._writemsg(self._fmsgout, type='note', *msg, **opts)
1510
1514
1511 def debug(self, *msg, **opts):
1515 def debug(self, *msg, **opts):
1512 '''write debug message to output (if ui.debugflag is True)
1516 '''write debug message to output (if ui.debugflag is True)
1513
1517
1514 This adds an output label of "ui.debug".
1518 This adds an output label of "ui.debug".
1515 '''
1519 '''
1516 if self.debugflag:
1520 if self.debugflag:
1517 self._writemsg(self._fmsgout, type='debug', *msg, **opts)
1521 self._writemsg(self._fmsgout, type='debug', *msg, **opts)
1518
1522
1519 def edit(self, text, user, extra=None, editform=None, pending=None,
1523 def edit(self, text, user, extra=None, editform=None, pending=None,
1520 repopath=None, action=None):
1524 repopath=None, action=None):
1521 if action is None:
1525 if action is None:
1522 self.develwarn('action is None but will soon be a required '
1526 self.develwarn('action is None but will soon be a required '
1523 'parameter to ui.edit()')
1527 'parameter to ui.edit()')
1524 extra_defaults = {
1528 extra_defaults = {
1525 'prefix': 'editor',
1529 'prefix': 'editor',
1526 'suffix': '.txt',
1530 'suffix': '.txt',
1527 }
1531 }
1528 if extra is not None:
1532 if extra is not None:
1529 if extra.get('suffix') is not None:
1533 if extra.get('suffix') is not None:
1530 self.develwarn('extra.suffix is not None but will soon be '
1534 self.develwarn('extra.suffix is not None but will soon be '
1531 'ignored by ui.edit()')
1535 'ignored by ui.edit()')
1532 extra_defaults.update(extra)
1536 extra_defaults.update(extra)
1533 extra = extra_defaults
1537 extra = extra_defaults
1534
1538
1535 if action == 'diff':
1539 if action == 'diff':
1536 suffix = '.diff'
1540 suffix = '.diff'
1537 elif action:
1541 elif action:
1538 suffix = '.%s.hg.txt' % action
1542 suffix = '.%s.hg.txt' % action
1539 else:
1543 else:
1540 suffix = extra['suffix']
1544 suffix = extra['suffix']
1541
1545
1542 rdir = None
1546 rdir = None
1543 if self.configbool('experimental', 'editortmpinhg'):
1547 if self.configbool('experimental', 'editortmpinhg'):
1544 rdir = repopath
1548 rdir = repopath
1545 (fd, name) = pycompat.mkstemp(prefix='hg-' + extra['prefix'] + '-',
1549 (fd, name) = pycompat.mkstemp(prefix='hg-' + extra['prefix'] + '-',
1546 suffix=suffix,
1550 suffix=suffix,
1547 dir=rdir)
1551 dir=rdir)
1548 try:
1552 try:
1549 f = os.fdopen(fd, r'wb')
1553 f = os.fdopen(fd, r'wb')
1550 f.write(util.tonativeeol(text))
1554 f.write(util.tonativeeol(text))
1551 f.close()
1555 f.close()
1552
1556
1553 environ = {'HGUSER': user}
1557 environ = {'HGUSER': user}
1554 if 'transplant_source' in extra:
1558 if 'transplant_source' in extra:
1555 environ.update({'HGREVISION': hex(extra['transplant_source'])})
1559 environ.update({'HGREVISION': hex(extra['transplant_source'])})
1556 for label in ('intermediate-source', 'source', 'rebase_source'):
1560 for label in ('intermediate-source', 'source', 'rebase_source'):
1557 if label in extra:
1561 if label in extra:
1558 environ.update({'HGREVISION': extra[label]})
1562 environ.update({'HGREVISION': extra[label]})
1559 break
1563 break
1560 if editform:
1564 if editform:
1561 environ.update({'HGEDITFORM': editform})
1565 environ.update({'HGEDITFORM': editform})
1562 if pending:
1566 if pending:
1563 environ.update({'HG_PENDING': pending})
1567 environ.update({'HG_PENDING': pending})
1564
1568
1565 editor = self.geteditor()
1569 editor = self.geteditor()
1566
1570
1567 self.system("%s \"%s\"" % (editor, name),
1571 self.system("%s \"%s\"" % (editor, name),
1568 environ=environ,
1572 environ=environ,
1569 onerr=error.Abort, errprefix=_("edit failed"),
1573 onerr=error.Abort, errprefix=_("edit failed"),
1570 blockedtag='editor')
1574 blockedtag='editor')
1571
1575
1572 f = open(name, r'rb')
1576 f = open(name, r'rb')
1573 t = util.fromnativeeol(f.read())
1577 t = util.fromnativeeol(f.read())
1574 f.close()
1578 f.close()
1575 finally:
1579 finally:
1576 os.unlink(name)
1580 os.unlink(name)
1577
1581
1578 return t
1582 return t
1579
1583
1580 def system(self, cmd, environ=None, cwd=None, onerr=None, errprefix=None,
1584 def system(self, cmd, environ=None, cwd=None, onerr=None, errprefix=None,
1581 blockedtag=None):
1585 blockedtag=None):
1582 '''execute shell command with appropriate output stream. command
1586 '''execute shell command with appropriate output stream. command
1583 output will be redirected if fout is not stdout.
1587 output will be redirected if fout is not stdout.
1584
1588
1585 if command fails and onerr is None, return status, else raise onerr
1589 if command fails and onerr is None, return status, else raise onerr
1586 object as exception.
1590 object as exception.
1587 '''
1591 '''
1588 if blockedtag is None:
1592 if blockedtag is None:
1589 # Long cmds tend to be because of an absolute path on cmd. Keep
1593 # Long cmds tend to be because of an absolute path on cmd. Keep
1590 # the tail end instead
1594 # the tail end instead
1591 cmdsuffix = cmd.translate(None, _keepalnum)[-85:]
1595 cmdsuffix = cmd.translate(None, _keepalnum)[-85:]
1592 blockedtag = 'unknown_system_' + cmdsuffix
1596 blockedtag = 'unknown_system_' + cmdsuffix
1593 out = self._fout
1597 out = self._fout
1594 if any(s[1] for s in self._bufferstates):
1598 if any(s[1] for s in self._bufferstates):
1595 out = self
1599 out = self
1596 with self.timeblockedsection(blockedtag):
1600 with self.timeblockedsection(blockedtag):
1597 rc = self._runsystem(cmd, environ=environ, cwd=cwd, out=out)
1601 rc = self._runsystem(cmd, environ=environ, cwd=cwd, out=out)
1598 if rc and onerr:
1602 if rc and onerr:
1599 errmsg = '%s %s' % (os.path.basename(cmd.split(None, 1)[0]),
1603 errmsg = '%s %s' % (os.path.basename(cmd.split(None, 1)[0]),
1600 procutil.explainexit(rc))
1604 procutil.explainexit(rc))
1601 if errprefix:
1605 if errprefix:
1602 errmsg = '%s: %s' % (errprefix, errmsg)
1606 errmsg = '%s: %s' % (errprefix, errmsg)
1603 raise onerr(errmsg)
1607 raise onerr(errmsg)
1604 return rc
1608 return rc
1605
1609
1606 def _runsystem(self, cmd, environ, cwd, out):
1610 def _runsystem(self, cmd, environ, cwd, out):
1607 """actually execute the given shell command (can be overridden by
1611 """actually execute the given shell command (can be overridden by
1608 extensions like chg)"""
1612 extensions like chg)"""
1609 return procutil.system(cmd, environ=environ, cwd=cwd, out=out)
1613 return procutil.system(cmd, environ=environ, cwd=cwd, out=out)
1610
1614
1611 def traceback(self, exc=None, force=False):
1615 def traceback(self, exc=None, force=False):
1612 '''print exception traceback if traceback printing enabled or forced.
1616 '''print exception traceback if traceback printing enabled or forced.
1613 only to call in exception handler. returns true if traceback
1617 only to call in exception handler. returns true if traceback
1614 printed.'''
1618 printed.'''
1615 if self.tracebackflag or force:
1619 if self.tracebackflag or force:
1616 if exc is None:
1620 if exc is None:
1617 exc = sys.exc_info()
1621 exc = sys.exc_info()
1618 cause = getattr(exc[1], 'cause', None)
1622 cause = getattr(exc[1], 'cause', None)
1619
1623
1620 if cause is not None:
1624 if cause is not None:
1621 causetb = traceback.format_tb(cause[2])
1625 causetb = traceback.format_tb(cause[2])
1622 exctb = traceback.format_tb(exc[2])
1626 exctb = traceback.format_tb(exc[2])
1623 exconly = traceback.format_exception_only(cause[0], cause[1])
1627 exconly = traceback.format_exception_only(cause[0], cause[1])
1624
1628
1625 # exclude frame where 'exc' was chained and rethrown from exctb
1629 # exclude frame where 'exc' was chained and rethrown from exctb
1626 self.write_err('Traceback (most recent call last):\n',
1630 self.write_err('Traceback (most recent call last):\n',
1627 ''.join(exctb[:-1]),
1631 ''.join(exctb[:-1]),
1628 ''.join(causetb),
1632 ''.join(causetb),
1629 ''.join(exconly))
1633 ''.join(exconly))
1630 else:
1634 else:
1631 output = traceback.format_exception(exc[0], exc[1], exc[2])
1635 output = traceback.format_exception(exc[0], exc[1], exc[2])
1632 self.write_err(encoding.strtolocal(r''.join(output)))
1636 self.write_err(encoding.strtolocal(r''.join(output)))
1633 return self.tracebackflag or force
1637 return self.tracebackflag or force
1634
1638
1635 def geteditor(self):
1639 def geteditor(self):
1636 '''return editor to use'''
1640 '''return editor to use'''
1637 if pycompat.sysplatform == 'plan9':
1641 if pycompat.sysplatform == 'plan9':
1638 # vi is the MIPS instruction simulator on Plan 9. We
1642 # vi is the MIPS instruction simulator on Plan 9. We
1639 # instead default to E to plumb commit messages to
1643 # instead default to E to plumb commit messages to
1640 # avoid confusion.
1644 # avoid confusion.
1641 editor = 'E'
1645 editor = 'E'
1642 else:
1646 else:
1643 editor = 'vi'
1647 editor = 'vi'
1644 return (encoding.environ.get("HGEDITOR") or
1648 return (encoding.environ.get("HGEDITOR") or
1645 self.config("ui", "editor", editor))
1649 self.config("ui", "editor", editor))
1646
1650
1647 @util.propertycache
1651 @util.propertycache
1648 def _progbar(self):
1652 def _progbar(self):
1649 """setup the progbar singleton to the ui object"""
1653 """setup the progbar singleton to the ui object"""
1650 if (self.quiet or self.debugflag
1654 if (self.quiet or self.debugflag
1651 or self.configbool('progress', 'disable')
1655 or self.configbool('progress', 'disable')
1652 or not progress.shouldprint(self)):
1656 or not progress.shouldprint(self)):
1653 return None
1657 return None
1654 return getprogbar(self)
1658 return getprogbar(self)
1655
1659
1656 def _progclear(self):
1660 def _progclear(self):
1657 """clear progress bar output if any. use it before any output"""
1661 """clear progress bar output if any. use it before any output"""
1658 if not haveprogbar(): # nothing loaded yet
1662 if not haveprogbar(): # nothing loaded yet
1659 return
1663 return
1660 if self._progbar is not None and self._progbar.printed:
1664 if self._progbar is not None and self._progbar.printed:
1661 self._progbar.clear()
1665 self._progbar.clear()
1662
1666
1663 def progress(self, topic, pos, item="", unit="", total=None):
1667 def progress(self, topic, pos, item="", unit="", total=None):
1664 '''show a progress message
1668 '''show a progress message
1665
1669
1666 By default a textual progress bar will be displayed if an operation
1670 By default a textual progress bar will be displayed if an operation
1667 takes too long. 'topic' is the current operation, 'item' is a
1671 takes too long. 'topic' is the current operation, 'item' is a
1668 non-numeric marker of the current position (i.e. the currently
1672 non-numeric marker of the current position (i.e. the currently
1669 in-process file), 'pos' is the current numeric position (i.e.
1673 in-process file), 'pos' is the current numeric position (i.e.
1670 revision, bytes, etc.), unit is a corresponding unit label,
1674 revision, bytes, etc.), unit is a corresponding unit label,
1671 and total is the highest expected pos.
1675 and total is the highest expected pos.
1672
1676
1673 Multiple nested topics may be active at a time.
1677 Multiple nested topics may be active at a time.
1674
1678
1675 All topics should be marked closed by setting pos to None at
1679 All topics should be marked closed by setting pos to None at
1676 termination.
1680 termination.
1677 '''
1681 '''
1678 if getattr(self._fmsgerr, 'structured', False):
1682 if getattr(self._fmsgerr, 'structured', False):
1679 # channel for machine-readable output with metadata, just send
1683 # channel for machine-readable output with metadata, just send
1680 # raw information
1684 # raw information
1681 # TODO: consider porting some useful information (e.g. estimated
1685 # TODO: consider porting some useful information (e.g. estimated
1682 # time) from progbar. we might want to support update delay to
1686 # time) from progbar. we might want to support update delay to
1683 # reduce the cost of transferring progress messages.
1687 # reduce the cost of transferring progress messages.
1684 self._fmsgerr.write(None, type=b'progress', topic=topic, pos=pos,
1688 self._fmsgerr.write(None, type=b'progress', topic=topic, pos=pos,
1685 item=item, unit=unit, total=total)
1689 item=item, unit=unit, total=total)
1686 elif self._progbar is not None:
1690 elif self._progbar is not None:
1687 self._progbar.progress(topic, pos, item=item, unit=unit,
1691 self._progbar.progress(topic, pos, item=item, unit=unit,
1688 total=total)
1692 total=total)
1689 if pos is None or not self.configbool('progress', 'debug'):
1693 if pos is None or not self.configbool('progress', 'debug'):
1690 return
1694 return
1691
1695
1692 if unit:
1696 if unit:
1693 unit = ' ' + unit
1697 unit = ' ' + unit
1694 if item:
1698 if item:
1695 item = ' ' + item
1699 item = ' ' + item
1696
1700
1697 if total:
1701 if total:
1698 pct = 100.0 * pos / total
1702 pct = 100.0 * pos / total
1699 self.debug('%s:%s %d/%d%s (%4.2f%%)\n'
1703 self.debug('%s:%s %d/%d%s (%4.2f%%)\n'
1700 % (topic, item, pos, total, unit, pct))
1704 % (topic, item, pos, total, unit, pct))
1701 else:
1705 else:
1702 self.debug('%s:%s %d%s\n' % (topic, item, pos, unit))
1706 self.debug('%s:%s %d%s\n' % (topic, item, pos, unit))
1703
1707
1704 def makeprogress(self, topic, unit="", total=None):
1708 def makeprogress(self, topic, unit="", total=None):
1705 '''exists only so low-level modules won't need to import scmutil'''
1709 '''exists only so low-level modules won't need to import scmutil'''
1706 return scmutil.progress(self, topic, unit, total)
1710 return scmutil.progress(self, topic, unit, total)
1707
1711
1708 def log(self, service, *msg, **opts):
1712 def log(self, service, *msg, **opts):
1709 '''hook for logging facility extensions
1713 '''hook for logging facility extensions
1710
1714
1711 service should be a readily-identifiable subsystem, which will
1715 service should be a readily-identifiable subsystem, which will
1712 allow filtering.
1716 allow filtering.
1713
1717
1714 *msg should be a newline-terminated format string to log, and
1718 *msg should be a newline-terminated format string to log, and
1715 then any values to %-format into that format string.
1719 then any values to %-format into that format string.
1716
1720
1717 **opts currently has no defined meanings.
1721 **opts currently has no defined meanings.
1718 '''
1722 '''
1719
1723
1720 def label(self, msg, label):
1724 def label(self, msg, label):
1721 '''style msg based on supplied label
1725 '''style msg based on supplied label
1722
1726
1723 If some color mode is enabled, this will add the necessary control
1727 If some color mode is enabled, this will add the necessary control
1724 characters to apply such color. In addition, 'debug' color mode adds
1728 characters to apply such color. In addition, 'debug' color mode adds
1725 markup showing which label affects a piece of text.
1729 markup showing which label affects a piece of text.
1726
1730
1727 ui.write(s, 'label') is equivalent to
1731 ui.write(s, 'label') is equivalent to
1728 ui.write(ui.label(s, 'label')).
1732 ui.write(ui.label(s, 'label')).
1729 '''
1733 '''
1730 if self._colormode is not None:
1734 if self._colormode is not None:
1731 return color.colorlabel(self, msg, label)
1735 return color.colorlabel(self, msg, label)
1732 return msg
1736 return msg
1733
1737
1734 def develwarn(self, msg, stacklevel=1, config=None):
1738 def develwarn(self, msg, stacklevel=1, config=None):
1735 """issue a developer warning message
1739 """issue a developer warning message
1736
1740
1737 Use 'stacklevel' to report the offender some layers further up in the
1741 Use 'stacklevel' to report the offender some layers further up in the
1738 stack.
1742 stack.
1739 """
1743 """
1740 if not self.configbool('devel', 'all-warnings'):
1744 if not self.configbool('devel', 'all-warnings'):
1741 if config is None or not self.configbool('devel', config):
1745 if config is None or not self.configbool('devel', config):
1742 return
1746 return
1743 msg = 'devel-warn: ' + msg
1747 msg = 'devel-warn: ' + msg
1744 stacklevel += 1 # get in develwarn
1748 stacklevel += 1 # get in develwarn
1745 if self.tracebackflag:
1749 if self.tracebackflag:
1746 util.debugstacktrace(msg, stacklevel, self._ferr, self._fout)
1750 util.debugstacktrace(msg, stacklevel, self._ferr, self._fout)
1747 self.log('develwarn', '%s at:\n%s' %
1751 self.log('develwarn', '%s at:\n%s' %
1748 (msg, ''.join(util.getstackframes(stacklevel))))
1752 (msg, ''.join(util.getstackframes(stacklevel))))
1749 else:
1753 else:
1750 curframe = inspect.currentframe()
1754 curframe = inspect.currentframe()
1751 calframe = inspect.getouterframes(curframe, 2)
1755 calframe = inspect.getouterframes(curframe, 2)
1752 fname, lineno, fmsg = calframe[stacklevel][1:4]
1756 fname, lineno, fmsg = calframe[stacklevel][1:4]
1753 fname, fmsg = pycompat.sysbytes(fname), pycompat.sysbytes(fmsg)
1757 fname, fmsg = pycompat.sysbytes(fname), pycompat.sysbytes(fmsg)
1754 self.write_err('%s at: %s:%d (%s)\n'
1758 self.write_err('%s at: %s:%d (%s)\n'
1755 % (msg, fname, lineno, fmsg))
1759 % (msg, fname, lineno, fmsg))
1756 self.log('develwarn', '%s at: %s:%d (%s)\n',
1760 self.log('develwarn', '%s at: %s:%d (%s)\n',
1757 msg, fname, lineno, fmsg)
1761 msg, fname, lineno, fmsg)
1758 curframe = calframe = None # avoid cycles
1762 curframe = calframe = None # avoid cycles
1759
1763
1760 def deprecwarn(self, msg, version, stacklevel=2):
1764 def deprecwarn(self, msg, version, stacklevel=2):
1761 """issue a deprecation warning
1765 """issue a deprecation warning
1762
1766
1763 - msg: message explaining what is deprecated and how to upgrade,
1767 - msg: message explaining what is deprecated and how to upgrade,
1764 - version: last version where the API will be supported,
1768 - version: last version where the API will be supported,
1765 """
1769 """
1766 if not (self.configbool('devel', 'all-warnings')
1770 if not (self.configbool('devel', 'all-warnings')
1767 or self.configbool('devel', 'deprec-warn')):
1771 or self.configbool('devel', 'deprec-warn')):
1768 return
1772 return
1769 msg += ("\n(compatibility will be dropped after Mercurial-%s,"
1773 msg += ("\n(compatibility will be dropped after Mercurial-%s,"
1770 " update your code.)") % version
1774 " update your code.)") % version
1771 self.develwarn(msg, stacklevel=stacklevel, config='deprec-warn')
1775 self.develwarn(msg, stacklevel=stacklevel, config='deprec-warn')
1772
1776
1773 def exportableenviron(self):
1777 def exportableenviron(self):
1774 """The environment variables that are safe to export, e.g. through
1778 """The environment variables that are safe to export, e.g. through
1775 hgweb.
1779 hgweb.
1776 """
1780 """
1777 return self._exportableenviron
1781 return self._exportableenviron
1778
1782
1779 @contextlib.contextmanager
1783 @contextlib.contextmanager
1780 def configoverride(self, overrides, source=""):
1784 def configoverride(self, overrides, source=""):
1781 """Context manager for temporary config overrides
1785 """Context manager for temporary config overrides
1782 `overrides` must be a dict of the following structure:
1786 `overrides` must be a dict of the following structure:
1783 {(section, name) : value}"""
1787 {(section, name) : value}"""
1784 backups = {}
1788 backups = {}
1785 try:
1789 try:
1786 for (section, name), value in overrides.items():
1790 for (section, name), value in overrides.items():
1787 backups[(section, name)] = self.backupconfig(section, name)
1791 backups[(section, name)] = self.backupconfig(section, name)
1788 self.setconfig(section, name, value, source)
1792 self.setconfig(section, name, value, source)
1789 yield
1793 yield
1790 finally:
1794 finally:
1791 for __, backup in backups.items():
1795 for __, backup in backups.items():
1792 self.restoreconfig(backup)
1796 self.restoreconfig(backup)
1793 # just restoring ui.quiet config to the previous value is not enough
1797 # just restoring ui.quiet config to the previous value is not enough
1794 # as it does not update ui.quiet class member
1798 # as it does not update ui.quiet class member
1795 if ('ui', 'quiet') in overrides:
1799 if ('ui', 'quiet') in overrides:
1796 self.fixconfig(section='ui')
1800 self.fixconfig(section='ui')
1797
1801
1798 class paths(dict):
1802 class paths(dict):
1799 """Represents a collection of paths and their configs.
1803 """Represents a collection of paths and their configs.
1800
1804
1801 Data is initially derived from ui instances and the config files they have
1805 Data is initially derived from ui instances and the config files they have
1802 loaded.
1806 loaded.
1803 """
1807 """
1804 def __init__(self, ui):
1808 def __init__(self, ui):
1805 dict.__init__(self)
1809 dict.__init__(self)
1806
1810
1807 for name, loc in ui.configitems('paths', ignoresub=True):
1811 for name, loc in ui.configitems('paths', ignoresub=True):
1808 # No location is the same as not existing.
1812 # No location is the same as not existing.
1809 if not loc:
1813 if not loc:
1810 continue
1814 continue
1811 loc, sub = ui.configsuboptions('paths', name)
1815 loc, sub = ui.configsuboptions('paths', name)
1812 self[name] = path(ui, name, rawloc=loc, suboptions=sub)
1816 self[name] = path(ui, name, rawloc=loc, suboptions=sub)
1813
1817
1814 def getpath(self, name, default=None):
1818 def getpath(self, name, default=None):
1815 """Return a ``path`` from a string, falling back to default.
1819 """Return a ``path`` from a string, falling back to default.
1816
1820
1817 ``name`` can be a named path or locations. Locations are filesystem
1821 ``name`` can be a named path or locations. Locations are filesystem
1818 paths or URIs.
1822 paths or URIs.
1819
1823
1820 Returns None if ``name`` is not a registered path, a URI, or a local
1824 Returns None if ``name`` is not a registered path, a URI, or a local
1821 path to a repo.
1825 path to a repo.
1822 """
1826 """
1823 # Only fall back to default if no path was requested.
1827 # Only fall back to default if no path was requested.
1824 if name is None:
1828 if name is None:
1825 if not default:
1829 if not default:
1826 default = ()
1830 default = ()
1827 elif not isinstance(default, (tuple, list)):
1831 elif not isinstance(default, (tuple, list)):
1828 default = (default,)
1832 default = (default,)
1829 for k in default:
1833 for k in default:
1830 try:
1834 try:
1831 return self[k]
1835 return self[k]
1832 except KeyError:
1836 except KeyError:
1833 continue
1837 continue
1834 return None
1838 return None
1835
1839
1836 # Most likely empty string.
1840 # Most likely empty string.
1837 # This may need to raise in the future.
1841 # This may need to raise in the future.
1838 if not name:
1842 if not name:
1839 return None
1843 return None
1840
1844
1841 try:
1845 try:
1842 return self[name]
1846 return self[name]
1843 except KeyError:
1847 except KeyError:
1844 # Try to resolve as a local path or URI.
1848 # Try to resolve as a local path or URI.
1845 try:
1849 try:
1846 # We don't pass sub-options in, so no need to pass ui instance.
1850 # We don't pass sub-options in, so no need to pass ui instance.
1847 return path(None, None, rawloc=name)
1851 return path(None, None, rawloc=name)
1848 except ValueError:
1852 except ValueError:
1849 raise error.RepoError(_('repository %s does not exist') %
1853 raise error.RepoError(_('repository %s does not exist') %
1850 name)
1854 name)
1851
1855
1852 _pathsuboptions = {}
1856 _pathsuboptions = {}
1853
1857
1854 def pathsuboption(option, attr):
1858 def pathsuboption(option, attr):
1855 """Decorator used to declare a path sub-option.
1859 """Decorator used to declare a path sub-option.
1856
1860
1857 Arguments are the sub-option name and the attribute it should set on
1861 Arguments are the sub-option name and the attribute it should set on
1858 ``path`` instances.
1862 ``path`` instances.
1859
1863
1860 The decorated function will receive as arguments a ``ui`` instance,
1864 The decorated function will receive as arguments a ``ui`` instance,
1861 ``path`` instance, and the string value of this option from the config.
1865 ``path`` instance, and the string value of this option from the config.
1862 The function should return the value that will be set on the ``path``
1866 The function should return the value that will be set on the ``path``
1863 instance.
1867 instance.
1864
1868
1865 This decorator can be used to perform additional verification of
1869 This decorator can be used to perform additional verification of
1866 sub-options and to change the type of sub-options.
1870 sub-options and to change the type of sub-options.
1867 """
1871 """
1868 def register(func):
1872 def register(func):
1869 _pathsuboptions[option] = (attr, func)
1873 _pathsuboptions[option] = (attr, func)
1870 return func
1874 return func
1871 return register
1875 return register
1872
1876
1873 @pathsuboption('pushurl', 'pushloc')
1877 @pathsuboption('pushurl', 'pushloc')
1874 def pushurlpathoption(ui, path, value):
1878 def pushurlpathoption(ui, path, value):
1875 u = util.url(value)
1879 u = util.url(value)
1876 # Actually require a URL.
1880 # Actually require a URL.
1877 if not u.scheme:
1881 if not u.scheme:
1878 ui.warn(_('(paths.%s:pushurl not a URL; ignoring)\n') % path.name)
1882 ui.warn(_('(paths.%s:pushurl not a URL; ignoring)\n') % path.name)
1879 return None
1883 return None
1880
1884
1881 # Don't support the #foo syntax in the push URL to declare branch to
1885 # Don't support the #foo syntax in the push URL to declare branch to
1882 # push.
1886 # push.
1883 if u.fragment:
1887 if u.fragment:
1884 ui.warn(_('("#fragment" in paths.%s:pushurl not supported; '
1888 ui.warn(_('("#fragment" in paths.%s:pushurl not supported; '
1885 'ignoring)\n') % path.name)
1889 'ignoring)\n') % path.name)
1886 u.fragment = None
1890 u.fragment = None
1887
1891
1888 return bytes(u)
1892 return bytes(u)
1889
1893
1890 @pathsuboption('pushrev', 'pushrev')
1894 @pathsuboption('pushrev', 'pushrev')
1891 def pushrevpathoption(ui, path, value):
1895 def pushrevpathoption(ui, path, value):
1892 return value
1896 return value
1893
1897
1894 class path(object):
1898 class path(object):
1895 """Represents an individual path and its configuration."""
1899 """Represents an individual path and its configuration."""
1896
1900
1897 def __init__(self, ui, name, rawloc=None, suboptions=None):
1901 def __init__(self, ui, name, rawloc=None, suboptions=None):
1898 """Construct a path from its config options.
1902 """Construct a path from its config options.
1899
1903
1900 ``ui`` is the ``ui`` instance the path is coming from.
1904 ``ui`` is the ``ui`` instance the path is coming from.
1901 ``name`` is the symbolic name of the path.
1905 ``name`` is the symbolic name of the path.
1902 ``rawloc`` is the raw location, as defined in the config.
1906 ``rawloc`` is the raw location, as defined in the config.
1903 ``pushloc`` is the raw locations pushes should be made to.
1907 ``pushloc`` is the raw locations pushes should be made to.
1904
1908
1905 If ``name`` is not defined, we require that the location be a) a local
1909 If ``name`` is not defined, we require that the location be a) a local
1906 filesystem path with a .hg directory or b) a URL. If not,
1910 filesystem path with a .hg directory or b) a URL. If not,
1907 ``ValueError`` is raised.
1911 ``ValueError`` is raised.
1908 """
1912 """
1909 if not rawloc:
1913 if not rawloc:
1910 raise ValueError('rawloc must be defined')
1914 raise ValueError('rawloc must be defined')
1911
1915
1912 # Locations may define branches via syntax <base>#<branch>.
1916 # Locations may define branches via syntax <base>#<branch>.
1913 u = util.url(rawloc)
1917 u = util.url(rawloc)
1914 branch = None
1918 branch = None
1915 if u.fragment:
1919 if u.fragment:
1916 branch = u.fragment
1920 branch = u.fragment
1917 u.fragment = None
1921 u.fragment = None
1918
1922
1919 self.url = u
1923 self.url = u
1920 self.branch = branch
1924 self.branch = branch
1921
1925
1922 self.name = name
1926 self.name = name
1923 self.rawloc = rawloc
1927 self.rawloc = rawloc
1924 self.loc = '%s' % u
1928 self.loc = '%s' % u
1925
1929
1926 # When given a raw location but not a symbolic name, validate the
1930 # When given a raw location but not a symbolic name, validate the
1927 # location is valid.
1931 # location is valid.
1928 if not name and not u.scheme and not self._isvalidlocalpath(self.loc):
1932 if not name and not u.scheme and not self._isvalidlocalpath(self.loc):
1929 raise ValueError('location is not a URL or path to a local '
1933 raise ValueError('location is not a URL or path to a local '
1930 'repo: %s' % rawloc)
1934 'repo: %s' % rawloc)
1931
1935
1932 suboptions = suboptions or {}
1936 suboptions = suboptions or {}
1933
1937
1934 # Now process the sub-options. If a sub-option is registered, its
1938 # Now process the sub-options. If a sub-option is registered, its
1935 # attribute will always be present. The value will be None if there
1939 # attribute will always be present. The value will be None if there
1936 # was no valid sub-option.
1940 # was no valid sub-option.
1937 for suboption, (attr, func) in _pathsuboptions.iteritems():
1941 for suboption, (attr, func) in _pathsuboptions.iteritems():
1938 if suboption not in suboptions:
1942 if suboption not in suboptions:
1939 setattr(self, attr, None)
1943 setattr(self, attr, None)
1940 continue
1944 continue
1941
1945
1942 value = func(ui, self, suboptions[suboption])
1946 value = func(ui, self, suboptions[suboption])
1943 setattr(self, attr, value)
1947 setattr(self, attr, value)
1944
1948
1945 def _isvalidlocalpath(self, path):
1949 def _isvalidlocalpath(self, path):
1946 """Returns True if the given path is a potentially valid repository.
1950 """Returns True if the given path is a potentially valid repository.
1947 This is its own function so that extensions can change the definition of
1951 This is its own function so that extensions can change the definition of
1948 'valid' in this case (like when pulling from a git repo into a hg
1952 'valid' in this case (like when pulling from a git repo into a hg
1949 one)."""
1953 one)."""
1950 return os.path.isdir(os.path.join(path, '.hg'))
1954 return os.path.isdir(os.path.join(path, '.hg'))
1951
1955
1952 @property
1956 @property
1953 def suboptions(self):
1957 def suboptions(self):
1954 """Return sub-options and their values for this path.
1958 """Return sub-options and their values for this path.
1955
1959
1956 This is intended to be used for presentation purposes.
1960 This is intended to be used for presentation purposes.
1957 """
1961 """
1958 d = {}
1962 d = {}
1959 for subopt, (attr, _func) in _pathsuboptions.iteritems():
1963 for subopt, (attr, _func) in _pathsuboptions.iteritems():
1960 value = getattr(self, attr)
1964 value = getattr(self, attr)
1961 if value is not None:
1965 if value is not None:
1962 d[subopt] = value
1966 d[subopt] = value
1963 return d
1967 return d
1964
1968
1965 # we instantiate one globally shared progress bar to avoid
1969 # we instantiate one globally shared progress bar to avoid
1966 # competing progress bars when multiple UI objects get created
1970 # competing progress bars when multiple UI objects get created
1967 _progresssingleton = None
1971 _progresssingleton = None
1968
1972
1969 def getprogbar(ui):
1973 def getprogbar(ui):
1970 global _progresssingleton
1974 global _progresssingleton
1971 if _progresssingleton is None:
1975 if _progresssingleton is None:
1972 # passing 'ui' object to the singleton is fishy,
1976 # passing 'ui' object to the singleton is fishy,
1973 # this is how the extension used to work but feel free to rework it.
1977 # this is how the extension used to work but feel free to rework it.
1974 _progresssingleton = progress.progbar(ui)
1978 _progresssingleton = progress.progbar(ui)
1975 return _progresssingleton
1979 return _progresssingleton
1976
1980
1977 def haveprogbar():
1981 def haveprogbar():
1978 return _progresssingleton is not None
1982 return _progresssingleton is not None
1979
1983
1980 def _selectmsgdests(ui):
1984 def _selectmsgdests(ui):
1981 name = ui.config(b'ui', b'message-output')
1985 name = ui.config(b'ui', b'message-output')
1982 if name == b'channel':
1986 if name == b'channel':
1983 if ui.fmsg:
1987 if ui.fmsg:
1984 return ui.fmsg, ui.fmsg
1988 return ui.fmsg, ui.fmsg
1985 else:
1989 else:
1986 # fall back to ferr if channel isn't ready so that status/error
1990 # fall back to ferr if channel isn't ready so that status/error
1987 # messages can be printed
1991 # messages can be printed
1988 return ui.ferr, ui.ferr
1992 return ui.ferr, ui.ferr
1989 if name == b'stdio':
1993 if name == b'stdio':
1990 return ui.fout, ui.ferr
1994 return ui.fout, ui.ferr
1991 if name == b'stderr':
1995 if name == b'stderr':
1992 return ui.ferr, ui.ferr
1996 return ui.ferr, ui.ferr
1993 raise error.Abort(b'invalid ui.message-output destination: %s' % name)
1997 raise error.Abort(b'invalid ui.message-output destination: %s' % name)
1994
1998
1995 def _writemsgwith(write, dest, *args, **opts):
1999 def _writemsgwith(write, dest, *args, **opts):
1996 """Write ui message with the given ui._write*() function
2000 """Write ui message with the given ui._write*() function
1997
2001
1998 The specified message type is translated to 'ui.<type>' label if the dest
2002 The specified message type is translated to 'ui.<type>' label if the dest
1999 isn't a structured channel, so that the message will be colorized.
2003 isn't a structured channel, so that the message will be colorized.
2000 """
2004 """
2001 # TODO: maybe change 'type' to a mandatory option
2005 # TODO: maybe change 'type' to a mandatory option
2002 if r'type' in opts and not getattr(dest, 'structured', False):
2006 if r'type' in opts and not getattr(dest, 'structured', False):
2003 opts[r'label'] = opts.get(r'label', '') + ' ui.%s' % opts.pop(r'type')
2007 opts[r'label'] = opts.get(r'label', '') + ' ui.%s' % opts.pop(r'type')
2004 write(dest, *args, **opts)
2008 write(dest, *args, **opts)
General Comments 0
You need to be logged in to leave comments. Login now