##// END OF EJS Templates
cmdutil: convert _revertprefetch() to a generic stored file hook (API)...
Matt Harbison -
r35941:efbd0423 default
parent child Browse files
Show More
@@ -1,3146 +1,3160 b''
1 1 # cmdutil.py - help for command processing in mercurial
2 2 #
3 3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 from __future__ import absolute_import
9 9
10 10 import errno
11 11 import os
12 12 import re
13 13 import tempfile
14 14
15 15 from .i18n import _
16 16 from .node import (
17 17 hex,
18 18 nullid,
19 19 nullrev,
20 20 short,
21 21 )
22 22
23 23 from . import (
24 24 bookmarks,
25 25 changelog,
26 26 copies,
27 27 crecord as crecordmod,
28 28 dirstateguard,
29 29 encoding,
30 30 error,
31 31 formatter,
32 32 logcmdutil,
33 33 match as matchmod,
34 34 obsolete,
35 35 patch,
36 36 pathutil,
37 37 pycompat,
38 38 registrar,
39 39 revlog,
40 40 rewriteutil,
41 41 scmutil,
42 42 smartset,
43 43 templater,
44 44 util,
45 45 vfs as vfsmod,
46 46 )
47 47 stringio = util.stringio
48 48
49 49 # templates of common command options
50 50
51 51 dryrunopts = [
52 52 ('n', 'dry-run', None,
53 53 _('do not perform actions, just print output')),
54 54 ]
55 55
56 56 remoteopts = [
57 57 ('e', 'ssh', '',
58 58 _('specify ssh command to use'), _('CMD')),
59 59 ('', 'remotecmd', '',
60 60 _('specify hg command to run on the remote side'), _('CMD')),
61 61 ('', 'insecure', None,
62 62 _('do not verify server certificate (ignoring web.cacerts config)')),
63 63 ]
64 64
65 65 walkopts = [
66 66 ('I', 'include', [],
67 67 _('include names matching the given patterns'), _('PATTERN')),
68 68 ('X', 'exclude', [],
69 69 _('exclude names matching the given patterns'), _('PATTERN')),
70 70 ]
71 71
72 72 commitopts = [
73 73 ('m', 'message', '',
74 74 _('use text as commit message'), _('TEXT')),
75 75 ('l', 'logfile', '',
76 76 _('read commit message from file'), _('FILE')),
77 77 ]
78 78
79 79 commitopts2 = [
80 80 ('d', 'date', '',
81 81 _('record the specified date as commit date'), _('DATE')),
82 82 ('u', 'user', '',
83 83 _('record the specified user as committer'), _('USER')),
84 84 ]
85 85
86 86 # hidden for now
87 87 formatteropts = [
88 88 ('T', 'template', '',
89 89 _('display with template (EXPERIMENTAL)'), _('TEMPLATE')),
90 90 ]
91 91
92 92 templateopts = [
93 93 ('', 'style', '',
94 94 _('display using template map file (DEPRECATED)'), _('STYLE')),
95 95 ('T', 'template', '',
96 96 _('display with template'), _('TEMPLATE')),
97 97 ]
98 98
99 99 logopts = [
100 100 ('p', 'patch', None, _('show patch')),
101 101 ('g', 'git', None, _('use git extended diff format')),
102 102 ('l', 'limit', '',
103 103 _('limit number of changes displayed'), _('NUM')),
104 104 ('M', 'no-merges', None, _('do not show merges')),
105 105 ('', 'stat', None, _('output diffstat-style summary of changes')),
106 106 ('G', 'graph', None, _("show the revision DAG")),
107 107 ] + templateopts
108 108
109 109 diffopts = [
110 110 ('a', 'text', None, _('treat all files as text')),
111 111 ('g', 'git', None, _('use git extended diff format')),
112 112 ('', 'binary', None, _('generate binary diffs in git mode (default)')),
113 113 ('', 'nodates', None, _('omit dates from diff headers'))
114 114 ]
115 115
116 116 diffwsopts = [
117 117 ('w', 'ignore-all-space', None,
118 118 _('ignore white space when comparing lines')),
119 119 ('b', 'ignore-space-change', None,
120 120 _('ignore changes in the amount of white space')),
121 121 ('B', 'ignore-blank-lines', None,
122 122 _('ignore changes whose lines are all blank')),
123 123 ('Z', 'ignore-space-at-eol', None,
124 124 _('ignore changes in whitespace at EOL')),
125 125 ]
126 126
127 127 diffopts2 = [
128 128 ('', 'noprefix', None, _('omit a/ and b/ prefixes from filenames')),
129 129 ('p', 'show-function', None, _('show which function each change is in')),
130 130 ('', 'reverse', None, _('produce a diff that undoes the changes')),
131 131 ] + diffwsopts + [
132 132 ('U', 'unified', '',
133 133 _('number of lines of context to show'), _('NUM')),
134 134 ('', 'stat', None, _('output diffstat-style summary of changes')),
135 135 ('', 'root', '', _('produce diffs relative to subdirectory'), _('DIR')),
136 136 ]
137 137
138 138 mergetoolopts = [
139 139 ('t', 'tool', '', _('specify merge tool')),
140 140 ]
141 141
142 142 similarityopts = [
143 143 ('s', 'similarity', '',
144 144 _('guess renamed files by similarity (0<=s<=100)'), _('SIMILARITY'))
145 145 ]
146 146
147 147 subrepoopts = [
148 148 ('S', 'subrepos', None,
149 149 _('recurse into subrepositories'))
150 150 ]
151 151
152 152 debugrevlogopts = [
153 153 ('c', 'changelog', False, _('open changelog')),
154 154 ('m', 'manifest', False, _('open manifest')),
155 155 ('', 'dir', '', _('open directory manifest')),
156 156 ]
157 157
158 158 # special string such that everything below this line will be ingored in the
159 159 # editor text
160 160 _linebelow = "^HG: ------------------------ >8 ------------------------$"
161 161
162 162 def ishunk(x):
163 163 hunkclasses = (crecordmod.uihunk, patch.recordhunk)
164 164 return isinstance(x, hunkclasses)
165 165
166 166 def newandmodified(chunks, originalchunks):
167 167 newlyaddedandmodifiedfiles = set()
168 168 for chunk in chunks:
169 169 if ishunk(chunk) and chunk.header.isnewfile() and chunk not in \
170 170 originalchunks:
171 171 newlyaddedandmodifiedfiles.add(chunk.header.filename())
172 172 return newlyaddedandmodifiedfiles
173 173
174 174 def parsealiases(cmd):
175 175 return cmd.lstrip("^").split("|")
176 176
177 177 def setupwrapcolorwrite(ui):
178 178 # wrap ui.write so diff output can be labeled/colorized
179 179 def wrapwrite(orig, *args, **kw):
180 180 label = kw.pop(r'label', '')
181 181 for chunk, l in patch.difflabel(lambda: args):
182 182 orig(chunk, label=label + l)
183 183
184 184 oldwrite = ui.write
185 185 def wrap(*args, **kwargs):
186 186 return wrapwrite(oldwrite, *args, **kwargs)
187 187 setattr(ui, 'write', wrap)
188 188 return oldwrite
189 189
190 190 def filterchunks(ui, originalhunks, usecurses, testfile, operation=None):
191 191 if usecurses:
192 192 if testfile:
193 193 recordfn = crecordmod.testdecorator(testfile,
194 194 crecordmod.testchunkselector)
195 195 else:
196 196 recordfn = crecordmod.chunkselector
197 197
198 198 return crecordmod.filterpatch(ui, originalhunks, recordfn, operation)
199 199
200 200 else:
201 201 return patch.filterpatch(ui, originalhunks, operation)
202 202
203 203 def recordfilter(ui, originalhunks, operation=None):
204 204 """ Prompts the user to filter the originalhunks and return a list of
205 205 selected hunks.
206 206 *operation* is used for to build ui messages to indicate the user what
207 207 kind of filtering they are doing: reverting, committing, shelving, etc.
208 208 (see patch.filterpatch).
209 209 """
210 210 usecurses = crecordmod.checkcurses(ui)
211 211 testfile = ui.config('experimental', 'crecordtest')
212 212 oldwrite = setupwrapcolorwrite(ui)
213 213 try:
214 214 newchunks, newopts = filterchunks(ui, originalhunks, usecurses,
215 215 testfile, operation)
216 216 finally:
217 217 ui.write = oldwrite
218 218 return newchunks, newopts
219 219
220 220 def dorecord(ui, repo, commitfunc, cmdsuggest, backupall,
221 221 filterfn, *pats, **opts):
222 222 from . import merge as mergemod
223 223 opts = pycompat.byteskwargs(opts)
224 224 if not ui.interactive():
225 225 if cmdsuggest:
226 226 msg = _('running non-interactively, use %s instead') % cmdsuggest
227 227 else:
228 228 msg = _('running non-interactively')
229 229 raise error.Abort(msg)
230 230
231 231 # make sure username is set before going interactive
232 232 if not opts.get('user'):
233 233 ui.username() # raise exception, username not provided
234 234
235 235 def recordfunc(ui, repo, message, match, opts):
236 236 """This is generic record driver.
237 237
238 238 Its job is to interactively filter local changes, and
239 239 accordingly prepare working directory into a state in which the
240 240 job can be delegated to a non-interactive commit command such as
241 241 'commit' or 'qrefresh'.
242 242
243 243 After the actual job is done by non-interactive command, the
244 244 working directory is restored to its original state.
245 245
246 246 In the end we'll record interesting changes, and everything else
247 247 will be left in place, so the user can continue working.
248 248 """
249 249
250 250 checkunfinished(repo, commit=True)
251 251 wctx = repo[None]
252 252 merge = len(wctx.parents()) > 1
253 253 if merge:
254 254 raise error.Abort(_('cannot partially commit a merge '
255 255 '(use "hg commit" instead)'))
256 256
257 257 def fail(f, msg):
258 258 raise error.Abort('%s: %s' % (f, msg))
259 259
260 260 force = opts.get('force')
261 261 if not force:
262 262 vdirs = []
263 263 match.explicitdir = vdirs.append
264 264 match.bad = fail
265 265
266 266 status = repo.status(match=match)
267 267 if not force:
268 268 repo.checkcommitpatterns(wctx, vdirs, match, status, fail)
269 269 diffopts = patch.difffeatureopts(ui, opts=opts, whitespace=True)
270 270 diffopts.nodates = True
271 271 diffopts.git = True
272 272 diffopts.showfunc = True
273 273 originaldiff = patch.diff(repo, changes=status, opts=diffopts)
274 274 originalchunks = patch.parsepatch(originaldiff)
275 275
276 276 # 1. filter patch, since we are intending to apply subset of it
277 277 try:
278 278 chunks, newopts = filterfn(ui, originalchunks)
279 279 except error.PatchError as err:
280 280 raise error.Abort(_('error parsing patch: %s') % err)
281 281 opts.update(newopts)
282 282
283 283 # We need to keep a backup of files that have been newly added and
284 284 # modified during the recording process because there is a previous
285 285 # version without the edit in the workdir
286 286 newlyaddedandmodifiedfiles = newandmodified(chunks, originalchunks)
287 287 contenders = set()
288 288 for h in chunks:
289 289 try:
290 290 contenders.update(set(h.files()))
291 291 except AttributeError:
292 292 pass
293 293
294 294 changed = status.modified + status.added + status.removed
295 295 newfiles = [f for f in changed if f in contenders]
296 296 if not newfiles:
297 297 ui.status(_('no changes to record\n'))
298 298 return 0
299 299
300 300 modified = set(status.modified)
301 301
302 302 # 2. backup changed files, so we can restore them in the end
303 303
304 304 if backupall:
305 305 tobackup = changed
306 306 else:
307 307 tobackup = [f for f in newfiles if f in modified or f in \
308 308 newlyaddedandmodifiedfiles]
309 309 backups = {}
310 310 if tobackup:
311 311 backupdir = repo.vfs.join('record-backups')
312 312 try:
313 313 os.mkdir(backupdir)
314 314 except OSError as err:
315 315 if err.errno != errno.EEXIST:
316 316 raise
317 317 try:
318 318 # backup continues
319 319 for f in tobackup:
320 320 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
321 321 dir=backupdir)
322 322 os.close(fd)
323 323 ui.debug('backup %r as %r\n' % (f, tmpname))
324 324 util.copyfile(repo.wjoin(f), tmpname, copystat=True)
325 325 backups[f] = tmpname
326 326
327 327 fp = stringio()
328 328 for c in chunks:
329 329 fname = c.filename()
330 330 if fname in backups:
331 331 c.write(fp)
332 332 dopatch = fp.tell()
333 333 fp.seek(0)
334 334
335 335 # 2.5 optionally review / modify patch in text editor
336 336 if opts.get('review', False):
337 337 patchtext = (crecordmod.diffhelptext
338 338 + crecordmod.patchhelptext
339 339 + fp.read())
340 340 reviewedpatch = ui.edit(patchtext, "",
341 341 action="diff",
342 342 repopath=repo.path)
343 343 fp.truncate(0)
344 344 fp.write(reviewedpatch)
345 345 fp.seek(0)
346 346
347 347 [os.unlink(repo.wjoin(c)) for c in newlyaddedandmodifiedfiles]
348 348 # 3a. apply filtered patch to clean repo (clean)
349 349 if backups:
350 350 # Equivalent to hg.revert
351 351 m = scmutil.matchfiles(repo, backups.keys())
352 352 mergemod.update(repo, repo.dirstate.p1(),
353 353 False, True, matcher=m)
354 354
355 355 # 3b. (apply)
356 356 if dopatch:
357 357 try:
358 358 ui.debug('applying patch\n')
359 359 ui.debug(fp.getvalue())
360 360 patch.internalpatch(ui, repo, fp, 1, eolmode=None)
361 361 except error.PatchError as err:
362 362 raise error.Abort(str(err))
363 363 del fp
364 364
365 365 # 4. We prepared working directory according to filtered
366 366 # patch. Now is the time to delegate the job to
367 367 # commit/qrefresh or the like!
368 368
369 369 # Make all of the pathnames absolute.
370 370 newfiles = [repo.wjoin(nf) for nf in newfiles]
371 371 return commitfunc(ui, repo, *newfiles, **pycompat.strkwargs(opts))
372 372 finally:
373 373 # 5. finally restore backed-up files
374 374 try:
375 375 dirstate = repo.dirstate
376 376 for realname, tmpname in backups.iteritems():
377 377 ui.debug('restoring %r to %r\n' % (tmpname, realname))
378 378
379 379 if dirstate[realname] == 'n':
380 380 # without normallookup, restoring timestamp
381 381 # may cause partially committed files
382 382 # to be treated as unmodified
383 383 dirstate.normallookup(realname)
384 384
385 385 # copystat=True here and above are a hack to trick any
386 386 # editors that have f open that we haven't modified them.
387 387 #
388 388 # Also note that this racy as an editor could notice the
389 389 # file's mtime before we've finished writing it.
390 390 util.copyfile(tmpname, repo.wjoin(realname), copystat=True)
391 391 os.unlink(tmpname)
392 392 if tobackup:
393 393 os.rmdir(backupdir)
394 394 except OSError:
395 395 pass
396 396
397 397 def recordinwlock(ui, repo, message, match, opts):
398 398 with repo.wlock():
399 399 return recordfunc(ui, repo, message, match, opts)
400 400
401 401 return commit(ui, repo, recordinwlock, pats, opts)
402 402
403 403 class dirnode(object):
404 404 """
405 405 Represent a directory in user working copy with information required for
406 406 the purpose of tersing its status.
407 407
408 408 path is the path to the directory
409 409
410 410 statuses is a set of statuses of all files in this directory (this includes
411 411 all the files in all the subdirectories too)
412 412
413 413 files is a list of files which are direct child of this directory
414 414
415 415 subdirs is a dictionary of sub-directory name as the key and it's own
416 416 dirnode object as the value
417 417 """
418 418
419 419 def __init__(self, dirpath):
420 420 self.path = dirpath
421 421 self.statuses = set([])
422 422 self.files = []
423 423 self.subdirs = {}
424 424
425 425 def _addfileindir(self, filename, status):
426 426 """Add a file in this directory as a direct child."""
427 427 self.files.append((filename, status))
428 428
429 429 def addfile(self, filename, status):
430 430 """
431 431 Add a file to this directory or to its direct parent directory.
432 432
433 433 If the file is not direct child of this directory, we traverse to the
434 434 directory of which this file is a direct child of and add the file
435 435 there.
436 436 """
437 437
438 438 # the filename contains a path separator, it means it's not the direct
439 439 # child of this directory
440 440 if '/' in filename:
441 441 subdir, filep = filename.split('/', 1)
442 442
443 443 # does the dirnode object for subdir exists
444 444 if subdir not in self.subdirs:
445 445 subdirpath = os.path.join(self.path, subdir)
446 446 self.subdirs[subdir] = dirnode(subdirpath)
447 447
448 448 # try adding the file in subdir
449 449 self.subdirs[subdir].addfile(filep, status)
450 450
451 451 else:
452 452 self._addfileindir(filename, status)
453 453
454 454 if status not in self.statuses:
455 455 self.statuses.add(status)
456 456
457 457 def iterfilepaths(self):
458 458 """Yield (status, path) for files directly under this directory."""
459 459 for f, st in self.files:
460 460 yield st, os.path.join(self.path, f)
461 461
462 462 def tersewalk(self, terseargs):
463 463 """
464 464 Yield (status, path) obtained by processing the status of this
465 465 dirnode.
466 466
467 467 terseargs is the string of arguments passed by the user with `--terse`
468 468 flag.
469 469
470 470 Following are the cases which can happen:
471 471
472 472 1) All the files in the directory (including all the files in its
473 473 subdirectories) share the same status and the user has asked us to terse
474 474 that status. -> yield (status, dirpath)
475 475
476 476 2) Otherwise, we do following:
477 477
478 478 a) Yield (status, filepath) for all the files which are in this
479 479 directory (only the ones in this directory, not the subdirs)
480 480
481 481 b) Recurse the function on all the subdirectories of this
482 482 directory
483 483 """
484 484
485 485 if len(self.statuses) == 1:
486 486 onlyst = self.statuses.pop()
487 487
488 488 # Making sure we terse only when the status abbreviation is
489 489 # passed as terse argument
490 490 if onlyst in terseargs:
491 491 yield onlyst, self.path + pycompat.ossep
492 492 return
493 493
494 494 # add the files to status list
495 495 for st, fpath in self.iterfilepaths():
496 496 yield st, fpath
497 497
498 498 #recurse on the subdirs
499 499 for dirobj in self.subdirs.values():
500 500 for st, fpath in dirobj.tersewalk(terseargs):
501 501 yield st, fpath
502 502
503 503 def tersedir(statuslist, terseargs):
504 504 """
505 505 Terse the status if all the files in a directory shares the same status.
506 506
507 507 statuslist is scmutil.status() object which contains a list of files for
508 508 each status.
509 509 terseargs is string which is passed by the user as the argument to `--terse`
510 510 flag.
511 511
512 512 The function makes a tree of objects of dirnode class, and at each node it
513 513 stores the information required to know whether we can terse a certain
514 514 directory or not.
515 515 """
516 516 # the order matters here as that is used to produce final list
517 517 allst = ('m', 'a', 'r', 'd', 'u', 'i', 'c')
518 518
519 519 # checking the argument validity
520 520 for s in pycompat.bytestr(terseargs):
521 521 if s not in allst:
522 522 raise error.Abort(_("'%s' not recognized") % s)
523 523
524 524 # creating a dirnode object for the root of the repo
525 525 rootobj = dirnode('')
526 526 pstatus = ('modified', 'added', 'deleted', 'clean', 'unknown',
527 527 'ignored', 'removed')
528 528
529 529 tersedict = {}
530 530 for attrname in pstatus:
531 531 statuschar = attrname[0:1]
532 532 for f in getattr(statuslist, attrname):
533 533 rootobj.addfile(f, statuschar)
534 534 tersedict[statuschar] = []
535 535
536 536 # we won't be tersing the root dir, so add files in it
537 537 for st, fpath in rootobj.iterfilepaths():
538 538 tersedict[st].append(fpath)
539 539
540 540 # process each sub-directory and build tersedict
541 541 for subdir in rootobj.subdirs.values():
542 542 for st, f in subdir.tersewalk(terseargs):
543 543 tersedict[st].append(f)
544 544
545 545 tersedlist = []
546 546 for st in allst:
547 547 tersedict[st].sort()
548 548 tersedlist.append(tersedict[st])
549 549
550 550 return tersedlist
551 551
552 552 def _commentlines(raw):
553 553 '''Surround lineswith a comment char and a new line'''
554 554 lines = raw.splitlines()
555 555 commentedlines = ['# %s' % line for line in lines]
556 556 return '\n'.join(commentedlines) + '\n'
557 557
558 558 def _conflictsmsg(repo):
559 559 # avoid merge cycle
560 560 from . import merge as mergemod
561 561 mergestate = mergemod.mergestate.read(repo)
562 562 if not mergestate.active():
563 563 return
564 564
565 565 m = scmutil.match(repo[None])
566 566 unresolvedlist = [f for f in mergestate.unresolved() if m(f)]
567 567 if unresolvedlist:
568 568 mergeliststr = '\n'.join(
569 569 [' %s' % util.pathto(repo.root, pycompat.getcwd(), path)
570 570 for path in unresolvedlist])
571 571 msg = _('''Unresolved merge conflicts:
572 572
573 573 %s
574 574
575 575 To mark files as resolved: hg resolve --mark FILE''') % mergeliststr
576 576 else:
577 577 msg = _('No unresolved merge conflicts.')
578 578
579 579 return _commentlines(msg)
580 580
581 581 def _helpmessage(continuecmd, abortcmd):
582 582 msg = _('To continue: %s\n'
583 583 'To abort: %s') % (continuecmd, abortcmd)
584 584 return _commentlines(msg)
585 585
586 586 def _rebasemsg():
587 587 return _helpmessage('hg rebase --continue', 'hg rebase --abort')
588 588
589 589 def _histeditmsg():
590 590 return _helpmessage('hg histedit --continue', 'hg histedit --abort')
591 591
592 592 def _unshelvemsg():
593 593 return _helpmessage('hg unshelve --continue', 'hg unshelve --abort')
594 594
595 595 def _updatecleanmsg(dest=None):
596 596 warning = _('warning: this will discard uncommitted changes')
597 597 return 'hg update --clean %s (%s)' % (dest or '.', warning)
598 598
599 599 def _graftmsg():
600 600 # tweakdefaults requires `update` to have a rev hence the `.`
601 601 return _helpmessage('hg graft --continue', _updatecleanmsg())
602 602
603 603 def _mergemsg():
604 604 # tweakdefaults requires `update` to have a rev hence the `.`
605 605 return _helpmessage('hg commit', _updatecleanmsg())
606 606
607 607 def _bisectmsg():
608 608 msg = _('To mark the changeset good: hg bisect --good\n'
609 609 'To mark the changeset bad: hg bisect --bad\n'
610 610 'To abort: hg bisect --reset\n')
611 611 return _commentlines(msg)
612 612
613 613 def fileexistspredicate(filename):
614 614 return lambda repo: repo.vfs.exists(filename)
615 615
616 616 def _mergepredicate(repo):
617 617 return len(repo[None].parents()) > 1
618 618
619 619 STATES = (
620 620 # (state, predicate to detect states, helpful message function)
621 621 ('histedit', fileexistspredicate('histedit-state'), _histeditmsg),
622 622 ('bisect', fileexistspredicate('bisect.state'), _bisectmsg),
623 623 ('graft', fileexistspredicate('graftstate'), _graftmsg),
624 624 ('unshelve', fileexistspredicate('unshelverebasestate'), _unshelvemsg),
625 625 ('rebase', fileexistspredicate('rebasestate'), _rebasemsg),
626 626 # The merge state is part of a list that will be iterated over.
627 627 # They need to be last because some of the other unfinished states may also
628 628 # be in a merge or update state (eg. rebase, histedit, graft, etc).
629 629 # We want those to have priority.
630 630 ('merge', _mergepredicate, _mergemsg),
631 631 )
632 632
633 633 def _getrepostate(repo):
634 634 # experimental config: commands.status.skipstates
635 635 skip = set(repo.ui.configlist('commands', 'status.skipstates'))
636 636 for state, statedetectionpredicate, msgfn in STATES:
637 637 if state in skip:
638 638 continue
639 639 if statedetectionpredicate(repo):
640 640 return (state, statedetectionpredicate, msgfn)
641 641
642 642 def morestatus(repo, fm):
643 643 statetuple = _getrepostate(repo)
644 644 label = 'status.morestatus'
645 645 if statetuple:
646 646 fm.startitem()
647 647 state, statedetectionpredicate, helpfulmsg = statetuple
648 648 statemsg = _('The repository is in an unfinished *%s* state.') % state
649 649 fm.write('statemsg', '%s\n', _commentlines(statemsg), label=label)
650 650 conmsg = _conflictsmsg(repo)
651 651 if conmsg:
652 652 fm.write('conflictsmsg', '%s\n', conmsg, label=label)
653 653 if helpfulmsg:
654 654 helpmsg = helpfulmsg()
655 655 fm.write('helpmsg', '%s\n', helpmsg, label=label)
656 656
657 657 def findpossible(cmd, table, strict=False):
658 658 """
659 659 Return cmd -> (aliases, command table entry)
660 660 for each matching command.
661 661 Return debug commands (or their aliases) only if no normal command matches.
662 662 """
663 663 choice = {}
664 664 debugchoice = {}
665 665
666 666 if cmd in table:
667 667 # short-circuit exact matches, "log" alias beats "^log|history"
668 668 keys = [cmd]
669 669 else:
670 670 keys = table.keys()
671 671
672 672 allcmds = []
673 673 for e in keys:
674 674 aliases = parsealiases(e)
675 675 allcmds.extend(aliases)
676 676 found = None
677 677 if cmd in aliases:
678 678 found = cmd
679 679 elif not strict:
680 680 for a in aliases:
681 681 if a.startswith(cmd):
682 682 found = a
683 683 break
684 684 if found is not None:
685 685 if aliases[0].startswith("debug") or found.startswith("debug"):
686 686 debugchoice[found] = (aliases, table[e])
687 687 else:
688 688 choice[found] = (aliases, table[e])
689 689
690 690 if not choice and debugchoice:
691 691 choice = debugchoice
692 692
693 693 return choice, allcmds
694 694
695 695 def findcmd(cmd, table, strict=True):
696 696 """Return (aliases, command table entry) for command string."""
697 697 choice, allcmds = findpossible(cmd, table, strict)
698 698
699 699 if cmd in choice:
700 700 return choice[cmd]
701 701
702 702 if len(choice) > 1:
703 703 clist = sorted(choice)
704 704 raise error.AmbiguousCommand(cmd, clist)
705 705
706 706 if choice:
707 707 return list(choice.values())[0]
708 708
709 709 raise error.UnknownCommand(cmd, allcmds)
710 710
711 711 def changebranch(ui, repo, revs, label):
712 712 """ Change the branch name of given revs to label """
713 713
714 714 with repo.wlock(), repo.lock(), repo.transaction('branches'):
715 715 # abort in case of uncommitted merge or dirty wdir
716 716 bailifchanged(repo)
717 717 revs = scmutil.revrange(repo, revs)
718 718 if not revs:
719 719 raise error.Abort("empty revision set")
720 720 roots = repo.revs('roots(%ld)', revs)
721 721 if len(roots) > 1:
722 722 raise error.Abort(_("cannot change branch of non-linear revisions"))
723 723 rewriteutil.precheck(repo, revs, 'change branch of')
724 724
725 725 root = repo[roots.first()]
726 726 if not root.p1().branch() == label and label in repo.branchmap():
727 727 raise error.Abort(_("a branch of the same name already exists"))
728 728
729 729 if repo.revs('merge() and %ld', revs):
730 730 raise error.Abort(_("cannot change branch of a merge commit"))
731 731 if repo.revs('obsolete() and %ld', revs):
732 732 raise error.Abort(_("cannot change branch of a obsolete changeset"))
733 733
734 734 # make sure only topological heads
735 735 if repo.revs('heads(%ld) - head()', revs):
736 736 raise error.Abort(_("cannot change branch in middle of a stack"))
737 737
738 738 replacements = {}
739 739 # avoid import cycle mercurial.cmdutil -> mercurial.context ->
740 740 # mercurial.subrepo -> mercurial.cmdutil
741 741 from . import context
742 742 for rev in revs:
743 743 ctx = repo[rev]
744 744 oldbranch = ctx.branch()
745 745 # check if ctx has same branch
746 746 if oldbranch == label:
747 747 continue
748 748
749 749 def filectxfn(repo, newctx, path):
750 750 try:
751 751 return ctx[path]
752 752 except error.ManifestLookupError:
753 753 return None
754 754
755 755 ui.debug("changing branch of '%s' from '%s' to '%s'\n"
756 756 % (hex(ctx.node()), oldbranch, label))
757 757 extra = ctx.extra()
758 758 extra['branch_change'] = hex(ctx.node())
759 759 # While changing branch of set of linear commits, make sure that
760 760 # we base our commits on new parent rather than old parent which
761 761 # was obsoleted while changing the branch
762 762 p1 = ctx.p1().node()
763 763 p2 = ctx.p2().node()
764 764 if p1 in replacements:
765 765 p1 = replacements[p1][0]
766 766 if p2 in replacements:
767 767 p2 = replacements[p2][0]
768 768
769 769 mc = context.memctx(repo, (p1, p2),
770 770 ctx.description(),
771 771 ctx.files(),
772 772 filectxfn,
773 773 user=ctx.user(),
774 774 date=ctx.date(),
775 775 extra=extra,
776 776 branch=label)
777 777
778 778 commitphase = ctx.phase()
779 779 overrides = {('phases', 'new-commit'): commitphase}
780 780 with repo.ui.configoverride(overrides, 'branch-change'):
781 781 newnode = repo.commitctx(mc)
782 782
783 783 replacements[ctx.node()] = (newnode,)
784 784 ui.debug('new node id is %s\n' % hex(newnode))
785 785
786 786 # create obsmarkers and move bookmarks
787 787 scmutil.cleanupnodes(repo, replacements, 'branch-change')
788 788
789 789 # move the working copy too
790 790 wctx = repo[None]
791 791 # in-progress merge is a bit too complex for now.
792 792 if len(wctx.parents()) == 1:
793 793 newid = replacements.get(wctx.p1().node())
794 794 if newid is not None:
795 795 # avoid import cycle mercurial.cmdutil -> mercurial.hg ->
796 796 # mercurial.cmdutil
797 797 from . import hg
798 798 hg.update(repo, newid[0], quietempty=True)
799 799
800 800 ui.status(_("changed branch on %d changesets\n") % len(replacements))
801 801
802 802 def findrepo(p):
803 803 while not os.path.isdir(os.path.join(p, ".hg")):
804 804 oldp, p = p, os.path.dirname(p)
805 805 if p == oldp:
806 806 return None
807 807
808 808 return p
809 809
810 810 def bailifchanged(repo, merge=True, hint=None):
811 811 """ enforce the precondition that working directory must be clean.
812 812
813 813 'merge' can be set to false if a pending uncommitted merge should be
814 814 ignored (such as when 'update --check' runs).
815 815
816 816 'hint' is the usual hint given to Abort exception.
817 817 """
818 818
819 819 if merge and repo.dirstate.p2() != nullid:
820 820 raise error.Abort(_('outstanding uncommitted merge'), hint=hint)
821 821 modified, added, removed, deleted = repo.status()[:4]
822 822 if modified or added or removed or deleted:
823 823 raise error.Abort(_('uncommitted changes'), hint=hint)
824 824 ctx = repo[None]
825 825 for s in sorted(ctx.substate):
826 826 ctx.sub(s).bailifchanged(hint=hint)
827 827
828 828 def logmessage(ui, opts):
829 829 """ get the log message according to -m and -l option """
830 830 message = opts.get('message')
831 831 logfile = opts.get('logfile')
832 832
833 833 if message and logfile:
834 834 raise error.Abort(_('options --message and --logfile are mutually '
835 835 'exclusive'))
836 836 if not message and logfile:
837 837 try:
838 838 if isstdiofilename(logfile):
839 839 message = ui.fin.read()
840 840 else:
841 841 message = '\n'.join(util.readfile(logfile).splitlines())
842 842 except IOError as inst:
843 843 raise error.Abort(_("can't read commit message '%s': %s") %
844 844 (logfile, encoding.strtolocal(inst.strerror)))
845 845 return message
846 846
847 847 def mergeeditform(ctxorbool, baseformname):
848 848 """return appropriate editform name (referencing a committemplate)
849 849
850 850 'ctxorbool' is either a ctx to be committed, or a bool indicating whether
851 851 merging is committed.
852 852
853 853 This returns baseformname with '.merge' appended if it is a merge,
854 854 otherwise '.normal' is appended.
855 855 """
856 856 if isinstance(ctxorbool, bool):
857 857 if ctxorbool:
858 858 return baseformname + ".merge"
859 859 elif 1 < len(ctxorbool.parents()):
860 860 return baseformname + ".merge"
861 861
862 862 return baseformname + ".normal"
863 863
864 864 def getcommiteditor(edit=False, finishdesc=None, extramsg=None,
865 865 editform='', **opts):
866 866 """get appropriate commit message editor according to '--edit' option
867 867
868 868 'finishdesc' is a function to be called with edited commit message
869 869 (= 'description' of the new changeset) just after editing, but
870 870 before checking empty-ness. It should return actual text to be
871 871 stored into history. This allows to change description before
872 872 storing.
873 873
874 874 'extramsg' is a extra message to be shown in the editor instead of
875 875 'Leave message empty to abort commit' line. 'HG: ' prefix and EOL
876 876 is automatically added.
877 877
878 878 'editform' is a dot-separated list of names, to distinguish
879 879 the purpose of commit text editing.
880 880
881 881 'getcommiteditor' returns 'commitforceeditor' regardless of
882 882 'edit', if one of 'finishdesc' or 'extramsg' is specified, because
883 883 they are specific for usage in MQ.
884 884 """
885 885 if edit or finishdesc or extramsg:
886 886 return lambda r, c, s: commitforceeditor(r, c, s,
887 887 finishdesc=finishdesc,
888 888 extramsg=extramsg,
889 889 editform=editform)
890 890 elif editform:
891 891 return lambda r, c, s: commiteditor(r, c, s, editform=editform)
892 892 else:
893 893 return commiteditor
894 894
895 895 def makefilename(repo, pat, node, desc=None,
896 896 total=None, seqno=None, revwidth=None, pathname=None):
897 897 node_expander = {
898 898 'H': lambda: hex(node),
899 899 'R': lambda: '%d' % repo.changelog.rev(node),
900 900 'h': lambda: short(node),
901 901 'm': lambda: re.sub('[^\w]', '_', desc or '')
902 902 }
903 903 expander = {
904 904 '%': lambda: '%',
905 905 'b': lambda: os.path.basename(repo.root),
906 906 }
907 907
908 908 try:
909 909 if node:
910 910 expander.update(node_expander)
911 911 if node:
912 912 expander['r'] = (lambda:
913 913 ('%d' % repo.changelog.rev(node)).zfill(revwidth or 0))
914 914 if total is not None:
915 915 expander['N'] = lambda: '%d' % total
916 916 if seqno is not None:
917 917 expander['n'] = lambda: '%d' % seqno
918 918 if total is not None and seqno is not None:
919 919 expander['n'] = (lambda: ('%d' % seqno).zfill(len('%d' % total)))
920 920 if pathname is not None:
921 921 expander['s'] = lambda: os.path.basename(pathname)
922 922 expander['d'] = lambda: os.path.dirname(pathname) or '.'
923 923 expander['p'] = lambda: pathname
924 924
925 925 newname = []
926 926 patlen = len(pat)
927 927 i = 0
928 928 while i < patlen:
929 929 c = pat[i:i + 1]
930 930 if c == '%':
931 931 i += 1
932 932 c = pat[i:i + 1]
933 933 c = expander[c]()
934 934 newname.append(c)
935 935 i += 1
936 936 return ''.join(newname)
937 937 except KeyError as inst:
938 938 raise error.Abort(_("invalid format spec '%%%s' in output filename") %
939 939 inst.args[0])
940 940
941 941 def isstdiofilename(pat):
942 942 """True if the given pat looks like a filename denoting stdin/stdout"""
943 943 return not pat or pat == '-'
944 944
945 945 class _unclosablefile(object):
946 946 def __init__(self, fp):
947 947 self._fp = fp
948 948
949 949 def close(self):
950 950 pass
951 951
952 952 def __iter__(self):
953 953 return iter(self._fp)
954 954
955 955 def __getattr__(self, attr):
956 956 return getattr(self._fp, attr)
957 957
958 958 def __enter__(self):
959 959 return self
960 960
961 961 def __exit__(self, exc_type, exc_value, exc_tb):
962 962 pass
963 963
964 964 def makefileobj(repo, pat, node=None, desc=None, total=None,
965 965 seqno=None, revwidth=None, mode='wb', modemap=None,
966 966 pathname=None):
967 967
968 968 writable = mode not in ('r', 'rb')
969 969
970 970 if isstdiofilename(pat):
971 971 if writable:
972 972 fp = repo.ui.fout
973 973 else:
974 974 fp = repo.ui.fin
975 975 return _unclosablefile(fp)
976 976 fn = makefilename(repo, pat, node, desc, total, seqno, revwidth, pathname)
977 977 if modemap is not None:
978 978 mode = modemap.get(fn, mode)
979 979 if mode == 'wb':
980 980 modemap[fn] = 'ab'
981 981 return open(fn, mode)
982 982
983 983 def openrevlog(repo, cmd, file_, opts):
984 984 """opens the changelog, manifest, a filelog or a given revlog"""
985 985 cl = opts['changelog']
986 986 mf = opts['manifest']
987 987 dir = opts['dir']
988 988 msg = None
989 989 if cl and mf:
990 990 msg = _('cannot specify --changelog and --manifest at the same time')
991 991 elif cl and dir:
992 992 msg = _('cannot specify --changelog and --dir at the same time')
993 993 elif cl or mf or dir:
994 994 if file_:
995 995 msg = _('cannot specify filename with --changelog or --manifest')
996 996 elif not repo:
997 997 msg = _('cannot specify --changelog or --manifest or --dir '
998 998 'without a repository')
999 999 if msg:
1000 1000 raise error.Abort(msg)
1001 1001
1002 1002 r = None
1003 1003 if repo:
1004 1004 if cl:
1005 1005 r = repo.unfiltered().changelog
1006 1006 elif dir:
1007 1007 if 'treemanifest' not in repo.requirements:
1008 1008 raise error.Abort(_("--dir can only be used on repos with "
1009 1009 "treemanifest enabled"))
1010 1010 dirlog = repo.manifestlog._revlog.dirlog(dir)
1011 1011 if len(dirlog):
1012 1012 r = dirlog
1013 1013 elif mf:
1014 1014 r = repo.manifestlog._revlog
1015 1015 elif file_:
1016 1016 filelog = repo.file(file_)
1017 1017 if len(filelog):
1018 1018 r = filelog
1019 1019 if not r:
1020 1020 if not file_:
1021 1021 raise error.CommandError(cmd, _('invalid arguments'))
1022 1022 if not os.path.isfile(file_):
1023 1023 raise error.Abort(_("revlog '%s' not found") % file_)
1024 1024 r = revlog.revlog(vfsmod.vfs(pycompat.getcwd(), audit=False),
1025 1025 file_[:-2] + ".i")
1026 1026 return r
1027 1027
1028 1028 def copy(ui, repo, pats, opts, rename=False):
1029 1029 # called with the repo lock held
1030 1030 #
1031 1031 # hgsep => pathname that uses "/" to separate directories
1032 1032 # ossep => pathname that uses os.sep to separate directories
1033 1033 cwd = repo.getcwd()
1034 1034 targets = {}
1035 1035 after = opts.get("after")
1036 1036 dryrun = opts.get("dry_run")
1037 1037 wctx = repo[None]
1038 1038
1039 1039 def walkpat(pat):
1040 1040 srcs = []
1041 1041 if after:
1042 1042 badstates = '?'
1043 1043 else:
1044 1044 badstates = '?r'
1045 1045 m = scmutil.match(wctx, [pat], opts, globbed=True)
1046 1046 for abs in wctx.walk(m):
1047 1047 state = repo.dirstate[abs]
1048 1048 rel = m.rel(abs)
1049 1049 exact = m.exact(abs)
1050 1050 if state in badstates:
1051 1051 if exact and state == '?':
1052 1052 ui.warn(_('%s: not copying - file is not managed\n') % rel)
1053 1053 if exact and state == 'r':
1054 1054 ui.warn(_('%s: not copying - file has been marked for'
1055 1055 ' remove\n') % rel)
1056 1056 continue
1057 1057 # abs: hgsep
1058 1058 # rel: ossep
1059 1059 srcs.append((abs, rel, exact))
1060 1060 return srcs
1061 1061
1062 1062 # abssrc: hgsep
1063 1063 # relsrc: ossep
1064 1064 # otarget: ossep
1065 1065 def copyfile(abssrc, relsrc, otarget, exact):
1066 1066 abstarget = pathutil.canonpath(repo.root, cwd, otarget)
1067 1067 if '/' in abstarget:
1068 1068 # We cannot normalize abstarget itself, this would prevent
1069 1069 # case only renames, like a => A.
1070 1070 abspath, absname = abstarget.rsplit('/', 1)
1071 1071 abstarget = repo.dirstate.normalize(abspath) + '/' + absname
1072 1072 reltarget = repo.pathto(abstarget, cwd)
1073 1073 target = repo.wjoin(abstarget)
1074 1074 src = repo.wjoin(abssrc)
1075 1075 state = repo.dirstate[abstarget]
1076 1076
1077 1077 scmutil.checkportable(ui, abstarget)
1078 1078
1079 1079 # check for collisions
1080 1080 prevsrc = targets.get(abstarget)
1081 1081 if prevsrc is not None:
1082 1082 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
1083 1083 (reltarget, repo.pathto(abssrc, cwd),
1084 1084 repo.pathto(prevsrc, cwd)))
1085 1085 return
1086 1086
1087 1087 # check for overwrites
1088 1088 exists = os.path.lexists(target)
1089 1089 samefile = False
1090 1090 if exists and abssrc != abstarget:
1091 1091 if (repo.dirstate.normalize(abssrc) ==
1092 1092 repo.dirstate.normalize(abstarget)):
1093 1093 if not rename:
1094 1094 ui.warn(_("%s: can't copy - same file\n") % reltarget)
1095 1095 return
1096 1096 exists = False
1097 1097 samefile = True
1098 1098
1099 1099 if not after and exists or after and state in 'mn':
1100 1100 if not opts['force']:
1101 1101 if state in 'mn':
1102 1102 msg = _('%s: not overwriting - file already committed\n')
1103 1103 if after:
1104 1104 flags = '--after --force'
1105 1105 else:
1106 1106 flags = '--force'
1107 1107 if rename:
1108 1108 hint = _('(hg rename %s to replace the file by '
1109 1109 'recording a rename)\n') % flags
1110 1110 else:
1111 1111 hint = _('(hg copy %s to replace the file by '
1112 1112 'recording a copy)\n') % flags
1113 1113 else:
1114 1114 msg = _('%s: not overwriting - file exists\n')
1115 1115 if rename:
1116 1116 hint = _('(hg rename --after to record the rename)\n')
1117 1117 else:
1118 1118 hint = _('(hg copy --after to record the copy)\n')
1119 1119 ui.warn(msg % reltarget)
1120 1120 ui.warn(hint)
1121 1121 return
1122 1122
1123 1123 if after:
1124 1124 if not exists:
1125 1125 if rename:
1126 1126 ui.warn(_('%s: not recording move - %s does not exist\n') %
1127 1127 (relsrc, reltarget))
1128 1128 else:
1129 1129 ui.warn(_('%s: not recording copy - %s does not exist\n') %
1130 1130 (relsrc, reltarget))
1131 1131 return
1132 1132 elif not dryrun:
1133 1133 try:
1134 1134 if exists:
1135 1135 os.unlink(target)
1136 1136 targetdir = os.path.dirname(target) or '.'
1137 1137 if not os.path.isdir(targetdir):
1138 1138 os.makedirs(targetdir)
1139 1139 if samefile:
1140 1140 tmp = target + "~hgrename"
1141 1141 os.rename(src, tmp)
1142 1142 os.rename(tmp, target)
1143 1143 else:
1144 1144 util.copyfile(src, target)
1145 1145 srcexists = True
1146 1146 except IOError as inst:
1147 1147 if inst.errno == errno.ENOENT:
1148 1148 ui.warn(_('%s: deleted in working directory\n') % relsrc)
1149 1149 srcexists = False
1150 1150 else:
1151 1151 ui.warn(_('%s: cannot copy - %s\n') %
1152 1152 (relsrc, encoding.strtolocal(inst.strerror)))
1153 1153 return True # report a failure
1154 1154
1155 1155 if ui.verbose or not exact:
1156 1156 if rename:
1157 1157 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
1158 1158 else:
1159 1159 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
1160 1160
1161 1161 targets[abstarget] = abssrc
1162 1162
1163 1163 # fix up dirstate
1164 1164 scmutil.dirstatecopy(ui, repo, wctx, abssrc, abstarget,
1165 1165 dryrun=dryrun, cwd=cwd)
1166 1166 if rename and not dryrun:
1167 1167 if not after and srcexists and not samefile:
1168 1168 repo.wvfs.unlinkpath(abssrc)
1169 1169 wctx.forget([abssrc])
1170 1170
1171 1171 # pat: ossep
1172 1172 # dest ossep
1173 1173 # srcs: list of (hgsep, hgsep, ossep, bool)
1174 1174 # return: function that takes hgsep and returns ossep
1175 1175 def targetpathfn(pat, dest, srcs):
1176 1176 if os.path.isdir(pat):
1177 1177 abspfx = pathutil.canonpath(repo.root, cwd, pat)
1178 1178 abspfx = util.localpath(abspfx)
1179 1179 if destdirexists:
1180 1180 striplen = len(os.path.split(abspfx)[0])
1181 1181 else:
1182 1182 striplen = len(abspfx)
1183 1183 if striplen:
1184 1184 striplen += len(pycompat.ossep)
1185 1185 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
1186 1186 elif destdirexists:
1187 1187 res = lambda p: os.path.join(dest,
1188 1188 os.path.basename(util.localpath(p)))
1189 1189 else:
1190 1190 res = lambda p: dest
1191 1191 return res
1192 1192
1193 1193 # pat: ossep
1194 1194 # dest ossep
1195 1195 # srcs: list of (hgsep, hgsep, ossep, bool)
1196 1196 # return: function that takes hgsep and returns ossep
1197 1197 def targetpathafterfn(pat, dest, srcs):
1198 1198 if matchmod.patkind(pat):
1199 1199 # a mercurial pattern
1200 1200 res = lambda p: os.path.join(dest,
1201 1201 os.path.basename(util.localpath(p)))
1202 1202 else:
1203 1203 abspfx = pathutil.canonpath(repo.root, cwd, pat)
1204 1204 if len(abspfx) < len(srcs[0][0]):
1205 1205 # A directory. Either the target path contains the last
1206 1206 # component of the source path or it does not.
1207 1207 def evalpath(striplen):
1208 1208 score = 0
1209 1209 for s in srcs:
1210 1210 t = os.path.join(dest, util.localpath(s[0])[striplen:])
1211 1211 if os.path.lexists(t):
1212 1212 score += 1
1213 1213 return score
1214 1214
1215 1215 abspfx = util.localpath(abspfx)
1216 1216 striplen = len(abspfx)
1217 1217 if striplen:
1218 1218 striplen += len(pycompat.ossep)
1219 1219 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
1220 1220 score = evalpath(striplen)
1221 1221 striplen1 = len(os.path.split(abspfx)[0])
1222 1222 if striplen1:
1223 1223 striplen1 += len(pycompat.ossep)
1224 1224 if evalpath(striplen1) > score:
1225 1225 striplen = striplen1
1226 1226 res = lambda p: os.path.join(dest,
1227 1227 util.localpath(p)[striplen:])
1228 1228 else:
1229 1229 # a file
1230 1230 if destdirexists:
1231 1231 res = lambda p: os.path.join(dest,
1232 1232 os.path.basename(util.localpath(p)))
1233 1233 else:
1234 1234 res = lambda p: dest
1235 1235 return res
1236 1236
1237 1237 pats = scmutil.expandpats(pats)
1238 1238 if not pats:
1239 1239 raise error.Abort(_('no source or destination specified'))
1240 1240 if len(pats) == 1:
1241 1241 raise error.Abort(_('no destination specified'))
1242 1242 dest = pats.pop()
1243 1243 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
1244 1244 if not destdirexists:
1245 1245 if len(pats) > 1 or matchmod.patkind(pats[0]):
1246 1246 raise error.Abort(_('with multiple sources, destination must be an '
1247 1247 'existing directory'))
1248 1248 if util.endswithsep(dest):
1249 1249 raise error.Abort(_('destination %s is not a directory') % dest)
1250 1250
1251 1251 tfn = targetpathfn
1252 1252 if after:
1253 1253 tfn = targetpathafterfn
1254 1254 copylist = []
1255 1255 for pat in pats:
1256 1256 srcs = walkpat(pat)
1257 1257 if not srcs:
1258 1258 continue
1259 1259 copylist.append((tfn(pat, dest, srcs), srcs))
1260 1260 if not copylist:
1261 1261 raise error.Abort(_('no files to copy'))
1262 1262
1263 1263 errors = 0
1264 1264 for targetpath, srcs in copylist:
1265 1265 for abssrc, relsrc, exact in srcs:
1266 1266 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
1267 1267 errors += 1
1268 1268
1269 1269 if errors:
1270 1270 ui.warn(_('(consider using --after)\n'))
1271 1271
1272 1272 return errors != 0
1273 1273
1274 1274 ## facility to let extension process additional data into an import patch
1275 1275 # list of identifier to be executed in order
1276 1276 extrapreimport = [] # run before commit
1277 1277 extrapostimport = [] # run after commit
1278 1278 # mapping from identifier to actual import function
1279 1279 #
1280 1280 # 'preimport' are run before the commit is made and are provided the following
1281 1281 # arguments:
1282 1282 # - repo: the localrepository instance,
1283 1283 # - patchdata: data extracted from patch header (cf m.patch.patchheadermap),
1284 1284 # - extra: the future extra dictionary of the changeset, please mutate it,
1285 1285 # - opts: the import options.
1286 1286 # XXX ideally, we would just pass an ctx ready to be computed, that would allow
1287 1287 # mutation of in memory commit and more. Feel free to rework the code to get
1288 1288 # there.
1289 1289 extrapreimportmap = {}
1290 1290 # 'postimport' are run after the commit is made and are provided the following
1291 1291 # argument:
1292 1292 # - ctx: the changectx created by import.
1293 1293 extrapostimportmap = {}
1294 1294
1295 1295 def tryimportone(ui, repo, hunk, parents, opts, msgs, updatefunc):
1296 1296 """Utility function used by commands.import to import a single patch
1297 1297
1298 1298 This function is explicitly defined here to help the evolve extension to
1299 1299 wrap this part of the import logic.
1300 1300
1301 1301 The API is currently a bit ugly because it a simple code translation from
1302 1302 the import command. Feel free to make it better.
1303 1303
1304 1304 :hunk: a patch (as a binary string)
1305 1305 :parents: nodes that will be parent of the created commit
1306 1306 :opts: the full dict of option passed to the import command
1307 1307 :msgs: list to save commit message to.
1308 1308 (used in case we need to save it when failing)
1309 1309 :updatefunc: a function that update a repo to a given node
1310 1310 updatefunc(<repo>, <node>)
1311 1311 """
1312 1312 # avoid cycle context -> subrepo -> cmdutil
1313 1313 from . import context
1314 1314 extractdata = patch.extract(ui, hunk)
1315 1315 tmpname = extractdata.get('filename')
1316 1316 message = extractdata.get('message')
1317 1317 user = opts.get('user') or extractdata.get('user')
1318 1318 date = opts.get('date') or extractdata.get('date')
1319 1319 branch = extractdata.get('branch')
1320 1320 nodeid = extractdata.get('nodeid')
1321 1321 p1 = extractdata.get('p1')
1322 1322 p2 = extractdata.get('p2')
1323 1323
1324 1324 nocommit = opts.get('no_commit')
1325 1325 importbranch = opts.get('import_branch')
1326 1326 update = not opts.get('bypass')
1327 1327 strip = opts["strip"]
1328 1328 prefix = opts["prefix"]
1329 1329 sim = float(opts.get('similarity') or 0)
1330 1330 if not tmpname:
1331 1331 return (None, None, False)
1332 1332
1333 1333 rejects = False
1334 1334
1335 1335 try:
1336 1336 cmdline_message = logmessage(ui, opts)
1337 1337 if cmdline_message:
1338 1338 # pickup the cmdline msg
1339 1339 message = cmdline_message
1340 1340 elif message:
1341 1341 # pickup the patch msg
1342 1342 message = message.strip()
1343 1343 else:
1344 1344 # launch the editor
1345 1345 message = None
1346 1346 ui.debug('message:\n%s\n' % message)
1347 1347
1348 1348 if len(parents) == 1:
1349 1349 parents.append(repo[nullid])
1350 1350 if opts.get('exact'):
1351 1351 if not nodeid or not p1:
1352 1352 raise error.Abort(_('not a Mercurial patch'))
1353 1353 p1 = repo[p1]
1354 1354 p2 = repo[p2 or nullid]
1355 1355 elif p2:
1356 1356 try:
1357 1357 p1 = repo[p1]
1358 1358 p2 = repo[p2]
1359 1359 # Without any options, consider p2 only if the
1360 1360 # patch is being applied on top of the recorded
1361 1361 # first parent.
1362 1362 if p1 != parents[0]:
1363 1363 p1 = parents[0]
1364 1364 p2 = repo[nullid]
1365 1365 except error.RepoError:
1366 1366 p1, p2 = parents
1367 1367 if p2.node() == nullid:
1368 1368 ui.warn(_("warning: import the patch as a normal revision\n"
1369 1369 "(use --exact to import the patch as a merge)\n"))
1370 1370 else:
1371 1371 p1, p2 = parents
1372 1372
1373 1373 n = None
1374 1374 if update:
1375 1375 if p1 != parents[0]:
1376 1376 updatefunc(repo, p1.node())
1377 1377 if p2 != parents[1]:
1378 1378 repo.setparents(p1.node(), p2.node())
1379 1379
1380 1380 if opts.get('exact') or importbranch:
1381 1381 repo.dirstate.setbranch(branch or 'default')
1382 1382
1383 1383 partial = opts.get('partial', False)
1384 1384 files = set()
1385 1385 try:
1386 1386 patch.patch(ui, repo, tmpname, strip=strip, prefix=prefix,
1387 1387 files=files, eolmode=None, similarity=sim / 100.0)
1388 1388 except error.PatchError as e:
1389 1389 if not partial:
1390 1390 raise error.Abort(str(e))
1391 1391 if partial:
1392 1392 rejects = True
1393 1393
1394 1394 files = list(files)
1395 1395 if nocommit:
1396 1396 if message:
1397 1397 msgs.append(message)
1398 1398 else:
1399 1399 if opts.get('exact') or p2:
1400 1400 # If you got here, you either use --force and know what
1401 1401 # you are doing or used --exact or a merge patch while
1402 1402 # being updated to its first parent.
1403 1403 m = None
1404 1404 else:
1405 1405 m = scmutil.matchfiles(repo, files or [])
1406 1406 editform = mergeeditform(repo[None], 'import.normal')
1407 1407 if opts.get('exact'):
1408 1408 editor = None
1409 1409 else:
1410 1410 editor = getcommiteditor(editform=editform,
1411 1411 **pycompat.strkwargs(opts))
1412 1412 extra = {}
1413 1413 for idfunc in extrapreimport:
1414 1414 extrapreimportmap[idfunc](repo, extractdata, extra, opts)
1415 1415 overrides = {}
1416 1416 if partial:
1417 1417 overrides[('ui', 'allowemptycommit')] = True
1418 1418 with repo.ui.configoverride(overrides, 'import'):
1419 1419 n = repo.commit(message, user,
1420 1420 date, match=m,
1421 1421 editor=editor, extra=extra)
1422 1422 for idfunc in extrapostimport:
1423 1423 extrapostimportmap[idfunc](repo[n])
1424 1424 else:
1425 1425 if opts.get('exact') or importbranch:
1426 1426 branch = branch or 'default'
1427 1427 else:
1428 1428 branch = p1.branch()
1429 1429 store = patch.filestore()
1430 1430 try:
1431 1431 files = set()
1432 1432 try:
1433 1433 patch.patchrepo(ui, repo, p1, store, tmpname, strip, prefix,
1434 1434 files, eolmode=None)
1435 1435 except error.PatchError as e:
1436 1436 raise error.Abort(str(e))
1437 1437 if opts.get('exact'):
1438 1438 editor = None
1439 1439 else:
1440 1440 editor = getcommiteditor(editform='import.bypass')
1441 1441 memctx = context.memctx(repo, (p1.node(), p2.node()),
1442 1442 message,
1443 1443 files=files,
1444 1444 filectxfn=store,
1445 1445 user=user,
1446 1446 date=date,
1447 1447 branch=branch,
1448 1448 editor=editor)
1449 1449 n = memctx.commit()
1450 1450 finally:
1451 1451 store.close()
1452 1452 if opts.get('exact') and nocommit:
1453 1453 # --exact with --no-commit is still useful in that it does merge
1454 1454 # and branch bits
1455 1455 ui.warn(_("warning: can't check exact import with --no-commit\n"))
1456 1456 elif opts.get('exact') and hex(n) != nodeid:
1457 1457 raise error.Abort(_('patch is damaged or loses information'))
1458 1458 msg = _('applied to working directory')
1459 1459 if n:
1460 1460 # i18n: refers to a short changeset id
1461 1461 msg = _('created %s') % short(n)
1462 1462 return (msg, n, rejects)
1463 1463 finally:
1464 1464 os.unlink(tmpname)
1465 1465
1466 1466 # facility to let extensions include additional data in an exported patch
1467 1467 # list of identifiers to be executed in order
1468 1468 extraexport = []
1469 1469 # mapping from identifier to actual export function
1470 1470 # function as to return a string to be added to the header or None
1471 1471 # it is given two arguments (sequencenumber, changectx)
1472 1472 extraexportmap = {}
1473 1473
1474 1474 def _exportsingle(repo, ctx, match, switch_parent, rev, seqno, write, diffopts):
1475 1475 node = scmutil.binnode(ctx)
1476 1476 parents = [p.node() for p in ctx.parents() if p]
1477 1477 branch = ctx.branch()
1478 1478 if switch_parent:
1479 1479 parents.reverse()
1480 1480
1481 1481 if parents:
1482 1482 prev = parents[0]
1483 1483 else:
1484 1484 prev = nullid
1485 1485
1486 1486 write("# HG changeset patch\n")
1487 1487 write("# User %s\n" % ctx.user())
1488 1488 write("# Date %d %d\n" % ctx.date())
1489 1489 write("# %s\n" % util.datestr(ctx.date()))
1490 1490 if branch and branch != 'default':
1491 1491 write("# Branch %s\n" % branch)
1492 1492 write("# Node ID %s\n" % hex(node))
1493 1493 write("# Parent %s\n" % hex(prev))
1494 1494 if len(parents) > 1:
1495 1495 write("# Parent %s\n" % hex(parents[1]))
1496 1496
1497 1497 for headerid in extraexport:
1498 1498 header = extraexportmap[headerid](seqno, ctx)
1499 1499 if header is not None:
1500 1500 write('# %s\n' % header)
1501 1501 write(ctx.description().rstrip())
1502 1502 write("\n\n")
1503 1503
1504 1504 for chunk, label in patch.diffui(repo, prev, node, match, opts=diffopts):
1505 1505 write(chunk, label=label)
1506 1506
1507 1507 def export(repo, revs, fntemplate='hg-%h.patch', fp=None, switch_parent=False,
1508 1508 opts=None, match=None):
1509 1509 '''export changesets as hg patches
1510 1510
1511 1511 Args:
1512 1512 repo: The repository from which we're exporting revisions.
1513 1513 revs: A list of revisions to export as revision numbers.
1514 1514 fntemplate: An optional string to use for generating patch file names.
1515 1515 fp: An optional file-like object to which patches should be written.
1516 1516 switch_parent: If True, show diffs against second parent when not nullid.
1517 1517 Default is false, which always shows diff against p1.
1518 1518 opts: diff options to use for generating the patch.
1519 1519 match: If specified, only export changes to files matching this matcher.
1520 1520
1521 1521 Returns:
1522 1522 Nothing.
1523 1523
1524 1524 Side Effect:
1525 1525 "HG Changeset Patch" data is emitted to one of the following
1526 1526 destinations:
1527 1527 fp is specified: All revs are written to the specified
1528 1528 file-like object.
1529 1529 fntemplate specified: Each rev is written to a unique file named using
1530 1530 the given template.
1531 1531 Neither fp nor template specified: All revs written to repo.ui.write()
1532 1532 '''
1533 1533
1534 1534 total = len(revs)
1535 1535 revwidth = max(len(str(rev)) for rev in revs)
1536 1536 filemode = {}
1537 1537
1538 1538 write = None
1539 1539 dest = '<unnamed>'
1540 1540 if fp:
1541 1541 dest = getattr(fp, 'name', dest)
1542 1542 def write(s, **kw):
1543 1543 fp.write(s)
1544 1544 elif not fntemplate:
1545 1545 write = repo.ui.write
1546 1546
1547 1547 for seqno, rev in enumerate(revs, 1):
1548 1548 ctx = repo[rev]
1549 1549 fo = None
1550 1550 if not fp and fntemplate:
1551 1551 desc_lines = ctx.description().rstrip().split('\n')
1552 1552 desc = desc_lines[0] #Commit always has a first line.
1553 1553 fo = makefileobj(repo, fntemplate, ctx.node(), desc=desc,
1554 1554 total=total, seqno=seqno, revwidth=revwidth,
1555 1555 mode='wb', modemap=filemode)
1556 1556 dest = fo.name
1557 1557 def write(s, **kw):
1558 1558 fo.write(s)
1559 1559 if not dest.startswith('<'):
1560 1560 repo.ui.note("%s\n" % dest)
1561 1561 _exportsingle(
1562 1562 repo, ctx, match, switch_parent, rev, seqno, write, opts)
1563 1563 if fo is not None:
1564 1564 fo.close()
1565 1565
1566 1566 def showmarker(fm, marker, index=None):
1567 1567 """utility function to display obsolescence marker in a readable way
1568 1568
1569 1569 To be used by debug function."""
1570 1570 if index is not None:
1571 1571 fm.write('index', '%i ', index)
1572 1572 fm.write('prednode', '%s ', hex(marker.prednode()))
1573 1573 succs = marker.succnodes()
1574 1574 fm.condwrite(succs, 'succnodes', '%s ',
1575 1575 fm.formatlist(map(hex, succs), name='node'))
1576 1576 fm.write('flag', '%X ', marker.flags())
1577 1577 parents = marker.parentnodes()
1578 1578 if parents is not None:
1579 1579 fm.write('parentnodes', '{%s} ',
1580 1580 fm.formatlist(map(hex, parents), name='node', sep=', '))
1581 1581 fm.write('date', '(%s) ', fm.formatdate(marker.date()))
1582 1582 meta = marker.metadata().copy()
1583 1583 meta.pop('date', None)
1584 1584 smeta = util.rapply(pycompat.maybebytestr, meta)
1585 1585 fm.write('metadata', '{%s}', fm.formatdict(smeta, fmt='%r: %r', sep=', '))
1586 1586 fm.plain('\n')
1587 1587
1588 1588 def finddate(ui, repo, date):
1589 1589 """Find the tipmost changeset that matches the given date spec"""
1590 1590
1591 1591 df = util.matchdate(date)
1592 1592 m = scmutil.matchall(repo)
1593 1593 results = {}
1594 1594
1595 1595 def prep(ctx, fns):
1596 1596 d = ctx.date()
1597 1597 if df(d[0]):
1598 1598 results[ctx.rev()] = d
1599 1599
1600 1600 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
1601 1601 rev = ctx.rev()
1602 1602 if rev in results:
1603 1603 ui.status(_("found revision %s from %s\n") %
1604 1604 (rev, util.datestr(results[rev])))
1605 1605 return '%d' % rev
1606 1606
1607 1607 raise error.Abort(_("revision matching date not found"))
1608 1608
1609 1609 def increasingwindows(windowsize=8, sizelimit=512):
1610 1610 while True:
1611 1611 yield windowsize
1612 1612 if windowsize < sizelimit:
1613 1613 windowsize *= 2
1614 1614
1615 1615 def _walkrevs(repo, opts):
1616 1616 # Default --rev value depends on --follow but --follow behavior
1617 1617 # depends on revisions resolved from --rev...
1618 1618 follow = opts.get('follow') or opts.get('follow_first')
1619 1619 if opts.get('rev'):
1620 1620 revs = scmutil.revrange(repo, opts['rev'])
1621 1621 elif follow and repo.dirstate.p1() == nullid:
1622 1622 revs = smartset.baseset()
1623 1623 elif follow:
1624 1624 revs = repo.revs('reverse(:.)')
1625 1625 else:
1626 1626 revs = smartset.spanset(repo)
1627 1627 revs.reverse()
1628 1628 return revs
1629 1629
1630 1630 class FileWalkError(Exception):
1631 1631 pass
1632 1632
1633 1633 def walkfilerevs(repo, match, follow, revs, fncache):
1634 1634 '''Walks the file history for the matched files.
1635 1635
1636 1636 Returns the changeset revs that are involved in the file history.
1637 1637
1638 1638 Throws FileWalkError if the file history can't be walked using
1639 1639 filelogs alone.
1640 1640 '''
1641 1641 wanted = set()
1642 1642 copies = []
1643 1643 minrev, maxrev = min(revs), max(revs)
1644 1644 def filerevgen(filelog, last):
1645 1645 """
1646 1646 Only files, no patterns. Check the history of each file.
1647 1647
1648 1648 Examines filelog entries within minrev, maxrev linkrev range
1649 1649 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
1650 1650 tuples in backwards order
1651 1651 """
1652 1652 cl_count = len(repo)
1653 1653 revs = []
1654 1654 for j in xrange(0, last + 1):
1655 1655 linkrev = filelog.linkrev(j)
1656 1656 if linkrev < minrev:
1657 1657 continue
1658 1658 # only yield rev for which we have the changelog, it can
1659 1659 # happen while doing "hg log" during a pull or commit
1660 1660 if linkrev >= cl_count:
1661 1661 break
1662 1662
1663 1663 parentlinkrevs = []
1664 1664 for p in filelog.parentrevs(j):
1665 1665 if p != nullrev:
1666 1666 parentlinkrevs.append(filelog.linkrev(p))
1667 1667 n = filelog.node(j)
1668 1668 revs.append((linkrev, parentlinkrevs,
1669 1669 follow and filelog.renamed(n)))
1670 1670
1671 1671 return reversed(revs)
1672 1672 def iterfiles():
1673 1673 pctx = repo['.']
1674 1674 for filename in match.files():
1675 1675 if follow:
1676 1676 if filename not in pctx:
1677 1677 raise error.Abort(_('cannot follow file not in parent '
1678 1678 'revision: "%s"') % filename)
1679 1679 yield filename, pctx[filename].filenode()
1680 1680 else:
1681 1681 yield filename, None
1682 1682 for filename_node in copies:
1683 1683 yield filename_node
1684 1684
1685 1685 for file_, node in iterfiles():
1686 1686 filelog = repo.file(file_)
1687 1687 if not len(filelog):
1688 1688 if node is None:
1689 1689 # A zero count may be a directory or deleted file, so
1690 1690 # try to find matching entries on the slow path.
1691 1691 if follow:
1692 1692 raise error.Abort(
1693 1693 _('cannot follow nonexistent file: "%s"') % file_)
1694 1694 raise FileWalkError("Cannot walk via filelog")
1695 1695 else:
1696 1696 continue
1697 1697
1698 1698 if node is None:
1699 1699 last = len(filelog) - 1
1700 1700 else:
1701 1701 last = filelog.rev(node)
1702 1702
1703 1703 # keep track of all ancestors of the file
1704 1704 ancestors = {filelog.linkrev(last)}
1705 1705
1706 1706 # iterate from latest to oldest revision
1707 1707 for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
1708 1708 if not follow:
1709 1709 if rev > maxrev:
1710 1710 continue
1711 1711 else:
1712 1712 # Note that last might not be the first interesting
1713 1713 # rev to us:
1714 1714 # if the file has been changed after maxrev, we'll
1715 1715 # have linkrev(last) > maxrev, and we still need
1716 1716 # to explore the file graph
1717 1717 if rev not in ancestors:
1718 1718 continue
1719 1719 # XXX insert 1327 fix here
1720 1720 if flparentlinkrevs:
1721 1721 ancestors.update(flparentlinkrevs)
1722 1722
1723 1723 fncache.setdefault(rev, []).append(file_)
1724 1724 wanted.add(rev)
1725 1725 if copied:
1726 1726 copies.append(copied)
1727 1727
1728 1728 return wanted
1729 1729
1730 1730 class _followfilter(object):
1731 1731 def __init__(self, repo, onlyfirst=False):
1732 1732 self.repo = repo
1733 1733 self.startrev = nullrev
1734 1734 self.roots = set()
1735 1735 self.onlyfirst = onlyfirst
1736 1736
1737 1737 def match(self, rev):
1738 1738 def realparents(rev):
1739 1739 if self.onlyfirst:
1740 1740 return self.repo.changelog.parentrevs(rev)[0:1]
1741 1741 else:
1742 1742 return filter(lambda x: x != nullrev,
1743 1743 self.repo.changelog.parentrevs(rev))
1744 1744
1745 1745 if self.startrev == nullrev:
1746 1746 self.startrev = rev
1747 1747 return True
1748 1748
1749 1749 if rev > self.startrev:
1750 1750 # forward: all descendants
1751 1751 if not self.roots:
1752 1752 self.roots.add(self.startrev)
1753 1753 for parent in realparents(rev):
1754 1754 if parent in self.roots:
1755 1755 self.roots.add(rev)
1756 1756 return True
1757 1757 else:
1758 1758 # backwards: all parents
1759 1759 if not self.roots:
1760 1760 self.roots.update(realparents(self.startrev))
1761 1761 if rev in self.roots:
1762 1762 self.roots.remove(rev)
1763 1763 self.roots.update(realparents(rev))
1764 1764 return True
1765 1765
1766 1766 return False
1767 1767
1768 1768 def walkchangerevs(repo, match, opts, prepare):
1769 1769 '''Iterate over files and the revs in which they changed.
1770 1770
1771 1771 Callers most commonly need to iterate backwards over the history
1772 1772 in which they are interested. Doing so has awful (quadratic-looking)
1773 1773 performance, so we use iterators in a "windowed" way.
1774 1774
1775 1775 We walk a window of revisions in the desired order. Within the
1776 1776 window, we first walk forwards to gather data, then in the desired
1777 1777 order (usually backwards) to display it.
1778 1778
1779 1779 This function returns an iterator yielding contexts. Before
1780 1780 yielding each context, the iterator will first call the prepare
1781 1781 function on each context in the window in forward order.'''
1782 1782
1783 1783 follow = opts.get('follow') or opts.get('follow_first')
1784 1784 revs = _walkrevs(repo, opts)
1785 1785 if not revs:
1786 1786 return []
1787 1787 wanted = set()
1788 1788 slowpath = match.anypats() or (not match.always() and opts.get('removed'))
1789 1789 fncache = {}
1790 1790 change = repo.changectx
1791 1791
1792 1792 # First step is to fill wanted, the set of revisions that we want to yield.
1793 1793 # When it does not induce extra cost, we also fill fncache for revisions in
1794 1794 # wanted: a cache of filenames that were changed (ctx.files()) and that
1795 1795 # match the file filtering conditions.
1796 1796
1797 1797 if match.always():
1798 1798 # No files, no patterns. Display all revs.
1799 1799 wanted = revs
1800 1800 elif not slowpath:
1801 1801 # We only have to read through the filelog to find wanted revisions
1802 1802
1803 1803 try:
1804 1804 wanted = walkfilerevs(repo, match, follow, revs, fncache)
1805 1805 except FileWalkError:
1806 1806 slowpath = True
1807 1807
1808 1808 # We decided to fall back to the slowpath because at least one
1809 1809 # of the paths was not a file. Check to see if at least one of them
1810 1810 # existed in history, otherwise simply return
1811 1811 for path in match.files():
1812 1812 if path == '.' or path in repo.store:
1813 1813 break
1814 1814 else:
1815 1815 return []
1816 1816
1817 1817 if slowpath:
1818 1818 # We have to read the changelog to match filenames against
1819 1819 # changed files
1820 1820
1821 1821 if follow:
1822 1822 raise error.Abort(_('can only follow copies/renames for explicit '
1823 1823 'filenames'))
1824 1824
1825 1825 # The slow path checks files modified in every changeset.
1826 1826 # This is really slow on large repos, so compute the set lazily.
1827 1827 class lazywantedset(object):
1828 1828 def __init__(self):
1829 1829 self.set = set()
1830 1830 self.revs = set(revs)
1831 1831
1832 1832 # No need to worry about locality here because it will be accessed
1833 1833 # in the same order as the increasing window below.
1834 1834 def __contains__(self, value):
1835 1835 if value in self.set:
1836 1836 return True
1837 1837 elif not value in self.revs:
1838 1838 return False
1839 1839 else:
1840 1840 self.revs.discard(value)
1841 1841 ctx = change(value)
1842 1842 matches = filter(match, ctx.files())
1843 1843 if matches:
1844 1844 fncache[value] = matches
1845 1845 self.set.add(value)
1846 1846 return True
1847 1847 return False
1848 1848
1849 1849 def discard(self, value):
1850 1850 self.revs.discard(value)
1851 1851 self.set.discard(value)
1852 1852
1853 1853 wanted = lazywantedset()
1854 1854
1855 1855 # it might be worthwhile to do this in the iterator if the rev range
1856 1856 # is descending and the prune args are all within that range
1857 1857 for rev in opts.get('prune', ()):
1858 1858 rev = repo[rev].rev()
1859 1859 ff = _followfilter(repo)
1860 1860 stop = min(revs[0], revs[-1])
1861 1861 for x in xrange(rev, stop - 1, -1):
1862 1862 if ff.match(x):
1863 1863 wanted = wanted - [x]
1864 1864
1865 1865 # Now that wanted is correctly initialized, we can iterate over the
1866 1866 # revision range, yielding only revisions in wanted.
1867 1867 def iterate():
1868 1868 if follow and match.always():
1869 1869 ff = _followfilter(repo, onlyfirst=opts.get('follow_first'))
1870 1870 def want(rev):
1871 1871 return ff.match(rev) and rev in wanted
1872 1872 else:
1873 1873 def want(rev):
1874 1874 return rev in wanted
1875 1875
1876 1876 it = iter(revs)
1877 1877 stopiteration = False
1878 1878 for windowsize in increasingwindows():
1879 1879 nrevs = []
1880 1880 for i in xrange(windowsize):
1881 1881 rev = next(it, None)
1882 1882 if rev is None:
1883 1883 stopiteration = True
1884 1884 break
1885 1885 elif want(rev):
1886 1886 nrevs.append(rev)
1887 1887 for rev in sorted(nrevs):
1888 1888 fns = fncache.get(rev)
1889 1889 ctx = change(rev)
1890 1890 if not fns:
1891 1891 def fns_generator():
1892 1892 for f in ctx.files():
1893 1893 if match(f):
1894 1894 yield f
1895 1895 fns = fns_generator()
1896 1896 prepare(ctx, fns)
1897 1897 for rev in nrevs:
1898 1898 yield change(rev)
1899 1899
1900 1900 if stopiteration:
1901 1901 break
1902 1902
1903 1903 return iterate()
1904 1904
1905 1905 def add(ui, repo, match, prefix, explicitonly, **opts):
1906 1906 join = lambda f: os.path.join(prefix, f)
1907 1907 bad = []
1908 1908
1909 1909 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
1910 1910 names = []
1911 1911 wctx = repo[None]
1912 1912 cca = None
1913 1913 abort, warn = scmutil.checkportabilityalert(ui)
1914 1914 if abort or warn:
1915 1915 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
1916 1916
1917 1917 badmatch = matchmod.badmatch(match, badfn)
1918 1918 dirstate = repo.dirstate
1919 1919 # We don't want to just call wctx.walk here, since it would return a lot of
1920 1920 # clean files, which we aren't interested in and takes time.
1921 1921 for f in sorted(dirstate.walk(badmatch, subrepos=sorted(wctx.substate),
1922 1922 unknown=True, ignored=False, full=False)):
1923 1923 exact = match.exact(f)
1924 1924 if exact or not explicitonly and f not in wctx and repo.wvfs.lexists(f):
1925 1925 if cca:
1926 1926 cca(f)
1927 1927 names.append(f)
1928 1928 if ui.verbose or not exact:
1929 1929 ui.status(_('adding %s\n') % match.rel(f))
1930 1930
1931 1931 for subpath in sorted(wctx.substate):
1932 1932 sub = wctx.sub(subpath)
1933 1933 try:
1934 1934 submatch = matchmod.subdirmatcher(subpath, match)
1935 1935 if opts.get(r'subrepos'):
1936 1936 bad.extend(sub.add(ui, submatch, prefix, False, **opts))
1937 1937 else:
1938 1938 bad.extend(sub.add(ui, submatch, prefix, True, **opts))
1939 1939 except error.LookupError:
1940 1940 ui.status(_("skipping missing subrepository: %s\n")
1941 1941 % join(subpath))
1942 1942
1943 1943 if not opts.get(r'dry_run'):
1944 1944 rejected = wctx.add(names, prefix)
1945 1945 bad.extend(f for f in rejected if f in match.files())
1946 1946 return bad
1947 1947
1948 1948 def addwebdirpath(repo, serverpath, webconf):
1949 1949 webconf[serverpath] = repo.root
1950 1950 repo.ui.debug('adding %s = %s\n' % (serverpath, repo.root))
1951 1951
1952 1952 for r in repo.revs('filelog("path:.hgsub")'):
1953 1953 ctx = repo[r]
1954 1954 for subpath in ctx.substate:
1955 1955 ctx.sub(subpath).addwebdirpath(serverpath, webconf)
1956 1956
1957 1957 def forget(ui, repo, match, prefix, explicitonly):
1958 1958 join = lambda f: os.path.join(prefix, f)
1959 1959 bad = []
1960 1960 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
1961 1961 wctx = repo[None]
1962 1962 forgot = []
1963 1963
1964 1964 s = repo.status(match=matchmod.badmatch(match, badfn), clean=True)
1965 1965 forget = sorted(s.modified + s.added + s.deleted + s.clean)
1966 1966 if explicitonly:
1967 1967 forget = [f for f in forget if match.exact(f)]
1968 1968
1969 1969 for subpath in sorted(wctx.substate):
1970 1970 sub = wctx.sub(subpath)
1971 1971 try:
1972 1972 submatch = matchmod.subdirmatcher(subpath, match)
1973 1973 subbad, subforgot = sub.forget(submatch, prefix)
1974 1974 bad.extend([subpath + '/' + f for f in subbad])
1975 1975 forgot.extend([subpath + '/' + f for f in subforgot])
1976 1976 except error.LookupError:
1977 1977 ui.status(_("skipping missing subrepository: %s\n")
1978 1978 % join(subpath))
1979 1979
1980 1980 if not explicitonly:
1981 1981 for f in match.files():
1982 1982 if f not in repo.dirstate and not repo.wvfs.isdir(f):
1983 1983 if f not in forgot:
1984 1984 if repo.wvfs.exists(f):
1985 1985 # Don't complain if the exact case match wasn't given.
1986 1986 # But don't do this until after checking 'forgot', so
1987 1987 # that subrepo files aren't normalized, and this op is
1988 1988 # purely from data cached by the status walk above.
1989 1989 if repo.dirstate.normalize(f) in repo.dirstate:
1990 1990 continue
1991 1991 ui.warn(_('not removing %s: '
1992 1992 'file is already untracked\n')
1993 1993 % match.rel(f))
1994 1994 bad.append(f)
1995 1995
1996 1996 for f in forget:
1997 1997 if ui.verbose or not match.exact(f):
1998 1998 ui.status(_('removing %s\n') % match.rel(f))
1999 1999
2000 2000 rejected = wctx.forget(forget, prefix)
2001 2001 bad.extend(f for f in rejected if f in match.files())
2002 2002 forgot.extend(f for f in forget if f not in rejected)
2003 2003 return bad, forgot
2004 2004
2005 2005 def files(ui, ctx, m, fm, fmt, subrepos):
2006 2006 rev = ctx.rev()
2007 2007 ret = 1
2008 2008 ds = ctx.repo().dirstate
2009 2009
2010 2010 for f in ctx.matches(m):
2011 2011 if rev is None and ds[f] == 'r':
2012 2012 continue
2013 2013 fm.startitem()
2014 2014 if ui.verbose:
2015 2015 fc = ctx[f]
2016 2016 fm.write('size flags', '% 10d % 1s ', fc.size(), fc.flags())
2017 2017 fm.data(abspath=f)
2018 2018 fm.write('path', fmt, m.rel(f))
2019 2019 ret = 0
2020 2020
2021 2021 for subpath in sorted(ctx.substate):
2022 2022 submatch = matchmod.subdirmatcher(subpath, m)
2023 2023 if (subrepos or m.exact(subpath) or any(submatch.files())):
2024 2024 sub = ctx.sub(subpath)
2025 2025 try:
2026 2026 recurse = m.exact(subpath) or subrepos
2027 2027 if sub.printfiles(ui, submatch, fm, fmt, recurse) == 0:
2028 2028 ret = 0
2029 2029 except error.LookupError:
2030 2030 ui.status(_("skipping missing subrepository: %s\n")
2031 2031 % m.abs(subpath))
2032 2032
2033 2033 return ret
2034 2034
2035 2035 def remove(ui, repo, m, prefix, after, force, subrepos, warnings=None):
2036 2036 join = lambda f: os.path.join(prefix, f)
2037 2037 ret = 0
2038 2038 s = repo.status(match=m, clean=True)
2039 2039 modified, added, deleted, clean = s[0], s[1], s[3], s[6]
2040 2040
2041 2041 wctx = repo[None]
2042 2042
2043 2043 if warnings is None:
2044 2044 warnings = []
2045 2045 warn = True
2046 2046 else:
2047 2047 warn = False
2048 2048
2049 2049 subs = sorted(wctx.substate)
2050 2050 total = len(subs)
2051 2051 count = 0
2052 2052 for subpath in subs:
2053 2053 count += 1
2054 2054 submatch = matchmod.subdirmatcher(subpath, m)
2055 2055 if subrepos or m.exact(subpath) or any(submatch.files()):
2056 2056 ui.progress(_('searching'), count, total=total, unit=_('subrepos'))
2057 2057 sub = wctx.sub(subpath)
2058 2058 try:
2059 2059 if sub.removefiles(submatch, prefix, after, force, subrepos,
2060 2060 warnings):
2061 2061 ret = 1
2062 2062 except error.LookupError:
2063 2063 warnings.append(_("skipping missing subrepository: %s\n")
2064 2064 % join(subpath))
2065 2065 ui.progress(_('searching'), None)
2066 2066
2067 2067 # warn about failure to delete explicit files/dirs
2068 2068 deleteddirs = util.dirs(deleted)
2069 2069 files = m.files()
2070 2070 total = len(files)
2071 2071 count = 0
2072 2072 for f in files:
2073 2073 def insubrepo():
2074 2074 for subpath in wctx.substate:
2075 2075 if f.startswith(subpath + '/'):
2076 2076 return True
2077 2077 return False
2078 2078
2079 2079 count += 1
2080 2080 ui.progress(_('deleting'), count, total=total, unit=_('files'))
2081 2081 isdir = f in deleteddirs or wctx.hasdir(f)
2082 2082 if (f in repo.dirstate or isdir or f == '.'
2083 2083 or insubrepo() or f in subs):
2084 2084 continue
2085 2085
2086 2086 if repo.wvfs.exists(f):
2087 2087 if repo.wvfs.isdir(f):
2088 2088 warnings.append(_('not removing %s: no tracked files\n')
2089 2089 % m.rel(f))
2090 2090 else:
2091 2091 warnings.append(_('not removing %s: file is untracked\n')
2092 2092 % m.rel(f))
2093 2093 # missing files will generate a warning elsewhere
2094 2094 ret = 1
2095 2095 ui.progress(_('deleting'), None)
2096 2096
2097 2097 if force:
2098 2098 list = modified + deleted + clean + added
2099 2099 elif after:
2100 2100 list = deleted
2101 2101 remaining = modified + added + clean
2102 2102 total = len(remaining)
2103 2103 count = 0
2104 2104 for f in remaining:
2105 2105 count += 1
2106 2106 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2107 2107 if ui.verbose or (f in files):
2108 2108 warnings.append(_('not removing %s: file still exists\n')
2109 2109 % m.rel(f))
2110 2110 ret = 1
2111 2111 ui.progress(_('skipping'), None)
2112 2112 else:
2113 2113 list = deleted + clean
2114 2114 total = len(modified) + len(added)
2115 2115 count = 0
2116 2116 for f in modified:
2117 2117 count += 1
2118 2118 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2119 2119 warnings.append(_('not removing %s: file is modified (use -f'
2120 2120 ' to force removal)\n') % m.rel(f))
2121 2121 ret = 1
2122 2122 for f in added:
2123 2123 count += 1
2124 2124 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2125 2125 warnings.append(_("not removing %s: file has been marked for add"
2126 2126 " (use 'hg forget' to undo add)\n") % m.rel(f))
2127 2127 ret = 1
2128 2128 ui.progress(_('skipping'), None)
2129 2129
2130 2130 list = sorted(list)
2131 2131 total = len(list)
2132 2132 count = 0
2133 2133 for f in list:
2134 2134 count += 1
2135 2135 if ui.verbose or not m.exact(f):
2136 2136 ui.progress(_('deleting'), count, total=total, unit=_('files'))
2137 2137 ui.status(_('removing %s\n') % m.rel(f))
2138 2138 ui.progress(_('deleting'), None)
2139 2139
2140 2140 with repo.wlock():
2141 2141 if not after:
2142 2142 for f in list:
2143 2143 if f in added:
2144 2144 continue # we never unlink added files on remove
2145 2145 repo.wvfs.unlinkpath(f, ignoremissing=True)
2146 2146 repo[None].forget(list)
2147 2147
2148 2148 if warn:
2149 2149 for warning in warnings:
2150 2150 ui.warn(warning)
2151 2151
2152 2152 return ret
2153 2153
2154 2154 def _updatecatformatter(fm, ctx, matcher, path, decode):
2155 2155 """Hook for adding data to the formatter used by ``hg cat``.
2156 2156
2157 2157 Extensions (e.g., lfs) can wrap this to inject keywords/data, but must call
2158 2158 this method first."""
2159 2159 data = ctx[path].data()
2160 2160 if decode:
2161 2161 data = ctx.repo().wwritedata(path, data)
2162 2162 fm.startitem()
2163 2163 fm.write('data', '%s', data)
2164 2164 fm.data(abspath=path, path=matcher.rel(path))
2165 2165
2166 2166 def cat(ui, repo, ctx, matcher, basefm, fntemplate, prefix, **opts):
2167 2167 err = 1
2168 2168 opts = pycompat.byteskwargs(opts)
2169 2169
2170 2170 def write(path):
2171 2171 filename = None
2172 2172 if fntemplate:
2173 2173 filename = makefilename(repo, fntemplate, ctx.node(),
2174 2174 pathname=os.path.join(prefix, path))
2175 2175 # attempt to create the directory if it does not already exist
2176 2176 try:
2177 2177 os.makedirs(os.path.dirname(filename))
2178 2178 except OSError:
2179 2179 pass
2180 2180 with formatter.maybereopen(basefm, filename, opts) as fm:
2181 2181 _updatecatformatter(fm, ctx, matcher, path, opts.get('decode'))
2182 2182
2183 2183 # Automation often uses hg cat on single files, so special case it
2184 2184 # for performance to avoid the cost of parsing the manifest.
2185 2185 if len(matcher.files()) == 1 and not matcher.anypats():
2186 2186 file = matcher.files()[0]
2187 2187 mfl = repo.manifestlog
2188 2188 mfnode = ctx.manifestnode()
2189 2189 try:
2190 2190 if mfnode and mfl[mfnode].find(file)[0]:
2191 2191 write(file)
2192 2192 return 0
2193 2193 except KeyError:
2194 2194 pass
2195 2195
2196 2196 for abs in ctx.walk(matcher):
2197 2197 write(abs)
2198 2198 err = 0
2199 2199
2200 2200 for subpath in sorted(ctx.substate):
2201 2201 sub = ctx.sub(subpath)
2202 2202 try:
2203 2203 submatch = matchmod.subdirmatcher(subpath, matcher)
2204 2204
2205 2205 if not sub.cat(submatch, basefm, fntemplate,
2206 2206 os.path.join(prefix, sub._path),
2207 2207 **pycompat.strkwargs(opts)):
2208 2208 err = 0
2209 2209 except error.RepoLookupError:
2210 2210 ui.status(_("skipping missing subrepository: %s\n")
2211 2211 % os.path.join(prefix, subpath))
2212 2212
2213 2213 return err
2214 2214
2215 2215 def commit(ui, repo, commitfunc, pats, opts):
2216 2216 '''commit the specified files or all outstanding changes'''
2217 2217 date = opts.get('date')
2218 2218 if date:
2219 2219 opts['date'] = util.parsedate(date)
2220 2220 message = logmessage(ui, opts)
2221 2221 matcher = scmutil.match(repo[None], pats, opts)
2222 2222
2223 2223 dsguard = None
2224 2224 # extract addremove carefully -- this function can be called from a command
2225 2225 # that doesn't support addremove
2226 2226 if opts.get('addremove'):
2227 2227 dsguard = dirstateguard.dirstateguard(repo, 'commit')
2228 2228 with dsguard or util.nullcontextmanager():
2229 2229 if dsguard:
2230 2230 if scmutil.addremove(repo, matcher, "", opts) != 0:
2231 2231 raise error.Abort(
2232 2232 _("failed to mark all new/missing files as added/removed"))
2233 2233
2234 2234 return commitfunc(ui, repo, message, matcher, opts)
2235 2235
2236 2236 def samefile(f, ctx1, ctx2):
2237 2237 if f in ctx1.manifest():
2238 2238 a = ctx1.filectx(f)
2239 2239 if f in ctx2.manifest():
2240 2240 b = ctx2.filectx(f)
2241 2241 return (not a.cmp(b)
2242 2242 and a.flags() == b.flags())
2243 2243 else:
2244 2244 return False
2245 2245 else:
2246 2246 return f not in ctx2.manifest()
2247 2247
2248 2248 def amend(ui, repo, old, extra, pats, opts):
2249 2249 # avoid cycle context -> subrepo -> cmdutil
2250 2250 from . import context
2251 2251
2252 2252 # amend will reuse the existing user if not specified, but the obsolete
2253 2253 # marker creation requires that the current user's name is specified.
2254 2254 if obsolete.isenabled(repo, obsolete.createmarkersopt):
2255 2255 ui.username() # raise exception if username not set
2256 2256
2257 2257 ui.note(_('amending changeset %s\n') % old)
2258 2258 base = old.p1()
2259 2259
2260 2260 with repo.wlock(), repo.lock(), repo.transaction('amend'):
2261 2261 # Participating changesets:
2262 2262 #
2263 2263 # wctx o - workingctx that contains changes from working copy
2264 2264 # | to go into amending commit
2265 2265 # |
2266 2266 # old o - changeset to amend
2267 2267 # |
2268 2268 # base o - first parent of the changeset to amend
2269 2269 wctx = repo[None]
2270 2270
2271 2271 # Copy to avoid mutating input
2272 2272 extra = extra.copy()
2273 2273 # Update extra dict from amended commit (e.g. to preserve graft
2274 2274 # source)
2275 2275 extra.update(old.extra())
2276 2276
2277 2277 # Also update it from the from the wctx
2278 2278 extra.update(wctx.extra())
2279 2279
2280 2280 user = opts.get('user') or old.user()
2281 2281 date = opts.get('date') or old.date()
2282 2282
2283 2283 # Parse the date to allow comparison between date and old.date()
2284 2284 date = util.parsedate(date)
2285 2285
2286 2286 if len(old.parents()) > 1:
2287 2287 # ctx.files() isn't reliable for merges, so fall back to the
2288 2288 # slower repo.status() method
2289 2289 files = set([fn for st in repo.status(base, old)[:3]
2290 2290 for fn in st])
2291 2291 else:
2292 2292 files = set(old.files())
2293 2293
2294 2294 # add/remove the files to the working copy if the "addremove" option
2295 2295 # was specified.
2296 2296 matcher = scmutil.match(wctx, pats, opts)
2297 2297 if (opts.get('addremove')
2298 2298 and scmutil.addremove(repo, matcher, "", opts)):
2299 2299 raise error.Abort(
2300 2300 _("failed to mark all new/missing files as added/removed"))
2301 2301
2302 2302 # Check subrepos. This depends on in-place wctx._status update in
2303 2303 # subrepo.precommit(). To minimize the risk of this hack, we do
2304 2304 # nothing if .hgsub does not exist.
2305 2305 if '.hgsub' in wctx or '.hgsub' in old:
2306 2306 from . import subrepo # avoid cycle: cmdutil -> subrepo -> cmdutil
2307 2307 subs, commitsubs, newsubstate = subrepo.precommit(
2308 2308 ui, wctx, wctx._status, matcher)
2309 2309 # amend should abort if commitsubrepos is enabled
2310 2310 assert not commitsubs
2311 2311 if subs:
2312 2312 subrepo.writestate(repo, newsubstate)
2313 2313
2314 2314 filestoamend = set(f for f in wctx.files() if matcher(f))
2315 2315
2316 2316 changes = (len(filestoamend) > 0)
2317 2317 if changes:
2318 2318 # Recompute copies (avoid recording a -> b -> a)
2319 2319 copied = copies.pathcopies(base, wctx, matcher)
2320 2320 if old.p2:
2321 2321 copied.update(copies.pathcopies(old.p2(), wctx, matcher))
2322 2322
2323 2323 # Prune files which were reverted by the updates: if old
2324 2324 # introduced file X and the file was renamed in the working
2325 2325 # copy, then those two files are the same and
2326 2326 # we can discard X from our list of files. Likewise if X
2327 2327 # was removed, it's no longer relevant. If X is missing (aka
2328 2328 # deleted), old X must be preserved.
2329 2329 files.update(filestoamend)
2330 2330 files = [f for f in files if (not samefile(f, wctx, base)
2331 2331 or f in wctx.deleted())]
2332 2332
2333 2333 def filectxfn(repo, ctx_, path):
2334 2334 try:
2335 2335 # If the file being considered is not amongst the files
2336 2336 # to be amended, we should return the file context from the
2337 2337 # old changeset. This avoids issues when only some files in
2338 2338 # the working copy are being amended but there are also
2339 2339 # changes to other files from the old changeset.
2340 2340 if path not in filestoamend:
2341 2341 return old.filectx(path)
2342 2342
2343 2343 # Return None for removed files.
2344 2344 if path in wctx.removed():
2345 2345 return None
2346 2346
2347 2347 fctx = wctx[path]
2348 2348 flags = fctx.flags()
2349 2349 mctx = context.memfilectx(repo, ctx_,
2350 2350 fctx.path(), fctx.data(),
2351 2351 islink='l' in flags,
2352 2352 isexec='x' in flags,
2353 2353 copied=copied.get(path))
2354 2354 return mctx
2355 2355 except KeyError:
2356 2356 return None
2357 2357 else:
2358 2358 ui.note(_('copying changeset %s to %s\n') % (old, base))
2359 2359
2360 2360 # Use version of files as in the old cset
2361 2361 def filectxfn(repo, ctx_, path):
2362 2362 try:
2363 2363 return old.filectx(path)
2364 2364 except KeyError:
2365 2365 return None
2366 2366
2367 2367 # See if we got a message from -m or -l, if not, open the editor with
2368 2368 # the message of the changeset to amend.
2369 2369 message = logmessage(ui, opts)
2370 2370
2371 2371 editform = mergeeditform(old, 'commit.amend')
2372 2372 editor = getcommiteditor(editform=editform,
2373 2373 **pycompat.strkwargs(opts))
2374 2374
2375 2375 if not message:
2376 2376 editor = getcommiteditor(edit=True, editform=editform)
2377 2377 message = old.description()
2378 2378
2379 2379 pureextra = extra.copy()
2380 2380 extra['amend_source'] = old.hex()
2381 2381
2382 2382 new = context.memctx(repo,
2383 2383 parents=[base.node(), old.p2().node()],
2384 2384 text=message,
2385 2385 files=files,
2386 2386 filectxfn=filectxfn,
2387 2387 user=user,
2388 2388 date=date,
2389 2389 extra=extra,
2390 2390 editor=editor)
2391 2391
2392 2392 newdesc = changelog.stripdesc(new.description())
2393 2393 if ((not changes)
2394 2394 and newdesc == old.description()
2395 2395 and user == old.user()
2396 2396 and date == old.date()
2397 2397 and pureextra == old.extra()):
2398 2398 # nothing changed. continuing here would create a new node
2399 2399 # anyway because of the amend_source noise.
2400 2400 #
2401 2401 # This not what we expect from amend.
2402 2402 return old.node()
2403 2403
2404 2404 if opts.get('secret'):
2405 2405 commitphase = 'secret'
2406 2406 else:
2407 2407 commitphase = old.phase()
2408 2408 overrides = {('phases', 'new-commit'): commitphase}
2409 2409 with ui.configoverride(overrides, 'amend'):
2410 2410 newid = repo.commitctx(new)
2411 2411
2412 2412 # Reroute the working copy parent to the new changeset
2413 2413 repo.setparents(newid, nullid)
2414 2414 mapping = {old.node(): (newid,)}
2415 2415 obsmetadata = None
2416 2416 if opts.get('note'):
2417 2417 obsmetadata = {'note': opts['note']}
2418 2418 scmutil.cleanupnodes(repo, mapping, 'amend', metadata=obsmetadata)
2419 2419
2420 2420 # Fixing the dirstate because localrepo.commitctx does not update
2421 2421 # it. This is rather convenient because we did not need to update
2422 2422 # the dirstate for all the files in the new commit which commitctx
2423 2423 # could have done if it updated the dirstate. Now, we can
2424 2424 # selectively update the dirstate only for the amended files.
2425 2425 dirstate = repo.dirstate
2426 2426
2427 2427 # Update the state of the files which were added and
2428 2428 # and modified in the amend to "normal" in the dirstate.
2429 2429 normalfiles = set(wctx.modified() + wctx.added()) & filestoamend
2430 2430 for f in normalfiles:
2431 2431 dirstate.normal(f)
2432 2432
2433 2433 # Update the state of files which were removed in the amend
2434 2434 # to "removed" in the dirstate.
2435 2435 removedfiles = set(wctx.removed()) & filestoamend
2436 2436 for f in removedfiles:
2437 2437 dirstate.drop(f)
2438 2438
2439 2439 return newid
2440 2440
2441 2441 def commiteditor(repo, ctx, subs, editform=''):
2442 2442 if ctx.description():
2443 2443 return ctx.description()
2444 2444 return commitforceeditor(repo, ctx, subs, editform=editform,
2445 2445 unchangedmessagedetection=True)
2446 2446
2447 2447 def commitforceeditor(repo, ctx, subs, finishdesc=None, extramsg=None,
2448 2448 editform='', unchangedmessagedetection=False):
2449 2449 if not extramsg:
2450 2450 extramsg = _("Leave message empty to abort commit.")
2451 2451
2452 2452 forms = [e for e in editform.split('.') if e]
2453 2453 forms.insert(0, 'changeset')
2454 2454 templatetext = None
2455 2455 while forms:
2456 2456 ref = '.'.join(forms)
2457 2457 if repo.ui.config('committemplate', ref):
2458 2458 templatetext = committext = buildcommittemplate(
2459 2459 repo, ctx, subs, extramsg, ref)
2460 2460 break
2461 2461 forms.pop()
2462 2462 else:
2463 2463 committext = buildcommittext(repo, ctx, subs, extramsg)
2464 2464
2465 2465 # run editor in the repository root
2466 2466 olddir = pycompat.getcwd()
2467 2467 os.chdir(repo.root)
2468 2468
2469 2469 # make in-memory changes visible to external process
2470 2470 tr = repo.currenttransaction()
2471 2471 repo.dirstate.write(tr)
2472 2472 pending = tr and tr.writepending() and repo.root
2473 2473
2474 2474 editortext = repo.ui.edit(committext, ctx.user(), ctx.extra(),
2475 2475 editform=editform, pending=pending,
2476 2476 repopath=repo.path, action='commit')
2477 2477 text = editortext
2478 2478
2479 2479 # strip away anything below this special string (used for editors that want
2480 2480 # to display the diff)
2481 2481 stripbelow = re.search(_linebelow, text, flags=re.MULTILINE)
2482 2482 if stripbelow:
2483 2483 text = text[:stripbelow.start()]
2484 2484
2485 2485 text = re.sub("(?m)^HG:.*(\n|$)", "", text)
2486 2486 os.chdir(olddir)
2487 2487
2488 2488 if finishdesc:
2489 2489 text = finishdesc(text)
2490 2490 if not text.strip():
2491 2491 raise error.Abort(_("empty commit message"))
2492 2492 if unchangedmessagedetection and editortext == templatetext:
2493 2493 raise error.Abort(_("commit message unchanged"))
2494 2494
2495 2495 return text
2496 2496
2497 2497 def buildcommittemplate(repo, ctx, subs, extramsg, ref):
2498 2498 ui = repo.ui
2499 2499 spec = formatter.templatespec(ref, None, None)
2500 2500 t = logcmdutil.changesettemplater(ui, repo, spec, None, {}, False)
2501 2501 t.t.cache.update((k, templater.unquotestring(v))
2502 2502 for k, v in repo.ui.configitems('committemplate'))
2503 2503
2504 2504 if not extramsg:
2505 2505 extramsg = '' # ensure that extramsg is string
2506 2506
2507 2507 ui.pushbuffer()
2508 2508 t.show(ctx, extramsg=extramsg)
2509 2509 return ui.popbuffer()
2510 2510
2511 2511 def hgprefix(msg):
2512 2512 return "\n".join(["HG: %s" % a for a in msg.split("\n") if a])
2513 2513
2514 2514 def buildcommittext(repo, ctx, subs, extramsg):
2515 2515 edittext = []
2516 2516 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
2517 2517 if ctx.description():
2518 2518 edittext.append(ctx.description())
2519 2519 edittext.append("")
2520 2520 edittext.append("") # Empty line between message and comments.
2521 2521 edittext.append(hgprefix(_("Enter commit message."
2522 2522 " Lines beginning with 'HG:' are removed.")))
2523 2523 edittext.append(hgprefix(extramsg))
2524 2524 edittext.append("HG: --")
2525 2525 edittext.append(hgprefix(_("user: %s") % ctx.user()))
2526 2526 if ctx.p2():
2527 2527 edittext.append(hgprefix(_("branch merge")))
2528 2528 if ctx.branch():
2529 2529 edittext.append(hgprefix(_("branch '%s'") % ctx.branch()))
2530 2530 if bookmarks.isactivewdirparent(repo):
2531 2531 edittext.append(hgprefix(_("bookmark '%s'") % repo._activebookmark))
2532 2532 edittext.extend([hgprefix(_("subrepo %s") % s) for s in subs])
2533 2533 edittext.extend([hgprefix(_("added %s") % f) for f in added])
2534 2534 edittext.extend([hgprefix(_("changed %s") % f) for f in modified])
2535 2535 edittext.extend([hgprefix(_("removed %s") % f) for f in removed])
2536 2536 if not added and not modified and not removed:
2537 2537 edittext.append(hgprefix(_("no files changed")))
2538 2538 edittext.append("")
2539 2539
2540 2540 return "\n".join(edittext)
2541 2541
2542 2542 def commitstatus(repo, node, branch, bheads=None, opts=None):
2543 2543 if opts is None:
2544 2544 opts = {}
2545 2545 ctx = repo[node]
2546 2546 parents = ctx.parents()
2547 2547
2548 2548 if (not opts.get('amend') and bheads and node not in bheads and not
2549 2549 [x for x in parents if x.node() in bheads and x.branch() == branch]):
2550 2550 repo.ui.status(_('created new head\n'))
2551 2551 # The message is not printed for initial roots. For the other
2552 2552 # changesets, it is printed in the following situations:
2553 2553 #
2554 2554 # Par column: for the 2 parents with ...
2555 2555 # N: null or no parent
2556 2556 # B: parent is on another named branch
2557 2557 # C: parent is a regular non head changeset
2558 2558 # H: parent was a branch head of the current branch
2559 2559 # Msg column: whether we print "created new head" message
2560 2560 # In the following, it is assumed that there already exists some
2561 2561 # initial branch heads of the current branch, otherwise nothing is
2562 2562 # printed anyway.
2563 2563 #
2564 2564 # Par Msg Comment
2565 2565 # N N y additional topo root
2566 2566 #
2567 2567 # B N y additional branch root
2568 2568 # C N y additional topo head
2569 2569 # H N n usual case
2570 2570 #
2571 2571 # B B y weird additional branch root
2572 2572 # C B y branch merge
2573 2573 # H B n merge with named branch
2574 2574 #
2575 2575 # C C y additional head from merge
2576 2576 # C H n merge with a head
2577 2577 #
2578 2578 # H H n head merge: head count decreases
2579 2579
2580 2580 if not opts.get('close_branch'):
2581 2581 for r in parents:
2582 2582 if r.closesbranch() and r.branch() == branch:
2583 2583 repo.ui.status(_('reopening closed branch head %d\n') % r)
2584 2584
2585 2585 if repo.ui.debugflag:
2586 2586 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx.hex()))
2587 2587 elif repo.ui.verbose:
2588 2588 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx))
2589 2589
2590 2590 def postcommitstatus(repo, pats, opts):
2591 2591 return repo.status(match=scmutil.match(repo[None], pats, opts))
2592 2592
2593 2593 def revert(ui, repo, ctx, parents, *pats, **opts):
2594 2594 opts = pycompat.byteskwargs(opts)
2595 2595 parent, p2 = parents
2596 2596 node = ctx.node()
2597 2597
2598 2598 mf = ctx.manifest()
2599 2599 if node == p2:
2600 2600 parent = p2
2601 2601
2602 2602 # need all matching names in dirstate and manifest of target rev,
2603 2603 # so have to walk both. do not print errors if files exist in one
2604 2604 # but not other. in both cases, filesets should be evaluated against
2605 2605 # workingctx to get consistent result (issue4497). this means 'set:**'
2606 2606 # cannot be used to select missing files from target rev.
2607 2607
2608 2608 # `names` is a mapping for all elements in working copy and target revision
2609 2609 # The mapping is in the form:
2610 2610 # <asb path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
2611 2611 names = {}
2612 2612
2613 2613 with repo.wlock():
2614 2614 ## filling of the `names` mapping
2615 2615 # walk dirstate to fill `names`
2616 2616
2617 2617 interactive = opts.get('interactive', False)
2618 2618 wctx = repo[None]
2619 2619 m = scmutil.match(wctx, pats, opts)
2620 2620
2621 2621 # we'll need this later
2622 2622 targetsubs = sorted(s for s in wctx.substate if m(s))
2623 2623
2624 2624 if not m.always():
2625 2625 matcher = matchmod.badmatch(m, lambda x, y: False)
2626 2626 for abs in wctx.walk(matcher):
2627 2627 names[abs] = m.rel(abs), m.exact(abs)
2628 2628
2629 2629 # walk target manifest to fill `names`
2630 2630
2631 2631 def badfn(path, msg):
2632 2632 if path in names:
2633 2633 return
2634 2634 if path in ctx.substate:
2635 2635 return
2636 2636 path_ = path + '/'
2637 2637 for f in names:
2638 2638 if f.startswith(path_):
2639 2639 return
2640 2640 ui.warn("%s: %s\n" % (m.rel(path), msg))
2641 2641
2642 2642 for abs in ctx.walk(matchmod.badmatch(m, badfn)):
2643 2643 if abs not in names:
2644 2644 names[abs] = m.rel(abs), m.exact(abs)
2645 2645
2646 2646 # Find status of all file in `names`.
2647 2647 m = scmutil.matchfiles(repo, names)
2648 2648
2649 2649 changes = repo.status(node1=node, match=m,
2650 2650 unknown=True, ignored=True, clean=True)
2651 2651 else:
2652 2652 changes = repo.status(node1=node, match=m)
2653 2653 for kind in changes:
2654 2654 for abs in kind:
2655 2655 names[abs] = m.rel(abs), m.exact(abs)
2656 2656
2657 2657 m = scmutil.matchfiles(repo, names)
2658 2658
2659 2659 modified = set(changes.modified)
2660 2660 added = set(changes.added)
2661 2661 removed = set(changes.removed)
2662 2662 _deleted = set(changes.deleted)
2663 2663 unknown = set(changes.unknown)
2664 2664 unknown.update(changes.ignored)
2665 2665 clean = set(changes.clean)
2666 2666 modadded = set()
2667 2667
2668 2668 # We need to account for the state of the file in the dirstate,
2669 2669 # even when we revert against something else than parent. This will
2670 2670 # slightly alter the behavior of revert (doing back up or not, delete
2671 2671 # or just forget etc).
2672 2672 if parent == node:
2673 2673 dsmodified = modified
2674 2674 dsadded = added
2675 2675 dsremoved = removed
2676 2676 # store all local modifications, useful later for rename detection
2677 2677 localchanges = dsmodified | dsadded
2678 2678 modified, added, removed = set(), set(), set()
2679 2679 else:
2680 2680 changes = repo.status(node1=parent, match=m)
2681 2681 dsmodified = set(changes.modified)
2682 2682 dsadded = set(changes.added)
2683 2683 dsremoved = set(changes.removed)
2684 2684 # store all local modifications, useful later for rename detection
2685 2685 localchanges = dsmodified | dsadded
2686 2686
2687 2687 # only take into account for removes between wc and target
2688 2688 clean |= dsremoved - removed
2689 2689 dsremoved &= removed
2690 2690 # distinct between dirstate remove and other
2691 2691 removed -= dsremoved
2692 2692
2693 2693 modadded = added & dsmodified
2694 2694 added -= modadded
2695 2695
2696 2696 # tell newly modified apart.
2697 2697 dsmodified &= modified
2698 2698 dsmodified |= modified & dsadded # dirstate added may need backup
2699 2699 modified -= dsmodified
2700 2700
2701 2701 # We need to wait for some post-processing to update this set
2702 2702 # before making the distinction. The dirstate will be used for
2703 2703 # that purpose.
2704 2704 dsadded = added
2705 2705
2706 2706 # in case of merge, files that are actually added can be reported as
2707 2707 # modified, we need to post process the result
2708 2708 if p2 != nullid:
2709 2709 mergeadd = set(dsmodified)
2710 2710 for path in dsmodified:
2711 2711 if path in mf:
2712 2712 mergeadd.remove(path)
2713 2713 dsadded |= mergeadd
2714 2714 dsmodified -= mergeadd
2715 2715
2716 2716 # if f is a rename, update `names` to also revert the source
2717 2717 cwd = repo.getcwd()
2718 2718 for f in localchanges:
2719 2719 src = repo.dirstate.copied(f)
2720 2720 # XXX should we check for rename down to target node?
2721 2721 if src and src not in names and repo.dirstate[src] == 'r':
2722 2722 dsremoved.add(src)
2723 2723 names[src] = (repo.pathto(src, cwd), True)
2724 2724
2725 2725 # determine the exact nature of the deleted changesets
2726 2726 deladded = set(_deleted)
2727 2727 for path in _deleted:
2728 2728 if path in mf:
2729 2729 deladded.remove(path)
2730 2730 deleted = _deleted - deladded
2731 2731
2732 2732 # distinguish between file to forget and the other
2733 2733 added = set()
2734 2734 for abs in dsadded:
2735 2735 if repo.dirstate[abs] != 'a':
2736 2736 added.add(abs)
2737 2737 dsadded -= added
2738 2738
2739 2739 for abs in deladded:
2740 2740 if repo.dirstate[abs] == 'a':
2741 2741 dsadded.add(abs)
2742 2742 deladded -= dsadded
2743 2743
2744 2744 # For files marked as removed, we check if an unknown file is present at
2745 2745 # the same path. If a such file exists it may need to be backed up.
2746 2746 # Making the distinction at this stage helps have simpler backup
2747 2747 # logic.
2748 2748 removunk = set()
2749 2749 for abs in removed:
2750 2750 target = repo.wjoin(abs)
2751 2751 if os.path.lexists(target):
2752 2752 removunk.add(abs)
2753 2753 removed -= removunk
2754 2754
2755 2755 dsremovunk = set()
2756 2756 for abs in dsremoved:
2757 2757 target = repo.wjoin(abs)
2758 2758 if os.path.lexists(target):
2759 2759 dsremovunk.add(abs)
2760 2760 dsremoved -= dsremovunk
2761 2761
2762 2762 # action to be actually performed by revert
2763 2763 # (<list of file>, message>) tuple
2764 2764 actions = {'revert': ([], _('reverting %s\n')),
2765 2765 'add': ([], _('adding %s\n')),
2766 2766 'remove': ([], _('removing %s\n')),
2767 2767 'drop': ([], _('removing %s\n')),
2768 2768 'forget': ([], _('forgetting %s\n')),
2769 2769 'undelete': ([], _('undeleting %s\n')),
2770 2770 'noop': (None, _('no changes needed to %s\n')),
2771 2771 'unknown': (None, _('file not managed: %s\n')),
2772 2772 }
2773 2773
2774 2774 # "constant" that convey the backup strategy.
2775 2775 # All set to `discard` if `no-backup` is set do avoid checking
2776 2776 # no_backup lower in the code.
2777 2777 # These values are ordered for comparison purposes
2778 2778 backupinteractive = 3 # do backup if interactively modified
2779 2779 backup = 2 # unconditionally do backup
2780 2780 check = 1 # check if the existing file differs from target
2781 2781 discard = 0 # never do backup
2782 2782 if opts.get('no_backup'):
2783 2783 backupinteractive = backup = check = discard
2784 2784 if interactive:
2785 2785 dsmodifiedbackup = backupinteractive
2786 2786 else:
2787 2787 dsmodifiedbackup = backup
2788 2788 tobackup = set()
2789 2789
2790 2790 backupanddel = actions['remove']
2791 2791 if not opts.get('no_backup'):
2792 2792 backupanddel = actions['drop']
2793 2793
2794 2794 disptable = (
2795 2795 # dispatch table:
2796 2796 # file state
2797 2797 # action
2798 2798 # make backup
2799 2799
2800 2800 ## Sets that results that will change file on disk
2801 2801 # Modified compared to target, no local change
2802 2802 (modified, actions['revert'], discard),
2803 2803 # Modified compared to target, but local file is deleted
2804 2804 (deleted, actions['revert'], discard),
2805 2805 # Modified compared to target, local change
2806 2806 (dsmodified, actions['revert'], dsmodifiedbackup),
2807 2807 # Added since target
2808 2808 (added, actions['remove'], discard),
2809 2809 # Added in working directory
2810 2810 (dsadded, actions['forget'], discard),
2811 2811 # Added since target, have local modification
2812 2812 (modadded, backupanddel, backup),
2813 2813 # Added since target but file is missing in working directory
2814 2814 (deladded, actions['drop'], discard),
2815 2815 # Removed since target, before working copy parent
2816 2816 (removed, actions['add'], discard),
2817 2817 # Same as `removed` but an unknown file exists at the same path
2818 2818 (removunk, actions['add'], check),
2819 2819 # Removed since targe, marked as such in working copy parent
2820 2820 (dsremoved, actions['undelete'], discard),
2821 2821 # Same as `dsremoved` but an unknown file exists at the same path
2822 2822 (dsremovunk, actions['undelete'], check),
2823 2823 ## the following sets does not result in any file changes
2824 2824 # File with no modification
2825 2825 (clean, actions['noop'], discard),
2826 2826 # Existing file, not tracked anywhere
2827 2827 (unknown, actions['unknown'], discard),
2828 2828 )
2829 2829
2830 2830 for abs, (rel, exact) in sorted(names.items()):
2831 2831 # target file to be touch on disk (relative to cwd)
2832 2832 target = repo.wjoin(abs)
2833 2833 # search the entry in the dispatch table.
2834 2834 # if the file is in any of these sets, it was touched in the working
2835 2835 # directory parent and we are sure it needs to be reverted.
2836 2836 for table, (xlist, msg), dobackup in disptable:
2837 2837 if abs not in table:
2838 2838 continue
2839 2839 if xlist is not None:
2840 2840 xlist.append(abs)
2841 2841 if dobackup:
2842 2842 # If in interactive mode, don't automatically create
2843 2843 # .orig files (issue4793)
2844 2844 if dobackup == backupinteractive:
2845 2845 tobackup.add(abs)
2846 2846 elif (backup <= dobackup or wctx[abs].cmp(ctx[abs])):
2847 2847 bakname = scmutil.origpath(ui, repo, rel)
2848 2848 ui.note(_('saving current version of %s as %s\n') %
2849 2849 (rel, bakname))
2850 2850 if not opts.get('dry_run'):
2851 2851 if interactive:
2852 2852 util.copyfile(target, bakname)
2853 2853 else:
2854 2854 util.rename(target, bakname)
2855 2855 if ui.verbose or not exact:
2856 2856 if not isinstance(msg, bytes):
2857 2857 msg = msg(abs)
2858 2858 ui.status(msg % rel)
2859 2859 elif exact:
2860 2860 ui.warn(msg % rel)
2861 2861 break
2862 2862
2863 2863 if not opts.get('dry_run'):
2864 2864 needdata = ('revert', 'add', 'undelete')
2865 _revertprefetch(repo, ctx, *[actions[name][0] for name in needdata])
2865 if _revertprefetch is not _revertprefetchstub:
2866 ui.deprecwarn("'cmdutil._revertprefetch' is deprecated, use "
2867 "'cmdutil._prefetchfiles'", '4.6', stacklevel=1)
2868 _revertprefetch(repo, ctx,
2869 *[actions[name][0] for name in needdata])
2870 oplist = [actions[name][0] for name in needdata]
2871 _prefetchfiles(repo, ctx,
2872 [f for sublist in oplist for f in sublist])
2866 2873 _performrevert(repo, parents, ctx, actions, interactive, tobackup)
2867 2874
2868 2875 if targetsubs:
2869 2876 # Revert the subrepos on the revert list
2870 2877 for sub in targetsubs:
2871 2878 try:
2872 2879 wctx.sub(sub).revert(ctx.substate[sub], *pats,
2873 2880 **pycompat.strkwargs(opts))
2874 2881 except KeyError:
2875 2882 raise error.Abort("subrepository '%s' does not exist in %s!"
2876 2883 % (sub, short(ctx.node())))
2877 2884
2878 def _revertprefetch(repo, ctx, *files):
2879 """Let extension changing the storage layer prefetch content"""
2885 def _revertprefetchstub(repo, ctx, *files):
2886 """Stub method for detecting extension wrapping of _revertprefetch(), to
2887 issue a deprecation warning."""
2888
2889 _revertprefetch = _revertprefetchstub
2890
2891 def _prefetchfiles(repo, ctx, files):
2892 """Let extensions changing the storage layer prefetch content for any non
2893 merge based command."""
2880 2894
2881 2895 def _performrevert(repo, parents, ctx, actions, interactive=False,
2882 2896 tobackup=None):
2883 2897 """function that actually perform all the actions computed for revert
2884 2898
2885 2899 This is an independent function to let extension to plug in and react to
2886 2900 the imminent revert.
2887 2901
2888 2902 Make sure you have the working directory locked when calling this function.
2889 2903 """
2890 2904 parent, p2 = parents
2891 2905 node = ctx.node()
2892 2906 excluded_files = []
2893 2907 matcher_opts = {"exclude": excluded_files}
2894 2908
2895 2909 def checkout(f):
2896 2910 fc = ctx[f]
2897 2911 repo.wwrite(f, fc.data(), fc.flags())
2898 2912
2899 2913 def doremove(f):
2900 2914 try:
2901 2915 repo.wvfs.unlinkpath(f)
2902 2916 except OSError:
2903 2917 pass
2904 2918 repo.dirstate.remove(f)
2905 2919
2906 2920 audit_path = pathutil.pathauditor(repo.root, cached=True)
2907 2921 for f in actions['forget'][0]:
2908 2922 if interactive:
2909 2923 choice = repo.ui.promptchoice(
2910 2924 _("forget added file %s (Yn)?$$ &Yes $$ &No") % f)
2911 2925 if choice == 0:
2912 2926 repo.dirstate.drop(f)
2913 2927 else:
2914 2928 excluded_files.append(repo.wjoin(f))
2915 2929 else:
2916 2930 repo.dirstate.drop(f)
2917 2931 for f in actions['remove'][0]:
2918 2932 audit_path(f)
2919 2933 if interactive:
2920 2934 choice = repo.ui.promptchoice(
2921 2935 _("remove added file %s (Yn)?$$ &Yes $$ &No") % f)
2922 2936 if choice == 0:
2923 2937 doremove(f)
2924 2938 else:
2925 2939 excluded_files.append(repo.wjoin(f))
2926 2940 else:
2927 2941 doremove(f)
2928 2942 for f in actions['drop'][0]:
2929 2943 audit_path(f)
2930 2944 repo.dirstate.remove(f)
2931 2945
2932 2946 normal = None
2933 2947 if node == parent:
2934 2948 # We're reverting to our parent. If possible, we'd like status
2935 2949 # to report the file as clean. We have to use normallookup for
2936 2950 # merges to avoid losing information about merged/dirty files.
2937 2951 if p2 != nullid:
2938 2952 normal = repo.dirstate.normallookup
2939 2953 else:
2940 2954 normal = repo.dirstate.normal
2941 2955
2942 2956 newlyaddedandmodifiedfiles = set()
2943 2957 if interactive:
2944 2958 # Prompt the user for changes to revert
2945 2959 torevert = [repo.wjoin(f) for f in actions['revert'][0]]
2946 2960 m = scmutil.match(ctx, torevert, matcher_opts)
2947 2961 diffopts = patch.difffeatureopts(repo.ui, whitespace=True)
2948 2962 diffopts.nodates = True
2949 2963 diffopts.git = True
2950 2964 operation = 'discard'
2951 2965 reversehunks = True
2952 2966 if node != parent:
2953 2967 operation = 'apply'
2954 2968 reversehunks = False
2955 2969 if reversehunks:
2956 2970 diff = patch.diff(repo, ctx.node(), None, m, opts=diffopts)
2957 2971 else:
2958 2972 diff = patch.diff(repo, None, ctx.node(), m, opts=diffopts)
2959 2973 originalchunks = patch.parsepatch(diff)
2960 2974
2961 2975 try:
2962 2976
2963 2977 chunks, opts = recordfilter(repo.ui, originalchunks,
2964 2978 operation=operation)
2965 2979 if reversehunks:
2966 2980 chunks = patch.reversehunks(chunks)
2967 2981
2968 2982 except error.PatchError as err:
2969 2983 raise error.Abort(_('error parsing patch: %s') % err)
2970 2984
2971 2985 newlyaddedandmodifiedfiles = newandmodified(chunks, originalchunks)
2972 2986 if tobackup is None:
2973 2987 tobackup = set()
2974 2988 # Apply changes
2975 2989 fp = stringio()
2976 2990 for c in chunks:
2977 2991 # Create a backup file only if this hunk should be backed up
2978 2992 if ishunk(c) and c.header.filename() in tobackup:
2979 2993 abs = c.header.filename()
2980 2994 target = repo.wjoin(abs)
2981 2995 bakname = scmutil.origpath(repo.ui, repo, m.rel(abs))
2982 2996 util.copyfile(target, bakname)
2983 2997 tobackup.remove(abs)
2984 2998 c.write(fp)
2985 2999 dopatch = fp.tell()
2986 3000 fp.seek(0)
2987 3001 if dopatch:
2988 3002 try:
2989 3003 patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None)
2990 3004 except error.PatchError as err:
2991 3005 raise error.Abort(str(err))
2992 3006 del fp
2993 3007 else:
2994 3008 for f in actions['revert'][0]:
2995 3009 checkout(f)
2996 3010 if normal:
2997 3011 normal(f)
2998 3012
2999 3013 for f in actions['add'][0]:
3000 3014 # Don't checkout modified files, they are already created by the diff
3001 3015 if f not in newlyaddedandmodifiedfiles:
3002 3016 checkout(f)
3003 3017 repo.dirstate.add(f)
3004 3018
3005 3019 normal = repo.dirstate.normallookup
3006 3020 if node == parent and p2 == nullid:
3007 3021 normal = repo.dirstate.normal
3008 3022 for f in actions['undelete'][0]:
3009 3023 checkout(f)
3010 3024 normal(f)
3011 3025
3012 3026 copied = copies.pathcopies(repo[parent], ctx)
3013 3027
3014 3028 for f in actions['add'][0] + actions['undelete'][0] + actions['revert'][0]:
3015 3029 if f in copied:
3016 3030 repo.dirstate.copy(copied[f], f)
3017 3031
3018 3032 class command(registrar.command):
3019 3033 """deprecated: used registrar.command instead"""
3020 3034 def _doregister(self, func, name, *args, **kwargs):
3021 3035 func._deprecatedregistrar = True # flag for deprecwarn in extensions.py
3022 3036 return super(command, self)._doregister(func, name, *args, **kwargs)
3023 3037
3024 3038 # a list of (ui, repo, otherpeer, opts, missing) functions called by
3025 3039 # commands.outgoing. "missing" is "missing" of the result of
3026 3040 # "findcommonoutgoing()"
3027 3041 outgoinghooks = util.hooks()
3028 3042
3029 3043 # a list of (ui, repo) functions called by commands.summary
3030 3044 summaryhooks = util.hooks()
3031 3045
3032 3046 # a list of (ui, repo, opts, changes) functions called by commands.summary.
3033 3047 #
3034 3048 # functions should return tuple of booleans below, if 'changes' is None:
3035 3049 # (whether-incomings-are-needed, whether-outgoings-are-needed)
3036 3050 #
3037 3051 # otherwise, 'changes' is a tuple of tuples below:
3038 3052 # - (sourceurl, sourcebranch, sourcepeer, incoming)
3039 3053 # - (desturl, destbranch, destpeer, outgoing)
3040 3054 summaryremotehooks = util.hooks()
3041 3055
3042 3056 # A list of state files kept by multistep operations like graft.
3043 3057 # Since graft cannot be aborted, it is considered 'clearable' by update.
3044 3058 # note: bisect is intentionally excluded
3045 3059 # (state file, clearable, allowcommit, error, hint)
3046 3060 unfinishedstates = [
3047 3061 ('graftstate', True, False, _('graft in progress'),
3048 3062 _("use 'hg graft --continue' or 'hg update' to abort")),
3049 3063 ('updatestate', True, False, _('last update was interrupted'),
3050 3064 _("use 'hg update' to get a consistent checkout"))
3051 3065 ]
3052 3066
3053 3067 def checkunfinished(repo, commit=False):
3054 3068 '''Look for an unfinished multistep operation, like graft, and abort
3055 3069 if found. It's probably good to check this right before
3056 3070 bailifchanged().
3057 3071 '''
3058 3072 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3059 3073 if commit and allowcommit:
3060 3074 continue
3061 3075 if repo.vfs.exists(f):
3062 3076 raise error.Abort(msg, hint=hint)
3063 3077
3064 3078 def clearunfinished(repo):
3065 3079 '''Check for unfinished operations (as above), and clear the ones
3066 3080 that are clearable.
3067 3081 '''
3068 3082 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3069 3083 if not clearable and repo.vfs.exists(f):
3070 3084 raise error.Abort(msg, hint=hint)
3071 3085 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3072 3086 if clearable and repo.vfs.exists(f):
3073 3087 util.unlink(repo.vfs.join(f))
3074 3088
3075 3089 afterresolvedstates = [
3076 3090 ('graftstate',
3077 3091 _('hg graft --continue')),
3078 3092 ]
3079 3093
3080 3094 def howtocontinue(repo):
3081 3095 '''Check for an unfinished operation and return the command to finish
3082 3096 it.
3083 3097
3084 3098 afterresolvedstates tuples define a .hg/{file} and the corresponding
3085 3099 command needed to finish it.
3086 3100
3087 3101 Returns a (msg, warning) tuple. 'msg' is a string and 'warning' is
3088 3102 a boolean.
3089 3103 '''
3090 3104 contmsg = _("continue: %s")
3091 3105 for f, msg in afterresolvedstates:
3092 3106 if repo.vfs.exists(f):
3093 3107 return contmsg % msg, True
3094 3108 if repo[None].dirty(missing=True, merge=False, branch=False):
3095 3109 return contmsg % _("hg commit"), False
3096 3110 return None, None
3097 3111
3098 3112 def checkafterresolved(repo):
3099 3113 '''Inform the user about the next action after completing hg resolve
3100 3114
3101 3115 If there's a matching afterresolvedstates, howtocontinue will yield
3102 3116 repo.ui.warn as the reporter.
3103 3117
3104 3118 Otherwise, it will yield repo.ui.note.
3105 3119 '''
3106 3120 msg, warning = howtocontinue(repo)
3107 3121 if msg is not None:
3108 3122 if warning:
3109 3123 repo.ui.warn("%s\n" % msg)
3110 3124 else:
3111 3125 repo.ui.note("%s\n" % msg)
3112 3126
3113 3127 def wrongtooltocontinue(repo, task):
3114 3128 '''Raise an abort suggesting how to properly continue if there is an
3115 3129 active task.
3116 3130
3117 3131 Uses howtocontinue() to find the active task.
3118 3132
3119 3133 If there's no task (repo.ui.note for 'hg commit'), it does not offer
3120 3134 a hint.
3121 3135 '''
3122 3136 after = howtocontinue(repo)
3123 3137 hint = None
3124 3138 if after[1]:
3125 3139 hint = after[0]
3126 3140 raise error.Abort(_('no %s in progress') % task, hint=hint)
3127 3141
3128 3142 class changeset_printer(logcmdutil.changesetprinter):
3129 3143
3130 3144 def __init__(self, ui, *args, **kwargs):
3131 3145 msg = ("'cmdutil.changeset_printer' is deprecated, "
3132 3146 "use 'logcmdutil.logcmdutil'")
3133 3147 ui.deprecwarn(msg, "4.6")
3134 3148 super(changeset_printer, self).__init__(ui, *args, **kwargs)
3135 3149
3136 3150 def displaygraph(ui, *args, **kwargs):
3137 3151 msg = ("'cmdutil.displaygraph' is deprecated, "
3138 3152 "use 'logcmdutil.displaygraph'")
3139 3153 ui.deprecwarn(msg, "4.6")
3140 3154 return logcmdutil.displaygraph(ui, *args, **kwargs)
3141 3155
3142 3156 def show_changeset(ui, *args, **kwargs):
3143 3157 msg = ("'cmdutil.show_changeset' is deprecated, "
3144 3158 "use 'logcmdutil.changesetdisplayer'")
3145 3159 ui.deprecwarn(msg, "4.6")
3146 3160 return logcmdutil.changesetdisplayer(ui, *args, **kwargs)
@@ -1,1835 +1,1835 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 tempfile
22 22 import traceback
23 23
24 24 from .i18n import _
25 25 from .node import hex
26 26
27 27 from . import (
28 28 color,
29 29 config,
30 30 configitems,
31 31 encoding,
32 32 error,
33 33 formatter,
34 34 progress,
35 35 pycompat,
36 36 rcutil,
37 37 scmutil,
38 38 util,
39 39 )
40 40
41 41 urlreq = util.urlreq
42 42
43 43 # for use with str.translate(None, _keepalnum), to keep just alphanumerics
44 44 _keepalnum = ''.join(c for c in map(pycompat.bytechr, range(256))
45 45 if not c.isalnum())
46 46
47 47 # The config knobs that will be altered (if unset) by ui.tweakdefaults.
48 48 tweakrc = b"""
49 49 [ui]
50 50 # The rollback command is dangerous. As a rule, don't use it.
51 51 rollback = False
52 52 # Make `hg status` report copy information
53 53 statuscopies = yes
54 54 # Prefer curses UIs when available. Revert to plain-text with `text`.
55 55 interface = curses
56 56
57 57 [commands]
58 58 # Make `hg status` emit cwd-relative paths by default.
59 59 status.relative = yes
60 60 # Refuse to perform an `hg update` that would cause a file content merge
61 61 update.check = noconflict
62 62
63 63 [diff]
64 64 git = 1
65 65 showfunc = 1
66 66 """
67 67
68 68 samplehgrcs = {
69 69 'user':
70 70 b"""# example user config (see 'hg help config' for more info)
71 71 [ui]
72 72 # name and email, e.g.
73 73 # username = Jane Doe <jdoe@example.com>
74 74 username =
75 75
76 76 # We recommend enabling tweakdefaults to get slight improvements to
77 77 # the UI over time. Make sure to set HGPLAIN in the environment when
78 78 # writing scripts!
79 79 # tweakdefaults = True
80 80
81 81 # uncomment to disable color in command output
82 82 # (see 'hg help color' for details)
83 83 # color = never
84 84
85 85 # uncomment to disable command output pagination
86 86 # (see 'hg help pager' for details)
87 87 # paginate = never
88 88
89 89 [extensions]
90 90 # uncomment these lines to enable some popular extensions
91 91 # (see 'hg help extensions' for more info)
92 92 #
93 93 # churn =
94 94 """,
95 95
96 96 'cloned':
97 97 b"""# example repository config (see 'hg help config' for more info)
98 98 [paths]
99 99 default = %s
100 100
101 101 # path aliases to other clones of this repo in URLs or filesystem paths
102 102 # (see 'hg help config.paths' for more info)
103 103 #
104 104 # default:pushurl = ssh://jdoe@example.net/hg/jdoes-fork
105 105 # my-fork = ssh://jdoe@example.net/hg/jdoes-fork
106 106 # my-clone = /home/jdoe/jdoes-clone
107 107
108 108 [ui]
109 109 # name and email (local to this repository, optional), e.g.
110 110 # username = Jane Doe <jdoe@example.com>
111 111 """,
112 112
113 113 'local':
114 114 b"""# example repository config (see 'hg help config' for more info)
115 115 [paths]
116 116 # path aliases to other clones of this repo in URLs or filesystem paths
117 117 # (see 'hg help config.paths' for more info)
118 118 #
119 119 # default = http://example.com/hg/example-repo
120 120 # default:pushurl = ssh://jdoe@example.net/hg/jdoes-fork
121 121 # my-fork = ssh://jdoe@example.net/hg/jdoes-fork
122 122 # my-clone = /home/jdoe/jdoes-clone
123 123
124 124 [ui]
125 125 # name and email (local to this repository, optional), e.g.
126 126 # username = Jane Doe <jdoe@example.com>
127 127 """,
128 128
129 129 'global':
130 130 b"""# example system-wide hg config (see 'hg help config' for more info)
131 131
132 132 [ui]
133 133 # uncomment to disable color in command output
134 134 # (see 'hg help color' for details)
135 135 # color = never
136 136
137 137 # uncomment to disable command output pagination
138 138 # (see 'hg help pager' for details)
139 139 # paginate = never
140 140
141 141 [extensions]
142 142 # uncomment these lines to enable some popular extensions
143 143 # (see 'hg help extensions' for more info)
144 144 #
145 145 # blackbox =
146 146 # churn =
147 147 """,
148 148 }
149 149
150 150 def _maybestrurl(maybebytes):
151 151 return util.rapply(pycompat.strurl, maybebytes)
152 152
153 153 def _maybebytesurl(maybestr):
154 154 return util.rapply(pycompat.bytesurl, maybestr)
155 155
156 156 class httppasswordmgrdbproxy(object):
157 157 """Delays loading urllib2 until it's needed."""
158 158 def __init__(self):
159 159 self._mgr = None
160 160
161 161 def _get_mgr(self):
162 162 if self._mgr is None:
163 163 self._mgr = urlreq.httppasswordmgrwithdefaultrealm()
164 164 return self._mgr
165 165
166 166 def add_password(self, realm, uris, user, passwd):
167 167 return self._get_mgr().add_password(
168 168 _maybestrurl(realm), _maybestrurl(uris),
169 169 _maybestrurl(user), _maybestrurl(passwd))
170 170
171 171 def find_user_password(self, realm, uri):
172 172 mgr = self._get_mgr()
173 173 return _maybebytesurl(mgr.find_user_password(_maybestrurl(realm),
174 174 _maybestrurl(uri)))
175 175
176 176 def _catchterm(*args):
177 177 raise error.SignalInterrupt
178 178
179 179 # unique object used to detect no default value has been provided when
180 180 # retrieving configuration value.
181 181 _unset = object()
182 182
183 183 # _reqexithandlers: callbacks run at the end of a request
184 184 _reqexithandlers = []
185 185
186 186 class ui(object):
187 187 def __init__(self, src=None):
188 188 """Create a fresh new ui object if no src given
189 189
190 190 Use uimod.ui.load() to create a ui which knows global and user configs.
191 191 In most cases, you should use ui.copy() to create a copy of an existing
192 192 ui object.
193 193 """
194 194 # _buffers: used for temporary capture of output
195 195 self._buffers = []
196 196 # 3-tuple describing how each buffer in the stack behaves.
197 197 # Values are (capture stderr, capture subprocesses, apply labels).
198 198 self._bufferstates = []
199 199 # When a buffer is active, defines whether we are expanding labels.
200 200 # This exists to prevent an extra list lookup.
201 201 self._bufferapplylabels = None
202 202 self.quiet = self.verbose = self.debugflag = self.tracebackflag = False
203 203 self._reportuntrusted = True
204 204 self._knownconfig = configitems.coreitems
205 205 self._ocfg = config.config() # overlay
206 206 self._tcfg = config.config() # trusted
207 207 self._ucfg = config.config() # untrusted
208 208 self._trustusers = set()
209 209 self._trustgroups = set()
210 210 self.callhooks = True
211 211 # Insecure server connections requested.
212 212 self.insecureconnections = False
213 213 # Blocked time
214 214 self.logblockedtimes = False
215 215 # color mode: see mercurial/color.py for possible value
216 216 self._colormode = None
217 217 self._terminfoparams = {}
218 218 self._styles = {}
219 219
220 220 if src:
221 221 self.fout = src.fout
222 222 self.ferr = src.ferr
223 223 self.fin = src.fin
224 224 self.pageractive = src.pageractive
225 225 self._disablepager = src._disablepager
226 226 self._tweaked = src._tweaked
227 227
228 228 self._tcfg = src._tcfg.copy()
229 229 self._ucfg = src._ucfg.copy()
230 230 self._ocfg = src._ocfg.copy()
231 231 self._trustusers = src._trustusers.copy()
232 232 self._trustgroups = src._trustgroups.copy()
233 233 self.environ = src.environ
234 234 self.callhooks = src.callhooks
235 235 self.insecureconnections = src.insecureconnections
236 236 self._colormode = src._colormode
237 237 self._terminfoparams = src._terminfoparams.copy()
238 238 self._styles = src._styles.copy()
239 239
240 240 self.fixconfig()
241 241
242 242 self.httppasswordmgrdb = src.httppasswordmgrdb
243 243 self._blockedtimes = src._blockedtimes
244 244 else:
245 245 self.fout = util.stdout
246 246 self.ferr = util.stderr
247 247 self.fin = util.stdin
248 248 self.pageractive = False
249 249 self._disablepager = False
250 250 self._tweaked = False
251 251
252 252 # shared read-only environment
253 253 self.environ = encoding.environ
254 254
255 255 self.httppasswordmgrdb = httppasswordmgrdbproxy()
256 256 self._blockedtimes = collections.defaultdict(int)
257 257
258 258 allowed = self.configlist('experimental', 'exportableenviron')
259 259 if '*' in allowed:
260 260 self._exportableenviron = self.environ
261 261 else:
262 262 self._exportableenviron = {}
263 263 for k in allowed:
264 264 if k in self.environ:
265 265 self._exportableenviron[k] = self.environ[k]
266 266
267 267 @classmethod
268 268 def load(cls):
269 269 """Create a ui and load global and user configs"""
270 270 u = cls()
271 271 # we always trust global config files and environment variables
272 272 for t, f in rcutil.rccomponents():
273 273 if t == 'path':
274 274 u.readconfig(f, trust=True)
275 275 elif t == 'items':
276 276 sections = set()
277 277 for section, name, value, source in f:
278 278 # do not set u._ocfg
279 279 # XXX clean this up once immutable config object is a thing
280 280 u._tcfg.set(section, name, value, source)
281 281 u._ucfg.set(section, name, value, source)
282 282 sections.add(section)
283 283 for section in sections:
284 284 u.fixconfig(section=section)
285 285 else:
286 286 raise error.ProgrammingError('unknown rctype: %s' % t)
287 287 u._maybetweakdefaults()
288 288 return u
289 289
290 290 def _maybetweakdefaults(self):
291 291 if not self.configbool('ui', 'tweakdefaults'):
292 292 return
293 293 if self._tweaked or self.plain('tweakdefaults'):
294 294 return
295 295
296 296 # Note: it is SUPER IMPORTANT that you set self._tweaked to
297 297 # True *before* any calls to setconfig(), otherwise you'll get
298 298 # infinite recursion between setconfig and this method.
299 299 #
300 300 # TODO: We should extract an inner method in setconfig() to
301 301 # avoid this weirdness.
302 302 self._tweaked = True
303 303 tmpcfg = config.config()
304 304 tmpcfg.parse('<tweakdefaults>', tweakrc)
305 305 for section in tmpcfg:
306 306 for name, value in tmpcfg.items(section):
307 307 if not self.hasconfig(section, name):
308 308 self.setconfig(section, name, value, "<tweakdefaults>")
309 309
310 310 def copy(self):
311 311 return self.__class__(self)
312 312
313 313 def resetstate(self):
314 314 """Clear internal state that shouldn't persist across commands"""
315 315 if self._progbar:
316 316 self._progbar.resetstate() # reset last-print time of progress bar
317 317 self.httppasswordmgrdb = httppasswordmgrdbproxy()
318 318
319 319 @contextlib.contextmanager
320 320 def timeblockedsection(self, key):
321 321 # this is open-coded below - search for timeblockedsection to find them
322 322 starttime = util.timer()
323 323 try:
324 324 yield
325 325 finally:
326 326 self._blockedtimes[key + '_blocked'] += \
327 327 (util.timer() - starttime) * 1000
328 328
329 329 def formatter(self, topic, opts):
330 330 return formatter.formatter(self, self, topic, opts)
331 331
332 332 def _trusted(self, fp, f):
333 333 st = util.fstat(fp)
334 334 if util.isowner(st):
335 335 return True
336 336
337 337 tusers, tgroups = self._trustusers, self._trustgroups
338 338 if '*' in tusers or '*' in tgroups:
339 339 return True
340 340
341 341 user = util.username(st.st_uid)
342 342 group = util.groupname(st.st_gid)
343 343 if user in tusers or group in tgroups or user == util.username():
344 344 return True
345 345
346 346 if self._reportuntrusted:
347 347 self.warn(_('not trusting file %s from untrusted '
348 348 'user %s, group %s\n') % (f, user, group))
349 349 return False
350 350
351 351 def readconfig(self, filename, root=None, trust=False,
352 352 sections=None, remap=None):
353 353 try:
354 354 fp = open(filename, u'rb')
355 355 except IOError:
356 356 if not sections: # ignore unless we were looking for something
357 357 return
358 358 raise
359 359
360 360 cfg = config.config()
361 361 trusted = sections or trust or self._trusted(fp, filename)
362 362
363 363 try:
364 364 cfg.read(filename, fp, sections=sections, remap=remap)
365 365 fp.close()
366 366 except error.ConfigError as inst:
367 367 if trusted:
368 368 raise
369 369 self.warn(_("ignored: %s\n") % str(inst))
370 370
371 371 if self.plain():
372 372 for k in ('debug', 'fallbackencoding', 'quiet', 'slash',
373 373 'logtemplate', 'statuscopies', 'style',
374 374 'traceback', 'verbose'):
375 375 if k in cfg['ui']:
376 376 del cfg['ui'][k]
377 377 for k, v in cfg.items('defaults'):
378 378 del cfg['defaults'][k]
379 379 for k, v in cfg.items('commands'):
380 380 del cfg['commands'][k]
381 381 # Don't remove aliases from the configuration if in the exceptionlist
382 382 if self.plain('alias'):
383 383 for k, v in cfg.items('alias'):
384 384 del cfg['alias'][k]
385 385 if self.plain('revsetalias'):
386 386 for k, v in cfg.items('revsetalias'):
387 387 del cfg['revsetalias'][k]
388 388 if self.plain('templatealias'):
389 389 for k, v in cfg.items('templatealias'):
390 390 del cfg['templatealias'][k]
391 391
392 392 if trusted:
393 393 self._tcfg.update(cfg)
394 394 self._tcfg.update(self._ocfg)
395 395 self._ucfg.update(cfg)
396 396 self._ucfg.update(self._ocfg)
397 397
398 398 if root is None:
399 399 root = os.path.expanduser('~')
400 400 self.fixconfig(root=root)
401 401
402 402 def fixconfig(self, root=None, section=None):
403 403 if section in (None, 'paths'):
404 404 # expand vars and ~
405 405 # translate paths relative to root (or home) into absolute paths
406 406 root = root or pycompat.getcwd()
407 407 for c in self._tcfg, self._ucfg, self._ocfg:
408 408 for n, p in c.items('paths'):
409 409 # Ignore sub-options.
410 410 if ':' in n:
411 411 continue
412 412 if not p:
413 413 continue
414 414 if '%%' in p:
415 415 s = self.configsource('paths', n) or 'none'
416 416 self.warn(_("(deprecated '%%' in path %s=%s from %s)\n")
417 417 % (n, p, s))
418 418 p = p.replace('%%', '%')
419 419 p = util.expandpath(p)
420 420 if not util.hasscheme(p) and not os.path.isabs(p):
421 421 p = os.path.normpath(os.path.join(root, p))
422 422 c.set("paths", n, p)
423 423
424 424 if section in (None, 'ui'):
425 425 # update ui options
426 426 self.debugflag = self.configbool('ui', 'debug')
427 427 self.verbose = self.debugflag or self.configbool('ui', 'verbose')
428 428 self.quiet = not self.debugflag and self.configbool('ui', 'quiet')
429 429 if self.verbose and self.quiet:
430 430 self.quiet = self.verbose = False
431 431 self._reportuntrusted = self.debugflag or self.configbool("ui",
432 432 "report_untrusted")
433 433 self.tracebackflag = self.configbool('ui', 'traceback')
434 434 self.logblockedtimes = self.configbool('ui', 'logblockedtimes')
435 435
436 436 if section in (None, 'trusted'):
437 437 # update trust information
438 438 self._trustusers.update(self.configlist('trusted', 'users'))
439 439 self._trustgroups.update(self.configlist('trusted', 'groups'))
440 440
441 441 def backupconfig(self, section, item):
442 442 return (self._ocfg.backup(section, item),
443 443 self._tcfg.backup(section, item),
444 444 self._ucfg.backup(section, item),)
445 445 def restoreconfig(self, data):
446 446 self._ocfg.restore(data[0])
447 447 self._tcfg.restore(data[1])
448 448 self._ucfg.restore(data[2])
449 449
450 450 def setconfig(self, section, name, value, source=''):
451 451 for cfg in (self._ocfg, self._tcfg, self._ucfg):
452 452 cfg.set(section, name, value, source)
453 453 self.fixconfig(section=section)
454 454 self._maybetweakdefaults()
455 455
456 456 def _data(self, untrusted):
457 457 return untrusted and self._ucfg or self._tcfg
458 458
459 459 def configsource(self, section, name, untrusted=False):
460 460 return self._data(untrusted).source(section, name)
461 461
462 462 def config(self, section, name, default=_unset, untrusted=False):
463 463 """return the plain string version of a config"""
464 464 value = self._config(section, name, default=default,
465 465 untrusted=untrusted)
466 466 if value is _unset:
467 467 return None
468 468 return value
469 469
470 470 def _config(self, section, name, default=_unset, untrusted=False):
471 471 value = itemdefault = default
472 472 item = self._knownconfig.get(section, {}).get(name)
473 473 alternates = [(section, name)]
474 474
475 475 if item is not None:
476 476 alternates.extend(item.alias)
477 477 if callable(item.default):
478 478 itemdefault = item.default()
479 479 else:
480 480 itemdefault = item.default
481 481 else:
482 482 msg = ("accessing unregistered config item: '%s.%s'")
483 483 msg %= (section, name)
484 484 self.develwarn(msg, 2, 'warn-config-unknown')
485 485
486 486 if default is _unset:
487 487 if item is None:
488 488 value = default
489 489 elif item.default is configitems.dynamicdefault:
490 490 value = None
491 491 msg = "config item requires an explicit default value: '%s.%s'"
492 492 msg %= (section, name)
493 493 self.develwarn(msg, 2, 'warn-config-default')
494 494 else:
495 495 value = itemdefault
496 496 elif (item is not None
497 497 and item.default is not configitems.dynamicdefault
498 498 and default != itemdefault):
499 499 msg = ("specifying a mismatched default value for a registered "
500 500 "config item: '%s.%s' '%s'")
501 501 msg %= (section, name, default)
502 502 self.develwarn(msg, 2, 'warn-config-default')
503 503
504 504 for s, n in alternates:
505 505 candidate = self._data(untrusted).get(s, n, None)
506 506 if candidate is not None:
507 507 value = candidate
508 508 section = s
509 509 name = n
510 510 break
511 511
512 512 if self.debugflag and not untrusted and self._reportuntrusted:
513 513 for s, n in alternates:
514 514 uvalue = self._ucfg.get(s, n)
515 515 if uvalue is not None and uvalue != value:
516 516 self.debug("ignoring untrusted configuration option "
517 517 "%s.%s = %s\n" % (s, n, uvalue))
518 518 return value
519 519
520 520 def configsuboptions(self, section, name, default=_unset, untrusted=False):
521 521 """Get a config option and all sub-options.
522 522
523 523 Some config options have sub-options that are declared with the
524 524 format "key:opt = value". This method is used to return the main
525 525 option and all its declared sub-options.
526 526
527 527 Returns a 2-tuple of ``(option, sub-options)``, where `sub-options``
528 528 is a dict of defined sub-options where keys and values are strings.
529 529 """
530 530 main = self.config(section, name, default, untrusted=untrusted)
531 531 data = self._data(untrusted)
532 532 sub = {}
533 533 prefix = '%s:' % name
534 534 for k, v in data.items(section):
535 535 if k.startswith(prefix):
536 536 sub[k[len(prefix):]] = v
537 537
538 538 if self.debugflag and not untrusted and self._reportuntrusted:
539 539 for k, v in sub.items():
540 540 uvalue = self._ucfg.get(section, '%s:%s' % (name, k))
541 541 if uvalue is not None and uvalue != v:
542 542 self.debug('ignoring untrusted configuration option '
543 543 '%s:%s.%s = %s\n' % (section, name, k, uvalue))
544 544
545 545 return main, sub
546 546
547 547 def configpath(self, section, name, default=_unset, untrusted=False):
548 548 'get a path config item, expanded relative to repo root or config file'
549 549 v = self.config(section, name, default, untrusted)
550 550 if v is None:
551 551 return None
552 552 if not os.path.isabs(v) or "://" not in v:
553 553 src = self.configsource(section, name, untrusted)
554 554 if ':' in src:
555 555 base = os.path.dirname(src.rsplit(':')[0])
556 556 v = os.path.join(base, os.path.expanduser(v))
557 557 return v
558 558
559 559 def configbool(self, section, name, default=_unset, untrusted=False):
560 560 """parse a configuration element as a boolean
561 561
562 562 >>> u = ui(); s = b'foo'
563 563 >>> u.setconfig(s, b'true', b'yes')
564 564 >>> u.configbool(s, b'true')
565 565 True
566 566 >>> u.setconfig(s, b'false', b'no')
567 567 >>> u.configbool(s, b'false')
568 568 False
569 569 >>> u.configbool(s, b'unknown')
570 570 False
571 571 >>> u.configbool(s, b'unknown', True)
572 572 True
573 573 >>> u.setconfig(s, b'invalid', b'somevalue')
574 574 >>> u.configbool(s, b'invalid')
575 575 Traceback (most recent call last):
576 576 ...
577 577 ConfigError: foo.invalid is not a boolean ('somevalue')
578 578 """
579 579
580 580 v = self._config(section, name, default, untrusted=untrusted)
581 581 if v is None:
582 582 return v
583 583 if v is _unset:
584 584 if default is _unset:
585 585 return False
586 586 return default
587 587 if isinstance(v, bool):
588 588 return v
589 589 b = util.parsebool(v)
590 590 if b is None:
591 591 raise error.ConfigError(_("%s.%s is not a boolean ('%s')")
592 592 % (section, name, v))
593 593 return b
594 594
595 595 def configwith(self, convert, section, name, default=_unset,
596 596 desc=None, untrusted=False):
597 597 """parse a configuration element with a conversion function
598 598
599 599 >>> u = ui(); s = b'foo'
600 600 >>> u.setconfig(s, b'float1', b'42')
601 601 >>> u.configwith(float, s, b'float1')
602 602 42.0
603 603 >>> u.setconfig(s, b'float2', b'-4.25')
604 604 >>> u.configwith(float, s, b'float2')
605 605 -4.25
606 606 >>> u.configwith(float, s, b'unknown', 7)
607 607 7.0
608 608 >>> u.setconfig(s, b'invalid', b'somevalue')
609 609 >>> u.configwith(float, s, b'invalid')
610 610 Traceback (most recent call last):
611 611 ...
612 612 ConfigError: foo.invalid is not a valid float ('somevalue')
613 613 >>> u.configwith(float, s, b'invalid', desc=b'womble')
614 614 Traceback (most recent call last):
615 615 ...
616 616 ConfigError: foo.invalid is not a valid womble ('somevalue')
617 617 """
618 618
619 619 v = self.config(section, name, default, untrusted)
620 620 if v is None:
621 621 return v # do not attempt to convert None
622 622 try:
623 623 return convert(v)
624 624 except (ValueError, error.ParseError):
625 625 if desc is None:
626 626 desc = pycompat.sysbytes(convert.__name__)
627 627 raise error.ConfigError(_("%s.%s is not a valid %s ('%s')")
628 628 % (section, name, desc, v))
629 629
630 630 def configint(self, section, name, default=_unset, untrusted=False):
631 631 """parse a configuration element as an integer
632 632
633 633 >>> u = ui(); s = b'foo'
634 634 >>> u.setconfig(s, b'int1', b'42')
635 635 >>> u.configint(s, b'int1')
636 636 42
637 637 >>> u.setconfig(s, b'int2', b'-42')
638 638 >>> u.configint(s, b'int2')
639 639 -42
640 640 >>> u.configint(s, b'unknown', 7)
641 641 7
642 642 >>> u.setconfig(s, b'invalid', b'somevalue')
643 643 >>> u.configint(s, b'invalid')
644 644 Traceback (most recent call last):
645 645 ...
646 646 ConfigError: foo.invalid is not a valid integer ('somevalue')
647 647 """
648 648
649 649 return self.configwith(int, section, name, default, 'integer',
650 650 untrusted)
651 651
652 652 def configbytes(self, section, name, default=_unset, untrusted=False):
653 653 """parse a configuration element as a quantity in bytes
654 654
655 655 Units can be specified as b (bytes), k or kb (kilobytes), m or
656 656 mb (megabytes), g or gb (gigabytes).
657 657
658 658 >>> u = ui(); s = b'foo'
659 659 >>> u.setconfig(s, b'val1', b'42')
660 660 >>> u.configbytes(s, b'val1')
661 661 42
662 662 >>> u.setconfig(s, b'val2', b'42.5 kb')
663 663 >>> u.configbytes(s, b'val2')
664 664 43520
665 665 >>> u.configbytes(s, b'unknown', b'7 MB')
666 666 7340032
667 667 >>> u.setconfig(s, b'invalid', b'somevalue')
668 668 >>> u.configbytes(s, b'invalid')
669 669 Traceback (most recent call last):
670 670 ...
671 671 ConfigError: foo.invalid is not a byte quantity ('somevalue')
672 672 """
673 673
674 674 value = self._config(section, name, default, untrusted)
675 675 if value is _unset:
676 676 if default is _unset:
677 677 default = 0
678 678 value = default
679 679 if not isinstance(value, bytes):
680 680 return value
681 681 try:
682 682 return util.sizetoint(value)
683 683 except error.ParseError:
684 684 raise error.ConfigError(_("%s.%s is not a byte quantity ('%s')")
685 685 % (section, name, value))
686 686
687 687 def configlist(self, section, name, default=_unset, untrusted=False):
688 688 """parse a configuration element as a list of comma/space separated
689 689 strings
690 690
691 691 >>> u = ui(); s = b'foo'
692 692 >>> u.setconfig(s, b'list1', b'this,is "a small" ,test')
693 693 >>> u.configlist(s, b'list1')
694 694 ['this', 'is', 'a small', 'test']
695 695 >>> u.setconfig(s, b'list2', b'this, is "a small" , test ')
696 696 >>> u.configlist(s, b'list2')
697 697 ['this', 'is', 'a small', 'test']
698 698 """
699 699 # default is not always a list
700 700 v = self.configwith(config.parselist, section, name, default,
701 701 'list', untrusted)
702 702 if isinstance(v, bytes):
703 703 return config.parselist(v)
704 704 elif v is None:
705 705 return []
706 706 return v
707 707
708 708 def configdate(self, section, name, default=_unset, untrusted=False):
709 709 """parse a configuration element as a tuple of ints
710 710
711 711 >>> u = ui(); s = b'foo'
712 712 >>> u.setconfig(s, b'date', b'0 0')
713 713 >>> u.configdate(s, b'date')
714 714 (0, 0)
715 715 """
716 716 if self.config(section, name, default, untrusted):
717 717 return self.configwith(util.parsedate, section, name, default,
718 718 'date', untrusted)
719 719 if default is _unset:
720 720 return None
721 721 return default
722 722
723 723 def hasconfig(self, section, name, untrusted=False):
724 724 return self._data(untrusted).hasitem(section, name)
725 725
726 726 def has_section(self, section, untrusted=False):
727 727 '''tell whether section exists in config.'''
728 728 return section in self._data(untrusted)
729 729
730 730 def configitems(self, section, untrusted=False, ignoresub=False):
731 731 items = self._data(untrusted).items(section)
732 732 if ignoresub:
733 733 newitems = {}
734 734 for k, v in items:
735 735 if ':' not in k:
736 736 newitems[k] = v
737 737 items = newitems.items()
738 738 if self.debugflag and not untrusted and self._reportuntrusted:
739 739 for k, v in self._ucfg.items(section):
740 740 if self._tcfg.get(section, k) != v:
741 741 self.debug("ignoring untrusted configuration option "
742 742 "%s.%s = %s\n" % (section, k, v))
743 743 return items
744 744
745 745 def walkconfig(self, untrusted=False):
746 746 cfg = self._data(untrusted)
747 747 for section in cfg.sections():
748 748 for name, value in self.configitems(section, untrusted):
749 749 yield section, name, value
750 750
751 751 def plain(self, feature=None):
752 752 '''is plain mode active?
753 753
754 754 Plain mode means that all configuration variables which affect
755 755 the behavior and output of Mercurial should be
756 756 ignored. Additionally, the output should be stable,
757 757 reproducible and suitable for use in scripts or applications.
758 758
759 759 The only way to trigger plain mode is by setting either the
760 760 `HGPLAIN' or `HGPLAINEXCEPT' environment variables.
761 761
762 762 The return value can either be
763 763 - False if HGPLAIN is not set, or feature is in HGPLAINEXCEPT
764 764 - False if feature is disabled by default and not included in HGPLAIN
765 765 - True otherwise
766 766 '''
767 767 if ('HGPLAIN' not in encoding.environ and
768 768 'HGPLAINEXCEPT' not in encoding.environ):
769 769 return False
770 770 exceptions = encoding.environ.get('HGPLAINEXCEPT',
771 771 '').strip().split(',')
772 772 # TODO: add support for HGPLAIN=+feature,-feature syntax
773 773 if '+strictflags' not in encoding.environ.get('HGPLAIN', '').split(','):
774 774 exceptions.append('strictflags')
775 775 if feature and exceptions:
776 776 return feature not in exceptions
777 777 return True
778 778
779 779 def username(self, acceptempty=False):
780 780 """Return default username to be used in commits.
781 781
782 782 Searched in this order: $HGUSER, [ui] section of hgrcs, $EMAIL
783 783 and stop searching if one of these is set.
784 784 If not found and acceptempty is True, returns None.
785 785 If not found and ui.askusername is True, ask the user, else use
786 786 ($LOGNAME or $USER or $LNAME or $USERNAME) + "@full.hostname".
787 787 If no username could be found, raise an Abort error.
788 788 """
789 789 user = encoding.environ.get("HGUSER")
790 790 if user is None:
791 791 user = self.config("ui", "username")
792 792 if user is not None:
793 793 user = os.path.expandvars(user)
794 794 if user is None:
795 795 user = encoding.environ.get("EMAIL")
796 796 if user is None and acceptempty:
797 797 return user
798 798 if user is None and self.configbool("ui", "askusername"):
799 799 user = self.prompt(_("enter a commit username:"), default=None)
800 800 if user is None and not self.interactive():
801 801 try:
802 802 user = '%s@%s' % (util.getuser(), socket.getfqdn())
803 803 self.warn(_("no username found, using '%s' instead\n") % user)
804 804 except KeyError:
805 805 pass
806 806 if not user:
807 807 raise error.Abort(_('no username supplied'),
808 808 hint=_("use 'hg config --edit' "
809 809 'to set your username'))
810 810 if "\n" in user:
811 811 raise error.Abort(_("username %s contains a newline\n")
812 812 % repr(user))
813 813 return user
814 814
815 815 def shortuser(self, user):
816 816 """Return a short representation of a user name or email address."""
817 817 if not self.verbose:
818 818 user = util.shortuser(user)
819 819 return user
820 820
821 821 def expandpath(self, loc, default=None):
822 822 """Return repository location relative to cwd or from [paths]"""
823 823 try:
824 824 p = self.paths.getpath(loc)
825 825 if p:
826 826 return p.rawloc
827 827 except error.RepoError:
828 828 pass
829 829
830 830 if default:
831 831 try:
832 832 p = self.paths.getpath(default)
833 833 if p:
834 834 return p.rawloc
835 835 except error.RepoError:
836 836 pass
837 837
838 838 return loc
839 839
840 840 @util.propertycache
841 841 def paths(self):
842 842 return paths(self)
843 843
844 844 def pushbuffer(self, error=False, subproc=False, labeled=False):
845 845 """install a buffer to capture standard output of the ui object
846 846
847 847 If error is True, the error output will be captured too.
848 848
849 849 If subproc is True, output from subprocesses (typically hooks) will be
850 850 captured too.
851 851
852 852 If labeled is True, any labels associated with buffered
853 853 output will be handled. By default, this has no effect
854 854 on the output returned, but extensions and GUI tools may
855 855 handle this argument and returned styled output. If output
856 856 is being buffered so it can be captured and parsed or
857 857 processed, labeled should not be set to True.
858 858 """
859 859 self._buffers.append([])
860 860 self._bufferstates.append((error, subproc, labeled))
861 861 self._bufferapplylabels = labeled
862 862
863 863 def popbuffer(self):
864 864 '''pop the last buffer and return the buffered output'''
865 865 self._bufferstates.pop()
866 866 if self._bufferstates:
867 867 self._bufferapplylabels = self._bufferstates[-1][2]
868 868 else:
869 869 self._bufferapplylabels = None
870 870
871 871 return "".join(self._buffers.pop())
872 872
873 873 def write(self, *args, **opts):
874 874 '''write args to output
875 875
876 876 By default, this method simply writes to the buffer or stdout.
877 877 Color mode can be set on the UI class to have the output decorated
878 878 with color modifier before being written to stdout.
879 879
880 880 The color used is controlled by an optional keyword argument, "label".
881 881 This should be a string containing label names separated by space.
882 882 Label names take the form of "topic.type". For example, ui.debug()
883 883 issues a label of "ui.debug".
884 884
885 885 When labeling output for a specific command, a label of
886 886 "cmdname.type" is recommended. For example, status issues
887 887 a label of "status.modified" for modified files.
888 888 '''
889 889 if self._buffers and not opts.get(r'prompt', False):
890 890 if self._bufferapplylabels:
891 891 label = opts.get(r'label', '')
892 892 self._buffers[-1].extend(self.label(a, label) for a in args)
893 893 else:
894 894 self._buffers[-1].extend(args)
895 895 elif self._colormode == 'win32':
896 896 # windows color printing is its own can of crab, defer to
897 897 # the color module and that is it.
898 898 color.win32print(self, self._write, *args, **opts)
899 899 else:
900 900 msgs = args
901 901 if self._colormode is not None:
902 902 label = opts.get(r'label', '')
903 903 msgs = [self.label(a, label) for a in args]
904 904 self._write(*msgs, **opts)
905 905
906 906 def _write(self, *msgs, **opts):
907 907 self._progclear()
908 908 # opencode timeblockedsection because this is a critical path
909 909 starttime = util.timer()
910 910 try:
911 911 self.fout.write(''.join(msgs))
912 912 except IOError as err:
913 913 raise error.StdioError(err)
914 914 finally:
915 915 self._blockedtimes['stdio_blocked'] += \
916 916 (util.timer() - starttime) * 1000
917 917
918 918 def write_err(self, *args, **opts):
919 919 self._progclear()
920 920 if self._bufferstates and self._bufferstates[-1][0]:
921 921 self.write(*args, **opts)
922 922 elif self._colormode == 'win32':
923 923 # windows color printing is its own can of crab, defer to
924 924 # the color module and that is it.
925 925 color.win32print(self, self._write_err, *args, **opts)
926 926 else:
927 927 msgs = args
928 928 if self._colormode is not None:
929 929 label = opts.get(r'label', '')
930 930 msgs = [self.label(a, label) for a in args]
931 931 self._write_err(*msgs, **opts)
932 932
933 933 def _write_err(self, *msgs, **opts):
934 934 try:
935 935 with self.timeblockedsection('stdio'):
936 936 if not getattr(self.fout, 'closed', False):
937 937 self.fout.flush()
938 938 for a in msgs:
939 939 self.ferr.write(a)
940 940 # stderr may be buffered under win32 when redirected to files,
941 941 # including stdout.
942 942 if not getattr(self.ferr, 'closed', False):
943 943 self.ferr.flush()
944 944 except IOError as inst:
945 945 if inst.errno not in (errno.EPIPE, errno.EIO, errno.EBADF):
946 946 raise error.StdioError(inst)
947 947
948 948 def flush(self):
949 949 # opencode timeblockedsection because this is a critical path
950 950 starttime = util.timer()
951 951 try:
952 952 try:
953 953 self.fout.flush()
954 954 except IOError as err:
955 955 if err.errno not in (errno.EPIPE, errno.EIO, errno.EBADF):
956 956 raise error.StdioError(err)
957 957 finally:
958 958 try:
959 959 self.ferr.flush()
960 960 except IOError as err:
961 961 if err.errno not in (errno.EPIPE, errno.EIO, errno.EBADF):
962 962 raise error.StdioError(err)
963 963 finally:
964 964 self._blockedtimes['stdio_blocked'] += \
965 965 (util.timer() - starttime) * 1000
966 966
967 967 def _isatty(self, fh):
968 968 if self.configbool('ui', 'nontty'):
969 969 return False
970 970 return util.isatty(fh)
971 971
972 972 def disablepager(self):
973 973 self._disablepager = True
974 974
975 975 def pager(self, command):
976 976 """Start a pager for subsequent command output.
977 977
978 978 Commands which produce a long stream of output should call
979 979 this function to activate the user's preferred pagination
980 980 mechanism (which may be no pager). Calling this function
981 981 precludes any future use of interactive functionality, such as
982 982 prompting the user or activating curses.
983 983
984 984 Args:
985 985 command: The full, non-aliased name of the command. That is, "log"
986 986 not "history, "summary" not "summ", etc.
987 987 """
988 988 if (self._disablepager
989 989 or self.pageractive):
990 990 # how pager should do is already determined
991 991 return
992 992
993 993 if not command.startswith('internal-always-') and (
994 994 # explicit --pager=on (= 'internal-always-' prefix) should
995 995 # take precedence over disabling factors below
996 996 command in self.configlist('pager', 'ignore')
997 997 or not self.configbool('ui', 'paginate')
998 998 or not self.configbool('pager', 'attend-' + command, True)
999 999 # TODO: if we want to allow HGPLAINEXCEPT=pager,
1000 1000 # formatted() will need some adjustment.
1001 1001 or not self.formatted()
1002 1002 or self.plain()
1003 1003 or self._buffers
1004 1004 # TODO: expose debugger-enabled on the UI object
1005 1005 or '--debugger' in pycompat.sysargv):
1006 1006 # We only want to paginate if the ui appears to be
1007 1007 # interactive, the user didn't say HGPLAIN or
1008 1008 # HGPLAINEXCEPT=pager, and the user didn't specify --debug.
1009 1009 return
1010 1010
1011 1011 pagercmd = self.config('pager', 'pager', rcutil.fallbackpager)
1012 1012 if not pagercmd:
1013 1013 return
1014 1014
1015 1015 pagerenv = {}
1016 1016 for name, value in rcutil.defaultpagerenv().items():
1017 1017 if name not in encoding.environ:
1018 1018 pagerenv[name] = value
1019 1019
1020 1020 self.debug('starting pager for command %r\n' % command)
1021 1021 self.flush()
1022 1022
1023 1023 wasformatted = self.formatted()
1024 1024 if util.safehasattr(signal, "SIGPIPE"):
1025 1025 signal.signal(signal.SIGPIPE, _catchterm)
1026 1026 if self._runpager(pagercmd, pagerenv):
1027 1027 self.pageractive = True
1028 1028 # Preserve the formatted-ness of the UI. This is important
1029 1029 # because we mess with stdout, which might confuse
1030 1030 # auto-detection of things being formatted.
1031 1031 self.setconfig('ui', 'formatted', wasformatted, 'pager')
1032 1032 self.setconfig('ui', 'interactive', False, 'pager')
1033 1033
1034 1034 # If pagermode differs from color.mode, reconfigure color now that
1035 1035 # pageractive is set.
1036 1036 cm = self._colormode
1037 1037 if cm != self.config('color', 'pagermode', cm):
1038 1038 color.setup(self)
1039 1039 else:
1040 1040 # If the pager can't be spawned in dispatch when --pager=on is
1041 1041 # given, don't try again when the command runs, to avoid a duplicate
1042 1042 # warning about a missing pager command.
1043 1043 self.disablepager()
1044 1044
1045 1045 def _runpager(self, command, env=None):
1046 1046 """Actually start the pager and set up file descriptors.
1047 1047
1048 1048 This is separate in part so that extensions (like chg) can
1049 1049 override how a pager is invoked.
1050 1050 """
1051 1051 if command == 'cat':
1052 1052 # Save ourselves some work.
1053 1053 return False
1054 1054 # If the command doesn't contain any of these characters, we
1055 1055 # assume it's a binary and exec it directly. This means for
1056 1056 # simple pager command configurations, we can degrade
1057 1057 # gracefully and tell the user about their broken pager.
1058 1058 shell = any(c in command for c in "|&;<>()$`\\\"' \t\n*?[#~=%")
1059 1059
1060 1060 if pycompat.iswindows and not shell:
1061 1061 # Window's built-in `more` cannot be invoked with shell=False, but
1062 1062 # its `more.com` can. Hide this implementation detail from the
1063 1063 # user so we can also get sane bad PAGER behavior. MSYS has
1064 1064 # `more.exe`, so do a cmd.exe style resolution of the executable to
1065 1065 # determine which one to use.
1066 1066 fullcmd = util.findexe(command)
1067 1067 if not fullcmd:
1068 1068 self.warn(_("missing pager command '%s', skipping pager\n")
1069 1069 % command)
1070 1070 return False
1071 1071
1072 1072 command = fullcmd
1073 1073
1074 1074 try:
1075 1075 pager = subprocess.Popen(
1076 1076 command, shell=shell, bufsize=-1,
1077 1077 close_fds=util.closefds, stdin=subprocess.PIPE,
1078 1078 stdout=util.stdout, stderr=util.stderr,
1079 1079 env=util.shellenviron(env))
1080 1080 except OSError as e:
1081 1081 if e.errno == errno.ENOENT and not shell:
1082 1082 self.warn(_("missing pager command '%s', skipping pager\n")
1083 1083 % command)
1084 1084 return False
1085 1085 raise
1086 1086
1087 1087 # back up original file descriptors
1088 1088 stdoutfd = os.dup(util.stdout.fileno())
1089 1089 stderrfd = os.dup(util.stderr.fileno())
1090 1090
1091 1091 os.dup2(pager.stdin.fileno(), util.stdout.fileno())
1092 1092 if self._isatty(util.stderr):
1093 1093 os.dup2(pager.stdin.fileno(), util.stderr.fileno())
1094 1094
1095 1095 @self.atexit
1096 1096 def killpager():
1097 1097 if util.safehasattr(signal, "SIGINT"):
1098 1098 signal.signal(signal.SIGINT, signal.SIG_IGN)
1099 1099 # restore original fds, closing pager.stdin copies in the process
1100 1100 os.dup2(stdoutfd, util.stdout.fileno())
1101 1101 os.dup2(stderrfd, util.stderr.fileno())
1102 1102 pager.stdin.close()
1103 1103 pager.wait()
1104 1104
1105 1105 return True
1106 1106
1107 1107 @property
1108 1108 def _exithandlers(self):
1109 1109 return _reqexithandlers
1110 1110
1111 1111 def atexit(self, func, *args, **kwargs):
1112 1112 '''register a function to run after dispatching a request
1113 1113
1114 1114 Handlers do not stay registered across request boundaries.'''
1115 1115 self._exithandlers.append((func, args, kwargs))
1116 1116 return func
1117 1117
1118 1118 def interface(self, feature):
1119 1119 """what interface to use for interactive console features?
1120 1120
1121 1121 The interface is controlled by the value of `ui.interface` but also by
1122 1122 the value of feature-specific configuration. For example:
1123 1123
1124 1124 ui.interface.histedit = text
1125 1125 ui.interface.chunkselector = curses
1126 1126
1127 1127 Here the features are "histedit" and "chunkselector".
1128 1128
1129 1129 The configuration above means that the default interfaces for commands
1130 1130 is curses, the interface for histedit is text and the interface for
1131 1131 selecting chunk is crecord (the best curses interface available).
1132 1132
1133 1133 Consider the following example:
1134 1134 ui.interface = curses
1135 1135 ui.interface.histedit = text
1136 1136
1137 1137 Then histedit will use the text interface and chunkselector will use
1138 1138 the default curses interface (crecord at the moment).
1139 1139 """
1140 1140 alldefaults = frozenset(["text", "curses"])
1141 1141
1142 1142 featureinterfaces = {
1143 1143 "chunkselector": [
1144 1144 "text",
1145 1145 "curses",
1146 1146 ]
1147 1147 }
1148 1148
1149 1149 # Feature-specific interface
1150 1150 if feature not in featureinterfaces.keys():
1151 1151 # Programming error, not user error
1152 1152 raise ValueError("Unknown feature requested %s" % feature)
1153 1153
1154 1154 availableinterfaces = frozenset(featureinterfaces[feature])
1155 1155 if alldefaults > availableinterfaces:
1156 1156 # Programming error, not user error. We need a use case to
1157 1157 # define the right thing to do here.
1158 1158 raise ValueError(
1159 1159 "Feature %s does not handle all default interfaces" %
1160 1160 feature)
1161 1161
1162 1162 if self.plain():
1163 1163 return "text"
1164 1164
1165 1165 # Default interface for all the features
1166 1166 defaultinterface = "text"
1167 1167 i = self.config("ui", "interface")
1168 1168 if i in alldefaults:
1169 1169 defaultinterface = i
1170 1170
1171 1171 choseninterface = defaultinterface
1172 1172 f = self.config("ui", "interface.%s" % feature)
1173 1173 if f in availableinterfaces:
1174 1174 choseninterface = f
1175 1175
1176 1176 if i is not None and defaultinterface != i:
1177 1177 if f is not None:
1178 1178 self.warn(_("invalid value for ui.interface: %s\n") %
1179 1179 (i,))
1180 1180 else:
1181 1181 self.warn(_("invalid value for ui.interface: %s (using %s)\n") %
1182 1182 (i, choseninterface))
1183 1183 if f is not None and choseninterface != f:
1184 1184 self.warn(_("invalid value for ui.interface.%s: %s (using %s)\n") %
1185 1185 (feature, f, choseninterface))
1186 1186
1187 1187 return choseninterface
1188 1188
1189 1189 def interactive(self):
1190 1190 '''is interactive input allowed?
1191 1191
1192 1192 An interactive session is a session where input can be reasonably read
1193 1193 from `sys.stdin'. If this function returns false, any attempt to read
1194 1194 from stdin should fail with an error, unless a sensible default has been
1195 1195 specified.
1196 1196
1197 1197 Interactiveness is triggered by the value of the `ui.interactive'
1198 1198 configuration variable or - if it is unset - when `sys.stdin' points
1199 1199 to a terminal device.
1200 1200
1201 1201 This function refers to input only; for output, see `ui.formatted()'.
1202 1202 '''
1203 1203 i = self.configbool("ui", "interactive")
1204 1204 if i is None:
1205 1205 # some environments replace stdin without implementing isatty
1206 1206 # usually those are non-interactive
1207 1207 return self._isatty(self.fin)
1208 1208
1209 1209 return i
1210 1210
1211 1211 def termwidth(self):
1212 1212 '''how wide is the terminal in columns?
1213 1213 '''
1214 1214 if 'COLUMNS' in encoding.environ:
1215 1215 try:
1216 1216 return int(encoding.environ['COLUMNS'])
1217 1217 except ValueError:
1218 1218 pass
1219 1219 return scmutil.termsize(self)[0]
1220 1220
1221 1221 def formatted(self):
1222 1222 '''should formatted output be used?
1223 1223
1224 1224 It is often desirable to format the output to suite the output medium.
1225 1225 Examples of this are truncating long lines or colorizing messages.
1226 1226 However, this is not often not desirable when piping output into other
1227 1227 utilities, e.g. `grep'.
1228 1228
1229 1229 Formatted output is triggered by the value of the `ui.formatted'
1230 1230 configuration variable or - if it is unset - when `sys.stdout' points
1231 1231 to a terminal device. Please note that `ui.formatted' should be
1232 1232 considered an implementation detail; it is not intended for use outside
1233 1233 Mercurial or its extensions.
1234 1234
1235 1235 This function refers to output only; for input, see `ui.interactive()'.
1236 1236 This function always returns false when in plain mode, see `ui.plain()'.
1237 1237 '''
1238 1238 if self.plain():
1239 1239 return False
1240 1240
1241 1241 i = self.configbool("ui", "formatted")
1242 1242 if i is None:
1243 1243 # some environments replace stdout without implementing isatty
1244 1244 # usually those are non-interactive
1245 1245 return self._isatty(self.fout)
1246 1246
1247 1247 return i
1248 1248
1249 1249 def _readline(self, prompt=''):
1250 1250 if self._isatty(self.fin):
1251 1251 try:
1252 1252 # magically add command line editing support, where
1253 1253 # available
1254 1254 import readline
1255 1255 # force demandimport to really load the module
1256 1256 readline.read_history_file
1257 1257 # windows sometimes raises something other than ImportError
1258 1258 except Exception:
1259 1259 pass
1260 1260
1261 1261 # call write() so output goes through subclassed implementation
1262 1262 # e.g. color extension on Windows
1263 1263 self.write(prompt, prompt=True)
1264 1264 self.flush()
1265 1265
1266 1266 # prompt ' ' must exist; otherwise readline may delete entire line
1267 1267 # - http://bugs.python.org/issue12833
1268 1268 with self.timeblockedsection('stdio'):
1269 1269 line = util.bytesinput(self.fin, self.fout, r' ')
1270 1270
1271 1271 # When stdin is in binary mode on Windows, it can cause
1272 1272 # raw_input() to emit an extra trailing carriage return
1273 1273 if pycompat.oslinesep == '\r\n' and line and line[-1] == '\r':
1274 1274 line = line[:-1]
1275 1275 return line
1276 1276
1277 1277 def prompt(self, msg, default="y"):
1278 1278 """Prompt user with msg, read response.
1279 1279 If ui is not interactive, the default is returned.
1280 1280 """
1281 1281 if not self.interactive():
1282 1282 self.write(msg, ' ', default or '', "\n")
1283 1283 return default
1284 1284 try:
1285 1285 r = self._readline(self.label(msg, 'ui.prompt'))
1286 1286 if not r:
1287 1287 r = default
1288 1288 if self.configbool('ui', 'promptecho'):
1289 1289 self.write(r, "\n")
1290 1290 return r
1291 1291 except EOFError:
1292 1292 raise error.ResponseExpected()
1293 1293
1294 1294 @staticmethod
1295 1295 def extractchoices(prompt):
1296 1296 """Extract prompt message and list of choices from specified prompt.
1297 1297
1298 1298 This returns tuple "(message, choices)", and "choices" is the
1299 1299 list of tuple "(response character, text without &)".
1300 1300
1301 1301 >>> ui.extractchoices(b"awake? $$ &Yes $$ &No")
1302 1302 ('awake? ', [('y', 'Yes'), ('n', 'No')])
1303 1303 >>> ui.extractchoices(b"line\\nbreak? $$ &Yes $$ &No")
1304 1304 ('line\\nbreak? ', [('y', 'Yes'), ('n', 'No')])
1305 1305 >>> ui.extractchoices(b"want lots of $$money$$?$$Ye&s$$N&o")
1306 1306 ('want lots of $$money$$?', [('s', 'Yes'), ('o', 'No')])
1307 1307 """
1308 1308
1309 1309 # Sadly, the prompt string may have been built with a filename
1310 1310 # containing "$$" so let's try to find the first valid-looking
1311 1311 # prompt to start parsing. Sadly, we also can't rely on
1312 1312 # choices containing spaces, ASCII, or basically anything
1313 1313 # except an ampersand followed by a character.
1314 1314 m = re.match(br'(?s)(.+?)\$\$([^\$]*&[^ \$].*)', prompt)
1315 1315 msg = m.group(1)
1316 1316 choices = [p.strip(' ') for p in m.group(2).split('$$')]
1317 1317 def choicetuple(s):
1318 1318 ampidx = s.index('&')
1319 1319 return s[ampidx + 1:ampidx + 2].lower(), s.replace('&', '', 1)
1320 1320 return (msg, [choicetuple(s) for s in choices])
1321 1321
1322 1322 def promptchoice(self, prompt, default=0):
1323 1323 """Prompt user with a message, read response, and ensure it matches
1324 1324 one of the provided choices. The prompt is formatted as follows:
1325 1325
1326 1326 "would you like fries with that (Yn)? $$ &Yes $$ &No"
1327 1327
1328 1328 The index of the choice is returned. Responses are case
1329 1329 insensitive. If ui is not interactive, the default is
1330 1330 returned.
1331 1331 """
1332 1332
1333 1333 msg, choices = self.extractchoices(prompt)
1334 1334 resps = [r for r, t in choices]
1335 1335 while True:
1336 1336 r = self.prompt(msg, resps[default])
1337 1337 if r.lower() in resps:
1338 1338 return resps.index(r.lower())
1339 1339 self.write(_("unrecognized response\n"))
1340 1340
1341 1341 def getpass(self, prompt=None, default=None):
1342 1342 if not self.interactive():
1343 1343 return default
1344 1344 try:
1345 1345 self.write_err(self.label(prompt or _('password: '), 'ui.prompt'))
1346 1346 # disable getpass() only if explicitly specified. it's still valid
1347 1347 # to interact with tty even if fin is not a tty.
1348 1348 with self.timeblockedsection('stdio'):
1349 1349 if self.configbool('ui', 'nontty'):
1350 1350 l = self.fin.readline()
1351 1351 if not l:
1352 1352 raise EOFError
1353 1353 return l.rstrip('\n')
1354 1354 else:
1355 1355 return getpass.getpass('')
1356 1356 except EOFError:
1357 1357 raise error.ResponseExpected()
1358 1358 def status(self, *msg, **opts):
1359 1359 '''write status message to output (if ui.quiet is False)
1360 1360
1361 1361 This adds an output label of "ui.status".
1362 1362 '''
1363 1363 if not self.quiet:
1364 1364 opts[r'label'] = opts.get(r'label', '') + ' ui.status'
1365 1365 self.write(*msg, **opts)
1366 1366 def warn(self, *msg, **opts):
1367 1367 '''write warning message to output (stderr)
1368 1368
1369 1369 This adds an output label of "ui.warning".
1370 1370 '''
1371 1371 opts[r'label'] = opts.get(r'label', '') + ' ui.warning'
1372 1372 self.write_err(*msg, **opts)
1373 1373 def note(self, *msg, **opts):
1374 1374 '''write note to output (if ui.verbose is True)
1375 1375
1376 1376 This adds an output label of "ui.note".
1377 1377 '''
1378 1378 if self.verbose:
1379 1379 opts[r'label'] = opts.get(r'label', '') + ' ui.note'
1380 1380 self.write(*msg, **opts)
1381 1381 def debug(self, *msg, **opts):
1382 1382 '''write debug message to output (if ui.debugflag is True)
1383 1383
1384 1384 This adds an output label of "ui.debug".
1385 1385 '''
1386 1386 if self.debugflag:
1387 1387 opts[r'label'] = opts.get(r'label', '') + ' ui.debug'
1388 1388 self.write(*msg, **opts)
1389 1389
1390 1390 def edit(self, text, user, extra=None, editform=None, pending=None,
1391 1391 repopath=None, action=None):
1392 1392 if action is None:
1393 1393 self.develwarn('action is None but will soon be a required '
1394 1394 'parameter to ui.edit()')
1395 1395 extra_defaults = {
1396 1396 'prefix': 'editor',
1397 1397 'suffix': '.txt',
1398 1398 }
1399 1399 if extra is not None:
1400 1400 if extra.get('suffix') is not None:
1401 1401 self.develwarn('extra.suffix is not None but will soon be '
1402 1402 'ignored by ui.edit()')
1403 1403 extra_defaults.update(extra)
1404 1404 extra = extra_defaults
1405 1405
1406 1406 if action == 'diff':
1407 1407 suffix = '.diff'
1408 1408 elif action:
1409 1409 suffix = '.%s.hg.txt' % action
1410 1410 else:
1411 1411 suffix = extra['suffix']
1412 1412
1413 1413 rdir = None
1414 1414 if self.configbool('experimental', 'editortmpinhg'):
1415 1415 rdir = repopath
1416 1416 (fd, name) = tempfile.mkstemp(prefix='hg-' + extra['prefix'] + '-',
1417 1417 suffix=suffix,
1418 1418 dir=rdir)
1419 1419 try:
1420 1420 f = os.fdopen(fd, r'wb')
1421 1421 f.write(util.tonativeeol(text))
1422 1422 f.close()
1423 1423
1424 1424 environ = {'HGUSER': user}
1425 1425 if 'transplant_source' in extra:
1426 1426 environ.update({'HGREVISION': hex(extra['transplant_source'])})
1427 1427 for label in ('intermediate-source', 'source', 'rebase_source'):
1428 1428 if label in extra:
1429 1429 environ.update({'HGREVISION': extra[label]})
1430 1430 break
1431 1431 if editform:
1432 1432 environ.update({'HGEDITFORM': editform})
1433 1433 if pending:
1434 1434 environ.update({'HG_PENDING': pending})
1435 1435
1436 1436 editor = self.geteditor()
1437 1437
1438 1438 self.system("%s \"%s\"" % (editor, name),
1439 1439 environ=environ,
1440 1440 onerr=error.Abort, errprefix=_("edit failed"),
1441 1441 blockedtag='editor')
1442 1442
1443 1443 f = open(name, r'rb')
1444 1444 t = util.fromnativeeol(f.read())
1445 1445 f.close()
1446 1446 finally:
1447 1447 os.unlink(name)
1448 1448
1449 1449 return t
1450 1450
1451 1451 def system(self, cmd, environ=None, cwd=None, onerr=None, errprefix=None,
1452 1452 blockedtag=None):
1453 1453 '''execute shell command with appropriate output stream. command
1454 1454 output will be redirected if fout is not stdout.
1455 1455
1456 1456 if command fails and onerr is None, return status, else raise onerr
1457 1457 object as exception.
1458 1458 '''
1459 1459 if blockedtag is None:
1460 1460 # Long cmds tend to be because of an absolute path on cmd. Keep
1461 1461 # the tail end instead
1462 1462 cmdsuffix = cmd.translate(None, _keepalnum)[-85:]
1463 1463 blockedtag = 'unknown_system_' + cmdsuffix
1464 1464 out = self.fout
1465 1465 if any(s[1] for s in self._bufferstates):
1466 1466 out = self
1467 1467 with self.timeblockedsection(blockedtag):
1468 1468 rc = self._runsystem(cmd, environ=environ, cwd=cwd, out=out)
1469 1469 if rc and onerr:
1470 1470 errmsg = '%s %s' % (os.path.basename(cmd.split(None, 1)[0]),
1471 1471 util.explainexit(rc)[0])
1472 1472 if errprefix:
1473 1473 errmsg = '%s: %s' % (errprefix, errmsg)
1474 1474 raise onerr(errmsg)
1475 1475 return rc
1476 1476
1477 1477 def _runsystem(self, cmd, environ, cwd, out):
1478 1478 """actually execute the given shell command (can be overridden by
1479 1479 extensions like chg)"""
1480 1480 return util.system(cmd, environ=environ, cwd=cwd, out=out)
1481 1481
1482 1482 def traceback(self, exc=None, force=False):
1483 1483 '''print exception traceback if traceback printing enabled or forced.
1484 1484 only to call in exception handler. returns true if traceback
1485 1485 printed.'''
1486 1486 if self.tracebackflag or force:
1487 1487 if exc is None:
1488 1488 exc = sys.exc_info()
1489 1489 cause = getattr(exc[1], 'cause', None)
1490 1490
1491 1491 if cause is not None:
1492 1492 causetb = traceback.format_tb(cause[2])
1493 1493 exctb = traceback.format_tb(exc[2])
1494 1494 exconly = traceback.format_exception_only(cause[0], cause[1])
1495 1495
1496 1496 # exclude frame where 'exc' was chained and rethrown from exctb
1497 1497 self.write_err('Traceback (most recent call last):\n',
1498 1498 ''.join(exctb[:-1]),
1499 1499 ''.join(causetb),
1500 1500 ''.join(exconly))
1501 1501 else:
1502 1502 output = traceback.format_exception(exc[0], exc[1], exc[2])
1503 1503 self.write_err(encoding.strtolocal(r''.join(output)))
1504 1504 return self.tracebackflag or force
1505 1505
1506 1506 def geteditor(self):
1507 1507 '''return editor to use'''
1508 1508 if pycompat.sysplatform == 'plan9':
1509 1509 # vi is the MIPS instruction simulator on Plan 9. We
1510 1510 # instead default to E to plumb commit messages to
1511 1511 # avoid confusion.
1512 1512 editor = 'E'
1513 1513 else:
1514 1514 editor = 'vi'
1515 1515 return (encoding.environ.get("HGEDITOR") or
1516 1516 self.config("ui", "editor", editor))
1517 1517
1518 1518 @util.propertycache
1519 1519 def _progbar(self):
1520 1520 """setup the progbar singleton to the ui object"""
1521 1521 if (self.quiet or self.debugflag
1522 1522 or self.configbool('progress', 'disable')
1523 1523 or not progress.shouldprint(self)):
1524 1524 return None
1525 1525 return getprogbar(self)
1526 1526
1527 1527 def _progclear(self):
1528 1528 """clear progress bar output if any. use it before any output"""
1529 1529 if not haveprogbar(): # nothing loaded yet
1530 1530 return
1531 1531 if self._progbar is not None and self._progbar.printed:
1532 1532 self._progbar.clear()
1533 1533
1534 1534 def progress(self, topic, pos, item="", unit="", total=None):
1535 1535 '''show a progress message
1536 1536
1537 1537 By default a textual progress bar will be displayed if an operation
1538 1538 takes too long. 'topic' is the current operation, 'item' is a
1539 1539 non-numeric marker of the current position (i.e. the currently
1540 1540 in-process file), 'pos' is the current numeric position (i.e.
1541 1541 revision, bytes, etc.), unit is a corresponding unit label,
1542 1542 and total is the highest expected pos.
1543 1543
1544 1544 Multiple nested topics may be active at a time.
1545 1545
1546 1546 All topics should be marked closed by setting pos to None at
1547 1547 termination.
1548 1548 '''
1549 1549 if self._progbar is not None:
1550 1550 self._progbar.progress(topic, pos, item=item, unit=unit,
1551 1551 total=total)
1552 1552 if pos is None or not self.configbool('progress', 'debug'):
1553 1553 return
1554 1554
1555 1555 if unit:
1556 1556 unit = ' ' + unit
1557 1557 if item:
1558 1558 item = ' ' + item
1559 1559
1560 1560 if total:
1561 1561 pct = 100.0 * pos / total
1562 1562 self.debug('%s:%s %d/%d%s (%4.2f%%)\n'
1563 1563 % (topic, item, pos, total, unit, pct))
1564 1564 else:
1565 1565 self.debug('%s:%s %d%s\n' % (topic, item, pos, unit))
1566 1566
1567 1567 def log(self, service, *msg, **opts):
1568 1568 '''hook for logging facility extensions
1569 1569
1570 1570 service should be a readily-identifiable subsystem, which will
1571 1571 allow filtering.
1572 1572
1573 1573 *msg should be a newline-terminated format string to log, and
1574 1574 then any values to %-format into that format string.
1575 1575
1576 1576 **opts currently has no defined meanings.
1577 1577 '''
1578 1578
1579 1579 def label(self, msg, label):
1580 1580 '''style msg based on supplied label
1581 1581
1582 1582 If some color mode is enabled, this will add the necessary control
1583 1583 characters to apply such color. In addition, 'debug' color mode adds
1584 1584 markup showing which label affects a piece of text.
1585 1585
1586 1586 ui.write(s, 'label') is equivalent to
1587 1587 ui.write(ui.label(s, 'label')).
1588 1588 '''
1589 1589 if self._colormode is not None:
1590 1590 return color.colorlabel(self, msg, label)
1591 1591 return msg
1592 1592
1593 1593 def develwarn(self, msg, stacklevel=1, config=None):
1594 1594 """issue a developer warning message
1595 1595
1596 1596 Use 'stacklevel' to report the offender some layers further up in the
1597 1597 stack.
1598 1598 """
1599 1599 if not self.configbool('devel', 'all-warnings'):
1600 1600 if config is None or not self.configbool('devel', config):
1601 1601 return
1602 1602 msg = 'devel-warn: ' + msg
1603 1603 stacklevel += 1 # get in develwarn
1604 1604 if self.tracebackflag:
1605 1605 util.debugstacktrace(msg, stacklevel, self.ferr, self.fout)
1606 1606 self.log('develwarn', '%s at:\n%s' %
1607 1607 (msg, ''.join(util.getstackframes(stacklevel))))
1608 1608 else:
1609 1609 curframe = inspect.currentframe()
1610 1610 calframe = inspect.getouterframes(curframe, 2)
1611 1611 self.write_err('%s at: %s:%s (%s)\n'
1612 1612 % ((msg,) + calframe[stacklevel][1:4]))
1613 1613 self.log('develwarn', '%s at: %s:%s (%s)\n',
1614 1614 msg, *calframe[stacklevel][1:4])
1615 1615 curframe = calframe = None # avoid cycles
1616 1616
1617 def deprecwarn(self, msg, version):
1617 def deprecwarn(self, msg, version, stacklevel=2):
1618 1618 """issue a deprecation warning
1619 1619
1620 1620 - msg: message explaining what is deprecated and how to upgrade,
1621 1621 - version: last version where the API will be supported,
1622 1622 """
1623 1623 if not (self.configbool('devel', 'all-warnings')
1624 1624 or self.configbool('devel', 'deprec-warn')):
1625 1625 return
1626 1626 msg += ("\n(compatibility will be dropped after Mercurial-%s,"
1627 1627 " update your code.)") % version
1628 self.develwarn(msg, stacklevel=2, config='deprec-warn')
1628 self.develwarn(msg, stacklevel=stacklevel, config='deprec-warn')
1629 1629
1630 1630 def exportableenviron(self):
1631 1631 """The environment variables that are safe to export, e.g. through
1632 1632 hgweb.
1633 1633 """
1634 1634 return self._exportableenviron
1635 1635
1636 1636 @contextlib.contextmanager
1637 1637 def configoverride(self, overrides, source=""):
1638 1638 """Context manager for temporary config overrides
1639 1639 `overrides` must be a dict of the following structure:
1640 1640 {(section, name) : value}"""
1641 1641 backups = {}
1642 1642 try:
1643 1643 for (section, name), value in overrides.items():
1644 1644 backups[(section, name)] = self.backupconfig(section, name)
1645 1645 self.setconfig(section, name, value, source)
1646 1646 yield
1647 1647 finally:
1648 1648 for __, backup in backups.items():
1649 1649 self.restoreconfig(backup)
1650 1650 # just restoring ui.quiet config to the previous value is not enough
1651 1651 # as it does not update ui.quiet class member
1652 1652 if ('ui', 'quiet') in overrides:
1653 1653 self.fixconfig(section='ui')
1654 1654
1655 1655 class paths(dict):
1656 1656 """Represents a collection of paths and their configs.
1657 1657
1658 1658 Data is initially derived from ui instances and the config files they have
1659 1659 loaded.
1660 1660 """
1661 1661 def __init__(self, ui):
1662 1662 dict.__init__(self)
1663 1663
1664 1664 for name, loc in ui.configitems('paths', ignoresub=True):
1665 1665 # No location is the same as not existing.
1666 1666 if not loc:
1667 1667 continue
1668 1668 loc, sub = ui.configsuboptions('paths', name)
1669 1669 self[name] = path(ui, name, rawloc=loc, suboptions=sub)
1670 1670
1671 1671 def getpath(self, name, default=None):
1672 1672 """Return a ``path`` from a string, falling back to default.
1673 1673
1674 1674 ``name`` can be a named path or locations. Locations are filesystem
1675 1675 paths or URIs.
1676 1676
1677 1677 Returns None if ``name`` is not a registered path, a URI, or a local
1678 1678 path to a repo.
1679 1679 """
1680 1680 # Only fall back to default if no path was requested.
1681 1681 if name is None:
1682 1682 if not default:
1683 1683 default = ()
1684 1684 elif not isinstance(default, (tuple, list)):
1685 1685 default = (default,)
1686 1686 for k in default:
1687 1687 try:
1688 1688 return self[k]
1689 1689 except KeyError:
1690 1690 continue
1691 1691 return None
1692 1692
1693 1693 # Most likely empty string.
1694 1694 # This may need to raise in the future.
1695 1695 if not name:
1696 1696 return None
1697 1697
1698 1698 try:
1699 1699 return self[name]
1700 1700 except KeyError:
1701 1701 # Try to resolve as a local path or URI.
1702 1702 try:
1703 1703 # We don't pass sub-options in, so no need to pass ui instance.
1704 1704 return path(None, None, rawloc=name)
1705 1705 except ValueError:
1706 1706 raise error.RepoError(_('repository %s does not exist') %
1707 1707 name)
1708 1708
1709 1709 _pathsuboptions = {}
1710 1710
1711 1711 def pathsuboption(option, attr):
1712 1712 """Decorator used to declare a path sub-option.
1713 1713
1714 1714 Arguments are the sub-option name and the attribute it should set on
1715 1715 ``path`` instances.
1716 1716
1717 1717 The decorated function will receive as arguments a ``ui`` instance,
1718 1718 ``path`` instance, and the string value of this option from the config.
1719 1719 The function should return the value that will be set on the ``path``
1720 1720 instance.
1721 1721
1722 1722 This decorator can be used to perform additional verification of
1723 1723 sub-options and to change the type of sub-options.
1724 1724 """
1725 1725 def register(func):
1726 1726 _pathsuboptions[option] = (attr, func)
1727 1727 return func
1728 1728 return register
1729 1729
1730 1730 @pathsuboption('pushurl', 'pushloc')
1731 1731 def pushurlpathoption(ui, path, value):
1732 1732 u = util.url(value)
1733 1733 # Actually require a URL.
1734 1734 if not u.scheme:
1735 1735 ui.warn(_('(paths.%s:pushurl not a URL; ignoring)\n') % path.name)
1736 1736 return None
1737 1737
1738 1738 # Don't support the #foo syntax in the push URL to declare branch to
1739 1739 # push.
1740 1740 if u.fragment:
1741 1741 ui.warn(_('("#fragment" in paths.%s:pushurl not supported; '
1742 1742 'ignoring)\n') % path.name)
1743 1743 u.fragment = None
1744 1744
1745 1745 return str(u)
1746 1746
1747 1747 @pathsuboption('pushrev', 'pushrev')
1748 1748 def pushrevpathoption(ui, path, value):
1749 1749 return value
1750 1750
1751 1751 class path(object):
1752 1752 """Represents an individual path and its configuration."""
1753 1753
1754 1754 def __init__(self, ui, name, rawloc=None, suboptions=None):
1755 1755 """Construct a path from its config options.
1756 1756
1757 1757 ``ui`` is the ``ui`` instance the path is coming from.
1758 1758 ``name`` is the symbolic name of the path.
1759 1759 ``rawloc`` is the raw location, as defined in the config.
1760 1760 ``pushloc`` is the raw locations pushes should be made to.
1761 1761
1762 1762 If ``name`` is not defined, we require that the location be a) a local
1763 1763 filesystem path with a .hg directory or b) a URL. If not,
1764 1764 ``ValueError`` is raised.
1765 1765 """
1766 1766 if not rawloc:
1767 1767 raise ValueError('rawloc must be defined')
1768 1768
1769 1769 # Locations may define branches via syntax <base>#<branch>.
1770 1770 u = util.url(rawloc)
1771 1771 branch = None
1772 1772 if u.fragment:
1773 1773 branch = u.fragment
1774 1774 u.fragment = None
1775 1775
1776 1776 self.url = u
1777 1777 self.branch = branch
1778 1778
1779 1779 self.name = name
1780 1780 self.rawloc = rawloc
1781 1781 self.loc = '%s' % u
1782 1782
1783 1783 # When given a raw location but not a symbolic name, validate the
1784 1784 # location is valid.
1785 1785 if not name and not u.scheme and not self._isvalidlocalpath(self.loc):
1786 1786 raise ValueError('location is not a URL or path to a local '
1787 1787 'repo: %s' % rawloc)
1788 1788
1789 1789 suboptions = suboptions or {}
1790 1790
1791 1791 # Now process the sub-options. If a sub-option is registered, its
1792 1792 # attribute will always be present. The value will be None if there
1793 1793 # was no valid sub-option.
1794 1794 for suboption, (attr, func) in _pathsuboptions.iteritems():
1795 1795 if suboption not in suboptions:
1796 1796 setattr(self, attr, None)
1797 1797 continue
1798 1798
1799 1799 value = func(ui, self, suboptions[suboption])
1800 1800 setattr(self, attr, value)
1801 1801
1802 1802 def _isvalidlocalpath(self, path):
1803 1803 """Returns True if the given path is a potentially valid repository.
1804 1804 This is its own function so that extensions can change the definition of
1805 1805 'valid' in this case (like when pulling from a git repo into a hg
1806 1806 one)."""
1807 1807 return os.path.isdir(os.path.join(path, '.hg'))
1808 1808
1809 1809 @property
1810 1810 def suboptions(self):
1811 1811 """Return sub-options and their values for this path.
1812 1812
1813 1813 This is intended to be used for presentation purposes.
1814 1814 """
1815 1815 d = {}
1816 1816 for subopt, (attr, _func) in _pathsuboptions.iteritems():
1817 1817 value = getattr(self, attr)
1818 1818 if value is not None:
1819 1819 d[subopt] = value
1820 1820 return d
1821 1821
1822 1822 # we instantiate one globally shared progress bar to avoid
1823 1823 # competing progress bars when multiple UI objects get created
1824 1824 _progresssingleton = None
1825 1825
1826 1826 def getprogbar(ui):
1827 1827 global _progresssingleton
1828 1828 if _progresssingleton is None:
1829 1829 # passing 'ui' object to the singleton is fishy,
1830 1830 # this is how the extension used to work but feel free to rework it.
1831 1831 _progresssingleton = progress.progbar(ui)
1832 1832 return _progresssingleton
1833 1833
1834 1834 def haveprogbar():
1835 1835 return _progresssingleton is not None
General Comments 0
You need to be logged in to leave comments. Login now