# cmdutil.py - help for command processing in mercurial # # Copyright 2005-2007 Matt Mackall # # 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, glob, tempfile, time import mdiff, bdiff, util, templater, patch, error, encoding, templatekw import match as _match revrangesep = ':' 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 = {} for e in table.keys(): aliases = e.lstrip("^").split("|") 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 bail_if_changed(repo): if repo.dirstate.parents()[1] != 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(_("outstanding uncommitted changes")) def logmessage(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 = sys.stdin.read() else: message = open(logfile).read() 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 remoteui(src, opts): 'build a remote ui from ui or repo and opts' if hasattr(src, 'baseui'): # looks like a repository dst = src.baseui.copy() # drop repo-specific config src = src.ui # copy target options from repo else: # assume it's a global ui object dst = src.copy() # keep all global options # copy ssh-specific options for o in 'ssh', 'remotecmd': v = opts.get(o) or src.config('ui', o) if v: dst.setconfig("ui", o, v) # copy bundle-specific options r = src.config('bundle', 'mainreporoot') if r: dst.setconfig('bundle', 'mainreporoot', r) # copy auth section settings for key, val in src.configitems('auth'): dst.setconfig('auth', key, val) return dst def revpair(repo, revs): '''return pair of nodes, given list of revisions. second item can be None, meaning use working dir.''' def revfix(repo, val, defval): if not val and val != 0 and defval is not None: val = defval return repo.lookup(val) if not revs: return repo.dirstate.parents()[0], None end = None if len(revs) == 1: if revrangesep in revs[0]: start, end = revs[0].split(revrangesep, 1) start = revfix(repo, start, 0) end = revfix(repo, end, len(repo) - 1) else: start = revfix(repo, revs[0], None) elif len(revs) == 2: if revrangesep in revs[0] or revrangesep in revs[1]: raise util.Abort(_('too many revisions specified')) start = revfix(repo, revs[0], None) end = revfix(repo, revs[1], None) else: raise util.Abort(_('too many revisions specified')) return start, end def revrange(repo, revs): """Yield revision as strings from a list of revision specifications.""" def revfix(repo, val, defval): if not val and val != 0 and defval is not None: return defval return repo.changelog.rev(repo.lookup(val)) seen, l = set(), [] for spec in revs: if revrangesep in spec: start, end = spec.split(revrangesep, 1) start = revfix(repo, start, 0) end = revfix(repo, end, len(repo) - 1) step = start > end and -1 or 1 for rev in xrange(start, end+step, step): if rev in seen: continue seen.add(rev) l.append(rev) else: rev = revfix(repo, spec, None) if rev in seen: continue seen.add(rev) l.append(rev) return l def make_filename(repo, pat, node, 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), } 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 make_file(repo, pat, node=None, total=None, seqno=None, revwidth=None, mode='wb', pathname=None): writable = 'w' in mode or 'a' in mode if not pat or pat == '-': return writable and sys.stdout or sys.stdin if hasattr(pat, 'write') and writable: return pat if hasattr(pat, 'read') and 'r' in mode: return pat return open(make_filename(repo, pat, node, total, seqno, revwidth, pathname), mode) def expandpats(pats): if not util.expandglobs: return list(pats) ret = [] for p in pats: kind, name = _match._patsplit(p, None) if kind is None: try: globbed = glob.glob(name) except re.error: globbed = [name] if globbed: ret.extend(globbed) continue ret.append(p) return ret def match(repo, pats=[], opts={}, globbed=False, default='relpath'): if not globbed and default == 'relpath': pats = expandpats(pats or []) m = _match.match(repo.root, repo.getcwd(), pats, opts.get('include'), opts.get('exclude'), default) def badfn(f, msg): repo.ui.warn("%s: %s\n" % (m.rel(f), msg)) m.bad = badfn return m def matchall(repo): return _match.always(repo.root, repo.getcwd()) def matchfiles(repo, files): return _match.exact(repo.root, repo.getcwd(), files) def findrenames(repo, added, removed, threshold): '''find renamed files -- yields (before, after, score) tuples''' copies = {} ctx = repo['.'] for r in removed: if r not in ctx: continue fctx = ctx.filectx(r) def score(text): if not len(text): return 0.0 if not fctx.cmp(text): return 1.0 if threshold == 1.0: return 0.0 orig = fctx.data() # bdiff.blocks() returns blocks of matching lines # count the number of bytes in each equal = 0 alines = mdiff.splitnewlines(text) matches = bdiff.blocks(text, orig) for x1, x2, y1, y2 in matches: for line in alines[x1:x2]: equal += len(line) lengths = len(text) + len(orig) return equal * 2.0 / lengths for a in added: bestscore = copies.get(a, (None, threshold))[1] myscore = score(repo.wread(a)) if myscore >= bestscore: copies[a] = (r, myscore) for dest, v in copies.iteritems(): source, score = v yield source, dest, score def addremove(repo, pats=[], opts={}, dry_run=None, similarity=None): if dry_run is None: dry_run = opts.get('dry_run') if similarity is None: similarity = float(opts.get('similarity') or 0) # we'd use status here, except handling of symlinks and ignore is tricky added, unknown, deleted, removed = [], [], [], [] audit_path = util.path_auditor(repo.root) m = match(repo, pats, opts) for abs in repo.walk(m): target = repo.wjoin(abs) good = True try: audit_path(abs) except: good = False rel = m.rel(abs) exact = m.exact(abs) if good and abs not in repo.dirstate: unknown.append(abs) if repo.ui.verbose or not exact: repo.ui.status(_('adding %s\n') % ((pats and rel) or abs)) elif repo.dirstate[abs] != 'r' and (not good or not util.lexists(target) or (os.path.isdir(target) and not os.path.islink(target))): deleted.append(abs) if repo.ui.verbose or not exact: repo.ui.status(_('removing %s\n') % ((pats and rel) or abs)) # for finding renames elif repo.dirstate[abs] == 'r': removed.append(abs) elif repo.dirstate[abs] == 'a': added.append(abs) if not dry_run: repo.remove(deleted) repo.add(unknown) if similarity > 0: for old, new, score in findrenames(repo, added + unknown, removed + deleted, similarity): if repo.ui.verbose or not m.exact(old) or not m.exact(new): repo.ui.status(_('recording removal of %s as rename to %s ' '(%d%% similar)\n') % (m.rel(old), m.rel(new), score * 100)) if not dry_run: repo.copy(old, new) 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") def walkpat(pat): srcs = [] m = match(repo, [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 '?r': 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 = util.canonpath(repo.root, cwd, otarget) reltarget = repo.pathto(abstarget, cwd) target = repo.wjoin(abstarget) src = repo.wjoin(abssrc) state = repo.dirstate[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.exists(target) 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: 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) util.copyfile(src, target) except IOError, inst: if inst.errno == errno.ENOENT: ui.warn(_('%s: deleted in working copy\n') % relsrc) 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 origsrc = repo.dirstate.copied(abssrc) or abssrc if abstarget == origsrc: # copying back a copy? if state not in 'mn' and not dryrun: repo.dirstate.normallookup(abstarget) else: if repo.dirstate[origsrc] == 'a' and origsrc == abssrc: if not ui.quiet: ui.warn(_("%s has not been committed yet, so no copy " "data will be stored for %s.\n") % (repo.pathto(origsrc, cwd), reltarget)) if repo.dirstate[abstarget] in '?r' and not dryrun: repo.add([abstarget]) elif not dryrun: repo.copy(origsrc, abstarget) if rename and not dryrun: repo.remove([abssrc], not after) # 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 = util.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 _match.patkind(pat): # a mercurial pattern res = lambda p: os.path.join(dest, os.path.basename(util.localpath(p))) else: abspfx = util.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.exists(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 = 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 _match.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 def service(opts, parentfn=None, initfn=None, runfn=None, logfile=None, runargs=None, appendpid=False): '''Run a command as a service.''' 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 pid = util.spawndetached(runargs) while os.path.exists(lockpath): time.sleep(0.1) 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 opts['pid_file']: mode = appendpid and 'a' or 'w' fp = open(opts['pid_file'], mode) fp.write(str(os.getpid()) + '\n') fp.close() 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(util.nulldev, 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() 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, **props): if self.buffered: self.ui.pushbuffer() self._show(ctx, copies, props) self.hunk[ctx.rev()] = self.ui.popbuffer() else: self._show(ctx, copies, props) def _show(self, ctx, copies, 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))) 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)] self.ui.write(_("changeset: %d:%s\n") % (rev, hexfunc(changenode))) branch = ctx.branch() # don't show the default branch name if branch != 'default': branch = encoding.tolocal(branch) self.ui.write(_("branch: %s\n") % branch) for tag in self.repo.nodetags(changenode): self.ui.write(_("tag: %s\n") % tag) for parent in parents: self.ui.write(_("parent: %d:%s\n") % parent) if self.ui.debugflag: mnode = ctx.manifestnode() self.ui.write(_("manifest: %d:%s\n") % (self.repo.manifest.rev(mnode), hex(mnode))) self.ui.write(_("user: %s\n") % ctx.user()) self.ui.write(_("date: %s\n") % date) if self.ui.debugflag: files = self.repo.status(log.parents(changenode)[0], changenode)[:3] for key, value in zip([_("files:"), _("files+:"), _("files-:")], files): if value: self.ui.write("%-12s %s\n" % (key, " ".join(value))) elif ctx.files() and self.ui.verbose: self.ui.write(_("files: %s\n") % " ".join(ctx.files())) if copies and self.ui.verbose: copies = ['%s (%s)' % c for c in copies] self.ui.write(_("copies: %s\n") % ' '.join(copies)) extra = ctx.extra() if extra and self.ui.debugflag: for key, value in sorted(extra.items()): self.ui.write(_("extra: %s=%s\n") % (key, value.encode('string_escape'))) description = ctx.description().strip() if description: if self.ui.verbose: self.ui.write(_("description:\n")) self.ui.write(description) self.ui.write("\n\n") else: self.ui.write(_("summary: %s\n") % description.splitlines()[0]) self.ui.write("\n") self.showpatch(changenode) def showpatch(self, node): if self.patch: prev = self.repo.changelog.parents(node)[0] chunks = patch.diff(self.repo, prev, node, match=self.patch, opts=patch.diffopts(self.ui, self.diffopts)) for chunk in chunks: self.ui.write(chunk) 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, 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: 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()) 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, matchfn=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 = False if opts.get('patch'): patch = matchfn or matchall(repo) tmpl = opts.get('template') style = None if tmpl: tmpl = templater.parsestring(tmpl, quoted=False) else: style = opts.get('style') # ui settings if not (tmpl or style): tmpl = ui.config('ui', 'logtemplate') if tmpl: tmpl = templater.parsestring(tmpl) 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 = 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 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.''' def increasing_windows(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 follow = opts.get('follow') or opts.get('follow_first') if not len(repo): return [] if follow: defrange = '%s:0' % repo['.'].rev() else: defrange = '-1:0' revs = revrange(repo, opts['rev'] or [defrange]) wanted = set() slowpath = match.anypats() or (match.files() and opts.get('removed')) fncache = {} change = util.cachefunc(repo.changectx) if not slowpath and not match.files(): # No files, no patterns. Display all revs. wanted = set(revs) copies = [] if not slowpath: # Only files, no patterns. Check the history of each file. def filerevgen(filelog, node): cl_count = len(repo) if node is None: last = len(filelog) - 1 else: last = filelog.rev(node) for i, window in increasing_windows(last, nullrev): revs = [] for j in xrange(i - window, i + 1): n = filelog.node(j) revs.append((filelog.linkrev(j), follow and filelog.renamed(n))) for rev in reversed(revs): # only yield rev for which we have the changelog, it can # happen while doing "hg log" during a pull or commit if rev[0] < cl_count: yield rev def iterfiles(): for filename in match.files(): yield filename, None for filename_node in copies: yield filename_node minrev, maxrev = min(revs), max(revs) 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_) slowpath = True break else: continue for rev, copied in filerevgen(filelog, node): if rev <= maxrev: if rev < minrev: break fncache.setdefault(rev, []) fncache[rev].append(file_) wanted.add(rev) if follow and copied: copies.append(copied) if slowpath: if follow: raise util.Abort(_('can only follow copies/renames for explicit ' 'filenames')) # The slow path checks files modified in every changeset. def changerevgen(): for i, window in increasing_windows(len(repo) - 1, nullrev): for j in xrange(i - window, i + 1): yield change(j) for ctx in changerevgen(): matches = filter(match, ctx.files()) if matches: fncache[ctx.rev()] = matches wanted.add(ctx.rev()) 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.changelog.rev(repo.lookup(rev)) ff = followfilter() stop = min(revs[0], revs[-1]) for x in xrange(rev, stop-1, -1): if ff.match(x): wanted.discard(x) 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 increasing_windows(0, len(revs)): change = util.cachefunc(repo.changectx) 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 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(opts) # extract addremove carefully -- this function can be called from a command # that doesn't support addremove if opts.get('addremove'): addremove(repo, pats, opts) return commitfunc(ui, repo, message, match(repo, pats, opts), opts) 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'") % encoding.tolocal(ctx.branch())) 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