|
|
# cmdutil.py - help for command processing in mercurial
|
|
|
#
|
|
|
# Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
|
|
|
#
|
|
|
# This software may be used and distributed according to the terms of the
|
|
|
# GNU General Public License version 2 or any later version.
|
|
|
|
|
|
from node import hex, nullid, nullrev, short
|
|
|
from i18n import _
|
|
|
import os, sys, errno, re, tempfile
|
|
|
import util, scmutil, templater, patch, error, templatekw, revlog, copies
|
|
|
import match as matchmod
|
|
|
import subrepo, context, repair, graphmod, revset, phases, obsolete
|
|
|
import changelog
|
|
|
import bookmarks
|
|
|
import lock as lockmod
|
|
|
|
|
|
def parsealiases(cmd):
|
|
|
return cmd.lstrip("^").split("|")
|
|
|
|
|
|
def findpossible(cmd, table, strict=False):
|
|
|
"""
|
|
|
Return cmd -> (aliases, command table entry)
|
|
|
for each matching command.
|
|
|
Return debug commands (or their aliases) only if no normal command matches.
|
|
|
"""
|
|
|
choice = {}
|
|
|
debugchoice = {}
|
|
|
|
|
|
if cmd in table:
|
|
|
# short-circuit exact matches, "log" alias beats "^log|history"
|
|
|
keys = [cmd]
|
|
|
else:
|
|
|
keys = table.keys()
|
|
|
|
|
|
for e in keys:
|
|
|
aliases = parsealiases(e)
|
|
|
found = None
|
|
|
if cmd in aliases:
|
|
|
found = cmd
|
|
|
elif not strict:
|
|
|
for a in aliases:
|
|
|
if a.startswith(cmd):
|
|
|
found = a
|
|
|
break
|
|
|
if found is not None:
|
|
|
if aliases[0].startswith("debug") or found.startswith("debug"):
|
|
|
debugchoice[found] = (aliases, table[e])
|
|
|
else:
|
|
|
choice[found] = (aliases, table[e])
|
|
|
|
|
|
if not choice and debugchoice:
|
|
|
choice = debugchoice
|
|
|
|
|
|
return choice
|
|
|
|
|
|
def findcmd(cmd, table, strict=True):
|
|
|
"""Return (aliases, command table entry) for command string."""
|
|
|
choice = findpossible(cmd, table, strict)
|
|
|
|
|
|
if cmd in choice:
|
|
|
return choice[cmd]
|
|
|
|
|
|
if len(choice) > 1:
|
|
|
clist = choice.keys()
|
|
|
clist.sort()
|
|
|
raise error.AmbiguousCommand(cmd, clist)
|
|
|
|
|
|
if choice:
|
|
|
return choice.values()[0]
|
|
|
|
|
|
raise error.UnknownCommand(cmd)
|
|
|
|
|
|
def findrepo(p):
|
|
|
while not os.path.isdir(os.path.join(p, ".hg")):
|
|
|
oldp, p = p, os.path.dirname(p)
|
|
|
if p == oldp:
|
|
|
return None
|
|
|
|
|
|
return p
|
|
|
|
|
|
def bailifchanged(repo):
|
|
|
if repo.dirstate.p2() != nullid:
|
|
|
raise util.Abort(_('outstanding uncommitted merge'))
|
|
|
modified, added, removed, deleted = repo.status()[:4]
|
|
|
if modified or added or removed or deleted:
|
|
|
raise util.Abort(_('uncommitted changes'))
|
|
|
ctx = repo[None]
|
|
|
for s in sorted(ctx.substate):
|
|
|
if ctx.sub(s).dirty():
|
|
|
raise util.Abort(_("uncommitted changes in subrepo %s") % s)
|
|
|
|
|
|
def logmessage(ui, opts):
|
|
|
""" get the log message according to -m and -l option """
|
|
|
message = opts.get('message')
|
|
|
logfile = opts.get('logfile')
|
|
|
|
|
|
if message and logfile:
|
|
|
raise util.Abort(_('options --message and --logfile are mutually '
|
|
|
'exclusive'))
|
|
|
if not message and logfile:
|
|
|
try:
|
|
|
if logfile == '-':
|
|
|
message = ui.fin.read()
|
|
|
else:
|
|
|
message = '\n'.join(util.readfile(logfile).splitlines())
|
|
|
except IOError, inst:
|
|
|
raise util.Abort(_("can't read commit message '%s': %s") %
|
|
|
(logfile, inst.strerror))
|
|
|
return message
|
|
|
|
|
|
def loglimit(opts):
|
|
|
"""get the log limit according to option -l/--limit"""
|
|
|
limit = opts.get('limit')
|
|
|
if limit:
|
|
|
try:
|
|
|
limit = int(limit)
|
|
|
except ValueError:
|
|
|
raise util.Abort(_('limit must be a positive integer'))
|
|
|
if limit <= 0:
|
|
|
raise util.Abort(_('limit must be positive'))
|
|
|
else:
|
|
|
limit = None
|
|
|
return limit
|
|
|
|
|
|
def makefilename(repo, pat, node, desc=None,
|
|
|
total=None, seqno=None, revwidth=None, pathname=None):
|
|
|
node_expander = {
|
|
|
'H': lambda: hex(node),
|
|
|
'R': lambda: str(repo.changelog.rev(node)),
|
|
|
'h': lambda: short(node),
|
|
|
'm': lambda: re.sub('[^\w]', '_', str(desc))
|
|
|
}
|
|
|
expander = {
|
|
|
'%': lambda: '%',
|
|
|
'b': lambda: os.path.basename(repo.root),
|
|
|
}
|
|
|
|
|
|
try:
|
|
|
if node:
|
|
|
expander.update(node_expander)
|
|
|
if node:
|
|
|
expander['r'] = (lambda:
|
|
|
str(repo.changelog.rev(node)).zfill(revwidth or 0))
|
|
|
if total is not None:
|
|
|
expander['N'] = lambda: str(total)
|
|
|
if seqno is not None:
|
|
|
expander['n'] = lambda: str(seqno)
|
|
|
if total is not None and seqno is not None:
|
|
|
expander['n'] = lambda: str(seqno).zfill(len(str(total)))
|
|
|
if pathname is not None:
|
|
|
expander['s'] = lambda: os.path.basename(pathname)
|
|
|
expander['d'] = lambda: os.path.dirname(pathname) or '.'
|
|
|
expander['p'] = lambda: pathname
|
|
|
|
|
|
newname = []
|
|
|
patlen = len(pat)
|
|
|
i = 0
|
|
|
while i < patlen:
|
|
|
c = pat[i]
|
|
|
if c == '%':
|
|
|
i += 1
|
|
|
c = pat[i]
|
|
|
c = expander[c]()
|
|
|
newname.append(c)
|
|
|
i += 1
|
|
|
return ''.join(newname)
|
|
|
except KeyError, inst:
|
|
|
raise util.Abort(_("invalid format spec '%%%s' in output filename") %
|
|
|
inst.args[0])
|
|
|
|
|
|
def makefileobj(repo, pat, node=None, desc=None, total=None,
|
|
|
seqno=None, revwidth=None, mode='wb', modemap=None,
|
|
|
pathname=None):
|
|
|
|
|
|
writable = mode not in ('r', 'rb')
|
|
|
|
|
|
if not pat or pat == '-':
|
|
|
fp = writable and repo.ui.fout or repo.ui.fin
|
|
|
if util.safehasattr(fp, 'fileno'):
|
|
|
return os.fdopen(os.dup(fp.fileno()), mode)
|
|
|
else:
|
|
|
# if this fp can't be duped properly, return
|
|
|
# a dummy object that can be closed
|
|
|
class wrappedfileobj(object):
|
|
|
noop = lambda x: None
|
|
|
def __init__(self, f):
|
|
|
self.f = f
|
|
|
def __getattr__(self, attr):
|
|
|
if attr == 'close':
|
|
|
return self.noop
|
|
|
else:
|
|
|
return getattr(self.f, attr)
|
|
|
|
|
|
return wrappedfileobj(fp)
|
|
|
if util.safehasattr(pat, 'write') and writable:
|
|
|
return pat
|
|
|
if util.safehasattr(pat, 'read') and 'r' in mode:
|
|
|
return pat
|
|
|
fn = makefilename(repo, pat, node, desc, total, seqno, revwidth, pathname)
|
|
|
if modemap is not None:
|
|
|
mode = modemap.get(fn, mode)
|
|
|
if mode == 'wb':
|
|
|
modemap[fn] = 'ab'
|
|
|
return open(fn, mode)
|
|
|
|
|
|
def openrevlog(repo, cmd, file_, opts):
|
|
|
"""opens the changelog, manifest, a filelog or a given revlog"""
|
|
|
cl = opts['changelog']
|
|
|
mf = opts['manifest']
|
|
|
msg = None
|
|
|
if cl and mf:
|
|
|
msg = _('cannot specify --changelog and --manifest at the same time')
|
|
|
elif cl or mf:
|
|
|
if file_:
|
|
|
msg = _('cannot specify filename with --changelog or --manifest')
|
|
|
elif not repo:
|
|
|
msg = _('cannot specify --changelog or --manifest '
|
|
|
'without a repository')
|
|
|
if msg:
|
|
|
raise util.Abort(msg)
|
|
|
|
|
|
r = None
|
|
|
if repo:
|
|
|
if cl:
|
|
|
r = repo.changelog
|
|
|
elif mf:
|
|
|
r = repo.manifest
|
|
|
elif file_:
|
|
|
filelog = repo.file(file_)
|
|
|
if len(filelog):
|
|
|
r = filelog
|
|
|
if not r:
|
|
|
if not file_:
|
|
|
raise error.CommandError(cmd, _('invalid arguments'))
|
|
|
if not os.path.isfile(file_):
|
|
|
raise util.Abort(_("revlog '%s' not found") % file_)
|
|
|
r = revlog.revlog(scmutil.opener(os.getcwd(), audit=False),
|
|
|
file_[:-2] + ".i")
|
|
|
return r
|
|
|
|
|
|
def copy(ui, repo, pats, opts, rename=False):
|
|
|
# called with the repo lock held
|
|
|
#
|
|
|
# hgsep => pathname that uses "/" to separate directories
|
|
|
# ossep => pathname that uses os.sep to separate directories
|
|
|
cwd = repo.getcwd()
|
|
|
targets = {}
|
|
|
after = opts.get("after")
|
|
|
dryrun = opts.get("dry_run")
|
|
|
wctx = repo[None]
|
|
|
|
|
|
def walkpat(pat):
|
|
|
srcs = []
|
|
|
badstates = after and '?' or '?r'
|
|
|
m = scmutil.match(repo[None], [pat], opts, globbed=True)
|
|
|
for abs in repo.walk(m):
|
|
|
state = repo.dirstate[abs]
|
|
|
rel = m.rel(abs)
|
|
|
exact = m.exact(abs)
|
|
|
if state in badstates:
|
|
|
if exact and state == '?':
|
|
|
ui.warn(_('%s: not copying - file is not managed\n') % rel)
|
|
|
if exact and state == 'r':
|
|
|
ui.warn(_('%s: not copying - file has been marked for'
|
|
|
' remove\n') % rel)
|
|
|
continue
|
|
|
# abs: hgsep
|
|
|
# rel: ossep
|
|
|
srcs.append((abs, rel, exact))
|
|
|
return srcs
|
|
|
|
|
|
# abssrc: hgsep
|
|
|
# relsrc: ossep
|
|
|
# otarget: ossep
|
|
|
def copyfile(abssrc, relsrc, otarget, exact):
|
|
|
abstarget = scmutil.canonpath(repo.root, cwd, otarget)
|
|
|
if '/' in abstarget:
|
|
|
# We cannot normalize abstarget itself, this would prevent
|
|
|
# case only renames, like a => A.
|
|
|
abspath, absname = abstarget.rsplit('/', 1)
|
|
|
abstarget = repo.dirstate.normalize(abspath) + '/' + absname
|
|
|
reltarget = repo.pathto(abstarget, cwd)
|
|
|
target = repo.wjoin(abstarget)
|
|
|
src = repo.wjoin(abssrc)
|
|
|
state = repo.dirstate[abstarget]
|
|
|
|
|
|
scmutil.checkportable(ui, abstarget)
|
|
|
|
|
|
# check for collisions
|
|
|
prevsrc = targets.get(abstarget)
|
|
|
if prevsrc is not None:
|
|
|
ui.warn(_('%s: not overwriting - %s collides with %s\n') %
|
|
|
(reltarget, repo.pathto(abssrc, cwd),
|
|
|
repo.pathto(prevsrc, cwd)))
|
|
|
return
|
|
|
|
|
|
# check for overwrites
|
|
|
exists = os.path.lexists(target)
|
|
|
samefile = False
|
|
|
if exists and abssrc != abstarget:
|
|
|
if (repo.dirstate.normalize(abssrc) ==
|
|
|
repo.dirstate.normalize(abstarget)):
|
|
|
if not rename:
|
|
|
ui.warn(_("%s: can't copy - same file\n") % reltarget)
|
|
|
return
|
|
|
exists = False
|
|
|
samefile = True
|
|
|
|
|
|
if not after and exists or after and state in 'mn':
|
|
|
if not opts['force']:
|
|
|
ui.warn(_('%s: not overwriting - file exists\n') %
|
|
|
reltarget)
|
|
|
return
|
|
|
|
|
|
if after:
|
|
|
if not exists:
|
|
|
if rename:
|
|
|
ui.warn(_('%s: not recording move - %s does not exist\n') %
|
|
|
(relsrc, reltarget))
|
|
|
else:
|
|
|
ui.warn(_('%s: not recording copy - %s does not exist\n') %
|
|
|
(relsrc, reltarget))
|
|
|
return
|
|
|
elif not dryrun:
|
|
|
try:
|
|
|
if exists:
|
|
|
os.unlink(target)
|
|
|
targetdir = os.path.dirname(target) or '.'
|
|
|
if not os.path.isdir(targetdir):
|
|
|
os.makedirs(targetdir)
|
|
|
if samefile:
|
|
|
tmp = target + "~hgrename"
|
|
|
os.rename(src, tmp)
|
|
|
os.rename(tmp, target)
|
|
|
else:
|
|
|
util.copyfile(src, target)
|
|
|
srcexists = True
|
|
|
except IOError, inst:
|
|
|
if inst.errno == errno.ENOENT:
|
|
|
ui.warn(_('%s: deleted in working copy\n') % relsrc)
|
|
|
srcexists = False
|
|
|
else:
|
|
|
ui.warn(_('%s: cannot copy - %s\n') %
|
|
|
(relsrc, inst.strerror))
|
|
|
return True # report a failure
|
|
|
|
|
|
if ui.verbose or not exact:
|
|
|
if rename:
|
|
|
ui.status(_('moving %s to %s\n') % (relsrc, reltarget))
|
|
|
else:
|
|
|
ui.status(_('copying %s to %s\n') % (relsrc, reltarget))
|
|
|
|
|
|
targets[abstarget] = abssrc
|
|
|
|
|
|
# fix up dirstate
|
|
|
scmutil.dirstatecopy(ui, repo, wctx, abssrc, abstarget,
|
|
|
dryrun=dryrun, cwd=cwd)
|
|
|
if rename and not dryrun:
|
|
|
if not after and srcexists and not samefile:
|
|
|
util.unlinkpath(repo.wjoin(abssrc))
|
|
|
wctx.forget([abssrc])
|
|
|
|
|
|
# pat: ossep
|
|
|
# dest ossep
|
|
|
# srcs: list of (hgsep, hgsep, ossep, bool)
|
|
|
# return: function that takes hgsep and returns ossep
|
|
|
def targetpathfn(pat, dest, srcs):
|
|
|
if os.path.isdir(pat):
|
|
|
abspfx = scmutil.canonpath(repo.root, cwd, pat)
|
|
|
abspfx = util.localpath(abspfx)
|
|
|
if destdirexists:
|
|
|
striplen = len(os.path.split(abspfx)[0])
|
|
|
else:
|
|
|
striplen = len(abspfx)
|
|
|
if striplen:
|
|
|
striplen += len(os.sep)
|
|
|
res = lambda p: os.path.join(dest, util.localpath(p)[striplen:])
|
|
|
elif destdirexists:
|
|
|
res = lambda p: os.path.join(dest,
|
|
|
os.path.basename(util.localpath(p)))
|
|
|
else:
|
|
|
res = lambda p: dest
|
|
|
return res
|
|
|
|
|
|
# pat: ossep
|
|
|
# dest ossep
|
|
|
# srcs: list of (hgsep, hgsep, ossep, bool)
|
|
|
# return: function that takes hgsep and returns ossep
|
|
|
def targetpathafterfn(pat, dest, srcs):
|
|
|
if matchmod.patkind(pat):
|
|
|
# a mercurial pattern
|
|
|
res = lambda p: os.path.join(dest,
|
|
|
os.path.basename(util.localpath(p)))
|
|
|
else:
|
|
|
abspfx = scmutil.canonpath(repo.root, cwd, pat)
|
|
|
if len(abspfx) < len(srcs[0][0]):
|
|
|
# A directory. Either the target path contains the last
|
|
|
# component of the source path or it does not.
|
|
|
def evalpath(striplen):
|
|
|
score = 0
|
|
|
for s in srcs:
|
|
|
t = os.path.join(dest, util.localpath(s[0])[striplen:])
|
|
|
if os.path.lexists(t):
|
|
|
score += 1
|
|
|
return score
|
|
|
|
|
|
abspfx = util.localpath(abspfx)
|
|
|
striplen = len(abspfx)
|
|
|
if striplen:
|
|
|
striplen += len(os.sep)
|
|
|
if os.path.isdir(os.path.join(dest, os.path.split(abspfx)[1])):
|
|
|
score = evalpath(striplen)
|
|
|
striplen1 = len(os.path.split(abspfx)[0])
|
|
|
if striplen1:
|
|
|
striplen1 += len(os.sep)
|
|
|
if evalpath(striplen1) > score:
|
|
|
striplen = striplen1
|
|
|
res = lambda p: os.path.join(dest,
|
|
|
util.localpath(p)[striplen:])
|
|
|
else:
|
|
|
# a file
|
|
|
if destdirexists:
|
|
|
res = lambda p: os.path.join(dest,
|
|
|
os.path.basename(util.localpath(p)))
|
|
|
else:
|
|
|
res = lambda p: dest
|
|
|
return res
|
|
|
|
|
|
|
|
|
pats = scmutil.expandpats(pats)
|
|
|
if not pats:
|
|
|
raise util.Abort(_('no source or destination specified'))
|
|
|
if len(pats) == 1:
|
|
|
raise util.Abort(_('no destination specified'))
|
|
|
dest = pats.pop()
|
|
|
destdirexists = os.path.isdir(dest) and not os.path.islink(dest)
|
|
|
if not destdirexists:
|
|
|
if len(pats) > 1 or matchmod.patkind(pats[0]):
|
|
|
raise util.Abort(_('with multiple sources, destination must be an '
|
|
|
'existing directory'))
|
|
|
if util.endswithsep(dest):
|
|
|
raise util.Abort(_('destination %s is not a directory') % dest)
|
|
|
|
|
|
tfn = targetpathfn
|
|
|
if after:
|
|
|
tfn = targetpathafterfn
|
|
|
copylist = []
|
|
|
for pat in pats:
|
|
|
srcs = walkpat(pat)
|
|
|
if not srcs:
|
|
|
continue
|
|
|
copylist.append((tfn(pat, dest, srcs), srcs))
|
|
|
if not copylist:
|
|
|
raise util.Abort(_('no files to copy'))
|
|
|
|
|
|
errors = 0
|
|
|
for targetpath, srcs in copylist:
|
|
|
for abssrc, relsrc, exact in srcs:
|
|
|
if copyfile(abssrc, relsrc, targetpath(abssrc), exact):
|
|
|
errors += 1
|
|
|
|
|
|
if errors:
|
|
|
ui.warn(_('(consider using --after)\n'))
|
|
|
|
|
|
return errors != 0
|
|
|
|
|
|
def service(opts, parentfn=None, initfn=None, runfn=None, logfile=None,
|
|
|
runargs=None, appendpid=False):
|
|
|
'''Run a command as a service.'''
|
|
|
|
|
|
def writepid(pid):
|
|
|
if opts['pid_file']:
|
|
|
mode = appendpid and 'a' or 'w'
|
|
|
fp = open(opts['pid_file'], mode)
|
|
|
fp.write(str(pid) + '\n')
|
|
|
fp.close()
|
|
|
|
|
|
if opts['daemon'] and not opts['daemon_pipefds']:
|
|
|
# Signal child process startup with file removal
|
|
|
lockfd, lockpath = tempfile.mkstemp(prefix='hg-service-')
|
|
|
os.close(lockfd)
|
|
|
try:
|
|
|
if not runargs:
|
|
|
runargs = util.hgcmd() + sys.argv[1:]
|
|
|
runargs.append('--daemon-pipefds=%s' % lockpath)
|
|
|
# Don't pass --cwd to the child process, because we've already
|
|
|
# changed directory.
|
|
|
for i in xrange(1, len(runargs)):
|
|
|
if runargs[i].startswith('--cwd='):
|
|
|
del runargs[i]
|
|
|
break
|
|
|
elif runargs[i].startswith('--cwd'):
|
|
|
del runargs[i:i + 2]
|
|
|
break
|
|
|
def condfn():
|
|
|
return not os.path.exists(lockpath)
|
|
|
pid = util.rundetached(runargs, condfn)
|
|
|
if pid < 0:
|
|
|
raise util.Abort(_('child process failed to start'))
|
|
|
writepid(pid)
|
|
|
finally:
|
|
|
try:
|
|
|
os.unlink(lockpath)
|
|
|
except OSError, e:
|
|
|
if e.errno != errno.ENOENT:
|
|
|
raise
|
|
|
if parentfn:
|
|
|
return parentfn(pid)
|
|
|
else:
|
|
|
return
|
|
|
|
|
|
if initfn:
|
|
|
initfn()
|
|
|
|
|
|
if not opts['daemon']:
|
|
|
writepid(os.getpid())
|
|
|
|
|
|
if opts['daemon_pipefds']:
|
|
|
lockpath = opts['daemon_pipefds']
|
|
|
try:
|
|
|
os.setsid()
|
|
|
except AttributeError:
|
|
|
pass
|
|
|
os.unlink(lockpath)
|
|
|
util.hidewindow()
|
|
|
sys.stdout.flush()
|
|
|
sys.stderr.flush()
|
|
|
|
|
|
nullfd = os.open(os.devnull, os.O_RDWR)
|
|
|
logfilefd = nullfd
|
|
|
if logfile:
|
|
|
logfilefd = os.open(logfile, os.O_RDWR | os.O_CREAT | os.O_APPEND)
|
|
|
os.dup2(nullfd, 0)
|
|
|
os.dup2(logfilefd, 1)
|
|
|
os.dup2(logfilefd, 2)
|
|
|
if nullfd not in (0, 1, 2):
|
|
|
os.close(nullfd)
|
|
|
if logfile and logfilefd not in (0, 1, 2):
|
|
|
os.close(logfilefd)
|
|
|
|
|
|
if runfn:
|
|
|
return runfn()
|
|
|
|
|
|
def export(repo, revs, template='hg-%h.patch', fp=None, switch_parent=False,
|
|
|
opts=None):
|
|
|
'''export changesets as hg patches.'''
|
|
|
|
|
|
total = len(revs)
|
|
|
revwidth = max([len(str(rev)) for rev in revs])
|
|
|
filemode = {}
|
|
|
|
|
|
def single(rev, seqno, fp):
|
|
|
ctx = repo[rev]
|
|
|
node = ctx.node()
|
|
|
parents = [p.node() for p in ctx.parents() if p]
|
|
|
branch = ctx.branch()
|
|
|
if switch_parent:
|
|
|
parents.reverse()
|
|
|
prev = (parents and parents[0]) or nullid
|
|
|
|
|
|
shouldclose = False
|
|
|
if not fp and len(template) > 0:
|
|
|
desc_lines = ctx.description().rstrip().split('\n')
|
|
|
desc = desc_lines[0] #Commit always has a first line.
|
|
|
fp = makefileobj(repo, template, node, desc=desc, total=total,
|
|
|
seqno=seqno, revwidth=revwidth, mode='wb',
|
|
|
modemap=filemode)
|
|
|
if fp != template:
|
|
|
shouldclose = True
|
|
|
if fp and fp != sys.stdout and util.safehasattr(fp, 'name'):
|
|
|
repo.ui.note("%s\n" % fp.name)
|
|
|
|
|
|
if not fp:
|
|
|
write = repo.ui.write
|
|
|
else:
|
|
|
def write(s, **kw):
|
|
|
fp.write(s)
|
|
|
|
|
|
|
|
|
write("# HG changeset patch\n")
|
|
|
write("# User %s\n" % ctx.user())
|
|
|
write("# Date %d %d\n" % ctx.date())
|
|
|
write("# %s\n" % util.datestr(ctx.date()))
|
|
|
if branch and branch != 'default':
|
|
|
write("# Branch %s\n" % branch)
|
|
|
write("# Node ID %s\n" % hex(node))
|
|
|
write("# Parent %s\n" % hex(prev))
|
|
|
if len(parents) > 1:
|
|
|
write("# Parent %s\n" % hex(parents[1]))
|
|
|
write(ctx.description().rstrip())
|
|
|
write("\n\n")
|
|
|
|
|
|
for chunk, label in patch.diffui(repo, prev, node, opts=opts):
|
|
|
write(chunk, label=label)
|
|
|
|
|
|
if shouldclose:
|
|
|
fp.close()
|
|
|
|
|
|
for seqno, rev in enumerate(revs):
|
|
|
single(rev, seqno + 1, fp)
|
|
|
|
|
|
def diffordiffstat(ui, repo, diffopts, node1, node2, match,
|
|
|
changes=None, stat=False, fp=None, prefix='',
|
|
|
listsubrepos=False):
|
|
|
'''show diff or diffstat.'''
|
|
|
if fp is None:
|
|
|
write = ui.write
|
|
|
else:
|
|
|
def write(s, **kw):
|
|
|
fp.write(s)
|
|
|
|
|
|
if stat:
|
|
|
diffopts = diffopts.copy(context=0)
|
|
|
width = 80
|
|
|
if not ui.plain():
|
|
|
width = ui.termwidth()
|
|
|
chunks = patch.diff(repo, node1, node2, match, changes, diffopts,
|
|
|
prefix=prefix)
|
|
|
for chunk, label in patch.diffstatui(util.iterlines(chunks),
|
|
|
width=width,
|
|
|
git=diffopts.git):
|
|
|
write(chunk, label=label)
|
|
|
else:
|
|
|
for chunk, label in patch.diffui(repo, node1, node2, match,
|
|
|
changes, diffopts, prefix=prefix):
|
|
|
write(chunk, label=label)
|
|
|
|
|
|
if listsubrepos:
|
|
|
ctx1 = repo[node1]
|
|
|
ctx2 = repo[node2]
|
|
|
for subpath, sub in subrepo.itersubrepos(ctx1, ctx2):
|
|
|
tempnode2 = node2
|
|
|
try:
|
|
|
if node2 is not None:
|
|
|
tempnode2 = ctx2.substate[subpath][1]
|
|
|
except KeyError:
|
|
|
# A subrepo that existed in node1 was deleted between node1 and
|
|
|
# node2 (inclusive). Thus, ctx2's substate won't contain that
|
|
|
# subpath. The best we can do is to ignore it.
|
|
|
tempnode2 = None
|
|
|
submatch = matchmod.narrowmatcher(subpath, match)
|
|
|
sub.diff(ui, diffopts, tempnode2, submatch, changes=changes,
|
|
|
stat=stat, fp=fp, prefix=prefix)
|
|
|
|
|
|
class changeset_printer(object):
|
|
|
'''show changeset information when templating not requested.'''
|
|
|
|
|
|
def __init__(self, ui, repo, patch, diffopts, buffered):
|
|
|
self.ui = ui
|
|
|
self.repo = repo
|
|
|
self.buffered = buffered
|
|
|
self.patch = patch
|
|
|
self.diffopts = diffopts
|
|
|
self.header = {}
|
|
|
self.hunk = {}
|
|
|
self.lastheader = None
|
|
|
self.footer = None
|
|
|
|
|
|
def flush(self, rev):
|
|
|
if rev in self.header:
|
|
|
h = self.header[rev]
|
|
|
if h != self.lastheader:
|
|
|
self.lastheader = h
|
|
|
self.ui.write(h)
|
|
|
del self.header[rev]
|
|
|
if rev in self.hunk:
|
|
|
self.ui.write(self.hunk[rev])
|
|
|
del self.hunk[rev]
|
|
|
return 1
|
|
|
return 0
|
|
|
|
|
|
def close(self):
|
|
|
if self.footer:
|
|
|
self.ui.write(self.footer)
|
|
|
|
|
|
def show(self, ctx, copies=None, matchfn=None, **props):
|
|
|
if self.buffered:
|
|
|
self.ui.pushbuffer()
|
|
|
self._show(ctx, copies, matchfn, props)
|
|
|
self.hunk[ctx.rev()] = self.ui.popbuffer(labeled=True)
|
|
|
else:
|
|
|
self._show(ctx, copies, matchfn, props)
|
|
|
|
|
|
def _show(self, ctx, copies, matchfn, props):
|
|
|
'''show a single changeset or file revision'''
|
|
|
changenode = ctx.node()
|
|
|
rev = ctx.rev()
|
|
|
|
|
|
if self.ui.quiet:
|
|
|
self.ui.write("%d:%s\n" % (rev, short(changenode)),
|
|
|
label='log.node')
|
|
|
return
|
|
|
|
|
|
log = self.repo.changelog
|
|
|
date = util.datestr(ctx.date())
|
|
|
|
|
|
hexfunc = self.ui.debugflag and hex or short
|
|
|
|
|
|
parents = [(p, hexfunc(log.node(p)))
|
|
|
for p in self._meaningful_parentrevs(log, rev)]
|
|
|
|
|
|
# i18n: column positioning for "hg log"
|
|
|
self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode)),
|
|
|
label='log.changeset changeset.%s' % ctx.phasestr())
|
|
|
|
|
|
branch = ctx.branch()
|
|
|
# don't show the default branch name
|
|
|
if branch != 'default':
|
|
|
# i18n: column positioning for "hg log"
|
|
|
self.ui.write(_("branch: %s\n") % branch,
|
|
|
label='log.branch')
|
|
|
for bookmark in self.repo.nodebookmarks(changenode):
|
|
|
# i18n: column positioning for "hg log"
|
|
|
self.ui.write(_("bookmark: %s\n") % bookmark,
|
|
|
label='log.bookmark')
|
|
|
for tag in self.repo.nodetags(changenode):
|
|
|
# i18n: column positioning for "hg log"
|
|
|
self.ui.write(_("tag: %s\n") % tag,
|
|
|
label='log.tag')
|
|
|
if self.ui.debugflag and ctx.phase():
|
|
|
# i18n: column positioning for "hg log"
|
|
|
self.ui.write(_("phase: %s\n") % _(ctx.phasestr()),
|
|
|
label='log.phase')
|
|
|
for parent in parents:
|
|
|
# i18n: column positioning for "hg log"
|
|
|
self.ui.write(_("parent: %d:%s\n") % parent,
|
|
|
label='log.parent changeset.%s' % ctx.phasestr())
|
|
|
|
|
|
if self.ui.debugflag:
|
|
|
mnode = ctx.manifestnode()
|
|
|
# i18n: column positioning for "hg log"
|
|
|
self.ui.write(_("manifest: %d:%s\n") %
|
|
|
(self.repo.manifest.rev(mnode), hex(mnode)),
|
|
|
label='ui.debug log.manifest')
|
|
|
# i18n: column positioning for "hg log"
|
|
|
self.ui.write(_("user: %s\n") % ctx.user(),
|
|
|
label='log.user')
|
|
|
# i18n: column positioning for "hg log"
|
|
|
self.ui.write(_("date: %s\n") % date,
|
|
|
label='log.date')
|
|
|
|
|
|
if self.ui.debugflag:
|
|
|
files = self.repo.status(log.parents(changenode)[0], changenode)[:3]
|
|
|
for key, value in zip([# i18n: column positioning for "hg log"
|
|
|
_("files:"),
|
|
|
# i18n: column positioning for "hg log"
|
|
|
_("files+:"),
|
|
|
# i18n: column positioning for "hg log"
|
|
|
_("files-:")], files):
|
|
|
if value:
|
|
|
self.ui.write("%-12s %s\n" % (key, " ".join(value)),
|
|
|
label='ui.debug log.files')
|
|
|
elif ctx.files() and self.ui.verbose:
|
|
|
# i18n: column positioning for "hg log"
|
|
|
self.ui.write(_("files: %s\n") % " ".join(ctx.files()),
|
|
|
label='ui.note log.files')
|
|
|
if copies and self.ui.verbose:
|
|
|
copies = ['%s (%s)' % c for c in copies]
|
|
|
# i18n: column positioning for "hg log"
|
|
|
self.ui.write(_("copies: %s\n") % ' '.join(copies),
|
|
|
label='ui.note log.copies')
|
|
|
|
|
|
extra = ctx.extra()
|
|
|
if extra and self.ui.debugflag:
|
|
|
for key, value in sorted(extra.items()):
|
|
|
# i18n: column positioning for "hg log"
|
|
|
self.ui.write(_("extra: %s=%s\n")
|
|
|
% (key, value.encode('string_escape')),
|
|
|
label='ui.debug log.extra')
|
|
|
|
|
|
description = ctx.description().strip()
|
|
|
if description:
|
|
|
if self.ui.verbose:
|
|
|
self.ui.write(_("description:\n"),
|
|
|
label='ui.note log.description')
|
|
|
self.ui.write(description,
|
|
|
label='ui.note log.description')
|
|
|
self.ui.write("\n\n")
|
|
|
else:
|
|
|
# i18n: column positioning for "hg log"
|
|
|
self.ui.write(_("summary: %s\n") %
|
|
|
description.splitlines()[0],
|
|
|
label='log.summary')
|
|
|
self.ui.write("\n")
|
|
|
|
|
|
self.showpatch(changenode, matchfn)
|
|
|
|
|
|
def showpatch(self, node, matchfn):
|
|
|
if not matchfn:
|
|
|
matchfn = self.patch
|
|
|
if matchfn:
|
|
|
stat = self.diffopts.get('stat')
|
|
|
diff = self.diffopts.get('patch')
|
|
|
diffopts = patch.diffopts(self.ui, self.diffopts)
|
|
|
prev = self.repo.changelog.parents(node)[0]
|
|
|
if stat:
|
|
|
diffordiffstat(self.ui, self.repo, diffopts, prev, node,
|
|
|
match=matchfn, stat=True)
|
|
|
if diff:
|
|
|
if stat:
|
|
|
self.ui.write("\n")
|
|
|
diffordiffstat(self.ui, self.repo, diffopts, prev, node,
|
|
|
match=matchfn, stat=False)
|
|
|
self.ui.write("\n")
|
|
|
|
|
|
def _meaningful_parentrevs(self, log, rev):
|
|
|
"""Return list of meaningful (or all if debug) parentrevs for rev.
|
|
|
|
|
|
For merges (two non-nullrev revisions) both parents are meaningful.
|
|
|
Otherwise the first parent revision is considered meaningful if it
|
|
|
is not the preceding revision.
|
|
|
"""
|
|
|
parents = log.parentrevs(rev)
|
|
|
if not self.ui.debugflag and parents[1] == nullrev:
|
|
|
if parents[0] >= rev - 1:
|
|
|
parents = []
|
|
|
else:
|
|
|
parents = [parents[0]]
|
|
|
return parents
|
|
|
|
|
|
|
|
|
class changeset_templater(changeset_printer):
|
|
|
'''format changeset information.'''
|
|
|
|
|
|
def __init__(self, ui, repo, patch, diffopts, mapfile, buffered):
|
|
|
changeset_printer.__init__(self, ui, repo, patch, diffopts, buffered)
|
|
|
formatnode = ui.debugflag and (lambda x: x) or (lambda x: x[:12])
|
|
|
defaulttempl = {
|
|
|
'parent': '{rev}:{node|formatnode} ',
|
|
|
'manifest': '{rev}:{node|formatnode}',
|
|
|
'file_copy': '{name} ({source})',
|
|
|
'extra': '{key}={value|stringescape}'
|
|
|
}
|
|
|
# filecopy is preserved for compatibility reasons
|
|
|
defaulttempl['filecopy'] = defaulttempl['file_copy']
|
|
|
self.t = templater.templater(mapfile, {'formatnode': formatnode},
|
|
|
cache=defaulttempl)
|
|
|
self.cache = {}
|
|
|
|
|
|
def use_template(self, t):
|
|
|
'''set template string to use'''
|
|
|
self.t.cache['changeset'] = t
|
|
|
|
|
|
def _meaningful_parentrevs(self, ctx):
|
|
|
"""Return list of meaningful (or all if debug) parentrevs for rev.
|
|
|
"""
|
|
|
parents = ctx.parents()
|
|
|
if len(parents) > 1:
|
|
|
return parents
|
|
|
if self.ui.debugflag:
|
|
|
return [parents[0], self.repo['null']]
|
|
|
if parents[0].rev() >= ctx.rev() - 1:
|
|
|
return []
|
|
|
return parents
|
|
|
|
|
|
def _show(self, ctx, copies, matchfn, props):
|
|
|
'''show a single changeset or file revision'''
|
|
|
|
|
|
showlist = templatekw.showlist
|
|
|
|
|
|
# showparents() behaviour depends on ui trace level which
|
|
|
# causes unexpected behaviours at templating level and makes
|
|
|
# it harder to extract it in a standalone function. Its
|
|
|
# behaviour cannot be changed so leave it here for now.
|
|
|
def showparents(**args):
|
|
|
ctx = args['ctx']
|
|
|
parents = [[('rev', p.rev()), ('node', p.hex())]
|
|
|
for p in self._meaningful_parentrevs(ctx)]
|
|
|
return showlist('parent', parents, **args)
|
|
|
|
|
|
props = props.copy()
|
|
|
props.update(templatekw.keywords)
|
|
|
props['parents'] = showparents
|
|
|
props['templ'] = self.t
|
|
|
props['ctx'] = ctx
|
|
|
props['repo'] = self.repo
|
|
|
props['revcache'] = {'copies': copies}
|
|
|
props['cache'] = self.cache
|
|
|
|
|
|
# find correct templates for current mode
|
|
|
|
|
|
tmplmodes = [
|
|
|
(True, None),
|
|
|
(self.ui.verbose, 'verbose'),
|
|
|
(self.ui.quiet, 'quiet'),
|
|
|
(self.ui.debugflag, 'debug'),
|
|
|
]
|
|
|
|
|
|
types = {'header': '', 'footer':'', 'changeset': 'changeset'}
|
|
|
for mode, postfix in tmplmodes:
|
|
|
for type in types:
|
|
|
cur = postfix and ('%s_%s' % (type, postfix)) or type
|
|
|
if mode and cur in self.t:
|
|
|
types[type] = cur
|
|
|
|
|
|
try:
|
|
|
|
|
|
# write header
|
|
|
if types['header']:
|
|
|
h = templater.stringify(self.t(types['header'], **props))
|
|
|
if self.buffered:
|
|
|
self.header[ctx.rev()] = h
|
|
|
else:
|
|
|
if self.lastheader != h:
|
|
|
self.lastheader = h
|
|
|
self.ui.write(h)
|
|
|
|
|
|
# write changeset metadata, then patch if requested
|
|
|
key = types['changeset']
|
|
|
self.ui.write(templater.stringify(self.t(key, **props)))
|
|
|
self.showpatch(ctx.node(), matchfn)
|
|
|
|
|
|
if types['footer']:
|
|
|
if not self.footer:
|
|
|
self.footer = templater.stringify(self.t(types['footer'],
|
|
|
**props))
|
|
|
|
|
|
except KeyError, inst:
|
|
|
msg = _("%s: no key named '%s'")
|
|
|
raise util.Abort(msg % (self.t.mapfile, inst.args[0]))
|
|
|
except SyntaxError, inst:
|
|
|
raise util.Abort('%s: %s' % (self.t.mapfile, inst.args[0]))
|
|
|
|
|
|
def show_changeset(ui, repo, opts, buffered=False):
|
|
|
"""show one changeset using template or regular display.
|
|
|
|
|
|
Display format will be the first non-empty hit of:
|
|
|
1. option 'template'
|
|
|
2. option 'style'
|
|
|
3. [ui] setting 'logtemplate'
|
|
|
4. [ui] setting 'style'
|
|
|
If all of these values are either the unset or the empty string,
|
|
|
regular display via changeset_printer() is done.
|
|
|
"""
|
|
|
# options
|
|
|
patch = None
|
|
|
if opts.get('patch') or opts.get('stat'):
|
|
|
patch = scmutil.matchall(repo)
|
|
|
|
|
|
tmpl = opts.get('template')
|
|
|
style = None
|
|
|
if not tmpl:
|
|
|
style = opts.get('style')
|
|
|
|
|
|
# ui settings
|
|
|
if not (tmpl or style):
|
|
|
tmpl = ui.config('ui', 'logtemplate')
|
|
|
if tmpl:
|
|
|
try:
|
|
|
tmpl = templater.parsestring(tmpl)
|
|
|
except SyntaxError:
|
|
|
tmpl = templater.parsestring(tmpl, quoted=False)
|
|
|
else:
|
|
|
style = util.expandpath(ui.config('ui', 'style', ''))
|
|
|
|
|
|
if not (tmpl or style):
|
|
|
return changeset_printer(ui, repo, patch, opts, buffered)
|
|
|
|
|
|
mapfile = None
|
|
|
if style and not tmpl:
|
|
|
mapfile = style
|
|
|
if not os.path.split(mapfile)[0]:
|
|
|
mapname = (templater.templatepath('map-cmdline.' + mapfile)
|
|
|
or templater.templatepath(mapfile))
|
|
|
if mapname:
|
|
|
mapfile = mapname
|
|
|
|
|
|
try:
|
|
|
t = changeset_templater(ui, repo, patch, opts, mapfile, buffered)
|
|
|
except SyntaxError, inst:
|
|
|
raise util.Abort(inst.args[0])
|
|
|
if tmpl:
|
|
|
t.use_template(tmpl)
|
|
|
return t
|
|
|
|
|
|
def finddate(ui, repo, date):
|
|
|
"""Find the tipmost changeset that matches the given date spec"""
|
|
|
|
|
|
df = util.matchdate(date)
|
|
|
m = scmutil.matchall(repo)
|
|
|
results = {}
|
|
|
|
|
|
def prep(ctx, fns):
|
|
|
d = ctx.date()
|
|
|
if df(d[0]):
|
|
|
results[ctx.rev()] = d
|
|
|
|
|
|
for ctx in walkchangerevs(repo, m, {'rev': None}, prep):
|
|
|
rev = ctx.rev()
|
|
|
if rev in results:
|
|
|
ui.status(_("found revision %s from %s\n") %
|
|
|
(rev, util.datestr(results[rev])))
|
|
|
return str(rev)
|
|
|
|
|
|
raise util.Abort(_("revision matching date not found"))
|
|
|
|
|
|
def increasingwindows(start, end, windowsize=8, sizelimit=512):
|
|
|
if start < end:
|
|
|
while start < end:
|
|
|
yield start, min(windowsize, end - start)
|
|
|
start += windowsize
|
|
|
if windowsize < sizelimit:
|
|
|
windowsize *= 2
|
|
|
else:
|
|
|
while start > end:
|
|
|
yield start, min(windowsize, start - end - 1)
|
|
|
start -= windowsize
|
|
|
if windowsize < sizelimit:
|
|
|
windowsize *= 2
|
|
|
|
|
|
class FileWalkError(Exception):
|
|
|
pass
|
|
|
|
|
|
def walkfilerevs(repo, match, follow, revs, fncache):
|
|
|
'''Walks the file history for the matched files.
|
|
|
|
|
|
Returns the changeset revs that are involved in the file history.
|
|
|
|
|
|
Throws FileWalkError if the file history can't be walked using
|
|
|
filelogs alone.
|
|
|
'''
|
|
|
wanted = set()
|
|
|
copies = []
|
|
|
minrev, maxrev = min(revs), max(revs)
|
|
|
def filerevgen(filelog, last):
|
|
|
"""
|
|
|
Only files, no patterns. Check the history of each file.
|
|
|
|
|
|
Examines filelog entries within minrev, maxrev linkrev range
|
|
|
Returns an iterator yielding (linkrev, parentlinkrevs, copied)
|
|
|
tuples in backwards order
|
|
|
"""
|
|
|
cl_count = len(repo)
|
|
|
revs = []
|
|
|
for j in xrange(0, last + 1):
|
|
|
linkrev = filelog.linkrev(j)
|
|
|
if linkrev < minrev:
|
|
|
continue
|
|
|
# only yield rev for which we have the changelog, it can
|
|
|
# happen while doing "hg log" during a pull or commit
|
|
|
if linkrev >= cl_count:
|
|
|
break
|
|
|
|
|
|
parentlinkrevs = []
|
|
|
for p in filelog.parentrevs(j):
|
|
|
if p != nullrev:
|
|
|
parentlinkrevs.append(filelog.linkrev(p))
|
|
|
n = filelog.node(j)
|
|
|
revs.append((linkrev, parentlinkrevs,
|
|
|
follow and filelog.renamed(n)))
|
|
|
|
|
|
return reversed(revs)
|
|
|
def iterfiles():
|
|
|
pctx = repo['.']
|
|
|
for filename in match.files():
|
|
|
if follow:
|
|
|
if filename not in pctx:
|
|
|
raise util.Abort(_('cannot follow file not in parent '
|
|
|
'revision: "%s"') % filename)
|
|
|
yield filename, pctx[filename].filenode()
|
|
|
else:
|
|
|
yield filename, None
|
|
|
for filename_node in copies:
|
|
|
yield filename_node
|
|
|
|
|
|
for file_, node in iterfiles():
|
|
|
filelog = repo.file(file_)
|
|
|
if not len(filelog):
|
|
|
if node is None:
|
|
|
# A zero count may be a directory or deleted file, so
|
|
|
# try to find matching entries on the slow path.
|
|
|
if follow:
|
|
|
raise util.Abort(
|
|
|
_('cannot follow nonexistent file: "%s"') % file_)
|
|
|
raise FileWalkError("Cannot walk via filelog")
|
|
|
else:
|
|
|
continue
|
|
|
|
|
|
if node is None:
|
|
|
last = len(filelog) - 1
|
|
|
else:
|
|
|
last = filelog.rev(node)
|
|
|
|
|
|
|
|
|
# keep track of all ancestors of the file
|
|
|
ancestors = set([filelog.linkrev(last)])
|
|
|
|
|
|
# iterate from latest to oldest revision
|
|
|
for rev, flparentlinkrevs, copied in filerevgen(filelog, last):
|
|
|
if not follow:
|
|
|
if rev > maxrev:
|
|
|
continue
|
|
|
else:
|
|
|
# Note that last might not be the first interesting
|
|
|
# rev to us:
|
|
|
# if the file has been changed after maxrev, we'll
|
|
|
# have linkrev(last) > maxrev, and we still need
|
|
|
# to explore the file graph
|
|
|
if rev not in ancestors:
|
|
|
continue
|
|
|
# XXX insert 1327 fix here
|
|
|
if flparentlinkrevs:
|
|
|
ancestors.update(flparentlinkrevs)
|
|
|
|
|
|
fncache.setdefault(rev, []).append(file_)
|
|
|
wanted.add(rev)
|
|
|
if copied:
|
|
|
copies.append(copied)
|
|
|
|
|
|
return wanted
|
|
|
|
|
|
def walkchangerevs(repo, match, opts, prepare):
|
|
|
'''Iterate over files and the revs in which they changed.
|
|
|
|
|
|
Callers most commonly need to iterate backwards over the history
|
|
|
in which they are interested. Doing so has awful (quadratic-looking)
|
|
|
performance, so we use iterators in a "windowed" way.
|
|
|
|
|
|
We walk a window of revisions in the desired order. Within the
|
|
|
window, we first walk forwards to gather data, then in the desired
|
|
|
order (usually backwards) to display it.
|
|
|
|
|
|
This function returns an iterator yielding contexts. Before
|
|
|
yielding each context, the iterator will first call the prepare
|
|
|
function on each context in the window in forward order.'''
|
|
|
|
|
|
follow = opts.get('follow') or opts.get('follow_first')
|
|
|
|
|
|
if opts.get('rev'):
|
|
|
revs = scmutil.revrange(repo, opts.get('rev'))
|
|
|
elif follow:
|
|
|
revs = repo.revs('reverse(:.)')
|
|
|
else:
|
|
|
revs = list(repo)
|
|
|
revs.reverse()
|
|
|
if not revs:
|
|
|
return []
|
|
|
wanted = set()
|
|
|
slowpath = match.anypats() or (match.files() and opts.get('removed'))
|
|
|
fncache = {}
|
|
|
change = repo.changectx
|
|
|
|
|
|
# First step is to fill wanted, the set of revisions that we want to yield.
|
|
|
# When it does not induce extra cost, we also fill fncache for revisions in
|
|
|
# wanted: a cache of filenames that were changed (ctx.files()) and that
|
|
|
# match the file filtering conditions.
|
|
|
|
|
|
if not slowpath and not match.files():
|
|
|
# No files, no patterns. Display all revs.
|
|
|
wanted = set(revs)
|
|
|
|
|
|
if not slowpath and match.files():
|
|
|
# We only have to read through the filelog to find wanted revisions
|
|
|
|
|
|
try:
|
|
|
wanted = walkfilerevs(repo, match, follow, revs, fncache)
|
|
|
except FileWalkError:
|
|
|
slowpath = True
|
|
|
|
|
|
# We decided to fall back to the slowpath because at least one
|
|
|
# of the paths was not a file. Check to see if at least one of them
|
|
|
# existed in history, otherwise simply return
|
|
|
for path in match.files():
|
|
|
if path == '.' or path in repo.store:
|
|
|
break
|
|
|
else:
|
|
|
return []
|
|
|
|
|
|
if slowpath:
|
|
|
# We have to read the changelog to match filenames against
|
|
|
# changed files
|
|
|
|
|
|
if follow:
|
|
|
raise util.Abort(_('can only follow copies/renames for explicit '
|
|
|
'filenames'))
|
|
|
|
|
|
# The slow path checks files modified in every changeset.
|
|
|
# This is really slow on large repos, so compute the set lazily.
|
|
|
class lazywantedset(object):
|
|
|
def __init__(self):
|
|
|
self.set = set()
|
|
|
self.revs = set(revs)
|
|
|
|
|
|
# No need to worry about locality here because it will be accessed
|
|
|
# in the same order as the increasing window below.
|
|
|
def __contains__(self, value):
|
|
|
if value in self.set:
|
|
|
return True
|
|
|
elif not value in self.revs:
|
|
|
return False
|
|
|
else:
|
|
|
self.revs.discard(value)
|
|
|
ctx = change(value)
|
|
|
matches = filter(match, ctx.files())
|
|
|
if matches:
|
|
|
fncache[value] = matches
|
|
|
self.set.add(value)
|
|
|
return True
|
|
|
return False
|
|
|
|
|
|
def discard(self, value):
|
|
|
self.revs.discard(value)
|
|
|
self.set.discard(value)
|
|
|
|
|
|
wanted = lazywantedset()
|
|
|
|
|
|
class followfilter(object):
|
|
|
def __init__(self, onlyfirst=False):
|
|
|
self.startrev = nullrev
|
|
|
self.roots = set()
|
|
|
self.onlyfirst = onlyfirst
|
|
|
|
|
|
def match(self, rev):
|
|
|
def realparents(rev):
|
|
|
if self.onlyfirst:
|
|
|
return repo.changelog.parentrevs(rev)[0:1]
|
|
|
else:
|
|
|
return filter(lambda x: x != nullrev,
|
|
|
repo.changelog.parentrevs(rev))
|
|
|
|
|
|
if self.startrev == nullrev:
|
|
|
self.startrev = rev
|
|
|
return True
|
|
|
|
|
|
if rev > self.startrev:
|
|
|
# forward: all descendants
|
|
|
if not self.roots:
|
|
|
self.roots.add(self.startrev)
|
|
|
for parent in realparents(rev):
|
|
|
if parent in self.roots:
|
|
|
self.roots.add(rev)
|
|
|
return True
|
|
|
else:
|
|
|
# backwards: all parents
|
|
|
if not self.roots:
|
|
|
self.roots.update(realparents(self.startrev))
|
|
|
if rev in self.roots:
|
|
|
self.roots.remove(rev)
|
|
|
self.roots.update(realparents(rev))
|
|
|
return True
|
|
|
|
|
|
return False
|
|
|
|
|
|
# it might be worthwhile to do this in the iterator if the rev range
|
|
|
# is descending and the prune args are all within that range
|
|
|
for rev in opts.get('prune', ()):
|
|
|
rev = repo[rev].rev()
|
|
|
ff = followfilter()
|
|
|
stop = min(revs[0], revs[-1])
|
|
|
for x in xrange(rev, stop - 1, -1):
|
|
|
if ff.match(x):
|
|
|
wanted.discard(x)
|
|
|
|
|
|
# Choose a small initial window if we will probably only visit a
|
|
|
# few commits.
|
|
|
limit = loglimit(opts)
|
|
|
windowsize = 8
|
|
|
if limit:
|
|
|
windowsize = min(limit, windowsize)
|
|
|
|
|
|
# Now that wanted is correctly initialized, we can iterate over the
|
|
|
# revision range, yielding only revisions in wanted.
|
|
|
def iterate():
|
|
|
if follow and not match.files():
|
|
|
ff = followfilter(onlyfirst=opts.get('follow_first'))
|
|
|
def want(rev):
|
|
|
return ff.match(rev) and rev in wanted
|
|
|
else:
|
|
|
def want(rev):
|
|
|
return rev in wanted
|
|
|
|
|
|
for i, window in increasingwindows(0, len(revs), windowsize):
|
|
|
nrevs = [rev for rev in revs[i:i + window] if want(rev)]
|
|
|
for rev in sorted(nrevs):
|
|
|
fns = fncache.get(rev)
|
|
|
ctx = change(rev)
|
|
|
if not fns:
|
|
|
def fns_generator():
|
|
|
for f in ctx.files():
|
|
|
if match(f):
|
|
|
yield f
|
|
|
fns = fns_generator()
|
|
|
prepare(ctx, fns)
|
|
|
for rev in nrevs:
|
|
|
yield change(rev)
|
|
|
return iterate()
|
|
|
|
|
|
def _makegraphfilematcher(repo, pats, followfirst):
|
|
|
# When displaying a revision with --patch --follow FILE, we have
|
|
|
# to know which file of the revision must be diffed. With
|
|
|
# --follow, we want the names of the ancestors of FILE in the
|
|
|
# revision, stored in "fcache". "fcache" is populated by
|
|
|
# reproducing the graph traversal already done by --follow revset
|
|
|
# and relating linkrevs to file names (which is not "correct" but
|
|
|
# good enough).
|
|
|
fcache = {}
|
|
|
fcacheready = [False]
|
|
|
pctx = repo['.']
|
|
|
wctx = repo[None]
|
|
|
|
|
|
def populate():
|
|
|
for fn in pats:
|
|
|
for i in ((pctx[fn],), pctx[fn].ancestors(followfirst=followfirst)):
|
|
|
for c in i:
|
|
|
fcache.setdefault(c.linkrev(), set()).add(c.path())
|
|
|
|
|
|
def filematcher(rev):
|
|
|
if not fcacheready[0]:
|
|
|
# Lazy initialization
|
|
|
fcacheready[0] = True
|
|
|
populate()
|
|
|
return scmutil.match(wctx, fcache.get(rev, []), default='path')
|
|
|
|
|
|
return filematcher
|
|
|
|
|
|
def _makegraphlogrevset(repo, pats, opts, revs):
|
|
|
"""Return (expr, filematcher) where expr is a revset string built
|
|
|
from log options and file patterns or None. If --stat or --patch
|
|
|
are not passed filematcher is None. Otherwise it is a callable
|
|
|
taking a revision number and returning a match objects filtering
|
|
|
the files to be detailed when displaying the revision.
|
|
|
"""
|
|
|
opt2revset = {
|
|
|
'no_merges': ('not merge()', None),
|
|
|
'only_merges': ('merge()', None),
|
|
|
'_ancestors': ('ancestors(%(val)s)', None),
|
|
|
'_fancestors': ('_firstancestors(%(val)s)', None),
|
|
|
'_descendants': ('descendants(%(val)s)', None),
|
|
|
'_fdescendants': ('_firstdescendants(%(val)s)', None),
|
|
|
'_matchfiles': ('_matchfiles(%(val)s)', None),
|
|
|
'date': ('date(%(val)r)', None),
|
|
|
'branch': ('branch(%(val)r)', ' or '),
|
|
|
'_patslog': ('filelog(%(val)r)', ' or '),
|
|
|
'_patsfollow': ('follow(%(val)r)', ' or '),
|
|
|
'_patsfollowfirst': ('_followfirst(%(val)r)', ' or '),
|
|
|
'keyword': ('keyword(%(val)r)', ' or '),
|
|
|
'prune': ('not (%(val)r or ancestors(%(val)r))', ' and '),
|
|
|
'user': ('user(%(val)r)', ' or '),
|
|
|
}
|
|
|
|
|
|
opts = dict(opts)
|
|
|
# follow or not follow?
|
|
|
follow = opts.get('follow') or opts.get('follow_first')
|
|
|
followfirst = opts.get('follow_first') and 1 or 0
|
|
|
# --follow with FILE behaviour depends on revs...
|
|
|
startrev = revs[0]
|
|
|
followdescendants = (len(revs) > 1 and revs[0] < revs[1]) and 1 or 0
|
|
|
|
|
|
# branch and only_branch are really aliases and must be handled at
|
|
|
# the same time
|
|
|
opts['branch'] = opts.get('branch', []) + opts.get('only_branch', [])
|
|
|
opts['branch'] = [repo.lookupbranch(b) for b in opts['branch']]
|
|
|
# pats/include/exclude are passed to match.match() directly in
|
|
|
# _matchfiles() revset but walkchangerevs() builds its matcher with
|
|
|
# scmutil.match(). The difference is input pats are globbed on
|
|
|
# platforms without shell expansion (windows).
|
|
|
pctx = repo[None]
|
|
|
match, pats = scmutil.matchandpats(pctx, pats, opts)
|
|
|
slowpath = match.anypats() or (match.files() and opts.get('removed'))
|
|
|
if not slowpath:
|
|
|
for f in match.files():
|
|
|
if follow and f not in pctx:
|
|
|
raise util.Abort(_('cannot follow file not in parent '
|
|
|
'revision: "%s"') % f)
|
|
|
filelog = repo.file(f)
|
|
|
if not filelog:
|
|
|
# A zero count may be a directory or deleted file, so
|
|
|
# try to find matching entries on the slow path.
|
|
|
if follow:
|
|
|
raise util.Abort(
|
|
|
_('cannot follow nonexistent file: "%s"') % f)
|
|
|
slowpath = True
|
|
|
|
|
|
# We decided to fall back to the slowpath because at least one
|
|
|
# of the paths was not a file. Check to see if at least one of them
|
|
|
# existed in history - in that case, we'll continue down the
|
|
|
# slowpath; otherwise, we can turn off the slowpath
|
|
|
if slowpath:
|
|
|
for path in match.files():
|
|
|
if path == '.' or path in repo.store:
|
|
|
break
|
|
|
else:
|
|
|
slowpath = False
|
|
|
|
|
|
if slowpath:
|
|
|
# See walkchangerevs() slow path.
|
|
|
#
|
|
|
if follow:
|
|
|
raise util.Abort(_('can only follow copies/renames for explicit '
|
|
|
'filenames'))
|
|
|
# pats/include/exclude cannot be represented as separate
|
|
|
# revset expressions as their filtering logic applies at file
|
|
|
# level. For instance "-I a -X a" matches a revision touching
|
|
|
# "a" and "b" while "file(a) and not file(b)" does
|
|
|
# not. Besides, filesets are evaluated against the working
|
|
|
# directory.
|
|
|
matchargs = ['r:', 'd:relpath']
|
|
|
for p in pats:
|
|
|
matchargs.append('p:' + p)
|
|
|
for p in opts.get('include', []):
|
|
|
matchargs.append('i:' + p)
|
|
|
for p in opts.get('exclude', []):
|
|
|
matchargs.append('x:' + p)
|
|
|
matchargs = ','.join(('%r' % p) for p in matchargs)
|
|
|
opts['_matchfiles'] = matchargs
|
|
|
else:
|
|
|
if follow:
|
|
|
fpats = ('_patsfollow', '_patsfollowfirst')
|
|
|
fnopats = (('_ancestors', '_fancestors'),
|
|
|
('_descendants', '_fdescendants'))
|
|
|
if pats:
|
|
|
# follow() revset interprets its file argument as a
|
|
|
# manifest entry, so use match.files(), not pats.
|
|
|
opts[fpats[followfirst]] = list(match.files())
|
|
|
else:
|
|
|
opts[fnopats[followdescendants][followfirst]] = str(startrev)
|
|
|
else:
|
|
|
opts['_patslog'] = list(pats)
|
|
|
|
|
|
filematcher = None
|
|
|
if opts.get('patch') or opts.get('stat'):
|
|
|
if follow:
|
|
|
filematcher = _makegraphfilematcher(repo, pats, followfirst)
|
|
|
else:
|
|
|
filematcher = lambda rev: match
|
|
|
|
|
|
expr = []
|
|
|
for op, val in opts.iteritems():
|
|
|
if not val:
|
|
|
continue
|
|
|
if op not in opt2revset:
|
|
|
continue
|
|
|
revop, andor = opt2revset[op]
|
|
|
if '%(val)' not in revop:
|
|
|
expr.append(revop)
|
|
|
else:
|
|
|
if not isinstance(val, list):
|
|
|
e = revop % {'val': val}
|
|
|
else:
|
|
|
e = '(' + andor.join((revop % {'val': v}) for v in val) + ')'
|
|
|
expr.append(e)
|
|
|
|
|
|
if expr:
|
|
|
expr = '(' + ' and '.join(expr) + ')'
|
|
|
else:
|
|
|
expr = None
|
|
|
return expr, filematcher
|
|
|
|
|
|
def getgraphlogrevs(repo, pats, opts):
|
|
|
"""Return (revs, expr, filematcher) where revs is an iterable of
|
|
|
revision numbers, expr is a revset string built from log options
|
|
|
and file patterns or None, and used to filter 'revs'. If --stat or
|
|
|
--patch are not passed filematcher is None. Otherwise it is a
|
|
|
callable taking a revision number and returning a match objects
|
|
|
filtering the files to be detailed when displaying the revision.
|
|
|
"""
|
|
|
if not len(repo):
|
|
|
return [], None, None
|
|
|
limit = loglimit(opts)
|
|
|
# Default --rev value depends on --follow but --follow behaviour
|
|
|
# depends on revisions resolved from --rev...
|
|
|
follow = opts.get('follow') or opts.get('follow_first')
|
|
|
possiblyunsorted = False # whether revs might need sorting
|
|
|
if opts.get('rev'):
|
|
|
revs = scmutil.revrange(repo, opts['rev'])
|
|
|
# Don't sort here because _makegraphlogrevset might depend on the
|
|
|
# order of revs
|
|
|
possiblyunsorted = True
|
|
|
else:
|
|
|
if follow and len(repo) > 0:
|
|
|
revs = repo.revs('reverse(:.)')
|
|
|
else:
|
|
|
revs = list(repo.changelog)
|
|
|
revs.reverse()
|
|
|
if not revs:
|
|
|
return [], None, None
|
|
|
expr, filematcher = _makegraphlogrevset(repo, pats, opts, revs)
|
|
|
if possiblyunsorted:
|
|
|
revs.sort(reverse=True)
|
|
|
if expr:
|
|
|
# Revset matchers often operate faster on revisions in changelog
|
|
|
# order, because most filters deal with the changelog.
|
|
|
revs.reverse()
|
|
|
matcher = revset.match(repo.ui, expr)
|
|
|
# Revset matches can reorder revisions. "A or B" typically returns
|
|
|
# returns the revision matching A then the revision matching B. Sort
|
|
|
# again to fix that.
|
|
|
revs = matcher(repo, revs)
|
|
|
revs.sort(reverse=True)
|
|
|
if limit is not None:
|
|
|
revs = revs[:limit]
|
|
|
|
|
|
return revs, expr, filematcher
|
|
|
|
|
|
def displaygraph(ui, dag, displayer, showparents, edgefn, getrenamed=None,
|
|
|
filematcher=None):
|
|
|
seen, state = [], graphmod.asciistate()
|
|
|
for rev, type, ctx, parents in dag:
|
|
|
char = 'o'
|
|
|
if ctx.node() in showparents:
|
|
|
char = '@'
|
|
|
elif ctx.obsolete():
|
|
|
char = 'x'
|
|
|
copies = None
|
|
|
if getrenamed and ctx.rev():
|
|
|
copies = []
|
|
|
for fn in ctx.files():
|
|
|
rename = getrenamed(fn, ctx.rev())
|
|
|
if rename:
|
|
|
copies.append((fn, rename[0]))
|
|
|
revmatchfn = None
|
|
|
if filematcher is not None:
|
|
|
revmatchfn = filematcher(ctx.rev())
|
|
|
displayer.show(ctx, copies=copies, matchfn=revmatchfn)
|
|
|
lines = displayer.hunk.pop(rev).split('\n')
|
|
|
if not lines[-1]:
|
|
|
del lines[-1]
|
|
|
displayer.flush(rev)
|
|
|
edges = edgefn(type, char, lines, seen, rev, parents)
|
|
|
for type, char, lines, coldata in edges:
|
|
|
graphmod.ascii(ui, state, type, char, lines, coldata)
|
|
|
displayer.close()
|
|
|
|
|
|
def graphlog(ui, repo, *pats, **opts):
|
|
|
# Parameters are identical to log command ones
|
|
|
revs, expr, filematcher = getgraphlogrevs(repo, pats, opts)
|
|
|
revdag = graphmod.dagwalker(repo, revs)
|
|
|
|
|
|
getrenamed = None
|
|
|
if opts.get('copies'):
|
|
|
endrev = None
|
|
|
if opts.get('rev'):
|
|
|
endrev = max(scmutil.revrange(repo, opts.get('rev'))) + 1
|
|
|
getrenamed = templatekw.getrenamedfn(repo, endrev=endrev)
|
|
|
displayer = show_changeset(ui, repo, opts, buffered=True)
|
|
|
showparents = [ctx.node() for ctx in repo[None].parents()]
|
|
|
displaygraph(ui, revdag, displayer, showparents,
|
|
|
graphmod.asciiedges, getrenamed, filematcher)
|
|
|
|
|
|
def checkunsupportedgraphflags(pats, opts):
|
|
|
for op in ["newest_first"]:
|
|
|
if op in opts and opts[op]:
|
|
|
raise util.Abort(_("-G/--graph option is incompatible with --%s")
|
|
|
% op.replace("_", "-"))
|
|
|
|
|
|
def graphrevs(repo, nodes, opts):
|
|
|
limit = loglimit(opts)
|
|
|
nodes.reverse()
|
|
|
if limit is not None:
|
|
|
nodes = nodes[:limit]
|
|
|
return graphmod.nodes(repo, nodes)
|
|
|
|
|
|
def add(ui, repo, match, dryrun, listsubrepos, prefix, explicitonly):
|
|
|
join = lambda f: os.path.join(prefix, f)
|
|
|
bad = []
|
|
|
oldbad = match.bad
|
|
|
match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
|
|
|
names = []
|
|
|
wctx = repo[None]
|
|
|
cca = None
|
|
|
abort, warn = scmutil.checkportabilityalert(ui)
|
|
|
if abort or warn:
|
|
|
cca = scmutil.casecollisionauditor(ui, abort, repo.dirstate)
|
|
|
for f in repo.walk(match):
|
|
|
exact = match.exact(f)
|
|
|
if exact or not explicitonly and f not in repo.dirstate:
|
|
|
if cca:
|
|
|
cca(f)
|
|
|
names.append(f)
|
|
|
if ui.verbose or not exact:
|
|
|
ui.status(_('adding %s\n') % match.rel(join(f)))
|
|
|
|
|
|
for subpath in sorted(wctx.substate):
|
|
|
sub = wctx.sub(subpath)
|
|
|
try:
|
|
|
submatch = matchmod.narrowmatcher(subpath, match)
|
|
|
if listsubrepos:
|
|
|
bad.extend(sub.add(ui, submatch, dryrun, listsubrepos, prefix,
|
|
|
False))
|
|
|
else:
|
|
|
bad.extend(sub.add(ui, submatch, dryrun, listsubrepos, prefix,
|
|
|
True))
|
|
|
except error.LookupError:
|
|
|
ui.status(_("skipping missing subrepository: %s\n")
|
|
|
% join(subpath))
|
|
|
|
|
|
if not dryrun:
|
|
|
rejected = wctx.add(names, prefix)
|
|
|
bad.extend(f for f in rejected if f in match.files())
|
|
|
return bad
|
|
|
|
|
|
def forget(ui, repo, match, prefix, explicitonly):
|
|
|
join = lambda f: os.path.join(prefix, f)
|
|
|
bad = []
|
|
|
oldbad = match.bad
|
|
|
match.bad = lambda x, y: bad.append(x) or oldbad(x, y)
|
|
|
wctx = repo[None]
|
|
|
forgot = []
|
|
|
s = repo.status(match=match, clean=True)
|
|
|
forget = sorted(s[0] + s[1] + s[3] + s[6])
|
|
|
if explicitonly:
|
|
|
forget = [f for f in forget if match.exact(f)]
|
|
|
|
|
|
for subpath in sorted(wctx.substate):
|
|
|
sub = wctx.sub(subpath)
|
|
|
try:
|
|
|
submatch = matchmod.narrowmatcher(subpath, match)
|
|
|
subbad, subforgot = sub.forget(ui, submatch, prefix)
|
|
|
bad.extend([subpath + '/' + f for f in subbad])
|
|
|
forgot.extend([subpath + '/' + f for f in subforgot])
|
|
|
except error.LookupError:
|
|
|
ui.status(_("skipping missing subrepository: %s\n")
|
|
|
% join(subpath))
|
|
|
|
|
|
if not explicitonly:
|
|
|
for f in match.files():
|
|
|
if f not in repo.dirstate and not os.path.isdir(match.rel(join(f))):
|
|
|
if f not in forgot:
|
|
|
if os.path.exists(match.rel(join(f))):
|
|
|
ui.warn(_('not removing %s: '
|
|
|
'file is already untracked\n')
|
|
|
% match.rel(join(f)))
|
|
|
bad.append(f)
|
|
|
|
|
|
for f in forget:
|
|
|
if ui.verbose or not match.exact(f):
|
|
|
ui.status(_('removing %s\n') % match.rel(join(f)))
|
|
|
|
|
|
rejected = wctx.forget(forget, prefix)
|
|
|
bad.extend(f for f in rejected if f in match.files())
|
|
|
forgot.extend(forget)
|
|
|
return bad, forgot
|
|
|
|
|
|
def duplicatecopies(repo, rev, fromrev):
|
|
|
'''reproduce copies from fromrev to rev in the dirstate'''
|
|
|
for dst, src in copies.pathcopies(repo[fromrev], repo[rev]).iteritems():
|
|
|
# copies.pathcopies returns backward renames, so dst might not
|
|
|
# actually be in the dirstate
|
|
|
if repo.dirstate[dst] in "nma":
|
|
|
repo.dirstate.copy(src, dst)
|
|
|
|
|
|
def commit(ui, repo, commitfunc, pats, opts):
|
|
|
'''commit the specified files or all outstanding changes'''
|
|
|
date = opts.get('date')
|
|
|
if date:
|
|
|
opts['date'] = util.parsedate(date)
|
|
|
message = logmessage(ui, opts)
|
|
|
|
|
|
# extract addremove carefully -- this function can be called from a command
|
|
|
# that doesn't support addremove
|
|
|
if opts.get('addremove'):
|
|
|
scmutil.addremove(repo, pats, opts)
|
|
|
|
|
|
return commitfunc(ui, repo, message,
|
|
|
scmutil.match(repo[None], pats, opts), opts)
|
|
|
|
|
|
def amend(ui, repo, commitfunc, old, extra, pats, opts):
|
|
|
ui.note(_('amending changeset %s\n') % old)
|
|
|
base = old.p1()
|
|
|
|
|
|
wlock = lock = newid = None
|
|
|
try:
|
|
|
wlock = repo.wlock()
|
|
|
lock = repo.lock()
|
|
|
tr = repo.transaction('amend')
|
|
|
try:
|
|
|
# See if we got a message from -m or -l, if not, open the editor
|
|
|
# with the message of the changeset to amend
|
|
|
message = logmessage(ui, opts)
|
|
|
# ensure logfile does not conflict with later enforcement of the
|
|
|
# message. potential logfile content has been processed by
|
|
|
# `logmessage` anyway.
|
|
|
opts.pop('logfile')
|
|
|
# First, do a regular commit to record all changes in the working
|
|
|
# directory (if there are any)
|
|
|
ui.callhooks = False
|
|
|
currentbookmark = repo._bookmarkcurrent
|
|
|
try:
|
|
|
repo._bookmarkcurrent = None
|
|
|
opts['message'] = 'temporary amend commit for %s' % old
|
|
|
node = commit(ui, repo, commitfunc, pats, opts)
|
|
|
finally:
|
|
|
repo._bookmarkcurrent = currentbookmark
|
|
|
ui.callhooks = True
|
|
|
ctx = repo[node]
|
|
|
|
|
|
# Participating changesets:
|
|
|
#
|
|
|
# node/ctx o - new (intermediate) commit that contains changes
|
|
|
# | from working dir to go into amending commit
|
|
|
# | (or a workingctx if there were no changes)
|
|
|
# |
|
|
|
# old o - changeset to amend
|
|
|
# |
|
|
|
# base o - parent of amending changeset
|
|
|
|
|
|
# Update extra dict from amended commit (e.g. to preserve graft
|
|
|
# source)
|
|
|
extra.update(old.extra())
|
|
|
|
|
|
# Also update it from the intermediate commit or from the wctx
|
|
|
extra.update(ctx.extra())
|
|
|
|
|
|
if len(old.parents()) > 1:
|
|
|
# ctx.files() isn't reliable for merges, so fall back to the
|
|
|
# slower repo.status() method
|
|
|
files = set([fn for st in repo.status(base, old)[:3]
|
|
|
for fn in st])
|
|
|
else:
|
|
|
files = set(old.files())
|
|
|
|
|
|
# Second, we use either the commit we just did, or if there were no
|
|
|
# changes the parent of the working directory as the version of the
|
|
|
# files in the final amend commit
|
|
|
if node:
|
|
|
ui.note(_('copying changeset %s to %s\n') % (ctx, base))
|
|
|
|
|
|
user = ctx.user()
|
|
|
date = ctx.date()
|
|
|
# Recompute copies (avoid recording a -> b -> a)
|
|
|
copied = copies.pathcopies(base, ctx)
|
|
|
|
|
|
# Prune files which were reverted by the updates: if old
|
|
|
# introduced file X and our intermediate commit, node,
|
|
|
# renamed that file, then those two files are the same and
|
|
|
# we can discard X from our list of files. Likewise if X
|
|
|
# was deleted, it's no longer relevant
|
|
|
files.update(ctx.files())
|
|
|
|
|
|
def samefile(f):
|
|
|
if f in ctx.manifest():
|
|
|
a = ctx.filectx(f)
|
|
|
if f in base.manifest():
|
|
|
b = base.filectx(f)
|
|
|
return (not a.cmp(b)
|
|
|
and a.flags() == b.flags())
|
|
|
else:
|
|
|
return False
|
|
|
else:
|
|
|
return f not in base.manifest()
|
|
|
files = [f for f in files if not samefile(f)]
|
|
|
|
|
|
def filectxfn(repo, ctx_, path):
|
|
|
try:
|
|
|
fctx = ctx[path]
|
|
|
flags = fctx.flags()
|
|
|
mctx = context.memfilectx(fctx.path(), fctx.data(),
|
|
|
islink='l' in flags,
|
|
|
isexec='x' in flags,
|
|
|
copied=copied.get(path))
|
|
|
return mctx
|
|
|
except KeyError:
|
|
|
raise IOError
|
|
|
else:
|
|
|
ui.note(_('copying changeset %s to %s\n') % (old, base))
|
|
|
|
|
|
# Use version of files as in the old cset
|
|
|
def filectxfn(repo, ctx_, path):
|
|
|
try:
|
|
|
return old.filectx(path)
|
|
|
except KeyError:
|
|
|
raise IOError
|
|
|
|
|
|
user = opts.get('user') or old.user()
|
|
|
date = opts.get('date') or old.date()
|
|
|
editmsg = False
|
|
|
if not message:
|
|
|
editmsg = True
|
|
|
message = old.description()
|
|
|
|
|
|
pureextra = extra.copy()
|
|
|
extra['amend_source'] = old.hex()
|
|
|
|
|
|
new = context.memctx(repo,
|
|
|
parents=[base.node(), old.p2().node()],
|
|
|
text=message,
|
|
|
files=files,
|
|
|
filectxfn=filectxfn,
|
|
|
user=user,
|
|
|
date=date,
|
|
|
extra=extra)
|
|
|
if editmsg:
|
|
|
new._text = commitforceeditor(repo, new, [])
|
|
|
|
|
|
newdesc = changelog.stripdesc(new.description())
|
|
|
if ((not node)
|
|
|
and newdesc == old.description()
|
|
|
and user == old.user()
|
|
|
and date == old.date()
|
|
|
and pureextra == old.extra()):
|
|
|
# nothing changed. continuing here would create a new node
|
|
|
# anyway because of the amend_source noise.
|
|
|
#
|
|
|
# This not what we expect from amend.
|
|
|
return old.node()
|
|
|
|
|
|
ph = repo.ui.config('phases', 'new-commit', phases.draft)
|
|
|
try:
|
|
|
repo.ui.setconfig('phases', 'new-commit', old.phase())
|
|
|
newid = repo.commitctx(new)
|
|
|
finally:
|
|
|
repo.ui.setconfig('phases', 'new-commit', ph)
|
|
|
if newid != old.node():
|
|
|
# Reroute the working copy parent to the new changeset
|
|
|
repo.setparents(newid, nullid)
|
|
|
|
|
|
# Move bookmarks from old parent to amend commit
|
|
|
bms = repo.nodebookmarks(old.node())
|
|
|
if bms:
|
|
|
marks = repo._bookmarks
|
|
|
for bm in bms:
|
|
|
marks[bm] = newid
|
|
|
marks.write()
|
|
|
#commit the whole amend process
|
|
|
if obsolete._enabled and newid != old.node():
|
|
|
# mark the new changeset as successor of the rewritten one
|
|
|
new = repo[newid]
|
|
|
obs = [(old, (new,))]
|
|
|
if node:
|
|
|
obs.append((ctx, ()))
|
|
|
|
|
|
obsolete.createmarkers(repo, obs)
|
|
|
tr.close()
|
|
|
finally:
|
|
|
tr.release()
|
|
|
if (not obsolete._enabled) and newid != old.node():
|
|
|
# Strip the intermediate commit (if there was one) and the amended
|
|
|
# commit
|
|
|
if node:
|
|
|
ui.note(_('stripping intermediate changeset %s\n') % ctx)
|
|
|
ui.note(_('stripping amended changeset %s\n') % old)
|
|
|
repair.strip(ui, repo, old.node(), topic='amend-backup')
|
|
|
finally:
|
|
|
if newid is None:
|
|
|
repo.dirstate.invalidate()
|
|
|
lockmod.release(lock, wlock)
|
|
|
return newid
|
|
|
|
|
|
def commiteditor(repo, ctx, subs):
|
|
|
if ctx.description():
|
|
|
return ctx.description()
|
|
|
return commitforceeditor(repo, ctx, subs)
|
|
|
|
|
|
def commitforceeditor(repo, ctx, subs):
|
|
|
edittext = []
|
|
|
modified, added, removed = ctx.modified(), ctx.added(), ctx.removed()
|
|
|
if ctx.description():
|
|
|
edittext.append(ctx.description())
|
|
|
edittext.append("")
|
|
|
edittext.append("") # Empty line between message and comments.
|
|
|
edittext.append(_("HG: Enter commit message."
|
|
|
" Lines beginning with 'HG:' are removed."))
|
|
|
edittext.append(_("HG: Leave message empty to abort commit."))
|
|
|
edittext.append("HG: --")
|
|
|
edittext.append(_("HG: user: %s") % ctx.user())
|
|
|
if ctx.p2():
|
|
|
edittext.append(_("HG: branch merge"))
|
|
|
if ctx.branch():
|
|
|
edittext.append(_("HG: branch '%s'") % ctx.branch())
|
|
|
if bookmarks.iscurrent(repo):
|
|
|
edittext.append(_("HG: bookmark '%s'") % repo._bookmarkcurrent)
|
|
|
edittext.extend([_("HG: subrepo %s") % s for s in subs])
|
|
|
edittext.extend([_("HG: added %s") % f for f in added])
|
|
|
edittext.extend([_("HG: changed %s") % f for f in modified])
|
|
|
edittext.extend([_("HG: removed %s") % f for f in removed])
|
|
|
if not added and not modified and not removed:
|
|
|
edittext.append(_("HG: no files changed"))
|
|
|
edittext.append("")
|
|
|
# run editor in the repository root
|
|
|
olddir = os.getcwd()
|
|
|
os.chdir(repo.root)
|
|
|
text = repo.ui.edit("\n".join(edittext), ctx.user())
|
|
|
text = re.sub("(?m)^HG:.*(\n|$)", "", text)
|
|
|
os.chdir(olddir)
|
|
|
|
|
|
if not text.strip():
|
|
|
raise util.Abort(_("empty commit message"))
|
|
|
|
|
|
return text
|
|
|
|
|
|
def commitstatus(repo, node, branch, bheads=None, opts={}):
|
|
|
ctx = repo[node]
|
|
|
parents = ctx.parents()
|
|
|
|
|
|
if (not opts.get('amend') and bheads and node not in bheads and not
|
|
|
[x for x in parents if x.node() in bheads and x.branch() == branch]):
|
|
|
repo.ui.status(_('created new head\n'))
|
|
|
# The message is not printed for initial roots. For the other
|
|
|
# changesets, it is printed in the following situations:
|
|
|
#
|
|
|
# Par column: for the 2 parents with ...
|
|
|
# N: null or no parent
|
|
|
# B: parent is on another named branch
|
|
|
# C: parent is a regular non head changeset
|
|
|
# H: parent was a branch head of the current branch
|
|
|
# Msg column: whether we print "created new head" message
|
|
|
# In the following, it is assumed that there already exists some
|
|
|
# initial branch heads of the current branch, otherwise nothing is
|
|
|
# printed anyway.
|
|
|
#
|
|
|
# Par Msg Comment
|
|
|
# N N y additional topo root
|
|
|
#
|
|
|
# B N y additional branch root
|
|
|
# C N y additional topo head
|
|
|
# H N n usual case
|
|
|
#
|
|
|
# B B y weird additional branch root
|
|
|
# C B y branch merge
|
|
|
# H B n merge with named branch
|
|
|
#
|
|
|
# C C y additional head from merge
|
|
|
# C H n merge with a head
|
|
|
#
|
|
|
# H H n head merge: head count decreases
|
|
|
|
|
|
if not opts.get('close_branch'):
|
|
|
for r in parents:
|
|
|
if r.closesbranch() and r.branch() == branch:
|
|
|
repo.ui.status(_('reopening closed branch head %d\n') % r)
|
|
|
|
|
|
if repo.ui.debugflag:
|
|
|
repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx.hex()))
|
|
|
elif repo.ui.verbose:
|
|
|
repo.ui.write(_('committed changeset %d:%s\n') % (int(ctx), ctx))
|
|
|
|
|
|
def revert(ui, repo, ctx, parents, *pats, **opts):
|
|
|
parent, p2 = parents
|
|
|
node = ctx.node()
|
|
|
|
|
|
mf = ctx.manifest()
|
|
|
if node == parent:
|
|
|
pmf = mf
|
|
|
else:
|
|
|
pmf = None
|
|
|
|
|
|
# need all matching names in dirstate and manifest of target rev,
|
|
|
# so have to walk both. do not print errors if files exist in one
|
|
|
# but not other.
|
|
|
|
|
|
names = {}
|
|
|
|
|
|
wlock = repo.wlock()
|
|
|
try:
|
|
|
# walk dirstate.
|
|
|
|
|
|
m = scmutil.match(repo[None], pats, opts)
|
|
|
m.bad = lambda x, y: False
|
|
|
for abs in repo.walk(m):
|
|
|
names[abs] = m.rel(abs), m.exact(abs)
|
|
|
|
|
|
# walk target manifest.
|
|
|
|
|
|
def badfn(path, msg):
|
|
|
if path in names:
|
|
|
return
|
|
|
if path in ctx.substate:
|
|
|
return
|
|
|
path_ = path + '/'
|
|
|
for f in names:
|
|
|
if f.startswith(path_):
|
|
|
return
|
|
|
ui.warn("%s: %s\n" % (m.rel(path), msg))
|
|
|
|
|
|
m = scmutil.match(ctx, pats, opts)
|
|
|
m.bad = badfn
|
|
|
for abs in ctx.walk(m):
|
|
|
if abs not in names:
|
|
|
names[abs] = m.rel(abs), m.exact(abs)
|
|
|
|
|
|
# get the list of subrepos that must be reverted
|
|
|
targetsubs = sorted(s for s in ctx.substate if m(s))
|
|
|
m = scmutil.matchfiles(repo, names)
|
|
|
changes = repo.status(match=m)[:4]
|
|
|
modified, added, removed, deleted = map(set, changes)
|
|
|
|
|
|
# if f is a rename, also revert the source
|
|
|
cwd = repo.getcwd()
|
|
|
for f in added:
|
|
|
src = repo.dirstate.copied(f)
|
|
|
if src and src not in names and repo.dirstate[src] == 'r':
|
|
|
removed.add(src)
|
|
|
names[src] = (repo.pathto(src, cwd), True)
|
|
|
|
|
|
def removeforget(abs):
|
|
|
if repo.dirstate[abs] == 'a':
|
|
|
return _('forgetting %s\n')
|
|
|
return _('removing %s\n')
|
|
|
|
|
|
revert = ([], _('reverting %s\n'))
|
|
|
add = ([], _('adding %s\n'))
|
|
|
remove = ([], removeforget)
|
|
|
undelete = ([], _('undeleting %s\n'))
|
|
|
|
|
|
disptable = (
|
|
|
# dispatch table:
|
|
|
# file state
|
|
|
# action if in target manifest
|
|
|
# action if not in target manifest
|
|
|
# make backup if in target manifest
|
|
|
# make backup if not in target manifest
|
|
|
(modified, revert, remove, True, True),
|
|
|
(added, revert, remove, True, False),
|
|
|
(removed, undelete, None, True, False),
|
|
|
(deleted, revert, remove, False, False),
|
|
|
)
|
|
|
|
|
|
for abs, (rel, exact) in sorted(names.items()):
|
|
|
mfentry = mf.get(abs)
|
|
|
target = repo.wjoin(abs)
|
|
|
def handle(xlist, dobackup):
|
|
|
xlist[0].append(abs)
|
|
|
if (dobackup and not opts.get('no_backup') and
|
|
|
os.path.lexists(target) and
|
|
|
abs in ctx and repo[None][abs].cmp(ctx[abs])):
|
|
|
bakname = "%s.orig" % rel
|
|
|
ui.note(_('saving current version of %s as %s\n') %
|
|
|
(rel, bakname))
|
|
|
if not opts.get('dry_run'):
|
|
|
util.rename(target, bakname)
|
|
|
if ui.verbose or not exact:
|
|
|
msg = xlist[1]
|
|
|
if not isinstance(msg, basestring):
|
|
|
msg = msg(abs)
|
|
|
ui.status(msg % rel)
|
|
|
for table, hitlist, misslist, backuphit, backupmiss in disptable:
|
|
|
if abs not in table:
|
|
|
continue
|
|
|
# file has changed in dirstate
|
|
|
if mfentry:
|
|
|
handle(hitlist, backuphit)
|
|
|
elif misslist is not None:
|
|
|
handle(misslist, backupmiss)
|
|
|
break
|
|
|
else:
|
|
|
if abs not in repo.dirstate:
|
|
|
if mfentry:
|
|
|
handle(add, True)
|
|
|
elif exact:
|
|
|
ui.warn(_('file not managed: %s\n') % rel)
|
|
|
continue
|
|
|
# file has not changed in dirstate
|
|
|
if node == parent:
|
|
|
if exact:
|
|
|
ui.warn(_('no changes needed to %s\n') % rel)
|
|
|
continue
|
|
|
if pmf is None:
|
|
|
# only need parent manifest in this unlikely case,
|
|
|
# so do not read by default
|
|
|
pmf = repo[parent].manifest()
|
|
|
if abs in pmf and mfentry:
|
|
|
# if version of file is same in parent and target
|
|
|
# manifests, do nothing
|
|
|
if (pmf[abs] != mfentry or
|
|
|
pmf.flags(abs) != mf.flags(abs)):
|
|
|
handle(revert, False)
|
|
|
else:
|
|
|
handle(remove, False)
|
|
|
|
|
|
if not opts.get('dry_run'):
|
|
|
def checkout(f):
|
|
|
fc = ctx[f]
|
|
|
repo.wwrite(f, fc.data(), fc.flags())
|
|
|
|
|
|
audit_path = scmutil.pathauditor(repo.root)
|
|
|
for f in remove[0]:
|
|
|
if repo.dirstate[f] == 'a':
|
|
|
repo.dirstate.drop(f)
|
|
|
continue
|
|
|
audit_path(f)
|
|
|
try:
|
|
|
util.unlinkpath(repo.wjoin(f))
|
|
|
except OSError:
|
|
|
pass
|
|
|
repo.dirstate.remove(f)
|
|
|
|
|
|
normal = None
|
|
|
if node == parent:
|
|
|
# We're reverting to our parent. If possible, we'd like status
|
|
|
# to report the file as clean. We have to use normallookup for
|
|
|
# merges to avoid losing information about merged/dirty files.
|
|
|
if p2 != nullid:
|
|
|
normal = repo.dirstate.normallookup
|
|
|
else:
|
|
|
normal = repo.dirstate.normal
|
|
|
for f in revert[0]:
|
|
|
checkout(f)
|
|
|
if normal:
|
|
|
normal(f)
|
|
|
|
|
|
for f in add[0]:
|
|
|
checkout(f)
|
|
|
repo.dirstate.add(f)
|
|
|
|
|
|
normal = repo.dirstate.normallookup
|
|
|
if node == parent and p2 == nullid:
|
|
|
normal = repo.dirstate.normal
|
|
|
for f in undelete[0]:
|
|
|
checkout(f)
|
|
|
normal(f)
|
|
|
|
|
|
copied = copies.pathcopies(repo[parent], ctx)
|
|
|
|
|
|
for f in add[0] + undelete[0] + revert[0]:
|
|
|
if f in copied:
|
|
|
repo.dirstate.copy(copied[f], f)
|
|
|
|
|
|
if targetsubs:
|
|
|
# Revert the subrepos on the revert list
|
|
|
for sub in targetsubs:
|
|
|
ctx.sub(sub).revert(ui, ctx.substate[sub], *pats, **opts)
|
|
|
finally:
|
|
|
wlock.release()
|
|
|
|
|
|
def command(table):
|
|
|
'''returns a function object bound to table which can be used as
|
|
|
a decorator for populating table as a command table'''
|
|
|
|
|
|
def cmd(name, options=(), synopsis=None):
|
|
|
def decorator(func):
|
|
|
if synopsis:
|
|
|
table[name] = func, list(options), synopsis
|
|
|
else:
|
|
|
table[name] = func, list(options)
|
|
|
return func
|
|
|
return decorator
|
|
|
|
|
|
return cmd
|
|
|
|
|
|
# a list of (ui, repo) functions called by commands.summary
|
|
|
summaryhooks = util.hooks()
|
|
|
|
|
|
# A list of state files kept by multistep operations like graft.
|
|
|
# Since graft cannot be aborted, it is considered 'clearable' by update.
|
|
|
# note: bisect is intentionally excluded
|
|
|
# (state file, clearable, allowcommit, error, hint)
|
|
|
unfinishedstates = [
|
|
|
('graftstate', True, False, _('graft in progress'),
|
|
|
_("use 'hg graft --continue' or 'hg update' to abort")),
|
|
|
('updatestate', True, False, _('last update was interrupted'),
|
|
|
_("use 'hg update' to get a consistent checkout"))
|
|
|
]
|
|
|
|
|
|
def checkunfinished(repo, commit=False):
|
|
|
'''Look for an unfinished multistep operation, like graft, and abort
|
|
|
if found. It's probably good to check this right before
|
|
|
bailifchanged().
|
|
|
'''
|
|
|
for f, clearable, allowcommit, msg, hint in unfinishedstates:
|
|
|
if commit and allowcommit:
|
|
|
continue
|
|
|
if repo.vfs.exists(f):
|
|
|
raise util.Abort(msg, hint=hint)
|
|
|
|
|
|
def clearunfinished(repo):
|
|
|
'''Check for unfinished operations (as above), and clear the ones
|
|
|
that are clearable.
|
|
|
'''
|
|
|
for f, clearable, allowcommit, msg, hint in unfinishedstates:
|
|
|
if not clearable and repo.vfs.exists(f):
|
|
|
raise util.Abort(msg, hint=hint)
|
|
|
for f, clearable, allowcommit, msg, hint in unfinishedstates:
|
|
|
if clearable and repo.vfs.exists(f):
|
|
|
util.unlink(repo.join(f))
|
|
|
|