[PATCH 1 of 1] perfarce: push to and pull from Perforce depots

Frank Kingswood frank at kingswood-consulting.co.uk
Wed Feb 10 12:52:53 UTC 2010


# HG changeset patch
# User Frank Kingswood <frank.kingswood at mstarsemi.com>
# Date 1265806270 0
# Node ID 482c1162179ac6b56fe0cd39dcc9b328e55af464
# Parent  b8acd325773e1e8c678572b535a9001994c0b2c1
perfarce: push to and pull from Perforce depots
This extension modifies the remote repository handling so that repository
paths that resemble
    p4://p4server[:port]/clientname
cause operations on the named p4 client specification on the p4 server.

diff -r b8acd325773e -r 482c1162179a hgext/perfarce.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hgext/perfarce.py	Wed Feb 10 12:51:10 2010 +0000
@@ -0,0 +1,1148 @@
+# Mercurial extension to push to and pull from Perforce depots.
+#
+# Copyright 2009-10 Frank Kingswood <frank at kingswood-consulting.co.uk>
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2, incorporated herein by reference.
+
+'''Push to or pull from Perforce depots
+
+This extension modifies the remote repository handling so that repository
+paths that resemble
+    p4://p4server[:port]/clientname
+cause operations on the named p4 client specification on the p4 server.
+The client specification must already exist on the server before using
+this extension. Making changes to the client specification Views causes
+problems when synchronizing the repositories, and should be avoided.
+
+Five built-in commands are overridden:
+
+ outgoing  If the destination repository name starts with p4:// then
+           this reports files affected by the revision(s) that are
+           in the local repository but not in the p4 depot.
+
+ push      If the destination repository name starts with p4:// then
+           this exports changes from the local repository to the p4
+           depot. If no revision is specified then all changes since
+           the last p4 changelist are pushed. In either case, all
+           revisions to be pushed are folded into a single p4 changelist.
+           Optionally the resulting changelist is submitted to the p4
+           server, controlled by the --submit option to push, or by
+           setting
+              --config perfarce.submit=True
+           If the option
+              --config perfarce.keep=False
+           is False then after a successful submit the files in the
+           p4 workarea will be deleted.
+
+ pull      If the source repository name starts with p4:// then this
+           imports changes from the p4 depot, automatically creating
+           merges of changelists submitted by hg push.
+           If the option
+              --config perfarce.keep=False
+           is False then the import does not leave files in the p4
+           workarea, otherwise the p4 workarea will be updated
+           with the new files.
+           The option
+              --config perfarce.clientuser="search replace"
+           can be used to enable quasi-multiuser operation, where
+           several users submit changes to p4 with the same user name
+           and have their real user name in the p4 client spec.
+           The search and replace regular expressions describe
+           the substitution to be made to turn a client spec name
+           into a user name. If the search regex does not match
+           then the username is left unchanged.
+
+ incoming  If the source repository name starts with p4:// then this
+           reports changes in the p4 depot that are not yet in the
+           local repository.
+
+ clone     If the source repository name starts with p4:// then this
+           creates the destination repository and pulls all changes
+           from the p4 depot into it.
+           If the option
+              --config perfarce.lowercasepaths=False
+           is True then the import forces all paths in lowercase,
+           otherwise paths are recorded unchanged.  Filename case is
+           always preserved.
+           This setting is a workaround to handle Perforce depots
+           containing a path spelled differently from file to file
+           (e.g. path/foo and PAth/bar).
+'''
+
+from mercurial import cmdutil, commands, context, copies, error, extensions, hg, node, util
+from mercurial.node import hex, short
+from mercurial.i18n import _
+
+import marshal, tempfile, os, re, string
+
+def uisetup(ui):
+    '''monkeypatch pull and push for p4:// support'''
+
+    extensions.wrapcommand(commands.table, 'pull', pull)
+    p = extensions.wrapcommand(commands.table, 'push', push)
+    p[1].append(('', 'submit', None, 'for p4:// destination submit new changelist to server'))
+    extensions.wrapcommand(commands.table, 'incoming', incoming)
+    extensions.wrapcommand(commands.table, 'outgoing', outgoing)
+    p = extensions.wrapcommand(commands.table, 'clone', clone)
+    p[1].append(('', 'startrev', '', 'for p4:// source set initial revisions for clone'))
+
+# --------------------------------------------------------------------------
+
+def loaditer(f):
+    "Yield the dictionary objects generated by p4"
+    try:
+        while True:
+            d = marshal.load(f)
+            if not d:
+                break
+            yield d
+    except EOFError:
+        pass
+
+
+class p4client(object):
+
+    def __init__(self, ui, repo, path):
+        'initialize a p4client class from the remote path'
+
+        try:
+            assert path.startswith('p4://')
+
+            self.ui = ui
+            self.repo = repo
+            self.server = None
+            self.client = None
+            self.root = None
+            self.keep = ui.configbool('perfarce', 'keep', True)
+            self.lowercasepaths = ui.configbool('perfarce', 'lowercasepaths', False)
+
+            # caches
+            self.clientspec = {}
+            self.usercache = {}
+            self.p4stat = None
+
+            # helpers to parse p4 output
+            self.re_type = re.compile('([a-z]+)?(text|binary|symlink|apple|resource|unicode|utf\d+)(\+\w+)?$')
+            self.re_keywords = re.compile(r'\$(Id|Header|Date|DateTime|Change|File|Revision|Author):[^$\n]*\$')
+            self.re_keywords_old = re.compile('\$(Id|Header):[^$\n]*\$')
+            self.re_hgid = re.compile('{{mercurial (([0-9a-f]{40})(:([0-9a-f]{40}))?)}}')
+            self.re_number = re.compile('.+ ([0-9]+) .+')
+            self.actions = { 'edit':'M', 'add':'A', 'move/add':'A', 'delete':'R', 'move/delete':'R', 'purge':'R', 'branch':'A', 'integrate':'M' }
+
+            try:
+                maxargs = ui.config('perfarce', 'maxargs')
+                self.MAXARGS = int(maxargs)
+            except:
+                if os.name == 'posix':
+                    self.MAXARGS = 250
+                else:
+                    self.MAXARGS = 25
+
+            s, c = path[5:].split('/')
+            if ':' not in s:
+                s = '%s:1666' % s
+            self.server = s
+            if c:
+                d = self.runs('client -o %s' % util.shellquote(c))
+                for n in ['Root'] + ['AltRoots%d' % i for i in range(9)]:
+                    if n in d and os.path.isdir(d[n]):
+                        self.root = util.pconvert(d[n])
+                        break
+                if not self.root:
+                    ui.note(_('the p4 client root must exist\n'))
+                    assert False
+
+                self.clientspec = d
+                self.client = c
+        except:
+            ui.traceback()
+            raise util.Abort(_('not a p4 repository'))
+
+
+    def latest(self, tags=False):
+        '''Find the most recent changelist which has the p4 extra data which
+        gives the p4 changelist it was converted from'''
+        for rev in xrange(len(self.repo)-1, -1, -1):
+            ctx = self.repo[rev]
+            extra = ctx.extra()
+            if 'p4' in extra:
+                if tags:
+                    # if there is a child with p4 tags then return the child revision
+                    for ctx2 in ctx.children():
+                        if ctx2.description().startswith('p4 tags\n') and ".hgtags" in ctx2:
+                            ctx = ctx2
+                            break
+                return ctx.node(), int(extra['p4'])
+        raise util.Abort(_('no p4 changelist revision found'))
+
+
+    def decodetype(self, p4type):
+        'decode p4 type name into mercurial mode string and keyword substitution regex'
+
+        mode = ''
+        keywords = None
+        p4type = self.re_type.match(p4type)
+        if p4type:
+            flags = (p4type.group(1) or '') + (p4type.group(3) or '')
+            if 'x' in flags:
+                mode = 'x'
+            if p4type.group(2) == 'symlink':
+                mode = 'l'
+            if 'ko' in flags:
+                keywords = self.re_keywords_old
+            elif 'k' in flags:
+                keywords = self.re_keywords
+        return mode, keywords
+
+
+    def parsenodes(self, desc):
+        'find revisions in p4 changelist description'
+        m = self.re_hgid.search(desc)
+        nodes = []
+        if m:
+            try:
+                nodes = self.repo.changelog.nodesbetween(
+                    [self.repo[m.group(2)].node()], [self.repo[m.group(4) or m.group(2)].node()])[0]
+            except:
+                self.ui.traceback()
+                self.ui.note(_('ignoring hg revision range %s from p4\n' % m.group(1)))
+        return nodes
+
+
+    def run(self, cmd, files=[], abort=True):
+        'Run a P4 command and yield the objects returned'
+
+        c = ['p4', '-G']
+        if self.server:
+            c.append('-p')
+            c.append(self.server)
+        if self.client:
+            c.append('-c')
+            c.append(self.client)
+        c.append(cmd)
+
+        old = os.getcwd()
+        try:
+            if self.root:
+                os.chdir(self.root)
+
+            for i in range(0, len(files), self.MAXARGS) or [0]:
+                cs = ' '.join(c + [util.shellquote(f) for f in files[i:i + self.MAXARGS]])
+                if self.ui.debugflag: self.ui.debug('> %s\n' % cs)
+
+                for d in loaditer(util.popen(cs, mode='rb')):
+                    if self.ui.debugflag: self.ui.debug('< %r\n' % d)
+                    code = d.get('code')
+                    data = d.get('data')
+                    if code is not None and data is not None:
+                        data = data.strip()
+                        if abort and code == 'error':
+                            raise util.Abort('p4: %s' % data)
+                        elif code == 'info':
+                            self.ui.note('p4: %s\n' % data)
+                    yield d
+        finally:
+            os.chdir(old)
+
+
+    def runs(self, cmd, one=True, **args):
+        '''Run a P4 command and return the number of objects returned,
+        or the object itself if exactly one was returned'''
+        count = 0
+        for d in self.run(cmd, **args):
+            if not count:
+                value = d
+            count += 1
+        if count == 1 and one:
+            return value
+        return count
+
+
+    SUBMITTED = -1
+    def getpending(self, node):
+        '''return p4 submission state for node: SUBMITTED, a changelist
+        number if pending or None if not in a changelist'''
+        if self.p4stat is None:
+            self._readp4stat()
+        return self.p4stat.get(node, None)
+
+    def getpendingdict(self):
+        'return p4 submission state dictionary'
+        if self.p4stat is None:
+            self._readp4stat()
+        return self.p4stat
+
+    def _readp4stat(self):
+        '''read pending and submitted changelists into pending cache'''
+        self.p4stat = {}
+
+        p4rev, p4id = self.latest()
+
+        def helper(self,d,p4id):
+            c = int(d['change'])
+            if c == p4id:
+                return
+
+            if d['status'] == 'submitted':
+                state = self.SUBMITTED
+            else:
+                state = c
+            for n in self.parsenodes(d['desc']):
+                self.p4stat[hex(n)] = state
+
+        for d in self.run('changes -l -c %s ...@%d,#head' %
+                           (util.shellquote(self.client), p4id)):
+            helper(self,d,p4id)
+        for d in self.run('changes -l -c %s -s pending' %
+                           (util.shellquote(self.client))):
+            helper(self,d,p4id)
+
+
+    def getuser(self, user):
+        'get full name and email address of user'
+        r = self.usercache.get(user)
+        if r:
+            return r
+
+        d = self.runs('user -o %s' % util.shellquote(user), abort=False)
+        if 'Update' in d:
+            try:
+                r = '%s <%s>' % (d['FullName'], d['Email'])
+                self.usercache[user] = r
+                return r
+            except:
+                pass
+        return user
+
+
+    def describe(self, change, local=None):
+        '''Return p4 changelist description, user name and date.
+        If the local is true, then also collect a list of 5-tuples
+            (depotname, revision, type, action, localname)
+        This does not work on pending changelists.
+        If local is false then the list returned holds 4-tuples
+            (depotname, revision, type, action)
+        Retrieving the local filenames is potentially very slow.
+        '''
+
+        self.ui.note(_('change %d\n') % change)
+        d = self.runs('describe -s %d' % change)
+        desc = d['desc']
+        user = self.getuser(d['user'])
+        date = (int(d['time']), 0)     # p4 uses UNIX epoch
+
+        if local:
+            files = self.fstat(change)
+        else:
+            files = []
+            i = 0
+            while True:
+                df = 'depotFile%d' % i
+                if df not in d:
+                    break
+                df = d['depotFile%d' % i]
+                rv = d['rev%d' % i]
+                tp = d['type%d' % i]
+                ac = d['action%d' % i]
+                files.append((df, int(rv), tp, self.actions[ac]))
+                i += 1
+
+        # quasi-multiuser operation, extract user name from client
+        cu = self.ui.config("perfarce", "clientuser")
+        if cu:
+            cus, cur = cu.split(" ",1)
+            u, f = re.subn(cus, cur, d['client'])
+            if f:
+                user = string.capwords(u)
+
+        return desc, user, date, files
+
+
+    def fstat(self, change, all=False):
+        '''Find local names for all the files belonging to a
+        changelist.
+        Returns a list of tuples 
+            (depotname, revision, type, action, localname)
+        with only entries for files that appear in the workspace.
+        If all is unset considers only files modified by the
+        changelist, otherwise returns all files *at* that changelist.
+        '''
+        result = []
+
+        if all:
+            p4cmd = 'fstat ...@%d' % change
+        else:
+            p4cmd = 'fstat -e %d ...' % change
+
+        if self.lowercasepaths:
+            root = os.path.normcase(self.root)
+        else:
+            root = self.root
+
+        for d in self.run(p4cmd):
+            if len(result) % 250 == 0:
+                if hasattr(self.ui, 'progress'):
+                    self.ui.progress('p4 fstat', len(result), unit='entries')
+                else:
+                    self.ui.note('%d files\r' % len(result))
+                    self.ui.flush()
+
+            if 'desc' in d or d['clientFile'].startswith('.hg'):
+                continue
+            else:
+                df = d['depotFile']
+                rv = d['headRev']
+                tp = d['headType']
+                ac = d['headAction']
+                lf = d['clientFile']
+                if self.lowercasepaths:
+                   pathname, fname = os.path.split(lf)
+                   lf = os.path.join(os.path.normcase(pathname), fname)
+                lf = util.pconvert(lf)
+                if lf.startswith('%s/' % root):
+                    lf = lf[len(root) + 1:]
+                else:
+                    raise util.Abort(_('invalid p4 local path %s') % lf)
+                result.append((df, int(rv), tp, self.actions[ac], lf))
+
+        if hasattr(self.ui, 'progress'):
+            self.ui.progress('p4 fstat', None)
+        self.ui.note('%d files \n' % len(result))
+
+        return result
+
+
+    def sync(self, change, fake=False, force=False, all=False, files=[]):
+        '''Synchronize the client with the depot at the given change.
+        Setting fake adds -k, force adds -f option. The all option is
+        not used here, but indicates that the caller wants all the files
+        at that revision, not just the files affected by the change.'''
+
+        cmd = 'sync'
+        if fake:
+            cmd += ' -k'
+        elif force:
+            cmd += ' -f'
+        if not files:
+            cmd += ' ...@%d' % change
+
+        n = 0
+        for d in self.run(cmd, files=[("%s@%d" % (f, change)) for f in files], abort=False):
+            n += 1
+            if n % 250 == 0:
+                if hasattr(self.ui, 'progress'):
+                    self.ui.progress('p4 sync', n, unit='files')
+            code = d.get('code')
+            if code == 'error':
+                data = d['data'].strip()
+                if d['generic'] == 17 or d['severity'] == 2:
+                    self.ui.note('p4: %s\n' % data)
+                else:
+                    raise util.Abort('p4: %s' % data)
+
+        if hasattr(self.ui, 'progress'):
+            self.ui.progress('p4 sync', None)
+
+        if files and n < len(files):
+            raise util.Abort(_('incomplete reply from p4, reduce maxargs'))
+
+
+    def getfile(self, entry):
+        '''Return contents of a file in the p4 depot at the given revision number.
+        If self.keep is set, assumes that the client is in sync.
+        Raises IOError if the file is deleted.
+        '''
+
+        if entry[3] == 'R':
+            self.ui.debug('getfile ioerror on %r\n'%(entry,))
+            raise IOError()
+
+        try:
+            mode, keywords = self.decodetype(entry[2])
+
+            if self.keep:
+                fn = os.sep.join([self.root, entry[4]])
+                fn = util.localpath(fn)
+                if mode == 'l':
+                    contents = os.readlink(fn)
+                else:
+                    contents = file(fn, 'rb').read()
+            else:
+                contents = []
+                for d in self.run('print %s#%d' % (util.shellquote(entry[0]), entry[1])):
+                    code = d['code']
+                    if code == 'text' or code == 'binary':
+                        contents.append(d['data'])
+
+                contents = ''.join(contents)
+                if mode == 'l' and contents.endswith('\n'):
+                    contents = contents[:-1]
+
+            if keywords:
+                contents = keywords.sub('$\\1$', contents)
+
+            return mode, contents
+        except Exception, e:
+            self.ui.traceback()
+            raise util.Abort(_('file %s missing in p4 workspace') % entry[4])
+
+
+    def labels(self, change):
+        'Return p4 labels a.k.a. tags at the given changelist'
+
+        tags = []
+        for d in self.run('labels ...@%d,%d' % (change, change)):
+            l = d.get('label')
+            if l:
+                tags.append(l)
+
+        return tags
+
+
+    @staticmethod
+    def pullcommon(original, ui, repo, source, **opts):
+        'Shared code for pull and incoming'
+
+        source = ui.expandpath(source or 'default')
+        try:
+            client = p4client(ui, repo, source)
+        except:
+            ui.traceback()
+            return True, original(ui, repo, source, **opts)
+
+        # if present, --rev will be the last Perforce changeset number to get
+        stoprev = opts.get('rev')
+        stoprev = stoprev and max(int(r) for r in stoprev) or 0
+
+        # for clone we support a --startrev option to fold initial changelists
+        startrev = opts.get('startrev')
+        startrev = startrev and int(startrev) or 0
+
+        if len(repo):
+            p4rev, p4id = client.latest(tags=True)
+        else:
+            p4rev = None
+            if startrev > 0:
+                p4id = startrev
+            else:
+                p4id = 0
+
+        if stoprev:
+           p4cset = '...@%d,@%d' % (p4id, stoprev)
+        else:
+           p4cset = '...@%d,#head' % p4id
+
+        if startrev < 0:
+            # most recent changelists
+            p4cmd = 'changes -s submitted -m %d -L %s' % (-startrev, p4cset)
+        else:
+            p4cmd = 'changes -s submitted -L %s' % p4cset
+
+        changes = []
+        for d in client.run(p4cmd):
+            c = int(d['change'])
+            if startrev or c != p4id:
+                changes.append(c)
+        changes.sort()
+
+        return False, (client, p4rev, p4id, startrev, changes)
+
+
+    @staticmethod
+    def pushcommon(out, original, ui, repo, dest, **opts):
+        'Shared code for push and outgoing'
+
+        dest = ui.expandpath(dest or 'default-push', dest or 'default')
+        try:
+            client = p4client(ui, repo, dest)
+        except:
+            ui.traceback()
+            return True, original(ui, repo, dest, **opts)
+
+        p4rev, p4id = client.latest(tags=True)
+        ctx1 = repo[p4rev]
+        rev = opts.get('rev')
+
+        if rev:
+            n1, n2 = cmdutil.revpair(repo, rev)
+            if n2:
+                ctx1 = repo[n1]
+                ctx1 = ctx1.parents()[0]
+                ctx2 = repo[n2]
+            else:
+                ctx2 = repo[n1]
+                ctx1 = ctx2.parents()[0]
+        else:
+            ctx2 = repo['tip']
+
+        nodes = repo.changelog.nodesbetween([ctx1.node()], [ctx2.node()])[0][1:]
+
+        if not opts['force']:
+            # trim off nodes at either end that have already been pushed
+            trim = False
+            for end in [0, -1]:
+                while nodes:
+                    n = repo[nodes[end]]
+                    if client.getpending(n.hex()) is not None:
+                        del nodes[end]
+                        trim = True
+                    else:
+                        break
+
+            # recalculate the context
+            if trim and nodes:
+                ctx1 = repo[nodes[0]].parents()[0]
+                ctx2 = repo[nodes[-1]]
+
+            # check that remaining nodes have not already been pushed
+            for n in nodes:
+                n = repo[n]
+                fail = False
+                if client.getpending(n.hex()) is not None:
+                    fail = True
+                for ctx3 in n.children():
+                    extra = ctx3.extra()
+                    if 'p4' in extra:
+                        fail = True
+                        break
+                if fail:
+                    raise util.Abort(_('can not push, changeset %s is already in p4' % n))
+
+        # find changed files
+        if not nodes:
+            mod = add = rem = []
+            cpy = {}
+        else:
+            mod, add, rem = repo.status(node1=ctx1.node(), node2=ctx2.node())[:3]
+            cpy = copies.copies(repo, ctx1, ctx2, repo[node.nullid])[0]
+
+            # forget about copies with changes to the data
+            forget = []
+            for c in cpy:
+                if ctx2[c].data() != ctx1[cpy[c]].data():
+                    forget.append(c)
+            for c in forget:
+                del cpy[c]
+
+            # remove .hg* files (mainly for .hgtags and .hgignore)
+            for changes in [mod, add, rem]:
+                i = 0
+                while i < len(changes):
+                    f = changes[i]
+                    if f.startswith('.hg'):
+                        del changes[i]
+                    else:
+                        i += 1
+
+        if not (mod or add or rem):
+            ui.status(_('no changes found\n'))
+            return True, 0
+
+        # detect MQ
+        try:
+            mq = repo.changelog.nodesbetween([repo['qbase'].node()], nodes)[0]
+            if mq:
+                raise util.Abort(_('source has mq patches applied'))
+        except error.RepoLookupError:
+            pass
+
+        # create description
+        desc = []
+        for n in nodes:
+            desc.append(repo[n].description())
+
+        if len(nodes) > 1:
+            h = [repo[nodes[0]].hex()]
+        else:
+            h = []
+        h.append(repo[nodes[-1]].hex())
+
+        desc='\n* * *\n'.join(desc) + '\n\n{{mercurial %s}}\n' % (':'.join(h))
+
+        return False, (client, p4rev, p4id, nodes, ctx2, desc, mod, add, rem, cpy)
+
+
+    def submit(self, nodes, change):
+        '''submit one changelist to p4 and optionally delete the files added 
+        or modified in the p4 workarea'''
+
+        cl = None
+        for d in self.run('submit -c %s' % change):
+            if d['code'] == 'error':
+                raise util.Abort(_('error submitting p4 change %s: %s') % (change, d['data']))
+            cl = d.get('submittedChange', cl)
+
+        self.ui.note(_('submitted changelist %s\n') % cl)
+
+        if not self.keep:
+            # delete the files in the p4 client directory
+            self.sync(0)
+
+
+# --------------------------------------------------------------------------
+
+def incoming(original, ui, repo, source='default', **opts):
+    '''show changes that would be pulled from the p4 source repository'''
+
+    done, r = p4client.pullcommon(original, ui, repo, source, **opts)
+    if done:
+        return r
+
+    client, p4rev, p4id, startrev, changes = r
+    for c in changes:
+        desc, user, date, files = client.describe(c, local=ui.verbose)
+        tags = client.labels(c)
+
+        ui.write(_('changelist:  %d\n') % c)
+        # ui.write(_('branch:      %s\n') % branch)
+        for tag in tags:
+            ui.write(_('tag:         %s\n') % tag)
+        # ui.write(_('parent:      %d:%s\n') % parent)
+        ui.write(_('user:        %s\n') % user)
+        ui.write(_('date:        %s\n') % util.datestr(date))
+        if ui.verbose:
+            ui.write(_('files:       %s\n') % ' '.join(f[4] for f in files))
+
+        if desc:
+            if ui.verbose:
+                ui.write(_('description:\n'))
+                ui.write(desc)
+                ui.write('\n')
+            else:
+                ui.write(_('summary:     %s\n') % desc.splitlines()[0])
+
+        ui.write('\n')
+
+
+def pull(original, ui, repo, source=None, **opts):
+    '''Wrap the pull command to look for p4 paths, import changelists'''
+
+    done, r = p4client.pullcommon(original, ui, repo, source, **opts)
+    if done:
+        return r
+
+    client, p4rev, p4id, startrev, changes = r
+    entries = {}
+    c = 0
+
+    def getfilectx(repo, memctx, fn):
+        'callback to read file data'
+        mode, contents = client.getfile(entries[fn])
+        return context.memfilectx(fn, contents, 'l' in mode, 'x' in mode, None)
+
+    # for clone we support a --startrev option to fold initial changelists
+    if startrev:
+        if len(changes) < 2:
+            raise util.Abort(_('with --startrev there must be at least two revisions to clone'))
+        if startrev < 0:
+            startrev = changes[0]
+        else:
+            if changes[0] != startrev:
+                raise util.Abort(_('changelist for --startrev not found'))
+
+    if client.lowercasepaths:
+        ui.status(_("converting pathnames to lowercase.\n"))
+
+    tags = {}
+
+    try:
+        for c in changes:
+            desc, user, date, files = client.describe(c)
+            files = client.fstat(c, all=bool(startrev))
+
+            if client.keep:
+                if startrev:
+                    client.sync(c, all=True, force=True)
+                else:
+                    client.runs('revert -k', files=[f[0] for f in files],
+                                abort=False)
+                    client.sync(c, force=True, files=[f[0] for f in files])
+
+            entries = dict((f[4], f) for f in files)
+
+            nodes = client.parsenodes(desc)
+            if nodes:
+                parent = nodes[-1]
+            else:
+                parent = None
+
+            if startrev:
+                # no 'p4' data on first revision as it does not correspond
+                # to a p4 changelist but to all of history up to a point
+                extra = {}
+                startrev = None
+            else:
+                extra = {'p4':c}
+
+            ctx = context.memctx(repo, (p4rev, parent), desc,
+                                 [f[4] for f in files], getfilectx,
+                                 user, date, extra)
+
+            p4rev = repo.commitctx(ctx)
+            ctx = repo[p4rev]
+
+            for l in client.labels(c):
+                tags[l] = (c, ctx.hex())
+
+            ui.note(_('added changeset %d:%s\n') % (ctx.rev(), ctx))
+
+    finally:
+        if tags:
+            p4rev, p4id = client.latest()
+            ctx = repo[p4rev]
+
+            if '.hgtags' in ctx:
+                tagdata = [ctx.filectx('.hgtags').data()]
+            else:
+                tagdata = []
+
+            desc = ['p4 tags']
+            for l in sorted(tags):
+                t = tags[l]
+                desc.append('   %s @ %d' % (l, t[0]))
+                tagdata.append('%s %s\n' % (t[1], l))
+
+            def getfilectx(repo, memctx, fn):
+                'callback to read file data'
+                assert fn=='.hgtags'
+                return context.memfilectx(fn, ''.join(tagdata), False, False, None)
+
+            ctx = context.memctx(repo, (p4rev, None), '\n'.join(desc),
+                                 ['.hgtags'], getfilectx)
+            p4rev = repo.commitctx(ctx)
+            ctx = repo[p4rev]
+            ui.note(_('added changeset %d:%s\n') % (ctx.rev(), ctx))
+
+    if opts['update']:
+        return hg.update(repo, 'tip')
+
+
+def clone(original, ui, source, dest=None, **opts):
+    '''Wrap the clone command to look for p4 source paths, do pull'''
+
+    try:
+        client = p4client(ui, None, source)
+    except:
+        ui.traceback()
+        return original(ui, source, dest, **opts)
+
+    if dest is None:
+        dest = hg.defaultdest(source)
+        ui.status(_("destination directory: %s\n") % dest)
+    else:
+        dest = ui.expandpath(dest)
+    dest = hg.localpath(dest)
+
+    if not hg.islocal(dest):
+        raise util.Abort(_("destination '%s' must be local") % dest)
+
+    if os.path.exists(dest):
+        if not os.path.isdir(dest):
+            raise util.Abort(_("destination '%s' already exists") % dest)
+        elif os.listdir(dest):
+            raise util.Abort(_("destination '%s' is not empty") % dest)
+
+    repo = hg.repository(ui, dest, create=True)
+
+    opts['update'] = not opts['noupdate']
+    opts['force'] = None
+
+    try:
+        r = pull(None, ui, repo, source=source, **opts)
+    finally:
+        fp = repo.opener("hgrc", "w", text=True)
+        fp.write("[paths]\n")
+        fp.write("default = %s\n" % source)
+        fp.write("\n[perfarce]\n")
+        fp.write("keep = %s\n" % client.keep)
+        fp.write("lowercasepaths = %s\n" % client.lowercasepaths)
+        cu = self.ui.config("perfarce", "clientuser")
+        if cu:
+            fp.write("clientuser = %s\n" % cu)
+        fp.close()
+
+    return r
+
+
+# --------------------------------------------------------------------------
+
+def outgoing(original, ui, repo, dest=None, **opts):
+    '''Wrap the outgoing command to look for p4 paths, report changes'''
+    done, r = p4client.pushcommon(True, original, ui, repo, dest, **opts)
+    if done:
+        return r
+    client, p4rev, p4id, nodes, ctx, desc, mod, add, rem, cpy = r
+
+    if ui.quiet:
+        # for thg integration until we support templates
+        for n in nodes:
+            ui.write('%s\n' % repo[n].hex())
+    else:
+        ui.write(desc)
+        ui.write('\naffected files:\n')
+        cwd = repo.getcwd()
+        for char, files in zip('MAR', (mod, add, rem)):
+            for f in files:
+                ui.write('%s %s\n' % (char, repo.pathto(f, cwd)))
+        ui.write('\n')
+
+
+def push(original, ui, repo, dest=None, **opts):
+    '''Wrap the push command to look for p4 paths, create p4 changelist'''
+
+    done, r = p4client.pushcommon(False, original, ui, repo, dest, **opts)
+    if done:
+        return r
+    client, p4rev, p4id, nodes, ctx, desc, mod, add, rem, cpy = r
+
+    # sync to the last revision pulled/converted
+    if client.keep:
+        client.sync(p4id)
+    else:
+        client.sync(p4id, fake=True)
+        client.sync(p4id, force=True, files=mod)
+
+    # attempt to reuse an existing changelist
+    use = ''
+    for d in client.run('changes -s pending -c %s -l' % client.client):
+        if d['desc'] == desc:
+            use = d['change']
+
+    # revert any other changes to the files in existing changelist
+    if use:
+        ui.note(_('reverting: %s\n') % ' '.join(mod+add+rem))
+        client.runs('revert -c %s' % use, files=mod + add + rem, abort=False)
+
+    # get changelist data, and update it
+    changelist = client.runs('change -o %s' % use)
+    changelist['Description'] = desc
+
+    fn = None
+    try:
+        # write changelist data to a temporary file
+        fd, fn = tempfile.mkstemp(prefix='hg-p4-')
+        fp = os.fdopen(fd, 'wb')
+        marshal.dump(changelist, fp)
+        fp.close()
+
+        # update p4 changelist
+        d = client.runs('change -i <%s' % util.shellquote(fn))
+        data = d['data']
+        if d['code'] == 'info':
+            if not ui.verbose:
+                ui.status('p4: %s\n' % data)
+            if not use:
+                m = client.re_number.match(data)
+                if m:
+                    use = m.group(1)
+        else:
+            raise util.Abort(_('error creating p4 change: %s') % data)
+
+    finally:
+        try:
+            if fn: os.unlink(fn)
+        except:
+            pass
+
+    if not use:
+        raise util.Abort(_('did not get changelist number from p4'))
+
+    # sort out the copies from the adds
+    ntg = [(cpy[f], f) for f in add if f in cpy]
+    add = [f for f in add if f not in cpy]
+
+    # now add/edit/delete the files
+    if mod:
+        ui.note(_('opening for edit: %s\n') % ' '.join(mod))
+        client.runs('edit -c %s' % use, files=mod)
+
+    if mod or add:
+        ui.note(_('retrieving file contents...\n'))
+
+        for f in mod + add:
+            out = os.path.join(client.root, f)
+            ui.debug(_('writing: %s\n') % out)
+            util.makedirs(os.path.dirname(out))
+            fp = cmdutil.make_file(repo, out, ctx.node(), pathname=f)
+            data = ctx[f].data()
+            fp.write(data)
+            fp.close()
+
+    if add:
+        ui.note(_('opening for add: %s\n') % ' '.join(add))
+        client.runs('add -c %s' % use, files=add)
+
+    if ntg:
+        ui.note(_('opening for integrate: %s\n') % ' '.join(f[1] for f in ntg))
+        for f in ntg:
+            client.runs('integrate -c %s %s %s' % (use, f[0], f[1]))
+
+    if rem:
+        ui.note(_('opening for delete: %s\n') % ' '.join(rem))
+        client.runs('delete -c %s' % use, files=rem)
+
+    # submit the changelist to p4 if --submit was given
+    if opts['submit'] or ui.configbool('perfarce', 'submit', default=False):
+        client.submit(nodes, use)
+    else:
+        ui.note(_('pending changelist %s\n') % use)
+
+
+# --------------------------------------------------------------------------
+
+def submit(ui, repo, change=None, dest=None, **opts):
+    '''submit a changelist to the p4 depot
+    then update the local repository state.'''
+
+    dest = ui.expandpath(dest or 'default-push', dest or 'default')
+    client = p4client(ui, repo, dest)
+
+    if change:
+        change = int(change)
+        desc, user, date, files = client.describe(change)
+        nodes = client.parsenodes(desc)
+        client.submit(nodes, change)
+    elif opts['all']:
+        changes = {}
+        pending = client.getpendingdict()
+        for i in pending:
+            i = pending[i]
+            if i != client.SUBMITTED:
+                changes[i] = True
+        changes = changes.keys()
+        changes.sort()
+
+        if len(changes) == 0:
+            raise util.Abort(_('no pending changelists to submit'))
+
+        for c in changes:
+            desc, user, date, files = client.describe(c)
+            nodes = client.parsenodes(desc)
+            client.submit(nodes, c)
+
+    else:
+        raise util.Abort(_('no changelists specified'))
+
+
+def revert(ui, repo, change=None, dest=None, **opts):
+    '''revert a pending changelist and all opened files
+    then update the local repository state.'''
+
+    dest = ui.expandpath(dest or 'default-push', dest or 'default')
+    client = p4client(ui, repo, dest)
+
+    if change:
+        changes = [int(change)]
+    elif opts['all']:
+        changes = {}
+        pending = client.getpendingdict()
+        for i in pending:
+            i = pending[i]
+            if i != client.SUBMITTED:
+                changes[i] = True
+        changes = changes.keys()
+        if len(changes) == 0:
+            raise util.Abort(_('no pending changelists to revert'))
+    else:
+        raise util.Abort(_('no changelists specified'))
+
+    for c in changes:
+        try:
+            desc, user, date, files = client.describe(c)
+        except:
+            files = None
+
+        if files is not None:
+            files = [f[0] for f in files]
+            ui.note(_('reverting: %s\n') % ' '.join(files))
+            client.runs('revert', files=files, abort=False)
+            ui.note(_('deleting: %d\n') % c)
+            client.runs('change -d %d' %c , abort=False)
+
+
+def pending(ui, repo, dest=None, **opts):
+    'report changelists already pushed and pending for submit in p4'
+
+    dest = ui.expandpath(dest or 'default-push', dest or 'default')
+    client = p4client(ui, repo, dest)
+
+    changes = {}
+    pending = client.getpendingdict()
+
+    for i in pending:
+        j = pending[i]
+        if i != client.SUBMITTED:
+            changes.setdefault(j, []).append(i)
+    keys = changes.keys()
+    keys.sort()
+    
+    len = not ui.verbose and 12 or None
+    for i in keys:
+        if i == client.SUBMITTED:
+            state = 'submitted'
+        else:
+            state = str(i)
+        ui.write('%s %s\n' % (state, ' '.join(r[:len] for r in changes[i])))
+
+
+def identify(ui, repo, *args, **opts):
+    '''show p4 and hg revisions for the most recent p4 changelist
+
+    With no revision, print a summary of the most recent revision
+    in the repository that was converted from p4.
+    Otherwise, find the p4 changelist for the revision given.
+    '''
+
+    rev = opts.get('rev')
+    if rev:
+        ctx = repo[rev]
+        extra = ctx.extra()
+        if 'p4' not in extra:
+            raise util.Abort(_('no p4 changelist revision found'))
+        changelist = int(extra['p4'])
+    else:
+        client = p4client(ui, repo, 'p4:///')
+        p4rev, changelist = client.latest()
+        ctx = repo[p4rev]
+
+    num = opts.get('num')
+    doid = opts.get('id')
+    dop4 = opts.get('p4')
+    default = not (num or doid or dop4)
+    output = []
+
+    if default or dop4:
+        output.append(str(changelist))
+    if num:
+        output.append(str(ctx.rev()))
+    if default or doid:
+        output.append(str(ctx))
+
+    ui.write("%s\n" % ' '.join(output))
+
+
+cmdtable = {
+    # 'command-name': (function-call, options-list, help-string)
+    'p4pending':
+        (   pending,
+            [ ],
+            'hg p4pending [p4://server/client]'),
+    'p4revert':
+        (   revert,
+            [ ('a', 'all', None,   _('revert all pending changelists')) ],
+            'hg p4revert [-a] changelist [p4://server/client]'),
+    'p4submit':
+        (   submit,
+            [ ('a', 'all', None,   _('submit all pending changelists')) ],
+            'hg p4submit [-a] changelist [p4://server/client]'),
+    'p4identify':
+        (   identify,
+            [ ('r', 'rev', '',   _('identify the specified revision')),
+              ('n', 'num', None, _('show local revision number')),
+              ('i', 'id',  None, _('show global revision id')),
+              ('p', 'p4',  None, _('show p4 revision number')),
+            ],
+            'hg p4identify [-inp] [-r REV]'
+        )
+}
diff -r b8acd325773e -r 482c1162179a tests/test-perfarce
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test-perfarce	Wed Feb 10 12:51:10 2010 +0000
@@ -0,0 +1,332 @@
+#!/bin/sh
+
+"$TESTDIR/hghave" p4 || exit 80
+
+set -e
+
+HELP=0
+DEBUG=0
+DOSHELL=0
+P4PWD=`pwd`
+P4ROOT=$P4PWD/depot
+KEEP=true
+
+while getopts "k:dr:s" OPT ; do
+   case "$OPT" in
+      d)
+         DEBUG=1
+         ;;
+      k)
+         KEEP=$OPTARG
+         ;;
+      r)
+         P4ROOT=$OPTARG
+         ;;
+      s)
+         DOSHELL=1
+         ;;
+      \?)
+         HELP=1
+         break
+         ;;
+   esac
+done
+
+shift $(($OPTIND-1))
+
+if [ $HELP -ne 0 ] ; then
+   echo "Usage: $0 [-dkrs]"
+   exit 1
+fi
+
+# setup
+export P4ROOT
+P4AUDIT=$P4ROOT/audit; export P4AUDIT
+P4JOURNAL=$P4ROOT/journal; export P4JOURNAL
+P4LOG=$P4ROOT/log; export P4LOG
+P4PORT=localhost:$HGPORT; export P4PORT
+P4DEBUG=$DEBUG; export P4DEBUG
+unset P4CONFIG
+unset P4DIFF
+unset P4EDITOR
+unset P4PASSWD
+unset P4USER
+
+cat <<EOF >$HGRCPATH
+[ui]
+username=perfarce
+
+[paths]
+default = p4://$P4PORT/$P4CLIENT
+
+[extensions]
+convert=
+graphlog=
+perfarce=
+
+[perfarce]
+keep = $KEEP
+EOF
+
+echo % create p4 depot
+
+if [ $DEBUG -ne 0 ] ; then
+    HGDEBUG=--debug
+    filter() { 
+        cat
+    }
+else
+    filter() { 
+        sed -r -e 's,[A-Z][a-z][a-z] [A-Z][a-z][a-z] [0-9][0-9] [0-9]{2}:[0-9]{2}:[0-9]{2} [0-9]{4} \+[0-9]{4},DATE,' \
+               -e 's,[0-9]{4}/[0-9]{2}/[0-9]{2},DATE,' \
+               -e 's,[0-9]{2}:[0-9]{2}:[0-9]{2},TIME,' \
+               -e 's/[a-z]+@/USER@/' \
+               -e 's/[0-9a-f]{40}/HGID40/g' \
+               -e 's/[0-9a-f]{12}/HGID12/g' \
+               -e 's!'$P4PWD'!P4PWD!g'
+    }
+fi
+
+DATA=0
+data() {
+   echo $DATA
+   DATA=$((DATA+1))
+}
+
+rm -fr $P4ROOT $P4PWD/src $P4PWD/dst $P4PWD/new || true
+
+echo % start the p4 server
+[ ! -d $P4ROOT ] && mkdir $P4ROOT
+p4d -f -J off >$P4ROOT/stdout 2>$P4ROOT/stderr &
+
+export DOSHELL
+cleanup() {
+    if [ $DOSHELL -ne 0 ] ; then
+        echo % run bash
+        bash
+    fi
+    echo % stop the p4 server
+    p4 admin stop
+}
+trap cleanup EXIT
+
+# wait for the server to initialize
+while ! p4 ; do
+   sleep 1
+done >/dev/null 2>/dev/null
+
+echo % create a client spec
+mkdir src
+cd src
+
+P4CLIENT=hg-p4-import; export P4CLIENT
+DEPOTPATH=//depot/test-mercurial-push
+
+p4 client -o | sed '/^View:/,$ d' >p4client-$$
+echo View: >>p4client-$$
+echo " $DEPOTPATH/... //$P4CLIENT/..." >>p4client-$$
+p4 client -i <p4client-$$
+rm p4client-$$
+
+#P4CONFIG=.p4config ; export P4CONFIG
+#echo P4PORT=$P4PORT     >$P4CONFIG
+#echo P4CLIENT=$P4CLIENT >>$P4CONFIG
+
+echo % populate the depot
+data > a
+mkdir b
+data > b/c
+p4 add a b/c
+p4 submit -d initial
+p4 tag -l change-one ...
+
+echo % change some files
+p4 edit a
+data >> a
+p4 submit -d "p4 change a"
+
+p4 edit b/c
+data >> b/c
+p4 submit -d "p4 change b/c"
+
+data >>b/d
+p4 add b/d
+p4 submit -d "p4 add b/d"
+p4 tag -l change-four ...
+
+p4 delete b/c
+p4 submit -d "p4 delete b/c"
+
+cd ..
+
+echo % convert
+
+HGRCPATH=$P4PWD/.hgrc ; export HGRCPATH
+
+cat <<EOF >$HGRCPATH
+[ui]
+username=perfarce
+
+[paths]
+default = p4://$P4PORT/$P4CLIENT
+
+[extensions]
+convert=
+graphlog=
+perfarce=
+
+[perfarce]
+keep = $KEEP
+EOF
+
+hg $HGDEBUG convert -s p4 $DEPOTPATH/... dst
+
+echo % now work in hg
+cd dst
+hg $HGDEBUG up
+
+check_contents() {
+   hg glog --template '{rev} "{desc|firstline}" files: {files}\n' | filter
+   md5sum $(hg manifest | grep -v .hgtags)
+}
+check_contents
+
+echo % nothing incoming
+hg $HGDEBUG incoming
+
+echo % nothing outgoing
+hg $HGDEBUG outgoing
+
+echo % change some files in hg
+data >> a
+hg $HGDEBUG commit -m "hg change a"
+
+data >> b/e
+hg $HGDEBUG add b/e
+hg $HGDEBUG ci -m "hg add b/e"
+
+data >>b/d
+hg $HGDEBUG commit -m "hg change b/d"
+
+echo % id
+hg $HGDEBUG p4identify | filter
+
+echo % outgoing
+hg $HGDEBUG outgoing | filter
+
+echo % skip a changelist
+p4 change -o | sed -r 's/<.+>/test/' >change.$$
+p4 change -d $(p4 change -i <change.$$ | awk '/created/ { print $2 }')
+rm change.$$
+
+echo % push
+hg $HGDEBUG push
+echo % p4pending
+hg $HGDEBUG p4pending | filter
+
+echo % pending
+hg $HGDEBUG p4pending | filter
+
+echo % submit
+hg $HGDEBUG p4submit -a
+echo % echo % p4pending
+hg $HGDEBUG p4pending | filter
+
+echo % pending
+hg $HGDEBUG p4pending | filter
+
+echo % outgoing
+hg $HGDEBUG outgoing | filter
+
+echo % p4 results
+cd ../src
+p4 changes       | filter
+p4 describe -s 7 | filter
+
+p4 tag -l change-seven ...
+
+cd ../dst
+
+echo % incoming
+hg $HGDEBUG incoming | filter
+
+echo % pull
+hg $HGDEBUG pull --update
+check_contents
+
+echo % push submit
+data >a
+data >c
+hg add c
+hg ci -m "ac" a c
+hg $HGDEBUG push --submit
+
+echo % echo % p4pending
+hg $HGDEBUG p4pending | filter
+
+echo % hg tag
+hg $HGDEBUG pull --update
+hg tag -r 5 tag-created-in-hg
+
+hg $HGDEBUG push --submit
+
+echo % copy files
+hg cp a aa
+hg $HGDEBUG commit -m "hg copy a"
+
+hg $HGDEBUG push -r tip --submit
+
+echo % move files
+hg mv c cc
+hg $HGDEBUG commit -m "hg move c"
+
+hg $HGDEBUG push -r tip  --submit
+
+echo % move dir
+hg mv b bb
+hg $HGDEBUG commit -m "hg move b"
+
+hg $HGDEBUG push -r tip  --submit
+
+SERVER=$(p4 info|sed -r -e '/Server version/!d' -e 's,[^/]+/[^.]+/([^/]+)/.+$,\1,' -e 's/\..$//')
+if [ $SERVER -ge 2009 ] ; then
+    cd ../src
+    p4 sync | filter
+
+    echo % new style move
+    data >f
+    p4 add f
+    p4 submit -d new-f
+    
+    p4 edit f
+    p4 move f g
+    p4 submit -d move-f-to-g
+
+    cd ../dst
+    hg $HGDEBUG pull --update
+    check_contents
+fi
+
+if [ $SERVER -ge 2005 ] ; then
+    echo % overlay mappings
+    cd ../src
+
+    p4 sync | filter
+    mkdir h
+    data > h/j
+    p4 add h/j
+    p4 submit -d "p4 add h/j"
+
+    p4 client -o >p4client-$$
+    echo " -$DEPOTPATH/h/... //$P4CLIENT/h/..." >>p4client-$$
+    echo " +$DEPOTPATH/h/... //$P4CLIENT/..." >>p4client-$$
+    p4 client -i <p4client-$$
+
+    p4 sync -f | filter
+
+    cd ../dst
+    hg $HGDEBUG pull --update
+    check_contents
+fi
+
+# EOF



More information about the Mercurial-devel mailing list