[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