hg.py
477 lines
| 16.9 KiB
| text/x-python
|
PythonLexer
|
r8250 | # hg.py - hg backend for convert extension | ||
# | ||||
# Copyright 2005-2009 Matt Mackall <mpm@selenic.com> and others | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
|
r10263 | # GNU General Public License version 2 or any later version. | ||
|
r4536 | |||
|
r5556 | # Notes for hg->hg conversion: | ||
# | ||||
# * Old versions of Mercurial didn't trim the whitespace from the ends | ||||
# of commit messages, but new versions do. Changesets created by | ||||
# those older versions, then converted, may thus have different | ||||
# hashes for changesets that are otherwise identical. | ||||
# | ||||
|
r8596 | # * Using "--config convert.hg.saverev=true" will make the source | ||
# identifier to be stored in the converted revision. This will cause | ||||
# the converted revision to have a different identity than the | ||||
# source. | ||||
|
r5013 | |||
|
r8693 | import os, time, cStringIO | ||
|
r5014 | from mercurial.i18n import _ | ||
|
r6211 | from mercurial.node import bin, hex, nullid | ||
|
r19891 | from mercurial import hg, util, context, bookmarks, error, scmutil | ||
|
r4536 | |||
|
r5013 | from common import NoRepo, commit, converter_source, converter_sink | ||
|
r4536 | |||
|
r20372 | import re | ||
sha1re = re.compile(r'\b[0-9a-f]{6,40}\b') | ||||
|
r5013 | class mercurial_sink(converter_sink): | ||
|
r4763 | def __init__(self, ui, path): | ||
|
r5440 | converter_sink.__init__(self, ui, path) | ||
|
r5173 | self.branchnames = ui.configbool('convert', 'hg.usebranchnames', True) | ||
self.clonebranches = ui.configbool('convert', 'hg.clonebranches', False) | ||||
|
r5260 | self.tagsbranch = ui.config('convert', 'hg.tagsbranch', 'default') | ||
|
r5173 | self.lastbranch = None | ||
|
r5441 | if os.path.isdir(path) and len(os.listdir(path)) > 0: | ||
try: | ||||
self.repo = hg.repository(self.ui, path) | ||||
|
r5918 | if not self.repo.local(): | ||
|
r10938 | raise NoRepo(_('%s is not a local Mercurial repository') | ||
% path) | ||||
|
r7637 | except error.RepoError, err: | ||
|
r8206 | ui.traceback() | ||
|
r5441 | raise NoRepo(err.args[0]) | ||
else: | ||||
try: | ||||
ui.status(_('initializing destination %s repository\n') % path) | ||||
self.repo = hg.repository(self.ui, path, create=True) | ||||
|
r5918 | if not self.repo.local(): | ||
|
r10938 | raise NoRepo(_('%s is not a local Mercurial repository') | ||
% path) | ||||
|
r5441 | self.created.append(path) | ||
|
r7875 | except error.RepoError: | ||
|
r8206 | ui.traceback() | ||
|
r10939 | raise NoRepo(_("could not create hg repository %s as sink") | ||
|
r10938 | % path) | ||
|
r5014 | self.lock = None | ||
self.wlock = None | ||||
|
r5378 | self.filemapmode = False | ||
|
r5014 | |||
def before(self): | ||||
|
r9467 | self.ui.debug('run hg sink pre-conversion action\n') | ||
|
r5052 | self.wlock = self.repo.wlock() | ||
|
r5014 | self.lock = self.repo.lock() | ||
def after(self): | ||||
|
r9467 | self.ui.debug('run hg sink post-conversion action\n') | ||
|
r10086 | if self.lock: | ||
self.lock.release() | ||||
if self.wlock: | ||||
self.wlock.release() | ||||
|
r4536 | |||
|
r5011 | def revmapfile(self): | ||
|
r15069 | return self.repo.join("shamap") | ||
|
r4536 | |||
|
r4589 | def authorfile(self): | ||
|
r15069 | return self.repo.join("authormap") | ||
|
r4589 | |||
|
r5934 | def setbranch(self, branch, pbranches): | ||
if not self.clonebranches: | ||||
|
r5173 | return | ||
|
r5934 | setbranch = (branch != self.lastbranch) | ||
|
r5173 | self.lastbranch = branch | ||
if not branch: | ||||
branch = 'default' | ||||
|
r5934 | pbranches = [(b[0], b[1] and b[1] or 'default') for b in pbranches] | ||
pbranch = pbranches and pbranches[0][1] or 'default' | ||||
|
r5173 | |||
branchpath = os.path.join(self.path, branch) | ||||
|
r5934 | if setbranch: | ||
self.after() | ||||
try: | ||||
self.repo = hg.repository(self.ui, branchpath) | ||||
|
r16689 | except Exception: | ||
|
r5173 | self.repo = hg.repository(self.ui, branchpath, create=True) | ||
|
r5934 | self.before() | ||
# pbranches may bring revisions from other branches (merge parents) | ||||
# Make sure we have them, or pull them. | ||||
missings = {} | ||||
for b in pbranches: | ||||
try: | ||||
self.repo.lookup(b[0]) | ||||
|
r16689 | except Exception: | ||
|
r5934 | missings.setdefault(b[1], []).append(b[0]) | ||
|
r6210 | |||
|
r5934 | if missings: | ||
self.after() | ||||
|
r18373 | for pbranch, heads in sorted(missings.iteritems()): | ||
|
r5934 | pbranchpath = os.path.join(self.path, pbranch) | ||
|
r14556 | prepo = hg.peer(self.ui, {}, pbranchpath) | ||
|
r5934 | self.ui.note(_('pulling from %s into %s\n') % (pbranch, branch)) | ||
self.repo.pull(prepo, [prepo.lookup(h) for h in heads]) | ||||
self.before() | ||||
|
r5173 | |||
|
r21076 | def _rewritetags(self, source, revmap, data): | ||
|
r8693 | fp = cStringIO.StringIO() | ||
for line in data.splitlines(): | ||||
s = line.split(' ', 1) | ||||
if len(s) != 2: | ||||
continue | ||||
revid = revmap.get(source.lookuprev(s[0])) | ||||
if not revid: | ||||
continue | ||||
|
r21076 | fp.write('%s %s\n' % (revid, s[1])) | ||
|
r8693 | return fp.getvalue() | ||
|
r22300 | def putcommit(self, files, copies, parents, commit, source, revmap, full): | ||
|
r6717 | files = dict(files) | ||
def getfilectx(repo, memctx, f): | ||||
|
r22300 | try: | ||
v = files[f] | ||||
except KeyError: | ||||
return None | ||||
|
r11134 | data, mode = source.getfile(f, v) | ||
|
r22296 | if data is None: | ||
return None | ||||
|
r8693 | if f == '.hgtags': | ||
|
r21076 | data = self._rewritetags(source, revmap, data) | ||
|
r21689 | return context.memfilectx(self.repo, f, data, 'l' in mode, | ||
'x' in mode, copies.get(f)) | ||||
|
r6717 | |||
|
r4536 | pl = [] | ||
for p in parents: | ||||
|
r6717 | if p not in pl: | ||
|
r4536 | pl.append(p) | ||
parents = pl | ||||
|
r5378 | nparents = len(parents) | ||
if self.filemapmode and nparents == 1: | ||||
m1node = self.repo.changelog.read(bin(parents[0]))[0] | ||||
parent = parents[0] | ||||
|
r4536 | |||
|
r10282 | if len(parents) < 2: | ||
parents.append(nullid) | ||||
if len(parents) < 2: | ||||
parents.append(nullid) | ||||
|
r4536 | p2 = parents.pop(0) | ||
text = commit.desc | ||||
|
r20372 | |||
sha1s = re.findall(sha1re, text) | ||||
for sha1 in sha1s: | ||||
oldrev = source.lookuprev(sha1) | ||||
newrev = revmap.get(oldrev) | ||||
if newrev is not None: | ||||
text = text.replace(sha1, newrev[:len(sha1)]) | ||||
|
r5439 | extra = commit.extra.copy() | ||
|
r21765 | |||
for label in ('source', 'transplant_source', 'rebase_source'): | ||||
node = extra.get(label) | ||||
if node is None: | ||||
continue | ||||
# Only transplant stores its reference in binary | ||||
if label == 'transplant_source': | ||||
node = hex(node) | ||||
newrev = revmap.get(node) | ||||
if newrev is not None: | ||||
if label == 'transplant_source': | ||||
newrev = bin(newrev) | ||||
extra[label] = newrev | ||||
|
r5038 | if self.branchnames and commit.branch: | ||
|
r4873 | extra['branch'] = commit.branch | ||
if commit.rev: | ||||
extra['convert_revision'] = commit.rev | ||||
|
r4957 | |||
|
r4536 | while parents: | ||
p1 = p2 | ||||
p2 = parents.pop(0) | ||||
|
r22300 | fileset = set(files) | ||
if full: | ||||
|
r22360 | fileset.update(self.repo[p1]) | ||
fileset.update(self.repo[p2]) | ||||
|
r22300 | ctx = context.memctx(self.repo, (p1, p2), text, fileset, | ||
|
r10282 | getfilectx, commit.author, commit.date, extra) | ||
|
r7874 | self.repo.commitctx(ctx) | ||
|
r4536 | text = "(octopus merge fixup)\n" | ||
|
r6217 | p2 = hex(self.repo.changelog.tip()) | ||
|
r4536 | |||
|
r5378 | if self.filemapmode and nparents == 1: | ||
man = self.repo.manifest | ||||
mnode = self.repo.changelog.read(bin(p2))[0] | ||||
|
r11673 | closed = 'close' in commit.extra | ||
if not closed and not man.cmp(m1node, man.revision(mnode)): | ||||
|
r8611 | self.ui.status(_("filtering out empty revision\n")) | ||
|
r15193 | self.repo.rollback(force=True) | ||
|
r5378 | return parent | ||
|
r4536 | return p2 | ||
def puttags(self, tags): | ||||
|
r7877 | try: | ||
parentctx = self.repo[self.tagsbranch] | ||||
tagparent = parentctx.node() | ||||
except error.RepoError: | ||||
parentctx = None | ||||
tagparent = nullid | ||||
|
r4536 | |||
|
r20376 | oldlines = set() | ||
for branch, heads in self.repo.branchmap().iteritems(): | ||||
for h in heads: | ||||
if '.hgtags' in self.repo[h]: | ||||
oldlines.update( | ||||
set(self.repo[h]['.hgtags'].data().splitlines(True))) | ||||
oldlines = sorted(list(oldlines)) | ||||
|
r4536 | |||
|
r8209 | newlines = sorted([("%s %s\n" % (tags[tag], tag)) for tag in tags]) | ||
|
r7877 | if newlines == oldlines: | ||
|
r9431 | return None, None | ||
|
r20377 | |||
# if the old and new tags match, then there is nothing to update | ||||
oldtags = set() | ||||
newtags = set() | ||||
for line in oldlines: | ||||
s = line.strip().split(' ', 1) | ||||
if len(s) != 2: | ||||
continue | ||||
oldtags.add(s[1]) | ||||
for line in newlines: | ||||
s = line.strip().split(' ', 1) | ||||
if len(s) != 2: | ||||
continue | ||||
if s[1] not in oldtags: | ||||
newtags.add(s[1].strip()) | ||||
if not newtags: | ||||
return None, None | ||||
|
r7877 | data = "".join(newlines) | ||
def getfilectx(repo, memctx, f): | ||||
|
r21689 | return context.memfilectx(repo, f, data, False, False, None) | ||
|
r6717 | |||
|
r7877 | self.ui.status(_("updating tags\n")) | ||
date = "%s 0" % int(time.mktime(time.gmtime())) | ||||
extra = {'branch': self.tagsbranch} | ||||
ctx = context.memctx(self.repo, (tagparent, None), "update tags", | ||||
[".hgtags"], getfilectx, "convert-repo", date, | ||||
extra) | ||||
self.repo.commitctx(ctx) | ||||
|
r9431 | return hex(self.repo.changelog.tip()), hex(tagparent) | ||
|
r5013 | |||
|
r5378 | def setfilemapmode(self, active): | ||
self.filemapmode = active | ||||
|
r13746 | def putbookmarks(self, updatedbookmark): | ||
if not len(updatedbookmark): | ||||
return | ||||
self.ui.status(_("updating bookmarks\n")) | ||||
|
r17922 | destmarks = self.repo._bookmarks | ||
|
r13746 | for bookmark in updatedbookmark: | ||
|
r17922 | destmarks[bookmark] = bin(updatedbookmark[bookmark]) | ||
destmarks.write() | ||||
|
r13746 | |||
|
r21635 | def hascommitfrommap(self, rev): | ||
# the exact semantics of clonebranches is unclear so we can't say no | ||||
return rev in self.repo or self.clonebranches | ||||
|
r21634 | def hascommitforsplicemap(self, rev): | ||
|
r16686 | if rev not in self.repo and self.clonebranches: | ||
|
r16162 | raise util.Abort(_('revision %s not found in destination ' | ||
|
r16106 | 'repository (lookups with clonebranches=true ' | ||
'are not implemented)') % rev) | ||||
return rev in self.repo | ||||
|
r13746 | |||
|
r5013 | class mercurial_source(converter_source): | ||
def __init__(self, ui, path, rev=None): | ||||
converter_source.__init__(self, ui, path, rev) | ||||
|
r7231 | self.ignoreerrors = ui.configbool('convert', 'hg.ignoreerrors', False) | ||
|
r8456 | self.ignored = set() | ||
|
r7815 | self.saverev = ui.configbool('convert', 'hg.saverev', False) | ||
|
r5358 | try: | ||
self.repo = hg.repository(self.ui, path) | ||||
|
r5437 | # try to provoke an exception if this isn't really a hg | ||
# repo, but some other bogus compatible-looking url | ||||
|
r5522 | if not self.repo.local(): | ||
|
r16687 | raise error.RepoError | ||
|
r7637 | except error.RepoError: | ||
|
r8206 | ui.traceback() | ||
|
r10939 | raise NoRepo(_("%s is not a local Mercurial repository") % path) | ||
|
r5013 | self.lastrev = None | ||
self.lastctx = None | ||||
|
r22299 | self._changescache = None, None | ||
|
r5554 | self.convertfp = None | ||
|
r6885 | # Restrict converted revisions to startrev descendants | ||
startnode = ui.config('convert', 'hg.startrev') | ||||
|
r19891 | hgrevs = ui.config('convert', 'hg.revs') | ||
if hgrevs is None: | ||||
if startnode is not None: | ||||
try: | ||||
startnode = self.repo.lookup(startnode) | ||||
except error.RepoError: | ||||
raise util.Abort(_('%s is not a valid start revision') | ||||
% startnode) | ||||
startrev = self.repo.changelog.rev(startnode) | ||||
children = {startnode: 1} | ||||
for r in self.repo.changelog.descendants([startrev]): | ||||
children[self.repo.changelog.node(r)] = 1 | ||||
self.keep = children.__contains__ | ||||
else: | ||||
self.keep = util.always | ||||
if rev: | ||||
self._heads = [self.repo[rev].node()] | ||||
else: | ||||
self._heads = self.repo.heads() | ||||
|
r6885 | else: | ||
|
r19891 | if rev or startnode is not None: | ||
raise util.Abort(_('hg.revs cannot be combined with ' | ||||
'hg.startrev or --rev')) | ||||
nodes = set() | ||||
parents = set() | ||||
for r in scmutil.revrange(self.repo, [hgrevs]): | ||||
ctx = self.repo[r] | ||||
nodes.add(ctx.node()) | ||||
parents.update(p.node() for p in ctx.parents()) | ||||
self.keep = nodes.__contains__ | ||||
self._heads = nodes - parents | ||||
|
r5013 | |||
def changectx(self, rev): | ||||
if self.lastrev != rev: | ||||
|
r6747 | self.lastctx = self.repo[rev] | ||
|
r5013 | self.lastrev = rev | ||
return self.lastctx | ||||
|
r6885 | def parents(self, ctx): | ||
|
r9531 | return [p for p in ctx.parents() if p and self.keep(p.node())] | ||
|
r6885 | |||
|
r5013 | def getheads(self): | ||
|
r19890 | return [hex(h) for h in self._heads if self.keep(h)] | ||
|
r5013 | |||
def getfile(self, name, rev): | ||||
try: | ||||
|
r11134 | fctx = self.changectx(rev)[name] | ||
return fctx.data(), fctx.flags() | ||||
|
r22296 | except error.LookupError: | ||
return None, None | ||||
|
r5013 | |||
|
r22300 | def getchanges(self, rev, full): | ||
|
r5013 | ctx = self.changectx(rev) | ||
|
r6885 | parents = self.parents(ctx) | ||
|
r22300 | if full or not parents: | ||
|
r22299 | files = copyfiles = ctx.manifest() | ||
|
r22300 | if parents: | ||
|
r22299 | if self._changescache[0] == rev: | ||
m, a, r = self._changescache[1] | ||||
else: | ||||
m, a, r = self.repo.status(parents[0].node(), ctx.node())[:3] | ||||
|
r22300 | if not full: | ||
files = m + a + r | ||||
|
r22299 | copyfiles = m + a | ||
# getcopies() is also run for roots and before filtering so missing | ||||
# revlogs are detected early | ||||
copies = self.getcopies(ctx, parents, copyfiles) | ||||
changes = [(f, rev) for f in files if f not in self.ignored] | ||||
changes.sort() | ||||
return changes, copies | ||||
|
r5013 | |||
|
r9532 | def getcopies(self, ctx, parents, files): | ||
|
r5013 | copies = {} | ||
|
r5280 | for name in files: | ||
|
r7231 | if name in self.ignored: | ||
continue | ||||
|
r5013 | try: | ||
|
r19457 | copysource, _copynode = ctx.filectx(name).renamed() | ||
if copysource in self.ignored: | ||||
|
r7231 | continue | ||
|
r9532 | # Ignore copy sources not in parent revisions | ||
found = False | ||||
for p in parents: | ||||
if copysource in p: | ||||
found = True | ||||
break | ||||
if not found: | ||||
continue | ||||
|
r7231 | copies[name] = copysource | ||
|
r5013 | except TypeError: | ||
pass | ||||
|
r7633 | except error.LookupError, e: | ||
|
r7231 | if not self.ignoreerrors: | ||
raise | ||||
|
r8456 | self.ignored.add(name) | ||
|
r7231 | self.ui.warn(_('ignoring: %s\n') % e) | ||
|
r5013 | return copies | ||
|
r5143 | |||
|
r5013 | def getcommit(self, rev): | ||
ctx = self.changectx(rev) | ||||
|
r9531 | parents = [p.hex() for p in self.parents(ctx)] | ||
|
r5556 | if self.saverev: | ||
crev = rev | ||||
else: | ||||
crev = None | ||||
|
r16514 | return commit(author=ctx.user(), | ||
date=util.datestr(ctx.date(), '%Y-%m-%d %H:%M:%S %1%2'), | ||||
|
r5556 | desc=ctx.description(), rev=crev, parents=parents, | ||
|
r8690 | branch=ctx.branch(), extra=ctx.extra(), | ||
sortkey=ctx.rev()) | ||||
|
r5013 | |||
def gettags(self): | ||||
|
r21498 | # This will get written to .hgtags, filter non global tags out. | ||
tags = [t for t in self.repo.tagslist() | ||||
if self.repo.tagtype(t[0]) == 'global'] | ||||
|
r6885 | return dict([(name, hex(node)) for name, node in tags | ||
if self.keep(node)]) | ||||
|
r5379 | |||
def getchangedfiles(self, rev, i): | ||||
ctx = self.changectx(rev) | ||||
|
r6885 | parents = self.parents(ctx) | ||
if not parents and i is None: | ||||
i = 0 | ||||
changes = [], ctx.manifest().keys(), [] | ||||
else: | ||||
i = i or 0 | ||||
|
r9531 | changes = self.repo.status(parents[i].node(), ctx.node())[:3] | ||
|
r7231 | changes = [[f for f in l if f not in self.ignored] for l in changes] | ||
|
r5379 | |||
if i == 0: | ||||
self._changescache = (rev, changes) | ||||
return changes[0] + changes[1] + changes[2] | ||||
|
r5554 | def converted(self, rev, destrev): | ||
if self.convertfp is None: | ||||
|
r15069 | self.convertfp = open(self.repo.join('shamap'), 'a') | ||
|
r5554 | self.convertfp.write('%s %s\n' % (destrev, rev)) | ||
self.convertfp.flush() | ||||
|
r5805 | |||
def before(self): | ||||
|
r9467 | self.ui.debug('run hg source pre-conversion action\n') | ||
|
r5805 | |||
def after(self): | ||||
|
r9467 | self.ui.debug('run hg source post-conversion action\n') | ||
|
r8691 | |||
def hasnativeorder(self): | ||||
return True | ||||
|
r8693 | |||
|
r18819 | def hasnativeclose(self): | ||
return True | ||||
|
r8693 | def lookuprev(self, rev): | ||
try: | ||||
return hex(self.repo.lookup(rev)) | ||||
except error.RepoError: | ||||
return None | ||||
|
r13757 | |||
def getbookmarks(self): | ||||
return bookmarks.listbookmarks(self.repo) | ||||
|
r19120 | |||
|
r20373 | def checkrevformat(self, revstr, mapname='splicemap'): | ||
|
r19120 | """ Mercurial, revision string is a 40 byte hex """ | ||
|
r20373 | self.checkhexformat(revstr, mapname) | ||