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