Mercurial > public > mercurial-scm > hg-stable
diff mercurial/cmdutil.py @ 24803:e89f909edffa stable 3.4-rc
merge default into stable for 3.4 freeze
author | Matt Mackall <mpm@selenic.com> |
---|---|
date | Thu, 16 Apr 2015 20:57:51 -0500 |
parents | c560d8c68791 |
children | edf907bd8144 |
line wrap: on
line diff
--- a/mercurial/cmdutil.py Thu Apr 16 22:33:53 2015 +0900 +++ b/mercurial/cmdutil.py Thu Apr 16 20:57:51 2015 -0500 @@ -7,18 +7,207 @@ from node import hex, nullid, nullrev, short from i18n import _ -import os, sys, errno, re, tempfile +import os, sys, errno, re, tempfile, cStringIO, shutil import util, scmutil, templater, patch, error, templatekw, revlog, copies import match as matchmod import context, repair, graphmod, revset, phases, obsolete, pathutil import changelog import bookmarks import encoding +import crecord as crecordmod import lock as lockmod def parsealiases(cmd): return cmd.lstrip("^").split("|") +def setupwrapcolorwrite(ui): + # wrap ui.write so diff output can be labeled/colorized + def wrapwrite(orig, *args, **kw): + label = kw.pop('label', '') + for chunk, l in patch.difflabel(lambda: args): + orig(chunk, label=label + l) + + oldwrite = ui.write + def wrap(*args, **kwargs): + return wrapwrite(oldwrite, *args, **kwargs) + setattr(ui, 'write', wrap) + return oldwrite + +def filterchunks(ui, originalhunks, usecurses, testfile): + if usecurses: + if testfile: + recordfn = crecordmod.testdecorator(testfile, + crecordmod.testchunkselector) + else: + recordfn = crecordmod.chunkselector + + return crecordmod.filterpatch(ui, originalhunks, recordfn) + + else: + return patch.filterpatch(ui, originalhunks) + +def recordfilter(ui, originalhunks): + usecurses = ui.configbool('experimental', 'crecord', False) + testfile = ui.config('experimental', 'crecordtest', None) + oldwrite = setupwrapcolorwrite(ui) + try: + newchunks = filterchunks(ui, originalhunks, usecurses, testfile) + finally: + ui.write = oldwrite + return newchunks + +def dorecord(ui, repo, commitfunc, cmdsuggest, backupall, + filterfn, *pats, **opts): + import merge as mergemod + hunkclasses = (crecordmod.uihunk, patch.recordhunk) + ishunk = lambda x: isinstance(x, hunkclasses) + + if not ui.interactive(): + raise util.Abort(_('running non-interactively, use %s instead') % + cmdsuggest) + + # make sure username is set before going interactive + if not opts.get('user'): + ui.username() # raise exception, username not provided + + def recordfunc(ui, repo, message, match, opts): + """This is generic record driver. + + Its job is to interactively filter local changes, and + accordingly prepare working directory into a state in which the + job can be delegated to a non-interactive commit command such as + 'commit' or 'qrefresh'. + + After the actual job is done by non-interactive command, the + working directory is restored to its original state. + + In the end we'll record interesting changes, and everything else + will be left in place, so the user can continue working. + """ + + checkunfinished(repo, commit=True) + merge = len(repo[None].parents()) > 1 + if merge: + raise util.Abort(_('cannot partially commit a merge ' + '(use "hg commit" instead)')) + + status = repo.status(match=match) + diffopts = patch.difffeatureopts(ui, opts=opts, whitespace=True) + diffopts.nodates = True + diffopts.git = True + originaldiff = patch.diff(repo, changes=status, opts=diffopts) + originalchunks = patch.parsepatch(originaldiff) + + # 1. filter patch, so we have intending-to apply subset of it + try: + chunks = filterfn(ui, originalchunks) + except patch.PatchError, err: + raise util.Abort(_('error parsing patch: %s') % err) + + contenders = set() + for h in chunks: + try: + contenders.update(set(h.files())) + except AttributeError: + pass + + changed = status.modified + status.added + status.removed + newfiles = [f for f in changed if f in contenders] + if not newfiles: + ui.status(_('no changes to record\n')) + return 0 + + newandmodifiedfiles = set() + for h in chunks: + isnew = h.filename() in status.added + if ishunk(h) and isnew and not h in originalchunks: + newandmodifiedfiles.add(h.filename()) + + modified = set(status.modified) + + # 2. backup changed files, so we can restore them in the end + + if backupall: + tobackup = changed + else: + tobackup = [f for f in newfiles + if f in modified or f in newandmodifiedfiles] + + backups = {} + if tobackup: + backupdir = repo.join('record-backups') + try: + os.mkdir(backupdir) + except OSError, err: + if err.errno != errno.EEXIST: + raise + try: + # backup continues + for f in tobackup: + fd, tmpname = tempfile.mkstemp(prefix=f.replace('/', '_')+'.', + dir=backupdir) + os.close(fd) + ui.debug('backup %r as %r\n' % (f, tmpname)) + util.copyfile(repo.wjoin(f), tmpname) + shutil.copystat(repo.wjoin(f), tmpname) + backups[f] = tmpname + + fp = cStringIO.StringIO() + for c in chunks: + fname = c.filename() + if fname in backups or fname in newandmodifiedfiles: + c.write(fp) + dopatch = fp.tell() + fp.seek(0) + + [os.unlink(c) for c in newandmodifiedfiles] + + # 3a. apply filtered patch to clean repo (clean) + if backups: + # Equivalent to hg.revert + choices = lambda key: key in backups + mergemod.update(repo, repo.dirstate.p1(), + False, True, choices) + + # 3b. (apply) + if dopatch: + try: + ui.debug('applying patch\n') + ui.debug(fp.getvalue()) + patch.internalpatch(ui, repo, fp, 1, eolmode=None) + except patch.PatchError, err: + raise util.Abort(str(err)) + del fp + + # 4. We prepared working directory according to filtered + # patch. Now is the time to delegate the job to + # commit/qrefresh or the like! + + # Make all of the pathnames absolute. + newfiles = [repo.wjoin(nf) for nf in newfiles] + return commitfunc(ui, repo, *newfiles, **opts) + finally: + # 5. finally restore backed-up files + try: + for realname, tmpname in backups.iteritems(): + ui.debug('restoring %r to %r\n' % (tmpname, realname)) + util.copyfile(tmpname, repo.wjoin(realname)) + # Our calls to copystat() here and above are a + # hack to trick any editors that have f open that + # we haven't modified them. + # + # Also note that this racy as an editor could + # notice the file's mtime before we've finished + # writing it. + shutil.copystat(tmpname, repo.wjoin(realname)) + os.unlink(tmpname) + if tobackup: + os.rmdir(backupdir) + except OSError: + pass + + return commit(ui, repo, recordfunc, pats, opts) + def findpossible(cmd, table, strict=False): """ Return cmd -> (aliases, command table entry) @@ -34,8 +223,10 @@ else: keys = table.keys() + allcmds = [] for e in keys: aliases = parsealiases(e) + allcmds.extend(aliases) found = None if cmd in aliases: found = cmd @@ -53,11 +244,11 @@ if not choice and debugchoice: choice = debugchoice - return choice + return choice, allcmds def findcmd(cmd, table, strict=True): """Return (aliases, command table entry) for command string.""" - choice = findpossible(cmd, table, strict) + choice, allcmds = findpossible(cmd, table, strict) if cmd in choice: return choice[cmd] @@ -70,7 +261,7 @@ if choice: return choice.values()[0] - raise error.UnknownCommand(cmd) + raise error.UnknownCommand(cmd, allcmds) def findrepo(p): while not os.path.isdir(os.path.join(p, ".hg")): @@ -80,16 +271,15 @@ return p -def bailifchanged(repo): - if repo.dirstate.p2() != nullid: +def bailifchanged(repo, merge=True): + if merge and 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) + ctx.sub(s).bailifchanged() def logmessage(ui, opts): """ get the log message according to -m and -l option """ @@ -110,22 +300,22 @@ (logfile, inst.strerror)) return message -def mergeeditform(ctxorbool, baseform): - """build appropriate editform from ctxorbool and baseform - - 'ctxorbool' is one of a ctx to be committed, or a bool whether +def mergeeditform(ctxorbool, baseformname): + """return appropriate editform name (referencing a committemplate) + + 'ctxorbool' is either a ctx to be committed, or a bool indicating whether merging is committed. - This returns editform 'baseform' with '.merge' if merging is - committed, or one with '.normal' suffix otherwise. + This returns baseformname with '.merge' appended if it is a merge, + otherwise '.normal' is appended. """ if isinstance(ctxorbool, bool): if ctxorbool: - return baseform + ".merge" + return baseformname + ".merge" elif 1 < len(ctxorbool.parents()): - return baseform + ".merge" - - return baseform + ".normal" + return baseformname + ".merge" + + return baseformname + ".normal" def getcommiteditor(edit=False, finishdesc=None, extramsg=None, editform='', **opts): @@ -225,7 +415,10 @@ writable = mode not in ('r', 'rb') if not pat or pat == '-': - fp = writable and repo.ui.fout or repo.ui.fin + if writable: + fp = repo.ui.fout + else: + fp = repo.ui.fin if util.safehasattr(fp, 'fileno'): return os.fdopen(os.dup(fp.fileno()), mode) else: @@ -301,7 +494,10 @@ def walkpat(pat): srcs = [] - badstates = after and '?' or '?r' + if after: + badstates = '?' + else: + badstates = '?r' m = scmutil.match(repo[None], [pat], opts, globbed=True) for abs in repo.walk(m): state = repo.dirstate[abs] @@ -387,7 +583,7 @@ srcexists = True except IOError, inst: if inst.errno == errno.ENOENT: - ui.warn(_('%s: deleted in working copy\n') % relsrc) + ui.warn(_('%s: deleted in working directory\n') % relsrc) srcexists = False else: ui.warn(_('%s: cannot copy - %s\n') % @@ -476,7 +672,6 @@ res = lambda p: dest return res - pats = scmutil.expandpats(pats) if not pats: raise util.Abort(_('no source or destination specified')) @@ -520,7 +715,10 @@ def writepid(pid): if opts['pid_file']: - mode = appendpid and 'a' or 'w' + if appendpid: + mode = 'a' + else: + mode = 'w' fp = open(opts['pid_file'], mode) fp.write(str(pid) + '\n') fp.close() @@ -613,6 +811,7 @@ update = not opts.get('bypass') strip = opts["strip"] + prefix = opts["prefix"] sim = float(opts.get('similarity') or 0) if not tmpname: return (None, None, False) @@ -672,8 +871,8 @@ partial = opts.get('partial', False) files = set() try: - patch.patch(ui, repo, tmpname, strip=strip, files=files, - eolmode=None, similarity=sim / 100.0) + patch.patch(ui, repo, tmpname, strip=strip, prefix=prefix, + files=files, eolmode=None, similarity=sim / 100.0) except patch.PatchError, e: if not partial: raise util.Abort(str(e)) @@ -710,7 +909,7 @@ try: files = set() try: - patch.patchrepo(ui, repo, p1, store, tmpname, strip, + patch.patchrepo(ui, repo, p1, store, tmpname, strip, prefix, files, eolmode=None) except patch.PatchError, e: raise util.Abort(str(e)) @@ -755,7 +954,11 @@ branch = ctx.branch() if switch_parent: parents.reverse() - prev = (parents and parents[0]) or nullid + + if parents: + prev = parents[0] + else: + prev = nullid shouldclose = False if not fp and len(template) > 0: @@ -775,7 +978,6 @@ 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()) @@ -800,7 +1002,7 @@ def diffordiffstat(ui, repo, diffopts, node1, node2, match, changes=None, stat=False, fp=None, prefix='', - listsubrepos=False): + root='', listsubrepos=False): '''show diff or diffstat.''' if fp is None: write = ui.write @@ -808,20 +1010,35 @@ def write(s, **kw): fp.write(s) + if root: + relroot = pathutil.canonpath(repo.root, repo.getcwd(), root) + else: + relroot = '' + if relroot != '': + # XXX relative roots currently don't work if the root is within a + # subrepo + uirelroot = match.uipath(relroot) + relroot += '/' + for matchroot in match.files(): + if not matchroot.startswith(relroot): + ui.warn(_('warning: %s not inside relative root %s\n') % ( + match.uipath(matchroot), uirelroot)) + 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) + prefix=prefix, relroot=relroot) 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): + changes, diffopts, prefix=prefix, + relroot=relroot): write(chunk, label=label) if listsubrepos: @@ -884,22 +1101,24 @@ '''show a single changeset or file revision''' changenode = ctx.node() rev = ctx.rev() + if self.ui.debugflag: + hexfunc = hex + else: + hexfunc = short + if rev is None: + pctx = ctx.p1() + revnode = (pctx.rev(), hexfunc(pctx.node()) + '+') + else: + revnode = (rev, hexfunc(changenode)) if self.ui.quiet: - self.ui.write("%d:%s\n" % (rev, short(changenode)), - label='log.node') + self.ui.write("%d:%s\n" % revnode, 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)), + self.ui.write(_("changeset: %d:%s\n") % revnode, label='log.changeset changeset.%s' % ctx.phasestr()) # branches are shown first before any other names due to backwards @@ -925,13 +1144,14 @@ # i18n: column positioning for "hg log" self.ui.write(_("phase: %s\n") % _(ctx.phasestr()), label='log.phase') - for parent in parents: - label = 'log.parent changeset.%s' % self.repo[parent[0]].phasestr() + for pctx in self._meaningful_parentrevs(ctx): + label = 'log.parent changeset.%s' % pctx.phasestr() # i18n: column positioning for "hg log" - self.ui.write(_("parent: %d:%s\n") % parent, + self.ui.write(_("parent: %d:%s\n") + % (pctx.rev(), hexfunc(pctx.node())), label=label) - if self.ui.debugflag: + if self.ui.debugflag and rev is not None: mnode = ctx.manifestnode() # i18n: column positioning for "hg log" self.ui.write(_("manifest: %d:%s\n") % @@ -945,7 +1165,7 @@ label='log.date') if self.ui.debugflag: - files = self.repo.status(log.parents(changenode)[0], changenode)[:3] + files = ctx.p1().status(ctx)[:3] for key, value in zip([# i18n: column positioning for "hg log" _("files:"), # i18n: column positioning for "hg log" @@ -1008,19 +1228,20 @@ match=matchfn, stat=False) self.ui.write("\n") - def _meaningful_parentrevs(self, log, rev): + def _meaningful_parentrevs(self, ctx): """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]] + parents = ctx.parents() + if len(parents) > 1: + return parents + if self.ui.debugflag: + return [parents[0], self.repo['null']] + if parents[0].rev() >= scmutil.intrev(self.repo, ctx.rev()) - 1: + return [] return parents class jsonchangeset(changeset_printer): @@ -1039,8 +1260,12 @@ def _show(self, ctx, copies, matchfn, props): '''show a single changeset or file revision''' - hexnode = hex(ctx.node()) rev = ctx.rev() + if rev is None: + jrev = jnode = 'null' + else: + jrev = str(rev) + jnode = '"%s"' % hex(ctx.node()) j = encoding.jsonescape if self._first: @@ -1050,13 +1275,13 @@ self.ui.write(",\n {") if self.ui.quiet: - self.ui.write('\n "rev": %d' % rev) - self.ui.write(',\n "node": "%s"' % hexnode) + self.ui.write('\n "rev": %s' % jrev) + self.ui.write(',\n "node": %s' % jnode) self.ui.write('\n }') return - self.ui.write('\n "rev": %d' % rev) - self.ui.write(',\n "node": "%s"' % hexnode) + self.ui.write('\n "rev": %s' % jrev) + self.ui.write(',\n "node": %s' % jnode) self.ui.write(',\n "branch": "%s"' % j(ctx.branch())) self.ui.write(',\n "phase": "%s"' % ctx.phasestr()) self.ui.write(',\n "user": "%s"' % j(ctx.user())) @@ -1071,7 +1296,11 @@ ", ".join('"%s"' % c.hex() for c in ctx.parents())) if self.ui.debugflag: - self.ui.write(',\n "manifest": "%s"' % hex(ctx.manifestnode())) + if rev is None: + jmanifestnode = 'null' + else: + jmanifestnode = '"%s"' % hex(ctx.manifestnode()) + self.ui.write(',\n "manifest": %s' % jmanifestnode) self.ui.write(',\n "extra": {%s}' % ", ".join('"%s": "%s"' % (j(k), j(v)) @@ -1134,18 +1363,6 @@ self.cache = {} - 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''' @@ -1429,7 +1646,6 @@ else: last = filelog.rev(node) - # keep track of all ancestors of the file ancestors = set([filelog.linkrev(last)]) @@ -1457,6 +1673,44 @@ return wanted +class _followfilter(object): + def __init__(self, repo, onlyfirst=False): + self.repo = repo + self.startrev = nullrev + self.roots = set() + self.onlyfirst = onlyfirst + + def match(self, rev): + def realparents(rev): + if self.onlyfirst: + return self.repo.changelog.parentrevs(rev)[0:1] + else: + return filter(lambda x: x != nullrev, + self.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 + def walkchangerevs(repo, match, opts, prepare): '''Iterate over files and the revs in which they changed. @@ -1473,14 +1727,7 @@ 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 = revset.spanset(repo) - revs.reverse() + revs = _logrevs(repo, opts) if not revs: return [] wanted = set() @@ -1493,7 +1740,7 @@ # wanted: a cache of filenames that were changed (ctx.files()) and that # match the file filtering conditions. - if not slowpath and not match.files(): + if match.always(): # No files, no patterns. Display all revs. wanted = revs @@ -1552,48 +1799,11 @@ 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() + ff = _followfilter(repo) stop = min(revs[0], revs[-1]) for x in xrange(rev, stop - 1, -1): if ff.match(x): @@ -1603,7 +1813,7 @@ # revision range, yielding only revisions in wanted. def iterate(): if follow and not match.files(): - ff = followfilter(onlyfirst=opts.get('follow_first')) + ff = _followfilter(repo, onlyfirst=opts.get('follow_first')) def want(rev): return ff.match(rev) and rev in wanted else: @@ -1699,7 +1909,10 @@ 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 + if opts.get('follow_first'): + followfirst = 1 + else: + followfirst = 0 # --follow with FILE behaviour depends on revs... it = iter(revs) startrev = it.next() @@ -1716,12 +1929,12 @@ # _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) + wctx = repo[None] + match, pats = scmutil.matchandpats(wctx, 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: + if follow and f not in wctx: # If the file exists, it may be a directory, so let it # take the slow path. if os.path.exists(repo.wjoin(f)): @@ -1822,6 +2035,21 @@ expr = None return expr, filematcher +def _logrevs(repo, 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') + if opts.get('rev'): + revs = scmutil.revrange(repo, opts['rev']) + elif follow and repo.dirstate.p1() == nullid: + revs = revset.baseset() + elif follow: + revs = repo.revs('reverse(:.)') + else: + revs = revset.spanset(repo) + revs.reverse() + return revs + 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 @@ -1830,28 +2058,14 @@ 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 _makelogrevset might depend on the - # order of revs - possiblyunsorted = True - else: - if follow and len(repo) > 0: - revs = repo.revs('reverse(:.)') - else: - revs = revset.spanset(repo) - revs.reverse() + revs = _logrevs(repo, opts) if not revs: return revset.baseset(), None, None expr, filematcher = _makelogrevset(repo, pats, opts, revs) - if possiblyunsorted: + if opts.get('rev'): + # User-specified revs might be unsorted, but don't sort before + # _makelogrevset because it might depend on the order of revs revs.sort(reverse=True) if expr: # Revset matchers often operate faster on revisions in changelog @@ -1882,16 +2096,7 @@ filtering the files to be detailed when displaying the revision. """ 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') - if opts.get('rev'): - revs = scmutil.revrange(repo, opts['rev']) - elif follow: - revs = repo.revs('reverse(:.)') - else: - revs = revset.spanset(repo) - revs.reverse() + revs = _logrevs(repo, opts) if not revs: return revset.baseset([]), None, None expr, filematcher = _makelogrevset(repo, pats, opts, revs) @@ -1930,6 +2135,8 @@ char = '@' elif ctx.obsolete(): char = 'x' + elif ctx.closesbranch(): + char = '_' copies = None if getrenamed and ctx.rev(): copies = [] @@ -2064,6 +2271,35 @@ forgot.extend(f for f in forget if f not in rejected) return bad, forgot +def files(ui, ctx, m, fm, fmt, subrepos): + rev = ctx.rev() + ret = 1 + ds = ctx.repo().dirstate + + for f in ctx.matches(m): + if rev is None and ds[f] == 'r': + continue + fm.startitem() + if ui.verbose: + fc = ctx[f] + fm.write('size flags', '% 10d % 1s ', fc.size(), fc.flags()) + fm.data(abspath=f) + fm.write('path', fmt, m.rel(f)) + ret = 0 + + if subrepos: + for subpath in sorted(ctx.substate): + sub = ctx.sub(subpath) + try: + submatch = matchmod.narrowmatcher(subpath, m) + if sub.printfiles(ui, submatch, fm, fmt) == 0: + ret = 0 + except error.LookupError: + ui.status(_("skipping missing subrepository: %s\n") + % m.abs(subpath)) + + return ret + def remove(ui, repo, m, prefix, after, force, subrepos): join = lambda f: os.path.join(prefix, f) ret = 0 @@ -2092,6 +2328,7 @@ % join(subpath)) # warn about failure to delete explicit files/dirs + deleteddirs = util.dirs(deleted) for f in m.files(): def insubrepo(): for subpath in wctx.substate: @@ -2099,7 +2336,8 @@ return True return False - if f in repo.dirstate or f in wctx.dirs() or f == '.' or insubrepo(): + isdir = f in deleteddirs or f in wctx.dirs() + if f in repo.dirstate or isdir or f == '.' or insubrepo(): continue if repo.wvfs.exists(f): @@ -2164,8 +2402,8 @@ if len(matcher.files()) == 1 and not matcher.anypats(): file = matcher.files()[0] mf = repo.manifest - mfnode = ctx._changeset[0] - if mf.find(mfnode, file)[0]: + mfnode = ctx.manifestnode() + if mfnode and mf.find(mfnode, file)[0]: write(file) return 0 @@ -2220,7 +2458,7 @@ def amend(ui, repo, commitfunc, old, extra, pats, opts): # amend will reuse the existing user if not specified, but the obsolete # marker creation requires that the current user's name is specified. - if obsolete._enabled: + if obsolete.isenabled(repo, obsolete.createmarkersopt): ui.username() # raise exception if username not set ui.note(_('amending changeset %s\n') % old) @@ -2555,7 +2793,9 @@ # 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. + # but not other. in both cases, filesets should be evaluated against + # workingctx to get consistent result (issue4497). this means 'set:**' + # cannot be used to select missing files from target rev. # `names` is a mapping for all elements in working copy and target revision # The mapping is in the form: @@ -2567,8 +2807,14 @@ ## filling of the `names` mapping # walk dirstate to fill `names` - m = scmutil.match(repo[None], pats, opts) - if not m.always() or node != parent: + interactive = opts.get('interactive', False) + wctx = repo[None] + m = scmutil.match(wctx, pats, opts) + + # we'll need this later + targetsubs = sorted(s for s in wctx.substate if m(s)) + + if not m.always(): m.bad = lambda x, y: False for abs in repo.walk(m): names[abs] = m.rel(abs), m.exact(abs) @@ -2586,7 +2832,6 @@ 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: @@ -2598,7 +2843,7 @@ changes = repo.status(node1=node, match=m, unknown=True, ignored=True, clean=True) else: - changes = repo.status(match=m) + changes = repo.status(node1=node, match=m) for kind in changes: for abs in kind: names[abs] = m.rel(abs), m.exact(abs) @@ -2621,9 +2866,8 @@ deladded = _deleted - smf deleted = _deleted - deladded - # We need to account for the state of file in the dirstate. - # - # Even, when we revert against something else than parent. This will + # We need to account for the state of the file in the dirstate, + # even when we revert against something else than parent. This will # slightly alter the behavior of revert (doing back up or not, delete # or just forget etc). if parent == node: @@ -2772,7 +3016,6 @@ (unknown, actions['unknown'], discard), ) - wctx = repo[None] for abs, (rel, exact) in sorted(names.items()): # target file to be touch on disk (relative to cwd) target = repo.wjoin(abs) @@ -2790,7 +3033,10 @@ ui.note(_('saving current version of %s as %s\n') % (rel, bakname)) if not opts.get('dry_run'): - util.rename(target, bakname) + if interactive: + util.copyfile(target, bakname) + else: + util.rename(target, bakname) if ui.verbose or not exact: if not isinstance(msg, basestring): msg = msg(abs) @@ -2799,21 +3045,19 @@ ui.warn(msg % rel) break - if not opts.get('dry_run'): needdata = ('revert', 'add', 'undelete') _revertprefetch(repo, ctx, *[actions[name][0] for name in needdata]) - - _performrevert(repo, parents, ctx, actions) - - # get the list of subrepos that must be reverted - subrepomatch = scmutil.match(ctx, pats, opts) - targetsubs = sorted(s for s in ctx.substate if subrepomatch(s)) - - if targetsubs: - # Revert the subrepos on the revert list - for sub in targetsubs: - ctx.sub(sub).revert(ctx.substate[sub], *pats, **opts) + _performrevert(repo, parents, ctx, actions, interactive) + + if targetsubs: + # Revert the subrepos on the revert list + for sub in targetsubs: + try: + wctx.sub(sub).revert(ctx.substate[sub], *pats, **opts) + except KeyError: + raise util.Abort("subrepository '%s' does not exist in %s!" + % (sub, short(ctx.node()))) finally: wlock.release() @@ -2821,7 +3065,7 @@ """Let extension changing the storage layer prefetch content""" pass -def _performrevert(repo, parents, ctx, actions): +def _performrevert(repo, parents, ctx, actions, interactive=False): """function that actually perform all the actions computed for revert This is an independent function to let extension to plug in and react to @@ -2855,10 +3099,35 @@ normal = repo.dirstate.normallookup else: normal = repo.dirstate.normal - for f in actions['revert'][0]: - checkout(f) - if normal: - normal(f) + + if interactive: + # Prompt the user for changes to revert + torevert = [repo.wjoin(f) for f in actions['revert'][0]] + m = scmutil.match(ctx, torevert, {}) + diff = patch.diff(repo, None, ctx.node(), m) + originalchunks = patch.parsepatch(diff) + try: + chunks = recordfilter(repo.ui, originalchunks) + except patch.PatchError, err: + raise util.Abort(_('error parsing patch: %s') % err) + + # Apply changes + fp = cStringIO.StringIO() + for c in chunks: + c.write(fp) + dopatch = fp.tell() + fp.seek(0) + if dopatch: + try: + patch.internalpatch(repo.ui, repo, fp, 1, eolmode=None) + except patch.PatchError, err: + raise util.Abort(str(err)) + del fp + else: + for f in actions['revert'][0]: + checkout(f) + if normal: + normal(f) for f in actions['add'][0]: checkout(f)