##// END OF EJS Templates
cmdutil: remove forwarding methods per deprecation policy
Augie Fackler -
r30877:8d7e4052 default
parent child Browse files
Show More
@@ -1,3486 +1,3472 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 bin,
18 18 hex,
19 19 nullid,
20 20 nullrev,
21 21 short,
22 22 )
23 23
24 24 from . import (
25 25 bookmarks,
26 26 changelog,
27 27 copies,
28 28 crecord as crecordmod,
29 dirstateguard as dirstateguardmod,
30 29 encoding,
31 30 error,
32 31 formatter,
33 32 graphmod,
34 33 lock as lockmod,
35 34 match as matchmod,
36 mergeutil,
37 35 obsolete,
38 36 patch,
39 37 pathutil,
40 38 phases,
41 39 pycompat,
42 40 repair,
43 41 revlog,
44 42 revset,
45 43 scmutil,
46 44 templatekw,
47 45 templater,
48 46 util,
49 47 )
50 48 stringio = util.stringio
51 49
52 50 # special string such that everything below this line will be ingored in the
53 51 # editor text
54 52 _linebelow = "^HG: ------------------------ >8 ------------------------$"
55 53
56 54 def ishunk(x):
57 55 hunkclasses = (crecordmod.uihunk, patch.recordhunk)
58 56 return isinstance(x, hunkclasses)
59 57
60 58 def newandmodified(chunks, originalchunks):
61 59 newlyaddedandmodifiedfiles = set()
62 60 for chunk in chunks:
63 61 if ishunk(chunk) and chunk.header.isnewfile() and chunk not in \
64 62 originalchunks:
65 63 newlyaddedandmodifiedfiles.add(chunk.header.filename())
66 64 return newlyaddedandmodifiedfiles
67 65
68 66 def parsealiases(cmd):
69 67 return cmd.lstrip("^").split("|")
70 68
71 69 def setupwrapcolorwrite(ui):
72 70 # wrap ui.write so diff output can be labeled/colorized
73 71 def wrapwrite(orig, *args, **kw):
74 72 label = kw.pop('label', '')
75 73 for chunk, l in patch.difflabel(lambda: args):
76 74 orig(chunk, label=label + l)
77 75
78 76 oldwrite = ui.write
79 77 def wrap(*args, **kwargs):
80 78 return wrapwrite(oldwrite, *args, **kwargs)
81 79 setattr(ui, 'write', wrap)
82 80 return oldwrite
83 81
84 82 def filterchunks(ui, originalhunks, usecurses, testfile, operation=None):
85 83 if usecurses:
86 84 if testfile:
87 85 recordfn = crecordmod.testdecorator(testfile,
88 86 crecordmod.testchunkselector)
89 87 else:
90 88 recordfn = crecordmod.chunkselector
91 89
92 90 return crecordmod.filterpatch(ui, originalhunks, recordfn, operation)
93 91
94 92 else:
95 93 return patch.filterpatch(ui, originalhunks, operation)
96 94
97 95 def recordfilter(ui, originalhunks, operation=None):
98 96 """ Prompts the user to filter the originalhunks and return a list of
99 97 selected hunks.
100 98 *operation* is used for to build ui messages to indicate the user what
101 99 kind of filtering they are doing: reverting, committing, shelving, etc.
102 100 (see patch.filterpatch).
103 101 """
104 102 usecurses = crecordmod.checkcurses(ui)
105 103 testfile = ui.config('experimental', 'crecordtest', None)
106 104 oldwrite = setupwrapcolorwrite(ui)
107 105 try:
108 106 newchunks, newopts = filterchunks(ui, originalhunks, usecurses,
109 107 testfile, operation)
110 108 finally:
111 109 ui.write = oldwrite
112 110 return newchunks, newopts
113 111
114 112 def dorecord(ui, repo, commitfunc, cmdsuggest, backupall,
115 113 filterfn, *pats, **opts):
116 114 from . import merge as mergemod
117 115 if not ui.interactive():
118 116 if cmdsuggest:
119 117 msg = _('running non-interactively, use %s instead') % cmdsuggest
120 118 else:
121 119 msg = _('running non-interactively')
122 120 raise error.Abort(msg)
123 121
124 122 # make sure username is set before going interactive
125 123 if not opts.get('user'):
126 124 ui.username() # raise exception, username not provided
127 125
128 126 def recordfunc(ui, repo, message, match, opts):
129 127 """This is generic record driver.
130 128
131 129 Its job is to interactively filter local changes, and
132 130 accordingly prepare working directory into a state in which the
133 131 job can be delegated to a non-interactive commit command such as
134 132 'commit' or 'qrefresh'.
135 133
136 134 After the actual job is done by non-interactive command, the
137 135 working directory is restored to its original state.
138 136
139 137 In the end we'll record interesting changes, and everything else
140 138 will be left in place, so the user can continue working.
141 139 """
142 140
143 141 checkunfinished(repo, commit=True)
144 142 wctx = repo[None]
145 143 merge = len(wctx.parents()) > 1
146 144 if merge:
147 145 raise error.Abort(_('cannot partially commit a merge '
148 146 '(use "hg commit" instead)'))
149 147
150 148 def fail(f, msg):
151 149 raise error.Abort('%s: %s' % (f, msg))
152 150
153 151 force = opts.get('force')
154 152 if not force:
155 153 vdirs = []
156 154 match.explicitdir = vdirs.append
157 155 match.bad = fail
158 156
159 157 status = repo.status(match=match)
160 158 if not force:
161 159 repo.checkcommitpatterns(wctx, vdirs, match, status, fail)
162 160 diffopts = patch.difffeatureopts(ui, opts=opts, whitespace=True)
163 161 diffopts.nodates = True
164 162 diffopts.git = True
165 163 diffopts.showfunc = True
166 164 originaldiff = patch.diff(repo, changes=status, opts=diffopts)
167 165 originalchunks = patch.parsepatch(originaldiff)
168 166
169 167 # 1. filter patch, since we are intending to apply subset of it
170 168 try:
171 169 chunks, newopts = filterfn(ui, originalchunks)
172 170 except patch.PatchError as err:
173 171 raise error.Abort(_('error parsing patch: %s') % err)
174 172 opts.update(newopts)
175 173
176 174 # We need to keep a backup of files that have been newly added and
177 175 # modified during the recording process because there is a previous
178 176 # version without the edit in the workdir
179 177 newlyaddedandmodifiedfiles = newandmodified(chunks, originalchunks)
180 178 contenders = set()
181 179 for h in chunks:
182 180 try:
183 181 contenders.update(set(h.files()))
184 182 except AttributeError:
185 183 pass
186 184
187 185 changed = status.modified + status.added + status.removed
188 186 newfiles = [f for f in changed if f in contenders]
189 187 if not newfiles:
190 188 ui.status(_('no changes to record\n'))
191 189 return 0
192 190
193 191 modified = set(status.modified)
194 192
195 193 # 2. backup changed files, so we can restore them in the end
196 194
197 195 if backupall:
198 196 tobackup = changed
199 197 else:
200 198 tobackup = [f for f in newfiles if f in modified or f in \
201 199 newlyaddedandmodifiedfiles]
202 200 backups = {}
203 201 if tobackup:
204 202 backupdir = repo.join('record-backups')
205 203 try:
206 204 os.mkdir(backupdir)
207 205 except OSError as err:
208 206 if err.errno != errno.EEXIST:
209 207 raise
210 208 try:
211 209 # backup continues
212 210 for f in tobackup:
213 211 fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.',
214 212 dir=backupdir)
215 213 os.close(fd)
216 214 ui.debug('backup %r as %r\n' % (f, tmpname))
217 215 util.copyfile(repo.wjoin(f), tmpname, copystat=True)
218 216 backups[f] = tmpname
219 217
220 218 fp = stringio()
221 219 for c in chunks:
222 220 fname = c.filename()
223 221 if fname in backups:
224 222 c.write(fp)
225 223 dopatch = fp.tell()
226 224 fp.seek(0)
227 225
228 226 # 2.5 optionally review / modify patch in text editor
229 227 if opts.get('review', False):
230 228 patchtext = (crecordmod.diffhelptext
231 229 + crecordmod.patchhelptext
232 230 + fp.read())
233 231 reviewedpatch = ui.edit(patchtext, "",
234 232 extra={"suffix": ".diff"},
235 233 repopath=repo.path)
236 234 fp.truncate(0)
237 235 fp.write(reviewedpatch)
238 236 fp.seek(0)
239 237
240 238 [os.unlink(repo.wjoin(c)) for c in newlyaddedandmodifiedfiles]
241 239 # 3a. apply filtered patch to clean repo (clean)
242 240 if backups:
243 241 # Equivalent to hg.revert
244 242 m = scmutil.matchfiles(repo, backups.keys())
245 243 mergemod.update(repo, repo.dirstate.p1(),
246 244 False, True, matcher=m)
247 245
248 246 # 3b. (apply)
249 247 if dopatch:
250 248 try:
251 249 ui.debug('applying patch\n')
252 250 ui.debug(fp.getvalue())
253 251 patch.internalpatch(ui, repo, fp, 1, eolmode=None)
254 252 except patch.PatchError as err:
255 253 raise error.Abort(str(err))
256 254 del fp
257 255
258 256 # 4. We prepared working directory according to filtered
259 257 # patch. Now is the time to delegate the job to
260 258 # commit/qrefresh or the like!
261 259
262 260 # Make all of the pathnames absolute.
263 261 newfiles = [repo.wjoin(nf) for nf in newfiles]
264 262 return commitfunc(ui, repo, *newfiles, **opts)
265 263 finally:
266 264 # 5. finally restore backed-up files
267 265 try:
268 266 dirstate = repo.dirstate
269 267 for realname, tmpname in backups.iteritems():
270 268 ui.debug('restoring %r to %r\n' % (tmpname, realname))
271 269
272 270 if dirstate[realname] == 'n':
273 271 # without normallookup, restoring timestamp
274 272 # may cause partially committed files
275 273 # to be treated as unmodified
276 274 dirstate.normallookup(realname)
277 275
278 276 # copystat=True here and above are a hack to trick any
279 277 # editors that have f open that we haven't modified them.
280 278 #
281 279 # Also note that this racy as an editor could notice the
282 280 # file's mtime before we've finished writing it.
283 281 util.copyfile(tmpname, repo.wjoin(realname), copystat=True)
284 282 os.unlink(tmpname)
285 283 if tobackup:
286 284 os.rmdir(backupdir)
287 285 except OSError:
288 286 pass
289 287
290 288 def recordinwlock(ui, repo, message, match, opts):
291 289 with repo.wlock():
292 290 return recordfunc(ui, repo, message, match, opts)
293 291
294 292 return commit(ui, repo, recordinwlock, pats, opts)
295 293
296 294 def findpossible(cmd, table, strict=False):
297 295 """
298 296 Return cmd -> (aliases, command table entry)
299 297 for each matching command.
300 298 Return debug commands (or their aliases) only if no normal command matches.
301 299 """
302 300 choice = {}
303 301 debugchoice = {}
304 302
305 303 if cmd in table:
306 304 # short-circuit exact matches, "log" alias beats "^log|history"
307 305 keys = [cmd]
308 306 else:
309 307 keys = table.keys()
310 308
311 309 allcmds = []
312 310 for e in keys:
313 311 aliases = parsealiases(e)
314 312 allcmds.extend(aliases)
315 313 found = None
316 314 if cmd in aliases:
317 315 found = cmd
318 316 elif not strict:
319 317 for a in aliases:
320 318 if a.startswith(cmd):
321 319 found = a
322 320 break
323 321 if found is not None:
324 322 if aliases[0].startswith("debug") or found.startswith("debug"):
325 323 debugchoice[found] = (aliases, table[e])
326 324 else:
327 325 choice[found] = (aliases, table[e])
328 326
329 327 if not choice and debugchoice:
330 328 choice = debugchoice
331 329
332 330 return choice, allcmds
333 331
334 332 def findcmd(cmd, table, strict=True):
335 333 """Return (aliases, command table entry) for command string."""
336 334 choice, allcmds = findpossible(cmd, table, strict)
337 335
338 336 if cmd in choice:
339 337 return choice[cmd]
340 338
341 339 if len(choice) > 1:
342 340 clist = choice.keys()
343 341 clist.sort()
344 342 raise error.AmbiguousCommand(cmd, clist)
345 343
346 344 if choice:
347 345 return choice.values()[0]
348 346
349 347 raise error.UnknownCommand(cmd, allcmds)
350 348
351 349 def findrepo(p):
352 350 while not os.path.isdir(os.path.join(p, ".hg")):
353 351 oldp, p = p, os.path.dirname(p)
354 352 if p == oldp:
355 353 return None
356 354
357 355 return p
358 356
359 357 def bailifchanged(repo, merge=True, hint=None):
360 358 """ enforce the precondition that working directory must be clean.
361 359
362 360 'merge' can be set to false if a pending uncommitted merge should be
363 361 ignored (such as when 'update --check' runs).
364 362
365 363 'hint' is the usual hint given to Abort exception.
366 364 """
367 365
368 366 if merge and repo.dirstate.p2() != nullid:
369 367 raise error.Abort(_('outstanding uncommitted merge'), hint=hint)
370 368 modified, added, removed, deleted = repo.status()[:4]
371 369 if modified or added or removed or deleted:
372 370 raise error.Abort(_('uncommitted changes'), hint=hint)
373 371 ctx = repo[None]
374 372 for s in sorted(ctx.substate):
375 373 ctx.sub(s).bailifchanged(hint=hint)
376 374
377 375 def logmessage(ui, opts):
378 376 """ get the log message according to -m and -l option """
379 377 message = opts.get('message')
380 378 logfile = opts.get('logfile')
381 379
382 380 if message and logfile:
383 381 raise error.Abort(_('options --message and --logfile are mutually '
384 382 'exclusive'))
385 383 if not message and logfile:
386 384 try:
387 385 if logfile == '-':
388 386 message = ui.fin.read()
389 387 else:
390 388 message = '\n'.join(util.readfile(logfile).splitlines())
391 389 except IOError as inst:
392 390 raise error.Abort(_("can't read commit message '%s': %s") %
393 391 (logfile, inst.strerror))
394 392 return message
395 393
396 394 def mergeeditform(ctxorbool, baseformname):
397 395 """return appropriate editform name (referencing a committemplate)
398 396
399 397 'ctxorbool' is either a ctx to be committed, or a bool indicating whether
400 398 merging is committed.
401 399
402 400 This returns baseformname with '.merge' appended if it is a merge,
403 401 otherwise '.normal' is appended.
404 402 """
405 403 if isinstance(ctxorbool, bool):
406 404 if ctxorbool:
407 405 return baseformname + ".merge"
408 406 elif 1 < len(ctxorbool.parents()):
409 407 return baseformname + ".merge"
410 408
411 409 return baseformname + ".normal"
412 410
413 411 def getcommiteditor(edit=False, finishdesc=None, extramsg=None,
414 412 editform='', **opts):
415 413 """get appropriate commit message editor according to '--edit' option
416 414
417 415 'finishdesc' is a function to be called with edited commit message
418 416 (= 'description' of the new changeset) just after editing, but
419 417 before checking empty-ness. It should return actual text to be
420 418 stored into history. This allows to change description before
421 419 storing.
422 420
423 421 'extramsg' is a extra message to be shown in the editor instead of
424 422 'Leave message empty to abort commit' line. 'HG: ' prefix and EOL
425 423 is automatically added.
426 424
427 425 'editform' is a dot-separated list of names, to distinguish
428 426 the purpose of commit text editing.
429 427
430 428 'getcommiteditor' returns 'commitforceeditor' regardless of
431 429 'edit', if one of 'finishdesc' or 'extramsg' is specified, because
432 430 they are specific for usage in MQ.
433 431 """
434 432 if edit or finishdesc or extramsg:
435 433 return lambda r, c, s: commitforceeditor(r, c, s,
436 434 finishdesc=finishdesc,
437 435 extramsg=extramsg,
438 436 editform=editform)
439 437 elif editform:
440 438 return lambda r, c, s: commiteditor(r, c, s, editform=editform)
441 439 else:
442 440 return commiteditor
443 441
444 442 def loglimit(opts):
445 443 """get the log limit according to option -l/--limit"""
446 444 limit = opts.get('limit')
447 445 if limit:
448 446 try:
449 447 limit = int(limit)
450 448 except ValueError:
451 449 raise error.Abort(_('limit must be a positive integer'))
452 450 if limit <= 0:
453 451 raise error.Abort(_('limit must be positive'))
454 452 else:
455 453 limit = None
456 454 return limit
457 455
458 456 def makefilename(repo, pat, node, desc=None,
459 457 total=None, seqno=None, revwidth=None, pathname=None):
460 458 node_expander = {
461 459 'H': lambda: hex(node),
462 460 'R': lambda: str(repo.changelog.rev(node)),
463 461 'h': lambda: short(node),
464 462 'm': lambda: re.sub('[^\w]', '_', str(desc))
465 463 }
466 464 expander = {
467 465 '%': lambda: '%',
468 466 'b': lambda: os.path.basename(repo.root),
469 467 }
470 468
471 469 try:
472 470 if node:
473 471 expander.update(node_expander)
474 472 if node:
475 473 expander['r'] = (lambda:
476 474 str(repo.changelog.rev(node)).zfill(revwidth or 0))
477 475 if total is not None:
478 476 expander['N'] = lambda: str(total)
479 477 if seqno is not None:
480 478 expander['n'] = lambda: str(seqno)
481 479 if total is not None and seqno is not None:
482 480 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
483 481 if pathname is not None:
484 482 expander['s'] = lambda: os.path.basename(pathname)
485 483 expander['d'] = lambda: os.path.dirname(pathname) or '.'
486 484 expander['p'] = lambda: pathname
487 485
488 486 newname = []
489 487 patlen = len(pat)
490 488 i = 0
491 489 while i < patlen:
492 490 c = pat[i]
493 491 if c == '%':
494 492 i += 1
495 493 c = pat[i]
496 494 c = expander[c]()
497 495 newname.append(c)
498 496 i += 1
499 497 return ''.join(newname)
500 498 except KeyError as inst:
501 499 raise error.Abort(_("invalid format spec '%%%s' in output filename") %
502 500 inst.args[0])
503 501
504 502 class _unclosablefile(object):
505 503 def __init__(self, fp):
506 504 self._fp = fp
507 505
508 506 def close(self):
509 507 pass
510 508
511 509 def __iter__(self):
512 510 return iter(self._fp)
513 511
514 512 def __getattr__(self, attr):
515 513 return getattr(self._fp, attr)
516 514
517 515 def __enter__(self):
518 516 return self
519 517
520 518 def __exit__(self, exc_type, exc_value, exc_tb):
521 519 pass
522 520
523 521 def makefileobj(repo, pat, node=None, desc=None, total=None,
524 522 seqno=None, revwidth=None, mode='wb', modemap=None,
525 523 pathname=None):
526 524
527 525 writable = mode not in ('r', 'rb')
528 526
529 527 if not pat or pat == '-':
530 528 if writable:
531 529 fp = repo.ui.fout
532 530 else:
533 531 fp = repo.ui.fin
534 532 return _unclosablefile(fp)
535 533 if util.safehasattr(pat, 'write') and writable:
536 534 return pat
537 535 if util.safehasattr(pat, 'read') and 'r' in mode:
538 536 return pat
539 537 fn = makefilename(repo, pat, node, desc, total, seqno, revwidth, pathname)
540 538 if modemap is not None:
541 539 mode = modemap.get(fn, mode)
542 540 if mode == 'wb':
543 541 modemap[fn] = 'ab'
544 542 return open(fn, mode)
545 543
546 544 def openrevlog(repo, cmd, file_, opts):
547 545 """opens the changelog, manifest, a filelog or a given revlog"""
548 546 cl = opts['changelog']
549 547 mf = opts['manifest']
550 548 dir = opts['dir']
551 549 msg = None
552 550 if cl and mf:
553 551 msg = _('cannot specify --changelog and --manifest at the same time')
554 552 elif cl and dir:
555 553 msg = _('cannot specify --changelog and --dir at the same time')
556 554 elif cl or mf or dir:
557 555 if file_:
558 556 msg = _('cannot specify filename with --changelog or --manifest')
559 557 elif not repo:
560 558 msg = _('cannot specify --changelog or --manifest or --dir '
561 559 'without a repository')
562 560 if msg:
563 561 raise error.Abort(msg)
564 562
565 563 r = None
566 564 if repo:
567 565 if cl:
568 566 r = repo.unfiltered().changelog
569 567 elif dir:
570 568 if 'treemanifest' not in repo.requirements:
571 569 raise error.Abort(_("--dir can only be used on repos with "
572 570 "treemanifest enabled"))
573 571 dirlog = repo.manifestlog._revlog.dirlog(dir)
574 572 if len(dirlog):
575 573 r = dirlog
576 574 elif mf:
577 575 r = repo.manifestlog._revlog
578 576 elif file_:
579 577 filelog = repo.file(file_)
580 578 if len(filelog):
581 579 r = filelog
582 580 if not r:
583 581 if not file_:
584 582 raise error.CommandError(cmd, _('invalid arguments'))
585 583 if not os.path.isfile(file_):
586 584 raise error.Abort(_("revlog '%s' not found") % file_)
587 585 r = revlog.revlog(scmutil.opener(pycompat.getcwd(), audit=False),
588 586 file_[:-2] + ".i")
589 587 return r
590 588
591 589 def copy(ui, repo, pats, opts, rename=False):
592 590 # called with the repo lock held
593 591 #
594 592 # hgsep => pathname that uses "/" to separate directories
595 593 # ossep => pathname that uses os.sep to separate directories
596 594 cwd = repo.getcwd()
597 595 targets = {}
598 596 after = opts.get("after")
599 597 dryrun = opts.get("dry_run")
600 598 wctx = repo[None]
601 599
602 600 def walkpat(pat):
603 601 srcs = []
604 602 if after:
605 603 badstates = '?'
606 604 else:
607 605 badstates = '?r'
608 606 m = scmutil.match(repo[None], [pat], opts, globbed=True)
609 607 for abs in repo.walk(m):
610 608 state = repo.dirstate[abs]
611 609 rel = m.rel(abs)
612 610 exact = m.exact(abs)
613 611 if state in badstates:
614 612 if exact and state == '?':
615 613 ui.warn(_('%s: not copying - file is not managed\n') % rel)
616 614 if exact and state == 'r':
617 615 ui.warn(_('%s: not copying - file has been marked for'
618 616 ' remove\n') % rel)
619 617 continue
620 618 # abs: hgsep
621 619 # rel: ossep
622 620 srcs.append((abs, rel, exact))
623 621 return srcs
624 622
625 623 # abssrc: hgsep
626 624 # relsrc: ossep
627 625 # otarget: ossep
628 626 def copyfile(abssrc, relsrc, otarget, exact):
629 627 abstarget = pathutil.canonpath(repo.root, cwd, otarget)
630 628 if '/' in abstarget:
631 629 # We cannot normalize abstarget itself, this would prevent
632 630 # case only renames, like a => A.
633 631 abspath, absname = abstarget.rsplit('/', 1)
634 632 abstarget = repo.dirstate.normalize(abspath) + '/' + absname
635 633 reltarget = repo.pathto(abstarget, cwd)
636 634 target = repo.wjoin(abstarget)
637 635 src = repo.wjoin(abssrc)
638 636 state = repo.dirstate[abstarget]
639 637
640 638 scmutil.checkportable(ui, abstarget)
641 639
642 640 # check for collisions
643 641 prevsrc = targets.get(abstarget)
644 642 if prevsrc is not None:
645 643 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
646 644 (reltarget, repo.pathto(abssrc, cwd),
647 645 repo.pathto(prevsrc, cwd)))
648 646 return
649 647
650 648 # check for overwrites
651 649 exists = os.path.lexists(target)
652 650 samefile = False
653 651 if exists and abssrc != abstarget:
654 652 if (repo.dirstate.normalize(abssrc) ==
655 653 repo.dirstate.normalize(abstarget)):
656 654 if not rename:
657 655 ui.warn(_("%s: can't copy - same file\n") % reltarget)
658 656 return
659 657 exists = False
660 658 samefile = True
661 659
662 660 if not after and exists or after and state in 'mn':
663 661 if not opts['force']:
664 662 if state in 'mn':
665 663 msg = _('%s: not overwriting - file already committed\n')
666 664 if after:
667 665 flags = '--after --force'
668 666 else:
669 667 flags = '--force'
670 668 if rename:
671 669 hint = _('(hg rename %s to replace the file by '
672 670 'recording a rename)\n') % flags
673 671 else:
674 672 hint = _('(hg copy %s to replace the file by '
675 673 'recording a copy)\n') % flags
676 674 else:
677 675 msg = _('%s: not overwriting - file exists\n')
678 676 if rename:
679 677 hint = _('(hg rename --after to record the rename)\n')
680 678 else:
681 679 hint = _('(hg copy --after to record the copy)\n')
682 680 ui.warn(msg % reltarget)
683 681 ui.warn(hint)
684 682 return
685 683
686 684 if after:
687 685 if not exists:
688 686 if rename:
689 687 ui.warn(_('%s: not recording move - %s does not exist\n') %
690 688 (relsrc, reltarget))
691 689 else:
692 690 ui.warn(_('%s: not recording copy - %s does not exist\n') %
693 691 (relsrc, reltarget))
694 692 return
695 693 elif not dryrun:
696 694 try:
697 695 if exists:
698 696 os.unlink(target)
699 697 targetdir = os.path.dirname(target) or '.'
700 698 if not os.path.isdir(targetdir):
701 699 os.makedirs(targetdir)
702 700 if samefile:
703 701 tmp = target + "~hgrename"
704 702 os.rename(src, tmp)
705 703 os.rename(tmp, target)
706 704 else:
707 705 util.copyfile(src, target)
708 706 srcexists = True
709 707 except IOError as inst:
710 708 if inst.errno == errno.ENOENT:
711 709 ui.warn(_('%s: deleted in working directory\n') % relsrc)
712 710 srcexists = False
713 711 else:
714 712 ui.warn(_('%s: cannot copy - %s\n') %
715 713 (relsrc, inst.strerror))
716 714 return True # report a failure
717 715
718 716 if ui.verbose or not exact:
719 717 if rename:
720 718 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
721 719 else:
722 720 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
723 721
724 722 targets[abstarget] = abssrc
725 723
726 724 # fix up dirstate
727 725 scmutil.dirstatecopy(ui, repo, wctx, abssrc, abstarget,
728 726 dryrun=dryrun, cwd=cwd)
729 727 if rename and not dryrun:
730 728 if not after and srcexists and not samefile:
731 729 util.unlinkpath(repo.wjoin(abssrc))
732 730 wctx.forget([abssrc])
733 731
734 732 # pat: ossep
735 733 # dest ossep
736 734 # srcs: list of (hgsep, hgsep, ossep, bool)
737 735 # return: function that takes hgsep and returns ossep
738 736 def targetpathfn(pat, dest, srcs):
739 737 if os.path.isdir(pat):
740 738 abspfx = pathutil.canonpath(repo.root, cwd, pat)
741 739 abspfx = util.localpath(abspfx)
742 740 if destdirexists:
743 741 striplen = len(os.path.split(abspfx)[0])
744 742 else:
745 743 striplen = len(abspfx)
746 744 if striplen:
747 745 striplen += len(pycompat.ossep)
748 746 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
749 747 elif destdirexists:
750 748 res = lambda p: os.path.join(dest,
751 749 os.path.basename(util.localpath(p)))
752 750 else:
753 751 res = lambda p: dest
754 752 return res
755 753
756 754 # pat: ossep
757 755 # dest ossep
758 756 # srcs: list of (hgsep, hgsep, ossep, bool)
759 757 # return: function that takes hgsep and returns ossep
760 758 def targetpathafterfn(pat, dest, srcs):
761 759 if matchmod.patkind(pat):
762 760 # a mercurial pattern
763 761 res = lambda p: os.path.join(dest,
764 762 os.path.basename(util.localpath(p)))
765 763 else:
766 764 abspfx = pathutil.canonpath(repo.root, cwd, pat)
767 765 if len(abspfx) < len(srcs[0][0]):
768 766 # A directory. Either the target path contains the last
769 767 # component of the source path or it does not.
770 768 def evalpath(striplen):
771 769 score = 0
772 770 for s in srcs:
773 771 t = os.path.join(dest, util.localpath(s[0])[striplen:])
774 772 if os.path.lexists(t):
775 773 score += 1
776 774 return score
777 775
778 776 abspfx = util.localpath(abspfx)
779 777 striplen = len(abspfx)
780 778 if striplen:
781 779 striplen += len(pycompat.ossep)
782 780 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
783 781 score = evalpath(striplen)
784 782 striplen1 = len(os.path.split(abspfx)[0])
785 783 if striplen1:
786 784 striplen1 += len(pycompat.ossep)
787 785 if evalpath(striplen1) > score:
788 786 striplen = striplen1
789 787 res = lambda p: os.path.join(dest,
790 788 util.localpath(p)[striplen:])
791 789 else:
792 790 # a file
793 791 if destdirexists:
794 792 res = lambda p: os.path.join(dest,
795 793 os.path.basename(util.localpath(p)))
796 794 else:
797 795 res = lambda p: dest
798 796 return res
799 797
800 798 pats = scmutil.expandpats(pats)
801 799 if not pats:
802 800 raise error.Abort(_('no source or destination specified'))
803 801 if len(pats) == 1:
804 802 raise error.Abort(_('no destination specified'))
805 803 dest = pats.pop()
806 804 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
807 805 if not destdirexists:
808 806 if len(pats) > 1 or matchmod.patkind(pats[0]):
809 807 raise error.Abort(_('with multiple sources, destination must be an '
810 808 'existing directory'))
811 809 if util.endswithsep(dest):
812 810 raise error.Abort(_('destination %s is not a directory') % dest)
813 811
814 812 tfn = targetpathfn
815 813 if after:
816 814 tfn = targetpathafterfn
817 815 copylist = []
818 816 for pat in pats:
819 817 srcs = walkpat(pat)
820 818 if not srcs:
821 819 continue
822 820 copylist.append((tfn(pat, dest, srcs), srcs))
823 821 if not copylist:
824 822 raise error.Abort(_('no files to copy'))
825 823
826 824 errors = 0
827 825 for targetpath, srcs in copylist:
828 826 for abssrc, relsrc, exact in srcs:
829 827 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
830 828 errors += 1
831 829
832 830 if errors:
833 831 ui.warn(_('(consider using --after)\n'))
834 832
835 833 return errors != 0
836 834
837 835 ## facility to let extension process additional data into an import patch
838 836 # list of identifier to be executed in order
839 837 extrapreimport = [] # run before commit
840 838 extrapostimport = [] # run after commit
841 839 # mapping from identifier to actual import function
842 840 #
843 841 # 'preimport' are run before the commit is made and are provided the following
844 842 # arguments:
845 843 # - repo: the localrepository instance,
846 844 # - patchdata: data extracted from patch header (cf m.patch.patchheadermap),
847 845 # - extra: the future extra dictionary of the changeset, please mutate it,
848 846 # - opts: the import options.
849 847 # XXX ideally, we would just pass an ctx ready to be computed, that would allow
850 848 # mutation of in memory commit and more. Feel free to rework the code to get
851 849 # there.
852 850 extrapreimportmap = {}
853 851 # 'postimport' are run after the commit is made and are provided the following
854 852 # argument:
855 853 # - ctx: the changectx created by import.
856 854 extrapostimportmap = {}
857 855
858 856 def tryimportone(ui, repo, hunk, parents, opts, msgs, updatefunc):
859 857 """Utility function used by commands.import to import a single patch
860 858
861 859 This function is explicitly defined here to help the evolve extension to
862 860 wrap this part of the import logic.
863 861
864 862 The API is currently a bit ugly because it a simple code translation from
865 863 the import command. Feel free to make it better.
866 864
867 865 :hunk: a patch (as a binary string)
868 866 :parents: nodes that will be parent of the created commit
869 867 :opts: the full dict of option passed to the import command
870 868 :msgs: list to save commit message to.
871 869 (used in case we need to save it when failing)
872 870 :updatefunc: a function that update a repo to a given node
873 871 updatefunc(<repo>, <node>)
874 872 """
875 873 # avoid cycle context -> subrepo -> cmdutil
876 874 from . import context
877 875 extractdata = patch.extract(ui, hunk)
878 876 tmpname = extractdata.get('filename')
879 877 message = extractdata.get('message')
880 878 user = opts.get('user') or extractdata.get('user')
881 879 date = opts.get('date') or extractdata.get('date')
882 880 branch = extractdata.get('branch')
883 881 nodeid = extractdata.get('nodeid')
884 882 p1 = extractdata.get('p1')
885 883 p2 = extractdata.get('p2')
886 884
887 885 nocommit = opts.get('no_commit')
888 886 importbranch = opts.get('import_branch')
889 887 update = not opts.get('bypass')
890 888 strip = opts["strip"]
891 889 prefix = opts["prefix"]
892 890 sim = float(opts.get('similarity') or 0)
893 891 if not tmpname:
894 892 return (None, None, False)
895 893
896 894 rejects = False
897 895
898 896 try:
899 897 cmdline_message = logmessage(ui, opts)
900 898 if cmdline_message:
901 899 # pickup the cmdline msg
902 900 message = cmdline_message
903 901 elif message:
904 902 # pickup the patch msg
905 903 message = message.strip()
906 904 else:
907 905 # launch the editor
908 906 message = None
909 907 ui.debug('message:\n%s\n' % message)
910 908
911 909 if len(parents) == 1:
912 910 parents.append(repo[nullid])
913 911 if opts.get('exact'):
914 912 if not nodeid or not p1:
915 913 raise error.Abort(_('not a Mercurial patch'))
916 914 p1 = repo[p1]
917 915 p2 = repo[p2 or nullid]
918 916 elif p2:
919 917 try:
920 918 p1 = repo[p1]
921 919 p2 = repo[p2]
922 920 # Without any options, consider p2 only if the
923 921 # patch is being applied on top of the recorded
924 922 # first parent.
925 923 if p1 != parents[0]:
926 924 p1 = parents[0]
927 925 p2 = repo[nullid]
928 926 except error.RepoError:
929 927 p1, p2 = parents
930 928 if p2.node() == nullid:
931 929 ui.warn(_("warning: import the patch as a normal revision\n"
932 930 "(use --exact to import the patch as a merge)\n"))
933 931 else:
934 932 p1, p2 = parents
935 933
936 934 n = None
937 935 if update:
938 936 if p1 != parents[0]:
939 937 updatefunc(repo, p1.node())
940 938 if p2 != parents[1]:
941 939 repo.setparents(p1.node(), p2.node())
942 940
943 941 if opts.get('exact') or importbranch:
944 942 repo.dirstate.setbranch(branch or 'default')
945 943
946 944 partial = opts.get('partial', False)
947 945 files = set()
948 946 try:
949 947 patch.patch(ui, repo, tmpname, strip=strip, prefix=prefix,
950 948 files=files, eolmode=None, similarity=sim / 100.0)
951 949 except patch.PatchError as e:
952 950 if not partial:
953 951 raise error.Abort(str(e))
954 952 if partial:
955 953 rejects = True
956 954
957 955 files = list(files)
958 956 if nocommit:
959 957 if message:
960 958 msgs.append(message)
961 959 else:
962 960 if opts.get('exact') or p2:
963 961 # If you got here, you either use --force and know what
964 962 # you are doing or used --exact or a merge patch while
965 963 # being updated to its first parent.
966 964 m = None
967 965 else:
968 966 m = scmutil.matchfiles(repo, files or [])
969 967 editform = mergeeditform(repo[None], 'import.normal')
970 968 if opts.get('exact'):
971 969 editor = None
972 970 else:
973 971 editor = getcommiteditor(editform=editform, **opts)
974 972 allowemptyback = repo.ui.backupconfig('ui', 'allowemptycommit')
975 973 extra = {}
976 974 for idfunc in extrapreimport:
977 975 extrapreimportmap[idfunc](repo, extractdata, extra, opts)
978 976 try:
979 977 if partial:
980 978 repo.ui.setconfig('ui', 'allowemptycommit', True)
981 979 n = repo.commit(message, user,
982 980 date, match=m,
983 981 editor=editor, extra=extra)
984 982 for idfunc in extrapostimport:
985 983 extrapostimportmap[idfunc](repo[n])
986 984 finally:
987 985 repo.ui.restoreconfig(allowemptyback)
988 986 else:
989 987 if opts.get('exact') or importbranch:
990 988 branch = branch or 'default'
991 989 else:
992 990 branch = p1.branch()
993 991 store = patch.filestore()
994 992 try:
995 993 files = set()
996 994 try:
997 995 patch.patchrepo(ui, repo, p1, store, tmpname, strip, prefix,
998 996 files, eolmode=None)
999 997 except patch.PatchError as e:
1000 998 raise error.Abort(str(e))
1001 999 if opts.get('exact'):
1002 1000 editor = None
1003 1001 else:
1004 1002 editor = getcommiteditor(editform='import.bypass')
1005 1003 memctx = context.makememctx(repo, (p1.node(), p2.node()),
1006 1004 message,
1007 1005 user,
1008 1006 date,
1009 1007 branch, files, store,
1010 1008 editor=editor)
1011 1009 n = memctx.commit()
1012 1010 finally:
1013 1011 store.close()
1014 1012 if opts.get('exact') and nocommit:
1015 1013 # --exact with --no-commit is still useful in that it does merge
1016 1014 # and branch bits
1017 1015 ui.warn(_("warning: can't check exact import with --no-commit\n"))
1018 1016 elif opts.get('exact') and hex(n) != nodeid:
1019 1017 raise error.Abort(_('patch is damaged or loses information'))
1020 1018 msg = _('applied to working directory')
1021 1019 if n:
1022 1020 # i18n: refers to a short changeset id
1023 1021 msg = _('created %s') % short(n)
1024 1022 return (msg, n, rejects)
1025 1023 finally:
1026 1024 os.unlink(tmpname)
1027 1025
1028 1026 # facility to let extensions include additional data in an exported patch
1029 1027 # list of identifiers to be executed in order
1030 1028 extraexport = []
1031 1029 # mapping from identifier to actual export function
1032 1030 # function as to return a string to be added to the header or None
1033 1031 # it is given two arguments (sequencenumber, changectx)
1034 1032 extraexportmap = {}
1035 1033
1036 1034 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
1037 1035 opts=None, match=None):
1038 1036 '''export changesets as hg patches.'''
1039 1037
1040 1038 total = len(revs)
1041 1039 revwidth = max([len(str(rev)) for rev in revs])
1042 1040 filemode = {}
1043 1041
1044 1042 def single(rev, seqno, fp):
1045 1043 ctx = repo[rev]
1046 1044 node = ctx.node()
1047 1045 parents = [p.node() for p in ctx.parents() if p]
1048 1046 branch = ctx.branch()
1049 1047 if switch_parent:
1050 1048 parents.reverse()
1051 1049
1052 1050 if parents:
1053 1051 prev = parents[0]
1054 1052 else:
1055 1053 prev = nullid
1056 1054
1057 1055 shouldclose = False
1058 1056 if not fp and len(template) > 0:
1059 1057 desc_lines = ctx.description().rstrip().split('\n')
1060 1058 desc = desc_lines[0] #Commit always has a first line.
1061 1059 fp = makefileobj(repo, template, node, desc=desc, total=total,
1062 1060 seqno=seqno, revwidth=revwidth, mode='wb',
1063 1061 modemap=filemode)
1064 1062 shouldclose = True
1065 1063 if fp and not getattr(fp, 'name', '<unnamed>').startswith('<'):
1066 1064 repo.ui.note("%s\n" % fp.name)
1067 1065
1068 1066 if not fp:
1069 1067 write = repo.ui.write
1070 1068 else:
1071 1069 def write(s, **kw):
1072 1070 fp.write(s)
1073 1071
1074 1072 write("# HG changeset patch\n")
1075 1073 write("# User %s\n" % ctx.user())
1076 1074 write("# Date %d %d\n" % ctx.date())
1077 1075 write("# %s\n" % util.datestr(ctx.date()))
1078 1076 if branch and branch != 'default':
1079 1077 write("# Branch %s\n" % branch)
1080 1078 write("# Node ID %s\n" % hex(node))
1081 1079 write("# Parent %s\n" % hex(prev))
1082 1080 if len(parents) > 1:
1083 1081 write("# Parent %s\n" % hex(parents[1]))
1084 1082
1085 1083 for headerid in extraexport:
1086 1084 header = extraexportmap[headerid](seqno, ctx)
1087 1085 if header is not None:
1088 1086 write('# %s\n' % header)
1089 1087 write(ctx.description().rstrip())
1090 1088 write("\n\n")
1091 1089
1092 1090 for chunk, label in patch.diffui(repo, prev, node, match, opts=opts):
1093 1091 write(chunk, label=label)
1094 1092
1095 1093 if shouldclose:
1096 1094 fp.close()
1097 1095
1098 1096 for seqno, rev in enumerate(revs):
1099 1097 single(rev, seqno + 1, fp)
1100 1098
1101 1099 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
1102 1100 changes=None, stat=False, fp=None, prefix='',
1103 1101 root='', listsubrepos=False):
1104 1102 '''show diff or diffstat.'''
1105 1103 if fp is None:
1106 1104 write = ui.write
1107 1105 else:
1108 1106 def write(s, **kw):
1109 1107 fp.write(s)
1110 1108
1111 1109 if root:
1112 1110 relroot = pathutil.canonpath(repo.root, repo.getcwd(), root)
1113 1111 else:
1114 1112 relroot = ''
1115 1113 if relroot != '':
1116 1114 # XXX relative roots currently don't work if the root is within a
1117 1115 # subrepo
1118 1116 uirelroot = match.uipath(relroot)
1119 1117 relroot += '/'
1120 1118 for matchroot in match.files():
1121 1119 if not matchroot.startswith(relroot):
1122 1120 ui.warn(_('warning: %s not inside relative root %s\n') % (
1123 1121 match.uipath(matchroot), uirelroot))
1124 1122
1125 1123 if stat:
1126 1124 diffopts = diffopts.copy(context=0)
1127 1125 width = 80
1128 1126 if not ui.plain():
1129 1127 width = ui.termwidth()
1130 1128 chunks = patch.diff(repo, node1, node2, match, changes, diffopts,
1131 1129 prefix=prefix, relroot=relroot)
1132 1130 for chunk, label in patch.diffstatui(util.iterlines(chunks),
1133 1131 width=width):
1134 1132 write(chunk, label=label)
1135 1133 else:
1136 1134 for chunk, label in patch.diffui(repo, node1, node2, match,
1137 1135 changes, diffopts, prefix=prefix,
1138 1136 relroot=relroot):
1139 1137 write(chunk, label=label)
1140 1138
1141 1139 if listsubrepos:
1142 1140 ctx1 = repo[node1]
1143 1141 ctx2 = repo[node2]
1144 1142 for subpath, sub in scmutil.itersubrepos(ctx1, ctx2):
1145 1143 tempnode2 = node2
1146 1144 try:
1147 1145 if node2 is not None:
1148 1146 tempnode2 = ctx2.substate[subpath][1]
1149 1147 except KeyError:
1150 1148 # A subrepo that existed in node1 was deleted between node1 and
1151 1149 # node2 (inclusive). Thus, ctx2's substate won't contain that
1152 1150 # subpath. The best we can do is to ignore it.
1153 1151 tempnode2 = None
1154 1152 submatch = matchmod.subdirmatcher(subpath, match)
1155 1153 sub.diff(ui, diffopts, tempnode2, submatch, changes=changes,
1156 1154 stat=stat, fp=fp, prefix=prefix)
1157 1155
1158 1156 def _changesetlabels(ctx):
1159 1157 labels = ['log.changeset', 'changeset.%s' % ctx.phasestr()]
1160 1158 if ctx.troubled():
1161 1159 labels.append('changeset.troubled')
1162 1160 for trouble in ctx.troubles():
1163 1161 labels.append('trouble.%s' % trouble)
1164 1162 return ' '.join(labels)
1165 1163
1166 1164 class changeset_printer(object):
1167 1165 '''show changeset information when templating not requested.'''
1168 1166
1169 1167 def __init__(self, ui, repo, matchfn, diffopts, buffered):
1170 1168 self.ui = ui
1171 1169 self.repo = repo
1172 1170 self.buffered = buffered
1173 1171 self.matchfn = matchfn
1174 1172 self.diffopts = diffopts
1175 1173 self.header = {}
1176 1174 self.hunk = {}
1177 1175 self.lastheader = None
1178 1176 self.footer = None
1179 1177
1180 1178 def flush(self, ctx):
1181 1179 rev = ctx.rev()
1182 1180 if rev in self.header:
1183 1181 h = self.header[rev]
1184 1182 if h != self.lastheader:
1185 1183 self.lastheader = h
1186 1184 self.ui.write(h)
1187 1185 del self.header[rev]
1188 1186 if rev in self.hunk:
1189 1187 self.ui.write(self.hunk[rev])
1190 1188 del self.hunk[rev]
1191 1189 return 1
1192 1190 return 0
1193 1191
1194 1192 def close(self):
1195 1193 if self.footer:
1196 1194 self.ui.write(self.footer)
1197 1195
1198 1196 def show(self, ctx, copies=None, matchfn=None, **props):
1199 1197 if self.buffered:
1200 1198 self.ui.pushbuffer(labeled=True)
1201 1199 self._show(ctx, copies, matchfn, props)
1202 1200 self.hunk[ctx.rev()] = self.ui.popbuffer()
1203 1201 else:
1204 1202 self._show(ctx, copies, matchfn, props)
1205 1203
1206 1204 def _show(self, ctx, copies, matchfn, props):
1207 1205 '''show a single changeset or file revision'''
1208 1206 changenode = ctx.node()
1209 1207 rev = ctx.rev()
1210 1208 if self.ui.debugflag:
1211 1209 hexfunc = hex
1212 1210 else:
1213 1211 hexfunc = short
1214 1212 # as of now, wctx.node() and wctx.rev() return None, but we want to
1215 1213 # show the same values as {node} and {rev} templatekw
1216 1214 revnode = (scmutil.intrev(rev), hexfunc(bin(ctx.hex())))
1217 1215
1218 1216 if self.ui.quiet:
1219 1217 self.ui.write("%d:%s\n" % revnode, label='log.node')
1220 1218 return
1221 1219
1222 1220 date = util.datestr(ctx.date())
1223 1221
1224 1222 # i18n: column positioning for "hg log"
1225 1223 self.ui.write(_("changeset: %d:%s\n") % revnode,
1226 1224 label=_changesetlabels(ctx))
1227 1225
1228 1226 # branches are shown first before any other names due to backwards
1229 1227 # compatibility
1230 1228 branch = ctx.branch()
1231 1229 # don't show the default branch name
1232 1230 if branch != 'default':
1233 1231 # i18n: column positioning for "hg log"
1234 1232 self.ui.write(_("branch: %s\n") % branch,
1235 1233 label='log.branch')
1236 1234
1237 1235 for nsname, ns in self.repo.names.iteritems():
1238 1236 # branches has special logic already handled above, so here we just
1239 1237 # skip it
1240 1238 if nsname == 'branches':
1241 1239 continue
1242 1240 # we will use the templatename as the color name since those two
1243 1241 # should be the same
1244 1242 for name in ns.names(self.repo, changenode):
1245 1243 self.ui.write(ns.logfmt % name,
1246 1244 label='log.%s' % ns.colorname)
1247 1245 if self.ui.debugflag:
1248 1246 # i18n: column positioning for "hg log"
1249 1247 self.ui.write(_("phase: %s\n") % ctx.phasestr(),
1250 1248 label='log.phase')
1251 1249 for pctx in scmutil.meaningfulparents(self.repo, ctx):
1252 1250 label = 'log.parent changeset.%s' % pctx.phasestr()
1253 1251 # i18n: column positioning for "hg log"
1254 1252 self.ui.write(_("parent: %d:%s\n")
1255 1253 % (pctx.rev(), hexfunc(pctx.node())),
1256 1254 label=label)
1257 1255
1258 1256 if self.ui.debugflag and rev is not None:
1259 1257 mnode = ctx.manifestnode()
1260 1258 # i18n: column positioning for "hg log"
1261 1259 self.ui.write(_("manifest: %d:%s\n") %
1262 1260 (self.repo.manifestlog._revlog.rev(mnode),
1263 1261 hex(mnode)),
1264 1262 label='ui.debug log.manifest')
1265 1263 # i18n: column positioning for "hg log"
1266 1264 self.ui.write(_("user: %s\n") % ctx.user(),
1267 1265 label='log.user')
1268 1266 # i18n: column positioning for "hg log"
1269 1267 self.ui.write(_("date: %s\n") % date,
1270 1268 label='log.date')
1271 1269
1272 1270 if ctx.troubled():
1273 1271 # i18n: column positioning for "hg log"
1274 1272 self.ui.write(_("trouble: %s\n") % ', '.join(ctx.troubles()),
1275 1273 label='log.trouble')
1276 1274
1277 1275 if self.ui.debugflag:
1278 1276 files = ctx.p1().status(ctx)[:3]
1279 1277 for key, value in zip([# i18n: column positioning for "hg log"
1280 1278 _("files:"),
1281 1279 # i18n: column positioning for "hg log"
1282 1280 _("files+:"),
1283 1281 # i18n: column positioning for "hg log"
1284 1282 _("files-:")], files):
1285 1283 if value:
1286 1284 self.ui.write("%-12s %s\n" % (key, " ".join(value)),
1287 1285 label='ui.debug log.files')
1288 1286 elif ctx.files() and self.ui.verbose:
1289 1287 # i18n: column positioning for "hg log"
1290 1288 self.ui.write(_("files: %s\n") % " ".join(ctx.files()),
1291 1289 label='ui.note log.files')
1292 1290 if copies and self.ui.verbose:
1293 1291 copies = ['%s (%s)' % c for c in copies]
1294 1292 # i18n: column positioning for "hg log"
1295 1293 self.ui.write(_("copies: %s\n") % ' '.join(copies),
1296 1294 label='ui.note log.copies')
1297 1295
1298 1296 extra = ctx.extra()
1299 1297 if extra and self.ui.debugflag:
1300 1298 for key, value in sorted(extra.items()):
1301 1299 # i18n: column positioning for "hg log"
1302 1300 self.ui.write(_("extra: %s=%s\n")
1303 1301 % (key, value.encode('string_escape')),
1304 1302 label='ui.debug log.extra')
1305 1303
1306 1304 description = ctx.description().strip()
1307 1305 if description:
1308 1306 if self.ui.verbose:
1309 1307 self.ui.write(_("description:\n"),
1310 1308 label='ui.note log.description')
1311 1309 self.ui.write(description,
1312 1310 label='ui.note log.description')
1313 1311 self.ui.write("\n\n")
1314 1312 else:
1315 1313 # i18n: column positioning for "hg log"
1316 1314 self.ui.write(_("summary: %s\n") %
1317 1315 description.splitlines()[0],
1318 1316 label='log.summary')
1319 1317 self.ui.write("\n")
1320 1318
1321 1319 self.showpatch(ctx, matchfn)
1322 1320
1323 1321 def showpatch(self, ctx, matchfn):
1324 1322 if not matchfn:
1325 1323 matchfn = self.matchfn
1326 1324 if matchfn:
1327 1325 stat = self.diffopts.get('stat')
1328 1326 diff = self.diffopts.get('patch')
1329 1327 diffopts = patch.diffallopts(self.ui, self.diffopts)
1330 1328 node = ctx.node()
1331 1329 prev = ctx.p1().node()
1332 1330 if stat:
1333 1331 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1334 1332 match=matchfn, stat=True)
1335 1333 if diff:
1336 1334 if stat:
1337 1335 self.ui.write("\n")
1338 1336 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1339 1337 match=matchfn, stat=False)
1340 1338 self.ui.write("\n")
1341 1339
1342 1340 class jsonchangeset(changeset_printer):
1343 1341 '''format changeset information.'''
1344 1342
1345 1343 def __init__(self, ui, repo, matchfn, diffopts, buffered):
1346 1344 changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
1347 1345 self.cache = {}
1348 1346 self._first = True
1349 1347
1350 1348 def close(self):
1351 1349 if not self._first:
1352 1350 self.ui.write("\n]\n")
1353 1351 else:
1354 1352 self.ui.write("[]\n")
1355 1353
1356 1354 def _show(self, ctx, copies, matchfn, props):
1357 1355 '''show a single changeset or file revision'''
1358 1356 rev = ctx.rev()
1359 1357 if rev is None:
1360 1358 jrev = jnode = 'null'
1361 1359 else:
1362 1360 jrev = str(rev)
1363 1361 jnode = '"%s"' % hex(ctx.node())
1364 1362 j = encoding.jsonescape
1365 1363
1366 1364 if self._first:
1367 1365 self.ui.write("[\n {")
1368 1366 self._first = False
1369 1367 else:
1370 1368 self.ui.write(",\n {")
1371 1369
1372 1370 if self.ui.quiet:
1373 1371 self.ui.write(('\n "rev": %s') % jrev)
1374 1372 self.ui.write((',\n "node": %s') % jnode)
1375 1373 self.ui.write('\n }')
1376 1374 return
1377 1375
1378 1376 self.ui.write(('\n "rev": %s') % jrev)
1379 1377 self.ui.write((',\n "node": %s') % jnode)
1380 1378 self.ui.write((',\n "branch": "%s"') % j(ctx.branch()))
1381 1379 self.ui.write((',\n "phase": "%s"') % ctx.phasestr())
1382 1380 self.ui.write((',\n "user": "%s"') % j(ctx.user()))
1383 1381 self.ui.write((',\n "date": [%d, %d]') % ctx.date())
1384 1382 self.ui.write((',\n "desc": "%s"') % j(ctx.description()))
1385 1383
1386 1384 self.ui.write((',\n "bookmarks": [%s]') %
1387 1385 ", ".join('"%s"' % j(b) for b in ctx.bookmarks()))
1388 1386 self.ui.write((',\n "tags": [%s]') %
1389 1387 ", ".join('"%s"' % j(t) for t in ctx.tags()))
1390 1388 self.ui.write((',\n "parents": [%s]') %
1391 1389 ", ".join('"%s"' % c.hex() for c in ctx.parents()))
1392 1390
1393 1391 if self.ui.debugflag:
1394 1392 if rev is None:
1395 1393 jmanifestnode = 'null'
1396 1394 else:
1397 1395 jmanifestnode = '"%s"' % hex(ctx.manifestnode())
1398 1396 self.ui.write((',\n "manifest": %s') % jmanifestnode)
1399 1397
1400 1398 self.ui.write((',\n "extra": {%s}') %
1401 1399 ", ".join('"%s": "%s"' % (j(k), j(v))
1402 1400 for k, v in ctx.extra().items()))
1403 1401
1404 1402 files = ctx.p1().status(ctx)
1405 1403 self.ui.write((',\n "modified": [%s]') %
1406 1404 ", ".join('"%s"' % j(f) for f in files[0]))
1407 1405 self.ui.write((',\n "added": [%s]') %
1408 1406 ", ".join('"%s"' % j(f) for f in files[1]))
1409 1407 self.ui.write((',\n "removed": [%s]') %
1410 1408 ", ".join('"%s"' % j(f) for f in files[2]))
1411 1409
1412 1410 elif self.ui.verbose:
1413 1411 self.ui.write((',\n "files": [%s]') %
1414 1412 ", ".join('"%s"' % j(f) for f in ctx.files()))
1415 1413
1416 1414 if copies:
1417 1415 self.ui.write((',\n "copies": {%s}') %
1418 1416 ", ".join('"%s": "%s"' % (j(k), j(v))
1419 1417 for k, v in copies))
1420 1418
1421 1419 matchfn = self.matchfn
1422 1420 if matchfn:
1423 1421 stat = self.diffopts.get('stat')
1424 1422 diff = self.diffopts.get('patch')
1425 1423 diffopts = patch.difffeatureopts(self.ui, self.diffopts, git=True)
1426 1424 node, prev = ctx.node(), ctx.p1().node()
1427 1425 if stat:
1428 1426 self.ui.pushbuffer()
1429 1427 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1430 1428 match=matchfn, stat=True)
1431 1429 self.ui.write((',\n "diffstat": "%s"')
1432 1430 % j(self.ui.popbuffer()))
1433 1431 if diff:
1434 1432 self.ui.pushbuffer()
1435 1433 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
1436 1434 match=matchfn, stat=False)
1437 1435 self.ui.write((',\n "diff": "%s"') % j(self.ui.popbuffer()))
1438 1436
1439 1437 self.ui.write("\n }")
1440 1438
1441 1439 class changeset_templater(changeset_printer):
1442 1440 '''format changeset information.'''
1443 1441
1444 1442 def __init__(self, ui, repo, matchfn, diffopts, tmpl, mapfile, buffered):
1445 1443 changeset_printer.__init__(self, ui, repo, matchfn, diffopts, buffered)
1446 1444 formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
1447 1445 filters = {'formatnode': formatnode}
1448 1446 defaulttempl = {
1449 1447 'parent': '{rev}:{node|formatnode} ',
1450 1448 'manifest': '{rev}:{node|formatnode}',
1451 1449 'file_copy': '{name} ({source})',
1452 1450 'envvar': '{key}={value}',
1453 1451 'extra': '{key}={value|stringescape}'
1454 1452 }
1455 1453 # filecopy is preserved for compatibility reasons
1456 1454 defaulttempl['filecopy'] = defaulttempl['file_copy']
1457 1455 assert not (tmpl and mapfile)
1458 1456 if mapfile:
1459 1457 self.t = templater.templater.frommapfile(mapfile, filters=filters,
1460 1458 cache=defaulttempl)
1461 1459 else:
1462 1460 self.t = formatter.maketemplater(ui, 'changeset', tmpl,
1463 1461 filters=filters,
1464 1462 cache=defaulttempl)
1465 1463
1466 1464 self.cache = {}
1467 1465
1468 1466 # find correct templates for current mode
1469 1467 tmplmodes = [
1470 1468 (True, None),
1471 1469 (self.ui.verbose, 'verbose'),
1472 1470 (self.ui.quiet, 'quiet'),
1473 1471 (self.ui.debugflag, 'debug'),
1474 1472 ]
1475 1473
1476 1474 self._parts = {'header': '', 'footer': '', 'changeset': 'changeset',
1477 1475 'docheader': '', 'docfooter': ''}
1478 1476 for mode, postfix in tmplmodes:
1479 1477 for t in self._parts:
1480 1478 cur = t
1481 1479 if postfix:
1482 1480 cur += "_" + postfix
1483 1481 if mode and cur in self.t:
1484 1482 self._parts[t] = cur
1485 1483
1486 1484 if self._parts['docheader']:
1487 1485 self.ui.write(templater.stringify(self.t(self._parts['docheader'])))
1488 1486
1489 1487 def close(self):
1490 1488 if self._parts['docfooter']:
1491 1489 if not self.footer:
1492 1490 self.footer = ""
1493 1491 self.footer += templater.stringify(self.t(self._parts['docfooter']))
1494 1492 return super(changeset_templater, self).close()
1495 1493
1496 1494 def _show(self, ctx, copies, matchfn, props):
1497 1495 '''show a single changeset or file revision'''
1498 1496 props = props.copy()
1499 1497 props.update(templatekw.keywords)
1500 1498 props['templ'] = self.t
1501 1499 props['ctx'] = ctx
1502 1500 props['repo'] = self.repo
1503 1501 props['ui'] = self.repo.ui
1504 1502 props['revcache'] = {'copies': copies}
1505 1503 props['cache'] = self.cache
1506 1504
1507 1505 # write header
1508 1506 if self._parts['header']:
1509 1507 h = templater.stringify(self.t(self._parts['header'], **props))
1510 1508 if self.buffered:
1511 1509 self.header[ctx.rev()] = h
1512 1510 else:
1513 1511 if self.lastheader != h:
1514 1512 self.lastheader = h
1515 1513 self.ui.write(h)
1516 1514
1517 1515 # write changeset metadata, then patch if requested
1518 1516 key = self._parts['changeset']
1519 1517 self.ui.write(templater.stringify(self.t(key, **props)))
1520 1518 self.showpatch(ctx, matchfn)
1521 1519
1522 1520 if self._parts['footer']:
1523 1521 if not self.footer:
1524 1522 self.footer = templater.stringify(
1525 1523 self.t(self._parts['footer'], **props))
1526 1524
1527 1525 def gettemplate(ui, tmpl, style):
1528 1526 """
1529 1527 Find the template matching the given template spec or style.
1530 1528 """
1531 1529
1532 1530 # ui settings
1533 1531 if not tmpl and not style: # template are stronger than style
1534 1532 tmpl = ui.config('ui', 'logtemplate')
1535 1533 if tmpl:
1536 1534 return templater.unquotestring(tmpl), None
1537 1535 else:
1538 1536 style = util.expandpath(ui.config('ui', 'style', ''))
1539 1537
1540 1538 if not tmpl and style:
1541 1539 mapfile = style
1542 1540 if not os.path.split(mapfile)[0]:
1543 1541 mapname = (templater.templatepath('map-cmdline.' + mapfile)
1544 1542 or templater.templatepath(mapfile))
1545 1543 if mapname:
1546 1544 mapfile = mapname
1547 1545 return None, mapfile
1548 1546
1549 1547 if not tmpl:
1550 1548 return None, None
1551 1549
1552 1550 return formatter.lookuptemplate(ui, 'changeset', tmpl)
1553 1551
1554 1552 def show_changeset(ui, repo, opts, buffered=False):
1555 1553 """show one changeset using template or regular display.
1556 1554
1557 1555 Display format will be the first non-empty hit of:
1558 1556 1. option 'template'
1559 1557 2. option 'style'
1560 1558 3. [ui] setting 'logtemplate'
1561 1559 4. [ui] setting 'style'
1562 1560 If all of these values are either the unset or the empty string,
1563 1561 regular display via changeset_printer() is done.
1564 1562 """
1565 1563 # options
1566 1564 matchfn = None
1567 1565 if opts.get('patch') or opts.get('stat'):
1568 1566 matchfn = scmutil.matchall(repo)
1569 1567
1570 1568 if opts.get('template') == 'json':
1571 1569 return jsonchangeset(ui, repo, matchfn, opts, buffered)
1572 1570
1573 1571 tmpl, mapfile = gettemplate(ui, opts.get('template'), opts.get('style'))
1574 1572
1575 1573 if not tmpl and not mapfile:
1576 1574 return changeset_printer(ui, repo, matchfn, opts, buffered)
1577 1575
1578 1576 return changeset_templater(ui, repo, matchfn, opts, tmpl, mapfile, buffered)
1579 1577
1580 1578 def showmarker(fm, marker, index=None):
1581 1579 """utility function to display obsolescence marker in a readable way
1582 1580
1583 1581 To be used by debug function."""
1584 1582 if index is not None:
1585 1583 fm.write('index', '%i ', index)
1586 1584 fm.write('precnode', '%s ', hex(marker.precnode()))
1587 1585 succs = marker.succnodes()
1588 1586 fm.condwrite(succs, 'succnodes', '%s ',
1589 1587 fm.formatlist(map(hex, succs), name='node'))
1590 1588 fm.write('flag', '%X ', marker.flags())
1591 1589 parents = marker.parentnodes()
1592 1590 if parents is not None:
1593 1591 fm.write('parentnodes', '{%s} ',
1594 1592 fm.formatlist(map(hex, parents), name='node', sep=', '))
1595 1593 fm.write('date', '(%s) ', fm.formatdate(marker.date()))
1596 1594 meta = marker.metadata().copy()
1597 1595 meta.pop('date', None)
1598 1596 fm.write('metadata', '{%s}', fm.formatdict(meta, fmt='%r: %r', sep=', '))
1599 1597 fm.plain('\n')
1600 1598
1601 1599 def finddate(ui, repo, date):
1602 1600 """Find the tipmost changeset that matches the given date spec"""
1603 1601
1604 1602 df = util.matchdate(date)
1605 1603 m = scmutil.matchall(repo)
1606 1604 results = {}
1607 1605
1608 1606 def prep(ctx, fns):
1609 1607 d = ctx.date()
1610 1608 if df(d[0]):
1611 1609 results[ctx.rev()] = d
1612 1610
1613 1611 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
1614 1612 rev = ctx.rev()
1615 1613 if rev in results:
1616 1614 ui.status(_("found revision %s from %s\n") %
1617 1615 (rev, util.datestr(results[rev])))
1618 1616 return str(rev)
1619 1617
1620 1618 raise error.Abort(_("revision matching date not found"))
1621 1619
1622 1620 def increasingwindows(windowsize=8, sizelimit=512):
1623 1621 while True:
1624 1622 yield windowsize
1625 1623 if windowsize < sizelimit:
1626 1624 windowsize *= 2
1627 1625
1628 1626 class FileWalkError(Exception):
1629 1627 pass
1630 1628
1631 1629 def walkfilerevs(repo, match, follow, revs, fncache):
1632 1630 '''Walks the file history for the matched files.
1633 1631
1634 1632 Returns the changeset revs that are involved in the file history.
1635 1633
1636 1634 Throws FileWalkError if the file history can't be walked using
1637 1635 filelogs alone.
1638 1636 '''
1639 1637 wanted = set()
1640 1638 copies = []
1641 1639 minrev, maxrev = min(revs), max(revs)
1642 1640 def filerevgen(filelog, last):
1643 1641 """
1644 1642 Only files, no patterns. Check the history of each file.
1645 1643
1646 1644 Examines filelog entries within minrev, maxrev linkrev range
1647 1645 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
1648 1646 tuples in backwards order
1649 1647 """
1650 1648 cl_count = len(repo)
1651 1649 revs = []
1652 1650 for j in xrange(0, last + 1):
1653 1651 linkrev = filelog.linkrev(j)
1654 1652 if linkrev < minrev:
1655 1653 continue
1656 1654 # only yield rev for which we have the changelog, it can
1657 1655 # happen while doing "hg log" during a pull or commit
1658 1656 if linkrev >= cl_count:
1659 1657 break
1660 1658
1661 1659 parentlinkrevs = []
1662 1660 for p in filelog.parentrevs(j):
1663 1661 if p != nullrev:
1664 1662 parentlinkrevs.append(filelog.linkrev(p))
1665 1663 n = filelog.node(j)
1666 1664 revs.append((linkrev, parentlinkrevs,
1667 1665 follow and filelog.renamed(n)))
1668 1666
1669 1667 return reversed(revs)
1670 1668 def iterfiles():
1671 1669 pctx = repo['.']
1672 1670 for filename in match.files():
1673 1671 if follow:
1674 1672 if filename not in pctx:
1675 1673 raise error.Abort(_('cannot follow file not in parent '
1676 1674 'revision: "%s"') % filename)
1677 1675 yield filename, pctx[filename].filenode()
1678 1676 else:
1679 1677 yield filename, None
1680 1678 for filename_node in copies:
1681 1679 yield filename_node
1682 1680
1683 1681 for file_, node in iterfiles():
1684 1682 filelog = repo.file(file_)
1685 1683 if not len(filelog):
1686 1684 if node is None:
1687 1685 # A zero count may be a directory or deleted file, so
1688 1686 # try to find matching entries on the slow path.
1689 1687 if follow:
1690 1688 raise error.Abort(
1691 1689 _('cannot follow nonexistent file: "%s"') % file_)
1692 1690 raise FileWalkError("Cannot walk via filelog")
1693 1691 else:
1694 1692 continue
1695 1693
1696 1694 if node is None:
1697 1695 last = len(filelog) - 1
1698 1696 else:
1699 1697 last = filelog.rev(node)
1700 1698
1701 1699 # keep track of all ancestors of the file
1702 1700 ancestors = set([filelog.linkrev(last)])
1703 1701
1704 1702 # iterate from latest to oldest revision
1705 1703 for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
1706 1704 if not follow:
1707 1705 if rev > maxrev:
1708 1706 continue
1709 1707 else:
1710 1708 # Note that last might not be the first interesting
1711 1709 # rev to us:
1712 1710 # if the file has been changed after maxrev, we'll
1713 1711 # have linkrev(last) > maxrev, and we still need
1714 1712 # to explore the file graph
1715 1713 if rev not in ancestors:
1716 1714 continue
1717 1715 # XXX insert 1327 fix here
1718 1716 if flparentlinkrevs:
1719 1717 ancestors.update(flparentlinkrevs)
1720 1718
1721 1719 fncache.setdefault(rev, []).append(file_)
1722 1720 wanted.add(rev)
1723 1721 if copied:
1724 1722 copies.append(copied)
1725 1723
1726 1724 return wanted
1727 1725
1728 1726 class _followfilter(object):
1729 1727 def __init__(self, repo, onlyfirst=False):
1730 1728 self.repo = repo
1731 1729 self.startrev = nullrev
1732 1730 self.roots = set()
1733 1731 self.onlyfirst = onlyfirst
1734 1732
1735 1733 def match(self, rev):
1736 1734 def realparents(rev):
1737 1735 if self.onlyfirst:
1738 1736 return self.repo.changelog.parentrevs(rev)[0:1]
1739 1737 else:
1740 1738 return filter(lambda x: x != nullrev,
1741 1739 self.repo.changelog.parentrevs(rev))
1742 1740
1743 1741 if self.startrev == nullrev:
1744 1742 self.startrev = rev
1745 1743 return True
1746 1744
1747 1745 if rev > self.startrev:
1748 1746 # forward: all descendants
1749 1747 if not self.roots:
1750 1748 self.roots.add(self.startrev)
1751 1749 for parent in realparents(rev):
1752 1750 if parent in self.roots:
1753 1751 self.roots.add(rev)
1754 1752 return True
1755 1753 else:
1756 1754 # backwards: all parents
1757 1755 if not self.roots:
1758 1756 self.roots.update(realparents(self.startrev))
1759 1757 if rev in self.roots:
1760 1758 self.roots.remove(rev)
1761 1759 self.roots.update(realparents(rev))
1762 1760 return True
1763 1761
1764 1762 return False
1765 1763
1766 1764 def walkchangerevs(repo, match, opts, prepare):
1767 1765 '''Iterate over files and the revs in which they changed.
1768 1766
1769 1767 Callers most commonly need to iterate backwards over the history
1770 1768 in which they are interested. Doing so has awful (quadratic-looking)
1771 1769 performance, so we use iterators in a "windowed" way.
1772 1770
1773 1771 We walk a window of revisions in the desired order. Within the
1774 1772 window, we first walk forwards to gather data, then in the desired
1775 1773 order (usually backwards) to display it.
1776 1774
1777 1775 This function returns an iterator yielding contexts. Before
1778 1776 yielding each context, the iterator will first call the prepare
1779 1777 function on each context in the window in forward order.'''
1780 1778
1781 1779 follow = opts.get('follow') or opts.get('follow_first')
1782 1780 revs = _logrevs(repo, opts)
1783 1781 if not revs:
1784 1782 return []
1785 1783 wanted = set()
1786 1784 slowpath = match.anypats() or ((match.isexact() or match.prefix()) and
1787 1785 opts.get('removed'))
1788 1786 fncache = {}
1789 1787 change = repo.changectx
1790 1788
1791 1789 # First step is to fill wanted, the set of revisions that we want to yield.
1792 1790 # When it does not induce extra cost, we also fill fncache for revisions in
1793 1791 # wanted: a cache of filenames that were changed (ctx.files()) and that
1794 1792 # match the file filtering conditions.
1795 1793
1796 1794 if match.always():
1797 1795 # No files, no patterns. Display all revs.
1798 1796 wanted = revs
1799 1797 elif not slowpath:
1800 1798 # We only have to read through the filelog to find wanted revisions
1801 1799
1802 1800 try:
1803 1801 wanted = walkfilerevs(repo, match, follow, revs, fncache)
1804 1802 except FileWalkError:
1805 1803 slowpath = True
1806 1804
1807 1805 # We decided to fall back to the slowpath because at least one
1808 1806 # of the paths was not a file. Check to see if at least one of them
1809 1807 # existed in history, otherwise simply return
1810 1808 for path in match.files():
1811 1809 if path == '.' or path in repo.store:
1812 1810 break
1813 1811 else:
1814 1812 return []
1815 1813
1816 1814 if slowpath:
1817 1815 # We have to read the changelog to match filenames against
1818 1816 # changed files
1819 1817
1820 1818 if follow:
1821 1819 raise error.Abort(_('can only follow copies/renames for explicit '
1822 1820 'filenames'))
1823 1821
1824 1822 # The slow path checks files modified in every changeset.
1825 1823 # This is really slow on large repos, so compute the set lazily.
1826 1824 class lazywantedset(object):
1827 1825 def __init__(self):
1828 1826 self.set = set()
1829 1827 self.revs = set(revs)
1830 1828
1831 1829 # No need to worry about locality here because it will be accessed
1832 1830 # in the same order as the increasing window below.
1833 1831 def __contains__(self, value):
1834 1832 if value in self.set:
1835 1833 return True
1836 1834 elif not value in self.revs:
1837 1835 return False
1838 1836 else:
1839 1837 self.revs.discard(value)
1840 1838 ctx = change(value)
1841 1839 matches = filter(match, ctx.files())
1842 1840 if matches:
1843 1841 fncache[value] = matches
1844 1842 self.set.add(value)
1845 1843 return True
1846 1844 return False
1847 1845
1848 1846 def discard(self, value):
1849 1847 self.revs.discard(value)
1850 1848 self.set.discard(value)
1851 1849
1852 1850 wanted = lazywantedset()
1853 1851
1854 1852 # it might be worthwhile to do this in the iterator if the rev range
1855 1853 # is descending and the prune args are all within that range
1856 1854 for rev in opts.get('prune', ()):
1857 1855 rev = repo[rev].rev()
1858 1856 ff = _followfilter(repo)
1859 1857 stop = min(revs[0], revs[-1])
1860 1858 for x in xrange(rev, stop - 1, -1):
1861 1859 if ff.match(x):
1862 1860 wanted = wanted - [x]
1863 1861
1864 1862 # Now that wanted is correctly initialized, we can iterate over the
1865 1863 # revision range, yielding only revisions in wanted.
1866 1864 def iterate():
1867 1865 if follow and match.always():
1868 1866 ff = _followfilter(repo, onlyfirst=opts.get('follow_first'))
1869 1867 def want(rev):
1870 1868 return ff.match(rev) and rev in wanted
1871 1869 else:
1872 1870 def want(rev):
1873 1871 return rev in wanted
1874 1872
1875 1873 it = iter(revs)
1876 1874 stopiteration = False
1877 1875 for windowsize in increasingwindows():
1878 1876 nrevs = []
1879 1877 for i in xrange(windowsize):
1880 1878 rev = next(it, None)
1881 1879 if rev is None:
1882 1880 stopiteration = True
1883 1881 break
1884 1882 elif want(rev):
1885 1883 nrevs.append(rev)
1886 1884 for rev in sorted(nrevs):
1887 1885 fns = fncache.get(rev)
1888 1886 ctx = change(rev)
1889 1887 if not fns:
1890 1888 def fns_generator():
1891 1889 for f in ctx.files():
1892 1890 if match(f):
1893 1891 yield f
1894 1892 fns = fns_generator()
1895 1893 prepare(ctx, fns)
1896 1894 for rev in nrevs:
1897 1895 yield change(rev)
1898 1896
1899 1897 if stopiteration:
1900 1898 break
1901 1899
1902 1900 return iterate()
1903 1901
1904 1902 def _makefollowlogfilematcher(repo, files, followfirst):
1905 1903 # When displaying a revision with --patch --follow FILE, we have
1906 1904 # to know which file of the revision must be diffed. With
1907 1905 # --follow, we want the names of the ancestors of FILE in the
1908 1906 # revision, stored in "fcache". "fcache" is populated by
1909 1907 # reproducing the graph traversal already done by --follow revset
1910 1908 # and relating revs to file names (which is not "correct" but
1911 1909 # good enough).
1912 1910 fcache = {}
1913 1911 fcacheready = [False]
1914 1912 pctx = repo['.']
1915 1913
1916 1914 def populate():
1917 1915 for fn in files:
1918 1916 fctx = pctx[fn]
1919 1917 fcache.setdefault(fctx.introrev(), set()).add(fctx.path())
1920 1918 for c in fctx.ancestors(followfirst=followfirst):
1921 1919 fcache.setdefault(c.rev(), set()).add(c.path())
1922 1920
1923 1921 def filematcher(rev):
1924 1922 if not fcacheready[0]:
1925 1923 # Lazy initialization
1926 1924 fcacheready[0] = True
1927 1925 populate()
1928 1926 return scmutil.matchfiles(repo, fcache.get(rev, []))
1929 1927
1930 1928 return filematcher
1931 1929
1932 1930 def _makenofollowlogfilematcher(repo, pats, opts):
1933 1931 '''hook for extensions to override the filematcher for non-follow cases'''
1934 1932 return None
1935 1933
1936 1934 def _makelogrevset(repo, pats, opts, revs):
1937 1935 """Return (expr, filematcher) where expr is a revset string built
1938 1936 from log options and file patterns or None. If --stat or --patch
1939 1937 are not passed filematcher is None. Otherwise it is a callable
1940 1938 taking a revision number and returning a match objects filtering
1941 1939 the files to be detailed when displaying the revision.
1942 1940 """
1943 1941 opt2revset = {
1944 1942 'no_merges': ('not merge()', None),
1945 1943 'only_merges': ('merge()', None),
1946 1944 '_ancestors': ('ancestors(%(val)s)', None),
1947 1945 '_fancestors': ('_firstancestors(%(val)s)', None),
1948 1946 '_descendants': ('descendants(%(val)s)', None),
1949 1947 '_fdescendants': ('_firstdescendants(%(val)s)', None),
1950 1948 '_matchfiles': ('_matchfiles(%(val)s)', None),
1951 1949 'date': ('date(%(val)r)', None),
1952 1950 'branch': ('branch(%(val)r)', ' or '),
1953 1951 '_patslog': ('filelog(%(val)r)', ' or '),
1954 1952 '_patsfollow': ('follow(%(val)r)', ' or '),
1955 1953 '_patsfollowfirst': ('_followfirst(%(val)r)', ' or '),
1956 1954 'keyword': ('keyword(%(val)r)', ' or '),
1957 1955 'prune': ('not (%(val)r or ancestors(%(val)r))', ' and '),
1958 1956 'user': ('user(%(val)r)', ' or '),
1959 1957 }
1960 1958
1961 1959 opts = dict(opts)
1962 1960 # follow or not follow?
1963 1961 follow = opts.get('follow') or opts.get('follow_first')
1964 1962 if opts.get('follow_first'):
1965 1963 followfirst = 1
1966 1964 else:
1967 1965 followfirst = 0
1968 1966 # --follow with FILE behavior depends on revs...
1969 1967 it = iter(revs)
1970 1968 startrev = next(it)
1971 1969 followdescendants = startrev < next(it, startrev)
1972 1970
1973 1971 # branch and only_branch are really aliases and must be handled at
1974 1972 # the same time
1975 1973 opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
1976 1974 opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
1977 1975 # pats/include/exclude are passed to match.match() directly in
1978 1976 # _matchfiles() revset but walkchangerevs() builds its matcher with
1979 1977 # scmutil.match(). The difference is input pats are globbed on
1980 1978 # platforms without shell expansion (windows).
1981 1979 wctx = repo[None]
1982 1980 match, pats = scmutil.matchandpats(wctx, pats, opts)
1983 1981 slowpath = match.anypats() or ((match.isexact() or match.prefix()) and
1984 1982 opts.get('removed'))
1985 1983 if not slowpath:
1986 1984 for f in match.files():
1987 1985 if follow and f not in wctx:
1988 1986 # If the file exists, it may be a directory, so let it
1989 1987 # take the slow path.
1990 1988 if os.path.exists(repo.wjoin(f)):
1991 1989 slowpath = True
1992 1990 continue
1993 1991 else:
1994 1992 raise error.Abort(_('cannot follow file not in parent '
1995 1993 'revision: "%s"') % f)
1996 1994 filelog = repo.file(f)
1997 1995 if not filelog:
1998 1996 # A zero count may be a directory or deleted file, so
1999 1997 # try to find matching entries on the slow path.
2000 1998 if follow:
2001 1999 raise error.Abort(
2002 2000 _('cannot follow nonexistent file: "%s"') % f)
2003 2001 slowpath = True
2004 2002
2005 2003 # We decided to fall back to the slowpath because at least one
2006 2004 # of the paths was not a file. Check to see if at least one of them
2007 2005 # existed in history - in that case, we'll continue down the
2008 2006 # slowpath; otherwise, we can turn off the slowpath
2009 2007 if slowpath:
2010 2008 for path in match.files():
2011 2009 if path == '.' or path in repo.store:
2012 2010 break
2013 2011 else:
2014 2012 slowpath = False
2015 2013
2016 2014 fpats = ('_patsfollow', '_patsfollowfirst')
2017 2015 fnopats = (('_ancestors', '_fancestors'),
2018 2016 ('_descendants', '_fdescendants'))
2019 2017 if slowpath:
2020 2018 # See walkchangerevs() slow path.
2021 2019 #
2022 2020 # pats/include/exclude cannot be represented as separate
2023 2021 # revset expressions as their filtering logic applies at file
2024 2022 # level. For instance "-I a -X a" matches a revision touching
2025 2023 # "a" and "b" while "file(a) and not file(b)" does
2026 2024 # not. Besides, filesets are evaluated against the working
2027 2025 # directory.
2028 2026 matchargs = ['r:', 'd:relpath']
2029 2027 for p in pats:
2030 2028 matchargs.append('p:' + p)
2031 2029 for p in opts.get('include', []):
2032 2030 matchargs.append('i:' + p)
2033 2031 for p in opts.get('exclude', []):
2034 2032 matchargs.append('x:' + p)
2035 2033 matchargs = ','.join(('%r' % p) for p in matchargs)
2036 2034 opts['_matchfiles'] = matchargs
2037 2035 if follow:
2038 2036 opts[fnopats[0][followfirst]] = '.'
2039 2037 else:
2040 2038 if follow:
2041 2039 if pats:
2042 2040 # follow() revset interprets its file argument as a
2043 2041 # manifest entry, so use match.files(), not pats.
2044 2042 opts[fpats[followfirst]] = list(match.files())
2045 2043 else:
2046 2044 op = fnopats[followdescendants][followfirst]
2047 2045 opts[op] = 'rev(%d)' % startrev
2048 2046 else:
2049 2047 opts['_patslog'] = list(pats)
2050 2048
2051 2049 filematcher = None
2052 2050 if opts.get('patch') or opts.get('stat'):
2053 2051 # When following files, track renames via a special matcher.
2054 2052 # If we're forced to take the slowpath it means we're following
2055 2053 # at least one pattern/directory, so don't bother with rename tracking.
2056 2054 if follow and not match.always() and not slowpath:
2057 2055 # _makefollowlogfilematcher expects its files argument to be
2058 2056 # relative to the repo root, so use match.files(), not pats.
2059 2057 filematcher = _makefollowlogfilematcher(repo, match.files(),
2060 2058 followfirst)
2061 2059 else:
2062 2060 filematcher = _makenofollowlogfilematcher(repo, pats, opts)
2063 2061 if filematcher is None:
2064 2062 filematcher = lambda rev: match
2065 2063
2066 2064 expr = []
2067 2065 for op, val in sorted(opts.iteritems()):
2068 2066 if not val:
2069 2067 continue
2070 2068 if op not in opt2revset:
2071 2069 continue
2072 2070 revop, andor = opt2revset[op]
2073 2071 if '%(val)' not in revop:
2074 2072 expr.append(revop)
2075 2073 else:
2076 2074 if not isinstance(val, list):
2077 2075 e = revop % {'val': val}
2078 2076 else:
2079 2077 e = '(' + andor.join((revop % {'val': v}) for v in val) + ')'
2080 2078 expr.append(e)
2081 2079
2082 2080 if expr:
2083 2081 expr = '(' + ' and '.join(expr) + ')'
2084 2082 else:
2085 2083 expr = None
2086 2084 return expr, filematcher
2087 2085
2088 2086 def _logrevs(repo, opts):
2089 2087 # Default --rev value depends on --follow but --follow behavior
2090 2088 # depends on revisions resolved from --rev...
2091 2089 follow = opts.get('follow') or opts.get('follow_first')
2092 2090 if opts.get('rev'):
2093 2091 revs = scmutil.revrange(repo, opts['rev'])
2094 2092 elif follow and repo.dirstate.p1() == nullid:
2095 2093 revs = revset.baseset()
2096 2094 elif follow:
2097 2095 revs = repo.revs('reverse(:.)')
2098 2096 else:
2099 2097 revs = revset.spanset(repo)
2100 2098 revs.reverse()
2101 2099 return revs
2102 2100
2103 2101 def getgraphlogrevs(repo, pats, opts):
2104 2102 """Return (revs, expr, filematcher) where revs is an iterable of
2105 2103 revision numbers, expr is a revset string built from log options
2106 2104 and file patterns or None, and used to filter 'revs'. If --stat or
2107 2105 --patch are not passed filematcher is None. Otherwise it is a
2108 2106 callable taking a revision number and returning a match objects
2109 2107 filtering the files to be detailed when displaying the revision.
2110 2108 """
2111 2109 limit = loglimit(opts)
2112 2110 revs = _logrevs(repo, opts)
2113 2111 if not revs:
2114 2112 return revset.baseset(), None, None
2115 2113 expr, filematcher = _makelogrevset(repo, pats, opts, revs)
2116 2114 if opts.get('rev'):
2117 2115 # User-specified revs might be unsorted, but don't sort before
2118 2116 # _makelogrevset because it might depend on the order of revs
2119 2117 if not (revs.isdescending() or revs.istopo()):
2120 2118 revs.sort(reverse=True)
2121 2119 if expr:
2122 2120 matcher = revset.match(repo.ui, expr, order=revset.followorder)
2123 2121 revs = matcher(repo, revs)
2124 2122 if limit is not None:
2125 2123 limitedrevs = []
2126 2124 for idx, rev in enumerate(revs):
2127 2125 if idx >= limit:
2128 2126 break
2129 2127 limitedrevs.append(rev)
2130 2128 revs = revset.baseset(limitedrevs)
2131 2129
2132 2130 return revs, expr, filematcher
2133 2131
2134 2132 def getlogrevs(repo, pats, opts):
2135 2133 """Return (revs, expr, filematcher) where revs is an iterable of
2136 2134 revision numbers, expr is a revset string built from log options
2137 2135 and file patterns or None, and used to filter 'revs'. If --stat or
2138 2136 --patch are not passed filematcher is None. Otherwise it is a
2139 2137 callable taking a revision number and returning a match objects
2140 2138 filtering the files to be detailed when displaying the revision.
2141 2139 """
2142 2140 limit = loglimit(opts)
2143 2141 revs = _logrevs(repo, opts)
2144 2142 if not revs:
2145 2143 return revset.baseset([]), None, None
2146 2144 expr, filematcher = _makelogrevset(repo, pats, opts, revs)
2147 2145 if expr:
2148 2146 matcher = revset.match(repo.ui, expr, order=revset.followorder)
2149 2147 revs = matcher(repo, revs)
2150 2148 if limit is not None:
2151 2149 limitedrevs = []
2152 2150 for idx, r in enumerate(revs):
2153 2151 if limit <= idx:
2154 2152 break
2155 2153 limitedrevs.append(r)
2156 2154 revs = revset.baseset(limitedrevs)
2157 2155
2158 2156 return revs, expr, filematcher
2159 2157
2160 2158 def _graphnodeformatter(ui, displayer):
2161 2159 spec = ui.config('ui', 'graphnodetemplate')
2162 2160 if not spec:
2163 2161 return templatekw.showgraphnode # fast path for "{graphnode}"
2164 2162
2165 2163 templ = formatter.gettemplater(ui, 'graphnode', spec)
2166 2164 cache = {}
2167 2165 if isinstance(displayer, changeset_templater):
2168 2166 cache = displayer.cache # reuse cache of slow templates
2169 2167 props = templatekw.keywords.copy()
2170 2168 props['templ'] = templ
2171 2169 props['cache'] = cache
2172 2170 def formatnode(repo, ctx):
2173 2171 props['ctx'] = ctx
2174 2172 props['repo'] = repo
2175 2173 props['ui'] = repo.ui
2176 2174 props['revcache'] = {}
2177 2175 return templater.stringify(templ('graphnode', **props))
2178 2176 return formatnode
2179 2177
2180 2178 def displaygraph(ui, repo, dag, displayer, edgefn, getrenamed=None,
2181 2179 filematcher=None):
2182 2180 formatnode = _graphnodeformatter(ui, displayer)
2183 2181 state = graphmod.asciistate()
2184 2182 styles = state['styles']
2185 2183
2186 2184 # only set graph styling if HGPLAIN is not set.
2187 2185 if ui.plain('graph'):
2188 2186 # set all edge styles to |, the default pre-3.8 behaviour
2189 2187 styles.update(dict.fromkeys(styles, '|'))
2190 2188 else:
2191 2189 edgetypes = {
2192 2190 'parent': graphmod.PARENT,
2193 2191 'grandparent': graphmod.GRANDPARENT,
2194 2192 'missing': graphmod.MISSINGPARENT
2195 2193 }
2196 2194 for name, key in edgetypes.items():
2197 2195 # experimental config: experimental.graphstyle.*
2198 2196 styles[key] = ui.config('experimental', 'graphstyle.%s' % name,
2199 2197 styles[key])
2200 2198 if not styles[key]:
2201 2199 styles[key] = None
2202 2200
2203 2201 # experimental config: experimental.graphshorten
2204 2202 state['graphshorten'] = ui.configbool('experimental', 'graphshorten')
2205 2203
2206 2204 for rev, type, ctx, parents in dag:
2207 2205 char = formatnode(repo, ctx)
2208 2206 copies = None
2209 2207 if getrenamed and ctx.rev():
2210 2208 copies = []
2211 2209 for fn in ctx.files():
2212 2210 rename = getrenamed(fn, ctx.rev())
2213 2211 if rename:
2214 2212 copies.append((fn, rename[0]))
2215 2213 revmatchfn = None
2216 2214 if filematcher is not None:
2217 2215 revmatchfn = filematcher(ctx.rev())
2218 2216 displayer.show(ctx, copies=copies, matchfn=revmatchfn)
2219 2217 lines = displayer.hunk.pop(rev).split('\n')
2220 2218 if not lines[-1]:
2221 2219 del lines[-1]
2222 2220 displayer.flush(ctx)
2223 2221 edges = edgefn(type, char, lines, state, rev, parents)
2224 2222 for type, char, lines, coldata in edges:
2225 2223 graphmod.ascii(ui, state, type, char, lines, coldata)
2226 2224 displayer.close()
2227 2225
2228 2226 def graphlog(ui, repo, *pats, **opts):
2229 2227 # Parameters are identical to log command ones
2230 2228 revs, expr, filematcher = getgraphlogrevs(repo, pats, opts)
2231 2229 revdag = graphmod.dagwalker(repo, revs)
2232 2230
2233 2231 getrenamed = None
2234 2232 if opts.get('copies'):
2235 2233 endrev = None
2236 2234 if opts.get('rev'):
2237 2235 endrev = scmutil.revrange(repo, opts.get('rev')).max() + 1
2238 2236 getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
2239 2237 displayer = show_changeset(ui, repo, opts, buffered=True)
2240 2238 displaygraph(ui, repo, revdag, displayer, graphmod.asciiedges, getrenamed,
2241 2239 filematcher)
2242 2240
2243 2241 def checkunsupportedgraphflags(pats, opts):
2244 2242 for op in ["newest_first"]:
2245 2243 if op in opts and opts[op]:
2246 2244 raise error.Abort(_("-G/--graph option is incompatible with --%s")
2247 2245 % op.replace("_", "-"))
2248 2246
2249 2247 def graphrevs(repo, nodes, opts):
2250 2248 limit = loglimit(opts)
2251 2249 nodes.reverse()
2252 2250 if limit is not None:
2253 2251 nodes = nodes[:limit]
2254 2252 return graphmod.nodes(repo, nodes)
2255 2253
2256 2254 def add(ui, repo, match, prefix, explicitonly, **opts):
2257 2255 join = lambda f: os.path.join(prefix, f)
2258 2256 bad = []
2259 2257
2260 2258 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2261 2259 names = []
2262 2260 wctx = repo[None]
2263 2261 cca = None
2264 2262 abort, warn = scmutil.checkportabilityalert(ui)
2265 2263 if abort or warn:
2266 2264 cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
2267 2265
2268 2266 badmatch = matchmod.badmatch(match, badfn)
2269 2267 dirstate = repo.dirstate
2270 2268 # We don't want to just call wctx.walk here, since it would return a lot of
2271 2269 # clean files, which we aren't interested in and takes time.
2272 2270 for f in sorted(dirstate.walk(badmatch, sorted(wctx.substate),
2273 2271 True, False, full=False)):
2274 2272 exact = match.exact(f)
2275 2273 if exact or not explicitonly and f not in wctx and repo.wvfs.lexists(f):
2276 2274 if cca:
2277 2275 cca(f)
2278 2276 names.append(f)
2279 2277 if ui.verbose or not exact:
2280 2278 ui.status(_('adding %s\n') % match.rel(f))
2281 2279
2282 2280 for subpath in sorted(wctx.substate):
2283 2281 sub = wctx.sub(subpath)
2284 2282 try:
2285 2283 submatch = matchmod.subdirmatcher(subpath, match)
2286 2284 if opts.get('subrepos'):
2287 2285 bad.extend(sub.add(ui, submatch, prefix, False, **opts))
2288 2286 else:
2289 2287 bad.extend(sub.add(ui, submatch, prefix, True, **opts))
2290 2288 except error.LookupError:
2291 2289 ui.status(_("skipping missing subrepository: %s\n")
2292 2290 % join(subpath))
2293 2291
2294 2292 if not opts.get('dry_run'):
2295 2293 rejected = wctx.add(names, prefix)
2296 2294 bad.extend(f for f in rejected if f in match.files())
2297 2295 return bad
2298 2296
2299 2297 def forget(ui, repo, match, prefix, explicitonly):
2300 2298 join = lambda f: os.path.join(prefix, f)
2301 2299 bad = []
2302 2300 badfn = lambda x, y: bad.append(x) or match.bad(x, y)
2303 2301 wctx = repo[None]
2304 2302 forgot = []
2305 2303
2306 2304 s = repo.status(match=matchmod.badmatch(match, badfn), clean=True)
2307 2305 forget = sorted(s[0] + s[1] + s[3] + s[6])
2308 2306 if explicitonly:
2309 2307 forget = [f for f in forget if match.exact(f)]
2310 2308
2311 2309 for subpath in sorted(wctx.substate):
2312 2310 sub = wctx.sub(subpath)
2313 2311 try:
2314 2312 submatch = matchmod.subdirmatcher(subpath, match)
2315 2313 subbad, subforgot = sub.forget(submatch, prefix)
2316 2314 bad.extend([subpath + '/' + f for f in subbad])
2317 2315 forgot.extend([subpath + '/' + f for f in subforgot])
2318 2316 except error.LookupError:
2319 2317 ui.status(_("skipping missing subrepository: %s\n")
2320 2318 % join(subpath))
2321 2319
2322 2320 if not explicitonly:
2323 2321 for f in match.files():
2324 2322 if f not in repo.dirstate and not repo.wvfs.isdir(f):
2325 2323 if f not in forgot:
2326 2324 if repo.wvfs.exists(f):
2327 2325 # Don't complain if the exact case match wasn't given.
2328 2326 # But don't do this until after checking 'forgot', so
2329 2327 # that subrepo files aren't normalized, and this op is
2330 2328 # purely from data cached by the status walk above.
2331 2329 if repo.dirstate.normalize(f) in repo.dirstate:
2332 2330 continue
2333 2331 ui.warn(_('not removing %s: '
2334 2332 'file is already untracked\n')
2335 2333 % match.rel(f))
2336 2334 bad.append(f)
2337 2335
2338 2336 for f in forget:
2339 2337 if ui.verbose or not match.exact(f):
2340 2338 ui.status(_('removing %s\n') % match.rel(f))
2341 2339
2342 2340 rejected = wctx.forget(forget, prefix)
2343 2341 bad.extend(f for f in rejected if f in match.files())
2344 2342 forgot.extend(f for f in forget if f not in rejected)
2345 2343 return bad, forgot
2346 2344
2347 2345 def files(ui, ctx, m, fm, fmt, subrepos):
2348 2346 rev = ctx.rev()
2349 2347 ret = 1
2350 2348 ds = ctx.repo().dirstate
2351 2349
2352 2350 for f in ctx.matches(m):
2353 2351 if rev is None and ds[f] == 'r':
2354 2352 continue
2355 2353 fm.startitem()
2356 2354 if ui.verbose:
2357 2355 fc = ctx[f]
2358 2356 fm.write('size flags', '% 10d % 1s ', fc.size(), fc.flags())
2359 2357 fm.data(abspath=f)
2360 2358 fm.write('path', fmt, m.rel(f))
2361 2359 ret = 0
2362 2360
2363 2361 for subpath in sorted(ctx.substate):
2364 2362 submatch = matchmod.subdirmatcher(subpath, m)
2365 2363 if (subrepos or m.exact(subpath) or any(submatch.files())):
2366 2364 sub = ctx.sub(subpath)
2367 2365 try:
2368 2366 recurse = m.exact(subpath) or subrepos
2369 2367 if sub.printfiles(ui, submatch, fm, fmt, recurse) == 0:
2370 2368 ret = 0
2371 2369 except error.LookupError:
2372 2370 ui.status(_("skipping missing subrepository: %s\n")
2373 2371 % m.abs(subpath))
2374 2372
2375 2373 return ret
2376 2374
2377 2375 def remove(ui, repo, m, prefix, after, force, subrepos, warnings=None):
2378 2376 join = lambda f: os.path.join(prefix, f)
2379 2377 ret = 0
2380 2378 s = repo.status(match=m, clean=True)
2381 2379 modified, added, deleted, clean = s[0], s[1], s[3], s[6]
2382 2380
2383 2381 wctx = repo[None]
2384 2382
2385 2383 if warnings is None:
2386 2384 warnings = []
2387 2385 warn = True
2388 2386 else:
2389 2387 warn = False
2390 2388
2391 2389 subs = sorted(wctx.substate)
2392 2390 total = len(subs)
2393 2391 count = 0
2394 2392 for subpath in subs:
2395 2393 count += 1
2396 2394 submatch = matchmod.subdirmatcher(subpath, m)
2397 2395 if subrepos or m.exact(subpath) or any(submatch.files()):
2398 2396 ui.progress(_('searching'), count, total=total, unit=_('subrepos'))
2399 2397 sub = wctx.sub(subpath)
2400 2398 try:
2401 2399 if sub.removefiles(submatch, prefix, after, force, subrepos,
2402 2400 warnings):
2403 2401 ret = 1
2404 2402 except error.LookupError:
2405 2403 warnings.append(_("skipping missing subrepository: %s\n")
2406 2404 % join(subpath))
2407 2405 ui.progress(_('searching'), None)
2408 2406
2409 2407 # warn about failure to delete explicit files/dirs
2410 2408 deleteddirs = util.dirs(deleted)
2411 2409 files = m.files()
2412 2410 total = len(files)
2413 2411 count = 0
2414 2412 for f in files:
2415 2413 def insubrepo():
2416 2414 for subpath in wctx.substate:
2417 2415 if f.startswith(subpath + '/'):
2418 2416 return True
2419 2417 return False
2420 2418
2421 2419 count += 1
2422 2420 ui.progress(_('deleting'), count, total=total, unit=_('files'))
2423 2421 isdir = f in deleteddirs or wctx.hasdir(f)
2424 2422 if (f in repo.dirstate or isdir or f == '.'
2425 2423 or insubrepo() or f in subs):
2426 2424 continue
2427 2425
2428 2426 if repo.wvfs.exists(f):
2429 2427 if repo.wvfs.isdir(f):
2430 2428 warnings.append(_('not removing %s: no tracked files\n')
2431 2429 % m.rel(f))
2432 2430 else:
2433 2431 warnings.append(_('not removing %s: file is untracked\n')
2434 2432 % m.rel(f))
2435 2433 # missing files will generate a warning elsewhere
2436 2434 ret = 1
2437 2435 ui.progress(_('deleting'), None)
2438 2436
2439 2437 if force:
2440 2438 list = modified + deleted + clean + added
2441 2439 elif after:
2442 2440 list = deleted
2443 2441 remaining = modified + added + clean
2444 2442 total = len(remaining)
2445 2443 count = 0
2446 2444 for f in remaining:
2447 2445 count += 1
2448 2446 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2449 2447 warnings.append(_('not removing %s: file still exists\n')
2450 2448 % m.rel(f))
2451 2449 ret = 1
2452 2450 ui.progress(_('skipping'), None)
2453 2451 else:
2454 2452 list = deleted + clean
2455 2453 total = len(modified) + len(added)
2456 2454 count = 0
2457 2455 for f in modified:
2458 2456 count += 1
2459 2457 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2460 2458 warnings.append(_('not removing %s: file is modified (use -f'
2461 2459 ' to force removal)\n') % m.rel(f))
2462 2460 ret = 1
2463 2461 for f in added:
2464 2462 count += 1
2465 2463 ui.progress(_('skipping'), count, total=total, unit=_('files'))
2466 2464 warnings.append(_("not removing %s: file has been marked for add"
2467 2465 " (use 'hg forget' to undo add)\n") % m.rel(f))
2468 2466 ret = 1
2469 2467 ui.progress(_('skipping'), None)
2470 2468
2471 2469 list = sorted(list)
2472 2470 total = len(list)
2473 2471 count = 0
2474 2472 for f in list:
2475 2473 count += 1
2476 2474 if ui.verbose or not m.exact(f):
2477 2475 ui.progress(_('deleting'), count, total=total, unit=_('files'))
2478 2476 ui.status(_('removing %s\n') % m.rel(f))
2479 2477 ui.progress(_('deleting'), None)
2480 2478
2481 2479 with repo.wlock():
2482 2480 if not after:
2483 2481 for f in list:
2484 2482 if f in added:
2485 2483 continue # we never unlink added files on remove
2486 2484 util.unlinkpath(repo.wjoin(f), ignoremissing=True)
2487 2485 repo[None].forget(list)
2488 2486
2489 2487 if warn:
2490 2488 for warning in warnings:
2491 2489 ui.warn(warning)
2492 2490
2493 2491 return ret
2494 2492
2495 2493 def cat(ui, repo, ctx, matcher, prefix, **opts):
2496 2494 err = 1
2497 2495
2498 2496 def write(path):
2499 2497 fp = makefileobj(repo, opts.get('output'), ctx.node(),
2500 2498 pathname=os.path.join(prefix, path))
2501 2499 data = ctx[path].data()
2502 2500 if opts.get('decode'):
2503 2501 data = repo.wwritedata(path, data)
2504 2502 fp.write(data)
2505 2503 fp.close()
2506 2504
2507 2505 # Automation often uses hg cat on single files, so special case it
2508 2506 # for performance to avoid the cost of parsing the manifest.
2509 2507 if len(matcher.files()) == 1 and not matcher.anypats():
2510 2508 file = matcher.files()[0]
2511 2509 mfl = repo.manifestlog
2512 2510 mfnode = ctx.manifestnode()
2513 2511 try:
2514 2512 if mfnode and mfl[mfnode].find(file)[0]:
2515 2513 write(file)
2516 2514 return 0
2517 2515 except KeyError:
2518 2516 pass
2519 2517
2520 2518 for abs in ctx.walk(matcher):
2521 2519 write(abs)
2522 2520 err = 0
2523 2521
2524 2522 for subpath in sorted(ctx.substate):
2525 2523 sub = ctx.sub(subpath)
2526 2524 try:
2527 2525 submatch = matchmod.subdirmatcher(subpath, matcher)
2528 2526
2529 2527 if not sub.cat(submatch, os.path.join(prefix, sub._path),
2530 2528 **opts):
2531 2529 err = 0
2532 2530 except error.RepoLookupError:
2533 2531 ui.status(_("skipping missing subrepository: %s\n")
2534 2532 % os.path.join(prefix, subpath))
2535 2533
2536 2534 return err
2537 2535
2538 2536 def commit(ui, repo, commitfunc, pats, opts):
2539 2537 '''commit the specified files or all outstanding changes'''
2540 2538 date = opts.get('date')
2541 2539 if date:
2542 2540 opts['date'] = util.parsedate(date)
2543 2541 message = logmessage(ui, opts)
2544 2542 matcher = scmutil.match(repo[None], pats, opts)
2545 2543
2546 2544 # extract addremove carefully -- this function can be called from a command
2547 2545 # that doesn't support addremove
2548 2546 if opts.get('addremove'):
2549 2547 if scmutil.addremove(repo, matcher, "", opts) != 0:
2550 2548 raise error.Abort(
2551 2549 _("failed to mark all new/missing files as added/removed"))
2552 2550
2553 2551 return commitfunc(ui, repo, message, matcher, opts)
2554 2552
2555 2553 def samefile(f, ctx1, ctx2):
2556 2554 if f in ctx1.manifest():
2557 2555 a = ctx1.filectx(f)
2558 2556 if f in ctx2.manifest():
2559 2557 b = ctx2.filectx(f)
2560 2558 return (not a.cmp(b)
2561 2559 and a.flags() == b.flags())
2562 2560 else:
2563 2561 return False
2564 2562 else:
2565 2563 return f not in ctx2.manifest()
2566 2564
2567 2565 def amend(ui, repo, commitfunc, old, extra, pats, opts):
2568 2566 # avoid cycle context -> subrepo -> cmdutil
2569 2567 from . import context
2570 2568
2571 2569 # amend will reuse the existing user if not specified, but the obsolete
2572 2570 # marker creation requires that the current user's name is specified.
2573 2571 if obsolete.isenabled(repo, obsolete.createmarkersopt):
2574 2572 ui.username() # raise exception if username not set
2575 2573
2576 2574 ui.note(_('amending changeset %s\n') % old)
2577 2575 base = old.p1()
2578 2576 createmarkers = obsolete.isenabled(repo, obsolete.createmarkersopt)
2579 2577
2580 2578 wlock = lock = newid = None
2581 2579 try:
2582 2580 wlock = repo.wlock()
2583 2581 lock = repo.lock()
2584 2582 with repo.transaction('amend') as tr:
2585 2583 # See if we got a message from -m or -l, if not, open the editor
2586 2584 # with the message of the changeset to amend
2587 2585 message = logmessage(ui, opts)
2588 2586 # ensure logfile does not conflict with later enforcement of the
2589 2587 # message. potential logfile content has been processed by
2590 2588 # `logmessage` anyway.
2591 2589 opts.pop('logfile')
2592 2590 # First, do a regular commit to record all changes in the working
2593 2591 # directory (if there are any)
2594 2592 ui.callhooks = False
2595 2593 activebookmark = repo._bookmarks.active
2596 2594 try:
2597 2595 repo._bookmarks.active = None
2598 2596 opts['message'] = 'temporary amend commit for %s' % old
2599 2597 node = commit(ui, repo, commitfunc, pats, opts)
2600 2598 finally:
2601 2599 repo._bookmarks.active = activebookmark
2602 2600 repo._bookmarks.recordchange(tr)
2603 2601 ui.callhooks = True
2604 2602 ctx = repo[node]
2605 2603
2606 2604 # Participating changesets:
2607 2605 #
2608 2606 # node/ctx o - new (intermediate) commit that contains changes
2609 2607 # | from working dir to go into amending commit
2610 2608 # | (or a workingctx if there were no changes)
2611 2609 # |
2612 2610 # old o - changeset to amend
2613 2611 # |
2614 2612 # base o - parent of amending changeset
2615 2613
2616 2614 # Update extra dict from amended commit (e.g. to preserve graft
2617 2615 # source)
2618 2616 extra.update(old.extra())
2619 2617
2620 2618 # Also update it from the intermediate commit or from the wctx
2621 2619 extra.update(ctx.extra())
2622 2620
2623 2621 if len(old.parents()) > 1:
2624 2622 # ctx.files() isn't reliable for merges, so fall back to the
2625 2623 # slower repo.status() method
2626 2624 files = set([fn for st in repo.status(base, old)[:3]
2627 2625 for fn in st])
2628 2626 else:
2629 2627 files = set(old.files())
2630 2628
2631 2629 # Second, we use either the commit we just did, or if there were no
2632 2630 # changes the parent of the working directory as the version of the
2633 2631 # files in the final amend commit
2634 2632 if node:
2635 2633 ui.note(_('copying changeset %s to %s\n') % (ctx, base))
2636 2634
2637 2635 user = ctx.user()
2638 2636 date = ctx.date()
2639 2637 # Recompute copies (avoid recording a -> b -> a)
2640 2638 copied = copies.pathcopies(base, ctx)
2641 2639 if old.p2:
2642 2640 copied.update(copies.pathcopies(old.p2(), ctx))
2643 2641
2644 2642 # Prune files which were reverted by the updates: if old
2645 2643 # introduced file X and our intermediate commit, node,
2646 2644 # renamed that file, then those two files are the same and
2647 2645 # we can discard X from our list of files. Likewise if X
2648 2646 # was deleted, it's no longer relevant
2649 2647 files.update(ctx.files())
2650 2648 files = [f for f in files if not samefile(f, ctx, base)]
2651 2649
2652 2650 def filectxfn(repo, ctx_, path):
2653 2651 try:
2654 2652 fctx = ctx[path]
2655 2653 flags = fctx.flags()
2656 2654 mctx = context.memfilectx(repo,
2657 2655 fctx.path(), fctx.data(),
2658 2656 islink='l' in flags,
2659 2657 isexec='x' in flags,
2660 2658 copied=copied.get(path))
2661 2659 return mctx
2662 2660 except KeyError:
2663 2661 return None
2664 2662 else:
2665 2663 ui.note(_('copying changeset %s to %s\n') % (old, base))
2666 2664
2667 2665 # Use version of files as in the old cset
2668 2666 def filectxfn(repo, ctx_, path):
2669 2667 try:
2670 2668 return old.filectx(path)
2671 2669 except KeyError:
2672 2670 return None
2673 2671
2674 2672 user = opts.get('user') or old.user()
2675 2673 date = opts.get('date') or old.date()
2676 2674 editform = mergeeditform(old, 'commit.amend')
2677 2675 editor = getcommiteditor(editform=editform, **opts)
2678 2676 if not message:
2679 2677 editor = getcommiteditor(edit=True, editform=editform)
2680 2678 message = old.description()
2681 2679
2682 2680 pureextra = extra.copy()
2683 2681 extra['amend_source'] = old.hex()
2684 2682
2685 2683 new = context.memctx(repo,
2686 2684 parents=[base.node(), old.p2().node()],
2687 2685 text=message,
2688 2686 files=files,
2689 2687 filectxfn=filectxfn,
2690 2688 user=user,
2691 2689 date=date,
2692 2690 extra=extra,
2693 2691 editor=editor)
2694 2692
2695 2693 newdesc = changelog.stripdesc(new.description())
2696 2694 if ((not node)
2697 2695 and newdesc == old.description()
2698 2696 and user == old.user()
2699 2697 and date == old.date()
2700 2698 and pureextra == old.extra()):
2701 2699 # nothing changed. continuing here would create a new node
2702 2700 # anyway because of the amend_source noise.
2703 2701 #
2704 2702 # This not what we expect from amend.
2705 2703 return old.node()
2706 2704
2707 2705 ph = repo.ui.config('phases', 'new-commit', phases.draft)
2708 2706 try:
2709 2707 if opts.get('secret'):
2710 2708 commitphase = 'secret'
2711 2709 else:
2712 2710 commitphase = old.phase()
2713 2711 repo.ui.setconfig('phases', 'new-commit', commitphase, 'amend')
2714 2712 newid = repo.commitctx(new)
2715 2713 finally:
2716 2714 repo.ui.setconfig('phases', 'new-commit', ph, 'amend')
2717 2715 if newid != old.node():
2718 2716 # Reroute the working copy parent to the new changeset
2719 2717 repo.setparents(newid, nullid)
2720 2718
2721 2719 # Move bookmarks from old parent to amend commit
2722 2720 bms = repo.nodebookmarks(old.node())
2723 2721 if bms:
2724 2722 marks = repo._bookmarks
2725 2723 for bm in bms:
2726 2724 ui.debug('moving bookmarks %r from %s to %s\n' %
2727 2725 (marks, old.hex(), hex(newid)))
2728 2726 marks[bm] = newid
2729 2727 marks.recordchange(tr)
2730 2728 #commit the whole amend process
2731 2729 if createmarkers:
2732 2730 # mark the new changeset as successor of the rewritten one
2733 2731 new = repo[newid]
2734 2732 obs = [(old, (new,))]
2735 2733 if node:
2736 2734 obs.append((ctx, ()))
2737 2735
2738 2736 obsolete.createmarkers(repo, obs)
2739 2737 if not createmarkers and newid != old.node():
2740 2738 # Strip the intermediate commit (if there was one) and the amended
2741 2739 # commit
2742 2740 if node:
2743 2741 ui.note(_('stripping intermediate changeset %s\n') % ctx)
2744 2742 ui.note(_('stripping amended changeset %s\n') % old)
2745 2743 repair.strip(ui, repo, old.node(), topic='amend-backup')
2746 2744 finally:
2747 2745 lockmod.release(lock, wlock)
2748 2746 return newid
2749 2747
2750 2748 def commiteditor(repo, ctx, subs, editform=''):
2751 2749 if ctx.description():
2752 2750 return ctx.description()
2753 2751 return commitforceeditor(repo, ctx, subs, editform=editform,
2754 2752 unchangedmessagedetection=True)
2755 2753
2756 2754 def commitforceeditor(repo, ctx, subs, finishdesc=None, extramsg=None,
2757 2755 editform='', unchangedmessagedetection=False):
2758 2756 if not extramsg:
2759 2757 extramsg = _("Leave message empty to abort commit.")
2760 2758
2761 2759 forms = [e for e in editform.split('.') if e]
2762 2760 forms.insert(0, 'changeset')
2763 2761 templatetext = None
2764 2762 while forms:
2765 2763 tmpl = repo.ui.config('committemplate', '.'.join(forms))
2766 2764 if tmpl:
2767 2765 templatetext = committext = buildcommittemplate(
2768 2766 repo, ctx, subs, extramsg, tmpl)
2769 2767 break
2770 2768 forms.pop()
2771 2769 else:
2772 2770 committext = buildcommittext(repo, ctx, subs, extramsg)
2773 2771
2774 2772 # run editor in the repository root
2775 2773 olddir = pycompat.getcwd()
2776 2774 os.chdir(repo.root)
2777 2775
2778 2776 # make in-memory changes visible to external process
2779 2777 tr = repo.currenttransaction()
2780 2778 repo.dirstate.write(tr)
2781 2779 pending = tr and tr.writepending() and repo.root
2782 2780
2783 2781 editortext = repo.ui.edit(committext, ctx.user(), ctx.extra(),
2784 2782 editform=editform, pending=pending,
2785 2783 repopath=repo.path)
2786 2784 text = editortext
2787 2785
2788 2786 # strip away anything below this special string (used for editors that want
2789 2787 # to display the diff)
2790 2788 stripbelow = re.search(_linebelow, text, flags=re.MULTILINE)
2791 2789 if stripbelow:
2792 2790 text = text[:stripbelow.start()]
2793 2791
2794 2792 text = re.sub("(?m)^HG:.*(\n|$)", "", text)
2795 2793 os.chdir(olddir)
2796 2794
2797 2795 if finishdesc:
2798 2796 text = finishdesc(text)
2799 2797 if not text.strip():
2800 2798 raise error.Abort(_("empty commit message"))
2801 2799 if unchangedmessagedetection and editortext == templatetext:
2802 2800 raise error.Abort(_("commit message unchanged"))
2803 2801
2804 2802 return text
2805 2803
2806 2804 def buildcommittemplate(repo, ctx, subs, extramsg, tmpl):
2807 2805 ui = repo.ui
2808 2806 tmpl, mapfile = gettemplate(ui, tmpl, None)
2809 2807
2810 2808 t = changeset_templater(ui, repo, None, {}, tmpl, mapfile, False)
2811 2809
2812 2810 for k, v in repo.ui.configitems('committemplate'):
2813 2811 if k != 'changeset':
2814 2812 t.t.cache[k] = v
2815 2813
2816 2814 if not extramsg:
2817 2815 extramsg = '' # ensure that extramsg is string
2818 2816
2819 2817 ui.pushbuffer()
2820 2818 t.show(ctx, extramsg=extramsg)
2821 2819 return ui.popbuffer()
2822 2820
2823 2821 def hgprefix(msg):
2824 2822 return "\n".join(["HG: %s" % a for a in msg.split("\n") if a])
2825 2823
2826 2824 def buildcommittext(repo, ctx, subs, extramsg):
2827 2825 edittext = []
2828 2826 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
2829 2827 if ctx.description():
2830 2828 edittext.append(ctx.description())
2831 2829 edittext.append("")
2832 2830 edittext.append("") # Empty line between message and comments.
2833 2831 edittext.append(hgprefix(_("Enter commit message."
2834 2832 " Lines beginning with 'HG:' are removed.")))
2835 2833 edittext.append(hgprefix(extramsg))
2836 2834 edittext.append("HG: --")
2837 2835 edittext.append(hgprefix(_("user: %s") % ctx.user()))
2838 2836 if ctx.p2():
2839 2837 edittext.append(hgprefix(_("branch merge")))
2840 2838 if ctx.branch():
2841 2839 edittext.append(hgprefix(_("branch '%s'") % ctx.branch()))
2842 2840 if bookmarks.isactivewdirparent(repo):
2843 2841 edittext.append(hgprefix(_("bookmark '%s'") % repo._activebookmark))
2844 2842 edittext.extend([hgprefix(_("subrepo %s") % s) for s in subs])
2845 2843 edittext.extend([hgprefix(_("added %s") % f) for f in added])
2846 2844 edittext.extend([hgprefix(_("changed %s") % f) for f in modified])
2847 2845 edittext.extend([hgprefix(_("removed %s") % f) for f in removed])
2848 2846 if not added and not modified and not removed:
2849 2847 edittext.append(hgprefix(_("no files changed")))
2850 2848 edittext.append("")
2851 2849
2852 2850 return "\n".join(edittext)
2853 2851
2854 2852 def commitstatus(repo, node, branch, bheads=None, opts=None):
2855 2853 if opts is None:
2856 2854 opts = {}
2857 2855 ctx = repo[node]
2858 2856 parents = ctx.parents()
2859 2857
2860 2858 if (not opts.get('amend') and bheads and node not in bheads and not
2861 2859 [x for x in parents if x.node() in bheads and x.branch() == branch]):
2862 2860 repo.ui.status(_('created new head\n'))
2863 2861 # The message is not printed for initial roots. For the other
2864 2862 # changesets, it is printed in the following situations:
2865 2863 #
2866 2864 # Par column: for the 2 parents with ...
2867 2865 # N: null or no parent
2868 2866 # B: parent is on another named branch
2869 2867 # C: parent is a regular non head changeset
2870 2868 # H: parent was a branch head of the current branch
2871 2869 # Msg column: whether we print "created new head" message
2872 2870 # In the following, it is assumed that there already exists some
2873 2871 # initial branch heads of the current branch, otherwise nothing is
2874 2872 # printed anyway.
2875 2873 #
2876 2874 # Par Msg Comment
2877 2875 # N N y additional topo root
2878 2876 #
2879 2877 # B N y additional branch root
2880 2878 # C N y additional topo head
2881 2879 # H N n usual case
2882 2880 #
2883 2881 # B B y weird additional branch root
2884 2882 # C B y branch merge
2885 2883 # H B n merge with named branch
2886 2884 #
2887 2885 # C C y additional head from merge
2888 2886 # C H n merge with a head
2889 2887 #
2890 2888 # H H n head merge: head count decreases
2891 2889
2892 2890 if not opts.get('close_branch'):
2893 2891 for r in parents:
2894 2892 if r.closesbranch() and r.branch() == branch:
2895 2893 repo.ui.status(_('reopening closed branch head %d\n') % r)
2896 2894
2897 2895 if repo.ui.debugflag:
2898 2896 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx.hex()))
2899 2897 elif repo.ui.verbose:
2900 2898 repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx))
2901 2899
2902 2900 def postcommitstatus(repo, pats, opts):
2903 2901 return repo.status(match=scmutil.match(repo[None], pats, opts))
2904 2902
2905 2903 def revert(ui, repo, ctx, parents, *pats, **opts):
2906 2904 parent, p2 = parents
2907 2905 node = ctx.node()
2908 2906
2909 2907 mf = ctx.manifest()
2910 2908 if node == p2:
2911 2909 parent = p2
2912 2910
2913 2911 # need all matching names in dirstate and manifest of target rev,
2914 2912 # so have to walk both. do not print errors if files exist in one
2915 2913 # but not other. in both cases, filesets should be evaluated against
2916 2914 # workingctx to get consistent result (issue4497). this means 'set:**'
2917 2915 # cannot be used to select missing files from target rev.
2918 2916
2919 2917 # `names` is a mapping for all elements in working copy and target revision
2920 2918 # The mapping is in the form:
2921 2919 # <asb path in repo> -> (<path from CWD>, <exactly specified by matcher?>)
2922 2920 names = {}
2923 2921
2924 2922 with repo.wlock():
2925 2923 ## filling of the `names` mapping
2926 2924 # walk dirstate to fill `names`
2927 2925
2928 2926 interactive = opts.get('interactive', False)
2929 2927 wctx = repo[None]
2930 2928 m = scmutil.match(wctx, pats, opts)
2931 2929
2932 2930 # we'll need this later
2933 2931 targetsubs = sorted(s for s in wctx.substate if m(s))
2934 2932
2935 2933 if not m.always():
2936 2934 for abs in repo.walk(matchmod.badmatch(m, lambda x, y: False)):
2937 2935 names[abs] = m.rel(abs), m.exact(abs)
2938 2936
2939 2937 # walk target manifest to fill `names`
2940 2938
2941 2939 def badfn(path, msg):
2942 2940 if path in names:
2943 2941 return
2944 2942 if path in ctx.substate:
2945 2943 return
2946 2944 path_ = path + '/'
2947 2945 for f in names:
2948 2946 if f.startswith(path_):
2949 2947 return
2950 2948 ui.warn("%s: %s\n" % (m.rel(path), msg))
2951 2949
2952 2950 for abs in ctx.walk(matchmod.badmatch(m, badfn)):
2953 2951 if abs not in names:
2954 2952 names[abs] = m.rel(abs), m.exact(abs)
2955 2953
2956 2954 # Find status of all file in `names`.
2957 2955 m = scmutil.matchfiles(repo, names)
2958 2956
2959 2957 changes = repo.status(node1=node, match=m,
2960 2958 unknown=True, ignored=True, clean=True)
2961 2959 else:
2962 2960 changes = repo.status(node1=node, match=m)
2963 2961 for kind in changes:
2964 2962 for abs in kind:
2965 2963 names[abs] = m.rel(abs), m.exact(abs)
2966 2964
2967 2965 m = scmutil.matchfiles(repo, names)
2968 2966
2969 2967 modified = set(changes.modified)
2970 2968 added = set(changes.added)
2971 2969 removed = set(changes.removed)
2972 2970 _deleted = set(changes.deleted)
2973 2971 unknown = set(changes.unknown)
2974 2972 unknown.update(changes.ignored)
2975 2973 clean = set(changes.clean)
2976 2974 modadded = set()
2977 2975
2978 2976 # split between files known in target manifest and the others
2979 2977 smf = set(mf)
2980 2978
2981 2979 # determine the exact nature of the deleted changesets
2982 2980 deladded = _deleted - smf
2983 2981 deleted = _deleted - deladded
2984 2982
2985 2983 # We need to account for the state of the file in the dirstate,
2986 2984 # even when we revert against something else than parent. This will
2987 2985 # slightly alter the behavior of revert (doing back up or not, delete
2988 2986 # or just forget etc).
2989 2987 if parent == node:
2990 2988 dsmodified = modified
2991 2989 dsadded = added
2992 2990 dsremoved = removed
2993 2991 # store all local modifications, useful later for rename detection
2994 2992 localchanges = dsmodified | dsadded
2995 2993 modified, added, removed = set(), set(), set()
2996 2994 else:
2997 2995 changes = repo.status(node1=parent, match=m)
2998 2996 dsmodified = set(changes.modified)
2999 2997 dsadded = set(changes.added)
3000 2998 dsremoved = set(changes.removed)
3001 2999 # store all local modifications, useful later for rename detection
3002 3000 localchanges = dsmodified | dsadded
3003 3001
3004 3002 # only take into account for removes between wc and target
3005 3003 clean |= dsremoved - removed
3006 3004 dsremoved &= removed
3007 3005 # distinct between dirstate remove and other
3008 3006 removed -= dsremoved
3009 3007
3010 3008 modadded = added & dsmodified
3011 3009 added -= modadded
3012 3010
3013 3011 # tell newly modified apart.
3014 3012 dsmodified &= modified
3015 3013 dsmodified |= modified & dsadded # dirstate added may need backup
3016 3014 modified -= dsmodified
3017 3015
3018 3016 # We need to wait for some post-processing to update this set
3019 3017 # before making the distinction. The dirstate will be used for
3020 3018 # that purpose.
3021 3019 dsadded = added
3022 3020
3023 3021 # in case of merge, files that are actually added can be reported as
3024 3022 # modified, we need to post process the result
3025 3023 if p2 != nullid:
3026 3024 mergeadd = dsmodified - smf
3027 3025 dsadded |= mergeadd
3028 3026 dsmodified -= mergeadd
3029 3027
3030 3028 # if f is a rename, update `names` to also revert the source
3031 3029 cwd = repo.getcwd()
3032 3030 for f in localchanges:
3033 3031 src = repo.dirstate.copied(f)
3034 3032 # XXX should we check for rename down to target node?
3035 3033 if src and src not in names and repo.dirstate[src] == 'r':
3036 3034 dsremoved.add(src)
3037 3035 names[src] = (repo.pathto(src, cwd), True)
3038 3036
3039 3037 # distinguish between file to forget and the other
3040 3038 added = set()
3041 3039 for abs in dsadded:
3042 3040 if repo.dirstate[abs] != 'a':
3043 3041 added.add(abs)
3044 3042 dsadded -= added
3045 3043
3046 3044 for abs in deladded:
3047 3045 if repo.dirstate[abs] == 'a':
3048 3046 dsadded.add(abs)
3049 3047 deladded -= dsadded
3050 3048
3051 3049 # For files marked as removed, we check if an unknown file is present at
3052 3050 # the same path. If a such file exists it may need to be backed up.
3053 3051 # Making the distinction at this stage helps have simpler backup
3054 3052 # logic.
3055 3053 removunk = set()
3056 3054 for abs in removed:
3057 3055 target = repo.wjoin(abs)
3058 3056 if os.path.lexists(target):
3059 3057 removunk.add(abs)
3060 3058 removed -= removunk
3061 3059
3062 3060 dsremovunk = set()
3063 3061 for abs in dsremoved:
3064 3062 target = repo.wjoin(abs)
3065 3063 if os.path.lexists(target):
3066 3064 dsremovunk.add(abs)
3067 3065 dsremoved -= dsremovunk
3068 3066
3069 3067 # action to be actually performed by revert
3070 3068 # (<list of file>, message>) tuple
3071 3069 actions = {'revert': ([], _('reverting %s\n')),
3072 3070 'add': ([], _('adding %s\n')),
3073 3071 'remove': ([], _('removing %s\n')),
3074 3072 'drop': ([], _('removing %s\n')),
3075 3073 'forget': ([], _('forgetting %s\n')),
3076 3074 'undelete': ([], _('undeleting %s\n')),
3077 3075 'noop': (None, _('no changes needed to %s\n')),
3078 3076 'unknown': (None, _('file not managed: %s\n')),
3079 3077 }
3080 3078
3081 3079 # "constant" that convey the backup strategy.
3082 3080 # All set to `discard` if `no-backup` is set do avoid checking
3083 3081 # no_backup lower in the code.
3084 3082 # These values are ordered for comparison purposes
3085 3083 backupinteractive = 3 # do backup if interactively modified
3086 3084 backup = 2 # unconditionally do backup
3087 3085 check = 1 # check if the existing file differs from target
3088 3086 discard = 0 # never do backup
3089 3087 if opts.get('no_backup'):
3090 3088 backupinteractive = backup = check = discard
3091 3089 if interactive:
3092 3090 dsmodifiedbackup = backupinteractive
3093 3091 else:
3094 3092 dsmodifiedbackup = backup
3095 3093 tobackup = set()
3096 3094
3097 3095 backupanddel = actions['remove']
3098 3096 if not opts.get('no_backup'):
3099 3097 backupanddel = actions['drop']
3100 3098
3101 3099 disptable = (
3102 3100 # dispatch table:
3103 3101 # file state
3104 3102 # action
3105 3103 # make backup
3106 3104
3107 3105 ## Sets that results that will change file on disk
3108 3106 # Modified compared to target, no local change
3109 3107 (modified, actions['revert'], discard),
3110 3108 # Modified compared to target, but local file is deleted
3111 3109 (deleted, actions['revert'], discard),
3112 3110 # Modified compared to target, local change
3113 3111 (dsmodified, actions['revert'], dsmodifiedbackup),
3114 3112 # Added since target
3115 3113 (added, actions['remove'], discard),
3116 3114 # Added in working directory
3117 3115 (dsadded, actions['forget'], discard),
3118 3116 # Added since target, have local modification
3119 3117 (modadded, backupanddel, backup),
3120 3118 # Added since target but file is missing in working directory
3121 3119 (deladded, actions['drop'], discard),
3122 3120 # Removed since target, before working copy parent
3123 3121 (removed, actions['add'], discard),
3124 3122 # Same as `removed` but an unknown file exists at the same path
3125 3123 (removunk, actions['add'], check),
3126 3124 # Removed since targe, marked as such in working copy parent
3127 3125 (dsremoved, actions['undelete'], discard),
3128 3126 # Same as `dsremoved` but an unknown file exists at the same path
3129 3127 (dsremovunk, actions['undelete'], check),
3130 3128 ## the following sets does not result in any file changes
3131 3129 # File with no modification
3132 3130 (clean, actions['noop'], discard),
3133 3131 # Existing file, not tracked anywhere
3134 3132 (unknown, actions['unknown'], discard),
3135 3133 )
3136 3134
3137 3135 for abs, (rel, exact) in sorted(names.items()):
3138 3136 # target file to be touch on disk (relative to cwd)
3139 3137 target = repo.wjoin(abs)
3140 3138 # search the entry in the dispatch table.
3141 3139 # if the file is in any of these sets, it was touched in the working
3142 3140 # directory parent and we are sure it needs to be reverted.
3143 3141 for table, (xlist, msg), dobackup in disptable:
3144 3142 if abs not in table:
3145 3143 continue
3146 3144 if xlist is not None:
3147 3145 xlist.append(abs)
3148 3146 if dobackup:
3149 3147 # If in interactive mode, don't automatically create
3150 3148 # .orig files (issue4793)
3151 3149 if dobackup == backupinteractive:
3152 3150 tobackup.add(abs)
3153 3151 elif (backup <= dobackup or wctx[abs].cmp(ctx[abs])):
3154 3152 bakname = scmutil.origpath(ui, repo, rel)
3155 3153 ui.note(_('saving current version of %s as %s\n') %
3156 3154 (rel, bakname))
3157 3155 if not opts.get('dry_run'):
3158 3156 if interactive:
3159 3157 util.copyfile(target, bakname)
3160 3158 else:
3161 3159 util.rename(target, bakname)
3162 3160 if ui.verbose or not exact:
3163 3161 if not isinstance(msg, basestring):
3164 3162 msg = msg(abs)
3165 3163 ui.status(msg % rel)
3166 3164 elif exact:
3167 3165 ui.warn(msg % rel)
3168 3166 break
3169 3167
3170 3168 if not opts.get('dry_run'):
3171 3169 needdata = ('revert', 'add', 'undelete')
3172 3170 _revertprefetch(repo, ctx, *[actions[name][0] for name in needdata])
3173 3171 _performrevert(repo, parents, ctx, actions, interactive, tobackup)
3174 3172
3175 3173 if targetsubs:
3176 3174 # Revert the subrepos on the revert list
3177 3175 for sub in targetsubs:
3178 3176 try:
3179 3177 wctx.sub(sub).revert(ctx.substate[sub], *pats, **opts)
3180 3178 except KeyError:
3181 3179 raise error.Abort("subrepository '%s' does not exist in %s!"
3182 3180 % (sub, short(ctx.node())))
3183 3181
3184 3182 def _revertprefetch(repo, ctx, *files):
3185 3183 """Let extension changing the storage layer prefetch content"""
3186 3184 pass
3187 3185
3188 3186 def _performrevert(repo, parents, ctx, actions, interactive=False,
3189 3187 tobackup=None):
3190 3188 """function that actually perform all the actions computed for revert
3191 3189
3192 3190 This is an independent function to let extension to plug in and react to
3193 3191 the imminent revert.
3194 3192
3195 3193 Make sure you have the working directory locked when calling this function.
3196 3194 """
3197 3195 parent, p2 = parents
3198 3196 node = ctx.node()
3199 3197 excluded_files = []
3200 3198 matcher_opts = {"exclude": excluded_files}
3201 3199
3202 3200 def checkout(f):
3203 3201 fc = ctx[f]
3204 3202 repo.wwrite(f, fc.data(), fc.flags())
3205 3203
3206 3204 def doremove(f):
3207 3205 try:
3208 3206 util.unlinkpath(repo.wjoin(f))
3209 3207 except OSError:
3210 3208 pass
3211 3209 repo.dirstate.remove(f)
3212 3210
3213 3211 audit_path = pathutil.pathauditor(repo.root)
3214 3212 for f in actions['forget'][0]:
3215 3213 if interactive:
3216 3214 choice = repo.ui.promptchoice(
3217 3215 _("forget added file %s (Yn)?$$ &Yes $$ &No") % f)
3218 3216 if choice == 0:
3219 3217 repo.dirstate.drop(f)
3220 3218 else:
3221 3219 excluded_files.append(repo.wjoin(f))
3222 3220 else:
3223 3221 repo.dirstate.drop(f)
3224 3222 for f in actions['remove'][0]:
3225 3223 audit_path(f)
3226 3224 if interactive:
3227 3225 choice = repo.ui.promptchoice(
3228 3226 _("remove added file %s (Yn)?$$ &Yes $$ &No") % f)
3229 3227 if choice == 0:
3230 3228 doremove(f)
3231 3229 else:
3232 3230 excluded_files.append(repo.wjoin(f))
3233 3231 else:
3234 3232 doremove(f)
3235 3233 for f in actions['drop'][0]:
3236 3234 audit_path(f)
3237 3235 repo.dirstate.remove(f)
3238 3236
3239 3237 normal = None
3240 3238 if node == parent:
3241 3239 # We're reverting to our parent. If possible, we'd like status
3242 3240 # to report the file as clean. We have to use normallookup for
3243 3241 # merges to avoid losing information about merged/dirty files.
3244 3242 if p2 != nullid:
3245 3243 normal = repo.dirstate.normallookup
3246 3244 else:
3247 3245 normal = repo.dirstate.normal
3248 3246
3249 3247 newlyaddedandmodifiedfiles = set()
3250 3248 if interactive:
3251 3249 # Prompt the user for changes to revert
3252 3250 torevert = [repo.wjoin(f) for f in actions['revert'][0]]
3253 3251 m = scmutil.match(ctx, torevert, matcher_opts)
3254 3252 diffopts = patch.difffeatureopts(repo.ui, whitespace=True)
3255 3253 diffopts.nodates = True
3256 3254 diffopts.git = True
3257 3255 reversehunks = repo.ui.configbool('experimental',
3258 3256 'revertalternateinteractivemode',
3259 3257 True)
3260 3258 if reversehunks:
3261 3259 diff = patch.diff(repo, ctx.node(), None, m, opts=diffopts)
3262 3260 else:
3263 3261 diff = patch.diff(repo, None, ctx.node(), m, opts=diffopts)
3264 3262 originalchunks = patch.parsepatch(diff)
3265 3263 operation = 'discard' if node == parent else 'revert'
3266 3264
3267 3265 try:
3268 3266
3269 3267 chunks, opts = recordfilter(repo.ui, originalchunks,
3270 3268 operation=operation)
3271 3269 if reversehunks:
3272 3270 chunks = patch.reversehunks(chunks)
3273 3271
3274 3272 except patch.PatchError as err:
3275 3273 raise error.Abort(_('error parsing patch: %s') % err)
3276 3274
3277 3275 newlyaddedandmodifiedfiles = newandmodified(chunks, originalchunks)
3278 3276 if tobackup is None:
3279 3277 tobackup = set()
3280 3278 # Apply changes
3281 3279 fp = stringio()
3282 3280 for c in chunks:
3283 3281 # Create a backup file only if this hunk should be backed up
3284 3282 if ishunk(c) and c.header.filename() in tobackup:
3285 3283 abs = c.header.filename()
3286 3284 target = repo.wjoin(abs)
3287 3285 bakname = scmutil.origpath(repo.ui, repo, m.rel(abs))
3288 3286 util.copyfile(target, bakname)
3289 3287 tobackup.remove(abs)
3290 3288 c.write(fp)
3291 3289 dopatch = fp.tell()
3292 3290 fp.seek(0)
3293 3291 if dopatch:
3294 3292 try:
3295 3293 patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None)
3296 3294 except patch.PatchError as err:
3297 3295 raise error.Abort(str(err))
3298 3296 del fp
3299 3297 else:
3300 3298 for f in actions['revert'][0]:
3301 3299 checkout(f)
3302 3300 if normal:
3303 3301 normal(f)
3304 3302
3305 3303 for f in actions['add'][0]:
3306 3304 # Don't checkout modified files, they are already created by the diff
3307 3305 if f not in newlyaddedandmodifiedfiles:
3308 3306 checkout(f)
3309 3307 repo.dirstate.add(f)
3310 3308
3311 3309 normal = repo.dirstate.normallookup
3312 3310 if node == parent and p2 == nullid:
3313 3311 normal = repo.dirstate.normal
3314 3312 for f in actions['undelete'][0]:
3315 3313 checkout(f)
3316 3314 normal(f)
3317 3315
3318 3316 copied = copies.pathcopies(repo[parent], ctx)
3319 3317
3320 3318 for f in actions['add'][0] + actions['undelete'][0] + actions['revert'][0]:
3321 3319 if f in copied:
3322 3320 repo.dirstate.copy(copied[f], f)
3323 3321
3324 3322 def command(table):
3325 3323 """Returns a function object to be used as a decorator for making commands.
3326 3324
3327 3325 This function receives a command table as its argument. The table should
3328 3326 be a dict.
3329 3327
3330 3328 The returned function can be used as a decorator for adding commands
3331 3329 to that command table. This function accepts multiple arguments to define
3332 3330 a command.
3333 3331
3334 3332 The first argument is the command name.
3335 3333
3336 3334 The options argument is an iterable of tuples defining command arguments.
3337 3335 See ``mercurial.fancyopts.fancyopts()`` for the format of each tuple.
3338 3336
3339 3337 The synopsis argument defines a short, one line summary of how to use the
3340 3338 command. This shows up in the help output.
3341 3339
3342 3340 The norepo argument defines whether the command does not require a
3343 3341 local repository. Most commands operate against a repository, thus the
3344 3342 default is False.
3345 3343
3346 3344 The optionalrepo argument defines whether the command optionally requires
3347 3345 a local repository.
3348 3346
3349 3347 The inferrepo argument defines whether to try to find a repository from the
3350 3348 command line arguments. If True, arguments will be examined for potential
3351 3349 repository locations. See ``findrepo()``. If a repository is found, it
3352 3350 will be used.
3353 3351 """
3354 3352 def cmd(name, options=(), synopsis=None, norepo=False, optionalrepo=False,
3355 3353 inferrepo=False):
3356 3354 def decorator(func):
3357 3355 func.norepo = norepo
3358 3356 func.optionalrepo = optionalrepo
3359 3357 func.inferrepo = inferrepo
3360 3358 if synopsis:
3361 3359 table[name] = func, list(options), synopsis
3362 3360 else:
3363 3361 table[name] = func, list(options)
3364 3362 return func
3365 3363 return decorator
3366 3364
3367 3365 return cmd
3368 3366
3369 def checkunresolved(ms):
3370 ms._repo.ui.deprecwarn('checkunresolved moved from cmdutil to mergeutil',
3371 '4.1')
3372 return mergeutil.checkunresolved(ms)
3373
3374 3367 # a list of (ui, repo, otherpeer, opts, missing) functions called by
3375 3368 # commands.outgoing. "missing" is "missing" of the result of
3376 3369 # "findcommonoutgoing()"
3377 3370 outgoinghooks = util.hooks()
3378 3371
3379 3372 # a list of (ui, repo) functions called by commands.summary
3380 3373 summaryhooks = util.hooks()
3381 3374
3382 3375 # a list of (ui, repo, opts, changes) functions called by commands.summary.
3383 3376 #
3384 3377 # functions should return tuple of booleans below, if 'changes' is None:
3385 3378 # (whether-incomings-are-needed, whether-outgoings-are-needed)
3386 3379 #
3387 3380 # otherwise, 'changes' is a tuple of tuples below:
3388 3381 # - (sourceurl, sourcebranch, sourcepeer, incoming)
3389 3382 # - (desturl, destbranch, destpeer, outgoing)
3390 3383 summaryremotehooks = util.hooks()
3391 3384
3392 3385 # A list of state files kept by multistep operations like graft.
3393 3386 # Since graft cannot be aborted, it is considered 'clearable' by update.
3394 3387 # note: bisect is intentionally excluded
3395 3388 # (state file, clearable, allowcommit, error, hint)
3396 3389 unfinishedstates = [
3397 3390 ('graftstate', True, False, _('graft in progress'),
3398 3391 _("use 'hg graft --continue' or 'hg update' to abort")),
3399 3392 ('updatestate', True, False, _('last update was interrupted'),
3400 3393 _("use 'hg update' to get a consistent checkout"))
3401 3394 ]
3402 3395
3403 3396 def checkunfinished(repo, commit=False):
3404 3397 '''Look for an unfinished multistep operation, like graft, and abort
3405 3398 if found. It's probably good to check this right before
3406 3399 bailifchanged().
3407 3400 '''
3408 3401 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3409 3402 if commit and allowcommit:
3410 3403 continue
3411 3404 if repo.vfs.exists(f):
3412 3405 raise error.Abort(msg, hint=hint)
3413 3406
3414 3407 def clearunfinished(repo):
3415 3408 '''Check for unfinished operations (as above), and clear the ones
3416 3409 that are clearable.
3417 3410 '''
3418 3411 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3419 3412 if not clearable and repo.vfs.exists(f):
3420 3413 raise error.Abort(msg, hint=hint)
3421 3414 for f, clearable, allowcommit, msg, hint in unfinishedstates:
3422 3415 if clearable and repo.vfs.exists(f):
3423 3416 util.unlink(repo.join(f))
3424 3417
3425 3418 afterresolvedstates = [
3426 3419 ('graftstate',
3427 3420 _('hg graft --continue')),
3428 3421 ]
3429 3422
3430 3423 def howtocontinue(repo):
3431 3424 '''Check for an unfinished operation and return the command to finish
3432 3425 it.
3433 3426
3434 3427 afterresolvedstates tuples define a .hg/{file} and the corresponding
3435 3428 command needed to finish it.
3436 3429
3437 3430 Returns a (msg, warning) tuple. 'msg' is a string and 'warning' is
3438 3431 a boolean.
3439 3432 '''
3440 3433 contmsg = _("continue: %s")
3441 3434 for f, msg in afterresolvedstates:
3442 3435 if repo.vfs.exists(f):
3443 3436 return contmsg % msg, True
3444 3437 workingctx = repo[None]
3445 3438 dirty = any(repo.status()) or any(workingctx.sub(s).dirty()
3446 3439 for s in workingctx.substate)
3447 3440 if dirty:
3448 3441 return contmsg % _("hg commit"), False
3449 3442 return None, None
3450 3443
3451 3444 def checkafterresolved(repo):
3452 3445 '''Inform the user about the next action after completing hg resolve
3453 3446
3454 3447 If there's a matching afterresolvedstates, howtocontinue will yield
3455 3448 repo.ui.warn as the reporter.
3456 3449
3457 3450 Otherwise, it will yield repo.ui.note.
3458 3451 '''
3459 3452 msg, warning = howtocontinue(repo)
3460 3453 if msg is not None:
3461 3454 if warning:
3462 3455 repo.ui.warn("%s\n" % msg)
3463 3456 else:
3464 3457 repo.ui.note("%s\n" % msg)
3465 3458
3466 3459 def wrongtooltocontinue(repo, task):
3467 3460 '''Raise an abort suggesting how to properly continue if there is an
3468 3461 active task.
3469 3462
3470 3463 Uses howtocontinue() to find the active task.
3471 3464
3472 3465 If there's no task (repo.ui.note for 'hg commit'), it does not offer
3473 3466 a hint.
3474 3467 '''
3475 3468 after = howtocontinue(repo)
3476 3469 hint = None
3477 3470 if after[1]:
3478 3471 hint = after[0]
3479 3472 raise error.Abort(_('no %s in progress') % task, hint=hint)
3480
3481 class dirstateguard(dirstateguardmod.dirstateguard):
3482 def __init__(self, repo, name):
3483 dirstateguardmod.dirstateguard.__init__(self, repo, name)
3484 repo.ui.deprecwarn(
3485 'dirstateguard has moved from cmdutil to dirstateguard',
3486 '4.1')
General Comments 0
You need to be logged in to leave comments. Login now