# # # patch "tracmtn/backend.py" # from [d1ea39556f9842df75cb1502b4f227297ae35679] # to [deb1d78ebe64f8d4b6d26b29122a6a6a8813aa42] # # patch "tracmtn/util.py" # from [75f384b8a0ddd209d1c640ddee6e34775302bbe3] # to [1b35d93b0c3afad5acfecb2555da6b14f8b0b628] # ============================================================ --- tracmtn/backend.py d1ea39556f9842df75cb1502b4f227297ae35679 +++ tracmtn/backend.py deb1d78ebe64f8d4b6d26b29122a6a6a8813aa42 @@ -29,21 +29,17 @@ from trac.config import Option, ListOpti from trac.util.datefmt import format_datetime as format_datetime_trac from trac.core import Component, implements, TracError from trac.config import Option, ListOption - -from tracmtn.automate import MTN, AutomateException from tracmtn.util import get_oldpath, get_parent, Memoize -from tracmtn.cache import get_cache - from cStringIO import StringIO from time import strptime +from urllib import quote as url_quote +import httplib +import simplejson +from urlparse import urlparse +from tracmtn.lru_cache import LRUCache import re try: - from threading import Lock -except ImportError: - from dummy_threading import Lock #IGNORE:E0611 - -try: from trac.versioncontrol.web_ui import IPropertyRenderer except ImportError: IPropertyRenderer = None @@ -58,12 +54,12 @@ INTERFACE_VERSIONS = { REVID_RULE = re.compile(r'^[0-9a-f]{40}$') INTERFACE_VERSIONS = { - '2.0': '0.26', '2.1': '0.27', '2.2': '0.28', - '3.0': '0.29', '3.1': '0.30', - '4.0': '0.31-0.33', '4.1': '0.34', '4.3': '0.35', - '5.0': '0.36', - '6.0': '0.37-0.38', - '7.0': '0.39', + (2,0): '0.26', (2,1): '0.27', (2,2): '0.28', + (3,0): '0.29', (3,1): '0.30', + (4,0): '0.31-0.33', (4,1): '0.34', (4,3): '0.35', + (5,0): '0.36', + (6,0): '0.37-0.38', + (7,0): '0.39-0.40', } @@ -71,7 +67,7 @@ class MultipleChangesets(TracError): def __init__(self, rev): TracError.__init__( self, "Multiple revisions found for '%s'" % rev, - title="Ambiguous changeset Number") + title="Ambiguous changeset number") class MonotoneConnector(Component): @@ -81,19 +77,15 @@ class MonotoneConnector(Component): implements(IRepositoryConnector, IWikiSyntaxProvider) # Configuration options - mtn_binary = Option('mtn', 'mtn_binary', default='/usr/bin/mtn', - doc='''Full path to the monotone binary.''') - cachespec = Option('mtn', 'cachespec', default='localmem', - doc='''Select a caching mechanism.''') xtracerts = ListOption('mtn', 'xtracerts', doc='''List of user certs to be displayed.''') + mtnurl = Option('mtn', 'url', + doc='''MTN server url.''') # IRepositoryConnector methods def __init__(self): Component.__init__(self) - self.mtn_procs = {} self.version = None - self.lock = Lock() def get_supported_types(self): """ @@ -110,57 +102,29 @@ class MonotoneConnector(Component): """ yield ("mtn", 0) - def get_revprops(self): - """ - Gets the user-defined configuration options for displaying - non-standard revision certs. - """ - revprops = {} - for cert in self.xtracerts: - section = 'mtn-cert-%s' % cert - revprops[cert] = [ - self.config.get(section, 'name', cert), - self.config.get(section, 'text', '%s'), - self.config.getbool(section, 'wikiflag', True), - self.config.get(section, 'htmlclass', None) or None - # FIXME: Config bug? Returns '' if unset instead of None. - ] - return revprops - def get_repository(self, type, path, authname): """ Return a Repository instance for the given repository type and dir. """ - self.lock.acquire() - try: - # note: we don't use type or authname, therefore we can always - # return the same Repository object for the same database path + repos = MonotoneRepository(self.mtnurl, self.log, self._get_revprops()) + + # this is the main entry point for users of this plugin, so let's set + # version information here + if not self.version: + major, minor = repos.get_interface_version() + binary = INTERFACE_VERSIONS.get((major, minor), None) + self.version = "interface: %d.%d" % (major, minor) + if binary: + self.version += ", binary: %s (guessed)" % binary try: - mtn = self.mtn_procs[path] - except KeyError: - mtn = MTN(path, self.log, self.mtn_binary) - self.mtn_procs[path] = mtn - repos = MonotoneRepository( - mtn, path, self.log, self.cachespec, self.get_revprops()) + self.env.systeminfo.append(('Monotone', self.version)) + except AttributeError: + # no systeminfo in 0.10 + pass - # this is the main entry point for users of this plugin, so let's set - # version information here - if not self.version: - interface = repos.get_interface_version() - binary = INTERFACE_VERSIONS.get(interface, None) - self.version = "interface: %s" % interface - if binary: - self.version += ", binary: %s (guessed)" % binary - try: - self.env.systeminfo.append(('Monotone', self.version)) - except AttributeError: - pass # no systeminfo in 0.10 + return repos - return repos - finally: - self.lock.release() - # IWikiSyntaxProvider methods def get_wiki_syntax(self): """ @@ -182,6 +146,23 @@ class MonotoneConnector(Component): yield('revtag', self._format_link) # Internal methods + def _get_revprops(self): + """ + Gets the user-defined configuration options for displaying + non-standard revision certs. + """ + revprops = {} + for cert in self.xtracerts: + section = 'mtn-cert-%s' % cert + revprops[cert] = [ + self.config.get(section, 'name', cert), + self.config.get(section, 'text', '%s'), + self.config.getbool(section, 'wikiflag', True), + self.config.get(section, 'htmlclass', None) or None + # FIXME: Config bug? Returns '' if unset instead of None. + ] + return revprops + def _format_link(self, formatter, ns, rev, label): """ Format a changeset link. @@ -238,8 +219,8 @@ if utc: """ Convert a monotone date string into a datetime object. """ - return datetime(tzinfo=utc, - *strptime(raw, "%Y-%m-%dT%H:%M:%S")[:6]) + return datetime( + tzinfo=utc, *strptime(raw, "%Y-%m-%dT%H:%M:%S")[:6]) def format_datetime(t): """ @@ -277,44 +258,80 @@ def dates(certvals): return result -class CachedMTN(object): +# for now. make that configurable later. +certcache = LRUCache(1000) +manifestcache = LRUCache(10) - def __init__(self, mtn, cachespec): - self.mtn = mtn - self.cachespec = cachespec - self.parents = Memoize(self.mtn.parents, self._get_cache) - self.manifest = Memoize(self.mtn.manifest, self._get_cache) - self.certs = Memoize(self.mtn.certs, self._get_cache) - self.file_length = Memoize(self.mtn.file_length, self._get_cache) - self.changesets = Memoize(self.mtn.changesets, self._get_cache) +class MonotoneConnection(object): + + def __init__(self, url): + o = urlparse(url, 'http') + self.conn = httplib.HTTPConnection(o.hostname, o.port) + self.path = o.path - def _get_cache(self, realm): - return get_cache(realm, self.cachespec) + def close(self): + self.conn.close() - def close(self): - self.parents.close() - self.manifest.close() - self.certs.close() - self.file_length.close() - self.changesets.close() + def get_certs(self, rev): + try: + certs = certcache[rev] + except KeyError: + certs = self.get('certs', rev) + if not certs: + raise NoSuchChangeset(rev) + certcache[rev] = certs + return certs - def __getattr__(self, attr): - return getattr(self.mtn, attr) + def get_raw(self, *args): + url = self.path + '/'.join( + [url_quote(arg.encode('utf-8'), safe='') for arg in args]) + self.conn.request('GET', url) + r = self.conn.getresponse() + if r.status == 200: # fixme: named constant + return r.read() + else: + # flush. is there a better way to do that? + r.read() + def get(self, *args): + r = self.get_raw(*args) + if r is not None: + return simplejson.loads(r) + #return cjson.decode(r) + + +class MonotoneManifest(object): + + def __init__(self, repos, rev): + # rev is already normalized + self.repos = repos + self.rev = rev + self.manifest = repos.conn.get('manifest', rev) + self._node_cache = LRUCache(100) + + def get_node(self, path): + # path is already normalized + try: + node = self._node_cache[path] + except KeyError: + node = MonotoneNode(self.repos, self.rev, path, self.manifest) + self._node_cache[path] = node + return node + + class MonotoneRepository(Repository): """ Represents a Monotone repository. """ - def __init__(self, mtn, path, log, cachespec, revpropspec = None): + def __init__(self, path, log, options): Repository.__init__(self, 'mtn:%s' % path, None, log) - self.mtn = CachedMTN(mtn, cachespec) - self.revpropspec = revpropspec or {} + self.conn = MonotoneConnection(path) def close(self): """Close the connection to the repository.""" - self.mtn.close() + self.conn.close() def get_changeset(self, rev): """ @@ -328,16 +345,13 @@ class MonotoneRepository(Repository): """ Like get_changeset, but skips the revision normalization. """ - try: - return MonotoneChangeset(self.mtn, rev, self.revpropspec) - except AutomateException: - raise NoSuchChangeset(rev) + return MonotoneChangeset(self, rev, {}) # self.revpropspec) def get_changesets(self, start, stop): """ Generate Changesets belonging to the given time period (start, stop). """ - for rev in self.mtn.select('l:%s/e:%s' % + for rev in self.conn.get('revisions', 'select', 'l:%s/e:%s' % (format_datetime(start), format_datetime(stop))): yield self._get_changeset(rev) @@ -351,15 +365,21 @@ class MonotoneRepository(Repository): # Note: in an mtn repository, there might be many file # hierarchies, so it makes no sense to ask for the latest # version of a path. + path, rev = self.normalize_path(path), self.normalize_rev(rev) + return self._get_node(path, rev) - # FIXME: normalize_rev can be skipped when called by ourselves - rev = self.normalize_rev(rev) - path = self.normalize_path(path) + def _get_node(self, path, rev): + """ + Like get_node, but skips the revision and path normalization. + """ try: - return MonotoneNode(self.mtn, rev, path) - except AutomateException: - raise NoSuchChangeset(rev) + manifest = manifestcache[rev] + except KeyError: + manifest = MonotoneManifest(self, rev) + manifestcache[rev] = manifest + return manifest.get_node(path) + def sync(self, feedback=None): """Perform a sync of the repository cache, if relevant. @@ -380,7 +400,7 @@ class MonotoneRepository(Repository): Here: Return the oldest root. """ roots = dict([(self.get_dates(rev)[0], rev) - for rev in self.mtn.roots()]) + for rev in self.conn.get('revisions', 'roots')]) dates = roots.keys() dates.sort() return roots[dates[0]] @@ -391,7 +411,7 @@ class MonotoneRepository(Repository): Here: Return the youngest leave. """ leaves = dict([(self.get_dates(rev)[-1], rev) - for rev in self.mtn.leaves()]) + for rev in self.conn.get('revisions', 'leaves')]) dates = leaves.keys() dates.sort() return leaves[dates[-1]] @@ -403,7 +423,7 @@ class MonotoneRepository(Repository): Return the revision immediately preceding the specified revision. """ # note: returning only one parent - parents = self.mtn.parents(rev) + parents = self.conn.get('revisions', 'parents', rev) parents.sort() return parents and parents[0] or None @@ -413,7 +433,7 @@ class MonotoneRepository(Repository): """ # note: ignoring path for now # note: returning only one child - children = self.mtn.children(rev) + children = self.conn.get('revisions', 'children', rev) children.sort() return children and children[0] or None @@ -438,7 +458,7 @@ class MonotoneRepository(Repository): Return a canonical representation of path in the repos. We strip trailing slashes except for the root. """ - return '/' + (path and path.strip('/') or '') + return path and path.strip('/') or '' def normalize_rev(self, rev): """Return a canonical representation of a revision. @@ -457,7 +477,7 @@ class MonotoneRepository(Repository): if not REVID_RULE.match(rev): # doesn't look like a hash, pass to mtn's select - revs = self.mtn.select(rev) + revs = self.conn.get('revisions', 'select', rev) if len(revs) < 1: raise NoSuchChangeset(rev) elif len(revs) > 1: @@ -490,7 +510,10 @@ class MonotoneRepository(Repository): might be needed in order to retrieve the tags, but in general it's best to produce all known tags. """ - return self.mtn.tags() + tags = self.conn.get('tags') + for tag in tags.keys(): + for entry in tags[tag]: + yield (tag, entry['revision']) def get_branches(self, rev): """ @@ -498,7 +521,7 @@ class MonotoneRepository(Repository): `rev` might be needed in order to retrieve the branches, but in general it's best to produce all known branches. """ - return self.mtn.non_merged_branches() + return [] # fixme def get_quickjump_entries(self, from_rev): """ @@ -525,13 +548,14 @@ class MonotoneRepository(Repository): Parse the date certs and return a sorted list of trac compatible dates for rev. """ - return dates(self.mtn.certs(rev).get('date', [])) + date_certs = self.conn.get_certs(rev).get('date', []) + return dates(cert['value'] for cert in date_certs) def get_interface_version(self): """ Returns the automation interface version string. """ - return self.mtn.get_interface_version() + return self.conn.get("version") class MonotoneNode(Node): @@ -540,22 +564,24 @@ class MonotoneNode(Node): revision. """ - def __init__(self, mtn, rev, path, manifest = None): - self.mtn = mtn - self.manifest = manifest or self.mtn.manifest(rev) + def __init__(self, repos, rev, path, manifest): + self.repos = repos + self.manifest = manifest - if not path in self.manifest: + if path in self.manifest['dirs']: + kind = Node.DIRECTORY + elif path in self.manifest['files']: + kind = Node.FILE + self.content_id = self.manifest['files'][path] + else: raise NoSuchNode(path, rev) - self.content_id = self.manifest[path][1] self.created_path = path self.created_rev = rev - kind = self.manifest[path][0] # 'file' or 'dir' - if kind == Node.FILE: # FIXME: we can't handle multiple marks - rev = self.mtn.content_changed(rev, path)[0] + #rev = self.mtn.content_changed(rev, path)[0] # trac bug, or at least problematic behavior: in the # browser window, Node.path is used for the link behind @@ -564,6 +590,7 @@ class MonotoneNode(Node): #marked = self.mtn.roster(rev)[1][curr.ident] #path = marked.name + pass Node.__init__(self, path, rev, kind) @@ -575,7 +602,7 @@ class MonotoneNode(Node): """ if self.isdir: return None - return StringIO(self.mtn.get_file(self.content_id)) + return StringIO(self.repos.conn.get_raw('file', self.content_id)) def get_entries(self): """ @@ -592,9 +619,12 @@ class MonotoneNode(Node): """ return get_parent(path) == self.path - for path in filter(ischild, self.manifest.keys()): # IGNORE:W0141 - yield MonotoneNode(self.mtn, self.rev, path, self.manifest) + for dir in filter(ischild, self.manifest['dirs']): + yield self.repos._get_node(dir, self.created_rev) + for file in filter(ischild, self.manifest['files'].keys()): + yield self.repos._get_node(file, self.created_rev) + def get_history(self, limit=None): """ Generator that yields (path, rev, chg) tuples, one for each @@ -613,12 +643,14 @@ class MonotoneNode(Node): the node. The set of properties depends on the version control system. """ - return self.manifest[self.path][2] + # return self.manifest[self.path][2] + return {} # fixme def get_content_length(self): if self.isdir: return None - return self.mtn.file_length(self.content_id) + #return self.mtn.file_length(self.content_id) + return 42 # fixme def get_content_type(self): if self.isdir: @@ -627,7 +659,8 @@ class MonotoneNode(Node): def get_last_modified(self): # fixme: might be to pessimistic - return dates(self.mtn.certs(self.rev).get('date', []))[-1] + #return dates(self.mtn.certs(self.rev).get('date', []))[-1] + return 0 # fixme class MonotoneChangeset(Changeset): @@ -635,18 +668,23 @@ class MonotoneChangeset(Changeset): Represents the set of changes in one revision. """ # changesets are retrieved via MonotoneRepository.get_changeset() - def __init__(self, mtn, rev, revpropspec = None): - self.certs = mtn.certs(rev) - self.messages = self.certs.get('changelog', ['-']) - self.authors = self.certs.get('author', ['-']) - self.dates = dates(self.certs.get('date', [])) - self.branches = self.certs.get('branch', []) - self.tags = self.certs.get('tag', []) + def __init__(self, repos, rev, revpropspec = None): + self.certs = repos.conn.get_certs(rev) + + def get_cert_vals(name): + return [cert['value'] for cert in self.certs.get(name, [])] + + self.messages = get_cert_vals('changelog') + self.authors = get_cert_vals('author') + self.dates = dates(get_cert_vals('date')) + self.branches = get_cert_vals('branch') + self.tags = get_cert_vals('tag') + # multiple dates not supported, so pick the first date date = self.dates[0] - # Trac doesn't support multiple authors + # multiple authors not supported author = ', '.join(self.authors) # concatenate the commit messages @@ -654,7 +692,7 @@ class MonotoneChangeset(Changeset): Changeset.__init__(self, rev, message, author, date) - self.mtn = mtn + self.repos = repos self.revpropspec = revpropspec or {} def get_changes(self): @@ -673,33 +711,37 @@ class MonotoneChangeset(Changeset): """ # We do not closely implement that api, for example, we don't # know the kind of a deleted or renamed node. - for changeset in self.mtn.changesets(self.rev): - oldrev = changeset.oldrev + for oldrev, cs in self.repos.conn.get('revision', self.rev).iteritems(): + # deletions - for oldpath in changeset.deleted: + for oldpath in cs.get('deleted', []): yield oldpath, None, Changeset.DELETE, oldpath, oldrev # pure renames - for (path, oldpath) in changeset.renamed.iteritems(): + for (path, oldpath) in cs.get('renamed', {}).iteritems(): yield path, None, Changeset.MOVE, oldpath, oldrev + + # additions + for path in cs.get('dirs_added', []): + yield path, Node.DIRECTORY, Changeset.ADD, None, -1 - # additions - for (path, kind) in changeset.added.iteritems(): - yield path, kind, Changeset.ADD, None, -1 + for path in cs.get('files_added', {}).iterkeys(): + yield path, Node.FILE, Changeset.ADD, None, -1 # patches - for path in changeset.patched: - oldpath = get_oldpath(path, changeset.renamed) + for path in cs.get('patched', {}).iterkeys(): + oldpath = get_oldpath(path, cs.get('renamed', {})) yield path, Node.FILE, Changeset.EDIT, oldpath, oldrev + if IPropertyRenderer: def get_properties(self): properties = {} - parents = self.mtn.parents(self.rev) + parents = self.repos.conn.get('revisions', 'parents', self.rev) if parents: properties['Parents'] = parents - children = self.mtn.children(self.rev) + children = self.repos.conn.get('revisions', 'children', self.rev) if children: properties['Children'] = children if self.branches: ============================================================ --- tracmtn/util.py 75f384b8a0ddd209d1c640ddee6e34775302bbe3 +++ tracmtn/util.py 1b35d93b0c3afad5acfecb2555da6b14f8b0b628 @@ -35,12 +35,16 @@ def get_parent(path): def get_parent(path): """Returns the name of the directory containing path, or None if - there is none (because path is '/' or None).""" + there is none (because path is '' or None).""" + if not path: + return None + # path.strip('/') # shouldn't be necessary + try: + return path[:path.rindex('/')] + except ValueError: + return "" - path = path and path.rstrip('/') - return path and (path[0:path.rfind('/')] or '/') or None - def get_oldpath(path, renames): """Find out the old name of a path taking into account the renames, which is a dictionary: oldname = renames[newname]."""