Mercurial

view mercurial/cmdutil.py @ 16778:2ac08d8b21aa

merge with stable
author Matt Mackall <mpm@selenic.com>
date Tue, 22 May 2012 14:37:20 -0500
parents 34c30506dd4e
children
line source
1 # cmdutil.py - help for command processing in mercurial
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
8 from node import hex, nullid, nullrev, short
9 from i18n import _
10 import os, sys, errno, re, tempfile
11 import util, scmutil, templater, patch, error, templatekw, revlog, copies
12 import match as matchmod
13 import subrepo, context, repair, bookmarks
15 def parsealiases(cmd):
16 return cmd.lstrip("^").split("|")
18 def findpossible(cmd, table, strict=False):
19 """
20 Return cmd -> (aliases, command table entry)
21 for each matching command.
22 Return debug commands (or their aliases) only if no normal command matches.
23 """
24 choice = {}
25 debugchoice = {}
27 if cmd in table:
28 # short-circuit exact matches, "log" alias beats "^log|history"
29 keys = [cmd]
30 else:
31 keys = table.keys()
33 for e in keys:
34 aliases = parsealiases(e)
35 found = None
36 if cmd in aliases:
37 found = cmd
38 elif not strict:
39 for a in aliases:
40 if a.startswith(cmd):
41 found = a
42 break
43 if found is not None:
44 if aliases[0].startswith("debug") or found.startswith("debug"):
45 debugchoice[found] = (aliases, table[e])
46 else:
47 choice[found] = (aliases, table[e])
49 if not choice and debugchoice:
50 choice = debugchoice
52 return choice
54 def findcmd(cmd, table, strict=True):
55 """Return (aliases, command table entry) for command string."""
56 choice = findpossible(cmd, table, strict)
58 if cmd in choice:
59 return choice[cmd]
61 if len(choice) > 1:
62 clist = choice.keys()
63 clist.sort()
64 raise error.AmbiguousCommand(cmd, clist)
66 if choice:
67 return choice.values()[0]
69 raise error.UnknownCommand(cmd)
71 def findrepo(p):
72 while not os.path.isdir(os.path.join(p, ".hg")):
73 oldp, p = p, os.path.dirname(p)
74 if p == oldp:
75 return None
77 return p
79 def bailifchanged(repo):
80 if repo.dirstate.p2() != nullid:
81 raise util.Abort(_('outstanding uncommitted merge'))
82 modified, added, removed, deleted = repo.status()[:4]
83 if modified or added or removed or deleted:
84 raise util.Abort(_("outstanding uncommitted changes"))
85 ctx = repo[None]
86 for s in ctx.substate:
87 if ctx.sub(s).dirty():
88 raise util.Abort(_("uncommitted changes in subrepo %s") % s)
90 def logmessage(ui, opts):
91 """ get the log message according to -m and -l option """
92 message = opts.get('message')
93 logfile = opts.get('logfile')
95 if message and logfile:
96 raise util.Abort(_('options --message and --logfile are mutually '
97 'exclusive'))
98 if not message and logfile:
99 try:
100 if logfile == '-':
101 message = ui.fin.read()
102 else:
103 message = '\n'.join(util.readfile(logfile).splitlines())
104 except IOError, inst:
105 raise util.Abort(_("can't read commit message '%s': %s") %
106 (logfile, inst.strerror))
107 return message
109 def loglimit(opts):
110 """get the log limit according to option -l/--limit"""
111 limit = opts.get('limit')
112 if limit:
113 try:
114 limit = int(limit)
115 except ValueError:
116 raise util.Abort(_('limit must be a positive integer'))
117 if limit <= 0:
118 raise util.Abort(_('limit must be positive'))
119 else:
120 limit = None
121 return limit
123 def makefilename(repo, pat, node, desc=None,
124 total=None, seqno=None, revwidth=None, pathname=None):
125 node_expander = {
126 'H': lambda: hex(node),
127 'R': lambda: str(repo.changelog.rev(node)),
128 'h': lambda: short(node),
129 'm': lambda: re.sub('[^\w]', '_', str(desc))
130 }
131 expander = {
132 '%': lambda: '%',
133 'b': lambda: os.path.basename(repo.root),
134 }
136 try:
137 if node:
138 expander.update(node_expander)
139 if node:
140 expander['r'] = (lambda:
141 str(repo.changelog.rev(node)).zfill(revwidth or 0))
142 if total is not None:
143 expander['N'] = lambda: str(total)
144 if seqno is not None:
145 expander['n'] = lambda: str(seqno)
146 if total is not None and seqno is not None:
147 expander['n'] = lambda: str(seqno).zfill(len(str(total)))
148 if pathname is not None:
149 expander['s'] = lambda: os.path.basename(pathname)
150 expander['d'] = lambda: os.path.dirname(pathname) or '.'
151 expander['p'] = lambda: pathname
153 newname = []
154 patlen = len(pat)
155 i = 0
156 while i < patlen:
157 c = pat[i]
158 if c == '%':
159 i += 1
160 c = pat[i]
161 c = expander[c]()
162 newname.append(c)
163 i += 1
164 return ''.join(newname)
165 except KeyError, inst:
166 raise util.Abort(_("invalid format spec '%%%s' in output filename") %
167 inst.args[0])
169 def makefileobj(repo, pat, node=None, desc=None, total=None,
170 seqno=None, revwidth=None, mode='wb', pathname=None):
172 writable = mode not in ('r', 'rb')
174 if not pat or pat == '-':
175 fp = writable and repo.ui.fout or repo.ui.fin
176 if util.safehasattr(fp, 'fileno'):
177 return os.fdopen(os.dup(fp.fileno()), mode)
178 else:
179 # if this fp can't be duped properly, return
180 # a dummy object that can be closed
181 class wrappedfileobj(object):
182 noop = lambda x: None
183 def __init__(self, f):
184 self.f = f
185 def __getattr__(self, attr):
186 if attr == 'close':
187 return self.noop
188 else:
189 return getattr(self.f, attr)
191 return wrappedfileobj(fp)
192 if util.safehasattr(pat, 'write') and writable:
193 return pat
194 if util.safehasattr(pat, 'read') and 'r' in mode:
195 return pat
196 return open(makefilename(repo, pat, node, desc, total, seqno, revwidth,
197 pathname),
198 mode)
200 def openrevlog(repo, cmd, file_, opts):
201 """opens the changelog, manifest, a filelog or a given revlog"""
202 cl = opts['changelog']
203 mf = opts['manifest']
204 msg = None
205 if cl and mf:
206 msg = _('cannot specify --changelog and --manifest at the same time')
207 elif cl or mf:
208 if file_:
209 msg = _('cannot specify filename with --changelog or --manifest')
210 elif not repo:
211 msg = _('cannot specify --changelog or --manifest '
212 'without a repository')
213 if msg:
214 raise util.Abort(msg)
216 r = None
217 if repo:
218 if cl:
219 r = repo.changelog
220 elif mf:
221 r = repo.manifest
222 elif file_:
223 filelog = repo.file(file_)
224 if len(filelog):
225 r = filelog
226 if not r:
227 if not file_:
228 raise error.CommandError(cmd, _('invalid arguments'))
229 if not os.path.isfile(file_):
230 raise util.Abort(_("revlog '%s' not found") % file_)
231 r = revlog.revlog(scmutil.opener(os.getcwd(), audit=False),
232 file_[:-2] + ".i")
233 return r
235 def copy(ui, repo, pats, opts, rename=False):
236 # called with the repo lock held
237 #
238 # hgsep => pathname that uses "/" to separate directories
239 # ossep => pathname that uses os.sep to separate directories
240 cwd = repo.getcwd()
241 targets = {}
242 after = opts.get("after")
243 dryrun = opts.get("dry_run")
244 wctx = repo[None]
246 def walkpat(pat):
247 srcs = []
248 badstates = after and '?' or '?r'
249 m = scmutil.match(repo[None], [pat], opts, globbed=True)
250 for abs in repo.walk(m):
251 state = repo.dirstate[abs]
252 rel = m.rel(abs)
253 exact = m.exact(abs)
254 if state in badstates:
255 if exact and state == '?':
256 ui.warn(_('%s: not copying - file is not managed\n') % rel)
257 if exact and state == 'r':
258 ui.warn(_('%s: not copying - file has been marked for'
259 ' remove\n') % rel)
260 continue
261 # abs: hgsep
262 # rel: ossep
263 srcs.append((abs, rel, exact))
264 return srcs
266 # abssrc: hgsep
267 # relsrc: ossep
268 # otarget: ossep
269 def copyfile(abssrc, relsrc, otarget, exact):
270 abstarget = scmutil.canonpath(repo.root, cwd, otarget)
271 if '/' in abstarget:
272 # We cannot normalize abstarget itself, this would prevent
273 # case only renames, like a => A.
274 abspath, absname = abstarget.rsplit('/', 1)
275 abstarget = repo.dirstate.normalize(abspath) + '/' + absname
276 reltarget = repo.pathto(abstarget, cwd)
277 target = repo.wjoin(abstarget)
278 src = repo.wjoin(abssrc)
279 state = repo.dirstate[abstarget]
281 scmutil.checkportable(ui, abstarget)
283 # check for collisions
284 prevsrc = targets.get(abstarget)
285 if prevsrc is not None:
286 ui.warn(_('%s: not overwriting - %s collides with %s\n') %
287 (reltarget, repo.pathto(abssrc, cwd),
288 repo.pathto(prevsrc, cwd)))
289 return
291 # check for overwrites
292 exists = os.path.lexists(target)
293 samefile = False
294 if exists and abssrc != abstarget:
295 if (repo.dirstate.normalize(abssrc) ==
296 repo.dirstate.normalize(abstarget)):
297 if not rename:
298 ui.warn(_("%s: can't copy - same file\n") % reltarget)
299 return
300 exists = False
301 samefile = True
303 if not after and exists or after and state in 'mn':
304 if not opts['force']:
305 ui.warn(_('%s: not overwriting - file exists\n') %
306 reltarget)
307 return
309 if after:
310 if not exists:
311 if rename:
312 ui.warn(_('%s: not recording move - %s does not exist\n') %
313 (relsrc, reltarget))
314 else:
315 ui.warn(_('%s: not recording copy - %s does not exist\n') %
316 (relsrc, reltarget))
317 return
318 elif not dryrun:
319 try:
320 if exists:
321 os.unlink(target)
322 targetdir = os.path.dirname(target) or '.'
323 if not os.path.isdir(targetdir):
324 os.makedirs(targetdir)
325 if samefile:
326 tmp = target + "~hgrename"
327 os.rename(src, tmp)
328 os.rename(tmp, target)
329 else:
330 util.copyfile(src, target)
331 srcexists = True
332 except IOError, inst:
333 if inst.errno == errno.ENOENT:
334 ui.warn(_('%s: deleted in working copy\n') % relsrc)
335 srcexists = False
336 else:
337 ui.warn(_('%s: cannot copy - %s\n') %
338 (relsrc, inst.strerror))
339 return True # report a failure
341 if ui.verbose or not exact:
342 if rename:
343 ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
344 else:
345 ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
347 targets[abstarget] = abssrc
349 # fix up dirstate
350 scmutil.dirstatecopy(ui, repo, wctx, abssrc, abstarget,
351 dryrun=dryrun, cwd=cwd)
352 if rename and not dryrun:
353 if not after and srcexists and not samefile:
354 util.unlinkpath(repo.wjoin(abssrc))
355 wctx.forget([abssrc])
357 # pat: ossep
358 # dest ossep
359 # srcs: list of (hgsep, hgsep, ossep, bool)
360 # return: function that takes hgsep and returns ossep
361 def targetpathfn(pat, dest, srcs):
362 if os.path.isdir(pat):
363 abspfx = scmutil.canonpath(repo.root, cwd, pat)
364 abspfx = util.localpath(abspfx)
365 if destdirexists:
366 striplen = len(os.path.split(abspfx)[0])
367 else:
368 striplen = len(abspfx)
369 if striplen:
370 striplen += len(os.sep)
371 res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
372 elif destdirexists:
373 res = lambda p: os.path.join(dest,
374 os.path.basename(util.localpath(p)))
375 else:
376 res = lambda p: dest
377 return res
379 # pat: ossep
380 # dest ossep
381 # srcs: list of (hgsep, hgsep, ossep, bool)
382 # return: function that takes hgsep and returns ossep
383 def targetpathafterfn(pat, dest, srcs):
384 if matchmod.patkind(pat):
385 # a mercurial pattern
386 res = lambda p: os.path.join(dest,
387 os.path.basename(util.localpath(p)))
388 else:
389 abspfx = scmutil.canonpath(repo.root, cwd, pat)
390 if len(abspfx) < len(srcs[0][0]):
391 # A directory. Either the target path contains the last
392 # component of the source path or it does not.
393 def evalpath(striplen):
394 score = 0
395 for s in srcs:
396 t = os.path.join(dest, util.localpath(s[0])[striplen:])
397 if os.path.lexists(t):
398 score += 1
399 return score
401 abspfx = util.localpath(abspfx)
402 striplen = len(abspfx)
403 if striplen:
404 striplen += len(os.sep)
405 if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
406 score = evalpath(striplen)
407 striplen1 = len(os.path.split(abspfx)[0])
408 if striplen1:
409 striplen1 += len(os.sep)
410 if evalpath(striplen1) > score:
411 striplen = striplen1
412 res = lambda p: os.path.join(dest,
413 util.localpath(p)[striplen:])
414 else:
415 # a file
416 if destdirexists:
417 res = lambda p: os.path.join(dest,
418 os.path.basename(util.localpath(p)))
419 else:
420 res = lambda p: dest
421 return res
424 pats = scmutil.expandpats(pats)
425 if not pats:
426 raise util.Abort(_('no source or destination specified'))
427 if len(pats) == 1:
428 raise util.Abort(_('no destination specified'))
429 dest = pats.pop()
430 destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
431 if not destdirexists:
432 if len(pats) > 1 or matchmod.patkind(pats[0]):
433 raise util.Abort(_('with multiple sources, destination must be an '
434 'existing directory'))
435 if util.endswithsep(dest):
436 raise util.Abort(_('destination %s is not a directory') % dest)
438 tfn = targetpathfn
439 if after:
440 tfn = targetpathafterfn
441 copylist = []
442 for pat in pats:
443 srcs = walkpat(pat)
444 if not srcs:
445 continue
446 copylist.append((tfn(pat, dest, srcs), srcs))
447 if not copylist:
448 raise util.Abort(_('no files to copy'))
450 errors = 0
451 for targetpath, srcs in copylist:
452 for abssrc, relsrc, exact in srcs:
453 if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
454 errors += 1
456 if errors:
457 ui.warn(_('(consider using --after)\n'))
459 return errors != 0
461 def service(opts, parentfn=None, initfn=None, runfn=None, logfile=None,
462 runargs=None, appendpid=False):
463 '''Run a command as a service.'''
465 if opts['daemon'] and not opts['daemon_pipefds']:
466 # Signal child process startup with file removal
467 lockfd, lockpath = tempfile.mkstemp(prefix='hg-service-')
468 os.close(lockfd)
469 try:
470 if not runargs:
471 runargs = util.hgcmd() + sys.argv[1:]
472 runargs.append('--daemon-pipefds=%s' % lockpath)
473 # Don't pass --cwd to the child process, because we've already
474 # changed directory.
475 for i in xrange(1, len(runargs)):
476 if runargs[i].startswith('--cwd='):
477 del runargs[i]
478 break
479 elif runargs[i].startswith('--cwd'):
480 del runargs[i:i + 2]
481 break
482 def condfn():
483 return not os.path.exists(lockpath)
484 pid = util.rundetached(runargs, condfn)
485 if pid < 0:
486 raise util.Abort(_('child process failed to start'))
487 finally:
488 try:
489 os.unlink(lockpath)
490 except OSError, e:
491 if e.errno != errno.ENOENT:
492 raise
493 if parentfn:
494 return parentfn(pid)
495 else:
496 return
498 if initfn:
499 initfn()
501 if opts['pid_file']:
502 mode = appendpid and 'a' or 'w'
503 fp = open(opts['pid_file'], mode)
504 fp.write(str(os.getpid()) + '\n')
505 fp.close()
507 if opts['daemon_pipefds']:
508 lockpath = opts['daemon_pipefds']
509 try:
510 os.setsid()
511 except AttributeError:
512 pass
513 os.unlink(lockpath)
514 util.hidewindow()
515 sys.stdout.flush()
516 sys.stderr.flush()
518 nullfd = os.open(util.nulldev, os.O_RDWR)
519 logfilefd = nullfd
520 if logfile:
521 logfilefd = os.open(logfile, os.O_RDWR | os.O_CREAT | os.O_APPEND)
522 os.dup2(nullfd, 0)
523 os.dup2(logfilefd, 1)
524 os.dup2(logfilefd, 2)
525 if nullfd not in (0, 1, 2):
526 os.close(nullfd)
527 if logfile and logfilefd not in (0, 1, 2):
528 os.close(logfilefd)
530 if runfn:
531 return runfn()
533 def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
534 opts=None):
535 '''export changesets as hg patches.'''
537 total = len(revs)
538 revwidth = max([len(str(rev)) for rev in revs])
540 def single(rev, seqno, fp):
541 ctx = repo[rev]
542 node = ctx.node()
543 parents = [p.node() for p in ctx.parents() if p]
544 branch = ctx.branch()
545 if switch_parent:
546 parents.reverse()
547 prev = (parents and parents[0]) or nullid
549 shouldclose = False
550 if not fp:
551 desc_lines = ctx.description().rstrip().split('\n')
552 desc = desc_lines[0] #Commit always has a first line.
553 fp = makefileobj(repo, template, node, desc=desc, total=total,
554 seqno=seqno, revwidth=revwidth, mode='ab')
555 if fp != template:
556 shouldclose = True
557 if fp != sys.stdout and util.safehasattr(fp, 'name'):
558 repo.ui.note("%s\n" % fp.name)
560 fp.write("# HG changeset patch\n")
561 fp.write("# User %s\n" % ctx.user())
562 fp.write("# Date %d %d\n" % ctx.date())
563 if branch and branch != 'default':
564 fp.write("# Branch %s\n" % branch)
565 fp.write("# Node ID %s\n" % hex(node))
566 fp.write("# Parent %s\n" % hex(prev))
567 if len(parents) > 1:
568 fp.write("# Parent %s\n" % hex(parents[1]))
569 fp.write(ctx.description().rstrip())
570 fp.write("\n\n")
572 for chunk in patch.diff(repo, prev, node, opts=opts):
573 fp.write(chunk)
575 if shouldclose:
576 fp.close()
578 for seqno, rev in enumerate(revs):
579 single(rev, seqno + 1, fp)
581 def diffordiffstat(ui, repo, diffopts, node1, node2, match,
582 changes=None, stat=False, fp=None, prefix='',
583 listsubrepos=False):
584 '''show diff or diffstat.'''
585 if fp is None:
586 write = ui.write
587 else:
588 def write(s, **kw):
589 fp.write(s)
591 if stat:
592 diffopts = diffopts.copy(context=0)
593 width = 80
594 if not ui.plain():
595 width = ui.termwidth()
596 chunks = patch.diff(repo, node1, node2, match, changes, diffopts,
597 prefix=prefix)
598 for chunk, label in patch.diffstatui(util.iterlines(chunks),
599 width=width,
600 git=diffopts.git):
601 write(chunk, label=label)
602 else:
603 for chunk, label in patch.diffui(repo, node1, node2, match,
604 changes, diffopts, prefix=prefix):
605 write(chunk, label=label)
607 if listsubrepos:
608 ctx1 = repo[node1]
609 ctx2 = repo[node2]
610 for subpath, sub in subrepo.itersubrepos(ctx1, ctx2):
611 tempnode2 = node2
612 try:
613 if node2 is not None:
614 tempnode2 = ctx2.substate[subpath][1]
615 except KeyError:
616 # A subrepo that existed in node1 was deleted between node1 and
617 # node2 (inclusive). Thus, ctx2's substate won't contain that
618 # subpath. The best we can do is to ignore it.
619 tempnode2 = None
620 submatch = matchmod.narrowmatcher(subpath, match)
621 sub.diff(diffopts, tempnode2, submatch, changes=changes,
622 stat=stat, fp=fp, prefix=prefix)
624 class changeset_printer(object):
625 '''show changeset information when templating not requested.'''
627 def __init__(self, ui, repo, patch, diffopts, buffered):
628 self.ui = ui
629 self.repo = repo
630 self.buffered = buffered
631 self.patch = patch
632 self.diffopts = diffopts
633 self.header = {}
634 self.hunk = {}
635 self.lastheader = None
636 self.footer = None
638 def flush(self, rev):
639 if rev in self.header:
640 h = self.header[rev]
641 if h != self.lastheader:
642 self.lastheader = h
643 self.ui.write(h)
644 del self.header[rev]
645 if rev in self.hunk:
646 self.ui.write(self.hunk[rev])
647 del self.hunk[rev]
648 return 1
649 return 0
651 def close(self):
652 if self.footer:
653 self.ui.write(self.footer)
655 def show(self, ctx, copies=None, matchfn=None, **props):
656 if self.buffered:
657 self.ui.pushbuffer()
658 self._show(ctx, copies, matchfn, props)
659 self.hunk[ctx.rev()] = self.ui.popbuffer(labeled=True)
660 else:
661 self._show(ctx, copies, matchfn, props)
663 def _show(self, ctx, copies, matchfn, props):
664 '''show a single changeset or file revision'''
665 changenode = ctx.node()
666 rev = ctx.rev()
668 if self.ui.quiet:
669 self.ui.write("%d:%s\n" % (rev, short(changenode)),
670 label='log.node')
671 return
673 log = self.repo.changelog
674 date = util.datestr(ctx.date())
676 hexfunc = self.ui.debugflag and hex or short
678 parents = [(p, hexfunc(log.node(p)))
679 for p in self._meaningful_parentrevs(log, rev)]
681 self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode)),
682 label='log.changeset')
684 branch = ctx.branch()
685 # don't show the default branch name
686 if branch != 'default':
687 self.ui.write(_("branch: %s\n") % branch,
688 label='log.branch')
689 for bookmark in self.repo.nodebookmarks(changenode):
690 self.ui.write(_("bookmark: %s\n") % bookmark,
691 label='log.bookmark')
692 for tag in self.repo.nodetags(changenode):
693 self.ui.write(_("tag: %s\n") % tag,
694 label='log.tag')
695 if self.ui.debugflag and ctx.phase():
696 self.ui.write(_("phase: %s\n") % _(ctx.phasestr()),
697 label='log.phase')
698 for parent in parents:
699 self.ui.write(_("parent: %d:%s\n") % parent,
700 label='log.parent')
702 if self.ui.debugflag:
703 mnode = ctx.manifestnode()
704 self.ui.write(_("manifest: %d:%s\n") %
705 (self.repo.manifest.rev(mnode), hex(mnode)),
706 label='ui.debug log.manifest')
707 self.ui.write(_("user: %s\n") % ctx.user(),
708 label='log.user')
709 self.ui.write(_("date: %s\n") % date,
710 label='log.date')
712 if self.ui.debugflag:
713 files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
714 for key, value in zip([_("files:"), _("files+:"), _("files-:")],
715 files):
716 if value:
717 self.ui.write("%-12s %s\n" % (key, " ".join(value)),
718 label='ui.debug log.files')
719 elif ctx.files() and self.ui.verbose:
720 self.ui.write(_("files: %s\n") % " ".join(ctx.files()),
721 label='ui.note log.files')
722 if copies and self.ui.verbose:
723 copies = ['%s (%s)' % c for c in copies]
724 self.ui.write(_("copies: %s\n") % ' '.join(copies),
725 label='ui.note log.copies')
727 extra = ctx.extra()
728 if extra and self.ui.debugflag:
729 for key, value in sorted(extra.items()):
730 self.ui.write(_("extra: %s=%s\n")
731 % (key, value.encode('string_escape')),
732 label='ui.debug log.extra')
734 description = ctx.description().strip()
735 if description:
736 if self.ui.verbose:
737 self.ui.write(_("description:\n"),
738 label='ui.note log.description')
739 self.ui.write(description,
740 label='ui.note log.description')
741 self.ui.write("\n\n")
742 else:
743 self.ui.write(_("summary: %s\n") %
744 description.splitlines()[0],
745 label='log.summary')
746 self.ui.write("\n")
748 self.showpatch(changenode, matchfn)
750 def showpatch(self, node, matchfn):
751 if not matchfn:
752 matchfn = self.patch
753 if matchfn:
754 stat = self.diffopts.get('stat')
755 diff = self.diffopts.get('patch')
756 diffopts = patch.diffopts(self.ui, self.diffopts)
757 prev = self.repo.changelog.parents(node)[0]
758 if stat:
759 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
760 match=matchfn, stat=True)
761 if diff:
762 if stat:
763 self.ui.write("\n")
764 diffordiffstat(self.ui, self.repo, diffopts, prev, node,
765 match=matchfn, stat=False)
766 self.ui.write("\n")
768 def _meaningful_parentrevs(self, log, rev):
769 """Return list of meaningful (or all if debug) parentrevs for rev.
771 For merges (two non-nullrev revisions) both parents are meaningful.
772 Otherwise the first parent revision is considered meaningful if it
773 is not the preceding revision.
774 """
775 parents = log.parentrevs(rev)
776 if not self.ui.debugflag and parents[1] == nullrev:
777 if parents[0] >= rev - 1:
778 parents = []
779 else:
780 parents = [parents[0]]
781 return parents
784 class changeset_templater(changeset_printer):
785 '''format changeset information.'''
787 def __init__(self, ui, repo, patch, diffopts, mapfile, buffered):
788 changeset_printer.__init__(self, ui, repo, patch, diffopts, buffered)
789 formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
790 defaulttempl = {
791 'parent': '{rev}:{node|formatnode} ',
792 'manifest': '{rev}:{node|formatnode}',
793 'file_copy': '{name} ({source})',
794 'extra': '{key}={value|stringescape}'
795 }
796 # filecopy is preserved for compatibility reasons
797 defaulttempl['filecopy'] = defaulttempl['file_copy']
798 self.t = templater.templater(mapfile, {'formatnode': formatnode},
799 cache=defaulttempl)
800 self.cache = {}
802 def use_template(self, t):
803 '''set template string to use'''
804 self.t.cache['changeset'] = t
806 def _meaningful_parentrevs(self, ctx):
807 """Return list of meaningful (or all if debug) parentrevs for rev.
808 """
809 parents = ctx.parents()
810 if len(parents) > 1:
811 return parents
812 if self.ui.debugflag:
813 return [parents[0], self.repo['null']]
814 if parents[0].rev() >= ctx.rev() - 1:
815 return []
816 return parents
818 def _show(self, ctx, copies, matchfn, props):
819 '''show a single changeset or file revision'''
821 showlist = templatekw.showlist
823 # showparents() behaviour depends on ui trace level which
824 # causes unexpected behaviours at templating level and makes
825 # it harder to extract it in a standalone function. Its
826 # behaviour cannot be changed so leave it here for now.
827 def showparents(**args):
828 ctx = args['ctx']
829 parents = [[('rev', p.rev()), ('node', p.hex())]
830 for p in self._meaningful_parentrevs(ctx)]
831 return showlist('parent', parents, **args)
833 props = props.copy()
834 props.update(templatekw.keywords)
835 props['parents'] = showparents
836 props['templ'] = self.t
837 props['ctx'] = ctx
838 props['repo'] = self.repo
839 props['revcache'] = {'copies': copies}
840 props['cache'] = self.cache
842 # find correct templates for current mode
844 tmplmodes = [
845 (True, None),
846 (self.ui.verbose, 'verbose'),
847 (self.ui.quiet, 'quiet'),
848 (self.ui.debugflag, 'debug'),
849 ]
851 types = {'header': '', 'footer':'', 'changeset': 'changeset'}
852 for mode, postfix in tmplmodes:
853 for type in types:
854 cur = postfix and ('%s_%s' % (type, postfix)) or type
855 if mode and cur in self.t:
856 types[type] = cur
858 try:
860 # write header
861 if types['header']:
862 h = templater.stringify(self.t(types['header'], **props))
863 if self.buffered:
864 self.header[ctx.rev()] = h
865 else:
866 if self.lastheader != h:
867 self.lastheader = h
868 self.ui.write(h)
870 # write changeset metadata, then patch if requested
871 key = types['changeset']
872 self.ui.write(templater.stringify(self.t(key, **props)))
873 self.showpatch(ctx.node(), matchfn)
875 if types['footer']:
876 if not self.footer:
877 self.footer = templater.stringify(self.t(types['footer'],
878 **props))
880 except KeyError, inst:
881 msg = _("%s: no key named '%s'")
882 raise util.Abort(msg % (self.t.mapfile, inst.args[0]))
883 except SyntaxError, inst:
884 raise util.Abort('%s: %s' % (self.t.mapfile, inst.args[0]))
886 def show_changeset(ui, repo, opts, buffered=False):
887 """show one changeset using template or regular display.
889 Display format will be the first non-empty hit of:
890 1. option 'template'
891 2. option 'style'
892 3. [ui] setting 'logtemplate'
893 4. [ui] setting 'style'
894 If all of these values are either the unset or the empty string,
895 regular display via changeset_printer() is done.
896 """
897 # options
898 patch = False
899 if opts.get('patch') or opts.get('stat'):
900 patch = scmutil.matchall(repo)
902 tmpl = opts.get('template')
903 style = None
904 if tmpl:
905 tmpl = templater.parsestring(tmpl, quoted=False)
906 else:
907 style = opts.get('style')
909 # ui settings
910 if not (tmpl or style):
911 tmpl = ui.config('ui', 'logtemplate')
912 if tmpl:
913 try:
914 tmpl = templater.parsestring(tmpl)
915 except SyntaxError:
916 tmpl = templater.parsestring(tmpl, quoted=False)
917 else:
918 style = util.expandpath(ui.config('ui', 'style', ''))
920 if not (tmpl or style):
921 return changeset_printer(ui, repo, patch, opts, buffered)
923 mapfile = None
924 if style and not tmpl:
925 mapfile = style
926 if not os.path.split(mapfile)[0]:
927 mapname = (templater.templatepath('map-cmdline.' + mapfile)
928 or templater.templatepath(mapfile))
929 if mapname:
930 mapfile = mapname
932 try:
933 t = changeset_templater(ui, repo, patch, opts, mapfile, buffered)
934 except SyntaxError, inst:
935 raise util.Abort(inst.args[0])
936 if tmpl:
937 t.use_template(tmpl)
938 return t
940 def finddate(ui, repo, date):
941 """Find the tipmost changeset that matches the given date spec"""
943 df = util.matchdate(date)
944 m = scmutil.matchall(repo)
945 results = {}
947 def prep(ctx, fns):
948 d = ctx.date()
949 if df(d[0]):
950 results[ctx.rev()] = d
952 for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
953 rev = ctx.rev()
954 if rev in results:
955 ui.status(_("Found revision %s from %s\n") %
956 (rev, util.datestr(results[rev])))
957 return str(rev)
959 raise util.Abort(_("revision matching date not found"))
961 def increasingwindows(start, end, windowsize=8, sizelimit=512):
962 if start < end:
963 while start < end:
964 yield start, min(windowsize, end - start)
965 start += windowsize
966 if windowsize < sizelimit:
967 windowsize *= 2
968 else:
969 while start > end:
970 yield start, min(windowsize, start - end - 1)
971 start -= windowsize
972 if windowsize < sizelimit:
973 windowsize *= 2
975 def walkchangerevs(repo, match, opts, prepare):
976 '''Iterate over files and the revs in which they changed.
978 Callers most commonly need to iterate backwards over the history
979 in which they are interested. Doing so has awful (quadratic-looking)
980 performance, so we use iterators in a "windowed" way.
982 We walk a window of revisions in the desired order. Within the
983 window, we first walk forwards to gather data, then in the desired
984 order (usually backwards) to display it.
986 This function returns an iterator yielding contexts. Before
987 yielding each context, the iterator will first call the prepare
988 function on each context in the window in forward order.'''
990 follow = opts.get('follow') or opts.get('follow_first')
992 if not len(repo):
993 return []
995 if follow:
996 defrange = '%s:0' % repo['.'].rev()
997 else:
998 defrange = '-1:0'
999 revs = scmutil.revrange(repo, opts['rev'] or [defrange])
1000 if not revs:
1001 return []
1002 wanted = set()
1003 slowpath = match.anypats() or (match.files() and opts.get('removed'))
1004 fncache = {}
1005 change = repo.changectx
1007 # First step is to fill wanted, the set of revisions that we want to yield.
1008 # When it does not induce extra cost, we also fill fncache for revisions in
1009 # wanted: a cache of filenames that were changed (ctx.files()) and that
1010 # match the file filtering conditions.
1012 if not slowpath and not match.files():
1013 # No files, no patterns. Display all revs.
1014 wanted = set(revs)
1015 copies = []
1017 if not slowpath and match.files():
1018 # We only have to read through the filelog to find wanted revisions
1020 minrev, maxrev = min(revs), max(revs)
1021 def filerevgen(filelog, last):
1022 """
1023 Only files, no patterns. Check the history of each file.
1025 Examines filelog entries within minrev, maxrev linkrev range
1026 Returns an iterator yielding (linkrev, parentlinkrevs, copied)
1027 tuples in backwards order
1028 """
1029 cl_count = len(repo)
1030 revs = []
1031 for j in xrange(0, last + 1):
1032 linkrev = filelog.linkrev(j)
1033 if linkrev < minrev:
1034 continue
1035 # only yield rev for which we have the changelog, it can
1036 # happen while doing "hg log" during a pull or commit
1037 if linkrev >= cl_count:
1038 break
1040 parentlinkrevs = []
1041 for p in filelog.parentrevs(j):
1042 if p != nullrev:
1043 parentlinkrevs.append(filelog.linkrev(p))
1044 n = filelog.node(j)
1045 revs.append((linkrev, parentlinkrevs,
1046 follow and filelog.renamed(n)))
1048 return reversed(revs)
1049 def iterfiles():
1050 pctx = repo['.']
1051 for filename in match.files():
1052 if follow:
1053 if filename not in pctx:
1054 raise util.Abort(_('cannot follow file not in parent '
1055 'revision: "%s"') % filename)
1056 yield filename, pctx[filename].filenode()
1057 else:
1058 yield filename, None
1059 for filename_node in copies:
1060 yield filename_node
1061 for file_, node in iterfiles():
1062 filelog = repo.file(file_)
1063 if not len(filelog):
1064 if node is None:
1065 # A zero count may be a directory or deleted file, so
1066 # try to find matching entries on the slow path.
1067 if follow:
1068 raise util.Abort(
1069 _('cannot follow nonexistent file: "%s"') % file_)
1070 slowpath = True
1071 break
1072 else:
1073 continue
1075 if node is None:
1076 last = len(filelog) - 1
1077 else:
1078 last = filelog.rev(node)
1081 # keep track of all ancestors of the file
1082 ancestors = set([filelog.linkrev(last)])
1084 # iterate from latest to oldest revision
1085 for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
1086 if not follow:
1087 if rev > maxrev:
1088 continue
1089 else:
1090 # Note that last might not be the first interesting
1091 # rev to us:
1092 # if the file has been changed after maxrev, we'll
1093 # have linkrev(last) > maxrev, and we still need
1094 # to explore the file graph
1095 if rev not in ancestors:
1096 continue
1097 # XXX insert 1327 fix here
1098 if flparentlinkrevs:
1099 ancestors.update(flparentlinkrevs)
1101 fncache.setdefault(rev, []).append(file_)
1102 wanted.add(rev)
1103 if copied:
1104 copies.append(copied)
1105 if slowpath:
1106 # We have to read the changelog to match filenames against
1107 # changed files
1109 if follow:
1110 raise util.Abort(_('can only follow copies/renames for explicit '
1111 'filenames'))
1113 # The slow path checks files modified in every changeset.
1114 for i in sorted(revs):
1115 ctx = change(i)
1116 matches = filter(match, ctx.files())
1117 if matches:
1118 fncache[i] = matches
1119 wanted.add(i)
1121 class followfilter(object):
1122 def __init__(self, onlyfirst=False):
1123 self.startrev = nullrev
1124 self.roots = set()
1125 self.onlyfirst = onlyfirst
1127 def match(self, rev):
1128 def realparents(rev):
1129 if self.onlyfirst:
1130 return repo.changelog.parentrevs(rev)[0:1]
1131 else:
1132 return filter(lambda x: x != nullrev,
1133 repo.changelog.parentrevs(rev))
1135 if self.startrev == nullrev:
1136 self.startrev = rev
1137 return True
1139 if rev > self.startrev:
1140 # forward: all descendants
1141 if not self.roots:
1142 self.roots.add(self.startrev)
1143 for parent in realparents(rev):
1144 if parent in self.roots:
1145 self.roots.add(rev)
1146 return True
1147 else:
1148 # backwards: all parents
1149 if not self.roots:
1150 self.roots.update(realparents(self.startrev))
1151 if rev in self.roots:
1152 self.roots.remove(rev)
1153 self.roots.update(realparents(rev))
1154 return True
1156 return False
1158 # it might be worthwhile to do this in the iterator if the rev range
1159 # is descending and the prune args are all within that range
1160 for rev in opts.get('prune', ()):
1161 rev = repo[rev].rev()
1162 ff = followfilter()
1163 stop = min(revs[0], revs[-1])
1164 for x in xrange(rev, stop - 1, -1):
1165 if ff.match(x):
1166 wanted.discard(x)
1168 # Now that wanted is correctly initialized, we can iterate over the
1169 # revision range, yielding only revisions in wanted.
1170 def iterate():
1171 if follow and not match.files():
1172 ff = followfilter(onlyfirst=opts.get('follow_first'))
1173 def want(rev):
1174 return ff.match(rev) and rev in wanted
1175 else:
1176 def want(rev):
1177 return rev in wanted
1179 for i, window in increasingwindows(0, len(revs)):
1180 nrevs = [rev for rev in revs[i:i + window] if want(rev)]
1181 for rev in sorted(nrevs):
1182 fns = fncache.get(rev)
1183 ctx = change(rev)
1184 if not fns:
1185 def fns_generator():
1186 for f in ctx.files():
1187 if match(f):
1188 yield f
1189 fns = fns_generator()
1190 prepare(ctx, fns)
1191 for rev in nrevs:
1192 yield change(rev)
1193 return iterate()
1195 def add(ui, repo, match, dryrun, listsubrepos, prefix, explicitonly):
1196 join = lambda f: os.path.join(prefix, f)
1197 bad = []
1198 oldbad = match.bad
1199 match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
1200 names = []
1201 wctx = repo[None]
1202 cca = None
1203 abort, warn = scmutil.checkportabilityalert(ui)
1204 if abort or warn:
1205 cca = scmutil.casecollisionauditor(ui, abort, wctx)
1206 for f in repo.walk(match):
1207 exact = match.exact(f)
1208 if exact or not explicitonly and f not in repo.dirstate:
1209 if cca:
1210 cca(f)
1211 names.append(f)
1212 if ui.verbose or not exact:
1213 ui.status(_('adding %s\n') % match.rel(join(f)))
1215 for subpath in wctx.substate:
1216 sub = wctx.sub(subpath)
1217 try:
1218 submatch = matchmod.narrowmatcher(subpath, match)
1219 if listsubrepos:
1220 bad.extend(sub.add(ui, submatch, dryrun, listsubrepos, prefix,
1221 False))
1222 else:
1223 bad.extend(sub.add(ui, submatch, dryrun, listsubrepos, prefix,
1224 True))
1225 except error.LookupError:
1226 ui.status(_("skipping missing subrepository: %s\n")
1227 % join(subpath))
1229 if not dryrun:
1230 rejected = wctx.add(names, prefix)
1231 bad.extend(f for f in rejected if f in match.files())
1232 return bad
1234 def forget(ui, repo, match, prefix, explicitonly):
1235 join = lambda f: os.path.join(prefix, f)
1236 bad = []
1237 oldbad = match.bad
1238 match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
1239 wctx = repo[None]
1240 forgot = []
1241 s = repo.status(match=match, clean=True)
1242 forget = sorted(s[0] + s[1] + s[3] + s[6])
1243 if explicitonly:
1244 forget = [f for f in forget if match.exact(f)]
1246 for subpath in wctx.substate:
1247 sub = wctx.sub(subpath)
1248 try:
1249 submatch = matchmod.narrowmatcher(subpath, match)
1250 subbad, subforgot = sub.forget(ui, submatch, prefix)
1251 bad.extend([subpath + '/' + f for f in subbad])
1252 forgot.extend([subpath + '/' + f for f in subforgot])
1253 except error.LookupError:
1254 ui.status(_("skipping missing subrepository: %s\n")
1255 % join(subpath))
1257 if not explicitonly:
1258 for f in match.files():
1259 if f not in repo.dirstate and not os.path.isdir(match.rel(join(f))):
1260 if f not in forgot:
1261 if os.path.exists(match.rel(join(f))):
1262 ui.warn(_('not removing %s: '
1263 'file is already untracked\n')
1264 % match.rel(join(f)))
1265 bad.append(f)
1267 for f in forget:
1268 if ui.verbose or not match.exact(f):
1269 ui.status(_('removing %s\n') % match.rel(join(f)))
1271 rejected = wctx.forget(forget, prefix)
1272 bad.extend(f for f in rejected if f in match.files())
1273 forgot.extend(forget)
1274 return bad, forgot
1276 def duplicatecopies(repo, rev, p1):
1277 "Reproduce copies found in the source revision in the dirstate for grafts"
1278 for dst, src in copies.pathcopies(repo[p1], repo[rev]).iteritems():
1279 repo.dirstate.copy(src, dst)
1281 def commit(ui, repo, commitfunc, pats, opts):
1282 '''commit the specified files or all outstanding changes'''
1283 date = opts.get('date')
1284 if date:
1285 opts['date'] = util.parsedate(date)
1286 message = logmessage(ui, opts)
1288 # extract addremove carefully -- this function can be called from a command
1289 # that doesn't support addremove
1290 if opts.get('addremove'):
1291 scmutil.addremove(repo, pats, opts)
1293 return commitfunc(ui, repo, message,
1294 scmutil.match(repo[None], pats, opts), opts)
1296 def amend(ui, repo, commitfunc, old, extra, pats, opts):
1297 ui.note(_('amending changeset %s\n') % old)
1298 base = old.p1()
1300 wlock = repo.wlock()
1301 try:
1302 # First, do a regular commit to record all changes in the working
1303 # directory (if there are any)
1304 node = commit(ui, repo, commitfunc, pats, opts)
1305 ctx = repo[node]
1307 # Participating changesets:
1309 # node/ctx o - new (intermediate) commit that contains changes from
1310 # | working dir to go into amending commit (or a workingctx
1311 # | if there were no changes)
1312 # |
1313 # old o - changeset to amend
1314 # |
1315 # base o - parent of amending changeset
1317 # Update extra dict from amended commit (e.g. to preserve graft source)
1318 extra.update(old.extra())
1320 # Also update it from the intermediate commit or from the wctx
1321 extra.update(ctx.extra())
1323 files = set(old.files())
1325 # Second, we use either the commit we just did, or if there were no
1326 # changes the parent of the working directory as the version of the
1327 # files in the final amend commit
1328 if node:
1329 ui.note(_('copying changeset %s to %s\n') % (ctx, base))
1331 user = ctx.user()
1332 date = ctx.date()
1333 message = ctx.description()
1334 # Recompute copies (avoid recording a -> b -> a)
1335 copied = copies.pathcopies(base, ctx)
1337 # Prune files which were reverted by the updates: if old introduced
1338 # file X and our intermediate commit, node, renamed that file, then
1339 # those two files are the same and we can discard X from our list
1340 # of files. Likewise if X was deleted, it's no longer relevant
1341 files.update(ctx.files())
1343 def samefile(f):
1344 if f in ctx.manifest():
1345 a = ctx.filectx(f)
1346 if f in base.manifest():
1347 b = base.filectx(f)
1348 return (not a.cmp(b)
1349 and a.flags() == b.flags())
1350 else:
1351 return False
1352 else:
1353 return f not in base.manifest()
1354 files = [f for f in files if not samefile(f)]
1356 def filectxfn(repo, ctx_, path):
1357 try:
1358 fctx = ctx[path]
1359 flags = fctx.flags()
1360 mctx = context.memfilectx(fctx.path(), fctx.data(),
1361 islink='l' in flags,
1362 isexec='x' in flags,
1363 copied=copied.get(path))
1364 return mctx
1365 except KeyError:
1366 raise IOError
1367 else:
1368 ui.note(_('copying changeset %s to %s\n') % (old, base))
1370 # Use version of files as in the old cset
1371 def filectxfn(repo, ctx_, path):
1372 try:
1373 return old.filectx(path)
1374 except KeyError:
1375 raise IOError
1377 # See if we got a message from -m or -l, if not, open the editor
1378 # with the message of the changeset to amend
1379 user = opts.get('user') or old.user()
1380 date = opts.get('date') or old.date()
1381 message = logmessage(ui, opts)
1382 if not message:
1383 cctx = context.workingctx(repo, old.description(), user, date,
1384 extra,
1385 repo.status(base.node(), old.node()))
1386 message = commitforceeditor(repo, cctx, [])
1388 new = context.memctx(repo,
1389 parents=[base.node(), nullid],
1390 text=message,
1391 files=files,
1392 filectxfn=filectxfn,
1393 user=user,
1394 date=date,
1395 extra=extra)
1396 newid = repo.commitctx(new)
1397 if newid != old.node():
1398 # Reroute the working copy parent to the new changeset
1399 repo.setparents(newid, nullid)
1401 # Move bookmarks from old parent to amend commit
1402 bms = repo.nodebookmarks(old.node())
1403 if bms:
1404 for bm in bms:
1405 repo._bookmarks[bm] = newid
1406 bookmarks.write(repo)
1408 # Strip the intermediate commit (if there was one) and the amended
1409 # commit
1410 lock = repo.lock()
1411 try:
1412 if node:
1413 ui.note(_('stripping intermediate changeset %s\n') % ctx)
1414 ui.note(_('stripping amended changeset %s\n') % old)
1415 repair.strip(ui, repo, old.node(), topic='amend-backup')
1416 finally:
1417 lock.release()
1418 finally:
1419 wlock.release()
1420 return newid
1422 def commiteditor(repo, ctx, subs):
1423 if ctx.description():
1424 return ctx.description()
1425 return commitforceeditor(repo, ctx, subs)
1427 def commitforceeditor(repo, ctx, subs):
1428 edittext = []
1429 modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
1430 if ctx.description():
1431 edittext.append(ctx.description())
1432 edittext.append("")
1433 edittext.append("") # Empty line between message and comments.
1434 edittext.append(_("HG: Enter commit message."
1435 " Lines beginning with 'HG:' are removed."))
1436 edittext.append(_("HG: Leave message empty to abort commit."))
1437 edittext.append("HG: --")
1438 edittext.append(_("HG: user: %s") % ctx.user())
1439 if ctx.p2():
1440 edittext.append(_("HG: branch merge"))
1441 if ctx.branch():
1442 edittext.append(_("HG: branch '%s'") % ctx.branch())
1443 edittext.extend([_("HG: subrepo %s") % s for s in subs])
1444 edittext.extend([_("HG: added %s") % f for f in added])
1445 edittext.extend([_("HG: changed %s") % f for f in modified])
1446 edittext.extend([_("HG: removed %s") % f for f in removed])
1447 if not added and not modified and not removed:
1448 edittext.append(_("HG: no files changed"))
1449 edittext.append("")
1450 # run editor in the repository root
1451 olddir = os.getcwd()
1452 os.chdir(repo.root)
1453 text = repo.ui.edit("\n".join(edittext), ctx.user())
1454 text = re.sub("(?m)^HG:.*(\n|$)", "", text)
1455 os.chdir(olddir)
1457 if not text.strip():
1458 raise util.Abort(_("empty commit message"))
1460 return text
1462 def revert(ui, repo, ctx, parents, *pats, **opts):
1463 parent, p2 = parents
1464 node = ctx.node()
1466 mf = ctx.manifest()
1467 if node == parent:
1468 pmf = mf
1469 else:
1470 pmf = None
1472 # need all matching names in dirstate and manifest of target rev,
1473 # so have to walk both. do not print errors if files exist in one
1474 # but not other.
1476 names = {}
1478 wlock = repo.wlock()
1479 try:
1480 # walk dirstate.
1482 m = scmutil.match(repo[None], pats, opts)
1483 m.bad = lambda x, y: False
1484 for abs in repo.walk(m):
1485 names[abs] = m.rel(abs), m.exact(abs)
1487 # walk target manifest.
1489 def badfn(path, msg):
1490 if path in names:
1491 return
1492 if path in ctx.substate:
1493 return
1494 path_ = path + '/'
1495 for f in names:
1496 if f.startswith(path_):
1497 return
1498 ui.warn("%s: %s\n" % (m.rel(path), msg))
1500 m = scmutil.match(ctx, pats, opts)
1501 m.bad = badfn
1502 for abs in ctx.walk(m):
1503 if abs not in names:
1504 names[abs] = m.rel(abs), m.exact(abs)
1506 # get the list of subrepos that must be reverted
1507 targetsubs = [s for s in ctx.substate if m(s)]
1508 m = scmutil.matchfiles(repo, names)
1509 changes = repo.status(match=m)[:4]
1510 modified, added, removed, deleted = map(set, changes)
1512 # if f is a rename, also revert the source
1513 cwd = repo.getcwd()
1514 for f in added:
1515 src = repo.dirstate.copied(f)
1516 if src and src not in names and repo.dirstate[src] == 'r':
1517 removed.add(src)
1518 names[src] = (repo.pathto(src, cwd), True)
1520 def removeforget(abs):
1521 if repo.dirstate[abs] == 'a':
1522 return _('forgetting %s\n')
1523 return _('removing %s\n')
1525 revert = ([], _('reverting %s\n'))
1526 add = ([], _('adding %s\n'))
1527 remove = ([], removeforget)
1528 undelete = ([], _('undeleting %s\n'))
1530 disptable = (
1531 # dispatch table:
1532 # file state
1533 # action if in target manifest
1534 # action if not in target manifest
1535 # make backup if in target manifest
1536 # make backup if not in target manifest
1537 (modified, revert, remove, True, True),
1538 (added, revert, remove, True, False),
1539 (removed, undelete, None, False, False),
1540 (deleted, revert, remove, False, False),
1543 for abs, (rel, exact) in sorted(names.items()):
1544 mfentry = mf.get(abs)
1545 target = repo.wjoin(abs)
1546 def handle(xlist, dobackup):
1547 xlist[0].append(abs)
1548 if (dobackup and not opts.get('no_backup') and
1549 os.path.lexists(target)):
1550 bakname = "%s.orig" % rel
1551 ui.note(_('saving current version of %s as %s\n') %
1552 (rel, bakname))
1553 if not opts.get('dry_run'):
1554 util.rename(target, bakname)
1555 if ui.verbose or not exact:
1556 msg = xlist[1]
1557 if not isinstance(msg, basestring):
1558 msg = msg(abs)
1559 ui.status(msg % rel)
1560 for table, hitlist, misslist, backuphit, backupmiss in disptable:
1561 if abs not in table:
1562 continue
1563 # file has changed in dirstate
1564 if mfentry:
1565 handle(hitlist, backuphit)
1566 elif misslist is not None:
1567 handle(misslist, backupmiss)
1568 break
1569 else:
1570 if abs not in repo.dirstate:
1571 if mfentry:
1572 handle(add, True)
1573 elif exact:
1574 ui.warn(_('file not managed: %s\n') % rel)
1575 continue
1576 # file has not changed in dirstate
1577 if node == parent:
1578 if exact:
1579 ui.warn(_('no changes needed to %s\n') % rel)
1580 continue
1581 if pmf is None:
1582 # only need parent manifest in this unlikely case,
1583 # so do not read by default
1584 pmf = repo[parent].manifest()
1585 if abs in pmf and mfentry:
1586 # if version of file is same in parent and target
1587 # manifests, do nothing
1588 if (pmf[abs] != mfentry or
1589 pmf.flags(abs) != mf.flags(abs)):
1590 handle(revert, False)
1591 else:
1592 handle(remove, False)
1594 if not opts.get('dry_run'):
1595 def checkout(f):
1596 fc = ctx[f]
1597 repo.wwrite(f, fc.data(), fc.flags())
1599 audit_path = scmutil.pathauditor(repo.root)
1600 for f in remove[0]:
1601 if repo.dirstate[f] == 'a':
1602 repo.dirstate.drop(f)
1603 continue
1604 audit_path(f)
1605 try:
1606 util.unlinkpath(repo.wjoin(f))
1607 except OSError:
1608 pass
1609 repo.dirstate.remove(f)
1611 normal = None
1612 if node == parent:
1613 # We're reverting to our parent. If possible, we'd like status
1614 # to report the file as clean. We have to use normallookup for
1615 # merges to avoid losing information about merged/dirty files.
1616 if p2 != nullid:
1617 normal = repo.dirstate.normallookup
1618 else:
1619 normal = repo.dirstate.normal
1620 for f in revert[0]:
1621 checkout(f)
1622 if normal:
1623 normal(f)
1625 for f in add[0]:
1626 checkout(f)
1627 repo.dirstate.add(f)
1629 normal = repo.dirstate.normallookup
1630 if node == parent and p2 == nullid:
1631 normal = repo.dirstate.normal
1632 for f in undelete[0]:
1633 checkout(f)
1634 normal(f)
1636 if targetsubs:
1637 # Revert the subrepos on the revert list
1638 for sub in targetsubs:
1639 ctx.sub(sub).revert(ui, ctx.substate[sub], *pats, **opts)
1640 finally:
1641 wlock.release()
1643 def command(table):
1644 '''returns a function object bound to table which can be used as
1645 a decorator for populating table as a command table'''
1647 def cmd(name, options, synopsis=None):
1648 def decorator(func):
1649 if synopsis:
1650 table[name] = func, options[:], synopsis
1651 else:
1652 table[name] = func, options[:]
1653 return func
1654 return decorator
1656 return cmd