1136 lines · 43.2 KB · Python
RawDownload
# Subversion 1.4/1.5 Python API backend## Copyright(C) 2007 Daniel Holth et al import osimport reimport sysimport cPickle as pickleimport tempfileimport urllib from mercurial import strutil, util, encodingfrom mercurial.i18n import _ # Subversion stuff. Works best with very recent Python SVN bindings# e.g. SVN 1.5 or backports. Thanks to the bzr folks for enhancing# these bindings. from cStringIO import StringIO from common import NoRepo, MissingTool, commit, encodeargs, decodeargsfrom common import commandline, converter_source, converter_sink, mapfile try:    from svn.core import SubversionException, Pool    import svn    import svn.client    import svn.core    import svn.ra    import svn.delta    import transport    import warnings    warnings.filterwarnings('ignore',            module='svn.core',            category=DeprecationWarning) except ImportError:    pass class SvnPathNotFound(Exception):    pass def geturl(path):    try:        return svn.client.url_from_path(svn.core.svn_path_canonicalize(path))    except SubversionException:        pass    if os.path.isdir(path):        path = os.path.normpath(os.path.abspath(path))        if os.name == 'nt':            path = '/' + util.normpath(path)        # Module URL is later compared with the repository URL returned        # by svn API, which is UTF-8.        path = encoding.tolocal(path)        return 'file://%s' % urllib.quote(path)    return path def optrev(number):    optrev = svn.core.svn_opt_revision_t()    optrev.kind = svn.core.svn_opt_revision_number    optrev.value.number = number    return optrev class changedpath(object):    def __init__(self, p):        self.copyfrom_path = p.copyfrom_path        self.copyfrom_rev = p.copyfrom_rev        self.action = p.action def get_log_child(fp, url, paths, start, end, limit=0, discover_changed_paths=True,                    strict_node_history=False):    protocol = -1    def receiver(orig_paths, revnum, author, date, message, pool):        if orig_paths is not None:            for k, v in orig_paths.iteritems():                orig_paths[k] = changedpath(v)        pickle.dump((orig_paths, revnum, author, date, message),                    fp, protocol)     try:        # Use an ra of our own so that our parent can consume        # our results without confusing the server.        t = transport.SvnRaTransport(url=url)        svn.ra.get_log(t.ra, paths, start, end, limit,                       discover_changed_paths,                       strict_node_history,                       receiver)    except SubversionException, (inst, num):        pickle.dump(num, fp, protocol)    except IOError:        # Caller may interrupt the iteration        pickle.dump(None, fp, protocol)    else:        pickle.dump(None, fp, protocol)    fp.close()    # With large history, cleanup process goes crazy and suddenly    # consumes *huge* amount of memory. The output file being closed,    # there is no need for clean termination.    os._exit(0) def debugsvnlog(ui, **opts):    """Fetch SVN log in a subprocess and channel them back to parent to    avoid memory collection issues.    """    util.set_binary(sys.stdin)    util.set_binary(sys.stdout)    args = decodeargs(sys.stdin.read())    get_log_child(sys.stdout, *args) class logstream(object):    """Interruptible revision log iterator."""    def __init__(self, stdout):        self._stdout = stdout     def __iter__(self):        while True:            entry = pickle.load(self._stdout)            try:                orig_paths, revnum, author, date, message = entry            except:                if entry is None:                    break                raise SubversionException("child raised exception", entry)            yield entry     def close(self):        if self._stdout:            self._stdout.close()            self._stdout = None  # Check to see if the given path is a local Subversion repo. Verify this by# looking for several svn-specific files and directories in the given# directory.def filecheck(path, proto):    for x in ('locks', 'hooks', 'format', 'db', ):        if not os.path.exists(os.path.join(path, x)):            return False    return True # Check to see if a given path is the root of an svn repo over http. We verify# this by requesting a version-controlled URL we know can't exist and looking# for the svn-specific "not found" XML.def httpcheck(path, proto):    return ('<m:human-readable errcode="160013">' in            urllib.urlopen('%s://%s/!svn/ver/0/.svn' % (proto, path)).read()) protomap = {'http': httpcheck,            'https': httpcheck,            'file': filecheck,            }def issvnurl(url):    try:        proto, path = url.split('://', 1)        path = urllib.url2pathname(path)    except ValueError:        proto = 'file'        path = os.path.abspath(url)    path = path.replace(os.sep, '/')    check = protomap.get(proto, lambda p, p2: False)    while '/' in path:        if check(path, proto):            return True        path = path.rsplit('/', 1)[0]    return False # SVN conversion code stolen from bzr-svn and tailor## Subversion looks like a versioned filesystem, branches structures# are defined by conventions and not enforced by the tool. First,# we define the potential branches (modules) as "trunk" and "branches"# children directories. Revisions are then identified by their# module and revision number (and a repository identifier).## The revision graph is really a tree (or a forest). By default, a# revision parent is the previous revision in the same module. If the# module directory is copied/moved from another module then the# revision is the module root and its parent the source revision in# the parent module. A revision has at most one parent.#class svn_source(converter_source):    def __init__(self, ui, url, rev=None):        super(svn_source, self).__init__(ui, url, rev=rev)         if not (url.startswith('svn://') or url.startswith('svn+ssh://') or                (os.path.exists(url) and                 os.path.exists(os.path.join(url, '.svn'))) or                issvnurl(url)):            raise NoRepo("%s does not look like a Subversion repo" % url)         try:            SubversionException        except NameError:            raise MissingTool(_('Subversion python bindings could not be loaded'))         try:            version = svn.core.SVN_VER_MAJOR, svn.core.SVN_VER_MINOR            if version < (1, 4):                raise MissingTool(_('Subversion python bindings %d.%d found, '                                    '1.4 or later required') % version)        except AttributeError:            raise MissingTool(_('Subversion python bindings are too old, 1.4 '                                'or later required'))         self.lastrevs = {}         latest = None        try:            # Support file://path@rev syntax. Useful e.g. to convert            # deleted branches.            at = url.rfind('@')            if at >= 0:                latest = int(url[at+1:])                url = url[:at]        except ValueError:            pass        self.url = geturl(url)        self.encoding = 'UTF-8' # Subversion is always nominal UTF-8        try:            self.transport = transport.SvnRaTransport(url=self.url)            self.ra = self.transport.ra            self.ctx = self.transport.client            self.baseurl = svn.ra.get_repos_root(self.ra)            # Module is either empty or a repository path starting with            # a slash and not ending with a slash.            self.module = urllib.unquote(self.url[len(self.baseurl):])            self.prevmodule = None            self.rootmodule = self.module            self.commits = {}            self.paths = {}            self.uuid = svn.ra.get_uuid(self.ra)        except SubversionException:            ui.traceback()            raise NoRepo("%s does not look like a Subversion repo" % self.url)         if rev:            try:                latest = int(rev)            except ValueError:                raise util.Abort(_('svn: revision %s is not an integer') % rev)         self.startrev = self.ui.config('convert', 'svn.startrev', default=0)        try:            self.startrev = int(self.startrev)            if self.startrev < 0:                self.startrev = 0        except ValueError:            raise util.Abort(_('svn: start revision %s is not an integer')                             % self.startrev)         self.head = self.latest(self.module, latest)        if not self.head:            raise util.Abort(_('no revision found in module %s')                             % self.module)        self.last_changed = self.revnum(self.head)         self._changescache = None         if os.path.exists(os.path.join(url, '.svn/entries')):            self.wc = url        else:            self.wc = None        self.convertfp = None     def setrevmap(self, revmap):        lastrevs = {}        for revid in revmap.iterkeys():            uuid, module, revnum = self.revsplit(revid)            lastrevnum = lastrevs.setdefault(module, revnum)            if revnum > lastrevnum:                lastrevs[module] = revnum        self.lastrevs = lastrevs     def exists(self, path, optrev):        try:            svn.client.ls(self.url.rstrip('/') + '/' + urllib.quote(path),                                 optrev, False, self.ctx)            return True        except SubversionException:            return False     def getheads(self):         def isdir(path, revnum):            kind = self._checkpath(path, revnum)            return kind == svn.core.svn_node_dir         def getcfgpath(name, rev):            cfgpath = self.ui.config('convert', 'svn.' + name)            if cfgpath is not None and cfgpath.strip() == '':                return None            path = (cfgpath or name).strip('/')            if not self.exists(path, rev):                if cfgpath:                    raise util.Abort(_('expected %s to be at %r, but not found')                                 % (name, path))                return None            self.ui.note(_('found %s at %r\n') % (name, path))            return path         rev = optrev(self.last_changed)        oldmodule = ''        trunk = getcfgpath('trunk', rev)        self.tags = getcfgpath('tags', rev)        branches = getcfgpath('branches', rev)         # If the project has a trunk or branches, we will extract heads        # from them. We keep the project root otherwise.        if trunk:            oldmodule = self.module or ''            self.module += '/' + trunk            self.head = self.latest(self.module, self.last_changed)            if not self.head:                raise util.Abort(_('no revision found in module %s')                                 % self.module)         # First head in the list is the module's head        self.heads = [self.head]        if self.tags is not None:            self.tags = '%s/%s' % (oldmodule , (self.tags or 'tags'))         # Check if branches bring a few more heads to the list        if branches:            rpath = self.url.strip('/')            branchnames = svn.client.ls(rpath + '/' + urllib.quote(branches),                                        rev, False, self.ctx)            for branch in branchnames.keys():                module = '%s/%s/%s' % (oldmodule, branches, branch)                if not isdir(module, self.last_changed):                    continue                brevid = self.latest(module, self.last_changed)                if not brevid:                    self.ui.note(_('ignoring empty branch %s\n') % branch)                    continue                self.ui.note(_('found branch %s at %d\n') %                             (branch, self.revnum(brevid)))                self.heads.append(brevid)         if self.startrev and self.heads:            if len(self.heads) > 1:                raise util.Abort(_('svn: start revision is not supported '                                   'with more than one branch'))            revnum = self.revnum(self.heads[0])            if revnum < self.startrev:                raise util.Abort(_('svn: no revision found after start revision %d')                                 % self.startrev)         return self.heads     def getfile(self, file, rev):        data, mode = self._getfile(file, rev)        self.modecache[(file, rev)] = mode        return data     def getmode(self, file, rev):        return self.modecache[(file, rev)]     def getchanges(self, rev):        if self._changescache and self._changescache[0] == rev:            return self._changescache[1]        self._changescache = None        self.modecache = {}        (paths, parents) = self.paths[rev]        if parents:            files, copies = self.expandpaths(rev, paths, parents)        else:            # Perform a full checkout on roots            uuid, module, revnum = self.revsplit(rev)            entries = svn.client.ls(self.baseurl + urllib.quote(module),                                    optrev(revnum), True, self.ctx)            files = [n for n,e in entries.iteritems()                     if e.kind == svn.core.svn_node_file]            copies = {}         files.sort()        files = zip(files, [rev] * len(files))         # caller caches the result, so free it here to release memory        del self.paths[rev]        return (files, copies)     def getchangedfiles(self, rev, i):        changes = self.getchanges(rev)        self._changescache = (rev, changes)        return [f[0] for f in changes[0]]     def getcommit(self, rev):        if rev not in self.commits:            uuid, module, revnum = self.revsplit(rev)            self.module = module            self.reparent(module)            # We assume that:            # - requests for revisions after "stop" come from the            # revision graph backward traversal. Cache all of them            # down to stop, they will be used eventually.            # - requests for revisions before "stop" come to get            # isolated branches parents. Just fetch what is needed.            stop = self.lastrevs.get(module, 0)            if revnum < stop:                stop = revnum + 1            self._fetch_revisions(revnum, stop)        commit = self.commits[rev]        # caller caches the result, so free it here to release memory        del self.commits[rev]        return commit     def gettags(self):        tags = {}        if self.tags is None:            return tags         # svn tags are just a convention, project branches left in a        # 'tags' directory. There is no other relationship than        # ancestry, which is expensive to discover and makes them hard        # to update incrementally.  Worse, past revisions may be        # referenced by tags far away in the future, requiring a deep        # history traversal on every calculation.  Current code        # performs a single backward traversal, tracking moves within        # the tags directory (tag renaming) and recording a new tag        # everytime a project is copied from outside the tags        # directory. It also lists deleted tags, this behaviour may        # change in the future.        pendings = []        tagspath = self.tags        start = svn.ra.get_latest_revnum(self.ra)        try:            for entry in self._getlog([self.tags], start, self.startrev):                origpaths, revnum, author, date, message = entry                copies = [(e.copyfrom_path, e.copyfrom_rev, p) for p, e                          in origpaths.iteritems() if e.copyfrom_path]                # Apply moves/copies from more specific to general                copies.sort(reverse=True)                 srctagspath = tagspath                if copies and copies[-1][2] == tagspath:                    # Track tags directory moves                    srctagspath = copies.pop()[0]                 for source, sourcerev, dest in copies:                    if not dest.startswith(tagspath + '/'):                        continue                    for tag in pendings:                        if tag[0].startswith(dest):                            tagpath = source + tag[0][len(dest):]                            tag[:2] = [tagpath, sourcerev]                            break                    else:                        pendings.append([source, sourcerev, dest])                 # Filter out tags with children coming from different                # parts of the repository like:                # /tags/tag.1 (from /trunk:10)                # /tags/tag.1/foo (from /branches/foo:12)                # Here/tags/tag.1 discarded as well as its children.                # It happens with tools like cvs2svn. Such tags cannot                # be represented in mercurial.                addeds = dict((p, e.copyfrom_path) for p, e                              in origpaths.iteritems()                              if e.action == 'A' and e.copyfrom_path)                badroots = set()                for destroot in addeds:                    for source, sourcerev, dest in pendings:                        if (not dest.startswith(destroot + '/')                            or source.startswith(addeds[destroot] + '/')):                            continue                        badroots.add(destroot)                        break                 for badroot in badroots:                    pendings = [p for p in pendings if p[2] != badroot                                and not p[2].startswith(badroot + '/')]                 # Tell tag renamings from tag creations                remainings = []                for source, sourcerev, dest in pendings:                    tagname = dest.split('/')[-1]                    if source.startswith(srctagspath):                        remainings.append([source, sourcerev, tagname])                        continue                    if tagname in tags:                        # Keep the latest tag value                        continue                    # From revision may be fake, get one with changes                    try:                        tagid = self.latest(source, sourcerev)                        if tagid and tagname not in tags:                            tags[tagname] = tagid                    except SvnPathNotFound:                        # It happens when we are following directories                        # we assumed were copied with their parents                        # but were really created in the tag                        # directory.                        pass                pendings = remainings                tagspath = srctagspath         except SubversionException:            self.ui.note(_('no tags found at revision %d\n') % start)        return tags 

Lines 1–500 of 1136.